【问题标题】:Why does dereferencing a pointer to string (char array) returns the whole string instead of the first character?为什么取消引用指向字符串(字符数组)的指针会返回整个字符串而不是第一个字符?
【发布时间】:2016-08-25 21:30:22
【问题描述】:

由于指向数组的指针指向数组的第一个元素(具有相同的地址),我不明白为什么会发生这种情况:

#include <stdio.h>

int main(void) {    
    char (*t)[] = {"test text"};
    printf("%s\n", *t + 1); // prints "est text"
}

另外,为什么下面的代码会打印2呢?

#include <stdio.h>

int main(void) {    
    char (*t)[] = {1, 2, 3, 4, 5};
    printf("%d\n", *t + 1); // prints "2"
}

【问题讨论】:

  • %s 说明符从参数列表中获取 起始地址 并打印所有内容,直到第一个零。
  • 有没有办法打印第一个字符?
  • char (*t)[] = {"test text"}; 是一个指针数组,而不是指向数组的指针char t[] = "test text";char 类型的数组,其中t 是指向第一个元素的指针。
  • cdecl&gt; explain char (*t)[];declare t as pointer to array of char。我停在这里。

标签: c arrays string pointers pointer-arithmetic


【解决方案1】:

在撰写此答案时,所有其他答案都不正确。此外,您的问题闻起来像an XY problem,因为您尝试的构造很可能不是您想要的。你真正想做的只是:

char *t = "test text";
printf("%s\n", t);  // prints "test text"

printf("%c\n", t[1]); // prints "e", the 2nd character in the string.

但既然你想了解为什么会发生这些事情,而所有其他解释都是错误的,那么这里是:

您的声明将 t 声明为指向 char 数组的指针:

cdecl> explain char (*t)[];
declare t as pointer to array of char

不是其他人建议的指针数组。此外,*t 的类型不完整,因此您无法获取其大小:

sizeof *t;

会导致

error: invalid application of ‘sizeof’ to incomplete type ‘char[]’
     sizeof *t;

在编译时。


现在,当你尝试用

初始化它时
 char (*t)[] = {"test text"};

它会发出警告,因为虽然"test text" 是一个由(常量)char 组成的数组,但这里它会衰减到一个指向char 的指针。此外,那里的牙套也没用;上面的摘录等于写:

char (*t)[] = "test text";

不一样

int a = 42;

int a = {42};

是同义词。这是C。

要获得指向数组的指针,您必须在数组(字符串字面量!)上使用“address-of”运算符,以避免它衰减为指针:

char (*t)[] = &"test text";

现在t 被正确初始化为指向char 的(不可变)数组的指针。但是,在您的情况下,使用指向不正确类型的指针并不重要,因为这两个指针尽管是不兼容的类型,但指向相同的地址 - 只有一个指向字符数组,另一个指向第一个字符在那个字符数组中;因此观察到的行为是相同的。


当您取消引用t(指向char 的数组的指针)时,您将获得数组char 的定位器值(左值)。然后,在正常情况下,char 数组的左值将衰减为指向第一个元素的指针,就像他们通常做的那样,所以 *t + 1 现在将指向该数组中的第二个字符;然后printfing 该值将打印一个以 0 结尾的字符串的内容从该指针开始

%s 的行为在 C11 (n1570) 中指定为

[%s]

如果不存在l 长度修饰符,则参数应为指向初始值的指针 字符类型数组的元素。 数组中的字符是 写到(但不包括)终止空字符。 [...] 如果 精度未指定或大于数组的大小,数组应 包含一个空字符。 [...]

(强调我的。)


至于你的第二次初始化:

char (*t2)[] = {1, 2, 3, 4, 5};

如果你使用最新版本的 GCC 编译它,默认情况下你会收到很多警告,首先:

test.c:10:19: warning: initialization makes pointer from integer without a cast [-Wint-conversion]
   char (*t2)[] = {1, 2, 3, 4, 5};
                   ^

因此,1int 转换为指向数组的指针char 没有任何强制转换。

然后,在剩下的值中,编译器会报错:

y.c:10:19: note: (near initialization for ‘t2’)
y.c:10:21: warning: excess elements in scalar initializer
   char (*t2)[] = {1, 2, 3, 4, 5};
                      ^

也就是说,在您的情况下,2、3、4 和 5 被默默地忽略了。

因此,该指针的值现在为 1,例如在 x86 平面内存模型上,它将指向内存位置 1(尽管这自然是实现定义的):

printf("%p\n", (void*)t2);

打印(定义双重实现)

0x1

当你取消引用这个值(它是一个指向字符数组的指针)时,你会得到一个字符数组的左值,它从内存地址 1 开始。当你加 1 时,这个 array-of-char 左值将衰减为指向字符的指针,因此您将得到((char*)1) + 1,它是指向char 的指针,其值为2。该值的类型可以从GCC(5.4.0)默认生成的警告中验证:

y.c:5:10: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘char *’ [-Wformat=]
   printf("%d\n",*t2+1); //prints "2"
          ^

参数的类型是char *

现在您将(char*)2 作为参数传递给printf,以使用%d 进行转换,它需要int。这具有未定义的行为;在您的情况下,(char*)2 的字节模式被充分混淆地解释为2,因此它被打印出来。

现在人们意识到打印的值与原始初始化程序中的2 无关

#include <stdio.h>

int main(void) {
    char (*t2)[] = {1, 42};
    printf("%d\n", *t2 + 1);
}

仍将打印2,而不是42。 QED。


对于这两种初始化,您可以使用 C99 复合文字来初始化:

// Warning: this code is super *evil*
char (*t)[] = &(char []) { "test text" };
char (*t2)[] = &(char []) { 1, 2, 3, 4, 5 };

虽然这可能会比您想要的更少,而且生成的代码没有任何机会在 C89 或 C++ 编译器中编译。

【讨论】:

  • 有没有办法让 t2 指向数组 {1,2,3,4,5} 的第一个元素?
  • 为什么不直接创建一个从初始化程序{1, 2, 3, 4, 5} 初始化的char 数组?例如。 char t2[] = {1, 2, 3, 4, 5};。正如 Antti 的回答所指出的那样,在大多数情况下,它将 convert to pointer to char 指向第一个元素。
  • 我想明确地初始化指针。有没有办法我可以或者我不能的原因是因为没有为数组分配内存?
  • 那个地址-of-compound-literal-array-to-char 是邪恶的:D
【解决方案2】:

*t 将获取您的第一个元素,然后您添加 1,并且由于 指针算法 这意味着推进一个元素,这解释了为什么您会得到第二个元素。

现在,在第一种情况下,您使用%s 打印,它说打印字符串(直到遇到 NULL 终止符),而在第二种情况下,您使用%d 打印,只是一个数字。

如果您也想在第一种情况下使用%c 体验等效的打印行为,当然这需要强制转换。


顺便说一句,如前所述,通常不会这样做:

char (*t)[] = {"test text"};

它创建了一个指针数组,第一个元素是字符串,应该引发警告:

C02QT2UBFVH6-lm:~ gsamaras$ gcc -Wall main.c 
main.c:4:18: warning: incompatible pointer types initializing 'char (*)[]' with an expression of type 'char [10]'
      [-Wincompatible-pointer-types]
  char (*t)[] = {"test text"};
                 ^~~~~~~~~~~

正如奥拉夫所说,这是:

char (*t)[] = {&"test text"};

将使警告消失,因为您现在将字符串的地址分配给指针。

现在试着想想这会打印什么:

include <stdio.h>

int main(void) {
  char (*t)[] = {&"test text"};
  printf("%s\n", *t + 1);
  printf("%c\n", *(*t + 1));

  return 0;
}

第一个将按照您的预期进行,而第二个需要额外的取消引用,才能真正获得角色。


但是这样的事情很常见:

char t[] = "test text";

当然还有其他方法。


那么,在这种情况下,请问这个程序会打印什么?

#include <stdio.h>

int main(void) {
  char t[] = "test text";
  printf("%s\n", t + 1); 
  printf("%c\n", *(t + 1));
  return 0;
}

第一个print()会取t,因为解引用指向数组的第一个元素,即字符串的第一个字符,然后你给它加一个,但是因为它是一个指针,它由于指向下一个元素的指针算术而前进(因为我们执行+1。如果我们执行+2,它将推进2个元素,依此类推..)。

现在正如我上面解释的,%s 将打印整个字符串,从 printf() 参数的起始指针开始,直到它到达字符串的 NULL 终止符。

因此,这将打印“est text”。

第二个printf() 遵循相同的原理,但它的参数前面是* 运算符,这意味着给我指向的元素,即字符串的第二个字符。

由于我们使用%c,它只会打印那个字符,即“e”。

【讨论】:

  • “创建一个指针数组” - 嗯,不!正如 OP 所写,它是指向“char 数组”的指针。一个指针数组将是char *a[XYZ]。使用二维数组是一个典型的概念。
  • 我也认为它是一个指向字符串/字符数组的指针。 @olaf 我又困惑了。
  • 在初始化程序中尝试&amp;"test text"
  • @CodeFusion:它是一个指向char数组的指针!但是,由于索引运算符和所有其他算术都在指针上工作,而不是数组,我们通常使用比我们使用的数组少一维的指针。因此,对于指向标量的一维数组指针,对于指向一维标量数组的二维数组指针,等等
  • 我必须再次更正(最后一个 sn-p):t 不会衰减到指向第一个条目的指针,只是因为它是 t,而是因为对于那个特定的操作它使用(取消引用) - 使用指针和数组非常精确! t 仍然是一个数组,而不是一个指针!试试sizeof(t)(数组使用的字节数)或t++(数组非法,指针合法)。
【解决方案3】:

在 C 中,字符串只是由 chars 组成的数组,以 \0 字符结尾。 当你这样做时:

char (*t)[] = {"test text"};

您正在创建一个指针数组,并用"test text" 填充第一个元素,这是一个指向编译器将为您创建的以零结尾的char 数组的指针。当您取消引用 t 时,您会得到一个指向字符串的指针,然后添加 1 使其指向第二个字符,%s 将打印直到零终止符的所有内容。

你也可以写:

char t[] = "test text";
printf("%s\n", t + 1);

或者:

char t[] = {'t', 'e', 's', 't', ' ', 't', 'e', 'x', 't', '\0'};
printf("%s\n", t + 1);

甚至,如果您不想修改字符串:

const char *t = "test text";
printf("%s\n", t + 1);

要打印单个字符,请使用%c(传入char,而不是指针,因此在您的代码中将是*(*t+1),或者在我的示例中只是t[1],这就是您的意思使用%d)。

【讨论】:

    猜你喜欢
    • 2021-03-07
    • 2014-03-06
    • 2017-07-19
    • 1970-01-01
    • 2021-06-28
    • 1970-01-01
    • 1970-01-01
    • 2013-09-27
    • 2011-06-17
    相关资源
    最近更新 更多