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

 

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-lengthmax-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可以使程序更简洁。不要忘记每一个casebreak语句,除非有意为之。

1.4.6 goto语句

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

1.5 常量

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

1.5.1 #defineconst常量

尽量定义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.        在堆上分配内存。用mallocnew申请的内存都是在堆上分配的内存,使用完毕要用freedelete手动释放内存。这种方式很灵活,但是问题也最多。

1.7.2 常见的内存错误

常见的内存错误有以下几种:

l        内存未分配成功,却使用了它;

l        内存分配成功,但是未被初始化;

l        内存分配成功且被初始化,但是使用时超过了所分配的界限;

l        分配了动态内存,但使用完毕未释放,造成内存泄漏;

l        释放了内存后却继续使用它;

响应的对策为:

l        使用mallocnew申请内存后立即用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)检查不出问题来。

mallocfree不能处理非内部数据类型的情况,而且由于其实库函数而不是运算符,编译器不能控制它,导致很多问题检查不出来,而newdelete是运算符,编译器可以控制。

如果用new创建了对象数组,则在释放时必须写成delete []objects

使用new创建对象数组时,只能使用不带参数的构造对象。

1.8 C++函数的高级特性

1.8.1 函数的重载

C++C语言多了重载(overloaded)、内联(inline)constvirtual四种机制。

如果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 继承

Ø        毫不相关的两个类不要生硬的继承

Ø        若在逻辑关系上BA的一种,并且A的所有数据及行为对B都有意义,则此时B可以从A继承

1.10.2 组合

Ø        若在逻辑关系上,BA的一部分,则不允许BA继承,只能用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      不要追求紧凑的代码,因为紧凑的代码并不能产生高效的机器码。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值