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

9

我想理解以下代码:

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

它起源于openbsd操作系统源代码中的文件ctype.h。此函数检查字符是否为ascii范围内的控制字符或可打印字母。这是我的当前思路:
  1. 调用iscntrl('a'),并将'a'转换为它的整数值
  2. 首先检查_c是否为-1,然后返回0,否则...
  3. 将未定义指针指向的地址增加1
  4. 将此地址声明为长度为(unsigned char)((int)'a')的数组的指针
  5. 将按位与运算符应用于_C (0x20)和数组(???)
一些奇怪的地方,它确实起作用,每次返回0时给定的字符_c不是可打印字符。否则,当它是可打印的时,该函数只返回一个不感兴趣的整数值。我在第3步、第4步(有点)和第5步中存在理解问题。
谢谢任何帮助。

1
_ctype_ 本质上是一组位掩码的数组。它由感兴趣的字符引索。因此, _ctype_ ['A'] 将包含对应于 "alpha" 和 "uppercase" 的位, _ctype_ ['a'] 将包含对应于 "alpha" 和 "lowercase" 的位, _ctype_ ['1'] 将包含一个对应于 "digit" 的位等等。看起来 0x20 是 "control" 对应的位。但由于某种原因, _ctype_ 数组偏移了1,所以 'a' 的位实际上在 _ctype_ ['a'+1] 中。(这可能是为了让它即使没有额外的测试也能适用于 EOF) - Steve Summit
(unsigned char)的转换是为了处理字符有可能是带符号和负数的情况。 - Steve Summit
6个回答

3

_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函数采用转换为unsigned char的char值(在OpenBSD上,char是有符号的):正确的调用方式是char c; … iscntrl((unsigned char)c)。它们的优点是保证没有缓冲区溢出:如果应用程序调用了一个超出unsigned char范围且不是-1的值来调用iscntrl,则此函数返回的值可能没有意义,但至少不会导致崩溃或泄漏私人数据,这些数据恰好位于数组边界之外的地址。即使以char c; … iscntrl(c)的方式调用该函数,只要c不是-1,该值也是正确的。
特殊情况出现的原因是 -1 代表了 EOF。许多标准 C 函数操作一个 char,例如 getchar,将字符表示为 int 值,该值在 char 值的正范围内,使用特殊值 EOF == -1 表示无法读取任何字符。对于诸如 getchar 的函数,EOF 表示文件结尾,因此被称为end-of-file。Eric Postpischil 建议该代码最初只是return _ctype_[_c + 1],这可能是正确的: _ctype_[0] 是 EOF 的值。如果函数被误用,这种简单的实现易受缓冲区溢出的攻击,而当前实现避免了这种情况,正如上面所讨论的那样,但需要注意保留 HTML 标签。
如果v是数组中找到的值,v & _C测试v中是否设置了0x20位上的比特。数组中的值是字符所属类别的掩码:_C用于控制字符,_U用于大写字母等。

ctype + 1)[_c]将使用C标准指定的正确数组索引,因为用户有责任传递EOF或unsigned char值。对于其他值,C标准未定义行为。强制转换不用于实现C标准所需的行为。它是一种解决方法,用于防止程序员错误地传递负字符值导致的错误。但是,它是不完整或不正确的(也无法更正),因为-1字符值必然被视为EOF。 - Eric Postpischil
这也解释了 +1 的含义。如果宏定义之前没有包含这个防御性调整,那么它可以仅被实现为 ((_ctype_+1)[_c] & _C),因此具有以预调整值-1到255索引的表。因此,第一个条目不会被跳过并且确实有用。当后来有人添加了防御性转换时,EOF 值为-1将无法使用该转换,因此他们添加了条件运算符以特殊处理它。 - Eric Postpischil

3

_ctype_似乎是一个受限制的内部符号表版本,我猜测+1是因为他们没有保存索引0,因为它不可打印。或者可能是使用了一种从1开始计数的表格,而不是C中惯用的从0开始计数的表格。

C标准规定所有ctype.h函数的参数都为int类型,其值应表示为unsigned char或等于宏EOF的值。

  • int iscntrl(int _c),int类型实际上是字符,但所有ctype.h函数都需要处理EOF,所以它们必须是int类型。
  • 对-1的检查是对EOF的检查,因为它的值为-1。
  • _ctype+1是指针算术运算,以获取数组项的地址。
  • [(unsigned char)_c]仅仅是访问该数组的一个元素,强制转换是为了强制要求参数应该表示为unsigned char。注意,char实际上可以包含负值,因此这是一种防御性编程。[]操作符访问结果是来自其内部符号表的单个字符。
  • &运算符是为了从符号表中获取某些字符。显然,所有具有设置第5位(掩码0x20)的字符都是控制字符。没有查看表格就无法理解这个意义。
  • 任何具有设置第5位的位都将返回与0x20掩码相掩蔽后的值,该值为非零值。这满足了函数在布尔真时返回非零的要求。

强制类型转换不能满足标准要求值可表示为“unsigned char”。标准要求,在调用函数时,该值必须已经可以表示为“unsigned char”,或等于“EOF”。强制类型转换仅用作“防御性”编程:纠正程序员在使用ctype.h宏时本应传递无符号char值,但实际却传递了有符号char(或signed char)的错误。需要注意的是,在使用-1表示EOF的实现中,当传递一个值为-1的char值时,这种方式不能纠正错误。 - Eric Postpischil
这也解释了 +1 的含义。如果宏定义之前没有包含这个防御性调整,那么它可以仅被实现为 ((_ctype_+1)[_c] & _C),因此具有以预调整值-1到255索引的表。因此,第一个条目不会被跳过并且确实有用。当后来有人添加了防御性转换时,EOF 值为-1将无法使用该转换,因此他们添加了条件运算符以特殊处理它。 - Eric Postpischil

2
我将从第三步开始:
增加未定义指针所指向的地址1。
该指针并非未定义,而是在其他编译单元中定义。这就是“extern”部分告诉编译器的内容。因此,当所有文件链接在一起时,链接器将解析对它的引用。
那么它指向什么?
它指向一个包含有关每个字符信息的数组。每个字符都有自己的条目。 条目是字符特征的位图表示。例如:如果设置了第5位,则表示该字符是控制字符。另一个例子:如果设置了第0位,则表示该字符是大写字符。
因此,类似于(_ctype_ + 1)['x']的内容将获取适用于'x'的特性。然后执行按位与运算以检查是否设置了第5位,即检查它是否为控制字符。
添加1的原因可能是真正的索引0保留用于某些特殊目的。

1
所有这里的信息都基于对源代码的分析(和编程经验)。
声明:
extern const char *_ctype_;

告诉编译器有一个名为_ctype_的指向const char的指针存在。
(4) 这个指针被当作数组来访问。
(_ctype_ + 1)[(unsigned char)_c]

使用强制类型转换 (unsigned char)_c 确保索引值位于 unsigned char 的范围内(0..255)。

指针运算 _ctype_ + 1 有效地将数组位置向右移动了一个元素。我不知道为什么他们要这样实现数组。对于字符值 0..255,使用范围 _ctype_[1].._ctype[256] 留下了值 _ctype_[0] 在此函数中未使用。(可以用几种替代方法来实现偏移量为1。)

数组访问使用字符值作为数组索引检索一个值(类型为 char,以节省空间)。

(5) 按位与操作从值中提取单个位。

显然,数组中的值被用作位字段,其中第5位(从最低有效位开始计数,= 0x20)是“控制字符”的标志。因此,该数组包含描述字符属性的位字段值。


我猜他们将 +1 移到指针上是为了清楚地访问元素 1..256 而不是 1..255,0_ctype_[1 + (unsigned char)_c] 由于隐式转换为 int,也是等效的。而 _ctype_[(_c & 0xff) + 1] 更加清晰简洁。 - cmaster - reinstate monica

0
关键在于理解表达式(_ctype_ + 1)[(unsigned char)_c]的作用(然后将其提供给按位与操作& 0x20以获取结果!)
简短回答:它返回指向_ctype_数组的_c + 1元素。
怎么做?
首先,尽管您似乎认为_ctype_未定义的,但实际上并非如此!头文件声明其为外部变量——但在构建程序时链接到其中一个运行时库中时,它会被定义。
为了说明语法如何对应到数组索引,请尝试编译以下简短程序:
#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;
}

如有需要进一步澄清和/或解释,请随时提问。


0

ctype.h 中声明的函数接受 int 类型的对象。对于用作参数的字符,假定它们已经被转换为 unsigned char 类型。该字符用作表中的索引,以确定字符的特性。

_c 包含 EOF 的值时,似乎使用检查 _c == -1。如果不是 EOF,则将 _c 转换为无符号字符类型,该类型用作由表达式 _ctype_ + 1 指向的表中的索引。如果由掩码 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)(这里使用了指针算术),而integral_expression即为索引,其值为表达式(unsigned char)_c


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接