面向对象程序设计的三大特性,封装性是基础、继承性是关键、多态性是补充。
多态分为静态多态和动态多态。函数重载、运算符重载以及模板,都属于静态多态,也称编译时多态;动态多态也称运行时多态,即在程序运行阶段才能确定关系。
运算符重载
运算符重载有几点限制:
- 只能重载C++已有的运算符,不可臆造
- 不能改变运算符的语法结构,如一元运算符只能重载为一元
- 不能改变运算符的优先级和结合性
C++不允许重载的运算符有5个:
? :
条件运算符.
成员访问运算符.*
成员指针访问运算符::
作用域运算符sizeof
求字节数运算符
运算符重载的两种方式
重载为类的成员函数
类内定义
<数据类型> operator <操作符> ( [ <参数列表> ] )
{···}
类外定义
<数据类型> <类名> :: operator <操作符> ( [ <参数列表> ] )
{···}
运算符重载的关键字operator
,当编译器看到operator
,就知道这是一个运算符重载函数
顺便说一句,前边提到内联函数和外联函数的区别,那这里如果操作符重载后使用得多,就在定义为外联函数,使用得少内联外联都可以
#include <iostream>
using namespace std;
class Complex{
double real,image;
public:
Complex(double r = 0, double i = 0):real(r),image(i){};
//Complex(){}; //上面构造函数的参数已经有缺省值了,再定义这个就报错了
// "Call to constructor of 'Complex' is ambiguous" 对“Complex”构造函数的调用不明确
Complex operator+(const Complex &c){
Complex t;
t.real = real + c.real;
t.image = image + c.image;
return t;
};
Complex operator+(double r){
return Complex(real+r,image);
};
Complex operator-(const Complex &c);
Complex operator-(double r);
Complex operator-(void);
Complex operator*(const Complex &c);
Complex operator/(const Complex &c);
void show(){
cout<<real;
if(image>0)
cout<<"+"<<image<<"i"; //需显式输出+
else if(image<0) cout<<image<<"i"; //负数会自动输出-
cout<<endl;
}
};
Complex Complex::operator-(const Complex &c) {
return Complex(real-c.real,image-c.image);
}
Complex Complex::operator-(double r) {
return Complex(real-r,image);
}
Complex Complex::operator-() {
return Complex(-real,0-image);
}
Complex Complex::operator*(const Complex &c){
double r,i;
r = real*c.real - image*c.image;
i = real*c.image + image*c.real;
return Complex(r,i);
}
Complex Complex::operator/(const Complex &c){
double t,r,i;
t = c.real*c.real + c.image*c.image;
r = (real*c.real + image*c.image)/t;
i = (image*c.real - real*c.image)/t;
return Complex(r,i);
}
int main(){
Complex c1(2,3),c2(4,-2),c3;
cout<<"c1 = ";
c1.show();
cout<<"c2 = ";
c2.show();
c3 = 5.0 ; //A
//好家伙,我直接好家伙,又get到了奇奇怪怪的东西
// 和 c3 = (1,4,5.0) 一样,调用构造函数,第二个参数缺省
cout<<"c3 = ";
c3.show();
c3 = c1+c2;
cout<<"c1+c2 = "; // B
c3.show();
c3 = c1 + 5;
cout<<"c1+5 = ";
c3.show();
c3 = c1 - c2;
cout<<"c1-c2 = ";
c3.show();
c3 = c1 - 5;
cout<<"c1-5 = ";
c3.show();
c3 = -c1; // C
cout<<"-c1 = ";
c3.show();
c3 = c1*c2;
cout<<"c1*c2 = ";
c3.show();
c3 = c1/c2;
cout<<"c1/c2 = ";
c3.show();
c3 = (c1+c2)*(c1-c2)*c2 / c1;
cout<<"(c1+c2)*(c1-c2)*c2/c1 = ";
c3.show();
}
A
行,由于赋值号两边的数据类型不一样,C++自动处理成c3 = Complex(5.0)
,第二个参数缺省,只是搞不懂为什么c3 = (1,2,3,5.0)
的结果也如上
其实上面定义的二元操作符,如B
行中c1+c2
,编译器将此解释为c1.operator+(c2)
只是特别的C
行,c3 = -c1
,编译器将此解释为c3 = c1.operator-()
所以,当成员函数重载二元操作符时,成员函数有一个参数,二元运算的第一个运算量是调用该操作符重载函数的对象自身,而第二个运算量就是函数的参数了;当成员函数重载一元操作符时,成员函数没有参数,一元运算的唯一运算量就是调用运算符重载函数的对象自身
重载为友元函数
友元函数:
- 不是成员函数
- 可以直接访问对象的私有成员
在操作符重载为成员函数时,二元操作符只有一个参数,那是因为操作符的第一个运算量对象进行调用操作符重载函数,第二个运算量对象作为参数;而重载为友元函数时,二元运算符有两个参数,一元运算符只有一个参数,上面第1点,不是成员函数,没有调用函数一说
类内声明:
friend <数据类型> operator<重载运算符> ( [<参数列表>] );
类外定义:
<数据类型> operator<重载运算符> ( [<参数列表>] ) //类外定义时,就不需要关键字friend了
{···}
这里举两个例子
class Complex{
friend Complex operator+(const Complex &c1,const Complex &c2);
friend Complex operator-(const Complex &c1);
};
Complex operator+(const Complex &c1,const Complex &c2){
return Complex(c1.real+c2.real,c1.image+c2.image);
}
Complex operator-(const Complex &c){
return Complex(-c.real,-c.image);
}
友元函数不是类的成员函数,所以类的private
,protected
,public
对其无效
还有一种操作符重载方法,不定义为成员函数或友元函数,就是普通的函数
很简单,在类里,定义获取私有成员变量的公有接口
比较两种方法
成员函数和友元函数,哪个更优?
答案是友元函数
举个例子
c3 = c1 + 10; //A
c3 = 10 + c1; //B
A
行两种方法都能得出正确答案,但是B
行只有友元函数可以计算正确
原因就在于运算符重载为成员函数后,运算符的第一个运算量是调用函数的对象,第二个运算量才是作为参数,所以C++会将B
行解释为c3 = 10.operator+(c1)
,这是什么啊?
而友元函数,会将B
行解释为c3 = operator+(10,c1)
,实参10
不是operator
类型,但在函数调用时实参自动转换成形参类型的量并赋值给形参,相当于c3 = operator+(Complex(10),c1)
重载 ++ 、- -
C++对自加
和自减
符区分前置还是后置,是通过是否在参数列表插入关键字int
前置:
Complex operator++(){} //成员函数
friend Complex operator++(Complex &obj){} //友元函数
友元函数的参数obj
是调用该函数的对象自身
后置:
Complex operator++(int){}
friend Complex operator++(Complex &obj,int){}
关于 = 和 +=
何时必须重载
emm这就是浅拷贝和深拷贝的问题了
如果类中有指针指向动态分配的存储空间,就必须定义赋值运算符重载函数
= 和 += 重载函数返回值类型
void
和本类类型
这乍一看吧,用void
作为返回类型好像并没有什么问题,重载为成员函数或友元函数,都可以在函数体中直接访问需要赋值的成员变量,没必要再return
一下
可偏偏有个例外
c1 += c2 += c3
由于复合赋值运算符+=
的运算结合性是自右向左
的,编译器将表达式处理成c1.operator+=(c2.operator+=(c3))
,如果操作符+=
返回类型是void
不就完?
返回本类对象或其引用
虽然引用类型的返回
少见,但不代表没有
Complex& Complex::operator+=(const Complex &c){
real += c.real;
image += c.image;
return *this;
}
this
关键字,是指向对象自身的指针,是类中没有显示出来的指针,不会随着函数的结束而被程序撤销
这里的+=
重载函数的返回值类型,是类的引用
,既然是引用
,那就相当于是别名
,如果函数返回的一个在函数内定义的局部对象,但函数return
后局部对象就被撤销了,不存在了,那这个别名
还有意义吗?所以可以这么说,引用类型的返回值
必须是*this
,指向自身的指针的引用
//错误示例
Complex& Complex::operator+=(const Complex &c){
real += c.real;
image += c.image;
Complex temp(real,image); //Complex temp = *this;
return temp;
}
//正确示例
Complex Complex::operator+=(const Complex &c){
real += c.real;
image += c.image;
Complex temp(real,image);
return temp; // return *this;
}
所以可以总结为
返回引用,引用是别名,
return
的必须是被调函数结束后仍然存在的对象
返回对象,需要初始化内存的临时变量,然后内存的临时变量作为return
结果,所以return
的不管被调函数结束后是否仍然存在
但还有一点要注意,初始化内存的临时变量,是通过拷贝构造函数
#include <iostream>
#include <cstring>
using namespace std;
class String{
char *strp;
public:
String(char *s){
if(s){
strp = new char[strlen(s)+1];
strcpy(strp,s);
}else strp = 0;
}
String& operator=(const String &s){
if(this == &s) return *this; // 自己赋值给自己
if(strp) delete []strp;
if(s.strp){
strp = new char[strlen(s.strp)+1];
strcpy(strp,s.strp);
} else strp = 0;
return *this;
}
~String(){
if(strp) delete []strp;
}
}
int main(){
String s1("123456");
String s2;
s2 = s1;
return 0;
}
main()
函数中只用到了=
操作符重载函数,所以这里没有拷贝构造函数也无所谓
小结
- 定义运算符重载函数时的形数不允许有缺省值。因为形参对应的实参肯定有运算量,不会出现缺省情况,所以这里干脆规定死
- C++中唯一不能被派生类继承的是赋值运算符重载函数
- 若用户未显式定义,编译器会未类自动生成
构造函数
,析构函数
,拷贝构造函数
,赋值运算符重载函数
类型转换函数
C++允许不同数据类型变量之间互相赋值,赋值时系统会自动进行类型转化
但对于自定义的类,情况就比较复杂了,例如Complex c(10,2)
和double n = 6.0
,他们之间进行赋值运算,C++会将c = n
处理成c = Complex(n)
,将n = c
处理成n = double(c)
,前者是调用了构造函数,但后者呢?对于后者,就必须为Complex
定义一个类型转换函数,才能将对象c
转换成double
型量并赋值给n
// 格式
operator <目标类型>()
{···}
operator double(){
return real;
}
注意几点
- 无参数
- 无返回值类型
- 函数名是
operator <目标类型>
- 只能是成员函数
静态联编
静态联编是指联编出现在编译链接阶段,又称为早期联编,通过静态联编可实现静态多态。函数重载,运算符重载都属于静态多态,函数调用关系的确定都是在编译阶段
普通函数的静态联编
int add(int a,int b){
return a+b;
}
double add(double a,double b){
return a+b;
}
int main(){
cout<<add(1,2)<<endl;
cout<<add(1.1,2.2)<<endl;
}
程序输出3
和3.3
在编译阶段,编译器根据参数的个数和类型确定调用哪一个函数,这就是静态联编。
类的成员函数的静态联编
#include <iostream>
using namespace std;
class Point{
protected: // 可别漏了!!!
double x,y;
public:
Point(double a = 0,double b = 0):x(a),y(b){}
double Area(){ return 0.0; } //函数1
};
class Rectangle: public Point{
protected:
double x1,y1;
public:
Rectangle(double a = 0, double b = 0, double c = 0, double d = 0):Point(a,b)
{ x1 = c;
y1 = d;
}
double Area(){ //函数2
return (x-x1)*(y-y1);
}
};
class Circle: public Point{
protected:
double r;
public:
Circle(double a = 0,double b = 0,double c = 0):Point(a,b),r(c){}
double Area() //函数3
{
return 3.14*r*r;
}
};
double CalcArea(Point &p){ // A 行
return p.Area();
}
int main(){
Rectangle r(0,0,1,1);
Circle c(0,0,1);
cout<<CalcArea(r)<<" "<<CalcArea(c)<<endl;
}
这里注意看函数CalcArea
的参数,是基类的对象引用,但main()
中调用两次该函数,传的参数是派生类,也就是派生类对象初始化基类的引用,这里可以参考赋值兼容;另外基类的两个成员变量x
和y
,访问类型可不能省略,否则就是private
,如此在派生类中就无法直接使用了
都是细节,这不得把握好?
程序最后的输出是0
和0
这两次传递的参数分别是Point &p = r
和Point &p = c
,即基类对象引用派生类对象,那么p
引用的是派生类对象中基类的部分,又因为p
是Point
类对象,所以程序在A
行时,调用函数1
动态联编
程序中若出现函数调用,但在编译阶段无法确定调用哪一个函数,只有到了程序的运行阶段才能确定调用哪一个函数,这就是动态联编。动态联编又称滞后联编,晚期联编。动态联编实技术现动态多态,通过虚函数实现。
虚函数的定义
关键字virtual
virtual <数据类型> <函数名> ([<参数列表>])
{···}
上边的例子稍微改一下
class Point{
···
public:
virtual double Area(){ return 0.0; }
};
class Rectangle:public Point{
···
public:
virtual double Area(){
return (x-x1)*(y-y1);
}
};
class Circle:public Point{
···
public:
virtual double Area(){
return 3.14*r*r;
}
};
double CalcArea(Point &p)
{
return p.Area(); // A
}
这时程序再输出,答案就是1
和3.14
了
因为将三个Area()
都定义为虚函数,C++就规定A
行的p.Area()
函数调用的处理方法是:在编译阶段不确定调用哪一个函数,而是在此处保留3个虚函数的入口地址,在程序运行阶段,根据实参
的类型来确定调用3个虚函数中的哪一个。
关于虚函数
-
当在基类中把成员函数定义为虚函数后,若派生类欲定义同名虚函数,则派生类中的虚函数,除了函数体可以改变,其余一律必须和基类的虚函数相同。
-
基类的虚函数的关键字
virtual
不能缺省,但派生类中同名虚函数可以,缺省后仍然是虚函数
派生类有与基类虚函数同名的函数,但又没有
virtual
修饰,这不一定就是同名虚函数,也可能是重载函数,只要参数列表不同
- 动态多态必须通过基类对象的引用或基类对象的指针调用虚函数才能实现
double CaleArea(Point *p){
return (p->Area());
}
int main(){
Rectangle r(0,0,1,1);
cout<<CaleArea(&r);
}
- 友元函数不能定义为虚函数,因为友元函数不是成员函数
- 静态成员函数不能定义为虚函数,因为静态成员函数属于类,与具体的某个对象无关
- 内联函数不能定义为虚函数,因为内联函数的调用是在编译时刻,即在编译时刻,用内联函数的实现代码替换函数调用,运行时内联函数已不存在;而虚函数的调用是动态联编,即运行时刻决定调用哪一个函数
这里指的内联函数,应该是用关键字
inline
修饰的内联函数,而不是类内定义的函数(默认是内联函数),可能是在编译时,如果定义在类内的函数,没有关键字virtual
修饰,才将此纳为内联
- 不能将构造函数定义为虚函数,但可以将析构函数定义为虚函数
- 虚函数与一般函数相比,调用时的执行速度要慢一些。这是因为,为了实现动态联编,编译器为每个含有虚函数的对象增加指向虚函数地址表的指针,通过该指针实现虚函数的间接调用。因此除非必须使用虚函数补充功能,否则一般不使用虚函数
- 在一般成员函数中调用虚函数,遵循动态多态规则。但在构造函数中调用虚函数,不遵循动态多态规则
#include <iostream>
using namespace std;
class A{
public:
virtual void f1(){ // D
cout<<"A1"<<" ";
f2();
}
void f2(){
cout<<"A2"<<" ";
f3(); // E
}
virtual void f3(){ // F
cout<<"A3"<<" ";
f4();
}
void f4(){
cout<<"A4"<<" ";
}
};
class B: public A{
public:
void f3(){ //虚函数
cout<<"B3"<<" ";
f4();
}
void f4(){
cout<<"B4"<<" ";
}
};
int main(){
A a;
B b;
a.f1();
b.f1(); // G
}
程序运行结果:
A1 A2 A3 A4
A1 A2 B3 B4
第一行容易理解,第二行就困难了
B
类一共有6个成员函数,分别是4个继承下来的,2个自定义的
程序执行b.f1()
,先调用A
类的f1()
,然后f1()
调用A
类的f2()
,然后呢?f2()
调用哪一个f3()
在一个成员函数中调用其他成员函数时,系统是通过对象自身的指针this
调用的
所以A
类的f2()
继承到B
类中后实际被处理成如下形式:
void f2(){
cout<<"A2"<<" ";
this->f3(); // E
}
此时,this
是基类A
类型的指针,但它指向派生类对象b
。因为在执行G
行时,会将指向派生类对象b
的指针转递给f1()
进而传递给f2()
,所以执行到E
行时,由于调用的f3()
是虚函数,调用的自然就是派生类B
的f3()
即当基类的指针指向派生类对象时,若通过它调用虚函数,则它指向的是哪个类的对象,调用的就是哪个类的虚函数
如果将基类的两个virtual
关键字去掉,则运行结果是
A1 A2 A3 A4
A1 A2 A3 A4
似乎和前面提到过的支配规则矛盾?
其实这是两回事,支配规则说的是,对象调用函数时,优先调用本类的同名函数。
但这里b
一开始调用的是继承下来的f1()
,而后面调用的f3(),f4()
上面都解释了,是通过this
调用的。
b.f3();
b.f4();
//输出: B3 B4
这才是体现了支配规则
纯虚函数和抽象类
在定义一个基类时,会遇到这种情况:无法给出某些成员函数的具体实现。例如:描述一个图形形状的Shape
类,从抽象思维考虑,这个图形应该具备一些公共的数值属性(图形的颜色,边长或者周长等)以及一些通用的操作(求面积,绘制图形)。这些都通过成员函数实现,但在描述抽象的Shape类时,无法给出这些通用操作的具体实现(函数定义)。
如果由抽象Shape
类派生出具体的形状如,点类Point
,长方形类Rectangle
,圆类Circle
,在派生类中就可以给出通用操作(求周长面积绘制图形等)的具体实现,而且每个派生类对这些通用操作的实现时不同的
C++中,把基类中没有给出具体实现的函数定义为纯虚函数
virtual <返回类型> <函数名> ([<参数列表>]) = 0;
这里是没有函数体的,用=0
代替函数体,但这不等同于空函数体{ }
把含有纯虚函数的类,称为抽象类
#include <iostream>
using namespace std;
class Shape{
public:
virtual double Area() = 0;
virtual void Draw() = 0;
};
class Point:public Shape{
protected:
double x,y;
public:
Point(double a = 0,double b = 0):x(a),y(b){}
double Area(){ return 0.0; }
void Draw(){ cout<<" Drwa Point! "<<endl;
};
class Rectangle:public Point{
protected:
double x1,y1;
public:
Rectangle(double a = 0,double b = 0,double c = 0,double d = 0):Point(a,b)
{
x1 = c;
y1 = d;
}
double Area()
{
return (x-x1)*(y-y1);
}
void Draw(){ cout<<" Draw Rectangle "<<endl; }
};
class Circle: public Point{
protected:
double r;
public:
Circle(double a = 0,double b = 0,double r = 0):Point(a,b)
{
this->r = r;
}
double Area()
{
return 3.14*r*r;
}
void Draw()
{
cout<<" Draw Circle "<<endl;
}
};
double CaleArea(Shape &s) //抽象类的用处体现出来了
{
return s.Area();
}
void DrawShape(Shape *s) //动态联编,除了抽象类,包含3个虚函数的接口
{
s->Draw();
}
关于纯虚函数和抽象类
- 抽象类只能作为派生的基类,不能定义抽象类的对象
- 若派生类实现了基类的所有纯虚函数,那派生类就不是抽象类,反之则是,简单来说,只要含有纯虚函数的类就是抽象类
- 正常来说,纯虚函数没有函数体,但也可在
=0
后面加上无return
的函数体
小结
再总结一下虚函数和纯虚函数的区别
其实很简单
首先:强调一个概念
定义一个函数为虚函数,不代表函数为不被实现的函数。
定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
定义一个函数为纯虚函数,才代表函数没有被实现,就是没实现的虚函数。
C++中虚函数的作用:
1、简单地说,那些被virtual关键字修饰的成员函数,就是虚函数。
2、实现多态性,多态性是将接口与实现进行分离。
3、当基类指针指向一个子类对象,通过这个指针调用子类和基类同名成员函数的时候,基类声明为虚函数就会调子类的这个函数,不声明就会调用基类的。
这个比喻很生动
C++中虚函数的用法:
1、比如你有个游戏,游戏里有个虚基类叫「怪物」,有纯虚函数 「攻击」。
2、派生出了三个子类「狼」「蜘蛛」「蟒蛇」,都实现了自己不同的「攻击」函数,比如狼是咬人,蜘蛛是吐丝,蟒蛇把你缠起来。
3、出现好多怪物的时候就可以定义一个 虚基类指针数组,把各种怪物的指针给它,然后迭代循环的时候直接monster[i]->attack()
攻击玩家就行了,大概见下图:
int main()
{
Monster *pMonster[3];
pMonster[0] = new Wolf;
pMonster[1] = new Spider;
pMonster[2] = new Snake;
for(int i=0;i<3;++i){
pMonster[i]->attack();
}
}