C++萌新来看,一篇文让你让你彻底搞定类(超详细)!

本文涉及的产品
访问控制,不限时长
简介: C++萌新来看,一篇文让你让你彻底搞定类(超详细)!

一、什么是类?


1.类的概念

有关类的官方定义可以通过百度百科查看,但是我觉得官方的话总是比较抽象的,就像类本身一样。我用通俗的语言说一下自己对类的理解,类就是把数据和函数进行抽象,然后进行封装起来,对外部只提供一个接口去使用。外部的调用者看不到里面发生了什么。


举个例子:我们可以把狗抽象成类,狗有头,有爪子,有耳朵,这些相当于类的数据成员,然后狗会跑,跳,吠,吃东西,这些行为可以定义为类中的函数,这样我们就完成了类的抽象。


我们都知道狗是一个类,那每条狗都是一个实例,狗都有自己的名字,比如“二愣”,“黑豹”,“八戒”,“花猫”等名字,这些都是狗这种类产生的对象,他们都具有那些跑跳等行为,也都有爪子和耳朵。如果家里面狗狗听话,你让它叫他就会叫,但是至于它具体是怎么发出声音的,就不是主人考虑的问题了,主人只是知道它具有叫的功能,并且会使用它就行了。如果我们想改变狗的叫声,想让他和绵羊一样发出咩咩的声音,它是做不到的,也就是说我们调用者没有办法改变已经封装好的接口,我们只能使用,这就是类的封装性。


那狗又可以划分成各种各样的品种,比如罗威纳,阿拉斯加,藏獒,牧羊犬等等,就拿牧羊犬来说吧,它具有狗的那些所有基本特征的同时,又有自己的特殊能力,那就是这种狗可以牧羊,这就是类的继承性,在原有类的基础上,子类可以派生出自己特殊的能力,相当于更细化它的能力了,且不影响其他的子类。


看了半天看的烦了,接下来给大家讲一个故事:小丁有一个狗场,狗场中有许多的狗,那必然是有一个狗老大,替主人管理狗场的治安,而“黑豹”就是狗场的老大,它是一条霸气的罗威纳,每当哪个狗不听话,小丁就让“黑豹”去收拾它,但是狗场不是打打杀杀,狗场那是狗情世故,“黑豹”遇到关系好的狗,比如“八戒”,它就意思意思假装吠两声不会实质性攻击,但是遇到那种看着不爽的,比如“花猫”,那就直接干倒在地。“黑豹”对不同的狗狗,有不同的收拾方式,这就是类的多态性。


类的概念就说到这里,上面说到的抽象性、封装性、继承性、多态性是类的四大特性,这四种特性互相依赖,会在后面的内容中都涉及到。


2.在c++中声明一个类

2.1类的简单声明与解释

class是C++中的关键词,关键词可以理解为C++本身定义的变量,那用这个变量就可以定义类了:

class Class_Dog
{
public:
  Class_Dog();//构造函数
  ~Class_Dog();//析构函数
};
Class_Dog::Class_Dog()
{
}
Class_Dog::~Class_Dog()
{
}

好好看一下这个类中包括了什么:

class关键字后面跟的Class_Dog就是类的名字,以后就用它来定义对象了。

接下来是publice关键词:public意思事公开的,那它在这里的意思自然就是下面包含的两个函数是公有的。有关public,接下来会细讲,先这样了解一下。


Class_Dog()是当前类的默认构造函数,构造函数可以干什么?可以在类生成一个对象时候,为这个对象分配内存空间。毕竟对象也是一个变量,需要存放在内存空间中,而构造函数就是干这个的。


~Class_Dog()是当前类的默认析构函数,析构函数可以在对象销毁的时候,释放内存空间,保证程序的安全性,防止造成内存泄露。

特别注意:类的名字和构造函数、析构函数的名字必须一致!!!

Class_Dog::Class_Dog()
{
}
Class_Dog::~Class_Dog()
{
}

这两段代码相信大家都能看懂,它什么也没干,就是写了个函数的定义,毕竟函数只是声明是不可以调用的,必须要有定义,但是这个函数定义好像和平时我们见到的函数定义并不相同?


拆解来看:首先在双冒号‘::’前面的Class_Dog是返回值类型,这个和我们平时见到的int、void都是一个概念,可以把它理解成数据类型,然后是双冒号“::”,这个东西叫做作用域,也就是说告诉编译器,后面这个函数,必须是我这个Class_Dog类生成的对象才能用,或者我内部自己用,别人不可以用!!!然后剩下的就是常规的函数定义了。


接下来,我们开始扩充我们的狗类!让我们的狗看起来威武一点,现在有点太虚了!


2.2 类的变量声明和初始化

这个代码可以直接运行测试,大家可以复制到自己的编译器里面进行测试。

#include<iostream>
using namespace std;
class Class_Dog
{
public:
  Class_Dog();
  ~Class_Dog();
  Class_Dog(string name,int age);//有参构造函数
private:
  int legs;//狗腿
  int head;//狗头
  int mouth;//狗嘴
public:
  string name;//狗狗名字
  int age;//狗狗年龄
};
Class_Dog::Class_Dog()
{
  //对于狗狗的外帽基本特征,在默认构造函数中进行赋值处理
  legs = 4;
  head = 1;
  mouth = 1;
  //对于这些不确定变量,可以在这里进行初始化为NULL
  name = "";
  age = 0;
}
Class_Dog::~Class_Dog()
{
}
Class_Dog::Class_Dog(string name, int age)
{
  //对于狗狗的外帽基本特征,在有参函数中也可以进行赋值处理
  legs = 4;
  head = 1;
  mouth = 1;
  this->name = name;
  this->age = age;
}
int main()
{
  Class_Dog Dog_1("Mikey", 5);
  Class_Dog Dog_2;
  return 0;
}

相较于之前的Class,我们现在添加了什么呢?

一个函数,一个public关键词,一个private关键词,还有一些变量。

首先我来解释一下为什么又添加一个pubilc关键字,之前已经有了一个public关键字了,我们确实可以将下面代码中的两个变量直接写到最初的public下,我这么写是为了将函数和变量分开放,不至于混乱,纯属个人习惯。

public:
  string name;//狗狗名字
  int age;//狗狗年龄

我们定义了两个变量,一个是字符串类型,存放狗狗的名字,一个是整型,用来存放狗狗的年龄。public可以保证我们的这些函数和变量在类外可以被访问。

private:
  int legs;//狗腿
  int head;//狗头
  int mouth;//狗嘴

private关键词,这是类的访问控制的又一关键词,只有类内成员函数才可以访问这些私有变量。在此处定义了一些基本的狗的特征,这些特征是不允许被外部成员访问的。


接下来定义了一个函数,这个函数我们把它叫做有参构造函数,和之前的无参构造函数相比,多了两个参数,是从外部传进来的:

Class_Dog(string name,int age);//有参构造函数
Class_Dog::Class_Dog(string name, int age)
{
  //对于狗狗的外帽基本特征,在有参函数中也可以进行赋值处理
  legs = 4;
  head = 1;
  mouth = 1;
  this->name = name;
  this->age = age;
}

从函数的定义来看,在有参构造函数中,我们对变量进行了初始化操作,注意:由于内部变量名和参数名是一致的,所以使用this指针进行区分,this指针是类特有的指针,可以用来指向类内的非静态成员和函数。

Class_Dog::Class_Dog()
{
  //对于狗狗的外帽基本特征,在默认构造函数中进行赋值处理
  legs = 4;
  head = 1;
  mouth = 1;
  //对于这些不确定变量,可以在这里进行初始化为NULL
  name = "";
  age = 0;
}

这是无参构造函数,也是简单的对变量进行了初始化操作。

int main()
{
  Class_Dog Dog_1("Mikey", 5);
  Class_Dog Dog_2;
  return 0;
}

我们来看一下最后的main函数中声明的两个变量Dog_1和Dog_2,首先说一下类的对象声明,一般来说和普通的类型声明是一样的,是 类名 对象名; 的形式,就像Dog_2一样,还有一种声明的形式就是Dog_1这种,类名加上对象名和参数。其中Dog_1应该是调用有参构造,Dog_2默认调用无参构造。其实这里涉及到了多态性的特性,因为Class_Dog(string name, int age)和Class_Dog()具有同样的函数名字,却可以根据是否有参数,编译器来决定去调用哪一个函数。


我们来看一下调试结果:

image.png

两个变量最大的不同在于共有成员的值不同,这也符合我们的预期。


2.3 类内成员函数的实现

现在这个狗稍微有点东西了,但是他还不会执行我们的命令,对于主人来说,肯定希望自己的狗狗有一些技能,我们现在来写一些供主人外部调用的接口,让狗狗完成进化。

#include<iostream>
using namespace std;
class Class_Dog
{
public:
  Class_Dog();
  ~Class_Dog();
  Class_Dog(string name,int age);//有参构造函数
  void Dog_run();//跑
  void Dog_bark();//吠
  void Dog_eat();//吃
private:
  int legs;//狗腿
  int head;//狗头
  int mouth;//狗嘴
public:
  string name;
  int age;
};
.........//此处省略的代码在上一个代码段中可以找到,时有关构造函数等函数的定义
//为了篇幅稍微短点,在此处只列出新增代码
void Class_Dog::Dog_run()
{
  cout << "I am "<< name.c_str() <<"I am Running now" << endl;
}
void Class_Dog::Dog_bark()
{
  cout << "I am " << name.c_str() << "I am Barking now" << endl;
}
void Class_Dog::Dog_eat()
{
  cout << "I am " << name.c_str() << "I am Eating now" << endl;
}
int main()
{
  Class_Dog Dog_1("Mikey", 5);
  Dog_1.Dog_bark();
  Dog_1.Dog_run();
  return 0;
  }

好的,此时我们从原本的基础之上又添加了吠、跑、叫三个成员函数,并且他们都是公有的,可以保证外部接口的调用。说一下几个重点拿Dog_run()函数举例:

void Class_Dog::Dog_run()
{
  cout << "I am "<< name.c_str() <<"I am Running now" << endl;
}

这整个函数的实现其实很简单,里面输出了一句话,但是注意name这个变量,这是类的内部成员变量,在初始化时候被初始化为Mikey,而这是一个string类型的数据,想要输出它的字符串,就需要调用string类型的方法c_str()。且我们注意到调用方法使用的是点(’ . ‘),对于类的对象来说,调用类内的成员函数或者成员变量,都是用点(’ . ')的方式。看一下main函数的调用:

int main()
{
  Class_Dog Dog_1("Mikey", 5);
  Dog_1.Dog_bark();
  Dog_1.Dog_run();
  return 0;
}

使用有参构造函数声明的对象Dog_1,使用Dog_1.Dog_bark()调用了吠这个函数,其实这样的调用和结构体中的调用时特别像的。我们来看一下最终的输出结果:

image.png

总结:以上的简单代码已经让我们对类有了初步的了解,类具有抽象性和封装性,我们把狗这个动物的各种属性和动作抽象在一起,进行public或者private关键字封装之后,这个类生成的对象就可以有接口访问类中的共有成员。类的多态性,说的通俗一点就是多种形态的性能,可以分为静态多态和动态多态,其实静态多态在这里也有体现就是有参构造函数和无参构造函数。至于类的继承性,将在接下来进行探讨,虚函数也是动态多态实现的基本前提。


二、类的访问控制


1. 类内关键字

为了将类内关键字,我们重新写一个简单的类,能说明情况就行,避免类过于庞大,因为主要是讲一下概念。

class Demo
{
  int defalut = 7;//这是个特殊的变量
public:
  Demo();
  ~Demo();
  int a = 1;
  int set_number(int a,int b,int c);
private:
  int b = 2;
protected:
  int c = 3;
};
Demo::Demo()
{
}
Demo::~Demo()
{
}
int Demo::set_number(int a, int b, int c)
{
  this->a = a;
  this->b = b;
  this->c = c;
  return 0;
}

1.1 public关键字

上述代码中public关键字下面包含构造函数和析构函数,还有一个变量a和一个函数,用来修改a,b,c的值。现在我们在main函数中执行操作:

int main()
{
  Demo demo;
  demo.a = 5;//操作成功,允许访问,可以把a的值从1修改成5
  demo.set_number(10, 11, 12);//允许调用,成功修改成员变量的值
  return 0;
}

代码单步执行结果,从修改a之前,到修改a,到修改a,b,c三张图

image.png

image.png


image.png

总结:public关键字的访问域是公共的,也就是说,在public中定义的成员函数或者成员变量可以在外部被访问,也就是提供给外部的接口。


1.2 private关键字

对于private,首先要说一下类中默认的访问属性,类中默认的访问属性是private类型,所以我们在类的一开头定义的defalut变量,是一个private变量,它和变量b的属性是一样的,都是private的。


回到main中测试,试着做相同的事情:

int main()
{
  Demo demo;
  demo.b = 5;//我们尝试直接修改b的值,发现报错
  return 0;
}

报错截图显示,Demo::b这个变量不可访问,也就是说类的私有成员变量不供外部接口使用,不能直接修改。那么defalut变量属于私有变量,自然也不可以访问。那我们可以修改私有成员吗?答案是可以,通过类的成员函数

image.png

int main()
{
  Demo demo;
  demo.set_number(10, 11, 12);
  return 0;
}

调用这个函数,我们可以成功把b的值进行修改,因为函数是public,所以可以被调用,而函数又属于类内部,又可以访问b,所以可以修改!


总结:类的私有成员不允许外部访问,这样的设计是为了保证类的安全性,我们把敏感的东西封装在private关键字的控制之下,就不用担心外部接口对它造成不可控制的操作。就算需要操作,也只能通过我们提供的外部接口来操作,而这些接口函数是我们编写,所以可以保证安全性。


1.3 protected关键字

protected关键字特别的有趣,它的性质和private特别的相似,在当前类中,可以说和private完全一样,也不能被外部访问。

image.png

这时候就有人有疑问了,那么为什么还要有这个关键字呢?它的意义又是什么?它的意义就在于继承时候,当前类如果发生继承,在子类中的可见性是不同的。关于这一块的内容,在稍后的继承性时候会细细讲!!先有个概念和映像就行了。


2. 类的静态成员

有关类的静态成员,是面试中高频知识点之一。上一篇博文我在说指针时候提到了类中的this指针,说到this指针时候说,this可以控制类内非静态成员函数以外的成员。那我们现在就要讲类的静态成员和静态函数了。


2.1 类的静态成员变量

我们说先说一下静态成员变量的一些特性,static是声明静态成员变量的关键字,静态成员变量在所有类的对象中都共享,比如我们定义Demo_1、Demo_2两个类对象,这两个类对象中的index变量是存放在同一个内存的值。这和我们的普通变量有很大的区别,如果是普通变量,每个对象中都会有新的内存空间,各个对象之间互不干扰。 并且静态数据成员必须在类外定义和初始化,因为需要以这样的方式来进行静态变量的初始化。正是因为这样,静态数据成员的生存周期也不随类的销毁而销毁下面我们结合代码来看一下:

class Demo
{
public:
  Demo();
  ~Demo();
  void get_index();
private:
  static int Index;//只声明,不用管初始化的事情
};
Demo::Demo()
{
  Index++;//每次有对象生成,就让索引值++
}
Demo::~Demo()
{
}
void Demo::get_index()
{
  cout << Index << endl;
}
//在类内已经进行过声明定了了,但是必须在此处继续定义以分配内存空间
int Demo::Index = 0;

以上代码中有静态成员变量在类中的定义,在类外的定义和初始化,这些东西都是固定的,比如我们必须在类外定义Index的时候加上Demo类名的作用域的限制。

我们来看一下main函数中的调用:

int main()
{
  Demo demo1;
  demo1.get_index();//输出1
  Demo demo2;
  demo1.get_index();//输出2
  demo2.get_index();//输出2
  return 0;
}

我们知道每次声明Demo变量,都会执行到默认构造函数中,而默认构造函数中对静态变量Index进行++,所以最终在使用demo1调用get_index也会输出2。证明了共享一个静态变量。

image.png

必须要再次说明的一点,静态成员变量,不属于类的任何一个实例。就是说demo1和demo2的内存空间中并不存在Index变量,Index变量存在了另外一个地址空间中,和这两个对象所在的内存空间没关系。


2.2 类的静态成员函数

既然我们提到类的静态数据成员不在任何对象的内存空间中,那么我们肯定可以不通过对象直接访问它,具体要怎么做呢?让静态成员函数来解决这个问题吧!!静态成员函数可以直接访问类中的静态数据成员,而访问非静态的数据成员,需要通过对象名限定。来看下面的代码示例:

class Demo
{
public:
  Demo();
  ~Demo();
  static void get_index();//静态成员函数
private:
  static int Index;
  int a = 3;
};
Demo::Demo()
{
  Index++;
}
Demo::~Demo()
{
}
void Demo::get_index()
{
  cout << Index << endl;
  //cout << a << endl;会报错
}
int Demo::Index = 0;
int main()
{
  Demo::get_index();
  Demo demo1;
  demo1.get_index();//输出1
  return 0;
}

我们把get_index定义为一个静态成员函数,当我们在静态成员函数中尝试访问非静态数据成员时,编译器会报错:

image.png

并且静态成员函数中不包含this指针:

image.png

静态成员函数一般用来处理静态数据成员,所以我们在main函数中直接用类名作用域限制之后就可以调用函数,当然也可以通过对象调用

int main()
{
  Demo::get_index();//输出0
  Demo demo1;
  demo1.get_index();//输出1
  return 0;
}

image.png

总结:静态成员函数一般用来对静态数据成员来进行处理,this指针在静态成员函数中不存在


3. 指针访问和对象访问

对于一个对象的声明,可以有两种方法,一种是静态分配内存,一种是用new来为对象动态申请内存,返回指针。我们来简单的看一下:


类还是之前的Class_Dog类,我们使用new来进行内存申请,返回一个Class_Dog类型的指针,然后我们执行其中的成员函数,或者取出其中的成员变量使用‘->’来取。如果是正常的分配内存,那就像Dog_1直接用’ . '来控制就行。

int main()
{
  Class_Dog* Dog_Pointer = new Class_Dog("Mikey", 5);
  Class_Dog Dog_1("Haney", 3);
  Dog_Pointer->Dog_bark();
  Dog_1.Dog_eat();
  return 0;
}

说明:以上代码只是为了说明new出一个对象和普通内存分配之后,指针和普通对象的访问方式不同,至于指针的使用,还是有漏洞,使用new申请内存,需要判断是否申请成功。


三、类的特性详解


关于类的特性,我们已经提到过了,抽象和封装就不过多解释了,接下来我们细细说一下继承和多态。接下来讲的内容也是面试的重要知识点


1. 类的继承

1.1 三种继承方式

先说下继承是什么,继承就是字面意思,被继承的类叫做父类,继承的类叫做子类或者派生类,和皇位继承一样,朕该给你的就是你的,但朕私有的就不给你,并且还要看子类是如何继承父类的。类的继承总共有3种继承方法,也就是之前我们说的访问控制:public、private、protected。

1.1.1 public继承

公有继承public就是说:除了朕的私有,那其他的就都给你了,想要啥拿啥,那当然也包括了protected里面的东西,简单来说,只要不是父类private控制下的变量和函数,都可以被子类拥有。 并且继承过来的东西,属性不发生改变

来看个例子:

class Dog
{
public:
  Dog();
  ~Dog();
  void set_private_b(int b);
  int a = 0;
private:
  int b = 0;
protected:
  int c = 0;
};
Dog::Dog()
{
  cout << "Dog Create" << endl;
}
Dog::~Dog()
{
  cout << "Dog Destory" << endl;
}
void Dog::set_private_b(int b)
{
  this->b = b;
}
//子类  共有继承
class Rottweiler_Dog : public Dog
{
public:
  Rottweiler_Dog();
  ~Rottweiler_Dog();
  void show_number();
private:
  int x = 0;
};
Rottweiler_Dog::Rottweiler_Dog()
{
  a = 10;
  set_private_b(10);
  c = 10;
  cout << "Rottweiler_Dog Create" << endl;
}
Rottweiler_Dog::~Rottweiler_Dog()
{
  cout << "Rottweiler_Dog Destory" << endl;
}
void Rottweiler_Dog::show_number()
{
  cout << a << endl;
  //cout << b << endl;  不可以直接访问父类私有变量
  cout << c << endl;
}
int main()
{
  Rottweiler_Dog*  Rettweiler_1 = new Rottweiler_Dog;
  Rettweiler_1->show_number();
  delete Rettweiler_1;
  return 0;
}

在以上代码中,对于子类Rottweiler_Dog来说,想要改变父类中私有变量b的值只能寄希望于Dog提供的外部接口set_private_b来修改b,否则不允许直接访问b,但是对于共有变量a和保护变量c,都可以直接访问。


1.1.2 pravite继承

私有继承:朕的东西都给你,但是到了你的手上,不管在朕这里是公有还是保护属性,你都要变成自己私有的属性。在私有继承下,父类中不管是public还是protected都将在子类中变成私有的,也就说子类的对象不可直接访问父类的公有和保护成员。

以下代码,引用的是之前的类代码,需要注意的是继承属性由public改为private即可:

将上面代码中的
class Rottweiler_Dog : public Dog
改为
class Rottweiler_Dog : private Dog
其余不变
int main()
{
  Rottweiler_Dog*  Rettweiler_1 = new Rottweiler_Dog;
  Rettweiler_1->show_number();
  //新加了代码,想直接调用父类公有方法
  Rettweiler_1->set_private_b(3);//这句代码是错的
  delete Rettweiler_1;
  return 0;
  }

新增了Rettweiler_1->set_private_b(3);想要调用父类公有方法,但是报错:

image.png


所以,私有继承确实很自私,原本父类公有的接口,全部变成自己的私有接口,不给外部提供使用。


1.1.3 protected继承

之前提到protected访问控制的时候,就说会在继承里面详细解释,终于它来了!!!!其实和private是原理相同。在父类中的protected和public成员都在子类中以protected的形式出现,而父类中的private不能访问。朕的私有不给你,朕的保护和公有,你拿去当保护的给外部用,就是这个意思。

继续来看个实例:

将上面代码中的
class Rottweiler_Dog : private  Dog
改为
class Rottweiler_Dog : protected Dog
其余不变
int main()
{
  Rottweiler_Dog*  Rettweiler_1 = new Rottweiler_Dog;
  Rettweiler_1->show_number();
  Rettweiler_1->set_private_b(3);//这句代码是错的
  delete Rettweiler_1;
  return 0;
}

上述代码也是有错误的,还是出在Rettweiler_1->set_private_b(3);上。因为父类的公有,现在变成了子类的protected,而protected属性我们也说过在类内部是相当于公有,对外部接口相当于私有。所以不可以直接访问。


总结:不管那种继承属性,父类私有成员,子类永远是不可访问。当使用公有继承,父类中的公有和保护在子类中属性不变;当使用私有继承,父类中公有和保护变成子类私有;当使用保护继承,父类中公有和保护变成子类保护。


1.2 构造函数和析构函数执行顺序

当有继承发生,并且子类创建一个对象的时候,就会发生父类和子类构造函数和析构函数的执行先后顺序,我们需要写个简单的代码测试一下构造和析构顺序:

class Dog
{
public:
  Dog();
  ~Dog();
  void set_private_b(int b);
  int a = 0;
private:
  int b = 0;
protected:
  int c = 0;
};
Dog::Dog()
{
  cout << "Dog Create" << endl;
}
Dog::~Dog()
{
  cout << "Dog Destory" << endl;
}
void Dog::set_private_b(int b)
{
  this->b = b;
}
class Rottweiler_Dog : public Dog
{
public:
  Rottweiler_Dog();
  ~Rottweiler_Dog();
  void show_number();
private:
  int x = 0;
};
Rottweiler_Dog::Rottweiler_Dog()
{
  a = 10;
  set_private_b(10);
  c = 10;
  cout << "Rottweiler_Dog Create" << endl;
}
Rottweiler_Dog::~Rottweiler_Dog()
{
  cout << "Rottweiler_Dog Destory" << endl;
}
void Rottweiler_Dog::show_number()
{
  cout << a << endl;
  cout << c << endl;
}
int main()
{
  Rottweiler_Dog*  Rettweiler_1 = new Rottweiler_Dog;
  Rettweiler_1->show_number();
  delete Rettweiler_1;
  return 0;
}

结果如下:

image.png

**结论:**当我们用子类创建一个对象时候,首先执行父类的构造函数,接着执行子类的构造函数,销毁对象的时候,首先执行子类析构,最后执行父类析构。一句话,先构造的后析构。


2. 动态多态实现

我理解动态分为静态多态和动态多态,静态多态在之前的有参构造和无参构造时候就讲过了。这节主要说一下动态多态,动态多态就是说多态发生是在程序的运行过程中,在程序最初编译阶段无法确定函数调用,只有动态执行到指定的代码,才能知道到底该执行哪个函数。实现动态多态举个例子,就是基类指针指向子类的对象,调用和父类同名的函数,这时候就会发生动态多态。


2.1 虚函数

首先我们来看看类中虚函数是怎么定义的,虚函数在声明的时候,用virtual关键字在类中进行声明,然后实现的时候,不带有virtual关键字,就和普通的类内成员函数实现是一样的,然后虚函数就可以被子类进行重写。

class Dog
{
public:
  Dog();
  ~Dog();
  //这样的函数我们把它叫做虚函数
  virtual void Dog_bark();//吠
};
Dog::Dog()
{
}
Dog::~Dog()
{
}
void Dog::Dog_bark()
{
  cout << "Dog is barking" << endl;
}

我们现在来个子类继承它,并且重写Dog_bark函数,可以看到下面的代码中,我们用public关键字继承了父类Dog,并且其中重写了Dog_bark函数,所谓重写,就是和父类虚函数相同的名字,子类可以自己实现。

class Rottweiler_Dog : public Dog
{
public:
  Rottweiler_Dog();
  ~Rottweiler_Dog();
  //重写父类虚函数Dog_bark
  void Dog_bark();//吠
};
Mastiff_Dog::Mastiff_Dog()
{
}
Mastiff_Dog::~Mastiff_Dog()
{
}
void Mastiff_Dog::Dog_bark()
{
  cout << "Mastiff_Dog is barking" << endl;
}

好,我们再写一个子类,继续公有继承Dog类。还是重写那个虚函数,然后每个不同的类,Dog_bark输出的内容是不一样的。

class Mastiff_Dog : public Dog
{
public:
  Mastiff_Dog();
  ~Mastiff_Dog();
  void Dog_bark();//吠
};
Rottweiler_Dog::Rottweiler_Dog()
{
}
Rottweiler_Dog::~Rottweiler_Dog()
{
}
void Rottweiler_Dog::Dog_bark()
{
  cout << "Rottweiler_Dog is barking" << endl;
}

接下来我们用main函数进行测试,然后看一下动态多态到底是如何实现的:

int main()
{
  Dog* Dog_1 = new Dog;
  Dog_1->Dog_bark();
  Dog_1 = new Mastiff_Dog();
  Dog_1->Dog_bark();
  Dog_1 = new Rottweiler_Dog();
  Dog_1->Dog_bark();
  return 0;
}

我们在这里面定义了一个指针Dog* Dog_1,这是一个父类的指针,当我们用父类指针指向子类对象的时候,就会出发多态机制。我们看到虽然接下来用new Mastiff_Dog()来生成一个对象,但是赋值给父类指针Dog_1,如果不会发生多态的话,我们预计会输出三个"Dog is barking" ,但是发生多态绑定的话,就会输出三个不同的字符串。我们来看一下结果:

image.png

结论:当父类中有虚函数,子类进行虚函数重写之后,使用父类指针指向子类对象,会触发多态机制,虽然是父类指针,依然会执行子类函数。


2.2 虚表指针

为什么明明是父类的指针,它指向子类的对象时候,调用的是子类的函数啊?这里面到底是什么原理?我曾经疑惑也在这里?我们通过调试来简单的看一下其中的秘密

image.png

当父类的指针指向父类创建的对象,其中有一个__vfptr的指针,指向了Dog::Dog_bark(),所以执行的时候,执行的自然就是父类的Dog_bark。说一下__vfptr是一个虚表指针,它是一个二维指针,指向虚函数地址。接着往下执行:

image.png

我们可以看到__vfptr这个二维指针中的值发生了变化,也就是我们说的重写覆盖了,所以调用时候就会发生执行Mastiff_Dog类中的Dog_bark函数。对于Rottweiler_Dog类也是一样的概念。这里就不做调试了。


总结:之所以发生多态性,是因为虚函数的存在让类中多了虚表指针,父类指针根据虚表指针中的函数地址去执行调用的。


ps:写文比较仓促,文中不免有些技术细节出现问题,如果各位在看的过程中有什么问题发现,可以评论区或者私聊我,我及时改正!!谢谢大家阅读!


那么到这里又是常规吟诗一句,与诸君共勉:

“莫愁前路无知己,天下谁人不识君”


相关实践学习
消息队列+Serverless+Tablestore:实现高弹性的电商订单系统
基于消息队列以及函数计算,快速部署一个高弹性的商品订单系统,能够应对抢购场景下的高并发情况。
云安全基础课 - 访问控制概述
课程大纲 课程目标和内容介绍视频时长 访问控制概述视频时长 身份标识和认证技术视频时长 授权机制视频时长 访问控制的常见攻击视频时长
目录
相关文章
|
8天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
35 4
|
9天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
32 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
23 3
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
19 1