effective cpp读书笔记5-12

构造/赋值/析构运算

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

如果你在empty class(空类)没有声明任何东西,编译器会为你声明(编译器版本的)一个copy构造函数,一个copy assignment 操作符和一个default构造函数和一个析构函数。这些函数都是public且inline。编译器为你声明的copy assignment 和 copy 构造函数是浅拷贝。编译器创建的copy构造函数和copy assignment中,给成员变量初始化或赋值,会调用成员变量的赋值构造函数和赋值操作符。

如果你写下

class Empty{};

就好像写下

class Empty{
public:
	Empty(){}
	Empty(const Empty& rhs){}
	Empty& operator=(const Empty& rhs){}
	~Empty(){}
};

一些情况下编译器拒绝为你生成copy assignment函数,考虑以下代码

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

std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog,2);
NamedObject<int> s(oldDog,36);

p = s;

reference和const对象本身不可以改动,所以编译器拒绝编译那一行赋值动作,如果你打算在一个内含reference和const成员的class内支持赋值操作,你必须自己定义copyassignment操作符。

一个类包含不可更改时编译器不会生成赋值操作符

还有一种情况

如果某个base class 将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。毕竞编译器为derived classes所生的copyassignment操作符想象中可以处理base class成分(见条款12),但它们当然无法调用derived class无权调用的成员函数。

请记住

  • 编译器可以暗自为class创建defaut构造函数、copy构造函数、copyassignment操作符,以及析构函数

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

地产中介商卖的是房子,一个中介软件系统自然而然想必有个class用来描述待售房屋

class HomeForSale{...};

没有两个一模一样的房子,因此copy构造函数和copy assignment操作符不应该使用

HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1);//应该出错
h1=h2;//应该出错

条款5中如果你不声明这两个函数,编译器会为你自动声明它们。有两种方法能够阻止其发生

1.将copy构造函数和copy assignme运算符设为私有

class HomeForSale
{
public:
	……
private:
	HomeForSale(const HomeForSale&);
	HomeForSale& operator=(const HomeForSale&);
};

在类外的普通调用会导致编译错误,发生在编译阶段,但是友元和member函数却可以访问私有部分,这样的话错误会发生的链接阶段。因为我们只是声明,没有实现。

2.设计一个不可复制的类,令你的class继承它

class Uncopyable{
{
protected:
	Uncopyable(){}
	~Uncopyable(){};
private:
	Uncopyable(const Uncopyable&);
	Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale:private Uncopyable{
...
};

所有调用copy构造函数和copy assignment操作符发生的错误都到了编译期(将错误提前到编译阶段是最好的,毕竟越早出现错误越好)

请记住

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

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

转载

https://blog.csdn.net/kangroger/article/details/41986049#t0

在创建有层次的类时,往往要把基类的析构函数声明为虚函数。
这是因为在使用这些类时,往往是通过基类指针或者引用使用的(类的实例在堆上),如果是析构对象时,通过delete + 指针,这时如果析构函数不是虚函数,将不会调用当前指针指向对象的析构函数。这是多态的原理。

同理可知,要实现多态的函数,在基类也要声明为虚函数。
当一个类不用做基类时,如果把其析构函数声明为虚函数是个馊主意。因为虚构函数是通过虚函数表调用的,在调用虚函数时多一步指针操作;除此之外,其对象占用的内存空间也会多一个虚函指针。

一个类不含虚函数,一般不适合做基类。例如string类不含任何的虚函数,但是有时程序员还是会这样用

class SpecialString:public std::string{
...
};

这时如果通过基类指针使用,在delete时,可能会造成内存泄漏。要记住,STL中的容器都没有虚的析构函数。

有时一个类含有纯虚函数。这样的类叫做抽象类,不能被实例化。

class AWOV
{
public:
	virtual ~AWOV()=0;
};

如果把它当作基类,会有问题,因为其析构函数只有声明。析构函数的调用时从派生类到基类,如果没定义,会发生链接错误,这是要定义个空的析构函数

AWOV::~AWOV(){}

条款8.别让异常逃离析构函数

C++ 代码并不禁止析构函数吐出异常,但它并不鼓励你这样做。考虑以下代码

class Widget{
public:
...
~Widget(){...} //假设这个可能突出一个异常
};
 
void doSomething()
{
	std::vector<Widget> v;
	 ...
	/在这里被自动销毁
}

假设v内含10个widgets,而在析构第一个元素时,有个异常被抛出。其他九个widgets还是应该被销毁(否则它们保存的任何资源会泄露),因此v应该调用它们各个析构函数,又出现了异常,现在有两个异常。两个异常同时存在会导致不明确的行为。

这很容易理解,但如果你的析构函数必须执行一个动作,该动作可能会在失败时抛出异常,举个例子,假设你用一个class负责数据库连接

class DBConnection
{
public:
...
void close();//关闭数据库
};

为确保用户不会忘记在DBConnection对象调用close,我们创建一个用来管理DBConnection资源的class,并在其析构函数中调用close(这一类用于资源管理的classes在第三章有详细讨论)。

class DBConnection
{
public:
……
	~DBConn()//析构函数关闭连接
	{
		db.close();
	}
private:	
	DBConnection db;
};

如果调用close成功,则一切都美好。但是如果出现异常,DBConn会抛出异常,也就是允许这个异常离开析构函数,这样会传播异常。
有两个方法可以避免这个问题:

  • 如果close抛出异常就结束程序。通常通过调用abort完成:

      DBCoon::~DBConn()
      	{
      		try{
      			db.close();
      		}
      		catch(...)
      		{
      			//记录下对close调用的失败
      			std::abort();
      		}
      		
      	}
    

如果程序遇到一个“于析构期间发生的错误”后无法继续执行,强迫结束程序是个合理选项。毕竟它可以阻止异常从析构函数传播出去(那将导致不明确的行为)。也就是说调用abort可以抢先制“不明确行为”于死地。

  • 吞下因调用close而发生的异常

      ~DBConn()//析构函数关闭连接
      	{
      		try{
      			db.close();
      		}
      		catch(……)
      		{
      			//记录下对close调用的失败
      		}
      		
      	} 
    

一般而言,将异常吞掉是个坏主意,因为它压制了“某些动作失败”的重要信息!然而有时候吞下异常也比负担“草率结束程序”或“不明确行为带来的风险”好。为了让这成为一个可行方案,程序必须能够继续可靠地执行,即使在遭遇并忽略一个错误之后。

一个比较好的策略是重新设计DBCoon接口,是客户能对可能出现的异常做出反应。例如DBConn可以自己提供一个close函数,可以给客户一个机会来处理“因该操作而发生的异常”。DBConn也可以追踪其所管理的DBConnection是否已经关闭,并在答案为否的情况下由其析构函数关闭,这样可以防止遗失数据库连接。但是如果DBConnection的析构函数调用close失败,问题又回到了起点。

class DBConn{
public:
...
void close() //供客户使用的新函数1
{
	db.close( ); 
	closed=true; 
}

~DBConn()
{

if (!closed){ 
	try{ //关闭连接(如果客户不那么做的话)
	  db.close();
}
catch(...){
制作运转记录,记下对close的调用失败
...
		//如果关闭动作失败,制作运转记录,记下对close的调用失败;
		//记录下来并结束程序
		//或吞下异常。 
		}
	}
}

private:
  DBConnection db; 
  bool closed; 
};

把调用close的责任从DBConn析构函数手上转移到DBConn客户手上,这样会多出一个保险。客户自己调用close函数并不会给它们带来负担,而且给了他们一个处理错误的机会,否则他们没机会响应。

请记住

  • 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序。

  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个普通函数(而非在析构函数中)执行该操作。

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

假设你有个class继承体系,用来塑模股市交易。每当创建一个交易对象,在审计日志(audit log)中也需要创建一笔适当记录。

class Transaction{
public:
	Transaction();		
	virtual void logTransaction()const = 0;
	
	...

};
Transaction::Transaction()
{
	...
	logTransaction();
}

	class BuyTransaction:public Transaction{
public:
	virtual void logTransaction()const;
	...
};
 
class SellTransaction:public Transaction{
public:
	virtual void logTransaction()const;
	...
};

BuyTransaction b;

b被创建时,它的Transaction构造函数一定先被调用,Transaction的构造函数最后一步调用logTransaction,这时候被调用的版本一定是Transaction的版本,不是BuyTransaction的版本。即在base class 构造期间,virtual 函数不是virtual 函数。

相同道理也适用于析构函数,一旦derived class 析构函数开始执行,对象内的derived class 成员便呈现未定义值,所以C++视它们不存在,进入base class 析构函数后对象就成为一个base class 对象

解决方法:将class Transaction 内将logTransaction函数改为non-virtual

class Transaction{
public:
	explicit Transaction(const std::string& logInfo);
	
	void logTransaction(const std::string& logInfo)const;//如今是non-virtual 函数
};
Transaction::Transaction(const std::string& logInfo)
{
	...
	logTransaction(parameter);
}
 
class BuyTransaction:public Transaction{
public:
	 BuyTransaction(parameters)
	 :Transaction(createLogString(parameters))//将log信息传给base class 构造函数 {...}
	...
private:
	 static std::string createLogString(parameters);
};

这样就解决了开始提出的问题。在derived类中使用了private static函数来创建参数。这样增强了代码的可读性,另外static函数不会指向未初始化的derived类中的变量。

请记住

  • 在构造和析构期间不要调用virtual函数,因为这类调用从不下降至derivedclass(比起当前执行构造函数和析构函数的那层)

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

关于赋值,你可以写成连锁形式:

int x,y,z;
x = y = z = 15;

赋值采用右结合律,所以上述连锁赋值被解析为:

x = (y = (z = 15));

为实现连锁赋值,赋值操作符必须返回一个reference指向操作符的左侧实参

class Widget{
public:
	...
Widget& operator= (const Widget& rhs)
{
	...
	return *this;//返回左侧对象
}
	...

}

这个协议用于所有赋值相关运算,例如 +=,*=,-=。

请记住

  • 令赋值操作符返回一个reference to *this。

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

自我赋值即给自身赋值,听起来很蠢,但是确实有可能发生

潜在的自我赋值

a[i] = a[j];
*px = *py

如果遵循条款13和条款14,你会用运用对象来管理资源,确定“资源管理对象”在copy发生时有正确的举措,这样自我赋值是安全的。如果自己管理资源(如果你自己打算写一个用于资源管理的class就得这样做),可能会“在停止使用资源之前意外释放了它”。

假设你建立一个class用来保存一个指针只想一块动态内存分配的位图(bitmap):

class Bitmap{...}
class Widget
{
public:
	...
private:
	Bitmap* pb;//指针,指向一个从Heap分配的对象
};

下面是operator的实现代码:

Widget& Widget::operator=(const Widget& rhs)
{
	delete pb;
	pb = new Bitmap(*rhs.pb);
	return *this;
}

如果*this和rhs是同一个对象,那么delete不只是销毁当前对象的Bitmap,还销毁了rhs的bitmap,最后返回一个指向已被删除对象的指针

欲组织这种错误,在operator=最前面加一个证同测试

Widget& Widget::operator=(const Widget& rhs)
{
	if(this == &rhs) return *this;//证同测试
	delete pb;
	pb = new Bitmap(*rhs.pb);
	return *this;
}

这样做行得通,但仍存在异常方面的麻,即“new Bitmap”导致异常,Widget最终会持有一个指针指向一块被删除的Bitmap。 令人高兴的是,让operator=具备“异常安全性”往往自动获得“自我赋值安全”的回报。因此愈来愈多人对“自我赋值”的处理态度是倾向不去管它。把焦点放在实现异常安全性上。

Widget& Widget::operator=(const Widget& rhs)
{
	Bitmap* pOrig = pb;
	pb = new Bitmap(*rhs.pb);//
	delete pOrig;
	return *this;
}

现在如果“new Bitmap”抛出异常,pb会保持原状,即使没有证同测试,这段代码还是能够处理自我赋值, 即使没有证同测试(identity test),这段代码还是能够处理自我赋值,因为我们对原bitmap做了一份复件、删除原bitmap、然后指向新制造的那个复件。它或许不是处理“自我赋值”的最高效办法,但它行得通。

还有一种解决方法是采用copy and swap 技术

class Widget
{
	...
	void swap(Widget& rhs);//交换*this和rhs的数据;
	...
};

Widget& Widget::operator=(const Widget& rhs)
{
	Widget temp(rhs);//为rhs制作一份复件
	swap(temp);//将*this数据和上述复件的数据交换
	return *this;//临时变量会自动销毁
}

如果赋值操作符参数是值传递,那么就不需要新建临时变量,直接使用函数参数即可。

Widget& Widget::operator=(Widget rhs)
{
	swap(temp);//将*this数据和上述复件的数据交换
	return *this;
}

这种写法为了巧妙的修补而性了清晰性。然而将“copying动作”从函数本体内移至“函数参数构造阶段”却可令编译器有时生
成更高效的代码

请记住

  • 确保当对象自我赋值时operator=有良好行为。其中技术包括比较“来源对象和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap。
  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确

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

当你放弃编译器为你提供的copy 构造函数和copy assignment操作符,不要忘记每一个成分

class Cutsomer
{
public:
	...
	Cutsomer(const Cutsomer& rhs);
	Cutsomer& operator=(const Cutsomer& rhs);
	...
private:
	std::string name;
};

Customer::Customer(const Customer& rhs)
: name(rhs.name)
{
	logCall("Customer copy constructor");
}

Customer& Customer::operator=(const Customer& rhs)
{
	logCall("Customer copy assignment operator");
	name = rhs.name;
	return *this;
}

当你加入一个新的变量,你的两个copying函数并不会发出警告.即如果你为class添加一个成员变量,你必须同时修改copying函数。

在继承体系中,这样的错误更难被发现

class PriorityCustomer:public Cutsomer
{
public:
	PriorityCustomer()
	{
		cout<<"PriorityCustomer Ctor"<<endl;
	}
	PriorityCustomer(const PriorityCustomer& rhs)
		:priority(rhs.priority)
	{
		cout<<"PriorityCustomer Copy Ctor"<<endl;
	}
	PriorityCustomer& operator=(const PriorityCustomer& rhs)
	{
		cout<<"PriorityCustomer assign operator"<<endl;
		priority=rhs.priority;
		return *this;
	}
private:
	int priority;
};

在PriorityCustomer的copying函数中,只是复制了PriorityCustomer部分的内容,基类内容被忽略了。在派生类中构造函数没有初始化的基类部分是通过基类默认构造函数初始化的但是在copy assignment操作符中,不会调用基类的默认构造函数,因为copy assignment只是给对象重新赋值,不是初始化,因此不会调用基类的构造函数,除非我们显示调用。

正确写法:

PriorityCustomer(const PriorityCustomer& rhs)
		:Cutsomer(rhs),priority(rhs.priority)
	{
		logCall("PriorityCustomer Copy Ctor");
	}
	PriorityCustomer& operator=(const PriorityCustomer& rhs)
	{
		logCall("PriorityCustomer assign operator");
		Cutsomer::operator=(rhs);
		priority=rhs.priority;
		return *this;
	}

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

1.复制所有local成员变量。

2.调用所有baseclasses内的适当的
copying函数。

请记住

  • Copying函数应该确保复制“对象内的所有成员变量”及“所有baseclass成分”
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个coping函数共同调用
posted @ 2022-11-02 14:47  WetYu  阅读(3)  评论(0)    收藏  举报