动态结构数组中动态分配的字符串(段错误)
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) 当不再需要它时可以释放。
解决您的问题的更好方法是将每一行读入一个简单的字符数组(大小足以容纳每一行)。然后,您可以将 age
、name
和 score
分开,并确定 name
中的字符数,以便为 parr[i].name
正确分配,然后您可以复制分配后的名称。如果你很小心,你可以简单地在缓冲区中找到两个 ','
,为 parr[i].name
分配,然后使用 sscanf()
和适当的格式字符串来分隔、转换和复制所有值到您的结构 parr[i]
一次调用。
由于您没有给出确定方法 //total line in the file is calculated
的方法,我们将假设一个足够大的数字来容纳您的示例文件以供讨论之用。找到那个号码留给你。
要将每一行读入一个数组,只需声明一个足够大的缓冲区(字符数组)来容纳每一行(取最长的预期行并乘以 2 或 4,或者如果在典型的 PC 上,只需使用1024
或 2048
字节的缓冲区,可容纳除行长于此的晦涩文件之外的所有文件。(规则:不要跳过缓冲区大小!! ) 你可以这样做,例如
#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
中的字符数后,分配个字符,不是 指针,例如使用 name
和 nparr
中的 len
个字符作为结构索引(而不是你的 i
)你会做:
parr[nparr].name = malloc (len + 1); /* allocate */
if (!parr[nparr].name) { /* validate */
perror ("malloc-parr[nparr].name");
break;
}
(注意: 你 break
而不是 exit
分配错误,因为分配和填充的所有先前结构仍将包含有效数据,你可以使用)
现在您可以制作一个 sscanf()
格式字符串并在一次调用中分隔 age
、name
和 score
,例如
/* 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()
,具体实现就交给你了。
检查一下,如果您还有其他问题,请告诉我。
我想将整个文件(逐行)读入结构数组中的字符指针“名称”。(想将名称(可以是任意长度)保存在动态分配的字符串中然后我将划分在 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) 当不再需要它时可以释放。
解决您的问题的更好方法是将每一行读入一个简单的字符数组(大小足以容纳每一行)。然后,您可以将 age
、name
和 score
分开,并确定 name
中的字符数,以便为 parr[i].name
正确分配,然后您可以复制分配后的名称。如果你很小心,你可以简单地在缓冲区中找到两个 ','
,为 parr[i].name
分配,然后使用 sscanf()
和适当的格式字符串来分隔、转换和复制所有值到您的结构 parr[i]
一次调用。
由于您没有给出确定方法 //total line in the file is calculated
的方法,我们将假设一个足够大的数字来容纳您的示例文件以供讨论之用。找到那个号码留给你。
要将每一行读入一个数组,只需声明一个足够大的缓冲区(字符数组)来容纳每一行(取最长的预期行并乘以 2 或 4,或者如果在典型的 PC 上,只需使用1024
或 2048
字节的缓冲区,可容纳除行长于此的晦涩文件之外的所有文件。(规则:不要跳过缓冲区大小!! ) 你可以这样做,例如
#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
中的字符数后,分配个字符,不是 指针,例如使用 name
和 nparr
中的 len
个字符作为结构索引(而不是你的 i
)你会做:
parr[nparr].name = malloc (len + 1); /* allocate */
if (!parr[nparr].name) { /* validate */
perror ("malloc-parr[nparr].name");
break;
}
(注意: 你 break
而不是 exit
分配错误,因为分配和填充的所有先前结构仍将包含有效数据,你可以使用)
现在您可以制作一个 sscanf()
格式字符串并在一次调用中分隔 age
、name
和 score
,例如
/* 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()
,具体实现就交给你了。
检查一下,如果您还有其他问题,请告诉我。