常见错误49: 未能意识到c++语言中复制操作的固守行为----读书笔记《c++ gotchas...
时间:2010-08-15 来源:lzueclipse
C++语言对于其复制操作极其审慎,尤其对于class对象而言。如果你不自己写,编译器就会代劳。
注意,复制赋值操作和复制构造函数(以及其他任何的构造函数,还有所有的析构函数)都不从基类继承。
所有的class都得定义自己的复制操作。
复制构造函数的默认实现,就是执行一个按成员进行的初始化(member-by-member initialization)。
考虑下面这个简单class实现:
template
class NBString {
public:
explicit NBString(const char *name);
//…
private:
std::string name_;
size_t len_;
char s_[maxlen];
};
假定没有定义任何复制操作,那么编译器就会暗中替我们合成。它们具有public访问层级,而且是inline的。
NBString<32> a(“String 1”);
//…
NBString<32> b(a);
编译器合成的复制构造函数会执行一个按成员进行的初始化操作:
以a.name_初始化b.name_;
a.len初始化b.len_;
a.s_的各个元素来初始化b.s_的各个元素。
对于 b = a;
隐式合成的复制操作会执行一个按成员进行的赋值操作:
b.name_赋值为a.name_;
b.len_赋值为a.len_;
b.s_的各个元素赋值为a.s_中的对应元素。
以上隐式合成的复制操作将实现正确的复制语义。
再看一个例子,一个普通类,不是模板类:
class NBString {
public:
explicit NBString(const char *name, int maxlen = 32)
: name_(name), len_(0), maxlen_(maxlen),
s_(new char[maxlen]) {
s_[0] = ‘\0’;
}
~NBString() {
delete [] s_;
}
private:
std::string name_;
size_t len_;
size_t maxlen_;
char *s_;
};
//…
NBString c(“String 2”);
NBString d(c);
NBString e(“String 3”);
e = c;
隐式构造函数把c和d的s_成员设置成同一个地址空间,当d和c的析构函数一次delete [] s_时,
有“二次清除“的问题。 相似地,当e被赋值为c的时候,e的s_成员被设置成合c的s_成员一样的存储块,
对c的清除操作也会让e的s_成员陷入空悬(dangling)。
所以NBString正确的实现必须既要写复制构造函数,又要写复制赋值运算符,不能让编译器来做:
class NBString {
public:
//…
NBString(const NBString &);
NBString & operator = (Const NBString &);
private:
std::String name_;
size_t len_;
size_t maxlen_;
char *s_;
};
//…
NBString::NBString(const NBString &that)
: name_(that.name_), len_(that.len_), maxlen_(that.maxlen_),
s_(strcpy(new char[that.maxlen_], that.s_))
{
}
NBString &NBString::operator = (const NBString &rhs) {
if(this != rhs) {
name_ = rhs.name_;
len_ = rhs.len_;
maxlen_ = rhs.maxlen_;
char *temp = new char[rhs.maxlen_];
delete [] s_;
s_ = strcpy(temp, rhs.s_);
}
return *this;
}
对于复制操作有3种选择:
1)工程师显式撰写
2)由编译器隐式合成
3)工程师显式拒绝
举个禁止复制的例子:
class NBString {
public:
//…
private:
NBString(const NBString &);
NBString & operator = (const NBString &);
//...
};
仅仅声明private访问层级的复制操作,但不给出定义的做法可以禁止复制操作。
大多数的代码不具有对该class的复制操作的访问层级,任何在该class的成员函数以及友元中意外调用复制的操作,
都会在链接时出错。
下面这个例子列举了三种企图与编译器斗智斗勇的代码,确是徒劳无益的:
class Derived;
class Base {
public:
Derived &operator = (const Derived &);
virtual Base &operator = (const Base &);
};
class Derived : public Base {
public:
using Base::operator = ;//被遮掩(hidden)
template <class T>
Derived &operator = (const T&);//并非复制赋值操作
Derived &operator = (const Base &);//并非复制赋值操作
};
1)复制操作无继承语义,即使是using声明从非虚基类导入的复制运算符也不能阻止编译器生成一个,并把这个编译器生成的
复制运算符遮掩导入的实现。
在基类类型中显式地写出了派生类的名字,这是很糟糕的实现。
2)使用模板赋值成员函数也没用,:模板成员从不用于实现复制操作。
3)基类里的复制赋值运算符可以再派生类中以虚函数方式写,但改写后的那个复制运算符不再是一个复制赋值运算符。
只能说是一个以基类对象为参数的赋值运算符。
总结:要么自己撰写复制操作,要么编译器替你合成一个。