前言:本篇博客基于《C++ prime》第十五章面向对象程序设计进行总结,以此方便查阅,该章主要介绍了C++继承与多态部分,有兴趣可以阅读。
OOP概述
面向对象程序设计的核心思想是封装、继承和多态。
- 封装可以将类的接口与实现分离。
- 继承可以定义相似的类型并对其相似关系建模。
- 多态可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象。
继承
通过继承联系在一起的类构成一种层次关系。通常在层次关系的根部有一个基类,其他类型则直接或间接从基类继承而来,这些继承得到的类称为派生类。
【注意】基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。

以人为基类,基类中拥有的特性是姓名、性别、年龄、国籍。设计两个派生类,以学生为人的派生类,派生类中拥有的特性是学号、专业、年级;以教师为人的派生类,派生类中拥有的特性是工号、教授科目。


派生类列表
派生类必须通过使用派生类列表明确指出派生类是从哪个基类继承而来的。
- 派生类列表的形式是:首先是一个冒号,后面紧跟以逗号分隔的基类列表,其中每个基类前面可以由访问说明符。

每个基类前面可以有三种访问说明符中的一个:public、protected或者private
访问控制与继承
派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员。正常来说,派生类与其他外部的代码(其他类)一样,能够访问基类的public成员,但是不能访问基类的private成员。但是存在一种特殊情况,基类仅仅希望派生类能够访问部分成员,而其他外部的代码(其他类)不能访问这部分成员。

在C++中,使用受保护(protected)访问运算符说明这样的成员。
【注意】在没有使用继承关系时,外部代码(其他类)可以访问public成员,而不能访问private成员和protected成员。使用了继承关系(一般来说继承方式为public)后,外部代码(其他类)只能访问基类的public成员,派生类可以访问基类的public成员和protected成员,对于基类的private成员,仅仅只能基类自己使用。
受保护的成员
每一个类控制着其成员对于派生类来说是否可以访问。基类一般会使用protected访问运算符来声明希望与派生类分享但是不想被其他公共访问使用的成员。protected可以看作是public和private中和后的产物:
- 和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
- 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。、
- 【protected重要性质】派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权,也就是说,派生类不能通过基类对象访问基类的受保护成员。
理解第三条性质:
class A
{
protected:
int _num;
};
class B : public A
{
public:
// 错误使用方式
friend void func1(A& a)
{
a._num = 1;
}
// 正确使用方式
friend void func2(B& b)
{
b._num = 1;
}
};
如果派生类可以访问基类受保护的成员,那么这里就可以通过设置派生类修改原本外部无法访问基类的成员修改基类的受保护成员。
修改_num的两种方式
(1)由A类内或者A类中的友元修改
(2)B(public或者protected)继承A的类中,使用派生类对象方式修改【注意】这里换一种思路,如果可以通过B访问A中受保护的成员,那么需要修改A类的受保护成员时,只需要设置一个新类即可,那么这个类的封装特性就会生效。为了防止这种方式,C++做出如下规定:即派生类的成员和友元只能访问派生类对象中基类的受保护成员,对于普通的基类对象中的受保护成员不能访问。
继承关系——公有、私有和受保护继承

| 类成员/继承方式 | public继承 | protected继承 | private继承 |
|---|---|---|---|
| 基类的public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
| 基类的private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想再类外直接访问,但需要再派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 通过表格的总结可以发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private .
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式时public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protected/private继承,也不提倡使protected/private继承,因为protected/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
某个类对其继承而来的访问权限受到两个因素影响:
- 一是基类中该成员的访问说明符
- 二是在派生类的派生列表中的访问说明符
派生访问说明符的目的是控制派生类用户对于基类成员的访问权限:
class Base
{
public:
void func1()
{
cout << "Base : public" << endl;
}
int num1;
protected:
void func2()
{
cout << "Base : protected" << endl;
}
int num2;
private:
void func3()
{
cout << "Base : private" << endl;
}
int num3;
};
class A : public Base
{
// 正确
int f1()
{
return num1;
}
// 正确
int f2()
{
return num2;
}
// 错误:private成员对于派生类来说是不可访问的
int f3()
{
return num3;
}
};
class B : protected Base
{
// 正确
int f1()
{
return num1;
}
// 正确
int f2()
{
return num2;
}
// 错误:private成员对于派生类来说是不可访问的
int f3()
{
return num3;
}
};
class C : private Base
{
// 正确:在派生类私有访问
int f1()
{
return num1;
}
// 正确:在派生类私有访问
int f2()
{
return num2;
}
// 错误:private成员对于派生类来说是不可访问的
int f3()
{
return num3;
}
};
只有继承是公有继承,基类中是公有的,才可以通过派生类访问。
class Base
{
public:
void func1()
{
cout << "Base : public" << endl;
}
protected:
void func2()
{
cout << "Base : protected" << endl;
}
private:
void func3()
{
cout << "Base : private" << endl;
}
};
class A : public Base {};
class B : protected Base {};
class C : private Base {};
int main()
{
A a;
a.func1(); // 正确:公有继承-公有成员
a.func2(); // 错误:公有继承-受保护成员
a.func3(); // 错误:公有继承-私有成员
B b;
b.func1(); // 错误:受保护继承-公有成员
b.func2(); // 错误:受保护继承-受保护成员
b.func3(); // 错误:受保护继承-私有成员
C c;
c.func1(); // 错误:私有继承-公有成员
c.func2(); // 错误:私有继承-受保护成员
c.func3(); // 错误:私有继承-私有成员
}
派生访问说明符还可以控制继承自派生类的新类的访问权限
【注意】当基类成员转换到不同访问限定符指定的派生类中,在派生类中的权限会改变,此时再使用继承自派生类的新类时,需要分析派生类的权限。
class Base
{
public:
void func1()
{
cout << "Base : public" << endl;
}
protected:
void func2()
{
cout << "Base : protected" << endl;
}
private:
void func3()
{
cout << "Base : private" << endl;
}
};
// A::fun1公有-fun2受保护-fun3无法访问
class A : public Base {};
// B::fun1受保护-fun2受保护-fun3无法访问
class B : protected Base {};
// C::fun1私有-fun2私有-fun3无法访问
class C : private Base {};
// A_public:: fun1公有-fun2受保护-fun3无法访问
class A_public : public A {};
// B_protected:: fun1受保护-fun2受保护-fun3无法访问
class B_protected : public B {};
// C_private:: fun1无法访问-fun3无法访问-fun3无法访问
class C_private : public C {};
类的设计与受保护成员
不考虑继承,我们可以认为一个类有两种不同的用户:普通用户和类的实现者。这其中,普通用户编写的代码使用类的对象,这部分代码只能访问类的公有(接口)成员;实现者则复制编写类的成员和友元的代码,成员和友元技能访问类的公有部分,也能访问类的私有(实现)部分。
如果进一步考虑继承的话就会出现第三种用户,即派生类。基类把它希望派生类能够使用的部分声明成受保护的。普通用户不能访问受保护的成员,而派生类及派生类的友元不能访问基类私有成员。
和其他类一样,基类应该将其接口成员声明为公有的;同时将属于其实现部分分成两组:
- 一组可供派生类访问,应该声明成受保护的,这样派生类就能实现自己的功能时使用基类的这些操作和数据
- 另一组只能由基类和基类的友元访问,应该声明成私有的。
默认的继承保护级别
struct和class 关键字定义的类具有不同的默认访问说明符。
类似的,默认派生类运算符也由定义派生类所用的关键字来决定。默认情况下,使用class关键字定义的派生类是私有继承;而使用struct关键字定义的派生类是公有继承。
class Base {};
struct A : Base {}; // 公有继承
class B : Base {}; // 私有继承
【注意】一般建议派生类继承基类的时候,显式地将派生访问说明符定义出来。
派生类的声明
派生类的声明与其他类的差别不大,声明中包含类名但是不包含其派生列表:
class A : class Base; // 错误:派生列表不能出现在这里
class A; // 正确:声明派生类的正确方式与正常类相同
【注意】一条声明语句的目的是令程序执行某个名字的存在以及该名字表示一个什么样的实体。例如:一个类声明、一个函数或者一个变量等等。派生列表以及与定义有关的其他细节必须与类的主题一起出现。
被用作基类的类
如果将某个类用作基类,则该类必须已经定义而非声明:
// 错误:Base必须定义,而非声明
class Base;
class A : public Base{};
原因是:派生类中包含并且可以使用它从基类继承而来的成员,为了使用这些成员,派生类需要知道这些成员是什么。
【隐含意思】一个类不能派生它本身。
直接继承与间接继承
一个类是基类,同时也可以是派生类:
class Base {};
class A : public Base {}; // Base是A的直接基类
class B : public A {}; // A是B的直接基类, Base是B的间接基类
在这个继承关系中,Base是A的直接基类,同时也是B的间接基类。直接基类出现在派生列表中,而间接基类由派生类通过其直接基类继承而来。
每一个类都会继承直接基类的所有可访问成员。
改变个别成员的可访问性——using
通过使用using声明可以改变派生类继承的某个名字的访问级别。
class Base
{
public:
int get_n()
{
return n;
}
protected:
int n;
};
// 保持对象尺寸相关的成员的访问级别
class A : private Base
{
public:
using Base::get_n;
protected:
using Base::n;
};
派生类使用私有继承的方式继承的基类在派生类都会变成私有成员。在派生类中,可以使用using声明语句改变这些成员的可访问性质,将该类中直接或者间接基类中的任何可访问成员(非私有成员)标记出来。
using声明语句中名字的访问权限由该using声明语句之前的访问说明符来决定。
- 当using声明语句出现在类的private部分,则该名字只能被类的成员和友元访问;
- 如果using声明语句出现在类的public部分,则类的所有用户都能访问它;
- 如果using声明语句位于protected部分,则该名字对于成员、友元和派生类是可访问二点。
【注意】派生类只能为其可访问的成员提供using声明
防止继承的发生——final
一个类不希望其他类所继承,C++11新标准中提供了一种防止继承发生的方法,即在类名后跟一个关键字final:
class Base final {};
class A : public Base {}; // 错误:Base不能被继承
派生类的构造函数
虽然派生类对象中含有从基类继承而来的成员,但是派生类并不能直接初始化这些成员。和其他创建了基类对象的代码一样,派生类也必须使用基类的构造函数来初始化其基类部分。
每个类控制它自己的成员初始化过程。
派生类对象的基类部分与派生类对象自己的数据成员都是在构造函数的初始化阶段执行初始化操作的。派生类构造函数同样是通过构成函数初始化列表来将实参传递给基类构造函数的。
class Base
{
public:
Base(int base1, int base2)
:_base1(base1)
,_base2(base2)
{}
private:
int _base1;
int _base2;
};
class A : public Base
{
public:
A(int a1, int a2, int base1, int base2)
:Base(base1, base2)
, _a1(a1)
, _a2(a2)
{
}
private:
int _a1;
int _a2;
};
这里首先会初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。
继承与静态成员
如果基类定义了一个静态成员,那么在整个继承体系中只存在该成员的唯一定义。不管是从基类派生出多少个派生类,对于每个静态成员来说都只存在唯一的实例。
- 静态成员遵循通用的访问控制权限,如果基类中的静态成员是private的,则派生类无权访问;如果基类的静态成员是可访问的,那么这个静态成员属于基类与派生类,即基类可以使用它,派生类也可以使用它
class Base
{
public:
static int get_base1();
private:
static int _base1;
};
int Base::_base1 = 1;
int Base::get_base1()
{
return _base1;
}
class A : public Base
{
// 正确
int fun1()
{
return get_base1();
}
// 错误:无权访问
int fun2()
{
return _base1;
}
};
友元类与继承
友元类的核心规则是:
- 友元关系是单向的:A 声明 B 为友元,B 可以访问 A ,但是 A 不能访问 B 。
- 友元关系不能传递:A 声明 B 为友元,B 声明 C 为友元,但是 C 不能访问 A。
- 友元关系不能继承:B 是 A 的友元,C 继承 B,但是 C 不能访问 A 的受保护成员和私有成员。
【注意】每个类负责控自己的成员的访问权限。
class Base
{
friend class FB;
int num1;
};
class A : public Base
{
int _a;
};
class FB
{
// 正确:FB是Base的友元
int func1(Base b)
{
return b.num1;
}
// 正确:对基类的访问权限由基类本身控制,及时对于派生类的基类部分也是如此
// FB虽然不是A的友元,但是可以通过A访问Base的成员
int func2(A a)
{
return a.num1;
}
// 错误:FB不是A的友元,不能访问A内的成员
int func3(A a)
{
return a._a;
}
};
类型转换与继承
派生类对象及派生类向基类的类型转换
一个派生类对象包含多个组成部分:
- 一个含有派生类自己定义的(非静态)成员的子对象
- 一个与该派生类继承的基类对应的子对象,如果有多个基类,那么这样的子对象也有多个。
class Base
{
int num1;
};
class A : public Base
{
int _a;
};
C++中标准中并没有明确规定派生类的对象在内存中如何分布,但是可以认为Base对象和A对象是包含这些成员的。

【注意】在一个对象中,继承自基类的成员和派生类自定义的成员不一定是连续存储的。
因为派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或者引用绑定到派生类对象的基类部分上。
Base b1; // 基类对象
A a1; // 派生类对象
Base* p = &b1; // p指向基类Base对象
p = &a1; // p指向派生类A的Base部分
Base& r = a1; // r绑定到派生类A的base部分
【说明】这种转换通常称为派生类到基类的类型转换。和其他类型转换一样,编译器会隐式地指向派生类到基类地转换。所以,这种隐式特性意味着可以把派生类对象或者派生类对象的引用或者指针用在需要基类引用的地方。
在派生类对象中含有其基类对应二点组成部分,这是继承的关键。
继承中的类作用域
每个类定义自己的定义域,在这个定义域内我们定义类的成员。当存在继承关系的时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内找不到合适的成员,则编译器会继续在外层的基类作用域中寻找该名字的定义。

同时也是因为存在类作用域这种继承嵌套的关系,所以派生类才能像使用自己的成员一样使用基类的成员。
继承的名字冲突——隐藏
派生类可以重新定义在其直接基类或者间接基类中的名字,此时定义在内层作用域(即派生类)的名字将隐藏定义在外层作用域(即基类)的名字。
class Base
{
public:
Base(int num = 1)
:_num(num)
{}
protected:
int _num;
};
class A : public Base
{
public:
A(int num = 2)
:_num(num)
{}
int get_num()
{
return _num;
}
protected:
int _num;
};

派生类的成员将会隐藏同名的基类成员
通过作用域运算符来使用隐藏的成员
通过作用域运算符可以使用一个被隐藏的基类成员
class Base
{
public:
Base(int num = 1)
:_num(num)
{}
protected:
int _num;
};
class A : public Base
{
public:
A(int num = 2)
:_num(num)
{}
int get_num()
{
// 使用类作用域运算符
return Base::_num;
}
protected:
int _num;
};

作用域运算符会覆盖原有的查找规则,并指示编译器从Base类的作用开始查找num。
在继承中,除了覆盖继承而来的虚函数(多态),派生类最好不要与基类定义重名成员。
持续更新...

被折叠的 条评论
为什么被折叠?



