继承的学习
1.继承
继承权限在类外,访问权限在类内部
1.1继承的概念
继承是面向对象程序设计使代码可以复用的重要手段(解决类层次的复用问题)
派生类:类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量)这样产生的新类
没有继承之前我们设计了Student和Teacher两个类,这两个类都有姓名 ,地址,电话,年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面是多余的
他们也有一些不同的成员函数和变量
class Student
{
public://身份认证void identty(){}//学习void study(){}
protected:string _name = "peter";//姓名string _address; //地址string _tel; //电话int _age = 20; //年龄int _stuid; //学号
};class Teacher
{
public://身份认证void identty(){}//授课void teaching(){}
protected:string _name = "peter";//姓名string _address; //地址string _tel; //电话int _age = 20; //年龄string _title; //职称
};
我们把从共成员都放到Person类中,Student和Teacher都继承Person,就可以复用这些成员,不需要重复定义了
class Person
{
public://身份认证void identity(){cout << "void identity()" << _name << endl;}protected:string _name = "小美";//姓名string _address; //地址string _tel; //电话int _age = 20; //年龄
};
class Student :public Person
{
public://学习void study(){}
protected:int _stuid; //学号
};class Teacher :public Person
{
public://授课void teaching(){}
protected:string _title; //职称
};int main()
{Student s;s.identity();
}//void identity()小美
1.2继承定义
1.2.1定义的格式
核心:实行复用,子类复用父类不用重新定义了
Person 是基类,也称作父类
Student 是派生类,也称作子类
派生类 继承方式 基类
class Student :public Person
{
public:
int _stuid;
};
1.2.2继承基类成员访问方式的变化
继承方式:public继承 protected继承 private继承
访问限定符:public访问 protected访问 private访问
1.基类prviate成员在派生类中无论以什么方式继承都是不可见的
基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能被访问
2.基类private成员在派生类中是不能被反问的,如果基类成员不想在类外面直接被访问,但需要在派生类中能访问,就定义为protected
3.基类的私有成员在派生类都是不可见
基类的其他成员在派生类的访问方式==Min(成员在基类的访问限定符,继承方式)
public > protrcted > private
4.使用关键字class时默认的继承方式是private
class Person
{
public://身份认证void identity(){cout << "void identity()" << _name << endl;}protected:string _name = "小美";//姓名
};
class Student :Person
{
public:void func(){}
protected:int _stuid; //学号
};int main()
{Student s;s.identity();
}
使用struct时默认的继承方式是public
可以
最好显示写出继承方式
派生类在继承基类时,可以不指定继承方式,默认为private
1.3继承类模板
1.3.1用继承的方式实现栈
#define _CRT_SECURE_NO_WARNINGS 1
#include<vector>
#include<iostream>
using namespace std;
namespace DD
{// stack和vector的关系,既符合is-a,也符合has-atemplate<class T>class stack : public std::vector<T>{public:void push(const T& x){// 基类是类模板时,需要指定⼀下类域,// 因为stack<int>实例化时,也实例化vector<int>了// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到vector<T>::push_back(x);//要指定一下//push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}
int main()
{DD::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}//3 2 1
模板(外加)
//模板
template<class Container>
void Print(const Container& c)
{//加typename//const_iterator可能是静态成员变量,也可能是内嵌类型(内部类,typedef)//typename Container::const_iterator it = c.begin();//也可以这样写auto it = c.begin();while (it != c.end()){cout << *it << " ";}cout << endl;
}
int main()
{vector<int> v1 = { 1,2,3,4 };list<int> l1 = { 10,20,30,40 };Print(v1);//会实例化一份vector的Print(l1);//会实例化一份list的return 0;
}
2.基类和派生类间的转换
1)piblic继承的派生类对象可以赋值给基类的指针/基类的引用
这里有个形象的说法叫切片或者切割 --- 寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分
2)基类对象不能赋值给派生类对象
3)基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但是必须是基类的指针是指向派生类对象时才是安全的
class Person
{
protected:string _name;string _sex;
public:int _age=18;
};
class Student :public Person
{
public:int _No;
};
int main()
{Student sobj;//赋值兼容转换,没有产生临时对象,特殊处理//派生类对象可以赋值给基类的指针/引用Person* pp = &sobj;//指针指向父类那一部分,指针还是存的这个位置的地址//指针指向派生类基类的一部分//在派生类里面父类的成员是在前面的,自己的成员是在后面的//所以切一个Person那么大Person& rp = sobj;//不是引用中间产生的临时对象,引用的是切割切片出来的那一部分//rp变成Student切割切片切出来的父类的那一部分的别名//变成Person这一部分的别名//age++修改的是Student里面的成员//引用引用的是派生类基类的那一部分rp._age++;return 0;
}
类型转换会产生临时对象
double d=1.1;
const int & i=d;
i引用的是临时对象,临时对象具有常性,要加const
string s1=“11111”;
const string& s2="11111";
拷贝构造,也会产生临时对象
关于基类与子类对象之间赋值说法:
1.基类指针可以直接指向子类对象
2.基类不能给子类对象直接赋值,父类类型对于子类类型来说类型不完全
3.不能用父类初始化子类引用
4.子类对象可以直接赋值给基类对象
3.继承中的作用域
3.1 隐藏规则
1.基类和派生类都有独立的作用域
2.隐藏:派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问
可以使用 基类::基类成员 在派生类成员函数中显示访问
class Person
{
protected:string _name = "小妹";int _num = 111;//身份证号
};
class Student :public Person
{
public:void Print(){cout << "姓名:" << _name << endl;cout << "学号:" << _num << endl;cout << "身份证:" <<Person:: _num << endl;//显示访问}
protected:int _num = 666;//学号
};
int main()
{Student s1;s1.Print();return 0;
}
姓名:小妹
学号:666
身份证:111D:\code\41-4-codes\11_6\x64\Debug\11_6.exe (进程 37424)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .
3.如果是成员函数的隐藏,只需要函数名相同就构成隐藏(参数相同是隐藏,参数不同也是隐藏)
4.在实际继承体系中最好不要定义同名的成员
成员函数在同一个类里面同名,此时构成了重载,但变量一定不能同名
4.派生类的默认成员函数
一般情况下,派生类只需要写构造
构造
//子类中继承下来的父类成员当作一个整体对象
//构造
//默认的行为:成员变量分为内置类型和自定义类型
//内置类型(有缺省值就用,没有缺省值它的值是不确定的 0/随机值)
// 自定义类型(会调用自定义类型的默认构造,要是这个自定义类型没有默认构造,就要显示去写它的构造---在初始化列表初始化它的自定义类型)
//派生类的构造要叠加父类日成员,会把父类成员当作一个整体 ,必须调用父类默认构造
//所以派生类构造
//子类成员 内置类型和自定义类型 + 父类成员
//会把父类当成一个整体就像自己里面的一个自定义类型的对象一样
//都需要调用父类的构造去初始化父类的一部分
//初始化列表是根据声明的顺序初始化的
//在派生类里面父类是声明在最前面的,所以先初始化父类
class Person
{
public:Person(const char* name): _name(name){cout << "Person()" << endl;}Person(const Person& p):_name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}
protected:string _name;//姓名
};
class Student :public Person
{
public:Student(int num,const char* address,const char* name):_num(num),_address(address),Person(name)//没有名称,就像定义一个匿名对象一样{}protected:int _num;//学号 内置类型string _address;//自定义类型
};
//子类中继承下来的父类成员当作一个整体对象
//构造
//默认的行为:成员变量分为内置类型和自定义类型
//内置类型(有缺省值就用,没有缺省值它的值是不确定的 0/随机值)
// 自定义类型(会调用自定义类型的默认构造,要是这个自定义类型没有默认构造,就要显示去写它的构造---在初始化列表初始化它的自定义类型)
//派生类的构造要叠加父类日成员,会把父类成员当作一个整体 ,必须调用父类默认构造
//所以派生类构造
//子类成员 内置类型和自定义类型 + 父类成员
//会把父类当成一个整体就像自己里面的一个自定义类型的对象一样//都需要调用父类的构造去初始化父类的一部分int main()
{Student s1(10,"北京","pp");return 0;
}
//初始化列表是根据声明的顺序初始化的
//在派生类里面父类是声明在最前面的,所以先初始化父类
拷贝构造
//拷贝构造:子类成员 内置类型(值拷贝)自定义类型(调用它的拷贝构造) + 父类成员(必须调用父类的拷贝构造
如果是一个基类对象,传基类对象就调用基类的拷贝构造
如果是一个派生类对象,传派生类对象就调用派生类的拷贝构造
//拷贝构造:子类成员 内置类型(值拷贝)自定义类型(调用它的拷贝构造)
// + 父类成员(必须调用父类的拷贝构造)int main()
{Student s1(10,"pp","北京");Student s2(s1);//自定义类型address会调用string的拷贝构造//我们不写默认生成//深拷贝类型的时候,拷贝构造和赋值才需要自己写return 0;
}
Person()
Person(const Person& p)
赋值重载
//赋值重载:类似拷贝构造
//赋值重载:类似拷贝构造
int main()
{Student s1(10,"北京","pp");Student s2(s1);//自定义类型address会调用string的拷贝构造//我们不写默认生成//深拷贝类型的时候,拷贝构造和赋值才需要自己写Student s3(10, "北京", "pd");s1 = s3;//我们不写,会调用父类的赋值重载//编译器会默认生成默认赋值重载return 0;
}
析构
//会默认调用(就够我们用了)
//子类成员 内置类型(不处理)自定义类型(调用它的析构) + 父类成员(调用它的析构)
//需要我们显示写时(深拷贝)
//自己实现的话,不需要显示调用父类析构,子类析构函数结束后,会自动调用父类析构
Person()
Person(const Person& p)
Person()
Person operator=(const Person& p)
~Person()
~Person()
~Person()
如果要我们显示写析构
因为多态中·一些场景析构函数需要重写,重写的条件之一是函数名相同。编译器会对析构函数进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构成隐藏关系(我们显示调的时候是调不到的,要指定一下)
class Person
{
public:{}//destructor()~Person(){cout << "~Person()" << endl;}
protected:string _name;//姓名
};class Student :public Person
{
public:{}//destructor~Student(){cout << "~Student()" << endl;//delete[] _ptr;Person::~Person();}protected:int _num;//学号 内置类型
};
Person()
Person(const Person& p)
Person()
Person operator=(const Person& p)
~Student()//1
~Person()
~Person()
~Student()//2
~Person()
~Person()
~Student()//3
~Person()
~Person()D:\code\41-4-codes\11_6\x64\Debug\11_6.exe (进程 10772)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .//有s1 s2 s3 三个对象//构造先父后子,析构先子后父
由于规定,析构时必须现析构子再析构父
显示写无法保证先子后父
//后定义的先析构,会先析构s3
//自己实现的话,不需要显示调用父类析构,子类析构函数结束后,会自动调用父类析构
class Person
{
public:{}//destructor()~Person(){cout << "~Person()" << endl;}
protected:string _name;//姓名
};class Student :public Person
{
public:{}//destructor~Student(){cout << "~Student()" << endl;//delete[] _ptr;//Person::~Person();}protected:int _num;//学号 内置类型
};
关于派生类构造函数与析构函数:
.)子类实例化对象,由于继承的有父类,所以会先构造父类,然后再构造子类,析构顺序完全按照构造的相反顺序进行析构
..)派生类的析构函数往往还需要连同父类析构函数一起调用,同时清除父类的资源
4.2实现一个不能被继承的类
1)
基类的构造函数私有,派生类的构成必须调用基类的构造函数,但是基类的构造函数私有化以后,派生类就看不见就不能调用了,这样派生类就无法实例化出对象
2)
C++11新增了一个final关键字,final修改基类,派生类就不能继承了
class Person final
{
};
class Student :public Person
{
};
5.继承与友元
友元关系不能继承,也就是说基类友元不能访问派生类私有保护成员
6.继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员,无论派生出多少个派生类,只有一个static成员实例
class Person
{
public:string _name;static int _count;
};
int Person::_count = 6;class Student :public Person
{
protected:int _stuNum;
};
int main()
{Person p;Student s;//非静态成员_name的地址是不一样的//派生类继承下来了,基类派生类对象各有一份cout << &p._name << endl;cout << &s._name << endl;//静态成员_count的地址是一样的//派生类和基类共用同一份静态成员cout << &p._count << endl;cout << &s._count << endl;//公有的情况下,基类派生类指定类域都可以访问静态成员cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}
000000B7CED3F978
000000B7CED3F9B8
00007FF6AA390004
00007FF6AA390004
6
6D:\code\41-4-codes\11_6\x64\Debug\11_6.exe (进程 19072)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .
7.多继承及其菱形继承问题
7.1继承类型
单继承:一个派生类只有一个直接基类时称这个继承关系为单继承
多继承:一个派生类有两个或两个以上直接基类时称这个继承关系为继承
多继承对象在内存中的模型是:先继承的基类在前面,后面继承的基类在后面,派生类成员在最后面
菱形继承:支持多继承就一定会有菱形继承
应该都具有唯一的一份,这样的继承会造成数据冗余和二义性的问题
尽量避免菱形继承
//显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
class Person{public:string _name; // 姓名
};
class Student : public Person
{
protected:int _num; //学号
};
class Teacher : public Person
{
protected:int _id; // 职⼯编号
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
int main()
{// 编译报错:error C2385: 对“_name”的访问不明确Assistant a;//a._name = "peter";// 不明确// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}
7.2 虚继承
有了菱形继承就有菱形虚拟继承,底层实现很复杂,性能也会有损失
//使用虚继承可以解决菱形继承冗余和二义性问题
class Person{public:string _name; // 姓名
};
//使用虚继承Person类
class Student : vittual public Person
{
protected:int _num; //学号
};
//使用虚继承Person类
class Teacher : virtual public Person
{
protected:int _id; // 职⼯编号
};
//教师助理
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
int main()
{ // 使用虚继承,可以解决数据冗余和二义性Assistant a;a._name = "pp";return 0;
}
7.3 多继承指针偏移问题
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}//p1==p3!=p2
8.继承和组合 ---- 本质上都是类之间的复用
1)public继承是一种 is-a 的关系 --- 每个派生类对象都是一个基类对象
2)组合是一种 has-a 的关系 --- 如果B组合了A,每个B对象中都有一个A对象
3)继承允许我们根据基类的实现来定义派生类的实现
在继承方式中,基类的内部细节派生类可见。继承一定程度破坏了基类的封装,基类的改变对派生类有很大影响
派生类和基类间的依赖关系很强,耦合度高
4)对象组合是类继承之外的另一种复用选择。对象组合要求被组合的对象具有良好的定义的接口
对象的内部细节是不可见的,组合之间没有很强的依赖关系,耦合度低(代码维护性好)
优先使用对象组合有助于保持每个类被封装
5)优先使用组合
类之间的关系就适合继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承,也适合用组合