[C++面试] 智能指针面试点(重点)

一、入门

1、什么是智能指针,为什么需要它?

[C++面试] RAII资源获取即初始化(重点)-CSDN博客

智能指针是一种类模板,用于管理动态分配的内存,能在对象生命周期结束时自动释放内存,避免内存泄漏。在传统的 C++ 中,使用 new 分配内存后,必须手动使用 delete 释放,若忘记或在异常情况下未执行 delete,就会导致内存泄漏。而智能指针利用 C++ 的 RAII(资源获取即初始化)技术,在对象构造时获取资源,在析构时释放资源,大大提高了代码的安全性。

#include 
#include 

int main() {
    std::unique_ptr ptr(new int(10));
    std::cout << *ptr << std::endl;
    // 当 ptr 离开作用域时,内存会自动释放
    return 0;
}

2、std::unique_ptr 是什么,有什么特点? 

std::unique_ptr 是一种独占式智能指针,同一时间只能有一个 std::unique_ptr 指向某个对象。它不允许拷贝构造和赋值操作,但可以通过 std::move 转移所有权。

#include 
#include 

int main() {
    std::unique_ptr ptr1(new int(20));
    std::unique_ptr p1 = std::make_unique(42);

    // std::unique_ptr ptr2 = ptr1; // 错误,不允许拷贝
    std::unique_ptr ptr2 = std::move(ptr1); // 转移所有权
    if (!ptr1) {
        std::cout << "ptr1 不再拥有对象" << std::endl;
    }
    if (ptr2) {
        std::cout << *ptr2 << std::endl;
    }
    return 0;
}

 避免跨线程传递 std::unique_ptr(所有权转移需明确)

std::unique_ptr 不允许拷贝构造和赋值操作,只能通过 std::move 来转移所有权。在跨线程传递时,如果没有正确使用 std::move,会导致编译错误;如果在多个线程中同时持有对同一个对象的 std::unique_ptr,会导致对象被多次释放,产生未定义行为。

3、C++11中有哪些智能指针

  • unique_ptr:独占所有权,适用于单一所有者场景(如工厂模式返回对象)
  • shared_ptr:共享所有权,适用于多个对象需要共享同一资源(如缓存系统)
  • weak_ptr:观察者模式,解决shared_ptr循环引用问题(如双向链表节点)

二、进阶

1、​std::shared_ptr 是什么,如何工作?

std::shared_ptr 是一种共享式智能指针,多个 std::shared_ptr 可以指向同一个对象。它使用引用计数来管理对象的生命周期,每增加一个指向该对象的 std::shared_ptr,引用计数加 1;每减少一个指向该对象的 std::shared_ptr,引用计数减 1。当引用计数变为 0 时,对象被自动释放。

#include 
#include 

int main() {
    std::shared_ptr ptr1(new int(30));
    std::cout << "引用计数: " << ptr1.use_count() << std::endl;
    std::shared_ptr ptr2 = ptr1;
    std::cout << "引用计数: " << ptr1.use_count() << std::endl;
    return 0;
}

2、 std::weak_ptr 有什么作用,它和 std::shared_ptr 有什么关系?

std::weak_ptr 是一种弱引用智能指针,它不拥有对象的所有权,不会增加对象的引用计数。它主要用于解决 std::shared_ptr 可能出现的循环引用问题。当 std::shared_ptr 之间存在循环引用时,引用计数永远不会变为 0,导致内存泄漏。

可以从 std::shared_ptr 或另一个 std::weak_ptr 构造,通过 lock() 方法可以获得一个 std::shared_ptr 来访问对象

#include 
#include 

class B;

class A {
public:
    std::shared_ptr b_ptr;
    ~A() { std::cout << "A 析构" << std::endl; }
};

class B {
public:
    std::weak_ptr a_ptr; // 使用 std::weak_ptr 打破循环引用
    ~B() { std::cout << "B 析构" << std::endl; }
};

int main() {
    std::shared_ptr a = std::make_shared();
    std::shared_ptr b = std::make_shared();
    a->b_ptr = b;
    b->a_ptr = a;
    return 0;
}

3、审视代码1:取临时实体的地址并赋值给智能指针的风险

#include 
#include 

class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
    void doSomething() { std::cout << "Doing something..." << std::endl; }
};

int main() {
    std::shared_ptr ptr(&MyClass());
    ptr->doSomething();
    return 0;
}

临时对象生命周期问题MyClass() 创建了一个临时对象,其生命周期在完整表达式 std::shared_ptr ptr(&MyClass()); 结束时就终止了。当这个临时对象被销毁后,ptr 就变成了一个悬空指针,后续调用 ptr->doSomething(); 会导致未定义行为。

双重释放风险std::shared_ptr 会在其生命周期结束时尝试释放所管理的对象。由于临时对象在完整表达式结束时已经被销毁,当 ptr 析构时再次尝试释放该对象,会造成双重释放,这也是未定义行为。

4、std::shared_ptr(new T()) 与 std::make_shared() 的差别

  • std::shared_ptr(new T()):这种方式会进行两次内存分配。一次是通过 new T() 为对象本身分配内存,另一次是为 std::shared_ptr 内部的引用计数和控制块分配内存。
  • std::make_shared():只进行一次内存分配,它会同时为对象和引用计数控制块分配内存。这减少了内存分配的开销,提高了性能。降低了内存碎片的可能性。
  • std::shared_ptr(new T()):在多步操作中,如果在创建对象后但在 std::shared_ptr 接管所有权之前发生异常,可能会导致内存泄漏。
void func(std::shared_ptr p1, std::shared_ptr p2) {
    // 函数体
}

func(std::shared_ptr(new int(1)), std::shared_ptr(new int(2)));

如果 new int(2) 抛出异常,而 new int(1) 已经成功分配内存但还未被 std::shared_ptr 完全接管,那么这块内存就会泄漏。

  • std::make_shared():由于是原子操作,要么整个内存分配和对象构造都成功,要么都失败,不会出现部分成功导致的内存泄漏问题,提供了更好的异常安全性。

std::make_shared:在 C++11 标准中被引入,作为一种更安全、高效的方式来创建 std::shared_ptr 对象。

std::make_unique:在 C++14 标准中被引入,用于更安全、高效地创建 std::unique_ptr 对象。

#include 

class MyClass {};

int main() {
    auto ptr = std::make_unique();
    return 0;
}

5、 shared_ptr的引用计数如何实现?线程安全吗?

通过控制块(包含引用计数、弱计数等)管理资源,拷贝时递增计数,析构时递减

线程安全:引用计数操作是原子性的,但指向的对象访问需要额外同步(如互斥锁)

三、高阶

1、如何自定义智能指针的删除器?

智能指针允许自定义删除器,当对象的生命周期结束时,会调用自定义的删除器来释放资源。自定义删除器可以是一个函数、函数对象或 lambda 表达式。

#include 
#include 

// 自定义删除器
void custom_deleter(int* ptr) {
    std::cout << "使用自定义删除器释放内存" << std::endl;
    delete ptr;
}

int main() {
    std::unique_ptr ptr(new int(40), custom_deleter);
    return 0;
}
auto deleter = [](FILE* f) { fclose(f); };
std::unique_ptr fp(fopen("test.txt", "r"), deleter);

2、 智能指针在多线程环境下的使用需要注意什么?

std::shared_ptr 的引用计数操作是线程安全的,但对象的访问和修改不一定是线程安全的。

如果多个线程同时访问和修改同一个对象,需要使用同步机制(如互斥锁)来保证线程安全。

在传递智能指针时,要避免出现数据竞争和悬空指针的问题。

#include 
#include 
#include 
#include 

std::shared_ptr shared_data;
std::mutex mtx;

void modify_shared_data() {
    std::lock_guard lock(mtx);
    if (shared_data) {
        *shared_data = 50;
    }
}

int main() {
    shared_data = std::make_shared(0);
    std::thread t(modify_shared_data);
    t.join();
    std::cout << *shared_data << std::endl;
    return 0;
}

3、审视代码2: 同一原始指针初始化多个shared_ptr

int* raw = new int(10);
std::shared_ptr p1(raw);
std::shared_ptr p2(raw); 

会导致重复释放。应使用p2 = p1make_shared 

4、 weak_ptrlock()方法返回什么?如何处理失效情况?

为了安全地访问 std::weak_ptr 所指向的对象,需要调用 lock() 方法。lock() 方法会尝试创建一个 std::shared_ptr 来管理 std::weak_ptr 所指向的对象。如果对象已经被销毁,lock() 会返回一个空的 std::shared_ptr;否则,会返回一个有效的 std::shared_ptr,并且对象的引用计数会增加。

#include 
#include 

int main() {
    std::shared_ptr shared = std::make_shared(42);
    std::weak_ptr weak = shared;

    // 使用 lock() 检查有效性
    if (auto locked = weak.lock()) {
        std::cout << "Value: " << *locked << std::endl;
    } else {
        std::cout << "Object has been destroyed." << std::endl;
    }

    // 释放 shared_ptr,对象被销毁
    shared.reset();

    // 再次检查
    if (auto locked = weak.lock()) {
        std::cout << "Value: " << *locked << std::endl;
    } else {
        std::cout << "Object has been destroyed." << std::endl;
    }

    return 0;
}

5、std::enable_shared_from_this的作用是什么?

std::enable_shared_from_this 是一个模板类,用于在类的成员函数内部安全地获取指向当前对象的 std::shared_ptr

#include 
#include 

class BadExample {
public:
    std::shared_ptr getShared() {
        return std::shared_ptr(this); // 错误做法
    }
    ~BadExample() {
        std::cout << "BadExample destroyed" << std::endl;
    }
};

int main() {
    std::shared_ptr ptr1 = std::make_shared();
    std::shared_ptr ptr2 = ptr1->getShared();
    // 程序结束时,ptr1 和 ptr2 会分别尝试释放对象,导致重复释放
    return 0;
}

std::enable_shared_from_this 就像是一个协调者,它能让新创建的 std::shared_ptr 和原来的 std::shared_ptr 共享同一个引用计数。

在类的成员函数中,如果直接使用 this 指针创建 std::shared_ptr,会导致创建一个新的、独立的 std::shared_ptr,这会使同一个对象有多个独立的引用计数,从而可能导致资源的重复释放。而 std::enable_shared_from_this 提供了一个 shared_from_this() 成员函数,该函数会返回一个指向当前对象的 std::shared_ptr,并且这个 std::shared_ptr 会与已有的 std::shared_ptr 共享同一个引用计数。

#include 
#include 

class MyClass : public std::enable_shared_from_this {
public:
    std::shared_ptr getShared() {
        return shared_from_this();
    }

    void doSomething() {
        std::cout << "Doing something..." << std::endl;
    }
};

int main() {
    std::shared_ptr shared = std::make_shared();
    std::shared_ptr anotherShared = shared->getShared();

    anotherShared->doSomething();

    return 0;
}

6、实现简化版本的shared_ptr

  • 一个指向对象的指针
  • 一个指向引用计数的指针,用于记录有多少个 shared_ptr 共享同一个对象。
  • 构造函数、析构函数、拷贝构造函数和赋值运算符,用于管理对象的生命周期和引用计数。
#include 

template 
class SimpleSharedPtr {
public:
    // 构造函数
    explicit SimpleSharedPtr(T* ptr = nullptr) : data(ptr), ref_count(new int(1)) {}

    // 拷贝构造函数
    SimpleSharedPtr(const SimpleSharedPtr& other) : data(other.data), ref_count(other.ref_count) {
        ++(*ref_count);
    }

    // 赋值运算符
    SimpleSharedPtr& operator=(const SimpleSharedPtr& other) {
        if (this != &other) {
            if (--(*ref_count) == 0) {
                delete data;
                delete ref_count;
            }
            data = other.data;
            ref_count = other.ref_count;
            ++(*ref_count);
        }
        return *this;
    }

    // 析构函数
    ~SimpleSharedPtr() {
        if (--(*ref_count) == 0) {
            delete data;
            delete ref_count;
        }
    }

    // 重载解引用运算符
    T& operator*() const {
        return *data;
    }

    // 重载箭头运算符
    T* operator->() const {
        return data;
    }

private:
    T* data;
    int* ref_count;
};

// 测试代码
class MyClass {
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }
    void doSomething() { std::cout << "Doing something..." << std::endl; }
};

int main() {
    SimpleSharedPtr ptr1(new MyClass());
    SimpleSharedPtr ptr2 = ptr1;

    ptr2->doSomething();

    return 0;
}

在多线程环境中,指向引用计数的指针需要考虑锁或者使用原子操作。因为多个线程可能同时对引用计数进行读写操作,如果不进行同步,会导致数据竞争,从而使引用计数的值出现错误,最终可能导致资源泄漏或重复释放等问题。 

#include 
#include 

template 
class ThreadSafeSharedPtr {
public:
    explicit ThreadSafeSharedPtr(T* ptr = nullptr) : data(ptr), ref_count(new std::atomic(1)) {}

    // 拷贝构造函数等操作需要正确处理原子引用计数
    // ...

private:
    T* data;
    std::atomic* ref_count;
};

你可能感兴趣的:(c++,面试)