为什么取消引用指向字符串(字符数组)的指针 returns 整个字符串而不是第一个字符?

Why does dereferencing a pointer to string (char array) returns the whole string instead of the first character?

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

#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"
}

*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,因为解引用指向数组的第一个元素,即字符串的第一个字符,然后你向它添加一个,但是因为它是一个指针,它会根据指针 arithmetic 前进到下一个元素(因为我们做了 +1。如果我们做了 +2,它会前进 2 个元素,依此类推。) .

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

因此,这将打印 "est text"。

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

因为我们使用 %c,它只会打印那个字符,即 "e".

在 C 中,字符串只是 char 的数组,以 [=15=] 字符结尾。 当你这样做时:

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', '[=12=]'};
printf("%s\n", t + 1);

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

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

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

所做的

在撰写此答案时,所有其他答案都不正确。此外,您的问题闻起来像 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 的(不可变)数组的指针。但是,在您的情况下,使用指向不正确类型的指针并不重要,因为这 2 个指针尽管类型不兼容,但指向相同的地址 - 只有一个指向字符数组,另一个指向第一个字符在那个字符数组中;因此观察到的行为是相同的。


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

开始的以 0 结尾的字符串的内容

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

[%s]

If no l length modifier is present, the argument shall be a pointer to the initial element of an array of character type. Characters from the array are written up to (but not including) the terminating null character. [...] If the precision is not specified or is greater than the size of the array, the array shall contain a null character. [...]

(强调我的。)


关于你的第二次初始化:

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 的指针,结果您将得到 ((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++ 编译器中编译。