阅读《Effective c++》第三版 day 2

发布时间 2023-12-04 00:26:28作者: 雨和风

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

​ 情况:使用父类指针接收子类对象,然后通过父类指针销毁该对象时,如果父类具有非虚拟析构函数,会发生切片现象 (子类对象通过基类指针赋值或传递给一个基类对象,只会保留子类对象中基类部分的数据。)为了解决这个问题,通常建议将基类析构函数声明为虚拟析构函数。


·别让异常逃离析构函数

​ 先看一个常见的类资源泄露的例子:假定资源偿还操作要独立的函数,不实现在析构函数中,而在其生命周期要结束的情况下,忘记调用函数偿还内存资源。

#include <iostream>
static int count = 0;
using namespace std;

class TimerKeeper{
public:
    TimerKeeper() : p(new int()){count++;cout<<"TimerKeeper()\n";}
    ~TimerKeeper(){ cout<<"~TimerKeeper()\n";}
    void release(){delete p;count--;}
private:
    int *p;
};
 
int main()
{
    cout<<count<<endl;
    {
        TimerKeeper p1;
        cout<<count<<endl;
        \\p1.release();
    }
    cout<<count<<endl;
    return 0;
}

可以借助另外一个类管理的方法来管理资源的回收:创建一个对使用类管理的类,然后通过指针获取该对象,需要注意的是,管理类使用指针管理使用类本身,要依据情况及时将使用类释放,否则会引发两次析构的错误存在。

#include <iostream>
static int count = 0;       //记录资源分配与收回情况
static int construct = 0;   //记录TimerKeeper 构造析构情况
using namespace std;

class TimerKeeper{
public:
    TimerKeeper() {++construct;cout<<"TimerKeeper()\n";}
    ~TimerKeeper(){if(construct>0)cout<<"~TimerKeeper()\n";}
    TimerKeeper* create(){
        ++count;
        return this;
    }
    void release(){
        --count;
    }
};

class ContrlTimerKeeper{
public:
    ContrlTimerKeeper(TimerKeeper &obj) : m_tk(&obj){
        cout<<"ctk()"<<count<<endl;
    }
    ~ContrlTimerKeeper(){
        if(count >0 && construct >0){
            m_tk->release();
            m_tk = nullptr;
        }
        cout<<"~ctk()"<<count<<endl;
    }
private:
    TimerKeeper *m_tk;
};
int main()
{
    cout<<count<<endl;
    TimerKeeper tk;
    ContrlTimerKeeper ctk(*tk.create());
  
    return 0;
}

·连续赋值,和自我赋值

int x = 5;
int y,z,w;
y = z = w = x; //过程实质为 y = ( z = (w = x));

这段就是使用了连续赋值,赋值运算符遵循从右到左的运算。如需要给我们自己设计的类实现连续赋值的话,应该为该类重载赋值运算符,并且是 reference to *this:

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

连续赋值只是一直可选的操作,并非是强制要使用的。
既然现在我们自定义的类可以用于被赋值,那就要关注一个问题:自我赋值。
假设类中定义有一个指针资源,并且是实现了operator=的赋值操作,我们声明两个类对象,让其都获取资源,使用其中一个类对象去赋值另一个类对象,如果不做处理,只是简单使用 ” = “一条赋值语句 如:

class Widget{...};
Widget w1,w2;
...
w1 = w1     //此处是明确在做自己赋值给自己的操作
w1 = w2     //那此处呢?这里可能是潜在的自我赋值操作

现在我们认为Widget的结构内有一个指针资源并包括operator= 的实现。如:

class MyclassForWidget{...};
class Widget{
    ...
private:
    MyclassForWidget* ptr;
};

Widget& operator=(const Widget& rhs){
    delete ptr;
    ptr = new MyclassForWidget(*rhs.ptr);
    return ptr;
};

如果说rhs和原来的*this是同一个对象的话,和明显ptr会接受一个指向被删除的对象。ptr的指向不明确,会很不安全。解决这个问题的方法就是对operato=实现做修改:

Widget& operator=(const Widget& rhs){
    if(this == &rhs)return *this    //先做判断对象是否是同一个

    delete ptr;
    ptr = new MyclassForWidget(*rhs.ptr);
    return *this;
};

这种做法可行,解决了自我赋值的问题,但还是不够安全。如果new失败,其返回的对象资源是delete ptr之后的,ptr的指向不明确的问题又来了,并且还有把问题异常扩散的风险。所以可以用更好的做法:

Widget& operator=(const Widget& rhs){
      
    MyclassForWidget* tempPtr = ptr;        //保存原先的ptr
    ptr = new MyclassForWidget(*rhs.ptr);   //如果new分配失败
    delete tempPtr;                         //ptr原来的指向也
    return *this;                           //不会改变,安全
};

另一种大多数人选择,但无法优化的版本:copy and swap:

class Widget{...};
void swap(Widegt &rhs){
    //也许使用了std::swap,但是标准库版本的swap需要指明拷贝构造函数
    //涉及到拷贝,那必须会影响到运行速度等,这就是该版本的缺点所在
    ...
}
Widget& Widget::operator=(const Widget &rhs){
    
    Widget temp(rhs);
    swap(rhs);
    return *this;
}

·脱离依赖”语句会被执行的“的想法,好好利用C++特性

​ 前面提到使用类来管理类,实际也可以不用那么麻烦,用标准库提供的share_ptr(T*)不就好了。这里提一下,使用shared_ptr对指针变量和对数组分配的方式有些不同:

//直接使用shared_ptr:
//对指针的:
template<typename T>
std::shared_ptr<T>spv(new T(),
            std::default_delete<T>());
//对数组的:
std::shared_ptr<T>spa(new T[],
            std::default_delete<T[]>());
//一般分配指针来说,第二个参数为可选项,但如果是数组,就要加上默认delete,否非编译器会不允许你通过

//使用make_shared会比较方便,但是对数组要使用容器,如vector<T>
template<typename T>
std::make_shared<T>(new T());

·在资源管理类中提供对原始资源的访问

在编写代码过程中,免不了使用API,但这些API都是固定好的要求某些数,如果我们要对我们的设计类调用API,应该从资源管理类中进行操作。因为要为安全着想,所以要给我们的资源管理类提供对原始资源的访问;

//法1:资源管理类提供一个显式转换函数
class FontHandle{...};
class Font{ //Font 管理 FontHandle
public:
    ...
    FontHandle get() const {return f;}  //为FontHandle提供显
    ...                                 //显式转换函数
};

//法2:资源管理类提供一个隐式转换函数
class Font{ //Font 管理 FontHandle
public:
    ...
    operator FontHandle() const {return f;}  //隐式转换函数
    ...
private:
    FontHandle f;       //隐式转换函数一般是在单一参数的构造
                        //函数或转换参数符                 
};
//隐式转换的缺点在于不明确
Font F1(getFont())      //这是返回一个FontHandle
Fonthander F2 = F1;     //究竟是拷贝一个Font对象,还是先隐式
                        //转换返回FontHander,若F1销毁,F2就
                        //成为一个安全隐患
//其他时候,标准库提供了获取原始资源的接口,在此不赘述

这里提多一嘴,要以独立语句将newd对象存入智能指针内,防止语句执行的某个时间点发生了错误,不能确认错误的出现处。


·隐式转换的可读性很差

当一个类中包含带有单一参数的构造函数时,该构造函数很可能被用于实

现隐 式类型转换。以下是一个简单的例子:

class Distance {
public:
    Distance(double meters) : meters(meters) {}

    // 隐式转换构造函数,将 Distance 转换为 double
    operator double() const {
        return meters;
    }

private:
    double meters;
};

int main() {
    Distance distance = 5.0; // 隐式转换发生,将 double 赋值给 Distance 对象

    // 隐式转换发生,将 Distance 转换为 double
    double distanceInMeters = distance;

    std::cout << "Distance in meters: " << distanceInMeters << std::endl;

    return 0;
}

在这个例子中,Distance 类包含一个带有单一参数的构造函数,该构造函数用于将 double 转换为 Distance 类型。因此,当我们使用 Distance distance = 5.0; 进行赋值时,发生了隐式类型转换。在 main 函数中,我们又将 Distance 对象转换为 double 类型,这同样是通过隐式转换构造函数实现的。