C++语言编程规范-类的构造、赋值和析构
01
包含成员变量的类,须定义构造函数或者默认构造函数
说明:如果类有成员变量,没有定义构造函数,又没有定义默认构造函数,编译器将自动生成一个构造函数,但编译器生成的构造函数并不会对成员变量进行初始化,对象状态处于一种不确定性。
例外:如果这个类是从另一个类继承下来,且没有增加成员变量,则不用提供默认构造函数。
示例:如下代码没有构造函数,私有数据成员无法初始化:
class CMessage{public:void ProcessOutMsg(){//…}private:unsigned int msgid;unsigned int msglen;unsigned char *msgbuffer;};CMessage msg; //msg成员变量没有初始化msg.ProcessOutMsg(); //后续使用存在隐患//因此,有必要定义默认构造函数,如下:class CMessage{public:CMessage ():msgid(0),msglen (0),msgbuffer (NULL){}//...};
02
为避免隐式转换,将单参数构造函数声明为 explicit
说明:单参数构造函数如果没有用 explict 声明,则会成为隐式转换函数
示例:
{public:explicit Foo(const string &name):m_name(name){}private:string m_name;};ProcessFoo("zhangsan"); //函数调用时,编译器报错,因为显式禁止隐式转换
定义了 Foo::Foo(string &name),当形参是 Foo 对象实参为字符串时,构造函数 Foo::Foo(string &name)被调用并将该字符串转换成一个 Foo 临时对象传给调用函数,可能导致非预期的隐式转换。
解决办法:在构造函数前加上 explicit 限制隐式转换。
03
包含资源管理的类应自定义拷贝构造函数、赋值操作符和析构函数
说明:如果用户不定义,编译器默认会生成拷贝构造函数、赋值操作符和析构函数。
自动生成的拷贝构造函数、赋值操作符只是将所有源对象的成员简单赋值给目的对象,即浅拷贝(shallow copy);自动生成析构函数是空的。这对于包含资源管理的类来说是不够的:比如从堆中申请的资源,浅拷贝会使得源对象和目的对象的成员指向同一内存,会导致资源重复释放。空的析构函数不会释放已申请内存。
如果不需要拷贝构造函数和赋值操作符,可以声明为 private 属性,让它们失效。
示例:如果结构或对象中包含指针,定义自己的拷贝构造函数和赋值操作符以避免野指针。
class GIDArr{public:GIDArr(){iNum = 0;pGid = NULL;}~GIDArr(){if (pGid){delete [] pGid;}}private:int iNum;char *pGid;GIDArr(const GIDArr& rhs);GIDArr& operator = (const GIDArr& rhs);};
04
让 operator=返回*this 的引用
说明:符合连续赋值的常见用法和习惯。
String& String::operator=(const String& rhs){//...return *this; //返回左边的对象}string w, x, y, z;w = x = y = z = "Hello";
05
在 operator=中检查给自己赋值的情况
说明:自己给自己赋值和普通赋值有很多不同,若不防范会出问题。
示例:
class String{public:String(const char *value);~String();String& operator=(const String& rhs);private:char *data;};//自赋值,合法String a;a=a;//不好的例子:忽略了给自己赋值的情况,导致访问野指针String& String::operator=(const String& rhs){delete [] data; //删除data//分配新内存,将rhs的值拷贝给它data = new char[strlen(rhs.data) + 1]; //rhs.data已经删除,变成野指针strcpy(data, rhs.data);return *this;}//好的例子:检查给自己赋值的情况String& String::operator=(const String& rhs){if(this != &rhs){delete [] data;data = new char[strlen(rhs.data) + 1];strcpy(data, rhs.data);}return *this;}
在拷贝构造函数、赋值操作符中对所有数据成员赋值
说明:确保构造函数、赋值操作符的对象完整性,避免初始化不完全。
06
通过基类指针来执行删除操作时,基类的析构函数设为公有且虚拟的
说明:只有基类析构函数是虚拟的,才能保证派生类的析构函数被调用。
示例:基类定义中无虚析构函数导致的内存泄漏。
//如下平台定义了基类A,完成获取版本号的功能。class A{public:virtual std::string getVersion()=0;};//产品派生类B,实现其具体功能,其定义如下:class B:public A{public:B(){cout<<"B()"<<endl;m_int = new int [100];}~B(){cout<<"~B()"<<endl;delete [] m_int;}std::string getVersion(){ return std::string("hello!");}private:int *m_int;};//模拟该接口的调用代码如下:int main(int argc, char* args[]){A *p = new B();delete p;return 0;}
派生类 B 虽然在析构函数中进行了资源清理,但不幸的是该派生类析构函数永远不会被调用。由于基类A 没有定义析构函数,更没有定义虚析构函数,当对象被销毁时,只会调用系统默认的析构函数,故导致内存泄漏。
07
避免在构造函数和析构函数中调用虚函数
说明:在构造函数和析构函数中调用虚函数,会导致未定义的行为。在 C++中,一个基类一次只构造一个完整的对象。
示例:类 BaseA 是基类,DeriveB 是派生类
class BaseA //基类BaseA{public:BaseA();virtual void log() const=0; //不同的派生类调用不同的日志文件};BaseA::BaseA() //基类构造函数{log(); //调用虚函数log}class DeriveB:public BaseA //派生类{public:virtual void log() const;};
当执行如下语句:
DeriveB B;会先执行 DeriveB 的构造函数,但首先调用 BaseA 的构造函数,由于 BaseA 的构造函数调用虚函数 log,此时 log 还是基类的版本,只有基类构造完成后,才会完成派生类的构造,从而导致未定义的行为。同样的道理也适用于析构函数。
08
拷贝构造函数和赋值操作符的参数定义成 const 引用类型
说明:拷贝构造函数和赋值操作符不可以改变它所引用的对象。
更多推荐


所有评论(0)