二、高级C语言

二、高级C语言

1.内存存储

(1)整型强转

计算机只能计算加法,因为有负数的存在,所以我们在保存的数值时,会相应的给数值提供一位符号位,其他为数值位---------->原反补码规则,所有的数据在计算机中的存储都以补码的形式存在------>可以解决加减法的问题

正数的原反补码都相同

负数的原码------------->反码:符号位不变其余为取反 补码:反码+1;

例子:

// ==========1. 原反补码演示==========例:

小字节→大字节:符号扩展:有符号数补符号位;无符号数高位补 0。

大字节→小字节:直接截断低位,丢弃高位。

类型字节格式符取值范围位说明
char1%hhd‑128 ~ 1271 符号位 + 7 数值位
short2%hd‑32768 ~ 327671 符号位 + 15 数值位
int4%d‑2³¹ ~ 2³¹‑11 符号位 + 31 数值位
long long8%lld‑2⁶³ ~ 2⁶³‑11 符号位 + 63 数值位
unsigned char1%hhu0 ~ 2558 位全数值位
unsigned short2%hu0 ~ 6553516 位全数值位
unsigned int4%u0 ~ 2³²‑132 位全数值位
unsigned long long8%llu0 ~ 2⁶⁴‑164 位全数值位

// ==========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", &amp;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*)(&amp;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):用于动态内存分配。程序员通过malloccallocrealloc等函数手动申请堆内存,并通过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; }

注意:

  1. malloc/calloc/realloc返回值一定要判断是否为NULL
  2. realloc使用临时变量接收返回值
  3. 堆内存用完必须freefree后指针置为NULL
  4. malloc随机值,calloc全部清零,realloc新增部分随机
  5. 栈数组不需要 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 读写函数(文本文件)
  1. fgetc(fp)读一个字符,读到文件末尾返回EOF(-1)
  2. fputc(ch, fp)写一个字符
  3. fgets(buf, size, fp)读取一行字符串
  4. fputs(str, fp)写字符串
  5. fprintf(fp, "格式",变量)格式化写入文件,类似 printf
  6. fscanf(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; }

注意:

  1. ftell返回long,不能用 int 接收。

  2. 计算文件大小套路:fseek(fp,0,SEEK_END); ftell(fp);,算完一定要rewind/fseek回到开头,否则读取是从末尾读,读不到内容。

  3. fseek(fp,0,SEEK_END)只是移动指针,不会增加文件内容

  4. 文本模式下尽量少用 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返回大于 0
  • s1 < 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; }
5.3.5 测试