C++学习笔记——虚函数

基本概念

虚函数是在某基类中声明为 virtual 并在一个或多个派生类中被重新定义的成员函数,用法格式为:

virtual 函数返回类型 函数名(参数表) {函数体};

C++中用它来实现多态性,通过指向派生类基类指针或引用,访问派生类中同名覆盖成员函数

虚函数实现机制

虚函数是如何做到因对象的不同而调用其相应的函数的呢?

现在我们就来剖析虚函数。我们先定义两个类

class A{//虚函数示例代码
public:
virtual voidfun(){cout<<1<<endl;}
virtual voidfun2(){cout<<2<<endl;}
};

class B : public A{
public:
void fun(){cout<<3<<endl;}
void fun2(){cout<<4<<endl;}
};

由于这两个类中有虚函数存在,所以编译器就会为他们两个分别插入一段你不知道的数据,并为他们分别创建一个表。那段数据叫做vptr指针,指向那个表。那个表叫做vtbl。

每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,我们可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就是虚函数的地址,请看图

通过左图,可以看到这两个vtbl分别为class A和class B服务。现在有了这个模型之后,我们来分析下面的代码

A *p=new A;

p->fun();

毫无疑问,调用了A::fun(),但是A::fun()是如何被调用的呢?它像普通函数那样直接跳转到函数的代码处吗?

No,其实是这样的,首先是取出vptr的值,这个值就是vtbl的地址,再根据这个值来到vtbl这里,由于调用的函数A::fun()是第一个虚函数,所以取出vtbl第一个slot里的值,这个值就是A::fun()的地址了,最后调用这个函数。现在我们可以看出来了,只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务。

而对于class A和class B来说,他们的vptr指针存放在何处呢?其实这个指针就放在他们各自的实例对象里。由于class A和class B都没有数据成员,所以他们的实例对象里就只有一个vptr指针。通过上面的分析,现在我们来实作一段代码,来描述这个带有虚函数的类的简单模型。

#include<iostream>
using namespace std;
//将上面“虚函数示例代码”添加在这里
int main(){
void(*fun)(A*);
A *p=new B;
long lVptrAddr;
memcpy(&lVptrAddr,p,4);
memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4);
fun(p);
delete p;
system("pause");
return 0;
}

用VC或Dev-C++编译运行一下,看看结果:输出3。

现在一步一步开始分析:

void (*fun)(A*); 这段定义了一个函数指针名字叫做fun,而且有一个A*类型的参数,这个函数指针待会儿用来保存从vtbl里取出的函数地址;

A* p=new B; new B是向内存(内存分5个区:全局名字空间,自由存储区,寄存器,代码空间,栈)自由存储区申请一个内存单元的地址然后隐式地保存在一个指针中,然后把这个地址赋值给A类型的指针P;

long lVptrAddr; 这个long类型的变量待会儿用来保存vptr的值;

memcpy(&lVptrAddr,p,4); 前面说了,他们的实例对象里只有vptr指针,所以我们就放心大胆地把p所指的4bytes内存里的东西复制到lVptrAddr中,所以复制出来的4bytes内容就是vptr的值,即vtbl的地址;

现在有了vtbl的地址了,那么我们现在就取出vtbl第一个slot里的内容;

memcpy(&fun,reinterpret_cast<long*>(lVptrAddr),4); 取出vtbl第一个slot里的内容,并存放在函数指针fun里。

需要注意的是lVptrAddr里面是vtbl的地址,但lVptrAddr不是指针,所以我们要把它先转变成指针类型fun(p); 这里就调用了刚才取出的函数地址里的函数,也就是调用了B::fun()这个函数,也许你发现了为什么会有参数p,其实类成员函数调用时,会有个this指针,这个p就是那个this指针,只是在一般的调用中编译器自动帮你处理了而已,而在这里则需要自己处理。

delete p; 释放由p指向的自由空间;

system("pause"); 屏幕暂停;

如果调用B::fun2()怎么办?那就取出vtbl的第二个slot里的值就行了

memcpy(&fun,reinterpret_cast<long*>(lVptrAddr+4),4); 为什么是加4呢?因为一个指针的长度是4bytes,所以加4。或者memcpy(&fun,reinterpret_cast<long*>(lVptrAddr)+1,4); 这更符合数组的用法,因为lVptrAddr被转成了long*型别,所以+1就是往后移sizeof(long)的长度

定义虚函数的限制

(1)非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类析构函数定义为虚函数。因为,将基类析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。

(2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。

(3)当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数自动成为虚函数。

(4)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、类型都相同的非虚函数。在以该类为基类派生类中,也不能出现这种同名函数。

虚函数和继承

虚函数联系到多态,多态联系到继承。

继承有以下三种方式:

public 基类的public和protected的成员被派生类继承后,保持原来的状态
private 基类的public和protected的成员被派生类继承后,变成派生类的private成员
protected 基类的public和protected的成员被派生类继承后,变成派生类的protected成员

注:无论何种继承方式,基类的private成员都不能被派生类访问。

从上面的表中可以看出:

声明为public的方法和属性可以被随意访问;

声明为protected的方法和属性只能被类本身和其子类访问;

而声明为private的方法和属性只能被当前类的对象访问。

时间: 2024-10-31 10:46:31

C++学习笔记——虚函数的相关文章

MySQL学习笔记-自定义函数

MySQL学习笔记-自定义函数 1.自定义函数简介 自定义函数:用户自定义函数(user-defined function,UDF)是一种对MySQL扩展的途径,其用法与内置函数相同 自定义函数的两个必要条件:(1)参数  (2)返回值 自定义函数: 创建自定义函数 CREATE FUNCTION function_name RETURNS {STRING|INTEGER|REAL|DECIMAL} routine_body 关于函数体: 1.函数体可以由合法的SQL语句构成: 2.函数体可以是

python学习笔记之函数总结--高阶函数以及装饰器

python学习笔记之函数总结--高阶函数以及装饰器 Python特点: 1.不是纯函数式编程(允许变量存在): 2.支持高阶函数(可以传入函数作为变量): 3.支持闭包(可以返回函数): 4.有限度的支持匿名函数: 高阶函数: 1.变量可以指向函数: 2.函数的参数可以接收变量: 3.一个函数可以接收另一个函数作为参数: 下面我将示例一些函数的写法以及使用,并说明python中函数的特性: 1.基本的高阶函数示例: #!/usr/bin/env python def func():      

0722-----C++Primer听课笔记----------虚函数和模板

1.虚指针和虚函数表 1.1 不含有任何数据成员或者虚函数的class或者struct大小为1,含有虚函数的对象在基地址部分有一个vptr,指向虚函数表,因此大小为4个字节. 1.2 动态绑定的原理:假设派生类和基类存在覆盖的关系(基类中定义了虚函数),那么派生类在虚函数表中,会覆盖掉基类相应的虚函数.当程序执行的时候,根据基类指针找到vptr,根据vptr找到vtable,然后找到相应的版本去执行.所以执行的是覆盖的版本,而具体被哪个版本覆盖是由具体的对象类型所决定的,所以才实现了根据对象的具

C和指针 学习笔记-4.函数

参数传递: 参数传递采用按值传递 ADT&黑盒 ADT:abstract data type,抽象数据类型 c可以用于设计与实现抽象数据类型,因为它可以限制函数和数据定义的作用域,这种技巧也称为黑盒设计 user.h #define MAXLEN 3 struct UserClz { char *name; char *phone; char *address; }; typedef struct UserClz User; /* *接函数 *通地名称查找地址 */ char const * l

day8_python学习笔记_chapter11_函数

1. 返回对象的数目   python实际返回的对象 0 -> None ; 1 -> object ; >1 -> tuple 2. 内部/内嵌函数:如果内部函数的定义包含了再外部函数里定义的对象的引用, 内部函数会变成被称为闭包的特别之物. 3. 装饰器 day8_python学习笔记_chapter11_函数,布布扣,bubuko.com

lua学习笔记之函数

Lua学习笔记之函数 1.  函数的作用 函数主要完成指定的任务,这样的情况下函数作为调用语句使用,函数可以计算并返回值,这样的情况下函数作为赋值语句的表达式使用. 语法: funcationfunc_name(arguments-list) Statements-list end 调用函数的时候,如果参数列表为空,必须使用()表示是函数调用. Print(8*9,9/8) a = math.sin(3) +math.cos(10) print(os.date()) 上述规则有一个例外,当函数只

matlab学习笔记 bsxfun函数

matlab学习笔记 bsxfun函数 最近总是遇到 bsxfun这个函数,前几次因为无关紧要只是大概看了一下函数体去对比结果,今天再一次遇见了这个函数,想想还是有必要掌握的,遂查了些资料总结如下.   函数bsxfun [功能描述]两个数组间元素逐个计算. [应用场合]当我们想对一个矩阵A的每一列或者每一行与同一个长度相等的向量a进行某些操作(比较大小,乘除等)时,我们只能用循环方法或者利用repmat函数将要操作的向量a复制成和A一样尺寸的矩阵,进而进行操作.从MATLAB R2007a开始

MySQL学习笔记—自定义函数

MySQL学习笔记-自定义函数 注释语法: MySQL服务器支持3种注释风格: 从'#'字符从行尾. 从'– '序列到行尾.请注意'– '(双破折号)注释风格要求第2个破折号后面至少跟一个空格符(例如空格.tab.换行符等等).该语法与标准SQL注释语法稍有不同. 从/序列到后面的/序列.结束序列不一定在同一行中,因此该语法允许注释跨越多行. 下面的例子显示了3种风格的注释: mysql> SELECT 1+1; # This comment continues to the end of li

C++学习笔记之函数指针

与数据项类似,函数也有地址.函数的地址是存储其机器语言代码的内存开始的地方. 一.函数指针的基础知识 假设要设计一个名为estimate()的函数,估算编写指定行数代码所需时间,并且希望不同的程序员都使用该函数,并且该函数允许每个程序员提供自己的算法来估计时间.为实现这种目标,采用的机制是,将程序员要使用的算法函数地址传给estimate(),必须完成以下工作: 获取函数地址 声明一个函数指针 用函数指针来调用函数 1.获取函数地址 使用函数名(后面不跟参数)即可.如:think()是一个函数,