C/C++编程质量提升笔记

《高质量C++/C编程指南-林锐》笔记

编程风格

文件结构

  1. 头文件

    • 版权和版本声明
    • 预处理块 ifndef/define/endif
    • 头文件引用<>标准库""非标准库
  2. 定义文件

    • 版权和版本声明
    • 引用头文件
    • 程序实现体

程序版式

  1. 空行
    • 规则2-1-1】在每个类声明之后、每个函数定义结束之后都要加空行。
    • 规则2-1-2】在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
  2. 代码行
    • 规则2-2-1】一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
    • 规则2-2-2】if、for、while、do等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。这样可以防止书写失误
  3. 行内空格
    • 规则2-3-1】关键字之后要留空格。象const、virtual、inline、case 等关键字之后至少要留一个空格,否则无法辨析关键字。象if、for、while等关键字之后应留一个空格再跟左括号‘(’,以突出关键字。
    • 规则2-3-2】函数名之后不要留空格,紧跟左括号‘(’,以与关键字区别。
    • 规则2-3-3】‘(’向后紧跟,‘)’、‘,’、‘;’向前紧跟,紧跟处不留空格。
    • 规则2-3-4】‘,’之后要留空格,如Function(x, y, z)。如果‘;’不是一行的结束符号,其后要留空格,如for (initialization; condition; update)。
    • 规则2-3-5】赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、“+=”“>=”、“<=”、“+”、“*”、“%”、“&&”、“||”、“<<”,“^”等二元操作符的前后应当加空格。
    • 规则2-3-6】一元操作符如“!”、“~”、“++”、“–”、“&”(地址运算符)等前后不加空格。
    • 规则2-3-7】象“[]”、“.”、“->”这类操作符前后不加空格
  4. 其他
    • {}独占一行并于母语句对齐
    • 长行拆分
    • 修饰符*&紧贴变量名
    • 类版式:先public再private

命名规则

  1. 命名规则尽量与所采用的操作系统或开发工具的风格保持一致。例如Windows应用程序的标识符通常采用“大小写”混排的方式,如WritterGacding。而Unix应用程序的标识符通常采用“小写加下划线”的方式,如writter_gacding。别把这两类风格混在一起用。
  2. “匈牙利”命名规则
    • 开头字母用变量类型的缩写,其余部分用变量的英文或英文的缩写,且每个单词的第一个字母都大写,例如创建变量"gacding"就要加上类型缩写person->p,变成"pGacding"

表达式和基本语句

  1. 注意运算符优先级,加括号明晰顺序
  2. 不滥用复合表达式
  3. 语句
    • if
      • 不可将布尔变量直接与TRUE、FALSE或者1、0进行比较
      • 应当将整型变量用“==”或“!=”直接与0比较。
      • 不可将浮点变量用“==”或“!=”与任何数字比较,应该设法转化成“>=”或“<=”形式。
      • 应当将指针变量用“==”或“!=”与NULL比较
    • for
      • 在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU跨切循环层的次数
      • 如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。
      • 不可在for 循环体内修改循环变量,防止for 循环失去控制
      • 建议for语句的循环控制变量的取值采用“半开半闭区间”写法,即使用<而非<=
    • switch
      • 每个case语句的结尾不要忘了加break
      • 不要忘记最后那个default分支
    • goto
      • 少用慎用

常量

  1. const定义常量好于#define
    • const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
    • 有些集成化的调试工具可以对const常量进行调试,但是不能对宏常量进行调试
  2. 定义规则
    • 规则5-3-1】需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
    • 规则5-3-2】如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值
    • 不能在类声明中初始化const数据成员。以下用法是错误的,因为类的对象未被创建时,编译器不知道SIZE的值是什么。const数据成员的初始化只能在类构造函数的初始化表中进行。建立在整个类中都恒定的常量呢?别指望const数据成员了,应该用类中的枚举常量来实现。

函数设计

  1. 参数规则
    • 参数的书写要完整,命名要恰当,顺序要合理:一般地,应将目的参数放在前面,源参数放在后面
    • 如果参数是指针,且仅作输入用,则应在类型前加const,以防止该指针在函数体内被意外修改
    • 如果输入参数以值传递的方式传递对象,则宜改用“const &”方式来传递,这样可以省去临时对象的构造和析构过程,从而提高效率
    • 参数个数尽量控制在5个以内
    • 尽量不要使用类型和数目不确定的参数
  2. 返回值规则
    • 不要省略返回值的类型
    • 函数名字与返回值类型在语义上不可冲突
    • 不要将正常值和错误标志混在一起返回。正常值用输出参数获得,而错误标志用return语句返回
    • 有时候函数原本不需要返回值,但为了增加灵活性如支持链式表达,可以附加返回值
    • 如果函数的返回值是一个对象,有些场合用“引用传递”替换“值传递”可以提高效率。而有些场合只能用“值传递”而不能用“引用传递”,否则会出错
  3. 函数内部规则
    • 在函数体的“入口处”,对参数的有效性进行检查,充分理解并正确使用“断言”(assert)来防止此类错误
      • 使用断言捕捉不应该发生的非法情况
      • 在函数的入口处,使用断言检查参数的有效性
      • 在编写函数时,要进行反复的考查,并且自问:“我打算做哪些假定?”一旦确定了的假定,就要使用断言对假定进行检查
//复制不重叠内存块
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;
}
  • 在函数体的“出口处”,对return语句的正确性和效率进行检查
    • return语句不可返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁
    • 要搞清楚返回的究竟是“值”、“指针”还是“引用”
    • 如果函数返回值是一个对象,要考虑return语句的效率。尽量使用临时对象,节省拷贝和析构的花费。即return String(s1+s2);好于String temp(s1+s2);return temp;
  1. 引用
    • 引用 int &n = m; 即别名
      • 引用的主要功能是传递函数的参数和返回值
    • 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
    • 不能有NULL引用,引用必须与合法的存储单元关联
    • 一旦引用被初始化,就不能改变引用的关系

专题

内存管理

  1. 内存分配方式
    • 静态存储区域
      • 全局变量、static变量
      • 函数内局部变量的存储单元
      • 指令集分配内存,高效低容
      • 动态内存分配
      • malloc/new申请
      • free/delete释放
  2. 常见错误
    • 内存分配未成功,却使用了它
      • 在使用内存之前检查指针是否为NULL。如果指针p是函数的参数,那么在函数的入口处用assert(p!=NULL)进行检查。如果是用malloc或new来申请内存,应该用if(p==NULL) 或if(p!=NULL)进行防错处理。
    • 内存分配虽然成功,但是尚未初始化就引用它
    • 内存分配成功并且已经初始化,但操作越过了内存的边界
    • 忘记了释放内存,造成内存泄露
    • 释放了内存却继续使用它
      • 函数的return语句写错了,注意不要返回指向“栈内存”的“指针”或者“引用”,因为该内存在函数体结束时被自动销毁
      • 使用free或delete释放了内存后,没有将指针设置为NULL。导致产生“野指针”
  3. 指针和数组差异
    • 数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。数组名对应着(而不是指向)一块内存,其地址与容量在生命期内保持不变,只有数组的内容可以改变。指针可以随时指向任意类型的内存块,它的特征是“可变”,所以我们常用指针来操作动态内存。指针远比数组灵活,但也更危险
    • 内容修改
      • char *p = “world”; 注意p指向常量字符串。这里与数组不同导致不能修改其内容
    • 内容复制与比较
    • 用运算符sizeof可以计算出数组的容量(字节数),但只能算出指针变量的字节数
      • 当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针
  4. 指针
    • 指针参数申请内存
      • void GetMemory(char *p, int num){p = (char *)malloc(sizeof(char) * num);}。编译器总是要为函数的每个参数制作临时副本,指针参数p的副本是 _p,编译器使 _p = p。如果函数体内的程序修改了_p的内容,就导致参数p的内容作相应的修改。这就是指针可以用作输出参数的原因。在本例中,_p申请了新的内存,只是把_p所指的内存地址改变了,但是p丝毫未变。所以函数GetMemory并不能输出任何东西。事实上,每执行一次GetMemory就会泄露一块内存,因为没有用free释放内存
      • 更改则应该使用GetMemory(char **p,int num){}并且参数GetMemory(&str,100)
    • 用函数返回值来传递动态内存
      • 不要用return语句返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡
  5. 野指针
    • “野指针”不是NULL指针,是指向“垃圾”内存的指针
    • 野指针产生
      • 指针变量没有被初始化
      • 指针操作超越了变量的作用范围
      • free和delete只是把指针所指的内存给释放掉,但并没有把指针本身干掉。指针p被free以后其地址仍然不变(非NULL),只是该地址对应的内存是垃圾,p成了“野指针”。如果此时不把p设置为NULL,会让人误以为p是个合法的指针。对于野指针判断if(p!=NULL)起不到防错作用。
    • 指针消亡了,并不表示它所指的内存会被自动释放。
    • 内存被释放了,并不表示指针会消亡或者成了NULL指针
  6. malloc/free和new/delete
    • 区别
      • malloc与free是C++/C语言的标准库函数,new/delete是C++的运算符
      • 对于非内部数据类型的对象,malloc/free不能自动执行构造函数和析构函数。而new/delete因为是运算符所以可以
    • malloc/free
      • int *p = (int *) malloc(sizeof(int) * length)malloc默认返回void类型所以需要转换,使用sizeof而非自己算int字节数
      • free(p)如果p是NULL指针,那么free对p无论操作多少次都不会出问题。如果p不是NULL指针,那么free对p连续操作两次就会导致程序运行错误。
    • new/delete
      • new创建对象数组,那么只能使用对象的无参数构造函数
      • 用delete释放对象数组时,留意不要丢了符号‘[]’。例如delete []objects; 正确的用法;delete objects;错误的用法
  7. 内存耗尽
    • 如果在申请动态内存时找不到足够大的内存块,malloc和new将返回NULL指针,宣告内存申请失败
    • 判断
      • 判断指针是否为NULL,如果是则马上用return语句终止本函数/用exit(1)终止整个程序的运行
      • 为new和malloc设置异常处理函数
    • 对于32位以上的应用程序而言,无论怎样使用malloc与new,几乎不可能导致“内存耗尽”。因为32位操作系统支持“虚存”,内存用完了,自动用硬盘空间顶替。

高级特性

  1. 函数重载
    • 以将语义、功能相似的几个函数用同一个名字表示,即函数重载
    • 实现
      • 区分:编译器根据参数为每个重载函数产生不同的内部标识符
        • 因此C++编译后的函数在库中的名字与C不一致。C++程序要调用已经被编译后的C函数:需要使用extern “C”{void foo(int x, int y);或#include “myheader.h”}
      • 并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的作用域不同。类的某个成员函数要调用全局函数Print,为了与成员函数Print区别,全局函数被调用时应加‘::’标志。如::Print(…); // 表示Print是全局函数而非成员函数
      • 当心隐式类型转换导致重载函数产生二义性,尽量在实参中避免隐式转换
    • 重载、覆盖、隐藏
      • 成员函数被重载的特征:
        • (1)相同的范围(在同一个类中);
        • (2)函数名字相同;
        • (3)参数不同;
        • (4)virtual关键字可有可无。
      • 覆盖是指派生类函数覆盖基类函数,特征是:
        • (1)不同的范围(分别位于派生类与基类);
        • (2)函数名字相同;
        • (3)参数相同;
        • (4)基类函数必须有virtual关键字。
      • “隐藏”指派生类的函数屏蔽了与其同名的基类函数:
        • (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆);
        • (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
//(1)函数Derived::f(float)覆盖了Base::f(float)。
//(2)函数Derived::g(int)隐藏了Base::g(float),而不是重载。
//(3)函数Derived::h(float)隐藏了Base::h(float),而不是覆盖。
#include <iostream.h>
class Base
{
public:
virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
void g(float x){ cout << "Base::g(float) " << x << endl; }
void h(float x){ cout << "Base::h(float) " << x << endl; }
};
class Derived : public Base
{
public:
virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
void g(int x){ cout << "Derived::g(int) " << x << endl; }
void h(float x){ cout << "Derived::h(float) " << x << endl; }
};
void main(void)
{
Derived d;
Base *pb = &d;
Derived *pd = &d;
// Good : behavior depends solely on type of the object
pb->f(3.14f); // Derived::f(float) 3.14
pd->f(3.14f); // Derived::f(float) 3.14
// Bad : behavior depends on type of the pointer
pb->g(3.14f); // Base::g(float) 3.14
pd->g(3.14f); // Derived::g(int) 3 (surprise!)
// Bad : behavior depends on type of the pointer
pb->h(3.14f); // Base::h(float) 3.14 (surprise!)
pd->h(3.14f); // Derived::h(float) 3.14
}

//摆脱隐藏
//隐藏导致错误
class Base
{
public:
void f(int x);
};
class Derived : public Base
{
public:
void f(char *str);
};
void Test(void)
{
Derived *pd = new Derived;
pd->f(10); // error
}
//修改
class Derived : public Base
{
public:
void f(char *str);
void f(int x) { Base::f(x); }
};
  1. 参数缺省值
    • 规则8-3-1】参数缺省值只能出现在函数的声明中,而不能出现在定义体中
    • 规则8-3-2】如果函数有多个参数,参数只能从后向前挨个儿缺省,否则将导致函数调用语句怪模怪样。
    • 不合理地使用参数的缺省值将导致重载函数output产生二义性
  2. 运算符重载
    • 以用关键字operator加上运算符来表示函数,叫做运算符重载,Complex operator +(const Complex &a, const Complex &b);
    • 对于普通函数,参数出现在圆括号内;而对于运算符,参数出现在其左、右侧
    • 如果运算符被重载为全局函数,那么只有一个参数的运算符叫做一元运算符,有两个参数的运算符叫做二元运算符。如果运算符被重载为类的成员函数,那么一元运算符没有参数,二元运算符只有一个右侧参数,因为对象自己成了左侧参数。
    • 不能重载
      • 内部数据类型的运算符
      • 运算符.
      • 没有的符号
      • 重载后优先级不变
  3. 函数内联
      • 宏在预处理阶段使用文本替换的方式。没有类型检查,作用域控制。
      • 优点:预处理器用复制宏代码的方式代替函数调用,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了速度。
      • 缺点:1.容易出错,预处理器在复制宏代码时常常产生意想不到的边际效应。2.无法操作类的私有数据成员。
    • 内联函数
      • 内联函数是在编译阶段由编译器决定是否将函数调用展开为函数体代码。具有类型检查,遵循 C++ 作用域规则。
      • 除了assert宏以外,应当使用内联函数取代所有宏代码
      • 内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。以下情况不宜使用内联:
        • (1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
        • (2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大
        • 不要随便地将构造函数和析构函数的定义体放在类声明中。
    • void Foo(int x, int y);inline void Foo(int x, int y) // inline与函数定义体放在一起{…}
      • inline是一种“用于实现的关键字”,而不是一种“用于声明的关键字”
      • 定义在类声明之中的成员函数将自动地成为内联函数

类的构造函数、析构函数与赋值函数

  1. 每个类只有一个析构函数和一个赋值函数,但可以有多个构造函数(包含一个拷贝构造函数,其它的称为普通构造函数)。
    • 对于任意一个类Gacding,如果不想编写上述函数,C++编译器将自动为Gacding产生四个缺省的函数,如
      • Gacding(void); // 缺省的无参数构造函数
      • Gacding(const Gacding &a); // 缺省的拷贝构造函数
      • ~Gacding(void); // 缺省的析构函数
      • Gacding & operate =(const Gacding &a); // 缺省的赋值函数
    • 如果使用“缺省的无参数构造函数”和“缺省的析构函数”,等于放弃了自主“初始化”和“清除”的机会
    • “缺省的拷贝构造函数”和“缺省的赋值函数”均采用“位拷贝”而非“值拷贝”的方式来实现,倘若类中含有指针变量,这两个函数注定将出错。
    • 构造函数与析构函数的另一个特别之处是没有返回值类型,这与返回值类型为void的函数不同
  2. 构造函数
    • 初始化表
      • ClassName(parameters) : member1(expression1), member2(expression2), ..{// 构造函数体}
      • 如果类存在继承关系,派生类必须在其初始化表里调用基类的构造函数
      • 类的const常量和引用只能在初始化表里被初始化,因为它不能在函数体内用赋值的方式来初始化
      • 非内部数据类型的成员对象应当采用初始化表初始化,以获取更高的效率
    • 构造析构次序
      • 成员对象初始化的次序完全不受它们在初始化表中次序的影响,只由成员对象在类中声明的次序决定。因为初始化表不唯一,使得析构次序不唯一。
      • 构造次序:基类->成员对象
      • 析构次序:成员对象->基类
    • 不想编写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数:将拷贝构造函数和赋值函数声明为私有函数,不用编写代码.
// String的普通构造函数
String::String(const char *str)
{
if(str==NULL)
{
m_data = new char[1];
*m_data = μ\0;
}
else
{
int length = strlen(str);
m_data = new char[length+1];
strcpy(m_data, str);
}
}
// String的析构函数
String::~String(void)
{
delete [] m_data;
// 由于m_data是内部数据类型,也可以写成 delete m_data;
}
// 拷贝构造函数
String::String(const String &other)
{
// 允许操作other的私有成员m_data
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
}
// 赋值函数
String & String::operate =(const String &other)
{
// (1) 检查自赋值,不能写成if( *this == other)
if(this == &other)
return *this;
// (2) 释放原有的内存资源
delete [] m_data;
// (3)分配新的内存资源,并复制内容
int length = strlen(other.m_data);
m_data = new char[length+1];
strcpy(m_data, other.m_data);
// (4)返回本对象的引用
return *this;
}

类继承和组合

  1. 继承
    • 如果类A和类B毫不相关,不可以为了使B的功能更多些而让B继承A的功能和属性。不要觉得“白吃白不吃”
    • 若在逻辑上B是A的“一种”(a kind of ),并且A的所有功能和属性对B而言都有意义,则允许B继承A的功能和属性
  2. 组合
    • 若在逻辑上A是B的“一部分”(a part of),则不允许B从A派生,而是要用A和其它东西组合出B

其他经验

  1. 使用const提高函数的健壮性
    • 修饰函数参数
      • 只能修饰用作输入功能的参数,会使参数失去输出功能(不可修改性,只读性)
      • 如果输入参数采用“指针传递”,那么加const修饰可以防止意外地改动该指针,起到保护作用
      • 如果输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const修饰
      • 对于非内部数据类型的参数而言,象void Func(A a) 这样声明的函数注定效率比较低,void Func(const A &a)可以提高效率并保持不改变参数a
    • 修饰函数返回值
      • 如果给以“指针传递”方式的函数返回值加const修饰,那么函数返回值(即指针)的内容不能被修改,该返回值只能被赋给加const修饰的同类型指针
      • 如果函数返回值采用“值传递方式”,由于函数会把返回值复制到外部临时的存储单元中,加const修饰没有任何价值
      • 如果返回值不是内部数据类型,将函数A GetA(void) 改写为const A & GetA(void)的确能提高效率。减少一次的构造函数调用。但相应的需要保证返回使用时局部对象依旧存在,可以使用static。
    • 常量成员函数
      • 任何不会修改数据成员的函数都应该声明为const类型
      • 常量成员函数的限制是,它不能修改所属对象的任何非 mutable 成员变量,并且只能调用其他的 常量成员函数,即那些也使用了 const 关键字的函数。如果希望某些数据成员可以在常量成员函数中被修改,可以使用 mutable 关键字。
      • const成员函数的声明看起来怪怪的:const关键字只能放在函数声明的尾部int GetCount(void) const; // const成员函数
  2. 其他建议
    • 建议11-3-1】当心那些视觉上不易分辨的操作符发生书写错误。我们经常会把“==”误写成“=”,象“||”、“&&”、“<=”、“>=”这类符号也很容易发生“丢1”失误。然而编译器却不一定能自动指出这类错误。
    • 建议11-3-2】变量(指针、数组)被创建之后应当及时把它们初始化,以防止把未被初始化的变量当成右值使用。
    • 建议11-3-3】当心变量的初值、缺省值错误,或者精度不够。
    • 建议11-3-4】当心数据类型转换发生错误。尽量使用显式的数据类型转换(让人们知道发生了什么事),避免让编译器轻悄悄地进行隐式的数据类型转换。
    • 建议11-3-5】当心变量发生上溢或下溢,数组的下标越界。
    • 建议11-3-6】当心忘记编写错误处理程序,当心错误处理程序本身有误。
    • 建议11-3-7】当心文件I/O有错误。
    • 建议11-3-8】避免编写技巧性很高代码。
    • 建议11-3-9】不要设计面面俱到、非常灵活的数据结构。YAGNI 原则。
    • 建议11-3-10】如果原有的代码质量比较好,尽量复用它。但是不要修补很差劲的代码,应当重新编写。
    • 建议11-3-11】尽量使用标准库函数,不要“发明”已经存在的库函数。
    • 建议11-3-12】尽量不要使用与具体硬件或软件环境关系密切的变量。
    • 建议11-3-13】把编译器的选择项设置为最严格状态。
    • 建议11-3-14】如果可能的话,使用PC-Lint、LogiScope等工具进行代码审查。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值