c BAD 访问 fscanf 与结构

c BAD access fscanf with structs

我正在读取格式如下的文本文件:

Firstname Surname Age NumberOfSiblings motherage dadage

正在导入头文件:

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

一个结构体定义如下:

typedef struct {
    int person_ID; //not included in file
    char full_name[20];
    char sex[2];
    char countryOfOrigin[20];
    int num_siblings;
    float parentsAges[2]; //this should store mother and fathers age in an array of type float
} PersonalInfo;


void viewAllPersonalInformation(){
    FILE* file = fopen("People.txt", "r");
    if (file == NULL){
        printf("File does not exist");
        return;
    }
    int fileIsRead = 0;
    int idCounter = 0;

    PersonalInfo People[1000];
    //headers
    printf("%2s |%20s |%2s |%10s |%2s |%3s |%3s\n", "ID", "Name", "Sex", "Born In", "Number of siblings", "Mother's age", "Father's Age");

    do{
        fileIsRead = fscanf(file, "%s %s %s %d %f %f\n", People[idCounter].full_name, People[idCounter].sex, People[idCounter].countryOfOrigin, &People[idCounter].num_siblings, &People[idCounter].parentsAges[0], &People[idCounter].parentsAges[1]);

        People[idCounter].person_ID = idCounter;
        printf("%d %s %s %s %d %f %f\n", People[idCounter].person_ID, People[idCounter].full_name, People[idCounter].sex, People[idCounter].countryOfOrigin, People[idCounter].num_siblings, People[idCounter].parentsAges[0], People[idCounter].parentsAges[1]);
        idCounter++;
    }
    while(fileIsRead != EOF);
    fclose(file);


    printf("Finished reading file");
}


int main() {
    viewAllPersonalInformation();
    return 0;
}

其中 People.txt 看起来像:

约翰·奥唐奈 F 爱尔兰 3 32.5 36.1

玛丽·麦克马洪 M 英格兰 0 70 75

彼得·汤普森 F 美国 2 51 60

如果你有一个指针字段char *full_name,这意味着只是一个指针,应该由某个存在的对象初始化,如果是char *,它通常应该是一个char数组。您可以通过两种方式修复它:

  • 只要像char full_name[100]那样做一个数组字段,把一个字符串的最大长度传给一个scanf格式的字符串,比如%100s,这是最简单的方法;
  • 使用 malloc 函数,不要忘记 free 该地址,或以其他方式将一些有效地址分配给指针,例如将数组声明为普通的自动存储变量,并将零索引元素的地址分配给您的指针字段,并记住在离开您的函数后,您的自动存储变量的地址将变为无效。

还有一个麻烦。 %s 转换说明符告诉 fscanf 读取 单个 单词直到 any whitespace 字符像space,因此根据您的输入格式,您的 full_name 字段将被读取到第一个 space,并且任何进一步读取整数的尝试都将失败。

fscanf() 将在遇到空格时停止读取。在全名的情况下,您希望使用格式说明符 %s 读取两个字符串。 %s 一发现空格就停止,因此它只会将名字存储在 full_name 中,而姓氏将转到第二个 %s,因此在 countryOfOrigin 中.

所以,如果你想阅读"Peter Thompson",那么你就需要引入两个字符串(char数组)来存储名字和姓氏,然后将它们连接起来。

但是,由于您要读取字数不同的全名,我建议您使用 fgets()(它也有缓冲区溢出保护)。例如,"Peter Thompson" 有 2 个,"Mary Mc Mahon" 有 3 个。那么,如果您坚持使用 fscanf(),您会使用多少个 %s? 2 还是 3?您不知道,这取决于您在运行时获得的输入。也许有一些正则表达式可以用 fscanf() 来解决这个问题,但相信使用 fgets() 然后解析读取的文件行更适合练习。


现在我们用 fgets() 读取了一行文件,我们要用它做什么?我们仍然不知道每个全名由多少个单词组成!如何查明?通过计算该行包含的空格。如果它包含 w 个空格,那么它有 w + 1 个标记(在您的示例中可以是单词、数字或字符)。

使用简单的 if-else 语句,我们可以在您的示例中区分这两种情况,当有 6 个空格(7 个标记)和 7 个空格("Mary Mc Mahon M England 0 70 75" 的 8 个标记)时。

现在,如何从字符串(行)中提取出标记(全名、年龄等)?我们可以有一个循环并使用一堆 if-else 语句来表示,直到我找到第二个(或第三个取决于空格的数量)空格,我将把当前标记附加到 full_name。然后,下一个标记将是性别,依此类推。

当然可以,但由于我有点懒惰,我将基于您使用 fscanf() 的出色工作,并使用 sscanf() 来提取标记。当然,使用这种方法,我们需要使用一两个(取决于空格的数量)额外的字符串,以便临时存储姓氏(在我们用 strcat() 将其附加到名字之前)。

最小的完整工作示例:

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

#define P 1000 // Max number of people
#define L 256  // Max length of line read from file (-1)

typedef struct {
    int person_ID; //not included in file
    char full_name[32];
    char sex[2];
    char countryOfOrigin[16];
    int num_siblings;
    float parentsAges[2];
} PersonalInfo;

int count_whitespaces(char* str)
{
    int whitespaces_count = 0;
    while(*str)
    {
        if(*str == ' ')
            whitespaces_count++;
        str++;
    }
    return whitespaces_count;
}

void viewAllPersonalInformation(){
    FILE* file = fopen("People.txt", "r");
    if (file == NULL){
        printf("File does not exist");
        return;
    }
    int fileIsRead = 0;
    int idCounter = 0;

    PersonalInfo People[P];
    // line of file, placeholder for biworded surnames, surname.
    char line[L], str[8], surname[16];
    //headers
    // You have 7 format specifiers for the headers, but only 6 six in fscanf!!!
    printf("%2s |%5s |%2s |%10s |%2s |%3s |%3s\n", "ID", "Name", "Sex", "Born In", "Number of siblings", "Mother's age", "Father's Age");

    // read into 'line', from 'file', up to 255 characters (+1 for the NULL terminator)
    while(fgets(line, L, file) != NULL) {
        //fileIsRead = fscanf(file, "%s %s %s %s %d %f %f\n", People[idCounter].full_name, People[idCounter].full_name, People[idCounter].sex, People[idCounter].countryOfOrigin, &People[idCounter].num_siblings, &People[idCounter].parentsAges[0], &People[idCounter].parentsAges[1]);
        // eat trailing newline of fgets
        line[strcspn(line, "\n")] = 0;

        // Skip empty lines of file
        if(strlen(line) == 0)
            continue;

        if(count_whitespaces(line) == 6)
        {
            sscanf(line, "%32s %16s %c %16s %d %f %f", People[idCounter].full_name, surname, People[idCounter].sex, People[idCounter].countryOfOrigin, &People[idCounter].num_siblings, &People[idCounter].parentsAges[0], &People[idCounter].parentsAges[1]);
        }
        else // 7 whitespaces, thus 8 token in the string
        {
            sscanf(line, "%32s %8s %16s %c %16s %d %f %f", People[idCounter].full_name, str, surname, People[idCounter].sex, People[idCounter].countryOfOrigin, &People[idCounter].num_siblings, &People[idCounter].parentsAges[0], &People[idCounter].parentsAges[1]);
            // Separate name and first word of surname with a space
            strcat(People[idCounter].full_name, " ");
            strcat(People[idCounter].full_name, str);
        }

        // Separate name and surname with a space
        strcat(People[idCounter].full_name, " ");
        strcat(People[idCounter].full_name, surname);

        People[idCounter].person_ID = idCounter;
        printf("%d %s %s %s %d %f %f\n", People[idCounter].person_ID, People[idCounter].full_name, People[idCounter].sex, People[idCounter].countryOfOrigin, People[idCounter].num_siblings, People[idCounter].parentsAges[0], People[idCounter].parentsAges[1]);
        idCounter++;
        if(idCounter == P)
        {
            printf("Max number of people read, stop reading any more data.\n");
            break;
        }
    };
    fclose(file);

    printf("Finished reading file.\n");
}


int main() {
    viewAllPersonalInformation();
    return 0;
}

输出:

ID | Name |Sex |   Born In |Number of siblings |Mother's age |Father's Age
0 John O'Donnell F Ireland 3 32.500000 36.099998
1 Mary Mc Mahon M England 0 70.000000 75.000000
2 Peter Thompson F America 2 51.000000 60.000000
Finished reading file.

您注意到 sscanf() 格式说明符中的数字了吗?他们是 .


动态内存分配怎么样?

在上面的代码中,我估计了姓名、原籍国等的最大长度。现在让这些尺寸动态化怎么样?我们可以,但我们仍然需要初步估计。

因此,我们可以读取固定长度的临时数组中的名称,然后找到字符串的实际长度 strlen(). With that information in hand, we are now able to dynamically allocate memory (pointing by a char pointer), and then copy with strcpy() 从临时数组到最终目的地的字符串。