effective C++总结(续)

本文提供了C++编程的多项最佳实践建议,包括使用const和inline替代宏定义、使用<iostream>替代<stdio.h>、使用new和delete替代malloc和free等。同时,文章还详细介绍了内存管理的最佳实践,如使用new和delete的正确方式、处理内存不足的策略等。

<改变旧有的C习惯>
1.尽量以const和inline取代#define

C语言用#define可以做到两点,第一,定义常量,如#define PI 3.1415926或#define NAME "Jade";第二,定义宏表达式(函数),如#define min(a,b) (a)<(b)?(a):(b)。
但是这样做会有些问题,首先,不易调试,因为这个宏在预编译时就被替换为实际代码了,跟踪器一般都很难跟踪;其次,使用宏表达式,虽然样子像函数,但是由于会被替换为实际代码,行为又和函数不同,容易造成错误。
因此,建议使用const变量和inline函数替换宏的这两个功能。
 
const double PI = 3.1415926;
const char NAME[] = "jade";
inline min(a,b) { if (a < b) return a; else return b; }
 
class A
{
public:
    static const int N;
};
 
const int A::N = 100;
 
2.尽量以<iostream>取代<stdio.h>
Scott Meyers建议我们使用<<和>>代替printf和scanf函数。
 
因为只要一个自定义类型实现了<<操作符(friend ostream& operator<<(ostream &os, const ClassName &r);),就可以使用<<打印自定义类型的内容,这是用printf所无法做到的。

<iostream>是在std名字空间里的函数,而<iostream.h>中的函数是全局函数,没有名字空间

3.尽量以new和delete取代malloc和free
如果使用new获得的指针,就使用delete释放它;如果用malloc申请的空间,就用free释放它,千万不要混用!

4.尽量使用C++风格的注释形式
C语言风格注释不支持嵌套

<内存管理>
5.使用相同形式的new和delete
使用new[]生成对象数组,释放时也要使用delete[],如果使用了delete,则除第一个对象的析构函数被调用外,其他对象的都没有被调用

6.记得在destructor中以delete对付指针成员
有point成员的类中,应做到:
1、在每一个构造函数中将该指针初始化。如果没有任何一个构造函数会将内存配置给该指针,那么指针应该初始化为NULL。
2、在=操作符中将指针原有的内存删除,重新配置一块。
3、在析构函数中delete这个指针(delete一个NULL指针是安全的)。
 
7.为内存不足的状况预作准备
当C++使用new分配内存出现内存不足的情况时,会抛出std::bad_alloc异常(不使用new(nothrow)形式,返回NULL),应该捕捉这个异常进行处理。另外还可以设置一个分配内存失败时的回调函数,在头文件<new>中提供了一个函数set_new_handler用来注册这个回调函数,原型如下:
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
参数为注册的回调函数,返回为原来的回调函数。
 
如果注册了回调函数,在new抛出bad_alloc异常前,会回调这个函数,如下例所示:
#include <memory>
using namespace std;
 
void func()
{
    set_new_handler(NULL);  //如果不把处理函数设为空,它会被new不停地调用,直到分配内存成功
    cout << "call func()" << endl;
}

int main(int argc, char *argv[])
{
    new_handler f = set_new_handler(func);
 
    try
    {
        char *p = new char[0x7fffffff];
    }
    catch(bad_alloc&)
    {
        set_new_handler(f);
        cout << "get exception" << endl;
    }
    getchar();
 
    return 0;
}
 
会打印出:
call func()
get exception
 
Scott Meyers并对set_new_handler函数行为作出了自己的建议:
1、让更多的内存可用,在一开始分配出一块大的内存,当发生内存不足时,释放此内存。
2、安装不同的new_handler,改变new_handler的行为。
3、卸载new_handler,即再次调用set_new_handler指针p传NULL,这样就不会每次发生内存不足重复调用new_handler了,它会抛出std::bad_alloc异常。
4、抛出自己的异常,可以抛出std::bad_alloc或它的派生类。
5、不返回,直接调用abort或exit函数。
 
也可以使每个类拥有自己的new_handler,这需要重载new操作符。因为这个new操作符和new_handler的行为都是相同的,所以可以定义为一个模板,让其他类继承它,得到这些接口。
 
template<class T>
class NewHandlerSupport
{
public:
    static new_handler set_new_handler(new_handler p);
    static void *operator new(size_t size);
private:
    static new_handler currentHandler;
};
 
template<class T>
new_handler NewHandlerSupport<T>::set_new_handler(new_handler p)
{
    new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}
 
template<class T>
void* NewHandlerSupport<T>::operator new(size_t size)
{
   
    new_handler globalHandler = std::set_new_handler(currentHandler);
 
    void *memory;
    try
    {
        memory = ::operator new(size);
    }
    catch (std::bad_alloc&)
    {
        std::set_new_handler(globalHandler);
        throw;
    }
 
    std::set_new_handler(globalHandler);
    return memory;
}
 
template<class T>
new_handler NewHandlerSupport<T>::currentHandler;
 
使用举例:
void noMoreMemory();
X::set_new_handler(noMoreMemory);
X *p = new X[N];     //内存不足
 
其实这样使用起来也挺麻烦,可以直接在类内定义自己的new_handler方法(作为一个抽象方法),不用set_new_hander直接使用,就更加方便了。
 
8.撰写new和delete操作符时应遵守公约
自己编写的new操作符的行为应与缺省的new操作符保持一致,就像上一个主题讲的那些new的特征:
1、当分配内存成功时,返回内存的地址;分配失败,如果注册了处理函数,则不停地调用此函数,直到分配内存成功或处理函数中抛出异常或改变回调函数,如果没有注册处理函数,则抛出bad_alloc异常
2、如果用户要求分配0字节内存,就给他分配1字节并返回此地址
3、没有3了吧?
 
Scott Meyers提供的伪代码说明:
void *operator new(size_t size )
{
    if (size == 0)
        size = 1;
 
    while (true)
    {
        尝试分配size个字节的内存;
        if (分配成功)
        {
            return 内存的地址;
        }
 
        //由于不能访问当前配置的handler,所以使用这种方法获得
        new_handler globalHandler = set_new_handler(0);
        set_new_handler(globalHandler);
 
        if (globalHandler)
            (*globalHandler)();
        else
            throw std::bad_alloc();
    }
}
 
很值得关注的一个问题,基类的new操作符会被它的子类继承!如果子类自己不实现new操作符,将会调用基类的new操作符,而不是缺省的!Scott Meyers的建议是在基类的new中检查分配内存大小是否等于sizeof(基类),如果不等于则调用缺省的new。
 
delete也和new一样要符合标准,但相对简单,只要保证delete一个NULL指针是安全的就可以了。
void operator delete(void *p)
{
    if (p == 0)
        return;
 
    释放p指向的内存;
 
    return;
}
 
void Base::operator delete(void *p, size_t size)
{
    ...
    if (size != sizeof(Base))
    {
        ::operator delete(p);
        return;
    }
    ...
}
 
另外要提一句,在调完自定义的new后,C++会自动去调构造函数;delete时则是先调析构函数,再调自定义的delete。
 
最后还有个有意思的,就是当函数有包含虚函数时,传给new的size会自动增加4字节的虚指针,而虚指针的初始化呢?会在构造函数中自动完成,这样用户重载自己的new操作符就完全不用关心虚表了,想得很周到。
 
9.避免遮掩了new的正规形式
new()除了有一个size_t的参数外还可以添加自己的参数,在调用new时指出,new (值) A;
 
如果你定义了一个自定义形式的new(不同于缺省new的参数),而没有定义缺省new形式,将导致调用像new A这样的语句失败。 所以应该再定义一个缺省new形式,或给新添参数加一个缺省值。
 
10.如果你写了一个new操作符,那对应也写一个delete操作符
通常编写自己的new操作符,是为了分配小内存。首先,申请一块大内存,然后,每次调new时,从中分配一块空闲的小内存,返回其地址。
 
不要用缺省的delete释放自定义的new分配的内存,其实如果你知道自己的new做了什么,就绝对不会这么做了。
 
Scott Meyers的意思是这样地:
class Pool
{
public:
    Pool(size_t n);
    ~Pool();
 
    void* alloc(size_t n);
    void free(void *p, size_t n);
 
    ...
};
 
class A
{
public:
    static void* operator new(size_t size)
    {
        return memPool.alloc(size);
    }
 
    static void operator delete(void *p, size_t size)
    {
        memPool.free(p, size);
    }
 
privat:
    static Pool memPool;
}
 
Pool A::memPool(sizeof(A) * N);
 
但Pool怎样工作,Scott Meyers的意思是让它的设计者去操心吧,其实就是推给读者了。。。
其实关于这个问题在《Modern C++ design》里有更详细的方案描述。
 
<构造函数、析构函数和赋值(=)运算符>
11.如果class内有动态分配内存的成员变量,则为此类声明一个copy构造函数和一个赋值运算符
默认的赋值运算符和拷贝构造函数是按位拷贝,它会拷贝指针的值,这很可怕!
问题那就多了:首先,a = b可能会造成原来a的内存泄漏;A a = b,如果b的内存释放将导致a的内存失效;其次,使用值传递时,临时变量退出时被析构导致指针指向内存失效。
 
实现拷贝构造函数和赋值运算符:
1、申请新内存,进行拷贝
2、使用引用计数,记录内存被引用次数,引用次数为0才释放。
 
如果确实不会使用到这两个函数时呢?请看第27条。
 
12.在构造函数中尽量以初始化动作取代赋值动作
首先,构造函数中使用赋值实际上是两步动作,第一,调用变量的默认构造,第二,调用赋值;而初始化呢?只有一步就是调用变量对应的构造函数,如拷贝构造。
其次,const成员变量和引用成员变量只能够被初始化,不能够被赋值。
 
另外,静态成员变量的初始化见第47条。
 
13.初始化链表中的成员初始化次序应该和其在class内的声明次序相同
类成员变量是以它们在类中的声明次序来初始化,与它们在成员初始化列表中出现的次序无关。
继承时,基类于类成员变量之前初始化;多重继承时,基类的初始化顺序和继承的顺序一致。亦与初始化列表中的次序无关。
 
一个类的析构过程是完全和它的构造过程相反的。
 
综上所述:
1、初始化链表中的成员初始化次序应该和其在class内的声明次序相同
2、成员变量初始化列表起始处列出基类的初值设定式
 
14.总是让基类拥有virtual析构函数
为了防止在使用多态时,用基类指针指向派生类成员,而delete此指针时,调用的是基类的析构函数,而没有调用派生类的,所以应该把基类的析构函数声明为virtual函数。
 
这四个的笔记都没保存上,丢了,懒得现在补了,以后再说吧!!!
 
15.令赋值操作符函数传回*this的引用
 
16.在赋值操作符函数中为所有的成员变量赋值
 
17.在赋值操作符函数中检查是否自己赋值给自己
 
<类与函数之设计和声明>
18.努力让接口全面和最小化
一个全面的接口,即对于用户可能希望完成的任何合理工作,类都应该提供一个合理的方法来完成。
一个最小化的接口,即尽可能让函数个数最少,不至于有任何两个成员函数功能重叠。
 
 
19.区分成员方法,非成员方法和友元方法三者
往往一个成员方法,变一个思路,通过提供一些基本的对外接口,也可以通过一个非成员方法实现;而如果将一个非成员方法设为友元,也可以不提供对外接口,而通过一个非成员友元方法实现。
 
那应该如何选择这几种方法呢?
1、虚函数必须是类成员函数
2、如果一个操作的左右两边参数可以互换,并且需要进行隐式转换,则要使用非类成员函数
例如,一个分数类Rational的乘法operator*,既会有r*2这样的使用,也会有2*r这样的使用,而2.operateor*(r),这样的接口是没有的,而且作为左值又不能进行转换。
3、一些惯用操作,如operator<<和operator>>应为非类成员函数
4、其他情况应该设计为类成员函数
 
构造函数、类型转换操作符,explicit
有类型转换符,不调用explict构造函数,是否可以进行隐式转换?
 
20.避免将成员变量放在公开接口中
 
尽可能使用const
 
尽可能使用传引用,少用传值
 
当你必须返回一个对象时,不要尝试返回引用
 
在函数重载和缺省参数之间谨慎抉择
 
避免对指针类型和数值类型进行重载
 
防范潜在的模糊状态
 
如果不想使用编译器自动生成的成员函数,就应该明确拒绝它
 
尝试切割全局命名空间
 
<类与函数之实现>
避免返回内部数据的handles
 
避免写出一个成员函数,返回一个指向较低存取层级(less accessible then themself)成员的非const指针或引用
 
千万不要传回函数内local对象的引用或函数内以new获得的指针所指的对象
 
尽量延缓变量定义
 
明智地运用内联
 
将文件之间的编译依赖关系降到最低
 
<继承关系与面向对象设计>
确定你的公开继承,使它成为is-a的模型
 
区分接口继承和实现继承
 
绝对不要重新定义继承而来的非虚拟函数
 
绝对不要重新定义继承而来的缺省参数值
 
避免在继承体系中做向下转型动作
 
通过分层技术来建模has-a或is-implemented-in-terms-of的关系
 
区分继承和模板
模板应该用来产生一群类,其中对象类型不会影响类的函数行为
继承应该用于一群类身上,其中对象类型会影响类的函数行为
 
明智地运用私有继承
 
 
明智地运用多继承
 
说出你的意思并了解你所说的每一句话
 
<杂项讨论>
清楚知道C++编译器默默为我们完成和调用哪些函数
 
宁可编译和连接出错,也不要执行时才错
 
使用非local静态对象之前先确定它已有初值
 
不要对编译器的警告信息视而不见
 
尽量让自己熟悉C++标准程序库
 
加强自己对C++的了解
 
<基础议题>
仔细区别指针和引用
 
最好使用C++转型操作符
 
绝对不要以多态方式处理数组
 
非必要不提供默认构造函数
 
<操作符>
对定制的类型转换函数保持警觉
 
区别++/--操作符的前置和后置形式
 
千万不要重载&&、||和,操作符
 
了解各种不同意义的new和delete
 
<异常>
利用析构函数避免泄漏资源
 
在构造函数内阻止资源泄漏
 
禁止异常流出析构函数之外
 
了解抛出一个异常与传递一个参数或调用一个虚函数之间的差异
 
以按引用方式捕捉异常
 
明智地运用异常specifications
 
了解异常处理的成本
 
<效率>
谨记80-20法则
 
考虑使用缓式评估(lazy evaluation)
 
分期摊还预期的计算成本
 
了解临时对象的来源
 
协助完成返回值优化
 
利用重载技术避免隐式类型转换
 
考虑以操作符复合形式(op=)取代其独身形式(op)
 
考虑使用其他程序库
 
了解虚函数、多重继承、虚基类、RTTI(运行时类型识别)的成本
 
<技术>
将构造函数和非成员函数虚化
 
限制某个类所能产生的对象数量
 
要求或禁止对象产生于堆中
 
智能指针(smart pointer)
 
引用计数
 
代理类(proxy classes)
 
让函数根据一个以上的对象类型来决定如何虚化
 
<杂项讨论>
在未来时态下编程
 
将非叶子类(non-leaf classes)设计为抽象类
 
如何在同一个程序中结合C++和C
 
让自己习惯于标准C++语言
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值