现在的位置: 首页 > 综合 > 正文

【程序语言】C++ 构造函数 拷贝构造函数 =操作符重载 析构函数 详细分析

2018年04月29日 ⁄ 综合 ⁄ 共 2590字 ⁄ 字号 评论关闭

 当你满怀轻松得心情写下一个简单的C++类:

class MyClass
{
private:
   int a;
}

你心里也许十分高兴,因为你恐怕找不到更简单的类了,写简单的东西总是轻松越快的。但是C++却附赠了些东西给你:

public:
MyClass()                         {/* 俺是无为之王——默认构造             */};
MyClass(MyClass &tmp)             {/* 俺是偷懒惯犯——浅拷贝               */};
MyClass& operator =(MyClass &tmp) { return *tmp; /* 俺是浅拷贝的赋值马甲 */};
~MyClass()                        {/* 俺是默认析构,俺压根儿就没想过做事情  */};

好了,你的轻松之旅暂时结束了,因为这4个默认的东东如果用不好可能让你头很大。

1.默认构造函数,往往有无数的新手,尤其是玩过高级语言的,理所当然地认为 int -->0  double-->0.0 bool-->false object-->null char-->\0 是默认的,这在一些高级语言中确实是的。但C++的类中不提供这种默认初始化!!!所以,请注意手写。

 

2.我们想把对象A复制到对象B,于是我们写了 MyClass B(A); 我们期望得到一份A的完美副本,这样的做法是对的吗?在此例中我们完成了这项任务。但如果对象是

class MyClass
{
private:
   int *a;
}

上面的写法就出问题了,因为A::a和B::a指向的位置是同一个“堆”位置,也就是说A、B对象的指针指向了同一个地方!那么有什么后果呢!

(1)你修改A::a所指向的值,那么B::a所指向的值也随之发生了变化。

(2)当某一处A的析构函数调用的时候,如果析构函数有 delete a; 或者 free(a); 操作的话,那么如果再访问B::a所指向的位置将发生程序崩溃或者不可知结果。

所以,当类中有 指针 或者 类似指针的系统资源(本质也是指针的),应当重载拷贝构造函数,实现“深拷贝”。

 

3.我们之后,幸福地写下MyClass C; C = B; 我们都已经重载了拷贝构造函数,实现了深拷贝,我们现在得到的 C 应该是 B 的完美副本了吧,NO!

(1) MyClass C = B; 这条语句才会调用拷贝构造函数,而 MyClass C; C=B; 调用的是 = 操作符!

(2)= 操作符在默认的情况下,做的事情和浅拷贝是一致的,必须重载实现深拷贝,才会具有深拷贝能力! 
(3)注意重载 = 的写法,如果不注意,系统会在 “传值时” 和 “返回值时” 创建附加的临时对象:

MyClass  operator = (MyClass  tmp)
{/*错误的写法,调用时:“拷贝构造函数构造临时对象--> =操作符 --> 拷贝构造函数构造临对象 ”*/};
MyClass& operator = (MyClass  tmp)
{/*错误的写法,调用时:“拷贝构造函数构造临时对象--> =操作符                             ”*/};
MyClass  operator = (MyClass &tmp)
{/*错误的写法,调用时:“                           =操作符 --> 拷贝构造函数构造临对象 ”*/};
MyClass& operator = (MyClass &tmp)
{/*正确的写法,调用时:“                           =操作符                     ”*/};

4.析构函数,这是用来在对象摧毁前被自动调用的函数,一般情况下是,大家习惯在对象中有指针的情况下,在析构函数中做 delete 或者 free 操作。但是其实这样做的想法是好的,那就是在对象被摧毁前,做系统不会自动做的清理工作,而且C++的析构函数的设置之初的目的也是这个。但不得不说在析构函数中做 delete 和 free 操作是一件十分危险的事。如果清理不好逻辑,出问题那是十分常见的:

(1)出现莫名其妙的NULL指针

(2)程序中不得不加入大量NULL判断语句

(3)这样做后,并不能抑制空指针的出现,只是保证出现空指针有出错处理而已!程序的健壮性没有本质变化!

 

那么一般富有经验的做法是什么样的呢?

(1)对象中,只存 “原始值” 和 “对象指针”。曾有人提议只存“对象”这样就可以只在“栈”上分配空间,一是提高效率,二是这样可以最简单的避免深浅拷贝和赋值操作符问题,三来是析构函数可以什么都不写了。但事实是这样做缺乏灵活性,只能在初始化时构造对象,很多时候行不通!

(2)手动写 “构造函数” 一般遵循:int-->0 double-->0.0 bool-->false char-->\0 *object-->NULL的原则,不要做“多余的”“令人费解的”的初始化,简单才是优雅!

(3)拷贝构造函数要么是“私有的”——禁止复制,要么是“公共的”——必须实现深拷贝。无数的情况下,你要么不会想复制,你直接取个对象的引用就可以了;如果你做复制操作,你基本上都是需要一个完美的复制品!一个普遍简化的原则是——“拒绝浅拷贝”。简单说就是:哥要么不玩,要玩儿就玩“深”的!

(4)重载=操作符,原则同拷贝构造函数,不整则已,整就整深的!同时注意写法,别写个错的低效(还可能出现错误,临时对象销毁时会调用析构函数!)版本!

(5)对于指针,我的观点是new 和 delete成对出现,而且像括号一样的嵌套,不在析构函数中删除对象指针!指针都在对象外删除!举个例子:

class Car
{
private:
Wheel  *wheel;
Engine *engine;
}

对象中有指针,那么构造Car 的一个实例时,我们通常都是 先造轮子,再造发动机,最后再组装成汽车。在汽车内部造轮子,汽车销毁时再销毁轮子其实用面向对象观点来看是荒谬的。如下的代码是一种大家可以验证的使用方式,供大家参考:

Wheel *wheel = new Wheel();
   Engine *engine = new Engine();
     Car car(wheel,engine);
   delete engine;
delete wheel;

这样做,而不是把对象的构造带入另一个对象的内部,然后等着出现各种问题。将一个对象的构造带入另一个对象:

①如果对象自己不清理,那么没人帮你清理!

②如果写了清理的析构函数,那么就可能出现问题,尤其是系统中有浅拷贝对象的时候。

③某些临时对象拥有和你的对象相同的指针,那么销毁时,也会调用析构函数,那么你本对象就成了一个程序黑洞,“谁敢调用谁就挂”!

 

最后对上述内容最个简化:

(1)对象存“简单值”和“对象指针”

(2)手写构造函数初始化

(3)拷贝和=,要不 是私有的,要不 是“深”的

(4)析构函数不作为

(5)new delete 成对出现

 

 

抱歉!评论已关闭.