vault backup: 2026-01-22 20:19:30

This commit is contained in:
2026-01-22 20:19:30 +08:00
parent 4f13f2f674
commit b810d55176
283 changed files with 52 additions and 86 deletions

View File

@@ -0,0 +1,130 @@
# 一、概述
C 语言是一种通用的、
## 1. **发展历程**
- **1972 年**C 语言诞生于贝尔实验室,作为 UNIX 系统的开发语言。
- **1989 年**ANSI CC89成为首个标准化版本奠定语言基础。
- **1999 年**C99 引入新特性(如布尔类型、变长数组)。
- **2011 年至今**C11、C17 等版本持续完善,保持语言活力。
## 2. **重要意义**
- **系统级编程**UNIX、Linux、Windows 等操作系统核心均用 C 语言编写。
- **语言桥梁**C++、Java、Python 等语言借鉴其语法结构,被称为 “编程的母语”。
- **性能标杆**:代码执行效率接近汇编语言,适合对性能要求极高的场景。
# **二、C 语言的核心特性**
## 1. **高效性**
- 直接操作内存(通过指针),减少运行时开销。
- 编译型语言,生成机器码,执行速度快。
## 2. **灵活性**
- 支持底层操作(如位运算、内存管理)。
- 可扩展为面向对象(如通过结构体和函数指针实现封装)。
## 3. **可移植性**
- 遵循标准C的代码可在不同平台如 Windows、Linux、嵌入式系统编译运行。
## 4. **强大的标准库**
- **输入输出**stdio.h如printf、scanf
- **字符串处理**string.h如strcpy、strlen
- **内存管理**stdlib.h如malloc、free
- **数学运算**math.h如sqrt、sin
# **三、C 语言的应用场景**
## 1. **系统软件**
- 操作系统内核(如 Linux、macOS
- 驱动程序、文件系统。
## 2. **嵌入式开发**
- 物联网设备、智能家居。
- 汽车电子、工业控制。
## 3. **游戏开发**
- 游戏引擎(如 Unity 的底层)。
- 高性能游戏逻辑。
## 4. **高性能应用**
- 数据库系统(如 MySQL
- 图形处理库(如 OpenGL
# **四、C 语言的优缺点**
## 1. **优点**
- 执行效率高,适合对性能敏感的场景。
- 跨平台兼容性好。
- 学习曲线平缓,适合编程入门。
## 2. **缺点**
- 手动内存管理如malloc/free易导致内存泄漏。
- 缺乏高级特性(如垃圾回收、泛型)。
- 错误处理依赖返回值和全局变量如errno
# **五、学习资源推荐**
1. **经典教材**
- 《C Primer Plus》入门
- 《C Programming Language》K&R 原著,进阶)
- 《Linux图文指南》
1. **在线教程**
-
-
-
1. **开发环境**
- 编译器GCCLinux/macOS、MinGWWindows
- IDECLionJetBrains、Code::Blocks、VsCode。
# 六、怎么学好C语言
1. 先学习C语言的基础知识打好基础
1. 多看、多写、多思考、多练习编程
1. 开始着手写一些简单的项目,如小游戏
1. 在网上寻找一些大佬的项目进行观摩、学习和积累经验。
> 只要写不死,就往死里写
> ----学好C语言

View File

@@ -0,0 +1,231 @@
# 一、C语言基础语法
1. **C语言结构**
C语言程序源码文件以.c结尾一个C语言程序主要包括以下部分
- 预处理指令:以'#'开头,如:#include --> 用于处理头文件
| 指令 | 说明 |
| -- | -- |
| #include | 包含(引入)一个源代码文件 |
| #define | 定义一个宏 |
| #undef | 取消已定义的宏 |
| #if | 如果给定条件为真,则编译其后的代码 |
| #ifdef | 如果宏已经定义,则编译其后的代码 |
| #ifndef | 如果宏没有定义,则编译其后的代码 |
| #elif | 如果前面的 #if 给定条件为假,并且当前条件为真,则编译其后的代码 |
| #endif | 结束一个 #if#ifdef 或者 #ifndef 条件编译块 |
| #defined | 这不是命令,而是操作符,用来判断某个宏是否已被定义。 |
- 主函数main函数 --> C语言程序的入口一个C语言程序有且仅有一个主函数main
- 变量int abc由符号用户标识符构成。
- 语句及表达式:语句以英文分号;结尾if(....)表达式等等
- 注释:语句或表达式的解释,单行(//)和多行(/* */
1. **C语言的语法**
1. C语言的令牌
在 C 语言中令牌Token是程序的基本组成单位编译器通过对源代码进行词法分析将代码分解成一个个的令牌。C 语言的令牌主要包括以下几种类型:
- 关键字Keywords具有特定含义的单词。
- 标识符Identifiers描述数据或功能的符号或单词
- 常量Constants数值不能修改的量
- 字符串值String Literals""
- 运算符Operators: +、-、*、/.....
- 分隔符Separators: ;
1. 分号;
在C语言程序中英文的分号是语句的结束符每个语句必须以分号结尾它表明一个逻辑的结束。
1. 注释
在C语言程序中注释用于说明语句或程序的功能和使用方法注释的语句不会参与到程序的运行只用于开发人员或 使用人员进行查阅C语言程序注释的方式有单行(// )和多行(/* */)两种。注释不能嵌套使用。
单行注释: // 需要注释的内容
多行注释:/* 需要注释的内容 */
1. 标识符
在C语言中标识符用于标识变量、函数、数组等或者其他用户自定义名称或项目名字具体要求如下
- 只能包含数字、 字母以及下划线, 不能包含其他任何特殊字符。
- 只能以大写、小写字母或者下划线开头。
- 不能跟系统已有的关键字重名, 也不能跟在相同作用域的其他标识符重名。
```c
abc(√) 123× _123(√) a&b× Ace() int(×)
```
1. 关键字
在C语言程序中具有特殊功能或含义的单词函数名、变量名、常量名及其他用户标识符不能是关键字
| 关键字 | 说明 |
| -- | -- |
| auto | 声明自动变量 |
| break | 跳出当前循环 |
| case | 开关语句分支 |
| char | 声明字符型变量或函数返回值类型 |
| const | 定义常量,如果一个变量被 const 修饰,那么它的值就不能再被改变 |
| continue | 结束当前循环,开始下一轮循环 |
| default | 开关语句中的"其它"分支 |
| do | 循环语句的循环体 |
| double | 声明双精度浮点型变量或函数返回值类型 |
| else | 条件语句否定分支(与 if 连用) |
| enum | 声明枚举类型 |
| extern | 声明变量或函数是在其它文件或本文件的其他位置定义 |
| float | 声明浮点型变量或函数返回值类型 |
| for | 一种循环语句 |
| goto | 无条件跳转语句 |
| if | 条件语句 |
| int | 声明整型变量或函数 |
| long | 声明长整型变量或函数返回值类型 |
| register | 声明寄存器变量 |
| return | 子程序返回语句(可以带参数,也可不带参数) |
| short | 声明短整型变量或函数 |
| signed | 声明有符号类型变量或函数 |
| sizeof | 计算数据类型或变量长度(即所占内存字节数) |
| static | 声明静态变量 |
| struct | 声明结构体类型 |
| switch | 用于开关语句 |
| typedef | 用以给数据类型取别名 |
| unsigned | 声明无符号类型变量或函数 |
| union | 声明共用体类型 |
| void | 声明函数无返回值或无参数,声明无类型指针 |
| volatile | 说明变量在程序执行中可被隐含地改变 |
| while | 循环语句的循环条件 |
### C99 新增关键字
| _Bool | _Complex | _Imaginary | inline | restrict |
### C11 新增关键字
| _Alignas | _Alignof | _Atomic | _Generic | _Noreturn |
| -- | -- | -- | -- | -- |
| _Static_assert | _Thread_local | | | |
# 二、编程规范
1. 命名方式
自定义标识符时要做到
变量名max_number zui_da_zhi maxNumber
函数名max_number_function ji_suan_zui_da_zhi MaxNumberFunction
常见的命名方式:
- 驼峰式
以小写或大写字母开头后面跟大写或小写字母如myAppMyName等类似的命名方式
- 匈牙利命名法
以变量类型的缩写开头如int iCount; unsigned int uiNum;等
- 帕斯卡命名法
类似与驼峰式命名法以大写字母开头如MyAppMyName等
- 下划线命名法
在Linux中最常见的命名方式Linux一些系统库、开源软件等编码中较为常见。
如char my_name[10];int first_num;等
1. 缩进
在C语言源码中缩进要对齐代码要有层级体现最好用 8 个空格缩进,避免用 4 个空格甚至是 2 个空格
如:
```c
void fun(void)
{
int n=10;
if(n<10){
printf("....");
}
}
```
1. 空格和空行
空格和空行也是提高程序可读性的很重要的一方面,谁都不愿意阅读挤在一起的代码, 因此适当的空格和空行能让程序看起来逻辑更加清晰。
1. 括号
函数体和循环结构、分支结构等代码块需要用到花括号将其代码括起来,对于函数而言, 在 LINUX 编码风格中,左右花括号分别占用一行,在其他的代码块中,左花括号可以放在 上一行的最右边,右花括号单独占一行(推荐像函数一样左右括号各自单独占一行,使得代码的风格更具一致性)。
1. 注释
在我们编写程序的时候,如果遇到比较复杂的情形,我们可以在代码中用自然语言添加 一些内容,来辅助我们自己和将来要阅读该程序的人员更好地理解程序。
更详细的规范见《编程规范.pdf》
[编程规范.pdf](attachments/WEBRESOURCE40a76a7d230733292ec04717f3f6f254编程规范.pdf)
# 三、第一个C语言程序
第一个C语言程序源码常常是在终端中输出一段话如"Hello world"
```c
#include <stdio.h>  // <> 表示在系统的路径中寻找头文件stdio.h未找到则报错
#include "myhead.h"  // ""表示在本目录中寻找或者在设置的路径中查找头文件,未找到则去系统路径中寻找,均未找到则报错
// 主函数C语言程序的入口一个完整的C语言程序必须有且只能有一个main函数
int main(int argc, char const *argv[])
{   // 函数的开始 表示函数的内容是一个整体
    printf("hello GEC\n"); // 将"hello GEC\n"输出到终端屏幕
    return 0;  // 函数功能的结束  不是必须的
} // 函数语句的结束
```
源码的第一行是#include <stdio.h>#include说明它一条预处理指令在C语言源文件编译之初就会执行执行的指令表示告诉编译器需要加载"stdio.h"这个文件的内容到源代码中来,<>尖括号表示告诉编译器寻找这个文件的路径是优先在系统的路径中,""双引号表示告诉编译器寻找头文件的路径优先选择当前路径或明确指定的路径中进行,若未找到则去系统路径中寻找。
![](images/WEBRESOURCE4bb07092451ff2e2b923c57cb023415dimage.png)
下一行int main(int argc, char *argv[])是主函数程序执行从这里开始自上而下进行逐一运行C语言的程序有且仅有一个main函数主函数的一般形式为int main(int argc, char *argv[])或int main(void)
下一行//单行注释,编译器会自动忽略,下图一个预编译后的文件在这个文件中头文件被其内容替换,注释被删除。
![](images/WEBRESOURCEb4c06b079d7fe90fbd6ea378458623dcimage.png)
下一行{表示代码块的开始在C语言程序中多条语句的集合常常用花括号进行包含表示复合语句函数体、循环体、条件语句等
下一行printf(....)是C语言中另一个函数的调用调用这个函数可以在屏幕上显示其中的内容如Hello world这个函数在stdio.h中声明在标准C库中定义。
下一行return 0;表示函数的结束执行这个语句表示main函数的终止并返回一个0值
最后一行}表示代码块的结束
![](images/WEBRESOURCE1ec0d881c347298235a88359695ad942image.png)
注意:如下图所示的问题是源码中缺少头文件<stdio.h>
![](images/WEBRESOURCE57ae4c50fdbe52495c1fec865fb6c5fbimage.png)
练习:
在终端输出自己的学校和姓名运行结果截图发QQ群。

View File

@@ -0,0 +1,717 @@
# 一、基本数据类型
1. **字符型**
数据类型关键字char
类型长度1Byte(字节) = 8bit(位)
Byte --> KB --> MB --> GB-->Tb --> PB 进制换算满1024进一
取值范围
无符号只有自然数unsigned char 取值范围 0~2^8-1 0~255
有符号(负数和自然数):(signed) char 取值范围 -2^7 ~ 2^7-1 -128~127
![](images/WEBRESOURCE38bf818da8bb069ed9eb989de0bff6eaimage.png)
使用方法
```c
char a = 'a'; // 字符在内存中以ASCII码值的形式存在,因此char a='a'等价于 char a=97
// 单字节整数类型
```
![](images/WEBRESOURCE73773b4065a244cce2bf36de18615e0cimage.png)
记忆方法:'A' -> 65 'a'->97 '0'->48
1. **整型**
1. 短整型
类型关键字short
类型长度2 字节32位与64位
取值范围
无符号只有自然数unsigned short 取值范围 0~2^16-1 0~65535
有符号(负数和自然数):(signed) short 取值范围 -2^15 ~ 2^15-1 -32768~32767
1. 整型
类型关键字int
类型长度4 字节32位与64位
取值范围
无符号只有自然数unsigned int 取值范围 0~2^32-1 0~4294967295
有符号(负数和自然数):(signed) short 取值范围 -2^31 ~ 2^31-1 -2147483648~2147483647
1. 长整型
类型关键字long32位与64位
类型长度4 / 8字节根据系统的位数决定32位4字节 64位8字节
取值范围
无符号只有自然数unsigned int 取值范围 0~2^32-1 / 0~2^64-1
有符号(负数和自然数):(signed) short 取值范围 -2^31 ~ 2^31-1 / -2^63~2^63-1
1. 长长整型
类型关键字long long
类型长度8 字节
取值范围
无符号只有自然数unsigned int 取值范围 0~2^64-1
有符号(负数和自然数):(signed) short 取值范围 -2^63~2^63-1
1. **浮点型(小数)**
1. 单精度浮点型
类型关键字float
类型长度4 字节6位有效位
取值范围1.2E-38 到 3.4E+38
![](images/WEBRESOURCE9056ea5007d1d9231711d63d2bbf867eimage.png)
![](images/WEBRESOURCEd429a77169c21de412bd8ee7b02e5313image.png)
1. 双精度浮点型
类型关键字double
类型长度8 字节15位有效位
取值范围2.3E-308 到 1.7E+308
![](images/WEBRESOURCE0e7ea7d0398d9e54868b0d0d16713384image.png)
1. 长双精度浮点型
类型关键字long double
类型长度16 字节19位有效位
取值范围3.4E-4932 到 1.1E+4932
![](images/WEBRESOURCE4bd255fbf9260ad534192cb1d5185cf0image.png)
注意:
比较两个int类型的变量是否相等时
```c
int a,b;
if(a == b)
...
```
比较两个float类型的变量进行比较时
```c
float a,b;
a=1.1;
b=2.2;
if(b-1.1 == a) // × 因为在C语言中浮点数是存在精度损失的有可能比原来的数大也有可能小
// 浮点数不能直接用 == 号比较。要使用精度进行判断,精度可以理解为引发一个浮点数发生改变的最小值。
// 当一个浮点数加上精度或者减去精度,都不等于该数本身。
// 精度可以是自己定义的也可以用C语言自带的
if(fabs(b-1.1-a) < 0.000001) // √ if(fabs(b-1.1-a) < 0.000001)  // b-1.1==a   b-1.1-a == 0
// fabs是数学可函数用于取绝对值的需要添加math.h头文件 #include <math.h>
```
1. **布尔型**
该类型使用时需要添加"#include <stdbool.h>"
类型关键字bool
类型长度1 字节
取值范围:真或假(true or false)
> 非0或非NULL即真负数为真常用1表示真
> 0或NULL
> int --> 非0 char -->非'\0' float --> 非'0.0' bool --> 非false 指针-->非NULL
1. **空类型**
注意该类型只能用于定义指针、函数或修饰函数的参数列表,不能定义普通变量
类型关键字void --> 空
类型长度1 字节
## 数据类型溢出
![](images/WEBRESOURCE2416db6773bc1892a970d22688b28c02image.png)
![](images/WEBRESOURCE9a3ffeea27bb5e725aff994cb67d133cimage.png)
# 二、变量
## 概念
在内存中,数据可以被修改的空间,在程序执行过程中数据的值可发生修改,
常见形式:数据类型 用户标识符;
变量声明声明定义:
```c
char a; // 声明定义一个char类型的变量a
int b; // 声明定义一个int类型的变量b
float c; // 声明定义一个float类型的变量c
```
变量的初始化
```c
char a = 'a'; // =在C语言中属于赋值操作即将某个数据放入谋片内存空间声明定义一个char类型的变量a并进行初始化赋值
```
![](images/WEBRESOURCE241c9ea7af5f38a2d179fd848e37cfa9image.png)
## 局部变量
声明定义在函数内部,在当前的{}中有效,函数的参数是局部变量
## 全局变量
声明定义在函数的外部,在本文件中有效,所有函数都可访问。
**注意:**
```c
#include <stdio.h>
float pi = 3.14; // 不在函数内不在{}中的变量——全局变量(可用范围是当前这个文件内)
int main(int argc, char const *argv[])
{
    char a = 'A';  // 变量的声明定义放在一对{}内的变量——局部变量(可用范围局限于这一对{}
    {
        int  b;   // 花括号{}包含的区域称为 —— 局部作用域
    }
    // b = 50;  // ❌超出作用域范围
    // pi = 6.28;  // ✔ 全局作用域包含局部作用域
    // pi ?  3.14
    float pi=1.32; // ✔ 变量在同一个作用域中不能重名,但在不同的作用域中可以重复
    // pi ?  1.32
    {
        // pi ?  1.32
        float pi=33.32; // ✔ 当不同作用域中的变量名字相同,使用时采用就近原则
       //  pi ? 33.32
    }
    // pi ?  1.32
    printf("a = %c\n", a);
    return 0;
}
```
# 三、常量
在内存中,数据不能被改变空间,在程序执行过程中数值不能被修改,保持固定的值也称
```c
10 3.14 5.20 66 88 'a'
```
1. **整型常量(二进制、八进制、十进制、十六进制)**
1. 二进制
表示范围0和1满2进一1 + 1 ==> 0B10B B1 + 1 10
1. 八进制
表示范围0~7满8进一7 +1 ==> 0o10O 07 +1 010
1. 十进制
表示范围0~9满10进一9 +1==> 10D 9+1 10
1. 十六进制
表示范围0~9 A~F(10~15)满16进一F + 1 ==> 0x10H 0xF+1 0x10
### 进制间支持相互转换
**二进制转N进制**
二进制01001101
转为八进制三位二进制等于一位八进制从右往左不足3位左补0421码进行转换0115
转为十进制使用从右往左逐位乘2的位数-1次幂求和0*2^7+1*2^6+0*2^5+0*2^4+1*2^3+1*2^2+0*2^1+1*2^0 = 0+64+0+0+8+4+0+1 = 77
转为十六进制四位二进制等于一位十六进制从右往左不足4位往左补08421码进行转换0x4D
**十进制转N进制**
十进制123
除N取余倒记法短除法
![](images/WEBRESOURCE01494a5ca65c3e8f09d6221377f879c2image.png)
**八进制转N进制**
转二进制一个八进制等于三位二进制42105 ==》 101
转十进制使用从右往左逐位乘8的0+位的位置-1次幂求和
转十六进制:转二进制后转十六进制
**十六进制转N进制**
转二进制一个十六进制等于四位二进制84210x7 ==》 0111
转十进制使用从右往左逐位乘16的0+位的位置-1次幂求和
转八进制:转二进制后转八进制
```c
223 // ✔️
215u // ✔️ unsign int
0xFEEL // ✔️ 十六进制常量 L long int
078 // ❌ 0表示数据类型为八进制但八进制数据没8
032UU // ❌ U不能重复
88 // ✔️
0233 // ✔️
30 // ✔️
30l // ✔️
0x4b // ✔️
312UL // ✔️ unsigned long int
0xFEUL // ✔️ 十六进制无符号成整型常量
```
有符号整型常量存储方法 —— 存储补码
原码:数据绝对值的二进制编码,无符号数在内存中以原码形式保存
反码:对原码逐位取反
补码反码加1有符号数的二进制码的最高位为符号位(1负 0正),在内存中用其绝对值原码的补码(符号位保持不变)进行存储,
![](images/WEBRESOURCE9923ec431e7de4a5464765a1739d32ccimage.png)
1. **浮点常量**
浮点型常量由符号位、整数部分、小数点、小数部分和E指数部分组成
```c
3.14159 // 合法
3.14159E5L // 合法 3.14159*10^5
510E // 非法 不完整的指数
110E // 非法 没有小数或指数
.e55 // 非法 缺少整数或分数
+1.2e+5 // 合法 +1.2*10^5
1.5e-9 // 合法 1.5*10^-9
-5.0e10 // 合法 -5.0*10^10
```
1. **字符常量**
字符常量用单引号包含,例如:'x' 可以存储在 
字符常量可以是一个普通的字符(例如 'x')、一个转义序列(例如 '\t'),或一个通用的字符(例如 '\u02C0')。
在 C 语言中,有一些特定的字符,当它们前面有反斜杠(转义字符 \ )时,它们就具有特殊的含义,被用来表示如换行符(\n或制表符\t等。
| 转义序列 | 含义 |
| -- | -- |
| \\ | \ 字符 |
| \' | ' 字符 |
| \" | " 字符 |
| \? | ? 字符 |
| \a | 警报铃声 |
| \b | 退格键 |
| \f | 换页符 |
| \n | 换行符 |
| \r | 行首符 |
| \t | 水平制表符 |
| \v | 垂直制表符 |
| \ooo | 一到三位的八进制数 |
| \xhh . . . | 一个或多个数字的十六进制数 |
\开头的表示八进制数对应的字符,如:'\43' --> 043 --> 100011 --> '#'
```c
char a = '\43'; // char a = 043; char a = '#'; // 没有分别
char *s = "\61\62"; // char *s = "12" //没有区别
char *s = "061062"; // 表示就是字符串"061062"不等价于char *s = "12";
```
\x开头表示十六进制数对应的字符'\x31' --> 0x31 --> '1'
```c
char c = '\x31'; // 等价于 char c = 0x31; 或 char c='1';
char *s = "\x31\x32"; // 等价于 char *s = "12";
char *s = "0x310x32"; // 表示就是字符串"0x310x32"不等同于char *s = "12";
```
字符常量中内存中存放时是存放其对应的ASCII码值实际上字符常量在内存中是单字节的整型常量数据
![](images/WEBRESOURCEda3558d0f467fb2e8b63e0e13cc8213dstickPicture.png)
**记忆ASCII'A' --> 65 'a' --> 97 '0'-->48**
![](images/WEBRESOURCE4f76333942e38a6ebe5008d9491852a9stickPicture.png)
**总结:整型常量、字符常量、浮点常量都可通过对应的类型的变量进行表示。**
1. **字符串常量**
字符串常量用双引号包含,例如:"xyz",字符串量使用指针或数组表示,具体细节如下
- 字符串在内存中实际是一个连续的字符常量组合
- 任何字符串都以一个字符'\0'作为结束标记,如"funny story"在内存中的存储细节如下
![](images/WEBRESOURCE1a26d5b47439bd40860c97725e4110c5image.png)
![](images/WEBRESOURCE999e2e4cb93fdf4ec8adf3a8df201afcimage.png)
- **注意**""表示一个字符串虽然是空的字符串但其内存不空拥有内存1字节保存的是'\0'字符。
1. **常量的定义**
在C语言中常量的表示方法有两种方式
1. 使用#define宏定义预处理指令表示
```c
#defing PI 3.14 // PI表示浮点常量3.14在预处理时使用了PI的代码会被3.14自动替换
// 这样使用的目的时将字面量在源码中进行隐藏使用更容易理解的单词进行表示可以提高代码的易读性
```
1. 使用const关键字, const关键字用于声明一个只读变量即变量的值不能再发生修改使用const关键声明常量时必须初始化赋值。
```c
const int a = 5; // a是一个只读变量即a是一个常量其值为5
a = 10; // 错误,不允许被修改
```
![](images/WEBRESOURCE51b11638af5912a048a35324caaea19cimage.png)
在C/C++中存在两种类型的表达式
1. 左值lvalue表示的是一个对象的标识符如变量名它可以出现在赋值语句的左侧
```c
int a = 10; // a既是左值也可以作为其他表达的右值
```
1. 右值rvalue表示一个临时对象或表达式如常量、函数返回值等它不可以出现在赋值语句的左侧
```c
int a = 10; // 10是右值不能作为左值
```
# 四、作业
[数据类型测试题.docx](attachments/WEBRESOURCE1ebc5cddf8773c3fb139b21db91946c8数据类型测试题.docx)
在作业文档的前面添加日期提交到 文件浏览器 作业文件夹下的自己名字文件夹中 然后在在线表格中登记
【腾讯文档】CQ2605-作业统计表
[https://docs.qq.com/sheet/DVHhFTkdQUXF6RWRP?tab=BB08J2](https://docs.qq.com/sheet/DVHhFTkdQUXF6RWRP?tab=BB08J2)
# 五、输入输出格式化控制符
![](images/WEBRESOURCE31a9952598334421c011d9f722ff3cceimage.png)
![](images/WEBRESOURCEa4614147cc011d922c72d84cca2528f3image.png)
![](images/WEBRESOURCE3c5663641a0b8dbf76f912751edbc3a3image.png)
输入scanf(); // 默认从键盘输入,只需求取数据
```c
#include <stdio.h>
int main(int argc, char *argv[])
{
    int years,month,day;
    /* scanf格式化输入输入过程必须按照scanf中要求的符号进行输入否则无法正确的进行输入数据 */
    // &表示取址  *表示取值
    scanf("%d%d%d",&years, &month, &day);  // 两个数据间可用空格或回车隔开scanf不能接收空格和回车
                                           // 若输入空格或回车则判定为本次输入的结束,不要加\n否则回车将失效                                      
    printf("%d/%d/%d\n", years, month, day);
    scanf("%d-%d-%d",&years, &month, &day);  // 输入数据间必须添加-  2025-7-10                        
    printf("%d/%d/%d\n", years, month, day);
    scanf("%da%da%d",&years, &month, &day);  // 输入数据间必须添加a     2025a7a10                    
    printf("%d/%d/%d\n", years, month, day);
    char  str[10];
    scanf("%s", str); // 数组的名字就是数组的地址, 输入过程中的空格或回车视为输入终止
    printf("%s\n", str);
    float f;
    scanf("%f", &f);
    printf("%f\n", f);
    char ch;
    scanf("%c", &ch);
    printf("%c\n", ch);
    return 0;
}
```
输出printf(); // 默认输出到终端屏幕
```c
#include <stdio.h>
int main(int argc, char *argv[])
{
    int a = 1;
    int b = 100;
    /* \n 表示换到下一行 */
    printf("%5d\n", a); // 输出有符号十进制数右对齐不足5位在左添空格
    printf("%5d\n", b); // 输出有符号十进制数右对齐不足5位在左添空格
    printf("%-5d\n", a);// 输出有符号十进制数左对齐不足5位在右添空格
    printf("%-5d\n", b);// 输出有符号十进制数左对齐不足5位在右添空格
    char c = 12;
    short d = 10;
    printf("d = %hd\n", d); // 输出半个有符号十进制数
    printf("c = %hhd\n", c); // 输出半半个有符号十进制数
    int a1 = 10;
    printf("%d\n", a1); // 输出有符号的十进制数
    unsigned int a2=20;
    printf("%u\n", a2); // 输出无符号的十进制数
    int a3 = 10;
    printf("%o\n", a3);  // 输出无符号八进制数,不带前缀
    printf("%#o\n", a3);  // 输出无符号八进制数,带前缀
    printf("%x\n", a3);  // 输出无符号十六进制数,不带前缀,字母部分小写
    printf("%#x\n", a3);  // 输出无符号十六进制数,带前缀,字母部分小写
    printf("%X\n", a3);  // 输出无符号十六进制数,不带前缀,字母部分大写
    printf("%#X\n", a3);  // 输出无符号十六进制数,带前缀,字母部分大写
    char a4 = 'a';
    printf("%c\n", a4); // 输出字符
    char *s = "abcd";
    printf("%s\n", s); // 输出字符串,%s遇到'\0'自动结束
    float a5 = 3.145592;
    printf("%f\n", a5); // 输出单精度浮点数小数形式默认6位精度
    float a6 = 3141592.6;
    printf("%e\n", a6); // 输出单精度浮点数(指数形式)
    printf("%.2f\n", a5); // 输出单精度浮点数小数形式2位精度
    printf("%.1f\n", a5); // 输出单精度浮点数小数形式1位精度
    printf("%.3e\n", a6); // 输出单精度浮点数(指数形式),3位精度
    int *p = &a;
    printf("%p\n", p); // 输出指针的值(地址)
    printf("%.3s\n", s); //只输出前3个字符
    long a7 = 10;
    printf("%ld\n", a7); // 输出有符号长整型十进制数
    long long a8 = 10;
    printf("%lld\n", a8); // 输出有符号长长整型十进制数
    char *s1 = "zhangsan";
    char *s2 = "lisi";
    char *s3 = "wangwu";
    int n1 = 10, n2 = 20, n3 = 30;
    printf("%-12s%-12s%-12s\n", s1,s2,s3);  // 左对齐,不足添空格
    printf("%-12d%-12d%-12d\n", n1,n2,n3);
    printf("%12s%12s%12s\n", s1,s2,s3);// 右对齐,不足添空格
    printf("%12d%12d%12d\n", n1,n2,n3);
    printf("%s\t%s\t%s\n", s1,s2,s3);// \t水平制表符
    printf("%d\t%d\t%d\n", n1,n2,n3);
    return 0;
}
```
## 练习
编写代码在终端输出如下内容
![](images/WEBRESOURCEd5ac80a4126ac71c2086d85336dbf99dimage.png)
# 六、IO流
1. **概念**
键盘是系统的标准输入设备从键盘输入数据称为标准输入stdin屏幕终端是系统的标准输出设备在屏幕上输出数据称为标准输出stdout在屏幕上输出出错信息称为标准出错stderr这些输入输出称为IO流。
在计算机系统中当需要使用一种或多种IO流设备时计算机系统就会自动的形成三种缓冲机制stdin、stdout、stderr用于在程序执行期间存储IO的数据这样的缓冲机制称为缓冲区临时存放数据数据是无价的
![](images/WEBRESOURCEe80dc3d48db078411a57b515668d8e57image.png)
```c
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
    int a = 0;
    char c = 0;
    // scanf("%d", &a);
    // getchar();// 清除stdin中上一次输入的残留
    // scanf("%c", &c);
    // printf("a=%d c=%c\n", a, c);
    // printf("明日复明日,明日何其多。"); // 默认存在于stdout ,程序正常结束会自动冲刷
    // printf("明日复明日,明日何其多。\n"); // \n stdout遇到换行会自动冲刷
    /* while (1)
    {
        printf("明日复明日,明日何其多。");   // 当stdout满时自动冲刷
        usleep(100*1000);
    } */
   
    /* printf("明日复明日,明日何其多。");  //缓冲区切换时会自动冲刷
    scanf("%d", &a); */
    printf("明日复明日,明日何其多。");
    fflush(stdout);  // 手动的冲刷stdout缓冲区
    pause(); // 暂停向下执行
    return 0;
}
```
**注意:**
1. scanf函数中的控制串不能随便乱写尤其是结尾的'\n'因为用户必须按照scanf函数中描述的控制串中的内容进行输入数据否则将输入失败scanf()函数是无法从键盘获得空格(' ')和回车('\n')若需要获得空格和回车则建议使用fgets函数。
1. scanf函数是有返回值的并且返回的值是正确输入的个数。
## 练习
编程实现如下功能:
- 如果用户输入大小写字母则输出字母对应的ASCII码值。
- 如果用户输入ASCII码值则输出对应的大小写字母。
# 七、类型转换
- 概念:不一致但相兼容的数据类型,在同一表达式中将发生类型转换
- 转换模式:
- 隐式转换:系统按照隐式规则自动进行转换
- 显示转换:也称强制转换,用户显式的自定义进行转换
- 隐式规则:从小类型到大类型转换,目的表达式中的数据精度不丢失
![](images/WEBRESOURCEf9401753ae5405e97b73dbc1662bfc9001416f17412c415232ef6faf014cdff5.png)
```c
char a = 'a';
int b = 12;
float c = 3.14;
float x = a + b - c; // 在该表达式中将会发生隐式类型转换所有的操作数都会被提升为float
```
- 显示转换:用户强行将一个类型转换为另一个类型,此过程可能会发生精度缺失
```c
char a = 'a';
int b = 12;
float c = 3.84;
float x = a + b - (int)c;// 在该表达式中a将隐士的转换为int类型
// c将被强制的转换为int类型同时丢失精度
// 运算结果将隐式转换为float类型
```
- 总结:不管是隐式转换还是强制转换,变换的都是操作数在运算过程中的类型,是临时发生的,操作数本身的数据类型不会发生修改,也无法修改。
- 数据类型转换的本质:各种不同的数据类型实际上在内存中都一样的都是二进制数,数据的类型描述相当于是用户于系统的一种约定,在用户能够接收后果的情况下这个约定可以被临时打破,但数据的本身不会发生改变。
# 八、可移植整型
- 概念:同一种整型数据在不同的编译系统下数据内存尺寸会发生变化,相同的程序在位数不同系统下运行得到的结果可能发生变化,因此可移植数据类型讨论的是相同的代码不关放到什么系统中,尺寸都保持不变的整型数据。
- 整型数据的尺寸C语言的标准并未规定整型数据的具体大小只规定了相互间的大小"相对大小"short不可比int长long不可比int短长整型数据的长度等于系统的字长。
- 系统字长CPU一次处理数据的长度称为字长。如32位系统CPU一次处理数据以32bit为单位64位系统CPU一次处理数据以64bit为单位。
- 数据类型的典型大小:
| 数据类型 | 16 位平台(字节) | 32 位平台(字节) | 64 位平台(字节) | 说明 |
| -- | -- | -- | -- | -- |
| char | 1 | 1 | 1 | 始终为 1 字节8 位),用于存储字符或小整数 |
| short | 2 | 2 | 2 | 至少 2 字节,通常固定为 2 字节16 位) |
| int | 2 | 4 | 4 | 与平台 “字长” 相关16 位平台为 2 字节32/64 位平台通常为 4 字节(但不绝对) |
| long | 4 | 4 | 8 | 32 位平台与 |
| long long | 8 | 8 | 8 | C99 标准引入,固定为 8 字节64 位),用于表示更大范围的整数 |
- 可移植整型关键typedef
```c
typedef int int32_t; // 给类型int取个别名为int32_t
typedef long long int64_t; // 给类型long long取个别名为int64_t
```
思路为所有系统提供一组固定的、能反映数据尺寸的、统一的可移植整型名称然后在不同的系统中为这些可移植整型提供对应的typedef语句即可例如Linux中"/usr/include/stdint.h"。
```c
int8_t // typedef char int8_t
int16_t
int32_t
int64_t
uint8_t // typedef unsigned char uint8_t
uint16_t
uint32_t
uint64_t
pid_t
time_t
size_t
...
```

View File

@@ -0,0 +1,328 @@
- 概述运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 语言内置了丰富的运算符,并提供了以下类型的运算符:
- 算术运算符:+、-、*、/、%、++、--
- 关系运算符:>、<、>=、<=、==、!=
- 逻辑运算符:&&、||、!
- 位运算符:&、|、~、<<、>>、^
- 赋值运算符:=、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=
- 杂项运算符:&、*、sizeof、?:、return、,
注意:
运算符可根据操作数的个数进行分类,有单目(一元)、双目(二元)、三目(三元)
| 操作数数量 | 运算符类型 | 具体运算符 |
| -- | -- | -- |
| 单目 | 自增 / 自减 | ++ |
| 正负号 | + |
| 逻辑非 | ! |
| 按位取反 | ~ |
| 地址与解引用 | & |
| 类型转换 | (类型) |
| 长度计算 | sizeof |
| 指针成员访问 | -> |
| 双目 | 算术运算 | + |
| 关系运算 | == |
| 逻辑运算 | && |
| 按位运算 | & |
| 赋值运算 | = |
| 逗号运算 | , |
| 三目 | 条件运算 | ? : |
# 一、算术运算符
![](images/WEBRESOURCE2fdad5c1e645e6811dfb870a3d8b4c56image.png)
- 注意:
- C 语言中很多关键字在不同的场合具有不同的意思, 比如”+”和”-”, 如果它们只有一个操作数, 则分别是取正和取负运算, 比如+10 -a 等, 如果它们左右两边有两个操作数, 则分别为加法和减法, 如 a-3。 再如星号”*”, 其出现在定义语句时为指针标记, 只有一个操作数时为解引用运算符, 有两个操作数时为乘法运算, 而与正斜杠”/”一起使用时又表示注释符。 由此可见, 同样的运算符在不同的场合有不同的意思。
- 两个整型相除的结果是整型, 小数部分将被舍弃(而不是四舍五入) 比如 17/10的结果是 1。
- 取模运算符左右两边的操作数都必须是整型(不仅是整数, 比如 3.0 在数学意义上是整数, 但在计算机中它是一个浮点型数而不是整型数, 这样的数据是不能作为取模运算的操作数的)
## 练习
从键盘输入一个三位数,在分别打印出它的个位十位和百位
拓展:任意位数,打印每一位
- ++:自增 --:自减
- 自增自减运算符使操作数加 1 或者减 1 当其作为前缀(如++a 时先进行自增自减再参与运算, 当其作为后缀(如 a++ 时先参与运算再进行自增自减。
```c
int a=10;
printf("a=%d\n", a++); // 后缀先用后加 10
printf("a=%d\n", a);   // 11
int b=10;
printf("b=%d\n", ++b); // 前缀先加后用 11
```
> int a=5,b=4;
> int c=a+++b; // a++ +b(√) a+ ++b a:6 b:4 c:9
> int d=a+++++b;// a++ ++ +b //编译报错 a:7 b:5 c:9 d:10 int d=a+++(++b);// a:7 b:5 c:9 d:11
> 问a,b,c,d的值分别是多少
# 二、关系运算符
![](images/WEBRESOURCE059f0c15158165303fc0fc990a90a445image.png)
- 注意:关系运算符的结果为真(1)或假(0),判断等于运算符是==,而=是赋值运算符若if(x==1)写为了if(x=1)编译时不报错,但判断表达式变成了赋值表达式,其结果取决于赋值的值是真(非0)还是假(0)为了避免这样的问题发生则建议在书写判断表达式时将常量放在左边如if(1==x)如果写成了if(1=x)则编译时会报错。
- 不等于运算符可以省略的情况
- int a; if(a) ==> if(a!=0)
- char a; if(a) ==> if(a!='\0')
- int *a; if(a) ==> if(a!=NULL)
- bool a; if(a) ==> if(a!=false)
```c
void send_string(char *msg)
{
while(msg && *msg) // msg!=NULL *msg!='\0'
{
msg++;
}
}
```
# 三、逻辑运算符
- 数学上的布尔代数, 可以用 C 语言中的逻辑运算符来表达, 也可以用位运算符来表达。当逻辑运算的操作数是表达式时用前者, 当操作数是位时用后者。
![](images/WEBRESOURCE167c2f360443ad063a8c62e5be84ad85image.png)
- 逻辑运算符的结果为逻辑真(1)或逻辑假(0),逻辑反运算符是单目运算符, 也就是说它只有一个目标操作数;逻辑与和逻辑或是双目运算符,它们对左右两边的表达式进行与操作和或操作
![](images/WEBRESOURCEba4b56e9d9f7a758da348629635d6cf6image.png)
逻辑与&&:将两个表达式串联起来,当且仅当左右两个表达式都为真时结果为真,否则为假。
逻辑或||:将两个表达式并联起来,当且仅当左右两个表达式都为假时结果为假,否则为真。
在条件表达式中若逻辑与&&左值为假右值将不进行判定,逻辑或||左值为真右值将不进行判定
## 练习
从键盘输入三个数,判断能否构成三角形
# 作业1
1. 编写程序实现从键盘输入年月日,输出该日期时该年的第几天。
1. 中国古代数学家张丘建在他的《算经》中提出了一个著名的“百钱买百鸡问题”,鸡翁一,值钱五,鸡母一,值钱三,鸡雏三,值钱一,百钱买百鸡,问翁、母、雏各几何?
1. 有3对情侣参加婚礼3个新郎为A、B、C3个新娘为X、Y、Z有人想知道究竟谁与谁结婚于是就问新人中的三位得到如下结果A说他将和X结婚X说她的未婚夫是CC说他将和Z结婚。这人事后知道他们在开玩笑说的全是假话。那么究竟谁与谁结婚呢
> 建立world文档注明题目编号将源代码及运行结果截图写入文档以自己的名字命名提交到飞秋
# 四、位运算符
![](images/WEBRESOURCEea237a3e83f613d86fdfed8580c001c5image.png)
- 以二进制的bit(位)进行运算,逐位进行运算
- 运算规则:
- 按位与&有0为0全1为1 如1001 & 0110 ==> 0000
- 按位或|有1为1全0为0 如1001 | 0110 ==> 1111
- 按位取反~,单目逐位取反 如:~1010 ==> 0101
- 按位异或^相同为0不同为1 如1011 ^ 0110 ==> 1101
```c
1011 ^ 0110 ==> 1101
1011 ^ 1101 ==> 0110
0110 ^ 1101 ==> 1011
// 常用于在不使用额外内存的情况下实现两数交换
a = a ^ b;
b = a ^ b;
a = a ^ b;
```
- 按位左移<< 数据向左(高位)移动,右(低位)补0移出范围直接丢弃
- 按位右移>> 数据向右(低位)移动,左(高位)补0移出范围直接丢弃
![](images/WEBRESOURCEf043bb345ce02a43791f1afe7466e30dimage.png)
## 练习
1. 假设有如下程序请问输出结果是什么?
1. 假设有一个无符号32位整型数据 unsigned int data=0x12FF0045请**编写程序**使用位运算将data的14、15位修改为1将22、23位修改为0其他位保持不变输出结果。
# 作业2
1. 写出你知道的将两数交换的所有方法
# 五、赋值运算符
| 运算符 | 描述 | 实例 |
| -- | -- | -- |
| = | 简单的赋值运算符,把右边操作数的值赋给左边操作数,左操作数不能是常量 | C = A + B 将把 A + B 的值赋给 C |
| += | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C += A 相当于 C = C + A |
| -= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C -= A 相当于 C = C - A |
| *= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C *= A 相当于 C = C * A |
| /= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C /= A 相当于 C = C / A |
| %= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C %= A 相当于 C = C % A |
| <<= | 左移且赋值运算符 | C <<= 2 等同于 C = C << 2 |
| >>= | 右移且赋值运算符 | C >>= 2 等同于 C = C >> 2 |
| &= | 按位与且赋值运算符 | C &= 2 等同于 C = C & 2 |
| ^= | 按位异或且赋值运算符 | C ^= 2 等同于 C = C ^ 2 |
| |= | 按位或且赋值运算符 | C |= 2 等同于 C = C | 2 |
```c
int a = 12;
a += 13; // a = a+13; a 25
a *= 2; // a = a*2; a 50
a %= 5; // a = a%5; a 0
a += 5+2; // a = a+(5+2) a 7
a *= 5+8; // a = a*(5+8) a 91
```
# 六、杂项运算符
| 运算符 | 描述 | 实例 |
| -- | -- | -- |
| sizeof(x) | 计算x所占内存的长度单位是字节。 | int asizeof(a) 将返回 4其中 a 是整数。 |
| & | 取得变量的地址(指地址)。 | &a; 将给出变量在内存中的地址。 |
| * | 取得变量地址中的值(指针)。 | *a; a是一个地址将地址中的值取出。 |
| ? : | 条件表达式(三目运算符) | X>Y?X:Y 如果X>Y为真 ? 则值为 X : 否则值为 Y |
| return | 在函数中返回一个值给上一级函数 | |
| , | 逗号运算符 | |
- sizeof(),它不是函数,是一个运算符;
```c
int a;
// 计算a的内存大小
sizeof (a);
sizeof a; // 若sizeof后跟变量名称则括号可以省略
sizeof (int);
sizeof int; // 若sizeof后跟数据类型则括号不可以省略
```
- 在C语言中&没有左操作数时表示取址(取址符),否则表示位与。取址的作用是获取存储数据变量的地址。
```c
int a = 500;
printf("a address:%p\n", &a); // 地址采用十六进制数表示
```
- 在C语言中*没有左操作数时表示取值(解引用符),若左操作数是数据类型则表示声明定义一个指针,否则表示乘法。
```c
int a = 20;
int *p = &a; // *表示声明定义一个指针p(用于存放地址的变量)
*p = 30; // *表示解引用p指针访问p所保存的地址的值并将这个值修改为30
*p; // *表示解引用p指针,访问p所保存的地址的值
```
- return的作用是在函数中将本函数中的值(通常是执行结果),返回给调用本函数的函数。
```c
int fun(void)
{
return 886; // 将886返回个调用者
}
int main(void)
{
int a = fun(); // 调用fun函数并接收其返回值
return 0;
}
```
- 逗号运算符',',运算时从左到有,整个表达式的结果取决于最后一个表达式。
```c
int a=1,b=2;
int x = (a++,b++,a+b); // 需要使用括号包含,因为,运算符的优先级最低
// 计算顺序a++ b++ a+b的结果赋值给x ==> 5
```
## 练习
- 输入三个数,输出最小值和最大值(体验不同运算符的区别)
- 学习成绩score>=90用'A'80~89用'B', 60~79用'C,60以下用'D'表示,编写代码输入成绩输出对应的的等级
# 七、运算符优先级
| 优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
| -- | -- | -- | -- | -- | -- |
| 1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | -- |
| () | 圆括号 | (表达式)/函数名(形参表) |
| . | 成员选择(对象) | 对象.成员名 |
| -> | 成员选择(指针) | 对象指针->成员名 |
| 2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
| ~ | 按位取反运算符 | ~表达式 |
| ++ | 自增运算符 | ++变量名/变量名++ |
| -- | 自减运算符 | --变量名/变量名-- |
| * | 取值运算符 | *指针变量 |
| & | 取地址运算符 | &变量名 |
| ! | 逻辑非运算符 | !表达式 |
| (类型) | 强制类型转换 | (数据类型)表达式 | -- |
| sizeof | 长度运算符 | sizeof(表达式) |
| 3 | / | 除 | 表达式/表达式 | 左到右 | 双目运算符 |
| * | 乘 | 表达式*表达式 |
| % | 余数(取模) | 整型表达式%整型表达式 |
| 4 | + | 加 | 表达式+表达式 | 左到右 |
| - | 减 | 表达式-表达式 |
| 5 | <<  | 左移 | 变量<<表达式 | 左到右 |
| >>  | 右移 | 变量>>表达式 |
| 6 | >  | 大于 | 表达式>表达式 | 左到右 |
| >= | 大于等于 | 表达式>=表达式 |
| <  | 小于 | 表达式<表达式 |
| <= | 小于等于 | 表达式<=表达式 |
| 7 | == | 等于 | 表达式==表达式 | 左到右 |
| = | 不等于 | 表达式!= 表达式 |
| 8 | & | 按位与 | 表达式&表达式 | 左到右 |
| 9 | ^ | 按位异或 | 表达式^表达式 | 左到右 |
| 10 | | | 按位或 | 表达式|表达式 | 左到右 |
| 11 | && | 逻辑与 | 表达式&&表达式 | 左到右 |
| 12 | || | 逻辑或 | 表达式||表达式 | 左到右 |
| 13 | ?: | 条件运算符 | 表达式1? | 右到左 | 三目运算符 |
| 14 | = | 赋值运算符 | 变量=表达式 | 右到左 | -- |
| /= | 除后赋值 | 变量/=表达式 |
| *= | 乘后赋值 | 变量*=表达式 |
| %= | 取模后赋值 | 变量%=表达式 |
| += | 加后赋值 | 变量+=表达式 |
| -= | 减后赋值 | 变量-=表达式 |
| <<= | 左移后赋值 | 变量<<=表达式 |
| >>= | 右移后赋值 | 变量>>=表达式 |
| &= | 按位与后赋值 | 变量&=表达式 |
| ^= | 按位异或后赋值 | 变量^=表达式 |
| |= | 按位或后赋值 | 变量|=表达式 |
| 15 | | 逗号运算符 | 表达式,表达式,… | 左到右 | 从左到右顺序运算 |
记忆办法:去掉一个最高优先级括号(),去掉两个最低优先级赋值和逗号
![](images/WEBRESOURCE024f291ce6ddbfc8e43e98913881be10image.png)
[C语言运算符优先级顺口溜](https://blog.csdn.net/wt051133/article/details/145663515)
# 练习
[运算符1.docx](attachments/WEBRESOURCEbbcc325c1c1c565fa7dbe4f13a3f2f9d运算符1.docx)
# 作业
[运算符测试.docx](attachments/WEBRESOURCE757c8acbaa371ecec8cc55fab8756448运算符测试.docx)

View File

@@ -0,0 +1,380 @@
# 一、分支控制
1. **二路分支**
- if...else语句
- 执行逻辑:非此即彼
![](images/WEBRESOURCE22d41843cc2f8e164fc5c3cd6753c92fimage.png)
![](images/WEBRESOURCEa9b36a9ebb8b287ddf16ffd2c09976d5image.png)
- 语法形式
```c
if( expression ) // 当 expression为一个条件表达式当为真时 执行 statements 1
{
statements 1
}
else // 当 expression 为假时, 执行 statements 2
{
statements 2
}
// 或
if( expression ) // 当 expression 为真时, 执行 statements 1
{
statements 1
}
```
1. **多路分支**
- **if...else多重嵌套**
- 表达一种多个选项中选择一个条件执行当前面的条件满足后面的if...else直接跳过当没有条件满足时则执行最后的else多选一。
![](images/WEBRESOURCE14ccf6590f7f4016b1afdcb6b9f04f4dimage.png)
- **if语句单独使用多次**
- 表达一种多种条件种选择执行的逻辑前面的if语句满足条件执行后面的if继续判断执行多选多或多选一。
![](images/WEBRESOURCEa492c1ba1be94ca9886a50ce9884cbc8image.png)
- **注意:**
- else语句只与前面最近且同级的if配套并且else不能单独使用。
- 若if语句或else语句中需要执行的代码只有一条语句则{}花括号可以省略,其他情况下{}花括号不能省略否则只有第1条语句属于if...else语句中
- **switch...case语句**
- 执行逻辑:根据选项选择不同的代码段进行执行
- 语法形式:
```c
switch(选择语句)
{
case 选项1:
break;
case 选项2:
break;
...
case 选项n:
break;
default:
break;
}
```
- 表达多个选项中选择一个进行执行
![](images/WEBRESOURCEaa39bdca22d940a0bb7d172aabd9a55dimage.png)
- **注意**
- switch语句中的选择语句必须是整数(整型、字符型、布尔型、枚举型、变量、 运算表达式甚至是函数调用case 语句中的选项必须是整型常量或char 型常量const 型变量都不行, 例如case 'w'
![](images/WEBRESOURCE4552fb76ccd7d5818b2a9d3abcf0061bimage.png)
- break语句是switch语句的结束标志。
- 一旦case选项匹配成功则立即执行其对应的语句直至遇到break语句。
- 不是所有的case语句都得加break语句但如果case语句后没有break语句则会无条件向下执行其他case语句直至遇到break语句或到switch语句的}花括号结束。
- default默认选项语句可以写也可以不写可以放在case的末尾也可放case最前或case语句选项的中央当所有的case选项都不匹配时则会执行dafault语句后的代码块
- case语句后可以为空语句如case 1:
## 练习
1. 编写程序实现,从键盘输入三个数从小到大输出到终端
1. 编写程序实现,从键盘输入一个字符串,将输入的大写字符转换为小写字母,小写字母转换为大写字母输出。
1. 从键盘输入一个数(0~15),将其输出为16进制数
## 作业
1. 从键盘输入一个字符串,统计输入的字符串中的大写字母、小写字母、数字、其他字符的个数【使用if语句和switch语句分别实现】
- **直接跳转**
- goto语句通常被告诫不要使用由于 goto 语句是一种无条件的直接跳转,有时甚至还会破坏程序的栈逻辑,因此不推荐使用,但是当我们在编写程序错误处理代码的时候,又会经常用到它,这是因为当程序发生错误时通常报告错误和及时退出比保护程序逻辑更加重要,并且 goto 语句可以忽视嵌套包裹它的任何代码块,直接跳转到错误处理单元。
```c
#include <stdio.h>
/* 除了出错处理其它情况一概不推荐使用goto因为goto会直接跳转到标签位置使得程序难以追踪难以阅读和修改 */
int main(int argc, char *argv[])
{
    printf("[%d]\n", __LINE__);
     goto globle;  // 往后跳
lable:   // 标签
    printf("[%d]\n", __LINE__);
    goto lable;  // 往前跳
globle:
    printf("[%d]\n", __LINE__);
    return 0;
}
/* 推荐使用场景 */
int main(void)
{
 // 初始化LCD
        goto lcd_err;
    // 初始化触摸屏
        // 出错 则需要释放LCD资源
        goto ts_err;
    // 开启采集环境温湿度
        // 出错 则需要释放LCD资源和触摸屏资源
        goto ht_err;
    // 获取环境光照
        // 出错 则需要释放LCD资源、触摸屏资源、温湿度模块资源
        goto light_err;
    // ....
light_err:
    ht_free();
ht_err:
    ts_free();
ts_err:
    lcd_free();
lcd_err:
    return 0;
}
```
# 二、循环控制
- **while语句**
```c
while(expression)
{
statements;
}
```
expression 可以是任意表达式C 语言中任何表达式都有一个确定的值), while语句根据表达式 expression 的值来决定是否执行下面的 statement被执行的这些语句也叫循环体 可以是一句简单的语句<可省略花括号> 也可以是用花括号括起来的若干条语句组合起来的复合语句<必须使用花括号包含> 如果 expression 的值为假则跳过 statement 如果为真则执行 statement 执行完了再来判断 expression 的值, 不断循环一直到其值为假为止(如果 expression 的值始终为真,则称为死循环或无限循环<Linux中可用ctrl+c强制退出程序>
![](images/WEBRESOURCE64848ec6b188f4b800d87151f5fdec76image.png)
👍
> Linux 的 C 代码风格中, 循环体结构(包括 while、 do…while 和 for 循环)的左花括号既可以写在循环语句的末尾, 也可以单独占一行, 如上述代码所示。 而对于函数而言, 包含函数体的左花括号一般单独占一行。 另外一定要注意缩进, 缩进的目的是为了增强代码的可读性, 在函数体、 循环结构、 分支结构等逻辑相对独立的代码块中都需要有适当的缩进。 每一层代码块推荐用 8 个空格来缩进, 过小则不易区分各个代码块, 在程序嵌套太深时也不能更好地起提醒作用。 当然如果并不经常用不同的编辑器来编辑代码, 用制表符代替空格也未尝不可, 毕竟敲多个空格键比较繁琐(如果需要用不同的编辑器编辑代码的话, 不同的编辑器可能对制表符的解释有所不同, 这就会导致在一款编辑器中显示正常的代码在另一款编辑器中却显示不正常) 。代码良好的可读性和易维护性庞大的工程中显得尤为重要, 因此良好的习惯必须在一开始写简单代码的时候就要养成。
```c
#include <stdio.h>
int main(int argc, char *argv[])
{
    int a;
    scanf("%d", &a);
    // while (a > 0)
    while (a)  // a的值非0 0
    {
        printf("%d ", a);
        a--;
    }
    printf("\n");
   
    return 0;
}
```
- **do...while语句**
```c
do{
statements;
}while(expression);
```
与while 循环类似, 根据表达式中的值来决定是否执行循环体, 区别是 do…while 循环不是先计算表达式, 而是先执行循环体在计算表达式的值, 因此它也被称为退出条件循环,即在每次执行循环体之后再检查判断条件, 这样
![](images/WEBRESOURCEe033a13a5da4e1bb7bc6f275fb254dfaimage.png)
```c
#include <stdio.h>
int main(int argc, char *argv[])
{
    int a;
    scanf("%d", &a);
    do{   // 至少执行一次循环体语句
        printf("%d ", a);  // 先执行循环体
        a--;
    }while (a); // 再进行判断条件是否为真(非0),为真则继续执行循环体中的语句,否则结束循环
    printf("\n");
   
    return 0;
}
```
- **for语句**
for 循环是一种更为灵活的循环结构, 在 Linux 内核中出现的频率大约是 while 循环和do…while 循环的四到五倍
```c
for(initialize; test; update)
{
statement;
}
```
语句块 statement 跟上两种循环体一样, 可以是单条语句也可以是用花括号括起来的复合语句。 关键字 for 之后的圆括号中包含有三个表达式, 第一个表达式initialize一般用来初始化循环控制变量 第二个表达式test一般用作循环测试条件 而第三个表达式update则一般用来更新循环控制变量 但从语法角度上讲它们可以是任意的语句,这些表达式可以是一个表达式或多个表示式(用逗号隔开)
for循环语句的执行流程是
1. 如果有initialize语句则执行它 然后执行test语句如果没有initialize语句则直接执行test语句。
1. 如果有test语句且其值为真或者没有该语句则执行语句statement循环体如果有 test 语句且其值为假则跳出for循环语句。
1. 执行循环体语句 statement 完成后执行update语句。
1. 如果有 update 语句则执行它, 然后跳到第 2 步test 语句, 如果没有 update 语句则直接跳到第 2 步test 语句。
```c
#include <stdio.h>
int main(int argc, char *argv[])
{
    for (int i = 0; i < 10; i++) /* 循环变量定义初始化赋值语句的写法int i = 0;是C99以后支持的
                                    老版编译器默认未采用c99标准则编译指令末尾添加-std=c99*/
    {
        printf("%d ", i);
    }
    printf("\n");
    return 0;
}
```
## 练习
1. 编写代码计算1~1000所有奇数的和【三种循环编写】
![](images/WEBRESOURCEddc658c0cce70a5068ad13c1c7567ac8image.png)
1. 计算1^2+2^2+...+n^2
![](images/WEBRESOURCE61673478243ea23a81ca7bed0da28733image.png)
1. 计算n的阶乘
- **break、continue与return语句**
- **break语句**
- 用于循环语句中时表示跳出当前循环(会结束循环)
- 用于switch语句时表示跳出switch语句
```c
for()
{
while()
{
do{
switch()
{
break;// 结束switch语句
}
break; // 结束do...while
} while();
break; // 结束while
}
break; // 结束for
}
```
- **continue语句**
- 只用于循环语句中,表示结束本次循环,进行下次循环
![](images/WEBRESOURCE459a0a24ac6047112d1d6479e8858315image.png)
- **return语句**
- 表示当前这个函数的结束
- **无限循环**
- 循环条件恒为真
```c
while(1);
for(;;);
do{}while(1);
```
## 练习
用循环在终端打印如下图案
```c
*
**
***
****
```
```c
****
***
**
*
```
```c
*
**
***
****
```
```c
****
***
**
*
```
```c
*
***
*****
***
*
```
```c
*
* *
* *
*******
```
```c
A
ABA
ABCBA
ABCDCBA
```
规律:
# 作业
1. 完成测试题
[判断、循环测试题.docx](attachments/WEBRESOURCE1b1c0ef38fb7e7623e35a7ffc254fd87判断、循环测试题.docx)
1. 编程实现一个学习100以内数学四则运算的C语言小程序要求随机生成题目让用户键入答案正确则输出"答对了!你真棒!",错误则提示"答错了,再接再厉"并运行重新回答,每题共三次机会,三次皆未答对则结束程序,答对则进行下一题,若想结束则输入''Esc"。
(提示100以内随机数生成使用rand()%100并添加头文件"#include <stdlib.h>")。

View File

@@ -0,0 +1,412 @@
# 一、概述
在C语言中函数指的是功能模块。一个典型的C语言程序就是由一个个的功能模块拼接起来的整体因此C语言也称模块化语言。
对于函数的使用者,可以简单的将函数视为一个黑箱子,使用者只管按照规定给黑箱一些输入,就会得到一些输出,使用者不需要关注黑箱内部的结构细节。
![](images/WEBRESOURCE792781c979620a94ca2b918b07a45ee3image.png)
函数分为两种形式
- 系统自带函数(库函数):只需了解如何使用和结果是什么,如:购买了一台电视机,我们只需要了解电视机对外的接口和使用的方法及最后的结果,不需要知道电视机内部构造----怎么用。
> 函数的头文件
> 函数的功能
> 函数的参数
> 函数的返回值
- 用户自定义函数:要明确最终实现的功能,如:自己设计电视机 ----怎么写。
> 函数的声明式
> 函数的实现
> 函数的调用
# 二、函数入门
![](images/WEBRESOURCE915e9aa0eaf899c958273f42c7683f07image.png)
- 函数头:函数对外的接口及运行的结果都在这里体现
- 组成
- 函数的类型是函数的运行结果数据类型即黑箱的输出数据类型函数返回值类型不是必须的没有返回值是使用void。
- 函数的名字:函数在内存中的地址,代表这个黑箱的名称(必须满足用户标识符定义规则),一个函数必须要有名字。
- 函数的参数列表:函数的输入,即黑箱的输入数据列表,不是必须的没有参数输入也不能省略括号()应当在参数列表中使用void进行修饰
- 函数体:函数的功能实现,即黑箱子的内部构造。
![](images/WEBRESOURCE5662a65e83bb0e026bd5798435f41eadimage.png)
```c
/* 给定两个数,得到最大值 */
int Maxfun(int x, int y)  // 这是一个函数的定义该函数接收两个int类型的参数返回一个int类型数据
{
    return x>y?x:y;
}
/* 交换两个浮点数 */
void swap(double *p1, double *p2) // 该函数接收两个double *类型的参数,无返回值
{
    if (p1 == NULL || p2 == NULL)
        return; // 退出当前函数
   
    double tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
/* 液晶屏初始化函数 */
char *initLCD(void)  // 该函数不接收参数返一个char *类型的数据
{
    int lcd = open("/dev/fb0", O_RDWR);
    struct fb_var_screeninfo vinfo;
    ioctl(lcd, FBIOGET_VSCREENINFO, &vinfo);
    int bpp = vinfo.bits_per_pixel;
    int size = vinfo.xers*vinfo.yers*bpp/8;
    char *fbmmem = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHSRED, lcd, 0);
    return fbmmem;
}
```
总结:
- 当函数的类型为void时表示函数不返回任何数据。
- 当函数的参数列表为void时表示函数不需要任何参数。
- 关键字return表示退出函数。
①若函数头中规定有函数的类型则return需要携带一个类型与之匹配的数据
②若函数头中规定函数的类型为void则return不需要携带数据
# 三、自定义函数
- **函数的定义**
- 表示函数的功能实现及函数的确立。
```c
返回值类型 函数名字(参数1, 参数2, ...) // 没有返回值则写void 没有参数也写void
{
功能语句;
}
```
- **函数的声明**
- 表示告诉编译器函数将会被使用,位于函数被调用前的函数外部。
```c
函数的类型 函数名称(参数类型及个数);
```
注意
- 当函数的调用出现在函数的定义位置前,则需要在调用前进行声明。
- 函数的声明一般放在头文件中,调用头文件则携带函数声明式。
- **函数的调用**
- 表示使用函数实现对应的功能,位于一个函数的内部
```c
函数的名字(参数所对应的数据);
```
注意
- 当函数被main直接调用或间接调用时函数才会被执行
```c
#include <stdio.h>
//int Maxfun(int x, int y); // 函数的声明:当函数的调用发生在函数的定义前时需要书写
int Maxfun(int , int ); // 函数的声明:当函数的调用发生在函数的定义前时需要书写
int main(int argc, char *argv[])
{
   
    printf("%d与%d的最大值是%d\n", 80, 800, Maxfun(80,800));  // 将Maxfun函数的执行结果作为printf函数的一个参数
    return 0;
}
/* 交换两个浮点数 */
void swap(double *p1, double *p2) // 函数的定义 该函数接收两个double *类型的参数,无返回值
{
    printf("%s is running\n", __FUNCTION__);
    if (p1 == NULL || p2 == NULL)
        return;
   
    double tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}
/* 给定两个数,得到最大值 */
int Maxfun(int x, int y)  // 这是一个函数的定义该函数接收两个int类型的参数返回一个int类型数据
{
    printf("%s is running\n", __FUNCTION__);
    double a = 3.14, b=5.26;
    printf("交换前a = %f  b = %f\n", a,b);
    swap( &a, &b);  // 调用一个函数:定义在调用前可,免去声明
    printf("交换后a = %f  b = %f\n", a,b);
    return x>y?x:y;
}
```
练习:编写一个函数实现从键盘获取三个数,从小到大输出
```c
/* 输入三个数从小到大输出 */
#include <stdio.h>
void outputNum(void);
int main(int argc, char *argv[])
{
    outputNum();
    return 0;
}
void swap(int *p1, int *p2) // 函数的定义
{
   
    if (p1 == NULL || p2 == NULL)
        return;
   
    *p1 ^= *p2;
    *p2 ^= *p1;
    *p1 ^= *p2;
}
void outputNum(void)
{
    int a,b,c;
    printf("请输入三个数:");
    scanf("%d%d%d", &a,&b,&c);
    if(a>b)
    {
        swap(&a,&b);
    }
    if(a>c)
    {
        swap(&a,&c);
    }
    if(b>c)
    {
        swap(&b, &c);
    }
    printf("%d < %d < %d\n",a,b,c);
}
```
## 函数参数的传递
- 形参与实参
- 形参:函数声明式定义中参数列表中的参数,属于函数的局部变量,在函数定义中只是一个形式参数没有实际的值。
- 实参:函数调用中参数列表中的参数,具有实际的数值或表示对象,在函数执行期间这个数值或对象将作用与函数内部,实参会在函数执行时初始化函数的形参。
- 值传递:表示实参的类型时一个数值,形参的类型是基本数据类型;
- 址传递:表示实参是一个地址,形参的类型是一个指针;
- 当需要将函数内部的形参的改变作用与实参时则需要使用址传递(传递地址),否则使用值传递(传递数值)。
## 总结
- 函数的优点
- 提高代码的重用性。 试想一下, 一个函数也许会在 N 多地方被使用, 如果没有将该功能封装起来, 而在每一个用到这个功能的地方都写一遍代码, 将会浪费很多资源。就像一个企业给每一个员工都配备一台打印机, 虽然每个人独占资源用起来很便捷, 但却浪费了大量的成本。
- 方便维护和升级源代码。 假设需要对一个的算法修正或者修改, 那只要不改变函数接口和功能的情况下, 可以方便地进行, 不需要知道该函数在何处被调用。 调用者也感觉不到代码的改变, 因为函数的封装性使得他们让调用者感觉起来是透明的。
- 有利于结构化代码。 将一个个功能封装在相对独立的函数里, 再将函数组装成程序,那么整个逻辑就很清晰, 出错了也较容易排查。 否则, 在一个没有结构化的代码中, 所有的功能杂乱地挤作一团, 逻辑复杂, 也极易出错。
- 函数封装的要求
- 高内聚:一个功能集中在一个函数的内部。
- 低耦合:功能函数与功能函数间的影响要低。
# 四、主函数传参
- 函数参数列表不为void则表示函数可以接收数据主函数"int main(int argc, char *argv[])"意味着主函数也支持接收数据。
![](images/WEBRESOURCE131d91bf42e75d64d2774081dd4992c2image.png)
![](images/WEBRESOURCEb181fe3155c18772408b6088b6ef9260image.png)
注意:
- 每个命令行参数间使用空格(一个或多个皆可),若参数本身就带有空格则需要使用单引号或双引号将整个内容包含
![](images/WEBRESOURCE04c87caf7ac6f6ffe55d92c338e3f197image.png)
- 一般情况下若需要接收命令行参数,则需要在主函数中判定参数是否符号要求,以保障程序的正确执行
![](images/WEBRESOURCE2f30409c67de5069191ecf8ca0167cd0image.png)
## 练习
接收命令行参数三个,输出这个三个数的和
# 五、变参函数
- 概念:调用函数时可根据实际需求来决定函数参数的个数
```c
int printf(const char *restrict format, ...);
int scanf(const char *restrict format, ...);
printf("%d", a);
printf("%d, %d", a, b);
printf("%d, %d, %d", a, b, c);
```
- 定义变参函数
- 添加头文件"#inclded stdarg.h"
- 定义函数时末尾参数使用省略号(...)表示可以更具需求来确认,省略号前可以自由设置参数类型(至少一个,强制参数)
- 在函数定义中创建va_list类型变量用于存放可变参数
- 使用va_start()用于初始化va_list类型变量初始化内存。
- 使用va_arg() ,来访问可变参数列表中的每个项。
- 使用va_end()来清理va_list变量的内存释放内存。
```c
#include <stdio.h>
#include <stdarg.h>
int fun(int n,...);
int main(int argc, char *argv[])
{
    fun(2, 1, 2);
    fun(5, 1, 2);
    return 0;
}
int fun(int n,...)
{
    // 在函数定义中创建一个va_list变量list将来调用时传递的参数存储在list中
    va_list list;
    // 使用强制参数初始化变量list
    va_start(list, n);
    // 使用va_arg()访问list中的每个项
    for (int i = 0; i < n; i++)
    {
        printf("%d\n", va_arg(list, int));
    }
   
    // 清除list中的缓存
    va_end(list);
}
```
## 练习
编写一个变参函数用于计算多个数的平均值double保留2位精度。
# 六、递归函数
- 概念:如果一个函数在内部调用自身,那么这个函数就是递归函数
- 组成:必须要有一个结束条件(简单条件),否则会导致程序栈溢出。
```c
void fun(void)
{
fun();
}
```
![](images/WEBRESOURCE8a15519dcbff531403a69214e1f98014image.png)
```c
#include <stdio.h>
/* 计算阶乘 */
int func(int n)
{
    if(n==1)
        return n;
    return n*func(n-1);
}
int main(int argc, char *argv[])
{
    int n = 0;
    scanf("%d", &n);
    printf("%d", func(n));
    return 0;
}
```
注意:递归虽好但不是所有情况都适合,比如数据规模太大会导致"栈溢出"
递归函数的特点:代码精简但效率低。
## 练习
- 使用递归打印N的斐波拉契数列
1 1 2 3 5 8 13 ......
- 有5个人坐在一起问第五个人多少岁?他说比第四个人大两岁。问第四个人岁数他说比第三个人大两岁。问第三个人又说比第二个大两岁。问第二个人说比第一个人大两岁。最后问第一个他说是10岁。编写程序当输入第几个人时求出其对应年龄。
# 七、回调函数
- 回调callback 是一种非常重要的机制, 主要可以用来实现软件的分层设计, 使得不同软件模块的开发者的工作进度可以独立出来, 不受时空的限制, 需要的时候通过约定好的接口(或者标准) 相互契合在一起, 也就是 C++或者 JAVA 等现代编程语言声称的所谓面向接口编程。 同时回调也是定制化软件的基石, 通过回调机制将软件的前端和后端分离, 前端提供逻辑策略, 后端提供逻辑实现。
- 作用:统一操作接口,开放功能定制。
# 八、内联函数
- 使用关键字inline关键字修饰的函数称为内联函数
```c
inline void func(void);
```
- 节省函数间切换所需的时间,提高函数的运行效率
- 原理:一个普通函数在调用过程中,会在调用这个函数的函数中形成保护现场和恢复现场的过程,这个需要花费时间,降低程序的运行效率,这时可以将这个函数设计内联函数,在编译器编译过程中,编译器会将符合标准的内联函数直接展开(使用函数的功能代码替换函数的调用),这样就会节省保护现场和恢复现场的时间,同时又做到了模块化编程。
![](images/WEBRESOURCE919546e5273412ccfee06772f169be0bimage.png)
- 内联函数的使用场景
- 代码精简功能语句简短不具有循环、switch等语句。
- 调用频繁
**注意内联函数不是写了inline声明的函数就是内联函数能否构成内联函数是由编译器决定。若添加inline关键字但不符合编译器的标准则编译器会将其视为普通函数。**
[函数练习题.docx](attachments/WEBRESOURCE94606765850c0696e801236ba49014a2函数练习题.docx)
# 作业
[函数作业题.docx](attachments/WEBRESOURCEa2d88b6292b52c177b4bc6323a5bdd07函数作业题.docx)

View File

@@ -0,0 +1,6 @@
---
tags:
- empty
aliases: empty
日期: 2026/1/22
---

View File

@@ -0,0 +1,410 @@
# **1. 准备知识**
**1.1 内存地址**
- 字节:字节是内存的容量单位,英文称为 Byte一个字节有8比特位即 1Byte = 8bits 1B = 8b
- 地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址,简称地址。
![](images/WEBRESOURCE73f0b54259e8460e97f84fd70e8aaef4stickPicture.png)
## **1.2 基地址**
- 单字节数据:对于单字节数据而言,其地址就是其字节编号。
- 多字节数据:对于多字节数据而言,其地址是其所有字节地址中编号最小的那个,称为基地址。
![](images/WEBRESOURCE06e4a4aac18e468d89b657886f10a5b3stickPicture.png)
### **1.3 取址符**
- 每个变量都是一块内存,都可以通过取址符 & 获取其地址
- 注意:&符号的左边有操作数则表示位与运算符,若左边没有操作数则表示取地址符
```c
int main(int argc, char *argv[])
{
    int a=110;
    printf("整型变量a的地址是%p\n", &a);
    char b='a';
    printf("字符型变量b的地址是%p\n", &b);
    float c=3.14;
    printf("浮点型变量c的地址是%p\n", &c);
    printf("整型变量a的地址长度是  %ld\n", sizeof(&a));
    printf("字符型变量b的地址长度是%ld\n", sizeof(&b));
    printf("浮点型变量c的地址长度是%ld\n", sizeof(&c));
    return 0;
}
```
- 总结
- 虽然不同类型的变量的内存尺寸不同,但是他们的地址尺寸(地址编号的位数)却是相同的(地址的尺寸与系统的字长相关32位系统地址尺寸为32位64位系统地址尺寸为64位)
- 不同的地址虽然表示形式一样,但他们所代表的内存尺寸(在内存中所占内存大小)和类型都不相同,因此相同形式的地址编号在逻辑上要严格进行区分
![](images/WEBRESOURCEc93ef0073e59a9197d1df93e15a42da9image.png)
# 2.指针入门
## 2.1指针概念
由于翻译问题及口语表达的习惯,在日常表述中,指针会有以下两种含义
- 指地址
```c
int a;
&a; // 我们可以说&a指向a的地址
```
- 指指针变量
```c
int *p; // 可以用于指向int类型数据的地址
```
- *号左右两边都有操作数,表示两数相乘(a*b)
- *号左右两边都有操作数,且左操作数数一个数据类型表示定义一个指针变量(int *p)
- *号左边没有操作数,表示取值操作(*p)
## 2.2指针的定义
- 用于存放数据类型变量的地址的变量称为指针
```c
int *p1; // 用于存储int类型数据的地址p1被称为int型指针或整型指针
char *p2; // 用于存储char类型数据的地址p2被称为char型指针或字符型指针
double *p3; // 用于存储double类型数据的地址p3被称为double型指针或浮点型指针
```
对于int *p;在32位系统下的理解如下
![](images/WEBRESOURCE2f532ae1a2635b8f6d6eb26f2b49be3eimage.png)
![](images/WEBRESOURCE970015c2b46c95da2d92fad50ded8cadimage.png)
- 注意:指针用于存放地址的变量,由于在相同的系统位数下地址的尺寸相同,故指针的尺寸相同
![](images/WEBRESOURCEe6d0d1a5088db204eab4343b8bff4ef4image.png)
- 指针变量的内存尺寸只于系统字长相关,与指针的类型无关。
## 2.3指针的赋值
- 可将一个地址类型于指针类型相同的地址赋值这个指针
```c
int a=100;
char b='A';
double c=3.14;
p1 = &a; // 将a的地址赋值给指针p1它们类型必须相同
p2 = &b; // 将b的地址赋值给指针p2它们类型必须相同
p3 = &c; // 将c的地址赋值给指针p4它们类型必须相同
int *p4 = p1; // 使用整型指针变量p1值初始化赋值整型指针p4
int *p5;
p5 = p1; // 整型指针变量p1值赋值给整型指针p5
int *p6 = &a; // 使用a的地址初始化赋值整型指针变量p6
```
## 2.4指针的索引
- 所谓索引,指的是通过指针变量取得其指向的目标(访问指针变量中所存储的地址中的数据)
```c
*p1 = 200;
*p2 = 'B';
*p3 = 6.62;
a = 10;
a = 10;
```
![](images/WEBRESOURCEe2db6c7a73c7bc9d780f9639545ccf3eimage.png)
## 练习
编写一个函数实现两个数交换。
# 3.特殊指针
## 3.1野指针
- 指向一块未知区域的指针,称为野指针。**野指针是危险的**
![](images/WEBRESOURCE6a63aa7276f21b9af08cd146f5959cbbimage.png)
- 危害
- 引用野指针相当于访问了非法内存常常会导致段错误segmentatio fault
- 引用野指针,可能会破坏系统关键数据,导致系统崩溃等严重后果。
- 产生原因
- 指针定义式未进行初始化
- 指针指向内存被系统回收
- 指针越界
- 如何防止
- 定义指针时进行及时初始化定义时无法明确指向则赋值为NULL
- 绝不引用已被回收的内存回收内存后令指针指向NULL
- 确认所申请的内存边界,谨防越界
## 3.2空指针
```c
NULL // (void *)0;
```
- 很多情况下,我们不可避免的会遇到野指针,如刚刚定义指针无法立即为其分配一块内存时,又或者指针所指向的内存被释放了等等。
- 对于一个暂时无法让其指向一块合法内存的指针而言, 我们最好将其初始化为“空指针” 即给他赋一个空值(零) 让他指向零地址
![](images/WEBRESOURCE698f0b5e2b11e6c3a965743dd01df4caimage.png)
```c
// 刚刚定义指针无法明确其指向,让其指向零地址以保证安全
int *p = NULL;
char *q = NULL;
//指针指向的内存被释放,令指针立即指向零地址保证安全
char *k = malloc(2); // 给指针分配内存
free(k); // 释放内存
k = NULL;
```
## 3.3void指针
- 概念所谓void指针也称为万能指针(泛型指针)无法明确指针所指向的目标的数据类型则可将这种指针定义为void类型
- 要点:
- void型指针无法索引目标必须将其转换为一个具体的类型指针方可索引目标。
- void型指针不支持加减运算。
> void只用于三种情况
> 定义指针
> 定义函数类型
> 表示函数的参数列表
```c
void swap(void *a, void *b)
{
    // int *p = a;
    // int *q = b;
    // *p^=*q;
    // *q^=*p;
    // *p^=*q;
    *((int *)a) ^= *((int *)b);
    *((int *)b) ^= *((int *)a);
    *((int *)a) ^= *((int *)b);
}
```
## 3.4char型指针
- char型指针实质上与别的类型指针并无本质区别但由于C语言中字符串以字符数组的方式进行存储而数组在大多数场合又表现为指针因此字符串在绝大多数场合下表现为char型指针
```c
char *p = "abcd"; // p指向的目标是字符串常量的地址
*p = 'x'; // 不可以修改常量数据
/* 上面的写法语法上无错,但逻辑不允许,推荐下面的写法,编译器会帮助我们检查语法错误 */
const char *q = "abcd"; // q指向的目标是字符串常量的地址
*q = 'x';
```
## 3.5const型指针
- const(只读)型指针有两种形式:①常指针(指针只读)②常目标指针(目标只读)
- 常指针const修饰指针本身表示指针变量本身无法修改(指针常量)
```c
char * const p;
```
![](images/WEBRESOURCE55871207d1eef11e0fd010e335e4cc0f576913ed193ece92e9ad7fc8a81dc9cf.png)
```c
char a,b;
char  * const p = &a;
// p = &b; // 错误,常指针不能修改其值
*p = 'S'; // 可通过指针对目标进行读写
printf("%c\n", *p);
```
- 常目标指针const修饰指针的目标表示无法通过该指针修改其目标常量指针
![](images/WEBRESOURCE51fc80bb52e549d39a1753ac77f656e5stickPicture.png)
```c
const char  *q = &a;
char const  *k = &a; // k和q等价
q = &b; // 指针指向的目标可以修改
k = &b;
*q = 'a'; // 错误,常目标指针无法写访问目标
printf("%c\n", *k); // 正确。常目标指针读可访问目标
```
- 常指针在实际应用中不常见。
- 常目标指针在实际应用中广泛可见,用来限制指针的读写权限
## 晚上作业
[指针与数组作业.doc](attachments/WEBRESOURCEd71090e9d6659f19f9b36c93099793f4指针与数组作业.doc)
## 3.6函数指针
- 指向函数的指针称为函数指针
- 函数指针与普通指针本质上并无区别,只是在取址和索引时取址符&与索引符*均可省略
```c
 double fun(double a[], int len)
{
   double max = a[0];
   for(int i=0; i<len; i++)
   {
        max = max>a[i]?max:a[i];
   }
   double min = a[0];
   for(int i=0; i<len; i++)
   {
        min = min<a[i]?min:a[i];
   }
   return max-min;
}
int main(int argc, char *argv[])
{
     double num[] = {1.23, 3.21,1.010,2.63,8.52};
     double (*p)(double [], int ) = &fun;  // p是一个函数指针指向一个返回值为double形参为一个double型地址与一个int型数据的函数
     printf("%02f\n", (*p)(num, 5));  // 通过指针p访问目标
     printf("%02f\n", fun(num, 5));
     /* C语言中函数的名字代表函数的地址 */
     double (*q)(double *, int) = fun;  
     printf("%02f\n", q(num, 5));  
     printf("%02f\n", fun(num, 5));
    return 0;
}
```
- 注意函数fun == &fun 数组array != &array
- 要点
- 函数指针是一类专门指向某种类型函数的指针
- 函数的类型(返回值与参数列表)不同,所需要的函数指针也不同(指针的类型由函数的返回值与参数列表共同构成)。
- 函数的类型判定方法是在函数声明式中去除掉用户标识符名称,剩下的既是函数类型
## 3.7多级指针
- 若一个指针变量p1存储的地址是普通变量a的地址则p1就称为一级指针
- 若一个指针变量p2存储的地址是指针变量p1的地址则p2就称为二级指针
- 以此类推p2就被称为多级指针
```c
int a;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
```
# 4.指针的运算
- 指针的运算:加与减(+、-、++、--
- 两个指针运算中'+'无意义,两个指针相加没有实际意义;两个指针做减法运算,结果可以得到两个地址间的偏移量(步长)
```c
int a,b;
int *p = &a;
int *q = &b;
p+q; // 得到一个未知的新地址并无意义
p-q; // 得到两个地址间的偏移量即从a的地址到b的地址间的距离步长
```
- 单指针的加法指的是指针自增运算或加上某个数值,表示指针指向的目标地址向上移动(向内存高地址移动)若干步长(步长与种指针的类型相关,一步为数据类型的内存尺寸)
- 单指针的减法指的是指针自减运算或减去某个数值,表示指针指向的目标地址向下移动(向内存低地址移动)若干步长(步长与种指针的类型相关,一步为数据类型的内存尺寸)
```c
int a = 100;
int *p = &a; // 指针p指向a地址
int *k1 = p+2; // 向上移动2个步长
int *k2 = p-3; // 向下移动3个步长
```
![](images/WEBRESOURCE020956fc6459842a83ae6c650bf7e422image.png)
- 要点数据类型不同步长也不同char *p; p++与int *q; q++; 中p移动一步等于内存的一个字节而q移动一步等于内存的四字节。
```c
int array[10] = {1,2,250,4,270,6,7,8,9,10};
int *p = array;
for (int i = 0; i < 10; i++)
{
    printf("%d\t", *p);
    p++; // 每次移动一步长4字节
}
printf("\n");
char *q=(char *)array;
for (int i = 0; i < 10; i++)
{
    printf("%d\t", *(int*)q);
    q+=4; // 每次移动一步长1字节
}
printf("\n");
```
## 练习
- 编写代码通过指针实现将两个字符串进行拼接(不要使用系统库函数)
-char s1[20] = "hello"; char s2[20] = "world";调用自己写的函数后得到一个新的字符串"helloworld"
```c
```
- 编写函数实现将将整型数组中重复的数字进行剔除,剔除后数组的元素个数要发生变化。
-int array[] = {1,2,3,3,4,3,4,5,5};调用函数后变为{1,2,3,4,5};
```c
```

View File

@@ -0,0 +1,312 @@
# 一、数组与指针间的转换
1. **数组与指针混合运算时**
- 在 C 程序中,除了指针可以进行加减法之外,数组也经常参与其中,但是 C 语言不认为数组是基本数据类型,因此没有像“指针运算”那样发明“数组运算”。而我们又确确实实可以写出数组参与运算的代码。
```c
int a[3] = {100, 200, 300};
int *p=a;
int *q;
q = p + 1;
p = a + 1; //a移动一步长步长与元素类型相关
```
- 要点:
- 任何数组的名字 a除了在其定义语句和 sizeof 语句之外均代表其首元素a[0]的地址。
- 数组 a 的首元素是 a[0],因此其中的 p = a + 1 实际上等价于 p = &a[0] + 1而又由于 a[0]的数据类型是 int 型,因此地址&a[0]的类型就是 int *。
![](images/WEBRESOURCEb33d0d9cc91e3c728da1b9c8650d7550image.png)
1. **指针与下标运算符相互作用时**
- 当指针指向的目标是一片连续的内存空间时
```c
int array[5] = {1,2,3,4,5};
int *p = array; // 指针p与数组名字array所代表的地址相同
array[1]; // 编译器一遇到这条语句马上将之转化为其本来面目:*(array+1)
*(p+1); // p[1]
// 结论如下
// array[x] == *(array+x);
// *(p+x) == p[x];
```
- 结论在使用数组名字访问数据元素是数组会自动转换为指针形式访问当指针指向一片连续内存空间时指针取目标就可以使用指针名加下标p[x]
# 二、指针数组
- 数组元素为指针的数组称为指针数组
```c
char *p1,*p2,*p3;
char *str[3] = {p1,p2,p3}; // 表示定义了一个数组其元素为3个指针并初始化。
```
- 常用与在数组中保存多个字符串。
# 三、数组指针
- 指向数组地址的指针称为数组指针
```c
char str[3];
char *p = str; // 指针指向str数组的基地址
char (*q)[3]; // 表示定义了一个指针可以指向一个长度为3的char型数组地址
q = &str;
char str1[2][3];
char (*k)[3] = str1;// 指针指向str1数组的基地址
```
- 常用在二维数组传参中
```c
#include <stdio.h>
void fun(int *p, int (*q)[], int **k)
{
}
int main(int argc, char *argv[])
{
    int array[2][3];
    int array1[2][5];
    int arr[2];
    int *ptr;
    fun(arr,array,&ptr);  //  array  == &array[0]    array[0] == &array[0][0]
    fun(arr,array1,&ptr);
    return 0;
}
```
## 练习
- 定义指针指向下列的目标并将代码补充完整
```c
int main(int argc, char *argv[])
{
    int a[3] = {1,2,3};
    int b[2][5]= {1,2,3};
    char *s[5]= {"hahaha","xixi","ooo"};  
    p1 = a;
    printf("%d\n", p1);
    p2 = b;
    printf("%d\n", p2);
    p3 = s;  
    printf("%s\n", p3);
    p4 = &b[0];
    printf("%d\n", p4);
    p5 = &b[1][3];
    printf("%d\n", p5);
    int p6 = &a;
    printf("%d\n", p6);
    int p7] = &b;
    printf("%d\n", p7);
    p8 = &s;
    return 0;
}
```
# 四、**数组、指针与函数**
### **数组参数**
- 核心语法:当数组在函数中被当做参数传递时,系统会将其自动转化为指针,具体而言,会将其转化为一个指向数组首元素的指针。
- 示例:
```c
int a[3] = {100,200,300};
void f1(a); // &a[0]; int *
```
- 说明:
- 数组a作为参数传给函数f1()。
- 实参a在传递进函数后系统随即将其转化为一个指向a[0]的指针,即一个 int* 指针指向首元素100。
- 此时若定义函数f1(),则可以有如下两种写法,它们是完全等价的:
```c
// 写法一:
void f1(int a[3]) // 或可以写成 void f1(int (a[3]) )
{
...
}
// 写法二:
void f1(int *a) // 或可以写成 void f1(int (*a) )
{
...
}
```
### **数组与指针表示字符串时的区别**
数组是一片连续的内存,这片内存中保存的数据是字符串,可以对这个片内存的数据进行修改。
指针是一个变量,只存储数据字符串的地址,而字符串数据本身是常量,不能通过指针对目标进行修改,但是可以改变指针指向的目标。
```c
  char  *p = "hello";  // 局部变量;"hello"存放在.rodata段中指针p指向的目标在.rodata中
    char str1[] = "hello";// 局部变量;"hello"存放在.rodata段中str是栈空间的一片连续内存存放的是"hello"
   
    printf("p: %s\n", p);
    printf("str: %s\n", str1);
str1 = "abcd"; // 错误将“abcd”字符串赋值给数组str1时需要一个一个的进行。
    str1[0] = 'a';  // 栈内存可以进行读写访问
    printf("str: %s\n", str1);
p = "abc"; // 可以修改p的指向 
    *p = 'a';  // 不能对常量区(.rodata内存进行写访问
    printf("p: %s\n", p);
```
### **复杂数组参数1**
以上示例的数组可以为任意类型的数组,比如:
```c
int b[3][4]; // &b[0] int [4]
int f2(b);
```
此时二维数组b被当做参数传给了函数f2()逻辑跟一位数组完全一样唯一的不同只是数组b的首元素不再是普通的int而是int [4]函数f2()的定义也可以有两种写法:
```c
// 写法一:
void f2(int b[3][4]) // 或可以写成 void f2(int (b[3]) [4] )
{
...
}
// 写法二:
void f2(int (*b)[4]) // 此处小圆括号(*b)不能省略
{
...
}
```
此例与上述数组a和函数f1()完全等同看不出来的同学注意将数组b中的元素类型 int [4] 视为一个整体等同于数组a中的元素类型 int。
### **复杂数组参数2**
继续讲数组参数进行变形,比如:
```c
char *c[3]; // &c[0] char *
int f3(c);
```
此时数组c被当做参数传给了函数f3()逻辑跟之前的两个例子完全一样唯一的不同只是数组c的首元素是 char *函数f3()的定义也可以有两种写法:
```c
// 写法一:
void f3(char *c[3])// 或可以写成 void f3(char * (c[3]) )
{
...
}
// 写法二:
void f3(char **c) // 或可以写成 void f3(char * (*c) )
{
...
}
```
### **总结**
- 任何数组成为参数被传递时,都一律会被转化为一个指针,一个指向其首元素的指针,系统这么做是因为要提高数据传递的效率,但这同时给编程开发者提了个醒 —— 与普通按值传递不同,数组传的都是地址,形参都可以直接访问实参。
- 数组作为函数参数与返回值时实际是传递数组的基地址与返回数组的基地址此时这个数组实际是一个指针如下示例代码中my_strcat与my_strcat1的返回值均是一个数组参数也是数组此时实际传递的是一个指针。
```c
/* 将字符串str2追加到str1的末尾返回追加后的地址 */
char *my_strcat(char str1[], char str2[])
{
    int i;
    for (i = 0; str1[i] != '\0'; i++)
    {
    }
    for (int j = 0; str2[j] != '\0'; j++)
    {
        str1[i+j] = str2[j];
    }
   
    return str1;
}
char *my_strcat1(char *str1, char *str2)
{
    int i;
    for (i = 0; str1[i] != '\0'; i++)
    {
    }
    for (int j = 0; str2[j] != '\0'; j++)
    {
        str1[i+j] = str2[j];
    }
   
    return str1;
}
```
# 五、复杂声明
- 分析复杂声明的步骤
- 从左至右找第一个非关键字标识符
- 以这个标识符为中心,逐个与()、[]、*结合
- 找到被小括号括起来的部分(从左到右原则)
- 跟后缀操作符集合((), [])结合
- ()后缀表示是一个函数,分析其返回值及参数类型
- []后缀表示是一个数组,分析其元素类型
- 跟前缀(*)结合
- *前缀表示是一个指针,分析指向的目标类型
- 示例
```c
char *(*fun)(int);
```
```c
char *(*fun[10])(int);
```
```c
int *(*fun(int))[3];
```
```c
char *(*fun(char *(*p)(char *)))[2](int);
```
[数组与指针作业.doc](attachments/WEBRESOURCEd160cdba37fb5f6e6e853a57af52b227数组与指针作业.doc)

View File

@@ -0,0 +1,397 @@
# 一、C语言函数库
- 概念在C语言发展过程中收录了很多经典的数据操作方法----函数将这些函数收录归纳汇总为开发人员方便使用的API接口函数如下图所示的各种操作接口库。
![](images/WEBRESOURCE607893845793236f767da405d9c9a4d5image.png)
# 二、字符串操作函数
- 使用三部曲
- 确认头文件
- 确认函数功能
- 确认函数的参数与返回值
- C语言标准字符串函数库头文件"#include <string.h>"
![](images/WEBRESOURCE104cae287459e90d523049be3c9ed591image.png)
- **函数strlen**
![](images/WEBRESOURCE0b5032b3e1de49ca903bd2db77d153e4b5ee282015ce47bc0f3295c8193891e2.png)
```c
char *p = "www.yueqian.edu.com.cn";
printf("粤嵌官网的地址长度是:%d\n", strlen(p));
```
- **函数strcat与strncat**
![](images/WEBRESOURCEeb0dc101742ac85592adc21845b91dab6f8df56f18a43f648269080ac22c4c87.png)
- 注意:
- 这两个函数的功能一样都是将src字符串复制到dest的末尾。
- strcat()没有边界控制因此可能会由于src字符串过长导致dest无法保存从而导致内存溢出。
- strncat()有边界控制可以限制拼接字符的格式保证dest不会因为越界而导致内存溢出。
- 更加值得推荐的字符串拼接函数sprintf()与snprintf(),头文件"#include <stdio.h>"
![](images/WEBRESOURCE62638d6f9956908d7b93dd99d70bcc61image.png)
![](images/WEBRESOURCEb4578182300ccebca038b5640e577b8eimage.png)
- 要点sprintf与snprintf不光可以拼接字符串还可以实现将其他数据类型也添加到字符串中sprintf对目标字符串的长度没有现在条件又内存溢出风险但snprintf没有。
```c
char str1[10] = "温度:";
    int temp = 27;
    char buf[50];
    // sprintf(buf,"%s%d随机数据啊科技时代粉红色大家发货的撒扩大飞机和\r\n", str1, temp); // 无长度控制
    // printf("buf:%s", buf);
    snprintf(buf,sizeof buf,"%s%d随机数据啊科技时代粉红色大家发货的撒扩大飞机和\r\n", str1, temp);  // 有长度控制
    printf("buf:%s", buf);
```
- **函数strtok**
![](images/WEBRESOURCEf54011d029f949fe3f754b46aa3853cbfc1fc8c6aeff83d2c08b1198d0df3ed6.png)
- 注意:
- 该函数会将改变原始字符串 str使其所包含的所有分隔符变成结束标记 \0
- 由于该函数需要更改字符串 str因此 str 指向的内存必须是可写的。
- 首次调用时 str 指向原始字符串,此后每次调用 str 用 NULL 代替。
```c
char s[20] = "www.yueqian.com.cn";
char *p = strtok(s, "."); // 首次调用时s 指向需要分割的字符串
while(p != NULL)
{
printf("%s\n", p);
p = strtok(NULL, "."); // 此后每次调用,均使用 NULL 代替。
}
```
***注:上述代码的运行结果就是将字符串 s 拆解为"www"、“yueqian”、“com” 和 “cn”***
- 其他子串提取sscanf()按照格式提取字符串中的内容
![](images/WEBRESOURCE226669d81bf2dc869c23059ac82f7a8eimage.png)
```c
char str[] = "www yueqian-edu com cn";
char str1[4];
char str2[20];
sscanf(str,"%s %s",str1,str2);
printf("str1:%s\n", str1);
```
- **函数strstr**
![](images/WEBRESOURCE54b672d577364079ba70fd45152a12e9stickPicture.png)
```c
char *str = "正午十二点,柏油路面被晒得发软。便利店的冷柜吐出白雾,穿校服的女孩咬着冰棒跑过,"
                "塑料包装纸在风里打着旋儿,最后贴在墙角那丛半枯的狗尾草上。"
                "远处的施工队歇了工,起重机的吊臂在烈日里投下细长的影子,像根被晒蔫的芦苇。";
   
    /* 在字符串中找子串 */
    char *p = strstr(str, "柏油路");  // 从前往后查找子串,返回子串第一次出现位置
    char *q = strstr(p+1, "你的女孩");// 若未找到则返回NULL
    if(p != NULL)
        puts(p);
   
    if(q != NULL)
        puts(q);
```
## **函数strcpy与strncpy**
![](images/WEBRESOURCE4bf7c30b9e994bda856db1440425e6fcstickPicture.png)
```c
int main(int argc, char *argv[])
{
    char *name = "张三丰";
    char name1[20] = "123465dsdskfhjdsahs";
    // 不能把一个进行字符串赋值
    // name1 = name;
    strcpy(name1, name);  // 将name的内容全部内容包括'\0'都拷贝给name1的内存中
    puts(name1);
    puts(name1+10);
    strncpy(name1, name, sizeof(name1));// 将name的内容sizeof(name1)字节部分内容包括'\0'都拷贝给name1的内存中不足的不会拷贝
    puts(name1);
    return 0;
}
```
- 注意:
1. 这两个函数的功能,都是将 src 中的字符串,复制到 dest 中。
1. strcpy() 没有边界控制,因此可能会由于 src 的过长而导致内存溢出。
1. strncpy() 有边界控制,最多复制 n+1 个字符(其中最后一个是 \0 )到 dest 中。
## **函数strcmp与strncmp**
![](images/WEBRESOURCE07b09d5113504e099c4fbfd3ef55f632stickPicture.png)
```c
#include <stdio.h>
#include <string.h>
void fun(char *p)
{
    char *q = "abc";
    if(p==q)
    {
        printf("p与q相等\n");
    }
    else
    {
        printf("p与q不相等\n");
    }
}
int main(int argc, char *argv[])
{
    // char *str1 = "abc";
    // char *str2 = "abc"; // 相同的常量在内存中只有一份
    // fun(str1);
    // if (str1 == str2) // 比较的是指针指向的目标是否相同
    // {
    //     printf("str1与str2相等\n");
    // }
    // else
    // {
    //     printf("str1与str2不相等\n");
    // }
    char s1[] = "abcacccaddc";
    char s2[] = "abca";
    // if (s1 == s2)// 比较的是s1和s2的内存地址是否相同
    // {
    //     printf("s1与s2相等\n");
    // }
   
    int ret = strcmp(s1,s2); // 不是比较长短,而是比较两个字符串内容的大小(ASCII码值),一直比较到有结果为止
                             // 若s1中字符的ASCII码值>s2中字符的ASCII码值则返回值大于0
                             // 若s1中字符的ASCII码值与s2中字符的ASCII码值全部相等则返回值等于0
                             // 若s1中字符的ASCII码值<s2中字符的ASCII码值则返回值小于0
    // printf("%d\n",ret);
    if(ret == 0)
        printf("s1与s2相等\n");
    else if(ret > 0)
        printf("s1大于s2\n");
    else
        printf("s1小于s2\n");
    ret = strncmp(s1,s2, 4); // 选择性比较,可以设置比较的字符长度
    // printf("%d\n",ret);
    if(ret == 0)
        printf("s1与s2相等\n");
    else if(ret > 0)
        printf("s1大于s2\n");
    else
        printf("s1小于s2\n");
    return 0;
}
```
- 注意:
- 比较字符串大小,实际上比较的是字符的 ASCII码值的大小。
- 从左到右逐个比较两个字符串的每一个字符,当能“决出胜负”时立刻停止比较(s1的字符ascii码值减去s2字符的ascii码值返回差值)。
## **函数strchr与strrchr**
![](images/WEBRESOURCEa0517f24461b40ebad5fb8cb35e5046estickPicture.png)
```c
char *str = "正午十二点,柏油路面被晒得发软。便利店的冷柜吐出白雾,穿校服的女孩咬着冰棒跑过,"
                "塑料包装纸在风里打着旋儿,最后贴在墙角那丛半枯的狗尾草上。"
                "远处的施工队歇了工,起重机的吊臂在烈日里投下细长的影子,像根被晒蔫的芦苇。";
/* 在字符串中找字符 */
    char *k = strchr(str, ','); // 从前往后查找字符返回字符第一次出现位置中文字符不行没有找到返回NULL
    if(k != NULL)
        puts(k);
    char *j = strrchr(str, '\0'); // 从后往前主要字符返回字符第一次出现位置中文字符不行没有找到返回NULL
    if(j != NULL)
        puts(j);
    printf("%#x\n", j[0]);
```
- 注意:
1. 这两个函数的功能,都是在指定的字符串 s 中,试图找到字符 c。
1. strchr() 从左往右找strrchr() 从右往左找。
1. 字符串结束标记 \0 被认为是字符串的一部分。
## 练习
自己封装函数实现strlenstrcatstrtokstrstrstrcpystrcmp函数的功能。
# 晚上作业
1. 定义一个长度为20的整型数据生成随机数对这个数组进行初始化编写排序函数对这个数据中的数据进行从小到大排序
- 要求编写五种排序函数:冒泡排序、选择排序、插入排序、快速排序、希尔排序
- 提示:
- **各种排序算法的****时间复杂度****与****空间复杂度**
![](images/WEBRESOURCEb440b29046c7b9a6f3c4315e2c59b6ffimage.png)
- **冒泡排序**
- 顺序:两个数据位置符合排序要求
- 逆序:两个数据位置不符合排序要求
- 思路:从头到尾让两个相邻数据进行比较,顺序保持不变,逆序交换位置,经过一轮比较序列中具有一个“极值”将被挪至末端。
![](images/WEBRESOURCE017da79a24a769cff3e5b4a570845821849589-20171015223238449-2146169197.gif)
```c
```
- **插入排序**
- 思路假设数列前面有i个节点的序列是有序的那么就从第i+1个节点开始插入到前面i个节点中的合适位置。由于序列的第一个节点始终视为有序所以实在从第二个节点开始。
![](images/WEBRESOURCE0bef0e4f20c74105d00fc30f18338300849589-20171015225645277-1151100000.gif)
```c
```
- **选择排序**
- 在无序序列中依次从头到尾挑选合适的节点放入有序序列。
![](images/WEBRESOURCE5936c4c59a03bf6508f3f2fc7f496572849589-20171015224719590-1433219824.gif)
```c
```
- **快速排序**
- 快排是一直典型的递归思想,相比较其他排序它需要跟多的空间,理论上时间效率是最高的。
- 思想在待排序序列中选取一个数据作为“支点”然后其他数据与支点比较升序比支点小的放左边比支点大的放右边全部比较完后支点位于两个序列的中间这叫一次划分partition
![](images/WEBRESOURCEe0805f5f18d746ebfe9e7c96c29f5a05image.png)
- 一次划分之后序列的内部也许无序但是左右序列与支点三者间形成了一种基本有序状态接下来使用相同的思路递归的对左右序列进行排序直到子序列的长度小于等于1为止
![](images/WEBRESOURCEb6a0efee1c1bc96325017b5204bf671f849589-20171015230936371-1413523412.gif)
```c
```
- **希尔排序**
- 插入排序的改进版本普通插入排序是从第2个节点开始依次插入到有序序列中这种做法在虽然一次成型但时间效率上不划算优化思路
- 不严格一个个插入使之有序,而是拉开插入节点的距离,让它们逐步有序,有待排序序列如下:
84、83、88、87、61、50、70、60、80、89
- 第一遍,先区间隔(Δ=5即依次对以下5组数据进行排序
**84**
84、
84、83、
84、83、88、
84、83、88、87、
**注意:**
**50**
50、
50、70、
50、70、60、
50、70、60、80、
**结果:**
**50**
50、
50、70、
50、70、60、
**得到:**
**50**
50、
50、61、
50、61、60、
**结果**
![](images/WEBRESOURCE77576e5ba8ad0a92819a29e42a156f4c849589-20180331170017421-364506073.gif)
```c
```
1. 将自己写的字符串操作函数进行吸收
1. 制作一个图书管理系统,要求:
1. 输入1增加书籍名称可以连续添加多本
1. 输入2删除数书籍名称删除一本或全部删除
1. 输入3修改书籍名称
1. 输入4查找书籍名称模糊查找或精确查找
1. 输入5显示所有书籍
1. 输入0退出系统
只要系统未退出则可以继续重复进行,直至系统退出。
提示char *book_name[1000]; // 表示表示最多可存放1000书书名的长度自己设计。
[设置终端信息字体及颜色](https://share.note.youdao.com/s/DydeICJh)

View File

@@ -0,0 +1,305 @@
# **C语言程序内存布局**
任何一个程序正常运行都需要内存资源用来存放诸如变量、常量、函数代码等等。这些不同的内容所存储的内存区域是不同的且不同的区域有不同的特性。因此我们需要研究C语言进程的内存布局逐个了解不同内存区域的特性。
每个C语言程序运行后进程都拥有一片结构相同的虚拟内存所谓的虚拟内存就是从实际物理内存映射出来的地址规范范围最重要的特征是所有的虚拟内存布局都是相同的极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3它们很显然会占据不同区段的物理内存但经过系统的变换和映射它们的虚拟内存的布局是完全一样的。
- PMPhysical Memory物理内存。
- VMVirtual Memory虚拟内存。
![](images/WEBRESOURCE386be62eabb8428892ab8defa9285f62stickPicture.png)
将其中一个C语言进程的虚拟内存放大来看会发现其内部包下区域
-stack
-heap
- 数据段
- 代码段
![](images/WEBRESOURCE8e93c80dd4024c349b761d46d6bc550astickPicture.png)
虚拟内存中内核区段1GB对于应用程序而言是禁闭的它们用于存放操作系统的关键性代码另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x0 ~ 0x08048000 之间也有一段禁闭的区段128MB该区段也是不可访问的。
虚拟内存中各个区段的详细内容:
![](images/WEBRESOURCEad72a89dc307481fb738c9fd32d03bb0截图.png)
# **栈内存**
- 什么东西存储在栈内存中?
- 环境变量
- 命令行参数
- 局部变量(包括形参)
- 栈内存有什么特点?
- 空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。
- 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。(随用随申请,用完系统自动释放)
- 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。(由系统自动进行管理)
- 注意:
![](images/WEBRESOURCEbfbfe340f67e40d8b8abd3e304990e0b截图.png)
Linux中栈空间的大小可以使用ulimit -a进行查看若使用时超出这个范围则称为"栈溢出"
![](images/WEBRESOURCE16558976e0bafd8d40e5bc88c3312104image.png)
- 示例代码:
```c
void func(int a, int *p) // 在函数 func 的栈内存中分配
{
double f1, f2; // 在函数 func 的栈内存中分配
... // 退出函数 func 时,系统的栈向上缩减,释放内存
}
int main(void)
{
int m = 100; // 在函数 main 的栈内存中分配
func(m, &m); // 调用func时系统的栈内存向下增长
}
```
# **数据段**
C语言中数据段中存放静态数据静态数据有两种
- 全局变量:定义在函数外部的变量。
- 静态变量:静态局部变量(定义在函数内部且被static修饰的变量)静态全局变量定义在函数外部且被static修饰的变量
- 示例:
```c
int a; // 全局变量,退出整个程序之前不会释放
void f(void)
{
static int b; // 静态局部变量,退出整个程序之前不会释放
printf("%d\n", b);
b++;
}
int main(void)
{
f();
f(); // 重复调用函数 f(),会使静态局部变量 b 的值不断增大
}
```
- 为什么需要静态数据?
1. 全局变量在默认的情况下,对所有文件可见,为某些需要在各个不同文件和函数间访问的数据提供操作上的方便(extern<声明外部变量>)。
1. 当我们希望一个函数退出后依然能保留局部变量的值,以便于下次调用时还能用时,静态局部变量可帮助实现这样的功能。
- 注意1
- 若定义时未初始化则系统会将所有的静态数据自动初始化为0
- 静态数据初始化语句,只会执行一遍。
- 静态数据从程序开始运行时便已存在,直到程序退出时才释放。
- 注意2
- static修饰局部变量使之由栈内存临时数据变成了静态数据。
- static修饰全局变量使之由各文件可见的静态数据变成了本文件可见的静态数据。
- static修饰函数使之由各文件可见的函数变成了本文件可见的静态函数。
# **数据段与代码段**
- 数据段细分成如下几个区域:
- .bss 段:存放未初始化(初始赋值)的静态数据它们将被系统自动初始化为0
- .data段存放已初始化的静态数据
- .rodata段存放常量数据程序内出现的所有常量不包含const修饰的变量
- 代码段细分成如下几个区域:
- .text段存放用户代码用户自己编写的所有程序源码
- .init段存放系统初始化代码编译系统会自动为每一个程序文夹添加系统初始化代码。
![](images/WEBRESOURCEdd9921bab1cd4252b7f237f0d8a12b47截图.png)
```c
int a; // 未初始化的全局变量,放置在.bss 中
int b = 100; // 已初始化的全局变量,放置在.data 中
int main(void)
{
static int c; // 未初始化的静态局部变量,放置在.bss 中
static int d = 200; // 已初始化的静态局部变量,放置在.data 中
// 以上代码中的常量100、200防止在.rodata 中
}
```
- 注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预。
# **堆内存**
堆内存heap又被称为动态内存、自由内存简称堆。堆是唯一可被开发者自定义的区段开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”所有的细节均由开发者自己把握系统不对此做任何干预给予开发者绝对的“自由”但也正因如此对开发者的内存管理提出了很高的要求。对堆内存的合理使用几乎是软件开发中的一个永恒的话题。
- 堆内存基本特征:
- 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
- 相比栈内存,堆内存从下往上增长。
- 堆内存是匿名的,只能由指针来访问。
- 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。
![](images/WEBRESOURCE7246c4ff67284b1f94285f137c008d32截图.png)
- 相关API
- 申请堆内存malloc() / calloc()
- 清零堆内存bzero()
- 释放堆内存free()
![](images/WEBRESOURCE3b6fc7cbc2e84d2e953b56d19a155223截图.png)
![](images/WEBRESOURCE3371a585a1a5dfcb33e097062bbbd329image.png)
- 示例:
```c
int *p = malloc(sizeof(int)); // 申请1块大小为 sizeof(int) 的堆内存
bzero(p, sizeof(int)); // 将刚申请的堆内存清零
*p = 100; // 将整型数据 100 放入堆内存中
free(p); // 释放堆内存
// 申请3块连续的大小为 sizeof(double) 的堆内存
double *k = calloc(3, sizeof(double));
k[0] = 0.618;
k[1] = 2.718;
k[2] = 3.142;
free(k); // 释放堆内存
k = NULL;
```
```c
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
int main(int argc, char *argv[])
{
    char *p = NULL;
    // char str[10] = {0};  // 栈数组,有名字
    p = malloc(7766279631452241920); // malloc可以动态的申请内存空间10字节成功返回这个连续10字节空间的地址堆数组无名字
    if(p == NULL)
    {
        // printf("申请内存失败\n");  // 特殊情况
        perror("申请内存失败");  // 输出错误内容
        return -1;
    }
    // p = calloc(2, 10);  // calloc动态的申请内存空间2片连续的内存每片10字节
    // char (*q)[10] = calloc(4, 10);// malloc与calloc的用法相似的返回值都是void *因此可以为你申请所需要的所有类型内存。
 
    // for (int i = 0; i < 20; i++)
    // {
    //     str[i] = 'a'+i;
    //     p[i] =  'a'+i;
    // }
    // str[9] = '\0';
    // p[19] = '\0';
    // printf("%s\n", str);
    // printf("%s\n", p);
    // free(p); // 释放空间,使用完毕后将内存归还系统。
    p = malloc(20);   // 未进行初始化的内存
    bzero(p,20); // 初始化内存(清零)
    for (int i = 0; i < 20; i++)
    {
        printf("%x\t",p[i]);  // *(p+i)  *(y+x) == y[x]
    }
    printf("\n");
    free(p); // 已经释放, p虽然保存的还是刚刚的空间但这空间能否使用未知
    p = NULL; // 防止释放后在进行访问
    p = calloc(1,20);  // 会进行初始化(清零)的内存
    for (int i = 0; i < 20; i++)
    {
        printf("%x\t",p[i]);  // *(p+i)  *(y+x) == y[x]
    }
    printf("\n");
    free(p); // 已经释放, p虽然保存的还是刚刚的空间但这空间能否使用未知
    p = NULL; // 防止释放后在进行访问
//     for (int i = 0; i < 20; i++)
//     {
//         p[i] =  'a'+i;
//     }
//    printf("%s\n", p);
    return 0;
}
```
- 注意:
- malloc()申请的堆内存,默认情况下是随机值,一般需要用 bzero()或者memset() 来清零。
- calloc()申请的堆内存默认情况下是已经清零了的不需要再清零且calloc可以申请多片连续内存。
- free()只能释放堆内存,并且只能释放整块堆内存,不能释放别的区段的内存或者释放一部分堆内存。
- realloc()重设内存的大小,若原地址后有足够的空间则新开拓的地址在原地址的基础上进行增加并返回原内存地址,若原地址后内存不足则重新开辟一片内存空间并将原地址内存中的数据拷贝到新的内存地址中,然后释放原地址内存并返回新内存地址。
```c
/* 验证realloc的使用 若原来的空间后有空闲的可以追加的空间则在原来的空间后进行追加,返回原地址;若不够则将原来空间中的数据拷贝到新的空间中并释放原来的空间,再返回新空间的地址 */
    p = malloc(20);  // 申请一片空间
    printf("p: %p\n", p);
    p = realloc(p, 20+2); // 重新设置已申请空间的大小  追加(再开辟)
    printf("p: %p\n", p);
    p = realloc(p, 20-15); // 重新设置已申请空间的大小 缩减(释放一部分)
    printf("p: %p\n", p);
```
- 释放内存的含义:
- 释放内存意味着将内存的使用权归还给系统。
- 释放内存并不会改变指针的指向手动立即令指针指向NULL。
- 释放内存并不会对内存做任何修改,更不会将内存清零。
![](images/WEBRESOURCE7c9c73af4eed4645beaa1264f8384921image.png)
- **什么时候用栈什么时候用堆?**
- 基本数据类型int char float double就用栈复合数据类型结构体、联合体就用堆。
- **练习**
- 使用堆空间计算两个大数的乘积123456789123456789123456789*987456321987456123698745

View File

@@ -0,0 +1,432 @@
# 函数栈内存
## **局部变量与栈内存**
- 局部变量概念:凡是被一对花括号包含的变量,称为局部变量
- 局部变量特点:
- 某一函数内部的局部变量,存储在该函数特定的栈内存中(函数的栈帧<当函数被执行时出现>中)
- 局部变量只能在该函数内可见,在该函数外部不可见
- 当该函数退出后,局部变量所占的内存立即被系统回收,因此局部变量也称为临时变量
- 函数的形参虽然不被花括号所包含,但依然属于该函数的局部变量
- 栈内存(函数的栈)特点:
- 每当一个函数被调用时,系统将自动分配一段栈内存给该函数,用于存放其局部变量
- 每当一个函数退出时,系统将自动回收其栈内存
- 系统为函数分配栈内存时,遵循从上(高地址)往下(低地址)分配的原则
- 示例代码:
```c
int max(int x, int y) // 变量 x 和 y 存储在max()函数的栈中
{
int z; // 变量 z 存储在max()函数的栈中
z = x>y ? x : y;
return z; // 函数退出后栈中的x、y 和 z 被系统回收将max函数栈中的z的值反馈传递
}
int main(void)
{
int a = 1; // 变量 a 存储在main()函数的栈中
int b = 2; // 变量 b 存储在main()函数的栈中
int m; // 变量 m 存储在main()函数的栈中,未赋值因此其值为随机值
m = max(a, b); // 将main栈中的a和b的值传递给max函数
}
```
函数调用时代码的执行流程
![](images/WEBRESOURCEebddd934ca1e421cb558c4c92f7d53f3截图.png)
- 技术要点:
- 栈内存相对而言是比较小的,不适合用来分配尺寸太大的变量。
- return 之后不可再访问函数的局部变量,因此返回一个局部变量的地址通常是错误的。
## 全局变量
- 全局变量概念:凡是定义在花括号外部的变量,称为全局变量
- 全局变量特点:
- 全局变量定义函数体外,存储在该程序特定的内存中(数据段)
- 全局变量在整个程序中可见
- 当该程序退出后,全局变量所占的内存立即被系统回收
# **作用域**
## **基本概念**
C语言中标识符都有一定的可见范围这些可见范围保证了标识符只能在一个有限的区域内使用这个可见范围被称为作用域scope
在软件开发中,尽量缩小标识符的作用域是一项基本原则,一个标识符的作用域超过它实际所需要的范围时,就会对整个软件的命名空间造成污染,导致一些不必要的名字冲突和误解。
## **函数声明作用域**
- 概念:在函数的声明式中定义的变量,其可见范围仅限于该声明式。
- 示例:
```c
void func(int fileSize, char *fileName);
```
- 要点:
- 变量 fileSize 和 fileName 只在函数声明式中可见。
- 变量 fileSize 和 fileName 可以省略,但一般不这么做,它们的作用是对参数的注解。
## **局部作用域**
- 概念:在代码块(被一对花括号包含)中定义的变量,其可见范围从其定义处开始,到代码块结束为止。
- 示例:
```c
int main()
{
int a=1;
int b=2; // 变量 c 的作用域是第4行到第9行
{
int c=4;
int d=5; // 变量 d 的作用域是第7行到第8行
int a = 100;
}
c = 100; // 超出c的作用域此次视为未定义c
}
```
- 要点:
- 代码块指的是一对花括号 { } 括起来的区域。
- 代码块可以嵌套包含,外层的标识符会被内嵌的同名标识符临时掩盖变得不可见。
- 代码块作用域的变量,由于其可见范围是局部的,因此被称为局部变量。
## **全局作用域**
- 概念:在代码块外定义的变量,其可见范围可以跨越多个文件。
- 示例:
```c
#include <stdio.h>
int global = 888; // 变量 global 的作用域是第2行到本文件结束
extern void f1();  // 外部声明
extern void f2();
int main()
{
    global = 666;
    printf("%s:global = %d\n", __FUNCTION__, global);
    f1();
    f2();
}
void f()
{
    printf("a.c :%s is running\n", __FUNCTION__);
}
```
```c
#include <stdio.h>
// extern 外部声明,表示这个标识符不属于本文件而是来自外部的文件,这个标识符的作用域足够大
// extern 外部声明的标识符不能初始化
extern int global; // 声明在 a.c 中定义的全局变量,使其在 b.c 中也可见
extern void f();  // f函数定义在本文件外  
void f1()
{
    printf("%s :global=%d\n",__FUNCTION__, global); // 访问的是a.c中定义的全局变量
}
void f2()
{
    printf("b.c :%s is running\n", __FUNCTION__);
    f();
}
// int main()
// {}
```
**注意由于b.c中存在外部变量和函数此时b.c是不能单独通过编译的因此需要b.c功能则需要将a.c和b.c一并编译生成一个程序“gcc b.c a.c”当多个C语言源文件同时参与编译时多个C语言文件中只能存在一个main函数。**
## **作用域的临时掩盖**
如果有多个不同的作用域相互嵌套,那么小范围的作用域会临时 “遮蔽” 大范围的作用域中的同名标识符,被 “遮蔽” 的标识符不会消失,只是临时失去可见性。
- 示例代码:
```c
#include <stdio.h>
int a = 100;  // 整个程序可见
// 函数代码块1
int main(void)
{
    printf("%d\n", a); // 输出100, 全局变量a
    int a = 200;
    printf("%d\n", a); // 输出200, 局部变量a的作用域掩盖全局变量a的作用域
   
    // 代码块2
    {
        printf("%d\n", a); // 输出200局部变量a
        int a = 300;
        printf("%d\n", a); // 输出300 当前代码块中定义的a作用域掩盖了代码块外的局部变量a的作用域
    }
    printf("%d\n", a); // 输出200局部变量a
    f();
}
void f()
{
    printf("%d\n", a); // 输出100, 全局变量a
}
```
**static关键字**
C语言的一大特色是相同的关键字在不同的场合下具有不同的含义。static关键字在C语言中有两个不同的作用
1. 将可见范围设定为标识符所在的文件(缩小作用域):
- 修饰全局变量:使得全局变量由原来的跨文件可见,变成仅限于本文件可见。
- 修饰普通函数:使得函数由原来的跨文件可见,变成仅限于本文件可见。
1. 将存储区域设定为数据段(延长存储期):
- 修饰局部变量:使得局部变量由原来存储在栈内存,变成存储在数据段。
- 示例:
```c
int a; // 普通全局变量,跨文件可见
static int b; // 静态全局变量,仅限本文件可见
void f1() // 普通函数,跨文件可见
{}
static void f2() // 静态函数,仅限本文件可见
{}
int main()
{
int c; // 普通局部变量,存储于栈内存
static int d; // 静态局部变量,存储于数据段
}
```
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 当变量标识符重名时优先使用当前{}内的标识符 */
int a=100; //跨文件可见
static int b = 10; //本文件可见
void fun(void)
{
    static int b = 20; //本函数可见,且只被初始化一次
    printf("%d:b=%d\n",__LINE__, b++);
}
int main(void)
{
    printf("%d:a=%d\n",__LINE__, a);
    int a=10; //当前{}内可见,被定义到}
   
    if(a==10)
    {
        printf("%d:a=%d\n",__LINE__, a);
        int a=20; //当前{}内可见,被定义到}
        printf("%d:a=%d\n",__LINE__, a);
    }
    printf("%d:a=%d\n",__LINE__, a);
    for (int i=0; i<3; i++) //当前{}内可见,被定义到}
    {
        fun();
    }
    return 0;
}
```
# **存储期**
C语言中变量都是有一定的生存周期的所谓生存周期指的是从分配到释放的时间间隔。为变量分配内存相当于变量的诞生释放其内存相当于变量的死亡。从诞生到死亡就是一个变量的生命周期。
根据定义方式的不同,变量的生命周期有三种形式:
1. 自动存储期
2. 静态存储期
3. 自定义存储期
不同存储区段对应不同存储期
![](images/WEBRESOURCEed95cb4bf9a6bdd870ada0859f790dcastickPicture.png)
## **自动存储期**
在栈内存中分配的变量,统统拥有自动存储期,因此也都被称为自动变量。这里自动的含义,指的是这些变量的内存管理不需要开发者操心,都是全自动的:在变量定义处自动分配,出了变量的作用域后自动释放。
- 以下三个概念是等价的:
- 自动变量:从存储期的角度,描述变量的时间特性。
- 临时变量:同上。
- 局部变量:从作用域的角度,描述变量的空间特性。
可以统一把它们称为栈变量,下面是示例代码:
```c
int main()
{
int a, b; // 自动存储期
static int c; // 加了static(修饰静态数据)的局部变量不再是栈变量,而是静态数据了
f(a, b);
}
void f(int x, int y) // 自动存储期
{
}
```
## **静态存储期**
在数据段中分配的变量,统统拥有静态存储期,因此也都被称为静态变量。这里静态的含义,指的是这些变量的不会因为程序的运行而发生临时性的分配和释放,它们的生命周期是恒定的,跟整个程序一致。
- 静态变量包含:
- 全局变量:不管加不加 static任何全局变量都是静态变量。
- static 型局部变量。
- 示例代码:
```c
int g1; // 静态存储期
static int g2; // 静态存储期
int main()
{
int a, b;
static int c; // 静态存储期
}
```
- 注意1
- 若定义时未初始化则系统会将所有的静态数据自动初始化为0
- 静态数据初始化语句,只会执行一遍。
- 静态数据从程序开始运行时便已存在,直到程序退出时才释放。
- 注意2
- static修饰局部变量使之由栈内存临时数据变成了静态数据可见范围不变。
- static修饰全局变量使之由各文件可见的静态数据变成了本文件可见的静态数据生命周期不变。
## **自定义存储期**
在堆中分配的变量,统统拥有自定义存储期,也就是说这些变量的分配和释放,都是由开发者自己决定的。由于堆内存拥有高度自治权,因此堆是程序开发中用得最多的一片区域。
![](images/WEBRESOURCE77cefa9018a70540104090158d8390b0stickPicture.png)
自由的堆内存
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *fun(void) //栈空间中会自动释放但数据段和堆空间例外
{
    // char str[20]; //栈空间仅在当前{}有效
    // return str;
    char *p = malloc(20); //堆空间可以在整个程序中有效,只要不释放就一直可用。
    if(p != NULL)
    {
        memset(p, 0, 20);
        return p; //返回的是指针的值及堆空间地址
    }    
   
    return NULL;
}
int main(void)
{
    char *p;
    p = fun(); //得到堆空间的地址
    char *k = "hello GEC";
    memcpy(p, k, strlen(k)); //将数据拷贝到堆空间中
    printf("__%d__:%s\n", __LINE__, p);
    free(p); //将fun函数中申请的堆空间释放
    return 0;
}
```
# 总结
作用域:是标识符(变量名字、函数名等)的可见范围(可以使用)。
存储期:是标识符的生命周期即标识符拥有内存开始到标识符的内存被释放为止。
标识符的作用域与生命周期在绝大多数情况下相同但除static修饰的标识符外
1、static修饰全局变量和函数时static限制了全局变量和函数的作用域但不修改其生命周期
2、static修饰局部变量时static修改局部变量的生命周期但不改变其作用域
(面试笔试题)堆和栈有什么区别?
1. 栈是由系统自动分配释放,堆是由用户手动进行申请和释放
1. 栈空间的效率高于堆空间
1. 堆空间要比栈空间大
1. 栈从上往下进行增长范围有限,堆从下往上增长范围没有明确的限制
堆内存的申请和释放方法?
1. malloc()和calloc()进行申请
1. free()进行释放
malloc()和calloc()的不同?
1. malloc只能申请一片连续的堆内存并且不会对内存进行处理。
1. calloc既可以申请一片空间也可以一次申请多片连续内存并且会对内存进行清零。
[内存管理测试题.docx](attachments/WEBRESOURCE224e5f8aa53ceb5fc88f45d79932378a内存管理测试题.docx)

View File

@@ -0,0 +1,743 @@
# 一、结构体
1. **结构体基本概念**
- C语言提供了众多的基本类型但现实生活中的对象一般都不是单纯的整型、浮点型或字符串而是这些基本类型的综合体。比如一个学生典型地应该拥有学号整型、姓名字符串、分数浮点型、性别枚举等不同层面的属性这些所有的属性都不应该被拆分开来而是应该组成一个整体代表一个完整的学生。
- 在C语言中可以使用结构体来将多种不同的数据类型组装起来形成某种具有现实意义的自定义的变量类型。结构体本质上是一种自定义类型。
- 结构体跟普通变量一样,涉及定义、初始化、赋值、取址、传值等等操作,这些操作绝大部分与普通变量操作并无二致,只有少数操作有特殊性。
- **结构体定义**
```c
struct 结构体标签
{
成员1;
成员2;
...
};
```
- 语法
- struct关键字用于声明定义结构体
- 结构体标签,用于区分各个不同的结构体
- 成员,是包含在结构体内部的数据,可以是任意的数据类型(不能是自己类型的普通变量但可以是自己类型指针变量)。
```c
// 定义了一种称为struct A的结构体类型
struct A  // struct A 是一个自定义的类型
{
    int a;
    char b;
    double c;
};
// 定义了一种称为struct student的结构体类型
struct student
{
    char name[20];  // 姓名
    int id;         // 学号
    char *sex;      // 性别
    short age;      // 年龄
    struct A a;     // 其他类型的结构体变量
    // struct student b; // 不能定义自己类型的结构体普通变量(递归申请空间),作为自己的成员
    struct student *p; // 能定义自己类型的结构体指针变量(指针的内存空间只有系统字长有关),作为自己的成员
    //...
};
int main(void)
{
// 定义一个int类型变量
int num; // int 数据类型 num变量名称
//定义了一个struct student (结构体)类型变量
    struct student zsf; // struct student 数据类型 zsf变量的名称
}
```
- **结构体初始化**
- 由于结构内部拥有多个不同类型的成员,因此采用与数组初始化类似的列表形式进行
- 初始化方式:普通初始化,指定成员初始化
- 一般推荐使用指定成员初始化,方便适应结构体类型的升级迭代
- 声明定义与初始化一体
```c
// 定义一个结构体类型
// 定义一个结构体类型
struct A
{
    int a;
    char b;
    double c;
}tmp = {
    100,'S',3.14
},// 定义一个结构体变量tmp并使用普通初始化方式初始化tmp成员
bbk={
    .a=500,
    .b='C'
}; // 定义一个结构体变量bbk并使用指定成员初始化方式初始化bbk成员
```
**注意:**
- 定义时初始化
```c
struct A xyz = {120,"w", 6.25}; // 普通初始化
    struct A jjk = {.b="w",.a=120, }; // 指定成员初始化
```
- 指定成员初始化的好处:
- 成员初始化顺序可以改变
- 可以只初始化一部分成员
- 结构体新增成员后初始化语句仍然可用。
- **结构体成员引用**
- 结构体相当于一个集合,内部包含了众多成员,每一个成员都是一个独立的变量,都可以独立地引用,引用结构体成员使用一个成员引用符'.'即可:
```c
结构体变量名称.成员名称
```
```c
    struct A kkl;
    kkl.a=866;
    kkl.b='L';
    kkl.c=9.36;
    printf("a=%d b=%c c=%f\n", kkl.a, kkl.b, kkl.c);
```
## 练习
定义书籍结构体(书名、作者、售价)并定义三本书,分别使用普通初始化、指定成员初始化与结构体引用三种方式给三本书赋值属性后在终端展示
1. **结构体指针与数组**
- 结构体指针
```c
struct student
{
    unsigned char *name;
    unsigned long id;
    unsigned char *sex;
    unsigned char age;
};
struct student *zh = malloc(sizeof(struct student)); // 定义一个结构体指针变量
    (*zh).name = "张三丰";  // (*). == ->
    (*zh).id = 10086;
    (*zh).sex = "男";
    (*zh).age = 18;
    printf("%s %ld %s %d\n", zh->name, zh->id, zh->sex, zh->age);
```
注意:结构体指针访问结构体成员时使用 ->
- 结构体数组
```c
// struct student wang[3] = {
    //                             "王局", 10010, "女", 17,
    //                             "王麻", 10000, "男",19,
    //                             "王铁", 10001, "男",19
    //                          }; // 普通初始化
    //  struct student wang[3] = {
    //                            [1]= "王局", 10010, "女", 17,
    //                            [0]= "王麻", 10000, "男",19,
    //                            [2]= "王铁", 10001, "男",19
    //                          }; // 指定元素普通初始化
     struct student wang[3] = {
                                [2].name="王局", [0].id=10010, [1].sex="女",[0].age=17,
                                // "王麻", 10000, "男",19,
                                // "王铁", 10001, "男",19
                             }; // 指定成员初始化
    for (int i = 0; i < sizeof(wang)/sizeof(wang[0]); i++)
    {
        printf("%s %ld %s %d\n", wang[i].name, wang[i].id, wang[i].sex, wang[i].age); // 访问结构体数组元素
    }
```
# 练习1
升级书籍管理系统功能,添加作者与售价属性,并将功能进行完善(如查找书籍可分为书名查找,按作者查找,按售价查找)
1. **typedef**
- 在 C 语言中typedef关键字用于为已有的数据类型创建别名增强代码的可读性、可维护性和可移植性。
- **基本应用:为基本数据类型创建别名**
- 为内置数据类型如int、char等定义更具描述性的名称使代码意图更清晰。
```c
typedef int Age; // 为int创建别名Age
typedef float Weight; // 为float创建别名Weight
Age user_age = 25;
Weight apple_weight = 0.3f;
```
- **为复杂类型创建简化别名**
1指针类型
简化指针类型的声明,尤其适合频繁使用的指针类型。
```c
typedef int* IntPtr; // 为int*创建别名IntPtr
IntPtr p1, p2; // 等价于int *p1, *p2;(避免遗漏*的问题)
```
2数组类型
为固定大小的数组定义别名,减少重复代码。
```c
typedef int IntArray[10]; // 为"int[10]"创建别名IntArray
IntArray arr; // 等价于int arr[10];
```
3结构体 / 联合体
简化结构体 / 联合体的使用,无需每次加
```c
// 传统方式
struct Student {
char name[20];
int id;
};
struct Student s1; // 必须加struct
// 使用typedef
typedef struct {
char name[20];
int id;
} Student;
Student s2; // 直接使用别名,更简洁
```
- **为函数指针创建别名**
函数指针语法复杂,
```c
// 为"int (*)(int, int)"类型创建别名CalcFunc
typedef int (*CalcFunc)(int, int);
// 定义符合该类型的函数
int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }
// 使用别名声明函数指针变量
CalcFunc func = add;
printf("%d\n", func(2, 3)); // 输出5
func = mul;
printf("%d\n", func(2, 3)); // 输出6
```
- **提高代码可移植性**
在跨平台开发中,不同系统的基础类型可能有差异(如
```c
// 在32位系统可能定义为
typedef int int32;
// 在16位系统可能定义为
typedef long int32;
// 代码中直接使用int32无需关心底层实现
int32 value = 100;
```
-  **增强代码可读性和可维护性**
- 通过别名直观表达变量的含义如Age比int更清晰
- 当需要修改类型时只需修改typedef处无需全局替换如将typedef int Age改为typedef long Age
- **总结**
- typedef的核心作用是为数据类型创建别名主要价值体现在
- 简化复杂类型(如结构体、函数指针)的使用;
- 提高代码的可读性和可维护性;
- 增强跨平台代码的可移植性。
1. **CPU字长**
- 字长的概念指的是处理器在一条指令中的数据处理能力当然这个能力还需要搭配操作系统的设定比如常见的32位系统、64位系统指的是在此系统环境下处理器一次存储处理的数据可以达32位或64位。
![](images/WEBRESOURCEae70b1f84fef405a968b6584513809aestickPicture.png)
CPU字长的含义
1. **地址对齐**
- CPU字长确定之后相当于明确了系统每次存取内存数据时的边界以32位系统为例32位意味着CPU每次存取都以4字节为边界因此每4字节可以认为是CPU存取内存数据的一个单元。
- 如果存取的数据刚好落在所需单元数之内,那么我们就说这个数据的地址是对齐的,如果存取的数据跨越了边界,使用了超过所需单元的字节,那么我们就说这个数据的地址是未对齐的。
![](images/WEBRESOURCE0486404994804c5fb05039cff405656astickPicture.png)
地址未对齐的情形
![](images/WEBRESOURCE886cfc7eae684a52b78b24c37be08569stickPicture.png)
地址已对齐的情形
- 从图中可以明显看出数据本身占据了8个字节在地址未对齐的情况下CPU需要分3次才能完整地存取完这个数据但是在地址对齐的情况下CPU可以分2次就能完整地存取这个数据。
![](images/WEBRESOURCEe2dc42a51771304b30e561af3f2ed294image.png)
> 总结如果一个数据满足以最小单元数存放在内存中则称它地址是对齐的否则是未对齐的。地址对齐的含义用大白话说就是1个单元能塞得下的就不用2个2个单元能塞得下的就不用3个。如果发生数据地址未对齐的情况有些系统会直接罢工有些系统则降低性能。
1. **普通变量的m值**
- 以32位系统为例由于CPU存取数据总是以4字节为单元因此对于一个尺寸固定的数据而言当它的地址满足某个数的整数倍时就可以保证地址对齐。这个数就被称为变量的m值。根据具体系统的字长和数据本身的尺寸m值是可以很简单计算出来的。
- 举例:
```c
 char   c; // 由于c占1个字节因此c不管放哪里地址都是对齐的因此m=1
 short s; // 由于s占2个字节因此s地址只要是偶数就是对齐的因此m=2
 int   i; // 由于i占4个字节因此只要i地址满足4的倍数就是对齐的因此m=4
 double f; // 由于f占8个字节因此只要f地址满足4的倍数就是对齐的因此m=4
 
 printf("%p\n", &c); // &c = 1*Nc的地址一定满足1的整数倍
 printf("%p\n", &s); // &s = 2*Ns的地址一定满足2的整数倍
 printf("%p\n", &i); // &i = 4*Ni的地址一定满足4的整数倍
 printf("%p\n", &f); // &f = 4*Nf的地址一定满足4的整数倍
```
- 注意变量的m值跟变量本身的尺寸有关但它们是两个不同的概念。
- 手工干预变量的m值
```c
 char c __attribute__((aligned(32))); // 将变量 c 的m值设置为32
```
- 语法:
- attribute 机制是GNU特定语法属于C语言标准语法的扩展。
- attribute 前后都是双下划线aligned两边是双圆括号。
- attribute 语句,出现在变量定义语句中的分号前面,变量标识符后面。
- attribute 机制支持多种属性设置,其中 aligned 用来设置变量的 m 值属性。
- 一个变量的 m 值只能提升不能降低且只能为正的2的n次幂。
1. **结构体的M值**
- 概念:
- 结构体的M值取决于其成员的m值的最大值。即M = max{m1, m2, m3, …};
- 结构体的和地址和尺寸都必须等于M值的整数倍。
- 示例:
```c
 struct node
 {
    short a; // 尺寸=2m值=2
    double b; // 尺寸=8m值=4
    char   c; // 尺寸=1m值=1
 };
 
 struct node n; // M值 = max{2, 4, 1} = 4;
```
- 以上结构体成员存储分析:
1. 结构体的M值等于4这意味着结构体的地址、尺寸都必须满足4的倍数。
1. 成员a的m值等于2但a作为结构体的首元素必须满足M值约束即a的地址必须是4的倍数
1. 成员b的m值等于4因此在a和b之间需要填充2个字节的无效数据一般填充0
1. 成员c的m值等于1因此c紧挨在b的后面占一个字节即可。
1. 结构体的M值为4因此成员c后面还需填充3个无效数据才能将结构体尺寸凑足4的倍数。
- 以上结构体成员图解分析:
![](images/WEBRESOURCEf305530c183d4d1682bc4b1fe628e14fstickPicture.png)
结构体成员布局
## 练习
计算下面结构体的大小
```c
struct s1
{
int a;
char b;
int c;
};
struct s2
{
char a;
char b;
int c;
};
struct s3
{
int a;
double b;
int c;
};
struct s4
{
char a;
char b;
char c;
};
struct s5
{
char a;
short b;
int c;
double d;
};
struct s6
{
char a;
short b;
int c;
struct s4 d;
};
```
[32、64位数据类型占用字节以及内存对齐原理](https://developer.aliyun.com/article/979533)
**位域:**
```c
 struct n
 {
    int a:1;  // 一共是32位只需要1位   m=4
    int b:4;  // 一共是32位只需要4位
    int c:2;  // 一共是32位只需要2位
 };
```
注意位域的类型只能是整数类型char、short、long、long long
1. **可移植性**
可移植指的是相同的一段数据或者代码,在不同的平台中都可以成功运行。
- 对于数据来说,有两方面可能会导致不可移植:
- 数据尺寸发生变化
- 数据位置发生变化
- 第一个问题,起因是基本的数据类型在不同的系统所占据的字节数不同造成的,解决办法是使用讨论过的可移植性数据类型即可。接下来讨论第二个问题
- 考虑结构体:
```c
 struct node
 {
    int8_t a;
    int32_t b;
    int16_t c;
 };
```
- 以上结构体在不同的的平台中成员的尺寸是固定不变的但由于不同平台下各个成员的m值可能会发生改变因此成员之间的相对位置可能是飘忽不定的这对数据的可移植性提出了挑战。
- 解决的办法有两种:
- 第一固定每一个成员的m值也就是每个成员之间的塞入固定大小的填充物固定位置
```c
 struct node
 {
    int8_t a __attribute__((aligned(1))); // 将 m 值固定为1
    int64_t b __attribute__((aligned(8))); // 将 m 值固定为8
    int16_t c __attribute__((aligned(2))); // 将 m 值固定为2
 };
```
- 第二,将结构体压实,也就是每个成员之间不留任何空隙:
```c
 struct node
 {
    int8_t a;
    int64_t b;
    int16_t c;
int8_t d;
 } __attribute__((packed));
```
# 二、联合体
1. **联合体的基本概念**
- 联合体在外形上跟结构体非常类似,但他们有一个本质的区别:结构体的各个成员是各自独立的,而联合体的各个成员间是共用一块内存,因此联合体也称共用体
![](images/WEBRESOURCEfb6a239bed5b462a85fa7ec4382c92210d7713ba345e0119c37b9a76eb31bf65.png)
- 联合体内部成员共用一块内存形参"堆叠"效果,使得联合体有如下特征
- 联合体变量的尺寸(内存大小),取决于联合体中尺寸最大的成员
- 联合体的某个成员赋值,会覆盖其他的成员,使它们失效
- 联合体各成员间形成一种"互斥"的逻辑,在某个时刻只有一个成员有效。
- 联合体定义
```c
union 联合体标签
{
成员1;
成员2;
...
};
```
- 语法:
- 联合体标签,用于区分各个不同的联合体。
- 成员,是包含在联合体内部的数据,可以是任意的数据类型。
```c
//定义了一个union attr的联合体类型
union attr
{
int a;
char b;
double c;
};
int mian(void)
{
// 定义union attr类型的联合体变量n
union attr n;
}
```
## 联合体操作
- 初始化
```c
  // 普通初始化第一个成员有效即只有122有效其余无效
    union attr at = {122, 'A', 3.14};
     // 指定成员初始化最后一个成员有效即只有3.14有效,其余无效)
    union attr at1 = {.a=122, .b='A', .c=3.14};
    printf("%d\n", at.a);
    printf("%f\n", at1.c);
```
- 成员引用
```c
union attr n;
    n.a = 233;
    n.b = 'B';
    n.c = 5.23; // 只有最近的一次赋值有效
    printf("%d\n", n.a);
    printf("%c\n", n.b);
    printf("%f\n", n.c);
```
- 联合体指针与数组
```c
p->a = 110;
    p->b = 'C';// 只有最近的一次赋值有效
    printf("%d\n", p->a);
    printf("%c\n", p->b);
    printf("%f\n", p->c);
union attr arr[3] = {[0].a=110,[1].b='a',[2].c=2.13};
    for (int i = 0; i < 3; i++)
    {
        printf("%d\n", arr[i].a);
        printf("%c\n", arr[i].b);
        printf("%f\n", arr[i].c);
    }
```
## 联合体的使用
- 联合体一般很少单独,它经常以一个结构体成员的形式存在,用来表达某种互斥的属性。
```c
struct node
{
int x;
char y;
double z;
union attr at; // at成员有三种属性非此即彼
};
int main(void)
{
struct node n;
n.at.a=100; // 使用连续的成员引用符来索引结构体中的联合体的成员
}
```
## 练习
使用联合体验证当前系统是大端序还是小端序
### 大端序Big-Endian
- **特点****高位字节存低地址,低位字节存高地址**(类似人类读写数字的习惯,从高位到低位)。
- 示例0x12345678的存储方式
| 内存地址 | 存储内容 |
| -- | -- |
| 0x00 | 0x12高位 |
| 0x01 | 0x34 |
| 0x02 | 0x56 |
| 0x03 | 0x78低位 |
- 常见场景:网络协议(如 TCP/IP、部分嵌入式系统、PowerPC 架构等。
### **小端序Little-Endian**
- **特点****低位字节存低地址,高位字节存高地址**(与人类读写习惯相反)。
- 示例0x12345678的存储方式
| 内存地址 | 存储内容 |
| -- | -- |
| 0x00 | 0x78低位 |
| 0x01 | 0x56 |
| 0x02 | 0x34 |
| 0x03 | 0x12高位 |
- 常见场景x86/x86_64 架构(如 Intel、AMD 处理器)、多数 PC 和服务器系统。
# 三、枚举
- 枚举类型的本质是提供一种范围受限的整型比如用0-6表示七种颜色用0-3表示四种状态等但枚举在C语言中并未实现其本来应有的效果直到C++环境下枚举才拥有原本该有的属性。
- 枚举常量列表
- enum是关键字
- spectrum是枚举常量列表标签可以省略。省略的情况下无法定义枚举变量
```c
 enum spectrum{red, orange, yellow=10, green, blue, cyan, purple};
 enum         {reset, running, sleep, stop};
```
- 枚举变量
```
 enum spectrum color = orange; // 等价于 color = 1
```
- 语法要点:
- 枚举常量实质上就是整型首个枚举常量默认为0。
- 枚举常量在定义时可以赋值若不赋值则取其前面的枚举常量的值加1。
- C语言中枚举等价于整型支持整型数据的一切操作。
- 使用举例:
```c
 switch(color)
 {
    case red:
        // 处理红色...
    case orange:
        // 处理橙色...
    case yellow:
        // 处理黄色...  
 }
```
- 枚举数据最重要的作用,是使用有意义的单词,来替代无意义的数字,提高程序的可读性。
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 声明枚举 */
// enum color
// {
//     red,   //常量red 代表值 0
//     orange,
//     yellow=9,
//     green,
//     blue=12,
//     purple
// };
enum
{
    red,   //常量red 代表值 0
    orange,
    yellow,
    green=5,
    blue=5,
    purple
};
int main(void)
{
    // enum color tmp;
   
    printf("red, %d\n", red);
    printf("green, %d\n", green);
    printf("blue, %d\n", blue);
    printf("purple, %d\n", purple);
    return 0;
}
```
# 晚上作业
[结构体测试题.docx](attachments/WEBRESOURCEc2ef36702a6ecf7e2169723a2d147b08结构体测试题.docx)

View File

@@ -0,0 +1,813 @@
# 前导知识
## GCC 编译一共分4个阶段**预处理、编译、汇编、链接**
> gcc 【选项】要编译的文件【选项】【输出文件】
| 选项 | 说明 |
| -- | -- |
| -E | 控制GCC编译器仅对源码做预处理操作 |
| -S | 控制GCC编译器仅对指定文件处理之编译阶段 |
| -c | 控制GCC编译器仅对制定文件处理至汇编阶段并生成相应的目标文件 |
| -o outfile | 指定输出文件的文件名 |
1. 预处理阶段
预处理阶段是编译的第一个阶段。在这个阶段GCC会扫描源代码并执行以下操作
1. 删除注释
1. 替换宏定义
1. 处理条件编译指令
1. 将头文件内容插入源代码中(展开头文件)
```shell
gcc -E hello.c -o hello.i
```
2. 编译阶段
在预处理阶段之后GCC会将源代码翻译成汇编代码这个过程称为编译。编译器会检查源代码是否符合语法以及是否存在语义错误。
在编译阶段,编译器将源代码翻译成汇编代码,以便下一步的汇编阶段使用。
```shell
gcc -S hello.i -o hello.s
```
3. 汇编阶段
汇编阶段是将汇编代码转换为机器代码的过程。在这个阶段,汇编器将汇编代码转换为机器指令,生成目标文件。
在汇编阶段,汇编器将汇编代码转换为机器指令,生成目标文件。
```shell
gcc -c hello.s -o hello.o
```
4. 链接阶段
链接阶段是将所有目标文件合并成一个可执行文件的过程。在这个阶段,链接器将目标文件中未定义的符号与其他目标文件中定义的符号进行匹配,并生成一个可执行文件。
在链接阶段,链接器将目标文件合并成一个可执行文件。
```shell
gcc hello.o -o hello
```
# **预处理**
在C语言程序源码中凡是以井号#开头的语句被称为预处理语句这些语句严格意义上并不属于C语言语法的范畴它们在编译的阶段统一由所谓预处理器cc1来处理。所谓预处理顾名思义指的是真正的C程序编译之前预先进行的一些处理步骤这些预处理指令包括
1. 头文件:#include
1. 定义宏:#define
1. 取消宏:#undef
1. 条件编译:#if#ifdef#ifndef#else#elif#endif
1. 显示错误:#error
1. 修改当前文件名和行号:#line
1. 向编译器传送特定指令:#progma
| 指令 | 描述 | 使用示例 |
| -- | -- | -- |
| #define | 定义宏(符号常量或函数式宏) | #define PI 3.14159 |
| #include | 包含头文件 | #include <stdio.h> |
| #undef | 取消已定义的宏 | #undef PI |
| #ifdef | 如果宏已定义则编译后续代码 | #ifdef DEBUG |
| #ifndef | 如果宏未定义则编译后续代码(常用于头文件保护) | #ifndef HEADER_H |
| #if | 条件编译可配合defined操作符使用 | #if VERSION > 2 |
| #else | #if | #ifdef WIN32 |
| #elif | 类似于else if | #if defined(UNIX) |
| #endif | 结束条件编译块 | 如上例所示 |
| #error | 产生编译错误并输出消息 | #if !defined(C99) |
| #pragma | 编译器特定指令(非标准,各编译器不同) | #pragma once |
- 基本语法
- 一个逻辑行只能出现一条预处理指令,多个物理行需要用反斜杠连接成一个逻辑行
- 预处理是整个编译全过程的第一步:预处理 - 编译 - 汇编 - 链接
- 可以通过如下编译选项来指定来限定编译器只进行预处理操作:
```c
gcc example.c -o example.i -E
```
## **宏**
macro实际上就是一段特定的字串在源码中用以替换为指定的表达式。例如
```c
#define PI 3.14
```
此处PI 就是宏宏一般习惯用大写字母表达以区分于变量和函数但这并不是语法规定只是一种习惯是一段特定的字串这个字串在源码中出现时将被替换为3.14。例如:
```c
int main()
{
printf("圆周率: %f\n", PI);
// 此语句将被替换为printf("圆周率: %f\n", 3.14);
}
```
- 宏的作用:
- 使得程序更具可读性:字串单词一般比纯数字更容易让人理解其含义。
- 使得程序修改更易行:修改宏定义,即修改了所有该宏替换的表达式。
- 提高程序的运行效率:程序的执行不再需要函数切换开销,而是就地展开。
### **无参宏**
无参宏意味着使用宏的时候,无需指定任何参数,比如:
```c
#define PI 3.14
#define SCREEN_SIZE 800*480*4
int main()
{
// 在代码中,可以随时使用以上无参宏,来替代其所代表的表达式:
printf("圆周率: %f\n", PI);
mmap(NULL, SCREEN_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, ...);
}
```
注意到,上述代码中,除了有自定义的宏,还有系统预定义的宏:
```c
// 自定义宏:
#define PI 3.14
#define SCREEN_SIZE 800*480*4
// 系统预定义宏
#define NULL ((void *)0)
#define PROT_READ 0x1 /* Page can be read. */
#define PROT_WRITE 0x2 /* Page can be written. */
#define MAP_SHARED 0x01 /* Share changes. */
```
宏的最基本特征是进行直接文本替换,以上代码被替换之后的结果是:
```c
int main()
{
printf("圆周率: %f\n", 3.14);
mmap(((void *)0), 800*480*4, 0x1|0x2, 0x01, ...);
}
```
### **带参宏**
带参宏意味着宏定义可以携带“参数”,从形式上看跟函数很像,例如:
```c
#define MAX(a, b) a>b ? a : b
#define MIN(a, b) a<b ? a : b
```
以上的MAX(a,b) 和 MIN(a,b) 都是带参宏,不管是否带参,宏都遵循最初的规则,即宏是一段待替换的文本,例如在以下代码中,宏在预处理阶段都将被替换掉:
```c
int main()
{
int x = 100, y = 200;
printf("最大值:%d\n", MAX(x, y));
printf("最小值:%d\n", MIN(x, y));
// 以上代码等价于:
// printf("最大值:%d\n", x>y ? x : y);
// printf("最小值:%d\n", x<y ? x : y);
}
```
- 带参宏的特点:
1. 直接文本替换,不做任何语法判断,更不做任何中间运算。
1. 宏在编译的第一个阶段就被替换掉,运行中不存在宏。
1. 宏将在所有出现它的地方展开,这一方面浪费了内存空间,另一方面有节约了切换时间。
### **带参宏的副作用**
由于宏仅仅做文本替换,中间不涉及任何语法检查、类型匹配、数值运算,因此用起来相对函数要麻烦很多。例如:
```c
#define MAX(a, b) a>b ? a : b
int main()
{
int x = 100, y = 200;
printf("最大值:%d\n", MAX(x, y==200?888:999));
}
```
直观上看,无论 y 的取值是多少,表达式 y==200?888:999 的值一定比 x 要大,但由于宏定义仅仅是文本替换,中间不涉及任何运算,因此等价于:
```c
printf("最大值:%d\n", x>y==200?888:999 ? x : y==200?888:999);
```
可见,带参宏的参数不能像函数参数那样视为一个整体,整个宏定义也不能视为一个单一的数据,事实上,不管是宏参数还是宏本身,都应被视为一个字串,或者一个表达式,或者一段文本,因此最基本的原则是:
- 将宏定义中所有能用括号括起来的部分,都括起来,比如:
```c
#define MAX(a, b) ((a)>(b) ? (a) : (b))
```
### **宏定义中的符号粘贴**
有些时候,宏参数中的符号并非用来传递数据,而是用来形成多种不同的字串,例如在某些系统函数中,系统本身规范了函数接口的部分标准,形如:
```c
void __zinitcall_service_1(void)
{
...
}
void __zinitcall_service_2(void)
{
...
}
void __zinitcall_feature_1(void)
{
...
}
void __zinitcall_feature_2(void)
{
...
}
```
此时,若需要向用户提供一个方便整合字串的宏定义,可以这么写:
```c
#define LAYER_INITCALL(layer, num) __zinitcall_##layer##_##num
```
用户的调用如下:
```c
LAYER_INITCALL(service, 1)();
LAYER_INITCALL(service, 2)();
LAYER_INITCALL(feature, 1)();
LAYER_INITCALL(feature, 2)();
```
**注意:**
在书写非字符串的字串时(如上述例子),使用两边双井号来
```
#define LAYER_INITCALL(num, layer) __zinitcall_##layer##_##num##
```
但如果粘贴的字串并非出现在最末尾,则前后都必须加上双井号:
```c
#define LAYER_INITCALL(num, layer) __zinitcall_##layer##_##num##end
```
**注意:**
另外,如果字串本身拼接为字符串,那么只需要使用一个井号即可,比如:
```c
#define domainName(a, b) "www." #a "." #b ".com"
int main()
{
printf("%s\n", domainName(yueqian, lab));
}
```
执行打印如下:
```shell
gec@ubuntu:~$ ./a.out
www.yueqian.lab.com
gec@ubuntu:~$
```
### **无值宏定义**
定义无参宏的时候,不一定需要带值,无值的宏定义经常在条件编译中作为判断条件出现,例如:
```c
#define BIG_ENDIAN
#define __cplusplus
```
```c
#include <stdio.h>
#define __DEFINE__H__ // 定义无值宏
#ifdef __DEFINE__H__  // 如果定义了宏__DEFINE__H__
// #ifndef __DEFINE__H__  // 如果未定义了宏__DEFINE__H__
// #define __DEFINE__H__  // 上下两个无值宏一般用于头文件中
#define PI    3.141526 // 无参宏
// 带参宏
#define MAX(a,b)  ((a)>(b)?(a):(b))  // 推荐
#define MIN(a,b)  (a<b?a:b)  // 不推荐
// 符号粘贴
#define FUNC(name, num)  func_##name##_##num    // ##表示粘贴字符。当粘贴符号是末尾是不需要结尾的##
#define FUN(name, num)  func_##name##_##num##_end //否则则不能省略结束##
#define domain(s1, s2)  "www."#s1"."#s2".com"  // 若需要粘贴的字符本身是字符串中的子串则只需#
#endif // 结束if条件
void func_abc_1(void)
{
    printf("%s is running\n", __FUNCTION__);
}
void func_abc_2(void)
{
    printf("%s is running\n", __FUNCTION__);
}
void func_xyz_1(void)
{
    printf("%s is running\n", __FUNCTION__);
}
void func_xyz_2(void)
{
    printf("%s is running\n", __FUNCTION__);
}
void func_abc_1_end(void)
{
    printf("%s is running\n", __FUNCTION__);
}
void func_abc_2_end(void)
{
    printf("%s is running\n", __FUNCTION__);
}
void func_xyz_1_end(void)
{
    printf("%s is running\n", __FUNCTION__);
}
void func_xyz_2_end(void)
{
    printf("%s is running\n", __FUNCTION__);
}
int main(int argc, char *argv[])
{
    // printf("圆周率:%.2f\n", PI);
    // int *p = NULL;
   
    // int x = 100, y = 200;
    // printf("最大值:%d\n", MAX(x, y==200?888:999));
    FUNC(abc,1)();
    FUNC(abc,2)();
    FUNC(xyz,1)();
    FUNC(xyz,2)();
    FUN(abc,1)();
    FUN(abc,2)();
    FUN(xyz,1)();
    FUN(xyz,2)();
    printf("%s\n", domain(gec,edu));
    return 0;
}
```
## **条件编译**
- 概念:有条件的编译,通过控制某些宏的值,来决定编译哪段代码。
- 形式:
- 形式1判断表达式 MACRO 是否为真,据此决定其所包含的代码段是否要编译
- 注意:#if形式条件编译需要有值宏
```c
#define A 0
#define B 1
#define C 2
#if A
... // 如果 MACRO 为真,那么该段代码将被编译,否则被丢弃
#endif
```
```c
// 二路分支
#if A
...
#elif B
...
#endif
```
```c
// 多路分支
#if A
...
#elif B
...
#elif C
...
#else
...
#endif
```
- 形式:
- 形式2判断宏 MACRO 是否已被定义,据此决定其所包含的代码段是否要编译
```c
// 单独判断
#ifdef MACRO
...
#endif
// 二路分支
#ifdef MACRO
...
#else
...
#endif
```
- 形式:
- 形式3判断宏MACRO是否未被定义据此决定其所包含的代码段是否要编译
```c
// 单独判断
#ifndef MACRO
...
#endif
// 二路分支
#ifndef MACRO
...
#else
...
#endif
```
- 总结:
- #ifdef#ifndef此种形式,判定的是宏是否已被定义,这不要求宏有值。
- #if#elif 这些形式,判定的是宏的值是否为真,这要求宏必须有值。
- #ifdef#ifndef和#if结尾需要添加#endif表示结束条件,用以包裹代码块选择那一部分代码参与编译。
### **条件编译的使用场景**
控制调试语句在程序中用条件编译将调试语句包裹起来通过gcc编译选项随意控制调试代码的启停状态。例如
```
gcc example.c -o example -DMACRO
```
以上语句中,-D意味着 DefineMACRO 是程序中用来控制调试语句的一个宏,如此一来就可以在完全不需要修改源代码的情况下,通过外部编译指令选项非常方便地控制调试信息的启停。
选择代码片段:在一些大型项目中(例如 Linux 内核),某个相同功能的模块往往有不同的实现,需要用户根据具体的情况来“配置”,这个所谓的配置的过程,就是对代码中不同的宏的选择的过程。
例如:
```c
#define A 0 // 网卡1
#define B 1 // 网卡2 √
#define C 0 // 网卡3
// 多路分支
#if A
...
#elif B
...
#elif C
...
#endif
```
```c
#include <stdio.h>
#define A 1   //声卡
#define B 0   //网卡
#define C 0   //串口
#if A
void fun_init_1(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
#endif
#if B
void fun_init_2(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
#endif
#if C
void fun_init_3(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
#endif
void fun_destory_1(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
void fun_destory_2(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
void fun_destory_2_end(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
int main()
{
#if A
    fun_init_1();
#endif
#if B
    fun_init_2();
#endif
#if C
    fun_init_3();
#endif
    return 0;
}
```
```c
#include <stdio.h>
#define A 0   //声卡
#define B 0   //网卡
#define C 1   //串口
#if A
void fun_init_1(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
#elif B
void fun_init_2(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
#else
void fun_init_3(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
#endif
void fun_destory_1(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
void fun_destory_2(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
void fun_destory_2_end(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
int main()
{
#if A
    fun_init_1();
#elif B
    fun_init_2();
#else
    fun_init_3();
#endif
    return 0;
}
```
```c
#include <stdio.h>
void fun_destory_1(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
// #define DEBUG
#ifndef DEBUG
void fun_destory_2(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
#endif
#ifdef  DEBUG   //gcc demo4.c -DDEBUG 编译时添加宏定义
void fun_destory_3_end(void)
{
    printf("%s is running...\n", __FUNCTION__);
}
#endif
int main(void)
{
    fun_destory_1();
#ifdef  DEBUG
    fun_destory_3_end();
#endif
#ifndef DEBUG
    fun_destory_2();
#endif
    return 0;
}
```
## **头文件**
通常一个常规的C语言程序会
![](images/WEBRESOURCE61a25737180b4d579df0faf4cf8bb1c4stickPicture.png)
### **头文件的内容**
- 头文件中所存放的内容,就是各个源码文件的彼此可见的公共资源,包括:
1. 全局变量的声明。
1. 普通函数的声明。
1. 静态函数的定义(内联函数)。
1. 宏定义。
1. 结构体、联合体的定义。
1. 枚举常量列表的定义。
1. 其他头文件。
- 示例代码:
```
// head.h
extern int global; // 1全局变量的声明
extern void f1(); // 2普通函数的声明
static void f2() // 3静态函数的定义
{
...
}
#define MAX(a, b) ((a)>(b)?(a):(b)) // 4宏定义
struct node // 5结构体的定义
{
...
};
union attr // 6联合体的定义
{
...
};
#include <unistd.h> // 7其他头文件
#include <string.h>
#include <stdint.h>
```
- 特别说明:
1. 全局变量、普通函数的定义一般出现在某个源文件(*.c )中,其他的源文件想要使用都需要进行声明(extern),因此一般放在头文件中更方便。
1. 静态函数、宏定义、结构体、联合体的定义都只能在其所在的文件可见,因此如果多个源文件都需要使用的话,放到头文件中定义是最方便,也是最安全的选择。
### **头文件的使用**
头文件编写好了之后,就可以被各个所需要的源码文件包含了,包含头文件的语句就是如下预处理指令:
```
// main.c
#include "head.h" // 包含自定义的头文件
#include <stdio.h> // 包含系统预定义的文件
int main()
{
...
}
```
可以看到,在源码文件中包含指定的头文件有两种不同的形式:
- 使用双引号:在指定位置 + 系统标准路径搜索 head.h
- 使用尖括号:在系统标准路径搜索 stdio.h
### **头文件的格式**
由于头文件包含指令 #include 的本质是复制粘贴,并且一个头文件中可以嵌套包含其他头文件,因此很容易出现一种情况是:头文件被重复包含。
- 使用条件编译,解决头文件重复包含的问题,格式如下:
```
#ifndef _HEADNAME_H
#define _HEADNAME_H
...
... (头文件正文)
...
#endif
```
其中HEADNAME一般取头文件名称的大写
# 文件组织
一个简易示例
![](images/WEBRESOURCEb5c62efd51d146318ed20b94a949b34astickPicture.png)
由于自定义的头文件一般放在源码文件的周围,因此需要在编译的时候通过特定的选项来指定位置,而系统头文件都统一放在标准路径下,一般无需指定位置。
假设在源码文件 main.c 中包含了两个头文件head.h 和 stdio.h ,由于他们一个是自定义头文件,一个是系统标准头文件,前者放在项目 pro/inc 路径下,后者存放于系统头文件标准路径下(一般位于 /usr/include因此对于这个程序的编译指令应写作
```
gec@ubuntu:~/pro$ gcc main.c -o main -I /home/gec/pro/inc
```
其中,/home/gec/pro/inc 是自定义头文件 head.h 所在的路径
- 语法要点:
- 预处理指令 #include 的本质是复制粘贴:将指定头文件的内容复制到源码文件中。
- 系统标准头文件路径可以通过编译选项 -v 来获知,比如:
```
gec@ubuntu:~/pro$ gcc main.c -I /home/gec/pro/inc -v
... ...
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/7/include
/usr/local/include
/usr/lib/gcc/x86_64-linux-gnu/7/include-fixed
/usr/include/x86_64-linux-gnu
/usr/include
... ...
```
![](images/WEBRESOURCEc1cc8a34d5ae4ef6bd588fcb003a0b83image.png)
![](images/WEBRESOURCEcb03335b039dd3101bd1e33868113656image.png)
**注意:*是通配符表示将src目录下的所有以.c结尾的文件都参与编译**
![](images/WEBRESOURCE54710c7a76c34ce186adf7550689b814image.png)
> **表示告诉编译器头文件所在路径,编译会去指定的路径寻找头文件**
**应用场景(模块化编程)**
## 练习
将图书管理系统的C语言文件使用
![](images/WEBRESOURCE2abc07694c4f358e3cffaedba77bcb90image.png)

View File

@@ -0,0 +1,121 @@
# 一、静态库和动态库
- 库文件与源文件的关系是C语言源代码文件是可以直接阅读且可以直观的了解其内部的实现原理库文件依然属于源码文件但是编程人员无法阅读。
- 如果我们不想将源码(函数的实现)展现给别人,但又需要给别人提供源码所实现的关键功能,就可以将这些源码文件封装成库文件,只提供给使用者对应的头文件和库文件即可。既可以使他人使用自己编写的代码,又能保护自己的知识产权,同时又能方便自己升级与维护。
- 库文件的一般格式为
- 动态库libxxx.so(libxxx.so.2.1.0)
- 静态库libxxx.a
## 1、动态库
- Linux中的动态库的后缀为.so前缀为lib即libxxx.soxxx是库的名字libmylib.sowindows的动态库的后缀为.dll等
1制作动态库
```shell
gcc  -shared  -fPIC  -o  libhello.so  hello.c  -I../include/
```
> 不加 -fPIC 生成的动态库,“ 生成动态库时假定它被加载在地址 0 处。加载时它会被加载到一个地址base这时要进行一次重定位relocation把代码、数据段中所有的地址加上这个 base 的值。这样代码运行时就能使用正确的地址了。”
> 加上 fPIC 选项生成的动态库,是位置与无关的,这样的代码本身就能被放到线性地址空间的任意位置,无需修改就能正确执行。
> 通常的方法是获取指令指针的值,加上一个偏移得到全局变量 / 函数的地址。
> 加 fPIC 选项的源文件对于它引用的函数头文件编写有很宽松的尺度。比如只需要包含某个声明的函数的头文件,即使没有相应的 C 文件来实现,编译成 so 库照样可以通过。
2使用动态库
编译:
```shell
gcc -o ./bin/hello ./src/main.c -I ./include/ -L ./lib/ -lhello
```
说明:
```
-I 告诉编译器头文件的路径
-L 告诉编译器库文件的路径
-l 链接库文件名如libhello.so去头去尾hello
```
库的名字是libmylib.so链接时就写-lmylib
运行:
![](images/WEBRESOURCE4684b7496c77366547f869ae2b65b6d7image.png)
报错:
![](images/WEBRESOURCE79d40806b934939775f7460001a79cbbimage.png)
解决方法:
第一种:指定库路径,但只对当前终端有效(绝对路径)
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:****/lib
第二种:将库文件拷贝到系统库目录中(/lib  or  /usr/lib
cp  libfun.so  /usr/lib
第三种:将添加环境变量的指令写入当前终端脚本种"~/.bashrc"
2、静态库
Linux中的动态库的后缀为.a前缀为lib即libxxx.axxx是库的名字libmylib.a
windows的动态库的后缀为.lib
1制作静态库
①先生成.o文件
```
gcc -o hello.o -c hello.c  -I ../include/
```
②生成静态库
```shell
ar  rc   libhello.a  hello.o
```
2使用静态库
编译:
```
gcc -o ./bin/target ./src/main.c -I ./include/ -L ./lib/ -lphoto
```
说明:
```c
-I 告诉编译器头文件的路径
-L 告诉编译器库文件的路径
-l 链接库文件名,如libhello.so去头去尾hello
```
库的名字是libmylib.aa链接时就写-lmylib
运行:
./target
![](images/WEBRESOURCE809cda2ce0aa92b5b344707c1d4a3629image.png)
![](images/WEBRESOURCE598297eb23f470b37d35d6d3f1451fe9image.png)
![](images/WEBRESOURCE11280c9480d3c2eee39d2dc737f123cfimage.png)
- 静态库和动态库的区别:
- 静态库生成的可执行文件运行时不再需要依赖静态库文件,因为静态库会直接参与编译因此可执行文件较大,不方便维护和升级(升级功能必须程序编译整个工程得到新的可执行程序方可使用新功能)。
- 动态库生成的可执行文件运行时还需要依赖动态库文件,因为动态库只链接而不参与编译因此可执行文夹较小,方便维护和升级(程序函数名字返回值与参数列表不改变的情况下只修改函数功能不需要重新编译整个工程,只需呀编译这个文件生成新的动态库替换原来的动态库即可使用新的功能(或者将库文件的名字带上版本号使用链接文件的形式生成库文件<lib****.so>))。
![](images/WEBRESOURCE048d46d9bb04ceb46b8fe1f3cc419d92image.png)

View File

@@ -0,0 +1,208 @@
作者:粤嵌-李林峰
# 终端显示字体和颜色的方法
在终端中使用printf输出时可以通过ANSI转义码来控制文本的颜色、背景色和字体样式。下面详细介绍各种控制方法。
## 基本格式
ANSI转义码的基本格式为
```c
printf("\033[属性值;...;属性值 m 文本内容 \033[0m");
```
其中:
- \033 是ESC字符的八进制表示也可以用\x1b十六进制表示
- [ 开始控制序列
- 属性值 是控制代码,多个属性用分号分隔
- m 结束控制序列
- \033[0m 重置所有属性
## 字体样式控制
| 代码 | 效果 |
| -- | -- |
| 0 | 重置所有属性 |
| 1 | 加粗/高亮 |
| 2 | 暗淡/弱化 |
| 3 | 斜体 |
| 4 | 下划线 |
| 5 | 闪烁 |
| 7 | 反显(反色) |
| 8 | 隐藏 |
| 9 | 删除线 |
示例:
```c
printf("\033[1m加粗文本\033[0m\n");
printf("\033[3m斜体文本\033[0m\n");
printf("\033[4m下划线文本\033[0m\n");
```
## 前景色(文字颜色)
| 代码 | 颜色 |
| -- | -- |
| 30 | 黑色 |
| 31 | 红色 |
| 32 | 绿色 |
| 33 | 黄色 |
| 34 | 蓝色 |
| 35 | 洋红 |
| 36 | 青色 |
| 37 | 白色 |
| 90 | 亮黑 |
| 91 | 亮红 |
| 92 | 亮绿 |
| 93 | 亮黄 |
| 94 | 亮蓝 |
| 95 | 亮洋红 |
| 96 | 亮青 |
| 97 | 亮白 |
示例:
```c
printf("\033[31m红色文字\033[0m\n");
printf("\033[92m亮绿色文字\033[0m\n");
```
## 背景色
| 代码 | 颜色 |
| -- | -- |
| 40 | 黑色 |
| 41 | 红色 |
| 42 | 绿色 |
| 43 | 黄色 |
| 44 | 蓝色 |
| 45 | 洋红 |
| 46 | 青色 |
| 47 | 白色 |
| 100 | 亮黑 |
| 101 | 亮红 |
| 102 | 亮绿 |
| 103 | 亮黄 |
| 104 | 亮蓝 |
| 105 | 亮洋红 |
| 106 | 亮青 |
| 107 | 亮白 |
示例:
```c
printf("\033[41m红色背景\033[0m\n");
printf("\033[104m亮蓝色背景\033[0m\n");
```
## 组合使用
可以同时设置多个属性,用分号分隔:
```c
printf("\033[1;31;42m加粗红色文字绿色背景\033[0m\n");
printf("\033[4;93;44m下划线亮黄色文字蓝色背景\033[0m\n");
```
## 256色模式
对于支持256色的终端可以使用更丰富的颜色
前景色:
```c
printf("\033[38;5;颜色编号m文本\033[0m");
```
背景色:
```c
printf("\033[48;5;颜色编号m文本\033[0m");
```
颜色编号范围0-255其中
- 0-15标准16色
- 16-2316×6×6 RGB立方
- 232-255灰度
示例:
```c
printf("\033[38;5;196m红色文字\033[0m\n");
printf("\033[48;5;226m黄色背景\033[0m\n");
```
## RGB真彩色模式
最新终端支持RGB真彩色
前景色:
```c
printf("\033[38;2;R;G;Bm文本\033[0m");
```
背景色:
```c
printf("\033[48;2;R;G;Bm文本\033[0m");
```
其中R,G,B为0-255的数值。
示例:
```c
printf("\033[38;2;255;100;100m自定义颜色文字\033[0m\n");
printf("\033[48;2;100;200;255m自定义背景色\033[0m\n");
```
## 注意事项
1. 不是所有终端都支持所有颜色和效果
1. 使用后记得用\033[0m重置属性
1. 某些效果(如闪烁)可能被终端禁用
1. Windows CMD默认不支持需要启用ANSI支持或使用其他终端
## 实用技巧
可以定义宏简化使用:
```c
#define RED "\033[31m"
#define GREEN "\033[32m"
#define RESET "\033[0m"
printf(RED "错误: " RESET "发生错误\n");
```
或者定义函数:
```c
void print_color(const char* color, const char* text)
{
printf("%s%s" RESET, color, text);
}
print_color("\033[1;34m", "重要信息");
```
参考文件:
[terminal_colors.h](attachments/WEBRESOURCEf8f411dd54c53da227a89fc7031bb0c7terminal_colors.h)

View File

@@ -0,0 +1,115 @@
// terminal_colors.h
/**
* @file terminal_colors.h
* @brief ANSI终端颜色和字体样式控制宏定义
* @author 李林峰
* @date 2025-07-22
* @version 1.0
*
* 本文件提供了一套完整的ANSI转义码宏定义用于控制终端文本的颜色、
* 背景色和字体样式。支持标准16色、256色模式下的颜色定义
* 以及各种字体样式(加粗、斜体、下划线等)。
*
* 使用说明:
* 1. 包含本头文件:#include "terminal_colors.h"
* 2. 使用预定义的宏组合颜色和样式
* 3. 每条彩色输出后应使用RESET宏重置终端属性
* 4. 推荐使用PRINT_COLOR系列宏它们会自动处理RESET
*
* 示例:
* printf(RED "红色文本" RESET "\n");
* PRINT_COLOR(BOLD BLUE BG_WHITE, "加粗蓝色文本白色背景");
* PRINT_ERROR("错误消息");
*/
#ifndef TERMINAL_COLORS_H
#define TERMINAL_COLORS_H
// ==================== 基本控制宏 ====================
#define RESET "\033[0m" // 重置所有属性
// ==================== 常规颜色 ====================
#define BLACK "\033[30m"
#define RED "\033[31m"
#define GREEN "\033[32m"
#define YELLOW "\033[33m"
#define BLUE "\033[34m"
#define MAGENTA "\033[35m"
#define CYAN "\033[36m"
#define WHITE "\033[37m"
// ==================== 亮色 ====================
#define BRIGHT_BLACK "\033[90m"
#define BRIGHT_RED "\033[91m"
#define BRIGHT_GREEN "\033[92m"
#define BRIGHT_YELLOW "\033[93m"
#define BRIGHT_BLUE "\033[94m"
#define BRIGHT_MAGENTA "\033[95m"
#define BRIGHT_CYAN "\033[96m"
#define BRIGHT_WHITE "\033[97m"
// ==================== 背景色 ====================
#define BG_BLACK "\033[40m"
#define BG_RED "\033[41m"
#define BG_GREEN "\033[42m"
#define BG_YELLOW "\033[43m"
#define BG_BLUE "\033[44m"
#define BG_MAGENTA "\033[45m"
#define BG_CYAN "\033[46m"
#define BG_WHITE "\033[47m"
// ==================== 亮背景色 ====================
#define BG_BRIGHT_BLACK "\033[100m"
#define BG_BRIGHT_RED "\033[101m"
#define BG_BRIGHT_GREEN "\033[102m"
#define BG_BRIGHT_YELLOW "\033[103m"
#define BG_BRIGHT_BLUE "\033[104m"
#define BG_BRIGHT_MAGENTA "\033[105m"
#define BG_BRIGHT_CYAN "\033[106m"
#define BG_BRIGHT_WHITE "\033[107m"
// ==================== 字体样式 ====================
#define BOLD "\033[1m" // 加粗/高亮
#define DIM "\033[2m" // 暗淡
#define ITALIC "\033[3m" // 斜体
#define UNDERLINE "\033[4m" // 下划线
#define BLINK "\033[5m" // 闪烁
#define REVERSE "\033[7m" // 反显
#define HIDDEN "\033[8m" // 隐藏
#define STRIKETHROUGH "\033[9m" // 删除线
// ==================== 组合宏 ====================
#define ERROR_COLOR BOLD RED
#define WARNING_COLOR BOLD YELLOW
#define SUCCESS_COLOR BOLD GREEN
#define INFO_COLOR BOLD BLUE
#define DEBUG_COLOR BOLD CYAN
#define ERROR_BG BOLD RED BG_WHITE
#define WARNING_BG BOLD YELLOW BG_BLACK
#define SUCCESS_BG BOLD GREEN BG_BLACK
#define INFO_BG BOLD BLUE BG_BLACK
// ==================== 打印宏 ====================
/**
* @brief 打印带颜色的消息(自动换行和重置)
* @param color 颜色/样式组合宏
* @param msg 要打印的消息
*/
#define PRINT_COLOR(color, msg) printf("%s%s" RESET "\n", color, msg)
/**
* @brief 打印带颜色的消息(不换行,自动重置)
* @param color 颜色/样式组合宏
* @param msg 要打印的消息
*/
#define PRINT_COLOR_NR(color, msg) printf("%s%s" RESET, color, msg)
// ==================== 常用消息快捷方式 ====================
#define PRINT_ERROR(msg) PRINT_COLOR(ERROR_COLOR, msg)
#define PRINT_WARNING(msg) PRINT_COLOR(WARNING_COLOR, msg)
#define PRINT_SUCCESS(msg) PRINT_COLOR(SUCCESS_COLOR, msg)
#define PRINT_INFO(msg) PRINT_COLOR(INFO_COLOR, msg)
#define PRINT_DEBUG(msg) PRINT_COLOR(DEBUG_COLOR, msg)
#endif // TERMINAL_COLORS_H

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Some files were not shown because too many files have changed in this diff Show More