C++面向对象的三大特性----继承

目录

一:继承的概念

二:C++三种继承方式

三:继承常见的问题

1,名字遮蔽问题

2,类的构造函数不能被继承 

3,类的析构函数不能被继承 


           龙生龙,凤生凤,老鼠的儿子会打洞。老鼠的儿子生来就会打洞是因为他老爹把打洞的基因传给他了,换句话说老鼠的娃继承了他爹打洞的本领,他不用从头摸索怎么去打洞而是捡现成的,天生的。继承的作用有多大?

         继承了皇位还要啥C++,本篇文章到此就可以返回了,不用往后看了。

        咱们言归正传,本篇文章将会详细讲解C++面向对象三大特性之一的继承篇,在大型的项目中,继承随处可见,灵活运用继承可以让代码变得简洁,结构更清晰。     

一:继承的概念

         C++中的继承是类与类之间的关系,是一个很简单很直观的概念,与现实世界中的继承类似,例如上述提到的老鼠的儿子继承了父辈打洞的本领。   

        继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类 B 继承于类 A,那么 B 就拥有 A 的成员变量和成员函数。

        在C++中,派生(Derive和继承是一个概念,只是站的角度不同。继承是儿子接收父亲的产业,派生是父亲把产业传承给儿子。

        被继承的类称为父类或基类,继承的类称为子类或派生类。“子类”和“父类”通常放在一起称呼,“基类”和“派生类”通常放在一起称呼。
        派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能。

        以下是两种典型的使用继承的场景:
        1) 当你创建的新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承,这样不但会减少代码量,而且新类会拥有基类的所有功能。

        2) 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,也可以使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员。

        来看下面一个具体的例子,我们定义一个叫Pepple的类,该类有两个私有变量,一个是人的名字,一个是人的年龄,我们提供四个函数,功能分别是修改人的名字,年龄,获取人的名字,年龄。另外我们再定义一个Student类,该类继承People类,接下来我们来看看,Student类继承People类后我们可以做的事情

#include<iostream>
using namespace std;
//基类 Pelple
class People{
public:
    void setname(char *name);
    void setage(int age);
    char *getname();
    int getage();
private:
    char *m_name;
    int m_age;
};
//定义People类的四个函数
void People::setname(char *name){ m_name = name; }
void People::setage(int age){ m_age = age; }
char* People::getname(){ return m_name; }
int People::getage(){ return m_age;}
//派生类 Student
class Student: public People{
public:
    void setscore(float score);
    float getscore();
private:
    float m_score;
};
void Student::setscore(float score){ m_score = score; }
float Student::getscore(){ return m_score; }
int main(){
    Student stu;
    stu.setname("小明");
    stu.setage(16);
    stu.setscore(95.5f);
    cout<<stu.getname()<<"的年龄是 "<<stu.getage()<<",成绩是 "<<stu.getscore()<<endl;
    return 0;
}

运行结果:
小明的年龄是 16,成绩是 95.5

        本例中,People 是基类,Student 是派生类。Student 类继承了 People 类的成员,同时还新增了自己的成员变量 score 和成员函数 setscore()、getscore()。这些继承过来的成员,可以通过子类对象访问,就像自己的一样。 如上述的程序,实例化Student类stu,stu可以直接调用父类的setname和setage函数给父类中的私有变量m_name和m_age赋值。

        下面我们来看看怎么申明派生类

class Student: public People

        这就是声明派生类的语法。class 后面的“Student”是新声明的派生类,冒号后面的“People”是已经存在的基类。在“People”之前有一关键宇 public,用来表示是公有继承。

        由此总结出继承的一般语法为:

class 派生类名:[继承方式] 基类名{
    派生类新增加的成员
};

         继承方式包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。我们将在第二部分详细讲解这些不同的继承方式。

 二:C++三种继承方式

         我们都知道类成员的访问权限由高到低依次为 public --> protected --> private,public 成员可以通过对象来访问,private 成员不能通过对象访问。protected 成员和 private 成员类似,也不能通过对象访问。但是当存在继承关系时,protected 和 private 就不一样了:基类中的 protected 成员可以在派生类中使用,而基类中的 private 成员不能在派生类中使用,下面是详细讲解。

        不同的继承方式会影响基类成员在派生类中的访问权限。

1) public继承方式

  • 基类中所有 public 成员在派生类中为 public 属性;
  • 基类中所有 protected 成员在派生类中为 protected 属性;
  • 基类中所有 private 成员在派生类中不能使用。

2) protected继承方式
  • 基类中的所有 public 成员在派生类中为 protected 属性;
  • 基类中的所有 protected 成员在派生类中为 protected 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

3) private继承方式
  • 基类中的所有 public 成员在派生类中均为 private 属性;
  • 基类中的所有 protected 成员在派生类中均为 private 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

通过上面的分析可以发现:
1) 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为 protected 时,那么基类成员在派生类中的访问权限最高也为 protected,高于 protected 的会降级为 protected,但低于 protected 不会升级。再如,当继承方式为 public 时,那么基类成员在派生类中的访问权限将保持不变。

也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。

2) 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用,就算你继承了皇位,但是前朝妃子属于先皇私有的,你不能继承更不能使用(不能在派生类的成员函数中访问或调用)。

3) 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。

4) 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。

        如果我想在派生类中访问基类 private 成员呢,也不是没有办法,唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问

        由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。

        使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。

        注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

        请看下面用using改变基类成员在子类访问权限的具体例子

#include<iostream>
using namespace std;
//基类People
class People {
public:
    void show();
protected:
    char *m_name;
    int m_age;
};
void People::show() {
    cout << m_name << "的年龄是" << m_age << endl;
}
//派生类Student
class Student : public People {
public:
    void learning();
public:
    using People::m_name;  //将protected改为public
    using People::m_age;  //将protected改为public
    float m_score;
private:
    using People::show;  //将public改为private
};
void Student::learning() {
    cout << "我是" << m_name << ",今年" << m_age << "岁,这次考了" << m_score << "分!" << endl;
}
int main() {
    Student stu;
    stu.m_name = "小明";
    stu.m_age = 16;
    stu.m_score = 99.5f;
    stu.show();  //compile error
    stu.learning();
    return 0;
}

        代码中首先定义了基类 People,它包含两个 protected 属性的成员变量和一个 public 属性的成员函数。定义 Student 类时采用 public 继承方式,People 类中的成员在 Student 类中的访问权限默认是不变的。

        不过,我们使用 using 改变了它们的默认访问权限,如代码第 21~25 行所示,将 show() 函数修改为 private 属性的,是降低访问权限,将 name、age 变量修改为 public 属性的,是提高访问权限。

        因为 show() 函数是 private 属性的,所以代码第 36 行会报错。把该行注释掉,程序输出结果为:
我是小明,今年16岁,这次考了99.5分!

三:继承常见的问题

1,名字遮蔽问题

        假如存在这样的一个场景,父类中有一个属性为public的方法名字为getname,子类中同样有一个getname方法,此时子类对象调用getname方法是调用从父类继承的getname方法还是子类自个的呢。

        如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。

        其实也好理解,相当于我们要和别人干仗了,手里有跟棒子,父亲手里也有根棒子,我们当然用自己的。

        基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。

2,类的构造函数不能被继承 

        前面我们说基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。

        在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。

        这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数

下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:

#include<iostream>
using namespace std;
//基类People
class People{
protected:
    char *m_name;
    int m_age;
public:
    People(char*, int);
};
People::People(char *name, int age): m_name(name), m_age(age){}
//派生类Student
class Student: public People{
private:
    float m_score;
public:
    Student(char *name, int age, float score);
    void display();
};
//People(name, age)就是调用基类的构造函数
Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
void Student::display(){
    cout<<m_name<<"的年龄是"<<m_age<<",成绩是"<<m_score<<"。"<<endl;
}
int main(){
    Student stu("小明", 16, 90.5);
    stu.display();
    return 0;
}

运行结果为:
小明的年龄是16,成绩是90.5。

构造函数的调用顺序:基类构造函数总是被优先调用,这说明创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数,如果继承关系有好几层的话,例如:

A --> B --> C

那么创建 C 类对象时构造函数的执行顺序为:

A类构造函数 --> B类构造函数 --> C类构造函数

 构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。

        还有一点要注意,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。以上面的 A、B、C 类为例,C 是最终的派生类,B 就是 C 的直接基类,A 就是 C 的间接基类。
         C++ 这样规定是有道理的,因为我们在 C 中调用了 B 的构造函数,B 又调用了 A 的构造函数,相当于 C 间接地(或者说隐式地)调用了 A 的构造函数,如果再在 C 中显式地调用 A 的构造函数,那么 A 的构造函数就被调用了两次,相应地,初始化工作也做了两次,这不仅是多余的,还会浪费CPU时间以及内存,毫无益处,所以 C++ 禁止在 C 中显式地调用 A 的构造函数。 

 3,类的析构函数不能被继承 

 和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。

另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:

  • 创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
  • 而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ftzchina

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值