联编
1.什么是联编?
C++中的联编指的是把代码和数据指定到内存地址的过程。联编可以分为静态联编和动态联编。
静态联编:
静态联编是指在编译时就已经完成了所有连接工作。
所有全局函数和全局变量的地址在编译时已经确定。
在静态联编中,相同的名字地址是相同的,在内存中只有一份相同名称的代码或数据。
动态联编:
动态联编是指在编译完成后,程序仍然需要访问外部的代码或数据。
因此,在程序运行时,才能确定函数的入口地址或变量的地址。
在动态联编中,在不同编译单元中的相同名称的代码和数据将会具有不同地址,也就是存在不同的副本。
这种情况下,我们需要根据运行时上下文来动态地确定函数的调用地址或变量的地址。
总之,静态联编在编译时确定地址,程序运行时不再进行连接操作。
而动态联编则在程序运行时进行,需要动态地查找代码和数据的地址。
动态联编的优点是灵活性好,可以减少冗余代码,支持动态库加载等操作。
然而,如果动态联编执行不当,会有运行时开销和安全问题,需要注意程序错误处理和安全措施。
多态的概念(多态多态,多种形态)
在 C++ 中,多态(Polymorphism)是一种特性,允许不同对象对同一消息做出不同的响应。
多态实现了让基类类型的指针和引用指向派生类的对象,以便以一种通用的方式调用同名成员函数。
C++ 中的多态可以通过虚函数实现。
当使用虚函数的时候,如果基类指针或引用指向一个派生类对象,那么虚函数可以动态绑定到派生类的函数。
由于虚函数表的存在,程序会根据实际对象的类型,调用相应的成员函数,而不是调用基类中定义的虚函数。
这样,可以实现在不同的派生类中有不同的虚函数实现,实现了多态。
多态可以让我们以通用的方式设计与编写代码,从而提高代码的可维护性和可重用性。
为了实现多态,需要在基类中定义虚函数,派生类中需要重写这些虚函数。
这样,通过基类类型的指针或引用,调用派生类的函数也不会出现错误,保证了程序的正确性和稳定性。
在C++中,多态是一个非常重要的概念,它的实现方式主要依赖于虚函数动态绑定的机制。
多态可以理解为在不同类型的对象之间共享相同的函数名,但是它们的行为表现却不尽相同。
举个例子,假设现在有一个基类Animal,其中有一个虚函数makeSound(),它的功能是发出动物的叫声。
然后我们又派生出了Dog和Cat两个类,它们都继承了Animal类,并且在makeSound()函数中分别实现了它们各自不同的叫声。
现在,如果我们声明一个基类指针,让它指向一个派生类对象,当我们调用makeSound()函数时,
程序会根据该指针指向的对象所属的类型来调用相应的函数,
因此我们可以在不考虑对象实际类型的情况下,方便地调用它们各自的行为表现。这就是多态的基本概念。
多态本质上是一种面向对象的设计思想,能够提高程序的可扩展性、可维护性和可读性,对于提升代码质量也有着至关重要的作用。
类之间存在层次结构,并且类之间通过继承关联的时候就可以用到多态;
c++中多态意味着调用类的成员函数的时候,会根据当前调用函数的对象类型不同去执行不同的函数;
多态使用实例:
// 多态1:语法基础使用
#include <iostream>
#include <string>
using namespace std;
// 多态的3个条件:
// 1.虚函数成员的父类
// 2.同名函数成员的子类
// 3.指针:父类类型的指针指向父类或子类对象
// 类族:不同类之间有继承和派生关系集合
// 虚指针->虚函数表
class Father
{
public:
Father() {
}
~Father() {
}
// 不适用virtual修饰的普通成员函数,静态联编,无法实现多态
void fatFunc() {
cout << "Father::fatFunc()" << endl;
}
// 虚函数以及虚函数的特点
// 1.虚函数在函数声明时使用virtual修饰,有了虚函数就有了动态联编的条件,基类会根据指针指向对象的类型去调用函数
// 2.虚函数最大的作用是可以在类族中实现多态,是实现多态的其中一个条件
// 在C++中,在类中定义虚函数后,对于每个具有至少一个虚函数的类,编译器都会生成一个对应的虚函数表(也称为vtable),
// 其中存储了该类中所有的虚函数指针。
// 这个虚函数表只包含该类中的虚函数,非虚函数不会存储在vtable中。
// 当这个类被继承后,派生类会复制父类的虚函数表,并在上面添加自己的虚函数指针。这样就可以通过指针来访问派生类和父类的虚函数,实现了多态。
// 虚函数表不会被子类继承(系统分配的东西子类不需要再去继承)
// 在 C++ 的继承体系中,每个类都有自己的虚函数表。
// 当子类继承父类时,并不是直接继承父类的虚函数表,
// 而是在子类中新生成一个虚函数表并继承父类的虚函数表中的内容,
// 同时在新的虚函数表中加入自身新增的或覆盖父类同名的虚函数。
// 具体来说,在父类的虚函数表中存储了虚函数的名称和对应的虚函数指针。
// 当子类继承父类时,子类会先复制父类的虚函数表,
// 然后在其上添加自己的新的虚函数指针和覆盖父类同名的虚函数指针。
// 这样,每个子类都有自己独立的虚函数表,而父类的虚函数表不会被子类继承。
// 需要注意的是,如果某个子类没有重写父类的虚函数,
// 则它在虚函数表中的函数指针与父类的虚函数指针是相同的,这被称为虚函数继承。
// 因此,在虚函数表中可能会存在一些得到继承且相同的虚函数指针。
// 总之,父类的虚函数表不会被子类继承,而是在子类中生成一个新的虚函数表并继承父类的虚函数表中需要的虚函数指针。
// 函数重写:
// 父类的普通成员函数被子类继承后,如果子类有同名函数,那么父类的这个同名函数会被隐藏,
// 如果是虚函数就会被隐藏,相当于在子类中将父类函数的同名函数的函数体重新声明并重新实现(定义)了一遍,
// 这就是重写,可以认为在加了virtual的情况下,子类继承的虚函数于父类中的同名函数就是统一个函数,子类有一次重新实现的机会
// 总之相比普通函数调用,虚函数调用多了以上步骤
// 虚函数表类似于数组类型或链表类型的结构,里边存放当前类中虚函数的函数指针(虚函数的首地址)
// 4byte的指针(虚指针)指向虚函数表,虚指针存放虚函数表的首地址,虚函数表存放每一个虚函数的函数指针(函数的首地址)
// 虚函数表不会被继承,但是虚函数会被继承
// 子类会在类中将虚函数的函数体重新实现一遍(子类重写父类虚函数)
virtual void fatFuncVtl() {
cout << "Father::fatFuncVtl()" << endl;
}
};
class Son1 : public Father
{
private:
public:
Son1() {
};
~Son1() {
};
void fatFunc() {
cout << "Son1::fatFunc()" << endl;
}
void fatFuncVtl() {
cout << "Son1::fatFuncVtl()" << endl;
}
};
class Son2 : public Father
{
private:
public:
Son2() {
};
~Son2() {
};
void fatFunc() {
cout << "Son2::fatFunc()" << endl;
}
void fatFuncVtl() {
cout << "Son2::fatFuncVtl()" << endl;
}
};
int main()
{
Father obj_fat;
obj_fat.fatFunc();
Son1 obj_Son1;
obj_Son1.fatFunc();
Son2 obj_Son2;
obj_Son2.fatFunc();
// 虚函数可以想普通成员函数一样被父类和子类调用
obj_fat.fatFuncVtl();
obj_Son1.fatFuncVtl();
obj_Son2.fatFuncVtl();
// 3.指针:父类类型的指针指向父类或子类对象
Father *p_fat;
// 父类类型指针获取对象首地址并执行对应类的函数
p_fat = &obj_fat;
cout << "普通函数继承:" << endl;
p_fat->fatFunc();
cout << "虚函数多态体现:" << endl;
p_fat->fatFuncVtl();
p_fat = &obj_Son1;
cout << "普通函数继承:" << endl;
p_fat->fatFunc();
cout << "虚函数多态体现:" << endl;
p_fat->fatFuncVtl();
p_fat = &obj_Son2;
cout << "普通函数继承:" << endl;
p_fat->fatFunc();
cout << "虚函数多态体现:" << endl;
p_fat->fatFuncVtl();
return 0;
}
多态中对象指针的内存释放实例:
// 多态中的内存释放
#include <iostream>
#include <string>
using namespace std;
class Father
{
public:
Father() {
cout << "Father::Father()" << endl;
}
// 如果使用普通的析构函数,使用delete释放内存时,
// 父类指针指向子类对象时无法完全释放子类类型的增加的内存,导致留下内存碎片
// ~Father() {
// cout << "Father::~Father()" << endl;
// }
// 父类指针如果需要执行子类类型的析构函数,需要将析构函数定义为虚析构
virtual ~Father() {
cout << "Father::~Father()" << endl;
}
virtual void fatherFunc() {
cout << "Father::fatherFunc()" << endl;
}
void SonFunc() {
cout << "Father::SonFunc()" << endl;
}
};
class Son : public Father
{
public:
Son() {
cout << "Son::Son()" << endl;
}
~Son() {
cout << "Son::~Son()" << endl;
}
void fatherFunc() {
cout << "Son::fatherFunc()" << endl;
}
void SonFunc() {
cout << "Son::SonFunc()" << endl;
}
};
int main()
{
// 父类类型的指针可以指向子类对象,从堆区申请Son类型指针,这里不需要强转
// New Son为匿名对象,只有通过指针去指向它才能访问到
Father *p_father = new Son;
// 调用子类类型重写的同名虚函数
p_father->fatherFunc();
// 父类类型的指针不能调用子类类型新增的函数
p_father->SonFunc() ;
// delete p_father释放内存时调用的时父类类型的析构
// 如果需要调用子类类型的析构去释放子类对象的内存则需要在父类类型中定义虚析构
// 因为指针指向什么类型的对象,就会去调用这个类型的同名函数
// 父类类型指针指向子类类型的析构函数,调用时执行的时子类类型的析构函数,不会造成内存碎片
// 定义为虚析构后会根据父类指针指向的类型去调用对应类型的析构函数
delete p_father;
// 执行结果打印如下
// 虚析构先调用指向子类对象的析构,指向子类对象析构时会自动调用父类析构
// 所以先执行子类析构在执行父类析构
// 如果没有定义为虚析构,delete时会直接调用指针类类型本身的析构
// Son::~Son()
// Father::~Father()
p_father = nullptr;
return 0;
}