额,这个名字有点怪怪的=_=
ok,下面进入正题,为了演示方便,代码只写出简略的部分。
copy构造函数
class Base
{
public:
Base() {}
Base(const Base& )
{
cout<<"Base copy "<<endl;
}
};
class Derived: public Base
{
public:
Derived() {}
Derived(const Derived& ind) {}
};
重点关注一下复制构造函数。
虽然类中没有任何数据–但是我们说了,为了演示方便–现在假设base 类正确地编写了自身的变量复制。
当有下面的代码的时候:
Derived d;
Derived s(d);
我们希望它的行为是,会输出
Base copy
当然了,当发生derived class 复制构造时,我们也希望其base class 的复制构造能够正确地运行。
然后真实的行为却是,没有输出。
因为,当我们必须为derived class 编写复制构造函数的时候,必须小心的复制那些base class 的成分[1]。
正确的编写应该是:
class Derived: public Base
{
public:
Derived() {}
Derived(const Derived& ind):Base(ind) {} //在这里调用base class 的构造函数
};
与复制相关的另一个类成员函数就是operator=了。
operator=操作符
在上文中,已经指出,当我们必须要自己去编写copy 构造函数时,要小心地复制对象。
在自己编写operator= 操作符时也一样,下面直接给出代码:
class Base
{
public:
Base() {}
Base(const Base& )
{
cout<<"Base copy "<<endl;
}
Base& operator = (const Base&) //令operator = 返回自身的引用是一个正确的做法
//为什么?自己去查查资料吧:)
{
cout<<"Base = "<<endl;
return *this;
}
};
class Derived: public Base
{
public:
Derived() {}
Derived(const Derived& ind):Base(ind) {}
Derived& operator = (const Derived& ind)
{
Base::operator=(ind); //!!这个不能漏掉!!
return *this;
}
};
*类中的输出只是为了更好地看到类的行为,仅为演示而用,实际上没有什么意义。
看到红色的注释那句了吗,注意不要漏掉。
也许你又有疑问,那岂不是以后每次为derived class 编写时,都要自己去编写copy 构造函数和operator = 操作符,即使编译器提供的默认版本就足够满足我们的需求?
这个不用担心,编译器会正确地处理的,不如你自己试试:)
然而,operator = 的情况更复杂一点,要注意在operator = 中处理自我赋值。
现在我们单独取一个类作为例子,并给它安上一个指针,指向某个资源。嗯…………差不多像是这样的:
struct T
{
int i;
};
class Base
{
T* p;
Base(const Base&);
public:
Base()
{
p = new T{3};
}
~Base()
{
delete p;
}
Base& operator = (const Base& inb)
{
delete p;
p = new T(*inb.p);
return *this;
}
void print() //这个函数是为了验证结果
{
cout<<p->i<<endl;
}
};
ok,现在有这样的代码使用它们:
Base b1;
Base b2;
b2 = b1;
b2 = b2;//注意这句
b2.print();
虽然写的类没什么意义,但是单看使用的话,我们期待它输出3,是吧。
但是真实的行为是:0。
原因就在于,在operator = 中没有正确地处理自我赋值。
当有b2 = b2
这样的语句时(看下面注释)
Base& operator = (const Base& inb)
{
delete p;
//此时inb == *this, delete p 的时候,其实也删除了自身的资源
//那么后面的行为就是不确定的了
p = new T(*inb.p);
return *this;
}
怪异的是,运行时竟然没有报错,程序一如既往,兴高采烈地运行,虽然行为不可预知。
那么处理这个问题可以加入自我证同测试:
Base& operator = (const Base& inb)
{
if(this == &inb) return *this; //自我证同测试
delete p;
p = new T(*inb.p);
return *this;
}
那么代价是什么?
引入了一个控制分支。
问题是,自我赋值的发生概率会有多大呢?
如果每次都要进行一次判断,势必会影响效率。
于是,聪明的人想到了第二种做法:copy and swap技术
Base& operator = (const Base& inb)
{
T* temp = new T(*inb.p);
delete p;
p = temp;
return *this;
}
先构造一份副本,然后在交换指针值。
就算自我赋值也不会影响正确的行为。
如果你有顾虑到,当自我赋值的时候,先构造一份自身的副本,然后又给回自己,会不会多此一举?
你想一下自我赋值的发生概率,然后再考虑一下这样处理是否值得:)
[参考资料]
[1] Scott Meyers 著, 侯捷译. Effective C++ 中文版: 改善程序技术与设计思维的 55 个有效做法[M]. 电子工业出版社, 2011.
(条款11:在operator= 中处理“自我赋值”;
条款12:复制对象时勿忘其每一个成分)