vault backup: 2026-04-14 23:26:55
130
Collection/YoudaoyunNotes/02C语言/01-C语言概述.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# 一、概述
|
||||
|
||||
C 语言是一种通用的、
|
||||
|
||||
## 1. **发展历程**
|
||||
|
||||
- **1972 年**:C 语言诞生于贝尔实验室,作为 UNIX 系统的开发语言。
|
||||
|
||||
- **1989 年**:ANSI C(C89)成为首个标准化版本,奠定语言基础。
|
||||
|
||||
- **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. **开发环境**
|
||||
|
||||
- 编译器:GCC(Linux/macOS)、MinGW(Windows)。
|
||||
|
||||
- IDE:CLion(JetBrains)、Code::Blocks、VsCode。
|
||||
|
||||
# 六、怎么学好C语言
|
||||
|
||||
1. 先学习C语言的基础知识,打好基础
|
||||
|
||||
1. 多看、多写、多思考、多练习编程
|
||||
|
||||
1. 开始着手写一些简单的项目,如小游戏
|
||||
|
||||
1. 在网上寻找一些大佬的项目进行观摩、学习和积累经验。
|
||||
|
||||
> 只要写不死,就往死里写
|
||||
> ----学好C语言
|
||||
715
Collection/YoudaoyunNotes/02C语言/03-数据类型及IO流.md
Normal file
@@ -0,0 +1,715 @@
|
||||
# 一、基本数据类型
|
||||
|
||||
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
|
||||
|
||||

|
||||
|
||||
使用方法
|
||||
|
||||
```c
|
||||
char a = 'a'; // 字符在内存中以ASCII码值的形式存在,因此char a='a'等价于 char a=97
|
||||
// 单字节整数类型
|
||||
```
|
||||
|
||||

|
||||
|
||||
记忆方法:**'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. 长整型
|
||||
|
||||
类型关键字:long(32位与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
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
1. 双精度浮点型
|
||||
|
||||
类型关键字:double
|
||||
|
||||
类型长度:8 字节,15位有效位
|
||||
|
||||
取值范围:2.3E-308 到 1.7E+308
|
||||
|
||||

|
||||
|
||||
1. 长双精度浮点型
|
||||
|
||||
类型关键字:long double
|
||||
|
||||
类型长度:16 字节,19位有效位
|
||||
|
||||
取值范围:3.4E-4932 到 1.1E+4932
|
||||
|
||||

|
||||
|
||||
注意:
|
||||
|
||||
比较两个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 字节
|
||||
|
||||
## 数据类型溢出
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
# 二、变量
|
||||
|
||||
## 概念
|
||||
|
||||
在内存中,数据可以被修改的空间,在程序执行过程中数据的值可发生修改,
|
||||
|
||||
常见形式:数据类型 用户标识符;
|
||||
|
||||
变量声明声明定义:
|
||||
|
||||
```c
|
||||
char a; // 声明定义一个char类型的变量a
|
||||
int b; // 声明定义一个int类型的变量b
|
||||
float c; // 声明定义一个float类型的变量c
|
||||
```
|
||||
|
||||
变量的初始化
|
||||
|
||||
```c
|
||||
char a = 'a'; // =在C语言中属于赋值操作即将某个数据放入谋片内存空间,声明定义一个char类型的变量a并进行初始化赋值
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 局部变量
|
||||
|
||||
声明定义在函数内部,在当前的{}中有效,函数的参数是局部变量
|
||||
|
||||
## 全局变量
|
||||
|
||||
声明定义在函数的外部,在本文件中有效,所有函数都可访问。
|
||||
|
||||
**注意:**
|
||||
|
||||
```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位左补0,421码进行转换):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位往左补0,8421码进行转换):0x4D
|
||||
|
||||
**十进制转N进制**
|
||||
|
||||
十进制:123
|
||||
|
||||
除N取余倒记法(短除法)
|
||||
|
||||

|
||||
|
||||
**八进制转N进制**
|
||||
|
||||
转二进制:一个八进制等于三位二进制,421(05 ==》 101)
|
||||
|
||||
转十进制:使用从右往左逐位乘8的(0+位的位置-1)次幂求和
|
||||
|
||||
转十六进制:转二进制后转十六进制
|
||||
|
||||
**十六进制转N进制**
|
||||
|
||||
转二进制:一个十六进制等于四位二进制,8421(0x7 ==》 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正),在内存中用其绝对值原码的补码(符号位保持不变)进行存储,
|
||||
|
||||

|
||||
|
||||
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码值,实际上字符常量在内存中是单字节的整型常量数据
|
||||
|
||||

|
||||
|
||||
**记忆ASCII:'A' --> 65 'a' --> 97 '0'-->48**
|
||||
|
||||

|
||||
|
||||
**总结:整型常量、字符常量、浮点常量都可通过对应的类型的变量进行表示。**
|
||||
|
||||
1. **字符串常量**
|
||||
|
||||
字符串常量用双引号包含,例如:"xyz",字符串量使用指针或数组表示,具体细节如下
|
||||
|
||||
- 字符串在内存中实际是一个连续的字符常量组合
|
||||
|
||||
- 任何字符串都以一个字符'\0'作为结束标记,如"funny story"在内存中的存储细节如下
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
- **注意**:""表示一个字符串,虽然是空的字符串但其内存不空拥有内存1字节保存的是'\0'字符。
|
||||
|
||||
1. **常量的定义**
|
||||
|
||||
在C语言中常量的表示方法有两种方式:
|
||||
|
||||
1. 使用`#define`宏定义预处理指令表示
|
||||
|
||||
```c
|
||||
#define PI 3.14 // PI表示浮点常量3.14,在预处理时使用了PI的代码会被3.14自动替换
|
||||
// 这样使用的目的时将字面量在源码中进行隐藏使用更容易理解的单词进行表示可以提高代码的易读性
|
||||
```
|
||||
|
||||
1. 使用const关键字, const关键字用于声明一个只读变量,即变量的值不能再发生修改,使用const关键声明常量时必须初始化赋值。
|
||||
|
||||
```c
|
||||
const int a = 5; // a是一个只读变量即a是一个常量其值为5
|
||||
a = 10; // 错误,不允许被修改
|
||||
```
|
||||
|
||||

|
||||
|
||||
在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)
|
||||
|
||||
# 五、输入输出格式化控制符
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
输入: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;
|
||||
}
|
||||
```
|
||||
|
||||
## 练习
|
||||
|
||||
编写代码在终端输出如下内容
|
||||
|
||||

|
||||
|
||||
# 六、IO流
|
||||
|
||||
1. **概念**
|
||||
|
||||
键盘是系统的标准输入设备,从键盘输入数据称为标准输入(stdin);屏幕终端是系统的标准输出设备,在屏幕上输出数据称为标准输出(stdout),在屏幕上输出出错信息称为标准出错(stderr),这些输入输出称为IO流。
|
||||
|
||||
在计算机系统中当需要使用一种或多种IO流设备时,计算机系统就会自动的形成三种缓冲机制(stdin、stdout、stderr)用于在程序执行期间存储IO的数据,这样的缓冲机制称为缓冲区(临时存放数据,数据是无价的)。
|
||||
|
||||

|
||||
|
||||
```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码值,则输出对应的大小写字母。
|
||||
|
||||
# 七、类型转换
|
||||
|
||||
- 概念:不一致但相兼容的数据类型,在同一表达式中将发生类型转换
|
||||
|
||||
- 转换模式:
|
||||
|
||||
- 隐式转换:系统按照隐式规则自动进行转换
|
||||
|
||||
- 显示转换:也称强制转换,用户显式的自定义进行转换
|
||||
|
||||
- 隐式规则:从小类型到大类型转换,目的表达式中的数据精度不丢失
|
||||
|
||||

|
||||
|
||||
```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
|
||||
...
|
||||
```
|
||||
380
Collection/YoudaoyunNotes/02C语言/05-控制与分支.md
Normal file
@@ -0,0 +1,380 @@
|
||||
# 一、分支控制
|
||||
|
||||
1. **二路分支**
|
||||
|
||||
- if...else语句
|
||||
|
||||
- 执行逻辑:非此即彼
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
- 语法形式
|
||||
|
||||
```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,多选一。
|
||||
|
||||

|
||||
|
||||
- **if语句单独使用多次**
|
||||
|
||||
- 表达一种多种条件种选择执行的逻辑,前面的if语句满足条件执行后面的if继续判断执行,多选多或多选一。
|
||||
|
||||

|
||||
|
||||
- **注意:**
|
||||
|
||||
- else语句只与前面最近且同级的if配套,并且else不能单独使用。
|
||||
|
||||
- 若if语句或else语句中需要执行的代码只有一条语句则{}花括号可以省略,其他情况下{}花括号不能省略否则只有第1条语句属于if...else语句中
|
||||
|
||||
- **switch...case语句**
|
||||
|
||||
- 执行逻辑:根据选项选择不同的代码段进行执行
|
||||
|
||||
- 语法形式:
|
||||
|
||||
```c
|
||||
switch(选择语句)
|
||||
{
|
||||
case 选项1:
|
||||
break;
|
||||
case 选项2:
|
||||
break;
|
||||
...
|
||||
case 选项n:
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
- 表达多个选项中选择一个进行执行
|
||||
|
||||

|
||||
|
||||
- **注意**
|
||||
|
||||
- switch语句中的选择语句必须是整数(整型、字符型、布尔型、枚举型、变量、 运算表达式甚至是函数调用)case 语句中的选项必须是整型常量或char 型常量,const 型变量都不行, 例如:case 'w'
|
||||
|
||||

|
||||
|
||||
- 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强制退出程序>)
|
||||
|
||||

|
||||
|
||||
👍
|
||||
|
||||
> 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 循环不是先计算表达式, 而是先执行循环体在计算表达式的值, 因此它也被称为退出条件循环,即在每次执行循环体之后再检查判断条件, 这样
|
||||
|
||||

|
||||
|
||||
```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所有奇数的和【三种循环编写】
|
||||
|
||||

|
||||
|
||||
1. 计算1^2+2^2+...+n^2
|
||||
|
||||

|
||||
|
||||
1. 计算n的阶乘
|
||||
|
||||
- **break、continue与return语句**
|
||||
|
||||
- **break语句**
|
||||
|
||||
- 用于循环语句中时表示跳出当前循环(会结束循环)
|
||||
|
||||
- 用于switch语句时表示跳出switch语句
|
||||
|
||||
```c
|
||||
for()
|
||||
{
|
||||
while()
|
||||
{
|
||||
do{
|
||||
switch()
|
||||
{
|
||||
break;// 结束switch语句
|
||||
}
|
||||
break; // 结束do...while
|
||||
} while();
|
||||
break; // 结束while
|
||||
}
|
||||
break; // 结束for
|
||||
}
|
||||
```
|
||||
|
||||
- **continue语句**
|
||||
|
||||
- 只用于循环语句中,表示结束本次循环,进行下次循环
|
||||
|
||||

|
||||
|
||||
- **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>")。
|
||||
412
Collection/YoudaoyunNotes/02C语言/06-函数基础.md
Normal file
@@ -0,0 +1,412 @@
|
||||
# 一、概述
|
||||
|
||||
在C语言中,函数指的是功能模块。一个典型的C语言程序,就是由一个个的功能模块拼接起来的整体,因此C语言也称模块化语言。
|
||||
|
||||
对于函数的使用者,可以简单的将函数视为一个黑箱子,使用者只管按照规定给黑箱一些输入,就会得到一些输出,使用者不需要关注黑箱内部的结构细节。
|
||||
|
||||

|
||||
|
||||
函数分为两种形式
|
||||
|
||||
- 系统自带函数(库函数):只需了解如何使用和结果是什么,如:购买了一台电视机,我们只需要了解电视机对外的接口和使用的方法及最后的结果,不需要知道电视机内部构造----怎么用。
|
||||
|
||||
> 函数的头文件
|
||||
> 函数的功能
|
||||
> 函数的参数
|
||||
> 函数的返回值
|
||||
|
||||
|
||||
- 用户自定义函数:要明确最终实现的功能,如:自己设计电视机 ----怎么写。
|
||||
|
||||
> 函数的声明式
|
||||
> 函数的实现
|
||||
> 函数的调用
|
||||
|
||||
|
||||
# 二、函数入门
|
||||
|
||||

|
||||
|
||||
- 函数头:函数对外的接口及运行的结果都在这里体现
|
||||
|
||||
- 组成
|
||||
|
||||
- 函数的类型:是函数的运行结果数据类型即黑箱的输出数据类型(函数返回值类型),不是必须的,没有返回值是使用void。
|
||||
|
||||
- 函数的名字:函数在内存中的地址,代表这个黑箱的名称(必须满足用户标识符定义规则),一个函数必须要有名字。
|
||||
|
||||
- 函数的参数列表:函数的输入,即黑箱的输入数据列表,不是必须的没有参数输入也不能省略括号(),应当在参数列表中使用void进行修饰
|
||||
|
||||
- 函数体:函数的功能实现,即黑箱子的内部构造。
|
||||
|
||||

|
||||
|
||||
```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[])"意味着主函数也支持接收数据。
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
注意:
|
||||
|
||||
- 每个命令行参数间使用空格(一个或多个皆可),若参数本身就带有空格则需要使用单引号或双引号将整个内容包含
|
||||
|
||||

|
||||
|
||||
- 一般情况下若需要接收命令行参数,则需要在主函数中判定参数是否符号要求,以保障程序的正确执行
|
||||
|
||||

|
||||
|
||||
## 练习
|
||||
|
||||
接收命令行参数三个,输出这个三个数的和
|
||||
|
||||
# 五、变参函数
|
||||
|
||||
- 概念:调用函数时可根据实际需求来决定函数参数的个数
|
||||
|
||||
```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();
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
- 节省函数间切换所需的时间,提高函数的运行效率
|
||||
|
||||
- 原理:一个普通函数在调用过程中,会在调用这个函数的函数中形成保护现场和恢复现场的过程,这个需要花费时间,降低程序的运行效率,这时可以将这个函数设计内联函数,在编译器编译过程中,编译器会将符合标准的内联函数直接展开(使用函数的功能代码替换函数的调用),这样就会节省保护现场和恢复现场的时间,同时又做到了模块化编程。
|
||||
|
||||

|
||||
|
||||
- 内联函数的使用场景
|
||||
|
||||
- 代码精简,功能语句简短不具有循环、switch等语句。
|
||||
|
||||
- 调用频繁
|
||||
|
||||
**注意:内联函数不是写了inline声明的函数就是内联函数,能否构成内联函数是由编译器决定。若添加inline关键字但不符合编译器的标准,则编译器会将其视为普通函数。**
|
||||
|
||||
[函数练习题.docx](attachments/WEBRESOURCE94606765850c0696e801236ba49014a2函数练习题.docx)
|
||||
|
||||
# 作业
|
||||
|
||||
[函数作业题.docx](attachments/WEBRESOURCEa2d88b6292b52c177b4bc6323a5bdd07函数作业题.docx)
|
||||
6
Collection/YoudaoyunNotes/02C语言/07-数组.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
tags:
|
||||
- empty
|
||||
aliases: empty
|
||||
日期: 2026/1/23
|
||||
---
|
||||
410
Collection/YoudaoyunNotes/02C语言/08-指针.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# **1. 准备知识**
|
||||
|
||||
**1.1 内存地址**
|
||||
|
||||
- 字节:字节是内存的容量单位,英文称为 Byte,一个字节有8比特位,即 1Byte = 8bits 1B = 8b
|
||||
|
||||
- 地址:系统为了便于区分每一个字节而对它们逐一进行的编号,称为内存地址,简称地址。
|
||||
|
||||

|
||||
|
||||
## **1.2 基地址**
|
||||
|
||||
- 单字节数据:对于单字节数据而言,其地址就是其字节编号。
|
||||
|
||||
- 多字节数据:对于多字节数据而言,其地址是其所有字节地址中编号最小的那个,称为基地址。
|
||||
|
||||

|
||||
|
||||
### **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位)
|
||||
|
||||
- 不同的地址虽然表示形式一样,但他们所代表的内存尺寸(在内存中所占内存大小)和类型都不相同,因此相同形式的地址编号在逻辑上要严格进行区分
|
||||
|
||||

|
||||
|
||||
# 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位系统下的理解如下:
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
- 注意:指针用于存放地址的变量,由于在相同的系统位数下地址的尺寸相同,故指针的尺寸相同
|
||||
|
||||

|
||||
|
||||
- 指针变量的内存尺寸只于系统字长相关,与指针的类型无关。
|
||||
|
||||
## 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;
|
||||
```
|
||||
|
||||

|
||||
|
||||
## 练习
|
||||
|
||||
编写一个函数实现两个数交换。
|
||||
|
||||
# 3.特殊指针
|
||||
|
||||
## 3.1野指针
|
||||
|
||||
- 指向一块未知区域的指针,称为野指针。**野指针是危险的**
|
||||
|
||||

|
||||
|
||||
- 危害
|
||||
|
||||
- 引用野指针,相当于访问了非法内存,常常会导致段错误(segmentatio fault)
|
||||
|
||||
- 引用野指针,可能会破坏系统关键数据,导致系统崩溃等严重后果。
|
||||
|
||||
- 产生原因
|
||||
|
||||
- 指针定义式未进行初始化
|
||||
|
||||
- 指针指向内存被系统回收
|
||||
|
||||
- 指针越界
|
||||
|
||||
- 如何防止
|
||||
|
||||
- 定义指针时进行及时初始化(定义时无法明确指向则赋值为NULL)
|
||||
|
||||
- 绝不引用已被回收的内存(回收内存后令指针指向NULL)
|
||||
|
||||
- 确认所申请的内存边界,谨防越界
|
||||
|
||||
## 3.2空指针
|
||||
|
||||
```c
|
||||
NULL // (void *)0;
|
||||
```
|
||||
|
||||
- 很多情况下,我们不可避免的会遇到野指针,如刚刚定义指针无法立即为其分配一块内存时,又或者指针所指向的内存被释放了等等。
|
||||
|
||||
- 对于一个暂时无法让其指向一块合法内存的指针而言, 我们最好将其初始化为“空指针” , 即给他赋一个空值(零) , 让他指向零地址
|
||||
|
||||

|
||||
|
||||
```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;
|
||||
```
|
||||
|
||||

|
||||
|
||||
```c
|
||||
char a,b;
|
||||
char * const p = &a;
|
||||
|
||||
// p = &b; // 错误,常指针不能修改其值
|
||||
|
||||
*p = 'S'; // 可通过指针对目标进行读写
|
||||
printf("%c\n", *p);
|
||||
```
|
||||
|
||||
- 常目标指针:const修饰指针的目标,表示无法通过该指针修改其目标(常量指针)。
|
||||
|
||||

|
||||
|
||||
```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个步长
|
||||
```
|
||||
|
||||

|
||||
|
||||
- 要点:数据类型不同步长也不同,如: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
|
||||
|
||||
```
|
||||
312
Collection/YoudaoyunNotes/02C语言/09-数组与指针进阶.md
Normal 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 *。
|
||||
|
||||

|
||||
|
||||
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)
|
||||
397
Collection/YoudaoyunNotes/02C语言/10-函数进阶.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# 一、C语言函数库
|
||||
|
||||
- 概念:在C语言发展过程中收录了很多经典的数据操作方法----函数,将这些函数收录归纳汇总为开发人员方便使用的API接口(函数),如下图所示的各种操作接口库。
|
||||
|
||||

|
||||
|
||||
# 二、字符串操作函数
|
||||
|
||||
- 使用三部曲
|
||||
|
||||
- 确认头文件
|
||||
|
||||
- 确认函数功能
|
||||
|
||||
- 确认函数的参数与返回值
|
||||
|
||||
- C语言标准字符串函数库,头文件"#include <string.h>"
|
||||
|
||||

|
||||
|
||||
- **函数strlen**
|
||||
|
||||

|
||||
|
||||
```c
|
||||
char *p = "www.yueqian.edu.com.cn";
|
||||
printf("粤嵌官网的地址长度是:%d\n", strlen(p));
|
||||
```
|
||||
|
||||
- **函数strcat与strncat**
|
||||
|
||||

|
||||
|
||||
- 注意:
|
||||
|
||||
- 这两个函数的功能一样,都是将src字符串复制到dest的末尾。
|
||||
|
||||
- strcat()没有边界控制,因此可能会由于src字符串过长导致dest无法保存从而导致内存溢出。
|
||||
|
||||
- strncat()有边界控制,可以限制拼接字符的格式,保证dest不会因为越界而导致内存溢出。
|
||||
|
||||
- 更加值得推荐的字符串拼接函数sprintf()与snprintf(),头文件"#include <stdio.h>"
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
- 要点: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**
|
||||
|
||||

|
||||
|
||||
- 注意:
|
||||
|
||||
- 该函数会将改变原始字符串 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()按照格式提取字符串中的内容
|
||||
|
||||

|
||||
|
||||
```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**
|
||||
|
||||

|
||||
|
||||
```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**
|
||||
|
||||

|
||||
|
||||
```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**
|
||||
|
||||

|
||||
|
||||
```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**
|
||||
|
||||

|
||||
|
||||
```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’ 被认为是字符串的一部分。
|
||||
|
||||
## 练习
|
||||
|
||||
自己封装函数实现strlen,strcat,strtok,strstr,strcpy,strcmp函数的功能。
|
||||
|
||||
# 晚上作业
|
||||
|
||||
1. 定义一个长度为20的整型数据生成随机数对这个数组进行初始化,编写排序函数对这个数据中的数据进行从小到大排序
|
||||
|
||||
- 要求编写五种排序函数:冒泡排序、选择排序、插入排序、快速排序、希尔排序
|
||||
|
||||
- 提示:
|
||||
|
||||
- **各种排序算法的****时间复杂度****与****空间复杂度**
|
||||
|
||||

|
||||
|
||||
- **冒泡排序**
|
||||
|
||||
- 顺序:两个数据位置符合排序要求
|
||||
|
||||
- 逆序:两个数据位置不符合排序要求
|
||||
|
||||
- 思路:从头到尾让两个相邻数据进行比较,顺序保持不变,逆序交换位置,经过一轮比较序列中具有一个“极值”将被挪至末端。
|
||||
|
||||

|
||||
|
||||
```c
|
||||
|
||||
```
|
||||
|
||||
- **插入排序**
|
||||
|
||||
- 思路:假设数列前面有i个节点的序列是有序的,那么就从第i+1个节点开始,插入到前面i个节点中的合适位置。由于序列的第一个节点始终视为有序,所以实在从第二个节点开始。
|
||||
|
||||

|
||||
|
||||
```c
|
||||
|
||||
```
|
||||
|
||||
- **选择排序**
|
||||
|
||||
- 在无序序列中依次从头到尾挑选合适的节点放入有序序列。
|
||||
|
||||

|
||||
|
||||
```c
|
||||
|
||||
```
|
||||
|
||||
- **快速排序**
|
||||
|
||||
- 快排是一直典型的递归思想,相比较其他排序它需要跟多的空间,理论上时间效率是最高的。
|
||||
|
||||
- 思想:在待排序序列中选取一个数据,作为“支点”,然后其他数据与支点比较,(升序)比支点小的放左边,比支点大的放右边,全部比较完后支点位于两个序列的中间,这叫一次划分(partition)
|
||||
|
||||

|
||||
|
||||
- 一次划分之后,序列的内部也许无序,但是左右序列与支点三者间,形成了一种基本有序状态,接下来使用相同的思路,递归的对左右序列进行排序,直到子序列的长度小于等于1为止;
|
||||
|
||||

|
||||
|
||||
```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、
|
||||
|
||||
**结果**
|
||||
|
||||

|
||||
|
||||
```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)
|
||||
305
Collection/YoudaoyunNotes/02C语言/11-内存管理.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# **C语言程序内存布局**
|
||||
|
||||
任何一个程序,正常运行都需要内存资源,用来存放诸如变量、常量、函数代码等等。这些不同的内容,所存储的内存区域是不同的,且不同的区域有不同的特性。因此我们需要研究C语言进程的内存布局,逐个了解不同内存区域的特性。
|
||||
|
||||
每个C语言程序运行后(进程)都拥有一片结构相同的虚拟内存,所谓的虚拟内存,就是从实际物理内存映射出来的地址规范范围,最重要的特征是所有的虚拟内存布局都是相同的,极大地方便内核管理不同的进程。例如三个完全不相干的进程p1、p2、p3,它们很显然会占据不同区段的物理内存,但经过系统的变换和映射,它们的虚拟内存的布局是完全一样的。
|
||||
|
||||
- PM:Physical Memory,物理内存。
|
||||
|
||||
- VM:Virtual Memory,虚拟内存。
|
||||
|
||||

|
||||
|
||||
将其中一个C语言进程的虚拟内存放大来看,会发现其内部包下区域:
|
||||
|
||||
- 栈(stack)
|
||||
|
||||
- 堆(heap)
|
||||
|
||||
- 数据段
|
||||
|
||||
- 代码段
|
||||
|
||||

|
||||
|
||||
虚拟内存中,内核区段(1GB)对于应用程序而言是禁闭的,它们用于存放操作系统的关键性代码,另外由于 Linux 系统的历史性原因,在虚拟内存的最底端 0x0 ~ 0x08048000 之间也有一段禁闭的区段(128MB),该区段也是不可访问的。
|
||||
|
||||
虚拟内存中各个区段的详细内容:
|
||||
|
||||

|
||||
|
||||
# **栈内存**
|
||||
|
||||
- 什么东西存储在栈内存中?
|
||||
|
||||
- 环境变量
|
||||
|
||||
- 命令行参数
|
||||
|
||||
- 局部变量(包括形参)
|
||||
|
||||
- 栈内存有什么特点?
|
||||
|
||||
- 空间有限,尤其在嵌入式环境下。因此不可以用来存储尺寸太大的变量。
|
||||
|
||||
- 每当一个函数被调用,栈就会向下增长一段,用以存储该函数的局部变量。(随用随申请,用完系统自动释放)
|
||||
|
||||
- 每当一个函数退出,栈就会向上缩减一段,将该函数的局部变量所占内存归还给系统。(由系统自动进行管理)
|
||||
|
||||
- 注意:
|
||||
|
||||

|
||||
|
||||
Linux中栈空间的大小可以使用ulimit -a进行查看,若使用时超出这个范围则称为"栈溢出"
|
||||
|
||||

|
||||
|
||||
- 示例代码:
|
||||
|
||||
```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段:存放系统初始化代码,编译系统会自动为每一个程序文夹添加系统初始化代码。
|
||||
|
||||

|
||||
|
||||
```c
|
||||
int a; // 未初始化的全局变量,放置在.bss 中
|
||||
int b = 100; // 已初始化的全局变量,放置在.data 中
|
||||
|
||||
int main(void)
|
||||
{
|
||||
static int c; // 未初始化的静态局部变量,放置在.bss 中
|
||||
static int d = 200; // 已初始化的静态局部变量,放置在.data 中
|
||||
|
||||
// 以上代码中的常量100、200防止在.rodata 中
|
||||
}
|
||||
```
|
||||
|
||||
- 注意:数据段和代码段内存的分配和释放,都是由系统规定的,我们无法干预。
|
||||
|
||||
# **堆内存**
|
||||
|
||||
堆内存(heap)又被称为动态内存、自由内存,简称堆。堆是唯一可被开发者自定义的区段,开发者可以根据需要申请内存的大小、决定使用的时间长短等。但又由于这是一块系统“飞地”,所有的细节均由开发者自己把握,系统不对此做任何干预,给予开发者绝对的“自由”,但也正因如此,对开发者的内存管理提出了很高的要求。对堆内存的合理使用,几乎是软件开发中的一个永恒的话题。
|
||||
|
||||
- 堆内存基本特征:
|
||||
|
||||
- 相比栈内存,堆的总大小仅受限于物理内存,在物理内存允许的范围内,系统对堆内存的申请不做限制。
|
||||
|
||||
- 相比栈内存,堆内存从下往上增长。
|
||||
|
||||
- 堆内存是匿名的,只能由指针来访问。
|
||||
|
||||
- 自定义分配的堆内存,除非开发者主动释放,否则永不释放,直到程序退出。
|
||||
|
||||

|
||||
|
||||
- 相关API:
|
||||
|
||||
- 申请堆内存:malloc() / calloc()
|
||||
|
||||
- 清零堆内存:bzero()
|
||||
|
||||
- 释放堆内存:free()
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
- 示例:
|
||||
|
||||
```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。
|
||||
|
||||
- 释放内存并不会对内存做任何修改,更不会将内存清零。
|
||||
|
||||

|
||||
|
||||
- **什么时候用栈什么时候用堆?**
|
||||
|
||||
- 基本数据类型(int char float double)就用栈,复合数据类型(结构体、联合体)就用堆。
|
||||
|
||||
- **练习**
|
||||
|
||||
- 使用堆空间计算两个大数的乘积(如:123456789123456789123456789*987456321987456123698745)
|
||||
813
Collection/YoudaoyunNotes/02C语言/14-预处理与文件组织.md
Normal 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意味着 Define,MACRO 是程序中用来控制调试语句的一个宏,如此一来就可以在完全不需要修改源代码的情况下,通过外部编译指令选项非常方便地控制调试信息的启停。
|
||||
|
||||
选择代码片段:在一些大型项目中(例如 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语言程序会
|
||||
|
||||

|
||||
|
||||
### **头文件的内容**
|
||||
|
||||
- 头文件中所存放的内容,就是各个源码文件的彼此可见的公共资源,包括:
|
||||
|
||||
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),因此一般放在头文件中更方便。
|
||||
|
||||
2. 静态函数、宏定义、结构体、联合体的定义都只能在其所在的文件可见,因此如果多个源文件都需要使用的话,放到头文件中定义是最方便,也是最安全的选择。
|
||||
|
||||
### **头文件的使用**
|
||||
|
||||
头文件编写好了之后,就可以被各个所需要的源码文件包含了,包含头文件的语句就是如下预处理指令:
|
||||
|
||||
```
|
||||
// main.c
|
||||
#include "head.h" // 包含自定义的头文件
|
||||
#include <stdio.h> // 包含系统预定义的文件
|
||||
|
||||
int main()
|
||||
{
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
可以看到,在源码文件中包含指定的头文件有两种不同的形式:
|
||||
|
||||
- 使用双引号:在指定位置 + 系统标准路径搜索 head.h
|
||||
|
||||
- 使用尖括号:在系统标准路径搜索 stdio.h
|
||||
|
||||
### **头文件的格式**
|
||||
|
||||
由于头文件包含指令 `#include` 的本质是复制粘贴,并且一个头文件中可以嵌套包含其他头文件,因此很容易出现一种情况是:头文件被重复包含。
|
||||
|
||||
- 使用条件编译,解决头文件重复包含的问题,格式如下:
|
||||
|
||||
```
|
||||
#ifndef _HEADNAME_H
|
||||
#define _HEADNAME_H
|
||||
|
||||
...
|
||||
... (头文件正文)
|
||||
...
|
||||
|
||||
#endif
|
||||
```
|
||||
|
||||
其中,HEADNAME一般取头文件名称的大写
|
||||
|
||||
# 文件组织
|
||||
|
||||
一个简易示例
|
||||
|
||||

|
||||
|
||||
由于自定义的头文件一般放在源码文件的周围,因此需要在编译的时候通过特定的选项来指定位置,而系统头文件都统一放在标准路径下,一般无需指定位置。
|
||||
|
||||
假设在源码文件 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
|
||||
... ...
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**注意:`*`是通配符,表示将src目录下的所有以.c结尾的文件都参与编译**
|
||||
|
||||

|
||||
|
||||
> **表示告诉编译器头文件所在路径,编译会去指定的路径寻找头文件**
|
||||
|
||||
|
||||
**应用场景(模块化编程)**
|
||||
|
||||
## 练习
|
||||
|
||||
将图书管理系统的C语言文件使用
|
||||
|
||||

|
||||
@@ -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
|
||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 456 KiB |
|
After Width: | Height: | Size: 178 KiB |
|
After Width: | Height: | Size: 4.3 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 395 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 768 B |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 312 KiB |
|
After Width: | Height: | Size: 86 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 27 KiB |