21天学通C++:第九、十章节

第九章:类和对象

带默认值的构造函数参数

注意:默认构造函数是调用时可不提供参数的构造函数,而并不一定是不接受任何参数的构造函数
因此,下面的构造函数虽然有两个参数,但它们都有默认值,因此也是默认构造函数:

#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 不能访问它们。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/780740.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

CurrentHashMap巧妙利用位运算获取数组指定下标元素

先来了解一下数组对象在堆中的存储形式【数组长度&#xff0c;数组元素类型信息等】 【存放元素对象的空间】 Ma 基础信息实例数据内存填充Mark Word,ClassPointer,数组长度第一个元素第二个元素固定的填充内容 所以我们想要获取某个下标的元素首先要获取这个元素的起始位置…

Java 有什么必看的书?

Java必看经典书有这两本&#xff1a; 1、Java核心技术速学版&#xff08;第3版&#xff09; 经典Java开发基础书CoreJava速学版本&#xff01;Java入门优选书籍&#xff0c;更新至Java17&#xff0c;内容皆是精华&#xff0c;让Java学习更简单&#xff0c;让Java知识应用更快速…

fasttext工具介绍

fastText是由Facebook Research团队于2016年开源的一个词向量计算和文本分类工具。尽管在学术上并未带来巨大创新&#xff0c;但其在实际应用中的表现却非常出色&#xff0c;特别是在文本分类任务中&#xff0c;fastText往往能以浅层网络结构取得与深度网络相媲美的精度&#x…

STM32CubeMX实现4X5矩阵按键(HAL库实现)

为了实现计算器键盘&#xff0c;需要使用4X5矩阵按键&#xff0c;因此&#xff0c;我在4X4矩阵键盘上重新设计了一个4X5矩阵按键。原理图如下&#xff1a; 原理描述&#xff1a; 4X5矩阵按键&#xff0c;可以设置4个引脚为输出&#xff0c;5个引脚为输入模式&#xff0c;4个引…

MPS---MPQ86960芯片layout设计总结

MPQ86960 是一款内置功率 MOSFET 和栅极驱动的单片半桥。它可以在宽输入电压 (VIN) 范围内实现高达 50A 的连续输出电流 (IOUT)&#xff0c;通过集成MOSFET 和驱动可优化死区时间 (DT) 并降低寄生电感&#xff0c;从而实现高效率。 MPQ86960 兼容三态输出控制器&#xff0c;另…

Ubantu22.04 通过FlatPak安装微信

Ubuntu22.04 下使用Flatpak稳定安装微信&#xff01; 国际惯例&#xff0c;废话不多说&#xff0c;先上效果图。为啥使用Flatpak,因为Wechat官方只在FlatPak发布了最新的版本。之前使用了Wine以及Dock安装Wechat,效果都不是很理想&#xff0c;bug很多。所以使用了FlatPak。 Fl…

GRPC使用之ProtoBuf

1. 入门指导 1. 基本定义 Protocol Buffers提供一种跨语言的结构化数据的序列化能力&#xff0c;类似于JSON&#xff0c;不过更小、更快&#xff0c;除此以外它还能用用接口定义(IDL interface define language)&#xff0c;通protoc编译Protocol Buffer定义文件&#xff0c;…

【Spring Cloud】微服务的简单搭建

文章目录 &#x1f343;前言&#x1f384;开发环境安装&#x1f333;服务拆分的原则&#x1f6a9;单一职责原则&#x1f6a9;服务自治&#x1f6a9;单向依赖 &#x1f340;搭建案例介绍&#x1f334;数据准备&#x1f38b;工程搭建&#x1f6a9;构建父子工程&#x1f388;创建父…

关闭vue3中脑瘫的ESLine

在创建vue3的时候脑子一抽选了ESLine,然后这傻卵子ESLine老是给我报错 博主用的idea开发前端 ,纯粹是用不惯vscode 关闭idea中的ESLine,这个只是取消红色波浪线, 界面中的显示 第二步,在vue.config.js中添加 lintOnSave: false 到这里就ok了,其他的我试过了一点用没有

Google Java Style Guide深度解读:打造优雅的代码艺术

在软件工程的世界里&#xff0c;代码不仅仅是实现功能的工具&#xff0c;它也是团队之间沟通的桥梁&#xff0c;是软件质量和可维护性的直接反映。Google Java Style Guide作为一套广受认可的编码规范&#xff0c;不仅定义了代码的书写规则&#xff0c;更深刻地影响着Java开发者…

绿色金融相关数据合集(2007-2024年 具体看数据类型)

数据类型&#xff1a; 1.绿色债券数据&#xff1a;2014-2023 2.绿色信贷相关数据&#xff1a;2007-2022 3.全国各省及地级市绿色金融指数&#xff1a;1990-2022 4.碳排放权交易明细数据&#xff1a;2013-2024 5.绿色金融试点DID数据&#xff1a;2010-2023 数据来源&#…

python操作SQLite3数据库进行增删改查

python操作SQLite3数据库进行增删改查 1、创建SQLite3数据库 可以通过Navicat图形化软件来创建: 2、创建表 利用Navicat图形化软件来创建: 存储在 SQLite 数据库中的每个值(或是由数据库引擎所操作的值)都有一个以下的存储类型: NULL. 值是空值。 INTEGER. 值是有符…

Linux—网络设置

目录 一、ifconfig——查看网络配置 1、查看网络接口信息 1.1、查看所有网络接口 1.2、查看具体的网络接口 2、修改网络配置 3、添加网络接口 4、禁用/激活网卡 二、hostname——查看主机名称 1、查看主机名称 2、临时修改主机名称 3、永久修改主机名称 4、查看本…

【python】pyqt5大学生成绩信息管理系统-图形界面(源码+报告)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

基于支持向量机、孤立森林和LSTM自编码器的机械状态异常检测(MATLAB R2021B)

异常检测通常是根据已有的观测数据建立正常行为模型&#xff0c;从而将不同机制下产生的远离正常行为的数据划分为异常类&#xff0c;进而实现对异常状态的检测。常用的异常检测方法主要有&#xff1a;统计方法、信息度量方法、谱映射方法、聚类方法、近邻方法和分类方法等。 …

飞书 API 2-4:如何使用 API 将数据写入数据表

一、引入 上一篇创建好数据表之后&#xff0c;接下来就是写入数据和对数据的处理。 本文主要探讨数据的插入、更新和删除操作。所有的操作都是基于上一篇&#xff08;飞书 API 2-4&#xff09;创建的数据表进行操作。上面最终的数据表只有 2 个字段&#xff1a;序号和邮箱。序…

巴图自动化PN转Modbus RTU协议转换网关模块快速配置

工业领域中常用的通讯协议有&#xff1a;Profinet协议&#xff0c;Modbus协议&#xff0c;ModbusTCP协议&#xff0c;Profibus协议&#xff0c;Profibus DP协议&#xff0c;EtherCAT协议&#xff0c;EtherNET协议&#xff0c;CAN&#xff0c;CanOpen等&#xff0c;它们在自动化…

kubeadm快速部署k8s集群

文章目录 Kubernetes简介1、k8s集群环境2、linux实验环境初始化【所有节点】3、安装docker容器引擎【所有节点】4、安装cri-dockerd【所有节点】5、安装 kubeadm、kubelet、kubectl【所有节点】6、部署 k8s master 节点【master节点】7、加入k8s Node 节点【node节点】8、部署容…

【链表】【双指针】1、合并两个有序链表+2、分隔链表+3、删除链表的倒数第N个结点+4、链表的中间结点+5、合并两个链表

3道中等2道简单 数组和字符串打算告一段落&#xff0c;正好最近做的几乎都是双指针&#xff0c;所以今天做链表&#xff01; 1、合并两个有序链表&#xff08;难度&#xff1a;简单&#xff09; 该题对应力扣网址 AC代码 思路简单 /*** Definition for singly-linked list.…