C/C++语言常见问题-智能指针、多态原理

文章目录

智能指针实现原理

在C++中,智能指针是一种自动管理内存分配和释放的工具,它们帮助程序员避免内存泄漏和其他与动态内存分配相关的问题。C++11标准引入了几种智能指针,主要包括std::unique_ptrstd::shared_ptrstd::weak_ptr。下面将分别介绍这些智能指针的实现原理。

  1. std::unique_ptr
    std::unique_ptr是一种独占所有权的智能指针,意味着同一时间只能有一个std::unique_ptr指向特定对象。它不允许复制,但可以移动。

实现原理:

  • 构造函数:接受原始指针,并在构造时接管所有权。
  • 析构函数 :当std::unique_ptr对象被销毁时,它会自动删除它所拥有的对象。
  • 移动语义 :通过移动构造函数和移动赋值运算符,可以将所有权从一个std::unique_ptr转移到另一个,原指针变为空。
  • 删除复制语义:复制构造函数和复制赋值运算符被删除,防止多个指针指向同一对象。
  1. std::shared_ptr
    std::shared_ptr是一种共享所有权的智能指针,允许多个std::shared_ptr实例共享同一对象。

实现原理:

  • 引用计数std::shared_ptr内部维护一个引用计数器,用来跟踪有多少个std::shared_ptr实例共享同一个对象。
  • 构造函数:接受原始指针,并在构造时接管所有权,同时初始化引用计数为1。
  • 拷贝构造函数和拷贝赋值运算符 :当std::shared_ptr被复制时,引用计数增加。
  • 移动构造函数和移动赋值运算符 :移动语义允许将所有权从一个std::shared_ptr转移到另一个,同时减少原指针的引用计数。
  • 析构函数 :当最后一个std::shared_ptr被销毁或被重新赋值时,引用计数减至0,此时自动删除所管理的对象。
  1. std::weak_ptr
    std::weak_ptr是一种不控制对象生命周期的智能指针,通常与std::shared_ptr配合使用,用于解决强引用循环问题。

实现原理:

  • 弱引用计数std::weak_ptr不增加对象的引用计数,但可以观察引用计数。
  • 构造函数 :可以从一个std::shared_ptr构造,但不增加引用计数。
  • 析构函数:不删除对象,只是减少内部的弱引用计数。
  • 升级为std::shared_ptr :如果对象仍然存在(即std::shared_ptr的引用计数大于0),std::weak_ptr可以被升级为一个std::shared_ptr

总结

智能指针通过封装原始指针和自动管理内存的生命周期,减少了内存泄漏的风险。std::unique_ptr适用于单一所有权的情况,std::shared_ptr适用于需要多个指针共享同一对象的情况,而std::weak_ptr则用于打破强引用循环,提供更灵活的控制。

智能指针在C++中的底层实现通常依赖于几种基本的数据结构,这些数据结构帮助智能指针管理对象的生命周期和引用计数。以下是这些数据结构的简要说明:

  1. std::unique_ptr
    std::unique_ptr通常不需要复杂的数据结构来管理引用计数,因为它不允许共享所有权。它的实现相对简单,通常只包含一个指向管理对象的原始指针,可能还包括一个自定义删除器(如果提供了的话)。
  2. std::shared_ptr
    std::shared_ptr的实现依赖于两个主要的数据结构:
  • 控制块(Control Block) :这是一个结构体,通常包含至少两个指针,一个指向管理的对象,另一个指向一个引用计数器。引用计数器记录了当前有多少个std::shared_ptr实例共享这个对象。
  • 引用计数器 :这是一个整数值,用来跟踪有多少个std::shared_ptr实例共享同一个对象。当最后一个std::shared_ptr被销毁或被重新赋值时,引用计数减至0,对象会被自动删除。
  1. std::weak_ptr
    std::weak_ptr的实现也依赖于控制块,但它不直接管理对象的生命周期。它包含以下元素:
  • 弱引用计数器 :与std::shared_ptr的控制块中的引用计数器不同,std::weak_ptr的弱引用计数器不增加对象的引用计数,它只是用来跟踪有多少个std::weak_ptr实例观察这个对象。
  • 指向控制块的指针std::weak_ptr包含一个指向对象控制块的指针,通过这个指针,std::weak_ptr可以访问对象的引用计数和对象本身。

实现细节

在实际的实现中,控制块可能包含更多的信息,例如:

  • 自定义删除器:如果提供了自定义删除器,控制块中也会包含指向这个删除器的指针。
  • 线程安全:在多线程环境中,引用计数的增加和减少需要是原子操作,以避免竞态条件。

总结

智能指针的底层实现通过控制块和引用计数器来管理对象的生命周期和共享状态。这些数据结构的设计使得智能指针能够在不牺牲性能的情况下,提供自动的内存管理和引用计数功能。

智能指针,里面的计数器何时会改变

在C++中,智能指针的引用计数器(对于std::shared_ptr)或控制块(对于std::unique_ptrstd::weak_ptr)的值会在以下情况下改变:

std::shared_ptr的引用计数器:

  1. 增加计数

    • 当通过拷贝构造函数或拷贝赋值运算符创建一个新的std::shared_ptr实例时,指向同一对象的std::shared_ptr数量增加,引用计数器增加。
    • 当通过std::shared_ptrclone函数创建一个新的std::shared_ptr时,引用计数器也会增加。
  2. 减少计数

    • 当一个std::shared_ptr实例被销毁(例如离开其作用域或被显式析构)时,引用计数器减少。
    • 当通过移动构造函数或移动赋值运算符将std::shared_ptr的所有权转移给另一个实例时,原实例的引用计数器减少。
    • 当通过std::shared_ptrreset方法重置指针时,如果新的控制块与旧的不同,旧控制块的引用计数器减少。

std::weak_ptr的弱引用计数器:

  1. 增加计数

    • 当创建一个新的std::weak_ptr实例,并且它与一个现有的std::shared_ptr共享同一个控制块时,弱引用计数器增加。
  2. 减少计数

    • 当一个std::weak_ptr实例被销毁时,如果它之前是指向一个对象的最后一个弱引用,弱引用计数器减少。

std::unique_ptr的控制块:

std::unique_ptr不使用引用计数器,因为它不允许共享所有权。但是,它的控制块(如果提供了自定义删除器)会在以下情况下改变:

  1. 所有权转移
    • 当通过移动构造函数或移动赋值运算符将std::unique_ptr的所有权从一个实例转移到另一个实例时,原实例的控制块变为空。

总结:

  • std::shared_ptr:引用计数器会在拷贝、赋值、销毁、移动和重置时改变。
  • std::weak_ptr:弱引用计数器会在创建和销毁时改变,但不会直接影响对象的生命周期。
  • std::unique_ptr:不使用引用计数器,控制块在所有权转移时改变。

智能指针的这些机制确保了对象的生命周期被正确管理,同时避免了内存泄漏和其他资源管理错误。

智能指针和管理的对象分别在哪个区

(智能指针本身在栈区,托管的资源在堆区,利用了栈对象超出生命周期后自动析构的特征,所以无需手动delete释放资源)

在C++中,智能指针和它们管理的对象的存储位置取决于它们是如何被创建和使用的。以下是智能指针和它们管理的对象可能位于的内存区域:

  1. 智能指针本身

    • 栈区(Stack):如果智能指针是局部变量或者作为函数参数传递,它们通常存储在栈区。当函数结束时,智能指针会被自动销毁,它们的析构函数会被调用,从而管理的对象也会被适当地释放。
    • 堆区(Heap) :如果智能指针是通过new操作符或者std::make_shared等函数动态分配的,它们将存储在堆区。这种情况下,需要手动调用delete来释放智能指针本身占用的内存,但它们管理的对象会在智能指针被销毁时自动释放。
  2. 管理的对象

    • 堆区(Heap):智能指针管理的对象通常存储在堆区。这是因为对象的生命周期可能需要比创建它们的函数更长,或者对象的大小可能在编译时未知,因此不能在栈区分配。
    • 例如,std::shared_ptrstd::unique_ptr通常指向堆区中的对象。当智能指针被销毁时,它们会自动释放指向的对象。
  3. 控制块

    • 对于std::shared_ptr来说,还有一个与之关联的控制块,它也存储在堆区。控制块包含了引用计数器,用于跟踪有多少个std::shared_ptr实例共享同一个对象。当最后一个引用被销毁时,控制块中的引用计数器会减少到0,对象和控制块都会被释放。
  4. 弱引用计数器

    • 对于std::weak_ptr来说,它不直接管理对象的生命周期,但是它可能包含一个指向控制块的指针,这个指针用于跟踪弱引用的数量。这个计数器通常也存储在控制块中,与std::shared_ptr的控制块相同。

总结来说,智能指针本身可以位于栈区或堆区,而它们管理的对象通常位于堆区。智能指针的析构函数确保了对象在不再需要时被自动释放,这是通过控制块中的引用计数器来管理的。这种设计模式减少了手动管理内存的需要,从而降低了内存泄漏和其他资源管理错误的风险。

面向对象的特性:多态原理

面向对象编程(OOP)是一种编程范式,它使用"对象"来设计应用程序和程序结构。在面向对象编程中,多态性(Polymorphism)是一个核心特性,它允许你以统一的方式处理不同类型的对象。多态性使得程序更加灵活和可扩展。

多态性的定义

多态性指的是同一个操作作用于不同的对象时,可以有不同的解释和不同的执行结果。在编程中,这通常意味着同一个方法或函数可以被不同的对象以不同的方式实现。

多态性的类型

  1. 编译时多态(静态多态)

    • 方法重载(Overloading):同一个类中有多个同名方法,它们的参数列表不同(参数类型、数量或顺序不同)。
    • 操作符重载(Operator Overloading):对已有的操作符赋予新的功能,使其可以作用于用户定义的类型。
  2. 运行时多态(动态多态)

    • 方法重写(Overriding):在派生类中重新定义基类的方法。这是通过使用虚函数(virtual function)实现的,允许在运行时根据对象的实际类型调用相应的方法。

多态性的原理

  • 虚函数表(Virtual Table):在C++中,运行时多态性是通过虚函数表实现的。每个包含虚函数的类都有一个虚函数表,这个表包含了指向类中所有虚函数的指针。
  • 虚函数指针:每个对象都有一个指向其类虚函数表的指针。当通过基类指针或引用调用虚函数时,会使用这个指针来查找并调用正确的函数。

多态性的实现步骤

  1. 声明虚函数 :在基类中声明函数为虚函数(使用virtual关键字)。
  2. 派生类重写:在派生类中重写这个虚函数。
  3. 使用基类指针或引用:通过基类的指针或引用来调用虚函数。

示例代码(C++)

cpp 复制代码
#include <iostream>

class Animal {
public:
    virtual void speak() {
        std::cout << "Animal makes a sound\n";
    }
    virtual ~Animal() {} // 虚析构函数以确保派生类对象的正确析构
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog barks\n";
    }
};

class Cat : public Animal {
public:
    void speak() override {
        std::cout << "Cat meows\n";
    }
};

int main() {
    Animal* myAnimal = new Dog();
    myAnimal->speak(); // 输出 "Dog barks"

    Animal* anotherAnimal = new Cat();
    anotherAnimal->speak(); // 输出 "Cat meows"

    delete myAnimal;
    delete anotherAnimal;
    return 0;
}

在这个例子中,Animal类有一个虚函数speak()DogCat类分别重写了这个函数。通过基类指针调用speak()时,会根据对象的实际类型调用相应的函数,这就是多态性的体现。

多态性是面向对象编程中非常重要的特性,它提高了代码的可重用性和可维护性,同时也使得代码更加灵活和易于扩展。

C++11 是 C++ 语言的一次重大更新,它引入了许多新特性,使得 C++ 编程更加现代化和高效。以下是你提到的几个关键特性的详细介绍:

  1. 智能指针(Smart Pointers)

    • std::unique_ptr:表示独占所有权的智能指针,确保同一时间只有一个智能指针可以指向某个对象。当 std::unique_ptr 被销毁时,它所拥有的对象也会被自动删除。
    • std::shared_ptr:表示共享所有权的智能指针,多个 std::shared_ptr 可以指向同一个对象,对象会在最后一个指向它的智能指针被销毁时自动删除。
    • std::weak_ptr:用于解决 std::shared_ptr 相互引用时可能出现的循环引用问题。std::weak_ptr 不会增加对象的引用计数,它可以用来观察一个对象而不增加其引用计数。
  2. 类型推导(Type Deduction)

    • auto 关键字:允许编译器自动推导变量的类型。例如,auto x = someFunction();x 的类型将与 someFunction() 的返回类型相同。
    • decltype:用于推导表达式的类型,但不创建一个值。例如,decltype(someVariable) 将推导出 someVariable 的类型。
  3. Lambda 表达式

    • Lambda 表达式是一种匿名函数,可以在需要的地方定义并立即使用。它们通常用于简短的函数对象,或者作为参数传递给函数。
    • 基本语法:[capture](parameters) -> return_type { function_body }
    • capture 可以是值捕获(如 x)或引用捕获(如 &x),也可以是 this 指针的捕获。
    • parameters 是 Lambda 表达式的参数列表,如果省略参数列表,则默认为 ()
    • return_type 是 Lambda 表达式的返回类型,如果省略,则由编译器根据 function_body 推导。
  4. 多种容器的使用

    • std::array:固定大小的数组,大小在编译时确定。
    • std::vector:动态大小的数组,可以自动调整大小。
    • std::deque:双端队列,支持快速的随机访问和在两端快速插入/删除。
    • std::list:双向链表,支持快速的插入和删除操作。
    • std::forward_list:单向链表,只支持在头部和尾部快速插入/删除。
    • std::mapstd::unordered_map:键值对的集合,分别基于红黑树和哈希表实现。
    • std::setstd::unordered_set:元素集合,分别基于红黑树和哈希表实现。
  5. 多线程的使用

    • std::thread:表示一个线程,可以用来创建和管理线程。
    • std::mutex:互斥锁,用于保护共享数据,防止多个线程同时访问。
    • std::lock_guardstd::unique_lock:RAII(资源获取即初始化)风格的锁,用于自动管理互斥锁的锁定和解锁。
    • std::condition_variable:条件变量,用于线程间的同步,允许一个或多个线程等待某个条件成立。
    • std::futurestd::promise:用于线程间的数据传递,std::promise 可以设置一个值,而 std::future 可以获取这个值。

这些特性极大地增强了 C++ 的表达力和灵活性,使得编写现代、高效、安全的代码变得更加容易。

相关推荐
TPBoreas2 小时前
Jenkins 改完端口号启动不起来了
java·开发语言
TE-茶叶蛋2 小时前
Vuerouter 的底层实现原理
开发语言·javascript·ecmascript
柒柒的代码学习日记3 小时前
数组和指针典型例题合集(一维数组、字符数组、二维数组)
c语言
云闲不收3 小时前
设计模式原则
开发语言
秋名RG3 小时前
深入解析建造者模式(Builder Pattern)——以Java实现复杂对象构建的艺术
java·开发语言·建造者模式
技术求索者3 小时前
c++学习
开发语言·c++·学习
山猪打不过家猪4 小时前
(二)毛子整洁架构(CQRS/Dapper/领域事件处理器/垂直切片)
开发语言·.net
方博士AI机器人6 小时前
Python 3.x 内置装饰器 (4) - @dataclass
开发语言·python
weixin_376934636 小时前
JDK Version Manager (JVMS)
java·开发语言
Logintern096 小时前
【每天学习一点点】使用Python的pathlib模块分割文件路径
开发语言·python·学习