【C++】继承

【C++】继承

概念


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

被继承的类称为基类/父类,而继承基类/父类的类称为派生类/子类

子类是在父类的基础上进行了特化,从而延展出特定的功能,体现了类设计代码的复用

例如,Person类是父类,而TeacherStudent等类继承了Person类,属于子类,除了具备父类的属性和方法,还各自特化出了不同的属性和方法

在这里插入图片描述

定义


PersonStudent为例,类的继承定义如下:

	class Person
	{
		// ...
	};

	//     子类    继承方式  父类
	class Student :public Person
	{
		// ...
	};

其中,Student为子类,public为继承方式,Person为父类

继承方式与访问权限


父类的成员有publicprotectedprivate三种访问限定符,而子类也有publicprotectedprivate三种继承方式

这样组合起来,子类继承后的成员就有以下9种情况

父类成员/继承方式publicprotectedprivate
父类的public成员子类的public成员子类的protected成员子类的private成员
父类的protected成员子类的protected成员子类的protected成员子类的private成员
父类的private成员子类中不可见子类中不可见子类中不可见

通过上表,我们可以知道:

  1. 子类的成员权限 = min(父类成员的访问限定符,继承方式)

  2. 父类的private成员,无论以何种方式继承,在子类中都不可见。不可见是指:在子类中存在,但无论在类中还是在类外,都不可直接访问

    class Person
    {
    public:
    	string _name;
    private:
    	// 私有成员,继承后子类不可直接访问
    	int _age;
    };
    
    class Student :public Person
    {
    public:
    	void func()
    	{
            // 访问从父类继承的公有成员
    		cout << _name << endl;
            // 访问从父类继承的私有成员
    		cout << _age << endl;
    	}
    protected:
    	int _studentid;
    };
    

    在这里插入图片描述

  3. 父类的private成员在子类中不可访问。如果不想父类的成员在类外被访问,同时想被子类访问,那父类的成员就可以用protected修饰,而不是private

    class Person
    {
    public:
    	string _name;
    protected:
    	// 保护成员,继承后在子类中可直接访问,在类外不可直接访问
    	int _age;
    };
    
    class Student :public Person
    {
    public:
    	void func()
    	{
    		cout << _name << endl;
    		// 类中访问保护成员
    		cout << _age << endl;
    	}
    protected:
    	int _stuid;
    };
    
    int main()
    {
    	Student s1;
    	s1._name = "abc";
    	// 类外访问保护成员 _age
    	s1._age = 10;
    	return 0;
    }
    

在这里插入图片描述

  1. 一般情况下,建议继承方式为public,因为protected/private继承后的子类成员不能在类外访问,使用起来过于不便

父类和子类对象的赋值转换


通过上面,我们已经了解到:每一个子类对象都是一个特殊的父类对象,这就可以让子类对象赋值给父类对象

子类对象将不属于父类对象的成员切割,将属于父类对象的成员赋值给父类对象。这种赋值方式被称为切片或者切割

在这里插入图片描述

下面我们通过代码来验证一下

class Person
{
protected:
	string _name;
	string _sex;
	int _age;
};

class Student :public Person
{
public:
	void Init(string name, string sex, int age, int sid)
	{
		_name = name;
		_sex = sex;
		_age = age;
		_stuid = sid;
	}
protected:
	int _stuid;
};

int main()
{
	Person p1;
	Student s1;

	// 给子类对象赋值
	s1.Init("张三", "男", 18, 111);
	// 子类对象赋值给父类对象
	p1 = s1;
	return 0;
}

我们通过调试来查看结果:

在这里插入图片描述

那能不能反过来,父类对象赋值给子类对象呢?尝试一下

在这里插入图片描述

很明显,结果是不可以

此外,切片赋值同样适用于指针与引用,我们可以将子类对象的指针/引用赋值给父类对象

int main()
{
	Person p1;
	Student s1;

	// 给子类对象赋值
	s1.Init("张三", "男", 18, 111);
	
    // 指针赋值
	Person* pp = &s1;
    // 引用赋值
	Person& pf = s1;
	return 0;
}

在这里插入图片描述

继承中的作用域


我们知道,每一个类都是一个独立的域,虽然子类继承于父类,但子类和父类都是独立的作用域

子类和父类作用域独立,是否就意味着子类成员可以和父类成员同名呢?

——可以同名,但是会出现这样的现象:子类会屏蔽对父类同名成员的直接访问,默认访问子类的同名成员,这叫做隐藏,也叫重定义

下面让我们来看一看具体是怎样的

// 父类A中的 _num 与 子类中的 _num 相同,构成隐藏
class A
{
protected:
	string _name = "张三";
	int _num = 111;
};

class B :public A
{
public:
	void Print()
	{
		cout << "_name->" << _name << endl;
		cout << "_num->" << _num << endl;
	}
protected:
	int _num = 222;
};

实例化一个B对象,看看结果:

int main()
{
	B b;
	b.Print();
	return 0;
}

在这里插入图片描述

对于同名成员,默认访问的是子类的成员。如果想访问父类的同名成员,可以这样:父类::父类成员

class B :public A
{
public:
	void Print()
	{
		cout << "_name->" << _name << endl;
        // 访问父类同名成员
		cout << "A::_num->" << A::_num << endl;
		cout << "B::_num->" << _num << endl;
	}
protected:
	int _num = 222;
};

在这里插入图片描述

以上是同名成员变量的隐藏,而同名函数也会隐藏,函数名相同即可形成隐藏

// 父类 A 中的 fun 与子类 B 中的 fun 同名,构成隐藏
class A
{
public:
	void fun()
	{
		cout << "fun()->A" << endl;
	}
};

class B :public A
{
public:
	void fun(int i)
	{
		cout << "fun(int i)->B" << endl;
	}
};

int main()
{
	B b;
	b.fun(10);
	return 0;
}

运行结果:

在这里插入图片描述

注意B中的funA中的fun虽然函数名相同参数不同,但是并不构成重载,因为它们并不在同一个作用域

如果想访问父类的同名函数,同样可以这样:父类::父类成员

int main()
{
	B b;
	b.fun(10);
	b.A::fun();
	return 0;
}

在这里插入图片描述

子类的默认成员函数


默认,意思是不用我们写,编译器就会自动生成,那么在子类中默认成员函数又是如何生成的?下面我们就来看一下

构造

子类在构造时可以分为两部分来构造:父类的成员,子类的成员

子类成员构造时

  1. 子类成员是内置类型,那就不做处理
  2. 子类成员是自定义类型,则调用自定义类型的构造

父类成员构造时

  1. 父类如果有默认构造,则调用父类的默认构造
  2. 父类如果没有默认构造,那么必须在子类构造的函数的初始化列表显示调用父类的构造函数

默认构造有以下3种

  1. 自己写的无参构造函数
  2. 自己写的全缺省构造函数
  3. 自己没写,编译器自动生成的构造函数

下面是父类有默认构造的例子

class Person
{
public:
	// 全缺省的默认构造
	Person(const char* name = "张三")
		:_name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name; // 姓名
};

class Student :public Person
{
public:
	// 没有写默认构造,编译器自己生成默认构造
    // 父类部分会调用父类的构造
    // 子类部分,内置类型不处理,自定义类型调用其构造
protected:
	int _num; // 学号
};

实例化一个Student对象s1,通过调试查看s1的成员:

在这里插入图片描述

可以看到,编译器生成的默认构造函数中,父类部分确实调用了自己的构造,而子类部分的_num是内置类型,所以不做处理

下面我们来看父类中没有默认构造的情况

class Person
{
public:
	// 构造函数,非默认构造
	Person(const char* name)
		:_name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name; // 姓名
};

class Student :public Person
{
public:
	// 没有写默认构造,编译器自己生成默认构造
    // 父类部分会调用父类的构造
    // 子类部分,内置类型不处理,自定义类型调用其构造
protected:
	int _num; // 学号
};

父类中的构造函数,既不是无参,也不是全缺省,所以不是默认构造;而只要我们写了构造函数之后,编译器也不会去生成默认构造了。这就导致父类中没有默认构造

这时再去实例化一个Student对象s1,运行就会报错:

在这里插入图片描述

在父类中没有默认构造的情况下,子类中就不能用编译器生成的默认构造了,需要我们自己去写构造函数。通常情况下,为了满足我们的需要,我们都会自己写构造函数,不会去用编译器生成的默认构造

在子类的构造函数中,需要在初始化列表阶段对父类进行构造

先来看一下错误的构造方式:

class Person
{
public:
	// 非默认构造
	Person(const char* name)
		:_name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name; // 姓名
};

class Student :public Person
{
public:
	Student(const char* name, int num)
		:_name(name)	// _name 为父类成员
		,_num(num)
	{
		cout << "Student()" << endl;
	}
protected:
	int _num; // 学号
};

在这里插入图片描述

报错显示_name不是基或成员,这就表明:初始化时,要么是对子类成员初始化,要么是对父类初始化

子类的初始化列表阶段,要将父类当成一个整体进行构造,而不是对父类的成员初始化,以下是正确的构造

class Person
{
public:
	// 非默认构造
	Person(const char* name)
		:_name(name)
	{
		cout << "Person()" << endl;
	}
protected:
	string _name; // 姓名
};

class Student :public Person
{
public:
	Student(const char* name, int num)
		:Person(name)	// 将父类视为一个整体,调用父类构造
		,_num(num)
	{
		cout << "Student()" << endl;
	}
protected:
	int _num; // 学号
};

运行结果:

在这里插入图片描述

总结:子类中的父类成员要单独看作一个整体,要将其当成一个整体的自定义类型。

后面要说的默认成员函数也是这样的,就不再花很大的篇幅解释了

拷贝构造

在子类中的拷贝构造中,同样要将父类成员视作整体,要调用父类的拷贝构造,形式如下

父类拷贝构造

	// 父类拷贝构造
	Person(const Person& p)
	{
		cout << "Person(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
	}

子类拷贝构造

	// 子类拷贝构造
	Student(const Student& s)
		:Person(s)	// 将父类当作整体,调用父类拷贝构造
		,_num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}

在子类的拷贝构造中,调用父类拷贝构造将子类对象传给父类,发生了切片

下面我们测试一下

class Person
{
public:
	// 默认构造
	Person(const char* name = "张三")
		:_name(name)
	{
		cout << "Person()" << endl;
	}
	// 拷贝构造
	Person(const Person& p)
	{
		cout << "Person(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
	}
protected:
	string _name; // 姓名
};

class Student :public Person
{
public:
	// 构造
	Student(const char* name, int num)
		:Person(name)
		,_num(num)
	{
		cout << "Student()" << endl;
	}
	// 拷贝构造
	Student(const Student& s)
		:Person(s)	// 将父类当作整体,调用父类拷贝构造
		,_num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
protected:
	int _num; // 学号
};

int main()
{
	Student s1("李四", 18);
	Student s2(s1);
	return 0;
}

运行结果:

在这里插入图片描述

在这里插入图片描述

赋值重载

没什么好说的,和上面一样,子类的赋值重载,要单独调用父类的赋值重载

注意:父类和子类的赋值重载函数名相同,会构成隐藏,因此在子类中调用父类赋值重载时,要显式 调用

class Person
{
public:
	// 默认构造
	Person(const char* name = "张三")
		:_name(name)
	{
		cout << "Person()" << endl;
	}
	// 拷贝构造
	Person(const Person& p)
	{
		cout << "Person(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
	}
	// 赋值重载
	Person& operator=(const Person& p)
	{
		cout << "Person& operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;
		return *this;
	}
protected:
	string _name; // 姓名
};

class Student :public Person
{
public:
	// 构造
	Student(const char* name, int num)
		:Person(name)
		,_num(num)
	{
		cout << "Student()" << endl;
	}
	// 拷贝构造
	Student(const Student& s)
		:Person(s)	// 将父类当作整体,调用父类拷贝构造
		,_num(s._num)
	{
		cout << "Student(const Student& s)" << endl;
	}
	// 赋值重载
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s)
		{
			// 显式调用
			Person::operator=(s);
			_num = s._num;
		}
		return *this;
	}
protected:
	int _num; // 学号
};

int main()
{
	Student s1("李四", 18);
	Student s2("王五", 20);
    // 赋值
	s2 = s1;
	return 0;
}

在这里插入图片描述

析构

到了析构这里,我们依然沿用前面的思想,在子类中调用父类的析构

	// 父类析构
	~Person()
	{
		cout << "~Person()" << endl;
	}
	
	// 子类析构
	~Student()
	{
		~Person();
		cout << "~Student()" << endl;
	}

然后发生了报错:

在这里插入图片描述

可以看到,这里将~识别为按位取反,并未识别出~Person是析构函数,这是为什么?

这是因为父类和子类的析构函数,构成了隐藏~Person~Student名字并不相同,却构成了隐藏

其实这是C++多态后面挖的坑,为了满足多态,析构函数的名字会被统一处理为destructor,所以在这里子类的析构会隐藏父类

所以我们需要显式调用父类的析构

	// 子类析构
	~Student()
	{
        // 显式调用
		Person::~Person();
		cout << "~Student()" << endl;
	}

实例化一个Student对象s1,s1生命周期结束,自动调用析构

在这里插入图片描述

然后出现了一个问题:父类的析构多调用了一次

从程序的运行结果可以看出:多调用的一次是在子类析构调用结束后才调用的,这就说明子类析构调用结束后会自动调用父类的析构

要说为什么,规定是这样的:构造时先构造父类,再构造子类;析构时先析构子类,再析构父类

析构先子后父,还为了预防这种情况:先析构了父类,子类就不能访问父类成员了

	~Student()
	{
		Person::~Person();
		// 访问父类成员
		cout << _name << endl;
		cout << "~Student()" << endl;
	}

在这里插入图片描述

还有两个默认成员函数:普通对象和const对象取地址重载,由于这两个基本不需要自己写,这里就不讲了

继承与友元


当父类中存在友元时,子类是继承不到的。这就导致父类中的友元访问不到子类的protectedprivate成员

class Student;
class Person
{
public:
    // 父类的友元
	friend void Display(const Person& p, const Student& s);
protected:
	string _name;
};
class Student : public Person
{
protected:
	int _stuid;
};
// 由于子类未继承父类的友元,所以访问不了子类的保护成员
void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuid << endl;
}
int main()
{
	Person p;
	Student s;
	Display(p, s);
	return 0;
}

运行结果:

在这里插入图片描述

子类不能继承父类的友元,那只能由我们手动加上了

class Student : public Person
{
public:
	// 友元
	friend void Display(const Person& p, const Student& s);
protected:
	int _stuid;
};

继承与静态成员


父类中定义了一个静态成员,那么这个父类的继承体系中就共用这个静态成员。无论这个父类派生出多少子类,都不会产生新的静态成员。

以下是Person类的一个继承体系

class Person
{
public:
	Person()
	{
		_count++;
	}
protected:
	string _name;
public:
	// 静态成员,用来计数
	static int _count;
};
int Person::_count = 0;

class Student :public Person
{
protected:
	int _stuid;
};

class Graduate :public Student
{
protected:
	int _course; // 研究科目
};

我们可以将父类和子类的静态成员_count的地址取出,查看是否相同;同时可以实例化数个子类和父类,子类构造时会调用父类构造,都会使静态成员_count加一

int main()
{
	Person p;
	Student s;
	Graduate g;
	// 查看地址
	cout << &Person::_count << endl;
	cout << &Student::_count << endl;
	cout << &Graduate::_count << endl;
	// 查看_count的值
	cout << "Person::_count->" << Person::_count << endl;
	// 在子类中将_count置零
	Student::_count = 0;
	cout << "Person::_count->" << Person::_count << endl;
	return 0;
}

在这里插入图片描述

可以看到,在同一个继承体系中,定义的某个静态成员只有一个,是共用的

菱形继承


单继承

当一个子类只有一个直接父类时,这种继承关系称为单继承

在这里插入图片描述

多继承

当一个子类有两个或以上的父类时,这种继承关系称为多继承

在这里插入图片描述

菱形继承

菱形继承也是多继承,特殊的多继承

在这里插入图片描述

class Person
{
public:
	string _name;
};

class Student : public Person
{
protected:
	int _stuid;
};

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

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

菱形继承存在两个问题:

  1. 数据冗余
  2. 二义性

数据冗余

在这里插入图片描述

从结构图可以看出,Assistant类继承了Student类和Teacher类,而Student类和Teacher类又继承了Person类,这就导致Assistant类中有两份Person类数据,造成数据冗余

二义性

Assistant类要访问Person类成员_name时,由于_name有两份,无法明确访问哪一个

int main()
{
	Assistant a;
    // 访问存在二义性
	a._name;
}

在这里插入图片描述

显式写是哪个父类的成员可以解决二义性问题

int main()
{
	Assistant a;
	a.Student::_name = "张三";
	cout <<"Student::name->" << a.Student::_name << endl;
}

在这里插入图片描述

这样虽然解决了菱形继承的二义性问题,但是没有解决数据冗余问题

虚拟继承


虚拟继承可以解决菱形继承的二义性及数据冗余问题。以前面的Assistant为例,虚拟继承会将两个_name合并为一个,变成StudentTeacher公用的

在腰部的类继承时,加上virtual即可构成虚拟继承

在这里插入图片描述

注意:只有发生菱形继承时要用虚拟继承,其他地方不要用

下面我们建一个简单的继承体系,通过调试来看一下虚拟继承到底是怎样的

先来看菱形继承情况下,类对象中成员在物理空间的存储情况

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

实例化一个D对象,并为其成员赋值

int main()
{
	D d;
	// B类
	d.B::_a = 1;
	d._b = 2;
	// C类
	d.C::_a = 3;
	d._c = 4;

	d._d = 5;

	return 0;
}

然后用调试的内存窗口,取d的地址,查看d的内存:

在这里插入图片描述

可以看到,内存中的数值与我们给d成员的赋值匹配

在这里插入图片描述

因为D类是先继承的B类,后继承的C类,所以先存B类成员,再存C类成员,最后是D类成员

如果先继承C类再继承B类,那么就先存C类成员,再存B类成员,这里就不做演示了

class D : public C, public B

下面使用虚拟继承

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

int main()
{
	D d;
	// B类
	d.B::_a = 1;
	d._b = 2;
	// C类
	d.C::_a = 3;
	d._c = 4;

	d._d = 5;

	return 0;
}

与前面的步骤相同, 查看虚拟继承下的D类对象

在这里插入图片描述

可以看到,在D类对象中,A类成员确实变为了公共的,并且被放到了D类的最下面

A就叫做虚基类,虚基类是公共的,被放在类的最下面

了解了什么是虚基类后,再来看另一个问题:B类和C类中原来是A类成员的地方,变成了两个指针,这两个指针代表了什么?

在这里插入图片描述

我们直接去这两个指针指向的地方看一下

在这里插入图片描述

两个指针指向的位置的下一个位置,各自存了一个数:140c,换算为十进制就是2012

这两个数字又是什么?这时我们看BC的两个指针的地址与虚基类的地址的偏移量,恰好是2012,就是说,通过偏移量,可以找到下面的虚基类

在这里插入图片描述

此时我们来做个总结:A为虚基类,B和C的两个指针称为虚基表指针,指向的是两张表,叫做虚基表。虚基表存的是偏移量,通过偏移量可以找到虚基类

到这里,我们可以感受到为了解决多继承的菱形继承问题,虚拟继承的底层实现是很复杂的

关于继承的总结与反思


  1. 多继承的语法复杂。有了多继承,就有可能写出菱形继承,为了解决菱形继承的问题,又需要用到虚拟继承。而虚拟继承的底层又是很复杂的,使用起来容易出问题

  2. 多继承算是C++的缺陷之一,以至于后来的语言有继承但没有多继承,例如Java

  3. 继承和组合

    • 我们已经了解了继承,组合是什么?

      组合与继承类似,例如B类继承A类,用组合可以这样实现:

      class A
      {
      protected:
      	int _a;
      };
      // B中有A,即为组合
      class B
      {
      protected:
      	A _A;
      };
      
    • 继承是is-a的关系,即每一个子类都是一个父类对象
      组合则是has-a的关系,B组合A,则表示B中有A

    • 优先使用对象组合,而不是类继承

    • 在继承中,父类的成员对子类可见,在一定程度上破环了父类的封装;而父类改变,子类也会受影响。父类与子类之间依赖关系很强,导致耦合度高

    • 而在组合中,组合对象的内部细节是不可见的,相互之间也不易影响,所以组合类之间没有很强的依赖关系,耦合度低

    • 因为继承耦合度高,组合耦合度低,所以为了更好的代码维护性,推荐使用组合。当然也不是不能使用继承,适合用的场景就用,而且多态的实现也离不开继承。

    结束,再见 😄

  • 16
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿洵Rain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值