更多内容可以查看系列文章C++语言入门全教程(持续更新)
目录
前言
一、继承的概念
1.楔子
2.派生类的定义
3.继承和派生的意义
4.案例1:派生类的定义
二、继承方式
1.公有继承(public)
2.保护继承(protected)
3.私有继承(private)
4.protected成员的特点与作用
5.公有继承举例
6.私有继承举例
三、继承中的构造与析构函数
1.例①
2.例②
3.例③
四、其他
1.组合与继承
2.基类与派生类之间的转化
3.派生类中重定义基类的函数
五、继承与派生案例
总结
前言
界 - 门 - 纲 - 目 - 科 - 属 - 种 ~
有一个笑话:医生让病人多吃水果,看着家人送来的梨、苹果和橘子,他不解的问,医生说的是“水果”啊,怎么是“梨”“苹果”“橘子”呢?
这就是哲学上的共性与个性,矛盾的普遍性与特殊性原理——当然,此篇博客并不是哲学的复习提纲,而本篇博客的内容,继承与派生,确乎与之密不可分。
一、继承的概念
1.楔子
虽然前言有一定程度的引入,然而详细地说明继承的概念以及使用继承的原因是非常必要的。
正如之前所说的,类似于我们所说的界门纲目科属种,对一种生物,从不同的层次我们可以进行大量的分类,两种动物,虽然不同,但从种往上,至少从一个层级开始(界、门、纲、目、科、属)都是一致的——至少都是动物界 。
换句话说,不同肯定存在但是,完全不同的两件事物也是不存在的——又哲学了。
继承的使用,可以形象地说,是抓住了某些对象,即类,特性中共同的部分。这些子类从母类中诞生而来,保留了母类中共同的部分,同时也灵活地接受各自的不同。——这些共同特性因为一些原因需要修改时,只需要修改母类中的定义即可,而不需要对所有类的共同部分进行冗余的修改。
进一步举例说明:
现实世界中,实体之间普遍存在着”是(is_a)”的关系
- 圆,三角形,长方形都是平面图形
对具有共性特征的类,如何避免对共性部分的重复定义
- 圆,三角形,长方形都包含坐标位置数据,以及求面积函数
如何统一管理所有具有共性特征的对象
- 如何统一管理所有的平面图形
这也就引申出继承和派生了。
2.派生类的定义
class 派生类名: 继承方式 基类名1, …
{
成员定义;
};
- 基类与派生类的关系是相对的
- 多继承:一个派生类有多个直接基类
- 单继承:一个派生类只有一个直接基类
就楔子部分的交通工具一例来说,具体程序可以这样设计:
Car 类只需要定义 Car::braking() 方法
Ship 类只需要定义 Ship::porting() 方法
Airplane 类只需要定义 Airplane::landing() 方法
3.继承和派生的意义
- 通过继承,可以从已有的类派生出新类,新类在已有类的基础上新增自己的特性
- 被继承的已有类称为基类(父类),派生出的新类称为派生类(子类)
- 继承可以减少代码的冗余性,实现代码的重用,并且通过作少量的修改,满足不断变化的具体应用要求,提高程序设计的灵活性
4.案例1:派生类的定义
class Student
{
protected:
int number;
char name[20];
float score;
public:
Student(int num=0, char *p="no name")
{
number = num;
strcpy(name, p);
}
void SetScore(float s=0.0)
{ score=s; }
};
class GraduateStudent : public Student
{
protected:
char advisor[20];
public:
void SetAdvisor(char *p) {strcpy(advisor,p);}
char* GetAdvisor() {return advisor;}
};
void main()
{
Student s1(1001, "Lucy");
GraduateStudent s2;
s2.SetScore(85.0);
s2.SetAdvisor("Mary");
}
二、继承方式
继承方式指定了派生类成员以及类外对象对继承来的成员的访问权限,有以下三种继承方式
- 公有继承(public)
- 保护继承(protected)
- 私有继承(private)
1.公有继承(public)
- 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能访问基类的private成员
- 将基类的protected区成员继承到派生类的protected区,基类的public区的成员继承到派生类的public区
- 外部函数中,派生类的对象只能访问基类的public成员
2.保护继承(protected)
- 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能访问基类的private成员
- 将基类的protected区和public区的所有成员都继承到派生类的protected区
- 外部函数中,派生类的对象不能访问基类中的任何成员
3.私有继承(private)
- 派生类中的成员函数可以直接访问基类中的public和protected成员,但不能访问基类的private成员
- 将基类的protected区和public区的所有成员都继承到派生类的private区
- 外部函数中,派生类的对象不能访问基类中的任何成员
4.protected成员的特点与作用
class A
{
protected:
int x;
};
int main()
{
A a;
a.x=5; //错误
}
class A
{
protected:
int x;
};
class B : public A
{
public:
void Function();
};
void B :: Function()
{
x=5; //正确
}
派生类可访问基类protected成员,外部函数不可访问基类内部protected成员
5.公有继承举例
class Location
{
public:
void InitL(int xx,int yy) {X=xx; Y=yy;}
void Move(int xOff,int yOff) {X+=xOff; Y+=yOff;}
int GetX() {return X;}
int GetY() {return Y;}
private:
int X,Y;
};
class Rectangle : public Location //派生类
{
public:
void InitR(int x,int y,int w,int h);
int GetH() {return H;}
int GetW() {return W;}
private:
int W,H;
};
inline void Rectangle::InitR(int x,int y,int w,int h)
{
InitL(x,y); //派生类直接访问基类的公有成员
W=w;
H=h;
}
int main()
{
Rectangle rect;
rect.InitR(2,3,20,10);
rect.Move(3,2); //对象访问基类的公有成员
cout<<rect.GetX()<<',' //对象访问基类的公有成员
<<rect.GetY()<<','
<<rect.GetH()<<','
<<rect.GetW()<<endl;
return 0;
}
6.私有继承举例
class Rectangle : private Location
{
public:
void InitR(int x,int y,int w,int h);
void Move(int xOff,int yOff);
int GetX();//因为基类对应函数继承到private类,因此需要重定义,也允许重定义
int GetY();
int GetH() {return H;}
int GetW() {return W;}
private:
int W,H;
};
inline void Rectangle::InitR(int x,int y,int w,int h)
{
InitL(x,y); //派生类直接访问原公有成员
W=w;
H=h;
}
void Rectangle::Move(int xOff,int yOff) //!!!
{ Location::Move(xOff,yOff); }
int Rectangle::GetX() //!!!
{ return Location::GetX(); }
int Rectangle::GetY() //!!!
{ return Location::GetY(); }
int main()
{
Rectangle rect;
rect.InitR(2,3,20,10);
rect.Move(3,2); //对象访问派生类的函数
cout<<rect.GetX()<<‘,‘ //对象访问派生类的函数
<<rect.GetY()<<','
<<rect.GetH()<<','
<<rect.GetW()<<endl;
return 0;
}
三、继承中的构造与析构函数
- 派生类不继承基类的构造函数和析构函数,但是能调用基类的构造函数和析构函数
- 派生类的构造函数总是先调用基类的构造函数来初始化派生类中的基类成员,再进行派生类中成员的初始化
- 派生类构造函数的定义中,要提供基类构造函数所需要的参数
- 如果派生类没有用户自定义的构造函数,执行其默认构造函数时,首先调用基类的构造函数
- 析构函数的调用顺序和构造函数的调用顺序相反
- 子类的构造函数要有一个默认的父类的构造函数对应
下面用三个例子说明。
1.例①
//----------------------------------Base类声明
class Base
{
public:
Base(int i);
~Base();
void print();
private:
int a;
};
//----------------------------------Derive类声明
class Derive : public Base
{
public:
Derive(int i, int j);
~Derive();
void print();
private:
int b;
};
//----------------------------------Base成员函数定义
Base::Base(int i)
{
a=i;
cout<< "Base constructor" <<endl;
}
Base::~Base()
{
cout<< "Base destructor" <<endl;
}
void Base::print()
{
cout<< a << endl;
}
//----------------------------------Derive成员函数定义
Derive::Derive(int i, int j) : Base(i)
{
b=j;
cout<< "Derive constructor" <<endl;
}
Derive::~Derive()
{
cout<< "Derive destructor" <<endl;
}
void Derive::print()
{
Base::print();
cout<< b << endl;
}
//----------------------------------主函数
void main()
{
Derive d(2,5);
d.print();
}
输出结果:
Base constructor
Derive constructor
2
5
Derive destructor
Base destructor
2.例②
在例①中进行相应的修改
class Base
{
public:
Base();
Base(int i);
......
};
class Derive : public Base
{
public:
Derive(int i, int j){}
......
};
此时可选择不带参数的构造函数
3.例③
class Base
{
public:
Base(){ cout<< "Base constructor" <<endl;}
~Base(){ cout<< "Base destructor" <<endl;}
void print();
};
class Derive : public Base
{
public:
void set(int i) { b=i; }
void print(){ cout<< b << endl; }
private:
int b;
};
void main()
{
Derive d;
d.set(2);
d.print();
}
结果输出:
Base constructor
2
Base destructor
说明调用了基类的构造函数和析构函数。
四、其他
1.组合与继承
- 组合(has_a):一个类的数据成员是另一个类的对象,其实就是复合(详见类的复合)
- 继承(is_a):一个类是另外一个类的特殊形式
- 继承和组合都利用了已经定义的类,但是类之间关系上有差别
2.基类与派生类之间的转化
一个公有继承的派生类对象可以隐式转化为一个基类对象
- 用派生类的对象给基类对象赋值
- 用派生类的对象来初始化基类的引用
- 把派生类对象的地址赋值给指向基类的指针
- 把指向派生类对象的指针赋值给指向基类对象的指针
注意访问范围!
注意基类对象不能代替派生类对象!
class A
{ public:
void print1(){}
};
class B: public A
{ public:
void print2(){}
};
void main()
{
B bp1, *pbp;
A ap1 = bp1; //OK
A &ap2 = bp1; //OK
A *ptr = &bp1; //OK
ptr = pbp; //OK
ptr->print1(); //OK
ptr->print2(); //Error
}
再看下面这个例子:
class Base
{
public:
void view() {cout << "In Base!"<<endl;}
};
class Derive:public Base
{
public:
void view() {cout << "In Derive!"<<endl;}
};
void func(Base& b)
{
b.view();
}
void main()
{
Base b1;
func(b1);
Derive d1;
func(d1);
}
上面这个例子中,Derive类的对象d1已经隐式的转换为Base对象,因此输出:
In Base!
In Base!
3.派生类中重定义基类的函数
- 派生类自动继承基类的数据成员和成员函数
- 若基类中的某成员函数无法满足派生类需求,派生类中可以重定义该函数
- 重定义基类的成员函数需要使用相同名字和参数
- 重定义之后,通过派生类访问到的函数是重定义之后的函数
- 如果需要访问基类中被重定义的函数,可以使用类名加作用域运算符 ::
在上面私有继承举例中,因为私有继承下来的成员函数为private区,外部函数类对象无法通过原本的成员函数访问,此时就需要进行重定义,对外界提供一个接口。
五、继承与派生案例
问题描述1. 编写一个音像资料类( Media ),该类包含作品名字( name )和制作公司( company )两个成员变量,以及一个显示音像资料信息的成员函数( showInfo )2. 编写音频媒体类( AudioMedia ),继承音像资料类,并包含新的数据成员歌手( singer )3. 编写视频媒体类( VideoMedia ),继承音像资料类,并包含新的数据成员导演( director )和演员( actor )4. 在 AudioMedia 和 VideoMedia 中重定义 showInfo 函数,要求不但要输出 Media 类的成员信息,还要输出其特殊数据成员的信息5. 编写 main 函数,测试上述三个类的正确性
下面提供完成到第二步,AudioMedia继承音像资料类的程序,其他部分还请读者自行补全 \吃瓜
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
#include<algorithm>
#include<iomanip>
#include<limits.h>
#include<stack>
#include<cstdlib>
#include<stdlib.h>
#include<queue>
#include<map>
#include<vector>
#define INF 0x3f3f3f3f
#define IOS ios::sync_with_stdio(0);cin.tie(0);cout.tie(0)
//头文件、宏定义请忽略(OIer的不良嗜好)
using namespace std;
class Media{
protected:
char name[30];
char company[50];
public:
Media(char* n,char* c)
{
strcpy(name,n);
strcpy(company,c);
}
void showInfo()
{
cout<<"_________Info Of Media_________\n";
cout<<name<<endl;
cout<<company<<endl;
}
};
class AudioMedia:public Media
{
private:
char singer[30];
public:
AudioMedia(char* n,char* company,char* s):Media(n,company)
{
strcpy(singer,s);
}
void showInfo()
{
Media::showInfo();
cout<<singer<<endl;
}
};
int main()
{
Media m("《1111》","XX传媒");
m.showInfo();
AudioMedia n("《2222》","XX传播","XXX");
n.showInfo();
return 0;
}
总结
继承与派生,一方面使得代码更简洁、去冗余,另一方面也使得“封闭内部信息”这一始终关注的要点在各个场合都有灵活的使用方法。