使用按位与运算符指向数组声明的 C 指针

C pointer to array declaration with bitwise and operator

我想了解以下代码:

//...
#define _C 0x20
extern const char *_ctype_;
//...
__only_inline int iscntrl(int _c)
{
    return (_c == -1 ? 0 : ((_ctype_ + 1)[(unsigned char)_c] & _C));
}

来源于obenbsd操作系统源码文件ctype.h。此函数检查 char 是控制字符还是 ascii 范围内的可打印字母。这是我目前的思路:

  1. iscntrl('a') 被调用并且 'a' 被转换成它的整数值
  2. 首先检查 _c 是否为 -1 然后 return 0 否则...
  3. 将未定义指针指向的地址加1
  4. 将此地址声明为指向长度为 (unsigned char)((int)'a')
  5. 的数组的指针
  6. 将按位与运算符应用于 _C (0x20) 和数组 (???)

不知何故,奇怪的是,它起作用了,每次当 0 被 returned 时,给定的 char _c 不是可打印的字符。否则,当它可打印时,函数只是 returns 一个没有任何特殊意义的整数值。我理解的问题在步骤3、4(有点)和5。

感谢您的帮助。

ctype.h 中声明的函数接受类型 int 的对象。对于用作参数的字符,假设它们被初步转换为 unsigned char 类型。此字符用作 table 中的索引,确定字符的特征。

似乎检查 _c == -1 是在 _c 包含 EOF 的值的情况下使用的。如果它不是 EOF,则 _c 被转换为 unsigned char 类型,用作表达式 _ctype_ + 1 指向的 table 中的索引。如果设置了掩码 0x20 指定的位,则该字符是控制符号。

理解表达式

(_ctype_ + 1)[(unsigned char)_c]

考虑到数组下标是定义为

的后缀运算符
postfix-expression [ expression ]

你可能不会这样写

_ctype_ + 1[(unsigned char)_c]

因为这个表达式等同于

_ctype_ + ( 1[(unsigned char)_c] )

所以表达式_ctype_ + 1被括在括号中得到一个初级表达式。

所以事实上你有

pointer[integral_expression]

生成索引处的数组对象,该索引计算为表达式 integral_expression,其中指针为 (_ctype_ + 1)(此处使用指针 arithmetuc),integral_expression 即索引是表达式 (unsigned char)_c.

_ctype_ 是一个指向 257 字节全局数组的指针。不知道_ctype_[0]有什么用。 _ctype_[1]_ctype_[256]_分别表示字符0,...,255的字符类别:_ctype_[c + 1]表示字符c的类别。这和说 _ctype_ + 1 指向一个 256 个字符的数组是一样的,其中 (_ctype_ + 1)[c] 表示字符的类别 c.

(_ctype_ + 1)[(unsigned char)_c] 不是声明。它是一个使用数组下标运算符的表达式。它正在访问从 (_ctype_ + 1).

开始的数组的位置 (unsigned char)_c

_cint 转换为 unsigned char 的代码并非绝对必要:ctype 函数将 char 值转换为 unsigned charchar 已签名在 OpenBSD 上):正确的调用是 char c; … iscntrl((unsigned char)c)。它们的优点是保证没有缓冲区溢出:如果应用程序调用 iscntrl 的值超出 unsigned char 的范围并且不是 -1,则此函数 returns一个可能没有意义但至少不会导致崩溃或恰好位于数组边界之外的地址的私有数据泄漏的值。如果函数被称为 char c; … iscntrl(c),只要 c 不是 -1,该值甚至是正确的。

-1 的特殊情况是因为它是 EOF。许多对 char 进行操作的标准 C 函数,例如 getchar,将字符表示为 int 值,即换行到正范围的 char 值,并使用特殊值 EOF == -1 表示无法读取任何字符。对于像 getchar 这样的函数,EOF 表示文件的结尾,因此名称为 end-of- f文件。 Eric Postpischil 表明代码最初只是 return _ctype_[_c + 1],这可能是正确的:_ctype_[0] 将是 EOF 的值。如果函数被滥用,这个更简单的实现会导致缓冲区溢出,而当前的实现如上所述避免了这种情况。

如果 v 是在数组中找到的值,v & _C 测试 0x20 的位是否在 v 中设置。数组中的值是字符所属类别的掩码:_C 设置为控制字符,_U 设置为大写字母等。

此处所有信息均基于对源代码(和编程经验)的分析。

宣言

extern const char *_ctype_;

告诉编译器有一个指针指向 const char 某处名为 _ctype_.

(4) 该指针作为数组访问。

(_ctype_ + 1)[(unsigned char)_c]

强制转换 (unsigned char)_c 确保索引值在 unsigned char (0..255).

范围内

指针算法_ctype_ + 1有效地将数组位置移动1个元素。我不知道他们为什么要这样实现数组。将范围 _ctype_[1].._ctype[256] 用于字符值 0..255 会使值 _ctype_[0] 未用于此函数。 (1 的偏移量可以通过多种替代方式实现。)

数组访问使用字符值作为数组索引检索一个值(类型 char,以保存 space)。

(5) 按位与运算从值中提取一位。

显然,数组中的值用作位字段,其中第 5 位(从 0 开始至少有效位计数,= 0x20)是 "is a control character" 的标志。因此数组包含描述字符属性的位域值。

这里的关键是理解表达式(_ctype_ + 1)[(unsigned char)_c]的作用(然后将其馈送到按位与运算,& 0x20得到结果!

简短回答:returns 数组 pointed-to 的元素 _c + 1_ctype_.

怎么办?

首先,虽然您似乎认为 _ctype_undefined 但实际上不是! header 声明 它是一个外部变量 - 但它是在(几乎可以肯定)你的程序在构建时链接的 run-time 库之一中定义的它。

为了说明语法如何对应于数组索引,请尝试完成(甚至编译)以下短程序:

#include <stdio.h>
int main() {
    // Code like the following two lines will be defined somewhere in the run-time
    // libraries with which your program is linked, only using _ctype_ in place of _qlist_ ...
    const char list[] = "abcdefghijklmnopqrstuvwxyz";
    const char* _qlist_ = list;
    // These two lines show how expressions like (a)[b] and (a+1)[b] just boil down to
    // a[b] and a[b+1], respectively ...
    char p = (_qlist_)[6];
    char q = (_qlist_ + 1)[6];
    printf("p = %c  q = %c\n", p, q);
    return 0;
}

随时要求进一步澄清and/or解释。

_ctype_ 似乎是符号 table 的受限内部版本,我猜 + 1 是他们没有费心保存索引 0因为那个不是 printable。或者他们可能使用 1-indexed table 而不是 C 中自定义的 0-indexed

C 标准规定了所有 ctype.h 函数:

In all cases the argument is an int, the value of which shall be representable as an unsigned char or shall equal the value of the macro EOF

逐步查看代码:

  • int iscntrl(int _c) int 类型确实是字符,但是所有 ctype.h 函数都需要处理 EOF,所以它们必须是 int.
  • 针对 -1 的检查是针对 EOF 的检查,因为它的值为 -1
  • _ctype+1是获取数组项地址的指针算法。
  • [(unsigned char)_c] 只是该数组的数组访问,其中强制转换是为了强制执行将参数表示为 table 为 unsigned char 的标准要求。请注意 char 实际上可以持有负值,因此这是防御性编程。 [] 数组访问的结果是来自其内部符号 table.
  • 的单个字符
  • & 掩码用于从符号 table 中获取特定的一组字符。显然,所有设置了位 5(掩码 0x20)的字符都是控制字符。如果不查看 table.
  • 就无法理解这一点
  • 任何设置了第 5 位的值都将 return 用 0x20 屏蔽的值,这是一个非零值。这满足了函数 return 在布尔值 true 的情况下非零的要求。

我将从第 3 步开始:

increment the adress the undefined pointer points to by 1

指针未定义。它只是在其他一些编译单元中定义的。这就是 extern 部分告诉编译器的内容。所以当所有文件链接在一起时,链接器将解析对它的引用。

那么它指向什么?

它指向一个包含每个字符信息的数组。每个角色都有自己的条目。条目是字符特征的位图表示。例如:如果第 5 位被设置,则表示该字符是一个控制字符。又如:如果设置了bit 0,则表示该字符为大写字符。

所以像 (_ctype_ + 1)['x'] 这样的东西将获得适用于 'x' 的特征。然后按位和执行检查第5位是否设置,即检查它是否是控制字符。

加1的原因可能是真正的索引0是为某些特殊目的保留的。