理解面向对象三大特性 -- 继承

文章目录

  • 一,继承的概念
  • 二,继承继承方式与作用域
  • 三,基类与派生类对象赋值转换
  • 四,基类与派生类的默认成员函数
  • 五,菱形继承及虚拟继承
  • 六,继承和组合

一,继承的概念

继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。

继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

  • 方式: 派生类 : 继承方式 基类(下面进行详解)
    理解面向对象三大特性 -- 继承_第1张图片

二,继承继承方式与作用域

1. 继承方式分为以下三种
理解面向对象三大特性 -- 继承_第2张图片
注意:

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

2 作用域

在继承体系中基类和派生类都有独立的作用域。

隐藏/重定义: 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,函数名相同即可

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

切片/切割: 派生类对象可以赋值给基类的对象 / 指针 / 引用

注意:

  • 基类对象不能赋值给派生类对象。
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。
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;

	}
};

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

理解面向对象三大特性 -- 继承_第3张图片
一个类一般有如上所示六个默认成员函数。

以下是关于派生类和基类的默认成员函数之间的一些注意事项。

简单来说分为三点,派生类必须对基类进行初始化, 派生类的拷贝构造和赋值运算符重载必须完成对基类的对应函数的调用,构造函数和析构函数顺序问题。

具体如下

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
  4. 派生类对象初始化先调用基类构造再调派生类构造。
  5. 派生类对象析构清理先调用派生类析构再调基类。
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;
		}
};

执行结果
理解面向对象三大特性 -- 继承_第4张图片

五,菱形继承及虚拟继承

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

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

因为存在多继承于是可能存在下图所示的继承关系
理解面向对象三大特性 -- 继承_第5张图片
这样的话我们的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));
	}
};

理解面向对象三大特性 -- 继承_第6张图片


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

理解面向对象三大特性 -- 继承_第7张图片
这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?

这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A.
理解面向对象三大特性 -- 继承_第8张图片

六,继承和组合

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

继承的优缺点

  • 优点:
    支持扩展,通过继承父类实现,但会使系统结构较复杂
    易于修改被复用的代码
  • 缺点:
    代码白盒复用,父类的实现细节暴露给子类,破坏了封装性
    当父类的实现代码修改时,可能使得子类也不得不修改,增加维护难度。
    子类缺乏独立性,依赖于父类,耦合度较高
    不支持动态拓展,在编译期就决定了父类

组合的优缺点

  • 优点:
    代码黑盒复用,被包括的对象内部实现细节对外不可见,封装性好。
    整体类与局部类之间松耦合,相互独立。
    支持扩展
    每个类只专注于一项任务
    支持动态扩展,可在运行时根据具体对象选择不同类型的组合对象(扩展性比继承好)
  • 缺点:
    创建整体类对象时,需要创建所有局部类对象。导致系统对象很多。

其他
友元关系不能被继承,基类友元函数不能访问子类私有和保护成员
基类中有静态成员,派生类继承后仍旧只有一个静态成员。
虚基表存储空间 数据段
菱形继承(不加virtual关机字)类的大小计算。有数据冗余
虚基类需要加上虚基表指针大小,发生菱形继承的类存在多个虚基表指针。

你可能感兴趣的:(c++基础详解,c++,c++,指针,多态,编程语言,c语言)