科林C++_4 类之间的关系

2024-02-20 23:20
文章标签 c++ 关系 之间 科林

本文主要是介绍科林C++_4 类之间的关系,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一. 类之间的横向关系

1.1 组合(复合)composition

部分与整体,包含与被包含,相同生命周期

class CHand{};
class CPeople{CHand m_hand;
};

1.2 依赖 dependency

A用B,A依赖B

class CComputer{};
class CPeople{void Code(CComputer *pc){}
};

1.3 关联 association

不是从属关系,而是平等关系,可以拥有对方,但不可以占有对方

class CFriend{};
class CPeople{CFriend *m_pFri;
};

1.4 聚合 Aggregate

多个对象聚合成一个整体,一种弱从属关系

class CPeople{};
class CFamoly{CPeople* m_pFamily[10];
};

 二. 继承

UML之类图关系(继承、实现、依赖、关联、聚合、组合)_uml 继承-CSDN博客

  • 当多个类中公共的成员时,此时就可以将这些公共成员放在一个类中,我们将这个类称之为基类,而需要用到这个类中成员的类我们就称之为子类(派生类),子类中可以定义不同于父类的成员。它们之间的关系就成为继承。

2.1 继承的概念

子类继承父类,可以直接使用父类的成员,也会包含父类的成员,子类也是对父类的延续和扩展

#include<iostream>using namespace std;//父类(基类)
class CFather {
public:int m_fa;void funFa() {cout << "funfather" << endl;}
};//子类(派生类) 子类类名 :继承方式 继承的父类
class CSon :public CFather {
public:int m_son;CSon() {m_fa = 2;m_son = 1;}void funSon() {cout << m_fa << endl;funFa();}
};int main() {CSon son;son.funSon();cout << sizeof(CFather) << ' ' << sizeof(CSon) << endl;cout << &son << ' ' << &son.m_fa << " " << &son.m_son << endl;
}

子类和父类出现了同名的成员,优先使用子类的,如果想使用父类成员,需要使用父类类名作用域显式的指定.

	son.fun();	//	son.CSon::fun();cout << son.m_aa << endl;son.CFather::fun();cout << son.CFather::m_aa << endl;

2.2 继承下的构造和析构

2.2.1 构造

#include <iostream>using namespace std;//在继承下:定义子类对象,执行构造顺序:父类->子类class CFather {
public:int m_a;CFather(int a):m_a(a){}
};class CSon : public CFather {
public:int m_b;//成员的初始化应该通过构造CSon(int a,int b): m_b(b),CFather(a){//m_a = 1;	父类的变量可以赋值操作,但是不可以放到参数列表中初始化}
};int main() {CSon son(1, 2);cout << son.m_a << ' ' << son.m_b << endl;
}

若没有在子类中调用父类的构造函数,编译器会默认调用无参的构造函数

	CSon() :m_b (1),tst(),CFather(7){}

初始化参数列表 初始化 成员的顺序 取决于 声明的先后顺序。在继承下,父类成员在最前面(声明在最前)。父类之间的顺序取决于继承的顺序

成员声明的先后顺序,和成员在内存布局中的先后顺序一致

2.2.2 析构

析构的执行顺序:先子类->父类(和构造的顺序正好相反)

析构:定义子类对象的前提下,在子类对象生命周期结束时,优先跳转到子类的析构,执行完子类析构,开始回收对象,对象中包含了CTest CFather,按照声明的相反顺序,依次执行析构(先CTest析构,再CTest对象,最后CFather析构,再CFather对象)

2.3 继承方式

继承方式:描述了父类成员在子类中表现的属性,和访问修饰符共同决定了父类的成员在子类中所能使用的一个范围

2.3.1 公有继承

class CFather {
public:int m_pub;
protected:int m_pro;
private:int m_pri;
public:CFather():m_pub(0),m_pro(1),m_pri(2){}
};class CSon :public CFather {
public:void funPublic() {cout << m_pub << endl;cout << m_pro << endl;//cout << m_pri << endl;	//继承了但是不能直接使用}
};

public:类内类外都能使用

protected:类内和继承的类中可以使用 

private:只能在类内使用

公有继承中父类成员属性,在子类中不变,无法访问 private

2.3.2 保护继承

父类中 public 的性质在子类中变成 protected,protected不变,无法访问 private

2.3.3 私有继承

父类中public 和  protected 在子类中变成 private,无法访问 private

2.4 继承的优点

在程序中,将一些功能相似的类中的公共的代码,提取出来形成一个类,这个类即是一个父类,子类继承后可直接使用其成员,提高了代码的复用性,扩展性,灵活性。

2.5 隐藏

子类和父类中出现同名的成员,此时不在同一作用域下,所以不是函数的重载。此时优先匹配子类,称之为隐藏。

父类指针,在继承下,可以不通过强转,直接指向子类对象。保证了子类对象可以成功的调用父类的函数。
子类指针,想不通过强转指向父类是非法的。

CFather* const pthis = &t;    //合法
CTest* pt=new CFather;    //非法

总其原因还是防止指针越界,安全性的问题。

我们知道指针的范围取决与指针的类型,父类指针类型怎么指也不会超子类对象,总是保持在父类的一部分。而子类对象指针的范围要>=父类对象,超过父类的地址空间并没有申请,很容易发生错误。

2.6 父类指针指向子类对象

优点:父类的指针可以统一多种子类类型,提高代码的复用性、扩展性

弊端:父类的指针不能直接调用子类的函数

void fun(CPeople* pPeo) {pPeo->cost(10);//pPeo->eat()pPeo->walk();
}

2.7 类成员函数指针

#include <iostream>using namespace std;void fun(int a) {cout << a << endl;
}class CTest {
public:void fun(/* CTest * const this */int a) {cout << "class" << a << endl;}
};
/*函数与类成员函数的区别:1、作用域不同2、类成员函数,存在隐藏的this指针
*/int main() {fun(1);void (*p)(int) = &fun;	//定义一个函数指针指向函数(*p)(2);typedef void (*P_FUN)(int);P_FUN p1 = &fun;p1(3);using P_FUNC = void (*)(int);P_FUNC p2 = &fun;p2(4);//类成员函数指针// ::* 是C++中提供的不可分割的整体操作符,表示指针指向类成员函数void(CTest ::* Cp)(int) = &CTest::fun;	//void(*Cp)(int) = &CTest::fun; //ERROR:"void (CTest::*)(int a)" 类型的值不能用于初始化 "void (*)(int)" 类型的实体	CTest t;// .* 调用类成员函数指针,把这个对象作为参数传递给类成员函数的this(t .* Cp)(5);CTest* tt = &t;tt->fun(6);
}

2.8 类成员函数指针解决问题

#include <iostream>using namespace std;class CFather {};class CSon :public CFather {
public:void fun() {cout << "CSon" << endl;}
};int main() {CFather* pfa = new CSon;//类成员函数指针/*void(CSon:: * p_fun)() = &CSon::fun;(pfa ->* p_fun)();	//ERROR:指向成员的指针选择类类型是不兼容的("CFather" 和 "CSon")*/void(CFather:: * p_fun)() = (void(CFather::*)()) & CSon::fun;(pfa->*p_fun)();	//CSon::fun}

通过强转,解决父类不能直接指向子类函数的问题(手写多态

#include <iostream>
using namespace std;class CPeople {
public:int m_money;CPeople(int v):m_money(v){}void cost(int n) {m_money -= n;}void walk() {cout << "walk" << endl;}
};class Cwhite :public CPeople{
public:Cwhite(int v):CPeople(v){}void eat() {cout << "apple" << endl;}
};class Cyellow :public CPeople {
public:Cyellow(int v):CPeople(v){}void eat() {cout << "melon" << endl;}
};class Cblack :public CPeople {
public:Cblack(int v) :CPeople(v) {}void eat() {cout << "banana" << endl;}
};using P_FUN = void (CPeople::*)();void fun(CPeople* pPeo,P_FUN p_fun) {pPeo->cost(10);//pPeo->eat()    //父类指针没法直接调用子类的函数//((Cyellow*)pPeo)->eat();    //不具有通用性(pPeo->*p_fun)();	//通过类成员函数指针解决问题pPeo->walk();
}class CFamily {
public:CPeople* arrPeo[2];CFamily() {arrPeo[0] = new Cyellow(1);arrPeo[1] = new Cwhite(2);}
};int main() {fun(new Cyellow(1), (P_FUN)&Cyellow::eat);
}

三. 多态

3.1 多态

多态是面向对象编程中的一个重要概念,它指的是通过一个基类指针或引用调用一个虚函数时,会根据具体对象的类型来调用该虚函数的不同实现。 在多态中,相同的操作可以作用于不同的对象,而具体执行的操作则取决于对象的类型和特性。 在C++中,通过将基类中的成员函数声明为虚函数,即可实现多态

多态:相同的行为方式导致了不同的行为结果,同一行语句展现了多种不同的表现形式,即:多态性。

c++多态:父类的指针可以指向任何继承于该类的子类对象,多种子类具有多种形态,都由父类指针同一管理。父类指针,具有了多种形态

多态条件:

1、在继承的基础上,父类指针指向子类对象

2、父类中存在虚函数,子类要重写父类的虚函数(override可以检测子类是否重写了父类虚函数)

重写:在虚函数的前提下,子类函数和父类的虚函数的函数名、参数列表都一样

#include <iostream>using namespace std;class CFather {
public:virtual void fun() {	//虚函数,virtual定义虚函数的关键字cout << "CFather" << endl;}
};
class CSon:public CFather {
public:void fun() {cout << "CSON" << endl;}
};int main() {CFather* pFa = new CSon;pFa->fun();	//CSon::fun
}

3.2 虚函数

__vfptr:虚函数指针,类型 二级指针 void**,属于对象。在每个对象的内存空间前,会申请指针大小的空间。

若类中存在虚函数,在定义对象时,编译器会默认添加到对象中,每个对象都会有自己的一份虚函数指针。

多个对象中虚函数指针,指向了函数指针数组

vfptable:虚函数列表,本质上是函数指针数组,每一个元素是当前类中的虚函数的地址,非虚函数不会存在表中。
属于类,只会存在一份,编译期存在,多个对象共享这个虚函数列表。
一般来说,编译器会按类中声明的顺序把虚函数放在虚函数列表中。

#include<iostream>
using namespace std;class CTest {
public:int m_a;public:CTest():m_a(0){}virtual void fun() { cout << "fun" << endl; }virtual void fun2(){}void funcom(){}
};int main() {cout << sizeof(CTest) << endl;	//4 8CTest t;cout << &t << endl;cout << &t.m_a << endl;CTest t2;return 0;
}

流程: 定义对象找到对象前面的虚函数指针,通过__vfptr 间接引用,下标定位,找到虚函数的地址,再通过这个地址,调用虚函数 

	CTest *ptst=nullptr;ptst->funcom();//ptst->fun();	//ERROR:读取访问权限冲突//空指针对象不能直接调用虚函数ptst = new CTest;/**(int*)ptst	//取到内存中的前4个字节-->虚函数指针__vfptr((int**)__vfptr)[0] == int*		//__vfptr是void**,没有类型没法间接引用,强转为int**然后[]后是int*(void(*)())(((int**)__vfptr)[0]	== int*)	//本质是函数指针,但是代码是整型指针被调用*/void (*p_fun1)() = (void(*)())(((int**)(*(int*)ptst))[0]);void (*p_fun2)() = (void(*)())(((int**)(*(int*)ptst))[1]);(*p_fun1)();(*p_fun2)();

运行时多态:在程序运行时,通过指针和表去寻找

虚函数指针是属于对象,在运行时确定

虚函数列表在编译期就确定了

虚函数 和 普通函数的区别:
1. 调用流程不同:如果类中定义了虚函数,会增加额外的内存空间(每个对象中虚函数指针,和虚函数列表(数组)), 但函数本身空间不会增加。虚函数改变了 调用流程,虚函数调用流程更复杂
2.  效率不同, 虚函数调用流程更复杂 ,消耗的时间增多
3. 使用场景不同,虚函数就是为了多态而出现,在单个类中定义意义不大,普通的函数不能够实现运行时多态。

例题:

void* (*fun(double*, int(&)[2]))(int*, void(*)(char, int));

返回值是函数指针,参数是double*和参数列表

3.3 继承下的虚函数

        虚函数指针:在定义对象的内存空间中
        虚函数列表属于类。

#include <iostream>
using namespace std;class CFather {
public:virtual void fun1() { cout << "CF1" << endl; }virtual void fun2() { cout << "CF2" << endl; }
};class CSon:public CFather {
public:virtual void fun1() { cout << "CS1" << endl; }virtual void fun3() { cout << "CS2" << endl; }
};int main() {CFather* pFa = new CSon;pFa->fun1();	//CS1pFa->fun2();	//CF2CSon son;CFather fa;
}

此时,程序中有两个虚函数列表。 

虚函数指向哪个类的虚函数列表取决于定义的哪个对象,和指向这个对象的指针类型无关。

此时,注意到 CSon 的虚函数列表中有两个虚函数,分别为 CSon::fun1 和 CFather::fun2

继承下的多态,子类的虚函数列表:
1、继承 不但继承了父类的成员 也会继承父类的虚函数列表  子类 CFather::fun1  CFather::fun2
2、检查子类中是否有重写父类的虚函数 有:原地覆盖  如果没有重写父类的 则父类的虚函数仍保留   子类: CSon::fun1,CFather::fun2
3、如果子类中存在 单独的虚函数 则会按照声明的先后顺序 依次添加到虚函数列表结尾  子类   :  CSon::fun1,CFather::fun2,CSon::fun3

通过监视窗口可以查看

继承多态下 虚函数调用流程  :父类指针指向 定义的子类 在子类对象内存空间前会找到虚函数指针(这个指针指向的是子类的虚函数列表)通过指针间接引用  找到子类虚函数列表 通过下标定位到具体的虚函数 获取地址 通过地址调用

例题:

class A {virtual void fun1(){}virtual void fun2(){}virtual void fun4(){}
};
class B :public A {virtual void fun1(int){}virtual void fun2()const{}virtual void fun0(){}virtual void fun4(){}
};

A的虚函数列表:A::fun1() A::fun2() A::fun4()
B的虚函数列表:A::fun1() A::fun2() B:fun4() B::fun1(int) B::fun2()const B::fun0()

 3.4 虚析构

#include <iostream>using namespace std;class CFather {
public:CFather() { cout << "CF" << endl; }~CFather() { cout << "~CF" << endl; }
};
class CSon:public CFather {
public:CSon() { cout << "CS" << endl; }~CSon() { cout << "~CS" << endl; }
};int main() {CFather* pfa = new CSon;	//CF CS ~CFdelete pfa;//delete(CSon*)pfa;pfa = nullptr;
}

发现子类的析构并没有执行

在继承多态下,父类指针指向子类对象,在回收空间时,只会调用父类的析构函数,有可能会导致子类额外申请的空间没有被回收,导致内存泄漏

    原因: delete 在回收对象空间时,调用析构取决于 传递指针的类型
    解决:把父类的析构变为 虚析构,子类的析构函数也会变为一个虚析构,通过多态,最终结果执行的是子类的析构函数

class CFather {
public:CFather() { cout << "CF" << endl; }virtual ~CFather() { cout << "~CF" << endl; }
};
class CSon:public CFather {
public:CSon() { cout << "CS" << endl; }~CSon() { cout << "~CS" << endl; }	//子类的函数一旦重写了父类的虚函数,子类的虚函数关键字,可以省略不写,也会认定为虚函数
};

为什么函数名不一样但是算作函数的重写呢?

在C++编译器中,会对函数名重新拼接,此时编译器看到的父类和子类的析构函数是一样的。

 3.5 纯虚函数

抽象类:包含纯虚函数的类,不能实例化对象

#include <iostream>using namespace std;class CAnimal {	
public:virtual void eat() = 0;	//声明纯虚函数,在本类中可以不用定义
};class CDog :public CAnimal {//在子类中,需要重写父类的所有纯虚函数,否则仍无法实例化对象virtual void eat() {	cout << "eat bones" << endl;}
};int main() {//CAnimal a;	//ERROR:不允许使用抽象类类型的对象CAnimal* p = new CDog;p->eat();
}

常见错误:子类没有重写父类的纯虚函数,继承到子类中,子类也成了抽象类

多态缺点:

1、空间问题:虚函数指针在每个对象中都定义一份虚函数列表。子类继承父类的虚函数列表,大小包含父类的虚函数列表

2、效率问题:虚函数调用流程复杂,增加调用的时间,效率低
3、安全性问题:如果一个函数为私有函数,不用把他变为私有的函数,私有的函数,就不要变为虚函数

这篇关于科林C++_4 类之间的关系的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/729850

相关文章

C++实现回文串判断的两种高效方法

《C++实现回文串判断的两种高效方法》文章介绍了两种判断回文串的方法:解法一通过创建新字符串来处理,解法二在原字符串上直接筛选判断,两种方法都使用了双指针法,文中通过代码示例讲解的非常详细,需要的朋友... 目录一、问题描述示例二、解法一:将字母数字连接到新的 string思路代码实现代码解释复杂度分析三、

Java对象和JSON字符串之间的转换方法(全网最清晰)

《Java对象和JSON字符串之间的转换方法(全网最清晰)》:本文主要介绍如何在Java中使用Jackson库将对象转换为JSON字符串,并提供了一个简单的工具类示例,该工具类支持基本的转换功能,... 目录前言1. 引入 Jackson 依赖2. 创建 jsON 工具类3. 使用示例转换 Java 对象为

python安装whl包并解决依赖关系的实现

《python安装whl包并解决依赖关系的实现》本文主要介绍了python安装whl包并解决依赖关系的实现,文中通过图文示例介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面... 目录一、什么是whl文件?二、我们为什么需要使用whl文件来安装python库?三、我们应该去哪儿下

C++一个数组赋值给另一个数组方式

《C++一个数组赋值给另一个数组方式》文章介绍了三种在C++中将一个数组赋值给另一个数组的方法:使用循环逐个元素赋值、使用标准库函数std::copy或std::memcpy以及使用标准库容器,每种方... 目录C++一个数组赋值给另一个数组循环遍历赋值使用标准库中的函数 std::copy 或 std::

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

C++初始化数组的几种常见方法(简单易懂)

《C++初始化数组的几种常见方法(简单易懂)》本文介绍了C++中数组的初始化方法,包括一维数组和二维数组的初始化,以及用new动态初始化数组,在C++11及以上版本中,还提供了使用std::array... 目录1、初始化一维数组1.1、使用列表初始化(推荐方式)1.2、初始化部分列表1.3、使用std::

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

java父子线程之间实现共享传递数据

《java父子线程之间实现共享传递数据》本文介绍了Java中父子线程间共享传递数据的几种方法,包括ThreadLocal变量、并发集合和内存队列或消息队列,并提醒注意并发安全问题... 目录通过 ThreadLocal 变量共享数据通过并发集合共享数据通过内存队列或消息队列共享数据注意并发安全问题总结在 J

Java文件与Base64之间的转化方式

《Java文件与Base64之间的转化方式》这篇文章介绍了如何使用Java将文件(如图片、视频)转换为Base64编码,以及如何将Base64编码转换回文件,通过提供具体的工具类实现,作者希望帮助读者... 目录Java文件与Base64之间的转化1、文件转Base64工具类2、Base64转文件工具类3、