Effective C++读书笔记(4) (一)

2014-11-24 12:20:03 · 作者: · 浏览: 2

条款05:了解C++默默编写并调用哪些函数

Knowing what functions C++ silentlywrites and calls

一个 emptyclass(空类)什么时候将不再是 empty class(空类)?

答案是当 C++ 处理过它之后。如果你自己不声明一个拷贝构造函数,一个 copyassignment运算符和一个析构函数,编译器就会声明一个它自己的版本。此外,如果你没有构造函数,编译器就会为你声明一个缺省构造函数,所有这些函数都被声明为 public 和 inline。因此:

class Empty{};

在本质上和你这样写是一样的:

class Empty {

public:

Empty() { ... } // default constructor

Empty(const Empty& rhs) { ... } // copy constructor

~Empty() { ... } // destructor

Empty& operator=(const Empty& rhs) {... } // copy assignmentoperator

};

这些函数只有在它们被调用的时候编译器才会创建。下面的代码会促使每一个函数生成:

Empty e1; // default constructor & destructor

Empty e2(e1); // copy constructor

e2 = e1; // copy assignment operator

缺省构造函数和析构函数主要是给编译器一个地方放置 “幕后代码”的,如调用基类和非静态数据成员的构造函数和析构函数。注意,生成的析构函数是non-virtual的,除非它所在的类是从一个基类继承而来,而基类自己声明了一个 virtual destructor虚拟析构函数(这种情况下,函数的virtualness(虚拟性)来自基类)。对于拷贝构造函数和拷贝赋值运算符,编译器生成版本只是简单地将来源对象的每一个non-static成员对象拷贝至目标对象。

template
public:
NamedObject(const char *name, const T& value);
NamedObject(const std::string& name, const T& value);

...

private:
std::string nameva lue;
T objectValue;
};

如果你已经为一个要求实参的类设计了构造函数,你就无需担心编译器会再添加一个default构造函数而遮掉你的版本。NamedObject 既没有声明拷贝构造函数也没有声明拷贝赋值运算符,所以编译器将生成这些函数(如果它们被调用的话):

NamedObject no1("SmallestPrime Number", 2);

NamedObject no2(no1); // calls copy constructor

编译器生成的拷贝构造函数会用 no1.nameva lue 和 no1.objectValue 分别初始化 no2.nameva lue 和 no2.objectValue。 nameva lue 的类型是 string,标准 string 类型有一个拷贝构造函数,所以将以 no1.nameva lue 作为参数调用 string 的拷贝构造函数初始化 no2.nameva lue。而另一方面,NamedObject::objectValue 的类型是 int(在这个模板实例化中 T 是 int),而 int 是 内置类型,所以将通过拷贝 no1.objectValue 的每一个bits初始化 no2.objectValue。编译器为 NamedObject 生成的拷贝赋值运算符本质上与拷贝构造函数有同样的行为。

例如,假设 NamedObject 如下定义,nameva lue 是一个 reference to string,而 objectValue 是一个const T:

template
class NamedObject {
public:
//以下构造函数不再接受const名称,因为nameva lue如今是个//reference-to-non-const string
NamedObject(std::string& name, const T& value);

...// 如前,假设并未声明operator=

private:
std::string& nameva lue; // 这里是reference
const T objectValue; // 这里是const
};

现在,考虑下面会发生什么:

std::string newDog("Persephone");

std::string oldDog("Satch");

NamedObject p(newDog, 2);

NamedObject s(oldDog, 36);

p = s; // 现在p的成员变量会发生什么?

注意nameva lue ,C++ 并没有提供使一个reference引用指向另一个对象的方法。此时C++ 拒绝编译那一行赋值代码。如果你希望一个包含引用成员的类支持赋值赋值,你必须自己定义拷贝赋值运算符。对于含有const 成员的类,编译器会有类似的行为(就像本例中的 objectValue)。更改 const 成员是不合法的,所以编译器隐式生成的赋值函数无法确定该如何处理。

最后还有一种情况,如果基类的拷贝赋值运算符声明为 private,编译器拒绝为从它继承的派生类生成隐式拷贝赋值运算符。毕竟,编译器为派生类生成的拷贝赋值运算符想象中可以处理其 baseclass 成分,但它们当然无法调用那些派生类无权调用的成员函数。

· 编译器可以隐式生成一个 class(类)的 default constructor(缺省构造函数),copy constructor(拷贝构造函数),copy assignmentoperator(拷贝赋值运算符)和 destructor(析构函数)。

条款06:若不想使用编译器自动生成的函数,就该明确拒绝

Explicitly disallow the use ofcompiler-generated functions you do not want

房地产代理商出售房屋,服务于这样的代理商的软件系统自然要有一个类来表示被出售的房屋.每一件房产都是独特的,因此最好让类似这种企图拷贝 HomeForSale对象的行为不能通过编译:

class HomeForSale { ... };

HomeForSale h1;

HomeForSale h2;

HomeForSale h3(h1); // 企图拷贝h1,不该通过编译

h1 = h2; //企图拷贝h2,不该通过编译

阻止这一类代码的编译并非那么简单。因为如果你不声明拷贝构造函数和拷贝赋值运算符,而有人又想调用它们,编译器就会替你声明它们。另一方面,如果你声明了这些函数,你的类依然会支持拷贝,而我们此时的目的却是防止拷贝!

解决这个问题的关键是所有的编译器生成的函数都是 public的。为了防止生成这些函数,你必须自己声明它们,但是没有理由把它们声明为public的。相反,应该将拷贝构造函数和拷贝赋值运算符声明为private的。通过明确声明一个成员函数,可以防止编译器生成它自己的版本,而且将这个函数声明为 private的,可以成功防止别人调用它。

声明成员函数为 private 却故意不去实现它确实很好,在 C