条款2: 尽量以const, enum, inline 替换#define
#define port 8888
- define可以定常量值,但是它没有作用域,并且它是直接被替换,如果是大型项目,报错显示的是8888而不是port,导致无法追踪该错误。这个问题也会出现在记号表调试中,define的常量没有进入记号表中。具体原因可以看ELF文件解析。
- define不能被封装,而const可以被封装在类中。
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
- 类似于上面定义的宏函数,必须要记住为宏中的所有实参加上小括号,否则有可能出现错误。define定义的宏函数的好处就是可以避免函数调用带来的额外开销。我们可以用inline替换
因为宏函数的参数没有类型,所有我们用模板实现。
template<typename T>
inline void callwithmax(const T& a, const T& b)
{
f((a) > (b) ? (a) : (b));
}
有了const、enum和inline,我们对预处理器(特别是#define)的需求降低,但并非完全消除。#include仍然是必需品,而#ifdef和#ifndef也继续扮演着控制编译的重要角色。
宏函数为什么一般要用do while(0)包含起来?
#define test() \
printf("\\\\\\n");\
printf("######\n");
int main()
{
if(false)
test();
}
看上面这段代码,if条件不成立,不会执行test宏函数,但是却打印出来了"#####"。是因为宏函数直接展开变为如下代码所示
if(false)
printf("\\\\\\n");
printf("######\n");
所以一般会加上do while(0)将宏函数内部代码包含在一起,封装了作用域。
条款3: 尽可能使用const
-
将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
-
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
代码如下
class TextBlock
{
public:
...
const char& operator[](std::size_t position) const
{
....
}
char& operator[](std::size_t position)
{
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
//先将*this转换为const调用const的[]函数,然后将返回值去除const
}
};
const成员调用非const成员函数绝对是一个错误的行为。但是非const成员可以调用const成员函数。
条款4:确保对象使用之前已被初始化
- 内置类型对象进行手工初始化,因为c++不保证它们的初始化
- 构造函数最好使用成员初值列,而不要在构造函数本体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
条款5:了解C++默默编写并调用哪些函数
类会帮助我们实现默认的构造函数,拷贝构造函数拷贝赋值函数和析构函数,如果定义了其中一个,那么它的默认函数就不会存在,除非显示声明需要默认函数,用到 = default;
条款6:若不想使用编译器自动生成的函数,就应该明确拒绝
如果不想要编译器自动生成的函数我们有两种方法
- 将不想要的函数声明在private中(c+11 之前的方法),因为默认的函数都是public。
- 显示的= delete (c++11新出)
条款7:为多态基类声明virtual析构函数
派生类在销毁的时候首先会调用基类的析构函数,如果基类的析构函数不是虚函数,那么就会产生局部销毁,这里的局部销毁指的是只会销毁基类的部分,而派生类中的资源没有被销毁,导致资源泄漏等错误。
#include<iostream>
#include<memory>
class base
{
public:
base() : str(new char('a'))
{
std::cout << "基类构造函数运行\n";
}
//virtual ~base()
~base()
{
std::cout << "基类析构函数运行\n";
delete str;
}
private:
char *str;
};
class derived : public base
{
public:
derived() : base(), der(new char('b')) {}
//virtual ~derived()
~derived()
{
delete der;
std::cout << "派生类析构函数运行\n";
}
private:
char *der;
};
int main()
{
base *c = new derived();
delete c;
}
当我们没有将析构函数声明为虚函数时,其上述代码结果运行如下
上述结果显示,只调用了基类的析构函数,没有调用派生类的析构函数,会导致派生类成员得不到析构,造成资源泄漏。
现在我们将析构函数声明为虚函数,运行结果如下
结论:将析构函数声明为virtual析构函数。
- 带有多态性质的基类应该声明一个virtual析构函数。如果class带有任何virtual析构函数,他就应该声明一个virtual析构函数。
- Classes的设计目的如果不是作为基类使用,或不是为了具备多态性,就不该声明virtual析构函数。如果不具备多态性声明了virtual析构函数,就是造成资源的浪费,因为virtual会对应虚函数表。
条款11: 在operator= 中处理"自我赋值"
#include<iostream>
#include<memory>
class Bitmap
{
public:
Bitmap() : count(new int(0)) {}
//因为类中存在堆上的对象,所以在赋值的时候要将被赋值对象所占用的堆内存释放
/*
Bitmap& operator = (const Bitmap& a)
{
delete count; //如果是自我赋值,删除自身的内存在去访问会导致非法的行为,所以我们要判断是否为自我赋值。
count = new int(*a.count);
return *this;
}
*/
//方法1: 防止自我赋值出现上述问题。
Bitmap& operator = (const Bitmap &a)
{
if(this == &a)
{
return *this;
}
delete count;
count = new int(*a.count);
return *this;
}
//方法2: 防止自我赋值出现上述问题。
Bitmap& operator = (const Bitmap &a)
{
int *temp = count;
count = new int(*a.count);
delete temp;
return *this;
}
private:
int *count;
};
- 确保当对象自我赋值时operator= 有良好的行为。其中技术包括比较“来源对象” 和"目标对象"的地址、精心周到的语句顺序、以及copy-and-swap。
- 确保任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。