为什么getline函数的第一个参数是指向指针"char**"而不是"char*"?

30

我使用getline函数从STDIN读取一行。

getline的原型为:

ssize_t getline(char **lineptr, size_t *n, FILE *stream);

我使用这个作为测试程序,它是从http://www.crasseux.com/books/ctutorial/getline.html#getline获取的。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int atgc, char *argv[])
{
    int bytes_read = 1;
    int nbytes = 10;
    char *my_string;

    my_string = (char *)malloc(nbytes+1);

    puts("Please enter a line of text");

    bytes_read = getline(&my_string, &nbytes, stdin);

    if (bytes_read == -1)
    {
        puts ("ERROR!");
    }
    else
    {
        puts ("You typed:");
        puts (my_string);
    }

    return 0;
}

这个很好用。

我的疑问是什么?

  1. 为什么在函数 getline 的参数中使用 char **lineptr 而不是 char *lineptr

  2. 为什么当我使用以下代码时会出错:

    char **my_string;
    bytes_read = getline(my_string, &nbytes, stdin); 
    
  3. 我对 *& 感到困惑。

以下是部分警告信息:

testGetline.c: In function ‘main’: 
testGetline.c:34: warning: pointer targets in passing argument 2 of  
  ‘getline’ differ in signedness 
/usr/include/stdio.h:671: 
  note: expected ‘size_t * __restrict__’ but argument is of typeint *’  
testGetline.c:40: warning: passing argument 1 of ‘putchar’ makes integer 
  from pointer without a cast 
/usr/include/stdio.h:582: note: expected ‘int’ but argument is of 
  type ‘char *’

我使用的是GCC版本4.4.5(Ubuntu/Linaro 4.4.4-14ubuntu5)。


1
顺便提一下,您在声明 bytes_read 时打错了字。还有,“incertitude” 是什么意思? - Lightness Races in Orbit
我使用“incertitude”一词来表示“谜团、误解的部分”。抱歉。 - ct586
5个回答

36

为什么在getline函数的参数中使用char **lineptr而不是char *lineptr

想象一下getline的原型看起来像这样:

ssize_t
getline(char *line, size_t n, FILE *stream);

你可以这样调用它:

char *buffer = NULL;
size_t len = 0;
ssize_t read = getline(buffer, len, stdin);

在调用getline之前,buffer是空的:

+------+
|buffer+-------> NULL
+------+

当调用getline函数时,由于C语言中函数参数是按值传递的,所以line变量将获得buffer的副本。在getline函数内部,我们无法再访问buffer变量:

+------+
|buffer+-------> NULL
+------+          ^
                  |
+------+          |
| line +----------+
+------+

getline 会使用 malloc 来分配一些内存并将 line 指向该块的开头:

+------+
|buffer+-------> NULL
+------+

+------+        +---+---+---+---+---+
| line +------->+   |   |   |   |   |
+------+        +---+---+---+---+---+

getline 返回之后,我们不再能够访问到 line

+------+
|buffer+-------> NULL
+------+

我们又回到了起点。由于我们只有buffer的一个副本,无法将buffer重新指向getline内部新分配的内存。


getline的原型实际上是:

ssize_t
getline(char **lineptr, size_t *n, FILE *stream);

而你可以这样调用它:

char *buffer = NULL;
size_t len = 0;
ssize_t read = getline(&buffer, &len, stdin);

&buffer 返回指向buffer的指针,所以我们有:

+-------+        +------+
|&buffer+------> +buffer+-------> NULL
+-------+        +---+--+

调用getline时,由于C是按值调用,lineptr会获得&buffer的副本。因此,lineptr指向与&buffer相同的位置:

+-------+        +------+
|&buffer+------->+buffer+-------> NULL
+-------+        +---+--+
                     ^
+-------+            |
|lineptr+------------+
+-------+

getline 使用 malloc 分配一些内存,并将指向 lineptr 的指针(即 lineptr 指向的东西)指向块的开头:

+-------+        +------+        +---+---+---+---+---+
|&buffer+------->+buffer+------->+   |   |   |   |   |
+-------+        +---+--+        +---+---+---+---+---+
                     ^
+-------+            |
|lineptr+------------+
+-------+

getline 返回后,我们不再可以访问 lineptr,但是我们仍然可以通过 buffer 访问新分配的内存:

+-------+        +------+        +---+---+---+---+---+
|&buffer+------->+buffer+------->+   |   |   |   |   |
+-------+        +---+--+        +---+---+---+---+---+

12

如果你传递一个空指针的指针给getline(),它会为你分配内存。

根据man page:

getline()从流中读取整行文本,并将包含该行文本的缓冲区地址存储到*lineptr中。该缓冲区以空字符结尾,如果找到换行符,则包括该字符。

如果*lineptr是NULL,则getline()将为该行分配一个缓冲区,应由用户程序释放。(在这种情况下,*n的值将被忽略。)

你需要传递一个char **(即指向char指针的指针),以便函数能够更新所指向的char*的值。

你可以使用以下代码:

char *my_string = NULL;  // getline will alloc

puts("Please enter a line of text");

bytes_read = getline(&my_string, &nbytes, stdin);

别忘了,如果你这样做,你需要负责释放由 getline() 分配的内存。


谢谢您的解释。我认为如果您传递一个空指针而不是一个指向空指针的指针,getline()将重新分配该指针的内存,并在其中保存字符串。这样可以吗? - ct586
getline(&my_string,&nbytes,stdin); 是不正确的,因为 int nbytes 是错误的类型。 - chux - Reinstate Monica

6

对于你的第一个问题,Therefromhere 的回答是正确的。将来请查看 man 手册,它包含了你所需要的信息。

你的第二行代码无法运行,因为指针没有初始化。如果你想要这样做,你需要编写以下代码:

char **my_string = malloc(sizeof(char**))

实际上,当你创建一个变量时,星号代表一个指针;当你引用一个变量时,它表示解除指针(获取指针所指向的内容)。&表示“指向此处的指针”。


3
在我的新工作中接手了一些旧代码后,我认为应该提醒大家不要调用calloc并返回指针-指针。虽然这样做应该是可行的,但它会隐藏getline()的操作方式。&操作符可以清楚地表示您正在传递从malloc()、calloc()得到的指针的地址。虽然在技术上相同,但将foo声明为char **foo而不是char *foo,然后调用getline(foo,,)而不是getline(&foo,,)会隐藏这个重要的点。
  1. getline()允许您分配存储空间并将指向malloc()、calloc()返回的指针的指针传递给getline(),然后将其分配给指针。例如:

    char *foo = calloc(size_t arbitrarily_large, 1);

  2. 您可以将其传递为&foo=NULL,这样它就会为您静默地调用malloc()、calloc()进行盲目分配。

  3. char *foo, **p_foo=&foo也可以工作。然后调用foo = calloc(size_t, size_t),再调用getline(p_foo,,); 我认为getline(&foo,,)更好。

盲目分配非常糟糕,容易引起内存泄漏问题,因为在您的代码中没有调用malloc()、calloc(),所以您或者稍后负责维护您的代码的人将不会知道释放指向该存储空间的指针,因为您调用的某个函数在您不知情的情况下分配了内存(除非阅读函数描述并理解它正在进行盲目分配)。由于getline()如果太小,将重新分配由malloc()、calloc()提供的存储器,所以最好只需调用calloc()来分配所需存储器的最佳猜测,并清楚指出指针char *foo的作用。我认为getline()只要您分配的是足够的calloc()就不会对存储器做任何事情。
请记住,如果getline()必须调用realloc()来分配更多的存储器,那么指针的值可能会被更改,因为新的存储器很可能来自堆上的不同位置。例如:如果您传递&foo,而foo的地址为12345,并且getline()重新分配了您的存储器,在新的位置,foo的新地址可能是45678。
这不是反对自己调用calloc()的论点,因为如果将foo=NULL,您就可以确保getline()必须调用realloc()。 总之,使用一些好的猜测调用calloc(),这样读取您的代码的任何人都明白正在分配内存,这必须被释放,无论getline()以后做什么。
if(NULL == line) {
     // getline() will realloc() if too small
    line = (char *)calloc(512, sizeof(char));
}
getline((char**)&line, (size_t *)&len, (FILE *)stdin);

+1. 我非常喜欢预先分配任意大量的存储空间,因为现代计算平台通常有大量可用的RAM,并且像这样的技术在防止对堆管理器的无尽命中带来了巨大的收益,否则这些内存将在尝试缓存磁盘或其他边缘生产力使用时被大量浪费。通过仔细规划,可以将内存分配到调用树的高处,以消除几乎所有对堆管理器的调用。将返回的指针放在结构体上并将结构体的指针传递到树下是最优的。 - user1899861
(size_t *)&len 是错误的。在声明 len 时使用正确的类型 size_t - chux - Reinstate Monica
OP代码为int nbytes = 10; ... bytes_read = getline(&my_string, &nbytes, stdin);intsize_t的大小不一定相同,也没有相同的对齐要求。这导致(size_t *)&len是未定义行为。例如,int为4字节,而size_t为8字节。使用(size_t *)&len将使getline()写入它不拥有的内存。 - chux - Reinstate Monica

1

为什么在函数getline的参数中要使用char **lineptr而不是char *lineptr?

使用char **lineptr是因为getline()要求传入指向存储字符串的指针的地址。
如果getline()期望指针本身,那么你将会使用char *lineptr(这是行不通的,可以参考ThisSuitIsBlackNot的答案)。

为什么我使用以下代码是错误的:
char **my_string; bytes_read = getline(my_string, &nbytes, stdin);

下面的代码可以正常工作:

char *my_string;
char **pointer_to_my_string = &my_string;
bytes_read = getline(my_string, &nbytes, stdin);

我对 * 和 & 感到困惑。

* 有双重含义。
当在指针声明中使用时,例如指向 char 的指针,它表示你想要一个指向 char 的指针而不是 char。
当在其他地方使用时,它会获取指针所指向的变量。

& 获取变量在内存中的地址(指针所创建的值)。

char letter = 'c';
char *ptr_to_letter = &letter;
char letter2 = *ptr_to_letter;
char *ptr2 = &*ptr_to_letter; //ptr2 will also point to letter

&*ptr_to_letter 表示给我指针 ptr_to_letter 所指向的变量的地址(&),与写成ptr_to_letter 是一样的。
你可以将 * 看作是 & 的相反数,它们互相抵消。


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