Effective C++学习笔记 第三弹 11-18

来源:互联网 发布:网络大电影杀小姐 编辑:程序博客网 时间:2024/06/07 07:46
 

条款11:如果class内动态配置有内存,请为此class声明一个copy constructor和一个assignment运算符

1、默认operator=函数

     当自定的operator= 不存在时,C++产生一个默认的assignment运算符,该默认函数对对象的复制遵循如下规律

     (1)对非指针成员变量 ,生成一个副本,并将内容赋给新副本

     (2)对指针变量,将地址赋给了新的副本,即:源和目标指向同一块地址,这便出现了一个问题,当其中一个对象释放这块共同的内存时,另一个对象就无法访问了。而且假如目标对象已经对指针赋值,那么调用默认operator=之后,原来那块内存会永远遗失,这是一个内存泄漏问题。

2、默认的拷贝构造函数

     默认的拷贝构造函数的动作和默认operator=函数基本相同,但是只要用值传递的地方都会用到拷贝构造函数

如:void doNothing(A localA){}

      String a("abcdefg");

      doNothing(a);

当a跳出doNothing作用域后调用析构函数,对象内的指针会被释放,会出现和上面第二点一样的问题。  

条款12:在constructor中尽量以initialization动作取代assignment动作(即尽量运用构造函数的成员初始化列表)  

撰写构造函数有两种形式

(1)A(const string& initName,T *initPtr)   //参数只能是引用传递,假如是值传递会产生一个副本,副本又会调用构造函数,陷入死循环

        :name(initName),ptr(initPtr)

        {}   

 (2)A(const string& initName,T *initPtr)

         {

             name = initName;

             ptr = initPtr;

         }  

 

对象的构造分两个阶段

(1) data members初始化

(2) 被调用之constructor函数执行起来

根据上面所知,const members和reference members只能被初始化,不能被赋值,假如用第二种方法的构造函数,在第一阶段members已经被初始化,在构造函数中不能执行赋值操作。所以const和reference只能用初始化列表进行初始化。

也可从上得知,用member initialization list效率更高,因为只是调用了一次拷贝构造函数,而第二种构造函数,在第一阶段要调用一次构造函数,第二阶段调用一次assignment运算符。 

       然而有一种情况下,用assignment取代initialization是合理的内建型别之data members的初始化。

       class ManyDataMbrs{

       public:

           ManyDataMbrs

           {

               a=b=c=d=e=f=g=1;

           }

       private:

           int a,b,c,d,e,f,g,h;

       } 

以上提到的const对象必须用对象初始化列表进行初始化,但是static const对象在每一个程序执行过程中只应该初始化一次,不应该在每一个对象的构造函数中assignment或者initialization。

条款13:initialization list中的members初始化次序应该和其在class内的声明次序相同

class members(nonstatic data members)系以它们在class内的声明次序来初始化;和它们在member initialization list中出现的次序完全无关。而对象的data members的destructors总是以“和其cosntructors相反的次序”被调用。

base class data members永远在derived class data members之前初始化,所以应该在你的member initialization lists起始处就列出base class的初值设定。如果你使用多重继承机制,base classes将以“被继承的次序”来初始化。

条款14:总是让base class拥有virtual destructor

class A
{
public:
 static int numA;
 A()
 {}
 ~A()
 {}
};

class B:public A
{
public:
 static int numB;
 B()
 {}
    ~B()
 {}
};

如上程序 用

A* m_a = new B;
delete m_a;测试

执行的顺序为 B() -> A() -> ~A()

当把基类A的析构函数声明为virtual函数时,执行的顺序为 B() -> A() -> ~B() -> ~A()

当class不企图成为一个base class时,令其destructor为virtual通常是个坏主意。因为会增加内存开销,用于维护vptr(virtual table pointer)。

当你无法给你基类虚函数定义时,可以用纯虚函数,有一个或一个以上pure irtual function的类叫抽象类,不能实例化。因为要在vptr中保存虚函数的信息,即使你给不出纯虚函数地址,也必须指定调用的地址。如:virtual ~AWOV() = 0;

条款15:令operator= 传回“*this的reference”

String& String::operator=(const String& other)

{

    if (this!=&other)

    {

        delete[] m_data;

        if(!other.m_data) m_data=0;

        else

        {

            m_data = new char[strlen(other.m_data)+1];

            strcpy(m_data,other.m_data);

        }

    }

    return *this;

}

通过实验得出以下结论:

1、C++中this是指针,不是对象,所以返回的是*this;

2、首先看传参,声明为const string&,一是为了防止被修改,第二是为了提高效率用引用的方法传递参数,避免值传递生成副本带来的开销;

3、返回类型不为void的原因是用于如下的情况

   string a,b,c,d;

   a = b = c = d ="Hello";

   首先执行d="Hello",返回一个string,作为c=d=("Hello")的右值,依次类推

4、返回类型声明为引用是当出现如下情况时:

   b = "Hello";

   c = "World";

   (a = b) = c;

   当不使用引用时,a=b的操作返回的是a的一个副本(使用引用时返回的是a的一个别名,实际就是a);

   当不使用引用时,因为返回的是副本于是接下来执行的实际是(a的副本) = c,所以a的值仍是"Hello"

   而用引用之后,实际动作是 a = c,这时候 a 才会变为"World"

   为了配合a=b=c=d和(a=b)=c的内置型别的机制,我们自己写的类就要用如下缺省版的operator=

   C& C::operator=(const C&);或C& C::operator=(const T*)

关于该条款,新手常见的错误是:

(1)返回值类型为void 无法实现 a = b = c这种机制;

(2)返回值声明为const string& 无法实现(a = b)= c,因为返回的是const,不能被修改,所以 a = c无法实现;

(3)参数最好声明为const,如果不是当实参是const时会出现无法转换const到非const的错误,而假如实参是非const,却可以转换为const。

条款16:在operator= 中为所有的data members设定(赋值)内容

看如下程序

class Base{
public:
   Base(int initialValue = 0):x(initialValue){}
private:
   int x;
};

class Derived:public Base{
public:
    Derived(int initialValue)
  :Base(initialValue), y(initialValue){}
 Derived& operator=(const Derived& rhs);
private:
 int y;
};

Derived& Derived::operator=(const Derived& rhs)
{
 if(this == &rhs) return *this;
 Base::operator=(rhs);

//static_cast<Base&>(*this) = rhs;
 y = rhs.y;
 return* this;
}

 

1、Derived(int initialValue)
  :Base(initialValue), y(initialValue){}

如果没有Base(initialValue),那只是初始化了y,x只是默认的赋为0

2、Base::operator=(rhs);

如果没有这句,那子类assignment动作时,也没有把x赋给左值;

3、为了将Base成为一起拷过来,也可以写成这样的形式static_cast<Base&>(*this) = rhs;

先将其强制转换成Base基类,然后调用基类的operator=操作。

但是此处一定要将其转换为Base&,如果不加引用,那强制转换后,返回的是源对象的一个副本,rhs赋给副本并为对源对象改变。

条款17:在operator= 中检查是否“自己赋值给自己”

看如下程序

String& String::operator=(const String& rhs)
{
 //if(this == &rhs) return *this;
 delete[] data;
 data = new char[strlen(rhs.data)+1];
 strcpy(data,rhs.data);
 return* this;
}

当a=a时,这程序会出现错误,data已经被delete,还再对其进行strcpy操作。

注:

相等会有两个概念:1、地址相等 2、值相等

地址相等容易判断,值相等就需要重写操作符==和!=对对象内的每一个成员变量进行比较。

原创粉丝点击