• 【C语言】指针初阶


     ✨个人主页: Anmia.
    🎉所属专栏: C Language

    🎃操作环境: Visual Studio 2019 版本


    1.指针是什么?

    指针理解的2个要点:

    1. 指针是内存中一个最小单元的编号,也就是地址
    2. 平时口语中说的指针,通常指的是指针变量,是用来存放内存地址的变量

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

    内存

      

    通过上面我们可以了解到指针就是地址。那么每个地址在内存中的大小是1byte,每个地址都有属于自己独一无二的编号。如上图:0xFFFFFFFF...(16进制)。

    • 我们可以通俗的理解为,内存是一个大酒店,每个地址(指针)都是酒店中的一个小房间,它们都有自己专属的房间号。

    指针变量

    我们可以通过&(取地址操作符)取出变量的内存起始地址,把地址可以存放到一个变量中,这个变量就是指针变量

    1. #include
    2. int main()
    3. {
    4. int a = 10;
    5. int* p = &a;
    6. return 0;
    7. }

    int a=10;//在内存中开辟一块空间

    int* p=&a;//这里我们对变量a,取出它的地址,可以使用&操作符。

    a变量占用4个字节的空间,这里是将a的4个字节的第一个字节的地址存放在p变量中,p就是一个之指针变量。

        

    • 首先,定义的局部变量在栈区,先定义,后分配(栈:先进后出的数据结构)
    • 先定义的先入栈(在栈内不分配空间),因此a在栈底p在栈顶。
    • 出栈的时候地址先分配给p,后分配给a,因而a的地址比p的大,如下图(16进制下)

    • 很明显,a的地址大于p的地址。

    总结:
    指针变量,用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
    那这里的问题是:

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

    经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的
    对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0);
    那么32根地址线产生的地址就会是:

    这里就有2的32次方个地址。
    每个地址标识一个字节,那我们就可以给

    (2^32Byte == 2^32/1024KB ==2^32/1024/1024MB==2^32/1024/1024/1024GB == 4GB) 4G的空闲进行编址。

    上面的int a = 10;为例整型在32位架构下是4个字节,会在堆区中申请4个字节的空间。

    • 同样的方法,那64位机器,如果给64根地址线,那能编址多大空间,自己计算哈。
    • 对于一个64位的机器,如果给出64根地址线,它可以编址的空间大小是2的64次方。由于每根地址线可以表示2个不同的状态(0或1),所以64根地址线可以表示2的64次方个不同的地址。这意味着该机器可以编址的空间大小为18,446,744,073,709,551,616个地址,或者约为18.4亿亿个地址。这是一个非常大的地址空间,远远超过目前大多数计算机系统所需的空间。

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


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

    总结:

    指针是用来存放地址的,地址是唯一标示一块地址空间的。
    指针的大小在32位平台是4个字节,在64位平台是8个字节。


    2.指针和指针类型 

    这里我们在讨论一下:指针的类型
    我们都知道,变量有不同的类型,整形,浮点型等。那指针有没有类型呢?
    准确的说:有的。

    1. int num = 10;
    2. p = #

    要将&num(num的地址)保存到p中,我们知道p就是一个指针变量,那它的类型是怎样的呢?我们给指针变量相应的类型。

    1. char* pc = NULL;
    2. int* pi = NULL;
    3. short* ps = NULL;
    4. long* pl = NULL;
    5. float* pf = NULL;
    6. double* pd = NULL;

    这里可以看到,指针的定义方式是: type + * 。

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

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

    如下代码,如果分别用int和char型对它进行操作,会有什么区别? 

    1. #include
    2. int main()
    3. {
    4. int a = 0x11223344;
    5. return 0;
    6. }

    int* p对其操作

    1. #include
    2. int main()
    3. {
    4. int a = 0x11223344;
    5. int* p = &a;
    6. *p = 0;
    7. return 0;
    8. }

    int指针对int变量进行操作

    char* p对其操作

    1. #include
    2. int main()
    3. {
    4. int a = 0x11223344;
    5. char* p = &a;
    6. *p = 0;
    7. return 0;
    8. }

    char指针对int变量进行操作

    通过上面两个视频,不难看出int型的指针可以一次操作4个字节,而char型的指针只能一次操作1个字节。所以指针类型必须对应指向的变量的数据类型。


    3.指针+-整数 

    用指针加减一个整数会怎么样

    1. #include
    2. int main()
    3. {
    4. int n = 10;
    5. char* pc = (char*)&n;
    6. int* pi = &n;
    7. printf("int型n的地址:%p\n", &n);
    8. printf("\n");
    9. printf("char指针指向int型变量的地址:%p\n", pc);
    10. printf("char指针指向int型变量的地址 +1 后:%p\n", pc + 1);
    11. printf("\n");
    12. printf("int指针指向int型变量的地址:%p\n", pi);
    13. printf("int指针指向int型变量的地址 +1 后:%p\n", pi + 1);
    14. return 0;
    15. }

    通过运行结果可以看出它们的首地址是一致的,但char指针对int变量操作的内存大小不一样,char指针操作int 变量一个字节的空间大小,而int 对int变量操作出4个字节的空间大小。

    • 总结:指针的类型决定了指针向前或者向后走一步有多大(距离),因此我们用指针+-整数时,我们应该要保证类型的对应。

    4.指针的解引用

    1. #include
    2. int main()
    3. {
    4. int n = 0x11223344;
    5. char* pc = (char*)&n;
    6. int* pi = &n;
    7. *pc = 0;
    8. *pi = 0;
    9. return 0;
    10. }

    上面用*对指针解引用操作,

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


    5.野指针

    概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的),因此野指针是我们编程需要避免的。

    为什么会出现野指针?

    1.指针未初始化

    1. #include
    2. int main()
    3. {
    4. int* p;//局部变量指针未初始化,默认为随机值
    5. *p = 20;
    6. return 0;
    7. }

    上面指针未初始化(没有任何指向的地方),这种代码是不好的。当我们需要用一个指针时,即使还没确定它要指向的地方时,可以用NULL对其赋值(初始化)。如下

    int* p = NULL;

    2.指针越界访问

    1. #include
    2. int main()
    3. {
    4. int arr[10] = { 0 };
    5. int* p = arr;
    6. int i = 0;
    7. for (i = 0; i <= 11; i++)
    8. {
    9. //当指针指向的范围超出数组arr的范围时,p就是野指针
    10. *(p++) = i;
    11. }
    12. return 0;
    13. }

    可以看到运行发生了异常,因为指针越界访问,因为指针只能对指向的内存空间进行操作,不可以操作自己没指向的区域,否则会发生异常!如图

    因为数组只有10个元素,*(p++)=i 每次对一个数组元素进行依次赋值操作,但问题就出现在for循环体的条件表达式i<=11,指针访问完数组继续访问不该访问的地方就会出现问题。

    3.指针指向的空间释放

    这里涉及动态内存开辟的时候讲解。


    那如何避免野指针的出现呢?

    总结为以下主要方法:

    1. 指针初始化
    2. 小心指针越界
    3. 指针指向空间释放即使置NULL
    4. 避免返回局部变量的地址
    5. 指针使用之前检查有效性

    前三种前面提及就不再讲解。

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

    1. #include
    2. int* test()
    3. {
    4. int a = 10;
    5. return &a;
    6. }
    7. int main()
    8. {
    9. int* p = test();
    10. printf("%d\n", *p);
    11. return 0;
    12. }

     这个代码是典型的错误案例。

    上面代码的意思是,在main函数中我们用指针p,来接收test函数的返回值。但我们用return来返回test函数中的变量地址,我们都知道局部变量在栈区,只要出了test函数就释放了,地址也就不存在了,因此指针p实际是不可能接收到地址值。

    但为什么在VS中运行不会出现问题呢?

    你目前只需要知道VS暂时帮你保留了值,为了避免代码出现异常。但只是注意只是“暂时”肯定不可能一直用下去吧?以后大工程函数之间互相调用,问题就大了。因此,我们要养好代码规范,不要返回局部变量的地址。


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

    1. #include
    2. int main()
    3. {
    4. int* p = NULL;
    5. int a = 10;
    6. p = &a;
    7. if (p != NULL)
    8. {
    9. *p = 20;
    10. }
    11. return 0;
    12. }

     上面代码用了if(p!=NULL)来验证指针是否有效也是其中一种方法,当然还有很多方法,需要根据实际情况使用。


    6.指针运算

    指针+-整数

    1. #define N 5
    2. float values[N];
    3. float* vp;
    4. #include
    5. int main()
    6. {
    7. for (vp = &values[0]; vp < &values[N];)
    8. {
    9. *vp++ = 0;
    10. }
    11. return 0;
    12. }
    上面代码的是利用for来依次访问数组values的地址,*vp++实际是*(vp++),但因为是后置++,所以先解引用赋值,后地址++,具体代码实现如下图。


    指针-指针

     下面我们来模拟实现一下strlen函数

    1. #include
    2. int my_strlen(char* s)
    3. {
    4. char* p = s;
    5. while (*p != '\0')
    6. p++;
    7. return p - s;
    8. }
    9. int main()
    10. {
    11. char str[10] = "Hello";
    12. int len=my_strlen(&str);
    13. printf("%d\n", len);
    14. return 0;
    15. }

    上面代码先写了一个my_strlen函数并传入一个字符串变量的地址,在my_strlen函数中,创建一个指向char类型的指针,把它指向传入的字符串地址。我们都知道char类型的指针一次只能操作一个字节的空间,一个字符刚好是一个字节的空间。因为while循环的意思是指针p依次访问字符串,如果*p=='\0'字符串结束字符,说明字符个数统计结束。最后返回p-s的值,p的值为字符串最后一个字母的地址,s的值是第一个字母的地址。

    所以我们记住结论:

    • 指针-指针的前提:两个指针指向同一块区域,指针类型是相同的。
    • 指针-指针的差值的绝对值,是指针和指针之间的元素个数。 

    当然上面的代码中while循环条件中的*p != '\0'可以改成*p,因为指针p在while循环中++最后会++到 '\0'这个位置,我们知道'\0'是转义字符"  \ddd类型 "因此它是八进制中的0,八进制中的0也是0,所以我们*p可以直接写成while循环的判断条件,当*p指向'\0'是相当于指向0,0为假,结束循环。

    \ddd可参考

    【C语言】初识C语言+进阶篇导读-CSDN博客中有讲解


    指针的关系运算  

    1. #define N 5
    2. float values[N];
    3. float* vp;
    4. #include
    5. int main()
    6. {
    7. for (vp = &values[N]; vp > &values[0];)
    8. {
    9. *--vp = 0;
    10. }
    11. return 0;
    12. }

    如上代码,vp开始指向values[5]的首地址,然后循环判断条件是vp>&values[0]的首地址时,vp先解引用赋值后地址 --。直到循环条件不成立后结束循环。

    我们假设一个数组元素是一个格子,每个元素前面的表示首地址,vp依次每次指向数组每个元素的首地址,然后修改它们的值为0,运行过程如图。


    那我把这个代码稍微改下,情况就截然不同了。

    1. #define N 5
    2. float values[N];
    3. float* vp;
    4. #include
    5. int main()
    6. {
    7. for (vp = &values[N_VALUES - 1]; vp >= &values[0]; vp--)
    8. {
    9. *vp = 0;
    10. }
    11. return 0;
    12. }

    这里vp开始指向values[4],当vp>=&values[0]则*vp=0,然后vp--,直到vp<&values[0]结束。

    看似好像没有什么问题,而且感觉比上面的代码更好理解。

    但这里有个反C语言标准的地方。

    C语言标准规定:

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

    上面代码运行过程如下:

     当vp指向values[0]的地址时,vp>=&values[0]循环条件成立,*vp=0;执行,vp--。但此时vp指向的是values数组前一块地址空间,我们把它叫做values[-1]吧,C语言规定是不允许与指向第一个元素之前的那个内存位置的指针进行比较。所以这个写法是不推荐的,实际在绝大部分的编译器上是可以顺利完成任务的,然而我们还是应该避免这样写,因为标准并不保证
    它可行。

    至于为什么它一定要这样,你可以简单的理解先,前面有着重要的数据,不能进行指针比较即可。


    7.指针和数组

    我们似乎感觉指针和数组有点像,容易混淆。通过下面例子就可以分清楚它们的区别了。

    • 指针就是指针,指针变量就是一个变量,用来存放地址,指针变量的大小是4/8个字节
    • 数组就是数组,用来存放一组数,数组的大小由数组元素的类型和个数决定的。

    数组名表示数组首元素的地址

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

     

     可以论证数组名就是数组首元素的地址,但也有2个例外

    1. sizeof(数组名),数组名单独放在sizeof内部,数组名表示是整个数组,计算的是整个数组的大小,单位是字节。
    2. &数组名,数组名表示整个数组,取出的是数组的地址,数组的地址和首元素的地址是一样的,但类型和意义不同。

    数组名的两个例外意义

    1. sizeof(数组名)

    1. int arr[10] = {0};
    2. printf("%d\n",sizeof(arr));

    可见此时数组名的意思因为sizeof的影响改变成,数组的大小。 

    2. &数组名

    我们来看看下面这段代码

    1. #include
    2. int main()
    3. {
    4. int arr[10] = { 0 };
    5. printf("%p\n", arr);
    6. printf("%p\n", arr + 1);//跳过4个字节
    7. printf("%p\n", &arr[0]);
    8. printf("%p\n", &arr[0] + 1);//跳过4个字节
    9. printf("%p\n", &arr);
    10. printf("%p\n", &arr + 1);//跳过40个字节
    11. return 0;
    12. }

    可见,&数组名,数组名此时 表示整个数组,取出的是数组的地址,虽然值和arr首元素一样但类型和意义完全不同。可以看下图

     因为类型和意义的不同,现阶段可以简单的理解为,arr是指向数组的首元素地址的,&arr[0]同理,但&arr是指向整个arr数组的,虽然他还是以首元素地址为值,但通过指针+1就可以看出区别所在了。

    既然可以把数组名当成地址存放到一个指针中,我们使用指针来访问一个就成为可能。

    1. #include
    2. int main()
    3. {
    4. int i = 0;
    5. int arr[] = { 1,2,3,4,5,6,7,8,9,0 };
    6. int* p = arr; //指针存放数组首元素的地址
    7. int sz = sizeof(arr) / sizeof(arr[0]);
    8. for (i = 0; i < sz; i++)
    9. {
    10. printf("&arr[%d] = %p  <====> p+%d = %p\n", i, &arr[i], i, p + i);
    11. }
    12. return 0;
    13. }

    所以 p+i 其实计算的是数组 arr 下标为i的地址。
    那我们就可以直接通过指针来访问数组。

    1. #include
    2. int main()
    3. {
    4. int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
    5. int* p = arr; //指针存放数组首元素的地址
    6. int sz = sizeof(arr) / sizeof(arr[0]);
    7. int i = 0;
    8. for (i = 0; i < sz; i++)
    9. {
    10. printf("%d ", *(p + i));
    11. }
    12. return 0;
    13. }


    8.二级指针

    指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里
    这就是 二级指针 。

    对于二级指针的运算有:
    *ppa 通过对ppa中的地址进行解引用,这样找到的是 pa , *ppa 其实访问的就是 pa .

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

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

    1. **ppa = 30;
    2. //等价于*pa = 30;
    3. //等价于a = 30

    9.指针数组

    指针数组是指针还是数组?
    答案:是数组。是存放指针的数组。
    数组我们已经知道整形数组,字符数组。

    1. int arr1[5];
    2. char arr2[6];

    那指针数组是怎样的?

    int* arr3[5];//是什么?

    arr3是一个数组,有五个元素,每个元素是一个整形指针。

    所以指针数组就是 存放指针的数组。


    本文简单入门指针的基础,后续将更新指针进阶内容。

  • 相关阅读:
    将本地文件夹添加到 Git 仓库
    Flink批处理HDFS文件(带完整源码,直接可使用)
    LLM大语言模型训练中常见的技术:微调与嵌入
    MongoDB备份与恢复
    React技巧之表单提交获取input值
    Shopro商城 高级版 Fastadmin和Uniapp进行开发的多平台商城(微信公众号、微信小程序、H5网页、Android-App、IOS-App)
    LeetCode:两数之和
    Linux进程地址空间
    二进制部署1.23.4版本k8s集群-2-安装DNS服务
    时代落在英伟达身上的是粒什么沙,国产GPU的机会又在哪?
  • 原文地址:https://blog.csdn.net/weixin_59511824/article/details/132719311