【高质量编程指南笔记】

本文详细介绍了C++编程中的文件结构、程序版式、命名规则、表达式和基本语句、常量、函数设计、内存管理、以及函数的高级特性,如函数重载、运算符重载和内联函数。特别强调了头文件的作用、版权和版本声明、目录结构以及内联函数的安全性和效率。此外,还讨论了类的构造函数、析构函数和赋值函数的重要性和注意事项。
摘要由CSDN通过智能技术生成

一、文件结构

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 类型的数据写在后面,采用这种版式的程序员主张类的设计“以行为为中心”,重点关注的是类应该提供什么样的接口(或服务)
因为用户最关心的是接口,谁愿意先看到一堆私有数据成员!

三、命名规则

命名规范:

  1. windows:驼峰命名,Linux:下划线;
  2. 用英文别用拼音;
  3. 长度应当符合“min-length && max-information”原则;
  4. 不要出现标识符完全相同的局部变量和全局变量(虽然作用域不同,容易混淆);
  5. 变量的名字应当使用“名词”或者“形容词+名词”;如oldValue;
  6. 全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。类的成员函数应当只使用“动词”,被省略掉的名词就是对象本身。
DrawBox(); // 全局函数
box->Draw(); // 类的成员函数
  1. 尽量避免名字中出现数字编号,如 Value1,Value2 等,除非逻辑上的确需要编号;
  2. 类名和函数名用大写字母开头的单词组合而成;
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。

四、表达式和基本语句

  1. 记不住优先级,用括号!
  2. 不要编写太复杂的复合表达式。
  3. 不要有多用途的复合表达式;

例如: d = (a = b + c) + r ;
该表达式既求 a 值又求 d 值。应该拆分为两个独立的语句:
a = b + c;
d = a + r;

  1. 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 参数的规则

  1. 一般地,应将目的参数放在前面,源参数放在后面。
    如果将函数声明为:
void StringCopy(char *strDestination, char *strSource);
  1. 如果参数是指针,且仅作输入用,则应在类型前加 const,以防止该
    指针在函数体内被意外修改。
    例如:
void StringCopy(char *strDestination,const char *strSource);
  1. 如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率。
  2. 函数参数尽量控制5个以内。

6.2 返回值的规则

  1. 不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用 return 语句返回。
  2. 有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值。
  3. 不要返回局部对象的引用或指针。如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传
    递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错。
    例如:
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 函数内部实现的规则

在函数的入口处出口处严格把关。

  1. 在入口处检查参数是否非法,我们应该充分理解并正确使用“断言”(assert)。
  2. 在函数体的“出口处”,对 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 其它建议

  1. 函数的功能要单一,不要设计多用途的函数。
  2. 函数体的规模要小,尽量控制在 50 行代码之内。
  3. 尽量避免函数带有“记忆”功能。相同的输入应当产生相同的输出。带有“记忆”功能的函数,其行为可能是不可预测的,因为它的行为可能取决于某种“记忆状态”。这样的函数既不易理解又不利于测试和维护。在 C/C++语言中,函数的 static 局部变量是函数的“记忆”存储器。建议尽量少用 static 局部变量,除非必需。
  4. 不仅要检查输入参数的有效性,还要检查通过其它途径进入函数体内的变量的有效性,例如全局变量、文件句柄等。
  5. 用于出错处理的返回值一定要清楚,让使用者不容易忽视或误解错误情况。

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;
}
  1. 使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况 之间的区别,后者是必然存在的并且是一定要作出处理的。
  2. 在函数的入口处,使用断言检查参数的有效性(合法性)。
  3. 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?” 一旦确定了的假定,就要使用断言对假定进行检查。
  4. 一般教科书都鼓励程序员们进行防错设计,但要记住这种编程风格可 能会隐瞒错误。当进行防错设计时,如果“不可能发生”的事情的确发生了,则要 使用断言进行报警。

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)“缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冷月无声~

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值