文章目录
一、继承的概念
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。继承是类设计层次的复用。
下面我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/年龄/地址/电话等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有⼀些不同的成员变量和函数,比如老师独有成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。
#include <iostream>
#include <string>
using namespace std;
class Student
{
public :
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
// 学习
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
int _stuid; // 学号
};
class Teacher
{
public :
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
// 授课
void teaching()
{
//...
}
protected:
string _name = "张三"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
string _title; // 职称
};
下面我们把公共的成员都放到person类中,Student和teacher都继承Person,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦。
#include <iostream>
#include <string>
using namespace std;
class person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年龄
string _address; // 地址
string _tel; // 电话
};
class Student :public person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid; // 学号
};
class Teacher :public person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string _title; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
二、继承的定义
1.定义格式
上述示例中我们看到person是基类,也称作父类。Student是派生类,也称作子类。
2.不同继承方式和不同访问方式搭配的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
- 基类private成员在派生类中无论以什么方式继承都是不可见的。 这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问的,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,可以定义为protected(可以看出保护成员限定符是因继承才出现的)。
- 实际上面的表格我们进行⼀下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。 比如:Min(基类的public成员,protected继承)相当于变成派生类的protected成员;Min(基类的protected成员,private继承)相当于变成派生类的private成员等。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
示例一(基类private成员在派生类中无论以什么方式继承都是不可见的):
class person
{
public:
void identity()
{
cout << "void identity()" << _name << endl;
}
private: // 基类private成员
string _name = "peter";
int _age = 18;
};
class Student :public person // 基类private成员在派生类中以public继承都不可访问,
{ // 使用其它两种继承方式,基类private成员肯定仍不能访问
public:
void Print()
{
cout << _name << endl; // 基类private成员(如_name)在派生类中无论以什么方式继承都是不可见的
}
protected:
int _stuid;
};
示例二(如果基类成员不想在类外直接被访问,但需要在派生类中能访问,可以定义为protected):
class person
{
public:
void identity()
{
cout << "void identity()" << _name << endl;
}
protected: // 基类protected成员
string _name = "peter";
int _age = 18;
};
class Student :public person
{
public:
void Print()
{
cout << _name << endl; // 基类protected成员(如_name)在派生类中无论以什么方式继承,
} // 都可以在派生类中直接访问
protected:
int _stuid;
};
int main()
{
Student s;
s._name; // protected成员本来就不能在类外直接访问,继承到派生类中同样不能在类外直接访问
return 0;
}
示例三(使用关键字class时默认的继承方式是private):
class person
{
public: // 基类public成员
string _name = "peter";
int _age = 18;
};
class Student :person // Min(基类的public成员,private继承)相当于变成派生类的private成员,
{ // 派生类对象在类外不能直接访问private成员
public:
void Print()
{
cout << _name << endl;
}
protected:
int _stuid;
};
int main()
{
Student s;
s._name;
return 0;
}
示例四(使用struct时默认的继承方式是public):
class person
{
public:
string _name = "peter";
int _age = 18;
};
struct Student :person // Min(基类的public成员,public继承)相当于变成派生类的public成员,
{ // 派生类对象在类外可以直接访问public成员
public:
void Print()
{
cout << _name << endl;
}
protected:
int _stuid;
};
int main()
{
Student s;
s._name;
return 0;
}
三、继承中的作用域(同名成员构成隐藏)
隐藏规则:
- 在继承体系中基类和派生类都有独立的作用域。
- 如果派生类和继承的基类中有同名成员,派生类成员将屏蔽对继承的基类中同名成员的直接访问,这种情况叫隐藏。(但还可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,被隐藏的函数不能直接访问(要使用 基类::基类成员函数 显示访问)
示例一(如果派生类和继承的基类中有同名成员,派生类成员将屏蔽对继承的基类中同名成员的直接访问):
#include <iostream>
#include <string>
using namespace std;
class Person
{
protected:
string _name = "张三"; // 姓名
int _num = 362301; // ⾝份证号
};
// 派生类Student中的_num和继承的基类Person中的_num构成隐藏关系
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << _num << endl;
cout << " 学号:" << _num << endl;
}
protected:
int _num = 123; // 学号
};
int main()
{
Student s;
s.Print();
return 0;
}
示例二(派生类成员屏蔽的是对继承的基类中同名成员的直接访问,可以通过指定类域 基类::基类成员 的方式显示访问):
class Person
{
protected:
string _name = "张三"; // 姓名
int _num = 362301; // ⾝份证号
};
class Student : public Person
{
public:
void Print()
{
cout << " 姓名:" << _name << endl;
cout << " 身份证号:" << Person::_num << endl;
// 通过指定类域 基类::基类成员 的方式显示访问可以越过派生类同名成员的屏蔽
cout << " 学号:" << _num << endl;
}
protected:
int _num = 123; // 学号
};
int main()
{
Student s;
s.Print();
return 0;
}
示例三(如果派生类和继承的基类中有同名成员函数,基类中同名成员函数被隐藏):
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
// 派生类和继承的基类中有同名成员函数fun,基类中同名成员函数被隐藏
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;
};
示例四(被隐藏的函数不能直接访问,要使用指定类域 基类::基类成员函数 的方式显示访问):
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.A::fun(); // 通过指定类域 基类::基类成员函数 的方式可以访问到被隐藏的基类成员函数
return 0;
};
四、基类和派生类间的转换(从派生类中切割出基类部分)
- public继承的派生类对象 可以赋值给 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。
- 基于以上原理,派生类对象可以赋值给基类对象(详见派生类的默认成员函数),基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但是必须保证基类的指针是指向派生类对象时才是安全的。
示例一(public继承的派生类对象 可以赋值给 基类的指针 / 基类的引用;基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用):
class Person
{
protected :
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public :
int _No;
};
int main()
{
Student s;
// 派⽣类对象可以赋值给基类的指针/引⽤
Person* pp = &s;
Person& rp = s;
// 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,
// 但是必须保证基类的指针是指向派生类对象时才是安全的。
Student* p1 = (Student*)pp;
Student& p2 = (Student&)rp;
return 0;
}
示例二(派生类对象可以赋值给基类对象,基类对象不能赋值给派生类对象):
class Person
{
protected :
string _name;
string _sex;
int _age;
};
class Student : public Person
{
public :
int _No;
};
int main()
{
Student s1;
// 派⽣类对象可以 拷贝和赋值给基类的对象 是通过调⽤基类的拷⻉构造和赋值运算符重载完成的
// (详见派生类的默认成员函数)
Person p1 = s1;
p1 = s1;
// 基类对象不能 拷贝或赋值给派⽣类对象,这⾥会编译报错
Student s2 = p1;
return 0;
}
五、派生类的默认成员函数
默认成员函数,默认的意思就是指我们不写,编译器会帮我们自动生成⼀个,那么在派生类中,这几个成员函数是如何生成的 (把派生类中基类的所有成员变量当成一个整体看待,类似于在派生类中的一个自定义类型成员变量):
- 派生类的构造函数必须调用基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域。
- 派生类对象初始化会先调用基类构造再调派生类构造,所以派生类对象析构清理时会先调用派生类析构再调基类的析构。(后构造的先析构)
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。 因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 因为多态中⼀些场景析构函数需要构成重写,重写的条件之一是函数名相同。那么编译器会对析构函数名进行特殊处理,统一处理成destructor(),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。(详见多态章节的介绍)
示例一(基类有默认构造函数。派生类未显示写构造函数,编译器会默认生成一个构造函数,它会自动调用基类的默认构造函数初始化基类的那⼀部分成员):
class Person
{
public :
// 全缺省构造函数属于默认构造函数
Person(const char* name = "peter")
: _name(name)
{
}
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
int main()
{
Student s1;
return 0;
}
示例二(基类没有默认构造函数。则必须在派生类构造函数的初始化列表阶段显示调用基类的构造函数):
class Person
{
public :
Person(const char* name)
: _name(name)
{
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public :
Student(const char* name, int num)
: Person(name) // 在派生类构造函数的初始化列表阶段显示调用基类的构造函数
,_num(num)
{
}
protected:
int _num; //学号
};
int main()
{
Student s1("jack", 18);
return 0;
}
示例三(派生类未显示写拷贝构造函数,编译器会默认生成一个拷贝构造函数,它会自动调用基类的拷贝构造函数初始化基类的那⼀部分成员):
class Person
{
public :
Person(const char* name)
: _name(name)
{
}
Person(const Person& p)
: _name(p._name)
{
}
protected:
string _name; // 姓名
};
class Student : public Person
{
public :
Student(const char* name, int num)
: Person(name)
,_num(num)
{
}
protected:
int _num; //学号
};
int main()
{
Student s1("jack", 18);
Student s2(s1);
return 0;
}
示例四(示例三中派生类默认生成的拷贝构造函数就足够使用。我们在示例四中显示写派生类的拷贝构造函数,只是为了模拟默认生成的拷贝构造函数实现拷贝的过程):
class Person
{
public :
Person(const char* name)
: _name(name)
{
}
Person(const Person& p) // 基类引用可以接收派生类中切出来的基类那部分
: _name(p._name)
{
}
protected:
string _name;
};
class Student : public Person
{
public :
Student(const char* name, int num)
: Person(name)
,_num(num)
{
}
Student(const Student& s)
: Person(s) // public继承的派生类对象 可以赋值给 基类的引用。
, _num(s._num) // 会把派生类中基类那部分切出来,基类引用指向的是派生类中切出来的基类那部分
{
}
protected:
int _num;
};
int main()
{
Student s1("jack", 18);
Student s2(s1);
return 0;
}
示例五(派生类未显示写赋值运算符重载函数,编译器会生成一个默认赋值运算符重载函数,它会自动调用基类的赋值重载函数给基类的那⼀部分成员赋值):
class Person
{
public :
Person(const char* name)
: _name(name)
{
}
Person& operator=(const Person& p)
{
if (this != &p)
_name = p._name;
return *this;
}
protected:
string _name;
};
class Student : public Person
{
public :
Student(const char* name = " ", int num = 0)
: Person(name)
,_num(num)
{
}
protected:
int _num;
};
int main()
{
Student s1("jack", 18);
Student s2;
s2 = s1;
return 0;
}
示例六(示例五中派生类默认生成的赋值运算符重载函数就足够使用。我们在示例六中显示写派生类的赋值重载函数,只是为了模拟默认生成的赋值重载函数实现赋值的过程):
class Person
{
public :
Person(const char* name)
: _name(name)
{
}
Person& operator=(const Person& p)
{
if (this != &p)
_name = p._name;
return *this;
}
protected:
string _name;
};
class Student : public Person
{
public :
Student(const char* name = " ", int num = 0)
: Person(name)
,_num(num)
{
}
Student& operator = (const Student& s)
{
if (this != &s)
{
// 派生类和基类的赋值重载函数同名,基类的赋值重载函数被隐藏,所以需要指定类域显⽰调⽤
Person::operator =(s);
_num = s._num;
}
return* this;
}
protected:
int _num;
};
int main()
{
Student s1("jack", 18);
Student s2;
s2 = s1;
return 0;
}
示例七(派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员):
class Person
{
public :
Person(const char* name)
: _name(name)
{
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public :
Student(const char* name = " ", int num = 0)
: Person(name)
,_num(num)
{
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _num;
};
int main()
{
Student s1("jack", 18);
return 0;
}
六、实现一个不能被继承的类(final关键字)
方法1:将基类的构造函数设为private。派生类对象的构造必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。(变相使得这个基类不能被继承)
方法2:C++11新增了一个final关键字,派生类不能继承被final修饰的基类。
示例一(C++11 之前常通过方法一实现一个不能被继承的类,但方法一存在一些弊端,它不够直接,派生类继承了构造函数为private的基类时不会报错,当用这个派生类实例化对象时才会报错;将基类的构造函数设为private,会导致基类也不能实例化对象):
class Base
{
protected:
int a = 1;
private:
// 将基类的构造函数设为private
Base()
{
}
};
class Derive :public Base // 派生类继承了构造函数为private的基类时不会报错,
{ // 当用这个派生类实例化对象时才会报错
protected:
int b = 2;
};
int main()
{
Base b; // 构造函数为private的类不能实例化对象
Derive d;
return 0;
}
示例二(C++11新增了一个final关键字,派生类不能继承被final修饰的基类,使用起来非常直观,解决了方法一的种种弊端):
class Base final
{
protected:
int a = 1;
};
class Derive :public Base // 派生类不能继承被final修饰的基类
{
protected:
int b = 2;
};
int main()
{
Base b;
return 0;
}
七、继承类模板
基类是类模板时,在派生类中调用基类中成员函数需要指定⼀下类域
#include <iostream>
#include <vector>
using namespace std;
template<class T>
class mystack : public std::vector<T>
{
public:
void push(const T& x)
{
// 基类是类模板时,在派生类中调用基类中成员函数需要指定⼀下类域,
// 否则编译报错:error C3861: “push_back”: 找不到标识符
// mystack<int>实例化时,也实例化vector<int>了,但是模版是按需实例化,
// vector<int>中的push_back等成员函数未实例化,所以找不到
vector<T>::push_back(x);
}
void pop()
{
vector<T>::pop_back();
}
const T& top()
{
return vector<T>::back();
}
};
int main()
{
mystack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}
八、继承与友元
友元关系不能继承,也就是说基类友元不能访问派生类私有和保护成员 。
class Student; // 声明Student类,因为Person类的友元声明中用到了Student类,
class Person // 不在它之前声明Student类会导致友元声明找不到Student类而报错
{
friend void Display(const Person& p, const Student& s); // 基类友元声明
protected:
string _name = "jack";
};
class Student : public Person
{
protected
int _stuNum = 1;
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
// 编译报错: “Student::_stuNum”: ⽆法访问 protected 成员
Display(p, s);
return 0;
}
解决方案:因为派生类不能继承基类的友元关系,所以手动添加Display为Student 的友元即可
class Student; // 声明Student类,因为友元声明中用到了Student类,不在它之前声明会报错
class Person
{
friend void Display(const Person& p, const Student& s);
protected:
string _name = "jack";
};
class Student : public Person
{
friend void Display(const Person& p, const Student& s);
// Display也变成了 Student 的友元
protected:
int _stuNum = 1;
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}
九、继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有⼀个这样的成员。无论派生出多少个派生类,都只有⼀个static成员实例。
class Person
{
public :
string _name;
static int _count;
};
int Person::_count = 10;
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;
// 静态成员是public的情况下,基类和派⽣类指定类域都可以访问静态成员
cout << Person::_count << endl;
cout << Student::_count << endl;
// 当基类突破类域修改静态成员的数值,基类和派生类里的静态成员数值都被改变了
// 说明派⽣类和基类共用同⼀份静态成员
Person::_count = 15;
cout << Person::_count << endl;
cout << Student::_count << endl;
return 0;
}
十、多继承及其菱形继承问题
1.单继承和多继承
单继承:一个派生类只有一个直接基类时称这个继承关系为单继承
多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承
2.菱形继承问题
菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。(多继承虽然用起来很方便,但带来便利的同时也是会带来一些代价的,支持多继承就一定会有菱形继承问题,实践中应尽量避免设计出菱形继承这样的模型)
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()
{
Assistant a;
// 编译报错:对“_name”的访问不明确
// _name在派生类Assistant继承的两个基类中各存了一份,编译器不知道该访问哪一个
a._name = "jack";
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()
{
Assistant a;
// 显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
a.Student::_name = "jack";
a.Teacher::_name = "rose";
return 0;
}
3.虚继承(解决菱形继承问题)
虚继承是通过virtual关键字进行使用的。使用方法是在会导致数据冗余和二义性的基类继承时添加virtual关键字。 使用效果如下:
class Person
{
public :
string _name;
};
class Student : virtual public Person // Person类会导致后续数据冗余和二义性问题,
{ // 在继承Person类时添加virtual关键字
protected :
int _num;
};
class Teacher : virtual public Person
{
protected :
int _id;
};
class Assistant : public Student, public Teacher
{
protected :
string _majorCourse;
};
int main()
{
Assistant a;
// 直接访问 “_name” 都不会报错,证明二义性问题被解决
a._name = "jack";
return 0;
}
本来派生类Assistant继承的两个基类中会各自存一份Person类的成员变量,但使用了virtual关键字之后,Person类的成员变量会从Assistant继承的两个基类中独立出来,不会在两个基类中各存一份,而是自己自成一派,单独存一份。这样就从根本上避免了菱形继承所导致的数据冗余和二义性问题。
4.多继承中指针偏移问题
下面说法正确的是( )
A:p1 == p2 == p3
B:p1 < p2 < p3
C:p1 == p3 != p2
D:p1 != p2 != p3
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;
}
答案是C.
分析:p1和p2虽然都是其父类,但在子类内存模型中,其位置不同,所以p1和p2所指子类的位置也不相同,因此p1!=p2,
由于p1对象是第一个被继承的父类类型,所有其地址与子类对象的地址p3所指位置都为子类对象的起始位置,因此p1==p3