文章目录
05:了解C++默认编写并调用哪些函数
编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。
这一章有以下几个点需要注意:
- 一个空的类,编译器自动生成了哪些函数。
class empty{};
// 上述代码与下类似
class empty{
public:
empty() {...} // default构造函数
~empty() {...} // 析构函数
empty(const empty& rhs) {...} // 拷贝构造函数
empty& operator=(const empty& rhs){...} // 拷贝赋值函数
};
- 编译器产出得析构函数是一个
non-vitual
函数。 - 编译器创建的copy构造函数和copy assignment操作符,只是单纯的将源对象中的每一个
non-static
成员变量拷贝到目标对象。 - 在以下三种情况下,我们需要自定义copying assignment操作符。
- 如果打算在一个”内含reference成员“的class内支持赋值操作,必须自定义。
- 如果类中包含const成员变量,那么编译器不会生成默认copying assignment操作符,因此需要自定义。
- 如果base class将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。
06:若不想使用编译器自动生成的函数,就该明确拒绝
为驳回编译器自动提供的机能,可将相应的函数声明为private并且不予实现。或者使用Uncopyable这样的base class。
这一章提供了两种拒绝编译器自动生成函数的方法。
**方法一:**这种方法就是将这些函数声明为private,并且不实现他们。
class HomeForSale{
public:
....
private:
...
HomeForSale(const HomeForSale&); //只声明
HomeForSale& operator=(const HomeForSale&)
};
这样的话,当客户试图拷贝HomeForSale
,那么编译器就会报错。如果在friend
函数或者member
函数内试图拷贝HomeForSale
,那么链接器会报错。(这里如果有疑问,可以读一下《程序员的自我修养》)。
**方法二:**方法一中,针对friend
和member
函数中拷贝对象,会在链接环节出现错误。如果我们将这类错误提前到编译期间,也算是一种代码的优化。因此出现以下方法:
class Uncopyable{
protected:
Uncopyable() {} // 允许derived对象构造和析构
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&); // 阻止copying
Uncopyalbe& operator=(const Uncopyable&)
};
那么当我们将HomeForSale
继承于该类后,就能将friend
和member
函数内拷贝的错误提前到编译器。
class HomeForSale : private Uncopyable{
....
};
07:为多态基类声明virtual析构函数
- 带多态性质的base class应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。
- Classes的设计目的如果不是作base class,或者不是为了实现多态性质,那么就不应该声明virtual 析构函数
这一章讲述了为什么我们需为多态基类什么virtual析构函数、以及什么时候不需要virtual析构函数。这章需要注意以下几点:
- 我们先来看下以下代码会出现什么问题?
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper();
....
};
class AtomicClock: public TimeKeeper{....}
// 工厂函数,在Go语言中由于没有构造函数,因此该类函数随处可见
// 返回一个指针,指针指向TimeKeeper派生类的动态分配对象
TimeKeeper* GetTimeKeeper();
// 注意GetTimeKeeper()返回的动态类型是子类对象指针
TimeKeeper* ptk = GetTimeKeeper();
delete ptk;
当我们执行delete ptk
时,就会出现问题。因为试图通过base class指针去删除一个derived class对象,并且父类析构函数为non-virtual
。那么就会出现未定义的情况——对象的derived部分没有被销毁。为了解决上述问题,我们只需将析构函数设置为virtual
即可。
- 如果一个类,我们不将它作为base class,那么就不需要将析构函数设置为virtual。这是因为,虚函数会带来内存消耗,比如虚指针、虚函数表。
- 标准string、stl容器vector、list、set均没有virtual析构函数,因此,我们在日常开发中,不应该设置类继承于他们;
- 为一个抽象的class声明一个pure virtua析构函数,并且提供该函数的定义。
class AWOV{
public:
virtual ~AWOV() = 0; // 声明pure virtual析构函数
};
AWOV::~AWOV() {} // 定义pure virtual析构函数
- 析构函数的运作方式:最深层的析构函数先被调用,然后其base class被调用。因此,
TODO 4
中我们需要给纯虚析构函数定义,如果没有,那么当最后调用AWOV纯虚构函数时会出现链接器报错。
08:别让异常逃离析构函数
- 析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下他们或者结束程序。
- 如果客户端需要对操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数执行该操作。
这一章讲述了为什么不能将异常从析构函数中逃离?我们首先来看以下例子:
class Widge{
public:
....
~Widget() {...} // 假设这个会吐出一个异常
};
void doSomething() {
std::vector<Widget> v;
.... // v在这里自动销毁
}
对于上述代码,当v
中的第一个Widget
销毁的时候出现异常,那么第二个又出现异常。如果这个时候不结束程序执行,就会导致不明确的行为。
我们再来看一个数据库连接的例子,
class DBConnection{
public:
...
static DBConnection create(); // 这个也是一个工厂函数,生成一个DBConnection对象
void close();
};
为了防止客户忘记执行close()
操作,因此,我们利用对象来管理资源,代码如下:
class DBConn{ // DBConn class 来管理DBConnection对象
public:
...
~DBConn(){ // 确保数据连接总是会被关闭
db.close();
}
private:
DBConnection db;
};
因此,客户可能写出以下代码:
{
DBConn dbc(DBConnection::create());
.... // 这一段通过dbc操作DBConnection
// 当离开这个作用域时,dbc就会调用析构函数,从而调用close()函数
}
如果,dbc
顺利调用了close()
函数,那到一切完美。但该调用导致异常,DBConn析构函数传播该异常,就会造成不可预知的错误。以下讲述了两种解决该问题的方法:
- 如果
close()
抛出异常就结束程序,可通过abort()
完成:
DBConn::~DBConn(){
try{
db.close();
} catch(...){
// 制作运转记录,记下对close的调用失败
std::abort();
}
}
-
吞下调用
close()
而发生的异常:DBConn::~DBConn(){ try{ db.close(); } catch(...){ // 制作运转记录,记下对close的调用失败 // 吞下异常 } }
这种方法,也不是最佳选择,因为,它不能对"导致close抛出异常"的情况做出反应。
-
一个比较好的方法是,重新设计DBConn接口,使客户能够对可能出现的情况做出反应:
class DBConn{ // DBConn class 来管理DBConnection对象
public:
...
void close{
db.close();
closed = true;
}
~DBConn(){ // 确保数据连接总是会被关闭
if (!closed){
try{
db.close();
} catch(...) {
// 制作运转记录,记下对close的调用失败
}
}
}
private:
DBConnection db;
bool closed;
};
最后一种方法,也就是提供了一个双保险,也就是,可以客户调用close
函数,也可依赖析构函数。当析构函数出现异常时,客户可调用close接口,从而释放资源。
09: 绝不在构造和析构过程中调用virtual函数
- 在构造和析构期间不要调用virtual函数,因为这类调用从不会下降到derived class。
这一章讲述为啥不能在构造和析构过程中调用virtual函数?我们首先看一个例子:
// Base 类
class Transaction{
public:
Transaction();
virtual void LogTransaction() const = 0;
....
};
Transaction::Transaction(){
...
logTransaction();
}
// Derived 类
class BuyTransaction : public Transaction{
public:
virtual coid logTransaction() const;
};
// 执行以下代码会出现什么????
BuyTransaction b;
这里,当我们定义一个BuyTransaction
对象时,首先会执行Base类的构造函数,在该构造函数内,会执行Base类的LogTransaction
函数,该函数不会下降到derived 类。这样就出现和我们意图不一致的想法。出现这种情况的原因是:在derive class对象的base class构造期间,对象类型是base class 而不是 derived class。因此,virtual 函数、运行期类型信息也会把对象视为base class类型。
同理,析构函数也是样的,因为,derived class析构函数开始执行,对象内的derived class成员变量先呈现未定义 值。这样,进入base class析构函数时,就视为一个base对象了,因此,其中的virtual function
就不会下降到derived class,产生和我们想法不一致的现象。
还有一个问题就是,上述代码中在抽象类中调用未提供定义的pure virtual function
,连接器会报错。
那我们如何修改上述代码呢??看下面两种解决方法!!!
// Base 类
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(logInfo);
}
// Derived 类
class BuyTransaction : public Transaction{
public:
BuyTransaction(params) : Transaction(createLogString(parameters)); // 将log信息
{....} // 传给base class构造函数
private:
static std::string createLogString(parameters)
};
// 执行以下代码会出现什么????
BuyTransaction b;
看上述代码,我们无法使用virtual函数从base class向下调用,那么在构造期间,可以通过“令derived class将必要的构造信息向上传递到base class构造函数替换加以弥补。”注意上述代码,我们将derived class 中的createLogStringg
设置为static函数。这就可避免“初期未成熟的BuyTransaction对象内尚未初始化的成员变量。” (在单例模型中,也可通过static避免多线程竞争问题)
10: 令operator=返回一个reference to *this
令赋值操作符返回一个reference to *this.
这一章节讲述为啥operator=
应该返回一个reference to *this.
我们想来看一个日常开发中,我们会经常遇到的案例。
int x, y, z;
x = y = z = 5;
这一段代码的意思就是,15先赋值给z,然后将更新后的z赋值给y,在将更新后的y赋值给x;
为了实现上述的 连锁赋值, 赋值操作必须返回一个reference指向操作符左侧的实参。这个协议不仅适用于以上的标准赋值形式,也适用于所有 赋值相关运算。
比如:
class Widget{
public:
...
Widget& operator=(const Widget& rhs) { // 返回类型是个reference
....
return *this; //返回左侧对象
}
Widget& operator+=(const Widget& ths) {
...
return *this;
}
Widget& operator=(int rhs){
...
return *this;
}
};
11: 在operator=中处理”自我赋值“
- 确保当对象自我赋值时operator = 有良好行为。其中技术包括比较”来源对象“和”目标对象“的地址、精心周到的语句顺序、以及copy-and-swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象时同一个对象时,其行为正确。
这一章首先讲述,我们日常开发中存在哪些隐形的自我赋值,其次,讲述我们该如何应对自我赋值这种情况。
隐形赋值有哪些???
a[i] = a[j]
,当i = j
时,这便是自我赋值;*px = *py
,当px,py
指向同一个东西,也是自我赋值;- 继承体系下,也容易出现自我赋值,比如
class Base{};
class Derived: public Base{};
void doSomething(const Base& rb, Derived* pd); // rb和*pd有可能其实是同一对象
我们该如何规避自我赋值陷阱,看下面这个例子
class Bitmap{...};
class Widget{
...
private:
Bitmap* pb; // 指针,指向一个从heap分配而得得对象
};
Widget& Widget::operator=(const Widget& rhs) {
delete pb;
pb = new Bitmap(*ths.pb);
return *this;
}
上述代码中rhs
和*this
可能是同一个对象。 那么delete pb
就会将rhs
也删除删除掉。
阻止这种错误,传统做法是在operator=
最前面加一个 “证同测试”达到“自我赋值”的目的。代码如下:
Widget& Widget::operator=(const Widget& rhs) {
if (this == &ths) return *this; // 证同测试,如果是自我赋值,就不做任何事情。
delete pb;
pb = new Bitmap(*ths.pb);
return *this;
}
虽然上述代码具备 “自我赋值安全性”,但是不具备 “异常安全性”。比如说,new
失败了,那么Widget最终会持有一个指针,指向一块被删除的Bitmap;为了阻止这种异常,我们可以写出以下代码:
Widget& Widget::operator=(const Widget& rhs) {
Bitmap* pOrig = pb; // 记住原来的pb
pb = new Bitmap(*ths.pb); // 令pb指向*pb的一个复制(副本)
delete pOrig; // 删除原来的pb
return *this;
}
上述代码具有“自我赋值安全性”且具有“异常安全性”。 处理步骤如下:
- 对原始Bitmap做一个复制
- 指向新复制的那个副本
- 删除原bitmap
上述代码的一个替代方案是:copy and swap技术。 我们可以来看看下述代码:
class Widget{
...
void swap(Widget& rhs); // 交换rhs和*this的数据;
};
Widget& Widget::operator=(const Widget& rhs){
Widget temp(rhs); // 为rhs制作一个副本
swap(temp); // 将*this数据和上述复制数据交换
return *this;
}
这里我们进一步可以将代码写成以下形式,这两个代码作用其实是一样的。
// 为rhs制作一个副本,这个过程在传值过程就发生了
Widget& Widget::operator=(const Widget rhs){
swap(rhs); // 将*this数据和上述复制数据交换
return *this;
}
第二种方案相对于第一种牺牲了一部分清晰性,但是将“copying”动作从函数本体移动到“函数参数构造阶段”可以令编译器有时生成更加高效的代码。
12: 复制对象时勿忘其每个成分
copying函数应该确保复制”对象内所有成员变量“及”所有base class成分“。
不要尝试以某个copying函数实现另一个copying函数。应该将共同技能放进放进第三个函数,由两个copying函数共同调用。
这一章讲述了,哪种情况我们需要自定义copying函数。以下几点需要注意:
- 我们称copying函数包含:copy构造函数和copy assignment操作符。
- 如果我们自定义copying函数,那么当copying函数的实现出现错误时,编译器却不会报错。
我们来看一个例子,
void logCall(const std::string& funcName); //制造一个log entry
class Customer{
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& 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;
}
对于上述类来说,当我们执行拷贝或赋值操作,没有任何问题。也就是以下操作:
Customer c1;
Customer C2(c1); // 拷贝构造函数
Customer c3;
c3 = c2; // 赋值操作符
但是,当我们在该类中,加入一个自定义对象,也就是组合一个对象,代码如下:
class Date{...}; // 日期
class Customer{
public:
...
private:
std::string name;
Date lastTransaction;
};
如果copying函数的实现没有改变,执行上述赋值或拷贝操作,那么就会出现局部数据拷贝,但是此时:编译器却不会报错。 因此,当我们加入一个新成员变量时,我们需要修改copying函数。
我们再来看一个继承的例子:
class PriorityCustomer : public Customer{
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriortyCustomer& rhs);
...
private:
int priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): priority(rhs.priority) {
logCall("priorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator(const PriorityCustomer& rhs){
logCall("priorityCustomer copy assignment operator");
priority = rhs.priority;
return *this;
}
上述代码,看起来没有问题,事实上,这个copying函数忽略了,继承与base class的成员变量。为了解决这个问题,应该使用下述写法:
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs): Customer(rhs), priority(rhs.priority) {
logCall("priorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator(const PriorityCustomer& rhs){
logCall("priorityCustomer copy assignment operator");
Customer::operator=(rhs); // 对base class成分进行赋值动作
priority = rhs.priority;
return *this;
}
注意Customer::operator=(rhs)
的调用;
除了需要注意上述点,还有以下两点需要注意:
- 当编写一个copying函数,确保(1)复制所有local成员变量,(2)调用所有base class内的适当copying函数。
- 如果copy assignment 操作符和copying 构造函数两者有共同的操作,那么我们应该定义一个
init
函数,然后两个函数调用它;
参考
- efffective C++