您的位置:首页 > 编程语言 > C语言/C++

有关c++空类问题

2012-04-24 16:30 260 查看
《 effective c++ 》

空类是没有数据成员,没有函数的类,例如 class Empty { };

1. 空类或者空类对象的大小(无继承情况下):编译器会在空类中安插一个char,使得这个空类的两个objects得以在内存中配置独一无二的地址。只有当一个类为空的时候,编译器才会向该类中加入这样一个char!

2. 空类或者空类对象的大小(有继承但无虚继承情况下):继承的基类都是空类,大小是1,不管有多少个父类。

3. 空类或者空类对象的大小(单一虚继承情况下):对每一个虚继承基类,都有一个虚基类索引(或者偏移量、指针),占4字节

定义一个空的C++类,例如

class Empty

{

};

一个空的class在C++编译器处理过后就不再为空,编译器会自动地为我们声明一些member function,一般编译过去就相当于

class Empty

{

public:

Empty(); // 缺省构造函数

Empty( const Empty& ); // 拷贝构造函数

~Empty(); // 析构函数

Empty& operator=( const Empty& ); // 赋值运算符

Empty* operator&(); // 取址运算符

const Empty* operator&() const; // 取址运算符 const

};

如果你只是声明一个空类,不做任何事情的话,编译器会自动为你生成一个默认构造函数、一个拷贝默认构造函数、一个默认拷贝赋值操作符和一个默认析构函数。这些函数只有在第一次被调用时,才会别编译器创建。所有这些函数都是inline和public的。

默认的析构函数是非虚函数(除非基类有自己声明的虚析构函数)。而拷贝默认构造函数和默认拷贝赋值操作符知识是单纯将来源对象的每一个非静态成员拷贝到对象目标中(bitwise
copy)。

其中的默认拷贝赋值操作符只有在生成的代码合法并且有机会证明它有意义存在时才会生成。这就说明,如果你打算在一个“内含引用成员”或者“内含const成员”的类内支持赋值操作,就必须定义自己的默认拷贝赋值操作符。因为C++本身不允许引用改指不同的对象,也不允许更改const成员。

最后一种情况,当基类将自己的默认拷贝赋值操作符声明为private时,子类就不会产生自己的的默认拷贝赋值操作符。因为假如产生了这样的默认拷贝赋值操作符,它会试着去调用基类的默认拷贝赋值操作符去处理基类的部分,不幸的是,它没有权利。

你可以将拷贝构造函数或默认拷贝赋值操作符声明为private。这样明确声明一个成员函数,就阻止了编译器暗自创建的默认版本,而这些函数为private,使得可以成功阻止人们调用它。

上面的做法有一个隐患,因为类自身的member和friend还是可以调用这些private函数。有一个很刁钻的方法,“将成员函数声明为private而且故意不实现它们”,这样既阻止了默认函数的生成,而且如果你试着调用这些函数,就会得到一个链接错误。只声明,不定义,链接器报错。甚至在声明的时候,你连参数也不用写。

试着将上述的链接器错误提前到编译器也是可以的。我们专门设计一个类Unconpyable。
--------------------------------------------------------------------
class Uncopybale {

protected:

    Uncopyable() {}

    ~Uncopyable() {}

private:

    Ucopyable(const Uncopyable&)

    Uncopyable& operator=(const Uncopyable&)

};

--------------------------------------------------------------------

为了阻止对象被拷贝,我们唯一需要做的就是继承Uncopyable。这些函数的默认生成版本会尝试调用其基类的对应版本,那些调用会被编译器拒绝,因为它基类的拷贝函数是private。

Boost提供的noncopyable类也有类似的功能。

忠告:

为了驳回编译器自动提供的技能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的基类也是一种做法。

并不是所有的编译器都包成对象的内置类型成员会被自动初始化为0。永远在使用对象之前先将它初始化。确保每一个构造函数都将对象的每一个成员初始化。

别把赋值错当成初始化。C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前(对于内置类型对象可能不确定),这点对于非内置类型对象来说尤其关键。如果你没有在成员初始化列表(member initialization list)为其初始化,它们将调用自己的默认构造函数,然后才进入构造函数内部(很可能你会在这里给他们赋值)。在成员初始化列表中的初始化只是调用了拷贝构造函数一次,而在构造函数内部再为其赋值则在调用默认构造函数后又调用了一次拷贝构造函数。哪个效率高你当然知道。

所以,请用成员初始化列表进行初始化,虽然效率提高只针对于非内置类型成员,但是规定总是在初始化列表中列出所有成员变量,这样就省的有些未被列出的内置类型成员被忘记初始化。而有些时候,即使成员变量是内置类型,也必须要用成员初始化列表(成员变量为const或者reference,它们一定要有初值,而且不能被赋值)。

总之,总是使用成员初始化列表,这样或者必要,或者高效。有个例外,当你重载多个构造函数,每个构造函数有很多成员变量和基类的时候(这意味这成员初始化列表会很多、很长而且重复较多),可以将一些内置类型变量的初始化动作(它们的赋值和初始化不影响效率)移到一个私有函数中,供所有的构造函数调用。

规定:初始化顺序是基类早于派生类,类成员变量则以其声明顺序为准。所以成员初始化列表中列出的各个成员的顺序最好与声明的顺序相同。

最后说个不常见的问题:某个对象A的非静态成员变量初始化动作正好使用了另外一个编译单元(另外一个cpp)中的某个非静态对象B,你不能保证A在需要B的时候,B就已经被编译好而且产生了。解决的办法是将对象A和对象B都分别放到函数中(貌似是专门为每个这样的对象定制的对象),并且声明为static。这些函数返回的是静态对象的引用。这是单例模式的一种实现。在程序中以前需要对象引用的地方直接调用这些函数就好了。这种reference-returning函数对于处理多线程环境下的“竞速形势(race
conditions”的方法是:在程序的单线程启动阶段手工调用所有的reference-returning函数。

忠告:

1 为内置对象进行手工初始化,因为C++不保证初始化它们。

2 构造函数最好使用成员初始化列表(member initialization list),而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列次序应该和它们在class中的声明次序相同。

3 为免除“跨编译单元的初始化次序”问题,请以local static对象替换non-local static对象。

链接: http://blog.sina.com.cn/s/blog_5f76aaf20100cwlj.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息