对于c++多态的总结:重载(函数、运算符)、隐藏(屏蔽) 及 覆盖(重写)

目录

重载

函数重载

运算符重载

隐藏,屏蔽overwrite

重写,覆盖override


重载

函数重载

作用:能使用多个同名的函数,即允许函数有多种形式。

重载的关键是函数的形参列表—也称为函数特征标(function signature)。如果两个函数的参数数目,参数类型,参数的排列顺序都相同,则他们的特征标相同。C++允许定义同名的函数前提是他们的特征标不同,只要上述特征标有一个不同则函数可重名。

例如下面的都是可重载的同名函数:

void print(const char* str, int width);
void print(char* str, int width);
void print(double length, int width);
void print(int i, int width);
void print(unsigned int i, int width);
void print(const char* str);

编译器将根据所采取的用法即函数实参的类型,数目,顺序来匹配相应的函数。使用被重载函数时必须使用目标函数对应的特征标,保证三个相同。注意返回值不是特征标!!

注意:如果没有完全匹配的函数,就可能会有类型强制转换进行匹配。

类型本身和类型引用对编译器来说是相同的,所以不可以这样重载

例如:

double foo(double x);
double foo(double& x); 

匹配函数是会区分const和非const,但是非const会强制转换为const进行匹配,c++不允许const转化为非const,所以不匹配。换句话说就是将非const值赋给const变量是合法的,反之是不合法的。

例如:

void foo1(char* bits);
void foo1(const char* bits);
void foo2(char* bits);
void foo3(const char* bits);
//下面为调用
const char str1[5]="Over"; //const
char str2[5]="Load"; //非const 

foo1(str1); // foo1(const char* bits);
foo1(str2); //foo1(char* bits);
foo2(str1); //不合法 
foo3(str2); //foo3(const char* bits);

重载引用参数:

double foo(double& x1);
double foo(const double& x2);
double foo(double&& x3);
  1. 左值引用参数 x1 与可修改的左值参数匹配。
  2. const左值引用参数 x2 与可修改的左值引用参数、const左值参数和右值参数匹配。
  3. 右值引用参数 x3 与右值匹配。
double a=1.1;
const double b=2.2;
foo(a); //foo(double& x1);
foo(b); //foo(const double& x2);
foo(a+b); //foo(double&& x3);

何时使用函数重载:

当两个函数的参数列表只有数目不同时,可以考虑使用默认形参,这样可以减少程序所需要的内存。而当参数类型不同时只能使用重载了。

C++如何实现重载:

编译器采用名称修饰(name decoration)或叫名称矫正(name mangling)来区分不同的重载函数,可以这么理解:函数 int foo( int a, double b) 在编译时会被改名成int foo_int_double(int a,double b),于是便区分出了不同的重载函数。编译器再调用时会把main函数里的重载函数根据参数列表改名成相应的重载函数,然后才调用。

运算符重载

将函数重载的思想运用到运算符上,就是运算符的重载,这使得一个运算符根据参数的不同而具有不同的意义。比如一只狗加一只狗等于两只狗,那一只狗加一只猫会等于什么呢,这就由运算符重载来定义啦。

重载格式:

返回值 类名::operator 待重载的运算符 ( 运算符的右操作数 )

这时左操作数数为该类对象本身,通过this指针隐式传递(因为是类成员函数),右操作数如果没有可以不写(当重载的是一元运算符时)。

比如定义如下 Object 类,在其中重载(+)和(=),具体实现省略。这样便实现了让两个 Object相加 以及 将一个 Object 赋值给另一个 Object 的操作。

class Object{
private:
	int width;
	int length;
public:
	Object();
	Object operator=(Object& temp);
	Object operator+(Object& temp);
};
int main(){
	Object obj1,obj2,obj3;
	obj1=obj2;
	obj3=obj1+obj2;
}

1.重载的运算符不一定必须是成员函数,但必须有一个操作数是用户自己定义的类型,这样可以防止对标准类型进行重载。比如,不能把(-)重载为计算两int之和

2.重载不能改变运算符原来的句法规则,比如求模运算符(%),不能重载为只有一个操作数的运算符。

3.不改变原来运算符的优先级,重载的(+)与原来的(+)有相同的优先级

4.不创建新的运算符,比如不能定义一个operator**()来求幂(c++没有求幂运算符哦)

不能被重载的运算符

sizeofsizeof 运算符
.成员运算符
.*成员指针运算符
::作用域解析运算符
?:条件运算符
typeid一个RTTI运算符
const_cast强制类型转换运算符
dynamic_cast强制类型转换运算符
reinterpret_cast强制类型转换运算符
static_cast强制类型转换运算符

可重载的运算符

+-*/%^
&|~=!=<
>+=-=*=/=%=
^=&=|=<<>>>>=
<<===!=<=>=&&
||++--,->*->
()[ ]newdeletenew[ ]delete[ ]

自增运算符与自减运算符

这两个运算符有两种模式,前缀和后缀,在重载时也要注意区分

返回值 类名::operator ++( ){ }  前缀模式

返回值 类名::operator ++( int ){ } 后缀模式

这里的 int 是一个虚拟形参在函数实体中没有什么实际作用,只是一个约定告诉编译器这是再后缀模式下重载。

#include <iostream>
using namespace std;
class integer{
private:
	int x;
public:
	integer(int xx=0):x(xx){}
	integer& operator ++(){
		cout<<"前缀重载++"<<' ';
		++x;
		return *this; 
	}
	integer operator ++( int ){
		cout<<"后缀重载++"<<' ';
		return integer(x++); 
	}
	void print(){
		cout<<"x的值是:"<<x<<endl;
	}
};
int main(){
	integer a(1);
	a.print();
	integer b=a++;
	b.print();
	integer c=++a;
	c.print();
} 

上面并没有重载赋值运算符,因为会有一个默认的赋值运算符,仅仅是把所有成员变量的值复制过去,一旦涉及到指针又可能会出现深拷贝浅拷贝的问题。 

根据前面我们可以重载运算符使得运算符可以进行我们定义的 比如,Object+Object,Object+2等等的运算,但是我要计算2*Object呢,这显然用之前的方法是行不通的,因为要求左操作数必须是用户自己定义的类。那我在类外定义一个重载函数不就行了?但这产生了一个新问题,类外函数无法访问类内私有成员。

友元——完美解决上述问题

把类外的函数声明为友元,使得它可以访问类内的私有成员。只需要在函数前加 friend 关键词就可以了,注意是在类内声明的时候加,类外定义的时候不加

使用了friend后,就告诉类说这个函数是我们的好朋友,不用藏着掖着让我康康!

友元机制是否违背了OOP数据隐藏的原则?

这种观点未免太片面……类声明仍然控制了哪些函数可以访问私有数据……类方法和友元只是表达接口的两种不同的机制。——《C++ Primer Plus》

常用的友元:重载<<,>>

Ostream,istream 返回类型为对应引用,记得return 相应对象

例如:

class Object{
private:
	int width;
	int length;
public:
	Object();
	Object operator=(Object& temp);
	Object operator+(Object& temp);
    friend ostream& operator<<(ostream& os, const Object temp);
    friend istream& operator>>(istream& is, Object temp);
};

ostream& Object::operator<<(ostream& os, const Object temp){
    os << temp.width << temp.length << std::endl;
    return os;
}
istream& Object::operator>>(istream& is, Object temp){
    is >> temp.width >> temp.length ;
    return is;
}

隐藏,屏蔽overwrite

屏蔽基类的函数定义

• 派生类的函数与基类的函数同名,但是参数列表有所差异

• 派生类的函数与基类的函数同名参数列表也相同,但是基类函数没有virtual关键字

定义如下类,以及类内相同名称的函数 void say(); 打印对应类名。

#include <iostream>

class Fruit{
public:
    void say() {
        printf("I'm a fruit!\n");
    }
};

class Apple : public Fruit{
public:
    void say() {
        printf("I'm an apple!\n");
    }
};

class BlueApple : public Apple{
public:
	void say(){
		printf("I'm a blueapple!\n");
	}
};
int main(){
	Fruit a;
    Apple b;
    BlueApple c;
    
    a.say();
    b.say();
    c.say();
    
}

上述程序运行结果为

当调用 c.(); 时,编译器会沿着BlueApple的继承树向上寻找第一个 say() 来调用,假如此时我们不在BlueApple类中实现 say() 函数,那么执行c.say()时就会执行Apple类中的 say() 函数。

运行结果变为:

 当我们使用指针来访问 say() 时,会调用指针对应的类成员函数

当使用基类指针指向派生类时,如下面的base指针,调用say()会调用基类中的say()函数。

int main(){
	Fruit a;
    Apple b;
    BlueApple c;
    Fruit* aa=&a;
    Apple* bb=&b;
    BlueApple* cc=&c;
    Fruit* base=&b; 
    
    aa->say();
    bb->say();
    cc->say();
    base->say();
    
}

int main(){
	Fruit a;
    Apple b;
    BlueApple c;
    Fruit& aa=a;
    Apple& bb=b;
    BlueApple& cc=c;
    Fruit& base=b; 
    
    aa.say();
    bb.say();
    cc.say();
    base.say();
    
}

使用引用来调用函数时情况与使用指针时相同,会调用对应类的say()函数。

重写,覆盖override

修改基类函数定义

• 基类或非直接基类,至少有一个成员函数被 virtual 修饰

派生类虚函数必须与基类虚函数有同样签名(或特征标),即函数名,参数类型,顺序和数量都必须相同。

一旦函数被virtual修饰,在其今后的派生类中始终为虚函数

函数被virtual修饰后,只会保留最后一个同特征标函数,不管用指针还是引用都将无法访问到被覆盖的函数,如下代码将全部调用最后 BlueApple 类中的 say() 函数。

#include <iostream>

class Fruit{
public:
    virtual void say() {
        printf("I'm a fruit!\n");
    }
};

class Apple : public Fruit{
public:
    void say() {
        printf("I'm an apple!\n");
    }
};

class BlueApple : public Apple{
public:
	void say(){
		printf("I'm a blueapple!\n");
	}
};
int main(){
	Fruit a;
    Apple b;
    BlueApple c;
	Fruit* baseptr = &c;
    Fruit& base=c; 
    
    c.say();
    baseptr->say();
    base.say();
}

当然如果一定要访问基类中的函数也是可以做到的,使用作用域解析运算符(::)来选择想要调用的类中的函数即可。

int main(){
	Fruit a;
    Apple b;
    BlueApple c;
	Fruit* baseptr = &c;
    Fruit& base=c;
    
    c.Fruit::say();
    baseptr->Fruit::say();
    base.Fruit::say();
}

 但是这个时候又要注意一个问题:作用域的类 必须是 对象、其指针或其引用的基类,如果不是就会报错,如下:

int main(){
	Fruit a;
    Apple b;
    BlueApple c;
	Fruit* baseptr = &c;
    Fruit& base=c;
    
    a.Apple::say();
    baseptr->Apple::say();
    base.Apple::say();
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值