指针详解(1)
  OA1z5to1EvPc 2023年11月15日 20 0

1.内存和地址

1.1内存

内存和地址类似于房间号,例如再哪个区哪栋楼房间号,而内存也是如此,在内存中也被分为一个个小的内存单元,类比房间号,可以很快地找到内存地址,再内存中分为bit->byte->kb->mb->gb->tb->pb,这是内存中cpu的储存方式的划分,类似于一个宿舍住八个人,一层楼住很多宿舍,而一栋楼分为多层,一个区分为多栋楼。通过地址我们可以很快地寻找到目标


1.2究竟该如何理解编址

理解内存和cpu,才能理解编址,cpu和内存是如何链接的?通过地址总线,数据总线和控制总线进行连接,在cpu上想找到某些数据,可以从cpu上输入该数据所在的地址,通过地址总线向内存中找到该数据所在的地址,然后再从内存通过数据总线把数据传输到cpu寄存器进行一些数据操作;先理解一个概念:32/64位机器是什么?可以简单理解为在内存中的每一个位,每一个位有两种状态 0/1 ,可以理解为电脉冲的有无,这个概念有助于理解后面的指针变量的大小;所以什么是编址?编址就是编辑的地址,是内存总自带的内存单元,是本身的地址,并不是每一个内存单元的地址都被存储,而是找到希望找到哪一个地址,哪一个地址被找到

2.指针变量和地址

从上面可以简单理解为指针就是地址,而指针变量就是存储地址的变量,以下是一段简单的指针代码理解

#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	*pa = 20;
	return 0;
}

2.1取地址操作符(&)

在上文中可以看到 &a 这个操作,那是什么意思? 从头解释:开辟4个字节空间,给a变量赋值10,创建一个数据类型为 int *类型的变量,int *就是pa的数据类型,是int类的指针变量,&a的意思是取出a的地址放在pa这个指针变量里,那么可以把指针变量pa的类型改为char*吗?可以,但是会报错,因为要存的a是int类,如果想改成char*类型,可以进行强制类型转换

#include <stdio.h>
int main()
{
	int a = 20;
	char* pa = &a;
	*pa = 20;
	printf("%d", a);
	return 0;
}

下列是进行强制类型转化之后就没有该警告了

#include <stdio.h>
int main()
{
	int a=20;
  char*pa=(char)&a;//这段代码无警告
  
	return 0;
}

2.2 指针变量和解引⽤操作符(*)

#include <stdio.h>
int main()
{
	int a = 20;
	char* pa = &a;
	*pa = 20;// * 操作符是拿出pa里的地址,指向地址所指向的空间,改变该空间里的内容
	printf("%d", a);
	return 0;
}

*操作符作用是把地址进行指向所在的空间,再通过该空间进行改变变量的值

2.2.1 指针变量

指针变量相当于基本数据类型后面加上*,例如int * ,char*,double *

2.2.2 如何拆解指针类型

指针类型,如int * ,char*,double *,把int *,拆成int和*

2.2.3 解引⽤操作符

&:解引用操作符,相当于在指针变量中存放地址,&操作符相当于把通过地址对所指向的空间的内容进行修改

2.3 指针变量的⼤⼩

指针变量的大小是不是固定的?int*,char*,double*,大小是不是一样的?

#include <stdio.h>
int main()
{
	int a = 20;
	char b;
	int* pa = &a;
	char* pb = &b;//pa和pb的大小是不是一致的?
  printf("%d\n",sizeof(int *);
  printf("%d\n",sizeof(char *);
	return 0;
}


指针详解(1)_指针变量

通过sizeof操作符可以知道int和char数据类型所占内存空间大小都为八字节,这是因为是64位机器的缘故,可以在上面将64改成x86环境,也就是32位环境,

指针详解(1)_操作符_02

x86是机器字长位32位,相当于32根线,有2^32次方种状态,也就可以存储相对应的内存大小,指针变量大小为4个字节;64则是有64根地址线,有2^64种状态,相对应的指针变量的大小为8字节

3.指针变量类型的意义

3.1 指针的解引⽤

既然指针变量的大小是一样的,那为什么还要分成各种指针变量,char*,int*呢?

#include <stdio.h>
int main()
{
 int n = 0x11223344;
 int *pi = &n; 
 *pi = 0; 
 return 0;
}

在调试期间打开监视

指针详解(1)_操作符_03

指针详解(1)_指针变量_04

#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;//强制类型转换可能会出现问题
 *pc = 0;
 return 0;
}

指针详解(1)_#include_05

指针详解(1)_操作符_06

这两段代码想展示的问题是给n的值进行改变,char会改变一个字节内容,而int会改变四个字节内容,而代码没显示这个问题

3.2 指针+-整数

指针+-整数应该是什么?指针!

#include <stdio.h>
int main()
{
	int a = 10;
	int* pa = &a;
	printf("%p\n", pa);//%p为打印地址
	printf("%p", pa+1);
	return 0;
}

指针详解(1)_指针变量_07

上述代码和图片可以看出指针+-整数后还是地址,存放在指针变量中

3.3 void* 指针

void*指针变量是可以接收任何地址的指针变量,例如在函数中可以接受各种类型的地址!

#include <stdio.h>
int main()
{
	int a=20;
  char *pa=(char)&a;//如果这样写编译器就会报错,这时加上强制类型转化就不会出现问题
  //如果是void*就可以接受任何类型地址,不用进行强制类型转换,但也有一定的缺点
  //例如通过void *类型的指针变量无法进行指针+-整数的操作,也无法进行*操作
	return 0;
}

指针详解(1)_操作符_08

指针详解(1)_#include_09

4.const修饰指针

4.1 const修饰变量

const修饰变量,修饰完后该变量不能被改变

#include <stdio.h>
int main()
{
const int a=20;
a=10;//err
return 0;
}

4.2 const修饰指针变量

虽然const修饰的变量不能被表面修改,但可以从别的方式进行改变

#include <stdio.h>
int main()
{
const int a=20;
int *pa=&a;
*pa=20;//通过地址的方式将a的值进行改变
return 0;
}

这里我们就要思考一下,如何让*pa不能改变a的值呢?可以采用以下const修饰来阻止这种修改

#include <stdio.h>
int main()
{
const int a=20;
const int *pa=&a;//const在*左边限制的是*
&pa=0;
return 0;
}

指针详解(1)_操作符_10

const在左边修饰的是pa,如果const在右边则修饰的是pa

#include <stdio.h>
int main()
{
		const int a = 20;
		int*const pa = &a;
		*pa = 30;
 	  pa=20;//err 这代表把pa所在的空间改变会受到限制
	return 0;
}

指针详解(1)_#include_11

指针详解(1)_指针变量_12

该图所代表意思为pa存放a的地址,若const修饰的是pa,则代表pa的空间内容受到限制,无法通过后续进行改变

5. 指针运算

指针运算分为三种:指针+-整数,指针-指针,指针的关系运算

5.1 指针+- 整数

指针+-整数在前面已经大概描述过,指针+-整数还是指针

数组中:

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* pa = arr[0];

	printf("%d ", *pa + 1);//arr[1],会出现1的结果
	return 0;
}

5.2 指针-指针

指针-指针是整数,由5.1可知指针+-整数还是指针,那么同理,指针-指针一定是整数,可以在数组中进行理解

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* pa = arr[0];
	int *pb=&arr[1];
	printf("%d ", pa+pb);
	return 0;
}

指针详解(1)_指针变量_13

由此图可知,指针减指针是整数,也是数组之间的元素个数,由此,我们可以写出strlen库函数的定义

#include <stdio.h>
#include <assert.h>
int my_strlen(const char* p)//这里可以进行优化,使用const修饰
{
	int count = 0;
	assert(p);//在循环外使用assert断言,使p这个指针变量
	while (*p)
	{
			if (*p == '\0')
			{
				break;
			}

		p++;
		count++;
	}
	return count;
}
int main()
{
	int len = my_strlen("abcdefgh");
	printf("%d ", len);
	return 0;
}


5.3 指针的关系运算

可以通过指针进行数组的遍历

#include <stdio.h>
int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);//为什么sizeof里放的arr的意思和后面注释意思不同,是否造成了矛盾?
  //后文将详细描述..
	int* pa = &arr[0];
	
	while (pa<arr+sz)//arr是数组首元素地址,arr+sz就是数组最后一个元素的地址
	{
		printf("%d ", *pa);
		pa++;//pa++代表将arr中元素下标向后移动一位
	}
	return 0;
}

6. 野指针

野指针定义:指针指向的对象是不明确的,没有对象,指向随机的地址

6.1 野指针成因

主要有三部分可以造成野指针:

1.指针没有进行初始化:

#include <stdio.h>
int main()
{
int *pa;
return 0;
}

指针详解(1)_操作符_14

2.指针越界访问

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* pa=&arr[0];
	for (int i = 0; i < 11; i++)
	{
		*pa = 1;
		pa++;
	}
	return 0;
}

指针详解(1)_操作符_15

这里会导致指针越界访问,编译器会出现警告,这也是常见野指针出现的原因

3.指针所指向的空间已经被销毁

int* test()
{
	int n = 100;
	return &n;
}
#include <stdio.h>
int main()
{
	int *ret = test();
	printf("%p\n", *ret);//此时的n的地址在调用函数结束后空间已经被销毁,再被销毁后的空间进行打印会出现野指针
	return 0;
}

指针详解(1)_操作符_16

6.2 如何规避野指针

6.2.1 指针初始化

指针初始化

#include <stdio.h>
int main()
{
int a=10;
int *pa=&a;
return 0;
}

如果不知道应该指向哪个对象,可以给个NULL对象

#include <stdio.h>
int main()
{
int *pa=NULL;
return 0;
}

指针详解(1)_指针变量_17

6.2.2 ⼩⼼指针越界

指针越界只能依靠自己或者编译器才能及时看出错误,平时在写指针时要仔细谨慎

6.2.3 指针变量不再使⽤时,及时置NULL,指针使⽤之前检查有效性

#include <stdio.h>
int main()
{
	int a = 20;
	//.........
	int* pa = &a;
	int* pa = NULL;
	if (pa != NULL)//判断
	{
		//......
	}
	return 0;
}

6.2.4 避免返回局部变量的地址

在上述第三个例子看到如果返回局部变量的地址,就会造成野指针,避免错误

7.assert断⾔

使用assert断言需引用头文件,断言告诉你错在错误的地址,如果返回值是0,则就会报错

#include <stdio.h>
#include <assert.h>
int main()
{
	int a = 10;
	int* pa = NULL;
	assert(pa != NULL);//断言为错误(0)就会报错
	return 0;
}

指针详解(1)_指针变量_18

assert只能在debug版本使用,在release会被优化;如何一条语句直接取消所有assert作用?

#define NDEBUG//只需在assert.h头文件中引用#define NDEBUG就会使所有assert失效,以后如果想再使用assert把NDEBUG注释掉
#include <stdio.h>
#include <assert.h>
int main()
{
	int a = 10;
	int* pa = NULL;
	assert(pa != NULL);
	return 0;
}

指针详解(1)_指针变量_19

assert优点已说明,也有一定的缺点,会占用内存判断时间

8.指针的使⽤和传址调⽤

8.1 strlen的模拟实现

复习前面所说的模拟strlen实现:字符串进行传参,传到函数中,在循环中,字符串地址向后移动,如果为\0则停止,循环中的计数器停止计数!

#include <stdio.h>
#include <assert.h>
int my_strlen(const char* p)//这里可以进行优化,使用const修饰
{
	int count = 0;
	assert(p);//在循环外使用assert断言,使p这个指针变量
	while (*p)
	{
			if (*p == '\0')
			{
				break;
			}

		p++;
		count++;
	}
	return count;
}
int main()
{
	int len = my_strlen("abcdefgh");
	printf("%d ", len);
	return 0;
}

8.2 传值调⽤和传址调⽤

传值调用很好理解,在函数中传值,把实参传给形参

#include <stdio.h>
int test(int x, int y)
{
	return x + y;
}
int main()
{
	int a = 0;
	int b = 0;
	int len = test(a, b);
	return 0;
}

指针详解(1)_操作符_20

传址调用:传送地址,例如使用函数进行交换数据,如果不使用函数有多种方式可以进行操作

使用函数:思路:如果只是单纯的把值传过去,则无法把两个值返回,所以把地址传送过去,再函数里对地址进行引用修改

#include <stdio.h>
void* swap(int* x,int* y)//在这里我犯了一个错误,const修饰,如果const修饰,下面赋值操作就会发生错误
{
	int tmp = *x;
	*x = *y;
	*y = tmp;
}
int main()
{
	int a = 10;
	int b = 20;
	swap(&a, &b);
	printf("%d\n", a);
	printf("%d\n", b);
	return 0;
}


指针详解(1)_操作符_21


以上就是指针的基础部分!

【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月15日 0

暂无评论

推荐阅读
OA1z5to1EvPc
作者其他文章 更多

2023-11-15