《高质量C、C++》笔记整理
本文章会随着编程能力的提高和思维的扩展不断更新,目前是1.0版本 2024/6/25
简介
本书以高质量编写C/C++代码为主题,在代码风格等方面列举了常见的规则和示例,旨在帮助读者写出风格统一的、可读性强的、易于维护的代码。
代码风格
什么是优秀的、高质量的代码?
- 优秀的编程风格
- 有出错处理
- 算法复杂度分析
文章结构
版权与版本的声明
版权和版本的声明:
位于头文件(.h)和定义文件(.cpp或者.c)的开头,主要内容有:
- 版权信息.
- 文件名称,标识符,摘要.
- 当前版本号,作者/修改者,完成日期.
- 版本历史信息.
示例1-1:
1 | /* |
头文件的结构
- 头文件开头处的版权和版本声明(参见示例1-1).
- 预处理块.
- 函数和类结构声明等.
规则;
- <>导入标准库的头文件,“” 导入非标准库的头文件
- 头文件只存放声明而不放定义
示例1-2 使用ifndef
1 | //假设头文件名称为graphics.h |
特点:能跨平台,但是编译时间较长,而且一旦宏名重复就会“不知所措“
示例1-2 使用#pragma once
1 |
特点:编译时间短,但是难以跨平台.
定义文件的结构
- 定义文件开头处的版权和版本声明(参见示例1-1).
- 对一些头文件的引用.
- 程序的实现体(包括数据和代码).
假设定义文件的名称为graphics.cpp,定义文件的结构参见示例1-3.
示例1-3
1 | //版权和版本声明见示例1-1,此处省略. |
目录结构
如果一个软件的头文件数目比较多(如超过十个),通常应将头文件和定义文件分别保存于不同的目录,以便于维护.例如可将头文件保存于include目录,将定义文件保存于source目录(可以是多级目录).
程序的版式
空行
适当留空行保持呼吸感
规则:
- 在每个类声明之后、每个函数定义结束之后都要加空行.
- 在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔.参见示例2-1
1 | //逻辑连续,不空行 |
代码行
规则
- 一行代码只做一件事情,如只定义一个变量,或只写一条语句.这样的代码容易阅读,并且方便于写注释.
- if、for、while、do等语句自占一行,执行语句不得紧跟其后.不论执行语句有多少都要加{}.这样可以防止书写失误.
- 尽可能定义变量的同时初始化该变量.
总而言之一句话,不要一行就写完
示例2-2
1 | //在定义变量的同时初始化变量 |
代码行内的空格
- 关键字之后要留空格.象const、virtual、inline、case等关键字之后至少要留一个空格(不留空格一般会报错),否则无法辨析关键字.象if、for、while等关键字之后应留一个空格再跟左括号‘(’,以突出关键字.
- 函数名之后不要留空格
- ‘(’向后紧跟,‘)’、‘,’、’‘;’向前紧跟,紧跟处不留空格.
- ‘,’之后要留空格,如Function(x,y,z).如果‘;’不是一行的结束符号,其后要留空格,如for(initialization; condition; update).
- 赋值操作符、比较操作符、算术操作符、逻辑操作符、位域操作符,如“=”、“+=”“>=”、“<=”、“+”、“*”、“%”、“&&”、“‖”、“<”,“Λ”等二元操作符的前后应当加空格.(在VS等编译器会自动格式化,但是也要养成良好的习惯)
- 一元操作符如“”!“~”、“++”、“–”、“&”(地址运算符)等前后不加空格.
- 象“[]”、“.”、“->”这类操作符前后不加空格.
- 对于表达式比较长的for语句和if语句,为了紧凑起见可以适当地去掉一些空格,如for(i=0;i<10;i++)和if(a<=b)&&(c<=d)
也就是说:在有大量操作符或者是关键字的情况下留空格避免过于紧凑.对于需要紧凑的符号就不要留空(比如&,this等)
示例 以下皆为优良代码风格
1 | void Funcl(int x,int y,int z); |
对齐
规则简单来说就是{}单独占一行并且同一级的要对齐.
示例
1 | void func(string name) |
长行拆分
- 一行最多70-80字
- 拆分时将操作符放在首位,要缩进排版
1 | if ((very_longer_variablel >very_longer_variablel2) |
修饰符的位置
主要是解决多个变量定义时的歧义问题
规则:*与&紧靠变量名
1 | char *name; |
注释
规则
- 一行用//,多行用/ * * /
- 注释不是文档,需要的时候才加上
- 边写代码边注释,注意更新
- 注释要写在所解释的代码的上方或者右方
- 较长的代码要在结尾处指明是谁的结束.
示例
1 | if() |
类的版式
类的版式主要有关心结构的“以数据为中心”风格和“以行为为中心“风格,这里提倡**“以行为为中心“风格**,毕竟在大部分情况下我们只关心怎样使用,有什么功能,而不会在乎里面是什么.
一句话:将public类型的函数放在前面,将private的数据放在后面.
示例:
1 | class A |
命名
在实际工作中以企业的开发手册为主
规则
- 不要用拼音,不用写完整英文翻译,也不要用中式英语命名
- 统一命名风格,要么驼峰命名法,要么匈牙利命名法,不要混着用.这里采用驼峰命名法
- 扩展:驼峰命名法:
- 变量一般用小驼峰法标识.驼峰法的意思是:除第一个单词之外,其他单词首字母大写
- 大驼峰法把第一个单词的首字母也大写了.常用于类名,函数名,属性,命名空间
- 少用易混淆的标识符,比如x与X,o与0
- 变量的名字("名词“或者”形容词+名词“)
- 全局函数的名字(“动词“或者"动词+名词”),类中的函数命名(“动词”),因为"自带"名词.
- 可以用反义词组表示具有相反意义的变量或者函数(如set与get)
- 避免数字编号;比如num1;
Windows命名规则
- 常量全大写,下划线分割单词.如int MAX_NUM = 100;
- 静态变量加s_前缀,全局变量加g_前缀,类成员加m_前缀 如int static s_num = 100;
- 对于不同库的变量可以添加反映库特性的前缀,如openGL,用gl_做前缀
表达式和基本语句
运算符的优先级 && 复合表达式
规则
- 适当用括号表示运算顺序
- 不要写复杂、多用途的表达式,除非你是出面试题的考官.比如
d = (a=b+c++)+r;
if语句
规则
- 不要将布尔值或者是0,1值直接进行比较. 比如能写
if(flag)
就不要写if(flag == true)
写if(value == 0)
就不要写if(value == false)
虽然false和0值相等,但是类型不同 - 浮点数不要用
==
!=
比较.因为浮点数精度问题 - 指针一般和NULL比较,或者和nullptr(C++11特性)比较
- 建议比较的时候将确定的数放在 == 之前,防止错写成=而无法发觉.
示例
1 | int a = 10; |
循环语句的效率
规则
- 建议将最长的循环放在内层,减少CPU跨切循环层的次数
- 逻辑判断最好在循环外面
示例
1 | //不用for循环里面每次都判断条件,提高了效率 |
for循环与switch多分支
规则
- 不要轻易在for里面改变循环变量
- for循环控制变量取值采用前闭后开的方法.能写
for (int i = 0;i < 5;i++)
就不要写`for (int i = 0;i <=4;i++) - 每个case都要写break防止case击穿.保留default语句
goto
- 慎用goto,用的好能跳出重重封锁(比如嵌套循环),用不好就万劫不复
常量
使用常量的好处在于一劳永逸,见文知意
规则
- 用常量表示多次出现的数字或者字符串
- 建议用const代替#define,方便调试.
- 公开的常量放在头文件中,不公开的放在定义文件中
- 常量之间有关联的用常量代替而不是用数字
示例
1 |
|
类中设计常量
不能直接用const,建议使用枚举
示例
1 | class A |
函数设计
规则
形参名
- 形参名要写完整不要省,没有就用void
- 形参的顺序要合理(比如source与destination,除数与被除数)
- 如果传参是值的话建议用const&,省去复制、构造与析构的过程
- 形参最好不要超过5个,而且参数最好有确定的类型与数目.
返回值
- 任何函数都要有类型,不返回就用void
- 函数名与返回值在语义方面不要冲突.比如别用getInt这种名字返回char型
- 对于赋值函数,使用“引用传递”返回对象;对于操作函数,使用“值传递”的方式返回对象.也就是根据实际情况选择值传递或者引用传递
函数
- 函数入口要检查-使用断言assert
- 函数出口要检查return
- 不能返回指向“栈内存”的“指针”或者“引用”
- 清楚返回的是什么
- 如果返回的是对象可以直接写 “创建对象并返回” 如: return Entity(s1); 这在效率上比 Entity temp(s1); return temp;要高得多.
- 函数功能单一、短小精悍、避免记忆、出错处理要清晰
断言
- 一般用于捕捉非法情况而不是错误情况
- 防错设计
比较——引用与指针
- 引用是取别名,指针是地址
- 引用被创建的时候必须初始化(如
string& name = m_Name;
- 引用不能为空;并且引用初始化之后不能改变引用的关系.
内存管理
规则
- 用malloc或new申请内存之后,应该立即检查指针值是否为NULL.防止使用指针值为NULL的内存.
- 不要忘记为数组和动态内存赋初值.防止将未被初始化的内存作为右值使用.
- 避免数组或指针的下标越界,特别要当心发生“多1”或者“少1”操作.
- 动态内存的申请与释放必须配对,防止内存泄漏.
- 用free或delete释放了内存之后,立即将指针设置为NULL,防止产生“野指针”.
1 | int main() |
内容复制与比较
规则
- 数组与字符串等不能轻易用== 判断相等(因为== 只比较值而没有比较地址),除非重载了运算符
- 当数组作为函数的参数进行传递时,数组自动退化成同类型的指针
1 | char a[] = "hello"; |
free 与delete
- 这“两大护法”并没有把指针干掉,而是释放内存,地址还在("一屋传三代,人走屋还在_(:з」∠)“)
指针的问题
- 指针消亡了,并不表示它所指的内存会被自动释放.
- 内存被释放了,并不表示指针会消亡或者成了NULL指针.
C中的malloc/free与C++中的new/delete
- new和delete会自动调用构造或者析构函数(如果有的话),而malloc与free没有
- C语言只有malloc与free.
内存耗尽问题
虽然对于32位及以上的系统很难将内存耗尽,但是也需要做好报错处理(不能因为国力强盛就不修长城)
处理方法
- 如果申请的内存为空,用return或者exit(1)终止
- 或者自定义处理函数 比如try catch
函数讲解
malloc
函数原型为:
void * malloc(size_t size);
讲解
- void* 是泛指针,如果要调用的时候要将其显示转换
- size是内存大小,一般用sizeof求出
使用示例
int *p = (int*)malloc(sizeof(int)*length);
free
函数原型为
void free(void * memblock)
new
new内置了sizeof、类型转换、类型安全检查等功能,建议多使用
delete
不要漏掉[]
举例
1 | delete []objects;//正确 |
心得
- 越是害怕,越是要勇敢面对它(特指指针),战胜恐惧的最好方法就是面对恐惧,加油,奥利给
- 犯错不可怕,走过万千路,天堑变通途.
C++高级特性
重载与覆盖(overload and override)
特征
类型 | 范围 | 函数名 | 参数 | 是否有virtual关键字 |
---|---|---|---|---|
重载 | 在同一个类中 | 相同 | 不同 | 可有可无 |
覆盖 | 在基类与派生类中 | 相同 | 相同 | 必须有 |
示例
1 |
|
错误高发区:隐藏
简单来说就是想通过派生类调用基类的函数却发现调用的都是派生类的函数,导致编译失败,在以下情况下会发生
- 派生类的函数与基类的函数同名,但是参数不同
- 派生类的函数与基类的函数同名,参数相同,但是基类函数没有virtual
参数的缺省值
缺省值就是默认值(default),中文翻译不准确,尽可能使用英文.
规则
- 参数的default值只能出现在函数的声明中
- default值从后往前写
运算符重载
函数内联(inline)
函数内联具有宏效率,又有效解决了宏无法调试和安全性的问题.建议在C++中使用内联取代宏(assert除外)
使用说明
- inline必须必须和函数定义体放在一起才能使函数成为内联函数.
1 | inline int add(const int& x,const int& y) |
- 特别的:虽然在类中定义的函数自动变成内联函数,但是一般不会在类中定义函数,如果需要内联记得在定义处加上inline.
- 内联是用空间换时间
类的构造函数、析构函数和赋值函数
简单示例
1 | class String |
类的继承与组合
简单讲解对象(object)、类(class)和实例(instance)的关系
对象就是“房子”,类就是设计房子的“蓝图”,对象是类的一个实例,根据“蓝图”可以盖很多的“房子”.
所谓面向对象编程,就是设计“图纸”.
继承的规则
- 不相关的两个类不要继承(“公若不弃,愿拜为义父”doge)
- 在逻辑上B是A的一种,并且A的所有属性和功能都对A有意义,就允许B继承A.比如虽然鸵鸟是鸟,但是“飞”的功能对鸵鸟是多余的,这种情况下最好不要继承与鸟
组合的规则
- 若在逻辑上A是B的“一部分”(a part of),则不允许B从A派生,而是要用A和其它东西组合出B.比如电池、屏幕和芯片是手机的一部分,就用这些类组合起来生成手机类,而不是让手机“拜电池等为义父”
示例
1 | class Cell |
编程经验
常用const
注意
- const只能修饰输入参数
- 对于值传递的参数无需使用const修饰
- 对于非内部数据类型(就是C|C++标准库之外的类型)的输入参数.可以使用const 类型& 的方式在提高效率的同时不改变参数内容.
- 比如int确实没必要使用const int &.,内部数据类型没有构造与析构,复制也很快,不需要多此一举
- 函数返回值如果是值传递,不需要用const修饰
- 不修改成员属性的函数都要加const
个人思考
写代码就像是写作文,理论上只要表达意思正确(也就是程序能运行并达到理想效果),什么首行缩进、分段都不影响文章的质量,但是作文是要修改的(程序要维护),是要给人看的(别人要使用你的程序),此时语句(代码)就是交流的主要工具,语句的逻辑是什么?怎样表达就能让对方理解?怎样吸引读者阅读你的文章?怎样减少语病与错误?这是我们在写作文的时候经常思考的问题,编程也是一样.
接下来就从写好一篇文章开始讲解代码是什么.首先,写作者的名字,这就引出了“版权和版本“的声明,表明作品是你写的,作品处于哪个阶段.然后,给人物取名字,一般老师会让学生用什么典雅的古文等方法给人物取名,不过在计算机这里就不适用了,要简洁直白,能叫"many"就不要用"quite a number of",又不是给英语作文凑字数(doge)
然后就要开始准备工作了,如果我们写的是人物传记,就要交代人物名字及其相关信息(声明及初始化变量),比如“廉颇者,赵之良将也”,声明一个Person类的变量名为“廉颇”,并将“赵国”“良将”作为参数构造了一个Person的实例对象——人.有的还会在正文之前写时间地点等,比如“元丰六年,余左迁……”(出自《琵琶行》)就是定义(define)”今年“为”元丰六年“,以后所有的”今年“都会被”元丰六年“等效代替.
以下以《廉颇蔺相如列传》第一段为例.
接下来我用“保安三问”(你是谁?你去哪?你干嘛?)来讲解如何写代码.
“廉颇者,赵之良将也”.首先,第一问“廉颇是谁?” ,我们需要给廉颇一个定义,是人是神还是鬼,是符号还是具体的事物?理论上都可以,但是从复用的角度和理解的角度来看,用“人”更加通俗易懂,即定义成“人”类(class)或者是结构体,这里以面向对象为主要编程思想,采用”类“来构造一个”人“. 然后,声明和定义一个“人”类,由于高质量编程的需要,最好将声明与定义放在不同的文件里面,类比图书放在不同的类别,都是为了后续减少维护成本.但是构造类的过程中有一点问题,类里面应该放点什么东西呢?我的建议是,随着”故事“的展开不断更新”人“类,毕竟人是会成长的,想要一开始一劳永逸只会顾此失彼.
第二问“廉颇去哪?” 一般来说,所有的代码都直接或者间接在main函数里面.这里的“廉颇”也不例外.
第三问”廉颇干嘛?“ 第一句没有交代廉颇的行为,但是交代了廉颇的”特性“(属性),这就提醒”人“类可以”更新“了,比如添加”国籍(nationality)“、”地位(status)“属性并公开(双料特工可以不公开doge),不过又有问题了,这些属性应该是哪一种变量呢?string、int还是class呢?我的建议是,在日常生活中常以文字出现的用字符串(char[] string等),以数字出现的(比如成绩)用整型(比如int)或者浮点型(比如double).不过仅作参考,合适的才是最好的.好了,现在我们给“人”类添加了nationality和status属性,但是我们要“构造”一个“廉颇”啊,不能只把“廉颇”的特性给女娲却不告诉她怎样“玩泥巴”吧,也就是声明并定义有参构造(教女娲如何用泥巴塑造廉颇),怎样定义呢?“廉颇”要什么,就给女娲什么,廉颇要名字、要身份、要国籍,就给他名字(m_name = name;).现在,“人”类的基本蓝图已经有了,也告诉女娲怎样“捏一个人类“了,接下来就要将廉颇的特性传递给构造函数了.如下,成功创建了”廉颇“.用cout输出一下其特性,完美.
廉颇有了,要干什么呢?伐齐!这是一个动作,可以用一个函数来表示,那么函数应该是类中的函数还是外面的函数呢(保安第二问)?我的建议是,看谁复用性更强,毕竟人都喜欢偷懒,这里用类中的函数,毕竟之后蔺相如”人“也要”攻打“齐国.对于类的定义与实现,也是同理的保安三问,是什么?在哪里?干什么?是什么——返回什么类型,要什么参数,名字是什么;在哪里——函数声明与定义在哪里;干什么——实现了什么功能.不停追问自己,然后不断完善代码.伐字实现了,又到”齐“了,看起来好像是string,又感觉可以是class,用什么比较好呢?我的建议是,从宏观看”齐“的”地位“,即”齐“出现频率高不高,包含的东西多不多,以此配位,从全文来看,”齐“明显是高频词汇,应该重视,string不配修饰”齐“,那就用类(class)吧.同理保安三问创建”国家(country)“类.添加防御力(m_defenseValue)和名字(name)属性等.
下一句是“大破之”是结果,但是过程是什么呢?也就是结果的“因”是什么呢?这里实现的方法就见仁见智了,可以通过数值的比较,可以是特定的属性触发,甚至可以随机判定输赢,这里就用游戏中常用的“数值”来比较,用判断语句(if)判定.另一个结果是”拜为上卿,以勇气闻於诸侯“,这里强调的属性是”上卿“”闻(出名的意思)“看”人“类中有没有,有一个status,但是没有”出名“的属性,可以添加一个,根据保安三问定律,取”出名“的属性为int,名字是m_fame,用数值代表出名程度.然后在if中添加结果(将m_fame的值提升,将m_status的值改变),最后根据m_fame的值输出语句.至此,程序完结.
示例如下
1 | //在Person.h中 |
1 | //在Person.cpp文件中(也就是实现类中) |
“赵惠文王十六年”
int to_year = 16;
“廉颇为赵将伐齐”
1 | //在Country.h 文件中 |
1 | //在Country.cpp |
“大破之,取阳晋,拜为上将,以勇气闻於诸侯”
1 | //在main.cpp文件里面 |
看到这里,你会有一个问题,为什么要用写作来形容代码的编写过程呢?对以中文为母语的人可能难以理解,但是对于以英语为母语的人早已习以为常,甚至达到了“美国人刚出生就会写代码”的程度.对他们来说,写代码就是写英语作文,无非是语法更严格,字数变更少,语句更加讲逻辑,对我们来说就是在“写天书”.这里想说的是,将写代码看做是写英语作文,可以有效提高写代码的逻辑性,而且,一旦你认为自己写的是一篇作文,要传阅给所有人看,你就会不自觉“美化”你的作文(代码),在无形之中,你对自己写的作文(代码)提高了要求。我以写作为例,更多的是想让你感受一下保安三问定律在写代码方面的作用和按照一定的规则写代码的好处,毕竟代码就是语句,多行代码构成段落,多个段落构成故事,故事都有起因经过结果,保安定律就是为了回答这三者究竟是什么,规则限定我们写故事写在哪,怎样写易读性更强。也许自己的故事有缺陷,也许编程能力有限,但总是能引导我们走向文章的结尾,给老师(编译器)和他人一个满意的答卷.
参考资料
- 《高质量C、C++》 林锐
- 《C++ Prime Plus(第六版)》