如何理解 char * ch="123"?

19

我应该如何理解 char * ch="123"

'1' 是一个 char,所以我可以使用:

char x = '1';
char *pt = &x;

那么我如何理解char *pt="123"?为什么char *pt可以指向字符串?

pt的值是"123"的第一个地址值吗?如果是,如何获取pt所指向的字符串的长度?


2
是的,这很不合逻辑,也很糟糕。不是你的问题 - 它是指具有空终止符的传统C风格字符串。+1 用于识别 char 指针实际上指向多个字符的不合逻辑之处。 - Martin James
1
@MartinJames:这不是C风格字符串特有的,任何指针都具有此功能... - Karoly Horvath
@KarolyHorvath 并不是每个指针都具有可以寻址字符串字面量中字符的特性。这个问题有几个方面(至少如果你想全面回答它),其中一些只涉及指针,但一些涉及到 C 对“字符串”的奇怪处理方式。 - jalf
1
@KarolyHorvath:只有指向可变大小数据结构的指针才会出现这个问题。即使如此,也只有在您认为“这个问题”是无法仅从类型和指针知道数据结构的大小时才会出现。例如,您可以将“字符串”定义为字符串的长度,然后是相应数量的字符数据,这将避开所有来自空终止的可怕问题。 - Phoshi
6个回答

34

这实际上是一个非常好的问题,它是C语言中几个奇怪之处的后果:

1:指向字符的指针(char*)当然也可以指向字符数组中的特定字符。这就是指针算术所依赖的内容:

// create an array of three chars
char arr[3] = { 'a', 'b', 'c'};
// point to the first char in the array
char* ptr = &arr[0]
// point to the third char in the array
char* ptr = &arr[2]

2: 字符串字面值("foo")实际上并不是一个字符串,而只是由字符数组和一个空字节组成的数组(所以"foo"实际上等同于数组{'f', 'o', 'o', '\0'}

3: 在C中,数组会“衰变”为指向第一个元素的指针。(这就是为什么许多人错误地说“在C中,数组和指针没有区别”的原因)。也就是说,在将数组赋给指针对象时,它将指针设置为指向数组的第一个元素。因此,给定上面声明的数组arr,您可以执行char* ptr = arr,这与char* ptr = &arr[0]相同。

4: 在任何其他情况下,这样的语法都会使指针指向rvalue(松散地说,这是一个临时对象,您无法获取其地址),这通常是非法的。(你不能做int *ptr = &42)。但是,当您定义一个字符串字面值(例如"foo")时,它不会创建rvalue。相反,它创建了具有静态存储的char数组。您正在创建一个静态对象,该对象在程序加载时创建,当然指针可以安全地指向它。

5: 字符串字面值实际上需要标记为const(因为它们是静态且只读的),但是由于早期版本的C没有const关键字,允许您省略const声明符(至少在C++11之前),以避免破坏旧代码(但是您仍然必须将变量视为只读)。

因此,char* ch = "123"实际上意味着:

  1. 将char数组{'1', '2', '3', '\0'}写入可执行文件的静态部分(因此当程序加载到内存中时,在内存的只读部分创建此变量)
  2. 在执行这行代码时,创建一个指向该数组第一个元素的指针

作为额外的有趣事实,这与char ch[] = "123";不同,后者意思是:

  1. 将char数组{'1', '2', '3', '\0'}写入可执行文件的静态部分(因此当程序加载到内存中时,在内存的只读部分创建此变量)
  2. 在执行这行代码时,在堆栈上创建一个包含此静态分配数组副本的数组。

为什么 int *pt={1,2,3} 不能工作,但 int pt[]={1,2,3} 可以工作?你能帮忙解释一下吗? - jiafu
3
@jiafu {1,2,3} 可以用来初始化一个数组,但它本身不是一个数组。因此,在第二种情况下,你使用它创建了一个名为 pt 的数组,其中包含这三个数字。但在第一种情况下,你试图创建一个指向非对象的指针。这就是字符串字面值被特别处理的地方(参见上面的第4点),因为它实际上会创建数组,而语法{1,2,3}则不会这样做。 - jalf
自从C++11以来,您不再允许省略const - Mike Seymour
@MikeSeymour 很好的观点,我添加了一条注释提到这个。谢谢。 - jalf
@Yakk 我觉得我的回答已经相当混乱了,所以除非它直接涉及到问题,否则我不想再添加更多信息。(请注意,正如Mike Seymour在上面提到的,在C++11中,你必须使用const,所以它不再重要,即使它主观上改善了代码。你只需要这样做。而问题不是关于最佳实践,而仅仅是关于语句的意思。(但你当然是对的,你应该使用const) - jalf
显示剩余2条评论

7

char* ptr = "123";char ptr[] = { '1', '2', '3', '\0' };兼容并且几乎等价(参见http://ideone.com/rFOk3R)。

C语言中,指针可以指向一个值或连续值的数组。C++继承了这一点。 因此,字符串只是以'\0'结尾的字符数组(char)。指向char的指针可以指向char数组。

长度由起始位置和结束位置'\0'之间的字符数给出。以下是C strlen函数给出字符串长度的示例:

size_t strlen(const char * str)
{
    const char *s;
    for (s = str; *s; ++s) {}
    return(s - str);
}

如果没有以'\0'结尾,它会失败得非常惨。


1
char* ptr = "123"; 不等同于 char* ptr = { '1', '2', '3', '\0' }; - BЈовић
9
我认为你的意思是:如果结尾没有'\0',它会彻底失败。5]È╦³û§E;‼∟█«2 ÜP¯@x6²↕I2÷×}I▄P - Michael Madsen
如果这样,char *pt1="123",char pt2[]="123",那么pt2和pt1是相同的吗? - jiafu
1
char* ptrchar ptr[] 不是等价的。在两种情况下,您都可以修改指向的值。猜猜如果您修改指向常量字符数组的非常量字符指针会发生什么 :) 就像这样:http://ideone.com/i37oMP - BЈовић
@BЈовић 因此是“几乎”。我在我的示例中使用了const char*。但是我认为如果您不放置const,那么关闭警告的编译器甚至不会抱怨...而且在我的记忆中,gcc中有一个选项可以将字符串字面量设置为可修改区域,从而允许在没有崩溃的情况下进行修改。 - Johan
显示剩余2条评论

5

字符串字面值是由N个const char组成的数组,其中N是包括隐式的NUL终止符的长度。它具有静态存储期,并且它的实现定义了它存储在哪里。从这里开始,它与普通数组相同-它会衰减为指向其第一个字符的指针-即const char*。在C++11标准出现后,你所写的代码不合法,在C++中,应该写成const char* ch = "123";

你可以使用sizeof操作符获取字面值的长度。一旦它衰减为一个指针,你需要遍历它并找到终止符(这就是strlen函数的作用)。

因此,对于const char* ch;,你得到一个指向常量字符类型的指针,它可以指向单个字符、字符数组的开头或任何起点和终点之间的位置。该数组可以是动态分配、自动分配或静态分配的,并且可以是可变或不可变的。

char ch[] = "text";中,你有一个字符数组。这是正常数组初始化器的语法糖(如char ch[] = {'t','e','x','t','\0'};,但请注意,文字仍将在程序的开始处加载)。这里发生的是:

  • 分配了一个具有自动存储期的数组
  • 它的大小由编译器根据字面值的大小推断得出
  • 将字面值的内容复制到数组中

因此,你有一个可以随意使用的存储区域(与字面值不同,字面值不应写入)。


如果这样,char *pt1="123", char pt2[]="123",那么pt2和pt1是否相同? - jiafu
@jiafu 不是的。我会在答案中详细解释,请稍等。 - jrok

3

指向数组的指针?

指针只指向一个内存地址。说指针指向一个数组只是在宽泛意义上使用——指针实际上不能同时存储多个地址。

在你的例子中,char *ch="123",指针ch实际上只指向第一个字节。你可以编写以下代码,并且它完全有意义:

char *ch = new char [1024];
sprintf (ch, "Hello");    
delete [] ch;

char x = '1';
ch = &x;

请注意指针ch的使用,它既指向new char [1024]行分配的内存,又指向变量x的地址,但仍然是相同的指针类型。

C风格字符串以null结尾

C中的字符串以前是以null结尾的,即在字符串末尾添加了一个特殊的'\0'字符,并假定所有基于char *的函数(如strlenprintf)都会存在这个字符。这样,您可以从第一个字节开始,一直继续到找到包含0x00的字节,从而确定字符串的长度。

一个冗长的strlen样式函数的示例实现如下:

int my_strlen (const char *startAddress)
{
  int count = 0;
  char *ptr = startAddress;
  while (*ptr != 0)
  {
     ++count;
     ++ptr;
  }

  return count;
}

如果这样,char *pt1="123",char pt2[]="123",那么pt2和pt1是相同的吗? - jiafu
几乎相同,但并非完全相同。如果您在我的第一个示例中将char *ch替换为char ch[],则后面的赋值ch = &x将失败。另外,请参见https://dev59.com/1XM_5IYBdhLWcg3wiDuA。 - Jaywalker

3
在C语言中没有字符串, 但是有指向字符的指针。*pt确实不指向一个字符串,而是指向单个字符('1')。然而,一些以char*作为参数的函数假定在它们参数所指向的地址后面的地址上的字节设置为0,如果它们不操作它。在你的例子中,如果你尝试使用pt用于一个期望"空终止字符串"的函数(基本上是期望在停止处理数据时会遇到一个值为0的字节),你会遇到一个分段错误,因为x='1'给x赋予了字符'1'的ascii值,但没有别的值,而char* pt="123"将pt的值设置为'1'的地址,同时也将包含字符'1'、'2'、'3'的字节放置在那块内存里,并在最后加上一个值为0的字节。因此,在一个8位机器上,这块内存可能是这样的:
地址 = 内容 (0x31是字符1的Ascii码)
0xa0 = 0x31
0xa1 = 0x32
0xa2 = 0x33
0xa3 = 0x00

假设你在同一台机器上,声明了一个char* otherString = malloc(4),假设malloc返回值为0xb0,现在otherString的值为0xb0,我们想将"pt"(其值为0xa0)复制到otherString中,那么strcpy函数应该这样调用:
strcpy( otherString, pt );

同样如

strcpy( 0xb0, 0x0a );

strcpy函数会将地址为0xa0的值复制到地址为0xb0的位置,并将其指针递增到“pt”指向的0xa1位置,检查0xa1是否为零,如果不是,则将其指针递增到“otherString”并将0xa1复制到0xb1中。以此类推,直到它的“pt”指针为0xa3,在这种情况下,它将返回,因为它检测到已经到达了“字符串”的结尾。

当然,这并不是完全正确的实现方式,因为它可以有很多不同的实现方法。

这里有一个链接


2
char* pt = "123"; does two things:

1. 在ROM中创建字符串字面量"123"(通常在.text部分) 2. 创建一个char*,该指针被赋值为存储字符串的内存位置的开头。

由于这些操作,像pt[1] = '2';这样的操作是非法的,因为你试图写入ROM内存。

但是你可以将指针分配给其他内存位置而没有任何问题。


如果这样,char *pt1="123",char pt2[]="123",那么pt2和pt1是相同的吗? - jiafu
不,它并不是等同于 char p2[] = "123";。字符串字面量"123"仍然会被创建在ROM中,但同时也会被复制到堆栈中,并且p2指向堆栈中的副本。因此,p2[0] = '1' 是合法的。 - Pandrei
谢谢,此外,为什么int *pt={1,2,3}不能工作,但int pt[]={1,2,3}可以工作?你能帮忙吗? - jiafu
对于pt={1,2,3},pt指向ROM中的位置,而对于pt[]={1,2,3},pt指向堆栈上的位置(可以写入)。 - Pandrei

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