一:构造函数
基本构造函数:
执行过程:1)传参 2)给类数据成员开辟空间 3)执行冒号语法给数据成员初始化 4)执行构造函数括号里面的内容
例如:
class Base{
public:
# 冒号语法后面的内容相当于int a = 10;(初始化)
Base(int var) : m_Var(var){
}
private:
int m_Var;
};
拷贝构造函数:
一般不需要自己编写,系统默认的拷贝构造函数就能抗住了,但是有些情况需要在构造的时候开辟空间,这时候就需要拷贝构造函数了,以下几种情况都会自动调用拷贝构造函数:
1)用一个已有的对象初始化一个新对象的时候
2)将一个对象以值传递的方式传给形参的时候
3)函数返回一个对象的时候
class Base{
public:
Base(int var) : m_Var(var){
}
//拷贝构造函数
Base(Base &ref) : m_Var(ref.m_Var){
}
private:
int m_Var;
};
具体例子:String的拷贝构造函数
String::String(const String &other)
{
int length = strlen(other.m_data);
m_data = new char[length+1]; // 若能加 NULL 判断则更好
strcpy(m_data, other.m_data);
}
普通派生类构造函数:
执行过程:1)传参 2)根据继承时的声明顺序构造基类 3)给类数据成员开辟空间 4)执行冒号语法后面的语句 5)执行构造函数函数体语句:
class Base{
public:
Base(int b) : m_b(b){
}
private:
int m_b;
};
class Derived : public Base{
public:
//普通派生类构造函数的写法
Derived(int b, int d) : Base(b), m_d(d){
}
private:
int m_d;
};
虚继承的派生类构造函数:
虚继承主要是针对多继承时,出现二义性问题而提出的。比如,如下代码就需要用到虚继承,否则的话Derived类继承时,Base类就会不明确,执行步骤为:1)传参 2)创建基类,注意这时候需要显示创建所有“有参构造函数”的基类,包括直接基类,间接基类。 3)给类数据成员开辟空间 4)执行冒号语法 5)执行构造函数函数体;
class Base{
public:
Base(int b) : m_b(b){
}
private:
int m_b;
};
class Base1 : virtual public Base{
public:
Base1(int b, int b1) : Base(b), m_b1(b1) {
}
private:
int m_b1;
};
class Base2 : virtual public Base{
public:
Base2(int b, int b2) : Base(b), m_b2(b2){
}
private:
int m_b2;
};
//虚继承,避免二义性
class Derived : public Base1, public Base2{
public:
Derived(int b, int b1, int b2, int d) : Base(b), Base1(b, b1), Base2(b, b2), m_d(d){
//注意冒号语法后面的顺序无所谓,创造基类是按照上面的继承声明顺序来进行的...
}
private:
int m_d;
};
二:虚析构函数
虚析构一般伴随着多态而产生,多态主要方式就是用基类的指针或引用指向或引用派生类,而形成多态。但是这样就会存在一个问题,当我们析构的时候,由于是基类的指针,就会调用的是基类的构造函数,从而造成派生内存溢出。为了解决这个问题,引入了虚析构的概念。将基类的构造函数声明为虚,从而使其在调用析构函数的时候能够准确的调用派生类的析构函数。如下代码必须用到虚析构才能准确的析构派生类,并释放其占有内存:
class Base{
public:
Base(int b) : m_b(b) {
}
//虚析构,使基类指针能准确的释放所指向的派生类里面的内容
virtual ~Base(){
}
private:
int m_b;
};
class Derived : public Base{
public:
Derived(int b, char *pStr) : Base(b){
m_pStr = new char[strlen(pStr)+1];
strcpy(m_pStr,pStr);
}
~Derived(){
delete m_pStr;
m_pStr = NULL;
}
private:
char *m_pStr;
};
int main(void) {
char *pStr = "abcdefg";
Base *b = new Derived(1,pStr);
delete b;
return 0;
}
以上引用该博客
三:虚函数
虚函数的基础这里不做记录,主要是整理一下对虚函数的理解。
虚函数很重要的一个作用就是实现多态,其实现过程为通过父类指针指向子类对象,单就父类指针指向子类对象时应当注意的是:
1,如果以一个基础类指针指向一个衍生类对象(派生类对象),那么经由该指针只能访问基础类定义的函数(静态联翩)
2,如果以一个衍生类指针指向一个基础类对象,必须先做强制转型动作(explicit cast),这种做法很危险,也不符合生活习惯,在程序设计上也会给程序员带来困扰。(一般不会这么去定义)
3,如果基础类和衍生类定义了相同名称的成员函数,那么通过对象指针调用成员函数时,到底调用那个函数要根据指针的原型来确定,而不是根据指针实际指向的对象类型确定
关键字virtual只用在类定义里的函数声明中,写函数体时不用
虚函数就是为了解决1、3的问题。
在使用指向子类对象的基类指针,并调用子类中的覆盖函数时,如果该函数不是虚函数,那么将调用基类中的该函数;如果该函数是虚函数,则会调用子类中的该函数
当一个类存在虚函数时,通过该类创建的对象实例,会在内存空间的前4字节保存一个指向虚函数表的指针__vfptr
。即使该类的父类没有虚函数(也就没有形成虚表),那么尽管继承了父类的成员变量,这些成员变量的位置依然排在指向虚函数标的指针__vfptr后面。__vfptr
指向的虚函数表,是类独有的,而且被该类的所有对象共享。
class Base{
public:
int base_1;
int base_2;
virtual void base_fun1() {}
virtual void base_fun2() {}
};
如果Base类实例了两个对象b1和b2,他们的内存分布如下:
可以看到&b1和&b2是不同地址的,但是__vfptr的指向却是同一个虚函数表。所以验证了那句:被该类的所有对象共享。其重点就是:
- 她是编译器在编译时期为我们创建好的, 只存在一份
- 定义类对象时, 编译器自动将类对象的__vfptr指向这个虚函数表
子类和父类不是同一类,这个应当注意。
虚函数表的实质,是一个虚函数地址的数组,它包含了类中每个虚函数的地址,既有当前类定义的虚函数,也有覆盖父类的虚函数,也有继承而来的虚函数。虚函数顺序与虚函数在类中的声明位置有关,在类中第一个声明的虚函数在虚表中的位置总是第一个,第二个虚函数则排放在虚表中的第二个位置,依次排放。如果子类中有新虚函的添加,那么直接放在该虚函数的表(数组)的最后,如果子类中重写了父类虚函数的,那么新函数的地址将覆盖原有地址。所以一个类的虚函数数量再怎么增加,他这个类的大小是不变的(其实普通成员函数也不占该类的大小,区别后续),变化的是__vfptr指向的
虚函数表这个数组的大小。
直接调用与间接调用(虚调用)
-
通过类对象的方式调用虚函数称为直接调用,编译器直接生成调用该虚函数的代码
例: -
int main(int argc, char* argv[]){ CTest t; t.ShowInfo(); t.ShowInfo1(); t.ShowInfo2(); return 0; }
观察上面代码的反汇编代码:
CTest t; 008D1A58 lea ecx,[t] 008D1A5B call CTest::CTest (08D1096h) t.ShowInfo(); 008D1A60 lea ecx,[t] 008D1A63 call CTest::ShowInfo (08D10B4h) t.ShowInfo1(); 008D1A68 lea ecx,[t] 008D1A6B call CTest::ShowInfo1 (08D11B3h) t.ShowInfo2(); 008D1A70 lea ecx,[t] 008D1A73 call CTest::ShowInfo2 (08D104Bh) return 0; 008D1A78 xor eax,eax
可以看出VC++编译器生成的直接调用对应虚函数的代码
-
通过指向对象的指针或引用调用虚函数,称为间接调用,编译器生成直接调用虚函数的代码,而是通过虚表指针
取出虚表内的虚函数指针,然后用虚函数指针调用虚函数,例:int main(int argc, char* argv[]){ CTest t; CTest & rt = t; CTest * pt = &t; rt.ShowInfo2(); pt->ShowInfo(); return 0; }
观察上面代码的反汇编代码:
CTest t; 00121A58 lea ecx,[t] 00121A5B call CTest::CTest (0121096h) CTest & rt = t; 00121A60 lea eax,[t] 00121A63 mov dword ptr [rt],eax CTest * pt = &t; 00121A66 lea eax,[t] 00121A69 mov dword ptr [pt],eax rt.ShowInfo2(); 00121A6C mov eax,dword ptr [rt] //取对象t的地址 00121A6F mov edx,dword ptr [eax] //取虚表指针 00121A71 mov esi,esp 00121A73 mov ecx,dword ptr [rt] 00121A76 mov eax,dword ptr [edx+8] //取出ShowInfo2在虚表中的函数指针 00121A79 call eax //调用虚函数ShowInfo2 00121A7B cmp esi,esp 00121A7D call __RTC_CheckEsp (0121140h) pt->ShowInfo(); 00121A82 mov eax,dword ptr [pt] //取对象t的地址 00121A85 mov edx,dword ptr [eax] //取虚表指针 00121A87 mov esi,esp 00121A89 mov ecx,dword ptr [pt] 00121A8C mov eax,dword ptr [edx+4] //取出ShowInfo在虚表中的函数指针 00121A8F call eax //调用虚函数ShowInfo 00121A91 cmp esi,esp 00121A93 call __RTC_CheckEsp (0121140h) return 0; 00121A98 xor eax,eax
在普通成员函数中调用虚函数依然是间接调用
-
例:在CTest类中加入一个如下的成员函数,使用如下代码测试:
void Test() { ShowInfo(); }
int main(int argc, char* argv[]){ CTest t; t.Test(); return 0; }
Test函数对应的反汇编代码如下:
void Test() { ShowInfo(); } 013719D0 push ebp 013719D1 mov ebp,esp 013719D3 sub esp,0CCh 013719D9 push ebx 013719DA push esi 013719DB push edi 013719DC push ecx 013719DD lea edi,[ebp-0CCh] 013719E3 mov ecx,33h 013719E8 mov eax,0CCCCCCCCh 013719ED rep stos dword ptr es:[edi] 013719EF pop ecx 013719F0 mov dword ptr [this],ecx 013719F3 mov eax,dword ptr [this] 013719F6 mov edx,dword ptr [eax] 013719F8 mov esi,esp 013719FA mov ecx,dword ptr [this] //取虚表指针 013719FD mov eax,dword ptr [edx+4] //从虚表中取虚函数ShowInfo的指针 01371A00 call eax //调用虚函数 01371A02 cmp esi,esp 01371A04 call __RTC_CheckEsp (01371140h) 01371A09 pop edi 01371A0A pop esi 01371A0B pop ebx 01371A0C add esp,0CCh 01371A12 cmp ebp,esp 01371A14 call __RTC_CheckEsp (01371140h) 01371A19 mov esp,ebp 01371A1B pop ebp 01371A1C ret
可以看出在成员函数中调用虚函数是间接调用,也是根据虚表来调用
-
在构造函数和析构函数中不会通过虚表来调用虚函数,而是直接在编译时生成直接调用虚函数的代码
例:CTest():m_nTest(1){ ShowInfo1(); } ~CTest(){ ShowInfo1(); }
对应反汇编代码如下:
CTest():m_nTest(1) 000D181C mov eax,dword ptr [this] 000D181F mov dword ptr [eax+4],1 ShowInfo1(); 000D1826 mov ecx,dword ptr [this] 000D1829 call CTest::ShowInfo1 (0D11C2h)
~CTest()
{ 000D1860 push ebp 000D1861 mov ebp,esp 000D1863 push 0FFFFFFFFh 000D1865 push 0D60D0h 000D186A mov eax,dword ptr fs:[00000000h] 000D1870 push eax 000D1871 sub esp,0CCh 000D1877 push ebx 000D1878 push esi 000D1879 push edi 000D187A push ecx 000D187B lea edi,[ebp-0D8h] 000D1881 mov ecx,33h 000D1886 mov eax,0CCCCCCCCh 000D188B rep stos dword ptr es:[edi] 000D188D pop ecx 000D188E mov eax,dword ptr [__security_cookie (0DB004h)] 000D1893 xor eax,ebp 000D1895 push eax 000D1896 lea eax,[ebp-0Ch] 000D1899 mov dword ptr fs:[00000000h],eax 000D189F mov dword ptr [this],ecx 000D18A2 mov eax,dword ptr [this] 000D18A5 mov dword ptr [eax],offset CTest::`vftable' (0D8B34h) ShowInfo1(); 000D18AB mov ecx,dword ptr [this] 000D18AE call CTest::ShowInfo1 (0D11C2h) //直接调用 }
例子:
class Base{
public:
void func1(){
func2();
}
virtual void func2(){
cout<<"Base::func2()"<<endl;
}
};
class Derived : public Base{
public:
virtual void func2(){
cout<<"Derived:func2()"<<endl;
}
};
int main(){
Derived d;
Base *pBase = &d;
pBase->func1();
return 0;
}
是不是大家觉得 pBase
指针对象虽然指向的是派生类对象,但是派生类里没有 fun1
成员函数,则就调用基类的 fun1
成员函数,Base::fun1()
里又会调用基类的 fun2
成员函数,所以输出结果是Base::fun2()
?假设我把上面的代码转换一下, 大家还觉得输出的是 Base::fun2()
吗:
class Base
{
public:
void fun1()
{
this->fun2(); // this是基类指针,fun2是虚函数,所以是多态
}
}
this
指针的作用就是指向成员函数所作用的对象, 所以非静态成员函数中可以直接使用 this 来代表指向该函数作用的对象的指针。
pBase
指针对象指向的是派生类对象,派生类里没有 fun1
成员函数,所以就会调用基类的 fun1
成员函数,在Base::fun1()
成员函数体里执行 this->fun2()
时,实际上指向的是派生类对象的 fun2
成员函数。
所以正确的输出结果是:
Derived:fun2()
所以我们需要注意:
在非构造函数,非析构函数的成员函数中调用「虚函数」,也是多态!!
虚函数实现多态原因:
将基类指针指向派生类可以实现多态是因为虚函数,但是其整个过程是如何呢?
首先,是基类指针指向派生类,或者说派生类地址赋给基类指针。这中间涉及到了类型的强制转换。首先刨开继承关系,每个类都有相应的大小尺寸,这就要分为从大尺寸到小尺寸的转换和小尺寸到大尺寸的转换。因此在内存上,数据就产生了补零和丢弃两种结果
- 若目标操作数的类型尺寸小于源操作数的类型尺寸,则以目标操作数的类型尺寸为准,目标操作数的超出源操作数类型尺寸的剩余高地址部分的字节数据会被丢弃
- 若目标操作数的类型尺寸大于源操作数的的尺寸类型,则目标操作数的所有字节数据会按低地址到高地址的顺序依次拷贝到目标操作数,目标操作数超出源操作数尺寸的剩余高地址部分保持字节数据,编译器会以字节0填充
因此,两个毫不相关的类实例相互强制转换会产生不可预知的后果,赋值操作符左边的类实例的数据完全被篡改,这是一种非常危险的行为。
对于有继承链相关的类型转换(派生类转为基础类指针,基础类转为派生类很少用,这里不做讨论),其实涉及到了‘对象切片’,切片后的影响为:当将派生类对象分配给基类对象(Upcast)时,派生类对象的所有成员都将复制到基类对象,但派生类原创的成员(属性和方法),这些成员被编译器"阉割"掉。
那么派生通过继承得到的基类成员函数的地址,在对象切片的作用下有三种情况。
- 对于非虚成员函数来说,基类对象只能得到基类原创定义且可被继承的成员函数的地址,派生类原创定义的成员函数的地址,对于upcast操作后的基类对象是不可见的。
- 对于虚成员函数来说,如下三种情况,对于基类对象运行时绑定哪个虚成员函数的地址,是依据填入基类的虚表的函数地址来判断的。
- 若该函数是派生类原创定义的,对于upcast操作后的基类对象是不可见的。
- 若该函数是基类原创定义且未被派生类重写,对于upcast操作后的基类对象,该基类版本的虚函数可见。
- 若该函数是基类原创定义且已被派生类重写,对与upcast操作后的基类对象,该派生类版本的虚成员函数可见。
父类公开或受保护的成员函数(包括虚函数)同样是被派生类继承,但继承的只是父类成员函数的调用权,在继承关系中,派生类从基类继承的成员函数实质上继承的是存储在代码段(Code Segment)内存区中,基类可共享的成员函数的内存地址,因为每个成员函数都有一个唯一的内存地址。引用自该博客
例子:
#include<iostream>
#include<string>
using namespace std;
class Employee{
private:
bool p_isValid=true;
public:
std::string m_name;
double m_salary=1500;
bool m_iService=true;
Employee(){}
Employee(const std::string &name):m_name(name){}
Employee(const std::string &name,double salary):
m_name(name),
m_salary(salary){}
virtual void show_info(){
std::cout<<"显示员工信息"<<std::endl;
}
void show_status(){
std::cout<<"当前员工的账户状态:";
if(!p_isValid){
std::cout<<"已禁用"<<std::endl;
}else{
std::cout<<"已启用"<<std::endl;
}
}
virtual void add_salary(double k){
m_salary+=k;
std::cout<<m_name<<" 加薪后:¥"<<m_salary<<std::endl;
}
void show_salary(){
std::cout<<m_name<<":¥"<<m_salary<<std::endl;
}
virtual void search(){
std::cout<<"查找用户功能"<<std::endl;
}
};
class Manager:public Employee{
public:
int level=0;
std::string group="administrator";
Manager(const std::string &name){
m_name=name;
m_salary=10000;
}
virtual void depart_mgnt(){
std::cout<<"部门管理功能"<<std::endl;
}
void show_undering(){
std::cout<<"直属员工管理"<<std::endl;
}
};
class Supervisor:public Employee{
public:
Supervisor(const std::string &name){
m_name=name;
m_salary=5000;
}
};
int main(void){
std::string e_name="职员";
std::string m_name="经理";
Employee *e=new Employee(e_name);
Manager *m=new Manager(m_name);
Employee *e2=m; //upcast
e2->show_salary();
//类型转换后只有,编译器只认定e2有40个字节
std::cout<<sizeof(*e2)<<std::endl;
//可以正常访问,因为show_salary继承在基类
e2->show_salary();
//不可访问,因为level是Manager类实例原创的数据成员
e2->level;
//不可访问,因为show_undering方法是
//Manager类实例原创的成员函数
e2->show_undering();
}
通过该图可以看出,派生类里面的成员函数和虚函数:只有在基础类里面原创定义的变量被复制进来。尽管虚表指针的值也被复制进来,但虚表内只复制了前三个函数。其实虚表指针指向的是三个元素的数组,同样,类型转换的时候也被切片了三个。
是否有虚成员变量的概念?
抱歉,没有。并且,一定要注意,如果派生类里面的定义的成员变量的名字和基础类一模一样(在虚函数里叫做重写),应该注意不要被迷惑了:
#include
using namespace std ;
class A{
public:
A() { m_c = 'A' ; }
char m_c ;
} ;
class B : public A{
public:
B() { m_c = 'B' ; }
char m_c ;
} ;
int main(){
A a ;
B b ;
A * p = &b ;
cout << p->m_c << endl ;
cout << ((B*)p)->m_c << endl ;
return 0 ;
}
/************输出***************/
A
B
注意,派生类B里面的m_c这个字符,是重新定义而不是继承过来的。
构造和析构函数是否能为虚函数?
虚函数的意义
虚函数的实际意义非常之大。比如在实际开发过程中,会用到别人封装好的框架和类库,我们可以通过继承其中的类,并覆盖基类中的函数,来实现自定义的功能。但是,有些函数是需要框架来调用,并且API需要传入基类指针类型的参数。而使用虚函数就可以,将指向子类对象的基类指针来作为参数传入API,让API能够通过基类指针,来调用我们自定义的子类函数。这就是多态性的真正体现。
重写,重载,重定义区分:
重写(覆盖)
- 是指派生类函数重写(覆盖)基类函数
- 不同的范围,分别位于基类和派生类中
- 函数的名字相同
- 参数相同
- 基类函数必须有virtual关键字
重载
- 成员函数
- 形同的范围(在同一个类中)
- 函数的名字相同
- virtual关键字可有可无
重定义(隐藏)
- 派生类屏蔽了与其同名的基类函数
- 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有关键字,此时,基类的函数被隐藏