C++ 编码实践指南






3.26/5 (36投票s)
本文档描述了 C++ 编码风格和实践,旨在帮助开发者编写健壮、可靠且易于其他开发者理解和维护的代码。
目录
引言
为了编写其他开发者能够理解的一致性代码,您应该遵循一些编码风格和实践,而不是发明自己的。这些包括命名约定(如何命名变量和函数)、代码和类布局(包括制表符、空格、括号放置)、强制性的 const 正确性等……要理解本文档,您应该了解面向对象编程和 C++ 的基础知识。除了本文档,您还可以参考一些优秀的在线资源。
命名约定
当你开始写代码时遇到的第一个任务是如何命名你的变量、对象和函数。命名约定的重要性不可低估。提供正确的命名将产生自文档化的代码,易于他人理解。除非你想混淆源代码,否则你应该遵循这些指导方针。
使用一个能毫不含糊地描述变量或对象的名称。函数在其名称中应包含一个实现其功能的动词。
Linux 风格(使用下划线分隔单词,名称全小写)
int some_variable;
float bar_weight;
unsigned int users_number;
bool is_engine_started;
double circle_area;
double m_circle_area; //class private variable, prepend m_ prefix
void* psome_void_pointer;
const int USERS_NUMBER;
int i, j, n, m, tmp; //some local loops variables, temporary variables
namespace mynamespace;
vector<int> users;
vector<char *> user_names;
class SomeClass;
int do_work(int time_of_day);
float calculate_radius(const Circle& rcircle);
int start_io_manager();
int open_dvd_player();
Windows 风格(MFC 应用程序,前缀加上 匈牙利 前缀以标识变量类型)
INT nSomeVariable;
FLOAT fBarWeight;
DWORD dwUsersNumber;
BOOL bIsEngineStarted;
DOUBLE dCircleArea;
DOUBLE m_dCircleArea; //class private variable, prepend m_ prefix
PVOID pSomeVoidPointer;
const INT USERS_NUMBER;
int i, j, n, m, tmp; //some local loops variables, temporary variables
namespace MyNameSpace;
vector<int> nUsers;
vector<char *> pszUserNames;
class CSomeClass;
INT DoWork(INT nTimeOfDay);
FLOAT CalculateRadius(const& Circle rCircle);
INT StartIOManager();
INT OpenDvdPlayer(); //if abbreviation takes more than
// 2 letters do not capitalize the whole word
.NET 平台(匈牙利命名法,前缀已废弃)
int someVariable;
float barWeight;
unsigned int usersNumber;
bool isEngineStarted;
double circleArea;
double circleArea; //refer to a class private variable
// in code as this->circleArea
void^ someVoidPointer;
const int UsersNumber;
int i, j, n, m, tmp; //some local loops variables, temporary variables
namespace MyNameSpace;
array<int> users;
array<String> userNames;
class SomeClass;
int DoWork(int timeOfDay);
float CalculateRadius(const& Circle circle);
int StartIOManager();
int OpenDvdPlayer(); //if abbreviation takes more than 2 letters
// do not capitalize the whole word
选项卡
制表符是 8 个空格,因此缩进也是 8 个字符。其主要思想是清晰地定义控制块的开始和结束位置,如果你有较大的缩进,会更容易看到缩进的工作方式。它还有一个额外的好处,当你的函数嵌套太深时会发出警告。
//very bad
void FaceDetector::estimate_motion_percent(const vec2Dc* search_mask)
{
if (search_mask == 0) { m_motion_amount=-1.0f; }
else
{
unsigned int motion_pixels=0;
unsigned int total_pixels=0;
for (unsigned int y = get_dy(); y < search_mask->height()-get_dy(); y++)
{
for (unsigned int x=get_dx(); x < search_mask->width()-get_dx(); x++)
{
total_pixels++;
if ((*search_mask)(y,x)==1) motion_pixels++;
}
}
m_motion_amount = float(motion_pixels)/float(total_pixels);
}
}
//very good
void FaceDetector::estimate_motion_percent(const vec2Dc* search_mask)
{
if (search_mask == 0)
m_motion_amount = -1.0f;
else {
unsigned int motion_pixels = 0;
unsigned int total_pixels = 0;
for (unsigned int y = get_dy(); y <
search_mask->height() - get_dy(); y++) {
for (unsigned int x = get_dx(); x <
search_mask->width() - get_dx(); x++) {
total_pixels++;
if ((*search_mask)(y, x) == 1)
motion_pixels++;
}
}
m_motion_amount = float(motion_pixels) / float(total_pixels);
}
}
空格、花括号和圆括号
对于函数和类定义,将开括号放在下一行是不含糊的。
void some_function(int param)
{
//function body
}
class SomeClass
{
//class body
};
但对于 ifs
、for
s、while
s 等,括号和圆括号有几种可能性。
if(some_condition)
{
//...
}
if( some_condition )
{
//...
}
if ( some_condition )
{
//...
}
if (some_condition)
{
//...
}
if (some_condition) {
//...
}
首选方式是最后一种,由 K&R 提出,即将开括号放在行的末尾,并将闭括号放在行首。其原因是尽量减少空行数。
数学表达式中的空格应遵循此风格。
float a, b, c, d;
int e, f, g, h;
a = (b + d) / (c * d);
e = f - ((g & h) >> 10); //good
e =f-( (g&h ) >>10); //bad
多重包含保护
用多重包含保护包装你的头文件内容,以防止重复包含。
#ifndef Foo_h
#define Foo_h
// ... Foo.h file contents
#endif
类布局
当你声明一个类时,请记住,如果你不提供以下内容的定义:
- 构造函数
- 复制构造函数
- 赋值运算符
- 取址运算符(const 和 non-const)
- 析构函数
C++ 将会自动提供。
//declaring that class
class SomeClass
{
};
//you get these functions provided automatically
class SomeClass
{
public:
SomeClass() { } //constructor
~SomeClass() { } //destructor
SomeClass(const SomeClass &rhs); //copy constructor
SomeClass& operator=(const SomeClass& rhs); //assignment operator
SomeClass* operator&(); //address-of
const SomeClass* operator&() const; //operators;
};
如果你不打算提供拷贝构造函数和赋值运算符,并且你的类在构造函数中分配内存并在析构函数中释放,请将拷贝构造函数和赋值运算符声明为私有成员,以避免意外克隆类对象和重复删除同一内存(在原始对象及其副本中)。
按 public
、protected
和 private
的顺序提供类声明。这样,类的用户就可以立即看到他们需要使用的 public
接口。
#ifndef ClassName_h
#define ClassName_h
class ClassName
{
public:
ClassName();
ClassName(const ClassName& classname);
virtual ~ClassName();
// Operators
const ClassName& operator=(const ClassName& classname);
// Operations
int do_work();
int start_engine();
int fire_nuke();
// Access
inline unsigned int get_users_count() const;
// Inquiry
inline bool is_engine_started() const;
protected:
//protected functions for descendant classes
private:
//private data and functions
unsigned int m_users_count;
bool m_is_engine_started;
};
// Inlines
inline unsigned int SomeClass::get_users_count() const
{
return m_users_count;
}
inline bool SomeClass::is_engine_started() const
{
return m_is_engine_started;
}
#endif ClassName_h
将内联函数放在头文件中,并在类定义之外,不要弄乱类的主体。
Const 正确性
Const 正确性是指使用 const
关键字显式声明并防止修改不应修改的数据或对象。从一开始就使用它,因为之后修复 const 正确性将是一场噩梦。
你可以为数据或类成员函数声明提供它。
const
声明;- 成员函数
const
;
const int max_array_size = 1024;
max_array_size++; //compilation error
int a;
int & ra1 = &a;
const int& ra2 = &a;
a = 10;
ra1 = 15; //ok
ra2 = 20; //compilation error
//the char data pointed by psour will not be modified
int copy(char* pdest, const char* psour)
{
//copy data from psour to pdest
}
使用 const
关键字声明成员函数,表示该函数是一个“只读”函数,它不会修改调用它的对象。
class Foo
{
//...
void set(int x, int val);
int get(int x) const;
//...
};
const Foo* pobj1; //modification of the pobj1 is forbidden
int i = pobj1->get(); //you can only call const member-functions
//with const pointer to class object
pobj1->set(0, 10); //compilation error
然而,将类成员变量声明为 mutable
允许它被 const
函数修改。
class SomeClass
{
public:
void some_function() const;
private:
mutable int a;
};
void SomeClass::some_function() const
{
a++; //you can modify a in const function
}
你可以防止一个指针在被赋值后再次被重新指向其他位置。这与引用声明的行为类似。
Foo foo1, foo2;
int data1[100];
int data2[100];
Foo* const pfoo;
int* const pdata;
pdata = data1;
pfoo = &foo1;
*pdata = 100; //you can change the data
pfoo->set(0, 10); //or class object
pdata = data2; //compilation error
pfoo = &foo2; //you can not reassign the pointer
你也可以同时声明一个 const 对象和一个指针。
//neither Foo object can be changed
//nor pfoo changed to point to another Foo object
const Foo* const pfoo;
const int* const pdata;
最后,如果你提供了 ()
或 []
运算符,也请提供它们的 const
版本,以便“只读”对象使用。
class SomeClass
{
...
inline int& operator()(int x);
inline int operator()(int x) const;
inline int& operator[](int x);
inline int operator[](int x) const;
...
};
现在,你将能够使用类对象的普通版本和 const
版本。
SomeClass a;
const SomeClass b;
int i = a(0);
int j = b(0);
a(1) = b(2);
b(3) = a(0); //compilation error, b object can not be modified
函数返回值
函数返回不同类型的值。最典型的一种是指示函数成功或失败的值。这样的值可以表示为错误代码整数(负数 = 失败,0 = 成功)或“成功”布尔值(0 = 失败,非零 = 成功)。
如果函数名是一个动作或祈使句,函数应该返回一个错误代码整数。如果函数名是一个谓词,函数应该返回一个“成功”布尔值。
int start_engine(); //0 = success, negative = error-codes
bool is_engine_started(); //true = success, false = error
杂项
将 &
或 *
与类型放在一起,而不是与变量放在一起。这使得类型更加清晰。
//good
int& rval; //'it is' reference int
float* pdata; //'it is' pointer float
//bad
int &rval;
float *pdata;
将不能为负值的变量定义为 unsigned
(例如,unsigned int array_length
而不是 int array_length
)。这将有助于避免意外赋值为负值。
引用是一个 const 指针,如 int* const p
,一旦赋值,就不能重新赋值。
int var1 = 100;
int var2 = 200;
int& ref = &var1;
ref = &var2; //avoid it!
要为 Foo** ppfoo
提供一个常量双指针,请使用声明 const Foo* const* ppfoo
。
在函数定义中始终提供形式参数。
void set_point(int, int); //ambiguous, what are parameters? avoid it,
void set_point(int x, int y); //good.
将指针与零进行比较。
void some_function(char* pname)
{
//bad
if (!pname) {
//...
}
//error sometimes
if (pname == NULL) {
//...
}
//good
if (pname == 0) {
//...
}
}
在函数参数中,类对象请通过引用或指针传递。如果你按值传递,将会创建一个类对象的完整副本,如果它占用了几兆字节的存储空间,这将非常糟糕。
//avoid it, pass big_object by reference or pointer
BigObject modify_it(BigObject big_object)
{
//a local copy of big_object will be created
...
return big_object;
}
//good practice
void modify_it(BigObject& big_object)
{
//no copy is created, you can modify the object as usually
}
面向对象编程技巧
切勿返回指向类私有数据成员的指针或非 const 引用。不要破坏封装原则。
class SomeClass
{
public:
SomeObject* get_object() { return &m_object; }
//do not break incapsulation principle
//m_object can be modified outside the class
const SomeObject& get_object() { return m_object; }
//good, the m_object will not be
//inadvertantly modified outside the class
private:
SomeObject* m_object;
};
始终在公共类中声明虚析构函数。如果你从没有虚析构函数的父类派生类,那么当您将派生类对象指针转换回父类指针,稍后删除父类指针时,只会调用父析构函数。
class Base
{
...
~Base(); //should be virtual ~Base()
...
};
class Derived : Base
{
...
};
...
Derived* pderived = new Derived();
Base* pbase = (Base *)pderived;
...
delete pbase; //only ~Base() will be invoked
...
使用友元。如果你不打算让库的用户使用某个类,但你又从其他公共类访问该类的私有成员,那么为其提供公共访问器方法是多余的。将你的类声明为它的友元,这样你就可以访问它的私有数据了。
class MainClass
{
...
void some_function();
...
HelperClass* phelper_class;
...
};
void some_function()
{
...
int i = phelper_class->m_variable; //you gain access to private members
//of HelperClass from MainClass
phelper_class->m_variable = 10;
...
}
class HelperClass
{
friend class MainClass;
...
private:
int m_variable;
...
};