Les fonctions malloc et free proviennent de la bibliothèque standard C. Elles se contentent d'allouer et de libérer des blocs de mémoire brute sans aucune notion d'objet. En C++, lorsqu'on instancie un type non trivial (classe, struct avec logique), le cycle de vie nécessite l'appel du constructeur lors de la création et du destructeur lors de la destructino. Or, malloc/free étant des fonctions déjà compilées, elles ne peuvent pas invoquer ces fonctions membres spéciales. Les opérateurs new et delete ont donc été introduits pour combiner allocation mémoire et initialisation/déconstruction d'objets de manière cohérente et sûre.
Les différentes déclinaisons de l'opérateur new
Le langage C++ propose trois formes principales d'utilisation de new, chacune répondant à un besoin spécifique.
1. new standard (plain new)
Il s'agit de la forme la plus courante. Sa signature est la suivante :
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void*) throw();
En cas d'échec d'allocation, cette variante lance une exception de type std::bad_alloc plutôt que de renvoyer un pointeur nul. Il est donc inutile de tester la valeur de retour contre nullptr. Exemple illustratif :
#include <iostream>
#include <new>
void testStandardNew() {
try {
char* buffer = new char[1000000000000];
delete[] buffer;
} catch (const std::bad_alloc& e) {
std::cerr << "Échec d'allocation : " << e.what() << std::endl;
}
}
int main() {
testStandardNew();
return 0;
}
// Sortie : std::bad_alloc
2. new sans exception (nothrow new)
Cette variante ne lance pas d'execption en cas d'échec. Au lieu de cela, elle retourne un pointeur nul. Sa déclaration est :
void* operator new(std::size_t, const std::nothrow_t&) throw();
void operator delete(void*) throw();
Exemple d'utilisation :
#include <iostream>
#include <new>
void testNoThrowNew() {
char* data = new(std::nothrow) char[1000000000000];
if (data == nullptr) {
std::cout << "Allocation échouée, pointeur nul retourné" << std::endl;
} else {
delete[] data;
}
}
int main() {
testNoThrowNew();
return 0;
}
// Sortie : Allocation échouée, pointeur nul retourné
3. placement new
Cette forme permet de construire un objet dans un emplacement mémoire déjà alloué. Elle n'effectue aucune allocation : elle se contente d'appeler le constructeur à l'adresse spécifiée. Sa signature est :
void* operator new(std::size_t, void*);
void operator delete(void*, void*);
Points clés à retenir :
- Le placement new est particulièrement utile pour réutiliser un bloc de mémoire important en y construisant successivement différents objets ou tableaux d'objets.
- Les objets construits via placement new doivent être détruits en appelant explicitement leur destructeur. Il ne faut surtout pas utiliser
deletedessus, car la taille de l'objet construit ne correspond pas nécessairement à la taille du bloc mémoire originel. Undeleteprovoquerait soit une fuite de mémoire, soit une erreur d'exécution lors de la libération.
Exemple pratique :
#include <iostream>
#include <new>
class Widget {
int a;
int b;
public:
Widget() : a(42), b(84) {
std::cout << "Construction de Widget : a=" << a << " b=" << b << std::endl;
}
~Widget() {
std::cout << "Destruction de Widget" << std::endl;
}
};
int main() {
// Étape 1 : allouer un bloc de mémoire brute
char* memBlock = new(std::nothrow) char[sizeof(Widget)];
if (memBlock == nullptr) {
std::cout << "Échec d'allocation du bloc" << std::endl;
return 1;
}
// Étape 2 : construire un Widget dans ce bloc (placement new)
Widget* w = new(memBlock) Widget;
// Étape 3 : destruction explicite — ne pas faire delete w !
w->~Widget();
// Étape 4 : libérer uniquement le bloc mémoire original
delete[] memBlock;
return 0;
}
// Sortie :
// Construction de Widget : a=42 b=84
// Destruction de Widget
delete, delete[] et allocator : distinctions et usages
Concernant les tableaux dynamiques : avec new, la taille entre crochets peut être une expression évaluée à l'exécution (variable), contraierment aux tableaux statiques où une constante de compilation est obligatoire. Par ailleurs, new Type[n] retourne un pointeur de type Type* et non un type tableau. Lors d'un delete[], les éléments sont détruits en ordre inverse de leur construction.
Limites du couplage allocation/construction : le mécanisme new fusionne systématiquement deux opérations distinctes — l'allocation de mémoire et la construction d'objets. De même, delete couple destruction et libération. Cette approche peut être inefficace lorsqu'on souhaite allouer un grand bloc sans initialiser immédiatement tous les éléments.
L'utilitaire std::allocator résout ce problème en découplant ces deux phases : il fournit une méthode allocate pour réserver de la mémoire brute non initialisée, puis construct (ou ::placement new en C++11 et antérieur) pour construire les objets au moment voulu. La libération suit la même logique avec destroy puis deallocate. Ce contrôle fin est notamment exploité par les conteneurs de la STL comme std::vector pour optimiser la gestion de capacité.