C++进阶-->多态(Polymorphism)

1. 多态的概念

多态,顾名思义多种形态;多态分为编译时多态静态多态)和运行时多态动态多态),静态多态就是就是我们前面讲的函数重载和函数模板,可以通过传不同类型,然后在编译期间就确定好使用哪个的称为静态多态动态多态就是一般在程序运行时确定使用指定函数的就称作动态多态。

动态多态,举个生活中的例子,例如铁路12306中买票的时候,当成年且没有特殊情况的人是全价买票,学生买票有特定的学生价买票,军人买票时可以优先买票,而这个过程抽象来说就是传入不同的对象就会完成不同的行为。


2. 多态的定义及使用

2.1 多态的构成条件

多态是一个继承关系下的类对象,去调用同一函数,产生了不同行为。比如Student继承了Person。Person对象买票全价,Student对象优惠买票。

2.1.1 实现多态的两个必要条件

1. 必须是基类的指针或引用调用函数。

2. 被调用的函数必须是虚函数(virtual修饰的)。

解释一下为什么必须是基类的指针或引用调用函数,因为只有是基类的指针或引用才能做到既接收基类的地址又接收派生类的地址;

第二派生类必须对基类的虚函数进行重写/覆盖,派生类对基类的虚函数重写了才能做到传入不同的类执行不同的操作的效果,才能达到多态。f9e5263f8b84403fbc9bb0754efb1f39.png


3. 虚函数

在继承那一章我们也讲到虚继承,虚继承是用来避免菱形继承带来代码的冗余和二义性的问题,关键字是virtual;虚函数的关键字也是virtual,只需要在类成员函数前面加上virtual修饰,那么这个成员函数就被称为虚函数。注意:如果不是成员函数是不能加virtual修饰的。如下代码所示:

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

3.1 虚函数的重写

派生类中有一个虚函数跟基类的虚函数完全一致的虚函数称为虚函数的重写。

完全一致指的是:函数名、参数列表、返回值类型。(注意!这里的参数列表指的是参数的类型,名字相同没有关系);如下代码所示:

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

还有一点需要提一下的是:如果基类的虚函数加了virtual,那么派生类的虚函数可以不加virtual,这里我们可以直接按“在继承的时候就把基类的虚函数的属性继承下来了”理解,这个不太建议这样子用,但在笔试或者考试选择题可能会考。

下面是虚函数、虚函数的重写、实现多态的代码使用:

#include<iostream>
using namespace std;

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

void BuyTicket(Person* ptr)
{
	ptr->BuyTicket(6);
}

int main()
{
	Person p1;
	Student s1;
	BuyTicket(&p1);
	BuyTicket(&s1);
	return 0;
}

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:43498d0f6b634b0c906997d2785f7a99.png

最后再补充一个重重重重要的知识:虚函数的重写本质上是重写虚函数的实现!!!!下面就有一道相关的练习题。(毕竟人教人教不会,事教人一脚就懂)

3.2 多态场景下的选择题

f41ecc81bc1a48a4baec16641cf16cfd.png

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

首先如果我们对重写的知识理解的不够全面的话,那么肯定会选择D, 但我们上面说了重写是重写函数的实现,函数的参数不会有变化,所以我们调用的func应该如下图所示:9aec85ea0f714c34bea7d1a5ac267686.png

所以正确的答案是B;37a6353d609e4e4687881f12f853fab7.png

最后我们再来细致分析一下p->test()究竟怎么构成多态,我们都知道在类里面的成员函数会有一个隐藏的参数就是this指针,而test是在A里面的,所以完整的函数应该为   virtual void test(A* this)这里就构成重载了,基类做this指针的返回值,完全符合上面所说的2个多态的构成条件。35fd32c94cc64bd388e7ac5a63ce8e6e.png


3.3 虚函数重写的一些问题

3.3.1 协变

协变:派生类重写基类的虚函数的时候返回值不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用,这个就成为协变。协变用处不多,仅需了解一下即可;如下代码所示:

#include<iostream>
using namespace std;
//基类
class A
{

};
//派生类
class B :public A
{

};

class Person
{
public:
	//基类做返回值
	virtual A* BuyTicket()
	//virtual Person* BuyTicket()也可以
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};

class Student :public Person
{
public:
	//派生类做返回值
	virtual B* BuyTicket()
	//virtual Student* BuyTicket()也可以
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};

void Func(Person* ptr)
{
	ptr->BuyTicket();
}

3.3.2 析构函数的重写

基类的析构函数只要定义为虚函数后,无论派生类的析构函数写不写virtual,都会与基类的析构函数构成重写。

前面继承有提到一嘴,就是析构函数最后都会被编译器转换为destructor(),那么这时候函数名(destructor)相同构成隐藏(这个是继承那边的)。

所以只要基类的析构加了virtual,派生类的就构成了重写;函数名一样都是destructor、参数一样都没有、返回值类型都没有。

接下来就谈谈为什么要实现析构函数的重写(重点!!!!!)

在思考为什么的时候我们就可以通过反推来证明即如果没有实现会发生什么情况;我们先看一下下面的代码:

#include<iostream>
using namespace std;

class A
{
public:
	 ~A()
	{
		cout << "~A()" << endl;
	}
};

class B :public A
{
public:
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};

int main()
{
	A* p1 = new A;
	A* p2 = new B;

	delete p1;
	delete p2;
	return 0;
}

对代码的解析:我们用基类A指针实例化了两个对象p1接收基类A的地址p2接收派生类B的地址,如果说我们没有实现析构函数的重写delete p1和delete p2 是直接走基类的析构函数,因为没有构成多态,也就不会产生不同的行为而只走基类A的析构函数,如果派生类B里面有动态申请空间,那么就会出现内存泄漏。如下图:

b738ad185b9145b29fe5b984044b249e.png

但如果我们让基类A的析构函数定义为虚函数,那么就构成多态,从而会根据传入不同的类型调用不同对象的析构函数。a43413a6925144f48d65349edfdb6835.png

这里为什么会还会调用一次基类A的析构函数呢?前面继承有提到,派生类的析构函数里面其实包含了基类的析构函数,主要是为了达到先子后父的析构顺序。

最后总结一下:C++的这些实现其实都是有关联的,他们为了处理上面的这种情况而实现了析构函数的重写,为了实现析构函数的重写而实现了让析构函数无论函数名为什么最后都会转换成destructor。真的很妙。


3.3.3 override和final关键字

override是用来检查你是否重写错误;d296969260e3403eaa4fcd1eafc3b2d3.png

final在继承那里也提到过,如果想实现一个类不被继承就用final,而这里的final是用来实现一个不能被重写的虚函数;981713e9edb04bc6beba32a0d8b57046.png


3.3.4 重载/重写/隐藏的对比032749fad3564b7eb4431220e422b9c3.png


3.3.5 纯虚函数和抽象类

纯虚函数只要在虚函数后面加个“=0”即可;纯虚函数不需要定义实现,没有很大作用,但是在语法上是支持实现的 。而包含纯虚函数的类称作抽象类,抽象类不能实例化出对象,但是派生类可以继承抽象类,如果派生类不重写纯虚函数,那么派生类也称为抽象类。纯虚函数其实就是间接强制了派生类要重写纯虚函数。

5e07f16386174a2fa66529fcfdc7041f.png

干说有点难理解,举个实际例子,狗和猫都是动物,狗的叫声是“旺旺”,猫的叫声是“喵喵”,狗和猫都是动物都有动物的特征所以可以继承动物的属性,但是动物是怎么叫的我们不知道,而动物就可以称作是抽象类,“叫”这一个动作可以用纯虚函数实现,猫和狗则是动物的派生类,所以需要重写这个纯虚函数,狗重写“叫”函数为“旺旺”,猫则是“喵喵”。如下代码所示:

#include<iostream>

class Animal
{
public:
	virtual void talk() = 0;
};
class Dog : public Animal
{
public:
	virtual void talk() 
	{
		std::cout << "汪汪" << std::endl;
	}
};

class Cat : public Animal
{
public:
	virtual void talk() 
	{
		std::cout << "(>^ω^<)喵" << std::endl;
	}
};
int main()
{
	Cat cat;
	Dog dog;
	cat.talk();
	dog.talk();
	return 0;
}

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:e0ee26f05fa64138975a82706ddfab98.png


4. 多态的原理

首先我们看一道题,用一道题引出下面的知识;67df6fb7104b4e8bb40023382fc2472b.png

#include<iostream>
using namespace std;

class Base {
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};

int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

我们知道,计算类的大小是使用对齐的方式计算的,那么计算出来的结果是8,可能我们的答案就选择了C,实则并不然,答案选的是D,首先我们通过调试看一下里面究竟多了什么玩意,如下图所示:

57f345d60a264bb895208fccb07578ff.png

我们发现里面除了成员变量_b和_char,还有一个指针_vfptr,这个指针我们称作虚函数表指针(virtual function pointer);

4.1 虚函数表指针

每个含有虚函数的类中都会有一个虚函数表指针,这个指针是指向一块存着虚函数地址的空间,虚函数表也称作虚表。所以我们传入不同类去调用该类的虚函数的时候就是通过这个指针去调用不同类中的虚函数的。

4.2 多态的实现

首先我们先来看一串代码

#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() 
	{
		cout << "买票-全价" << endl;
	}
protected:
	string _name;
};

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

protected:
	int _id;
};

class Soldier : public Person 
{
public:
	virtual void BuyTicket() 
	{ 
		cout << "买票-优先" << endl;
	}

protected:
	string _codename; // 代号
};

void Func(Person* ptr)
{
	// 这里可以看到虽然都是Person指针Ptr在调用BuyTicket
	// 但是跟ptr没关系,而是由ptr指向的对象决定的。
	ptr->BuyTicket();
}

int main()
{
	// 其次多态不仅仅发生在派生类对象之间,多个派生类继承基类,重写虚函数后
	// 多态也会发生在多个派生类之间。
	Student st;
	Soldier sr;
	Person pr;
	Func(&st);
	Func(&sr);
	Func(&pr);
	return 0;
}

我们可能会好奇,Func内的ptr是如何做到传入不同的类对象的地址然后走不同的虚函数执行不同操作的,下面就来讲解一下。

如下图

c80ba045e6684b748455df3ea1d7af28.png

3d5f3d6949ee41ee8ba6fe88b1fa0d49.png

通过上图我们看到,满足多态后,不再是编译时通过调用对象确定函数的地址,而是运行时到指定的对象内找到对象的__vfptr(虚表)然后确定对应的虚函数的地址,然后执行,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类的虚函数。


4.3 动态绑定和静态绑定

动态绑定则是满足多态条件的函数调用是在运行时绑定,说简单点就是在运行时去虚函数表内找到函数地址然后调用。

静态绑定则是在编译期间确定函数的地址然后调用。

这个可以通过汇编层看一下。

动态绑定👇看着挺复杂的,挺多操作,因为他是在运行时先去找到__vptr这个指针,然后找到虚函数表,再通过虚函数表内的地址找到虚函数,再调用该函数。

47759162ddf74cc9b7f69f69453ca4ae.png

静态绑定👇编译器直接确定函数地址然后直接调用。

2c61f3db7b3143b3b816dde418ee1fc8.png


4.4 虚函数表

1. 基类对象的虚函数表中存放基类所有虚函数的地址。如果同类型实例化对象的话则虚表共用,不同类型虚表各自独立。

2. 派生类由两部分构成,一部分是派生类自己的成员,还有一部分是基类的成员,还有基类的虚表指针,继承下来的基类中有虚表指针的话就不会自己再生成一个新的虚表指针,但继承下来的和虚表指针和基类的虚表指针不是同一个,他们的地址不同。d37324b29ea1407ab366b220ec5964b7.png

3. 如果派生类中有虚函数的重写,那派生类虚表中对应的虚函数就会被重写的虚函数覆盖。

4. 派生类的虚表中包含基类的虚函数地址,派生类重写的虚函数地址,还有派生类自己的虚函数地址。

5. 虚函数的本质是一个存虚函数指针的指针数组,一般情况下这个数组后面会放一个0x00000000的标记   (这个C++并没有进⾏规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000 标记,g++系列编译不会放)e1d4a3f479be41a6b0fb55b483fb441e.png

6. 虚函数和普通函数一样,编译好后是一段指令,都是存在代码段的,只是虚函数的地址存在虚表中。

7. 虚函数表的存在位置是看编译器的,C++并没有规定,下面将来看一下vs的在哪里;如下代码所示:

#include<iostream>
using namespace std;

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
	void func5() { cout << "Base::func5" << endl; }
protected:
	int a = 1;
};

class Derive : public Base
{
public:
	// 重写基类的func1
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func1" << endl; }
	void func4() { cout << "Derive::func4" << endl; }

protected:
	int b = 2;
};

int main()
{
	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);

	Base b;
	Derive d;
	Base* p3 = &b;
	Derive* p4 = &d;
	printf("Person虚表地址:%p\n", *(int*)p3);
	printf("Student虚表地址:%p\n", *(int*)p4);
	printf("虚函数地址:%p\n", &Base::func1);
	printf("普通函数地址:%p\n", &Base::func5);
	return 0;
}

👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇  👇

运行结果为:c838968a73ae41c4b2a62c55ed66fdc4.png

由此可见,虚表的地址与常量区的地址很接近,那就说明虚表是存在常量区的。


5. 额外补充的知识

首先我们先来看一个代码。

#include<iostream>
using namespace std;

class A 
{ 
public: 
	void test(float a) 
	{
		cout << a; 
	} 
}; class B :public A 
{
public: 
	void test(int b) 
	{
		cout << b; 
	} 
}; void main() 
{
	A* a = new A;
	B* b = new B;
	a = b; 
	a->test(1.1); 
}

按照我们的思路,a赋值给b之后,那么A的test的隐式指针this就应该转换成B类的this,应该调用的是cout出b的值为1。

但是编译器不是这么做的,编译器虽然通过指针的指向访问成员变量,但是不能通过指针的指向访问成员函数,而是通过指针的类型来访问成员函数。也就是说无论a的指针指向了谁,都只能访问到A类内的成员函数。(引用也是一样的!!!!!)


派生类有几个父类,且那几个父类都有虚函数的话,那么派生类就会有几张虚表。而如果在派生类中增加虚函数的话只会放在第一张虚表的最后。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

stm32教程:keil5安装及stm32f1xx系列芯片包下载

早上好啊&#xff0c;大佬们&#xff0c;咱们这个专栏是来浅学一下stm32的内容&#xff0c;然后本篇是一个导言篇&#xff0c;主要是让大家安装好软件&#xff0c;能够正常的进入stm32的学习。 keil5安装包夸克网盘链接&#xff1a; 链接&#xff1a;https://pan.quark.cn/s/1…

保护压缩文件安全:为RAR文件添加密码的两种方法

在日常办公中&#xff0c;给RAR文件设置密码可以保护其中的敏感信息不被随意访问。想要给RAR文件设置密码&#xff0c;需要用到支持RAR格式的解压缩工具&#xff0c;比如WinRAR。本文将介绍WinRAR为RAR文件设置密码的两种常用方法&#xff0c;一起来看看吧&#xff01; 方法一…

【Java语言】类和对象

类 类是用来对一个对象进行描述的&#xff0c;主要描述这个对象哪些属性。 类需要class进行修饰&#xff0c;一个Java文件中可以存在多个类&#xff0c;但是只能存在一个public类且必须与Java文件名相同。eg&#xff1a;有一个Demo.Java文件&#xff0c;在文件中只能存在publi…

大模型系列——AlphaZero/强化学习/MCTS

AlphaGo Zero无需任何人类历史棋谱&#xff0c;仅使用深度强化学习&#xff0c;从零开始训练三天的成就已远远超过了人类数千年积累的围棋知识。 1、围棋知识 &#xff08;1&#xff09;如何简单理解围棋知识 &#xff08;2&#xff09;数子法分胜负&#xff1a;https://zhu…

CSS.导入方式

1.内部样式 在head的style里面定义如 <style>p1{color: brown;}</style> 2.内联样式 直接在标签的里面定义如 <p2 style"color: blue;">这是用了内联样式&#xff0c;蓝色</p2><br> 3.外部样式表 在css文件夹里面构建一个css文件…

LeetCode题(二分查找,C++实现)

LeetCode题&#xff08;二分查找&#xff0c;C实现&#xff09; 记录一下做题过程&#xff0c;肯定会有比我的更好的实现办法&#xff0c;这里只是一个参考&#xff0c;能帮到大家就再好不过了。 目录 LeetCode题&#xff08;二分查找&#xff0c;C实现&#xff09; 一、搜…

ComfyUI初体验

ComfyUI 我就不过多介绍了&#xff0c;安装和基础使用可以看下面大佬的视频&#xff0c;感觉自己靠图文描述的效果不一定好&#xff0c;大家看视频比较方便。 ComfyUI全球爆红&#xff0c;AI绘画进入“工作流时代”&#xff1f;做最好懂的Comfy UI入门教程&#xff1a;Stable D…

STM32G474硬件CRC7和软件CRC7校验

1、CRC7的多项式和初始值 #define CRC_Hardware_POLYNOMIAL_7B 0x09//硬件CRC多项式为0x09 //SD卡中的校验算法CRC7&#xff0c;生成多项式为x^7 x^3 1&#xff0c;由于bit7不存在&#xff0c;只有bit31和bit01&#xff0c;所以多项式为0x09#define CRC7_INIT_VALUE 0…

传输线临界长度

临界长度 临界长度是联结传输线长度与信号反射量之间的一个重要参数。如果用信号在传输线 上的时间延迟来表示传输线长度&#xff0c;临界长度在数值上可表示为 临界长度是传输线末端信号能否达到振铃的最大幅度的传输线长度临界值。传输线长度小于临界长度时&#xff0c;振铃…

微信小程序 - 动画(Animation)执行过程 / 实现过程 / 实现方式

前言 因官方文档描述不清晰,本文主要介绍微信小程序动画 实现过程 / 实现方式。 实现过程 推荐你对照 官方文档 来看本文章,这样更有利于理解。 简单来说,整个动画实现过程就三步: 创建一个动画实例 animation。调用实例的方法来描述动画。最后通过动画实例的 export 方法…

UI设计软件全景:13款工具助力创意实现

选择恰当的UI设计工具对于创建美观且用户体验良好的应用程序界面至关重要。不同的APP功能可能需要不同的界面设计软件&#xff0c;但并非所有工具都需要精通&#xff0c;熟练掌握几个常用的就足够了。以下是13款APP界面设计软件&#xff0c;它们能够为你的团队提供绘制APP界面所…

【动手学强化学习】part2-动态规划算法

阐述、总结【动手学强化学习】章节内容的学习情况&#xff0c;复现并理解代码。 文章目录 一、什么是动态规划&#xff1f;1.1概念1.2适用条件 二、算法示例2.1问题建模2.2策略迭代&#xff08;policyiteration&#xff09;算法2.2.1伪代码2.2.2完整代码2.2.3运行结果2.2.4代码…

2024年【焊工(中级)】最新解析及焊工(中级)考试总结

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 焊工&#xff08;中级&#xff09;最新解析参考答案及焊工&#xff08;中级&#xff09;考试试题解析是安全生产模拟考试一点通题库老师及焊工&#xff08;中级&#xff09;操作证已考过的学员汇总&#xff0c;相对有…

Java题集练习4

Java题集练习4 1 异常有什么用&#xff1f; 用来找到代码中产生的错误 防止运行出错2 异常在java中以什么形式存在&#xff1f; 异常在java中以类的形式存在&#xff0c;分为运行时异常和编译期异常&#xff0c;他们都在类Exception中3 异常是否可以自定义&#xff1f;如何自…

2024年【金属非金属矿山(地下矿山)安全管理人员】考试报名及金属非金属矿山(地下矿山)安全管理人员复审考试

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 金属非金属矿山&#xff08;地下矿山&#xff09;安全管理人员考试报名是安全生产模拟考试一点通生成的&#xff0c;金属非金属矿山&#xff08;地下矿山&#xff09;安全管理人员证模拟考试题库是根据金属非金属矿山…

海洋生物图像分割系统:一键训练

海洋生物图像分割系统源码&#xff06;数据集分享 [yolov8-seg-C2f-EMSCP&#xff06;yolov8-seg-dyhead等50全套改进创新点发刊_一键训练教程_Web前端展示] 1.研究背景与意义 项目参考ILSVRC ImageNet Large Scale Visual Recognition Challenge 项目来源AAAI Global Al l…

基于SpringBoot+Vue+MySQL的房屋租赁系统

系统展示 系统背景 随着城市化进程的加速和人口流动性的增加&#xff0c;房屋租赁市场逐渐成为城市生活的重要组成部分。然而&#xff0c;传统的房屋租赁方式存在诸多问题&#xff0c;如信息不对称、交易成本高、租赁关系不稳定等&#xff0c;这些问题严重影响了租赁市场的健康…

第三届“基于模型的系统工程及数字工程大会”盛况回顾,同元软控发表精彩演讲

2024年10月27日&#xff0c;第三届“基于模型的系统工程及数字工程大会”&#xff08;MBSE&DE 2024&#xff09;在合肥召开。本届大会是中国系统工程学会第23届学术年会重点分会场论坛之一&#xff0c;由中国系统工程学会科技系统工程专业委员会联合中国图学学会数字化设计…

云原生笔记

#1024程序员节|征文# 单页应用(Single-Page Application&#xff0c;SPA) 云原生基础 云原生全景内容宽泛&#xff0c;以至于刚开始就极具挑战性。 云原生应用是高度分布式系统&#xff0c;它们存在于云中&#xff0c;并且能够对变化保持韧性。系统是由多个服务组成的&#…

在 AMD GPU 上构建解码器 Transformer 模型

Building a decoder transformer model on AMD GPU(s) — ROCm Blogs 2024年3月12日 作者 Phillip Dang. 在这篇博客中&#xff0c;我们展示了如何使用 PyTorch 2.0 和 ROCm 在单个节点上的单个和多个 AMD GPU 上运行Andrej Karpathy’s beautiful PyTorch re-implementation …