文章目录
命名习惯
- lhs、rhs分别作为左值与右值
- pT表示指向一个T型对象的指针
- rT表示T型对象的一个引用
- doSomething作为胡写的函数
- 使用Stuff后缀、the前缀、func后缀
- 类名与名称空间的首字母大写,变量与函数名的首字母小写
- other、temp、msg、ret、ans等变量名
class AccessLevels{
public:
int getReadOnly();
void doSomething();
private:
int readOnly;
}
尽量以const,enum,inline替换#define
尽量替换#define PI 3.14为:const double PI = 3.14;
对于类的专有成员, 使用static或者enum:
class GamePlayer {
private:
static const int NumTurns = 5;
int scores[NumTurns];
}
const int GamePlayer::NumTurns; //只声明不定义
class GamePlayer {
private:
enum { NumTurns = 5 };
int scores[NumTurns];
}
以传常引用代替传值
函数调用时,要创建单独的实参,传值就伴随着多个构造与析构操作。
另一方面,引用的汇编底层是常量指针,传引用的代价很小。
不要返回临时变量的引用
函数调用过程中的临时变量,在函数执行完毕后会析构,不应返回其指针。一个例外是返回函数static对象的引用。
提供const与non-const成员函数并使用类型转换重写
const与non-const成员函数的匹配是函数重载。常量对象优先调用const成员函数。
class TextBlock {
public:
const char& operator[](size_t position) const
{
return text[position];
}
char& operator[](size_t position)
{
//先转型为const对象调用const成员函数,再对返回值类型做修改
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
private:
string text;
};
使用列表初始化和类内初始值
使用未被初始化的对象会导致不明确的行为,要确保对象被使用前已被初始化。
构造函数先执行初始化操作,后执行构造函数体,使用列表初始化可以减少一次默认构造函数的调用。
class ABEntry {
public:
ABEntry( ): theName(), thePhones();
private:
string theName;
list<PhoneNumber> thePhones;
int numTimesConsulted = 0;
};
使用工厂函数
工厂函数是一个用来创建对象的函数,它像工厂一样,“生产”出来的函数都是“标准件”(拥有同样的属性)。
其一般由两部分组成:创建一个对象,返回引用或指针
工厂函数返回智能指针较为合适。
使用local static对象替换non-local static对象
类设计者提供的接口往往包含一个具体的对象实例,由用户进行使用。如果用户编译时,对该对象的使用早于该对象的创建,那么就会出现未定义的行为。
如果工厂函数创建的是一个static对象,即可解决这一问题,称为Singleton(独生子女)模式。
合理利用编译器自动生成的构造函数
编译器自动合成default构造函数、copy构造函数、copy赋值运算符重载,以及析构函数。(当类中内含const 或 引用时,不生成copy)
默认构造函数的作用是对每个成员调用他自己的默认构造函数。
copy的作用是对每一个成员进行赋值操作。
析构函数的作用是对每个成员调用它自己的析构函数。
可使用 =default 保留编译器自动合成的函数,使用 =delete要求编译器不自动合成。
明确类的用途
有些类被设计成多态基类,这类函数包含许多virtual成员函数,且一定使用virtual析构函数。
有些类单纯被设计为继承基类,不需要virtual析构函数。
有些类压根不被设计为基类,如string、vector等。
virtual函数带来了虚函数表的空间开销和运行时动态联编的时间开销。
不在构造和析构函数过程中调用virtual函数
因为构造顺序为基类 -> 派生类,析构顺序为派生类 -> 基类,在构造和析构函数中调用virtual函数运行的是基类的函数。
注意派生类的拷贝构造和拷贝运算符
处理派生类的copying函数时,一定要单独处理基类部分。
class Base { };
class Derived : public Base{
public:
Derived(const Derived& d) : Base(d), name(d.name) {}
Derived& operaotr=(const Derived& rhs)
{
Base::operator=(rhs);
name = d.name;
}
private:
string name;
};
inline
-
inline函数是一个函数,但它又不会带来函数调用带来的开销。
-
但它也会增加目标码的体积,inline带来的代码膨胀在运行过程中亦会导致额外的换页行为。此外,inline函数无法设置断点,无法进行调试。
-
inlining是编译期间的行为,通常把inline函数写在头文件中(因为在编译时要看见函数的原型)
-
inline 对编译器而言只是建议,编译器可以拒绝把函数inlining。
-
构造与析构函数尤其不适合作为inline函数。因为C++对“对象被创建和被销毁时发生什么事”做了各种各样的保证。实际的构造析构函数的体积比我们写的体积大得多。
-
较为合理的策略是将inlining限制在小型、被调用频繁的函数身上。
copy- and - swap
swap是一个有趣的函数。原本只是STL的一部分,而后成为异常安全性编程的中流砥柱,也可用于处理自我赋值。
拷贝并复制往往能提供较好的异常安全性,其基本思想是创建一个实际需要的副本,对副本做修改,最后将原对象与副本做交换。
std::swap的典型实现,包含三个赋值操作:
namespace std{
template<typename T>
void swap(T& a, T& b)
{
T temp = a;
a = b;
b = temp;
}
}
std::swap有时不能满足我们的需求,考虑下面的Example类。
class ExampleImpl {
private:
vector<string> G;
};
class Example{
public:
Example& operator=(const Example& rhs)
{
*pImpl = *(rhs.pImpl); //pImpl执行深拷贝
}
private:
ExampleImpl* pImpl;
};
如果对两个Example对象进行std::swap调用,那么会进行两个ExampleImpl对象的深拷贝,非常耗时。
但实际上,我们只需要交换两个pImpl指针即可。
void Example::swap(Example& other)
{
std::swap(pImpl, other.pImpl);
}
想要进行swap时,执行成员函数版本的swap
处理自我赋值
处理自我复制的难题主要出现在深拷贝中,假设Example类含有一个string* p。
以下为一种异常安全码。
Example& Example::operator=(const Example& rhs)
{
string* pnew = new(*(rhs.p));
delete p;
p = pnew;
return *this;
}
另一种方法是使用copy - and - swap 技术
Example& Example::operator=(const Example& rhs)
{
Example temp(rhs);
swap(*this, temp);
return *this;
}
资源管理
详见我的另一篇博客:C++ 资源管理 shared_ptr
让接口容易被使用,不容易被误用
详见我的另一篇博客:封装与接口设计 C++类与对象
降低编译依存度,减少接口与实现之间的耦合
详见我的另一篇博客:C++接口与实现分离 降低编译依存度的两种做法代码
面向对象设计
详见我的另一篇博客:继承与面向对象设计 virtual的替换方式