Condividi tramite


Puntatori non elaborati (C++)

Un puntatore è un tipo di variabile. Archivia l'indirizzo di un oggetto in memoria e viene usato per accedere a tale oggetto. Un puntatore non elaborato è un puntatore la cui durata non è controllata da un oggetto incapsulamento, ad esempio un puntatore intelligente. Un puntatore non elaborato può essere assegnato all'indirizzo di un'altra variabile non puntatore oppure può essere assegnato un valore di nullptr. Un puntatore a cui non è stato assegnato un valore contiene dati casuali.

Un puntatore può anche essere dereferenziato per recuperare il valore dell'oggetto a cui punta. L'operatore di accesso ai membri fornisce l'accesso ai membri di un oggetto.

    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 puntatore può puntare a un oggetto tipizzato o a void. Quando un programma alloca un oggetto nell'heap in memoria, riceve l'indirizzo di tale oggetto sotto forma di puntatore. Tali puntatori sono detti puntatori proprietari. Un puntatore proprietario (o una copia di esso) deve essere usato per liberare in modo esplicito l'oggetto allocato dall'heap quando non è più necessario. Se non si libera la memoria, si verifica una perdita di memoria e viene eseguito il rendering della posizione di memoria non disponibile per qualsiasi altro programma nel computer. La memoria allocata tramite new deve essere liberata tramite delete (o delete[]). Per altre informazioni, vedere new Operatori edelete .

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

Un puntatore (se non è dichiarato come const) può essere incrementato o decrementato in modo che punti a un'altra posizione in memoria. Questa operazione è denominata aritmetica del puntatore. Viene usato nella programmazione in stile C per scorrere gli elementi in matrici o altre strutture di dati. Un const puntatore non può essere fatto per puntare a una posizione di memoria diversa e in questo senso è simile a un riferimento. Per altre informazioni, vedere const e volatile puntatori.

    // 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.

Nei sistemi operativi a 64 bit, un puntatore ha una dimensione di 64 bit. Le dimensioni del puntatore di un sistema determinano la quantità di memoria indirizzabile che può avere. Tutte le copie di un puntatore puntano alla stessa posizione di memoria. I puntatori (insieme ai riferimenti) vengono usati ampiamente in C++ per passare oggetti più grandi da e verso le funzioni. Spesso è più efficiente copiare l'indirizzo di un oggetto rispetto a copiare l'intero oggetto. Quando si definisce una funzione, specificare i parametri del puntatore come const a meno che non si intenda modificare l'oggetto. In generale, const i riferimenti sono il modo preferito per passare oggetti alle funzioni, a meno che il valore dell'oggetto non possa essere nullptr.

I puntatori alle funzioni consentono di passare le funzioni ad altre funzioni. Vengono usati per i "callback" nella programmazione in stile C. C++ moderno usa espressioni lambda a questo scopo.

Inizializzazione e accesso ai membri

Nell'esempio seguente viene illustrato come dichiarare, inizializzare e usare un puntatore non elaborato. Viene inizializzato usando new per puntare un oggetto allocato nell'heap, che è necessario in modo esplicito delete. L'esempio mostra anche alcuni dei pericoli associati ai puntatori non elaborati. Tenere presente che questo esempio è la programmazione in stile C e non C++!

#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
}

Aritmetica e matrici del puntatore

I puntatori e le matrici sono strettamente correlati. Quando una matrice viene passata per valore a una funzione, viene passata come puntatore al primo elemento. L'esempio seguente illustra le proprietà importanti seguenti di puntatori e matrici:

  • L'operatore sizeof restituisce le dimensioni totali in byte di una matrice
  • Per determinare il numero di elementi, dividere i byte totali in base alle dimensioni di un elemento
  • Quando una matrice viene passata a una funzione, viene decaduta a un tipo di puntatore
  • Quando l'operatore sizeof viene applicato a un puntatore, restituisce le dimensioni del puntatore, ad esempio 4 byte su x86 o 8 byte su 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);
}

Alcune operazioni aritmetiche possono essere usate suconst puntatori non per fare in modo che puntino a un'altra posizione di memoria. I puntatori vengono incrementati e decrementati usando gli ++operatori , +=-= e -- . Questa tecnica può essere usata nelle matrici ed è particolarmente utile nei buffer di dati non tipizzato. Un void* oggetto viene incrementato in base alle dimensioni di un oggetto char (1 byte). Un puntatore tipizzato viene incrementato in base alle dimensioni del tipo a cui punta.

Nell'esempio seguente viene illustrato come usare l'aritmetica del puntatore per accedere a singoli pixel in una bitmap in Windows. Si noti l'uso di new e deletee l'operatore di dereferenziazione.

#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();
}

void* Puntatori

Un puntatore a void punta semplicemente a una posizione di memoria non elaborata. A volte è necessario usare void* puntatori, ad esempio quando si passa tra il codice C++ e le funzioni C.

Quando viene eseguito il cast di un puntatore tipizzato a un void puntatore, il contenuto della posizione di memoria rimane invariato. Tuttavia, le informazioni sul tipo vengono perse, in modo che non sia possibile eseguire operazioni di incremento o decremento. È possibile eseguire il cast di una posizione di memoria, ad esempio da MyClass* a void* e di nuovo a MyClass*. Tali operazioni sono intrinsecamente soggette a errori e richiedono grande attenzione per evitare errori. C++ moderno sconsiglia l'uso di void puntatori in quasi tutte le circostanze.

//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);
}

Puntatori alle funzioni

Nella programmazione in stile C, i puntatori a funzione vengono usati principalmente per passare funzioni ad altre funzioni. Questa tecnica consente al chiamante di personalizzare il comportamento di una funzione senza modificarla. Nelle moderne espressioni lambda C++, le espressioni lambda offrono la stessa funzionalità con maggiore sicurezza dei tipi e altri vantaggi.

Una dichiarazione del puntatore di funzione specifica la firma che la funzione a cui punta deve avere:

// 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);

Nell'esempio seguente viene illustrata una funzione combine che accetta come parametro qualsiasi funzione che accetta un std::string oggetto e restituisce un oggetto std::string. A seconda della funzione passata a combine, antepone o aggiunge una stringa.

#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";
}

Vedi anche

Operatore indiretto puntatoriintelligenti: *
Address-of Operator: &
Welcome back to C++