科林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++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

day-51 合并零之间的节点

思路 直接遍历链表即可,遇到val=0跳过,val非零则加在一起,最后返回即可 解题过程 返回链表可以有头结点,方便插入,返回head.next Code /*** Definition for singly-linked list.* public class ListNode {* int val;* ListNode next;* ListNode() {}*

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

POJ1269 判断2条直线的位置关系

题目大意:给两个点能够确定一条直线,题目给出两条直线(由4个点确定),要求判断出这两条直线的关系:平行,同线,相交。如果相交还要求出交点坐标。 解题思路: 先判断两条直线p1p2, q1q2是否共线, 如果不是,再判断 直线 是否平行, 如果还不是, 则两直线相交。  判断共线:  p1p2q1 共线 且 p1p2q2 共线 ,共线用叉乘为 0  来判断,  判断 平行:  p1p

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现