前言:
本章我们将学习C++
三大特性之一的继承。同时继承作为C++
另一大特性——多态的重要基石,非常值得我们深入学习~
// 基类(父类)
class Person
{
public:
void test()
{
cout << "Person" << endl;
}
protected:
string _name; // 名字
};
// 派生类(子类)
class Student : public Person
{
private:
int _stuid; // 学号
};
Student
类继承了Person
类Person
类称作基类或父类Student
类称作派生类或者子类public
为一种继承方式当子类继承父类之后,父类Person
的成员(成员函数+成员变量)都会变成子类的一部分。例如,当我们创建好一个子类对象,并查看对象的成员:
在学习类时,我们曾经认识了 3 个访问限定符:
public
—— 公有访问protected
—— 保护访问private
—— 私有访问在继承中,这三个关键字同样可以表示 3 种继承方式:
public
—— 公有继承protected
—— 保护继承private
—— 私有继承虽然这两组概念中,这 3 个关键字都是相同的,但是所表达的意义却不同。继承方式与访问限定符(指基类中)共同决定了子类中成员的访问权限的上限。我们可以用一张表来展示继承方式与访问限定符的不同组合:
基类成员/继承方式 | public 继承 |
protected 继承 |
private 继承 |
---|---|---|---|
基类的public 成员 |
派生类的public 成员 |
派生类的protected 成员 |
派生类的private 成员 |
基类的protected 成员 |
派生类的protected 成员 |
派生类的protected 成员 |
派生类的private 成员 |
基类的private 成员 |
在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
一些重要结论:
private
成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。private
成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected
。可以看出保护成员限定符是因继承才出现的。 ==Min
(成员在基类的访问限定符,继承方式),public > protected > private
。class
时默认的继承方式是private
,使用struct
时默认的继承方式是public
,不过最好显示的写出继承方式。public
继承,几乎很少使用protetced/private
继承,也不提倡使用protetced/private
继承,因为protetced/private
继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。在之前的学习中,我们知道一个类型的对象赋值给另一个类型相似的对象时,会发生隐式类型转换并生成一个中间临时变量。例如:
double d = 1.1;
int i = d; // 隐式类型转换
在继承中,子类对象也可以赋值给一个父类对象,但并不会发生类型转换。有如下注意事项:
class Person
{
public:
void test()
{
cout << "Person" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
void test()
{}
private:
int _stuid;
};
int main()
{
Student s;
// 1.子类对象可以赋值给父类对象/指针/引用
Person p = s;
Person* p_str = &s;
Person& p_ref = s;
return 0;
}
//2.基类对象不能赋值给派生类对象
s = p; ❌
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
p_str = &s;
Student* s_ptr1 = (Student*)p_str; // 正确
p_str = &p;
Student* s_ptr1 = (Student*)p_str; // 有越界访问的危险
在继承体系中基类和派生类都有独立的作用域。我们需要注意以下事项:
class Person
{
public:
void print()
{
cout << "Person" << endl;
}
protected:
string _name = "person";
};
class Student : public Person
{
public:
void test()
{
cout << _name << endl;
}
private:
string _name = "peter";
int _stuid;
};
int main()
{
Student s;
s.test();
return 0;
}
class Person
{
public:
void print()
{
cout << "Person" << endl;
}
protected:
string _name = "person";
};
class Student : public Person
{
public:
void test()
{
cout << Person::_name << endl;
}
private:
string _name = "peter";
int _stuid;
};
int main()
{
Student s;
s.test();
return 0;
}
class Person
{
public:
void print(int)
{
cout << "In Person" << endl;
}
protected:
string _name = "person";
};
class Student : public Person
{
public:
int print( double)
{
cout << "In Student" << endl;
return 0;
}
private:
string _name = "peter";
int _stuid;
};
int main()
{
Student s;
s.print(1);
return 0;
}
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
class Person
{
public:
Person(const string& name)
:_name(name)
{
cout << "Person(const string& name)" << endl;
}
/*Person(const Person& p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}*/
protected:
string _name;
};
class Student : public Person
{
public:
Student(const string name,int id)
:Person(name) // 显示调用构造函数
, _stuid(id)
{
cout << "Student(const string name,int id)" << endl;
}
/*Student(const Student& s)
:Person(s)
, _stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}*/
private:
int _stuid;
};
int main()
{
Student s("peter",12345);
return 0;
}
class Person
{
public:
// ...省略之前代码
Person(const Person& p)
:_name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
// ...省略之前代码
Student(const Student& s)
:Person(s)
, _stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}
private:
int _stuid;
};
int main()
{
Student s1("peter",12345);
Student s2(s1);
return 0;
}
operator=
必须要调用基类的operator=
完成基类的复制class Person
{
public:
// ...省略之前代码
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
protected:
string _name;
};
class Student : public Person
{
public:
// ...省略之前代码
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s); // 调用基类的赋值重载
_stuid = s._stuid;
}
cout << "Student& operator=(const Student& s)" << endl;
return *this;
}
private:
int _stuid;
};
int main()
{
Student s1("peter",12345);
Student s2("xxxx",0);
s2= s1;
return 0;
}
class Person
{
public:
// ...省略之前代码
protected:
string _name;
};
class Student : public Person
{
public:
// ...省略之前代码
~Student()
{
cout << "~Student()" << endl;
}
private:
int _stuid;
};
int main()
{
Student s1("peter",12345);
return 0;
}
static
静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static
成员实例// 统计一个创建了多少个子类对象
class Person
{
public:
static int count;
protected:
string _name;
};
int Person::count = 0;
class Student : public Person
{
public:
Student()
{
count++;
}
private:
int _stuid;
};
int main()
{
Student s1;
Student s2;
Student s3;
Student s4;
Student s5;
cout << Person::count << endl;
return 0;
}
一个类可以被多个类继承(有多个儿子),同样的,一个类也可以继承多个类(有多个父亲)。
class Person
{};
class Student : public Person
{};
class Teacher
{};
class Student
{};
class Assistant:public Student,public Teacher
{};
class Person
{};
class Teacher :public Person
{};
class Student :public Person
{};
class Assistant:public Student,public Teacher
{};
一下是一种菱形继承
class Person
{
public:
string _name;
};
class Teacher :public Person
{
protected:
int _id; // 职工编号
};
class Student :public Person
{
protected:
int _num; //学号
};
class Assistant :public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
当我们定义了一个Assistant
类型的对象,该对象的成员中包含两个_name
,分别是从Student
和Teacher
所继承。
int main()
{
Assistant a;
return 0;
}
当我们想要访问_name
成员时,就会出现二义性,编译器并不知道我们要访问哪一个_name
成员。
cout << a._name << endl;
当然,我们可以通过指定类域来访问:
int main()
{
Assistant a;
cout << a.Student::_name << endl;
cout << a.Teacher::_name << endl;
return 0;
}
数据冗余是指,当我们创建一个对象时,它的某个属性(某个成员)只有一个值即可。但是内存中却实实在在的存储了两份数据,其中有一份数据必然是多余的。就如同,现实中一个人可能在不同的环境中有不同的称呼,但是,身份证上只有一个名字就够了。
产生二义性和数据冗余的本质就是子类继承了多份相同成员。
解决菱形继承问题的方法——虚拟继承。在继承方式前面加上关键字virtual
,如下:
class Person
{
public:
string _name;
};
class Teacher :virtual public Person
{
protected:
int _id; // 职工编号
};
class Student :virtual public Person
{
protected:
int _num; //学号
};
class Assistant :public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
int main()
{
Assistant a;
a._name = "peter";
return 0;
}
注意此时监视窗口虽然依旧看到存在两个_name
,但其实在内存中只存在一份数据,监视窗口是编译器修饰过的,为了方便我们观察
为了便于观察,我们再来看一组菱形继承的例子:
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
打开内存窗口,输入对象d
的地址,我们可以粗略的看到对象模型:
注意,此时_a
的数据有两份,一份属于B
类,一份属于C
类。再来看看加了虚拟继承之后的效果
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
打开内存窗口,输入对象d
的地址,我们可以粗略的看到对象模型:
通过观察我们可以发现虚拟继承与非虚拟继承的几个不同点:
_a
只会保留一份,占用一份内存空间B
和C
中好像各自多了一个指针一样的数字其实,B
和C
中存放的奇怪数字就是两个指针,我们叫它们——虚基表指针。这两个指针分别指向两张表,称之为——虚基表。
我们继续通过内存窗口观察一下这两个表中分别存了什么东西吧。
如图所示,两张虚基表中分别存了两个数字——20
,12
。那么这两个数字有何含义呢?它们其实是偏移量——是_a
的地址相对于B
和C
起始地址的偏移量。通过这个偏移量就可以找到_a
的地址,以此解决二义性与数据冗余的问题。
下图是上面的Person
关系菱形虚拟继承的原理解释:
C+
+的缺陷之一,很多后来的OO语言都没有多继承,如Java
public
继承是一种is-a
的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种has-a
的关系。假设B
组合了A
,每个B
对象中都有一个A
对象。
优先使用对象组合,而不是类继承 。
继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。
实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
本文到此结束,码文不易,还请多多支持哦!!!