• C++的多态性定义

所谓多态性就是不同对象收到相同的消息产生不同的动作。通俗的说,多态性是指一个名字定义不同的函数,这些函数执行不同但又类似的操作,即用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。

多态性又分为两种:一种是编译时的多态性,主要通过函数重载和运算符重载实现。一种是运行时的多态性,主要通过继承和虚函数来实现的。

这一部分,我们主要讲函数重载和继承与虚函数,运算符的重载我准备单独做一部分来记载。

  • 函数重载

在同一范围中声明几个功能类似的同名函数,但是这些同名函数的形参(参数的个数、类型或者顺序)必须不同,这就是函数重载。函数重载通常被用来实现功能类似而所处理的数据类型不同的问题。

 // base class
class Base
{
public:
void print(int a, int b) {
printf("a = %d, b = %d\n", a, b);
} void print(int c) {
printf("c = %d\n", c);
}
};
// main
int _tmain(int argc, _TCHAR* argv[])
{
Base base;
base.print();
base.print(, ); system("pause");
return ;
}

输出结果:

c = 1

a = 2, b = 3

几个注意点:

a) 函数的类型不在参数匹配检查之列。若两个函数除了返回类型不同,其他的全部相同,这是非法的。如:

int Print(int a);

double Print(int a);

虽然这两个函数的返回类型不同,但是由于参数个数和参数类型完全相同,编译程序将无法区分这两个函数。因为在确定调用哪一个函数之前,返回类型是不知道的。

b) 函数的重载与带默认值的函数一起使用时,需要注意有可能会引起二义性。如:

void Print(int a = 0, int b = 0);

void Print(int a);

当我们调用Print(1)时,程序无法确定调用的是哪一个。

c) 函数调用时,注意输入的实参要与形参的类型相符,否则容易造成编译器自动做类型转换是导致的错误。

  • 隐藏(基类函数的隐藏)

网上看到很多博客和文章提到这一个概念,在有的书上面,也发现这一概念被称作为在基类和派生类中的函数重载。不过这里为了和函数重载区分,我们还是把它叫做隐藏。

我们来根据两个例子来说明:

一:派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字(有关键字virtual的话就是虚函数了,这就是后面要说的重写(覆盖)的概念了)。此时,基类的函数被隐藏。

 // 基类
class Base
{
public:
// 不能有关键字virtual
void print(int a) {
printf("Base::a = %d\n", a);
}
};
// 派生类
class Derivel :
public Base
{
public:
// 隐藏
void print(int a) {
printf("Derivel::a = %d\n", a);
}
};
// main
int _tmain(int argc, _TCHAR* argv[])
{
Base base;
base.print(); Derivel derivel;
derivel.print(); // 隐藏了基类的print函数,调用的是派生类中的print函数 system("pause");
return ;
}

输出结果:

Base::a = 1

Derivel::a = 1

说明:

派生类Derivel公有继承了基类Base,对于它的公有函数print。派生类的对象derivel是可以直接访问的。但是派生类中自己也定义了一个同名且同参数的print函数,我们可以看到对象derivel调用的print函数时派生类自己定义的print函数,而不是基类继承下来的print函数,由此可知,基类的print函数被隐藏了。

二:派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏。

 // 基类
class Base
{
public:
// 关键字virtual有无不影响
virtual void print(int a) {
printf("Base::a = %d\n", a);
}
};
// 派生类
class Derivel :
public Base
{
public:
// 隐藏
void print(int a,int b) {
printf("Derivel::a = %d, b = %d\n", a, b);
}
};
// main
int _tmain(int argc, _TCHAR* argv[])
{
Base base;
base.print(); Derivel derivel;
// derivel.print(1); // 错误,基类的print已经被隐藏
derivel.print(,); // 正确 system("pause");
return ;
}

输出结果:

Base::a = 1

Derivel::a = 1, b = 2

说明:这里与上面的不同是派生类中定义了一个名字相同,参数不同的函数,这个函数同样把继承的print函数给隐藏了。同时,由于参数不同,就不用考虑基类的print函数有没有virtual关键字定义了。

延伸:

重载与隐藏的区别:

(1)   范围不同,重载的函数是在一个类里面,隐藏和被隐藏的函数是在基类和派生类两个类里面。

(2)   参数的区别,重载的函数之间参数肯定是不一样的,隐藏和被隐藏的函数参数可以一样也可以不一样。

  • 虚函数和函数重写(覆盖)

上面说的函数重载和隐藏以及没有说明的运算符重载都是编译时的多态,也被称为静态多态。下面要说的虚函数和重写就是多态的另外一种,运行时的多态,也被称为动态多态。

要了解重写,我们首先得了解虚函数的概念。

虚函数是函数重载的另一种表现形式,这是一种动态的重载方式,它提供了一种更为灵活的多态性机制。这里也就离不开派生类的对象指针了,我们来看一个例子。

 // 基类
class Base
{
public:
void print(int a) {
printf("Base::a = %d\n", a);
}
};
// 派生类
class Derivel :
public Base
{
public:
void print(int a) {
printf("Derivel::a = %d", a);
} void print1() {
printf("Derivel::print1");
}
};
// main
int _tmain(int argc, _TCHAR* argv[])
{
Base base;
Derivel derivel;
Base *b; b = &base;
b->print();
b = &derivel;
b->print(); system("pause");
return ;
}

输出结果:

Base::a = 1

Base::a = 1

对于对象指针,应该注意几个问题:

(1). 声明为指向基类对象的指针可以指向它的公有派生类对象,但不允许指向它的私有派生类对象和保护派生类对象。

(2). 允许将一个声明为指向基类对象的指针指向它的派生类对象,但是不能将一个指向派生类对象的指针指向它的基类对象。

(3). 声明为指向基类对象的指针,当其指向公有派生类对象时,只能用它来直接访问派生类从基类继承来的成员,而不能直接访问公有派生类中自己定义的成员。如上面例子中如果你写这句话:b->print1();,那它将报错。正确的应该写成((Derivel*)b)->print1();。

从输出结果可以看出,虽然b以及指向了派生类对象derivel,但是它所调用的print函数还是基类对象的print函数。这就是我们的问题,不管指针指向那个对象(基类对象或者派生类对象),它所调用的print函数一直都是基类的print函数。原因是普通成员函数的调用是在编译时静态联编的。在这种情况下,若要调用派生类中的成员函数,必须采用显示的调用方法,如:derivel.print(1),或者指针强制类型转换((Derivel*)derivel)->print();,我们使用对象指针的目的就是为了表达一种动态的特性,即指针指向的对象不同时执行的操作不同。如果采用以上的两种显示调用方法也就起不到这种作用了。其实真正的解决方法就是把基类的print函数说明为虚函数,这就是我们为什么要引入虚函数的原因了。

所以,如果我们把上面那段代码里面基类中的void print(int a)函数前面加上一个virtual关键字,把它声明为虚函数,那么输出结果就是这样:

Base::a = 1

Derivel::a = 1

在基类中的某个成员函数被声明为虚函数后,此虚函数就可以在一个或者是多个派生类中被重新定义。在派生类中重新定义时,其函数原型,包括返回类型,函数名,参数个数,参数类型和参数顺序都必须和基类完全相同。

虚函数定义的几点说明:

(1). 在基类中,用virtual关键字可以将其public或者protected部分的成员函数声明为虚函数,建议在public部分声明。

(2). 在派生类对基类声明的虚函数进行重新定义的时候,关键字virtual是可写可不写的,但是不写的时候有的情况下容易引起混乱,所以建议在派生类重新定义的是时候也写上virtual关键字。

(3). 虚函数在派生类中被重新定义时,其函数的原型与基类中的函数原型必须完全相同。

(4). 一个虚函数无论被公有继承多少次,他仍然保持他的虚函数特性。

(5). 虚函数必须是其所在类的成员函数,而不能是友元函数,也不能是静态成员函数。

(6). 构造函数不能被声明为虚函数,但是析构函数可以是虚函数。

通过一个例子,我们来最后总结一下虚函数下的重写(覆盖)。

 // 基类
class Base
{
public:
virtual void print(int a) {
printf("Base::a = %d\n", a);
}
};
// 基类1
class Base1
{
public:
void print(int a) {
printf("Base1::a = %d\n", a);
}
};
// 派生类
class Derivel :
public Base,public Base1
{
public:
void print(int a) {
printf("Derivel::a = %d\n", a);
}
};
// main
int _tmain(int argc, _TCHAR* argv[])
{
Derivel derivel; // 定义派生类的对象derivel
Base *b; // 定义基类base的指针b
Base1 *b1; // 定义基类base1的指针b1 b = &derivel;
b->print(); // 基类Base中print是虚函数,派生类重写了print函数
// 因此此处调用派生类的print函数
b1 = &derivel;
b1->print(); // 基类Base1中print不是虚函数,b1是Base1的指针
// 因此此处调用基类Base1的print函数 system("pause");
return ;
}

输出结果:

Derivel::a = 1

Base1::a = 1

延伸:

重写和重载的区别:

1) .范围不同,重载的函数是在一个类里面,重写的函数是在基类和派生类两个类里面。

2) .参数的区别,重载的函数之间参数肯定是不一样的,重写的函数的函数类型(函数名,函数参数,参数个数,参数顺序)必须与基类的函数完全相同。

3) .virtual修饰的区别,重载函数virtual修饰可有可无,重写的基类函数必须有virtual修饰。

重写和隐藏的区别:

1) .参数的区别,隐藏和被隐藏的的函数之间参数是可以一样也可以不一样,重写函数的函数类型(函数名,函数参数,参数个数,参数顺序)必须与基类函数完全相同。

2) .virtual修饰的区别,当隐藏和被隐藏的函数之间参数一样时,不能有virtual(有virtual修饰其实就是重写了),参数不一样是,virtual可以可无。重写的基类函数必须有virtual修饰。

C++ 面向对象的三个特点--多态性(一)的更多相关文章

  1. C++ 面向对象的三个特点--多态性(二)

    运算符重载 运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型. 类外部的运算符重载 首先,我们通过一个例子来说明为什么要有运算符重载. // Complex.h cl ...

  2. C++编程之面向对象的三个基本特征

    面向对象的三个基本特征是:封装.继承.多态. 封装 封装最好理解了.封装是面向对象的特征之一,是对象和类概念的主要特性. 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类 ...

  3. 荒芜的周六-PHP之面向对象(三)

    hi 又是开森的周六了.积攒的两周的衣服,终于是差不多洗完了.大下午的才来学点东西~~ 1.PHP面向对象(三) 四.OOP的高级实践 4.3 Static-静态成员 <?phpdate_def ...

  4. OO面向对象第三次作业总结

    面向对象第三次作业总结 一.JML基础梳理及工具链 注释结构 行注释://@annotation 块注释:/*@ annotation @*/ 两种注释都是放在被注释部分上面. 常见表达式 原子表达式 ...

  5. java面向对象(三)

    java面向对象(三) 1.四种权限修饰符 public > protected > (default) > private 注意:(default)并不是关键字default,而是 ...

  6. Java面向对象的三个特征

    首先,Java面向对象的三大特征: 三大特征: ▪ 封装 ▪ 继承 ▪ 多态 首先面向对象的第一个特性 封装 : 封装:就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操 ...

  7. C++之面向对象的三个基本特征

    三大特性是:封装,继承,多态 所谓封装 就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏.封装是面向对象的特征之一,是对象和类概念的主要特性. ...

  8. C++进阶 面向对象基础(三)

    类的的定义: 初始化一般建议使用构造函数初始化列表形式: Person(const string nm, const string addr):name(nm), address(addr){} th ...

  9. C++ 面向对象的三个特点--继承与封装(一)

    面试的时候经常会有很多概念性的东西,许久不用都会很生疏,特意整理一下方便自己以后不记得了可以查看一下,也顺便帮助自己复习一下. 概念 继承是面向对象程序设计的一个重要特性,它允许在既有类的基础上创建新 ...

随机推荐

  1. 内容营销三大实用法则(内含干货)-同样可运用在EDM数据营销中

    内容为王的时代,注重内容的发展才能屹立于互联网的浪潮之中.一个优秀内容在搜寻引擎优化.用户互动,促进销售等方面都扮演重要的角色,博主在这方面深有体会,但是很多人往往走向事情的反面,不注重实际的内容,而 ...

  2. oracle数据库的字符集更改

    A.oracle server 端 字符集查询  select userenv('language') from dual 其中NLS_CHARACTERSET 为server端字符集 NLS_LAN ...

  3. C#的回调被C++调用

    __stdcall 要加这个 extern "C" __declspec(dllexport) 要用这样的东东 定义 extern "C" __declspec ...

  4. 树莓派 LED+蜂鸣+声音传感器+红外模块组合打造声控/红外控制LED

    昨天搞了控制LED,玩了第一个,剩下的就感觉很简单了,这里记录一下 先来几张照片 玩了蜂蜜模块才发现规律,一般这种模块,都会有三个针脚,VCC(3.3V或5V供电输出针脚).GNC(对应GPIO针脚的 ...

  5. `cocos2dx 非完整` UI解析模块

    昨天在cocos2dx的一个群里,遇到一位匿名为x的朋友询问的问题,是关于ui的.他使用c++写了不少的ui封装节点,用来实现游戏中的各种不同效果.然后现在想改用lua,于是尝试使用最小代价去复用自己 ...

  6. Core Animation 学习

    core animation 是在UIKit层之下的一个图形库,用于在iOS 和 OS X 实现动画. Core Animation管理App内容 core animation不是一个完整的绘图系统, ...

  7. bower的使用

    一.bower的安装 安装nodejs的最新版本: 安装npm. 由于npm是nodejs的包管理器,所以在将nodejs安装完成后,npm也就自动安装完成. 安装git. 安装bower. 使用 n ...

  8. 【转载】Linux下动态共享库加载时的搜索路径详解

    转载自:http://www.eefocus.com/article/09-04/71617s.html 对动态库的实际应用还不太熟悉的读者可能曾经遇到过类似“error while loading ...

  9. shell的重定向

    >file  将file文件重定向为输出源,新建模式,可以将正确的结果输出到file文件 >>file 将file文件重定向为输出源,追加模式 <file  将file文件重定 ...

  10. sprint3(第六天)

    由于昨天下午晚上停电又没网,所以我们没做成什么功能,而且也没发博客. 今天燃尽图: