C++ 第一阶段:对象、内存与生命周期

C++ 第一阶段:对象、内存与生命周期

目标:面向 Windows C++ 客户端 / Qt / CEF / 基础软件 岗位,掌握第一阶段最核心的对象模型,并达到中级 C++ 面试可表达的程度。


1. 对象、内存、生命周期:先把三个概念分开

C++ 中最容易混淆的是:

text 复制代码
对象 ≠ 内存

例如:

cpp 复制代码
MyClass* p = new MyClass;

概念上可以拆成:

text 复制代码
获得存储空间
    ↓
在这块空间中构造 MyClass 对象

而:

cpp 复制代码
delete p;

概念上是:

text 复制代码
调用析构函数
    ↓
结束对象生命周期
    ↓
释放动态存储空间

所以:

构造函数负责初始化对象,不等于负责给对象本身申请内存。

析构函数负责对象销毁前的清理,不等于"直接释放对象那块内存"。


面试:new/deletemalloc/free 的核心区别

text 复制代码
malloc/free
→ 管理原始内存
→ 不自动调用构造/析构

new/delete
→ 参与 C++ 对象创建和销毁
→ new 会初始化对象
→ delete 会先析构,再释放存储

注意:

"new 最终一定调用 Windows HeapAlloc"不是 C++ 标准规定,这是实现层问题。


2. 栈、堆与存储期

工程里常说:

text 复制代码
栈对象
堆对象

但标准层面更准确的是:

text 复制代码
automatic storage duration
dynamic storage duration

例如:

cpp 复制代码
void Test()
{
    MyClass a;
}

a 是自动存储期对象,离开作用域时生命周期自动结束。

cpp 复制代码
MyClass* p = new MyClass;

这里:

text 复制代码
p

通常是局部指针变量;

text 复制代码
p 指向的 MyClass

才是动态存储期对象。

所以不要说:

p 在堆上。

更准确:

p 是局部指针,p 指向的对象具有动态存储期。


3. 指针、引用、所有权不是一回事

cpp 复制代码
int value = 10;
int* p = &value;
int& ref = value;

区别:

项目 指针 引用
可以为空 可以 普通引用必须绑定对象
可以改为指向其他对象 可以 不能重新绑定
语义 地址关系 对象别名
是否天然拥有对象

最重要的一点:

指针和引用都不天然表示资源所有权。

例如:

cpp 复制代码
QObject* obj;
HWND hwnd;
FILE* file;

看到这些成员时,必须继续判断:

text 复制代码
谁创建?
谁销毁?
当前对象是否拥有它?
只是观察,还是负责释放?

这就是客户端工程里真正重要的生命周期判断。


4. 空指针、悬空指针、野指针

空指针

cpp 复制代码
int* p = nullptr;

明确表示当前不指向有效对象。


悬空指针

cpp 复制代码
int* p = new int(10);

delete p;

此时:

text 复制代码
p 变量还存在
但原对象已经销毁

继续:

cpp 复制代码
*p = 20;

属于 未定义行为(UB)


野指针

cpp 复制代码
int* p;

未初始化就使用:

cpp 复制代码
*p = 10;

同样危险。

工程中裸指针至少应优先初始化为:

cpp 复制代码
nullptr

delete 后设 nullptr 能解决全部问题吗?

不能。

cpp 复制代码
int* p1 = new int(10);
int* p2 = p1;

delete p1;
p1 = nullptr;

此时:

text 复制代码
p2 仍然是悬空指针

所以真正重要的不是"释放后置空",而是:

明确资源所有权,避免多个地方误以为自己负责释放。


5. const:看清楚限制的是谁

指向 const 的指针

cpp 复制代码
const int* p = &value;

不能:

cpp 复制代码
*p = 20;

但可以:

cpp 复制代码
p = nullptr;

限制的是:

text 复制代码
不能通过 p 修改它指向的对象

const 指针

cpp 复制代码
int* const p = &value;

可以:

cpp 复制代码
*p = 20;

不能:

cpp 复制代码
p = nullptr;

限制的是:

text 复制代码
p 自己不能改指向

常见函数参数

cpp 复制代码
void Print(const std::string& text);

目的:

text 复制代码
引用 → 避免复制
const → 不允许函数修改原对象

对于 int/double/pointer 这种小对象,通常直接按值传递即可。


6. 构造、析构与初始化列表

构造函数

cpp 复制代码
class Date
{
public:
    Date(int year, int month)
        : m_year(year),
          m_month(month)
    {
    }

private:
    int m_year;
    int m_month;
};

构造函数的核心职责:

让刚进入生命周期的对象处于合法、合理的初始状态。


初始化列表为什么重要

cpp 复制代码
Date::Date(int year)
    : m_year(year)
{
}

这里是:

text 复制代码
直接初始化成员

而:

cpp 复制代码
Date::Date(int year)
{
    m_year = year;
}

函数体执行时,成员已经完成了自己的初始化过程,然后才进行赋值。


哪些成员必须通过初始化列表处理

典型:

cpp 复制代码
const int m_value;
int& m_ref;

因为:

text 复制代码
const 成员不能先构造再赋值
引用必须在初始化时绑定

另外,没有默认构造函数的类类型成员,也必须显式构造。


初始化顺序

真正初始化顺序由:

text 复制代码
成员在类中的声明顺序

决定,而不是初始化列表的书写顺序。

cpp 复制代码
class A
{
public:
    A()
        : m_second(10),
          m_first(m_second)
    {
    }

private:
    int m_first;
    int m_second;
};

实际上:

text 复制代码
先 m_first
再 m_second

因此这里可能读取未初始化的 m_second


7. 拷贝构造与拷贝赋值

判断方法非常简单:

text 复制代码
目标对象正在创建
→ 拷贝构造

目标对象已经存在
→ 拷贝赋值

例如:

cpp 复制代码
Buffer a;

Buffer b = a;   // 拷贝构造

Buffer c;
c = a;          // 拷贝赋值

为什么 Buffer b = a; 不是赋值?

因为:

text 复制代码
b 原来不存在

这一行是在:

text 复制代码
定义并初始化 b

所以调用拷贝构造。


8. 默认拷贝不等于 memcpy

面试不要说:

编译器默认拷贝就是把对象按字节复制一遍。

更准确:

隐式生成的拷贝构造会对基类和非静态成员逐个执行对应的拷贝。

例如:

cpp 复制代码
class A
{
    int x;
    std::string name;
};

概念上:

text 复制代码
复制 x
调用 string 的拷贝逻辑复制 name

对于:

cpp 复制代码
char* data;

复制的是:

text 复制代码
指针值

而不是指针指向的内存。

这就是浅拷贝问题的根源。


9. 深拷贝与浅拷贝

cpp 复制代码
class Buffer
{
private:
    char* m_data;
};

默认成员级拷贝可能得到:

text 复制代码
a.m_data ─┐
          ├──→ 同一块内存
b.m_data ─┘

两个对象析构时:

text 复制代码
delete[] 同一个地址两次

导致:

text 复制代码
Double Free

属于 UB。


深拷贝

深拷贝的本质:

新对象创建独立资源,再复制资源内容。

text 复制代码
a → Resource A
b → Resource B

内容可以相同,但生命周期互相独立。


10. 为什么资源类经常禁止拷贝

例如:

cpp 复制代码
class FileSession
{
private:
    FILE* m_file;
    char* m_buffer;
};

如果它表达:

text 复制代码
独占文件
独占缓冲区

那么"复制对象"本身就没有合理语义。

C++11 以后:

cpp 复制代码
FileSession(const FileSession&) = delete;
FileSession& operator=(const FileSession&) = delete;

VS2008 / C++03 老项目常见:

cpp 复制代码
private:
    FileSession(const FileSession&);
    FileSession& operator=(const FileSession&);

所以:

text 复制代码
C++11+
→ = delete

VS2008 / C++03
→ private 声明等传统方式

11. 移动语义:转移所有权,不是复制资源

如果资源很大:

text 复制代码
100 MB buffer

深拷贝意味着:

text 复制代码
重新申请 100 MB
复制 100 MB

但如果源对象马上就不再需要资源,可以:

text 复制代码
直接把资源所有权转交给新对象

这就是移动语义。


12. std::move 本身不会移动任何资源

cpp 复制代码
std::move(a);

它本身不会:

text 复制代码
释放资源
复制资源
把 a 置空
调用移动构造

它主要做的是:

把表达式转换成可以匹配移动操作的右值类别。

例如:

cpp 复制代码
Buffer&& ref = std::move(a);

这里只是:

text 复制代码
右值引用 ref 绑定到 a

没有创建新 Buffer 对象,所以:

text 复制代码
没有移动构造
没有资源转移

13. 真正移动发生在移动构造 / 移动赋值

移动构造

cpp 复制代码
Buffer b(std::move(a));

典型实现:

cpp 复制代码
Buffer::Buffer(Buffer&& other) noexcept
    : m_data(other.m_data),
      m_size(other.m_size)
{
    other.m_data = nullptr;
    other.m_size = 0;
}

过程:

text 复制代码
b 接管 a 的资源
    ↓
a 放弃所有权

注意:

a 对象还存在,只是状态改变了。


移动赋值

cpp 复制代码
b = std::move(a);

这里:

text 复制代码
b 已经存在

所以它可能已经持有旧资源。

必须:

text 复制代码
先释放 b 的旧资源
    ↓
再接管 a 的资源
    ↓
让 a 进入可安全析构状态

否则会资源泄漏。


14. moved-from 对象是什么状态

错误理解:

text 复制代码
move 后原对象不存在

正确:

text 复制代码
原对象仍然存在
仍然会析构
只是状态发生变化

对标准库类型,常见表述:

text 复制代码
valid but unspecified state

即:

text 复制代码
对象仍然有效
但不能随意假定具体值

对于我们自己的 FileSession,如果明确写:

cpp 复制代码
other.m_file = nullptr;
other.m_buffer = nullptr;

那就是我们自己定义:

text 复制代码
移动后为空状态

15. const 为什么可能让 std::move 最终走拷贝

cpp 复制代码
const Buffer a;

Buffer b(std::move(a));

std::move(a) 保留 const 属性。

常见移动构造:

cpp 复制代码
Buffer(Buffer&&);

不能绑定到:

text 复制代码
const Buffer&&

如果类还有:

cpp 复制代码
Buffer(const Buffer&);

那么可能最终调用:

text 复制代码
拷贝构造

所以:

std::move 不保证一定发生移动。


16. 为什么移动构造常加 noexcept

cpp 复制代码
Buffer(Buffer&&) noexcept;

第一层意义:

text 复制代码
这个函数承诺不向外抛异常

第二层与 STL 有关:

cpp 复制代码
std::vector<Buffer>

扩容时,如果移动可能抛异常,而拷贝可用,为了异常安全,实现通常可能倾向拷贝。

所以对于只做:

text 复制代码
指针/句柄转移

的移动构造,若确实不会抛异常,应标记:

cpp 复制代码
noexcept

17. RVO / NRVO

cpp 复制代码
Buffer Make()
{
    Buffer temp;
    return temp;
}

不能说:

text 复制代码
一定调用移动构造

可能发生:

text 复制代码
NRVO
→ temp 直接构造到调用者最终对象位置
→ 没有拷贝
→ 也没有移动

所以一般:

cpp 复制代码
return temp;

不要写:

cpp 复制代码
return std::move(temp);

后者可能妨碍 NRVO。


18. new[]/delete[] 必须匹配

cpp 复制代码
int* p = new int[10];
delete[] p;

错误:

cpp 复制代码
delete p;

属于 UB。

同样:

cpp 复制代码
new T

必须匹配:

cpp 复制代码
delete

19. 三类高频内存错误

Double Free

cpp 复制代码
delete p;
delete p;

同一资源重复释放。


Use-After-Free

cpp 复制代码
delete p;
p->Run();

对象生命周期已结束,还继续使用。


Memory Leak

cpp 复制代码
char* p = new char[1024];
// 丢失 p,没有 delete[]

资源失去释放机会。

这些问题在 Windows 客户端崩溃、堆损坏、长时间运行内存上涨中都很常见。


20. Windows 工程:跨 DLL 分配释放

历史 Windows C++ 项目里要特别注意:

text 复制代码
DLL A 分配
DLL B / EXE 释放

如果两边 CRT 版本或运行库配置不同,可能产生堆不匹配问题。

常见设计:

cpp 复制代码
Handle* CreateHandle();
void DestroyHandle(Handle* p);

也就是:

text 复制代码
谁创建
谁销毁

注意:

这是 Windows / CRT 工程实践,不是 C++ 标准规定。


21. FileSession:第一阶段综合理解

cpp 复制代码
class FileSession
{
public:
    FileSession(
        const std::string& path,
        std::size_t size
    );

    ~FileSession();

    FileSession(const FileSession&) = delete;
    FileSession& operator=(const FileSession&) = delete;

    FileSession(FileSession&& other) noexcept;
    FileSession& operator=(FileSession&& other) noexcept;

private:
    FILE* m_file = nullptr;
    char* m_buffer = nullptr;
    std::size_t m_bufferSize = 0;
};

你需要能解释:

text 复制代码
为什么析构?
为什么禁止拷贝?
为什么移动后源对象还存在?
为什么 std::move 不等于移动?
为什么移动赋值要先处理旧资源?
为什么 noexcept 对移动重要?

如果这些可以连续解释清楚,第一阶段基本达标。


22. 高频面试回答

指针和引用有什么区别?

指针本身是对象,可以为空,也可以重新指向其他对象;引用表示对已有对象的绑定,初始化后不能重新绑定。两者都不天然代表资源所有权,是否负责生命周期要看接口设计。


拷贝构造和拷贝赋值区别?

核心看目标对象是否已经存在。拷贝构造是用已有对象创建新对象;拷贝赋值是两个对象都已经存在,再复制状态。资源类的赋值还需要处理目标对象已有的旧资源。


什么是移动语义?

移动语义用于转移资源所有权,避免昂贵的深拷贝。移动构造或移动赋值接管源对象的资源,再把源对象调整到可安全析构的状态。移动的是资源,不是让源对象消失。


std::move 做了什么?

std::move 本身不移动资源,它主要进行类型转换,让表达式可以匹配移动操作。真正的资源转移发生在移动构造或移动赋值中。


默认拷贝是不是 memcpy

不能这样说。语言语义上是对基类和成员逐个执行对应的拷贝。某些平凡类型编译器可能优化成字节复制,但这是实现层。裸指针成员只复制地址,因此会产生浅拷贝的所有权风险。


为什么移动构造经常 noexcept

对只转移指针、句柄的移动操作,如果确实不会抛异常,应声明 noexcept。这不仅表达异常保证,也会影响 vector 等容器迁移元素时是否更愿意使用移动。


23. 第一阶段验收

必须能回答:

text 复制代码
1. 对象和内存是什么关系?
2. 指针、引用、所有权有什么区别?
3. const T* 和 T* const 区别?
4. 构造和析构什么时候发生?
5. 初始化列表和函数体赋值区别?
6. 为什么初始化顺序看声明顺序?
7. 拷贝构造和拷贝赋值区别?
8. 为什么裸指针默认拷贝可能 double free?
9. 移动构造和移动赋值区别?
10. std::move 为什么不等于移动?
11. moved-from 对象还存在吗?
12. const 对象 std::move 后为什么可能还是拷贝?
13. noexcept 为什么影响 STL?
14. return local 为什么不一定移动?
15. new/delete 和 malloc/free 核心区别?

能稳定解释这些问题,第一阶段就已经达到中级 C++ 面试要求。


总结

第一阶段最重要的不是记函数签名,而是形成这条判断链:

text 复制代码
对象在哪里?
    ↓
生命周期多长?
    ↓
它是否拥有资源?
    ↓
拷贝时资源如何处理?
    ↓
移动时所有权如何变化?
    ↓
对象销毁时谁负责清理?

最终记住四句话:

对象不等于内存。
指针不等于所有权。
默认拷贝不等于深拷贝。
std::move 不等于真正移动。

相关推荐
夜航海图2 小时前
S-57 数据解剖:把一个 ENC 文件拆给你看
c++
(Charon)2 小时前
【C++】网络缓冲区设计(三):Chain Buffer链式缓冲区、动态扩容与跨节点读写
服务器·c++
玖玥拾2 小时前
Lua 基础语法(二)
开发语言·unity·lua
王的宝库2 小时前
GO常用标准库包
开发语言·后端·golang
geovindu3 小时前
python: Face Recognition
开发语言·后端·python·人脸识别
平头哥技术团队3 小时前
Day 13 | 调 line-height 和 margin:三处间距让名片页脱离模板感
开发语言·前端·javascript·学习·html5
黑马程序员毕设3 小时前
基于Java的仪器管理系统设计与实现
java·开发语言·spring boot·后端·微信小程序
平头哥AI3 小时前
Day 13 | 一个函数回两个值:Go 的 error 是从哪冒出来的
开发语言·后端·golang
asdzx674 小时前
Python 实现 PDF 文本查找与高亮标注
开发语言·python·pdf