【阅读笔记】Effective C++

【C++】万字详解Effective C++

C++是一个语言联邦

  1. C
  2. Object-Oriented C++
  3. Template C++
  4. STL

四大范式:

  1. 过程式编程PP:Procedural Programming。
  2. 面向对象编程OO:Object-Oriented Programming。
  3. 泛型编程GP:Generic Programming。
  4. 模板元编程:Template Metaprogramming。

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

  1. C++的初始化列表(构造函数初始化列表)初始化顺序只与定义的顺序有关,跟在初始化列表中的顺序无关。

构造/析构/赋值运算

  1. C++11增加了关键字delete, default等,可以方便地控制默认函数(C++自动生成)。
  2. 基类析构函数应声明为virtual,否则当对基类指针进行delete操作时,会调用基类析构函数,而不是子类析构函数。如果继承并不是为了多态,则应该不用虚析构函数。
    • C++11增加了final关键字,可以防止某个类被继承。
  3. 析构函数不应该抛出异常
    • 异常点后面的程序可能有资源释放,提前抛出异常则资源泄漏
    • 异常发生时,C++会通过调用析构函数来释放资源,但如果是析构函数本身抛出异常。。。。然后崩溃
  4. 构造和析构过程中不要调用virtual函数,虚函数在构造析构过程中并不虚。
  5. 赋值操作符
    • 返回*this的引用(处理连续等号)
    • 处理自我复制(删除当前成员,构造新成员,设置新成员,删除后参数指向的对象也没了)
    • 处理异常安全(new失败了,然而当前成员已经删除,这造成该成员指针悬垂)

资源管理

  1. 以对象管理资源:RAII
  2. 智能指针(使用make_shared和make_unique)
    • make_unique在C++14中加入
    • 这样内存分配时引用计数控制块和对象在一块儿,一次new也比两次new快
    • 还有造成泄露的可能,new对象后,其他函数抛出异常,这时new出来的对象指针遗失

设计与声明

  1. 将成员变量声明为private
    • 实际上只有两种权限:封装与不封装
      • 用户视角:private/protect=封装,public=不封装
      • 子类视角:private=封装,protect/public=不封装
      • 整体来看protect并不具有封装性
    • 基类的private成员只能被基类的成员函数或友元函数访问
    • https://blog.csdn.net/scott198510/article/details/129075736
    • 封装性意味着改变之后影响的范围尽可能小。

    public和private。在用户的视角,public就是public,private和protected被他一同视作了private;在子类的视角,private就是private,public和protected被他一起视作了public。只不过是视角不同而已

  2. non-member函数有时比member函数更恰当
    • non-member函数不能访问private成员,也因此non-member函数不依赖private成员,更具封装性。
    • 如需所有参数都可以类型转换(尤其是一些数学计算之类的),使用non-member

实现

  1. 尽量延后变量定义出现的时间
  2. 类型转换
    • 尽量避免转型,将转型隐藏于某个函数背后,使用新式转型
    • dynamic_cast<>执行效率不高(运行时完成转换)
      • 如果不正确会返回空指针
      • 依赖运行时类型信息(RTTI),必须具备多态性(父类有虚方法),如果父类没有虚方法则只能使用static_cast
  3. 避免返回对象内部成分的引用、指针等
    • 返回一个私有对象的引用会破坏封装性,退一步也尽量返回一个const类型的
    • 例外:operator[]
  4. 异常处理
    • 异常安全:(1)不泄露资源,如上锁后,中间发生异常,锁没有释放;(2)不允许数据破坏,如一系列操作是一起的,某个操作失败,则其他操作会破坏一致性
    • 对于new这种操作
      • 如果项目有处理,就抛异常,否则最多在最外层打印个日志。
      • set_new_hander钩子,new失败后清理资源,然后继续程序或结束程序
      • 使用new(std::nothrow) xxx,这样new失败会返回null,不抛异常
      • 智能指针
    • 三种保证
      • nothrow:使用nonexcept关键字,不是100%不抛处,而是抛出时遇到了重大问题
      • 强烈保证:失败则恢复调用之前状态
      • 基本承诺:状态可能改变,但具备一致性
  5. 减少文件间的编译依赖关系
    • pimpl
      • 类成员放到struct里面,类中只放一个指针,在类前面前置声明,impl文件中定义该struct。
        • 头文件中值包含了“接口信息”,具体实现都在impl类,而且对impl类的引用只有一个指针,所以该头文件并不需要include impl类。故而降低依赖关系。
        • 好处是降低依赖关系,一定程度上增加了封装性,减少增量编译时间
        • 缺点是可读性变差,运行效率降低,而且接口本身不能被直接构建出来(不能在栈中,需要工厂类)
    • 把所有的东西都包含在一起,这样依赖关系扁平化,也可以加快增量编译速度,哈哈哈
    • 尽量将实现写进cpp而不是头文件,尽量不要包含不需要的头文件

继承与面向对象设计

  1. 记住public继承是一种“is a”的关系,不要随便使用继承(比如鸟类的飞行属性,但企鹅并不会飞;比如正方形是矩形,但对矩形的操作并不能全都可以作用在正方形上,例如增加某个方向的长度)要时刻记住is a的面向对象关系并不一定和现实生活中的相同
    • 每次继承的时候也要问一句:这是不是is a的关系?
  2. 注意继承时可能会发生的名称遮盖
    • 虚函数也可能被覆盖(参数列表不相同)
    • 非虚函数和成员变量则会覆盖(只要同名就会覆盖,不论参数和返回值),这是作用域的原因。如果要访问,直接在名称前面加上ClassA::即可
      • 对于同名函数,如果想要父子实现的函数都可见(以类似重载的关系存在),可以使用using在子类中声明。
      • 从“is a”的关系来看,编写代码时不应该出现覆盖
  3. 接口继承和实现继承
    • 纯虚函数指定接口继承,普通虚函数同时指定接口继承和默认实现继承,非虚函数指定接口继承和强制实现继承
    • 可以选择将接口继承和实现继承分开,例如定义一个默认实现(protect的非虚函数),然后定义一个纯虚函数。这样就要求子类必须去处理这个功能应该使用什么方式运行(自己定义或者即便不自己定义也要调用一下默认实现)。还有一种形式是把默认实现放在父类的纯虚函数定义中,子类通过ClassA::fun()的形式调用默认实现
  4. 考虑virtual函数以外的选择
    • NVI(Non-Vertual Interface)手法实现模板方法设计模式
      • 非虚函数规定了框架,调用private的虚函数,子类可以重写某一步骤的具体实现
    • 函数指针/function对象实现策略模式
      • 可灵活指定具体实现
      • 古典策略模式是通过将功能也设计成父子类,并通过多态指针来实现的
  5. 绝不重新定义继承而来的缺省参数值
    • 虚函数是动态绑定的,但是虚函数的默认参数值却是静态绑定的
    • 就是说如果有一个父类多态指针,其调用某个已经被子类重写的函数,函数虽然确实是子类定义的函数,但是使用的默认参数值却是父类中的,也即“静态绑定”
    • 所以不要重新定义缺省参数值,以免程序不符合预期
  6. private继承和组合
    • private继承并不是ia a和关系,而是is-implemented-in-terms of(根据某物实现出)的关系
    • 组合表示has a的关系和is-implemented-in-terms of的关系
    • 大部分情况下应该使用组合,只在特殊情况可以考虑private继承:
      • 需要访问protected成员,或者需要重新定义继承来的虚函数
      • 追求对象尺寸最小化(父类没有成员变量,空白基类)
    • private继承无法阻止派生类重新定义虚函数,可使用public继承定义一个嵌套类,然后组合在外面的类中
      • C++11后增加了final关键字
  7. 多重继承
    • 歧义性
      • 如果从多个基类继承到了相同名称,会导致歧义
    • 菱形继承导致出现重复成员
      • 默认是复制,成员重复。要想只保留一份,基类必须全部是虚继承得来的,如class basic_ostream : virtual public basic_ios{},但虚继承会增加很多开销
    • 因为其复杂性,所以除非不得已,不要使用多重继承
    • 如果要使用多重继承,请不要在基类中放置数据(类似java中的interface)

模板与泛型编程

  1. 隐式接口和显式接口
    • 如果不用泛型,那么全部的接口都可以通过直接阅读代码得知,这些接口都有具体的声明或定义。多态发生在运行时
    • 某个模板函数可能支持传入各种类型,但并不是任意的,这个类型必须能够完成函数内部可能的操作,这种约束是隐式的。多态发生在编译期
  2. typename与“嵌套从属类型”
    • class和typename在模板参数部分可以通用,嵌套从属类型只能用typename
    • 嵌套从属类型是类似C::iter这样的,这种写法默认是变量,而不是类型,要显式在前面加上typename才说明这是个类型
    • 嵌套从属类型在继承列表和初始化列表里面不需要加typename
  3. 处理模板化基类的名称
    • 模板基类的派生类中,并不可直接访问基类成员函数/变量
      • 加上this->
      • 使用using声明
      • 使用TBase<T>::fun()来访问。不推荐,因为如果fun是虚函数,那么无法完成多态
  4. 尽量减少可能的代码膨胀
    • 非类型模板参数可能更容易产生代码膨胀,改成函数参数更好一些

new和delete

  1. STL容器虽然在堆上分配内容,但并不由new和delete管理,而是自己的分配器管理
  2. 在一些情况下使用定制的new和delete会提高很多性能
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值