科林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++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码

《Java中Date、LocalDate、LocalDateTime、LocalTime、时间戳之间的相互转换代码》:本文主要介绍Java中日期时间转换的多种方法,包括将Date转换为LocalD... 目录一、Date转LocalDateTime二、Date转LocalDate三、LocalDateTim

C++ vector的常见用法超详细讲解

《C++vector的常见用法超详细讲解》:本文主要介绍C++vector的常见用法,包括C++中vector容器的定义、初始化方法、访问元素、常用函数及其时间复杂度,通过代码介绍的非常详细,... 目录1、vector的定义2、vector常用初始化方法1、使编程用花括号直接赋值2、使用圆括号赋值3、ve

如何高效移除C++关联容器中的元素

《如何高效移除C++关联容器中的元素》关联容器和顺序容器有着很大不同,关联容器中的元素是按照关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,本文介绍了如何高效移除C+... 目录一、简介二、移除给定位置的元素三、移除与特定键值等价的元素四、移除满足特android定条件的元

Python获取C++中返回的char*字段的两种思路

《Python获取C++中返回的char*字段的两种思路》有时候需要获取C++函数中返回来的不定长的char*字符串,本文小编为大家找到了两种解决问题的思路,感兴趣的小伙伴可以跟随小编一起学习一下... 有时候需要获取C++函数中返回来的不定长的char*字符串,目前我找到两种解决问题的思路,具体实现如下:

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

golang获取当前时间、时间戳和时间字符串及它们之间的相互转换方法

《golang获取当前时间、时间戳和时间字符串及它们之间的相互转换方法》:本文主要介绍golang获取当前时间、时间戳和时间字符串及它们之间的相互转换,本文通过实例代码给大家介绍的非常详细,感兴趣... 目录1、获取当前时间2、获取当前时间戳3、获取当前时间的字符串格式4、它们之间的相互转化上篇文章给大家介

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

C/C++错误信息处理的常见方法及函数

《C/C++错误信息处理的常见方法及函数》C/C++是两种广泛使用的编程语言,特别是在系统编程、嵌入式开发以及高性能计算领域,:本文主要介绍C/C++错误信息处理的常见方法及函数,文中通过代码介绍... 目录前言1. errno 和 perror()示例:2. strerror()示例:3. perror(

C++变换迭代器使用方法小结

《C++变换迭代器使用方法小结》本文主要介绍了C++变换迭代器使用方法小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录1、源码2、代码解析代码解析:transform_iterator1. transform_iterat