您的位置:首页 > 编程语言 > C语言/C++

高质量C++编程指南学习笔记

2007-06-25 20:13 471 查看
1.1 文件结构

1.1.1 版权和版本的声明

版权和版本声明位于头文件或者实现文件的开头,具体内容包括:
Ø 版权信息
Ø 文件名称、标识符和摘要
Ø 当前版本号,作者/修改者,完成日期
Ø 版本的历史信息
例如以下模板:
/*
* Copyright (c) 2007,长沙威胜电子有限公司电能质量事业部
* All rights reserved.
*
* 文件名称:filename.h
* 文件标识:见配置管理计划书
* 摘 要:简要描述本文件的内容
*
* 当前版本:1.1
* 作 者:输入作者(或修改者)名字
* 完成日期:2007年6月18日
*
* 取代版本:1.0
* 原作者 :输入原作者(或修改者)名字
* 完成日期:2007年6月10日
*/

1.1.2 头文件的结构

头文件的结构包括三个部分:
² 版权和版本说明(参见1.1.1)
² 预处理块
² 数据结构和函数声明
为防止头文件被重复引用,每一个头文件要用#ifndef/#define/#endif结构产生预处理块;
以#include <stdio.h>形式引用头文件时,编译器将从头文件的标准库目录中搜寻该文件,以#include “stdio.h”形式引用头文件时,编译器将从程序目录中搜寻该文件;
为便于管理以及实现信息隐藏之目的,头文件中只存放函数的声明而不存放函数的实现代码;
尽量不要在头文件中使用全局变量,如extern int a等。

1.1.3 实现文件的结构

定义文件也包括三部分,即:
Ø 版权和版本声明(参见1.1.1)
Ø 对相应头文件的引用
Ø 数据及实现代码

1.1.4 头文件的作用

头文件主要有以下作用:
² 可以用头文件来调用标准库功能,实现代码的隐藏
² 头文件能加强入口参数类型检查

1.1.5 目录结构

为方便代码的管理,可以将程序的头文件放在include目录下,将实现文件放在source目录下。

1.2 程序的版式

程序的版式主要是为使程序结构清晰明了而作的工作,例如适当的空行和对齐(长行拆分,短行不要合并)可以使程序更容易理解。
函数之间要有空行,函数内逻辑关系进的语句之间不要空行,逻辑关系不紧密的语句之间要加空行。每一个语句段(‘{}’括起来的语句)要将‘{’和‘}’对齐。各个代码段要缩进对齐,如以下代码:

void main()
{
int sum = 0;

for (int i=0; i<10; i++)
{
sum += I;
}
}
void main(){

int sum = 0;

for (int i=0; i<10; i++){

sum += I;
}
}

很明显第一个mian函数的结构更加清晰。

变量一定要赋初值,当然这里的一定其实不一定,但是为防止在程序运行时出现莫名其妙又很难查到的bug,则很有可能是由于变量未赋初值引起的。所以在声名变量时对齐赋初值是一个很好的习惯。

修饰符的位置因人而异,例如以下两种声明变量的方法:
int* p;
int *p;
当用第一种方法声明时,如果同时声明两个变量,则容使人误解。如:
int* p, q; //容易使人误以为q也是int型指针变量

适当的注释是使程序容易理解最好的手段,但注释不要太过琐碎。但注释要写的准确和简洁,容易引起误解的注释无胜于有。

类的版式有两种:
Ø 以数据为中心的人会把类中的变量定义放在前面
Ø 以行为为中心的人会把类中的函数定义放在前面
我们要以行为为中心,加入我们要做一个标准库,则提供给别人的都是方法的接口,别人并不关心你的方法是如何实现的(也没有必要)。

1.3 命名规则

1.3.1 共性规则

所谓共性规则就是被广大程序员所接纳的规则,如:
l 标识符应直观可读,可望文知意
很多中国的程序员以拼音命名法,当然这种方法肯定是有其不合理之处,但是仍为很多人所用,何解?
l 标识符应符合max-length和max-information原则
l 命名规则应尽量和操作系统和开发工具的命名规则保持一致
例如Windows操作系统的函数一般是大小写结合的方式,如AddNode(),而Linux操作系统则一般采用下划线连接的方式,如add_node()。
l 程序中不要出现仅仅以大小写区分的函数或变量
l 类名、结构体名和函数名以大写开头,大小写结合的方式,变量名以小写开头,大小写结合的方式?
l const常量全部以大写命名,静态变量以s_开头,全局变量以g_开头
l 类的成员变量以m_开头,表示为类的成员(member)

1.3.2 团队规则

团队进行开发之前,在遵照以上标准的前提下,应该给全体成员提供代码撰写规则文档。

1.4 表达式和基本语句

1.4.1 运算符优先级

有一个最简单的不用记运算符优先级的方法就是使用括号,但是过度的使用括号会使程序看起来晦涩难懂,所以适当的记住一些运算符优先级是有必要的。
一些记忆优先级的准则:
Ø 一元运算符的优先级高于二元运算符
Ø 算数运算符优先级高于逻辑运算符
Ø ()、[]、->和“.”运算符的优先级最高

1.4.2 复合表达式

例如以下赋值语句:
a = b = c = 1;
是一个完全正确的赋值语句,但最好不要用这种复合型的表达式,而应该分别赋值。

1.4.3 if语句

if语句中变量值与零值的比较:
Ø 布尔变量与零值比较
if (flag)或if (!flag)
Ø 整型值与零值比较
if (x == 0)或if (x != 0)
Ø 浮点值与零值比较
if (x >= -EPSINON) && (x <= EPSINON),即对浮点型数据要允许一定的精度偏差,EPSINON即为偏差精度。
Ø 指针与零值比较
if (p == NULL)或if (p != NULL)

if语句段中包含return语句:

if (condition)
{
return x;
}
else
{
return y;
}
if (condition)
{
return x;
}

return y;

以上左边为规则的写法,右边为不规则的写法,更为简洁的写法是:return (condition ? x:y)

1.4.4 循环语句的效率

如果程序中包含多重循环,循环的顺寻为由内到外循环次数逐渐减少,即将循环次数最多的循环放在最里面。
如果循环体内包含逻辑判断,则在循环次数较多时须将逻辑判断放在循环外部,否则会打断CPU的流水线作业。
对for循环语句,尽量不要在循环内部修改循环变量的值。

1.4.5 switch语句

合理的利用switch可以使程序更简洁。不要忘记每一个case的break语句,除非有意为之。

1.4.6 goto语句

尽量少用或者不用goto语句,但是在特殊情况下利用goto语句能起到意想不到的效果,例如跳出多重循环,否则则需要很多的break语句来完成此功能。

1.5 常量

尽量将程序中用到次数很多的不变值的变量定义为const常量,这样一旦该变量值发生改变,则只需要修改常量定义即可。

1.5.1 #define与const常量

尽量定义const常量,原因是const常量带类型,而#define常量不带类型,而且有些调试器可以调试const常量,但是#define常量不可以调试。
常量值如果密切相关的话,应在定义时包括这种关联关系,而不要分别赋值,如:
const float R = 10.0;
const float PI = 3.14;
const float AREA = PI * R * R;

1.5.2 类中的常量

类中的const常量只能在类的构造函数的初始化表中赋值,如:
class A
{⋯
A(int size); // 构造函数
const int SIZE ;
};
A::A(int size) : SIZE(size) // 构造函数的初始化表
{

}
A a(100); // 对象 a 的SIZE 值为100
整个类中值固定的常量只能通过枚举变量实现,enum( RED = 1; BLUE = 2),但是枚举常量只能为短整型,这是其不足之处。

1.6 函数设计

函数接口的两个要素是参数和返回值,C++中的参数有三种传递方式,值传递、指针传递和引用传递。

1.6.1 参数规则

参数的命名要简洁规范,在声名函数时,要将参数名称写完整,如果函数没有参数,则要用void补充。
函数参数的顺序安排要合理,如字符串拷贝函数strcpy(char* pDst,const char* pSrc),如果写成了strcpy(const char* pSrc, char* pDst),则别人用该函数时很可能顺手写成strcpy(szNew,szOld),从而颠倒了参数的调用。
如果函数输入参数为指针,且仅作输入用,则应加const修饰,防止被以外修改。
如果函数输入参数为值传递,应用const &修饰,以增强函数的执行效率。
避免使用过多的输入参数。

1.6.2 返回值规则

不要图省事而省略参数的返回值,如果确实没有返回值,则应声明为void。
不要将正常值和错误参数一起返回,最好是正常值用输出参数获取,错误参数用返回值得到。
赋值函数要声明为引用类型,以提高效率。
相加函数要声明为值传递类型,因为是引用类型的话,返回的是指向局部对象的变量,当函数执行完毕后该对象释放,得不到正确的值。

1.6.3 函数内部实现的规则

在函数的入口处对参数进行有效性检查,适当使用断言机制,当然断言机制只在调试中起作用,但是那会使我们更容易发现错误并加以改正。
在函数返回处进行返回值检查,如:
char* get(void)
{
char szTmp[] = “hello world!”;
return szTmp;
}
以下几个原则要特别注意:
² return语句不可以返回指向栈内存的“指针”或者“引用”;
² 要弄清楚要返回的是值、指针还是引用;
² 如果返回的是对象,请考虑return语句的效率;

return string(s1+s2);

string strTmp;
strTmp = string(s1+s2);
return strTmp;

比较上面两段代码,右边的代码在执行时比左边的多了构造和析构两个过程,效率要低很多,尤其是该对象为一个很复杂的对象的时候。
尽量编写功能单一的函数;
函数体内的语句如果过长,考虑是否应将函数拆分;
不要使程序有记忆功能,相同的输入应该有相同的输出,除非有意为之。

1.6.4 引用与指针的区别

u 引用在被创建的同时必须被初始化;
u 所引用的对象一旦指定就不可再更改;
u 不能有NULL引用,即引用必须指向一个具体的对象;
u 对引用变量所作的一切改动,都会反映在该变量所引用的变量身上。

1.7 内存管理

1.7.1 内存分配方式

内存分配方式有三种:
1. 在静态存储区分配。内存在程序编译时就已经分配好,在整个程序存在期间一直存在,直到程序结束才被释放,如全局变量和static常量;
2. 在栈上分配内存。函数内部的变量在执行时会临时分配内存,当函数执行完毕自动释放所分配的内存;
3. 在堆上分配内存。用malloc和new申请的内存都是在堆上分配的内存,使用完毕要用free和delete手动释放内存。这种方式很灵活,但是问题也最多。

1.7.2 常见的内存错误

常见的内存错误有以下几种:
l 内存未分配成功,却使用了它;
l 内存分配成功,但是未被初始化;
l 内存分配成功且被初始化,但是使用时超过了所分配的界限;
l 分配了动态内存,但使用完毕未释放,造成内存泄漏;
l 释放了内存后却继续使用它;
响应的对策为:
l 使用malloc和new申请内存后立即用if (p == NULL)来做检查;
l 要养成对内存进行初始化的习惯,哪怕只是微不足道的小字符串;
l 对内存界限要特别注意循环过程中的多1或者少1的错误;
l 动态内存的申请必须与释放语句配对使用;
l 动态内存被释放后,要立即将其置为NULL,防止产生野指针,因为if (p == NULL)语句检查不出这类错误。

1.7.3 指针与数组

数组或者在静态存储区被创建(如全局数组),或者在栈上被创建,数组是占用一块内存区域,而不是指向一块内存区域,一旦被创建,就不能更改内存区域,只能更改存储内容。
指针可以指向随意大小的内存块,而且可以更换指向的区域,使用起来更加灵活。
指向常量的指针不能更改存储区内的内容;
数组不能用=语句进行赋值,但是指针可以,指针的=操作实际就是改变指针所指向的内存区,而不是真正意义上的复制;
数组内容的比较也不能用==,而只能用strcmp来进行;
用sizeof计算内存容量时,指针变量的大小都是4,而数组则是实际数组内存的大小。

1.7.4 利用指针参数传递内存

比较下面三段语句:

void GetMemory(char* p)
{

p = new char[10];
}
void GetMemory(char** p)
{
p = (char*)(new char[10]);

}
char* GetMemory(void){
char* p;
p = new char[10];
return p;
}

第一个函数是在栈上申请的内存,函数结束后被释放了,所以失败,第二个虽然也是,但是因为是指向指针的指针申请的,所以会成功,但是很难理解,第三个是用返回值申请的内存,而且是成功的。

char* GetMemory2(void)
{
char szTmp[] = “hello world”;
return szTmp; //指向了栈内存,最终使返回的指针所指向的区域内容为垃圾
}
char* GetMemory2(void)
{
char* p = “hello world”;
return p; //指向了常量内存,无论何时使用该函数申请内存都将得到一块常量内存区,不可更改内容
}

1.7.5 动态内存的释放

动态内存必须手动释放,内存被释放了,并不代表指向该内存的指针为NULL指针了,所以当未将释放的指针赋值为NULL时,利用if (p == NULL)检查不出问题来。
malloc和free不能处理非内部数据类型的情况,而且由于其实库函数而不是运算符,编译器不能控制它,导致很多问题检查不出来,而new和delete是运算符,编译器可以控制。
如果用new创建了对象数组,则在释放时必须写成delete []objects。
使用new创建对象数组时,只能使用不带参数的构造对象。

1.8 C++函数的高级特性

1.8.1 函数的重载

C++比C语言多了重载(overloaded)、内联(inline)、const和virtual四种机制。
如果C++要调用已经编译过了的C函数,要像下面这样写:
extern “C”
{
void foo(int x, int y);
}
这个语句告诉编译器,要用的函数是标准的C函数。
函数重载的一个最主要特征就是函数名称相同,但是要注意的是,并不是所有重名的函数都构成了重载,重名的全局函数和类的成员函数就不是重载,类的函数被全局函数覆盖了,全局函数前面加::加以区别。
另外要注意的是要当心自动类型转换带来的语义二义性特征,如:
void func(int x);
void func(float x);
如果在以下情况中使用时:
func(0.5);
这种情况编译器无法区分具体调用哪个函数,所以要用以下形式调用:
func(float(0.5));

1.8.2 成员函数的重载,覆盖与隐藏

成员函数被重载的特征有:
1、在同一个类中
2、函数名字相同
3、参数不同
4、virtual关键字可有可无
5、
成员函数被覆盖的特征:
1、不在同一个类中
2、函数名字相同
3、参数相同
4、virtual关键字必须有
5、
成员函数被隐藏的特征有:
1、不在用一个类中
2、函数名字相同
3、参数不同
4、virtual关键字可有可无?

1.8.3 参数的缺省值

l 缺省值只能出现在函数的定义中,不能出现在函数的实现中
l 缺省参数只能从后往前挨个缺省,否则将会使函数在调用时不正确

1.8.4 运算符重载

l 重载的运算符必须是C++中原有的,但是不包括.
l 运算符重载虽然看起来怪模怪样,但是本质上和其他成员函数是一样的
Complex operator +(const Complex& a, const Complex& b);

1.8.5 内联函数

宏不能操作类的数据成员,C++中的内联机制完全取代了宏定义,而且增加了类型检查,还可以自如的操纵类的数据成员。
inline是一种用于实现的关键字,在声名函数时加inline关键字而在实现时不加不会使该函数成为内联函数,反之,在函数的实现代码前加inline关键字,则无论在声名函数时是否加inline关键字,函数都成为了内联函数。
另外,在类声名中直接写了实现代码的函数自动成为内联函数。
内联函数不要过分使用,当执行内联函数代码时的开销比调用函数的开销大,则用内联函数是不明智的。
当内联函数内有循环时,最好不要用使之成为内联函数。
类的构造和析构代码不要加在类的声名中。

1.9 类的构造、析构和赋值

1.9.1 构造函数的初始化表

构造函数初始化表的使用规则:
u 如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数
u 类中的const常量只能在初始化表里进行初始化
u 类的成员函数可以在初始化表里或者函数体内被赋值

1.9.2 构造和析构的次序

构造从类层次的根处开始,在每一层中,首先调用基类的构造函数,然后调用成员对象的构造函数。析构则严格按照与构造相反的次序执行,该次序是唯一的,否则编译器将无法自动执行析构过程。
对类成员变量的初始化顺序并不是按照初始化表进行的,而是按照变量的声明顺序来的。
典型的构造析构和赋值函数:
//不带参数的构造函数
String::String()
{
m_Data = NULL;
}

//带参数的构造函数
String::String(const char* pStr)
{
if ( psz == NULL )
{
m_data = new char[1];
*m_data = '/0';
}
else
{
m_data = new char[strlen(pStr)+1];
strcpy(m_data, pStr);
}
}

String::~String()
{
delete []m_data;
}

String& String::operator =(const String& str)
{
if (this == &str) //防止自我复制
return *this;

if (m_data != NULL) //释放内存
{
delete []m_data;
m_data = NULL;
}

m_data = new char[strlen(str.m_data)+1];
strcpy(m_data, str.m_data);

return *this;
}

1.10 类的继承与组合

1.10.1 继承

Ø 毫不相关的两个类不要生硬的继承
Ø 若在逻辑关系上B是A的一种,并且A的所有数据及行为对B都有意义,则此时B可以从A继承

1.10.2 组合

Ø 若在逻辑关系上,B是A的一部分,则不允许B从A继承,只能用B和其他的类组合成A

1.11 其他的一些经验

1.11.1 使用const关键字提高程序健壮性

Ø 使用const关键字修饰参数
const关键字不仅仅可以用来定义常量,跟主要的是被用来修饰参数和函数返回值,甚至是函数的定义体(查找一下例子);
如果参数做输出用,无论该参数是引用参数还是指针参数,都不能用const修饰;
如果参数为输入参数,而且采用值传递,则无论用不用const,都不会使程序效率提高;
如果参数为输入参数,且采用引用传递,则为防止被引用的参数被以外改变,则加const修饰;
如以下函数:
void Func1(int num);
void Func1(const int& num);
这两个函数效率一样,因为int为内部数据类型,不存在构造,析构等过程,但假如函数为下面这样:
void Func1(A a);
void Func1(const A& a);
A为自定义的类,则后一个函数效率明显要高。
Ø 使用const修饰函数返回值
如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针;
如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const 修饰没有任何价值;
函数返回值采用“引用传递”的场合并不多,这种方式一般只出现在类的赋值函数中,目的是为了实现链式表达。如:A & operate = (const A &other);
Ø 任何不会修改数据成员的函数都应该声明为const 类型。
如果在编写const 成员函数时,不慎修改了数据成员,或者调用了其它非const 成员函数,编译器将指出错误,这无疑会提高程序的健壮性。
class Stack
{
public:
void Push(int elem);
int Pop(void);
int GetCount(void) const; // const 成员函数
private:
int m_num;
int m_data[100];
};
int Stack::GetCount(void) const
{
++ m_num; // 编译错误,企图修改数据成员m_num
Pop(); // 编译错误,企图调用非const 函数
return m_num;
}

1.11.2 提高程序的效率

以下是几条对提高程序效率有帮助的建议:
u 不要一味地追求程序的效率,应当在满足正确性、可靠性、健壮性、可读性等质量因素的前提下,设法提高程序的效率。
u 以提高程序的全局效率为主,提高局部效率为辅。
u 在优化程序的效率时,应当先找出限制效率的“瓶颈”,不要在无关紧要之处优化。
u 先优化数据结构和算法,再优化执行代码。
u 有时候时间效率和空间效率可能对立,此时应当分析那个更重要,作出适当的折衷。例如多花费一些内存来提高性能。
u 不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: