一、从c转向c++
1.尽量用const和inline而不用#define
尽量用编译器而不用预处理
const定义常量,当编译出错时能在提示信息中看到符号名
inline定义内联函数,避免类似#define max(a,b) ((a)>(b)?(a):(b)) 在调用max(++a,b)时出现的不确定性
2.尽量用<iostream>而不用<stdio.h>
类型安全,扩展性,避免变量和控制读写格式信息分开
#include<iostream>得到置于std下的库元素;#include<iostream.h>得到置于全局空间的同样的元素,可能导致命名冲突
3.尽量用new和delete而不用malloc和free
区别在于会不会调用构造和析构函数。
4.尽量使用c++风格的注释//
/* */不允许嵌套
二、内存管理
5.new和delete要采用相同的形式
new[]对应delete[]。使用typedef定义数组类型别名,当用delete去释放事实上为new[]的new分配的内存时内存泄漏
6.析构函数里对指针成员调用delete
类的每个指针成员要么指向有效内存,要么指向空,构造函数必须确保这一点,析构函数才能直接delete掉。
7.预先准备好内存不够的情况
使用set_new_handler处理new失败情况
8.写operator new 和operator delete 时要遵循常规
分配程序支持new-handler并正确地处理了零内存请求;释放程序处理空指针。
9.避免隐藏标准形式的new
类里定义了一个称为“operator new”的函数后,会阻止对标准new的访问。
一个办法是在类里写一个支持标准 new 调用方式的operator new;另一个方法为每一个增加到 operator new 的参数提供缺省值。
10.如果写了operator new 就要同时写operator delete
11.为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符
未定义拷贝构造函数的以处理赋值操作符的方式进行处理(对指针成员进行逐位复制),会导致指针混乱内存泄漏。
12.尽量使用初始化而不要在构造函数里赋值
一步动作替代两步动作;const成员不能被赋值。大量固定类型数据成员的情况可能例外。
推广之,尽量使用一步初始化替代初始化后再赋值。
13.初始化列表中的成员列出的顺序和它们在类中声明的顺序相同
因为要保证析构函数被调用的顺序与构造函数相反。静态成员例外;基类总是在派生类之前被初始化,跟继承顺序一致。
14.确保基类有虚析构函数
原因:通过基类指针删除派生类对象,如果基类没有虚析构函数,结果不可确定。
虚析构函数工作方式:最底层的派生类的析构函数最先被调用,然后各个基类的析构函数。
技巧:使用纯虚析构函数可以使类成为抽象类(无法被实例化),但仍需提供纯虚析构函数的定义,考虑定义为内联函数。
15.让operator=返回*this的引用
兼容固定类型的常规做法“(a=b)=c ”。定义:String& String::operator=(const String& rhs){ return *this; }
对于没有声明相应参数为const的函数来说,传递一个const是非法的。
16.在operator=中对所有数据成员赋值
为类增加数据成员时容易遗漏。
对基类调用operator=,使用“static_cast<Base&>(*this)=rhs;”,因为当operator=为编译器生成时“Base::operator=(rhs)”写法无法通过编译)
拷贝构造函数中有同样的问题,使用“Derived(const Derived& rhs):Base(rhs),data(rhs.data){}”
17.在operator=中检查给自己赋值的情况
当类有指针成员时尤其注意,否则导致指针混乱。 C& C::operator=(const C& rhs){ if(this==&rhs) return *this; ... }
三、类和函数:设计与声明
18.争取使类的接口完整并且最小
19.分清成员函数,非成员函数和友元函数
虚函数必须是成员函数;若函数需对左边的参数进行类型转换,则使用非成员函数;需要访问非公有成员的非成员函数只能是类的友员函数,尽量避免友员。
20.避免public接口出现数据成员
接口一致性,全是函数;读写控制;具备灵活性。
21.尽可能使用const
编译器实施只读约束。
const MyClass* p; 或 int const* p; //常量的指针
MyClass* const p;//常指针
const MyClass* const p;//常量的常指针
const MyClass& c;//常量的引用
const_cast<MyClass*>(p);//将p强制转换为常量的指针
const成员函数在声明后面加const,表示const对象的成员函数版本。
const成员函数不能修改对象中的任何一个比特,但能修改指针指向的数据。若要修改,可以使用自定义的非const版this指针。
22.尽量用“传引用”而不是“传值”
引用是通过指针来实现的,本质上一样。
避免多次调用拷贝构造函数和析构函数;避免切割问题(丧失多态特性)。
23.必须返回一个对象时不要试图返回一个引用
当需要在返回引用和返回对象间做决定时,选择完成正确功能的那个。
24.在函数重载和设定参数缺省值间慎重选择
如果可以选择一个合适的缺省值并且只是用到一种算法,则使用缺省参数。
在重载函数中调用一个为重载函数完成某些功能的公共的底层函数。
25.避免对指针和数字类型重载
因为传入NULL或0时不会使用指针版本,编译器也不认为这种调用具有多义性。
26.当心潜在的二义性
c++允许有潜在的二义性,调用到时才会编译出错。
单参数构造函数与类型转换运算符重载可能出现二义性;
调用重载函数时隐式类型转换可能出现二义性,可通过显式转换解决;
多重继承基类成员函数同名(无论是否公有),派生类中显式调用解决。
27.如果不想使用隐式生成的函数就要显式地禁止它
把函数定义为私有的而且不去实现它,当成员函数或友元函数想去调时可以让程序在链接时出错。
适用与条款45中的所有自动生成的函数。
28.划分全局命名空间
用命名空间替代命名前缀。
四、类和函数:实现
29.避免返回内部数据的句柄
防止调用者通过句柄直接修改内部数据
30.避免返回指向成员的非const指针或引用,但成员的访问级比这个函数低
这样等于提升了内部成员的访问级别。可以通过返回const指针或引用解决。
31.千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针或引用
返回局部对象的句柄在函数返回时局部对象被销毁导致句柄失效;返回new出来的对象极可能导致内存泄漏。此时应该返回对象。
32.尽可能地推迟变量的定义
不同于c,没必要在函数开始处声明变量,在用时声明有助理解。
尽量推迟变量的声明到可以为它提供一个初始化参数为止,可以避免不必要的初始化或析构开销。
33.明智地使用内联
编译器可能拒绝内联复杂的函数,编译时会发出警告。
在类定义内实现的成员函数自动为内联(隐式内联)。
内联函数的定义在头文件里面,如果编译器当外联处理,则当头文件被不同cpp包含时,链接出现重复定义(部分编译器在这种情况下只编译一份,防止链接出错)。
构造函数和析构函数通常不适合内联,因为构造基类、数据成员的代码会自动加到构造函数中,导致体积变大不适合当内联。
虚函数也不能内联,因为需要动态绑定。
调试版的不会使用内联,当使用内联时无法进入函数调试。
34.将文件间的编译依赖性降至最低
区别声明、定义、实现,
尽可能使用类的声明,而不使用类的定义。类声明“class MyClass;”替代类定义“#include <MyClass.h>”。
可以使用句柄类(代理类)和协议类(接口类)降低编译依赖性。依赖接口,不依赖实现。
五、继承和面向对象设计
35.使公有继承体现“是一个”的含义
D从B公有继承,表示任何可以使用B对象的地方都可以用D对象。
36.区分接口继承和实现继承
纯虚函数控制派生类接口继承;非虚函数实现继承;
如果想提供默认实现,最好利用纯虚函数同时提供实现,强制派生类实现接口,但允许用Base::VFunc之类的调用默认实现。
37.决不要重定义继承而来的非虚函数
38.决不要重新定义继承而来的缺省参数值
虚函数是动态绑定,但缺省参数值是静态绑定(编译期确定,提高运行效率)。
通过基类句柄调用派生类函数将使用基类的缺省参数值,导致混乱。
39.避免“向下转换”继承层次
向下转换指从基类指针转换为派生类指针。
万不得意时,可以用dynamic_cast做安全转换(dynamic_cast通过RTTI检查类型,区别static_cast的编译器静态转换)。
40.通过分层来体现“有一个”或“用...来实现”
分层:较高抽象层次的类依赖较低抽象层次的类(人依赖于脑、胃,可以理解为组合)
继承:具体的类依赖抽象的类
41.区分继承和模版
当对象的类型不影响类中函数的行为时,使用模版来生成这样一组类。
42.明智地使用私有继承
私有继承意味着“用...来实现”,继承实现而忽略接口。
与分层的区别是,派生类能利用基类的保护成员和通过虚函数动态绑定。
43.明智地使用多继承
可能导致二义性,可用显式调用或更改成员函数名解决;
可能导致菱形继承,可用虚基类解决,但虚基类影响效率,而且很难在设计时决定是否使用。
构造函数的调用顺序:虚基类的构造函数、非虚基类、对象成员、派生类的构造函数。
A是B和C的虚基类,D同时继承自B和C,则D的构造函数初始化列表中必须包含对A的初始化,而B和C构造函数中对A的初始化将被忽略。
虚基类应该禁止包含数据,即使用纯接口,这样可以避免D构造函数初始化列表中对A的初始化。
若D继承自B而虚继承自C,则首先仍然初始化A,不过之后C比B先被初始化。
44.说你想说的;理解你所说的
六、杂项
45.弄清c++在幕后为你所写、所调用的函数
编译器可能会自动生成类的一个拷贝构造函数、一个赋值运算符、一个析构函数、一对取址运算符。
如果没定义任何构造函数(包括拷贝构造),编译器将默认生成一个。
拷贝构造和赋值运算符规则是:逐一拷贝(调用拷贝构造函数)非静态数据成员。
包含引用类型的数据成员,c++不会自动生成拷贝构造和赋值运算符。
生成的都是public函数,如果想禁用必须显示声明为private
46.宁可编译和链接时出错,也不要运行时出错
47.确保非局部静态对象在使用前被初始化
由于无法控制不同被编译单元中非局部变量的初始化顺序,故不同非局部静态变量之间的依赖将导致混沌。
使用Singleton模式。
48.重视编译器警告
49.熟悉标准库
为兼容c,支持<stdio.h>、<string.h>之类的c库。
具有c库功能的c++标准库,<cstdio>、<cstring>,c库的std命名空间版本。
c++模板库STL,<iostream>、<string>、<vector>等,处于std命名空间下。
50.提高对c++的认识
任何奇怪的特性都是有原因的,都是经过深思熟虑权衡利弊的结果。