笨办法学C 练习15:指针,可怕的指针

练习15:指针,可怕的指针

原文:Exercise 15: Pointers Dreaded Pointers

译者:飞龙

指针是C中的一个著名的谜之特性,我会试着通过教授你一些用于处理它们的词汇,使之去神秘化。指针实际上并不复杂,只不过它们经常以一些奇怪的方式被滥用,这样使它们变得难以使用。如果你避免这些愚蠢的方法来使用指针,你会发现它们难以置信的简单。

要想以一种我们可以谈论的方式来讲解指针,我会编写一个无意义的程序,它以三种方式打印了一组人的年龄:

#include <stdio.h>

int main(int argc, char *argv[])
{
    // create two arrays we care about
    int ages[] = {23, 43, 12, 89, 2};
    char *names[] = {
        "Alan", "Frank",
        "Mary", "John", "Lisa"
    };

    // safely get the size of ages
    int count = sizeof(ages) / sizeof(int);
    int i = 0;

    // first way using indexing
    for(i = 0; i < count; i++) {
        printf("%s has %d years alive.\n",
                names[i], ages[i]);
    }

    printf("---\n");

    // setup the pointers to the start of the arrays
    int *cur_age = ages;
    char **cur_name = names;

    // second way using pointers
    for(i = 0; i < count; i++) {
        printf("%s is %d years old.\n",
                *(cur_name+i), *(cur_age+i));
    }

    printf("---\n");

    // third way, pointers are just arrays
    for(i = 0; i < count; i++) {
        printf("%s is %d years old again.\n",
                cur_name[i], cur_age[i]);
    }

    printf("---\n");

    // fourth way with pointers in a stupid complex way
    for(cur_name = names, cur_age = ages;
            (cur_age - ages) < count;
            cur_name++, cur_age++)
    {
        printf("%s lived %d years so far.\n",
                *cur_name, *cur_age);
    }

    return 0;
}

在解释指针如何工作之前,让我们逐行分解这个程序,这样你可以对发生了什么有所了解。当你浏览这个详细说明时,试着自己在纸上回答问题,之后看看你猜测的结果符合我对指针的描述。

ex15.c:6-10

创建了两个数组,ages储存了一些int数据,names储存了一个字符串数组。

ex15.c:12-13

为之后的for循环创建了一些变量。

ex15.c:16-19

你知道这只是遍历了两个数组,并且打印出每个人的年龄。它使用了i来对数组索引。

ex15.c:24

创建了一个指向ages的指针。注意int *创建“指向整数的指针”的指针类型的用法。它很像char *,意义是“指向字符的指针”,而且字符串是字符的数组。是不是很相似呢?

ex15.c:25

创建了指向names的指针。char *已经是“指向char的指针”了,所以它只是个字符串。你需要两个层级,因为names是二维的,也就是说你需要char **作为“指向‘指向字符的指针’的指针”。把它学会,并且自己解释它。

ex15.c:28-31

遍历agesnames,但是使用“指针加偏移i”。*(cur_name+i)name[i]是一样的,你应该把它读作“‘cur_name指针加i’的值”。

ex15.c:35-39

这里展示了访问数组元素的语法和指针是相同的。

ex15.c:44-50

另一个十分愚蠢的循环和其它两个循环做着相同的事情,但是它用了各种指针算术运算来代替:

ex15.c:44

通过将cur_namecur_age置为namesage数组的起始位置来初始化for循环。

ex15.c:45

for循环的测试部分比较cur_age指针和ages起始位置的距离,为什么可以这样写呢?

ex15.c:46

for循环的增加部分增加了cur_namecur_age的值,这样它们可以只想namesages的下一个元素。

ex15.c:48-49

cur_namecur_age的值现在指向了相应数组中的一个元素,我们我可以通过*cur_name*cur_age来打印它们,这里的意思是“cur_namecur_age指向的值”。

这个看似简单的程序却包含了大量的信息,其目的是在我向你讲解之前尝试让你自己弄清楚指针。直到你写下你认为指针做了什么之前,不要往下阅读。

你会看到什么

在你运行这个程序之后,尝试根据打印出的每一行追溯到代码中产生它们的那一行。在必要情况下,修改printf调用来确认你得到了正确的行号:

$ make ex15
cc -Wall -g    ex15.c   -o ex15
$ ./ex15
Alan has 23 years alive.
Frank has 43 years alive.
Mary has 12 years alive.
John has 89 years alive.
Lisa has 2 years alive.
---
Alan is 23 years old.
Frank is 43 years old.
Mary is 12 years old.
John is 89 years old.
Lisa is 2 years old.
---
Alan is 23 years old again.
Frank is 43 years old again.
Mary is 12 years old again.
John is 89 years old again.
Lisa is 2 years old again.
---
Alan lived 23 years so far.
Frank lived 43 years so far.
Mary lived 12 years so far.
John lived 89 years so far.
Lisa lived 2 years so far.
$

解释指针

当你写下一些类似ages[i]的东西时,你实际上在用i中的数字来索引ages。如果i的值为0,那么就等同于写下ages[0]。我们把i叫做下标,因为它是ages中的一个位置。它也能称为地址,这是“我想要ages位于地址i处的整数”中的说法。

如果i是个下标,那么ages又是什么?对C来说ages是在计算机中那些整数的起始位置。当然它也是个地址,C编译器会把任何你键入ages的地方替换为数组中第一个整数的地址。另一个理解它的办法就是把ages当作“数组内部第一个整数的地址”,但是它是整个计算机中的地址,而不是像i一样的ages中的地址。ages数组的名字在计算机中实际上是个地址。

这就产生了一种特定的实现:C把你的计算机看成一个庞大的字节数组。显然这样不会有什么用处,于是C就在它的基础上构建出类型和大小的概念。你已经在前面的练习中看到了它是如何工作的,但现在你可以开始了解C对你的数组做了下面一些事情:

  • 在你的计算机中开辟一块内存。
  • ages这个名字“指向”它的起始位置。
  • 通过选取ages作为基址,并且获取位置为i的元素,来对内存块进行索引。
  • ages+i处的元素转换成大小正确的有效的int,这样就返回了你想要的结果:下标i处的int

如果你可以选取ages作为基址,之后加上比如i的另一个地址,你是否就能随时构造出指向这一地址的指针呢?是的,这种东西就叫做指针。这也是cur_agecur_name所做的事情,它们是指向计算机中这一位置的变量,agesnames就处于这一位置。之后,示例程序移动它们,或者做了一些算数运算,来从内存中获取值。在其中一个实例中,只是简单地将cur_age加上i,这样等同于array[i]。在最后一个for循环中,这两个指针在没有i辅助的情况下自己移动,被当做数组基址和整数偏移合并到一起的组合。

指针仅仅是指向计算机中的某个地址,并带有类型限定符,所以你可以通过它得到正确大小的数据。它类似于将agesi组合为一个数据类型的东西。C了解指针指向什么地方,所指向的数据类型,这些类型的大小,以及如何为你获取数据。你可以像i一样增加它们,减少它们,对他们做加减运算。然而它们也像是ages,你可以通过它获取值,放入新的值,或执行全部的数组操作。

指针的用途就是让你手动对内存块进行索引,一些情况下数组并不能做到。绝大多数情况中,你可能打算使用数组,但是一些处理原始内存块的情况,是指针的用武之地。指针向你提供了原始的、直接的内存块访问途径,让你能够处理它们。

在这一阶段需要掌握的最后一件事,就是你可以对数组和指针操作混用它们绝大多数的语法。你可以对一个指针使用数组的语法来访问指向的东西,也可以对数组的名字做指针的算数运算。

实用的指针用法

你可以用指针做下面四个最基本的操作:

  • 向OS申请一块内存,并且用指针处理它。这包括字符串,和一些你从来没见过的东西,比如结构体。
  • 通过指针向函数传递大块的内存(比如很大的结构体),这样不必把全部数据都传递进去。
  • 获取函数的地址用于动态调用。
  • 对一块内存做复杂的搜索,比如,转换网络套接字中的字节,或者解析文件。

对于你看到的其它所有情况,实际上应当使用数组。在早期,由于编译器不擅长优化数组,人们使用指针来加速它们的程序。然而,现在访问数组和指针的语法都会翻译成相同的机器码,并且表现一致。由此,你应该每次尽可能使用数组,并且按需将指针用作提升性能的手段。

指针词库

现在我打算向你提供一个词库,用于读写指针。当你遇到复杂的指针语句时,试着参考它并且逐字拆分语句(或者不要使用这个语句,因为有可能并不好):

type *ptr

type类型的指针,名为ptr

*ptr

ptr所指向位置的值。

*(ptr + i)

ptr所指向位置加上i)的值。

译者注:以字节为单位的话,应该是ptr所指向的位置再加上sizeof(type) * i

&thing

thing的地址。

type *ptr = &thing

名为ptrtype类型的指针,值设置为thing的地址。

ptr++

自增ptr指向的位置。

我们将会使用这份简单的词库来拆解这本书中所有的指针用例。

指针并不是数组

无论怎么样,你都不应该把指针和数组混为一谈。它们并不是相同的东西,即使C让你以一些相同的方法来使用它们。例如,如果你访问上面代码中的sizeof(cur_age),你会得到指针的大小,而不是它指向数组的大小。如果你想得到整个数组的大小,你应该使用数组的名称age,就行第12行那样。

译者注,除了sizeof&操作和声明之外,数组名称都会被编译器推导为指向其首个元素的指针。对于这些情况,不要用“是”这个词,而是要用“推导”。

如何使它崩溃

你可以通过将指针指向错误的位置来使程序崩溃:

  • 试着将cur_age指向names。可以需要C风格转换来强制执行,试着查阅相关资料把它弄明白。
  • 在最后的for循环中,用一些古怪的方式使计算发生错误。
  • 试着重写循环,让它们从数组的最后一个元素开始遍历到首个元素。这比看上去要困难。

附加题

  • 使用访问指针的方式重写所有使用数组的地方。
  • 使用访问数组的方式重写所有使用指针的地方。
  • 在其它程序中使用指针来代替数组访问。
  • 使用指针来处理命令行参数,就像处理names那样。
  • 将获取值和获取地址组合到一起。
  • 在程序末尾添加一个for循环,打印出这些指针所指向的地址。你需要在printf中使用%p
  • 对于每一种打印数组的方法,使用函数来重写程序。试着向函数传递指针来处理数据。记住你可以声明接受指针的函数,但是可以像数组那样用它。
  • for循环改为while循环,并且观察对于每种指针用法哪种循环更方便。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容

  • 指针是C语言中广泛使用的一种数据类型。 运用指针编程是C语言最主要的风格之一。利用指针变量可以表示各种数据结构; ...
    朱森阅读 3,424评论 3 44
  • 重新系统学习下C++;但是还是少了好多知识点;socket;unix;stl;boost等; C++ 教程 | 菜...
    kakukeme阅读 19,793评论 0 50
  • C语言指针的总结 1. 变量 不同类型的变量在内存中占据不同的字节空间。 内存中存储数据的最小基本单位是字节,每一...
    xx_cc阅读 3,689评论 11 39
  • 《冈仁波齐》是由张杨执导的电影,由尼玛扎堆、杨培、斯朗卓嘎等主演。 该片主要讲述了尼玛扎堆等十一个藏...
    时空说阅读 204评论 0 0
  • 当第一缕阳光划过树梢,鸟儿在枝头鸣叫,我便肩负着喜悦的心情,将要去邂逅一片未知的天地,一个斑斓的巴蜀宝地。 收拾好...
    挪威的森林126阅读 268评论 0 0