C++学习笔记(第14章->代码重用->包含,继承,多重继承,虚基类)

本文探讨了C++中代码复用的多种方法,包括使用包含、私有继承及保护继承等方式实现has-a关系,以及如何利用虚基类解决多重继承带来的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.包含

C++的一个主要目标就是促进代码复用。公有继承是实现复用的机制之一。但这并非唯一机制,还有其他机制:

(1)类成员本身是另一个类对象,这种方法称为包含(containment),组合(composition)或层次化(layering)。

(2)使用私有继承或保护继承。

以上两种方法,用于实现has-a关系,即新的类包含另一个类对象。

(3)函数模板,类模板。类模板使我们能够使用通用的术语定义类。

1.1包含对象成员的类--Student类

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

现在我们来设计一个student类,使用一个string类表示姓名,valarry<double>来表示分数。这就是运用到类包含了。

#ifndef _STUDENT_H_
#define _STUDENT_H_
#include<iostream>
#include<string>
#include<valarray>
class Student
{
private:
	typedef std::valarray<double> ArrayDb;
	std::string name; //contain object
	ArrayDb scores; //contain object
	//private method for scores output
	std::ostream & arr_out(std::ostream & os)const;
public:
	Student():name("Null Student"),scores(){}
	Student(const std::string & s):name(s),scores(){}
	explicit Student(int n):name("Nully"),scores(n){}  //使用explict防止单参数构造函数的隐式转换
	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 std::string &s, const double * pd, int n):name(s),scores(pd, n){}
	~Student(){}
	double average() const;  //使用const限制方法修改数据,等等,根本原因:在编译阶段出现错误优于在运行阶段出现错误
	const std::string & Name() const;
	double & operator[](int i);
	double operator[](int i) const;
	friend std::istream & operator >> (std::istream & is, Student & stu);  //必须是友元,才能访问name成员
	friend std::istream & getline(std::istream & is, Student & stu);
	friend std::ostream & operator << (std::ostream & os, const Student & stu);
};
#endif

下面对这个类的方法进行定义:

#include"student.h"
using std::ostream;
using std::istream;
using std::endl;
using std::string;

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];
}

double Student::operator[](int i) const
{
	return scores[i];	
}

ostream & Student::arr_out(ostream & os) const
{
	int lim = scores.size();
	if(lim > 0)
	{
		for(int i = 0; i < lim; i++)
		{
			os<<scores[i]<<" ";
			if(i % 5 == 4)
				os<<endl;
		}
		if(lim % 5 != 0)
			os<<endl;
	}
	else
		os<<"empty array";
	return os;
}

istream & operator>>(istream & is, Student & stu)
{
	is>>stu.name;
	return is;
}

istream & getline(istream & is, Student & stu)
{
	getline(is, stu.name);
	return is;
}

ostream & operator<<(ostream & os, const Student & stu)
{
	os<<"scores for"<<stu.name<<":\n";
	stu.arr_out(os);
	return os;
}

测试文件:

#include<iostream>
#include"student.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<<"\n Student list : \n";
	for(i = 0; i < pupils; i++)
	{
		cout<<ada[i].Name()<<endl;
	}
	cout<<"\n Result:";
	for(i = 0; i < pupils; i++)
	{
		cout<<endl<<ada[i];  //重载操作符
		cout<<"averave: "<<ada[i].average()<<endl;
	}
	cout<<"Done\n";
	while(1);
	return 0;
	/*
	Please enter the student name:hongzong.lin
please enter 5 quiz scores:
99 88 77 66 100
Please enter the student name:dizong.lin
please enter 5 quiz scores:
99 11 99 100 222
Please enter the student name:bingzong.lin
please enter 5 quiz scores:
11 22 33 44 55

 Student list :
hongzong.lin
dizong.lin
bingzong.lin

 Result:
scores forhongzong.lin:
99 88 77 66 100
averave: 86

scores fordizong.lin:
99 11 99 100 222
averave: 106.2

scores forbingzong.lin:
11 22 33 44 55
averave: 33
Done
	*/
}

void set(Student & sa, int n)
{
	cout<<"Please enter the student 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;
}

上面这个例子,可以看到包括包含,重载操作符等,student通过自身方法调用包含类的方法,验证了获得实现,未获接口.

2.私有继承

现在再来看看has-a的另一种实现--->私有继承.私有继承使得基类的公有成员和保护成员都将成为派生类的私有成员.这说明基类方法不能成为派生对象的公有接口一部分.但可以在派生对象的成员函数中使用他们.而使用公有继承,派生类能够继承接口,是is-a关系的一部分.简而言着,私有继承,派生类不继承基类的接口,获得实现,但不获接口.这样的特性和包含相同.

2.2Student采用私有继承设计

与包含版本的主要区别
(1)两个对象成员,包含版本显示命名name和scores; 现在私有继承提供了两个无名称的子对象成员.不能使用name和scores显示命名了.需要用类名.std::string, ArrayDb
(2)使用包含时,用对象名来调用方法,私有继承使用类型和作用域解析操作符来调用方法.
(3)私有继承时,string没有名称,使用强制类型转换(const string&)*this.
引用student不会自动转换为string的根本原因在于:在私有继承中,在不进行显示类型转换的情况下,不能将指向派生类的引用或指针赋给基类引用或指针.
#ifndef _STUDENTI_H_
#define _STUDENTI_H_
#include<iostream>
#include<string>
#include<valarray>
class Student:private std::string,private std::valarray<double>
{
private:
	typedef std::valarray<double> ArrayDb;
	std::ostream & arr_out(std::ostream & os) const;
public:
	Student():std::string("NULL student"),ArrayDb(){}
	Student(const std::string & s, int n):std::string(s),ArrayDb(n){} //使用类名进行声明
	Student(int n):std::string("NULLY"),ArrayDb(n){}
	Student(const std::string & s, const ArrayDb & a):std::string(s),ArrayDb(a){}
	Student(const std::string & s, const ArrayDb n):std::string(s),ArrayDb(n){}
	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;

	friend std::istream & operator >>(std::istream & is, Student & stu);
	friend std::istream & getline(std::istream & is, Student &stu);
	friend std::ostream & operator <<(std::ostream & os, const Student & stu);
};
#endif
定义文件
#include"studenti.h"
using std::ostream;
using std::endl;
using std::istream;
using std::string;

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; //强制类型转换为string引用
}

double & Student::operator[](int i)
{
	return ArrayDb::operator[](i); //使用类方法
}

double Student::operator[](int i) const
{
	return ArrayDb::operator[](i);
}

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;
}

istream & operator>>(istream & is, Student & stu)
{
	is>>(string & )stu;
	return is;
}

istream & getline(istream & is, Student & stu)
{
	getline(is, (string &)stu);
	return is;
}

ostream & operator<<(ostream & os, const Student & stu)
{
	os<<"Scores for "<<(const string &)stu<<":\n";
	stu.arr_out(os);
	return os;
}
再来看看测试文件:
#include<iostream>
#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)};
	for(int i = 0; i < pupils; i++)
	{
		set(ada[i], quizzes);
	}
	cout<<"\nstudent list:\n";
	for(int i = 0; i < pupils; i++)
	{
		cout<<ada[i].Name()<<endl;
	}
	cout<<"\n results:";
	for(int i = 0; i < pupils; i++)
	{
		cout<<endl<<ada[i];
		cout<<"average "<<ada[i].average()<<endl;
	}
	cout<<"Done.\n";
	while(1);
	return 0;
	/*
	please enter the student name:
hongzong.lin
please eneter 5 quiz scores:
99 88 77 66 55
please enter the student name:
dizong.lin
please eneter 5 quiz scores:
99 88 77 66 44
please enter the student name:
bingzong.lin
please eneter 5 quiz scores:
99 88 77 55 22

student list:
hongzong.lin
dizong.lin
bingzong.lin

 results:
Scores for hongzong.lin:
99 88 77 66 55
average 77

Scores for dizong.lin:
99 88 77 66 44
average 74.8

Scores for bingzong.lin:
99 88 77 55 22
average 68.2
Done.

	*/
}

void set(Student & sa, int n)
{
	cout<<"please enter the student name:\n";
	getline(cin, sa);
	cout<<"please eneter "<<n<<" quiz scores:\n";
	for(int i = 0; i < n; i++)
	{
		cin>>sa[i];
	}
	while(cin.get() != '\n')
		continue;
<h2>}</h2>
以上,对比下,我们是使用包含还是私有继承呢,这个需要具体问题具体分析,呵呵.废话.但大多数倾向于使用包含:
(1)易于理解。
(2)继承容易引发许多问题。
(3)包含能够包括多个同类的子对象。比如需要3个string对象。
所以,通常应使用包含来建立has-a关系,如果新类需要访问原有类的保护成员,或许要重新定义虚函数,则应当使用私有继承.

2.2保护继承

保护继承是私有继承的变体.基类的公有成员和保护成员都将成为派生类的保护成员.
(1)使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中变成私有方法.
(2)使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类还可以使用它们。
下面表格说明不同种类的继承
特征公有继承保护继承私有继承
公有成员变成派生类公有成员派生类保护成员派生类私有成员
保护成员变成派生类保护成员派生类保护成员派生类私有成员
私有成员变成只能通过基类接口访问只能通过基类接口访问只能通过基类接口访问
能够隐式向上转换是(但只能在派生类中)
(3)使用using重新定义访问权限
A.使用派生类的方法调用基类的方法,例如:
double Student::sum() const
{
    return std::valarray<double>::sum();
}
B.使用using声明,指出派生类可以使用特定的基类成员,即使采用的是私有派生。
class Student:private std::string,private std::valarray<double>
{
    public:
    using std::valarray<double>::min;
    using std::valarray<double>::max; //注意,不用圆括号,呵呵
};
上述声明使得valarray<double>::min()/max() 像student的公有方法一样.:
cout<<sa[i].max()<<endl;
using声明只适用于继承,不适用于包含..

3.多重继承

我们来看一个例子,定义一个抽象基类Worker,并使用他派生出Waiter和Singer类.然后,使用MI从Waiter和Singer类派生出SingingWaiter类,这种情况下,Singer和Waiter都继承了一个Worker组件,因此,SingingWaiter将包含两个Worker组件.这将会导致问题,比如
SingingWaiter ed;
worker * pw = &ed;
把派生类对象的地址赋给基类指针时,会导致二义性,因为ed中包含两个worker对象,有两个地址可供选择,他并不知道指向哪一个.所以应使用类型转换来指定对象:
worker* pw1 = (Waiter*)&ed;
worker* pw2 = (Singer*)&ed;
这将使得使用基类指针来引用不同的对象(多态性)复杂化.MI引入了一种新技术,虚基类.virtual base class.

3.1虚基类

虚基类使得从多个类(他们的基类相同)派生出的对象只继承了一个基类对象.语法如下:
class Singer:virtual public Worker{...};
class Waiter:public virtual Worker{...};//public virtual次序无关紧要
class SingingWaiter:public Singer,public Waiter {...};
从本质上来说,继承的Singer和Waiter对象共享一个Worker对象,而不是各自引入自己的Worker对象拷贝.
(1)新的构造函数规则
使用虚基类,需要采用一种新的构造函数方法.
A.对于非虚基类,唯一可以出现在初始化列表中的构造函数是即时基类构造函数.如下:
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){m = m;}
};
class C:public B
{
	int c;
public:
	/*这里可以看出,C类调用B类的构造函数,B类自动回去调用A类的构造函数,而虚基类则禁止这样的自动调用*/
	C(int q = 0, int m = 0, int n = 0):B(m, n){c = q;}
};
B.对于虚基类,像上面的这样自动调用A类构造函数的,将不生效.应该如下:
SingingWaiter(const Worker & wk, int p = 0, int v = Singer::other):Worker(wk),Waiter(wk, p),Singer(wk, v){}
对于虚基类,必须这样做,对于非虚基类,这是非法的.如果类有间接虚基类,则除非只需使用该虚基类的默认构造函数,否则必须显示的调用该虚基类的某个构造函数.
以上,在祖先相同时,使用MI必须引入虚基类,并修改构造函数的初始化列表的规则.另外,如果在编写这些类时没有考虑到MI,则还可能需要重新编写他们.下面贴个程序例子,呵呵,坚持抄完C++primer
mi.h头文件
#if 1
#ifndef _MI_H_
#include<string>
class Worker
{
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:public virtual Worker//继承虚基类
{
private:
	int panache;
protected:
	void Get();
	void Data()const;
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:public virtual Worker//继承虚基类
{
protected:
	enum{other, alto, contrlto, soprano, bass, baritone, tenor};
	enum{Vtypes = 7};
	void Data()const;
	void Get();
private:
	static char *pv[Vtypes];
	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;
};
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 & ws, int p  = 0):Worker(ws),Waiter(ws, p),Singer(ws){}//有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的,上面已经看到.
	void Set();
	void Show()const;
};
#endif
#endif
类定义文件
#include"mi.h"
#include<iostream>
using namespace std;

Worker::~Worker(){}
void Worker::Data()const //output emplyee name and id
{
	cout<<"Name:"<<fullname<<endl;
	cout<<"Employee ID:"<<id<<endl;
}
void Worker::Get() //input the employee name and id
{
	getline(cin, fullname);
	cout<<"Enter worker's ID:";
	cin>>id;
	while(cin.get() != '\n')
		continue;
}
void Waiter::Set()  //set worker and waiter 
{
	cout<<"Enter waiter's name:";
	Worker::Get(); //input worker name and id
	Get();  //input waiter panache
}
void Waiter::Show()const
{
	cout<<"Category:waiter\n";
	Worker::Data();
	Data();
}
void Waiter::Data()const   //only output waiter's member, not including the base class member
{
	cout<<"pananche rating :"<<panache<<endl;
}
void Waiter::Get()  //only input waiter's member,not including the base class member
{
	cout<<"Enter waiter's panache rating: ";
	cin>>panache;
	while(cin.get() != '\n')
		continue;
}
//singer method
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<<"Catagory: singer\n";
	Worker::Data();
	Data();
}
void Singer::Data()const  //not including the base class member
{
	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<<endl;
	}
	cin>>voice;
	while(cin.get() != '\n')
		continue;
}
void SingingWaiter::Data()const //only include singer and waiter,not including virtual class Worker
{
	Singer::Data();
	Waiter::Data();
}

void SingingWaiter::Get()  //only include singer and waiter,not including virtual class Worker
{
	Singer::Get();
	Waiter::Get();
}
void SingingWaiter::Set()
{
	cout<<"Enter singing waiter's name: ";
	Worker::Get();
	Get();
}
void SingingWaiter::Show()const  //singingWaiter重写show().
{
	cout<<"Category: singing waiter\n";
	Worker::Data();
	Data();
}
测试文件
#include<iostream>
#include<cstring>
#include"mi.h"
const int SIZE = 5;
int main()
{
	using namespace std;
	Worker* lolas[SIZE];
	int ct;
	for(ct = 0; ct < SIZE; ct++)
	{
		char choice;
		cout<<"Enter the employee category:\n"<<"w:waiter s:singer t:singingwaiter q:quit\n";
		cin>>choice;
		while(strchr("wstq", choice) == NULL)
		{
			cout<<"please enter a w, s, t, 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<<"\n here is your staff:\n";
	for(int i = 0; i < ct; i++)
	{
		cout<<endl;
		lolas[i]->Show();
	}
	for(int i = 0; i < ct; i++)
	{
		delete lolas[i];
	}
	cout<<"Bye. \n";
	while(1);
	return 0;
	/*
	Enter the employee category:
w:waiter s:singer t:singingwaiter q:quit
w
Enter waiter's name:hongzong.lin
Enter worker's ID:11
Enter waiter's panache rating: 11
Enter the employee category:
w:waiter s:singer t:singingwaiter q:quit
s
Enter singer's name: dizong.lin
Enter worker's ID:22
Enter number for singer's vocal range:
0:other 1:alto 2:contralto 3:soprano
4:bass 5:baritone 6:tenor
2
Enter the employee category:
w:waiter s:singer t:singingwaiter q:quit
t
Enter singing waiter's name: bingzai.lin
Enter worker's ID:33
Enter number for singer's vocal range:
0:other 1:alto 2:contralto 3:soprano
4:bass 5:baritone 6:tenor
3
Enter waiter's panache rating: 33
Enter the employee category:
w:waiter s:singer t:singingwaiter q:quit
q

 here is your staff:

Category:waiter
Name:hongzong.lin
Employee ID:11
pananche rating :11

Catagory: singer
Name:dizong.lin
Employee ID:22
vocal range: contralto

Category: singing waiter
Name:bingzai.lin
Employee ID:33
vocal range: soprano
pananche rating :33
Bye.

	*/
}

3.2MI小结

(1)非虚基类,假如一个类从两个不同的类那里继承了两个同名的成员,则需要在派生类中使用限定符来区分他们.否通则会产生二义性.
(2)虚基类继承,使用关键字virtual,基类就成为虚基类继承.
A.有间接虚基类的派生类包含直接调用间接基类构造函数的构造函数,这对于间接非虚基类来说是非法的,上面已经看到.
B.通过优先规则解决名称二义性.
MI会增加编程的复杂度,需要注意的是,在必要时,对继承的名称进行限定,这种复杂度是由多条路径继承同一个基类引起的.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值