本文主要是介绍科林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 类之间的关系的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!