内存管理
如何处理内存泄漏?
通过编写没有内存泄漏的代码。显然,如果你的代码到处都是 `new` 操作、`delete` 操作和指针算术,你迟早会在某个地方出错,导致内存泄漏、野指针等。这与你分配内存的自觉程度无关:最终代码的复杂性会超出你所能投入的时间和精力。
因此,成功的技术依赖于将分配和释放隐藏在更易于管理类型中:对于单个对象,首选 `make_unique` 或 `make_shared`。对于多个对象,首选使用标准容器,如 `vector` 和 `unordered_map`,因为它们比你在不付出不成比例的努力下,更好地管理其元素的内存。考虑一下在没有 `string` 和 `vector` 帮助的情况下编写这段代码
#include<vector>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
int main() // small program messing around with strings
{
cout << "enter some whitespace-separated words:\n";
vector<string> v;
string s;
while (cin>>s) v.push_back(s);
sort(v.begin(),v.end());
string cat;
for (auto & str : v) cat += str+"+";
cout << cat << '\n';
}
你第一次就写对的几率有多大?你又怎么知道没有内存泄漏呢?
请注意,其中没有显式内存管理、宏、类型转换、溢出检查、显式大小限制和指针。 通过使用函数对象和标准算法,代码还可以消除迭代器的指针式使用,但这对于如此小的程序来说似乎有些过度。
这些技术并不完美,而且并非总能系统地使用它们。然而,它们的应用范围惊人地广泛,通过减少显式分配和释放的数量,你可以更轻松地跟踪其余示例。早在 1981 年,Stroustrup 就指出,通过将需要显式跟踪的对象数量从数万个减少到几十个,他将正确编写程序所需的智力投入从一项艰巨的任务变成了可管理甚至容易的任务。
如果你的应用程序领域没有能让显式内存管理最小化的编程变得简单的库,那么最快完成并正确编写程序的方法可能是首先构建这样一个库。
模板和标准库使得容器、资源句柄等的使用比几年前容易得多。异常的使用使得它几乎必不可少。
如果你不能在应用程序中隐式处理作为对象一部分的分配/释放,那么你可以使用资源句柄来最大限度地减少内存泄漏的机会。这里有一个示例,你需要从函数中返回一个在自由存储上分配的对象。这是一个忘记删除该对象的机会。毕竟,我们不能仅仅通过查看指针就知道它是否需要释放,如果需要,谁负责。使用资源句柄,这里是标准库的 `unique_ptr`,可以清楚地表明责任所在。
#include<memory>
#include<iostream>
using namespace std;
struct S {
S() { cout << "make an S\n"; }
~S() { cout << "destroy an S\n"; }
S(const S&) { cout << "copy initialize an S\n"; }
S& operator=(const S&) { cout << "copy assign an S\n"; }
};
S* f()
{
return new S; // who is responsible for deleting this S?
};
unique_ptr<S> g()
{
return make_unique<S>(); // explicitly transfer responsibility for deleting this S
}
int main()
{
cout << "start main\n";
S* p = f();
cout << "after f() before g()\n";
// S* q = g(); // this error would be caught by the compiler
unique_ptr<S> q = g();
cout << "exit main\n";
// leaks *p
// implicitly deletes *q
}
请考虑资源,而不仅仅是内存。
如果你的环境中无法系统地应用这些技术(你必须使用其他地方的代码,你的部分程序是由尼安德特人编写的等),请务必在你的标准开发过程中使用内存泄漏检测器,或插入垃圾回收器。
我可以像在 Java 中一样使用 `new` 吗?
差不多,但不要盲目地这样做;如果你真的想用,最好将其拼写为 `make_unique` 或 `make_shared`,而且通常有更简单、更健壮的替代方案。请考虑
void compute(cmplx z, double d)
{
cmplx z2 = z+d; // c++ style
z2 = f(z2); // use z2
cmplx& z3 = *new cmplx(z+d); // Java style (assuming Java could overload +)
z3 = f(z3);
delete &z3;
}
对于 `z3` 笨拙地使用 `new` 是不必要的,与惯用的局部变量(`z2`)相比,速度也较慢。如果你在同一作用域内创建对象并 `delete` 该对象,则无需使用 `new` 创建对象;这样的对象应该是局部变量。
我应该使用 `NULL` 还是 `0` 还是 `nullptr`?
你应该使用 `nullptr` 作为空指针值。其他两者仍然可以用于与旧代码的向后兼容性。
`NULL` 和 `0` 作为空指针值的问题是 `0` 是一个特殊的“可能是一个整数值也可能是一个指针”的值。只对整数使用 `0`,这种混淆就会消失。
`delete p` 是删除指针 `p`,还是删除 `*p` 指向的数据?
指向的数据。
这个关键字实际上应该是 `delete_the_thing_pointed_to_by`。在 C 语言中释放指针指向的内存时,也会出现同样的英语滥用:`free(p)` 实际上意味着 `free_the_stuff_pointed_to_by(p)`。
重复 `delete` 同一个指针安全吗?
不安全!(假设你在此期间没有从 `new` 重新获得该指针。)
例如,以下是一个灾难
class Foo { /*...*/ };
void yourCode()
{
Foo* p = new Foo();
delete p;
delete p; // DISASTER!
// ...
}
第二次 `delete p` 那一行可能会给你带来非常糟糕的事情。它可能会,取决于月相,破坏你的堆,使你的程序崩溃,对已经在堆上的对象进行任意和奇怪的更改等等。不幸的是,这些症状可能会随机出现和消失。根据墨菲定律,你会在最糟糕的时刻(当客户在看,当高价值交易试图提交时等等)受到最严重的打击。
注意:一些运行时系统会保护你免受某些非常简单的双重 `delete` 情况。根据具体细节,如果碰巧在其中一个系统上运行并且没有人将你的代码部署到处理方式不同的另一个系统上并且你正在删除一个没有析构函数的对象并且你在两次 `delete` 之间没有做任何重要的事情并且没有人更改你的代码以在两次 `delete` 之间做重要的事情并且你的线程调度器(你可能无法控制!)碰巧没有在两次 `delete` 之间交换线程并且,并且,并且。所以回到墨菲定律:既然它可能出错,它就会出错,而且它会在最糟糕的时刻出错。
请不要给我发电子邮件说你测试过了,它没有崩溃。清醒一点。不崩溃并不能证明没有 bug;它只是未能证明存在 bug。
相信我:双重 `delete` 糟糕透了。拒绝使用。
我可以使用 `free()` 释放用 `new` 分配的指针吗?我可以使用 `delete` 释放用 `malloc()` 分配的指针吗?
不行! 简而言之,从概念上讲,`malloc` 和 `new` 从不同的堆中分配内存,因此不能互相 `free` 或 `delete` 对方的内存。它们也在不同的层次上操作——原始内存与构造对象。
你可以在同一个程序中使用 `malloc()` 和 `new`。但是你不能使用 `malloc()` 分配一个对象,然后使用 `delete` 释放它。也不能使用 `new` 分配,然后使用 `free()` 删除,或者在用 `new` 分配的数组上使用 `realloc()`。
C++ 运算符 `new` 和 `delete` 保证正确的构造和析构;当需要调用构造函数或析构函数时,它们会被调用。C 风格的函数 `malloc()`、`calloc()`、`free()` 和 `realloc()` 不保证这一点。此外,`new` 和 `delete` 用于获取和释放原始内存的机制与 `malloc()` 和 `free()` 是否兼容,并没有任何保证。如果混合使用样式在你的系统上有效,你只是“幸运”——暂时如此。
如果你觉得需要 `realloc()`——很多人都有这种感觉——那么考虑使用标准库 `vector`。例如
// read words from input into a vector of strings:
vector<string> words;
string s;
while (cin>>s && s!=".") words.push_back(s);
`vector` 会按需扩展。
另请参阅 Stroustrup 出版物列表中可下载的“将标准 C++ 作为新语言学习”中的示例和讨论。
`new` 和 `malloc()` 有什么区别?
首先,`make_unique`(或 `make_shared`)几乎总是优于 `new` 和 `malloc()`,并且完全消除了 `delete` 和 `free()`。
话虽如此,两者之间的区别如下
`malloc()` 是一个函数,它以一个数字(字节数)作为参数;它返回一个指向未初始化存储的 `void*`。`new` 是一个运算符,它以一个类型和(可选)该类型的一组初始化器作为参数;它返回一个指向其类型(可选)已初始化对象的指针。当你想分配一个具有非平凡初始化语义的用户定义类型的对象时,这种区别最为明显。例如
class Circle : public Shape {
public:
Circle(Point c, int r);
// no default constructor
// ...
};
class X {
public:
X(); // default constructor
// ...
};
void f(int n)
{
void* p1 = malloc(40); // allocate 40 (uninitialized) bytes
int* p2 = new int[10]; // allocate 10 uninitialized ints
int* p3 = new int(10); // allocate 1 int initialized to 10
int* p4 = new int(); // allocate 1 int initialized to 0
int* p4 = new int; // allocate 1 uninitialized int
Circle* pc1 = new Circle(Point(0,0),10); // allocate a Circle constructed
// with the specified argument
Circle* pc2 = new Circle; // error no default constructor
X* px1 = new X; // allocate a default constructed X
X* px2 = new X(); // allocate a default constructed X
X* px2 = new X[10]; // allocate 10 default constructed Xs
// ...
}
请注意,当你使用“(value)”表示法指定初始化器时,你将获得该值的初始化。通常,`vector` 是自由存储分配数组的更好替代方案(例如,考虑异常安全性)。
无论何时使用 `malloc()`,你都必须考虑初始化以及将返回指针转换为正确的类型。你还需要考虑你是否获得了正确数量的字节以供使用。当你考虑初始化时,`malloc()` 和 `new` 之间没有性能差异。
`malloc()` 通过返回 `0` 来报告内存耗尽。`new` 通过抛出异常(`bad_alloc`)来报告分配和初始化错误。
通过 `new` 创建的对象通过 `delete` 销毁。通过 `malloc()` 分配的内存区域通过 `free()` 释放。
我为什么要使用 `new` 而不是值得信赖的老旧 `malloc()`?
首先,`make_unique`(或 `make_shared`)几乎总是优于 `new` 和 `malloc()`,并且完全消除了 `delete` 和 `free()`。
话虽如此,使用 `new` 而不是 `malloc` 的好处是:构造函数/析构函数、类型安全、可重写性。
- 构造函数/析构函数:与 `malloc(sizeof(Fred))` 不同,`new Fred()` 调用 `Fred` 的构造函数。同样,`delete p` 调用 `*p` 的析构函数。
- 类型安全:`malloc()` 返回一个 `void*`,它不是类型安全的。`new Fred()` 返回正确类型(`Fred*`)的指针。
- 可重写性:`new` 是一个可以被类重写的运算符,而 `malloc()` 不能按类重写。
我可以在通过 `new` 分配的指针上使用 `realloc()` 吗?
不!
当 `realloc()` 必须复制分配时,它使用按位复制操作,这将使许多 C++ 对象被撕成碎片。C++ 对象应该允许自行复制。它们使用自己的复制构造函数或赋值运算符。
除此之外,`new` 使用的堆可能与 `malloc()` 和 `realloc()` 使用的堆不同!
为什么 C++ 没有等同于 `realloc()` 的功能?
如果你想,你当然可以使用 `realloc()`。但是,`realloc()` 只保证对由 `malloc()`(和类似函数)分配的、不包含用户定义复制构造函数的数组有效。此外,请记住,与幼稚的期望相反,`realloc()` 偶尔确实会复制其参数数组。
在 C++ 中,处理重新分配的更好方法是使用标准库容器,例如 `vector`,并让它自然增长。
在 `p = new Fred()` 之后,我需要检查是否为空吗?
不需要!(但是如果你有一个古老的、石器时代的编译器,你可能需要强制 `new` 运算符在内存耗尽时抛出异常。)
每次 `new` 分配后都显式编写 `nullptr` 测试,这确实很麻烦。下面的代码就很冗长:
Fred* p = new Fred();
if (nullptr == p) // Only needed if your compiler is from the Stone Age!
throw std::bad_alloc();
如果你的编译器不支持(或者你拒绝使用)异常,你的代码可能会更繁琐
Fred* p = new Fred();
if (nullptr == p) { // Only needed if your compiler is from the Stone Age!
std::cerr << "Couldn't allocate memory for a Fred" << std::endl;
abort();
}
振作起来。在 C++ 中,如果在 `p = new Fred()` 期间运行时系统无法分配 `sizeof(Fred)` 字节的内存,则会抛出 `std::bad_alloc` 异常。与 `malloc()` 不同,`new` 从不返回 null!
因此你应该简单地写
Fred * p = new Fred(); // No need to check if p is null
再想一想。划掉它。你应该简单地写
auto p = make_unique<Fred>(); // No need to check if p is null
好了,好了……现在好多了!
然而,如果你的编译器很老,它可能还不支持此功能。通过查看编译器的“`new`”文档来了解。如果它很老,你可能需要强制编译器具有此行为。
如何让我的(旧)编译器自动检查 `new` 是否返回空值?
最终你的编译器会的。
如果你有一个旧编译器,它不能自动执行空值测试,你可以通过安装一个“new handler”函数来强制运行时系统执行测试。你的“new handler”函数可以做任何你想做的事情,例如抛出异常,删除一些对象并返回(在这种情况下,`operator new` 将重试分配),打印消息并中止程序等等。
这是一个打印消息并抛出异常的示例“new handler”。该处理程序使用 `std::set_new_handler()` 安装
#include <new> // To get std::set_new_handler
#include <cstdlib> // To get abort()
#include <iostream> // To get std::cerr
class alloc_error : public std::exception {
public:
alloc_error() : exception() { }
};
void myNewHandler()
{
// This is your own handler. It can do anything you want.
throw alloc_error();
}
int main()
{
std::set_new_handler(myNewHandler); // Install your "new handler"
// ...
}
执行 `std::set_new_handler()` 行之后,`operator new` 将在内存不足时调用你的 `myNewHandler()`。这意味着 `new` 将永远不会返回 null。
Fred* p = new Fred(); // No need to check if p is null
注意:如果你的编译器不支持异常处理,作为最后手段,你可以将 `throw` …`;` 行更改为
std::cerr << "Attempt to allocate memory failed!" << std::endl;
abort();
注意:如果某个命名空间范围/全局/静态对象的构造函数使用 `new`,它可能不会使用 `myNewHandler()` 函数,因为该构造函数通常在 `main()` 开始之前被调用。不幸的是,没有方便的方法可以保证 `std::set_new_handler()` 会在第一次使用 `new` 之前被调用。例如,即使你将 `std::set_new_handler()` 调用放在全局对象的构造函数中,你仍然不知道包含该全局对象的模块(“编译单元”)是先被初始化还是后被初始化,或者介于两者之间。因此,你仍然无法保证你的 `std::set_new_handler()` 调用会在任何其他命名空间范围/全局构造函数被调用之前发生。
在 `delete p` 之前需要检查是否为空吗?
不!
C++ 语言保证如果 `p` 为空,`delete p` 将不执行任何操作。由于你可能会测试错误,并且由于大多数测试方法强制你显式测试每个分支点,因此你不应该添加冗余的 `if` 测试。
错误
if (p != nullptr) // or just "if (p)"
delete p;
正确
delete p;
当我执行 `delete p` 时,会发生哪两个步骤?
`delete p` 是一个两步过程:它调用析构函数,然后释放内存。为 `delete p` 生成的代码在功能上类似于此(假设 `p` 的类型为 `Fred*`)
// Original code: delete p;
if (p) { // or "if (p != nullptr)"
p->~Fred();
operator delete(p);
}
语句 `p->~Fred()` 调用 `p` 指向的 `Fred` 对象的析构函数。
语句 `operator delete(p)` 调用内存释放原语 `void operator delete(void* p)`。该原语在精神上类似于 `free(void* p)`。(然而请注意,这两者是不可互换的;例如,无法保证这两个内存释放原语甚至使用相同的堆!)
为什么 `delete` 不将其操作数置空?
首先,你应该通常使用智能指针,所以你不会关心——你无论如何都不会编写 `delete`。
对于那些你确实进行手动内存管理并因此需要关心的情况,请考虑
delete p;
// ...
delete p;
如果 `...` 部分不接触 `p`,那么第二个 `delete p;` 是一个严重的错误,C++ 实现无法有效保护自己(除非采取异常预防措施)。由于删除空指针本质上是无害的,一个简单的解决方案是让 `delete p;` 在完成所有其他必要操作后执行 `p=nullptr;`。然而,C++ 不保证这一点。
一个原因是 `delete` 的操作数不一定是左值。考虑
delete p+1;
delete f(x);
在这里,`delete` 的实现没有可供其置空的指针。这些示例可能很少见,但它们确实意味着无法保证“任何指向已删除对象的指针都为空”。绕过该“规则”的更简单方法是拥有两个指向对象的指针
T* p = new T;
T* q = p;
delete p;
delete q; // ouch!
C++ 明确允许 `delete` 的实现将左值操作数置零,但这种想法似乎并未在实现者中流行起来。
如果你认为将指针置零很重要,请考虑使用销毁函数
template<class T> inline void destroy(T*& p) { delete p; p = 0; }
请将其视为又一个理由,通过依赖标准库智能指针、容器、句柄等,最大限度地减少显式使用 `new` 和 `delete`。
请注意,将指针作为引用传递(允许指针置空)还具有额外的好处,即阻止为右值调用 `destroy()`
int* f();
int* p;
// ...
destroy(f()); // error: trying to pass an rvalue by non-const reference
destroy(p+1); // error: trying to pass an rvalue by non-const reference
为什么析构函数在作用域结束时没有被调用?
简单的答案是“当然会!”但看看这个经常伴随这个问题的例子
void f()
{
X* p = new X;
// use p
}
也就是说,存在一些(错误的)假设,即由 `new` 创建的对象将在函数结束时被销毁。
基本上,你只有在希望对象在创建它的作用域生命周期之外继续存在时才应该使用堆分配。即便如此,你通常也应该使用 `make_unique` 或 `make_shared`。在那些你确实想要堆分配并且选择使用 `new` 的罕见情况下,你需要使用 `delete` 来销毁对象。例如
X* g(int i) { /* ... */ return new X(i); } // the X outlives the call of g()
void h(int i)
{
X* p = g(i);
// ...
delete p; // caveat: not exception safe
}
如果你希望一个对象只存在于一个作用域中,就完全不要使用堆分配,而只需定义一个变量
{
ClassName x;
// use x
}
该变量在作用域结束时被隐式销毁。
使用 `new` 创建对象然后在同一作用域结束时 `delete` 它的代码是丑陋的、容易出错的、低效的,而且通常不是异常安全的。例如
void very_bad_func() // ugly, error-prone, and inefficient
{
X* p = new X;
// use p
delete p; // not exception-safe
}
在 `p = new Fred()` 中,如果 `Fred` 构造函数抛出异常,`Fred` 内存是否会“泄漏”?
不是。
如果在 `p = new Fred()` 的 `Fred` 构造函数期间发生异常,C++ 语言保证分配的 `sizeof(Fred)` 字节内存将自动释放回堆。
详细信息如下:`new Fred()` 是一个两步过程
- 使用原语 `void* operator new(size_t nbytes)` 分配 `sizeof(Fred)` 字节的内存。该原语在精神上类似于 `malloc(size_t nbytes)`。(但是请注意,这两者是不可互换的;例如,不能保证这两个内存分配原语甚至使用相同的堆!)
- 它通过调用 `Fred` 构造函数在该内存中构造一个对象。从第一步返回的指针作为 `this` 参数传递给构造函数。此步骤被封装在一个 `try` … `catch` 块中,以处理此步骤中抛出异常的情况。
因此,实际生成的代码在功能上类似于
// Original code: Fred* p = new Fred();
Fred* p;
void* tmp = operator new(sizeof(Fred));
try {
new(tmp) Fred(); // Placement new
p = (Fred*)tmp; // The pointer is assigned only if the ctor succeeds
}
catch (...) {
operator delete(tmp); // Deallocate the memory
throw; // Re-throw the exception
}
标记为“placement `new`”的语句调用 `Fred` 构造函数。指针 `p` 在构造函数 `Fred::Fred()` 内部成为 `this` 指针。
如何分配/释放数组?
使用 `p = new T[n]` 和 `delete[] p`
Fred* p = new Fred[100];
// ...
delete[] p;
无论何时通过 `new` 分配对象数组(通常在 `new` 表达式中使用 `[`n`]`),你都必须在 `delete` 语句中使用 `[]`。这种语法是必要的,因为指向一个对象的指针和指向一个对象数组的指针在语法上没有区别(这是我们从 C 继承的)。
如果我在 `delete` 通过 `new T[n]` 分配的数组时忘记了 `[]` 会怎样?
所有生命都将走向灾难性的终结。
正确连接 `new T[n]` 和 `delete[] p` 是程序员的责任,而不是编译器的责任。如果你搞错了,编译器不会生成编译时错误消息,也不会生成运行时错误消息。堆损坏是很可能的结果。甚至更糟。你的程序可能会崩溃。
我可以在 `delete` 内置类型(`char`、`int` 等)的数组时省略 `[]` 吗?
不!
有时程序员认为 `delete[] p` 中的 `[]` 仅仅是为了让编译器为数组中的所有元素调用适当的析构函数。基于这种推理,他们假定像 `char` 或 `int` 这样的内置类型数组可以不带 `[]` 地被 `delete`。例如,他们假设以下是有效的代码
void userCode(int n)
{
char* p = new char[n];
// ...
delete p; // ← ERROR! Should be delete[] p !
}
但是上面的代码是错误的,它可能在运行时导致灾难。特别是,`delete p` 调用的代码是 `operator delete(void*)`,而 `delete[] p` 调用的代码是 `operator delete[](void*)`。后者的默认行为是调用前者,但用户可以将其替换为不同的行为(在这种情况下,他们通常也会替换 `operator new[](size_t)` 中相应的 `new` 代码)。如果他们替换了 `delete[]` 代码使其与 `delete` 代码不兼容,并且你调用了错误的代码(即,如果你说 `delete p` 而不是 `delete[] p`),你最终可能会在运行时遇到灾难。
在 `p = new Fred[n]` 之后,编译器如何知道在 `delete[] p` 期间有多少个对象 `n` 需要被析构?
简短回答:魔法。
长答案:运行时系统将对象数量 `n` 存储在某个地方,只要你知道指针 `p` 就可以检索到。有两种常用的技术可以实现这一点。这两种技术都已在商业级编译器中使用,两者都有权衡,而且都不完美。这些技术是
成员函数中 `delete this` 合法(且道德)吗?
只要你小心谨慎,一个对象自杀(`delete this`)是可以的(并非邪恶)。
我这样定义“小心谨慎”
- 你必须百分之百确定 `this` 对象是通过 `new` 分配的(而不是通过 `new[]`,也不是通过placement `new`,也不是栈上的局部对象,也不是命名空间作用域/全局对象,也不是另一个对象的成员;而是通过普通的 `new`)。
- 你必须百分之百确定你的成员函数将是此对象上调用的最后一个成员函数。
- 你必须百分之百确定你的成员函数(在 `delete this` 行之后)的其余部分不会触及 `this` 对象的任何部分(包括调用任何其他成员函数或触及任何数据成员)。这包括将在仍然存活的任何栈上分配的对象的析构函数中运行的代码。
- 你必须百分之百确定在 `delete this` 行之后没有人会触及 `this` 指针本身。换句话说,你不能检查它,不能将其与其他指针比较,不能将其与 `nullptr` 比较,不能打印它,不能转换它,不能对它做任何事情。
当然,在你的 `this` 指针是指向基类的指针而你没有虚析构函数的情况下,通常的注意事项仍然适用。
如何使用 `new` 分配多维数组?
有很多方法可以做到这一点,具体取决于你希望数组大小的灵活性。在一个极端,如果你在编译时知道所有维度,你可以静态分配多维数组(如在 C 中)
class Fred { /*...*/ };
void someFunction(Fred& fred);
void manipulateArray()
{
const unsigned nrows = 10; // Num rows is a compile-time constant
const unsigned ncols = 20; // Num columns is a compile-time constant
Fred matrix[nrows][ncols];
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
// Here's the way you access the (i,j) element:
someFunction( matrix[i][j] );
// You can safely "return" without any special delete code:
if (today == "Tuesday" && moon.isFull())
return; // Quit early on Tuesdays when the moon is full
}
}
// No explicit delete code at the end of the function either
}
更常见的情况是,矩阵的大小直到运行时才知道,但你知道它是矩形的。在这种情况下,你需要使用堆(“自由存储”),但至少你可以将所有元素分配在一个自由存储块中。
void manipulateArray(unsigned nrows, unsigned ncols)
{
Fred* matrix = new Fred[nrows * ncols];
// Since we used a simple pointer above, we need to be VERY
// careful to avoid skipping over the delete code.
// That's why we catch all exceptions:
try {
// Here's how to access the (i,j) element:
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
someFunction( matrix[i*ncols + j] );
}
}
// If you want to quit early on Tuesdays when the moon is full,
// make sure to do the delete along ALL return paths:
if (today == "Tuesday" && moon.isFull()) {
delete[] matrix;
return;
}
// ...code that fiddles with the matrix...
}
catch (...) {
// Make sure to do the delete when an exception is thrown:
delete[] matrix;
throw; // Re-throw the current exception
}
// Make sure to do the delete at the end of the function too:
delete[] matrix;
}
最后,在另一个极端,你甚至可能无法保证矩阵是矩形的。例如,如果每一行可以有不同的长度,你需要单独分配每一行。在以下函数中,`ncols[i]` 是第 `i` 行的列数,其中 `i` 的范围在 `0` 和 `nrows-1` 之间(包括边界)。
void manipulateArray(unsigned nrows, unsigned ncols[])
{
typedef Fred* FredPtr;
// There will not be a leak if the following throws an exception:
FredPtr* matrix = new FredPtr[nrows];
// Set each element to null in case there is an exception later.
// (See comments at the top of the try block for rationale.)
for (unsigned i = 0; i < nrows; ++i)
matrix[i] = nullptr;
// Since we used a simple pointer above, we need to be
// VERY careful to avoid skipping over the delete code.
// That's why we catch all exceptions:
try {
// Next we populate the array. If one of these throws, all
// the allocated elements will be deleted (see catch below).
for (unsigned i = 0; i < nrows; ++i)
matrix[i] = new Fred[ ncols[i] ];
// Here's how to access the (i,j) element:
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols[i]; ++j) {
someFunction( matrix[i][j] );
}
}
// If you want to quit early on Tuesdays when the moon is full,
// make sure to do the delete along ALL return paths:
if (today == "Tuesday" && moon.isFull()) {
for (unsigned i = nrows; i > 0; --i)
delete[] matrix[i-1];
delete[] matrix;
return;
}
// ...code that fiddles with the matrix...
}
catch (...) {
// Make sure to do the delete when an exception is thrown:
// Note that some of these matrix[...] pointers might be
// null, but that's okay since it's legal to delete null.
for (unsigned i = nrows; i > 0; --i)
delete[] matrix[i-1];
delete[] matrix;
throw; // Re-throw the current exception
}
// Make sure to do the delete at the end of the function too.
// Note that deletion is the opposite order of allocation:
for (unsigned i = nrows; i > 0; --i)
delete[] matrix[i-1];
delete[] matrix;
}
请注意在删除过程中对 `matrix[i-1]` 的巧妙使用。这可以防止当 `i` 小于零时 `unsigned` 值发生环绕。
最后,请注意指针和数组是邪恶的。通常最好将指针封装在一个具有安全简单接口的类中。以下常见问题解答展示了如何做到这一点。
但是上一个 FAQ 的代码太复杂且容易出错了!有没有更简单的方法?
有的。
上一个 FAQ 中的代码之所以如此复杂且容易出错,是因为它使用了指针,而我们知道指针和数组是邪恶的。解决方案是将你的指针封装在一个具有安全简单接口的类中。例如,我们可以定义一个处理矩形矩阵的 `Matrix` 类,这样我们的用户代码与上一个 FAQ 中的矩形矩阵代码相比将大大简化
// The code for class Matrix is shown below...
void someFunction(Fred& fred);
void manipulateArray(unsigned nrows, unsigned ncols)
{
Matrix matrix(nrows, ncols); // Construct a Matrix called matrix
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
// Here's the way you access the (i,j) element:
someFunction( matrix(i,j) );
// You can safely "return" without any special delete code:
if (today == "Tuesday" && moon.isFull())
return; // Quit early on Tuesdays when the moon is full
}
}
// No explicit delete code at the end of the function either
}
最值得注意的是,代码中缺少清理代码。例如,上面的代码中没有任何 `delete` 语句,但只要 `Matrix` 析构函数正确地完成了它的工作,就不会有内存泄漏。
这是实现上述功能的 `Matrix` 代码
class Matrix {
public:
Matrix(unsigned nrows, unsigned ncols);
// Throws a BadSize object if either size is zero
class BadSize { };
// Based on the Law Of The Big Three:
~Matrix();
Matrix(const Matrix& m);
Matrix& operator= (const Matrix& m);
// Access methods to get the (i,j) element:
Fred& operator() (unsigned i, unsigned j); // Subscript operators often come in pairs
const Fred& operator() (unsigned i, unsigned j) const; // Subscript operators often come in pairs
// These throw a BoundsViolation object if i or j is too big
class BoundsViolation { };
private:
unsigned nrows_, ncols_;
Fred* data_;
};
inline Fred& Matrix::operator() (unsigned row, unsigned col)
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}
inline const Fred& Matrix::operator() (unsigned row, unsigned col) const
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}
Matrix::Matrix(unsigned nrows, unsigned ncols)
: nrows_ (nrows)
, ncols_ (ncols)
//, data_ ← initialized below after the if...throw statement
{
if (nrows == 0 || ncols == 0)
throw BadSize();
data_ = new Fred[nrows * ncols];
}
Matrix::~Matrix()
{
delete[] data_;
}
请注意,上述 `Matrix` 类完成了两件事:它将一些复杂的内存管理代码从用户代码(例如 `main()`)转移到类中,并减少了程序的总体体积。后者很重要。例如,假设 `Matrix` 即使是轻度可重用,将复杂性从 `Matrix` 的用户(复数)转移到 `Matrix` 本身(单数)等同于将复杂性从多数转移到少数。任何看过《星际迷航 2》的人都知道,多数人的利益大于少数人……或一个人的利益。
但是上面的 `Matrix` 类是针对 `Fred` 特定的!有没有办法让它通用化?
有;只需使用模板
以下是如何使用它
#include "Fred.h" // To get the definition for class Fred
// The code for Matrix<T> is shown below...
void someFunction(Fred& fred);
void manipulateArray(unsigned nrows, unsigned ncols)
{
Matrix<Fred> matrix(nrows, ncols); // Construct a Matrix<Fred> called matrix
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
// Here's the way you access the (i,j) element:
someFunction( matrix(i,j) );
// You can safely "return" without any special delete code:
if (today == "Tuesday" && moon.isFull())
return; // Quit early on Tuesdays when the moon is full
}
}
// No explicit delete code at the end of the function either
}
现在使用 `Matrix
#include <string>
void someFunction(std::string& s);
void manipulateArray(unsigned nrows, unsigned ncols)
{
Matrix<std::string> matrix(nrows, ncols); // Construct a Matrix<std::string>
for (unsigned i = 0; i < nrows; ++i) {
for (unsigned j = 0; j < ncols; ++j) {
// Here's the way you access the (i,j) element:
someFunction( matrix(i,j) );
// You can safely "return" without any special delete code:
if (today == "Tuesday" && moon.isFull())
return; // Quit early on Tuesdays when the moon is full
}
}
// No explicit delete code at the end of the function either
}
因此,你可以从模板中获得一整个类族。例如,`Matrix
以下是模板的一种实现方式
template<typename T> // See section on templates for more
class Matrix {
public:
Matrix(unsigned nrows, unsigned ncols);
// Throws a BadSize object if either size is zero
class BadSize { };
// Based on the Law Of The Big Three:
~Matrix();
Matrix(const Matrix<T>& m);
Matrix<T>& operator= (const Matrix<T>& m);
// Access methods to get the (i,j) element:
T& operator() (unsigned i, unsigned j); // Subscript operators often come in pairs
const T& operator() (unsigned i, unsigned j) const; // Subscript operators often come in pairs
// These throw a BoundsViolation object if i or j is too big
class BoundsViolation { };
private:
unsigned nrows_, ncols_;
T* data_;
};
template<typename T>
inline T& Matrix<T>::operator() (unsigned row, unsigned col)
{
if (row >= nrows_ || col >= ncols_) throw BoundsViolation();
return data_[row*ncols_ + col];
}
template<typename T>
inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
{
if (row >= nrows_ || col >= ncols_)
throw BoundsViolation();
return data_[row*ncols_ + col];
}
template<typename T>
inline Matrix<T>::Matrix(unsigned nrows, unsigned ncols)
: nrows_ (nrows)
, ncols_ (ncols)
//, data_ ← initialized below after the if...throw statement
{
if (nrows == 0 || ncols == 0)
throw BadSize();
data_ = new T[nrows * ncols];
}
template<typename T>
inline Matrix<T>::~Matrix()
{
delete[] data_;
}
还有另一种构建 `Matrix` 模板的方法是什么?
使用标准 `vector` 模板,并创建一个 `vector` 的 `vector`。
以下代码使用 `std::vector
#include <vector>
template<typename T> // See section on templates for more
class Matrix {
public:
Matrix(unsigned nrows, unsigned ncols);
// Throws a BadSize object if either size is zero
class BadSize { };
// No need for any of The Big Three!
// Access methods to get the (i,j) element:
T& operator() (unsigned i, unsigned j); // Subscript operators often come in pairs
const T& operator() (unsigned i, unsigned j) const; // Subscript operators often come in pairs
// These throw a BoundsViolation object if i or j is too big
class BoundsViolation { };
unsigned nrows() const; // #rows in this matrix
unsigned ncols() const; // #columns in this matrix
private:
std::vector<std::vector<T>> data_;
};
template<typename T>
inline unsigned Matrix<T>::nrows() const
{ return data_.size(); }
template<typename T>
inline unsigned Matrix<T>::ncols() const
{ return data_[0].size(); }
template<typename T>
inline T& Matrix<T>::operator() (unsigned row, unsigned col)
{
if (row >= nrows() || col >= ncols()) throw BoundsViolation();
return data_[row][col];
}
template<typename T>
inline const T& Matrix<T>::operator() (unsigned row, unsigned col) const
{
if (row >= nrows() || col >= ncols()) throw BoundsViolation();
return data_[row][col];
}
template<typename T>
Matrix<T>::Matrix(unsigned nrows, unsigned ncols)
: data_ (nrows)
{
if (nrows == 0 || ncols == 0)
throw BadSize();
for (unsigned i = 0; i < nrows; ++i)
data_[i].resize(ncols);
}
请注意,这比上一个示例简单得多:构造函数中没有显式的 `new`,也不需要任何三大法则(析构函数、拷贝构造函数或赋值运算符)。简而言之,如果你使用 `std::vector` 而不是显式的 `new T[n]` 和 `delete[] p`,你的代码出现内存泄漏的可能性会大大降低。
另请注意,`std::vector` 不会强制你分配大量内存块。如果你更喜欢为整个矩阵只分配一个内存块,就像前面的示例那样,只需将 `data_` 的类型更改为 `std::vector
C++ 有长度可以在运行时指定的数组吗?
是的,从这个意义上讲,标准库有一个 `std::vector` 模板,它提供了这种行为。
不,从这个意义上讲,内置数组类型需要在编译时指定其长度。
是的,在某种意义上,即使是内置数组类型也可以在运行时指定第一个索引边界。例如,与上一个 FAQ 相比,如果你只需要第一个数组维度改变,那么你可以直接向 `new` 请求一个数组的数组,而不是一个指向数组的指针数组
const unsigned ncols = 100; // ncols = number of columns in the array
class Fred { /*...*/ };
void manipulateArray(unsigned nrows) // nrows = number of rows in the array
{
Fred (*matrix)[ncols] = new Fred[nrows][ncols];
// ...
delete[] matrix;
}
如果你需要除了数组的第一维之外的任何维度在运行时改变,你就不能这样做。
但是请,除非你必须使用,否则不要使用数组。数组是邪恶的。如果可以,请使用某个类的对象。只有在必要时才使用数组。
如何强制我的类的对象始终通过 `new` 创建,而不是作为局部、命名空间作用域、全局或 `static` 对象?
使用命名构造函数习语。
与命名构造函数习语一样,所有构造函数都是 `private` 或 `protected` 的,并且有一个或多个 `public static create()` 方法(所谓的“命名构造函数”),每个构造函数一个。在这种情况下,`create()` 方法通过 `new` 分配对象。由于构造函数本身不是 `public` 的,因此没有其他方法可以创建类的对象。
class Fred {
public:
// The create() methods are the "named constructors":
static Fred* create() { return new Fred(); }
static Fred* create(int i) { return new Fred(i); }
static Fred* create(const Fred& fred) { return new Fred(fred); }
// ...
private:
// The constructors themselves are private or protected:
Fred();
Fred(int i);
Fred(const Fred& fred);
// ...
};
现在创建 `Fred` 对象的唯一方法是通过 `Fred::create()`
int main()
{
Fred* p = Fred::create(5);
// ...
delete p;
// ...
}
如果你希望 `Fred` 拥有派生类,请确保你的构造函数位于 `protected` 部分。
另外请注意,如果你想允许 `Wilma` 拥有一个 `Fred` 类的成员对象,你可以使另一个类 `Wilma` 成为 `Fred` 的`friend`,但这当然是对最初目标的一种软化,即强制 `Fred` 对象通过 `new` 分配。
如何实现简单的引用计数?
如果你想要的只是能够传递指向同一对象的一组指针,并具有当指向它的最后一个指针消失时该对象会自动被 `delete` 的功能,你可以使用以下“智能指针”类:
// Fred.h
class FredPtr;
class Fred {
public:
Fred() : count_(0) /*...*/ { } // All ctors set count_ to 0 !
// ...
private:
friend class FredPtr; // A friend class
unsigned count_;
// count_ must be initialized to 0 by all constructors
// count_ is the number of FredPtr objects that point at this
};
class FredPtr {
public:
Fred* operator-> () { return p_; }
Fred& operator* () { return *p_; }
FredPtr(Fred* p) : p_(p) { ++p_->count_; } // p must not be null
~FredPtr() { if (--p_->count_ == 0) delete p_; }
FredPtr(const FredPtr& p) : p_(p.p_) { ++p_->count_; }
FredPtr& operator= (const FredPtr& p)
{ // DO NOT CHANGE THE ORDER OF THESE STATEMENTS!
// (This order properly handles self-assignment)
// (This order also properly handles recursion, e.g., if a Fred contains FredPtrs)
Fred* const old = p_;
p_ = p.p_;
++p_->count_;
if (--old->count_ == 0) delete old;
return *this;
}
private:
Fred* p_; // p_ is never NULL
};
当然,你可以使用嵌套类将 `FredPtr` 重命名为 `Fred::Ptr`。
请注意,通过在构造函数、拷贝构造函数、赋值运算符和析构函数中进行更多检查,你可以放宽上述“永不 `NULL`”规则。如果你这样做,你最好在“`*`”和“`->`”运算符中添加 `p_ != NULL` 检查(至少作为 `assert()`)。我建议不要使用 `operator Fred*()` 方法,因为它会让人们意外地获得 `Fred*`。
`FredPtr` 的一个隐式约束是它只能指向通过 `new` 分配的 `Fred` 对象。如果你想真正安全,可以通过将 `Fred` 的所有构造函数声明为 `private` 来强制执行此约束,并且对于每个构造函数,都有一个 `public`(`static`)`create()` 方法,该方法通过 `new` 分配 `Fred` 对象并返回一个 `FredPtr`(而不是 `Fred*`)。这样,创建 `Fred` 对象的唯一方式就是获得一个 `FredPtr`(“`Fred* p = new Fred()`”将被“`FredPtr p = Fred::create()`”替换)。这样,没有人可以意外地破坏引用计数机制。
例如,如果 `Fred` 有一个 `Fred::Fred()` 和一个 `Fred::Fred(int i, int j)`,则对 `class Fred` 的更改将是
class Fred {
public:
static FredPtr create(); // Defined below class FredPtr {...};
static FredPtr create(int i, int j); // Defined below class FredPtr {...};
// ...
private:
Fred();
Fred(int i, int j);
// ...
};
class FredPtr { /* ... */ };
inline FredPtr Fred::create() { return new Fred(); }
inline FredPtr Fred::create(int i, int j) { return new Fred(i,j); }
最终结果是,你现在有了一种使用简单引用计数为给定对象提供“指针语义”的方法。你的 `Fred` 类用户显式使用 `FredPtr` 对象,它们的行为或多或少像 `Fred*` 指针。好处是用户可以创建任意数量的 `FredPtr`“智能指针”对象的副本,并且当最后一个这样的 `FredPtr` 对象消失时,指向的 `Fred` 对象将自动被 `delete`。
如果你更愿意为用户提供“引用语义”而不是“指针语义”,你可以使用写时复制的引用计数。
如何提供具有写时复制语义的引用计数?
引用计数可以使用指针语义或引用语义来完成。上一个 FAQ 展示了如何使用指针语义进行引用计数。这个 FAQ 展示了如何使用引用语义进行引用计数。
基本的想法是允许用户认为他们在复制你的 `Fred` 对象,但实际上,除非并且直到某个用户实际尝试修改底层 `Fred` 对象,否则底层实现不会进行任何复制。
`Fred::Data` 类包含通常会放入 `Fred` 类中的所有数据。`Fred::Data` 还额外有一个数据成员 `count_`,用于管理引用计数。`Fred` 类最终是一个“智能引用”,它(内部)指向一个 `Fred::Data`。
class Fred {
public:
Fred(); // A default constructor
Fred(int i, int j); // A normal constructor
Fred(const Fred& f);
Fred& operator= (const Fred& f);
~Fred();
void sampleInspectorMethod() const; // No changes to this object
void sampleMutatorMethod(); // Change this object
// ...
private:
class Data {
public:
Data();
Data(int i, int j);
Data(const Data& d);
// Since only Fred can access a Fred::Data object,
// you can make Fred::Data's data public if you want.
// But if that makes you uncomfortable, make the data private
// and make Fred a friend class via friend class Fred;
// ...your data members are declared here...
unsigned count_;
// count_ is the number of Fred objects that point at this
// count_ must be initialized to 1 by all constructors
// (it starts as 1 since it is pointed to by the Fred object that created it)
};
Data* data_;
};
Fred::Data::Data() : count_(1) /*init other data*/ { }
Fred::Data::Data(int i, int j) : count_(1) /*init other data*/ { }
Fred::Data::Data(const Data& d) : count_(1) /*init other data*/ { }
Fred::Fred() : data_(new Data()) { }
Fred::Fred(int i, int j) : data_(new Data(i, j)) { }
Fred::Fred(const Fred& f)
: data_(f.data_)
{
++data_->count_;
}
Fred& Fred::operator= (const Fred& f)
{
// DO NOT CHANGE THE ORDER OF THESE STATEMENTS!
// (This order properly handles self-assignment)
// (This order also properly handles recursion, e.g., if a Fred::Data contains Freds)
Data* const old = data_;
data_ = f.data_;
++data_->count_;
if (--old->count_ == 0) delete old;
return *this;
}
Fred::~Fred()
{
if (--data_->count_ == 0) delete data_;
}
void Fred::sampleInspectorMethod() const
{
// This method promises ("const") not to change anything in *data_
// Other than that, any data access would simply use "data_->..."
}
void Fred::sampleMutatorMethod()
{
// This method might need to change things in *data_
// Thus it first checks if this is the only pointer to *data_
if (data_->count_ > 1) {
Data* d = new Data(*data_); // Invoke Fred::Data's copy ctor
--data_->count_;
data_ = d;
}
assert(data_->count_ == 1);
// Now the method proceeds to access "data_->..." as normal
}
如果调用 `Fred` 的默认构造函数很常见,你可以通过为所有通过 `Fred::Fred()` 构造的 `Fred` 共享一个公共的 `Fred::Data` 对象来避免所有这些 `new` 调用。为了避免`static` 初始化顺序问题,这个共享的 `Fred::Data` 对象在函数内部“首次使用时”创建。以下是对上述代码所做的更改(请注意,共享的 `Fred::Data` 对象的析构函数从不被调用;如果这是一个问题,要么希望你没有 `static` 初始化顺序问题,要么回到上面描述的方法):
class Fred {
public:
// ...
private:
// ...
static Data* defaultData();
};
Fred::Fred()
: data_(defaultData())
{
++data_->count_;
}
Fred::Data* Fred::defaultData()
{
static Data* p = nullptr;
if (p == nullptr) {
p = new Data();
++p->count_; // Make sure it never goes to zero
}
return p;
}
注意:如果你的 `Fred` 类通常是基类,你还可以为类层次结构提供引用计数。
如何为类层次结构提供具有写时复制语义的引用计数?
上一个 FAQ 提出了一个引用计数方案,它为用户提供了引用语义,但它是针对单个类而不是类层次结构的。本 FAQ 将前面的技术扩展到允许类层次结构。基本区别在于 `Fred::Data` 现在是一个类层次结构的根,这可能导致它有一些`virtual` 函数。请注意,`Fred` 类本身仍然不会有任何 `virtual` 函数。
使用虚构造函数习语来复制 `Fred::Data` 对象。为了选择要创建的派生类,下面的示例代码使用了命名构造函数习语,但也可以使用其他技术(构造函数中的 `switch` 语句等)。示例代码假设有两个派生类:`Der1` 和 `Der2`。派生类中的方法不知道引用计数。
class Fred {
public:
static Fred create1(const std::string& s, int i);
static Fred create2(float x, float y);
Fred(const Fred& f);
Fred& operator= (const Fred& f);
~Fred();
void sampleInspectorMethod() const; // No changes to this object
void sampleMutatorMethod(); // Change this object
// ...
private:
class Data {
public:
Data() : count_(1) { }
Data(const Data& d) : count_(1) { } // Do NOT copy the 'count_' member!
Data& operator= (const Data&) { return *this; } // Do NOT copy the 'count_' member!
virtual ~Data() { assert(count_ == 0); } // A virtual destructor
virtual Data* clone() const = 0; // A virtual constructor
virtual void sampleInspectorMethod() const = 0; // A pure virtual function
virtual void sampleMutatorMethod() = 0;
private:
unsigned count_; // count_ doesn't need to be protected
friend class Fred; // Allow Fred to access count_
};
class Der1 : public Data {
public:
Der1(const std::string& s, int i);
virtual void sampleInspectorMethod() const;
virtual void sampleMutatorMethod();
virtual Data* clone() const;
// ...
};
class Der2 : public Data {
public:
Der2(float x, float y);
virtual void sampleInspectorMethod() const;
virtual void sampleMutatorMethod();
virtual Data* clone() const;
// ...
};
Fred(Data* data);
// Creates a Fred smart-reference that owns *data
// It is private to force users to use a createXXX() method
// Requirement: data must not be NULL
Data* data_; // Invariant: data_ is never NULL
};
Fred::Fred(Data* data) : data_(data) { assert(data != nullptr); }
Fred Fred::create1(const std::string& s, int i) { return Fred(new Der1(s, i)); }
Fred Fred::create2(float x, float y) { return Fred(new Der2(x, y)); }
Fred::Data* Fred::Der1::clone() const { return new Der1(*this); }
Fred::Data* Fred::Der2::clone() const { return new Der2(*this); }
Fred::Fred(const Fred& f)
: data_(f.data_)
{
++data_->count_;
}
Fred& Fred::operator= (const Fred& f)
{
// DO NOT CHANGE THE ORDER OF THESE STATEMENTS!
// (This order properly handles self-assignment)
// (This order also properly handles recursion, e.g., if a Fred::Data contains Freds)
Data* const old = data_;
data_ = f.data_;
++data_->count_;
if (--old->count_ == 0) delete old;
return *this;
}
Fred::~Fred()
{
if (--data_->count_ == 0) delete data_;
}
void Fred::sampleInspectorMethod() const
{
// This method promises ("const") not to change anything in *data_
// Therefore we simply "pass the method through" to *data_:
data_->sampleInspectorMethod();
}
void Fred::sampleMutatorMethod()
{
// This method might need to change things in *data_
// Thus it first checks if this is the only pointer to *data_
if (data_->count_ > 1) {
Data* d = data_->clone(); // The Virtual Constructor Idiom
--data_->count_;
data_ = d;
}
assert(data_->count_ == 1);
// Now we "pass the method through" to *data_:
data_->sampleMutatorMethod();
}
当然,`Fred::Der1` 和 `Fred::Der2` 的构造函数和 `sampleXXX` 方法需要以适当的方式实现。
我能绝对阻止人们破坏引用计数机制吗?如果能,我应该吗?
不能,而且(通常)不应该。
破坏引用计数机制有两种基本方法
- 如果有人获得了一个 `Fred*`(而不是被迫使用 `FredPtr`),该方案可能会被破坏。如果 `FredPtr` 类有一个返回 `Fred&` 的 `operator*()`,有人可能会获得一个 `Fred*`:`FredPtr p = Fred::create(); Fred* p2 = &*p;`。是的,这很奇怪且出乎意料,但它可能会发生。这个漏洞可以通过两种方式关闭:重载 `Fred::operator&()` 使其返回一个 `FredPtr`,或者更改 `FredPtr::operator*()` 的返回类型,使其返回一个 `FredRef`(`FredRef` 将是一个模拟引用的类;它需要具有 `Fred` 拥有的所有方法,并且需要将所有这些方法调用转发到底层 `Fred` 对象;根据编译器内联方法的优化程度,这第二个选择可能会有性能损失)。另一种修复方法是删除 `FredPtr::operator*()`——并失去相应的获取和使用 `Fred&` 的能力。但即使你做了所有这些,仍然有人可以通过显式调用 `operator->()` 来生成 `Fred*`:`FredPtr p = Fred::create(); Fred* p2 = p.operator->();`。
- 如果有人泄露和/或产生指向 `FredPtr` 的悬空指针,该方案可能会被破坏。基本上我们这里说的是 `Fred` 现在是安全的,但我们却想阻止人们对 `FredPtr` 对象做愚蠢的事情。(如果我们能通过 `FredPtrPtr` 对象解决这个问题,我们也会对它们遇到同样的问题)。这里的一个漏洞是,如果有人使用 `new` 创建了一个 `FredPtr`,然后让 `FredPtr` 泄漏(最坏的情况是泄漏,这很糟糕,但通常比悬空指针好一点)。这个漏洞可以通过将 `FredPtr::operator new()` 声明为 `private` 来堵塞,从而阻止有人说 `new FredPtr()`。另一个漏洞是,如果有人创建了一个局部 `FredPtr` 对象,然后获取该 `FredPtr` 的地址并传递 `FredPtr*`。如果该 `FredPtr*` 的生命周期比 `FredPtr` 长,你可能会得到一个悬空指针——颤抖吧。这个漏洞可以通过阻止人们获取 `FredPtr` 的地址来堵塞(通过将 `FredPtr::operator&()` 重载为 `private`),同时也会失去相应的功能。但即使你做了所有这些,他们仍然可以创建一个 `FredPtr&`,这几乎和 `FredPtr*` 一样危险,只需这样做:`FredPtr p; ... FredPtr& q = p;`(或者将 `FredPtr&` 传递给其他人)。
即使我们堵住了所有这些漏洞,C++ 还有那些叫做指针转换的奇妙语法。通过一两个指针转换,一个足够有动机的程序员通常可以制造一个足够大的漏洞,可以形象地开卡车通过。(顺便说一句,指针转换是邪恶的。)
所以这里的教训似乎是:(a) 无论你多么努力,都无法阻止间谍活动,(b) 你可以轻松地防止错误。
因此,我建议满足于“低垂的果实”:使用易于构建且易于使用的机制来防止错误,而不要费心去阻止间谍活动。你不会成功,即使成功了,它(可能)也得不偿失。
那么,如果不能使用 C++ 语言本身来防止间谍行为,还有其他方法吗?是的。我个人为此使用了老式的代码审查。而且由于间谍技术通常涉及一些奇怪的语法和/或使用指针转换和联合体,你可以使用工具指出大多数“热点”。
我可以在 C++ 中使用垃圾回收器吗?
是的。
如果你想要自动垃圾回收,C++ 有很好的商业和公共领域垃圾回收器。对于适合垃圾回收的应用程序,C++ 是一种出色的垃圾回收语言,其性能与其他垃圾回收语言相比具有优势。有关 C++ 中自动垃圾回收的讨论,请参阅《C++ 编程语言(第 4 版)》。另请参阅 Hans-J. Boehm 关于C 和 C++ 垃圾回收的网站。
此外,C++ 支持允许内存管理在没有垃圾回收器的情况下安全隐式的编程技术。垃圾回收对于特定需求很有用,例如在无锁数据结构的实现中,以避免 ABA 问题,但不能作为资源管理的一般默认方式。我们不是说 GC 没用,只是在许多情况下有更好的方法。
C++11 提供了 GC ABI。
- 可移植性较差
- 通常效率更高(尤其是在平均对象大小较小或在多线程环境中)
- 能够处理数据中的“循环”(如果数据结构可以形成循环,引用计数技术通常会“泄漏”)
- 有时会泄漏其他对象(因为垃圾回收器必然是保守的,它们有时会看到看起来像是指向分配的随机位模式,尤其是在分配较大时;这可能导致分配泄漏)
- 与现有库配合得更好(由于智能指针需要显式使用,它们可能难以与现有库集成)
C++ 有哪两种垃圾回收器?
一般来说,C++ 的垃圾回收器似乎有两种类型
- 保守型垃圾回收器。 它们对栈或 C++ 对象的布局知之甚少,只是寻找看起来像指针的位模式。在实践中,它们似乎适用于 C 和 C++ 代码,尤其是在平均对象大小较小时。以下是一些示例,按字母顺序排列
- 混合垃圾回收器。 这些通常保守地扫描栈,但要求程序员提供堆对象的布局信息。这需要程序员做更多的工作,但可能会提高性能。以下是一些示例,按字母顺序排列
由于 C++ 的垃圾回收器通常是保守的,如果位模式“看起来像”是通向一个未使用的块的指针,它们有时可能会泄漏。此外,当指向一个块的指针实际上指向块的范围之外(这是非法的,但有些程序员就是必须突破界限;唉)并且(很少)当指针被编译器优化隐藏时,它们有时会感到困惑。在实践中,这些问题通常并不严重,但是向收集器提供有关对象布局的提示有时可以缓解这些问题。
在哪里可以获得有关 C++ 垃圾回收器的更多信息?
有关更多信息,请参阅垃圾回收器 FAQ。
`auto_ptr` 是什么,为什么没有 `auto_array`?
它现在被拼写为`unique_ptr`,它同时支持单个对象和数组。
`auto_ptr` 是一个旧的标准智能指针,已被弃用,仅为与旧代码的向后兼容性而保留在标准中。不应在新代码中使用。