初阶C语言-指针

1.指针是什么?

理解指针的两个要点:

1.指针是内存中一个最小单元的编号,也就是地址

2.口头语中说的指针,通常是指指针变量,是用来存放内存地址的变量

        总结:指针就是地址,口语中说的指针通常是指针变量。 

        用图理解如下:

        内存的最小单元是一字节,对每一字节去编号对应的就是指针(地址)。 

#include <stdio.h>

int main()
{
	int a = 5;//是向内存中的栈空间申请4个字节的空间,这4个字节用来存放5这个数值
	int* pa = &a;//pa存的是a的首地址(第一个字节的地址)

	return 0;
}

        这里指针变量pa存放了变量a的首地址0x00112233(假设是这个地址)。指针变量是用来存放地址的变量。(存放在指针中的值都会被当成地址处理)。

        所以这里的问题是:

一个小单元到底是多大?(1个字节)

如何编址?

         经过仔细计算和权衡我们发现一个字节给一个对应的地址是比较合适的。

        对于32位机器,假设有32根线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);那么32根地址线产生的地址就会是:

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

...

11111111 11111111 11111111 11111111

        这样就能找到我们的内存单元。一共2^32个地址,对应这么多个字节,所以2^32字节的空间是4GB空间。同理,如果是64位机器,那就是2^64字节空间。

        在32位的机器上,地址是32个0或者1组成的二进制序列,那地址就得用4个字节来存储,所以一个指针变量的大小就应该是4个字节

        在64位机器上,如果有64根地址线,那一个指针变量的大小就是8字节,才能存放一个地址。

         总结:指针变量是用来存放地址的,地址是唯一标识一个内存单元的;指针变量的大小在32位平台上是4个字节,在64位平台上是8个字节。

         注意:语法上:int* p和int *p都是可以的。int *p, *q;当这样连续定义好几个指针时,需要这样写,*q的*是不能省略的int* 也是,但最好分开定义,一行定义一个指针变量,分开初始化。

2.指针和指针类型

        目前我们对指针的应用大多停留在取地址&解引用*->

2.1指针类型 

        既然这些指针类型的大小都是4(x86)或者8(x64平台),那为什么不用一个通用性指针ptr_t p来代替这么多些个指针类型呢?C语言没有这样设计,是因为不同类型的指针是有区别的->

2.2指针的解引用操作

#include <stdio.h>

int main()
{
	int a = 0x11223344;
	int* pa = &a;
	*pa = 0;

	return 0;
}

        我们按F11调试看看这段代码在内存中发生了什么-> 

        我们关闭监视窗口打开内存,并显示4列->

        在地址那块我们能输入&a,就能出现对应的a的地址。我们能看到a初始化完,在内存中是这样存储的,我们再看*pa = 0会发生啥->

        这个*pa = 0操作把内存中的四字节空间全部变成了0。 

#include <stdio.h>

int main()
{
	int a = 0x11223344;
	/*int* pa = &a;
	*pa = 0;*/
	char* pa = &a;
	*pa = 0;

	return 0;
}

        char* 和int* 的大小都是同样大的,都能存放a的地址,那这样会发生什么呢->

        我们发现这次操作只改了这四个字节的一个字节。我们这两段代码唯一不同的地方就是一个是int*,一个是char*,在解引用的时候,一个是访问了四个字节,另一个是访问了一个字节。因此,指针类型是有意义的,指针类型决定了指针进行解引用操作的时候,访问几个字节。一个char*的指针在解引用的时候访问了一个字节。一个int*的指针在解引用的时候访问了四个字节。因此,如果我们想从某个地址向后访问一个字节就可以解引用char*,访问四个字节可以解引用int*。当我们想从某个地址向后访问两个字节的时候可以使用short*类型的指针。

        虽然有个小警告:&a的类型是int*的,=两边类型不一致,编译器弹出了警告,不想看到这个警告可以把&a强制类型转换为char*。 

#include <stdio.h>

int main()
{
	int a = 0;
	int* pa = &a;
	char* pc = &a;
	printf("pa = %p\n", pa);
	printf("pa + 1 = %p\n", pa + 1);
	printf("pc = %p\n", pc);
	printf("pc + 1 = %p\n", pc + 1);

	return 0;
}

         很明显,如果是一个int*的指针+1跳了四个字节,而char*的指针+1跳了一个字节。因为指针类型的不一样,导致+1出现不同的结果,int*的指针,指向的变量是一个int类型的变量,它+1就跳过四个字节的空间,char*的指针,指向的是一个char类型的变量,它+1就跳过了一个字节的空间。

        所以,指针类型是有意义的。指针类型决定了指针+1/-1跳过了几个字节。

char*的指针+1,跳过1个字节

short*的指针+1,跳过2个字节

int*的指针+1,跳过4个字节

double*的指针+1,跳过8个字节

         总结:指针的类型决定了指针向前或者向后走一步有多大(距离)。

         这样访问起来就比较舒服了,不管是解引用一次还是跳过一次,都是一个整形一个整形访问的。假设也有一个short*的指针也找到了首元素地址那个位置,这样访问两个字节和+1跳过两个字节就比较别扭了,就不合适,循环十次才访问了五个整形空间。拿一个int*的指针,循环十次,就能访问完。如果你就是想一个字节一个字节访问,那也可以拿char*指向那个地址,一字节一字节的访问也是ok的。想以什么方式去访问,就应该拿什么样的指针去访问。

         ->int*一次修改四字节空间。

        ->char*一次修改一字节空间。

        在内存中,只要我们得到一个地址,我们就能利用指针对其进行向前后者向后的访问。

3.野指针

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

eg:

#include <stdio.h>

int main()
{
	int* p = (int*)0x11223344;
	*p;

	return 0;
}

        这个0x11223344是一个随便捏造的地址,然后访问这段未知空间,这是不合适的,是很危险的!就像有人给你打骚扰电话一样。 

3.1野指针成因

1.指针没有初始化

#include <stdio.h>

int main()
{
	int* p;
	*p = 20;

	return 0;
}

        在vs2022下直接报错。 

        因为局部变量p未初始化,默认为随机值。 

2.指针越界访问

#include <stdio.h>
int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i = 0; i <= 10; i++)
	{
		//当指针指向的范围超出数组arr的范围时,p就是野指针
		*p = i;
		p++;
	}
	return 0;
}

        上述*p = i;p++可以写成*p++,因为优先级的原因,++先和p结合,所以是*(p++),假设p的值为0x1122344,*对后面(p++)这个表达式解引用,这个表达式的值是0x11223344,但先进行计算的是p++,此时p变成了0x11223348。也可以简单理解为先使用p再++。

3.指针指向的空间释放

#include <stdio.h>

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

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

	return 0;
}

         虽然报的是警告,但这里还是挺致命的,当test函数结束的时候,局部变量a就要被销毁,空间就会被回收,这个时候,访问一个被回收的空间就是野指针,为啥说它致命呢?当这个函数结束时,这块内存空间会被回收,但如果这时,这块空间又被申请了,你的p指针还指向那块空间,你以为还是a。有点像你去租房子,上一间租客还留着钥匙,万一发疯当自己房子,拿钥匙开门了,那不就完蛋了吗。

        动态内存开辟的时候会仔细讲解,现在只是提一下。

3.2如何规避野指针

1.指针初始化

2.小心指针越界

3.指针指向的空间释放,及时置NULL

4.避免返回局部变量的地址

5.使用指针之前检查有效性

         如果明确知道指针应该指向哪里,就指向正确的地址;如果不知道指针初始化什么值,为了安全初始化为空指针NULL(本质是0)。

         0作为地址时,用户程序是不能访问的!

#include <stdio.h>

int main()
{
	int* p = NULL;
	if (p != NULL)
	{
		//...
	}


	return 0;
}

        在使用指针之前可以检查指针的有效性。 至于为什么要增加一个宏定义NULL,是为了可读性,虽然在值上,二者是相等的并且int* p = 0;也没问题,但可读性没那么高,0可以表示的东西多多了。一但发现一个东西被赋值NULL那一定知道这个东西是个指针。

4.指针运算

1.指针+-整数

2.指针-指针

3.指针的关系运算

#include <stdio.h>

int main()
{
	int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
	int* p = arr;
	for (int i = 0; i < 10; ++i)
	{
		printf("%d ", *(p + i));
		//p指向的是数组首元素
		//p+i是数组中下标为i的元素的地址
		//p+i其实是跳过了i*sizeof(int)个字节
	}

	return 0;
}

        观察易得知: 

 arr == p;它两都是数组首元素的地址

arr+i == p+i

*(arr+i) == *(p+i) == arr[i]//都是数组第i个元素

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

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

        所以[]仅仅只是个操作符,他和+一样,支持操作数的交换律。同时也说明,arr[i]只是数组第i个元素的表示形式,编译器在处理的时候会转化为*(arr+i),可以简单理解为语法糖吧。 这就是数组第i个元素访问的本质——数组名是个地址,i是个偏移量。不信的话看看p[i];

         本质都是一个地址加一个偏移量就能得到一个新地址,对其解引用就能访问了。其实这里并不是教大家去学“茴”的四种写法,而是知道原来指针的偏移量和数组元素的访问是有关系的。其实还就是地址、偏移量、解引用等一系列操作。

        在指针的关系运算中->

#define N_VALUES 5
float values[N_VALUES];
float *vp;
for(vp = &values[N_VALUES]; vp > &values[0];)
{
    *--vp = 0;
}
for(vp = &values[N_VALUES-1]; vp >= &values[0];vp--)
{
    *vp = 0;
}
        实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免第二种写法,因为标准并不保证它可行。
标准规定:
允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存位置的指针进行比较。

         第二种写法,出循环的时候,指针已经指向了第一个元素之前的那个内存位置,并且是做了比较的。可能是因为(推测):当向内存申请40字节空间时,其实操作系统会多给你一块空间用来存你申请了多少字节空间。此时来说向后越界比向前越界相对安全。

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[9] - &arr[0]);

	return 0;
}

         指针-指针的前提是:两个指针指向同一块区域,指针类型也是相同的,得到的是指针和指针之间的元素个数。

         小地址-大地址为负的元素个数。

        之前在函数那章节写过两种模拟实现strlen,一种是计数器;另一种是递归初阶C语言-函数-****博客,现在利用这个特性可以模拟实现一下strlen->

#include <stdio.h>

size_t My_strlen(char* str)
{
	char* start = str;//记录初始位置
	while (*str != '\0')
		str++;
	return str - start;
}

int main()
{
	char arr[] = "abcdef";
	size_t len = My_strlen(arr);
	printf("%zd\n", len);

	return 0;
}

         '\0'也是一个字符,是八进制形式(\ddd),它的ascii码值是0。

5.指针与数组

指针就是指针,指针变量就是一个变量,存放地址,指针变量的大小是4/8字节

数组就是数组,可以存放一组数,数组的大小取决于元素个类型与个数

联系就是:

数组的数组名是数组首元素地址,地址是可以存放在指针变量里面中。

#include <stdio.h>

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

	return 0;
}

         绝大多数情况下,数组名表示数组首元素的地址,有两个例外->

1.sizeof 数组名,数组名单独放在sizeof内部,计算数组的大小,单位是字节。

2.&数组名,这里的数组名表示整个数组,取出的是数组的地址,数组的地址和数组首元素的地址值是一样的,但类型和意义是不一样的。类似char* p1 = 0x1122,int* p2 = 0x1122。

#include <stdio.h>

int main()
{
	int arr[10] = { 0 };
	printf("%zd\n", sizeof arr);
	printf("%p\n", arr);
	printf("%p\n", &arr);

	return 0;
}

         arr+1跳过的是数组一个元素的地址,&arr+1跳过的是arr一个数组的地址->

        这里也能说明二者的指针类型是不一样的。 

6.二级指针

        指针变量也是变量,也有自己的地址,于是我们能用指针变量去存指针变量的地址,这就是二级指针。 

#include <stdio.h>

int main()
{
	int a = 10;
	int* p = &a;
	int** pa = &p;
	

	return 0;
}

        p是指针变量,一级指针变量;pa也是指针变量,二级指针变量,还可以有三级,四级...指针变量。

7.指针数组

         指针数组是指针还是数组呢?答案是数组,类比字符数组,整型数组,字符和整型是类型,重点是数组。

字符数组——存放字符的数组        char arr[7];

整型数组——存放整型的数组        int arr[8];

->

指针数组——存放指针的数组        char* arr[7];or int* arr[7];等等

#include <stdio.h>

int main()
{
	//用指针数组来模拟二维数组
	int arr1[] = { 1, 2, 3, 4, 5 };
	int arr2[] = { 2, 3, 4, 5, 6 };
	int arr3[] = { 3, 4, 5, 6, 7 };
	int* arr[] = { arr1, arr2, arr3 };
	for (int i = 0; i < 3; ++i)
	{
		for (int j = 0; j < 5; ++j)
		{
			printf("%d ", arr[i][j]);
		}
		printf("\n");
	}

	return 0;
}

         这只是模拟二维数组,并不是真的二维数组,因为二维数组的内存分布是连续的,这个内存分布是不连续的。

上一篇:C++仿函数( 调用运算符重载)


下一篇:npm