当前位置: 首页 > news >正文

C++继承机制:面向对象编程的基石

目录

1 继承的概念和定义

1.1 什么是继承

1.2 继承定义

1.2.1 定义格式

1.2.2 继承基类成员访问方式的变化

1.3 继承类模板

2 基类和派生类间的转换

3 继承中的作用域

3.1 隐藏规则

3.2 两道继承作用域的选择题

4 派生类的默认成员函数

5 实现一个不能被继承的类

6 继承与友元

7 继承与静态成员

8 多继承与菱形继承问题

8.1 继承模型

8.2 虚继承

9 继承与组合


1 继承的概念和定义

1.1 什么是继承

继承机制是在面向对象编程时使代码可以复用的重要手段,可以对原有的类进行扩展,可以通过增加新的成员函数或者成员变量,产生新的类,这个类叫做派生类,也可以叫做子类;原有的类叫做基类,也可以叫做父类。继承是对类设计层次的复用

下面通过一个例子引出继承的用法,我们分别设计两个类:学生(Student)、老师(Teacher),Student和Teacher类中都有姓名/地址/电话/年龄等成员变量,都有identity身份认证函数。当然这两个类中有一些不同的成员变量和函数,比如老师的独有成员变量是职称,学生的独有成员变量是学号;老师的独有成员函数是授课,学生的独有成员函数是学习:

#include <iostream>
using namespace std;
class Student
{
public://身份认证void identity(){//...}//学习void study(){//...}
protected:string _name = "peter"; // 姓名string _address;//地址string _tel;//电话int _age = 18;//年龄int _stuid;//学号
};
class Teacher
{
public://身份认证void identity(){//...}//授课void teaching(){//...}
protected:string _name = "jack"; // 姓名string _address;//地址string _tel;//电话int _age = 28;//年龄int _stuid;//学号string _title;//职称
};

这两个类除了单独的成员变量和成员函数不同外,其余属性和方法都相同,这样就显得冗余,这时候我们可以将公共的成员放到Person类中,Student和Teacher类都继承Person,这样就可以复用这些成员,就不去重复定义了:

class Person
{
public://身份认证void identity(){cout << "void identity()" << _name << endl;}
protected:string _name = "jack"; // 姓名string _address;//地址string _tel;//电话int _age;//年龄
};
class Student :public Person
{
public://学习void study(){//...}
protected:int _stuid;//学号
};
class Teacher :public Person
{
public://授课void teaching(){//...}
protected:string title;//职称
};int main()
{Student s;Teacher t;//都能调用到identity函数s.identity();t.identity();return 0;
}

1.2 继承定义

1.2.1 定义格式

上述中我们看到的Person是基类,也称作父类。Student是派生类,也称作子类,用一张图举例表明继承的定义:

继承方式有三种:public继承、protected继承、private继承,而基类中不同访问限定符(public,protected,private)的成员,在不同继承中有对应的规定,下面用一个表格说明:

1.2.2 继承基类成员访问方式的变化

类成员/继承方式public继承protected继承private继承
基类的public成员派生类的public成员派生类的protected成员派生类的private成员
基类的protected成员派生类的protected成员派生类的protected成员派生类的private成员
基类的private成员在派生类中不可见在派生类中不可见在派生类中不可见

这个表格可以理解性的记忆。

1 基类中的private成员无论以什么方式都是不可见的,也就是说,私有成员虽然继承到了派生类中,但是在派生类内部和外部都不可以去访问基类的私有成员。

2 如果基类成员不想在类外被访问,在派生类中可以访问,就定义为protected成员。protected成员是因继承才被创造出来。

3 基类的其他成员在派生类的访问方式等于min{成员在基类的访问限定符,继承方式},其中public>protected>private。比如基类的protected成员使用public继承,在派生类就是protected成员;基类的public成员使用protected继承,在派生类就是protected成员,取二者的较小方。

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

5 在实际使用中一般都是public继承,不提倡使用protected/private继承,原因是protected/private继承下来的成员都只能在派生类里面使用,扩展维护性不强。

1.3 继承类模板

一个类也可以继承库里面的类模板,如vector,list等,举例说明:

namespace as
{template <class T>class stack :public std::vector<T>{public:void push(const T& x){push_back(x);}void pop(){pop_back();}const T& top(){return back();}bool empty(){return empty();}};
}
int main()
{as::stack<int> st;st.push(1);st.push(2);st.push(3);st.push(4);while (!st.empty()){cout << st.top() << " ";st.pop();}cout << endl;return 0;
}

可以通过继承vector模板来模拟实现stack的一些功能,但是这么写运行时会编译报错,原因在于调用函数时没有指定类域,在main函数中,stack<int>实例化时,vector<int>也实例化,因为模板是按需实例化,如果成员函数没有实例化,就会报错:

namespace as
{template <class T>class stack :public std::vector<T>{public:void push(const T& x){//需要指定类域vector<int>::push_back(x);}void pop(){vector<int>::pop_back();}const T& top(){return vector<int>::back();}bool empty(){return vector<int>::empty();}};
}
int main()
{as::stack<int> st;st.push(1);st.push(2);st.push(3);st.push(4);while (!st.empty()){cout << st.top() << " ";//4 3 2 1st.pop();}cout << endl;return 0;
}

2 基类和派生类间的转换

public继承的派生类对象可以赋值给基类的指针或者基类的引用。这么做就是将派生类中基类的那部分切出来,基类的指针或引用指向的是派生类中切出来的基类那部分。

class Person
{
protected:string _name;string _gender;int _age;
};class Student :public Person
{
public:int _No;
};int main()
{Student s;//派生类对象可以赋值给基类的指针或引用Person* pp = &s;Person& rp = s;//基类对象不能赋值给派生类对象Person p;s = p;//错误return 0;
}

基类对象不能赋值给派生类对象,派生类对象可以赋值给基类对象。

3 继承中的作用域

3.1 隐藏规则

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

派生类和基类中有同名成员,派生类成员将屏蔽基类同名成员的访问,这种情况叫隐藏,如果在派生类中想要访问,需要使用基类::基类成员的方式进行访问。

class Person
{
protected:string _name = "peter";int _num = 111;
};
class Student :public Person
{
public:void Print(){cout << _name << endl;//打印petercout << Person::_num << endl;//打印111cout << _num << endl;//打印999}
protected:int _num = 999;
};
int main()
{Student s;s.Print();return 0;
}

3.2 两道继承作用域的选择题

1 A和B类的两个func构成什么关系()

A 重载  B 隐藏  C 没关系

2 下面程序的编译运行结果是什么?

A 编译报错 B 运行报错 C 正常运行

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

答案:B C

4 派生类的默认成员函数

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

class Person
{
public:Person(const char* name="xxx"):_name(name){cout << "Person()" << endl;}
protected:string _name;
};
class Student :public Person
{
public:Student(const char* name,int num,const char* address):Person(name)//编写方式 基类名(成员),_num(num),_address(address){}
protected:int _num = 1;string _address = "北京市";
};
int main()
{Student s("zhangsan",1,"北京市");return 0;
}

(2)派生类的拷贝构造函数必须调用基类的拷贝构造函数完成对基类的拷贝初始化,一般来说调用基类的拷贝构造函数就够用了,如果派生类有深拷贝的资源,才需要在派生类内部实现拷贝构造

class Person
{
public:Person(const char* name="xxx"):_name(name){cout << "Person()" << endl;}//拷贝构造Person(const Person& p):_name(p._name){cout << "Person(const Person & p)" << endl;}
protected:string _name;
};
class Student :public Person
{
public:Student(const char* name,int num,const char* address):Person(name),_num(num),_address(address){}//可以不用写,有深拷贝资源再写Student(const Student& s):Person(s)//派生类对象可以传给基类引用或指针,_num(s._num),_address(s._address){ }
protected:int _num = 1;string _address = "北京市";
};
int main()
{Student s("zhangsan",1,"北京市");Student s2(s);return 0;
}

(3)派生类的赋值重载必须要调用基类的operator=完成基类的复制,注意如果派生类写operator=,会隐藏基类的operator=,需要指定基类作用域显示调用基类的operator=

class Person
{
public:Person(const char* name="xxx"):_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(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,const char* address):Person(name),_num(num),_address(address){}//可以不用写,有深拷贝资源再写Student(const Student& s):Person(s)//派生类对象可以传给基类引用或指针,_num(s._num),_address(s._address){ }//可以不用写,有深拷贝资源再写Student& operator=(const Student& s){if (this != &s){//基类和派生类operator=构成隐藏关系Person::operator=(s);//派生类对象可以传给基类的引用_num = s._num;_address = s._address;}}
protected:int _num = 1;string _address = "北京市";
};
int main()
{Student s("zhangsan",1,"北京市");//Student s2(s);Student s2 = s;return 0;
}

(4)派生类的析构函数会在被调用完成后自动调用基类析构函数清理基类成员。因为这样能保证派生类对象先清理,再清理基类成员的顺序,派生类和基类析构成隐藏关系,具体到后面的多态再详细说明。

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

(6)派生类对象析构先调用派生类析构再调用基类析构

class Person
{
public:Person(const char* name="xxx"):_name(name){cout << "Person()" << endl;}//析构~Person(){cout << "~Person()" << endl;}
protected:string _name;
};
class Student :public Person
{
public:Student(const char* name,int num,const char* address):Person(name),_num(num),_address(address){}~Student(){//有需要显示释放的资源,才需要自己实现//析构顺序,先子后父//子类的析构和父类的析构构成隐藏关系}
protected:int _num = 1;string _address = "北京市";
};
int main()
{Student s("zhangsan",1,"北京市");//Student s2(s);//Student s2 = s;return 0;
}

5 实现一个不能被继承的类

方法1:基类的构造函数私有,派生类的构成无法调用基类的构造函数,派生类无法实例化对象

方法2:C++11中新增了一个final关键字,final修改基类,派生类就不能继承了,这个相较于方法1常用。

class Base final//这个类不能被继承
{
public:void func(){cout << "Base::func" << endl;}
protected:int a = 0;
};

6 继承与友元

友元关系不可以被继承,基类有友元声明,不可以访问派生类的私有和保护乘员。解决办法是在派生类中也进行友元声明:

class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name="zhangsan";
};class Student :public Person
{friend void Display(const Person& p, const Student& s);//让Display也成为Student,因为友元不能被继承
protected:int _num=1;
};void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._num << endl;
}
int main()
{Person p;Student s;Display(p, s);return 0;
}

7 继承与静态成员

如果基类定义了static静态成员,无论派生出多少个类,都只有这一个static成员实例。

class Person
{
public:string _name;static int _count;
};int Person::_count = 0;
class Student :public Person
{int _num;
};
int main()
{Person p;Student s;cout << &p._name << endl;cout << &s._name << endl;cout << endl;cout << &p._count << endl;cout << &s._count << endl;return 0;
}

运行结果:

可以看到非静态成员的地址是不一样的,说明派生类和基类对象各自有一份。静态成员的地址是一样的,说明派生类和基类共用同一份静态成员。

8 多继承与菱形继承问题

8.1 继承模型

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

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

class A
{
public:string _name;
};
class B
{
protected:int _num;
};
class C :public A,public B//多继承
{
protected:int _id;
};

菱形继承:菱形继承是多继承的一种特殊情况,举例:

class Person
{
public:string _name;
};
class Student :public Person
{
protected:int _num;
};
class Teacher :public Person
{
protected:int _id;
};
class Assistant :public Teacher, public Student//菱形继承
{
protected:string _majorCourse;
};int main()
{Assistant a;a._name = "peter";//这里会报错:“对_name的访问不明确”,因为Student和Teacher都有成员变量_name,产生二义性问题//需要显示指定访问哪个基类成员访问二义性问题,但是数据冗余问题无法解决a.Student::_name = "peter";a.Teacher::_name = "peter";return 0;
}

我们写代码时不要设计出菱形继承的模型,在Assistant的对象中Person成员会有两份,造成数据冗余,支持多继承就一定会有菱形继承,所以使用多继承时要谨慎,避免写出菱形继承。

8.2 虚继承

在菱形继承中可以使用虚继承解决菱形继承带来的数据冗余和二义性问题

class Person
{
public:string _name;
};//使用虚继承Person类
class Student :virtual public Person
{
protected:int _num;
};
//使用虚继承Person类
class Teacher :virtual public Person
{
protected:int _id;
};
class Assistant :public Teacher, public Student//菱形继承
{
protected:string _majorCourse;
};int main()
{Assistant a;//使用虚继承,解决数据冗余和二义性a._name = "peter";return 0;
}

即使使用菱形虚拟继承,无论是使用还是底层都会复杂很多,不建议设计出菱形继承。

9 继承与组合

  • public继承是一种is-a的关系。就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
  • 基类的改变,对派生类有很大影响,派生类和基类依赖性强,耦合高,这种通过生成派生类的复用叫做白箱复用。
  • 对象组合是类继承之外的另一种复用选择,组合类没有很强的依赖关系,耦合度低。这种复用被称为黑箱复用。
  • 我们常说写出来的代码要“高内聚,低耦合”,所以优先使用组合,而不是继承。继承也有它的用武之地,不过当类的关系既适合用继承(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;}

以上就是有关继承的内容,如果这篇文章对你有用,可以点点赞哦,你的支持就是我写下去的动力,后续会不断的分享知识。

http://www.dtcms.com/a/545789.html

相关文章:

  • 公司网站设计很好的怎么看一个网站是什么时候做的
  • pc不同网段间的通信过程
  • 成功移植游戏《四叶苜蓿》第二章——支持Linux和龙芯
  • 移动网站开发百科评价校园网站建设范例
  • 网站建设 别墅国家信用信息公示系统查询入口
  • 实验室安全教育与管理平台学习记录(二)化学类安全2
  • 武功县住房与城乡建设局网站太阳能建设网站
  • SAP-ABAP:SAP ABAP中的数学艺术:掌握向上取整与向下取整实例详解
  • OpenEuler中mysql这是在执行 MySQL 密码重置操作时出现的 “找不到mysqld_safe命令” 的错误场景。
  • GXDE 25.1 发布:系统优化若干
  • 【开题答辩全过程】以 病虫害监测管理系统的设计与实现为例,包含答辩的问题和答案
  • 深入浅出 MQTT:轻量级消息协议在物联网中的应用与实践
  • 天津网站开发自己怎么做商城网站视频教程
  • flash-attn安装卡在Building wheel for flash-attn (setup.py)
  • 【人工智能数学基础】什么是高斯分布/正态分布?
  • 医院网站建设策划怎么注册国外网站
  • 广州专业建网站公司微电影制作
  • 做网站买什么服务器上蔡专业网站建设
  • 计算机网络自顶向下方法15——应用层 P2P文件分发与BitTorrent协议
  • 深入理解 UDP:从协议基础到可靠实现与 QUIC 演进
  • wordpress 站点地址一个人建设小型网站
  • [人工智能-大模型-105]:模型层 - 为什么需要池化层,池化层的物理意义
  • 引流推广推广微信hyhyk1效果好亚马逊seo是什么
  • 统信桌面专业版安装应用显示架构不匹配怎么处理
  • Sqoop将MySQL数据导入HDFS
  • Rust 中的数据结构选择与性能影响:从算法复杂度到硬件特性 [特殊字符]
  • 做电脑网站手机能显示做网站学哪方面知识
  • 测试开发话题04---用例篇(1)
  • 44-基于ZigBee和语音识别的智能家居控制系统设计与实现
  • 锂离子电池恒流恒压充电(CC-CV)Simulink仿真模型