C++ 幕后的函数

来源:互联网 发布:吃货老外常乐 知乎 编辑:程序博客网 时间:2024/04/28 16:59

《C++ 幕后的函数》原文转自:http://blog.csdn.net/armman/archive/2007/03/03/1519899.aspx

一个空类什么时候不是空类? ---- 当C++编译器通过它的时候。如果没有声明下列函数,体贴的编译器会声明它自己的版本。这些函数是:一个拷贝构造函数,一个赋值运算符,一个析构函数,一 对取址运算符。另外,如果你没有声明任何构造函数,它也将为你声明一个缺省构造函数。所有这些函数都是公有的。换句话说,如果这么写:

class Empty{};
和这么写是一样的:
class Empty {
public:
  Empty();                                 // 缺省构造函数
  Empty(const Empty& rhs);                  // 拷贝构造函数
  ~Empty();                              // 析构函数是否为虚函数看下文说明
  Empty&  operator=(const Empty& rhs);    // 赋值运算符
  Empty* operator&();                     // 取址运算符
  const Empty* operator&() const;
};

如果需要,这些函数就会被生成,但程序员很容易就需要它们。下面的代码将使得每个函数被生成:
const Empty e1;                     // 缺省构造函数
                                    // 析构函数
Empty e2(e1);                       // 拷贝构造函数
e2 = e1;                            //  赋值运算符
Empty *pe2 = &e2;                   // (非const)取址运算符
const Empty *pe1 = &e1;             // (const)取址运算符

 假 设编译器为程序员写了函数,这些函数又做些什么呢?是这样的,缺省构造函数和析构函数实际上什么也不做,它们只是能够创建和销毁类的对象(对编译器来说, 将一些 "幕后" 行为的代码放在此处也很方便)。注意,生成的析构函数一般是非虚拟的,除非它所在的类是从一个声明了虚析构函数的基类继承而来。缺省取址运算符只是返回对 象的地址。这些函数实际上就如同下面所定义的那样:

inline Empty::Empty() {}
inline Empty::~Empty() {}
inline Empty * Empty::operator&() { return this; }
inline const Empty * Empty::operator&() const
{ return this; }

 至 于拷贝构造函数和赋值运算符,官方的规则是:缺省拷贝构造函数(赋值运算符)对类的非静态数据成员进行 "以成员为单位的" 逐一拷贝构造(赋值)。即,如果m是类C中类型为T的非静态数据成员,并且C没有声明拷贝构造函数(赋值运算符),m将会通过类型T的拷贝构造函数(赋值 运算符)被拷贝构造(赋值)---- 如果T有拷贝构造函数(赋值运算符)的话。如果没有,规则递归应用到m的数据成员,直至找到一个拷贝构造函数(赋值运算符)或固定类型(例 如,int,double,指针,等)为止。默认情况下,固定类型的对象拷贝构造(赋值)时是从源对象到目标对象的 "逐位" 拷贝。对于从别的类继承而来的类来说,这条规则适用于继承层次结构中的每一层,所以,用户自定义的构造函数和赋值运算符无论在哪一层被声明,都会被调用。

看一个NamedObject模板的定义,它的实例是可以将名字和对象联系起来的类:
template<class T>
class NamedObject {
public:
  NamedObject(const char *name, const T& value);
  NamedObject(const string& name, const T& value);
  ...
private:
  string nameValue;
  T objectValue;
};

 因为NamedObject类声明了至少一个构造函数,编译器将不会生成缺省构造函数;但因为没有声明拷贝构造函数和赋值运算符,编译器将生成这些函数(如果需要的话)。

看下面对拷贝构造函数的调用:
NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1);      // 调用拷贝构造函数

 编 译器生成的拷贝构造函数必须分别用no1.nameValue和no1.objectValue来初始化no2.nameValue和 no2.objectValue。nameValue的类型是string,string有一个拷贝构造函数,所以no2.nameValue初始化时将 调用string的拷贝构造函数,参数为no1.nameValue。另一方 面,NamedObject<int>::objectValue的类型是int(因为这个模板实例中,T是int),int没有定义拷贝构 造函数,所以no2.objectValue是通过从no1.objectValue拷贝每一个比特(bit)而被初始化的。

 编译器为 NamedObject<int>生成的赋值运算符也以同样的方式工作,但通常,编译器生成的赋值运算符要想如上面所描述的那样工作,与此相 关的所有代码必须合法且行为上要合理。如果这两个条件中有一个不成立,编译器将拒绝为类生成operator=,就会在编译时收到一些诊断信息。

 例如,假设NamedObject象这样定义,nameValue是一个string的引用,objectValue是一个const T:
template<class T>
class NamedObject {
public:
  // 这个构造函数不再有一个const名字参数,因为nameValue
  // 现在是一个非const string的引用。char*构造函数
  // 也不见了,因为引用要指向的是string
  NamedObject(string& name, const T& value);
  ...                          // 同上,假设没有声明operator=
private:
  string& nameValue;           // 现在是一个引用
  const T objectValue;         // 现在为const
};
现在看看下面将会发生什么:
string newDog("Persephone");
string oldDog("Satch");
NamedObject<int> p(newDog, 2);     
NamedObject<int> s(oldDog, 29);    
p = s;                              // p中的数据成员将会发生些什么

 赋 值之前,p.nameValue指向某个string对象,s.nameValue也指向一个string,但并非同一个。赋值会给 p.nameValue带来怎样影响。赋值之后,p.nameValue应该指向 "被s.nameValue所指向的string" 吗,即引用本身应该被修改吗?如果是这样,就很罕见了,因为C++没有办法让一个引用指向另一个不同的对象。或者,p.nameValue所指的 string对象应该被修改吗?这样的话,含有 "指向那个string的指针或引用" 的其它对象也会受影响,也就是说,和赋值没有直接关系的其它对象也会受影响。这是编译器生成的赋值运算符应该做的吗?

 面对这样的难 题,C++拒绝编译这段代码。如果想让一个包含引用成员的类支持赋值,就得自己定义赋值运算符。对于包含const成员的类(例如上面被修改的类中的 objectValue)来说,编译器的处理也相似;因为修改const成员是不合法的,所以编译器在隐式生成赋值函数时也会不知道怎么办。还有,如果派 生类的基类将标准赋值运算符声明为private,  编译器也将拒绝为这个派生类生成赋值运算符。因为,编译器为派生类生成的赋值运算符也应该处理基类部分,但这样做的话,就得调用对派生类来说无权访问的基 类成员函数,这当然是不可能的。

原创粉丝点击