C语言指针详解(初阶)

一.前言

在编写代码的过程中,难免要调用或访问内存,这时就需要一个媒介来帮助我们与内存产生联系。于是,先辈们创建了指针,指针的出现极大提高了程序员们的发挥空间和上限水平。可以说,一个优秀的程序员一定能够灵活、熟练地运用指针。但是,指针的复杂程度也是相当高,需要老铁们日后在编写代码的时候不断体会、加深理解才行。这期博客,庄哥主要介绍关于指针的初阶知识,目的是让老铁们了解指针,后续博客的内容会不断深入,由浅入深。

二.指针与指针变量的概念

2.1指针是什么

  • 字面意思:指针意思是指向目标空间的方针,可以类比生活中的方向标,帮助你找向目标空间。而在编程中,指针的作用是帮助你找向目标内存空间
  • 内涵:指针是内存中最小的单元编号,也就是地址

注意:口头上的指针,指的是指针变量,是用来存放地址的变量(下文的指针都是指地址,而不是指针变量)

2.2内存

在细说指针之前,我们必须先了解指针所指向的空间,也就是内存。

内存是电脑用来存储信息的部分,这部分空间被分为许多小单元,每一个单元的大小是一个字节。为了更好的管理这些内存空间,先辈们对这些单元进行编号,而这些编号就是对应单元的地址,所以一个地址对应着一个字节的内存空间。可以将内存类比成一栋楼,而楼中的每一个房间对应的房间号就是一个指针(地址),我们可以通过这个指针精准定位到内存空间的任意一个字节。

电脑分为32位和64位,其中32位机器有32根地址线,64位机器有64根地址线。每根地址线在寻找地址的时候,会产生高电平(代表1)和低电平(代表0)。在32位机器中,32根地址线会根据不同电平产生32个二进制位,也就是总共会产生2^32次方个内存地址。每个地址代表一个字节,我们就刚好可以给4GB的内存空间进行编址。同理,如果是64位机器,我们就可以给大约16GB的内存空间进行编址。

2.3指针变量

定义:用来存放地址的变量称为指针变量

例如:

#include<stdio.h>
int main()
{
	int a = 0;
	int* p = &a;//用取地址符号"&",取出a的地址
	            //p存储的是整型变量a的地址,所以它的类型为int*,这里的*表示p是个指针变量
	printf("%p", p);
	return 0;
}

运行结果为:0057FB18

这里需要注意:指针变量p存储的是变量a所占内存空间大小的第一个字节的地址。变量a为int类型,占用内存空间四个字节,但是变量p只存储第一个字节的地址,如下图所示

2.4指针变量的内存大小

指针变量的内存大小取决于当前机器是64位还是32位
在32位机器中,地址由32个二进制位组成。每1个二进制位占用1bite的内存空间,8个二进制位就占用一个字节空间,所以存储一个地址就需要4个字节的空间,即一个指针变量占用内存的大小为4个字节。同理,在64位机器中,存储一个地址需要8个字节的空间,即一个指针变量占用内存的大小为8个字节。

  • 易错点:一个地址空间可以存储一个字节的内容,而存储一个地址所需的内存大小需要4个或8个字节(示机器而定)

三.指针和指针类型

3.1指针类型

我们知道,变量有整型类型、字符类型、浮点型等等。根据不同类型的变量,也需要有不同类型的指针来存储它们的地址。于是,衍生出如下常见的指针类型:

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


那么为什么要区分指针变量的类型,这有什么意义吗?
下面我们就来一起探索一下吧!

3.2指针±整数

我们通过一段代码来进行学习:

#include<stdio.h>
int main()
{
	int a = 0;
	char* p1 = (char*) & a;
	int* p2 = &a;
	printf("%p\n", p1);
	printf("%p\n", p1 + 1);
	printf("%p\n", p2);
	printf("%p\n", p2 + 1);
	return 0;
}

运行结果:

p1指针加1后,向后走了1个字节;p2指针加1后,向后走了4个字节。而这里p1的指针类型为char,p2的指针类型为int。由此可知,指针的类型决定了指针加1或减1后,向前或者向后走一步有多大

  • short类型的指针加1后,向前走2个字节
  • char类型的指针加1后,向前走1个字节
  • int类型的指针加1后,向前走4个字节
  • double类型的指针加1后,向前走8个字节
    ……

3.3指针的解引用

将一个目标变量的地址存入指针变量当中后,又如何通过指针变量来寻找目标变量呢?
这就需要用到解引用符号“ * ”,使用格式:*p
注意:在声明指针变量的时候“ * ”的意义是说明这个变量为指针变量,而在之后对指针解引用中“ * ”的意义,是让指针变量通过地址找向目标变量,并与之产生联系。

我们同样通过一段代码来学习:

#include<stdio.h>
int main()
{
	int n = 0x0012ff40;
	char* p1 = (char*)&n;//这里(char*)是将此处地址从int*类型强制转换成char*类型
	int* p2 = &n;
	*p1 = 0;//重点观察调试的时候n的内存变化
	*p2 = 0;
	return 0;
}

刚开始n的存储内容如下图所示:

当运行*p1=0这条语句之后,首地址中存储的40变为00

当运行*p2=0这条语句之后,n的存储内容全部变成00

可见,指针的类型决定对指针解引用的时候有多大的权限(能操作几个字节)
例如:char的指针解引用就访问一个字节的空间,int的指针解引用就访问四个字节的空间。

四.野指针

概念:指针变量指向的目标是不明确的、随机的、没有明确限制的。

4.1野指针的产生原因

1.指针变量没有进行初始化

例如:

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

这里指针变量p未进行初始化,就对其进行解引用是错误的。

2.指针越界访问

例如:

#include<stdio.h>
int main()
{
	int arr[4] = { 1,2,3,4 },i=0;
	int* p = arr;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = 0;
	}
	return 0;
}

当p指向的目标超过数组的范围时,p就成为一个野指针。

3.指向的空间被释放

#include<stdio.h>
int* test()
{
	int a = 10;
	return &a;//这里虽然返回a的地址,但是a的空间已经被释放,还给操作系统了
}
int main()
{
	int* p = test();
	printf("%d", *p);
	return 0;
}

这段代码运行起来编译器不会报错,甚至还将结果打印出来

但是这段代码真的没有问题吗?
请老铁们再运行下面这段代码:

#include<stdio.h>
int* test()
{
	int a = 10;
	return &a;
}
int test1()
{
	int b = 100;
	return 0;
}
int main()
{
	int* p = test();
	test1();
	printf("%d", *p);
	return 0;
}

这段代码在之前的基础上加上了函数test1(),但是运行结果却发生了变化

这就涉及到计算机的内存管理。当栈帧的空间权限还给操作系统时,之前开辟的栈帧中的内容并不会发生变化,只有当再次使用这块空间的时候,才会修改之前的内容。所以,在对指针变量p解引用之前运行test1函数,对之前的栈帧内容进行重置,此时指针变量p指向的目标内容就发生了改变。第一种情况和第二种情况虽然都能打印出结果,编译器也没有报错,但是p指向的空间权限已经发生了改变,所以p是一个非常危险的野指针。

注:关于野指针产生原因还有动态内存被释放的情况,但是限于文章篇幅,庄哥后期讲动态内存管理的时候会进行补充。

4.2如何规避野指针

  • 1.对指针进行初始化(未知其指向则初始化为NULL)
  • 2.小心指针越界
  • 3.指针指向的空间被释放后,及时置NULL
  • 4.避免返回局部变量的地址
  • 5.指针使用之前检查能否使用

当我们遇到一个指针不知其指向的时候,将其初始化为NULL

#include <stdio.h>
int main()
{
    int *p = NULL;//未知指向初始化为NULL
    int a = 0;
    p = &a;//明确初始化
    if(p != NULL)//为空指针不访问(无效指针)
   {
        *p = 20;//不为空再访问
   }
    return 0; 
}

五.指针和数组名

联系:在数组的章节中,我们提到数组名代表数组首元素地址(但有两个例外,这里不再赘述),那么我们是否能够将数组名中的地址存入指针当中,用指针±整数来遍历数组呢?

来看下面这段代码:

#include <stdio.h>
int main()
{
	int i = 0 , arr[5] = { 0 };
	int* p = arr; //指针存放数组首元素的地址
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = 0; i < sz; i++)
	{
		printf("&arr[%d] = %p <====> p+%d = %p\n", i , &arr[i], i , p + i);
	}
	return 0;
}

运行结果如下:

可以看到,&arr[i]与p+i中存入的地址是一模一样的。其实,arr[i]的本质就是arr+i,只不过这里将arr的地址存入指针变量p当中,用p来代替arr,但结果是一样的。所以,我们可以通过p+i来遍历数组。

六.指针的运算

6.1指针的关系运算

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

例如:通过指针来打印数组

#include<stdio.h>
int main()
{
	int arr[5] = { 9,8,7,6,5 };
	int* p = NULL;
	//1.最后一个元素的地址与&arr[4]+1相比较
	for (p = arr; p <= &arr[4]; p++)//p最后会指向数组最后一个元素后面那个内存位置,此时它会与数组最后一个元素的地址相比较
	{
		printf("%d ", *p);
	}
	printf("\n");
	//2.第一个元素的地址与&arr[0]-1相比较
	for (p = &arr[4]; p >= &arr[0]; p--)//p最后会指向数组第一个元素之前的那个内存位置,此时它会与数组第一个元素的地址相比较
	{
		printf("%d ", *p);
	}
}

虽然,第两种写法编译器没有报错,并且能运行出结果,但是,这种写法并不符合C语言的标准规定,在少数编译器上是不能通过的。所以,这里不建议使用第二种写法。

6.2指针-指针

指针和指针相减的绝对值为两个指针之间的元素个数
注:(1).两个指针相减的前提是,它们指向同一块连续的空间
    (2).指针+指针没有意义

#include<stdio.h>
int main()
{
	
	int arr[10] = {0};
	printf("%d\n", &arr[9] - &arr[0]);
	printf("%d\n", &arr[0] - &arr[9]);
	//下面两个指针指向的空间不连续	
	int a = 10;
	char c = 'w';
	printf("%d\n", &a - &c);//err
	return 0;
}

七.多级指针

7.1二级指针

指针变量也是变量,是变量就有地址,那么指针变量的地址又存放在哪里呢?
这就是我们接下来要将的二级指针,我们先通过一张图来理解。

从图中可以看到,二级指针变量中存放了一级指针变量的地址。在变量声明方面,二级指针的类型从一级指针的int转变成了int *,这也恰巧说明了指针变量ppa指向的目标是指针变量。在使用方面,二级指针解引用一次就会根据存储的地址找向目标变量,但是这个目标是指针变量,要想改变最初变量的数值,必须再次解引用。

7.2多级指针

多级指针其实就是二级指针的拓展,原理都是一样的,不过是对原来变量地址进行多层包装罢了,以后用到多级指针的机会也很少,这里直接通过一段代码来进行理解。

#include <stdio.h>
int main()
{
    int a = 0;                        // int 
    int *oneStar = &a;                  // int *
    int **twoStar = &oneStar;           // int **
    int ***threeStar = &twoStar;        // int ***
    int ****fourStar = &threeStar;      // int ****
    int *****fiveStar = &fourStar;      // int *****
    printf("a = %d\n", *****fiveStar);  // 五次取值,还原为int
    return 0;
}

八.指针数组

8.1指针数组的概念

指针数组是一个数组,只不过里面的元素类型都是指针(指针变量也行,因为指针变量里存放的也是地址)

请各位老铁判断下面哪个是指针数组:

#include<stdio.h>
int main()
{
	int arr[5] ;//1
	int(*arr)[5];//2
	int* arr[5];//3
}

答案是第三个,因为[ ]的优先级比“ * ”要高,所以arr先跟[ ]结合形成一个数组,再跟“ int* ”结合说明里面的元素是整型指针。
注:第二种是数组指针,这部分内容放在下一篇博客进行讲解。

8.2指针数组的应用

下面请老铁们用指针数组模拟一下二维数组

#include<stdio.h>
int main()
{
	int i = 0,j = 0;
	int arr1[5] = { 1,2,3,4,5 };
	int arr2[5] = { 6,7,8,9,10 };
	int arr3[5] = { 11,12,13,14,15 };
	int* arr[3] = { arr1,arr2,arr3 };
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 5; j++)
		{
			printf("%d ", arr[i][j]);//实质上是*((*(arr+i))+j)
		}
		printf("\n");
	}
}

运行结果:

怎么样老铁们做对了吗,下面附上图解

九.总结

好了,本篇文章的内容到此就结束了。关于指针的内容庄哥分3篇博客进行讲解,这是第一篇,后续还有进阶部分和习题。指针是C语言的一大难点,但是学好指针是成为一名优秀程序员的第一步,庄哥希望各位老铁能够耐下心来,打好基础,在编程之路上越走越远;也希望老铁们能够指出本文不足之处,庄哥一定听取您宝贵的意见!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值