C++通过函数重载模板实现编译期多态(静态绑定),通过虚函数实现运行时多态(动态绑定)。

1.函数重载

#include <stdio.h>

int add(int a, int b)
{
return a + b;
} int add(double a, double b)
{
return a + b;
} void testAdd()
{
int sum = add(1.1, 3.2);
printf("sum:%d\n", sum);
}

以上代码用g++编译可以通过,如果用gcc编译则会报错(说明C语言不支持函数重载):

看下编译后的函数名:

可以看到,编译器在add函数名后加上了参数类型ii(int,int) dd(double,double)。

2.模板

函数模板

template <typename T>
T sub(T a, T b)
{
return a + b;
} void testSub()
{
int subi = sub(1, 3);
printf("subi:%d\n", subi);
double subd = sub(1.1, 3.3);
printf("subd:%f\n", subd);
}

这种方式只需要写一套代码,编译器会根据用户调用 生成不同的函数定义,这里生成了两个函数(int和double)

类模板

template <typename T>
class Foo
{
public:
static T mul(T a, T b)
{
return a * b;
}
}; void testMul()
{
int mul = Foo<int>::mul(2, 3);
printf("mul:%d\n", mul);
}

类模板和函数模板类似,可以先把变量类型用模板占位,编译时通过用户指定的类型来生成具体定义。

3.虚函数

class Base
{
public:
virtual void print()
{
printf("Base::print()\n");
} int m_b;
}; class Derive : public Base
{
public:
virtual void print()
{
printf("Derive::print()\n");
}
virtual void show()
{
printf("Base::show()\n");
} int m_d;
}; class Derive2 : public Base
{
public:
virtual void print()
{
printf("Derive2::print()\n");
}
}; void testVirtual()
{
Derive d;
Derive2 d2;
Base *p = &d;
p->print();
p = &d2;
p->print();
}

C++通过virtual关键字实现虚函数,运行结果如下:

可以看到,父类指针指向不同的派生类,就能够调用不同派生类的实现,这就是所谓的运行时多态。

C++对象内存布局

C/C++程序的内存格局通常分为四个区:全局数据区,代码区,栈区,堆区。

C++由于类和虚函数的存在,会复杂一点:

  • 如果是没有继承关系的普通类,那么类成员变量的内存排列和结构体一样;
  • 如果是继承的子类,那么新的成员变量位于父类的成员变量之后;
  • 如果类里只存在普通函数,普通函数位于代码区,这个类不会增加额外的空间;
  • 如果类里有virtual函数,那么编译器会在头部增加一个虚表指针,指向虚函数表,运行时通过虚函数表来调用不同的虚函数。
    比如上面的Base和Derive类,我们写一个测试函数
void testMemory()
{
Derive d;
printf("sizeof(Derive):%ld\n", sizeof(d)); d.m_b = 10;
d.m_d = 2;
printf("vtable ptr=%p, &m_b=%p, &m_d=%p\n", &d, &(d.m_b), &(d.m_d));
}

64位机器上输出结果:

可以看到虚表指针位于最开始的位置,大小是8byte。基类的成员变量m_b位于中间位置,大小是4byte。子类的成员变量m_d位于最后。

有virutal函数的每个类对象都挂着一个或多个(如果有多继承)虚函数表。
基类虚函数表存的是基类的虚函数地址:

派生类维护一套自己的虚函数表,默认和基类的虚函数地址一样。如果派生类重写了某一个虚函数,则会覆盖基类的虚函数:

现在有一个类,用到多继承(实际编码中不推荐多继承)

class Base2
{
public:
virtual void msg(){}
}; class MultiDerive : public Base, public Base2
{
public:
virtual void print()
{
printf("MultiDerive::print()\n");
}
int m_d;
}; void testMulMemory()
{
MultiDerive md;
printf("sizeof(MultiDerive):%ld\n", sizeof(md));
printf("vtable ptr=%p, &m_b=%p, &m_d=%p\n", &md, &(md.m_b), &(md.m_d));
}

运行结果如下:

基类Base的虚表指针和成员变量(int 4byte内存对齐成了8byte)位于最开始,基类Base2的虚表指针位于中间,最后是派生类MultiDerive的成员变量。
图示如下:

4.常见问题

  • 模板和虚函数能混用吗?

模板类可以包含虚函数,普通成员函数可以使用模板,代码如下:

template <typename P>
class Math1
{
public:
template <typename T>
T add(T a, T b)
{
return a + b;
} virtual int sub(int a, int b)
{
return a - b;
} P m_a;
};

virtual成员函数不能使用模板,以下代码会报错:

class Math2
{
public:
template <typename T>
virtual T add(T a, T b)
{
return a + b;
}
};


原因是:编译器需要在编译时确定虚函数表的大小,并为每一个虚函数指针分配存储空间。由于不确定模板虚函数会实例化多少个,也就没办法分配空间,所以禁止这么做。

C++怎么实现多态?的更多相关文章

  1. Java中的多态

    1.多态:具有表现多种形态的能力的特征 父类: public abstract class Animal {public abstract void Say();} 子类: public class ...

  2. C# 工厂模式+虚方法(接口、抽象方法)实现多态

    面向对象语言的三大特征之一就是多态,听起来多态比较抽象,简而言之就是同一行为针对不同对象得到不同的结果,同一对象,在不同的环境下得到不同的状态. 实例说明: 业务需求:实现一个打开文件的控制台程序的d ...

  3. C#非常重要基础之多态

    前几天看了一位同志的博客,写的是关于他自己去支付宝面试的经历.过程大体是这样的:问答的时候,前面部分,作者都应答如流,说起自己经验如何之丰富,最后面试官问了作者一个问题:请简述多态的概念和作用.结果这 ...

  4. C++多态详解

    多态是面向对象的程序设计的关键技术.多态:调用同一个函数名,可以根据需要但实现不同的功能.多态体现在两个方面,我们以前学过的编译时的多态性(函数重载)和现在我们这一章将要学习的运行时的多态性(虚函数) ...

  5. 【那些年关于java多态应用】

    1.多态:具有表现多种形态的能力的特征 父类: public abstract class Animal { public abstract void Say();} 子类: public class ...

  6. JAVA多态

    多态是指当系统A访问系统B的服务时,系统B可以通过多种方式来提供服务,而这一切对系统A是透明的.比如动物园的饲养员能够给各种各样的动物喂食.下图显示了饲养员Feeder,食物Food和动物Animal ...

  7. C#多态“说来也说”——逻辑层BLL中的多态使用

    本文版权归博客园和作者吴双本人共同所有.欢迎转载,转载和爬虫请注明原文地址 http://www.cnblogs.com/tdws/p/5861842.html 昨天晚上,有个朋友说学了好久,依然没搞 ...

  8. java多态的理解

    面向对象语言中的类有三个特征,封装.继承.多态.封装与继承很好理解,那什么是多态呢? 1.什么是多态? 多态的定义:指允许不同类的对象对同一消息做出响应.即同一消息可以根据发送对象的不同而采用多种不同 ...

  9. java中如何实现多态

    复习基础知识 多态,就是重载和重写.重载发生在一个类中.重写发生在子类,意思就是子类重写父类相同名称的方法.刚学语言有的东西,不必搞得那么清楚,只有知道怎么用就行了,有的问题你要想真正把它搞得很懂,短 ...

  10. OC多态

    要点: 1.多种形态,引用的多种形态对于一个引用变量,可以指向任何类的对象.对于一个父类的引用(类与类之间有一种继承关系),可以指向子类,也可以指向本类,指向的类型不同.当通过此引用向对象发送消息,调 ...

随机推荐

  1. python中字符串与列表之间的相互转换

    1.字符串>列表:split() a = 'my first python' b = a.split(" ") print(b)输出: 2.列表>字符串:join() ...

  2. springboot通过maven下载依赖失败解决

    公司直接用不了内网遇到的好的坑.导入网上的springboot项目的启动入口一致有报错,如:Cannot resolve symbol 'SpringApplication' maven配置设置都做了 ...

  3. Visual Studio 2022 预览版下载来了(x64位)

    Visual Studio 2022 预览版下载:https://visualstudio.microsoft.com/zh-hans/vs/preview/vs2022/

  4. git 的使用(新手)

    git的使用心得 windows版本下载git地址 git config --global user.name 用户名 在使用git前要注册用户名(个人称谓) git config --global ...

  5. 面试问题之C++语言:简述编译过程

    转载于:https://blog.csdn.net/ypshowm/article/details/89374706 编译过程主要分为四步: 1.词法分析(扫描) 运行类似于有限状态机的算法将源代码的 ...

  6. Java容器基础概况

    一.什么是Java容器 书写程序时,我们常常需要对大量的对象引用进行管理.为了实现有效的归类管理,我们常常将同类的引用放置在同一个数据容器中.Java容器类是java提供的工具包,包含了常用的数据结构 ...

  7. (转载)mos管电压规格是什么,什么是VMOS管栅极

    电压规格:VDSS.VDS.BVDSS.V(BR)DSS VDSS中的"V"表示电压,前面的"D"."S"表示"Drain&quo ...

  8. 利用contextmenu事件,自定义右键

    <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <m ...

  9. 打败算法 —— 删除链表的倒数第n个结点

    本文参考 出自LeetCode上的题库 -- 删除链表的倒数第n个结点,官方的双指针解法没有完全符合"只遍历一遍链表"的要求,本文给出另一种双指针解法 https://leetco ...

  10. servlet中的HttpServletRequest对象

    HttpServletRequest对象表示客户端浏览器发起的请求,当客户端浏览器通过HTTP协议访问服务器时,Tomcat会将HTTP请求中的所有信息解析并封装在HttpServletRequest ...