Skip to content

语言基础

C 和 C++ 区别是什么?

一句话理解: C 更偏“过程式 + 底层控制”,核心是函数、指针、结构体、手动内存管理;C++ 在 C 的基础上加入了类、对象、继承、多态、模板、STL、RAII 等机制,更适合组织大型复杂工程。

c-vs-cpp-difference

零基础理解

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
free

C++ 有:

new
delete
构造函数
析构函数
智能指针
RAII

C 示例:

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.h

C++ 标准库更强:

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++ 既有指针,也有引用。

pointer-vs-reference

零基础理解

假设有一个变量:

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 修饰指针时,重点看它在 * 的哪边:* 左边限制“指向的内容”,* 右边限制“指针变量本身”。

const-pointer-cases

第一种:指向常量的指针

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 放的位置不同,含义不同:放局部变量上,主要改变生命周期;放全局变量/函数前,主要限制可见范围;放类成员上,表示它属于类,而不是某个对象。

static-meanings-cpp

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++ 面试更看重第二点。

inline-keyword-cpp

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

所以很多 gettersetter 会直接写在头文件类内部。

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。

宏和内联函数区别是什么?

一句话理解: 宏是预处理阶段的“文本替换”,内联函数是编译阶段的“真正函数”。能用 inlineconstexpr、模板解决的,通常不要用函数式宏。

macro-vs-inline-function

宏是什么

宏是预处理器处理的东西。

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 或模板,而不是函数式宏。

但是宏仍然有存在价值,比如条件编译、平台适配、日志中使用 FILELINE、头文件保护等,这些是内联函数做不了的。

最短记忆版

宏 = 预处理文本替换,快但不安全。
inline = 真正函数,有类型检查和作用域,是否展开由编译器决定。
函数逻辑优先用 inline/constexpr/template,条件编译才用宏。

new/deletemalloc/free 区别是什么?

一句话理解:malloc/free 只负责申请和释放“原始内存”;new/delete 不仅申请和释放内存,还会调用构造函数和析构函数,真正管理 C++ 对象生命周期。

new-delete-vs-malloc-free

最核心区别

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 管理释放的内存区域。

stack-vs-heap

零基础理解

栈像“临时桌面”。 函数进来时,把临时变量放桌上;函数结束时,桌面自动清空。

堆像“仓库”。 你需要多少空间,就去仓库申请;用完后必须归还,否则仓库会越来越乱、越来越满。

栈:自动、快、生命周期短。
堆:灵活、容量大、生命周期可控,但要管理好释放。

栈是什么

函数调用时,会创建一块“栈帧”。

栈帧里通常放:

函数参数
局部变量
返回地址
一些临时数据

例子:

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 管堆。

野指针和悬空指针是什么?

一句话理解: 野指针是“没有初始化,里面是乱地址的指针”;悬空指针是“原来指向有效对象,但对象已经被释放或失效,指针还指着旧地址”。

wild-vs-dangling-pointer

野指针是什么

野指针就是没有被正确初始化的指针。

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

deletefree 都一样: 释放后继续访问,就是悬空指针问题,也叫 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/智能指针。

内存泄漏如何排查?

一句话理解: 内存泄漏就是“申请了资源,但后来没有释放,而且程序还失去了管理它的机会”。排查时不要靠猜,要先复现内存持续增长,再用工具找到是哪一行申请后没释放。

memory-leak-investigation

什么是内存泄漏

最简单的例子:

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 持有 BB 又持有 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/智能指针/正确释放修复 → 再验证。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0