一、多态性概述
是指同样的消息被不同类型的对象接受时导致完全不同的行为。 从实现的角度来看,多态可以划分为两类:
- 编译时的多态:在编译的过程中确定了同名操作的具体操作对象;
- 运行时的多态:在程序运行过程中才动态的确定操作所针对的具体对象;
联编(绑定):确定操作的具体对象的过程。
- 静态联编:联编在编译和连接时进行。函数的重载、函数模板的实例化均属于静态联编,优点是程序执行效率高;
- 动态联编:联编在运行时进行。优点是灵活,运行速度慢一些;
二、运算符重载
赋予已有运算符新的功能,使他能够用于特定类型执行特定的操作,使同一个运算符作用于不同类型的数据时导致不同的行为的机制称为运算符重载。
1、运算符重载机制
通过重载一种特殊的函数—运算符函数实现。 实际上是函数的重载 运算符函数
operator 运算符名
2、运算符重载规则
1、可重载的运算符
除以下5个运算符之外,其余全部可以被重载:
. :成员选择运算符 . * :成员指针运算符 :: :作用域分辨符 ?:三目选择运算符 sizeof:计算数据大小运算符
2、运算符的重载规则
- 重载后运算符的优先级与结合性不会改变;
- 不能改变原运算符操作数的个数;
- 不能重载C++中没有的运算符;
- 不能改变运算符的原有语义;
3、重载为类的友元函数
这样可以自由的访问该类的任何成员 定义格式:
friend 函数返回类型 operator 运算符(形参表) { 函数体; }
例子:重载运算符为类的友元函数进行复数类数据运算 使用(a,b)表示一个复数a+bi,复数的运算规则: (a,b)+(c,d)=(a+c,b+d); (a,b)-(c,d)=(a-c,b-d); -(a,b)=(-a,-b); (a,b)++ =(a+1,b);
#include<iostream>
using namespace std;
class Complex
{
private:
double real;
double image;
public:
Complex(double real=0.0,double image=0.0)
{
this->real=real,this->image=image;
}
void display()
{
cout<<"("<<real<<","<<image<<")"<<endl;
}
friend Complex operator +(Complex A,Complex B)
{
return Complex(A.real+B.real,A.image+B.image);
}
friend Complex operator -(Complex A,Complex B)
{
return Complex(A.real-B.real,A.image-B.image);
}
friend Complex operator -(Complex A)
{
return Complex(-A.real,-A.image);
}
friend Complex operator ++(Complex &A)
{
return Complex(++A.real,A.image);
}
friend Complex operator ++(Complex &A,int)
{
return Complex(A.real++,A.image);
}
};
int main()
{
Complex A(2,3);
Complex B(4,5);
Complex C;
cout<<"A=", A.display();
cout<<"B=", B.display();
C=A+B;
cout<<"c=a+b=",C.display();
C=A-B;
cout<<"c=a-b=",C.display();
C=-A;
cout<<"-a=",C.display();
C=++A;
cout<<"++a=",C.display();
C=A++;
cout<<"A++ =",C.display();
C=A+5;
C.display();
return 0;
}
4、重载为类的成员函数
这样运算函数可以自由地访问本类的数据成员,语法格式:
返回类型 类名 :: operator 运算符(形参表) {函数体} 在类中定义则类名::可省
此时,函数的参数个数比原来的操作个数少一个,因为通过对象调用该运算符函数时,对象本身充当了运算符函数最左边的操作数,少的操作数就是该对象本身,由this指针指出。
- 双目运算符重载为类的成员函数时,只显式说明一个参数,即右操作数;
- 前置单目运算符不需要说明参数;
- 后置单目运算符要带有一个整型形参,与前置相区别。
#include<iostream>
using namespace std;
class Complex
{
private:
double real;
double image;
public:
Complex(double real=0.0,double image=0.0)
{
this->real=real,this->image=image;
}
void display()
{
cout<<"("<<real<<","<<image<<")"<<endl;
}
Complex operator +(Complex B)
{
return Complex(real+B.real,image+B.image);
}
Complex operator -(Complex B)
{
return Complex(real-B.real,image-B.image);
}
Complex operator -()
{
return Complex(-real,-image);
}
Complex operator ++()
{
return Complex(++real,image);
}
Complex operator ++(int)
{
return Complex(real++,image);
}
};
int main()
{
Complex A(2,3);
Complex B(4,5);
Complex C;
cout<<"A=", A.display();
cout<<"B=", B.display();
C=A+B;
cout<<"c=a+b=",C.display();
C=A-B;
cout<<"c=a-b=",C.display();
C=-A;
cout<<"-a=",C.display();
C=++A;
cout<<"++a=",C.display();
C=A++;
cout<<"A++ =",C.display();
C=A+5;
C.display();
return 0;
}
总结:
- 一般情况下,单目运算符最好重载为类的成员函数,双目运算符重载为类的友元函数;
- 一些双目运算符不能重载为类的友元函数:=,(),[ ],->;
- 类型转换函数只能定义为成员函数;
- 若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好;
- 若所需的操作数有隐式类型转换,只能选用友元函数;
- 当需要重载运算符的运算具有可交换性时,选择重载为友元函数;
- 当运算符函数是一个成员函数时,最左边的操作数必须是运算符类的一个类对象(或者是对该类对象的引用)。
- 如果左边的操作数是一个不同类的对象,或者是一个基本数据类型的对象,该运算符函数必须作为友元函数。
5、典型运算符重载
1、重载复数赋值 = 运算
注意:=运算符只能重载为类的成员函数,常常用于深拷贝
#include<iostream>
using namespace std;
class Complex
{
private:
double real,image;
public:
Complex(double x=0.0,double y=0.0)
{
real=x,image=y;
}
void display()
{
cout<<"("<<real<<","<<image<<")"<<endl;
}
friend Complex operator +(Complex A,Complex B)
{
return Complex(A.real+B.real,A.image+B.image);
}
Complex operator =(Complex B)
{
real=B.real,image=B.image;
return *this;
}
Complex operator +=(Complex B)
{
real=real+B.real;
image=image+B.image;
return *this;
}
};
int main()
{
Complex A(1,2),B(2,3),C;
C=A+B;
cout<<"c=a+b=", C.display();
C=A;
cout<<"c=a=", C.display();
return 0;
}
2、重载成员指针运算符->
只能重载为类的成员函数 重载成员指针运算符能确保指向类对象的指针总是指向某个有意义的对象(有效内存地址),即创建一个指向对象的指针,否则返回错误信息,这样避免了对空指针、垃圾指针内容的存取。 成员指针运算符的操作数有两个,左边是一个对象指针,右边是一个对象的成员,由于右边的对象成员的类型不能确定,因此只能作为一元运算符重载。
#include<iostream>
using namespace std;
class Complex
{
private:
double real,image;
public:
Complex(double real=0.0,double image=0.0)
{
real=this->real,image=this->image;
}
void display()
{
cout<<"("<<real<<","<<image<<")"<<endl;
}
};
class PComplex
{
private:
Complex *PC;
public:
PComplex(Complex *PC=NULL)
{
this->PC=PC;
}
Complex * operator ->()
{
static Complex NullComplex(0,0);
if(PC==NULL)
{
return &NullComplex;
}
return PC;
}
};
int main()
{
PComplex p1;
p1->display();
Complex c1(1,2);
p1=&c1;
p1->display();
return 0;
}
3、重载下标运算符[ ]
只能重载为类的成员函数 操作对象有两个,左边是一个对象指针,[ ]中间是一个作为下标的整型数,因此作为二元运算符重载。
#include<iostream>
using namespace std;
#include<cstring>
class String
{
private:
char *str;
int len;
public:
void showStr(){
cout<<"string:"<<str<<",length"<<len<<endl;
}
String(const char *p=NULL)
{
if(p)
{
len=strlen(p);
str=new char[len+1];
strcpy(str,p);
}else {
len=0;
str=NULL;
}
}
~String()
{
if(str!=NULL)
{
delete []str;
}
char &operator [](int n)
{
return *(str+n);
}
const char &operator[](int n)const
{
return *(str+n);
}
};
int main()
{
String s1("12344566");
s1.showStr();
s1[3]='a';
cout<<"s1[3]"<<s1[3]<<endl;
const String s2("abcdef");
cout<<"s2[3]"<<s2[3]<<endl;
return 0;
}
三、虚函数
1、静态联编与动态联编
静态联编根据指针和引用的类型而不是根据实际指向的目标确定调用的函数,有时会导致错误。 动态联编则在程序的运行过程中,根据指针与引用实际指向的目标调用对应的函数,就是在程序运行时才决定如何动作。 虚函数是允许函数调用与函数体之间的联系在运行时才建立,是实现动态联编的基础。
2、虚函数的定义与使用
虚函数的定义形式:
virtual 函数返回类型 函数(形参表) { }
- 虚函数不能是静态成员函数,也不能是友元函数,因为二者不属于某个对象;
- 内联函数是不能在运行中动态确定位置的;
- 只有类的成员函数才能说明为虚函数,仅适用于具有继承关系的类对象;
- 构造函数不能是虚函数,但是析构函数可以是虚函数,而且通常为虚函数。
要实现动态联编需要满足:
- 应满足类型兼容规则;
- 在基类中定义虚函数,并且在派生类中要重新定义虚函数;
- 要由成员函数或者通过指针、引用访问虚函数;
在基类中尽可能地将成员函数设置为虚函数,除了增加一些资源开销,没有其他坏处。 例子:
#include <iostream>
using namespace std;
class BaseCalculator {
public:
int m_A;
int m_B;
virtual int getResult();
};
class AddCalculator : public BaseCalculator {
virtual int getResult()
{
return m_A+m_B;
}
};
class SubCalculator : public BaseCalculator {
virtual int getResult()
{
return m_A-m_B;
}
};
int main() {
BaseCalculator* cal = new AddCalculator;
cal->m_A = 10;
cal->m_B = 20;
cout << cal->getResult() << endl;
delete cal;
cal = new SubCalculator;
cal->m_A = 20;
cal->m_B = 10;
cout << cal->getResult() << endl;
delete cal;
return 0;
}
#include<iostream>
using namespace std;
class Point
{
private:
int x,y;
public:
Point(int x=0,int y=0)
{
this->x=x,this->y=y;
}
virtual double area()
{
return 0.0;
}
};
const double PI=3.14;
class Circle: public Point
{
private:
double r;
public:
Circle(int x,int y,double r):Point(x,y)
{
this->r=r;
}
double area()
{
return PI*r*r;
}
};
int main()
{
Point p1(10,10);
cout<<"p1.area="<<p1.area()<<endl;
Circle c1(10,10,20);
cout<<"c1.area="<<c1.area()<<endl;
Point *p;
p=&c1;
cout<<"p->area="<<p->area()<<endl;
Point & p2=c1;
cout<<"p2.area="<<p2.area()<<endl;
return 0;
}
3、虚析构函数
虚析构函数的定义形式
virtual ~ 类名();
当基类的析构函数被声明为虚函数,则派生类的析构函数无论是否使virtual声明都自动成为虚函数。 析构函数被声明为虚函数后,可以确保使用基类类型的指针能够自动调用适当的析构函数对不同对象进行清理。 如果使用基类指针指向由new运算建立的对象,而delete又作用于指向派生类对象的基类指针,就要将基类的析构函数声明为虚析构函数。
#include<iostream>
using namespace std;
class A
{
public:
A()
{
cout<<"A::A()"<<endl;
}
virtual ~A()
{
cout<<"~A()"<<endl;
}
};
class B:public A{
private:
int *ip;
public:
B(int size=0)
{
ip=new int[size];
cout<<"B::B()"<<endl;
}
~B()
{
cout<<"~B()"<<endl;
delete []ip;
}
};
int main()
{
A *b=new B(10);
delete b;
return 0;
}
A::A() B::B() ~B() ~A()
如果基类的析构函数不是虚函数,结果为
A::A() B::B() ~A() 即派生类对象中动态分配的内存空间没有被释放,将造成内存泄露
四、抽象类
是一种特殊的类,可以通过它多态地使用其中的成员函数,抽象类自身无法实例化。 带有纯虚函数的类称为抽象类,一个抽象类至少具有一个纯虚函数。
1、纯虚函数
是一个在基类中说明的虚函数,但在基类中没有定义具体实现,要求各派类根据实际需要定义函数实现。其作用是为派生类提供一致的接口。 纯虚函数的定义:
virtual 函数类型 函数名(参数表)=0; 与虚函数的区别是:=0
2、抽象类与具体类
抽象类的特点:
- 只能作为其他类的基类使用,不能定义对象,纯虚函数的实现由派生类给出;
- 派生类也可以不给出纯虚函数的定义,继续作为抽象类,如果派生类给出实现,就是一个具体类,可以定义对象。
- 抽象类不能用作参数类型,函数返回值,强制类型转换;
- 可以定义·一个·抽象类的指针和引用,通过抽象类的指针和引用可以指向并访问各派生类成员,这种访问具有多态特征。
#include<iostream>
using namespace std;
class Shape
{
public:
virtual double area()const=0;
virtual void show()=0;
};
class Point:public Shape
{
protected:
int x,y;
public:
Point(int x=0,int y=0)
{
this->x=x,this->y=y;
}
void show()
{
cout<<"("<<x<<","<<y<<")"<<endl;
}
double area()const
{
return 0;
}
};
const double PI=3.14;
class Circle:public Point
{
protected:
double r;
public:
Circle(int x,int y,double r):Point(x,y)
{
this->r=r;
}
double area()const{
return PI*r*r;
}
void show()
{
cout<<"centre:",Point::show();
cout<<"radius:"<<r<<endl;
}
};
int main()
{
Circle c1(1,2,3);
Shape *p;
p=&c1;
p->show();
cout<<"area:"<<p->area();
return 0;
}
3、对象指针数组
对象指针数组是一个指针数组,这些指针指向对象。类的兼容性使基类指针可以指向其各类派生类对象,这意味着一个基类类型的指针数组的各个元素可以指向不同的派生类对象,这就是异类对象存储机制。 对象指针数组的使用
设计一个Person类,派生出Teacher、Student类,用一个Person类的指针数组指向Teacher和Student类的对象。
#include<iostream>
using namespace std;
#include<cstring>
class Person
{
protected:
char name[20];
public:
Person(char *iname)
{
strcpy(name,iname);
}
virtual void who()=0;
virtual ~Person()
{
cout<<"~person"<<endl;
}
};
class Student:public Person
{
private:
char major[20];
public:
Student(char * iname,char *imajor):Person(iname)
{
strcpy(major,imajor);
}
void who()
{
cout<<name<<","<<major<<endl;
}
~Student()
{
cout<<"~student"<<endl;
}
};
class Teacher:public Person
{
private:
char teah[20];
public:
Teacher(char *iname,char *iteach):Person(iname)
{
strcpy(teah,iteach);
}
void who()
{
cout<<name<<","<<teah<<endl;
}
~Teacher()
{
cout<<"~teacher"<<endl;
}
};
int main()
{
Person *personArr[5];
personArr[0]=new Student("zhangsan","math");
personArr[1]=new Teacher("lisi","computer");
int len=sizeof(personArr)/sizeof(personArr[0]);
for(int i=0;i<len;i++)
{
personArr[i]->who();
delete personArr[i];
}
}
zhangsan,math ~student ~person lisi,computer ~teacher ~person
|