类继承和类派生

继承(Inheritance): 子类接收父类的产业.

派生(Derive) : 父类把产业传给子类.

继承语法:

class 派生类名: [继承方式] 基类名{

派生类新增成员;

};

#include <iostream>
#include <string>
using namespace std;
class People{
public:
void setname(string name);
void setage(int age);
string getname();
int getage();
private:
string name;
int age;
}; void People::setname(string name){ this->name = name; }
void People::setage(int age) { this->age = age ; }
string People::getname(){ return this->name; }
int People::getage() { return this->age ; } class Student: public People{ //公有继承
public:
void setscore(float score);
float getscore();
void show();
private:
float score;
};
void Student::setscore(float score){ this->score = score; }
float Student::getscore(){ return this->score; }
void Student::show(){
cout << "name=" << this->getname() << ", age=" << this->getage() << ", score=" << this->getscore() << endl;
} int main(){
Student *p_stu0 = new Student; //通过new关键字返回的是指针
p_stu0->setname("Xiaoming"); //指针通过->调用成员
p_stu0->setage(17);
p_stu0->setscore(95.2);
p_stu0->show(); Student stu1; //没有new, 返回的是对象.
stu1.setname("XiaoHong"); //对象通过"."调用成员
stu1.setage(18);
stu1.setscore(59.2);
stu1.show();
}

继承方式有三种: public, protect, private.

不同继承方式, 基类成员在派生类中的属性

继承方式 public成员 protected成员 private成员
public 继承 public protected 不可见
protect继承 protected protected 不可见
private继承 private private 不可见

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

不管继承方式如何,基类中的private成员在派生类中始终不能使用

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

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

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

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

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

继承时名字遮蔽

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

比如父类People和派生类Student中都定义了函数show(), 则

stu.show() 调用的是派生类的成员.

stu.People::show()调用的是父类的成员.

注意: 父类与派生类名称一样的函数不构成重载, 只要名字相同就会遮蔽, 不管参数是否相同.

基类和派生类的构造函数

构造函数不能继承, 所有一般需要在派生类的构造函数中调用基类的构造函数.

因为基类构造函数不会被继承,不能当做普通的成员函数来调用, 只能在初始化列表中使用.

#include <iostream>
using namespace std;
class People{
public:
People(string name, int age);
void show();
protected:
string m_name;
int m_age;
};
People::People(string name, int age): m_name(name), m_age(age) {}
void People::show(){
cout << "name=" << m_name << ", age=" << m_age << endl;
} class Student: public People{
public:
Student(string name, int age, float score);
void show();
private:
float m_score;
}; //定义派生类的构造函数
//Student::Student(string name, int age, float score): People(name, age), m_score(score){} //此句没问题
Student::Student(string name, int age, float score): People(name, age){ //在初始化列表中调用基类构造函数
//People(name, age); //此句是错的, 因为基类构造函数不会被继承,不能当做普通的成员函数来调用, 只能在初始化列表中使用
m_score = score;
}
void Student::show(){
cout << "name=" << m_name << ", age=" << m_age << ", score=" << m_score << endl;
} int main(){
Student stu0("XiaoMing", 13, 19.8);
stu0.show();
}

构造函数调用顺序

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

还有一点要注意,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。

事实上,通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。换句话说,定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。

基类和派生类的析构函数

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

多重继承

一个派生类可以有多个基类.

多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java、C#、PHP 等干脆取消了多继承。

多继承语法:

class 派生类名称: [继承类型] 基类A, [继承类型] 基类B[, ...]{

派生类成员;

};

虚继承和虚基类

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。

将派生类赋值给基类(向上转型)

数据类型转换的前提是,编译器知道如何对数据进行取舍.

类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括

1)将派生类对象赋值给基类对象、

2)将派生类指针赋值给基类指针、

3)将派生类引用赋值给基类引用,

这在 C++ 中称为向上转型(Upcasting)。

相应地,将基类赋值给派生类称为向下转型(Downcasting)。

赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。

运行结果也有力地证明了这一点,虽然有a=b;这样的赋值过程,但是 a.display() 始终调用的都是 A 类的 display() 函数。

换句话说,对象之间的赋值不会影响成员函数,也不会影响 this 指针。

将派生类对象赋值给基类对象时,会舍弃派生类新增的成员,也就是“大材小用”

将派生类指针赋值给基类指针。

与对象变量之间的赋值不同的是,对象指针之间的赋值并没有拷贝对象的成员,也没有修改对象本身的数据,仅仅是改变了指针的指向。

将派生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数.

编译器虽然通过指针的指向来访问成员变量,但是却不通过指针的指向来访问成员函数:编译器通过指针的类型来访问成员函数。

对于 pa,它的类型是 A,不管它指向哪个对象,使用的都是 A 类的成员函数

总结:

  1. 编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;
  2. 编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数。

将派生类引用赋值给基类引用

多态

“多态(polymorphism)”指的是同一名字的事物可以完成不同的功能。

多态可以分为编译时的多态和运行时的多态。

编译时的多态: 主要是指函数的重载(包括运算符的重载)、对重载函数的调用,在编译时就能根据实参确定应该调用哪个函数,因此叫编译时的多态;

运行时的多态: 和继承、虚函数等概念有关,是本章要讲述的内容。本教程后面提及的多态都是指运行时的多态。

有了虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。

多态是面向对象编程的主要特征之一,C++中虚函数的唯一用处就是构成多态。

C++提供多态的目的是:可以通过基类指针对所有派生类(包括直接派生和间接派生)的成员变量和成员函数进行“全方位”的访问,尤其是成员函数。如果没有多态,我们只能访问成员变量。

多态的用途

//虚函数的注意事项:

  1. 只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可以不加。
  2. 为了方便,你可以只将基类中的函数声明为虚函数,这样所有派生类中具有遮蔽关系的同名函数都将自动成为虚函数。
  3. 当在基类中定义了虚函数时,如果派生类没有定义新的函数来遮蔽此函数,那么将使用基类的虚函数。
  4. 只有派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)
  5. 构造函数不能是虚函数. 对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承.也就是说,派生类不继承基类的构造函数,将构造函数声明为虚函数没有什么意义.
  6. 析构函数可以声明为虚函数,而且有时候必须要声明为虚函数,这点我们将在下节中讲解。

//构成多态的条件

  1. 必须存在继承关系;
  2. 继承关系中必须有同名的函数, 并且它们是覆盖关系;
  3. 存在基类的指针, 通过该指针调用虚函数;
#include <iostream>
class Base{
public:
virtual void func();
virtual void func(int n);
};
void Base::func(){ std::cout << "In Base::func()" << std::endl; }
void Base::func(int n){ std::cout << "In Base::func(int n)" << std::endl; } class Derived: public Base{
public:
void func();
void func(std::string str);
};
void Derived::func(){ std::cout << "In Derived::func()" << std::endl; }
void Derived::func(std::string str){ std::cout << "In Derived::func(std::string str)" << std::endl; } int main(){
Base *p = new Derived; //指针p是基类类型, 但指向派生类. p->func(); //调用派生类的虚函数, 构成了多态;
p->func(6);//调用基类的虚函数, 因为派生类中没有覆盖它;
//p->func("name"); //编译错误, 因为基类的指针只能访问从基类继承过去的成员, 不能访问新增的成员.
}

纯虚函数 和 抽象类

纯虚函数

声明方法: virtual 返回值类型 函数名(函数参数) = 0;

特点: 只有函数声明, 没有函数体, 函数声明时在结尾加了=0;

抽象类

概念: 包含纯虚函数的类称为抽象类.

特点: 抽象类无法实例化(无法创建对象), 因为纯虚函数没有函数体, 无法调用, 无法分配内存空间;

作用: 抽象类通常是做为基类, 让派生类去实现纯虚函数. 派生类必须实现纯虚函数才能被实例化;

#include <iostream>
class Line{
public:
Line(float len);
virtual float area() = 0;
virtual float volume() = 0;
protected:
float m_len;
};
Line::Line(float len): m_len(len){} class Rec: public Line{
public:
Rec(float len, float width);
float area();
protected:
float m_width;
};
Rec::Rec(float len, float width): Line(len), m_width(width){}
float Rec::area(){
return m_len * m_width;
} class Cuboid: public Rec{
public:
Cuboid(float len, float width, float height);
float area();
float volume();
protected:
float m_height;
};
Cuboid::Cuboid(float len, float width, float height): Rec(len, width), m_height(height){}
float Cuboid::area(){
return 2*(m_len*m_width + m_len*m_height + m_width*m_height);
}
float Cuboid::volume(){
return m_len*m_width*m_height;
} class Cube: public Cuboid{
public:
Cube(float len);
float area();
float volume();
};
Cube::Cube(float len): Cuboid(len, len, len){}
float Cube::area(){
return 6*m_len*m_len;
}
float Cube::volume(){
return m_len*m_len*m_len;
} int main(){
Line *p_cuboid = new Cuboid(10, 20, 30);
Line *p_cube = new Cube(15); std::cout << "Cuboid.area() =" << p_cuboid->area() << std::endl;
std::cout << "Cuboid.volume()=" << p_cuboid->volume() << std::endl; std::cout << "Cube.area() =" << p_cube->area() << std::endl;
std::cout << "Cube.volume()=" << p_cube->volume() << std::endl;
}

在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。

抽象基类除了约束派生类的功能,还可以实现多态。请注意第 51 行代码,指针 p 的类型是 Line,但是它却可以访问派生类中的 area() 和 volume() 函数,正是由于在 Line 类中将这两个函数定义为纯虚函数;如果不这样做,51 行后面的代码都是错误的。我想,这或许才是C++提供纯虚函数的主要目的。

注意:

  1. 一个纯虚函数就可以使类成为抽象基类,但是抽象基类中除了包含纯虚函数外,还可以包含其它的成员函数(虚函数或普通函数)和成员变量。
  2. 只有类中的虚函数才能被声明为纯虚函数,普通成员函数和顶层函数均不能声明为纯虚函数。

c++ class派生与多态的更多相关文章

  1. Day08:继承与派生,多态,封装,绑定与非绑定方法,面向对象高级(反射,__str__,__del__)

    上节课复习:1.编程思想    面向过程        核心是过程二字,过程指的是解决问题的步骤,即先干什么再干什么后干什么        基于该思想编写程序就好比在设计一条流水线,是一种机械式的思维 ...

  2. Day29 派生, 封装 , 多态, 反射

    Day29 派生, 封装 , 多态, 反射 内容概要 派生方法的实践 面向对象之封装 面向对象之多态 面向对象之反射 反射的实践案例 内容详细 1.派生方法的实践 #需求展示 import json ...

  3. python之继承、抽象类、派生、多态、组合、封装

    1.继承概念的实现方式主要有2类:实现继承.接口继承. Ø         实现继承是指使用基类的属性和方法而无需额外编码的能力: Ø         接口继承是指仅使用属性和方法的名称.子类必须提供 ...

  4. day30 继承、派生与多态,类中方法和内置函数

    目录 一.多继承出现的问题(mixins机制) 二.派生与方法重用 三.多态 1 什么是多态 2 为什么要有多态 3 python中多态的鸭子类型 四.绑定方法与非绑定方法 1 绑定方法 1.1对象的 ...

  5. python3 之 面向对象(类)、继承、派生和多态

    类提供了一种 组合数据和功能 的方法.创建一个新类意味着:创建一个新 类型  的对象,从而允许创建一个该类型的新 实例. 每个类的实例可以拥有: 保存自己状态的属性. 一个类的实例也可以有改变自己状态 ...

  6. 简单分析JavaScript中的面向对象

    初学JavaScript的时候有人会认为JavaScript不是一门面向对象的语言,因为JS是没有类的概念的,但是这并不代表JavaScript没有对象的存在,而且JavaScript也提供了其它的方 ...

  7. 【干货分享】前端面试知识点锦集03(JavaScript篇)——附答案

    三.JavaScript部分 1.谈谈你对Ajax的理解?(概念.特点.作用) AJAX全称为“Asynchronous JavaScript And XML”(异步JavaScript和XML) 是 ...

  8. 正确理解javascript当中的面向对象

    认识面向对象: 为了说明 JavaScript 是一门彻底的面向对象的语言,首先有必要从面向对象的概念着手 , 探讨一下面向对象中的几个概念: 1.万物皆为空:万物皆对象 2.对象具有封装和继承特性 ...

  9. javase基础笔记3——this关键字和内存图

    什么是面向对象? 面向过程. 面向过程:解决一个问题的思路和方法以及步骤 面向对象:把一些具有相同特征的问题抽象成一个对象,用""""对象.方法()" ...

  10. 全面理解面向对象的 JavaScript (share)

     以下分享自:  http://www.ibm.com/developerworks/cn/web/1304_zengyz_jsoo/   简介: JavaScript 函数式脚本语言特性以及其看似随 ...

随机推荐

  1. [python] 基于Gradio可视化部署机器学习应用

    Gradio是一个开源的Python库,用于构建机器学习和数据科学演示应用.有了Gradio,你可以围绕你的机器学习模型或数据科学工作流程快速创建一个简单漂亮的用户界面.Gradio适用于以下情况: ...

  2. [数据与分析可视化] D3入门教程1-d3基础知识

    d3.js入门教程1-d3基础知识 文章目录 d3.js入门教程1-d3基础知识 1 HTML介绍 1.1 什么是HTML? 1.2 自定义文档样式CSS 1.3 构建svg图形 2 d3绘图入门 2 ...

  3. [OpenCV实战]50 用OpenCV制作低成本立体相机

    本文主要讲述利用OpenCV制作低成本立体相机以及如何使用OpenCV创建3D视频,准确来说是模仿双目立体相机,我们通常说立体相机一般是指双目立体相机,就是带两个摄像头的那种(目就是指眼睛,双目就是两 ...

  4. 一文教会你mock(Mockito和PowerMock双剑合璧)

    作者:京东物流 杨建民 1.什么是Mock Mock有模仿.伪造的含义.Mock测试就是在测试过程中,对于某些不容易构造或者不容易获取的对象,用一个虚拟的对象来创建以便测试的测试方法.mock工具使用 ...

  5. (一)elasticsearch 编译和启动

    1.准备 先从github官网上clone elasticsearch源码到本地,选择合适的分支.笔者这里选用的是7.4.0(与笔者工作环境使用的分支一致),此版本编译需要jdk11. 2.编译 Re ...

  6. P1848 [USACO12OPEN]Bookshelf G

    简要题意 给你 \(N\) 本书 \((h_i,w_i)\),你要将书分成任意段(顺序不能改变),使得每一段 \(j\) 中 \(\sum\limits_{i \in j} w_i \leq L\), ...

  7. Linux 查找某一线程是否已运行,并启动的方法

    参考资料:(3条消息) [Linux]守护线程自动重启某个程序的3种常用办法_L7256的博客-CSDN博客_守护进程 自动重启 方法一:使用编写一个监控APP的脚本 start.sh脚本如下:exp ...

  8. 【转】C#接口知识

    参考:日常收集 C# 接口知识 (知识全面) 目录 第一节 接口慨述 第二节 定义接口 第三节 定义接口成员 第四节.访问接口 第五节.实现接口 第六节.接口转换 第七节.覆盖虚接口 第一节 接口慨述 ...

  9. C++获取含有中文字符的string长度

    :前言 造车轮的时候要用到中文字符串的长度辨别,发现char的识别不准,进行了一番研究. > 开始研究 在Windows下,中文字符在C++中的内存占用为2字节,此时采用字符串长度获取函数得到的 ...

  10. git操作失误,提交代码因为网络问题没有成功,然后操作时候点错按钮导致代码全部没有了,也没用备份,如何解决

    最好的提交代码办法, 1.先创建一个空文件夹, 2.然后创建一个在线仓库 3. git remote add origin '仓库地址' 4.查看远程仓库 git remote remove orig ...