C++对象浅谈
last-edit-by: lungangfang 12/03/2006>
引言
感谢 training team 给我这个机会和大家交流我学习、使用C++的一点心得体会,也感谢大家抽出宝贵的时间来和我交流。
面向对象是C++最重要的性质之一。既然是面向对象,自然就离不开对象的创建、销毁和修改。今天我就想和大家探讨一下这方面的内容。
今天的内容是这样安排的:首先想讲一下“对象的生成”其中包括“对象生成时机” 和“限制对象的生成”,接下来再讲“对象的销毁”。然后再介绍一个充分利用构造、析构函数避免内存泄漏的的技巧:“resource acquisition is initialization”。之后再谈谈利用 std::auto_ptr 扩展这个技巧的使用范围。接着是异常处理机制中和对象创建、销毁相关的一些注意事项。最后是“对象的修改”,这一部分主要讨论用关键字 const 和 mutable 还有c++风格的强制类型转换来贯彻最小权限原则。
对象的生成
C++提供的以构造函数和析构函数为主的对象生成、销毁机制最大好处是使得程序员可以定制对象生成或销毁时的行为,编译器会确保这些行为在相应的情况下执行。这既简化了代码又避免了手工调用这些操作可能出现的遗漏和错误。不仅如此,程序员还可以控制是否允许对象被构造、复制。
对象生成时机
首先,我们看看什么时候会生成新的对象。
class B;
class C;
变量定义
C a; // constructor
C b = a; // copy constructor
函数调用
参数、返回值传值调用
B f(C input){ // copy constructor
B ret;
// ...
return ret; // copy constructor
}
类型转换
AClass a;
BClass b = static_cast<BClass>(a); // explicit type cast
// construct a temp obj of BClass according to a,
// then copy construct b from temp obj
前面几种场景相对来说还比较容易发现,自动类型转换产生临时对象相对而言就十分隐蔽了。
class AClass{
public:
AClass(int i);
};
// All the following code or legal.
// But chances are that you type them in by mistake。
AClass a = 5; // copy construct a with AClass(5)
a == 3; // compare a with AClass(3)
作为成员变量或基类
初始化列表
调用构造函数时,进入函数体之前,会先构造所有的成员变量(静态成员变量除外)和基类部分。
这个时候我们常常会用到初始化列表:
DerivedClass::DerivedClass(T a1, T a2)
: Base(1), m1(a1), m2(a2)
{ // ...
}
初始化列表的好处:
- 可以给成员变量、基类的构造造函数传递参数;
- 可以用来初始化成员常量;
- 避免了“先构造-再赋值”带来的问题(效率、复杂度)。
成员变量的初始化(构造)顺序
需要注意成员变量是按照它们在类定义中出现的顺序的被构造,而不是在初始化列表中的顺序。因为:初始化列表可以有多个(每个构造函数都可以有自己的初始化列表),但类定义只有一个。
对象的生命周期从构造函数后开始
所有成员构造完成后,对象还不算构造完成。只有构造函数的函数体成功执行完毕后一个对象才算是生成了。换句话说,构造函数没执行完,一些针对对象的机制不一定会作用在这个半拉子对象上。例如栈解退(stack unwinding)就不会析构这个对象。
限制对象的生成
从上一节的讨论可以看出:程序复杂到一定程度后,程序员很容易在无意间复制对象。有时这无关紧要,但有时这些意料之外的对象构造会带来严重后果。我们需要利用C++的语言特性来确保不发生不期望的对象构造。
使用关键字explicit——避免自动类型转换
用关键字"explicit"修饰只有一个参数的构造函数,避免它被用于自动类型转换:
class AClass
{
public:
explicit AClass(int i):m_i(i){ /* ... */};
// ...
};
AClass a(1); // ok
AClass b = static_cast<AClass>(2); // ok
// AClass b = 2; // error
// a == 3; // error
阻止编译器自动生成函数——避免拷贝构造和赋值
如果我们没有为某个类定义构造函数、拷贝构造函数(还有赋值操作符、析构函数),编译器会自动生成它们。这个设计在大多数情况下方便了程序员,但也带来相应的问题:一定要确保每个类要么有合适的拷贝构造函数和赋值操作符,要么就根本没有这两个函数,千万别允许编译器为你生成不合用的函数。
如果确实不需要这些函数,或者说某个类的拷贝构造、复制操作没有意义怎么办呢?这种情况下,一般的做法是将它们的拷贝构造函数和赋值操作符声明成私有成员,而且不定义它们(即只有函数声明没有函数体)。这样,如果对该类对象作相应操作,编译时就会报"没有权限"的错误。即使有权限,也会在链接时报"找不到函数体 "的错误。
class AClass{
public:
AClass();
private:
AClass(const AClass&);
// if you don't want copy ctor, you don't want assign operator
AClass& operator=(const AClass&);
// ...
};
抽象类——禁止实例化该类
有时候为某个类创建对象没有任何意义:
- 接口类:一个类被作为抽象的公共接口;
- 工具类:一个类提供了若干通用工具(函数)。
这两种情况下,我们可以将这个类定义成“抽象类(abstract class)”,也就是为其定义至少一个纯虚函数(pure virtual function)使之不能实例化。
class AClass{ // abstract class
virtual void f() = 0; // pure virtual function
};
BTW: “此类能且只能生成一个对象”的要求应当用设计模式中的单例模式来实现。
对象的销毁
对象生存期结束时,程序会调用它的析构函数。
生存期结束的几种情况
- 自动(auto)变量出了作用域
- 全局变量退出main函数之后
- 自动变量被栈解退(其实和第一种类似)
- 动态创建的变量被显式地(explicitly)delete
析构函数
基类的析构函数必须定义成虚函数
为什么?看例子:
int main(){
BaseClass* b = new DerivedClass();
// ...
delete b;
b = 0;
// ...
}
基类的析构函数无从知道子类的构造函数中又申请了哪些资源、做了哪些操作,也就无法做相应的清理工作。如果忘记将基类的析构函数定义成虚函数,程序很可能会core dump。
纯虚析构函数也必须定义函数体
基类的虚构函数也可以根据需要定义成纯虚函数。但是,即便是纯虚函数,也必须为这个析构函数定义函数体。
反过来,它的子类则不必明确定义虚构函数:编译器会帮我们生成一个。
class BaseClass{
public:
virtual ~BaseClass() = 0;
};
BaseClass::~BaseClass(){/*...*/}
class DerivedClass : public BaseClass {
public:
//~DerivedClass(){} // this dtor is NOT needed.
};
继承类的析构
不论基类的析构函数是虚函数还是纯虚函数,执行继承类的析构函数时都会调用基类的析构函数。
resource acquisition is initialization
C++程序中有个避免资源泄漏的常见方法巧妙地利用了构造函数和析构函数,那就是被称为“resource acquisition is initialization”的技巧。其核心思想是利用编译器会自动构造、析构自动变量这一特点,让编译器记得帮我们释放资源,以避免“手工操作”造成遗漏或错误。具体做法是定义一个资源对象,在其构造函数中申请资源,其析构函数中释放资源。这样需要申请资源时就定义一个资源对象,资源对象超出生存期后,编译器会去调用资源对象的析构函数来释放资源。
试比较下列三种方法:
方案一:易出错,可读性也不好(略)
多处负责释放资源;每处释放的资源和想申请的资源还不一样;各处释放资源数目也不一样;还有多个return语句。
if (! getResource1()){
// error handling
return;
}
if (! getResource2()){
// error handling
//free resource1
return;
}
if (! getResource3()){
// error handling
// free resource2
// free resource1
return;
}
// operation
// free resource3
// free resource2
// free resource1
方案二:出错概率稍低,但代码可读性更差(略)
多处释放资源,但每处只要释放自己申请的资源。
if ( getResource1()){
if ( getResource2()){
if (getResource3()){
// operation
// free resource3
} else { // error handling
}
free resource2
} else {// error handling
}
free resource1
} else {
// error handling
}
方案三:安全性、可读性大幅提高
采用“resource acquisition is initialization”(但也存在控制流程跳转的问题)
try {
ResClass1 a; // throw if fail applying res
ResClass2 b; // throw if fail applying res
ResClass3 c; // throw if fail applying res
// operation
}
catch (ex1& e){
// error handling
}
catch (ex2& e){
// error handling
}
catch (ex3& e){
// error handling
}
实例: mutex Lock
{
MutexLock lock(mutex);
// operation
} // unlock mutex automatically
std::auto_ptr
前面提到的“resource acquisition is initialization”有个应用前提是资源对象为自动变量。如果对象是动态创建(在heap上)的,就无法直接应用这个技巧了。因为编译器不会去自动销毁这个对象。这时可以把这个资源再封装一下,封装到一个局部变量中。STL已经为我们提供了这样的工具类:std::auto_ptr。 std::auto_ptr是STL中一个很重要的工具,它可以用来持有动态创建的对象。出作用域的时候它会自动删除所持有的对象。这样我们就可以把动态创建的对象看成是自动变量,不用担心忘记销毁它们。所以每次动态创建对象的时候,我们都应该看看是否能利用std::auto_ptr避免资源泄漏。
用法示例
void f(){ // a scope
std::auto_ptr<AClass> ap (new AClass());
// operations may throw
// return ap.get(); // we can also give up the ownership.
}
设计思路
From STL4.6.2 _auto_ptr.h:
explicit auto_ptr(_Tp* __px) { this->__set(__px); }
~auto_ptr() { delete this->get(); }
_Self& operator=(auto_ptr_ref<_Tp> __r) {
reset(__r.release()); // transfer owner ship (comments by lgfang)
return *this;
}
注意事项
- 它不能用来存放数组。可能是因为大部分时候std::vector和std::string就已经够用了,所以STL中没有与auto_ptr相对应的auto_array。不过如果真的有特殊需要的话,也很容易仿照std::auto_ptr写个auto_array处理动态数组。
- 它不能作为标准容器的元素:它采用了所有权转移的机制,因此不符合STL容器对元素的要求。(copyable、assignable and destroyable)
- 函数调用中的std::auto_ptr
传入:
auto_ptr<T> ap(new T(3));
f(ap);
// ap doesn't own the obj any longer传出:
auto_ptr<T> AClass::f() {
// ...
return m_ap; // m_ap gives up the ownership, danger to use it again.
}


被折叠的 条评论
为什么被折叠?



