目录

1. 什么是智能指针?为什么需要智能指针?

2. C++11 中有哪几种智能指针?它们的核心区别是什么?

3. 谈谈 std::unique_ptr 的实现原理和使用场景。

4. std::shared_ptr 的实现原理是什么?它是线程安全的吗?

5. 为什么需要 std::weak_ptr?它是如何解决循环引用问题的?

6. std::make_shared 和 new 的方式创建 shared_ptr 有什么区别?为什么优先使用 std::make_shared?


1. 什么是智能指针?为什么需要智能指针?

答案:

智能指针是C++中一个模板类,它利用了RAII(Resource Acquisition Is Initialization,资源获取即初始化)技术来模拟指针的行为,同时能自动管理所指向对象的生命周期。

为什么需要智能指针?

在传统的C++编程中,我们使用 new 关键字在堆上分配内存,并需要手动使用 delete 来释放,这被称为“裸指针”或“原始指针”。手动管理内存有以下几个主要痛点:

  1. 内存泄漏(Memory Leaks): 忘记调用 delete 释放内存,导致程序长时间运行后耗尽内存。

  2. 悬挂指针(Dangling Pointers): 内存已经被释放,但指针仍然指向该地址。此时通过该指针访问内存是未定义行为,可能导致程序崩溃。

  3. 重复释放(Double Free): 对同一块内存执行多次 delete,这也是未定义行为,通常会导致程序崩溃。

  4. 异常安全问题: 在 newdelete 之间如果发生异常,delete 语句可能不会被执行,从而导致内存泄漏。

智能指针的出现就是为了解决这些问题。它将对象的生命周期与一个栈上的智能指针对象绑定,当智能指针对象离开作用域时,其析构函数会自动被调用,并在析构函数中释放它所管理的堆内存。这极大地简化了资源管理,提高了代码的健壮性和异常安全性。

2. C++11 中有哪几种智能指针?它们的核心区别是什么?

答案:

C++11 在 <memory> 头文件中引入了三种主要的智能指针:

  1. std::unique_ptr独占所有权的智能指针。

  2. std::shared_ptr共享所有权的智能指针。

  3. std::weak_ptr:一种非拥有型的智能指针,用于解决 std::shared_ptr 的循环引用问题。

核心区别总结:

特性

std::unique_ptr

std::shared_ptr

std::weak_ptr

所有权

独占、唯一的

共享的

临时的、观察者

拷贝

禁止拷贝,只允许移动

允许拷贝

允许拷贝

引用计数

有(强引用计数)

有(弱引用计数)

核心用途

对资源进行独占管理

多个指针共享同一个资源

监视 shared_ptr,打破循环引用

资源释放

unique_ptr 被销毁时

当最后一个 shared_ptr 被销毁时

不直接管理资源生命周期

开销

极低,与裸指针几乎无差别

较大(包含一个控制块和原子操作)

较小

3. 谈谈 std::unique_ptr 的实现原理和使用场景。

答案:

std::unique_ptr 体现了“独占所有权”的语义。在任何时候,只有一个 unique_ptr 可以指向一个给定的对象。

实现原理:

unique_ptr 内部封装了一个裸指针。它通过禁止拷贝构造函数和拷贝赋值运算符,只提供移动构造函数和移动赋值运算符来实现所有权的唯一性。当一个 unique_ptr 对象被销毁(例如离开作用域)时,它的析构函数会释放其管理的资源。

所有权的转移必须是显式的,通常通过 std::move() 来完成。

// 创建一个 unique_ptr
std::unique_ptr<int> p1(new int(10));

// std::unique_ptr<int> p2 = p1; // 编译错误!不允许拷贝

// 转移所有权
std::unique_ptr<int> p3 = std::move(p1); 

// 此时 p1 变为空指针,p3 拥有资源
if (!p1) {
    std::cout << "p1 is now empty." << std::endl;
}
std::cout << "p3 points to: " << *p3 << std::endl; 

使用场景:

  1. 函数返回值: 当函数需要返回一个在堆上创建的对象时,返回 unique_ptr 是一个非常好的选择。这清晰地表明了所有权的转移,并且效率很高。

  2. 作为类的成员变量: 如果一个类拥有某个资源的唯一所有权(例如,一个工厂类创建并拥有一个产品对象),使用 unique_ptr 作为成员变量可以确保当类实例被销毁时,其拥有的资源也被自动释放。

  3. 管理动态数组: unique_ptr 有一个特化版本 std::unique_ptr<T[]>,可以安全地管理动态数组,并在销毁时调用 delete[]

std::unique_ptr<int[]> arr(new int[5]{1, 2, 3, 4, 5});
arr[0] = 100; // 可以像数组一样使用
// 当 arr 离开作用域时,会自动调用 delete[]

优先选择 unique_ptr:除非你需要共享所有权,否则 unique_ptr 应该是你的首选,因为它更轻量,所有权模型更简单。

4. std::shared_ptr 的实现原理是什么?它是线程安全的吗?

答案:

std::shared_ptr 实现了“共享所有权”的语义。多个 shared_ptr 可以指向同一个对象。只有当最后一个指向该对象的 shared_ptr 被销毁时,对象才会被释放。

实现原理:

shared_ptr 的实现通常包含两个指针:

  1. 一个指向它所管理的对象

  2. 一个指向一个控制块(Control Block)

这个控制块是一个非常关键的数据结构,它在首次创建 shared_ptr 时被分配,并被所有指向同一对象的 shared_ptr 共享。控制块中至少包含:

  • 强引用计数(Strong Reference Count): 记录有多少个 shared_ptr 正在共享该对象。当这个计数变为0时,对象被删除。

  • 弱引用计数(Weak Reference Count): 记录有多少个 weak_ptr 正在观察该对象。当这个计数变为0时,控制块本身被删除。

  • (可选)自定义删除器、分配器等信息。

每次拷贝 shared_ptr 时(拷贝构造或赋值),强引用计数会原子性地加1。每次 shared_ptr 被销毁时,强引用计数会原子性地减1。

线程安全性:

这是一个经典的面试题,答案需要精确:

  • 控制块是线程安全的:多个线程可以同时拷贝、赋值或销毁指向同一个对象的 shared_ptr 实例,因为引用计数的增减操作是原子操作,不会导致数据竞争。

  • 所管理的对象不是线程安全的shared_ptr 只保证了引用计数的线程安全,但它并不保证所指向对象的线程安全。如果多个线程需要同时访问或修改 shared_ptr 所指向的对象,你仍然需要手动加锁(例如使用 std::mutex)。

5. 为什么需要 std::weak_ptr?它是如何解决循环引用问题的?

答案:

std::weak_ptr 是一种非拥有型(non-owning)的智能指针。它指向一个由 shared_ptr 管理的对象,但它不会增加强引用计数。它的主要作用是“观察”一个对象,而不会影响其生命周期。

为什么需要 weak_ptr

主要是为了解决 shared_ptr 可能导致的**循环引用(Circular Reference)**问题。

循环引用问题举例:

假设有两个类 AB,它们都通过 shared_ptr 相互引用:

#include <iostream>
#include <memory>

struct B; // 前向声明

struct A {
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destructor called" << std::endl; }
};

struct B {
    std::shared_ptr<A> a_ptr;
    ~B() { std::cout << "B destructor called" << std::endl; }
};

int main() {
    std::shared_ptr<A> pa = std::make_shared<A>();
    std::shared_ptr<B> pb = std::make_shared<B>();

    pa->b_ptr = pb;
    pb->a_ptr = pa;

    std::cout << "pa use_count: " << pa.use_count() << std::endl; // 输出 2
    std::cout << "pb use_count: " << pb.use_count() << std::endl; // 输出 2

    // main函数结束时,pa 和 pb 被销毁
    // pa 的引用计数从 2 变为 1
    // pb 的引用计数从 2 变为 1
    // 它们的引用计数永远不会变为0,导致 A 和 B 的对象无法被释放,造成内存泄漏。
    // A 和 B 的析构函数永远不会被调用。
    return 0;
}

在这个例子中,A 对象内部有一个指向 Bshared_ptrB 对象内部也有一个指向 Ashared_ptr。当 main 函数结束时,papb 这两个栈上的智能指针被销毁,它们各自使其所指对象的引用计数减1。但此时,A 对象的引用计数仍然为1(被pb->a_ptr持有),B 对象的引用计数也为1(被pa->b_ptr持有)。这导致它们的引用计数永远无法归零,析构函数永远不会被调用,从而造成内存泄漏。

如何用 weak_ptr 解决:

解决方法是将其中一方的引用(通常是“子”指向“父”或相互依赖关系中较弱的一方)改为 std::weak_ptrweak_ptr 不会增加强引用计数,从而打破了循环。

struct B;

struct A {
    std::shared_ptr<B> b_ptr;
    ~A() { std::cout << "A destructor called" << std::endl; }
};

struct B {
    std::weak_ptr<A> a_ptr; // <--- 改为 weak_ptr
    ~B() { std::cout << "B destructor called" << std::endl; }
};

// ... main 函数与之前相同
// 当 pa, pb 离开作用域后:
// pa 的引用计数变为 1 -> 0, A 对象被销毁。
// A 的析构函数中,其成员 b_ptr 被销毁,导致 B 对象的引用计数从 1 -> 0, B 对象被销毁。
// 内存泄漏问题解决!

如何使用 weak_ptr

weak_ptr 不能直接访问所指向的对象,因为它不确定对象是否还存在。必须通过调用 lock() 方法来检查:

  • 如果对象仍然存在,lock() 会返回一个指向该对象的有效的 shared_ptr

  • 如果对象已经被销毁,lock() 会返回一个空的 shared_ptr

这种“检查再使用”的机制保证了访问的安全性。

6. std::make_sharednew 的方式创建 shared_ptr 有什么区别?为什么优先使用 std::make_shared

答案:

创建 std::shared_ptr 有两种常见方式:

// 方式一:使用 std::make_shared
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>();

// 方式二:使用 new
std::shared_ptr<MyClass> ptr2(new MyClass());

虽然两者都能创建 shared_ptr,但强烈推荐优先使用 std::make_shared,原因有二:

1. 性能更高(效率优势)

  • std::shared_ptr<MyClass> ptr(new MyClass()); 这行代码会执行两次堆内存分配:

    1. new MyClass():为 MyClass 对象本身分配内存。

    2. shared_ptr 的构造函数:为控制块(包含引用计数等)分配内存。

  • std::make_shared<MyClass>() 这行代码只会执行一次堆内存分配。它会一次性地分配一块足够大的内存,同时容纳 MyClass 对象和控制块。这减少了内存分配的开销,也提高了内存局部性,从而提升了性能。

2. 异常安全(健壮性优势)

考虑以下函数调用: process_data(std::shared_ptr<Data>(new Data()), compute_priority());

C++标准不保证函数参数的求值顺序。一个可能的执行顺序是:

  1. new Data():成功分配 Data 对象。

  2. compute_priority():调用此函数,但不幸地抛出了一个异常

  3. std::shared_ptr 的构造函数没有机会被调用

结果是,第一步中分配的 Data 对象的内存就泄漏了,因为没有任何智能指针能接管它。

如果使用 std::make_sharedprocess_data(std::make_shared<Data>(), compute_priority());

std::make_shared<Data>() 会原子性地完成对象的创建和智能指针的构造,即使 compute_priority() 抛出异常,Data 对象的内存也会被安全地管理和释放,不会发生泄漏。

什么情况下不能使用 std::make_shared

场景 make_shared 为何不行 解决方案
自定义删除器 函数接口没地方传删除器。 使用 shared_ptr 的构造函数:std::shared_ptr<T>(new T(), my_deleter);
私有构造函数 make_shared 是外部函数,没有访问权限。 在类的公开静态工厂函数内部使用 new 创建对象并包装成 shared_ptr 返回。

在这种情况下,我们就需要告诉 shared_ptr:“别用 delete,请用我指定的这个函数来释放资源。” 这个指定的函数就是“自定义删除器”。

(2) make_shared 为何不支持?

std::make_shared 的设计目标是效率,它通过一次性分配一块大内存(同时给对象和控制块使用)来实现这一点。它的函数模板签名是这样的:

std::make_shared<类型>(构造函数参数...);

您会发现,它的参数列表里只接受所创建对象的构造函数参数,根本没有地方让我们传入一个自定义删除器。这是设计上的一个权衡,为了保持其接口的简洁和高效的实现,它放弃了支持自定义删除器这个相对不那么常用的功能。

(3) 怎么做?

当需要自定义删除器时,我们必须放弃 make_shared,回到使用 new 的方式,因为 shared_ptr 的构造函数可以直接接收一个删除器作为参数。

示例代码:

#include <iostream>
#include <memory>
#include <cstdlib> // for malloc and free

// 这是一个自定义删除器,用于释放 malloc 分配的内存
void malloc_deleter(int* ptr) {
    std::cout << "Using custom deleter to free memory!" << std::endl;
    free(ptr);
}

int main() {
    // 正确做法:使用 shared_ptr 的构造函数传入指针和删除器
    std::shared_ptr<int> p1((int*)malloc(sizeof(int)), malloc_deleter);

    // 错误做法:make_shared 无法指定删除器,下面这行代码无法编译
    // std::shared_ptr<int> p2 = std::make_shared<int>(...); // 怎么传 deleter?没地方传!

    return 0;
}

2. 为什么不能用于“私有/受保护构造函数”?

(1) 什么是私有/受保护构造函数?

这是一种常见的设计模式(例如工厂模式、单例模式)。类的作者不希望你直接创建这个类的对象,所以把构造函数声明为 privateprotected。取而代之的是,它会提供一个公开的静态成员函数(通常叫 create()getInstance())来作为创建对象的唯一入口。

(2) make_shared 为何不支持?

std::make_shared<MyClass>() 本质上是一个外部的辅助函数。当它尝试创建 MyClass 的对象时,它需要调用 MyClass 的构造函数。如果这个构造函数是 private 的,那么根据 C++ 的访问权限规则,一个外部函数是无权访问一个类的私有成员的,因此编译器会报错。

只有类的成员函数或者友元(friend)才有权限访问私有成员。

(3) 怎么做?

在这种情况下,只能在那个公开的静态工厂函数内部,使用 new 来调用私有构造函数(因为作为成员函数,它有这个权限),然后将 new 出来的裸指针包装成 shared_ptr 并返回。

示例代码:

#include <iostream>
#include <memory>

class MyClass {
private:
    // 构造函数是私有的!
    MyClass() {
        std::cout << "MyClass private constructor called." << std::endl;
    }

public:
    // 提供一个公开的静态工厂函数来创建对象
    static std::shared_ptr<MyClass> create() {
        // 在类的内部,我们有权调用私有构造函数。
        // 因为 make_shared 无法访问,所以这里必须用 new。
        return std::shared_ptr<MyClass>(new MyClass());
    }
};

int main() {
    // 正确做法:通过静态工厂函数创建实例
    std::shared_ptr<MyClass> p1 = MyClass::create();

    // 错误做法:make_shared 是外部函数,无权访问私有构造函数,编译失败
    // std::shared_ptr<MyClass> p2 = std::make_shared<MyClass>();

    return 0;
}

  1. 当需要使用自定义删除器时。make_shared 不支持传递自定义删除器。

  2. 当你要管理的对象的构造函数是私有或受保护的,而你需要通过一个特殊的工厂函数来创建对象并包装成 shared_ptr 时。

  3. 好的,没问题。这两个确实是 std::make_shared 的经典局限场景,我们来逐一拆解,用具体的例子帮助您理解。

    1. 为什么不能用于“自定义删除器” (Custom Deleter)?

    (1) 什么是自定义删除器?

    默认情况下,shared_ptr 在释放内存时会调用 delete。但有时我们管理的资源并非是通过 new 创建的,比如:

  4. 用 C 语言的 malloc 分配的内存,需要用 free 释放。

  5. 一个文件指针,需要用 fclose 关闭。

  6. 一个数据库连接,需要调用特定的 close_connection 函数。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐