重点:给大家推荐一个学习网站https://github.com/0voice
1.什么是智能指针?智能指针有什么作用?分为哪几种?各自有什么样的特点?
智能指针(Smart Pointer)是 C++ 标准库中的一种特殊对象,用来自动管理动态分配的内存。智能指针在对象生命周期结束时自动释放内存,避免手动管理指针可能导致的内存泄漏或野指针等问题。
C++ 11 引入了标准库中的三种主要智能指针,分别是 std::unique_ptr
、std::shared_ptr
和 std::weak_ptr
,它们通过 RAII(Resource Acquisition Is Initialization)机制管理资源。
智能指针的作用
- 自动管理内存:智能指针负责在适当的时间自动释放动态分配的内存,避免内存泄漏。
- 减少错误:传统指针需要手动调用
delete
来释放内存,容易导致内存管理错误(如双重释放、忘记释放等),而智能指针通过自动清理资源降低了这些风险。 - 异常安全:智能指针可以确保在程序抛出异常时,仍然能够正确释放内存,避免资源泄漏。
智能指针的种类和特点
-
std::unique_ptr
(独占所有权的智能指针)- 特点:
unique_ptr
表示对象的独占所有权,即同一时间只能有一个unique_ptr
拥有该对象的所有权。不能复制unique_ptr
,只能通过移动语义转移所有权(使用std::move
)。 - 作用:适合用来管理那些只需要单一所有者的资源,确保对象只被一个指针管理,防止对象被多个指针访问时的混乱。
- 使用场景:独占资源的管理,如文件句柄、网络连接等。
#include <memory> #include <iostream> int main() { std::unique_ptr<int> ptr1 = std::make_unique<int>(10); std::cout << *ptr1 << std::endl; // 输出 10 // std::unique_ptr<int> ptr2 = ptr1; // 错误:不能拷贝 unique_ptr std::unique_ptr<int> ptr2 = std::move(ptr1); // 正确:转移所有权 if (!ptr1) { std::cout << "ptr1 is empty after move" << std::endl; // 输出此消息 } }
- 特点:
-
std::shared_ptr
(共享所有权的智能指针)- 特点:
shared_ptr
允许多个指针共享同一个对象的所有权,当最后一个指针销毁时,所管理的对象会被自动删除。每个shared_ptr
内部维护一个引用计数器,当指针拷贝时引用计数加 1,指针销毁时引用计数减 1。 - 作用:适合多个所有者共享一个资源,并且希望在所有者都不再使用该资源时自动释放它。
- 使用场景:多个对象共享资源,比如图形对象的共享、数据库连接的管理等。
#include <memory> #include <iostream> int main() { std::shared_ptr<int> ptr1 = std::make_shared<int>(10); std::shared_ptr<int> ptr2 = ptr1; // ptr2 共享 ptr1 的所有权 std::cout << "ptr1 use count: " << ptr1.use_count() << std::endl; // 输出 2 std::cout << "ptr2 use count: " << ptr2.use_count() << std::endl; // 输出 2 ptr1.reset(); // ptr1 不再指向对象 std::cout << "ptr2 use count after reset: " << ptr2.use_count() << std::endl; // 输出 1 std::cout << *ptr2 << std::endl; // 输出 10 }
- 特点:
-
std::weak_ptr
(弱引用智能指针)- 特点:
weak_ptr
是对shared_ptr
的一种弱引用,它不会增加引用计数,因此不会影响shared_ptr
的生命周期。weak_ptr
主要用于解决循环引用的问题,即当两个对象互相持有shared_ptr
时,会导致对象无法被释放的内存泄漏问题。weak_ptr
不能直接访问对象,需要通过lock()
方法获取指向对象的shared_ptr
。 - 作用:防止循环引用,同时可以在对象存在时通过
lock()
安全地访问对象。 - 使用场景:避免
shared_ptr
的循环引用,尤其是在需要双向引用的情况下,如父子对象之间的关系。
#include <memory> #include <iostream> int main() { std::shared_ptr<int> sptr = std::make_shared<int>(10); std::weak_ptr<int> wptr = sptr; // weak_ptr 不增加引用计数 std::cout << "sptr use count: " << sptr.use_count() << std::endl; // 输出 1 if (auto locked = wptr.lock()) { // 获取 shared_ptr,防止对象被销毁 std::cout << "Value: " << *locked << std::endl; // 输出 10 } sptr.reset(); // 销毁对象 if (wptr.expired()) { std::cout << "Object is destroyed." << std::endl; // 输出此消息 } }
- 特点:
三种智能指针的比较
智能指针类型 | 所有权 | 可拷贝性 | 内存释放时机 | 使用场景 |
---|---|---|---|---|
std::unique_ptr | 独占所有权 | 不能拷贝,只能移动 | 当智能指针超出作用域时释放 | 独占资源(文件句柄、锁、连接等) |
std::shared_ptr | 共享所有权 | 可拷贝,共享资源 | 所有引用计数归零时释放 | 资源共享(图形对象、缓存等) |
std::weak_ptr | 无所有权(依赖于 shared_ptr ) | 不增加引用计数 | 引用计数归零时,资源由 shared_ptr 释放 | 防止循环引用(如双向关系中的指针) |
总结
- 智能指针通过 RAII 机制管理动态内存的生命周期,避免手动管理内存带来的复杂性。
unique_ptr
提供独占的对象所有权,不允许多个指针共享同一对象。shared_ptr
提供共享所有权,允许多个指针同时拥有并管理同一对象,通过引用计数机制自动释放内存。weak_ptr
提供对shared_ptr
的弱引用,用于避免循环引用问题,不增加引用计数。
2.shared_ptr是如何实现的?
std::shared_ptr
是 C++11 引入的一种智能指针,提供了共享所有权的机制,即多个 shared_ptr
对象可以指向同一个动态分配的内存对象。当最后一个指向该对象的 shared_ptr
被销毁时,才会释放该对象的内存。shared_ptr
的核心实现依赖于引用计数(reference counting)来管理资源的生命周期。
std::shared_ptr
的实现原理
-
引用计数机制
- 每个
shared_ptr
都会维护两个计数器:- 强引用计数(strong reference count):表示有多少个
shared_ptr
实例共享对象的所有权。 - 弱引用计数(weak reference count):表示有多少个
weak_ptr
引用该对象,weak_ptr
不增加强引用计数,只增加弱引用计数。
- 强引用计数(strong reference count):表示有多少个
当一个
shared_ptr
创建时,强引用计数会增加。当某个shared_ptr
被复制时,强引用计数会再次增加;当shared_ptr
被销毁或重置时,强引用计数会减少。当强引用计数归零时,表示没有shared_ptr
指向该对象,程序会自动销毁对象并释放内存。 - 每个
-
控制块(Control Block)
-
shared_ptr
并不是直接将引用计数保存在智能指针对象本身,而是引入了一个额外的控制块来维护引用计数及其他信息。 -
控制块包含:
- 对象的引用计数(强引用计数和弱引用计数)。
- 指向实际管理的对象的指针。
- 可能还包含自定义的删除器,用来定义如何释放对象内存。
-
当
shared_ptr
创建时,控制块也会被分配,shared_ptr
会指向这个控制块并管理其生命周期。当shared_ptr
被复制时,所有shared_ptr
都指向同一个控制块,并共享其引用计数。当最后一个shared_ptr
被销毁时,控制块中的对象指针被释放,随后控制块本身也会被销毁。
-
-
线程安全
shared_ptr
的引用计数是线程安全的,这意味着多个线程可以安全地访问和修改引用计数。为了确保线程安全,shared_ptr
使用了原子操作来管理引用计数的增减操作。
-
析构与释放内存
- 当
shared_ptr
的强引用计数降为 0 时,对象的析构函数会被调用,并且它所管理的内存会被释放。只有当强引用和弱引用计数都为 0 时,控制块才会被销毁。weak_ptr
引用的存在并不会阻止对象的释放,但是控制块会继续存在直到所有weak_ptr
都被销毁。
- 当
shared_ptr
实现的核心组件
下面是一些 shared_ptr
实现的核心步骤和流程:
1. 创建 shared_ptr
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(10); // 创建一个 shared_ptr,并初始化值为 10
std::cout << "Value: " << *sp << std::endl; // 输出 10
}
- 当执行
std::make_shared<int>(10)
时:- 分配了一个新的控制块。
- 分配了一个新的
int
对象,并将值初始化为 10。 - 强引用计数设为 1。
2. 拷贝 shared_ptr
std::shared_ptr<int> sp1 = std::make_shared<int>(20);
std::shared_ptr<int> sp2 = sp1; // sp1 和 sp2 共享所有权
3. 销毁 shared_ptr
sp1.reset(); // sp1 不再管理对象,引用计数减少
- 当
sp1
调用reset()
后:sp1
不再管理对象,强引用计数减为 1(因为sp2
仍在)。- 对象的内存不会被释放,因为
sp2
仍在使用该对象。
当 sp2
离开作用域时:
- 强引用计数降为 0。
- 管理的对象被销毁,释放内存。
4. 控制块示意图
当你有多个 shared_ptr
时,每个 shared_ptr
只指向控制块,而控制块持有指向实际对象的指针。控制块如下所示:
控制块包含了实际对象的指针和引用计数。随着 sp1
和 sp2
的拷贝和销毁,控制块的引用计数会相应变化。
shared_ptr
的示例代码
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass Constructor\n"; }
~MyClass() { std::cout << "MyClass Destructor\n"; }
};
int main() {
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); // 创建对象,强引用计数为 1
std::cout << "Reference count after creation: " << ptr1.use_count() << std::endl; // 输出 1
{
std::shared_ptr<MyClass> ptr2 = ptr1; // 共享所有权,强引用计数为 2
std::cout << "Reference count after copy: " << ptr1.use_count() << std::endl; // 输出 2
} // ptr2 离开作用域,强引用计数变回 1
std::cout << "Reference count after ptr2 goes out of scope: " << ptr1.use_count() << std::endl; // 输出 1
} // ptr1 离开作用域,强引用计数为 0,对象销毁,析构函数调用
输出:
MyClass Constructor Reference count after creation: 1 Reference count after copy: 2 Reference count after ptr2 goes out of scope: 1 MyClass Destructor
- 创建
ptr1
时,强引用计数为 1。 - 当
ptr2
拷贝ptr1
时,强引用计数增加到 2。 - 当
ptr2
离开作用域时,强引用计数减回 1。 - 最后,当
ptr1
离开作用域时,强引用计数为 0,调用MyClass
的析构函数并释放内存。
总结
shared_ptr
通过控制块管理动态对象的引用计数,当强引用计数归零时释放对象内存。- 它可以在多个
shared_ptr
间共享同一个对象,同时提供线程安全的引用计数管理机制。 shared_ptr
解决了传统指针中内存泄漏、重复释放等问题,适用于需要多个所有者共享资源的场景。
3.右值引用的作用
右值引用(rvalue reference)是 C++11 引入的一种新类型引用,允许对临时对象(即右值)进行引用操作。与传统的左值引用(T&
)不同,右值引用(T&&
)专门用于绑定到右值(即那些不具名的临时对象或即将被销毁的对象),从而可以对这些临时对象进行高效的资源操作。
右值引用的主要作用如下:
- 支持移动语义(Move Semantics)
- 避免不必要的拷贝(优化性能)
- 实现完美转发(Perfect Forwarding)
1. 支持移动语义(Move Semantics)
右值引用的最大作用是支持移动语义,在某些情况下避免昂贵的深拷贝操作。移动语义允许将资源(如内存、文件句柄等)从一个对象“移动”到另一个对象,而不是进行拷贝,从而显著提高程序的性能。
- 拷贝语义:传统上,拷贝构造函数会深拷贝对象的所有资源,这会导致不必要的性能开销,尤其是对于大对象或复杂的容器类(如
std::vector
)。 - 移动语义:通过右值引用,可以将一个对象的资源“偷取”到另一个对象,而不需要创建新的副本。移动操作通常只涉及指针的交换或简单的内存指针赋值,而不是完整的对象拷贝。
#include <iostream>
#include <vector>
class MyClass {
public:
std::vector<int> data;
MyClass(size_t size) : data(size) {
std::cout << "Constructor: allocating " << size << " elements.\n";
}
// 拷贝构造函数(深拷贝)
MyClass(const MyClass& other) : data(other.data) {
std::cout << "Copy Constructor: deep copying.\n";
}
// 移动构造函数(偷取资源)
MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {
std::cout << "Move Constructor: moving.\n";
}
};
int main() {
MyClass obj1(100); // 构造函数:分配 100 个元素
MyClass obj2 = std::move(obj1); // 移动构造函数:将资源移动到 obj2
}
输出:
Constructor: allocating 100 elements. Move Constructor: moving.
在上述代码中,obj2
通过移动构造函数“偷取”了 obj1
的资源,而不需要进行深拷贝。这大大提高了性能,特别是当对象包含大量资源时。
2. 避免不必要的拷贝
通过右值引用,临时对象可以避免不必要的拷贝。例如,在函数返回大型对象时,如果该对象是右值,编译器可以通过移动构造函数直接“偷取”返回值的资源,而不是进行拷贝操作。
MyClass createObject() {
MyClass obj(1000);
return obj; // obj 是右值,可以通过移动构造来避免拷贝
}
int main() {
MyClass obj = createObject(); // 移动而不是拷贝
}
通过 std::move
,可以将返回的临时对象标记为右值,避免不必要的拷贝,提高效率。
3. 实现完美转发(Perfect Forwarding)
右值引用配合 std::forward
可以实现所谓的完美转发,它允许将参数原封不动地传递给另一个函数,无论参数是左值还是右值。这对于编写泛型代码尤为重要,能够确保函数模板在调用其他函数时保持参数的值类别。
#include <iostream>
// 泛型转发函数模板
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // 完美转发
}
void process(int& arg) { // 左值版本
std::cout << "Lvalue reference\n";
}
void process(int&& arg) { // 右值版本
std::cout << "Rvalue reference\n";
}
int main() {
int x = 10;
wrapper(x); // 左值引用调用
wrapper(10); // 右值引用调用
}
输出:
Lvalue reference Rvalue reference
通过 std::forward<T>(arg)
,可以完美地转发参数,无论传入的是左值还是右值,都能调用正确的重载函数。
左值引用 vs 右值引用
特性 | 左值引用(Lvalue Reference) | 右值引用(Rvalue Reference) |
---|---|---|
引用对象 | 引用左值(具名的、可持久的对象) | 引用右值(临时对象、即将销毁的对象) |
典型用途 | 持续使用或修改对象 | 转移资源,避免拷贝 |
表示方式 | T& | T&& |
拷贝 vs 移动 | 拷贝对象数据 | 移动对象资源(偷取资源) |
总结
- 右值引用为 C++ 提供了一种机制来处理右值对象,从而实现高效的移动语义。
- 移动语义通过偷取资源,避免了不必要的深拷贝操作,显著提高程序性能。
- 完美转发通过右值引用和
std::forward
使得泛型代码能够灵活处理左值和右值。
4.悬挂指针和野指针的区别
悬挂指针(Dangling Pointer)和野指针(Wild Pointer)都是 C/C++ 中常见的指针错误,可能导致未定义行为甚至程序崩溃。虽然它们都与无效的指针指向有关,但有本质区别。
悬挂指针是指向已被释放或已经失效的内存区域的指针。换句话说,悬挂指针指向的内存曾经是有效的,但由于内存已经被释放或超出作用域,指针仍然持有旧的地址,导致悬挂状态。
产生悬挂指针的常见原因:
-
释放动态分配的内存后未将指针置空: 动态分配的内存通过
delete
或free
释放后,指针仍然保存该内存地址,尽管内存已经被释放。int* ptr = new int(10); delete ptr; // 释放内存,但 ptr 仍指向已释放的区域 *ptr = 5; // 未定义行为,因为 ptr 是悬挂指针
修正:释放内存后,将指针置为
nullptr
,避免继续访问该内存。delete ptr; ptr = nullptr; // 防止悬挂指针
-
指向自动变量的指针在超出作用域后被使用: 自动变量(局部变量)在函数返回时会销毁,如果在返回后仍然访问指向这些变量的指针,会造成悬挂指针。
int* danglingPointer() { int localVar = 42; return &localVar; // 返回局部变量的地址 } int* p = danglingPointer(); // p 成为悬挂指针,指向已销毁的 localVar
野指针(Wild Pointer)
野指针是指向一个随机的、未初始化的内存地址的指针。野指针没有被赋予一个有效的内存地址,通常是未经过正确初始化的指针。
产生野指针的常见原因:
-
指针声明后未初始化: 声明一个指针变量时,未对其进行初始化,默认情况下它可能指向一个随机的地址。如果试图使用这样的指针,会导致野指针问题。
int* ptr; // 未初始化的指针 *ptr = 10; // 野指针,未定义行为
int* ptr = nullptr; if (ptr) { *ptr = 10; // 只有在 ptr 指向有效地址时才操作 }
-
指针偏移越界: 当对数组或内存块进行操作时,如果指针偏移超出合法的内存范围,就会成为野指针。
int arr[5]; int* p = arr + 10; // p 指向数组边界外,野指针 *p = 20; // 未定义行为
悬挂指针与野指针的区别总结
区别点 | 悬挂指针(Dangling Pointer) | 野指针(Wild Pointer) |
---|---|---|
定义 | 指向已释放或已失效内存的指针 | 指向随机或未初始化内存的指针 |
常见原因 | 内存释放后未将指针置空、超出作用域的局部变量地址 | 未初始化的指针、数组或内存块越界 |
如何避免 | 在释放动态内存后将指针置为 nullptr ,避免使用超出作用域的指针 | 在指针声明时进行初始化,避免未初始化使用 |
行为特征 | 指针曾指向有效的内存,但该内存已无效 | 指针从未指向过有效的内存区域 |
导致的后果 | 可能访问已释放的内存,导致程序崩溃或异常行为 | 访问未初始化的地址,导致未定义行为(程序崩溃、无效数据等) |
总结
- 悬挂指针:指向曾经有效但已被释放的内存地址,常见于释放内存后未置空的指针。
- 野指针:指向未初始化的随机内存地址,常见于指针未初始化或越界访问。
5.句柄和指针的区别和联系是什么?
句柄(Handle)和指针(Pointer)在计算机编程中都是用于引用或访问资源的方式,但它们有着不同的概念和使用场景。理解它们的区别和联系有助于更好地理解系统资源管理的机制,尤其是在操作系统或大型系统开发中。
指针是一个变量,它直接存储内存地址,用于指向内存中的某个具体位置。指针可以用来访问、修改该地址处的数据。在 C/C++ 中,指针是核心概念,允许对内存进行低级操作。
指针的特性:
- 直接指向内存地址:指针变量持有的是内存的实际地址。
- 灵活性:可以用于指向任何类型的数据(包括数组、结构体、函数等)。
- 操作内存:通过指针可以直接读写指向的内存区域。
- 需要管理内存:程序员需要手动管理指针指向的内存(特别是在动态内存分配时),否则容易出现内存泄漏或未定义行为。
指针的常见用途:
- 动态内存管理(如
malloc
或new
)。 - 数据结构(如链表、树等)的实现。
- 访问数组或传递数组给函数。
- 实现函数指针、回调机制等。
句柄是一种抽象化的引用,用于表示操作系统或库中的资源(如文件、窗口、线程、进程、设备等)。它是由系统或库返回的一个标识符,而不是直接的内存地址。句柄通常是一个整数或指针,程序通过这个标识符间接访问资源,操作系统负责管理句柄所代表的实际资源。
句柄的特性:
- 间接访问资源:句柄是一种抽象的引用,不能直接访问内存,而是通过系统或库提供的 API 进行操作。
- 例如,在 Windows 中,打开一个文件时会得到一个文件句柄(如
HANDLE
类型),之后所有对文件的操作都通过这个句柄进行:
- 例如,在 Windows 中,打开一个文件时会得到一个文件句柄(如
- 资源安全性和管理:句柄隐藏了资源的底层实现细节,操作系统或库可以通过句柄进行资源管理(如分配、释放、访问控制等),使得用户无法直接操作底层资源,增强了系统的稳定性和安全性。
- 系统依赖:句柄通常与操作系统紧密相关,在不同的操作系统上,句柄的实现方式可能不同。例如,Windows 系统中有文件句柄、进程句柄、线程句柄等,Linux 系统中文件描述符(file descriptor)也可以看作一种句柄。
- 自动管理:操作系统通常负责创建和销毁句柄所对应的资源,程序员只需要在适当的时候释放句柄。
句柄的常见用途:
- 操作系统资源管理:在操作系统中,所有的系统资源(如文件、设备、窗口等)都通过句柄来访问。
- 例如,在 Windows API 中,句柄用来操作窗口(
HWND
)、设备上下文(HDC
)、进程(HANDLE
)等。
- 例如,在 Windows API 中,句柄用来操作窗口(
区别和联系
特性 | 指针(Pointer) | 句柄(Handle) |
---|---|---|
存储内容 | 存储的是直接的内存地址 | 存储的是间接的标识符,系统通过句柄来管理资源 |
访问方式 | 可以直接访问和操作内存地址中的数据 | 通过系统或库的 API 访问资源,不能直接操作内存 |
灵活性 | 可以指向任意内存地址,具有很大的灵活性和自由度 | 只能通过句柄的 API 来访问资源,安全性较高 |
安全性 | 不安全,可能会指向无效的内存地址,容易导致悬挂指针等错误 | 安全性较高,资源管理由操作系统或库来负责 |
资源类型 | 用于指向任何类型的内存数据 | 用于表示系统资源(文件、进程、线程、窗口等) |
平台相关性 | 平台无关,C/C++ 代码中的指针在不同平台都可以使用 | 平台相关,句柄通常与操作系统的实现密切相关 |
资源管理 | 程序员需要手动管理内存,可能出现内存泄漏或悬挂指针 | 由操作系统或库管理,程序员只需在适当时机释放句柄 |
总结
- 指针是直接存储内存地址的变量,可以直接操作内存,灵活但不安全,程序员需要自行管理内存。
- 句柄是操作系统或库提供的抽象引用,用于间接访问资源。句柄隐藏了资源的具体实现和内存细节,增强了系统的安全性和可管理性。
- 联系:句柄有时在底层实现上可能会使用指针(如 Windows 中的
HANDLE
可能是一个指针),但开发者无法直接操作资源内存,必须通过 API 访问。