C++进阶 | [2] 多态

news2024/12/23 14:39:54

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


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; 参数列表为(); 的函数指针为 VFUNC

void 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 的vft
	std::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.coloradmin.cn/o/1643800.html

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

相关文章

Redis(Jedis和SpringBoot整合Redis)

文章目录 1.Jedis1.介绍2.环境配置1.创建maven项目2.pom.xml引入依赖3.新建一个包并创建一个文件 3.Jedis远程连接到Redis1.Redis放到服务器可以连接的前提条件2.为Redis设置密码1.编辑配置文件2.找到 requirepass3.设置密码为root4.重启Redis&#xff0c;在shutdown的时候报错…

C语言写一个终端进度条

C语言写一个终端进度条 这个功能挺简单的&#xff0c;主要有以下两点&#xff1a; 如何获取终端宽度如何让字符在原地闪烁 如何获取终端宽度 这里用到了设备控制接口函数ioctl()&#xff0c;下面简单的介绍一下这个函数的用法&#xff1a; ioctl是一个在Unix和类Unix系统中…

[C语言]指针进阶详解

指针是C语言的精髓所以内容可能会比较多&#xff0c;需要我们认真学习 目录 1、字符指针 2、指针数组 3、数组指针 3.1数组指针的定义 3.2&数组名vs数组名 3.3数组指针的使用 4、数组传参和指针传参 4.1一维数组传参 4.2二维数组传参 4.3一级指针传参 4.4二级指…

如何使用SSH密钥克隆仓库

1.创建SSH Key 在用户目录下查看有没有.ssh目录。如果有且该.ssh目录下有id_rsa&#xff08;私钥&#xff09;&#xff0c;和id_rse_pub(公钥)这俩文件&#xff0c;那么这一步就可以跳过。否则使用以下指令创建SSH Key ssh-keygen -t rsa -C "xxxqq.com" "xx…

【C语言】详解预处理

、 最好的时光&#xff0c;在路上;最好的生活&#xff0c;在别处。独自上路去看看这个世界&#xff0c;你终将与最好的自己相遇。&#x1f493;&#x1f493;&#x1f493; 目录 •✨说在前面 &#x1f34b;预定义符号 &#x1f34b; #define • &#x1f330;1.#define定义常…

解决HTTP 403 Forbidden错误:禁止访问目录索引问题的解决方法

解决HTTP 403 Forbidden错误&#xff1a;禁止访问目录索引问题的解决方法 过去有人曾对我说&#xff0c;“一个人爱上小溪&#xff0c;是因为没有见过大海。”而如今我终于可以说&#xff0c;“我已见过银河&#xff0c;但我仍只爱你一颗星。” 在Web开发和服务器管理中&#x…

3-qt综合实例-贪吃蛇的游戏程序

引言&#xff1a; 如题&#xff0c;本次实践课程主要讲解贪吃蛇游戏程序。 qt贪吃蛇项目内容&#xff1a; 一、功能需求 二、界面设计 各组件使用&#xff1a; 对象名 类 说明 Widget QWidge 主窗体 btnRank QPushButton 排行榜-按钮 groupBox QGroupBox 难…

C/C++开发,opencv-ml库学习,ml模块代码实现研究

目录 一、opencv-ml模块 1.1 ml简介 1.2 StatModel基类及通用函数 1.3 ml模块各算法基本应用 二、ml模块的实现原理 2.1 cv::ml::StatModel的train函数实现原理 2.2 cv::ml::StatModel的predict函数实现原理 2.3 cv::ml::StatModel的save函数和load函数 一、opencv-ml模…

Nginx(搭建高可用集群)

文章目录 1.基本介绍1.在微服务架构中的位置2.配置前提3.主从模式架构图 2.启动主Nginx和两个Tomcat1.启动linux的tomcat2.启动win的tomcat3.启动主Nginx&#xff0c;进入安装目录 ./sbin/nginx -c nginx.conf4.windows访问 http://look.sunxiansheng.cn:7777/search/cal.jsp 3…

力扣 647. 回文子串

题目来源&#xff1a;https://leetcode.cn/problems/palindromic-substrings/description/ C题解1&#xff1a;暴力解法。不断地移动窗口&#xff0c;判断是不是回文串。 class Solution { public:int countSubstrings(string s) {int len s.size();int res 0;for(int i 0;…

【机器学习-21】集成学习---Bagging之随机森林(RF)

【机器学习】集成学习---Bagging之随机森林&#xff08;RF&#xff09; 一、引言1. 简要介绍集成学习的概念及其在机器学习领域的重要性。2. 引出随机森林作为Bagging算法的一个典型应用。 二、随机森林原理1. Bagging算法的基本思想2. 随机森林的构造3. 随机森林的工作机制 三…

Samsung三星NP930XCJ-K01CN笔记本原厂Win10系统安装包下载

三星SAMSUNG笔记本电脑原装出厂Windows10预装OEM系统&#xff0c;恢复开箱状态自带系统 链接&#xff1a;https://pan.baidu.com/s/1Y3576Tsp8MtDxIpJGDucbA?pwdt0ox 提取码&#xff1a;t0ox 三星原装W10系统自带声卡,网卡,显卡,指纹,蓝牙等所有驱动、三星出厂主题专用壁纸…

vivado 在硬件中调试串行 I/O 设计-属性窗口

只要在“硬件 (Hardware) ”窗口中选中 GT 或 COMMON 块、在“链接 (Link) ”窗口中选中链接 &#xff0c; 或者在“扫描 (Scan)”窗口中选中扫描 &#xff0c; 那么就会在“ Properties ”窗口中显示该对象的属性。对于 GT 和 COMMON &#xff0c; 包括这些对象的所有属性、…

未雨绸缪:25岁Python程序员如何规划职业生涯,避免35岁职业危机?

一、程序员如何避免中年危机&#xff1f; 为了避免在35岁时被淘汰&#xff0c;程序员在25岁时可以采取一系列策略来规划自己的职业发展和提升技能。以下是我给大家整理的一些建议&#xff1a; 1. 持续学习 科技行业更新换代迅速&#xff0c;程序员需要保持对新技术和工具的敏…

揭秘大模型应用如何成为当红顶流?

Kimi广告神话背后的关键词战略 如果你生活在中国&#xff0c;你可能不认识ChatGPT&#xff0c;但你一定知道Kimi。无论是学生党还是打工人&#xff0c;都无法避开Kimi的广告。 刘同学在B站上搜教学视频时&#xff0c;弹出了一则软广&#xff0c;上面写着&#xff1a;“作业有…

SQL 基础 | BETWEEN 的常见用法

在SQL中&#xff0c;BETWEEN是一个操作符&#xff0c;用于选取介于两个值之间的数据。 它包含这两个边界值。BETWEEN操作符常用于WHERE子句中&#xff0c;以便选取某个范围内的值。 以下是BETWEEN的一些常见用法&#xff1a; 选取介于两个值之间的值&#xff1a; 使用 BETWEEN来…

批处理优化

1.4、总结 Key的最佳实践 固定格式&#xff1a;[业务名]:[数据名]:[id]足够简短&#xff1a;不超过44字节不包含特殊字符 Value的最佳实践&#xff1a; 合理的拆分数据&#xff0c;拒绝BigKey选择合适数据结构Hash结构的entry数量不要超过1000设置合理的超时时间 2、批处理优…

​【收录 Hello 算法】第 3 章 数据结构

第 3 章 数据结构 Abstract 数据结构如同一副稳固而多样的框架。 它为数据的有序组织提供了蓝图&#xff0c;算法得以在此基础上生动起来。 本章内容 3.1 数据结构分类3.2 基本数据类型3.3 数字编码 *3.4 字符编码 *3.5 小结

课时115:sed命令_进阶实践_高阶用法2

2.2.4 高阶用法2 学习目标 这一节&#xff0c;我们从 暂存实践、其他实践、小结 三个方面来学习。 暂存实践 简介 我们可以在缓存空间和暂存空间中进行数据的简单读取&#xff0c;还可以对数据进行一些复杂性的编辑操作常见的高阶命令P 打印模式空间开端至\n内容&#xff0…

Unable to find assembler. Install ‘yasm‘ or ‘nasm.‘ To build without

Unable to find assembler. Install yasm or nasm. To build without 一、概述二、解决办法 一、概述 系统&#xff1a;Ubuntu 22.04 在编译一个项目的时候提示我汇编器有一个问题&#xff0c;一个ysam 或者 nasm未安装 二、解决办法 sudo apt install yasm