在C++初阶我们学习了面向对象的经典三大特性(封装,继承,多态)之一的封装,今天我们便走进继承,感受其中的奥妙
定义:
继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展(类层次的复用),增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类/子类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤
简单来说,原有的类是父类,继承产生的类是子类,儿子站在父亲的肩膀上——先用别人(父类)做好的基础,再在上面添加自己的东西
下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣的独有成员函数是学习,⽼师的独有成员函数是授课
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; // 职称
};在学生类和老师类中的身份认证,姓名等是相同的,那么可以把这些公共的成员均放到一个语义上更大的人类中(Person类中),学生和老师都属于人类,我们让student和Person均继承person,这样我们就不需要重复定义了,减少冗余
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; // 职称
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}这里便就体现出继承了,我们在原有的Person上进行复用与扩展得到了子类——student和teacher类,在student类中并没有identity这个类,但是学生和老师均可以使用,这便是复用

定义格式: 下⾯我们看到Person是基类,也称作
⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以既叫基类/派⽣类,也叫⽗类/⼦类)



继承基类成员访问⽅式的变化
在父类中用public/protected/private修饰的成员通过不同方式的继承方式会在子类中变成不同的成员,如下9种情况:

这里我们需要注意,在父类中的private成员在子类中存在(但是不可见),但是子类不可以直接使用,这里我们把age私有化
class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" << _name << endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
private:
int _age = 18; // 年龄
};
class Student : public Person
{
public:
// 学习
void study()
{
// ...
cout << _age << endl;
}
protected:
int _stuid; // 学号
};经过调试发现,_age在学生类中存在,但是我们访问使用便会报错

类里面都无法访问age,更别提类外面了

虽然私有的不可以直接使用,但是我们可以间接使用,我们父类中调用该成员即可,如在Person类的identity函数中调用age,该函数是共有的,那么子类学生类便可以使用该函数,因此相当于子类间接访问了age
class Person
{
public:
// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
void identity()
{
cout << "void identity()" << _name << endl;
cout << _age << endl;
}
protected:
string _name = "张三"; // 姓名
string _address; // 地址
string _tel; // 电话
private:
int _age = 18; // 年龄
};
int main()
{
Student s;
Teacher t;
s.identity();
t.identity();
return 0;
}
在前面我们学习实现栈使用的是适配器模式,在这里我们可以使用继承,通过栈来继承vector,如下:
namespace hxx
{
//template<class T>
//class vector
//{};
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定⼀下类域,
// 否则编译报错:error C3861: “push_back”: 找不到标识符
// 因为stack<int>实例化时,也实例化vector<int>了(stack继承vector)
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到 (在stack和vector<?>中均找不到则报错,)
//因此需要指定其类域,具体可见gitee源码详解
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();
}
};
}
int main()
{
hxx::stack<int> st;
st.push(1);
st.push(2);
st.push(3);
while (!st.empty())
{
cout << st.top() << " ";
st.pop();
}
return 0;
}基类对象不能赋值给派⽣类对象
基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time TypeInformation)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个我们后⾯类型转换章节再单独专⻔讲解,这⾥先提⼀下)

下面我们通过示例来感受下:
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;
}
因此可以得出基类对象不能赋值给派生类,这里为派生类对象可以赋值给基类的对象是通过调用后面介绍的基类拷贝构造实现的(这里没有发生类型转换,即没有产生临时变量)
class Person
{
protected:
string _name = "小李子";//姓名
int _num = 111;//身份证
};
class Student :public Person
{
public:
void Print()
{
cout << _num << endl;
cout
}
protected:
//在子类中便有了两个叫num的变量,父类的继承下来了,
int _num = 999;
};
int main()
{
Student s1;
s1.Print();
return 0;
}在main函数中调用了Print函数,那么打印的是父类num的111,还是子类的999?我们得出是999

那么想访问父类的则如下:

下面我们来看一道有趣的题目: A和B的两个func构成了:重载 / 隐藏 / 没关系的哪一个
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);
return 0;
};乍眼一看,函数名相同,参数不同,如果你秒选构成了重载,那很遗憾的和你说,答案是隐藏,为什么? 因为函数重载要求在同一个作用域中,而继承的父类和子类是两个不同的类域。在该小点中提到继承中成员函数只需函数名相同便是隐藏
这里我们还需要注意一点,在main函数中调用b.func(),是会报错的,因为调用fun是先在B(子类)搜索,在子类的fun参数不匹配,但其不会在父类中去查找,因为隐藏了父类,因此如果想调用必须指定作用域

在前面类和对象中我们讲到了6个默认成员函数,默认的意思就是指我们不写,编译器会帮我们自动生成一个,那么在子类中,这几个成员函数是如何生成的?
1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤
下面我们依旧通过student类和Person类感受下默认生成的构造函数的行为,其分为3部分: 1.内置类型不确定 2.自定义类型调用其的默认构造 3.继承父类成员看做一个整体对象,要求调用父类的默认构造
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:
protected:
int num;//学号
string _addrss;
};
int main()
{
Student s;
return 0;
}
那么在Person类中没有默认构造,因此我们需要在子类构造函数的初始化列表阶段显⽰调⽤,如下

2. 子类的拷⻉构造函数必须调⽤父类的拷⻉构造完成父类的拷⻉初始化
这里的拷贝构造和第一点构造函数一样,也是三种情况: 1.内置类型是值拷贝 2.自定义类型调用其的拷贝构造函数 3.父类会调用其父类的拷贝构造
在上面的Person和Student类中,严格来说,Student生成的拷贝构造够用了,但是如果在该类中有需要深拷贝的资源,才需自己实现拷贝构造
因此我们便可得出,子类的构造需要自己写,拷贝构造/赋值/析构一般都不需要自己写(三者是一体化的),那么如果需要手动写拷贝构造,就需要解决如何拷贝父类的一部分,把父类当成一个整体的对象,会显示调用父类的拷贝构造,但是在Student类中没有Person类的对象,我们就可以通过前面介绍的父类与子类的兼容转换来解决
Student(const Student& s)
:_num(s._num)
,_addrss(s._addrss)
,Person(s)
{
//深拷贝
}派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域(这里和拷贝构造类似)
那么如果需要我们显示写赋值重载该如何实现?这里我们需要注意的是如果显示调用父类的operator=,因为子类的隐藏了父类的operator
//赋值重载
Student& operator=(const Student& s)
{
if (this != &s)
{
//父类和子类的operator构成隐藏关系
Person::operator=(s);
_num = s._num;
_addrss = s._addrss;
}
return *this;
}同理,这里的析构也和上面的拷贝/赋值重载类似,严格来说子类生成的析构就够用了,如果有需要显示示范法的资源,才需要自己实现
//析构
~Student()
{
~Person();
}
我们发现很奇怪:为什么调不动父类的析构?这里就要提到多态的知识了:
因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
简单来说就是编译器将父类和子类的析构函数名都处理为destructor(),即构成了隐藏关系,因此还需显示指定作用域
//析构
~Student()
{
//子类的析构和父类的析构函数构成隐藏关系
Person:: ~Person();
}
int main()
{
Student s1("张三",1,"南京市");
Student s2(s1);
Student s3("李四", 2, "成都");
s1 = s3;
return 0;
}
咦?这里我们发现值创建了三个对象,结果调用了6次析构,这样会导致delete多次调用析构函数,会出问题,因此在这里析构不需要显示调用
//析构
~Student()
{
//子类的析构和父类的析构函数构成隐藏关系
//规定:不需要显示调用,子类析构函数之后,会自动调用父类析构
//这样就可以保证析构的顺序,先子后父
//Person:: ~Person();
}
对象构造时先是构造父类,后是子类,析构时,后定义的需要先析构,这也便是我们不显示写析构的原因,保证析构是先子后父,显示调用则不能保证是先子后父,如下:
~Student()
{
Person::Person();
delete _ptr;
}坚持到这里,已经很棒啦,希望读完本文可以帮读者大大更好了父类与子类之间的关系!!!如果喜欢本文的可以给博主点点免费的攒攒,你们的支持就是我前进的动力🎆
资源分享:继承源码