继承
单继承
class Derived:<public/protected/private> Base{
…// 派生类成员数据
…//派生类成员函数
}
基类 | 共有成员 | 私有成员 | 保护成员 |
---|---|---|---|
公有派生类 | 公共成员 | 不可访问成员 | 保护成员 |
保护派生类 | 保护成员 | 不可访问成员 | 保护成员 |
私有派生类 | 私有成员 | 不可访问成员 | 保护成员 |
派生类与基类同名成员访问
派生类成员会默认覆盖基类成员,即访问基类和派生类的同名成员会默认访问派生类的成员
如果想要访问基类成员,可以通过显式规则访问:
基类名::成员
赋值兼容原则
在公有派生情况下,派生类可以作为基类对象来使用:
-
派生类对象可以直接赋值给基类对象
-
基类对象的引用可以直接引用一个派生类对象
-
基类对象指针可以直接指向一个派生类对象
即,所有的派生类都是基类,但是基类不一定是派生类
(当一个派生类对象赋值给基类对象时,赋值的只是基类所拥有派生类的一部分,称作派生类对象的“切片”)
单继承的构造与析构
派生类构造函数:
DerivedFunction(argList):BaseFunction(argList),Obj1(argList)…Objn(argList){
…//初始化自定义成员
}
例如:
class Circle{
public:
int x,y,r;
Circle(int x,int y,int r){
this->x=x;
this->y=y;
this->r=r;
}
~Circle()
}
class ColorCircle:public Circle{
char* color;
ColorCircle(int x,int y,int r,cahr*color):Circle(x,y,r){
this->color=color;
}
/*或者可以这么调用基类构造函数
ColorCircle(int x,int y,int r,char* color):x(x),y(y),r(r){
this->color=color;
}
}
构造函数与析构函数调用顺序:
-
构造函数调用顺序:
- 先调用基类构造函数
- 再调用类中对象成员的构造函数
- 最后调用派生类构造函数
-
析构函数调用顺序:
-
先调用派生类析构函数
-
在调用类中对象成员的析构函数
-
最后调用基类析构函数
-
多态
**动态绑定:**编译器可以自动根据情况确定使用基类函数还是派生类函数
编译时的多态
- 函数重载
- 运算符重载
运行时的多态
- 虚函数
二元运算符重载
基本数据类型运算符本身已经隐含重载,比如“+”可以用作整型、浮点型等不同数据类型的加法
如果直接把运算符用在类对象上,由于本身没有关于自定义类的重载的运算符,因此需要在代码中手动重载
class Complex{
double re, im;
public:
Complex(double r=0.0, double i=0.0): re(r), im(i){}
Complex add(Complex c){
Complex t;
t.re=re+c.re;t.im=im+c.im;
return t;
}
}
int main(){
Complex c1(1,2),c2(3,4);
Complex c3=c1.add(c2);
return 0;
}
//这是一般情况下两个类加法最直接想到的方法
//但是实际上c1+c2这种方法更符合我们的使用习惯,此时就需要进行运算符重载
class Complex{
double re, im;
public:
Complex(double r=0.0, double i=0.0): re(r), im(i){}
Complex operator+(Complex c){
//在重载某一个运算符时要在运算符前加上关键字:operator
//ReturnType operator(argList){}
complex t;
t.re=re+c.re;
t.im=im+c.im;
return t;
}
}
int main(){
Complex c1(1,2),c2(3,4);
Complex c3=c1+c2;
return 0;
}
//通过这种方法看起来好像实现了运算符重载,但事实上并没有,这里的 + 是Complex成员函数 operator+ 的隐式调用
//即,c3=c1+c2...(隐式调用) <==> c3=c1.operator+(c2)...(显式调用)
//如果 + 前面的变量不是Complex类型就仍然无法进行运算,仍旧报错
/*
例如:
c3 = c2+27; ···········正确
c3 = 27+c2; ···········错误
*/
用友元函数实现重载:这样当使用"+"时,由于编译器自带 operator+ 函数没有参数类型为我们自定义类类型的重载,便会来调用类中自定义 operator+ 函数
class Complex{
double re, im;
public:
Complex(double r=0.0, double i=0.0): re(r), im(i){}
friend Complex operator+(Complex c1,Complex c2){
//称 Complex operator+(Complex c1,Complex c2) 为运算符重载函数
complex t;
t.re=c1.re+c2.re;
t.im=c1.im+c2.im;
return t;
}
}
int main(){
Complex c1(1,2);
Complex c3=c1+27; ·······正确
Complex c3=27+c1; ·······正确
return 0;
}
操作符"<<"的重载
ostream& operator<<(Complex& a,Complex& b){
a<<b.re<<"+"<<b.im<<"i"<<endl;
return a;
}
一元运算符重载
class Counter{
int value;
public:
Counter(value=0){
this->value=value;
}
Counter++();
++Counter();
cout<<Counter; ········这三种重载又如何实现呢
}
可以参照 + 的重载办法,用 friend 重载
friend Counter operator++(Counter &obj){
//通过引用可以直接改变值,不然只是改变了复制进来的形参的值
obj.value+=1;
return obj;
}
实际上这种重载只是重载了 ++Counter
而Counter++的重载就需要增加占位参数
friend Counter operator++(Counter &obj,int){
Counter tmp=obj;
obj.value++;
return tmp;
}
// 这里添加临时变量是考虑了Counter++本身的特性:值自增,但是返回的是自增前的值
也可以直接定义为成员函数
Counter operator++(){
value++;
}
Counter operator++(int){
tmp=*this;
value++;
return tmp;
}
两种重载函数的比较:
- 多数情况下,运算符可以重载为类的成员函数,也可以重载为友元函数。但是两种方式也各有特点:
- 一般情况下,单目运算符重载为类的成员函数;双目运算符重载为类的友元函数;
- 有些运算符不能重载为类的友元函数:=,(),[],->;
- 类型转换函数只能定义为类的 成员函数;
- 若一个运算符需要修改对象的状态,则重载为成员函数比较好;
- 若运算符所需要的操作数(尤其是第一个操作数) 希望有隐式类型转换,则只能选择友元函数;
- 若运算符是成员函数,最左边的操作数必须是运算符类的类对象(或者类对象的引用)。如果左边操作数必须是一 个不同类的对象,或者是基本数据类型,则必须重载为友元函数;
- 当需要重载运算符的元素具有交换性(就如前面的c2+27 <==> 27+c2)时,重载为友元函数.
缺省的赋值运算符重载
如果没有为类重载赋值运算符,那么编译器会生成一个缺省的赋值运算符函数,其作用是通过为拷贝的方式将原对象复制到目的对象。
赋值运算符与拷贝构造函数异同 | ------------------------------------------ |
---|---|
相同点 | 不同点 |
将一个对象数据成员复制到另一个对象 | 拷贝构造函数是要初始化一个新对象,赋值运算符是要改变一个已经存在的对象 |
虚函数与多态性
重载成员函数的两种方式(静态多态):
-
同一个类内重载:以参数特征区分重载函数
-
派生类中重载成员函数:
调用时通过 “类名::函数名” 调用
**虚函数(动态多态):**函数调用与函数体的匹配在运行时确定
class Base{
public:
void print(){
cout<<"I'm base"<<endl;
}
}
class Derived:public Base{
public:
void print(){
cout<<"I'm derived"<<endl;
}
}
int main(){
Base base,*p;
Derived derived;
p=&base;
p->print(); //···········调用的是基类的print函数
p=&derived;
p->print(); //···········调用的是基类的print函数
return 0;
}
通过基类引用或指针访问的对象,派生类对象的属性是不可见的,访问的全部是基类的属性
可以将基类函数定义为虚函数,这样在利用基类引用或指针访问对象时,可以通过其引用或对象类型来自动确定访问基类或者派生类对象
class Base{
public:
virtual void print(){
cout<<"I'm base"<<endl;
}
}
class Derived:public Base{
public:
void print(){
cout<<"I'm derived"<<endl;
}
}
int main(){
Base base,*p;
Derived derived;
p=&base;
p->print(); //···········调用的是基类的print函数
p=&derived;
p->print(); //···········调用的是派生类的print函数
return 0;
}
-
在基类中用virtual关键字声明的成员函数即为虚函教。
-
虚函数可以在一个或多个派生类中被重新定义,但要求在重定义时虚函数的原型(包括返回值类型、函数名、参数列表)必须完全相同。
基类函数具有虚特性条件:
-
在基类中用virtual将函数说明为虚函数。
-
在公有派生类中原型一致地重 载该虚函数。
-
定义基类弓|用或指针,使其引用或指向派生类对象。当通过该引用或指针调用虚函数时,该函数将体现出虚特性来。
-
C++中,基类必须指出希望派生类重定义那些函数。定义为virtual的函数是基类期待派生类重新定义的,基类希望派生类继承的函数不能定义为虚函数。
注意事项:
-
在派生类中重载虚函数时必须与基类中的函数原型相同,否则该函数将丢失虚特性。
-
仅返回类型不同,其他相同。C+ +编译器认为这种情况是不允许的。
-
函数原型不同,仅函数名相同。C++编译器认为这是一般的函数重载,此时虚特性丢失。
纯虚函数与抽象类
基类表示抽象的概念,提供公共接口,只需要有说明而不需要实现,即纯虚函数,具体实现在基类中实现。
定义纯虚函数语法:
virtual type FunctionName(argList)=0
-
将一个函数说明为纯虚函数,就要求任何派生类都定义自己的实现。
-
拥有纯虚函数的类被称为抽象类。抽象类不能被实例化,只能作为基类被使用。
-
抽象类的派生类需要实现纯虚函数,否则该派生类也是一个抽象类。
-
当抽象类的所有函数成员都是纯虚函数时, 这个类被称为接口类。