第一条款
使用const / enum inline 来代替你的#define
如define一个常量数值
#define num 1.333
使用num出错的时候会报错 #$@%@#$%1.33!@#!@意义不明
const float num;//const代替
class A{
static float num;
};
float A::num = 1.333;//静态变量代替
//enum huck代替
class A{
enum {num = 5}
int a[num];
};
//这里枚举成员当做常量使用,无法改变值,亦无法取得其地址
另外,在定义一个函数的时候
如#define (a,b) f( (a) > (b) ? (a) : (b);
引发问题,a/b的累加将取决于a,b的大小,因为如果用(a++,b)的方式调用时,(a++)被视为整体而展开,结果就是a较大的时候,a在与b比较时累加了一次,之后选择了(a)的时候实际又执行了(a++),也就是a累加了两次。
可以使用相应的template来解决这个问题,定义一个template的同意义比较函数就好了,还可以视使用情况加上inline。
结论
const / static变量 / enum hack代替define变量
inline template代替define函数
条款二
尽量使用const
当确定变量是无法改变(不应该改变)的时候就使用const
函数的返回值也尽量使用const来防止对返回的结果进行一个赋值,有时可能只是想要进行比较的时候却少打了一个等号,使用const会让编译器检测到这个情况(毕竟你不能对常量(右值)赋值)
const成员函数
mutable来定义一个可以在const成员函数之中可以修改的成员变量(非static)
后置cosnt的成员函数返回的若是成员,则返回值通常也是cosnt的,为了服从后置const不改变成员值得定义。
(事实上如果想要返回一个普通的成员引用是需要用到const_cast的,否则返回的也是一个const成员的引用)
const的对象会(也只能)调用后置const的成员函数,一般的对象则会调用非const版本,前提这两个版本都有。故其实非cosnt成员函数的返回值可能被赋值,不过只在返回左值时发生
另外stataic成员是可以在const成员函数内改变的,毕竟static变量是对象共享的。
class A {
public :
A() { a = 15; }
const int& f() const
{
cout << "cosnt" << endl;
b = 200; //可以改变static
return a;
}
int & f()
{
cout << "non-cosnt" << endl;
return a;
}
const A fa() const
{
return *this;
}
A fa()
{
return *this;
}
int a = 10;
static int b;
};
int A::b = 100;
void main()
{
A a;
const A b;
a.f() = 100;//非const版本
b.f();
a.fa() = A();//调用非后置const版本,返回值为对象,故可以改变
}
//输出
//non const
//const
可以选择使用const 成员来实现non-cosnt成员,但是不要反过来,因为这样就违背了const后置的初衷了(不在函数内改变成员)
一个对[ ]运算符的实现
const_cast<return_type&>(
static_cast<const type&>(*this) //把当前的非const实例的this指针转换成const类型的
[position]//然后调用const类型的[ ]运算符
);//最后除去顶层const(返回的是一个指向的常量指针/引用)
对于顶层const 与底层const
自己不可改变则为顶层cosnt,指向的东西不可改变则为底层const
const int *p为底层,int *const p 为顶层。
结论
1、const可以帮助编译器去检测一些难以察觉的错误,比如把值赋给了函数的返回值,函数的返回值、参数、成员函数之后、定义变量这些地方都是cosnt使用的位置
2、在const成员函数中其实是可以改变成员值的,比如改变对象中指针指向的值(而不是指针本身),也就像这样
struct s {
int c = 0;
int *p = &c;
void fun()const {
*p = 10;
}
};
3、如果non-const与const成员函数本质上都做着一样的事,那就让const版本去实现非cosnt版本。
条款三
确认对象使用前已经被初始化
1、使用初始化列表来代替赋值初始化,即使使用默认初始化也把这个变量/类的构造函数写在初始化列表中
class A{
A():name(){}//默认初始化
}
2、non-local static初始化
global(全局对象),namesapce中的对象;classes内、函数内、file作用域内加上了static的对象均是static对象。定义在函数内的static对象称为non local static对象。所有static对象在main()结束的时候释放(调用析构)
编译单元:产出单一目标文件的源码、基本上是单一的源文件加上头文件
当一个编译单元互相使用了另一个编译单元的non-local static对象处似乎还的时候,无法保证用来初始化的non-local static先被初始化了,所以应该使用local static对象来解决这个问题
class A{}
extern A a;//声明一个全局a
替换为
class A{}
A &getA()
{
static A a;
return a;
}
也就是用一个函数来返回函数内作用相同的对象引用,编译器会保证函数内的loacl static对象使用的时候已经初始化。
(c++11后会保证这种对象在多线程下使用安全)
结论
1、总是为内置值赋值(没有显示定义默认构造的时候系统执行的默认初始化可不会初始化内置类型)
2、总是使用初始化列表并且顺序与声明一致,与声明一致;而不是在函数体内赋值,这个时候所有成员都先有了一次默认赋值(如果需要的话),当然,如果要用一个成员初始化另一个成员,那就考虑把这个步骤放到函数体内,防止初始化顺序搞错
3、使用local static的函数方式去保证一个static对象在使用的时候必定初始化了
条款四
编译器会自动生成默认构造、拷贝构造、拷贝赋值、析构函数等(如果他们都有必要的话)
拥有 const成员/引用 的类不会合成各种拷贝移动构造成员(任何无法拷贝的行为都会导致拷贝赋值等成员不合成)
条款五
拒绝拷贝的使用
=delete
旧的做法是使用private来限定拷贝成员的使用
条款六
为基类定义virtual的析构函数,有派生类时才能正确析构
不要继承string等容器,他们的析构函数非virtual
如果想要一个基类是抽象类,但是又不想要让其中的任何成员函数为pure virtual,那么就把析构函数定义成纯虚函数。
但是此时就需要为这个虚函数提供一个“定义”,派生类需要调用它
class A {
public:
virtual ~A() = 0;//析构函数要放在public下,析构函数为纯虚函数
};
A::~A() {}//提供了定义
class B :A {
};
int main()
{
B b;//允许创建对象
}
条款七
让析构函数noexcept
析构函数之中不应当执行可能抛出错误的操作,如果某种操作无论如何都需要在销毁对象之前执行,定义一个函数来把它交给你的用户去调用,析构函数中在用户没有调用的时候去用try{}catch{}语句把这个操作拦下,并吞下可能抛出的错误,防止析构函数需要抛出让外界来处理的异常。
class database{
void close()
{
db.close()
closed = true;
}//close可能抛出异常,那么用户应当来调用它,如果用户想要得知处理这个异常
~database() noexcept
{
if(!closed)//如果用户没有调用这个操作,则由我们来调用,但是要掉异常
{
try{
db.close()
}
catch(exception &e)
{处理异常并记录}
}
}
}
//处理后可以使用abort()终止,或者exit()按序退出
条款八
不要在构造/析构函数里调用虚函数
调用的虚函数是当前执行的构造函数所构造的对象的版本,即即使使用派生类构造一个对象,但在派生类的构造函数调用基类构造函数,并执行基类构造函数的时候,基类的构造函数中调用的虚函数是基类的版本,其实这个时候的this指针指向的基类,也就是说,这个时候类的实际类型是基类,而不是派生类,因为派生类还没有构造好。这个时候若调用的是一个纯虚函数会无法连接。
class A {
public:
virtual ~A() noexcept
{
try {
fun();//调用了自己的A.fun(),故无法通过连接
}
catch(exception e)
{
}
}
virtual void fun() = 0;//fun为纯虚函数
};
class B : A {
public:
~B() override {}
void fun()override {}
};
int main()
{
B a;//虽然创建的是B类型对象
}
因此,应该把这些操作放进一个派生类的private static函数中去,这样就可以确保不会使用派生类的尚未初始化的成员,也不会被客户误用;然后在初始化值列表中调用一个带参数的基类构造函数来传入相关的数据,再使用基类的相应功能函数。
class Base {
public:
Base(string info) { fun(info); } //完成最后需要基类的数据操作
private:
static void fun(string info) {}
};
class Derived : Base {
public:
Derived() :Base(createInfo()){} //派生类中调用含参数的基类构造
private:
static const string &createInfo() //把操作放到派生类的函数中
{
return "info";
}
};
结论
不要在构造/析构函数中使用虚函数,因为使用的是当前的版本,动态绑定不会绑定到当前的派生类,而是绑定到正在构造的基类部分。
条款九
让operator=返回一个reference to *this
(包括其他的+= -= *= /=等赋值运算符)
即返回一个当前对象的引用来保证连锁赋值的可能,这也使这些函数应该被定义成成员函数
条款十
在operator=中处理自我赋值检测
1、使用自我赋值检测
Base & operator=(const Base& b)
{
if (this == &b)return *this;
}
2、把参数对象中的指针(需要delete释放的对象)复制一次后赋值到类内,再删除原来的指针指向的对象
class Base {
public:
Base & operator=(const Base& b)
{
if (this == &b)return *this;//自我赋值检测
int *old = num;//记住旧的指针
num = new int(*b.num);//复制一份参数对象中指针指向的对象
delete old;//删除旧的指针
return *this;
}
int *num;
};
是否要结合自我赋值检测和手动复制视使用情况而定
3、使用拷贝并交换
即用swap函数实现operator=(swap需要自己先实现,不是使用 swap(*this, other) 这样是死循环),或者说,先构建一个和参数一样的对象,然后把当前对象的值和新构建的这个对象交换。
3、使用拷贝并交换(copy and swap)
class Base {
public:
Base & operator=(const Base& b) //较清晰
{
Base copy(b);//拷贝对象
swap(copy); //交换参数对象与当前对象
return *this;
}
Base & operator=(Base b) //更高效,但清晰度略逊
{
swap(b); //直接交换对象,把拷贝的工作交给函数参数的构造阶段,这个swap需要自己定义
return *this;
}
private:
int *num;
void swap(Base &lhs)
{
std::swap(num, lhs.num);
}
};
条款十一
赋值基类与派生类的每个部分
不要想着在赋值运算符里调用构造函数、或是反过来。如果两个函数的操作重复过多,那么就把他们包装到一个priavte中然后在两个函数里去调用他。