C++继承详解

目录

前言

继承的概念以及定义

继承的概念

继承的定义

定义格式

继承父类成员后访问方式的变化

继承类模板

基类和派生类之间的转换

继承中的作用域

隐藏规则:

派生类的默认成员函数

派生类常见的默认成员函数规则

一个不能被继承的类

继承与友元

继承与静态成员

多继承以及菱形继承问题

继承模型

虚继承

多继承中的指针偏移问题

继承和组合


前言

我们知道C++是一门面向对象的编程语言,而面向对象编程的三大特性是封装(STL)、继承和多态,它们共同实现代码的模块化、复用性和灵活性。本片文章我们将来开始学习C++中的继承,理解为什么它可以作为三大特性之一,怎么实现代码的复用

继承的概念以及定义

继承的概念

继承,我们抛开编程语言,以我们的生活经验来看,脑子里很容易联想到继承家产......这一类事情,将父类资产归为自己所有。那么继承这一词到C++中作用其实与上面大差不差,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。即子类可以继承父类的资源,同时允许自己"创业"......

我们举一个例子,当我们设计两个类:Student类以及Teacher类

如下:

cpp 复制代码
class student
{
public:
	void identity()
	{
		//......一些身份认证操作
	}
	void study()
	{
		//......学习
	}
private:
	string _name = "peter"; // 姓名
    string _address;        // 地址
	string _tel;            // 电话
	int _age = 18;          // 年龄

	int _stuid;             // 学号
};
class teacher
{
public:
	void identity()
	{
		//......一些认证操作
	}
	void teach()
	{
		//......一些教学操作
	}

private:
	string _name = "张三"; // 姓名
	string _address;        // 地址
	string _tel;            // 电话
	int _age = 18;          // 年龄

	string _title;          // 职称
};

我们能看到在这两个类的成员变量中,有较多重复的部分,那我们就可以利用继承来复用这些部分,如下:

cpp 复制代码
//我们将重复的信息写成一个单独的类person
class person
{
	//这里要设计成保护类型
public:
	void identity()
	{
		
	}
protected:
	string _name = "peter"; // 姓名
	string _address;        // 地址
	string _tel;            // 电话
	int _age = 18;          // 年龄
};

class student:public person
{
public:
	void study()
	{

	}
private:
	int _stuid;             // 学号
};

class teacher :public person
{
public:
	void teach()
	{

	}
private:
	string _title;          // 职称
};

以上只是做一个简单的展示,下面将会详细地说明

继承的定义

定义格式

下面我们看到Person是基类,也称作父类。Student是派生类,也称作子类。(因为翻译的原因,所以既叫基类/派生类,也叫父类/子类)

继承父类成员后访问方式的变化

说明:

1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。

2.基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

3.实际上面的表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式==(成员在基类的访问限定符,继承方式中最小的),public >protected > private。

4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。

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

继承类模板

cpp 复制代码
namespace mine
{
	template<class T>
	class stack :public std::vector<T>
	{
	public:
		void push(const T& val)
		{
			vector<T>::push_back(val);
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty() const
		{
			return vector<T>::empty();
		}
	};
}
int main()
{
	mine::stack<int> s;
	s.push(1);
	s.push(2);
	return 0;
}

这里我们需要说明一下:当基类是类模板时,需要指定一下类域,否则编译报错:error C3861 :"push_back": 找不到标识符,因为stack<int>实例化时,也实例化vector<int>了但是模版是按需实例化,push_back等成员函数未实例化,所以找不到

基类和派生类之间的转换

1.public继承的派生类对象可以赋值给基类的指针/基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中基类那部分切出来,基类指针或引用指向的是派生类中切出来的基类那部分。(我们类比成可以缩小,但是不能放大)

2.基类对象不能赋值给派生类对象。(不能放大)

3.基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。(ps:这个我们后面类型转换章节再单独专门讲解,这里先提一下)

cpp 复制代码
class person
{
protected:
	string name;//姓名
	string sex;//性别
	int age;//年龄
};

class student :public person
{
public:
	int _id;
};

int main()
{
	student s;
	//子类的对象可以赋值给父类的对象
	person p = s;
	//子类的对象可以赋值给父类的引用 指针
	person* pp = &s;
	person& rp = s;
	//父类不能给子类对象赋值
	s = p;
}

继承中的作用域

隐藏规则:

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

2.派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在派生类成员函数中,可以使用基类::基类成员显示访问,指定类域访问)

3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,不存在与继承的成员函数构成重载。

cpp 复制代码
class A
{
public:
    void fun()
    {
    cout << "func()" << endl;
    }
};

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

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

根据上述规则,我们思考一下A和B类中的两个func构成什么关系,以及程序运行结果

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

cpp 复制代码
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person
{
protected:
    string _name = "小李子"; // 姓名
    int _num = 111;    // 身份证号
};

class Student : public Person
{
public:
    void Print()
    {
        cout << " 姓名:" << _name << endl;
        cout << " 身份证号:" << Person::_num << endl;//我们需要指定类域
        cout << " 学号:" << _num << endl;
    }
protected:
    int _num = 999; // 学号
};

int main()
{
    Student s1;
    s1.Print();
    return 0;
}

答案:隐藏关系,编译报错(3中问题的答案,放这里是为了给各位思考空间)

派生类的默认成员函数

派生类常见的默认成员函数规则

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?

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

我们可以将子类的成员分成三个部分来看:内置类型,对于无缺省参数的我们无法确定结果;自定义类型,自动调用自己的默认构造;继承的父类成员看作一个整体,要求调用父类的构造函数

代码展示:

cpp 复制代码
class person
{
public:
	person()
	{
		_name = "小蔡";
	}
protected:
	string _name;//姓名
};

class student:public person
{
public:
	student()
		: _num(24346101),
		person()//我们这里调用的是父类的构造函数
	{ }
	void print()
	{
		cout << "姓名:" << _name << endl;
		cout << "学号:" << _num << endl;
	}
private:
	int _num;//学号
};

int main()
{
	student s;
	s.print();
}

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

cpp 复制代码
class person
{
public:
	person()
	{
		_name = "小蔡";
	}
	person(const person& p)
	{
		_name = p._name;
	}
protected:
	std::string _name;//姓名
};

class student:public person
{
public:
	student(int id=24346101)
		: _num(id),
		person()//我们这里调用的是父类的构造函数
	{ }
	student(const student& st)
		: person(st),
		_num(st._num)
	{}
	void print()
	{
		cout << "姓名:" << _name << endl;
		cout << "学号:" << _num << endl;
	}
private:
	int _num;//学号
};

int main()
{
	student s1(24336101);
	student s2(s1);
	s2.print();
}

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

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

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

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

7.因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor(),所以基类析构函数不加 virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。我们在写基类的析构时需要加上virtual

cpp 复制代码
class Person
{
public:
    Person(const char* name = "peter")
        : _name(name)
    {
        cout << "Person()" << endl;
    }

    Person(const Person& p)
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }

    Person& operator=(const Person& p)
    {
        cout << "Person operator=(const Person& p)" << endl;
        if (this != &p)
            _name = p._name;
        return *this;
    }

    virtual ~Person()
    {
        cout << "~Person()" << endl;
    }

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

    ~Student()
    {
        cout << "~Student()" << endl;//会自动调用父类的析构,我们可以不用手动写,如果要写也需要加virtual
    }

protected:
    int _num; // 学号
};

int main()
{
    Student s1("jack", 18);
    Student s2(s1);
    Student s3("rose", 17);
    s1 = s3;

    return 0;
}

一个不能被继承的类

方法1:将基类的构造函数私有,因为派生类的构成必须调用基类的构造函数,但是基类的构成函数私有化以后,派生类看不见就不能调用了,那么派生类就无法实例化出对象。

方法2:C++11新增了一个final关键字,final修改基类,派生类就不能继承了。

方法2展示:

我们发现当试图让Derive公有继承Base时,编译器会报语法错误:

继承与友元

C++规定,友元关系不能被继承。

我们回忆一下,友元关系意味着外部类或者函数可以访问另一个类中的私有成员,友元关系不能被继承说明父类的友元不能访问子类的私有以及保护成员

cpp 复制代码
class Student;
class Person
{
public:
    friend void Display(const Person& p, const Student& s);
protected:
    string _name; // 姓名
};

class Student : public Person
{
protected:
    int _stuNum; // 学号
};

void Display(const Person& p, const Student& s)
{
    cout << p._name << endl;
    cout << s._stuNum << endl;
}

int main()
{
    Person p;

    Student s;
    // 编译报错: error C2248: "Student::_stuNum": 无法访问 protected 成员
    // 解决方案: Display也变成Student 的友元即可

    Display(p, s);

    return 0;
}

继承与静态成员

如果说存在一个基类定义了一个静态成员,那么在整个继承体系中,都只存在一个这样的成员,所有子类都共用这一个成员,该静态成员都只有这一个实例。

cpp 复制代码
class person
{
public:
	string _name;
	static int num;
};
int person::num = 0;
class student :public person
{
protected:
	string stuid;
};

int main()
{
	person p;
	student s;
	cout << &p._name << endl;
	cout << &s._name << endl;

	cout << &p.num << endl;
	cout << &s.num << endl;
}

我们运行代码发现,基类以及派生类中的静态成员的地址是一样的,说明静态成员确实只有一个实例

多继承以及菱形继承问题

继承模型

单继承:一个派生类只有一个直接基类时称这个继承关系为单继承

多继承:一个派生类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前面,后面继承的基类在后面,派生类成员在放到最后面。

菱形继承:菱形继承是多继承的一种特殊情况。菱形继承的问题,从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题,在Assistant的对象中Person成员会有两份。支持多继承就一定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

观看下面的代码,这就是典型的菱形继承的问题

cpp 复制代码
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; // 主修课程
};

int main()
{
    // 编译报错: error C2385: 对"_name"的访问不明确
    Assistant a;
    a._name = "peter";
    // 需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
    return 0;
}

无法确定指向的到底是哪个基类的_name

虚继承

很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂,性能也会有一些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之一,后来的一些编程语言都没有多继承,如Java。

cpp 复制代码
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; // 主修课程
};

int main()
{
  
    Assistant a;
    a._name = "peter";
    a.Student::_name = "xxx";
    a.Teacher::_name = "yyy";
    return 0;
}

哪个类会导致数据冗余,我们就对哪个类使用虚继承

我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后,无论是使用还是底层都会复杂很多。当然有多继承语法支持,就一定存在会设计出菱形继承,像Java是不支持多继承的,就避开了菱形继承。

多继承中的指针偏移问题

下面正确的是()

A:p1 ==p2 ==p3 B:p1 :p1 ==p3 !=p2 C:p1==p2!=p3 D:p1 !=p2 !=p3

cpp 复制代码
class Base1 {
    public: int _b1; };

class Base2 {
    public: int _b2; };

class Derive : public Base1, public Base2 {
    public: int _d; };

int main()
{
    Derive d;
    Base1* p1 = &d;
    Base2* p2 = &d;
    Derive* p3 = &d;

    return 0;
}

解决这个问题我们需要用到切割,并且知道继承顺序对基类在派生类位置的影响

p1是Base1的指针,派生类可以给基类的指针赋值,那么它指向的应该与p3相同,都是该空间最上面,而P2需要对该控件进行切割,因为它是后继承的,那么答案应该就是p1==p3!=p2。

继承和组合

public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。

• 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

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

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

• 优先使用组合,而不是继承。实际尽量多去用组合,组合的耦合度低,代码维护性好。不过也不太那么绝对,类之间的关系就适合继承(is-a)那就用继承,另外要实现多态,也必须要继承。类之间的关系既适合用继承(is-a)也适合组合(has-a),就用组合。

我们在考虑是需要思考 是'是'的关系还是'有'的关系,如果都可以,那我们优先选择'有'的关系,也就是组合关系

cpp 复制代码
//Tire(轮胎)和Car(车)更符合has-a 的关系
class Tire {
protected:
    string _brand = "Michelin";  // 品牌
    size_t _size = 17;          // 尺寸

};

class Car {
protected:
    string _colour = "白色";          // 颜色
    string _num = "陕ABIT00";        // 车牌号

    Tire _t1;                        // 轮胎
    Tire _t2;                        // 轮胎
    Tire _t3;                        // 轮胎
    Tire _t4;                        // 轮胎
};

class BMW : public Car {
public:
    void Drive() { cout << "好开-操控" << endl; }
};

// Car和BMW/Benz更符合is-a 的关系
class Benz : public Car {
public:
    void Drive() { cout << "好坐-舒适" << endl; }
};

template<class T>
class vector
{};

// stack和vector的关系,既符合is-a,也符合has-a
template<class T>
class stack : public vector<T>
{};

template<class T>
class stack
{
public:
    vector<T> _v;
};

int main()
{
    return 0;
}
相关推荐
qq_2518364571 小时前
基于java Web 哈尔滨文化活动网站毕业论文
java·开发语言·前端
cft56200_ln2 小时前
TDA4时间同步3 网卡添加虚拟时间戳
c语言·开发语言·arm开发·驱动开发·嵌入式硬件·网络协议
Brilliantwxx2 小时前
【C++】 手撕哈希表:封装 unordered_set和unordered_map
c++·哈希算法·散列表
Rookie Linux2 小时前
使用Qt6 QML以及第三方库FluentUI、PCapPlusPlus开发一个自定义抓包软件
网络·c++·qt·cmake·qml
geovindu2 小时前
go: Coroutines Pattern
开发语言·后端·设计模式·golang·协程模式
Stick_ZYZ2 小时前
A2A:让 Agent 从单兵作战走向团队协作
java·开发语言·网络·人工智能·python·ai
江屿风2 小时前
C++图论基础拓扑排序算法流食般投喂
开发语言·c++·笔记·算法·排序算法
郝学胜-神的一滴2 小时前
Qt 高级开发 030:QListWidget 右键菜单全解,从策略配置到精准删除的优雅实现
开发语言·c++·qt·程序人生·用户界面
knighthood20012 小时前
ros2-quick-runner插件v0.0.4版本发布
android·java·开发语言