C++进阶 | [2] 多态

摘要:多态的概念,多态的条件,虚函数的重写,抽象类,多态的原理,虚函数与虚函数表,与多态有关的问答题


1. Concept

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。

举例:买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人 买票时是优先买票。(该例子的示例代码如下)

#include<iostream>class ticket
{
public:virtual void buy(){std::cout << "ticket" << std::endl;}
};class Student_ticket :public ticket
{
public:virtual void buy(){std::cout << "Student:半价" << std::endl;}
};void test1()
{ticket t;Student_ticket st;ticket* pt = &st;pt->buy();pt = &t;pt->buy();
}int main()
{test1();return 0;
}

👆上述代码的运行结果:

Student:半价
ticket


上述代码说明:当基类的指针 pt 指向的对象是 Student Type 时,pt->buy() 会去调用 Student 的 buy() 函数,当 pt 指向的对象时 ticket Type 时,pt->buy() 回去调用 ticket 的 buy() 函数。最后代码的运行结果就如同我们所解释的这样。


2. Condition

多态的条件:
完成虚函数的重写


基类的指针或引用去调用虚函数

1)虚函数的重写

虚函数:即被virtual修饰的类成员函数称为虚函数。

  1. virtual 该关键词只能修饰成员函数,只能用在成员函数的声明上!
  2. 虚函数的重写要满足三同函数名、参数列表(仅指参数个数、参数数据类型及顺序)、返回值
    (ps.派生类可以不加 virtual,只要满足三同,基类和子类之间也能实现虚函数的重写,因为“虚函数的重写”的“重写”是对基类某个成员函数的重写,可以认为是基类的虚函数的 virtual 被派生类继承下来了。然而不建议这样做,最好基类和子类的虚函数都加 virtual)

虚函数重写的两个例外:

  • 协变(基类与派生类虚函数返回值类型不同) :
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)(示例如下)
    class A{};
    class B : public A {};class Person 
    {
    public:virtual A* f() {return new A;}
    };class Student :public Person 
    {
    public:virtual B* f() {return new B;}
    };
    

  • 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
class Person{};class Student :public Person{};void test2()
{Person* p = new Person;delete p;//p->destructor()+operator detele(p)Person* p2 = new Student;delete p2;//p2->destructor()+operator detele(p2)
}

如上代码,按编译器的处理,基类 Person 和派生类 Student 的 destructor 构成隐藏关系,然而这里我们期待构成多态,p2 指针指向的是一个 Student 类型,则 delete p2 应该调用 Student 的析构。因此这里必须对基类和派生类的析构函数实现虚函数的重写如下

class Person
{
public:virtual ~Person(){//……}
};class Student :public Person
{
public:virtual ~Student()//此处的virtual是可以省略的,但是不建议省略{//……}
};

基于此,建议在继承关系中,析构函数都实现成虚函数,以防止析构出现问题。 

2)多态的调用

  • 对于自定义类型调用成员函数
    普通调用:调用这个函数的 Type 是什么,就去调用这个 Type 的函数。例如 class Person 类型的指针调用函数就会去调用 Person 的成员函数,class Student 类型的指针去调用函数就会去调用 Student 的函数。
    多态调用(凡是不满足多态调用的两个条件的都是普通调用):(继承关系是多态调用的前提)调用函数的 指针 或 引用 指向的对象是什么,就去调用这个 对象(object) 的函数。例如 Person* 的指针如果指向的是一个 Person 类型的对象,就去调用 Person 的函数;Person* 的指针如果指向的是一个 Student 类型的对象,就去调用 Student 的函数。(指针类型和指针指向的数据的类型是两码事,指针类型的不同决定了对指针本身进行操作的结果的不同,对此感到理解困难的可以去温习C语言关于指针的解释)

关于虚函数的重写。重写其实可以理解为继承了函数的接口(或者说函数的声明),重写了函数的定义(重写定义不是重定义,重定义是隐藏,这里只是为了形象地解释重写的过程)

下面来看一道题来加深对多态调用的理解:

以下程序输出结果是什么()

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: 以上都不正确

分析及解答: 

如上图所说,从 p->test()普通调用 到 A* this ->func() 构成了多态调用(func完成了虚函数重写,只是参数的缺省值给的不同不影响完成重写),所以这里会去调用 B 的 func 函数,又因为我们前面说到过,重写继承了基类的函数接口,因此这里 val 的缺省值为 1 。(从这里也印证了为什么参数的缺省值给的不同不影响完成虚函数的重写,因为不管派生类给什么缺省值,多态调用的时候都只会用基类的接口给)最后我们得到本题的答案:选B选项。

3)函数重载、重写(覆盖)、重定义(隐藏)的比较

  • 函数重载:①位于同一作用域;②函数名相同,参数列表不同(仅返回值不同不能构成函数重载)
  • 函数重写:①分别位于基类和派生类的作用域;②三同(函数名同,参数列表同,返回值同);③virtual 修饰
  • 函数重定义:①分别位于基类和派生类的作用域;②函数名同

(从上面不难看出,重写的要求比重定义的言责,所以我们可以把重写看成是一种特殊的重定义)

4)关键词

final:对于 类 可以使得该类不能被继承;对于 虚函数 可以使得不能被重写(使用示例如下)

class ticket final
{
public:virtual void buy() final{std::cout << "ticket" << std::endl;}
};

override:修饰派生类的虚函数,用于检查是否完成重写,只能完成检查。如果没有重写编译报错。(使用示例如下)

class ticket //final
{
public:virtual void buy() //final{std::cout << "ticket" << std::endl;}
};class Student_ticket :public ticket
{
public:virtual void buy()override{std::cout << "Student:半价" << std::endl;}
};

3. 抽象类

概念在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)。抽象类不能实例化出对象(但是可以定义这个类class的类型Type的指针→多态调用。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

意义:①抽象类某种程度上强制派生类实现虚函数的重写;②提供了“抽象”的概念。(抽象是从众多的事物中抽取出共同的、本质性的特征)例如,class Animal,现实中没有个具体的对象,而可以其派生类 class Cat、Gog……具体的物种现实中有具体的对应(即概念上等同于可以实例化对象)因此一般抽象类有多个派生类。所以通俗来讲就是抽象类的存在告诉我们这个类所指的概念在现实中无具体的实体。

class Animal
{
public:virtual void Print() = 0;
};class Cat :public Animal
{
public:void Print(){std::cout << "Cat" << std::endl;}
};void func()
{Animal* pa = new Cat;pa->Print();
}

接口继承与实现继承:虚函数继承就是一种接口继承;普通函数继承就是一种实现继承


4. 多态的原理

vft——virtual function table 虚函数表;vftptr——virtual function table pointer 虚函数标指针.

虚函数表及虚函数表指针-图例

(虚函数表指针命名为 __vfptr 是 vs 平台下编译器的个人行为) 

如上,含虚函数的类中都有一个“vft(虚函数表,简称虚表)”

(x64平台)

多态调用的原理: 

class Base
{
public:virtual void func1(){std::cout << "Base:func1()" << std::endl;}
private:int _a = 0;
};class Derive :public Base
{
public:virtual void func1(){std::cout << "Derive:func1()" << std::endl;}
private:int _b = 1;
};

从上图我们看到,多态调用其实就是去虚函数表里取函数的地址直接调用,所以如果指针指向的是派生类对象,那么取到的 vftptr 就是派生类对象的虚表指针,再由此找到虚表里存储的函数地址;如果指向的是基类对象就是取到不一样的虚表。这样就完成了多态调用。

虚函数的“重写”又可以称为“覆盖”,可以 形象地理解 为派生类先把基类的虚表拷贝一份过来,在虚表中对于实现了虚函数重写的函数的地址将被覆盖成一个新地址。(只是这样理解而不是说编译器会真的按所说的做)

注意:对于虚表,一个类只有一个虚表,不同类不共用虚表,这个类所有实例化出来的对象共享一份虚表。成员函数对于一个类来说是“公共区域”,存储函数地址的虚表同样也是。

了解完多态的原理之后我们再来看普通调用与多态调用的区别:
多态调用是运行时,去虚表里面找到函数的地址,确定地址后,调用这个地址;普通调用时编译或链接时,确定地址。

  • 为什么多态调用一定要基类的指针或引用
    ①派生类可以赋值给基类的指针/引用/对象,基类对象不可以赋值给派生类。
    ②基类的指针/引用直接指向/指代派生类对象(切片)
    ③派生类对象 赋值给 基类对象,不会拷贝虚函数表指针!如果脸虚表都拷贝,那么多态调用就失效了,我们无法分清这个虚表里存储的是哪个类的成员函数地址,更严重的,会导致析构函数调用错误。

5. 虚函数与虚函数表

  • 虚函数存在哪?虚函数表存在哪?
    答:代码段

虚函数本质上就是函数,很容易想到和普通函数一样存在代码段;

对于虚函数表
①应该不在栈上。如果在栈上出作用域就会被销毁,而一个类共用一个虚表;
②应该不在堆上。堆上的空间是需要动态申请的,如果是在堆上,谁去申请空间,谁又去清理空间呢?分别发生在什么时候呢?

下面对上述猜想进行验证:

class Base{//……};//具体内容省略
void test4()
{static int a = 0;printf("静态区:%p\n", &a);int b = 0;printf("栈:%p\n", &b);int* p = new int;printf("堆:%p\n", p);const char* pc = "hello";printf("代码段:%p\n", pc);Base bs;printf("虚表指针:%p\n", *((int*)&bs));printf("虚表:%p\n", &Base::func1);
}

上述代码-说明ps. x86(32位)平台下指针的大小为 4 byte,与 int 的大小相同。
①打印虚表指针的地址时,对 bs 取地址之后强制转换为 int* 的指针再解引用,是因为虚表指针位于 Base 对象的开头位置,而不同的指针类型之间的转化是自然的,我们很容易取到 bs 开头的虚表指针之后将其转化后再解引用;
②函数名即为函数地址,但这里要打印这个地址的之后仍需要加 “&”,这是一个特殊的语法规定;
③func1 是 class Base 中的成员函数,要突破类域才能访问。

执行上述代码可知,虚表的地址和代码段的地址很相近,由此我们可以粗略得出——虚表位于代码段(更靠近常量区)

1)单继承的情况

  • 虚函数都存在虚函数表里吗?
  • 答:是的

为了验证上面的说法,我们需要打印虚函数表(有时候编译时的监视窗口并不能完整的显示虚表),代码如下:
(运行时遇到的问题:关于vs平台下对虚函数表的结尾的处理(下面代码的注释中有写)存疑,在虚函数实现的不同的情况下好像有所不同。如果该错误由代码本身导致,欢迎指正。)
(代码注释提供了一些关于个别语句的解释,如有不懂,注意参看注释)
实现打印虚表的思路很简单:虚函数表就是一个指针数组,我们只需要像打印普通数组那样打印虚表即可。代码涉及到函数指针,对该部分感到难以理解的请去温习C语言的函数指针。

class Base
{
public:virtual void func1() { std::cout << "Base:func1()" << std::endl; }virtual void func2() { std::cout << "Base:func2()" << std::endl; }virtual void func3() { std::cout << "Base:func3()" << std::endl; }
};class Derive :public Base
{
public:virtual void func2() { std::cout << "Derive:func2()" << std::endl; }virtual void func4() { std::cout << "Derive:func4()" << std::endl; }
};class D_Derve :public Derive
{
public:virtual void func3() { std::cout << "D_Derive:func3()" << std::endl; }
};typedef void(*VFUNC)();//定义一个函数指针类型 void(*)() 返回值为void; 参数列表为(); 的函数指针为 VFUNCvoid printVFT(VFUNC a[])//本质上就是 VFUNC* a,a是一个函数指针数组,数组名
{for (size_t i = 0; a[i] != 0; ++i)//因为vs平台对vft会以‘0’结尾,所以这里以不等于0为循环继续的条件{printf("[%d] -> %p", i, a[i]);std::cout << " ";VFUNC fp = a[i];fp();//通过函数指针调用函数}}void test5()
{Base b;printVFT((VFUNC*)(*(int*)&b));//先对b取地址, 再将其强转为int*(取到vftptr), 再对其解引用(拿到vft的地址), 再将这个地址强转为VFUNC*std::cout << "----------------------------------------------Base" << std::endl;std::cout << std::endl;Derive d;printVFT((VFUNC*)(*(int*)&d));//????std::cout << "----------------------------------------------Derive" << std::endl;std::cout << std::endl;//Base* pb = &d;D_Derve d_d;printVFT((VFUNC*)(*(int*)&d_d));std::cout << "----------------------------------------------D_Derive" << std::endl;//Derive* ptest = &d_d;}

关于指针类型强转的说明:
x64(64位):指针大小为 8 byte. 👉 这个时候不能再强转成 int* 而应为 long long* ,而 long long* 同样适用于32位的环境,因为编译器会对地址进行截断,但这样写对32位的环境来说并不安全。可以使用 条件编译 的方式来使得代码有更好的跨平台性。 
x86(32位):指针大小为 4 byte.

2)多继承的情况

如下代码为多继承的情况:(下面代码使用了单继承中实现了的函数)

class Base1
{
public:virtual void func1() { std::cout << "Base1:func1()" << std::endl; };virtual void func2() { std::cout << "Base1:func2()" << std::endl; };
private:int _b1;
};class Base2
{
public:virtual void func1() { std::cout << "Base2:func1()" << std::endl; };virtual void func2() { std::cout << "Base2:func2()" << std::endl; };
private:int _b2;
};class Derive_B1B2 :public Base1, public Base2
{
public:virtual void func1() { std::cout << "Derive_B1B2:func1()" << std::endl; };virtual void func3() { std::cout << "Derive_B1B2:func3()" << std::endl; };
private:int _d;
};void test6()
{Derive_B1B2 d;printVFT((VFUNC*)*(int*)&d);//打印 Derive_B1B2 中的 Base1 的vftstd::cout << "---------------------------------------------------------------------" << std::endl;//printVFT((VFUNC*)*((char*)&d + sizeof(Base1)));//打印 Derive_B1B2 中的 Base2 的vft//强转成 char* 是因为char类型占1byte,而sizeof的单位为byte, 这样做可以使得指针往后挪动到我们想要的地方去//更优的写法Base2* pb2 = &d;printVFT((VFUNC*)*(int*)pb2);
}

执行结果:(具体地址每次执行结果会不同)

[0] -> 007411DB Derive_B1B2:func1()
[1] -> 00741398 Base1:func2()
[2] -> 0074108C Derive_B1B2:func3()
---------------------------------------------------------------------
[0] -> 0074100F Derive_B1B2:func1()
[1] -> 007410EB Base2:func2()

上述代码逻辑图解:(ps.多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。如下func3函数。

*拓展了解:从上面多继承的代码的执行结果我们可以发现都是 Derive_B1B2 的 func1 函数,但是打印出来的地址却不同。e.g. [0] -> 007411DB Derive_B1B2:func1() 与 [0] -> 0074100F Derive_B1B2:func1()。我们需要进一步通过反汇编来了解其中发生了什么。

p1与p2调用func1

从上图我们可以看出,最终调用函数的地址还是一样的。形象地理解为:最后func1的地址是目的地,监视窗口看见不同的地址是因为调用func1的时候走了两个不同的“路线”。另外,从上图可以发现,p2调用函数过程中有一个“ sub        ecx,8 ”的操作。我们可以由此推测p2调用函数过程中这么多中间过程是为了修正 this 指针。因为此处 p1 与 p2 分别是 Base1* 和 Base2* 类型,而 func1 函数作为 Derive_B1B2 的成员函数,具有隐藏的参数 Derive_B1B2* this,p1恰巧与&d是相同的指针内容,只是指针类型不同,调用的时候不需要修正this指针,只需要修改对该指针的类型识别即可;p2则不同,这个指针与&d的内容不同,所以需要在中间修正this指针。

菱形继承的情况(了解)

一般的菱形继承就跟多继承的情况是一样的。

菱形虚拟继承的情况:(仅举例展示)


6. 总结-问答题⭐

(节选部分问题)

1. ( )是面向对象程序设计语言中的一种机制。这种机制实现了方法的定义与具体的对象无关,而对方法的调用则可以关联于具体的对象。
A: 继承        B: 模板        C: 对象的自身引用        D: 动态绑定
答:静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载 2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。

2. 内联函数可以是虚函数吗?
答:普通调用时,inline起作用;多态调用时,inline不起作用。

3. 静态成员函数可以是虚函数吗?
答:不能。静态成员函数强行实现成虚函数会引发编译错误。
分析:静态成员函数可看作成受类域限制的全局函数,没有隐藏的形参 this指针,无法进行多态调用。

4. 构造函数可以是虚函数吗?
答:不能。会引发编译错误。
分析:类实例化出一个对象的时候,这个对象的虚函数表指针是通过构造函数阶段才被初始化的,而多态调用,要去虚函数表中去找函数的地址,而在执行构造函数之前,虚函数表指针还未被初始化。

5. 对象调用普通函数快还是调用虚函数快?
答:看是普通调用还是多态调用,多态调用慢一点。注意:虚函数不一定是被多态调用,也可以是被普通调用,注意多态调用的两个条件。

6. 虚函数表什么时候生成,存储在哪?
答:编译时生成,但执行构造函数的时候虚函数表指针才被初始化;存储在代码段。


END

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

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

相关文章

W801学习笔记十九:古诗学习应用——下

经过前两章的内容&#xff0c;背唐诗的功能基本可以使用了。然而&#xff0c;仅有一种模式未免显得过于单一。因此&#xff0c;在本章中对其进行扩展&#xff0c;增加几种不同的玩法&#xff0c;并且这几种玩法将采用完全不同的判断方式。 玩法一&#xff1a;三分钟限时挑战—…

深入理解网络原理4----TCP核心特性介绍(下)

文章目录 前言一、拥塞控制二、延时应答三、捎带应答四、面向字节流五、异常处理 前言 随着时代的发展&#xff0c;越来越需要计算机之间互相通信&#xff0c;共享软件和数据&#xff0c;即以多个计算机协同⼯作来完成业务&#xff0c;就有了⽹络互连。 一、拥塞控制 虽然TCP有…

使用memcache 和 redis 、 实现session 会话复制和保持

一、NoSQL介绍 NoSQL是对Not Only SQL、非传统关系型数据库的统称 NoSQL一词诞生于1998年&#xff0c;2009年这个词汇再次提出指非关系型、分布式、不提供ACID的数据库设计模式 随着互联网时代的数据爆发时增长、数据库技术发展的日新月异&#xff0c;要适应新的业务需求&am…

[C#] Visual Studio开发工具编译出错匹配目标平台x86或x64架构问题解决

使用Visual Studio开发工具的NuGet管理包插件时&#xff0c;开发者常常会遇到编译错误。尤其是在切换目标平台架构&#xff08;如AnyCPU、x86或x64&#xff09;时&#xff0c;有时会发现切换似乎不起作用&#xff0c;导致编译仍然出错。 文章目录 You need to specify platform…

英语复习之英语形近词总结(二)

接着总结形近词 单词释义例句 impress 英 /ɪmˈpres/ 美 /ɪmˈpres/ vt.盖印&#xff1b;强征&#xff1b;传送&#xff1b;给予某人深刻印象 vi.给人印象。印象&#xff0c;印记&#xff1b;特征&#xff0c;痕迹 1.It didnt impress me as a good place to live. 那地方…

uniapp 自定义相机插件(组件版、缩放、裁剪)组件 Ba-CameraView

自定义相机插件&#xff08;组件版、缩放、裁剪&#xff09; Ba-CameraView 简介&#xff08;下载地址&#xff09; Ba-CameraView 是一款自定义相机拍照组件&#xff0c;支持任意界面&#xff0c;支持裁剪 支持任意自定义界面支持手势缩放支持裁剪&#xff08;手势拖动、比…

Leetcode—1396. 设计地铁系统【中等】

2024每日刷题&#xff08;127&#xff09; Leetcode—1396. 设计地铁系统 实现代码 class UndergroundSystem { public:typedef struct Checkin {string startStation;int time;} Checkin;typedef struct Checkout{int tripNum;int totalTime;} Checkout;UndergroundSystem()…

器件配置比特流或 PDI 设置-Zynq-7000 比特流设置

Zynq-7000 比特流设置 下表所示 Zynq -7000 器件的器件配置设置可搭配 set_property <Setting> <Value> [current_design] Vivado 工具 Tcl 命令一起使用。 注释 &#xff1a; 用于加密的比特流设置对 Zynq-7000 器件无效。

HNU-人工智能-实验2-简单CSP问题

人工智能-实验2 计科210x 甘晴void 一、实验目的 求解约束满足问题 使用回溯搜索算法求解八皇后问题 二、实验平台 课程实训平台https://www.educoder.net/paths/369 三、实验内容 3.0 题目要求 回溯搜索算法 搜索与回溯是计算机解题中常用的算法&#xff0c;很多问…

Redis__数据持久化

文章目录 &#x1f60a; 作者&#xff1a;Lion J &#x1f496; 主页&#xff1a; https://blog.csdn.net/weixin_69252724 &#x1f389; 主题&#xff1a;Redis__数据持久化 ⏱️ 创作时间&#xff1a;2024年05月01日 ———————————————— 这里写目录标题…

3.10设计模式——Template Method 模版方法模式(行为型)

意图 定义一个操作中的算法骨架&#xff0c;而将一些步骤延迟到子类中&#xff0c;Template Method 使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定步骤。 结构 AbstractClass&#xff08;抽象类&#xff09;定义抽象的原语操作&#xff0c;具体的子类将重定…

幼猫粮适合几个月的猫?

关于幼猫粮的选择&#xff0c;你是否有过疑惑呢&#xff1f;幼猫粮适合几个月的猫呢&#xff1f;今天&#xff0c;就让我来为大家详细解答这个问题吧&#xff01;&#x1f43e; 首先&#xff0c;我们要明确一点&#xff0c;幼猫粮是为4-12个月大的小猫咪特别设计的。在这个阶段…

[Java EE] 多线程(六):线程池与定时器

&#x1f338;个人主页:https://blog.csdn.net/2301_80050796?spm1000.2115.3001.5343 &#x1f3f5;️热门专栏:&#x1f355; Collection与数据结构 (90平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm1001.2014.3001.5482 &#x1f9c0;Java …

功能描述如何逻辑

assign相当于连线&#xff0c;注意每个assign独立&#xff0c;如果有变化立刻变化

CSS选择器、字体文本属性、三大特性、盒子模型等

目录 导入css简介HTML的局限性CSS-网页美化CSS语法规范CSS代码风格 选择器基础选择器复合选择器 CSS字体属性字体系列字体大小字体粗细文字样式字体复合属性 CSS文本属性文本颜色对齐文本装饰文本文本缩进行间距(即行高) CSS的引入方式emmet语法元素显示模式什么是&#xff1f;…

python:dict(字典、映射)使用解析

列表使用&#xff1a;[] 元组使用&#xff1a;() 字符串使用&#xff1a;"" 集合使用&#xff1a;{} 字典&#xff1a;{key:value} 生活中的字典&#xff1a;记录大量的字与含义&#xff1b; python中的字典&#xff1a;通过key去找到对应的value;[key]:value …

Vue 工程化开发入门

Vue开发的两种方式&#xff1a; 核心包传统开发模式&#xff1a;基于html/css/js文件&#xff0c;直接引入核心包&#xff0c;开发Vue工程化开发模式&#xff1a;基于构建工具的环境中开发Vue 这里选择Vue cli脚手架 进行开发&#xff0c;搜索教程自行下载。 组件化开发 一个页…

STM32入门学习之ADC

1.ADC在STM32进行数据采集时十分重要。通过ADC可以将外界的数字信号转换为模拟信号&#xff0c;以满足采样的需求。(资料参考于正点原子) STM32 拥有 1~3 个 ADC &#xff08; STM32F101/102 系列只有 1 个 ADC &#xff09;&#xff0c;这些 ADC 可以独立使用&#…

苍穹外卖,接入redis cache后,新增套餐有问题

终端报错&#xff1a; java.lang.IllegalArgumentException: Null key returned for cache operation (maybe you are using named params on classes without debug info?) Builder[public com.sky.result.Result com.sky.controller.admin.SetmealController.save(com.sky.d…

虚拟机网络实现桥接模式

虚拟机网络实现桥接模式 虚拟化软件&#xff1a;VMware 17 Linux&#xff1a;rocky8_9 主机&#xff1a;Win10 文章目录 虚拟机网络实现桥接模式1. 桥接模式介绍2. 查看Win本机的网络信息&#xff08;以笔记本电脑以WiFi联网为例&#x…