Partager via


Pointeurs bruts (C++)

Un pointeur est un type de variable. Il stocke l’adresse d’un objet en mémoire et est utilisé pour accéder à cet objet. Un pointeur brut est un pointeur dont la durée de vie n’est pas contrôlée par un objet encapsulant, tel qu’un pointeur intelligent. Un pointeur brut peut recevoir l’adresse d’une autre variable non pointeur, ou il peut se voir attribuer une valeur de nullptr. Un pointeur qui n’a pas été initialisé contient des données aléatoires.

Un pointeur peut également être déréférencé pour récupérer la valeur de l’objet auquel il fait référence. L’opérateur d’accès aux membres permet d’accéder aux membres d’un objet.

    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 pointeur peut pointer vers un objet typé ou vers void. Lorsqu’un programme alloue un objet sur le tas en mémoire, il reçoit l’adresse de cet objet sous la forme d’un pointeur. Ces pointeurs sont appelés pointeurs possédants. Un pointeur possédant (ou une copie de celui-ci) doit être utilisé pour libérer explicitement l’objet alloué sur le tas lorsqu’il n’est plus nécessaire. Ne pas libérer la mémoire entraîne une fuite de mémoire et rend cet emplacement de mémoire indisponible pour tout autre programme sur la machine. La mémoire allouée en utilisant new doit être libérée en utilisant delete (ou delete[]). Pour plus d’informations, veuillez consulter la section opérateurs new et delete.

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

Un pointeur (s’il n’est pas déclaré comme const) peut être incrémenté ou décrémenté pour pointer vers un autre emplacement en mémoire. Cette opération s’appelle arithmétique des pointeurs. Elle est utilisée en programmation de style C pour itérer sur les éléments dans des tableaux ou d’autres structures de données. Un pointeur const ne peut pas être réaffecté à un autre emplacement mémoire, et en ce sens, il est similaire à une référence. Pour plus d’informations, veuillez consulter la section pointeurs const et 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.

Sur les systèmes d’exploitation 64 bits, un pointeur a une taille de 64 bits. La taille du pointeur d’un système détermine la quantité de mémoire adressable qu’il peut avoir. Toutes les copies d’un pointeur pointent vers le même emplacement mémoire. Les pointeurs (ainsi que les références) sont largement utilisés en C++ pour passer des objets plus volumineux vers et depuis des fonctions. Il est souvent plus efficace de copier l’adresse d’un objet que de copier l’objet entier. Lors de la définition d’une fonction, spécifiez les paramètres de pointeur comme const à moins que vous n’ayez l’intention que la fonction modifie l’objet. En général, les références const sont le moyen privilégié pour passer des objets à des fonctions, sauf si la valeur de l’objet peut éventuellement être nullptr.

Les pointeurs vers des fonctions permettent de passer des fonctions à d’autres fonctions. Ils sont utilisés pour les « callbacks » dans la programmation de style C. Le C++ moderne utilise des expressions lambda à cette fin.

Initialisation et accès aux membres

L’exemple suivant montre comment déclarer, initialiser et utiliser un pointeur brut. Il est initialisé en utilisant new pour pointer un objet alloué sur le tas, que vous devez explicitement delete. L’exemple montre également quelques-uns des dangers associés aux pointeurs bruts. (Souvenez-vous, cet exemple correspond à une programmation de style C et non au C++ moderne !)

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

Arithmétique des pointeurs et tableaux

Les pointeurs et les tableaux sont étroitement liés. Lorsqu’un tableau est passé par valeur à une fonction, il est transmis sous forme de pointeur vers le premier élément. L’exemple suivant démontre les propriétés importantes suivantes des pointeurs et des tableaux :

  • L’ opérateur sizeof renvoie la taille totale en octets d’un tableau
  • Pour déterminer le nombre d’éléments, divisez le total des octets par la taille d’un élément
  • Lorsqu’un tableau est passé à une fonction, il se dégrade en un type de pointeur
  • Lorsque l’ opérateur sizeof est appliqué à un pointeur, il renvoie la taille du pointeur, par exemple, 4 octets sur x86 ou 8 octets sur 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);
}

Certaines opérations arithmétiques peuvent être utilisées sur les pointeurs non-const pour les faire pointer vers un autre emplacement mémoire. Les pointeurs sont incrémentés et décrémentés en utilisant les opérateurs ++, +=, -= et --. Cette technique peut être utilisée dans les tableaux et est particulièrement utile dans les tampons de données non typées. Un void* est incrémenté de la taille d’un char (1 octet). Un pointeur typé est incrémenté de la taille du type auquel il pointe.

L’exemple suivant montre comment l’arithmétique des pointeurs peut être utilisée pour accéder aux pixels individuels dans une image bitmap sous Windows. Notez l’utilisation de new et delete, et de l’opérateur de déréférencement.

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

Pointeurs void*

Un pointeur vers void pointe simplement vers un emplacement mémoire brut. Il est parfois nécessaire d’utiliser des pointeurs void*, par exemple lors du passage entre le code C++ et les fonctions C.

Lorsqu’un pointeur typé est converti en pointeur void, le contenu de l’emplacement mémoire reste inchangé. Cependant, les informations de type sont perdues, de sorte que vous ne pouvez pas effectuer d’opérations d’incrémentation ou de décrémentation. Un emplacement mémoire peut être converti, par exemple, de MyClass* à void* puis de nouveau à MyClass*. De telles opérations sont intrinsèquement sujettes aux erreurs et nécessitent une grande prudence pour éviter les erreurs. Le C++ moderne décourage l’utilisation de pointeurs void dans presque toutes les circonstances.

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

Pointeurs vers des fonctions

En programmation de style C, les pointeurs de fonction sont principalement utilisés pour transmettre des fonctions à d’autres fonctions. Cette technique permet à l’appelant de personnaliser le comportement d’une fonction sans la modifier. En C++ moderne, les expressions lambda offrent la même capacité avec une sécurité de type accrue et d’autres avantages.

Une déclaration de pointeur de fonction spécifie la signature que la fonction pointée doit avoir :

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

L’exemple suivant montre une fonction combine qui prend comme paramètre n’importe quelle fonction qui accepte un std::string et renvoie un std::string. Selon la fonction transmise à combine, elle ajoute une chaîne de caractères au début ou à la fin.

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

Voir aussi

Pointeurs intelligentsOpérateur d’indirection : *
Opérateur d’adresse : &
Bon retour en C++