你所不知道的C語言:指針篇

说明:头条文章无法添加外链接,因此所有带有外链接的部分统一使用下划线来表示。

即便你对指针毫无掌握,你还是能写程序

Ólafur Waage在CppCon 2018有个5分钟的演讲“ Let's learn programming by inventing it ”提及,学习C语言的过程,以K&R一书为例,许多人闻风丧胆的「指针」一直到第5章,约全书一半才提到,可解读为「你可以在不懂指针是什么之前,仍可掌握多数的C语言功能」。

这个讲座并非「头脑风暴」

  • stackoverflow上的头脑风暴
<code>头脑风暴链接:https://stackoverflow.com/questions/8208021/how-to-increment-a-pointer-address-and-pointers-value/8208106#8208106/<code>

取自C Traps and Pitfalls的案例“Understanding Declarations”:

<code>(*(void(*)())0)();/<code>

可改写为以下叙述:

<code>typedef void (*funcptr)();
(* (funcptr) 0)();/<code>
  • godbolt :直接在网页上看到gcc生成的程序源码
<code>godbolt链接:https://gcc.godbolt.org//<code>
<code>int main() {
  typedef void (*funcptr)();
  (* (funcptr) (void *) 0)();
 } /<code>

对应的组合语言,搭配-Os(空间最佳化)

<code>main: pushq %rax 
					xorl %eax, %eax 
					call *%rax 
					xorl %eax, %eax 
					popq %rdx 
					ret/<code>
你所不知道的C语言:指针篇

一道面试题:

<code>void **(*d) (int &, char **(*)(char *, char **));/<code>

上述声明的解读:

  • d is a pointer to a function that takes two parameters:
  1. a reference to an int and
  2. a pointer to a function that takes two parameters:

a pointer to a char and

a pointer to a pointer to a char

3.and returns a pointer to a pointer to a char

  • and returns a pointer to a pointer to void

signal函数的声明方式也很经典:

  • How to read this prototype?
<code>链接:https://stackoverflow.com/questions/15739500/how-to-read-this-prototype/<code>

Go 语言也有指针

1999年4月27日,Ken Thompson和Dennis Ritchie自美国总统柯林顿手中接过1998年National Medal of Technology (国家科技奖章),隔年12月,时年58岁的Ken Thompson自贝尔实验室退休。Ken Thompson自贝尔实验室退休后成为一名飞行员。大概是整日翱翔天际,获得颇多启发,在2006年,他进入Google工作,隔年他和过去贝尔实验室的同僚Rob Pike及Robert Griesemer等人在公司内部提出崭新的Go程序语言,后者可用于云端运算在内的众多领域。

指针这个好东西,当然也要从C 语言带过去给Go 语言,连同美妙的struct。

  • 根据第一份Golang Talk,Robert Griesemer, Ken Thompson及Rob Pike等三人认为,世界在变,但系统语言却已十年未有剧烈变革
  • Go 之前的程序语言未能达到:

新增函数库不是一个正确的方向

需要重新思考整个架构来开发新的程序语言

在实践层面,pointer 和struct 往往是成双成对存在(下方会解释)

先罗列你已经知道的部分

  • C 语言: 超好懂的指针
<code>链接地址:https://kopu.chat/2017/05/15/c%E8%AA%9E%E8%A8%80-%E8%B6%85%E5%A5%BD%E6%87%82%E7%9A%84%E6%8C%87%E6%A8%99%EF%BC%8C%E5%88%9D%E5%AD%B8%E8%80%85%E8%AB%8B%E9%80%B2%EF%BD%9E//<code>
  • Everything you need to know about pointers in C
<code>链接地址:https://boredzo.org/pointers//<code>
  • 疑惑

该如何解释qsort(3)的参数和设计考量呢?

为何我看到的程序往往写成类似下面这样?

<code>struct list **lpp; 
for (lpp = &list; *lpp != NULL; lpp = &(*lpp)->next)/<code>

回头看C 语言规范

在开发工具和规范标准篇提过参考第一手资料的重要性,以下ISO/IEC 9899 (简称“C99”)和指针相关的描述:

  • 规范 搜寻“ object ”,共出现735处

搜寻“ pointer ”,共出现637处。有趣的是,许多教材往往不谈object,而是急着谈论pointer,殊不知,这两者其实就是一体两面

object != object-oriented前者的重点在于「数据表达法」,后者的重点在于“everything is object”

C11 ( ISO/IEC 9899:201x ) / 网页版

  • & 不要都念成and,涉及指针操作的时候,要读为“address of”

C99 标准[6.5.3.2] Address and indirection operators 提到'&' address-of operator

  • C99 [3.14] object

region of data storage in the execution environment, the contents of which can represent values

C 语言的对象就指在执行时期,数据储存的区域,可以明确表示数值的内容

很多人误认在C 语言程序中,(int) 7 和(float) 7.0 是等价的,其实以数据表示的角度来看,这两者截然不同,前者对应到二进位的“111”,而后者以IEEE 754 表示则大异于“111”

  • C99 [6.2.4] Storage durations of objects

An object has a storage duration that determines its lifetime. There are three storage durations: static, automatic, and allocated.

注意生命周期(lifetime)的概念,中文讲「初始化」时,感觉像是「盘古开天」,很容易令人误解。其实initialize的英文意义很狭隘: “to set (variables, counters, switches, etc.) to their starting values at the beginning of a program or subprogram.”

The lifetime of an object is the portion of program execution during which storage is guaranteed to be reserved for it. An object exists, has a constant address and retains its last-stored value throughout its lifetime. If an object is referred to outside of its lifetime, the behavior is undefined.

在object 的生命周期以内,其存在就意味着有对应的常数内存地址。注意,C 语言永远只有call-by-value

The value of a pointer becomes indeterminate when the object it points to reaches the end of its lifetime.

作为object操作的「代名词」(alias)的pointer,倘若要在object生命周期以外的时机,去取出pointer所指向的object内含值,是未知的。考虑先做ptr = malloc(size); free(ptr);倘若之后做*ptr,这个allocated storage已经超出原本的生命周期

An object whose identifier is declared with no linkage and without the storage-class specifier static has automatic storage duration.

  • C99 [6.2.5] Types

A pointer type may be derived from a function type, an object type, or an incomplete type, called the referenced type. A pointer type describes an object whose value provides a reference to an entity of the referenced type. A pointer type derived from the referenced type T is sometimes called ''pointer to T''. The construction of a pointer type from a referenced type is called ''pointer type derivation''.

注意到术语!这是C语言只有call-by-value的实证,函数的传递都涉及到数值,这里的“incomplete type”要注意看,稍后会解释。要区分char []和char *

Arithmetic types and pointer types are collectively called scalar types. Array and structure types are collectively called aggregate types.

注意“scalar type”这个术语,日后我们看到++, --, +=, -=等操作,都是对scalar (标量)标量只有大小,它们可用数目及单位来表示(例如温度= 30 o C)。标量遵守算数和普通的代数法则。注意:标量有「单位」(可用sizeof操作得知单位的「大小」),假设ptr是个pointer type,对ptr++来说,并不是单纯ptr = ptr + 1,而是递增或递移1个「单位」

An array type of unknown size is an incomplete type. It is completed, for an identifier of that type, by specifying the size in a later declaration (with internal or external linkage). A structure or union type of unknown content is an incomplete type . It is completed, for all declarations of that type, by declaring the same structure or union tag with its defining content later in the same scope.

这是C/C++常见的forward declaration技巧的原理,比方说我们可以在头文件声明struct GraphicsObject;(不用给内部定义)然后struct GraphicsObject *initGraphics(int width, int height);是合法的,但struct GraphicsObject obj;不合法

Array, function, and pointer types are collectively called derived declarator types. A declarator type derivation from a type T is the construction of a derived declarator type from T by the application of an array-type, a function-type, or a pointer- type derivation to T.

这句话很重要,看起来三个不相关的术语「数组」、「函数」,以及「指针」都称为derived declarator types,读到此处会觉得惊讶的人,表示不够理解C 语言

你所不知道的C语言:指针篇

“derivative”这词在是微积分学中就是导数。一个函数在某一点的导数描述了这个函数在这一点附近的变化率。导数的本质是通过极限的概念对函数进行局部的线性逼近。

你所不知道的C语言:指针篇

回到C 语言,你看到一个数值,是scalar,但可能也是自某个派生出的declarator type derivation,实际对应到array, function, pointer 等型态的derivation

(练习题)设定绝对地址为0x67a9的32-bit整数变数的值为0xaa6,该如何写?

<code>*(int32_t * const) (0x67a9) = 0xaa6;
/* Lvalue *//<code>
  • A pointer to void shall have the same representation and alignment requirements as a pointer to a character type.

关键描述!规范void *和char *彼此可互换的表示法

<code>void *memcpy(void *dest, const void *src, size_t n);/<code>

英文很重要

安装cdecl程序,可以帮你产生C程序声明。

<code>$ sudo apt-get install cdecl/<code>

使用案例:

<code>$ cdecl
cdecl> declare a as array of pointer to function returning pointer to function returning pointer to char/<code>

会得到以下输出:

<code>char *(*(*a[])())()/<code>

把前述C99 规范的描述带入,可得:

<code>cdecl> declare array of pointer to function returning struct tag
struct tag (*var[])()/<code>

如果你没办法用英文来解说C 程序的声明,通常表示你不理解!

cdecl 可解释C 程序声明的意义,比方说:

<code>cdecl> explain char *(*fptab[])(int)
declare fptab as array of pointer to function (int) returning pointer to char/<code>

void * 之谜

  • void 在最早的C 语言是不存在的,直到C89 才确立,为何要设计这样的类型呢?
<code>链接地址:https://www.bell-labs.com/usr/dmr/www/primevalC.html/<code>

最早的C语言中,任何函数若没有特别标注返回类型,一律变成int(伴随着0作为返回值),但这导致无从验证function prototype和实际使用的状况

  • void * 的设计,导致开发者必须透过 explicit (显式) 或强制转型,才能存取最终的object,否则就会丢出编译器的错误信息,从而避免危险的指针操作

我们无法直接对void *做数值操作

<code>void *p = ...; 
void *p2 = p + 1; /* what exactly is the size of void? */ /<code>

换句话说,void *存在的目的就是为了强迫使用者使用显式转换类型或是强制转换类型,以避免Undefined behavior 产生

C/C++ Implicit conversion vs. Explicit type conversion

  • C99对sign extension的定义和解释
<code>链接地址:https://www.ptt.cc/bbs/C_and_CPP/M.1460791524.A.603.html/<code>
  • 对某硬件架构,像是ARM,我们需要额外的 alignment。ARMv5 (含)以前,若要操作32-bit整数(uint32_t),该指针必须对齐32-bit边界(否则会在dereference时触发exception)。于是,当要从void *位址读取uint16_t时,需要这么做:
<code>/* may receive wrong value if ptr is not 2-byte aligned */
/* portable way of reading a little-endian value */
uint16_t value = *(uint16_t *) ptr;
uint16_t value = *(uint8_t *) ptr | ((*(uint8_t *) (ptr + 1)) << 8);/<code>

延伸阅读: 内存管理、对齐及硬件特性

void * 真的万能吗?

依据C99 规格6.3.2.3:8 [ Pointers ]

A pointer to a function of one type may be converted to a pointer to a function of another type and back again; the result shall compare equal to the original pointer. If a converted pointer is used to call a function whose type is not compatible with the pointed-to type, the behavior is undefined.

换言之,C99不保证pointers to data (in the standard, “objects or incomplete types” eg char *or void *)和pointers to functions之间相互转换是正确的

  • 可能会招致undefined behavior (UB)
  • 注意:C99规范中,存在一系列的UB,详见未定义行为篇

延伸阅读

  • C 语言内存定址基础
  • The Lost Art of Structure Packing
<code>链接地址:http://www.catb.org/esr/structure-packing//<code>

没有「双指针」只有「指针的指针」

「双马尾」(左右「独立」的个体) 和「马尾的马尾」(由单一个体关联到另一个体的对应) 不同

  • 中文的「双」有「对称」且「独立」的含义,但这跟「指针的针」行为完全迥异
  • 讲「双指针」已经不是「懂不懂C 语言」,而是语言认知的问题

C 语言中,万物皆是数值(everything is a value),函数调用当然只有call-by-value。「指针的指针」(英文就是a pointer of a pointer) 是个常见用来改变「传入变量原始数值」的技巧

考虑以下程序:

<code>int B = 2;
void func(int *p) { p = &B; }
int main() {
    int A = 1, C = 3;
    int *ptrA = &A;
    func(ptrA);
    printf("%d\n", *ptrA);
    return 0;
}/<code>

在第5 行(含) 之前的内存示意:

你所不知道的C语言:指针篇

第6行ptrA数值传入func后的内存示意:

你所不知道的C语言:指针篇

func依据函数调用篇描述,将变数p的值指定为&B

你所不知道的C语言:指针篇

由上图可见,原本在main中的ptrA内存值没有改变。这不是我们期望的结果,该如何克服呢?可透过「指针的指针」来改写,如下:

<code>int B = 2;
void func(int **p) { *p = &B; }
int main() {
    int A = 1, C = 3;
    int *ptrA = &A;
    func(&ptrA);
    printf("%d\n", *ptrA);
    return 0;
}:/<code>

在第5 行(含) 之前的内存示意:

你所不知道的C语言:指针篇

第6行时,传入func的参数是&ptrA,也就是下图中的&ptrA(temp):

你所不知道的C语言:指针篇

进入func执行后,在编译器产生对应参数传递的程序中,会复制一份刚传入的&ptrA,产生一个自动变量p,将&ptrA内的值存在其中,示意如下:

你所不知道的C语言:指针篇

在func中把p指向到的值换成&B:

你所不知道的C语言:指针篇

经过上述「指针的指针」,*ptrA的数值从1变成2,而且ptrA指向的对象也改变了。

未完待续


分享到:


相關文章: