C语言教程-13_1-初识指针

打印 上一主题 下一主题

主题 537|帖子 537|积分 1611


title: C语言教程-13_1-初识指针
tags: [C]
categories: C语言教程
description: 接触C语言的魂魄-指针


概要:

  • 扼要讲解内存地址与内存模子
  • 简朴介绍C语言的指针这一数据范例
  • 把握指针相关最基本的两种互逆运算
前置知识:

  • 理解能力和想象能力
  • 耐烦和实验精神
  • 数组与函数的知识
互换两个变量的问题

我们从一个问题开始引入指针.
考虑这个问题:在main()函数中有两个int变量a和b,我该如何互换这两个变量的值?
如果我们要求仅在一个函数中解决这个问题,那么很轻易想到,最简朴的办法就是新建一个int范例的中间变量,好比定名为temp.那么我们就有如下操作进行互换(非常简朴不详细表明):
  1. #include <stdio.h>
  2. int main() {
  3.     int a=3,b=4;
  4.     int c; // 中间变量
  5.     // 经典3步进行交换
  6.     c = a;
  7.     a = b;
  8.     b = c;
  9.     printf("a=%d,b=%d\n",a,b); // 输出结果 a=4,b=3   
  10.     return 0;
  11. }
复制代码
只需要这3步即可进行互换.

现在问题来了,如果我们要求创建一个函数swap()来实现这个操作,该如何实现?
也许我们可以这样:
  1. #include <stdio.h>
  2. void swap(int a, int b) {
  3.     int temp;
  4.     temp = a;
  5.     a = b;
  6.     b = temp;
  7. }
  8. int main() {
  9.     int a = 3, b = 4;
  10.     swap(a, b);
  11.     printf("a=%d,b=%d\n", a, b); // 输出错误的结果:a=3,b=4   
  12.     return 0;
  13. }
复制代码
我们实验简朴地把a和b传递给swap()函数,运行一下,效果显然是错误的,a和b的值并没有互换.
回顾前面函数的知识,前面讲过,C函数的参数都是按值传递,这里也就是将main()中的a和b的值简朴地复制给swap()的两个参数a和b,换句话说,此a,b非彼a,b.
效果就是,在swap()中的a,b确实被成功地互换了,但是main()中的a,b完全没有变革.

我们想要在swap()函数中互换main()中的a和b,根本的问题在于我们需要访问到他们,C语言的指针范例提供了这种功能.
地址和指针

计算机内存与地址

计算机运行时需要的各种数据都存储于内存中(就是寻常说的内存条),从逻辑上来看,一整个内存可以视为一个超级巨大的数组,例如我们的内存是4GB,那么这个数组的总大小就是4GB.我们仅仅讲解内存地址这个概念,详细的内存结构这里并不关心.
程序的相关数据就存储在内存中,例如实行的机器代码,局部变量,全局变量(定义在函数外的变量),常量字面值.他们以某种特定的模式进行存储,存储的位置各不相同,为了找到他们,我们需要以字节为单元为整个内存进行编号.也就是所谓的内存地址.需要注意的是,我们通常以16进制来表示地址值(究竟内存如此巨大,2,10进制是不够方便的).
以4GB内存举例,我们需要8个16进制位来完整编号,即16^8==4,294,967,296,也就是4GB的大小.从0开始,第一个字节编号为0x00000000,最后一个字节编号为0xFFFFFFFF.当然,如此巨大的内存范围不大概全部让我们恣意取用,现实上我们自己的应用程序只能使用操作系统(例如Windows)规定的一块内存,当然这完全够用.
例如,我们在内存地址0x2~0x8存储了一些特定的数据,内容如下:
可以看到每一个地址都指向内存中的一个特定字节,这个1字节大小的空间中存储了某些特定的数据,程序根据数据的地址从内存中找到他们,以进行运算.

另一方面,尽管我们对每一个字节都进行了编号,但是往往我们将多少个字节组合起来使用,例如一个int变量,就直接占用了4个字节,此时我们将4个连续的字节视为一个团体,取最开头的那个字节作为代表,指代整个int变量.
例如我们有int i=2;则i在内存中的结构如下:
注意,我们上面的地址值仅仅作为演示,在现在流行的x64机器中并不是这样的:
  1. #include <stdio.h>
  2. int main() {
  3.     int i = 2;
  4.     // 使用循环,逐字节输出i在内存中的值(十六进制),同时输出每个字节的地址
  5.     for (int j = 0; j < sizeof(i); j++) {
  6.         printf("0x%p %02x\n", (char *)&i + j, *((char *)&i + j));
  7.     }
  8.     return 0;
  9. }
复制代码
在我的条记本电脑运行如下:
先不管上面的代码是什么原理,仅仅看一下效果,输出了i占用的这4个字节内部的值,同时可以看到前面的内存是连续的.如果我们使用printf("%p",&i);输出i的地址,效果将会是第一个地址,这意味着使用最小的那个地址指代整个变量i.
读者无需关心为什么是02 00 00 00而不是00 00 00 02,这涉及到大小端序的问题.
处理地址-C语言的指针

现在我们已经相识了最基本的内存知识,并举了一个int变量的例子,相识了变量的存储.下面引入指针.
用最简朴的一句话概括指针就是:指针就是地址.我们有时间需要在程序中获取到某个变量的地址,C语言提供了指针这一数据范例,用指针范例声明的变量就叫指针变量,其内部存储一个无符号的整数(往往是4或8字节大小),代表一个地址.
顾名思义,指针,就像一个箭头指向一个地方,和地址的作用相同.只不外前面说的地址是指计算机内存的编址,而指针,是C语言为了能够处理内存地址而引入的一种机制.某种角度而言,地址值仍旧是一个整数,以是我们想要存储他,和平凡的整数无异,但是为了特殊化,C语言引入了指针范例这种数据范例,这种(类)范例的变量存储的是一个特殊整数代表一个指针(地址).

获取一个变量的地址非常简朴,使用&取地址运算符,输入一个指针也很简朴,在printf()中使用%p即可:
  1. #include <stdio.h>
  2. int main() {
  3.     int i = 3; // 声明并初始化为3一个int类型变量i
  4.     printf("%p",&i); // &i这个表达式代表获取到i在内存中的地址
  5.     return 0;
  6. }
复制代码
输出如下:
这代表着变量i就存储在这个地址值指向的内存块中.

如果我们想要将这个地址存储下来,那么就需要使用C语言的指针.
声明一个最简朴的指针变量仅仅需要在变量名前多加一个*,语法如下:
  1. #include <stdio.h>
  2. int main() {
  3.     int i = 3; // 声明并初始化为3一个int类型变量i
  4.     int *p; // 声明一个变量p,其类型为int*,代表它是一个指针
  5.     p = &i; // 将i的地址赋值给p
  6.     printf("%p",p); // 此时不再需要&,因为p存储的就是i的地址
  7.     return 0;
  8. }
复制代码
输出效果同样,也是一个地址.

再进一步,我们既然存储了某个变量的地址(指针),那么就意味着我们想要根据这个地址(指针)去访问其指向的内存单元,我们使用另一种运算符,即*解引用运算符(或者叫指针运算符),与&相反,*用于对一个地址进行访问,反向获取到此处存储的详细变量(值),这种相对于取地址操作的逆操作称为解引用操作.
仍旧是上面的例子,我们实验使用p存储的地址去间接访问变量i:
  1. #include <stdio.h>
  2. int main() {
  3.     int i = 3; // 声明并初始化为3一个int类型变量i
  4.     int *p; // 声明一个变量p,其类型为int*,代表它是一个指针
  5.     p = &i; // 将i的地址赋值给p
  6.     printf("%d",*p); // 此时对p的值进行*运算,也就是解引用,效果就是将p中的值作为一个
  7.     // 指针,去访问对应的内存,从而取出变量i的值
  8.     // 换句话说,这里的*p和i是等价的.
  9.     return 0;
  10. }
复制代码
运行效果:
注意:在对一个指针进行解引用时,一定要确保其指向了有用的地址!!!这是一个非常重要的问题!对未精确赋值的指针进行解引用(访问)是非常危险的行为.
指针声明的问题

我们在声明指针的时间,一定要注意和平凡范例进行区分.平凡范例与其对应的指针可以在一个声明中出现:
  1. int *p1,a,*p2;
复制代码
这里声明确2个int*范例的指针p1和p2,和一个int范例的变量a.
要注意的是,p2前面仍旧需要一个*代表它是一个指针,另外,尽管p1前面已经有了一个*,但是a仍旧仅仅是一个int的变量而已.
解决互换问题

现在我们可以实验使用指针进行互换两个变量的值.
我们既然要使用函数互换两个变量,那么就要求函数能够访问到这两个变量,现在,我们可以使用两个指针参数实现.
  1. // 在函数中使用指针进行交换两个变量的值
  2. #include <stdio.h>
  3. void swap(int *a, int *b) {
  4.     int temp = *a;
  5.     *a = *b;
  6.     *b = temp;
  7. }
  8. int main() {
  9.     int a = 10, b = 20;
  10.     printf("a = %d, b = %d\n", a, b);
  11.     swap(&a, &b);
  12.     printf("a = %d, b = %d\n", a, b);
  13.     return 0;
  14. }
复制代码
我们的swap()函数的两个参数不再是两个int范例的参数,而是int *范例的指针,代表着这个指针可以指向一个int变量.
main()函数中,swap(&a,&b);使用&取地址运算符计算a和b的地址,传递给swap的两个形参.
接下来,在swap()中使用一个中间变量(temp仍旧需要),进行互换,对指针变量使用*进行解引用,获取到两个要互换的int值,然后进行互换即可.
运行效果如下:


上面的例子我们相识了如下内容:

  • 如何获取一个地址(取地址运算符)
  • 如何存储一个地址(指针变量)
  • 如何使用一个地址去访问内存(解引用运算符)
接下来探究指针范例.
指针的范例

差别基本范例的指针

前面的例子都是使用了int *这个范例,代表着对应的指针变量(应该)指向的是一个int范例的变量.
其他范例的变量同理,如果我们需要指向一个float范例的变量,那么就使用float *即可:
  1. // 其他类型的变量同理,如果我们需要指向一个float类型的变量,那么就使用`float *`即可:
  2. #include <stdio.h>
  3. int main(){
  4.     float var = 3.1415;
  5.     float *ptr = &var;
  6.     printf("var == %f\n", var);
  7.     return 0;
  8. }
复制代码
使用ptr指针就能访问到var.
别的,指针范例不匹配是不允许的操作:
  1. // 此外,指针类型不匹配是不允许的操作
  2. #include <stdio.h>
  3. int main(){
  4.     int a = 10;
  5.     int *p = &a;
  6.     char *q = p; // 这里报错: cannot convert 'int*' to 'char*' in initialization
  7.     return 0;
  8. }
复制代码
代表着两个指针不兼容,即一个int *的指针不能赋值给一个char *的指针.
空范例指针

有时间,我们大概仅仅想要存一个地址,而不关心其范例,那么可以使用void *范例,即空范例指针,任何范例的指针都能赋值给void *:
  1. #include <stdio.h>
  2. int main() {
  3.     int a = 1024;
  4.     void *p = &a; // &a为指针,其类型为int*,可以直接赋值给void*而无需任何处理
  5.     printf("%d\n", *(int *)p); // void*指针不允许直接解引用,必须进行强制类型转换
  6.     return 0;
  7. }
复制代码
上面的代码使用void*指针p存储了a的地址,在使用p访问a的时间,必须使用逼迫范例转换将void*转换为int*才能进行解引用.由于对void*解引用的话,无法判断现实占用了多少内存,以是下面的代码编译器报错"不允许使用不完整的范例":
  1. #include <stdio.h>
  2. int main() {
  3.     int a = 1024;
  4.     void *p = &a;
  5.     printf("%d\n", *p);
  6.     return 0;
  7. }
复制代码
这就是一个重要的问题:指针指向的数据范例的大小.
后面我们会渐渐的接触到void* 指针的重要作用.
特殊的指针值-NULL

还有的时间,我们希望一个指针变量不指向任何有用的地址,那么我们可以对其赋值为NULL空指针值.
  1. #include <stdio.h>
  2. int main() {
  3.     int *p = NULL;
  4.     printf("%p\n", p);
  5.     // 实际上, NULL 就是 0
  6.     return 0;
  7. }
复制代码
运行效果:
可以看到,p的值就是0. NULL是一个宏(宏定义),定义在stdio.h中(宏定义将在后面的头文件部分讲解):
  1. // stdio.h
  2. #define NULL ((void *)0)
复制代码
这个宏意味着NULL在预处理的时间直接替换为((void *)0)
也就是说,当一个指针值为NULL时,我们以为他不指向任何地址,并且以为NULL是安全的—我们查抄一个指针是否等于NULL来判断这个指针是否被初始化等…
后面会深入强调初始化的问题.
逼迫范例转换

指针本质上还是一个整数(无符号的),但是指针范例仍旧不能和平凡的整型互相赋值,如果我们想要将某个数值作为指针值进行赋值,可以使用逼迫范例转换.
  1. int value=0x7fffffff;
  2. int *p = (int *)value;
复制代码
这样,指针p就指向了0x7fffffff这个内存地址对应的内存单元.
别的,这里也能看出,int和int*是完全差别的两种范例!
多级指针

指针用于指向某种范例的变量(地址),同样,指针变量也可以被另外一个指针变量所指向,即指向指针的指针,这就是多级指针.
使用二级指针

可以这样声明一个二级指针:
  1. #include <stdio.h>
  2. int main(){
  3.     int a = 3,*p = &a;
  4.     int **p2 = &p; // p2是一个二级指针,指向p
  5.     printf("%d\n",**p2); // 输出3, **p2 == *p == a == 3
  6.     return 0;
  7. }
复制代码
运行效果:
声明int **p2就等价于int *(*p2);,也就是说p2是一个指针,指向的范例为int*,因此显然p2是一个二级指针,int *p可以称为一级指针.
二级指针仍旧是一个指针,只不外我们可以对它进行2次解引用:
  1. #include <stdio.h>
  2. int main() {
  3.     int a = 3, *p = &a; // 声明一个int类型的指针变量p,指向a
  4.     int **p2 = &p; // 声明一个int类型的二级指针变量p2,指向p
  5.     /* 输出a的值 */
  6.     printf("a = %d\n", a); // a = 3
  7.     printf("*p = %d\n", *p); // *p = 3
  8.     printf("**p2 = %d\n", **p2); // **p2 = 3
  9.     /* 输出a的地址 */
  10.     printf("&a = %p\n", &a); // &a即为a的地址
  11.     printf("p = %p\n", p); // p存储的值即为a的地址
  12.     /* 输出指针变量p的地址 */
  13.     printf("&p = %p\n", &p); // &p即为p的地址
  14.     printf("p2 = %p\n", p2); // p2存储的值即为p的地址
  15.     return 0;
  16. }
复制代码
运行效果:
对二级指针的解引用

我们可以看出,二级指针可以进行2次解引用,第一次解引用的效果是访问其指向的变量,例如上面的例子中,
*p2即为p,p仍旧是一个指针,指向整型变量a,则对其再次解引用**p即可访问到a.
换言之,**p2可以视为*(*p2),读者应该清楚地意识到,这里的**完全是2步操作,你甚至可以在中间加一个空格.
  1. #include <stdio.h>
  2. int main() {
  3.     int a = 3, *p = &a; // 声明一个int类型的指针变量p,指向a
  4.     int **p2 = &p; // 声明一个int类型的二级指针变量p2,指向p
  5.     /* 输出a的值 */
  6.     printf("* *p2 = %d\n", * *p2); // **p2 = 3
  7.    
  8.     return 0;
  9. }
复制代码
运行效果:
以上使用二级指针进行了举例,三级指针等更"高级"的指针同理,只不外可以指向级数更高的指针而已,现实应用中,基本只用到二级指针.
  1. #include <stdio.h>
  2. int main() {
  3.     int a = 3, *p = &a; // 声明一个int类型的指针变量p,指向a
  4.     int **p2 = &p; // 声明一个int类型的二级指针变量p2,指向p
  5.     int ***p3 = &p2; // 声明一个int类型的三级指针变量p3,指向p2
  6.     return 0;
  7. }
复制代码
二级指针非常重要,特别是在使用C实现各种数据结构时,需要修改某些指针的指向时非常关键,后面的学习会频繁碰到.

指针是C语言的"魂魄",指针的内容险些占有了C语言的半壁山河,本部分简朴讲解了指针的基本概念和使用方法,后面会详细展开讲解.
---WAHAHA

注:文章原文在本人博客https://gngtwhh.github.io/上发布

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

天空闲话

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表