《Effective c++》

文章目录

第一章 习惯c++

1.视C++为一个语言联邦

  • C++的组成部分(4个次语言)
    C面向对象模板STL

  • 次语言间切换时注意事项
    1.对于内置类型(C-like)而言,pass-by-value通常比pass-by-reference高效;
    2.但是对于对象而言,由于构造函数和析构函数的存在,pass-by-reference-to-const往往更好
    3.但是对于STL的迭代器和函数对象而言,旧式的C pass-by-value守则再次适用

2.尽量以const、enum、inline替换#define

  • 对于常量,用const替换宏
    #define ASPECT_RATIO 1.653
    
    在预处理时符号ASPECT_RATIO会被替换为数字,于是这个符号从未进入符号表,从而导致难以调试;
    => 解决方法是将其替换为const常量:
    const double AspectRatio=1.653;
    
    注:注意区分const指针和指向常量的指针……(见博客:c++语言基础)
  • 部分情况下,用enum替代整数宏
    the enum hack:enum可被视为ints使用……它也是模板元编程的基础技术……
  • 对于函数,用inline替换形似函数的宏
    使用宏作为函数存在的问题:
    // 以a和b的较大值调用f
    #define CALL_WITH_MAX(a,b) 	f((a)>(b) ? (a):(b))
    
    int a=5; int b=0;
    CALL_WITH_MAX(++a,b);	// a被累加两次
    CALL_WITH_MAX(++a,b+10); // a被累加一次
    
    inline既能获得宏带来的效率(运行期不必调用函数),又能保证上面例子中a不会被错误累加……

3.尽可能使用const

  • const修饰普通常量
    const修饰普通常量:const int a和int const a等价;
    const与指针:如果const在星号左边,则被指物是常量;若const在星号右边,则指针本身是常量(作物油脂 => 左物右指)。
  • const修饰参数
    保证传入的参数不会被修改
  • const修饰返回结果
    1.const 修饰内置类型的返回值:修饰与不修饰返回值作用一样;
    2.const 修饰自定义类型的作为返回值:此时返回的值不能作为左值使用,既不能被赋值,也不能被修改;
    3.const 修饰返回的指针或者引用:返回的指针不能被修改
  • const修饰成员函数
    保证不能修改对象的成员值

4.确定对象被使用前已先被初始化

  • 内置类型的初始化
    内置类型(int、float……)必须手动初始化,C++不保证初始化内置类型

  • 对象的成员变量的初始化
    对象的成员变量的初始化动作发生在进入构造构造函数本体之前,比如:

    ABEntry::ABEntry(const std::string& name,const std::string& address, const std::list<PhoneNumber> phones){
    	this->theName=name;
    	this->theAddress=address;
    	this->thePhones=phones;
    	this->numTimesConsulted=0;
    }
    

    就是说成员theName、theAddress、thePhones的初始化在调用构造函数ABEntry之前,这里花括号内的语句只能算赋值而不是初始化!初始化发生的时间在这些成员的default构造函数被自动调用之时(比进入ABEntry的时间更早)

  • 建议使用初始值列表
    上面例子中先初始化再赋值造成了资源的浪费(成员在其default构造函数中先初始化了一遍,之后再重新赋值),应该使用初始值列表:

    ABEntry::ABEntry(const std::string& name,const std::string& address, const std::list<PhoneNumber> phones)
    :theName(name),
    theAddress(address),
    thePhones(phones),
    numTimesConsulted(0){
    // ......
    }
    

    这种情况下,传入的实参直接作为成员的构造函数的实参,从而减少了赋值动作……

  • 成员初始化次序
    1.base classes更早于其derived classes被初始化
    2.class的成员变量总是以其声明次序被初始化
    3.初始值列表应该总是与成员声明顺序一致,否则可能参数传递情况与预期不符

  • non-local static对象的初始化
    C++对定义于不同编译单元内的non-local static对象的初始化次序并无明确定义 => 应该以local static对象替换non-local static对象(方法:单例模式)

第二章 构造/析构/赋值运算

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

  • 编译器自动创建的函数
    1.default构造函数:……
    2.copy构造函数:……
    3.copy assignment:……
    4.析构函数:non-virtual的!……
    注意只有需要(即被调用)的时候才创建,并不是总是自动创建这些函数;所有这些函数都是public且inline的;但是若用户自己声明了,则编译器不会创建

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

  • 场景分析
    比如不希望class支持copy构造函数、copy assignment,然而编译器总是会在调用的地方自动创建public的copy构造函数、copy assignment,这时怎么办呢?
  • 方法1
    自己声明copy构造函数、copy assignment函数,让他们是private且不实现,这个时候编译器就不会自动创建这些函数(因为已经自己声明),别人也不能成功调用(因为没有实现)
  • 方法2
    (本质上还是方法1)
    将父类Base的copy构造函数、copy assignment函数声明为private且不实现,然后继承父类Base,从而子类也无法正常调用……因为编译器自动生成默认函数时,会先尝试调用父类的对应函数,但是由于private会被拒绝……

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

  • 父类非virtual析构函数的问题
    当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未定义 => 很有可能是对象的子类成分没有被销毁,而只是父类成分被销毁;=> 为什么?
    解决方法:给base class一个vurtual析构函数,此后删除derived class对象就能析构所有成分……
  • 必要时才使用virtual析构函数
    如果class不含virtual函数,通常表示它并不意图被用作一个base class,此时令其析构函数为virtual往往是个馊主意,因为virtual机制会增加对象的大小;
    => 只有当class内至少含有一个virtual函数,才为他声明virtual析构函数
  • 小结
    1.给base class一个virtual析构函数,这个规则只适用于多态的基类;
    2.多态base class应该声明一个vurtual析构函数;
    3.如果class 的目目的不是作为多态的base class,就不应该声明virtual析构函数;

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

  • 概述
    C++ 并不禁止析构函数吐出异常,但是不鼓励这样做; 析构函数吐出异常可能因为程序过早结束而导致一些不好的结果 => 比如应该销毁的资源尚未销毁完毕,造成内存泄漏……
  • 应对方法
    1.要么直接abort结束程序;
    2.要么捕获异常,进行适当处理……

9.绝不在构造和析构过程中调用virtual函数

  • 概述
    假设父类Parent中有虚函数vf,且Parent的构造函数调用了vf;子类Son继承Parent并实现了vf。当创建子类对象时,Son的构造函数会自动先调用父类的构造函数,而这个时候父类构造函数中执行的vf是父类的而不是子类的vf => 即base class构造期间virtual函数不会下降到derived class阶层!!!
    class Parent{
    public:
        Parent(){
            vf();
        }
        virtual void vf(){
            cout<<"this is virtual func of Parent!"<<endl;
        }
    };
    
    
    class Son:public Parent{
    public:
        Son(){		// 不要误以为创建Son对象时Parent构造函数会自动调用Son对virtual函数的具体实现
        }
        void vf(){
            cout<<"this is virtual func of Son"<<endl;
        }
    };
    
    // 输出:this is virtual func of Parent!
    
    不要误以为上面创建Son对象时Parent构造函数会自动调用Son对virtual函数的具体实现,这样写并没有想象中的多态!

10.令operator=返回一个reference to *this

  • 注意
    1.这不是强制协议,但是很多标准库的这样进行;
    2.返回reference to *this的的目的是实现连锁赋值(即x=y=z=…);
    3.这个规则不仅针对=,+=、-=、*=同样适用…

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

  • 自我赋值的潜在风险

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

    若存在自我赋值,则*this和rhs是同一个对象,执行赋值操作最终会销毁了pb指针指向的BitMap对象 => 所以operator=需要特别考虑自我赋值的情况

  • 处理方法
    1.比较原对象和目标对象的地址,若是自我赋值则直接返回…;
    2.精心周到的语句顺序(p55);
    3.copy-and-swap(p56);

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

  • copy constructor 和copy assignment operator
    良好的面向对象系统通常只保留两个函数负责对象拷贝:
    copy consstrutor:即复制构造函数…
    copy assignment operator:即运算符=重载,编译器通常会默认实现该类/对象的运算符重载,复制时它会拷贝所有成员;但是,若用户自定义了=,则需要小心复制时不要漏掉每一个成员
  • 自定义赋值操作时勿忘
    1.对象内的所有成员变量;
    2.对象的所有base class成分,这部分通常通过调用父类的copy constructor完成拷贝

第三章 资源管理

13.以对象管理资源(重要)

  • 资源的形式
    内存(最常见的资源)、文件描述符互斥锁数据连接网络socket……

  • 手动释放资源的不足

    void f(){
    	Investment* pInv=createInvestment();
    	// ......
    	// 中间可能某些原因过早退出,从而没有释放动态分配的内存 !!!
    	delete pInv;	
    }
    

    中间可能某些原因过早退出,从而没有释放动态分配的内存

  • 核心思想
    把资源放进对象,从而依赖C++的析构函数机制确保资源被分配;
    C++中针对这一想法,提供了auto_ptrshared_ptr

  • auto_ptr
    auto_ptr是一个类指针对象(pointer-like object),其析构函数自动对所指的对象调用delete,从而上面的函数可修改如下:

    void f(){
    	std::auto_ptr<Investment> pInv(createInvestment());
    	// ....
    	// 不必手动释放内存了...
    }
    

    注:上面pInv是一个局部变量,所以离开函数f()时,其生命周期结束,智能指针自动释放pInv指向的内存…
    auto_ptr怪异的复制行为若将auto_ptr对象pInv1赋值(=或者copy constructor)给另一个auto_ptr2,那么auto_ptr1将变为null => STL为了保持正常的复制行为,所以不能使用auto_ptr

  • shared_ptr
    RCSP:即引用计数型智慧指针,它能持续追踪共有多少对象指向某笔资源,并在无人指向它时删除资源 => 这就类似引用计数的垃圾回收方式,缺点在于无法解决循环引用问题……shared_ptr就是RCSP的一种实现
    所以,上面的函数可改为:

    void f(){
    	std::tr1::shared_ptr<Investment> pInv(createInvestment());
    	// ....
    	// 不必手动释放内存了...
    }
    

    shared_ptr的复制行为不像auto_ptr那样怪异,STL可以使用

  • auto_ptr和shared_ptr不支持array
    这两个智能指针在析构函数内都是做delete而不是delete[],所以在动态分配的数组上不应该引用这两个指针!!!

14.在资源管理类中小心copying行为

  • 当RAII对象被复制时,可能的拷贝策略
    根据RAII类的定义、应用情况,复制时可能选择以下策略之一:
    禁止复制对管理的资源进行"引用计数"深拷贝管理的资源转移资源的拥有权
    前两种是最常用的做法
  • 禁止复制
    有的RAII,对其进行复制是不合理的(比如p66的Lock类),所以应该禁止复制,具体的做法就是将copying操作声明为private
  • 对底层资源祭出引用计数
    tr1:shared_ptr就是此类例子(见条款13)……
  • 复制底部资源
    即复制资源管理器的同时,复制其中的资源(深拷贝)
  • 转移底部资源的拥有权
    auto_ptr就是此类例子(见条款13)……

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

  • 概述
    对于shared_ptr< Investment> pInv(createInvestment());,pInv是对原始指针的包装(资源管理),当某些函数比如int daysHeld(const Investment* pi);需要原始指针时,我们就需将pInv通过隐式/显示转换成原始指针

  • 显示转换
    tr1::shared_ptrauto_ptr都提供了一个get()函数执行显示转换,比如:

    int days=daysHeld(pInv.get());
    
  • 隐式转换
    几乎所有智能指针都重载了指针取值操作符(-> 和 *),所以可以直接通过这两个进行隐式转换,即直接pInv->doSomething(....)而不用pInv.get()->doSomething(...)

  • 小结
    对原始资源的访问可能经由显示转换或者隐式转换。一般显示转换更安全,但隐式转换对客户比较方便……

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

  • 总结
    1.如果在new表达式中使用[],必须在相应的delete表达式中也使用[];
    2.如果在new表达式中没有使用[],一定不要在delete表达式中使用[];

17.以独立的语句将new创建的对象置入智能指针

  • 问题举例
    对于语句:

    processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());
    

    在核算processWidget(…)实参的时候,以下三个语句的执行顺序不确定(C++是这样,Java不是):

    调用priority()、执行new Widget、调用tr1::shared_ptr构造函数
    

    如果遇到这样的顺序

    1.执行new Widget
    2.调用priority
    3.调用tr1::shared_ptr的构造函数
    

    这种情况下,一旦priority发生异常,步骤3就没有被执行,从而无法释放Widget,导致内存泄漏……

  • 解决方法
    将new新建的对象放入智能指针作为独立的语句,即:

    std::tr1::shared_ptr<Widget> pw(new Widget);
    processWidget(pw,priority());
    

第四章 设计与声明

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

  • 促进正确使用的方法
    1.接口的一致性(STL所有容器都有一致的size()方法,而Java有的是size(),有的是length()不够一致);
    2.与内置类型的行为兼容:自定义的types的行为应该与内置types的行为一致…
  • 阻止误用
    1.建立新类型
    2.限制类型上的操作
    3.束缚对象值
    4.消除客户的资源管理责任(条款13)
    ……

19.设计class犹如设计type

谨慎地设计class,主要从以下方面考虑

  • 新type的对象应该如何被创建和销毁
    涉及到构造、析构函数的设计……
  • 对象的初始化和对象的赋值该有什么样的差别
    决定了copy构造函数、copy operator的具体行为及其差异
  • 新type的对象如果被passed by value,意味着什么?
    记住:copy构造函数可用来定义一个type的passed by value该如何实现
  • 什么是新type的合法值
    这个决定了成员变量的约束条件、在成员函数中应该进行的错误检查……
  • 新type需要什么样的转换
    隐式转换、显示转换、explicit…
  • 谁该取用新type的成员
    决定哪个成员为public、protected、private
    ……
    ……
    ……
    还有很多内容,详见p85

20.宁以pass-by-reference-to-const替换pass-by-value(重要)

  • pass-by-value的缺点1
    值传递对象参数时,需要先调用对象的copy构造函数复制对象,造成很高的成本……(注:《C++对象模型》p63讲述了pass-by-val的复制过程)
  • pass-by-value的缺点2 => 对象切割问题
    当子类对象作为实参传递给父类形参时,复制参数时调用的将会是父类的copy构造函数,从而最终传递进去的仅仅是子类对象中的父类部分!!! 这就是对象切割
  • 特例
    这条规则不适用于内置类型、STL迭代器、函数对象

21.必须返回对象时,别妄想返回其reference

  • don’t do
    1.不要返回pointer或者reference指向一个local stack对象;
    2.不要返回pointer 或者reference指向一个heap-allocated对象;
    注:2只是看起来似乎没有问题,它的问题举例参考p92
  • 那么如何解决呢
    如果函数必须返回新对象,那么就让他直接返回新对象;
    注:返回对象时,仍然需要对函数中的新对象进行拷贝,所以即使栈上分配的对象,仍然能够传递出去,具体过程可参考《C++对象模型》p64

22.将成员变量声明为private

  • 概述
    不过多解释, 问题的关键就在于封装性……
  • 记住
    1.切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证、并提供class作者以充分的实现弹性…
    2.protected并不比public更具有封装性

23.宁以non-member、non-friend替换member函数(未理解)

……尚不怎么能够理解,待二次学习……

24.若所有参数皆需类型转换,请为此采用non-member函数

  • 举例

    class Rational{
    	.....
    	// 运算符重在,定义两个Rational对象的乘法
    	const Rational operator*(const Rational& rhs)const;
    }
    
    // 若调用
    Rational oneHalf(1,2);
    Rational result1=oneHalf*2;        // => 正确
    Rational result2=2*oneHalf;       // => 错误
    

    为什么第一个调用正确,第二个调用错误?
    本质上,上述两个调用过程是这样的:

    result1=oneHalf.operator*(2);
    result2=2.operator*(oneHalf);
    

    重载的运算符其实有两个参数,一个是传入的rhs,另一个是隐含的this
    第一个调用直接将2隐式转换为了Rational对象;
    但是第二个调用时2在前面,它并没有相应的class,也就没有operator*成员函数,从而无法进行类型转换……

  • 解决方法
    将operator*()这个成员函数改为non-member函数:

    // 不属于任何类/对象
    const Rational operator*(const Rational& lhs,const Rational& rhs){
    	// ................
    }
    

    于是就可以调用result2=2.operator*(oneHalf);

  • 结论
    如果你需要为某个函数的所有参数(包括被this指针所指的哪个隐喻参数)进行类型转换,那么这个函数必须是各non-member

  • 补充
    1.menber函数的反面是non-member函数,而不是friend函数;
    2.不要错误的假设:如果一个与某class相关的函数不应该成为一个member函数,就该是个friend函数
    3.无论何时,都该尽量避免friend函数

25.考虑写出一个不抛异常的swap函数(待二次学习)

  • 备注
    这里主要是讲std::swap进行交换对象时,针对pimpl手法的对象交换效率很低,可以如何改进……由于目前尚未遇到过,感觉还有很多不是很懂之处,待重新学习……

第五章 实现

26.尽可能延后边变量定义式的出现时间

  • 原因
    对于对象而言,过早地定义变量,但是如果最终没有使用该变量程序就离开了其作用域,那么就白白耗费了该变量构造析构的成本
  • 常见做法
    1.直到真正需要变量时才定义它;
    2.尽量直接在定义时指定初值(效率高于先定义再赋值);
    3.循环内定义视情况可提到循环外定义(要计算是否可能有效率提升);

27.尽量少做转型动作(重要)

  • C中的两种转型动作

    	(T) expression			// 将expression转型为T
    	T(expression)			// 将expression转型为T
    

    两种方式没有区别

  • C++中的四种转型动作
    const_castdynamic_castreinterpret_caststatic_cast

  • const_cast
    const_cast转换符是用来移除变量的const或volatile限定符(即可将const变量转换为non-const变量,volatile同理)

  • dynamic_cast
    主要用于向下转型,即将一个基类对象指针(或引用)转换到继承类指针,它是唯一无法用C语言的类型转换替代的动作

  • reinterpret_cast
    就是把内存里的值重新解释,它可以暴力完成两个完全无关类型的指针之间或指针和数之间的互转,比如用char类型指针指向double值。它对原始对象的位模式提供较低层次上的重新解释(即reinterpret),完全复制二进制比特位到目标对象,转换后的值与原始对象无关但比特位一致,前后无精度损失……
    => 但它本质是一个编译期指令,实际动作可能取决于编译器,虽然功能最强但风险最大,且失去了移植性

  • static_cast
    强迫进行类型转换,没有运行时类型检查来保证转换的安全性

  • C++单一对象可能拥有多个地址

    class Base{....}
    class Derived:public Base{...}
    Derived d;
    Base* pd=&d;
    

    上面执行后,pd和&d的地址可能并不相同,这是因为C++中可能会有个offset在运行期被施行于Derived*指针身上,用以取得正确的Base*指针(详见《C++对象模型》)。这种情况几乎在多重继承上总是会发生……

  • dynamic_cast的替代策略
    之所以需要使用dynamic_cast,通常是因为手里只有一个Base的指针/引用,但是想执行Derived身上的函数。由于dynamic_cast效率很低,更好的方式是以下两种替换策略:
    1.使用容器并在其中存储直接指向Derived class对象的指针(可能看文字不好理解,详见p121);
    2.直接通过virtual函数,即多态实现……

  • 小结
    1.尽量避免类型转换,尤其是dynamic_cast,因为其效率很低;
    2.如果转型是必要的,试着将它隐藏与某个函数背后,客户随后可以调用该函数,而不需将转型放进他们自己的代码内;
    3.宁可使用C++转型,不要使用C转型

28.避免返回handles指向对象内部成分

  • 原因
    返回引用、指针、迭代器这三类代表对象内部数据的handle,可能导致降低对象封装性的风险,因为可以通过这些handle修改对象内部的数据……
  • 特例
    当然有的情况下必须返回handle,比如string、vector重载了operator[],返回的就是指向容器内部数据的引用 => 但这仅仅是特例

29.为"异常安全"而努力是值得的

  • 异常安全函数
    异常安全函数定义:即使发生异常也不会资源泄漏(比如没有释放内存)或者数据结构败坏(比如数据被改到了非预期的值) => 实现方式:比如通过资源管理器管理资源(条款13)、copy-and-swap(条款11或者131);
    分类:这样的异常安全函数又可根据他们对异常安全的保证分为三类 => 基本型、强烈型、不抛异常型
  • 基本型
    如果异常被抛出,程序内的任何事物仍然保持在有效状态(但不一定是调用之前的原始状态)
  • 强烈型
    如果异常抛出,程序状态不改变 => 如果函数成功,就是完全成功;如果函数失败,程序会回复到调用之前的状态
  • 不抛异常型
    承诺绝不抛出异常,总是能够完成成功的功能。作用与内置类型身上的所有操作都提供nothrow保证。
  • 小结
    1.强烈保证往往能够以copy-and-swap实现,但是效率较低;
    2.函数提供的"异常安全保证"通常最高只等于其所调用的若干个函数的"异常安全保证中"的最弱者
  • copy-and-swap提供安全保证的解释
    为打算修改的对象复制一个副本,然后在副本上修改。若有修改动作抛出异常,原对象仍保持未改变状态。待所有改变都成功后,再将修改过的副本和原对象在一个不抛出异常的操作中置换

30.透彻了解inline的里里外外

  • inline可能增加目标代码大小,因为它会将所有被调用的“函数”替换为实现源码
  • inline只是对编译器的一个申请,不是强制命令;最终是否被内联,由编译器视情况而定
  • 使用inline关键字是显示申请;
  • 将函数定义在class内是隐式的inline申请;
  • inline函数和template通常都被定义与头文件,但不意味着函数模板一定要声明为inline;
  • 构造函数和析构函数往往是inline的糟糕候选人 => 因为编译器会在其中添加很多代码,导致其中的代码量不是想象中的那么少!;
  • incline函数无法随着程序库的升级而升级=> 因为它不是函数,所有调用了inline“函数”的地方必须重新编译链接;
  • 大部分调试器面对inline函数都束手无策 => 因为它不是函数……

31.将文件间的编译依存关系降至最低(重要)

这部分还需细细看出,重新理解!!!!!

  • 核心:接口与实现分离
    实在不知该如何描述……总之就是,编译时含入接口对应的头文件,不管怎么修改实现源码,重新编译时都只需要编译修改的部分源码,而不用所有代码一起重新编译
  • 两种实现方式
    Handle Classes:也就是pImpl手法,先声明一个Class作为接口,它内部有一个指针指向具体的实现(即ImplClass类/对象);具体的实现在ImplClass中……
    Interface Classes:这种方法类似于Java的接口及其实现;先定义一个Base类并将其中的函数都置为纯虚函数,然后定义Derived类继承Base并实现其虚函数……
  • 小结
    1. “编译依存最小化”的核心是:相依于声明式,不要相依于定义式(即接口与实现分离;
    2. 程序库头文件应完全且仅有声明式

第六章 继承与面向对象设计

32.确定你的public继承模塑出is-a关系

  • classes之间的关系
    is-a:是一个
    has-a:有一个
    is-implemented-in-terms-of:实现
  • public继承的is-a语义
    public继承主张,能够施行于base class对象身上的每件事情,也可以施行于derived class身上,因为每个derived class对象也是一个base class对象
  • 不正确的public继承举例
    class Rectangle{
    	double h;
    	double w;
    	void changeHeight(double h);
    	void changeWidth(double w);
    };
    
    class Square:public Rectangle{
    	// .......
    };
    
    这种情况下,如果子类(正方形Square)对象既修改了高度、又修改了宽度,它就不是正方形了,所以这里不能对Square对象施行Retangle的所有动作,也就是不应该使用public继承!!!

33.避免遮掩继承而来的名称(重要)

  • 继承中的名称遮掩示例

    class Base{
    	private:
    		int x;
    	public:
    		virtual void mf1()=0;
    		virtual void mf1(int);
    		virtual void mf2();
    		void mf3();
    		void mf3(double);
    		.....
    };
    
    class Derived:public Base{
    	public:
    		virtual void mf1();		// 将遮盖父类中的两个mf1函数(即使参数类型不同)
    		void mf3();				// 将遮盖父类中的两个mf3函数(即使参数类型不同)
    		void mf4();
    };
    // .......类的定义.....
    

    此时执行以下调用及其结果如下:

    Derived d;
    int x;
    .......
    d.mf1();	// 正确,调用的是Derived::mf1
    d.mf1(x);	// 错误,Derived遮掩了Base的mf1(int) => 所以这里还是调用的Derived::mf1,由于子类的mf1没有参数,所以执行错误...
    d.mf2();    // 正确,调用的是Base::mf2
    d.mf3();    // 正确,调用的是Derived::mf3
    d.mf3(x);   // 错误,Derived这样了Base的mf3(double) => 所以这里调用的还是Derived::mf3,但是参数不对……
    

    总结:名称查找规则就是先查局部,找不到再查更大范围 => 对这里的继承而言,就是先查子类,找不到再查父类……

  • 方法1:using 声明式
    使用using指定子类中可见父类的某些名称……

    class Base{
    	private:
    		int x;
    	public:
    		virtual void mf1()=0;
    		virtual void mf1(int);
    		virtual void mf2();
    		void mf3();
    		void mf3(double);
    		.....
    };
    
    class Derived:public Base{
    	public:
    		using Base::mf1;	 // 让Base内的名为mf1和mf3的所有东西,在Derived作用于内都可见
    		using Base::mf3;
    		virtual void mf1();		
    		void mf3();				
    		void mf4();
    };
    // .......类的定义.....
    

    此时调用及其结果如下:

    Derived d;
    int x;
    .......
    d.mf1();	// 正确,调用的是Derived::mf1 => 子类父类中都有,优先子类
    d.mf1(x);	// 正确,调用的是Base::mf1 
    d.mf2();    // 正确,调用的是Base::mf2
    d.mf3();    // 正确,调用的是Derived::mf3 => 子类父类中都有,优先子类
    d.mf3(x);   // 正确,调用的是Base::mf3
    
  • 方法2:转交函数
    简单来说就是通过private继承+inline => 详见p160

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

  • 接口继承与实现继承
    类似于声明式与定义式 => 接口继承是说继承父类中函数的声明;实现继承是说继承父类中函数的实现
    public继承之下,成员函数的接口总是会被继承
  • pure-virtual:只继承接口
    声明一个pure virtual函数的目的就是为了让子类只继承函数接口,函数实现需要且必须由子类自行完成;这是因为基类中不能实现纯虚函数,而在子类中则需要自行实现……
  • impure-virtual:继承接口和缺省实现
    声明普通virtual函数的目的,是让子类继承该函数的接口和缺省实现;这是因为基类中可以有virtual的实现,子类可以覆盖它,也可以选择直接使用父类的实现;但是要注意:如果基类没有virtual函数的缺省实现,子类必须实现它
  • non-virtual:继承接口及一份强制实现
    ……
  • 疑问:既然说non-virtual继承的是强制实现,那么子类为什么还可以重写/覆盖父类的non-virtual方法呢?
    虽然子类中可写定义同名、同参数的父类non-virtual方法,但是这种情况下不能实现多态 => 更多见条款36:
    class Parent{
        public:
        void nodePrint();
    };
    
    void Parent::nodePrint(){
        cout<<"this is parent"<<endl;
    }
    
    class Son:public Parent{
        public:
            void nodePrint();
    };
    void Son::nodePrint(){
        cout<<"this is son"<<endl;
    }
    
    
    int main(){
        Son son;
        Parent* pSon=new Son();
        pSon->nodePrint();
    
        cout<<"\nend!"<<endl;
        return 0;
    }
    执行结果:
    this is parent
    
    => 若将Parent的nodePrint改为virtual:
    this is son 
    

35.考虑virtual函数以外的其他选择(待二次学习)

  • 备注
    本条款主要讲述常规的virtual方法使用其他方法代替,包括模板方法模式、策略模式、以及一些其他内容,由于目前接触不断,尚未仔细学习……

36.绝不重新定义继承而来的non-virtual函数

  • 覆盖继承而来的non-virtual导致的问题

    class Parent{
        public:
        void nodePrint();
    };
    
    void Parent::nodePrint(){
        cout<<"this is parent"<<endl;
    }
    
    class Son:public Parent{
        public:
            void nodePrint();
    };
    void Son::nodePrint(){
        cout<<"this is son"<<endl;
    }
    
    int main(){
        Son son;    
        Parent* pSon1=&son;
        pSon1->nodePrint();
    
        Son* pSon2=&son;
        pSon2->nodePrint();
    
        cout<<"\nend!"<<endl;
        return 0;
    }
    

    执行结果如下:

    this is parent
    this is son
    

    对于指针pSon1并没有表现出多态性;只有当Parent中的nodePrint方法改为virtual时才能有多态性!!!

  • 原因分析
    non-virtual函数都是静态绑定的:即由于pSon1被声明为Parent的指针,调用的方法就用于是Parent中的方法;
    virtual函数都是动态绑定的:……

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

  • 静态类型和动态类型
    静态类型:就是在代码中被声明时采用的类型 => 程序执行过程中不能改变;
    动态类型:目前所指对象的类型 => 可在程序执行过程中改变(通常是由于赋值动作)

  • 对于non-virtual
    对于non-virtual(静态绑定)来说,重新定义继承而来的non-virtual是错误的(条款36),修改继承的non-virtual的缺省值同理……

  • 对于virtual
    virtual函数是动态绑定的:即调用一个virtual函数时,究竟调用哪一份函数实现代码,取决与发出调用的哪个对象的动态类型;
    缺省参数值是静态绑定的(不限于virtual函数的缺省参数):从而可能在调用一个定义于derived class内的virtual函数时,使用的却是base class给他指定的缺省参数值

  • 为什么缺省参数要静态绑定
    关键在于效率;如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值,这比目前实行的“在编译期决定”的机制更慢而且更复杂!!!

38.通过复合模塑出has-a或is-implemented-in-terms-of

  • 复合
    定义:复合是类型之间的一种关系,当某种类型的对象内含其他类型的对象,便是复合关系;
    复合的含义:复合意味着“has-a” 或者 “is-implemented-in-terms-of”
    复合(composition)的其他同义词:分层(layering)、内含(containment),聚合(aggregation)、内嵌(embedding)
  • has-a复合举例
    class Person{
    	private:
    		Address addr;					
    		PhoneNumber voiceNumber;
    		PhoneNumber faxNumber;
    	// ......
    };
    
    一个人拥有addr、voiceNumber…等,这些成员都是其他类; 属于has-a
  • is-implemented-in-terms-of复合举例
    比如使用list来实现set:
    template<class T>
    class Set{
    	public:
    		void member(const T& item) const;
    		void insert(const T& item);
    		void remove(const T& item);
    		std::size_t size()const;
    	private:
    		std::list<T> myset;
    };
    
    这里是打算通过链表来实现Set,Set类中对类型std::list< T> 的包含就属于“is-implemented-in-terms-of”

39.明智而审慎地使用private继承

  • private继承的一些规则
    规则1:如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个Base对象(比如需要父类对象作为参数,但是传入的是子类对象时) => 这就决定了private继承不是is-a
    规则2由private base class继承而来的所有成员,在derived class中都会变成private属性,即使他们在base class中原本是protected或者public

  • private继承的含义
    private继承意味着“implemented-in-terms-of” =>即如果D以private形式继承B,意思是D对象是根据B对象实现而得,除此没有其他含义

  • 在implemented-in-terms-of语义下,如何取舍private继承和composition
    尽可能使用复合, 必要时才使用private继承;

  • 使用private继承的时机
    (下面前提都是在implemented-in-terms-of语义下)
    1.当类A想要访问类B的protected成员时 => 如果采用复合,类A仍然不能访问类B的protected成员,但是private继承可以;
    2.当类A想要重新定义类B的virtual函数时 => 如果采用复合,A无法重新定义B的虚函数…
    3.EBO(empty base optimization):即对空间要求很严格的时候(详见下面叙述)

  • EBO
    凡是独立(非附属)的对象都必须有非零大小

    class Empty{};
    class HoldsAnInt{		// 按理说只需要一个int空间
    	private:
    		int x;	
    		Empty e;		// 按理说应该不需要内存,因为Empty内没有任何成员
    };
    

    但是sizeof(HoldsAnInt) > sizeof(int),因为c++编译器会自动安插一个char到空对象内
    上面的约束不适用与derived class内的base class成分:因为它们并非独立的:

    class HoldsAnInt:private Empty{
    	private:
    		int x;
    };
    

    sizeof(HoldsAnInt)==sizeof(int)
    => 对于A is-implemented-in-terms-of B,且基类空白的情况,使用private继承显然更节约内存,这就是EBO(empty base optimization),但是它只适用与单一继承……

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

  • 多重继承带来的问题
    1.歧义:程序可能从一个以上的base classes继承相同名称(如函数、typedef等),那会导致较多的歧义 => 需要通过类名指明要调用的具体成员…
    2.空间冗余:如果不是virtual继承方式,哪么某个父类的成分可能经过多条路径出现在子类中,造成空间冗余……
  • 补充:virtual继承也不一定总是好地
    virtual继承会增加大小、速度、初始化、赋值等的成本;
    如果virtual base classes 不带任何数据,将是最具使用价值的情况
  • 多重继承的使用场景
    1.尽量将多重继承改为单一继承的替代方案;
    2.以下场景推荐使用多重继承:某个实现类A需要继承接口类B,同时它需要通过类C实现其功能 => class A:public B,private C{.....};

第七章 模板与泛型编程(储备不足,待整理)

第八章 定制new和delete(储备不足,待整理)

第九章 杂项讨论

53.不要轻忽编译器的警告

  • 概述
    警告信息天生与编译器相依,不同编译器有不同的警告标准
    努力在你的编译器的最高(最严苛)警告级别下争取“无任何警告”!!!

54.让自己熟悉包括TR1在内的标准程序库

  • C++98标准库
    STL、Iostreams、国际化支持、数值处理、异常体系、C89标准程序库……

  • TR1包含的组件
    TR1算是对C++标准库的扩展,它位于std::tr1命名空间……,主要包括以下组件:
    智能指针、tr1::function、tr1::bind、hash tables、正则表达式、tuples、tr1::array、tr1::mem_fn、tr1::reference_wrapper、随机数生成工具、数学特殊函数、C99兼容扩展、type traits、tr1::result_of

  • 小结
    1.TR1是对C++标准库的扩展;
    2.TR1只是一份规范,Boost包含对tr1的实现(当然boost不仅只是tr1);

55.让自己熟悉boost

  • 概述
    Boost库是一个可移植、提供源代码的C++库,作为标准库的后备
    boost提供许多tr1组件的实现,此外还有许多其他程序库……
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值