你需要知道的关于C中指针的一切(2010)

2020-09-15 17:56:32

本文档附带一个附带的示例程序,可作为一个文件或多个文件(压缩)使用。

此变量会占用一些内存。在当前主流的Intel处理器上,它占用4字节的内存(因为int是4字节宽)。

Foo_ptr被声明为指向int的指针。我们已将其初始化为指向foo。

正如我所说的,foo占用了一些内存。它在内存中的位置称为它的地址。&;foo是foo的地址(这就是&;被称为“地址操作符”的原因)。

把每个变量想象成一个盒子。Foo是大小为sizeof(Int)字节的框。这个盒子的位置就是它的地址。当您访问该地址时,您实际上访问了它所指向的框中的内容。

这适用于所有变量,无论其类型如何。事实上,从语法上讲,根本没有“指针变量”这回事:所有变量都是一样的。然而,有不同类型的变量。Foo';的类型是int。Foo_ptr';的类型为int*。(因此,“指针变量”实际上意味着“指针类型的变量”。)。

关键是指针不是变量!指向foo的指针是foo_ptr的内容。您可以在foo_ptr框中放置不同的指针,该框仍然是foo_ptr。但它将不再指向Foo。

顺便说一下,指针也有一个类型。其类型为int。因此,它是一个“int指针”(指向int的指针)。Int的类型是int*(它指向指向int的指针)。指向指针的指针的使用称为多个间接。稍后会详细介绍这一点。

并且单个声明可以通过简单地提供逗号分隔的列表(ptr_a,ptr_b)来声明相同类型的多个变量,

然后,只需给出整型指针类型(int*),后跟一个逗号分隔的变量名称列表(ptr_a,ptr_b),就可以声明多个整型指针变量。

当将一个类型传递到多个声明时,C';的声明语法忽略指针星号。如果将ptr_a和ptr_b的声明拆分为多个声明,则会得到以下结果:

可以将其视为为每个变量分配一个基本类型(Int),加上由星号数量表示的间接级别(ptr_b;s为0;ptr_a;s为1)。

可以清晰地进行单行声明。这是立竿见影的改善:

请注意,星号已移动。现在它就在单词ptr_a的旁边。这是一个微妙的联想含义。

最清楚的绝对是将每个声明都放在单独的行上,但这可能会占用大量的垂直空间。就用你自己的判断吧。

顺便说一句,C允许变量名称和星号前后有零个或多个级别的圆括号:

现在,如何将int赋值给这个指针呢?此解决方案可能很明显:

对指针变量的任何直接赋值都会更改变量中的地址,而不是该地址处的值。在本例中,foo_ptr的新值(即该变量中的新“指针”)是42。但是我们不知道这是否指向任何东西,所以它很可能不会。尝试访问此地址可能会导致分段冲突(读取:崩溃)。

(顺便说一下,当您尝试将int赋值给指针变量时,编译器通常会发出警告。GCC会说“警告:初始化使指针来自整数而不进行强制转换”。)。

在此声明中,解引用运算符(前缀*,不要与乘法运算符混淆)查找存在于某个地址的值。(这称为“加载”操作。)。

也可以写入解引用表达式(C语言的说法是:解引用表达式是一个左值,这意味着它可以出现在赋值的左侧):

请注意,我们使用[]表示法,因为我们声明的是一个数组。Int*数组在这里是非法的;编译器不会接受我们为其分配{45,67,89}初始值设定项。

C的一个很好的特性是,在大多数地方,当您再次使用name数组时,实际上将使用指向其第一个元素的指针(在C术语中,&;array[0])。这称为“衰减”:数组“衰减”为指针。ARRAY的大多数用法等同于将ARRAY声明为指针。

当然,也有一些案例并不等同。一种是为名称数组本身赋值(ARRAY=…)。-那是非法的。

另一种是将其传递给sizeof运算符。结果将是数组的总大小,而不是指针的大小(例如,在当前的Mac OS X系统上,使用上述数组的sizeof(Array)将计算为(sizeof(Int)=4)×3=12)。这说明您实际上正在处理一个数组,而不仅仅是一个指针。

例如,假设您想要将一个数组传递给printf。您可以:当您将数组作为参数传递给函数时,实际上是将指针传递给数组的第一个元素,因为数组会衰减为指针。您只能给printf提供指针,而不是整个数组。(这就是printf无法打印数组的原因:它需要告诉它数组中的类型以及有多少元素,格式字符串和参数列表都会很快变得混乱。)。

衰减是隐式&;;array==&;array==&;array[0]。在英语中,这些表达式读作“array”、“指向数组的指针”和“指向数组第一个元素的指针”(下标运算符[]比address-of运算符具有更高的优先级)。但在C语言中,这三个表达式的意思是一样的。

(如果“array”实际上是一个指针变量,则它们的含义并不相同,因为指针变量的地址与其内部的地址不同-因此,中间的表达式&;array将不等于其他两个表达式。仅当array确实是数组时,这三个表达式才完全相等。)。

Int*array_ptr=array;printf(";第一个元素:%i\n";,*(array_ptr++));printf(";第二个元素:%i\n";,*(array_ptr++));printf(";第三个元素:%i\n";,*array_ptr);

如果您不熟悉++运算符:它将变量加1,与变量+=1相同(请记住,因为我们使用的是后缀表达式ARRAY_PTR++,而不是前缀表达式++ARRAY_PTR,所以表达式的计算结果是递增前的ARRAY_PTR的值,而不是递增之后的值)。

嗯,指针的类型很重要。这里的指针类型是int。当您向指针添加或从指针中减去时,执行该操作的数量将乘以指针类型的大小。在我们的三个增量的情况下,您添加的每个1都乘以sizeof(Int)。

顺便说一句,虽然sizeof(Void)是非法的,但是void指针是递增或递减1字节的。

如果您对1==4感到疑惑,请记住,前面我提到过,在当前的英特尔处理器上,整数是四个字节。因此,在具有这种处理器的机器上,向int指针加1或从int指针减去1会将其更改4个字节。因此,1==4。(程序员幽默。)。

这是C的另一个秘密。下标运算符(array[0]中的[])与数组无关。

哦,当然可以,那是它最常见的用法。但请记住,在大多数上下文中,数组会衰减为指针。这就是其中之一:这是您传递给该操作符的指针,而不是数组。

Array指向数组的第一个元素;array_ptr设置为&;array[1],因此它指向数组的第二个元素。因此array_ptr[1]等效于array[2](array_ptr从数组的第二个元素开始,因此array_ptr的第二个元素是数组的第三个元素)。

此外,您可能会注意到,因为第一个元素是sizeof(Int)字节宽(是一个int),所以第二个元素是数组开始之前的sizeof(Int)字节。您是正确的:数组[1]等同于*(数组+1)。(请记住,添加到指针或从指针减去的数字乘以指针类型的大小,因此“1”会将sizeof(Int)字节加到指针值。)。

C中两种更有趣的类型是结构和联合。您可以使用struct关键字创建结构类型,并使用Union关键字创建联合类型。

这些类型的确切定义超出了本文的范围。可以说,结构或联合的声明如下所示:

块内的每个声明都称为成员。工会也有会员,但他们的用法不同。访问成员如下所示:

*d==c;取消引用一次(int*)将得到一个(int**)(3-1=2)。

**d==*c==b;取消引用一个(int*)两次,或取消引用一次(int**),得到一个(int*)(3-2=1;2-1=1)。

*d==**c==*b==a==3;取消引用(int*)三次、两次(int**)或一次(int*)将得到int(3-3=0;2-2=0;1-1=0)。

因此,可以将&;运算符视为添加星号(我称之为增加指针级别),而将*、->;和[]运算符视为删除星号(降低指针级别)。

涉及指针时,const关键字的用法略有不同。这两个声明等效:

在第一个示例中,int(即*ptr_a)是const;您不能将*ptr_a=42。在第二个示例中,指针本身是常量;您可以很好地更改*ptr_b,但不能(使用指针算法,例如ptr_b++)更改指针本身。

注意:所有这一切的语法似乎有点异乎寻常。它是。这让很多人感到困惑,甚至C巫师也是如此。请耐心听我说。

也可以获取函数的地址。而且,与数组类似,函数在使用其名称时会衰减为指针。比方说,如果你想知道strcpy的地址,你可以说strcpy或&;strcpy。(由于显而易见的原因,&;strcpy[0]不会成功。)

调用函数时,使用称为函数调用运算符的运算符。函数调用操作符在其左侧获取一个函数指针。

在本例中,我们将dst和src作为内部参数传递,将strcpy作为要调用的函数(即函数指针)传递:

Enum{str_length=18u};记住NUL终止符!char src[str_length]=";这是一个字符串。";,dst[str_length];strcpy(dst,src);函数调用运算符正在运行(请注意左侧的函数指针)。

Char*strcpy(char*dst,const char*src);一个普通函数声明,用于引用echar*(*strcpy_ptr)(char*dst,const char*src);指向类似strcpy函数的指针strcpy_ptr=strcpy;strcpy_ptr=&;strcpy;这太管用了,但这个不行。

请注意上面声明中*strcpy_ptr周围的圆括号。它们将表示返回类型(char*)的星号与表示变量指针级别(*strcpy_ptr-one级别,指向函数的指针)的星号分开。

Char*(*strcpy_ptr_noparams)(char*,const char*)=strcpy_ptr;参数名称已删除-类型仍然相同。

指向strcpy的指针类型是char*(*)(char*,const char*);您可能注意到这是上面的声明,减去了变量名。你会把这个用在石膏上。例如:

如您所料,指向函数指针的指针在括号内有两个星号:

Char*(*strcpies[3])(char*,const char*)={strcpy,strcpy,strcpy};char*(*strcpies[])(char*,const char*)={strcpy,strcpy,strcpy};数组大小可选,与everstrcpies[0](dst,src)相同;

这是一份病理声明,取自C99标准。“[此声明]声明函数f不带参数返回int,函数fip不带参数指定返回int的指针,以及不带参数指定的函数的指针pfi返回int。”(6.7.5.3[16])。

函数指针甚至可以是函数的返回值。这一部分真的很让人费解,所以稍微伸展一下你的大脑,以免有受伤的风险。

为了解释这一点,我将总结一下您到目前为止学到的所有声明语法。首先,声明一个指针变量:

该声明告诉我们指针类型(Char)、指针级别(*)和变量名(Ptr)。后两个可以放入括号中:

如果我们将第一个声明中的变量名替换为后跟一组参数的名称,会发生什么情况?

但我们也删除了指示指针级别的*-记住,此函数声明中的*是函数返回类型的一部分。因此,如果我们向后添加指针级别的星号(使用圆括号):

不过,等一下。如果这是一个变量,并且第一个声明也是一个变量,我们可以不用名称和一组参数替换此声明中的变量名吗?

不,我们可以!结果是返回函数指针的函数声明:

请记住,指向不带参数并返回int的函数的指针类型为int(*)(Void)。因此,该函数返回的类型是char*(*)(char*,const char*)(同样,内部*表示指针,外部*是指向函数的返回类型的一部分)。您可能还记得,这也是strcpy_ptr的类型。

因此,此函数(调用时不带参数)返回一个指向类strcpy函数的指针:

如果没有字符串类型,为什么到处都能看到对“C字符串”的引用?

事实是,“C字符串”的概念是虚构的(字符串文字除外)。没有字符串类型。C字符串实际上只是字符数组:

此数组长度为16字节:15个字符代表";I是Walrus";,外加NUL(字节值0)终止符。换句话说,str[15](最后一个元素)是0。这就是“字符串”结尾的信号方式。

这个习惯用法是指C语言有字符串类型的程度。但这就是它的全部:一个习语。但受以下各项支持的情况除外:

H中的函数用于字符串操作。但是,如果没有字符串类型,这怎么可能呢?

下面是简单函数strlen的一个可能实现,它返回字符串的长度(不包括NUL结束符):

Size_t strlen(const char*str){请注意此处的指针语法size_t len=0U;while(*(str++))++len;return len;}。

请注意指针算法和取消引用的使用。这是因为,尽管函数名为“字符串”,但这里没有“字符串”;只有一个指向至少一个字符的指针,最后一个是0。

Size_t strlen(const char*str){size_t i;for(i=0u;str[i];++i);循环退出时,i为字符串返回i的长度;}。

那个使用索引。正如我们前面发现的,它使用指针(不是数组,也绝对不是字符串)。

更改了将“”用作撇号的不当用法,改为使用正确的撇号。大多数字体仍然将撇号(‘';’)呈现为直接的单引号,但这不是我的问题。

更正了下标运算符(我以前称为索引运算符)的名称。

已将对PowerPC的引用替换为对英特尔处理器的引用。(幸运的是,没有必要改变事实。)

修复了示例代码中的几个编译错误和一些警告(除了前面提到的数组衰减讨论之外)。

在版权声明中将连字符更改为年份范围内的连字符(参考:芝加哥风格手册)。

在数组中添加了array==&;array==&;array[0](即衰减是隐式&;)。

改变句子,以大写字母开头,根据反馈,这样读起来应该更清楚。

阐明了标题中的1==4表达式,以及“指针算法”中++的用法。

本文档还提供zip格式。以前的版本(1.2.1、1.2、1.1和1.0)也可用。