48.总结
48.1 知识点

学习的主要内容

强调


48.2 核心要点速览
函数
函数基础
- 作用:封装代码、提升复用率、抽象行为。
- 声明与定义:标准在头文件(.h)声明、源文件(.cpp)定义;临时可在.cpp中处理,需提前声明。
- 语法:
返回类型 函数名(参数列表) { 逻辑; return 值; },void无返回值,return可提前结束函数。
形参与实参
- 形参:函数内声明的参数(如
int a)。 - 实参:调用时传入的具体值(如
add(5, 10))。 - 值传递:形参是实参拷贝,修改不影响实参。
参数默认值
- 作用:调用时可省略参数,使用默认值。
- 注意:声明可带默认值,定义不可;可选参数需放在普通参数后。
变量作用域
- 局部变量:语句块内声明,块结束后销毁。
- 全局变量:函数外声明,程序结束后销毁,可被全局访问。
- 同名变量:内部作用域屏蔽外部。
函数重载
- 条件:同作用域、同名函数,参数数量/类型/顺序不同(与返回值无关)。
- 注意:可选参数可能导致编译器歧义。
递归函数
- 关键:必须有结束条件(如
if (num>10) return;),函数自调用。
内联函数
- 作用:用
inline声明,编译时将代码插入调用处,减少函数调用开销。 - 限制:递归/大型函数不适用,可能导致代码膨胀。
变量的存储类型
auto变量
- 作用:让编译器自动推断变量类型,减少手动定义。
- 特点:
- 需声明时初始化,否则无法推断类型。
- 优点:代码简洁;
- 缺点:影响可读性,初学阶段不建议频繁使用。
- 示例:
auto i = 10;(编译器推定为int)
static变量
- 作用:生命周期贯穿程序运行,存储全局或局部静态变量。
- 分类:
- 静态局部变量:函数内声明,仅初始化一次,函数返回后不销毁。
- 静态全局变量:文件内作用域,不可跨文件访问(普通全局变量可跨文件)。
- 特点:
- 优点:适合缓存或全局共享数据,仅初始化一次。
- 缺点:持续占用内存,可能增加内存消耗。
register变量
- 作用:建议存储在CPU寄存器中,提高访问速度(现代编译器已自动优化,较新版本C++已废弃)。
- 限制:
- 无法获取地址(无寄存器地址)。
- 现代开发中极少使用,编译器优化更高效。
- 对比:
类型 存储位置 速度 容量限制 寄存器 CPU内部 极快 有限 内存 内存条 较慢 较大
extern变量
- 作用:跨文件共享全局变量或函数,声明时不定义,需在另一文件中定义。
- 注意:
- 不允许跨文件定义同名全局变量,会编译报错。
- 仅声明外部已定义的内容,不能重复定义。
- 示例:
// 文件1.cpp中定义 int globalVar = 10; // 文件2.cpp中声明 extern int globalVar;
数组
一维数组
| 核心要点 | 说明 |
|---|---|
| 基本概念 | 相同类型数据的线性集合,内存连续存储。 |
| 声明方式 | int arr[5];(指定容量)、int arr[] = {1,2,3};(自动推导容量)。 |
| 长度计算 | sizeof(arr) / sizeof(int),获取数组元素个数。 |
| 元素访问 | 索引从0开始,arr[0]为首个元素,越界访问可能返回不确定值。 |
| 初始化规则 | 未指定元素初始化为0,如int arr[3] = {1};,其余元素为0。 |
| 操作限制 | 静态数组容量固定,增删元素需重新分配数组(如复制到新数组)。 |
二维数组
| 核心要点 | 说明 |
|---|---|
| 基本概念 | 行列结构的数据集合,用arr[row][col]访问元素。 |
| 声明方式 | int arr[3][4];(3行4列)、int arr[][4] = {{1,2},{3,4}};(自动推导行数)。 |
| 行列计算 | 行数:sizeof(arr)/sizeof(arr[0]),列数:sizeof(arr[0])/sizeof(int)。 |
| 元素访问 | arr[i][j],i为行索引,j为列索引。 |
| 函数传参 | 需指定列数,如void func(int arr[][4], int rows)。 |
字符数组
| 核心要点 | 说明 |
|---|---|
| 字符串初始化 | 用char str[] = "hello";时,末尾自动添加\0作为结束符。 |
| 乱码原因 | 未初始化或无\0时,打印会读取到随机内存,导致乱码(如“烫烫烫”)。 |
| 手动添加结束符 | char arr[] = {'A','B','\0'};,确保字符串正确结束。 |
| 与字符串区别 | 字符数组是数组,字符串是string类型,后者更灵活(如自动管理长度)。 |
指针基础
内存地址与指针概念
| 核心要点 | 说明 |
|---|---|
| 内存地址 | 内存单元的唯一编号(字节为单位),如int i=2占4字节,地址为连续4个字节的首地址。 |
| 指针定义 | 存储变量地址的变量,通过&获取地址,*解引用获取值。例:int* p=&i,p存i的地址,*p取i的值。 |
指针基础操作
| 操作 | 语法 | 示例 |
|---|---|---|
| 声明 | 类型* 指针名 |
int* ptr; |
| 初始化 | 指针=&变量 |
int a=10; int* p=&a; |
| 解引用 | *指针 |
cout << *p;(输出10) |
| 空指针 | void* p=nullptr; |
不指向任何有效地址,避免野指针。 |
| 野指针 | 未初始化或指向已释放内存的指针,可能导致程序崩溃。 |
指针进阶操作
| 要点 | 细节 | 示例 |
|---|---|---|
| 内存占用 | 64位系统下占8字节(32位占4字节),与类型无关。 | sizeof(int*)=8 |
| &与*混合使用 | &*p:先解引用再取地址(等价于p);*&a:先取地址再解引用(等价于a)。 |
&*ptr == ptr |
| 指针自增减 | 按类型大小跳转:int* ptr自增跳4字节,char*跳1字节。 |
int a=10; int* p=&a; p++后地址+4。 |
| 空类型指针 | void*可指向任意类型,使用时需强转。 |
void* p=&s; short s2=*(short*)p; |
指针与常量
| 类型 | 定义 | 特点 |
|---|---|---|
| 指向常量的指针 | const int* p; |
可改指向,不可改值。p=&j; *p=10报错,p=&i允许。 |
| 指针常量 | int* const p; |
不可改指向,可改值。p=&j报错,*p=20允许。 |
| 指向常量的指针常量 | const int* const p; |
不可改指向和值。p=&j和*p=10均报错。 |
指针和数组
指针与一维数组
| 核心要点 | 说明 |
|---|---|
| 内存关系 | 数组连续存储,指针指向首地址,通过指针增减访问元素。 |
| 建立联系 | - 数组名即首地址:int* p = arr; - 首元素地址: p = &arr[0]; |
| 元素访问 | - 解引用:*p(首元素),*(p+1)(次元素) - 下标语法: p[0]等价于*(p+0) |
| 指针运算 | 自增/减按元素大小偏移(如int*自增跳4字节)。 |
| 数组名限制 | 数组名是固定地址,不能重定向;指针可指向任意合法内存。 |
指针与二维数组
| 核心要点 | 说明 |
|---|---|
| 内存结构 | 二维数组按行连续存储,可视为一维数组的一维数组。 |
| 数组指针 | - 定义:int (*p)[4](指向含4个int的一维数组) - 偏移: p+1跳一行(4×4=16字节) |
| 元素访问 | - 按元素指针:int* p = &arr[0][0]; 指向首个元素,*p访问元素,p+1跳1个int(4字节)。- 按行指针: int (*p)[4] = arr; 指向整行,*(p+i)取第i行首地址,*(*(p+i)+j)取i行j列元素。示例: *(*(p+1)+2)等价于arr[1][2],步骤为:1. p+1:指向第2行(偏移16字节)。2. *(p+1):转换为行内首元素地址(类型int*)。3. *(p+1)+2:地址加2(偏移8字节),指向第2行第3列。4. *(*(p+1)+2):解引用得到元素值。 |
| 二维数组名 | - arr是行指针,*arr是列指针(首元素地址) - sizeof(arr)为整体大小,sizeof(p)为指针大小(8字节) |
指针与字符数组
| 核心要点 | 说明 |
|---|---|
| 字符串初始化 | - char arr[] = "hello"; 自动加\0 - 纯字符数组需手动加 \0,否则打印乱码 |
| 字符指针 | - 指向常量字符串:const char* p = "test"; - 可直接打印,遇 \0结束 |
| 遍历方式 | - while (*p != '\0') { cout << *p++; } - 偏移量控制: while (*(p+i) != '\0') |
| 注意事项 | - 常量字符串不可修改(如"test"[0] = 'a'报错) - 指针未初始化或越界访问导致崩溃 |
指针数组
| 操作 | 语法示例 | 说明 |
|---|---|---|
| 定义与初始化 | int* p[3] = {&a, &b, &c}; |
每个元素存储变量地址,需初始化或后续赋值 |
| 访问值 | cout << *p[0]; |
解引用指针数组元素,获取指向的值 |
| 修改指向 | p[0] = &d; |
改变指针数组元素存储的地址 |
| 修改值 | *p[0] = 99; |
通过指针修改指向变量的值 |
| 遍历 | for (int i=0; i<3; i++) cout << *p[i]; |
遍历每个指针,解引用获取值 |
数组指针 vs 指针数组
| 对比项 | 数组指针 | 指针数组 |
|---|---|---|
| 定义 | 指向一个数组的指针 | 由多个指针组成的数组 |
| 本质 | 指针变量,存储数组的首地址 | 数组变量,元素为指针 |
| 语法 | 类型 (*指针名)[元素个数] |
类型* 数组名[数组长度] |
| 示例 | int (*p)[4];(指向含4个int的数组) |
int* p[4];(含4个int*指针的数组) |
| 存储内容 | 数组的首地址 | 多个变量的地址 |
| 访问元素 | *(*(p+i)+j)(二维数组第i行j列) |
*(p[i])(解引用第i个指针指向的值) |
| 应用场景 | 操作二维数组、矩阵 | 存储多个指针(如函数指针、动态内存) |
多级指针
各级指针对比
| 级别 | 定义 | 语法声明 | 示例 | 访问值 | 应用场景 |
|---|---|---|---|---|---|
| 一级指针 | 指向普通变量的指针 | int* p; |
int a=10; int* p=&a; |
*p |
普通变量地址存储、函数参数传递 |
| 二级指针 | 指向一级指针的指针 | int** pp; |
int* p=&a; int** pp=&p; |
**pp |
指针数组、函数中修改指针指向 |
| 三级指针 | 指向二级指针的指针 | int*** ppp; |
int** pp=&p; int*** ppp=&pp; |
***ppp |
复杂数据结构(如指针的指针数组) |
内存分配
内存存储区域对比
| 区域 | 存储内容 | 生命周期 | 读写属性 | 管理方式 |
|---|---|---|---|---|
| 代码段 | 函数机器码、执行指令 | 程序运行期 | 只读 | 系统自动加载 |
| 数据段 | 全局变量、静态变量 | 程序运行期 | 可读写 | 系统自动管理 |
| 常量段 | 字符串字面量、只读数据 | 程序运行期 | 只读 | 系统自动管理 |
| 栈 | 局部变量、函数参数、返回值 | 函数调用周期 | 可读写 | 编译器自动分配/释放 |
| 堆 | 动态分配的内存(new申请) | 程序员手动控制 | 可读写 | 手动用new分配/delete释放 |
堆内存分配与释放
| 操作 | 语法 | 示例 | 注意事项 |
|---|---|---|---|
| 单个对象分配 | 类型* 指针 = new 类型(初始值); |
int* p = new int(10); |
- 指针变量在栈,指向堆内存 - 必须初始化或赋值 |
| 数组分配 | 类型* 指针 = new 类型[容量]; |
int* arr = new int[5]; |
- 需用delete[]释放- 可聚合初始化: new int[3]{1,2,3} |
| 释放单个对象 | delete 指针; |
delete p; p = nullptr; |
- 释放后指针置为nullptr,避免野指针- 重复释放会崩溃 |
| 释放数组 | delete[] 指针; |
delete[] arr; arr = nullptr; |
- 必须与new[]配对使用- 释放后指针置空 |
内存安全核心要点
| 问题类型 | 原因 | 解决方案 |
|---|---|---|
| 内存泄漏 | 堆内存未用delete释放 |
- 及时调用delete/delete[]- 指针释放后置 nullptr |
| 越界访问 | 数组索引超出范围、野指针操作 | - 用sizeof计算数组长度- 避免访问未分配的内存 |
| 空指针错误 | 访问nullptr或未初始化指针 |
- 声明时置nullptr- 使用前检查 if (p != nullptr) |
| 野指针 | 指向已释放的内存或未分配区域 | - 释放后立即置nullptr- 不 |
引用
引用的基础使用
| 核心要点 | 说明 |
|---|---|
| 定义 | 变量的别名,与原变量共享内存地址,声明时用&,如int& ref = a;。 |
| 初始化 | 必须初始化,如int& ref = a;(不能int& ref;)。 |
| 类型一致性 | 引用类型必须与原变量一致,否则编译报错(如short& ref = intVar会报错)。 |
| 绑定限制 | 绑定后不能更改指向,如ref = b;是修改a的值而非重新绑定。 |
| 内存占用 | 不占用新内存,与原变量共用同一内存地址。 |
左值引用 vs 右值引用
| 类别 | 左值引用(&) |
右值引用(&&) |
|---|---|---|
| 语法 | int& ref = a;(绑定左值变量) |
int&& ref = 10;(绑定右值临时对象) |
| 绑定对象 | 持久化对象(如变量、数组、指针) | 临时对象(如字面量10、函数返回值) |
| 生命周期 | 与原变量一致,随作用域结束销毁 | 延长右值生命周期至引用作用域结束 |
| 可修改性 | 可修改原变量的值(如ref = 20修改a) |
可修改绑定的临时对象(如ref = 20修改临时值) |
| 应用场景 | 函数参数传递(避免拷贝)、变量别名 | 移动语义(避免临时对象拷贝)、右值优化 |
引用与函数
| 场景 | 说明 |
|---|---|
| 左值引用参数 | - 函数参数为int& value,可直接修改实参,避免值拷贝开销。- 例: void swap(int& a, int& b)交换两个变量的值。 |
| 右值引用参数 | - 函数参数为int&& value,绑定右值临时对象,优化临时值操作。- 例: void process(int&& tmp)处理临时计算结果。 |
| 常量引用参数 | - 函数参数为const int& value,防止修改实参,常用于只读场景。- 例: void print(const string& str)打印字符串,不修改原数据。 |
| 函数返回引用 | - 不能返回局部变量的引用(局部变量销毁后引用无效)。 - 例: int& getRef(int& d) { return d; }返回传入变量的引用,修改返回值会影响原变量。 |
枚举
| 对比项 | 普通枚举 | 强类型枚举(enum class) |
|---|---|---|
| 声明语法 | enum E_Name { A, B, C }; |
enum class E_Name { A, B, C }; |
| 命名空间 | 枚举项直接暴露在全局作用域 | 枚举项属于枚举类作用域(如E_Name::A) |
| 类型安全 | 无类型安全,可与整数混用 | 严格类型安全,禁止与整数隐式转换 |
| 隐式转换 | 自动转换为int(如int x = A;) |
需显式转换(如int x = static_cast<int>(E_Name::A);) |
| 底层类型 | 默认为int,不可指定 |
可指定(如enum class E : short { A, B };) |
| 作用域 | 枚举项在全局作用域,可能冲突 | 枚举项在枚举类内,避免冲突 |
| 跨枚举比较 | 不同枚举项可比较(如A == B) |
禁止不同枚举类比较(编译错误) |
| 初始化规则 | 未赋值时从0开始递增(如A=1, B, C→B=2, C=3) |
未赋值时从0开始递增,需显式访问(如E_Name::A) |
结构体
结构体基本用法
| 核心要点 | 说明 |
|---|---|
| 声明语法 | struct Student { int age; string name; }; |
| 初始化方式 | - 聚合初始化:Student s = {18, "张三"}; - 构造函数初始化: Student s(18, "张三"); |
| 成员访问 | - 普通变量:s.age - 指针变量: s_ptr->name |
| 内存占用 | 包含所有成员变量的字节和内存对齐填充字节,如struct {int a; char b;}占8字节(4+1+3填充) |
结构体嵌套与内存对齐
| 核心要点 | 说明 |
|---|---|
| 嵌套语法 | struct A { struct B { int b; } b; }; |
| 内存对齐规则 | 1. 成员变量起始地址为自身类型大小的倍数 2. 整体大小为最大成员类型的倍数 |
| 示例 | struct {double a; int b;}占16字节(8+4+4填充) |
| 优化建议 | 按成员大小降序排列,减少填充字节,如double在前,int在后 |
构造函数与析构函数
| 类型 | 构造函数 | 析构函数 |
|---|---|---|
| 声明 | Student() {} / Student(int a) : age(a) {} |
~Student() {} |
| 作用 | 初始化结构体对象 | 释放对象资源(如堆内存) |
| 调用时机 | 声明对象时自动调用 | 对象释放时自动调用 |
| 注意事项 | 可重载,无返回值,函数名与结构体同名 | 不可重载,无参数,函数名前加~ |
结构体与函数
| 场景 | 说明 |
|---|---|
| 参数传递 | - 值传递:拷贝副本,不影响原对象 - 指针/引用传递:直接操作原对象 |
| 返回值 | 可返回结构体,但避免返回局部变量(生命周期结束后失效),推荐返回堆内存对象 |
| 示例 | void func(Student& s) { s.age++; }(引用传递修改原对象) |
结构体数组与指针
| 核心要点 | 说明 |
|---|---|
| 声明与初始化 | - 静态数组:Student arr[5];(默认调用无参构造)- 聚合初始化: Student arr[3] = {{18,"A"}, {20,"B"}, {22,"C"}};- 动态数组(堆):Student* ptr = new Student[5];(需匹配delete[]释放) |
| 指针指向数组 | - 指针指向数组首地址:Student* p = arr;- 指针偏移访问: p->age(首元素)、(p+1)->name(次元素) |
| 数组元素访问 | - 下标访问:arr[0].age - 指针访问: p[0].age(等价于arr[0].age)、*(p+1).name(等价于arr[1].name) |
| 动态内存管理 | - 堆上数组分配:Student* heapArr = new Student[5]{ {18,"A"}, ... };- 释放: delete[] heapArr;(必须与new[]配对,否则内存泄漏) |
| 构造与析构调用 | - 静态数组:声明时调用构造函数,作用域结束时调用析构函数 - 动态数组: new[]时调用构造函数,delete[]时调用析构函数 |
| 指针数组 vs 数组指针 | - 指针数组:Student* ptrArr[3];(数组元素为结构体指针)- 数组指针: Student (*arrPtr)[5];(指针指向结构体数组) |
类型别名(typedef/using)
| 核心要点 | 说明 |
|---|---|
| 语法 | - typedef 原类型 别名; - using 别名 = 原类型; |
| 作用 | 简化复杂类型书写,提高可读性,如:typedef void (*FuncPtr)(int, double); |
| 示例 | - 数组别名:typedef int Arr[10]; Arr arr;- 函数指针别名: using FPtr = void(*)(Student&); |
宏
核心概念
| 要点 | 说明 |
|---|---|
| 本质 | 预处理指令,编译前进行文本替换,无类型检查,类似“文本别名”。 |
| 定义语法 | #define 宏名 替换内容(不带参数)#define 宏名(参数) 表达式(带参数) |
分类与示例
| 类型 | 示例 | 注意事项 |
|---|---|---|
| 不带参数宏 | #define PI 3.14159 |
常用于定义常量,如PI、MAX_SIZE。 |
| 带参数宏 | #define ADD(x,y) (x+y) |
表达式需加括号(如(x)*(y)),避免优先级错误。 |
关键用法
- 常量定义:
#define ELEMENT_NUM 100 - 代码简化:
#define PRINT(x) cout << x << endl - 条件编译:
#ifdef DEBUG cout << "调试模式" << endl; #endif - 取消定义:
#undef MACRO_NAME
优缺点
| 优点 | 缺点 |
|---|---|
| 1. 简化重复代码,便于全局修改。 2. 条件编译灵活(适配不同平台/版本)。 |
1. 无类型检查,可能引发隐性错误。 2. 命名冲突风险,缺乏作用域限制。 |
48.3 面试题精选
基础题
1. 值传递和引用传递的区别
题目
请说明C++中值传递和引用传递的区别,各自的使用场景是什么?
深入解析
值传递和引用传递是C++函数参数传递的两种核心方式:
值传递:
- 形参是实参的副本,函数内修改不影响实参
- 适合小对象(基本类型),避免拷贝开销大的对象
- 安全性高,函数不会产生副作用
引用传递:
- 形参是实参的别名,共享同一内存地址
- 避免拷贝开销,适合大对象传递
- 可直接修改实参,常用于输出参数
void swap_value(int a, int b) {
int temp = a; a = b; b = temp;
}
void swap_ref(int& a, int& b) {
int temp = a; a = b; b = temp;
}
int main() {
int x = 1, y = 2;
swap_value(x, y);
cout << x << "," << y << endl;
swap_ref(x, y);
cout << x << "," << y << endl;
}
答题示例
值传递创建实参副本,函数内修改不影响原值;引用传递传递别名,修改直接影响原值。小对象用值传递安全高效,大对象或需要修改实参时用引用传递。只读场景推荐
const引用,既避免拷贝又防止修改。
参考文章
- 3.函数-形参和实参.md
- 35.引用-引用和函数.md
2. 指针和引用的区别
题目
指针和引用有什么区别?什么情况下使用指针,什么情况下使用引用?
深入解析
| 对比项 | 指针 | 引用 |
|---|---|---|
| 本质 | 存储地址的变量 | 变量的别名 |
| 初始化 | 可以不初始化(野指针风险) | 必须初始化 |
| 重新绑定 | 可改变指向 | 绑定后不可更改 |
| 空值 | 可为nullptr |
不存在”空引用” |
| 运算 | 支持算术运算 | 不支持 |
| 内存占用 | 8字节(64位系统) | 不占用额外空间 |
int a = 10, b = 20;
int* p = &a;
p = &b;
int& ref = a;
ref = b;
使用场景:
- 指针:需要重新指向、可能为空、需要指针运算、动态内存管理
- 引用:函数参数传递、操作符重载、简单别名场景
答题示例
指针是存储地址的独立变量,可重新赋值、可为空、支持运算;引用是变量别名,必须初始化且绑定后不可更改。需要重新指向或可能为空时用指针,函数传参或简单别名场景用引用更安全。
参考文章
- 16.指针-指针的基本概念.md
- 33.引用-引用的基础使用.md
3. static关键字的作用
题目
C++中static关键字有哪些用途?
深入解析
static关键字在不同上下文中有不同含义:
| 位置 | 作用 | 生命周期 | 作用域 |
|---|---|---|---|
| 局部变量 | 仅初始化一次,值保持 | 程序运行期 | 函数内部 |
| 全局变量 | 限制在本文件内访问 | 程序运行期 | 本文件 |
| 类成员变量 | 所有对象共享一份 | 程序运行期 | 类作用域 |
| 类成员函数 | 无this指针,只能访问静态成员 | - | 类作用域 |
void counter() {
static int count = 0;
count++;
cout << count << endl;
}
static int globalVar = 100;
class MyClass {
static int shared;
static void func() { cout << shared; }
};
答题示例
static有三种用法:修饰局部变量使其只初始化一次、值在函数调用间保持;修饰全局变量限制其作用域在本文件内,避免命名冲突;修饰类成员表示所有对象共享,静态函数无this指针只能访问静态成员。
参考文章
- 10.变量的存储类型-static变量.md
4. new/delete和malloc/free的区别
题目
new/delete和malloc/free有什么区别?
深入解析
| 对比项 | new/delete | malloc/free |
|---|---|---|
| 性质 | 运算符 | 库函数 |
| 返回类型 | 类型安全,返回具体类型指针 | 返回void*需强转 |
| 构造/析构 | 自动调用 | 不调用 |
| 失败处理 | 抛出bad_alloc异常 |
返回NULL |
| 内存大小 | 自动计算 | 手动指定 |
| 重载 | 可重载 | 不可重载 |
int* p1 = new int(10);
delete p1;
int* p2 = (int*)malloc(sizeof(int));
*p2 = 10;
free(p2);
Student* s1 = new Student("张三");
delete s1;
Student* s2 = (Student*)malloc(sizeof(Student));
free(s2);
答题示例
new/delete是C++运算符,malloc/free是C库函数。关键区别:new自动计算大小并返回类型安全指针,会调用构造函数;malloc需手动指定大小、返回void*需强转、不调用构造函数。C++中推荐用new/delete,特别是涉及对象时必须用它们保证构造析构正确执行。
参考文章
- 31.内存分配-堆上分配内存.md
5. 什么是内存泄漏,如何避免
题目
什么是内存泄漏?如何检测和避免内存泄漏?
深入解析
内存泄漏指程序动态分配的堆内存未被释放,导致内存持续占用无法回收。
常见原因:
new后忘记delete- 异常跳过释放代码
- 指针被覆盖导致无法释放
避免方法:
- 及时释放:
delete后置nullptr - RAII原则:用对象管理资源
- 智能指针:
unique_ptr、shared_ptr - 配对使用:
new配delete,new[]配delete[]
int* p = new int(10);
delete p;
p = nullptr;
int* arr = new int[5];
delete[] arr;
arr = nullptr;
答题示例
内存泄漏是堆内存分配后未释放,导致内存持续占用。避免方法:一是及时delete并置nullptr;二是遵循RAII用对象管理资源;三是使用智能指针自动管理;四是确保new/delete、new[]/delete[]配对使用。
参考文章
- 32.内存分配-内存安全.md
进阶题
1. 函数重载的原理和限制
题目
C++函数重载是如何实现的?有什么限制?
深入解析
函数重载通过名称修饰(Name Mangling)实现,编译器根据参数列表生成唯一内部名称。
重载条件:
- 同一作用域
- 函数名相同
- 参数数量/类型/顺序不同
- 与返回值无关
限制与陷阱:
- 默认参数可能导致歧义
- 顶层const不影响重载
- 指针和引用的底层const可以重载
void func(int a);
void func(int a, int b = 10);
int x = 5;
func(x);
void print(int val);
void print(const int* ptr);
void print(int* const ptr);
答题示例
函数重载通过编译器名称修饰实现,将函数名和参数类型编码成唯一标识。重载要求参数列表不同(数量、类型、顺序),返回值不参与区分。注意默认参数可能导致调用歧义,顶层const不构成重载,底层const(指向常量的指针)可以重载。
参考文章
- 6.函数-函数重载.md
2. 指针常量和常量指针的区别
题目
请解释const int* p和int* const p的区别?
深入解析
区分关键:const修饰谁,谁就不可变。
| 类型 | 语法 | 指向可变 | 值可变 |
|---|---|---|---|
| 指向常量的指针 | const int* p |
✓ | ✗ |
| 指针常量 | int* const p |
✗ | ✓ |
| 指向常量的指针常量 | const int* const p |
✗ | ✗ |
记忆技巧:从右往左读
const int* p:p is a pointer to const int(指向常量的指针)int* const p:p is a const pointer to int(指针常量)
int a = 10, b = 20;
const int* p1 = &a;
p1 = &b;
int* const p2 = &a;
*p2 = 30;
const int* const p3 = &a;
答题示例
从右往左读:
const int* p是指向常量的指针,可改指向但不可改值;int* const p是指针常量,不可改指向但可改值。记忆口诀:const在左边修饰值,const在右边修饰指针。
参考文章
- 19.指针-指针和常量.md
3. 数组指针和指针数组的区别
题目
int (*p)[4]和int* p[4]有什么区别?
深入解析
| 对比项 | int (*p)[4] |
int* p[4] |
|---|---|---|
| 名称 | 数组指针 | 指针数组 |
| 本质 | 指向数组的指针 | 存储指针的数组 |
| p的类型 | int(*)[4] |
int** |
| 内存占用 | 8字节(指针) | 32字节(4个指针) |
| 用途 | 操作二维数组 | 存储多个地址 |
解析:
int (*p)[4]:p是指针,指向含4个int的数组int* p[4]:p是数组,含4个int*元素
int arr[3][4] = {{1,2,3,4}, {5,6,7,8}, {9,10,11,12}};
int (*p1)[4] = arr;
cout << *(*(p1+1)+2) << endl;
int a=1, b=2, c=3, d=4;
int* p2[4] = {&a, &b, &c, &d};
cout << *p2[0] << endl;
答题示例
数组指针
int (*p)[4]是一个指针,指向含4个int的数组,常用于遍历二维数组;指针数组int* p[4]是一个数组,存储4个int指针。区分技巧:括号优先,(*p)说明p是指针,否则p先与[4]结合是数组。
参考文章
- 28.指针-指针数组.md
4. 深拷贝和浅拷贝
题目
什么是深拷贝和浅拷贝?什么情况下需要自定义拷贝构造函数?
深入解析
| 类型 | 行为 | 风险 |
|---|---|---|
| 浅拷贝 | 逐字节复制,指针成员共享地址 | 重复释放、数据篡改 |
| 深拷贝 | 为指针成员分配新内存 | 需手动管理 |
需要自定义拷贝构造的场景:
- 类包含指针成员
- 类管理动态内存
- 类持有资源句柄
class Buffer {
int* data;
int size;
public:
Buffer(int s) : size(s), data(new int[s]) {}
Buffer(const Buffer& other) : size(other.size) {
data = new int[size];
memcpy(data, other.data, size * sizeof(int));
}
~Buffer() { delete[] data; }
};
Buffer b1(10);
Buffer b2 = b1;
答题示例
浅拷贝直接复制成员值,指针成员会共享同一地址,析构时重复释放导致崩溃;深拷贝为指针成员分配新内存并复制内容。当类包含指针成员或管理动态内存时,必须自定义拷贝构造函数实现深拷贝,避免资源管理问题。
参考文章
- 31.内存分配-堆上分配内存.md
- 41.结构体-析构函数.md
5. 左值引用和右值引用
题目
什么是左值引用和右值引用?右值引用有什么应用价值?
深入解析
| 类型 | 语法 | 绑定对象 | 典型用途 |
|---|---|---|---|
| 左值引用 | T& |
持久对象(变量) | 避免拷贝、参数传递 |
| 右值引用 | T&& |
临时对象(右值) | 移动语义、资源转移 |
右值引用的核心价值:
- 移动语义:避免临时对象的深拷贝
- 完美转发:保持参数的值类别
string s1 = "hello";
string& ref = s1;
string&& rref = string("world");
string s2 = std::move(s1);
void process(int& x) { cout << "左值"; }
void process(int&& x) { cout << "右值"; }
template<typename T>
void forward(T&& arg) {
process(std::forward<T>(arg));
}
答题示例
左值引用
T&绑定持久对象,右值引用T&&绑定临时对象。右值引用的核心价值是移动语义,通过std::move将左值转为右值,实现资源所有权的转移而非拷贝,大幅提升性能。常用于实现移动构造函数和移动赋值运算符。
参考文章
- 34.引用-左值引用和右值引用.md
深度题
1. 二维数组的内存布局与指针访问
题目
请详细说明二维数组在内存中的存储方式,以及如何通过指针访问arr[i][j]?
深入解析
二维数组按行优先连续存储,逻辑上的行列结构在内存中是一维连续空间。
内存布局:
int arr[3][4]:
地址: 0 4 8 12 16 20 24 28 32 36 40 44
内容: [0,0][0,1][0,2][0,3][1,0][1,1][1,2][1,3][2,0][2,1][2,2][2,3]
两种指针访问方式:
- 元素指针:
int* p = &arr[0][0];
*(p + i*4 + j)
- 行指针:
int (*p)[4] = arr;
*(*(p + i) + j)
**解析*(*(p+i)+j)**:
p+i:跳过i行(i×16字节)*(p+i):得到第i行首元素地址(类型int*)*(p+i)+j:在第i行内偏移j个int*(*(p+i)+j):解引用得到元素值
答题示例
二维数组按行优先连续存储,3行4列的数组在内存中是12个连续int。访问
arr[i][j]有两种方式:一是用元素指针int* p = &arr[0][0],通过*(p + i*列数 + j)计算偏移;二是用行指针int (*p)[4] = arr,通过*(*(p+i)+j)访问,其中p+i跳过i行,*(p+i)转为列指针,再+j偏移到具体元素。
参考文章
- 14.数组-二维数组.md
- 21.指针-指针和数组-指针和二维数组.md
2. 函数指针与回调函数
题目
什么是函数指针?如何实现回调函数机制?
深入解析
函数指针存储函数入口地址,可实现运行时动态调用不同函数。
声明与使用:
int (*pFunc)(int, int);
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
pFunc = add;
cout << pFunc(3, 2) << endl;
pFunc = sub;
cout << pFunc(3, 2) << endl;
回调函数实现:
void sort(int* arr, int n, bool (*compare)(int, int)) {
for (int i = 0; i < n-1; i++) {
for (int j = 0; j < n-i-1; j++) {
if (compare(arr[j], arr[j+1])) {
int temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
bool ascending(int a, int b) { return a > b; }
bool descending(int a, int b) { return a < b; }
int arr[] = {3, 1, 4, 1, 5};
sort(arr, 5, ascending);
sort(arr, 5, descending);
答题示例
函数指针是存储函数入口地址的变量,声明如
int (*p)(int, int)。回调函数是将函数指针作为参数传递,由被调函数在适当时机调用。典型应用是排序算法的比较函数,通过传入不同的比较函数实现升序或降序排列,实现算法逻辑与比较策略的解耦。
参考文章
- 24.指针-指针和函数-指向函数的指针.md
- 25.指针-指针和函数-回调函数.md
3. 内存分区及各区域特点
题目
C++程序运行时内存分为哪些区域?各有什么特点?
深入解析
| 区域 | 存储内容 | 生命周期 | 管理方式 |
|---|---|---|---|
| 代码段 | 函数机器码 | 程序运行期 | 系统自动 |
| 数据段 | 全局变量、静态变量 | 程序运行期 | 系统自动 |
| 常量段 | 字符串字面量 | 程序运行期 | 系统自动 |
| 栈 | 局部变量、函数参数 | 函数调用期 | 编译器自动 |
| 堆 | 动态分配内存 | 手动控制 | 程序员管理 |
关键区别:
- 栈:自动管理,空间有限(约1-8MB),增长方向向下
- 堆:手动管理,空间大,增长方向向上
示例:
int global;
static int s_global;
const char* str = "hello";
void func(int param) {
int local;
static int s_local;
int* heap = new int(10);
}
答题示例
C++内存分五个区域:代码段存指令只读;数据段存全局和静态变量;常量段存字面量;栈存局部变量自动管理空间有限;堆存动态分配内存手动管理空间大。栈由编译器自动分配释放,堆需程序员用new/delete管理,忘记释放会导致内存泄漏。
参考文章
- 30.内存分配-内存存储区域.md
4. 结构体内存对齐规则
题目
什么是内存对齐?结构体内存对齐的规则是什么?如何优化结构体大小?
深入解析
内存对齐是编译器为了提高CPU访问效率而进行的内存布局优化。
对齐规则:
- 成员起始地址为自身大小的整数倍
- 结构体总大小为最大成员大小的整数倍
- 可通过
#pragma pack(n)修改对齐值
示例分析:
struct A {
char a;
int b;
char c;
};
struct B {
int b;
char a;
char c;
};
cout << sizeof(A) << endl;
cout << sizeof(B) << endl;
A的布局(12字节):
|a(1)|---填充(3)---|----b(4)----|c(1)|---填充(3)---|
B的布局(8字节):
|----b(4)----|a(1)|c(1)|--填充(2)--|
优化原则:按成员大小降序排列,减少填充字节。
答题示例
内存对齐规则:成员起始地址为自身大小的整数倍,结构体总大小为最大成员的整数倍。优化方法是按成员大小降序排列,如double在前、char在后,可减少填充字节。示例中
struct{char,int,char}占12字节,调整为struct{int,char,char}仅占8字节。
参考文章
- 39.结构体-结构体嵌套内存大小内存对齐.md
5. 宏定义与内联函数的对比
题目
宏定义和内联函数有什么区别?为什么推荐用内联函数替代带参数的宏?
深入解析
| 对比项 | 宏定义 | 内联函数 |
|---|---|---|
| 处理时机 | 预处理阶段 | 编译阶段 |
| 类型检查 | 无 | 有 |
| 调试支持 | 无法调试 | 可调试 |
| 作用域 | 无作用域限制 | 遵循作用域规则 |
| 参数求值 | 可能多次求值 | 只求值一次 |
宏的陷阱:
#define SQUARE(x) ((x) * (x))
int a = 5;
cout << SQUARE(a++) << endl;
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 1, y = 2;
int z = MAX(x++, y++);
内联函数替代:
inline int square(int x) {
return x * x;
}
inline int max(int a, int b) {
return a > b ? a : b;
}
答题示例
宏定义在预处理阶段文本替换,无类型检查、可能多次求值导致副作用;内联函数在编译阶段展开,有类型检查、参数只求值一次、可调试。典型陷阱是
SQUARE(a++)展开为((a++)*(a++)),a被自增两次。推荐用内联函数替代带参宏,既保留性能优势又保证类型安全。
参考文章
- 8.函数-内联函数.md
- 45.宏.md
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com