C++语言特性常见问题

devtools/2024/9/23 22:36:53/

C++智能指针share_ptr 循环引用,怎么解决?

循环引用是指两个或多个对象相互持有对方的引用,导致这些对象的引用计数永远不会归零,从而无法释放内存,最终导致内存泄漏。

循环引用的例子

假设有两个对象 AB,其中 A 持有 Bstd::shared_ptr,同时 B 也持有 Astd::shared_ptr。这种情况下,两个对象的引用计数都大于零,即使程序不再使用这些对象,它们也无法被销毁,内存不会被释放。

#include <iostream>
#include <memory>class B;  // 前向声明class A {
public:std::shared_ptr<B> b_ptr;~A() { std::cout << "A destroyed\n"; }
};class B {
public:std::shared_ptr<A> a_ptr;~B() { std::cout << "B destroyed\n"; }
};int main() {auto a = std::make_shared<A>();auto b = std::make_shared<B>();a->b_ptr = b;b->a_ptr = a;return 0;
}

在上述代码中,AB 之间形成了循环引用。当 main 函数结束时,ab 离开作用域,但由于循环引用,它们的引用计数无法归零,导致 AB 对象的析构函数不会被调用,内存泄漏。

解决循环引用

解决循环引用的常用方法是将其中一个 std::shared_ptr 改为 std::weak_ptr,这样就不会增加引用计数,从而打破循环。

#include <iostream>
#include <memory>class B;  // 前向声明class A {
public:std::shared_ptr<B> b_ptr;~A() { std::cout << "A destroyed\n"; }
};class B {
public:std::weak_ptr<A> a_ptr;  // 使用 std::weak_ptr 打破循环引用~B() { std::cout << "B destroyed\n"; }
};int main() {auto a = std::make_shared<A>();auto b = std::make_shared<B>();a->b_ptr = b;b->a_ptr = a;return 0;
}

在这段代码中,我们将 B 类中的 a_ptr 改为 std::weak_ptrstd::weak_ptr 不增加引用计数,这意味着当 std::shared_ptr<A> astd::shared_ptr<B> b 离开作用域时,它们的引用计数会正确归零,AB 的析构函数将被调用,内存被释放。

总结

  • 循环引用 是指两个或多个对象相互持有对方的 std::shared_ptr,导致内存无法释放。
  • 解决方案 是将其中一个 std::shared_ptr 改为 std::weak_ptr,从而打破循环引用,避免内存泄漏。
  • 引用计数std::shared_ptr 的引用计数在创建、复制、移动和销毁时发生变化,用于管理对象的生命周期。
  • 弱引用计数std::weak_ptr 的弱引用计数在创建、复制、移动和销毁时变化,不影响对象的生命周期,但用于监控对象是否仍然存在。
  • 当最后一个 std::shared_ptr 被销毁时,引用计数归零,对象会被销毁并释放内存。

new delete malloc free 区别?

3. 主要区别

  • 内存分配与初始化:

    • new 不仅分配内存,还会自动调用构造函数进行对象初始化。
    • malloc 只分配内存,不会调用构造函数,不进行初始化。
  • 内存释放与清理:

    • delete 释放内存时,会自动调用析构函数,以清理对象。
    • free 只释放内存,不会调用析构函数。
  • 返回类型:

    • new 返回的是分配对象类型的指针,不需要显式转换。
    • malloc 返回 void*,通常需要进行显式类型转换。
  • 异常处理:

    • new 分配失败时会抛出异常。
    • malloc 分配失败时返回 nullptr
  • 兼容性:

    • newdelete 是 C++ 的特性,而 mallocfree 是 C 语言的标准库函数,C++ 中也可以使用。
  • 用法推荐:

    • 在 C++ 中,推荐使用 new/delete,因为它们与对象的构造和析构结合更紧密,有助于防止内存泄漏和未初始化问题。
    • malloc/free 更适合 C 语言或需要手动管理对象生命周期的特定场景。

野指针的危害

使用野指针会导致以下问题:

  • 未定义行为:访问已释放或无效的内存地址可能导致程序崩溃、数据损坏或不可预测的行为。
  • 内存错误:可能导致内存泄漏、非法访问错误(如 segmentation fault)、程序异常终止等问题。
  • 安全漏洞:在某些情况下,攻击者可能利用野指针引发的漏洞进行攻击,如缓冲区溢出攻击。

如何避免野指针?

为了避免野指针,可以采取以下措施:

  1. 指针初始化

    • 在声明指针时,将指针初始化为 nullptr,确保指针指向一个已知的无效地址。
    • 始终检查指针是否为 nullptr 后再进行解引用操作。

虚函数机制

虚函数表(vtable)

为了支持虚函数,编译器在内部实现了虚函数表(vtable)和虚函数表指针(vptr):

  • 虚函数表(vtable):

    • 对于每个包含虚函数的类,编译器生成一个虚函数表。虚函数表是一个指针数组,数组中的每个指针指向该类的虚函数的实际实现。
    • 虚函数表的存在使得通过基类指针或引用可以正确地调用派生类中重写的虚函数。
  • 虚函数表指针(vptr):

    • 每个对象都有一个隐藏的指针(vptr),它指向对象所属类的虚函数表。
    • 当调用虚函数时,编译器通过对象的 vptr 查找对应的虚函数地址,并调用实际的函数实

当基类指针指向派生类对象时,对象的虚函数表指针(vptr)指向派生类的虚函数表。通过基类指针调用虚函数时,程序使用对象的 vptr 查找虚函数表,从而执行实际的派生类函数实现。这种机制支持运行时多态,使得通过基类指针可以动态地调用派生类中的重写函数。

3. 构造函数和析构函数可以是虚函数吗?

构造函数不能是虚函数
  • 原因:
    • 在对象创建时,构造函数是用于初始化对象的,并且在对象的构造函数执行期间,对象还没有完全形成。因此,虚函数表还未完全建立或指向基类的虚函数表。此时无法进行虚函数的多态行为。
    • 如果构造函数是虚函数,会导致编译器无法确定调用哪个类的构造函数,因为多态性依赖于对象的完全构造,而构造函数是为完成这一过程而设计的。

因此,构造函数不能是虚函数

析构函数可以是虚函数
  • 原因:
    • 析构函数可以是虚函数,这是为了确保当通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,避免资源泄漏。
    • 当一个对象通过基类指针或引用被销毁时,如果基类的析构函数是虚函数,C++ 运行时系统会根据对象的实际类型调用正确的析构函数链,确保派生类的资源得到正确释放。

深浅拷贝?

  • 浅拷贝:

    • 复制对象的所有成员变量,但指针成员仍指向相同的内存。
    • 易导致内存管理问题,如悬空指针、重复释放内存等。
  • 深拷贝:

    • 复制对象的所有成员变量,并为指针成员分配新的内存。
    • 安全性更高,避免了多个对象间的内存共享问题。

set map unordered_map ,unordered_set?

  • setmap:

    • 使用平衡二叉搜索树(如红黑树)实现。
    • 元素按顺序排列,支持按序遍历。
    • 操作复杂度为 O(log n)。
    • set 存储单一元素,map 存储键值对。
  • unordered_setunordered_map:

    • 使用哈希表实现。
    • 元素无序,不能保证遍历时的顺序。
    • 平均操作复杂度为 O(1),最坏情况下为 O(n)。
    • unordered_set 存储单一元素,unordered_map 存储键值对。

左值引用右值引用?

1. 左值引用(Lvalue Reference)和右值引用(Rvalue Reference)

  • 左值引用 (Lvalue Reference):

    • 左值引用可以引用一个左值,左值是表达式中一个持久的对象或内存位置。典型的左值包括变量、数组元素、对象的成员等。
    • 语法:Type& ref = obj;
    • 例如:int a = 10; int& ref = a; 这里 refa 的左值引用。
  • 右值引用 (Rvalue Reference):

    • 右值引用可以引用一个右值,右值是表达式中一个临时的、不持久的对象或内存位置,通常用于表示即将被销毁的对象。
    • 语法:Type&& ref = std::move(obj);
    • 右值引用的引入使得 C++ 可以更好地实现移动语义(Move Semantics),例如:int&& rref = 10;
  • 用途:

    • 右值引用主要用于实现移动构造函数和移动赋值操作符,避免不必要的深度拷贝,从而提高性能。

什么是菱形继承

  • 菱形继承 是多重继承的一种情况,指的是一个类从两个基类继承,而这两个基类又继承自同一个父类。它形成一个菱形的继承关系图。

  • 问题: 这会导致基类的成员在派生类中出现两次,从而造成二义性和资源浪费。为了解决这个问题,C++ 提供了虚继承

今天先总结这些常见问题吧-·············


http://www.ppmy.cn/devtools/100157.html

相关文章

Linux网络基础

从本篇开始将进入介绍 Linux 中的网络知识&#xff0c;本篇是先对网络中的一些基础概念做介绍。 其中主要介绍了 OSI 七层协议&#xff08;TCP/IP 五层协议&#xff09;&#xff0c;以及网络传输的流程&#xff0c;其中主要介绍了局域网传输和广域网&#xff08;跨网络&#xf…

ARM——操作示例

操作流程: 一、实现一个led亮灯 &#xff08;1&#xff09;GPIO&#xff1a;可编程的输入输出引脚 每一组io都有一个寄存GP*CON控制引脚作用&#xff0c;每个io都有2个位&#xff0c;控制引脚作用 每一组io都有一个寄存GP*DAT控制引脚数据&#xff0c;每个io都有1个位&a…

python之selenium操作下拉滚动条方法

前言 ①在HTML页面中&#xff0c;由于前端技术框架的原因&#xff0c;页面中的一些元素为动态显示&#xff0c;元素根据滚动条的下拉而被加载&#xff08;元素在当前显示的页面不可见&#xff0c;拖动页面下拉直到该元素出现&#xff0c;此时才可以定位到该元素。&#xff09;…

打卡学习Python爬虫第六天|处理cookie登录小说网

引言&#xff1a;一些网站不需要登录就能看到信息&#xff0c;但对于需要登录才能看见信息的网站&#xff0c;我们就需要借助cookie&#xff0c;使爬虫能够顺利登录网站&#xff0c;从而获取所需数据。 1、登录后查看页面源代码 并没有我们需要的数据 2、利用抓包工具 右键--…

Spring框架

spring的整体架构&#xff1a; DefaultListableBeanFactory是整个bean加载的核心部分&#xff0c;它是Spring注册及加载bean的默认实现。 test&#xff1a; bean.xml中有很多bean的定义&#xff1a; <!-- bean 标签演示 --><bean id"m416" class"com…

数论之组合数

组合数1&#xff1a; 预处理每一个组合数的大小 类似于dp&#xff0c;从a个苹果里面选b个出来&#xff1a;首先从a个苹果里面拿出来一个&#xff0c;这样就分成了两种&#xff0c;一种是包括这个拿出来的苹果的方案数&#xff0c;此时就只需要拿b-1个苹果。一种是不包括这种苹…

文件包含漏洞(1)

目录 PHP伪协议 php://input Example 1&#xff1a; 造成任意代码执行 Example 2&#xff1a; 文件内容绕过 php://filer zip:// PHP伪协议 php://input Example 1&#xff1a; 造成任意代码执行 搭建环境 <meta charset"utf8"> <?php error_repo…

【vue运行报错】无法加载文件 D:\nodejs\node_global\webpack.ps1,因为在此系统上禁止运行脚本。

根据这篇博客安装nodejs和vue&#xff0c;执行webpack -v 和vue -v 报错。 Vue安装与配置教程&#xff08;非常详细&#xff09;从零基础入门到精通&#xff0c;看完这一篇就够了-CSDN博客 【解决方法】 &#xff08;1&#xff09;以管理员身份运行终端/命令提示符&#xff…