C++继承

 个人主页:Lei宝啊 

愿所有美好如期而遇


目录

1,继承概念及定义

2,基类和派生类赋值兼容转换

3,继承中的作用域

4,派生类的默认成员函数

5,继承与友元

6,继承与静态成员

7,复杂的菱形继承及菱形虚拟继承

8,组合和继承


1,继承概念及定义

通俗点说,继承就是子类从父类继承下来成员变量和成员方法,但是不是拷贝这个变量和方法给子类一份,而是给了子类使用权,子类的继承方式决定了是否可以使用父类的成员变量的成员方法。

举个例子:

#include 
using namespace std;

class Person
{
public:
	Person() 
	{
		cout << "Person()" << endl;
	};

	void print() {};

protected:
	int _a;

public:
	int _b;
};

class Student : public Person
{
public:

private:
	int _b;
};

int main()
{
	Student s;
	
	return 0;
}

C++继承_第1张图片

C++继承_第2张图片

我们也就可以很清晰的看到成员变量和方法还是属于父类,只是我们通过公有继承可以使用罢了。

定义方式:

C++继承_第3张图片

继承方式:public,protected,private

我们这里有个问题:继承方式的不同会导致成员变量和成员方法访问方式不同,是怎样的不同呢?

教材上都会给出像这样的一个表格:

C++继承_第4张图片

我们可以这样理解 ,基类private成员不管以什么方式进行继承,都是不可见的,什么叫不可见?首先,private成员和方法我们还是继承了下来,只是不可以在子类显式使用,这样继承下来的成员,我们通过继承父类的公有方法,通过父类的构造函数,还是可以访问的,举个例子

#include 
using namespace std;

class Person
{
public:
	Person(int d)
		:_d(d)
	{}

	Person() 
	{
		cout << "Person()" << endl;
	};

	void print()
	{
		cout << _d << endl;
	};

private:
	int _d;
};

class Student : public Person
{
public:
	Student(int b, int d)
		:_b(b)
		,Person(d)
	{}
private:
	int _b;
};

int main()
{
	Student s(2,3);
	s.print();
	
	return 0;
}

我们运行后得到结果是3。 

而protected成员,public成员被子类继承下来后,访问权限就取决于继承方式和访问限定符,我们可以从表中看到,继承方式和访问限定符,哪个权限小,子类继承下来的成员和方法就是什么权限,举个例子:

  • 父类public成员,子类通过protected继承,那么继承下来的成员访问权限就是protected。
  • 父类protected成员,子类通过public继承,那么继承下来的成员访问权限就是protected。

这里我们补充一点:private成员子类继承下来,在子类中不能直接访问,那么我们想要在子类中直接访问,但是又不想被类外访问到,那么父类就定义protected成员,子类继承下来就是protected权限。

继承方式可以不写,但是class默认继承方式为private,这个我们基本上不用,所以还是得写出来继承方式,struct默认继承方式为public。

2,基类和派生类赋值兼容转换

赋值兼容转换,我们也叫做向上转型,什么意思呢?

就是说派生类对象/指针/引用可以赋值给基类的对象/指针/引用,我们这里不研究基类对象赋值给派生类对象,后面再说。

对于赋值兼容转换,我们也形象的叫做切片,也就是说,把子类对象中父类的那部分切割出来赋值给子类对象,举个例子,画个图:

int main()
{
	Person p;
	Student s;

	//子类对象赋值给父类对象
	p = s;

	Person* p1;
	Student* s1;

	//子类对象指针赋值给父类对象指针
	p1 = s1;

	//引用
	Person &p2 = s;

	return 0;
}

C++继承_第5张图片

子类对象赋值给父类对象,就是将继承下来的那部分赋值给父类对象,而子类对象指针赋值给父类对象指针,就是让父类对象指针指向子类中继承自父类的那一部分,引用也是同理。

这里我们就有一个值得思考的问题:类型转换,会产生临时变量,临时变量具有常性,而非const变量引用临时变量,权限放大,直接就报错了;不同类型指针,赋值需要强制类型转换,而我们上面没有。举个例子:

int main()
{
	int a = 4;
	double b = 3.14;

	//double类型变量赋值给int类型变量
	//中间产生临时变量,临时变量值为3
	a = b;
	cout << b;

	//这里是强制类型转换
	int c = 4;
    char* d = (char*)&c;

	//double类型转int类型,中间产生int型临时变量,临时变量具有常性
	//所以我们引用也需要常引用,否则权限放大就会报错。
	const int& c = a;

	return 0;
}

那么凭什么 向上转型不用遵循这个规律呢?这就是比较特殊的地方,编译器对其做了特殊处理,在向上转型时,不会产生临时变量,直接就可以赋值。

3,继承中的作用域

继承中,子类和父类都是独立的作用域,当他们有同名函数存在时,父类的函数会被隐藏(重定义,只要函数名相同,不管参数是否相同,就构成隐藏),也就是说当我们调用这个同名函数时,调用的是子类的,父类的需要显式调用,举个例子:

#include 
using namespace std;

class Person
{
public:
	int add(int a = 1, int b = 2)
	{
		return a + b;
	}
};

class Student : public Person
{
public:
	int add(int a = 4, int b = 3)
	{
		return a + b;
	}
};

int main()
{
	Person p;
	Student s;

	cout << p.add() << endl;
	cout << s.add() << endl;

	return 0;
}

C++继承_第6张图片

那么我们怎么调用父类的add函数呢?

C++继承_第7张图片

4,派生类的默认成员函数

派生类的六个默认成员函数在我们不写的时候会默认生成,但是同之前我们单个类的默认生成有些许不同的地方。

1.子类初始化子类的成员变量,父类初始化父类的成员变量,举个例子:

class Person
{
public:
	Person(){}
	Person(int a)
		:_a(a)
	{}

private:
	int _a;
};

class Student : public Person
{
public:
	Student() {}
	Student(int b, int a)
		:_b(b)
		,Person(a)
	{}

private:
	int _b;
};

2.类的构造顺序,析构顺序

  • 构造时,先父后子
  • 析构时,先子后父

构造时,因为子类要继承父类,如果父类不先构造,子类无法继承父类的成员变量,因为成员变量的定义在初始化列表,而初始化列表在构造之前。

析构时,因为此时可能还会用到父类的成员变量,但是先把父类析构了,子类再想用就扯淡了,所以要先析构子类。

子类中不需要写父类的析构函数,编译器在子类析构后自动调用父类的析构函数。

注意:子类和父类的析构函数不管人为起的名字是否相同,最后编译器都会将他特殊处理成叫做destructor()的析构函数,也就是说,他们的析构函数构成隐藏,这点在后面的多态中会用到。

5,继承与友元

友元关系不可被继承。

class Student;
class Person
{
public:
	friend void use(Person& p, Student& s);

	Person(){}
	Person(int a)
		:_a(a)
	{}

private:
	int _a;
};

class Student : public Person
{
public:
	Student() {}
	Student(int b, int a)
		:_b(b)
		,Person(a)
	{}

private:
	int _b;
};

void use(Person &p, Student& s)
{
	cout << p._a << endl;
}

上面的代码现在没有问题,我们再加上一句。

C++继承_第8张图片

也就是说,友元不可被继承,如果仍然想要访问子类的成员变量,那么就成为子类的朋友吧:

C++继承_第9张图片

6,继承与静态成员

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

#include 
using namespace std;

class A
{
public:
	A()
	{
		count++;
	}

	void print()
	{
		cout << "A: count = " << count << endl;
	}

protected:
	static int count;
};

int A::count = 0;

class B : public A
{
public:
	B()
	{
		count++;
	}
};

class C : public B
{
public:
	C()
	{
		count++;
	}
};

int main()
{
	A a;
	B b;
	C c;

	a.print();
	return 0;
}

结果为6 

7,复杂的菱形继承及菱形虚拟继承

C++继承_第10张图片

C++继承_第11张图片

C++继承_第12张图片

我们就可以发现一个问题,assistant对象中,Person成员会有两份,这样会有数据冗余和二义性的问题,举个例子:

我们先故意写一个菱形继承:

#include 
using namespace std;

class Person
{
	public:
		string _name; // 姓名
};

class Student : public Person
{
	protected:
		int _num; //学号
};

class Teacher : public Person
{
	protected:
		int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
	protected:
		string _majorCourse; // 主修课程
};

C++继承_第13张图片

C++继承_第14张图片

虚拟继承可以解决上面的数据冗余和二义性问题。我们在继承Person的派生类的继承方式前加上virtual,谁是导致菱形继承的根源,那么他的派生类继承时就虚继承。

C++继承_第15张图片

C++继承_第16张图片

这一次没有报错。这次我们再举个例子:

C++继承_第17张图片

我们发现修改了Teacher类里的_name,  Student类里的_name也修改了,那么接下来我们来研究一下虚继承的原理。

从最开始具有冗余数据和二义性问题的菱形继承来看:

#include 
using namespace std;

class A
{
	public:
		int _a;
};

class B : public A
//class B : virtual public A
{
	public:
		int _b;
};

class C : public A
//class C : virtual public A
{
	public:
		int _c;
};

class D : public B, public C
{
	public:
		int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;
	return 0;
}

我们查看一下内存中d是如何存储的: 

C++继承_第18张图片

我们就可以看到数据冗余了,我们来看菱形虚拟继承。

C++继承_第19张图片

C++继承_第20张图片

我们现在可以看到,B和C虚继承后,之前存A的地方现在不再存A,而是存储了一个地址,这个地址叫做虚基表指针,这个指针指向的空间,还存储了一个值,这个值就是偏移量,B,C相对于A地址的偏移量,他们将根据这个偏移量找到A;这两个类共享A,所以_a = 1被覆盖成2。

8,组合和继承

public 继承是一种 is-a 的关系。也就是说每个派生类对象都是一个基类对象。
组合是一种 has-a 的关系。假设 B 组合了 A ,每个 B 对象中都有一个 A 对象。
举个例子:
学生是个人,class Student : public Person
学校有学生,class School {Student s}
这里我们要提一句,尽量使用对象组合,而不是类继承。
软件工程提倡低耦合,高内聚,什么是低耦合呢?就是说两者之间的关联性,耦合度低,两者的关联度就低,想象一下,你写了很多代码,而且不管什么,就用继承,有天出了bug,你需要做修改,但是由于类继承比较多,修改一处,另一处也得改,这么串联下去,需要修改的地方就很多,但是如果使用的是组合,耦合度就比较低。
实际上尽量多去使用组合,代码耦合度低,代码维护性好。但是不是说就不用继承了, 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。
总结:
1. 什么是菱形继承?菱形继承的问题是什么?
菱形继承就是多继承的一种特殊情况,导致数据冗余和二义性问题。
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的
菱形虚拟继承就是解决菱形继承数据冗余和二义性问题的方法,在导致菱形继承的类的子类继承方式前加上virtual。通过虚基表指针,共享导致菱形继承的类,解决这两个问题。
3. 继承和组合的区别?什么时候用继承?什么时候用组合?
一个是is-a,一个是has-a,同时继承的耦合度高,组合的耦合度低,在使用多态时需要继承,同时类之间的关系可以使用继承,但是优先考虑组合。

你可能感兴趣的:(C++,c++,继承)