十二、指针和引用(二)1、指针和数组的关系

1)思考

​假设你要设计一种编程语言,你要如何实现数组呢?思考之前请先牢记:数组在内存中是连续的,维度由低到高(大部分操作系统下)。

2)汇编分析数组如何实现

//C++代码#include int main(){    int a[5]{};    int* ptrA{ &a[0] };    *ptrA = 5;    //通过指针设置数组的值         a[0] = 5;     //通过数组下标设置数组的值    a[1] = 5;}//上述代码汇编分析int a[5]{};    int* ptrA{ &a[0] };00A51840  mov         eax,4                     //eax=400A51845  imul        ecx,eax,0                 //imul为乘法,即ecx=eax*0=000A51848  lea         edx,[ebp+ecx-1Ch]         //edx=ebp+ecx-1Ch=ebp-1Ch00A5184C  mov         dword ptr [ebp-28h],edx   //ptr表示指针,即[ebp-28h]=ebp-1Ch, []中的表示地址    *ptrA = 5;                                   00A5184F  mov         eax,dword ptr [ebp-28h]    //eax=[ebp-28h] 即eax=ebp-1Ch00A51852  mov         dword ptr [eax],5         //[eax]=5,即[ebp-1Ch]=5,即a[0]=5    a[0] = 5;00A51858  mov         eax,4                    //eax=400A5185D  imul        ecx,eax,0                //ecx=eax*0=000A51860  mov         dword ptr [ebp+eax-1Ch],5    //[ebp+eax-1Ch]=5,即[ebp-1Ch]=5    a[1] = 5;00A51868  mov         eax,4                    //eax=400A5186D  shl         eax,0                    //shl为左位移,即eax<<0,即eax=eax*2^0即eax=eax*1=400A51870  mov         dword ptr a[ebp+eax-1Ch],5   //[a[4]]=5}

总结数组实现:使用数组第一个元素的起始地址,加上访问第N个元素*偏移量(即类型的大小)来达到访问数组中的每一个元素 。数组a的地址本质上和a[0]的地址相等。

//数组a的地址本质上和a[0]的地址相等。#include int main(){    int a[5]{};    int* ptrA{ &a[0] };    *ptrA = 5;    //通过指针设置数组的值        std::cout << "数组a的地址为:" << a << std::endl;    std::cout << "a[0]的地址为: " << &a[0] << std::endl;}//注:因数组a的地址和a[0]的地址一样,所以ptrA和a在本质上一样,所以在定义指针数组时,可以直接写为int* ptrA {a}; ptrA[1]=1002;注:本质上来说,数组名就是一个指针。但是sizeof()函数处理数组时,还是当作数组类型处理,所以通过sizeof()查看内存占用大小是和指针是不同的

3)结论:指针可以当数组用

​数组的底层实现是利用了指针,因此,我们甚至可以大胆的说,起始C/C++里根本不存在什么数组,所谓的数组不过是利用指针玩的小把戏而已。

​从原理上来讲,指针和数组是同一个方法的不同表达,而数组名本身就是一个指针,数组元素只是这个指针按照一定的偏移量后,对应的内存区域里的内容。

​因此我们尝试一下按照数组的使用方式来使用一下指针,看看会发生什么事情。

#include int main(){int a[5]{};int* ptrA{ a };*ptrA = 5;a[0] = 5;a[1] = 50001;a[2] = 5; std::cout << a << std::endl;     //010FF9F8std::cout << &a[0] << std::endl;  //010ff9F8std::cout << ptrA[1] << std::endl; //50001std::cout << a[1] << std::endl;  //50001std::cout << sizeof(a) << std::endl;     //20std::cout << sizeof(ptrA) << std::endl;  //4}

4)多维数组思考

​数组的本质是连续的内存区域,所以我们可以大胆设想,所谓的多维数组起始不存在,多维只是我们人类为了方便我们自己的理解创造出来的逻辑方法。

!

注:数组指针+1表示1*数据类型的大小,逻辑是几,则每次+1步跳跳几

5)数组在内存中的表现形式

​不管是int a[2][5]还是int a[5][2]在内存中的本质都是一样的,都是一块连续的内存空间,并且占用的地址也都一样。

#include int main(){int test[2][5]{{1001,1002,1003,1004,1005},{2001,2002,2003,2004,2005}    //多维数组在内存中的人为逻辑排序};//1001, 1002, 1003, 1004, 1005,2001,2002,2003,2004,2005   内存中多维数组的真实排序int* ptrTest{ (int*)test };  //可以进行强制类型转化,因为test本质是一个地址,里面保存中test[0][0]的地址//输入test数组最后一个元素std::cout << "通过数组下标获取test最后一个元素:" << test[1][4] << std::endl;std::cout << "通过指针获取test最后一个元素:    " << ptrTest[9] << std::endl;}//test是一个数组指针

6)数组指针和指针数组

①指针数组:指针数组本质上来讲是一个数组,数组的内容是指针,如int* ptrA[5];

②数组指针:数组指针本质上来讲是一个指针,只是这个指针用来处理数组问题

//数组指针定义int (*ptrB)[5];
//数组指针使用#include int main(){int test[2][5]{{1001,1002,1003,1004,1005},{2001,2002,2003,2004,2005}};int* ptestA[5];  //指针数组,本质上是一个数组,即将数组中存放5个地址int(*pTestA)[5]{ test }; //数组指针(本质上是一个指针),表示每一行按照5个元素断开,即该指针处理每一行5个数据的数组std::cout << test[1][4] << std::endl;        //2005std::cout << pTestA[0][1] << std::endl;      //1002std::cout << sizeof(pTestA) << std::endl;      //4, 数组指针是一个指针,指针大小为4std::cout << pTestA << std::endl;pTestA = pTestA + 1;               //指针加1,表示加对应数组类型的大小,即+1*数据类型大小*数组指针元素个数,即1*4*5=20std::cout << pTestA << std::endl;}

2、动态内存分配

1)项目设计

​麟江湖副本系统 :玩家进入麟江湖副本系统后,地图上会随机刷新一批怪物,数量不等,从100-10000个都有可能,我们如何应对这种不确定数量对象的问题?

2)C语言中的动态内存分配malloc

//C语言中的动态内存分配malloc语法void* malloc(size_t size);  //malloc将为用户分为size_t字节个内存,并且返回内存分配的地址,如果分配失败,那么返回0//示例int* pa=(int*)malloc(4);  //pa是分配好的内存的地址,4是要分配的内存的大小,如果内存分配失败,那么pa=0注:size_t是一个通过typedef自定义的数据类型,相当于无符号整数。可通过邮件函数-转到定义查看typedef unsigned int     size_t;

malloc的简单使用:

//malloc的简单使用#include int main(){unsigned x;std::cout <> x;//malloc(x*4); //放什么类型的变量,就*什么类型的变量大小,如int类型*4。可直接使用seizeof(int)计算int* p = (int*)malloc(sizeof(int) * x);   //void*  是空类型的指针,即没有任何类型,无法将其赋值给int*类型的指针,因此需要强制类型转化//内存不一定分配成功if (p == nullptr)  //nullptr就相当于0,但是推荐使用nullptr,C语言中只能使用0{std::cout << "内存分配失败!!!" << std::endl;}else      //内存分配成功后,再执行相关操作{p[0] = 952;p[1] = 953;p[2] = p[0] * p[1];std::cout<<"p[0]:" << p[0] << " p[1]:" << p[1] << " p[2]:" << p[2] << std::endl;}}

3)C语言中的动态内存分配callloc

//动态内存分配函数callloc语法void* calloc(size_t count,size_t size);  //calloc将为用户分配count乘size_t字节个内存,并且返回内存分配的地址,如果分配失败,那么返回0//calloc示例int* pa=(int*)calloc(1,4); //pa是分配好的内存地址,1是要分配的元素个数,4是要分配的每个元素个数的大小注:calloc运行效率比malloc的低,但是比较方便,会将分配好的内存空间设置为0,且不需要手动计算分配的内存空间的大小。calloc会默认将分配好的内存区域设置为0.
//动态内存分配callloc示例#include int main(){unsigned x;std::cout <> x;//calloc直接说明需要申请的内存个数,及每个内存的大小int* p = (int*)calloc(x, sizeof(int));//calloc会将自动分配的内存空间的值设置为:0std::cout << "calloc会将自动分配的内存空间的值设置为:" << p[0] << std::endl;if (p == nullptr){std::cout << "内存分配失败!!!";}else {p[0] = 952;p[1] = 953;p[2] = p[0] * p[1];std::cout << "p[0]:" << p[0] << " p[1]:" << p[1] << " p[2]:" << p[2] << std::endl;}}

3)C语言中的动态内存分配realloc

//动态内存分配realloc语法void* realloc(void* _Blocak,size_t _szie); //realloc将为用户重新分配内存,_Block是用户已经分配好的内存,Size是要求重新分配的大小,函数返回重新分配后的内存//示例int* pa=(int*)malloc(4);pa=(int*)realloc(pa,8);  //pa是重新分配后的内存地址,8是重新分配后的大小。若分配失败,pa=0注:重新分配内存空间后,原先的值还在,不丢失。内存地址有可能变化,有可能不变
#include int main(){unsigned x;std::cout <> x;int* p = (int*)malloc(x * sizeof(int));if (p == nullptr){std::cout << "分配内存失败";}else{p[0] = 123;p[1] = 456;p[2] = p[0] * p[1];std::cout << "p[0]:" << p[0] << " p[1]:" << p[1] << " p[2]:" << p[2] << std::endl;}std::cout <> x;p = (int*)realloc(p, x);  //注:重新分配内存空间后,原先的值还在。std::cout << "p[0]:" << p[0] << " p[1]:" << p[1] << " p[2]:" << p[2] << std::endl;free(p); p = 0;    //释放内存空间并同时清0,}

4)C语言中的动态内存分配free

//释放内存空间free语法void free(void* _Blocak);//示例int* pa=(int*)malloc(4);free(pa);         //pa表示所占用的内存被释放,释放内存不可能失败pa=0;             //释放以后要将指针清零,防止出现悬挂指针

5)C++的动态内存分配

C++内存分配是基于C语言的内存分配实现的

//C++的动态内存分配语法数据类型* 指针变量名称=new 数据类型;数据类型* 指针变量名称=new 数据类型[数量];//示例int* pa = new int;int* pa = new int[5];          //分配一段能够存放5个int变量类型的内存空间注:分配内存失败,pa返回0
//C++的动态内存分配语法#include int main(){unsigned x;std::cout <> x;int* pa = new int[x];   //分配存放x个int类型大小的数据if (pa == nullptr){std::cout << "分配内存失败";}else{pa[0] = 123;pa[1] = 456;pa[2] = 789;std::cout << "pa[0]:" << pa[0] << std::endl;std::cout << "pa[1]:" << pa[1] << std::endl;std::cout << "pa[2]:" << pa[2] << std::endl;}}
//反汇编       if (void* const block = malloc(size))006227F4  mov         eax,dword ptr [size]  006227F7  push        eax  006227F8  call        _malloc (0621069h)  006227FD  add         esp,4  00622800  mov         dword ptr [ebp-4],eax  00622803  cmp         dword ptr [ebp-4],0  00622807  je          operator new+1Eh (062280Eh)             注:通过查看反汇编,可知new底层是通过malloc实现的

6)C++内存释放free

//C++内存释放free语法delete 指针;      //释放用new分配的内存delete[] 指针;    //释放用new 数据类型[]分配的内存注:new和delete要成对出现
#include int main(){unsigned x;std::cout <> x;int* p = new int[x];int* mp = new int;std::cout << p << std::endl;;std::cout << p[0]<<std::endl;delete mp;     //释放内存,如果只分配了一个,不需要加[]delete[] p;   //释放内存,如果分配了多个内存空间,需要加[],}

3、动态内存分配的风险

1)悬挂指针问题(野指针)

注:指针申请并释放后,仍然去使用这一块内存空间,有时候会出错,有时候不会出错。

//通过malloc申请的内存,释放以后,继续使用不会报错#include int main(){int* p = (int*)malloc(10 * sizeof(int));int* pold = p;p[0] = 255;free(p);p[0] = 500;    //虽然p已经释放了,但是还是去用,//free(pold); //重复释放会出错std::cout << p[2] << std::endl;;    //-572665370}

//通过new申请的内存,delete释放以后,继续使用会报错#include int main(){int* p = new int[10];p[0] = 255;delete[] p;p[0] = 500;    //虽然p已经释放了,但是还是去用,//deblete[] p; //重复释放会出错std::cout << p[2] << std::endl;;    }

//将new申请的内存,再赋值给其他变量,释放以后,继续使用不报错#include int main(){int* p = new int[10];int* pa = p;p[0] = 255;delete[] p;pa[2] = 500;    //虽然p已经释放了,但是还是去用不会报错,但是输出会报错//deblete[] p; //重复释放会出错}

注:指针重复释放一定会出错,且不可以

//指针重复释放一定会出错#include int main(){int* p = new int[10];int* pa = p;delete[] p;//delete[] pa; //指针重复释放一定会出错}

2)内存碎片问题

​频繁的申请和释放小块内存会造成内存碎片,虽然原则上还有内存可以使用,但是实际上由于剩余内存碎片化的存在,使得我们无法分配新的内存,当然,现在也不必太担心这样的情况,因为new和delete背后的算法会尽力规避这个分线,并且现在我们的虚拟内存也足够大,但是如果是嵌入式开发或者要求较高的开发,就要注意这个问题,我们也可以自己来控制我们的内存分配,但这是比较高阶的操作。

3)内存分配的思考

​原则上来说malloc分配的内存可以用delete释放(new的底层是malloc),new分配的内存可以使用free释放,但是实际使用时不允许两者混用,malloc分配的内存一定要用free释放、new分配的内存一定使用delete释放。

4)复制内存memcpy

//复制内存memcpy语法void* memcpy(void* _Dst,const void* _Src,size_t _size);  //memcpy可以将_Src区域的内存复制到_Dst区域,复制的长度为_Size,赋值的单位为字节//示例int a[5]{1001,1002,1003,1004,1005};int *p = new int[5];memcpy(p,a,5*sizeof(int));
//复制内存,将数组中的数据,复制到指针数组中#include int main(){int a[5]{ 1001,1002,1003,1004,1005 };int* p = new int[5];/*法一:for (int i = 0; i < 5; i++){p[i] = a[i];}*/法二memcpy(p, a, 5 * sizeof(int));      //单位为字节for (int j = 0; j < 5; j++){std::cout << p[j]<<std::endl;}}

4)设置内存memset

//设置内存memset语法void* memset(void* _Dst,int val,size_t _Size);  //memset可以将指定内存区域每一个字节的值都设置为val,_Size表示要设置的长度(单位:字节)//示例int* p=new int[100];memset(p,0,100*sizeof(int));注:因每次只能填充一个字节,所以val的范围为0至-1,即0x0至0xff,一般都设置为0或者-1
#include int main(){int* p = new int[100];memset(p, 0, 100 * sizeof(int));      //设置内存,[地址,设置的值,设置的大小],不管设置的大小为多少,只能一个字节一个字节初始化//memset(p, 0xff, 100 * sizeof(int));     //初始化内存为ffffffstd::cout << p[0] << std::endl;;}

4、引用类型

作用:提高代码效率,引用类型必须初始化

//引用语法数据类型& 变量名称{引用对象的名称};  //引用是创建一个变量的引用名称//示例int a{500};int& la{a};    //以后对la的操作就相当于对a的操作,修改la就相当于修改了a注:引用类型一定要进行初始化,并且一旦引用后,引用后的值无法修改
//引用使用#include int main(){int a{ 5250 };//引用谁就等于谁,即将a起了另外一个名字,将a的值复制给la,包括地址。一个变量可以有多个引用int& la{ a };int& la1{ a };int& la2{ a };int& la3{ a };//常量的引用const int b{ 3000 };const int& lb{ b };std::cout << lb << std::endl;  //3000std::cout << &a << std::endl;     //00cBEFF8a++;std::cout << la << std::endl;   //a++就相当于la++std::cout << &la1 << std::endl;   //00cBEFF8std::cout << &la2 << std::endl;std::cout << &la3 << std::endl;}

5、指针练习

​暗网杀手排名系统:暗网是一个不为人知的网络世界,在暗网中有10位臭名昭著的杀手,如果按照臭名值来排名,分别是:

​正序排列:105,98,73,58,32,31,25,22,3,1

​倒序排列:1,3,2,25,31,32,58,73,98,105

​现在我们要给一位新来的杀手插入到这个排名系统中去要求设计一个算法,要求用户输入一个值代表新杀手的臭名值,然后不管是正序排列,还是倒序排列,这个算法都能够正确的将新杀手的臭名值正确的排序到现有排序中去!并且打印出来比如:用户输入97则输出:

105,98,97,73,58,32,31,25,22,3,1或者是1,3,22,25,31,32,58,73,98,105

!

#include int main(){//int a[10]{ 105,98,73,58,32,31,25,22,3,1 };int a[10]{ 1,3,22,25,31,32,58,73,98,105 };int acount = sizeof(a) / sizeof(int);       //计算数组a的元素个数int* anew = new int[acount + 1];         //新的数组int killer;std::cout <> killer;int getIndex{ acount };       //插入的索引位置if (a[0] > a[1]){for (int i = 1; i  a[i]){getIndex = i;break;}}}if (a[0] < a[1]){for (int i = 1; i < acount; i++){if (killer < a[i]){getIndex = i;break;}}}memcpy(anew, a, getIndex * sizeof(int));  //拷贝插入位置之前的数据memcpy(anew + getIndex + 1, a + getIndex, (acount - getIndex)*sizeof(int));  //拷贝插入位置之后的数据anew[getIndex] = killer;for (int i = 0; i <= acount; i++){std::cout << anew[i] << std::endl;}//std::cout << getIndex << std::endl; }

//上述代码优化(实现效果一致)#include int main(){//int a[10]{ 105,98,73,58,32,31,25,22,3,1 };int a[10]{ 1,3,22,25,31,32,58,73,98,105 };int acount = sizeof(a) / sizeof(int);       //计算数组a的元素个数int* anew = new int[acount + 1];         //新的数组int killer;std::cout <> killer;int getIndex{ acount };       //插入的索引位置bool bcase = a[0] > a[1];           //代码优化for (int i = 0; i < acount; i++){if (bcase ^ (killer < a[i])){getIndex = i;break;}}memcpy(anew, a, getIndex * sizeof(int));  //拷贝插入位置之前的数据memcpy(anew + getIndex + 1, a + getIndex, (acount - getIndex) * sizeof(int));  //拷贝插入位置之后的数据anew[getIndex] = killer;for (int i = 0; i <= acount; i++){std::cout << anew[i] << std::endl;}//std::cout << getIndex << std::endl; }/*for(int i=0;ia[1])    {        if(x>a[i])        {            getIndex = i;            break;        }    }    else    {        if(x<a[i])        {            getIndex=i;            break;        }    }}*/

12、理解数组和指针

1)数组和指针

由于数组是利用指针来实现的,因此数组和指针有很多共通的功能,比如指针可以按照数组的方式来访问数据;

数组可以使用sizeof求占用内存大小,这个过程是由编译器实现的,实际运行过程中并不存在!

int a[5];int c = sizeof(a);006E17D8 mov dword ptr [c],14h

2)引用的本质

引用的本质其实就是一种被阉割了的指针,虽然我们取值引用变量得到的是原值的内存地址,但是引用变量也是占用内存的。

3)引用的分析

4)堆和栈

​堆的本质其实就是空闲内存,C++中把堆称为自由储存区,只要是你的程序加载后,没有占用的空闲内存,都是自由存储区,我们用new或者malloc申请一块新的内存区域,都是由操作系统从堆上操作。

​栈是程序在编译时就已经确定了大小的一段内存区域,主要是用于临时变量的存储,栈的效率要高于堆,但是容量有限。

13、技能系统(一)

1)技能系统

​需求:本次重新设计麟江湖的技能系统,技能是在游戏设计之处已经设计好的,游戏设计了十一种技能,属性如下:

技能消耗内力消耗怒气值攻击力冷却
普通工具0010+基础攻击0
大力金刚指10050+基础攻击1
云龙三观10060+基础攻击1
一阳指3002*基础攻击3
迎风破浪3003003
八卦掌5005*基础攻击4
六合八荒5005004
仙人指路100010*基础攻击6
横扫千军100050+2*基础攻击6
气吞山河0100500+5*基础攻击0
秋风到法0100200+10*基础攻击0

2)项目设计

​设计需求:每个角色随着等级不同,最少拥有一个技能,即普通攻击;最多拥有5个技能,技能拥有等级,技能等级每提升1级,角色的最终伤害提升15%,设计角色的属性面板,能够显示角色的属性和技能,角色属性根据需求自己发挥。

#include struct Skill                //定义技能结构体{char Name[48]; //技能名称int Mpp; //内力消耗int Spp; //怒气int Act; //攻击力int ActB; //翻倍攻击int CoolDown; //冷却时间};struct USkill        //定义用户当前的技能{Skill* skill{};int lv;int cooldown;bool buse{};};typedef struct Role{char Name[48];int HP;int MaxHp;int MP;int MaxMp;int Sp;     //怒气值int MaxSp;int Act;    //普通攻击力USkill skills[5];   //每个角色最多5技能}*PROLE;int main(){Skill AllSkills[11]{{"普通攻击",0,0,10,1,0},{"大力金刚指",10,0,50,1,1},{"云龙三观",10,0,60,1,1},{"一阳指",30,0,0,2,3},{"迎风破浪",30,0,300,0,3},{"八卦掌",50,0,0,5,4},{"六合八荒",50,0,500,0,4},{"仙人指路",100,0,0,10,6},{"横扫千军",100,0,50,2,6},{"气吞山河",0,100,500,5,0},{"秋分刀法",0,100,200,0,0}};PROLE User = new Role{"郝英俊",1000,1000,1000,1000,0,100,100,{{&AllSkills[0],0,0,true},{&AllSkills[1],0,0,true},{&AllSkills[2],0,0,false},{&AllSkills[3],0,0,false},{&AllSkills[10],0,0,true}}};PROLE Monster = new Role{"凹凸曼",1000,1000,1000,1000,0,100,100,{{&AllSkills[0],0,0,true},{&AllSkills[1],0,0,true},{&AllSkills[2],0,0,true},{&AllSkills[4],0,0,true},{&AllSkills[10],0,0,true}}};std::cout << "角色姓名:"<Name << std::endl;std::cout << "生命:" <HP << "/" <MaxHp << std::endl;std::cout << "内力:" <MP << "/" <MaxMp << std::endl;std::cout << "怒气:" <Sp << "/" <MaxSp << std::endl;std::cout << "基本攻击:" <Act <skills){if (skill.buse)std::cout<<"技能名称 ["<Name<<"]"<< "消耗MP " <Mpp<< "消耗SP " <Spp<< "附加攻击 " <Act<< "翻倍攻击 " <ActB<< "冷却 " <CoolDown<< "技能等级 " << skill.lv<< std::endl;}}