C++(多态)

多态就是不同对象去完成某种行为的时候会有不同的状态

多态的使用:

多态的硬性条件:1、虚函数的重写(函数名、参数、返回值)(重写是一种接口继承,重写的是继承父类这个函数的实现);2、 父类的指针或者和引用调用

对于普通函数的继承是一种实现继承,派生类将函数整体全都继承过来了。而对于虚函数的继承派生类只继承了接口,目的是为了重写实现达成多态。

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

class Student : public Person
{
public:
	// 函数名相同,参数相同,返回值相同并且都由virtual修饰,不叫隐藏,叫做重写/覆盖。重写的是实现,继承的是接口。
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)//如果这边的参数类型不是引用而是Person,会导致调用结果都是全价。
//不满足多态---看调用者的类型,调用这个类型的成员函数。都是"买票-全价"
//满足多态--看指向对象的类型,调用指向这个类型的成员函数
{
	p.BuyTicket();//这里会实现多态,Person和Student不同对象调用会有不同的结果
	//多态的要求:1是实现虚函数的重写;2是父类的指针或者引用去调用
}
int main()
{
	Person ps;
	Student st;
	Func(ps);//买票-全价
	Func(st);//买票-半价
	return 0;
}

 多态的使用场景

class Person {
public:
	~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	~Student() { cout << "~Student()" << endl; }
};
int main()
{
	Person* ptr1 = new Person;
	Person* ptr2 = new Student;//父类的指针可以指向子类的对象
	delete ptr1;
	delete ptr2;//子类对象没有调用析构,这是因为delete是由两个部分构成,先调用析构函数,ptr2->destructor()(这里调用的是父类的析构,因为ptr2的类型是父类的指针,普通的调用是按照指针的类型去调用,没有多态的概念。
	//但是我们期望的是按照指向的对象去掉,指向的是子类就按照子类去调用。如何实现了,首先这里满足了采用父类的指针去调用,只要满足虚函数的重写就可以了,但是他俩的函数名不同,一个是~Person()一个是~Student(),于是编译器在底层对析构函数进行特殊处理将析构的函数名都改成了destructor(),
	//就是为了这种场景做准备,只有函数名相同才能构成多态。所以我们将析构函数加上关键字virtual就可以实现多态,按照执政的类型去进行调用。
}

采用多态进行优化

class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
	Person* ptr1 = new Person;
	Person* ptr2 = new Student;//父类的指针可以指向子类的对象
	delete ptr1;
	delete ptr2;
}

多态的特殊情况 

子类的虚函数可以不加virtual

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

class Student : public Person
{
public:
	void BuyTicket() { cout << "买票-半价" << endl; }//子类不加virtual也构成多态,因为重写体现的是接口继承,也就是继承了父类的接口virtual void BuyTicket(),重写实现就可以了。
};
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);//买票-全价
	Func(st);//买票-半价
	return 0;
}

编译器认为虚函数重写是接口继承,也就是子类继承了virtual void BuyTicket()这个函数声明,然后子类只需要重写继承函数父类的函数的实现,这就是接口继承。所以就算子类不加virtual也认为这是虚函数,因为从父类继承下来了virtual void BuyTicket()这个函数声明,我只要将实现改一下就可以构成多态了。

协变 

 还有一个特殊情况是协变,返回值可以不同,但是返回值必须是父子关系的指针或者引用。 

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

class Person {
public:
	virtual Person* BuyTicket()
	//virtual class A* BuyTicket()//返回父子类也不一定得是自己,其它的父子类关系指针或引用也可以,如class A*和class B*也行。
	{ 
		cout << "买票-全价" << endl;
		return nullptr;
	}
};

class Student : public Person {
public:
	virtual Student* BuyTicket()
	//virtual B* BuyTicket() //返回父子类也不一定得是自己,其它的父子类关系指针或引用也可以,如class A*和class B*也行。
	{ 
		cout << "买票-半价" << endl;
		return nullptr;
	}
};
void Func(Person* p)
{
	p->BuyTicket();
	delete p;
}
int main()
{
	Func(new Person);
	Func(new Student);
	return 0;
}

关于多态的题目  

class A
{
public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
    virtual void test() { func(); }//1、func满足虚函数重写,三同条件之一的参数相同指的是参数的类型,子类虽然func()没有加virtual,但是子类继承了父类的接口,重写它的实现就可以了。 2、对于是否满足父类指针调用的条件我们需要分析func()是有谁调用的。该行代码中的func()是this去调用的,而this的类型是A*是父类的指针,编译器是这样理解的this->func(),但是this隐藏了。满足多态。
    //子类对象继承父类对象的函数,并不会修改父类对象的参数类型,这里test()编译器理解的是test(A* this)(this其实是在日常调用中被藏起来了),然后main函数中将子类指针传p递给父类函数test()的A* this指针(继承中切割的语法)
    //test()函数中func()的执行其实是this->func(),所以这里满足多态。
};

class B : public A
{
public:
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }//3、重写的是实现,继承的是接口(包括virtual、缺省值等等,继承的是这一大块virtual void func(int val = 1),所以我的value是1)
};

int main(int argc, char* argv[])
{
    B* p = new B;
    p->test();//结果是B->1
    return 0;
}
class A
{
public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
    virtual void test() { func(); }
};

class B : public A
{
public:
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};

int main(int argc, char* argv[])
{
    A* p = new B;
    p->test();//结果还是是B->1,p虽然是A*类型指针,但是指向的还是B。p还是B对象的地址,所以跟上面的题目没有区别。
    return 0;
}
class A
{
public:
    virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
};

class B : public A
{
public:
    void func(int val = 0) { std::cout << "B->" << val << std::endl; }
    virtual void test() { func(); }
};

int main(int argc, char* argv[])
{
    B* p = new B;
    p->test();//结果是B->0.因为这里不是父类的指针调用func(),不满足多态。正常调用哪个类型就调用谁的,并且正常调用就没有虚函数的重写概念了,也就没有了接口继承这么一说。这里没有多态就不用去谈接口继承。就是当作一个普通函数看待即可。这里是子类指针的调用class B* p。 
    return 0;
}
class Base1 { public:  int _b1; };
class Base2 { public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };//Base1先继承,所以Base1在前面。Derive实例化对象的存储模型当中,Base1先继承的就是先声明的,所以在Derive实例化对象的存储模型当中前部分。
int main() 
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;//p1和p3虽然值是一样的,指向同一个地方,但是权限不一样,P1看的是Base1,P3看的是整体。
	return 0;
}

final()和override()函数

//final:修饰虚函数,表示该虚函数不能再被重写。
class Car
{
public:
	virtual void Drive() final {}
};
class Benz :public Car
{
public:
	virtual void Drive() { cout << "Benz-舒适" << endl; }//会报错无法重写final函数
};
//override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car {
public:
	virtual void Drive() {}
};
class Benz :public Car {
public:
	virtual void Drive() override { cout << "Benz-舒适" << endl; }//如果基类未进行Drive()函数的定义,会报错。
};

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

抽象类(纯虚函数)

//一个类型在现实中没有对应的实体,我们就可以一个类定义为抽象类。
class Car
{
public:
	//纯虚函数——包含纯虚函数的类就是抽象类——不能实例化出对象
	virtual void Drive() = 0;
};
class BMW :public Car//BMW在这里继承了抽象类Car,相当于也包含了纯虚函数也是抽象类。但是我们怎么样才可以让BMW不是抽象类,可以通过对Drive()这个纯虚函数进行重写来解决。重写之后Drive()在该类当中就不是纯虚函数,该类就成了普通类可以实例化对象。
//抽象类的一个作用就是要求其子类必须对该纯虚函数实现重写,否则无法实例化。
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};
int main()
{
	// Car car;
	BMW bmwCar;

	return 0;
}

多态原理

Base的size结果是12而不是8,因为多了一个_vfptr的虚函数表指针,该指针指向的是一个虚函数表,而虚函数表里面存放的是虚函数指针数组。虚函数表其实在编译的过程中就确立好了,重写了就进行覆盖,没有重写就保持原来的样子。

父类对象的虚表里面存的是父类的虚函数指针,子类的对象的虚表里面是子类的虚函数指针。

通过汇编语言可以知道如果不构成多态在编译的时候就可以确定调用的是什么,跟指向没有关系,父类的指针或者引用就调用父类的函数,我就直接在父类里面找到该函数的地址,直接calll就可以了。

如果构成多态, 就不能直接确定地址,因为不确定是调用父类的还是子类的,此时通过汇编语言我们知道采用的方式就是先找该对象的头四个字节(也就是_vfptr找到这个虚表的指针),然后找到虚表,虚表里面存放的是虚函数的指针,通过指针就可以找到虚函数,将虚函数的指针放到eax俩面,call的就是eax。

单说Func函数里面的p->BuyTicket()是无法知道p是什么对象,都是执行一样的汇编指令,但是这一样的汇编指令却可以根据不同的调用对象产生不同的后果(多态),因为他要去找指针p的指向,指向的头四个字节所表示的地址对于父类和子类存放的内容是不一样的。也就是说传不同的对象,他虚表的内容是不一样的,但是去找虚表的过程对于不同对象都是一样的即找头四个字节的指针的指向就可以了,虚函数所在虚表的相对位置对于不同对象都是一样的,多态调用转换成汇编指令对于任何对象都是无感的,看到的都是父类成员,只是这个父类成员只是这个父类成员是父类对象的父类成员还是子类对象当中的父类成员的问题,不同类的对象最后放入eax的地址是不一样的。

重写是语法层的概念,而覆盖是原理层的概念。多态通过汇编语言我们可以看到也是傻傻的指向指令,只是指令比非多态的情况要多上一些,就是去虚表中找虚函数,只是不同类对象虚表不一样,但是通过虚表找虚函数的方法是一样的,也是傻瓜式的执行,在编译的时候已经写死了的,之所以给人一种可以根据实际情况进行调整的感觉是因为它通过设计使得不同对象执行相同的行为有了不同的结果。

那为什么不把虚函数直接存在对象当中呀?因为当同类型的对象多了,其实这些对象那个的虚函数表都是一样的,每个对象只需要多花额外的四个指针存一下虚函数表指针就可以了。虚函数表指针指向一个虚函数表,其实虚函数表是一个虚函数指针数组,它是一个数组,里面存的是虚函数指针。 

虚函数表在编译的过程中就已经确定好了,运行的时候找虚函数表就可以了,不同类对象的虚函数表是不一样的。虚函数表里面有可能有多个虚函数地址,如何知道自己调用的是哪一个虚函数,通过虚函数的声明顺序是第几个,谁先声明谁救在前面,比如该虚函数是第二个声明的就去找第二个函数指针就可以了。

对象也可以切片,为什么对象不可以实现多态?

如果采用指针和引用,指针和引用的切片是将子类对象中属于父类的那一部分切出来,让后再指向我,那么我们看到的还是子类对象的一部分,那么虚表还是子类的。对象的话,子类传给父类进行切片,需要调用拷贝构造,这时候涉及到了一个问题,虚表是否需要拷贝过去,如果不拷贝过去父类对象永远是父类的虚函数,除非传子类对象传给父类的时候除了拷贝成员还要拷贝虚表,这样的话就可以实现多态,但是是敢这样做吗?引用和指针是去切子类对象中父类成员的那一部分,但是对象的话不敢拷贝虚表,因为拷贝之后就乱了,父类对象的虚表现在就不能确定是父类的虚函数还是子类的虚函数。所以对象的切片只拷贝成员不拷贝虚表。

用程序打印虚表

//定义基类和派生类
class Base
{
public:
	Base()
		:_b(10)
	{
		++_b;
	}
	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;
	}

	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};
//用程序打印虚表
typedef void(*VF_PTR)();//函数指针的typedef和其它指针的typedef是不一样的,函数指针typedef不能写成typedef void(*)() VF_PTR这种前面是类型后面是重新定义的名字这种传统写法,需要将重定义的名子放在类型中间 
//void PrintVFTable(VF_PTR table[])//该行代码和下面一个意思,数组传参传的就是首元素的地址。
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];//将函数指针传给变量f,有函数指针就可以如下行代码一样调用函数了。因为我们上面定义的函数都没有给参数和返回值,所以函数指针的类型是统一的。
		f();//调用f函数指针所指向的函数
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	PrintVFTable((VF_PTR*)(*(int*)&b));//只能在32位平台下跑。对象的头四个字节村的是虚函数表指针,也就是一个数组的地址(该数组的元素类型是VF_PTR),其类型可以理解为VF_PTR*。我们要进行解引用才能取到&b(&b就是对象b的地址)所指向的地址的头四个字节的值。
	PrintVFTable((VF_PTR*)(*(int*)&d));//这种写法的问题就是写死了,将&d强转成int*类型在进行解引用只能取到前面四个字节,对于64位下的平台是不适用的,因为再64位下一个指针的大小是8个字节。PrintVFTable((*(VF_PTR**)&d))这种写法在32位和64位都是适用,但是这种方法可读性没有PrintVFTable((VF_PTR*)(*(int*)&d))强。
	//PrintVFTable((*(VF_PTR**)&b));
	//PrintVFTable((*(VF_PTR**)&d));
	return 0;
}



 虚表存放的位置

int main()
{
	Base b;
	Derive d;
	int x = 0;
	static int y = 0;
	int* z = new int;
	const char* p = "xxxxxxxxxxxxxxxxxx";
	printf("栈对象:%p\n", &x);
	printf("堆对象:%p\n", z);
	printf("静态区对象:%p\n", &y);
	printf("常量区对象:%p\n", p);
	printf("b对象虚表:%p\n", *((int*)&b));
	printf("d对象虚表:%p\n", *((int*)&d));
	return 0;
	// 虚表是什么阶段生成的?编译的时候就已经生成好了,因为编译这些函数的地址就已经有了  
	// 对象中虚表指针什么时候初始化的?构造函数的初始化列表阶段
	// 虚表存在哪里?  虚表指针在对象里面,而虚表是在常量区。因为编译好了之后,虚表其实是不会改变了,放在常量区更合理一些。
}

 

内存分为堆、栈、数据段(静态区)(存放静态数据和全局数据) 、代码段(常量区)。通过上面的各个变量位置分析,我们可以证实虚表存放在常量区。

 多继承下的虚表问题

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;
	int bb;
};
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(*VF_PTR)();
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	PrintVFTable((VF_PTR*)(*(int*)&d));//这里对象d有两个虚表,而且不是连续存放的,所以要分成两次打印
	//PrintVFTable((VF_PTR*)(*(int*)((char*)&d+sizeof(Base1))));// 这是自己进行手动的偏移,下面的主动偏移方法更巧妙的利用了切片的原理
	Base2* ptr2 = &d;//由于切片的原因,此时&d的指向的位置会主动偏移,并不是最原始的位置了再赋值给ptr2
	PrintVFTable((VF_PTR*)(*(int*)(ptr2)));
	return 0;
}



func1既重写了Base1,也重写了Base2。但是重写的地址是不一样的,地址虽然不一样,但是两个函数指针指向的函数都是重写的func1,为什么这边要处理成不一样的呢?

int main()
{
	Derive d;
	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
	ptr1->func1();
	ptr2->func1();
	return 0;
}

通过汇编我们可以看到ptr1->func1()是直接jump到0865A10h这个地址,而ptr2->func1()是绕了几个弯在jump到0865A10h这个地址。其实两个函数最终调用的都是同一个函数,只是ptr2->func1()看着像是经过了多层封装的,为什么要这样处理呢?首先我们要知道ecx存的是this指针,ptr1和ptr2调用的都是子类的函数,ptr1调用子类的函数其实这个指针是没有问题的,因为ptr1指向的是子类对象的开始的,调用子类对象的函数这个this指针应该指向的位置是子类对象,ecx拿的就是ptr1的值所以不用做处理。而ptr2指向的子类对象的中间位置,而ecx此时拿的就是ptr2的值很明显是不对的,需要进行修正,而修正ecx(存放this指针)的过程就是之前看似绕弯的过程。
调用的是子类的成员函数,那么this指针指向的应该是真个子类对象,此时ptr1恰好指向的就是子类对象。如果是先继承的Base2再继承的Base1,需要调用的变成了ptr1的调用了。

动态绑定与静态绑定

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载,也就是传不同的参数,通过函数名的修饰规则在编译过程中就进行了相关的匹配。动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。

动态的多态和静态的多态本质上都是在编译的过程中都已经写死了的,只是静态的多态在编译的过程中就已经变成了直接call地址了,但是动态的多态在编译的过程中call的是一个寄存器,寄存器是还要到虚表里面找的,是在运行过程中要去虚表里面找的,但是虚表在编译的时候也是定死的,也就是在运行的过程中才确定的地址的。

 菱形继承下的虚表和虚基表的问题

//虚函数的virtual和虚继承的virtual是没有任何的关联的,一个是虚函数的重写,一个是解决数据冗余和二义性的问题,虽然使用的是同一个关键字,但是二者不一样。
class A
{
public:
	virtual void func1()
	{}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func1()
	{}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func1()
	{}
public:
	int _c;
};
class D : public B, public C
{
public:
	virtual void func1()//B和C都虚继承了A并且重写了func1,但是由于虚继承的关系,D中只能有一个公共的A,导致问题就是class D实例化对象中的A成员中的虚表放的是哪一个func1,编译器决定最终需要class D实例化的对象完成A类中成员函数func1的重写才能搞定这个事情,这样才公平,B和C重写的func1我都不用也就是都不偏袒。所以如果Class D不重写func1编译器是会报错虚函数A::func1()重写不明确。
	{

	}
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

class A
{
public:
	virtual void func1()
	{}
public:
	int _a;
};

class B : virtual public A
{
public:
	virtual void func1()
	{}
	virtual void func2()
	{}
public:
	int _b;
};

class C : virtual public A
{
public:
	virtual void func1()
	{}
	virtual void func3()
	{}
public:
	int _c;
};
class D : public B, public C
{
public:
	virtual void func1()
	{

	}
public:
	int _d;
};
int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

 简答题

内联函数可以是虚函数吗?

内联函数的特点是在调用的地方进行展开,如果真实一个内联函数的话没有必要有地址,所以内联函数不会声明和定义分离,因为分离的话在链接的过程中需要去找地址。所以一个函数成为内联函数,是没有地址的,所以内联函数粗看是没有办法成为虚函数的,因为虚函数需要进虚表就需要虚函数的地址。但是还有一个知识点是内联对于编译器仅仅只是一个建议(inline virtual void func()),如果不符合多态就看函数的长度来判断是否采取内联,符合多态编译器就会忽略内联这个建议。

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

静态成员函数可以不通过对象调用,因为它没有this指针,它并不适合实现多态,虽然静态成员函数也可以通过对象那个去进行调用,但是平常我们并不喜欢通过对象对其进行调用,而是喜欢通过类型对其进行调用,这就无法访问虚函数表,也不适合将其放到虚表里面去。

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

 虚函数地址是放在虚表里面的,虚表指针在构造函数的初始化列表中进行初始化。加入构造函数可以是虚函数,调用该虚函数的时候要去需表里面找,但是我有虚表吗,没有,因为虚表指针是在调用该虚函数的初始化列表阶段才有的,所以构造函数重写没有意义。

构造函数不可以是多态,因为派生类的构造函数必须要去显式调用其父类的构造函数。

同理,拷贝构造和赋值运算符重载也不能是虚函数,构造、拷贝构造、赋值运算符重载这三类函数按理来说不应该是重写他们,而是子类要返回过来调用父类的构造、拷贝构造、赋值运算符重载,属于一种合成。

析构函数是可以是虚函数,并且建议写成虚函数。

 普通函数快还是虚函数快?

一般情况下普通函数更加快,因为普通函数是直接确定地址然后去call这个地址,而虚函数一般是去对象的虚表找到这个地址然后再去call,但是虚函数不一定要到需表里面去找,虚函数要构成多态才去虚表里面找。而虚函数是构成多态的必要条件但不是构成多态的的充分条件,如果构不成多态,就是和普通函数一样快。

class A
{
public:
	virtual void func1()
	{
		cout << "A::func1" << endl;
	}

	void func2()
	{
		cout << "func2" << endl;
	}
};
int main()
{
	A aa;
	aa.func1();//虽然是虚函数但是未构成多态,调用速度和普通函数一样快。
	aa.func2();
// 多态调用  -- 去虚表中找虚函数地址
	A* ptr = &aa;
	ptr->func1();//这里虚函数虽然没有被重写,但是调用的方式还是采用多态调用而非普通调用。由于该函数并未进行重写,编译器也可以将这里识别成普通调用,但是成本太高了,实际上编译器是傻瓜一样的去运行,只要你是父类的指针或者引用,并且你是虚函数(不管是否有子类进行了重写),你就在虚表,就按照多态的方式去调用,去虚表里面寻找。但并不不会构成多态的现象,因为这里并没有进行重写,不存在不同对象对同一函数造成不同的行为。
    //只要是虚函数就会进入到虚表当中,哪怕没有重写。 
	return 0;
}

虚函数表是在什么阶段生成的?

虚函数表是在编译阶段就生成了,一般情况下存在在代码段的常量区的。初始化列表初始化的是虚表指针,而不是虚表,虚表早就已经搞好了。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值