和我一起读effective C++ ——构造-析构-赋值运算

构造/析构/赋值运算

标签: effective C++



文中中英文混杂非我本意,因为翻译上有差别,比如 default constructor 翻译为缺省构造函数或者默认构造函数,copy constructor 翻译为复制构造函数或拷贝构造函数,base class翻译为父类或者基类,也是心累。
翻译上的一些狗血地方确实很讨厌,比如句柄、鲁棒性等中文翻译反而不如英文清晰。

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

编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment运算符,以及析构函数。

需要注意几点:

  • 这里是可以,而不是必然,在某些条件下不会生成default构造函数,比如
    基类没有default构造函数,类成员对象没有default构造函数,已经提供有参构造函数等情况;
  • copy构造函数和copy assignment运算符只是单纯的将来源对象的每一个 non-static 成员变量拷贝到目标对象,对于内置类型按bit拷贝,其他类型会调用对象的copy构造函数;
  • 对于引用类型成员对象、const 类型成员对象,需要自定义copy assignment运算符;
  • 拒绝某个函数可以使用 delete C++11
  • 如果不想要copy构造函数和copy assignment运算符,可以将其声明为 private
  • 如果base class 将copy assignment运算符声明为 private,那么编译器将拒绝生成一个copy assignment运算符,毕竟编译器为derived classes所生的copy assignment运算符应该处理base class成分。

建议学习《深度探索C++对象模型》


若不想使用编译器自动生成的函数,就该明确拒绝

通常,如果不希望class支持某一特定机能,只要不声明对应函数即可。但这个策略对copy构造函数、copy assignment运算符不起作用,原因是编译器会为你声明它们。
如果我们不想class支持copy功能,需要明确拒绝它们。

将copy构造函数、copy assignment声明为private

这样可以成功的阻止外部调用。
然而这个做法并不绝对安全,因为 member 函数和 friend 函数依然可以调用,更聪明的做法是将其声明为 private 而且故意不去实现它们,这样即使有人不想用调用也会发生链接错误,从而达到提醒的效果。

class HomeForSale
{
public:
private:
    HomeForSale(const HomeForSale&);                    //仅有声明
    HomeForSale operator=(const HomeForSale&);

};

继承Uncopyable

将链接期错误移到编译期是可能的,而且也是更好的。
方法是:继承一个 专门设计阻止copying行为的base class。

class Uncopyable
{
protected:
    Uncopyable() {}                     //允许derived对象构造和析构
    ~Uncopyable() {}
private:
    Uncopyable(const Uncopyable&);      //阻止copying
    Uncopyable operator=(const Uncopyable&);
};

Uncopyable的实现和使用颇为微妙,

  • 不一定是 public 继承
  • 析构函数不一定得是 virtual(因为不是多态)

//TODO 一些微妙的地方需要回顾查看

C++11 =delete关键字

C++11 提供 delete 关键字,驳回class提供的机能。

class HomeForSale
{
publicHomeForSale(const HomeForSale&)=delete;            //仅有声明
    HomeForSale operator=(const HomeForSale&)=delete;
private:
};

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

当derived class 对象经由一个base class指针 删除,而该base class析构函数为 non-virtual,实际执行时通常发生的是对象的derived成分没有被销毁。

class Base
{
public:
    Base() { std::cout << "Base()" << std::endl; }
    ~Base() { std::cout << "~Base()" << std::endl; }
    virtual void print() { std::cout << "Base" << std::endl; }
};

class Derived:public Base
{
public:
    Derived() { std::cout << "Derived()" << std::endl; }
    ~Derived() { std::cout << "~Derived()" << std::endl; }
    virtual void print() { std::cout << "derived" << std::endl; }
};

int main()
{
    Base *pbase = new Derived();
    delete pbase;
}

//执行结果
Base()
Derived()
~Base()

消除这个问题做法是base class 析构函数改为 virtual

class Base
{
public:
    Base() { std::cout << "Base()" << std::endl; }
    virtual ~Base() { std::cout << "~Base()" << std::endl; }
    virtual void print() { std::cout << "Base" << std::endl; }
};

它既能保证自身的析构,也能完成派生类对象的析构,似乎把所有的class 析构函数都声明为 virtual 是个好的选择?
那就大错特错了。
将一个不含 virtual 函数的class的析构函数声明为 virtual,是一个影响效率的行为。从虚函数的实现原理来看这个问题,对象维护一个vptr(virtual pointer)指针,指向一个由函数指针构成的数组,称为vtbl(vitual table),每一个带有 virtual 函数的class都有一个相应的vtbl,当对象调用某一 virtual 函数,实际被调用的函数去觉悟该对象的vptr所指的哪个vtbl,编译器在其中寻找适当的函数指针。
而vtbl由构造函数负责生成,在没有 virtual 引入,C++ class 效率 和C几乎一致,引入 virtual 是必然引起效率降低的。从内存上看似乎对象多维护了32bit或64bit的指针,然而背后还有一个虚函数表vtbl。
因此,无端的将所有class的析构函数声明为 virtual 是错误的行为。

给class一个 virtual 析构函数 规则仅适用于多态基类
一些类或基类本身不服务于多态,比如item 6的Uncopyable,STL中的 string
当你继承自他们时,不要妄想用其base指针指向该对象而后 delete,他们大概率没有 virtual 函数,更不会提供 virtual 析构函数

总结:

  • 带多态性质的基类应该声明 virtual 析构函数。许多人的心得是:如果class带有任何 vitrual 函数,应该声明 virtual 析构函数
  • classes的设计目的如果不是作为 base classes使用,或不是为了具备多态性,就不该声明 virtual 析构函数。

别让异常逃出析构函数

C++异常处理概念参考菜鸟教程

当执行一个 throw 语句,在 throw 语句之后的语句将不再执行,导致在调用栈上的函数可能提早退出。

构造函数与异常机制

构造函数中使用异常机制是合理的,构造函数抛出异常表明构造函数没有执行完,对象没有真正的生成,因此对应的析构函数不会自动调用。

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

class A
{
public:
    A(): _a(new B()) 
    {
        cout << "A()" << endl;
        throw -1;
    }
    ~A()
    { 
        delete _a;
        cout << "~A()" << endl; 
    }
private:
    B* _a;

};

int main ()
{
    try {
        A();
    } catch(int a) {
        cerr << "exception: " << a << endl;
    }
   return 0;
}

//执行结果
B()
exception: -1

不会调用 ~A(),导致内存泄漏
方法是使用RAII机制,即资源获取时初始化,参考第3节//TODO

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

class A
{
public:
    A(): _a(new B()) 
    {
        throw -1;
        cout << "A()" << endl;
    }
    ~A()
    { 
        cout << "~A()" << endl; 
    }
private:
    std::shared_ptr<B> _a;
};


 
int main ()
{
    try {
        A();
    } catch(int a) {
        cerr << "exception: " << a << endl;
    }
   return 0;
}

//执行结果
B()
~B()
exception: -1

析构函数与异常机制

C++不禁止析构函数吐出异常,但它不鼓励你这样做

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

class A
{
public:
    A(): _a(new B()) 
    {
        cout << "A()" << endl;
    }
    ~A()
    {
        throw -1;
        cout << "~A()" << endl; 
    }
private:
    std::shared_ptr<B> _a;
};


int main ()
{
    try {
        A();
    } catch(int a) {
        cerr << "exception: " << a << endl;
    }
   return 0;
}

//执行结果
B()
A()
terminate called after throwing an instance of 'int'
Aborted

这里析构函数向函数外抛出异常,将直接调用 terminator() 系统函数终止程序,抛出异常后,没有释放 _a,导致内存泄漏。
处理方法是将异常机制在析构函数中完成处理,有两种方式:

  • 若想抛出异常就结束,通过 std::abord() 完成
  • 也可以吞下异常,不做处理
~A()
{
    try
    {
        throw -1;
        cout << "~A()" << endl;
    } catch(int) {
        std::abort();               //终止       
    } 
}

//执行结果
B()
A()
Aborted
~A()
{
    try
    {
        throw -1;
        cout << "~A()" << endl;
    } catch(int) {
        //吞下
    } 
}

//执行结果
B()
A()
~B()

绝不在构造和析构过程种调用virtual函数

在base构造期间,virtual 函数不是 virtual 函数,更严格来说,在derived class对象的base class构造期间,对象的类型是base class 而非 derived class

容易记住的是:当进入base class 构造函数,对象成为base class对象,
进入derived class构造函数时,对象成为derived class 对象
析构函数同样如此

如果需要在构造函数中使用 virtual 函数,解决方案是:
non-virtual 函数替代,以参数传递形式实现

class A
{
public:
    A() { print(); }
    virtual void print() { std::cout << "A::Print()" << std::endl; }
};

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

int main()
{
    B b;
    return 0;
}

//执行结果
A::Print()
class A
{
public:
    A(const std::string &log) { print(log); }
    void print(const std::string &log) { std::cout << log << std::endl; }
};

class B : public A
{
public:
    B() : A("B::print()") {}
};

int main()
{
    B b;
    return 0;
}

//执行结果
B::print()

令operator= 返回一个reference to *this

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

class Widget
{
public:
    Widget& operator=(const Widget& rhs)
    {
    ...
    return *this;
    }
};

不使用引用导致效率降低,
返回类型如果是 void 无法写成连锁形式


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

自我赋值发生在对象被赋值给自己时,听上去有些愚蠢,但无法保证不会发生这种情况。
比如

a[i] = a[j];            //潜在的自我赋值
*px = *py               //潜在的自我赋值

如果 i=j 则为自我赋值,pxpy 所指相同也是自我赋值

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

这是个不安全的代码,原因就在于自我赋值时,会使用指向已 delete 的对象,同时,也存在异常安全性的问题
new 抛出异常,Widget 持有一个已被 deletepb,这样的指针有害

传统的做法是进行一次证同测试

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

这样做确实解决了自我赋值安全性的问题,却依然存在异常方面的麻烦(不论是因为分配时内存不足或因为Bitmap的copy构造函数抛出异常)Widget最终会持有一个指针指向一块被删除的Bitmap

令人高兴的是,让 operator 具备异常安全性往往会自动获得自我赋值安全性的回报,因此焦点被放到了异常安全性的处理上。
许多时候一群精心安排的语句就可以导出异常安全的代码

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

现在,如果new Bitmap抛出异常,pb会保持现状,即使没有证同测试,也能够处理自我赋值

另一个替代方案是 copy and swap 技术,它是一个常见而且够好的 operator= 撰写方法

class Widget
{
public:
    void swap(Widget& rhs)             //交换*this 和rhs的数据
    {
        std::swap(this->pb, rhs.pb);
    }
    Widget& operator=(const Widget& rhs)
    {
        Widget temp(rhs);               //为rhs数据制作一个附件
        swap(temp);                     //将*this数据和上述附件的数据交换
        return *this;
    }
private:
    Bitmap *pb;
};

一个更伶俐巧妙的做法是:使用值传递pass-by-value方式接受实参,值传递传参会构造一个临时副本,利用这种特性将传参和制作附件的过程合并,有缺陷是这样的做法牺牲了清晰性。

Widget& Widget::operator=(Widget rhs)
    {
        swap(rhs);
        return *this;
    }

复制对象勿忘其每一个成分

设计良好的面向对象C++系统具有良好的封装性,仅留两个函数负责对象的拷贝:copy构造函数和copy assignment 操作符,称之为copying函数。
如果你声明自己的copying函数,意味着告诉编译器你不喜欢编译器生成的默认copying函数,编译器似乎被冒犯,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时,却不告诉你

void logCall (const std::string& funcName)
{
    std::cout << funcName << std::endl;
}

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;
}

看起来不错,确实也不错,直到另一个成员变量加入

class Data {...}
class Customer
{
public:
...
private:
    std::string name;
    Date lastTransaction;
};

新增了成员变量 lastTransaction,copy函数复制了 name ,但没有复制 lastTransaction ,大多数编译器没有发出任何提示或警告。如果你忘了,编译器不太可能提醒你。

另一个潜在的危机是出现继承的情形

class PriorityCustomer: public Customer
{
public:
    PriorityCustomer(const PriorityCustomer& rhs);
    PriorityCustomer& operator=(const PriorityCustomer& 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;
}

看上去复制了了 PriorityCustomer 内的每一样东西,但是却没有复制它所继承的Customer成员变量附件
任何时候只要你承担为derived class 撰写copying函数的责任,必须很小心的复制base class部分,那些成分往往是 private 的,因此应调用相应的base的copying函数

PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs) : 
Customer(rhs),                  //调用base的copy构造函数
priority(rhs.priority)
{
    logCall("PriorityCustomer copy constructor");
}
PriorityCustomer& PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
    logCall("PriorityCustomer copy assignment operator");
    Customer::operator=(rhs);   //对base成分进行复制操作
    priority = rhs.priority;
    return *this;
}

注意:

  • copying函数应该确保复制对象内所有成员变量以及所有base class成分
  • 不要尝试以某个copying函数实现另一个copying函数,应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

关于第二点:copying函数实现另一个copying函数指的是:

  • 令copy构造函数调用copy assignment操作符
  • 令copy assignment操作符调用copy 构造函数

这两种行为都是不合理的,最佳实践:
如果copy构造函数和copy assignment操作符有相近的代码,消除重复代码的方式是:
建立一个新的成员函数给两者共用。这样的函数往往是 private 而且通常命名为 init ,这个策略可以安全消除代码重复的问题。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值