文章目录
- 前言
- 我眼中的C语言
- 一、开发环境
-
- [1. 代码编辑工具](#1. 代码编辑工具)
- [2. 代码编译器](#2. 代码编译器)
- 二、C语言的运行流程机制
- 三、变量
-
- [1. 三要素](#1. 三要素)
- [2. 标识符规范](#2. 标识符规范)
- 四、常量
-
- [1. 常量分类](#1. 常量分类)
- [2. 如何定义标识符常量](#2. 如何定义标识符常量)
- 五、二进制
-
- [1. 进制转换](#1. 进制转换)
- [2. 源码,反码,补码](#2. 源码,反码,补码)
- 六、数据类型
-
- [1. 基本类型](#1. 基本类型)
- [2. 数据类型转换](#2. 数据类型转换)
- 七、运算符
-
- [1. 分类](#1. 分类)
- [2. 运算符汇总](#2. 运算符汇总)
- [3. 运算符优先级](#3. 运算符优先级)
- 八、流程控制语句
-
- [1. 分支控制语句](#1. 分支控制语句)
- [2. 循环控制语句](#2. 循环控制语句)
- [3. 跳转控制语句](#3. 跳转控制语句)
前言
工作后过度侧重实践,忽视了理论学习,导致难以理解核心代码。
如今重新学习C语言,结合工作经验总结了一些心得体会。
水平有限,如有不足,欢迎指正!
我眼中的C语言
C语言项目的编写类似于古代皇朝的治理。你就是皇帝,内存是国库,存储是皇库,外设是你的州府百官。一个项目的好坏,取决于你对这个国家的治理能力如何。
CPU相当于是你的丞相,编写代码的过程就是你书写圣旨的过程,丞相无论能力强弱,都要按照圣旨执行,所以圣旨的编写尤为重要。圣旨编写的是否既简洁又明确,是检验一个皇帝治理水平的核心标准。
一、开发环境
1. 代码编辑工具
向我们日常使用的windows记事本,稍微功能多一点儿的vs code,Notepad++等,再高级一点集成开发环境(IDE),如qt,keil,DEV- C++等,是编辑器+编译器+调试器的一站式开发工具,包含编辑器的核心功能,还能直接编译运行代码。
由此可见,只要能编写,修改文本的软件都可称为代码编辑工具。
2. 代码编译器
如GCC,Min GW,MSVC,Clang等都属于编译器。本质上是将程序员写的高级语言翻译成计算机能识别的机器语言。
以GCC为例编译代码分为4个阶段:
-
预处理阶段:gcc -E test.c -o test.i。展开宏、处理头文件、删除注释
-
编译阶段:gcc -S test.i -o test.s。核心阶段将C语言转成汇编。高级语言-》低级语言的核心阶段。
-
汇编阶段:gcc -c test.s -o test.o。把汇编语言代码(.s)转换成机器语言指令(二进制文件)。
-
链接阶段:gcc test.o -o test.exe 。把目标文件(.o)和系统库(如printf所在的libc库)链接起来,生成可执行文件(Windows 下.exe,Linux 下无后缀)。
二、C语言的运行流程机制
生成可执行文件后,我们点击执行,操作系统会做三件事。分别是:
- 分配内存:为程序开辟独立的内存空间(栈、堆、全局 / 静态区、代码区);
- 加载指令:把可执行文件中的机器码指令加载到内存的 "代码区";
- 定位入口:找到程序的入口点(main函数的起始地址),准备执行。
这里补充一下C语言4大区
| 内存区域 | 存储内容 | 特点 |
|---|---|---|
| 栈区 | 局部变量、函数参数、函数返回地址、临时变量 | 编译器自动分配释放;后进先出;速度快;空间小(易溢出);生命周期随函数作用域结束而结束 |
| 堆区 | 动态分配的数据(如malloc/calloc申请的内存) | 程序员手动申请释放;速度较慢;空间大;需自行管理(易内存泄漏或悬空指针) |
| 全局区(静态区) | 全局变量、static修饰的变量(含局部静态变量) | 程序编译时分配,运行结束释放;生命周期贯穿整个程序;未初始化变量自动清零(BSS段) |
| 代码区(常量区) | 编译后的机器指令、字符串字面量、const修饰的全局常量 | 只读存储;程序运行期间存在;试图修改会触发段错误 |
流程图如下:
编写源代码
↓
预处理\] → 处理宏、头文件
↓
\[编译\] → 生成汇编代码
↓
\[汇编\] → 生成目标文件(机器码)
↓
\[链接\] → 合并目标文件 + 库 → 可执行文件
↓
\[加载\](OS负责)→ 分配内存(代码区/全局区/栈/堆)
↓
\[执行\] → CPU取指执行
├── 函数调用 → 创建栈帧
├── malloc → 从堆申请内存
└── 变量访问 → 根据所在区进行读写
↓
\[退出\] → 释放资源,终止进程
对于嵌入式来说实际硬件中存储的位置:
| 存储介质 | 中文名称 | 核心特点 | 对应软件逻辑 |
|-----------|-------------|----------------------|-------------------------|
| RAM | 随机存取存储器(内存) | 可读可写、掉电丢失、速度快 | 栈、堆、全局 / 静态区(运行时) |
| Flash/ROM | 闪存 / 只读存储器 | 只读(或需特殊操作写)、掉电不丢、速度慢 | 存储程序代码、全局 / 静态 / 常量的初始值 |
## 三、变量
### 1. 三要素
**1.1 类型:**
C语言中的类型分类:
基本类型:int(4字节)、char(1字节)、float(4字节)、double(8字节)
修饰类型:short(2字节)、long(4/8字节)、unsigned、signed
构造类型:数组、结构体、联合体、枚举
指针类型:指向其他类型的变量
空类型:void
解释:创建变量时首要考虑的就是类型,本质上是开辟不同大小的内存空间。但由于芯片的内存和数据链的负载能力有限,实际工作过程中,**我们要尽可能的完美利用我们申请空间中的每一个0和1。**
**1.2 变量名:**
解释:变量名就是用来标记一处内存位置。变量名一定要起的规范好理解,一个好的变量名既能方便我们理解代码,也能方便我们后续的使用。
**1.2 值:**
值存放在变量对应的内存单元中,可以随时改变(除非用 const 修饰)。
变量的值在未初始化时是垃圾值(即之前该内存遗留的二进制内容),直接使用会导致未定义行为。
赋值操作就是将新的数据写入内存单元。
解释:值是变量当前存储的具体数据,它必须与变量的类型相匹配。实际工作中,我们一定要认真管理好代码中所有变量的初始值,防止出现开机上电屏幕上出现一堆随机值。
### 2. 标识符规范
| 规则 | 说明 |
|--------|------------------------------------------|
| 组成字符 | 只能由字母(a-z, A-Z)、数字(0-9)和下划线(_)组成。 |
| 首字符 | 不能以数字开头。 |
| 区分大小写 | sum、Sum、SUM 是三个不同的标识符。 |
| 不能是关键字 | 不能使用 C 语言的关键字(如 int、if、while 等)作为标识符。 |
| 长度限制 | 理论上无限制,但编译器通常只识别前若干字符(如 31/63 个),建议不要太长。 |
解释:变量名属于标识符(Identifier)的一种。标识符还包括函数名、结构体名、宏名等。实际工作中,标识符的命名可以参考Linux内核代码中的驼峰式命名法,清晰易理解,方便后期维护拓展项目。
// 规范的标识符
int firstNumber; // 驼峰命名
float my_var; // 下划线命名
char className; // 避免与关键字冲突
const int MAX_STUDENTS = 100; // 宏常量全大写
## 四、常量
**概念:** 常量是固定值,在程序执行期间不能修改。项目中常用于当作全局设置指令等。
为何要引入常量这一概念?
1. 提高可读性:用有意义的名称代替魔法数字
2. 便于维护:修改一处即可影响所有使用处
3. 增强安全性:防止意外修改
实际工作中,常量往往被用来标记一些常用的固定值,如:3.1415926,特定三角函数值,最大值最小值等等。
### 1. 常量分类
**1.1 字面常量**
字面量常量是直接写在代码中的具体值,无需额外定义。
| 类型 | 示例 | 说明 |
|--------------|------------------------------------------------|-----------------------------------------------------|
| 整型字面量 | 123、-456、0xFF(十六进制)、075(八进制)、0b1010(二进制,C23 起) | 默认 int,后缀 L/l 表示 long,LL 表示 long long,U 表示 unsigned |
| 浮点型字面量 | 3.14、2.0e-5、.5、1. | 默认 double,后缀 f/F 表示 float,l/L 表示 long double |
| 字符型字面量 | 'A'、'\\n'、'\\x41' | 单引号括起,类型为 int,可含转义序列 |
| 字符串字面量 | "Hello"、"Line1\\nLine2" | 双引号括起,类型为 char \[\](只读存储),末尾自动添加 \\0 |
| 布尔字面量(C99 起) | true、false | 需包含 \
存在"+0"和"-0"两个零,浪费编码空间。
示例(8 位)
| 数值 | 原码 |
|---|---|
| +5 | 0000 0101 |
| -5 | 1000 0101 |
| +0 | 0000 0000 |
| -0 | 1000 0000 |
缺点
- 加减法运算复杂,需先判断符号位再决定运算类型;
- 两个零的存在浪费了一个编码组合。
2. 反码(Ones' Complement)
定义
- 正数的反码与原码相同;
- 负数的反码是对应正数原码的所有位取反(符号位也取反)。
表示范围(n 位)
同原码: [ − ( 2 n − 1 − 1 ) , + ( 2 n − 1 − 1 ) ] \left[-(2^{n-1} - 1), +(2^{n-1} - 1)\right] [−(2n−1−1),+(2n−1−1)]
依然存在"+0"和"-0"两个零。
示例(8 位)
| 数值 | 反码 | 说明 |
|---|---|---|
| +5 | 0000 0101 | 与原码相同 |
| -5 | 1111 1010 | 对 +5 的原码(0000 0101)所有位取反 |
| +0 | 0000 0000 | 与原码相同 |
| -0 | 1111 1111 | 对 +0 的原码所有位取反 |
缺点
- 仍存在两个零,浪费编码;
- 加法运算需处理"循环进位"(端回进位),硬件实现复杂。
3. 补码(Two's Complement)
定义
- 正数的补码与原码相同;
- 负数的补码是其反码加 1(忽略溢出)。
表示范围(n 位)
− 2 n − 1 , + ( 2 n − 1 − 1 ) \] \\left\[-2\^{n-1}, +(2\^{n-1} - 1)\\right\] \[−2n−1,+(2n−1−1)
只有一个零,相比原码/反码多表示一个最小负数。
示例(8 位)
| 数值 | 补码 | 说明 |
|---|---|---|
| +5 | 0000 0101 | 与原码/反码相同 |
| -5 | 1111 1011 | -5 的反码(1111 1010)加 1 |
| +0 | 0000 0000 | 取反加 1 后仍为 0(唯一零) |
| -128 | 1000 0000 | 无对应正数 128,8 位补码范围:-128 ~ 127 |
优点
- 唯一零:避免编码浪费,仅 0000 0000 表示零;
- 加减法统一:无需判断符号位,直接进行二进制加法,溢出自动丢弃进位;
- 硬件实现简单:CPU 的 ALU(算术逻辑单元)只需加法器即可完成加减法运算。
补码的快速计算方法
-
给定负数 − x -x −x,其 n 位补码 = 2 n − x 2^n - x 2n−x(忽略溢出);
-
从原码转补码(负数):符号位不变,数值位取反后加 1(符号位参与运算)。
六、数据类型
在C语言中,数据类型是变量和表达式的本质属性,它直接决定了内存占用空间、数值取值范围以及可执行的操作类型。当我们定义不同数据类型的变量时,实际上就是在为这些变量分配相应大小的内存空间。
工作实践中,数据类型就是我们用C语言量化现实 的最小单元。
数据类型的主要功能:
- 内存分配:明确变量所需的内存空间大小(以字节为单位)
- 取值范围:限定变量能够存储的数值范围(最小值和最大值)
- 操作权限:规定允许执行的操作类型(例如整型支持位运算,而浮点型则不支持)
C语言数据类型的分类体系:
-
基本数据类型:
- 整型(int)
- 浮点型(float/double)
- 字符型(char)
- 布尔型(bool)
-
构造数据类型:
- 数组
- 结构体(struct)
- 联合体(union)
- 枚举(enum)
-
指针类型
-
特殊类型:
- 空类型(void)
1. 基本类型
1.1 整形
整型用于存储整数,分为有符号(signed,默认)和无符号(unsigned)。常用整型有:
| 类型 | 常见大小(字节) | 取值范围(有符号) | 取值范围(无符号) |
|---|---|---|---|
| short | 2 | -32,768 ~ 32,767 | 0 ~ 65,535 |
| int | 4 | -2,147,483,648 ~ 2,147,483,647 | 0 ~ 4,294,967,295 |
| long | 4 或 8(取决于平台) | 至少 -2³¹ ~ 2³¹-1 | 至少 0 ~ 2³²-1 |
| long long | 8 | -9.2×10¹⁸ ~ 9.2×10¹⁸ | 0 ~ 1.8×10¹⁹ |
1.2 浮点型
浮点类型常用于存储带小数的实数。C语言提供了三种浮点类型:
| 类型 | 常见大小(字节) | 精度(有效十进制位数) | 取值范围(约) |
|---|---|---|---|
| float | 4 | 6~7 位 | ±1.2×10⁻³⁸ ~ ±3.4×10³⁸ |
| double | 8 | 15~16 位 | ±2.2×10⁻³⁰⁸ ~ ±1.8×10³⁰⁸ |
| long double | 8/10/16(平台相关) | 更高精度 | 更大范围 |
1.3字符类型
char 类型用于存储单个字符,其本质是在内存中以整数形式存储字符对应的 ASCII 码(或扩展编码)值。
特性:
- 固定大小:1 字节
- 取值范围:
signed char:-128 ~ 127unsigned char:0 ~ 255
- 默认是否有符号由编译器决定,可通过
signed/unsigned关键字显式指定
字符表示:
- 字面量:用单引号包裹,如
'A'、'\n' - 转义序列:
\n换行符\t制表符\\反斜杠\x41十六进制 ASCII 值表示
c
char ch = 'A';
char newline = '\n';
1.4 布尔类型
就是0和1。C99 之前没有专门的布尔类型,通常用 int 模拟(0 表示假,非 0 表示真)。C99 引入了 _Bool,并提供 <stdbool.h> 头文件简化使用。
三种定义布尔类型的方式:
(1) 使用 int 类型模拟(传统方法)
c
int flag = 0; // 表示假
int done = 1; // 表示真
if (done) { ... }
缺点:类型语义不明确,代码可读性较差。
(2) 使用 _Bool 类型(C99 标准引入)
c
_Bool flag = 0; // 0 表示假
_Bool done = 1; // 1 表示真
说明:_Bool 是无符号整型,仅能存储 0 或 1。任何非零值赋给 _Bool 变量都会转换为 1。
不足:语法略显生硬,可读性一般。
(3) 使用 <stdbool.h> 标准库(C99 标准引入)
c
#include <stdbool.h>
bool flag = false; // bool 是 _Bool 的别名
bool done = true; // true/false 是预定义宏,分别展开为 1/0
推荐:使用 bool 类型名配合 true/false 值,代码意图清晰明了。
2. 数据类型转换
在C语言中,不同类型的数据进行运算时会自动发生数据类型转换。转换方式分为自动转换和强制转换两种。
2.1 自动类型转换
由编译器自动完成,主要发生在以下两种情况:
(1) 运算过程中的类型转换
- 整型提升:在表达式中,所有小于int的整型(如char、short、_Bool)都会先提升为int(或unsigned int)后再参与运算。
c
char c1 = 100, c2 = 200;
int sum = c1 + c2; // c1和c2先提升为int类型,再进行相加
-
寻常算术转换 :当操作数类型不同时,编译器会将类型统一转换为精度更高的类型。转换规则按以下优先级进行:
long double > double > float > unsigned long long > long long > unsigned long > long > unsigned int > int
c
int i = 10;
float f = 3.14f;
double d = i + f; // i先转换为float,运算结果再转换为double
(2) 赋值过程中的类型转换
将右侧表达式的值赋给左侧变量时,会自动转换为左侧变量的类型。
- 高精度转低精度:可能发生数据截断或舍入
c
double pi = 3.1415926;
int i = pi; // i值为3,小数部分丢失
- 大范围转小范围:若值超出目标类型范围,会发生溢出
c
unsigned char uc = 300; // 300%256=44
signed char sc = 130; // 结果未定义(通常为-126)
2.1 强制转换
程序员显式指定将某个表达式的类型转换为另一种类型。语法为:
c
(类型名) 表达式
int a = 5, b = 2;
float result = (float)a / b; // 先将 a 转换为 float,再与 b 相除,结果为 2.5
注意事项:
- 数据丢失风险:强制转换可能导致数据截断或精度损失
- 指针转换隐患:不同类型指针间的强制转换可能引发未定义行为(如内存对齐问题或类型双关)
- 谨慎使用:过度依赖强制转换会屏蔽编译器警告,潜在掩盖逻辑错误
七、运算符
运算符就是对数据进行运算的符号。
1. 分类
一元运算符:作用于单个操作数,例如负号运算(-a)、自增(++i)和逻辑非(!flag)。
二元运算符:需要两个操作数参与运算,如加法(a + b)和比较运算(x > y)。
三元运算符:涉及三个操作数的运算,在C语言中仅条件运算符(?:)属于此类。
2. 运算符汇总
| 类别 | 运算符 | 说明 |
|---|---|---|
| 算术运算符 | + - * / % ++ -- | 基本数学运算、自增自减 |
| 关系运算符 | > < >= <= == != | 比较大小,返回真(1)/假(0) |
| 逻辑运算符 | && || ! | 逻辑与、或、非 |
| 位运算符 | & | ^ ~ << >> | 按位与、或、异或、取反、左移、右移 |
| 赋值运算符 | = += -= *= /= %= &= |= ^= <<= >>= | 赋值及复合赋值 |
| 条件运算符 | ? : | 三元条件表达式 |
| 其他 | sizeof & * , () [] . -> | 长度运算符、取地址、间接引用、逗号、函数调用、下标、结构体成员等 |
3. 运算符优先级
运算符优先级决定了表达式中运算的执行顺序,而结合性则规定了同一优先级运算符的计算方向。
运算符优先级表
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1(最高) | () [] . -> ++(后缀) --(后缀) |
从左到右 |
| 2 | ++(前缀) --(前缀) +(一元) -(一元) ! ~ *(解引用) &(取址) (类型) sizeof |
从右到左 |
| 3 | * / % |
从左到右 |
| 4 | + - |
从左到右 |
| 5 | << >> |
从左到右 |
| 6 | < > <= >= |
从左到右 |
| 7 | == != |
从左到右 |
| 8 | &(按位与) |
从左到右 |
| 9 | ^ |
从左到右 |
| 10 | ` | ` |
| 11 | && |
从左到右 |
| 12 | ` | |
| 13 | ?: |
从右到左 |
| 14(最低) | = += -= 等赋值运算符 |
从右到左 |
| 15 | , |
从左到右 |
记忆口诀
括号优先级最高,运算顺序依次为:单目 > 算术 > 移位 > 关系 > 按位 > 逻辑 > 条件 > 赋值 > 逗号。
八、流程控制语句
我们生活中的所有事件逻辑都可以用三大流程表示,分别是分支、循环、跳转。这三大流程就是C语言中的流程控制语句,流程控制语句决定了程序的执行路径。
1. 分支控制语句
1.1 if 语句
c
if (条件) {
// 条件为真时执行的代码
}
1.2 if-else 语句
c
if (条件) {
// 条件为真时执行的代码
} else {
// 条件为假时执行的代码
}
- 可通过
else if实现多分支条件判断 - 注意:单条语句可省略大括号,但建议保留以提高代码可读性
1.3 switch-case 语句
c
switch (表达式) {
case 常量1:
// 表达式等于常量1时执行的代码
break;
case 常量2:
// 表达式等于常量2时执行的代码
break;
default:
// 无匹配时执行的代码
}
- 表达式必须为整型(包括 char 和枚举类型)
- case 标签必须是整型常量表达式(不能使用变量或范围)
- 通常每个 case 后需加
break语句,否则会继续执行后续 case(穿透效应) default分支可选,可放置在任意位置。
2. 循环控制语句
- while 循环
c
while (条件) {
// 条件成立时重复执行
}
先判断条件后执行,可能完全不执行。
- do-while 循环
c
do {
// 循环体至少执行一次
} while (条件);
先执行一次循环体,再判断条件,保证至少执行一次。
- for 循环
c
for (初始化; 条件; 更新) {
// 循环体
}
执行流程:初始化 → 条件判断 → 循环体 → 更新 → 再次条件判断...
注意事项:
- 三个表达式都可省略,但必须保留分号
for(;;)表示无限循环- 从 C99 开始,可在初始化部分定义变量,其作用域仅限于循环内部。
3. 跳转控制语句
-
break
用于跳出当前所在的
switch语句或最内层的while、do-while、for循环。
仅影响当前循环层,外层循环不受干扰。 -
continue
用于循环结构中,跳过本次循环剩余的语句,直接进入下一次迭代。
- 在
while/do-while中:跳转至条件判断部分 - 在
for循环中:跳转至更新表达式部分
- 在
-
goto
无条件跳转到函数内的指定标签位置。语法:
cgoto label; ... label:需谨慎使用,可能降低代码可读性,但在处理多层循环跳出或错误处理时仍具实用价值。
-
return
终止当前函数执行并返回调用者:
- 若函数有返回值,必须跟随返回表达式
- 若返回类型为
void,可简写为return;1