《More Effective C++》 笔记

本文详细解读了《More Effective C++》中的重要条款,涵盖指针与引用的区别、类型转换、多态处理数组的注意事项、避免默认构造函数的滥用、操作符重载策略、异常处理的最佳实践、效率优化技巧以及技术选择等多个方面,旨在帮助开发者深入理解C++编程的高级技巧和潜在陷阱。

条款1: 仔细区分 pointers 和 references

pointers 是一个变量,其本身存放实际内容的地址
references 是一个引用,其就是实际内容的别名

两者都支持多态但是还是有一定区别的

pointer 在进行创建的时,不一定要立即给定一个准确值,虽然发生这种情况时,通常会赋予一个 null,来区别当前指针未进行初始化,杜绝由此产生的野指针,而 pointer 也是可以进行后续修改的,如果后续有该种需求,一个 pointer 可能会指向不同的对象,并利用多态来实现接口实现的切换,如设计模式中的 策略模式 ,就该用 pointer

    //如果在某处定义了一个指针,在使用前必须要先进行检测才进行使用
    string* _s = nullptr;
    /*
    ...
    */
    if(_s){
        /*dosth*/
    }
    
    

reference 是一个引用,相对于 pointer ,其在声明时就必须被指向具体的内容,也就是必须要有一个初始值,且在后续逻辑中不能进行改变, 当实现某些操作符的时,也需要使用 reference 否则语法会让人产生歧义 如 operator[]

    char* _c_p = nullptr;
    char& _c_ref = *_c_p;

这种写法是不允许存在的, reference 不能指向 null ,会产生未定义的结果

所以从两者中进行选择不会有太大的困难,如果后续有切换指向内容的需要就使用指针 pointer ,否则使用引用 reference

条款2: 最好使用 C++ 转型操作符

程序中很难避免发生转型操作,而转型操作通从用小括号加上对象类型来进行转换,

    (type) expression

很难在外部统计的时候知晓当前程序是否有转型操作,所以有了以下几种转型操作符

    static_cast<type> expression

该操作符可以进行类型的转换

//如果需要将一个 double 转换成 int
double _d = 12.3;
//C形式转换
int _i_old =  (int)_d
//新转型操作符转换
int _i_new = static_cast<int> (_d)
    const_cast<type> expression

该操作符可以改变变量的常量性或易变性

    int _val = 10;
    const int& _i_const = _val;
    int& _i_non_const = const_cast<int&>(_i_const);
    _i_non_const = 20;
    std::cout << _val << std::endl;

类似于上述例子,该操作符可以改变 const 的属性,添加或删除,但是也仅限于此了,不能进行类型的转换

    dynamic_coast<type> expression

动态类型转换,如果转换不成功,指针会为0,引用会抛出异常.
用该转型操作符,由于可以知晓是否转换成功,所以可以安全的进行转换.

    reinterpret_cast<type> expression

强制类型转换,其会无视一切规则强行转换,所以很危险,在非绝境时不要进行这种转换.

条款3: 绝对不要以多态方式处理数组

    class BST {};
    class Derived : public BST {};
    BST _b_arr[10];
    
    for (int i = 0; i < 10; i++) {
        _b_arr[i];
    }

上述情景中,_b_arr是很危险的,当对其进行遍历时,编译器会按照父类BST 的大小进行遍历,但是如果数组中存在一个子类 Derived ,那就会出现未定义的行为.
通常认为,子类的大小是大于父类的,在编译器遍历的时候,将不知晓当前对象的实际类型,产生错误的跨度,导致未定义的结果

条款4:非必要不提供 default constructor

如非必要不需要提供 default constructor

default constructor 指的是默认构造函数,也就是一个对象即使不提供任何参数也可以被构造出来

    SClass _c();

但是这么做实际上会造成很多实际逻辑中的困扰,如果一个对象必须有一个 唯一 id 来进行 初始化,那么进行了默认构造初始化,在某一个时刻,将会允许没有 id 的对象产生,这样明显是危险的,但是如果不提供 default constructor ,那么在进行 stl 使用时无法进行方便的使用,因为 stl 往往需要一个 def ctor 来进行初始构造,且在一个继承链中,所有的子类也必须要向上进行跟踪了解构造函数的意义,保证不会出现差错.

根据书上对于 stl 的解决方案可以知道即使不提供 def ctor ,我们也是有很多办法来解决上述问题,并且减少了很多的复杂度

操作符

条款5: 对定制的"类型转换函数"保持警觉

    class Rational{
    public:
        operator double(){}
    }
    

类型转换函数虽然很方便,但有些时候会造成编译器的强制匹配造成一些无法预见的结果,所以如果真的有转换的需求,最好是通过明显的方式来进行转换

    class Rational{
    public:
        double asDouble(){}
    }
    

这样的行为明显是通过大众认可的,就比如string 类型需要转换成 const char *时,并不是提供类型转换函数,而是通过 c_str 进行的显示转换

条款6:区别 increment/decrement 操作符的前置(refix)和后置(postfix)的形式

//refix
Int& Int::operator++(){
    *this += 1
    return *this;
}

//postfix
const Int Int::operator++(int ){
    Int _old_value = *this;
    ++*this;
    return _old_value;
}

关于前置和后置的问题可以通过源码进行分析,包括一些奇妙用法,

通过源码可以看出, 后置由于一个临时变量,书面上来说,效率就会低于前置操作符,所以在编写代码时候尽量使用前置操作符

再说复杂的操作符使用方式

Int _i = 10;
++++_i
_i++++

从代码角度分析,上述两种写法只有++++_i可能会达到我们的编写目的,其返回的自身的引用,而_i++++后一个后置操作符操作对象其实是一个临时变量,且 operator 是一个 none const member function ,也不能执行该种操作

条款7:千万不要重载 && || , 三个操作符

当操作符号被修改的,假如以下形式:

    operator&&(expression1,expression2)

那么 expression1,expression2 的其内容的先后顺序将是未定义的,取决服编译器

 i++&&i--

会发生未定义的结果, 可能i++先执行也可能i–先执行

并且如果重载了操作符,那么对其他维护人员来说将是一场灾难,并不会有人有人想到会有人去重载,即使可以进行重载,并且当重载内容完全颠覆了缘由的含义,那么会造成更严重的后果

条款8:了解各种不同意义的 new 和delete

通常我们使用的 new 都是new operator 操作符,在声请内存的同时,调用构造函数进行初始化,如果我们希望只申请而不调用构造函数,可以使用 operator new 来进行

    void* _point = operator new(sizeof(char));

还有一种 placement new 定位初始化,

    void _point = (point_)new(10);

无论是 operator new 还是 new operator 都是从 heap 上申请内存,而 placement new不同,会将对象内存创建在我们给定的地址上面,并调用构造函数,通常作用域内存池中,给定指定的地址,进行构造.
但是跟 operator new 和 new operator 不同,其没有对应的 delete 函数,因为其内存并不是由该操作符声请的,所以在释放内存时,还是要根据内存的不同申请方式来进行对应的释放

    operator new[]
    operator delete[]

数组内存声请,会声请一片连续的内存,并根据传入的大小来声请对应的大小,功能类似于 operator new ,除了释放时需要使用 operator delete[] 来进行释放,其他基本一致

条款9:利用destructors 避免泄露资源

考虑一下这样的代码

class SthClass{}
void foo(){
        
    SthClass* _sth = createSthClass();
    _sth->doSth();
    delete _sth;
}

正常情况下,代码很正常, _sth 被创建出来,使用完毕后我们正常的释放他

直到 _sth->doSth() 抛出一个异常为止.
当抛出异常时,当前的操作权限会被交给 foo() 函数的调用端,但是我们已经不能释放 _sth 了,丢失了记录地址的指针,也造成了内存泄露.

解决方法为使用句柄类来对 SthClass 进行维护,使用智能指针以及专属的句柄handle 类都可以完成目的
构造函数完成对 SthClass 的包裹,析构函数的时候完成对 SthClass 的释放,即使发生了异常,也不会出现内存泄露的情况

条款10:在 constructor 内组织资源泄露

class SthClass{
    SthClass():m_param_1(new string("string")),m_param_2(new string("string")){}
    
    string* m_param_1;
    string* m_param_2;
}

类同与条款9, 在 构造函数进行多个指针的内存分配时,也会发生内存泄露的情况,更惨的是,由于构造函数未完成, destructor 并不会被处罚,所以即使在构造函数中进行了对应指针的销毁操作,实际上也不会被执行到,所以,当然,我们也可以对new operator 行为进行函数的包装处理,然后对其进行异常捕捉来消化异常,但是这么做会增加代码的复杂度,难以维护,所以对于该种情况,解决办法为不要直接使用裸指针,而是使用智能指针进行包裹,在抛出异常时会自动进行资源销毁

条款11: 禁止异常流出 destructors 之外

~SthClose{
   sth_0();
   sth_1();
}

在析构函数中,不能让异常逃脱脱离当前调用的控制,一旦发生逃脱,析构中的代码可能会无法执行完全,导致逻辑的错误,如上述代码,如果 sth_0 抛出了异常 sth_1的代码将不会被执行,如果是一个与构造函数的对称句柄维护函数,也就造成了内存泄露的后果

~SthClose{
   try{
       sth_0()
   }catch(...) {
       //通过 ... 捕捉所有异常并进行拦截
   }
   try{
        sth_1()
   }catch(...) {
       //通过 ... 捕捉所有异常并进行拦截
   }
    
}

条款12: 理解"抛出一个异常"与"传递一个参数"或"调用一个虚函数的差异"

当异常被抛出时,鉴于不同catch参数的不同,异常被拷贝的次数也将不同,

catch(EXception e)              将会发生2次拷贝
catch(EXception& e)             将会发生1次拷贝,引用的其实是临时变量
catch(const EXception& e)       将会发生1次拷贝,引用的其实是临时变量

后两种情况在非异常的代码中是一个减少拷贝的好办法,但是在异常处理中,异常是可能作为一个局部变量被创建出来的.所以编译器会对其进行一次默认拷贝,即使是被抛出的是静态全局变量,也会被拷贝
基于这种情况,将整套异常以指针的形式进行抛出.能有效的减少拷贝,但必须保证异常位于静态变量区或堆上.

class BaseException{}
class DerviedException:public BaseException{}
try{
    throw  new  DerviedException();
}
catch(BaseException*){
    //将优先被捕捉处理
    delete e;
}
catch(DerivedException*){
    
    delete e;
}

在catch一个继承链的异常中,用的是顺序优先原则而不是最佳匹配原则

class BaseException{}
class DerviedException:public BaseException{}
try{
    throw  new  DerviedException();
}
catch(DerivedException* e){
    //将优先被捕捉处理
    delete e;
}
catch(BaseException* e){
    delete e;
}

须将派生类至于基类的前方,否则将不会被运行,编译器也会发出警告

条款13: 通过引用 refrence 捕捉异常

异常的抛出有三种方式

catch by pointer
catch by value
catch by reference

从性能角度来说, pointer 无疑是最优选择,没有发生拷贝,只不过是需要在 catch 代码块中对 pointer 进行一个维护操作

try{
    throw new Exception()
}
catch(Exception* e_){
    
    //dosth
    
    delete e_
}

上面看似美好,但是如果抛出的异常是 global 或者是 static

try{
    static Exception _s_e;
    throw &_s_e;
}
catch(Exception* e_){
    
    //dosth
    
    delete e_
}

那么catch 代码块要怎么样才能知道不需要进行删除操作呢,虽然提升了效率,但是在代码的编写中,无疑是一个负担
从无数的代码优化,代码架构书籍中都有这一点的描述,代码越少越好,不需要潜规则

那么似乎 catch by value 是一个更好的选择,但是实际上更糟, catch by pointer 只不过是造成了代码编写的困扰,
而catch by value 可能已经破坏了代码的逻辑,当catch中为基类,抛出的是派生类对象时,将发生切片动作,多态性质也完全消失了.

所以权衡下来,只有 catch by reference 是我们的最佳选择,虽然有一次拷贝,但是免去了维护的烦恼以及保持了多态的特性.

try{
    
    throw DerviedException();
}
catch(BaseException& e_){
    
    //dosth
}

条款14: 审慎使用异常规格

异常规格为在函数最后给出当前函数会抛出的异常,用来提醒使用者和后续维护者,但是当抛出的异常不同于事先声明的异常格式时,会调用全局的 unexpected 函数,缺省情况下会终止程序,但是要整个逻辑调用链都进行异常规格的遵守是很困难的事,特别是当前需要额外的回调定义时,很难去把控外部传入的回调函数,特别是当编译器无法识别这种行为时.
如果真的需要使用异常规格
一个办法是定义一个基类作为捕捉的类型,派生类作为真正的实现.并重写 unexpected 函数,进行异常抛出行为的纠正.
异常规格是一个应被审慎使用的特性,在使用前必须考虑带来的行为是否是你希望的行为

条款15: 了解异常处理的系统开销

异常功能是需要一定开销的,即使是完全没有进行使用,虽然在某些情况下可以进行异常功能的关闭,但前提是,当前的所有代码所有模块都没有进行异常功能的使用,一旦有一个模块使用了异常,将导致程序无法运行.

抛出异常这个工作是比较消耗资源的,相对于平常的函数返回值,大约是3倍的资源消耗,但是不必恐慌,除非将异常作为了一种常规手段,否则偶尔的使用基本是不会影响整体效率的

异常功能整体上会使程序变大 5%~10%,同时也一定比例的减慢程序的运行速度.

这就是异常处理的系统开销

效率

条款16: 谨记 80 - 20 法则

一个程序大量的资源是消耗在少部分的代码上面,所有的程序都符合这个规则,所以,我们要做的并不是对每一处代码都进行优化,虽然这么做固然很好,但是每个人的能力和精力是一个固定值,一味的优化80%部分的代码,提升的效果可能达不到20%中的几行代码,我们要善于利用各种工具,找到真正需要进行优化的逻辑,然后去进行优化.

条款 17: 考虑使用 lazy evaluation (缓式评估)

缓式评估 lazy evaluation 核心为尽可能的延迟各种消耗资源的行为的发生时间,只在必须需要的时候才进行资源的获取,如一个对象有10个属性,分别需要从数据库进行获取,但是往往在一个逻辑内只需要1条属性进行运算,那么一次性加载10个属性是没有必要的,所以以 lazy evaluation 的核心思想,代码上需要进行一下设计,尽可能晚的进行属性从数据库获取数据的行为.

class exsample{
    Attr* _attr = nullptr;
}

void somtLogc(){
    if(!_attr){
        _attr->queryFormDb();
    }
    _attr->dosth();
}

书上举例,由于 lazy evalution 的存在,底运行效率的cpu 也可以进行 矩阵的加减乘除,计算矩阵时候一般也只是计算一部分的数值,而 lazy evalution 就能完美的进行性能的优化.

需要注意的是,lazy evalution 并不是万能的,可以说可以使用的条件也是苛刻的,必须是一个行为如果全部执行时会对当前的系统资源进行明显额外消耗为前提,且如果一个行为内产生的数据实际在进行逻辑时是全部需要进行计算的,那么该种策略也是失败的,一味的增加了代码复杂度而已.
在C++中,当一个类的加载明显出现瓶颈时,那么将需要加载的成员进行 lazy evalution 策略,那将在合适不过了

条款18:分摊期望的计算

与条款17完全相反的观点,核心为利用缓存进行数据的预处理,如果某个值的计算需要一定的资源,且在一个较长时间内不会被频繁的更新,那么我们可以将这个结果进行一个缓存,在需要值的时候直接返回结果.

用额外的空间来减少资源的消耗,如 vector 中也是这种思路,如果当前的空间不够用,不会申请刚好够用的空间,而是额外申请一倍的空间,用来减少对系统调用 operator new 的次数

条款19:理解临时对象的来源

临时对象没有名字,是在函数运行过程中自动生成的

void foo(cosnt string& str_);
中,如果传入的是 C 类型字符串则会发生了一次隐式转换,自动生成一个临时变量并与 str_ 进行一个绑定

void foo(string& str_);
当参数去掉const 标志时,将不会产生临时变量,函数的意义也变调了, 表示可能 str_ 可能会被修改,所以必须要与一个真正的 string 变量进行一个绑定.

const Number operator+(cosnt Number& lhs_, cosnt Number& rhs_ );

返回值也会有一个临时变量生成, 返回值的const 为了防止连续调用 operaotr+

临时对象的开销是不能忽视的,所以要尽量的去消除他们

条款20:协助完成返回值优化

仅从函数返回值来说,无法完全避免进行一个对象的构造,如果返回 T* 将会有释放问题,返回 T& 返回一个局部对象.

可以利用 constructor arguments 来起到编译器能做一些 return value optimization 优化,尽量的减少拷贝,但是无法避免 construct 的调用

更可以在函数声明前加上 inline 来减少调用函数的跳转

条款21: 利用重载技术(overload)避免隐式类型转换(implicit type conversions)

class UPInt {
    UPInt(int val_)
    cosnt UPInt operator+(const UPInt& lhs_,const UPInt& rhs_){}
}

如有上述代码,以及以下调用行为

UPInt _u_int;
const UPInt _tmp = _u_int +10;
const UPInt _tmp = 10 + _u_int;

虽然可以执行,但是实际上是因为发生了隐式转换, 10 实际上被构造成了一个 UPInt 临时对象,也就意味着一个对象的生成,这个开销可能会让我们对 operator+ 发生效率的低估

所以如果不希望发生隐式转换,我们可以进行重载行为来阻止隐式转换的发生

cosnt UPInt operator+(const UPInt& lhs_,const int& rhs_){}

条款22: 考虑以操作符复合形式(op=)取代其独身形式(op)

cosnt UPInt operator+=(cosnt UPInt& lhs_,cosnt UPInt& rhs_);
cosnt UPInt operator+(cosnt UPInt& lhs_,cosnt UPInt& rhs_){
    return UPInt(lhs_) += rhs_;
}

将独生形式的实现用复合形式进行实现,可以稳定两者的实现方式,而且可以利用 template 来进行独生形式的统一定义

templat<class T>
const T operator+(const T& lhs_, const T& rhs_){
    return T(lhs_) + rhs_;
}

且 return T(lhs_) + rhs_ 通过临时变量进行返回,更高的几率进行 return value optimization ,返回值优化

条款23: 考虑使用其他程序库

在使用一个程序库的时,理因知道可以了解有什么类型功能的程序库,出于或性能或者调试等理由的考量,两者各自的优劣在什么时候会区分出差异,并在代码中静态或动态的给出一定的切换空间,让程序在适合的环境能给出适当的运行结果

条款24: 理解虚拟函数,多继承,虚继承和RTTI (run time type Identification)所需的代价

C++ 中的多态是有 virtual point 和 virtual table 来进行实现的,每个派生类如果有虚函数,则会有一个虚函数指针用来指向虚函数表,而当前class 中 none virtual functions 就和普通函数实现一样,被放在代码区,所以对于 none virtual functions来说,即使在一个派生类中也没有额外的资源消耗,虚函数指针会让当前 class 多出一个地址字节,会让class 的大小变大,变大理所当然也意味着程序执行效率的降低,虽然每个编译器的实现都不同,但可以把 vptr 想象成放在内存结构的尾端,当虚函数被调用时,会找到对应的虚函数指针再找到对应的虚函数表格,通过运行时确认需要真正被调用的函数

pc1->f1()

(*pc1->vptr[i])(pc1)

在多继承的时候,相当于单继承也就是在结构中对应的多出了 相应的vptr

在虚函数前面加上 inline 是没有意义的,inline 是在编译器直接展开,减少运行时的调用,而虚函数往往意味着多态调用,而多态只有在运行时才能知道具体的执行函数,除非用 对象而非引用或指针,

RTTI 让我们可以在运行时获得 objects 和 classes 的相关信息,这个结构是type_info, 该结构可能会被放在 vtbl 的某个固定位置上

更多的相关信息,可以参考 <<inside c++ object model>> 进行了解

技术

条款25: 将 constructor 和 non_member functions 虚化

构造函数是不能进行多态的,但是在很多情况下,基于多个相同的基类的派生类,其构造方式可能是不同的,也必须允许不同的情况,如下代码

class Base{
public:
    Base(const Base& other_base_)
}


class DerivedOne{
public:
    DerivedOne(const DerivedOne& other_base_)
}


class DerivedTwo{
public:
    DerivedTwo(const DerivedOne& other_base_)
}

class BaseList{
public:
    vector<BaseList*> m_vector
    
    BaseList(const BaseList& other_base_list){
        for(auto& _it:other_base_list)
            m_vector.push(new Base(*_it))
    }
}

如此代码,BaseList的copy constructor 将无法发生作用,被用于拷贝的对象将全部被退化成 class Base ,也就是对所有的对象进行了一次切片行为,明显是不满足我的需求的,这时我们就需要一个 virtual constructor 来满足我们.
当然, virtual constructor 并不是指的将 structor 加上 virtual 标识符,这是不允许的.
所以我们退而求其次,将具体的构造行为通过专为这个行为而编写的函数来处理

class Base{
public:
    Base(const Base& other_base_);
    virtual Base* clone() = 0;
}


class DerivedOne{
public:
    DerivedOne(const DerivedOne& other_base_)
    virtual DerivedOne* clone(){
        return new DerivedOne(*this)
    }
}


class DerivedTwo{
public:
    DerivedTwo(const DerivedOne& other_base_);
    virtual DerivedTwo* clone(){
        return new DerivedTwo(*this)
    }
}

class BaseList{
public:
    vector<BaseList*> m_vector;
    
    BaseList(const BaseList& other_base_list){
        for(auto& _it:other_base_list)
            m_vector.push(_it->clone());
    }
}

不同的返回值无法区别成不同的函数,所以派生类中的虚函数可以允许不同的返回值,通过该办法,就可以完成类似于 virtual constructor 的功能.

而 none member functions 也是上述的思路

void foo(Base* base_){
base_->member_foo();
}

通过实现各自的 member_foo 函数来达到, none member functions 的多态功能,也就是 foo 专门用来运行 member_foo() 用来运行各自的实际函数

条款26: 限制某个 class 所能产生的对象数量

本条款是用单例模式思路来进行限制,保证当前对象数量处于我们的控制之中,在只允许一定数量的对象时,用智能指针的思路进行维护,在超过限制时抛出异常.
但是,个人看来,这么做的成本也太高了,这里的成本并不是指的运行开销,而是代码维护上的成本,首先,每个想获得此功能的类需要继承一个 模板类,且在类进行内存申请时,需要进行try catch 维护, 然而, try catch 对于一般程序来说就是一场噩梦,这方面在本书的其他条目中也有所提及,还不如创建时直接返回一个空指针,类似于内存池的思路,在使用时来维护一个对象池.
也应该抛弃继承来实现对应的数量控制功能,当一个普通类继承了这种目的性强的功能类后,其实际上就已经进行了强绑定,但是实际上两者基本上是没有什么逻辑关系的,就是为了多一个控制功能,好的办法是,将这种数量控制功能单独做一成一个类,用对象来管理对象,减少两者之间的耦合度,一旦我们取消了数量控制功能,或者替换了数量功能的逻辑,我们就只需要修改一个类就可以满足,而不用去找出所有继承了数量控制类的类来进行纠正校对.

条款27: 要求(或禁止)对象产生于 heap 之中

假如出现一个需求,要求必须在 heap 上被申请,也就是不许在 stack 上被创建出来,该如何实现.

很简单,将desstructor 的访问权限设置为 private 即可,在临时变量区域,将允许被创建,因为析构函数被禁止外部调用.
当然随之也会有其他问题被暴露出来

如果该类是基类,那么需要将 destructor 改成 proteced, 可以让子类继承
如果该类需要被其他类以组合形式持有,那么持有时需要将其以指针的形式

class Base{
proteced:
    ~Base(){}
    void destory(){
        delete this;
    }
}

class Other{
public:
    Other():m_base(new Base()){}
    ~Other(){m_base->destory();}
    Base* m_base;
    
}

关于确定是否是heap 上的对象,没有完美的办法,只有在创建时进行追踪,然后在需要的时候进行判断

条款28: smart pointers 智能指针

智能指针是用来解决发生在 C++ 中内存问题,是一种漂亮的解决办法,但是在实际的实现中,会有很多问题让我们去解决
需要说明的是,书中的 auto_ptr 是C++98 的方案,在C++11中已经被抛弃, 并使用 unique ptr 来完全替代,但是这并不影响我们去了解 auto_ptr 毕竟,温故而知新

构造,赋值,析构

auto_ptr 是利用构造函数和析构函数来对存储在自身的 dumb point 进行维护,达到解决内存泄露的问题

在发生赋值的时,为了防止 delete 两次的操作发生,将会主动进行 dumb pointer 所属权的移交.

所以当 auto_ptr 作为参数进行传递时,绝对不能使用 by to value 的形式,当发生这种行为,auto_ptr 马上会被销毁,所以必须使用 by referece来传递

实现 deferencing operators 解引操作符

实现解引用操作符后,可以如同使用正常指针一般使用智能指针

operator *()
operator ->()
测试smart pointers 是否为null

通过重载

operator void*()
operator !()
operator bool()

都可以进行当前是否为null 的判断

将 smart pointer 转换为 dumb pointer

通过增加 类型转换操作符来进行实现

template <class T>
...
operator T*()
...

且顺带实现了是否为null的问题

smart pointer 和 继承有关 的类型转换

可以明显的看出,目前的 smart point 如果不进行特殊的处理,面多需要多态的功能将无能为力.

通过增加 template member funcition 来进行实现

template<class newType>
operator SmartPtr<newType>(){
    return SmartPtr<newType>(m_dumb_point);
}

条款指出,当编译器遇到传入参数和实际参数不匹配的情况时,会想尽一切办法去实现,就包括了去找各种函数来进行比较,尽可能的完成,所以就会找到我们给出的转换函数,也就完成了实现

Smart Pointers 与 const

通过 Smart point 的派生类我们可以做到完美的如果正常 cosnt 一样的 const 标志添加位.并使用 union 来解决只需要存在一个指针的内存即可

条款29: Reference counting (引用计数)

这项计数,在 c++11 中用 shared_ptr 来实现,对象被引用计数进行维护,在 shared_ptr 的引用计数为0时,进行内容的释放

如果一个对象在不同的地方进行共享,也就是同时存在指向一个对象的多个指针,那么如何维护这个对象的释放就是一个值得讨论的问题,解决方案为,构造一个指针维护对象,当多个指针存在时,用引用计数来表示当前有多少个指针句柄对象,即可在指针句柄对象被释放时,减少引用计数,在计数等于0时,将指向的对象进行删除操作

条款 30: proxy classes (替身类,代理类)

用代理类可以实现多为数组,区分左值/右值,压抑隐式转换.

但是实现的代价往往会产生一些临时兑现,需要被产生和销毁,也增加了软件的复杂度

条款31: 让函数根据一个以上的对象类型来决定如何虚化

如果map 存储目标碰撞指针,来决定两个多态之间的执行函数,当然如果要精确到具体的派生类和派生类的目标函数,还需要 自己 设计一套RTTI 手段,最简单的用string来表示

map<pair<string,string>,function<void()>>

条款32: 在未来时态下发展程序

代码是为了当前的计划来编写的,没有人能预见未来的需求,但是,我们可以在当前的代码保留一定的预见性.
努力写出可移植的代码,在使用时尽可能的使用跨平台的库,或者直接使用c++标准
当细节代码尽可能的内部化,private,对外只保留接口,避免写出 if else 的代码,这种代码就是维护的噩梦
如果当前类是几把类,写上virtual destructor

条款33: 将非尾端类(non leaf classes )设计为抽象类(abstract classes)

class Base{
private:
    virtual ~Base(){}
};

clas DerivedOne : public Base{
};

clas DerivedTwo : public Base{
};

Base* _base_one = new DerivedOne;
Base* _base_two = new DerivedTwo;
*_base_one = *_base_two; //需要禁止
Base* _base_two_cp = new DerivedTwo;
*_base_two = *_base_two_cp; //需要允许

当2个不同派生类作为基类指针时,其相互之间是可以进行赋值操作的,显然这样会造成内存的错误,肯定是不能允许的,那么该如何进行禁止呢?
当2个相同派生类多态为基类时,相互之间要允许进行赋值操作.

一个显而易见的方法是将赋值操作符 设置为 虚函数,在派生类各自实现时,使用 dynamic_case<>先进行转型判断,当错误的类型传入的时,该转型操作或返回错误指针或抛出异常,总能让我们能进行当前无法继续进行的判断


class Base{
private:
    virtual ~Base(){}
    virtual Base& operator=(cosnt Base& rhs_){}
};

clas DerivedOne : public Base{
    virtual DerivedOne& operator=(cosnt Base& rhs_){
        const DerivedOne& _tmp_derived_one = dynamic_case<DerivedOne& >(rhs_);
        *this = _tmp_derived_one;
    }
};

clas DerivedTwo : public Base{
    virtual DerivedTwo& operator=(cosnt Base& rhs_){
        const DerivedOne& _tmp_derived_one = dynamic_case<DerivedTwo& >(rhs_);
        *this = _tmp_derived_one;
    }
};

但是每次这么进行赋值,好像又有点不划算,在某些必须进行转型的情况下,每次进行向下转型,并可能抛出异常.

这时候将多态赋值操作符和当前赋值操作符分开即可

clas DerivedTwo : public Base{
    DerivedTwo& operator=(cosnt DerivedTwo& rhs_){
        if(this != &rhs){
            *this = rhs_;
        }
        return *this;
    }
    virtual DerivedTwo& operator=(cosnt Base& rhs_){
        return operator=(dynamic_case<DerivedTwo>(rhs_));
    }
};

但是还有个问题,我们需要在每次赋值的时候都对异常进行捕捉,否则如果类型不正确,异常会被向外传递.

解决方法为,将 赋值操作符设置为private,只允许相同类型进行赋值.

但是为了允许 Base 和 DerivedTwo 进行赋值,我们还是将赋值操作符设置为 protected 为好.

最后为了解决 Base 类之间的问题,我们将 Base 设置为抽象类.

最后结果为

class Base{
private:
    virtual ~Base() = 0
    Base& operator=(cosnt Base& rhs_){}
};
Base::~Base(){}
clas DerivedOne : public Base{
    DerivedOne& operator=(cosnt DerivedOne& rhs_);
};

clas DerivedTwo : public Base{
    DerivedTwo& operator=(cosnt DerivedTwo& rhs_);
};

当两个之间需要通过 public inheritance 来进行联系时,我们就需要考虑在两者之间增加一个抽象类,来使我们的设计更为灵活.

条款34: 如何在同一程序中混合使用c++和c

名转换

由于 c++ 有重载机制,所以在编译器实际进行链接时,会将函数名进行 magling 操作,但是 c 中没有重载机制,也就没有 magling 处理,所以如果一个函数是 c 中实现的,需要在声明前面上 extern “C” 用以区分.

extern "C"
viod foo(int,int);


extern "C"{
viod foo(int,int);    
viod fooTwo(int,int);
}

extern "C"{
    #include "someCHead.h"
}

上述写法均可以让编译器不进行 name magling 转换

静态初始化

虽然 main 是程序的入口,但实际上其并不是程序所做的第一件事.
在程序中有很多静态变量,其构造函数就是在 main 函数调用之前发生的.
这里就会有一件需要我们值得注意的事情,如果 c 的代码中混有 c++ 模块时, 就不能使用 C 的 main 作为入口,而是必须要使用 c++ 的 main 作为入口,否则,上述提到的静态变量构造函数将不会被调用,相应的,析构函数也会被丢弃.

extern "C"
int realMian(int argc_, char* argv_[]);
int main(int argc_, char* argv_[]){
    return realMian(argc_, argv_[]);
};
动态内存分配

C++ 内存分配为 new 和 delete
C 内存分配为 malloc 和 free
其之间的相互混用产生的结果是未定义的.
正常情况下我们一般不会去混用这两者
但是如果调用的是别人提供的库或函数,而调用方自己为你分配了一段内存,这时候就可能会出现混用情况,所以库为你分配了一段内存,一定要对应的使用他人对应给你的释放函数,不要擅自使用 delete 或 free

数据结构的兼容性

C++ 中的 struct 可以与 C 进行通信,但是有以下条件.
不能作为派生类, 可能是因为有 vptr?
不能有虚函数, 原因同上

只允许 C++ 版本的结构并包含非虚成员函数.

条款35: 让自己习惯使用标准 C++ 语言

多学多写多思考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值