【C指针详解】初阶篇

举报
YIN_尹 发表于 2023/08/04 10:17:49 2023/08/04
【摘要】 1.什么是指针要认识指针,首先我们要知道什么是内存。1.1内存与地址内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。那这些编号是怎么产生的呢?在32/64 位平台上,就有32/64根地址线,这...

1.什么是指针

要认识指针,首先我们要知道什么是内存。


1.1内存与地址

内存是电脑上特别重要的存储器,计算机中程序的运行都是在内存中进行的 。

所以为了有效的使用内存,就把内存划分成一个个小的内存单元,每个内存单元的大小是1个字节。

为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号被称为该内存单元的地址。


那这些编号是怎么产生的呢?


在32/64 位平台上,就有32/64根地址线,这些地址线是物理线,在通电之后,产生电信号(正电为1,负电为0),然后电信号再转化为数字信息,即32/或64位由0,1组成的二进制序列,每一个内存单元对应的二进制序列就是它的编号。

3fd89ce02a634cdbb38e6e0b8ab170a5.png

我们要知道,我们每定义一个变量,都需要内存给这个变量分配一块合适的空间,比如整型int分配4个字节,char分配1个字节,double分配8个字节。

变量是创建内存中的(在内存中分配空间的),每个内存单元都有地址,所以变量也是有地址的

取出变量地址如下:

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

de97753fdffa4447aa2937382b29a89a.png

191a4a24fa854ab9a863009fd053119f.png

1.2指针变量及其使用

(1)


既然变量的地址可以使用取地址操作符(&)取出,那可不可以把一个变量的地址存储起来呢?

当然可以!!!


在C语言,有一种专门用来存储地址的变量,叫做指针变量。

指针变量的定义方法:

类型 * 指针变量名;(*说明该变量是一个指针变量)


我们来演示一下:

int num = 10;
int *p;//p为一个整形指针变量
p = &num;

13a373692b244f85b4067e617b53d379.png

这样就把一个整型变量的地址放到了一个整型指针变量里边。


(2)


现在我们知道怎么把变量的地址存起来了,那么我们可不可以使用我们存起来的地址找到这个变量呢?

当然可以,就像你有了你一个朋友的住址,你就可以通过这个地址找到他家。


如果想通过指针找到这个变量,还要用到 * 这个操作符,在这里它叫做解引用操作符。


举个例子:

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

看这段代码,打印出来num的结果是几?

db0c280d3a9b441bb38209b51fddb46b.png


35d04c6f84ee4a9aa345cd03655678a6.png

以整形指针举例,可以推广到其他类型,如:

#include <stdio.h>
int main()
{
 char ch = 'w';
 char* pc = &ch;
 *pc = 'q';
 printf("%c\n", ch);
 return 0; 
}

569710a0d63d40ef8488a023f991d14e.png

1.3指针变量的大小

思考一个问题,整型变量的大小是4个字节,char类型1个字节,double8个字节,那么指针变量的大小是多少?不同类型的指针变量大小是不是也不一样呢?

我们就来测试一下:

#include <stdio.h>

int main()
{
    printf("%d\n", sizeof(char *));
    printf("%d\n", sizeof(short *));
    printf("%d\n", sizeof(int *));
    printf("%d\n", sizeof(double *));
    return 0; 
}

运行结果是啥?

7bf2f31b60da415f8582667a7e27eddb.png

为什么不同类型的指针变量大小是一样的呢?又为什么是4个字节呢?

原因是:


指针是用来存放地址的,所以指针变量的大小取决于地址的大小,而在同一平台上地址的大小是固定不变的。

32位平台下地址是32个bit位(即4个字节)

64位平台下地址是64个bit位(即8个字节)

在32 位平台上,内存单元的地址就是由32个1,0组成二进制序列构成的编号,那就是32个比特位,即4个字节。

同理,在64位平台上,64个0,1组成的二进制序列构成编号,那就是64个比特位,即8个字节。


我们来验证一下:

在32位平台上:

6f96da792581439eafe8d823ba427f8d.png

4个字节

64位平台上:

835f230445c54a0fb04222d805770283.png

8个字节

所以,我们得出结论:

指针变量的大小在同一平台是是固定的:

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

2. 指针及指针类型的意义

我们知道指针也有不同的类型:

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

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

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

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


我们知道在同一平台上,不同类型的指针大小其实是一样的,那为什么还要给指针分类型呢,或者说:

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


2.1指针的步长

我们一起来看这样一段代码:

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

大家分析一下结果可能是怎样的?

a6e93b0322e2422f871696428ff3d0b3.png

为啥是这样的结果呢?pc和pi都是指针变量,也都加的是1,为什么结果就不同了呢?

其实我们很容易就能分析出原因:


指针变量pc和指针变量pi的唯一区别就是两者的类型不同,pc是char类型的指针变量,存放的是字符变量(大小为1个字节)的地址;

而pi是int类型的指针变量,存放的是整型变量(大小为4个字节)的地址,

而结果pc和pc+1的差值恰好就是1;pi和pi+1的差值恰好就是4


所以我就可以得出指针类型的第一个意义就是:

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


char类型的指针+1就向后走1个字节

int类型的指针+1就向后走4个字节

double类型的指针+1就向后走8个字节

…以此类推(减也是同样的道理)


2.2 指针解引用的权限

一起来看一段代码:

#include <stdio.h>
int main()
{
 int n = 0x11223344;
 char *pc = (char *)&n;
 int *pi = &n;
 *pc = 0;   //重点在调试的过程中观察内存的变化。
 *pi = 0;   //重点在调试的过程中观察内存的变化。
 return 0; 
 }

我们先来分析一下这段代码是干啥滴:

844524b74b7a4ba69385dd01d5bf82e3.png

我们来调试运行一下观察内存中到底如何变化:

由于改的是同一个变量,所以我们分开看,先看pc:

8ea9fa6aa1bd476fb4acc30e5c9f4f7d.png

然后我们看一下Pi:70acda38a1d04764bde188eff15acab4.png

对比两个不同的结果,我们能够发现不同类型的指针变量解引用访问到的空间大小是不同的;


这也就是指针类型的第二个意义:

指针的类型决定了,对指针解引用的时候有多大的权限(能操作几个字节)。


比如:

char* 的指针解引用就只能访问一个字节,

而 int* 的指针的解引用就能访问四个字节

3. 野指针

什么是野指针呢?

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

3.1 野指针成因

1. 指针未初始化

看这样一段代码:

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

这里我们定义了一个整型指针变量P但没有给它初始化,所以它指向的位置是不可知的或是随机的,但我们直接对他进行了解引用,这样的操作可行吗?

我们运行一下看看:

2e09c220b3f8489194b8f93d110ebf9f.png

2. 指针越界访问

上代码:

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

这里我们把数组首元素的地址赋给了指针变量P,然后通过P来访问数组元素,但是我们很容易发现访问数组的时候越界了

我们来运行看看结果:

8ac11da4064c411783e523459680a946.png

我们会发现运行直接报错了,为啥呢?

还是因为野指针,我们通过指针来访问数组,当指针指向的范围超出数组arr的范围时,p就是野指针,对野指针解引用是不行的!!!

3. 指针指向的空间释放

这里放在动态内存开辟的时候讲解,先给大家简单提示一下:

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

int main()
{
    int* p = test();
    *p = 20;
    return 0;
}

大家看看这段代码,看看有没有什么问题:

ae8ffdc573fd4acf881b2d62a472dc1e.png

函数test返回a的地址放在指针变量p中,但函数调用结束时a的生命周期结束,为a开辟的空间被释放,归还给操作系统,此时再去解引用p访问这片空间,就非法访问了,这里的p也就成了野指针!

那这样的程序运行起来就一定是有问题的:

87581abe5b8d492f8d2f2f058c7a884a.png

3.2 如何规避野指针

那我们在平时写代码的过程中,如何无规避野指针呢?

1. 指针初始化


当我们定义一个指针变量时,就直接把我们想要获取的变量的地址赋给改指针变量;

如果我们不知道该给指针变量赋什么初值时,我们可以将指针变量置为空指针NULL(NULL是定义在头文件中的)

    int *p = NULL;
    //....
    int a = 10;
    p = &a;

2. 小心指针越界


比如当我们使用指针去访问或遍历一个数组的时候,我们一定要控制好指针访问的范围,让指针在数组开辟的空间范围内移动,避免指针越界造成野指针的出现。


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


如果一个指针变量指向的空间被释放掉了,那我们再去解引用该指针,就属于非法访问内存了,此时该指针变量就变成野指针了。


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


因为局部变量的作用域是自己所在的局部范围,一旦出了作用域,它的生命周期就结束了,为它开辟的空间也就归还给操作系统了,我们如果将它的地址返回给一个指针变量,再去解引用,那必然会出问题的!!!


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


在使用一个指针变量之前,我们可以先判断一下,它是否为空指针,不是空指针,我们才能放心的使用,如果它是空指针,我们直接解引用就会出问题!!!

    if(p != NULL)
   {
        *p = 20;
   }

做到以上几点,在很多情况下,我们就能很好的规避野指针了。


4. 指针运算

指针运算大致可以分为3中:


指针± 整数

指针-指针

指针的关系运算


4.1 指针±整数

通过上面指针步长的介绍,其实我们已经基本知道了指针加减整数时怎么回事了,再带大家巩固巩固,

这里举一个例子吧,我们实现一个函数通过指针遍历打印一个数组的元素:

void my_print(int* p, int len)
{
    int i = 0;
    for (i = 0; i < len; i++)
    {
        printf("%d ", *(p + i));
    }
}

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

其实这段代码就是通过指针±整数来实现对整个数组的遍历,p+i 其实计算的是数组 arr 下标为i的地址。

6306ca7aa1a94a8890c8a65d7c4bf0ba.png

当然我们不止可以一次加1,任意整数我们都可以加或减,当然前提是我们要控制好,不能越界访问

那我们运行看看能不能达到想要的结果:

414dd24acb75404d8b1063be1af51a5d.png

当然其它类型的指针变量都可以加减整数,不过不同类型的指针变量加减整数所走的步长不同,要根据类型决定


4.2 指针-指针

首先我们要知道,指针-指针的前提是:

两指针指向同一块空间

指向同一块空间的两个指针才能向减,如果一个指针P1指向数组arr1,另一个指针P2指向数组arr2,那P1和P2是绝对不能相减的。


2.那我们可能会有一些问题,指针-指针得到的是什么呢,指针+指针可以吗?

指针-指针得到的是两个指针之间的元素个数

指针不能+指针


指针-指针得到的是两个指针之间的元素个数,所以我们说两指针指向同一块空间,指向同一个数组的两个指针相减就能得到它们之间的元素个数,但若两指针分别指针指向两个数组,两个不同数组之间差的元素个数好像没法确定吧!

给大家做一个类比,帮助理解,指针-指针得到的是两个指针之间的元素个数,就好比两个日期相减可以得到它们之间差了多少天,但两个日期相加好像没什么意义吧!


我们举个实例,利用指针-指针来模拟实现库函数strlen:

int my_strlen(const char* s)
{
    const char* p = s;
    while (*p != '\0')
        p++;
    return p - s;
}

int main()
{
    printf("%d", my_strlen("abcdefg"));
    return 0;
}

50b8528591684bf5bc4c7328b6f7b1c8.png

f0f07e65cb444ede80389d256adbeb0c.png

4.3 指针的关系运算

指针的关系运算即指针之间进行大小关系的比较,作为判断条件

举个例子:

int main()
{
    int arr[5] = { 1,2,3,4,5 };
    int* p = NULL;
    for (p = arr; p < &arr[5];p++)
    {
        *p = 0;
    }
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

简单解释一下这段代码:

43817f891cf145d8a71b96d559744b9a.png

我们看看结果:

b92ab55fb68747a58f444358e3b53e4d.png

没有报错,而且达到效果了。

思考一下,这段代码是不是还可以这样写:

int main()
{
    int arr[5] = { 1,2,3,4,5 };
    int* p = NULL;
    for (p = &arr[4]; p >&arr[-1]; p--)
    {
        *p = 0;
    }
    int i = 0;
    for (i = 0; i < 5; i++)
    {
        printf("%d ", arr[i]);
    }
    return 0;
}

看出有什么区别了吗?

这次我们拿到数组最后一个元素的地址,让他与数组第一个元素前面的那个内存位置(&arr[-1])进行比较。

可以达到效果吗?

9dd891ddefc241c6844249ac533098b7.png这样好像也可以。

但是我们要注意:


这样写在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证它可行。


那标准怎么规定的呢?


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


5. 二级指针

我们现在已经知道了,指针是用来存放变量的地址的,那我们有没有思考一个问题:


指针变量也是变量,是变量就有属于自己的地址,那么指针变量的地址如果想存起来,应该放到哪里呢?——这就是二级指针!

int main()
{
    int a = 10;
    int* pa = &a;
    int** ppa = *pa;
    return 0;
}

9dbc1ac73b64416687aaa654eb5e8ac9.png

既然有二级指针,那二级指针也有地址,自然也有三级指针、四级指针…,当然,后面的我们就不常用了。

一级指针可以解引用,当然二级指针也可以:

int b = 20;
*ppa = &b;//等价于 pa = &b;

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

**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

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

以上就是对指针初阶内容的讲解,希望能帮助到大家,如果有写的不好的地方,欢迎大家指正!!!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。