目录
继承,是一种可以使代码复用的重要手段
但是我们已经有类了,类也可使实现泛型编程来解决代码复用,那需要继承干什么呢?
继承可以解决普通代码复用难以解决的问题
比如dog是animal的一种,dog一定有animal的几种特性,如age,sex,那只需要在公用特性animal 中添加这几种特性就可以了,而不需要再次在dog中进行添加共用特性,而只关注dog中特有的特性。
// 公共信息放到Person类中
class Person
{
public:
void identity()
{
cout << "identity()" << endl;
}
protected:
string _name;//姓名
string _address;//地址
string _tel;//电话
private:
int _age;//年龄
};
class Student :public Person
{
public:
void study()
{
cout << "study()" << endl;
}
protected:
int _stuid;//学生学号
};
class Teacher :public Person
{
public:
void teachering()
{
cout << "teachering()" << endl;
}
protected:
string _title;//老师职称
};
1.继承的访问限定符 和 继承方式


一.访问限定符,用于限制类成员 在 子类,类外部 的可见性
1.父类成员中的访问限定符如果是private,表示在子类中,即使继承了父类的private成员,但是也不能访问,包括在子类里面访问和在类外面不能访问。
2.父类成员中的访问限定符是protected,表示在子类中,继承了父类的private成员,但在子类里面可以访问,在类外面不能访问
3.父类成员中的访问限定符是public,表示在子类和类外面都可以访问
二.继承方式,决定了 子类成员 继承 父类成员的权限
父类成员在子类成员中的继承权限=min(父类原权限,子类继承方式),public>protected>private
class futher {
public: int pub; // 父类公有成员
protected: int pro; // 父类保护成员
private: int pri; // 父类私有成员
};
class son : protected futher {
// 父类成员在子类中的权限:
// pub:min ( public , protected) =prtected
// pro: min ( protected ,protected)=protected
// pri: min ( private,protected)=private
public:
void func() {
pub = 1; // 可访问(子类内部访问父类原public成员,现为protected)
pro = 2; // 可访问(子类内部访问父类protected成员)
// pri = 3; // 子类不能访问父类private成员
}
};
使用关键字class,默认继承方式是private
使用struct关键字,默认继承方式是public
在实际中,大多使用public继承
刚刚是普通类继承,现在我们进行模板类继承
比如 栈 继承vector
那父类(vector)的成员都是 stack的
namespace sxm
{
template <class T>
class stack :public std::vector<T>
{
public:
void push(const T& x)
{
push_back(x);//必须在前面指定类域vector<T>
}
};
}
int main()
{
sxm::stack<int> st;
st.push(1);
st.push(2);
st.pop();
return 0;
}
sxm::stack<int> st,这一句实例化stack的同时也将vector实例化了,但因为vecto<T>是模板,只会按需实例化,也就是只实例化stack<T> vector<T> 的构造函数,其他vector<T>的成员还没被实例化。
接下来st.push(1);栈要实例化push,先调用 vector<int>push(const int& x) 函数,在push内部就会调用push_back,对于非模板父类,其成员在子类使用时会被自动查找;对于模板类父亲,成员在子类使用时不会自动到父类中去找,也不会主动实例化父类没使用的成员,因为要避免命名冲突,
因此,要在前面显示指定类域vector<int>强制编译器去父类中查找;强制实例化,因为被使用了。
namespace sxm
{
template <class T>
class stack :public std::vector<T>
{
public:
void push(const T& x)
{
//push_back(x);//本质上还会到父类中去找push_back,但
vector<T>::push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
};
}
当然stack也可以继承list<T>,deque<T>
这是可以用到宏替换
#define CONTAINER std::list
//#define CONTAINER deque
namespace sxm
{
template <class T>
class stack :public CONTAINER<T>
{
public:
void push(const T& x)
{
//push_back(x);//本质上还会到父类中去找push_back,但
CONTAINER<T>::push_back(x);
}
void pop()
{
CONTAINER<T>::pop_back();
}
};
}
int main()
{
sxm::stack<int> st;//实例化stack时,同时把vector实例化了(只会实例化vector的构造函数,按需)
st.push(1);
st.push(2);
st.pop();
return 0;
}
2.赋值兼容转换
父类和子类的类型不同,可以相互转换吗?
前提,要求是公有继承父类的子类,因为这样,子类才可以使用父类的全部接口
1.子类对象直接赋值给父类 对象
父类有的成员子类都有,可以把子类赋值给父类,子类特有的那一部分将其 "切片"
Student ss;
Person pp = ss;//子类对象直接赋值给父类对象,通过自动调用基类的拷贝构造实现,仅拷贝子类中属于父类的成员。

那子类对象"切片"行为是如何实现的?
当用子类对象定义父类对象时,Person pp=ss;编译器自动调用父类的拷贝构造,仅拷贝子类中属于父类的成员。
当父类对象已经存在,再用子类对象进行赋值时,p=ss;编译器会调用父类的赋值运算符重载,同样只拷贝父类部分的成员。
2.子类对象的指针/引用可以 赋值给 父类对象的 指针/引用
父类的指针或者引用指向的是子类被切出的那一部分
但它们之间没有类型转换(没有临时变量),直接切出来拷贝过去。
否则这样就编不过去
Student ss;
Person& p2 = ss;//如果有类型变换,那ss产生一个临时变量,临时变量有常性,再赋给p2,权限会放大,但此时编译通过了
int i=1;
//double& r=i;权限放大
const double& r=i;
3.父类对象不能直接赋值 给子类对象
反过来,父类对象不能给给子类,因为父类中没有子类特有的那部分(但是指针和引用可以dynamic_cast,父类的对象是可以指向子类的,因此可以转换回去,后续类型转换章节会讲解,现在提一下)
3.继承中的作用域
1.隐藏,如果子类和父类中有同名成员(包括成员变量和成员函数),子类成员会屏蔽父类成员,(类似于类,避免继承中的命名冲突)
class Person
{
protected:
string _name;//姓名
int _num = 111;
};
class Student :public Person
{
public:
void Print()
{
cout << _num << endl;
}
protected:
int _num=999;
};
int main()
{
Student s;
s.Print();//输出999
return 0;
}
如果要访问父类中的_num,前面加上类域
我们先看下面这道题:
1 .A和B类中的两个func构成什么关系(B)
A. 重载 B.隐藏 C.没关系
2.下面程序的编译运行结果是什么(A)
A. 编译报错 B.运行报错 C.正常运行
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)" <<i<<endl;
}
};
int main()
{
B b;
b.fun(10);
b.fun();//在子类中调用不到也不会去父类中找,即为“隐藏”
//b.A::fun();如果想调用需指定父类作用域
return 0;
};
重载:发生同一作用域下,函数名相同但参数列表不相同
注意,只要函数名相同,就构成隐藏关系。
4.派生类的默认成员函数
子类中的成员包括两部分,一是从父类中继承的成员,二是子类自己特有的成员(内置类型,自定义类型)
1.默认构造函数
内置类型--不确定
自定义类型--调用它的默认构造
继承的父类成员看作一个整体对象--调用父类的默认构造(无参数或所有参数都有默认值)

子类的默认构造必须调用父类的默认构造初始化父类的那一部分成员,
如果父类没有默认构造函数,比如我们自己显式定义了带参构造函数(一旦显式定义了任何构造函数(带参或不带参),编译器就不会再自动生成默认构造函数。)
就必须在子类初始化列表阶段显式调用
class Student :public Person
{
public:
Student(const char* name,int num,const char* address)//子类构造
:Person(name)//显式调用父类的构造函数,把父类看成一个整体,当作一个对象
,_num(num)
,_address(address)
{
cout << "Student()" << endl;
}

不能这样,在子类中直接初始化父类成员
Student(const char*name,int num,const char* address)//子类构造
:_name(name)//_name为父类成员
,_num(num)
,_address(address)
{
cout << "Student()" << endl;
}
也不能直接在子类函数体内初始化父类Person(name),这样相当于创建了一个临时匿名对象,而无法初始化父类
2.默认拷贝构造函数
同之前的原则类似
内置类型--完成值拷贝
自定义类型--调用它的拷贝构造
父类作为一个整体--调用父类的拷贝构造
Student的拷贝构造不需要我们写,默认的就够了,因为没有资源要释放
如何写拷贝构造?如果我要把一个父类拷贝给其他继承的子类,那我应该把这个父类的对象当作参数传递给你,但是子类中的拷贝构造参数中没有父类对象 Student(const Student& s),
这时就要用到上文所说的赋值兼容转换(Student s2(s1),将s1拷贝给s2,时,先调用父类的拷贝构造,将子类s1通过 父类的拷贝构造切片成父类的成员,然后调用子类的拷贝构造,将这个成员拷贝给s2)
s是一个子类对象,要拷贝父类的那一部分,需要把这一部分"切出来"
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;
}
protected:
string _name;//姓名
};
class Student :public Person
{
public:
Student(const char* name,int num,const char* address)//子类构造
:Person(name)//显式调用父类的构造函数,把父类看成一个整体,当作一个对象
,_num(num)
,_address(address)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)//初始化列表初始化顺序跟声明顺序有关,父类最先声明,一般把父类的赋值,构造等写在初始化列表最前面
,_num(s._num)
,_address(s._address)
{
//深拷贝
}
protected:
int _num;//学号
string _address;//地址
//int* _ptr;有资源
};
int main()
{
Student s1("zhangsan", 1, "home");
Student s2(s1);
return 0;
}
子类的拷贝构造函数必须调用父类的拷贝构造 完成 父类的拷贝初始化
3.赋值
完全类似
如果有深拷贝的资源,就需要自己实现,否则,默认的就够了
接下来是显示写
同样子类赋值需要显式调用父类的赋值,但此时我只有子类对象,而没有父类对象作为参数 传递给父类的 赋值函数,
同样用到了赋值兼容转换,传子类 默认 把子类 切片转换为父类
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;
}
protected:
string _name;//姓名
};
class Student :public Person
{
public:
Student(const char* name,int num,const char* address)//子类构造
:Person(name)//显式调用父类的构造函数,把父类看成一个整体,当作一个对象
,_num(num)
,_address(address)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
,_num(s._num)
,_address(s._address)
{ }
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);//显示调用父类的赋值,指定类域,否则构成隐藏
_num = s._num;
_address = s._address;
}
return *this;
}
protected:
int _num;//学号
string _address;//地址
};
int main()
{
Student s1("zhangsan", 1, "home");
//Student s2(s1);
Student s3("lisi", 2, "school");
s1 = s3;
return 0;
}
子类的赋值operator=必须要调用父类的赋值完成父类的复制。注意的是父类赋值函数operator=隐藏了子类的赋值函数operator=,所以显示调用父类的operator=要指定类域 为父类
4.默认析构
Student的默认析构已经够用了
但是我们发现在子类析构中显示调用父类析构 构成了隐藏关系
因为一些原因,析构函数的函数名会被特殊处理,destructor(),所以父类析构函数名与子类相同,要指定类域。
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(const char* name,int num,const char* address)//子类构造
:Person(name)//显式调用父类的构造函数,把父类看成一个整体,当作一个对象
,_num(num)
,_address(address)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
,_num(s._num)
,_address(s._address)
{ }
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s);//显示调用父类的赋值,指定类域,否则构成隐藏
_num = s._num;
_address = s._address;
}
return *this;
}
~Student()
{
Person::~Person();//指定作用域
}
protected:
int _num;//学号
string _address;//地址
};
int main()
{
Student s1("zhangsan", 1, "home");
//Student s2(s1);
Student s3("lisi", 2, "school");
s1 = s3;
return 0;
}
~Student()
{
Person::~Person();//显式调用,先父后子
}
注意要保证析构顺序,子类调用析构函数之后,会自动调用父类析构。如果先析构了父类,会导致子类的未定义行为
不需要显式调用析构,否则打乱顺序
5.如何实现一个不能被继承的类?
1.将父类的构造函数设置为私有,因为子类构造一定会调用父类的构造 来初始化父类的哪一个部分的成员,如果设置为私有以后,那父类的构造就不能再子类中被调用
2.C++11增加了final,在父类中家长这个关键字之后,这个父类就不能被继承了
class Base final
{
protected:
int _a = 1;
};
class Derive :public Base
{
protected:
int _b = 2;
};
友元关系会被继承吗
父类的友元不会被子类继承
class Base
{
public:
friend void Display();//Display是Base的友元
protected:
int _a = 1;
};
class Derive :public Base
{
protected:
int _b = 2;
};
void Display()
{
Base b;
cout << b._a << endl;//正确,Display是Base的友元,Display可以访问 Base中 被保护的成员
Derive d;
cout << d._b << endl;//错误,Derive 没有继承 Base 的友元 Display,Derive 和 Display不是友元关系,不能在Display 中访问 Derive中 被保护的成员
cout << d._a << endl;//正确,_a是从父类中继承过来的,可以在 Display 中 访问父类被保护的成员
}
子类的友元也无法访问父类的成员
class Base
{
protected:
int _a = 1;
};
class Derive :public Base
{
public:
friend void Display();// Derive 和 Display是友元
protected:
int _b = 2;
};
void Display()
{
Derive d;
cout << d._b << endl;//正确,Derive 和 Display 是友元,可以在Display中访问 在Derive中被保护的成员
cout << d._a << endl;//错误,Base 和 Display 不是友元,不能通过 子类 访问 在Base中被保护的基类成员
}
6.继承与静态成员
父类中有一个静态成员,则在整个继承体系之中,只会是这同一个static成员。
而如果是普通的非静态成员,整个继承体系会有多份变量名相同但地址不同的 同名变量

单继承,多继承和菱形继承
单继承,即子类只有一个父类
多继承,子类继承了多个父类,多继承的这个派生类中,多个父类在内存中的顺序是,先继承的基类在前面,后继承的在后面,派生类的成员在最后

有了多继承,就一定会有菱形继承,即两个派生类继承了相同的基类,然后这两个派生类又被 派生类 当作基类继承,

则这时候就会出现二义性的问题
子类Assistant中会有两个Person基类中的成员,即会有2个_name
那这时候我们就有了虚继承的概念,
虚继承
关键字virtual,在中间基类的继承方式前添加virtual关键字,让最终派生类只保留一份重复的基类
比如,
class A{public:int a = 1;};
//B,C使用虚继承
class B :virtual public A{public:int b = 1;};
class C :virtual public A{public:int c = 1;};
// D继承自B和C,此时A的成员在D中只存在一份
class con:public B,public C{public:int con = 1;};
那如何初始化呢
规定,虚基类必须由最终的派生类直接初始化,而中间基类对虚基类的初始化会被编译器强制忽略
class Person
{
public:
Person(const char* name)
:_name(name)
{
cout << "Person()" << endl;
}
protected:
string _name;//姓名
};
class Student :virtual public Person
{
public:
Student(const char* name,int num)
:Person(name)
,_num(num)
{}
protected:
int _num;//学号
};
class Teacher :virtual public Person
{
public:
Teacher(const char* name, int id)
:Person(name)
, _id(id)
{}
protected:
int _id;//教师编号
};
//即使学生又是老师
class Assistant:public Student,public Teacher
{
public:
Assistant(const char* name1,const char* name2,const char*name3)
:Person(name3)//有一个 Person 子对象,初始化
,Student(name1,1)//调用Student基类的构造函数,但忽略对Person(name1)的初始化
,Teacher(name2,2)//调用Teacher基类的构造函数,但忽略对Person(name2)的初始化
{ }
protected:
int _major;
};
int main()
{
Assistant a("张三", "李四", "王五");//_name=“王五”
return 0;
}

我们在开发时尽量避免使用菱形继承
C++中的iostream就是菱形继承
我们看下面一道题
先声明的先初始化

7.继承和组合
继承is-a(是一个),比如苹果是一种水果,A继承B,A是B的特殊类型,也就是每一个派生类对象都是一个基类对象,类似白箱复用,继承关系中,子类可以访问父类的内容,耦合度极高。破坏了面向对象中 封装的概念。
组合has-a(有一个),一个类包含另一个类,A中包含B的对象,即A由B组成,也就是每个A对象中都包含了一个B对象。黑箱复用,类似于之前的栈,它可以用deque,list等作为容器,list等底层容器的一些功能在 stack中是隐藏的。对象的具体细节在外部不能被看到,此为"黑箱"。组合类之间没有很强的依赖关系。
如果类之间的关系既适合用继承(is-a)也适合组合(has-a),那就用组合。






