译自:http://www.caravan.net/ec2plus/guide.html
A. 代码从C语言到C++语言的移植
A.1 字符常量
注解
在C中,字符常量是int类型,而在C++中,其类型为char.
示例
I = sizeof(‘a’);
在C中,i保存的实际内容是sizeof(int)的计算结果值,该值要比1大。而在C++中,i存储的实际内容是sizeof(char),该值总是为1。
准则
当将C代码移植为C++代码时,所有对字符常量的sizeof有依赖关系的表达式都要被移除。
A.2 文件范围内的对象声明
注解
在C++中,在文件范围内,声明一个没有指定存储类型标志符的对象时,实际上是定义该对象为外部链接符号(extern)。如果这个定义中没有初始化表达式,那么该对象将被初始化为0。相比较于C来说,C++程序中声明的对象会被明确的定义且只定义过1次。
而在C中,这将被视为一个暂时的定义,这种定义方式在一个translation unit中允许出现多次。
示例
int a; /* (1) */
int a = 10; /* (2) */
在C中,(1)是一种暂时性的定义。由于(2)被看作是确切的定义,(1)被看作是只是声明一个变量。
在C++中,(1)、(2)都被认为是定义式。由于存在(1)、(2)两个重复定义式,因此是一个错误。
准则
在C++中,如果想在文件范围内仅声明一个对象(而不是定义该对象),那么该对象不能拥有初始化表达式,并且需要使用extern来修饰。
声明于文件范围内的每一个对象只能显式的被定义一次。除了一次声明外,其余所有声明都必须具有extern修饰并且没有初始化表达式。
A.3 const 类型修饰符
注解
在C中,在文件范围内,一个使用const修饰而未指明存储类型的对象具有外部链接属性。而在C++中,它具有内部连接属性。
示例
+- file1 --------------------+
| extern const int n; |
+----------------------------+
+- file2 --------------------+
| const int n = 10; |
+----------------------------+
在C中,文件file2中的对象n具有外部连接属性,因此,文件file1中对象n(该对象同样具有外部链接属性)可以引用它。在C++中,文件file2中的对象n具有的属性是内部链接的,因此,file1中的对象n无法引用到它。
准则
使用extern显示修饰const对象,使该对象具有外部链接属性。
A.4 void*的转型
注解
C语言标准允许void*转换为T*(T表示任何对象),而在C++中没有这样的标准。在C++中类似的转换需要显示转换来完成。
下面这些C标准库函数都返回void*:
calloc, malloc, realloc, bsearch, memcpy, memmove,
memchr, memset
在C++中,将这些函数的返回值赋值给一个非void*的指针时,需要
显示转换。
示例
int* p;
p = malloc(10 * sizeof(int));
在C++中,对指针p的赋值需要显示的转换,如:
p = (int *)malloc(10 * sizeof(int));
准则
在C++中使用new来代替calloc、malloc、realloc(参考A.12);
忽略memcpy, memmove, 以及memset的返回值(通常这些返回值由函数的第一个参数转换而来);对于其他所有返回void*函数(包括标准库函数和用户自定义函数),需要使用显式的转换来将返回值转换为其他指针类型。
A.5 枚举类型
注解
C中的枚举变量是整型。程序中,枚举类型和整型可以互相转换,而不需要显式的转换。C程序允许枚举类型对象进行++和--运算。
C++中的枚举是一种用户自定义类型。C++标准允许枚举类型转换为整型,但从整型转换为枚举类型是非法的。C++程序中还不能将内置的++和—以及符合运算符(如+=)作用于枚举类型变量上。
示例
enum RGB { red, green, blue } rgb;
++rgb;
如果表达式(++rgb)采用内置的++运算符,那么在C++中这是一个错误表达式。该表达式的语意相当于:
rgb = rgb + 1;
上面的表达式在C++中还是错误的。而像下面这样,对枚举值进行显示的转换就是正确的:
rgb = RGB(rgb + 1);
最好的解决方法是为枚举类型RGB实现一个++运算符。
RBG &operator++(RGB &x)
{
return x = RGB(x + 1);
}
准则
将C程序移植为C++程序时,在需要的时候,要为枚举类型实现类型安全的++和--操作符。
A.6 在转型、参数声明、sizeof中定义类型
注解
在C中,转型表达式、参数声明以及sizeof表达式中都可以进行类
型声明,而在C++中则不能。
示例
void func(struct TAG { int a; } st)
{
...
}
如上所示,TAG在参数声明是被定义。
准则
把在参数中声明的类型的定义式,挪到函数声明式作用域开始处,或者大于该作用域的某处。
在转型和sizeof表达式作用域开始处,或者大于该作用域的某处定义类型。
A.7 忽略局部对象定义式的控制流程跳转
注解
在C中,goto和switch语句可以使控制流程跳过块作用域内的局
部对象声明式,甚至有可能是对象的初始化式。而在C++中不存在这种跳转。
示例
goto LABEL;
{
int v = 0;
...
LABEL:
...
}
在C中,上面的代码是合法的,它假设标签LABEL之后的代码不依赖于整型变量v的初值0。而在C++中,这段代码总是不能通过编译。
准则
确保goto或switch语句没有跳过局部对象的初始化式。
A.8 字符数组的初始化
注解
在C中,允许使用字符串来初始化一个字符数组。初始化表达式中,数组可以比字符串少一个字符空间(‘\0’字符)。在C++中数组则必须能完全容纳字符串。
示例
char s[3] = "abc";
尽管常量字符串为4个字符大小,但数组s的大小为3。这种做法在C中合法,而在C++中非法。
准则
为了容纳‘\0’,请确保字符数组大小要比字符串长度大1。因而,有必要将字符数组大小指定为字符串长度+1。(也即:char s[4] = "abc";)
然而,为了使定义式自适应于不同的常量字符串,在定义式中不指定数组大小不失为一种好方法(即:char s[] = "abc";)。
A.9 原型声明
注解
C++程序要求在使用函数之前必须声明该函数原型。此外,C++程序会将函数声明式f()解释为f(void)----不带参数的函数。而在C中,这样的行为是不确定的。
示例
extern void func();
....
sub();
func(0);
因为没有sub函数的声明,因此调用该函数是错误的。由于声明式中函数不带参数,因此以0为参数调用func也是个错误。
准则
确保被调用的函数已经被声明。为了强调函数f不带参数,好的做法是在声明函数时,将参数声明为void,即声明式为:f(void).
A.10 C++增加的关键字
注解
C中没有下面的C++关键字:
asm bool catch class
const_cast delete dynamic_cast explicit
false friend inline mutable
namespace new operator private
protected public reinterpret_cast
static_cast template this throw
true try typeid typename
using virtual wchar_t
示例
int class, new, old;
准则
确保没有使用C++关键字作为标记符。
A.11 嵌套类型的作用域
注解
C结构体或联合体内定义嵌套类型,该类型的作用域终止点和该结构
体或联合体相同。而C++中定义的嵌套类型作用域终结于该结构或联合体。
示例
struct S {
int a;
struct T {
int t;
} b;
int c;
enum E { V1, V2 } e;
};
struct T x;
enum E y;
x、y的声明在C程序中是合法的,但在C++中非法。在C++程序中,
在类S中定义的类型T、E的作用域不会超过类S的定义范围。
准则
除非只是在结构体或联合体内使用定义的嵌套类型,否则不在嵌套作用域内定义类型。
A.12 动态内存管理
注解
new/delete和malloc/free没有采用相同的内存管理策略。因此,
除非已经使用new获取了内存,否则应用程序中不能使用delete来释放内
存。同样,除非使用了malloc分配内存,否则不能使用free来释放内存。
示例
int (*p)[10];
p = (int (*)[10])malloc(sizeof(*p));
....
delete p;
这里的delete具有未定义的行为。
准则
在C++中避免使用malloc/calloc/realloc/free函数,而仅使用
new/delete.
A.13 '/'之后的'/*'
注解
C++程序具有‘//’的注释风格,因此紧接在符号‘/’之后的C风格
注释‘/**/’,会被C++程序作为理解为‘//’后接‘**/’。
示例
i = j //* comment */ k ;
‘//’被解释为注释分隔符,因而表达式被解释为‘i=j’而不是‘i=j/k’。
准则
尽量避免紧接着符号‘/’后写C风格注释‘/**/’.
B. 关于代码容量的准则
B.1 对象初始化
注解
有很多方法来初始化某个对象,有些初始化方法将会产生不必要的
临时对象,从而导致代码量膨胀。
例如:
T x(i) // (1)
T x = i; // (2)
T x = T(i) // (3)
T x; // (4)
x = i; //
(1) 直接使用构造函数来初始化对象x,这样就不会产生临时对象。
调用形式类似于:
x.T(i); // x对象调用构造函数
(2) 在某些实现中,方法(2)类似于方法(1),即对象x直接调用
构造函数。而在另外一些实现中,该方法会先使用构造函数生成一个临时对象,然后将该临时对象作为对象x的初始值。调用形式类似于:
temp.T(i); // 生成temp
x.T(temp); // x调用拷贝构造函数
temp.~T(); // 析构temp
(3) 等同于(2)
(4) 使用T的默认构造函数来初始化x,然后调用赋值操作符将新值
赋给x。赋值操作符可能会释放x正在使用的资源,并重新为x获取新的资源。
x.T(); // x调用默认构造函数
x.operator=(i); // x调用赋值操作符
准则
在上面的四种方法中,优先考虑方法(1)。
B.2 inline标记符
注解
内联减少了函数进出栈上的管理开销,但这也同时增加了代码容量。
在内内部定义的成员函数默认是内联的。
准则
仅对小函数进行inline修饰。在类体之外定义所有不适合作为内联函数的成员方法,从而使得该成员不是内联函数。
B.3 返回值中的临时对象
注解
函数按值返回一个对象时,可能需要创建并销毁一个临时对象,从而
导致代码量增加并且带来运行时开销。
示例
class Matrix {
int a, b;
public:
Matrix &operator+=(const Matrix &);
friend
Matrix operator+(const Matrix &, const Matrix &);
};
Matrix operator +(const Matrix &, const Matrix &)
{
...
}
void func()
{
Matrix a,b;
a = a + b; // (1)
a += b; // (2)
}
函数func在(1)处调用了+操作符,该操作符按值返回一个Matrix
对象。在某些编译器实现中,会先构造一个Maxtrix临时对象,然后销毁
该临时对象。
函数func在(2)处调用+=操作符,该操作符返回一个Matrix对象
引用,因而不会构造临时对象。
准则
对于类类型对象,使用复合赋值操作符(如使用‘+=’而不是‘+’和
‘=’)来避免编译器生成再销毁不必要的临时对象。
注:个人认为这个准则有失准确。此例中,+=操作符不产生临时对象的原因在于该操作符按引用返回对象而非按值返回对象。因此,准确的说法是,使用按引用或指针返回对象的函数来避免构造不必要的临时对象。
B.4 new和delete操作符
准则
在必要时,实现类属的new和delete操作符,从而提升管理动态内存
的速度和内存利用率。
B.5 全局对象的初始化
注解
全局对象初始化的顺序依赖于编译器的实现。但可以肯定的是,在同一个解释单元其初始化顺序和对象的声明顺序相同。
示例
文件1 文件2 文件3
int a = f(); int b = f(); int f(void)
{
static int a = 0;
return a++;
}
程序可能将a初始化为0,而b为1,或者反过来。这依赖于编译器如何选择他们的初始化顺序。
如果将文件2中变量b的声明移到文件1中,那么两变量的初始化顺序就随即确定了,即:
文件1 文件2 文件3
int a = f(); int f(void)
int b = f(); {
static int a = 0;
return a++;
}
这种情况下,a要先于b被初始化。
避免编写依赖于不同解释单元内全局对象初始化顺序的代码。
C. 关于速度的准则
C.1 元素为类对象的数组中的new/delete
注解
声明元素为类对象的数组时,编译器会为该数组每一个元素调用构造
函数。在超出该数组的作用域范围时,又会一一调用每个元素的析构函数。构造/析构可能占用超乎想象的时间,这在实时过程系统中是一个问题。
准则
在对时间要求比较高的过程系统中,避免创建/销毁大型的元素为类对象的数组。
C.2 循环体内的对象声明
注解
如果在循环体内声明类变量,那么在循环的每次迭代时都需要构造并
析构该对象。这种构造并析构带来的问题就是循环执行速度降低。
示例
for (i = 0; i < 1000; i++)
{
FOO a;
...
}
准则
在循环体外部声明类类型变量,而避免在内部声明。
D. 编写只读型代码准则
D.1 ROM中的const对象
注解
通常,如果const对象具有如下一些属性,则可以存储在ROM中:
-- 具有static存储时长
-- 由常量表达式初始化
-- 是一个POD(plain old data)类型对象
POD类型对象具有如下属性:
-- 一个无属性(scalar)类型(数字, 枚举和指针)
-- 类/结构/联合体中所有数据成员都是public访问权限并且是
POD类型, 同时类中没有用户自定义的构造函数/析构函数,没有基类和虚函数
-- 所有元素都是POD类型的数组
示例
static const char lang[] = "EC++";
class A {
int a;
public:
A();
~A();
};
const A x;
'lang'可能存储于ROM中,而‘x’则不会。
准则
欲将对象存储于ROM中,则将对象声明为POD类型,并使用常量初始化该对象。
NOTE: The form of presentation used here, and several of the specific
guidelines, were inspired by the excellent book by Thomas Plum and
Dan Saks, 'C++ Programming Guidelines' (Plum Hall Inc., 1991).