C++相对于C的一个非常大的优势就是你不必太担心内存。如果你的代用到了对象,只需要确信每个类可以好好管理自己的内存。通过构造与析构函数,编译器通过告诉你什么时候去做来帮助你管理内存。在类内隐藏了对内存的管理在使用上带来了很大的不同,就像标准库类展示的那样。然而,对于一些应用或者遗留代码,你可能也会碰到需要在底层处理内存。不管是遗留代码、效率、排错或者是好奇,懂得一些对于原始字节的操作技巧还是很有帮助的。
1、指针的算术运算
C++编译器使用指针的声明类型来允许你执行指针的算术运算。如果你声明了一个整数指针,然后对指针进行加1操作,指针就会在内存中以整型的大小向前移动,而不是以一个单独字节的长度进行移动。对于数组来说这种类型的操作非常有用,因为它们在内存中包含了顺序的同类型的数据。例如,假设你声明了一个在栈上的整型数组:
int* myArray { new int[8] };
你应该对于下面的将索引值为2的元素进行赋值的语法很熟悉了吧:
myArray[2] = 33;
对于指针算术运算,你可以同样地使用下面的语法,它包含了一个myArray数组的“2个整数之前”的内存的指针,然后通过引用取值来对其进行赋值:
*(myArray + 2) = 33;
作为访问单个元素的另一种语法,指针算术运算并不那么吸引人。它真正的能力在于事实上像myArray+2这样的表达式仍然是一个指向Int的指针,可以代表一个小一点的整型数组。
让我们看一个宽字符串的例子。宽字符串会在以后讨论,在我们要表达的观点上其细节并不重要。现在,只要知道宽字符串支持Unicode字符就可以了,例如,日文字符串。wchar_t类型是一个能够放置这样的Unicode字符的字符类型,一般来说要比char大;也就是说,其长度不仅是一个字节。告诉编译器一个字符串常量是一个宽字符串常量,以L为前缀。例如,假设你有下面的宽字符串:
const wchar_t* myString { L"Hello, World" };
假定你还要有一个以一个宽字符串为输入参数的函数,返回一个包含了输入参数的大写版本的新字符串:
wchar_t* toCaps(const wchar_t* text);
你可以通过将myString传递给这个函数来将其转化为大写。然而,如果你只是要将myString的部分进行大写转化的话,你可以使用指针算术运算来指向字符串的后面的一部分。下面的代码调用了对World部分的宽字符串的toCaps()函数,简单地在指针上进行了加7的操作,即使wchar_t通常是大于1个字节的:
toCaps(myString + 7);
另一个指针算术运算的用处就是减法。将一个指针从另一个同类型的指针相减会给出两个指针之间指向类型的元素数量,而不是它们之间的绝对字节数。
2、客户化内存管理
你碰到的99%的情形(有人可能会说100%),在c++中内置的内存分配工具就足够了。在这背后,new与delete干了所有以适合大小的chunk进行内存分发的所有工作,维护了可用内存区域的列表,在删除时释放内存chunk到列表中。
当资源限制特别严格时,或者在特别特殊的情况下,例如管理共享内存,实现客户化内存管理可能就是一个可行的选项。别担心----并不像听起来那么吓人。本质上讲,自己管理内存意味着类分配一大串内存,然后根据需要进行小块的内存发放。
这个方法怎么会好一些呢?管理自己的内存可以有效地减少开销。当你使用new来分配内存时,程序也需要留出一小块空间来记录分配了多少内存。用这种方式,当你调用delete时,合适大小的内存可以被释放。对于大部分对象来说,开销要比内存分配小的多得多,不产生本质性的影响。然而,对于小的对象或者拥有大量对象的程序来说,这个开销可能会有影响。
当你自己管理内存时,你会预先知道每个对象的大小,也就能够避免每个对象的额外开销。对于大量的小对象来说会有非常大的不同。执行客户化的内存管理需要对new与delete操作符进行重载,这个主题我们以后再讨论。
3、垃圾回收
在支持垃圾回收的环境,程序员如果有的话,也很少去显式地对对象进行释放内存的操作。事实上,不再被引用的对象会被运行时库在适当时进行自动清理。
垃圾回收并不像C#与Java一样被集成至c++语言中。在现代c++中,你会用智能指针来管理内存,而在遗留代码中,你会看到在对象层次用new与delete进行内存管理。像shared_ptr(我们会在后面进行讨论)这样的智能指针提供了与内存垃圾回收非常类似的功能;也就是说,对于特定的资源,当最后的shared_ptr实例被破坏时,在这个时点上资源也被破坏了。在c++中实现垃圾回收是可能的,但有没有那么容易,但是,在释放内存的任务中自己进行释放可能会带来新的令人头痛的问题。
垃圾收集的一个方法被叫做标记且清除。用这个方法,垃圾回收器不断地检查程序中的每个单独的指针,标注所引用的内存依然被使用的事实。在每个循环的结束,任何不再被标注的内存就完成了使命,会被释放掉。在c++中实现这样的算法并不麻烦,但是,如果没有弄对,会比使用delete更容易出错!
尝试以安全且容易的技术进行垃圾回收已经在C++中进行,但是即使是一个完美的垃圾回收在c++中的实现的出现,也不可能做到对所有应用都适用。其中垃圾回收的负面总结如下:
- 在垃圾收集器还在运行时,程序不响应了。
- 对于垃圾收集器,你却有不确定的析构函数。因为在垃圾回收之前,对象不会被破坏,当对象离开其作用范围时,析构函数不会立刻执行。这就意味着被析构函数完成的清理资源(比如关闭文件,释放锁资源等等)会在未来的某个不确定的时间执行。
写一个垃圾回收装置非常难。不可避免地会出错,还容易出错,更可能的是会变慢,如果你真的想在应用中使用垃圾回收,推荐你调研可重用的既存的专业的垃圾回收库。
4、对象池
垃圾收集就像为野营购买盘子,把用过的盘子丢在露营地,在某个时间点有人把它们捡起来扔掉。当然了,内存管理也有一个更生态化的方法。
对象池就是相应的循环池。你买了一些盘子,在用过一个以后,洗好之后留作后用。对象池对于你需要使用同类型的对象多次的情况的理想选择,生成每一个对象都会有开销产生。
对于使用对象池进行性能提升的细节我们以后再讨论。