6. C++的引用与指针

摘要:本文首先介绍 C++ 的内存模型和变量周期作为知识背景,接着对C++中的引用和指针(原始指针和智能指针)进行介绍。

1. 对象生命周期

什么是对象生命周期?简单来说,对象生命周期指的是:对象从创建直到被释放的时间跨度。很自然地,我们会意识到不是所有变量的创建方式和释放时间都是一样的,据此,我们把对象的生命周期氛围四种类型:静态存储周期、线程存储周期、自动存储周期和动态存储周期。

  1. 静态存储周期: Static Storage Duration

静态存储周期类型的对象在程序执行开始的时候就会分配内存,直至整个程序结束了才会释放。主要包括:全局变量、静态的类数据成员和函数中的局部变量,如下例子所示:

// 1. 全局变量
int global_var = 10;
// 2. 类中的静态数据成员
class MyClass{static int static_var_class;
}
// 3. 函数中的静态局部变量
int myFunc(){static int static_var_func;
}
  1. 线程存储周期: Thread Storage Duration

线程存储周期类型的对象只会在指定的线程内进行内存的分配与释放。在多线程编程中,为了避免数据紊乱,可以使用该方法,当然以下提及的自动存储周期严格来说也能算是线程周期,只不过这个线程是代码默认的主线程,因此不需要额外标注。如下例子展示如何使用线程存储周期的变量:

thread_local int thread_var;
  1. 自动存储周期: Automatic Storage Duration

自动存储周期类型的对象只会存在于其被声明和定义的作用域内,一旦退出作用域则自动释放,如函数的参数或者其内部定义的局部变量。这是最常见或者说默认的定义类型,不需要额外的关键字,以下例子展示的是函数内部定义的局部变量:

void myFunction() {int local_var;           // Automatic storage duration
}
  1. 动态存储周期: Dynamic Storage Duration

动态存储周期类型的对象在程序执行的时候通过关键字 new 或 malloc 实时分配内存,直至整个作用域退出也不会自动释放内存,必须通过使用关键字 delete 或 free 函数进行手动释放,否则会造成内存泄漏的问题。如下例子展示如何分配和释放一个原始指针变量:

int* ptr = new int;  // 定义一个原始指针
delete ptr;  // 释放原始指针

1*. 补充:内存泄漏

内存泄漏:指的是程序从堆分配内存但是不把内存释放到操作系统中,导致内存耗尽或者程序奔溃。以下一个例子展示内存泄漏:

void memory_leak(){int* ptr = new int[100];  // 创建一个原始指针并指向一个整型数组// 其他功能代码 ...// 确实手动释放内存操作: delete[] ptr;
}   // 导致内存泄漏:函数作用域结束了,ptr 指针已经没用了,但是没有释放内存

除了通过使用关键字 delete 或 free 函数进行手动释放外,还可以通过智能指针、RAII(Resource Acquisition Is Initialization)和C++标准库的容器(vector)来进行自动释放。

2. 内存模型

为什么需要了解C++的内存模型?内存模型定义了如何在C++去存储和使用数据(与变量的生命周期对应),了解C++的有利于优化对内存资源的使用和整个程序的表现。

C++的数据模型主要包括四个部分:栈、堆、数据段和代码段。

  1. 栈内存

自动生命周期的变量,如函数参数或局部变量都是使用栈的形式进行存储的。栈内存通过编译器进行管理,可以实现自动分配和释放。根据栈 先进后出 的特定,很容易知道最后定义的变量其实是最先释放的。

  1. 堆内存

堆内存则被用于动态生命周期变量,如通过 new 关键字手动定义的对象。根据堆 logn 的查找复杂度特定,很容易知道由堆内存管理的对象存储空间更大,但查找速度更快。

  1. 数据段

数据段包括两个部分:初始化数据段和未初始化数据段。两者的区别在于是否在变量声明的同时做定义,数据段主要包括:全局变量、静态变量和常变量。以下例子进行简单解释:

// 初始化数据段
int global_var = 10;  // 全局变量
static int static_var = 20;  // 静态变量
const int const_var = 30; // 常变量// 未初始化数据段
int global_var2;  // 只是声明了变量,但是没有对取值进行初始化定义
  1. 代码段

代码段,也成为文本段,用于存储程序的可执行代码(机器语言),通常存在仅读的内存,防止被意外修改;

3. 引用 reference

引用,常跟别名联系在一起,变量的引用和变量本身共享同一块内存,修改变量的引用时,变量本身的取值也会发生改变,通俗来说,两者只是一个对象的两个名字而已,故称为别名。在另一个角度,引用操作可以看作一个常指针,一旦这个常指针存储了地址,这个地址是无法修改的。

  1. 引用的声明与初始化:见例子
int raw = 10;  // 原始变量
int& ref = raw; // 创建一个引用变量 ref 指向(引用)变量 raw
raw = 20;
std::cout << "ref is: " << ref << endl; // 修改原数据的取值,引用变量的取值也会发生变换,反之亦然;
  1. 引用作为函数参数:浅拷贝/地址传递
void swap(int& a, int& b) {int temp = a;a = b;b = temp;
}int main() {int x = 5, y = 10;cout << "Before Swap: x = " << x << " y = " << y << endl; // Outputs 5 10swap(x, y);cout << "After Swap: x = " << x << " y = " << y << endl;  // Outputs 10 5
}

4. 指针

指针,本质上也是一个变量,只是这个变量存储的是另外一个变量/函数的地址/首地址。

4.1 原始指针

原始指针指的是直接存储其他低层次数据的地址的指针,如 int/float/char 、数组等。可以通过关键字 new/new[] 来创建指针,通过关键字 delete/delete[] 来释放内存,如下面例子所示:

int* ptr_int = new int;  // 创建整型指针
float* ptr_array = new float[100];  // 创建浮点型数组for(i=0; i<100;i++){ptr_array[i] = i; // 数组指针的使用;
}delete[] ptr_array;  // 释放数组指针
delete ptr_int;  // 释放整型指针

4.2 智能指针

智能指针相对原始指针,一个显著的区别在于智能指针不需要手动释放。智能指针主要包括:unique指针、shared指针和weak指针。

4.2.1 unique指针

unique指针可通过 std::unique_ptr 标准库创建,本质上是一个用于管理单个对象或者数组的模板类。

unique,顾名思义,唯一的。它表示每个对象/数组只能被一个unique指针所指,这个对象/数组可以可以通过所有权的转移,实现被另一个unique指针所指,但是无法同时被两个unique指针所指。

为什么需要unique指针?

unique指针具有避免悬垂指针、减少内存泄漏和避免手动释放内存的好处!
对于原始指针,手动释放内存可以避免内存泄漏,这个在1.1*有提及到。那么指针悬空指针是什么?悬空指针的定义是:指针最初指向的内存已经被释放了的一种指针,其所指向的地址存储的值是无法预测的随机值,以下举例说明出现指针悬空的情形。而在unique指针根本就不存在 delete unique_p1语句,也不存在两个unique指针指向同一个变量,故而指针悬空的问题。

#include<iostream>void func(int* p){// 局部作用域int var = 5;p = &var;std::cout << *p << std::endl;  // 输出:5
}int main(){// 情形1:指针所指的内存被释放,则指针悬空,返回值无法估计int* p1 = new int;*p1 = 5;std::cout << *p1 << std::endl;  // 输出:5delete p1;std::cout << *p1 << std::endl;  // 输出:-1152576448// 情形2:两个指针指向同一块内存,其中指针所指的内存被释放,则另外一个指针悬空,返回值无法估计int* p2 = new int;int* p3 = new int;*p2 = 5;p3 = p2;std::cout << *p3 << std::endl;  // 输出:5delete p2;std::cout << *p3 << std::endl;  // 输出:-2100685696// 情形3:指针所指向的局部变量退出作用域,则局部变量被自动释放,指针悬空int* p4 = new int;func(p4);std::cout << *p4 << std::endl;  // 输出:-1163005939
}

接下来就是 unique指针 如何使用?包括如何创建 unique 指针,如何转移变量的所有权,以及如何自定义删除智能指针;

#include<iostream>
#include<memory> // 1. 引入 memory 头文件int main(){// 2. 初始化变量:创建unique指针指向整型变量的两种方式std::unique_ptr<int> p1(new int(666));  // 所指内存存储取值为666的整型变量std::unique_ptr<int> p2 = std::make_unique<int>(999);  // 更常用std::cout << *p1 << ", " << *p2 << std::endl; //输出: 666, 999// 3. 创建数组:创建unique指针指向整型数组的两种方式std::unique_ptr<int[]> p3(new int[10]);  // 长度为10的整型数组,数组取值未初始化std::unique_ptr<int[]> p4 = std::make_unique<int[]>(10);   // 更常用for(int i=0;i<10;i++){p3[i] = i;p4[i] = i;std::cout << p3[i] << ", " << p4[i] << std::endl;}// 4. 变量所有权的转移std::unique_ptr<int> p5 = std::move(p1); // p5 拥有变量,而指针 p1 自动销毁if(p1){std::cout << "p1 owns the object" << std::endl;}else if (p5){std::cout << "p5 owns the object" << std::endl;   // 输出:p5 owns the object}// 5. 自定义析构函数:智能指针默认会自动销毁,但是也可以自定义销毁方法;struct MyDeleter{void operator()(int* ptr){std::cout << "Custom Deleter: Deleting pointer" << std::endl;delete ptr;}};// std::unique_ptr<int, MyDeleter> p6 = std::make_unique<int, MyDeleter>(999, MyDeleter()); 使用此方法自定义析构函数会报错std::unique_ptr<int, MyDeleter> p7(new int(999), MyDeleter());return 0; //主函数结束后自动调用 MyDeleter() 删除指针,输出:Custom Deleter: Deleting pointer
}

4.2.2 shared 指针

unique 指针提到:每个对象/数组只能被一个unique指针所指,这个对象/数组可以可以通过所有权的转移,实现被另一个unique指针所指,但是无法同时被两个unique指针所指。

那么很自然的一个想法就是:存不存在一种智能指针,可以实现多个指针指向同一个变量?
答案是可以的,这种指针就是 shared 指针。

那么这种多个指针指向同一个变量智能指针会导致什么问题吗
很自然的一个问题就是:被多个指针所指的变量什么时候才会销毁?举个例子就是10个shared指针指向同一个变量,那么其中的一个或两个指针销毁后,被指的变量还在不在?当然,为了避免悬空指针,我们通过希望的是所有指针销毁后,变量才被销毁。为了实现这个直观的想法,不得不引入一个引用计数的概念,因此,每当增加一个shared指针指向变量,引用计数 +1,当引用计数等于0的时候,证明已经没有指针指向该变量了,该变量就可以自动销毁了。

那么我们下面用两个智能shared指针指向类对象来说明引用计数的使用方法:

#include<iostream>
#include<memory> // 1. 引入 memory 头文件// 2. 定义一个类
class MyClass{public:// 类里面只有构造函数和析构函数,通俗来说就是在对象的创建和销毁时就会调用该函数// 我们在构造函数和析构函数print一些内容就可以知道被shared指针所指的对象何时创建/销毁MyClass(){ std::cout << "Object is Constructed !" << std::endl;};~MyClass(){ std::cout << "Object is Destructed !" << std::endl;};
};int main(){std::shared_ptr<MyClass> p1(new MyClass());{// 以下进入局部作用域std::shared_ptr<MyClass> p2 = p1; // 类对象同时被 p1 和 p2 所指,引用计数为 2std::cout << "Inside the inner scope." << std::endl;// 退出局部作用域}// 退出局部作用域,引用计数减少为 1std::cout << "Outside the inner scope." << std::endl;
}

该段程序依次打印的内容是:

Object is Constructed !
Inside the inner scope.
Outside the inner scope.
Object is Destructed !

Destructed 语句在 Outside the inner scope语句之后,证明了只有引用计数为0,才会销毁变量;

4.2.3 weak 指针

弱指针,也叫做弱智能指针,顾名思义也就是处于智能指针和原始指针的指针类型。它能够处理 share 指针 中存在的循环引用的问题。

很自然什么是循环引用呢?以下举个例子说明:

#include<iostream>
#include<memory> // 1. 引入 memory 头文件// 2. 定义一个类
class MyClassB;
class MyClassA{public:MyClassA(){ std::cout << "Object A is Constructed !" << std::endl;};~MyClassA(){ std::cout << "Object A is Destructed !" << std::endl;};std::shared_ptr<MyClassB> pB;
};
class MyClassB{public:MyClassB(){ std::cout << "Object B is Constructed !" << std::endl;};~MyClassB(){ std::cout << "Object B is Destructed !" << std::endl;};std::shared_ptr<MyClassA> pA;
};
int main(){// while(True)  // 3. 如果添加 while 循环会造成内存验证泄漏,可能导致死机,欢迎试一试{// 以下进入局部作用域// 4. 创建两个 shared 指针,std::shared_ptr<MyClassA> p1(new MyClassA());  // 对象 A 的引用计数为 1std::shared_ptr<MyClassB> p2(new MyClassB());   // 对象 B 的引用计数为 1// 两个指针的成员函数互相指向对方, -> 表示取成员变量p1->pB = p2;  // 对象 B 的引用计数为 2p2->pA = p1;  // 对象 A 的引用计数为 2std::cout << "Inside the inner scope." << std::endl;// 退出局部作用域,智能指针无法自动释放}std::cout << "Outside the inner scope." << std::endl;
}

该段程序依次打印的内容是:

Object A is Constructed !
Object B is Constructed !
Inside the inner scope.
Outside the inner scope.

Destructed 语句在退出局部作用域之后(Inside the inner scope之后,Outside the inner scope.之前)并没有打印,证明了循环引用导致退出局部作用域后,引用计数仍为2,不会销毁变量,导致内存泄漏;如此一来,A和B都互相指着对方吼,“放开我的引用!“,“你先发我的我就放你的!”,于是悲剧发生了。

那么接下来的问题就是:什么是 weak 指针,它是如何避免内存泄漏的?
weak 指针和 shared 指针的显著区别在于 weak 指针不会增加被指对象的引用计数。这能保证当一个 shared 指针和一个weak指针同时指向一个对象时,只要 shared 指针退出作用域后,这个对象就会被自动销毁。

那么 weak 指针的使用方法和 shared 指针有什么区别?如何去使用 weak 指针?
在使用 weak 指针时,必须要使用 lock() 函数基于weak指针创建一个新的 shared 指针,然后在新的作用域里面安全地使用这个新的 shared 指针,以下我们将基于上述循环引用的例子,将类A的shared指针转化为weak指针,讲述如何避免内存泄漏的。

#include<iostream>
#include<memory> // 1. 引入 memory 头文件// 2. 定义一个类
class MyClassB;
class MyClassA{public:MyClassA(){ std::cout << "Object A is Constructed !" << std::endl;};~MyClassA(){ std::cout << "Object A is Destructed !" << std::endl;};std::weak_ptr<MyClassB> pB;  // 使用弱指针
};
class MyClassB{public:MyClassB(){ std::cout << "Object B is Constructed !" << std::endl;};~MyClassB(){ std::cout << "Object B is Destructed !" << std::endl;};std::shared_ptr<MyClassA> pA;void DoSomething(){std::cout << "Doing something..." << std::endl;}
};
int main(){// while(True)  // 3. 如果添加 while 循环会造成内存验证泄漏,可能导致死机,欢迎试一试{// 以下进入局部作用域// 4. 创建两个 shared 指针,std::shared_ptr<MyClassA> p1(new MyClassA());  // 对象 A 的引用计数为 1std::shared_ptr<MyClassB> p2(new MyClassB());   // 对象 B 的引用计数为 1// 两个指针的成员函数互相指向对方, -> 表示取成员变量p1->pB = p2;  // weak指针不增加引用计数,对象 B 的引用计数为 1p2->pA = p1;  // 对象 A 的引用计数为 2if (auto shareFromWeak = p1->pB.lock()){  // 通过lock函数创建新的 shared 指针,shareFromWeak->DoSomething();  // 在新的局部作用域里面安全间接调用std::cout << "Shared uses count: " << shareFromWeak.use_count() << std::endl; //此时对象 B 的引用计数为 2}// 退出 if 函数的局部作用域,shareFromWeak指针自动释放,对象 B 的引用计数变为 1std::cout << "Inside the inner scope." << std::endl;// 退出局部作用域,智能指针自动释放}std::cout << "Outside the inner scope." << std::endl;
}

该段程序依次打印的内容是:

Object A is Constructed !
Object B is Constructed !
Doing something...
Shared uses count: 2
Inside the inner scope.
Object B is Destructed !
Object A is Destructed !
Outside the inner scope.

可以看到,退出局部作用域后,对象A和对象B都得到了释放,也就是说避免了循环引用导致的内存泄漏。

4. 总结

在本博客中,为了讲述 智能指针这一个概念,我们首先铺垫了一些基础知识,例如变量的声明周期和C++的内存模型,这对于理解内存的释放和局部作用域等概念非常有用。接着我们快速地介绍了引用和原始指针,针对原始指针的内存管理(释放和泄漏问题),我们进一步解释了智能指针,这包括 unique, shared 和 weak指针。其中,我们着重地介绍了shared因为循环引用导致的内存泄漏问题,以及如何使用weak指针避免这个循环引用,使得智能指针能够正确释放!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/582951.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JMeter逻辑控制器之While控制器

JMeter逻辑控制器之While控制器 1. 背景2.目的3. 介绍4.While示例4.1 添加While控制器4.2 While控制器面板4.3 While控制器添加请求4.3 While控制器应用场景 1. 背景 存在一些使用场景&#xff0c;比如&#xff1a;某个请求必须等待上一个请求正确响应后才能开始执行。或者&…

Idea如何从磁盘中应用 下载好的插件流程,安装zip压缩包。

1、将下载的插件文件&#xff08;通常是一个ZIP文件&#xff09;复制到IntelliJ IDEA的“plugins”文件夹中。 IDEA版本 2、重启IntelliJ IDEA。 3、在设置窗口中&#xff0c;选择左侧的“Plugins”。 4、选择之前复制到“plugins”文件夹中的插件文件&#xff0c;点击“OK”按…

基于Wenet长音频分割降噪识别

Wenet是一个流行的语音处理工具&#xff0c;它专注于长音频的处理&#xff0c;具备分割、降噪和识别功能。它的长音频分割降噪识别功能允许对长时间录制的音频进行分段处理&#xff0c;首先对音频进行分割&#xff0c;将其分解成更小的段落或语音片段。接着进行降噪处理&#x…

springboot学习(八十五) 解决springboot3.2找不到资源无法抛出404错误的问题

前言 springboot3.2以下可以定义ErrorPageRegistrar将404错误转发到一个接口地址&#xff0c;但升级到springboot3.2&#xff08;spring6.1&#xff09;后,该配置不生效&#xff0c;抛出了500错误。 以前的错误页面处理如下&#xff1a; ConditionalOnClass(ErrorPageRegist…

深入理解二分查找算法(一)

&#x1f497;&#x1f497;&#x1f497;欢迎来到我的博客&#xff0c;你将找到有关如何使用技术解决问题的文章&#xff0c;也会找到某个技术的学习路线。无论你是何种职业&#xff0c;我都希望我的博客对你有所帮助。最后不要忘记订阅我的博客以获取最新文章&#xff0c;也欢…

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK获取相机当前实时帧率(C#)

Baumer工业相机堡盟工业相机如何通过NEOAPI SDK使用UserSet功能保存和载入相机的各类参数&#xff08;C#&#xff09; Baumer工业相机Baumer工业相机的帧率的技术背景Baumer工业相机的帧率获取方式CameraExplorer如何查看相机帧率信息在NEOAPI SDK里通过函数获取相机帧率 Baume…

Android Studio新手实战——深入学习Activity组件

目录 前言 一、Activity简介 二、任务栈相关概念 三、常用Flag 四、结束当前Activity 五、Intent跳转Activity 六、更多资源 前言 Android是目前全球最流行的移动操作系统之一&#xff0c;而Activity作为Android应用程序的四大组件之一&#xff0c;是Android应用程序的核…

JAX: 快如 PyTorch,简单如 NumPy - 深度学习与数据科学

JAX 是 TensorFlow 和 PyTorch 的新竞争对手。 JAX 强调简单性而不牺牲速度和可扩展性。由于 JAX 需要更少的样板代码&#xff0c;因此程序更短、更接近数学&#xff0c;因此更容易理解。 长话短说&#xff1a; 使用 import jax.numpy 访问 NumPy 函数&#xff0c;使用 import …

华为云CCE-集群内访问-根据ip访问同个pod

华为云CCE-集群内访问-根据ip访问同个pod 问题描述&#xff1a;架构如下&#xff1a;解决方法&#xff1a; 问题描述&#xff1a; 使用service集群内访问时&#xff0c;由于启用了两个pod&#xff0c;导致请求轮询在两个pod之间&#xff0c;无法返回正确的结果。 架构如下&am…

Javascript 可迭代对象与yeild

一、可迭代对象&#xff08;Iterable object&#xff09; Javascript 可迭代对象是指实现了Symbol.iterator方法的对象&#xff0c;该方法返回一个迭代器对象&#xff0c;可以通过迭代器对象来遍历对象中的元素。常见的可迭代对象包括数组、字符串、Map、Set等。可以使用for..…

Redis布隆过滤器BloomFilter

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码、Kafka原理、分布式技术原理、数据库技术&#x1f525;如果感觉博主的文章还不错的…

PS绘图,切图方法

在PS绘图并切图时&#xff0c;以下是一种常见的方法&#xff1a; 绘图 打开 Photoshop&#xff0c;并创建一个新的文件以开始您的绘图工作。使用绘图工具&#xff08;例如画笔、铅笔、形状工具等&#xff09;进行您的绘图操作。您可以使用不同的图层来组织和管理不同的元素。…

Kafka优异的性能是如何实现的?

Apache Kafka是一个分布式流处理平台&#xff0c;设计用来处理高吞吐量的数据。它被广泛用于构建实时数据管道和流式应用程序。Kafka之所以能够提供优秀的性能和高吞吐量&#xff0c;主要得益于以下几个方面的设计和实现&#xff1a; 1. 分布式系统设计 Kafka是一个分布式系统…

Unreal Engine游戏引擎的优势

在现在这个繁荣的游戏开发行业中&#xff0c;选择合适的游戏引擎是非常重要的。其中&#xff0c;Unreal Engine作为一款功能强大的游戏引擎&#xff0c;在业界广受赞誉。那Unreal Engine游戏引擎究竟有哪些优势&#xff0c;带大家简单的了解一下。 图形渲染技术 Unreal Engin…

遇见sql语句拼装报错 sql injection violation, syntax error: syntax error, expect RPAREN

在使用PostgreSql瀚高数据库时&#xff0c;相同的语句 select * from public.files_info fi where fi.file_size notnull 在DBever能执行&#xff0c;但是在spring中报错 在spring中JPA版本问题导致&#xff0c;不支持这种写法&#xff0c;会识别为sql注入风险&#xff0c;应…

硅像素传感器文献调研(四)

写在前面&#xff1a; 好喜欢这种短论文哈哈哈哈哈 感觉这篇文献已经提到了保护环的概念啊&#xff0c;只不过叫的是&#xff1a;场限制环。 1986——高压功率器件场终端横向掺杂的变化 0.摘要 对于高压平面结提出了一个简单的新概念。通过在氧化物掩模中的小开口和随后的驱…

python如何读取被压缩的图像

读取压缩的图像数据&#xff1a; PackBits 压缩介绍&#xff1a; CCITT T.3 压缩介绍&#xff1a; 读取压缩的图像数据&#xff1a; 在做图像处理的时候&#xff0c;平时都是使用 函数io.imread() 或者是 函数cv2.imread( ) 函数来读取图像数据&#xff0c;很少用PIL.Image…

Grafana Loki 组件介绍

Loki 日志系统由以下3个部分组成&#xff1a; Loki是主服务器&#xff0c;负责存储日志和处理查询。Promtail是专为loki定制的代理&#xff0c;负责收集日志并将其发送给 loki 。Grafana用于 UI展示。 Distributor Distributor 是客户端连接的组件&#xff0c;用于收集日志…

2022年全国职业院校技能大赛(高职组)“云计算”赛项赛卷①第一场次:私有云

2022年全国职业院校技能大赛&#xff08;高职组&#xff09; “云计算”赛项赛卷1 第一场次&#xff1a;私有云&#xff08;30分&#xff09; 目录 2022年全国职业院校技能大赛&#xff08;高职组&#xff09; “云计算”赛项赛卷1 第一场次&#xff1a;私有云&#xff0…

python获取异常信息exc_info和print_exc

1 python获取异常信息exc_info和print_exc python通过sys.exc_info获取异常信息&#xff0c;通过traceback.print_exc打印堆栈信息&#xff0c;包括错误类型和错误位置等信息。 1.1 异常不一定是错误 所有错误都是异常&#xff0c;但并非所有异常都是错误。比如&#xff0c;…