c++基础重补-Day03

Struct/Union/Enum

struct 结构体(最常用)

作用:把多种不同类型的数据打包成一个整体,成员互相独立,各自占用内存。

特点

  1. 每个成员都有独立内存空间,互不覆盖;

  2. 总占用内存 ≥ 所有成员大小之和(存在内存对齐填充);

  3. 可同时读写所有成员。

    struct Student {
    int id;
    char name[20];
    float score;
    };
    struct Student s;
    s.id = 1001;

union 共用体(你觉得很少用,但底层开发高频)

作用:所有成员共享同一块内存,同一时刻只能存其中一个成员。

核心特点

  1. 内存大小 = 最大成员的字节大小,全部成员重叠覆盖;

  2. 赋值一个成员,其他成员数据直接被破坏;

  3. 同一时间只能有效使用一个成员。

复制代码
union Data {
    int num;
    char ch;
};
union Data u;
u.num = 65;
printf("%c", u.ch); // 输出'A',同一块内存解读成不同类型

union 实际用途(为什么存在)

  1. 类型转换:一块二进制数据,时而按 int 读、时而按字节读;

  2. 节省内存:两种数据永远不会同时使用,共用空间;

  3. 底层驱动、网络协议、寄存器解析必备。

enum 枚举

作用:给一组固定常量起有意义的名字,替代魔术数字,可读性更高。

特点

  1. 默认从 0 开始自动递增赋值,可手动指定数值;

  2. 本质是 int 类型常量。

复制代码
enum Color {
    RED,    // 0
    GREEN,  // 1
    BLUE=5  // 手动设为5
};
enum Color c = RED;

适用:状态、选项、错误码、指令类型等有限固定集合。
三者一句话区分

  1. struct:多个成员同时共存,各占各内存;

  2. union:多个成员互斥共存,共用一块内存;

  3. enum:一组固定数字常量,提升代码可读性。

踩坑记录

BookLib 结构体录入打印综合笔记(全踩坑汇总 + 规范代码)

一、结构体 typedef 语法要点

  1. 完整规范写法

c

运行

复制代码
typedef struct BookLib
{
    char *name;
    char *author;
    int year;
    float price;
} BookLib; // 末尾必须写别名,否则不能直接 BookLib b;
  1. 区分两种写法
  • typedef:末尾标识符是类型别名,不会创建全局变量;

  • 不带typedefstruct BookLib {} b;末尾是全局变量,无简写类型;

  1. 全局变量 BookLib b; 存放在 BSS 段,内部char*指针初始值为NULL空指针。

二、指针、栈、堆核心概念(高频混淆点)

  1. 两层地址区分
  • 指针变量自身存储位置:局部指针在 ,结构体成员指针在BSS 全局区

  • 指针存储的值:malloc/calloc返回堆内存地址

  • b.name = temp_name:仅复制堆地址数值,两块指针共用同一块堆内存(浅拷贝)。

  1. free 规则:free只回收堆内存,栈局部变量自动销毁,不能 free 栈变量。

三、内存分配 calloc/malloc 踩坑

  1. calloc(元素数量, 单个字节大小),第一个参数不能写 0:calloc(0,60)分配 0 字节,返回无效指针,scanf 写入直接段错误;

  2. 字符串分配长度多预留 1 字节存\0结束符;

  3. 安全 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:直接赋值浅拷贝(代码简洁,一次分配)

适用:不需要保留临时输入缓冲区,仅结构体保存数据 流程:

  1. char *temp = calloc(60, sizeof(char)); 临时堆接收输入

  2. b.name = temp; 直接赋值,共享同一块堆

  3. 中途禁止 free (temp),否则结构体指针变为悬垂指针

  4. 全部使用完毕后,统一 free 结构体成员释放堆

  5. 临时局部指针 temp 是栈变量,函数结束自动销毁,无需手动释放

分支 B:深拷贝(两块独立堆,数据互不干扰)

适用:需要同时保留临时缓冲区与结构体两份字符串 流程:

  1. 临时 temp 分配堆接收输入

  2. 结构体成员单独再 calloc 一块全新堆

  3. strcpy(b.name, temp)拷贝字符内容

  4. 临时 temp 可立刻 free,不影响结构体数据

  5. 程序末尾再释放结构体的堆内存

五、scanf /printf 格式符全部坑点

scanf

  1. scanf("%d\n", &num)格式串末尾不能加\n/ 空格,会持续等待额外输入,程序卡死;

  2. 读取浮点数只能写%f,禁止%0.2f,scanf 不识别小数精度,读取错乱;

  3. 普通变量加&,数组 /char * 指针不加&

printf

  1. %s只能打印字符串地址,int 用%d、float 用%.2f

  2. %0.2f输出前导 0 无意义,标准写法%.2f

  3. 访问悬垂指针 / NULL 指针用 % s 打印,直接段错误。

六、悬垂指针产生原因 & 规避

  1. 成因:堆内存 free 后,仍有指针保存该已释放地址;

c

运行

复制代码
// 错误示范
b.name = temp_name;
free(temp_name); // 堆释放,b.name变成悬垂指针
funcPrint();     // 访问b.name直接崩溃
  1. 规避方法
  • 浅拷贝方案:全程不 free 临时 temp,末尾统一释放结构体指针;

  • 深拷贝方案:两份独立堆,释放临时 temp 不影响结构体;

  • 销毁函数释放后手动置空指针b.name = NULL;,防止重复 free。

七、内存泄漏解决方案

  1. 泄漏根源:malloc/calloc申请的堆内存,程序退出前未执行 free;

  2. 规范操作:封装统一销毁函数,集中释放所有结构体堆指针;

c

运行

复制代码
void destroy(){
    if(b.name != NULL){
        free(b.name);
        b.name = NULL;
    }
    if(b.author != NULL){
        free(b.author);
        b.author = NULL;
    }
}
  1. 执行时机:所有打印、逻辑执行完成,return 0前调用一次;

  2. 短小程序即使不 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;
}

九、终极背诵总结

  1. 结构体 typedef 末尾必须写别名;全局指针默认 NULL;

  2. calloc(0, x)非法,字符串分配预留\0;scanf 加长度限制防溢出;

  3. 指针直接赋值是浅拷贝,共用一块堆,中途不能释放临时指针;

  4. scanf 不能带\n、浮点读取不用精度;printf 格式符和变量类型匹配;

  5. malloc 堆内存必须 free,封装销毁函数统一释放,杜绝内存泄漏;

  6. 提前释放共享堆内存会产生悬垂指针,访问直接段错误;

  7. 简单作业优先结构体内置数组,彻底避开内存管理所有 bug。

enum标准语法(typedef 简化版,推荐)

c

运行

复制代码
typedef enum 枚举名
{
    常量1=数值,
    常量2,
    常量3
} 别名;
  1. 大括号内常量用逗号分隔,不能用分号;

  2. 常量默认从 0 开始依次 + 1,可手动赋值;

  3. typedef 后可直接用别名定义变量,不用写enum

二、核心本质

  1. 枚举只是整型常量别名,底层等价 int;

  2. 不是字典,只有「常量名→数字」单向映射,不能反向查字符串;

  3. 枚举常量是只读,运行时不能修改、增删。

三、变量使用 & 输入

  1. 定义变量:Week week;

  2. 赋值:week = MONDAY; 或直接赋值数字 week = 3;

  3. 输入:scanf("%d", &week); 语法合法,底层兼容 int 规范写法:先用 int 接收,校验范围再赋值给枚举。

四、搭配 switch(最常用场景)

c

运行

复制代码
switch(week)
{
    case MONDAY:
        break;
    default: // 处理非法数字
}

五、想要输出英文名称(模拟字典)

枚举不能直接输出文字,搭配字符串数组映射:

c

运行

复制代码
const char *str[] = {"","MON","TUE"...};
printf("%s", str[week]);

六、使用枚举的好处

  1. 替代魔法数字,代码可读性高;

  2. 限定数据范围,类型更安全;

  3. 统一管理常量,一处修改全局生效;

  4. 适配固定状态场景:菜单、星期、选项。

七、易错点

  1. enum 内部分隔符是逗号,禁止分号;

  2. 不能存字符串、浮点数,仅支持整数;

  3. 未 typedef 时,定义变量必须加enum

  4. 枚举变量可接收任意 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

  1. 操作单位:单字节,不识别 int/double/ 结构体等多字节类型

  2. 参数 c 仅保留低 8 位,每个字节统一填充该值

  3. 安全用法:仅填 0,多字节变量整体为 0;填非 0 会出错 例:memset(int数组,1,长度) → 每个字节 0x01,int 值为 0x01010101≠1

  4. char 单字节类型可任意填充字符

  5. 返回值:目标内存首地址,可丢弃不接收

二、memcpy

  1. 按 n 字节原样拷贝二进制,无视\0,适合数组、结构体、缓冲区复制

  2. 致命限制:源、目标内存不能重叠,重叠会数据覆盖错乱

  3. 速度优于 memmove,无分支判断,CPU 流水线无中断

  4. 返回值:dest 首地址,可直接丢弃

三、memmove

  1. memcpy 安全升级版,自动处理内存重叠

  2. 内部带地址判断分支,CPU 流水线易中断,性能略低

  3. 不确定内存是否重叠时优先使用

  4. 返回值:dest 首地址

四、三者通用点

  1. 返回值均为目标内存指针,语法允许不接收返回值直接调用

  2. 头文件:#include <string.h>

五、配套 sizeof 易错点

  1. 静态数组int a[5]:sizeof (a) = 数组总字节

  2. malloc 堆指针int *p:sizeof (p) 仅得指针大小 (4/8 字节),堆长度需手动保存变量

  3. 数组作函数形参自动退化为指针,sizeof 只能得到指针大小

文件操作 fopen/fclose/fread/fgets/fputs/fwrite/fseek/ftell/rewind/ferror/feof

一、核心概念

  1. 文件流 FILE * 程序和磁盘文件之间的数据通道就是文件流,FILE* fp 保存流信息(缓冲区、读写位置、文件状态)。 打开文件得到 fp,操作全程用 fp,用完必须关闭。

  2. 四大基础动作 打开文件 (fopen) → 读 / 写数据 → 移动读写位置 (fseek/rewind) → 关闭文件 (fclose)

  3. 两类文件

  • 文本文件:txt,换行自动转换,用 fgets/fputs;

  • 二进制文件:图片、结构体、音视频,原样字节存储,用 fread/fwrite。

二、打开 fopen / 关闭 fclose

  1. 函数原型

c

运行

复制代码
FILE *fopen(const char *filename, const char *mode);
int fclose(FILE *stream);
  • fopen:成功返回有效 FILE*;失败返回 NULL(文件不存在 / 权限不足)

  • fclose:释放缓冲区、释放文件资源,打开的文件必须关闭,否则内存泄漏、数据丢失。

  1. 6 种最常用打开模式区分(重点)

只读类 "r"

"r":只读,只能读不能写;文件不存在直接打开失败;读写位置在文件开头。

只写类 "w"

"w":只写,只能写不能读;文件不存在则新建;文件存在直接清空全部内容

追加类 "a"

"a":只写,只能写;文件不存在新建;文件存在不清空,每次写入自动加到文件末尾。

可读可写拓展(带 +)

  1. "r+":可读可写;文件必须存在;不清空文件;读写起始在开头。

  2. "w+":可读可写;不存在新建,存在直接清空全部内容

  3. "a+":可读可写;不存在新建;不清空;写操作永远追加到末尾,读可以随便定位。

补充后缀:加 b 代表二进制,如 "rb" "wb" "ab+",Windows 下区分换行符,Linux 文本 / 二进制无区别。

标准打开模板(必写判空)

c

运行

复制代码
FILE* fp = fopen("test.txt", "r");
if(fp == NULL)
{
    perror("打开文件失败"); // 打印错误原因
    return -1;
}
// 读写操作...
fclose(fp); // 用完关闭

三、文件读取函数(fgets 文本 /fread 二进制)

  1. fgets 读取文本(字符串)

c

运行

复制代码
char buf[128];
// 参数:缓冲区、最大读取字符数、文件指针
char* ret = fgets(buf, 128, fp);

特性:

  1. 一次读一行,读到换行 \n 停止,换行符会存入 buf;

  2. 读到文件末尾返回 NULL;

  3. 适合 txt 日志、文本配置读取。

  4. fread 二进制整块读取(数组 / 结构体)

c

运行

复制代码
// fread(存放缓冲区, 单个元素大小, 元素个数, 文件指针)
struct Stu s;
size_t num = fread(&s, sizeof(struct Stu), 1, fp);

返回值:实际读到的元素个数,小于预期代表读到文件末尾。 适合:结构体、数组、图片等二进制数据,按字节原样读取。

四、文件写入函数(fputs 文本 /fwrite 二进制)

  1. fputs 写入字符串文本

c

运行

复制代码
fputs("hello world\n", fp);

不会自动加换行,需要手动写 \n

  1. fwrite 整块二进制写入

c

运行

复制代码
int arr[5] = {1,2,3,4,5};
fwrite(arr, sizeof(int), 5, fp);

直接把内存数据原样写入磁盘,适合批量结构体、数组存储。

五、文件定位:fseek ftell rewind

读写有一个文件位置指针,标记下一次读写从哪个字节开始。

  1. rewind(fp):直接把位置指针挪到文件开头,等价 fseek (fp,0,SEEK_SET)

  2. ftell(fp):返回当前位置距离文件开头多少字节,返回 long

  3. fseek 灵活跳转

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

  1. feof(fp):判断是否到达文件末尾 注意:读完数据后调用才有效,不能循环开头直接判断。

  2. ferror(fp):判断文件流是否发生读写错误(权限损坏、磁盘故障等)

  3. 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;
}

八、高频踩坑点(重点)

  1. fopen 后不判 NULL,文件不存在直接崩溃;

  2. "w" 模式打开旧文件会清空全部数据,不要误用;

  3. "a" 追加模式,无法修改文件前面内容,写操作永远在末尾;

  4. 二进制结构体不要用 fgets/fputs,会乱码,必须 fread/fwrite;

  5. 打开文件不 fclose,缓冲区数据没落地,文件内容丢失;

  6. feof 不能作为循环第一判断条件,会多读一次无效数据。

  • 纯文本、正常字符串 → fputs
  • 结构体 / 数组 / 二进制 / 含\0的缓冲区 → fwrite
  • 文本文件配 fputs;二进制文件配 fwrite

继承、虚函数、重写、多态

一、继承基础

  1. 基础语法

cpp

运行

复制代码
class 子类 : public 父类 { ... };
  • 只有 public 公有继承是日常开发标准写法。
  1. 三种继承权限(只记常用)

  2. public 公有继承 父类 public 成员 → 子类 public;父类 protected → 子类 protected。

  3. protected/private 继承极少使用,会隐藏父类对外接口。

  4. 访问权限铁则

父类 private 私有成员,子类永远不能直接访问; 两种解决方式:

  1. 父类提供 protected /public 的 getter 函数(符合封装规范)

  2. 父类成员改为 protected(子类可直接读写)

  3. 构造与析构执行顺序

创建子类对象:先调用父类构造函数 → 再调用子类构造 销毁子类对象:先调用子类析构 → 再调用父类析构

  1. 子类构造必须初始化列表调用父类构造

父类有参构造无默认构造时,子类只能在初始化列表调用父类构造,函数体内调用会编译报错:

cpp

运行

复制代码
Graduate(string n,int a):Student(n,a){} // 正确
  1. 初始化列表细节

成员(参数) 初始化成员,比函数体内赋值效率更高; 初始化顺序由类内成员定义顺序决定,和列表书写顺序无关。

二、虚函数(重写前置条件)

  1. 普通虚函数定义

父类函数加 virtual,带函数体,有默认实现:

cpp

运行

复制代码
virtual void display() { ... }
  1. 重写条件(函数完全匹配)

子类重写虚函数必须满足:函数名、返回值、参数列表完全一致。

  1. override 关键字

写在子类函数末尾,作用:编译器校验是否正确重写父类虚函数,写错直接报错,避免隐形 bug。

cpp

运行

复制代码
void display() override { ... }
  1. 虚析构(高频大坑)

场景:父类指针指向 new 出来的子类对象,delete 释放

  • 父类析构不加 virtual:只调用父类析构,子类资源不释放,内存泄漏

  • 规范:只要类内有任意虚函数,父类析构必须写成虚析构

cpp

运行

复制代码
virtual ~Student(){}

三、重写 & 运行时多态

  1. 实现多态三大硬性条件(缺一不可)

  2. public 公有继承

  3. 父类存在 virtual 虚函数,子类完成重写

  4. 使用父类指针 / 父类引用接收子类对象调用函数

  5. 无 virtual 的同名函数(隐藏,不是重写)

父类不加 virtual,子类写同名函数叫函数隐藏,不存在多态: 父类指针调用时,永远执行父类函数,不会走子类逻辑。

  1. 子类内部调用父类被重写的函数

作用域限定 父类名::函数名()

cpp

运行

复制代码
void display() override
{
    Student::display(); // 调用父类原有逻辑
    cout << "子类新增信息" << endl;
}
  1. 区分两种调用场景

  2. 直接创建子类对象 Graduate g; g.display(); 不依赖 virtual,直接调用子类,不涉及多态。

  3. 父类指针 / 引用接收子类,才会触发运行时多态。

四、纯虚函数 & 抽象类

1. 纯虚函数语法

无函数体,末尾 =0 标记,不代表赋值,是语法标识:

cpp

运行

复制代码
virtual void display() = 0;
  1. 抽象类规则

  2. 包含至少一个纯虚函数的类 = 抽象类

  3. 抽象类不能直接创建对象,编译报错

  4. 子类继承抽象类,必须重写所有纯虚函数;有一个没重写,子类依旧是抽象类,无法实例化

  5. 纯虚函数作用

定义统一接口规范,父类不提供实现,强制所有子类实现专属逻辑。

五、全套坑点整理(我们聊过的所有问题)

  1. 父类成员是 private,子类直接访问变量 → 编译报错,要用 getter 或 protected

  2. 构造函数类内写实现,类外重复写同一构造 → 重定义编译错误

  3. 子类构造不在初始化列表调用父类有参构造 → 编译失败

  4. 父类析构无 virtual,父类指针 delete 子类 → 内存泄漏

  5. 重写虚函数时参数 / 名字写错,不加 override → 编译器不提醒,无多态,隐性 bug

  6. 只用子类对象调用函数,误以为是多态;多态必须父类指针 / 引用

  7. 纯虚函数类尝试创建对象;子类不实现全部纯虚函数还想创建对象 → 报错

  8. 不加 virtual 写同名函数,以为会多态,实际只是函数隐藏

  9. 初始化列表误以为按书写顺序初始化成员,实际按类内定义顺序

  10. 忘记公有继承,用 private/protected 继承,无法实现多态