目录
联编
一联编的概念:
1、计算机自身彼此关联的过程
2、映射关系
3、明确了要执行的代码
4、分为静态联编和动态联编
计算机自身彼此关联的过程,计算机运行过程中,总有一些东西是互相关联的,在联编的过程中,需要去确定程序当中的操作,和调用的执行代码,中间会有一个叫做映射的东西,这种映射关系可以举个例子,比如说:写了函数重载,那么编译器就会根据你的这个函数名,找到重载函数,然后根据参数,找到具体要执行的那个重载函数,实际上就是找到了具体代码去执行,也就是谁干了什么事,要去找谁,实际上就是明确了要执行的代码段
二:静态联编:
1、联编工作出现在写代码的阶段
2、早期联编,静态约束
3、在编译时就确定了函数实现及函数调用的关联
联编工作出现在写代码的时候,也就是说为什么称之为静态联编呢,因为具有需要联编工作的地方,一旦出现就不会再变了,那什么时候会出现这种情况呢,就是写代码的时候,代码写好了,那么映射关系也就确定了。而写代码这步操作是出现在早期的时候,所以也可以称之为早期联编或者静态约束,所谓静态联编,实际上就是在早期阶段,写代码去确定关系,但是对于一个程序运行来讲,早先编译源代码,然后再把这些编译出来的文件链接起来,才能够成为一个可执行的exe文件,那么静态联编是在编译时就确定了函数实现及函数调用的关联
三:动态联编
1、编译阶段不能确定知道将要调用的函数
2、只能在程序运行的时候才能确定将要调用的函数
3、在程序运行的时候才将函数实现和函数调用关联
4、晚期联编,动态约束
编译阶段不能确定知道将要调用的函数,在编译阶段不能确定这些操作的话,那么什么时候才能确定呢,我们刚说到了编译,链接,执行三个阶段,编译阶段确定不了的话,那就只能是在链接或者执行的阶段,但是链接其实就是,把你编译出来的这些文件给连接起来,所以在这个阶段也不会去确定,这个阶段不会对联编起太大的影响,在程序运行的时候,才将函数实现和函数调用关联,那么这个时候就已经是比较靠后的阶段了,所以也称为晚期联编或动态约束
四:实现动态联编的条件:
实际上静态联编我们一直都在使用,只不过我们没有发现而已
1、必须把动态联编的行为定义为类的虚函数(要实现动态联编必须要有虚函数)
2、必须要有继承关系(不止一个类,两个或者两个以上,这些类之间必须还要有继承关系)
所以说两个重点要有继承关系的类,还得有虚函数,而且虚函数是可以被继承的,所以父类里的虚函数要继承到子类里面去,这就是实现动态联编的基础条件,这些是前提保障,要有这么两个类,一个类里面有一些虚函数,然后这个类里面又有子类,父类的虚函数又被继承到子类中去,然后才可以去做后期的事情,这是前期的基础准备
3、使用基类指针调用虚函数成员
有了这些前期的基础准备之后,就通过这个基类的指针去调用虚函数成员,那么这个基类指针究竟指向一个什么样的对象呢,我们之前说过基类指针可以指向基类对象,基类指针还可以指向子类对象,那么这两个点加上基类指针,调用虚函数成员的话,那么指针究竟是指向父类对象还是子类对象,就能够决定是调用父类的同名虚函数,还是调子类的同名虚函数,这就是实现的条件以及后续怎么样去用它,实际上在这篇文章中,动态联编就是我们C++中的多态。
多态
五:多态的概念:
如果说要直接去理解多态的话,就组个词就可以了,多态嘛就是多种形态
1、直接理解:具有多种形态或者状态
2、高级理解:同一接口通过不同对象调用有不同效果(这里的接口就是函数名,也就是同一函数名通过不同对象去调用有不同的效果,那么这个对象什么不同呢,就是所属的类不同,那么不同类的对象去调用同一名字的函数,有不同的效果产生,这个实际上就是多态的高级理解,也就是多态的一个定义。不同类的对象去调用同一名字的函数,会有不同的效果,和情况,总的来说就是,不同的对象去完成同一行为会产生不同的效果)
六:多态存在的意义:
方便程序接口的实现(也就是函数你写了,具体是什么功能,可以通过多态实现)
七:虚函数语法:
1、类中声明时前面加上virtual关键字则为虚函数(virtual是虚拟的意思)
2、virtual 函数返回值类型 函数名 (参数列表表){函数体;}
八、虚函数的作用:
结合继承实现多态
九:虚函数的特点:
1、在类中定义虚函数,系统会为这个类维护一个虚函数表(就是一个低保,就是说你一个类里面,如果说你定义了虚函数,那么就可以多吃一份低保,这份低保的名字叫做虚函数表,系统给你的,你看不见,也不需要你去定义,用不上的时候,就跟没有一样,光有虚函数表也不行,还得有一个指针)
2、类中会多出4个字节的指针指向虚函数表(这个指针也是低保,会多出来这个指针,这个指针的名字叫做虚指针,虚指针的作用就是指向虚函数表,虚函数表可以这样去理解,就是一个类似于数组的东西,在这个里面,要保存一些内容,虚函数表的元素内容是指针,就是这些虚函数函数指针,这些指针指向函数,那么这些指针存放在哪里呢,就是存放在虚函数表里,就可以把虚函数表理解成一个指针数组,里面就是虚函数指针,虚指针就是一个指针,指向的是虚函数表)
3、调用虚函数时会通过虚指针查找虚函数表,然后调用(作用就是调用虚函数的时候,不直接通过函数名去调用了,因为我们有虚函数表,里面有元素可以访问到这些虚函数了,而要访问虚函数表一定是通过虚指针,为什么这么做呢,那是因为我们在调用这些函数的时候,要知道哪个对象去调用的,从而产生一些区别,怎么样去明确这个区别呢?不同对象的虚指针肯定是不一样的,那么这个时候就知道了究竟是哪个对象调的)虚函数表和虚指针都属于是低保,是系统提供的,既然是系统提供的,那么每一个类,系统都会一视同仁的,既然每一个类系统都会给你提供一份的话,那么还有必须去继承嘛,就没有必要了,只要是低保,就不用继承了,但是表中的项会被继承,因为表中的项不就是那些虚函数嘛,虚函数是会被继承的。)
代码举例
、
#include<iostream>
using namespace std;
class Classname
{
public:
Classname();
~Classname();
virtual void test_Func_1();//前面加个virtual,变成虚函数
virtual void test_Func_2()//前面加个virtual,变成虚函数
{
cout << "test_Func()" << endl;
}
};
Classname::Classname()
{
}
Classname::~Classname()
{
}
void Classname::test_Func_1()//定义的时候不需要把virtual放在前面,像virtual void test Func_1();错的
{
cout << "test_Func_1" << endl;
}
int main()
{
Classname obj;
obj.test_Func_1();
obj.test_Func_2();
return 0;
}
这样的话,两个虚函数就写成功了。
#include<iostream>
using namespace std;
class Classname
{
public:
virtual void test_Func_1();//前面加个virtual,变成虚函数
virtual void test_Func_2()//前面加个virtual,变成虚函数
{
cout << "test_Func()" << endl;
}
int num;
};
void Classname::test_Func_1()//定义的时候不需要把virtual放在前面,像virtual void test Func_1();错的
{
cout << "test_Func_1" << endl;
}
int main()
{
Classname obj;
obj.test_Func_1();
obj.test_Func_2();
cout << "sizeof(obj)" << sizeof(obj) << endl;
return 0;
}
这里的obj应该是4字节,但是输出结果却是8字节,是因为这个类里有虚函数,有虚函数就会有虚函数表,系统自动给的嘛,那么这个虚函数表呢,里面就有虚函数的地址,保存了地址之后,虚指针就出现了,虚函数表和虚指针,虚指针每个对象有一个,所以是8字节,但是虚函数表不属于这些对象,直接sizeof是看不出它的字节数的,但是它的确是存在的,这里多出来的4个字节就是指向虚函数表的虚指针。虚函数表只有一个。所以虚指针永远只有一个,只要能找到虚函数表就可以了,不管有多少个虚函数,都是放在虚函数表里面。这样做也可以尽量减少对象所占的内存,实际上也是可以把虚函数表直接给到对象上的,但是这样内存就大了嘛,所以我们用虚指针去访问虚函数表,然后对象里面放虚指针,这样既可以能够区分是哪个对象,也节约了单个对象的内存。
十:实现多态——理论支撑
1、写具有继承关系的两个类
2、子类和父类有同名的虚函数,但是功能不同(函数体不一样)
3、通过父类指针或引用,使用多态
其实就是,用子类和父类的不同对象,去访问同名的虚函数,会有不同的结果。
代码举例
#include<iostream>
using namespace std;
class Father
{
public:
virtual void test_Func();
};
void Father::test_Func()
{
cout << "Father::test_Func()" << endl;
}
//写一个Father的继承出来son
class Son :public Father//写一个Father的继承出来son
{
public:
virtual void test_Func();//写一个和father同名的虚函数
private:
};
void Son::test_Func()
{
cout << "Son::test_Func()" << endl;
}
int main()
{
Father*p_obj;//搞一个父类指针
Father fa_obj;//弄两个不同类的对象:子类和父类的对象,用这两个对象去调用同名虚函数
Son son_obj;
p_obj = &fa_obj;//父类指针指向父类对象
p_obj->test_Func();//父类对象调用同名虚函数
p_obj = &son_obj;//父类指针指向子类对象
p_obj->test_Func();//子类对象调用同名函数
return 0;
}
那么父类对象和子类对象都去调用同名虚函数,那会发生什么事情呢
运行一下代码,执行结果为:
Father::test_Func()
Son::test_Func()
通过执行结果你会发现如果指向的是父类对象话,就是调用的父类的虚函数,如果指向子类对象的话,就是调用子类的虚函数,所以同样的函数名,同样的指针指向不同,调用的函数就不一样。
十一:注意事项
1、基类中有虚函数,且通过父类指针分配子类对象时,释放只能通过父类指针进行释放
2、在delete父类指针时会调用父类的析构函数,不会调用子类的析构函数
Father*p_obj = new Son;//这就叫通过父类指针分配子类对象;
delete p_obj;//释放只能通过父类指针进行释放
p_obj = NULL;
为什么会调用父类的析构函数,明明我们分配的是子类的对象啊,正常情况下,在删除的时候,不应该调用子类的析构函数嘛,不然的话,如果子类里面新增了一些需要释放的内存,父类的析构函数肯定是不可能去释放子类里新增的需要释放内存的东西,因为先有父类再有子类,子类里面新增的,在父类里面是释放不了的,既然如此,就得想办法去调用子类的析构函数。
方法一:改变指针指向(这种做法不好)
Father*p_obj = new Son;
Son*p1 = (Son*)p_obj;
delete p_obj;
p1= NULL;
方法二:将父类析构函数定义为虚析构,在释放父类对象时候也会调用子类的析构函数(先调用子类的析构,后调用父类的,因为先创建的后析构)