C++继承与多态六:虚基类、虚继承、多重继承中菱形继承问题及解决方案

一、虚基类和虚继承

虚基类:被虚继承的类,就称为虚基类。

virtual作用:
1.virtual修饰了成员方法是虚函数。
2.可以修饰继承方式,是虚继承。被虚继承的类就称为虚基类。

vfptr:一个类有虚函数,这个类生成的对象就有vfptr,指向vftable。
vbptr:在派生类中从基类虚继承而来。
vftable:第一行为向上偏移量,第二行为虚基类指针离虚基类内存的偏移量。
vbtable:存放的RTTI指针,指向运行时RTTI信息与虚函数地址。

我们来看一个例子:

class A
{
public:
private:
	int ma;
};

class B : public A
{
public:
private:
	int mb;
};
//A a; 4个字节
//B b; 8个字节

这里我们的对象a占4个字节,对象8占8个字节。但如果我们给B的继承方式访问限定符前面加了一个virtual关键字。
我们使用命令:cl –d1reportSingleClassLayout[classname] xxx.cpp查看此时的内存布局。
类A被虚继承了,但内存布局没有变化。
在这里插入图片描述
我们再看一下类B,不是之前的8个字节,变为12个字节,多了一个vbptr指针。原来最上面应该为ma与mb,但是现在多了一个vbptr(虚基类指针),ma跑到派生类最后面去了。vbptr指向的是vbtable,vbtable第一行为0,第二行为虚基类指针到虚基类数据的偏移量。
在这里插入图片描述
当我们遇到虚继承时候。要考虑派生类B的内存布局时,首先我们先不考虑虚继承。类B继承了基类的ma,还有自己的mb;当我们基类被虚继承后,基类变为虚基类,虚基类的数据一定要在派生类数据最后面,再在最上面添加一个vbptr。派生类的内存就由这几部分来构成。
在这里插入图片描述
虚基类指针(vbptr)指向虚基类表(vbtable),vbtable第一行为向上的偏移量,因为vbptr在该派生类内存起始部分,因此向上的偏移量为0;第二行为向下的偏移量(vbptr离虚基类数据的偏移量),原来基类的数据放到最后,找ma的时候还是在最开始找,但ma被移动,根据偏移的字节就可以找到。
在这里插入图片描述

二、虚基类和虚继承出错情况分析

那么当我们虚基类指针与虚函数指针在一起出现的时候会发生什么呢?
调用是没有问题的,但是delete会出错。

class A
{
public:
	virtual void func()
	{
		cout << "call A::func" << endl;
	}
private:
	int ma;
};

class B : virtual public A
{
public:
	void func()
	{
		cout << "call B::func()" << endl;
	}
private:
	int mb;
};

int main()
{
	//基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址。
	A *p = new B();//B::vftable
	p->func();
	delete p;

	return 0;
}

如图:调用成功,但delete时会出错。
在这里插入图片描述
我们分析一下:
B的内存布局:B首先从A中获取vfptr与ma,B中还有自己的mb;此时A被虚继承,A中所有的东西都移动到派生类最后面,最上面补一个vbptr,vbptr指向vbtable,vfptr指向vftable;我们基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址。普通情况下,派生类内存布局先是基类,在是派生类,基类指针指向派生类对象时,基类指针指向的就是派生类内存的起始部分。但是虚继承下,基类称为虚基类,虚基类的数据在派生类最后面,原地方补上vbptr,此时再用基类指针指向派生类对象时候,基类指针永远指向派生类基类部分的起始地址。虚基类一开始就是vfptr,能够用p指向的对象访问vfptr与vftable的原因。释放内存时候出错,因为对象开辟是在最上面即绿色部分,但是p所持有的是虚基类的地址,delete时从虚基类起始地址delete,因此出错。
在这里插入图片描述
命令验证如下:
在这里插入图片描述
我们验证一下内存地址:

class A
{
public:
	virtual void func()
	{
		cout << "call A::func" << endl;
	}
	void operator delete(void *ptr)
	{
		cout << "operator delete p:" << ptr << endl;
		free(ptr);
	}
private:
	int ma;
};

class B : virtual public A
{
public:
	void func()
	{
		cout << "call B::func()" << endl;
	}

	void* operator new(size_t size)
	{
		void *p = malloc(size);
		cout << "operator new p:" << p << endl;
		return p;
	}
private:
	int mb;
};
//A a; 4个字节
//B b; 8个字节
int main()
{
	A *p = new B();//B::vftable
	cout << "main p:" << p << endl;
	p->func();
	delete p;

	return 0;
}

在这里插入图片描述
0115D9B8为分配的内存的起始地址,我们用基类指针指向派生类对象一定是指向派生类内存基类的起始部分:0115D9C0刚好比0115D9B8多了8个字节,是vbptr与mb,但是delete时候从0115D9C0开始释放,因此崩溃。
在这里插入图片描述
Windows的VS下这样写会出错,但是Linux下的g++delete时会自动偏移到new内存的起始部分,进行内存free(),不会出错。

我们如果在栈上开辟内存,基类指针指向派生类对象,出了作用域自己进行析构,这样是没有问题的。

B b;
A *p = &b;//B::vftable
cout << "main p:" << p << endl;
p->func();

在这里插入图片描述

三、菱形继承问题

多重继承:可以复用多个基类的代码到派生类中

但是多重继承中也会出现问题:我们来看一个经典的问题:
菱形继承问题:会导致派生类有多份间接基类的数据,可以采用虚继承来解决。 A为B、C的基类,B从A单继承而来,C从A也是单继承而来;D是B和C多继承而来,D有两个基类分别为B和C。A称为D的间接基类,D也有A的数据。
       假设A中有ma变量,B从A继承来并且有自己的mb,C从A继承来并且有自己的mc,D从B与C多继承而来,从B继承来了ma与mb,从C继承来了ma与mc,D也有自己的属性md;那么就出现了问题,我们的间接基类D有多份ma属性,这就是菱形继承问题。
在这里插入图片描述
当然,我们多重继承还会出现别的问题:
半圆形继承问题: B从A单一继承而来,C有一个基类B而且同时还从A继承而来。A到B为单继承,C为多继承。
       假设ma为类A的属性,B从A继承而来,B有自己的mb;C从B与A继承而来,C有自己的mc,又从A继承来了ma,从B继承来了mb与mc;产生了菱形继承同样的问题,C中出现了两个ma属性。
在这里插入图片描述
多重继承虽然可以复用多个基类的代码到派生类中,但是会出现许多问题,因此C++开源代码上很少见到多重继承。

我们用代码来实现一下菱形继承问题:

class A
{
public:
	A(int data):ma(data)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	} 
protected:
	int ma;
};

class B : public A
{
public:
	B(int data):A(data), mb(data)
	{
		cout << "B()" << endl;
	}
	~B()
	{
		cout << "~B()" << endl;
	} 
protected:
	int mb;
};

class C : public A
{
public:
	C(int data):A(data), mc(data)
	{
		cout << "C()" << endl;
	}
	~C()
	{
		cout << "~C()" << endl;
	} 
protected:
	int mc;
};

class D : public B , public C
{
public:
	D(int data):B(data), C(data), md(data)
	{
		cout << "D()" << endl;
	}
	~D()
	{
		cout << "~D()" << endl;
	} 
protected:
	int md;
};

int main()
{
	D d(10);

	return 0;
}

我们画一下d对象的内存布局。
在这里插入图片描述
D能看见B,C与md,所以D在构造时调用B,C的构造及ma的初始化。ma的初始化在B与C的构造函数中进行,因此D内存为20个字节。打印一下:
在这里插入图片描述
先是A的构造,B的构造,又是A的构造,C的构造,最后是D的构造;析构顺序与其相反。我们发现,D这个派生类中调用了两次A的构造,数据重复,浪费内存,这种情况必须被杜绝。

那么如何处理这种问题呢?就需要虚继承来处理了。
所有从A继承而来的地方都采用虚继承,A就为虚基类。
此时:B从A虚继承而来,A为虚基类,A::ma移动到派生类最后面,在A::ma位置上补一个vbptr;C也是从A虚继承而来,A::ma移动到派生类最后面,但发现已经有一份同样的虚基类的数据,那么C的A::ma丢弃,在A::ma位置存放vbptr。此时派生类中只有一份基类A::ma的数据,以后访问都是同一个ma;同时ma的初始化由D来负责。虚继承就可以解决多重继承中的菱形继承与半圆形继承出现的问题了。
在这里插入图片描述
代码修改为如下:

class A
{
public:
	A(int data):ma(data)
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	} 
protected:
	int ma;
};

class B : virtual public A
{
public:
	B(int data):A(data), mb(data)
	{
		cout << "B()" << endl;
	}
	~B()
	{
		cout << "~B()" << endl;
	} 
protected:
	int mb;
};

class C : virtual public A
{
public:
	C(int data):A(data), mc(data)
	{
		cout << "C()" << endl;
	}
	~C()
	{
		cout << "~C()" << endl;
	} 
protected:
	int mc;
};

class D : public B , public C
{
public:
	D(int data):A(data), B(data), C(data), md(data)
	{
		cout << "D()" << endl;
	}
	~D()
	{
		cout << "~D()" << endl;
	} 
protected:
	int md;
};

int main()
{
	D d(10);

	return 0;
}

打印结果:修改成功;A、B、C、D各初始化与析构一次。
在这里插入图片描述

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我会根据您的要求进行设计。首先,我们需要确定公司信息管理系统需要管理哪些信息。一般而言,一个小型公司的信息包括员工信息、部门信息、工资信息等。我们可以先定义一个类,称为Company,它包括了公司的本信息,比如公司名称、地址等。 ```c++ class Company { public: Company(const std::string& name, const std::string& address) : name_(name), address_(address) {} virtual ~Company() {} virtual std::string GetInfo() const { return "Company name: " + name_ + " Address: " + address_; } private: std::string name_; std::string address_; }; ``` 在Company的础上,我们可以定义Employee(员工)、Department(部门)和Salary(工资)类。这三个类都是从Company类派生而来。 ```c++ class Employee : virtual public Company { public: Employee(const std::string& name, const std::string& address, const std::string& id) : Company(name, address), id_(id) {} virtual ~Employee() {} virtual std::string GetInfo() const { return Company::GetInfo() + " Employee ID: " + id_; } private: std::string id_; }; class Department : virtual public Company { public: Department(const std::string& name, const std::string& address, const std::string& depName) : Company(name, address), depName_(depName) {} virtual ~Department() {} virtual std::string GetInfo() const { return Company::GetInfo() + " Department Name: " + depName_; } private: std::string depName_; }; class Salary : virtual public Company { public: Salary(const std::string& name, const std::string& address, double salary) : Company(name, address), salary_(salary) {} virtual ~Salary() {} virtual std::string GetInfo() const { return Company::GetInfo() + " Salary: " + std::to_string(salary_); } private: double salary_; }; ``` 我们可以看到,这三个类都重载了类Company的GetInfo()函数,使得每个类可以返回自身的信息。 接下来,我们需要定义一个管理系统类,称为CompanyManager。该类可以对公司信息进行添加、修改、查询等操作。 ```c++ class CompanyManager { public: void AddEmployee(const Employee& employee) { employees_.push_back(employee); } void AddDepartment(const Department& department) { departments_.push_back(department); } void AddSalary(const Salary& salary) { salaries_.push_back(salary); } std::vector<Employee> SearchEmployee(const std::string& id) const { std::vector<Employee> result; for (const auto& employee : employees_) { if (employee.GetID() == id) { result.push_back(employee); } } return result; } std::vector<Department> SearchDepartment(const std::string& depName) const { std::vector<Department> result; for (const auto& department : departments_) { if (department.GetDepartmentName() == depName) { result.push_back(department); } } return result; } std::vector<Salary> SearchSalary(double salary) const { std::vector<Salary> result; for (const auto& s : salaries_) { if (s.GetSalary() == salary) { result.push_back(s); } } return result; } private: std::vector<Employee> employees_; std::vector<Department> departments_; std::vector<Salary> salaries_; }; ``` 在CompanyManager,我们定义了三个向量分别存储员工、部门和工资信息。同时,我们还定义了三个查询函数,分别用于查询员工、部门和工资信息。 最后,我们可以在main函数测试我们的程序。 ```c++ int main() { CompanyManager company; Employee e1("Tom", "Beijing", "001"); Employee e2("Jerry", "Shanghai", "002"); Department d1("HR", "Beijing", "HR Department"); Department d2("IT", "Shanghai", "IT Department"); Salary s1("Tom", "Beijing", 10000.0); Salary s2("Jerry", "Shanghai", 12000.0); company.AddEmployee(e1); company.AddEmployee(e2); company.AddDepartment(d1); company.AddDepartment(d2); company.AddSalary(s1); company.AddSalary(s2); auto employeeList = company.SearchEmployee("001"); for (const auto& e : employeeList) { std::cout << e.GetInfo() << std::endl; } auto departmentList = company.SearchDepartment("IT Department"); for (const auto& d : departmentList) { std::cout << d.GetInfo() << std::endl; } auto salaryList = company.SearchSalary(12000.0); for (const auto& s : salaryList) { std::cout << s.GetInfo() << std::endl; } return 0; } ``` 这样,我们就完成了一个小型的公司信息管理系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值