脆皮之“指针宝典(1)”


大家好,又见面啦!这一次带来的是有关指针的内容,请享用。
每个人都在努力,我们也不能落下哦。加油加油加油!!!

仍然是:点赞加关注,追番不迷路,蟹蟹大家!
在这里插入图片描述
在这里插入图片描述

一:内存和地址

1.地址

导言:大家想象一下,如果宿舍楼没有门牌号,想找一个人是很困难的,但如果有门牌号,就很容易了。CPU and 内存(存放数据),CPU处理数据时,处理的数据从哪里来,从内存中来;处理完数据后,将数据放在哪里,也是内存中。那内存中这么多东西,内存空间如何高效的管理呢?

可以像宿舍楼一样,宿舍楼里面有无数个宿舍,一个宿舍八个人。将内存也分为一个个的内存单元,每个内存的大小取一个字节(即8个比特位,一个字节大小可以放8个比特位),可以想象为一个单元可以住8个小比特。

在这里插入图片描述

每个内存单元(宿舍)都有自己的编号,以便于CPU可以快速找到一个内存空间。生活中我们把宿舍叫为我们住的地址,在计算机中,我们把内存单元的编号也叫做地址,C语言中又把地址叫做指针。总结:编号= =即地址= =即指针

计算机中的单位:
bit(比特位)
Byte(8比特位=1字节Byte)
KB(1KB=1024Byte)
MB(1MB=1024KB)
GM(IGM=1024MB)
TB(1TB=1024GM)

二: 指针变量和地址

(1.)取地址操作符

(1). 理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量其实就是向内存申请空间

int main()
{
	int a = 20;
	//创建变量的本质就是:向内存申请空间
	//int a=20意思是:向内存申请四个字节的空间(整型int的长度是4个字节)
	//变量名是给程序员看的,编译器看的是地址,它是通过地址找到内存单元的
	return 0;
}

在这里插入图片描述
(2)取地址操作符&

在刚刚操作中可以看出,变量是有地址的,那如何拿到地址呢,这就需要取地址操作符&了&a的意思是“取变量a的地址”,a有4个字节,每个字节都有地址,那取的到底是哪个地址?

我们可以尝试打印出a的地址:printf(“%p”,&a);(%p打印地址)(在看地址的时候,可以改成x86,地址较短,方便观察)
在这里插入图片描述
由图可知:&a取出的是a所占4个字节中地址较小的字节的地址。只要我们知道了地址较小的字节的地址,顺藤摸瓜我们就可以找到其他三个字节的地址了。

(2.)指针变量

目前为止,我们已经找到地址了,那如果我们想储存地址该怎么办?我们可以创建一个变量来存放地址(指针),可以将这个变量叫做指针变量

int * pa = &a;  //这里创建了指针变量pa来存放a的地址,  int *是pa的类型
                //*表示pa是指针变量
                //int表示pa指向的变量a是int类型
char ch='q';
char* pc=&ch;

(3.)解引用操作符*(间接访问操作符)

那地址有什么用呢?我们可以通过地址找到原本存放的变量a。

  1. 地址前+解引用操作符*就可以找到a(*pa实际上就是a)

在这里插入图片描述

  1. 我们还可以通过*pa来改变a的大小
    在这里插入图片描述
  • 指针里涉及到的两个操作符就是&和*

(4.)指针变量的大小(与类型无关)

int a = 20;
int* pa = &a;
printf("%zd", sizeof(pa)); //可以将地址的大小打出来看看

我们现在知道a的大小是4个字节,那指针变量pa的大小是多少嘞?指针变量是用来存放地址的,那地址的存放需要多大空间,那么指针变量的大小就是多大。
在这里插入图片描述

前面的内容我们了解到,32位机器(x86)假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产生的2进制序列当做一个地址,【二进制的数字无非是0或1,占1比特位】那么一个地址就是32个bit位,需要4个字节才能存储。【8比特位 = 1字节(Byte)】

如果指针变量是用来存放地址的,那么指针变量的大小就得是4个字节的空间才可以。

同理64位机器(x64),假设有64根地址线,一个地址就是64个二进制位组成的二进制序列,存储起来就需要8个字节的空间,指针变量的大小就是8个字节。

32位平台下地址是32个bit位,指针变量大小是4个字节
64位平台下地址是64个bit位,指针变量大小是8个字节

那接下来想一下,刚刚是指针变量的类型是int*,那如果是char*,它的大小是多少?答案是也是4个字节(x86环境下)

大家想一下,如果a是整型,那地址取得是第一个字节的地址;char的长度是1个字节,它的地址取得也是第一个,所以还是4个字节(x86)。一个字符的地址 or 一个整型的地址,取得都是第一个地址,一个地址需要4个字节的空间,所以,指针变量(存放地址的变量)的大小与类型无关,只要在同一平台下(x86还是x64),大小就相同

在这里插入图片描述

三: 指针变量类型的意义

我们已经知道,指针变量的大小与类型没有关系,那为什么要有这么多类型,它有什么意义呢?

  1. 指针变量的类型决定了对指针解引用时的权限大小
  2. 指针类型决定了指针向前或向后走一步的步长(±整数),单位是字节!

1.指针的解引用

*pa = 32; 这就是指针的解引用 在这里插入图片描述


在这里插入图片描述
从上面的3张图片我们可以看出:变量类型是int,指针变量类型是int*,我可以将变量int的4个字节全改为0;当指针变量类型是char*时,仅可以将变量int的1个字节改为0。

指针的类型决定 对 指针解引用时有多大权限(一次可以操作几个字节) ,int* 的指针解引用可以访问4个字节,char* 的指针解引用仅仅可以访问1个字节

2.指针±整数

int a = 20;
int* pa = &a;
char* pc = &a;
printf("&a=%p", &a);
printf("pa=%p", pa);
printf("pc=%p", pc);

printf("&a+1=%p", &a+1);
printf("pa+1=%p", pa+1);
printf("pc+1=%p", pc+1);

在这里插入图片描述
&a,pa+1之后都向后了4个字节,pc向后了1个字节

char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。这就是指针变量的类型差异带来的变化。

在这里插入图片描述

3.void*指针(无具体类型的指针)

void*指针可接受任意类型的指针,但它也有限制,不能进行解引用和±整数的操作(不能解引用因为不知道权限,不知道该访问几个字节。不能±整数,因为不知道该跳过几个字节)

唯一的用处就是当不知道别人将传过来什么类型的地址时,可以用void*接收。

	int a = 20;
	char ch = 'q';
	void* pa = &a;  //int*
	void* pc = &ch;  //char*
	*pa = 90; //不可以解引用
	pa + 1;  //不可以±整数

⼀般 void* 类型的指针是使用在函数参数的部分,⽤来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果。使得一个函数来处理多种类型的数据,在指针(4)中我还会提及。

四: const修饰

(1)const修饰“变量”

在平常,我们先对变量进行初始化,之后还可以赋值(改变 变量的值)。

	int a = 10;
	a = 300;
	printf("%d\n", a); //打印出来的a是300

当我们用const修饰变量时,变量叫做常变量(本质还是变量,但不可修改)

const int a = 10; //这样的话,a是不可以改变的

我们可以通过地址找见它,再修改它,但是这样不太好,因为我们用const修饰a就是为了让它不被修改(违规操作把它修改了)

	const int a = 89;
	int* pa = &a;
	*pa = 99;
	printf("%d", a);

(2)const修饰“指针变量”

一般来讲const修饰指针变量,可以放在 * 的左边,也可以放在 * 的右边,意义是不一样的。

int a = 99;
int const* pa = &a; //和const int* pa = &a;一样 //const在*左边
int* const pa = &a; const在*右边
  • 在此区分一下p,*p,&p
  • p是指针变量,里面放着地址(注意注意,p可不是常量,它是指针变量,是可以后期赋值的)
  • *p指向那个地址的对象
  • &p是p变量的地址
	int a = 9;
	int b = 89;
	int* pa = &a; //这里将a的地址放在p里
	*pa = 99; //通过地址找到a并改变它的值
	pa = &b;  //这里将b的地址放在pa里,pa里不再是a的地址
  1. const修饰指针变量,放在 * 左边(int const * pa =&a),const修饰的是 * pa,自此之后 * pa变成常变量,不能再通过指针变量来改变 * pa所指向的对象。
    ( 上面的代码中,* pa指向的是a,所以不能通过 * pa来改变a的值,但是还是可以直接改变a的,比如a=88之类的)(上面的代码中,* pa=99;不能实现)
  2. const修饰指针变量,放在* 右边(int * const pa = &a;),const修饰的是变量pa,自此之后pa变成常量,它的指向已确定,之后不能改变(pa里面就是a的地址,不能之后再变了,例如上面代码中pa = &b;不能实现),但是它所指向的内容(a)是可以改滴

五: 指针运算

1.指针±整数

大家还记得之前如何循环打印数组中的数字吗,现在我们可以通过指针±来实现

  1. 之前的打印方式
int main()
{
	int arr[9] = { 1,2,3,4,5,6,7,8,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int index = 0;
	for (index = 0; index < sz; index++)
	{
		printf("%d ", arr[index]);
	}
	return 0;
}
  1. 用指针打印
int main()
{
	int arr[9] = { 1,2,3,4,5,6,7,8,9 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int index = 0;
	int* pa = &arr[0]; //pa存放的是第一个元素的地址,*pa指向arr[0],第一个元素
	for (index = 0; index < sz; index++)
	{
		printf("%d ", *pa);  
		pa++; //即p+1,整型指针+1,向后走一个整型
	}

}
  • 解释:
    数组里的数字元素在内存里连续存放,我先得到第一个元素的地址放在pa里,解引用( * pa)就可就得到第一个元素了。
    当指针变量+1时,由于指针变量是int*类型,会走4个字节,即一个整数,这是指针变量就是下一个元素的地址了,再解引用,得到第二个元素…

2.指针 - 指针

为什么只有指针-指针呢?以日期为例,你见过5月8日+天数(指针±整数),5月8日-4月9日=期间一共有多少天(指针-指针),但是你见过5月8+8月9=啥?(没有指针+指针)

指针 - 指针 = 指针和指针之间元素的个数的绝对值
指针 - 指针 ,计算的前提是:俩指针指向的是同一个空间

数组中,随着下标的增长,地址由低到高增长【下标小的地址小,下标大的地址大】

	int arr[9] = { 1,2,3,4,5,6,7,8,9 };
	int sz1 = &arr[8] - &arr[0]; //最后一个的下标:sz-1
	int sz2 = &arr[0] - &arr[8];

	printf("%d\n", sz1); //结果是8
	printf("%d\n", sz2); //结果是-8,因为这里是低地址-高地址

在这里插入图片描述
先知:
1.strlen统计的是:字符串中,\ 0之前的字符个数
2.数组名其实是数组首元素的地址:arr == &arr[0]
过程:
(1)先将首元素的地址传过去,形参那里用char*…接收
(2)之后只要*…不是\0,我们就让count+1,之后再指针+1,如果还不是\0,再+1,循环起来

size_t computer_geshu(char* pc)  //返回值类型是size_t,无符号整型(个数肯定是整数)
{
	size_t count = 0;
	while (*pc != '\0')
	{
		count++;
		pc++;
	}
	return count;
}
int main()
{
	char ch[] = "qwertyuiop";
	int r = computer_geshu(ch); //数组名ch就是&ch[0]
	printf("%d", r);
}

2.指针的关系运算

即让指针进行大小比较

在这里插入图片描述
只要我p里面存的地址小于&arr[sz]就能进入循环。
(在这里说一下为什么是&arr[sz],最后一位元素地址是&arr[sz-1],他的后面是没有的,即地址只能≤&arr[sz-1]才能进入循环,≤&arr[sz-1]不就相当于<&arr[sz]。)
在这里插入图片描述

//用比较大小来当作循环条件
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9 };
	int* p = arr; //这里的arr是数组名,数组名就是数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (p = arr; p < &arr[sz]; p++)  //也可以写成while(p<&arr[sz]),后面添一个p++
	{
		printf("%d ", *p);
	}
}

六: 野指针

何为野指针?野指针就是指向位置不明确,比如:随机,不正确,没限制
(没有权限访问它,就会成野指针)

(1.)野指针成因

  1. 指针变量指向 随机(指针未初始化)
int* p; 
//p是指针变量,存放地址,但这里并没有说p指向谁
//局部(指针)变量如果没有初始化,它的值是随机的
printf("%p\n", p);

*p=20//没有初始化的局部变量p,它的值是随机的,如果将p中存放的值当作地址
//解引用操作符就会形成非法访问
  1. 指针越界访问
    在这里插入图片描述
    你访问你申请的空间没有问题,但不能越界,如果它指向了不属于自己的空间,而且还解引用访问人家(解引用第10位,则p就成野指针了)

  2. 指针指向的空间被释放
    在这里插入图片描述
    在传给p之前都没有问题,但之后,它将p所指向的值(m)改为900,但m是函数那部分的,当进入函数,m被创建,并申请了4个字节的空间,但是只要一出函数,m就被销毁,申请的空间也还给操作系统了(虽然还给人家了,但它还是坚定地把地址传给p了,只要一但解引用,访问它,则p就成野指针了

(2.)如何规避野指针

  1. 指针初始化:如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL.(空指针)(NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。)

在这里插入图片描述
空指针是不能进行访问的。

  1. 小心指针越界
  2. 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
  3. 避免返回局部变量的地址

七:assert断言

assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。(断言通常被用来判断指针的有效性)

assert() 宏接受一个表达式作为参数。如果该表达式为真(返回值非零), assert() 不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写入一条错误信息,显示没有通过的表达式以及包含这个表达式的文件名和行号。

  • 在我看来,它的效果和if差不多,断言是:如果你不对,它就会报错;但 if 是:如果你不对,不满足条件,他会不进入那个if里面,继续往后运行,不会给你报错的。
    在这里插入图片描述

我这里的意思是,若 t 不是空指针,则令 t 指向的对象=900
我用了断言,它帮我判断出t是空指针,并报错了

  • 有一种无需更改代码就能开启或关闭 assert() 的机制。
    如果已经确认程序没有问题,不需要再做断言,就在 #include <assert.h> 语句的前面,定义一个宏 NDEBUG 。【即在最前面写#define NDEBUG】
  • 缺点:引入了额外的检查,增加了程序的运行时间。【一般我们可以在 Debug 中使用断言,在 Release 版本中选择禁用 assert 就行。在 VS 这样的集成开发环境中,在 Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在 Release 版本不影响用户使用时程序的效率。】

八:指针的使用和传址调用

  1. 传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量;
  2. 函数中只是需要(主调函数中的变量值)来(实现计算),就可以采用传值调用。
  3. 如果函数内部要(修改主调函数中的变量的值),就需要传址调用。

仔细想了一下,这个还是需要例子才能说明的更清楚,我将单独写一个帖子,区分一下传值调用和传址调用。

好啦,今天的内容就到这里啦,还有就是谢谢大家的点赞和收藏,拜拜,下次见!

  • 35
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 脆皮trimps是一款非常受欢迎的策略类游戏,玩家需要进一步修改游戏代码来增加游戏的乐趣和挑战。下面是对脆皮trimps代码进行修改的一些建议。 首先,可以增加更多的敌人类型和难度。在游戏中加入更多的敌人类型,每个敌人都有不同的能力和弱点,玩家需要根据不同的敌人制定不同的战略来应对。同时,增加敌人的难度,让玩家在游戏中面临更大的挑战,提高游戏的可玩性。 其次,可以增加更多的技能和装备选择。在游戏中增加更多的技能和装备选项,使玩家能够更灵活地制定战略和使用不同的策略来获得胜利。玩家可以通过击败敌人或者完成特定任务来解锁新的技能和装备,增加游戏的乐趣和挑战性。 另外,可以增加更多的游戏关卡和任务。游戏中可以增加更多的关卡和任务,每个关卡和任务都有不同的目标和要求,玩家需要通过完成这些任务来推动游戏的进程。同时,增加隐藏关卡和任务,让玩家能够在游戏中发现更多的惊喜和挑战。 最后,可以增加更多的游戏模式和挑战。除了传统的游戏模式之外,可以增加更多的特殊模式和挑战,让玩家能够以不同的方式来体验游戏。这些特殊模式和挑战可以根据玩家的需求和偏好来设置,增加游戏的可玩性和持久性。 总之,通过对脆皮trimps游戏代码的修改,我们可以增加更多的敌人类型和难度,增加更多的技能和装备选择,增加更多的游戏关卡和任务,以及增加更多的游戏模式和挑战,从而提高游戏的乐趣和挑战性。 ### 回答2: 脆皮trimps是一款受欢迎的游戏,玩家可以通过修改其代码来实现一些游戏中的变化。首先,我们需要了解trimps的基本原理和代码结构。trimps是一款基于浏览器的游戏,玩家需要通过点击屏幕来获取资源和建立基地。 要修改trimps的代码,首先,我们需要找到游戏的代码文件。在大多数基于浏览器的游戏中,这些文件通常是以.js为后缀的JavaScript文件。常见的文件名可能是game.js或main.js。 一旦我们找到了代码文件,我们可以使用任何文本编辑器来打开它。如果我们想要更方便地修改代码,可以使用开发者工具来编辑代码。在大多数现代的浏览器中,开发者工具可以通过点击右键并选择“检查元素”来打开。 一旦我们找到了代码文件并打开它,我们就可以开始修改代码了。根据个人的需求,我们可以调整游戏中的各种参数,如资源数量、建筑物效果等。 例如,如果我们想要增加资源数量,我们可以搜索游戏代码中与资源有关的部分,并修改相关参数。这通常涉及到修改变量的初始值或增加资源的获取速度。 除了修改资源数量之外,我们还可以调整游戏中的其他参数,如敌人的强度、建筑物的效果等。通过仔细阅读代码并根据自己的需求进行修改,我们可以对游戏进行个性化的调整。 总之,修改脆皮trimps的代码是一项有趣的任务,它可以让我们对游戏进行个性化的调整和优化。但是,我们需要对代码结构和编程知识有一定的了解才能进行有效的修改。 ### 回答3: 脆皮trimps 修改代码的意思是对游戏《脆皮trimps》的代码进行改动。《脆皮trimps》是一款像素风格经营战略游戏,玩家需要管理一个小队并帮助他们在地下洞穴中挖掘资源和对抗敌人。 修改代码可以带来多种效果。比如,可以改动游戏角色的属性,让他们的生命、攻击力、耐力等值提升或下降,增加或减少游戏的难度;还可以改变敌人的行为模式和强度,提高游戏的挑战性;或者添加新的地下洞穴地图和任务,扩大游戏的内容。 此外,修改代码还可以修复游戏中的一些bug和错误,提高游戏的稳定性和流畅度。 脆皮trimps 的代码修改通常需要具备一定的编程知识和技能,比如了解游戏的代码结构和逻辑,熟悉编程语言等。一些高级玩家和开发者经常在游戏社区或者自定义模组中分享他们的代码修改,供其他玩家使用和学习。 总之,通过修改《脆皮trimps》的代码,玩家可以根据自己的喜好和需求,个性化游戏体验,增加游戏的乐趣和挑战性。也可以通过修改代码来完善游戏的功能和内容,提高游戏的品质和吸引力。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值