【C++】2.C++入门(2)

news2024/9/22 23:28:26

文章目录

  • 6.引用
    • 6.1 引用概念
    • 6.2 引用特性
    • 6.3 使用场景
    • 6.4 const引用(常引用)
    • 6.5 引用和指针的区别
  • 7.inline
    • 7.1inline代码举例:
    • 7.2inline代码错误示范
    • 7.3实现一个ADD宏函数的常见问题:
  • 8.nullptr


6.引用

6.1 引用概念

引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间

比如:孙悟空,又叫齐天大圣

类型& 引用变量名(对象名) = 引用实体;

void TestRef() // 定义一个没有返回值和参数的函数 TestRef
{
     int a = 10; // 定义一个整型变量 a 并初始化为 10
     int& ra = a; // 定义一个引用 ra,它是变量 a 的别名
     printf("%p\n", &a); // 打印变量 a 的地址
     printf("%p\n", &ra); // 打印引用 ra 的地址,由于 ra 是 a 的别名,这将打印出与 &a 相同的地址
}

==注意:==引用类型必须和引用实体是同种类型的

int main(){
    int a = 0;
    int& d = a;
    int x = 11;
    d = x;//这里是把x的值赋值给d,而不是让d变成x的引用(别名)
    printf("%d %d %d", a, d, x);
    //CPP里面引用的指向是不会变的
    return 0;
}

打印:

11 11 11

6.2 引用特性

  1. 引用在定义时必须初始化
  2. 一个变量可以有多个引用
  3. 引用一旦引用一个实体,再不能引用其他实体
void TestRef()
{
   int a = 10;
   // int& ra;   // 该条语句编译时会出错,因为引用在定义时必须初始化
   int& ra = a;
   int& rra = a;
   printf("%p %p %p\n", &a, &ra, &rra);  
}

6.3 使用场景

  • 引用在实践中主要是于引用传参和引用做返回值中减少拷贝提高效率和改变引用对象时同时改变被引用对象。

  • 引用传参跟指针传参功能是类似的,引用传参相对更方便一些。

  • 引用返回值的场景相对比较复杂,我们在这里简单讲了一下场景,还有一些内容后续类和对象章节中会继续深入讲解。

  • 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用跟其他语言的引用(如Java)是有很大的区别的,除了用法,最大的点,C++引用定义后不能改变指向,Java的引用可以改变指向。

  • 一些主要用C代码实现版本数据结构教材中,使用C++引用替代指针传参,目的是简化程序,避开复杂的指针,但是没学过引用,就会导致一头雾水。

1.做参数

例如原来我们这么写,是不会交换x,y的值的,因为这个是传值调用,而不是传址调用。

typedef:给类型取别名,不能给变量取别名

引用:给变量取别名

void Swap(int a, int b) {
	int tmp = a;
	a = b;
	b = tmp;
}

int main() {
	int x = 0;
	int y = 1;
	Swap(x, y);

	cout << x << " " << y << endl;

	return 0;
}

打印:

0 1

但是学完引用后,我们可以这么干:

void Swap(int& a, int& b) {//a是x的引用,b是y的引用
	int tmp = a;
	a = b;
	b = tmp;
}

int main() {
	int x = 0;
	int y = 1;
	Swap(x, y);

	cout << x << " " << y << endl;

	return 0;
}

打印:

1 0

我们也可以这样:

void Swap(int& a, int& b) {
	int tmp = a;
	a = b;
	b = tmp;
}

void Swap(int*& a, int*& b) {
	int* tmp = a;
	a = b;
	b = tmp;
}

int main() {
	int x = 0;
	int y = 1;
    cout << x << " " << y << endl;
    
	Swap(x, y);

	cout << x << " " << y << endl;

	int* px = &x;
	int* py = &y;
	cout << px << " " << py << endl;
	Swap(px, py);
	cout << px << " " << py << endl;

	return 0;
}

打印:

0 1
1 0
000000C0C2CFF4E4 000000C0C2CFF504
000000C0C2CFF504 000000C0C2CFF4E4

有些数据结构的书上会这么玩:

typedef struct ListNode {
	int val;
	struct ListNode* next;
};

void ListPushBack(struct ListNode*& phead, int x) {

}

int main() {

	struct ListNode* plist = NULL;
	ListPushBack(plist, 1);

	return 0;
}

还有些会这么写:

typedef struct ListNode {
	int val;
	struct ListNode* next;
}LTNode;

void ListPushBack(LTNode*& pphead, int x) {

}

int main() {

	LTNode* plist = NULL;
	ListPushBack(plist, 1);

	return 0;
}

还有些会这么写:

typedef struct ListNode {
	int val;
	struct ListNode* next;
}LTNode, *PLTNode;

void ListPushBack(PLTNode& pphead, int x) {

}

int main() {

	PLTNode plist = NULL;
	ListPushBack(plist, 1);

	return 0;
}

这个写法里面的PLTNode&就相当于ListNode*&就相当于struct ListNode*&,但是更加让人不太好理解。

初衷可能是二级指针的定义难以理解,就用了这个引用,但好多人C语言都没学明白,就来了引用,往往适得其反。


6.4 const引用(常引用)

  • 可以引用一个const对象,但是必须用const引用。const引用也可以引用普通对象,因为对象的访问权限在引用过程中可以平移或者缩小,但是不能放大

  • 需要注意的是,类似 int& rb = a*3; double d = 12.34; int& rd = d; 这样一些场景下a*3的和结果保存在一个临时对象中, int& rd = d 也是类似,在类型转换中会产生临时对象存储中间值rbrd引用的都是临时对象,而C++规定临时对象具有常性,所以这里就触发了权限放大,必须要用常引用才可以。

  • 所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。

为了方便理解,我举了好几个例子:

在类型转换中会产生临时对象存储中间值

int main() {
	int i = 1;
	double j = 1.1;
	if (j > i) {
		//因为在运算符两边的两个变量的类型不同的时候,会发生提升,一般是小的向大的提升
		//在这里i向j提升,会生成一个double的临时变量,然后和j比较
		cout << "xxxxx" << endl;//打印:xxxxx
	}

	return 0;
}

对象的访问权限在引用过程中可以平移或者缩小,但是不能放大

int main()
{
    const int a = 10;
    //int& ra = a;   // 不可以,这里的引用是对a访问权限的放大
    const int& ra = a;// 这样才可以
    //ra++;// 编译报错:error C3892: “ra”: 不能给常量赋值
    int c = a;//这个可以,这里c的改变不影响a
    
    int b = 20;
    const int& rb = b;// 这里的引用是对b访问权限的缩小 
    //rb++;// 编译报错:error C3892: “rb”: 不能给常量赋值
    
    return 0;
}
#include<iostream>
using namespace std;
int main()
{
    int a = 10;
    const int& ra = 30;
    // int& ra = a * 3;// 编译报错: “初始化”: 无法从“int”转换为“int &”
    const int& rb = a*3;
    
    double d = 12.34;
    // int& rd = d;// 编译报错:“初始化”: 无法从“double”转换为“int &”
    const int& rd = d;
    
    return 0;
}
//可以
//引用过程中,权限可以平移或者缩小
int main() {
	int x = 0;
	int& y = x;
	const int& z = x;//可以,这里是权限的缩小。这里缩小的不是x的权限,缩小的是z作为别名的权限
	++x;//可以,因为x是一个变量,他怎么变没有限制,有限制的是z。const修饰z,缩小了z作为别名的权限。
	//++z;//不可以

	return 0;
}
int main() {
	const int& m = 10;//这里是可以的,因为这里权限是不能被修改的,是权限的平移

	double a = 1.11;
	int b = a;//类型转换的时候会产生一个int类型的临时变量,也就是说这里给b的不是a而是临时变量。
	//临时变量具有常性,相当于被const修饰了

	//int& c = a;//这里权限放大了,所以不可以
	const int& c = a;//这里可以,

	return 0;
}
//辩证
int func1() {
	static int x = 0;
	return x;
}

int& func2() {
	static int y = 0;
	return y;
}

int main() {
	int ret1 = func1();//可以接收,这里就是拷贝
	//int& ret1 = func1();//不可以接收,因为权限的放大。这里返回的其实不是x,而是一个临时变量,临时变量具有常性。
	const int& ret1 = func1();//这样就可以了,这里是权限的平移

	//ret2现在是对Count函数返回的静态局部变量y的引用,ret2可以被视为引用变量名
	int ret2 = func2();//可以,上面有例子
	int& ret2 = func2();//可以,这里返回的是y的别名,是权限的平移
	const int& ret2 = func2();//可以,这里是权限的缩小

	return 0;
}
int main()
{
	// 权限不能放大
	const int a = 10;
	const int* p1 = &a;
	//int* p2 = p1;// 权限不能放大

	// 权限可以缩小
	int b = 20;
	int* p3 = &b;
	const int* p4 = p3;// 权限可以缩小

	// 不存在权限放大,因为const修饰的是p5本身不是指向的内容
	int* const p5 = &b;
	int* p6 = p5;

	return 0;
}

6.5 引用和指针的区别

C++的引用是无法完全代替指针的

  • 语法概念上引用是一个变量的取别名不开空间,指针是存储一个变量地址,要开空间。

  • 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。

  • 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。

  • 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。

  • sizeof中含义不同,引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)

  • 指针很容易出现空指针和野指针的问题,引用很少出现,引用使用起来相对更安全一些。

// 指针
int* STTop(ST& rs){
	assert(rs.top > 0);
	return rs.a + [rs.top - 1];
}


int main(){
	// 指针
	*(STTop(st1)) += 1;
	return 0;
}

/******************************************************************************************************************/
//引用
int& STTop(ST& rs){
	assert(rs.top > 0);
	return &(rs.a[rs.top - 1]);
}

int main(){
	//引用
	(STTop(st1)) += 1;
	return 0;
}
int main() {
	int a = 10;
	
	//语法层面:不开空间,是对a取别名
	//底层汇编指令实现角度看:引用是类似指针的方式实现的
	int& ra = a;
	ra = 20;

	//语法层面:开空间,存储a的地址
	int* pa = &a;
	*pa = 20;

	return 0;
}

7.inline

  • inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开内联函数,这样调用内联函数就需要建立栈帧了,就可以提高效率。

  • inline对于编译器而言只是一个建议,也就是说,你加了inline编译器也可以选择在调用的地方不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。

  • C语言实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不方便调试,C++设计了inline目的就是替代C的宏函数。

  • vs编译器 debug版本下面默认是不展开inline的,这样方便调试,debug版本想展开需要设置一下以下两个地方。

  • inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。

f0ffe39b567e2796fc171571d82f80885fdff4eeb20fcb6aadb21d71f8f480da

7.1inline代码举例:

#include<iostream>

using namespace std;

inline int Add(int x, int y)
{
    int ret = x + y;
    ret += 1;
    ret += 1;
    ret += 1;
    return ret;
}
int main()
{
    // 可以通过汇编观察程序是否展开
    // 有call Add语句就是没有展开,没有就是展开了
    int ret = Add(1, 2);
    cout << Add(1, 2) * 5 << endl;
    return 0;
}

inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

适用于短小的,频繁调用的函数,对于长代码会出现代码膨胀

比如,Func函数编译后是50行指令,如果有10000个位置调用Func的话:
1.Func不是inline,合计多少行指令?
10000+50
    
2.Func是inline,合计多少行指令?
10000*50

inline对于编译器仅仅只是一个建议,最终是否成为inline,编译器自己决定

像类似函数就算加了inline,也会被否决掉

  1. 比较长的函数

  2. 递归函数

默认debug模式下,inline不会起作用,否则不方便调试了


7.2inline代码错误示范

inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到

// F.h
#include <iostream>
using namespace std;

inline void f(int i);

// F.cpp
#include "F.h"

void f(int i)
{
    cout << i << endl;
}

// main.cpp
#include "F.h"

int main()
{
    // 链接错误:无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z)
    f(10);
    return 0;
}

这里的错误是因为

使用了 inline 关键字在F.h头文件中声明函数 f,但是在 F.cpp 文件中,但没有使用 inline 关键字。

这导致链接器在尝试将程序链接在一起时找不到 f 函数的定义,因为 inline 函数的定义需要在每个包含其声明的编译单元中可见。

解决办法:

  1. 移除 inline 关键字:从头文件中移除 inline 关键字,这样函数 f 就不再要求在每个编译单元中都有定义。这样,只需要在 F.cpp 中定义函数即可。
  2. 确保 inline 函数定义在每个编译单元中可见:如果希望保持 inline,则需要在每个包含 F.h 的文件中也包含 F.cpp,或者将 F.cpp 的内容直接放入 F.h 中。

7.3实现一个ADD宏函数的常见问题:

#include<iostream>
using namespace std;
// 实现一个ADD宏函数的常见问题

//#define ADD(int a, int b) return a + b;//错误写法
//替换后函数直接return了

//#define ADD(a, b) a + b;//错误写法
	Add(10, 20) * 20;//这是例子

//#define ADD(a, b) (a + b)//错误写法
	int a = 1, b = 2;
    Add(a | b, a & b); //这是例子
	// (a | b + a & b)//不可以,会出错。因为+号的优先级更高,会出现问题


// 正确的宏实现
#define ADD(a, b) ((a) + (b))
// 为什么不能加分号?
// 为什么要加外面的括号?
// 为什么要加里面的括号?
int main()
{
    int ret = ADD(1, 2);
    cout << ADD(1, 2) << endl;
    cout << ADD(1, 2)*5 << endl;
    int x = 1, y = 2;
    ADD(x & y, x | y); // -> ((x&y)+(x|y))
    return 0;
}

宏函数

优点-- 不需要建立栈帧,提高调用效率

缺点-- 复杂,容易出错、可读性差、不能调试


8.nullptr

NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

#ifndef NULL
	#ifdef __cplusplus
		#define NULL 0
	#else
		#define NULL ((void *)0)
	#endif
#endif
  • C++NULL可能被定义为字面常量0,或者C中被定义为无类型指针(void*)的常量。不论采取何种定义,在使用空值的指针时,都不可避免的会遇到一些麻烦,本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。f((void*)NULL);调用会报错。

  • C++11中引入nullptrnullptr是一个特殊的关键字,nullptr是一种特殊类型的字面量,它可以转换成任意其他类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式地转换为指针类型,而不能被转换为整数类型。

#include<iostream>
using namespace std;

void f(int x)
{
    cout << "f(int x)" << endl;
}

void f(int* ptr)
{
    cout << "f(int* ptr)" << endl;
}

int main()
{
    f(0);//打印:f(int x)
    
    // 本想通过f(NULL)调用指针版本的f(int*)函数,但是由于NULL被定义成0,调用了f(int x),因此与程序的初衷相悖。
    f(NULL);//打印:f(int x)
    
    f((int*)NULL);//打印:f(int* x)
    
    // f((void*)NULL);// 编译报错:error C2665: “f”: 2 个重载中没有一个可以转换所有参数类型
    //因为C++不允许void*转换成任意类型的指针

    f(nullptr);//打印:f(int* x)
    
    return 0;
}

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

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

相关文章

Yolov8在RK3588上进行自定义目标检测(三)

参考 Yolov8在RK3588上进行自定义目标检测(一) Yolov8在RK3588上进行自定义目标检测(二) best.onnx转yolov8.rknn onnx转rknn需要用到rknn-toolkit2&#xff0c;这个工具暂时不支持windows&#xff0c;所以我们移步linux&#xff0c;我用的是虚拟机创建的ubuntu20.4的系统&a…

JS+H5美观的带搜索的博客文章列表(可搜索多个参数)

实现 美观的界面&#xff08;电脑、手机端界面正常使用&#xff09;多参数搜索&#xff08;文章标题&#xff0c;文章简介&#xff0c;文章发布时间等&#xff09;文章链接跳转 效果图 手机端 电脑端 搜索实现 搜索功能实现解释 定义文章数据: 文章数据保存在一个 JavaScri…

评价指标--深度学习

目录 1分类任务1.1 二分类1.1.1 含义介绍1.1.2 指标 1.2多分类 2图像分割2.1 常用指标2.2 具体含义2.3 代码实现 1分类任务 1.1 二分类 混淆矩阵 1.1.1 含义介绍 TP&#xff1a;预测为真所以是Positive&#xff0c;预测结果和真实结果一致所以为TrueTN&#xff1a;预测为假…

【Python 逆向滑块】(实战六)逆向滑块,并实现用Python+Node.js 生成滑块、识别滑块、验证滑块、发送短信

逆向日期&#xff1a;2024.08.04 使用工具&#xff1a;Python&#xff0c;Node.js 本章知识&#xff1a;逆向【NECaptchaValidate】参数并成功发送短信 文章难度&#xff1a;中等&#xff08;没耐心的请离开&#xff09; 文章全程已做去敏处理&#xff01;&#xff01;&#xf…

【SpringBoot】 定时任务之任务执行和调度及使用指南

【SpringBoot】 定时任务之任务执行和调度及使用指南 Spring框架分别通过TaskExecutor和TaskScheduler接口为任务的异步执行和调度提供了抽象。Spring还提供了支持应用程序服务器环境中的线程池或CommonJ委托的那些接口的实现。最终&#xff0c;在公共接口后面使用这些实现&…

POE服务机器人-快速开始

快速开始 POE与服务机器人部署服务机器人与poe集成迭代你的机器人其他 POE与服务机器人 在本快速入门指南中&#xff0c;我们将使用 Python 构建一个机器人服务器&#xff0c;然后将其与 Poe 集成。一旦您创建了由您的服务器驱动的 Poe 机器人&#xff0c;任何 Poe 用户都可以…

解密XXE漏洞:原理剖析、复现与代码审计实战

在网络安全领域&#xff0c;XML外部实体&#xff08;XXE&#xff09;漏洞因其隐蔽性和危害性而备受关注。随着企业对XML技术的广泛应用&#xff0c;XXE漏洞也逐渐成为攻击者们利用的重点目标。一个看似无害的XML文件&#xff0c;可能成为攻击者入侵系统的利器。因此&#xff0c…

R语言统计分析——描述性统计

参考资料&#xff1a;R语言实战【第2版】 1、整体统计 对于R语言基础安装&#xff0c;可以使用summary()函数来获取描述性统计量。summary()函数提供了最小值、最大值、四分位数、中位数和算术平均数&#xff0c;以及因子向量和逻辑向量的频数统计。 myvars<-c("mpg&…

JRT多维取数据三件套

今天补齐DolerData判断数据是否存在的API&#xff0c;即M的$d。 兜兜转转&#xff0c;经过近十年探索&#xff0c;3年的酝酿&#xff0c;10个月的开发&#xff0c;JRT终于集齐多维取数据三件套。分别是$get,$listget,$data。通过多维取数据的支持&#xff0c;JRT特别适合医疗数…

7.怎么配置一个axios来拦截前后端请求

首先创建一个axios.js文件 导入我们所需要的依赖 import axios from "axios"; import Element from element-ui import router from "./router"; 设置请求头和它的类型和地址 注意先注释这个url,还没有解决跨域问题,不然会出现跨域 // axios.defaults.…

6-5 多输入多输出通道

虽然我们在前面描述了构成每个图像的多个通道和多层卷积层。例如彩色图像具有标准的RGB通道来代表红、绿和蓝。 但是到目前为止&#xff0c;我们仅展示了单个输入和单个输出通道的简化例子。 这使得我们可以将输入、卷积核和输出看作二维张量。 当我们添加通道时&#xff0c;我…

搭建高可用OpenStack(Queen版)集群(一)之架构环境准备

一、搭建高可用OpenStack&#xff08;Queen版&#xff09;集群之架构环境准备 一、架构设计 二、初始化基础环境 1、管理节点创建密钥对&#xff08;方便传输数据&#xff09; 所有控制节点操作 # ssh-keygen #一路回车即可 Generating public/private rsa key pair. Enter f…

MTK Android12 分析system_app允许vendor_mtk_audiohal_prop SELinux 权限问题

本文将尝试分析&#xff0c;在开发 Android 12 MTK 平台时遇到了 vendor_mtk_audiohal_prop 属性相关的 SELinux 权限问题。包括如何修改 SELinux 策略以允许 system_app 设置 vendor_mtk_audiohal_prop 属性。 问题描述 希望允许 system_app 设置 vendor_mtk_audiohal_prop 属…

SpringBoot+Vue图书(图书借阅)管理系统-附项目源码与配套文档

摘 要 本论文阐述了一套先进的图书管理系统的设计与实现&#xff0c;该系统采用Java语言&#xff0c;结合现代Web开发框架和技术&#xff0c;旨在为图书馆提供高效、灵活且用户友好的资源管理解决方案。系统利用Spring Boot框架为核心&#xff0c;整合MyBatis ORM工具&#…

基于 systemc-2.3.1的virtual device 接入 qemu-arm

1&#xff0c;下载systemc-2.3.1 下载网址&#xff1a; SystemC Files $ wget https://www.accellera.org/images/downloads/standards/systemc/systemc-2.3.1.tgz 2&#xff0c;编译安装 systemc-2.3.1 tar zxf systemc-2.3.1.tgz cd systemc-2.3.1/ export CXXg mkdir bu…

PS 2024 百种常用插件下载安装教程【免费使用,先到先得】

文章目录 软件介绍软件下载安装步骤 专栏推荐&#xff1a; 超多精品软件&#xff08;持续更新中…&#xff09; 软件推荐&#xff1a; PS 2024 PR 2024 软件介绍 PS常用插件 此软件整合了市面近百款ps处理插件&#xff0c;可实现&#xff1a;一键制作背景&#xff0c;一键抠图…

linux安装docker(实操教程)

一、安装前准备工作 1.查看服务器操作系统版本 2.查看服务器的操作系统内核版本 3.安装依赖包 yum install -y yum-utils device-mapper-persistent-data lvm2如果不是root用户登陆的系统&#xff0c;需要手动输入sudo -i切换到root帐户 4.设置阿里云docker-ce镜像源 yum-c…

美国失业率大幅上升,增加九月份降息利率的可能性

令人失望的是&#xff0c;美国7月份经济增加了11.4万个工作岗位&#xff0c;低于预期的17.5万个和6月的17.9万个。平均小时工资持续下降&#xff0c;但失业率升至4.3%。美元继续走低&#xff0c;美国国债也在下跌&#xff0c;而黄金则获得了提振。 7月份的非农业支付数据令人失…

ST语言支持包下载安装(VS CODE)

VSCODE是微软提供的代码编辑器&#xff0c;支持非常多的语言。 1、VSCODE下载 2、ST语言支持包 3、ST语言支持包下载 4、ST语言 。。

IndentationError: expected an indented block 深度解析

IndentationError: expected an indented block 深度解析与实战指南 在Python编程中&#xff0c;IndentationError: expected an indented block是一个常见的错误&#xff0c;通常发生在代码块没有正确缩进时。这个错误表明代码中存在格式问题&#xff0c;可能是缩进不一致或缺…