C++程序经常使用指向对象的指针,通常会搭配 new 来使用,例如,Class_Name 是类,value 是 Type_Name 类型:
Class_Name * ptr = new Class_Name(value);
该语句会调用如下的构造函数:
Class_Name(Type_Name &);
而下面的初始化则是会调用默认构造函数:
Class_Name * ptr = new Class_Name;
再谈 new 和 delete
对于类对象我们可以在两个层次上使用 new 和 delete。
首先,可以使用 new 为类的数据成员分配存储空间。例如,String 类的char * str
成员,在构造函数中使用 new 生成足够存储字符串的空间,并将地址赋值给 str。由于这是在构造函数中进行的,因此在析构函数中必须使用 delete 释放空间。因为字符串是字符数组,所以构造函数和析构函数中使用带中括号的 new 和 delete。
其次,可以使用 new 来为整个类对象分配内存。String * pstr = new String("YouKa");
这不是为要存储的字符串分配内存,而是为对象分配内存,即为保存字符串地址的 str 指针和 len 成员分配内存。需要注意,程序不会给 strNum 成员分配内存,因为 strNum 是静态成员,是独立于对象的。创建对象将调用构造函数,构造函数分配用于保存字符串的内容,并将字符串的地址赋给 str。当程序不在需要该对象时,可以使用 delete 删除它,delete pstr;
,这里的 delete 只释放保存 str 和 len 成员的空间,不释放 str 指向的空间,该任务是由析构函数完成的。
在下述情况下析构函数将被调用:
- 如果对象是自动变量,则当执行完定义该对象的程序块时,将调用改对象的析构函数。
- 如果对象是静态变量,则在程序结束时将调用对象的析构函数。
- 如果对象是用 new 创建的,则仅当显式使用 delete 删除对象时,析构函数才会被调用。
指针和对象的小结
使用对象指针时,要注意几点:
- 使用常规表示法来声明指向对象的指针:
String * pstr;
- 可以将指针初始化为指向已有的对象。
- 可以使用 new 来初始化指针,调用相应的类构造函数来初始化创建一个新的对象。
- 可以使用 -> 运算符通过指针访问类方法。
- 可以对对象指针应用解除引用运算符(*)来获得指针指向的对象。
定位 new 运算符
定位 new 运算符能够在分配内存时指定内存位置。使用定位 new 运算符必须先包含头文件 。
#include <new>
#include <iostream>
struct book{
char name[20];
double price;
};
char buffer1[100];
char buffer2[100];
int main() {
std::cout << "buffer1 address: " << (void *) buffer1 << std::endl;
std::cout << "buffer2 address: " << (void *) buffer2 << std::endl;
book *p1, *p2;
p1 = new book;
// 定位 new 运算符,在 buffer1 的位置上创建一个 book 结构的变量
p2 = new (buffer1) book;
std::cout << "p1 address: " << p1 << std::endl;
std::cout << "p2 address: " << p2 << std::endl;
int * p3 = new (buffer2) int[10];
std::cout << "p3 address: " << p3 << std::endl;
return 0;
}
将 new 定位运算符运用在对象上,还会有所不同。
class TestType {
public:
TestType(const std::string & s = "Just use to test", int number = 0) {
words = s;
this->number = number;
std::cout << words << " constructed.\n";
}
~TestType() {
std::cout << words << " destructed.\n";
}
friend std::ostream & operator<<(std::ostream & out, const TestType & t);
private:
std::string words;
int number;
};
std::ostream & operator<<(std::ostream & out, const TestType & t) {
std::cout << t.words << ", " << t.number;
return out;
}
int main() {
char * buffer = new char[512];
std::cout << "Memory address: " << (void *) buffer << std::endl;
TestType * p1, *p2, *p3, *p4;
p1 = new (buffer) TestType;
p2 = new TestType("Heap1", 20);
std::cout << "buffer(p1) address: " << p1 << std::endl;
std::cout << "heap(p2) address: " << p2 << std::endl;
std::cout << "Memory content:\n";
std::cout << p1 << ": " << *p1 << "; " << p2 << ": " << *p2 << std::endl << std::endl;
p3 = new (buffer) TestType("Bad", 6);
p4 = new TestType("Heap2", 10);
std::cout << "Memory content:\n";
std::cout << p3 << ": " << *p3 << "; " << p4 << ": " << *p4 << std::endl << std::endl;
std::cout << "p1: " << *p1 << std::endl << std::endl;
delete p2;
delete p4;
delete [] buffer;
std::cout << "Finish.\n";
return 0;
}
该程序使用 new 运算符创建了一个 512 字节的内存缓冲区;然后使用 new 运算符在堆中创建两个 TestType 对象,使用定位 new 运算符在内存缓冲区中创建两个 TestType 对象;最后,它使用 delete 来释放使用 new 分配的内存。下面是程序的输出:
Memory address: 0xd95e60
Just use to test constructed.
Heap1 constructed.
buffer(p1) address: 0xd95e60
heap(p2) address: 0xd91b10
Memory content:
0xd95e60: Just use to test, 0; 0xd91b10: Heap1, 20
Bad constructed.
Heap2 constructed.
Memory content:
0xd95e60: Bad, 6; 0xd91b40: Heap2, 10
p1: Bad, 6
Heap1 destructed.
Heap2 destructed.
Finish.
该程序使用定位 new 运算符时存在两个问题:
- 在创建第二个对象时,定位 new 运算符使用一个新对象来覆盖第一个对象的内存单元。显然,如果类使用 new 为其成员动态分配内存将引发问题。
- 其次,将 delete 用于 p2 和 p4 时,将自动调用 p2 和 p4 指向的对象调用其析构函数;然而,将 delete[] 用于缓存区 buffer 时,不会为使用 定位 new 运算符创建的对象调用析构函数。
该程序给我们的第一个教训是,程序员必须负责确保使用定位 new 运算符使用的缓冲区内存单元正确。要使用不同的内存单元,程序员需要提供两个位于缓冲区的不同地址,并确保这两个地址单元不重叠。例如,可以这样做:
p1 = new (buffer) TestType;
p3 = new (buffer + sizeof(TestType)) TestType("Bad", 6);
其中 p3 指针指向相对于 p1 的偏移量为 TestType 对象的地址。
第二个教训是,如果使用定位 new 运算符来为对象分配内存,必须确保其析构函数被调用。但如何确保呢?对于在堆中创建的对象,可以这样做:delete p2;
,但由定位 new 运算符创建的对象不能这样做:delete p1; delete p3;
。原因在于 delete 可与常规 new 运算符配合使用,但不能与定位 new 运算符配合使用。另一方面,指针 p1 指向的地址与 buffer 相同,但 buffer 是使用 new[] 初始化的,因此必须使用 delete[] 而不是 delete 来释放。即使 buffer 是使用 new 而不是 new[] 初始化的,delete p1 也将释放 buffer,而不是 p1。这是因为 new/delete 系统知道已分配的 512 字节块 buffer,但对于定位 new 运算符对该内存块做了何种处理一无所知。delete [] buffer;
释放使用常规 new 运算符分配的整个内存块,但它没有为定位 new 运算符在该内存块中创建的对象调用析构函数,这一点可以根据输出信息来确定。输出信息中有 p2、p4 这两个在堆中申请内存的对象的析构函数调用痕迹,而 p1 和 p3 指向的对象没有调用析构函数。
这种问题的解决方案是,显式地为使用定位 new 运算符创建的对象调用析构函数。正常情况下需要自动调用析构函数,这是需要显式调用析构函数的少数几种情况之一。显式地调用析构函数时,必须指定要销毁的对象,由于有指向对象的指针,因此可以使用这些指针:
p3->~TestType();
p1->~TestType();
需要注意的一点是正确的删除顺序!对于使用 new 运算符创建的对象,应用与创建顺序相反的顺序进行删除。原因在于,晚创建的对象可能依赖于先创建的对象。另外,仅当所有对象都被销毁之后才能释放用于存储这些对象的缓冲区。
正确使用定位 new 运算符创建对象的程序如下:
int main() {
char * buffer = new char[512];
std::cout << "Memory address: " << (void *) buffer << std::endl;
TestType * p1, *p2, *p3, *p4;
p1 = new (buffer) TestType;
p2 = new TestType("Heap1", 20);
std::cout << "buffer(p1) address: " << p1 << std::endl;
std::cout << "heap(p2) address: " << p2 << std::endl;
std::cout << "Memory content:\n";
std::cout << p1 << ": " << *p1 << "; " << p2 << ": " << *p2 << std::endl << std::endl;
// 错误, 这样创建对象会覆盖 p1 指向的对象. 如果类动态为成员分配内存, 会导致内存泄漏.
// p3 = new (buffer) TestType("Bad", 6);
p3 = new (buffer + sizeof (TestType)) TestType("Bad", 6);
p4 = new TestType("Heap2", 10);
std::cout << "Memory content:\n";
std::cout << p3 << ": " << *p3 << "; " << p4 << ": " << *p4 << std::endl << std::endl;
std::cout << "p1: " << *p1 << std::endl << std::endl;
delete p2;
p2 = nullptr;
delete p4;
p4 = nullptr;
p3->~TestType();
p3 = nullptr;
p1->~TestType();
p1 = nullptr;
delete [] buffer;
std::cout << "Finish.\n";
return 0;
}
该程序使用定位 new 运算符在相邻的内存单元中创建了两个对象,并调用了合适的析构函数。