《Effective C++》条款5-12 构造/析构/赋值运算

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

class Empty { };

1、编译器会默认为空类声明copy构造函数、default构造函数、析构函数、copy assignment操作符。相当于:


class Empty { 
public:
	Empty() {...}//default构造函数
	Empty(const Empty& rhs) {...}//copy构造函数
	~Empty() {...}            //析构函数。编译器产出的析构函数是个non-virtual,除非这个class的base class自身声明有virtual析构函数。
	Empty& operator=(const Empty& rhs) {...}//copy assignment操作符
};

2、如果自己声明了构造函数,编译器会选择自己构造的那个构造函数,而非默认的。


template<typename T>
class NamedObject {
public:
	NamedObject(const char* name, const T& value);
	NamedObject(const string& name, const T& value);
	...
private:
	string nameValue;//两个参数
	T objectValue;
};

更改了copy 构造函数。使用这个版本而非默认版本。

NamedObject<int> no1("Smallest Prime Number", 2);
NamedObject<int> no2(no1);

copy构造函数的调用过程:编译器生成的copy构造函数会以no1.nameValue和no1.objectValue为初值来设定no2.nameValue和no2.objectValue。

3、赋值操作符operator=自己定义


template<typename T>
class NamedObject {
public:
	NamedObject(string& name, const T& value);
	...
private:
	string &nameValue;
	const T objectValue;
};

string newDog("Persephone");
string oldDog("Satch");
NamedObject<int> p(newDog, 2);
NamedObject<int> s(oldDog, 36);
 
p = s;//错误!!!

C++编译器会拒绝编译那一行赋值操作!因为默认情况下reference不能指向不同的对象!同样更改const是不合法的

解决办法:自己定义copy assignment操作符

#include <iostream>
using namespace std;

class Nameobject{
public:
	Nameobject(string& name);//声明copy构造函数
	Nameobject& operator=(const Nameobject& rhs);//声明成员函数operator=
private:
	string& namevalue;
};

Nameobject::Nameobject(string& name) :namevalue(name){}//定义copy构造函数

Nameobject& Nameobject::operator=(const Nameobject& rhs)//定义赋值构造函数,前面的Nameobject&类似于int,是operator=返回的类型
{
	namevalue = rhs.namevalue;
	return *this;
}

int main(){
	string newDog("ppppp");
	string oldDog("sssss");
	Nameobject p(newDog);
	Nameobject s(oldDog);
	p = s;
	return 0;
}

条款6 若不想使用编译器自动生成的函数,要明确拒绝

总结:

为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private,并且只声明不定义

使用像Uncopyable这样的base class也是一种做法。

引出原因:当你想阻止对象copying时,他会使用默认生成的构造函数和赋值操作符起作用,这是我们不想用的。

class HomeForsale  {};
HomeForsale h1;
HomeForsale h2;
HomeForsale h3(h2);//使用了默认构造函数,错误!!
h1=h2//使用了默认赋值操作符,错误!!

方法一:把copy构造函数跟copy assignment操作符在private中声明,并且不要定义

class HomeForSale {
public:
	...
private:
	...
	HomeForSale(const HomeForSale&);
	HomeForSale& operator=(const HomeForSale&);     //只有声明,不能定义!
};

 这样的话,当用户企图拷贝HomeForSale对象的时候,编译器会报错!因为他们没法调用private函数。

方法二:我们设计一个专门为了阻止copying动作而设计的base 类:class Uncopyable

class Uncopyable {
public:
	Uncopyable() { }
	~Uncopyable() { }
private:
	Uncopyable(const Uncopyable&);
	Uncopyable& operator=(const Uncopyable&);
};
 
class HomeForSale : private Uncopyable {
	...     //这个时候该class就不要再声明copy构造函数或copy assignment操作符了!
}

尝试拷贝HomeForSale对象的时候,编译器就是试着生成HomeForSale的copy构造函数跟copy assignment操作符!这个时候就要先调用base class的copy构造函数跟copy assignment操作符!这时候编译器会拒绝这样的调用!因为其base class的拷贝函数是private!

备注:

boost::noncopyable 的基本思想是把构造函数和析构函数设置protected权限,这样子类可以调用,但是外面的类不能调用,那么当子类需要定义构造函数的时候不至于通不过编译。但是最关键的是noncopyable把复制构造函数和复制赋值函数做成了private,这就意味着除非子类定义自己的copy构造和赋值函数,否则在子类没有定义的情况下,外面的调用者是不能够通过赋值和copy构造等手段来产生一个新的子类对象的


条款7 为多态基类声明virtual析构函数

总结:

1)polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。

      如果class带有任何virtual函数,它也应该拥有一个virtual析构函数!

(2)Classes的设计目的如果不是作为base classes使用,或不是为了具备多态性!就不该声明virtual析构函数。

1、为多态基类声明虚析构函数

引出原因:non-virtual析构函数,derived class对象被删除,通常其bass class被销毁,但是derived class 存留了下来

#include <iostream>
using namespace std;

class A
{
public:
	A(){ cout << "A()" << endl; }
	~A(){ cout << "~A()" << endl; }
	//virtual ~A(){ cout << "~A()" << endl; }//虚析构函数
};

class B :public A
{
public:
	B(){ cout << "B()" << endl; }
	~B(){ cout << "~B()" << endl; }
};

int main(){
	A *pa = new B;//new一个B类对象,
	delete pa;
	return 0;
}

问题出现在基类A的指针指向了派生类B的对象,A的析构函数调用了,但是派生类B的析构函数没有被调用。于是这是一个诡异的“局部销毁”的问题,会导致内存泄露,败坏数据结构,在调试时浪费时间。

解决办法为基类添加一个virtual析构函数,这样,通过基类指针销毁派生类对象就会将会调用派生类的构造函数,那么会将整个对象销毁。

virtual ~A(){ cout << "~A()" << endl; }

顺序

构造函数:先Base再Derived;析构函数:先Derived再Base。

2、如果类目的不是作为基类或不是为了具备多态性,就不要声明虚析构函数

但并不是所有基类设计的目的都是为了多态用途。如表中string和STL容器都不是设计作为基类使用,更不要提多态了。因此他们不需要虚析构函数。如果你试图继承一个没有任何虚析构函数的类,包括STL容器如vector,list,unordered_map等,容易导致错误

3、纯虚析构函数

为你希望成为抽象的那个class声明一个pure virtual 析构函数。除了声明,还要定义。

class A{
  public:
    virtual ~A()=0; // 声明纯虚析构函数

}

A::~A()  {}  // 定义纯虚析构函数


条款9 绝不在构造和析构过程中调用virtual函数

总结:

在构造和析构期间不要调用virtual函数,因为它会停留在base class,而不会下降至derived class。


 虚函数virtual:

在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,以共同的方法,但因个体差异,而采用不同的策略。

虚函数的作用,用专业术语来解释就是实现多态性(Polymorphism),多态性是将接口与实现进行分离;

实现多态性,通过指向派生类的基类指针或引用(两个都是指向基类的指针)!!!,访问派生类中同名覆盖成员函数。

#include<iostream>
using namespace std;
class A
{
    public:
        void print()
        {
            cout<<"This is A"<<endl;
        }
};
 
class B : public A
{
    public:
        void print()
        {
            cout<<"This is B"<<endl;
        }
};
 
int main()
{
    //为了在以后便于区分,我这段main()代码叫做main1
    A a;
    B b;
    a.print();
    b.print();
    return 0;
}

分别是“ThisisA”、“ThisisB”。但这并不是多态性行为(使用的是不同类型的指针),没有用到虚函数的功能。

现在把main()处的代码改一改,保证指针类型相同。运行一下看看结果,结果却是两个This is A(错)。

int main()
{
    //main2
    A a;
    B b;
    A *p1 = &a;//改为相同类型指针A*
    A *p2 = &b;//之前相当于B *p2=&b
    p1->print();
    p2->print();
    return 0;
}

应该更改为基类成员函数为virtual虚函数。

class A
{
    public:
        virtual void print(){cout<<"This is A"<<endl;}
};
class B : public A
{
    public:
    void print(){cout<<"This is B"<<endl;}
};

现在重新运行main2的代码,这样输出的结果就是This is A和This is B了。


1、构造函数中出现的virtual成员函数,在调用成员函数时仍然是Base class内的版本

 我们原本认为构造子类对象时,如果在父类的构造函数调用虚函数就会调用子类的虚函数,然而编译器会调用父类的虚函数

编译器对这种在父类中调用虚函数的做法的一个解决方案是:继承类对象的基类对象构造期间对象的类型是基类而不是继承类。

合理的解释: 在基类构造函数执行时,继承类的成员变量尚未初始化,如果使用这些未初始化的成员将导致不明确行为。 


class Transaction {                            //所有交易的base class
public:
  Transaction();
  virtual void logTransaction() const = 0;     //做出一份因类型不同而不同的日志记录(log entry)
  ...
};
 
Transaction::Transaction()                     //base class构造函数之实现
{
  ...
  logTransaction();                            //最后动作是志记这笔交易
}
 
class BuyTransaction: public Transaction {     //derived class
public:
  virtual void logTransaction() const;         //志记(log)此型交易
  ...
};
 
class SellTransaction: public Transaction {    //derived class
public:
  virtual void logTransaction() const;         //志记(log)此型交易
  ...
};

现在执行以下语句:

BuyTransaction b;

 Transaction的构造函数最后一行调用logTransaction();这时候调用的logTransaction();是Transaction版本(基类Bass)而非BuyTransaction(派生类Derived class)。

2、将 Transaction (基类)中的 logTransaction 转变为一个 non-virtual函数,然后要求派生类构造函数将必要的信息传递给 Transaction 构造函数,而后那个函数就可以安全地调用 non-virtual的 logTransaction。

class Transatcion { 
public:
       explicit Transaction(cosnt std::string& logInfo);
       void logTransaction(const std::string& logInfo) const; //non-virtual
};
 
Transaction::Transaction(const std::string& logInfo) {
     logTransaction(logInfo);                                //non-virtual调用 
}
 
 
class BuyTransaction:public Transaction {
public:
       BuyTransaction(parameters) 
       : Transaction(createLogString(parameters)) {}        //将log信息传递给基类构造函数 
private:
        static std::string createLogString(parameters); 
};

 这句话,令派生类BuyTransaction将必要的构造信息向上传递至基类构造函数Transaction

       BuyTransaction(parameters) : Transaction(createLogString(parameters)) {} 

条款10 令operator=返回一个reference to *this

总结:

重载赋值运算符(包括所有赋值=相关的运算)、前自增和前自减运算符(++a、--a)都返回*this的引用。而后自增和后自减(a++、a--)返回的是对象

1、连锁赋值的情况。赋值运算符必须返回一个指向引用指向操作符左侧的实参, *this指向左侧对象。

stu& operator=(const stu& s);
class stu
{
public:
	int _x;
public:
	stu(int x) :_x(x){};
	stu& operator=(const stu& s)
	{
		_x = s._x;
		return *this;
	}
};
int main()
{
	stu s(1);
	stu s2(2);
	stu s3(3);
	s = s2= s3;
	return 0;
}

可以实现连锁赋值。

	stu& operator=(const stu& s)
	{
         ...
		return *this;//返回左侧对象
	}

 *this 是自身对象,this是当前对象的指针。

2、前自增和前自减运算符(++a、--a)都返回*this的引用;后自增和后自减(a++、a--)返回的是对象


class CheckedPtr {
public:
      CheckedPtr& operate++(); //前自增运算符重载
      CheckedPtr& operate--();
      ...
 };
//前自增
CheckedPtr& CheckedPtr::operator++() {
     ...
     ++curr;
     return *this;    //注意返回值,返回的是对象的引用
}

//后自增
CheckedPtr& CheckedPtr::operator++() {
     ...
     CheckedPtr ret(*this);
     ++*curr;
     return ret;    //注意返回值,返回的是当前对象
}


后自增、自减的效率较低。

条款11 在operator=中处理“自我赋值”

总结:

确保当对象自我赋值时operator = 有良好行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap.

确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

1、自我赋值不安全和异常不安全版本:

class Bitmap {
 
};
class Widget {
private:
	Widget(Bitmap*pb):p(pb){ }
	Bitmap* p;  //指向一个从heap分配而得的对象。
	Widget& operator = (const Widget&rhs);
};
Widget& Widget::operator=(const Widget&rhs) {
	delete p;
	p = new Bitmap(*(rhs.p));
	return *this;
}
/*
这里的this(赋值的目标)和 rhs 可能是同一个对象。果真如此 delete 不仅会销毁当前对象的bitmap,
也会销毁 rhs 的 bitmap。在函数的结尾,Widget(原本不该被自我赋值动作改变的)
发现自己持有一个指向已删除对象的指针。
*/

 delete会先删除自身对象,最终发现自己持有一个指针指向一个已被删除的对象。

2、自我赋值安全,但不是异常安全的版本,先加上 identity test(证同测试)

if (this == &rhs) return *this;

如果是自我赋值,就不做任何事 

3、异常安全+自我赋值安全版本

我们只要注意复制pb所指东西之前别删除pb。

Widget& Widget::operator=(const Widget& rhs)
{
   Bitmap* pOrig=p;//指向原先的p对象
   p=new Bitmap(*rhs.p);
   delete pOrig;
   return *this;
}

4、copy and swap" 技术

class Widget {
    ...
    void swap(Widget& rhs); // 交换*this和rhs数据
    ...
};
 
Widget& Widget::operator=(constWidget& rhs)
{
     Widget temp(rhs); // 为rhs数据制作一份副本
     swap(temp); // 将*this数据和上述复件的数据交换
     return *this;
}

条款12 复制对象时勿忘其每一个成分

总结:

Copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”。

不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

以下规则是在声明定义自己的copying 函数时出现的问题。

当我们编写一个copying函数,请确保

(1)复制所有local成员变量!(2)调用所有base classes内的适当的copying函数。

 

1、添加新的成员变量,需要同时修改copying函数

如果你声明自己的copying函数(拷贝构造,拷贝赋值),那么编译器在你做出错误的动作时不会告诉你,下面定义的copying函数没有对Defau赋值或初始化,而编译器不会管

// 第一个类
class Defau {
public:
	int i = 10;
};

// 第二个类
class A {
public:
	A() = default;
	A(const A&rhs):name(rhs.name){}
	A& operator = (const A&rhs) {
		this->name = rhs.name;
	}
	string name;
    // 用到了第一个类
	Defau u;
};


int main() {
	A a;
	a.u.i = 15;
	A b(a);
	cout << b.u.i;
}

在这里,已有的Copying函数只进行了局部拷贝它们复制了 第二个类A 的 name,但是没有复制第一个类Defau 的对象 u。然而,大部分编译器即使是在最高的警告级别也不出任何警告。

结论显而易见:如果你为一个类增加了一个数据成员,你务必要做到更新拷贝函数,你还需要更新类中的全部的构造函数以及任何非标准形式的 operator=

2、发生继承,只有derived自定义的变量,base 的只能用缺省的默认初始化

任何时候只要我们承担起“为derived class撰写copying函数”的重责大任,必须很小心的也复制其base class成分。

但是那些成分往往是private,所以我们无法直接访问它们,所以我们应该让derived class的copying函数调用相应的base class函数

PriorityCustomer::PriorityCustomer(constPriorityCustomer& rhs)
:priority(rhs.priority)
{logCall("PriorityCustomer copy constructor");}

PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
logCall("PriorityCustomer copy assignment operator");

priority = rhs.priority;

    return*this;
}

 现在只有复制了local成员变量,对于base class的成员变量只能缺省的默认构造函数。

3、正确做法:derived class 的copying函数调用base class的构造函数和赋值函数。

PriorityCustomer::PriorityCustomer(const PriorityCustomer &rhs) : Customer(rhs), priority(rhs.priority)
{
}
 
PriorityCustomer& PriorityCustomer::operator =(const PriorityCustomer &rhs) 
{
	Customer::operator=(rhs);
	priority = rhs.priority;
	return *this;
}

当我们编写一个copying函数,请确保(1)复制所有local成员变量!(2)调用所有base classes内的适当的copying函数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值