Effective C++ 总结

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

尽量用编译器而不用预处理

常量的代码在编译时报错,就会很令人费解。因为报错的是常量。

代替函数调用的宏会有很多问题。

如:#define max(a,b) ((a) > (b) ? (a) : (b)) 比如里面带有++之类的操作

定义某个类(class)的常量一般也很方便,只有一点点不同。要把常量限制在类中,首先要使它成为类的成员;为了保证常量最多只有一份拷贝,还要把它定义为静态成员。(借用enum可以简单的代替为整型的情况)

可以使用内联的函数模板来代替宏函数。

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

scanf和printf等不是类型安全的。

没有类型扩展性。

读写对象时的语法形式相同。(即使里面有类,只要重载<<和>>操作符即可,但重载<<和>>时,不能做为成员函数。如果是成员函数的话,调用它们时就必须把string对象放在它们的左边。所以做成全局的,声明为友元函数。)

缺点:iostream效率可能稍低。移植性稍差(因为不同的厂商遵循标准的程度也不同)。iostream库的类有构造函数而<stdio.h>里的函数没有,在某些涉及到静态对象初始化顺序的时候,如果可以确认不会带来隐患,用标准C库会更简单实用。

如果使用了#include <iostream>, 得到的是置于名字空间std下的iostream库的元素;如果使用#include <iostream.h>,得到的是置于全局空间的同样的元素。

class XXX{

friend ostream& operator<<(ostream& s, const Rational& );

};

ostream& operator<<(ostream& s, const Rational& r) {}

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

string *stringarray1 =

static_cast<string*>(malloc(10 * sizeof(string)));

string *stringarray2 = new string[10];

stringarray1 只申请了可以容纳10个string对象的空间,但是在内存里并没有创建这些对象。

而stringarray2也创建了这些对象。

而且用free释放stringarray1的时候,并不会调用string类的析构函数。

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

c风格的不能嵌套注释

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

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

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

即当operator new不能满足请求时,会在抛出异常之前调用客户指定的一个出错处理函数——一般称为new-handler函数。

指定出错处理函数时要用到set_new_handler函数,它在头文件<new>里大致是象下面这样定义的:

typedef void (*new_handler)();

new_handler set_new_handler(new_handler p) throw();

一个设计得好的new-handler函数必须实现下面功能中的一种:

 ·产生更多的可用内存。

 ·安装另一个不同的new-handler函数。

 ·卸除new-handler。(没有安装new-handler,operator new分配内存不成功时就会抛出一个标准的std::bad_alloc类型的异常。)

 ·抛出std::bad_alloc或从std::bad_alloc继承的其他类型的异常。

 ·没有返回。

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

要有正确的返回值;可用内存不够时要调用出错处理函数;处理好0字节内存请求的情况。此外,还要避免不小心隐藏了标准形式的new

c++标准要求,即使在请求分配0字节内存时,operator new也要返回一个合法指针。(处理0字节请求时,把它当作1个字节请求来处理)

父类的operator new经常会被子类继承。这会导致某些复杂性。因为大多数针对类所写的operator new都是只为特定的类设计的,它申请的空间大小就是sizeof(class)的大小,子类的大小一般比父类大

如果想控制基于类的数组的内存分配,必须实现operator new的数组形式——operator new[]。

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

内部范围声明的名称会隐藏掉外部范围的相同的名称.

在类里定义了一个称为“operator new”的函数后,会不经意地阻止了对标准new的访问。

一个解决办法是在类里写一个支持标准new调用方式的operator new,它和标准new做同样的事。

class x{

  static void * operator new(size_t size)

  { return ::operator new(size); }

};

x* px = new x;  // 调用 x::operator new(size_t)

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

缺省的operator new和operator delete具有非常好的通用性,通用性某些情况下导致了效率下降。重写可提高效率,尤其在那些需要动态分配大量的但很小的对象的应用程序里,情况更是如此。

可以在重写的operator new里面申请大块内存,当作内存池来用,以及进行其它的操作。 但是,重写new操作,就要重写delete操作,并且要使new和delete的内存块大小一致。可以通过在类对象里面使用成员变量来保存new的内存块的大小,在delete里面使用。

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

如果没有定义operator=操作符,则c++会生成并调用一个缺省的operator=操作符。这个操作符仅仅是完成了浅复制,即按位复制。并不会复制指针所指向的内存。

如果没有定义赋值构造函数,C++也会生成一个缺省的,并进行浅复制。会导致相同的问题。

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

比如const成员,必须在初始化列表里面初始化。

类对象,如果写到构造函数里赋值的话,开始会调用类对象的构造函数来初始化,然后再进入构造函数体,进行赋值,效率会比较低。

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

初始化列表里面初始化的顺序和声明的顺序一致!!与在初始化列表里面所写的顺序无关。

如果与在初始化列表里面的顺序一致的话,当有两个构造函数的时候,如果初始化顺序不一样,则类要负责跟踪每一个对象成员初始化顺序!以便能正确的析构。这样增加了类的大小及复杂性。如果和声明顺序一致,则可以避免这种问题。

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

如果基类没有虚析构函数:

用父类指针指向子类对象的时候,用delete不会调用子类的非虚析构函数。

如果确认类不会被继承,则也不要将析构函数声明为虚函数。(因为含有虚指针,可能会增加类的大小)

很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。

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

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

当为派生类赋值运算符重载时,要记得将父类的成员变量也都初始化。

当父类的成员变量为私有的时候:

base::operator=(rhs);    // 调用this->base::operator=

static_cast<base&>(*this) = rhs; //为了适应某些编译器不能使用上面这行的方法。

//但如果将*this强制转为base(而不是base的引用)的话,会调用base的构造函数产生一个临时对象,然后临时对象作为赋值的目标了。

在拷贝构造函数中也有相同的问题,也要将父类的成员初始化。

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

原因:一是为了效率,在碰到给自己赋值的情况就立刻返回,可以节省大量的工作。

二是保证正确性,一个赋值运算符必须首先释放掉本身所占有的资源(去掉旧值),然后根据新值分配新的资源。如果已经被释放了,再用它本身给自己赋值,就会出错。

可以通过比较两个对象的值是否相同,如果相同就直接返回。

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

第一,接口中函数越多,以后的潜在用户就越难理解。

第二个缺点是难以维护

类的定义太长会导致项目开发过程中浪费大量的编译时间。

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

将运算符重载为类成员函数时参数个数=原操作数个数-1 (后置++、--除外) (操作符声明为成员函数和外部友元函数的时候还是有一些区别的。 如果该运算符函数没有访问该类的私有变量,则也可以不是友元)

将构造函数声明为explicit,可以防止隐式的转换。

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

一致性:因为对外公开的只有函数,所以不用费脑筋想是否调用成员变量等。

可以实现精确控制:即可以通过函数提供接口,来达到控制成员变量是只读、只写?还是可读写。如果使数据成员为public,每个人都可以对它读写;

功能分离: 如某个成员函数,作用是返回一组数的平均值。这个平均值存储在a中。到后面,我们完全可以不用返回a,而是直接用数据计算出一个值来返回。

条款21: 尽可能使用const

在头脑里画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,指针指向的数据为常量;如果const出现在线的右边,指针本身为常量;如果const在线的两边都出现,二者都是常量。

使用const的好处在于它允许指定一种语意上的约束——某种对象不能被修改,如果某个值要保持不变,就要明确地使用const,这样做就可以借助编译器的帮助确保这种约束不被破坏。

让函数返回一个常量值经常可以在不降低安全性和效率的情况下减少用户出错的几率。 如:operator*的返回结果是一个const对象,否则可能出现 (a*b)=c;的情况。

仅在const方面有不同的成员函数可以重载。 const对象调用成员函数的时候,优先调用const成员函数。

当对非静态数据成员运用mutable时,这些成员的“bitwise constness”限制就被解除,它们可以在任何地方被修改,即使是在const成员函数里面。

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

传值一般会造成昂贵开销。 因为它会调用拷贝构造函数等。如果对象里面还有对象,则还会调用这些对象的构造以及析构函数……

当一个派生类的对象作为基类对象被传递时,它(派生类对象)的作为派生类所具有的行为特性会被“切割”掉,从而变成了一个简单的基类对象。(切割问题),而且行为也都是基类的。

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

当传递局部变量的时候,要返回一个对象,而不要返回引用。因为有可能引用到一个“不存在”(离开函数时被销毁)的对象。

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

会对函数重载和设定参数缺省值产生混淆的原因在于,它们都允许一个函数以多种方式被调用。

第一,确实有那么一个值可以作为缺省吗?第二,要用到多少种算法?一般来说,如果可以选择一个合适的缺省值并且只是用到一种算法,就使用缺省参数。否则,就使用函数重载。

如:求几个数的平均值,就只能用函数重载。

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

如:void f(int x);

void f(string *ps);

f(0); //此时只会调用 void f(int x)

若想调用第二个,必须利用类成员函数模板。

template<class t>                       // 为所有类型的t

operator t*() const { return 0; }     // 产生operator t*

上面这段代码可以为任意类型生成一个NULL指针。

所以,尽量避免对指针和数字类型重载。

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

如:

void f(int);

void f(char);

double d = 6.02;

f(d); //错误,会产生二义性

(上面的问题可以通过显示类型转换来解决,如:f(static_cast<int>(d); )

还有其它很多情况都会造成二义性。如多继承时,两个基类分别具体相同名称的成员函数,此时必须要通过类域名访问。等等……

条款27: 如果不想使用隐式生成的函数就要显式地禁止它

C++会隐式生成一些成员函数。某些时候可能会因此而产生错误,如若不使用,最好显示的将其声明为private的。(不用定义)

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

 

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

尽量不要返回类成员变量的地址或引用等句柄,以防止被外部轻易的改变。

而且很容易出问题。如返回了一个类内部成员变量指针指向某个缓冲区,在外部调用delete等操作这个缓冲区,势必造成类内部成员变量不能再有效的使用了。

也不要返回了一个局部对象,局部对象出了作用域之后就被销毁。

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

被返回不仅仅可以是数据成员,还要考虑到成员函数。因为返回一个成员函数的指针也是有可能的,而这个成员函数为私有的时候,通过返回它的指针,外部就可以调用它。

如果你想提高效率而返回成员的指针或引用,那么请加上const。

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

返回局部对象的引用的话,局部对象有可能随时被销毁。

也不要返回函数内部用new初始化的指针的引用,以防止在外部,用户忘记释放这个空间。

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

因为在某些时候,还不等不到执行某个变量的定义,函数就返回了。

这样,为了提高效率,在使用对象的地方再定义它。

如:

fun()

{

  somecode;

  if (bVal) return;

  Object a(10);

}

如果bVal为true,则可以避免调用a的构造函数来对a进行初始化,可以提高效率。

条款33: 明智地使用内联

内联会导致代码体积膨胀。

虚函数不能为内联。

函数中有循环或递归时,不能用内联。

构造函数和析构函数不适合内联。因为它们里面常常含有很多隐式的代码。比如说,类成员变量A a; 则会在构造函数里面调用它的默认构造函数进行初始化。如果A里面还有其它

对象,还会进一步的调用,实际上它的代码体积很大。

如果函数中包含静态对象,通常要避免将它声明为内联函数。

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

很多情况下,改变某个类的定义,会导致很多文件重新被编译。将其定义与声明分离,可能可以解决这种情况。

分离的关键在于,"对类定义的依赖" 被 "对类声明的依赖" 取代了。

只要有可能,尽量让头文件不要依赖于别的文件;如果不可能,就借助于类的声明,不要依靠类的定义。

下面就是这一思想直接深化后的含义:

· 如果可以使用对象的引用和指针,就要避免使用对象本身。定义某个类型的引用和指针只会涉及到这个类型的声明。定义此类型的对象则需要类型定义的参与。

· 尽可能使用类的声明,而不使用类的定义。因为在声明一个函数时,如果用到某个类,是绝对不需要这个类的定义的,即使函数是通过传值来传递和返回这个类

如:

  class Date; // 类的声明

  Date returnADate(); // 正确 ---- 不需要Date的定义

  void takeADate(Date d);

这些情况下只需要知道一个类的声明即可,用不着类的定义。

在头文件中尽量不依赖别的文件,而在cpp中包含。

也可以通过指针,来将对象的实现隐藏起来。

如:

  Person *p; //此时就不用知道Person的定义

工厂模式,也有这些思想在里面。

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

公有继承意味着 "是一个" 。当写下类D("Derived" )从类B("Base")公有继承时,你实际上是在告诉编译器(以及读这段代码的人):类型D的每一个对象也是类型B的一个对

象。你是在声明:任何可以使用类型B的对象的地方,类型D的对象也可以使用,因为每个类型D的对象是一个类型B的对象。

但要注意,如企鹅是鸟,可以从鸟里面仅有继承下来,但是企鹅不会飞。所以,好的设计是和软件系统现在和将来所要完成的功能密不可分的。

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

可以为纯虚函数提供实现,但调用它的唯一方式是通过类名完整地指明是哪个调用。

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

因为非虚函数在静态编译的时候就已经确定了,无法实现多态,所以,重写继承来的非虚函数,调用这个非虚函数的时候,只会和指针类型相同。

如: Base *p = new Derive;

p->fun(); //如果fun为非虚的,就会调用 Base::fun(),而不会调用Derive::fun();

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

成员函数有非虚函数和虚函数。根据上个条款可知,非虚函数在派生类中重写一般是错误的。 所以,我们完全可以把讨论的范围缩小为 "继承一个有缺省参数值的虚函数" 的情况。

但是,虚函数是动态绑定而缺省参数值是静态绑定的。

所以,继承而来的缺省参数和父类是相同的,而不受子类指定的默认参数的影响。

如果重写继承而来的缺省参数,这意味着你最终可能调用的是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。

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

向下转换难看、容易导致错误,而且使得代码难于理解、升级和维护

不过有时候向下转换还是很有用处的。如:

A是父类

B B2 是子类

C C2 C3 C是B的子类,C2 C3是B2的子类。

fun (A *pa)

{

  if (dynamic_cast <B *> (pa)) {...}  //如果传给fun的是B或B的子类(即C)的指针,这时候就会返回TRUE,就是有效的。

  else if (dynamic_cast <B2 *> (pa)) {...} //如果fun是B2或B2的子类(即C2 C3)的指针,成功。

}

用typeid运算符则达不到这样的效果,它只能具体到某个类,而不能判断是否为它的子类。

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

使某个类的对象成为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这一过程称为 "分层"(Layering)。

"分层" 这一术语有很多同义词,它也常被称为:构成(composition),包含(containment)或嵌入(embedding)。

公有继承的含义是 "是一个"。对应地,分层的含义是 "有一个" 或 "用...来实现"。

条款41: 区分继承和模板

类型T影响类的行为吗?如果T不影响行为,你可以使用模板。如果T影响行为,你就需要虚函数,从而要使用继承。

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

如果两个类之间的继承关系为私有,编译器一般不会将派生类对象(如Student)转换成基类对象(如Person)。

私有继承意味着只是继承实现,接口会被忽略。而且调用私有继承的父类的函数,只能用父类名称::函数名()的方式来调用。

类的组合也是只使用某个类的实现,但与私有继承是有区别的,不过一般情况下能使用组合就使用组合。

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

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

多继承带来二义性。如两个父类含有相同的成员函数名子的时候。

此时只能通过指定父类名+函数名来调用,但当显式地用一个类名来限制修饰一个虚函数时,函数的行为将不再具有虚拟的特征。

多继承有可能导致钻石结构。

但使用好多继承,避免钻石结构,还是很有用处的。

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

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

如果你没声明如下函数,编译器将会为你自动生成:

一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符。另外,如果你没有声明任何构造函数,它也将为你声明一个缺省构造函数。

注意,生成的析构函数一般是非虚拟的。生成的默认拷贝和赋值构造函数,都是潜拷贝,只按位拷贝。

如果想禁止使用这些函数,显示地将它们声明为private型的。

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

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

如果在某个被编译单元中,一个对象的初始化要依赖于另一个被编译单元中的另一个对象的值,并且这第二个对象本身也需要初始化,但,第一个对象的初始化可能在第二个对象

的初始化之前进行的,这时候就会出问题。

而且,你绝对无法控制不同被编译单元中非局部静态对象的初始化顺序。

使用单件模式(singleton pattern)可以解决这个问题,把每个非局部静态对象转移到函数中,声明它为static。其次,让函数返回这个对象的引用。

原因:C++却明确指出:它们在函数调用过程中初次碰到对象的定义时被初始化。所以,如果你不对非局部静态对象直接访问,而用返回局部静态对象引用的函数调用来代替,就能

保证从函数得到的引用指向的是被初始化了的对象。这样做的另一个好处是,如果这个模拟非局部静态对象的函数从没有被调用,也就永远不会带来对象构造和销毁的开销;而对

于非局部静态对象来说就没有这样的好事。

条款48: 重视编译器警告

只要谈到警告,就要想到警告是和编译器紧密相关的,所以在编程时不要马马虎虎,寄希望于编译器为你找出每一条错误。

条款49: 熟悉标准库

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

C++的设计目标在于:

· 和C的兼容性。

· 效率。

· 和传统开发工具及环境的兼容性。

· 解决真实问题的可应用性。

 

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/ciahi/archive/2009/06/22/4290195.aspx

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值