为什么我不能在字符指针变量上使用像scanf(),gets(),strcat()和strcpy()之类的字符串函数?

3

我不明白为什么当我尝试使用任何字符串函数来处理字符指针变量时,总是会出现分段错误。我完全清楚fgets()函数可以更好地确保不会发生覆盖,但我只是想编写一个程序来演示字符串函数实际上如何覆盖内存地址中的数据,而重新声明指针变量,即pValues = "New Name",将指向完全不同的一组地址(其中包含所需的字符)。

我充分知道字符串函数似乎可以与指针常量一起工作,例如在这种情况下定义:

char values[ ] = "Name Here";

但是,为什么它们不适用于指针变量,即在这样定义时:

char *pValues[ ] = "Name Here";

这是一直出现问题的部分...

#include <stdio.h>
#include <string.h>     // required for strcpy(), strcat(), strlen()

main()

{
    
    char *pName = "Peter Jones";
    
    printf("The string pName is \'%s\'\n\n", pName);
    
    // Now lets change it to a longer string with more litterals...
    
    pName = "Dragons Den is GREAT!";
    
    printf("\nThe string pName is %s\n", pName);
    
    puts("Please enter a new string for pName ");
    gets(pName);
    
    printf("pName is now %s\n", pName); // THIS PRODUCES SEGMENTATION FAULT
    
    return 0;
    
}

我正在学习的书中没有提到动态内存或堆 - 我还没有学过这个。

2
指针 pName 指向不能进行写操作的某个位置:字符串字面值。 - pmg
2
最好不要使用gets函数,因为它是一个基本上不安全的函数,而且已经被弃用了。 - Christian Gibbons
1
你认为 char *pValues[ ]= "Name Here" ; 这行代码会给你什么结果? - Andrew Henle
今天我多次回顾了您的问题,我发现您的标题并不完全符合您问题中的代码。示例代码只显示了gets()的故障,但没有使用strcat()strcpy()scanf()。使用其中一个函数失败并不意味着它们所有的函数都会以类似的方式失败。 - Code-Apprentice
4个回答

6
为什么当指针变量这样定义时它们不能工作呢:

char *pValues[] = "Name Here";

因为pValues不是字符指针,而是字符指针的数组。如果您这样做:
char *pValues = "Name Here" ;

大多数你想使用的函数都可以正常工作。唯一的例外是任何试图写入该指针所指向的内存的函数:
    puts("Please enter a new string for pName ");
    gets(pName);
    
    printf("pName is now %s\n", pName); // THIS PRODUCES SEGMENTATION FAULT

这部分失败的原因是gets()写入了指向受保护内存部分的指针。你用以下方式进行初始化:
    char *pName = "Peter Jones";

因此,pName 指向保存字符串字面值的内存。正如您所看到的,尝试写入该内存区域会导致段错误。

为了使用指针和 gets() 函数,您需要学习有关动态内存分配和堆的知识。


1
@P__J__ 它在上面代码的问题文本中。我添加了一个解释,针对实际代码中的段错误。 - Code-Apprentice
2
应该尽量避免使用 gets - 0___________
@AdrianSalt 不同之处在于赋值会导致指针指向不同的内存地址,而 gets() 则尝试写入指针已经指向的内存。 - Code-Apprentice
1
@ryyker 是的,那样会修复声明,但是后面在 OP 的示例代码中正确使用字符串函数需要额外的语法。 - Code-Apprentice
1
@AdrianSalt - 如果这个回答解决了你的问题(从你在这里的评论来看,似乎是这样),你可以通过点击左侧顶部的小空心复选标记来表达非常惯用的感谢之意,从而指示此帖子为被接受的答案。 - ryyker
显示剩余11条评论

3

为什么我不能像scanf() [等等]这样使用字符串函数来处理字符指针变量?

您是可以的,但是必须遵循以下规则:

  • 定义变量的类型必须与初始化程序的正确形式匹配。
  • 将字符串文字初始化程序用于指针变量会创建不可编辑的变量。
  • 将字符串文字初始化程序用于char数组是可编辑但不可调整大小的。

所发布的代码示例违反了上述每一条规则。

例如,此代码:

char values[] = "Name Here";

创建一个可编辑的 C字符串,其隐式长度设置为 sizeof("Name Here")。因为数组的大小是隐式的(未指定),所以需要初始化器。但是,如果数组大小是显式的,则初始化器是可选的。
//either line is legal, and both resulting variables are editable
char values[20];
char values2[20] = "Name Here";

 

这个:

char *pValues[]= "Name Here";

这是一个指向char的指针数组,它需要一个初始化器列表,可以通过用{...}包围字符串文字来完成:

char *pValue[] = {"Name Here:"};
char *pValues[] = {{"Name Here:"},{"Address Here:"}};//see image
char *pValues2[] = {{"Name Here:"},{"Address Here:"}, ...};

由此产生了一个到多个不可编辑的C字符串,每个字符串存储在内存的不同位置。
表达式:
char *pValue3 = "Name Here:";  //create non-editable variable

也会将字符串字面量"Name Here:"的内容放入内存位置中,并且像前一个示例一样,处于不可写的内存区域。一旦在创建时初始化,它就不再可编辑。
例如,给定:
char *var1 = "this is a non-editable string"; 
char var2[80] = {"This is an editable string"};

strcpy(var1, var2);//is illegal
strcpy(var2, var1);//is legal

以下图片对应上面的char *pValues[]
enter image description here

2

pName是一个指针。你已经将其赋值为字符串字面量的引用。字符串字面量无法修改 - 因此可能会导致调用UB时出现segfault。

你需要为字符串分配一些空间。

你可以:

char str[100];
pName = str;
/* or */
pName = malloc(100);

/*or even*/
pName = (char[100]){};

/* then */

fgets(pName, 100, stdin);

不要使用 gets 函数,因为它是危险的。


2

Adrian,我不明白为什么你“在尝试使用任何字符串函数与字符指针变量时都会出现分段错误”,我也不知道如何判断scanf()相对于fgets()的优劣。

但是回到这个问题,也许我可以帮助你。还有一些你需要“完全意识到”的事情:

当你写

char *pName = "Peter Jones";

你正在定义一个字符串字面量。接着pName将指向内存中的一个区域,该区域后续地址中有"Peter Jones"。是的,pName在某种程度上是指向char的指针,但它是指向常量区域的指针。你可以使pName指向另一个地址,但不能用它来改变"Peter Jones"区域中的内容。它会一直存在。
所以当你写下:
// Now lets change it to a longer string with more literals...  
  
     pName = "Dragons Den is GREAT!";

它会成功...但也许不应该这样做。当你这样做时,你正在告别字符串"Peter Jones"。可能是无害的,但从某种意义上说,这是一种内存泄漏,因为它存在着,却又丢失了。

但是当你调用gets()时情况就不同了:你试图覆盖一个常量字面量。你不能这样做,你的程序就挂了。

这与堆栈或动态分配无关,只与分配有关。你需要有一些可写的区域来指向gets()写入的位置。

示例

char qName[40];
char* pName = "Peter Jones";
strcpy(qName, pName);
printf("The string pName is \'%s\'\n\n", pName);
// Now lets change it to a longer string with more litterals...
pName = "Dragons Den is GREAT!";
printf("\nThe string pName is %s\n", pName);
puts("Please enter a new string for qName ");
fgets(qName,10,stdin);
pName = qName;
// now pName points to the data read by gets()
printf("pName is now %s\n", pName);

现在它可以运行了。声明:
char qName[40];

我们现在有40个可用的字节。 qName是指向40个连续字节的RAM区域的指针。从某种意义上说,它是char的指针。
但是现在你可以告诉gets()读取到qName中。这里没有问题。 你甚至可以将pName指向qName。但是另一个区域就会丢失:包含"Dragons Den is GREAT!"的区域消失了。
另一方面,如果你声明
char* p = NULL;

现在你有一个 char 指针。它现在指向空,但是随时可以动态分配内存并将 p 指向它,或者让 p 指向程序中的某个区域,比如写入。
p = qname + 2;

然后让p指向qName的第三个字节。


非常感谢您,这对我非常有帮助!似乎一旦像这样分配指针变量pValue =“Name”;,您就无法通过字符串函数更改它。但在分配pValue之前,fgets()确实起作用,这对我来说有点奇怪。似乎一旦分配pValue =“name”;或char * pValue =“name”;,就会发生静态事情,您只能通过将指针指向完全不同的位置来更改其值(而在此之前分配的方式如下char name [] =“Peter Jones”;char * pValues =&name [0];然后,fgets(pValues,81,stdin)有效。 - Adrian Salt
需要注意的是,最初pName指向一个常量字面值。您可以使其他指针指向相同的区域,但是一旦最后一个指针被更改为指向另一个位置,该区域将永远丢失。另一方面,您可以使pName指向另一个区域,以便永远不再查看原来的区域。由于pName是访问包含“Peter Jones”的区域的唯一途径,因此这些字节将丢失。至于gets(),这些函数不分配内存,因此必须传递函数可以写入的地址。在此示例中,qName指向一个坚实的40个字符块,因此它可以正常工作。 - arfneto

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