第一章:从c转向c++
条款1:尽量使用const、inline代替define——尽量使用编译器而不是预处理器
define是预处理阶段的宏替换,使用宏替换不便于调试,因为宏替换所使用的别名并不会在编译器生成的符号列表中。如果要定义常量,应该使用const,对于常字符串,则应该使用const char * const,当const只在类中使用时,为了只存在一份拷贝,应该定义为static如:
const char * const strdlversion = "1.0.0.1"
const double PI = 3.14.5926
class ci{
...
static const int MAX_FILE_PATH_LENGTH = 256;
};
另外还可以使用模板来取代宏以实现多种类型的某一功能,例如:
#define swap(a,b) do \
{\
a = a + b;\
b = a - b;\
a = a - b;\
} while (0);
template<typename U,typename V>
inline swap(U &u, V &v) {
U tmp = (U)v;
v = (V)u;
u = (U)tmp;
}
条款3:尽量使用new和delete,而不是malloc和free
malloc和free(及其变体)会产生问题的原因在于它们太简单:他们不知道构造函数和析构函数。malloc分配的内存不会被初始化,free时仅仅释放空间。而且malloc不能用来为拥有动态内存的类分配空间。new/delete和malloc/free的不兼容性常常会导致一些严重的复杂性问题。举个例子,
第三章 构造函数,析构函数和赋值操作符
构造函数:控制对象生成时的基本操作,并保证对象被初始化
析构函数:摧毁一个对象并保证它被彻底清除
赋值操作:符则给对象一个新的值
条款11: 为需要动态分配内存的类声明一个拷贝构造函数和一个赋值操作符——只要类里有指针时,就要写自己版本的拷贝构造函数和赋值操作符函数
如果一个类不定义自己的拷贝构造函数和赋值操作符,则编译器为这个类生成一个缺省的拷贝构造与赋值操作符,他们只是对类的中的各个成员进行逐位拷贝。
如果这个类中有动态分配的内存可能存在2个问题:
已分配的内存在赋值时不能被正确释放掉
在赋值操作时可能存在没有为需要的内存分配空间,导致多个对象使用同一内存
// 一个很简单的string类
class string {
public:
string(const char *value);
~string();
... // 没有拷贝构造函数和operator=
private:
char *data;
};
string::string(const char *value)
{
if (value) {
data = new char[strlen(value) + 1];
strcpy(data, value);
}
else {
data = new char[1];
*data = '\0';
}
}
inline string::~string() { delete [] data; }
string a("hello");//OK
string b("world");//OK
b = a;//b曾指向的内存永远不会被删除,因而会永远丢失。赋值后a和b包含的指针指向同一个字符串,那么只要其中一个离开了它的生存空间,其析构函数就会删除掉另一个指针还指向的那块内存。
以上问题只需要为string类提供自定义的拷贝构造和赋值操作符即可:
class string{
....
string & operator=(cosnt string &rhs);
};
string & string::operator=(cosnt string &rhs) {
if (this == &rhs)
{
return *this;
}
delete[] data;
data = new char[strlen(rhs.data) + 1];
strcpy(data, rhs.data);
}
条款12: 尽量使用初始化而不要在构造函数里赋值
1.基类必须在初始化列表中初始化,所以使用继承时,要把基类的初始化列在成员初始化列表的最前面
2.初始化列表中初始化比在构造函数中赋值要快
3.const成员变量和引用成员变量必须在初始化列表中初始化
4.static成员变量不要在构造函数中初始化,因为这样没有意义,它在一个类中只会被初始化一次,也只有一份实例
条款13: 初始化列表中成员列出的顺序和它们在类中声明的顺序相同
在初始化列表中,最先初始化的并不是写在最前面的,而是最先声明的,因此初始化列表的顺序必须保证和声明的顺序一致,否则如果成员之间存在依赖关系,很肯能导致未知的错误
条款14: 确定基类有虚析构函数——为所有的基类都提供虚析构函数
1.当通过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可确定的。
2.当一个类不打算作为基类被继承时,一般不要将其析构函数声明为虚函数,因为虚函数需要占用额外的空间(vptr虚函数指针占用额外4个字节,vptr指向的是一个称为vtbl(虚函数表)的函数指针数组)
3.如果要定义一个抽象类,但是没有其他的纯虚函数,则最好的办法是将析构函数生命为纯虚函数
4.构造函数调用时,先构造基类再构造子类,析构时,先析构基类再析构子类
条款15: 让operator=返回*this的引用
在一个类c中,缺省版本的operator=函数具有如下形式:
c& c::operator=(const c&);
1.c++程序员经常犯的一个错误是让operator=返回void,这好象没什么不合理的,但它妨碍了连续(链式)赋值操作,所以不要这样做。
2.除非必须,则不要让operator=返回一个const对象的引用
条款16: 在operator=中对所有数据成员赋值
条款17: 在operator=中检查给自己赋值的情况
1.防止自赋值,比如在string类中,在赋值前需要先将以前的数据清除,如果发生自赋值,则会导致再次赋值时出现内容为空的情况。
2.通过提供operator == ,然后使用 *this == rhs来判断
第四章 类和函数:设计与声明
条款18: 争取使类的接口完整并且最小——典型的接口里只有函数存在
1.能用const就尽量用const——比如对于const常量,返回const的引用
条款19: 分清成员函数,非成员函数和友元函数
1.成员函数和非成员函数最大的区别在于成员函数可以是虚拟的而非成员函数不行。所以,如果有个函数必须进行动态绑定(见条款38),就要采用虚拟函数,而虚拟函数必定是某个类的成员函数。
2.只要能避免使用友元函数就要避免
3.需要访问类的非公有成员的情况,需要将一个类声明为另一个类的友元
4.对于输入输出,为了链式操作,声明为友元函数
5.如果函数声明为全局函数可以满足需求,则生命为全局函数而不是友元函数
条款21: 尽可能使用const
1.const成员函数的目的当然是为了指明哪个成员函数可以在const对象上被调用——const函数不修改对象的任何属性
2.一般来说,你可以在头脑里画一条垂直线穿过指针声明中的星号(*)位置,如果const出现在线的左边,指针指向的数据为常量;如果const出现在线的右边,指针本身为常量;如果const在线的两边都出现,二者都是常量。
3.const的对象不能调用非const成员函数,非const对象能调用const成员函数
条款22: 尽量用“传引用”而不用“传值”
1.如果在函数调用过程中,将子类对象以基类值得形式传递,子类的星系将被全部丢弃,最终得到的只是一个纯粹的几类对象,在调用该类的函数时将调用基类的函数,解决办法是通过传递引用的方式
2.传递引用时尽量使用const
条款23: 必须返回一个对象时不要试图返回一个引用——operator=除外
1.返回对象的引用时这个对象可能是一个临时变量,在离开作用域时已经被析构
2.即使返回的是一个堆上的对象,不会造成作用域的问题,也会带来内存泄露的隐患
条款25: 避免对指针和数字类型重载——会造成调用时的歧义
条款27: 如果不想使用隐式生成的函数就要显式地禁止它——使用explicit或者声明为private
第五章 类和函数: 实现
条款29: 避免返回内部数据的句柄
条款30: 避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级比这个函数要低
1.当返回的指针或引用为一个protected或private的对象或指针时,将会暴露不希望被公有访问的成员变量
2.如果这个成员变量是protected或private的,最好不要返回其非const的指针或引用,如果要返回,请返回const的指针或引用
条款31: 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用
1.返回局部对象的引用——导致引用无效
2.返回函数内部用new初始化的指针的引用——导致可能存在的内存泄漏
条款33: 明智地使用内联
1.比较小巧,经常调用的函数才声明为内联
2.递归函数不能声明为内联
3.滥用内联将会导致编译结果的膨胀