C++虚函数

C++ 多态

触发条件:

调用虚函数。

有以下几种情况:

  • 通过对象调用虚函数;
  • 通过指针或引用调用虚函数;
  • 通过成员函数调用虚函数。

其中,通过对象调用调用虚函数,由于对象和实际类型已经是相同的,所以观察不到多态的效果。但是调用过程依然是动态绑定的。

例子:

#include <iostream>
using namespace std;

class Base {
public:
	virtual void f() {
		cout << "Base::f()" << endl;
	}

	// 通过成员函数调用虚函数
	void g() {
		cout << "Base::g()" << endl;
		// 只有非 static 函数才能调用虚函数
		//! 只要调用虚函数,就会有多态。
		//! 通过 Base::this 指针调用虚函数
		f();
	}
};

class Derived : public Base {
public:
	void f() override {
		cout << "Derived::f()" << endl;
	}
};

int main() {
	Derived son;
	// 通过成员函数调用虚函数。
	son.g(); // 打印 Base::g() 和 Derived::f() 

	// 通过指针调用虚函数
	Base *pSon = new Derived;
	pSon->f(); // 打印 Derived::f() 
	
	delete pSon;

	return 0;
}

虚函数实现机制

32 bit 机器测试:

#include <iostream>
using namespace std;

// A 有一个虚函数,于是 A 对象会生成一个虚函数表指针
// sizeof(A) = 4
class A {
public:
    virtual void a() {};
};
// sizeof(B) = 4
class B {
public:
    virtual int b() { return 3; }
};
// 多重继承,会有两个虚函数表指针
#include <iostream>
using namespace std;

// A 有一个虚函数,于是 A 对象会生成一个虚函数表指针
// sizeof(A) = 4
class A {
public:
	virtual void a() {};
	void f() {
		cout << "A::f()" << endl;
	}
};
// sizeof(B) = 4
class B {
public:
	virtual int b() { return 3; }
};
// 多重继承,会有两个虚函数表指针
// sizeof(C) = 8
class C : public A, B {
public:
	virtual int c() { return 4; }
};
// 单继承,来自父类的虚函数表指针会被覆盖,只有一个虚函数表指针
// sizeof(D) = 4
class D : public A {
public:

};

int main() {

	cout << "size A: " << sizeof(A) << endl; // 4
	cout << "size B: " << sizeof(B) << endl; // 4
	cout << "size C: " << sizeof(C) << endl; // 8
	cout << "size D: " << sizeof(D) << endl; // 4

	// malloc 没有进行对象构造,虚函数表指针未定义、未初始化。
	A* pa = (A*)malloc(sizeof(A));
	pa->f(); // 调用非虚函数,正确运行。
	// pa->a(); // 调用虚函数,引发运行时异常。因为试图解引用未定义的虚函数表指针。

	getchar();

	return 0;
}

虚函数表的图示可以参考 虚表指针、虚函数指针位置

  • 只要 class 有 virtual 函数,那么该类就会生成一张虚函数表(可以简称“虚表”),linux 放在 rodata 段,windows 放在 data 段。父类和子类的虚函数表是两张不同的表,各有一张,互不干扰。
  • 虚函数表存放的是虚函数指针。注意:非虚函数的指针不会出现在该虚函数表中。
  • 带有虚函数的类的对象至少有一个虚函数表指针。在单继承下,子类只有一个虚函数表指针,但是多重继承就会有多个虚函数表指针。
  • 单继承体系下,子类将会拥有一张从父类复制过来的虚函数表。 当子类对父类的 virtual 函数进行重写时,子类虚函数表中对应的虚函数指针就会被覆盖成子类的函数指针。如果子类新增了父类中没有的虚函数,那么新增的虚函数指针将追加加在子类虚函数表里。
  • 多重继承体系下,例如上面的 C 类,会复制所有父类的虚函数表。如果子类新增了父类中没有的虚函数,那么新增的虚函数指针会追加在第一张虚函数表里,也就是从第一个父类复制过来的虚函数表里。

虚函数表指针存放在哪里?

  • 必然存放在对象的内存空间。
    唯一可能的实现方案,就是放在对象的内存空间中。因为在对象存在之前,这个 vptr 是不可知的;在对象存在之后,这个 vptr 是确知的。即使放在全局区(类 static 空间),在对象存在之前 vptr 又不能确知,没有意义。

  • 是放在对象的数据成员的前面、中间还是后面?
    答:测试结果表明,作为类的数据成员的一部分,放在最前面。

#include <iostream>
using namespace std;

class A {
public:
	virtual void* get() {
		cout << "A::get()" << endl;
		return NULL;
	}
};

class B : public A {
public:
	// 构造函数的初始化列表中调用虚函数
	B() : ptr_(get()) {
	}

	void* get() override {
		cout << "B::get()" << endl;
		return NULL;
	}
private:
	void* ptr_;
};

int main() {
	// 子类的构造函数可以安全地调用虚函数,说明在构造函数的初始化列表执行之前,
	// 虚函数表指针已经被初始化了。
	// 这说明:虚函数表指针必然是第一个数据成员。
	B b; // 打印 B::get()
	
	getchar();

	return 0;
}
  • 虚函数表指针放置位置有什么影响?
    放在数据成员的最前面,则会最先被初始化。这样即使是在子类初始化列表中调用虚函数,也能运行期正确调用。

虚函数表指针的构造和析构过程

#include <iostream>
using namespace std;

class Base {
public:
	// 在父类构造函数中调用虚函数
	Base() {
		cout << "Base(): ";
		f();
	}
	// 在析构函数中调用虚函数
	virtual ~Base() {
		cout << "~Base(): ";
		f();
	}

	virtual void f() {
		cout << "Base::f()" << endl;
	}
};

class Derived : public Base {
public:
	Derived() {
		cout << "Derived(): ";
		f();
	}
	~Derived() {
		cout << "~Derived(): ";
		f();
	}
	void f() override {
		cout << "Derived::f()" << endl;
	}
};

class DerivedDerived : public Derived {
public:
	DerivedDerived() {
		cout << "DerivedDerived(): ";
		f();
	}
	~DerivedDerived() {
		cout << "~DerivedDerived(): ";
		f();
	}
	void f() override {
		cout << "DerivedDerived::f()" << endl;
	}
};

int main() {
	{
		cout << sizeof(DerivedDerived) << endl; // 4
		DerivedDerived son;
		// 打印如下:
		// 4
		// Base(): Base::f()
		// Derived(): Derived::f()
		// DerivedDerived(): DerivedDerived::f()
		// ~DerivedDerived(): DerivedDerived::f()
		// ~Derived(): Derived::f()
		// ~Base(): Base::f()  
	}

	return 0;
}

测试结论:

构造过程:

  • 父类:1、虚函数表指针初始化;2、初始化默认值、初始化列表(如果有的话);3、执行构造函数的函数体。
  • 子类:1、虚函数表指针初始化,即修改虚表指针指向,指向子类本身的虚表;2、初始化默认值、初始化列表(如果有的话);3、执行构造函数体。

析构过程:

  • 子类:1、执行析构函数体;2、虚表指针重置为上级父类指针。
  • 最顶层父类:1、执行析构函数体;2、虚表指针释放。

构造函数能否调用虚函数?

说明:这里的虚函数指的是继承体系中的虚函数,如果在构造函数中调用一个与父类、子类都无关的无关类的虚函数,当然不会有问题。

参考文章:构造函数和析构函数中可以调用调用虚函数吗

答案:从上面的例子可以看出来,可以构造函数是可以调用虚函数的,并且不会发生运行期错误。但是,既然是虚函数,我们希望其能够达到动态绑定的多态,可以保证吗?

  • 在子类的构造函数调用虚函数,能保证多态。
#include <iostream>
using namespace std;

class A {
public:
	virtual void* get() {
		cout << "A::get()" << endl;
		return NULL;
	}

	virtual void release() {
		cout << "A::release()" << endl;
	}
};

class B : public A {
public:
	B() : ptr_(get()) {
	}

	~B() {
		release();
	}

	void* get() override {
		cout << "B::get()" << endl;
		return NULL;
	}

	void release() override {
		cout << "B:: release()" << endl;
	}

private:
	void* ptr_;
};

int main() {
	{
		B b; // 打印 B::get()
		// 析构时打印 B::release()
	}
	return 0;
}

我们讨论一下构造函数中调用虚函数的过程:
构造顺序:调用父类构造函数 → 调用子类的构造函数(列表初始化 → 执行构造函数的函数体)。
首先,既然我们讨论的是子类的构造函数,显然父类部分已经通过父类的构造函数构造好了,父类部分是完整的,其包含了虚函数表的指针。

接下来分两种情况:

  • 在子类构造函数的初始化列表中调用虚函数;
  • 在子类构造函数的函数体中调用虚函数。

有了前面的讨论,我们知道,虚函数表指针最先被子类对象初始化,然后执行列表初始化其他成员,最后执行构造函数的函数体。所以能够保证多态。

  • 在父类构造函数中调用虚函数,不能保证多态。

《Effective C++》《C++编程思想》不建议在构造函数和析构函数中调用虚函数。

#include <iostream>
using namespace std;

class Base {
public:
	// 在父类构造函数中调用虚函数
	Base() {
		f();
	}

	virtual void f() {
		cout << "Base::f()" << endl;
	}
};

class Derived : public Base {
public:
	void f() override {
		cout << "Derived::f()" << endl;
	}
};

int main() {
	// 先构造 Base 部分,Base 构造函数调用了虚函数 f(),
	// 由于此时只有 Base 部分,所以只能通过 Base 的虚函数表指针查找虚函数,
	// 于是 Base::f() 被调用。
	// 这与我们的预期不符,因为我们希望调用的是 Derived 的虚函数。
	Derived son; // 打印 Base::f() ,不符合预期。

	return 0;
}

析构函数的情况类似:如果在父类析构函数中调用虚函数,那么由于进入父类析构函数之前,子类部分已经析构了,并且会将虚函数表指针重置为父类的虚函数表指针,那么父类的析构函数调用的将是父类的虚函数。这与我们的预期不符,因为一个子类对象,却调用了父类的虚函数。

建议:

不要在构造 / 析构函数中调用虚函数。
因为你无法保证当前类不被其他类继承,发生继承时,无法保证多态。

构造函数能否是虚函数?

答案:不能。编译器会报错。

参考文章: 构造函数为什么不能是虚函数

  • 概念

    • 从概念上理解,所谓“虚函数”,就是在编译期不能知道函数入口,只有在运行期才能通过虚函数表指针查找虚函数表才能得到。
    • 所谓“构造函数”,就是对象构造时,需要调用的函数。
  • 矛盾
    那么矛盾来了:一个对象构造时,需要通过虚函数表指针查找对应的构造函数;但是由于这个对象还没有构造,虚函数表指针还不存在。

  • 结论:
    所以,构造函数不能是虚函数。

虚析构函数的好处?

在继承体系中,建议使用虚析构函数。
先看看析构过程:首先析构子类部分,然后析构父类部分。
如果对象的实际类型和绑定类型是相同的,可以正确析构。
但是如果子类对象绑定的是父类类型呢?此时如果析构函数是非虚函数,那么将不存在动态绑定的多态机制。此时将按照其绑定类型析构,也就是说子类对象析构时,仅仅调用了父类的析构函数(TODO:会不会造成内存泄露,因为子类部分没有析构,是不是意味着内存没有释放?)。

析构函数能否是纯虚函数?

参考文章:析构函数可以是纯虚函数

答案:可以。但是必须同时在类外提供该纯虚函数的定义(这算哪门子的“纯虚函数”)。

可以这样理解:“纯虚函数” 仅仅是一种声明,我们可以同时提供这个“纯虚函数”的定义

由于析构函数在子类对象析构时,一定会被调用,所以,这个纯虚析构函数的定义是有效的。
但是,由于纯虚成员函数必须被子类重写,提供基类的纯虚成员函数的定义虽然语法上不报错,但是这个定义永远不会被调用到,没有意义。

结论:

我们可以将析构函数声明为“纯虚函数”,但是必须提供析构函数的定义
声明纯虚析构函数的好处是,该类将被认为是抽象类,不允许定义其对象。

示例1:

#include <iostream>
using namespace std;

class A {
public:
    virtual ~A() = 0; // 声明式
};

// 定义式
// 必须提供定义,否则 B 继承 A 时,会报错。
// A 仍然被视为抽象类,不允许构建 A 的对象。
A::~A(){
	cout << "~A()" << endl;
}

class B : public A {
public:
    ~B() {}
};

int main() {
	{
		// A a; // 报错
		B b;
	}
}

示例2:

#include <iostream>
using namespace std;

class A {
public:
    virtual void f() = 0; // 声明式
};

// 定义式
// 由于不允许定义抽象函数 A 的对象,这个函数定义永远不会被调用。
void A::f() {
    cout << "A::f()" << endl;
}

class B : public A {
public:
	// 重写父类的纯虚函数
    void f() override {
        cout << "B::f()" << endl;
    }
};

int main() {
    {
        B b;
        b.f();
    }

    return 0;
}

重写(override)仅仅适用于虚函数

  • override 必须与 virtual 配对:父类使用 virtual ,子类才能使用 override。
  • 重写(override)仅仅针对虚函数。
  • 如果子类定义和父类相同的签名的非虚函数,只能叫“隐藏”了父类的对应函数。
  • 析构函数也可以重写。

参考文章:重写、重载、重定义三者的概念

#include <iostream>
using namespace std;

class Base {
public:
	Base() {
		cout << "Base()" << endl;
	}
	virtual ~Base() {
		cout << "~Base()" << endl;
	}
	// 父类使用 virtual
	// 子类才能使用 override
	// 否则报错。
	virtual void f() {
		cout << "Base::f()" << endl;
	}
};

class Derived : public Base {
public:
	Derived() {
		cout << "Derived()" << endl;
	}
	// 析构函数也可以重写
	~Derived() override {
		cout << "~Derived(): ";
	}
	// 重写仅仅针对虚函数
	// 如果子类定义和父类相同的签名的非虚函数,只能叫“隐藏”了父类的对应函数。
	//
	// 如果父类函数不是 virtual,那么使用 override 关键字将报错:
	// member function declared 'override' does not override a base class member
	// “声明为 override 的成员函数没有覆盖(重写)父类的成员”
	void f() override {
		cout << "Derived::f()" << endl;
	}
};


int main() {
	{
		Derived son;
	}

	return 0;
}

虚函数类的对象 double free 的情况

《Effective C++》写道:“当类的对象调用虚函数时,调用的是与自身类型匹配的虚函数”。
子类对象析构时,使用子类对象的虚函数表指针,释放掉子类部分,于是剩余的对象变成了父类的类型;然后虚函数表指针会被修正为父类的虚函数表指针,于是接着调用父类的虚析构函数。
在运行时可能出现,一个线程已经释放了对象,但是另外一个持有对象指针的线程又释放一次的情况。

子类继承抽象函数时,没有重写纯虚函数,那么如果能成功编译,这个虚函数指针在虚表中是什么?

答:该函数指针为 NULL,也就是 0。
一般情况下,编译都会报错,但是,在 dynamic 的情况下,可能存在这种情况。

TODO:怎么查看内存空间?

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值