C++之多态 虚函数表

news2024/12/23 5:57:30

多态

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。

需要区分一下:1、菱形虚拟继承,是在继承方式前面加上virtual

class Person {};
class Student : virtual public Person {};
class Teacher : virtual public Person {};
class Assistant : public Student, public Teacher {};

2、而多态是单继承中,某个函数接口前面加上virtual

class Person {
public:
    //虚函数
	virtual void BuyTicket() { cout << "Person-买票-全价" << endl; }
};
class Student : public Person {
public:
    //虚函数的重写
	virtual void BuyTicket() { cout << "Student-买票-半价" << endl; }
};

多态调用

普通调用:跟要调用这个函数的对象类型有关;

多态调用:跟指针或者引用指向的对象有关。

//引用
void func(Person& p)//普通调用的话,都是调用Person类所指向的函数,这里是多态
{
	p.BuyTicket();//跟指针或者引用指向的对象不同调用不同的虚函数
}
int main()
{
	Person adult;
	Student stu;
    //两个不同类对象调用同一个函数,看一下多态的实验结果
	func(adult);//引用指向的对象是Person类,故输出 adult-买票-全价
	func(stu);//引用指向的对象是Student类,故输出 Student-买票-半价
	return 0;
}
------------------------------------
//指针
void func(Person* p)//多态
{
	p.BuyTicket();//跟指针或者引用指向的对象不同调用不同的虚函数
}
int main()
{
	Person adult;
	Student stu;
	func(&adult);//输出 adult-买票-全价
	func(&stu);//输出 Student-买票-半价
	return 0;
}

所以多态是根据指针/引用指向对象的类型来决定调用某个重写的虚函数。如果函数变成void func(Person p);【不是引用也不是指针】就不是多态了。

多态的构成条件

  1. 必须通过父类的指针或者引用调用虚函数;【父类对象不可以】
  2. 被调用的函数必须是虚函数,且子类必须对父类的虚函数进行重写
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "全票" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTicket()
	{
		cout << "学生票" << endl;
	}
};

void func(Person& p)//对于该函数,看到的都是父类对象,子类对象中则为父类对象那一部分切割或者切片出来的,但是多态,可以让p根据指向或者引用的对象来调用不同的重写函虚数
{
	p.BuyTicket();
}

int main()
{
	Person adult;
	Student stu;
    //两个不同类对象调用同一个函数
	func(adult);
	func(stu);
	return 0;
}

注意:切割(切片)是子类对象可以赋值给父类对象/指针/引用。就是拿着子类对象中父类的那一部分,调用父类的拷贝构造再生成一个父类。

虚函数

即被virtual修饰的类成员函数称为虚函数。子类的虚函数可以不加virtual。

即三同【函数名、参数、返回值】函数满足多态的条件为:1、父类虚函数,子类虚函数;2、父类虚函数,子类该同名函数不加virtual。

注意:父类不加virtual,子类加virtual;父类和子类都不加virtual;这两种情况都不满足多态的条件。

可以写为虚函数的有:析构函数【很推荐】、内联函数【写成虚函数后,就不是内联函数了】

不可以是虚函数的有:友元函数、构造函数、static类型的成员函数;子类不一定要重新定义父类的虚函数,视情况而定。

虚函数的重写-三同

虚函数的重写(覆盖):子类中有一个跟父类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型函数名参数列表完全相同),称子类的虚函数重写了父类的虚函数。

例外1:协变

三同中,返回值不同,但要求返回值必须是父类/子类(包括自己和其他类)的指针或引用

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用;或者父类虚函数和子类虚函数均返回父类对象的指针或应用;或者父类虚函数返回其他父类的指针或引用,子类虚函数返回其他子类对象的指针或引用;或者父类虚函数和子类虚函数均返回其他父类对象的指针或应用

例外2:析构函数建议重写

三同中,看似函数名不同,实际上相同,均为destructor。如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

//类A的对象析构会先调用~A
class A
{
public:
	~A()
	{
		delete[] _a;
		cout << "A析构";
	}
protected:
	int* _a = new int[10];
};
//类B的对象析构会先调用~B,再自动调用~A
class B:public A
{
public:
	~B()
	{
		delete[] _b;
		cout << "B析构";
	}
protected:
	int* _b = new int[10];
};
int main()
{
    A* ptr1 = new A;
	A* ptr2 = new B;
	//这是普通调用!根据参数类型来调用,类型都是A*
	delete ptr1;//调用A的析构函数
	delete ptr2;//调用A的析构函数
    //存在内存泄漏问题
    ----------
    virtual ~A() {};//将A的析构函数变成虚函数,B的也加上
    //此时是多态调用!根据指针所指向的类型来调用
    delete ptr1;//调用A的析构函数
	delete ptr2;//调用B的析构函数,再调用A的析构函数
    return 0;
}

分析此处delete行为【使用 delete 释放 C++ 类对象的内存时,将在释放该对象的内存之前调用该对象的析构 函数(如果该对象具有析构函数)】:1、使用指针调用析构函数;2、operator delete(ptr);

建议:父类的虚构函数无脑加virtual,即可构成重写,避免内存泄漏。

如何实现一个不能被继承的类?

  1. 父类的构造函数设成私有成员函数【C++98的方法】
  2. 在类定义时+final【C++11的方法】class Person final {};此时Person称为最终类。

final和override

final:修饰父类虚函数,表示该虚函数不能再被重写【既可以修饰-不能被继承,也可以修饰父类虚函数–不能被重写】

override: 检查子类虚函数是否重写了父类某个虚函数,如果没有重写编译报错。

再次区分重写(覆盖)、隐藏(重定义)、重载

隐藏(重定义):父类和子类的函数,函数名(1同)即可;

重写(覆盖):父类和子类的虚函数,返回值、函数名、参数列表(3同)即可;

重载:同一个作用域,函数名相同(1同),返回值和参数列表不同

在这里插入图片描述

抽象类(纯虚函数)

包含纯虚函数的类叫做抽象类(也叫接口类)。【在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。】

  • 抽象类不能实例化出对象子类继承后也不能实例化出对象
  • 只有重写/实现纯虚函数,派生类才能实例化出对象(此时子类对象中也包含了父类对象)。
  • 纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
  • override是检查重写,而抽象类是强制重写。
  • 纯虚函数可以有函数体

如果一个类在实际生活中没有具体的对象,可以考虑将这个类定义为抽象类。

接口继承和实现继承

  1. 普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
  2. 虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

牢记:多态是接口继承,子类用的是父类的虚函数接口!!

//情况1:构成多态
class A
{
public:
	virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
	virtual void test(){ func();}//this->func()
    //这里是父类指针this(类型为A*)调用该虚函数,且该虚函数已经完成重写,此时符合多态调用
    //多态调用是看实际指向对象的类型,p是子类对象的指针B*,故调用的是B类的func函数
    //因为多态调用是接口继承,故使用的是A类的func接口(用A类的缺省参数)
};
class B : public A
{
public:
	void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
	B*p = new B;
	p->test();//输出结果是B->1
	return 0;
}
//情况2:不构成多态
class A
{
public:
	virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
};
class B : public A
{
public:
	void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }    
	virtual void test(){ func();}//this->func,func是B*类型,所以不构成多态【多态要求是父类指针/引用调用】
};
int main(int argc ,char* argv[])
{
	B*p = new B;
	p->test();//输出结果是B->0
	return 0;
}
//情况3:不构成多态
class A
{
public:
	virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
};
class B : public A
{
public:
	void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }    
	virtual void test(){ func();}//this->func,func是B*类型,所以不构成多态【多态要求是父类指针/引用调用】
};
int main(int argc ,char* argv[])
{
	B*p = new B;
	p->func();//输出结果是B->0
	return 0;
}
//情况4:编译报错
class A
{
public:
	virtual void func(int val){ std::cout<<"A->"<< val <<std::endl;}//A没有缺省参数,会报错
	virtual void test(){ func();}
};
class B : public A
{
public:
	void func(int val=0){ std::cout<<"B->"<< val <<std::endl; }    
};
int main(int argc ,char* argv[])
{
	B*p = new B;
	p->func();//编译报错
	return 0;
}

类的大小–虚函数表

单继承的虚表指针

先给出结论:

当父类定义了虚函数时,在子类进行继承的时候会将父类的虚函数表也给继承下来所以那一些虚函数在子类中也是virtual类型的,如果要对父类中的虚函数进行重写时或添加虚函数,顺序是:

  1. 先将父类的虚函数列表复制过来
  2. 重写虚函数时是把从父类继承过来的虚函数表中对应的虚函数进行相应的替换。
  3. 如果子类自己要添加自己的虚函数,则是把添加的虚函数加到从父类继承过来虚函数表的尾部。

注意:

  1. 严格说是同一个的类的不同对象都有各自的虚函数表,只是指向相同的虚函数,虚函数是共用的
  2. 子类和父类中的虚函数表中没重写的虚函数也是共用的,不是共有虚表哦!

先看一道笔试题

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
//sizeof(Base) = 8;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
};
//sizeof(Base) = 4;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
    char _c;//内存对齐
};
//sizeof(Base) = 12;

除了private成员,还多了一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针叫做虚函数表指针(v代表virtual,f代表function)。

一个含有虚函数的类中都至少都有一个虚函数表指针,因为该类内的虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。本质是函数指针数组。不是虚函数就不会进虚基表。

在这里插入图片描述

虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。【VS2013版本下是这样】

如何实现多态:当父类的指针或者引用指向父类对象时,对应的是父类对象的模型;当父类的指针或引用指向子类对象时,是子类对象的父类对象的切片【如果子类对象重写了虚函数,那么虚函数表会不一样】,以上这两种情况对应的对象模型是一样的,但是虚表不一定相同。多态实际是依靠虚表来实现的,里面虚函数的地址不一样。这里涉及的是动态绑定。

自己写一个代码访问虚表里的虚函数

class A
{
public:
	virtual void func1() { cout << "A::func1" << endl; }
	virtual void func2() { cout << "A::func2" << endl; }
private:
	int _a;
};

class B: public A
{
public:
	virtual void func1() { cout << "B::func1" << endl; }
	virtual void func3() { cout << "B::func3" << endl; }
	void func4() { cout << "B::func4" << endl; }
private:
	int _b;
};

typedef void(*VF_Ptr)();//类中的4个函数声明都是一样的,故可以typedef一样的
void PrintVFTable(VF_Ptr vft[])//传的是函数指针数组
{
	for (int i = 0; vft[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, vft[i]);
		vft[i]();
	}
}

int main()
{
	A a;
	B b;
	PrintVFTable((VF_Ptr*)(*((void**)&a)));//此代码在32/64位平台均可,void**解引用得到void*,那void*在不同平台大小也不一样,故可适应//只要是个二级指针就可以

	//把虚表打印出来
	PrintVFTable((VF_Ptr*)(*((int*)&a)));//仅适合32位平台--指针大小位4自己
	PrintVFTable((VF_Ptr*)(*((int*)&b)));
}

多继承的虚表指针

结论:

  1. 该类继承自几个不同对象就有多少个虚函数指针。

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中

//访问第一张虚表
PrintfVFTable((VFPtr*)(*(void**)&d));
//访问第二张虚表
PrintfVFTable((VFPtr*)(*(void**)((char*)&d+sizeof(Base1))));
//访问第二张虚表
Base2* ptr = &d;
PrintfVFTable((VFPtr*)(*(void**)(ptr));

在这里插入图片描述

菱形继承的虚表指针

和多继承下的对象模型差不多,具体看图

菱形虚拟继承的虚表指针

最后继承的那个子类D要重写父类的虚函数,因为菱形虚拟继承只会有一份父类A,而B和C都重写了A中的虚函数,那D对象模型中的A的虚表就不知道应该放谁重写的虚函数,当D重写以后,A的虚表中放D重写的虚函数地址即可。

在这里插入图片描述

虚函数在哪?虚表在哪?

通过代码验证一下

int main()
{
	int a = 0;
    cout << "栈:" << &a << endl;//0083FBC0
    
    int* p = new int;
    cout << "堆:" << p << endl;//00CCA430
    
    const char* str = "aaa";
    cout << "代码段/常量区:" << (void*)str << endl;//00729B78
    
    static int b = 0;
    cout << "静态区/数据段:" << &b << endl;//0072C400
    
    A a;
    cout << "虚表:" << (void*)*((int*)&a) << endl;//00729B34
	return 0;
}

注意:上述地址每次运行都不一样。只需要看地址区间即可,可看到虚表是在代码段/常量区在类对象里的是虚表指针虚表存的是虚函数指针虚函数和普通函数一样的,都是存在代码段的

虚表对同一个类创建的多个对象而言,是共享的,就只有一份,所以在常量区。不同类的虚表不一样。

静态绑定和动态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。编译时的多态性是通过函数重载和模板体实现的

动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。根据指针指向的类型去调用具体的函数【父类对象本身或父类对象的切片==>虚函数表不一样】,运行时的多态性是通过虚函数实现的

题目

class A
{
public:
    A ():m_iVal(0){test();}
    virtual void func() { std::cout<<m_iVal<<‘ ’;}
    void test(){func();}
public:
    int m_iVal;
};

class B : public A
{
public:
    B(){test();}
    virtual void func()
    {
        ++m_iVal;
        std::cout<<m_iVal<<‘ ’;
    }
};

int main(int argc ,char* argv[])
{
    A*p = new B;
    p->test();
    return 0;
}
//输出0 1 2
分析:new B时先调用父类A的构造函数,执行test()函数,再调用func()函数
    由于此时还处于对象构造阶段,多态机制还没有生效,所以,此时执行的func函数为父类的func函数,打印0
    构造完父类后执行子类构造函数,又调用test函数,然后又执行func()
    由于父类已经构造完毕,虚表已经生成,func满足多态的条件,所以调用子类的func函数,对成员m_iVal加1,进行打印,所以打印1,
    最终通过父类指针p->test(),也是执行子类的func,所以会增加m_iVal的值,最终打印2
class A
{
public: 
    virtual void f()
    {
        cout<<"A::f()"<<endl;
    }
};
class B : public A
{
private:
    virtual void f()
    {
        cout<<"B::f()"<<endl;
    }
};
A* pa = (A*)new B;
pa->f();
//输出B::f()
分析:虽然子类函数为私有,但是多态仅仅是用子类函数的地址覆盖虚表,最终调用的位置不变,只是执行函数发生变化

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

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

相关文章

【深蓝学院】手写VIO第2章--IMU传感器--笔记

0. 内容 1. 旋转运动学 角速度的推导&#xff1a; 左ω∧\omega^{\wedge}ω∧&#xff0c;而ω\omegaω是在z轴方向运动&#xff0c;θ′[0,0,1]T\theta^{\prime}[0,0,1]^Tθ′[0,0,1]T 两边取模后得到结论&#xff1a; 线速度大小半径 * 角速度大小 其中&#xff0c;对旋转矩…

Spring Security 实现自定义登录和认证(1):使用自定义的用户进行认证

1 SpringSecurity 1.1 导入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId> </dependency>1.2 编写配置类 在spring最新版中禁用了WebSecurityConfigurerAdapter…

04-项目立项:项目方案、可行性分析、产品规划、立项评审

文章目录4.1 项目方案立项阶段4.2 可行性分析4.3 产品规划4.4 立项评审4.4.1 立项说明书的主要内容4.4.2 立项评审流程章节总结4.1 项目方案 学习目标&#xff1a; 能够输出产品项目方案 项目开发设计流程的主要阶段&#xff1a; 立项阶段 → 设计阶段 → 开发阶段 → 测试阶…

机器学习 | 实验一:线性回归

文章目录&#x1f4da;描述&#x1f4da;数据&#x1f4da;监督学习问题&#x1f4da;二维线性回归&#x1f4da;理解J(θ)⭐️对应笔记 单变量线性回归多变量线性回归 &#x1f4da;描述 第一个练习将提供线性回归练习。这些练习已经在Matlab上进行了广泛的测试。但它们也应该…

Spring Boot @Aspect 切面编程实现访问请求日志记录

aop切面编程想必大家都不陌生了&#xff0c;aspect可以很方便开发人员对请求指定拦截层&#xff0c;一般是根据条件切入到controller控制层&#xff0c;做一些鉴权、分析注解、获取类名方法名参数、记录操作日志等。 在SpringBoot中使用aop首先是要导入依赖如下&#xff1a; …

软工2023个人作业二——软件案例分析

项目内容这个作业属于哪个课程2023年北航敏捷软件工程这个作业的要求在哪里个人作业-软件案例分析我在这个课程的目标是学习并掌握现代软件开发和项目管理技术&#xff0c;体验敏捷开发工作流程这个作业在哪个具体方面帮助我实现目标从软件工程角度分析比较我们所熟悉的软件&am…

Doris集成Spark读写的简单示例

Doris集成Spark读写的简单示例 文章目录Doris集成Spark读写的简单示例0、写在前面1、Spark Doris Connector介绍2、基本示例2.1 提前准备表和数据2.2 新建项目2.3 使用SQL方式进行读写2.3.1 代码2.3.2 相关Error2.4 使用DataFrame方式读写数据&#xff08;**batch**&#xff09…

CS5261typec转HDMI|CS5260typec转VGA视频转换方案参考设计与PCB板开发

CS5261typec转HDMI|CS5260typec转VGA视频转换方案参考设计与PCB板开发 CS5261 CS5260分别是Type-C转HDMI或者VGA高性能 视频转换芯片&#xff0c;CS5261 是Type-C转HDMI 4K30HZ转换芯片 CS5260是Type-C转VGA 转换芯片。CS5261与CS5260两种芯片的功能和参数特性如下&#xff1…

热乎的面经——初出茅庐

⭐️前言⭐️ 本篇文章记录博主与2023.03.04面试上海柯布西公司&#xff0c;一面所被问及的面试问题&#xff0c;回答答案仅供参考。 &#x1f349;欢迎点赞 &#x1f44d; 收藏 ⭐留言评论 &#x1f4dd;私信必回哟&#x1f601; &#x1f349;博主将持续更新学习记录收获&am…

EdgeYOLO学习笔记

EdgeYOLO学习笔记 EdgeYOLO: An Edge-Real-Time Object Detector Abstract 本文基于最先进的YOLO框架&#xff0c;提出了一种高效、低复杂度、无锚的目标检测器&#xff0c;该检测器可以在边缘计算平台上实时实现。为了有效抑制训练过程中的过拟合&#xff0c;我们开发了一种…

git分支

分支什么是分支在版本控制过程中&#xff0c;同时推进多个任务&#xff0c;为每个任务&#xff0c;我们就可以创建每个任务的单独分支。使用分支意味着程序员可以把自己的工作从开发主线上分离开来&#xff0c;开发自己分支的时候&#xff0c;不会影响主线分支的运行。对于初学…

DOM型XSS

DOM型XSSDOM是什么DOM型XSSDOM型XSS实操DOM是什么 DOM就是Document。 文档是由节点构成的集合&#xff0c;在DOM里存在许多不同类型的节点&#xff0c;主要有&#xff1a;元素节点、文本节点&#xff0c;属性节点。 元素节点&#xff1a;好比< body >< p >< h …

Go语言函数高级篇

Go语言函数高级篇1.高阶函数函数作为参数函数作为返回值2.匿名函数3.defer4.内置函数1.高阶函数 高阶函数分为函数作为参数和函数作为返回值两部分。 函数作为参数 函数可以作为参数&#xff1a; package mainimport "fmt"func add(x, y int) int {return x y }…

论文解析[11] CAT: Cross Attention in Vision Transformer

发表时间&#xff1a;2021 论文地址&#xff1a;https://arxiv.org/abs/2106.05786v1 文章目录摘要3 方法3.1 总体结构3.1.1 Inner-Patch Self-Attention Block3.1.2 Cross-Patch Self-Attention Block3.1.3 Cross Attention based Transformer结论摘要 使用图像patch来替换tr…

【Servlet篇4】cookie和session

在这一篇文章当中&#xff0c;我们提到了什么是cookie和session。 【网络原理8】HTTP请求篇_革凡成圣211的博客-CSDN博客HTTP的常见属性&#xff0c;URL&#xff0c;User-Agent&#xff0c;Refer,get 和post的区别https://blog.csdn.net/weixin_56738054/article/details/1291…

[数据集][VOC][目标检测]河道垃圾水面漂浮物数据集目标检测可用yolo训练-1304张介绍

数据集格式&#xff1a;Pascal VOC格式(不包含分割路径的txt文件和yolo格式的txt文件&#xff0c;仅仅包含jpg图片和对应的xml) 图片数量(jpg文件个数)&#xff1a;1304 标注数量(xml文件个数)&#xff1a;1304 标注类别数&#xff1a;1 标注类别名称:["trash"] …

如何从错误中成长?

在上一篇文章“技术人的犯错成本”里&#xff0c;我和你聊了技术人可能会犯的各式各样的错误&#xff0c;也举了很多例子&#xff0c;说明了技术人犯错的成本。在竞争激烈的互联网时代&#xff0c;试错当然是好事&#xff0c;但了解错误成本&#xff0c;避免不应该犯的错误&…

测试概念及模型

今日目标掌握测试用例包含的基本内容使用等价类方法设计出测试用例1. 软件测试分类&#xff08;复习&#xff09;1.1 按阶段划分单元测试测试&#xff1a;针对单个功能进行测试&#xff0c;如&#xff1a;登录、购物车等开发&#xff08;更多的理解&#xff09;&#xff1a;针对…

C/C++实现发送邮件功能(附源码)

C++常用功能源码系列 本文是C/C++常用功能代码封装专栏的导航贴。部分来源于实战项目中的部分功能提炼,希望能够达到你在自己的项目中拿来就用的效果,这样更好的服务于工作实践。 专栏介绍:专栏讲本人近10年后端开发常用的案例,以高质量的代码提取出来,并对其进行了介绍。…

Linux -- 作业控制进程

作业控制 &#xff1a;官方 &#xff1a; 作业控制是一个命令行功能&#xff0c;允许一个shell 实例来运行和管理多个命令。作用 &#xff1a; 使用作业控制&#xff0c;可以选择性暂停&#xff0c;恢复&#xff0c;以及异步运行命令&#xff0c;让 shell 可以在子进程运行期…