《Effective C++》学习笔记

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

C++

C++是一个多重泛型编程语言,同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式,具有强大的能力和弹性。

四个次语言

将C++视为语言联邦,有多个次语言

  1. C: 没有模板,异常,重载,类的C。
  2. Object-Oriented C++: C with class, 封装,继承,多态,虚函数。
  3. Template C++: C++泛型编程。拓展出元编程能力。
  4. STL: 容器,迭代器,算法
什么是元编程

可以扩展程序自身,这样的能力,为元编程。
扣定义的话,动态生成html的程序也可以叫元编程。
Lisp 以元编程为基本。而 Ruby 因为元编程易用又强大。

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

#define
#define //处于预处理阶段 
#define MAX 1.6
#define CALL_MAX(a,b) f((a)>(b)?(a):(b)) 
问题
  1. #define MAX 1.6
  • 预处理阶段就会把MAX全换成1.6,当运用此常量得到编译错误信息后,可能不知所云,因为错误信息也许会提到1.6而不是MAX
  • 可能会导致生成多个常量1.6,码量可能会大一点。
  1. #define CALL_MAX(a,b) f((a)>(b)?(a):(b))
a=5;
b=0;
CALL_MAX(++a,b);  //a被加两次
CALL_MAX(++a,b+10);  //a被加一次
    //a的递增次数要看代码执行了几次。

可以改为

inline int test(const int& a,const int& b){
    return a>b?a:b;
}
  1. 在类中使用一个不变的值,可以定义为const,最好再定义为static,只保留一份
谨记
  1. 对于单纯常量,最好用const或enum替换#define.
  2. 对于形似函数的宏,最好用inline函数替换#define.
  3. #define在控制编译上还是有用的,#ifdef,#ifndef

3.尽可能使用const

const

char val="hello";
char* p = val;
const char* p = val;    //p指向的数据不能变
char* const p = val;    //p不能变
const char* const p = val;  //都不能变

除了上面的懂了,下面的,讲真的,complex。

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

初始化
  1. 为内置对象进行手工初始化;
  2. 对于对象,构造函数最好用初始化列表,在构造函数中写的是赋值,不是初始化。初始化次序和它们在类中声明的顺序一样。
  3. 单实例模式。

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

一个空类默认生成的几个函数
  1. 默认构造 default
Student s;
  1. 拷贝构造 copy
Student s(s2);
  1. =重载 copy assinment
Student s;
s=s2;
  1. 析构函数

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

  1. 将copy构造和copy assignment声明为private而不实现
  2. 写一个uncopyable,将copy构造和copy assignment声明为private而不实现,然后继承此类的类就都不能copy和=了。

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

virtual析构函数
  1. 抽象类最好声明一个virtual的析构函数。如果类中带有任何virtual,都最好声明一个virtual的析构函数。
  2. 非基类最好不要声明virtual析构函数

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

析构函数的异常
  1. 析构函数最好不要抛出任何异常,不然会导致不可预料的结果。如果可能有异常,就在异常发生时强制结束程序或者记录异常,不处理继续执行。

  2. 析构函数中的异常处理,程序猿最好在外面自己另外定义一个函数处理。

例如:
有一个数据库管理类,在析构函数中要调用db.close()。可能会有异常。
这是应该自己写个close函数,将关闭数据库的函数写到自己的函数中,并自己调用,而不应该交给析构函数。

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

  1. 因为基类在构造和析构过程中调用的虚函数不能传到派生类
  2. 派生类实例化时,先基类调用构造函数,但此时虚函数还是一个未知的函数。

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

为了实现连锁赋值
  1. x=y=z=5 等价于 x=(y=(z=5))
  2. 为了实现“连锁赋值”,必须返回一个reference to *this.
int& operator=(const int& rhs){
    ---
    return *this;
}

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

  1. 必须判断自我赋值,不然可能出问题,例如返回本身的引用,却在赋值前先把自己delete了(书上的例子),总之实现自我赋值就是了
if(this==&rhs){
    //如果是本身就不做操作
    return *this;
}
else 做其他的赋值操作

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

copy构造和copy assignment要写完全
  1. 即该复制的都要复制
  2. 派生类复制对象的时候也必须调用基类的拷贝函数
  3. 一般自己写copy构造是需要深拷贝的情况,即有指针的情况下,不然的话默认拷贝构造可能比自己写的好用。
  4. 不要让赋值函数调用拷贝构造函数,建立独立的成员函数来做这件事。一般是private函数并命名为Init。所有copying函数共同调用此函数

13.以对象管理资源

资源
  1. 动态分配内存
  2. 文件描述符
  3. 互斥锁
  4. 图形界面中的笔刷
  5. 数据库连接
  6. socket
资源释放
  1. 获得资源后立即放进管理对象,如智能指针auto_ptr,定义为智能指针,自己不用释放,指针结束后会自己调用析构函数释放资源。

  2. 管理对象运用析构函数确保资源被释放

  3. 两个RAII(罐装容器):tr1::shared_ptr 和 auto_ptr. RAII设计还是比较精巧的,而且很有用,不过比较难用。

  4. tr1::shared_ptr 的复制会增加引用计数,析构会减少引用计数。当引用计数变成0才真正释放资源。

  5. auto_ptr 复制的时候,原来的指针变为NULL,只保留一份资源

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

  1. 复制RAII对象必须一起复制他所管理的资源,资源的copying行为决定了RAII(罐装容器)对象的copying行为
  2. RAII即对对象资源处理的封装,要控制释放资源。
  3. RAII class copying行为一般有两种:禁止copy,对资源引用计数。

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

  1. 每一个RAII class应该提供一个”取得资源” 的办法
  2. 比如tr1::shared_ptr 提供了get函数

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

  1. new[] --> delete[],即new中不用[],delete也不要用[],要不就都用。

17.以独立语句将newed对象置入智能指针

  1. 总结就是 new 一个对象和把它放入一个只能指针中间不能再执行其他代码,不然抛异常的话,对象没有被智能指针管理,就会造成内存泄露。书上的例子比较有意思。

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

  1. 不一致性造成的开发人员心理和肉体上的摩擦与争执,没任何一个IDE可以完全消除

  2. “促进正确使用”的办法包括接口的一致性以及与内置类型的行为兼容

  3. “阻止误用”的办法包括建立新类型、限制类型上的操作、束缚对象值、消除客户的资源管理责任

  4. tr1::shared_ptr 支持定制型删除器。可被用来自动解除互斥锁

19.设计class犹如设计type

  1. 就是设计class时要考虑全面

20.宁以pass-by-reference-to-const替换pass-by-value

  1. 自己定义的对向尽量引用传递,而不是值传递。
  2. 引用传递可以避免切割问题。什么是切割问题呢?就是传一个带有自己特性的子类对象,若直接值传递,就会复制,可能就传的是个基类对向了。如传Windows窗口,具体看书吧。

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

  1. 不要返回一个reference指向local stack对象或者heap-allcated对象

22.将成员变量声明为private

  1. 这可赋予用户数据访问的一致性。

  2. protected不比public更具封装性

  3. 封装性与 “当其内容改变时可能造成的代码破坏量” 成反比。(封装的越好,内容改变造成的破坏越小)

23.宁以non-member、non-friend替换member函数

  1. 即如果可以的话,尽量非成员函数实现某些功能,可以传入对向的引用对对象进行操作。

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

  1. 令classes支持隐式类型转换是个糟糕的主意,即最好加上explicit声明禁止隐式转换。

  2. 如果需要为某个函数的所有参数进行类型转换,那么这个函数必须是一个non-member

25.考虑写出一个不抛异常的swap函数

  1. 标准库的swap(T &a,T &b);仅支持T类型有copy构造或copy assignment的情况。

  2. 需要自己写swap的时候要注意,,,一般也不需要自己写吧。

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

  1. 对象的构造和析构都会造成开销。尽量在使用的时候再定义。

  2. 最好在定义的时候初始化,可以省去默认构造的开销。

27.尽量少做转型动作

转型
(1)旧式转型
(int)a;
int(a);//都是将a转换成int类型
(2)C++新式转型
const_case<T>( expression )     //移除对象的常量性
dynamic_case<T>( expression )   //安全向下转型
reinterpret_case<T>( expression )   //低级转型
static_case<T>( expression )    //强迫隐士转换,和(int)a类似
注意
  1. 尽量避免转型
  2. 如果必须转型,应将转型动作隐藏在函数里,客户直接调用函数。
  3. 尽量使用C++新式转型

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

  1. 不要返回内部handle,也不要返回指向访问级别较低的成员函数。降低dangling handle的可能性

  2. 即public函数不要返回private成员的引用,必须返回的话,要加上const,即返回const XXX& 类型。

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

异常安全
  1. 不泄露任何资源
  2. 不允许数据败坏
异常安全函数的三个保证
  1. 基本承诺:如果异常抛出,程序的其他数据仍然保持有效状态,不会被破坏,例如改变屏幕背景,若失败,背景应该为改变前的样子或默认背景。
  2. 强烈保证:如果函数失败,会回到程序调用函数前的状态。能通过copy-and-swap实现,但并非对所有函数都可实现且有意义。
  3. 不抛掷保证:承诺不抛出异常,函数总能完成它该有的功能。

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

  1. inline应该用在小型的、频繁的函数,主要是省去函数调用的开销。
  2. 若过多用inline,代码量就会增大,代码膨胀可能会导致额外的换页行为,降低指令告诉缓存的命中率,随而降低效率。

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

  1. 能使用reference 和pointer的,就不要用object,就是尽量用引用和指针。
  2. 类中用对象的话,编译阶段就会确定类的大小,用指针的话计算的是指针的大小。
  3. 类中尽量用指针,不过用指针的话要注意深度拷贝和析构的问题,默认析构函数不会delete指针成员
  4. 程序库头文件应该仅有声明式

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

  1. 如果要继承的话,一定要是is-a关系,即基类的每个函数都适用于派生类

33.避免遮掩继承而来的名称

  1. 在子类中的同名成员会遮掩基类中的成员

  2. 适用using:Base::函数名(),就可以让基类中使用基类中的函数

    • 若基类有多个重载,子类中有重名函数就会覆盖所有重载,而使用using就可以重新用那些重载
    • 若基类中是私有变量,使用using就能在子类中使用

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

  1. pure virtual —— 接口继承,在子类中必须实现

  2. non-pure virtual —— 接口 + 缺省实现继承,子类中可以再实现,也可以用默认的,不过有时候不想用默认的却会忘了实现,可以看书中飞机例子的解决办法。

  3. non virtual —— 接口+强制实现继承,子类中不能更改它的行为

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

  1. 不一定非得用virtual函数,替代方案有很多,比如:函数指针成员变量,NVI手法,Strategy设计模式等

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

  1. 对应34条,不要在子类中重新定义non-virtual,若需要重写,就把基类中的定义为virtual(怎么感觉又和35矛盾了。。。)

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

  1. 主要是对virtual的子类实现来说,若基类中virtual函数有缺省值,子类中不应该改变。
  2. 缺省值是静态绑定的,而virtual是动态绑定的。

38.通过复合塑模出has-a或“根据某物实现出”

  1. “复合(composition)”的近义词:组合,分层(layering),内含(containment),聚合(aggregation),内嵌(embedding)
  2. 要有 has-a 的关系

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

  1. 一般而言,不需要用private继承。复合基本可以完全包含private继承的能力

  2. 一般必须要用virtual函数的时候可以用私有继承,否则复合基本可以完全包含private继承的能力

  3. 2可能说的不对0.0

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

  1. 多重继承可能导致成员歧义,即基类中有相同的成员名字

  2. 菱形/钻石继承,所有继承的类都变为虚继承,但是,虚继承的那些class产生的对象往往比使用non-virtual继承的体积大,访问这些成员变量也相对较慢。

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

  1. 在构造函数前声明 explicit 关键字,禁止隐式转换,从而变成显式接口

  2. 类中某些成员函数是virtual的,当调用这些函数的时候就表现出运行期多态。

  3. 编译期多态主要是对templates模板来说,类型的多态

  4. 讲真这条不太理解在强调什么。

42.了解typename的双重意义

  1. 声明template时,可以用class或typename,二者等价
template<class T>
template<typename T>

43.学习处理模块化基类内的名称

  1. 不太懂,等需要模板编程的时候再细看吧。

44.将与参数无关的代码抽离templates

  1. 若两个函数实现过程中有很多重复代码,应该将重复代码单独写成一个函数,调用之。

  2. 对模板同理,至于怎么操作。。。

45.运用成员函数模板接受所有兼容类型

  1. 模板这块先略过吧,没有什么编程需求,所以没有直观感受。

46.需要类型转换时请为模板定义非成员函数

  1. 模板这块先略过吧,没有什么编程需求,所以没有直观感受。

47.请使用traits classes表现类型信息

  1. 模板这块先略过吧,没有什么编程需求,所以没有直观感受。

48.认识template元编程

  1. 元编程执行于编译期,可以把工作转移一部分到编译期,而且一些在运行期才能检查出来的错误也能转到在编译期检查出来。

  2. 通过template<当做参数>实现

49.了解new-handler的行为

  1. new_handler声明与std
namespace std{
    typedef void (*new_handler);
    new_handler set_new_handler(new_handler p) throw();
}
  1. new_handler 是一个函数指针,它有什么用呢?
    当new一块空间不足时,它就会抛出异常,但在异常之前会先调用一个客户指定 的错误处理函数,就是new_handler,通过ser_new_handler(函数名)来设置。

  2. 卸载new_handler
    set_new_handler(NULL)

  3. 作用局限,只适用于内存分配,后续的构造函数调用可能还是会抛出异常。

50.了解new和delete的合理替换时机

  1. 一般替换掉默认的new和delete一般是为了: 提高性能(比如tcmalloc),收集数据、检测错误等。比如valgrind之类(Valgrind是一款用于内存调试、内存泄漏检测以及性能分析的软件开发工具。)

  2. 替换方法:简单来说就是封装malloc和free,relloc等,做出自己想要的效果。。。。简单来说。

51.编写new和delete时需固守常规

  1. 无限循环尝试分配内存,若无法满足内存需求,就该调用new_handler

  2. 也要有能力处理0bytes申请。还应该能处理比正确大小更大的申请

  3. delete 应该在收到NULL指针时不做任何事

52.写了placement new也要写placement delete

  1. 主要讨论一个在构造函数中抛出异常会不会内存泄露的问题。placement new/delete 就是在类中自己重载new和delete操作符。

  2. 一个对象在构造函数中抛出异常,对象本身的内存会被成功释放,但是其析构函数不会被调用。其内部局部成员对象清栈时是会被释放掉的,故其析构函数被调用,但是用户在构造函数中动态生成的对象没有被delete掉(new出来的是一个指针,清栈时是不会delete掉栈里的指针)。

  3. 如果一个对象在构造函数中打开很多系统资源,但是构造函数中后续代码抛出了异常,则这些资源将不会被释放,建议在构造函数中加入try catch语句,对先前申请的资源进行释放后(也就是做析构函数该做的事情)再次抛出异常,确保内存和其他资源被成功回收。

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

  1. 额总之就是在不过度依赖编译器的情况下达到0警告

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

C++98加入的新特性
  1. STL
  2. iostreams
  3. 国际化支持,如:wchar_t等
  4. 数值处理:如复数模板complex
  5. 异常阶层体系
  6. C89标准程序库
TR1

14个新组件,std::tr1::shared_ptr

  1. 智能指针
  2. tr1::function,传递函数指针,详情再看教程吧。
  3. tr1::bind,比bind1st,bind2nd更好用的STL绑定器,绑定的函数有点像传递仿函数的功能。
  4. Hash tables,STL的map和se是用红黑树实现的,这实现的Hash结构的。
  5. 正则表达式
  6. Tuples(变量组)
  7. tr1::array
  8. tr1::mem_fun
  9. tr1::reference_wrapper
  10. 随机数,超越rand,rand是C库的
  11. 数学特殊函数,拉格朗日等
  12. C99 兼容扩充
tr2
  1. type traits
  2. tr1::result_of
???

看到最后,来一句:TR1只是规范,还没有实物???但是智能指针什么的《C++ primer》都说了。。。好吧,05年的书太早了,看了上面这么多,瞬间觉得自己根本不懂C++,都快C++20了,complex,学的好累0.0,还是python简单些。。。

55.让自己熟悉Boost

定义
目标
  • 作为一个“可被加入标准C++库的测试场”,以TR1提案进入标准C++的14个新程序序库中,超过三分之二是基于Boost的成果
网上言论
  1. 说尽量用STL不要用boost什么的,现在很多boost都成标准了什么的。。。崩溃,STL还了解一些,不过理解也就是一些容器迭代器仿函数什么的。看了boost那么大,及其崩溃,不知从何学起。唉慢慢来吧。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值