zoukankan      html  css  js  c++  java
  • 如何掌握 C 语言的一大利器——指针?

    一览:初学 C 语言时,大家肯定都被指针这个概念折磨过,一会指向这里、一会指向那里,最后把自己给指晕了。本文从一些基本的概念开始介绍指针的基本使用。

    内存

    考虑到初学 C 语言时,大家可能对计算机的组成原理不太了解,所以这里先简单介绍一些“内存”这个概念。

    众所周知,任何东西都需要有物理载体作为基础。

    比如说人产生的“思维”这个东西,我们看不见摸不着,但并不是说它就可以凭空存在了,思维的物理载体就是我们的大脑。“大脑”之于“思维”就如同“土地”之于“人类”。

    同样地,我们看不见摸不着的软件 / 代码也需要类似于“土地”和“大脑”的物理载体——存储器

    存储器分为两种:

    • 内存:计算机中正在运行的程序以及运行过程中暂时产生的数据都在这里。
    • 外存:那些暂时不需要运行的程序和最终的运算结果存储在这里。

    比如一个 HelloWorld 程序:

    #include <stdio.h>
    
    int main()
    {
        printf("Hello World!
    ");
        return 0;
    }
    

    写完保存之后,程序会被存储在外存(硬盘)中。

    当开始运行时,程序会被从外存调入内存中运行,打印 HelloWorld。

    上面是内存的简单概念(一个很浅的印象):内存可以暂时存储数据。

    那内存的结构是什么样的?

    这里我们把内存想象为一幢有很多房间的酒店,每个房间都有一个独一无二的房间号。

    人就是数据;内存就是酒店。

    酒店的职责就是供人暂时居住;内存的职责就是供数据暂时存储。

    图片来自网络

    内存的结构也像酒店一样,有很多“房间”,称之为“内存单元”,每个内存单元也有一个独一无二的“房间号”,称之为“内存地址”。数据就“住”在内存单元中。

    假设现在张三住在酒店的 1001 号房间了。

    我们就有以下关系:

    房间号为1001的房间住了 客户张三
    

    放到内存中,就是:

    内存地址为1001的内存单元存储了 整数5
    

    如此一来,我们就可以根据地址1001找到对应的内存单元,并对其中数据进行操作了。

    但这样有一个问题,就是为了操作 5,而不得不记住其地址,对于人来说,记忆这么多数字太麻烦了。

    想象一下你平常和别人打招呼时说:“早上好啊,某人的身份证号”,而不是“早上好啊,某人的名字”。

    光是记住自己的身份证号就不容易了,更别说别人的了,所以我们平常的称呼是名字。尽管身份证号唯一,而名字可能会重复。

    没错,就是名字,使用名字来代替对人不友好的内存地址。我们可以给 1001 号内存单元取个名字,就叫 a 吧。

    我们取的这个“名字”就是编程语言都会有的“变量名”。

    int a = 5;
    

    变量名对我们人类来说就很友好了,什么 zhangsanlisi等等都可以起。

    通过变量名,就可以访问其值了。现在我们有一个变量 a,存储了值 5,可以直接通过变量名打印其值:

    int a = 5;
    printf("%d", a);
    

    但这样也出现了一个问题,就是我们不知道某个变量的地址了。

    这就好比,你去酒店找张三,只知道他名字叫张三,而不知道他的房间号是多少,怎么办?一间间的敲门吗?

    不可能。我们应该去前台问工作人员:“请问张三的房间号是多少?”,前台工作人员会告诉我们:“1001号”

    类似地,要获取某个变量的地址,我们也可以向“前台的工作人员”询问:“请问变量 a 的‘房间号’是多少?”,当然,在现在的语境下,这句话就变成了“请问变量 a 的内存地址是多少?”。

    在 C 语言中,这个充当“前台工作人员”的角色的是取地址运算符 &

    int a = 5;
    printf("%p", &a); //请问a的内存地址是多少?
    

    通过 &,我们可以得到某个变量的内存地址,通常是一串十六进制数字,比如 0061FF1C

    到这里就一切安好了吗?不!

    指针

    概念

    至此,我们只有能力得到某个变量的内存地址,即使用 &。现在的问题是我们如何使用它。

    为什么现实中的人和事都会有一个名字?为了方便称呼和使用。

    名字之于事物,就好比刀柄之于刀身。一件事物一旦有了名字,我们就有了使用他的力量。

    在程序中,我们会有大量的数据,为了使用这些数据,我们有了变量和变量名的概念。比如整型数据用整型变量存储:

    int i = 5;
    float f = 5.0;
    char c = 'x';
    

    地址也属于数据,换句话说,我们也应该有某种类型的变量来存储地址:

    int a = 5;
    int p = &i; //错误代码
    

    我们的目的是使用变量 p 来存储 int 类型变量a的地址,但是上面的代码是错误的。因为我们的变量 p 被声明为 int类型,所以变量 p 就只能存储 int 类型数据,而不能存储 int 类型变量的地址。

    这个时候我们就需要一种能存储整型变量的地址的变量,C 语言为我们提供了一种机制——指针。

    int a = 5;
    int *pa = &a;
    

    现在我们声明了一个能存储 int 类型变量的地址 的变量 pa,然后使用 & 获取变量 a 的地址,赋值给变量 pa,非常完美。

    这里的 pa,就是一个指针(pointer)。可以看一下指针的定义:

    In computer science, a pointer is an object in many programming languages that stores a memory address.

    In computer science, an object can be a variable, a data structure, a function, or a method, and as such, is a value in memory referenced by an identifier.

    在计算机科学中,指针是许多语言中存储内存地址的对象。这里的对象可以是变量、结构体、函数或方法。

    即,指针中存储的是内存地址

    指针的声明需要使用 * 来表示该变量是一个指针变量:

    [pointer_type] *[pointer_name];
    
    int a = 5;
    float b = 5.0;
    char c = 'x';
    
    int *pa = &a; 
    float *pb = &b;
    char *pc = &c;
    

    由于指针中存储了某个变量的地址,所以我们可以说该指针指向了那个变量。比如 pa 被声明为了指向 int 类型的指针,指向了变量 a

    间接访问操作符

    我们有了取地址运算符 &用来获取某个变量的地址,也知道了如何声明某种类型的指针用来存储地址。

    知道如何获取了、懂了怎么存储了,那么怎么使用指针呢?

    房间号不是用来好看的,而是用来找到房间和房间中的人。我们已经通过 & 这个“前台工作人员”找到了房间号并记了下来,下一步就是上门把人找出来。

    通过间接访问操作符 *,我们就可以根据指针“上门找人”了。

    int a = 5; //变量a中存储5
    int *pa = &a; //获取房间号
    printf("%d", *pa); //上门找人
    

    *pa,就是取指针 pa 所指向的变量的值。

    区分

    初学 C语言时会容易混淆一些概念,所以这里区分一下。

    int a = 5;
    int b = 6;
    int c = 7;
    
    int *pa = &a;
    int *pb = &b;
    int *pc = &b;
    
    printf("a = %d
    ", a);
    printf("b = %d
    ", b);
    printf("c = %d
    ", c);
    
    printf("&a = %p
    ", &a);
    printf("&b = %p
    ", &b);
    printf("&c = %p
    ", &c);
    
    printf("pa = %p
    ", pa);
    printf("pb = %p
    ", pb);
    printf("pc = %p
    ", pc);
    
    printf("*pa = %d
    ", *pa);
    printf("*pb = %d
    ", *pb);
    printf("*pc = %d
    ", *pc);
    

    输出为

    a = 5
    b = 6
    c = 7
    &a = 0061FF10
    &b = 0061FF0C
    &c = 0061FF08
    pa = 0061FF10
    pb = 0061FF0C
    pc = 0061FF0C
    *pa = 5
    *pb = 6
    *pc = 6
    
    • a:变量
    • &aa的地址
    • int *pa:声明一个指向 int 类型的指针 pa
    • pa:指针
    • *pa:指针 pa 指向的变量值

    int *pa*pa 中的 * 不一样,这一点容易让人迷惑。在声明时,int * 是一起的,用来声明一个指向 int 类型变量的指针,虽然写开了,但不要分开来看。

    int a; //声明了一个变量a
    int *pa; //声明了一个变量pa
    

    &* 是一对相反的操作,& 根据变量求地址, * 根据地址求变量。

    int a = 5;
    printf("%d", *&a); //5
    printf("%d", a); //5
    

    *&a 的值为 5,即 a

    初始化

    我们在声明某个变量后,在使用某个变量前,一定要对其进行初始化。

    比如在声明变量 a 的同时将其初始化为 5:

    int a = 5;
    

    也可以声明后再初始化:

    int a;
    a = 5;
    

    如果不初始化,那么变量的值将是难以想象的。

    指针也是变量,也必须对其进行初始化。先运行下面一段代码:

    int *p;
    *p = 5;
    return 0;
    

    这段代码的意思很简单:声明一个指针 p, 将 5 赋值给指针 p 所指向的那个变量。但这种代码是错误的!

    请问指针 p 指向了谁?由于我们没有对其进行初始化,所以根本就不知道指针 p 指向了谁,那怎么赋值?

    这就好比一个人对你说:“请把这个包裹给李四”。但是你根本就不知道李四是谁,李四住在哪里,你怎么给?

    快递员不认识你就能送货,那是因为包裹上有地址,这就足够了。

    但是在上面的代码中,你告诉 p 地址了吗?没有!因为我们没有对指针进行初始化!

    所以初始化指针非常重要!!!未初始化的指针不能用!!!

    更改如下:

    int a = 4;
    int *p = &a;
    *p = 5;
    

    或者:

    int a = 4;
    int *p;
    p = &a;
    *p = 5;
    

    现在变量 a 的值由 4 变为 5 了。

    因为我们在“包裹”上写了变量 a 地址,所以能把 5 送给变量 a

    赋值

    我们可以将一个指针赋值给另外一个指针。

    int a = 5;
    
    int *p1 = &a;
    int *p2;
    p2 = p1;
    

    我们将指针 p1 的值赋给 p2,然后打印以下内容:

    printf("a = %d
    ", a);
    printf("&a = %p
    ", &a);
    
    printf("p1 = %p
    ", p1);
    printf("p2 = %p
    ", p2);
    
    printf("*p1 = %d
    ", *p1);
    printf("*p2 = %d
    ", *p2);
    
    printf("&p1 = %p
    ", &p1);
    printf("&p2 = %p
    ", &p2);
    

    输出为:

    a = 5
    &a = 0061FF1C
    p1 = 0061FF1C
    p2 = 0061FF1C
    *p1 = 5
    *p2 = 5
    &p1 = 0061FF18
    &p2 = 0061FF14
    

    可以看到,将指针 p1 赋值给另一个指针 p2 的结果是: p1 指向哪里, p2 就指向哪里。如此一来,我们可以通过两个指针操作变量 a

    *p1 = 4;
    printf("a = %d
    ", a); //从5变为4
    
    *p2 = 3;
    printf("a = %d
    ", a); //从4变为3
    

    赋值过程

    空指针

    空指针的值为 NULL, 表示不指向任何对象。

    int *p = NULL;
    

    当我们初始化一个指针的时候,如果还不知道要指向谁的时候,就把它初始化为空指针。

    一些用法

    我们已经以”指向变量的指针”为例,介绍了指针的基本用法。现在介绍一些指针的其他用法。

    指向指针的指针

    前面我们介绍了“指向变量的指针”:

    int a = 5;
    int *pa = &5;
    

    指向整型变量的指针

    指针也是个变量,只不过相对于其他类型的变量有点特殊,指针变量中存储的是其他变量的地址。

    也就是说,指针作为一个变量也有地址,该地址可以被其他指针存储,即指向了指针的指针。

    指向指针的指针

    对应代码如下:

    int a = 5;
    int *pa = &5;
    int **ppa = &pa;
    

    如你所见,声明一个“指向指针的指针”需要使用两个*

    [pointer_type] **[pointer_name];
    

    同样地,要获取 指向指针的指针 指向的 指针 指向的 变量值 需要进行两次间接访问,即**ppa

    请仔细体会以下代码:

    #include <stdio.h>
    
    int main()
    {
        int a = 5;
        int *pa = &a;
        int **ppa = &pa;
        
        printf("a = %d
    ", a);
        printf("&a = %p
    ", &a);
    
        printf("pa = %p
    ", pa);
        printf("*pa = %d
    ", *pa);
        printf("&pa = %p
    ", &pa);
    
        printf("ppa = %p
    ", ppa);
        printf("*ppa = %p
    ", *ppa);
        printf("**ppa = %d
    ", **ppa);
        printf("&ppa = %p
    ", &ppa);
        
        return 0;
    }
    

    通过代码,我们可以得到以下等价关系:

    表达式 等价表达式
    a 5
    pa &a
    ppa &pa
    *pa a5
    *ppa pa&a
    **ppa *paa5

    举一反三,你还可以试试 {指向[指向(指针)的指针]的指针}。

    指针和数组

    首先运行以下代码:

    int arr[5] = {1, 2, 3, 4, 5};
    printf("arr = %p
    ", arr);
    
    int *p = &arr[0];
    printf("&arr[0] = %p
    ", &arr[0]);
    printf("p = %p
    ", p);
    printf("arr[0] = %d
    ", arr[0]);
    printf("*p = %d
    ", *p);
    
    p++;
    printf("运行p++之后...
    ");
    
    printf("&arr[1] = %p
    ", &arr[1]);
    printf("p = %p
    ", p);
    printf("arr[1] = %d
    ", arr[1]);
    printf("*p = %d
    ", *p);
    

    输出为:

    arr = 0061FF08
    &arr[0] = 0061FF08
    p = 0061FF08
    arr[0] = 1
    *p = 1
    运行p++之后...
    &arr[1] = 0061FF0C
    p = 0061FF0C
    arr[1] = 2
    *p = 2
    

    可以得到以下结论:

    • arr =&arr[0]arr是数组的首元素指针
    • int *p = &arr[0]int *p = arr 是等效的
    • arr[n]*(p+n)是等效的

    指针和函数

    先运行以下函数:

    #include <stdio.h>
    
    void swap(int x, int y)
    {
        int temp = x;
        x = y;
        y = temp;
    }
    
    int main()
    {
        int x = 5, y = 10;
        printf("交换前 x = %d, y = %d
    ", x, y);
        swap(x, y);
        printf("交换后 x = %d, y = %d
    ", x, y);
        return 0;
    }
    

    swap 函数的目的很简单:传进来两个值,交换他们。

    但是结果令人失望——根本没交换。原因是什么?

    我们打印一些东西:

    #include <stdio.h>
    
    void swap(int x, int y)
    {
        printf("在swap()中,x的地址为%p,y的地址为%p
    ", &x, &y);
        printf("swap() 交换前 x = %d, y = %d
    ", x, y);
        int temp = x;
        x = y;
        y = temp;
        printf("swap() 交换后 x = %d, y = %d
    ", x, y);
    }
    
    int main()
    {
        int x = 5, y = 10;
        printf("在main()中,x的地址为%p,y的地址为%p
    ", &x, &y);
        printf("main() 交换前 x = %d, y = %d
    ", x, y);
        swap(x, y);
        printf("main() 交换后 x = %d, y = %d
    ", x, y);
        return 0;
    }
    

    输出为:

    在main()中,x的地址为0061FF1C,y的地址为0061FF18
    main() 交换前 x = 5, y = 10
    在swap()中,x的地址为0061FF00,y的地址为0061FF04
    swap() 交换前 x = 5, y = 10
    swap() 交换后 x = 10, y = 5
    main() 交换后 x = 5, y = 10
    

    可以看到,在 swap() 中,我们确实交换了值,但是swap() 函数执行完后回到 main() 中,值却没有交换。

    可以看到,swap() 中的 xymain() 中的 xy 的地址并不相同,这就意味着**此 xy 非彼 xy **。

    值传递

    原因很简单,swap(int x, int y) 的参数传递为值传递,所谓值传递,即将实参的值复制到形参的对应内存单元中。函数操作的是形参的内存单元,无论形参如何变化,都不会影响到实参。

    void swap(int x, int y) //xy为形参
    {.....}
    
    int main()
    {
        int x = 5, y = 10;
        swap(x, y); //xy为实参
    }
    

    这里就解释了为什么 main()swap()打印出来的 xy 的地址不同,也解释了为什么交换失败。

    那么,为了通过函数直接操作实参,我们必须使形参和实参是同一块内存。所以我们直接把实参的地址传给函数,也即,函数的参数为指针,指向实参的内存单元。这种参数传递为地址传递

    地址传递保证了形参的变化即为实参的变化。

    地址传递

    代码更正:

    #include <stdio.h>
    
    void swap(int *px, int *py) //形参为指针,接收实参的地址
    {
        printf("在swap()中,px = %p,py = %p
    ", px, py);
        printf("swap() 交换前 x = %d, y = %d
    ", *px, *py);
        int temp = *px;
        *px = *py;
        *py = temp;
        printf("swap() 交换后 x = %d, y = %d
    ", *px, *py);
    }
    
    int main()
    {
        int x = 5, y = 10;
        printf("在main()中,x的地址为%p,y的地址为%p
    ", &x, &y);
        printf("main() 交换前 x = %d, y = %d
    ", x, y);
        swap(&x, &y);
        printf("main() 交换后 x = %d, y = %d
    ", x, y);
        return 0;
    }
    

    输出为:

    在main()中,x的地址为0061FF1C,y的地址为0061FF18
    main() 交换前 x = 5, y = 10
    在swap()中,px = 0061FF1C,py = 0061FF18
    swap() 交换前 x = 5, y = 10
    swap() 交换后 x = 10, y = 5
    main() 交换后 x = 10, y = 5
    

    指针和结构体

    先定义一个结构体:

    typedef struct Node {
        int data;
        struct Node *next;
    } Node;
    

    然后声明一个结构体:

    Node node;
    

    要访问结构体内的成员,需要使用 . 操作符:

    node.data;
    node.next;
    

    现在我们有一个指向该结构体的指针:

    Node *p = &node;
    

    想要通过指针访问结构体的成员:

    (*p).data;
    (*p).next;
    

    也可以使用 -> 操作符:

    p->data;
    p->next;
    

    注意,-> 要对指向结构体的指针使用才行。

    对于初学者,某个概念一时搞不懂其实很正常。谁都不是一下子就学会用筷子和走路的,我们需要的是花时间进行大量的实践。就拿指针来说吧,初学者觉得难以理解是因为用得少,反过来说,对于经常使用 C/C++ 写代码的人,指针肯定早就不是问题了。所以搞清基本原理,接下来就花时间去大量实践吧,时间到了,自然就会豁然开朗。

    如有错误,还请指正。

    如果觉得写的不错可以关注一下我。

  • 相关阅读:
    POJ3320 Jessica's Reading Problem
    POJ3320 Jessica's Reading Problem
    CodeForces 813B The Golden Age
    CodeForces 813B The Golden Age
    An impassioned circulation of affection CodeForces
    An impassioned circulation of affection CodeForces
    Codeforces Round #444 (Div. 2) B. Cubes for Masha
    2013=7=21 进制转换
    2013=7=15
    2013=7=14
  • 原文地址:https://www.cnblogs.com/xingrenguanxue/p/14462933.html
Copyright © 2011-2022 走看看