Effective C++读书笔记
1. C++的四个组成部分(相较C)c的过程式部分、面向对象、模板编程和STL。
2. 尽量用const或者enum取代c语言中的#define变量定义,因为define会逃过编译器的预编译检查。使用inline代替define的宏定义,以防止片面效应
3. 尽量使用const,常见的场景如下
/*if const in the left of "*", the data is const. if it in the right of "*", the pointer is const */ const char*p = "abc"; //non-const pointer, const data char const *p = "abc" //non-const pointer, const data char* const p= "abc"; //const pointer, non-const data const char* const p = "abc"; //const pointer, const data
//Declare const iterator const std::vector<int>::iterator it = vec.begin(); //like T * const *it = 10; // it's ok ++it; //error! it is a const std vector<int>::const_iterator it = vec.begin(); //like const T * *it = 10; //error ++it; //it's ok /*******************************************************/ /*const also could be used on parameter, function and return value**/ /*******************************************************/
4. 对象成员变量的初始化发生在进入构造函数之前,所以初始化工作应该在构造函数初始化列表中完成。否则对效率有影响(初始化一次,赋值一次); 另外,初始化顺序总是先基类,后派生类。而且成员变量的初始化顺序按照类中声明的顺序进行,而与初始化列表中的顺序无关。
NOTE:为防止跨文件的对象初始化依赖问题,可以将全局静态改为函数内的静态。因为全局静态变量在多个源文件间没有先后初始化的明确说明。详见条款四末尾。
5. 若一个类没有声明任何一个构造函数,编译器会为其自动生成一个构造函数、析构函数、拷贝构造函数和赋值构造函数。这些函数都是public且inline的。
6. 可以声明私有的拷贝构造函数和赋值构造函数防止编译器自动生成这两个函数。
7. 1)带有多态性质(类中含有虚函数)的类,需要声明virtual析构函数,否则派生类的析构函数不会被调用,导致内存泄露。2)不是为派生而设计的类,不需要声明virtual函数,因为为满足虚函数机制,需要在对象中添加vptr指针,无缘浪费空间。
8. 为防止析构函数中抛出异常一般处理方法是catch到异常后终止程序(abort())或者记录这个异常(不是好主意)
9. 不要在构造函数和析构函数中调用virtual函数!因为有可能在派生类没有构建之前就尝试调用派生类的构造函数,如:
class B { public: B(); virtual void log(); }; class D : public class B { public: D(); virtual void log(); }; D d; //Invoke base B constructor firstly, however B() will invoke B's log, not D's(which is expected)
Note:如果要完成类似“构造函数内记录log信息”的需求,可以将log函数声明为非virtual函数,派生在构造函数时将参数向上传递给base 类。如:
class B { public: explicit B(para); void log(string para); }; class D { public: D(para) : B(para) {}//set the para to base constructor };
10. 赋值函数需要返回一个指向*this的引用,从而支持连续赋值运算。赋值函数的基本形式为:Widget & operator=(const Widget& rhs);
11. 谨慎处理operator=的自赋值问题。示例如下:
class B { public: B(); ~B(); B& operator= (B& rhs) { //This is the error method // delete s; // if this rhs = *this, the original data will be deleted // this->s =new String (rhs.s); //This is the right method. another method is check whether rhs equals to *this String * porig = s; s = new String(rhs.s); delete *porig; return *this; } private: String s*; }
12. copy函数应该确保赋值对象内所有成员,包括基类成分。不要尝试用某个copy函数去实现另外一个copy函数,应将共同的部分放在另一个函数中。
13. 以对象的形式管理资源。尽量减少使用手工调用delete释放资源的行为,应该更多的依赖于对象的析构函数。
1)auto_ptr智能指针,起析构函数自动对起所指对象调用delete。使用auto_ptr时注意,一定保证别让多个auto_ptr指向同一个对象,这样将导致对象被删除多次。而且,若通过拷贝构造函数和赋值构造函数复制auto_ptr, 原始的auto_ptr将变成null,而复制的指针将获得对象的唯一所有权。
class Investment // base class {}; Investment * createInvestment(); // Investment factory f() { //auto_ptr std::auto_ptr<Investpement> pInv(createInvestment()); //copy std::auto_ptr<Investment> pInv2(pInv);//pInv is null, pInv2 point to object pInv2 = pInv; // pInv is null, pInv2 point to object }
2)tr1::shared_ptr(基本使用方法和auto_ptr相同)可以避免auto_ptr的单引用问题,shared_ptr是一种引用计数指针,当对象计数为0时销毁。使用shared_ptr时应注意环形引用问题。
Note:auto_ptr和shared_ptr在其析构时调用的是delete,而释放[]型资源不可以使用上述指针。
14. 实现自己的资源管理类时注意copying复制行为,例如:一个mutex lock(constructor) unlock(destructor)类。处理copying的方式为禁止复制函数和使用shared_ptr进行析构函数注册。
15. new和delete的使用形式要完全一致,否则会产生内存泄露。一致的意思是:new 与delete搭配,new []与delete[]搭配。
16. 设计一个新类一般要考虑的问题:新类型如何被创建和销毁、对象的初始化和赋值、新对象如何按值传递、新对象的继承体系、何种操作符和函数是合理的、那些标准函数应该被驳回(设为private)等
17. 除内置类型、stl的迭代器和函数对象外,尽量用const对象的引用(const B &b)代替对象的传值调用(B b)。因为const引用型,不仅可以避免没有必要的拷贝构造函数和析构函数的调用以提高效率,而且可以防止(const)对象在函数内被改变。
18. 必须在一个函数中返回一个对象时,返回该local对象的值而不是heap/stack/static对象。
19. 将成员变量声明为private,这不仅赋予客户访问数据的一致性,而且提供了很好的封装性。更改private数据不会对用户产生任何影响。
20. 如果非成员函数(通过调用其他成员函数)和成员函数可以提供一样的功能时,采用非成员函数形式,这样可以增加类的封装性!
21. 从效率的角度出发,应该尽量延后对象变量的定义。因为对象的定义会调用构造函数和析构函数,所以一些操作时要考虑是否使用带参数的构造函数代替默认构造函数从而节约构造函数的调用次数,如:
//Only b(B)constructor invoked B b(b1); f(b); //Default constructor, assignment function invoked. B b; b = b1; f(b)
22. 类型转换问题, 通常有以下三种类型转换语法:
1)旧式c风格
(T)expression //expression to T
T(expression) // expression to T(via constructor)
2)新式转换
const_cast<T>(expression):用来讲对象的常量型移除(const 转换为非const),唯一由此能力的C++转换函数
dynamic_cast<T>(expression):用来执行“安全向下转型”, 即用来决定某对象是否归属继承体系中的某个类型
reinterpret_cast<T>(expression):执行低级别转换(如将int *转换为int),不可移植,具体实现取决于编译器
static_cast<T>(expression):强迫隐形转换,如将non-const转换为const,int转换为double等
准则:尽量避免转型,特别注重效率的代码避免dynamic_cast转型;如果转型是必要的,将转型隐藏在某个函数之后,避免客户直接调用;使用新式转型代替旧式c转型(容易搜索辨认,且新类型分类明细);
23. 避免返回handler(私有数据成员或私有成员函数引用)到对象外部,这将导致private数据成员的private性降低(外部可以通过返回的引用改变私有数据成员)和引用的数据已经不复存在的风险。
24. 条款31阐述了pImpl实现方式(类中不包含数据成员,仅定义该类的一个具体实现)具体形式如下,可参考logcplus的logger类和loggerImpl类
class T { public: T * getInstance(p1, p2, p3); //interface virtual functions which will invoke impl->**() ... private: T(); TImpl *impl; }; class TImpl { public: //interface with implmentation };
25. public 继承的含义是is a, 凡是积累适用的地方派生类同样适用。在现实设计领域要注意设计上的分析,如矩形和正方形,鸟和企鹅(企鹅不会飞)
26. 派生类的名称会遮掩基类对象的名称(遮掩时值考虑名称,忽略具体类型,例如:double a;可能遮掩int a;),可通过using声明式使用基类的对象。
class Base { private: int a; public: virtual void mf1() = 0; virtual void mf1(int); virtual void mf2(int); void mf3(); void mf3(double); ... }; class Derived : public Base { public: virtual void mf1(); void mf3(); void mf4(); ... }; Derived d; int x; ... d.mf1();//ok!, Derived::mf1() d.mf1(x);//error! Derived::mf1() override Base::mf1() d.mf2();//ok! Base::mf2 d.mf3();//Derived::mf3() d.mf3(x);//error! Derived::mf3() override Base::mf3();
27. 区分接口继承和实现继承
1)声明一个纯虚函数的目的是让其派生类只继承函数接口
2)声明非纯虚函数的目的是让派生类继承接口和缺省实现
3)声明非虚函数的目的是为了令派生类继承函数的接口和一份强制实现
28. 考虑虚函数以外的一些选择,策略类举例:
//cal()完成了一些函数的初始化工作,派生类实现自己特点需要的doCal方法, class Base { public: void cal(); { ... doCal(); ... } private: virtual doCal(); }; ============================ //由函数指针实现的策略类 class Base; int defaultFun(const Base & b); class Base { typedef int (*fp) (const Base &); explicit Base(fp p = defaultFun) : mp(p) {} int fun() const { return mp(*this); } private: fp mp; }; //Different object could use different functions int obj1fun(const Base &b); int obj2fun(const Base &b); Base obj1(obj1fun); Base obj2(obj2fun); ======================================
29. 不要重新定义继承的非虚成员函数
30. 不要随意更改或定义缺省的参数值,因为缺省参数值不会派生类的重定义而动态更改。
注:以下条款需要进一步阅读,条款24、25,29,第七章(模板与范型编程)和第八章(定制new和delete)。