面向对象编程的主要目的是之一是提供可重用的代码。开发新项目,尤其是当项目十分庞大时,重用经过测试的代码比重新编写代码要好得多。使用已有的代码可以节省时间,由于已有的代码已被使用和测试过,因此有助于避免在程序中引入错误。另外,必须考虑的细节越少,便越能专注于程序的整体策略。
c++类提供了更高层次的重用性。目前,很多厂商提供了类库,类库由类声明和实现构成。因为类组合了数据表示和类方法,因此提供了比函数库更加完整的程序包。通常,类库是以源代码的方式提供的,这意味这可以对其进行修改,以满足需求。然而,c++提供了比修改代码更好的方法来扩展和修改类。这种方法叫做类继承,它能够从已有的类派生出新的类,而派生类继承了原有类(基类)的特征,包括方法。通过继承派生出的类通常比设计新类要容易得多。
可以通过继承完成的一些工作
1、可以在已有类的基础上添加功能。
2、可以给类添加数据。
3、可以修改类方法的行为。
可以通过复制原始类代码,并对其进行修改来完成上述工作,但继承机制只需提供新特性,甚至不需要访问源代码就可以诞生出类。
1、类的继承
概念:新的类(子类)从已知的类(父类)中得到已有的特征的过程
从一个类派生出另一个类时,原始类称为基类(也叫父类),继承类称为派生类(子类)。
作用:继承可以减少重复的代码,比如弗雷已经提供的方法,子类可以直接使用,不必再去实现
继承的格式(单继承)
class 子类名:继承方式 父类(继承方式省略默认是私有继承)
{
子类成员
};
1.1派生一个类
派生类对象具有以下特征:
1、派生类对象存储了基类的数据成员(派生类继承了基类的实现)
2、派生类对象可以使用基的方法(派生类继承了基类的接口)
需要在继承特性中添加什么呢?
1、派生类需要自己的构造函数(构造函数必须给新成员和继承的成员提供数据)
2、派生类可以根据需要添加额外的数据成员和成员函数
1.2构造函数:访问权限的考虑
派生类不能直接访问基类的私有成员,而必须通过基类方法进行访问
创建派生类对象时,程序首先创建基类对象。从概念上说,这意味这基类对象应当在程序进入派生类构造函数之前被创建。c++使用成员初始化列表语法来完成这种工作。必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数。除非要使用默认构造函数,否则应显式调用正确的构造函数。
有关派生类构造函数的要求如下:
1、首先创建基类对象
2、派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数
3、派生类构造函数应初始化派生类新增的数据成员
释放对象的顺序与创建对象的顺序相反,即首先执行派生类的析构函数,然后自动调用基类的析构函数。
注意:创建派生类对象是,程序首先调用基类构造函数,然后再调用派生类构造函数。基类构造函数负责初始化继承的数据成员,派生类构造函数主要用于初始化新增的数据成员。派生类的构造函数总是调用一个基类构造函数。可以使用初始化器语法指明要使用的基类构造函数,否则将使用默认的基类构造函数。派生类对象过期式,程序将使用用派生类析构函数。然后再调用基类析构函数。
成员初始化列表
派生类构造函数可以使用初始化器列表机制值传递给基类构造函数
子类::子类(数据类型):基类(数据类型){ ... }
如果没有在成员初始化列表中提供基类构造函数,程序将使用默认的基类构造函数。成员初始化列表只能用于构造函数。
#include <iostream>
#include<cstring>
using namespace std;
//基类-->父类
class Base
{
public:
Base(){cout << "Base()" << endl;}
~Base(){cout << "~Base()" << endl;}
void setA(int n)
{
a = n;
}
int getA()
{
return a;
}
void show(int a,int b)
{
cout<<a<<" "<<b<<endl;
}
private:
int a;
};
//派生类-->子类
class Child:public Base //继承方式省略默认是私有继承
{
public:
Child(){cout << "Child()" << endl;}
~Child(){cout << "~Child()" << endl;}
// void show(int a,int b) //基类里面有这种方法,那么子类里面可以不用重复实现
// {
// cout<<a<<" "<<b<<endl;
// }
private:
int b;
};
int main()
{
//实例化一个子类的对象
Child c1;
//通过子类的对象来调用父类的方法
c1.setA(100);
cout<<c1.getA()<<endl;
c1.show(10,20);
return 0;
}
1.3继承的对象模型
问题提出:从父类继承过来的成员,哪些属于子类对象中?
利用Visual studio这个编程软件下自带的开发人员命令提示符来进行查看
使用步骤:
1、打开工具窗口
2、定位到当前cpp文件的盘符
3、c1 /d1 reportSingleClassLayout查看的类名 所属文件(TAB键补全)
结论:父类中私有成员也是被子类的继承下去了,只是由编译器给隐藏后访问不到
1.4继承同名成员处理方式
当子类和父类出现同名的成员,如何通过子类的对象,访问到子类或父类中同名的数据
1、访问子类同名成员 直接访问即可
2、访问父类同名成员 需要加作用域
#include <iostream>
using namespace std;
class Base {
public:
Base()
{
m_A = 100;
}
void func()
{
cout << "Base - func()调用" << endl;
}
void func(int a)
{
cout << "Base - func(int a)调用" << endl;
}
public:
int m_A;
};
class Son : public Base {
public:
Son()
{
m_A = 200;
}
//当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数
//如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域
void func()
{
cout << "Son - func()调用" << endl;
}
public:
int m_A;
};
void test01()
{
Son s;
cout << "Son下的m_A = " << s.m_A << endl;
cout << "Base下的m_A = " << s.Base::m_A << endl;
s.func();
s.Base::func();
s.Base::func(10);
}
int main() {
test01();
return 0;
}
总结:
1. 子类对象可以直接访问到子类中同名成员
2. 子类对象加作用域可以访问到父类同名成员
3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数
问题:继承中同名的静态成员在子类对象上如何进行访问?
静态成员和非静态成员出现同名,处理方式一致
- 访问子类同名成员 直接访问即可
- 访问父类同名成员 需要加作用域
同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)
2、类的多种继承方式
继承方式有三种:公有继承(public),私有继承(private),保护继承(protected)
父类成员权限/继承方式 | 公有继承(public) | 保护继承(protected) | 私有继承(private) |
public | 可用/public | 可用/protected | 可用/private |
protected | 可用/protected | 可以/protected | 可用/private |
private | 可以继承,但是不能直接访问,可以通过基类的公有方法和保护方法 | 可以继承,但是不能直接访问,可以通过基类的公有方法和保护方法 | 可以继承,但是不能直接访问,可以通过基类的公有方法和保护方法 |
上面表格权限是基类中的成员继承到子类后的成员权限
如果派生类在继承基类的时候选择公有继承(public)
那么基类的公有成员就是在派生类中也是充当公有成员,可以直接在派生类的内部和外部使用
那么基类的保护成员就是在派生类中也是充当保护成员,可以在派生类的内部使用,但是不能外部使用
那么基类的私有成员可以继承,但是不能直接访问,可以通过基类的公有方法和保护方法间接访问
如果派生类在继承基类的时候选择保护继承(protected)
那么基类的公有成员就是在派生类中是充当保护成员,可以在派生类的内部使用,但是不能外部使用
那么基类的保护成员就是在派生类中是充当保护成员,可以在派生类的内部使用,但是不能外部使用
那么基类的私有成员可以继承,但是不能直接访问,可以通过基类的公有方法和保护方法间接访问
如果派生类在继承基类的时候选择私有继承(private)
那么基类的公有成员就是在派生类中是充当私有成员,可以在派生类的内部使用,但是不能外部使用
那么基类的保护成员就是在派生类中是充当私有成员,可以在派生类的内部使用,但是不能外部使用
那么基类的私有成员可以继承,但是不能直接访问,可以通过基类的公有方法和保护方法间接访问
class Base1
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
//公共继承
class Son1 :public Base1
{
public:
void func()
{
m_A; //可访问 public权限
m_B; //可访问 protected权限
//m_C; //不可访问
}
};
void myClass()
{
Son1 s1;
s1.m_A; //其他类只能访问到公共权限
}
//保护继承
class Base2
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son2:protected Base2
{
public:
void func()
{
m_A; //可访问 protected权限
m_B; //可访问 protected权限
//m_C; //不可访问
}
};
void myClass2()
{
Son2 s;
//s.m_A; //不可访问
}
//私有继承
class Base3
{
public:
int m_A;
protected:
int m_B;
private:
int m_C;
};
class Son3:private Base3
{
public:
void func()
{
m_A; //可访问 private权限
m_B; //可访问 private权限
//m_C; //不可访问
}
};
class GrandSon3 :public Son3
{
public:
void func()
{
//Son3是私有继承,所以继承Son3的属性在GrandSon3中都无法访问到
//m_A;
//m_B;
//m_C;
}
}
3、类的多继承
多继承的格式
class 子类:继承方式 基类1,继承方式 基类2
{
变量成员;
函数成员;
};
多继承的时候,创建子类对象构造函数执行的顺序,跟基类继承的先后顺序有关
在派生类的构造函数初始化列表中指定基类的构造函数
多重继承可能给程序员带来很多新问题:
1、从两个不同的基类继承同名方法
2、从两个或更多相关基类那里继承同一个类的多个实例
所以,C++实际开发中不建议用多继承。
第一个问题可以通过子类重新实现或通过父类类名显示调用解决,我们着重于第二个问题的解决,这个问题引出一个很重要的继承:菱状继承(环状继承)
3.1菱形继承(环状继承,钻石继承)
概念:两个派生类继承同一个基类,又有某个类同时继承这两个派生类,这种继承被称为菱形继承
#include <iostream>
using namespace std;
//动物类
class Animal{
public:
Animal(){
cout<<"Animal()"<<endl;
}
Animal(int a):dataD(a)
{
cout<<"Animal(int a)"<<endl;
}
~Animal(){
cout<<"~Animal()"<<endl;
}
int dataD;
};
//羊类 --继承于 动物类
class Sheep:public Animal{
public:
Sheep(){
cout<<"Sheep()"<<endl;
}
Sheep(int a):dataA(a)
{
cout<<"Sheep(int a)"<<endl;
}
~Sheep(){
cout<<"~Sheep()"<<endl;
}
void show(){
cout<<"dataA value:"<<dataA<<"\t addr:"<<&dataA<<endl;
//cout<<"dataD value:"<<dataD<<"\t addr:"<<&dataD<<endl;
}
int dataA;
};
//驼类
class Tuo:public Animal{
public:
Tuo(){
cout<<"Tuo()"<<endl;
}
Tuo(int a):dataB(a)
{
cout<<"Tuo(int a)"<<endl;
}
~Tuo(){
cout<<"~Tuo()"<<endl;
}
void show(){
cout<<"dataB value:"<<dataB<<"\t addr:"<<&dataB<<endl;
}
int dataB;
};
//羊驼类
//多继承 的时候 基类的构造函数执行的顺序 跟 继承的先后顺序有关
// class SheepTuo:public Tuo,public Sheep
class SheepTuo:public Sheep,public Tuo
{
public:
//在构造函数的参数列表中指定基类的构造函数
SheepTuo(int a,int b):Tuo(b),Sheep(a)
{
cout<<"SheepTuo()"<<endl;
}
~SheepTuo(){
cout<<"~SheepTuo()"<<endl;
}
//在子类中重写 基类的同名函数,那么基类中的同名函数就会自动隐藏
void show(){
cout<<"dataA value:"<<dataA<<"\t addr:"<<&dataA<<endl;
cout<<"dataB value:"<<dataB<<"\t addr:"<<&dataB<<endl;
cout<<"dataC value:"<<dataC<<"\t addr:"<<&dataC<<endl;
// cout<<"dataD value:"<<dataC<<"\t addr:"<<&dataD<<endl;
}
int dataC;
};
int main()
{
//派生类对象的实例化
SheepTuo mya(10,20);
cout<<"mya size:"<<sizeof(mya)<<"\tmya addr:"<<&mya<<endl;
// mya.show();
//发生二义性问题
//mya.Animal::dataD = 100; //错误:羊驼类里面不能直接访问动物类里面的成员
// 通过基类的类名显示调用
mya.Sheep::show(); //在羊驼里面可以通过羊类或者驼类的公有方法来访问动物类的成员
// mya.Tuo::show();
return 0;
}
3.2菱形继承的解决方法-----虚继承
概念:为了解决从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题,将共同基类设置为虚基类,这时从不同的路 径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射。这样不仅就解决了二义性问题,也解决了内存,避免了数据不一致的问题
虚继承格式:
class 子类名称:virtual 继承方式关键字 父类名 { };
在多继承情况下,虚基类关键字的作用范围和继承方式关键字相同,只对紧跟其后的基类起作用
#include <iostream>
using namespace std;
class Animal
{
public:
int m_Age;
};
//继承前加virtual关键字后,变为虚继承
//此时公共的父类Animal称为虚基类
class Sheep : virtual public Animal {};
class Tuo : virtual public Animal {};
class SheepTuo : public Sheep, public Tuo {};
void test01()
{
SheepTuo st;
st.Sheep::m_Age = 100;
st.Tuo::m_Age = 200;
cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;
cout << "st.Tuo::m_Age = " << st.Tuo::m_Age << endl;
cout << "st.m_Age = " << st.m_Age << endl;
}
int main() {
test01();
return 0;
}
vbptr详细解释
v --- virtual --- 虚拟的
b --- base --- 基类
ptr --- pointer --- 指针
vbptr --- 虚基类指针,指向vbtable,记录相应的偏移量,指向唯一的数据内存存放位置