动态结构数组中动态分配的字符串(段错误)

Dynamically allocated string in dynamic structure array(seg fault)

我想将整个文件(逐行)读入结构数组中的字符指针“名称”。(想将名称(可以是任意长度)保存在动态分配的字符串中然后我将划分在 struct.I 中读取的字符串(名称)到块(年龄名称分数)中出现段错误。(文件格式为:

age name score
25,Rameiro Rodriguez,3
30,Anatoliy Stephanos,0
19,Vahan: Bohuslav,4.2

struct try{
  double age;
  char *name;
  double score;
};
void allocate_struct_array(struct try **parr,int total_line);
int main(){
int count=0,i=0; 
char ch;
fileptr = fopen("book.txt", "r");
//total line in the file is calculated
struct try *parr;
allocate_struct_array(&parr,count_lines);

//i got segmentation fault at below.(parsing code is not writed yet just trying to read the file)
    while((ch=fgetc(fileptr))!=EOF) {
        count++;
        if(ch=='\n'){
          parr->name=malloc(sizeof(char*)*count+1);
          parr[i].name[count+1]='[=10=]';
          parr+=1;
          count=0;
        }
    }
    fclose(fileptr);
}
void allocate_struct_array(struct try **parr,int total_line){
    *parr = malloc(total_line * sizeof(struct try));
}

继续我的评论,在 allocate_struct_array(struct try **parr,int total_line) 中,您分配了 struct try 块而不是指针块(例如 struct try*)。您的分配 parr->name=malloc(sizeof(char*)*count+1); 尝试分配 count + 1 指针 。此外,在每次迭代中,您都会覆盖 parr->name 所持有的地址,从而造成内存泄漏,因为指向先前分配的指针已丢失且无法释放。

在您编写的任何动态分配内存的代码中,您对分配的任何内存块负有 2 责任:(1) 始终保留指向内存块的起始地址 因此,(2) 当不再需要它时可以释放

解决您的问题的更好方法是将每一行读入一个简单的字符数组(大小足以容纳每一行)。然后,您可以将 agenamescore 分开,并确定 name 中的字符数,以便为 parr[i].name 正确分配,然后您可以复制分配后的名称。如果你很小心,你可以简单地在缓冲区中找到两个 ',',为 parr[i].name 分配,然后使用 sscanf() 和适当的格式字符串来分隔、转换和复制所有值到您的结构 parr[i] 一次调用。

由于您没有给出确定方法 //total line in the file is calculated 的方法,我们将假设一个足够大的数字来容纳您的示例文件以供讨论之用。找到那个号码留给你。

要将每一行读入一个数组,只需声明一个足够大的缓冲区(字符数组)来容纳每一行(取最长的预期行并乘以 2 或 4,或者如果在典型的 PC 上,只需使用10242048 字节的缓冲区,可容纳除行长于此的晦涩文件之外的所有文件。(规则:不要跳过缓冲区大小!! ) 你可以这样做,例如

#define COUNTLINES   10     /* if you need a constant, #define one (or more) */
#define MAXC       1024
#define NUMSZ        64
...
int main (int argc, char **argv) {
    
    char buf[MAXC];                 /* temporary array to hold each line */
    ...

在循环中读到 '\n'EOF 时,更容易连续循环并在循环中检查 EOF。这样,最后一行将作为读取循环的正常部分处理,并且您不需要特殊的最终代码块来处理最后一行,例如

    while (nparr < count_lines) {       /* protect your allocation bounds */
        int ch = fgetc (fileptr);       /* ch must be type int */
        
        if (ch != '\n' && ch != EOF) {  /* if not \n and not EOF */
            ...
        }
        else if (count) {               /* only process buf if chars present */
            ...
        }
        
        if (ch == EOF) {                            /* if EOF, now break */
            break;
        }
    }

(注意: 对于您的示例,我们继续阅读您使用的 fgetc(),但在正常实践中,您只需使用 fgets() 即可用行填充字符数组)

要查找数组中的第一个和最后一个 ',',您可以简单地使用 #include <string.h> 并使用 strchar() 查找第一个,使用 strrchr() 查找最后一个。使用设置为第一个和最后一个 ',' 的指针和结束指针,名称中的字符数变为 ep - p - 1;。您可以找到 ','s 并找到名称的长度:

            char *p = buf, *ep;         /* pointer & end-pointer */
            ...
            /* locate 1st ',' with p and last ',' with ep */
            if ((p = strchr (buf, ',')) && (ep = strrchr (buf, ',')) && 
                p != ep) {  /* confirm pointers don't point to same ',' */
                size_t len = ep - p - 1;            /* get length of name */

找到第一个','和第二个','并确定name中的字符数后,分配个字符,不是 指针,例如使用 namenparr 中的 len 个字符作为结构索引(而不是你的 i)你会做:

                parr[nparr].name = malloc (len + 1);        /* allocate */
                if (!parr[nparr].name) {                    /* validate */
                    perror ("malloc-parr[nparr].name");
                    break;
                }

(注意:break 而不是 exit 分配错误,因为分配和填充的所有先前结构仍将包含有效数据,你可以使用)

现在您可以制作一个 sscanf() 格式字符串并在一次调用中分隔 agenamescore,例如

                /* separate buf & convert into age, name, score -- validate */
                if (sscanf (buf, "%d,%[^,],%lf", &parr[nparr].age, 
                            parr[nparr].name, &parr[nparr].score) != 3) {
                    fputs ("error: invalid line format.\n", stderr);
                    ...
                }

将它完全放入一个短程序中以读取和分离您的 exmaple 文件,您可以这样做:

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

#define COUNTLINES   10     /* if you need a constant, #define one (or more) */
#define MAXC       1024
#define NUMSZ        64

typedef struct {            /* typedef for convenient use as type */
  int age;                  /* age is generally an integer, not double */
  char *name;
  double score;
} try;

/* always provde a meaningful return when function can
 * succeed or fail. Return result of malloc.
 */
try *allocate_struct_array (try **parr, int total_line)
{
    return *parr = malloc (total_line * sizeof **parr);
}

int main (int argc, char **argv) {
    
    char buf[MAXC];                 /* temporary array to hold each line */
    int count = 0, 
        nparr = 0, 
        count_lines = COUNTLINES;
    try *parr = NULL;
    /* use filename provided as 1st argument (book.txt by default) */
    FILE *fileptr = fopen (argc > 1 ? argv[1] : "book.txt", "r");
    
    if (!fileptr) {     /* always validate file open for reading */
        perror ("fopen-fileptr");
        return 1;
    }
    
    if (!fgets (buf, MAXC, fileptr)) {  /* read/discard header line */
        fputs ("file-empty\n", stderr);
        return 1;
    }
    
    /* validate every allocation */
    if (allocate_struct_array (&parr, count_lines) == NULL) {
        perror ("malloc-parr");
        return 1;
    }
    
    while (nparr < count_lines) {       /* protect your allocation bounds */
        int ch = fgetc (fileptr);       /* ch must be type int */
        
        if (ch != '\n' && ch != EOF) {  /* if not \n and not EOF */
            buf[count++] = ch;          /* add char to buf */
            if (count + 1 == MAXC) {    /* validate buf not full */
                fputs ("error: line too long.\n", stderr);
                count = 0;
                continue;
            }
        }
        else if (count) {               /* only process buf if chars present */
            char *p = buf, *ep;         /* pointer & end-pointer */
            
            buf[count] = 0;             /* nul-terminate buf */
            
            /* locate 1st ',' with p and last ',' with ep */
            if ((p = strchr (buf, ',')) && (ep = strrchr (buf, ',')) && 
                p != ep) {  /* confirm pointers don't point to same ',' */
                size_t len = ep - p - 1;            /* get length of name */
                
                parr[nparr].name = malloc (len + 1);        /* allocate */
                if (!parr[nparr].name) {                    /* validate */
                    perror ("malloc-parr[nparr].name");
                    break;
                }
                
                /* separate buf & convert into age, name, score -- validate */
                if (sscanf (buf, "%d,%[^,],%lf", &parr[nparr].age, 
                            parr[nparr].name, &parr[nparr].score) != 3) {
                    fputs ("error: invalid line format.\n", stderr);
                    if (ch == EOF)                  /* if at EOF on failure */
                        break;                      /* break read loop */
                    else {
                        count = 0;                  /* otherwise reset count */
                        continue;                   /* start read of next line */
                    }
                }
            }
            nparr += 1;                             /* increment array index */
            count=0;                                /* reset count zero */
        }
        
        if (ch == EOF) {                            /* if EOF, now break */
            break;
        }
    }
    fclose(fileptr);                    /* close file */
    
    for (int i = 0; i < nparr; i++) {
        printf ("%3d   %-20s %5.1lf\n", 
                parr[i].age, parr[i].name, parr[i].score);
        free (parr[i].name);            /* free strings when done */
    }
    free (parr);                        /* free struxts */
}

(注意: 永远不要对文件名进行硬编码或在代码中使用幻数。如果你需要一个常量,#define ... 一个。传递要读取的文件名作为程序的第一个参数或将文件名作为输入。您不必重新编译代码就可以读取不同的文件名)

示例Use/Output

使用 dat/parr_name.txt 中的示例数据,您将拥有:

$ ./bin/parr_name dat/parr_name.txt
 25   Rameiro Rodriguez      3.0
 30   Anatoliy Stephanos     0.0
 19   Vahan: Bohuslav        4.2

内存Use/Error检查

您必须使用内存错误检查程序来确保您不会尝试访问内存或写入 beyond/outside 您分配的块的边界,尝试读取或基于未初始化的条件跳转值,最后,确认您释放了所有已分配的内存。

对于Linux valgrind是正常的选择。每个平台都有类似的内存检查器。它们都很简单易用,只需运行你的程序就可以了。

$ valgrind ./bin/parr_name dat/parr_name.txt
==17385== Memcheck, a memory error detector
==17385== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==17385== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==17385== Command: ./bin/parr_name dat/parr_name.txt
==17385==
 25   Rameiro Rodriguez      3.0
 30   Anatoliy Stephanos     0.0
 19   Vahan: Bohuslav        4.2
==17385==
==17385== HEAP SUMMARY:
==17385==     in use at exit: 0 bytes in 0 blocks
==17385==   total heap usage: 7 allocs, 7 frees, 5,965 bytes allocated
==17385==
==17385== All heap blocks were freed -- no leaks are possible
==17385==
==17385== For counts of detected and suppressed errors, rerun with: -v
==17385== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

始终确认您已释放所有分配的内存并且没有内存错误。

使用fgets()读取每一行和name

的临时数组

为了不给你留下错误的印象,这个问题可以通过使用 fgets() 将每一行读入一个字符数组并用 sscanf() 分隔所需的值来大大简化这个问题,节省 name 到一个足够大小的临时数组中。现在所需要做的就是为 parr[nparr].name 分配,然后将临时 name 复制到 parr[nparr].name.

通过这种方式,您大大降低了逐个字符读取的复杂性,并且通过为 name 使用临时数组,您无需定位 ',' 以获得名字的长度。

唯一需要的更改是为临时名称数组添加一个新常量,然后您可以将整个读取循环替换为:

#define NAMSZ       256
...
    /* protect memory bounds, read each line into buf */
    while (nparr < count_lines && fgets (buf, MAXC, fileptr)) {
        char name[NAMSZ];       /* temporary array for name */
        size_t len;             /* length of name */
        
        /* separate buf into age, temp name, score & validate */
        if (sscanf (buf, "%d,%[^,],%lf", &parr[nparr].age, name,
                    &parr[nparr].score) != 3) {
            fputs ("error: invalid line format.\n", stderr);
            continue;
        }
        len = strlen (name);    /* get length of name */
        
        parr[nparr].name = malloc (len + 1);        /* allocate for name  */
        if (!parr[nparr].name) {                    /* validate allocation */
            perror ("malloc-parr[nparr].name");
            break;
        }
        memcpy (parr[nparr].name, name, len + 1);
        
        nparr += 1;
    }
    fclose(fileptr);                    /* close file */
    ...

(相同的输出和相同的内存检查)

另请注意,如果您的编译器提供 strdup(),您可以将分配和复制作为单个操作。这会将 name 的分配和复制减少到单个调用,例如

        parr[nparr].name = strdup (name);

由于 strdup() 分配内存(并且可能会失败),您必须像使用 malloc()memcpy() 一样验证分配。但是,请理解,strdup() 不是标准 C。它是一个不属于标准库的 POSIX 函数。

您可以进行的另一项改进是添加逻辑以在结构块 (parr) 已满时调用 realloc()。这样你就可以从一些合理预期数量的结构开始,然后在你 运行 出来时重新分配更多。这将消除对您可以存储的行数的人为限制——并且不需要知道 count_lines。 (本站有很多使用示例realloc(),具体实现就交给你了。

检查一下,如果您还有其他问题,请告诉我。