Partager via


Pointeurs bruts (C++)

Un pointeur est un type de variable. Elle stocke l’adresse d’un objet en mémoire et est utilisée 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 être affecté à l’adresse d’une autre variable non pointeur, ou elle peut être affectée à une valeur de nullptr. Un pointeur qui n’a pas été affecté à une valeur contient des données aléatoires.

Un pointeur peut également être déréférencement pour récupérer la valeur de l’objet auquel il pointe. L’opérateur d’accès membre fournit l’accès 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 propriétaires. Un pointeur propriétaire (ou une copie de celui-ci) doit être utilisé pour libérer explicitement l’objet alloué au tas lorsqu’il n’est plus nécessaire. L’échec de la libération de la mémoire entraîne une fuite de mémoire et restitue cet emplacement de mémoire indisponible pour tout autre programme sur l’ordinateur. La mémoire allouée à l’aide new de l’utilisation doit être libérée à l’aide delete (ou delete[]). Pour plus d’informations, consultez new et delete opérateurs.

    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 est appelée arithmétique du pointeur. Il est utilisé dans la programmation de style C pour itérer sur des éléments dans des tableaux ou d’autres structures de données. Un const pointeur ne peut pas être fait pour pointer vers un autre emplacement de mémoire, et dans ce sens est similaire à une référence. Pour plus d’informations, consultez const et volatile pointeur.

    // 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’elle peut avoir. Toutes les copies d’un pointeur pointent vers le même emplacement de mémoire. Les pointeurs (ainsi que les références) sont 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 des paramètres de pointeur comme const sauf si vous avez l’intention de modifier l’objet. En général, const les références sont le moyen préféré de passer des objets à des fonctions, sauf si la valeur de l’objet peut être nullptr.

Les pointeurs vers les fonctions permettent aux fonctions d’être passées à d’autres fonctions. Ils sont utilisés pour les « rappels » dans la programmation de style C. C++ moderne utilise des expressions lambda à cet effet.

Initialisation et accès aux membres

L’exemple suivant montre comment déclarer, initialiser et utiliser un pointeur brut. Il est initialisé à l’aide new du point d’un objet alloué sur le tas, que vous devez explicitement delete. L’exemple montre également quelques-uns des dangers associés aux pointeurs bruts. (N’oubliez pas que cet exemple est la programmation de style C et non 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
}

Tableaux et arithmétiques du pointeur

Les pointeurs et les tableaux sont étroitement liés. Lorsqu’un tableau est passé par valeur à une fonction, il est passé en tant que pointeur vers le premier élément. L’exemple suivant illustre les propriétés importantes suivantes des pointeurs et des tableaux :

  • L’opérateur sizeof retourne la taille totale en octets d’un tableau
  • Pour déterminer le nombre d’éléments, divisez le nombre total d’octets par la taille d’un élément
  • Lorsqu’un tableau est passé à une fonction, il se décompose en un type de pointeur
  • Lorsque l’opérateur sizeof est appliqué à un pointeur, il retourne 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 des non-pointeursconst pour les faire pointer vers un autre emplacement de mémoire. Les pointeurs sont incrémentés et décrémentés à l’aide des opérateurs et +=-=-- des ++opérateurs. Cette technique peut être utilisée dans des tableaux et est particulièrement utile dans les mémoires tampons des données non typées. Un void* obtient incrémenté par la taille d’un char (1 octet). Un pointeur typé est incrémenté par taille du type vers lequel il pointe.

L’exemple suivant montre comment l’arithmétique du pointeur peut être utilisé pour accéder à des pixels individuels dans une bitmap sur Windows. Notez l’utilisation et newdeletel’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();
}

void* Pointeurs

Un pointeur vers un void simple pointeur pointe vers un emplacement de mémoire brute. Parfois, il est nécessaire d’utiliser void* des pointeurs, par exemple lors de la transmission entre le code C++ et les fonctions C.

Lorsqu’un pointeur typé est converti en pointeur void , le contenu de l’emplacement de mémoire n’est pas modifié. Toutefois, les informations de type sont perdues, afin que vous ne puissiez pas effectuer d’opérations d’incrémentation ou de décrémentation. Un emplacement de mémoire peut être casté, par exemple, de MyClass* vers void* et de retour vers MyClass*. Ces opérations sont intrinsèquement sujettes à des erreurs et nécessitent un grand soin d’unevoid erreur. C++ moderne décourage l’utilisation de void pointeurs 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

Dans la programmation de style C, les pointeurs de fonction sont utilisés principalement pour passer des fonctions à d’autres fonctions. Cette technique permet à l’appelant de personnaliser le comportement d’une fonction sans la modifier. Dans C++moderne, les expressions lambda offrent la même fonctionnalité avec une plus grande sécurité de type 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 retourne un std::string. En fonction de la fonction passée à combine, elle s’ajoute ou ajoute une chaîne.

#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

Opérateur indirection de pointeursintelligents : *
Adresse de l’opérateur : &
Bienvenue dans C++