C++ 面向对象之继承
一.继承的概念及定义
1.继承的概念
面向对象这个概念,就是将现实中的各种物体及关系抽象而成的重要方法,继承可以看作是现实生活中的一种亲缘关系,因继承而产生的类叫做子类(或派生类),被继承的类则形象地称为父类。在现实中有很多继承地例子,例如人——学生,人——教师,车——卡车等等这样地关系都可称为继承
2.继承的定义
要表示继承关系,只需要在子类的类名后加冒号和父类即可。如下:
#include<iostream>
#incldue<string>
using namespace std;
class Person {
public:void func1() { cout << "Person::func1" << endl; }protected:string _name;int _age;string _gender;
};class Student : public Person
{
public:void func2() { cout << "Student::func2" << endl; }
protected:string _stuNum;int _score;string _major;
};
可以看到继承父类Person还加了访问限定符public,其实继承的方式有很多种,而public继承最常用。
1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它。
2. 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式),public > protected >private。
4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式。
5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤
protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实
际中扩展维护性不强。
3.类模板继承
类模板继承的规则和形式与继承普通类相同,但是他的基类是一个类模板。例如这里用vector实现stack(同样可以用空间适配器实现stack)
namespace wjh
{
//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>了
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
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()
{
wjh::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 来进⾏识别后进⾏安全转换。
using namespace std;
class Person {
public:void func1() { cout << "Person::func1" << endl; }protected:string _name;int _age;string _gender;
};class Student : public Person
{
public:void func2() { cout << "Student::func2" << endl; }
protected:string _stuNum;int _score;string _major;
};int main()
{Student st1=new Student();Person *p=&st1;
}
三.继承中的作用域
在派生类和基类的体系中都存在各自的作用域,这会导致一些问题:
. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
(在派⽣类成员函数中,可以使⽤ 基类::基类成员显⽰访问)。需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,在实际中在继承体系⾥⾯最好不要定义同名的成员。
#include<iostream>
#include<string>
using namespace std;
class Person
{protected :string _name = "⼩李⼦"; // 姓名int _num = 111; // ⾝份证号
};
class Student : public Person
{public :void Print(){cout << " 姓名:" << _name << endl;cout << " ⾝份证号:" << Person::_num << endl;cout << " 学号:" << _num << endl;}
protected:int _num = 999; // 学号
};
int main()
{Student s1;s1.Print();return 0;
}
运行结果:
若要访问基类中的同名变量则加上访问限定符
#include<iostream>
#include<string>
using namespace std;
class Person
{protected :string _name = "⼩李⼦"; // 姓名int _num = 111; // ⾝份证号
};
class Student : public Person
{public :void Print(){cout << " 姓名:" << _name << endl;cout << " ⾝份证号:" << Person::_num << endl;cout << " 学号:" << Person::_num << endl;}
protected:int _num = 999; // 学号
};
int main()
{Student s1;s1.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;
}
答案是编译报错。b.fun(10)正常编译,b.fun()将基类同名函数隐藏
四.派生类中的默认成员函数
在入门类与对象时我们详细探讨了类的各种默认成员函数的行为,而继承关系我们需要进一步对其进行探讨。
1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构
函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。这很好理解,因为派生类是在继承了基类的情况下产生的,从上面的切片也可以看出,在派生类初始化时必然也需要初始化基类的那一部分。
2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
3. 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域
4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。这一顺序可以看作是构造函数的对称行为。
5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。
6. 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。
7. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加
virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。
#include<iostream>
#include<string>
using namespace std;
class Person {
protected:string name;int age;
public:Person(string name, int age) {this->name = name;this->age = age;cout << "调用Person构造函数" << endl;}Person(const Person& p) :name(p.name){cout << "调用Person拷贝构造函数" << endl;}Person& operator=(const Person& p) {if (this != &p) {this->name = p.name;}return *this;}~Person() {cout << "Person析构函数" << endl;}void show() {cout << "姓名:" << name << ",年龄:" << age << endl;}
};class Student :public Person {
protected:int score;
public:Student(string name, int age, int score) :Person(name, age) {this->score = score;cout << "调用Student构造函数" << endl;}Student(const Student& s) :Person(s) {cout << "调用Student拷贝构造函数" << endl;}Student& operator=(const Student& s) {if (this != &s) {Person::operator=(s);this->score = s.score;}return *this;}~Student() {cout << "Student析构函数" << endl;}};int main() { Person p("张三", 20);cout << endl;Student s("李四", 19, 99);
}
结果如下:
五.无法被继承的类
我们可以人为构造一些无法被继承的类,有两种方法。
1.基类构造函数私有化
从之前的讲解我们知道,如果一个派生类进行构造时,必然要调用基类的构造函数用于初始化基类的那一部分参数,而对基类的构造函数私有化,使得派生类无法访问其构造函数,因此从形式上使得这种基类无法被继承。
2.使用final关键字
#include<iostream>
#include<string>
using namespace std;
class Person final{
protected:string name;int age;
public:Person(string name, int age) {this->name = name;this->age = age;cout << "调用Person构造函数" << endl;}Person(const Person& p) :name(p.name){cout << "调用Person拷贝构造函数" << endl;}Person& operator=(const Person& p) {if (this != &p) {this->name = p.name;}return *this;}~Person() {cout << "Person析构函数" << endl;}void show() {cout << "姓名:" << name << ",年龄:" << age << endl;}
};class Student :public Person {
protected:int score;
public:Student(string name, int age, int score) :Person(name, age) {this->score = score;cout << "调用Student构造函数" << endl;}Student(const Student& s) :Person(s) {cout << "调用Student拷贝构造函数" << endl;}Student& operator=(const Student& s) {if (this != &s) {Person::operator=(s);this->score = s.score;}return *this;}~Student() {cout << "Student析构函数" << endl;}};int main() { Student s("李四", 19, 99);
}
使用final关键字会让试图继承该基类的类无法通过编译
六.继承与友元
值得一提的是,基类的友元关系并不会继承到派生类中。从现实中讲,你父母的盆友当然不一定是你的盆友。当然也有一些更为复杂的情况,这里暂时不作过多解释
#include <iostream>
#include <string>
using namespace std;// 基类
class Base {
private:int privateVar;
protected:int protectedVar;
public:Base() : privateVar(1), protectedVar(2) {}// 声明友元函数friend void friendFunction(Base& b);// 声明友元类friend class FriendClass;
};// 友元函数定义
void friendFunction(Base& b) {cout << "友元函数访问Base的私有成员: " << b.privateVar << endl;cout << "友元函数访问Base的保护成员: " << b.protectedVar << endl;
}// 友元类定义
class FriendClass {
public:void accessBase(Base& b) {cout << "友元类访问Base的私有成员: " << b.privateVar << endl;cout << "友元类访问Base的保护成员: " << b.protectedVar << endl;}
};// 派生类
class Derived : public Base {
public:void accessBaseMembers() {// 可以访问保护成员cout << "派生类访问Base的保护成员: " << protectedVar << endl;// 不能访问私有成员// cout << "派生类访问Base的私有成员: " << privateVar << endl; // 错误}// 尝试访问基类友元 - 这会失败,因为友元关系不能继承void tryAccessFriend(Base& b) {// 不能访问基类的私有成员,即使通过派生类// cout << "派生类尝试访问Base的私有成员: " << b.privateVar << endl; // 错误}
};// 测试友元关系不能继承
class AnotherClass {
public:void tryAccess(Base& b) {// 不能访问Base的私有和保护成员// cout << "其他类尝试访问Base的私有成员: " << b.privateVar << endl; // 错误// cout << "其他类尝试访问Base的保护成员: " << b.protectedVar << endl; // 错误}
};int main() {Base b;Derived d;FriendClass fc;AnotherClass ac;cout << "=== 测试友元函数 ===" << endl;friendFunction(b);cout << "\n=== 测试友元类 ===" << endl;fc.accessBase(b);cout << "\n=== 测试派生类访问 ===" << endl;d.accessBaseMembers();cout << "\n=== 测试友元关系不能继承 ===" << endl;// ac.tryAccess(b); // 这行会导致编译错误return 0;
}
测试结果:
=== 测试友元函数 ===
友元函数访问Base的私有成员: 1
友元函数访问Base的保护成员: 2=== 测试友元类 ===
友元类访问Base的私有成员: 1
友元类访问Base的保护成员: 2=== 测试派生类访问 ===
派生类访问Base的保护成员: 2
七.继承与静态成员
如果基类定义了一个静态成员,无论它有多少个派生类,所有的继承关系中只有这唯一一个静态成员
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;
}
八.多继承与菱形继承问题
不同于Java等编程语言,C++中是允许多继承的情况存在的
1.继承模型
单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以看出菱形继承有数据冗余和⼆义性的问题,在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()
{
// 编译报错:error C2385: 对“_name”的访问不明确
Assistant a;
a._name = "peter";
// 需要显⽰指定访问哪个基类的成员可以解决⼆义性问题,但是数据冗余问题⽆法解决
a.Student::_name = "xxx";
a.Teacher::_name = "yyy";
return 0;
}
2.虚继承
虚继承是解决菱形继承的一种机制,它可以确保在菱形继承结构中,最高级的基类只被继承一次,即在派生类中只有一份最高级基类的成员。
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;
}
注意事项:
性能开销:虚继承可能会带来一些性能开销,因为需要通过额外的指针来访问虚基类的成员。
初始化顺序:虚基类的初始化由最派生类负责,这可能会改变预期的初始化顺序。
设计复杂性:使用虚继承会增加代码的复杂性,应当只在确实需要解决菱形继承问题时使用。
构造函数调用:虚基类的构造函数总是先于非虚基类的构造函数被调用
可以看出多继承本身就会带来很多性能和语义上的问题,实践中尽量避免实现多继承。
I/O库中的虚拟继承
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};
一个关于多继承中指针偏移的练习
多继承中指针偏移问题?下⾯说法正确的是(C )
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;
}
画一个图可以清晰地解决这个问题
上面提到,一个派生类初始化时需要调用基类地构造函数初始化基类的那一部分参数,那么作为两个基类的Base1,Base2就可以如图表示与Derive的关系。至于这两个基类谁先谁后,这个看派生类的继承顺序。Derive中包含了Base1,Base2的成员和独属于自己的成员,题目的操作类似上面提到的切片操作,自然p1和p3指向同一个位置,而p2指向Base2的首地址。
九.继承与组合
继承与组合是C++中两种重要的代码复用机制,他们的作用特点各有千秋
继承(Inheritance)
继承是一种"is-a"关系,表示派生类是基类的一种特殊形式。例如这里的人——学生
#include<iostream>
#include<string>
using namespace std;
class Person {
protected:string name;int age;
public:Person(string name, int age) {this->name = name;this->age = age;cout << "调用Person构造函数" << endl;}Person(const Person& p) :name(p.name){cout << "调用Person拷贝构造函数" << endl;}Person& operator=(const Person& p) {if (this != &p) {this->name = p.name;}return *this;}~Person() {cout << "Person析构函数" << endl;}void show() {cout << "姓名:" << name << ",年龄:" << age << endl;}
};class Student :public Person {
protected:int score;
public:Student(string name, int age, int score) :Person(name, age) {this->score = score;cout << "调用Student构造函数" << endl;}Student(const Student& s) :Person(s) {cout << "调用Student拷贝构造函数" << endl;}Student& operator=(const Student& s) {if (this != &s) {Person::operator=(s);this->score = s.score;}return *this;}~Student() {cout << "Student析构函数" << endl;}};
组合(Composition)
组合是一种"has-a"关系,表示一个类包含另一个类的对象作为其成员。例如轮胎与车
// Tire(轮胎)和Car(⻋)更符合has-a的关系
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺⼨
};
class Car {
protected:
string _colour = "⽩⾊"; // 颜⾊
string _num = "陕ABIT00"; // ⻋牌号
Tire _t1; // 轮胎
Tire _t2; // 轮胎
Tire _t3; // 轮胎
Tire _t4; // 轮胎
}
而stack这类空间配置器,可以看作既符合is-a,也符合has-a的模型(由于其空间配置器)
template<class T>
class vector
{};
// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public vector<T>
{};
template<class T>
class stack
{
public:
vector<T> _v;
};
int main()
{
return 0;
}
总结
在实际应用中,应优先使用组合方式设计代码,降低代码耦合度。而继承也是多态的一种重要形式,实战应该结合这两点的优点设计出强大稳定易扩展的代码。