一.继承的定义
继承是面向对象编程(OOP)中的一个核心概念,它允许一个类(称为子类或派生类)基于另一个类(称为父类、基类或超类)来定义。它允许我们在保留原有类特性的基础上进行扩展,新增方法(成员函数)和属性(成员变量),由此产生的新类称为派生类。继承机制构建了面向对象程序设计的层次结构,体现了从简单到复杂的认知过程。
例如:我们有两个类Student类和Teacher类,我们发现两个类有大量相同的成员属性,和各自的成员方法,这个时候我们可以把两个类提取出一个父类,采用继承方式可以省去一些麻烦。
cpp
class Student
{
public:
void study()
{
}
void havelunch()
{
}
std::string _name; // 姓名
std::string _age; // 年龄
std::string _address; // 家庭地址
std::string _sex; // 性别
std::string _sid; //学号
};
class Teacher
{
public:
void teach()
{
}
void havelunch()
{
}
std::string _name; // 姓名
std::string _age; // 年龄
std::string _address; // 家庭地址
std::string _sex; // 性别
std::string _sid; //教师号
};
提取出父类后,进行继承复用,我们看到代码变得简洁不少,通过继承也保留了原来的属性
cpp
#include <iostream>
#include <string>
class Person
{
public:
void havelunch()
{
std::cout << _name << "在吃午饭" << std::endl;
}
std::string _name; // 姓名
std::string _age; // 年龄
std::string _address; // 家庭地址
std::string _sex; // 性别
};
class Student : public Person
{
public:
void study()
{
}
std::string _sid; // 学号
};
class Teacher : public Person
{
public:
void teach()
{
}
std::string _sid; // 教师号
};
int main()
{
Student s1;
s1._name = "赵六";
Teacher t1;
t1._name = "王老师";
s1.havelunch();
t1.havelunch();
return 0;
}
二.继承的格式
2.1继承方式
从下面看到,Student是派生类(子类),Person(基类),public是继承方式

C++继承中有三种继承方式分别为public ,protected ,private,这三种也是类的访问限定符


2.2继承类成员访问的变化
| 类成员 / 继承方式 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
| 基类的 public 成员 | 派生类的 public 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 protected 成员 | 派生类的 protected 成员 | 派生类的 protected 成员 | 派生类的 private 成员 |
| 基类的 private 成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
1.基类的private成员无论那种继承方式都不能被派生类以及外部访问。
2.如果说派生类想要访问基类的成员,但是有不想基类被外部访问,就可以把基类的成员改成protect属性。
4.从上面表格可以看出,除了基类的private成员以外,其他的成员在派生类访问方式 =min(访问限定符, 继承方式) public > protected > private;
5.当然继承方式的关键字也可以忽略掉,但是建议最好加上,class默认的继承方式是private而struct的继承方式则是public。
6.一般实际运用中使用的大部分是public继承,因为private和protected继承的成员都只能在派生类内部使用,可拓展性和维护性不强。
2.3继承类模版
下面我们通过继承vector类模版实现了一个栈,其中有一点很重要,要是用模版基类的函数必须要加上类域不然会报错找不到这个函数,模版是按需进行的实例化,如果你不加上类域那么,编译器不会认为你这个函数是模版基类里的函数从而不给你实例化,这时自然而然就找不到函数了。
cpp
#include <iostream>
#include <vector>
namespace valanliya {
template <class T>
class stack : public std::vector<T>
{
public:
void Push(const T& value)
{
// 基类是类模版时需要指定一下类域 不然编译器会报错找不到push_back
// error C3861: "push_back": 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
//push_back(value);
std::vector<T>::push_back(value);
}
void Pop()
{
std::vector<T>::pop_back();
}
const T& Top()
{
return std::vector<T>::back();
}
bool Empty()
{
return std::vector<T>::empty();
}
};
}
int main()
{
valanliya::stack<int> st;
st.Push(1);
st.Push(2);
st.Push(3);
std::cout << st.Top() << std::endl;
st.Pop();
std::cout << st.Top() << std::endl;
return 0;
}
三.基类和派生类之间的转换
public继承的派生类对象 可以赋值给基类的指针或基类的引用。这种有个说法叫切片,意思就是把派生类中基类的部分切分出来。指针和引用指向基类那部分。
当然只能把派生类赋给基类指针或引用,可不能把基类赋给派生类的指针或引用。毕竟总不可能从基类里切出一个派生类吧。

cpp
#include <iostream>
#include <string>
class Person
{
public:
std::string _name; // 姓名
std::string _age; // 年龄
std::string _address; // 家庭地址
std::string _sex; // 性别
};
class Student : public Person
{
public:
void study()
{
}
std::string _sid; // 学号
};
int main()
{
Person* p = new Student();//把派生类指针赋给基类指针
Student s1;
Person& p2 = s1;//把派生类赋给基类引用
//反过来就不行
//Student* p = new Person();
//Person s1;
//Student& p2 = s1;
return 0;
}
3.1继承中的作用域
隐藏的规则:
1.基类和派生类都有各种的独立的作用域。
2.假如基类和派生类有同名的成员,那么派生类将屏蔽基类中的同名成员,不能直接访问这种叫隐藏,如果要访问需要加上基类作用域例如 (基类::基类成员)
3.只要基类的成员变量或成员函数名和派生类中同名,无论参数和返回值是否相同都构成隐藏。
cpp
#include <iostream>
#include <string>
class Person
{
public:
std::string study(std::string name)
{
return name + "在学习";
}
std::string _name; // 姓名
std::string _age; // 年龄
std::string _address; // 家庭地址
std::string _sex; // 性别
};
class Student : public Person
{
public:
void study()
{
std::cout << _name << "在学习" << std::endl;
}
std::string _sid; // 学号
};
int main()
{
Student s1;
s1._name = "李四";
std::cout << s1.Person::study("二狗") << std::endl; //基类函数被隐藏了需要加上作用域调用
s1.study();//派生类的函数
return 0;
}
3.2派生类默认成员函数
4个常见默认成员函数
类中一般有6个默认成员函数,默认的意思就是不写但是编译器会为你自动生成一个,那在派生类中这几个成员函数是怎么生成呢
1.派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式调用。
2.派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
3.派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显式调用基类的operator=,需要指定基类作用域
4.派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
5.派生类对象初始化先调用基类构造再调派生类构造。
6.派生类对象析构清理先调用派生类析构再调基类的析构。
7.因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。
cpp
#include <iostream>
#include <string>
class Person
{
public:
Person(std::string name)
{
_name = name;
}
Person(const Person& per)
{
_name = per._name;
}
Person& operator=(const Person& per)
{
if (this != &per)
{
_name = per._name;
}
return *this;
}
~Person()
{
std::cout << "~Person()" << std::endl;
}
std::string _name; // 姓名
};
class Student : public Person
{
public:
Student(std::string sid, std::string name)
:Person(name)//基类没有默认构造函数需要显式调用
,_sid(sid)
{
}
Student(const Student& stu) // 拷贝构造,显式调用基类拷贝构造
:Person(stu)
, _sid(stu._sid)
{
}
Student& operator=(const Student& s)
{
if (this != &s)
{
// 构成隐藏,需要显示调用基类的operator=
Person::operator=(s);
_sid = s._sid;
}
return *this;
}
~Student()
{
std::cout << "~Student()" << std::endl;
}
std::string _sid; // 学号
};
int main()
{
Student s1("11","张三");
Student s2(s1);
Student s3("19","王五");
s1 = s3;
return 0;
}
四.无法继承的类
C++中也有办法让类变得无法继承,有两种实现方法
方法一:把基类构造函数私有化,这样派生类就看不见也无法调用基类的构造函数无法实例化出派生类对象了。
cpp
class Person
{
//把构造函数变私有化派生类就无法实例化出对象了,变相无法继承基类了
private:
Person(std::string name)
{
_name = name;
}
Person(const Person& per)
{
_name = per._name;
}
public:
Person& operator=(const Person& per)
{
if (this != &per)
{
_name = per._name;
}
return *this;
}
~Person()
{
std::cout << "~Person()" << std::endl;
}
std::string _name; // 姓名
};
方法二:C++11中增加了新的关键字final,被final修饰的基类就不能被继承了
cpp
class Person final //final修饰的类不能被继承
{
public:
Person(std::string name)
{
_name = name;
}
Person(const Person& per)
{
_name = per._name;
}
Person& operator=(const Person& per)
{
if (this != &per)
{
_name = per._name;
}
return *this;
}
~Person()
{
std::cout << "~Person()" << std::endl;
}
std::string _name; // 姓名
};
五.继承与友元与静态函数
在C++继承中,友元关系不能被继承,意思是基类中的友元并不能访问派生类的私有和保护成员
cpp
#include <iostream>
#include <string>
class Student;
class Person
{
public:
Person() {}
Person(std::string name)
:_name(name)
{
}
friend void printid(const Person& Per,const Student& stu);
std::string _name; // 姓名
};
class Student : public Person
{
public:
Student(std::string sid)
:_sid(sid)
{
}
private:
std::string _sid; // 学号
};
void printid(const Person& Per, const Student& stu)
{
std::cout << stu._sid << std::endl;
std::cout << Per._name << std::endl;
}
int main()
{
Person p("张三");
Student s("112");
//基类的友元并不能访问子类的私有和保护成员
printid(p, s);//error C2248: "Student::_sid": 无法访问 private 成员(在"Student"类中声明)
return 0;
}
基类中的static静态成员,无论派生类实例化都少次都只有一份static实例
cpp
#include <iostream>
#include <string>
class Student;
class Person
{
public:
static int num;
std::string _name;
};
int Person::num = 50;
class Student : public Person
{
public:
std::string _sid;
};
class Teacher : public Person
{
public:
std::string _tid;
};
int main()
{
Student s1,s2;
std::cout << s1.num << std::endl;
std::cout << s2.num << std::endl;
s1.num = 260;
std::cout << s1.num << std::endl;
std::cout << s2.num << std::endl;
Teacher t1,t2;
std::cout << t1.num << std::endl;
std::cout << t2.num << std::endl;
t2.num = 70;
std::cout << t1.num << std::endl;
std::cout << t2.num << std::endl;
return 0;
}
六.多继承
6.1继承模型
单继承:一个派生类只有一个直接基类时,这种继承关系称为单继承。

多继承:一个派生类有两个或以上直接基类时,这种继承关系称为多继承。多继承对象的内存模型是:按继承声明顺序,先继承的基类子对象在前,后继承的基类子对象在后,派生类自身新增的成员放在最后。

**菱形继承:**菱形继承是多继承的一种特殊的情况。
看下面的模型,可从中看出菱形继承存在着一些继承问题,数据冗余且具有二义性。在Lawfirm类中会同时存在2份Person类成员。支持多继承的语言就会有菱形继承问题,所以在实践中尽量避免设计出菱形继承这样的模型。


cpp
#include <iostream>
#include <string>
class Person
{
public:
std::string _name;
};
class Lawyer : public Person
{
public:
std::string _lid;
};
class Accountant : public Person
{
public:
std::string _aid;
};
class Lawfirm : public Lawyer, public Accountant
{
public:
std::string _lawfirmname;
};
int main()
{
Lawfirm lawfirm;
//lawfirm._name;//编译器直接报错: error C2385: 对"_name"的访问不明确
//必须要显示指定哪个类中的_name,不然就会导致二义性访问不明确
lawfirm.Accountant::_name = "胡佛";
lawfirm.Lawyer::_name = "约翰";
return 0;
}
6.2虚继承
虚继承是解决菱形继承问题的一种机制,通过虚继承让派生类只保留一份基类的成员。
虚继承保证:无论继承路径中出现多少次,最终派生类中只共享一份虚基类子对象
cpp
#include <iostream>
#include <string>
class Person
{
public:
std::string _name;
};
class Lawyer : virtual public Person
{
public:
std::string _lid;
};
class Accountant : virtual public Person
{
public:
std::string _aid;
};
class Lawfirm : public Lawyer, public Accountant
{
public:
std::string _lawfirmname;
};
int main()
{
//通过虚继承保证了共享一份基类成员,解决了数据的二义性和冗余
Lawfirm lawfirm;
lawfirm._name = "张三";
std::cout << lawfirm._name<< std::endl;
return 0;
}
虽然说实践中最好不要用菱形继承,但是C++的IO库中还是用了菱形继承的。

七.继承与组合
public继承是一种is-a的关系,组合是has-a的关系,is-a关系中,派生类对象也是一个基类对象,而has-a是派生类对象中包含一个基类对象,而这两种是核心代码的复用方式。
继承:白箱复用
继承允许派生类基于基类的实现来定义自身。这种复用方式被称为白箱复用(white-box reuse),因为基类的内部细节对派生类是可见的。继承在一定程度上破坏了基类的封装:基类的任何改变都可能影响派生类,导致两者之间依赖关系强、耦合度高。
组合:黑箱复用
对象组合是除继承之外的另一种复用方式。通过组装或组合具有良好定义接口的对象,可以获得更复杂的功能。这种风格称为黑箱复用(black-box reuse),因为对象的内部细节对外不可见,它们只以"黑箱"形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于保持各个类的封装性。
日常实践中,尽量使用has-a这样可以保持各个类的封装性,低耦合和可维护性,但也不是那么绝对,一般看情况类之间适合用is-a就用继承,适合has-a就组合,一般像多态这种就必须使用继承。
cpp
class Person
{
public:
std::string _name;
};
//像 Techer类和Student类 对Person就是一种is-a关系,老师和学生是人类,也是衍生出来的职业,高度依赖Person这个类,
class Techaer : public Person
{
public:
std::string _teach;//教授的学科
};
class Student : public Person
{
public:
std::string _sid;//学号
};
class Tire
{
public:
std::string _size;//轮胎尺寸
};
// 像Car类和Tire就是一种has-a的关系,汽车不等于轮胎,但是汽车里装着轮胎
class Car
{
Tire t1;
Tire t2;
Tire t3;
Tire t4;
};
//Stack这种类和vector既可以是is-a关系也可以是has-a的关系
template <class T>
class Stack : public std::vector<T>
{
};
template <class T>
class Stack
{
std::vector<T> _v;
};