C++ 面向对象的三个特点--多态性(一)
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++ 面向对象的三个特点--多态性(一)的更多相关文章
- C++ 面向对象的三个特点--多态性(二)
运算符重载 运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型. 类外部的运算符重载 首先,我们通过一个例子来说明为什么要有运算符重载. // Complex.h cl ...
- C++编程之面向对象的三个基本特征
面向对象的三个基本特征是:封装.继承.多态. 封装 封装最好理解了.封装是面向对象的特征之一,是对象和类概念的主要特性. 封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类 ...
- 荒芜的周六-PHP之面向对象(三)
hi 又是开森的周六了.积攒的两周的衣服,终于是差不多洗完了.大下午的才来学点东西~~ 1.PHP面向对象(三) 四.OOP的高级实践 4.3 Static-静态成员 <?phpdate_def ...
- OO面向对象第三次作业总结
面向对象第三次作业总结 一.JML基础梳理及工具链 注释结构 行注释://@annotation 块注释:/*@ annotation @*/ 两种注释都是放在被注释部分上面. 常见表达式 原子表达式 ...
- java面向对象(三)
java面向对象(三) 1.四种权限修饰符 public > protected > (default) > private 注意:(default)并不是关键字default,而是 ...
- Java面向对象的三个特征
首先,Java面向对象的三大特征: 三大特征: ▪ 封装 ▪ 继承 ▪ 多态 首先面向对象的第一个特性 封装 : 封装:就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操 ...
- C++之面向对象的三个基本特征
三大特性是:封装,继承,多态 所谓封装 就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏.封装是面向对象的特征之一,是对象和类概念的主要特性. ...
- C++进阶 面向对象基础(三)
类的的定义: 初始化一般建议使用构造函数初始化列表形式: Person(const string nm, const string addr):name(nm), address(addr){} th ...
- C++ 面向对象的三个特点--继承与封装(一)
面试的时候经常会有很多概念性的东西,许久不用都会很生疏,特意整理一下方便自己以后不记得了可以查看一下,也顺便帮助自己复习一下. 概念 继承是面向对象程序设计的一个重要特性,它允许在既有类的基础上创建新 ...
随机推荐
- 架构模式之REST架构
直至今日,分布式系统(Distributed System)已经取得了大规模的应用,特别是Web的发展,已经给软件开发带来了翻天覆地的变化,这一点已经毋庸置疑了. 构建分布式系统常用的技术通常就是使用 ...
- js 与或运算符 || && 妙用
js 与或运算符 || && 妙用,可用于精简代码,降低程序的可读性. 首先出个题: 如图: 假设对成长速度显示规定如下: 成长速度为5显示1个箭头: 成长速度为10显示2个箭头: ...
- POJ 1650 Integer Approximation
Integer Approximation Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 5081 Accepted: ...
- asp.net mvc 利用过滤器进行网站Meta设置
过去几年都是用asp.net webform进行开发东西,最近听说过时了,同时webform会产生ViewState(虽然我已经不用ruanat=server的控件好久了 :)),对企业应用无所谓,但 ...
- easyui datagrid to excel
$.extend($.fn.datagrid.methods, { toExcel: function(jq, filename){ return jq.each(function(){ var ur ...
- FLEX自定义事件
有时候我们需要让两个组件之间实现联动,并且在其中传递数据,自定义事件机制可以帮助我们比较优雅的实现这种需要. 下面的例子,是打算实现一个列表和一个编辑框的联动. 编辑框代码 <?xml vers ...
- 用MapX与C#开发地理信息系统
转:http://www.cnblogs.com/dachie/archive/2010/08/17/1801598.html 第四章 MapX与C#实例... 5 4.1 MapX图层建立... 5 ...
- 浏览器 Pointer Events
前言 Pointer Events是一套触控输入处理规格,支持Pointer Events的浏览器包括了IE和Firefox,最近Chrome也宣布即将支持该处理规则. PointerEvent Po ...
- Linux的段错误调试方法
linux段错误的调试方法 相关博文: http://blog.csdn.net/htianlong/article/details/7439030 http://www.cnblogs.com/pa ...
- 如何找出你性能最差的SQL Server查询
我经常会被反复问到这样的问题:”我有一个性能很差的SQL Server.我如何找出最差性能的查询?“.因此在今天的文章里会给你一些让你很容易找到问题答案的信息向导. 问SQL Server! SQL ...