现在的位置: 首页 > 综合 > 正文

Effective C++ (一)

2012年08月28日 ⁄ 综合 ⁄ 共 5129字 ⁄ 字号 评论关闭

 

条款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: 划分全局名字空间

 

抱歉!评论已关闭.