【C++】多态 -- 详解

news2024/11/20 20:41:01

⚪前言

声明一下,下面的代码和解释都是在 VS2019 下的 X86 程序中进行的,涉及的指针都是 4 bytes。如果要其他平台下,部分代码需要改动。比如:如果是 X64 程序,则需要考虑指针是 8 bytes 问题等等。其它编译环境下,可能会有一些些底层实现的小细节不一样。

一、多态的概念

1、概念

通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态
举个例子,买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
再比如,最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包 - 支付 - 给奖励金的活动。那么为什么有人扫的红包很大,而有人扫的却都是1毛,5毛....,其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户,比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码的金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没有钱,那么平台就认为不太需要鼓励你去使用支付宝,那么就你扫码的金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。

二、多态的定义及实现

1、多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person。Person 对象买票全价,Student 对象买票半价。
那么在继承中要构成多态还有两个条件
  1. 必须通过基类的指针或者引用调用虚函数。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写


2、虚函数

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

class Person {
public:
    virtual void BuyTicket()
    {
        cout << "买票-全价" << endl;
    }
};

上述的代码中,成员函数 BuyTicket() 即为虚函数。 


3、虚函数的重写

虚函数的重写 (覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的 返回值类型、函数名字、参数列表完全相同,称子类的虚函数重写了基类的虚函数
class Person {
public:
    virtual void BuyTicket()
    {
        cout << "买票-全价" << endl;
    }
};

class Student : public Person {
public:
    virtual void BuyTicket()
    {
        cout << "买票-半价" << endl;
    }

    //void BuyTicket() { cout << "买票-半价" << endl}
};

void Func(Person& p)
{
    p.BuyTicket();
}

int main()
{
    Person ps;
    Func(ps);

    Student st;  
    Func(st);

    return 0;
}

注意在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

虚函数重写的两个例外:

1、协变(基类与派生类虚函数返回值类型不同)

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

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;
    }
};
2、析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成 destructor。
class Person {
public:
	 ~Person()
    {
        cout << "~Person()" << endl;
    }
};

class Student : public Person {
public:
	 ~Student()
    {
        cout << "~Student()" << endl;
    }
};
 
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;
 
	delete p1;
	delete p2;

	return 0;
}

 

结果好像跟我们想的不太一样,那为什么会出现上面的结果呢?

别忘记了,子类的指针赋给父类指针时,会发生切割。p2 指针只会指向属于父类的那一部分。所以调用的是父类的析构函数。 并不能正确的释放掉动态开辟的空间。 

下面是修改后的代码: 

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;
}

只有派生类 Student 的析构函数重写了 Person 的析构函数,下面的 delete 对象调用析构函
数,才能构成多态,才能保证 p1 和 p2 指向的对象正确的调用析构函数。


4、C++11 override 和 final

从上面可以看出,C++ 对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来 debug 会得不偿失,因此:C++11 提供了 override final 两个关键字,可以帮助用户检测是否重写
(1)final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
    virtual void Drive() final
    {}
};

class Benz :public Car
{
public:
    virtual void Drive()
    {
        cout << "Benz-舒适" << endl;
    }
};
(2)override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
class Car
{
public:
    virtual void Drive()
    {}
};

class Benz :public Car 
{
public:
    virtual void Drive() override
    {
        cout << "Benz-舒适" << endl;
    }
};

5、重载、覆盖(重写)、隐藏(重定义)的对比


三、抽象类

1、概念

在虚函数的后面写上 = 0 ,则这个函数为 纯虚函数 包含纯虚函数的类叫做 抽象类 (也叫接口 类),抽象类不能实例化出对象派生类继承后也不能实例化出对象, 只有重写纯虚函数,派生类才能实例化出对象
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
// 基类 - 抽象类 - 不能实例化出对象
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;
}

2、接口继承和实现继承

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

可以这样理解:派生类中重写基类的虚函数可以不加 virtual 关键字,因为基类的虚函数的接口(声明)被继承下来了,在派生类依旧保持虚函数属性。


四、多态的原理

1、虚函数表

// sizeof(Base)是多少?
class Base
{
public:
    virtual void Func1()
    {
        cout << "Func1()" << endl;
    }
private:
    int _b = 1;
};

int main()
{
	cout << sizeof(Base) << endl;

	return 0;
}
        通过观察测试我们发现 b 对象是 8 bytes,除了 _b 成员,还多一个 _vfptr 放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做 虚函数表指针 (v 代 表 virtual,f 代表 function)
        一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。那么 派生类中这个表放了些什么呢? 请看下面的详细分析。

针对上面的代码我们做出以下改造:

  1. 我们增加一个派生类 Derive 去继承 Base。
  2. Derive 中重写 Func1。
  3. Base 再增加一个虚函数 Func2 和一个普通函数 Func3。
// 基类
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;
    }
private:
    int _d = 2;
};

int main()
{
    Base b;
    Derive d;

    return 0;
}

Base 类对象模型:

Base 类对象和 Derive 类对象内存模型: 

通过观察和测试,我们发现了以下几点问题:

【总结】 

1、虚函数表创建的时机是在编译期间。

编译期间编译器就为每个有虚函数的类确定好了对应的虚函数表里的内容。

2、虚函数表指针 _vfptr 创建的时机:在构造函数中的初始化列表位置,这里才是真正的初始化,这个时候才会生成虚表指针,并把虚函数表的首地址赋给虚表指针。

  • _vfptr 跟着对象走,所以对象什么时候创建出来,_vfptr 就什么时候创建出来,也就是运行的时候。
  • 程序在编译期间,编译器会为构造函数中增加为 _vfptr 赋值的代码(这个是编译器的行为)。
  • 当程序在运行时,遇到创建对象的代码,执行对象的构造函数,那么在这个构造函数里有为这个对象的 _vfptr 赋值的语句。

所以在程序运行时,编译器会把虚函数表的首地址赋值给虚函数表指针,那么这个虚函数表指针就有值了。

3、派生类对象 d 中也有一个虚表指针 _vfptr,d 对象由两部分构成,一部分是基类继承下来的成员,虚表指针也就是存在这部分的,另一部分是自己的成员。

4、基类 b 对象和派生类 d 对象虚表是不一样的,这里我们发现基类虚函数 Func1 在派生类中完成了重写,所以 d 的虚表中存的是重写的 Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。

所以,派生类对象 d 的虚表中本来该放的是基类虚函数的地址,但是因为派生类重写了基类的虚函数,所以基类虚函数的地址就被覆盖变成了派生类虚函数的地址,本意是调用基类的虚函数,结果却调到了派生类的虚函数,这就实现了多态。

5、另外 Func2 继承下来后是虚函数,所以放进了虚表;Func3 也继承下来了,但它不是虚函数,所以不会放进虚表。

6、虚函数表本质是一个存放虚函数指针的指针数组,一般情况这个数组最后面放了一个 nullptr

7、总结一下派生类的虚表生成:

a. 先将基类中的虚表内容拷贝一份到派生类虚表中。

b. 如果派生类重写了基类中某个虚函数,用派生类自己重写的虚函数覆盖虚表中基类的虚函数。

c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

8、基类和派生类,无论是否完成了虚函数的重写,都有各自独立的虚表。

9、一个类的所有对象共享同一张虚表。(就像一个类的所有对象共享成员函数一样)

10、这里还有一个很容易混淆的问题:虚函数存在哪里?虚表存在哪里?

答:虚函数存在虚表,虚表存在对象中。(错误答案❌)

注意上面的回答是错的。但是很多人都是这样深以为然的。

注意虚表存的是虚函数指针,不是虚函数虚函数和普通函数一样的,都是存在代码段的,只是它的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

那么虚表存在哪里呢?实际我们去验证一下会发现 VS 下是存在代码段的,Linux G++下大家可以自己去验证一下。

VS 下进行验证:

class Base
{
public:
	virtual void func1()
    {
        cout << "Base::func1" << endl;
    }
private:
	int a;
};

int main()
{
	Base b;

	int a1 = 0; // 栈帧
	int* p1 = new int; // 堆区
	const char* p2 = "hello"; // 常量区
	auto pf = Test; // 函数地址
	static int a2 = 1; // 静态区

	printf("栈帧        :0x%p\n", &a1);
	printf("堆区        :0x%p\n", p1);
	printf("常量区      :0x%p\n", p2);
	printf("函数地址    :0x%p\n", pf);
	printf("静态区      :0x%p\n", &a2);
	printf("虚函数表地址:0x%p\n", *((int*)&b));

	return 0;
}


2、多态的原理

多态的原理到底是什么?还记得这里 Func 函数传 Person 调用的 Person::BuyTicket,传 Student 调用的是 Student::BuyTicket。

 

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 Mike;
    Func(Mike);

    Student Johnson;
    Func(Johnson);

    return 0;
}
  1. 观察下图的红色箭头我们看到,p 是指向 mike 对象时,p->BuyTicket 在 mike 的虚表中找到虚函数是 Person::BuyTicket。
  2. 观察下图的蓝色箭头我们看到,p 是指向 johnson 对象时,p->BuyTicket 在 johson 的虚表中找到虚函数是 Student::BuyTicket。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

  • 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。反思一下为什么?
(1)基类对象的指针 / 引用调用虚函数的原理是什么?

不管基类指针 / 引用指向的是基类还是派生类,执行这段代码 p->BuyTicket() 的指令是一模一样的,都是先找到虚表指针(对象中的头 4 个字节),通过虚表指针找到虚表取对应虚函数的地址并调用该虚函数

  • 再通过下面的汇编代码分析,可以看出满足多态以后的函数调用,不是在编译时确定的,而是运行起来以后到对象的中去找的。不满足多态的函数调用是编译时确认好的
class Person {
public:
	virtual void BuyTicket()
    {
        cout << "买票-全价" << endl;
    }
};

void Func(Person* p)
{
    //...
    p->BuyTicket();

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

int main()
{
    Person Mike;
    Func(&Mike);

    return 0;
}
(2)为什么多态必须要用基类的指针 / 引用来调用虚函数,而用基类对象调用却不行? 

派生类对象赋值给基类对象,不会拷贝派生类的虚表指针,只会拷贝对象中的数据成员过去。

不妨这样来理解:一个类的所有对象共享同一张虚表,就像一个类的所有对象共享成员函数一样,只能供这个类自己的对象使用,所以派生类对象是不可能把虚表拷贝过去的,不然就违背同一个类共享的规则了。

那么既然不会把派生类的虚表指针拷贝过去,那基类对象自然就不能调用到派生类的虚函数了。

int main()
{
    //...
// 首先BuyTicket虽然是虚函数,但是Mike是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call地址
    Person Mike;
    Mike.BuyTicket();

00195182  lea         ecx,[mike]
00195185  call        Person::BuyTicket (01914F6h) 
 
    //... 
}

下面是上面继承关系中的 Person 类对象 Mike 和 Student 类对象 Johnson 模型:

解释了用基类引用 / 指针引用不同对象去完成同一行为时,如何展现出不同的形态。


3、动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为也称为编译时多态性和静态多态,比如:函数重载、内联函数、函数模板。
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为运行时多态性和动态多态,比如:虚函数。
  3. 前面买票的汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。

五、单继承和多继承关系的虚函数表

需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类的虚表模型前面我们已经看过了,没什么需要特别研究的。

1、 单继承中的虚函数表

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

class Derive :public Base { 
public :
    virtual void func1()
    {
        cout << "Derive::func1" << endl;
    }
    virtual void func3()
    {
        cout << "Derive::func3" << endl;
    }
    virtual void func4()
    {
        cout << "Derive::func4" << endl;
    }
private :
    int b;
};
观察下图中的监视窗口中我们发现看不见 func3 和 func4
这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小 bug。
那么我们如何查看 d 的虚表呢?下面我们使用代码打印出虚表中的函数

// 函数指针VFPTR
typedef void(*VFPTR) ();

// 打印虚表,传入虚函数指针数组
void PrintVTable(VFPTR vTable[])
{
    // 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        // 依次打印虚表各元素
        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
        // 把虚表各元素由void*强转为函数指针类型后,赋值给函数指针f
        VFPTR f = vTable[i];
        // 调用函数
        f();
    }
    cout << endl;
}

int main()
{
    Base b;
    Derive d;

    // 思路:取出b、d对象的头4字节,就是虚表的指针,前面我们说到虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
    // 1、先取b的地址,强转成一个int*的指针
    // 2、再解引用取值,就取到了b对象头4字节的值,这个值就是指向虚表的指针
    // 3、再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
    // 4、虚表指针传递给PrintVTable进行打印虚表
    // 5、需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。

    VFPTR* vTableb = (VFPTR*)(*(int*)&b);
    PrintVTable(vTableb); // 打印对象b的虚表

    VFPTR* vTabled = (VFPTR*)(*(int*)&d);
    PrintVTable(vTabled); // 打印对象d的虚表

    return 0;
}


2、多继承中的虚函数表

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

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

class Derive : public Base1, public Base2 {
public:
    virtual void func1()
    {
        cout << "Derive::func1" << endl;
    }
    virtual void func3()
    {
        cout << "Derive::func3" << endl;
    }
private:
    int d1;
};

// 函数指针VFPTR
typedef void(*VFPTR) ();

// 打印虚表,传入虚函数指针数组的地址(即虚表指针)
void PrintVTable(VFPTR vTable[])
{
    cout << " 虚表地址>" << vTable << endl;
    for (int i = 0; vTable[i] != nullptr; ++i)
    {
        // 依次打印虚表各元素
        printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
        // 把虚表各元素赋值给函数指针f
        VFPTR f = vTable[i];
        // 调用函数
        f();
    }
    cout << endl;
}

int main()
{
    Derive d;

    VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
    PrintVTable(vTableb1); // 打印第一张虚表

    // 必须先强转成char*,然后加Base1大小个字节,再强转成int*,解引用,强转成VFPTR*
    VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
    PrintVTable(vTableb2); // 打印第二张虚表

    return 0;
}
(1)Base1 和 Base2 中都有虚函数 func1,那么 Derive 类中的 func1 到底是重写的哪一个基类的呢? 

两个基类 Base1 和 Base2 中的虚函数 func1 都会被重写,因为要满足多态条件。

 (2)多继承体系,Derive 继承了两个基类,那么 Derive 对象中有几张虚表呢?

Derive 对象中有两张虚表。

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。

这里 Derive 对象的两张虚表中的重写的 Derive::func1 函数,虽然函数地址不一样,但是当 Base1 或 Base2 指针指向 Derive对象时,调的都是 Derive 中的 func1,是同一个函数。这其中的具体原因和编译器的设计有关。 


3、菱形继承、菱形虚拟继承

在实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表就不深入探究了,一般我们也不需要研究清楚,因为实际中很少用。
如果对此感兴趣的可以查看下面两篇博客:
C++ 虚函数表解析 | 酷 壳 - CoolShell
C++ 对象的内存布局 | 酷 壳 - CoolShell

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

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

相关文章

TCP/IP(十八)TCP 实战抓包分析(二)TCP 三次握手和四次挥手

一 TCP三次握手和四次挥手 说明&#xff1a; 本文三次握手和四次挥手 无异常情况下的分析目标&#xff1a; 通过抓取和分析 HTTP 协议网络包,理解 TCP 三次握手和四次挥手的工作原理 ① 抓包和测试准备 1、 服务端事先执行 tcpdump 抓包 --> 172.25.2.100tcpdump -i b…

VMware 虚拟机里连不上网的解决方案

解决办法一&#xff1a; 虚拟机设置里&#xff0c;找到“网络适配器”,右边的网络连接选择“NAT 模式”&#xff0c;如果不行的话再 linux系统。还是不行的话接着看第二种解决方案。 解决方法二&#xff1a; 可能原因是VMware NET Service服务没开 win R&#xff0c;输入&a…

CSS 笔记/练习

CSS 概述 与 html 配合&#xff0c;实现内容与样式分离样式美化 标签中元素作用 class&#xff1a;class属性用于为元素指定一个或多个样式类。通过为元素添加class属性&#xff0c;可以将其与CSS样式表中的样式规则关联起来&#xff0c;从而改变元素的外观和行为。一个元素可…

Confluence 解决PDF导出乱码问题

1.原因 PDF导出乱码是因为由于服务器缺少必要字体 2.解决办法 下载字体文件将字体文件重命名为simhei.ttf Confluence→管理→PDF导出语言支持&#xff0c;导入字体即可

sqlalchemy更新json 字段的部分字段

需求描述&#xff1a; 我们有个json字段&#xff0c;存储的数据形如下&#xff0c;现在需要修改love {"dob":"21","subject":{"love":"programming"}}工程结构 main.py from sqlalchemy import Column, String, Integer,c…

微信公众号如何变更为订阅号?

公众号迁移有什么作用&#xff1f;只能变更主体吗&#xff1f;大家都知道&#xff0c;微信公众号是不支持直接变更主体的&#xff1b;但是很多情况下&#xff0c;我们又不得不进行账号主体的更换&#xff1b;这时候&#xff0c;我么就可以通过账号迁移功能&#xff0c;将A公众号…

图像抓取代码示例

以下是一个使用luasocket-http库的Lua下载器程序&#xff0c;用于下载图像。此程序使用了https://www.duoip.cn/get_proxy的代码。 -- 引入luasocket和luasocket-http库 local socket require("socket") local http require("http") ​ -- 获取代理服务…

Java入门GUI第一章

Java入门GUI第一章 一.简介 GUI的核心技术&#xff1a;Swing AWT 现在GUI逐渐被淘汰&#xff0c;因为&#xff1a; 界面不美观需要jre环境 为什么我们要学习&#xff1f; 可以写出自己想要的小工具工作时候可能会遇到维护Swing界面&#xff0c;概率特别小&#xff01;了…

「蓝桥·算法双周赛」第一场公开赛【待补题填坑】

三带一【算法赛】 - 蓝桥云课 (lanqiao.cn) 给定四个字符&#xff0c;判断是否其中有三个相同&#xff0c;另一个与他们不同 #include <bits/stdc.h> void solve() {std::string s;std::cin>>s;char as[0],bs[1],cs[2],ds[3];if(ab&&ac&&a!d) std:…

AdaInf: Data Drift Adaptive Scheduling 阅读笔记

论文原文链接 文章目录 摘要一、背景、动机和主要贡献背景动机主要问题研究挑战 主要贡献 二、实验分析数据漂移对准确性的影响推理的早期退出结构最优请求批处理大小GPU内存通信 三、AdaInf系统设计概览数据漂移感知再训练-推理DAG生成决定数据漂移影响生成再训练推理DAG 数据…

【LeetCode75】第七十三题 用最少数量的箭引爆气球

目录 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 代码&#xff1a; 题目&#xff1a; 示例&#xff1a; 分析&#xff1a; 本题和上一题基本一样&#xff0c;上一题是要我们尽量让区间不重叠&#xff0c;而本题是要我们尽量让区间重叠。 所以我们的做法和上一题…

3D Web轻量化工具HOOPS Web Platform助力Rapid DCS快速上市碳估算产品!

总部位于英国的Rapid DCS提供全面的交钥匙解决方案和服务&#xff0c;帮助建筑环境领域的客户充分利用数字化的优势。 Rapid DCS技术总监James Hunter表示&#xff1a;“如今的建筑项目需要一套与20甚至10年前的建筑项目不同的功能。” “例如&#xff0c;虽然成本规划一直很重…

每日汇评:随着上升趋势的恢复,黄金在1950美元上方等待破位

周三早间&#xff0c;黄金价格逼近1950美元&#xff0c;买家纷纷出手&#xff1b; 尽管市场情绪谨慎&#xff0c;但美元与美债交投疲弱&#xff0c;中国的乐观情绪逐渐消退&#xff1b; 金价重拾200日移动均线&#xff0c;但料持续升穿1950美元&#xff1b; 金价正从每盎司1943…

E056-web安全应用-File Inclusion文件包含漏洞进阶

课程名称&#xff1a; E056-web安全应用-File Inclusion文件包含漏洞进阶 课程分类&#xff1a; web安全应用 --------------------------------------------------------------------------------------------------------------------------------- 实验等级: 中级 任…

Python批量替换文件中内容,效率加倍

文章目录 我遇到的问题解决代码我遇到的问题 由于我的SQL文件太大了,手动去替换估计累死…所以需要一个批量替换。其它类似问题,同理处理。 解决代码 # 定义要替换的内容 replacements = {utf8mb4_0900_ai_ci: utf8_general_ci,utf8mb4: utf8 }# 读取文件内容 with open<…

积分球荧光光谱测试光电检测方式有哪些优点?

积分球荧光光谱测试是一种测量光通量的方法&#xff0c;其基本原理是将待测光源放入积分球中&#xff0c;在积分球内壁涂以白色漫反射层&#xff0c;光源发出的光经球壁多次反射后&#xff0c;使整个球壁上的照度均匀分布&#xff0c;再通过球壁上的孔投射到光电探测器上的光通…

Databend hash join spill 设计与实现 | Data Infra 第 16 期

本周六&#xff0c;我们将迎来最新一期的 Data Infra 直播活动&#xff0c;本次活动我们邀请到了 Databend 研发工程师-王旭东&#xff0c;与大家分享主题为《 Databend hash join spill 设计与实现 》的相关知识。 通过本次分享&#xff0c;我们能更加了解 Databend 的 hash …

[发轫之始 百尺竿头] 家多彩居家供应链001号旗舰店正式开业

发轫之始不负期待 家多彩居家供应链001号旗舰店开业盛典于2023年10月17日上午10时18分盛大开幕&#xff0c;家多彩居家供应链品牌经过3个月的沉淀发展&#xff0c;积极探索赛道壁垒、不断尝试创新模式&#xff0c;始终坚持“数字驱动”之路&#xff0c;为广大装修业主筑就一站…

智能体、多模态化大势所趋,探大模型的未来!

导语 | 今年以来&#xff0c;以 ChatGPT 为代表的生成式 AI&#xff0c;在最具挑战性的自然语言处理领域实现革命性突破&#xff0c;在行业掀起新一轮发展热潮。开源大模型正成为人工智能领域的新潮流&#xff0c;AI 大模型在未来将走向何方&#xff1f;今天&#xff0c;我们特…

众和策略:多少成交量才算放大?

成交量是股市中非常重要的指标&#xff0c;常常被用于判别商场活跃度及股票价格涨跌起伏等。可是&#xff0c;要想了解一个股票何时扩展&#xff0c;又需求考虑哪些要素呢&#xff1f;在本文中&#xff0c;我们将从多个角度进行剖析&#xff0c;以便更好地了解多少成交量才算扩…