多态性概念
多态的类型
面向对象的多态性可以分为4类:
重载多态、强制多态、包含多态和参数多态
多态的实现
编译时多态和运行时多态
静态绑定(编译时多态):绑定工作在编译链接阶段完成的情况为静态绑定
动态绑定(运行时多态):绑定工作在程序运行阶段完成的情况为多动态绑定
编译时多态:重载多态、强制多态和参数多态
运行时多态:包含多态
运算符重载
运算符重载的规则
①C++中的运算符除了少数几个之外,全部可以重载,而且只能重载C++中已经有的运算符。
②重载之后运算符的优先级和结合性都不会改变
③运算符重载是针对新类型数据的实际需要,对原有运算符进行适当的改造。一般来讲,重载的功能应当与原有功能相似,不能改变原有运算符的操作对象个数,同时至少要有一个操作对象是自定义类型。
C++标准规定,有些操作符是不能重载的,它们是类属关系运算符“.”、成员指针运算符“.*”、作用域分辨符“::”、三目运算符“? :”和sizeof运算符。
“[]”、“->”、“()”、“=”只能重载为成员函数。
“<<”、“>>”只能重载为非成员函数。
运算符的重载形式
运算符重载形式:
返回类型 operator运算符 (形参表)
{
函数体;
}
运算符重载实质上就是函数重载,重载为成员函数,它就可以自由地访问本类的成员。除了在函数声明及实现的时候使用了键字operator之外,运算符重载成员函数与类的普通成员函数没有什么区别。
重载类型转换运算符
由于返回类型同转换后的类型一致,所以重载类型转换运算符没有返回类型
operator T() //注意,返回类型为T(即”()”前面的T)
{
returnT; //T为类型名,return处为T类型的数据
}
如:将Complex对象转换为double类型的数据
operator double() const //const保证常量也可以使用该函数
{
return(real*10+imag) //10.0;//返回double类型的数据
}
运算符重载的注意事项
单目运算符(++、--)作为后置运算符重载时,它比前置运算符多一个int型参数作为区分。
当运算符重载为类的成员函数时,函数的参数个数比原来的操作数个数要少一个(后置“++”、“--”除外);当重载为非成员函数时,参数个数与原操作个数相同。将运算符重载为非成员函数,至少要有一个具有自定义类型的参数。
不要机械的将重载运算符的非成员函数声明为类的友元函数,仅在需要访问类的私有成员或保护成员时再这样做。如果不将其声明为友元,该函数仅依赖于类的接口,只要类的接口不变化,该函数的实现就无须变化;如果将其声明为友元函数,该函数会依赖于类的实现,即使类的接口不变,只要类的私有数据成员的设置发生了变化,该函数的实现就需要变化。
提示:运算符的两种重载形式各有千秋。成员函数的重载方式更加方便,但有时出于以下原因,需要使用非成员函数的重载方式。
①要重载的操作符的第一个操作数不是可以更改的类型,例如流插入运算符“<<”的第一个操作数的类型为ostream,是标准库的类型,无法向其中添加成员函数。
②以非成员函数的形式重载,支持更灵活的类型转换。
例如:当左操作数不是自定义类型时,可以调用类型转换构造函数将其转换为自定义类型。
而以成员函数重载时,左操作数必须具有自定义类型,因为调用成员函数的目的对象不会被隐含转换,只有函数参数才可以隐含转换。
个人总结:
双目运算符一般重载为非成员函数,当左右操作数只有一个是自定义的类型时,另一操作数会隐含转换为自定义类型参加运算。习惯上也将双目运算符重载为const,那样常对象也可以作为运算符的左操作数。
complex c1(2, 3);
const Complex c2(4, 5);
Complex c3 = c2 + c1;//如果重载的“+”是非const,编译不通过。
后置”++”、”--”运算符,显示调用时,一定要传参,否则编译出错。
如:
T.operator ++(0); //显示调用,T为对象名
重载“=”运算符时,要考虑自己给自己赋值的情况。
如:
T = T; //T为对象
虚函数
虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派生之后,在类族中就可以实现运行过程中的多态。虚函数声明只能出现在类定义中的函数原型声明中,而不能在成员函数实现的时候。
运行过程中的多态需要满足三个条件:
①赋值兼容原则
②声明虚函数
③要由成员函数来调用或者是通过指针、引用来访问虚函数。
如果是通过对象名来访问虚函数,则绑定在编译过程中就可以进行(静态绑定),而无须在运行过程中进行。
习惯:虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的,所以虚函数一般不能以内联函数处理。但将虚函数声明为内联函数也是不会引起错误的。
当派生类没有显示声明为虚函数时,这时系统会遵循以下规则来判断派生类的一个函数成员是不是虚函数:
①该函数是否与基类的虚函数有相同的名称
②该函数是否与基类的虚函数有相同的参数表
③该函数是否与基类的虚函数有相同的返回值类型或者满足类型兼容规则的指针、引用的返回值。
满足上述3个条件,派生类的虚函数会覆盖基类中的虚函数。不仅如此,派生类中的虚函数还会隐藏基类中同名函数的所有其他重载形式。
当基类的构造函数调用虚函数时,不会调用派生类的虚函数。因为创建对象时,先创建基类再创建派生类,在调用基类的构造函数时,派生类的对象还没有创建,就只能调用本类中的虚函数。同理,当基类的析构函数调用虚函数时,不会调用派生类的虚函数。因为对象析构时,先析构派生类再析构基类,在调用基类的析构函数时,派生类的对象已经被析构,就只能调用本类中的虚函数。
注意:在重写继承来的虚函数时,如果函数有默认形参值,不要重新定义不同的值(当然在派生类中也可以给虚函数指定默认参数值,但只有在由派生类成员函数来调用时,才使用该默认值)。原因是:虽然虚函数是动态绑定的,但默认形参值是静态绑定的。也就是说,通过一个指向派生类对象的基类指针或引用,可以访问到派生类的虚函数,但默认形参值却只能来自基类的定义。
只有通过基类的指针或引用调用虚函数时,才会发生动态绑定。
用派生类的对象初始化基类的对象,将调用基类的复制构造函数对基类对象进行初始化(初始化时,只对基类的数据成员进行初始化),这种行为称为对象切片。当通过基类的对象调用虚函数时,调用的将是基类的函数。
虚析构函数
在C++中,不能声明虚构造函数,但是可以声明虚析构函数。如果一个类的析构函数是虚函数,那么由它派生而来的所有子类的析构函数也是虚函数。如果有可能通过基类指针调用对象的析构函数(通过delete),就需要让基类的析构函数成为虚函数,否则会产生不确定的后果。
如:
基类Base,派生类Base1
Base1 b;
Base *p = new Base1;
delete p;
当基类的析构函数不是虚函数时,delete p将只调用基类的析构函数(如果派生类的析构函数中有内存的释放,将导致内存泄露);当基类的析构函数是虚函数时,delete p将先调用派生类的析构函数,再调用基类的析构函数。
对于对象b,无论基类的析构函数是否为虚函数,当对象b消亡时,先调用派生类的析构函数,再调用基类的析构函数。
普通函数(非成员函数)、友元函数、构造函数和静态成员函数不能声明为虚函数。内联成员函数、赋值操作符重载函数可以声明为虚函数,但是没有意义。
虚表
只要一个类中有一个虚函数,那么在该类中将有一个指针,用来存放该类虚表的地址。而虚表中存放的是该类中所有虚函数(包括从基类继承而来的虚函数)的地址。如果派生类重写了基类的虚函数,将在派生类的虚表中覆盖基类的相同函数,而基类的其他虚函数在虚表中的位置不变。如果派生类新定义了虚函数,新增虚函数地址将置于虚表的后面。
纯虚函数和抽象类
纯虚函数
纯虚函数的声明格式:
virtual 函数类型 函数名(参数表) [const]= 0;
声明为纯虚函数之后,基类中就可以不再给出函数的实现部分。
细节:基类中仍然允许对纯虚函数给出实现,但即使给出实现,也必须由派生类来覆盖,否则无法实例化。在基类和派生类中对基类纯虚函数定义的函数体的调用,必须通过“基类名::函数名(参数表)”的形式。如果将析构函数声明为纯虚函数,必须给出它的实现,因为派生类的析构函数体执行完毕后需要调用基类的纯虚函数。
注意:纯虚函数不同于函数体为空的虚函数。
程序实例:
class MyClass
{
public:
MyClass(){ cout << "MyClass()" << endl; }
//virtual ~MyClass() = 0;//error 纯析构函数必须实现
virtual ~MyClass() = 0{ cout <<"~MyClass()" << endl; }
void f1()
{
f2();//只调用派生类的f2函数
MyClass::f2();//只调用基类的f2函数
}
virtual void f2() = 0//纯虚函数
{
cout << "virtual void f2() = 0" << endl;
}
};
class Derived:public MyClass
{
public:
Derived(){ cout << "Derived()" << endl; }
virtual ~Derived(){cout << "~Derived()" << endl;}
void f2()//必须对基类的纯虚函数进行覆盖,否则不能实例化对象
{
//MyClass::f2();//调用父类的f2函数
cout << "void f2()" << endl;
}
};
抽象类
带有纯虚函数的类是抽象类。抽象类派生出新的类之后,如果派生类给出所有纯虚函数的函数实现,这个派生类就可以定义自己的对象,因而不再是抽象类;反之,如果派生类没有给出全部纯虚函数的函数实现,这时的派生类仍然是一个抽象类。
抽象类不能实例化,即不能定义一个抽象类的对象,但是可以定义一个抽象类的指针和引用。
typeid
typeid是C++的一个关键字,用它可以获得一个类的相关信息。
它有两种语法形式:
typeid(表达式);
或
typeid(类型说明符);
通过typeid得到的是一个type_info类型的常引用。type_info是C++标准库中的一个类,专用于在运行时表示类型信息,它定义在typeinfo头文件中。type_info类有一个名为name的函数,用来获得类型的名称。
虽然typeid可以作用于任何类型的表达式,但只有它作用于多态类型的表达式时,进行的才是运行时类型识别,否则只是简单的静态类型信息的获取。
class Base { ... };//定义了虚析构函数
class Derived : public Base { ... };//定义了虚析构函数
void f()
{
Derived* pd =new Derived;
Base* pb = pd;
...
const type_info&t = typeid(pb);
consttype_info& t1 = typeid(*pb);
cout <<t.name() << endl; //class Base *
cout <<t1.name() << endl; //class Derived
...
}
Pb是指针,指针本身不具有多态类型,是什么类型的指针就输出什么类型的信息。而*pb得到的是pb指针所指向对象的具体类型(具有多态类型),输出的是所指向对象的类型信息,即class Derived
注意:指针本身不具有多态类型,指针所指向的对象具有多态类型。
类型转换的四种形式
static_cast
static_cast < type-id > ( exdivssion ) 该运算符把exdivssion转换为type-id类型,但没有运行时类型检查来保证转换的安全性。它主要有如下几种用法:
①用于类层次结构中基类和子类之间指针或引用的转换。
进行上行转换(把子类的指针或引用转换成基类表示)是安全的;
进行下行转换(把基类指针或引用转换成子类表示)时,由于没有动态类型检查,所以是不安全的。将基类的指针转换为派生类的指针,可以通过转换后的指针调用派生类的成员函数,但是无法使用派生类的非静态数据成员(得到的是垃圾值),即使在成员函数中也不行。
②用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性也要开发人员来保证。
③把空指针转换成目标类型的空指针。
④把任何类型的表达式转换成void类型。
注意:static_cast不能转换掉exdivssion的const、volitale、或者__unaligned属性。
reinterpret_cast
只要是个地址,reinterpret_cast都能转,不管原来数据是啥,甚至不是一个数据的开始。就像C语言中的指针强制转换,其实只是把地址赋给了新的指针,其它的不做改变,只在新的指针使用的时候,进行不一样的解释。所以,请正确的使用它,用你希望的方式。
程序实例:
#include <iostream>
using namespace std;
void main()
{
int i = 875770417;
cout<<i<<" ";
char* p = reinterpret_cast<char*>(&i);
for(int j=0; j<4; j++)
{
cout<<p[j];
}
cout<<endl;
float f = 0.00000016688933;
cout <<f<<" ";
p = reinterpret_cast<char*>(&f);
for(j=0; j<4; j++)
{
cout<<p[j];
}
cout<<endl;
}
我们看到不管是int型的 i还是float型的 f,经过reinterpret_cast<char*>(&addr)的转换后,输出都是"1234"
这是因为整型的875770417和单精度浮点型的0.00000016688933,在内存中的数据表示从低位到高位依次是0x31 0x32 0x33 0x34(ASCII码分别对应字符”1” ”2” ”3” ”4”)。
数据的表示是相同,你把它当int型数据来看,他就是875770417;你把它当float型数据来看,它就是 0.00000016688933,关键就是看编译器怎么来解释数据,也就是语义表达的问题。
所以reinterpret_cast<char*>的转换就是不理会数据本来的语义,而重新赋予它char*的语义。有了这样的认识有以后,再看看下面的代码就好理解了。
#include <iostream>
using namespace std;
void main()
{
char* p = "1234";
int* pi = reinterpret_cast<int*>(p);
cout<<*pi<<endl;
}
输出的是875770417(0x34333231,注意高低位),就是上面的逆运算,好理解吧。
再看一个诡异的。
#include <iostream>
using namespace std;
class CData
{
public:
CData(int a, int b)
{
val1 = a;
val2 = b;
}
private:
int val1;
int val2;
};
void main()
{
CData data(0x32313536, 0x37383433);
int* pi = reinterpret_cast<int*>((char*)&data + 2);
cout<<*pi<<endl;
}
pi指向哪里?指向CData类型数据data的内存地址偏移2个字节。也就是说指向CData::val1和CData::val2的中间。这不是乱指嘛!没错,就是乱指,我们仍然看到输出数据显示875770417。
dynamic_cast
语法:dynamic_cast < type-id > ( expression )
该运算符把expression转换成type-id类型的对象。Type-id必须是类的指针、类的引用或者void *。
dynamic_cast的转换是在运行时进行的,它的一个好处是会在运行是做类型检查,如果对象的类型不是期望的类型,它会在指针转换的时候返回NULL,并在引用转换的时候抛出一个std::bad_cast异常。
dynamic_cast一般只在继承类对象的指针之间或引用之间进行类型转换。
dynamic_cast运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中。如果没有虚函数表则会报错。
dynamic_cast在运行时遍历继承树,只要是在继承树上的类都可以相互转换。
程序实例:
#include <iostream>
using namespace std;
struct A
{
virtual void f() { }
};
struct B : public A { };
struct C { };
void f ()
{
Aa;
Bb;
A* ap = &b;
B* b1 = dynamic_cast<B*>(&a); // NULL, because 'a' is not a 'B'
B* b2 = dynamic_cast<B*>(ap); // 'b'
C* c = dynamic_cast<C*>(ap); // NULL.
A& ar = dynamic_cast<A&>(*ap); // Ok.
B& br = dynamic_cast<B&>(*ap); // Ok.
//C& cr = dynamic_cast<C&>(*ap); // std::bad_cast
}
int main()
{
f();
return0;
}
使用'dynamic_cast'需要打开run-time type information 支持,否则会报以上警告,这里以VC 6.0为例:
打开方式:菜单project -> setting ->c /c++ -> c++ language -> enable run-time type information (RTTI)(勾选此项)