理解面向对象三大特性 -- 继承(详解!!!)

继承的概念

概念

继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

定义

  • 被继承的类叫做基类
  • 继承的类叫做派生类
  • 方式: 派生类 : 继承方式 基类(下面进行详解)
    在这里插入图片描述

简单继承代码示例

namespace test1 {
	class Person {
	public:
		void Print() {
			std::cout << "name:" << _name << std::endl;
			std::cout << "age:" << _age << std::endl;
		}
	private:
		std::string _name = "Adam";
		int _age = 20;
	};
	//teacher 和 student 分别继承person
	class Student : public Person {
	protected:
		int _schoolID;
	};
	class Teacher : public Person {
	private:
		int _jobID;
	};

	void mytest() {
		Student s1;
		Teacher s2;
		s1.Print();
		s2.Print();
	}
};

运行结果:
在这里插入图片描述
注意

  • 基类中的友元函数不能被继承。
  • 基类中有静态成员,派生类继承后仍旧只有一个静态成员。

继承的访问限定符,继承方式与作用域

继承方式

继承方式分为以下三种

  • public
  • protected
  • private

三者的区别通过下面一张表可以很容易理解
在这里插入图片描述
简单来说就是权限只能缩小不能放大。

注意:

  1. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。
  2. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public.

代码

namespace test2 {
	class Person {
	public:
		void Print() {
			std::cout << "name:" << _name << std::endl;
			std::cout << "age:" << _age << std::endl;
		}
	protected:
		std::string _name = "Adam";
	private:
		int _age = 20;
	};
	//teacher 和 student 分别继承person
	class Student : protected Person {
	protected:
		int _schoolID;
	};
	class Teacher : private Person {
	private:
		int _iobID;
	};
	void mytest() {
		Student s1;
		//s1.Print();  错误使用无权限
	}
};

作用域

在继承体系中基类和派生类都有独立的作用域。子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(函数名相同即可)
代码

namespace test4 {
	class Person {
	public:
		Person() {
			std::cout << "Person Base" << std::endl;
		}
	protected:
		std::string _name = "Adam";
	};

	class Student : public Person {
	public:
		void Print() {
			std::cout << "name" << _name << std::endl;
		}
	protected:
		std::string _name = "adam";//构成隐藏或者叫重定义
	};
	void mytest() {
		Student s1;
		s1.Print();
	}
};

执行结果
在这里插入图片描述

基类与派生类对象赋值转换

概念

派生类对象可以赋值给基类的对象 / 指针 / 引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
注意:

  • 基类对象不能赋值给派生类对象。
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。

代码

namespace test3 {
	class Person {
	protected:
		std::string _name;
		std::string _grand;
		int _age;
	};
	class Student : public Person {
	public:
		int _ID;
	};
	void mytest() {
		Student s1;//子类
		//派生类可以给基类的对象 指针 引用赋值 叫做切片
		Person p1 = s1;
		Person* p2 = &s1;
		Person& p3 = s1;
		//基类对象一般不可以给派生类对象赋值 下面两种情况除外
		Person* p4 = &s1;
		Student* s2 = (Student*)p4;//对基类进行强制类型转换
		s2->_ID = 100;

		Person* p5 = &p1;//p1实际是存储的派生类 但是切片了 因此当作基类对象指针使用
		Student* s3 = (Student*)p5;
		s3->_ID = 100;

	}
};

基类与派生类的默认成员函数

概念

在这里插入图片描述
一个类一般有如上所示六个默认成员函数。那么在继承中也是相同的。
但是继承中的默认函数的调用顺序却是有点不一样的。
通过代码我们可以得到如下结论。

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类
    对象先清理派生类成员再清理基类成员的顺序。
  5. 派生类对象初始化先调用基类构造再调派生类构造。
  6. 派生类对象析构清理先调用派生类析构再调基类。

代码与运行结果

namespace test5 {
	class Person {
	public:
		Person(const char* name = "Adam")
			:_name(name)
		{
			std::cout << "Person()" << std::endl;
		}
		Person(const Person& p)
			:_name(p._name)
		{
			std::cout << "Person(&p)" << std::endl;
		}
		Person& operator=(const Person& p) {
			std::cout << "operator" << std::endl;
			if (this != &p) {
				_name = p._name;
			}
			return *this;
		}
		~Person() {
			std::cout << "~person()" << std::endl;
		}
	protected:
		std::string _name;
	};

		class Student : public Person
		{
		public:
			Student(const char* name, int num)
				: Person(name)
				, _num(num)
			{
				std::cout << "Student()" << std::endl;
			}

			Student(const Student& s)
				: Person(s)
				, _num(s._num)
			{
				std::cout << "Student(const Student& s)" << std::endl;
			}

			Student& operator = (const Student& s)
			{
				std::cout << "Student& operator= (const Student& s)" << std::endl;
				if (this != &s)
				{
					Person::operator =(s);
					_num = s._num;
				}
				return *this;
			}

			~Student()
			{
				std::cout << "~Student()" << std::endl;
			}
		protected:
		int _num;
	};
		void mytest() {
			Student s1("Adam", 20);
			Student s2(s1);
			//Student s3("vive", 20);
			//s1 = s3;
		}
};

执行结果
在这里插入图片描述

菱形继承

概念

c++中继承分成单继承和多继承

  • 单继承:一个子类只有一个直接父类时称这个继承关系为单继承.
  • 多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
    因为存在多继承于是可能存在下图所示的继承关系
    在这里插入图片描述
    这样的话我们的Assistant类中就会存储两份Person中的数据, 造成数据冗余与二义性。
    这就是菱形继承。 而解决的办法就是虚拟继承。

虚拟继承

概念

虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。

原理

我们借助代码来研究。

namespace test7 {
	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 mytest() {
		D d;
		d.B::a = 0;
		d.C::a = 1;
		d.b = 2;
		d.c = 3;
		d.d = 4;
		printf("B->a: %p\n", &(d.B::a));
		printf("C->a: %p\n\n", &(d.C::a));
		printf("D->b: %p\n", &(d.b));
		printf("D->c: %p\n", &(d.c));
		printf("D->d: %p\n", &(d.d));
	}
};

在这里插入图片描述


namespace test6 {
	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 mytest() {
		D d;
		d.B::a = 0;
		d.C::a = 1;
		d.b = 2;
		d.c = 3;
		d.d = 4;
		printf("B->a: %p\n", &(d.B::a));
		printf("C->a: %p\n\n", &(d.C::a));
		printf("D->a: %p\n", &(d.a));
		printf("D->b: %p\n", &(d.b));
		printf("D->c: %p\n", &(d.c));
		printf("D->d: %p\n", &(d.d));
	}
};

在这里插入图片描述
这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A.
在这里插入图片描述


总结

继承是一种代码复用的手段,但是多继承,菱形继承却带给了我们一些不便,因此在使用继承的时候尽量少用多继承,避免出现菱形继承。
继承与组合类似但是又不同,继承一定程度上的破坏了封装性,增加了程序的耦合性,而组合则不同,因此在平时程序设计中可以选择使用组合和继承的结合使用,不要一味的使用继承。

猜你喜欢

转载自blog.csdn.net/ifwecande/article/details/106693836