tag 标签: 函数重载

相关博文
  • 热度 2
    2018-2-11 16:24
    2293 次阅读|
    0 个评论
    C++多态(1)
    1. 多态 多态是人类思维方式的一种直接模拟,多态性是指不同对象接收到相同的消息时,根据对象类的不同而产生不同的动作。多态性提供了同一个接口可以用多种方法进行调用的机制,从而可以通过相同的接口访问不同的函数。就是同一个函数名称,作用在不同的对象上将产生不同的行为。 多态从实现的角度来讲可以划分为两类:编译时的多态和运行时的多态。前者是在编译过程中确定了同名操作的具体操作对象,而后者是在程序运行过程中才动态地确定操作所针对的具体对象。这种确定操作的具体对象过程就是联编。联编是指计算机程序自身彼此关联的过程,即把一个标识符名和一个存储地址联系在一起的过程。在面向对象上来讲,就是把一条消息和一个对象的方法结合起来的过程。按照联编进行阶段的不同,分为:静态联编和动态联编,它们分别对应着多态的两种实现方式。 1.1 多态的引入 #includeiostream using namespace std; class Animal { public: void sleep() { cout "Animal sleep...." endl; } void breath() { cout "Animal breath..." endl; } }; class Fish:public Animal { void breath() { cout "Fish breath...." endl; } }; int main() { Fish fh; Animal *an = fh; an-breath(); system("pause"); return 0; } 在上述代码中,主函数定义了一个 Fish 类的对象 fh, 接着定义了一个指向 Animal 类的指针变量 an ,将 fh 的地址赋给了指针变量 an ,然后利用该变量调用 breath() 。这种情况很容易和 C++ 的多态性混淆 ,fh 是 Fish 类的对象,应该调用 Fish 的 breath() ,但是结果却不是这样的。 这是因为 C++ 编译器在编译的时候,要确定每个对象调用的函数地址,这称为早期绑定。当 Fish 类的对象 fh 的地址赋给 an 时, C++ 编译器进行了类型转换,编译器认为变量 an 保存的就是 Animal 对象的地址。所以在主函数调用的是 Animal 对象的 breath() 函数。此时, Fish 创建的对象 fh 在内存中的存储如下图所示。 在构造 Fish 类的对象时,系统首先要调用 Animal 的构造函数去构造 Animal 类的对象,然后才调用 Fish 类的构造函数完成自身部分的构造。因此,当将 Fish 类的对象转换为 Animal 类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是图中“ Animal 的对象所占的内存”。当利用类型转换完成后的对象指针去调用它的方法时,是调用它所在的内存中的方法。 2 .函数重载 由静态联编支持的多态性称为编译时的多态性或静态多态性,也就是说,确定同名操作的具体对象的过程是在编译过程中完成的。可以用函数重载和运算符重载来实现编译时的多态性。 函数重载也称为多态函数,是实现编译时的多态性的形式之一。函数重载时,函数名相同,但是函数所带的参数个数或数据类型不同。函数重载分为两种情况: 参数个数或类型有所差别的重载; 函数的参数完全相同但属于不同的类; 当函数的参数完全相同但属于不同的类时,为了让编译器正确区分调用哪个类的同名函数,可以采用以下两种方法: 用对象名区别:在函数名前面加上对象名来限制; 用类名和作用域运算符加以区别; 3. 虚函数 由动态联编支持的多态性称为运行时的多态性或者动态多态性,也就是说同名操作的具体操作对象的过程是在运行过程中完成的。在 C++ 中,可以用虚函数来实现运行时的多态。虚函数是实现运行时多态的一个重要方式,是重载的另一种形式,实现的是动态重载,即函数调用与函数体之间的联系是在运行时建立的,也就是动态联编。 #includeiostream using namespace std; class Animal { public: void sleep() { cout "Animal sleep...." endl; } virtual void breath() { cout "Animal breath..." endl; } }; class Fish:public Animal { void breath() { cout "Fish breath...." endl; } }; int main() { Fish fh; Animal *an = fh; an-breath(); system("pause"); return 0; } 实事上,当将基类中的成员函数 breath() 声明为虚函数时,编译器在编译的时候发现 Animal 类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表,该表是一个一维数组,在这个数组中存放每个虚函数的地址。上述代码中 Animal 类和 Fish 类都包含了一个虚函数 breath() ,因此编译器会为这两个类分别建立一个虚表。 当 Fish 类的 fh 对象构造完成后,其内部的虚表指针也就被初始化为指向 Fish 类的虚表。在类型转换后,调用 an-breath() ,由于 an 实际上指向的是 Fish 类的对象,该对象内部的虚表指针指向的是 Fish 类的虚表,因此最终调用的是 Fish 类的 breath() 函数。 下边说明使用派生类对象指针时应注意的问题: (1)声明为指向基类对象的指针可以指向它的公有派生类的对象,但不允许指向它的私有派生类的对象; (2)允许声明为指向基类对象的指针指向它的公有派生类的对象,但不允许将一个声明为指向派生类对象的指针指向基类的对象; (3)声明为指向基类对象的指针,当其指向它的公有派生类对象时,只能直接访问派生类中从基类继承下来的成员,不能直接访问公有派生类中定义的成员。要想访问,可以将基类指针用显式类型转换方式转换为派生类指针。 虚函数可以很好地实现多态,使用时要注意以下的问题: (1)虚函数的声明只能出现在类函数原型的声明中,不能出现在函数体实现的时候,而且基类中只有保护成员或公有成员才能被声明为虚函数; (2)在派生类中重新定义虚函数时,关键字 virtual 可以写可以不写,但是在容易引起混乱的地方,应该写上关键字; (3)动态联编只能通过成员函数来调用或通过指针、引用来访问虚函数,如果以对象名的形式来访问虚函数,将采用静态联编。 在派生类中重新定义基类中的虚函数,是函数重载的另一种形式,但是它与函数重载有如下区别: (1)一般的函数重载,要求其函数的参数或参数类型必须有所不同,函数的返回类型也可以不同; (2)重载一个虚函数时,要求函数名、返回类型、参数个数、参数类型和参数的顺序必须与基类中的虚函数的原型完全相同; (3)如果仅返回类型不同,其余相同,则系统会给出错误的信息; (4)如果函数名相同,而参数个数、参数的类型或者参数顺序不同,系统认为是普通的函数重载,虚函数的特性将被丢失。 3.1 多级继承和虚函数 多级继承可以看作是多个单继承的组合,多级继承的虚函数与单继承的虚函数的调用相同,一个虚函数无论被继承多少次,仍保持其虚函数的特性,与继承的次数无关。 #includeiostream using namespace std; class Base { public: virtual ~Base() {}; virtual void func() { cout "Base func..." endl; } }; class Derived1 :public Base { public: void func() { cout "Derived1 func..." endl; } }; class Derived2 :public Derived1 { public: void func() { cout "Derived2 func..." endl; } }; void test(Base b) { b.func(); } int main() { Base bobj; Derived1 d1Obj; Derived2 d2Obj; test(bobj); test(d1Obj); test(d2Obj); system("pause"); return 0; } 在上述代码中,定义了一个多级继承,在基类中定义了虚函数 func() ,在主函数调用该函数时,不同类创建的对象其调用的函数是不同的,即实现了多态“一个接口,多种实现”的功能。上述代码中在析构函数前加上关键字 virtual 进行说明,则该析构函数就称为虚析构函数。 使用虚析构函数时,要注意以下两点: (1)只要基类的析构函数被声明为虚析构函数,则派生类的析构函数无论是否使用 virtual 关键字进行声明,都自动成为虚函数; (2)如果基类的析构函数为虚函数,则当派生类未定义析构函数时,编译器所生成的析构函数也为虚函数; 一般来说,在程序中最好把基类的析构函数声明为虚函数。这将使所有派生类的析构函数自动成为虚函数 。这样,如果程序中用 delete 运算符准备删除一个对象,而 delete 运算符的操作对象是指向派生类对象的基类指针,系统会调用相应类的析构函数;否则系统会只执行基类的析构函数,而不执行派生类的析构函数,从而可能导致异常发生。 所以专业的编程人员一般习惯声明虚析构函数,即使基类不需要虚析构函数,也显式地定义一个函数体为空的虚析构函数,以保证在撤销动态存储空间时能得到正确的处理。 构造函数不能声明为虚函数,这是因为在执行构造函数时类对象还未完全建立过程,当然谈不上函数与类对象的关联。
  • 热度 13
    2013-3-10 23:50
    1548 次阅读|
    0 个评论
    原文链接一:http://hi.baidu.com/wjun520/blog/item/1678a11da07fe68086d6b653.html C++中有函数重载这种方法,以供我们调用时要可以不确定实参的个数,其实 C 语言也可以,而且更高明! 我们在stdio.h 中可以看到 printf() 函数的原型: int printf(char * format,...) 事实上,我们如果要写这样的函数也可以类似的写,那么在定义函数时用上这个符号“ ... ” ,它叫占位符,喊它 “ 三个点 ” 也可以,只要你愿意!那么我可以这样定义我的函数: fun(int a,...) {   }      只要上课认真听了的同学(傻瓜除外)都知道,这是个空函数,它是什么都不做的,光这样写还不行的,具体应该怎样定义呢? 且听我介绍3 个小东东: 1、 va_list 2、 va_arg() 3、 va_start() 在学习这3 个小东东之前,我们先回忆一下, C 语言是怎么操作文件时,是怎么样处理内存中的数据的呢?学习文件操作时,我们提到了“流”的概念,我们用指针指向数据所在的内存地址,再一个一个的操作。 学习指针时,我们知道有函数指针这个东东,不是指针函数而是函数打针哦!(呵呵,我的同学如果还记得就当复习一下,不要嫌我啰嗦^_^ )。我们记得程序在执行时,会将函数存储到内存中去。现在深入的讲一点点,存储函数时,参数传递的过程是怎样实现的呢?所谓的形式参数(局部变量)实质上又是什么呢?把这些问题连起来想想,想通了,你的思维势如破竹! 在调用函数时,程序同样会把实参传入,在函数存储区保存起来,如果有很多参数,将一起保存起来。   这时候就要用到va_list 了,这是个类型定义,我们可以把它理解成一个指针,它指向第一个参数的地址。   如果,我们这样定义: va_list pp ; 则pp 就是这样一种变量,它是指向所有参数中的第一个参数的。它不同于一般的指针变量,它是个复合变量,什么是复合变量啊?结构体类型的嘛,呵呵。如果 a 是第一个参数,能不能写成 pp=a 呢?   假设我定义了char d ="gelin"; 我要把 e 的值赋给 d ,能不能写成 d=e 呢?得用 strcpy() ,是吧!呵呵,一样的道理,这儿我们也用一个函数来实现,它就是 va_start();   如果这样写:va_start(pp,a); 那么pp 就指向第一个参数 a 了,并且可得到 a 的类型 int 。   这时候如果有下一个参数,就需要使pp 指向下一个参数,并且得到它的类型。同样需要使用函数来实现,这个函数是: va_arg()   可以这样写:va_arg(pp, 类型 ) ,这样 pp 就指向一个参数,并且可以得到那个参数的类型了。   注意!类型非常重要,学过指针的都应该清楚,指针的类型如果弄错的话,位置正确,取出来的数可能也是乱七八糟的。   下面我们看一个简单的例子: #include #include void fun(int a,...) { va_list pp; int n=1;//使用 n 计量参数个数 va_start(pp,a); do { printf("第 %d 个参数 =%d/n",n++,s); a=va_arg(pp,int);//使 pp 指向下一个参数,将下一个参数的值赋给变量 a } while (a!=0);//直到参数为 0 时停止循环 } main() { fun(20,40,60,80,0); }   注意! 一定要有上面两个文件包含命令,因为程序中用到的那3个小东东都在那个文件里。其实真正意义上应该说那是函数,实质上那不过是两个宏,呵呵。 什么是宏,什么是函数,不是这儿要讲的,也和这没太大关系。写出来,仅仅是回答一个为什么……   原文链接二:http://blog.csdn.net/joliny/archive/2008/04/28/2340299.aspx VA_LIST 是在C语言中解决变参问题的一组宏 VA_LIST的用法:              (1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针       (2)然后用VA_START宏初始化变量刚定义的VA_LIST变量,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数。        (3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型。        (4)最后用VA_END宏结束可变参数的获取。然后你就可以在函数里使用第二个参数了。如果函数有多个可变参数的,依次调用VA_ARG获取各个参数。 VA_LIST在编译器中的处理: 1)在运行VA_START(ap,v)以后,ap指向第一个可变参数在堆栈的地址。 (2)VA_ARG()取得类型t的可变参数值,在这步操作中首先apt = sizeof(t类型),让ap指向下一个参数的地址。然后返回ap-sizeof(t类型)的t类型*指针,这正是第一个可变参数在堆栈里的地址。然后用*取得这个地址的内容。 (3)VA_END(),X86平台定义为ap = ((char*)0),使ap不再指向堆栈,而是跟NULL一样,有些直接定义为((void*)0),这样编译器不会为VA_END产生代码,例如gcc在Linux的X86平台就是这样定义的。 要注意的是:由于参数的地址用于VA_START宏,所以参数不能声明为寄存器变量,或作为函数或数组类型。 使用VA_LIST应该注意的问题:    (1)因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型. 也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.     (2)另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码。   小结:可变参数的函数原理其实很简单,而 VA系列是以宏定义来定义的,实现跟堆栈相关。我们写一个可变函数的C函数时,有利也有弊,所以在不必要的 场合,我们无需用到可变参数,如果在C++里,我们应该利用C++多态性来实现可变参数的功能,尽量避免用C语言的方式来实现。