C++ 的空类

今天面试一个公司的初级c++职位,一个印象很深的题目:

C++ 的空类包含哪些成员函数??


现提供转载2个blog供参考:


blog1:


定义一个空的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这样的基类也是一种做法。

C++】使用对象前请先正确初始化 ——《Effective C++》读书笔记3

2009-03-15 06:28

并不是所有的编译器都包成对象的内置类型成员会被自动初始化为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对象。



blog2:

class Empty

{

  public:

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

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

      ~Empty(); // 析构函数

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

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

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

};

默认构造函数 
析构函数 
拷贝构造函数 
赋值运算符(operator=) 
取址运算符(operator&)(一对,一个非const的,一个const的) 

     当然,所有这些只有当被需要才会产生。比如你定义了一个类,但从来定义过该类的对象,也没使用过该类型的函数参数,那么

基本啥也不会产生。在比如你从来没有进行过该类型对象之间的赋值,那么operator=不会被产生。

class Empty
{
public:
          Empty();                  //   缺省构造函数

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

          ~Empty();                 //   析构函数

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

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

};


例如有以下class:

class StringBad
{
 private :
         char   * str;
          int len;

 public :
           StringBad( const   char   * s);
           StringBad();
           ~ StringBad();

} ;


在构造函数和析构函数定义当中有如下定义:

StringBad::StringBad( const   char   * s)
{
         len = std::strlen(s);
         str =   new   char [len +   1 ];

}

StringBad::StringBad()
{
        len =   4 ;
        str =   new   char [ 4 ];

}

StringBad:: ~ StringBad()
{

        delete [] str;
}

    那么在程序当中如果有以下代码:

    StringBad sports( " Spinach Leaves Bow1 for bollars " );

    StringBad sailor = sports;

    以上的第二条初始化语句将会调用什么构造函数?记住,这种形式的初始化等效于下面的语句:

    StringBad sailor = StringBad(sports);

    因为sports的类型为StringBad,因此相应的构造函数原型应该如下:

    StringBad( const StringBad & );

      当我们使用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数(称为复制构造函数,因为它创建对象的一

个副本)

      现在我们来看看我们没有定义复制构造函数的情况下调用隐式复制构造函数将会出现什么情况。

      从构造函数定义的代码片断可以看到,当中使用new操作符初始化了一个指针str,而隐式的复制构造函数是按值进行复制

的,那么对于指针str,将会进行如下复制:

      sailor.str = sports.str;

       这里复制的不是字符串,而是一个指向字符串的指针!也就是说,我们将得到两个指向同一个字符串的指针!由此会产生的

问题将不言而喻。当其中一个对象调用了 析构函数之后,其str指向的内存将被释放,这个时候我们如果调用另一个对象,其

str指向的地址数据会是什么?很明显将会出现不可预料的结果。

       所以由此可见,如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,

被称为深度复制。因为默认的浅复制(或成为成员复制)仅浅浅的赋值指针信息。

      我们再看以下代码片断,我们稍做修改:

      StringBad headline1( " Celery Stalks at Midnight " );

      StringBad knot;

      knot = headline1;

       这里的最后一行将与以上例子有所区别,现在是将已有对象赋给另一个已有对象,这将会采取其他操作,即使用重载的赋值

操作符。(我们需要知道的是:初始化总是会调用复制构造函数,而使用=操作符时也可能调用赋值操作符)因为C++允许对

象赋值,这是通过自动为类重载赋值操作符实现的。其原型如下:

      Class_name & Class_name:: operator   = ( const Class_name & );

      它接受并返回一个指向类对象的引用。

      与隐式的复制构造函数一样,隐式的对象赋值操作符也会产生同样的问题,即包含了使用new初始化的指针成员时,只会采

用浅复制。所以我们需要使用同样的解决办法,即定义一个重载的赋值操作符来实现深度复制。

      所以综上所述,如果类中包含了使用new初始化的指针成员,我们应该显式定义一个复制构造函数和一个重载的赋值

操作符来实现其深度复制,避免由此带来的成员复制问题

    1. 以下函数哪个是拷贝构造函数,为什么?

    X::X(const X&);

    X::X(X);

    X::X(X&, int a=1);

    X::X(X&, int a=1, b=2);

    2. 一个类中可以存在多于一个的拷贝构造函数吗?

    3. 写出以下程序段的输出结果, 并说明为什么? 如果你都能回答无误的话,那么你已经对拷贝构造函数有了相当的了解。

#include <iostream>
#include <string>

struct X {
template<typename T>
      X( T& ) { std::cout << "This is ctor." << std::endl; }

template<typename T>
      X& perator=( T& ) { std::cout << "This is ctor." << std::endl; }
};

void main() {
      X a(5);
      X b(10.5);
      X c = a;
      c = b;
}

 

    解答如下:

    1. 对于一个类X,如果一个构造函数的第一个参数是下列之一:

    a) X&

    b) const X&

    c) volatile X&

    d) const volatile X&

    且没有其他参数或其他参数都有默认值,那么这个函数是拷贝构造函数。

    X::X(const X&); //是拷贝构造函数

    X::X(X&, int=1); //是拷贝构造函数

   2.类中可以存在超过一个拷贝构造函数,

class X {
public:
      X(const X&);
      X(X&);            // OK
};
注意,如果一个类中只存在一个参数为X&的拷贝构造函数,那么就不能使用const X或volatile X的对象实行拷贝初始化。

class X {
public:
     X();
     X(X&);
};

const X cx;
X x = cx;    // error

    如果一个类中没有定义拷贝构造函数,那么编译器会自动产生一个默认的拷贝构造函数。

    这个默认的参数可能为X::X(const X&)或X::X(X&),由编译器根据上下文决定选择哪一个。

    默认拷贝构造函数的行为如下:

    默认的拷贝构造函数执行的顺序与其他用户定义的构造函数相同,执行先父类后子类的构造。

    拷贝构造函数对类中每一个数据成员执行成员拷贝(memberwise Copy)的动作。

    a)如果数据成员为某一个类的实例,那么调用此类的拷贝构造函数。

    b)如果数据成员是一个数组,对数组的每一个执行按位拷贝。

    c)如果数据成员是一个数量,如int,double,那么调用系统内建的赋值运算符对其进行赋值。

    3. 拷贝构造函数不能由成员函数模版生成。

struct X {
template<typename T>
      X( const T& );    // NOT copy ctor, T can't be X

 template<typename T>
      perator=( const T& ); // NOT copy ass't, T can't be X
};

        原因很简单, 成员函数模版并不改变语言的规则,而语言的规则说,如果程序需要一个拷贝构造函数而你没
有声明它,那么编译器会为你自动生成一个。 所以成员函数模版并不会阻止编译器生成拷贝构造函数, 赋值运算
符重载也遵循同样的规则。(参见Effective C++ 3edition, Item45

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值