Effective C++ notes

Effective C++ notes

三月 28, 2019 阅读 57 字数 7461 评论 0 喜欢 0


这本书译于06年,里面的技术有些已经产生变化,需要在学习中进一步辨别

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

C++主要的次语言:

1.C

2.Object-Oriented C++

3.Template C++

4.STL

二 尽量以const,enumeration,inline替换#define

1.使用template inline function代替宏函数:

template<typename T>

inline void callWithMax(const T& a, const T& b) {
f(a>b?a:b);
}

2.定义数组时必须为数组分配内存,可以采用enum hack方式,enum不可以取地址:

class GamePlayer {
private:

enum {NumTurns = 5};

int scores[NumTurns];
};

三 尽可能使用const

1.STL里面两种iterator:

const std::vector<int>::iterator iter = vec.begin(); //iter相当于T* const

*iter = 10; //没问题

++iter; //错

std::vector<int>::const_iterator cIter = vec.begin(); //cIter相当于const T*

*cIter = 10; //错

++cIter; //没问题

2.const成员函数不能修改成员变量,可以加上mutable

3.四种类型转换:

static_cast:允许內建类型和自定义类型转换,自定义类型向上转是安全的,即有继承关系的子类指针转向父类指针,向下转是不安全的

dynamic_cast:安全向下转型,不允许內建类型转换,向下转换时不安全会报错,发生多态时转换不会报错。

const_cast:常量转换,去除常量属性,只能用于指针和引用。

reinterpret_cast:重新解释转换,允许任意类型转换,不建议使用。

4.运用const成员函数实现出其non-const孪生兄弟技术:

const_cast<char&>(static_cast<const T&>(*this)[position]); //第一次转换将*this加上const,第二次将[]返回值的const转除

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

1.尽量使用初始化列表,如果成员变量不确定初始值,就使用空的初始换列表: A::A():val1(),val2(){},如果有多个构造函数,可以合理遗漏部分成员变量,并将赋值操作移到某个private成员函数供所有构造函数使用。

2.为免除“跨编译单元值初始化次序”问题(避免出现客户在未构造对象前调用),用local static对象替换non-local static对象:

FileSystem& tfs() {
static FileSystem fs;

return fs;
}

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

big five

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

若不想对象被拷贝构造或者赋值,可以将拷贝构造函数和赋值操作符声明为private并不实现它们,或者私有继承一个将拷贝写为私有的父类.

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

string和STL里面的容器没有虚析构函数,不要去继承它们,不然会导致析构出错,内存泄漏.

八 别让异常逃离析构函数

析构函数绝对不要吐出异常,析构函数可以捕捉异常,然后吞下它们或者结束程序.如果客户需要对异常做出反应,那么在class里面提供一个普通函数来执行该操作.

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

因为这类调用从不下降至派生类.

十 令operator=返回一个reference to *this

同理于+= *= -=等,这样操作是为了能够使用链式表达式.

十一 在operator=中处理”自我赋值”

1.考虑证同,但会导致效率下降.

2.先记录原对象指针,后面删除,避免删除

Widge& operator=(const Widge& rhs) {
if (this == &rhs) return *this;

Bitmap* pOrig = pb;

pb = new Bitmap(*rhs.pb);

delete pOrig;

return *this;
} //Bitmap *pb为对象成员

十二 复制对象时勿忘其每一个成分

发生继承时,要在对应的构造和赋值函数里面调用父类的函数.

十三 以对象管理资源

1.RAII:Resource Acquisition Is Initialization,获得资源后立刻放进管理对象,管理对象的析构函数确保资源被释放.

std::auto_ptr<Investment> pInv(createInvestment());

auto_ptr的特性:通过拷贝构造和拷贝赋值操作,它们会变为NULL,而复制的指针将获得资源的唯一所有权.所以auto_ptr不用于STL容器里,代替它的是std::tr1::shared_ptr(RCSP:Reference Counting Smart Pointer),它允许正常的复制行为.不要使用这两种指针管理array new,因为它们的析构函数使用的是delete,而不是delete[].

RTTI:Run Time Type Identification背后的实现机制:(扩展)

  无论基本类型还是用户定义类型,都需要额外的内存来存放type_info对象。

  多态类的type_info对象是如何存放的?要给每一个多态类增加一个指针成员,一个type_info对象,以及给虚函数表增加一项。该方法对任何多态类都是一样的,与程序中对象的个数无关。因此,使用typeid()来检索每个对象运行时的类型信息所花时间是一样的(通过vptr间接完成)。

同任何其他对象一样,type_info对象的创建过程也需要时间。

RTTI要求程序维护一颗继承树,dynamic_cast<>能够判断源对象与目标类型之间是否具有is-a关系,这需要在运行时遍历继承树,并且其开销会随着源对象类型与目标类型之间距离(层次)的增大而增大。

关于这点书中所言技术已经过时,C11中引用了scoped_ptr代替auto_ptr,被scoped_ptr所指的对象不能转移所有权

十四 在资源管理类中小心copying行为

复制RAII对象时候要复制它所管理的资源,当一个RAII对象被复制时可能的应对方法:

1.禁止复制,参考条例六

2.对底层资源使用”引用计数法”,tr1::shared_ptr允许指定deleter

class Lock { public:

explicit Lock(Mutex *pm): mutexPtr(pm, unlock) { //unlock为指定的deleter lock(mutexPtr.get()); //get()返回原始指针 }

private:

std::tr1::shared_ptr<Mutex> mutexPtr; }

3.复制底部资源

4.转移底部资源的拥有权

十五 在资源管理类中提供对原始资源的访问

一般都会get()取得原始指针的方法,也重载了指针相关操作符.

十六 成对使用new和delete时要采取相同形式

new->delete new[]->delete[],在这里注意不要使用typedef std::string Address[4]这种语句,因为它要求使用delete[],但是容易用错delete.

十七 以独立语句将newed对象置入智能指针(newed表示对象的创建的被动性,是英语语法)

例如:在函数调用时将放置到智能指针的语句写到参数列,如果其他参数调用失败(比如其他参数是某个函数的返回值,该函数抛出异常),将导致难以察觉的资源泄露.

十八 让接口容易被正确使用,不易被误用

cross-DLL problem:对象在动态链接程序库DLL种被new创建,却在另一个DLL中被delete销毁.tr1::shared_ptr不存在这个问题,它是原生指针的两倍大小.以动态分配内存作为记录用途和deleter之专属数据,以virtual形式调用deleter,在多线程中修改引用次数时蒙受线程同步化得额外开销.

十九 设计class犹如设计type

对象该如何被创建和销毁

对象的初始化和赋值该有什么差别(构造函数和重载=)

新的对象如果被以值传递,意味什么(拷贝构造函数的实现)

对象成员的合法性(维护class的invariants约束条件,如月份不超过12之类)

class需要配合某个继承图系吗(virtual函数)

需要什么样的转换(如在class T1内写出一个类型转换函数(operator T2),T2为想要转换为的类型,如:operator string(){})

什么样的操作符和函数对class是合理的

什么标准函数该驳回(private)

谁该取用class的成员(权限声明,友元)

class的未声明接口(undeclared interface)

class有多么一般化(是否该定义template)

真的需要一个新的class吗?

二十 宁以pass-by-referece-to-const替换pass-by-value

bool fun(Student s) —> bool fun(const Student& s);

slicing:对象切割,一个derived对象以value传递,并被视为base,调用了base的构造函数.

即使是小型的class,但是调用它的拷贝构造函数可能会面临着深拷贝的问题,代价昂贵.

对于基本类型,STL的迭代器和函数对象,用pass-by-value比较合适.

二十一 必须返回对象时,别妄想返回其reference

避免返回一个局部对象,或者使用heap的对象(因为没人去负责delete)

二十二 将成员变量声明为private

1.为了接口的一致性,只能调用类的成员方法

2.封装性,保护数据和便于维护

二十三 宁以non-member,non-friend替换member函数

是否让一个函数放到类内,主要看他是否有访问成员变量的需求.如果只是对成员函数的封装,可以将这个函数放到头文件扔到namespace声明.

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

例如: result = oneHalf * 2;

result = 2 * oneHalf;

若果在类内重载*,第二个语句报错.解决办法是在类外重载,允许两个位置的参数都接受类型转换.

二十五 考虑写出一个不抛异常的swap函数

不完全理解!

class Widget {  

public:  

    …  

    void swap(Widget&other)  

    {  

        using std::swap; // 此声明是必要的  

        swap(pImpl, other.pImpl); // 若要置换Widget就置换其pImpl指针  

    }  

    …  

 };  

 namespace std {     

     template<> // 这是std::swap针对“T是Widget”的特化版本  

     void swap<Widget>(Widget& a, Widget& b)  

     {  

         a.swap(b); // 若要置换Widget, 调用其swap成员函数  

     }  

二十六 尽可能延后变量定义式的出现时间

为了避免对象构造后长时间不用占用资源,(如果中间有异常抛出,则在处理异常中该对象移植占用资源)在即将用到对象时候定义,并且调用其合适的构造函数,而非先调用默认构造,然后拷贝复制这种冗繁操作.

二十七 尽量少做转型动作

类型转换是生成一个副本以供使用,如果转换的是对象,那么操作的就是临时副本,会导致意想不到的结果.

dynamic_cast如果作用于多重继承的对象身上,会影响效率.

二十八 避免返回handles指向对象内部成分

1.返回的handles可能导致内部成员的改变,可以加在返回值前加const

2.可能导致指向内部成员的handles寿命比对象长,发生dangling handles的风险.

二十九 为”异常安全”而努力是值得的

异常安全的三种保证:

1.基本承诺:若果异常抛出,程序内的事物仍保持在有效状态

2.强烈保证:若异常抛出,程序状态不改变.采用copy and swap策略,实现上采用pimpl idiom->pointer to implementation,构造一个对象将他们的指针互换,但这种策略的额外消耗值得衡量.

3.Nothrow保证.

三十 透彻了解inlining的里里外外

慎用inline,编译器可以自行优化,inline不利于扩展和debug.

三十一 将文件间的编译依存关系降至最低

1.如果使用引用或者指针可以完成任务,就不要使用对象.

2.使用抽象类

三十二 确定你的public继承塑模出is-a关系

Liskov Substitution Principle

三十三 避免遮掩继承而来的名称

派生类内的名称会遮掩基类的名称,可使用using在派生类使用基类名称.

三十四 区分接口继承和实现继承

1.pure virtual纯虚函数指定接口继承

2.impure virtual非纯虚函数具体指定接口继承和缺省实现继承

3.non-virtual朴素函数指定接口继承和强制性实现继承

三十五 考虑virtual函数以外的其他选择

完全没看懂!

三十六 绝不重新定义继承而来的non-virtual函数

三十七 绝不重新定义继承而来的缺省参数值

三十八 通过复合塑模出has-a或”根据某物实现出”

public一般而言要形成is-a的关系,而private形成has-a的关系.

三十九 明智而审慎地使用private继承

当派生类需要访问基类的成员,或者重新定义virtual函数时采用private继承.

四十 明智而谨慎地使用多继承

1.多重继承有正当的用途,可以public继承接口,private继承实现

2.多重继承会导致额外开销,使用virtual继承可以节省开销,如果虚基类不带数据,虚继承会很有价值.

四十一 了解隐式接口和编译期多态

四十二 了解typename的双重意义

1.nested dependent name:嵌套类型类型名

tmpalte<typename T>

void print2nd(const C& container) {
if (…)

typename C::const_iterator iter(container.begin());  //nested,告诉编译器C::const_iterator是一个类型,一定要使用typename
}

2.不可以在基类列或成员初值列使用:(模板化基类不能通过编译)

template<typename T> 

class Derived: public Base<T>::Nested { //不能加typename
public:

explicit Derived(int x): Base<T>::Nested(x) { //不能加typename

typename Base<T>::Nested temp; //可以

};

3.了解下面的语句,告诉编译器value_type是一个类型

typedef typename std::iterator_traits<IterT>::value_type value_type;

四十三 学习处理模板化基类内的名称

上个例子中的问题有三个解决办法:

1.在函数前面加this->调用

2.使用using声明这个函数属于基类

3.明确使用基类::的作用域符

但这些只是告诉编译器使用者承诺会写出对应的版本,但如果没写出来,对该函数的调用会导致错误.

四十四 将与参数无关的代码抽离templates

1.因非类型模板参数导致的膨胀:以函数参数或者成员变量代替模板参数,template<typename T,int n>->template<typename T>

2.因类型参数导致的膨胀:让带有完全相同二进制表述的具体类型共享代码.如list<int*>,list<const list*>,list<class<long>*>等

四十五 运用成员函数模板接受所有兼容类型

不太理解里面的例子

如果声明了泛化的构造函数和拷贝构造函数,还是需要声明正常的copy和assignment函数

template<class T>

class shared_ptr {
public:

shared_ptr(shared_ptr const& r);
template<class Y>

shared_ptr(shared_ptr<Y> const& r);


};

四十六 需要类型转换时请为模板定义非成员函数

当我们编写一个class template,想让它支持所有参数隐式类型转换时,将那些函数定义为class template内部的友元函数.因为class内部的函数自动成为inline函数,所有可以只让友元函数调用一个类外写好的函数.

四十七 请使用traits classes表现类型信息

四十八 认识template元编程(template metaprogramming)

以C++写成,执行于C++编译器内的程序.一旦TMP程序结束执行,其输出(从template具现出来的C++代码)便回被编译.最终的程序更小,更高效,但是编译时间会变长,一些运行期才能发现的错误可以在编译器发现.

template<unsigned n>

struct Factorial {
enum {value = n * Factorial<n-1>::value};
};

template<>

struct Factorial<0> {
enum {value = 1};
};

int main() {
std::out << Factorial<5>::value;
}

TMP以”递归模板具现化”取代循环,可以用来生成”基于政策选择组合”的客户定制代码,也可以便面生成对某些特殊类型不适合的代码.

四十九 了解new-handler的行为

set_new_handler允许客户指定一个函数,用于在内存分配无法满足时调用.

std::set_new_handler(OutOfMem); //参数是一个函数指针

五十 了解new和delete的合理替换时机

五十一 编写new和delete时需固守常规

五十二 写了placement new也要写placement delete

五十三 不要轻忽编译器的警告

五十四 让自己熟悉包括TR1在内的标准程序库

五十五 让自己熟悉Boost

发表评论

电子邮件地址不会被公开。