一、文件结构
1.1 版权和版本的声明
(1)版权信息。
(2)文件名称,标识符,摘要。
(3)当前版本号,作者/修改者,完成日期。
(4)版本历史信息。
/*
1. Copyright (c) 2001,上海贝尔有限公司网络应用事业部
2. All rights reserved.
3. 4. 文件名称:filename.h
4. 文件标识:见配置管理计划书
5. 摘 要:简要描述本文件的内容
6.
7. 当前版本:1.1
8. 作 者:输入作者(或修改者)名字
9. 完成日期:2001年7月20日
10. 12. 取代版本:1.0
11. 原作者 :输入原作者(或修改者)名字
12. 完成日期:2001年5月10日
*/
1.2 头文件的结构
规则
1)防止头文件重复引用,用 #ifndef/#define/#endif 预处理
2)用 #include <filename.h> 引用标准库的头文件(编译器将从标准库目录开始搜索)。
3)用 #include “filename.h” 引用非标准库的头文件(编译器将从用户的工作目录开始搜索)。
建议
1)头文件只声明不定义
== C++允许在声明时直接定义,会自动转成内联函数,书写风格不一致,不建议。==
2)尽量不使用全局变量,不在头文件出现extern int value 。
// 版权和版本声明,此处省略。
#ifndef GRAPHICS_H // 防止 graphics.h 被重复引用
#define GRAPHICS_H
#include <math.h> // 引用标准库的头文件
…
#include “myheader.h” // 引用非标准库的头文件
…
void Function1(…); // 全局函数声明
…
class Box // 类结构声明
{
…
};
#endif
1.3 定义(.cpp) 文件的结构
定义文件有三部分内容:
(1) 定义文件开头处的版权和版本声明。
(2) 对一些头文件的引用。
(3) 程序的实现体(包括数据和代码)。
// 版权和版本声明,此处省略。
#include “person.h”// 引用头文件
…
// 全局函数的实现体
void Function1(…)
{
…
}
// 类成员函数的实现体
void Person::Draw(…)
{
…
}
1.4 头文件的作用
(1)通过头文件来调用库功能。
源代码不便(或不准)向用户公布时,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口怎么实现的。编译器会从库中提取相应的代码。
(2)头文件能加强类型安全检查。
如果某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,这一简单的规则能大大减轻程序员调试、改错的负担。
1.5 目录结构
如果项目的头文件比较多(超过10个) 可以将头文件和定义文件分开,例如将头文件放入include文件夹,源文件放入source文件夹.
如果某些头文件是私有的,它不会被用户的程序直接引用,则没有必要公开其“声明”。为了加强信息隐藏,这些私有的头文件可以和定义文件存放于同一个目录。
二、程序的版式
2.1 长行拆分
1)代码行最长控制在 70 ~ 80 个字符以内。
2)长表达式要在低优先级操作符处拆分成新行,操作符放在新行之首(以便突出操作符)。拆分出的新行要进行适当的缩进,使排版整齐,语句可读。
if ((very_longer_variable1 >= very_longer_variable12)
&& (very_longer_variable3 <= very_longer_variable14)
&& (very_longer_variable5 <= very_longer_variable16))
{
dosomething();
}
virtual CMatrix CMultiplyMatrix (CMatrix leftMatrix,
CMatrix rightMatrix);
for (very_longer_initialization;
very_longer_condition;
very_longer_update)
{
dosomething();
}
2.2类的版式
建议:
将 public 类型的函数写在前面,而将 private 类型的数据写在后面,采用这种版式的程序员主张类的设计“以行为为中心”,重点关注的是类应该提供什么样的接口(或服务)。
因为用户最关心的是接口,谁愿意先看到一堆私有数据成员!
三、命名规则
命名规范:
- windows:驼峰命名,Linux:下划线;
- 用英文别用拼音;
- 长度应当符合“min-length && max-information”原则;
- 不要出现标识符完全相同的局部变量和全局变量(虽然作用域不同,容易混淆);
- 变量的名字应当使用“名词”或者“形容词+名词”;如oldValue;
- 全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。
DrawBox(); // 全局函数
box->Draw(); // 类的成员函数
- 尽量避免名字中出现数字编号,如 Value1,Value2 等,除非逻辑上的确需要编号;
- 类名和函数名用大写字母开头的单词组合而成;
class Node; // 类名
class LeafNode; // 类名
void Draw(void); // 函数名
void SetValue(int value); // 函数名
9.变量和参数用小写字母开头的单词组合而成;
BOOL flag;
int drawMode;
10.常量全用大写的字母,用下划线分割单词;
const int MAX = 100;
const int MAX_LENGTH = 100;
11.如果不得已需要全局变量,则使全局变量加前缀 g_(表示 global)。
int g_howManyPeople; // 全局变量
int g_howMuchMoney; // 全局变
12.类的数据成员加前缀 m_(表示 member),这样可以避免数据成员与
成员函数的参数同名。
void Object::SetValue(int width, int height)
{
m_width = width;
m_height = height;
}
13.库函数可能会使用统一标识前缀,如QMap。
四、表达式和基本语句
- 记不住优先级,用括号!
- 不要编写太复杂的复合表达式。
- 不要有多用途的复合表达式;
例如: d = (a = b + c) + r ;
该表达式既求 a 值又求 d 值。应该拆分为两个独立的语句:
a = b + c;
d = a + r;
- bool类型判断真假
if (flag) // 表示 flag 为真
if (!flag) // 表示 flag 为假
其它的用法都属于不良风格,例如:
if (flag == TRUE)
if (flag == 1 )
if (flag == FALSE)
if (flag == 0)
5.整型变量与零值比较
应当将整型变量用“==”或“!=”直接与 0 比较。
if (value == 0)
if (value != 0)
不可模仿布尔变量的风格而写成:
if (value) // 会让人误解 value 是布尔变量
if (!value)
6.浮点变量与零值比较
不可将浮点变量用“==” 或 “!=”与任何数字比较。注意精度,应该设法转化成“>=”或“<=”形式。
假设浮点变量的名字为 x,
if (x == 0.0) // 隐含错误的比较
转化为
if ((x>=-EPSINON) && (x<=EPSINON))
其中 EPSINON 是允许的误差(即精度)。
7.指针变量与零值比较
应当将指针变量用“==”或“!=”与 NULL 比较。
指针变量的零值是“空”(记为 NULL)。尽管 NULL 的值与 0 相同,但是两者意义不
同。假设指针变量的名字为 p,它与零值比较的标准 if 语句如下:
if (p == NULL) // p 与 NULL 显式比较,强调 p 是指针变量
if (p != NULL)
不要写成
if (p == 0) // 容易让人误解 p 是整型变量
if (p != 0)
或者
if (p) // 容易让人误解 p 是布尔变量
if (!p)
五、常量
C语言里用#define定义常量,C++除了#define还可以用const。
5.1 const常量与#define的比较
const常量的优点:
(1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安
全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会 产生意料不到的错误(边际效应)。
(2) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。
C++可以完全用const取代#define宏。
5.2 常量定义规则
需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。
例如:
const float RADIUS = 100;
const float DIAMETER = RADIUS * 2;
5.3 类中的常量
有时我们希望某些常量只在类中有效。由于#define 定义的宏常量是全局的,不能达到目的,于是想当然地觉得应该用 const 修饰数据成员来实现。const 数据成员的确是存在的,但其含义却不是我们所期望的。const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。
不能在类声明中初始化 const 数据成员。以下用法是错误的,因为类的对象未被创
建时,编译器不知道 SIZE 的值是什么。
class A
{…
const int SIZE = 100; // 错误,企图在类声明中初始化 const 数据成员
int array[SIZE]; // 错误,未知的 SIZE
};
const 数据成员的初始化只能在类构造函数的初始化表中进行,例如
class A
{…
A(int size); // 构造函数
const int SIZE ;
};
A::A(int size) : SIZE(size) // 构造函数的初始化表
{
…
}
A a(100); // 对象 a 的 SIZE 值为 100
A b(200); // 对象 b 的 SIZE 值为 200
怎样才能建立在整个类中都恒定的常量呢?别指望 const 数据成员了,应该用类中
的枚举常量来实现。例如
class A
{…
enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量
int array1[SIZE1];
int array2[SIZE2];
};
枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:
它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如 PI=3.14159)。
六、函数设计
6.1 参数的规则
- 一般地,应将目的参数放在前面,源参数放在后面。
如果将函数声明为:
void StringCopy(char *strDestination, char *strSource);
- 如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该
指针在函数体内被意外修改。
例如:
void StringCopy(char *strDestination,const char *strSource);
- 如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
- 函数参数尽量控制5个以内。
6.2 返回值的规则
- 不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用 return 语句返回。
- 有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
- 不要返回局部对象的引用或指针。如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传
递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。
例如:
class String
{…
// 赋值函数
String & operate=(const String &other);
// 相加函数,如果没有 friend 修饰则只许有一个右侧参数
friend String operate+( const String &s1, const String &s2);
private:
char *m_data;
}
String 的赋值函数 operate = 的实现如下:
String & String::operate=(const String &other)
{
if (this == &other)
return *this;
delete m_data;
m_data = new char[strlen(other.data)+1];
strcpy(m_data, other.data);
return *this; // 返回的是 *this 的引用,无需拷贝过程
}
6.2 函数内部实现的规则
在函数的入口处和出口处严格把关。
- 在入口处检查参数是否非法,我们应该充分理解并正确使用“断言”(assert)。
- 在函数体的“出口处”,对 return 语句的正确性和效率进行检查。
a) return 语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数
体结束时被自动销毁。
b) 要搞清楚返回的究竟是“值”、“指针”还是“引用”。
c) 如果函数返回值是一个对象,要考虑 return 语句的效率。
return String(s1 + s2);
这是临时对象的语法,表示“创建一个临时对象并返回它”。不要以为它与“先创建
一个局部对象 temp 并返回它的结果”是等价的,如
String temp(s1 + s2);
return temp;
实质不然,上述代码将发生三件事。首先,temp 对象被创建,同时完成初始化;然
后拷贝构造函数把 temp 拷贝到保存返回值的外部存储单元中;最后,temp 在函数结束
时被销毁(调用析构函数)。然而“创建一个临时对象并返回它”的过程是不同的,编译
器直接把临时对象创建并初始化在外部存储单元中,省去了拷贝和析构的化费,提高了
效率。
类似地,我们不要将
return int(x + y); // 创建一个临时变量并返回它
写成
int temp = x + y;
return temp;
由于内部数据类型如 int,float,double 的变量不存在构造函数与析构函数,虽然该“临
时变量的语法”不会提高多少效率,但是程序更加简洁易读。
6.4 其它建议
- 函数的功能要单一,不要设计多用途的函数。
- 函数体的规模要小,尽量控制在 50 行代码之内。
- 尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在 C/C++语言中,函数的 static 局部变量是函数的“记忆”存储器。建议尽量少用 static 局部变量,除非必需。
- 不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。
- 用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。
6.5 使用断言
断言宏(assert)只在Debug模式起作用,用来检查**“不应该”**发生的情况。
void *memcpy(void *pvTo, const void *pvFrom, size_t size)
{
assert((pvTo != NULL) && (pvFrom != NULL)); // 使用断言
byte *pbTo = (byte *) pvTo; // 防止改变 pvTo 的地址
byte *pbFrom = (byte *) pvFrom; // 防止改变 pvFrom 的地址
while(size -- > 0 )
*pbTo ++ = *pbFrom ++ ;
return pvTo;
}
- 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况 之间的区别,后者是必然存在的并且是一定要作出处理的。
- 在函数的入口处,使用断言检查参数的有效性(合法性)。
- 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?” 一旦确定了的假定,就要使用断言对假定进行检查。
- 一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可 能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要 使用断言进行报警。
6.6 引用与指针的区别
“引用传递”的性质像“指针传递”,而书写方式像“值传递”。实际上“引用”可以做的任何事情“指针”也都能够做,为什么还要“引用”这东西?
答案是“用适当的工具做恰如其分的工作”。
指针能够毫无约束地操作内存中的如何东西,尽管指针功能强大,但是非常危险。就象一把刀,它可以用来砍树、裁纸、修指甲、理发等等,谁敢这样用?如果的确只需要借用一下某个对象的“别名”,那么就用“引用”,而不要用“指针”,以免发生意外。比如说,某人需要一份证明,本来在文件上盖上公章的印子就行了,如果把取公章的钥匙交给他,那么他就获得了不该有的权利。
七、内存管理
7.1 指针参数是如何传递内存的?
如果函数的参数是一个指针,不要指望用该指针去申请动态内存。示例 7-4-1 中,
Test 函数的语句 GetMemory(str, 200)并没有使 str 获得期望的内存,str 依旧是 NULL,
为什么?
void GetMemory(char *p, int num)
{
p = (char *)malloc(sizeof(char) * num);
}
void Test(void)
{
char *str = NULL;
GetMemory(str, 100); // str 仍然为 NULL
strcpy(str, "hello"); // 运行错误
}
毛病出在函数 GetMemory 中。编译器总是要为函数的每个参数制作临时副本,指针
参数 p 的副本是 _p,编译器使 _p = p 。如果函数体内的程序修改了_p 的内容,就导致
参数 p 的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p 申请
了新的内存,只是把_p 所指的内存地址改变了,但是 p 丝毫未变。所以函数 GetMemory
并不能输出任何东西。事实上,每执行一次 GetMemory 就会泄露一块内存,因为没有用
free 释放内存。
如果非得要用指针参数去申请内存,那么应该改用“指向指针的指针”:
void GetMemory2(char **p, int num)
{
*p = (char *)malloc(sizeof(char) * num);
}
void Test2(void)
{
char *str = NULL;
GetMemory2(&str, 100); // 注意参数是 &str,而不是 str
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
由于“指向指针的指针”这个概念不容易理解,我们可以用函数返回值来传递动态
内存。这种方法更加简单,
char *GetMemory3(int num)
{
char *p = (char *)malloc(sizeof(char) * num);
return p;
}
void Test3(void)
{
char *str = NULL;
str = GetMemory3(100);
strcpy(str, "hello");
cout<< str << endl;
free(str);
}
用函数返回值来传递动态内存这种方法虽然好用,但是常常有人把 return 语句用错
了。这里强调不要用 return 语句返回指向“栈内存”的指针,因为该内存在函数结束时
自动消亡.
为什么 free 函数不象 malloc 函数那样复杂呢?这是因为指针 p 的类型以及它所指
的内存的容量事先都是知道的,语句 free§能正确地释放内存。如果 p 是 NULL 指针,
那么 free 对 p 无论操作多少次都不会出问题。如果 p 不是 NULL 指针,那么 free 对 p
连续操作两次就会导致程序运行错误。
八、 C++函数的高级特性
对比于 C 语言的函数,C++增加了重载(overloaded)、内联(inline)、const 和 virtual
四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,const 与 virtual 机制仅用于类的成员函数。
8.1 函数重载的概念
函数的参数不同(个数,类型)的函数互相重载。
编译器会通过参数修改函数的名字来区分不同的重载函数。例如:foo(int,int);会转换成像_foo_int_int
的函数。
关于C函数foo() 的调用吗,告诉 C++编译译器,函数 foo 是个 C 连接,应该到库中找名字_foo 而不是找
_foo_int_int。C++编译器开发商已经对 C 标准库的头文件作了 extern“C”处理,所以我们可以用#include 直接引用这些头文件。
8.2 运算符重载
在 C++语言中,可以用关键字 operator 加上运算符来表示函数,叫做运算符重载。例如两个复数相加函数:
Complex Add(const Complex &a, const Complex &b);
可以用运算符重载来表示:
Complex operator +(const Complex &a, const Complex &b);
运算符与普通函数在调用时的不同之处是:对于普通函数,参数出现在圆括号内;而对于运算符,参数出现在其左、右侧。例如
Complex a, b, c;
…
c = Add(a, b); // 用普通函数
c = a + b; // 用运算符 +
如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。
如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有一个右侧参数,因为对象自己成了左侧参数。
8.3 内联函数
C++ 语言的函数内联机制既具备宏代码的效率,又增加了安全性,而且可以自由操
作类的数据成员。所以在 C++ 程序中,应该用内联函数取代所有宏代码,“断言 assert”
恐怕是唯一的例外。assert 是仅在 Debug 版本起作用的宏,它用于检查“不应该”发生
的情况。为了不在程序的 Debug 版本和 Release 版本引起差别,assert 不应该产生任何
副作用。如果 assert 是函数,由于函数调用会引起内存、代码的变动,那么将导致 Debug
版本与 Release 版本存在差异。所以 assert 不是函数,而是宏。
内联函数不应该定义在声明文件中。
将成员函数的定义体放在类声明之中虽然能带来书写上的方便,但不是一种良好的编程
风格,下面是良好的风格:
// 头文件
class A
{
public:
void Foo(int x, int y);
}
// 定义文件
inline void A::Foo(int x, int y)
{
…
}
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
九、类的构造函数、析构函数与赋值函数
既然能自动生成函数,为什么还要程序员编写?
原因如下:
(1)如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会,C++发明人 Stroustrup 的好心好意白费了。
(2)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。