使用 C++ 时,我们经常用指针表示对象,比如像下面这样:
Object *myObject = new Object;
而不是:
Object myObject;
或者在调用成员函数的时候,都会这样:
myObject->testFunc();
而不是:
myObject.testFunc();
对于现代 C++ (尤其是 C++ 11 之后),大量使用 new 动态分配是不明智的选择。
下面从两个方面来解释:
-
什么时候该使用 new?
-
什么时候该使用指针?
对象生命周期
上面两种创建对象的语句有什么不同呢?
对于 Object myObject;
该对象被创建在栈上,它的特点就是脱离作用域后会自动销毁。而对于 new Object()
,它会在堆上动态创建一个对象,它的特点就是即使脱离作用域,该对象也会一直存在,除非你手动释放(delete)它,否则就会出现内存泄漏。
什么时候该使用 new?
-
你需要延长对象生命周期。 意思是说你想一直使用某个地址位置的变量,而不是它的副本,对于后者,我们更应该使用
Object myObject;
的语法。 -
你需要很多内存。 大家都知道,栈空间比堆空间小的多。
当你确实要用动态内存分配的话,应该用智能指针或者其它的 RAII 技术来管理这部分资源。
什么时候该使用指针?
除了动态分配内存之外,原始指针还有其它用途。
-
引用语义(reference semantics): 有的时候,你希望函数传递进来的参数不是一份副本(copy),因为创建副本的代价很大。这个时候,你就可以通过指针。不过 C++ 11 已经有了移动(move)语义,这个问题就不用担心了。
-
多态(polymorphic): 对于多态类型,指针和引用可以避免对象被切片(slice)。切片的意思就是说:在函数传参处理多态变量时,如果一个派生类对象在向上转换(upcast),用的是传值的方式,而不是指针和引用,那么,这个派生类对象在 upcast 以后,将会被 slice 成基类对象,也就是说,派生类中独有的成员变量和方法都被 slice 掉了,只剩下和基类相同的成员变量和属性。
class Base { ... };class Derived : public Base { ... };void fun(Base b) { ... }void gun(Base* b) { ... }void hun(Base& b) { ... }Derived d;fun(d); // oops, all Derived parts silently "sliced" offgun(&d); // OK, a Derived object IS-A Base objecthun(d); // also OK, reference also doesn't slice
-
你希望表示对象是可选的(optional): 指针可以被赋值为 nullptr,也就是空的意思,你可以通过设置指针为 nullptr,来表达忽略该变量的含义。C++ 17 新增了
std::optional
,那么这个问题也可以得到解决。 -
你想通过解耦编译单元来减少编译时间: 如果对象都是指针指向的,那么只需要这个类型的前向声明就可以。这可以分离编译过程的各个部分,会显著提高编译时间。
#include "B.h" // 必须 include 来包含类 B 的定义class A; // 只需 A 的前向声明即可func(A* a, B a){}
- 兼容 C 库: C 库的接口大多都是以指针返回对象,这个时候你就不得不用指针。当然你也可以使用智能指针来封装它,这样使用起来就方便了。