C++ 基础-面试题03(智能指针、右值引用、悬挂指针和野指针、指针常量和常量指针、句柄和指针的区别和联系、sharedptr,uniqueptr,weakptr,autoptr)

重点:给大家推荐一个学习网站https://github.com/0voice

1.什么是智能指针?智能指针有什么作用?分为哪几种?各自有什么样的特点?

智能指针(Smart Pointer)是 C++ 标准库中的一种特殊对象,用来自动管理动态分配的内存。智能指针在对象生命周期结束时自动释放内存,避免手动管理指针可能导致的内存泄漏或野指针等问题。

C++ 11 引入了标准库中的三种主要智能指针,分别是 std::unique_ptrstd::shared_ptrstd::weak_ptr,它们通过 RAII(Resource Acquisition Is Initialization)机制管理资源。

智能指针的作用
  • 自动管理内存:智能指针负责在适当的时间自动释放动态分配的内存,避免内存泄漏。
  • 减少错误:传统指针需要手动调用 delete 来释放内存,容易导致内存管理错误(如双重释放、忘记释放等),而智能指针通过自动清理资源降低了这些风险。
  • 异常安全:智能指针可以确保在程序抛出异常时,仍然能够正确释放内存,避免资源泄漏。
智能指针的种类和特点
  1. 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;  // 输出此消息
        }
    }
    
  2. 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
    }
    
  3. 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 的实现原理
  1. 引用计数机制

    • 每个 shared_ptr 都会维护两个计数器:
      • 强引用计数(strong reference count):表示有多少个 shared_ptr 实例共享对象的所有权。
      • 弱引用计数(weak reference count):表示有多少个 weak_ptr 引用该对象,weak_ptr 不增加强引用计数,只增加弱引用计数。

    当一个 shared_ptr 创建时,强引用计数会增加。当某个 shared_ptr 被复制时,强引用计数会再次增加;当 shared_ptr 被销毁或重置时,强引用计数会减少。当强引用计数归零时,表示没有 shared_ptr 指向该对象,程序会自动销毁对象并释放内存。

  2. 控制块(Control Block)

    • shared_ptr 并不是直接将引用计数保存在智能指针对象本身,而是引入了一个额外的控制块来维护引用计数及其他信息。

    • 控制块包含

      • 对象的引用计数(强引用计数和弱引用计数)。
      • 指向实际管理的对象的指针。
      • 可能还包含自定义的删除器,用来定义如何释放对象内存。
    • shared_ptr 创建时,控制块也会被分配,shared_ptr 会指向这个控制块并管理其生命周期。当 shared_ptr 被复制时,所有 shared_ptr 都指向同一个控制块,并共享其引用计数。当最后一个 shared_ptr 被销毁时,控制块中的对象指针被释放,随后控制块本身也会被销毁。

  3. 线程安全

    • shared_ptr 的引用计数是线程安全的,这意味着多个线程可以安全地访问和修改引用计数。为了确保线程安全,shared_ptr 使用了原子操作来管理引用计数的增减操作。
  4. 析构与释放内存

    • 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 只指向控制块,而控制块持有指向实际对象的指针。控制块如下所示:

控制块包含了实际对象的指针和引用计数。随着 sp1sp2 的拷贝和销毁,控制块的引用计数会相应变化。

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&&)专门用于绑定到右值(即那些不具名的临时对象或即将被销毁的对象),从而可以对这些临时对象进行高效的资源操作。

右值引用的主要作用如下:

  1. 支持移动语义(Move Semantics)
  2. 避免不必要的拷贝(优化性能)
  3. 实现完美转发(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++ 中常见的指针错误,可能导致未定义行为甚至程序崩溃。虽然它们都与无效的指针指向有关,但有本质区别。

悬挂指针是指向已被释放或已经失效的内存区域的指针。换句话说,悬挂指针指向的内存曾经是有效的,但由于内存已经被释放或超出作用域,指针仍然持有旧的地址,导致悬挂状态。

产生悬挂指针的常见原因

  1. 释放动态分配的内存后未将指针置空: 动态分配的内存通过 deletefree 释放后,指针仍然保存该内存地址,尽管内存已经被释放。

    int* ptr = new int(10);
    delete ptr;  // 释放内存,但 ptr 仍指向已释放的区域
    *ptr = 5;    // 未定义行为,因为 ptr 是悬挂指针
    

    修正:释放内存后,将指针置为 nullptr,避免继续访问该内存。

    delete ptr;
    ptr = nullptr;  // 防止悬挂指针
    
  2. 指向自动变量的指针在超出作用域后被使用: 自动变量(局部变量)在函数返回时会销毁,如果在返回后仍然访问指向这些变量的指针,会造成悬挂指针。

    int* danglingPointer() {
        int localVar = 42;
        return &localVar;  // 返回局部变量的地址
    }
    
    int* p = danglingPointer();  // p 成为悬挂指针,指向已销毁的 localVar
    
野指针(Wild Pointer)

野指针是指向一个随机的、未初始化的内存地址的指针。野指针没有被赋予一个有效的内存地址,通常是未经过正确初始化的指针。

产生野指针的常见原因

  1. 指针声明后未初始化: 声明一个指针变量时,未对其进行初始化,默认情况下它可能指向一个随机的地址。如果试图使用这样的指针,会导致野指针问题。

    int* ptr;  // 未初始化的指针
    *ptr = 10; // 野指针,未定义行为
    
    int* ptr = nullptr;
    if (ptr) {
        *ptr = 10;  // 只有在 ptr 指向有效地址时才操作
    }
    

  2. 指针偏移越界: 当对数组或内存块进行操作时,如果指针偏移超出合法的内存范围,就会成为野指针。

    int arr[5];
    int* p = arr + 10;  // p 指向数组边界外,野指针
    *p = 20;  // 未定义行为
    
悬挂指针与野指针的区别总结
区别点悬挂指针(Dangling Pointer)野指针(Wild Pointer)
定义指向已释放或已失效内存的指针指向随机或未初始化内存的指针
常见原因内存释放后未将指针置空、超出作用域的局部变量地址未初始化的指针、数组或内存块越界
如何避免在释放动态内存后将指针置为 nullptr,避免使用超出作用域的指针在指针声明时进行初始化,避免未初始化使用
行为特征指针曾指向有效的内存,但该内存已无效指针从未指向过有效的内存区域
导致的后果可能访问已释放的内存,导致程序崩溃或异常行为访问未初始化的地址,导致未定义行为(程序崩溃、无效数据等)
总结
  • 悬挂指针:指向曾经有效但已被释放的内存地址,常见于释放内存后未置空的指针。
  • 野指针:指向未初始化的随机内存地址,常见于指针未初始化或越界访问。

5.句柄和指针的区别和联系是什么?

句柄(Handle)和指针(Pointer)在计算机编程中都是用于引用或访问资源的方式,但它们有着不同的概念和使用场景。理解它们的区别和联系有助于更好地理解系统资源管理的机制,尤其是在操作系统或大型系统开发中。

指针是一个变量,它直接存储内存地址,用于指向内存中的某个具体位置。指针可以用来访问、修改该地址处的数据。在 C/C++ 中,指针是核心概念,允许对内存进行低级操作。

指针的特性:
  1. 直接指向内存地址:指针变量持有的是内存的实际地址。
  2. 灵活性:可以用于指向任何类型的数据(包括数组、结构体、函数等)。
  3. 操作内存:通过指针可以直接读写指向的内存区域。
  4. 需要管理内存:程序员需要手动管理指针指向的内存(特别是在动态内存分配时),否则容易出现内存泄漏或未定义行为。
指针的常见用途:
  • 动态内存管理(如 mallocnew)。
  • 数据结构(如链表、树等)的实现。
  • 访问数组或传递数组给函数。
  • 实现函数指针、回调机制等。

句柄一种抽象化的引用,用于表示操作系统或库中的资源(如文件、窗口、线程、进程、设备等)。它是由系统或库返回的一个标识符,而不是直接的内存地址。句柄通常是一个整数或指针,程序通过这个标识符间接访问资源,操作系统负责管理句柄所代表的实际资源。

句柄的特性:
  1. 间接访问资源:句柄是一种抽象的引用,不能直接访问内存,而是通过系统或库提供的 API 进行操作。
    • 例如,在 Windows 中,打开一个文件时会得到一个文件句柄(如 HANDLE 类型),之后所有对文件的操作都通过这个句柄进行:
  2. 资源安全性和管理:句柄隐藏了资源的底层实现细节,操作系统或库可以通过句柄进行资源管理(如分配、释放、访问控制等),使得用户无法直接操作底层资源,增强了系统的稳定性和安全性。
  3. 系统依赖:句柄通常与操作系统紧密相关,在不同的操作系统上,句柄的实现方式可能不同。例如,Windows 系统中有文件句柄、进程句柄、线程句柄等,Linux 系统中文件描述符(file descriptor)也可以看作一种句柄。
  4. 自动管理:操作系统通常负责创建和销毁句柄所对应的资源,程序员只需要在适当的时候释放句柄。
句柄的常见用途:
  • 操作系统资源管理:在操作系统中,所有的系统资源(如文件、设备、窗口等)都通过句柄来访问。
    • 例如,在 Windows API 中,句柄用来操作窗口(HWND)、设备上下文(HDC)、进程(HANDLE)等。
区别和联系
特性指针(Pointer)句柄(Handle)
存储内容存储的是直接的内存地址存储的是间接的标识符,系统通过句柄来管理资源
访问方式可以直接访问和操作内存地址中的数据通过系统或库的 API 访问资源,不能直接操作内存
灵活性可以指向任意内存地址,具有很大的灵活性和自由度只能通过句柄的 API 来访问资源,安全性较高
安全性不安全,可能会指向无效的内存地址,容易导致悬挂指针等错误安全性较高,资源管理由操作系统或库来负责
资源类型用于指向任何类型的内存数据用于表示系统资源(文件、进程、线程、窗口等)
平台相关性平台无关,C/C++ 代码中的指针在不同平台都可以使用平台相关,句柄通常与操作系统的实现密切相关
资源管理程序员需要手动管理内存,可能出现内存泄漏或悬挂指针由操作系统或库管理,程序员只需在适当时机释放句柄
总结
  1. 指针是直接存储内存地址的变量,可以直接操作内存,灵活但不安全,程序员需要自行管理内存。
  2. 句柄是操作系统或库提供的抽象引用,用于间接访问资源。句柄隐藏了资源的具体实现和内存细节,增强了系统的安全性和可管理性。
  3. 联系:句柄有时在底层实现上可能会使用指针(如 Windows 中的 HANDLE 可能是一个指针),但开发者无法直接操作资源内存,必须通过 API 访问。

6.extern “C”

参考我的文章extern “C“ 的作用、C++ 和 C 编译的不同、C++ 编译过程的五个主要阶段

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值