feisky

云计算、虚拟化与Linux技术笔记
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

Effective C++

Posted on 2009-11-04 15:31  feisky  阅读(777)  评论(0编辑  收藏  举报

最近又重新看了Effective C+,不过到现在还是有好多地方不懂的,先记下笔记,待用的时候再细细琢磨。

条款1:尽量用const和inline而不用#define

这个条款最好称为:“尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。

用const的好处是,调试时,可以直接获取变量,而非定义的数字,这个在使用gdb跟踪代码的时候很有用,比如#define NUM 123;如果在gdb中print NUM,会出现NUM找不到符号表的问题,这样在复杂表达式中出现NUM进行watch的时候要回去找NUM的具体值很郁闷。

使用const不仅如此,其不允许改动的语义才是其存在的精华, 常量指针和指针常量很容易让人弄混,来再记一遍,星号在中间,左定内容右定针。
const char *p;    char const* p都表明p指向的内容不能变
char * const p 则表明指针不能变.

抛弃#define使用inline的原因就是类似#define max(a,b) ((a)>(b)?(a):(b)) 这种写法除了使用无数括号很bt之外,max(++a,b)这个简单的表达式便可轻而易举废掉程序员的本意,这点足以放弃#define。

条款2:尽量用<iostream>而不用<stdio.h>

EC(Effective c++)给出Item2的理由,cin/cout型别安全,比如Rational r; cout<<r在stdio.h中是无法想象的。当然,Rational要针对<<进行支持,有关写operator <<又是一个要讨论的Item,这里事先给出正确写法
class Rational{
    friend ostream& operator<<(ostream&s, const Rational& r)
}

ostream& operator<<(ostream& s, const Rational& r)
{
    s<< r.n << '/' << r.d;
    return s;
}

顺便说一句,本条款的标题没有打印错;我确实说的是<iostream>而非<iostream.h>。从技术上说,其实没有<iostream.h>这样的东西——标准化委员会在简化非C标准头文件时用<iostream>取代了它。他们这样做的原因在条款49进行了解释。还必须知道的是,如果编译器同时支持 <iostream>和<iostream.h>,那头文件名的使用会很微妙。例如,如果使用了#include <iostream>, 得到的是置于名字空间std(见条款28)下的iostream库的元素;如果使用#include <iostream.h>,得到的是置于全局空间的同样的元素。在全局空间获取元素会导致名字冲突,而设计名字空间的初衷正是用来避免这种名字冲突的发生。还有,打字时<iostream>比<iostream.h>少两个字,这也是很多人用它的原因。:)

条款3:尽量用new和delete而不用malloc和free

malloc和free(及其变体)会产生问题的原因在于它们太简单:他们不知道构造函数和析构函数。
new的过程:申请内存(也即malloc的作用),调用构造函数,返回对象指针(后面讲到operator new ,placement new都是基于这个基本知识)

另外,new和delete对应,malloc和free对应这个也是常识了,但是为啥呢?EC里面讲到,如果混用会导致不可预料的错误。

条款4:尽量使用c++风格的注释

相对/**/这种注释,多用用//,而//在VS2005以及Eclipse下面都有快捷键,VS2005是Ctrl+K,C(按住Ctrl,先后按K和C)取消是Ctrl+K,U,Eclipse则方便的多,只用Ctrl+/即可

条款5:对应的new和delete要采用相同的形式

简单的说,就是单个对象和数组要区分对待。C++使用[]区分这是单个对象还是数组,所以new中有[]的时候,请用delete[]。

条款6:析构函数里对指针成员调用delete

这条为了防止内存泄露,具体说来要做三件事情:

  每个构造函数中将该指针初始化
  每个赋值运算符中将原有内存删除,重新配置一块
  每个析构函数中,delete这个指针

条款7:预先准备好内存不够的情况

operator new申请内存得不到满足时,在抛出std::bad_alloc之前会调用用户设置的handler,该调用找到足够的内存才停止

typedef void(*new_handler)();
new_handler set_new_handler(new_handler p) throw();

因此,自己定义new handler需要遵循以下原则:

让更多内存可用
自己处理不了的情况下,安装一个不同的new handler
卸载这个new handler,抛出std::bad_alloc
直接调用abort或者exit
现在标准的operator new的行为时抛出一个std::bad_alloc的异常,其实,现在很少有情况会无法申请到内存,按照标准的做法即可。在抛出bad_alloc异常之后,做好log记录和分析,一旦遇到这种情况,加内存就是了。

条款8: 写operator new和operator delete时要遵循常规

void * operator new(size_t size)

{

if(size==0){ //1.大小为0的new也可以成功

size=1;

}

while(true){ //2.不断循环,尝试申请内存

if(alloc success) return *pointer //3.成功返回指针

//4.不成功,处理错误

new_handler globalHandler = set_new_handler(0);

set_new_handler(globalHandler);

if(globalHandler) (*globalHandler)();

else throw std::bad_alloc(); //5.没有处理函数,则抛异常

}

}

delete应该遵循的则很简单,即删除一个null指针永远是安全的

void operator delete(void * rawMemory)

{

if(rawMemory ==0 ) return;

否则再删除内存

}

条款9: 避免隐藏标准形式的new

因为内部范围声明的名称会隐藏掉外部范围的相同的名称,所以对于分别在类的内部和全局声明的两个相同名字的函数f来说,类的成员函数会隐藏掉全局函数

条款10: 如果写了operator new就要同时写operator delete

让我们回过头去看看这样一个基本问题:为什么有必要写自己的operator new和operator delete?

答案通常是:为了效率。缺省的operator new和operator delete具有非常好的通用性,它的这种灵活性也使得在某些特定的场合下,可以进一步改善它的性能。尤其在那些需要动态分配大量的但很小的对象的应用程序里,情况更是如此。

缺省的operator new需要在返回指针的前方使用一点空间记录该指针占用的大小(该空间称作cookie),用于delete的正常运行。自己重载operator new,则可以自己进行管理这个区块,减少内存使用

实现内存池,每次从内存池中申请,若内存池也不够的话,则扩张之

所以,写了一个operator new之后,要对应写一个operator delete,因为只有自己才知道到底是如何申请内存的。

条款11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符

也就是说,class内有一个指针,使用new来动态申请内存的情况下,默认的copy constructor和assignment运算符是浅拷贝(bitwise copy),也即直接拷贝指针的值,可能会有内存泄露的危险
String a("hello");{String b("hello");b=a;}当b=a,b原来的内容变成野指针,当b结束作用域后a的内容也被删除。这真是灾难
所以条款11告诉我们:class内有指针需要申请内存,则自己撰写拷贝构造和赋值函数,避免内存泄露和异常。

条款12: 尽量使用初始化而不要在构造函数里赋值

原因:
1,const、reference只能通过初始化列表进行初始化
2,从效率角度。对象的构造分成两阶段:初始化data member(可以根据初始化列表进行,无则初始化为0等默认值),执行被调用的构造函数。所以执行assignment实际执行了两次赋值。
3,基本类型的non-const, non-reference对象,初始化和赋值之间没有2所说的区别

条款13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同

编译器构造和析构的顺序是相反的,编译器不可能针对初始化列表中的顺序进行初始化,否则重载不同初始化顺序的构造函数会让编译器头晕的。编译器内部确定是按照class内的声明次序,如果初始化列表不同,很可能初始化列表的数据会错误。
核心:先按class内声明成员默认赋值,然后调用初始化参数列表进行初始化。

条款14: 确定基类有虚析构函数

基类指针指向具体派生类,delete基类指针的时候,需要虚函数进行多态。
小tip:如果析构函数不是虚的,那么基类和派生类的析构都要调用 ,先调用派生,再调用基类
tip2:虚函数要占用class空间,要综合考量

条款15: 让operator=返回*this的引用

原因:objA = objB = objC这种连续的重载=号行为
e.g.

String& String::operator =(const String&rhs)
{

return *this;
}

条款16: 在operator=中对所有数据成员赋值

原因:编译器会默认为你生成一个operator=,采用bitwise,所以最好都是自己写一个
注意点:继承机制的引入,Base中的私有成员Derived对象无法访问
要么 Base::operator=(rhs),要么staqtic_cast<Base&>(*this) = rhs两种方式解决

条款17: 在operator=中检查给自己赋值的情况

一般采用的方法:

C& C::operator=(const C&rhs)
{
if(this==&rhs) return * this;
}

这主要是针对如何判断对象相等的问题,这里采用的是地址相等的方法

条款18: 争取使类的接口完整并且最小

条款19: 分清成员函数,非成员函数和友元函数

成员函数和非成员函数最大的区别在于成员函数可以是虚拟的而非成员函数不行。所以,如果有个函数必须进行动态绑定(见条款38),就要采用虚拟函数,而虚拟函数必定是某个类的成员函数。
1,如果要实现虚函数,必须是member function
2,让operator<<和operator>>成为non-members,如果还需要获取类的非公共成员变量,声明为friend。原因,如果是func为member,那么以后书写顺序应该是obj>>cin,obj<<cout,这样不符合习惯
3,只有non-member才能在最左参数身上实施型别转换。如果需要对函数f的最左侧参数进行型别转换,那么f为non-function,如果还需要获取类的非公共成员变量,声明为frind。
举例,operator *(Class &lhs, Class &rhs)这种声明,2*obj2的调用,需要对2进行型别转换(构造函数声明为explicit可以阻止隐式型别转换),这样就必须为non-member

条款20: 避免public接口出现数据成员

Effective中举了三个原因,说明为什么不要放在公开接口中

一致性,以后对类对象的所有操作,均需要带(),也就是只能调用函数,不能获取变量
获取控制性,比如只读、可读可写、不处理,通过不同的函数实现
函数抽象性,提供一个借口,底层如何实现上层用户不用关心
不过在实际编程中,很少人能够完全做到这点,毕竟需要自己花些时间来写get和set,暂时我也没找到自动生成get、set函数的方法,所以鱼与熊掌不可兼得,若想获得好处,就得费力写get、set了。

条款21: 尽可能使用const

使用const的好处在于它允许指定一种语意上的约束——某种对象不能被修改——编译器具体来实施这种约束。通过const,你可以通知编译器和其他程序员某个值要保持不变。只要是这种情况,你就要明确地使用const ,因为这样做就可以借助编译器的帮助确保这种约束不被破坏。

const关键字实在是神通广大。在类的外面,它可以用于全局或名字空间常量(见条款1和47),以及静态对象(某一文件或程序块范围内的局部对象)。在类的内部,它可以用于静态和非静态成员(见条款12)。

1,*号在中间,前定内容后定针
2,返回值用const修饰,说明返回值是只读的,不能修改
3,函数后面用const修饰,说明该函数不能修改任何变量。函数可以据此进行重载,有const的函数被const对象调用,没有const的函数被非const对象调用
const的真正意义是什么?不变性,具体的体现有两种说法:A, bitwise. B, conceptual,A说法对位进行比较,如果没有修改则认为是不变的。B从概念层面进行判断,即使底层有修改,但对上层概念来讲是不变的,那就是不变的。但是C++语言只支持A,所以为了应付B,引入mutable修饰词,用来修饰上层概念不变,但是底层要修改的底层变量。

条款22: 尽量用“传引用”而不用“传值”

c语言中,什么都是通过传值来实现的,c++继承了这一传统并将它作为默认方式。除非明确指定,函数的形参总是通过“实参的拷贝”来初始化的,函数的调用者得到的也是函数返回值的拷贝。

正如我在本书的导言中所指出的,“通过值来传递一个对象”的具体含义是由这个对象的类的拷贝构造函数定义的。这使得传值成为一种非常昂贵的操作。

void printnameanddisplay(const window& w)

C语言里面都是传值
传值成本比较大,会调用对象的拷贝构造,如果类比较复杂,则会创建和析构更多的对象
传引用会避免切割问题。Func(base&) 和Func(base)两种函数声明,内部调用f()虚函数,如果传递个derived对象,则传引用会调用derived.f(),而传值则会切割而调用base.f()

条款23: 必须返回一个对象时不要试图返回一个引用

用重载乘法举例

Inline const Rational Operator*( const Rational& lhs, const Rational & rhs)

{

return Rational(lhs.n*rhs.n, lhs.d*rhs.d);

}

传回的是value,如果传回reference的话,内部变量析构之后,引用没有真正的对象

写一个必须返回一个新对象的函数的正确方法就是让这个函数返回一个新对象。

条款24: 在函数重载和设定参数缺省值间慎重选择

void g(int x=0);

g();

g(10);

void f(); void f(int x);

f();

f(10);

两种方式要谨慎选择,避免出现模棱两可的情况

条款25: 避免对指针和数字类型重载

void f(int x);

void f(string *ps);

f(0)

0的存在会对指针和数值造成模棱两可,所以要坚决避免针对指针和数值进行重载

条款26: 当心潜在的二义性

C++有一个哲学信仰,它相信潜在的模棱两可状态不是一种错误,但是对程序员来讲,将所有问题放到运行后发现就是一种灾难。所以程序员应该避免模棱两可。
类的转换,一是拷贝构造方式可以隐式转换,一是operator Class()方式,当需要型别转换时,就会有模棱两可

语言标准转换,6.02可以转换成int也可以转换成char
多继承也是如此
当遇到模棱两可情况时,程序员应该显式的说明采用哪种方式。

条款27: 如果不想使用隐式生成的函数就要显式地禁止它
使用private修饰防止公开调用
不定义防止friend等调用
private:

Array& operator=(const Array &rhs);(注意这里;表示不定义)

条款28: 划分全局名字空间

namespace name1{

}

using namespace name1;

最好每个人都以自己姓名为name,进行分割,这样可以类似Java中的包的概念

条款29: 避免返回内部数据的句柄

传回handle之后,打破了抽象性,所以要避免
对于non-const member functions而言,传回内部handle也会导致麻烦,当涉及暂时对象,Handle可能变成悬空的(dangling)

条款30: 避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级比这个函数要低

条款31: 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用

条款32: 尽可能地推迟变量的定义

推迟变量定义可以提高程序的效率,增强程序的条理性,还可以减少对变量含义的注释。看来是该和那些开放式模块的变量定义吻别了。
需要的时候再定义,延缓定义式的出现,当出错时就会减少内存的使用。

条款33: 明智地使用内联

内联函数------多妙的主意啊!它们看起来象函数,运作起来象函数,比宏(macro)要好得多(参见条款1),使用时还不需要承担函数调用的开销。你还能对它们要求更多吗?

然而,你从它们得到的确实比你想象的要多,因为避免函数调用的开销仅仅是问题的一个方面。为了处理那些没有函数调用的代码,编译器优化程序本身进行了专门的设计。所以当内联一个函数时,编译器可以对函数体执行特定环境下的优化工作。这样的优化对"正常"的函数调用是不可能的。
1,好处:直接用代码替换,减少函数调用成本
2,坏处:造成代码膨胀现象,可能会导致病态的换页现象
3,大部分编译器会拒绝将复杂的(内有循环或递归调用)函数inline,而所有虚拟函数都不能inline
4,构造函数和析构函数最好不要inline,即使inline,编译器也会产生出out-of-line副本,以方便获取函数指针

条款34: 将文件间的编译依赖性降至最低

条款35: 使公有继承体现 "是一个" 的含义

条款36: 区分接口继承和实现继承

声明一个纯虚函数的目的是让子类只继承其接口
声明一般(非纯)虚函数的目的,是为了让子类继承该函数的接口和缺省行为
声明非虚函数的目的是为了让子类继承函数的接口和实现。且"不变性"凌驾于"变异性"之上,我们不应该在子类重新定义它。

条款37: 决不要重新定义继承而来的非虚函数

条款38: 决不要重新定义继承而来的缺省参数值

条款39: 避免 "向下转换" 继承层次

条款40: 通过分层来体现 "有一个" 或 "用...来实现"

条款41: 区分继承和模板

模板用来产生一群class,其中对象性别不会影响class的函数行为

继承应用于一群class身上,其中对象性别会影响class的函数行为

条款42: 明智地使用私有继承

私有继承意味着 "用...来实现"。如果类D私有继承于类B,类型D的对象只不过是用类型B的对象来实现而已;类型B和类型D的对象之间不存在概念上的关系

如果是私有继承,编译器不会隐式的将子类对象转化成基类对象
私有继承,基类所有函数在子类都变成私有属性
私有继承意味着根据某物实现,与layering相比,当protected members和虚拟函数牵扯进来会有很大的优越性。
私有继承,子类仅仅是使用了父类中的代码,他们没有任何概念上的关系。

条款43: 明智地使用多继承

多继承会产生模棱两可,子类调用方法如何两个父类都有,则必须指明使用的是哪个父类
多继承会产生钻石型继承体现,为了使得祖先类只有一份,请在两个父类继承祖先的时候采用虚继承(而这在设计祖先类的时候一般是无法预料到的)
可以通过public继承方式继承接口,private继承方式继承实现,来完成目的

条款44: 说你想说的;理解你所说的

条款45: 弄清C++在幕后为你所写、所调用的函数

条款46: 宁可编译和链接时出错,也不要运行时出错

条款47: 确保非局部静态对象在使用前被初始化

条款48: 重视编译器警告

条款49: 熟悉标准库

条款50: 提高对C++的认识

 

参考:  http://www.cnblogs.com/liuchen/archive/2009/10/21/1587278.html

           《Effective c++》

无觅相关文章插件,快速提升流量