C++我们必须要了解的事之具体做法(1)——构造、复制构造、析构、赋值操作符背后的故事

1. C++默认调用哪些函数

当类中的数据成员类型是trival数据类型(就是原c语言的struct类型)时,编译器默认不会创建ctor、 copy ctor、assign operator、dctor。

只有在这些函数被调用时,编译器才会创建他们。

这时候我们要自己创建构造函数,初始化内置数据类型。一般我们不需要复制控制函数,当需要时编译器合成的就很好。一般编译器合成的复制控制函数只是简单的复制成员,若能满足要求就不需要自己写。

当类中含有引用、const成员时,必须在初始化列表中初始化成员。且它们的copy cotr、assign operator都是不允许的。

三元素法则:一般有构造函数的类不需要析构函数。但是当类需要析构函数(往往是要删除构造函数初始化的资源如堆上的指针)时,一般同时也需要copy ctor、assign operaotr。

但是以下几种编译器一定会合成ctor:

类中含有类的(如vector等),编译器要调用其默认构造函数初始化成员。

类中含有虚函数的,编译器要初始化vptr。

类是虚继承的,要初始化虚基类在本类中的偏移量。

若只有这些类型,编译器合成的ctor就很好用。但是要注意,若有内置数据类型,我们需要自己创建ctor并初始化内置数据成员。

详细信息参见另外一篇博客:

2. 若不想使用编译器合成的copy ctor、copy assign需要明确的拒绝

在必要的时候编译器会为我们合成这两个函数,但是对于有些类我们并不需要它们(例如iostream中的类,或者是某种第一无二的资源等)。

这时我们需要明确拒绝:方法是将这两者声明为私有,不要定义它们。

class home {

public:

private:

home(const home&);                //声明而不定义它们

home &operator=(const home&);

};

当类企图拷贝home时编译器发出错误(没有访问权限)。对于member函数和friend函数链接器发出错误(有访问权限,但是有声明没有定义时)。

另外一种方法是定义一个base class:编译时发生错误,没有访问权限。

class uncopyable {

protected:   //允许derived类构造和析构

uncopyable() { }

~uncopyable() { }    //不需要为virtual,这不是多态基类。而且不含数据成员,可以实现空基类优化。

private:     //阻止coping

uncopyable(const uncopyable&);

uncopyable &operator=(const uncopyable);

};

class home : private uncopyable {   //private继承,不一定需要public继承

…                                              //不在声明copy构造函数、copy assign操作符

};

3. 为多态基类声明virtual析构函数

原则:

当我们编写的类会被作为基类,且会多态的使用这个基类(基类的指针或引用会处理继承类对象)时,这时我们需要将基类析构函数声明为virtual。

原因在于若基类的指针指向派生类(在堆上)时,在我们delete指针时(首先调用基类析构函数,发现不是virtual就不会再调用派生类析构函数了),会发生未定义的行为,

大多数情况下是只析构了基类对象,派生类没有被销毁,产生了局部销毁。

若类带有一个虚函数(允许派生类实现定制化),应该有虚析构函数。

对于不作为基类的类,我们就不应该声明虚析构函数。

但是有些类可以作为基类,但是不想具有多态性,我们就不应该声明虚析构函数。如上节的uncopyable, string, STL容器,它们的数据成员往往都是protect,我们可以继承,但是不具有多态性。

当然最后是不要派生它们。

4. 析构函数绝不应该抛出异常

C++不禁止析构函数抛出异常,但是不应该这么做。这样一定会带来过早终止或发生不明确行为。

在其他函数抛出异常时,stack unwind(栈展开)发生(目的是要catch异常,函数调用的现场信息等),会调用对象的析构函数,若析构函数再抛出异常,程序会过早终止或发生不明确行为。

若是正常的调用析构函数,析构函数抛出一个异常,处于异常调用点之后的代码不会不执行,其中可能会有回收资源,就发生了资源泄露。

然后再发生栈展开,又抛出一个异常程序会过早终止或发生不明确行为。

几种常见处理方式:

在类中有指针时:

class test {

public:

test(int val) : p(new int(val)) { }

~test() { delete p; }

private:

int *p;

};

当类中含有指针时,往往需要析构函数,两个copy函数。

此时new可能抛出异常bad_alloc。

当类析构函数需要处理一些必要的操作时,例如close_usb, close_db(关闭数据库),但是析构函数可能会抛出异常。

以数据库连接为例:

//负责数据库连接

class dbconnection {

public:

static dbconnection create();  //联机

void close();        //关闭联机,失败抛出异常

};

//管理dbconnection

class dbconn  {

public:

~dbconn()

{

db.close();
}

private:

dbconnection db;
};

我们可以这样使用:

dbconn dbc(dbconnection::create());

自动调用~dbconn();close();但这只是理想状态,若close()抛出异常,析构函数抛出异常就会出问题。

可能做法1:抛出异常就结束程序,调用abort()完成。

dbconn::~dbconn()

{

try {

db.close();

} catch(…) {

//记录一些必要信息,表明close()失败。

std::abort;

}
}

可能做法2:吞下异常

dbconn::~dbconn()

{

try {

db.close();

} catch(…) {

//记录一些必要信息,表明close()失败。

}

}

一般认为,吞下异常是个坏主意,因为压制了“某些动作失败”的重要信息。

但是有时候比直接终止好。

这两个可能的做法都不太好,一种较好的做法是从新设计dbconn,给客户一个处理该异常。

class dbconn  {

public:

void close()       //客户使用的新函数

{

db.close();

closed = true;
}

~dbconn()

{

if (!closed) {

try {

db.close();

} catch(…) {

//记录一些必要信息,表明close()失败。

}

}

private:

dbconnection db;

bool closed;
};

这样就给客户一个机会处理错误的机会,若客户没有调用这个close,析构函数在调用。

在这里db.close()会抛出异常,我们绝不应该在析构函数中抛出,而是像这里的close(),在一个普通函数中执行该操作,给客户处理这个异常的机会。

5. 绝不在构造和析构函数中调用virtual函数

在构造函数中调用虚函数:

虚函数涉及到基类与派生类。在基类的构造函数期间虚函数绝不会下降到派生类,即此时的虚函数不是虚函数。根本原因在派生类对象的基类构造期间,对象的类型是基类而不是派生类。

不只虚函数会被编译器解析为基类,运行期类型信息(dynamic_cast, typeid)也会被视为基类类型。

这么做的理由:

基类的构造函数执行早于派生类构造函数,基类构造函数执行时派生类成员尚未初始化。此时若要使用这些尚未初始化的成员变量,会造成不明确的行为。C++不允许你这么做。

在析构函数中调用虚函数:一旦派生类的析构函数开始执行,对象内的派生类成员变量就处于为定义值,C++时它们仿佛不存在。进入基类析构函数对象就成为基类对象,

而C++任何部分包括虚函数、dynamic_cast等也就这么看待它。

构造函数或析构函数可能会把需要执行的相同代码放在一个函数中,例如:init(),destroy();这些调用的函数可能调用虚函数,这个比较隐蔽,不容易察觉。

怎样知道是否调用了虚函数呢?

方法是:确定你的构造函数和析构函数都没有调用虚函数,而且它们调用的所有函数都符合这个要求。

解决这个问题的一个方法是:派生类构造函数传递必要的信息给基类构造函数,基类构造函数可以安全调用非虚函数。

class base {

public:

explicit base(const std::string &loginfo)

{

log(loginfo);

}

void log(const std::string &loginfo) const;        //此时这时非虚函数
};

class derived : public base {

public:

derived(para) : base(create_loginfo(para))  { }      //将log信息传递给基类构造函数

private:

static std::string create_loginfo(para);                 //静态成员函数,不会调用成员函数,可以用过传递一个形参使用成员函数。
};

6. opreator=

由于内置的赋值操作符返回的是左操作数的引用。所以正确形式是:

testclass &operator=(const testclass &rhs)

{

return *this;     //返回左操作数
}

要处理的问题是怎样处理“自我赋值”:

class bitmap {


};

class wrapper {

private:

bitmap *pb;    //指向从heap上分配的对象

};

wrapper &operator(const wrapper &rhs)

{

delete pb;

pb = new bitmap(*rhs.pb);

return *this;
}

在没有处理自我赋值时:pb所值的资源已经被回收,他所执行的值处于未定义状态(随机值),*rhs.pb是个已经删除的对象new不可能得到正确的指针。

处理自我赋值方法1:证同测试(identify test);

wrapper &operator(const wrapper &rhs)

{

if (&rsh == this) {       //自我赋值时什么都不做

reuturn *this;

}

delete pb;

pb = new bitmap(*rhs.pb);

return *this;
}

方法2:方法1的问题是不具异常安全性:若new抛出异常pb会指向已经被删除的bitmap。好的做法是使它具有“异常安全性”,附带防止自我赋值。

//通过合理安排语句顺序

wrapper &operator(const wrapper &rhs)

{

bitmap *old = pb;

pb = new bitmap(*rhs.pb);    //若抛出异常会处于原状态

delete old;

return *this;
}

可以把证同测试放在前面,但这么做会使代码变大,并降低执行速度。我们需要自己“自我赋值”发生频率。

方法3:copy and swap技术,这也是异常安全的一种方式。

wrapper &operator(const wrapper &rhs)

{

wrapper tmp(rhs);

swap(tmp);

return *this;
}

下面的做法与这个等同:使用实参副本,清晰性不够,但有时会产生更高效的代码

wrapper &operator(wrapper rhs)

{

swap(tmp);

return *this;
}

在函数会操作一个以上对象时,我们要保证对个对象是同一个对象时,其行为仍然正确。

7. coping 函数必须复制每个部分

若派生类构造函数没有调用基类的构造函数,则会调用基类的默认构造函数,若没有default构造函数则无法编译成功。

copy构造函数也有同样的问题,若复制构造函数没有调用基类的构造函数,则同样调用基类的默认构造函数,造成基类的数据成员仍然是基类的部分,

而派生类的数据成员则被const testclass &rhs中派生类数据初始化,造成数据的不一致。

copy assign 操作符与copy ctor有些不同,它不会修改基类数据成员,这些成员保持不变。

所以我们要做的是除了复制对象中的所有成员变量,和调用基类的适当的构造函数base(rhs)、调用基类的operator=(rhs)完成对基类的所有数据的初始化。

注意事项:

我们不能令copy assignment操作符调用copy 构造函数,因为copy构造函数是用来构造对象的,相当于我们在构造一个已经存在的对象。

同样,令copy构造函数调用copy assignment操作符也是不允许的,因为copy assignment操作符是作用于已经初始化的对象,而此时对象尚没有构造好。

正确做法:

将它们相近的代码放在一个private成员函数中,常常命名为init。

最最重要的是:我们要知道什么时候我们需要自己写coping函数,而不是使用编译器默认合成的。参见前面讲述。

时间: 09-13

C++我们必须要了解的事之具体做法(1)——构造、复制构造、析构、赋值操作符背后的故事的相关文章

C++我们必须要熟悉的事之具体做法(3)——类的设计与声明

1. 让接口被正确使用 最重要的方法是:保持与内置类型的一致性. 方法1:外覆类型(wrapper types) 例如在需要年月日时,使用 struct day { explicit day(int d) : val(d) { } private: int val; }; 方法2:函数替代对象 class month { public: static month jan() { return month(1); } … private: explicit month(int);    //禁止生

C++的那些事:面向对象

1 OOP概述 面向对象基于三个基本概念:数据抽象.继承和动态绑定.通过使用数据抽象,我们可以将类的接口与实现分离:使用继承,可以定义相似的类型并对其相似关系建模:使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象. 1.1 继承 继承是派生类与基类之间的关系,它们共享了一些公共的东西,而派生类特化了一些本质不同的东西.类与类之间的继承关系构成了继承层次.在C++中,基类必须指定希望派生类重定义哪些函数,定义为virtual的函数是基类期待派生类重新定义的,需要在派生

智能指针简介

智能指针用于解决常规指针所带来的内存泄露.重复释放.野指针等内存问题.智能指针基于这样的事实得以发挥作用:定义在栈中的智能指针,当超出其作用域时,会自动调用它的析构函数,从而可以释放其关联的内存资源. 之前C++标准库中定义的智能指针std::auto_ptr<T>,因其设计存在缺陷,所以已不再推荐使用.C++11引入了新的智能指针:unique_ptr.shared_ptr和weak_ptr. 一:unique_ptr unique_ptr类似于auto_ptr.两个unique_ptr实例

C++基础知识---构造函数 &amp; 析构函数 &amp; 虚拟析构函数

问题: 类需要一个无参的构造函数么? 类需要一个析构函数么? 类的构造函数需要初始化所有的对象成员么? 类需要一个虚析构函数么? 有些类需要虚析构函数只是为了声明他们的析构函数是虚的.绝不会用作基类的类是不需要虚析构函数的:任何虚函数只在继承的情况下才有用.假设B为父类,D为子类,B何时需要一个虚析构函数?只有有人肯呢过会对实际指向D类型对象的B*指针执行delete表达式,你就需要给B加上一个虚析构函数,即使B和D都没有虚函数,这也是需要的 B* bp = new D; Delete bp; 

等待线程结束

正常环境下等待线程结束 如果需要等待线程结束,就在线程的实例对象上调用join().在管理线程之创建线程最后的例子中,用my_thread.join()代替my_thread.detach()就可以确保在函数终止前.局部变量析构前,线程会终止.在这种情况下,用分开的线程来运行函数就没有什么意义了.因为在等待my_thread终止时,这个线程就不做任何事情了.在实际的工程应用中,要么这个线程做自己的事,要么创建多个线程等待它们结束. join()是简单粗暴的,或者你等待线程终止,或者你不等待.如果

第28件事 挖掘用户真实需求的6大撒手锏

如何挖掘用户的需求,即如何挖掘出用户想要的是什么,这是所有产品经理的必修课,也是最难修炼的一门课,这门课需要极高的悟性. 1.人性法在第27件事中,对用户的人性进行了一些剖析,用户的人性主要表现在矛盾.虚伪.贪婪.欺骗.幻想.疑惑.简单.善变.好强.无奈.孤独.脆弱.自私.无聊.变态.冒险.好色.善良.博爱.诡辩.懒惰.快乐.好玩.猎奇.嫉妒.执著.恐惧.欲望等方面,本质上来说,人其实还是一种“高级动物”.获取用户需求的传统方法很多,比如调查问卷.用户访谈.焦点小组等,但是这些方法显得有些过时了

常见C++面试题(三)

strcpy和memcpy有什么区别?strcpy是如何设计的,memcpy呢? strcpy提供了字符串的复制.即strcpy只用于字符串复制,并且它不仅复制字符串内容之外,还会复制字符串的结束符.(保证dest可以容纳src.) memcpy提供了一般内存的复制.即memcpy对于需要复制的内容没有限制,因此用途更广. strcpy的原型是:char* strcpy(char* dest, const char* src); char * strcpy(char * dest, const

Lisp简明教程

此教程是我花了一点时间和功夫整理出来的,希望能够帮到喜欢Lisp(Common Lisp)的朋友们.本人排版很烂还望多多海涵! <Lisp简明教程>PDF格式下载 <Lisp简明教程>ODT格式下载 具体的内容我已经编辑好了,想下载的朋友可以用上面的链接.本人水平有限,如有疏漏还望之处(要是有谁帮我排排版就好了)还望指出!资料虽然是我整理的,但都是网友的智慧,如果有人需要转载,请至少保留其中的“鸣谢”页(如果能有我就更好了:-)). Lisp简明教程 整理人:Chaobs 邮箱:[

(转)从内存管 理、内存泄漏、内存回收探讨C++内存管理

http://www.cr173.com/html/18898_all.html 内存管理是C++最令人切齿痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的自由,C++菜鸟的收获则是一遍一遍的检查代码和对 C++的痛恨,但内存管理在C++中无处不在,内存泄漏几乎在每个C++程序中都会发生,因此要想成为C++高手,内存管理一关是必须要过的,除非放弃 C++,转到Java或者.NET,他们的内存管理基本是自动的,当然你也放弃了自由和对内存的支配权,还放弃了C++超绝的性能