Effective C++阅读笔记

首先着重推荐这篇bolg,大神级总结https://blog.csdn.net/thefutureisour/article/category/1230423

条款1.C++和C,它是一个语言联邦,有很多东西一起存储

 C++ 是由C、面向对象、模板和STL组成,我们是站在C这个巨人的肩膀上进行的,C++ 引入类和对象的概念,进行了封装和抽象,非常重要的一点。模板由编译器自动执行,减少了代码的
冗长,提供复用性,不用提供所有的类型函数的重载版本。STL提供了优秀的C++模板库,以便程序员进行使用,特别棒。

条款2. 尽量以const、enum、inline替换#define,即少用全局变量

一、const和enum替换#define

1. 编译器处理方式不同

  • define宏是在预处理阶段展开。
  • const常量是编译运行阶段使用。

2. 类型和安全检查不同

  • define没有类型,不进行类型检查,仅仅是展开
  • const有具体的类型,在编译阶段会执行类型检查

3.存储方式不同

  • define宏仅仅是展开,有多少地方使用,就展开多少次,不会分配内存
  • const会在内存中分配(堆或者栈)

4.const可以节省空间,避免不必要的内存分配

const定义常量从汇编的角度来看,是给出了对应的内存地址,而不是和define一样是个立即数,所以cosnt定义的常量在程序运行过程中只有一份拷贝,而define定义的常量在内存中有若干个拷贝

  例: 
  #define PI 3.14159 //常量宏  
  const doulbe Pi=3.14159; //此时并未将Pi放入ROM中 ......  
  double i=Pi; //此时为Pi分配内存,以后不再分配!  
  double I=PI; //编译期间进行宏替换,分配内存  
  double j=Pi; //没有内存分配  
  double J=PI; //再进行宏替换,又一次分配内存!

5.const提高了效率

6.有些集成IDE的环境可以对const进行调试,而define是不能进行调试的

二、用inline替换带参的宏

带参的宏函数只是简单的进行替换,用起来四处危机,尤其是展开的时候,特别容易出错,bug不断

条款3.尽可能的使用const,const大法好呀

1. 修饰变量,不改表

2. 修饰指针

const int* p = &a;/* p为指向常量的指针,即p指向的对象是常量,不可以通过*p = 3 来修改a的值,但这时p = &b换个指向还是可以的 */

int* const p = &a; /* p为常量指针,即p的指向不可更改,不可以通过p = &b来修改p的指向,但这时*p = 3改变a的值还是可以的 */

const int* const p = &a; /* p为指向常量的常量指针,p的指向以及指向的对象都不可以更改,无论是*p = 3,还是p = &b都是错误的 */

3. 修饰函数的形参或者函数的返回值,保证传入的实参不在函数内发生改变,保证了安全

4. 修饰迭代器

const vector::iterator表示这个迭代器的指向不可以更改,即表示的是常量迭代器

5. 在类中修饰成员函数,一般放在成员函数的后面,保证这个函数不会改变类的成员变量

  • 重要:有无const是可以构成成员函数的重载的!,但是这种重载的相似度一般是非常高的,改动一个函数体内的内容,另一个就要同步更新,万一更新,会产生错误。
这种方式是可取的:
char& operator[] (size_t position)
{
   return const_cast<char&>(
     static_cast<const TestBlock&>(*this)[postion]);
}

条款4. 确定对象使用前被初始化

-(1) 为内置型对象进行手工初始化,因为C++不保证初始化它们;

-(2) 构造函数最好使用成员初始化列表(实际初始化顺序不与列表的排列顺序有关,只取决于类中的声明顺序),而不要在构造函数体内使用赋值操作;

-(3) 未避免“跨编译单元的初始化次序”问题,请用local static代替non-local static对象。 不要使变量有全局的作用域,用局部static

1.内置类型

c++的内置类型在定义时,会给你随机分配一个值,初值都是垃圾值,那么最佳的处理的方法在使用对象之前都将之初始化

2.STL,容器中写好了构造函数,自动初始化

3.自定义了类

尽可能使用初始化列表,

初始化成员变量的顺序与列表排列的顺序没有关系,只是取决于声明这些成员变量的顺序,下面的代码中就是先a,后b,然后text,因此一定要注意写顺序。

建议初始化类别中最好总是以声明的次序进行初始化
class A
{
private:
         int a;
         double b;
         string text;
public:
         A():a(0), b(0), text("hello world"){} //构造函数
};

槽的方法,如下:

class A
{
private:
         int a;
         double b;
         string text;
public:
         A(){
            a = 0;b = 0;text = "hello world";
         }
};
为什么很糟呢?

因为在进入构造函数的函数体时,这些成员变量已经被初始化了,a和b初始化成垃圾值,string因为是STL,调用默认的构造函数初始化为空字符串,在函数体内进行的操作实为“赋值”,也就是用新值覆盖旧值。这也正是说它的执行效率不高的原因,既进行了初始化,又在之后进行了赋值,

条款5. C++默默调用的函数

  • 构造函数、析构函数、拷贝构造函数、赋值构造函数

为什么赋值构造函数里的形参是加引用的?

  1. 引用修饰形参时,可以避免实参对形参的拷贝,一方面可以节省空间和时间资源,更为重要的是若实参对形参拷贝了,又会调用一次拷贝构造函数,这样拷贝构造函数就会一遍又一遍的被调用,造成无穷递归。
  2. 引用修饰返回值时,可以使返回的对象原地修改
EmptyClass a(b); // 调用的是拷贝构造函数
EmptyClass a = b; // 调用的是拷贝构造函数

EmptyClass a;
a = b; // 调用的是赋值运算符

特殊情况,成员变量是const或者是引用,编译会错,是拒绝生成默认的构造函数和赋值构造函数的

这是由于const和引用会在需要声明的时候进行初始化,而编译器提供的默认构造函数是无法做到这一点的,只能显示的调用构造函数进行。

条款6. 可以拒绝编译器为类自动生成函数,不让外部类来构造对应类的对象

  • 将不想被编译器的生成的函数写成private或者是protected,只能在类内部被访问,还有友元,如下:
class HomeForSale
{
private:
         HomeForSale(const HomeForSale& house){...}
         HomeForSale& operator= (const HomeForSale& house){...}
};
  • 如果是友元,是可以访问到private下的成员函数的,如果不想那么将定义变为声明就可以了(但是这种方法还是不太好,因为函数声明编译会过,但是链接需要函数进行实现,链接就会报错,毕竟错误越早发现越好)
class HomeForSale
{
private:
         HomeForSale(const HomeForSale&);
         HomeForSale& operator= (const HomeForSale&);
};
  • 可以继承的方法,限制拷贝,其实也是通过继承将访问修饰符变化
class Uncopyable {
public:
    Uncopyable() { }
    ~Uncopyable() { }
private:
    Uncopyable(const Uncopyable&);
    Uncopyable& operator=(const Uncopyable&);
};

class HomeForSale : private Uncopyable {
    ...     //这个时候该class就不要再声明copy构造函数或copy assignment操作符了!
}

这样,任何人,甚至是member函数跟friend函数尝试拷贝HomeForSal对象的时候。编译器就试着生成HomeForSale的拷贝构造函数跟赋值操作符。但是这个时候要先调用基类的拷贝构造函数和赋值操作符。这时候,编译器就会拒绝这样调用。因为基类的拷贝构造函数式private。

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

  • 1)带有多态性质的基类应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数

    如果没有virtual,在多态中,会出现内存泄漏,主要出现的原因是简单地析构了基类的部分,然后派生类被架空,delete后只释放了基类的内存,而派生类的内存没有释放。


有了virtual后,析构时会检查指针实际指向的对象,先定位到相应的派生类,完成派生类的析构之后,再析构基类。那么如何检查指针指向的对象呢,c++会为每个virtual关键字的类中创建一个vptr,指向一个由函数指针构成的数组,为虚表,运行时维护类中的虚表进行析构。
  • 2)若一个类不作为基类,或者不具备多态性(比如Uncopyable),就不该声明virtual析构函数。

    加上virtual确实是可以防止多态过程中析构造成的内存泄露,但是一旦出现virtual,编译器就会出现vptr,这是需要占用空间的,因此出现多态继承时一定要加上virtual,非基类或者不用与多态的基类就不要加

  • 3)纯虚函数,不能定义对象,不通过编译,就可以使得不想成为对象的类成为抽象类。但是有时难以找到一个纯虚函数,为了自然,可以将析构函数变为纯虚函数,如下:

class Shape
{
private:
         int color;
public:
         virtual ~Shape() = 0;
};

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

  • 1) 析构函数不吐出异常,应该在内部消化掉这些异常,防止多个异常同时存在
~Widget()
{
         try
         {…}
         catch
         {
            记录出错的位置
            终止程序
         }
}
  • 2) 如果客户需要对某个函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数,而非在析构函数中执行

条款9. 绝不在构造和析构函数中调用virtual函数:会破坏虚函数的特性

class Base
{
public:
         Base()
         {
                   VirtualFunction();
         }

         virtual void VirtualFunction()
         {
                   cout << "In the Base Class" << endl;
         };
};
class Derived: public Base
{
public:
         void VirtualFunction()
         {
                   cout << "In the Derived Class" << endl;
         }
};
int main()
{
         Derived d;
}

现象:上述例子中, 定义派生类对象时,会先构造基类,调用基类的构造函数,在基类的构造函数中调用了虚函数,如果按照多态的思路,行为的执行者应该是派生类的VirtualFunction(),也就是输出的是In the Derived Class,然而实际跑一下这个程序,运行结果却是In the Base Class。

本质:派生类部分必须是在基类部分构造完毕之后才会去构造,也就是说构造函数运行完毕后,函数进行返回,派生类才开始起作用,那么在base()中,只有派生类的VirtualFnction()运行结束才行,而现在虚函数表中也只有基类的VirtualFnction(),因此运行的结果是In the Base Class。

条款10. 令operator=返回一个引用指针

为什么赋值后需要有返回值呢?

例如:int x,y,z;x=y=z;等号是右结合的,因此有x=(y=z); 如果没有返回值的话,这个等式是不成立的,不能通过编译,而返回引用不是编译器强制的

  • 1) 返回引用可以节省资源,不必要为返回值调用构造函数

  • 2)形如(x=y)=常数值这样的连等,编译器也能接受,这样的写法要求赋值运算符返回的是可以修改的左值

  • 3)STL和boost库中的标准代码都是一个求德行,还是兼容着写吧,别胡搞了

  • 4)+=、-=等与赋值相关的运算,最好也返回自身的引用

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

  • 1)确保当对象自我赋值时operator=有良好的行为,其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap
  • 2)确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。
  • 浅拷贝:默认的构造就是浅拷贝,只是对指针的拷贝,拷贝完后两个指针指向同一空间
  • 深拷贝:深拷贝不但对指针进行拷贝,同时也对指针指向的内容进行拷贝,结束后指针是指向两个不同地址的指针。

以下两种方法最好了

SampleClass& operator= (const SampleClass& s)
{
         if(this == &s) return *this; //可以删掉
         a = s.a;
         b = s.b;
         float* tmp = p; // 先保存了旧的指针
         p = new float(*s.p); // 再申请新的空间,如果申请失败,p仍然指向原有的地址空间
         delete tmp; // 能走到这里,说明申请空间是成功的,这时可以删掉旧的内容了
         return *this;
}
SampleClass& operator= (const SampleClass& s)
{
         if(this == &s) return *this; //可以删掉
         a = s.a;
         b = s.b;
         float* tmp = new float(*s.p); // 先使用临时指针申请空间并填充内容
         delete p; // 若能走到这一步,说明申请空间成功,就可以释放掉本地指针所指向的空间
         p = tmp; // 将本地指针指向临时指针
         return *this;
}

还有一种最屌的实现方法

SampleClass& operator= (const SampleClass& s)
{
         SampleClass tmp(s);
         swap(*this, tmp);
         return *this;
}

SampleClass& operator= (const SampleClass s)
{
         swap(*this, s);
         return *this;
}

注意点

  • ==经常是用于对象内每一个成员变量是否相同的判断,不是地址是否重叠的判断,所以有
    *this == s是地址的问题
    this == &s才是王道

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

1.所有的成员变量,特别是后加入的,相应的拷贝构造函数和赋值运算符要及时更新,把类中涉及的每一个变量都进行复制

class SampleClass
{
private:
         int a;
         double d;
public:
         SampleClass(const SampleClass& s):a(s.a),d(s.d)
        {}
};

2.存在继承关系是,别忘了基类部分的复制,构造函数和赋值函数中都该这样写

class Derived: public SampleClass
{
private:
     int derivedVar;
public:
     Derived(const Derived& d):SampleClass(d), derivedVar(d.derivedVar){}
     Derived& operator=(const Derived& d)
     {
         SampleClass::operator=(d);
         derivedVar = d.derivedVar;
         return *this;
     }
}

条款13. 以对象管理资源

-1)为了防止资源泄露,使用RAII思想(获得资源时进行初始化),造函数中获得资源,并在析构函数中释放资源

-2) 两个常用的RAII类shared_ptr和auto_ptr,前者一般是更佳的选择

尽量不去使用C/C++的原生指针,我们很容易忘记delete的,那么智能指针就是最屌的,直接利用对象资源进行管理,在析构中进行资源的释放 例:这样看不出原始指针,直接将new出来的内存交给了一个“管理者”,然后这个管理者可以是下列两个中的一个,程序员自己可以忘记delete,当管理的生命周期接收自动释放资源,交换系统

auto_ptr ap(new int(10))

特别强势的一个指针,独占资源,不允许其他管理者插手,否则就退出管理,自己不管了,脾气还大的不行。

特性:auto_ptr不支持STL容器,因为容器要求“可复制”,但auto_ptr只能有一个掌握管理权。另外,auto_ptr也不能用于数组,因为内部实现的时候,用的是delete,而不是delete[]。

auto_ptr<int> boss1(new int(10)); // 这个时候boss1对这个资源进行管理
auto_ptr<int> boss2(boss1); // 这个时候boss2要插手进来,boss1很生气,所以把管理权扔给了boss2,自己就不管事了。所以boss2持有这个资源,而boss1不再持有(持有的是null)
1 auto_ptr<int> boss1(new int(10)); 
2 auto_ptr<int> boss2(new int(10));
这种情况下,两者都是持有资源的,因为new执行了两次,虽然内存空间里的初始值是一样的,但地址并不同,所以不算相同的资源。
1.int *p = new int(10);
2.auto_ptr<int> boss1(p);
3.auto_ptr<int> boss2(p);
执行到第三句时,弹出assertion failed,程序崩溃,这是由于p指向同一块资源,当boss2访问时,知道p的资源已经被占用了,则将发生assertion failed。

原始指针和智能指针的混用特别烂,一定要避免

shared_ptr sp(new int(10))

是一个分享与合作的管理者,内部实现是引用计数的,每多引用一次,计数值就会+1,每一次引用的生命周期结束,计数值-1,直到计数为0时,说明内存中没有管理者了,最后管理这个内存的管理者就会将之释放。

条款14. 在资源类中小心copying行为

条款15. 在资源管理类中需要提供对原始资源的访问,也就是原始指针

资源管理类的初衷是防止资源泄露的有力武器,但是许多APis直接指涉到资源,这时就必需绕过资源管理类直接访问原始资源,例如如下:
std::tr1::shared_ptr pInv(createInvestment());
int daysHeld(const Investment* pi);
int days = daysHeld(pInv); //无法通过编译!

daysHeld需要的是Investment*指针,但是传入的却是一个类型为tr1::shared_ptr的对象,那么类型匹配都不成功,肯定是无法通过编译的。
为此,我们需要暴露出原始资源,有两种方式进行解决
  • 显示转换:这个才是王道,明确告诉用户获得内部资源
    提供一个get成员函数,用来执行显式转换。例如tr1::shared_ptr和auto_ptr都提供一个get成员函数来返回智能指针内部的原始指针(副本):
int days = daysHeld(pInv.get());
  • 隐式转换:将资源管理类隐式转换为原始指针
一种特殊的方法:没有返回值,没有形参,重载了返回值的类型
operator T*() const
{
    return ptr;
}

同样,暴露资源还是会出现一些问题,例如资源管理类离开作用域,销毁了资源,而外部有指针指向资源管理类的内部资源,如果还在使用,将会导致未定义的行为

条款16. 成对使用new和delete时需要采取相同的形式

1 int *p = new int(); // 申请一个int资源
2 int *p = new int[3]; // 申请连续三块int资源

那么对应的也有两种释放资源的方式

1 delete p; // delete指针所指向的单一资源
2 delete [] p; // delete指针所指向的多个连续资源块

总结下,如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],一定不要在相应的delete表达式中使用[]。但是事实上,如果一般用错了,编译器也不会进行任何提示,也没有运行时错误,因此这个得程序员自己好好上心。

条款17. 以独立语句将new产生的对象置入智能指针中

以独立语句将new出来的对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

int GetPriority();
void processWidget(shared_ptr pw, int priority);
//如果有这样的调用
processWidget(new Widget(), GetPriority());
//这里会发生隐式转化,但是为了更满足程序员的需求,显示一点,有了explicit,不允许隐式转换,编译出错,因此有了
processWidget(shared_ptr pw(new Widget), GetPriority());
//但是这样会发生内存泄露,因为不同的编译器参数的调用顺序是不确定的,那么可能的调用顺序是:
(1) new Widget
(2) GetPriority() 并将返回值传给priority
(3) pw接管newWidget,而后传给函数形参
如果第二步中发生了异常,new返回的指针就会遗失,并未置入shared_ptr中,创建资源后并未能够转换为资源管理对象,那么两个时间点之间可能发生了异常导致资源泄露。

正确的做法,直接在第一步就成功进行资源的连接,即使GetPriority()发生异常,pw也不虚,我照样能干我自己的事,释放资源

shared_ptr pw(new Widget);
processWidget(pw, GetPriority());

条款18. 让接口容易被正确使用,不易被误用

多考虑用户的使用,引导用户,让用户动最少的脑筋却能最佳地使用接口,进行防御式编程

  • 好的接口容易被正确使用,不容易被误用;
  • 促进正确使用的办法包括接口的一致性,以及与内置类型的行为兼容;
  • 阻止误用的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任;

  • 多多使用shared_ptr来代替原始指针

条款19. 设计class犹如设计type一样,也就是如何设计高效的类

  • 1.对象如何创意和销毁?
    构造函数和析构函数进行,如果想要有堆的分配方式,还需要重载new、new[]、delete和delete[]运算符
  • 2.对象初始化与对象的赋值应该有什么样的差别?
    初始化是构造函数和拷贝构造函数的事(这时候对象还没有产生),但对象的赋值却不同,因为此时等号左边的对象已经存在了。
  • 3.新type如果被pass-by-value意味着什么?
    会调用相应的拷贝构造函数,要注意是否需要一个深拷贝。
  • 4.什么是新type的合法值?
    要对对象的合法值进行限定,通常在构造函数中就对成员变量的范围给出限定,警惕不安全的输入值!
  • 5.判断自己设计的类是基类还是派生类,如果是基类,是否允许这个基类生成对象,以及要将析构函数声明为virtual
  • 6.新设计的类需要什么转换,尤其主义隐式转换,设计要谨慎,如果不想让隐式构造发生,就在前面加上explict关键字。
  • 7.设计出合理的成员函数,还需要那些运算符
  • 8.那些函数应该公开,那些函数应该对内使用呢,private、public和protected的关键作用,又是谁该取用新的type成员呢?
  • 9.你的新类多么一般化,模板可以吗?
  • 10.思考真的需要这个type吗,如果不需要就不用设计了

条款20. 宁以const引用替换值传递

pass-by-reference-to-const的优点:一是可以节省资源复制的时间和空间,减少构造函数的调用次数,二是可以避免切割(值传递将会将多出的派生类内容丢弃),触发多态,在运行时决定调用谁的虚函数。 但是也不是const引用传递就一定好,答案是否定的,这并不适用与内置类型、STL迭代器和函数对象。

条款21. 当必须返回对象时,别妄想着返回其reference

绝不要返回一个指向局部对象的指针或者引用,也不要返回指向分配在堆上的对象,也不要返回全局或者静态对象。返回值还是pass by value比较好

条款22. 将变量声明为private

  1. 能够提供语法一致性,写代码的时候能够马上区分要不要加函数括号,private智能通过成员函数访问的;
  2. 提供更好的访问控制,只读、只写还是可读可写,都容易控制;
  3. 封装,减少与外界接触的机会,方便修改和维护。
  4. 不让类外客户端直接对变量进行操作
为什么proteted不行呢?

如果不存在继承关系,protected的作用与private等同,除了本类之外,其他类或者类外都不能访问,但如果存在继承关系时,protected标识的成员变量,在它的子类中仍可以直接访问,所以封装性就会受到冲击。这时如果更改父类的功能,相应子类涉及到的代码也要修改,这就麻烦了。而如果使用private标识,则根据规则,所有private标识的变量或者函数根本不被继承的,这样就可以保护父类的封装性了,仍可以在子类中通过父类的成员函数进行访问。

条款23. 宁可拿non-member non-friend函数替换member函数 在看看

愈多东西被封装,愈少人可以看到它,而愈少人看到它,我们就有愈大的弹性去改变它,因为我们的改变仅仅影响看到改变的那些人或事物

条款24.

条款25.

条款26. 尽可能延后变量定义式的出现时间,增加程序的清晰度并改善程序效率

  • 使用变量时再去定义,而不是过早地进行定义,这个适用于对变量位置没有要求的语言
  • 增加可读性,看到陌生的变量名,不是翻好几页进行查找,而是就近
  • 节省资源,如下
void example(const A& parm)
{
    A a;
    fun(); // 这个fun()函数不会使用a
    a = parm;
    …
}

那么在上面的代码中,如果fun()抛出异常,对象a的构造函数就白调用了,浪费空间也浪费时间,因此有了:

void example(const A& parm)
{
    fun();
    A a;
    a = parm;
    …
}

但是这里面a调用了A的拷贝构造函数,也调用了赋值运算,也浪费,那么最终版如下:

void example(const A& parm)
{
    fun();
    A a(parm);
    …
}

循环中的常量定义,实在外边好呢,还是里面呢?

分析一下,如果采用放在循环外的方法,代价是1次构造+1次析构+N次赋值;如果采用放在循环内的方法,代价是N次构造+N次析构。所以究竟哪个更好,还是要看构造函数、析构函数VS赋值操作,哪一个比较废。

但如果两方的代价差不多,还是推荐放在循环内的做法,因为这种做法使得a的作用域局限于for循环内,符合作用域能小就小的编程原则。

条款27. 尽量少使用转型动作

  • 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts。
  • 如果转型是必要的,试着将它隐藏于某个函数背后,客户可以随后调用这个函数,而不需要将转型放在他们自己的代码里。
  • 宁可使用C++风格的新式转型,少用C风格转型,因为前者很容易辨识出来,而且也比较有着分门别类的职掌

不清楚原理导致各种无脑bug,还很难发现,要使用就用下面几种
- const_cast(expression):取出常量属性

1.只是针对指针或者引用,包括this指针,不能用于普通变量

2.既可以将const->非const,也可以将非const->const,这个操作并不改变被转换对象的常属性

int a= 3;
const int* ca = &a;
int *pb = const_cast<int*> (ca);

其中ca仍是指向常量的指针,对它的操作,编译器会报错

  • static_cast(expression)
    这是最常见的c++转换了,常见的int->double或者其他都是,就是简单的基本类项转换

例:

1 void SpecialWindow::OnResize()
2 {
3     static_cast(*this).OnResize();
45 }

static_cast生成的是一个临时的基类的对象,这个对象并不是真正组成派生类的那个基类,而是它的一个副本(这个副本的成员变量值与派生类对象中基类的成分是一样的,但地址不同),调用OnResize()变更的结果是这个副本的成员变量变了,但派生类中包含的基类的成员变量却是没有任何变化的。好了,这就是bug了,之后测试中会抓狂。

那么总结就是转型生成一个copy,一份副本,有时就不是你想要的,修正其实很简单,如下

1 void SpecialWindow::OnResize()
2 {
3     Window::OnResize(); // OK了,就这么简单
45 }
  • dynamic_cast(expression)

dynamic_cast是当想把基类指针转成派生类指针时用,这种转换可以保证安全性,当把两个不相干的类之间用dynamic_cast时,转换的结果将是空指针,同时若基类指针指向的对象只是基类本身时,对基类指针进行dynamic_cast向下转型到派生类,会得到空指针,防止进一步的操作。

其实这个有时用起来也比较废,这个在转型的过程中要判断是否相等,那么就有这个转型会对类名称进行strcmp,以判断向下转型是否合理,如果继承深度比较大,那么每一次的dynamic_cast将会进行多次strcmp,这将严重影响程序的执行效率。这还不如利用多态的虚函数特性,或者工厂模式都可以实现基类向子类的转换,指向子类即可。

  • reinterpret_cast(expression)
    一个指针转成整数或者把整数看成指针,对平台依赖性很强,不建议使用

条款28. 避免返回handlers指向对象内部成分

避免返回handles(包括reference、指针、迭代器)指向对象内部,遵守这个条款可增加封装性,并将发生danglinghandles的可能性降至最低。如果有必要必须要返回handles,在编写代码时就一定要注意对象和传出handle的生命周期。

条款29. 为“异常安全”努力

异常安全函数是指即使发生异常也不会泄漏资源或者允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。

  • 基本型:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或者数据结构会因此被破坏
  • 强烈型:在基本承诺的基础上,保证成功就是完全成功,失败也能回到之前的状态,不存在介于成功或失败之间的状态。也就是果断,要么做要么不做

    方法:copy and swap,也就是说再修改对象之前,先创建一个副本,然后对副本进行操作,如果操作发生异常,异常发生在副本上,不会影响本尊,如果没有异常在进行swap。

    这种方法代价是时间和空间,必须为每一个即将被改动的对象创造副本

  • 不抛出异常:承诺这个代码在任何情况下都不会抛出异常,但这只适用于简单的语句。

强烈保证往往可以通过copy-and-swap实现出来,但“强烈保证”并非对所有函数都可以实现或具备现实意义。

1 void someFunc()
2 {
3     f1();
4     f2();
5 }

上例中,f1可能没异常,f2抛出异常,数据回到f2之前,但是程序猿想要回到f1之前,这样只能将f1和f2相结合才行,都nice着,就可进行copy and swap。但是代价需要改变函数的结构,代价大,破坏函数的模块性,如果不想这么做那就得放弃这个方法,将异常安全保障回退到基本保障。

异常安全保证通常最高只等于其所调用之各个函数的异常安全保证中的最弱者

条款30. inline的里里外外

  • inline是内敛,建议编译器将函数的每一个调用都用inline所声明的函数本体进行替换,以空间换时间,但是这种方式,会造成代码膨胀,优点是节省了函数调用的成本,直接进行替换。

    一般的函数调用需要将之前的参数以堆栈的形式保存起来,调用结束后又要从堆栈中恢复那些参数。

  • 但注意inline只是对编译器的一个建议,编译器并不表示一定会采纳

    比如当一个函数内部包含对自身的递归调用时,inline就会被编译器所忽略。对于虚函数的inline,编译器也会将之忽略掉,因为内联(代码展开)发生在编译期,而虚函数的行为是在运行期决定的,所以编译器忽略掉对虚函数的inline。对于函数指针,当一个函数指针指向一个inline函数的时候,通过函数指针的调用也有可能不会被编译器处理成内联。

  • 直接写class内部的成员函数都是inline,例如
class Person
{
private:
    int age;
public:
    int getAge() const   //内敛
    {
        return age;
    }
    void setAge(const int o_age); //类外定义,不是内敛
};
void Person::setAge(const int o_age)
{
    age = o_age;
}
  • 不对构造函数和析构函数设置为inline

    因为空的构造函数内被编译写上对所有成员函数的初始化,如果设为inlien,那不是在每个成员函数或者变量出都会展开,造成大量的赋值

  • 慎用inline,一旦修改,就要重新编译,就大多数inline限制在小型、被频繁调度的函数身上。建议一开始不设置任何函数为inline,测试之后,确实需要inline,再来。

条款31. 将文件间的编译依存关系降至最低

  • 在头文件中用class声明外来类,用指针或引用代替变量的声明;在cpp文件中包含外来类的头文件

具体说明,还是别人家的总结的好
第二部分
第三部分

条款32. 确定你的public继承关系为is-a关系:子类可以扩展父类的功能,但不能改变父类原有的功能

抽离出父类与子类之间所有的共性,放入父类中,然后类中特有的部分在子类中实现

适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。

条款33. 避免遮掩继承而来的名称:编译器开始解释

遮掩问题分为变量遮掩与函数遮掩,其实本质都是名字的查找方式,当编译器要查找一个名字时,它一旦找到一个相符的名字,就不会放下找了,因此遮掩问题是优先查找哪个名字的问题。

其实遮掩就是个作用域问题,在作用域内先查找那个名称或变量,有以下的例子:

//例1:普通变量遮掩
int i = 3;

int main()
{
    int i = 4;
    cout << i << endl; // 输出4,优先查找局部作用域
}

那么在继承中,也是一样的,都是在各自的作用域内进行查找

//例4:重载函数的遮掩
class Base
{
public:
    void CommonFunction(){cout << "Base::CommonFunction()" << endl;}
    void virtual VirtualFunction(){cout << "Base::VirturalFunction()" << endl;}
    void virtual VirtualFunction(int x){cout << "Base::VirtualFunction() With Parms" << endl;}
    void virtual PureVirtualFunction() = 0;
};

class Derived: public Base
{
public:
    void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
    void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
    void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};

int main()
{
    Derived d;
    d.VirtualFunction(3); // 这里将会出现编译时错误,因为派生类出现了遮掩问题,在派生类的作用域中就搜索不到相应的函数,所以会报错
    return 0;
}
  • 派生类的名称会遮掩基类中的名称,那么在派生类想要访问基类的方法,则是不可能的,尤其是在public继承下
  • 为了将遮掩的父类名称再见天日,那么可以利用using声明式,告诉编译器using的父类名称将归为派生的优先查找范围内。
//例5:采用using声明,使查找范围扩大至父类指定的函数:
class Base
{
public:
    void CommonFunction(){cout << "Base::CommonFunction()" << endl;}
    void virtual VirtualFunction(){cout << "Base::VirturalFunction()" << endl;}
    void virtual VirtualFunction(int x){cout << "Base::VirtualFunction() With Parms" << endl;}
    void virtual PureVirtualFunction() = 0;
};

class Derived: public Base
{
public:
    using Base::VirtualFunction; // 第一级查找也要包括Base::VirtualFunction
    void CommonFunction(){cout << "Derived::CommonFunction()" << endl;}
    void virtual VirtualFunction(){cout << "Derived::VirturalFunction()" << endl;}
    void virtual PureVirtualFunction(){cout << "Derived::PureVirtualFunction()" << endl;}
};

int main()
{
    Derived d;
    d.VirtualFunction(3); // 这样没问题了,编译器会把父类作用域里面的函数名VirtualFunciton也纳入第一批查找范围,这样就能发现其实是父类的函数与main中的调用匹配得更好(因为有一个形参),这样会输出Base::VirtualFunction() With Parms
    return 0;
}

条款34. 区分接口继承和实现继承

class BaseClass
{
public:
    void virtual PureVirtualFunction() = 0; // 纯虚函数
    void virtual ImpureVirtualFunction(); // 虚函数
    void CommonFunciton(); // 普通函数
};
  • 纯虚函数只指定进行继承接口

    纯虚函数有一个“等于0”的声明,具体实现一般放在派生中(但基类也可以有具体实现),所在的类(称之为虚基类)是不能定义对象的,派生类中仍然也可以不实现这个纯虚函数,交由派生类的派生类实现,总之直到有一个派生类将之实现,才可以由这个派生类定义出它的对象。

  • 虚函数制定继承接口,也提供了一份默认实现
    虚函数则必须有实现,否则会报链接错误。虚函数可以在基类和多个派生类中提供不同的版本,利用多态性质,在程序运行时动态决定执行哪一个版本的虚函数(机制是编译器生成的虚表)。virtual关键字在基类中必须显式指明,在派生类中不必指明,即使不写,也会被编译器认可为virtual函数,virtual函数存在的类可以定义实例对象。
  • 普通函数既继承接口也强制继承实现

    普通函数则是将接口与实现都继承下来了,如果在派生类中重定义普通函数,将会出现名称的遮盖。 我们极不推荐在派生类中覆盖基类的普通函数的,如果真的要这样做,请一定要考虑是否该把基类的这个函数声明为虚函数或者纯虚函数。

  • 多用纯虚函数来代替虚函数,因为虚函数将会提供一份默认的实现,如果派生类想要的行为与基类不一样,但是又忘了进行覆盖虚函数,那就会问题。而纯虚函数就会杜绝这种情况,派生必须去实现它,否则无法定义。 同时纯虚函数也是有默认实现的,可以代替虚函数。

条款35. 考虑virtual函数以外的其他选择

virtual函数的本质是由虚指针和虚表来控制,虚指针指向虚表中的某个函数入口地址,就实现了多态。因此,我们也可以仿照一个虚指针指向函数的手法,来做一个函数指针

条款36. 不要重新定义继承而来的非虚函数

  • 适用于BaseClass的行为一定适用于DerivedClass,因为每一个DerivedClass对象都是一个BaseClass对象;
  • 如果基类里面有非虚函数,那么派生类一定既继承了接口,又继承了实现
  • 子类里面的同名函数会覆盖父类的同名函数,参考条款33,搜索作用域

如果派生类重新定义了一个非虚函数,即是每个类都有自己的想法,那用继承干嘛,自己子类都可以分离开来。

条款37. 绝不重新定义继承而来的缺省参数值

缺省参数值是静态绑定,virtual是动态绑定

enum MyColor
{
    RED,
    GREEN,
    BLUE,
};
class Shape
{
public:
    void virtual Draw(MyColor color = RED) const = 0;
};
class Rectangle: public Shape
{
public:
    void Draw(MyColor color = GREEN) const
    {
        cout << "default color = " << color << endl;
    }
};
class Triangle : public Shape
{
public:
    void Draw(MyColor color = BLUE) const
    {
        cout << "default color = " << color << endl;
    }
};
int main()
{
    Shape *sr = new Rectangle();
    Shape *st = new Triangle();
    cout << "sr->Draw() = "; // ?
    sr->Draw();
    cout << "st->Draw() = "; // ?
    st->Draw();

    delete sr;
    delete st;
}

在这个例子中有虚函数,如果父类中有虚函数,父类中将会生成一个虚指针和一个虚表,程序运行过程中,将会根据 虚指针的指向,来决定调用哪个虚函数,这称之为动态绑定,而与之相对应的就是静态绑定,是在编译器进行的。实现动态绑定的代价是非常大的,因此函数参数这部分都是静态绑定的,编译时就决定下来了。那么如果对继承而来的虚函数使用不同的缺省值,造成代码阅读者的困惑,因为缺省的参数在继承下来是不改变的。

条款38. 通过复合塑模出has-a或者is-implemented-in-terms-of(依据某物来实现)

复合就是在一个类中采用其他类的对象作为自身的成员变量,是一种拥有的关系,has-a。

在程序中,往往是需要依据某一个模块来实现另一个模块,一般来说想到了继承的方法,但是继承一般有一种法则就是子类将会替代父类的特性,不能更改,但是我们这个时候既想利用一个模块现有的特性,也不想违反可替代原则,那么就可以利用复合的关系。==最明显的就是STL中stack和queue都是利用deque来实现的,这就是一种依赖,一种复合==

#include 
#include 
using namespace std;
template <class T>
class MySet
{
private:
    list MyList;
public:
    int Size() const
    {
        return MyList.size();
    }
    bool IsContained(T Element) const
    {
        return (find(MyList.begin(), MyList.end(), T) != MyList.end());
    }
    bool Insert(T Element)
    {
        if (!IsContained(T))
        {
            MyList.push_back(Element);
            return true;
        }
        else
        {
            return false;
        }
    }
    bool Remove(T Element)
    {
        list::iterator Iter = find(MyList.begin(), MyList.end(), T);
        if (Iter != MyList.end())
        {
            MyList.erase(Iter);
            return true;
        }
        else
        {
            return false;
        }
    }
};

条款39. 慎用private继承

public和private的不同:

  • public继承在子类中保持父类的访问权限,即父类中是public的成员函数或成员变量,在子类中仍是public,对private或者protected的成员函数或成员变量亦是如此;但private继承则不是这样了,它破坏了父类中的访问权限标定,将之都转成private,这对子类本身并无影响(照常访问),但却影响了子类的子类,子类的子类将无法访问这些声明/定义在爷爷辈类的成员变量或成员函数
  • Liskov法则不再适用,也就是说“一切父类出现的地方都可以被子类所替代”的法则在private这里不成立,就是父类和子类都不一样了
class TypeDefine
{};
class SimpleClass
{
    int a;
    TypeDefine obj;
};
class SimpleDerivedClass : private TypeDefine
{
    int a;
};
int main()
{
    cout << sizeof(TypeDefine) << endl; // ?
    cout << sizeof(SimpleClass) << endl; // ?
    cout << sizeof(SimpleDerivedClass) << endl; // ?
}

第一个是空类,对于空类,编译器生成了四个默认的函数:默认构造函数、默认拷贝构造、默认析构函数和默认赋值运算符。但是这四个都不占空间,而类的每一个对象都需要一个独一无二的内存地址,所以编译器会在空类中插入一个1字节变量,正是这个1字节变量,才能区分空类的不同对象。对于非空类,编译器可以利用成员变量来进行内存地址的区分。

第二个输出是8,因为会有内存地址对齐的规则

第三个结果是4,因为有private继承,原本是要为空白类对象插入1字节的,但是因为子类中有了对象,这样理论上就可以凭借这个对象来进行区分识别了,这时编译器就不再插入字节了。

条款40. 明智而审慎地使用多重继承

尽量选择单继承进行以免在项目中带来可读性的问题,==多重继承中容易碰到的问题就是名字冲突==,如下面的例子所示。 但是如果想要去访问重名的函数,那么可以指定作用域d.Base1::fun()进行,因此可以知道编译器会优先去查找最合适的重载函数,再去考虑它的可访问性。

class Base1
{
public:
    void fun(){}
};

class Base2
{
private:
    void fun(){}
};

class Derived : public Base1, public Base2
{};

int main()
{
    Derived d;
    d.fun(); // error C2385: 对“fun”的访问不明确
    return 0;
}

为了解决多重继承中出现多个祖类成员的问题,那么有了虚拟继承,可以避免这个问题。假如说是:有一个父类名为A,类B和类C都继承于A,类D又同时继承了B和C(多重继承),那么如果不做任何处理,C++的类继承图里会包含两份A。采用了virtual继承后,D中就只有一份A了,如下所示:

class B: virtual public A{…}
class C: virtual pulibc A{…}

但是,为了保证不会出现两份父类,只要是public继承理论上都应该有virutal关键字,但virutal也是有代价的,访问virtual base class的成员变量要比访问non-virutal base class的成员变量速度要慢

因此有:
- 非必要不使用virtual classes继承,普通情况请使用non-virtual classes继承
- 如果必须使用virtual base classes,尽可能避免在其中放置数据。

  1. 多重继承比单一继承更复杂。它可能导致新的歧义性,以及对virtual继承的需要。
  2. virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况。
  3. 多重继承的确有正当用途。其中一个情节涉及”public继承某个Interface class”和”private继承某个协助实现的class”的两两组合。

条款41. 了解隐式接口和编译期多态

  1. class和template都支持接口与多态;
  2. 对classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期;
  3. 对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。
  4. 运行时多态是决定“哪一个virtual函数应该被绑定”,而编译期多态决定“哪一个重载函数应该被调用”

你可能感兴趣的:(C++)