面向对象
1 面向对象及其三大特性
面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。
面向对象的三大特性:
- 封装:将具体的实现过程和数据封装成一个函数,只能通过接口进行访问,降低耦合性。
- 继承:子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写,增强了类之间的耦合性,但是当父类中的成员变量、成员函数或者类本身被 final 关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
- 多态:多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在 C++ 中多态一般是使用虚函数来实现的,使用基类指针调用函数方法时,如果该指针指向的是一个基类的对象,则调用的是基类的虚函数;如果该指针指向的是一个派生类的对象,则调用的是派生类的虚函数。
2 重载、重写、隐藏的区别
函数重载(函数名相同但是形参列表不同,和函数的返回值无关):
重载是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
class A
{
public:
void fun(int tmp);
void fun(float tmp); // 重载 参数类型不同(相对于上一个函数)
void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
int fun(int tmp); // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型
};
函数隐藏(子类重写了父类的函数,导致父类的函数被隐藏):
函数隐藏是指派生类的函数屏蔽了与其同名的基类函数,只要是与基类同名的成员函数,不管参数列表是否相同,基类函数都会被隐藏。
#include <iostream>
using namespace std;
class Base
{
public:
void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; }
};
class Derive : public Base
{
public:
void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
};
int main()
{
Derive ex;
ex.fun(1); // Derive::fun(int tmp)
ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided
return 0;
}
//说明: 上述代码中 ex.fun(1, 0.01); 出现错误,说明派生类中将基类的同名函数隐藏了。若是想调用基类中的同名函数,
//可以加上类型名指明 ex.Base::fun(1, 0.01);,这样就可以调用基类中的同名函数。
函数重写(覆盖):
函数覆盖是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。派生类调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有 virtual 修饰。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun(int tmp) { cout << "Base::fun(int tmp) : " << tmp << endl; }
};
class Derived : public Base
{
public:
virtual void fun(int tmp) { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
};
int main()
{
Base *p = new Derived();
p->fun(3); // Derived::fun(int) : 3
return 0;
}
重写和重载的区别:
范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。
参数区别:重载的函数需要与原函数有相同的函数名、不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型都需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。
virtual 关键字:重写的函数基类中必须有 virtual 关键字的修饰,重载的函数可以有 virtual 关键字的修饰也可以没有。
隐藏和重写,重载的区别:
- 范围区别:隐藏与重载范围不同,隐藏发生在不同类中。
- 参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。
- 利用重写可以实现多态,而隐藏不可以。如果使用基类指针 p 指向派生类对象,利用这个指针调用函数时,对于隐藏的函数,会根据指针的类型去调用函数;对于重写的函数,会根据指针所指对象的类型去调用函数。重写必须使用 virtual 关键字,此时会更改派生类虚函数表的表项。
- 隐藏是发生在编译时,即在编译时由编译器实现隐藏,而重写一般发生运行时,即运行时会查找类的虚函数表,决定调用函数接口。
3 多态及其实现方法
多态的概念:
多态就是不同继承类的对象,对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
程序示例如下:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun() { cout << "Base::fun()" << endl; }
virtual void fun1() { cout << "Base::fun1()" << endl; }
virtual void fun2() { cout << "Base::fun2()" << endl; }
};
class Derive : public Base
{
public:
void fun() { cout << "Derive::fun()" << endl; }
virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
};
int main()
{
Base *p = new Derive();
p->fun(); // Derive::fun() 调用派生类中的虚函数
return 0;
}
多态的实现原理:
多态是通过虚函数实现的,虚函数的地址保存在虚函数表中,虚函数表的地址保存在含有虚函数的类的实例对象的内存空间中。
在类中用 virtual 关键字声明的函数叫做虚函数;
存在虚函数的类都有一个虚函数表,当创建一个该类的对象时,该对象有一个指向虚函数表的虚表指针(虚函数表和类对应的,虚表指针是和对象对应);
当基类指针指向派生类对象,基类指针调用虚函数时,该基类指针指的虚表指针实际指向派生类虚函数表,通过遍历虚表,寻找相应的虚函数然后调用执行。
基类的虚函数表如下图所示:
派生类的对象虚函数表如下:
简单解释:当基类的指针指向派生类的对象时,通过派生类的对象的虚表指针找到虚函数表(派生类的对象虚函数表),进而找到相应的虚函数 Derive::f() 进行调用。
多态的总结:
根据上述的结论,我们可以知道虚函数的调用是在运行时决定,是由本身所指向的对象所决定的。
如果使用虚函数,基类指针指向派生类对象并调用对象方法时,使用的是子类的方法;
如果未使用虚函数,则是普通的隐藏,则基类指针指向派生类对象时,使用的是基类的方法(与指针类型看齐)
基类指针能指向派生类对象,但是派生类指针不能指向基类对象
4 虚函数和纯虚函数详解
虚函数:
被 virtual 关键字修饰的成员函数,C++ 的虚函数在运行时动态绑定,从而实现多态。
#include <iostream>
using namespace std;
class A
{
public:
virtual void v_fun() // 虚函数
{
cout << "A::v_fun()" << endl;
}
};
class B : public A
{
public:
void v_fun()
{
cout << "B::v_fun()" << endl;
}
};
int main()
{
A *p = new B(); //父类 类名 = new 子类()
p->v_fun(); // B::v_fun()
return 0;
}
纯虚函数:
纯虚函数在类中声明时,用 virtual 关键字修饰且加上 = 0,且没有函数的具体实现;
含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口定义,没有具体的实现方法;
继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
对于抽象类需要说明的是:
- 抽象类对象不能作为函数的参数,不能创建对象,不能作为函数返回类型;
- 可以声明抽象类指针,可以声明抽象类的引用;
- 抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
纯虚函数的作用:
含有纯虚函数的基类要求任何派生类都要定义自己的实现方法,以实现多态性。实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数。定义纯虚函数是为了实现统一的接口属性,用来规范派生类的接口属性,也即强制要求继承这个类的程序员必须实现这个函数。纯虚函数的意义在于,让所有的类对象(主要是派生类对象)都可以要求实现纯虚函数的属性,在面对对象设计中非常有用的一个特性。
5 虚函数和纯虚函数的区别
虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类(含有纯虚函数的类称为抽象基类)。
使用方式不同:虚函数可以直接使用,纯虚函数必须在派生类中实现后才能使用;
定义形式不同:虚函数在定义时在普通函数的基础上加上 virtual 关键字,纯虚函数定义时除了加上 virtual 关键字还需要加上 =0;
虚函数必须实现,否则编译器会报错;
对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数,虚函数和纯虚函数都可以在派生类中重写;
析构函数最好定义为虚函数,特别是对于含有继承关系的类;析构函数可以定义为纯虚函数,此时,其所在的类为抽象基类,不能创建实例化对象。(这样可以先析构派生类在析构基类,可以释放在派生类在堆区中的数据)
6 虚函数的实现机制
1、虚函数的实现原理:
**实现机制:**虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。
每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚函数表。该表是编译器在编译时设置的静态数组,一般我们称为 vtable。虚函数表包含可由该类调用的虚函数,此表中的每个条目是一个函数指针,指向该类可访问的虚函数。
每个对象在创建时,编译器会为对象生成一个指向该类的虚函数表的指针,我们称之为 vptr。
vptr 在创建类实例时自动设置,以便指向该类的虚拟表。如果对象(或者父类)中含有虚函数,则编译器一定会为其分配一个 vptr;如果对象不包含(父类也不含有),此时编译器则不会为其分配 vptr。与 this 指针不同,this 指针实际上是编译器用来解析自引用的函数参数,vptr 是一个真正的指针。
虚函数表相关知识点:
虚函数表存放的内容:类的虚函数的地址。
虚函数表建立的时间:编译阶段,即程序的编译过程中会将虚函数的地址放在虚函数表中。
虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。
虚函数表和类绑定,虚表指针和对象绑定。即类的不同的对象的虚函数表是一样的,但是每个对象在创建时都有自己的虚表指针
vptr,来指向类的虚函数表vtable。
实例:
无虚函数覆盖的情况:
#include <iostream>
using namespace std;
class Base
{
public:
virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }
virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};
class Derive : public Base
{
public:
virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
int main()
{
Base *p = new Derive();
p->B_fun1(); // Base::B_fun1()
return 0;
}
基类和派生类的继承关系:
基类的虚函数表:
派生类的虚函数表:
主函数中基类的指针 p 指向了派生类的对象,当调用函数 B_fun1() 时,通过派生类的虚函数表找到该函数的地址,从而完成调用。
2、虚拟函数表指针 vptr:
带有虚函数的类,通过该类所隐含的虚函数表来实现多态机制,该类的每个对象均具有一个指向本类虚函数表的指针,这一点并非 C++ 标准所要求的,而是编译器所采用的内部处理方式。实际应用场景下,不同平台、不同编译器厂商所生成的虚表指针在内存中的布局是不同的,有些将虚表指针置于对象内存中的开头处,有些则置于结尾处。如果涉及多重继承和虚继承,情况还将更加复杂。因此永远不要使用 C 语言的方式调用 memcpy() 之类的函数复制对象,而应该使用初始化(构造和拷构)或赋值的方式来复制对象。
程序示例,我们通过对象内存的开头处取出 vptr,并遍历对象虚函数表。
#include <iostream>
#include <memory>
using namespace std;
typedef void (*func)(void);
class A {
public:
void f() { cout << "A::f" << endl; }
void g() { cout << "A::g" << endl; }
void h() { cout << "A::h" << endl; }
};
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
class Derive: public Base {
public:
void f() { cout << "Derive::f" << endl; }
void g() { cout << "Derive::g" << endl; }
void h() { cout << "Derive::h" << endl; }
};
int main()
{
Base base;
Derive derive;
//获取vptr的地址,运行在gcc x64环境下,所以将指针按unsigned long *大小处理
//另外基于C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置
unsigned long* vPtr = (unsigned long*)(&base);
//获取vTable 首个函数的地址
func vTable_f = (func)*(unsigned long*)(*vPtr);
//获取vTable 第二个函数的地址
func vTable_g = (func)*((unsigned long*)(*vPtr) + 1);//加1 ,按步进计算
func vTable_h = (func)*((unsigned long*)(*vPtr) + 2);//同上
vTable_f();
vTable_g();
vTable_h();
vPtr = (unsigned long*)(&derive);
//获取vTable 首个函数的地址
vTable_f = (func)*(unsigned long*)(*vPtr);
//获取vTable 第二个函数的地址
vTable_g = (func)*((unsigned long*)(*vPtr) + 1);//加1 ,按步进计算
vTable_h = (func)*((unsigned long*)(*vPtr) + 2);//同上
vTable_f();
vTable_g();
vTable_h();
cout<<sizeof(A)<<endl;
cout<<sizeof(base)<<endl;
cout<<sizeof(derive)<<endl;
return 0;
}
/*
Base::f
Base::g
Base::h
Derive::f
Derive::g
Derive::h
1
8
8
*/
我们可以看到同样的函数实现,对象在分配空间时,编译器会为对象多分配一个 vptr 指针的空间。
3、虚函数的使用场景:
构造函数不能为虚函数:构造函数不能定义为虚函数。构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。
析构函数为虚函数:一般建议析构函数定义成虚函数,这样做可以有效是防止内存泄漏,实际应用时当基类的指针或者引用指向或绑定到派生类的对象时,如果未将基类的析构函数定义成虚函数,当我们对基类指针执行 delete 操作时,此时只会调用基类的析构函数,将基类的成员所占的空间释放掉,而派生类中特有的资源就会无法释放而导致内存泄漏。
static 函数不能定义为虚函数。
7 构造函数、析构函数是否可以定义成虚函数
构造函数一般不定义为虚函数:
**从存储空间的角度考虑:**构造函数是在实例化对象的时候进行调用,如果此时将构造函数定义成虚函数,需要通过访问该对象所在的内存空间才能进行虚函数的调用(因为需要通过指向虚函数表的指针调用虚函数表,虽然虚函数表在编译时就有了,但是没有虚函数的指针,虚函数的指针只有在创建了对象才有),但是此时该对象还未创建,便无法进行虚函数的调用。所以构造函数不能定义成虚函数。
**从使用的角度考虑:**虚函数是基类的指针指向派生类的对象时,通过该指针实现对派生类的虚函数的调用,构造函数是在创建对象时自动调用的。
**从实现上考虑:**虚函数表指针是在创建对象之后才有的,因此不能定义成虚函数。
**从类型上考虑:**在创建对象时需要明确其类型。
析构函数一般定义成虚函数:
析构函数定义成虚函数是为了防止内存泄漏,因为当基类的指针或者引用指向或绑定到派生类的对象时,如果未将基类的析构函数定义成虚函数,会调用基类的析构函数,那么只能将基类的成员所占的空间释放掉,派生类中特有的就会无法释放内存空间导致内存泄漏。
比如以下程序示例:
#include <iostream>
using namespace std;
class A {
private:
int val;
public:
~A() {
cout<<"A destroy!"<<endl;
}
};
class B: public A {
private:
int *arr;
public:
B() {
arr = new int[10];
}
~B() {
cout<<"B destroy!"<<endl;
delete arr;
}
};
int main() {
A *base = new B();
delete base;
return 0;
}
// A destroy!
我们可以看到如果析构函数不定义为虚函数,此时执行析构的只有基类,而派生类没有完成析构。我们将析构函数定义为虚函数,在执行析构时,则根据对象的类型来执行析构函数,此时派生类的资源得到释放
#include <iostream>
using namespace std;
class A {
private:
int val;
public:
virtual ~A() {
cout<<"A destroy!"<<endl;
}
};
class B: public A {
private:
int *arr;
public:
B() {
arr = new int[10];
}
virtual ~B() {
cout<<"B destroy!"<<endl;
delete arr;
}
};
int main() {
A *base = new B();
delete base;
return 0;
}
// B destroy!
// A destroy!
8 多重继承的常见问题及避免方法
多重继承(多继承):是指从多个直接基类中产生派生类。多重继承容易出现命名冲突和数据冗余问题。
程序示例如下:
#include <iostream>
using namespace std;
// 间接基类
class Base1
{
public:
int var1;
};
// 直接基类
class Base2 : public Base1
{
public:
int var2;
};
// 直接基类
class Base3 : public Base1
{
public:
int var3;
};
// 派生类
class Derive : public Base2, public Base3
{
public:
void set_var1(int tmp) { var1 = tmp; } // error: reference to 'var1' is ambiguous. 命名冲突
void set_var2(int tmp) { var2 = tmp; }
void set_var3(int tmp) { var3 = tmp; }
void set_var4(int tmp) { var4 = tmp; }
private:
int var4;
};
int main()
{
Derive d;
return 0;
}
上述程序的继承关系如下:(菱形继承)

上述代码中存的问题:
对于派生类 Derive 上述代码中存在直接继承关系和间接继承关系。
- 直接继承:Base2 、Base3
- 间接继承:Base1
对于派生类中继承的的成员变量 var1 ,从继承关系来看,实际上保存了两份,一份是来自基类 Base2,一份来自基类 Base3。因此,出现了命名冲突。
1、解决方法:显式声明出现冲突的成员变量来源于哪个类。
#include <iostream>
using namespace std;
// 间接基类
class Base1
{
public:
int var1;
};
// 直接基类
class Base2 : public Base1
{
public:
int var2;
};
// 直接基类
class Base3 : public Base1
{
public:
int var3;
};
// 派生类
class Derive : public Base2, public Base3
{
public:
void set_var1(int tmp) { Base2::var1 = tmp; } // 这里声明成员变量来源于类 Base2,当然也可以声明来源于类 Base3
void set_var2(int tmp) { var2 = tmp; }
void set_var3(int tmp) { var3 = tmp; }
void set_var4(int tmp) { var4 = tmp; }
private:
int var4;
};
int main()
{
Derive d;
return 0;
}
2、解决方法: 虚继承
#include <iostream>
using namespace std;
// 间接基类,即虚基类
class Base1
{
public:
int var1;
};
// 直接基类
class Base2 : virtual public Base1 // 虚继承
{
public:
int var2;
};
// 直接基类
class Base3 : virtual public Base1 // 虚继承
{
public:
int var3;
};
// 派生类
class Derive : public Base2, public Base3
{
public:
void set_var1(int tmp) { var1 = tmp; }
void set_var2(int tmp) { var2 = tmp; }
void set_var3(int tmp) { var3 = tmp; }
void set_var4(int tmp) { var4 = tmp; }
private:
int var4;
};
int main()
{
Derive d;
return 0;
}
类之间的继承关系:

由于使用多重继承很容易出现二义性的问题,将使得程序调试和维护工作变得非常复杂,C++ 之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。
9 深拷贝与浅拷贝
深拷贝和浅拷贝的区别
如果一个类拥有资源,该类的对象进行复制时,如果资源重新分配,就是深拷贝,否则就是浅拷贝。
深拷贝:该对象和原对象占用不同的内存空间,既拷贝存储在栈空间中的内容,又拷贝存储在堆空间中的内容。
浅拷贝:该对象和原对象占用同一块内存空间,仅拷贝类中位于栈空间中的内容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1SMHEBnv-1676377210026)(…/…/…/AppData/Roaming/Typora/typora-user-images/image-20230213175533920.png)]
当类的成员变量中有指针变量时,最好使用深拷贝。因为当两个对象指向同一块内存空间,如果使用浅拷贝,当其中一个对象的删除后,该块内存空间就会被释放,另外一个对象指向的就是垃圾内存。
#include <iostream>
using namespace std;
class Test
{
private:
int *p;
public:
Test(int tmp)
{
this->p = new int(tmp);
cout << "Test(int tmp)" << endl;
}
~Test()
{
if (p != NULL)
{
delete p;
}
cout << "~Test()" << endl;
}
};
int main()
{
Test ex1(10);
Test ex2 = ex1; //编译器默认的是浅拷贝
return 0;
}
/*
运行结果:
Test(int tmp)
~Test()
*/
深拷贝
#include <iostream>
using namespace std;
class Test
{
private:
int *p;
public:
Test(int tmp)
{
p = new int(tmp);
cout << "Test(int tmp)" << endl;
}
~Test()
{
if (p != NULL)
{
delete p;
}
cout << "~Test()" << endl;
}
Test(const Test &tmp) // 定义拷贝构造函数
{
p = new int(*tmp.p); //这里用的是*tmp
cout << "Test(const Test &tmp)" << endl;
}
};
int main()
{
Test ex1(10);
Test ex2 = ex1;
return 0;
}
/*
Test(int tmp)
Test(const Test &tmp)
~Test()
~Test()
*/
编译器生成的默认拷贝函数均大部分都是浅拷贝,所有在特定场景下需要禁止编译器生成默认拷贝构造函数。在遇到需要使用堆内存的构造函数中,我们需要特别注意浅拷贝和深拷贝的使用方式,防止两个不同的对象指向同一块内存区域。
10 单继承和多继承的虚函数表结构
编译器将虚函数表的指针放在类的实例对象的内存空间中,该对象调用该类的虚函数时,通过指针找到虚函数表,根据虚函数表中存放的虚函数的地址找到对应的虚函数。
如果派生类没有重新定义基类的虚函数 A,则派生类的虚函数表中保存的是基类的虚函数 A 的地址,也就是说基类和派生类的虚函数 A 的地址是一样的。
如果派生类重写了基类的某个虚函数 B,则派生的虚函数表中保存的是重写后的虚函数 B 的地址,也就是说虚函数 B 有两个版本,分别存放在基类和派生类的虚函数表中。
如果派生类重新定义了新的虚函数 C,派生类的虚函数表保存新的虚函数 C 的地址。
1、单继承无虚函数覆盖的情况:
#include <iostream>
#include <memory>
using namespace std;
typedef void (*func)(void);
#include <iostream>
using namespace std;
class Base
{
public:
virtual void B_fun1() { cout << "Base::B_fun1()" << endl; }
virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};
class Derive : public Base
{
public:
virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
void printVtable(unsigned long *vptr, int offset) {
func fn = (func)*((unsigned long*)(*vptr) + offset);
fn();
}
int main()
{
Base *p = new Derive();
p->B_fun1(); // Base::B_fun1()
unsigned long* vPtr = (unsigned long*)(p);
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
printVtable(vPtr, 3);
printVtable(vPtr, 4);
printVtable(vPtr, 5);
cout<<sizeof(Base)<<endl; // 8
cout<<sizeof(Derive)<<endl; // 8
return 0;
}
/*
Base::B_fun1()
Base::B_fun1()
Base::B_fun2()
Base::B_fun3()
Derive::D_fun1()
Derive::D_fun2()
Derive::D_fun3()
8
8
*/
基类和派生类的继承关系:
基类的虚函数表:
派生类的虚函数表:
2、单继承有虚函数覆盖的情况:
#include <iostream>
#include <memory>
using namespace std;
typedef void (*func)(void);
class Base
{
public:
virtual void fun1() { cout << "Base::fun1()" << endl; }
virtual void B_fun2() { cout << "Base::B_fun2()" << endl; }
virtual void B_fun3() { cout << "Base::B_fun3()" << endl; }
};
class Derive : public Base
{
public:
virtual void fun1() { cout << "Derive::fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
void printVtable(unsigned long *vptr, int offset) {
func fn = (func)*((unsigned long*)(*vptr) + offset);
fn();
}
int main()
{
Base *p = new Derive();
unsigned long* vPtr = (unsigned long*)(p);
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
printVtable(vPtr, 3);
printVtable(vPtr, 4);
cout<<sizeof(Base)<<endl; // 8
cout<<sizeof(Derive)<<endl; // 8
return 0;
}
/*
Derive::fun1()
Base::B_fun2()
Base::B_fun3()
Derive::D_fun2()
Derive::D_fun3()
8
8
*/
派生类的虚函数表:
#include <iostream>
using namespace std;
class Base1
{
public:
virtual void B1_fun1() { cout << "Base1::B1_fun1()" << endl; }
virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; }
virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; }
};
class Base2
{
public:
virtual void B2_fun1() { cout << "Base2::B2_fun1()" << endl; }
virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; }
virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; }
};
class Base3
{
public:
virtual void B3_fun1() { cout << "Base3::B3_fun1()" << endl; }
virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; }
virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; }
};
class Derive : public Base1, public Base2, public Base3
{
public:
virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
typedef void (*func)(void);
void printVtable(unsigned long *vptr, int offset) {
func fn = (func)*((unsigned long*)(*vptr) + offset);
fn();
}
int main(){
Base1 *p = new Derive();
unsigned long* vPtr = (unsigned long*)(p);
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
printVtable(vPtr, 3);
printVtable(vPtr, 4);
printVtable(vPtr, 5);
vPtr++;
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
vPtr++;
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
cout<<sizeof(Base1)<<endl; // 8
cout<<sizeof(Base2)<<endl; // 8
cout<<sizeof(Base3)<<endl; // 8
cout<<sizeof(Derive)<<endl; // 8
return 0;
}
/*
Base1::B1_fun1()
Base1::B1_fun2()
Base1::B1_fun3()
Derive::D_fun1()
Derive::D_fun2()
Derive::D_fun3()
Base2::B2_fun1()
Base2::B2_fun2()
Base2::B2_fun3()
Base3::B3_fun1()
Base3::B3_fun2()
Base3::B3_fun3()
8
8
8
24
*/
派生类的虚函数表:(基类的顺序和声明的顺序一致)

4、多继承有虚函数覆盖的情况:
#include <iostream>
using namespace std;
class Base1
{
public:
virtual void fun1() { cout << "Base1::fun1()" << endl; }
virtual void B1_fun2() { cout << "Base1::B1_fun2()" << endl; }
virtual void B1_fun3() { cout << "Base1::B1_fun3()" << endl; }
};
class Base2
{
public:
virtual void fun1() { cout << "Base2::fun1()" << endl; }
virtual void B2_fun2() { cout << "Base2::B2_fun2()" << endl; }
virtual void B2_fun3() { cout << "Base2::B2_fun3()" << endl; }
};
class Base3
{
public:
virtual void fun1() { cout << "Base3::fun1()" << endl; }
virtual void B3_fun2() { cout << "Base3::B3_fun2()" << endl; }
virtual void B3_fun3() { cout << "Base3::B3_fun3()" << endl; }
};
class Derive : public Base1, public Base2, public Base3
{
public:
virtual void fun1() { cout << "Derive::fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
virtual void D_fun3() { cout << "Derive::D_fun3()" << endl; }
};
typedef void (*func)(void);
void printVtable(unsigned long *vptr, int offset) {
func fn = (func)*((unsigned long*)(*vptr) + offset);
fn();
}
int main(){
Base1 *p1 = new Derive();
Base2 *p2 = new Derive();
Base3 *p3 = new Derive();
p1->fun1(); // Derive::fun1()
p2->fun1(); // Derive::fun1()
p3->fun1(); // Derive::fun1()
unsigned long* vPtr = (unsigned long*)(p1);
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
printVtable(vPtr, 3);
printVtable(vPtr, 4);
vPtr++;
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
vPtr++;
printVtable(vPtr, 0);
printVtable(vPtr, 1);
printVtable(vPtr, 2);
cout<<sizeof(Base1)<<endl; // 8
cout<<sizeof(Base2)<<endl; // 8
cout<<sizeof(Base3)<<endl; // 8
cout<<sizeof(Derive)<<endl; // 8
return 0;
}
/*
Derive::fun1()
Derive::fun1()
Derive::fun1()
Derive::fun1()
Base1::B1_fun2()
Base1::B1_fun3()
Derive::D_fun2()
Derive::D_fun3()
Derive::fun1()
Base2::B2_fun2()
Base2::B2_fun3()
Derive::fun1()
Base3::B3_fun2()
Base3::B3_fun3()
8
8
8
24
*/
基类和派生类的关系:

派生类的虚函数表:

(这里应该虚函数表中都是Derive::fun1())
11 如何禁止构造函数的使用
为类的构造函数增加 = delete 修饰符,可以达到虽然声明了构造函数但禁止使用的目的。
#include <iostream>
using namespace std;
class A {
public:
int var1, var2;
A(){
var1 = 10;
var2 = 20;
}
A(int tmp1, int tmp2) = delete;
};
int main()
{
A ex1;
A ex2(12,13); // error: use of deleted function 'A::A(int, int)'
return 0;
}
如果我们仅仅将构造函数设置为私有,类内部的成员和友元还可以访问,无法完全禁止。而在 C++11 以后,在成员函数声明后加 "= delete"则可以禁止该函数的使用,而需要保留的加 “= default”。
12 什么是类的默认构造函数
**默认构造函数(default constructor)**就是在没有显式提供初始化式时调用的构造函数。它由不带参数的构造函数,或者为所有的形参提供默认实参的构造函数定义。如果定义某个类的变量时没有提供初始化时就会使用默认构造函数。
1、用户定义的默认构造函数:
- 用户自定义的不带参数的构造函数:
#include <iostream>
using namespace std;
class A
{
public:
A(){ // 类的默认构造函数
var = 10;
c = 'q';
}
int var;
char c;
};
int main()
{
A ex;
cout << ex.c << endl << ex.var << endl;
return 0;
}
/*
运行结果:
q
10
*/
说明:上述程序中定义变量 ex 时,未提供任何实参,程序运行时会调用默认的构造函数。
- 用户自定义的构造函数,但为所有形参提供默认值的构造函数:
#include <iostream>
using namespace std;
class A
{
public:
A(int _var = 10, char _c = 'q'){ // 类的默认构造函数,需要注意函数参数有默认值
var = _var;
c = _c;
}
int var;
char c;
};
int main()
{
A ex;
cout << ex.c << endl << ex.var << endl;
return 0;
}
/*
运行结果:
q
10
*/
说明:上述程序中定义变量 ex 时,未提供任何实参,程序运行时会调用所有形参提供默认值的构造函数。
2、编译器自动分配的合成默认构造函数
如果用户定义的类中没有显式的定义任何构造函数,编译器就会自动为该类型生成默认构造函数,称为合成的默认构造函数。
#include <iostream>
using namespace std;
class A
{
public:
int var;
char c;
};
int main()
{
A ex;
cout << ex.c << endl << ex.var << endl;
return 0;
}
/*
运行结果:
0
*/
此时编译器会自动为 A 分配一个默认的构造函数,在上述示例中,类 A 中的变量 c 默认赋值为 \0,var 默认赋值为 0。
一般情况下,如果类中包含内置或复合类型的成员,则该类就不应该依赖于合成的默认构造函数,它应该定义自己的构造函数来初始化这些成员。多数情况下,编译器为类生成一个公有的默认构造函数,只有下面两种情况例外:
- 一个类显式地声明了任何构造函数,编译器不生成公有的默认构造函数。在这种情况下,如果程序需要一个默认构造函数,需要由类的设计者提供。
- 一个类声明了一个非公有的默认构造函数,编译器不会生成公有的默认构造函数。
在大多数情况下,C++ 编译器为未声明构造函数之 class 合成一个默认构造函数: - 如果该类没有任何构造函数,但是包含一个对象类型的成员变量,且该变量有一个显式的默认构造函数;
- 如果该类没有任何构造函数,但是其父类含有显式的默认构造函数;
- 如果该类没有任何构造函数,但是含有(或父类含有)虚函数;
- 如果该类没有任何构造函数,但是带有一个虚基类;
13 如何减少构造函数开销
在构造函数时尽量使用类初始化列表,会减少调用默认的构造函数产生的开销,
具体原因可以参考本章《为什么用成员初始化列表会快一些?》这个问题。
class A
{
private:
int val;
public:
A()
{
cout << "A()" << endl;
}
A(int tmp)
{
val = tmp;
cout << "A(int " << val << ")" << endl;
}
};
class Test1
{
private:
A ex;
public:
// 成员列表初始化方式,注意末尾没有分号;
Test1(): ex(1) {}
};
14 C++ 类对象的初始化顺序
1、构造函数调用顺序:
按照派生类继承基类的顺序,即派生列表中声明的继承顺序,依次调用基类的构造函数;
在有虚继承和一般继承存在的情况下,优先虚继承。比如虚继承:class C: public B, virtual public A,
此时应当先调用 A 的构造函数,再调用 B 的构造函数。
按照派生类中成员变量的声明顺序,依次调用派生类中成员变量所属类的构造函数;
执行派生类自身的构造函数。
2、类对象的初始化顺序:
按照构造函数的调用顺序,调用基类的构造函数
按照成员变量的声明顺序,调用成员变量的构造函数函数,成员变量的初始化顺序与声明顺序有关;
调用该类自身的构造函数;
析构顺序和类对象的初始化顺序相反。
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "A()" << endl; } //构造函数
~A() { cout << "~A()" << endl; }//析构函数
};
class B
{
public:
B() { cout << "B()" << endl; } //构造函数
~B() { cout << "~B()" << endl; }//析构函数
};
class Test : public A, public B // 派生列表
{
public:
Test() { cout << "Test()" << endl; }
~Test() { cout << "~Test()" << endl; }
private:
B ex1;
A ex2;
};
int main()
{
Test ex;
return 0;
}
/*
运行结果:
A()
B()
B()
A()
Test()
~Test()
~A()
~B()
~B()
~A()
*/
程序运行结果分析:
- 首先调用基类 A 和 B 的构造函数,按照派生列表 public A, public B 的顺序构造;
- 然后调用派生类 Test 的成员变量 ex1 和 ex2 的构造函数,按照派生类中成员变量声明的顺序构造;
最后调用派生类的构造函数;
接下来调用析构函数,和构造函数调用的顺序相反。
3、类的成员初始化:
类中可能含有静态变量和全局变量,由于静态变量和全局变量都被放在静态存储区,他们的初始化在 main 函数执行之前已被初始化,且 static 变量必须在类外进行初始化。
成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了。
如果类不使用初始化列表初始化,而在类的构造函数内部进行初始化时,此时成员变量的初始化顺序与构造函数中代码逻辑有关。
类成员在定义时,是不能初始化的
类中 const 成员常量必须在构造函数初始化列表中初始化。
类中 static 成员变量,必须在类外初始化。
15 成员初始化列表效率高的原因
对象的成员函数数据类型可分为语言内置类型和用户自定义类,**对于用户自定义类型,利用成员初始化列表效率高。用户自定义类型如果使用类初始化列表,直接调用该成员变量对应的构造函数即完成初始化;**如果在构造函数中初始化,由于 C++ 规定对象的成员变量的初始化动作发生在进入自身的构造函数本体之前,那么在执行构造函数之前首先调用默认的构造函数为成员变量设初值,在进入函数体之后,再显式调用该成员变量对应的构造函数。
因此使用列表初始化会减少调用默认的构造函数的过程,效率更高一些。
直接看下面的代码更清晰
#include <iostream>
using namespace std;
class A
{
private:
int val;
public:
A()
{
cout << "A()" << endl;
}
A(int tmp)
{
val = tmp;
cout << "A(int " << val << ")" << endl;
}
};
class Test1
{
private:
A ex;
public:
Test1() : ex(1) // 成员列表初始化方式
{
}
};
class Test2
{
private:
A ex;
public:
Test2() // 函数体中赋值的方式
{
ex = A(2);
}
};
int main()
{
Test1 ex1;
cout << endl;
Test2 ex2;
return 0;
}
/*
运行结果:
A(int 1)
A()
A(int 2)
*/
16 友元函数的作用及使用场景
友元函数的作用:友元(friend)提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。通过友元,一个普通的函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。
使用场景:
1、普通函数定义为类的友元函数
使得普通函数能够访问该类的私有成员和保护成员。
#include <iostream>
using namespace std;
class A
{
friend ostream &operator<<(ostream &_cout, const A &tmp); // 声明为类的友元函数,ostream以引用的方式返回
public:
A(int tmp) : var(tmp){}
private:
int var;
};
ostream &operator<<(ostream& _cout, const A &tmp) //一个main文件中只能有一个cout
{
_cout << tmp.var;
return _cout;
}
int main()
{
A ex(4);
cout << ex << endl; // 4
return 0;
}
2、友元类
由于类的 private 和 protected 成员变量只能由类的成员函数访问或者派生类访问,友元类则提供提供一种通用的方法,使得不同类之间可以访问其 private 和 protected 成员变量,用于不同类之间共享数据。
#include <iostream>
using namespace std;
class A
{
friend class B;
public:
A() : var(10){}
A(int tmp) : var(tmp) {}
void fun()
{
cout << "fun():" << var << endl;
}
private:
int var;
};
class B
{
public:
B() {}
void fun()
{
cout << "fun():" << ex.var << endl; // 访问类 A 中的私有成员
}
private: //类A作为B中的数据成员,A中的var变量是私有的,只能在类中进行访问,这是需要利用友元来获取访问权限
A ex;
};
int main()
{
B ex;
ex.fun(); // fun():10
return 0;
}
17 静态绑定和动态绑定的实现
为了支持c++的多态性,才用了动态绑定和静态绑定。理解他们的区别有助于更好的理解多态性,以及在编程的过程中避免犯错误。
需要理解四个名词:
- 1、对象的静态类型:对象在声明时采用的类型。是在编译期确定的。
- 2、对象的动态类型:目前所指对象的类型。是在运行期决定的。对象的动态类型可以更改,但是静态类型无法更改。
关于对象的静态类型和动态类型,看一个示例:
class B { };
class C : public B { };
class D : public B { };
B* pb = new D();
D* pD = new D(); //等价于B* pD = new D();
/*
pD的静态类型是它声明的类型D*,动态绑定:D* B* pB = pD;
pB的静态类型是它声明的类型B*,动态类型是pB所指向的对象pD的类型D*
C* pC = new C(); pB = pC;
pB的动态类型是可以更改的,现在它的动态类型是C*
*/
3、静态绑定:绑定的是对象的静态类型,某特性(比如函数)依赖于对象的静态类型,发生在编译期。
4、动态绑定:绑定的是对象的动态类型,某特性(比如函数)依赖于对象的动态类型,发生在运行期。
class B { void DoSomething() {cout << "B::DoSomething()" << endl;}; virtual void vfun() {cout << "B::vfun()" << endl;}; //虚函数 }; class C : public B { void DoSomething() {cout << "C::DoSomething()" << endl;}; //首先说明一下,这个子类重写父类的no-virtual函数,这是一个不好的设计,会导致名称遮掩;这里只是为了说明动态绑定和静态绑定才这样使用。 virtual void vfun() {cout << "C::vfun()" << endl;};; }; class D : public B { void DoSomething() {cout << "D::DoSomething()" << endl;}; virtual void vfun() {cout << "D::vfun()" << endl;};; }; D* pD = new D(); B* pB = pD; pD->DoSomething(); //D::DoSomething() pB->DoSomething(); //注意!!! B::DoSomething() pD->vfun(); //D::vfun() pB->vfun(); //D::vfun()
让我们看一下,pD->DoSomething()和pB->DoSomething()调用的是同一个函数吗?
不是的,虽然pD和pB都指向同一个对象。因为函数DoSomething是一个no-virtual函数,它是静态绑定的,也就是编译器会在编译期根据对象的静态类型来选择函数。pD的静态类型是D*,那么编译器在处理pD->DoSomething()的时候会将它指向D::DoSomething()。同理,pB的静态类型是B*,那pB->DoSomething()调用的就是B::DoSomething()。
让我们再来看一下,pD->vfun()和pB->vfun()调用的是同一个函数吗?
是的。因为vfun是一个虚函数,它动态绑定的,也就是说它绑定的是对象的动态类型,pB和pD虽然静态类型不同,但是他们同时指向一个对象,他们的动态类型是相同的,都是D*,所以,他们的调用的是同一个函数:D::vfun()。
上面都是针对对象指针的情况,对于引用(reference)的情况同样适用。
指针和引用的动态类型和静态类型可能会不一致,但是对象的动态类型和静态类型是一致的。
D.DoSomething()和D.vfun()永远调用的都是D::DoSomething()和D::vfun()。
至于哪些是动态绑定,哪些是静态绑定,有篇文章总结的非常好:
我总结了一句话:只有虚函数才使用的是动态绑定,其他的全部是静态绑定。目前我还没有发现不适用这句话的,如果有错误,希望你可以指出来。
特别需要注意的地方
当缺省参数和虚函数一起出现的时候情况有点复杂,极易出错。我们知道,虚函数是动态绑定的,但是为了执行效率,缺省参数是静态绑定的。
class B {
virtual void vfun(int i = 10);
}
class D : public B {
virtual void vfun(int i = 20);
}
D* pD = new D();
B* pB = pD;
pD->vfun();
pB->vfun();
有上面的分析可知pD->vfun()和pB->vfun()调用都是函数D::vfun(),但是他们的缺省参数是多少?
分析一下,缺省参数是静态绑定的,pD->vfun()时,pD的静态类型是D*,所以它的缺省参数应该是20;同理,pB->vfun()的缺省参数应该是10。
对于这个特性,估计没有人会喜欢。所以,记住:
“绝不重新定义继承而来的缺省参数(Never redefine function’s inherited default parameters value.)”
静态类型和动态类型:
静态类型:变量在声明时的类型,是在编译阶段确定的。静态类型不能更改。
动态类型:目前所指对象的类型,是在运行阶段确定的。动态类型可以更改。
静态绑定和动态绑定:
静态绑定是指程序在编译阶段确定对象的类型(静态类型)。
动态绑定是指程序在运行阶段确定对象的类型(动态类型)。
静态绑定和动态绑定的区别:
发生的时期不同:静态绑定是指程序在编译阶段确定对象的类型(静态类型),动态绑定是指程序在运行阶段确定对象的类型(动态类型)。
对象的静态类型不能更改,动态类型可以更改。
注:对于类的成员函数,只有虚函数是动态绑定,其他都是静态绑定。
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun() {
cout << "Base::fun()" << endl;
}
};
class Derive : public Base
{
public:
void fun() {
cout << "Derive::fun()";
}
};
int main()
{
Base *p = new Derive(); // p 的静态类型是 Base*,动态类型是 Derive*
p->fun(); // fun 是虚函数,运行阶段进行动态绑定
return 0;
}
/*
Derive::fun()
*/
- 动态绑定的实现原理:
以下程序示例:
#include <iostream>
using namespace std;
class A{
private:
int a1;
int a2;
public:
virtual void display(){ cout<<"A::display()"<<endl;}
virtual void clone(){ cout<<"A::clone()"<<endl;}
};
class B: public A{
private:
int b;
public:
virtual void display(){ cout<<"B::display()"<<endl;} override
virtual void init(){ cout<<"B::init()"<<endl;}
};
class C: public B{
private:
int c;
public:
virtual void display(){ cout<<"C::display()"<<endl;} override
virtual void execute(){ cout<<"C::execute()"<<endl;}
virtual void init(){cout<<"C::init()"<<endl;} override
};
int main() {
A *p1 = new B();
A *p2 = new C();
p1->display();
p2->display();
return 0;
}
/*
B::display()
C::display()
*/
我们对上述程序进行编译,并查看这里给出 A, B, C 三个类的虚函数表,如下图所示:

可以得出以下结论:
- 类的内存占用由成员变量和指向虚函数表的指针组成,同时派生类的成员变量是会把基类的成员变量都继承的
- 同名虚函数在基类和派生类中的虚函数表中,在虚函数表中偏移位置是一致的,图 A,B,C 的 display 的偏移位置都为 0。同样名称的虚函数,在基类中定义的虚函数与派生类中定义的虚函数,在虚函数表中的偏移量都是一致的,只有这样才能保证动态绑定。
- 如果派生类中定义了与基类同名的虚函数,那么派生类的虚函数表中响应函数的入口地址会被替换成覆盖后的函数的地址。
- 一旦有新的虚函数定义,会加入到当前虚函数表的末端。
- 派生类的成员变量顺序也按照声明的顺序依次在内存中分配。
我们可以分以下动态绑定的实现:
- 当我们用虚函数表指针去查找虚函数表中对应的函数的地址时,此时首先会找到函数地址的在虚函数表中的索引,这里 display 索引是 0。
- 然后编译器会做一个替换,
(*(p->vptr)[0]),找到 p 指针的函数入口地址。 - 程序运行后会执行这条语句
*(p->vptr)[0](),完成函数的调用,实际即完成了动态绑定。
18 编译时多态和运行时多态的区别
1、编译时多态和运行时多态
编译时多态:在程序编译过程中出现,发生在模板和函数重载中(泛型编程)。实际在编译器内部看来不管是重载还是模板,编译器内部都会生成不同的函数,在代码段中分别装有两个函数的不同实现。
运行时多态:运行时多态也称动态绑定,在程序运行过程中出现,发生在继承体系中,是指通过基类的指针或引用访问派生类中的虚函数。
2、编译时多态和运行时多态的区别:
**时期不同:**编译时多态发生在程序编译过程中,运行时多态发生在程序的运行过程中;
**实现方式不同:**编译时多态运用泛型编程来实现,运行时多态借助虚函数表来实现。