【C++】:多态

朋友们、伙计们,我们又见面了,本期来给大家解读一下有关多态的知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

目录

1. 多态的概念

1.1 概念

2. 多态的定义及实现

2.1 多态的构成条件

2.2 虚函数

2.3 虚函数的重写

2.3.1 虚函数重写的两个例外 

2.4 C++11 override 和 final

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

3. 抽象类 

3.1 概念

3.2 接口继承和实现继承

4. 多态的原理

4.1 虚函数表 

4.2 多态的原理

4.3 动态绑定与静态绑定

5. 单继承和多继承关系的虚函数表

5.1 单继承中的虚表

5.2 多继承中的虚函数表 

6. 总结


1. 多态的概念

1.1 概念

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

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

2. 多态的定义及实现

2.1 多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件:

  • 1. 必须通过基类的指针或者引用调用虚函数
  • 2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
    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 p;
    	Func(p);
    
    	Student s;
    	Func(s);
    	return 0;
    }

      

2.2 虚函数

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

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

2.3 虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

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

注意:

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

2.3.1 虚函数重写的两个例外 

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

class A
{};
class B :public A
{};

class Person
{
public:
	virtual A* BuyTicket()
	{
		cout << "票价->全价" << endl;
		return nullptr;
	}
};
class Student : public Person
{
public:
	//void BuyTicket()
	virtual B* BuyTicket()
	{
		cout << "票价->半价" << endl;
		return nullptr;
	}
};

2. 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。

虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成estructor。

class Person
{
public:
	virtual ~Person(){ cout << "~Person()" << endl; }
};
class Student : public Person
{
public:
	virtual ~Student() { cout << "~Student()" << endl; }
	//~Student(){ cout << "~Student()" << endl; }
};

int main()
{
	Person* p = new Person;
	Student* s = new Student;
	delete p;
	delete s;
	return 0;
}

2.4 C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时才能发现端倪,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

1. final:修饰虚函数,表示该虚函数不能再被重写

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

  

拓展:

final修饰类,则该类不能被继承

class A final
{
public:
	int _a;
};
class B final :public A
{
public:
	int _b;
};
class C : public B
{
public:
	int _c;
};

  

2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

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

  

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

3. 抽象类 

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

void TestCar()
{
	Car* pBenz = new Benz;
	pBenz->Drive();

	Car* pBMW = new BMW;
	pBMW->Drive();
}

3.2 接口继承和实现继承

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

4. 多态的原理

首先先来看一下这道题:

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

根据之前的判断,成员函数在公共代码区,所以这个类的大小只有4字节,如果我们打印出来看一下的话并不是4字节,而是8字节,那这是为什么呢?我们可以调试来观察一波:

通过调试可以观察到在b里面还存在一个__vfptr,这个玩意叫做虚函数表,到底有什么用呢?这就是我们接下来的话题:

4.1 虚函数表 

通过上面的观察可以发现在Base对象中的这个指针我们叫做虚函数表指针(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;
};

通过观察可以发现:

  • 1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  • 2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  • 3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  • 4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  • 5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  • 6. 这里还有存在一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在代码段的。

4.2 多态的原理

在前面的代码中我们关于买票做了区分,在那里Func函数传Person调用Person::BuyTicket(全价),传Student调用的是Student::BuyTicket(半价)

  • 1. 观察上图的红色箭头我们看到,p是指向基类对象时,p->BuyTicket在基类的虚表中找到虚函数是Person::BuyTicket。
  • 2. 观察上图的紫色箭头我们看到,p是指向派生类对象时,p->BuyTicket在派生类的虚表中找到虚函数是Student::BuyTicket。
  • 3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
  • 4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
  • 5. 再通过下面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

4.3 动态绑定与静态绑定

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

5. 单继承和多继承关系的虚函数表

5.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的虚表呢?下面我们使用代码打印出虚表中的函数。

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]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;

    //1.先取b的地址,强转成一个int*的指针
    //2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
    //3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
    //4.虚表指针传递给PrintVTable进行打印虚表
  
    VFPTR * vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);
	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);
	return 0;
}

思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr。

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

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

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]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);
	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);
	return 0;
}

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

6. 总结

1. 什么是多态?

多态分为静态多态动态多态,静态多态在编译时就已经确定好了,动态多态在运行时才会确定,常见静态多态是函数重载,动态多态则是通过父类的指针或者引用调用虚函数,对基类虚函数的重写,指向谁,就调用谁的虚函数,以此来实现多种形态。

2. 什么是重载、重写(覆盖)、重定义(隐藏)?


重载:同一作用域内,函数名相同参数不同(参数类型/数量/顺序)。
重写:子类和父类的虚函数,名称、返回值(协变例外)、参数都相同,叫做子类重写了父类的虚函数。
重定义:子类和父类的函数名相同,称子类隐藏了父类的某个函数。 

3.  多态的原理

父类和子类之中保存的虚表指针是不一样的,通过传入指针或者引用确定去子类还是父类中去寻找虚表指针,最后达到调用不同虚函数的目的。

4. inline函数可以是虚函数吗?

可以,在VS之下,如果构成多态,编译器会放弃inline的属性,这个函数就不是内联函数了,因为内联函数会被展开,是没有地址的。而虚函数会将其地址放入至虚表之中。

普通调用:inline起作用;多态调用:inline不起作用。

5. 静态成员可以是虚函数吗?


不可以,编译报错,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,但是它可以指定类域调用,所以静态成员函数无法放进虚函数表。

6. 构造函数可以是虚函数吗?


不可以,编译报错,对象中虚表指针是构造函数初始化列表阶段才初始化的。
虚函数多态调用,要到虚表中找,但是虚表指针还未初始化。

7. 析构函数可以是虚函数吗?

最好将析构函数定义成虚函数,否则会出现指向子类对象的父类指针调用父类析构函数而不会调用子类虚构函数的情况,出现内存泄漏。

8. 对象访问普通函数快还是虚函数更快?


首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?

虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。

10. C++菱形继承的问题?虚继承的原理?

菱形继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。

通过虚基表中的偏移量来找到对应的成员,从而解决了数据冗余和二义性的问题。

11. 什么是抽象类?抽象类的作用?

抽象类不能实例化出对象,抽象类强制重写了虚函数

另外抽象类体现出了接口继承关系。

 

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!    

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

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

相关文章

Python Opencv实践 - 全景图片拼接stitcher

做一个全景图片切片的程序Spliter 由于手里没有切割好的全景图片资源&#xff0c;因此首先写了一个切片的程序spliter。 如果有现成的切割好的待拼接的切片文件&#xff0c;则不需要使用spliter。 对于全景图片的拼接&#xff0c;需要注意一点&#xff0c;各个切片图片之间要有…

什么是机器学习

前言 机器学习&#xff08;Machine Learning, ML&#xff09;是一个总称&#xff0c;用于解决由各位程序员自己基于 if-else 等规则开发算法而导致成本过高的问题&#xff0c;想要通过帮助机器 「发现」 它们 「自己」 解决问题的算法来解决 &#xff0c;而不需要程序员将所有…

影响语音芯片识别率的因素概述

语音芯片识别率是指芯片对人类语音信号的识别能力。在实际应用中&#xff0c;语音芯片识别率的高低直接影响了用户对芯片的体验和满意度。因此&#xff0c;提高语音芯片识别率是当前语音技术领域的重要任务之一。 1.、语音芯片的硬件设计&#xff1a;设计良好的芯片可以更好地…

竹云参编《公共数据授权运营平台技术要求》团体标准正式发布

2023年11月23日&#xff0c;第二届全球数字贸易博览会“数据要素治理与市场化论坛”于杭州成功召开&#xff0c;国家数据局党组书记、局长刘烈宏&#xff0c;浙江省委常委、常务副省长徐文光出席会议并致辞。会上&#xff0c;国家工业信息安全发展研究中心发布并解读了我国首部…

大厂前沿技术导航

百度Geek说 - 知乎 腾讯技术 - 知乎 美团技术团队

Java 项目中常用注解汇总!! (自整理)

Spring框架的注解 PostMapping("/getDetails") post请求 映射到接口 RequestBody 用来接收HTTP请求体中参数 GetMapping("/getDetails") get请求 映射到接口 RequestParam 用来接收URL中的查询参数 PutMappi…

Tomcat 配置

1&#xff1a; 打开 2&#xff1a;选择版本号&#xff0c;我这边是 1.7 3&#xff1a;添加 web 4: 添加jar包 5&#xff1a;添加 6&#xff1a;添加 Tomcat

逆矩阵相关性质与例题

1.方阵的行列式&#xff1a;就是将方阵中的每一个元素转换至行列式中。 1.性质一&#xff1a;转置方阵的行列式等于转置前的行列式。&#xff08;对标性质&#xff1a;行列式与它的转置行列式相等&#xff09; 2.性质二&#xff1a;|ka||a|*k的n次方&#xff0c;n为方阵阶数。 …

平台工程时代的 Kubernetes 揭秘:2023年生产状况报告深度剖析

Kubernetes 在生产环境中的复杂性已经成为常态&#xff0c;在2023年这个平台工程盛行的时代&#xff0c;容器管理的最大亮点可能在于其灵活性&#xff0c;然而在运维政策和治理等方面仍然存在诸多挑战。八年过去了&#xff0c;在生产环境中使用 Kubernetes 仍然需要面临许多挑战…

目前比较好用的护眼台灯,小学生适合的护眼台灯推荐

随着技术的发展&#xff0c;灯光早已成为每家每户都需要的东西。但是灯光不好可能会对眼睛造成伤害是很多人没有注意到的。现在随着护眼灯产品越来越多&#xff0c;市场上台灯的选择越来越多样化&#xff0c;如何选择一个对眼睛无伤害、无辐射的台灯成为许多家长首先要考虑的问…

mysql:修改密码的几种方式

背景 当我们 brew install mysql 新安装 mysql 的时候&#xff0c;是没有密码的&#xff0c;我们可以直接通过 mysql -u root 连接上。但是密码还是要设置的&#xff0c;一是为了安全&#xff0c;二是有些数据库软件如 Sequel 连接都是必须要密码的&#xff0c;接下来我们来看…

自监督LIGHTLY SSL教程

Lightly SSL 是一个用于自监督学习的计算机视觉框架。 github链接&#xff1a;GitHub - lightly-ai/lightly: A python library for self-supervised learning on images. Documentation&#xff1a;Documentation — lightly 1.4.20 documentation 以下内容主要来自Documen…

ElasticSearch 7 SQL 详解

平时使用Elasticsearch的时候,会在Kibana中使用Query DSL来查询数据.每次要用到Query DSL时都基本忘光了,需要重新在回顾一遍,最近发现Elasticsearch已经支持SQL查询了(6.3版本以后),整理了下一些用法. 简介 Elasticsearch SQL是一个X-Pack组件,它允许针对Elasticsearch实时执…

Zabbix-Liunx服务器内存使用率测试

要在Python 2.7中运行内存消耗脚本并安装psutil&#xff0c;您需要先安装pip。以下是完整的步骤&#xff0c;包括如何在Python 2.7环境中安装pip&#xff0c;然后安装psutil&#xff0c;以及最后如何运行内存消耗脚本。 步骤1: 安装pip 在Python 2.7中安装pip&#xff1a; 首先…

2016年8月15日 Go生态洞察:Go 1.7版本发布

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

JVM内存模型及调优

本文将为大家详细介绍JVM内存模型及如何对JVM内存进行调优。我们将分为以下几个部分进行讲解&#xff1a; JVM内存模型概述JVM内存区域及作用JVM内存调优方法实战案例与优化技巧 一、JVM内存模型概述 在深入了解JVM内存模型之前&#xff0c;我们需要先了解一下Java内存模型&am…

通俗理解词向量模型,预训练模型,Transfomer,Bert和GPT的发展脉络和如何实践

最近研究GPT&#xff0c;深入的从transfomer的原理和代码看来一下&#xff0c;现在把学习的资料和自己的理解整理一下。 这个文章写的很通俗易懂&#xff0c;把transformer的来龙去脉&#xff0c;还举例了很多不错的例子。 Transformer通俗笔记&#xff1a;从Word2Vec、Seq2S…

2016年8月18日 Go生态洞察:Go 1.7版本二进制文件缩小

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

MFC所有控件介绍及基本使用

一、前言 本篇文档介绍了MFC控件的基本使用&#xff0c;同时提供了关于MFC控件使用的工程代码&#xff0c;程序界面如下图&#xff0c;有兴趣的可以到文档最后的链接处进行下载。 二、控件介绍 2.1 Button &#xff08;按钮&#xff09; 2.2 CheckBox&#xff08;复选框&am…

【新手解答】深入探索 C 语言:一些常见概念的解析

C语言的相关问题解答 写在最前面目录 问题1变量名与变量的关系与区别变量和数据类型形参&#xff08;形式参数&#xff09;的概念 问题2解析延伸解析对于多文件程序的理解总结 问题3类和对象变量和数据类型变量是否为抽象的数据类型&#xff1f;总结 问题4解析源文件&#xff0…