C++ 面向对象程序三大特性之 继承

2021/4/28 12:25:15

本文主要是介绍C++ 面向对象程序三大特性之 继承,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

目录

  • 继承的概念
  • 继承的定义及使用
    • 继承方式和访问权限
    • 基类和派生类对象的赋值转换
    • 继承中的各成员的作用域
    • 派生类的默认成员函数
      • 构造函数
      • 拷贝构造
      • 赋值运算符重载函数
      • 析构函数
    • 继承与友元
    • 继承与静态成员
    • 多继承
      • 菱形继承
        • 虚拟继承
        • 虚拟继承的实现原理
  • 组合
    • 继承与组合的区别和使用场景

继承的概念

继承:继承是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类的特性的基础上进行扩展,增加功能。产生新的一个类。我们之前复用手段都是通过函数或者模板函数来实现的,而继承上升到类的层次上面。

原有的类称为是父类或者基类,而继承了该类的类称为子类或者派生类

继承的定义及使用

继承格式:class childName : 继承方式 parentName
例如下面代码,B类继承了A类

//父类 基类
class A
{
public:
	void printA()
	{
		cout << _a << endl;
	}

	int _a = 1;
};

//子类 派生类
//格式 class childName : 继承方式 parentName
class B :public A
{
public:
	void printB()
	{
		cout << _b << endl;
	}

	int _b = 2;
};

B类继承了A类之后,A类的成员函数和成员变量都是B类的一部分。相等于B类拥有了A类中的_a成员变量和printA成员函数,但是A类并没有B类的成员,因为继承是单向而非双向的。
我们通过测试代码来看看

void test()
{
	A a;
	B b;
	//A类中没有B类的成员
	//a._b = 3;
	b._a = 3;
	b.printA();
}

测试结果:B类中拥有了A类中的所有成员
在这里插入图片描述
我们在来看看继承后的类的大小是多大

void test()
{
	cout << sizeof(A) << endl;//4
	cout << sizeof(B) << endl;//8
}

测试结果:
在这里插入图片描述

继承方式和访问权限

继承方式和访问权限类似,也是拥有3种继承方式
在这里插入图片描述
如果我们不写继承的方式,也是可以继承的,用class定义的类继承方式默认是private,而用struct定义类继承方式默认是public,继承方式不同在派生类中基类成员的访问权限也有可能不同
下面我们来看看继承的成员变量在派生类中的访问权限

类成员 / 继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

解释表格:纵坐标是表示在基类中的成员访问权限,横坐标是派生类的继承方式。例如以public继承方式继承,那基类中public成员在派生类中的访问权限就是public;以public继承方式继承,那基类中protected成员在派生类中的访问权限就是protected;以public继承方式继承,那基类中private成员在派生类中的访问权限就是不可见的;这里的不可见是指基类的私有成员还是 被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
在这里插入图片描述

表格总结
1、基类private成员在派生类中无论以什么方式继承都是不可见的
2、基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就将该成员定义为protected
3、除了基类中的private成员,基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private

在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用 protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强

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

基类和派生类是不同的类型,那他们之间也是能进行赋值的,但是限制条件有很多。我们平时使用内置类型时,即使类型不同,但是编译器会帮我们将数据正确转换成我们想要的数据,这种转换叫做隐式类型转换。而对于自定义类型来说,如果两个没有任何关系的类,是不能进行赋值操作的,而如果是父子类,是可以进行赋值操作的,这个转换并非叫做隐式类型转换,而是叫做切片操作,而且这种切片操作仅限于子类赋值给父类。原因是父类中有的成员,子类中都拥有了,此时只要将子类中继承了父类的成员的值赋给父类中的成员即可

class Person
{
protected: 
	string _name = "WhiteShirtI"; 
	int _age = 21;
};

class Student : public Person
{
private:
	int _stuId = 101;
};

void test()
{
	Student stu;
	Person p;
	//子类赋值给父类:切片操作
	p = stu;
	Person& rs = stu;
	Person* ps = &stu;

	//轻强制类型转换,此时的pstu中可以访问所有成员,且该类中的stuID的值等于stu的stuID的值
	Student* pstu = (Student*)ps;
}

切片操作
在这里插入图片描述

在子类赋值给父类中,支持子类赋值给父类、支持子类指针赋值给父类指针、也支持子类引用赋值给父类引用

我们再来看看父类赋值给子类

void test()
{
	Student stu;
	Person p;
	//父类赋值给子类
	stu = p;//error,p中没有stuId属性,赋值属性缺失,编译器不支持
	//父类的引用和指针不能直接赋值给子类的引用和指针
	Student& rp = p;//error
	Student* pp = &p;//error
	//如果要赋值必须通过强制类型转换赋值
	//但是存在风险,不建议,可能会产生非法访问的问题
	Student& rp = (Student&)p;//ok
	Student* pp = (Student*)&p;//ok
	//当pp去访问自己的stuId时其实就是一个非法访问
	//在p中并没有这个属性,编译器没有给这个属性分配空间
}

在父类赋值给子类中,不支持父类赋值给子类、不支持父类指针直接赋值给子类指针、也不支持父类引用直接赋值给子类引用。但是支持父类指针经过强转赋值给子类指针、支持父类引用经过强转赋值给子类引用。但是这种方式不安全,会产生非法访问问题

继承中的各成员的作用域

在继承体系中基类和派生类都有独立的作用域,子类和父类中如果存在同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫同名隐藏。如果是成员函数的同名隐藏,只需要函数名相同就构成隐藏。此时要想访问父类中同名的成员,只能通过作用域的方式访问(基类::基类成员)

class A
{
public:
	void print()
	{
		cout << "A::print()" << endl;
	}
protected:
	int _val = 1;
};

class B : public A
{
public:
	void print()
	{
		cout << "B::print()" << endl;
	}
	void printVal()
	{
		cout << _val << endl;
		cout << A::_val << endl;
	}
private:
	int _val = 2;
};

void test()
{
	B b;
	b.print(); //B::print()
	b.A::print();//A::print()
	b.printVal();//2 1
}

在这里插入图片描述

派生类的默认成员函数

在这里插入图片描述

构造函数

1、如果父类中有默认构造函数,编译器会自动调用父类的构造函数

class Person
{
public:
	Person(int id = 1, int age = 21)
		:_id(id)
		, _age(age)
	{
		cout << "Person(int, int)" << endl;
	}
protected:
	int _id;
	int _age;
};

class Student : public Person
{
public:
	void print()
	{
		cout << _id << " " << _age << " " << _stuId << endl;
	}
private:
	int _stuId = 101;
};

void test()
{
	Student stu;
	stu.print();
}

运行结果:
在这里插入图片描述
2、在子类的初始化列表中,不能直接初始化父类的成员

	Student(int id, int age, int stuId)
		:_id(id)
		,_age(age)
		,_stuId(stuId)
	{}

在这里插入图片描述
3、如果要初始化父类的成员或者父类没有默认构造,必须在初始化列表中显示调用父类的构造函数

	Student(int id = 2, int age = 22, int stuId = 102)
		:Person(id, age)
		,_stuId(stuId)
	{}

在这里插入图片描述
4、调用构造函数的顺序:先调用父类的构造函数,再调用子类的构造函数

	Student(int id = 2, int age = 22, int stuId = 102)
		:Person(id, age)
		,_stuId(stuId)
	{
		cout << "Student(int, int, int)" << endl;
	}

在这里插入图片描述

拷贝构造

1、子类的默认拷贝构造会自动调用父类的拷贝构造

class Person
{
public:
	Person(int id = 1, int age = 21)
		:_id(id)
		, _age(age)
	{
		cout << "Person(int, int)" << endl;
	}
	Person(const Person& p)
		:_id(p._id)
		, _age(p._age)
	{
		cout << "Person(const Person&)" << endl;
	}

protected:
	int _id;
	int _age;
};

class Student : public Person
{
public:
	Student(int id = 2, int age = 22, int stuId = 102)
		:Person(id, age)
		,_stuId(stuId)
	{
		cout << "Student(int, int, int)" << endl;
	}

	void print()
	{
		cout << _id << " " << _age << " " << _stuId << endl;
	}
private:
	int _stuId = 101;
};

void test()
{
	Student stu;
	Student stu2 = stu;
}

在这里插入图片描述
2、在子类的拷贝构造函数初始化列表中,不能直接初始化父类的成员

	Student(const Student& stu)
		:_id(stu._id)
		,_age(stu._age)
		,_stuId(stu._stuId)
	{}

在这里插入图片描述
3、如果子类显示定义了拷贝构造,子类的拷贝构造会自动调用父类的默认构造

	Student(const Student& stu)
		:_stuId(stu._stuId)
	{}

在这里插入图片描述
4、在子类显示定义的拷贝构造中,显示调用父类的拷贝构造,此时就不会自动调用父类的默认构造函数了

	Student(const Student& stu)
		:Person(stu)//调用父类的拷贝构造,也属于切片操作
		,_stuId(stu._stuId)
	{}

在这里插入图片描述

赋值运算符重载函数

1、子类的默认赋值运算符重载函数会自动调用父类的赋值运算符重载

class Person
{
public:
	Person(int id = 1, int age = 21)
		:_id(id)
		, _age(age)
	{
		cout << "Person(int, int)" << endl;
	}

	Person& operator=(const Person& p)
	{
		if (this != &p)
		{
			_id = p._id;
			_age = p._age;
		}
		cout << "PerSon& operator=(const Person&)" << endl;
		return *this;
	}
protected:
	int _id;
	int _age;
};

class Student : public Person
{
public:
	Student(int id = 2, int age = 22, int stuId = 102)
		:Person(id, age)
		,_stuId(stuId)
	{
		cout << "Student(int, int, int)" << endl;
	}

	void print()
	{
		cout << _id << " " << _age << " " << _stuId << endl;
	}
private:
	int _stuId = 101;
};

void test()
{
	Student stu;
	Student stu2;
	stu2 = stu;
}

在这里插入图片描述
2、在子类定义的赋值运算符重载函数中,不会自动调用父类的赋值运算符重载函数,且不能显示调用operaot=(stu),因为显示调用会发生同名隐藏,此时会无线调用子类的赋值运算符重载函数。必须加上作用域就可以显示调用Person::operator(stu)

	Student& operator=(const Student& stu)
	{
		if (this != &stu)
		{
			/*_id = stu._id;
			_age = stu._age;*/
			//operaot=(stu)
			Person::operator=(stu);
			_stuId = stu._stuId;
			cout << "Student& operator=(const Student&)" << endl;
		}
		return *this;
	}

在这里插入图片描述

析构函数

1、子类中默认的析构函数会自动调用父类的析构函数

class Person
{
public:
	Person(int id = 1, int age = 21)
		:_id(id)
		, _age(age)
	{
		cout << "Person(int, int)" << endl;
	}
	~Person()
	{
		cout << "~Person" << endl;
	}
protected:
	int _id;
	int _age;
};

class Student : public Person
{
public:
	Student(int id = 2, int age = 22, int stuId = 102)
		:Person(id, age)
		,_stuId(stuId)
	{
		cout << "Student(int, int, int)" << endl;
	}

private:
	int _stuId = 101;
};

在这里插入图片描述
2、即使子类中的析构函数显示定义了,也会调用父类的析构函数

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

在这里插入图片描述
3、子类中的析构函数和父类中的析构函数存在同名隐藏,原因是析构函数在底层的函数名都是destructor
4、析构函数调用顺序,与构造顺序相反,先调用子类的析构函数,再调用父类的析构函数
在这里插入图片描述

继承与友元

子类不会继承父类的友元关系

继承与静态成员

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

class A
{
public:
	A()
	{
		++_a;
	}
	static int _a;
};
int A::_a = 0;

class B : public A
{};

在这里插入图片描述

多继承

多继承概念:一个子类有两个或以上直接父类时称这个继承关系为多继承
多继承的格式class childName : 继承方式 parentName1, 继承方式 parentName2
在这里插入图片描述

菱形继承

菱形继承也是属于多继承中的一种,是一种特殊的多继承。派生类有2个及两个以上的基类,如果这些基类中存在有一样的基类,那么这种情况就属于菱形继承。
在这里插入图片描述

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

但是这个菱形继承一个数据冗余的现象,首先Student继承了Person类中_name成员,Teacher类也继承了Person类中的_name成员,最后Assistant继承它们两个的时候,就会存在两个_name。此时就会存在冗余。我们通过Assistant类的大小查看是否继承了2个_name。
在这里插入图片描述
我们发现是92,92怎么来的呢?其实就是我们所说的数据冗余导致的。Student类的大小为28+4=32;Teacher类的大小为28+4=32。Assistant类存在一个属性,大小为28。当把Student类和Teacher类的大小包括进来,28+32+32=92

我们在尝试给Assitant对象中的_name属性赋值,会出现数据二义的问题
在这里插入图片描述

虚拟继承

虚拟继承就是为解决菱形继承的问题所存在的,虚拟继承可以解决菱形继承的二义性和数据冗余的问题。
在公共父类前加上关键字virtual,格式为:class childName : virtual 继承方式 parentName
虚拟继承不应该放到其他普通的基类上,只有存在菱形继承的情况,才加上虚拟继承

class Person
{
public:
	string _name;
};

class Student : virtual public Person
{
protected:
	int _num;
};

class Teacher : virtual public Person
{
protected:
	int _id;
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse;
};

测试结果:对象a中只_name成员只有一个,并不会存在多个,也就是修改_name时,此时所有的作用域上的_name都修改成一样的数据
在这里插入图片描述
在这里插入图片描述

虚拟继承的实现原理

我们用一个简单的菱形继承来理解虚拟继承的实现原理

先看没有虚拟继承时的菱形继承
类代码

class A
{
public:
	int _a;
};
class B : public A
{
public:
	int _b;
};
class C : public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};

测试代码

void test()
{
	D d;
	d.B::_a = 1;
	d._b = 2;
	d.C::_a = 3;
	d._c = 4;
	d._d = 5;
}

通过调试查看内存的分布情况,查看对象d的内存分布,我们发现编译器会为每个成员都分配了空间,并且将每个值都赋给对应的成员
在这里插入图片描述
在这里插入图片描述
表明菱形继承存在多个作用域,每个作用域下的同名成员都是没有互相影响,没有任何关系的,所以当要访问_a时需要指明作用域,不然编译器就不知道你想访问的是B类中的_a还是C类中的_a。导致了数据的二义性,且一个对象中存在两个同名成员,这样也就导致了数据的冗余。

使用虚拟继承来解决菱形继承的问题
类代码

class A
{
public:
	int _a;
};
class B : virtual public A
{
public:
	int _b;
};
class C : virtual public A
{
public:
	int _c;
};
class D : public B, public C
{
public:
	int _d;
};

测试代码

void test()
{
	D d;
	d.B::_a = 1;
	d._b = 2;
	d.C::_a = 3;
	d._c = 4;
	d._d = 5;
}

在这里插入图片描述
我们发现同名成员中的地址并不是对应的数字,而是还是一个地址,而且最后一个地址中出现了3,这不是C作用域中_a的值么?
在这里插入图片描述
我们地址分配都对应上了,那第一行和第三行的地址分别代表的是什么呢?我们通过调试打开这两个地址
在这里插入图片描述
我们来看看他们地址中都存了哪些值,第一个地址中偏移4个字节后存的值是14,第二个地址中偏移4个字节后存的值是c。这两个数字代表的其实就是获得_a的偏移量,这个偏移量是当前位置相对公共父类成员的偏移量。第一个地址向下偏移20个字节获得的就是_a的地址,第二个地址向下偏移14个字节获得的是_a的地址

之所以能解决数据二义和数据冗余的问题,就是将这个成员放在一个公共的区域,当每个父类需要访问或者修改这个成员,就可以根据自己保存的偏移量,经过偏移量就可以访问到共享区域中指定的成员。
这些偏移量的地址就保存在虚基表中
在这里插入图片描述

组合

组合和继承类似,也是复用类的一种方式。我们所使用的public继承实际上是一种is-a的关系,也就是说每个派生类对象都是一个基类对象。而组合是一种has-a的关系,例如B组合了A,就表明每个B对象中都有一个独立的A对象。就是在B类中自定义A类型,将A类置为B类的一个成员变量。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口

继承与组合的区别和使用场景

1、继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用 (white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。 继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高

2、对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对 象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse), 因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系, 耦合度低。优先使用对象组合有助于你保持每个类被封装

平时如果类之间的关系不明确能用组合就尽量不用继承。看上去组合可以完全代替继承,其实不是的,继承能实现面向对象程序中一个重要的特性,就是多态。要实现多态,就必须要有继承。

继承使用场景示例:

//手机 和mi、huawei形成is-a的关系
class MobilePhone
{
protected:
	string _color; //颜色
	string _model;//型号
};
class Mi: public MobilePhone
{
public:
	void getName()
	{ cout << "xiaomi" << endl; }
};
class Huawei: public MobilePhone
{
public:
	void getName()
	{ cout << "Huawei" << endl; }
};

//Cpu 和手机形成has-a的关系
class Cpu
{
protected:
	string _Code;//代号
	string _supplier;//供应商
};
class MobilePhone
{
protected:
	string _color; //颜色
	string _model;//型号
	Cpu _cpu;//核心处理器
};

is-a在例子中表示,小米、华为手机;而has-a在例子中表示手机包含cpu



这篇关于C++ 面向对象程序三大特性之 继承的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程