类和对象(友元、运算符重载、继承、多态)---C++

类和对象

  • 4.友元
    • 4.1全局函数做友元
    • 4.2类做友元
    • 4.3成员函数做友元
  • 5.运算符重载
    • 5.1 加号运算符重载
      • 5.1.1成员函数实现运算符重载
      • 5.1.2全局函数实现运算符重载
    • 5.2 左移运算符重载
      • 5.2.1全局函数实现运算符重载
      • 5.2.2成员函数实现运算符重载
    • 5.3 递增/递减运算符重载
      • 5.3.1 前置++
        • 5.3.1.1成员函数实现运算符重载
        • 5.3.1.2全局函数实现运算符重载
      • 5.3.2 后置++
        • 5.3.2.1成员函数实现运算符重载
        • 5.3.2.2全局函数实现运算符重载
      • 5.3.3 前置--
        • 5.3.3.1成员函数实现运算符重载
        • 5.3.3.2全局函数实现运算符重载
      • 5.3.4 后置--
        • 5.3.4.1成员函数实现运算符重载
        • 5.3.4.2全局函数实现运算符重载
    • 5.4 赋值运算符重载
    • 5.5 关系运算符重载
      • 5.5.1 关系运算符(==)重载
      • 5.5.2 关系运算符(>)重载
    • 5.6 函数调用运算符重载
  • 6.继承
    • 6.1 继承的基本语法
    • 6.2继承的方式
    • 6.3继承中的对象模型
    • 6.4继承中的构造和析构顺序
    • 6.5继承同名成员处理方式
      • 6.5.1继承非静态同名成员处理方式
        • 6.5.1.1非静态同名成员变量处理方式
        • 6.5.1.2非静态同名成员函数处理方式
      • 6.5.2继承同名静态成员处理方式
        • 6.5.2.1同名静态成员变量处理方式
        • 6.5.2.2同名静态成员函数处理方式
    • 6.6多继承语法
    • 6.7菱形继承
  • 7.多态
    • 7.1 多态的基本应用
      • 7.1.1 多态的基本概念
      • 7.1.2 多态的基本原理
    • 7.2 纯虚函数和抽象类
    • 7.3 虚析构和纯虚析构

在这里插入图片描述

4.友元

在程序里,有些私有属性也想让类外特殊的一些函数或者类进行访问,就需要用到友元的技术;

友元的目的就是让一个函数或者类访问另一个类中私有成员
友元的关键字为 friend

友元的三种实现

  • 全局函数做友元
  • 类做友元
  • 成员函数做友元

4.1全局函数做友元

class Building
{//告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容friend void goodGay(Building* building);public:Building(){m_SittingRoom = "客厅";m_BedRoom = "卧室";}public://公共权限string m_SittingRoom; //客厅private://私有权限string m_BedRoom; //卧室
};//全局函数
void goodGay(Building* building)
{cout << "好朋友正在访问: " << building->m_SittingRoom << endl;cout << "好朋友正在访问: " << building->m_BedRoom << endl;
}void test01()
{Building b;goodGay(&b);
}

在这里插入图片描述
friend void goodGay(Building* building);

  • 告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容

可见,调用test01函数,当全局函数做友元时,该全局函数可以访问私有权限内容。

4.2类做友元

class Building;//先声明,防止goodGay类出错
class goodGay
{
public://类内声明,类外实现goodGay();void visitor();~goodGay();private:Building* building;
};class Building
{//告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容friend class goodGay;public:Building();public:string m_SittingRoom; //客厅
private:string m_BedRoom;//卧室
};//类外实现成员函数(注意加上所在类空间)
Building::Building()//Building类构造函数
{this->m_SittingRoom = "客厅";this->m_BedRoom = "卧室";
}
goodGay::goodGay()//goodGay类构造函数
{//堆区开辟空间,注意要释放(析构函数释放,delete)building = new Building;
}
goodGay::~goodGay()
{if(building!=NULL){delete building;building=NULL;}
}
void goodGay::visitor()
{cout << "好朋友正在访问" << building->m_SittingRoom << endl;cout << "好朋友正在访问" << building->m_BedRoom << endl;
}void test01()
{goodGay gg;gg.visitor();}

在这里插入图片描述
== friend class goodGay;==

  • 告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容

可见,调用test01函数后,当类做友元时,该类内可以访问另一个类内的私有权限内容。

4.3成员函数做友元

class Building;//先声明,防止goodGay类出错
class goodGay
{
public:goodGay();void visitor1(); //只让visitor1函数作为Building的好朋友,可以发访问Building中私有内容void visitor2();private:Building* building;
};class Building
{//告诉编译器  goodGay类中的visitor1成员函数 是Building好朋友,可以访问私有内容friend void goodGay::visitor1();public:Building();public:string m_SittingRoom; //客厅
private:string m_BedRoom;//卧室
};Building::Building()
{this->m_SittingRoom = "客厅";this->m_BedRoom = "卧室";
}
goodGay::goodGay()
{building = new Building;
}void goodGay::visitor1()
{cout << "好朋友1正在访问" << building->m_SittingRoom << endl;cout << "好朋友1正在访问" << building->m_BedRoom << endl;
}
void goodGay::visitor2()
{cout << "好朋友2正在访问" << building->m_SittingRoom << endl;//cout << "好朋友正在访问" << building->m_BedRoom << endl;//无法访问
}void test01()
{goodGay  gg;gg.visitor1();gg.visitor2();}

在这里插入图片描述
friend void goodGay::visitor1();

  • 告诉编译器 goodGay类中的visitor1成员函数 是Building好朋友,可以访问私有内容

可见,调用test01函数后,当一个类内的成员函数做另一个类的友元时,该类内的成员函数可以访问另一个类内的私有权限内容。

5.运算符重载

运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。

5.1 加号运算符重载

作用:实现两个自定义数据类型相加的运算。

对于内置数据类型,编译器知道如何进行运算:
例如两个整型数据的相加,可以直接应用,但对于一些非内置数据类型,比如两个对象的相加,编译器内部没有相关运算方式,故需要我们自己对运算符进行补充。

如果不进行运算符重载,就会出现下面类似的错误:
在这里插入图片描述

5.1.1成员函数实现运算符重载

实例:实现两个对象的相加

class Person {
public:Person() {};//为了提供对无参构造函数的调用//若无,自己定义了有参构造函数,系统默认无无参构造函数//则Person temp;无法成立Person(int a, int b)//有参构造{this->m_A = a;this->m_B = b;}//成员函数实现 + 号运算符重载//函数名operator+系统默认,可以实现简化调用//自己定义函数名也可,但无法实现下面的简化调用Person operator+(const Person& p) {Person temp;//无参构造temp.m_A = this->m_A + p.m_A;temp.m_B = this->m_B + p.m_B;return temp;//值返回(重新创建新对象)}public:int m_A;int m_B;
};//运算符重载 可以发生函数重载 
//同一函数名表示不同运算
Person operator+(const Person& p2, int val)
{Person temp;temp.m_A = p2.m_A + val;temp.m_B = p2.m_B + val;return temp;
}//测试函数
void test() {Person p1(10, 10);Person p2(20, 20);//成员函数方式//本质调用为://Person p3=p2.operaor+(p1)Person p3 = p2 + p1; cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;//本质调用为://Person p4=operaor+(p3,10)Person p4 = p3 + 10; cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;}

在这里插入图片描述

5.1.2全局函数实现运算符重载

示例:

class Person {
public:Person(int a, int b)//有参构造{this->m_A = a;this->m_B = b;}public:int m_A;int m_B;
};//全局函数实现 + 号运算符重载
//对象+对象
Person operator+(const Person & p1, const Person & p2) {Person temp(0, 0);temp.m_A = p1.m_A + p2.m_A;temp.m_B = p1.m_B + p2.m_B;return temp;
}
//运算符重载 可以发生函数重载 
//对象+int
Person operator+(const Person& p2, int val)
{Person temp(0,0);temp.m_A = p2.m_A + val;temp.m_B = p2.m_B + val;return temp;
}void test() {Person p1(10, 10);Person p2(20, 20);//成员函数方式//本质实现:Person p3 = operator+ (p1, p2);//Person p3 = p2 + p1;  cout << "mA:" << p3.m_A << " mB:" << p3.m_B << endl;//本质实现:Person p4 = operator+ (p3, 10);Person p4 = p3 + 10; cout << "mA:" << p4.m_A << " mB:" << p4.m_B << endl;
}

在这里插入图片描述

总结1:对于内置的数据类型的表达式的的运算符是不可能改变的;
总结2:不要滥用运算符重载。(即不可命名为加号运算符重载,实现用减法)

5.2 左移运算符重载

作用:可以输出自定义数据类型。

5.2.1全局函数实现运算符重载

示例:

class Person {//友元:实现对私有权限成员的访问friend ostream& operator<<(ostream& out, Person& p);public:Person(int a, int b){this->m_A = a;this->m_B = b;}private:int m_A;int m_B;
};//全局函数实现左移重载
//ostream对象只能有一个
ostream& operator<<(ostream& out, Person& p) {out << "a:" << p.m_A << " b:" << p.m_B;return out;
}void test() {Person p1(10, 20);cout << p1 << "hello world" << endl; //链式编程
}

在这里插入图片描述

5.2.2成员函数实现运算符重载

示例:

  • 1.对象本身做形参
class Person {public:Person(int a, int b){this->m_A = a;this->m_B = b;}//成员函数 实现左移运算符重载,可以实现,但不是我们想要的效果ostream& operator<<(Person& p){cout << "a:" << p.m_A << " b:" << p.m_B;return cout;//链式编程}	
private:int m_A;int m_B;
};void test() {Person p1(10, 20);p1.operator<<(p1)<<endl;//简化p1 << p1 << " hello world" <<endl;//与内置函数实现不一致
}

在这里插入图片描述

  • 2.标准输出流做形参

示例:

class Person {public:Person(int a, int b){this->m_A = a;this->m_B = b;}//成员函数 实现左移运算符重载,可以实现,但不是我们想要的效果ostream& operator<<(ostream &out) {out << "a:" << this->m_A << " b:" << this->m_B;;return cout;//链式编程}private:int m_A;int m_B;
};void test() {Person p1(10, 20);//本质实现://p1.operator<<(cout);p1 << cout << " hello world" << endl;//可见与内置函数输出不一致
}

利用成员函数重载左移运算符,无法实现与内置输出一致的顺序(即cout<<p,cout在左侧),故不会利用成员函数重载<<运算符。

总结:重载左移运算符配合友元可以实现输出自定义数据类型

5.3 递增/递减运算符重载

作用: 通过重载递增运算符,实现自己的整型数据

5.3.1 前置++

5.3.1.1成员函数实现运算符重载

示例:

class MyInteger {friend ostream& operator<<(ostream& out, MyInteger myint);public:MyInteger() {m_Num = 0;}//前置++//局部函数实现//返回引用MyInteger& operator++() {//先++m_Num++;//再返回return *this;//返回对象本身(引用),实现对一直对一个对象进行递增操作}private:int m_Num;
};//左移运算符重载:全局函数
ostream& operator<<(ostream& out, MyInteger myint) {out << myint.m_Num;return out;
}//前置++ 先++ 再返回
void test01() {MyInteger myInt;cout << ++(++myInt) << endl;cout << myInt << endl;
//本质实现:
//operator<<(cout, myInt.operator++())<<endl;//相当于cout << ++myInt << endl;
//cout << myInt.operator++().operator++() << endl;//相当于cout << ++(++myInt) << endl;
}

分析:++myInt:m_Num=1(返回对象本身);
++(++myInt)(对同一个对象++) :m_Num=2(返回对象本身);

在这里插入图片描述

  • 如果将返回值改为值返回:即
//值返回
MyInteger operator++() {//先++m_Num++;//再返回return *this;//拷贝一个新对象
}

分析:++myInt(第一次:对象本身++):m_Num=1(创建新对象);
++(++myInt)(对新对象++):m_Num=2(返回新对象);

  • 即cout << ++(++myInt) << endl中的++(++myInt)不再是原对象,而是创建的第二个新对象(其内容和返回对象本身结束的时候一样);
    cout << myInt << endl;输出对象本身。

在这里插入图片描述
可见,返回类型不同,最后结果不同。

  • 返回对象本身(引用):实现对一直对一个对象进行递增操作,每一次++都对同一个进行运算。
  • 值返回方式:第一次调用++是对对象本身进行,但之后回利用拷贝构造函数创建一个新的对象,就形成了每次调用都会形成一个新对象,无法实现如同第一种(返回引用)的对同一个对象持续累加。
5.3.1.2全局函数实现运算符重载
MyInteger& operator++(MyInteger &myInt) {//先++myInt.m_Num++;//再返回return myInt;//返回对象本身(引用)
}

注:全局函数下,要实现链式访问只能返回对象本身(返回引用);

5.3.2 后置++

在这里插入图片描述
对于后置++,无法实现链式编程。故对于后置++的重载也无法实现链式编程。
报错原因:表达式必须是可修改的左值。

5.3.2.1成员函数实现运算符重载

示例:

class MyInteger {friend ostream& operator<<(ostream& out, MyInteger myint);
public:MyInteger() {m_Num = 0;}//后置++MyInteger operator++(int) {//先返回MyInteger temp = *this; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;m_Num++;return temp;}private:int m_Num;
};
//左移运算符重载:全局函数
ostream& operator<<(ostream& out, MyInteger myint) {out << myint.m_Num;return out;
}//后置++ 先返回 再++
void test02() {MyInteger myInt;cout << myInt++ << endl;cout << myInt << endl;//本质实现://operator<<(cout, myInt.operator++(0))<<endl;//operator<<(cout, myInt)<<endl;
}

在这里插入图片描述

MyInteger operator++(int):int-占位参数

5.3.2.2全局函数实现运算符重载

示例:

//int为占位参数,为了和前置++区分;调用时需补占位参数(任意数都可)
MyInteger operator++(MyInteger& myInt,int) {//先返回MyInteger temp = myInt; //记录当前本身的值,然后让本身的值加1,但是返回的是以前的值,达到先返回后++;myInt.m_Num++;return temp;
}//后置++ 先返回 再++
void test02() {MyInteger myInt;cout << myInt++ << endl;cout << myInt << endl;// //本质实现://operator<<(cout, operator++(myInt,0)) << endl;//operator<<(cout, myInt)<<endl;}

5.3.3 前置–

5.3.3.1成员函数实现运算符重载

参考前置++:(前置–可以实现链式编程,只列写返回对象本身)

MyInteger& operator--() {//先--m_Num--;//再返回return *this;//返回对象本身(引用),实现对一直对一个对象进行递增操作
}
5.3.3.2全局函数实现运算符重载
MyInteger& operator--(MyInteger& myInt) {//先++myInt.m_Num--;//再返回return myInt;//返回对象本身(引用)
}

5.3.4 后置–

参见后置++:

5.3.4.1成员函数实现运算符重载
//后置--
MyInteger& operator--(int) {//先返回MyInteger temp = *this; //记录当前本身的值,然后让本身的值减1,但是返回的是以前的值,达到先返回后--;m_Num--;return temp;
}
5.3.4.2全局函数实现运算符重载
//int为占位参数,为了和前置--区分;调用时需补占位参数(任意数都可)
MyInteger operator--(MyInteger& myInt,int) {//先返回MyInteger temp = myInt; //记录当前本身的值,然后让本身的值减1,但是返回的是以前的值,达到先返回后--;myInt.m_Num--;return temp;
}

递增/递减运算符重载总结: 前置递增返回引用,后置递增返回值.

5.4 赋值运算符重载

c++编译器至少给一个类添加4个函数:

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝
  4. 赋值运算符 operator=, 对属性进行值拷贝

前三个之前介绍过,此处着重介绍第四个:
如果类中有属性指向堆区,做赋值操作时也会出现深浅拷贝问题 。
深浅拷贝

示例:

class Person
{
public:Person(int age){//将年龄数据开辟到堆区m_Age = new int(age);}//重载赋值运算符 Person& operator=(Person& p){//判断是否有属性在堆区,若有先释放干净,再进行深拷贝if (m_Age != NULL){delete m_Age;m_Age = NULL;}//编译器提供的代码是浅拷贝//m_Age = p.m_Age;//提供深拷贝 解决浅拷贝的问题m_Age = new int(*p.m_Age);//返回自身return *this;//链式编程}~Person(){//释放堆区空间if (m_Age != NULL){delete m_Age;m_Age = NULL;}}//年龄的指针int* m_Age;};
void test01()
{Person p1(18);Person p2(20);Person p3(30);p3 = p2 = p1; //赋值操作:链式编程cout << "p1的年龄为:" << *p1.m_Age << endl;cout << "p2的年龄为:" << *p2.m_Age << endl;cout << "p3的年龄为:" << *p3.m_Age << endl;
}

在这里插入图片描述
附:
参照内置类型赋值的链式程序。

int a = 10;
int b = 20;
int c = 30;c = b = a;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;

在这里插入图片描述

5.5 关系运算符重载

作用:重载关系运算符,可以让两个自定义类型对象进行对比操作。

5.5.1 关系运算符(==)重载

示例:

class Person
{
public:Person(string name, int age){this->m_Name = name;this->m_Age = age;};bool operator==(Person& p){if (this->m_Name == p.m_Name && this->m_Age == p.m_Age){return true;}else{return false;}}string m_Name;int m_Age;
};void test01()
{Person a("Tom", 18);Person b("Tom", 25);if (a == b){cout << "a和b相等" << endl;}else{cout << "a和b不相等" << endl;}}

在这里插入图片描述

5.5.2 关系运算符(>)重载

示例:

class Person
{
public:Person(string name, int age){this->m_Name = name;this->m_Age = age;};int operator>(Person& p){//按字母进行比较return this->m_Name.compare(p.m_Name);//compare按照每一个字母的ASCII值进行比较,根据结果返回0,大于0,小于0}string m_Name;int m_Age;
};void test01()
{Person a("Tom", 18);Person b("Marry", 25);if ((a > b) == 0){cout << "a==b" << endl;}else if ((a > b) > 0){cout << "a>b" << endl;}elsecout << "a<b" << endl;
}

在这里插入图片描述

5.6 函数调用运算符重载

  • 函数调用运算符 () 也可以重载;
  • 由于重载后使用的方式非常像函数的调用,因此称为仿函数;
  • 仿函数没有固定写法,非常灵活。

示例:

class MyAdd
{
public:int operator()(int v1, int v2){return v1 + v2;}
};void test01()
{MyAdd add;int ret = add(10, 10);//相当于:int ret = add.operator()(10, 10)cout << "ret = " << ret << endl;//匿名对象调用  cout << "MyAdd()(100,100) = " << MyAdd()(100, 100) << endl;
}

在这里插入图片描述

6.继承

继承是面向对象三大特性之一

有些类与类之间存在特殊的关系,例如下图中:
在这里插入图片描述
猫和狗都具备动物的属性,同时其又有很多品种(自己的属性)。

我们发现,定义这些类时,下级别的成员除了拥有上一级的共性,还有自己的特性。
这个时候我们就可以考虑利用继承的技术,减少重复代码

6.1 继承的基本语法

继承的语法:class 子类 : 继承方式 父类
借助下面的事例,介绍继承的优势和语法:

例如:
我们看到很多网站(以某网站编程培训为例)中,都有公共的头部,公共的底部,甚至公共的左侧列表,只有中心内容不同;接下来我们分别利用普通写法和继承的写法来实现网页中的内容,看一下继承存在的意义以及好处:

普通实现:

//Java页面
class Java 
{
public:void header(){cout << "首页、公开课、登录、注册...(公共头部)" << endl;}void footer(){cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;}void content(){cout << "JAVA学科视频" << endl;}
};
//Python页面
class Python
{
public:void header(){cout << "首页、公开课、登录、注册...(公共头部)" << endl;}void footer(){cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;}void content(){cout << "Python学科视频" << endl;}
};
//C++页面
class CPP 
{
public:void header(){cout << "首页、公开课、登录、注册...(公共头部)" << endl;}void footer(){cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;}void content(){cout << "C++学科视频" << endl;}
};//测试函数
void test01()
{//Java页面cout << "Java下载视频页面如下: " << endl;Java ja;ja.header();ja.footer();ja.content();cout << "--------------------" << endl;//Python页面cout << "Python下载视频页面如下: " << endl;Python py;py.header();py.footer();py.content();cout << "--------------------" << endl;//C++页面cout << "C++下载视频页面如下: " << endl;CPP cp;cp.header();cp.footer();cp.content();}

可见,对于上述代码,有一部分代码多次重复引用,虽然结构清晰,但会造成代码冗余,内存浪费。
借助继承的特性可以实现简化:

继承实现:

//公共页面
class BasePage
{
public:void header(){cout << "首页、公开课、登录、注册...(公共头部)" << endl;}void footer(){cout << "帮助中心、交流合作、站内地图...(公共底部)" << endl;}};//Java页面
class Java : public BasePage
{
public:void content(){cout << "JAVA学科视频" << endl;}
};
//Python页面
class Python : public BasePage
{
public:void content(){cout << "Python学科视频" << endl;}
};
//C++页面
class CPP : public BasePage
{
public:void content(){cout << "C++学科视频" << endl;}
};

在这里插入图片描述
将普通实现的每个类内容替换,测试函数不变,会实现如上的结果。

总结:

继承的好处:可以减少重复的代码
class A : public B
A 类称为子类 或 派生类
B 类称为父类 或 基类

派生类中的成员,包含两大部分

一类是从基类继承过来的,一类是自己增加的成员
从基类继承过过来的表现其共性,而新增的成员体现了其个性

6.2继承的方式

继承的语法:class 子类 : 继承方式 父类

继承方式一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承
    不同继承方式下,对于父类的不同权限的内容的访问条件,可以用下面的图进行说明。
    在这里插入图片描述
    可联系,封装权限中的保护权限和私有权限的区别:
  • protected 保护权限 :类内可以访问 类外不可以访问(例如:儿子可以访问到父亲中的保护内容)
  • private 私有权限 :类内可以访问 类外不可以访问(例如:儿子不可以访问到父亲中的私有内容)

6.3继承中的对象模型

问题:
从父类继承过来的成员,哪些属于子类对象中?
或者说子类的大小对父类中的继承权限有无关系?

示例:

class Base
{
public:int m_A;
protected:int m_B;
private:int m_C; //私有成员只是被隐藏了,但是还是会继承下去
};//公共继承
class Son :public Base
//对于父类中的公共权限和保护权限,可访问,不可访问私有权限
{
public:int m_D;
};void test01()
{cout << "sizeof Son = " << sizeof(Son) << endl;
}

在这里插入图片描述
可见,虽然子类无法访问父类中的私有权限内容,但子类大小是包含父类中的私有权限的。
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:

在这里插入图片描述
由上图可知,对于Son类中,包含父类的全部内容(三种权限内容都被继承下来)和自己的特有内容,虽然对于公共继承而言,父类中私有权限无法访问,但其也被子类继承,只是被编译器隐藏。

6.4继承中的构造和析构顺序

子类继承父类后,当创建子类对象,也会调用父类的构造函数;
问题:父类和子类的构造和析构顺序是谁先谁后?

  • 继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
    示例:
class Base
{
public:Base(){cout << "Base构造函数!" << endl;}~Base(){cout << "Base析构函数!" << endl;}
};class Son : public Base
{
public:Son(){cout << "Son构造函数!" << endl;}~Son(){cout << "Son析构函数!" << endl;}};void test01()
{Son s;
}

在这里插入图片描述

总结:继承中 先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反。
(可对比类对象作为类成员的构造和析构函数调用顺序)2.7类对象作为类成员

6.5继承同名成员处理方式

6.5.1继承非静态同名成员处理方式

问题:当子类与父类出现同名的成员,如何通过子类对象,访问到子类或父类中同名的数据呢?

  • 访问子类同名成员 直接访问即可;
  • 访问父类同名成员 需要加作用域
6.5.1.1非静态同名成员变量处理方式

示例:

//父类
class Base {
public:Base(){m_A = 100;}public:int m_A;
};//子类
class Son : public Base {
public:Son(){m_A = 200;}
public:int m_A;
};void test01()
{//子类和父类都有m_A成员变量Son s;cout << "Son类下m_A:" << s.m_A << endl;cout << "Base类下m_A:" << s.m_A << endl;cout << "Base类下m_A:" << s.Base::m_A << endl;
}

在这里插入图片描述
可见,子类和父类中有同名成员变量时,如要访问父类中成员变量需要加上父类所在作用域。

6.5.1.2非静态同名成员函数处理方式

当子类与父类拥有同名的成员函数,子类会隐藏父类中所有版本的同名成员函数;
如果想访问父类中被隐藏的同名成员函数,需要加父类的作用域.

示例:

//父类
class Base {
public:void func(){cout << "Base - func()调用" << endl;}void func(int a){cout << "Base - func(int a)调用" << endl;}public:int m_A;
};//子类
class Son : public Base {
public:void func(){cout << "Son - func()调用" << endl;}
public:int m_B;
};void test02()
{Son s;s.func();//s.func(10);//报错s.Base::func();s.Base::func(10);
}

在这里插入图片描述
总结:

  1. 子类对象可以直接访问到子类中同名成员
  2. 子类对象加作用域可以访问到父类同名成员
  3. 当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数

6.5.2继承同名静态成员处理方式

问题:继承中同名的静态成员在子类对象上如何进行访问?

静态成员和非静态成员出现同名,处理方式一致

  • 访问子类同名成员 直接访问即可;
  • 访问父类同名成员 需要加作用域
6.5.2.1同名静态成员变量处理方式

示例:

class Base {
public:static int m_A;
};//类内声明,类外初始化
int Base::m_A = 100;class Son : public Base {
public:static int m_A;
};
//类内声明,类外初始化
int Son::m_A = 200;//同名成员属性
void test01()
{//通过对象访问cout << "通过对象访问: " << endl;Son s;cout << "Son  下 m_A = " << s.m_A << endl;cout << "Base 下 m_A = " << s.Base::m_A << endl;//通过类名访问cout << "通过类名访问: " << endl;cout << "Son  下 m_A = " << Son::m_A << endl;cout << "Base 下 m_A = " << Son::Base::m_A << endl;
}

在这里插入图片描述
对于静态同名成员变量,子类和父类中成员变量的访问有两种方式:

  • 通过对象访问:子类对象直接访问子类同名成员,父类成员访问需要子类对象加上父类作用域;
  • 通过类名访问:子类对象可以直接在子类类名直接访问,父类成员需要在子类对象类名的基础上加上父类的作用域。
    • Son::Base::m_A 中第一个::代表通过类名方式访问;第二个::代表访问父类作用域下。
6.5.2.2同名静态成员函数处理方式

同同名静态成员变量访问一样。
示例:

class Base {
public:static void func(){cout << "Base - static void func()" << endl;}static void func(int a){cout << "Base - static void func(int a)" << endl;}static int m_A;
};class Son : public Base {
public:static void func(){cout << "Son - static void func()" << endl;}static int m_A;
};//同名成员函数
void test02()
{//通过对象访问cout << "通过对象访问: " << endl;Son s;s.func();s.Base::func();s.Base::func(10);//通过类名访问cout << "通过类名访问: " << endl;Son::func();Son::Base::func();//出现同名,子类会隐藏掉父类中所有同名成员函数,需要加作作用域访问Son::Base::func(100);
}

在这里插入图片描述

总结:同名静态成员处理方式和非静态处理方式一样,只不过有两种访问的方式(通过对象 和 通过类名)

大总结:

  • 访问子类同名成员 直接访问即可
  • 访问父类同名成员 需要加作用域

6.6多继承语法

C++允许一个类继承多个类
语法: class 子类 :继承方式 父类1 , 继承方式 父类2...

多继承可能会引发父类中有同名成员出现,需要加作用域区分。
注:C++实际开发中不建议用多继承

示例:

//父类1
class Base1 {
public:Base1(){m_A = 100;}
public:int m_A;
};//父类2
class Base2 {
public:Base2(){m_A = 200;  }
public://同名成员变量int m_A;
};//语法:class 子类:继承方式 父类1 ,继承方式 父类2 
class Son : public Base2, public Base1
{
public:Son(){m_C = 300;m_D = 400;}
public:int m_C;int m_D;
};//多继承容易产生成员同名的情况
//通过使用类名作用域可以区分调用哪一个基类的成员
void test01()
{Son s;cout << "sizeof Son = " << sizeof(s) << endl;cout << s.Base1::m_A << endl;cout << s.Base2::m_A << endl;
}

在这里插入图片描述
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
在这里插入图片描述

总结: 多继承中如果父类中出现了同名情况,子类使用时候要加作用域

6.7菱形继承

菱形继承概念:
​ 两个派生类继承同一个基类;
​ 又有某个类同时继承者两个派生类;
​ 这种继承被称为菱形继承,或者钻石继承。
典型的菱形继承案例:
在这里插入图片描述
在上图中,羊和驼都继承了动物中的属性,同时羊驼又分别继承两者的属性,就会造成羊驼中重复包含动物属性,造成浪费。即:

菱形继承问题:

  1. 羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性(不知道继承谁的)。
  2. 草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。

普通示例:

class Animal
{
public:int m_Age;
};class Sheep :  public Animal {};
class camel :  public Animal {};//camel-骆驼
class alpaca : public Sheep, public camel {};//alpaca-羊驼void test01()
{alpaca st;st.Sheep::m_Age = 100;st.camel::m_Age = 200;cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;cout << "st.camel::m_Age = " << st.camel::m_Age << endl;
}

在这里插入图片描述
对于,羊和驼中都含有动物中年龄成员变量,羊驼中就会含有两份年龄成员变量,就会造成以哪个为准呢?
虽然我们可通过添加作用域进行区分,但仍会造成内存浪费,毕竟羊驼只需要一份就可。这个问题可以通过下面的方式进行解决:

优化示例:

虚继承:

  • 继承前加virtual关键字后,变为虚继承
  • 此时公共的父类Animal称为虚基类
class Animal
{
public:int m_Age;
};class Sheep : virtual public Animal {};
class camel : virtual public Animal {};//camel-骆驼
class alpaca : public Sheep, public camel {};//alpaca-羊驼void test01()
{alpaca st;st.Sheep::m_Age = 100;st.camel::m_Age = 200;cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;cout << "st.camel::m_Age = " << st.camel::m_Age << endl;cout << "st.m_Age = " << st.m_Age << endl;st.Sheep::m_Age = 300;cout << "st.Sheep::m_Age = " << st.Sheep::m_Age << endl;cout << "st.camel::m_Age = " << st.camel::m_Age << endl;cout << "st.m_Age = " << st.m_Age << endl;
}

在这里插入图片描述
可以看出,采用虚继承方式,羊驼继承动物的属性就变成了一份,无论改变羊和驼中哪个属性,三者中的属性变量就都改变了。
下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:

  • 普通示例:
    在这里插入图片描述
  • 优化示例:
    在这里插入图片描述
    对比两者,我们可以方向,参与虚继承方式下,动物类中属性(m_Age)只含有一份。
    虚继承下,羊和驼类中包含一个指针(vbptr-virtual base pointer虚基类指针)指向vbtable(虚基类表-图中1和2的位置),羊和驼中的虚基类表包含各自的偏移量,用于找到动物类中的属性(m_Age),羊和驼就不再分别基础动物类中的属性了,只继承一个指针用于找到动物类的属性,减少空间浪费。

总结:

  • 菱形继承带来的主要问题是子类继承两份相同的数据,导致资源浪费以及毫无意义
  • 利用虚继承可以解决菱形继承问题

7.多态

在编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口
多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上

7.1 多态的基本应用

7.1.1 多态的基本概念

多态是C++面向对象三大特性之一

多态分为两类:

  • 静态多态: 函数重载和运算符重载属于静态多态,复用函数名;
  • 动态多态: 派生类(子类)和虚函数实现运行时多态。

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

非多态示例:

class Animal
{
public:void speak(){cout << "动物在说话" << endl;}
};class Cat :public Animal
{
public:void speak(){cout << "小猫在说话" << endl;}
};class Dog :public Animal
{
public:void speak(){cout << "小狗在说话" << endl;}};//希望实现每次调用函数,给什么形参就调用谁的函数
//提供一个公共接口,否则就需要多个函数才可实现对每个动物叫的实现
void DoSpeak(Animal& animal)
{animal.speak();
}void test01()
{Cat cat;DoSpeak(cat);Dog dog;DoSpeak(dog);
}

在这里插入图片描述
但由输出来看与我们的想法不同,每次都调用动物类的说话函数(speak)。

原因:
由于doSpeak函数在编译阶段就确定了函数地址,即地址早绑定——静态多态

对于此种问题可以采用多态技术进行解决:

要实现调用谁,谁执行就需要使得doSpeak函数地址在运行阶段进行绑定,实现地址晚绑定——动态多态。

多态示例:
只需要在父类speak函数前添加关键字-virtual,形成虚函数。

class Animal
{
public://Speak函数就是虚函数//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。virtual void speak(){cout << "动物在说话" << endl;}
};
//我们希望传入什么对象,那么就调用什么对象的函数
//如果函数地址在编译阶段就能确定,那么静态联编
//如果函数地址在运行阶段才能确定,就是动态联编

在这里插入图片描述

多态满足条件
1、有继承关系(继承是多态实现的基础);
2、子类重写父类中的虚函数:

  • 也就是与父类虚函数一样(关键字virtual可加可不加),返回值类型 函数名 形参列表完全一致

多态使用: 父类指针或引用指向子类对象,即:
void DoSpeak(Animal& animal)-父类引用;
void DoSpeak(Animal* animal)-父类指针。

7.1.2 多态的基本原理

class Animal
{
public://Speak函数就是虚函数//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。virtual void speak(){cout << "动物在说话" << endl;}
};class Cat :public Animal
{
public:void speak(){cout << "小猫在说话" << endl;}
};class Dog :public Animal
{
public:void speak(){cout << "小狗在说话" << endl;}};//希望实现每次调用函数,给什么形参就调用谁的函数
//提供一个公共接口,否则就需要多个函数才可实现对每个动物叫的实现
void DoSpeak(Animal& animal)
{animal.speak();
}void test01()
{Cat cat;DoSpeak(cat);
}

对于多态来说,需要满足两个条件,继承和重写虚函数。

  • 在子类未重写父类的虚函数时:
    在这里插入图片描述
    子类(猫类)由于继承父类,因此在子类未重写父类虚函数时,子类中将父类中的虚函数完全复制一份。
    (图中,父类存储一个指针(即vfptr),其指向vftable-虚函数表,表中放着虚函数的地址)
    下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
    在这里插入图片描述
    在这里插入图片描述

  • 子类重写父类虚函数
    在这里插入图片描述
    子类虚函数表内部就替换成子类的(虚)函数地址,子类就有了自己的(虚)函数,从而可以进行调用自己的函数。
    下面借助VS自带的开发人员命令提示符窗口,对上述内容进行一个深入介绍:
    在这里插入图片描述

  • 父类指针或引用指向子类对象

void DoSpeak(Animal& animal)
{animal.speak();
}
//父类函数传入子类对象:DoSpeak(cat);
//Animal& animal=Cat;
//再进行调用父类函数:animal.speak();
//由于Animal& animal=Cat,指向子类对象(Cat),编译器就会在子类的虚函数表中去找内部的函数地址,从而调用子类函数。

7.2 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类

抽象类特点

  • 无法实例化对象;
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
    示例:
class Base
{
public://纯虚函数//类中只要有一个纯虚函数就称为抽象类virtual void func() = 0;
};class Son :public Base
{
public://子类必须重写父类中的纯虚函数,否则也属于抽象类virtual void func(){cout << "func调用" << endl;};
};void test01()
{Base* base = NULL;//base = new Base; // 错误,抽象类无法实例化对象//父类指针指向子类对象base = new Son;base->func();//通过父类指针调用子类函数delete base;//记得销毁
}

7.3 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
问题示例:

class Animal {
public:Animal(){cout << "Animal 构造函数调用!" << endl;}virtual void Speak() = 0;~Animal(){cout << "Animal析构函数调用!" << endl;}
};class Cat : public Animal {
public:Cat(string name){cout << "Cat构造函数调用!" << endl;m_Name = new string(name);}virtual void Speak(){cout << *m_Name << "小猫在说话!" << endl;}~Cat(){cout << "Cat析构函数调用!" << endl;if (this->m_Name != NULL) {delete m_Name;m_Name = NULL;}}public:string* m_Name;
};void test01()
{Animal* animal = new Cat("Tom");//父类指针指向子类对象animal->Speak();delete animal;//释放
}

在这里插入图片描述

由此可知,对于子类在堆区开辟的空间,对父类指针释放未能对子类空间释放,造成内存泄漏。(未能调用子类析构函数)

解决方式
将父类中的析构函数改为虚析构或者纯虚析构

  • 虚析构和纯虚析构共性

    • 可以解决父类指针释放子类对象;
    • 都需要有具体的函数实现
  • 虚析构和纯虚析构区别:

    • 如果是纯虚析构,该类属于抽象类,无法实例化对象。

虚析构语法:

virtual ~类名(){}

纯虚析构语法:(类内声明,类外实现

类内声明: virtual ~类名() = 0;

类外实现:类名::~类名(){}
示例:

  • 虚析构函数
class Animal {
public:Animal(){cout << "Animal 构造函数调用!" << endl;}//纯虚函数virtual void Speak() = 0;法一:析构函数加上virtual关键字,变成虚析构函数virtual~Animal(){cout << "Animal虚析构函数调用!" << endl;}
};

在这里插入图片描述

  • 纯虚析构函数
class Animal {
public:Animal(){cout << "Animal 构造函数调用!" << endl;}//纯虚函数virtual void Speak() = 0;//法二:类内声明:纯虚析构函数//和包含普通纯虚函数的类一样,包含了纯虚析构函数的类也是一个抽象类。不能够被实例化。virtual ~Animal() = 0;
};//法二:类外实现:
Animal::~Animal()
{cout << "Animal 纯虚析构函数调用!" << endl;
}

在这里插入图片描述
注意:纯虚析构函数不要忘了类外实现,否则会出现下面的错误;
在这里插入图片描述

总结:

​ 1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象;

​ 2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构;

​ 3. 拥有纯虚析构函数的类也属于抽象类。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/645545.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

将vue组件发布成npm包

文章目录 前言一、环境准备1.首先最基本的需要安装nodejs&#xff0c;版本推荐 v10 以上&#xff0c;因为需要安装vue-cli2.安装vue-cli 二、初始化项目1.构建项目2.开发组件/加入组件3. 修改配置文件 三、调试1、执行打包命令2、发布本地连接包3、测试项目 四、发布使用1、注册…

德州仪器(TI):市场形势仍不明朗

TI作为模拟芯片大厂龙头&#xff0c;客户超过100,000家&#xff0c;产品上千万种&#xff0c;前10大客户占公司营收5%&#xff0c;前100大产品占公司营收0.1%。客户群庞大且拥有半导体业界最广的产品范围。因此&#xff0c;TI的市场行情展望对整个产业具参考价值。 根据TI公布…

Mediasoup Demo-v3笔记(一)——框架和Nodejs的基本语法

Medisasop Demo的框架 Nodejs基本语法 后记   个人总结&#xff0c;欢迎转载、评论、批评指正

SSH 解析 | 关键参数 | 安全配置

介绍 SSH&#xff08;Secure Shell&#xff09;是一种用于在计算机网络上进行安全远程访问和执行命令的协议。提供加密通信通道&#xff0c;防止敏感信息在传输过程中被窃听或篡改。SSH还支持文件传输和端口转发等功能&#xff0c;使其成为广泛使用的安全远程管理工具。 1. 安…

使用POI生成word文档的table表格

文章目录 使用POI生成word文档的table表格1. 引入maven依赖2. 生成table的两种方式介绍2.1 生成一行一列的table2.2 生成固定行列的table2.3 table合并列2.4 创建多个table存在的问题 使用POI生成word文档的table表格 1. 引入maven依赖 <dependency><groupId>org.…

“探索C语言操作符的神秘世界:从入门到精通的全方位解析“

各位少年&#xff0c;我是博主那一脸阳光&#xff0c;今天来分享深度解析C语言操作符&#xff0c;C语言操作符能帮我们解决很多逻辑性的问题&#xff0c;减少很多代码量&#xff0c;就好比数学的各种符号&#xff0c;我们现在深度解剖一下他们。 前言 在追求爱情的道路上&…

深入浅出AI落地应用分析:AI视频生成Top 5应用

接下俩会每周集中体验一些通用或者垂直的AI落地应用&#xff0c;主要以一些全球或者国外国内排行较前的产品为研究对象&#xff0c;「AI 产品榜&#xff1a; aicpb.com」以专题的方式在博客进行分享。 一、Loom 二、Runway 产品链接&#xff1a;https://app.runwayml.com/ …

ubuntu 22.04 安装mysql-8.0.34

ubuntu 22.04 安装mysql-8.0.34 1、基础安装配置 更新软件包&#xff1a; sudo apt update查看可用软件包&#xff1a; sudo apt search mysql-server安装最新版本&#xff1a; sudo apt install -y mysql-server或者&#xff0c;安装指定版本&#xff1a; sudo apt inst…

【Python程序开发系列】并发执行协程任务超时的解决方案(案例分析)

一、问题 假如我在利用协程并发执行任务的时候&#xff0c;会出现有些任务特别耗时&#xff0c;从而导致程序运行卡住&#xff0c;我们想跳过这些执行特别耗时的任务&#xff0c;只返回不超时的任务结果该怎么解决&#xff1f; 二、实现过程 2.1 情景 假如我有四个任务需要并…

MySQL--删除数据表(6)

MySQL中删除数据表是非常容易操作的&#xff0c;但是你在进行删除表操作时要非常小心&#xff0c;因为执行删除命令后所有数据都会消失。 语法 以下为删除 MySQL 数据表的通用语法&#xff1a; DROP TABLE table_name ; -- 直接删除表&#xff0c;不检查是否存在 或 DROP…

力(FFT,acwing2313)

题目路径&#xff1a; https://www.acwing.com/problem/content/2315/ 思路&#xff1a;

Python可执行文件的转换

当开发者向普通用户分享程序时&#xff0c;为了方便用户在未安装Python环境的情况 下能够正常运行&#xff0c;需要将开发好的程序进行打包&#xff0c;转换成用户可运行的文件类 型。本节将介绍在Windows和Linux两种系统下&#xff0c;将Python类型的文件转换成可执 行文件的方…

kotlin $ (字符串模版)的使用

$ 在kotlin 中当做字符串模版使用&#xff0c;作用就是在字符串里面识别自己定义的字符 例如打印一个字符 这个时候编译就提示我们使用字符串模版的是个 $ 的作用就是识别字符串里面的i 字数有点少了&#xff0c;在写一个demo private fun String.appendArchive(): String …

云手机与实体手机的对比

在数字化时代&#xff0c;云手机作为一种虚拟手机在云端服务器上运行&#xff0c;与传统的实体手机相比存在诸多差异。让我们深入探讨云手机与实体手机之间的区别&#xff0c;以便更好地了解它们的特点和优势。 外观上的差异 实体手机具有实际的外观和重量&#xff0c;占据一定…

编译安装Nginx和使用五种算法实现Nginx反向代理负载均衡

目录 Ubuntu中安装Nginx 概念介绍 负载均衡 几种负载均衡算法 反向代理 环境规划 配置反向代理 加权负载均衡&#xff08;Weighted Load Balancing&#xff09; 轮询&#xff08;Round Robin&#xff09; IP 哈希&#xff08;IP Hash&#xff09; 最少连接&#xff…

多维时序 | Matlab实现EVO-TCN-Multihead-Attention能量谷算法优化时间卷积网络结合多头注意力机制多变量时间序列预测

多维时序 | Matlab实现EVO-TCN-Multihead-Attention能量谷算法优化时间卷积网络结合多头注意力机制多变量时间序列预测 目录 多维时序 | Matlab实现EVO-TCN-Multihead-Attention能量谷算法优化时间卷积网络结合多头注意力机制多变量时间序列预测效果一览基本介绍程序设计参考资…

Spring5系列学习文章分享---第四篇(JdbcTemplate+概念配置+增删改查数据+批量操作 )

目录 JdbcTemplateJdbcTemplate&#xff08;概念和准备&#xff09;JdbcTemplate 操作数据库&#xff08;新增update&#xff09;JdbcTemplate 操作数据库&#xff08;修改和删除update&#xff09;JdbcTemplate 操作数据库&#xff08;查询返回某个值queryForObject&#xff0…

shopee的AI学习之路——GPTs通过AdInteli 广告变现

GPTs|AdInteli 广告变现 一、什么是 AdInteli AdIntelli 是一个旨在为生成 GPTs 接入广告并实现变现的平台。它连接了全球最大的广告联盟&#xff0c;允许广告商进行竞价&#xff0c;确保展示最有价值的广告。AdIntelli 采用 AI 驱动的收入生成技术&#xff0c;优化广告选择。…

《游戏-03_3D-开发》之—新输入系统人物移动攻击连击

本次修改unity的新输入输出系统。本次修改unity需要重启&#xff0c;请先保存项目&#xff0c; 点击加号起名为MyCtrl&#xff0c; 点击加号设置为一轴的&#xff0c; 继续设置W键&#xff0c; 保存 生成自动脚本&#xff0c; 修改MyPlayer代码&#xff1a; using UnityEngine;…

华为产业链之车载激光雷达

一、智能汽车 NOA 加快普及&#xff0c;L3 上路利好智能感知硬件 1、感知层是 ADAS 最重要的一环 先进驾驶辅助系统 &#xff08;ADAS&#xff0c; Advanced driver-assistance system&#xff09;分“感知层、决策层、执行层”三个层级&#xff0c;其中感知层是最重要的一环…