【C语言】从入门到入土(指针篇)

本文详细介绍了C语言中的指针概念,包括指针的定义、运算、类型、野指针的避免以及指针与数组的关系。通过实例解析了指针的解引用、指针运算以及二级指针的使用。同时,探讨了指针数组的定义和操作。文章旨在帮助读者深入理解C语言中指针的精髓。
摘要由CSDN通过智能技术生成

前言:
本篇为你介绍什么是指针以及指针的基本运用,让你更了解什么是指针,指针的运算,指针与数组,二级指针等

指针,是C语言中的一个重要概念及其特点,也是掌握C语言比较困难的部分。

指针也就是内存地址,指针变量是用来存放内存地址的变量,不同类型的指针变量所占用的存储单元长度是相同的,而存放数据的变量因数据的类型不同,所占用的存储空间长度也不同。有了指针以后,不仅可以对数据本身,也可以对存储数据的变量地址进行操作。

一. 指针是什么

在计算机科学中,指针(Pointer)是编程语言中的一个对象,利用地址,它的值直接指向(points to)存在电脑存储器中另一个地方的值。由于通过地址能找到所需的变量单元,可以说,地址指向该变量单元。因此,将地址形象化的称为“指针”。意思是通过它能找到以它为地址的内存单元。

我们可以这样理解:

这里是编译器里面的一个存储数据的内存,然后我们把他分为若干个字节,每一个字节叫做一个内存单元。每一个字节进行一个编号,这个编号是唯一跟这个单元匹配的,用这个编号就可以找到这个内存单元。

也就是说:地址指向了一个确定的空间,所以地址形象的被称为指针。

那既然存储的内存单元是有编号的,我们来看看下面的代码:

int main()
{
	int a = 10;
	//在内存中开辟一块空间存放a的值
	//a的地址:0x00CFFC6C

	int * pa = &a;
	//pa是用来存放地址的,所以pa是指针变量。

	return 0;
}

上面的代码中,我们先创建变量a,也就是在内存中开辟一块空间存放a的值,而&符则可以取出该变量的地址,然后把a的地址存到pa里面去,说明pa就是一个指针变量。

所以总的来说:

指针就是变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。


那么这里存在两个问题:

  1. 一个小的单元到底是多大?(1个字节)
  2. 如何编址?

上面我们说到,指针是存放地址的变量,那一个地址多大呢,又有多少地址呢,编址能拥有多大的空间呢,这就是我们接下来要研究的问题。

我们知道,机器有32位的,也有64位的。那32位的机器有32根地址线,然后通电后的高电频低电频由电信号转化为数字信号,这个数字信号就是1或者0,而32根地址线产生32个1/0。

那一个内存单元多大才合适呢,这里我们有bit,byte,kb,mb,gb,tb等供您选择,我们先试试最小的bit合不合适,2的32次方bit化为gb就是0.5gb,一共大小才0.5gb,不合适吧。经过仔细的计算和权衡我们发现一个字节(byte)给一个对应的地址是比较合适的!

每一个地址一个字节,那么就有4GB的空间来进行编制,对于计算机来说是足够的了。同样的方法,64位机器,如果给64根地址线,那能编址多大空间,有兴趣的小伙伴可以自己计算一下。

所以总结起来就是:

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

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

3.指针的大小在32位平台是4个字节,在64位平台是8个字节。


二. 指针和指针类型

首先我们知道变量有不同的类型,整形,浮点型等,那指针也有不同的类型吗,答案是:有的

我们来看一下这个代码:

int main()
{
	int a = 10;
    p = &a;
    //这样的代码可不可行?
	return 0;
}

答案是不可行的,因为p根本没有定义。我们现在是想用p来存储a的地址,所以p也要带上他自己的类型,也就是int * p = &a;

p就是一个指针变量,也有自己的类型,指针变量的类型:

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

我们可以发现:指针的定义方式是: type + * (类型+星号)。 其实: char* 类型的指针是为了存放 char 类型变量的地址。 short* 类型的指针是为了存放 short 类型变量的地址。也就是说存储什么变量类型就用什么指针变量类型。



那指针的类型的意义又是什么呢?

1.指针±整数

我们来看下面一段代码:

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;
}

这一段代码的意思是:创建一个int变量,用char * 类型去存储他的地址,和用int * 去存他的地址,当打印地址时,观察他们的区别。我们来看看结果:

我们可以观察到,n的地址是00CFFD0C(当然每一次运行的时候都有可能不一样),然后我们创建的char * pc指针变量存储n的地址,这里因为类型不同在&n前面加了强制类型转换,而int * pi同样也是存储n的地址。然后我们发现,pc跟pi存的地址打印出来的时候都是一样的,说明存储这个过程是可以进行的,但当pc和pi加1后,就产生了差异。

这里就涉及到指针 ± 整数的意义了,指针在 ± 整数的时候,实际上就是往下一个地址去,那作为指针(地址)我们知道是有大小的,比如存一个int类型的变量的地址要4个字节,所以我们在指针±时,也是要一个这样的大小。比如上面的代码中,char类型的pc+1,地址在变动了一个字节,而int类型的pi+1,就跳过了四个字节。

总的来说就是:

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

2.指针的解引用

解引用过程中也是同样关乎到大小的问题,我们用代码来说明:

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

	*pc = 0;    
	*pi = 0;  
    //我们来观察解引用的时候会有什么区别。
	return 0;
}

我们来逐步观察这个代码的变化:

① 这里的第一步就是创建n变量,第二步创建char * pc存储n的地址,第三步创建int * pi 存储n的地址。这些都不重要,下面两条代码才是主要的区别,看他们值的变化。


② 在这里,第三步转换第四步的时候,实际上就是执行了*pc = 0的代码,然后* pc变为0,但是pc所存的地址也就是n的地址里面的内容却只变了高位的44,而不是将地址内的内容改为0。


③ 这里第四到第五步中,执行的是*pi =0的代码,然后* pi变成0了,而且pi所存的地址也就是n的地址里面的内容也变成了0。


说明了什么?

说明了在指针解引用操作的时候,你所存储地址的指针类型是多大,你能操作的地址就是多少,比如char *类型的指针,在解引用的时候操作的就是一个字节的内容,所以上面* pc改变内容的时候,只有一个字节的内容发生了变化,而* pi改变的时候可以将全部都改变,因为这个指针也是int 的类型。

所以总的来说:

指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。 比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

这就是指针类型的意义。

三. 野指针

1.什么是野指针?

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

2.为什么会有野指针呢?

野指针的成因是什么?其实野指针就是没有指向具体位置的指针。而成因主要有以下三种:

①指针未初始化

#include <stdio.h>
int main()
{ 
    int *p;
 //局部变量指针未初始化,默认为随机值
 
    *p = 10;
 return 0;
}

我们创建指针变量,通常会指向某一个确定的变量的地址,但是直接创建指针变量却不初始化,那这个指针就如同无家可归的孩子,只能随机找一个地方呆了。然后给这个指针解引用赋值,也是赋值到了不知道何处。这就是其中一种野指针。

②指针越界访问

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

这里就是超出数组范围后,虽然指针指向一个地址本身是没有错的,但不能去改变里面的内容,这样子就是错误的了。当超出数组范围后,指针就是越界访问了,就不是一个正常的指针了。这就是第二种野指针。

③ 指针指向的空间释放

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

int main()
{
	int* p = fun();
	printf("%d\n", *p);
	return 0;
}

在这里我们创建一个p的指针变量,然后调用fun函数返回a的地址,但是注意,这里fun函数在返回地址后就会销毁,也就是说这个函数调用完之后推出就不见了,当我们的p指针变量去找他的时候,那里已经不是a确定的地址了,这是十分危险的。

但是我们运行后得出来*p的值仍然是10,但这并不是a的10,而是这个内存中这一个地址里面的数据没有变,p指针找过去得到的而已,所以这也不是一个确定具体的地址。当这个地址被覆盖的时候,得到就也就不会还是10了。这就是第三种野指针。

④如何规避野指针

1. 指针初始化

2. 小心指针越界

3. 指针指向空间释放即使置NULL

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

PS:如果不懂得函数创建销毁的,可以看一下【C语言】函数栈帧的创建与销毁这里面具体讲到了代码每一步运行是怎样的,基于栈帧的运行时,函数是怎么创建销毁的。


四. 指针运算

1.指针运算类型:

①.指针±整数
②.指针-指针
③.指针的关系运算

等等

看到第二点的时候就会有人觉得,指针有±整数,为什么第二点就只有指针-指针,而没有指针+指针呢?这是因为指针+指针,就是地址加地址,有什么意义呢,这如同日期-日期知道天数差距,日期+日期却没什么意义,所以我们不讨论指针+指针。

2.指针±整数

我们来看这一段代码:

#define N_VALUES 5
int main()
{
     float values[N_VALUES];
     float *vp;
     //指针+-整数;指针的关系运算
     for (vp = &values[0]; vp < &values[N_VALUES];)
    {
     *vp++ = 0;
    }
    return 0;
}

这里的#define N_VALUES 5是一个宏定义,就是不会改变的一个数值。然后我们创建一个values数组,代入了N_VALUES所以这个数组就是5个元素。然后for循环其实就是把0放进数组里面,因为是后置++,先放进去再++,所以就是能填满数组。

这里就有指针±整数了,指针±整数,实际上就是以该指针类型大小向后面的内存中划出一个指针类型大小,然后指向这一个地址。图示可能更容易理解:

这里虽然到N_VALUES ,但是并没有访问,所以这里并不是一个野指针。

3.指针-指针

对于指针-指针呢,我们看这个代码;

int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	
	printf("%d\n", &arr[9] - &arr[0]);
	printf("%d\n", &arr[0] - &arr[9]);
	return 0;
}

这里我们创建一个数组,然后地址-地址,也就是指针-指针,得到的是什么呢?我们来看一下结果:

得到的是他们之间的内存空间吗,其实并不是。结果是9和-9,这是什么意思呢,其实指针-指针的含义是这样的:

指针-指针,在满足两个指针指向同一块区域的前提下,得到的数字的绝对值是指针和指针之间元素的个数。

所以我们得到的是arr[0]到arr[9]之间的元素个数,也就是0-8这9个元素。

4.指针的关系运算

我们知道关系运算有等于、大于、小于、大于等于、小于等于和不等于六种。对于指针来说,等于和不等于就是判断两个指针的值是否相同或不同,即两个指针是否指向了相同或不同的地方。而大于和小于是判断指针的值哪个大哪个小。值较小的在存储器中的位置比较靠前,值较大的在存储器中的位置比较靠后。

我们用一个代码来说明:

#define N_VALUES 5
float values[N_VALUES];
float* vp;

int main()
{
    for (vp = &values[N_VALUES]; vp > &values[0];)
    {
        *--vp = 0;
    }

}

这里就是拿指针变量vp和数组比较,然后按照大到小依次把数组里面的值变为0,指针的比较就是指针的关系运算。

但比较的也有他的前提:

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

这就是指针的运算。


五. 指针和数组

数组名是什么?老规矩,上代码:

#include <stdio.h>
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9,0};
    printf("%p\n", arr);
    printf("%p\n", &arr[0]);
    return 0;
}

我们来观察一下,数组名和数组首元素地址有什么联系,结果:

我们会发现,数组名和数组首元素的地址是一样的,这可以让我们得出一个猜测的结论:数组名表示的是数组首元素的地址。

这个结论是不是正确的呢,我们来测试一下让数组名代替首元素地址看看得出的是否可以正常运行。

int main()
{
    int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
    int* p = arr; //指针存放的是数组首元素的地址
    int sz = sizeof(arr) / sizeof(arr[0]);
    for (int i = 0; i < sz; i++)
    {
        printf("&arr[%d] = %p   <====> p+%d = %p\n", i, &arr[i], i, p + i);
        //打印观察arr能不能作为首元素地址
    }
    return 0;
}

答案是可行的,当我们将arr表示的地址存储起来后,让指针变量p±整数时,变化的就是该数组的元素输出,那我们就可以直接通过指针来访问数组,所以arr所表示的地址确实为数组首元素的地址。

但是也有例外,

  1. sizeof(数组名) - 这里的数组名不是首元素的地址,是表示整个数组的,这里计算的是整个数组的大小,单位还是字节

  2. &数组名 - 这里的数组名不是首元素的地址,是表示整个数组的,拿到的是整个数组的地址。

    看一段代码:

int main()
{
	int arr[10] = { 0 };
	printf("%p\n", arr);//数组名是首元素的地址
	printf("%p\n", arr + 1);

	printf("%d\n", sizeof(arr));//第一个元素的地址

	printf("%p\n", &arr);//取出整个数组的地址,但打印的是首元素地址
	printf("%p\n", &arr + 1);

	return 0;
}

我们先看打印结果:

然后我们来分析一下:
① 数组名打印首元素地址,表示首元素,然后数组名+1,得到的是跳过同类型大小的地址,也就是数组下一个元素的地址,说明数组名确实表示数组首元素的地址。

② sizeof(数组名)所打印的是整个数组的大小,这里10个元素表示40字节。

③ 取地址+数组名打印的也是首元素地址,但他并不是表示数组首元素地址,在我们加一后,跳过的并不是一个元素的大小,而是整个数组的大小,说明取地址+数组名代表的是整个数组的地址。


六. 二级指针

c语言中有指针,那有没有二级指针呢,三级呢,答案也是:有的

首先我们知道,指针是用来存放变量的地址的,那指针是不是变量,指针变量,当然是变量,所以是变量就有地址,那指针变量的地址存放在哪里? 这就是 二级指针,那二级指针是不是变量,是变量,地址存储用什么,三级指针。(俄罗斯套娃又来了) 。

对于上面的二级指针的运算有:

1.三级操作二级

*p2 通过对p2中的地址进行解引用,这样找到的是 p1*p2 其实访问的就是 p1.

我们看这一个代码:

int main()
{
	int a = 10;
	int* p1 = &a;
	int** p2 = &p1;

	int b = 20;
	*p2 = &b;

	printf("%p\n", &a);
	printf("%p\n", &b);
	printf("%p\n", p1);
	return 0;
}

运行的结果为:

我们在一开始的时候是将a的地址存到了p1上去,然后再以*p2=&b去访问p1的地址把b的地址放进去了。所以 *p2 其实访问的就是 p1

2.三级操作一级

**p2 先通过 *p2 找到 p1 ,然后对 p1 进行解引用操作: *p1 ,那找到的是 a.

我们看这一个代码:

int main()
{
	int a = 10;
	int* p1 = &a;
	int** p2 = &p1;

	**p2 = 30;
	printf("%d\n", a);
	//答案是30还是10?
	return 0;
}

运行结果:30

这里就是说明其实三级指针也是可以二次解引用,访问a的地址,然后去操作a地址上的内容,这里**p2也可以想象成*(*p2),*p2就是访问p1,所以化为*p1,而*p1访问的就是a的地址。 所以最终结果是30.


七. 指针数组

Q:指针数组是指针还是数组?

答案:是数组。是存放指针的数组。

数组我们已经知道有整形数组,字符数组等如:

那指针数组是怎样的?我们知道变量和指针都有不同类型,而同一种大小类型的变量和指针之间差一个 * 号,所以指针数组也和数组相似:

int main()
{
	int arr1[3];
	char arr2[5];

	int * arr3[3];
	char* arr4[5];

	return 0;
}

int * arr3[3];表示什么呢?

其实,这表示的意思就是arr3是一个数组,有五个元素,每个元素是一个整形指针。就如同上面的数组一样,一个数组,里面都是这个类型。

这几种数组或指针数组的意思和定义:

int main()
{
	int arr[10];
	//整型数组 - 存放整型的数组就是整型数组
	
	char ch[5];
	//字符数组 - 存放字符的数组就是字符数组
	
	//指针数组 - 存放指针的数组就是指针数组
	//int* 整型指针的数组
	//char* 字符指针的数组

	int* parr[5];
	//整型指针的数组
	
	char* pc[6];
	//字符指针的数组

	return 0;
}

好啦,本篇的内容就到这里,小白制作不易,有错的地方还请xdm指正,互相关注,共同进步。

还有一件事:

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

恒等于C

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

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

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

打赏作者

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

抵扣说明:

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

余额充值