最后修改时间:2024/07/14
程序设计基础(C&C++)| 第一章:绪论
推荐书籍:
- C与C++程序设计
- C程序设计语言
- 明解C语言
- C程序设计
- Accelerated C++
- C++ Primer
- Essential C++
软件与程序
计算机硬件与软件
硬件是什么?以我们平常使用的台式机为例:
![](https://img-blog.csdnimg.cn/direct/f28e66389df24ffda6420f97e55139e5.png)
- 显示器
- 主板
- CPU
- 内存条
- 显卡
- 电源
- 光驱
- 硬盘
- 键盘
- 鼠标
如果只有计算机是没什么意义的,必须安装操作系统和各种应用程序。
程序是什么?是为实现特定目标或解决特定问题而用计算机语言编写的命令序列的集合。为实现预期目的而进行操作的一系列语句和指令。
程序,一般分为系统程序和应用程序两大类。
软件是什么?是一系列按照特定顺序组织的计算机数据和指令的集合。简单的说,软件就是程序加文档的集合体。软件包含程序。
软件,一般分为编程语言、系统软件、应用软件和这两者之间的中间体。
软件并不只是包括可以在计算机(这里的计算机,指广义的计算机)上运行的电脑程序,与这些电脑程序相关的文档一般也被认为是软件的一部分。
程序设计是什么?给出解决特定问题程序的方法和过程,是软件构造活动的重要组成部分。
程序设计过程应当包括:
需求分析 -> 设计 -> 编码 -> 测试 -> 维护
What to do How to do Code Test Maintainence
五个阶段,并生成各种文档资料。
程序设计语言
计算机语言,可以分为机器语言、汇编语言和高级语言这三大类。
电脑每做的一次动作、一个步骤,都是按照已经用计算机语言编好的程序来执行的。
程序是计算机要执行的指令集合,所以人们要控制计算机要通过计算机语言向计算机发出命令。
机器语言,是计算机能识别的语言,即由0和1构成的代码。但人们编程的时候无法使用机器语言,因为2进制对于人来说非常难以记忆和识别。
汇编语言,其实质和机器语言是相同的,都是直接对硬件进行操作,只不过指令采用了英文缩写的标识符,更容易识别和记忆。它同样需要编程者把每一步具体的操作用命令的形式写出来。
高级语言的程序主要是描述计算机的解题过程,即描述复杂的加工处理过程,所以也称高级语言为面向过程语言。
计算机不能直接执行高级语言的程序(源程序),通常有解释方式和编译方式两种方法在计算机上执行程序(目标程序):
- 解释
解释类的执行类似于日常生活中的同声翻译应用程序的源代码:一边由相应的语言解释器翻译成目标代码,一边执行。因此,效率相对较低,而且无法生成可独立执行的可执行文件。应用程序无法脱离这个解释器。但,这种方式也相对灵活,可以动态调整、修改。
- 编译
通过编辑程序写出源程序后,通过编译程序得到目标代码,通过链接程序得到可直接执行的可执行程序。目标程序可以脱离语言环境独立执行,使用相对方便、效率较高。但应用程序一旦需要修改,必须从修改源代码开始,再重新一步步得到新的可执行程序。
许多编程语言都是编译型的,比如C、C++等等。
C语言是高级语言之一。C语言适合做什么?
- 编写操作系统和基础工具
- 对运行效率要求较高的系统:设备驱动程序,高性能、实时中间件,嵌入式领域,并发程序设计等
- 继承和维护已有的C代码
软件开发环境与编程
开发环境部署
本人使用 clion
进行开发,创建c语言项目可以参考这个文档 ^1^。
编程实践
1. hello world
认识C语言从运行这个程序开始:
c
#include <stdio.h> // #表示预处理,后面的include表示本项目要包含 stdio.h 这个文件。stdio.h 是一个和输入、输出操作相关的头文件,凡是需要进行输入和输出操作的就需要通过预处理操作包含该文件。
int main{ // main函数即主函数,每一个项目有且只能有一个main函数。如果运行时出现重复定义的提示,则一定是有不止一个main函数出现了。
printf("Hello World!\n");
return 0; // return 0表示程序正常执行完毕,没有错误。
}
2. 求整数的绝对值
c
#include <stdio.h> /*涉及到输入、输出操作,需要预处理include stdio.h文件*/
int main() {
int num; /*定义一个名为num的变量,变量的数据类型为int*/
int abs; /*代表绝对值运算结果:绝对值*/
int r;
printf("请输入一个整数:");
r=scanf("%d", &num); /*scanf_s来获取要输入的数据,%d表示要输入的数据是整数类型,这个整数存在num这个变量中。&表示存在num这个变量所在的地址空间中*/
printf("返回值:%d\n", r);
if (r==1)
{
if (num>0) abs=num;
else abs=-num;
printf("%d的绝对值:%d\n", num, abs);
}
else printf("输入错误!\n");
return 0;
}
3. 求长方体的体积
c
#include <stdio.h> /*涉及到输入、输出操作,需要预处理include stdio.h文件*/
#include <stdlib.h>
int volume(int x, int y, int z) /*定义volume函数*/
{
return (x * y * z); /*将volume值返回调用处*/
}
int main() {
int x, y, z, v; /*定义同一类型的变量可以写在同一行*/
int r;
printf("请输出立方体3边长度的整数: ");
r = scanf("%d %d %d", &x, &y, &z);
printf("返回值:%d\n", r);
if (r==3 && x>0 && y>0 && z>0)
{
v = volume(x, y, z);
printf("体积是:%d", v);
}
else printf("error\n");
return 0;
}
算法与流程图
程序 = 算法 + 数据结构 + 程序设计方法 + 语言工具和环境
算法
算法的特点是:
1)有输入
可以有零个或多个输入
在一个算法的执行过程中,提供需要处理的数据或控制算法执行过程的信息
2)有输出
算法必须具有一个或多个执行结果的输出
没有输出的算法是一个无效的算法
3)有穷性
任何算法都应该在执行有穷步骤之后结束
4)确定性
算法不能具有二义性
算法中每一步的语义都应该清晰明了,明确指出应该执行什么操作、如何执行操作
5)高效性
根据算法编写出来的程序应具有较高的时空效率:执行时间短、不占用过多内存
算法的表示(描述)有以下方式:
1)自然语言表示
易理解和交流,但易产生二义性
2)伪代码表示
伪代码使用介于自然语言和计算机语言之间的文字和符号来描述算法
3)程序流程图
用图形符号和文字说明来表示数据处理的过程和步骤
4)N-S流程图
也称方框图,适于结构化程序设计的算法描述工具
举例
以输入1个整数,输出其绝对值为例,有以下几种表示该算法的方法:
1)自然语言表示
1.输入一个整数;
2.若该数为整数,则其绝对值取该数的值。否则,其绝对值取该数的相反数值;
3.输出其绝对值
2)伪代码表示
BEGIN
READ numb
IF numb > 0
abs = numb
ELSE
abs = -numb
PRINT abs
END
3)程序流程图
#include <stdio.h> /*涉及到输入、输出操作,需要预处理include stdio.h文件*/
void main()
{
int numb; /*代表一个整数*/
int abs; /*代表运算结果:绝对值*/
scanf("%d", &numb); /*输入整数的值*/
if (numb > 0) abs = numb;
else abs = -numb;
printf("numb的绝对值是:%d\n", abs);
}
流程图
程序的流程图:
1)描述算法的良好工具
2)逻辑框和流向线组成
3)逻辑框是表示功能的图形符号
4)流向线指示逻辑处理顺序
![](https://img-blog.csdnimg.cn/direct/7e56d9d5992b4b2b9c848601f599c4c8.png)
① 起止框:输入输出框,表示程序的开始和结束。
② 处理框:表示一种处理功能或者程序段框里面用文字简单的描述功能。
③ 选择框:判断框,在这里进行判断来决定走左边那条路还是右边那条路,通常用文字来标注这个条件为真则走这条,为假则走这条。
④ 连接框:这个框里面会有字母。当我们的流程图要跨页表示时,或者有可能出现流向线交叉时,就用连接框来表示彼此之间的关系。
⑤ 流向线:通过这种有方向的线把各种逻辑框串联起来,表达算法的步骤序列。
编程规范
1. 命名
- 标识符
- 标识符的命名要清晰、明了,有明确含义,符合阅读习惯,容易理解
- 标识符的命名使用驼峰风格
驼峰风格(CamelCase)大小写字母混用、单词连在一起,不同单词间通过单词首字母大写来分开。按连接后的首字母是否大写,又分大驼峰(UpperCamelCase)、小驼峰(LowerCamelCase)。 - 作用域越大, 命名应越精确
- 函数
- 函数的命名遵循阅读习惯
- 函数的命名遵循阅读习惯
- 变量
- 全局变量应增加g_前缀,函数内静态变量命名不需要加特殊前缀
- 局部变量应该简短,且能够表达相关含义
- 避免滥用 typedef / #define 对基本类型起别名
- 避免函数式宏中的临时变量命名污染外部作用域
2. 排版风格
建议行宽不超过120个字符
建议使用空格进行缩进,每次缩进4个空格
使用 K&R 缩进风格,如下图所示:
函数声明、定义的返回类型和函数名在同一行;函数参数列表换行时应合理对齐
函数调用参数列表换行时保持参数进行合理对齐
条件语句必须使用大括号
禁止 if / else / else if 写在同一行
循环语句必须使用大括号
switch 语句的 case / default 要缩进一层
表达式换行要保持换行的一致性,操作符放行末
3. 注释风格
注释内容要简洁、明了、无二义性,信息全面且不冗余
文件头注释必须包含版权许可和功能说明
禁止空有格式的函数头注释
代码注释放于对应代码的上方或右边
注释符与注释内容间要有1空格;右置注释与前面代码至少1空格
不用的代码段直接删除,不要注释掉
正式交付给客户的代码不能包含 TODO / TBD / FIXME 注释
case语句块结束时如果不加 break / return,需要有注释说明(fall-through)
按照需要进行注释,如下所示:
4. 文件
头文件在编译时被文本替换展开,从而实现代码复用,使用同一套接口(声明),遵从相同的 "约定"
头文件职责:
- 头文件是模块或文件的对外接口
- 头文件中适合放置接口的声明,不适合放置实现(内联函数除外)
- 头文件应当职责单一。头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因
- 每一个 *.c 文件都应该有相应的 .h 文件,用于声明需要对外公开的接口
- 每一个功能模块都应该提供一个单独的 .h 文件,用于声明模块整体对外提供的接口
- 头文件的扩展名只使用 .h,不使用非习惯用法的扩展名,如 .inc
- 禁止头文件循环依赖
编写头文件时应当防止循环依赖
,指a.h包含 b.h、b.h包含c.h,c.h包含a.h。任何一个头文件修改,都导致所有包含了a.h / b.h / c.h 的代码全部重新编译一遍。而是应该单向依赖,比如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h / c.h 的源代码重新编译。
- 禁止包含用不到的头文件
- 头文件应当自包含
自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能编译通过的话,给这个头文件的用户增添不必要的负担。
比如,如果a.h不是自包含的,需要包含b.h才能编译,则:
每个使用a.h头文件的.c文件,为了让引入的a.h内容编译通过,都需要额外包含b.h头文件
额外的头文件b.h必须在a.h之前进行包含,这在包含顺序上产生了依赖 - 头文件必须编写 #define 保护,防止重复包含
- 禁止通过声明的方式引用外部函数接口、变量
只能通过包含头文件的方式使用其他模块或文件提供的接口
通过extern声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。同时这种隐式依赖,容易导致架构腐化
- 禁止在 extern "C" 中包含头文件
在extern "C"中包含头文件,有可能会导致extern "C"嵌套,部分编译器会对 extern "C" 嵌套层次有限制,嵌套层次太多会编译错误。extern "C" 通常出现在C,C++混合编程的情况
在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏,比如链接规范被不正确地更改
5. 函数
避免重复代码、增加可用性;分层,降低复杂度、隐藏实现细节,使程序更加模块化,从而更有利于程序的阅读,维护
避免函数过长,函数不超过50行(非空非注释)
避免函数的代码块嵌套过深,不要超过4层
对函数的错误返回码要全面处理
设计函数时,优先使用返回值而不是输出函数
使用强类型参数,避免使用void *
模块内部函数参数的合法性检查,由调用者负责
函数的指针参数如果不是用于修改所指向的对象就应该声明为指向const的指针
函数的参数个数不超过5个
内联函数不超过10行(非空非注释)
被多个源文件调用的内联函数要放在头文件中定义
6. 宏
尽量少用函数式宏
常量建议使用const定义代替宏
定义宏时,宏参数要使用完备的括号
包含多条语句的函数式宏的实现语句必须放在 do-while(0)中
不允许把带副作用的表达式作为参数传递给函数式宏
函数式宏定义慎用 return、goto、continue、break等该表程序流程的语句
函数式宏不超过10行(非空非注释)
宏的缺点
- 宏缺乏类型检查,不如函数调用检查严格
- 宏展开可能会产生意想不到的副作用,如 #define SQUARE(a) *(a)这样的定义
- 以宏形势写的代码难以调试、难以打断点,不利于定位问题
- 宏相当于代码展开,只是减少代码编辑空间时间,但不会减少编译后的代码空间的
宏的场景
- 用到了宏的特殊功能,如 '#'、'##',不定长参数
- 用到了预定义宏值,如 'FILE ','TIME' 等
- 包含了不完整语句的封装,比如for循环的循环条件封装
- 经测试,不使用函数式宏,性能无法满足需求的
以下是宏的使用时没有严格使用括号而导致副作用产生的例子:
![](https://img-blog.csdnimg.cn/direct/ef20445153cf446d98214590d8dd4dd2.png)
当使用多条语句的函数宏时,宏本身没有代码块的概念,当宏在调用点展开后宏内定义的表达式和变量融合到调用代码中,可能会出现变量名冲突和宏内语句被分割等问题。通过 do-while(0)显式为宏加上边界,让宏有独立的作用域,并且跟分号能更好的结合而形成单条语句,从而规避此类问题。
错误示例如下:
c
#define FOO(x)\
(void)printf("arg is %d\n", (x));\
DoSomething((x));
正确示例如下:
c
#define FOO(x)
do{\
(void)printf("arg is %d\n", (x));\
DoSomething((x));\
}while (0)
宏使用场景示例:
for (i=1; i<10; i++)
FOO(i);
当使用表达式作为参数传递给函数式宏时,不允许把带副作用的表达式作为参数传递给函数式宏。由于宏只是文本替换,对于内部多次使用同一个宏函数的函数式宏,将带副作用的表达式作为宏参数传入会导致非预期的结果。
错误示例如下:
c
#define SQUARE(a)((a)*(a))
int a=5;
int b;
b=SQUARE(a++); // 实际a自增加了2次,SQUARE(a++)展开后为((a++)*(a++)),变量a自增加了2次,其值为7,而不是预期的6
正确示例如下:
c
#define SQUARE(a)((a)*(a))
int a=5;
int b;
b=SQUARE(a);
a++; // 结果a=6,只自增了一次
7. 变量
7.1
变量在使用时,应始终遵循职责单一原则,尽量不用或少用全局变量:
使用全局变量的优缺点如下:
优点
全局变量意的内存地址固定,读写效率高
便于传递参数;减少传递参数的时间,减少程序的运行时间
数据共享
缺点破坏函数的独立性和可移植性,使函数对全局变量产生依赖,存在耦合
降低函数的代码可读性和可维护性。当多个函数读写全局变量时,某一时刻取值可能不是确定的,对于代码的阅读和维护不利
在并发编程环境中,使用全局变量会破坏函数的可重入性,需要增加额外的同步处理才能确保数据安全
如果要在其他模块使用全局变量,可以将该全局变量封装为函数并提供函数接口来对外和其他函数做数据交互,函数接口如下所示:
c
unsigned int GetRecvBufSize()
{
return g_recvBufSize();
}
则,原先调用全局变量方式 extern unsigned int g_recvBufSize;
可以改为现在的函数接口来调用目标变量 extern unsigned int GetRecvBufSize();
7.2
严禁使用未经初始化的变量:
这里的变量,指的是局部动态变量,并且还包括内存堆上申请的内存块,因为它们的初始值都是不可预料的,所以禁止未经有效初始化就直接读取其值。
如果有不同分支,则需确保所有分支都得到初始化后才能使用。这是因为它们在初始化之前我们刚刚申请下来这块内存,它的值是不可预料的,这可能会导致错误的结果,甚至会造成程序的崩溃。
c
void Foo(...)
{
int data;
if (...){
data = 100;
}
Bar(data); // 这里传递的data是未初始化过的
}
7.3
不允许使用魔鬼数字。魔鬼数字,即看不懂、难以理解的数字。
魔鬼数字会影响程序的可读性,在代码中多点耦合、可维护性差。
对于单点使用的数字,可以增加注释来说明。对于多处使用的数字,必须定义宏或const变量,并通过符号命名自注释。例如:
c
const double pi=3.1415925
使用魔鬼数字的示例如下:
c
totalMons = year*12 + mon;
if (chipType == 12){ // 12在这里没有任何的说明,即魔鬼数字
...
}
8. 实践
- 表达式的比较,应当遵循左侧倾向于变化、右侧倾向于不变的原则。
常数放右边,更符合人的阅读习惯,如下所示:
c
if (score == 60){PASS;}
if (60 == score){PASS;}
if (60 <= score){PASS;}
- 含有变量自增或自减运算的表达式中禁止再次引用该变量。
含有变量自增或自减运算的表达式中如果再引用该变量,其结果在C标准中未明确定义。各个编译器或者同一个编译器不同版本实现可能会不一致。
错误示例如下:
c
x = b[i] + i++; // 运算中b[i]和i++的先后顺序并不明确
正确示例如下:
c
x = b[i] + i;
i++; // 含有变量自增/自减运算的表达式应单独一行
- 用括号明确表达式的操作顺序,避免过分依赖默认优先级。
- 赋值语句不要用作函数参数,不要用在产生布尔值的表达式里。
赋值语句作为函数参数来使用,因为比如说一些函数存在减值或者提前结束等执行不完整的特殊情况,那么可能你以为它执行了然而实际上它并没有执行这个赋值语句,则结果可能非预期,而且可读性差。
错误示例如下:
c
int Foo(...)
{
int a=0, int b;
if ((a==0) || ((b=Fun1()) > 10){
(void)printf("a:%d\n", a);
}
(void)printf("b:%d\n", b);
}
在if语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行。
正确示例如下:
c
x = y;
if (x!=0){
Foo();
}
如果布尔值表达式需要赋值操作,那么赋值操作必须在操作数之外分别进行。这可以帮助避免 '=' 和 '==' 的混淆,帮助我们静态地检查错误。
-
switch语句要有 default 分支。
-
慎用 goto 语句。
goto语句会破坏程序的结构性,所以除非确实需要否则最好不使用goto语句。使用时,也只允许挑准到本函数goto语句之后的语句。
错误观点:
- 禁止使用goto
- 滥用goto。比如所有函数都用goto实现统一出口
- 用do-while(0) + break来模拟 goto 跳转
当一个函数需要统一的return出口处理结尾事务时,则可以使用goto语句。
注意,即使使用goto,不要往回跳。
- 尽量减少没有必要的数据类型默认转换与强制转换。
当进行数据类型强制转换时,其数据的意义、转换后的取值等等有可能发生变化,而这些细节若考虑不周,就有可能留下隐患。
c
signed char ch;
unsigned short int exam;
ch = -1;
exam = ch; // 这时编译器不产生告警,此时exam为0xFFFF