第九章:类和对象
带默认值的构造函数参数
注意:默认构造函数是调用时可不提供参数的构造函数,而并不一定是不接受任何参数的构造函数。
因此,下面的构造函数虽然有两个参数,但它们都有默认值,因此也是默认构造函数:
#include <iostream>
using namespace std;
class Human {
private:
string name;
int age;
public:
//一个包含了两个默认参数值的构造函数
Human(string humanName = "Adam", int humanAge = 25) {
name = humanName;
age = humanAge;
cout << "重载的构造函数产生了" << endl;
cout << name << " 的年龄是 " << age << endl;
}
};
int main()
{
//实例化Human对象时,仍然可以不提供任何参数
Human adam;
return 0;
}
运行效果如下:
何时及如何使用析构函数
使用 char* 缓冲区时,您必须自己管理内存分配和释放,因此本书建议不要使用它们,而使用 std::string。std::string 等工具都是类,它们充分利用了构造函数和析构函数,让您无需考虑分配和释放等内存管理工作。下面的程序中的类 MyString 在构造函数中为一个字符串分配内存,并在析构函数中释放它:
一个简单的类,它封装了字符缓冲区并通过析构函数释放:
#include <iostream>
#include <string.h>
//错误 C4996 是一个编译器警告,表示使用了一个被认为是不安全的函数。
//在这个例子中,警告是关于 strcpy 函数的,它用于复制字符串。
//而我们确认是正确的,那么就可以使用下面这行代码关闭该警告
#pragma warning(disable:4996)
using namespace std;
class MyString {
private:
char* buffer;
public:
//构造函数
MyString(const char* initString) {
//如果不是空串,那么就申请内存,+1是为了存储\0
if (initString != NULL) {
buffer = new char[strlen(initString) + 1];
strcpy(buffer, initString);
}
else {
buffer = NULL;
}
}
//析构函数
~MyString() {
cout << "析构函数,执行清理操作" << endl;
//buffer 不为空,说明有内容,那么就执行释放内存的操作
if (buffer != NULL)
delete[] buffer;
}
//获得缓冲区长度
int GetLength() {
return strlen(buffer);
}
//得到字符串缓冲区的内容
const char* GetString() {
return buffer;
}
};
int main()
{
MyString sayHello("Hello from String Class");
cout << "String buffer in sayHello is " << sayHello.GetLength();
cout << "characters long " << endl;
cout << "Buffer contains : " << sayHello.GetString() << endl;
return 0;
}
运行效果如下:
注意: 析构函数不能重载,每个类都只能有一个析构函数。如果您忘记了实现析构函数,编译器将创建一个伪(dummy)析构函数并调用它。伪析构函数为空,即不释放动态分配的内存。
复制构造函数
假设有一个函数如下:
double Area(double radius);
在调用 Area() 时,实参会被复制给形参 radius ,这种规则也使用于对象(类的实例)。
浅拷贝及其存在的问题
上面一节中的 MyString 类包含一个指针成员 buffer,它指向动态分配的内存(这些内存是
在构造函数中使用 new 分配的,并在析构函数中使用 delete[]进行释放)。复制这个类的对象时,将复
制其指针成员,但不复制指针指向的缓冲区,其结果是两个对象指向同一块动态分配的内存。销毁其
中一个对象时,delete[]释放这个内存块,导致另一个对象存储的指针拷贝无效。这种复制被称为浅复
制,会威胁程序的稳定性,如程序清单 9.8 所示:
程序清单 9.8 按值传递类(如 MyString)的对象带来的问题:
#include <iostream>
#include <string.h>
//错误 C4996 是一个编译器警告,表示使用了一个被认为是不安全的函数。
//在这个例子中,警告是关于 strcpy 函数的,它用于复制字符串。
//而我们确认是正确的,那么就可以使用下面这行代码关闭该警告
#pragma warning(disable:4996)
using namespace std;
class MyString {
private:
char* buffer;
public:
//构造函数
MyString(const char* initString) {
buffer = NULL;
if (initString != NULL) {
buffer = new char[strlen(initString) + 1];
strcpy(buffer, initString);
}
}
//析构函数
~MyString() {
cout << "析构函数,执行清理操作" << endl;
//buffer 不为空,说明有内容,那么就执行释放内存的操作
if (buffer != NULL)
delete[] buffer;
}
//获得缓冲区长度
int GetLength() {
return strlen(buffer);
}
//得到字符串缓冲区的内容
const char* GetString() {
return buffer;
}
};
void UseMyString(MyString str) {
cout << "String buffer is MyString is " << str.GetLength();
cout << "characters long " << endl;
cout << "buffer contains : " << str.GetString() << endl;
return;
}
int main()
{
MyString sayHello("Hello from String Class");
//函数调用时,实参sayHello会被复制给形参str,造成浅复制问题
UseMyString(sayHello);
return 0;
}
运行结果如下,直接出现堆栈错误了:
在 main( )中将工作交给这个函数的结果是,对象 sayHello 被复制到形参 str,并在 UseMyString( )中使用它。编译器之所以进行复制,是因为函数 UseMyString( )的参数 str被声明为按值(而不是按引用)传递。对于整型、字符和原始指针等 POD 数据,编译器执行二进制复制,因此 sayHello.buffer 包含的指针值被复制到 str 中,即 sayHello.buffer 和 str.buffer 指向同一个内存单元,如图 9.3 所示:
二进制复制不复制指向的内存单元,这导致两个 MyString 对象指向同一个内存单元。函数UseMyString( )返回时,变量 str 不再在作用域内,因此被销毁。为此,将调用 MyString 类的析构函数,而该析构函数使用 delete[]释放分配给 buffer 的内存(如程序清单 9.8 的第 22 行所示)。这将导致 main( )中的对象 sayHello 指向的内存无效,而等 main( )执行完毕时,sayHello 将不再在作用域内,进而被销毁。但这次第 22 行对不再有效的内存地址调用 delete(销毁 str 时释放了该内存,导致它无效)。正是这种重复调用 delete 导致了程序崩溃。
使用复制构造函数确保深拷贝
复制构造函数是一个重载的构造函数,由编写类的程序员提供。每当对象被复制时,编译器都将
调用复制构造函数。
为 MyString 类声明复制构造函数的语法如下:
class MyString{
//拷贝构造函数
MyString(const MyString& copySource);
};
//类外实现
MyString::MyString(const MyString& copySource){
//拷贝复制的实现代码
}
复制构造函数接受一个以引用方式传入的当前类的对象作为参数。这个参数是源对象的别名,您
使用它来编写自定义的复制代码,确保对所有缓冲区进行深复制,如程序清单 9.9 所示。
程序清单 9.9 定义一个复制构造函数,确保对动态分配的缓冲区进行深复制:
#include <iostream>
#include <string.h>
//错误 C4996 是一个编译器警告,表示使用了一个被认为是不安全的函数。
//在这个例子中,警告是关于 strcpy 函数的,它用于复制字符串。
//而我们确认是正确的,那么就可以使用下面这行代码关闭该警告
#pragma warning(disable:4996)
using namespace std;
class MyString {
private:
char* buffer;
public:
//构造函数
MyString(const char* initString) {
buffer = NULL;
cout << "默认构造函数:创建新的 MyString" << endl;
if (initString != NULL) {
buffer = new char[strlen(initString) + 1];
strcpy(buffer, initString);
cout << "buffer points to: 0x" << hex;
cout << (unsigned int*)buffer << endl;
}
}
//拷贝构造函数
MyString(const MyString& copySource) {
buffer = NULL;
cout << "拷贝构造函数:拷贝来自 MyString" << endl;
//不为空才执行拷贝
if (copySource.buffer != NULL) {
//分配新空间
buffer = new char[strlen(copySource.buffer) + 1];
//深拷贝,拷贝指向空间而不是单纯的值
strcpy(buffer, copySource.buffer);
cout << "buffer points to: 0x" << hex;
cout << (unsigned int*)buffer << endl;
}
}
//析构函数
~MyString() {
cout << "析构函数,执行清理操作" << endl;
//buffer 不为空,说明有内容,那么就执行释放内存的操作
if (buffer != NULL)
delete[] buffer;
}
//获得缓冲区长度
int GetLength() {
return strlen(buffer);
}
//得到字符串缓冲区的内容
const char* GetString() {
return buffer;
}
};
void UseMyString(MyString str) {
cout << "String buffer is MyString is " << str.GetLength();
cout << "characters long " << endl;
cout << "buffer contains : " << str.GetString() << endl;
return;
}
int main()
{
MyString sayHello("Hello from String Class");
//函数调用时,实参sayHello会被复制给形参str,造成浅复制问题
UseMyString(sayHello);
return 0;
}
运行结果如下,此时不再报错:
创建 sayHello 导致了第 1 行输出,这是由 MyString 的构造函数的第 12 行生成的。出于方便考虑,这个构造函数还显示了 buffer 指向的内存地址。接下来,main( )将 sayHello 按值传递个函数 UseMyString( ),如第 66 行所示,这将自动调用复制构造函数,输出指出了这一点。复制构造函数的代码与构造函数很像,基本思想也相同:检查 copySource.buffer 包含的 C 风格字符串的长度(第 30 行),分配相应数量的内存并将返回的指针赋给 buffer,再使用 strcpy 将 copySource.buffer 的内容复制到 buffer(第 33 行)。这里并非浅复制(复制指针的值),而是深复制,即将指向的内容复制到给当前对象新分配的缓冲区中,如图 9.4 所示:
程序清单 9.9 的输出表明,拷贝中的 buffer 指向的内存地址不同,即两个对象并未指向同一个动态分配的内存地址。因此,函数 UseMyString( )返回、形参 str 被销毁时,析构函数对复制构造函数分配的内存地址调用 delete[],而没有影响 main( )中 sayHello 指向的内存。因此,这两个函数都执行完毕时,成功地销毁了各自的对象,没有导致应用程序崩溃。
最佳实践:
有助于改善性能的移动构造函数
由于 C++的特征和需求,有些情况下对象会自动被复制。请看下面的代码:
正如注释指出的,实例化 sayHelloAgain 时,由于调用了函数 Copy(sayHello),而它按值返回一个 MyString,因此调用了复制构造函数两次。然而,这个返回的值存在时间很短,且在该表达式外不可
用。因此,C++编译器严格地调用复制构造函数反而降低了性能,如果复制的对象很大,对性能的影
响将很严重。
为避免这种性能瓶颈,C++11 引入了移动构造函数。移动构造函数的语法如下:
有移动构造函数时,编译器将自动使用它来“移动”临时资源,从而避免深复制。实现移动构造函数后,应将前面的注释改成下面这样:
移动构造函数通常是利用移动赋值运算符实现的,这将在第 12 章更详细地讨论。程序清单 12.12
是一个更好的 MyString 版本,实现了移动构造函数和移动赋值运算符。
构造函数和析构函数的其他用途
单例模式
要创建单例类,关键字 static 必不可少,如程序清单 9.10 所示:
程序清单 9.10 单例类 President,它禁止复制、赋值以及创建多个实例:
#include <iostream>
#include <string.h>
using namespace std;
class President {
private:
//私有化构造器
President() {};
//私有化拷贝构造
President(const President&);
//私有化赋值拷贝运算符
const President& operator=(const President&);
string name;
public:
//获取唯一实例对象
static President& GetInstance() {
static President onlyInstance;
return onlyInstance;
}
//获取名称
string GetName() {
return name;
}
//设置名称
void SetName(string inputName) {
name = inputName;
}
};
int main()
{
President& onlyPresident = President::GetInstance();
onlyPresident.SetName("Abraham Lincoln");
//下面注释起来的都无法通过编译
//President senond;
//President* third = new President();
//President fourth = onlyPresident;
//onlyPresident = President::GetInstance();
cout << "The name of the president is : ";
cout << President::GetInstance().GetName() << endl;
return 0;
}
禁止在栈中实例化的类
栈空间通常有限。如果您要编写一个数据库类,其内部结构包含数 TB 数据,可能应该禁止在栈
上实例化它,而只允许在自由存储区中创建其实例。为此,关键在于将析构函数声明为私有的:
上述代码试图在栈上创建实例。退栈时,将弹出栈中的所有对象,因此编译器需要在 main( )末尾调用析构函数~MonsterDB(),但这个析构函数是私有的,即不可用,因此上述语句将导致编译错误。
将析构函数声明为私有的并不能禁止在堆中实例化:
上述代码将导致内存泄露。由于在 main 中不能调用析构函数,因此也不能调用 delete。为了解决这种问题,需要在 MonsterDB 类中提供一个销毁实例的静态公有函数(作为类成员,它能够调用析构函数),如程序清单 9.11 所示。
程序清单 9.11 数据库类 MonsterDB,只能使用 new 在自由存储区中创建其对象:
#include <iostream>
#include <string.h>
using namespace std;
class MonsterDB {
private:
~MonsterDB() {};
public:
static void DestroyInstance(MonsterDB* pInstance) {
delete pInstance;
}
void DoSomething(){}
};
int main()
{
//堆上创建实例
MonsterDB* myDB = new MonsterDB();
myDB->DoSomething();
//下面的代码错误,因为析构函数被私有化了
//delete myDB;
//使用静态成员释放内存
MonsterDB::DestroyInstance(myDB);
return 0;
}
这些代码旨在演示如何创建禁止在栈中实例化的类。为此,关键是将构造函数声明成私有的,如第 6 行所示。为分配内存,第 9~12 行的静态函数 DestroyInstance( )必不可少,因为在 main( )中不能对 myDB 调用 delete。为了验证这一点,您可取消对第 23 行的注释。
使用构造函数进行类型转换(隐式类型转换问题)
本章前面介绍过,可给类提供重载的构造函数,即接受一个或多个参数的构造函数。这种构造函
数常用于进行类型转换。请看下面的 Human 类,它包含一个将整数作为参数的重构构造函数:
这个构造函数让您能够执行下面的转换:
注意,这就会导致下面的隐式转换问题:
总结就一句话:并非必须使用关键字 explicit,但在很多情况下,这都是一种良好的编程实践。
this指针
将sizeof()用于类
您知道,通过使用关键字 class 声明自定义类型,可封装数据属性和使用数据的方法。第 3 章介绍过,运算符 sizeof( )用于确定指定类型需要多少内存,单位为字节。这个运算符也可用于类,在这种情况下,它将指出类声明中所有数据属性占用的总内存量,单位为字节。sizeof( )可能对某些属性进行填充,使其与字边界对齐,也可能不这样做,这取决于您使用的编译器。用于类时,sizeof( )不考虑成员函数及其定义的局部变量,如程序清单 9.13 所示。
结构体不同于类的地方
关键字 struct 来自 C 语言,在 C++编译器看来,它与类及其相似,差别在于程序员未指定时,默认的访问限定符(public 和 private)不同。因此,除非指定了,否则结构中的成员默认为公有的(而类成员默认为私有的);另外,除非指定了,否则结构以公有方式继承基结构(而类为私有继承)。继承将在第 10 章详细讨论。
共用体:一种特殊的数据存储机制
共用体是一种特殊的类,每次只有一个非静态数据成员处于活动状态。因此,共用体与类一样,可包含多个数据成员,但不同的是只能使用其中的一个。
要声明共用体,可使用关键字 union,再在这个关键字后面指定共用体名称,然后在大括号内指定其数据成员:
要实例化并使用共用体,可像下面这样做:
在结构体中,常使用共用体来模拟复杂的数据类型。共用体可将固定的内存空间解释为另一种类型,有些实现利用这一点进行类型转换或重新解释内存,但这种做法存在争议,而且可采用其他替代方式。
对类和结构体使用聚合初始化
程序清单 9.17 将聚合初始化用于类:
#include <iostream>
#include<string>
using namespace std;
class Aggregate1
{
public:
int num;
double pi;
};
struct Aggregate2
{
char hello[6];
int impYears[3];
string world;
};
int main()
{
int myNums[] = { 9, 5, -1 }; // myNums is int[3]
Aggregate1 a1{ 2017, 3.14 };
cout << "Pi is approximately: " << a1.pi << endl;
Aggregate2 a2{ {'h', 'e', 'l', 'l', 'o'}, {2011, 2014, 2017}, "world"};
// Alternatively
Aggregate2 a2_2{'h', 'e', 'l', 'l', 'o', '\0', 2011, 2014, 2017, "world"};
cout << a2.hello << ' ' << a2.world << endl;
cout << "C++ standard update scheduled in: " << a2.impYears[2] << endl;
return 0;
}
这个示例演示了如何将聚合初始化用于类(结构)。第 4~9 行定义的 Aggregate1 是一个包含公有数据成员的类,而第 11~16 行定义的 Aggregate2 是一个结构。第 21、24 和 27 行分别演示了如何将聚合初始化用于类和结构。为了证明编译器将初始值存储到了相应的数据成员中,我们访问了类和结构的数据成员。注意到有些成员为数组,另外,请注意 Aggregate2 的 std::string 成员是如何被初始化的。
运行结果如下:
注意:
将constexpr用于类和对象
第 3 章介绍了 constexpr,这为改善 C++应用程序的性能提供了一种强有力的方式。通过使用constexpr 来声明操作常量或常量表达式的函数,可让编译器计算并插入函数的结果,而不是插入计算结果的指令。
这个关键字也可用于类和结果为常量的对象。
但是要注意,将这样的函数或类用于非常量实体时,编译器将忽略关键字 constexpr。
测验
使用 new 创建类实例时,将在什么地方创建它?
堆空间
标准答案:在自由存储区(堆空间)中创建,与使用 new 给 int 变量分配内存时一样。
我的类包含一个原始指针 int*,它指向一个动态分配的 int 数组。请问将 sizeof 用于这个类的
对象时,结果是否取决于该动态数组包含的元素数?
不包括
标准答案:sizeof( )根据声明的数据成员计算类的大小。将 sizeof( )用于指针时,结果与指向的数据量无关,因此类包含指针成员时,将 sizeof 用于该类的结果也是固定的。
假设有一个类,其所有成员都是私有的,且没有友元类和友元函数。请问谁能访问这些成员?
谁都不行
标准答案:除该类的成员方法外,在其他地方都不能访问。
可以在一个类成员方法中调用另一个成员方法吗?
可以
标准答案:可以
构造函数适合做什么?
类数据成员的初始化
标准答案:构造函数通常用于初始化数据成员和资源。
析构函数适合做什么?
资源释放以及一些在类对象被销毁时该做的事情
标准答案:析构函数通常用于释放资源和内存。
第十章:实现继承
继承基础
访问限定符 protected
显然,需要让基类的某些属性能在派生类中访问,但不能在继承层次结构外部访问。这意味着您希望 Fish 类的布尔标志 isFreshWaterFish 可在派生类 Tuna 和 Carp 中访问,但不能在实例化 Tuna 和 Carp的 main( )中访问。为此,可使用关键字 protected。
如果需要让基类的某些属性能在派生类中访问,但不能在继承层次结构外部访问的话,那么需要使用关键字 protected。
基类初始化——向基类传递参数
#include <iostream>
using namespace std;
class Fish{
protected:
// 受保护的变量只允许派生类访问
bool isFreshWaterFish;
public:
Fish(bool isFreshWater): isFreshWaterFish(isFreshWater){}
void Swim() {
if (isFreshWaterFish) {
cout << "Swim in lake" << endl;
}
else {
cout << "Swim in sea" << endl;
}
}
};
class Tuna : public Fish {
public:
//如果基类包含重载的构造函数,需要在实例化时给它提供实参,该怎么办呢?
//创建派生对象时将如何实例化这样的基类?
//方法是使用初始化列表,并通过派生类的构造函数调用合适的基类构造函数
Tuna() :Fish(true) {}
};
在派生类中覆盖基类的方法
如果派生类实现了从基类继承的函数,且返回值和特征标相同,就相当于覆盖了基类的这个方法,
如下面的代码所示:
#include <iostream>
using namespace std;
class Fish{
protected:
// 受保护的变量只允许派生类访问
bool isFreshWaterFish;
public:
Fish(bool isFreshWater): isFreshWaterFish(isFreshWater){}
void Swim() {
if (isFreshWaterFish) {
cout << "Swim in lake" << endl;
}
else {
cout << "Swim in sea" << endl;
}
}
};
class Tuna : public Fish {
public:
Tuna() :Fish(true) {}
void Swim() {
cout << "Tuna swims real fast" << endl;
}
};
int main()
{
Tuna tuna;
tuna.Swim();
}
运行效果:
调用基类中被覆盖的方法
直接使用作用域限定符引用基类中的方法即可:
tuna.Fish::Swim();
在派生类中调用基类的方法
同样是使用作用域限定符,只不过是在类内使用罢了:
#include <iostream>
using namespace std;
class Fish{
protected:
// 受保护的变量只允许派生类访问
bool isFreshWaterFish;
public:
Fish(bool isFreshWater): isFreshWaterFish(isFreshWater){}
void Swim() {
if (isFreshWaterFish) {
cout << "Swim in lake" << endl;
}
else {
cout << "Swim in sea" << endl;
}
}
};
class Tuna : public Fish {
public:
Tuna() :Fish(true) {}
void Swim() {
Fish::Swim();
}
};
int main()
{
Tuna tuna;
tuna.Swim();
}
在派生类中隐藏基类的方法
覆盖的一种极端情形是,子类 Tuna::Swim( )可能隐藏父类 Fish::Swim( )的所有重载版本,使得调用这些重载版本会导致编译错误(因此称为被隐藏)。
#include <iostream>
using namespace std;
class Fish{
protected:
// 受保护的变量只允许派生类访问
bool isFreshWaterFish;
public:
Fish(bool isFreshWater): isFreshWaterFish(isFreshWater){}
void Swim() {
cout << "Fish swims ... " << endl;
}
void Swim(bool isFreshWaterFish) {
if (isFreshWaterFish) {
cout << "Swim in lake" << endl;
}
else {
cout << "Swim in sea" << endl;
}
}
};
class Tuna : public Fish {
public:
Tuna() :Fish(true) {}
void Swim() {
cout << "Tuna swims real fast" << endl;
}
};
int main()
{
Tuna tuna;
// 下面这一行编译错误,由于子类重写了父类的Swim方法
// 因此父类所有叫 Swim 的同名方法都被隐藏,无法被子类调用
// tuna.Swim(true);
}
解决方式依然可以使用作用域限定符。
构造顺序
如果 Tuna 类是从 Fish 类派生而来的,创建 Tuna 对象时,先调用 Tuna 的构造函数还是 Fish 的构造函数?
另外,实例化对象时,成员属性(如 Fish::isFreshWaterFish)是调用构造函数之前还是之后实例化?
好在实例化顺序已标准化,基类对象在派生类对象之前被实例化。
因此,首先构造 Tuna 对象的Fish 部分,这样实例化 Tuna 部分时,成员属性(具体地说是 Fish 的保护和公有属性)已准备就绪,可以使用了。实例化 Fish 部分和 Tuna 部分时,先实例化成员属性(如 Fish::isFreshWaterFish),再调用构造函数,确保成员属性准备就绪,可供构造函数使用。这也适用于 Tuna::Tuna( )。
析构顺序
Tuna 实例不再在作用域内时,析构顺序与构造顺序相反。
输出表明,实例化 Tuna对象时,将从继承层次结构顶部开始,因此首先实例化 Tuna 对象的 Fish 部分。为此,首先实例化 Fish的成员属性,即 Fish::dummy。构造好成员属性(如 dummy)后,将调用 Fish 的构造函数。构造好基类部分后,将实例化 Tuna 部分—首先实例化成员 Tuna::dummy,再执行构造函数 Tuna::Tuna( )的代码。输出表明,析构顺序正好相反。
私有继承
私有继承意味着在派生类的实例中,基类的所有公有成员和方法都是私有的—不能从外部访问。换句话说,即便是 Base 类的公有成员和方法,也只能被 Derived 类使用,而无法通过 Derived 实例来使用它们。
私有继承使得只有子类才能使用基类的属性和方法,因此也被称为 has-a 关系。
#include <iostream>
using namespace std;
class Motor {
public:
void SwitchIgnition() {
cout << "Ignition ON" << endl;
}
void PumpFuel() {
cout << "Fuel in cylinders" << endl;
}
void FireCylinders() {
cout << "Vroooom" << endl;
}
};
class Car :private Motor {
public:
void Move() {
SwitchIgnition();
PumpFuel();
FireCylinders();
}
};
int main() {
Car mycar;
mycar.Move();
//如果是私有继承,那么即便是父类的公有成员和方法,也只能被子类所调用
// 而无法被子类的实例化对象所调用。因此下面这一行代码报错
// mycar.PumpFuel();
}
有一个问题需要注意:以公有方式继承基类的派生类能访问基类的私有成员吗?
不能。编译器总是执行最严格的访问限定符。无论继承关系如何,类的私有成员都不能在类外访问,一个例外是类的友元函数和友元类。
保护继承
在保护继承层次结构中,子类的子类(即 Derived2)能够访问 Base 类的公有和保护成员。如果 Derived 和 Base 之间的继承关系是私有的,就不能这样做。
切除问题
要避免切除问题,不要按值传递参数,而应以指向基类的指针或者 const 引用的方式传递。
多继承
多继承语法:
class Derived: access-specifier(访问修饰符) Base1, access-specifier Base2
{
// class members
};
使用 final 禁止继承
从 C++11 起,编译器支持限定符 final。被声明为 final 的类不能用作基类。
使用形式如下:
// 使用了 final 限定符修饰的类不能被用作父类
class Platypus final: public Mammal, public Bird, public Reptile
{
public:
void Swim()
{
cout << "Platypus: Voila, I can swim!" << endl;
}
};
除了用于类外,还可以将 final 用于成员函数来控制多态行为。
测验
我希望基类的某些成员可在派生类中访问,但不能在继承层次结构外访问,该使用哪种访问限定符?
保护限定符
标答:通过使用访问限定符 protected,可确保派生类能够访问基类的成员,但不能通过派生类实例进
行访问。
如果一个函数接受一个基类对象作为参数,而我将一个派生类对象作为实参按值传递给它,结果将如何?
只会复制派生类对象中有关基类的内容,从而导致派生类对象一些信息的缺失
标答:将复制派生类对象的基类部分,并将其作为参数进行传递。这种切除导致的行为无法预测。
该使用私有继承还是组合?
分情况
标答:使用组合,这样可提高设计的灵活性。
在继承层次结构中,关键字 using 有何用途?
避免在派生类中隐藏掉基类的成员方法
标答:用于避免隐藏基类方法。
Derived 类以私有方式继承了 Base 类,而 SubDerived 类以公有方式继承了 Derived 类。请问SubDerived 类能访问 Base 类的公有成员吗?
不能
标答:不能,因为 Derived 类与 Base 类是私有继承关系,这导致 Base 类对 SubDerived 类隐藏了其公
有成员,即 SubDerived 不能访问它们。