C++【多态】

文章目录:

  • C++ 多态
    • 1. 多态的概念
    • 2. 多态的定义和实现
      • 2.1 构成多态的必要条件
      • 2.2 虚函数和重写
      • 2.3 虚函数重写的两个例外
      • 2.4 例题运用
      • 2.5 final 和 override
      • 2.6 重载、重写、重定义
    • 3. 抽象类
    • 4. 多态的原理
      • 4.1 虚表指针
      • 4.2 多态和非多态调用
      • 4.3 回想满足条件
      • 4.4 虚函数表
      • 4.5 知识补充
      • 4.6 多继承中的虚函数表
      • 4.7 动态绑定与静态绑定
      • 4.8 多继承例题

C++ 多态

前面介绍了封装和继承,本文就来介绍面向对象三大特征的最后一个—多态,多态就是面对不同对象时所展现的不同状态,下面就一起来看看吧

1. 多态的概念

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

举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票

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

2. 多态的定义和实现

实现多态需要借助虚函数表(虚表),而构成虚表又需要虚函数,最后再使用虚表指针来进行函数定位和调用

2.1 构成多态的必要条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

2.2 虚函数和重写

virtual修饰的类成员函数称为虚函数

举个例子

class A 
{
public:virtual void fun() { cout << "A::fun()" << endl; }
};

上面的func() 就是虚函数,下面再来见见多态吧

#include <iostream>
using namespace std;class Person 
{
public:virtual void BuyTicket() { cout << "成人全价票" << endl; }
};class Student : public Person 
{
public:virtual void BuyTicket() { cout << "学生半价票" << endl; }
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

是否满足多态:满足继承前提,Func()函数中通过基类的引用p调用函数,被调用的函数BuyTicket()是虚函数,且派生类对基类的虚函数BuyTicket()进行了重写

上面说到了重写,那到底什么是虚函数的重写呢?

  • 虚函数的重写也叫覆盖,派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
  • 虚函数的作用是在目标函数(想要构成多态的函数)之间构成 重写,一旦构成了 重写,那么子类对象在实现此虚函数时,会继承父类中的虚函数接口(返回值、函数名、参数列表),然后覆盖至子类对应的虚函数处,因此重写又叫做覆盖

下面我们去掉派生类中的virtual关键字看看运行结果

这里为什么还是构成重写呢?

  • 这是因为在重写基类虚函数后,派生类的虚函数在不加virtual关键字时,也可以构成重写,这里叫做接口继承(因为继承后基类的虚函数被继承下来了,继承了参数,也就是拷贝了一份基类的虚函数的参数),在派生类依旧保持虚函数属性,但是该种写法不是很规范,不建议这样使用。

这里如果只是把基类中的virtual修饰关键字去掉能否构成虚函数重写呢?

显然是不能的,这是因为此时已经不满足派生类对基类的虚函数进行重写了,也就是不满足多态了,这里p.BuyTicket()中的p对象是Person类型的,所以两次调用都是调用的基类的BuyTicket()函数

最后再来看看父子类中的函数都不加virtual的情况

结果是必然的,因为此时已经不满足多态条件中的任何一条了。但是观察得到这里构成了继承中的隐藏,但是这里是基类对象去调用的基类成员函数,这里虽然构成隐藏,但是这里没有用子类对象去调用子类的成员函数BuyTicket(),也就不体现隐藏关系,只有子类调用才会体现隐藏关系

讲个经验:

满足多态看调用对象,不满足多态看调用对象类型

2.3 虚函数重写的两个例外

例外一:析构函数重写(虚函数名不同)

#include <iostream>
using namespace std;class Person 
{
public:virtual ~Person() { cout << "~Person()" << endl; }
};class Student : public Person 
{
public:virtual ~Student() { cout << "~Student()" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

上述中满足多态两个条件:通过基类Person的指针调用虚函数,同时也Person()构成了虚函数,并且子类的虚函数Studen()对父类进行了重写,但是这里的虚函数重写违反了派生类虚函数与基类虚函数的三同(返回值类型、函数名字、参数列表完全相同)中的函数名相同,但是为什么还成立呢?

  • 这是编译器会对析构函数的名称做特殊处理,编译后析构函数的名称统一处理成destructor,这就可能存在析构错误调用的问题,因此可以利用 virtual 修饰父类的析构函数,这样子类在继承时,会自动形成多态,确保不同对象的析构函数能被成功调用,避免内存泄漏

这里如果把基类的析构函数中的virtual关键字去掉呢?

这里发现程序直接崩溃了

  • 这是因为这里没有构成多态,导致看对象类型调用函数,又因为p1p2都是Person类型,所以都是调用的~Person()函数,析构了两次导致程序崩溃

例外二:协变(返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(不常用,了解即可)

#include <iostream>
using namespace std;class A {};
class B : public A {};class Person 
{
public:virtual A* f() {cout << "Person() -> f()" << endl;return new A;}
};class Student : public Person 
{
public:virtual B* f() {cout << "Student() -> f()" << endl;return new B;}
};void fun(Person& p)
{p.f();
}int main()
{Person p;fun(p);Student s;fun(s);return 0;
}

2.4 例题运用

下面来看一个题目

#include <iostream>
using namespace std;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: 以上都不正确

解析:

题目改编一:

#include <iostream>
using namespace std;
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[])
{A* p = new B; //B* --> A* 对象赋值转换p->test(); return 0;
}

最终结果和原题一样输出 B->1

题目改编二:

#include <iostream>
using namespace std;
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(); }
};
int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}

解析:不满足多态

2.5 final 和 override

finaloverrideC++11中新增的两个多态相关的关键字

override关键字:

  • 修饰子类的虚函数,检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错

可见因为参数列表不同,无法完成重写,override关键字可以在编译之前就将其检查出来,以保证完成实现多态

final关键字:

  • 修饰父类的虚函数,表示该虚函数不能再被子类的虚函数重写

可见父类的虚函数加上了final关键字后无法被子类的虚函数重写

2.6 重载、重写、重定义

三者总结:

  • 重载:就是函数重载,函数名相同参数不同构成重载,不同的函数参数最终修饰结果不同,确保链接时不会出错

  • 重写(覆盖):父子类中,当出现虚函数且符合重写的三同原则时,就会发生重写(覆盖)行为

  • 重定义(隐藏):父子类中,当子类中的函数名与父类中的函数名相同时,会隐藏父类同名函数,默认调用子类的函数,可以通过 :: 指定调用

3. 抽象类

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

纯虚函数也可以与普通虚函数构成重写,也能实现多态,但是抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

#include <iostream>
using namespace std;class Car //抽象类
{
public:virtual void Drive() = 0; //纯虚函数
};
class Benz :public Car
{
public:virtual void Drive() //虚函数重写{cout << "Benz-舒适" << endl;}
};
class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};int main()
{Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

抽象类适合用于描述无法拥有实体的类,比如动物、植物等,这些都是不能直接使用的,需要经过继承赋予特殊属性后,才能作为一个对象

4. 多态的原理

4.1 虚表指针

先来看一道笔试题,这里问sizeof(Base)是多少?

#include <iostream> 
using namespace std;class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _a = 0;int _b = 1;char _c;
};int main()
{Base B;cout << sizeof(B) << endl;return 0;
}

32位平台下,如果按照内存对齐的规则,这里应该是12字节大小,但真是如此吗?

这多出来的4个字节是哪里来的呢,调整到64位平台下,大小会变成24,大小能随平台变化而发生变化,我们第一个想到的自然是指针了,通过监视再来看一下

这里我们发现果然多了一个void**类型的指针_vfptr的指针

对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表functionptr代表pointer)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,多态也就是依靠虚表指针+虚表实现了的

多态满足条件中有个条件是:必须通过基类的指针或者引用调用虚函数。那么这里怎么来实现的用引用或者指针就可以调用不同类的成员函数?为什么用普通类型就不能实现调用不用类的成员函数呢?

#include <iostream> 
using namespace std;class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{p.BuyTicket();
}int main()
{Person pxx;Func(pxx);Student sxx;Func(sxx);return 0;
}

这就验证了,_vfptr指针就是用来调用不同类的成员函数的

4.2 多态和非多态调用

先来看看非多态调用

不是多态时,直接按照对象的类型来调用对应的成员函数

再来看看多态调用

上述观察到,进行执行p.BuyTicket()语句时,编译器是不知道调用哪个类的成员函数的,当完成p.BuyTicket()语句后, 编译器做出了处理工作,直接跳转到Person::BuyTicket()成员函数。

证明一下在执行p.BuyTicket()语句时编译器是不知道调用哪个类的成员函数

虚函数的调用过程

  • 存在虚函数且构成重写的前提下,使用父类指针或父类引用指向对象,其中包含切片行为
  • 切片后,将子类中不属于父类的切掉,只保留父类指针可调用到的部分函数
  • 在调用时,父类指针的调用逻辑就是,虚表第一个位置调用第一个函数,虚表第二个位置调用第二个函数,但是因为此时的虚表是切片得到的,所以同一位置可以调用到不同的函数,这就是多态

4.3 回想满足条件

满足虚函数重写

  • 我们描述了虚函数表,那么如果父类中没有虚函数的话就不能构成多态,再来看这句话不难理解了,因为没有虚函数,就没有虚函数表,当使用父类的引用和指针来调用对应的函数时,就不会在虚函数表中查找,而是直接依据类型来进行调用

满足父类的指针或引用

  • 为什么指针或者引用可以调用相对应的成员函数,但是对象不行呢?对象也能够完成切片啊?原因还是在虚函数表上,对象的话就要拷贝,这里也就要拷贝虚函数表,如果拷贝就会有很大的问题:就比如子类拷贝给了父类,此时就分不清了,父类的虚函数表也是子类的了

4.4 虚函数表

有虚函数的类的对象中都有虚表指针_vfptr,虚表指针所指向的就是对应类的虚函数表(虚表),即virtual function table -> vft,虚表中存储的是虚函数指针,可以在调用函数时根据不同的地址调用不同的方法

下面来看一段代码

#include <iostream>
using namespace std;class Person
{
public:virtual void func1() { cout << "Person::fun1()" << endl; };virtual void func2() { cout << "Person::fun2()" << endl; };void func3() { cout << "Person::fun3()" << endl; };
};
class Student : public Person
{
public:virtual void func1() { cout << "Student::fun1()" << endl; };virtual void func4() { cout << "Student::fun4()" << endl; };
};int main()
{Person p;Student s;return 0;
}

通过监视和内存来详细观察一下虚表指针,虚函数地址以及虚表的关系

再来看看把虚函数重写也叫做覆盖的原理

上述我们观察到BuyTicket()成员函数进行了重写,但是travel()ClaimCoupon()成员函数并没有重写,此时观察到Person类维护的有一个虚函数表,Student维护的也有一个虚函数表,BuyTicket()成员函数重写了也就覆盖了

可以这么理解,原本的子类是继承父类的成员函数的,那么当前子类的虚函数表也就是父类的虚函数表,但是当子类对父类的虚函数重写后,原本的父类的虚函数就被覆盖为新的重写的虚函数,这个例子的体现就在_vfptr[0]位置上的BuyTicket()虚函数指针。 _vfptr[1] _vfptr[2]两个虚函数并没有被重写,所以就是继承的父类的虚函数。

我们可以打印虚函数表来验证其真实性

  • 虚函数表是一个函数指针数组,所以打印这里的函数指针数组即可。虚函数表是以nullptr为结束标记的(vs中)。虚函数表指针是对象前4个或者8个字节
  • 可以先将虚表指针强转为指向首个虚函数的指针,然后遍历虚表打印各个虚函数地址即可
#include <iostream>
using namespace std;
class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}virtual void Func4(){cout << "Derive::Func4()" << endl;}
private:int _d = 2;
};typedef void(*_vfptr)(); //void(*_vfptr)() --> 函数指针 --> 起别名为_vfptrvoid print_virtual_function(_vfptr table[]) //打印虚函数表 table -> 数组名 -> _vfptr*(类型)
{for (int i = 0; table[i]; ++i) //虚函数表是以nullptr为结束标记的{printf("[%d]:%p\n", i, table[i]);}cout << endl;
}
int main()
{Base b;Derive d;//只是支持32位机器下//&b -> Base* -> (int*)&b -> 强制转换为int* -> (*(int*)&b) -> 拿到b对象的前四个字节 -> (_vfptr*)(*(int*)&b) -> 强制转换为_vfptr*(函数指针的指针)print_virtual_function((_vfptr*)(*(int*)&b));print_virtual_function((_vfptr*)(*(int*)&d));//支持32和64位机器下print_virtual_function((_vfptr*)(*(long long*)&b));print_virtual_function((_vfptr*)(*(long long*)&d));//支持32和64位机器下//&b -> Base* -> (_vfptr**)&b -> Base*强制转换为_vfptr**(函数指针的指针) -> (*(_vfptr**)&b) -> _vfptr*print_virtual_function((*(_vfptr**)&b));print_virtual_function((*(_vfptr**)&d));return 0;
}

我们可以发现Func4()这个函数是在Derive这个子类的虚函数表中的,只不过这个监视窗口进行了修饰而已,其实是有的。可以对打印窗口优化:

typedef void(*_vfptr)(); //void(*_vfptr)() --> 函数指针 --> 起别名为_vfptrvoid print_virtual_function(_vfptr table[]) //打印虚函数表 table -> 数组名 -> _vfptr*(类型)
{for (int i = 0; table[i]; ++i) //虚函数表是以nullptr为结束标记的{printf("[%d]:%p->", i, table[i]);_vfptr f = table[i]; //拿到指针f(); //调用类中的虚函数}cout << endl;
}

4.5 知识补充

虚函数表补充:

  • 虚表是在编译阶段生成的,因为编译就有函数的地址了
  • 对象中的虚表指针是在构造函数的初始化列表中初始化的
  • 虚表一般存储在常量区(代码段),比如visual studio 2019,有的平台中可能存储在静态区(数据段)

4.6 多继承中的虚函数表

上面讲解的时单继承中的虚表,总结一下就是子类中的虚函数对父类中对应的虚函数进行重写(覆盖)

单继承中新增虚函数

  • 父类新增虚函数:父类的虚表中会新增,同时子类会继承到自己的虚表中
  • 子类新增虚函数:只有子类的虚表中会新增,父类看不到也无法调用

多继承也就会出现多个虚函数重写的情况,那么父类是如何处理不同虚表中的相同虚函数的重写呢?

#include <iostream>
using namespace std;class Base1
{
public:virtual void func1() { cout << "Base1::func1()" << endl; }virtual void func2() { cout << "Base1::func2()" << endl; }
};class Base2
{
public:virtual void func1() { cout << "Base2::func1()" << endl; }virtual void func2() { cout << "Base2::func2()" << endl; }
};class Derive : public Base1, public Base2
{
public:virtual void func1() { cout << "Derive::func1()" << endl; }virtual void func3() { cout << "Derive::func3()" << endl; }	//子类新增虚函数
};int main()
{Derive d;return 0;
}

可以看到此时子类拥有两张虚表

那么此时就出现了多继承中的两个主要问题了

  1. 子类中新增的虚函数func3位于哪个虚表中?
  2. 为什么重写的同一个 func1 函数,在两张虚表中的地址不相同?

首先来回答第一个问题,子类中新增的虚函数默认添加到第一张虚表中,可以通过打印虚表来验证

  • 利用直接取地址再类型强转,来打印第一张虚表;在第一张虚表的起始地址处,跳过第一张虚表的大小,来获取第二张虚表的起始地址
typedef void(*VF_T)();typedef void(*VF_T)();void PrintVFTable(VF_T table[])
{//虚函数表是以nullptr为结束标记int i = 0;while (table[i]){printf("[%d]:%p->", i, table[i]);VF_T f = table[i];f();	//调用函数,相当于func()i++;}cout << endl;
}int main()
{Derive d;PrintVFTable(*(VF_T**)&d);	//打印第一张虚表PrintVFTable(*(VF_T**)((char*)&d + sizeof(Base1)));	//跳过第一张虚表,打印第二张虚表return 0;
}

运行代码可以看出,子类中新增的虚函数func3位于第一张虚表中

也可以通过切片的方式,天然的取到第二张虚表的地址

Base2* table2 = &d;	//切片
PrintVFTable(*(VF_T**)table2);	//打印第二张虚表

再来看看第二个问题的原因,这里会存在多继承中的虚函数冗余调用问题,编译器在调用时,根据不同的地址寻找到同一函数,来解决这一问题

通过反汇编来看看

根据汇编可以看的出来调用p1->func1()p2->func1()最终都是调用Derive::func1()。需要注意的是p2->func1()函数中第一次jmp跳转到sub指令,这里的ecx寄存器就是存的this指针,通过减少4个字节找到Base1类的

这个过程叫做this指针修正,用于解决冗余虚函数的调用问题,这里会修正后继承的父类

这也就得到了第二个问题的答案

  • 两张虚表中同一个函数的地址不同,是因为调用方式不同,后继承类中的虚表需要通过this指针修正的方式调用虚函数,根据不同的地址寻找到同一函数

感谢大佬北 海提供的好图!

4.7 动态绑定与静态绑定

静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载

动态绑定

  • 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

静态绑定就像函数重载,在编译阶段就确定了不同函数的调用;而动态绑定是虚函数的调用过程,需要 虚表指针+虚表,在程序运行时,根据不同的对象调用不同的函数

4.8 多继承例题

class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };int main() {Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

关于多继承中指针偏移问题,下面说法正确的是什么?

  • A:p1 == p2 == p3
  • B:p1 < p2 < p3
  • C:p1 == p3 != p2
  • D:p1 != p2 != p3

由多继承虚函数表的相关知识可以的出,选择C,Base1和Base2是Derive父类,这里Base1先声明,先继承Base1


C++ 多态到这里就介绍结束了,本篇文章对你由帮助的话,期待大佬们的三连,你们的支持是我最大的动力!

文章有写的不足或是错误的地方,欢迎评论或私信指出,我会在第一时间改正

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

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

相关文章

Selenium中WebDriver最新Chrome驱动安装教程

&#x1f60f;作者简介&#xff1a;博主是一位测试管理者&#xff0c;同时也是一名对外企业兼职讲师。 &#x1f4e1;主页地址&#xff1a;【Austin_zhai】 &#x1f646;目的与景愿&#xff1a;旨在于能帮助更多的测试行业人员提升软硬技能&#xff0c;分享行业相关最新信息。…

【广州华锐互动】智能家居设计3D虚拟还原系统

随着科技的飞速发展&#xff0c;人们对家居生活的需求也在不断提高。智能家居作为一种新兴的生活方式&#xff0c;正逐渐成为现代人追求的理想居住环境。而智能家居设计3D虚拟还原系统&#xff0c;正是为了让人们更好地了解和体验智能家居带来的便捷与舒适&#xff0c;让未来生…

聚观早报 |2024年春节连休8天;RTE2023开幕

【聚观365】10月26日消息 2024年春节连休8天 RTE2023开幕 一加12首发“东方屏” 微软公布2024财年第一财季财报 Alphabet Q3业绩好于预期 2024年春节连休8天 国务院办公厅发布关于2024年部分节假日安排的通知。2024年春节&#xff0c;2月10日至17日放假调休&#xff0c;共…

面向边缘场景的 PWA 实践

背景 随着5G技术的发展&#xff0c;物联网边缘侧主要应用于数据传输量大、安全要求高以及数据实时处理等行业与应用场景中。其中&#xff0c;边缘计算是一种分布式计算模式&#xff0c;其将计算资源和数据处理能力推向接近数据源的边缘设备&#xff0c;以减少延迟并提高响应速度…

信息系统架构的设计理论与实践

信息系统架构的设计理论与实践 信息系统架构概述 信息系统架构的定义和发展 信息系统架构的定义 骚戴理解&#xff1a;这里只要背定义即可 信息系统架构的发展 信息系统架构的分类&#xff08;集中式和分布式&#xff09; 集中式结构 分布式结构 信息系统常用的四种架构模型…

番外8.2---配置/管理硬盘

""" Step1&#xff1a;清楚磁盘、硬盘&#xff08;HDD&#xff09;、光驱的概念及是否具有包含关系。 Step2&#xff1a;硬件设备&#xff08;IDE、SCSI、SATA、NVMe、软驱等&#xff09;命名方式及在linux系统里对应的文件名称。 Step3&#xff1a;&#xff1…

2023-10学习笔记

1.sql注入 不管是上一篇博客&#xff0c;通过java代码执行sql 还是我们常用的Mybatis的#{}和${} 都会提到sql注入的问题 1.1啥是sql注入 应该知道是说传入无关的参数&#xff0c;比如本来是想要一个where条件查询参数 但是你拼了一个drop 比如 原来的sql select * from…

正点原子嵌入式linux驱动开发——RGB转HDMI

目前大多数的显示器都提供了HDMI接口&#xff0c;HDMI的应用范围也越来越广&#xff0c;但是STM32MP157这颗芯片原生并不支持HDMI显示。可以通过RGB转HDMI芯片将RGB信号转为HDMI信号&#xff0c;这样就可以连接HDMI显示器了。本章就来学习一下如何在正点原子的STM32MP1开发板上…

《从零开始大模型开发与微调 :基于PyTorch与ChatGLM》简介

内 容 简 介 大模型是深度学习自然语言处理皇冠上的一颗明珠&#xff0c;也是当前AI和NLP研究与产业中最重要的方向之一。本书使用PyTorch 2.0作为学习大模型的基本框架&#xff0c;以ChatGLM为例详细讲解大模型的基本理论、算法、程序实现、应用实战以及微调技术&#xff0c;…

Qt中的枚举变量,Q_ENUM,Q_FLAG以及Qt中自定义结构体、枚举型做信号参数传递

Qt中的枚举变量,Q_ENUM,Q_FLAG,Q_NAMESPACE,Q_ENUM_NS,Q_FLAG_NS以及其他 理论基础&#xff1a;一、Q_ENUM二、QMetaEnum三、Q_FLAG四、示例 Chapter1 Qt中的枚举变量,Q_ENUM,Q_FLAG,Q_NAMESPACE,Q_ENUM_NS,Q_FLAG_NS以及其他前言Q_ENUM的使用Q_FLAG的引入解决什么问题&#xf…

Pytorch指定数据加载器使用子进程

torch.utils.data.DataLoader(train_dataset, batch_sizebatch_size, shuffleTrue,num_workers4, pin_memoryTrue) num_workers 参数是 DataLoader 类的一个参数&#xff0c;它指定了数据加载器使用的子进程数量。通过增加 num_workers 的数量&#xff0c;可以并行地读取和预处…

如何将音频与视频分离

您一定经历过这样的情况&#xff1a;当你非常喜欢视频中的背景音乐时&#xff0c;希望将音频从视频中分离出来&#xff0c;以便你可以在音乐播放器中收听音乐。有没有一种有效的方法可以帮助您快速从视频中提取音频呢&#xff1f;当然是有的啦&#xff0c;在下面的文章中&#…

根据输入类型来选择函数不同的实现方法functools.singledispatch

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 根据输入类型来选择函数不同的实现方法 functools.singledispatch 输入6后&#xff0c;下列输出正确的是&#xff1f; from functools import singledispatch singledispatch def calcu…

树莓派系统文件解析

title: “树莓派系统文件分析” date: 2023-10-25 permalink: /posts/2023/10/blog-post-5/ tags: 树莓派 本篇blog来分析和总结下树莓派系统文件以及他们的作用。使用的系统是Raspberry Pi OS with desktop System: 64-bitKernel version: 6.1Debian version: 12 (bookworm)…

经典链表试题(二)

文章目录 一、移除链表元素1、题目介绍2、思路讲解3、代码实现 二、反转链表1、题目介绍2、思路讲解3、代码实现 三、相交链表1、题目介绍2、思路讲解3、代码实现 四、链表的中间结点1、题目介绍2、思路讲解3、代码实现 五、设计循环队列1、题目介绍2、思路讲解3、代码实现 六、…

2023高频前端面试题-http

1. HTTP有哪些⽅法&#xff1f; HTTP 1.0 标准中&#xff0c;定义了3种请求⽅法&#xff1a;GET、POST、HEAD HTTP 1.1 标准中&#xff0c;新增了请求⽅法&#xff1a;PUT、PATCH、DELETE、OPTIONS、TRACE、CONNECT 2. 各个HTTP方法的具体作用是什么&#xff1f; 方法功能G…

『C语言进阶』动态内存管理

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f516;系列专栏&#xff1a; C语言、Linux、Cpolar ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 文章目录 前言一、动态内存函数的介绍1.1 malloc和free函数1.2 calloc函数1.3 realloc函数 二、常见的动态内存错误2.1 …

行业模型应该如何去拆解?

行业模型应该如何去拆解&#xff1f; 拆解行业模型是一个复杂的过程&#xff0c;涉及对整个行业的深入分析和理解。下面是一些步骤和方法&#xff0c;可以帮助你系统地拆解行业模型&#xff1a; 1. 确定行业范围 定义行业&#xff1a;明确你要分析的行业是什么&#xff0c;包括…

React中的Virtual DOM(看这一篇就够了)

文章目录 前言了解Virtual DOMreact创建虚拟dom的方式React Element虚拟dom的流程虚拟dom和真实dom的对比后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;react合集 &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌…

在pycharm中创建python模板文件

File——>Setting——>File and Code Templates——>Python Scripts 在文本框中输入模板内容