C++第五次作业——重载与多态
重载和多态
1. 区别:重载是同名但参数不同,通过参数来确定调用哪个函数;
多态是同名同参数,通过函数的实际类型决定调用哪个函数。
2. 多态的实现(以下将具体分析这三点):
① 函数重载
② 运算符重载
③ 虚函数
多态从实现的角度来讲可分为:编译时的多态和运行时的多态。
前者是在编译的过程中确定了同名操作的具体操作对象(静态绑定);
而后者则是在程序运行过程中才动态地确定操作所针对的具体对象(动态绑定)。
这种确定操作地具体对象地过程就是绑定(就是把一条消息和一个对象的方法相结合的过程)。
3. 重写:若子类和父类的某个函数具有相同的函数名,形参列表,且父类中的函数被定义为虚函数,则子类对该函数的实现被称为函数的重写。重写是常见的多态实现方式。
一、 函数重载
1. 概念:两个以上的函数,具有相同的函数名,但是形参的个数或者类型不同,编译器根据实参和形参的类型及个数的最佳匹配,自动确定调用哪一个函数,也就是函数的重载。简单理解,就是“一物多用”。
注:重载函数的参数个数、参数类型或者参数顺序三者中必须有一个不同。
·例如:
1 int add(int x, int y); 2 float add(float x, float y);//形参类型不同 3 4 int add(int x, int y); 5 int add(int x, int y, int z);//形参个数不同
2.C++允许功能相近的函数在相同的作用域内声明几个类似的同名函数,从而形成重载。编译程序对实参和形参的类型、个数、顺序,来选择调用哪一个函数。重载函数常用来处理实现功能类似但数据类型不同的问题。
·以下指出几种错误情况:
int add(int x, int y); int add(int b, int b);//错误,编译器不以形参名来区分函数 int add(int x, int y); void add(int x, int y);//错误,编译器不以返回值来区分函数
1 //当使用具有默认形参值得函数重载形式时,需要注意防止二义性 2 //以下两个函数原型,在编译时便无法区别为不同的重载形式: 3 4 void fun(int length, int width = 2, int height = 33); 5 void fun(int length); 6 7 //也就是说,当以fun(1)来调用函数fun时,编译器无法确定应该执行哪个重载函数
//不要将不同功能的函数定义为重载函数 int add(int x, int y) { return x + y; } float add(float x, float y) { return x - y; }
·具体实例1(形参类型不同):
1 #include<iostream> 2 using namespace std; 3 4 int ADD(int left, int right) 5 { 6 return left + right; 7 } 8 9 double ADD(double left, double right) //与第一个函数相比,形参类型不同 10 { 11 return left + right; 12 } 13 14 int main() 15 { 16 17 cout << ADD(1, 2) << endl; 18 cout << ADD(1.3, 2.3) << endl; 19 return 0; 20 }
·具体实例2(形参个数不同):
#include<iostream> using namespace std; int ADD(int a, int b) { return a + b; } double ADD(int a, int b,int c) //与第一个函数相比,形参个数不同 { return a + b+c; } int main() { cout << ADD(1, 2) << endl; cout << ADD(1, 2,3) << endl; return 0; }
二、 运算符重载
·基本知识
1. 概念:运算符重载是对已有的运算符赋予多重含义,是同一个运算符作用域不同类型的数据时导致不同的行为。(运算符重载的实质就是函数重载)
2. 运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。
例如:
·使复数类的对象可以用“+”运算符实现加法;
·使时钟类的对象可以用“++”运算符实现时间增加1秒。
3. C++几乎可以重载全部的运算符,而且只能够重载C++中已经有的。不能重载的运算符:“.” “.*” “::” “?:”。
4. 重载之后运算符的优先级和结合性都不会改变。
5. 运算符重载的形式有两种:
·重载为类的非静态成员函数;
·重载为非成员函数。
·具体讲解
一、 重载为类的非静态成员函数
1.定义:
1 返回类型 operator 运算符(形参表) 2 { 3 函数体 4 } 5 参数个数=原操作数个数-1(后置++、--除外)
·返回类型:指定了重载运算符的返回值类型,也就是运算结果类型。
·operator:是定义运算符重载的关键字。
·运算符:是要重载的运算符名称,比如要重载加法运算符,这里就写“+”。
·形参表:给出重载运算符所需要的参数和类型。
2. 重载为成员函数,她就可以自由地访问本类的数据成员。
3. 双目运算符重载:
(1)如果要重载B为类成员函数,使之能够实现表达式oprd1 B oprd2,其中oprd1为A类对象,则B应被重载为A类的成员函数,该函数只有一个形参,形参类型是oprd2所属的类型。
(2)经重载后,表达式oprd1 B oprd2相当于oprd1.operator B(oprd2)。
·实例1(复数加减运算):
1 #include<iostream> 2 using namespace std; 3 4 class Complex { //复数类定义 5 public: //外部接口 6 Complex(double r = 0.0, double i = 0.0) :real(r), imag(i) {}//构造函数 7 Complex operator+(const Complex& c2)const;//运算符+重载成员函数 8 Complex operator-(const Complex& c2)const;//运算符-重载成员函数 9 void display()const; //输出复数 10 private: 11 double real; //复数实部 12 double imag; //复数虚部 13 }; 14 Complex Complex::operator+(const Complex& c2)const //重载运算符函数实现 15 { 16 return Complex(this->real + c2.real, this->imag + c2.imag);//创建一个临时无名对象作为返回值,含义:调用Complex构造函数创建一个临时对象并返回它。 17 } 18 Complex Complex::operator-(const Complex& c2)const 19 { 20 return Complex(this->real - c2.real, this->imag - c2.imag); 21 } 22 void Complex::display()const 23 { 24 cout << "(" << real << "," << imag << ")" << endl; 25 } 26 int main() { 27 Complex c1(5, 4), c2(2, 10), c3; 28 cout << "c1="; c1.display(); 29 cout << "c2="; c2.display(); 30 c3 = c2 - c1; 31 cout << "c3=c2-c1"; c3.display();//使用重载运算符完成复数减法 32 c3 = c1 + c2; 33 cout << "c3=c1+c2="; c3.display();//使用重载运算符完成复数加法 34 return 0; 35 }
上面的语句:return Complex(this->real + c2.real, this->imag + c2.imag);
可以改写为:Complex Complex::operator+(const Complex& c2)const {
Complex c(this->real + c2.real, this->imag + c2.imag);
return c;
}
4. 前置单目运算符重载:
(1)如果要重载U为类成员函数,使之能够实现表达式U oprd,其中oprd为A类对象,则U应被重载为A类的成员函数,无形参。
(2)经重载后,表达式U oprd相当于oprd.operator U()。
5. 后置单目运算符++和--重载
(1)如果要重载++和--类成员函数,使之能够实现表达式oprd++或者oprd--,其中oprd为A类对象,则++和--应被重载为A类的成员函数,且具有一个int类型形参。
(2)经重载后,表达式oprd++相当于oprd.operator ++()。
(3)前置单目运算符重载为成员函数时没有形参;后置单目运算符重载为成员函数时需要有一个int型形参,该int型参数在函数体中并未使用,纯粹只是用来区别前置和后置,所以参数表中可以只给出类型名,没有参数名。
·实例2(时钟类):
1 #include<iostream> 2 using namespace std; 3 4 class Clock { 5 public: 6 Clock(int hour = 0, int minute = 0, int second = 0); 7 void showTime()const; 8 Clock& operator++();//前置单目运算符重载 9 Clock operator++(int);//后置单目运算符重载 10 private: 11 int hour, minute, second; 12 }; 13 Clock::Clock(int hour/*=0*/, int minute/*=0*/, int second/*=0*/) 14 { 15 if (0 <= hour && hour < 24 && 0 <= minute && minute < 60 && 0 <= second && second, 60) { 16 this->hour = hour; 17 this->minute = minute; 18 this->second = second; 19 } 20 else 21 cout << "Time error!" << endl; 22 } 23 void Clock::showTime()const { 24 cout << hour << ":" << minute << ":" << second << endl; 25 } 26 Clock& Clock::operator++() {//前置单目运算符重载函数 27 second++; 28 if (second >= 60) { 29 second -= 60; 30 if (minute >= 60) { 31 minute -= 60; 32 hour = (hour + 1) % 24; 33 } 34 } 35 return(*this); 36 } 37 Clock Clock::operator++(int) {//后置单目运算符重载函数 38 Clock old = *this; 39 ++(*this); 40 return old; 41 } 42 int main() { 43 Clock myClock(23, 59, 59); 44 cout << "First time output:"; 45 myClock.showTime(); 46 cout << "Show myClock++:"; 47 (myClock++).showTime(); 48 cout << "Show ++myClock:"; 49 (++myClock).showTime(); 50 return 0; 51 }
二、 重载为非成员函数
1.定义:
1 返回类型 operator 运算符(形参表) 2 { 3 函数体 4 } 5 参数个数=原操作数个数(后置++、--除外)
2. 函数的形参代表依自左至右次序排列的各操作数。
3. 至少应该有一个自定义类型的参数。
4. 如果在运算符的重载函数中需要对操作某类对象的私有成员,可以将此函数声明为该类的友元函数。
5. 双目运算符B重载后:
表达式oprd1 B oprd2相当于operator B(oprd1,oprd2)。
6. 前置单目运算符B重载后:
表达式B oprd相当于operator B(oprd)。
7. 后置单目运算符++和--重载后:
表达式B oprd相当于operator B(oprd,0)。
·实例3(复数加减运算):
1 #include<iostream> 2 using namespace std; 3 4 class Complex { 5 public: 6 Complex(double r=0.0,double i=0.0):real(r),imag(i){} 7 friend Complex operator+(const Complex& c1, const Complex& c2);//运算符+重载 8 friend Complex operator-(const Complex& c1, const Complex& c2);//运算符-重载 9 friend ostream& operator<<(ostream& out, const Complex& c);//运算符<<重载 10 private: 11 double real, imag; 12 }; 13 Complex operator+(const Complex& c1, const Complex& c2) { 14 return Complex(c1.real + c2.real, c1.imag + c2.imag); 15 } 16 Complex operator-(const Complex& c1, const Complex& c2) { 17 return Complex(c1.real - c2.real, c1.imag - c2.imag); 18 } 19 ostream& operator<<(ostream& out, const Complex& c) { 20 out << "(" << c.real << "," << c.imag << ")"; 21 return out; 22 } 23 int main() { 24 Complex c1(5, 4), c2(2, 10), c3; 25 cout << "c1=" << c1 << endl; 26 cout << "c2=" << c2 << endl; 27 c3 = c1 - c2; 28 cout << "c3=c1-c2=" << c3 << endl; 29 c3 = c1 + c2; 30 cout << "c3=c1+c2=" << c3 << endl; 31 return 0; 32 }
1.上面将<<(双目)重载为非成员函数,并将其声明为复数类的友元,它的左操作数是ostream类型的引用,右操作数是Complex类型的引用,这样在执行cout<<c1时,就会调用用operator<<(cout,c1)。该函数把通过第一个参数传入的ostream对象以引用形式返回,用以支持下面形式的输出:
cout<<a<<b;
该输出调用的是:
operator<<(operator<<(cout,a),b);
2.以非成员函数重载,可以直接使用5.0+c1或者c1+5.0,因为Complex的构造函数使得实数可以被隐含转换为Complex类型。而以成员函数重载时,左操作数必须具有Complex类型,不能是实数,只有右操作数可以是实数。
三、虚函数
·基本知识
1. 虚函数的声明:
virtual 函数类型 函数名(形参表);
2. 虚函数声明只能出现在类定义的函数原型声明中,不能在成员函数实现的时候。
3. 虚函数是动态绑定的基础,必须是非静态的成员函数。虚函数经过派生之后,在类族中就可以实现运行过程中的多态。
4. 如果需要通过基类得指针指向派生类的对象,并访问某个与基类同名的成员,那么首先在基类中将这个同名函数说明为虚函数。这样,通过基类类型的指针,就可以使不同派生类的不同对象产生不同的行为,从而实现运行过程中的多态。
5. 运行时的多态需要满足以下三个条件:
·满足赋值兼容规则;
·要声明虚函数;
·由成员函数来调用或者是通过指针、引用来访问虚函数。(如果是使用对象名来访问虚函数,则绑定在编译过程中就可以进行(静态绑定),而无须在运行过程中进行)
6. 虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的。但将虚函数声明为内联函数也不会引起错误。
7. 构造、静态函数不能是虚函数,析构函数可以是虚函数。
·具体讲解
·实例4:(书例7-3):
1 #include<iostream> 2 using namespace std; 3 4 class Base1 { 5 public: 6 virtual void display()const;//虚函数 7 /*可在该语句前加virtual,可以更清楚地提示这是一个虚函数,也可不加*/ 8 }; 9 void Base1::display()const { 10 cout << "Base1::display()" << endl; 11 } 12 class Base2:public Base1 { 13 public: 14 void display()const;//覆盖基类的虚函数 15 }; 16 void Base2::display()const { 17 cout << "Base2::display()" << endl; 18 } 19 class Derived :public Base2 { 20 public: 21 void display()const;//覆盖基类的虚函数 22 }; 23 void Derived::display()const { 24 cout << "Derived::display()" << endl; 25 } 26 void fun(Base1* ptr) {//参数为指向基类对象的指针 27 ptr->display();//“对象指针->成员名” 28 } 29 int main() { 30 Base1 base1; 31 Base2 base2; 32 Derived derived; 33 fun(&base1);//用Base1对象的指针调用fun函数 34 fun(&base2);//用Base2对象的指针调用fun函数 35 fun(&derived);//用Derived对象的指针调用fun函数 36 return 0; 37 }
1.在本程序中,派生类并没有显式给出虚函数声明virtual,这时系统就会遵循以下规则来判断派生类的一个函数成员是不是虚函数。如果确定为虚函数,这时,派生类的虚函数将覆盖基类的虚函数。
·该函数是否与基类的虚函数有相同的名称
·该函数是否与基类的虚函数有相同的参数个数及相同的对应参数类型
·该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值
2.一般习惯于在派生类的函数中也使用virtual关键字,以增强程序的可读性
3.如果把fun函数中的ptr->display()改为ptr->Base::display(),无论ptr所指向对象的动态类型是什么,最终被调用的总是Base1类的display()函数。在派生类的函数中,有时就可以用“基类名::函数名(…)”来调用基类中被覆盖的函数。
4.在重写继承来的虚函数时,如果函数有默认形参值,不要重新定义不同的值。虽然虚函数是动态绑定的,但默认形参值是静态绑定的。也就是说,通过一个指向派生类对象的基
类指针,可以访问到派生类的虚函数,但默认形参之却只能来自基类的定义。
例如:将实例4中的fun函数的参数类型设定为Base1而非Base1*,那么3次fun函数的调用中,被执行的都会是Base1::display()。因为基类的对象不能表示派生类的对象
5.只有通过基类的指针或引用调用虚函数时,才会发生动态绑定。
例如:将实例4中的fun函数的参数类型设定为Base1而非Base1*,那么3次fun函数的调用中,被执行的都会是Base1::display()。因为基类的对象不能表示派生类的对象。
1 Derived d; //定义派生类对象 2 Base* ptr = &d;//基类指针ptr可以指向派生类对象 3 Base& ref = d;//基类引用ref可以作为派生类对象的别名 4 Base b = d;//调用Base1的复制构造函数用d构造b,b的类型是Base而非Derived