引入
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。派生类可以继承基类的属性和方法。
在自然界中,我们把物种归为不同的界门纲目科属种。以偶蹄目为例,偶蹄目都能吃草;但是不同动物的叫声不同。我们就可以把吃草这个方法保存的基类当中,把不同动物的设置成不同的派生类。
eg.有一只猪彭彭,一只河马花花。他们都是偶蹄目动物。故把偶蹄目作为他们的基类,猪和河马都作为偶蹄目这个基类的派生类,都继承了吃草这个方法。猪和河马有不同。他们不同的属性和方法分别实现在Pig和Hippopotamus这个派生类中。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class Artiodactyla //偶蹄目
{
public:
void func()
{
cout << "能吃草" << endl;
}
private:
};
class Pig :public Artiodactyla
{
public:
void voice()
{
cout << "猪叫" << endl;
}
private:
int nums = 2;//猪有2趾
};
class Hippopotamus :public Artiodactyla//河马
{
public:
void voice()
{
cout << "河马叫" << endl;
}
private:
int nums = 4;//河马有4趾
};
int main()
{
Pig pengpeng;
Hippopotamus huahua;
cout << "pengpeng的属性" << endl;
pengpeng.func();
pengpeng.voice();
cout << "huahua的属性" << endl;
huahua.func();
huahua.voice();
return 0;
}
继承的定义
定义格式
我们看到的Artiodactyla称为基类,也叫父类。Pig和Hippopotamus称为派生类,也称子类。
继承方式分为三种:public,private,protected。这和三个访问限定符一样,但是功能却不相同。在访问限定符中, private和protected几乎没有差别,都不能由类外直接访问。而在继承方式这里,private与protected就有所区别了。
继承方式的访问限制
public的限制最少,protected次之,private限制最多。要判断继承的成员在子类中为什么成员,看它在基类中的类型和继承的方式,取限制较多的类型。例:基类中的protected成员取public继承,protected限制多,所以取protected。
1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。
2. 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式),
public > protected>private。
4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。
5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤ protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实 际中扩展维护性不强。
注意:基类中的private成员只是在子类中无法被访问(不可见),不是没有被继承。注意这样的判断题。
演示三种继承:
public继承:
class Artiodactyla //偶蹄目
{
public:
void func()
{
cout << "能吃草" << endl;
}
protected:
int weight;
private:
int age;
};
class Pig :public Artiodactyla
{
public:
void voice()
{
cout << "猪叫" << endl;
}
// 添加一个成员函数来获取weight
int getWeight() const {
return weight;
}
private:
int nums = 2;//猪有2趾
};
int main()
{
Pig pengpeng;
pengpeng.voice();
pengpeng.func();
pengpeng.getWeight();
return 0;
}
没有报错。
protected继承:
class Artiodactyla //偶蹄目
{
public:
void func()
{
cout << "能吃草" << endl;
}
protected:
int weight;
private:
int age;
};
class Pig :protected Artiodactyla
{
public:
void voice()
{
cout << "猪叫" << endl;
}
// 添加一个成员函数来获取weight
int getWeight() const {
return weight;
}
private:
int nums = 2;//猪有2趾
};
int main()
{
Pig pengpeng;
pengpeng.voice();
pengpeng.func();
pengpeng.getWeight();
return 0;
}
func变成protected成员,不能在类域外直接访问。
private继承:
class Artiodactyla //偶蹄目
{
public:
void func()
{
cout << "能吃草" << endl;
}
protected:
int weight;
private:
int age;
};
class Pig :private Artiodactyla
{
public:
void voice()
{
cout << "猪叫" << endl;
}
// 添加一个成员函数来获取weight
int getWeight() const {
return weight;
}
private:
int nums = 2;//猪有2趾
};
int main()
{
Pig pengpeng;
pengpeng.voice();
pengpeng.func();
pengpeng.getWeight();
return 0;
}
func()变为private对象,不可由类外直接访问。
继承类模板
当我们需要实现有些功能时,往往会想一想自己之前是否实现过类似的功能,如果把以前的代码修改一下接口就能完美匹配当下的业务,就没有必要再自己手搓了。
现在我们需要手动实现一个栈,我们之前已经实现了vector。这时,我们可以新建一个Stack类来继承vector的一些接口来实现Stack这个类,这样大大提高了我们的效率,减少我们代码的重复率。
#include<vector>
namespace yg
{
template<class T>
class Stack :public std::vector<T>
{
public:
void push(const T& x)
{
vector<T>::push_back(x);//Stack<T>实例化时,vector<T>也实例化了,但是编译器是按需实例化的,push_back()没有实例化,所以要指定
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
bool empty()
{
return vector<T>::empty();
}
private:
};
}
int main()
{
yg::Stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}
基类和派生类之间的转换
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student sobj;
// 1.派⽣类对象可以赋值给基类的指针/引⽤
Person* pp = &sobj;
Person& rp = sobj;
// ⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
Person pobj = sobj;
//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
sobj = pobj;
return 0;
}
贴片:
用Student类继承的_name,_sex和_age来初始化Person。_No砍掉不要。
继承中的作用域
隐藏规则
#include<string>
class Person
{
public:
string _name = "pp";
int _id = 100087;//身份证号
protected:
};
class Student:public Person
{
public:
int _id;//学号
void print()
{
cout << "姓名:" << _name << endl;
cout << "身份证号:" << Person::_id << endl;//构成隐藏后要指定类域访问才能访问基类中的同名成员
cout << "学号:" << _id << endl;
}
protected:
};
int main()
{
Student a;
a._id = 10086;
a.print();
return 0;
}

考继承作用域相关选择题
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();
return 0;
};
1.A和B两个类中的func()构成什么关系?
这个时候就考验我们对各个概念的掌握情况了,函数重载有一个先行条件,两个函数要在同一作用域。而基类和子类是两个不同的作用域,且函数同名,所以构成隐藏。
2.这段程序的编译运行结果是什么?
b是一个B类型的对象,基类中的func()和子类中的func(int i)构成隐藏,不能访问到基类中的不传参的func()函数。所以会编译报错。
派生类的默认成员函数
4个常见的默认成员函数
在学习类和对象的时候我们知道一共有6个默认成员函数,但是我们经常接触的只有,构造,析构,拷贝构造和赋值重载着四个。
1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。
2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
3. 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。
5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
6. 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
7. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
class Person
{
public:
Person(const char* name = "peter")
: _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)
: Person(name)
, _num(num)
{
cout << "Student()" << endl;
}
Student(const Student& s)
: Person(s)
, _num(s._num)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
// 构成隐藏,所以需要显⽰调⽤
Person::operator =(s);
_num = s._num;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num; //学号
};
int main()
{
Student s1("jack", 18);
Student s2(s1);
Student s3("rose", 17);
s1 = s3;
return 0;
}
总结就是,派生类的初始化总在基类之后,先初始化继承的基类部分再处理派生类独有的部分才具有逻辑连贯性,先初始化派生类的化可能会用到基类的一些成员,但这时候基类的成员是未初始化的。派生类总是比基类先析构,调用派生类的析构函数之后,会自动调用基类的析构函数。也是同样的原因,如果先析构基类,那么派生类所继承的部分就是未初始化的。
不能被继承的类
如果我们想实现一个不能被继承的类,一共有两个办法。
1.私有化构造函数
派生类需要基类的构造函数来初始化继承的部分,我们直接将基类的构造函数私有化。这样派生类就不能初始化。
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
private:
A()
{
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "func(int i)" << i << endl;
}
};
int main()
{
B b;
b.fun(10);
return 0;
};
2.final关键字
c++11为我们提供了一个关键字,在不想被继承的类后边加一个final,就不能再被其他类继承。
class A final
{
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);
return 0;
};
继承与友元
友元关系不能被继承
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);//要用到Student类,前置声明一下
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
// 编译报错:error C2248: “Student::_stuNum”: ⽆法访问 protected 成员
// 解决⽅案:Display也变成Student 的友元即可
Display(p, s);
return 0;
}
继承与静态成员
static成员储存在静态区,在继承其他成员时派生类会单独开空间来储存,而静态变量派生类不会再额外开空间来储存这个静态变量,派生类和基类共用这一个对象。
class Person
{
public:
string _name;
static int _count;
};
int Person::_count = 0;
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;
}
多继承与菱形继承
继承模型



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;
}

虚继承
为了解决菱形继承的问题,c++引入了一个新的概念——虚继承。在有菱形继承时,只继承一份。
class A {
public:
int value;
};
class B : virtual public A {}; // B 虚继承 A
class C : virtual public A {}; // C 虚继承 A
class D : public B, public C {}; // D 继承 B 和 C
注意加virtual的位置,如果是普通继承,那么A中的成员会被D继承两份。所以在要在第一个继承A的类型加virtual。
例如在这样一个菱形继承关系当中:
我们应该对B和D加virtual关键字。使用virual就可以解决,数据冗余和二义性。但是我们一般不设计菱形继承。
class Person
{
public:
string _name; // 姓名
/*int _tel;
int _age;
string _gender;
string _address;*/
// ...
};
// 使⽤虚继承Person类
class Student : virtual 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 = "peter";
return 0;
}
多继承中的指针偏移问题
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会指向base1开头,p2会指向base2开头。Drive继承了base1和base2,先继承基类的成员,而base1在base2之前,故Drive以base1开头。
故选择C项。
内存分布图:
继承与组合
class Engine
{
public:
string brand = "丰田";
};
class Car
{
public:
Engine engine;//Car有一个Engine
string color = "white";
};
这就是一个组合。
is-a:猪是偶蹄目的
class Artiodactyla //偶蹄目
{
public:
void func()
{
cout << "能吃草" << endl;
}
protected:
int weight;
private:
int age;
};
class Pig :public Artiodactyla
{
public:
void voice()
{
cout << "猪叫" << endl;
}
private:
int nums = 2;//猪有2趾
};
当然,有些特殊情况既满足has-a又满足is-a。
例如:vector和Stack
template<class T>
class stack : public vector<T>
{};
template<class T>
class stack
{
public:
vector<T> _v;
};
既可以看成Stack是一个特殊的vector,也可以看成Stack中有一个vector。