【C++】-- 继承

目录

一、什么是继承

1.继承定义 

2.继承基类成员访问方式

二、基类和派生类互相赋值转换

1.赋值规则

2.切片

(1)子类对象赋值给父类对象

(2)子类对象赋值给父类指针

(3)子类对象赋值给父类引用

三、继承中的作用域

1.父类和子类都有独立的作用域

2.隐藏

3.如果是成员函数的隐藏,只需要函数名相同就构成隐藏

4.继承中最好不要定义同名成员

四、子类的默认成员函数

1.规则①  

2.规则②

3.规则③

4.规则④

五、继承和友元

六、继承和静态成员

七、菱形继承

 1.单继承

2.多继承

3.菱形继承

八、菱形虚拟继承

1.虚继承

2.虚继承的解决数据冗余和二义性的原理

(1)普通继承 

(2)虚继承

九.继承和组合


一、什么是继承

1.继承定义 

继承机制是面向对象程序设计使代码可以复用的最重要的手段,该机制自动地为一个类提供来自另一个类的操作和数据结构。只需要在新类中定义已有的类中没有的成分来建立一个新类。继承是类设计层次的复用。

继承包括成员变量和成员函数。

如下所示,Animal类的成员变量有age,成员函数有eat( )和sleep( )。Dog类的成员变量有legs,继承了Animal的age成员变量、成员函数eat( )和sleep( ),并且还定义了新的成员变量legs和新的成员函数bark( ),因此Dog类有2个成员变量:age和legs,3个成员函数:eat( )、sleep( )、bark( )。

Animal叫做父类,也叫做基类。Dog叫做子类,也叫作派生类。

 转化成代码:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;
class Animal
{
public:
	void eat()
	{}

	void sleep()
	{}

private:
	int _age = 1;
};


class Dog :public Animal
{
public:
	void bark()
	{}

public:
	int _legs = 4;
};

int main()
{
	Animal a;
	Dog d;

	return 0;
}

F10监视,可以看到对象a的成员变量只有一个,age。对象d的成员变量有两个,一个是继承自Animal的age,一个是Dog类自己定义的成员变量legs:

2.继承基类成员访问方式

父类有3种访问方式:public、protected、private,子类继承父类也有3种继承方式:public、protected、private。组合起来一共有9种:

 总结:

① 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
② 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
③ 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
④ 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,最好显式写出继承方式。
⑤ 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用
protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

关于什么叫做不可见:

假如将父类成员改为private,那么子类继承了父类的私有成员,内存上,子类对象有这个成员,但是语法上不允许访问:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;
class Animal
{
public:
	void eat()
	{}

	void sleep()
	{}

private://访问方式改为private
	int _age = 1;
};


class Dog :public Animal
{
public:
	void bark()
	{}

	void func()
	{
		//cout << _age << endl;//语法上不可以访问,但是内存上,子类对象有父类的私有成员
	}
public:
	int _legs = 4;
};

int main()
{
	Animal a;
	Dog d;

	return 0;
}

F10监视:

二、基类和派生类互相赋值转换

1.赋值规则

(1)派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。即把派生类中父类那部分切来赋值过去。(子可以给父)
(2)基类对象不能赋值给派生类对象。(父不能给子)
(3)基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。

2.切片

(1)子类对象赋值给父类对象

子可以赋值给父,但父不能赋值给子: 

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
#include<string>
using namespace std;

class Person
{
public:
	string _name;
	string _sex;
	int _age;
};

class Student:public Person
{
public:
	int _No;
};

int main()
{
    //1.子类对象赋值给父类对象
    //定义对象时,内置类型不处理,自定义类型会调用自己默认的构造函数
	Person p;
	Student s;
    
    s._name = "Delia";
    s._sex = "女";
    s._No = 21356;

	p = s;//子可以给父
	//s = p;编译报错,父不能给子
	return 0;
}

F10监视: 像拷贝构造一样,把子类中父类那部分切片赋值给父类对象,也就是把子类成员依次赋值给父类对象:

 从string的buffer的地址可以看出,string调用了深拷贝,因为s和p的_name虽然内容都是"Delia",但是两个Delia的地址不同:

 

假如父类和子类有同名成员变量,那么切片赋值时,只会切割从父类继承的成员,即把父类的成员变量切过去,因为父类并不拥有子类的成员变量,因此切片后,父类的成员变量的值为父类的成员变量的值。如,父类和子类拥有同名成员变量_No:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
#include<string>
using namespace std;

class Person
{
public:
	string _name;
	string _sex;
	int _age;
	int _No = 999;
};

class Student :public Person
{
public:
	int _No;
};

int main()
{
	//1.子类对象赋值给父类对象
	//定义对象时,内置类型不处理,自定义类型会调用自己默认的构造函数
	Person p;
	Student s;

	s._name = "Delia";
	s._sex = "女";
	s._No = 21356;

	p = s;//子可以给父
	cout << "p._No : " << p._No << endl;

	return 0;
}

(2)子类对象赋值给父类指针

① 子类对象可以赋值给父类指针

int main()
{
	//2.子类对象赋值给父类指针
	Person *p;
	Student s;

	s._name = "Delia";
	s._sex = "女";
	s._No = 21356;

	p = &s;

	return 0;
}

F10-监视:发现子类对象s给父类指针p赋值成功,子类对象初始化了父类对象的一部分,_name和_sex,因此子类对象赋值给父类对象后,父类对象只能初始化子类对象赋值的成员,对于其他成员,如果是内置类型不处理,_age为随机值,自定义类型会调用它的默认构造函数:

② 若父类指针要赋值给子类指针,必须将父类指针强转为子类指针:

int main()
{

	//父类指针赋值给子类指针
	Person p;//父类对象
	Student *s;//子类指针
	
	p._name = "Delia";
	p._sex = "女";
	p._age = 6;

	Person* pp = &p;//父类指针

	s = (Student*)pp;//将父类指针强转为子类型指针后,再赋值给子类型指针

	return 0;
}

F10监视:父类指针成功赋值给子类指针: 

③ 指向父类对象的父类指针,强转为子类指针后赋值给子类指针可能会造成访问越界

int main()
{
	//指向父类对象的父类指针,强转为子类指针后赋值给子类指针可能会造成访问越界
	Person p;
	
	p._name = "Delia";
	p._sex = "女";
	p._age = 7;
	//s._No = 21530;

	Person* pp = &p;

	Student* ss = (Student*)pp;
	//ss->_No = 6;访问越界造成程序崩掉

	return 0;
}

 F10监视:发现赋值成功了

(3)子类对象赋值给父类引用

int main()
{

	//子类对象赋值给父类引用
	Student s;
	
    s._name = "Delia";
	s._sex = "女";
	s._age = 7;
	s._No = 21369;

	Person& rp = s;//子类对象赋值给父类引用

	return 0;
}

 F10监视:父类引用被成功赋值了:

三、继承中的作用域

1.父类和子类都有独立的作用域

 如下:子类中初始化的变量的值不受父类中初始化的变量的值的影响。

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;

class Shape
{
public:
	void setWidth(float w)
	{
		_width = w;
	}

	void setHeight(float h)
	{
		_height = h;
	}

	void ShapePrintf()
	{
		cout << "Shape : _width" << ":" << _width << endl;
		cout << "Shape : _height" << ":" << _height << endl;
	}

public:
	float _width = 10;
	float _height = 10;
};

class Rectangle : public Shape
{
public:
	float Area()
	{
		return _width * _height;
	}

	void RectanglePrintf()
	{
		_width = 20;
		_height = 30;
		cout << "Rectangle : _width" << ":" << _width << endl;
		cout << "Rectangle : _height" << ":" << _height << endl;
	}

};

int main()
{
	Shape s;
	s.ShapePrintf();

	Rectangle r;
	r.RectanglePrintf();

    return 0;
}

父类的_width和_height是父类中初始化的值,子类的_width和_height是子类中初始化的值:

2.隐藏

子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 父类::父类成员 显式访问父类的同名成员)

在父类和子类中定义同名成员_width: 

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;

class Shape
{
public:
	void setWidth(float w)
	{
		_width = w;
	}

	void setHeight(float h)
	{
		_height = h;
	}

	void ShapePrintf()
	{
		cout << "Shape : _width" << ":" << _width << endl;
		cout << "Shape : _height" << ":" << _height << endl;
	}

public:
	float _width = 10;
	float _height = 10;
};

class Rectangle : public Shape
{
public:
	float Area()
	{
		return _width * _height;
	}

	void RectanglePrintf()
	{
		cout << "Rectangle : _width" << ":" << _width << endl;
		cout << "Rectangle : _height" << ":" << _height << endl;
	}
public:
	float _width = 50;//子类重定义同名成员
};

int main()
{
	Shape s;
	s.ShapePrintf();

	Rectangle r;
	r.RectanglePrintf();

    return 0;

}

 发现子类打印的_width的值是子类初始化的值,这是因为子类和父类的成员同名时,构成了隐藏,子类成员屏蔽了父类对同名成员的访问:

 那么子类对象r中到底有一个_width还是有两个_width呢?F10-监视:发现r中有两个_width,根据刚才的打印发现_width访问的是子类的_width 50。那么说明代码根据就近原则,在子类中优先访问了子类的同名成员。

 如果就想在子类中访问父类的同名成员呢?需要用::访问限定符指定访问的是父类的同名成员,即父类::父类成员

将上述代码中RectanglePrintf()函数中的

        cout << "Rectangle : _width" << ":" << _width << endl;

改为显式访问父类成员:

        cout << "Shape : _width" << ":" << Shape::_width << endl;

 这就打印了父类的同名成员:

3.如果是成员函数的隐藏,只需要函数名相同就构成隐藏

由于子类继承父类的所有成员,因此,子类对象可以直接访问父类的成员,那么子类对象r可以访问父类Shape类的成员setHeight():

int main()
{
	Rectangle r;
	r.setHeight(100);
	cout << "r._height : " << r._height << endl;
	
	return 0;
}

访问成功:

 如果子类成员函数名和父类成员函数名相同,不管参数相不相同,都会构成隐藏,即子类成员屏蔽父类同名函数的访问,子类对象不可以访问父类的同名称成员函数:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;

class Shape
{
public:
	void setWidth(float w)
	{
		_width = w;
	}

	void setHeight(float h)
	{
		_height = h;
	}

	//和子类成员函数同名
	void printf()
	{
		cout << "Shape : _width" << ":" << _width << endl;
		cout << "Shape : _height" << ":" << _height << endl;
	}

public:
	float _width = 10;
	float _height = 10;
};

class Rectangle : public Shape
{
public:
	float Area()
	{
		return _width * _height;
	}

	//和父类成员函数同名,不论参数和父类同名函数的参数相不相同
	void printf(float width,float height)
	{
		cout << "Rectangle : _width" << ":" << width << endl;
		cout << "Rectangle : _height" << ":" << height << endl;
	}

public:
	float _width = 50;
};

int main()
{
	
	Rectangle r;
	r.printf();
	
	return 0;
}

r.printf()这一行会报错:

这是因为,只要父类函数和子类函数同名,都会构成隐藏,不管参数相不相同。隐藏了父类同名函数之后,子类对象就不能访问父类同名函数了。

4.继承中最好不要定义同名成员

四、子类的默认成员函数

以前学过类的默认成员函数,父类遵从类的默认成员函数规则,那么子类的默认成员函数如何生成呢?

6个默认成员函数中,取地址操作符重载和const修饰的取地址操作符重载不常用,只需要看前4种即可。

写上父类的默认成员函数:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;

class Shape
{
public:
	Shape(float width = 10,float height = 10)
		:_width(width)
		,_height(height)
	{
		cout << "Shape()" << endl;
	}

	Shape(const Shape& s)
		:_width(s._width)
		,_height(s._height)
	{
		cout << "Shape(const Shape& s)" << endl;
	}

	Shape& operator=(const Shape& p)
	{
		cout << "Shape& operator=(const Shape& p)" << endl;
		if (&p != this)
		{
			_width = p._width;
			_height = p._height;
		}

		return *this;
	}

	~Shape()
	{
		cout << "~Shape()" << endl;
	}

	void setWidth(float w)
	{
		_width = w;
	}

	void setHeight(float h)
	{
		_height = h;
	}

	//和子类成员函数同名
	void ShapePrintf()
	{
		cout << "Shape : _width" << ":" << _width << endl;
		cout << "Shape : _height" << ":" << _height << endl;
	}

public:
	float _width;//声明成员变量时不给默认值
	float _height;//声明成员变量时不给默认值
};

class Rectangle : public Shape
{
public:
	float Area()
	{
		return _width * _height;
	}

	void RectanglePrintf(float width,float height)
	{
		cout << "Rectangle : _width" << ":" << width << endl;
		cout << "Rectangle : _height" << ":" << height << endl;
	}

public:
	int _rightAngleCounts;//直角个数,声明成员变量时不给默认值
};

这段代码中, 子类和父类各自有各自的打印函数,并且子类的成员变量变成了直角个数_rightAngleCounts,目的是为了方便理解后面的子类默认成员函数的生成规则。

子类和普通类的默认成员函数不同点:

① 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显式调用。

② 子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。

③ 子类的operator=必须要调用父类的operator=完成父类的赋值。

④ 子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序。

⑤ 子类对象初始化先调用父类构造再调子类构造。

⑥ 子类对象析构清理先调用子类析构再调父类的析构。

1.规则①  

规则① 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显式调用。

由于Ranctangle继承了Shape类的成员变量_width和_height,再加上自己的成员变量_rightAngleCounts,子类一共有3个成员变量,现在写子类的构造函数:

    Rectangle(float width = 20, float height = 20, int rightAngleCounts = 4)
		:_width(width),
		_height(height),
		_rightAngleCounts(rightAngleCounts)
	{
		cout << "Rectangle(float width = 20, float height = 20, int rightAngleCounts = 4)" << endl;
	}

但是编译报错:

这是因为父类有默认构造函数,子类必须调用父类的默认构造函数去初始化父类的那一部分成员,所以当父类有默认构造函数时,子类不用在自己的构造函数内初始化父类的那一部分成员,只初始化自己的成员就可以了:

    Rectangle(int rightAngleCounts = 4)
        :_rightAngleCounts(rightAngleCounts)
	{
		cout << "Rectangle()" << endl;
	}

编译通过。我们知道,默认构造函数分为3种:

(1)我们没写,编译器默认自动生成

(2)我们写的无参默认构造函数

(3)我们写的带参全缺省默认构造函数 

假如父类的构造函数不是以上3种呢?也就是父类没有默认的构造函数呢?子类如何初始化?

父类构造函数写成这样就不是默认构造函数:

    Shape(float width, float height)
		:_width(width)
		,_height(height)
	{
		cout << "Shape()" << endl;
	}

 那么子类构造函数只初始化子类的成员还可行吗?发现编译出错:

当父类没有默认构造函数时,子类的初始化列表必须显式调用父类的默认构造函数:

    Rectangle(float width = 10,float height = 10,int rightAngleCounts = 4)
		:Shape(width, height)
		,_rightAngleCounts(rightAngleCounts)
	{
		cout << "Rectangle()" << endl;
	}

这时候,显式调用父类默认构造函数,是把当作一个整体去调父类的构造函数

int main()
{
	Rectangle r1(6,6,4);

	return 0;
}

监视:都初始化成功了

总结:

(1)父类有默认构造函数,子类只需要在构造函数中初始化子类自己的成员

(2)父类没有默认构造函数,子类要在构造函数初始化列表显式调用父类的构造函数,再初始化子类自己的成员

2.规则②

规则② 子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝初始化。

子类的拷贝构造函数该如何写呢?

调用父类拷贝构造函数时,将子类对象作为参数传给父类拷贝构造函数,这时会发生切片行为,将子类对象中父类成员部分切片赋值给父类成员,完成父类拷贝构造;再将子类自己的成员初始化为拷贝对象的子类成员值:

	Rectangle(const Rectangle& r)
		:Shape(r)//切片,子类对象赋值给父类引用
		, _rightAngleCounts(r._rightAngleCounts)//子类自己的成员初始化为拷贝对象的子类成员值
	{
		cout << "Rectangle(const Rectangle& r)" << endl;
	}
int main()
{
	Rectangle r1(6,6,4);
	Rectangle r2(r1);//拷贝构造
	
	return 0;
}

3.规则

规则子类的operator=必须要调用父类的operator=完成父类的赋值

如果子类的赋值运算符重载函数里只调用子类的operator=,那么会发生栈溢出:

	Rectangle& operator=(const Rectangle& r)
	{
		if (this != &r)
		{
			Shape::operator=(r);
			_rightAngleCounts = r._rightAngleCounts;
		}
		cout << "Rectangle& operator=(const Rectangle& r)" << endl;

		return *this;
	}
int main()
{
	Rectangle r1(6,6,4);
	Rectangle r3(9,9,4);
	r3 = r1;//赋值运算符重载

	return 0;
}

 这是因为子类的operator=和父类的operator=同名,构成隐藏,调不到父类的operator=,只会不断调用自己的operator=,就会发生栈溢出。那如何才能访问到父类的operator=呢?加父类作用域,指定访问的是父类的operator=即可:

	Rectangle& operator=(const Rectangle& r)
	{
		if (this != &r)
		{
			Shape::operator=(r);//加父类作用域,指定访问的是父类的operato=
			_rightAngleCounts = r._rightAngleCounts;
		}
        
        cout << "Rectangle& operator=(const Rectangle& r)" << endl;
		return *this;
	}

4.规则④

规则④ 子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理子类成员再清理父类成员的顺序

如果按照前面的思路,先显式调用父类的析构函数,会报错:

	~Rectangle()
	{
		~Shape();
	}

 这是由于父类的析构函数和子类的析构函数构成隐藏,奇不奇怪?

按照隐藏的定义,父类和子类的同名函数才会构成隐藏,为什么父类和子类的析构函数不同名也会构成隐藏呢?

这是因为多态的缘故,任何类的析构函数名都会被统一处理成destructor( )。所以父类和子类析构函数同名,会构成隐藏。解决方法就是加父类作用域,指定访问的是父类的析构函数就可以了:

	~Rectangle()
	{
		Shape::~Shape();

		//清理子类自己的空间
		//delete ptr;

		cout << "~Rectangle()" << endl;
	}
int main()
{
	Rectangle r1(6,6,4);

	return 0;
}

但是,会发现,竟然调用了2次父类的析构函数:(这里调了两次父类析构函数没有崩的原因是父类的析构函数啥也没干)

 这是因为,构造子类对象时,先调用父类构造函数,再调用子类构造函数,即规则⑤,如果按照正确的析构顺序,那么就得先调用子类的析构函数,再调用父类的析构函数。为了保证析构时,先调用子类的析构函数,再调用父类的析构函数的顺序,会在调用完子类析构函数后,自动再调用一次父类的析构函数。所以就出现了上面调用了两次父类析构函数的情况。

其实析构函数比较特殊,不需要显式调用父类析构函数,因为子类析构函数调用完毕后,会自动调用父类的析构函数:

	~Rectangle()
	{
		//清理自己的空间
		//delete ptr;

		cout << "~Rectangle()" << endl;
	}

 这就符合先构造的后析构的顺序,即规则⑥

 子类对象构造函数和析构函数调用顺序:

五、继承和友元

(1)友元关系不能被继承

(2)根据友元性质,父类友元可以直接访问父类的所有成员,也可以访问子类的共有成员,但不能访问子类私有成员和保护成员

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;

class Rectangle;//前置声明Rectangle类
class Shape
{
public:
	friend void fun(Shape& s, Rectangle& r);//声明fun是Shape类的友元

	//构造函数
	Shape(float width, float height)
		:_width(width),
		_height(height)
	{
		cout << "Shape()" << endl;
	}

	//拷贝构造函数
	Shape(const Shape& s)
		:_width(s._width),
		_height(s._height)
	{
		cout << "Shape(const Shape& s)" << endl;
	}

	//赋值运算符重载函数
	Shape& operator=(const Shape& p)
	{
		cout << "Shape& operator=(const Shape& p)" << endl;
		if (&p != this)
		{
			_width = p._width;
			_height = p._height;
		}

		return *this;
	}

	//析构函数
	~Shape()
	{
		cout << "~Shape()" << endl;
	}

	void setWidth(float w)
	{
		_width = w;
	}

	void setHeight(float h)
	{
		_height = h;
	}

	void ShapePrintf()
	{
		cout << "Shape : _width" << ":" << _width << endl;
		cout << "Shape : _height" << ":" << _height << endl;
	}

public:
	float _width;
	float _height;
};

class Rectangle : public Shape
{
public:
	
	//构造函数
	Rectangle(float width = 10, float height = 10, int rightAngleCounts = 4)
		:Shape(width, height)
		, _rightAngleCounts(rightAngleCounts)
	{
		cout << "Rectangle()" << endl;
	}

	//拷贝构造函数
	Rectangle(const Rectangle& r)
		:Shape(r)//切片,子类对象赋值给父类引用
		, _rightAngleCounts(r._rightAngleCounts)//子类自己的成员初始化为拷贝对象的子类成员值
	{
		cout << "Rectangle(const Rectangle& r)" << endl;
	}

	//赋值运算符重载函数
	Rectangle& operator=(const Rectangle& r)
	{
		if (this != &r)
		{
			Shape::operator=(r);
			_rightAngleCounts = r._rightAngleCounts;
		}
		cout << "Rectangle& operator=(const Rectangle& r)" << endl;

		return *this;
	}

	//析构函数
	~Rectangle()
	{
		//Shape::~Shape();

		//清理自己的空间
		//delete ptr;

		cout << "~Rectangle()" << endl;
	}

	float Area()
	{
		return _width * _height;
	}

	void RectanglePrintf(float width, float height)
	{
		cout << "Rectangle : _width" << ":" << width << endl;
		cout << "Rectangle : _height" << ":" << height << endl;
	}

public://将子类自己的成员变量定义为公有
	int _rightAngleCounts;//直角个数
};

//友元函数
void fun(Shape& s, Rectangle& r)
{
	cout << "fun:s._width = " << s._width << endl;
	cout << "fun:r._rightAngleCounts = " << r._rightAngleCounts << endl;//子类自己的成员变量为公有时,友元可以访问
}

int main()
{
	Shape s(10, 10);
	Rectangle r(6, 6, 4);

	fun(s, r);

	return 0;
}

 将子类自己的成员变量定义为公有时,友元可以访问:

 但是将子类自己的成员变量定义为保护或私有时,友元不可以访问,将上述代码中的子类自己的成员变量的访问限定符:

public://将子类自己的成员变量定义为公有
	int _rightAngleCounts;//直角个数

由公有改为保护:

private://将子类自己的成员变量定义为保护
	int _rightAngleCounts;//直角个数

 编译就会报错:

六、继承和静态成员

父类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。 

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;

class Shape
{
public:
	//构造函数
	Shape(float width = 10, float height = 10)
		:_width(width),
		_height(height)
	{
		++_count;
	}

public:
	float _width;
	float _height;
	static int _count;
};

int Shape::_count = 0;

//长方形
class Rectangle : public Shape
{
public:
	int _rightAngleCounts;//直角个数
};

//三角形
class Triangle : public Shape
{
public:
	float _acuteAngleCounts;//锐角个数
};


int main()
{
	Shape s;
	Rectangle r;
	Triangle c;
	
	cout << Shape::_count << endl;
	cout << Rectangle::_count << endl;
	cout << Triangle::_count << endl;

	Triangle::_count = 20;

	cout << Shape::_count << endl;
	cout << Rectangle::_count << endl;
	cout << Triangle::_count << endl;

	return 0;
}

 _count在整份代码中只有一份:

定义了3个类对象,_count就会自增3次变为3;重置了Circle类中的_count,父类中的_count也就被重置了:

 

静态成员可以统计整个代码一共产生了多少个对象,由于子类无论构造还是拷贝构造都会调用父类的构造函数(父类的拷贝构造对象没有被统计在内,因为父类的拷贝构造函数没有调用父类的构造函数)。

七、菱形继承

继承关系分为:单继承、多继承、菱形继承。 

 1.单继承

一个子类只有一个直接父类时称这个继承关系为单继承

2.多继承

一个子类有两个或以上直接父类时称这个继承关系为多继承

多继承会导致菱形继承。 

3.菱形继承

是多继承的一种特殊情况:

 

菱形继承会存在两个问题

(1)数据冗余

(2)二义性

如上面的TriangleEmbeddedRectangle对象中会存在两份Shape成员:

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;

class Shape
{
public:
	//构造函数
	Shape(float width = 10, float height = 10)
		:_width(width),
		_height(height)
	{}

public:
	float _width;
	float _height;
};


//长方形
class Rectangle : public Shape
{
public:
	int _rightAngleCounts;//直角个数
};

//三角形
class Triangle : public Shape
{
public:
	int _acuteAngleCounts;//锐角个数
};

//三角形内嵌于长方形
class TriangleEmbeddedRectangle : public Rectangle, public Triangle
{
public:
	int _sideCounts;//边数
};
int main()
{
	TriangleEmbeddedRectangle t;
	t._width = 60;//报错,指向不明
	
	return 0;
}

 数据冗余:Rectangle和Triangle都有_width和_height,TriangleEmbeddedRectangle也只有一个,_width和_height,但是编译错误却表示有两个_width:

二义性:访问_width时,不知道要访问谁的_width:

指定作用域可以解决二义性的问题,即指定访问谁的_width:

int main()
{
	TriangleEmbeddedRectangle t;
	t.Triangle::_width = 60;//指定访问Triangle的_width
	
	return 0;
}

 编译OK。

但是这只解决了二义性的问题,并不能解决数据冗余的问题,TriangleEmbeddedRectangle的对象t中还是有两份_width和_height。菱形虚拟继承既可以解决数据冗余也可以解决二义性的问题。

八、菱形虚拟继承

1.虚继承

在两个直接父类的继承方式访问限定符前加vitual:

 

#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;

class Shape
{
public:
	//构造函数
	Shape(float width = 10, float height = 10)
		:_width(width),
		_height(height)
	{}

public:
	float _width;
	float _height;
};


//长方形
class Rectangle : virtual public Shape//虚继承
{
public:
	int _rightAngleCounts;//直角个数
};

//三角形
class Triangle : virtual public Shape//虚继承
{
public:
	int _acuteAngleCounts;//锐角个数
};

//三角形内嵌于长方形
class TriangleEmbeddedRectangle : public Rectangle, public Triangle
{
public:
	int _sideCounts;//边数
};

int main()
{
	TriangleEmbeddedRectangle t;
	t.Triangle::_width = 60;
	
	return 0;
}

监视发现现在改变的是同一个_width,Rectangle、Triangle和Shape中的_width是同一个_width:

 现在可以不用指定作用域了。

那么C++编译器是如何通过虚继承解决数据冗余和二义性的问题的呢?

由于监视窗口被编译器处理过,看不到真实过程。可以使用内存窗口来查看。

2.虚继承的解决数据冗余和二义性的原理

(1)普通继承 

 对于Shape、Rectangle、Triangle、TriangleEmbeddedRectangle类,如果没有虚继承,仅仅只是菱形继承

#define  _CRT_SECURE_NO_WARNINGS  1
#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;

class Shape
{
public:
	//构造函数
	Shape(float width = 10,float height = 10)
		:_width(width)
		,_height(height)
	{}

public:
	float _width;
	float _height;
};


//长方形
class Rectangle : public Shape//非虚继承
{
public:
	int _rightAngleCounts;//直角个数
};

//三角形
class Triangle : public Shape//非虚继承
{
public:
	int _acuteAngleCounts;//锐角个数
};

//三角形内嵌于长方形
class TriangleEmbeddedRectangle : public Rectangle, public Triangle
{
public:
	int _sideCounts;//边数
};

int main()
{
	TriangleEmbeddedRectangle t;
	t.Rectangle::_width = 1;
	t.Rectangle::_height = 2;
	t.Triangle::_width = 3;
	t.Triangle::_height = 4;
	t._rightAngleCounts = 5;
	t._acuteAngleCounts = 6;
	t._sideCounts = 7;

	return 0;
}

F10-调试-窗口-内存,输入&t,enter,将列改为4:

由于_width和_height的类型为float型,因此内存中存放的是浮点数的十六进制表示形式,VS编译器内存以小端形式存储。F10走到return 0:

可以看到内存中,蓝色为Rectangle的成员,绿色为Triangle的成员,Rectangle中存了一份Shape的成员,Triangle中存了一份Shape的成员,这就存在数据冗余,本来只需要存一份Shape就够了,但是内存中存了两份。

(2)虚继承

如果改成虚继承

#define  _CRT_SECURE_NO_WARNINGS  1
#define  _CRT_SECURE_NO_WARNINGS  1
#include<iostream>
using namespace std;

class Shape
{
public:
	//构造函数
	Shape(float width = 10,float height = 10)
		:_width(width)
		,_height(height)
	{}

public:
	float _width;
	float _height;
};


//长方形
class Rectangle : virtual public Shape//虚继承
{
public:
	int _rightAngleCounts;//直角个数
};

//三角形
class Triangle : virtual public Shape//虚继承
{
public:
	int _acuteAngleCounts;//锐角个数
};

//三角形内嵌于长方形
class TriangleEmbeddedRectangle : public Rectangle, public Triangle
{
public:
	int _sideCounts;//边数
};

int main()
{
	TriangleEmbeddedRectangle t;
	t.Rectangle::_width = 1;
	t.Rectangle::_height = 2;
	t.Triangle::_width = 3;
	t.Triangle::_height = 4;
	t._rightAngleCounts = 5;
	t._acuteAngleCounts = 6;
	t._sideCounts = 7;

	return 0;
}

会发现Rectangle指针中存了地址0x00D39B54,Triangle的指针中也存了地址0x00D39B30,这两个地址是用来干嘛的呢?

新开两个内存分别输入0x00D39B54和0x00D39B30,在它们的下一个位置分别存放了0x14和0x0C,这两个都是偏移量,0x00B9FCDC+0x14=0x00B9FCF0,0x00B9FCE4+0x0C=0x00B9FCF0,都指向了同一地址0x00B9FCF0:这个地址是Shape成员的起始地址:

普通继承存放两份Shape成员,而虚继承只需要存在一份Shape成员即可,通过指针找到虚基表,通过虚基表指针的偏移量计算出Shape成员的起始地址。

 菱形虚拟继承原理图:

虚拟菱形继承相比较于菱形继承,TriangleEmbeddedRectangle对象t的直接父类Rectangle和Triangle中存储的不再是Shape的成员,而是Shape的偏移量地址,通过该指针找到虚基表之后,计算偏移,就能知道Shape成员存放的地址,位于Rectangle和Triangle成员的下边,不再像普通继承那样位于Rectangle和Triangle成员的上边,这时候Shape既不属于Rectangle也不属于Triangle。

无论Shape类有多大,付出的代价只有2个指针,即8个字节,解决了数据冗余问题;整个内存中只需要存一份Shape的成员,解决了二义性。

一般不建议设计多继承,这样就不会出现菱形继承,也就不会有数据冗余和二义性的问题。

九.继承和组合

(1) public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。如Shape和Rectangle。他们之间是强关联关系。

(2)组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。如Room和Door。

他们之间是弱关联关系

#include<iostream>
using namespace std;

class Room
{
public:
	float _roomHegiht;//房间高度
	int layers;//层数
};

class Door
{
public:
	float _doorHegiht;//门高度
	string brand = "梦天木门";//品牌
};

 

(3)优先使用对象组合,而不是类继承 。为了降低对象之间的关联度

(4)继承允许根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。父类所有非私有成员对子类都可见,父类的改变会影响子类,父类的封装对子类不太起作用。
(5)继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
(6)对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于保持每个类被封装。子类只能使用父类的共有成员 ,子类和父类关联度低。
(7)实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。

  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 22
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值