刚学习虚函数的时候,会混淆什么情况下,发生了调用,为什么会调用子类,为什么有的时候是父类,故记录此笔记。
1. 什么是虚函数指针和虚函数表?
在C++中,每个包含至少一个虚函数的类都会有一个对应的虚函数表(vtable)。这个虚函数表是一个编译器生成的数组,包含了指向类的所有虚函数的指针。每个这样的类的对象都会有一个虚函数指针(vptr),这个指针指向类的虚函数表。
虚函数表(vtable):这是一个静态数组,每个含虚函数的类都有一个独立的虚函数表。如果一个类有继承而来的虚函数,那么它的虚函数表中会包含这些函数的指针。如果派生类覆盖了基类中的虚函数,则其虚函数表中对应的条目会被更新为指向派生类中的函数。
虚函数指针(vptr):这是对象级别的指针,每个类的实例(对象)都会有一个vptr,它指向该对象所属类的虚函数表。当一个对象构造时,编译器会设置vptr以指向正确的虚函数表。这确保了即使是通过基类的指针或引用,也能调用到派生类中正确的函数版本。
2. 虚函数指针和虚函数表存在哪里?
在C++中,虚函数表(vtable)和虚函数指针(vptr)的存在是为了支持运行时多态性,即在程序运行时根据对象的实际类型调用正确的虚函数。下面是它们的内存布局:
虚函数表 (vtable):
- 虚函数表通常是一个静态的数组,其元素是函数指针,指向类的虚函数实现。
- 对于每个有虚函数的类,编译器都会生成一个独一无二的虚函数表。
- 这个表一般存储在程序的只读数据段(如ELF格式的`.rodata`段或PE格式的`.rdata`段),因为它的内容在程序执行期间不会被修改。虚函数指针 (vptr):
- 每个含有虚函数的对象都会在其内存布局的开始部分有一个虚函数指针。
- 这个指针指向相应的虚函数表,使得对象能够在运行时定位并调用正确的虚函数实现。
- 虚函数指针的位置和大小取决于对象在内存中的布局,它通常是对象实例在堆或栈上的一部分。
3. 虚函数表和指针存在多少个?
在C++中,每个包含至少一个虚函数的类都会有一个对应的虚函数表(vtable)。这个虚函数表是一个编译器生成的数组,包含了指向类的所有虚函数的指针。每个这样的类的对象都会有一个虚函数指针(vptr),这个指针指向类的虚函数表。
class Base {
public:
virtual void virtualFunction() { /* ... */ }
};
class Derived : public Base {
public:
void virtualFunction() override { /* ... */ }
};
// Base 类有一个虚函数表,其中包含 virtualFunction 的入口。
// Derived 类有一个虚函数表,其中 virtualFunction 的入口指向 Derived::virtualFunction。
Base* b = new Derived();
b->virtualFunction(); // 运行时通过 b 的 vptr 找到 Derived 的 vtable,并调用 Derived::virtualFunction。
在这个例子中,尽管b是一个指向Base类型的指针,
但由于指向的对象实际上是Derived类型,
所以b的vptr指向的是Derived的虚函数表,
因此调用的是Derived::virtualFunction。
这一过程在运行时动态地发生。
4. 子类没有重写虚函数,那么子类包含虚函数表吗?
在C++中,如果一个类中声明了虚函数,那么编译器会为这个类生成一个虚函数表(vtable),以及一个指向这个虚函数表的指针(vptr)。虚函数表是一个包含指向类的虚函数的指针数组。当类被继承时,即使子类没有自己声明新的虚函数,子类仍然会从父类继承虚函数表。
如果子类覆盖了父类的虚函数(即使子类中没有显式地声明新的虚函数),它会在自己的虚函数表中有一个指向这些覆盖实现的指针。
如果子类没有覆盖父类的虚函数,它的虚函数表会包含指向父类虚函数实现的指针。
因此,即使子类内部没有自己声明虚函数,只要它继承自包含虚函数的父类,它就会有虚函数表。这是因为虚函数的机制保证了多态性——即使是通过指向子类的父类指针调用虚函数,也能够调用到正确的、派生类中的实现。
5. 子类没有重写虚函数,子类可以直接访问父类的虚函数吗?
子类对象可以直接访问父类的虚函数,除非这些函数被子类覆盖或者是私有的。
在C++中,当一个类继承自另一个类时,它继承了父类的所有成员函数和变量(除非它们是私有的,那样的话就不能直接访问,但仍然会被继承)。如果子类没有提供自己的版本(即没有覆盖父类的虚函数),那么父类的虚函数就会成为子类的一部分,并且可以像访问自己的成员一样直接调用。
例如,下面的代码展示了这个过程:
class Base {
public:
virtual void func() {
cout << "Base func" << endl;
}
};
class Derived : public Base {
// 没有覆盖 func()
};
int main() {
Derived d;
d.func(); // 将调用 Base::func,因为 Derived 没有覆盖它
return 0;
}
在这个例子中,`Derived` 类继承了 `Base` 类并且没有覆盖 `func()` 函数。因此,当 `Derived` 类的对象调用 `func()` 时,实际上调用的是 `Base` 类中的实现。
如果子类覆盖了父类的虚函数,那么即使使用父类的指针或引用调用该函数,也会执行子类中的版本。要调用父类中被覆盖的版本,可以使用作用域解析运算符(`::`)来指定要调用的函数版本,如下所示:
class Derived : public Base {
public:
virtual void func() override {
cout << "Derived func" << endl;
}
};
int main() {
Derived d;
d.func(); // 调用 Derived::func
d.Base::func(); // 显式调用 Base::func
return 0;
}
在这里,`d.func()` 调用的是 `Derived` 类的版本,而 `d.Base::func()` 显式调用的是 `Base` 类中的版本。
但是如果创建了父类 `Base` 的对象并调用其成员函数,那么调用的将始终是 `Base` 类定义的函数,而不是任何子类中的覆盖版。具体来说,如下代码:
class Base {
public:
virtual void func() {
cout << "Base func" << endl;
}
};
class Derived : public Base {
virtual void func() override {
cout << "Derived func" << endl;
}
};
int main() {
Base b;
b.func();
}
这里调用的将是 `Base` 类中定义的 `func` 函数,即使它被标记为 `virtual`(虚函数)。当您直接使用父类类型的对象时,并不涉及多态或动态分派。多态仅当通过指针或引用访问对象时才发生,且该指针或引用的类型是父类类型,而对象本身是派生类类型的情况下。
举个例子,如果有一个指向 `Derived` 类对象的 `Base` 类型指针或引用,当通过这个指针或引用调用虚函数时,将发生多态调用,即调用派生类 `Derived` 的覆盖函数(如果有的话):
Base* b = new Derived(); // b 是 Base 类型的指针,但指向 Derived 实例
b->func(); // 如果 Derived 覆盖了 func,则调用 Derived::func;否则,调用 Base::func
delete b; // 释放内存
在这个例子中,由于 `func` 是虚函数,并且 `b` 是指向 `Derived` 类型对象的 `Base` 类型指针,所以这里的 `func` 调用将根据对象的实际类型(在这个例子中是 `Derived`)来解析,这是动态绑定的一个典型例子。如果 `Derived` 提供了 `func` 的覆盖版本,就会调用 `Derived::func`;如果 `Derived` 没有覆盖 `func`,就会调用 `Base::func`。
6. 关键字说明
上述例子中子类存在override关键字,在此说明这只是种写法,也可以不带virtual和override关键字。
在C++中,一旦在基类中声明了虚函数,派生类中对应签名和返回类型的函数自动成为虚函数,无论是否使用`virtual`关键字。因此,即使派生类中的函数没有显式地被标记为`virtual`,它依然是虚函数,并且如果其签名和返回类型与基类中的虚函数相匹配,那么它也被视为重写了基类中的虚函数。
关于`override`关键字,它是C++11引入的一个新特性,它不影响函数是否为虚函数或者是否重写基类中的虚函数。`override`关键字的作用是显式地告诉编译器该函数旨在重写基类中的一个虚函数。如果标记为`override`的函数没有从基类中重写虚函数(例如,由于函数签名不匹配),编译器将报错。这有助于捕获错误,例如误拼写函数名或参数类型。
所以,给出的类`Derived`中的`func`函数,即使没有`virtual`和`override`关键字,仍然是一个虚函数,并且重写了基类`Base`中的虚函数,前提是基类`Base`中有一个可被重写的`func`虚函数声明。
这里是一个简化的例子:
#include <iostream>
using namespace std;
class Base {
public:
virtual void func() {
cout << "Base func" << endl;
}
};
class Derived : public Base {
public:
// 这里没有virtual关键字,也没有override关键字
void func() {
cout << "Derived func" << endl;
}
};
int main() {
Base* b = new Derived();
b->func(); // 输出 "Derived func"
delete b;
return 0;
}
在这个例子中,`Derived::func`被调用,即使没有`virtual`和`override`关键字,因为它重写了基类`Base`中的虚函数`func`。
7. 编译器如何确定调用哪一个函数的呢?
在C++中,编译器和运行时系统如何确定要调用哪个函数,取决于函数是否是虚拟的(virtual),以及调用的上下文。这里有两种主要的情况:
1. 非虚函数调用
2. 虚函数调用
7.1 非虚函数调用
对于非虚函数(即普通函数或非虚成员函数),编译器在编译时期就确定了调用哪个函数。这称为静态绑定,因为调用的函数基于变量的声明类型。例如:
class Base {
public:
void nonVirtualFunc() { /* ... */ }
};
class Derived : public Base {
public:
void nonVirtualFunc() { /* ... */ }
};
Base b;
Derived d;
b.nonVirtualFunc(); // 调用 Base::nonVirtualFunc
d.nonVirtualFunc(); // 调用 Derived::nonVirtualFunc
Base* ptr = &d;
ptr->nonVirtualFunc(); // 调用 Base::nonVirtualFunc,因为 ptr 是 Base 类型的指针
即使 `ptr` 指向一个 `Derived` 对象,调用 `nonVirtualFunc()` 也会执行 `Base` 类中的函数,因为这不是一个虚函数。
7.2 虚函数调用(动态绑定)
对于虚函数,编译器采用动态绑定。这意味着调用哪个函数是在运行时决定的,基于对象的实际类型,而不仅是指针或引用的类型。每个含有虚函数的类都有一个虚函数表(vtable),这是一个函数指针数组,指向类中所有虚函数的实现。每个这样的对象都有一个指向其类的vtable的指针(vptr)。
当你调用一个虚函数时:
class Base {
public:
virtual void virtualFunc() { /* ... */ }
};
class Derived : public Base {
public:
void virtualFunc() override { /* ... */ }
};
Base* ptr = new Derived();
ptr->virtualFunc(); // 运行时确定调用哪个函数
这里发生的是:
- 编译器为 `Base` 类和 `Derived` 类生成各自的虚函数表。
- 当 `ptr->virtualFunc();` 被调用时,运行时系统查找 `ptr` 所指对象的 `vptr`。
- 使用 `vptr` 访问虚函数表,找到 `virtualFunc` 对应的入口。
- 然后,调用该入口指针所指向的函数——在这种情况下,是 `Derived::virtualFunc`。
这就是所谓的后期绑定或动态绑定,它允许多态——即允许父类指针或引用调用实际子类对象的函数。
7.3 总结
- 对于非虚函数,编译器在编译时进行函数解析,这称为静态绑定。
- 对于虚函数,运行时进行函数解析,这称为动态绑定。
- 虚函数调用涉及查找对象的虚函数表(vtable)并通过它来调用适当的函数。这是由运行时系统在程序执行时进行的。
8. 包含虚函数的父类和子类大小是多大呢?
一个类的大小取决于其成员变量的大小,而不包括成员函数。成员函数(包括虚函数)的大小不计入类对象的大小,因为不论创建多少对象,每个成员函数只有一份代码,它存在于程序的代码段中。
对于含有虚函数的类,每个对象通常会比不含虚函数的类大一些,因为它需要存储一个指向虚函数表(vtable)的指针(vptr)。这个指针的大小通常和操作系统的指针大小相同,例如,在32位系统上是4个字节,在64位系统上是8个字节。
假设我们有以下的类定义:
class Example {
public:
virtual void func();
int a;
};
如果我们在一个32位的系统上,一个`int`的大小是4个字节,`vptr`也是4个字节,那么`Example`类的对象大小将是8个字节:4个字节用于成员变量`a`,另外4个字节用于`vptr`。
在64位系统上,指针的大小通常是8个字节,因此`Example`类的对象大小将是16个字节:8个字节用于`vptr`,另外8个字节用于成员变量`a`(注意,实际的对象大小还可能受到内存对齐的影响)。
要精确知道一个类的大小,你可以使用`sizeof`运算符。例如:
#include <iostream>
class Example {
public:
virtual void func() int a;
};
int main() {
std::cout << "Size of Example: " << sizeof(Example) << " bytes" << std::endl;
return 0;
}
这段代码将输出`Example`类的确切大小,包括其中的成员变量和虚函数指针(如果有的话)。
在C++中,对象的大小最小是1字节,以确保每个对象都有一个独一无二的地址。即使类没有数据成员,对象的大小也至少是1字节。然而,一旦类有虚函数,对象的大小至少会包含一个指向虚函数表的指针的大小。
8.1 子类实现了父类的虚函数
如果子类实现了父类中的虚函数,并且没有添加任何新的成员变量或者虚函数,子类对象的大小通常和父类对象的大小相同。这是因为子类对象中的虚函数指针(vptr)仍然只占用一个指针的大小,并且指向子类自己的虚函数表(vtable)。由于虚函数表是静态存在的,并不储存在对象实例中,所以它不会增加对象实例的大小。
以下面的代码为例:
class Base {
public:
virtual void virtualFunction() { /* ... */ }
};
class Derived : public Base {
public:
void virtualFunction() override { /* ... */ }
};
在这个例子中,即使`Derived`类覆盖了`virtualFunction()`函数,它的大小也会和`Base`类一样,因为`Derived`没有添加任何新的数据成员。对象的大小只包括数据成员和一个虚函数指针,不论这个虚函数被覆盖了多少次。
8.2 子类没有实现父类的虚函数
如果子类没有实现(覆盖)基类中的虚函数,只是简单地继承了它,子类的对象大小仍然通常和基类对象的大小相同。这是因为它仍然包含一个指向虚函数表的指针(vptr),指针的大小不变,而虚函数表只是继承了基类的表或者在表中有相应的指向基类虚函数的入口,这些都是静态的,不会影响对象的大小。
这里是一个简单的示例:
class Base {
public:
virtual void virtualFunction() { /* 基类实现 */ }
};
class Derived : public Base {
// 没有覆盖 virtualFunction
};
int main() {
std::cout << "Size of Base: " << sizeof(Base) << " bytes" << std::endl;
std::cout << "Size of Derived: " << sizeof(Derived) << " bytes" << std::endl;
return 0;
}
在这种情况下,`Derived`类没有添加任何新的数据成员或虚函数,因此`Derived`对象的大小将与`Base`对象的大小相同。`Derived`的虚函数表将包含一个指向`Base::virtualFunction`的指针,因为它没有提供自己的实现。
8.3 子类实现许多虚函数
在C++中,类的大小受其数据成员(包括从基类继承的成员)和虚函数表指针(vptr)的影响,但是类的大小并不受虚函数本身的数量的影响。
如果一个子类没有自己的数据成员,只有虚函数,那么该子类的对象大小至少为一个指针的大小,这个指针就是用于支持虚函数机制的虚函数表指针(vptr)。
即使子类有多个虚函数,这些虚函数的指针都存储在虚函数表中,而不是对象本身。虚函数表是一个静态的结构,由编译器生成,它的大小不计入单个对象的大小。每个对象只包含一个虚函数表指针,指向这个虚函数表。
因此,如果子类`Derived`没有其他数据成员,只有虚函数,那么`Derived`的大小应该等于虚函数表指针的大小。在大多数现代架构中,这通常是4字节(32位系统)或8字节(64位系统)。
下面是一个具体的代码例子来展示这一点:
#include <iostream>
class Base {
public:
virtual void func1() virtual void func2()};
class Derived : public Base {
public:
virtual void func3() virtual void func4() virtual void func5() // ...更多虚函数...
};
int main() {
std::cout << "Size of Base: " << sizeof(Base) << " bytes\n";
std::cout << "Size of Derived: " << sizeof(Derived) << " bytes\n";
return 0;
}
在这个例子中,即使`Derived`类定义了额外的虚函数,`sizeof(Derived)`的输出也应该与`sizeof(Base)`相同,因为都只包含一个vptr。实际的大小将取决于编译器和平台,但原理是相同的。