• C++中的代码重用


    11.12 C++中的代码重用

    C++的一个主要目标是促进代码重用。公有继承是实现这种目标的机制之一,但并不是唯一的机制。
    本章将介绍其他方法,其中之一是使用这样的类成员:本身是另一个类的对象。这种方法称为包含(containment)、组合(composition)或层次化(layering)。另一种方法是使用私有或保护继承。通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。

    类模板使得程序员能够使用通用术语定义类,然后使用模板来创建针对特定类型定义的特殊类。

    11.12.1 类与对象成员

    学生是什么?是有名字和有一系列考试分数的人。

    11.12.1.1 valarray类简介

    valarray类是由头文件 valarray支持的。顾名思义,这个类用于处理数值(或具有类似特性的类),它支持诸如将数组中所有元素的值相加以及在数组中找出最大和最小的值等操作。valarray被定义为一个模板类,以便能够处理不同的数据类型。

    valarray<int> q_values; // an array of int
    valarray<double> weights; // an array of double
    
    double gpa[5] = {3.1, 3.5, 3.8, 2.9, 3.3};
    valarray<double> v1; // an array of double, size 0
    valarray<int> v2(8); // an array of 8 int elements
    valarray<int> v3(10,8); // an array of 8 int elements, each set to 10
    valarray<double> v4(gpa, 4); // an array of 4 elements initialized to the first 4 elements of gpa
    
    //In C++11,using an initializer list is also allowed.
    valarray<int> v5 = {20, 32, 17, 9}; // C++11
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • The operator method provides access to individual elements.
    • The size() method returns the number of elements.
    • The sum() method returns the sum of the elements.
    • The max() method returns the largest element.
    • The min() method returns the smallest element
    11.12.1.2 Student类定义
    11.12.1.2.1 has-a关系和容器

    您可能想以公有的方式从这两个类派生出Student类,这将是多重公有继承,C++允许这样做,但在这里并不合适,因为学生与这些类之间的关系不是is-a模型。学生不是姓名,也不是一组考试成绩。这里的关系是has-a,学生有姓名,也有一组考试分数。通常,用于建立has-a关系的C++技术是组合(包含),即创建一个包含其他类对象的类。

    class Student
    {
    private:
        string name; // use a string object for name
        valarray<double> scores; // use a valarray object for scores
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    11.12.1.2.2 接口和实现

    同样,上述类将数据成员声明为私有的。这意味着Student类的成员函数可以使用string和valarray类的公有接口来访问和修改name和scores对象,但在类的外面不能这样做,而只能通过Student类的公有接口访问name和score(请参见图14)。对于这种情况,通常被描述为Student类获得了其成员对象的实现,但没有继承接口。

    使用公有继承时,类可以继承接口,可能还有实现(基类的纯虚函数提供接口,但不提供实现)。获得接口是is-a关系的组成部分。而使用组合,类可以获得实现,但不能获得接口。不继承接口是has-a关系的组成部分。

    对于has-a关系来说,类对象不能自动获得被包含对象的接口是一件好事。在学生类中,姓名字符串的+操作符重载是不需要的,另一方面,被包含的类的接口部分对新类来说可能是有意义的。

    11.12.1.2.3 初始化顺序

    当初始化列表包含多个项目时,这些项目被初始化的顺序为它们被声明的顺序,而不是它们在初始化列表中的顺序。例如,假设Student构造函数如下:

    Student(const char * str, const double * pd, int n)
    : scores(pd, n), name(str) {}
    
    • 1
    • 2

    则name成员仍将首先被初始化,因为在类定义中它首先被声明。对于这个例子来说,初始化顺序并不重要,但如果代码使用一个成员的值作为另一个成员的初始化表达式的一部分时,初始化顺序就非常重要了。

    11.12.1.3 Student类举例

    studentc.h

    #pragma once
    // studentc.h -- defining a Student class using containment
    #ifndef STUDENTC_H_
    #define STUDENTC_H_
    #include 
    #include 
    #include 
    class Student
    {
    private:
    	typedef std::valarray<double> ArrayDb;
    	std::string name; // contained object
    	ArrayDb scores; // contained object
    	// private method for scores output
    	std::ostream& arr_out(std::ostream& os) const;
    public:
    	Student() : name("Null Student"), scores() {}
    	explicit Student(const std::string& s)
    		: name(s), scores() {}
    	explicit Student(int n) : name("Nully"), scores(n) {}
    	Student(const std::string& s, int n)
    		: name(s), scores(n) {}
    	Student(const std::string& s, const ArrayDb& a)
    		: name(s), scores(a) {}
    	Student(const char* str, const double* pd, int n)
    		: name(str), scores(pd, n) {}
    	~Student() {}
    	double Average() const;
    	const std::string& Name() const;
    	double& operator[](int i);
    	double operator[](int i) const;
    	// friends
    	// input
    	friend std::istream& operator>>(std::istream& is,
    		Student& stu); // 1 word
    	friend std::istream& getline(std::istream& is,
    		Student& stu); // 1 line
    		// output
    	friend std::ostream& operator<<(std::ostream& os,
    		const Student& stu);
    };
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    studentc.cpp

    // studentc.cpp -- Student class using containment
    #include "studentc.h"
    using std::ostream;
    using std::endl;
    using std::istream;
    using std::string;
    //public methods
    double Student::Average() const
    {
    	if (scores.size() > 0)
    		return scores.sum() / scores.size();
    	else
    		return 0;
    }
    const string& Student::Name() const
    {
    	return name;
    }
    double& Student::operator[](int i)
    {
    	return scores[i]; // use valarray::operator[]()
    }
    double Student::operator[](int i) const
    {
    	return scores[i];
    }
    // private method
    ostream& Student::arr_out(ostream& os) const
    {
    	int i;
    	int lim = scores.size();
    	if (lim > 0)
    	{
    		for (i = 0; i < lim; i++)
    		{
    			os << scores[i] << " ";
    			if (i % 5 == 4)
    				os << endl;
    		}
    		if (i % 5 != 0)
    			os << endl;
    	}
    	else
    		os << " empty array ";
    	return os;
    }
    // friends
    // use string version of operator>>()
    istream& operator>>(istream& is, Student& stu)
    {
    	is >> stu.name;
    	return is;
    }
    // use string friend getline(ostream &, const string &)
    istream& getline(istream& is, Student& stu)
    {
    	getline(is, stu.name);
    	return is;
    }
    // use string version of operator<<()
    ostream& operator<<(ostream& os, const Student& stu)
    {
    	os << "Scores for " << stu.name << ":\n";
    	stu.arr_out(os); // use private method for scores
    	return os;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    main.cpp

    // use_stuc.cpp -- using a composite class
    // compile with studentc.cpp
    #include 
    #include "studentc.h"
    using std::cin;
    using std::cout;
    using std::endl;
    void set(Student& sa, int n);
    const int pupils = 3;
    const int quizzes = 5;
    int main()
    {
    	Student ada[pupils] =
    	{ Student(quizzes), Student(quizzes), Student(quizzes) };
    	int i;
    	for (i = 0; i < pupils; ++i)
    		set(ada[i], quizzes);
    	cout << "\nStudent List:\n";
    	for (i = 0; i < pupils; ++i)
    		cout << ada[i].Name() << endl;
    	cout << "\nResults:";
    	for (i = 0; i < pupils; ++i)
    	{
    		cout << endl << ada[i];
    		cout << "average: " << ada[i].Average() << endl;
    	}
    	cout << "Done.\n";
    	return 0;
    }
    void set(Student& sa, int n)
    {
    	cout << "Please enter the student's name: ";
    	getline(cin, sa);
    	cout << "Please enter " << n << " quiz scores:\n";
    	for (int i = 0; i < n; i++)
    		cin >> sa[i];
    	while (cin.get() != '\n')
    		continue;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    运行结果:

    Please enter the student's name: Jasmine
    Please enter 5 quiz scores:
    99
    99
    99
    99
    99
    Please enter the student's name: Lily
    Please enter 5 quiz scores:
    99
    99
    99
    99
    99
    Please enter the student's name: Booo
    Please enter 5 quiz scores:
    99
    99
    99
    99
    99
    
    Student List:
    Jasmine
    Lily
    Booo
    
    Results:
    Scores for Jasmine:
    99 99 99 99 99
    average: 99
    
    Scores for Lily:
    99 99 99 99 99
    average: 99
    
    Scores for Booo:
    99 99 99 99 99
    average: 99
    Done.
    
    D:\Prj\C++\Private_Inheritance\Debug\Private_Inheritance.exe (进程 1348)已退出,代码为 0。
    要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
    按任意键关闭此窗口. . .
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    11.12.2 私有继承

    C++还有另一种实现has-a关系的途径–私有继承。使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们。

    包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。我们将使用术语子对象(subobject)来表示通过继承或包含添加的对象。

    11.12.2.1 容器与私有继承的不同
    class Student : private std::string, private std::valarray<double>
    {
    public:
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    第一个区别:容器版本提供了两个被显式命名的对象成员,而私有继承提供了两个无名称的子对象成员。

    11.12.2.1.1 初始化基类组件

    第二个区别:

    Student(const char * str, const double * pd, int n)
    : name(str), scores(pd, n) {} // use object names for containment
    
    Student(const char * str, const double * pd, int n)
    : std::string(str), ArrayDb(pd, n) {} // use class names for inheritance
    
    • 1
    • 2
    • 3
    • 4
    • 5
    11.12.2.1.2 访问基类方法

    在这里插入图片描述

    总之,使用容器时将使用对象名来调用方法,而使用私有继承时将使用类名和作用域解析运算符来调用方法。

    11.12.2.1.3 访问基类对象

    使用作用域运算符可以访问基类的方法,但如果要使用基类对象本身,该如何作呢?

    答案是使用强制类型转换。由于Student类是从string类派生而来的,因此可以通过强制类型转换,将Student对象转换为string对象;结果为继承而来的string对象。

    const string & Student::Name() const
    {
        return (const string &) *this;
    }
    
    • 1
    • 2
    • 3
    • 4
    11.14.2.1.4 访问基类友元函数

    用类名显式地限定函数名不适合于友元函数,这是因为友元不属于类。然而,可以通过显式地转换为基类来调用正确的函数。

    ostream & operator<<(ostream & os, const Student & stu)
    {
        os << "Scores for " << (const String &) stu << ":\n";
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    考虑以下代码:

    os << "Scores for " << (const String &) stu << ":\n";
    
    • 1

    显式地将stu转换为string对象引用,进而调用函数operator<<(ostream&,const String&)。
    引用stu不会自动转换为string引用。根本原因在于,在私有继承中,在不进行显式类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针。
    另一个原因是,由于这个类使用的是多重继承,编译器将无法确定应转换成哪个基类,如果两个基类都提供了函数operator<<()。

    11.12.2.2 私有继承实现学生类

    studenti.h

    #pragma once
    // studenti.h -- defining a Student class using private inheritance
    #ifndef STUDENTC_H_
    #define STUDENTC_H_
    #include 
    #include 
    #include 
    class Student : private std::string, private std::valarray<double>
    {
    private:
    	typedef std::valarray<double> ArrayDb;
    	// private method for scores output
    	std::ostream& arr_out(std::ostream& os) const;
    public:
    	Student() : std::string("Null Student"), ArrayDb() {}
    	explicit Student(const std::string& s)
    		: std::string(s), ArrayDb() {}
    	explicit Student(int n) : std::string("Nully"), ArrayDb(n) {}
    	Student(const std::string& s, int n)
    		: std::string(s), ArrayDb(n) {}
    	Student(const std::string& s, const ArrayDb& a)
    		: std::string(s), ArrayDb(a) {}
    	Student(const char* str, const double* pd, int n)
    		: std::string(str), ArrayDb(pd, n) {}
    	~Student() {}
    	double Average() const;
    	double& operator[](int i);
    	double operator[](int i) const;
    	const std::string& Name() const;
    	// friends
    	// input
    	friend std::istream& operator>>(std::istream& is,
    		Student& stu); // 1 word
    	friend std::istream& getline(std::istream& is,
    		Student& stu); // 1 line
    		// output
    	friend std::ostream& operator<<(std::ostream& os,
    		const Student& stu);
    };
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    studenti.cpp

    // studenti.cpp -- Student class using private inheritance
    #include "studenti.h"
    using std::ostream;
    using std::endl;
    using std::istream;
    using std::string;
    // public methods
    double Student::Average() const
    {
    	if (ArrayDb::size() > 0)
    		return ArrayDb::sum() / ArrayDb::size();
    	else
    		return 0;
    }
    const string& Student::Name() const
    {
    	return (const string&)*this;
    }
    double& Student::operator[](int i)
    {
    	return ArrayDb::operator[](i); // use ArrayDb::operator[]()
    }
    double Student::operator[](int i) const
    {
    	return ArrayDb::operator[](i);
    }
    // private method
    ostream& Student::arr_out(ostream& os) const
    {
    	int i;
    	int lim = ArrayDb::size();
    	if (lim > 0)
    	{
    		for (i = 0; i < lim; i++)
    		{
    			os << ArrayDb::operator[](i) << " ";
    			if (i % 5 == 4)
    				os << endl;
    		}
    		if (i % 5 != 0)
    			os << endl;
    	}
    	else
    		os << " empty array ";
    	return os;
    }
    // friends
    // use String version of operator>>()
    istream& operator>>(istream& is, Student& stu)
    {
    	is >> (string&)stu;
    	return is;
    }
    // use string friend getline(ostream &, const string &)
    istream& getline(istream& is, Student& stu)
    {
    	getline(is, (string&)stu);
    	return is;
    }
    // use string version of operator<<()
    ostream& operator<<(ostream& os, const Student& stu)
    {
    	os << "Scores for " << (const string&)stu << ":\n";
    	stu.arr_out(os); // use private method for scores
    	return os;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66

    main.cpp

    // use_stui.cpp -- using a class with private inheritance
    // compile with studenti.cpp
    #include 
    #include "studenti.h"
    using std::cin;
    using std::cout;
    using std::endl;
    void set(Student& sa, int n);
    const int pupils = 3;
    const int quizzes = 5;
    int main()
    {
    	Student ada[pupils] =
    	{ Student(quizzes), Student(quizzes), Student(quizzes) };
    	int i;
    	for (i = 0; i < pupils; i++)
    		set(ada[i], quizzes);
    	cout << "\nStudent List:\n";
    	for (i = 0; i < pupils; ++i)
    		cout << ada[i].Name() << endl;
    	cout << "\nResults:";
    	for (i = 0; i < pupils; i++)
    	{
    		cout << endl << ada[i];
    		cout << "average: " << ada[i].Average() << endl;
    	}
    	cout << "Done.\n";
    	return 0;
    }
    void set(Student& sa, int n)
    {
    	cout << "Please enter the student's name: ";
    	getline(cin, sa);
    	cout << "Please enter " << n << " quiz scores:\n";
    	for (int i = 0; i < n; i++)
    		cin >> sa[i];
    	while (cin.get() != '\n')
    		continue;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    运行结果:

    Please enter the student's name: Jasmine
    Please enter 5 quiz scores:
    99
    99
    99
    99
    99
    Please enter the student's name: Lily
    Please enter 5 quiz scores:
    99
    99
    99
    99
    99
    Please enter the student's name: Booo
    Please enter 5 quiz scores:
    99
    99
    99
    99
    99
    
    Student List:
    Jasmine
    Lily
    Booo
    
    Results:
    Scores for Jasmine:
    99 99 99 99 99
    average: 99
    
    Scores for Lily:
    99 99 99 99 99
    average: 99
    
    Scores for Booo:
    99 99 99 99 99
    average: 99
    Done.
    
    D:\Prj\C++\Private_Inheritance\Debug\Private_Inheritance.exe (进程 1348)已退出,代码为 0。
    要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
    按任意键关闭此窗口. . .
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    11.12.3 容器还是私有继承?

    既然可以使用容器,也可以使用私有继承来建立has-a关系,那么应使用哪种方式呢?大多数C++程序员倾向于使用容器。

    • 首先,它易于理解。类声明中包含表示被包含类的显式命名对象,代码可以通过名称引用这些对象,而是要继承将使得关系更抽象。
    • 其次,继承会引起很多问题,尤其从多个基类继承时,可能必须处理很多问题,如共享祖先的独立基类。
    • 另外,容器能够包括多个同类的子对象。如果某个类需要3个string对象,可以使用容器声明3个独立的string成员。而继承只能使用这样一个对象。

    然而,私有继承提供的特性确实比容器多:

    • 假设类包含保护成员,则这样的成员在派生类中是可用的,但在继承层次结构外是不可用的。如果使用容器将有保护成员的类包含在另一个类中,则后者将不是派生类,而是位于继承层次结构之外,因此不能访问保护成员。但通过继承得到的将是派生类,因此它能够访问保护成员。
    • 重新定义虚函数:派生类可以重新定义虚函数,但容器类不能。使用私有继承,重新定义的函数只能在类中使用,而不是共有的。

    11.12.4 保护继承

    class Student : protected std::string,
    protected std::valarray<double>
    {...};
    
    • 1
    • 2
    • 3

    使用保护继承时,基类的公有成员和保护成员都将成为派生类的保护成员。和私有私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口。

    隐式向上转换意味着无需进行显式类型转换,就可以将基类指针或引用用于派生类对象。

    11.12.5 使用using重新定义访问权限

    使用保护派生或私有派生时,基类的公有成员将成为保护成员或私有成员。假设要让基类的方法在派生类外面可用,方法之一是定义一个使用该基类方法的派生类方法。

    double Student::sum() const // public Student method
    {
        return std::valarray<double>::sum(); // use privately-inherited method
    }
    
    • 1
    • 2
    • 3
    • 4

    另一种方法是,将函数调用包装在另一个函数调用中,即使用一个using声明(就像名称空间那样)
    来指出派生类可以使用特定的基类成员,即使采用的是私有派生。

    class Student : private std::string, private std::valarray<double>
    {
        ...
    public:
        using std::valarray<double>::min;
        using std::valarray<double>::max;
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上述using声明使得 valarray::min()和valarray::max()可用,就像它们是Student的公有方法一样:

    cout << "high score: " << ada[i].max() << endl;
    
    • 1

    注意,using声明只使用成员名–没有圆括号、函数特征标和返回类型。

    11.12.6 多继承

    MI描述的是有多个直接基类的类。与单继承一样,公有MI表示的也是is-a关系。例如,可以从Waiter类和Singer类派生出SingingWaiter类:

    class SingingWaiter : public Waiter, public Singer {...};
    
    • 1

    请注意,必须使用关键字public来限定每一个基类。这是因为,除非特别指出,否则编译器将认为是私有派生:

    class Singingwaiterpublic waiter,Singer(...)://Singer is a private base
    
    • 1

    MI可能会给程序员带来很多新问题。其中两个主要的问题是:从两个不同的基类继承同名方法:从两个或更多相关基类那里继承同一个类的多个实例。
    在这里插入图片描述

    11.12.6.1 有几个Workers?

    通常,这种赋值将把基类指针设置为派生对象中的基类对象的地址。但ed中包含两个Worker对象,有两个地址可供选择,所以应使用类型转换来指定对象:

    SingingWaiter ed;
    Worker * pw = &ed; // ambiguous
    Worker * pw1 = (Waiter *) &ed; // the Worker in Waiter
    Worker * pw2 = (Singer *) &ed; // the Worker in Singer
    
    • 1
    • 2
    • 3
    • 4

    包含两个Worker对象拷贝还会导致其他的问题。然而,真正的问题是:为什么需要Worker对象的两个拷贝?

    针对MI导致的基类多副本问题,C++引入了一种新技术—虚基类(virtual base class)。

    在这里插入图片描述

    11.12.6.2 虚基类
    11.12.6.2.1 虚基类定义

    虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。

    就是只有一个副本。

    class Singer : virtual public Worker {...};
    class Waiter : public virtual Worker {...};
    class SingingWaiter: public Singer, public Waiter {...};
    
    • 1
    • 2
    • 3

    现在,SingingWaiter对象将只包含Worker对象的一个副本。从本质上说,继承的Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象副本。

    在这里插入图片描述

    11.12.6.2.2 三个问题
    • 首先是为什么使用virtual关键字。毕竞,在虚函数和虚基类之间并不存在明显的联系。C++用户强烈反对引入新的关键字,因为这将给他们带来很大的压力。例如,如果新关键字与重要程序中的重要函数或变量的名称相同,这将非常麻烦。因此,C++对这种新特性也使用关键字virtual一有点像关键字重载。
    • 第二,为什么我们不丢弃将基类声明为虚类,并将虚行为作为MI的规范呢?首先,在某些情况下,您可能需要一个基类的多个副本。其次,使基类为虚类需要一个程序做一些额外的计算,如果你不需要它,你不应该为这个功能付费。
    • 最后,是否存在麻烦?是的。为使虚基类能够工作,需要对C++规则进行调整,必须以不同的方式编写一些代码。另外,使用虚基类还可能需要修改已有的代码。
    11.12.6.3 新构造函数规则

    对于非虚基类,初始化列表中唯一可以出现的构造函数是直接基类的构造函数。但是这些构造函数反过来可以将信息传递给它们的基类。

    class A
    {
        int a;
    public:
        A(int n = 0) : a(n) {}
        ...
    };
    class B: public A
    {
        int b;
    public:
        B(int m = 0, int n = 0) : A(n), b(m) {}
        ...
    };
    class C : public B
    {
        int c;
    public:
        C(int q = 0, int m = 0, int n = 0) : B(m, n), c(q) {}
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    C类的构造函数只能调用B类的构造函数,而B类的构造函数只能调用A类的构造函数。

    如果Worker是虚基类,则这种信息传递将不起作用。

    SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other)
    : Waiter(wk,p), Singer(wk,v) {} // flawed
    
    • 1
    • 2

    存在的问题是,自动传递信息时,将通过2条不同的途径(Waiter和Singer)将wk传递给Worker对象。为避免这种冲突,C++在基类是虚的时,禁止信息通过中间类自动传递给基类。因此,上述构造函数将初始化成员panache和voice,但wk参数中的信息将不会传递给子对象Waiter。然而,编译器必须在构造派生对象之前构造基类对象组件;在上述情况下,编译器将使用Worker的默认构造函数。
    如果不希望默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。

    SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other)
    : Worker(wk), Waiter(wk,p), Singer(wk,v) {}
    
    • 1
    • 2

    上述代码将显式地调用构造函数worker(const Worker&)。请注意,这种用法是合法的,对于虚基类,必须这样做;但对于非虚基类,则是非法的。

    11.12.6.4 哪个方法?

    假设要给SingingWaiter类扩展Show() 方法。

    SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
    newhire.Show(); // ambiguous
    
    • 1
    • 2

    对于单继承,如果没有重新定义Show(),则将使用最近祖先中的定义。而在多重继承中,每个直接祖先都有一个Show()函数,这使得上述调用是二义性的。

    可以使用作用域解析运算符来澄清编程者的意图:

    SingingWaiter newhire("Elise Hawks", 2005, 6, soprano);
    newhire.Singer::Show(); // use Singer version
    
    • 1
    • 2

    然而,更好的方法是在SingingWaiter中重新定义Show(),并指出要使用哪个Show()。

    void SingingWaiter::Show()
    {
        Singer::Show();
    }
    
    • 1
    • 2
    • 3
    • 4

    这种让派生方法调用基方法的方法对于单一继承来说工作得很好。但是,该方法忽略了Waiter组件,也可以通过调用Waiter版本来解决这个问题:

    void SingingWaiter::Show()
    {
        Singer::Show();
        Waiter::Show();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    然而,这将显示姓名和ID两次,因为Singer::Show()和Waiter::Show()都调用了Worker::Show()。
    如果解决呢?一种办法是使用模块化方式,而不是递增方式,即提供一个只显示Worker组件的方法和一个只显示Waiter组件或Singer组件(而不是Waiter和Worker组件)的方法。然后,在Singing Waiter::Show()方法中将组件组合起来。

    void Worker::Data() const
    {
        cout << "Name: " << fullname << "\n";
        cout << "Employee ID: " << id << "\n";
    }
    void Waiter::Data() const
    {
        cout << "Panache rating: " << panache << "\n";
    }
    void Singer::Data() const
    {
        cout << "Vocal range: " << pv[voice] << "\n";
    }
    void SingingWaiter::Data() const
    {
        Singer::Data();
        Waiter::Data();
    }
    void SingingWaiter::Show() const
    {
        cout << "Category: singing waiter\n";
        Worker::Data();
        Data();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    而Data()方法只在类内部可用,作为协助公有接口的辅助方法。然而,使Data()方法成为私有的将阻止Waiter中的代码使用Worker::Data(),这正是保护访问类的用武之地。如果Data()方法是保护的,则只能在继承层次结构中的类中使用它,在其他地方则不能使用。

    11.12.6.5 举例

    workermi.h

    #pragma once
    // workermi.h -- working classes with MI
    #ifndef WORKERMI_H_
    #define WORKERMI_H_
    #include 
    class Worker // an abstract base class
    {
    private:
    	std::string fullname;
    	long id;
    protected:
    	virtual void Data() const;
    	virtual void Get();
    public:
    	Worker() : fullname("no one"), id(0L) {}
    	Worker(const std::string& s, long n)
    		: fullname(s), id(n) {}
    	virtual ~Worker() = 0; // pure virtual function
    	virtual void Set() = 0;
    	virtual void Show() const = 0;
    };
    class Waiter : virtual public Worker
    {
    private:
    	int panache;
    protected:
    	void Data() const;
    	void Get();
    public:
    	Waiter() : Worker(), panache(0) {}
    	Waiter(const std::string& s, long n, int p = 0)
    		: Worker(s, n), panache(p) {}
    	Waiter(const Worker& wk, int p = 0)
    		: Worker(wk), panache(p) {}
    	void Set();
    	void Show() const;
    };
    class Singer : virtual public Worker
    {
    protected:
    	enum {
    		other, alto, contralto, soprano,
    		bass, baritone, tenor
    	};
    	enum { Vtypes = 7 };
    	void Data() const;
    	void Get();
    private:
    	const static char* pv[Vtypes]; // string equivs of voice types
    	int voice;
    public:
    	Singer() : Worker(), voice(other) {}
    	Singer(const std::string& s, long n, int v = other)
    		: Worker(s, n), voice(v) {}
    	Singer(const Worker& wk, int v = other)
    		: Worker(wk), voice(v) {}
    	void Set();
    	void Show() const;
    };
    // multiple inheritance
    class SingingWaiter : public Singer, public Waiter
    {
    protected:
    	void Data() const;
    	void Get();
    public:
    	SingingWaiter() {}
    	SingingWaiter(const std::string& s, long n, int p = 0,
    		int v = other)
    		: Worker(s, n), Waiter(s, n, p), Singer(s, n, v) {}
    	SingingWaiter(const Worker& wk, int p = 0, int v = other)
    		: Worker(wk), Waiter(wk, p), Singer(wk, v) {}
    	SingingWaiter(const Waiter& wt, int v = other)
    		: Worker(wt), Waiter(wt), Singer(wt, v) {}
    	SingingWaiter(const Singer& wt, int p = 0)
    		: Worker(wt), Waiter(wt, p), Singer(wt) {}
    	void Set();
    	void Show() const;
    };
    #endif
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80

    workermi.cpp

    // workermi.cpp -- working class methods with MI
    #include "workermi.h"
    #include 
    using std::cout;
    using std::cin;
    using std::endl;
    // Worker methods
    Worker::~Worker() { }
    // protected methods
    void Worker::Data() const
    {
    	cout << "Name: " << fullname << endl;
    	cout << "Employee ID: " << id << endl;
    }
    void Worker::Get()
    {
    	getline(cin, fullname);
    	cout << "Enter worker's ID: ";
    	cin >> id;
    	while (cin.get() != '\n')
    		continue;
    }
    // Waiter methods
    void Waiter::Set()
    {
    	cout << "Enter waiter's name: ";
    	Worker::Get();
    	Get();
    }
    void Waiter::Show() const
    {
    	cout << "Category: waiter\n";
    	Worker::Data();
    	Data();
    }
    // protected methods
    void Waiter::Data() const
    {
    	cout << "Panache rating: " << panache << endl;
    }
    void Waiter::Get()
    {
    	cout << "Enter waiter's panache rating: ";
    	cin >> panache;
    	while (cin.get() != '\n')
    		continue;
    }
    // Singer methods
    const char * Singer::pv[Singer::Vtypes] = { "other", "alto", "contralto","soprano", "bass", "baritone", "tenor" };
    void Singer::Set()
    {
    	cout << "Enter singer's name: ";
    	Worker::Get();
    	Get();
    }
    void Singer::Show() const
    {
    	cout << "Category: singer\n";
    	Worker::Data();
    	Data();
    }
    // protected methods
    void Singer::Data() const
    {
    	cout << "Vocal range: " << pv[voice] << endl;
    }
    void Singer::Get()
    {
    	cout << "Enter number for singer's vocal range:\n";
    	int i;
    	for (i = 0; i < Vtypes; i++)
    	{
    		cout << i << ": " << pv[i] << " ";
    		if (i % 4 == 3)
    			cout << endl;
    	}
    	if (i % 4 != 0)
    		cout << '\n';
    	cin >> voice;
    	while (cin.get() != '\n')
    		continue;
    }
    // SingingWaiter methods
    void SingingWaiter::Data() const
    {
    	Singer::Data();
    	Waiter::Data();
    }
    void SingingWaiter::Get()
    {
    	Waiter::Get();
    	Singer::Get();
    }
    void SingingWaiter::Set()
    {
    	cout << "Enter singing waiter's name: ";
    	Worker::Get();
    	Get();
    }
    void SingingWaiter::Show() const
    {
    	cout << "Category: singing waiter\n";
    	Worker::Data();
    	Data();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105

    main.cpp

    // workmi.cpp -- multiple inheritance
    // compile with workermi.cpp
    #include 
    #include 
    #include "workermi.h"
    const int SIZE = 5;
    int main()
    {
    	using std::cin;
    	using std::cout;
    	using std::endl;
    	using std::strchr;
    	Worker* lolas[SIZE];
    	int ct;
    	for (ct = 0; ct < SIZE; ct++)
    	{
    		char choice;
    		cout << "Enter the employee category:\n"
    			<< "w: waiter s: singer "
    			<< "t: singing waiter q: quit\n";
    		cin >> choice;
    		//This function returns the address of the first occurrence of the choice character value in the string "wstq"; the function returns the NULL pointer if the character isn’t found.
    		while (strchr("wstq", choice) == NULL)
    		{
    			cout << "Please enter a w, s, t, or q: ";
    			cin >> choice;
    		}
    		if (choice == 'q')
    			break;
    		switch (choice)
    		{
    		case 'w': lolas[ct] = new Waiter;
    			break;
    		case 's': lolas[ct] = new Singer;
    			break;
    		case 't': lolas[ct] = new SingingWaiter;
    			break;
    		}
    		cin.get();
    		lolas[ct]->Set();
    	}
    	cout << "\nHere is your staff:\n";
    	int i;
    	for (i = 0; i < ct; i++)
    	{
    		cout << endl;
    		lolas[i]->Show();
    	}
    	for (i = 0; i < ct; i++)
    		delete lolas[i];
    	cout << "Bye.\n";
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53

    运行结果:

    Enter the employee category:
    w: waiter s: singer t: singing waiter q: quit
    t
    Enter singing waiter's name: Jamsine
    Enter worker's ID: 1
    Enter waiter's panache rating: 99
    Enter number for singer's vocal range:
    0: other 1: alto 2: contralto 3: soprano
    4: bass 5: baritone 6: tenor
    6
    Enter the employee category:
    w: waiter s: singer t: singing waiter q: quit
    s
    Enter singer's name: bobo
    Enter worker's ID: 2
    Enter number for singer's vocal range:
    0: other 1: alto 2: contralto 3: soprano
    4: bass 5: baritone 6: tenor
    6
    Enter the employee category:
    w: waiter s: singer t: singing waiter q: quit
    q
    
    Here is your staff:
    
    Category: singing waiter
    Name: Jamsine
    Employee ID: 1
    Vocal range: tenor
    Panache rating: 99
    
    Category: singer
    Name: bobo
    Employee ID: 2
    Vocal range: tenor
    Bye.
    
    D:\Prj\C++\Multiple_Inheritance\Debug\Multiple_Inheritance.exe (进程 14556)已退出,代码为 0。
    要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
    按任意键关闭此窗口. . .
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

  • 相关阅读:
    查找或替换excel换行符ctrl+j和word中的换行符^p,^l
    Flink状态
    TensorFlow学习:在web前端如何使用Keras 模型
    瑞芯微rk356x板子快速上手
    【MySQL】数据库数据类型
    hutool处理excel时候空指针小记
    性能测试工具wrk安装使用详解
    wangEditor 富文本编辑
    从模型容量的视角看监督学习
    常见Prometheus exporter部署
  • 原文地址:https://blog.csdn.net/weixin_44410704/article/details/127961230