Appearance
语言基础
C 和 C++ 区别是什么?
一句话理解: C 更偏“过程式 + 底层控制”,核心是函数、指针、结构体、手动内存管理;C++ 在 C 的基础上加入了类、对象、继承、多态、模板、STL、RAII 等机制,更适合组织大型复杂工程。
零基础理解
C 像是你直接拿工具修机器:
你自己申请内存
你自己释放内存
你自己组织数据
你自己调用函数处理数据C++ 像是在 C 的工具箱上,加了一套更大的工程管理能力:
可以把数据和函数封装成类
可以用构造函数自动初始化
可以用析构函数自动释放资源
可以用继承和多态组织复杂对象
可以用模板写通用容器和算法
可以用 STL 少造很多轮子所以 C++ 不是简单“C 的升级版”,更准确地说: C++ 兼容大量 C 的语法,但它是一门更复杂、更强调抽象和工程组织能力的语言。
编程思想不同
C 是过程式语言,通常是:
数据放在结构体里
函数负责处理这些数据C 示例:
c
#include <stdio.h>
typedef struct Player
{
int hp;
int attack;
} Player;
// C 里通常用函数处理结构体数据
void TakeDamage(Player* player, int damage)
{
if (player == NULL)
{
return;
}
player->hp -= damage;
if (player->hp < 0)
{
player->hp = 0;
}
}
int main()
{
Player player;
player.hp = 100;
player.attack = 20;
TakeDamage(&player, 30);
printf("玩家血量: %d\n", player.hp);
return 0;
}C++ 可以把数据和行为放在一起:
c
#include <iostream>
using namespace std;
class Player
{
private:
int hp;
int attack;
public:
// 构造函数:创建对象时自动初始化
Player(int hp, int attack)
{
this->hp = hp;
this->attack = attack;
}
// 成员函数:对象自己的行为
void TakeDamage(int damage)
{
hp -= damage;
if (hp < 0)
{
hp = 0;
}
}
int GetHp() const
{
return hp;
}
};
int main()
{
Player player(100, 20);
player.TakeDamage(30);
cout << "玩家血量: " << player.GetHp() << endl;
return 0;
}区别很明显:
C:函数 + 数据分开。
C++:数据 + 行为可以封装在类里。面向对象能力不同
C 没有原生的类、继承、多态。 C++ 有:
class 类
private / public / protected 访问控制
构造函数
析构函数
继承
虚函数
多态
运算符重载比如:
c
#include <iostream>
using namespace std;
class Enemy
{
public:
// virtual 表示这个函数支持多态
virtual void Attack()
{
cout << "敌人攻击" << endl;
}
// 虚析构函数:通过基类指针删除子类对象时更安全
virtual ~Enemy()
{
}
};
class Boss : public Enemy
{
public:
void Attack() override
{
cout << "Boss 释放大招" << endl;
}
};
int main()
{
Enemy* enemy = new Boss();
// 虽然指针类型是 Enemy*,但实际调用 Boss 的 Attack
enemy->Attack();
delete enemy;
return 0;
}这就是 C++ 的多态。 C 想实现类似效果,需要自己用函数指针模拟,写起来更麻烦。
内存管理不同
C 常用:
c
malloc
freeC++ 有:
new
delete
构造函数
析构函数
智能指针
RAIIC 示例:
c
#include <stdlib.h>
int main()
{
// C 中手动申请内存
int* numbers = (int*)malloc(sizeof(int) * 10);
if (numbers == NULL)
{
return 1;
}
// 使用内存
numbers[0] = 100;
// 必须手动释放,否则内存泄漏
free(numbers);
return 0;
}C++ 更推荐 RAII 和智能指针:
c
#include <iostream>
#include <memory>
using namespace std;
class Texture
{
public:
Texture()
{
cout << "加载纹理" << endl;
}
~Texture()
{
cout << "释放纹理" << endl;
}
};
int main()
{
// unique_ptr 会在离开作用域时自动释放对象
unique_ptr<Texture> texture = make_unique<Texture>();
// 不需要手动 delete
return 0;
}RAII 的意思是: 资源的生命周期绑定到对象生命周期。对象创建时拿资源,对象销毁时自动释放资源。
这对游戏开发很重要,比如:
纹理资源
文件句柄
网络连接
锁
内存对象泛型能力不同
C 没有模板。 C++ 有模板,可以写类型通用的代码。
c
#include <iostream>
using namespace std;
// 模板函数:T 可以是 int、float、double 等类型
template <typename T>
T Max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
cout << Max(3, 5) << endl;
cout << Max(1.5f, 2.8f) << endl;
return 0;
}C++ 的 STL 就大量使用模板:
c
#include <vector>
#include <string>
#include <iostream>
using namespace std;
int main()
{
vector<string> names;
names.push_back("Alice");
names.push_back("Bob");
for (const string& name : names)
{
cout << name << endl;
}
return 0;
}C 里如果要做类似 vector 的东西,通常要自己写动态数组管理。
标准库差异
C 标准库比较基础:
c
stdio.h
stdlib.h
string.h
math.hC++ 标准库更强:
c
vector
map
unordered_map
string
queue
stack
set
algorithm
smart pointer
iostream
thread
mutex比如 C++ 里用 vector:
c
#include <vector>
using namespace std;
int main()
{
vector<int> nums;
nums.push_back(10);
nums.push_back(20);
nums.push_back(30);
return 0;
}C 里你要自己处理:
容量
扩容
拷贝
释放
越界struct 区别
C 的 struct 主要是数据集合:
c
typedef struct Player
{
int hp;
int attack;
} Player;C++ 的 struct 可以有函数、构造函数、继承,几乎像 class。
主要区别是默认访问权限:
c
C++ struct 默认 public。
C++ class 默认 private。示例:
c
struct Player
{
int hp;
void PrintHp()
{
cout << hp << endl;
}
};这个在 C 里不成立,因为 C 的结构体不能直接放成员函数。
函数重载不同
C 不支持函数重载。 C++ 支持。
C++ 示例:
c
#include <iostream>
using namespace std;
void Print(int value)
{
cout << "int: " << value << endl;
}
void Print(float value)
{
cout << "float: " << value << endl;
}
void Print(const string& value)
{
cout << "string: " << value << endl;
}同名函数可以根据参数类型不同调用不同版本。 这也是为什么 C++ 编译后会有名字修饰,也叫 name mangling。
如果 C++ 要暴露 C 风格接口,常见写法是:
c
extern "C"
{
void InitPlugin();
}这样可以避免 C++ 名字修饰,让 C 或其他语言更容易链接调用。
异常处理不同
C 没有原生异常,一般用错误码:
c
int result = LoadFile("data.txt");
if (result != 0)
{
// 处理错误
}C++ 有异常:
c
try
{
LoadFile("data.txt");
}
catch (const std::exception& e)
{
cout << e.what() << endl;
}不过游戏开发里,很多引擎或项目会限制异常使用,因为异常可能影响性能、二进制体积和可控性。 所以面试时可以说:C++ 支持异常,但游戏项目不一定大量使用。
C++ 是否完全兼容 C
不完全。
可以说:
C++ 兼容大量 C 语法。
但 C 不是严格意义上完全等于 C++ 子集。原因包括:
C++ 有更多关键字
C 和 C++ 对某些隐式转换规则不同
C++ 编译器对类型检查更严格
C 的一些写法在 C++ 里不合法所以面试里不要说:
C 是 C++ 的子集。这句话不够严谨。更好的说法是:
C++ 在设计上兼容了大量 C 语言语法,但两者是不同语言,并不是所有 C 代码都能直接当 C++ 编译。游戏开发里怎么用
在游戏行业里:
Unity 游戏逻辑常用 C#
Unreal Engine 核心开发用 C++
引擎底层、渲染、物理、资源系统经常用 C/C++
平台 SDK、插件接口经常提供 C API为什么引擎常用 C++?
性能强
可控性高
可以面向对象组织大型系统
模板和 STL 提供工程能力
RAII 适合资源管理
能接近底层,也能写高层抽象常见误区
1. 说 C++ 就是 C 的升级版,不够准确
2. 说 C 是 C++ 的严格子集,不严谨
3. 以为 C++ 一定比 C 慢,不对
4. 以为 C++ 只能面向对象,不对,它是多范式
5. 以为 new/delete 是 C++ 最推荐写法,现在更推荐智能指针和 RAII
6. 以为 C 没法做面向对象,C 可以模拟,但语言本身不直接支持面试高分回答
可以这样答:
TIP
C 和 C++ 都是偏底层、性能很强的语言,但设计理念不同。
C 更偏过程式编程,核心是函数、指针、结构体和手动内存管理。它适合系统底层、嵌入式、驱动、C API 这类场景。
C++ 兼容大量 C 语法,但它不是简单的 C 升级版,而是在此基础上加入了类、封装、继承、多态、模板、STL、异常、命名空间、RAII 等机制。它既能写过程式代码,也能写面向对象和泛型代码。
内存管理方面,C 主要用 malloc/free,C++ 有 new/delete,但现代 C++ 更推荐 RAII 和智能指针,让资源随着对象生命周期自动释放。
在大型工程里,C++ 的抽象能力更强,比如游戏引擎、渲染系统、资源系统会大量使用 C++ 来组织复杂模块。
但 C++ 复杂度也更高,编译模型、对象生命周期、模板、虚函数、多态等都需要更强的工程纪律。
最短记忆版
C = 函数 + 指针 + 结构体 + 手动内存。
C++ = C 风格底层能力 + 类/对象 + 模板/STL + RAII。
C 偏简单直接,C++ 偏大型工程抽象。指针和引用区别是什么?
一句话理解: 指针是“存地址的变量”,引用是“已有变量的别名”。C 语言只有指针;C++ 既有指针,也有引用。
零基础理解
假设有一个变量:
c
int x = 10;指针像一张纸条,上面写着 x 的地址:
c
int* p = &x;引用像给 x 起了一个外号:
c
int& r = x;所以:
p 是一个变量,它里面存的是 x 的地址。
r 不是新对象,它就是 x 的另一个名字。基本区别
c
#include <iostream>
using namespace std;
int main()
{
int x = 10;
// 指针:保存 x 的地址
int* p = &x;
// 引用:r 是 x 的别名
int& r = x;
// 通过指针修改 x,需要先解引用
*p = 20;
cout << x << endl; // 20
// 通过引用修改 x,写法像普通变量
r = 30;
cout << x << endl; // 30
return 0;
}核心感受:
指针访问对象:*p
引用访问对象:r能不能为空
指针可以为空:
c
int* p = nullptr;
if (p != nullptr)
{
*p = 100;
}引用正常情况下必须绑定到一个有效对象:
c
int x = 10;
// 正确:引用创建时必须初始化
int& r = x;不能这样写:
c
// 错误:引用必须初始化
int& r;所以面试里可以说:
如果一个参数可以没有,就用指针。
如果一个参数必须存在,就更适合用引用。能不能改指向
指针可以改指向:
c
int a = 10;
int b = 20;
int* p = &a;
// p 一开始指向 a
cout << *p << endl; // 10
// p 可以改成指向 b
p = &b;
cout << *p << endl; // 20引用不能重新绑定:
c
int a = 10;
int b = 20;
int& r = a;
// 这里不是让 r 改成引用 b
// 而是把 b 的值赋给 a
r = b;
cout << a << endl; // 20这点很容易错。 r = b 的意思是“通过引用修改 a 的值”,不是“让引用改绑到 b”。
访问语法不同
指针:
c
struct Player
{
int hp;
};
Player player;
player.hp = 100;
Player* p = &player;
// 指针访问成员,用 ->
p->hp = 80;
// 或者先解引用,再用 .
(*p).hp = 60;引用:
c
Player player;
player.hp = 100;
Player& r = player;
// 引用就像对象本身,用 .
r.hp = 80;所以记:
c
指针访问对象:*p
指针访问成员:p->member
引用访问成员:r.member函数传参区别
指针传参:
c
#include <iostream>
using namespace std;
void AddHpByPointer(int* hp)
{
// 指针可能为空,所以要判断
if (hp == nullptr)
{
return;
}
*hp += 10;
}
int main()
{
int hp = 100;
AddHpByPointer(&hp);
cout << hp << endl; // 110
return 0;
}引用传参:
c
#include <iostream>
using namespace std;
void AddHpByReference(int& hp)
{
// 引用语义上表示:这个参数必须存在
hp += 10;
}
int main()
{
int hp = 100;
AddHpByReference(hp);
cout << hp << endl; // 110
return 0;
}区别:
c
指针参数:调用者传地址,函数里要考虑 nullptr。
引用参数:调用者像传普通变量,函数里直接用。const 引用很常用
如果参数是一个大对象,直接传值会拷贝,可能浪费性能。
c
#include <iostream>
#include <string>
using namespace std;
void PrintName(const string& name)
{
// const 表示函数不会修改 name
// & 表示不拷贝整个 string
cout << name << endl;
}const T& 的意义:
不拷贝大对象
不允许函数修改它
适合只读参数这是 C++ 面试很常见的点。
指针更适合哪些场景
适合用指针:
对象可以为空
需要动态内存
需要数组遍历
需要改变指向
和 C API 交互
表达“可能没有这个对象”例子:
c
void SetTarget(Player* target)
{
if (target == nullptr)
{
// 没有目标
return;
}
target->hp -= 10;
}引用更适合哪些场景
适合用引用:
参数必须存在
不想拷贝对象
希望函数能修改外部变量
写法更自然
表达“这个对象一定有效”例子:
c
void Heal(Player& player)
{
// player 必须是一个有效对象
player.hp += 10;
}底层上引用是不是指针
面试可以这样说,比较稳:
从语言语义上看,引用是对象的别名,不是普通变量。
从编译器实现上看,引用很多时候可以用地址或指针来实现。
但不能简单把引用理解成完全等价于指针,因为引用有更强的语义限制:必须初始化,不能重新绑定,访问时不需要解引用。这句话很加分,因为它同时讲了“语义”和“底层实现”。
共同风险:悬空
指针会悬空:
c
int* p = nullptr;
{
int x = 10;
p = &x;
}
// x 已经销毁,p 还保存着旧地址
// 此时使用 *p 是危险的引用也可能悬空:
c
int& BadReference()
{
int x = 10;
// 错误:返回局部变量引用
// 函数结束后 x 就销毁了
return x;
}所以引用比指针语法更安全一点,但不是绝对安全。 对象生命周期依然必须正确。
常见面试对比表
是否存地址:
指针:是,指针变量保存地址。
引用:语义上是别名。
是否可以为空:
指针:可以 nullptr。
引用:正常语义下不能为空。
是否必须初始化:
指针:可以先不初始化,但很危险。
引用:必须初始化。
是否能改指向:
指针:可以。
引用:不能重新绑定。
访问方式:
指针:*p、p->member。
引用:像普通变量一样用。
是否支持指针运算:
指针:支持,比如 p++。
引用:不支持。
函数参数含义:
指针:可能为空,可能换对象。
引用:对象必须存在,语义更明确。面试高分回答
可以这样答:
TIP
指针是一个变量,它保存的是对象的地址;引用是一个对象的别名。
指针可以为空,可以重新指向其他对象,也可以做指针运算,所以它更灵活,但也更容易出现空指针、野指针和悬空指针问题。
引用必须在定义时初始化,并且绑定后不能再改绑到其他对象。使用引用时语法上就像使用原对象,所以更适合表达“这个参数必须存在”。
函数传参时,如果参数可以为空,或者需要表达可选对象,我会用指针;如果参数必须存在,而且不想拷贝对象,我会用引用,尤其是 const 引用。
底层实现上,引用很多时候可以由编译器用地址来实现,但语言语义上引用不是普通指针,它有必须初始化、不能重新绑定这些限制。
最短记忆版
NOTE
指针 = 地址变量,可以为空,可以换指向,用 * 或 -> 访问。 引用 = 对象别名,必须初始化,不能换绑定,像普通变量一样用。 可选对象用指针,必有对象用引用,只读大对象用 const 引用。
const 修饰指针有哪些情况?
一句话理解:const 修饰指针时,重点看它在 * 的哪边:* 左边限制“指向的内容”,* 右边限制“指针变量本身”。
第一种:指向常量的指针
c
const int* p;也可以写成:
c
int const* p;这两种完全等价。
意思是:
c
p 是一个指针。
p 指向的是 const int。
不能通过 p 修改它指向的内容。
但是 p 自己可以改指向。例子:
c
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int y = 20;
// p 指向一个“不能通过 p 修改”的 int
const int* p = &x;
// 错误:不能通过 p 修改 x
// *p = 100;
// 正确:p 自己可以改成指向 y
p = &y;
cout << *p << endl; // 20
return 0;
}注意一句话:
c
const int* p 限制的是 *p,不是 p。也就是说:
*p 不能改
p 可以改第二种:常量指针
c
int* const p = &x;意思是:
p 是一个 const 指针。
p 自己不能改指向。
但是可以通过 p 修改它指向的内容。例子:
c
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int y = 20;
// p 必须初始化,因为 p 自己是 const
int* const p = &x;
// 正确:可以通过 p 修改 x
*p = 100;
// 错误:p 不能再改成指向 y
// p = &y;
cout << x << endl; // 100
return 0;
}记住:
c
int* const p 限制的是 p,不是 *p。也就是说:
p 不能改
*p 可以改第三种:指向常量的常量指针
c
const int* const p = &x;意思是:
p 自己不能改指向。
也不能通过 p 修改指向的内容。例子:
c
#include <iostream>
using namespace std;
int main()
{
int x = 10;
int y = 20;
// p 是常量指针,并且指向 const int
const int* const p = &x;
// 错误:不能通过 p 修改 x
// *p = 100;
// 错误:p 不能改成指向 y
// p = &y;
cout << *p << endl; // 10
return 0;
}记忆:
const int* const p
前面的 const 管 *p
后面的 const 管 p所以:
*p 不能改
p 也不能改第四种:指针本身和内容都不 const
c
int* p;这是普通指针。
c
int x = 10;
int y = 20;
int* p = &x;
// 可以改内容
*p = 100;
// 可以改指向
p = &y;也就是:
c
p 可以改
*p 也可以改四种情况对比
c
int* p
p 可以改,*p 也可以改。
const int* p
p 可以改,*p 不可以改。
int* const p
p 不可以改,*p 可以改。
const int* const p
p 不可以改,*p 也不可以改。怎么快速判断
看 const 在哪里:
c
const int* p;const 在 * 左边,说明:
c
指向的内容不能通过 p 改。
int* const p;const 在 * 右边,说明:
c
指针变量 p 本身不能改。
const int* const p;* 左边和右边都有 const,说明:
内容不能改,指针也不能改。一个很好用的读法
从变量名开始读。
c
const int* p;读作:
c
p 是一个指针,指向 const int。所以:
c
p 能换地址
*p 不能改
int* const p;读作:
c
p 是一个 const 指针,指向 int。所以:
c
p 不能换地址
*p 能改
const int* const p;读作:
c
p 是一个 const 指针,指向 const int。所以:
c
p 不能换地址
*p 不能改重要细节:const 保护的是“通过这个指针访问时不能改”
看这个例子:
c
#include <iostream>
using namespace std;
int main()
{
int x = 10;
const int* p = &x;
// 错误:不能通过 p 改
// *p = 20;
// 正确:x 本身不是 const,可以直接改
x = 20;
cout << *p << endl; // 20
return 0;
}这说明:
c
const int* p 不是说 x 这个变量永远不能改。
它只是说:不能通过 p 这条路径去改。这句话面试很加分。
真正的常量对象
c
const int x = 10;
// 正确:普通指针不能指向 const int
// int* p = &x; // 错误
// 正确:必须用指向 const 的指针
const int* p = &x;为什么?
因为如果允许:
c
int* p = &x;那就能通过:
c
*p = 20;去修改一个真正的 const int,这显然不安全。
函数参数里怎么用
如果函数只读,不修改数据,可以用:
c
void PrintValue(const int* p)
{
// 指针可能为空,所以先判断
if (p == nullptr)
{
return;
}
// 这里只读,不修改
cout << *p << endl;
// 错误:不能修改
// *p = 100;
}如果参数必须存在,更常用 const 引用:
c
void PrintValue(const int& value)
{
// value 必须存在,并且不会被修改
cout << value << endl;
}简单选择:
参数可以为空:用 const T*
参数必须存在:用 const T&
需要修改对象:用 T* 或 T&多级指针简单理解
多级指针会更绕,比如:
c
const int** pp;这里可以先不用死背。 你只要记住:
每多一个 *,就多一层指针。
const 放在哪一层附近,就限制哪一层。比如:
c
int* const* pp;可以理解为:
pp 指向一个 const 指针。
那个被指向的指针不能改。面试如果只是问基础,一般重点是前三种:
c
const int* p
int* const p
const int* const p常见坑
1. 以为 const int* p 表示 p 不能改,其实是 *p 不能改。
2. 以为 int* const p 表示内容不能改,其实是 p 不能改。
3. 忘记 const 指针必须初始化。
4. 以为 const int* p 会让原变量永远不能改,其实只是不能通过 p 改。
5. 把 int const* p 和 const int* p 当成不同东西,它们其实一样。面试高分回答
可以这样答:
const 修饰指针主要看 const 在 * 的哪一边。
如果是 const int* p,或者 int const* p,表示 p 是指向常量的指针,不能通过 p 修改指向的内容,但 p 本身可以改指向。
如果是 int* const p,表示 p 是常量指针,p 一旦初始化后不能再指向别的地址,但可以通过 p 修改指向的内容。
如果是 const int* const p,表示 p 是指向常量的常量指针,既不能改 p 的指向,也不能通过 p 修改内容。
另外要注意,const int* p 只是限制通过 p 这条访问路径不能修改,不代表原对象一定不能被其他方式修改。最短记忆版
c
const 在 * 左边:*p 不能改,p 能改。
const 在 * 右边:p 不能改,*p 能改。
两边都有 const:p 和 *p 都不能改。static 有哪些含义?
一句话理解: 在 C/C++ 里,static 放的位置不同,含义不同:放局部变量上,主要改变生命周期;放全局变量/函数前,主要限制可见范围;放类成员上,表示它属于类,而不是某个对象。
1. 局部 static 变量
c
#include <iostream>
using namespace std;
void Count()
{
// 只初始化一次
// 函数结束后不会销毁,值会保留下来
static int count = 0;
count++;
cout << count << endl;
}
int main()
{
Count(); // 1
Count(); // 2
Count(); // 3
return 0;
}这里的 count 有两个特点:
作用域:只在 Count 函数内部能访问。
生命周期:从第一次执行到它时初始化,一直活到程序结束。普通局部变量是:
进入函数创建
退出函数销毁局部 static 是:
第一次进入函数时创建
程序结束时销毁所以它常用于:
计数器
缓存
延迟初始化
函数内单例2. 全局 static 变量
c
// Enemy.cpp
static int enemyCount = 0;这个 enemyCount 是全局变量,但它只能在当前 .cpp 文件里访问。
它的作用是:
限制链接性。
避免别的 .cpp 文件直接访问它。
防止全局符号冲突。普通全局变量可能被其他文件通过 extern 访问:
extern int enemyCount;但如果加了 static,它就只属于当前文件。 这叫内部链接,英文是 internal linkage。
3. static 函数
c
// MathUtil.cpp
static int Add(int a, int b)
{
return a + b;
}这个函数只能在当前 .cpp 文件里调用。 别的文件看不到它。
适合用来写当前文件内部的工具函数:
只给本文件用
不想暴露出去
避免名字冲突在 C 语言里,static 函数非常常见,用来模拟“私有函数”。
4. 类的 static 成员变量
c
#include <iostream>
using namespace std;
class Player
{
public:
// 声明静态成员变量
static int playerCount;
Player()
{
playerCount++;
}
};
// 类外定义静态成员变量
int Player::playerCount = 0;
int main()
{
Player p1;
Player p2;
Player p3;
cout << Player::playerCount << endl; // 3
return 0;
}static 成员变量的特点:
属于类,不属于某个对象。
所有对象共享同一份。
可以通过 类名::变量名 访问。普通成员变量是每个对象一份:
p1 有自己的 hp
p2 有自己的 hp
p3 有自己的 hp静态成员变量是所有对象共享一份:
c
Player::playerCount 只有一份5. 类的 static 成员函数
c
#include <iostream>
using namespace std;
class GameConfig
{
public:
static int maxLevel;
static int GetMaxLevel()
{
// static 成员函数可以访问 static 成员变量
return maxLevel;
}
};
int GameConfig::maxLevel = 100;
int main()
{
cout << GameConfig::GetMaxLevel() << endl;
return 0;
}static 成员函数特点:
属于类,不属于对象。
可以通过类名直接调用。
没有 this 指针。
不能直接访问非静态成员变量。为什么不能直接访问非静态成员?
因为非静态成员属于某个对象,而静态函数不绑定具体对象。
错误例子:
c
class Player
{
public:
int hp = 100;
static void PrintHp()
{
// 错误:static 函数没有 this,不知道访问哪个对象的 hp
// cout << hp << endl;
}
};正确做法:
c
#include <iostream>
using namespace std;
class Player
{
public:
int hp = 100;
static void PrintHp(Player& player)
{
// 传入具体对象后,就知道访问哪个对象了
cout << player.hp << endl;
}
};6. static 和生命周期
static 变量通常有静态存储期,意思是:
它不是函数调用结束就销毁。
它会存在很久,通常到程序结束才销毁。包括:
全局 static 变量
类 static 成员变量
局部 static 变量区别在于:
局部 static:作用域小,但生命周期长。
全局 static:作用域在当前文件,生命周期长。
类 static:作用域受类控制,所有对象共享一份。7. C++11 后局部 static 初始化线程安全
这个点面试加分。
c
class Singleton
{
public:
static Singleton& Instance()
{
// C++11 起,局部 static 初始化是线程安全的
static Singleton instance;
return instance;
}
private:
Singleton()
{
}
};这个写法常用于单例。 它的特点:
第一次调用 Instance 时创建。
之后每次返回同一个对象。
C++11 以后初始化过程线程安全。注意:线程安全的是“初始化过程”,不代表对象里面所有业务操作都自动线程安全。
8. static 和 const 的区别
很多人会混。
static:
关注生命周期、链接性、成员归属。
const:
关注能不能修改。例子:
c
static int count = 0;表示:
c
count 生命周期长,值可以改。
const int maxCount = 100;表示:
maxCount 不允许修改,但不一定是 static。也可以组合:
c
static const int MaxLevel = 100;意思是:
静态的、不可修改的整数常量。常见坑
1. 以为 static 一定表示静态类,不对,C++ 没有 C# 那种 static class。
2. 以为 static 局部变量每次进函数都会重新初始化,不对,只初始化一次。
3. 以为 static 成员函数能直接访问普通成员变量,不对,因为没有 this 指针。
4. 以为全局 static 变量全项目可见,不对,它只在当前文件可见。
5. 在头文件里乱定义 static 全局变量,可能导致每个包含它的 .cpp 都有一份副本。面试高分回答
可以这样答:
NOTE
static 在 C/C++ 里要看使用位置。
如果 static 修饰局部变量,表示这个变量有静态存储期,只初始化一次,函数结束后不会销毁,但作用域仍然只在函数内部。
如果 static 修饰全局变量或普通函数,表示它具有内部链接,只能在当前源文件中访问,常用于隐藏文件内部实现,避免符号冲突。
如果 static 用在类成员变量上,表示这个成员属于类而不是某个对象,所有对象共享一份,需要在类外定义。
如果 static 用在类成员函数上,表示这个函数属于类,可以通过类名调用,但它没有 this 指针,所以不能直接访问非静态成员。
总结就是:函数内 static 管生命周期,文件作用域 static 管链接性,类里 static 管成员归属。
最短记忆版
c
static 在函数内:变量只初始化一次,生命周期到程序结束。
static 在全局变量/函数前:只在当前文件可见。
static 在类成员上:属于类,所有对象共享。
static 成员函数:没有 this,不能直接访问普通成员。inline 有什么作用?
一句话理解:inline 有两个作用:一是建议编译器把函数展开到调用处,减少函数调用开销;二是允许函数定义放在头文件里,被多个 .cpp 包含时不报重复定义错误。现代 C++ 面试更看重第二点。
1. inline 最初的作用:减少函数调用开销
普通函数调用大概会有这些成本:
准备参数
跳转到函数地址
执行函数
返回调用点
处理栈帧如果函数非常小,比如:
c
int Add(int a, int b)
{
return a + b;
}调用:
c
int result = Add(1, 2);编译器可能直接变成类似:
int result = 1 + 2;这就是“内联展开”。
写法:
c
inline int Add(int a, int b)
{
return a + b;
}意思是:
这个函数很小,可以考虑直接展开。但注意: inline 只是建议,不是命令。
2. inline 不等于强制展开
这是面试高频点。
c
inline int Add(int a, int b)
{
return a + b;
}不代表编译器一定会展开。
编译器可能会根据情况决定:
函数太大,不展开
递归函数,不一定展开
优化等级不够,不展开
展开后代码膨胀,不展开反过来,即使你没写 inline,编译器也可能自动内联小函数。
所以准确说法是:
inline 是优化提示,最终是否内联由编译器决定。3. inline 在现代 C++ 更重要的作用:允许头文件定义
假设你在头文件里这样写:
c
// MathUtil.h
int Add(int a, int b)
{
return a + b;
}然后两个 .cpp 都包含它:
c
// A.cpp
#include "MathUtil.h"
// B.cpp
#include "MathUtil.h"这样链接时可能会报错:
c
multiple definition of Add因为每个 .cpp 都生成了一份 Add 函数定义。
加上 inline 后:
c
// MathUtil.h
inline int Add(int a, int b)
{
return a + b;
}多个 .cpp 包含同一个定义就可以了。 这就是 inline 和 ODR 的关系。
ODR 是 One Definition Rule,意思是“一处定义规则”。 inline 函数是特殊情况:可以在多个翻译单元里有定义,但这些定义必须完全一致。
4. 类内部定义的成员函数默认是 inline
c
class Player
{
private:
int hp;
public:
int GetHp() const
{
return hp;
}
};这个 GetHp 写在类定义里面,默认就是 inline 候选。
等价理解为:
c
class Player
{
private:
int hp;
public:
inline int GetHp() const
{
return hp;
}
};所以很多 getter、setter 会直接写在头文件类内部。
5. 类外定义时,如果放头文件,最好加 inline
c
// Player.h
class Player
{
private:
int hp;
public:
int GetHp() const;
};
// 如果这个定义也写在头文件里,建议加 inline
inline int Player::GetHp() const
{
return hp;
}如果不加 inline,多个 .cpp 包含这个头文件时,可能重复定义。
6. inline 和宏相比有什么好处
以前 C 里可能用宏:
c
#define ADD(a, b) ((a) + (b))但宏有问题:
没有类型检查
容易有副作用
不受作用域限制
调试不友好比如:
c
#define SQUARE(x) ((x) * (x))
int i = 2;
int result = SQUARE(i++);展开后可能变成:
c
((i++) * (i++))结果很危险。
用 inline 函数更安全:
c
inline int Square(int x)
{
return x * x;
}优点:
有类型检查
遵守作用域
更容易调试
不会像宏那样简单文本替换7. inline 适合什么函数
适合:
短小函数
频繁调用函数
getter / setter
简单数学函数
简单运算符重载
模板函数
头文件中的小工具函数例子:
c
class Vector2
{
public:
float x;
float y;
inline float LengthSquared() const
{
// 不开平方,只返回长度平方
return x * x + y * y;
}
};8. inline 不适合什么函数
不适合:
函数体很大
循环很多
分支很多
递归复杂
很少调用
展开后会让代码膨胀为什么代码膨胀不好?
函数本来只有一份代码。
内联后,每个调用点都复制一份代码。
调用点很多时,机器码变大。
机器码变大可能影响指令缓存。
最终不一定更快。所以 inline 不是越多越好。
9. C++17 的 inline 变量
C++17 以后还有 inline variable。
比如头文件里定义全局变量:
c
// Config.h
inline int MaxLevel = 100;这样多个 .cpp 包含 Config.h 时,不会因为重复定义报错。
也可以用于类静态成员:
c
// GameConfig.h
class GameConfig
{
public:
inline static int MaxLevel = 100;
};在 C++17 以前,静态成员变量通常要在 .cpp 里额外定义。 C++17 的 inline static 简化了这个问题。
10. inline 和 virtual 的关系
虚函数能不能内联? 答案是:有可能,但要看情况。
c
class Enemy
{
public:
virtual void Attack()
{
}
};如果通过基类指针调用:
c
Enemy* enemy = GetEnemy();
enemy->Attack();运行时才知道真正类型,通常需要虚表动态分派,不容易直接内联。
但如果编译器能确定真实类型,比如:
c
Enemy enemy;
enemy.Attack();或者优化阶段完成了去虚拟化,也可能内联。
面试可以说:
virtual 和 inline 不冲突,但虚调用是否能内联取决于编译器是否能确定真实调用目标。常见误区
1. 以为 inline 一定会展开,不对。
2. 以为不写 inline 就不会展开,不对。
3. 以为 inline 只和性能有关,不对,它还和 ODR、头文件定义有关。
4. 大函数乱加 inline,可能导致代码膨胀。
5. 在头文件里定义普通函数却不加 inline,可能链接时报重复定义。面试高分回答
可以这样答:
WARNING
inline 最初的作用是建议编译器把函数体展开到调用点,减少函数调用时的压栈、跳转和返回开销,适合短小且频繁调用的函数。
但 inline 不是强制命令,编译器可以忽略它;即使没写 inline,编译器也可能自动内联。
现代 C++ 里 inline 还有一个很重要的作用,就是允许函数定义放在头文件中。多个翻译单元包含同一个 inline 函数定义时,只要定义一致,就不会违反 ODR。
类内部定义的成员函数默认是 inline。模板函数和小工具函数也经常放在头文件里,因此常和 inline 相关。
不过 inline 不能滥用,大函数内联可能导致代码膨胀,影响指令缓存,反而降低性能。
最短记忆版
inline = 优化提示 + 允许头文件多处定义。
它不保证一定展开。
小函数适合 inline,大函数乱 inline 可能代码膨胀。
类内定义的成员函数默认 inline。宏和内联函数区别是什么?
一句话理解: 宏是预处理阶段的“文本替换”,内联函数是编译阶段的“真正函数”。能用 inline、constexpr、模板解决的,通常不要用函数式宏。
宏是什么
宏是预处理器处理的东西。
c
#define SQUARE(x) ((x) * (x))在编译前,预处理器会把代码里的宏展开。
比如:
c
int a = SQUARE(5);会被替换成类似:
c
int a = ((5) * (5));所以宏的本质是:
文本替换
不是函数
不做类型检查内联函数是什么
内联函数是真正的函数。
c
inline int Square(int x)
{
return x * x;
}它会参与正常的 C++ 编译流程:
有参数类型
有返回值类型
有作用域
可以调试
可以重载
可以被编译器优化注意: inline 不保证一定展开,只是告诉编译器“这个函数适合考虑内联”。最终是否展开,由编译器决定。
最大区别:处理阶段不同
宏:
预处理阶段处理,编译前就被文本替换。
内联函数:
编译阶段处理,是正常函数。也就是说,宏在编译器真正理解类型之前就已经展开了。 所以宏不懂 C++ 类型系统。
类型安全不同
宏没有类型检查:
c
#define ADD(a, b) ((a) + (b))你可以传任何东西进去,预处理器只是替换:
c
ADD(1, 2)
ADD(1.5, 2.8)
ADD("hello", 3)它不关心对不对,后面编译器再报错。
内联函数有类型检查:
c
inline int Add(int a, int b)
{
return a + b;
}如果你乱传参数,编译器会明确报类型错误。
如果想支持多种类型,可以用模板:
c
template <typename T>
inline T Add(T a, T b)
{
return a + b;
}这比宏安全得多。
宏容易有副作用
经典坑:
c
#include <iostream>
using namespace std;
#define SQUARE(x) ((x) * (x))
int main()
{
int i = 3;
int result = SQUARE(i++);
cout << result << endl;
cout << i << endl;
return 0;
}你以为是:
c
i++ 执行一次但宏展开后是:
c
int result = ((i++) * (i++));i++ 被执行了两次。 这就很危险。
内联函数不会这样:
c
#include <iostream>
using namespace std;
inline int Square(int x)
{
return x * x;
}
int main()
{
int i = 3;
// i++ 只会在传参时执行一次
int result = Square(i++);
cout << result << endl;
cout << i << endl;
return 0;
}函数参数会先求值一次,再传给函数。 所以内联函数比宏更安全。
宏容易有优先级问题
不加括号的宏很危险:
c
#define DOUBLE(x) x + x调用:
c
int result = DOUBLE(1 + 2) * 3;展开后:
c
int result = 1 + 2 + 1 + 2 * 3;这和你想的不一样。
所以宏通常要写很多括号:
c
#define DOUBLE(x) ((x) + (x))但即使加括号,也解决不了 i++ 被求值多次的问题。
调试不同
宏展开后就不是函数了。
c
#define LOG(x) cout << x << endl调试时你很难像普通函数一样进入宏内部。 错误信息也可能指向展开后的代码,让人不舒服。
内联函数是函数:
c
inline void Log(int value)
{
cout << value << endl;
}它更符合正常调试体验,也更容易看调用栈和类型错误。
作用域不同
宏不受 C++ 作用域限制。
c
#define VALUE 100从定义位置开始,到 #undef 或文件结束前,它都会影响后面的代码。
内联函数可以放在:
命名空间里
类里
文件里例如:
c
namespace MathUtil
{
inline int Add(int a, int b)
{
return a + b;
}
}调用:
c
int result = MathUtil::Add(1, 2);作用域清晰很多。
性能上有什么区别
宏没有函数调用开销,因为它直接展开文本。 内联函数如果被编译器展开,也没有普通函数调用开销。
但是注意:
c
宏一定展开。
inline 不一定展开。
没写 inline 的小函数也可能被编译器自动展开。所以现代 C++ 里,不要为了性能盲目用宏。 编译器优化已经很强,小函数用 inline 或直接写普通函数通常更好。
宏还能做内联函数做不了的事
宏并不是完全没用。 有些事情只有预处理器能做。
比如条件编译:
c
#ifdef DEBUG
#define LOG(msg) std::cout << msg << std::endl
#else
#define LOG(msg)
#endif平台差异:
c
#ifdef _WIN32
#define PLATFORM_NAME "Windows"
#else
#define PLATFORM_NAME "Other"
#endif日志里拿文件名和行号:
c
#include <iostream>
using namespace std;
#define LOG_ERROR(msg) \
cout << "[Error] " << __FILE__ << ":" << __LINE__ << " " << msg << endl
int main()
{
LOG_ERROR("加载失败");
return 0;
}还有:
c
字符串化 #
符号拼接 ##
条件编译 #if / #ifdef
头文件保护 #ifndef / #define / #endif这些是宏的强项。
现代 C++ 推荐怎么替代宏
常量宏:
c
#define MAX_LEVEL 100更推荐:
c
constexpr int MaxLevel = 100;函数宏:
c
#define SQUARE(x) ((x) * (x))更推荐:
c
template <typename T>
constexpr T Square(T x)
{
return x * x;
}开关宏:
c
#ifdef DEBUG这类仍然可以用宏,因为它属于条件编译。
常见对比表
c
处理阶段:
宏:预处理阶段。
inline:编译阶段。
本质:
宏:文本替换。
inline:真正函数。
类型检查:
宏:没有。
inline:有。
作用域:
宏:不遵守普通 C++ 作用域。
inline:遵守作用域。
副作用:
宏:参数可能被求值多次。
inline:参数正常求值一次。
调试:
宏:不友好。
inline:更像普通函数。
性能:
宏:直接展开。
inline:可能展开,最终由编译器决定。
适用:
宏:条件编译、平台适配、日志元信息。
inline:短小频繁调用函数。面试高分回答
可以这样答:
TIP
宏和内联函数最大的区别是处理阶段不同。宏在预处理阶段做文本替换,它不是函数,没有类型检查,也不遵守普通 C++ 的作用域规则,所以容易出现优先级问题和参数被多次求值的问题,比如 SQUARE(i++) 会导致 i++ 执行多次。
内联函数是真正的函数,参与编译器类型检查,有作用域,可以调试,也可以重载。inline 只是建议编译器内联展开,最终是否展开由编译器决定。
现代 C++ 中,如果只是为了写短小函数或常量,通常更推荐 inline、constexpr 或模板,而不是函数式宏。
但是宏仍然有存在价值,比如条件编译、平台适配、日志中使用 FILE 和 LINE、头文件保护等,这些是内联函数做不了的。
最短记忆版
宏 = 预处理文本替换,快但不安全。
inline = 真正函数,有类型检查和作用域,是否展开由编译器决定。
函数逻辑优先用 inline/constexpr/template,条件编译才用宏。new/delete 和 malloc/free 区别是什么?
一句话理解:malloc/free 只负责申请和释放“原始内存”;new/delete 不仅申请和释放内存,还会调用构造函数和析构函数,真正管理 C++ 对象生命周期。
最核心区别
malloc/free:
只管内存,不管对象。
new/delete:
既管内存,也管对象构造和析构。比如一个类:
c
#include <iostream>
using namespace std;
class Player
{
public:
Player()
{
cout << "Player 构造函数" << endl;
}
~Player()
{
cout << "Player 析构函数" << endl;
}
};用 new/delete:
c
int main()
{
// new 会申请内存,并调用构造函数
Player* p = new Player();
// delete 会调用析构函数,并释放内存
delete p;
return 0;
}输出:
c
Player 构造函数
Player 析构函数用 malloc/free:
c
#include <cstdlib>
int main()
{
// malloc 只申请一块 sizeof(Player) 大小的原始内存
// 不会调用 Player 构造函数
Player* p = (Player*)malloc(sizeof(Player));
// free 只释放内存
// 不会调用 Player 析构函数
free(p);
return 0;
}这里不会自动执行构造和析构。 所以如果类里面管理资源,malloc/free 会很危险。
new 做了什么
c
Player* p = new Player();大概分两步:
1. 申请足够放 Player 的内存。
2. 在这块内存上调用 Player 构造函数。delete p 大概也是两步:
1. 调用 Player 析构函数。
2. 释放这块内存。所以 new/delete 是 C++ 对象语义的一部分。
malloc 做了什么
c
Player* p = (Player*)malloc(sizeof(Player));它只做一件事:
申请一块指定字节数的内存。它不知道你要放的是不是 Player。 它只知道:
c
你要 sizeof(Player) 这么多字节。free(p) 也只做一件事:
释放这块内存。它不知道对象需不需要析构。
类型安全不同
new 知道类型:
c
Player* p = new Player();你不需要写 sizeof(Player),也不需要强转。
malloc 返回的是 void*:
c
Player* p = (Player*)malloc(sizeof(Player));在 C++ 里通常需要强制转换。 而且你要自己写大小:
c
sizeof(Player)如果大小写错,就会很危险。
失败行为不同
new 默认失败会抛异常:
c
try
{
int* p = new int[100000000000];
}
catch (const std::bad_alloc& e)
{
cout << "内存申请失败" << endl;
}malloc 失败会返回 NULL:
c
int* p = (int*)malloc(sizeof(int) * 100);
if (p == NULL)
{
// 内存申请失败
}所以:
c
new:默认抛 std::bad_alloc。
malloc:失败返回 NULL。当然,new 也可以写成不抛异常版本:
c
#include <new>
int* p = new (std::nothrow) int[100];
if (p == nullptr)
{
// 申请失败
}必须成对使用
这点非常重要。
c
new 配 delete
new[] 配 delete[]
malloc 配 free正确:
c
int* a = new int;
delete a;
int* arr = new int[10];
delete[] arr;
int* p = (int*)malloc(sizeof(int));
free(p);错误:
c
int* a = new int;
free(a); // 错误
int* p = (int*)malloc(sizeof(int));
delete p; // 错误
int* arr = new int[10];
delete arr; // 错误,应该 delete[]这些属于未定义行为。 可能看起来没崩,但本质是错的。
new[] 和 delete[]
如果创建数组:
c
Player* players = new Player[3];会调用 3 次构造函数。
释放:
c
delete[] players;会调用 3 次析构函数。
如果你写成:
c
delete players;就错了。 因为 delete 只适合单个对象,delete[] 才适合数组。
malloc 能不能创建对象
严格说,malloc 只能申请内存,不能完成对象构造。
如果你真的想在 malloc 的内存上构造对象,需要 placement new:
c
#include <cstdlib>
#include <new>
int main()
{
// 申请原始内存
void* memory = malloc(sizeof(Player));
// 在这块内存上手动调用构造函数
Player* p = new (memory) Player();
// 手动调用析构函数
p->~Player();
// 释放原始内存
free(memory);
return 0;
}这个写法一般底层库、内存池、引擎底层才会用。 初学者记住:普通 C++ 对象优先用 new/delete 或更现代的智能指针和容器。
malloc 有 realloc,new 没有直接等价物
C 里可以:
c
int* p = (int*)malloc(sizeof(int) * 10);
// 扩容到 20 个 int
p = (int*)realloc(p, sizeof(int) * 20);new 没有直接对应的 realloc。 C++ 里更推荐:
c
std::vector<int> nums;
nums.push_back(1);
nums.push_back(2);让容器自己管理扩容。
现代 C++ 更推荐什么
在现代 C++ 里,很多时候不推荐你直接裸写 new/delete。
更推荐:
c
std::vector
std::string
std::unique_ptr
std::shared_ptr
RAII 对象例子:
c
#include <memory>
int main()
{
// 自动释放,不需要手动 delete
std::unique_ptr<Player> player = std::make_unique<Player>();
return 0;
}离开作用域时,unique_ptr 会自动释放对象。 这比手动 delete 更安全。
数组更推荐:
c
#include <vector>
int main()
{
std::vector<int> nums;
nums.push_back(10);
nums.push_back(20);
return 0;
}而不是:
c
int* nums = new int[100];
delete[] nums;常见对比表
c
所属语言:
new/delete:C++ 运算符。
malloc/free:C 标准库函数。
申请单位:
new:按类型申请。
malloc:按字节数申请。
构造函数:
new:会调用。
malloc:不会调用。
析构函数:
delete:会调用。
free:不会调用。
返回类型:
new:返回具体类型指针。
malloc:返回 void*。
失败行为:
new:默认抛 std::bad_alloc。
malloc:返回 NULL。
重载:
new/delete:可以重载 operator new/delete。
malloc/free:不能像运算符那样重载。
数组:
new[] 配 delete[]。
malloc 配 free。
重新分配:
malloc 家族有 realloc。
new 没有直接 realloc。常见坑
c
1. malloc 创建 C++ 对象,忘了构造函数不会执行。
2. free 释放 C++ 对象,忘了析构函数不会执行。
3. new 和 free 混用。
4. malloc 和 delete 混用。
5. new[] 后用 delete,而不是 delete[]。
6. 手动管理内存后忘记释放,造成泄漏。
7. 异常路径提前返回,delete 没执行。面试高分回答
可以这样答:
NOTE
new/delete 是 C++ 的运算符,malloc/free 是 C 的库函数。
malloc 只根据字节数申请一块原始内存,返回 void*,不会调用构造函数;free 只释放内存,也不会调用析构函数。
new 会根据类型自动计算大小,申请内存后调用构造函数;delete 会先调用析构函数,再释放内存。所以 new/delete 管的是对象生命周期,而 malloc/free 管的是原始内存。
失败时,new 默认抛 std::bad_alloc,malloc 返回 NULL。
使用时必须成对匹配:new 配 delete,new[] 配 delete[],malloc 配 free,混用是未定义行为。
现代 C++ 中,普通业务代码更推荐用 RAII、智能指针、vector、string 等容器来管理资源,减少手动 new/delete。
最短记忆版
c
malloc/free = 申请/释放字节内存,不构造不析构。
new/delete = 申请/释放对象内存,会构造会析构。
new 配 delete,new[] 配 delete[],malloc 配 free,绝不能混用。栈和堆区别是什么?
一句话理解: 栈是函数调用时自动分配、自动释放的内存区域;堆是程序运行时动态申请、需要手动或用 RAII 管理释放的内存区域。
零基础理解
栈像“临时桌面”。 函数进来时,把临时变量放桌上;函数结束时,桌面自动清空。
堆像“仓库”。 你需要多少空间,就去仓库申请;用完后必须归还,否则仓库会越来越乱、越来越满。
栈:自动、快、生命周期短。
堆:灵活、容量大、生命周期可控,但要管理好释放。栈是什么
函数调用时,会创建一块“栈帧”。
栈帧里通常放:
函数参数
局部变量
返回地址
一些临时数据例子:
c
#include <iostream>
using namespace std;
void Func()
{
// x 是局部变量,通常在栈上
int x = 10;
cout << x << endl;
} // Func 结束后,x 自动销毁
int main()
{
Func();
return 0;
}x 不需要你手动释放。 函数结束后,它的生命周期就结束了。
堆是什么
堆用于动态申请内存。
c
#include <iostream>
using namespace std;
int main()
{
// 在堆上创建一个 int
int* p = new int(10);
cout << *p << endl;
// 用完后必须释放
delete p;
return 0;
}这里:
p 这个指针变量本身通常在栈上。
p 指向的 int 对象在堆上。这句话很重要。 很多初学者会混:
c
int* p = new int(10);不是说 p 在堆上。 而是说:
p 是一个局部指针变量,通常在栈上。
new int(10) 创建出来的那个 int 在堆上。生命周期不同
栈:
c
void CreatePlayer()
{
Player player;
} // 函数结束,player 自动析构堆:
c
void CreatePlayer()
{
Player* player = new Player();
} // 函数结束,player 指针变量没了,但堆上的 Player 还在上面这段堆代码有问题: player 指针变量消失了,但堆上的对象没有 delete,你再也找不到它了,造成内存泄漏。
正确写法:
c
void CreatePlayer()
{
Player* player = new Player();
// 使用 player
// ...
delete player;
}更现代的 C++ 写法:
c
#include <memory>
void CreatePlayer()
{
// unique_ptr 会自动释放堆对象
std::unique_ptr<Player> player = std::make_unique<Player>();
} // 离开作用域时自动 delete速度不同
通常:
栈分配更快。
堆分配更慢。为什么?
栈的分配大概就是移动一下栈指针:
函数进入:栈指针移动,开一块栈帧。
函数退出:栈指针移回来。堆分配要复杂一些:
找一块合适大小的空闲内存
记录分配信息
处理碎片
可能涉及锁
释放时还要归还给堆管理器所以高频小对象如果频繁 new/delete,可能会有性能问题。 游戏开发里常用对象池,就是为了减少堆上频繁申请和释放。
空间大小不同
栈一般比较小。 如果你在栈上放一个特别大的数组,可能栈溢出。
c
void Bad()
{
// 很危险:数组太大可能导致栈溢出
int bigArray[10000000];
}更适合放到堆上:
c
#include <vector>
void Good()
{
// vector 对象本身通常在栈上
// 但它内部的大块数据通常在堆上
std::vector<int> bigArray(10000000);
}注意这个细节也很高频:
c
std::vector<int> v;
v 这个对象本身可以在栈上。
但 v 内部管理的动态数组通常在堆上。栈和堆的风险不同
栈常见风险:
递归太深导致栈溢出
返回局部变量地址
局部变量生命周期结束后继续使用错误例子:
c
int* Bad()
{
int x = 10;
// 错误:返回局部变量地址
// 函数结束后 x 已经销毁
return &x;
}堆常见风险:
c
内存泄漏
重复释放
释放后继续使用
忘记 delete[]
new/free 混用错误例子:
c
void Bad()
{
int* p = new int(10);
delete p;
// 错误:p 指向的内存已经释放
// 这叫悬空指针
cout << *p << endl;
}栈对象和堆对象构造析构
栈对象:
c
#include <iostream>
using namespace std;
class Player
{
public:
Player()
{
cout << "构造 Player" << endl;
}
~Player()
{
cout << "析构 Player" << endl;
}
};
void Test()
{
Player p;
} // 自动调用析构函数
int main()
{
Test();
return 0;
}堆对象:
c
void Test()
{
Player* p = new Player();
// 必须 delete 才会调用析构函数
delete p;
}如果忘记 delete,析构函数不会执行,资源可能泄漏。
什么时候用栈
适合用栈:
小对象
短生命周期
只在当前作用域使用
不需要跨函数长期保存
创建销毁非常频繁但对象很小例子:
c
void Move()
{
Vector3 direction;
Vector3 velocity;
float speed;
}这些临时计算数据很适合放栈上。
什么时候用堆
适合用堆:
对象很大
数量运行时才知道
生命周期需要跨函数或跨作用域
需要动态扩容
需要共享所有权或延迟释放例子:
c
std::vector<int> scores;
std::unique_ptr<Player> player;
std::shared_ptr<Texture> texture;现代 C++ 不建议到处裸 new/delete。 优先用:
c
std::vector
std::string
std::unique_ptr
std::shared_ptr
对象池
RAII 封装常见误区
1. 以为 new 出来的指针变量在堆上,不准确,指针变量本身可能在栈上。
2. 以为栈一定安全,不对,返回局部变量地址就很危险。
3. 以为堆一定慢,不绝对,要看分配频率、内存池和访问模式。
4. 以为 vector 对象在堆上,不一定,vector 对象本身可能在栈上,但内部缓冲区通常在堆上。
5. 以为函数结束堆对象会自动释放,不对,裸指针 new 出来的对象必须释放。面试高分回答
可以这样答:
NOTE
栈和堆的主要区别在生命周期、分配方式和管理方式。
栈跟函数调用绑定,函数进入时创建栈帧,局部变量和参数通常放在栈帧中,函数退出时自动释放,所以栈分配速度快,但空间通常较小,递归过深或大数组可能导致栈溢出。
堆用于动态内存分配,比如 new、malloc 或容器内部动态扩容。堆上的对象生命周期不受当前函数作用域限制,可以跨函数存在,但需要程序员手动 delete/free,或者用智能指针、容器、RAII 自动管理,否则会出现内存泄漏、悬空指针、重复释放等问题。
现代 C++ 中,小而短生命周期的对象优先放栈上;需要动态大小、较大对象、跨作用域生命周期时使用堆,但通常通过 vector、unique_ptr、shared_ptr 或对象池管理,而不是裸 new/delete。
最短记忆版
c
栈 = 自动分配释放,快,小,生命周期随作用域结束。
堆 = 动态申请释放,灵活,大,生命周期由程序控制。
小而短用栈,大而长用堆;现代 C++ 用 RAII 管堆。野指针和悬空指针是什么?
一句话理解: 野指针是“没有初始化,里面是乱地址的指针”;悬空指针是“原来指向有效对象,但对象已经被释放或失效,指针还指着旧地址”。
野指针是什么
野指针就是没有被正确初始化的指针。
c
#include <iostream>
using namespace std;
int main()
{
// 错误:p 没有初始化,里面可能是随机地址
int* p;
// 非常危险:你不知道 p 指向哪里
// *p = 100;
return 0;
}这里的 p 不是空指针。 它里面可能是一个随机值,这个随机值会被当成地址。
所以野指针的危险是:
它可能指向任何地方。
你一解引用,就可能崩溃,也可能悄悄改坏别的内存。正确写法:
c
int* p = nullptr;
if (p != nullptr)
{
*p = 100;
}悬空指针是什么
悬空指针是曾经指向有效对象,但对象已经不存在了。
c
#include <iostream>
using namespace std;
int main()
{
int* p = new int(10);
delete p;
// 这里 p 还保存着旧地址
// 但那块内存已经被释放了
// *p = 20; // 错误:悬空指针
p = nullptr;
return 0;
}delete p 释放的是 p 指向的堆内存。 但 p 这个指针变量本身还在,它还保存旧地址。
所以释放后最好:
c
p = nullptr;这样再使用前可以判断:
c
if (p != nullptr)
{
*p = 20;
}两者区别
野指针:
从一开始就没有指向合法对象。
典型原因:未初始化。
悬空指针:
以前指向合法对象,但对象生命周期已经结束。
典型原因:delete/free 后继续使用。简单记:
野指针 = 不知道指哪。
悬空指针 = 还指着已经没了的东西。返回局部变量地址也会产生悬空指针
c
int* Bad()
{
int x = 10;
// 错误:返回局部变量地址
// 函数结束后 x 就销毁了
return &x;
}
int main()
{
int* p = Bad();
// p 指向的 x 已经不存在了
// cout << *p << endl; // 错误
return 0;
}x 是局部变量,通常在栈上。 函数结束后,栈帧销毁,x 的生命周期结束。 此时 p 就成了悬空指针。
正确做法可以是返回值:
c
int Good()
{
int x = 10;
// 返回值会拷贝或被优化
return x;
}或者让调用者传入参数:
c
void Good(int& outValue)
{
outValue = 10;
}free 后继续用也是悬空指针
c
#include <cstdlib>
int main()
{
int* p = (int*)malloc(sizeof(int));
*p = 10;
free(p);
// 错误:p 指向的内存已经释放
// *p = 20;
p = nullptr;
return 0;
}delete 和 free 都一样: 释放后继续访问,就是悬空指针问题,也叫 use after free。
为什么危险
野指针和悬空指针都会导致未定义行为。
未定义行为的意思是:
可能立刻崩溃
可能看起来正常
可能改坏别的变量
可能隔很久才出问题
不同机器结果不同
Debug 和 Release 结果不同这也是 C/C++ 难调试的地方。 错误发生在 A 行,崩溃可能出现在很远的 B 行。
常见产生原因
1. 指针未初始化
2. delete/free 后继续使用
3. 返回局部变量地址
4. 数组越界写坏了指针
5. 容器扩容后继续使用旧指针或旧迭代器
6. 对象生命周期结束后仍然保存它的地址容器例子:
c
#include <vector>
using namespace std;
int main()
{
vector<int> nums;
nums.push_back(1);
int* p = &nums[0];
// push_back 可能导致 vector 扩容
// 扩容后原来的内部数组会被释放
nums.push_back(2);
nums.push_back(3);
nums.push_back(4);
// p 可能已经失效
// *p = 100; // 危险
return 0;
}vector 扩容后,原来的内存可能搬家。 旧指针、旧引用、旧迭代器就可能失效。
怎么避免
1. 指针声明时初始化为 nullptr
2. 使用前判断 nullptr
3. delete/free 后立刻置 nullptr
4. 不返回局部变量地址
5. 尽量不用裸指针表达所有权
6. 优先使用智能指针、容器、RAII
7. 注意 vector 扩容导致指针/引用/迭代器失效现代 C++ 推荐:
c
#include <memory>
int main()
{
// unique_ptr 自动管理对象生命周期
std::unique_ptr<int> p = std::make_unique<int>(10);
*p = 20;
// 离开作用域自动释放,不需要手动 delete
return 0;
}数组推荐:
c
#include <vector>
int main()
{
std::vector<int> nums;
nums.push_back(1);
nums.push_back(2);
return 0;
}比起裸 new[] / delete[],vector 更安全。
注意:置 nullptr 不是万能药
c
int* p1 = new int(10);
int* p2 = p1;
delete p1;
p1 = nullptr;
// p2 仍然是悬空指针
// *p2 = 20; // 仍然危险这说明一个对象可能被多个指针指向。 你只把 p1 置空,不代表 p2 也安全。
所以更根本的解决方式是:
明确所有权
减少裸指针共享
使用智能指针面试高分回答
IMPORTANT
野指针是指没有初始化或指向未知地址的指针,比如 int* p; 之后直接使用 p。它从一开始就没有指向合法对象。
悬空指针是指曾经指向合法对象,但对象已经被释放或生命周期结束,指针还保存旧地址,比如 delete p 后继续使用 p,或者返回局部变量地址。
两者本质上都会导致非法内存访问,属于未定义行为,可能崩溃,也可能悄悄破坏内存。
避免方式是:指针初始化为 nullptr,使用前判断,释放后置空,不返回局部变量地址;现代 C++ 中更推荐使用 RAII、智能指针和标准容器来管理对象生命周期,减少裸指针所有权问题。
最短记忆版
c
野指针 = 没初始化,不知道指哪。
悬空指针 = 对象没了,指针还指着旧地址。
解决:初始化 nullptr,释放后置空,别返回局部地址,优先 RAII/智能指针。内存泄漏如何排查?
一句话理解: 内存泄漏就是“申请了资源,但后来没有释放,而且程序还失去了管理它的机会”。排查时不要靠猜,要先复现内存持续增长,再用工具找到是哪一行申请后没释放。
什么是内存泄漏
最简单的例子:
c
void Bad()
{
int* p = new int(10);
// 忘记 delete
}函数结束后,p 这个指针变量没了。 但是 new int(10) 申请的堆内存还在。
此时程序已经找不到那块内存了,也就没办法释放它。 这就是内存泄漏。
第一步:先确认是不是泄漏
不是内存涨了就一定是泄漏。
有些情况是正常的:
资源缓存
对象池预热
内存分配器缓存
vector 扩容后暂时不缩容
系统库延迟释放真正像泄漏的现象是:
重复执行同一个流程
内存持续上涨
流程结束后不下降
多跑几轮越来越高比如游戏里可以这样复现:
进入战斗 → 退出战斗
进入战斗 → 退出战斗
进入战斗 → 退出战斗如果每次退出后内存都比上一次高,而且不回落,就要怀疑泄漏。
第二步:用工具定位
C/C++ 常用工具:
AddressSanitizer:检测泄漏、越界、Use After Free
Valgrind:Linux 常用内存检测工具
Visual Studio Diagnostic Tools:Windows 下看内存快照
CRT Debug Heap:MSVC 下检测 new/malloc 泄漏
Dr.Memory:Windows/Linux 都可用排查思路:
1. 跑一遍正常流程
2. 记录内存快照 A
3. 执行可疑操作,比如进入退出场景
4. 记录内存快照 B
5. 对比新增对象和未释放分配
6. 看分配调用栈重点是找到:
谁申请的?
什么时候申请的?
应该由谁释放?
为什么没有释放?典型泄漏 1:new 后忘记 delete
c
class Player
{
public:
int hp = 100;
};
void CreatePlayer()
{
Player* player = new Player();
// 使用 player
// ...
// 错误:忘记 delete
}修复:
c
void CreatePlayer()
{
Player* player = new Player();
// 使用 player
// ...
delete player;
player = nullptr;
}但更推荐现代 C++ 写法:
c
#include <memory>
void CreatePlayer()
{
// unique_ptr 会在离开作用域时自动释放
std::unique_ptr<Player> player = std::make_unique<Player>();
// 使用 player
// 不需要手动 delete
}典型泄漏 2:提前 return 导致释放没执行
c
bool LoadData()
{
char* buffer = new char[1024];
if (!ReadFile(buffer))
{
// 错误:提前 return,buffer 没释放
return false;
}
delete[] buffer;
return true;
}修复方式一:
c
bool LoadData()
{
char* buffer = new char[1024];
if (!ReadFile(buffer))
{
delete[] buffer;
return false;
}
delete[] buffer;
return true;
}更推荐:
c
#include <vector>
bool LoadData()
{
// vector 自动管理内存
std::vector<char> buffer(1024);
if (!ReadFile(buffer.data()))
{
return false;
}
return true;
}这样不管从哪里 return,都不会泄漏。
典型泄漏 3:异常路径没释放
c
void Bad()
{
Resource* res = new Resource();
DoSomething(); // 如果这里抛异常,下面 delete 不会执行
delete res;
}修复:
c
#include <memory>
void Good()
{
// 异常发生时,unique_ptr 也会自动释放
std::unique_ptr<Resource> res = std::make_unique<Resource>();
DoSomething();
}这就是 RAII 的价值: 资源交给对象管理,对象离开作用域时自动释放。
典型泄漏 4:容器一直持有对象
c
#include <vector>
std::vector<Player*> players;
void AddPlayer()
{
Player* p = new Player();
players.push_back(p);
}如果一直 push_back,但从不释放:
players 越来越大
Player 对象越来越多
内存一直涨修复:
c
void ClearPlayers()
{
for (Player* p : players)
{
delete p;
}
players.clear();
}更推荐:
c
#include <memory>
#include <vector>
std::vector<std::unique_ptr<Player>> players;
void AddPlayer()
{
players.push_back(std::make_unique<Player>());
}
void ClearPlayers()
{
// clear 时 unique_ptr 自动释放 Player
players.clear();
}典型泄漏 5:shared_ptr 循环引用
c
#include <memory>
class B;
class A
{
public:
std::shared_ptr<B> b;
};
class B
{
public:
std::shared_ptr<A> a;
};如果 A 持有 B,B 又持有 A,引用计数永远不为 0。 对象就释放不了。
修复:其中一边用 weak_ptr。
c
#include <memory>
class B;
class A
{
public:
std::shared_ptr<B> b;
};
class B
{
public:
// weak_ptr 不增加引用计数,避免循环引用
std::weak_ptr<A> a;
};典型泄漏 6:逻辑泄漏
有时不是 new/delete 层面的问题,而是“逻辑上一直持有”。
比如:
资源缓存只加不删
事件监听不取消
全局 Map 一直存对象
对象池只进不出
场景切换后 Manager 还引用旧场景对象这种也会导致内存越来越高。
例如事件没取消订阅:
c
// 伪代码
EventBus::Subscribe("OnDamage", this);对象销毁时没有:
c
EventBus::Unsubscribe("OnDamage", this);事件系统还持有这个对象,导致它释放不了。 这类问题在游戏客户端特别常见。
Windows 下简单用 CRT 检测泄漏
MSVC 可以这样:
c
#ifdef _MSC_VER
#define _CRTDBG_MAP_ALLOC
#include <crtdbg.h>
#endif
int main()
{
#ifdef _MSC_VER
// 程序退出时打印内存泄漏信息
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
int* p = new int(10);
// 故意不 delete,用来测试泄漏报告
return 0;
}实际项目中,可以配合分配编号定位具体泄漏点。 不过更现代的方式是用 AddressSanitizer 或 IDE 内存快照。
AddressSanitizer 会帮你看什么
ASan 可以检测:
内存泄漏
堆越界
栈越界
释放后使用 Use After Free
重复释放你运行程序后,它通常会告诉你:
哪块内存泄漏了
大小是多少
在哪个调用栈分配的面试里说出“看分配调用栈”很加分。 因为排查泄漏最重要的不是知道泄漏了,而是知道谁申请的。
排查流程总结
1. 构造稳定复现路径
2. 观察内存是否持续上涨
3. 用工具抓快照或泄漏报告
4. 找到未释放对象和分配调用栈
5. 分析对象所有权
6. 检查异常、return、场景切换、缓存、事件订阅
7. 用 RAII、智能指针、容器或明确释放修复
8. 再次跑工具确认泄漏消失常见坑
c
1. 只看内存上涨,不判断是否是缓存
2. 只修 delete,不分析所有权
3. new[] 后忘记 delete[]
4. malloc 后忘记 free
5. shared_ptr 循环引用
6. 容器 clear 了指针,但没 delete 指向对象
7. 事件订阅没取消
8. 异常或提前 return 路径漏释放
9. 修完不复测面试高分回答
CAUTION
我排查内存泄漏会先复现问题,而不是直接猜代码。比如重复进入和退出某个场景,观察内存是否持续上涨并且不回落。
确认后,我会用 AddressSanitizer、Valgrind、Visual Studio Diagnostic Tools 或 CRT Debug Heap 这类工具抓内存快照和泄漏报告,重点看未释放内存的分配调用栈。
定位到分配点后,我会分析所有权:谁申请,谁释放,对象生命周期应该到哪里结束。重点检查提前 return、异常路径、new/delete 是否成对、new[]/delete[] 是否匹配、malloc/free 是否匹配。
如果是 C++ 项目,我会优先用 RAII 修复,比如 unique_ptr、shared_ptr/weak_ptr、vector、string 或自定义资源管理类,尽量避免裸 new/delete。
如果是逻辑泄漏,还会检查缓存、事件订阅、全局容器、对象池、场景切换后的引用残留。最后修复后一定再跑一遍工具,确认泄漏报告消失。
最短记忆版
c
内存泄漏排查 = 复现内存持续增长 → 工具看分配调用栈 → 分析所有权 → 用 RAII/智能指针/正确释放修复 → 再验证。