Struct/Union/Enum
struct 结构体(最常用)
作用:把多种不同类型的数据打包成一个整体,成员互相独立,各自占用内存。
特点
-
每个成员都有独立内存空间,互不覆盖;
-
总占用内存 ≥ 所有成员大小之和(存在内存对齐填充);
-
可同时读写所有成员。
struct Student {
int id;
char name[20];
float score;
};
struct Student s;
s.id = 1001;
union 共用体(你觉得很少用,但底层开发高频)
作用:所有成员共享同一块内存,同一时刻只能存其中一个成员。
核心特点
内存大小 = 最大成员的字节大小,全部成员重叠覆盖;
赋值一个成员,其他成员数据直接被破坏;
同一时间只能有效使用一个成员。
union Data {
int num;
char ch;
};
union Data u;
u.num = 65;
printf("%c", u.ch); // 输出'A',同一块内存解读成不同类型
union 实际用途(为什么存在)
类型转换:一块二进制数据,时而按 int 读、时而按字节读;
节省内存:两种数据永远不会同时使用,共用空间;
底层驱动、网络协议、寄存器解析必备。
enum 枚举
作用:给一组固定常量起有意义的名字,替代魔术数字,可读性更高。
特点
默认从 0 开始自动递增赋值,可手动指定数值;
本质是 int 类型常量。
enum Color {
RED, // 0
GREEN, // 1
BLUE=5 // 手动设为5
};
enum Color c = RED;
适用:状态、选项、错误码、指令类型等有限固定集合。
三者一句话区分
struct:多个成员同时共存,各占各内存;
union:多个成员互斥共存,共用一块内存;
enum:一组固定数字常量,提升代码可读性。
踩坑记录
BookLib 结构体录入打印综合笔记(全踩坑汇总 + 规范代码)
一、结构体 typedef 语法要点
- 完整规范写法
c
运行
typedef struct BookLib { char *name; char *author; int year; float price; } BookLib; // 末尾必须写别名,否则不能直接 BookLib b;
- 区分两种写法
带
typedef:末尾标识符是类型别名,不会创建全局变量;不带
typedef:struct BookLib {} b;末尾是全局变量,无简写类型;
- 全局变量
BookLib b;存放在 BSS 段,内部char*指针初始值为NULL空指针。二、指针、栈、堆核心概念(高频混淆点)
- 两层地址区分
指针变量自身存储位置:局部指针在栈 ,结构体成员指针在BSS 全局区;
指针存储的值:
malloc/calloc返回堆内存地址;
b.name = temp_name:仅复制堆地址数值,两块指针共用同一块堆内存(浅拷贝)。
- free 规则:
free只回收堆内存,栈局部变量自动销毁,不能 free 栈变量。三、内存分配 calloc/malloc 踩坑
calloc(元素数量, 单个字节大小),第一个参数不能写 0:calloc(0,60)分配 0 字节,返回无效指针,scanf 写入直接段错误;字符串分配长度多预留 1 字节存
\0结束符;安全 scanf 限制长度:
scanf("%59s", buf),防止输入超长堆溢出破坏堆头部,引发后续 IO 崩溃。四、两种存储字符串方案对比
方案 1:结构体内置字符数组(作业最优,无 malloc/free)
c
运行
typedef struct BookLib { char name[60]; char author[20]; int year; float price; };
优点:无堆、无内存泄漏、无悬垂指针、无需手动释放;
使用:栈临时数组接收输入,
strcpy(b.name, temp)拷贝字符串内容;缺点:数组长度固定,内存占用固定。
方案 2:结构体 char * 动态堆(坚持使用 malloc/free)
分支 A:直接赋值浅拷贝(代码简洁,一次分配)
适用:不需要保留临时输入缓冲区,仅结构体保存数据 流程:
char *temp = calloc(60, sizeof(char));临时堆接收输入
b.name = temp;直接赋值,共享同一块堆中途禁止 free (temp),否则结构体指针变为悬垂指针
全部使用完毕后,统一 free 结构体成员释放堆
临时局部指针 temp 是栈变量,函数结束自动销毁,无需手动释放
分支 B:深拷贝(两块独立堆,数据互不干扰)
适用:需要同时保留临时缓冲区与结构体两份字符串 流程:
临时 temp 分配堆接收输入
结构体成员单独再 calloc 一块全新堆
strcpy(b.name, temp)拷贝字符内容临时 temp 可立刻 free,不影响结构体数据
程序末尾再释放结构体的堆内存
五、scanf /printf 格式符全部坑点
scanf
scanf("%d\n", &num)格式串末尾不能加\n/ 空格,会持续等待额外输入,程序卡死;读取浮点数只能写
%f,禁止%0.2f,scanf 不识别小数精度,读取错乱;普通变量加
&,数组 /char * 指针不加&;printf
%s只能打印字符串地址,int 用%d、float 用%.2f;
%0.2f输出前导 0 无意义,标准写法%.2f;访问悬垂指针 / NULL 指针用 % s 打印,直接段错误。
六、悬垂指针产生原因 & 规避
- 成因:堆内存 free 后,仍有指针保存该已释放地址;
c
运行
// 错误示范 b.name = temp_name; free(temp_name); // 堆释放,b.name变成悬垂指针 funcPrint(); // 访问b.name直接崩溃
- 规避方法
浅拷贝方案:全程不 free 临时 temp,末尾统一释放结构体指针;
深拷贝方案:两份独立堆,释放临时 temp 不影响结构体;
销毁函数释放后手动置空指针
b.name = NULL;,防止重复 free。七、内存泄漏解决方案
泄漏根源:
malloc/calloc申请的堆内存,程序退出前未执行 free;规范操作:封装统一销毁函数,集中释放所有结构体堆指针;
c
运行
void destroy(){ if(b.name != NULL){ free(b.name); b.name = NULL; } if(b.author != NULL){ free(b.author); b.author = NULL; } }
执行时机:所有打印、逻辑执行完成,
return 0前调用一次;短小程序即使不 free,系统退出自动回收;长期运行程序必须完整释放。
八、可运行规范代码(动态 char * 直接赋值版)
c
运行
#include <stdio.h> #include <stdlib.h> typedef struct BookLib { char *name; char *author; int year; float price; } BookLib; BookLib b; void destroy(){ if(b.name != NULL){ free(b.name); b.name = NULL; } if(b.author != NULL){ free(b.author); b.author = NULL; } } void funcPrint(){ printf("the book name is: %s\n",b.name); printf("the author name is: %s\n",b.author); printf("the publish year is: %d\n",b.year); printf("the price of the book is: %.2f\n",b.price); } int main(int argc, char const *argv[]) { printf("please enter book name: "); char *b_name = (char*)calloc(60, sizeof(char)); scanf("%59s",b_name); printf("the book name is: %s\n",b_name); b.name = b_name; printf("please enter author name: "); char *b_author = (char*)calloc(10, sizeof(char)); scanf("%9s",b_author); printf("the author name is: %s\n",b_author); b.author = b_author; printf("please enter year: "); int b_year=0; scanf("%d",&b_year); printf("the publish year is: %d\n",b_year); b.year = b_year; printf("please enter price: "); float b_price=0; scanf("%f",&b_price); printf("the price of the book is: %.2f\n",b_price); b.price = b_price; printf("====\n"); funcPrint(); destroy(); return 0; }九、终极背诵总结
结构体 typedef 末尾必须写别名;全局指针默认 NULL;
calloc(0, x)非法,字符串分配预留\0;scanf 加长度限制防溢出;指针直接赋值是浅拷贝,共用一块堆,中途不能释放临时指针;
scanf 不能带
\n、浮点读取不用精度;printf 格式符和变量类型匹配;malloc 堆内存必须 free,封装销毁函数统一释放,杜绝内存泄漏;
提前释放共享堆内存会产生悬垂指针,访问直接段错误;
简单作业优先结构体内置数组,彻底避开内存管理所有 bug。
enum标准语法(typedef 简化版,推荐)
c
运行
typedef enum 枚举名 { 常量1=数值, 常量2, 常量3 } 别名;
大括号内常量用逗号分隔,不能用分号;
常量默认从 0 开始依次 + 1,可手动赋值;
typedef 后可直接用别名定义变量,不用写
enum。二、核心本质
枚举只是整型常量别名,底层等价 int;
不是字典,只有「常量名→数字」单向映射,不能反向查字符串;
枚举常量是只读,运行时不能修改、增删。
三、变量使用 & 输入
定义变量:
Week week;赋值:
week = MONDAY;或直接赋值数字week = 3;输入:
scanf("%d", &week);语法合法,底层兼容 int 规范写法:先用 int 接收,校验范围再赋值给枚举。四、搭配 switch(最常用场景)
c
运行
switch(week) { case MONDAY: break; default: // 处理非法数字 }五、想要输出英文名称(模拟字典)
枚举不能直接输出文字,搭配字符串数组映射:
c
运行
const char *str[] = {"","MON","TUE"...}; printf("%s", str[week]);六、使用枚举的好处
替代魔法数字,代码可读性高;
限定数据范围,类型更安全;
统一管理常量,一处修改全局生效;
适配固定状态场景:菜单、星期、选项。
七、易错点
enum 内部分隔符是逗号,禁止分号;
不能存字符串、浮点数,仅支持整数;
未 typedef 时,定义变量必须加
enum;枚举变量可接收任意 int,但超出定义范围无意义。
memcpy/memset/memmove
void *memcpy(void *dest, const void *src, size_t n);内存拷贝,不支持内存重叠,拷贝 n 字节。void *memset(void *s, int c, size_t n);内存填充,逐字节赋值 c,n 个字节。按照字节填充不是按照数据类型填充,所以没法对一个int 数组memset 1,但是可以对char数组做,因为char是一个字节
void *memmove(void *dest, const void *src, size_t n);安全版 memcpy,支持源、目标内存重叠,内部会做缓冲区中转。三者返回值完全一样
统一返回:
void*,目标内存 dest 的起始指针
memcpy 是从前到后依次拷贝:先复制 src 0 到 dest 0,再 src 1→dest 1...... 当 dest 在 src 前面、区间重叠时: 还没读完后面的 src 数据,前面的 src 字节已经被覆盖,数据丢失。
char buf\[\] = "123456"; // 把 "345" 往前拷贝到开头,重叠 memcpy(buf, buf+2, 3);memmove 开头必须做地址判断逻辑:
if (dest 和 src 重叠) { 倒序拷贝 / 临时缓存拷贝 } else { 顺序拷贝 }多了分支判断、条件跳转,CPU 流水线会被打断;
工程场景区分绝大多数业务场景根本不存在内存重叠:
- 拷贝数组到另一个独立数组
- 拷贝结构体到堆上新分配的内存
- 缓冲区之间互不相交 这种场景 memcpy 更快,没有任何风险,没必要付出 memmove 的性能代价。
**函数特性:按指定字节数复制,无视
\0,二进制原样复制。对比 strcpy:strcpy 只到
\0,不能复制二进制、结构体、int 数组,这是 memcpy 优势。**细节把握
一、memset
操作单位:单字节,不识别 int/double/ 结构体等多字节类型
参数 c 仅保留低 8 位,每个字节统一填充该值
安全用法:仅填 0,多字节变量整体为 0;填非 0 会出错 例:
memset(int数组,1,长度)→ 每个字节 0x01,int 值为 0x01010101≠1char 单字节类型可任意填充字符
返回值:目标内存首地址,可丢弃不接收
二、memcpy
按 n 字节原样拷贝二进制,无视
\0,适合数组、结构体、缓冲区复制致命限制:源、目标内存不能重叠,重叠会数据覆盖错乱
速度优于 memmove,无分支判断,CPU 流水线无中断
返回值:dest 首地址,可直接丢弃
三、memmove
memcpy 安全升级版,自动处理内存重叠
内部带地址判断分支,CPU 流水线易中断,性能略低
不确定内存是否重叠时优先使用
返回值:dest 首地址
四、三者通用点
返回值均为目标内存指针,语法允许不接收返回值直接调用
头文件:#include <string.h>
五、配套 sizeof 易错点
静态数组
int a[5]:sizeof (a) = 数组总字节malloc 堆指针
int *p:sizeof (p) 仅得指针大小 (4/8 字节),堆长度需手动保存变量数组作函数形参自动退化为指针,sizeof 只能得到指针大小
文件操作 fopen/fclose/fread/fgets/fputs/fwrite/fseek/ftell/rewind/ferror/feof
一、核心概念
文件流 FILE * 程序和磁盘文件之间的数据通道就是文件流,
FILE* fp保存流信息(缓冲区、读写位置、文件状态)。 打开文件得到 fp,操作全程用 fp,用完必须关闭。四大基础动作 打开文件 (fopen) → 读 / 写数据 → 移动读写位置 (fseek/rewind) → 关闭文件 (fclose)
两类文件
文本文件:txt,换行自动转换,用 fgets/fputs;
二进制文件:图片、结构体、音视频,原样字节存储,用 fread/fwrite。
二、打开 fopen / 关闭 fclose
- 函数原型
c
运行
FILE *fopen(const char *filename, const char *mode); int fclose(FILE *stream);
fopen:成功返回有效 FILE*;失败返回 NULL(文件不存在 / 权限不足)
fclose:释放缓冲区、释放文件资源,打开的文件必须关闭,否则内存泄漏、数据丢失。
- 6 种最常用打开模式区分(重点)
只读类 "r"
"r":只读,只能读不能写;文件不存在直接打开失败;读写位置在文件开头。只写类 "w"
"w":只写,只能写不能读;文件不存在则新建;文件存在直接清空全部内容。追加类 "a"
"a":只写,只能写;文件不存在新建;文件存在不清空,每次写入自动加到文件末尾。可读可写拓展(带 +)
"r+":可读可写;文件必须存在;不清空文件;读写起始在开头。
"w+":可读可写;不存在新建,存在直接清空全部内容。
"a+":可读可写;不存在新建;不清空;写操作永远追加到末尾,读可以随便定位。补充后缀:加
b代表二进制,如"rb" "wb" "ab+",Windows 下区分换行符,Linux 文本 / 二进制无区别。标准打开模板(必写判空)
c
运行
FILE* fp = fopen("test.txt", "r"); if(fp == NULL) { perror("打开文件失败"); // 打印错误原因 return -1; } // 读写操作... fclose(fp); // 用完关闭三、文件读取函数(fgets 文本 /fread 二进制)
- fgets 读取文本(字符串)
c
运行
char buf[128]; // 参数:缓冲区、最大读取字符数、文件指针 char* ret = fgets(buf, 128, fp);特性:
一次读一行,读到换行
\n停止,换行符会存入 buf;读到文件末尾返回 NULL;
适合 txt 日志、文本配置读取。
fread 二进制整块读取(数组 / 结构体)
c
运行
// fread(存放缓冲区, 单个元素大小, 元素个数, 文件指针) struct Stu s; size_t num = fread(&s, sizeof(struct Stu), 1, fp);返回值:实际读到的元素个数,小于预期代表读到文件末尾。 适合:结构体、数组、图片等二进制数据,按字节原样读取。
四、文件写入函数(fputs 文本 /fwrite 二进制)
- fputs 写入字符串文本
c
运行
fputs("hello world\n", fp);不会自动加换行,需要手动写
\n。
- fwrite 整块二进制写入
c
运行
int arr[5] = {1,2,3,4,5}; fwrite(arr, sizeof(int), 5, fp);直接把内存数据原样写入磁盘,适合批量结构体、数组存储。
五、文件定位:fseek ftell rewind
读写有一个文件位置指针,标记下一次读写从哪个字节开始。
rewind(fp):直接把位置指针挪到文件开头,等价 fseek (fp,0,SEEK_SET)
ftell(fp):返回当前位置距离文件开头多少字节,返回 longfseek 灵活跳转
c
运行
int fseek(FILE* fp, long offset, int origin);origin 三种基准:
SEEK_SET:文件开头,offset 只能≥0
SEEK_CUR:当前位置,offset 可正负
SEEK_END:文件末尾,offset 负数向前偏移
示例:跳到文件末尾倒数 10 字节
fseek(fp, -10, SEEK_END);六、错误与文件末尾判断 feof /ferror
feof(fp):判断是否到达文件末尾 注意:读完数据后调用才有效,不能循环开头直接判断。
ferror(fp):判断文件流是否发生读写错误(权限损坏、磁盘故障等)
perror("提示文字"):自动打印系统错误原因,调试必备七、简单实操示例
示例 1:文本读写
c
运行
#include <stdio.h> int main() { // 写入 FILE* fp = fopen("test.txt", "w"); fputs("第一行文字\n第二行", fp); fclose(fp); // 读取 fp = fopen("test.txt", "r"); char buf[100]; while(fgets(buf, 100, fp) != NULL) { printf("%s", buf); } fclose(fp); return 0; }示例 2:二进制结构体读写
c
运行
#include <stdio.h> struct Stu { char name[20]; int age; }; int main() { struct Stu s1 = {"小明", 18}; // 写入 FILE* fp = fopen("stu.dat", "wb"); fwrite(&s1, sizeof(struct Stu), 1, fp); fclose(fp); // 读取 struct Stu s2; fp = fopen("stu.dat", "rb"); fread(&s2, sizeof(struct Stu), 1, fp); printf("%s %d", s2.name, s2.age); fclose(fp); return 0; }八、高频踩坑点(重点)
fopen 后不判 NULL,文件不存在直接崩溃;
"w"模式打开旧文件会清空全部数据,不要误用;
"a"追加模式,无法修改文件前面内容,写操作永远在末尾;二进制结构体不要用 fgets/fputs,会乱码,必须 fread/fwrite;
打开文件不 fclose,缓冲区数据没落地,文件内容丢失;
feof 不能作为循环第一判断条件,会多读一次无效数据。
- 纯文本、正常字符串 → fputs
- 结构体 / 数组 / 二进制 / 含
\0的缓冲区 → fwrite- 文本文件配 fputs;二进制文件配 fwrite
继承、虚函数、重写、多态
一、继承基础
- 基础语法
cpp
运行
class 子类 : public 父类 { ... };
- 只有
public公有继承是日常开发标准写法。
三种继承权限(只记常用)
public公有继承 父类 public 成员 → 子类 public;父类 protected → 子类 protected。protected/private 继承极少使用,会隐藏父类对外接口。
访问权限铁则
父类
private私有成员,子类永远不能直接访问; 两种解决方式:
父类提供 protected /public 的 getter 函数(符合封装规范)
父类成员改为 protected(子类可直接读写)
构造与析构执行顺序
创建子类对象:先调用父类构造函数 → 再调用子类构造 销毁子类对象:先调用子类析构 → 再调用父类析构
- 子类构造必须初始化列表调用父类构造
父类有参构造无默认构造时,子类只能在初始化列表调用父类构造,函数体内调用会编译报错:
cpp
运行
Graduate(string n,int a):Student(n,a){} // 正确
- 初始化列表细节
成员(参数)初始化成员,比函数体内赋值效率更高; 初始化顺序由类内成员定义顺序决定,和列表书写顺序无关。二、虚函数(重写前置条件)
- 普通虚函数定义
父类函数加
virtual,带函数体,有默认实现:cpp
运行
virtual void display() { ... }
- 重写条件(函数完全匹配)
子类重写虚函数必须满足:函数名、返回值、参数列表完全一致。
- override 关键字
写在子类函数末尾,作用:编译器校验是否正确重写父类虚函数,写错直接报错,避免隐形 bug。
cpp
运行
void display() override { ... }
- 虚析构(高频大坑)
场景:父类指针指向 new 出来的子类对象,delete 释放
父类析构不加 virtual:只调用父类析构,子类资源不释放,内存泄漏
规范:只要类内有任意虚函数,父类析构必须写成虚析构
cpp
运行
virtual ~Student(){}三、重写 & 运行时多态
实现多态三大硬性条件(缺一不可)
public 公有继承
父类存在 virtual 虚函数,子类完成重写
使用父类指针 / 父类引用接收子类对象调用函数
无 virtual 的同名函数(隐藏,不是重写)
父类不加 virtual,子类写同名函数叫函数隐藏,不存在多态: 父类指针调用时,永远执行父类函数,不会走子类逻辑。
- 子类内部调用父类被重写的函数
作用域限定
父类名::函数名()cpp
运行
void display() override { Student::display(); // 调用父类原有逻辑 cout << "子类新增信息" << endl; }
区分两种调用场景
直接创建子类对象
Graduate g; g.display();不依赖 virtual,直接调用子类,不涉及多态。父类指针 / 引用接收子类,才会触发运行时多态。
四、纯虚函数 & 抽象类
1. 纯虚函数语法
无函数体,末尾
=0标记,不代表赋值,是语法标识:cpp
运行
virtual void display() = 0;
抽象类规则
包含至少一个纯虚函数的类 = 抽象类
抽象类不能直接创建对象,编译报错
子类继承抽象类,必须重写所有纯虚函数;有一个没重写,子类依旧是抽象类,无法实例化
纯虚函数作用
定义统一接口规范,父类不提供实现,强制所有子类实现专属逻辑。
五、全套坑点整理(我们聊过的所有问题)
父类成员是 private,子类直接访问变量 → 编译报错,要用 getter 或 protected
构造函数类内写实现,类外重复写同一构造 → 重定义编译错误
子类构造不在初始化列表调用父类有参构造 → 编译失败
父类析构无 virtual,父类指针 delete 子类 → 内存泄漏
重写虚函数时参数 / 名字写错,不加 override → 编译器不提醒,无多态,隐性 bug
只用子类对象调用函数,误以为是多态;多态必须父类指针 / 引用
纯虚函数类尝试创建对象;子类不实现全部纯虚函数还想创建对象 → 报错
不加 virtual 写同名函数,以为会多态,实际只是函数隐藏
初始化列表误以为按书写顺序初始化成员,实际按类内定义顺序
忘记公有继承,用 private/protected 继承,无法实现多态