• C现代方法(第11章)笔记——指针


    第11章 指针

    ——我记不清第十一条戒律是“你应该计算”还是“你不应该计算”了。

    指针C语言最重要——也是最常被误解——的特性之一。由于指针的重要性,本书将用3章的篇幅对其进行讨论。本章侧重于基础知识,而第12章和第17章则介绍指针的高级应用

    本章将从内存地址及其与指针变量的关系入手(11.1节),然后11.2节介绍取地址运算符间接寻址运算符,11.3节探讨指针赋值的内容,11.4节说明给函数传递指针的方法,而11.5节则讨论从函数返回指针

    11.1 指针变量

    理解指针的第一步是在机器级上观察指针表示的内容。大多数现代计算机将内存分割为字节(byte),每个字节可以存储8位的信息。

    0 1 0 1 0 0 1 1
    
    • 1

    每个字节都有唯一的地址(address),用来和内存中的其他字节相区别。如果内存中有n个字节,那么可以把地址看作0~n-1的数。

    地址    内容
     0    01010011
    
     1    01110101
    
     2    01110011
            ....
    n-1   01000011
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    可执行程序由代码(原始C程序中与语句对应的机器指令)和数据(原始程序中的变量)两部分构成。程序中的每个变量占有一个或多个字节内存,把第一个字节的地址称为变量的地址。下图中,变量i占有地址为20002001的两个字节,所以变量i的地址是2000

                i--------
    地址  .... 2000  2001  ....
    内容  .... xxxx  xxxx  ....
    
    • 1
    • 2
    • 3

    这就是指针的出处。虽然用数表示地址,但是地址的取值范围可能不同于整数的范围,所以一定不能用普通整型变量存储地址。但是,可以用特殊的指针变量(pointer variable)存储地址。在用指针变量p存储变量i的地址时,我们说p“指向”i。换句话说,指针就是地址,而指针变量就是存储地址的变量

    11.1.1 指针变量的声明

    对指针变量的声明与对普通变量的声明基本一样,唯一的不同就是必须在指针变量名字前放置星号

    int *p;
    
    • 1

    上述声明说明p是指向int类型对象的指针变量。这里我们用术语对象来代替变量,是因为p可以指向不属于变量的内存区域(见第17章)。(注意,在第19章讨论程序设计时“对象”一词将有不同的含义。)

    指针变量可以和其他变量一起出现在声明中:

    int i, j, a[10], b[20], *p, *q; 
    
    • 1

    在这个例子中,ij都是普通整型变量,ab是整型数组,而pq是指向整型对象的指针。

    C语言要求每个指针变量只能指向一种特定类型(引用类型)的对象:

    int *p;    /* points only to integers   */ 
    double *q; /* points only to doubles  */ 
    char *r;   /* points only to characters */ 
    
    • 1
    • 2
    • 3

    至于引用类型是什么类型则没有限制。事实上,指针变量甚至可以指向另一个指针,即指向指针的指针(17.6节)。

    11.2 取地址运算符和间接寻址运算符

    为使用指针,C语言提供了一对特殊设计的运算符。为了找到变量的地址,可以使用&(取地址)运算符。如果x是变量,那么&x就是x在内存中的地址。为了获得对指针所指向对象的访问,可以使用*(间接寻址)运算符。如果p是指针,那么*p表示p当前指向的对象。

    11.2.1 取地址运算符

    声明指针变量是为指针留出空间,但是并没有把它指向对象:

    int *p;    /* points nowhere in particular */
    
    • 1

    在使用前初始化p是至关重要的。一种初始化指针变量的方法是使用&运算符把某个变量的地址赋给它,或者更常采用左值(4.2节):

    int i, *p; 
    ... 
    p = &i;
    
    • 1
    • 2
    • 3

    通过把i的地址赋值给变量p的方法,上述语句把p指向了i

    在声明指针变量的同时对它进行初始化是可行的:

    int i; 
    int *p = &i; 
    
    //甚至可以把i的声明和p的声明合并,但是需要首先声明i:
    int i, *p = &i;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    11.2.2 间接寻址运算符

    一旦指针变量指向了对象,就可以使用*运算符访问存储在对象中的内容。例如,如果p指向i,那么可以显示出i的值,如下所示:

    printf("%d\n", *p);
    //printf函数将显示i的值,而不是i的地址。
    
    • 1
    • 2

    习惯于数学思维的读者可能希望把*想象成&的逆运算。实际上,对变量使用&运算符产生指向变量的指针,而对指针使用*运算符则可以返回到原始变量:

    j = *&i;  //等同于 j = i;
    
    • 1

    只要p指向i*p就是i的别名。*p不仅拥有和i相同的值,而且对*p的改变也会改变i的值。(*p是左值,所以对它赋值是合法的。)

    p = &i; 
    i = 1; 
    
    printf("%d\n", i);    /* prints 1 */ 
    printf("%d\n", *p);   /* prints 1 */ 
    *p = 2; 
    
    printf("%d\n", i);    /* prints 2 */ 
    printf("%d\n", *p);   /* prints 2 */ 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    请注意!不要把间接寻址运算符'*'用于未初始化的指针变量。如果指针变量p没有初始化,那么试图使用p的值会导致未定义的行为:

    int *p; 
    printf("%d", *p);  /*** WRONG ***/ 
    
    /*
    给*p赋值尤其危险。如果p恰好具有有效的内存地址,下面的赋值会试图修改存储在该地址的数据:
    */
    int *p; 
    *p = 1;  /*** WRONG ***/
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如果上述赋值改变的内存单元属于该程序,那么可能会导致出乎意料的行为;如果改变的内存单元属于操作系统,那么很可能会导致系统崩溃。编译器可能会给出警告消息,告知p未初始化,所以请留意收到的警告消息。

    11.3 指针赋值

    C语言允许使用赋值运算符进行指针的复制,前提是两个指针具有相同的类型。假设ijpq声明如下:

    int i, j, *p, *q;
    
    • 1

    则语句p = &i;是指针赋值的实例,把i的地址复制给p。而q = p;这条语句是把p的内容(即i的地址)复制给q,效果是把q指向了p指向的地方。

    现在pq都指向了i,所以可以用*p*q赋新的值的方法来改变i(任意数量的指针变量都可以指向同一对象):

    *p = 1;
    *q = 2;
    
    • 1
    • 2

    注意不要把q = p;*q = *p搞混。第一条语句是指针赋值,而第二条语句不是。就如下面的例子:

    p = &i; 
    q = &j; 
    i = 1; 
    
    *q = *p;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    赋值语句*q = *p;是把p指向的值(i的值)复制到q指向的对象(变量j)中。

    11.4 指针作为参数

    到目前为止,我们回避了一个十分重要的问题:指针对什么有益呢?因为C语言中指针有几种截然不同的应用,所以此问题没有唯一的答案。在本节中,我们将看到如何把指向变量的指针用作函数的参数。指针的其他应用将在11.5节第12章第17章讨论。

    9.3节中我们看到,因为C语言用值进行参数传递,所以在函数调用中用作实际参数的变量无法改变。当希望函数能够改变变量时,C语言的这种特性就很讨厌了。

    指针提供了此问题的解决方法:不再传递变量x作为函数的实际参数,而是提供&x,即指向x的指针。声明相应的形式参数p为指针。调用函数时,p的值为&x,因此*pp指向的对象)将是x的别名。函数体内*p的每次出现都将是对x的间接引用,而且函数既可以读取x也可以修改x

    为了用实例证明这种方法,下面通过把形式参数int_partfrac_part声明成指针的方法来修改decompose函数。现在decompose函数的定义形式如下:

    void decompose(double x, long *int_part, double *frac_part) 
    { 
        *int_part = (long) x; 
        *fract_part = x - *int_part; 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    decompose函数的原型既可以是

    void decompose(double x, long *int_part, double *frac_part); 
    
    • 1

    也可以是

    void decompose(double, long *, double *); 
    
    • 1

    以下列方式调用decompose函数:

    decompose(3.14159, &i, &d); 
    
    • 1

    因为id前有取地址运算符&,所以decompose函数的实际参数是指向id的指针,而不是id的值。调用decompose函数时,把值3.14159复制到x中,把指向i的指针存储在int_part中,把指向d的指针存储在frac_part中。

    decompose函数体内的第一个赋值把x的值转换为long类型,并且把此值存储在int_part指向的对象中。因为int_part指向i,所以赋值把值3放到i中。

    第二个赋值把int_part指向的值(即i的值)取出,现在这个值是3。把此值转换为double类型,并且用x减去它,得到0.14159。然后把这个值存储在frac_part指向的对象中。

    decompose函数返回时,就像原来希望的那样,id将分别有值30.14159


    用指针作为函数的实际参数实际上并不新鲜,从第2章开始就已经在scanf函数调用中使用了。思考下面的例子:

    int i; 
    ... 
    scanf("%d", &i); 
    
    • 1
    • 2
    • 3

    须把&放在i的前面以便给scanf函数传递指向i的指针,指针会告诉scanf函数把读取的值放在哪里。如果没有&运算符,传递给scanf函数的将是i的值。

    虽然scanf函数的实际参数必须是指针,但并不总是需要&运算符。在下面的例子中,我们向scanf函数传递了一个指针变量:

    int i, *p; 
    ... 
    p = &i; 
    scanf("%d", p);
    
    • 1
    • 2
    • 3
    • 4

    既然p包含了i的地址,那么scanf函数将读入整数并且把它存储在i中。在调用中使用&运算符将是错误的:

    scanf("%d", &p);  /*** WRONG ***/
    
    //scanf函数读入整数并且把它存储在p中而不是i中。
    
    • 1
    • 2
    • 3

    请注意!!向函数传递需要的指针却失败了,这可能会产生严重的后果。假设我们在调用decompose函数时没有在id前面加上&运算符:

    decompose (3.14159, i, d);
    
    • 1

    decompose函数期望第二个和第三个实际参数是指针,传入的却是id的值。decompose函数没有办法区分,所以它会把id的值当成指针来使用。当decompose函数把值存储到*int_part*frac_part中时,它会修改未知的内存地址,而不是修改id

    如果已经提供了decompose函数的原型(当然,应该始终这样做),那么编译器将告诉我们实际参数的类型不对。然而,在scanf的例子中,编译器通常不会检查出传递指针失败,因此scanf函数特别容易出错。

    11.4.1 找出数组中的最大元素和最小元素

    为了说明如何在函数中传递指针,下面来看一个名为max_min的函数,该函数用于找出数组中的最大元素和最小元素。调用max_min函数时,将传递两个指向变量的指针;然后max_min函数把答案存储在这些变量中。max_min函数具有下列原型:

    void max_min(int a[], int n, int *max, int *min); 
    
    • 1

    max_min函数的调用可以具有下列的形式:

    max_min(b, N, &big, &small);
    
    • 1

    b是整型数组,而N是数组b中的元素数量。bigsmall是普通的整型变量。当max_min函数找到数组b中的最大元素时,通过给*max赋值的方法把值存储在big中。(因为max指向big,所以给*max赋值会修改big的值。)类似地,可以通过给*min赋值把b中最小元素的值存储在small中。

    为了测试max_min函数,我们编写程序用来往数组中读入10个数,然后把数组传递给max_min函数,并且显示出结果,:

    Enter 10 numbers: 34 82 49 102 7 94 23 11 50 31  
    Largest: 102 
    Smallest: 7
    
    • 1
    • 2
    • 3
    /*
    maxmin.c
    --Finds the largest and smallest elements in an array  
    */
    
    #include  
    
    #define N 10 
    
    void max_min(int a[], int n, int *max, int *min); 
    
    int main(void) 
    { 
        int b[N], i, big, small; 
        
        printf("Enter %d numbers: ", N); 
        for (i = 0; i < N; i++)  
            scanf("%d", &b[i]); 
        
        max_min(b, N, &big, &small); 
        
        printf("Largest: %d\n", big); 
        printf("Smallest: %d\n", small); 
        
        return 0; 
    } 
    
    void max_min(int a[], int n, int *max, int *min) 
    { 
        int i; 
        
        *max = *min = a[0]; 
        for (i = 1; i < n; i++) { 
            if(a[i] > *max) 
                *max = a[i]; 
            else if(a[i] < *min) 
                *min = a[i]; 
        } 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    11.4.2 用const保护参数

    当调用函数并且把指向变量的指针作为参数传入时,通常会假设函数将修改变量(否则,为什么函数需要指针呢?)。例如,如果在程序中看到语句:f(&x);

    大概是希望f改变x的值。但是,f也可能仅需要检查x的值而不是改变它的值。指针可能高效的原因是,如果变量需要大量的存储空间,那么传递变量的值会浪费时间和空间

    可以使用单词const来表明函数不会改变指针参数所指向的对象。const应放置在形式参数的声明中,后面紧跟着形式参数的类型说明:

    void f(const int *p) 
    { 
       *p = 0;   /*** WRONG ***/
    }  
    
    • 1
    • 2
    • 3
    • 4

    这一用法表明p是指向“常整数”的指针。试图改变*p是编译器会检查的一种错误。

    11.5 指针作为返回值

    我们不仅可以为函数传递指针,而且还可以编写返回指针的函数。返回指针的函数是相对普遍的,第13章中将遇到几个。

    当给定指向两个整数的指针时,下列函数返回指向两个整数中较大数的指针:

    int *max(int *a, int *b) 
    { 
        if (*a > *b) 
            return a; 
        else 
            return b; 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    调用max函数时,用指向两个int类型变量的指针作为参数,并且把结果存储在一个指针变量中:

    int *p, i, j; 
    ... 
    p = max(&i, &j); 
    
    • 1
    • 2
    • 3

    调用max期间,*ai的别名,而*bj的别名。如果i的值大于j,那么max返回i的地址;否则,max返回j的地址。调用函数后,p或者指向i,或者指向j

    请注意!!永远不要返回指向自动局部变量的指针:

    int *f(void) 
    { 
       int i; 
       ... 
       return &i; 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    一旦f返回,变量i就不存在了,所以指向变量i的指针将是无效的。有的编译器会在这种情况下给出类似“function returns address of local variable”的警告。

    指针可以指向数组元素,而不仅仅是普通变量。设a为数组,则&a[i]是指向a中元素i的指针。当函数的参数中有数组时,返回一个指向数组中的某个元素的指针有时是挺有用的。

    例如,下面的函数假定数组an个元素,并返回一个指向数组中间元素的指针:

    int *find_middle(int a[], int n) { 
        return &a[n/2]; 
    }
    
    //第12章会详细讨论指针和数组的关系。
    
    • 1
    • 2
    • 3
    • 4
    • 5

    问与答

    问1:指针总是和地址一样吗?

    答:通常是,但不总是。考虑用而不是字节划分内存的计算机。字可能包含36位、60位等。

    当用字划分内存时,每个字都有一个地址。通常整数占一个字长度,所以指向整数的指针可以就是一个地址。但是,字可以存储多于一个的字符。例如,36位的字可以存储66位的字符,或者49位的字符。

    由于这个原因,可能需要用不同于其他指针的格式存储指向字符的指针。指向字符的指针可以由地址(存储字符的字)加上一个小整数(字符在字内的位置)组成。

    在一些计算机上,指针可能是“偏移量”而不完全是地址。例如,Intel x86 CPU(用于许多个人计算机)可以在多种模式下执行程序。最老的模式称为实模式(real mode),可以追溯到1978年的8086处理器。在这种模式下,地址有时用一个16位数(偏移量)表示,有时用两个16位数(段—偏移量对)表示。偏移量不是真正的内存地址,CPU必须把它和存储在专用寄存器中的段值结合起来。为了支持实模式,旧的C语言编译器通常提供两种指针:近指针(16位偏移量)和远指针(32位段—偏移量对)。这些编译器通常保留单词nearfar作为非标准关键字,用于指针变量的声明。

    问2:如果指针可以指向程序中的数据,那么使指针指向程序代码是否可能?

    答:可能。17.7节将介绍指向函数的指针

    问3:声明int *p = &i;和语句p = &i;不一致。为什么在语句中p没有像其在声明中那样前面加*号呢?

    答:造成困惑的根源在于,根据使用上下文的不同C语言中的*号可以有多种含义。

    在声明int *p = &i;中,*号不是间接寻址运算符,其作用是指明p的类型以便告知编译器p是一个指向int类型变量的指针;而在语句中出现时,*号(作为一元运算符使用时)会执行间接寻址。

    语句*p = &i;是不正确的,因为它把i的地址赋给了p指向的对象,而不是p本身。

    问4:有没有办法显示变量的地址?

    答:任何指针(包括变量的地址)都可以通过调用printf函数并在格式串中使用转换说明%p来显示。详见22.3节。

    问5

    void f(const int *p); 
    
    • 1

    这是说函数f不能修改p吗?

    答:不是。这说明不能改变指针p指向的整数,但是并不阻止f改变p自身。

    void f(const int *p) 
    { 
        int j; 
        *p = 0; /*** WRONG ***/ 
        p = &j; /* legal */ 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    因为实际参数是值传递的,所以通过使指针指向其他地方的方法给p赋新值不会对函数外部产生任何影响

    问6:声明指针类型的形式参数时,像下面这样在参数名前面放置单词const是否合法?

    void f(int * const p); 
    
    • 1

    答:是合法的。然而效果不同于把const放在p的类型前面。在11.4节中已经见过在p的类型前面放置const可以保护p指向的对象。在p的类型后面放置const可以保护p本身:

    void f(int * const p) 
    { 
        int j; 
        *p = 0; /* legal */ 
        p = &j; /*** WRONG ***/ 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这一特性很少用到。因为p仅仅是另一个指针(调用函数时的实际参数)的副本,所以极少有什么理由保护它。

    更罕见的一种情况是需要同时保护p和它所指向的对象,这可以通过在p类型的前后都放置const来实现:

    void f(const int * const p) 
    { 
        int j; 
        *p = 0; /*** WRONG ***/ 
        p = &j; /*** WRONG ***/ 
    } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    写在最后

    本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!

  • 相关阅读:
    抖音短视频流量获取攻略,掌握好这些一定可以出爆款
    搭建nuxt3项目(框架构建)
    一维时间序列信号的奇异小波时频分析方法(Python)
    巯基SH/氨基/NH2/羧基COOH/PEG/蛋白Protein/抗体antibody修饰Au@SiO2核壳纳米粒子/二氧化硅包裹金表面
    数学才是顶级码农的核心修养,码农怎样搞好数学?来看看这些网友强推的数学神作!文末评论区进行评论参与送书哟
    垃圾收集器ParNew&CMS与底层三色标记算法
    【前端】webpack打包去除console.log
    一文熟练使用python修改Excel中的数据
    JS下载地图离线数据,前端下载谷歌离线地图
    5分钟搞懂词向量生成技术:Word2Vec
  • 原文地址:https://blog.csdn.net/New_Teen/article/details/133967786