资讯中心

二、高级C语言

📅 2026/8/12 18:46:41
二、高级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尾数规格化1M2隐藏整数部分的1内存只存小数部分E真实指数存储时要加偏移量float---4Byte---32bit S(1) E(8) M(23)偏移量127指数值真实值127double---8Byte---64bitS(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) 给不同的字节进行不同编号 计算机在申请使用时可以更方便的操作每一个Bytex64下 有64根地址线产生的0 -(1) 给不同的字节进行不同编号 计算机在申请使用时可以更方便的操作每一个Byte1解引用解引用 *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地址 1short*1地址 2int*1地址 4long 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(a0) %d\n, sizeof(a 0)); //a0 不是sizeof直接包裹数组名数组**退化**为 int*首元素地址 //a0 等价 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 40arr 退化类型 int**arr1 偏移 4 字节一个指针大小。数组指针int (*brr)[10]brr 先和*结合是指针指向int[10]整个数组sizeof(brr) 4*brr等价一维数组名int[10]brr1 偏移 10×440 字节一整行。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(*(*(arr1)3) %d\n, *(*(arr 1) 3)); //arr1: 01*sizeof(int *[5])20---int [5] 3--203*432 int 32 33 34 35 ----9 // 2. 行指针偏移演示 int (*p_row)[5] arr; printf(arr地址%p\n, arr); printf(arr1地址%p 相差%d字节\n, arr 1, (char*)(arr 1) - (char*)arr); //arr1:04*520 // 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结构体变量的内存大小规则结构体整体对齐数 所有成员中最大基础类型字节每个成员偏移必须是自身大小整数倍不足填充空白字节内存填充 paddingstruct 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{mon1,tues2,wed3,thurs4,fri5,sat6,sun7};//可以给每一个名字都指定一个值enum week{mon1,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 /free1什么是动态内存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使用临时变量接收返回值堆内存用完必须freefree后指针置为NULLmalloc随机值calloc全部清零realloc新增部分随机栈数组不需要 free不要 free 栈变量不要重复 free5.文件操作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 abWindows 处理图片、压缩包必须加 bLinux 下 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、rewindint 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才适合随机定位。3memset /memcpy/memmove /memcmp 内存函数5.3.1 memset 内存设置内存初始化void *memset(void *s, int c, size_t n);s要填充的内存起始地址c要设置的字节值0‑255n填充多少字节返回值返回 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; }5.3.5 测试