当前位置:网站首页>C语言指针(上篇)

C语言指针(上篇)

2022-07-07 06:26:00 一只大喵咪1201

作者:一只大喵咪1201
专栏:《C语言学习》
格言:你只管努力,剩下的交给时间!
图

指针是什么?

C语言中的指针相信大家都听说过,那么它到底是个什么东西呢?
先看一张图:
图
这张图中有很多框框,像是一栋楼,一层接着一层,它就是内存,里面存放各种各样的数据。而且每一层都是有编号的,就像是门牌号一样,不同的门牌号就是不同的内存空间的地址,通过地址我们就能找到对应内存空间中的数据。那么一个内存空间的大小是多少呢?

以32位的机器为例:
32位的机器就有32根地址线(64位的机器有64个地址线),每根地址线都有两种电平状态,高电平和低电平,高电平用1表示,低电平用0表示。
那么32个0和1的组合就是232种。
每个空间的大小是1个字节B,那么就需要
232B÷210=222KB
222KB÷210=212MB
212MB÷210=4GB
只需要给4GB大小的空间用来编址就可以。
各位小伙伴可以试试当一个内存空间的大小不是1个字节的时候需要多大的空间用来编制,你会发现,只有一个内存空间大小是1个字节的时候是最合理的。

通过上诉的计数和分析,我们知道一个内存空间的大小就是1个字节,这样的分配在使用起来也会非常的方便。
我们已经了解了内存,那么指针到底是什么呢?

指针就是用来存放内存空间的地址的,它存放的是内存最小单元的编号。

注意:

  1. 按照严格定义,地址就是指针,指针变量才是用来存放地址的,也就是指针变量是用来存放指针的。
  2. 我们通常口语中说的指针其实是指指针变量。

知道了指针是什么后,那么它是怎么使用的呢?

int a = 10;
int* p = &a;
printf("%p\n", p);

通过操作符&,将创建的int类型的变量a的地址取到,然后再赋值给指针变量p,我们可以通过指针变量中的地址找到对应内存空间中的数据。

指针变量的大小

我们看一下上面程序中变量a的地址
图
它的地址是0x010FFC24,这是以16进制形式表示的二进制序列,是由32个0和1组成的

而这32个比特位需要4个字节大小的空间才能放的下,所以指针变量的大小就是4个字节。
在32位的机器上,指针变量的大小是4个字节,而在64位机器上指针变量的大小就是8个字节。

指针和指针类型

我们知道,变量有不同的类型,有int类型,char类型,double类型…,那么指针变量有没有类型呢?答案是有的
当我们创建了几个变量:

int a = 10;
char b = 20;
float c = 3.14;
double d = 2.456;

我们知道,不同的变量类型它的大小也不一样,也就是它在内存中所占的字节数不同。
如果指针变量没有类型,我们在访问指针变量中地址所指向的数据的时候就不会知道这个数据是什么样的类型,此时编译器就凌乱了,它不知道该取几个字节空间中的数据。
所以我们要让编译器明白,指针变量中的地址所指向的内存空间中放的是什么类型的数据。

int* pa = &a;
char* pb = &b;
float* pc = &c;
double* pd = &d;

指针变量的创建:
拿int* pa = &a;为例
这个语句可以写成int * pa = &a;
等号的左边可以分为3个部分,int,*号,pa
*号代表这是一个指针变量,pa是指针变量的名字,int是表示这个指针变量所指向的数据类型是int类型的。

指针类型的意义

已经确切的知道了指针变量是由类型的,那么它存在的意义到底是什么呢?

int main()
{
    
	int a = 10;
	int* pa = &a;
	char* p = (char*)&a;//将变量a的指针强制类型转换成char类型的指针变量

	printf("&a = %p\n", &a);

	printf("pa = %p\n", pa);
	printf("pa + 1 = %p\n", pa + 1);

	printf("p = %p\n", p);
	printf("p + 1 = %p\n", p + 1);

	return 0;
}

我们创建一个变量a,将它的地址取出来,分别放在int类型的指针变量和char的指针变量中,然后对其进行加1操作。
图
可以看到,int类型的指针变量在加1后,它的地址增加了4个字节,而char类型的指针变量在加1后,它的地址只增加了1个字节,同样是变量的a的地址,为什么在加1后的结果不同呢?
图
我们将内存的布局横着画出来,左边是低地址,右边是高地址,变量a的值存在内存中。

指针变量pa和p指向的都是变量a的地址,如上图中的绿色箭头和蓝色箭头。
将这俩个指针变量进行加1操作后
由于指针变量pa是int类型的,此时编译器认为它所指向的数据就是int类型的,大小是四个字节,所以将pa+1后它跳过了一个int类型,也就是4个字节,指向了下一个int类型数据。
由于指针变量p是char
类型的,此时编译器认为它所指向的数据就是char类型的,大小是一个字节,所以将p+1后它跳过了一个char类型,也就是1个字节,指向了下一个char类型数据。

加减整数的操作是相同的道理,无非就是跳过了几个指针变量所指向数据的类型。
所以我们说,指针变量类型的意义是:指针变量类型决定了指针变量访问时的步长。

接下来我们对上面的指针进行解引用操作

int main()
{
    
	int a = 0x11223344;
	int* pa = &a;
	char* p = (char*)&a;//将变量a的指针强制类型转换成char类型的指针变量

	printf("*pa = %x\n", *pa);
	printf("*p = %x\n", *p);

	return 0;
}

我们创建一个变量a,里面放的是16进制数字11223344。
代码运行结果
图
我们可以看到,对int类型的指针变量解引用得到的结果是11223344,对char类型的指针变量接引用得到的结果是44,这是为什么呢?
由于VS2019是小端字节序存储方式(详情请移步数据在内存中的储存),所以他在内存中的样子是
图

此时指针变量pa和p指向的都是变量a的地址
当对指针变量pa和p解引用时
指针变量pa是int类型,编译器认为pa指向的地址中存放的是int类型的数据,大小是4个字节,所以在解引用访问的时候,要访问的是一个int类型,也就是4个字节内存空间的内容。所以得到是11223344
指针变量p是char
类型,编译器认为p指向的地址中存放的是char类型的数据,大小是1个字节,所以在解引用访问的时候,要放的是一个char类型,也就是1个字节内存空间的内容。所以得到是第一个字节空间的内容,也就是44

所以我们说,指针变量类型的意义是:指针变量类型决定了解引用时访问的内存空间大小。

总结:
指针变量类型的意义有俩个

  1. 指针变量类型决定了指针的步长
  2. 指针变量类型决定了解引用时访问的内存空间大小

野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

野指针的成因

  1. 指针未初始化
int main()
{
    
	int* p;
	*p = 20;
	return 0;
}

此时的指针变量p是没有初始化的,所以它的值是随机的,也就是它所指向的地址是随机的。

  1. 指针越界访问
int main()
{
    
	int arr[10] = {
     0};
	int* p = arr;
	int i = 0;
	for (i = 0; i <= 12; i++)
	{
    
		*(p++) = i;
	}
	return 0;
}

当数组中的10个元素被赋值完后,循环还没有退出,当指针变量p指向第11个元素的时候,此时指针变量p就越界访问了,此时的指针变量p就是野指针。

  1. 指针指向的空间释放
int* test()
{
    
	int a = 0;
	return &a;
}
int main()
{
    
	int* p = test();
	printf("hehe\n");
	printf("%d\n", *p);
	return 0;
}

运行结果
图
此时对指针变量p解引用后的值是5,并不是在test函数中创建的a。

这是因为test函数中创建的变量a在函数执行完以后,就将变量a创建的内存空间释放了。
主函数中的pintf(“hehe”)语句将原本变量a的空间覆盖掉了。
所以解引用后是访问不到原本的变量a。
此时的指针变量p就是野指针。

规避野指针的方法

  1. 指针初始化
  2. 小心指针越界
  3. 指针指向的空间释放后,要即时置NULL
  4. 避免返回局部变量的地址
  5. 指针使用之前检查有效性
int main()
{
    
	//在不知道指针变量的初始化值时,要初始化为控制在NULL
    int *p = NULL;
    
    int a = 10;
    p = &a;
    //进行有效性检查
    if(p != NULL)
   {
    
        *p = 20;
   }
    return 0;
}

指针的运算

指针变量像其他变量一样是可以进行运算的。

  1. 指针加减整数的运算
int main()
{
    
	int arr[] = {
     1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = NULL;
	for (p = arr; p < arr + sz;)
	{
    
		printf("%d ", *p);
		p++;
	}
	return 0;
}

运行结果
图
上面程序是通过指针变量p加1来控制循环的进行,加减整数与加1是相同的道理。

  1. 指针减指针
#include <stdio.h>
#include <string.h>

int main()
{
    
	char arr[] = "abcdef";
	char* start = arr;
	char* end = arr;
	while (*end != '\0')
	{
    
		end++;
	}
	printf("strlen(arr) = %d\n", strlen(arr));
	printf("end - start = %d", end - start);
	return 0;
}

运行结果
图
可以看到,指针变量end减去指针变量start的值与数组中字符串的长度相同。
图

指针变量start指向的是’a’
指针变量end最终指向的是’f’,它的实质是start+6
所以相减后得到的结果是就是6

指针变量减指针变量得到的结果是俩个指针变量之间的元素个数。

  1. 指针的关系运算
int main()
{
    
	int arr[] = {
     1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	int* p = arr;
	for (; p < arr + sz; p++)
	{
    
		*p = 0;
	}
	return 0;
}

以上代码是将数组arr中的十个元素全部置为0。

其中,通过指针变量p与数组中最后一个元素地址之间的大小关系来控制循环的。

图

可以看到,arr+sz已经越界了,该地址是数组arr后面第一个int类型变量的地址,指针变量p是参照该地址控制循环的。

上面程序是按照从低到高的顺序挨个置0的,那么我们也可以按照从高到低是顺序来置0

int main()
{
    
	int arr[] = {
     1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);

	for (int* p = &arr[sz - 1]; p >= &arr[0]; p--)
	{
    
		*p = 0;
	}

	return 0;
}

这从高地址向低地址将数组中的元素置0的代码。
图

最终指针变量p会指向数组arr前面第一个元素的地址,此时便不符合控制条件,跳出循环

虽然这样写在大多数编译器下是可以实现的,但是还是要尽量避免这样来写,因为这不符合C语言标准

因为C语言标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

图

原网站

版权声明
本文为[一只大喵咪1201]所创,转载请带上原文链接,感谢
https://blog.csdn.net/weixin_63726869/article/details/125147392