Compartir a través de


Punteros básicos (C++)

Un puntero es un tipo de variable. Almacena la dirección de un objeto en memoria y se usa para acceder a ese objeto. Un puntero básico es un puntero cuya duración no está controlada por un objeto encapsulado, como un puntero inteligente. Se puede asignar un puntero básico a la dirección de otra variable que no sea de puntero, o bien se le puede asignar un valor de nullptr. Un puntero al que no se ha asignado un valor contiene datos aleatorios.

También se puede desreferenciar un puntero para recuperar el valor del objeto al que apunta. El operador de acceso a miembros proporciona acceso a los miembros de un objeto.

    int* p = nullptr; // declare pointer and initialize it
                      // so that it doesn't store a random address
    int i = 5;
    p = &i; // assign pointer to address of object
    int j = *p; // dereference p to retrieve the value at its address

Un puntero puede apuntar a un objeto con tipo o a void. Cuando un programa asigna un objeto en el montón en memoria, recibe la dirección de ese objeto en forma de puntero. Estos punteros se denominan punteros propietarios. Se debe usar un puntero propietario (o una copia de él) para liberar explícitamente el objeto asignado al montón cuando ya no sea necesario. Si no se libera la memoria, se produce una fuga de memoria y se representa esa ubicación de memoria como no disponible para cualquier otro programa de la máquina. La memoria asignada mediante new debe liberarse mediante delete (o delete[]). Para más información, consulte los operadores new y delete.

    MyClass* mc = new MyClass(); // allocate object on the heap
    mc->print(); // access class member
    delete mc; // delete object (please don't forget!)

Un puntero (si no se declara como const) se puede incrementar o reducir para que apunte a otra ubicación de la memoria. Esta operación se denomina aritmética de puntero. Se usa en la programación de estilo C para iterar en elementos de matrices u otras estructuras de datos. No se puede hacer que un puntero const apunte a una ubicación de memoria diferente y, en ese sentido, se parece a una referencia. Para más información, consulte los artículos sobre los punteros const y volatile.

    // declare a C-style string. Compiler adds terminating '\0'.
    const char* str = "Hello world";

    const int c = 1;
    const int* pconst = &c; // declare a non-const pointer to const int
    const int c2 = 2;
    pconst = &c2;  // OK pconst itself isn't const
    const int* const pconst2 = &c;
    // pconst2 = &c2; // Error! pconst2 is const.

En sistemas operativos de 64 bits, un puntero tiene un tamaño de 64 bits. El tamaño del puntero de un sistema determina la cantidad de memoria direccionable que puede tener. Todas las copias de un puntero apuntan a la misma ubicación de memoria. Los punteros (junto con las referencias) se usan ampliamente en C++ para pasar objetos más grandes hacia las funciones y desde estas. A menudo es más eficaz copiar la dirección de un objeto que copiar todo el objeto. Al definir una función, especifique los parámetros de puntero como const a menos que quiera que la función modifique el objeto. En general, las referencias a const son la manera preferida de pasar objetos a funciones a menos que el valor del objeto pueda ser nullptr.

Los punteros a funciones permiten pasar funciones a otras funciones. Se usan para "devoluciones de llamada" en la programación de estilo C. La programación actual en C++ usa expresiones lambda con este fin.

Inicialización y acceso de miembros

En el ejemplo siguiente se muestra cómo declarar, inicializar y usar un puntero básico. Se inicializa mediante new para que apunte a un objeto asignado en el montón, el cual debe delete de manera explícita. En el ejemplo también se muestran algunos de los peligros asociados a punteros básicos. (Recuerde que este ejemplo es programación de estilo C y no C++ moderno).

#include <iostream>
#include <string>

class MyClass
{
public:
    int num;
    std::string name;
    void print() { std::cout << name << ":" << num << std::endl; }
};

// Accepts a MyClass pointer
void func_A(MyClass* mc)
{
    // Modify the object that mc points to.
    // All copies of the pointer will point to
    // the same modified object.
    mc->num = 3;
}

// Accepts a MyClass object
void func_B(MyClass mc)
{
    // mc here is a regular object, not a pointer.
    // Use the "." operator to access members.
    // This statement modifies only the local copy of mc.
    mc.num = 21;
    std::cout << "Local copy of mc:";
    mc.print(); // "Erika, 21"
}

int main()
{
    // Use the * operator to declare a pointer type
    // Use new to allocate and initialize memory
    MyClass* pmc = new MyClass{ 108, "Nick" };

    // Prints the memory address. Usually not what you want.
    std:: cout << pmc << std::endl;

    // Copy the pointed-to object by dereferencing the pointer
    // to access the contents of the memory location.
    // mc is a separate object, allocated here on the stack
    MyClass mc = *pmc;

    // Declare a pointer that points to mc using the addressof operator
    MyClass* pcopy = &mc;

    // Use the -> operator to access the object's public members
    pmc->print(); // "Nick, 108"

    // Copy the pointer. Now pmc and pmc2 point to same object!
    MyClass* pmc2 = pmc;

    // Use copied pointer to modify the original object
    pmc2->name = "Erika";
    pmc->print(); // "Erika, 108"
    pmc2->print(); // "Erika, 108"

    // Pass the pointer to a function.
    func_A(pmc);
    pmc->print(); // "Erika, 3"
    pmc2->print(); // "Erika, 3"

    // Dereference the pointer and pass a copy
    // of the pointed-to object to a function
    func_B(*pmc);
    pmc->print(); // "Erika, 3" (original not modified by function)

    delete(pmc); // don't forget to give memory back to operating system!
   // delete(pmc2); //crash! memory location was already deleted
}

Aritmética de punteros y matrices

Los punteros y matrices están estrechamente relacionados. Cuando una matriz se pasa por valor a una función, se pasa como puntero al primer elemento. En el ejemplo siguiente se muestran las siguientes propiedades importantes de punteros y matrices:

  • El operador sizeof devuelve el tamaño total en bytes de una matriz.
  • Para determinar el número de elementos, divida el total de bytes por el tamaño de un elemento.
  • Cuando se pasa una matriz a una función, se degrada a un tipo de puntero.
  • Cuando el operador sizeof se aplica a un puntero, este devuelve el tamaño del puntero, por ejemplo, 4 bytes en x86 o 8 bytes en x64.
#include <iostream>

void func(int arr[], int length)
{
    // returns pointer size. not useful here.
    size_t test = sizeof(arr);

    for(int i = 0; i < length; ++i)
    {
        std::cout << arr[i] << " ";
    }
}

int main()
{
    int i[5]{ 1,2,3,4,5 };
    // sizeof(i) = total bytes
    int j = sizeof(i) / sizeof(i[0]);
    func(i,j);
}

Ciertas operaciones aritméticas se pueden usar en punteros que no son const para que apunten a otra ubicación de memoria. Los punteros se incrementan y reducen mediante los operadores ++, +=, -= y --. Esta técnica se puede usar en matrices y es especialmente útil en búferes de datos sin tipo. Un objeto void* se incrementa según el tamaño de un char (1 byte). Un puntero con tipo se incrementa según el tamaño del tipo al que apunta.

En el ejemplo siguiente se muestra cómo se puede usar la aritmética de punteros para acceder a píxeles individuales en un mapa de bits en Windows. Anote el uso de new y delete, y el operador de desreferencia.

#include <Windows.h>
#include <fstream>

using namespace std;

int main()
{
    BITMAPINFOHEADER header;
    header.biHeight = 100; // Multiple of 4 for simplicity.
    header.biWidth = 100;
    header.biBitCount = 24;
    header.biPlanes = 1;
    header.biCompression = BI_RGB;
    header.biSize = sizeof(BITMAPINFOHEADER);

    constexpr int bufferSize = 30000;
    unsigned char* buffer = new unsigned char[bufferSize];

    BITMAPFILEHEADER bf;
    bf.bfType = 0x4D42;
    bf.bfSize = header.biSize + 14 + bufferSize;
    bf.bfReserved1 = 0;
    bf.bfReserved2 = 0;
    bf.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); //54

    // Create a gray square with a 2-pixel wide outline.
    unsigned char* begin = &buffer[0];
    unsigned char* end = &buffer[0] + bufferSize;
    unsigned char* p = begin;
    constexpr int pixelWidth = 3;
    constexpr int borderWidth = 2;

    while (p < end)
    {
            // Is top or bottom edge?
        if ((p < begin + header.biWidth * pixelWidth * borderWidth)
            || (p > end - header.biWidth * pixelWidth * borderWidth)
            // Is left or right edge?
            || (p - begin) % (header.biWidth * pixelWidth) < (borderWidth * pixelWidth)
            || (p - begin) % (header.biWidth * pixelWidth) > ((header.biWidth - borderWidth) * pixelWidth))
        {
            *p = 0x0; // Black
        }
        else
        {
            *p = 0xC3; // Gray
        }
        p++; // Increment one byte sizeof(unsigned char).
    }

    ofstream wf(R"(box.bmp)", ios::out | ios::binary);

    wf.write(reinterpret_cast<char*>(&bf), sizeof(bf));
    wf.write(reinterpret_cast<char*>(&header), sizeof(header));
    wf.write(reinterpret_cast<char*>(begin), bufferSize);

    delete[] buffer; // Return memory to the OS.
    wf.close();
}

Punteros void*

Un puntero a void simplemente apunta a una ubicación de memoria sin procesar. A veces es necesario usar punteros void*, por ejemplo, al pasar entre el código de C++ y las funciones de C.

Cuando se convierte un puntero con tipo en un puntero void, el contenido de la ubicación de memoria no cambia. Sin embargo, se pierde la información de tipo, por lo que no se pueden realizar operaciones de incremento ni de reducción. Una ubicación de memoria se puede convertir, por ejemplo, de MyClass* a void* y de nuevo a MyClass*. Estas operaciones son inherentemente propensas a errores y requieren gran cuidado para evitarlos. La programación en C++ moderna desaconseja el uso de punteros void en casi todas las circunstancias.

//func.c
void func(void* data, int length)
{
    char* c = (char*)(data);

    // fill in the buffer with data
    for (int i = 0; i < length; ++i)
    {
        *c = 0x41;
        ++c;
    }
}

// main.cpp
#include <iostream>

extern "C"
{
    void func(void* data, int length);
}

class MyClass
{
public:
    int num;
    std::string name;
    void print() { std::cout << name << ":" << num << std::endl; }
};

int main()
{
    MyClass* mc = new MyClass{10, "Marian"};
    void* p = static_cast<void*>(mc);
    MyClass* mc2 = static_cast<MyClass*>(p);
    std::cout << mc2->name << std::endl; // "Marian"
    delete(mc);

    // use operator new to allocate untyped memory block
    void* pvoid = operator new(1000);
    char* pchar = static_cast<char*>(pvoid);
    for(char* c = pchar; c < pchar + 1000; ++c)
    {
        *c = 0x00;
    }
    func(pvoid, 1000);
    char ch = static_cast<char*>(pvoid)[0];
    std::cout << ch << std::endl; // 'A'
    operator delete(pvoid);
}

Punteros a funciones

En la programación de estilo C, los punteros de función se usan principalmente para pasar funciones a otras funciones. Esta técnica permite al autor de la llamada personalizar el comportamiento de una función sin modificarla. En C++ actual, las expresiones lambda proporcionan la misma funcionalidad con mayor seguridad de tipos y otras ventajas.

Una declaración de puntero de función especifica la firma que debe tener la función a la que apunta:

// Declare pointer to any function that...

// ...accepts a string and returns a string
string (*g)(string a);

// has no return value and no parameters
void (*x)();

// ...returns an int and takes three parameters
// of the specified types
int (*i)(int i, string s, double d);

En el ejemplo siguiente se muestra una función combine que toma como parámetro cualquier función que acepte un std::string y devuelva un std::string. Según la función que se pase a combine, se antepone o se anexa una cadena.

#include <iostream>
#include <string>

using namespace std;

string base {"hello world"};

string append(string s)
{
    return base.append(" ").append(s);
}

string prepend(string s)
{
    return s.append(" ").append(base);
}

string combine(string s, string(*g)(string a))
{
    return (*g)(s);
}

int main()
{
    cout << combine("from MSVC", append) << "\n";
    cout << combine("Good morning and", prepend) << "\n";
}

Consulte también

Punteros inteligentesOperador de direccionamiento indirecto: *
Operador Address-of: &
Aquí está otra vez C++