C++进阶——继承

目录

前言:

一、继承的概念

二、继承的定义

1.定义格式

2.继承访问方式

三、继承类模板

四、隐藏

五、派生类的默认成员函数

六、切片------基类和派生类的转换

七、实现一个不能被继承的类

八、继承与友元

九、继承与静态成员

十、多继承、菱形继承


前言:

继承是面向对象编程三大特性(封装、继承和多态)之一,继承如其字面意思,就是子类继承父类的各种属性(代码复用),在面向对象编程的框架中是极为重要的一环。

一、继承的概念

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

下面这份代码先大概看看,不求刚开始就完全理解,看个大概就好,后面会逐步理解其中的种种。

cpp 复制代码
#include <iostream>
using namespace std;

// 定义一个Person类,再定义一个Student类和Teacher类,继承自Person类
class Person
{
public:
    Person(string name, int age)
        :_name(name)
        , _age(age)
    {}
    void printInfo()
    {
        cout << "姓名:" << _name << endl;
        cout << "年龄:" << _age << endl;
    }
protected:
    string _name;
    int _age;
};

// 派生类  :   继承方式   基类
// class A :  访问限定符 B { }          A继承自B
class Student : public Person 
{
public:
    Student(string name, int age, string school)
        : Person(name, age)
        , _school(school)
    {}
    void printInfo() 
    {
        Person::printInfo();
        cout << "学校:" << _school << endl;
    }
private:
    string _school;
};

class Teacher : public Person 
{
public:
    Teacher(string name, int age, string subject)
        : Person(name, age)
        , _subject(subject)
    {}
    void printInfo() 
    {
        Person::printInfo();
        cout << "科目:" << _subject << endl;
    }
private:
    string _subject;
};

比如这份代码,Person类具有姓名和年龄的对象,而Student和Teacher都具有Person的特征,又具有其它另外的特性,所以我们可以让Student和Teacher继承Person,也就继承了Person的所有成员变量和成员函数,并且在原有的基础上扩展,形成了Student和Teacher类。

这样,Student和Teacher类就可以复用Person的成员,就不需要重复定义了,大大简化了我们的代码。

二、继承的定义

1.定义格式

2.继承访问方式

从继承定义的格式来看,有三种访问限定符,所有就应该有三种继承方式,实际上也确实是这样的。

但其实我们一般都会使用public继承,不会改变基类成员的访问限定方式,而protected和private继承很少见,且继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

cpp 复制代码
#include <iostream>
using namespace std;

// 定义一个Person类,再定义一个Student类和Teacher类,继承自Person类
class Person
{
public:
    Person(string name, int age)
        :_name(name)
        , _age(age)
    {}
    void printInfo()
    {
        cout << "姓名:" << _name << endl;
        cout << "年龄:" << _age << endl;
    }
protected:
    string _name;
    int _age;
};

// 派生类  :   继承方式   基类
// class A :  访问限定符 B { }          A继承自B
class Student : public Person 
{
public:
    Student(string name, int age, string school)
        : Person(name, age)
        , _school(school)
    {}
    void printInfo() 
    {
        Person::printInfo();
        cout << "学校:" << _school << endl;
        // 如果Person类中_name为私有,编译就会报错,因为私有成员只能在类内部访问,不能在派生类中访问
        // 如果想在派生类中访问私有成员,可以用protected关键字修饰
        //cout << Person::_name << endl;
    }
private:
    string _school;
};

class Teacher : public Person 
{
public:
    Teacher(string name, int age, string subject)
        : Person(name, age)
        , _subject(subject)
    {}
    void printInfo() 
    {
        Person::printInfo();
        cout << "科目:" << _subject << endl;
    }
private:
    string _subject;
};

结合新增的注释可以再重新理解一下之前的代码。

另外还需要注意的是:使用class关键字默认的继承方式为private继承,struct则为public继承,不过还是最好显示写出继承方式。

三、继承类模板

举个实现栈的例子,自然秒懂!

cpp 复制代码
#include <iostream>
#include <vector>
#include <list>
using namespace std;

#define CONTAINER vector

namespace muss
{
	template<typename T>
	class stack : public CONTAINER<T>
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域,
			// 否则编译报错:error C3861: "push_back": 找不到标识符
			// 因为stack<int>实例化时,也实例化vector<int>了
			// 但是模版是按需实例化,在需要调用stack的push时,才会实例化出push
			// 但是push中的所用容器的 push_back 却没有实例化,所以编译报错
			CONTAINER<T>::push_back(x);
			//push_back(x);
		}
		void pop()
		{
			CONTAINER<T>::pop_back();
		}
		const T& top()
		{
			return CONTAINER<T>::back();
		}
		bool empty()
		{
			return CONTAINER<T>::empty();
		}
	};
}

int main()
{
	muss::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;
}

四、隐藏

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

  2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。

(在派生类成员函数中,可以使用基类::基类成员显示访问)

  1. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

  2. 注意在实际中在继承体系里面最好不要定义同名的成员。

看之前的代码来理解:

cpp 复制代码
#include <iostream>
using namespace std;

// 定义一个Person类,再定义一个Student类和Teacher类,继承自Person类
class Person
{
public:
    Person(string name, int age)
        :_name(name)
        , _age(age)
    {}
    void printInfo()
    {
        cout << "姓名:" << _name << endl;
        cout << "年龄:" << _age << endl;
    }
protected:
    string _name;
    int _age;
};

class Student : public Person
{
public:
    Student(string name, int age, string school)
        : Person(name, age)
        , _school(school)
        , _name(name)     // 初始化Student的_name变量,与Person类中的_name构成隐藏
                                     // 当然这里的举例不是很清楚,因为传参导致了两个_name变量内容相同
    {}

    // 同名函数,与Person类中的printInfo()构成隐藏
    // 在Student对象中调用时,编译器会优先调用Student的printInfo()函数
    void printInfo()
    {
        Person::printInfo();
        cout << "学校:" << _school << endl;
    }
private:
    // 同名变量,与Person类中的_name构成隐藏
    // 在Student对象中调用时,编译器会优先调用Student的_name变量
    string _name;
    string _school;
};

class Teacher : public Person
{
public:
    Teacher(string name, int age, string subject)
        : Person(name, age)
        , _subject(subject)
    {}
    void printInfo()
    {
        Person::printInfo();
        cout << "科目:" << _subject << endl;
    }
private:
    string _subject;
};

int main()
{
    Person p("张三", 20);
    p.printInfo();

    Student s("muss", 19, "西南石油大学");
    // 这里为什么会调用Student的printInfo()函数而不是Person的printInfo()函数
    // 原因就是Student和Person都有printInfo()函数,在Student对象中调用时,编译器会优先调用Student的printInfo()函数
    // 如果想调用Person的printInfo()函数,就需要指定作用域,比如Person::printInfo()
    s.printInfo();
    s.Person::printInfo();

    Teacher t("t老师", 30, "C++");
    t.printInfo();

    return 0;
}

五、派生类的默认成员函数

派生类继承自基类,那么派生类的构造函数都需要做哪些事情呢?

其实,我们可以把基类当成自定义类型,而派生类种就有一个此种自定义类型。

cpp 复制代码
#include <iostream>
using namespace std;

// 定义一个Person类,再定义一个Student类和Teacher类,继承自Person类
class Person
{
public:
    Person(string name, int age)
        :_name(name)
        , _age(age)
    {}
    void printInfo()
    {
        cout << "姓名:" << _name << endl;
        cout << "年龄:" << _age << endl;
    }
protected:
    string _name;
    int _age;
};


// 其实,这里的拷贝构造,赋值运算符重载,析构函数
// 因为Person类这个"自定义类型"中没有资源需要释放,编译器默认生成的就够用
// 如果有需要显示释放的资源,就需要自己写
// 不过为了演示还是写了出来
class Student : public Person
{
public:
    // 构造函数
    Student(string name = "muss", int age = 19, string school = "西南石油大学")
        // 处理基类这个"自定义类型"的方式:在初始化列表显示调用基类的构造函数:基类名(参数...)
        // 其实这个形式类似于匿名对象的创建,大家可以这么记忆
        : Person(name, age)
        , _school(school)
    {}
    // 拷贝构造函数
    Student(const Student& s)
        // 跟自定义类型一样,在初始化列表显示调用基类的拷贝构造
        : Person(s._name, s._age)
        , _school(s._school)
    {}

    // 赋值运算符重载
    Student& operator=(const Student& s)
    {
        if (this != &s)
        {
            // 两个赋值运算符重载构成隐藏,所以需要显示调用基类的赋值运算符
            // 这里必须调用基类的赋值运算符,否则如果有自定义类型很容易造成浅拷贝,养成习惯
            Person::operator =(s);
            _school = s._school;
        }
        return *this;
    }
    // 析构函数
    ~Student()
    {
        // 没有资源需要释放,所以不用写什么
        
        // 这里不需要显示调用基类的析构函数,编译器会在Student的析构函数结束后自动调用Person的析构函数
    }

    void printInfo()
    {
        Person::printInfo();
        cout << "学校:" << _school << endl;
    }
private:
    string _school;
};

class Teacher : public Person
{
public:
    Teacher(string name, int age, string subject)
        : Person(name, age)
        , _subject(subject)
    {}
    void printInfo()
    {
        Person::printInfo();
        cout << "科目:" << _subject << endl;
    }
private:
    string _subject;
};

int main()
{

    Student s("muss", 19, "西南石油大学");
    s.printInfo();

    Student s2(s);
    s2.printInfo();

    Student s3;
    s3 = s;
    s3.printInfo();

    return 0;
}

有一些需要注意的点:

  1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显示调用。

  2. 派⽣类的拷贝构造函数必须调⽤基类的拷贝构造完成基类的拷贝初始化。

  3. 派⽣类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域

  4. 派⽣类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派⽣类对象先清理派⽣类成员再清理基类成员的顺序。

  5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。

  6. 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。

六、切片------基类和派生类的转换

• public继承的派生类对象可以赋值给基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切

割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派⽣类中切出来的基类那部分。

• 基类对象不能赋值给派⽣类对象。

代码演示:

cpp 复制代码
#include <iostream>
using namespace std;

// 定义一个Person类,再定义一个Student类和Teacher类,继承自Person类
class Person
{
public:
    Person(string name, int age)
        :_name(name)
        , _age(age)
    {}
    void printInfo()
    {
        cout << "姓名:" << _name << endl;
        cout << "年龄:" << _age << endl;
    }
    void setName(string name)
    {
        _name = name;
    }

protected:
    string _name;
    int _age;
};

class Student : public Person 
{
public:
    Student(string name = "muss", int age = 19, string school = "西南石油大学")
        : Person(name, age)
        , _school(school)
    {}
    void printInfo() 
    {
        Person::printInfo();
        cout << "学校:" << _school << endl;
    }
private:
    string _school;
};

int main() 
{
    Student sobj;
    // 1.派⽣类对象可以赋值给基类的指针/引⽤
    Person* pp = &sobj;
    Person& rp = sobj;
    // 派生类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
    Person pobj = sobj;
    //2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
    //sobj = pobj;

    
    sobj.printInfo();
    pp->printInfo();
    rp.printInfo();
    pobj.printInfo();

    // rp由于是引用,所以rp的修改也会修改sobj
    rp.setName("李四");
    sobj.printInfo();
    rp.printInfo();

    return 0;
}

七、实现一个不能被继承的类

方法有二:

1.基类构造函数私有:这样派生类就不能调用基类的构造函数来构造派生类对象了,但其实这种方法也可以继承,只是在派生类实例化的时候才会报错。

2.final关键字:C++11新增了final关键字,final修饰的基类不能被派生类继承,如果继承了,在编译阶段就会报错,所以这种方式会更常用一点。

cpp 复制代码
#include <iostream>
using namespace std;

// C++11的⽅法
class Base final
{
protected:
	int a = 1;
private:
	// C++98的⽅法:私有构造函数
	/*Base()
	{}*/
};

// 继承了final修饰的基类在编译阶段就会报错
class Derive :public Base
{
protected:
	int b = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}

八、继承与友元

友元关系不能被继承,也就是说基类的友元不能访问派生类的私有和保护成员。

九、继承与静态成员

基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都只有⼀个static成员实例。

cpp 复制代码
#include <iostream>
using namespace std;

// 定义一个Person类,再定义一个Student类和Teacher类,继承自Person类
class Person
{
public:
    Person(string name = "lisi", int age = 30)
        :_name(name)
        , _age(age)
    {}
    void printInfo()
    {
        cout << "姓名:" << _name << endl;
        cout << "年龄:" << _age << endl;
    }

    string _name;
    int _age;
    static int _count;
};

int Person::_count = 0;

class Student : public Person 
{
public:
    Student(string name = "muss", int age = 19, string school = "西南石油大学")
        : Person(name, age)
        , _school(school)
    {}
    void printInfo() 
    {
        Person::printInfo();
        cout << "学校:" << _school << endl;
    }
private:
    string _school;
};


int main()
{
	Person p;
	Student s;
	// 这⾥的运⾏结果可以看到⾮静态成员_name的地址是不⼀样的
	// 说明派⽣类继承下来了,⽗派⽣类对象各有⼀份
	cout << &p._name << endl;
	cout << &s._name << endl;
	// 这⾥的运⾏结果可以看到静态成员_count的地址是⼀样的
	// 说明派⽣类和基类共⽤同⼀份静态成员
	cout << &p._count << endl;
	cout << &s._count << endl;
	// 公有的情况下,⽗派⽣类指定类域都可以访问静态成员
	// 当然通常情况下,静态成员我们通过类名来访问,而不是通过对象来访问
	cout << Person::_count << endl;
	cout << Student::_count << endl;

	return 0;

}

十、多继承、菱形继承

1.继承模型

前面我们所用到的继承方式都为单继承,也就是派生类只继承一个基类,但是实际上C++也支持多继承,也就是一个派生类继承多个基类,只需要在类名后面的冒号后面多写上一个限定符+类名即可,和前面的用逗号隔开。

有了多继承,势必也会出现菱形继承的情况:

大家有没有发现菱形继承有什么问题?

Assistant继承自Student和Teacher,而Student和Teacher中都具有一份Person的成员,所以在Assistant中就势必会造成数据冗余和二义性的结果,也就是说Assistant中有两份同名数据,而且这两份同名数据在访问的时候还需要指定类域访问。

虽然菱形继承也有其解决数据冗余和二义性的方案,但是底层是付出了很大代价的,所以一般情况下不要设计出菱形继承。

2.虚继承

虚继承被设计出来主要是用来解决菱形继承的问题的,如果发现多继承的时候造成了菱形继承,就需要在继承同一个基类的类的继承方式前加上virtual关键字,这样在后续因为多继承了这些虚继承的类而造成的菱形继承就不会出现数据冗余和二义性的问题,不过底层是付出了巨大代价的,多继承可以用,不过一般不要设计出菱形继承

虚继承举例:

cpp 复制代码
#include <iostream>
using namespace std;

// 定义一个Person类,再定义一个Student类和Teacher类,继承自Person类
class Person
{
public:
    Person(string name = "lisi", int age  = 40)
        :_name(name)
        , _age(age)
    {}
    void printInfo()
    {
        cout << "姓名:" << _name << endl;
        cout << "年龄:" << _age << endl;
    }
protected:
    string _name;
    int _age;
};

// 在继承同一个基类的类的继承方式前加上virtual关键字
class Student : virtual public Person 
{
public:
    Student(string name = "muss", int age = 19, string school = "西南石油大学")
        : Person(name, age)
        , _school(school)
    {}
    void printInfo() 
    {
        Person::printInfo();
        cout << "学校:" << _school << endl;
    }
private:
    string _school;
};

// 在继承同一个基类的类的继承方式前加上virtual关键字
class Teacher : virtual public Person 
{
public:
    Teacher(string name = "t老师", int age = 30, string subject = "C++")
        : Person(name, age)
        , _subject(subject)
    {}
    void printInfo() 
    {
        Person::printInfo();
        cout << "科目:" << _subject << endl;
    }
private:
    string _subject;
};

// 由于前面是虚继承,所以避免了数据冗余和二义性
class Assistant : public Student, public Teacher 
{
public:
    Assistant(string name = "助教", int age = 20, string school = "西南石油大学", string subject = "C++")
        : Student(name, age, school)
        , Teacher(name, age, subject)
    {}
    void printInfo() 
    {
        Student::printInfo();
        Teacher::printInfo();
        // 打印出来两者的姓名和年龄是一样的
    }
};

int main() 
{
    Assistant a;
    a.printInfo();

    return 0;
}

另外,这个也算是菱形继承,菱形继承需要在继承同一个基类的类的继承方式前加上virtual关键字,于是这里只用在B和C继承A的时候加virtual,D和E不用加。

菱形继承也会把构造函数等等的语法搞得复杂,博主也不是完全能搞清楚,只了解一下就够了,什么时候需要用到再查一下语法即可

十一、继承和组合

我们之前用两种方式实现了stack。

一种是传容器模板的方式,也就是说stack里面有一个该容器,也可以再添加别的内容,也就是组合,是一种has-a的关系。

另一种是刚刚前面继承的方式,也就是说stack是这个容器,"学生"是"人",是一种is-a的关系。

两种方式都是代码复用的一种体现,但是通常情况下两者的意思差距不会特别大,也并没有那么绝对,如果两种方式都可以用,那么建议使用第一种组合的方式,原因就是组合代码的耦合度比较低,维护性较好,但是也并非这么绝对。

类之间的关系就适合继承(is-a)那就⽤继承,另外要实现多态,也必须要继承。类之间的关系既适合⽤继承(is-a)也适合组合(has-a),就⽤组合。


完结撒花撒花撒花撒花~~~~~~~~~~~~

٩(๑òωó๑)۶

相关推荐
梦想科研社8 分钟前
【无人机设计与控制】红嘴蓝鹊优化器RBMO求解无人机路径规划MATLAB
开发语言·matlab·无人机
混迹网络的权某11 分钟前
每天一道C语言精选编程题之求数字的每⼀位之和
c语言·开发语言·考研·算法·改行学it·1024程序员节
一只特立独行的猪6111 小时前
Java面试题——微服务篇
java·开发语言·微服务
Ddddddd_1583 小时前
C++ | Leetcode C++题解之第504题七进制数
c++·leetcode·题解
J_z_Yang3 小时前
LeetCode 202 - 快乐数
c++·算法·leetcode
喵手3 小时前
Java 与 Oracle 数据泵实操:数据导入导出的全方位指南
java·开发语言·oracle
硬汉嵌入式4 小时前
H7-TOOL的LUA小程序教程第16期:脉冲测量,4路PWM,多路GPIO和波形打印(2024-10-25, 更新完毕)
开发语言·junit·小程序·lua
Wx120不知道取啥名4 小时前
C语言之长整型有符号数与短整型有符号数转换
c语言·开发语言·单片机·mcu·算法·1024程序员节
Python私教5 小时前
Flutter颜色和主题
开发语言·javascript·flutter
代码吐槽菌5 小时前
基于SSM的汽车客运站管理系统【附源码】
java·开发语言·数据库·spring boot·后端·汽车