目录
1.继承的概念
继承机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有
类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。
当我们有两以下两个类:
#include<iostream>
#include<string>
using namespace std;
class Student
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 学习
void study()
{
// ...
}
protected:
string _name = "peter"; // 姓名
string _address;
// 地址
string _tel;
// 电话
int _age = 18;
// 年龄
int _stuid;
// 学号
};
class Teacher
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
// ...
}
// 授课
void teaching()
{
//...
}
protected:
string _name = "张三";
// 姓名
int _age = 18;
// 年龄
string _address;
// 地址
string _tel;
// 电话
string _title;
// 职称
};
我们很容易发现:二者都有公共部分:name、address、age、tel、identity这些,我们如果这样设计很麻烦,那么就需要用到继承了。
我们可以把这些公共成员函数和公共成员变量放到一个类里面,然后再通过继承写成这样的形式(现在看不懂之后会逐步讲解的!):
//用继承写的类
//父类/基类
class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三"; // 姓名
string _address;
// 地址
string _tel;
// 电话
int _age = 18;
// 年龄
};
//派生类/子类
class Student : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid;
// 学号
};
class Teacher : public Person
{
public:
// 授课
void teaching()
{
//...
}
protected:
string title;
// 职称
};
2.继承的定义
继承举例就是:如果家里有矿,就可以直接继承家业,这是不劳而获的一个方式,假设家里原来是父亲管理,现在到你这里就是不需要你自己努力了,也就是说原来的那些东西就不用自己写了。所以我们把被继承的类称为:父类/基类,而继承下来的类就称为派生类/子类。
如上代码:class Student : public Person 其中Person就是父类,Student就是子类。
2.1定义格式
派生类在定义的时候的格式是:class关键字+派生类的名字+:+继承方式(private/protected/public)+基类名字。
有些人可能就发现了,类定义的时候也有public/protected/private三个访问限定符,那么这些访问限定符与继承方式有什么关联呢?
2.2 继承基类成员访问方式的变化
其中不可见的意思就是在派生类中无法访问基类的该类型成员。通过该图,也就能说明:基类的private成员在类外面和子类都无法访问,也就是说受到访问限定符的限制。
那三种继承方式又有什么区别呢?
通过表格我们很容易发现:如果是private继承,不管在基类里面是public成员还是protected成员,都是派生类的private成员,也就是说:假设基类有一个成员变量a是public成员(从访问限定符public到下一个其他类型的访问限定符或到类的结尾就称为public成员),有一个成员变量b是protected成员,那么在被private继承后,在派生类里面就自动变成了private成员了。
对于public继承,那么基类的public成员还是派生类的public成员,也能从派生类来访问基类的public成员;基类的protected成员还是派生类的protected成员。
所以推出了以下的结论:
(1)基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。
//父类/基类
class Person
{
private:
int _age;
};
//子类/派生类
class Student : public Person
{
//……
};
其中的_age在任何方式(public/protected/private)的继承在Student都是不可见的!
(2)基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符(protected)是因继承才出现的。
(3)实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员(private)在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式),public > protected >private(访问权限对比)。
(3)中最后一句话的意思是成员在基类中作为什么类型的成员与继承方式中取权限小的作为派生类的什么类型成员。如:前者public、后者private,在派生类就是private;前者public、后者protected,在派生类就是protected。
(4)使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显式的写出继承⽅式。
这句话的意思就是如果我们定义类开始没有写访问限定符,那么从类定义开始位置到第一个访问限定符或者到类定义结束的时候对于用class定义类的成员默认就是private成员,如果用struct定义类的成员默认就是public成员。但是建议自己写访问限定符。
(5)在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤
protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实
际中扩展维护性不强。
开始的表格也不要死记硬背,用得多了自然就会了!
class Person
{
public:
void Print()
{
cout << _name << endl;
}
protected:
string _name; // 姓名
private:
int _age;
// 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected:
int _stunum; // 学号
};
通过所学,Person是基类/父类,Student是派生类/子类,如果用public继承,派生类中public成员:Print函数,protected成员:_name成员变量、_stunum成员变量,private成员没有;如果用protected继承,派生类中public成员没有,protected成员:_name成员变量、Print函数、_stunum成员变量,private成员没有;如果用private继承,派生类中public成员没有,protected成员:_stunum成员变量;private成员:_name成员变量、Print函数。
派生类可以访问和修改基类的protected成员。且派生类还可以继续向下继承protected成员。
2.3继承类模板
在我们学习栈的时候,我们可以用vector来实现,我们之前的写法是这样的:
//原写法
template<class T,class Container = vector<T>>
class MyStack
{
public:
void push(const T& x)
{
_con.push_back(x);
}
void pop()
{
_con.pop_back();
}
const T& top()
{
return _con.back();
}
bool empty()
{
return _con.empty();
}
private:
Container _con;
};
原写法我们是直接默认用vector作为容器来完成栈的各种操作的。这种方式称之为组合,组合这个方法在一些STL容器中用得也比较多,组合的更多知识会在继承2进行讲解!
其中由于Stack和vector的成员都差不多,所以我们可以用public继承得到另外一种写法:
template<class T>
class Stack :public vector<T>
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定⼀下类域,
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();
}
};
我们主要记住一个点:如果父类是模板,我们就需要指定类域,否则会找不到。
原因:如果Stack<int>实例化时,只有当已经实例化为vector<int>了,才不会有问题。但是模板是按需实例化,也就是说push_back在vector没有实例化时,push_back等成员函数是不会实例化的,因此找不到(push_back(const int x)并没有找到这个函数)。而我们vector<T>::push_back(x)则会一起实例化了。
3.基类和派生类间的转换
(1)public继承的派⽣类对象 可以赋值给 基类的指针 / 基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
//基类和派生类间的转换
class Person
{
protected:
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student s;
//指针
Person* ptr = &s;
//引用
Person& ref = s;
return 0;
}
其中Person* ptr=&s和Person& ref=s这些都是没有任何问题的,实际上,ptr和ref都是只拿到了那个基类在派生类的那一部分,即把Person的所有protected成员拿出来了而已。
如果我们这样写:
class Person
{
public:
string _name = "张三"; // 姓名
string _sex = "男"; // 性别
int _age = 1; // 年龄
};
class Student : public Person
{
public:
int _No; // 学号
};
int main()
{
Student s;
//指针
Person* ptr = &s;
//引用
Person& ref = s;
cout << ref._name << ' ' << ref._sex << ' ' << ref._age << ' ' << ref._No << endl;
return 0;
}
那么就会报错:
我们如果只这样写:
cout << ref._name << ' ' << ref._sex << ' ' << ref._age << endl;
那么结果就是:
所以这就叫基类和派生类间的转换,简称:赋值兼容转换。
(2)基类对象不能赋值给派⽣类对象。
我们如果这样写:
int main()
{
Person p;
Student* ptr = &p;
Student& ref = p;
}
那么就会出现:
(3)基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time TypeInformation)的dynamic_cast 来进⾏识别后进⾏安全转换。(这个现在不会讲解)
我们可以写成这样,这样是没问题的:
int main()
{
Student so;
Person pobj = so;
return 0;
}
这个涉及到后面要讲的拷贝构造,所以等到讲到那里再说。
开始的那个代码指向:
切割/切片:
4.继承中的作用域
4.1隐藏规则
(1)在继承体系中基类和派⽣类都有独⽴的作⽤域。
(2) 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显式访问)
如:假设我们在Person中定义了一个成员变量_id,又在其派生类Student中也定义了一个成员变量_id,那么这样Person的_id就会构成隐藏,但是我们还是可以访问,只是优先访问变成了派生类自己定义的而已了,我们如果要访问就需要指定作用域如:Person::_id。因此我们如果在Student中直接:_id = 1;那么就是Student::_id=1,如果我们想要改变Person的_id那么就要指定作用域:Person::_id = 2;
(3) 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
(4)注意在实际中在继承体系⾥⾯最好不要定义同名的成员。
4.2小试牛刀
观察下面代码,回答两个问题:
//小试牛刀
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构成什么关系()
A. 重载 B. 隐藏 C.没关系
问题2:下⾯程序的编译运⾏结果是什么()
A. 编译报错 B. 运⾏报错 C. 正常运⾏
首先由规则(3)可以知道:A和B类一定是构成隐藏的,但是如果没有看那个规则,我觉得可能会选出B,因为它们两个是差了一个参数,并不是完全相同的。但是规定是只要函数名相同就构成隐藏,所以第一题是选A。
第二题如果在第一题选错的基础上,那么就会选C。但是由于你了解的规则(3)所以就会选A和B 的一个,运行报错是运行时出现了问题,比如:变量未定义、类型错误(“1”+2)、索引错误(数组越界)、键错误(在pair或map或set这些容器中用find函数找没存在的键或者值)、除零错误(如:1/0),运行结果没达到预期(可能if判断出错,可能算法逻辑出错)、内存不足(忘记释放内存)、输入数据不合法等等。而编译错误是指:语法错误(如C++中for要有循环条件、“;”、“{}”导致没有运行就出错),拼写出错(printf写成prinf),缺少符号(多循环+多判断条件下很容易忘记加{}或者()或者C/C++程序少;),类型不匹配(int x="123"或调用函数时传的参数与形参类型不匹配),使用了未定义的变量或函数或类,作用域的问题。
编译错误一般在编译器上没运行的时候就能检查出来,上述代码就是:编译错误。所以选B。因为在B 中找不到对应的函数b.fun()这个函数是不存在的,所以会:
这个题目难度不是很高,但是主要是要理解:什么是运行错误,什么是编译错误,还有以后面试可能遇到的什么数据传到内存的哪个空间上这些问题,一定不要忽略这些问题!
5.继承与友元
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员 。也就是说:父类的友元不是派生类的友元。如果想要派生类也和某个类或函数成为友元就需要再次把这个函数作为派生类的友元。
//继承与友元
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name = "张三"; // 姓名
};
class Student : public Person
{
protected:
int _stuNum = 711; // 学号
};
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;
}
这样运行结果为:
一定要注意的一点是:并不是把Display作为Student的友元就没有问题了,因为就算这样还有问题:
主要的原因是在Person定义的时候,Student并没有定义,导致Display没有找到Student,所以要再加一个Student的声明:
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name = "张三"; // 姓名
};
class Student : public Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum = 711; // 学号
};
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;
}
6.继承与静态成员
基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个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;
cout << &p.count << ' ' << &s.count << endl;
return 0;
}
则运行结果为:
这两个类都是一个count,所以不管是哪个派生类修改了count都会导致基类的count被修改!
能打印出来还是可以证明:count这个静态成员变量被继承下来了,但是不是重新生成了一个count,主要是静态成员变量本来就没放到类对象里面,所以只有一个count,不过我们这种访问静态成员变量的方式并不正确,正确的访问成员变量的方式是:
cout << Person::count << ' ' << Student::count << endl;
7.总结
这一讲的继承难度不是很高,但是继承还有额外的知识这一讲不能完全讲完,下一讲将讲解:继承的四个成员函数、多继承与菱形继承问题、继承与组合这些难度可能大一些。
好了,这一讲就到这里,喜欢的可以一键三连哦,下讲再见!