C++之继承总结

声明:本人所有测试代码环境都为vs2017

一.继承的概念

1.什么是继承?
专业术语:继承机制是一种面向对象程序设计可以使代码复用的重要手段,允许程序员在保持原有类特性的基础上进行扩展,是类和类之间的一种关系。
举个例子:继承好比汽车与奥迪、奥拓之间的关系,也叫父子关系。汽车是父类,奥迪和奥拓成为派生类(或子类)。子类可以继承父类的属性,也可以新增加自己独特的属性。奥迪和奥拓都继承了汽车有四个轮子,可以载人,可以加油驱动的属性,然而奥迪又新增加了马达2.0的属性。
在这里插入图片描述
2.继承怎么实现?

class 派生类 : 继承权限 基类

代码实现:

class Car
{
public:
	void Set(const char* name, int age, int price)
	{
		_name = name;
		_age = age;
		_price = price;
	}
	void Print() 
	{
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
		cout << "price" << _acount << endl;
	}
public:
	const char* _name;//主人姓名
protected:
	int _age;//使用时间
private:
	int _price;//价格
};
class Aodi :public Car
{};
void Test()
{

	Car c;
	c.Set("zss", 8, 12345);
	c.Print();
	cout << "c:" << sizeof(a) << endl;

	aodi a;
	// 验证:基类中的成员函数释放被子类继承???
	a.Set("pxy", 8, 12345);
	a.Print();
	cout << "a:" << sizeof(a) << endl;//通过打印一个aodi对象的大小,以验证:派生类将基类中的成员变量继承到子类中

	cout << endl;
}

二.继承的作用

实现代码复用,主要是为了实现多态。

三.继承的方式(权限)与访问

类成员访问方式/继承方式 public继承
public成员 public
protected成员 protected
private成员 不可见
类成员访问方式/继承方式 protected继承
public成员 protected
protected成员 protected
private成员 不可见
类成员访问方式/继承方式 private继承
public成员 private
protected成员 private
private成员 不可见

总结:

  • 基类的其他成员在子类的访问方式=min(成员在基类中的访问方式,继承方式) public>protected>private.基类private成员子派生类中无论以什么方式继承都是不可见的,这个不可见是指,派生类依然继承到了该成员,只是在语法限制上,派生类对象不管在类里还是类外都不能访问。
  • 如果基类成员不想在类外直接被访问,但需要在派生类中访问,那就定义为protected。
  • 关键字class默认的继承方式为private,关键字struct默认继承方式为public。
  • 不提倡使用protected/private继承,实际维护性不强。

四.基类和派生类的对象赋值转换

  • 派生类对象可以赋值给基类的对象/基类的指针/基类的引用。简称切切片,意思就是把派生类再基类的部分切下来赋值给基类。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。

画图解释:
在这里插入图片描述
代码说明:

class A
{
public:
	void Set(int a)
	{
		_a = a;
	}
public:
	int _a;
};
class B :public A
{
public:
	void Set(int a, int b)
	{
		_a = a;
		_b = b;
	}
protected:
	int _b;
};
void Test5()
{
	A a;
	a.Set(10);
	B b;
	b.Set(20, 30);
	//1.用子类对象给基类对象赋值
	a = b;
	cout << "a._a:" << a._a << endl;

	//2.给基类指针赋值
	A* ap = &b;
	cout << ap->_a<< endl;

	//3.给基类引用赋值
	A& aa = b;
	cout << aa._a << endl;

	//B* bp = &a;  报错
	//4.基类对象不能赋值给派生类
	//b = a;

	//5.基类的指针可以通过强制类型转换赋值给派生类的指针,但注意:这种情况转换时虽然可以,但是会存在越界访问的问题
	B* bp = (B*)&a;
	cout << bp->_a << endl;
}

四.继承中的作用域

  • 在继承体系中基类和派生类都有独立的作用域。所以同名函数就不会重载。
  • 同名隐藏:派生类和基类中具有相同名称的成员(成员函数和成员变量)。如果使用派生类对象直接访问同名成员,则派生类会优先访问自己的成员,相当于基类的同名成员隐藏。
  • 注意:成员变量同名---->与成员变量类型是否相同无关。成员函数同名---->与函数原型是否相同无关
  • 同名时:想要通过派生类访问基类成员的方法: 使用时,在同名前面+基类的名称和作用域限定符
  • 在实际中在继承体系里面最好不要定义同名的成员。

代码说明:

//同名隐藏
//基类
class Q
{
public:
	void Set(char q)
	{
		_q = q;
	}
//protected:
public:
	char _q;
};
//派生类
class W :public Q
{
public:
	void Set(int q, int w) 
	{
		_q = q;
	}
//protected:
public:
	int _q;
};

void Test()
{
	cout <<"sizeof(W)"<< sizeof(W) << endl;
	W w;
	w._q = 'a';//同名隐藏,直接访问派生类的元素
	w.Q::_q = 'b';//想访问基类元素,加上命名空间即可
	cout << w._q << endl;
	cout << w.Q::_q << endl;
}

五.派生类的默认成员函数

  • 1.派生类的构造函数会调用基类的构造函数初始化基类的那一部分成员。

//1.1如果基类的构造函数是无参或者全缺省的默认构造函数,则派生类初始化列表的位置调用或不调用都可以,如果用户没有调用,则编译器会默认调用。
//1.2如果基类没有默认的构造函数(基类自定义了构造函数),则必须在派生类构造函数的初始化列表阶段显示调用该显式构造函数。
//因为显式构造函数后,编译器就找不到默认构造函数了

//验证1.1
class Base7
{
public:
	////无参
	//Base7()
	//	:_b(10)
	//	
	//{
	//	cout << "无参Base7()" << endl;
	//}

	//全缺省
	Base7(int b=100)
		:_b(b)
	{
		cout << "全缺省Base7(int b=100)" << endl;
	}
//protected:
public:
	int _b;
};

class Derived7 : public Base7
{
public:
	Derived7()
		:_d(10)
	{
		cout << "无参Derived7()" << endl;
	}	
//protected:
public:
	int _d;
};

void Test7()
{
	Base7 b;//调一次构造
	Derived7 d;//调一次基类构造,调一此自身构造
	cout << b._b << endl;
	cout << d._d << endl;
	cout << d._b << endl;
	cout << endl;
}
//验证1.2
class Base8
{
public:
	Base8(int b)
		:_b(b)
	{
		cout << "自定义Base8()" << endl;
	}
//protected:
public:
	int _b;
};

class Derived8 : public Base8
{
public:
	Derived8()
		:Base8(100)
		,_d(10)
	{
		cout << "无参Derived8()" << endl;
	}
//protected:
public:
	int _d;
};

void Test8()
{
	Base8 b(1000);
	Derived8 d;
	cout << b._b << endl;
	cout << d._d << endl;
	cout << endl;
}
  • 派生类的拷贝构造函数会调用基类的拷贝构造完成基类的拷贝初始化。

//2.1基类如果没有显示定义构造函数,则派生类也可以不用定义,即两个类都可以采用默认构造函数,前提:不涉及资源管理
//2.2基类如果显式定义了构造函数,则派生类就必须在里面初始化列表显式调用基类的构造函数

  • 派生类的operator=会要调用基类的operator=完成基类的复制。

//3.1如果基类没有显示定义派生类也可以不提供,除非派生类需要作出其他操作,再根据情况给出。
//3.2如果基类显式给出了赋值运算符重载,一般派生类也要提供,而且要在其中调用基类的重载。

//验证2.1
class Base9
{
public:
	Base9(int b)
		:_b(b)
	{}
	//protected:
public:
	int _b;
};
class Derived9 :public Base9
{
public:
	Derived9(int d)
		:Base9(100)
		,_d(d)
	{}

public:
	int _d;
};

void Test9()
{
	Derived9 d1(1000);
	Derived9 d2(d1);
	cout << d1._d << endl;//1000
	cout << d1._b << endl;//100
	cout << d2._d << endl;//1000
	cout << d2._b << endl;//100
	cout << endl;
}

//验证2.2  3.2
class Base10
{
public:
	Base10(int b)
		: _b(b)
	{}

	Base10(const Base10& b)
		: _b(b._b)
	{}

	Base10& operator=(const Base10& b)
	{
		if (this != &b)
		{
			_b = b._b;
		}

		return *this;
	}
public:
	int _b;
};

class Derived10 : public Base10
{
public:
	Derived10(int b, int d)
		: Base10(b)
		, _d(d)
	{}

	Derived10(const Derived10& d)
		: Base10(d)
		, _d(d._d)
	{}

	Derived10& operator=(const Derived10& d)
	{
		if (this != &d)
		{

			//*this = d;
			// 给基类部分成员赋值
			Base10::operator=(d);
			// 给派生类新增加成员赋值
			_d = d._d;
		}
		return *this;
	}
public:
	int _d;
};

void Test10()
{
	Derived10 d1(1, 2);
	Derived10 d2(d1);

	Derived10 d3(3, 4);
	d2 = d3;
	cout << d1._b << endl;
	cout << d1._d << endl;
	cout << d2._b << endl;
	cout << d2._d << endl;
	cout << d3._b << endl;
	cout << d3._d << endl;
}
  • 派生类和基类构造函数和析构函数的调用次序
 1. 运行结束打印结果
	   
	   函数体的执行次序:先调基类构造--->派生类构造--->派生类析构--->基类析构
		  Base::Base(int)
		  Derived::Derived(int,int)
		  Derived::~Derived()
		  Base::~Base()
	   
 2. 构造和析构的调用次序
	   
			 构造次序:
			   派生类构造函数()
				  : 基类构造函数()
			   {}
               析构次序:
			   派生类析构函数()
			   {
				  // 释放派生类资源
                // 编译器在派生类析构函数最后一条有效语句后插了一条汇编代码
				  call 基类析构函数;
			   }
	   
class Base11
{
public:
	Base11(int b)
		:_b(b)
	{
		cout << "Base10::Base10(int)" << endl;
	}
	~Base11()
	{
		cout << "Base10::~Base10()" << endl;
	}
public:
	int _b;
};
class Derived11 :public Base11
{
public:
	Derived11(int b, int d)
		: Base11(b)
		, _d(d)
	{
		cout << "Derived::Derived(int,int)" << endl;
	}

	~Derived11()
	{
		cout << "Derived::~Derived()" << endl;
		// call Base::~Base();
	}
public:
	int _d;
};

void Test11()
{
	Derived11 d(10, 20);
}

六.设计一个不能被继承的类

1.C++98
构造函数私有化,派生类调不到基类的构造函数,就无法继承

class Base1
{
public:
	//要在类外定义对象,但是因为基类构造函数访问权限是private,所以定义一个内部成员来访问基类私有的构造函数
	static Base1 GetObject(int b)//按理说要在类外定义对象使用此方法前必须要有一个Base1类型的对象,所以在该方法前加一个static
	{
		return Base1(b);
	}
private:
	Base1(int b)
		:_b(b)
	{}
//protected:
public:
	int _b;
};
void Test1()
{
	//类外定义对象
	Base1 b(Base1::GetObject(10));
	cout << b._b << endl;
}

2.C++11
final如果修饰一个类:表示该类不能被继承

class Base final
{
public:
	Base(int b)
		: _b(b)
	{}

protected:
	int _b;
};

七.继承与友元

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

class Base2
{
	friend void Test2();
public:
	Base2(int b)
		:_b(b)
	{}
	int GetB()
	{
		return _b;
	}
protected:
	int _b;
};
class Derived2 : public Base2
{
public:
	Derived2(int b, int d)
		: Base2(b)
		, _d(d)
	{}

protected:
	int _d;
};

void Test2()//看基类友元函数不能访问子类私有和保护成员
{
	Base2 b(10);
	cout << b.GetB() << endl;
	cout << b._b << endl;

	Derived2 d(10, 20);
	cout << d._d << endl;//崩溃  不能访问
}

八.继承与静态成员

// 结论:
1. 基类中静态成员变量可以被子类继承
2. 在整个继承体系中,静态成员变量只有一份

// 统计一个类创建了多少个对象
class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;
class Student : public Person
{
protected:
	int _stuNum; // 学号
};
class Graduate : public Student
{
protected:
	string _seminarCourse; // 研究科目
};
void TestPerson()
{
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << " 人数 :" << Person::_count << endl;
	Student::_count = 0;
	cout << " 人数 :" << Person::_count << endl;
}

九.多继承–菱形继承

1.单继承
一个类只有一个基类。
在这里插入图片描述
2.多继承
一个类有多个基类(至少是两个)。
在这里插入图片描述
3.菱形继承(钻石继承)–单继承和多继承的结合
在这里插入图片描述
3.1菱形继承缺陷
数据冗余和二义性。
解决方式

  1. 访问明确,加上基类作用域(但仍会数据冗余)。
  2. 使得最高基类Base在派生类中只有一份,节省空间,且去除二义性----3.2菱形虚拟继承

代码说明:

//菱形继承
class B
{
public:
	int _b;
};

class C1 : public B
{
public:
	int _c1;
};

class C2 : public B
{
public:
	int _c2;
};

class D : public C1, public C2// 注意:每个基类前必须给出继承权限,否则就是默认的继承权限
{
public:
	int _d;
};

void Test4()
{
	cout << sizeof(D) << endl;//20

	D d;
	//d._b = 1;  // 菱形继承缺陷:会存在二义性问题

    //解决方式一:通过作用域访问,但会有数据冗余
	d.C1::_b = 1;
	d._c1 = 2;

	d.C2::_b = 3;
	d._c2 = 4;

	d._d = 5;
}
//解决方式二: 菱形虚拟继承
class B
{
public:
	int _b;
};

class C1 : virtual public B
{
public:
	int _c1;
};

class C2 : virtual public B
{
public:
	int _c2;
};

class D : public C1, public C2
{
public:
	int _d;
};

void Test5()
{
	cout << sizeof(D) << endl;//24
	D d;//调用构造函数构造该对象时就生成了两个虚基表指针
	d._c1 = 10;
	d._c2 = 20;
	d._b = 30;
	d._d = 40;
	cout << d._b << endl;
	cout << d._c1 << endl;
	cout << d._c2 << endl;
	cout << d._d << endl;
}
//虚继承后:对象中会多4个字节。

3.3菱形继承与菱形虚拟继承的模型比较(通过内存窗口证明):
在这里插入图片描述
3.4虚拟继承解决原理:
虚继承后:发现对象中多4个字节。
原因:在虚拟继承里,编·译器给派生类默认生成的构造函数,该函数会给对象的前四个字节填充数据放置的就是虚基表指针。虚基表指针指向表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到最高基类。

十.总结和反思

  • 多继承提供了软件重用的强大功能,也增加了程序的复杂性。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计多继承,一定不要设计出菱形继承。

那么关于继承的总结就到这里。

发布了50 篇原创文章 · 获赞 30 · 访问量 9178

猜你喜欢

转载自blog.csdn.net/qq_42913794/article/details/102987333