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

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

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

相关文章

python数据分析——数据分析的统计推断

数据分析的统计推断 前言一、提出问题二、统计归纳方法三、统计推断四、统计推断步骤4.1.点估计4.2.区间估计4.2.1. 总体方差已知4.2.2总体方差未知 4.3. 假设检验4.4. 假设检验的假设4.5.显著性水平 五、检验统计量六、检验方法七、拒绝域八、假设检验步骤九、重要假设检验方法…

五一假期零碎时间练习学习过的内容(商城版)

目录 1 总览1.1 技术架构1.2 其他1.2.1 数据库1.2.2 后端部分1.2.2.1 复习feign1.2.2.2 复习下网关网关的核心功能特性&#xff1a;网关路由的流程断言工厂过滤器工厂全局过滤器 过滤器执行顺序解决跨域问题 1.2.2.3 es部分复习 1.2.3 前端部分 2 day1 配置网关2.1 任务2.2 网关…

cloudreve手动添加文件

cloudreve导入本地已有的文件&#xff0c;不需要再次上传 需要更新版本到3.1及更高 在【管理面板】-----【文件】导入 如上图&#xff1a; 选择目标用户、原始路径、目的路径&#xff0c;创建导入任务即可&#xff01;

免费可商用字体素材大全,办公设计字体合集打包166款

一、素材描述 这是一套免费可商用字体素材&#xff0c;这些字体一般会在办公与设计的时候用到&#xff0c;比如&#xff0c;Photoshop、illustrator、Coreldraw、AfterEffects、Indesign、WPS、Office&#xff0c;等等&#xff0c;想要更好更快地办公与设计&#xff0c;字体还…

项目管理【人】概述

系列文章目录 【引论一】项目管理的意义 【引论二】项目管理的逻辑 【环境】概述 【环境】原则 【环境】任务 【环境】绩效 【人】概述 一、项目涉及到的人 1.1 项目发起人、项目指导委员会和变更控制委员会 项目发起人&#xff08;Sponsor&#xff09; 项目指导委员会&…

翻译《The Old New Thing》 - Why does the CreateProcess function do autocorrection?

Why does the CreateProcess function do autocorrection? - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20050623-03/?p35213 Raymond Chen 在 2005 年 6 月 23 日 为什么 CreateProcess 函数会进行自动更正&#xff1f; 译注&#xff…

正则表达式_字符匹配/可选字符集

正则表达式&#xff08;Regular Expression&#xff09;也叫匹配模式(Pattern)&#xff0c;用来检验字符串是否满足特 定规则&#xff0c;或从字符串中捕获满足特定规则的子串。 字符匹配 最简单的正则表达式由“普通字符”和“通配符”组成。比如“Room\d\d\d”就这样 的正则…

农作物害虫检测数据集VOC+YOLO格式18975张97类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;18975 标注数量(xml文件个数)&#xff1a;18975 标注数量(txt文件个数)&#xff1a;18975 标…

MySQL-集群的高可用

MMM: Multi-Master Replication Manager for MySQL&#xff0c;Mysql主主复制管理器是一套灵活的脚本程序&#xff0c;基于perl实现&#xff0c;用来对mysql replication进行监控和故障迁移&#xff0c;并能管理mysql Master-Master复制的配置(同一时间只有一个节点是可写的) …

【重难点算法题】设计哈希集合、哈希映射

文章目录 Tag题目来源解题思路方法一&#xff1a;链地址法 类似题目代码1代码2 写在最后 Tag 【哈希集合】【哈希映射】【链地址法】【数据结构设计】 题目来源 705. 设计哈希集合 解题思路 在解题之前需要先明确两组概念&#xff1a; 哈希表与散列表哈希函数与散列函数 上…

关于图形库

文章目录 1. 概念介绍2. 使用方法2.1 普通路由2.2 命名路由 3. 示例代码4. 内容总结 我们在上一章回中介绍了"使用get显示Dialog"相关的内容&#xff0c;本章回中将介绍使用get进行路由管理.闲话休提&#xff0c;让我们一起Talk Flutter吧。 1. 概念介绍 我们在本章…

分布式领域计算模型及SparkRay实现对比

目录 一、分布式计算领域概览 二、Spark计算模型分析 三、Ray计算模型分析 3.1 需求分析 3.2 系统设计 3.3 系统实现 四、总结 一、分布式计算领域概览 当前分布式计算模型主要分为以下4种&#xff1a; Bulk Synchronous Parallel Model&#xff08;块同步并行模型&…

视频下载器 UC网盘

老王导航 - 复杂问题找老王&#xff0c;简单问题百度搜 神器啊

入门2-分支结构

【深基2.习6】Apples Prologue / 苹果和虫子 题目描述 小 B 喜欢吃苹果。她现在有 m m m&#xff08; 1 ≤ m ≤ 100 1 \le m \le 100 1≤m≤100&#xff09;个苹果&#xff0c;吃完一个苹果需要花费 t t t&#xff08; 0 ≤ t ≤ 100 0 \le t \le 100 0≤t≤100&#xff0…

500行代码实现贪吃蛇(1)

文章目录 目录1. Win32 API 介绍1.1 Win32 API1.2 控制台程序&#xff08;Console&#xff09;1.3 控制台屏幕上的坐标COORD1.4 [GetStdHandle](https://learn.microsoft.com/zh-cn/windows/console/getstdhandle)1.5 [GetConsoleCursorInfo](https://learn.microsoft.com/zh-c…

LAME及 iOS 编译

文章目录 关于 LAME编译 for iOS 关于 LAME 官网&#xff1a;https://lame.sourceforge.io LAME是根据LGPL许可的高质量MPEG音频层III&#xff08;MP3&#xff09;编码器。 LAME的开发始于1998年年中左右。Mike Cheng 最开始将它作为针对8hz-MP3编码器源的补丁。在其他人提出…

python学习笔记----异常、模块与包(九)

一、异常 1.1 什么是异常 在Python中&#xff0c;异常是程序执行时发生的错误。当Python检测到一个错误时&#xff0c;它会引发一个异常&#xff0c;这可能是由于多种原因&#xff0c;如尝试除以零、访问不存在的文件&#xff0c;或者尝试从列表中获取不存在的索引等。异常处…

踏春正当时!VELO Prevail Ride带你探索多元骑行潮流体验~

嘿&#xff0c;朋友&#xff01;踏春正当时嘞&#xff01;在这个追求个性化与多元化的新时代&#xff0c;骑行爱好者们也开始寻找能适应各种骑行场景的理想坐垫。从悠闲自在的日常通勤&#xff0c;到热血沸腾的公路竞速&#xff0c;再到勇攀高峰的山地探险&#xff0c;维乐VELO…

【Linux—进程间通信】共享内存的原理、创建及使用

什么是共享内存 共享内存是一种计算机编程中的技术&#xff0c;它允许多个进程访问同一块内存区域&#xff0c;以此作为进程间通信&#xff08;IPC, Inter-Process Communication&#xff09;的一种方式。这种方式相对于管道、套接字等通信手段&#xff0c;具有更高的效率&…

论文辅助笔记:TimeLLM

1 __init__ 2 forward 3 FlattenHead 4 ReprogrammingLayer