c++: 继承(上)

继承的概念与定义

继承的概念

继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类(也叫子类)。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤。
下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/
电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们
也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣
的独有成员函数是学习,⽼师的独有成员函数是授课

cpp 复制代码
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 = "张三";   // 姓名
// 年龄
int _age = 18;          
string _address;  // 地址      
string _tel;   // 电话         
string _title;          
};
int main()
{
return 0;
}

我们简单看一下这两个类,是不是发现有些成员函数和成员变量都是一样的这样就很冗余,所以接下来的讲的继承就能解决这个冗余提高代码的质量

继承的定义

继承就是把上面学生和老师的一些相同重复的属性统一放在一个类里,这个类也叫做基类(也可以叫父类)。然后学生类和老师类里放它们独有的成员,它们就做派生类(也叫做子类)。

为了理解可以类比函数的复用,比如我们以前写的STL容器中vector和string里的扩容函数,里面的插入函数里不就复用了扩容函数类比到这里我们把上面学生和老师的一些相同重复的属性统一放在一个类叫做person,它就相当于是要复用的类,学生类和老师类继承它的所有成员,如下面的person类:

cpp 复制代码
//基类
class person {
public:
	void identity()
	{
		cout << "identity()" << _name << endl;
	}
protected:
//private:
	string _name="张三";
	string _address;  //地址      
  string _tel; //电话号码           
  int _age = 18;  //年龄

};

我这里就拿学生类来打比,现在我们的学生类就只需要包含下面这些:

cpp 复制代码
class Student : public Person
{
public:
// 学习
void study()
{
// ...
}
protected:
int _stuid;             
};


int main
(
student s;
)

我们可以看到s的成员里面有person的和自己独有的

语法的讲解

继承方式的不同可能会影响基类被派生类继承的效果不同,意思是基类类的成员在派生类类的访问限定符会变化,这个变化的公式就是:基类成员在派生类类的访问限定符= min(在基类的限定符,继承方式) 访问限定符的比较为 public> protected>private

举个例子,就如上面的图片 class student : public person 以public继承,在public里的成员继承到student类里也是public, protected的成员在student类里是protected.

private成员与protected的区别

如果在基类是public成员,继承方式为public那没话说,在派生类里基类继承的所有public成员在派生类里和类外都能被使用

但是private不同,如果是private成员,不管派生类以什么方式继承,基类继承到派生类的成员在派生类里和类外都不能使用,

这样可不行呐,那我继承到你派生类不能用可不行,我的结果是你继承到我里面的成员要在类里能使用在类外我受保护不能被使用才好

所以祖师爷就新加了一个protected的访问限定符,这个限定符被继承到派生类就能达到在类里能使用在类外不能使用。

但注意你的继承方式如果是private那在派生类就变成了private,所以继承方式很重要,我们实际开发中大部分情况都是用public,这样继承下来的限定符就是基类的限定符,你可以以你的初衷去设计我想让派生类访问就用public和protected,不想让任何人访问就用prviate

继承类模板

cpp 复制代码
namespace TAO
{
//template<class T>
//class vector
//{};
// stack和vector的关系,既符合is-a,也符合has-a 
template<class T>
class stack : public std::vector<T>
{
public:
void push(const T& x)
{
// 基类是类模板时,需要指定⼀下类域,
  vector<T>:: push_back(x)
// 否则编译报错:error C3861: "push_back": 找不到标识符
 ............//还有其他函数这里省略

这里实现的栈就是继承vector的所有成员,这里使用基类vector的函数时需要指定类域,虽然vector类被实例化了,但它的函数没有被实例化,所以这里的插入函数push需要如上那样写要显示写vector

基类和派⽣类间的转换

通常情况下我们把⼀个类型的对象赋值给另⼀>个类型的指针或者引⽤时,存在类型转换,中间会产 ⽣临时对象,所以需要加const,如: int a = 1; const double& d = a;

cpp 复制代码
int a = 1;
//double& b = a;//报错,因为a会产生临时的double对象,并且临时对象是具有常性,这里的引用本身是权限的放大
const double& b = a;//加const就是符合左右两边都是常性对象的引用

public继承中,就是⼀个特殊处理的例外,派⽣类对象可以赋值给基类的指针/基类的引⽤,⽽不需要加const,这⾥的指针和引⽤绑定是派⽣类对象中的基类部分,如下图所⽰。也就意味着⼀个基类的指针或者引⽤,可能指向基类对象,也可能指向派⽣类对象。

cpp 复制代码
student s;
person* p1 = &s;//父类对象指向的成员会是父类原本拥有的,派生类自己的不会给父类
person& p2 = s;//并且类型转换不会产生临时对象,这是对继承的特殊处理
person p3 = s;
//student s = p3;//目前的知识暂时认为父类不可以作为子类的对象,以后可以用安全转换类型后面再说

这里的派生类转换父类的指针和引用和对象中的成员只包含基类中有的,派生类独有的成员不会被指向,并且你现在可以认为只有派生类类->基类, 基类->派生类类不行,后面需要学安全转换的知识才能实现,所以我们这里暂时认为不行。这种特殊处理对下面要讲的在派生类写默认构造函数有大用

继承中的作用域

隐藏规则:
1. 在继承体系中基类和派⽣类都有独⽴的作⽤域。
2. 派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
(在派⽣类成员函数中,可以使⽤基类::基类成员显⽰访问)
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
4. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员

而且如果调用函数或要使用成员变量它会就近原则先在当前的派生类找,没找到才去基类找,所以如果有同名函数和变量会优先使用派生类的独有的函数和成员变量

派⽣类的默认成员函数

4个常⻅默认成员函数
6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这
⼏个成员函数是如何⽣成的呢?

构造函数

如果我们不在派生类写构造函数编译器会自动生成,这里的构造域类与对象的构造高度相似,派生类中的基类的成员要在基类的构造函数构造,派生类独有的就在自己这里构造与类与对象中的构造方式一样

这里就简单写个person类,它是基类

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

    ~Person()
    {
        cout << "~Person()" << endl;
    }
protected:
    string _name; // 姓名

};`
cpp 复制代码
class student : public Person
{
public:
    //默认构造函数
    //构造函数分为两部分
    //1. 基类的成员(整体构造,用它自己的构造函数)
    //2. 派生类剩下的成员跟以前类与对象构造一样;
    student(const char* name,int id, const char* address)
       // :_name(name)不行需要整体构造
        :Person(name)
        ,_id(id)
        , _address(address)
    {}
protected:
    int _id;
    string _address;
};

而且是基类的成员变量的构造要是个整体,所以在初始化列表中就用基类的匿名对象构造就行,基类的成员有什么参数写上去就行,而且初始化列表的构造顺序是先构造继承基类的成员,再是派生类自己的

拷贝构造函数

cpp 复制代码
Person(const Person& p)//基类的拷贝构造
        : _name(p._name)
    {
        cout << "Person(const Person& p)" << endl;
    }


 student(const student& s)
     :Person(s)
     ,_id(s._id)
     ,_address(s._address)
 {
     cout << " student(const student& s)" << endl;
 }

这里就能体现在特殊化类型转换在继承中的效果,这里的初始化,复制基类的成员会去基类这个类里去调用它的拷贝构造,这里的p就是s不是临时对象,如果这里是临时对象那就会无限递归就会出错。

赋值重载 =

cpp 复制代码
  student& operator=(const student& s)
  {
    Person:: operator=(s);
      _id = s._id;
      _address = s._address;
      return *this;
  }

理解了前面几个这个就很好理解,派生类中的基类成员那就调用基类的赋值重载,但是注意这里有个隐形的坑,那就是函数名隐藏,这里的派生类的函数名与派生类的函数名构成隐藏,所以这里调用基类的operator=()需要指定类域,不然会内存泄漏

析构函数

cpp 复制代码
 ~student()
 {
  // Person:: ~Person();//不用显示写编译器默认会有,而且是先析构子类在析构父类,显示写了就会先析构父类,这种有些情况会有问题
   _id = 0;           //所以编译器就默认自己生成析构基类的析构函数
   _address = "";

 }

析构要格外注意,以我们顺下来的思路我们肯定会认为要先写基类的析构再析构派生类的成员,但这里不是,这里编译器会自己生成默认的基类的析构函数,而且析构的顺序为先析构派生类成员再析构基类成员 因为析构中如果要用到基类的成员去当条件时,如果析构了基类那就会有矛盾,所以就为了避免这种情况那干脆编译器就默认再派生类的析构函数中生成基类的析构函数,所以不用写,如果写了就会析构两次就错误了

实现⼀个不能被继承的类

⽅法1:基类的构造函数私有,派⽣类的构成必须调⽤基类的构造函数,但是基类的构成函数私有化以
后,派⽣类看不⻅就不能调⽤了,那么派⽣类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个final关键字,final修改基类,派⽣类就不能继承了。

cpp 复制代码
// C++11
的⽅法

class Base final
{
public:
    void func5() { cout << "Base::func5" << endl; }
protected:
    int a = 1;
private:
    // C++98
    的⽅法

        /*Base()
        {}*/
};
class Derive :public Base
{
    void func4() { cout << "Derive::func4" << endl; }
protected:
    int b = 2;
};
int main()
{
    Base b;
    Derive d;
    return 0;
}

继承与友元

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

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

这里会报错,Display是person的友元函数,不是student的友元函数,所以Display不能访问student的成员变量,一个形象的例子是:你爸爸的朋友不是你的朋友,所以你爸爸的财产可以继承你朋友的你不能继承,所以这里的Display不能使用student中的成员变量

相关推荐
程序员小羊!18 小时前
06Java 异常机制与常用类
java
2401_8734794018 小时前
企业安全运营中,如何用IP离线库提前发现失陷主机?三步实现风险画像
网络·数据库·python·tcp/ip·ip
weixin_5231853218 小时前
Java基础知识总结(四):引用数据类型与参数传递机制
java·开发语言·python
小欣加油18 小时前
leetcode994 腐烂的橘子
数据结构·c++·算法·leetcode·bfs
宸津-代码粉碎机19 小时前
Spring AI企业级实战|从RAG优化到Agent多工具调度
java·大数据·人工智能·后端·python·spring
噢,我明白了19 小时前
QueryWrapper的使用
java
Chase_______19 小时前
【Java基础 | 15】集合框架(中):Set、HashSet、TreeSet 与哈希表
java·windows·散列表
摇滚侠19 小时前
Maven 入门+高深 微服务案例 122-125
java·微服务·maven
.千余19 小时前
【C++】手写双向链表:list容器模拟实现
开发语言·c++·笔记·学习·其他
QuZero19 小时前
Guava Cache Deep Dive
java·后端·算法·guava