Effective C++ 学习笔记

一、从c转向c++
1.尽量用const和inline而不用#define
 尽量用编译器而不用预处理
 const定义常量,当编译出错时能在提示信息中看到符号名
 inline定义内联函数,避免类似#define max(a,b) ((a)>(b)?(a):(b)) 在调用max(++a,b)时出现的不确定性
 
2.尽量用<iostream>而不用<stdio.h>
 类型安全,扩展性,避免变量和控制读写格式信息分开
 #include<iostream>得到置于std下的库元素;#include<iostream.h>得到置于全局空间的同样的元素,可能导致命名冲突
 
3.尽量用new和delete而不用malloc和free
 区别在于会不会调用构造和析构函数。
 
4.尽量使用c++风格的注释//
 /* */不允许嵌套
 
二、内存管理
5.new和delete要采用相同的形式
 new[]对应delete[]。使用typedef定义数组类型别名,当用delete去释放事实上为new[]的new分配的内存时内存泄漏
 
6.析构函数里对指针成员调用delete
 类的每个指针成员要么指向有效内存,要么指向空,构造函数必须确保这一点,析构函数才能直接delete掉。

7.预先准备好内存不够的情况
 使用set_new_handler处理new失败情况
 
8.写operator new 和operator delete 时要遵循常规
 分配程序支持new-handler并正确地处理了零内存请求;释放程序处理空指针。

9.避免隐藏标准形式的new
 类里定义了一个称为“operator new”的函数后,会阻止对标准new的访问。
 一个办法是在类里写一个支持标准 new 调用方式的operator new;另一个方法为每一个增加到 operator new 的参数提供缺省值。
 
10.如果写了operator new 就要同时写operator delete

11.为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
 未定义拷贝构造函数的以处理赋值操作符的方式进行处理(对指针成员进行逐位复制),会导致指针混乱内存泄漏。

12.尽量使用初始化而不要在构造函数里赋值
 一步动作替代两步动作;const成员不能被赋值。大量固定类型数据成员的情况可能例外。
 推广之,尽量使用一步初始化替代初始化后再赋值。

13.初始化列表中的成员列出的顺序和它们在类中声明的顺序相同
 因为要保证析构函数被调用的顺序与构造函数相反。静态成员例外;基类总是在派生类之前被初始化,跟继承顺序一致。

14.确保基类有虚析构函数
 原因:通过基类指针删除派生类对象,如果基类没有虚析构函数,结果不可确定。
 虚析构函数工作方式:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数。
 技巧:使用纯虚析构函数可以使类成为抽象类(无法被实例化),但仍需提供纯虚析构函数的定义,考虑定义为内联函数。
 
15.让operator=返回*this的引用
 兼容固定类型的常规做法“(a=b)=c ”。定义:String& String::operator=(const String& rhs){ return *this; }
 对于没有声明相应参数为const的函数来说,传递一个const是非法的。

16.在operator=中对所有数据成员赋值
 为类增加数据成员时容易遗漏。
 对基类调用operator=,使用“static_cast<Base&>(*this)=rhs;”,因为当operator=为编译器生成时“Base::operator=(rhs)”写法无法通过编译)
 拷贝构造函数中有同样的问题,使用“Derived(const Derived& rhs):Base(rhs),data(rhs.data){}”
 
17.在operator=中检查给自己赋值的情况
 当类有指针成员时尤其注意,否则导致指针混乱。 C& C::operator=(const C& rhs){ if(this==&rhs) return *this; ... }
 
三、类和函数:设计与声明
18.争取使类的接口完整并且最小

19.分清成员函数,非成员函数和友元函数
 虚函数必须是成员函数;若函数需对左边的参数进行类型转换,则使用非成员函数;需要访问非公有成员的非成员函数只能是类的友员函数,尽量避免友员。

20.避免public接口出现数据成员
 接口一致性,全是函数;读写控制;具备灵活性。
 
21.尽可能使用const
 编译器实施只读约束。
 const MyClass* p; 或 int const* p; //常量的指针
 MyClass* const p;//常指针
 const MyClass* const p;//常量的常指针
 const MyClass& c;//常量的引用
 const_cast<MyClass*>(p);//将p强制转换为常量的指针
 const成员函数在声明后面加const,表示const对象的成员函数版本。
 const成员函数不能修改对象中的任何一个比特,但能修改指针指向的数据。若要修改,可以使用自定义的非const版this指针。
 
22.尽量用“传引用”而不是“传值”
 引用是通过指针来实现的,本质上一样。
 避免多次调用拷贝构造函数和析构函数;避免切割问题(丧失多态特性)。
 
23.必须返回一个对象时不要试图返回一个引用
 当需要在返回引用和返回对象间做决定时,选择完成正确功能的那个。
 
24.在函数重载和设定参数缺省值间慎重选择
 如果可以选择一个合适的缺省值并且只是用到一种算法,则使用缺省参数。
 在重载函数中调用一个为重载函数完成某些功能的公共的底层函数。
 
25.避免对指针和数字类型重载
 因为传入NULL或0时不会使用指针版本,编译器也不认为这种调用具有多义性。
 
26.当心潜在的二义性
 c++允许有潜在的二义性,调用到时才会编译出错。
 单参数构造函数与类型转换运算符重载可能出现二义性;
 调用重载函数时隐式类型转换可能出现二义性,可通过显式转换解决;
 多重继承基类成员函数同名(无论是否公有),派生类中显式调用解决。

27.如果不想使用隐式生成的函数就要显式地禁止它
 把函数定义为私有的而且不去实现它,当成员函数或友元函数想去调时可以让程序在链接时出错。
 适用与条款45中的所有自动生成的函数。
 
28.划分全局命名空间
 用命名空间替代命名前缀。
 
四、类和函数:实现
29.避免返回内部数据的句柄
 防止调用者通过句柄直接修改内部数据
 
30.避免返回指向成员的非const指针或引用,但成员的访问级比这个函数低
 这样等于提升了内部成员的访问级别。可以通过返回const指针或引用解决。
 
31.千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针或引用
 返回局部对象的句柄在函数返回时局部对象被销毁导致句柄失效;返回new出来的对象极可能导致内存泄漏。此时应该返回对象。
 
32.尽可能地推迟变量的定义
 不同于c,没必要在函数开始处声明变量,在用时声明有助理解。
 尽量推迟变量的声明到可以为它提供一个初始化参数为止,可以避免不必要的初始化或析构开销。
 
33.明智地使用内联
 编译器可能拒绝内联复杂的函数,编译时会发出警告。
 在类定义内实现的成员函数自动为内联(隐式内联)。
 内联函数的定义在头文件里面,如果编译器当外联处理,则当头文件被不同cpp包含时,链接出现重复定义(部分编译器在这种情况下只编译一份,防止链接出错)。
 构造函数和析构函数通常不适合内联,因为构造基类、数据成员的代码会自动加到构造函数中,导致体积变大不适合当内联。
 虚函数也不能内联,因为需要动态绑定。
 调试版的不会使用内联,当使用内联时无法进入函数调试。

34.将文件间的编译依赖性降至最低
 区别声明、定义、实现,
 尽可能使用类的声明,而不使用类的定义。类声明“class MyClass;”替代类定义“#include <MyClass.h>”。
 可以使用句柄类(代理类)和协议类(接口类)降低编译依赖性。依赖接口,不依赖实现。
 
五、继承和面向对象设计
35.使公有继承体现“是一个”的含义
 D从B公有继承,表示任何可以使用B对象的地方都可以用D对象。

36.区分接口继承和实现继承
 纯虚函数控制派生类接口继承;非虚函数实现继承;
 如果想提供默认实现,最好利用纯虚函数同时提供实现,强制派生类实现接口,但允许用Base::VFunc之类的调用默认实现。

37.决不要重定义继承而来的非虚函数
 
38.决不要重新定义继承而来的缺省参数值
 虚函数是动态绑定,但缺省参数值是静态绑定(编译期确定,提高运行效率)。
 通过基类句柄调用派生类函数将使用基类的缺省参数值,导致混乱。
 
39.避免“向下转换”继承层次
 向下转换指从基类指针转换为派生类指针。
 万不得意时,可以用dynamic_cast做安全转换(dynamic_cast通过RTTI检查类型,区别static_cast的编译器静态转换)。

40.通过分层来体现“有一个”或“用...来实现”
    分层:较高抽象层次的类依赖较低抽象层次的类(人依赖于脑、胃,可以理解为组合)
    继承:具体的类依赖抽象的类

41.区分继承和模版
 当对象的类型不影响类中函数的行为时,使用模版来生成这样一组类。
 
42.明智地使用私有继承
 私有继承意味着“用...来实现”,继承实现而忽略接口。
 与分层的区别是,派生类能利用基类的保护成员和通过虚函数动态绑定。

43.明智地使用多继承
 可能导致二义性,可用显式调用或更改成员函数名解决;
 可能导致菱形继承,可用虚基类解决,但虚基类影响效率,而且很难在设计时决定是否使用。
 构造函数的调用顺序:虚基类的构造函数、非虚基类、对象成员、派生类的构造函数。
 A是B和C的虚基类,D同时继承自B和C,则D的构造函数初始化列表中必须包含对A的初始化,而B和C构造函数中对A的初始化将被忽略。
 虚基类应该禁止包含数据,即使用纯接口,这样可以避免D构造函数初始化列表中对A的初始化。
 若D继承自B而虚继承自C,则首先仍然初始化A,不过之后C比B先被初始化。

44.说你想说的;理解你所说的
 
六、杂项
45.弄清c++在幕后为你所写、所调用的函数
 编译器可能会自动生成类的一个拷贝构造函数、一个赋值运算符、一个析构函数、一对取址运算符。
 如果没定义任何构造函数(包括拷贝构造),编译器将默认生成一个。
 拷贝构造和赋值运算符规则是:逐一拷贝(调用拷贝构造函数)非静态数据成员。
 包含引用类型的数据成员,c++不会自动生成拷贝构造和赋值运算符。
 生成的都是public函数,如果想禁用必须显示声明为private
 
46.宁可编译和链接时出错,也不要运行时出错
 
47.确保非局部静态对象在使用前被初始化
 由于无法控制不同被编译单元中非局部变量的初始化顺序,故不同非局部静态变量之间的依赖将导致混沌。
 使用Singleton模式。
 
48.重视编译器警告
 
49.熟悉标准库
 为兼容c,支持<stdio.h>、<string.h>之类的c库。
 具有c库功能的c++标准库,<cstdio>、<cstring>,c库的std命名空间版本。
 c++模板库STL,<iostream>、<string>、<vector>等,处于std命名空间下。
 
50.提高对c++的认识
 任何奇怪的特性都是有原因的,都是经过深思熟虑权衡利弊的结果。
 

原文地址:https://www.cnblogs.com/chenwx/p/2240910.html