二、高级C语言
1.内存存储
(1)整型强转
计算机只能计算加法,因为有负数的存在,所以我们在保存的数值时,会相应的给数值提供一位符号位,其他为数值位---------->原反补码规则,所有的数据在计算机中的存储都以补码的形式存在------>可以解决加减法的问题
正数的原反补码都相同
负数的原码------------->反码:符号位不变其余为取反 补码:反码+1;
例子:
// ==========1. 原反补码演示==========例:
小字节→大字节:符号扩展:有符号数补符号位;无符号数高位补 0。
大字节→小字节:直接截断低位,丢弃高位。
| 类型 | 字节 | 格式符 | 取值范围 | 位说明 |
|---|---|---|---|---|
| char | 1 | %hhd | ‑128 ~ 127 | 1 符号位 + 7 数值位 |
| short | 2 | %hd | ‑32768 ~ 32767 | 1 符号位 + 15 数值位 |
| int | 4 | %d | ‑2³¹ ~ 2³¹‑1 | 1 符号位 + 31 数值位 |
| long long | 8 | %lld | ‑2⁶³ ~ 2⁶³‑1 | 1 符号位 + 63 数值位 |
| unsigned char | 1 | %hhu | 0 ~ 255 | 8 位全数值位 |
| unsigned short | 2 | %hu | 0 ~ 65535 | 16 位全数值位 |
| unsigned int | 4 | %u | 0 ~ 2³²‑1 | 32 位全数值位 |
| unsigned long long | 8 | %llu | 0 ~ 2⁶⁴‑1 | 64 位全数值位 |
// ==========2. 同一内存,有符号/无符号解析不同==========例:
// ==========3. 大字节变小字节:直接截断低位==========例:
// ==========4. 小字节变大字节==========例:
// ==========5. char特殊值 -128 10000000==========例:
// ==========6. 无符号数没有负数,减法会变成大正数==========
//==========7. 循环陷阱:无符号做循环变量==========
(2)小数强转
浮点数没有原码、反码、补码,则使用SME规则,分为单精度浮点数和双精度浮点数
S:符号位,0 正,1 负
M:尾数,规格化:1<=M<2,隐藏整数部分的
1,内存只存小数部分E:真实指数,存储时要加偏移量
float---4Byte---32bit :S(1) + E(8) + M(23),偏移量127,指数值=真实值+127
double---8Byte---64bit:S(1) + E(11) + M(52),偏移量1023,指数值=真实值+1023
例如:
// ==========1. 同一块内存,int 和 float 解析完全不同==========
// ==========2. 精度丢失:小数不能精确二进制表示==========
// ==========3. 正负0,无穷大,NaN==========
// ==========4. limits.h float.h 宏常量==========
2.高级指针
什么是指针:可以保存一个字节编号的空间
什么叫做字节的编号:人们更方便的使用字节,所以给每个字节进行特定的编号
例如:电影院每个座位都有编号 编号的存在更方便的使用这些空间
计算机为了更好使用字节去保存人们给他的东西,所以给计算机每个Byte进行了特定的编号
x86下 有32根地址线产生的+(0) -(1) 给不同的字节进行不同编号 计算机在申请使用时,可以更方便的操作每一个Byte
x64下 有64根地址线产生的+(0) -(1) 给不同的字节进行不同编号 计算机在申请使用时,可以更方便的操作每一个Byte
(1)解引用
解引用 *pType *p:
解引用规则:拿 p 存的起始地址,向后读取sizeof(Type)个字节,把这一串二进制按照Type类型解析。 内存里永远存补码:不同类型指针指向同一块内存,读取出来的值完全不一样。
int main(void) { int a = 100; int* p = &a; // p保存a的地址,p是指针 printf("a的值:%d\n", a); printf("a的地址:%p\n", &a); printf("指针p存的地址:%p\n", p); printf("解引用*p,访问地址里的值:%d\n", *p); // 通过解引用修改原变量的值 *p = 200; printf("修改*p后 a = %d\n", a); return 0; }(2)指针加减运算
指针+数:
type*p:
p+/-n=原地址+/-n*sizeof(type),通过p中保存的地址往前或者往后加或者减(sizeof(Type)*n)个Byte的地址
char*+1:地址 + 1
short*+1:地址 + 2
int*+1:地址 + 4
long long*/double*+1:地址 + 8
指针相加减:
条件:两个指针类型完全相同,指向同一块连续空间
(3) 数组名退化(重中之重)
数组名只有两种场景**不退化**,其余全部退化为首元素地址:
①sizeof(数组名):代表整个数组,计算数组总字节;
②&数组名:取整个数组的地址,类型为「数组指针」Type (*)[N]。
其余场景数组名退化规则:
一维数组 int a[n] → 退化类型 int*;
二维数组 int a[m][n] → 退化类型 int (*)[n](行指针);
三维数组 int a[m][n][k] → 退化类型 int (*)[n][k]。
#include <stdio.h> int main() { int a[4] = { 1,2,3,4 }; printf("sizeof(a) = %d\n", sizeof(a)); // `a` 放在`sizeof`内部,**数组不退化**,代表整个数组 `int[4]`; // 总字节 = 元素个数 × 单个 int 大小 = `4 * 4 = 16`; // 输出:`sizeof(a) = 16` printf("sizeof(a+0) = %d\n", sizeof(a + 0)); //`a+0` 不是`sizeof`直接包裹数组名,数组**退化**为 `int*`(首元素地址); //`a+0` 等价 `&a[0]`,本质是指针; //32 位下所有指针 sizeof 结果都是 4; // 输出:`sizeof(a + 0) = 4` printf("sizeof(*a) = %d\n", sizeof(*a)); //`a`退化`int*`,`*a`等价`a[0]`,是普通`int`类型; //int 固定 4 字节; // 输出:`sizeof( * a) = 4` printf("sizeof(&a) = %d\n", sizeof(&a)); //&a:取整个数组的地址,类型为数组指针 int (*)[4]; //只要是指针,32 位 sizeof 一律 4; //输出:sizeof(&a) = 4 printf("sizeof(*&a) = %d\n", sizeof(*&a)); //&a 是 int (*)[4] 数组指针; //*&a 对数组指针解引用,还原出原始数组 int[4]; //和第一条sizeof(a)完全等价,总大小 16; // 输出:sizeof(*&a) = 16 printf("sizeof(&a[0]+1) = %d\n", sizeof(&a[0] + 1)); //- `&a[0]`:首元素地址,类型`int*`; //`&a[0] + 1`:指针算术,指向下一个元素, * *运算后依然是 int* 指针 * *; // sizeof 作用在指针,结果 4; //输出:`sizeof( & a[0] + 1) = 4` return 0; }(4)指针数组VS数组指针
优先级:`[]`>`*`,变量名先和谁结合就是谁
例:
指针数组:int *arr[10]
arr 先和[]结合,是数组,每个元素类型int*;
sizeof (arr) = 10 × 4 = 40,arr 退化类型 int**;
arr+1 偏移 4 字节(一个指针大小)。
数组指针:int (*brr)[10]
brr 先和*结合,是指针;指向int[10]整个数组;
sizeof(brr) = 4;*brr等价一维数组名int[10];
brr+1 偏移 10×4=40 字节(一整行)。
(5)二维数组
例:int arr[m][n]--------->二维数组
int main() { int arr[2][5] = { {1,2,3,4,5}, {6,7,8,9,10} }; // 1. 打印元素 printf("arr[1][3] = %d\n", arr[1][3]); printf("*(*(arr+1)+3) = %d\n", *(*(arr + 1) + 3)); //arr+1: 0+1*sizeof(int *[5])=20---int [5] +3--20+3*4=32 int 32 33 34 35 ----9 // 2. 行指针偏移演示 int (*p_row)[5] = arr; printf("arr地址:%p\n", arr); printf("arr+1地址:%p 相差%d字节\n", arr + 1, (char*)(arr + 1) - (char*)arr); //arr+1:0+4*5=20 // 3. 普通列指针偏移 int* p_col = arr[0]; printf("arr[0]地址:%p\n", p_col); printf("arr[0]+1地址:%p 相差%d字节\n", p_col + 1, (char*)(p_col + 1) - (char*)p_col); // 4. 经典考题 int* ptr1 = (int*)(&arr + 1); int* ptr2 = *(arr + 1); printf("*(ptr1-1)=%d, *(ptr2-1)=%d\n", *(ptr1 - 1), *(ptr2 - 1)); return 0; }(6)强制类型转换指针
任意一块连续内存,只要把首地址强制转为不同类型指针:
①访问步长随sizeof(Type)变化;
②二进制解析规则改变(整型补码 / 浮点数 SME);
③大小端:x86 小端存储,低字节存在低地址,强转 short/int 会逆序读取字节。
3.结构体/联合体/枚举
(1)结构体类型设计
结构体struct:自定义复合数据类型,把多个不同类型 / 相同类型的变量打包成一个整体,用来描述现实中一个完整事物(学生、图书、员工、坐标等)。
struct 结构体名
{
成员列表(基本数据类型、指针、数组、其他的结构体类型);};//末尾直接定义变量
3.1.1结构体三种定义方式:
方式 1:先声明结构体类型,再定义(使用最多)
方式 2:定义类型同时直接创建变量
方式 3:匿名结构体(只能当场定义变量,无法复用,极少用)
测试:
// 子结构体:日期 typedef struct Date { int year; int month; int day; } Date; // 外层结构体:学生,嵌套Date typedef struct Student { char name[20]; int id; Date birth; // 嵌套结构体成员 } Stu; int main() { // 结构体初始化 Stu s = { "小张", 2026001, {2006,6,1} }; // 嵌套访问:变量.子结构体.成员 printf("出生年月:%d-%d-%d", s.birth.year, s.birth.month, s.birth.day); Stu* p = &s; // 指针嵌套访问:p->子结构体.成员 printf("\n姓名:%s", p->name); printf("\n年份:%d", p->birth.year); return 0; }结构体是一种数据结构,是创建变量的模板,不占用内存空间;结构体变量才是数据,需要存储空间。
3.1.2结构体变量的内存大小:
规则:
结构体整体对齐数 = 所有成员中最大基础类型字节
每个成员偏移必须是自身大小整数倍,不足填充空白字节(内存填充 padding)
struct S1 { char a; // 0 (1 2 3) int b;//4 5 6 7 };//8Byte struct S2 { int b;//0 1 2 3 char a;//4 (5 6 7) };//8Byte struct S3 { char c1;//0 (1) short s;//2 3 char c2;//4 5 };//6Byte struct Date { char ch;//0 (1 2 3 ) int i;//4 5 6 7 };//8Byte struct Stu//嵌套结构体 { char name;//0 (1 2 3) struct Date d;//4 5 6 7 8 9 10 11 int score;//12 13 14 15 };//16Byte struct Test { char a;//0 (1^7) double b;//8 9 10 11 12 13 14 15 int c;//16 17 18 19 (10 21 22 23 ) };//24Byte int main(void) { printf("sizeof(S1)=%d\n", sizeof(struct S1)); //8 printf("sizeof(S2)=%d\n", sizeof(struct S2)); //8 printf("sizeof(S3)=%d\n", sizeof(struct S3)); //6 printf("sizeof(Date)=%d\n", sizeof(struct Date));//8 printf("sizeof(Stu)=%d\n", sizeof(struct Stu)); //16 printf("sizeof(Test)=%d\n", sizeof(struct Test));//24 return 0; }3.1.3结构体变量和结构体指针:(变量用点,指针用箭头)
结构体变量:
访问成员:.点运算符
格式:结构体变量.成员名
结构体指针:结构体指针,存放结构体变量的地址
两种访问成员方式:->箭头运算符(指针专用,最常用 )、‘(*指针).成员` 等价写法,括号**不能丢**
注意:在C语言中不存在对结构体类型强制转换(和内置类型的区别)
(2)共用体(联合体)
union-----共用体(联合体)
3.2.1共用体和结构体的区别:
①结构体变量的各个数据成员同时被分配了各自独立的存储空间(内存),相互之间没有影响;共用体变量的每一个数据成员的起始地址都相同,所有数据成员占用同一段内存,修改一个数据成员会影响其他所有数据成员。②结构体变量占用的内存大小,大于等于所有数据成员占用内存的总和,字节对齐问题。
3.2.2共用体的内存大小
规则:取变量的数据类型最大
union Un1 { char c; int i; };//4Byte union Un2 { char arr[5]; double d; };//8Byte struct S { char a; int b; };//4Byte union Un3 { struct S s; double db; };//8Byte int main(void) { printf("Un1 = %d\n", sizeof(union Un1)); //4 printf("Un2 = %d\n", sizeof(union Un2)); //8 printf("Un3 = %d\n", sizeof(union Un3)); //8 //共用内存演示 union Un1 u; u.i = 0x11223344; printf("赋值i之后 i = %#x\n", u.i); u.c = 0x99; printf("给c赋值后 i = %#x\n", u.i); //i低字节被覆盖 return 0; }(3)枚举
enum枚举:把一组有限、固定、有含义的常量打包,替代数字魔法值,可读性更高。枚举就是---列举,把需要的取值一一列举。
enum 枚举名 {
枚举常量1,
枚举常量2,
枚举常量3
};
enum week{mon,tues,wed,thurs,fri,sat,sun};
//不指定值,枚举值默认从0开始,往后逐个加1(递增)
enum week{mon=1,tues=2,wed=3,thurs=4,fri=5,sat=6,sun=7};
//可以给每一个名字都指定一个值
enum week{mon=1,tues,wed,thur,fri,sat,sun};
//也可只给第一个名字指定值,往后在原有值得基础上逐个加1(递增)
3.3.1枚举类型的特点:
①枚举类型是受限制的有符号整数数据类型;int;
②枚举元素(枚举常量)必须是有符号整型数据值;
③枚举类型定义的变量,取值范围必须是枚举集合中枚举元素的值
测试1:星期枚举,默认赋值(从 0 开始)
测试2:手动指定常量值--------指定某一个,后面自动 + 1,常量可以重复数值。
3.3.2枚举与常变量、#define定义常量的不区别:
①增加代码的可读性和可维护性;
②和#define定义的标识符比较、枚举有类型检查,更加严重;
③封装性好;
④便于调试;
⑤使用方便,一次可以定义多个变量。
3.3.3枚举常量注意:
①枚举标识符是常量,不能修改;
②默认自动赋值规则---第一个枚举值默认 = 0,后面每一个 = 前一个常量 + 1。
③手动指定数值,后面顺延 + 1;
④允许多个枚举常量值重复;
⑤常量只能写整数,不能写小数、字符串。
3.3.4枚举变量注意:
①枚举变量本质是 int 类型,sizeof(任意枚举类型)` 永远等于 `sizeof(int)`,32 位 / 64 位下都是 4 字节。
②枚举变量可以直接赋值整数(语法允许,但不推荐);
③只能用 `==`、`!=` 比较;可以和 int 互相比较
4.动态内存管理(malloc /calloc/realloc /free)
(1)什么是动态内存
4.1.1程序内存分为程序运行时的内存通常分为以下几个主要区域:
- 栈(Stack):用于存储局部变量、函数参数、返回地址等。栈内存由编译器自动分配和释放,遵循后进先出(LIFO)原则。其大小有限,通常较小。
- 堆(Heap):用于动态内存分配。程序员通过
malloc、calloc、realloc等函数手动申请堆内存,并通过free函数手动释放。堆内存空间通常较大,但管理不当容易导致内存泄漏或碎片。 - 全局区/静态存储区:存放全局变量、静态变量(包括静态局部变量和静态全局变量)。该区域在程序启动时分配,程序结束时释放。
- 代码段(Text Segment):存放程序的机器指令(即代码本身),通常是只读的。
因此,动态内存 = 堆(Heap)内存,指的是程序员在运行时主动申请和释放的内存空间,与栈、全局区等自动管理的内存区域相区别。
程序内存是栈、堆、全局区、代码段,动态内存 = 堆 (Heap) 内存。
4.1.2动态内存VS静态内存
| 动态内存 | 堆区(heap),手动申请、手动释放; 运行时自由指定大小、随时扩容缩小; 必须头文件:`#include <stdlib.h>` |
| 静态内存 | 局部变量:栈,函数结束自动销毁;数组长度必须常量,运行时不能改大小。 全局 / 静态变量:全局数据区,程序结束释放。 缺陷:数组长度写死,无法根据运行数据扩容 / 缩容。 |
(2)动态内存管理函数
4.2.1 malloc
向堆区申请一块大小得连续内存空间:
#include <stdlib.h> void *malloc(size_t size);//typedef unsigned int size_t功能:分配size字节得末初始化内存,若分配成功,则返回为任何拥有基础对齐得对象类型对齐得指针。可返回空指针;也可以返回非空指针,到最后释放内存--free以避免内存泄漏。malloc是线程安全的。
参数:size要分配的字节数
返回值:
成功:返回分配到内存的首地址(void*,需要强转成对应类型指针)
失败:返回NULL(内存不够分配)
4.2.2 calloc
分配并使用零初始化连续内存空间:
void* calloc(size_t num,size_t size); int *ip=(int *)calloc(10,sizeof(int));//memset(p,0,10*sizeof(int));参数:
num:元素个数
size:每个元素占的字节数
返回值:成功返回首地址;失败返回NULL
注意:分配内存,并且自动把全部字节初始化为 0
功能:为num个对象(元素)的数组分配内存,并初始化所有分配存储中的字节为零。calloc是线程安全的。
4.2.3 realloc
使用realloc函数扩充之前分配的内存 块(重新分配内存块):
void*realloc(void*ptr,size_t new_size);参数:
ptr:之前 malloc /calloc 得到的旧内存指针
new_size:新的总字节数
返回值:成功则指向新分配内存的指针,失败则返回空指针
功能:重新分配给定的内存区域,realloc是线程安全的
4.2.4 free
用来释放从malloc、realloc、calloc成功获取到动态内存分配的空间:
void free(void*ptr);功能:释放之前由malloc、calloc、realloc等分配的空间
若ptr为空指针,则函数不进行操作就返回。
若ptr的值不是之前从malloc、calloc、realloc等返回的值,则行为未定义。
若ptr所指代的内存区域已经被分配(已释放),则行为未定义。
参数:ptr指向要解分配的内存的指针。free是线程安全的。
(3)动态内存的使用
#include <stdio.h> #include <stdlib.h> int main() { // 1.malloc:分配4个int,不初始化 int* arr = (int*)malloc(4 * sizeof(int)); if (arr == NULL) // 必须判断分配是否失败 { perror("malloc fail");//打印系统错误信息。 //当库函数(`malloc`、`open`、`realloc`等)调用失败时,系统会把错误编号存到全局变量 `errno`; // `perror(字符串)`:先打印你传的自定义字符串,再打印冒号,接着打印系统对应的错误描述。 return 1; } // 赋值使用 for (int i = 0; i < 4; i++) { arr[i] = i + 1; } printf("malloc初始:"); for (int i = 0; i < 4; i++) { printf("%d ", arr[i]); } printf("\n"); // 2.realloc扩容到7个int,必须用临时指针接收! int* tmp = (int*)realloc(arr, 7 * sizeof(int)); if (tmp == NULL) { perror("realloc fail"); free(arr); // 失败旧内存要释放 return 1; } arr = tmp; // 成功再赋值 // 给新增元素赋值,realloc新增内存是随机值,不会清零 for (int i = 4; i < 7; i++) { arr[i] = i + 1; } printf("realloc扩容后:"); for (int i = 0; i < 7; i++) { printf("%d ", arr[i]); } printf("\n"); // 3.calloc 分配3个int,自动全部初始化为0 int* p = (int*)calloc(3, sizeof(int)); if (p == NULL) { perror("calloc fail"); free(arr); return 1; } printf("calloc自动清零:"); for (int i = 0; i < 3; i++) { printf("%d ", p[i]); } printf("\n"); // 释放堆内存 free(arr); arr = NULL; // 置空,防止野指针 free(p); p = NULL; return 0; }注意:
malloc/calloc/realloc返回值一定要判断是否为NULLrealloc使用临时变量接收返回值- 堆内存用完必须
free,free后指针置为NULL malloc随机值,calloc全部清零,realloc新增部分随机- 栈数组不需要 free;不要 free 栈变量,不要重复 free
5.文件操作
(1)核心 FILE 指针:
FILE* fp;
FILE是结构体,代表打开的一个文件;所有文件操作都靠FILE*指针完成。
文件操作:程序和磁盘上的文件进行数据读写。 程序运行在内存,数据临时存在内存;程序结束内存数据就消失。 把数据保存到硬盘的文件里,下次运行程序还能读出来,这个读写文件的过程就是文件操作
(2)文件操作符号
5.2.1 fopen 打开文件
FILE *fopen(const char *filename, const char *mode);- filename:文件名(可以相对路径 / 绝对路径)
- mode:打开模式
- 返回值:成功返回 FILE*;失败返回 NULL,一定要判空,可以用 perror 打印错误
打开模式:
| 模式 | 含义 | 文件不存在 | 文件存在 |
| “w" | 写文本 | 创建新文件 | 清空原文件内容 |
| "r" | 读文本 | 打开失败 | 读取原有内容 |
| "a" | 追加文本 | 创建新文件 | 在文件末尾追加,不覆盖旧内容 |
| "w+" | 读写 | 创建 | 清空原有内容 |
| "r+" | 读写 | 失败 | 可读可写 |
| "a+" | 读+追加 | 创建 | 末尾追加,可读 |
带b:二进制文件 "rb" "wb" "ab",Windows 处理图片、压缩包必须加 b;Linux 下 b 可忽略。
5.2.2 fclose 关闭文件
int fclose(FILE *fp);用完必须 fclose,缓冲区数据才会真正写入磁盘,防止数据丢失、资源泄漏。
fclose 之后 fp 变成野指针。
5.2.3 读写函数(文本文件)
fgetc(fp)读一个字符,读到文件末尾返回EOF(-1)fputc(ch, fp)写一个字符fgets(buf, size, fp)读取一行字符串fputs(str, fp)写字符串fprintf(fp, "格式",变量)格式化写入文件,类似 printffscanf(fp, "格式",变量地址)从文件格式化读取,类似 scanf
// fread:读 size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fp); // fwrite:写 size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fp);ptr:存放数据缓冲区
size:单个元素字节大小
nmemb:要读写多少个元素
返回值:实际读到 / 写出的元素个数,不是字节数
#include <stdio.h> int main(void) { // 1.打开,写模式"w" FILE* fp = fopen("test.txt", "w"); if (fp == NULL) { perror("fopen w"); return 1; } // 写入 fprintf(fp, "hello c文件操作\n"); fputs("第二行文字\n", fp); fclose(fp); //写完关闭 // 2.重新打开读 "r" fp = fopen("test.txt", "r"); if (fp == NULL) { perror("fopen r"); return 1; } char buf[100]; //循环读一行 while (fgets(buf, sizeof(buf), fp) != NULL) { printf("%s", buf); } fclose(fp); fp = NULL; return 0; }5.2.4二进制读写结构体
#include <stdio.h> struct Student { int id; char name[20]; }; int main(void) { struct Student s1 = {1001, "zhangsan"}; struct Student s2; // 二进制写 wb FILE* fp = fopen("stu.dat", "wb"); if(fp == NULL) { perror("fopen wb"); return 1; } // 将s1写入文件 fwrite(&s1, sizeof(struct Student), 1, fp); fclose(fp); // 二进制读 rb fp = fopen("stu.dat", "rb"); if(fp == NULL) { perror("fopen rb"); return 1; } fread(&s2, sizeof(struct Student),1, fp); printf("id=%d name=%s\n", s2.id, s2.name); fclose(fp); fp = NULL; return 0; }5.2.5预定义标准流
stdin与标准输入流关联的FILE*类型表达式
stdout与标准输出流关联的FILE*类型表达式
stderr与标准错误输出流关联的FILE*类型表达式
5.2.6 文件位置指针:fseek、ftell、rewind
int fseek(FILE *fp, long offset, int whence); // whence: // SEEK_SET 文件开头 // SEEK_CUR 当前位置 // SEEK_END 文件末尾 long ftell(FILE *fp); // 获取当前位置偏移字节 rewind(fp); // 文件指针回到开头#include <stdio.h> int main(void) { FILE* fp = fopen("test.txt", "wb+"); if (fp == NULL) { perror("fopen"); return 1; } fputs("ABCDEFGHIJ", fp); rewind(fp); //回到开头 printf("当前位置:%ld\n", ftell(fp)); //0 fseek(fp, 3, SEEK_SET); //跳到第3个字节 printf("当前位置:%ld\n", ftell(fp)); //3 fseek(fp, -2, SEEK_CUR); //往前回退2字节 printf("当前位置:%ld\n", ftell(fp)); //1 fseek(fp, 0, SEEK_END); printf("文件总字节:%ld\n", ftell(fp)); rewind(fp); fclose(fp); fp = NULL; return 0; }注意:
ftell返回long,不能用 int 接收。计算文件大小套路:
fseek(fp,0,SEEK_END); ftell(fp);,算完一定要rewind/fseek回到开头,否则读取是从末尾读,读不到内容。fseek(fp,0,SEEK_END)只是移动指针,不会增加文件内容。文本模式下尽量少用 fseek 负数偏移,二进制文件(rb/wb)才适合随机定位。
(3)memset /memcpy/memmove /memcmp 内存函数
5.3.1 memset 内存设置(内存初始化)
void *memset(void *s, int c, size_t n);s:要填充的内存起始地址
c:要设置的字节值(0‑255)
n:填充多少字节
返回值:返回 s 的起始地址
注意:memset 是按字节赋值!不要用来给 int 数组设为 1!
手搓:
void* my_memset(void* s, int c, size_t n) { unsigned char* p = (unsigned char*)s; for(size_t i = 0; i < n; i++) { p[i] = (unsigned char)c; } return s; }5.3.2 memcpy 内存拷贝(不处理内存重叠)
void *memcpy(void *dest, const void *src, size_t n);dest:目标地址
src:源地址
n:拷贝字节数
功能:从 src 拷贝 n 字节到 dest
注意:如果 src 和 dest 内存区域重叠,结果未定义,不能用 memcpy!
手搓:
void My_memcpy(void* arr, const void* brr, size_t len) { char* p = (char*)arr; const char* q = (const char*)brr; for (int i = 0;i < len;i++) { p[i] = q[i]; } } void My_memcpy_plus(void* arr, const void* brr, size_t len) { char* p = (char*)arr; const char* q = (const char*)brr; if (p > q) { for (int i = len - 1;i >= 0;i++) { p[i] = q[i]; } } else { for (int i = 0;i < len;i++) { p[i] = q[i]; } } }5.3.3 memmove 安全内存拷贝(支持内存重叠)
void *memmove(void *dest, const void *src, size_t n);功能和 memcpy 几乎一样。
区别:允许源和目标内存重叠,内部做了判断,安全。
手搓:
void* my_memmove(void* dest, const void* src, size_t n) { unsigned char* d = (unsigned char*)dest; const unsigned char* s = (const unsigned char*)src; // 没有重叠 或者 dest 在src前面:从前向后复制 if(d <= s || d >= s + n) { for(size_t i = 0; i < n; i++) { d[i] = s[i]; } } else { // dest落在src区间内,从后往前拷贝 for(size_t i = n; i > 0; i--) { d[i-1] = s[i-1]; } } return dest; }5.3.4 memcmp 内存比较(按字节对比)
int memcmp(const void *s1, const void *s2, size_t n);比较两块内存的前 n 个字节:
s1 > s2返回大于 0s1 < s2返回小于 0- 完全相等返回0
注意:和 strcmp 区别:strcmp 遇到'\0'就停止;memcmp不会停,严格比较 n 个字节。
手搓:
int my_memcmp(const void* s1, const void* s2, size_t n) { const unsigned char* p1 = (const unsigned char*)s1; const unsigned char* p2 = (const unsigned char*)s2; for(size_t i = 0; i < n; i++) { if(p1[i] != p2[i]) { return p1[i] - p2[i]; } } return 0; }