栈和堆都是用于存储程序数据的内存区域。
new/delete
或 malloc/free
来分配和释放。程序运行时,内存大致分为以下几个区域(从低地址到高地址),每个区域负责不同的任务:
new
或 malloc
)。malloc
、new
等动态分配内存后,没有及时使用 free
或 delete
释放。如果这段内存不再被引用,也没有被释放,就会造成堆内存泄漏。virtual
,则在通过父类指针删除对象时,子类的析构函数不会被调用,导致子类分配的资源没有被释放。class Base {
~Base() { ... } // 析构函数,不是 virtual
};
class Derived : public Base {
~Derived() { ... } // 析构函数,释放子类资源
};
Base* b = new Derived();
delete b; // 问题出在这里
如果 Base 的析构函数 不是 virtual,就会发生如下情况:
Base* b = new Derived();
delete b; // ❌ 只调用 ~Base(),不会调用 ~Derived()
结果就是:
new
出来的数组)不会被释放。如何解决?将父类的析构函数设为 virtual!
class Base {
public:
virtual ~Base() { ... } // ✅ virtual
};
class Derived : public Base {
public:
~Derived() { ... } // 子类自己的析构逻辑
};
这样就不会造成内存泄露了。
Base* b = new Derived();
delete b; // ✅ 会调用 ~Derived() 然后再调用 ~Base()
总结:
是否需要写 virtual |
说明 |
---|---|
父类的析构函数 | 必须写 virtual,否则通过父类指针 delete 子类会内存泄漏 |
子类的析构函数 | 可以不写 virtual,因为继承自父类就已经是虚函数了 |
std::unique_ptr
, std::shared_ptr
):自动管理内存,生命周期自动绑定到对象上。智能指针就是一个封装了普通指针的类,自动帮你管理内存资源,在不用时自动释放,防止忘记 delete 造成内存泄漏,防止多次释放同一内存。它们定义在 C++11 的头文件 < memory > 中。
std::unique_ptr
独占指针unique_ptr
拥有。std::move()
)#include
std::unique_ptr<int> ptr = std::make_unique<int>(42);
make_unique
创建,语法简洁安全。独占指针举例:
#include
#include
class Dog {
public:
Dog() { std::cout << "Dog created\n"; }
~Dog() { std::cout << "Dog destroyed\n"; }
void bark() { std::cout << "Woof!\n"; }
};
int main() {
std::unique_ptr<Dog> dog1 = std::make_unique<Dog>(); // 创建
dog1->bark();
// std::unique_ptr dog2 = dog1; // ❌ 错误:不能复制
std::unique_ptr<Dog> dog2 = std::move(dog1); // ✅ 可以移动
if (!dog1) std::cout << "dog1 is null\n";
dog2->bark(); // 现在 dog2 拥有资源
}
输出:
Dog created
Woof!
dog1 is null
Woof!
Dog destroyed
在 main() 函数结束时,dog2 离开作用域:C++ 会自动调用 dog2 的析构函数,而 unique_ptr
的析构函数内部会调用 delete
去释放它管理的对象。这句输出:Dog destroyed
说明:dog2 在主函数结束时被销毁,自动释放了堆上的 Dog 对象。
**unique_ptr 的好处就在于 不需要你手动 delete,它会在生命周期结束时自动释放所拥有的资源,避免内存泄漏。**但注意:
如果你在函数中 new 了对象但没有用智能指针接管它的生命周期,那就需要你自己 delete,否则就内存泄漏了.
PS:
. (变量类型为对象)和 -> (变量类型为指针) 用于访问对象的成员,和是否是 class / struct 无关; ::` 用于访问作用域中的静态成员、类型名、常量等。class 和 struct 都可以用。
std::shared_ptr
共享指针shared_ptr
拷贝,引用计数 +1;每销毁一个,引用计数 -1;引用计数为 0 时,资源释放。weak_ptr
)。std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // 引用计数 +1
共享指针举例:
#include
#include
class Cat {
public:
Cat() { std::cout << "Cat created\n"; }
~Cat() { std::cout << "Cat destroyed\n"; }
void meow() { std::cout << "Meow!\n"; }
};
int main() {
std::shared_ptr<Cat> cat1 = std::make_shared<Cat>(); // 创建对象,count = 1
{
std::shared_ptr<Cat> cat2 = cat1; // count = 2
std::cout << "Use count: " << cat1.use_count() << '\n';
cat2->meow();
} // cat2 出作用域,引用计数 -1,count = 1
std::cout << "Use count after cat2: " << cat1.use_count() << '\n';
cat1->meow();
// cat1 要出作用域,count 从 1 减到 0,此时调用 delete -> 析构
}
输出:
Cat created
Use count: 2
Meow!
Use count after cat2: 1
Meow!
Cat destroyed
std::weak_ptr
弱引用指针shared_ptr
循环引用的问题shared_ptr
是否还有效,通过 .lock()
转换成 shared_ptr
使用。std::shared_ptr<int> sharedPtr = std::make_shared<int>(42);
std::weak_ptr<int> weakPtr = sharedPtr; // 不增加引用计数
使用:
if (auto sp = weakPtr.lock()) {
// 成功获得 shared_ptr,可以使用资源
}
弱引用指针举例:
#include
#include
class Person {
public:
std::shared_ptr<Person> friendPtr;
~Person() { std::cout << "Person destroyed\n"; }
};
int main() {
std::shared_ptr<Person> p1 = std::make_shared<Person>();
std::shared_ptr<Person> p2 = std::make_shared<Person>();
// 错误方式:会导致循环引用
// p1->friendPtr = p2;
// p2->friendPtr = p1;
// 正确方式:用 weak_ptr 打破循环
std::weak_ptr<Person> weak = p1;
if (auto sp = weak.lock()) { // 尝试获得 shared_ptr
std::cout << "Accessed person through weak_ptr\n";
}
return 0;
}
输出:
Accessed person through weak_ptr
Person destroyed
Person destroyed
什么是循环引用:
如果两个 shared_ptr
互相持有对方,就会造成引用计数永远不为 0,资源永远无法释放 → 内存泄漏!
总结:
类型 | 所有权 | 引用计数 | 可复制 | 适用场景 |
---|---|---|---|---|
unique_ptr |
独占 | ❌ | ❌ | 独占资源 |
shared_ptr |
共享 | ✅ | ✅ | 多个共享拥有者 |
weak_ptr |
观察者 | ✅(不+1) | ✅ | 避免循环引用,观察 shared_ptr |
野指针指的是:指向一块已经释放或者无效的内存地址的指针。 这种指针如果被错误使用,可能会导致:程序崩溃(如 segmentation fault),数据损坏,不可预测的行为(Undefined Behavior)。
int* ptr = new int;
delete ptr;
// 这时 ptr 成为野指针,仍然指向已释放的内存
虽然原来指向的地址内存已经释放,但指针 ptr
仍然指向原来的地址,这就可能误操作一块无效内存。
避免方法:
delete ptr;
ptr = nullptr; // 将指针设为 nullptr,表示不再指向任何有效内存
int* createInt() {
int x = 10;
return &x; // 错误!x 是局部变量,函数结束后内存被销毁
}
x
是局部变量,函数结束后其内存在栈上被回收,但指针返回了这块无效地址。
避免方法:
void foo(int* ptr) {
delete ptr; // 在这里释放
}
int main() {
int* ptr = new int;
foo(ptr); // 调用函数释放 ptr
// 这里 ptr 成为了野指针,仍然可以访问已释放的内存
}
虽然在 foo()
中释放了内存,但 main()
中的 ptr
并不知道这件事,还在使用这块已释放的内存。
避免方法:
delete
后一定记得 ptr = nullptr
。std::unique_ptr
, std::shared_ptr
)来自动管理生命周期。delete ptr
中到底发生了什么假设我们有下面这段代码:
int* ptr = new int(42); // 第一步
delete ptr; // 第二步
第一步:int* ptr = new int(42)
这个语句分两件事:
int
,值是 42,0x1000
,可以理解为:堆内存地址:0x1000
存放的值: 42
ptr
,它是一个“指针变量”,ptr
本身的地址是 0x2000
,但它存放的是 0x1000
,即堆内存的地址:栈内存地址:0x2000
变量名: ptr
内容: 0x1000
第二步:delete ptr;
这个语句只做一件事:
让系统回收 ptr 指向的那块堆内存(即地址 0x1000
的 42
)。也就是说,堆上的值 42
被释放掉了(该内存地址会被标记为“可用”,原来的值 42
一般还在,但是你不能保证它还在,也不能再用它),这块内存以后就不能再安全访问了。
但是重要的一点:ptr
这个变量本身还在,它还保留着 0x1000
这个地址,也就是说:堆内存的那块空间(0x1000
)变成“废地”,但 ptr
这个“钥匙”还握在你手里,如果你继续用 *ptr = 100;
就相当于你在废地上盖房子 —— 没人知道会发生什么!
名称 | 定义说明 |
---|---|
野指针 | 指的是指向已经被释放或无效地址的指针。常见于 delete 后,指针本身未置为 nullptr ,仍然指向原地址。 |
悬浮指针(悬浮引用) | 指的是引用(或指针)指向一个局部变量,当函数返回后,该局部变量已被销毁,引用仍然存在,就称为悬浮引用。 |
分类维度 | 野指针 | 悬浮指针(引用) |
---|---|---|
所涉类型 | 指针类型 | 引用类型(也可能是指针) |
本质问题 | 内存释放后指针未清空,仍然访问原地址 | 局部变量生命周期结束,引用仍在使用它 |
风险表现 | 访问无效内存:程序崩溃、数据损坏 | 访问已销毁对象,行为未定义(数据异常) |
悬浮指针例子:
int& foo() {
int x = 10;
return x; // ❌ 错误:返回了局部变量的引用
}
int main() {
int& ref = foo(); // ref 是悬浮引用
cout << ref << endl; // ❌ 错误:访问已销毁变量
}
类型 | 原因 |
---|---|
野指针 | 由于没有正确管理指针生命周期,特别是释放后未将指针置为 nullptr |
悬浮指针 | 函数返回了对局部变量的引用或指针,超出了变量的作用域 |
new
)或将变量声明为静态:// 正确示例(静态变量)
int& foo() {
static int x = 10;
return x; // 把该变量作为引用返回,因为函数的返回类型是 int&。
}
野指针是“指向已释放内存的指针”,悬浮指针/引用是“引用了生命周期已结束的变量”。两者都会导致未定义行为,但来源不同,解决方式也不同。
内存对齐是指:数据在内存中的起始地址必须是某个特定“整数倍”地址,这个“整数”叫对齐值(alignment)。
举例:
如果一个 int 是 4 字节,很多 CPU 要求这个 int 的起始地址必须是 4 的倍数,比如
0x1000
、0x1004
、0x1008
,而不能从 0x1001 或 0x1003 开始。这种规则叫“自然对齐”:一个数据的对齐值一般是它的大小。
为什么有这种限制:
因为 CPU 读取内存不是一个字节一个字节读的,它是一段一段读取,如果你的数据没有放在“合适”的边界上,CPU 要么:
- 读两次(性能下降)
- 报错(有些系统,如 SPARC)
- 甚至程序崩溃(极端情况)
本质原因是 提高 CPU 访问内存的效率。