48.C++基础语法知识总结

  1. 48.总结
    1. 48.1 知识点
      1. 学习的主要内容
      2. 强调
    2. 48.2 核心要点速览
      1. 函数
        1. 函数基础
        2. 形参与实参
        3. 参数默认值
        4. 变量作用域
        5. 函数重载
        6. 递归函数
        7. 内联函数
      2. 变量的存储类型
        1. auto变量
        2. static变量
        3. register变量
        4. extern变量
      3. 数组
        1. 一维数组
        2. 二维数组
        3. 字符数组
      4. 指针基础
        1. 内存地址与指针概念
        2. 指针基础操作
        3. 指针进阶操作
        4. 指针与常量
      5. 指针和数组
        1. 指针与一维数组
        2. 指针与二维数组
        3. 指针与字符数组
      6. 指针数组
        1. 数组指针 vs 指针数组
      7. 多级指针
        1. 各级指针对比
      8. 内存分配
        1. 内存存储区域对比
        2. 堆内存分配与释放
        3. 内存安全核心要点
      9. 引用
        1. 引用的基础使用
        2. 左值引用 vs 右值引用
        3. 引用与函数
      10. 枚举
      11. 结构体
        1. 结构体基本用法
        2. 结构体嵌套与内存对齐
        3. 构造函数与析构函数
        4. 结构体与函数
        5. 结构体数组与指针
        6. 类型别名(typedef/using)
        1. 核心概念
        2. 分类与示例
        3. 关键用法
        4. 优缺点
    3. 48.3 面试题精选
      1. 基础题
        1. 1. 值传递和引用传递的区别
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 指针和引用的区别
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. static关键字的作用
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        4. 4. new/delete和malloc/free的区别
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        5. 5. 什么是内存泄漏,如何避免
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 1. 函数重载的原理和限制
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 指针常量和常量指针的区别
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 数组指针和指针数组的区别
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        4. 4. 深拷贝和浅拷贝
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        5. 5. 左值引用和右值引用
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 1. 二维数组的内存布局与指针访问
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 函数指针与回调函数
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. 内存分区及各区域特点
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        4. 4. 结构体内存对齐规则
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        5. 5. 宏定义与内联函数的对比
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

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=&ipi的地址,*pi的值。

指针基础操作

操作 语法 示例
声明 类型* 指针名 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 常用于定义常量,如PIMAX_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
  • 异常跳过释放代码
  • 指针被覆盖导致无法释放

避免方法

  1. 及时释放:delete后置nullptr
  2. RAII原则:用对象管理资源
  3. 智能指针:unique_ptrshared_ptr
  4. 配对使用:newdeletenew[]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)实现,编译器根据参数列表生成唯一内部名称。

重载条件

  • 同一作用域
  • 函数名相同
  • 参数数量/类型/顺序不同
  • 与返回值无关

限制与陷阱

  1. 默认参数可能导致歧义
  2. 顶层const不影响重载
  3. 指针和引用的底层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* pint* 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&& 临时对象(右值) 移动语义、资源转移

右值引用的核心价值

  1. 移动语义:避免临时对象的深拷贝
  2. 完美转发:保持参数的值类别
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]

两种指针访问方式

  1. 元素指针
int* p = &arr[0][0];
*(p + i*4 + j)
  1. 行指针
int (*p)[4] = arr;
*(*(p + i) + j)

**解析*(*(p+i)+j)**:

  1. p+i:跳过i行(i×16字节)
  2. *(p+i):得到第i行首元素地址(类型int*
  3. *(p+i)+j:在第i行内偏移j个int
  4. *(*(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访问效率而进行的内存布局优化。

对齐规则

  1. 成员起始地址为自身大小的整数倍
  2. 结构体总大小为最大成员大小的整数倍
  3. 可通过#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

×

喜欢就点赞,疼爱就打赏