BIT - 4 指针( 9000字详解 )

一: 指针

  • 内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。
  • 所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是 1 个字节。
  • 为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。

一个小格为一个单位,一个字节就是8个小格
在这里插入图片描述
因为一个二进制位可以存放 0 和 1,有两种组合方式,一个字节有 8 位,所以一个字节可以有 28= 256 中组合方式

在这里插入图片描述
在每一个字节的起始位置处,都有一个地址,我们通过这个地址就可以找到这个空间在内存中的位置

注意,每个地址都是唯一的

1.1: 指针和地址的大小

目前计算机主要分为 32 位计算机和 64 位计算机两种,

  • 在 32 位计算机中,指针的大小为 4 字节,
  • 在 64 位计算机中,指针的大小为 8 字节

指针是用于存放地址的变量

在计算机上,有地址线电线产生的高低电平信号,这些信号会转换成数字信号 0 和 1, 32 位机器顾名思义就是有 32 根地址线,而 64 位机器就是有 64 根地址线

  • 对于一根地址线,它有 2 种组合,即 0 和 1
  • 对于两根地址线,它有 4 种组合,即 00 01 10 11
  • 对于三根地址线,它有 8 种组合,即 000 001 010 011 100 101 110 111
  • ……

以此类推。

  • 32 根地址线,就有 232 种组合
  • 64 根地址线,就有 264 种组合

所以我们要想储存一个变量的地址,

  • 在 32 位机器种只需要 32 个二进制位
  • 在 64 位机器种只需要 64 个二进制位即可

而一个字节又等于 8 个比特位,所以

  • 在 32 位机器中,指针的大小为 4 字节
  • 在 64 位机器中,指针的大小为 8 字节

1.2: 指针的使用

那么我们该如何通过指针来存放一个变量的地址呢?

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

运行结果如图所示
在这里插入图片描述

接下来我将通过画图的形式讲解:

10 的二进制是 1010,如果补齐 64 位的话,就是

  • 00000000 00000000 00000000 00000000
  • 00000000 00000000 00000000 00001010

这就是 10 在内存中存储的数据
在这里插入图片描述

如图所示,我们定义了一个数值为 10 的空间,接着我们通过取地址符 &,将 num 的地址取出来,注意,取地址取出来的是开头处的地址,然后把地址赋给了专门存放地址的变量:指针 int *p,此时 p 就存放了 num 开头的地址了

那么此时我们就有了 num 的地址,并将地址存放在指针变量 p 中,我们该如何通过 p 来找到 num 呢?

这时就要用到另外一种操作符 :解引用操作符

解引用操作符就是取地址操作符的逆运算

  • &num 就是通过 num 找到 num 的地址
  • 而 *p 则是通过 num 的地址,找到 num(此时p存放的是num的地址)

我们说过,一个变量它有值属性和址属性两种属性


那么,这两个属性有什么关联吗?答案是有的,我们可以通过值属性找到址属性,也可以通过址属性找到值属性
在这里插入图片描述
那么我们再来解释一下上面的代码

在这里插入图片描述
首先我们定义了一个 int 类型的 num 变量,并赋值为 10,接着将 num 的地址取出来,并将地址赋给了指针 p,接着我们通过解引用操作符 * 来找到 num 的值,并将 20 赋给它,所以 num 的值也就被改成了 20

1.3 指针和指针类型

我们都知道,对于一个变量,变量可以有不同的类型,比如说整型浮点型等等,那么指针有没有类型呢?答案是有的:

char  *pc = NULL;
int  *pi = NULL;
short *ps = NULL;
long  *pl = NULL;
float *pf = NULL;
double *pd = NULL;

这里可以看到,指针的定义方式是: type + * 指针名。

  • char* 类型的指针是为了存放 char 类型变量的地址。
  • short* 类型的指针是为了存放 short 类型变量的地址。
  • int*类型的指针是为了存放 int 类型变量的地址。

那指针类型的意义是什么?下面通过代码例子讲解:

#include <stdio.h>
//演示实例
int main()
{
	int n = 10;
	char *pc = (char*)&n;
	int *pi = &n;
	printf("%p\n", &n);
	printf("%p\n", pc);
	printf("%p\n", pc+1);
	printf("%p\n", pi);
	printf("%p\n", pi+1);
	
	return  0;
}

运行结果如下:

在这里插入图片描述
总结一下:

  1. 指针的类型决定了指针向前或者向后走一步有多大(距离),即 指针 + 1 的意义是跳过当前指针所指空间的类型大小
  2. 指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。

比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

int main()
{
	int arr[10] = { 0 };
	int* p = arr;

	return  0;
}

所以我们应该很容易知道,

  • *( p + i ) == arr[ i ]

对于数组,数组的名字表示首元素的地址,因为 p 也指向了首元素的地址,所以*( p + i )也可以写成*(arr + i),即:

  • *( p +i ) == *(arr + i)

所以:

  • arr [ i ] == *(arr + i)

1.4 野指针

在 c 语言中对于野指针的定义是:

  • 一个指针指向已被释放的内存地址,
  • 一个指针未被赋值任何一个地址

而 null 则是一个特殊的指针,

  • null 表示指针变量不指向任何有效的内存地址

下面通过代码举例:

  1. :未被赋值任何一个地址
#include <stdio.h>
int main()
{
	int *p;//局部变量指针未初始化,默认为随机值,是一个野指针
	*p = 20;
	return 0;
}

在这个例子中,我们并未将任何地址赋值给 p,所以 p 并未指向任何有效的空间,所以这个指针是一个野指针

  1. 指向已被释放的内存地址
#include <stdio.h>


int* test()
{
	int a = 10;
	return &a;
}

int main()
{
	int* p = test();
	printf("%d\n", *p);

	return  0;

}

这段代码看似没问题,实则问题很大,因为在 test 函数中,a 是一个局部变量,当出了函数这个作用域之后,a 这个空间就会被释放了,而 test 函数又返回 a 的地址,此时 p 也是一个野指针

那么我们该如何规避野指针呢?

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向空间释放,及时置 NULL
  4. 避免返回局部变量的地址
  5. 指针使用之前检查有效性

1.5 两个指针的相减

在 c 语言中,两个指针相减,表示的是在两个指针内,所含有的指针类型所指空间类型的个数 (绝对值就是个数了,因为可能是负数)

1.6 字符指针

int main()
{
  const char* pstr = "hello bit.";//这里是把一个字符串放到pstr指针变量里了吗?
  printf("%s\n", pstr);
  return 0;
}

代码 const char* pstr = “hello bit.”; 特别容易让同学以为是把字符串 hello bit 放到字符指针 pstr 里了,但是其本质是把字符串 hellobit. 首字符的地址放到了 pstr 中
在这里插入图片描述
那就有可这样的面试题:

#include <stdio.h>
int main()
{
	  char str1[] = "hello bit.";
	  char str2[] = "hello bit.";
	  const char *str3 = "hello bit.";
	  const char *str4 = "hello bit.";
	  if(str1 ==str2)
	  printf("str1 and str2 are same\n");
	  else
	  printf("str1 and str2 are not same\n");
	  
	  if(str3 ==str4)
	  printf("str3 and str4 are same\n");
	  else
      printf("str3 and str4 are not same\n");
  
  return 0;
}

这里最终输出的是:

str1 and str2 are not same
str3 and str4 are same

这里 str3 和 str4 指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域,当几个指针。指向同一个字符串的时候,他们实际会指向同一块内存,因为对于被 const 修饰的常量字符串,它是不能被修改的,所以编译器会认为这种字符串存一份就够了,当你需要使用的时候只需要用新的指针指向它即可

但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以 str1 和 str2 不同

1.7 数组指针和指针数组

在 c 语言中,数组指针和指针数组是两个不同的概念

  • 数组指针是指针
  • 指针数组是数组

数组指针是指针很好理解,我们可以通过类比的方式,整型指针是指针,字符指针也是指针,整型指针指向的空间是整型,字符指针指向的空间是字符,类似的,数组指针指向的是一个数组

指针数组是数组也很好理解,整型数组是存放整型的数组,字符数组是存放字符的数组,那么指针数组就是存放指针的数组

1.7.1 数组指针和指针数组的定义

那么数组指针和指针数组该怎么定义呢?

指针数组:

  • int *p1[10];

数组指针:

  • int (*p2)[10];

因为 [ ] 下标引用操作符比 * 间接访问操作符的优先级高

所以在 int *p1[10] 中,p1会先和 [ ] 下标引用操作符结合,这说明 p1 是一个数组,把数组名和 [10 ] 删掉,就是数组内存储元素的类型,所以这个数组存储的类型是 int * 的也就是存储指针的数组,简称指针数组

对于 int (*p2)[10]; 来说 p2 先和 * 结合,这说明 p2 是一个指针,将( *p2) 删掉就是这个指针所指向的空间类型,很明显,这个 p2 指向了一个数组,是一个指向数组的指针,叫做数组指针

1.8 数组名的意义

在 c 语言中,数组名在一般情况下都代表着数组首元素的地址,但是也存在着两个例外:

  1. sizeof(数组名)

此时数组名表示整个数组,计算的是整个数组的大小,单位是字节

  1. &数组名

这里的数组名表示整个数组,取的是整个数组的地址,虽然在值上和数组首元素的地址相同,但是意义不一样(指针类型决定指针+1走的距离)所以说,数组首元素的地址+1跳过的大小是一个元素,数组的地址+1跳过的是整个数组的大小

1.8.1 二维数组数组名的含义:

二维数组的数组名代表的是第一个一维数组的数组地址,下面通过一个代码来说明:

int main()
{
	int arr[2][3] = { {1,2,3},{4,5,6} };
	printf("%p\n", arr);
	printf("%p\n", arr[0]);
	printf("%p\n", arr[1]);
	printf("\n");
	printf("%p\n", arr+1);
	printf("%p\n", arr[0]+1);
	printf("%p\n", arr[1]+1);
	return 0;

}

这段代码的输出结果是:
在这里插入图片描述

这是因为 arr 是一个二维数组,它的数组名代表着第一个一维数组 arr[0] 的地址。

当我们对指针进行一些地址运算时,指针的值会改变。arr+1 表示指针向后移动一个一维数组的大小。在这里,一个一维数组的大小是 3 个整型元素,所以 arr+1 结果的地址为原始地址0019FD8C再增加 3 个 int 的大小,即 0019FD98

类似地,arr[0]+1 表示 arr[0] 的地址向后移动一个整型的大小,即 0019FD8C+ sizeof(int) = 0019FD90

1.9 数组传参

一维数组传参:

#include <stdio.h>
void test(int arr[])//ok
{}
void test(int arr[10])//ok
{}
void test(int* arr)//ok
{}
void test2(int* arr[20])//ok
{}
void test2(int** arr)//ok
{}
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
	
	return 0;
}

arr 代表的是数组首元素的地址,所以在传参的时候,我们可以通过 int* arr 来接收参数,但是为什么 int arr[ ] 也行呢?

在 C 语言中,数组作为函数参数时,它在本质上会被转换为指针传递给函数。这意味着形如 int arr[ ] 的数组参数和 int* arr 的指针参数实际上是等价的 ( 加一级地址 ),当你在函数声明或定义中使用 int arr[ ] 时,编译器会将其转换为 int* arr。

接下来再看一个例子:

void test(int arr[3][5])//ok
{}
void test(int arr[][])//no
{}
void test(int arr[][5])//ok
{}
//总结:二维数组传参,函数形参的设计只能省略第一个[]的数字。因为对一个二维数组,可以不知道有多少行,但是必须知道一行多少元素,这样才方便运算。
void test(int* arr)//no
{}
void test(int* arr[5])//no
{}
void test(int(*arr)[][5])//ok
{}
void test(int** arr)//no
{}
int main()
{
	int arr[3][5] = { 0 };
	test(arr);
}

1.10 函数指针

顾名思义,函数指针就说指向函数的一个指针,和数组指针的定义非常类似:

数组指针的定义:

  • int (*p) [ ]

而函数指针的定义是:

  • int (*p)()

p 先和 * 结合,说明 p 是一个指针,()说明指针指向的是一个函数,并且这个函数没有参数,而 int 则是这个函数的返回值类型

同理,有了函数指针,我们同样可以有函数指针数组,这样可以无线套娃,函数指针数组的定义方式如下:

  • int (*parr1[10])();

parr1 先和 [ ] 结合,说明 parr1 是一个数组,将数组名和 [ ] 删除,剩下的就是元素的类型,很明显这是一个函数指针,所以这是一个函数指针数组

注意:在 c 语言中,void * 的指针不能解引用,也不能进行指针运算,因为不知道+1会跳多远,void * 指针表示无具体类型的指针,可以接收任意类型的地址,类似与 java 中的泛型,我们在使用 void *类型时只能进行强转

#include <stdio.h>

int add(int x, int y)
{
	return x + y;
}

int main()
{
	int (*pf)(int, int) = &add;//等价于  int (*pf)(int, int) = add;
	int m = (*pf)(4, 5); //等价于  int m = pf(4, 5);
	return 0;
}

对于一个函数来说,函数名代表的是函数的地址,& 函数名也是函数的地址,所以:

  • 函数名 == &函数名

并且对于函数指针来说可以不用取地址,直接引用即可,所以

  • int m = (*pf)(4, 5); 等价于 int m = pf(4, 5);

1.11 sizeof 和 strlen 关于数组和指针的习题

在做题前记住三个规则:

数组名代表的是首元素的地址,但是有两个例外:

  1. sizeof(数组名)

此时数组名表示整个数组,计算的是整个数组的大小,单位是字节

  1. &数组名

这里的数组名表示整个数组,取的是整个数组的地址

  1. arr [ i ] == *(arr + i)
int main()
{
	//一维数组
	int a[] = { 1,2,3,4 };
	printf("%d\n", sizeof(a));//16
	printf("%d\n", sizeof(a + 0));//4或者8(首元素的地址)
	printf("%d\n", sizeof(*a));//4(对地址解引用,即int的大小)
	printf("%d\n", sizeof(a + 1));//4或8(第二个元素的地址)
	printf("%d\n", sizeof(a[1]));//4 (第二个元素的大小)
	printf("%d\n", sizeof(&a));//4或8 (数组的地址,数组的地址也是地址,是地址就是4或8)
	printf("%d\n", sizeof(*&a));//16 *和&互相抵消了,相当于sizeof(a)
	printf("%d\n", sizeof(&a + 1));//4或8 &a表示数组的地址,+1跳过整个数组,但此刻还是地址
	printf("%d\n", sizeof(&a[0]));//4或8 首元素地址
	printf("%d\n", sizeof(&a[0] + 1));//4或8 第二个元素地址

	//字符数组
	char arr[] = { 'a','b','c','d','e','f' };//字符数组没有 /0
	printf("%d\n", sizeof(arr));//6 6个字符
	printf("%d\n", sizeof(arr + 0));//4或8 首元素的地址
	printf("%d\n", sizeof(*arr));//1 首元素大小
	printf("%d\n", sizeof(arr[1]));//1 第二个元素的大小
	printf("%d\n", sizeof(&arr));//4或8 数组的地址
	printf("%d\n", sizeof(&arr + 1));//4或8 数组的地址+1跳过整个数组,还是地址
	printf("%d\n", sizeof(&arr[0] + 1));//4或8 第二个元素的地址
	printf("%d\n", strlen(arr));//随机值,strlen的计算以 /0的出现结束
	printf("%d\n", strlen(arr + 0));//随机值
	printf("%d\n", strlen(*arr));//错误 strlen(地址)中需要的是地址
	printf("%d\n", strlen(arr[1]));//错误 a变成了b而已,strlen('a')的意思是将地址为97的数据进行传参
	printf("%d\n", strlen(&arr));//随机值
	printf("%d\n", strlen(&arr + 1));//随机值
	printf("%d\n", strlen(&arr[0] + 1));//随机值

	char arr[] = "abcdef";//a b c d e f /0   /0算一个字符
	printf("%d\n", sizeof(arr));//7
	printf("%d\n", sizeof(arr + 0));//4或8 首元素地址
	printf("%d\n", sizeof(*arr));//1 首元素大小
	printf("%d\n", sizeof(arr[1]));//1 第二个元素大小
	printf("%d\n", sizeof(&arr));//4或8 数组的地址
	printf("%d\n", sizeof(&arr + 1));//数组的地址+1跳过整个数组,还是地址
	printf("%d\n", sizeof(&arr[0] + 1));//4或8 第二个元素的地址
	printf("%d\n", strlen(arr));//6
	printf("%d\n", strlen(arr + 0));//6
	printf("%d\n", strlen(*arr));//错误
	printf("%d\n", strlen(arr[1]));//错误
	printf("%d\n", strlen(&arr));//6
	printf("%d\n", strlen(&arr + 1));//随机值
	printf("%d\n", strlen(&arr[0] + 1));//5

	char* p = "abcdef";//把a的地址放在了p中
	printf("%d\n", sizeof(p));//4或8 地址
	printf("%d\n", sizeof(p + 1));//4或8 地址
	printf("%d\n", sizeof(*p));//1 a的大小
	printf("%d\n", sizeof(p[0]));//1 p [ 0 ] == *(p + 0)
	printf("%d\n", sizeof(&p));//4或8 二级指针也是指针
	printf("%d\n", sizeof(&p + 1));//4或8
	printf("%d\n", sizeof(&p[0] + 1));//4或8 第二个元素的地址
	printf("%d\n", strlen(p));//6
	printf("%d\n", strlen(p + 1));//5
	printf("%d\n", strlen(*p));//错误
	printf("%d\n", strlen(p[0]));//错误
	printf("%d\n", strlen(&p));//随机值
	printf("%d\n", strlen(&p + 1));//随机值
	printf("%d\n", strlen(&p[0] + 1));//5

	//二维数组
	int a[3][4] = { 0 };
	printf("%d\n", sizeof(a));//48
	printf("%d\n", sizeof(a[0][0]));//4
	printf("%d\n", sizeof(a[0]));//16 a[0]是第一个一维数组的数组名  sizeof(数组名)
	printf("%d\n", sizeof(a[0] + 1));//4或8 第一行第二个元素的地址
	printf("%d\n", sizeof(*(a[0] + 1)));//4
	printf("%d\n", sizeof(a + 1));//4或8 第二行的地址
	printf("%d\n", sizeof(*(a + 1)));//16
	printf("%d\n", sizeof(&a[0] + 1));//4或8 第二行的地址
	printf("%d\n", sizeof(*(&a[0] + 1)));//16
	printf("%d\n", sizeof(*a));//16 a是第一行的地址,*a则是第一行
	printf("%d\n", sizeof(a[3]));//16

	return 0;
}
  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ice___Cpu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值