反转基因福娃 发表于 3 小时前

C++之继承

本节我们将要学习C++作为面向对象语言的三大特性之一的继承。
媒介
   一、继承的概念
二、继承的界说
2.1 界说格式
2.2 继承基类成员访问方式的变革
2.3 继承类模板
三、基类和派生类之间的转换
四、继承中的作用域
五、派生类的默认成员函数
六、实现一个不能被继承的类
七、继承与友元
八、继承与静态成员变量
九、多继承及其菱形继承标题
9.1 继承模子
9.2 虚继承
十、继承和组合
继承(Inheritance)
组合(Composition)
总结

媒介
我们在之前的学习中,已经学习过了C++中面向对象中三大特性之一的封装,这一特性通过将对象的各种属性和方法通过一个类包装在一起,如许就可以或许提供清楚接口,埋伏内部实现,提拔代码质量和维护性。本节我们就来看看C++中的另一个告急特性——继承。

一、继承的概念

什么是继承呢?我们先从字面上猜一下,继承岂非就是从一个东西上来获取一些东西来给自己来使用嘛。在C++中如许说就有点太抽象了,我们现在在C++中使用最多的就是类,那么继承应该也是围绕着类来做手脚的吧,着实究竟确实云云。继承机制是面向对象步调计划使代码可以复用的一个告急本领,它允许我们在原来的类上举行扩展,增长新的属性(成员变量)和方法(成员方法),从而天生一个新的类,称之为派生类。继承出现了面向对象步调计划的条理布局,表现了由简单到复杂的认知过程。从前我们打仗的是函数条理的复用,而继承就是类计划条理的复用。

二、继承的界说

2.1 界说格式

我们可以看到下面的图片中:Student是派生类,它是继承自基类Person,在两个类中心另有一个继承方式。(由于翻译的差异,基类也叫做父类,派生类也叫做子类)
https://i-blog.csdnimg.cn/direct/549b81c7e9b749c89db29ebbdea53de2.png
如下代码是一个简单的继承,我们将那些派生类所共有的属性/方法都放到了基类中去了,然后我们将派生类从基类中举行继承,然后再在派生类中增长一些属性/方法。从监视窗口,我们可以看出来派生类实例化出来的对象是具有基类的全部属性的。
class animal
{
public:
        void eat()
        {}

        void sleep()
        {}

protected:
        int age=3;
        string name;
        string color="黄色";
};

class Cat :public animal
{
public:
        void Grapmouse()
        {}

protected:
        string address="安徽";
        string name="mimi";

};

class Dog :public animal
{
public:
        void bark()
        {}

protected:
        string food="骨头";
        string name="wangwang";

};


int main()
{
        Cat mimi;
        Dog wangwang;
        return 0;
}

https://i-blog.csdnimg.cn/direct/50d3e8719e504fc083c11671863416cd.png
2.2 继承基类成员访问方式的变革

https://i-blog.csdnimg.cn/direct/be37c724c6874ae7a04b013ba446bd10.pnghttps://i-blog.csdnimg.cn/direct/a60976eac31d4ab5b1ef0e5e92df7b1e.png
上面两个图是我们的继承方式和访问限定符的汇总,我们可以清楚地看出,两个的内容是一样的,都是public,protected,private。我们在之前的学习中已经根本相识了public,private这两个的用法了,一个是公有的,无论是类外照旧类内都是可以举行访问的,另一个是私有的,只可以或许在类内举行访问在类外貌是绝对不可以举行访问的。本日我们又要学习一个新的访问限定符——protected.这个可以使派生类可以访问到基类的成员变量/成员方法。
说了这么多,为什么要用这三个类似的关键字来表现两个方式呢?由于我们背面在继承中,不能简单地通过某一种方式来确定其访问方式了,我们必要通过上面两种方式综合来确定其访问方式。
https://i-blog.csdnimg.cn/direct/ce36c306d88f44f4a83c7b5d61811d93.png
现在我就根据上面表格简单说一说,访问方式的规则:
1.对于基类的继承方式是private的话,那么岂论派生类是什么(public/protected/private)都是无妨举行访问基类中的成员的;
2.对于基类的继承方式是public或protected的话,我们可以如许来举行判定访问权限:派生类的访问权限=Min(基类的继承方式,派生类的访问方式);
3.我们一样寻常都是使用public来作为基类的继承方式的,由于如许才气很好的实现继承这一特性,假如我们使用protected/private来作为基类的继承方式,就会很轻易造成派生类不可见的情况,实际扩展性不强,那么继承又有什么意义呢?
4.我们使用差异的关键字(class/struct)来界说类时,假如我们不表现写继承方式的话,其默认的继承方式和之前的是一样的:class——private,struct——public。我们发起自己表现写继承方式,那样就会淘汰许多不须要的贫苦。
//实例展示不同限定访问符与继承方式
class animal
{
public:
        void eat()
        {}
protected:
        int age = 3;
private:
        int id = 110;
};

class Cat :private animal
//class Cat :protected animal
//class Cat :public animal
{
public:
        void mouse()
        {}
protected:
        string name="mimi";
private:
        string food = "fish";
};

int main()
{
        Cat a;
        a.mouse();
        return 0;
} https://i-blog.csdnimg.cn/direct/258b824eebc443918d84edb31b8ce91f.png
我们使用private作为基类的继承方式,我们从监视窗口可以看出来,我们固然不可以或许访问基类的成员,但它们并非是不存在的,我们只是不可见而已,这点我们必要注意一下。

2.3 继承类模板

这个我们使用之前模仿实现stack作为例子,我们之前模仿实现stack是通过类模板和容器适配器(复用容器)的。现在我们学习了继承,我们是不是可以通过继承来获取基类的一些成员,从而模仿实现出来呢?
namespace hjc
{
        //template<class T>
        //class vector
        //{};
        // stack和vector的关系,既符合is-a,也符合has-a
        template<class T>
        class stack : public vector<T>
        {
        public:
                void push(const T& x)
                {
                        // 基类是类模板时,需要指定⼀下类域,
                        // 否则编译报错:error C3861: “push_back”: 找不到标识符
                        // 因为stack<int>实例化时,也实例化vector<int>了
                        // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
                        vector<T>::push_back(x);
                        //push_back(x);
                }
                void pop()
                {
                        vector<T>::pop_back();
                }
                const T& top()
                {
                        return vector<T>::back();
                }
                bool empty()
                {
                        return vector<T>::empty();
                }
        };
}
int main()
{
        hjc::stack<int> st;
        st.push(1);
        st.push(2);
        st.push(3);
        while (!st.empty())
        {
                cout << st.top() << " ";
                st.pop();
        }
        return 0;
}
我们从上面的模仿实今世码,我们可以看出来我们是将stack作为vector的派生类来天生的,我们在模仿实现stack的成员方法时,是直接通过复用基类的成员方法来举行实现的。但是我们每次复用基类的成员方法时,我们都必要使用作用域运算符来指定类域,固然按原理来说,我们实例化出来派生类了,基类不应该也实例化出来了嘛,究竟确实云云,但是基类并不是一把实例化出来的,他的有些成员还没实例化出来,这时间我们就必要自己手动指示类域举行实例化了(假如我们不指明的话,编译器就会报未声明的报错),这就是我们之前所说的按需实例化。

三、基类和派生类之间的转换

我们在之前的学习中,对于一些范例之间的转换,偶尔候在转换中出现一些暂时变量,这些变量具有常性,因此就无法赋值给寻常范例的变量,由于如许就会造成权限的放大(如许是违法的),因此我们一样寻常都是必要加上const,才气克制报错。
https://i-blog.csdnimg.cn/direct/67d2ebb6012e46628f4a059e50d5fe4e.png
但是到了我们继承这,有一种特别的使用——public继承的派生类可以直接赋值给基类的指针/基类的引用了。它们之间不会产生暂时变量,因此也不具有常性。这是未为什么呢?这是由于派生类是通过继承基类产生的,派生类具有基类中的成员,因此我们可以直接将派生类中基类的那一部门直接给基类。有个形象的说法是切片/切割。寓意是把派生类中基类的那一部门切割下来,然后基类的指针和引用指向派生类被切割出来的这一部门。
基类是不可以直接赋值给派生类指针或引用的,由于基类有的,派生类也有,而派生类有的,基类大概没有。
基类的指针大概引用可以通过逼迫范例转换赋值给派生类的指针大概引用。但是必须是基类的指针 是指向派生类对象时才是安全的。这里基类假如是多态范例,可以使用RTTI(Run-Time Type  Information)的dynamic_cast 来举行辨认后举行安全转换。
https://i-blog.csdnimg.cn/direct/f09bdb04d1954dad9169b66c89fc9e59.png
class Person
{
protected :
string _name; // 姓名
string _sex; // 性别
int _age; // 年龄
};
class Student : public Person
{
public :
int _No ; // 学号
};
int main()
{
Student sobj ;
// 1.派⽣类对象可以赋值给基类的指针/引⽤
Person* pp = &sobj;
Person& rp = sobj;

// ⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
Person pobj = sobj;

//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
sobj = pobj;

return 0;
}

四、继承中的作用域

在C++中基类与派生类是两个差异的作用域,我们不能以为派生类是由基类继承下来的,那么它们两个就是共有一个作用域,这是不对的。
埋伏规则
1.派生类中假如有与基类中同名的成员变量,那么派生类就会屏蔽基类中的同名成员变量,这叫做埋伏(假如我们想要使用基类中的谁人同名成员变量,我们就表现调用 基类::同名成员变量);
2.对于成员函数,只要两者的函数名类似,那么两者就会构造埋伏关系,不必看函数参数是否类似;(我们之前的函数重载,必须要求是在同一个作用域中,函数名类似,函数参数差异才构成函数重载)
3.注意我们在实际中发起不要使用同名成员。

class Person
{
public:

protected:
        int num = 12345; //身份证号码
        int age = 18;
};

class Student :public Person
{
public:
        void print()
        {
                cout << Person::num << endl;
                cout << num << endl;
        }
protected:
        int num = 999; //学号
};

int main()
{
        Student s;
        s.print();
        return 0;
} https://i-blog.csdnimg.cn/direct/d542067eba3645808379d2cb1dbb0a36.png

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

https://i-blog.csdnimg.cn/direct/d6f5a27c9b914e5d9d329e30f644e72d.png
上面是我们类中编译器默认天生的六种默认成员函数,由于背面两种成员函数不是非常告急,我们侧重先容上面四种成员函数。
派生类本质也是一种类,只不外它与基类有着千丝万缕的关系。现在我们来先容一下派生类的四个默认成员函数。
1.派生类的构造函数必须调用基类的构造函数来初始化基类的那一部门成员,假如基类中没有的那一部门就必要我们自己在派生类的初始化列表中自己来表现初始化;
2.派生类的拷贝构造必须调用基类的拷贝构造完成基类的拷贝初始化;
3.派生类的operator=必须调用基类的operator=来完成基类的赋值,别的的部门必要我们自己来实现赋值。我们必要注意的是派生类的operator=与基类的operator=构成了埋伏关系,因此我们调用基类的operator=时必要自己表现来举行调用;
4.派生类的析构函数会在被调用完后自动调用基类的析构函数来整理基类内村,如许就能确保析构次序正确(先调用的后析构);
5.构造函数的调用次序:先基类再派生类。析构函数的调用次序:先派生类再基类;
6.由于多态中一些场景析构函数必要构成重写,重写的条件之一是函数名类似(这个我们多态章节会解说)。那么编译器会对析构函数名举行特别处理处罚,处理处罚成destructor(),以是基类析构函数不加 virtual的情况下,派生类析构函数和基类析构函数构成埋伏关系。
https://i-blog.csdnimg.cn/direct/732309991b304ae38db92a907e27c950.png
如下代码是四个默认成员函数的表现实现方式。我们可以看到派生类的四个默认成员函数的表现实现方式:它根本都是调用了基类的默认成员函数,然后自己再增补点基类中所没有的成员变量初始化/赋值。但是在析构函数的表现实现中,我们并没有看到调用了基类的析构函数,这是由于我们表现实现了派生类的析构函数后,基类的析构函数会在派生类的析构函数调用完后自动举行调用。假如我们再表现写基类的析构函数的话,就会造成基类析构函数出现好几个的情况。
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 ; // 姓名
};

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;
   }
protected :
int _num ; //学号
};

int main()
{
   Student s1 ("jack", 18);
   Student s2 (s1);
   Student s3 ("rose", 17);
   s1 = s3 ;
return 0;
}
六、实现一个不能被继承的类

我们假如想要实现一个不可以或许被继承的类,我们有如下两种方法:
方法一:这种方法有点粗暴,我们直接将我们的基类放在private成员那里,那样我们就不能使用派生类来举行继承了;
方法二:在派生类继承基类时,在基类的旁边加上一个关键字final,如许就可以或许逼迫限定它不可以或许被继承了。这种方法是在C++11中才开始实行的。
// 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;
}

七、继承与友元

友元是在我们刚刚学习类不久就开始打仗到了,我们什么时间使用友元呢?我们界说的一个函数的参数是不可以或许省略的(作为类的成员函数有一个埋伏的参数),于是我们就必要自己在类外貌实现。但是我们背面要在类中调用这个函数,于是我们就必要将这个函数定为友元函数(在谁人函数声明前加上一个关键字friend,再放到类中)。友元在继承中同样实用的,究竟它们的本质都是类,但是友元是不可以或许继承的,也就是说假如我们在基类中界说了一个友元函数,它的派生类是不能继承这个友元函数的。打个比方:好比说你的爸爸和王叔叔是在战场上出生入死过的战友,有着过命的友爱,平常你爸爸跟王叔叔假如乞贷的话也就是一句话的事,但是你找王叔叔乞贷的话也是一句话的事嘛?这显然是不太大概的,究竟你跟王叔叔不熟,人家不会一句话就把钱借给你。因此,假如你也想像你爸爸和王叔叔那样,你就要和王叔叔打好关系,与他做过命的朋侪,到当时间乞贷也就是一句话的事了。
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;
} 八、继承与静态成员变量

我们知道静态成员变量是放在静态区的,它是在全局存在的,因此假如我们在基类中界说了一个静态成员变量的话,那么派生类在继承基类的谁人静态成员变量就是同一个了。我们之前所说的继承成员变量,都是派生类根据基类重新界说的一个变量并不是原版的了。基类界说了static静态成员,则整个继承体系⾥⾯只有⼀个如许的成员。无论派生出多少个派生类,都只有一个static成员实例。
class Person
{
public:
string _name;
static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
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;
}
九、多继承及其菱形继承标题

9.1 继承模子

单继承:当一个派生类只有一个直接基类时,我们就称之为单继承;
多继承:当一个派生类有两个或以上的直接基类时,我们称之为多继承,多继承对象在内存中的模子是:先继承的在前面,后继承的在背面,末了一个是派生类成员;
菱形继承:菱形继承是多继承的⼀种特别情况。菱形继承的标题,从下面的对象成员模子构造,可以 看出菱形继承有数据冗余和二义性的标题,在Assistant的对象中Person成员会有两份。支持多继承就肯定会有菱形继承,像Java就直接不支持多继承,规避掉了这里的标题,以是实践中我们也是不发起计划出菱形继承如许的模子的。
https://i-blog.csdnimg.cn/direct/4ccb26aa401a48f0bcbf1ccb5f634a04.png
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;
}
如上代码,它就使用了菱形继承,它界说了一个基类,三个派生类,它起首通过Person这个基类来界说了两个派生类Student,Teacher,然后再将这两个派生类作为基类继承天生一个派生类Assistant。我们在实例化一个Assistant对象后,我们调用它的成员变量时就出现了二义性的标题,我们不可以或许确定谁人_name是从哪个基类继承来的,因此假如我们必要自己表现指定一下哪个基类,但是如许做只是办理了二义性的标题,数据冗余的标题照旧没可以或许被办理。

9.2 虚继承

C++中的多继承固然说在实际中大概应用挺广泛的,但是它应运而生的菱形继承所带来的标题有点多,这也是java中没有多继承的一个重要缘故起因,但是C++对于之前已经建立的语法是不可以举行修改的,我们只可以或许向前兼容,于是我们就又创建了一个新的语法——虚继承。虚继承就是在我们派生类继承基类的时间,在继承方式前加上一个关键字virtual即可。在C++中,虚继承(Virtual Inheritance)是一种用于办理多继承中重复继承标题标机制。其重要意义在于确保共同的基类只被继承一次,克制数据冗余和访问歧义。
#include <iostream>

class A {
public:
    A() { std::cout << "A的构造函数被调用\n"; }
    ~A() { std::cout << "A的析构函数被调用\n"; }
};

class B : virtual public A {
public:
    B() { std::cout << "B的构造函数被调用\n"; }
    ~B() { std::cout << "B的析构函数被调用\n"; }
};

class C : virtual public A {
public:
    C() { std::cout << "C的构造函数被调用\n"; }
    ~C() { std::cout << "C的析构函数被调用\n"; }
};

class D : public B, public C {
public:
    D() { std::cout << "D的构造函数被调用\n"; }
    ~D() { std::cout << "D的析构函数被调用\n"; }
};

int main() {
    D obj;
    return 0;
} 上述代码,我们使用了虚继承的方式来模仿展示,每个类构造/析构时的方式。我们从下面的运行结果可以看出来,它们只会调用它们的基类一次克制了基类被重复调用的标题,而且构造函数的调用次序是按照继承条理的深度优先次序举行调用的,析构则是先调用的后析构。
https://i-blog.csdnimg.cn/direct/ce04f48c63c94105a3453037a6458744.png
总而言之,我们可以计划多继承,但是不发起计划菱形继承,纵然我们有其办理方法但是如许总归会有运行斲丧的,因此我们在后续的学习中是不发起计划菱形继承的。

十、继承和组合

在C++中,继承与组合是两种告急的机制,用于构建类之间的关系和复用代码。明白它们的区别和应用场景,对于计划高效、可维护的代码至关告急。
继承(Inheritance)

继承是一种“is-a”关系,表现一个类(子类)是另一个类(父类)的特例。子类继承父类的属性和方法,并可以添加新的功能或重写父类的方法。
特点:


[*]代码复用:子类可以复用父类的代码,淘汰重复编码。
[*]条理布局:通过继承,可以构建类的条理布局,反映实际天下中的分类关系。
[*]多态性:继承支持多态性,子类可以重写父类的方法,以实现差异的举动。
示例:
class Animal {
public:
    virtual void sound() = 0;
};

class Dog : public Animal {
public:
    void sound() override {
      std::cout << "汪汪叫" << std::endl;
    }
};

class Cat : public Animal {
public:
    void sound() override {
      std::cout << "喵喵叫" << std::endl;
    }
}; 在这个示例中,Dog和Cat类都继承自Animal类,并重写了sound()方法,展示了继承和多态性的应用。
组合(Composition)

组合是一种“has-a”关系,表现一个类(容器类)包罗另一个类(被包罗类)的对象。通过组合,容器类可以复用被包罗类的功能,而不必继承其全部属性和方法。
特点:


[*]模块化计划:组合允许将功能分解为独立的类,进步代码的模块化和可维护性。
[*]机动性:容器类可以根据必要动态地添加或移除被包罗类的对象,而不会影响团体布局。
[*]克制多重继承标题:组合可以克制多重继承带来的二义性和复杂性。
示例:
class Engine {
public:
    void start() {
      std::cout << "引擎启动" << std::endl;
    }
};

class Car {
private:
    Engine engine;
public:
    void startEngine() {
      engine.start();
    }
}; 在这个示例中,Car类通过组合Engine类,复用了引擎的功能,而无需继承Engine类。
继承与组合的比力
特性继承组合关系“是”(is-a)“拥有”(has-a)代码复用通过继承父类的属性和方法通过包罗其他类的对象条理布局构建类的条理布局提供模块化计划多态性支持多态性不直接支持多态性机动性修改父类大概影响全部子类动态添加或移除功能复杂性大概导致类之间的耦合性增长淘汰多重继承带来的复杂性 选择继承照旧组合
在实际编程中,选择继承照旧组合,必要根据详细的需求和计划原则来决定:

[*] “是”关系:假如类之间存在“是”的关系,即子类是父类的特例,那么继承是符合的选择。
[*] “拥有”关系:假如类之间是“拥有”的关系,大概必要模块化计划和功能复用,那么组合更符合。
[*] 克制多重继承:假如必要克制多重继承带来的复杂性,组合是一个更好的选择。
[*] 代码复用与扩展:假如必要通过继承实今世码复用和扩展,同时保持条理布局的清楚,继承是符合的选择。
[*] 动态功能添加:假如必要在运行时动态地添加或移除功能,组合提供了更大的机动性。
总结

继承与组合是C++中两种告急的机制,用于构建类之间的关系和复用代码。继承实用于“是”的关系,提供了代码复用和多态性的本领;组合实用于“拥有”的关系,提供了模块化计划和动态功能添加的机动性。在实际编程中,应根据详细的需求和计划原则,选择符合的机制,或联合两者以实现最优的计划。



免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: C++之继承