c++多态详解

news2024/9/21 2:33:35

前言:

        对于面向对象语言来说,多态是面向对象的三大特性之一,简单一点来说多态就是多种形态,是不同对象接收到同一种消息产生的不同动作或者反应,听起来有点抽象,实际上就是完成一个任务让不同的对象来做产生的效果是不同的。让我们一起来深入了解一下多态吧!

目录

1.多态的概念

2.多态的定义及实现

        2.1多态的构成条件

        2.2虚函数和虚函数的重写

                2.2.1虚函数的重写 

                 2.2.2虚函数重写的特殊情况

        2.3c++11关键字final和override

        2.4重载,覆盖和重写的辨析 

3.抽象类

         3.1抽象类的定义

        3.2接口继承和实现继承

4.多态是如何实现的

        4.1虚函数表

        4.2多态的原理

        4.3动态绑定和静态绑定 

5.多态在单继承和多继承中

        5.1单继承中的虚函数表

        5.2多继承中的虚函数表

 总结

1.多态的概念

        多态通俗的说就是多种形态,比如我们在买票的时候不同人群买票的价格和方式是不同的,比如军人可以优先买票,学生票半价等等。 

        在举个例子,比如我们在抢红包的时候,不同人抢到的红包的金额也是不同的,有的人可能抢的很多,有的人可能抢的很少。之前支付宝做活动的时候,大家都扫码领红包,每个人领的红包都是不一样的,而且随着领取次数的增加,领到的红包的金额也是会越来越少。这些行为都可以认为是多态。 

2.多态的定义及实现

        2.1多态的构成条件

        多态是不同继承关系的类对象,去调用同一函数,产生不同的行为(基类对象和派生类对象去调用同一函数所产生的效果不同)。那么在继承中构成多态的条件有两个:1.被调用的函数必须是虚函数,且必须完成虚函数的重写。2.必须通过基类的指针或者引用去调用虚函数。  

        例如:

#include<iostream>
using namespace std;
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "抢到五元钱" << endl;
	}
};
class Student:public Person
{
	virtual void BuyTicket()
	{
		cout << "抢到五角钱" << endl;
	}
};
void Print(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person p;
	Student s;
	Print(p);//调用父类的BuyTicket
	Print(s); //调用Student的BuyTicket

	return 0;
}

        2.2虚函数和虚函数的重写

                2.2.1虚函数的重写 

        虚函数就是被virtual关键字修饰的成员函数。例如:

        虚函数的重写(覆盖):派生类中一定要有和基类中的函数的函数名相同,函数的参数类型相同,函数的返回值相同的函数(三同)。派生类中有和基类相同的虚函数就完成了虚函数的重写,称为派生类的虚函数完成了对基类虚函数的重写。         

 

                 2.2.2虚函数重写的特殊情况

        那么虚函数的重写是不是一定要满足函数名相同,函数的参数相同,函数的返回值也相同呢?

        答案是不一定,这里存在特殊情况,如果派生类重写基类的虚函数时,与基类的虚函数的返回值不同。即基类虚函数的返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。例如:

#include<iostream>
using namespace std;
class Base
{
public:
	virtual Base* Func()
	{
		cout << "hello world" << endl;
		return this;
	}
};
class Derive:public Base
{
public:
	virtual Derive* Func()//协变,返回值不同,是派生类Derive的指针
	{
		cout << "i am so happy" << endl;
		return this;
	}
};
void Print(Base& b)
{
	b.Func();
}
int main()
{
	Base b1;
	Derive d1;
	//多态
	Print(b1);//Base类调用实现Base的效果
	Print(d1);//Derive类调用实现另外的效果

	return 0;
}

          注意,派生类重写基类的虚函数时,派生类与基类相同的函数在派生类中不加关键字virtual修饰也是可以实现多态的,例如:

class Base
{
public:
	virtual Base* Func()
	{
		cout << "hello world" << endl;
		return this;
	}
};
class Derive:public Base
{
public:
	//派生类中与基类相同的函数,在基类中不用关键字virtual修饰也开始实现多态
	 Derive* Func()//协变,返回值不同,是派生类Derive的指针
	{
		cout << "i am so happy" << endl;
		return this;
	}
};

        这样写不规范,所以一般不推荐这样写。但是如果基类中的虚函数不用virtual修饰是没有办法实现多态的。 

        注意:析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

析构函数可以是虚函数,如果有以下这样的场景:

#include<iostream>
#include<stdio.h>
using namespace std;
class Base1
{
public:
	virtual void Fun1() { cout << "Base2---Fun1" << endl; }
	int _b1 = 0;
	~Base1()
	{
		cout << "~Base()" << endl;
	}
};
class Derive :public Base1
{
public:
	virtual void Fun1() { cout << "Derive---Fun1" << endl; }
	virtual void Fun2() { cout << "Derive---Fun3" << endl; }
	~Derive()
	{
		cout << "~Derive()";
	}
	int _d = 0;
};
int main()
{
	Base1* p1 = new Derive;
	delete p1;
	return 0;
}

如果将派生类动态开辟的空间赋值给基类的对象,此时就需要析构函数也是虚函数。

如果没有重写虚函数,此时delete p1的时候就只会调用基类的析构函数。如图:

解决方法:给基类的析构函数加上关键字virtual修饰 

 

        2.3c++11关键字final和override

        c++11中新增了两个关键字finaloverride ,从上面可以看出,c++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出来的,只有在程序运行时没有得到预期结果才会来debug,会得不偿失,因此:c++提供的关键字final和override帮助用户监测是否重写。 

        override 检查派生类虚函数是否重写了 ,如果虚函数没有进行重写,程序就会报错,例如

using namespace std;
class Base
{
public:
	virtual void Func()
	{
		cout << "hello world" << endl;
	}
};
class Derive:public Base
{
public:
	
	 void Fun()override
	{
		cout << "i am so happy" << endl;
	}
};

        此时没有完成对虚函数的重写,所以程序编译就会报错,

        final:修饰虚函数,表示该虚函数不能被重写,例如:

class Base
{
public:
	virtual void Func()final//final修饰虚函数,该虚函数不能被重写
	{
		cout << "hello world" << endl;
	}
};
class Derive:public Base
{
public:
	virtual void Func()
	{
		cout << "i am so happy" << endl;
	}
};

         final修饰类,该类不能被继承,例如:

class Base final
{
public:
	virtual void Func()
	{
		cout << "hello world" << endl;
	}
};
class Derive:public Base
{
public:
	virtual void Func()
	{
		cout << "i am so happy" << endl;
	}
};

 

        2.4重载,覆盖和重写的辨析 

        对比,如图: 

 

3.抽象类

         3.1抽象类的定义

        在虚函数的后面写上=0 ,则这个函数就是纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承也不能实例化出对象,只有重写纯虚函数后,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更加体现了接口继承。

        例如:

class Base //有纯虚函数的类叫做抽象类
{
public:
	virtual void Func() = 0;//纯虚函数,只需要声明不用实现也可以
};
int main()
{
    Base b1;//这里会报错因为抽象类不能实例化出对象
    return 0;
}

        为什么会有抽象类呢,在现实世界中,我们有具象的事物,画家的派别也有很多种,有山水田园化,写实的画作等,也有抽象的画作,抽象就是在现实中没有对应的实体,很难想象形态,抽象类也是类似的,比如,我们定义的某个类,在现实中没有对于的具体事物,但是有一定的功能,我们就可以将它定义为抽象类。比如植物类,不是某种具体的植物,我们就可以将它定义为抽象类,可以让其他派生类去继承它的接口来实现必须的一些功能和要求。

        3.2接口继承和实现继承

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

4.多态是如何实现的

        4.1虚函数表

        有这样一个问题,下面这个程序执行的结果是什么?

#include<iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()//虚函数
	{
		cout << "Func1"  <<endl;
	}
	int _b = 0;
};
int main()
{
	Base b1;
	cout<<sizeof(b1);//这里的b1的大小是多少个字节呢?(32位平台下)
}

 结果如下:

是不是和猜想的有出入呢,为什么会是这个结果呢?让我们一起来分析一下,通过观察我们发现在b对象中除了_b成员外还多了一个_vfptr的指针放在对象的最前面(注意有些平台可能放在后面,这个与平台有关)。对象中的这个指针,我们叫做虚函数表指针(v代表virtual,f代表function)。如图:

 一个虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址是要放到虚函数表中的。虚函数表也称为虚表,那么派生类的这个表是怎么样的呢?都放了哪些东西呢?让我们一起来看看吧!

测试代码:

#include<iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base"  <<endl;
	}
	int _b = 0;
};
class Derive:public Base
{
	virtual void Func1()
	{
		cout << " Derive" << endl;
	}
	int _d = 0;
};
int main()
{
	Derive d1;
	Base b1;
	return 0;
}

        上面我们新增了一个派生类Derive,并且继承了父类Base,重写了基类的Func1函数。

        通过观察我们发现,派生类对象是由两部分构成的,一部分是继承基类的,另一部分是自己的 ,派生类也有虚函数表指针。虚函数表中存着自己和继承基类的虚函数。

        基类对象b1和派生类对象d1的虚函数表是不一样的,这里我们可以看见在派生类中,Func1函数完成了重写,所以d1的虚函数表中存的是重写以后的Func1(属于derive类),所以重写也叫作覆盖,如图:

 另外对于没有重写的基类的虚函数Func2,也放进了虚函数表中,Func3也继承下来了,但是由于Func3是普通函数所以,没有将它放进续表中。

        虚函数表本质上是存虚函数指针的数组,一般情况下,这个数组是以nullptr结束的

        总结一下:派生类的虚函数表的生成:1.先继承基类的虚函数表,将基类的虚函数表的内容拷贝一份到派生类的虚函数表中。2.如果派生类重写了基类的虚函数,就在虚函数表中被重写的基类的虚函数的地址覆盖成派生类的虚函数的地址。3.派生类自己新增的虚函数一次放到虚函数表的最后。

          这里有两个问题,虚函数存在哪里,虚函数表存在哪里?

        答案是:它们都存在代码段,可能现在就有人不能理解了,为什么虚函数不是存在虚函数表里的而是存放在代码段的呢?注意在虚函数表里面只存放了虚函数的地址,运行的时候编译器是在虚函数表里面找虚函数的地址从而调用虚函数的,虚函数存放的地方和普通函数是一样的,都在代码段,而且虚函数表也是存放在代码段的,为什么呢?其实可以好好思考一下,一个类的对象需要很多虚函数表吗?不需要吧,一个类中的虚函数表只有有一份那么这个类中的所有对象就是可以都是使用的,而且也会足够的,就像是一个班级里面需要多份课表吗,是不是有一份就可以了,多了贴在班级里面也没有很多的用处。

        从这里可以看出同一类对象共用一份虚函数表。 

        我们可以写一份代码来验证一下,验证代码:

#include<iostream>
#include<stdio.h>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base"  <<endl;
	}
	virtual void Func2()
	{
		cout << "Func2" << endl;
	}
	void Func3()
	{
		cout << "Func3" << endl;
	}
	int _b = 0;
};
class Derive:public Base
{
public:
	virtual void Func1()
	{
		cout << " Derive" << endl;
	}
	virtual void Func4()
	{
		cout << "Func4" << endl;
	}
	int _d = 0;
};
typedef void(*pFun)();
void PrintFTB(pFun table[])
{

}
void Print()
{
	//基类对象
	Base b1;
	Base b2;
	//派生类对象
	Derive d1;
	Derive d2;
	//去派生类对象虚函数表
		cout<<"虚函数表的地址" << *(int*)&d1 << endl;//找到虚函数表的地址
	int a = 0;//栈上创建的对象
	int *p1 = &a;
	int *p2 = new int();//堆上创建的对象
	const char* p3 = "hello";
	printf("栈变量:%p\n", p1);
	printf("堆变量:%p\n", p2);
	printf("代码段常量:%p\n", p3);
	printf("虚函数地址:%p\n", &Derive::Func2 );
	printf("普通函数:%p\n", Print);
}
int main()
{
	Derive d1;
	Base b1;
	Print();
	return 0;
}

        结果:

 

        4.2多态的原理

        那么我们研究了这么久,多态的原理到底是什么呢?下面的这个代码就是实现多态的代码:

#include<iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base"  <<endl;
	}
	virtual void Func2()
	{
		cout << "Func2" << endl;
	}
	void Func3()
	{
		cout << "Func3" << endl;
	}
	int _b = 0;
};
class Derive:public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive" << endl;
	}
	virtual void Func4()
	{
		cout << "Func4" << endl;
	}
	int _d = 0;
};
void Fun(Base&b)//多态的实现
{
	b.Func1();
}
int main()
{
	//基类对象
	Base b2;
	//派生类对象
	Derive d1;
	Fun(b2);
	Fun(d1);
	return 0;
}

 这里的Fun函数传Base类的对象调用Base::Func1,传Derive类的对象调用Derive::Func1

如图:

这里观察这幅图中的红色箭头我们发现b指向Base对象时,调用的就是Base类的虚函数,

观察这幅图中的蓝色箭头,我们可以看到b指向Derive对象时,调用的就是Derive类的虚函数。 

这样就实现了不同对象去执行同一行为时,展现出不同的形态。

        实际上就是在调用时,编译会去b指向的对象的虚函数表中去找要调用的虚函数,因为派生类的虚函数已经被重写了,所以派生类虚函数表中存放的是重写以后的虚函数的地址,当然就实现指向谁就调用谁的虚函数了。

反过来我们思考一下,为什么达到多态要满足两个条件,一个是虚函数的重写,另一个是基类对象的引用或者指针来调用虚函数。反思一下为什么?

再通过下面的汇编代码分析一下

// b中存的是b2对象的引用,将p移动到eax中
001940DE mov eax,dword ptr [b]
// [eax]就是取eax值指向的内容,这里相当于把b2对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx,dword ptr [eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax,dword ptr [edx]
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
以后到对象的中取找的。
001940EA call eax
00头1940EC cmp esi,esp

        可以看出满足多态以后的函数调用,不是在编译时确定的而是在运行时去到对象中去找的。不满足多态的函数调用是在运行时确定的。

        4.3动态绑定和静态绑定 

        1.静态绑定:又叫早期绑定,是在程序编译的时候确定程序的行为,也称静态多态。比如函数重载。

        2.动态绑定:又称后期绑定,是在程序运行期间,根据具体拿到的类型来确定程序执行的功能,调节具体的函数,也称动态多态。 

5.多态在单继承和多继承中

          需要注意的是在单继承和多继承关系中,派生类的虚函数表是有所差异的。 下面我们一起来研究一下:

        5.1单继承中的虚函数表

       我们一起来看看单继承中的虚函数表,测试代码:

#include<iostream>
#include<stdio.h>
using namespace std;
class Base
{
public:
	virtual void Fun1() { cout << "Base---Fun1" << endl; }
	virtual void Fun2() { cout << "Base---Fun2" << endl; }
};
class Derive:public Base
{
public:
	virtual void Fun1() { cout << "Derive---Fun1" << endl; }//重写了Fun1
	virtual void Fun3() { cout << "Derive---Fun3" << endl; }
	virtual void Fun4() { cout << "Derive---Fun4" << endl; }
};
int main()
{
	Derive d1;
	return 0;
}

如图: 

 这里我们发现d1对象的虚表里面应该有四个虚函数的地址,但是现在为什么只有两个了,其实有时候监视窗口也会骗人的。因为监视窗口也是被优化了的,如果我们想看看不同的对象的虚表里面的内容,可以自己写一个程序来观察一下,代码如下:

typedef void(*pFun)();//对函数指针重新起名字
void PrintVFTable(pFun* pTable)
{
	for (size_t i = 0; pTable[i] != 0; ++i)
	{
		printf("pFunarr[i]:%p", i, pTable[i]);
		pFun f = pTable[i];//调用虚表中的虚函数
		f();
	}
}
/ 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成pFun*,因为虚表就是一个存pFun类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
//后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再
//编译就好了。
int main()
{
	Derive d1;
	Base b1;
	PrintVFTable( (pFun*)  (*(int*)&d1) );//取出对象的前四个字节找到虚表
	PrintVFTable((pFun*)(*(int*)  &b1  )); //打印Base的续表
	return 0;
}

 

如图:

如果是单继承我们发现 ,派生类继承了基类的虚函数表,如果派生类的对基类的虚函数进行了重写派生类的虚函数的指针就会覆盖虚表中本来存放基类虚函数的地方。将派生类中其他的虚函数都放到这个续表中。

        5.2多继承中的虚函数表

 测试代码:

#include<iostream>
using namespace std;
class Base1
{
public:
	virtual void Fun1() { cout << "Base1---Fun1" << endl; }
	virtual void Fun2() { cout << "Base1---Fun2" << endl; }
	int _b1;
};
class Base2
{
public:
	virtual void Fun1() { cout << "Base1---Fun1" << endl; }
	virtual void Fun2() { cout << "Base1---Fun2" << endl; }
	int _b2;
};
class Derive :public Base1,public Base2
{
public:
	virtual void Fun1() { cout << "Derive---Fun1" << endl; }
	virtual void Fun3() { cout << "Derive---Fun3" << endl; }
	virtual void Fun4() { cout << "Derive---Fun4" << endl; }
	int _d;
};
typedef void(*pFun)();//对函数指针重新起名字
void PrintVFTable(pFun* pTable)
{
	for (size_t i = 0; pTable[i] != 0; ++i)
	{
		printf("第%d个虚函数的地址:%p", i, pTable[i]);
		pFun f = pTable[i];//调用虚表中的虚函数
		f();
	}
}
int main()
{
	Derive d1;

	PrintVFTable((pFun*)(*(int*)&d1));//取出对象的前四个字节找到虚表
	cout << endl;
	PrintVFTable((pFun*)(*(int*)(  (char*)&d1+sizeof(Base1)) )); //打印Base2的虚表
	
	return 0;
}

 如图:

        可以看出多继承派生类的未重写的虚函数放在第一个继承的基类的虚函数表中。派生类对于继承的两个基类的虚函数都进行了重写。 

 总结

 这里有一些练习题有兴趣的同学可以练习一下:

1. 下面哪种面向对象的方法可以让你变得富有( )
A: 继承 B: 封装 C: 多态 D: 抽象
2. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承 B: 模板 C: 对象的自身引用 D: 动态绑定
3. 面向对象设计中的继承和组合,下面说法错误的是?()
A:继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复
用,也称为白盒复用
B:组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动
态复用,也称为黑盒复用
C:优先使用继承,而不是组合,是面向对象设计的第二原则
D:继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封
装性的表现
4. 以下关于纯虚函数的说法,正确的是( )
A:声明纯虚函数的类不能实例化对象 B:声明纯虚函数的类是虚基类
C:子类必须实现基类的纯虚函数 D:纯虚函数必须是空函数
5. 关于虚函数的描述正确的是( )
A:派生类的虚函数与基类的虚函数具有不同的参数个数和类型 B:内联函数不能是虚函数
C:派生类必须重新定义基类的虚函数 D:虚函数可以是一个static型的函数
6. 关于虚表说法正确的是( )
A:一个类只能有一张虚表
B:基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C:虚表是在运行期间动态生成的
D:一个类的不同对象共享该类的虚表
7. 假设A类中有虚函数,B继承自A,B重写A中的虚函数,也没有定义任何虚函数,则( )
A:A类对象的前4个字节存储虚表地址,B类对象前4个字节不是虚表地址
B:A类对象和B类对象前4个字节存储的都是虚基表的地址
C:A类对象和B类对象前4个字节存储的虚表地址相同
D:A类和B类虚表中虚函数个数相同,但A类和B类使用的不是同一张虚表
8. 下面程序输出结果是什么? ()

#include<iostream>
using namespace std;
class A {
public:
    A(const char* s) { cout << s << endl; }
    ~A() {}
};
class B :virtual public A
{
public:
    B(const char* s1,const  char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
    C(const char* s1, const char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
    D(const char* s1, const char* s2,const  char* s3,const  char* s4) :B(s1, s2), C(s1, s3), A(s1)
    {
        cout << s4 << endl;
    }
};
int main()

{
    D* p = new D("class A", "class B", "class C", "class D");
    delete p;
    return 0;
}

A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
9. 多继承中指针偏移问题?下面说法正确的是( )
A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
10. 以下程序输出结果是什么()
class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}

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->test();
return 0;
}

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
问答题
1. 什么是多态?
2. 什么是重载、重写(覆盖)、重定义(隐藏)?
3. 多态的实现原理?答:参考本节课件内容
4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是
inline,因为虚函数要放到虚表中去。
5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数
的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表
阶段才初始化的。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析
构函数定义成虚函数。
8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针
对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函
数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况
下存在代码段(常量区)的。

选择题答案:

A D C A B   D D A C B

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

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

相关文章

合宙Air724UG Cat.1模块硬件设计指南--模拟语音通道

模拟语音通道 简介 模拟音频技术是由传感器采集得到的连续变化的值&#xff0c;根据其电压的幅度用来展示声音强弱。CAT.1内置3种音频输出模式&#xff0c;分别为扬声器(SPK)输出&#xff0c;耳机(HP)输出和听筒(RECEIVER)输出。 特性 SPK接口 SPK-、SPK。Speaker差分信号接口…

记压测环境数据表死锁导致接口长时间pending问题

背景 压测过程中测试小伙伴反映某个页面长时间loading无法打开&#xff0c;接下来我们排查一下&#xff0c;既然是压测环境&#xff0c;那么就需要排除服务器资源层面的因素&#xff0c;现在考验的就是在系统资源不足时系统的情况&#xff0c;那么我们就直接从代码层面开始排查…

PADS-LAYOUT菜单及工具说明

目录 1 材料清单输出 2 元件属性检查 3 材料清单制作 4 原理图生成PDF 4.1PDF文件生成 4.2PDF文件查阅 4.3PDF文件打印 5 PADS转Altium Designer 5 Altium Designer转PADS 5.1 直接导入法 5.2 软件生成法 6 PADS层定义 7 设计规则输出 7.1 PADS对象管理 7.2 PAD…

pandas---分箱(离散化处理)、绘图、交叉表和透视表

1. 分箱 分箱操作就是将连续型数据离散化。分箱操作分为等距分箱和等频分箱. 1.1 等宽分箱 pandas.cut(x, bins, rightTrue, labelsNone, retbinsFalse, precision3, include_lowestFalse, duplicatesraise, orderedTrue) x&#xff1a;要分箱的一维数组或者 Series。 bi…

分布式系统概念和设计——分布式共享内存

分布式系统概念和设计 分布式共享内存 分布式共享内存是在不共享物理内存的计算机之间实现数据的共享的一个抽象。 有一个底层运行的系统保证其透明性&#xff0c;但是进程还是根据内存的分布处理物理内存的分布式能力 DMS最关键点&#xff1a; 不需要关心数据的通信&#xff…

百家号热议排名代发

百家号热议排名代发&#xff0c;百度排名怎么做&#xff0c;有什么技巧或者方式方法吗#百度首页关键词排名#百度推广#百度竞价推广#百度关键词排名#百度首页关键词排名方法# 其实百度推广并不赚钱&#xff0c;也没有你想象中的那么好做。 我说三点&#xff0c; 99% 的用户都遇…

C语言之动态内存分配讲解(2)

动态内存函数的介绍 在开始本章节之前&#xff0c;我们来复习一下动态内存分配&#xff08;1&#xff09;中所讲到的知识&#xff0c;看下面目录一和目录二 为什么存在动态内存分配 我们已经掌握的内存开辟方式有 int val 20;//在栈空间上开辟四个字节 char arr[10] {0};/…

Go语言并发之扇入和扇出

1、Go语言并发之扇入和扇出 编程中经常遇到扇入和扇出两个概念&#xff0c;所谓的扇入是指将多路通道聚合到一条通道中处理&#xff0c;Go 语言最简单的扇入 就是使用 select 聚合多条通道服务&#xff1b;所谓的扇出是指将一条通道发散到多条通道中处理&#xff0c;在Go语言…

揭开视频识别(动作识别)的神秘面纱(附代码和demo)!

PaddleDetection 在计算机视觉中&#xff0c;视频识别和检测是一个重要的方向。历年来CVPR和ICCV等顶会文章中这类论文是最多的。视频识别和检测也是最有落地场景前景的&#xff0c;像人脸识别、动作检测、异常检测、行人重识别、行人计数等都是很有落地前景的应用方向。本文介…

shardingsphere第三课各种功能点及核心源码

一、功能点 1.审计功能 分片审计功能是针对数据库分片场景下对执行的 SQL 语句进行审计操作。分片审计既可以进行拦截操作&#xff0c;拦截系统配置的非法 SQL 语句&#xff0c;也可以是对 SQL 语句进行统计操作。 目前ShardingSphere内置的分片审计算法只有一个&#xff0c;…

C语言指针初进阶知识汇总

目录 1 指针 1.1 指针是乜嘢 1.2 指针的声明 1.3 运算符 1.4 简单的小例子们 1.5 指针的运算 1.5.1 指针加减运算 1.5.2 间址运算 1.5.3 指针的相减 两个地址之间的偏移量 2 指针与数组 2.1 指针和一维数组 2.1.1 定义数组及指针变量 2.1.2 能动手就不要瞎扯 2.…

当 Rokid 遇上函数计算

作者&#xff1a;王彬&#xff08;阿里云解决方案架构师&#xff09;、姚兰天&#xff08;Rokid 技术专家&#xff09;、聂大鹏&#xff08;阿里云高级技术专家 &#xff09; 公司背景和业务 Rokid 创立于2014年&#xff0c;是一家专注于人机交互技术的产品平台公司。Rokid 通…

2023/6/18周报

目录 摘要 论文阅读 1、题目和现有问题 2、工作流程 3、图神经网络模块 4、注意力网络 5、实验结果和分析 深度学习 1、GNN和GRU的融合 2、相关公式推导 总结 摘要 本周在论文阅读上&#xff0c;对基于图神经网络和改进自注意网络的会话推荐的论文进行了学习&#…

DPdisPCA算法原理笔记

概要 本文简单理顺《Differentially Private Distributed Principal Component Analysis》论文中的算法原理&#xff0c;它主要提出了一种基于差分隐私的分布式PCA算法&#xff0c;研究了该算法在实验数据以及真实数据中的表现&#xff0c;在参数相同的情况下本算法取得了和没…

OpenGL之深度测试

文章目录 深度测试深度测试函数源代码 深度测试 深度缓冲就像颜色缓冲(Color Buffer)&#xff08;储存所有的片段颜色&#xff1a;视觉输出&#xff09;一样&#xff0c;在每个片段中储存了信息&#xff0c;并且&#xff08;通常&#xff09;和颜色缓冲有着一样的宽度和高度。深…

Python3 条件控制与循环语句 | 菜鸟教程(八)

目录 一、Python3 条件控制 &#xff08;一&#xff09;Python 条件语句是通过一条或多条语句的执行结果&#xff08;True 或者 False&#xff09;来决定执行的代码块。 &#xff08;二&#xff09;if 语句 1、Python中if语句的一般形式如下所示&#xff1a; 2、注意&#…

<Linux开发>驱动开发 -之-platform 驱动

&#xff1c;Linux开发&#xff1e;驱动开发 -之-platform 驱动 交叉编译环境搭建&#xff1a; &#xff1c;Linux开发&#xff1e; linux开发工具-之-交叉编译环境搭建 uboot移植可参考以下&#xff1a; &#xff1c;Linux开发&#xff1e; -之-系统移植 uboot移植过程详细记…

基于SpringBoot+Vue的民宿管理平台系统设计与实现

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架下…

某音短视频评论采集翻页(移动安全篇)

本章测试一个作品的评论及翻页&#xff1a; 以及前面的抓x包方式&#xff0c;在专栏里也有很多&#xff0c;xposed抓包过sslping&#xff0c;通用版本等&#xff1b; https://codeooo.blog.csdn.net/category_11500477.html 翻页通过页码来控制&#xff1a; # -*- coding:…

Docker部署(1)——将jar包打成docker镜像并启动容器

在代码编写完成即将部署的时候&#xff0c;如果采用docker容器的方法&#xff0c;需要将jar包打成docker镜像并通过镜像将容器启动起来。具体的步骤如下。 一、首先下载java镜像 先使用docker search java命令进行搜索。 然而在拉取镜像的时候要注意不能直接去选择pull java ,…