C: fscanf 作为 while 参数的问题

C: issues with fscanf as while parameter

这个程序段应该是读入未知数量的姓名和身份证号码,并将它们编译成一个链表。名字排在最前面,id 最后(George Washington, 188867)。我已将问题缩小到我的 fopen 语句,但我无法弄清楚是什么导致我的程序崩溃。

head 与我的结构同时被声明为 null。

struct studentInfo{
    char name[50];
    int id;
    struct studentInfo *next;
}*head = NULL;

void createList(){
    FILE *ofp;
    struct studentInfo *new_node = (struct studentInfo *)malloc(sizeof(struct studentInfo));
    struct studentInfo *temp = (struct studentInfo *)malloc(sizeof(struct studentInfo));
    char firstName[25], lastName[25];

    new_node = head;
    temp = head;

    ofp = fopen("C:\Users\Brent Rademaker\Desktop\COP3502C\Assignment 1\AssignmentOneInput.txt", "r"); 


    while(fscanf(ofp, "%s %s %d", &firstName, &lastName, &new_node->id) != EOF){
        strncat((new_node->name), firstName, 10);
        strncat((new_node->name), lastName, 10);
        new_node->next = NULL;

        if(head == NULL){
            head = new_node;
        }

        temp->next = new_node;
        temp = temp->next;
    }
    fclose(ofp);
}

main()
{
    createList();

    return 0;
}

编辑:在代码中添加了 struct 和 main,试图找出如何使用 errno 和 sterror 来提供更多信息

edit2:感谢建议,问题不再出在我的 fopen 上,而是出在我的 while 循环变量上。我不确定将 strerror 放在哪里来解决问题。

假设您说文件已成功打开是正确的,那么崩溃的发生是因为您在 head == NULL; 时设置了 new_node = head;,然后尝试读取 &new_node->id 中的数字,取消引用一个空指针。事实上,即使文件无法打开,这也可能是崩溃发生的时候,但这只是先取消引用哪个空指针的问题。

这段代码中有不少问题。你是:

  • 内存泄漏,
  • 不检查错误 returns,
  • 不将输入字符串限制为变量的大小
  • char (*)[25] 值传递给需要 char * 个值的函数
  • 解引用空指针,
  • 不在名称组件之间放置 spaces,
  • 在非空终止字符串的内存块末尾连接,
  • 不为每位阅读的学生分配新的space,并且
  • 输入数据错误容易导致 ID 号处理不当。

这导致在重写代码时出现大量注释。

struct studentInfo
{
    char name[50];
    int id;
    struct studentInfo *next;
} *head = NULL;

static const char filename[] =
    "C:\Users\Brent Rademaker\Desktop\COP3502C\Assignment 1\AssignmentOneInput.txt";

// Pass file name as argument so that multiple files can be processed
void createList(const char *file)
{
    // I'd use ofp for an output file; fp when there's one file, or
    // ifp for an input file
    FILE *ofp;
    // struct studentInfo *new_node = (struct studentInfo *)malloc(sizeof(struct studentInfo));
    // struct studentInfo *temp = (struct studentInfo *)malloc(sizeof(struct studentInfo));
    struct studentInfo *tail = head;  // Major change
    char firstName[25];
    char lastName[25];
    int id;  // Added

    // Originally:
    // new_node = head;  // Overwrites newly allocated memory with NULL (leak 1)
    // temp = head;      // Overwrites newly allocated memory with NULL (leak 2)

    ofp = fopen(file, "r"); 
    if (ofp == NULL)
    {
        fprintf(stderr, "failed to open file %s for reading\n", filename);
        exit(1);
    }

    // Traverse to end of list (necessary on second call)
    while (tail != NULL && tail->next != NULL)
        tail = tail->next;

    // Limit strings to size of variables (size - 1 specified)
    // Test that you get 3 values; if you only get two, the ID is invalid.
    while (fscanf(ofp, "%24s %24s %d", firstName, lastName, &id) == 3)
    {
        // Originally:
        // strncat((new_node->name), firstName, 10);
        // strncat((new_node->name), lastName, 10);
        // These are appending to the end of a string that is not guaranteed
        // to be null terminated.  The names were not limited to 10 bytes.
        // There is no space between the first and last names in the concatenated string.

        // Allocate new node when student information read correctly.
        // Cast left in place since compiler may be a C++ compiler compiling C
        struct studentInfo *new_node = (struct studentInfo *)malloc(sizeof(*new_node));
        if (new_node == NULL)
            break;
        // This sequence is safe because firstname contains up to 24 bytes plus null,
        // lastName contains up to 24 bytes plus null, and new_node->name can
        // hold 24 + 1 + 24 + 1 = 50 bytes.
        strcpy(new_node->name, firstName);
        strcat(new_node->name, " ");
        strcat(new_node->name, lastName);
        // If need be, use strcpy_s() and strcat_s()
        // strcpy_s(new_node->name, sizeof(new_node->name), firstName);
        // strcat_s(new_node->name, sizeof(new_node->name), " ");
        // strcat_s(new_node->name, sizeof(new_node->name), lastName);
        new_node->id = id;
        new_node->next = NULL;

        // Add new node to end of list
        if (head == NULL)
            head = new_node;
        else
            tail->next = new_node;
        tail = new_node;
        // Alternatively, and more simply, add new node to head of list
        // Don't need the tail variable any more, or any special case code
        // new_node->next = head;
        // head = new_node;
    }
    fclose(ofp);
}

int main(void)
{
    createList(filename);

    return 0;
}

替代测试代码

而不是现有的存根 main(),我使用此代码来测试 createList() 函数:

static void check_file(const char *file)
{
    struct studentInfo *node;

    printf("Before %s\n", file);
    createList(file);
    printf("After %s\n", file);
    node = head;
    while (node != NULL)
    {
        printf("%.5d %s\n", node->id, node->name);
        node = node->next;
    }
    printf("Done %s\n", file);
}

int main(void)
{
    check_file("data.1");
    check_file("data.2");
    return 0;
}

测试数据 — data.1:

Firstname LastName 1234
Abby Holmes 2345
PersonWithVeryLongFirst AndWithVeryLongLastName 3456

测试数据 — data.2:

Firstname LastName 12784
Abby Holmes 27845
PersonWithVeryLongFirst AndWithVeryLongLastName 78456

示例输出:

$ ./stud
Before data.1
After data.1
01234 Firstname LastName
02345 Abby Holmes
03456 PersonWithVeryLongFirst AndWithVeryLongLastName
Done data.1
Before data.2
After data.2
01234 Firstname LastName
02345 Abby Holmes
03456 PersonWithVeryLongFirst AndWithVeryLongLastName
12784 Firstname LastName
27845 Abby Holmes
78456 PersonWithVeryLongFirst AndWithVeryLongLastName
Done data.2
$

中间名和其他深奥的知识

Is there a way that I could make it variable? For instance if some people had middle names?

是的,有很多方法可以做到,但当然更难。考虑到英国王室每个人都有大量的名字(在你开始处理头衔等之前),你可能会得出结论,名字的数量是有上限的,比如 8 个。你将需要回到 fgets() 加上 sscanf() 处理。然后你可以使用:

typedef char Name[25];
Name n[9];          // At most 8 names plus an ID
char buffer[4096];
while (fgets(buffer, sizeof(buffer), ofp) != NULL)
{
    int n_scan = fscanf(ofp, "%s %s %s %s %s %s %s %s %s",
                        n[0], n[1], n[2], n[3], n[4], n[5], n[6], n[7], n[8]);
    if (n_scan < 3)
        …format error…
    else
    {
        …process elements n[0] to n[n_scan-2] as names…
        …process n[n_scan-1] as id…with sscanf() probably…
        …add the information to the list…
    }
}

有多种方法可以检测您是否使用了该行中的所有内容,但它们更加深奥。您也可以考虑简单地阅读该行,然后向后扫描,忽略尾随的白色 space,然后检查最后一个 'word' 是一个 ID,并简单地使用所有先前的 material 作为 'the name'。但是,有了这个,您需要考虑您的结构是否应该包含一个 char *name; 元素,以便您将名称复制 (strdup()?) 到其中。它使“free()”过程复杂化(您需要在释放节点之前释放名称),但这通常是一个明智的惩罚。

您还可以查找 How to use sscanf() in loops? 以遍历行中的文本。

您也可以考虑使用 C99 'flexible array member' (FAM) 作为名称;它必须是结构的最后一部分。由于必须动态分配包含 FAM 的结构,因此将它们与列表一起使用几乎没有问题。您可能需要也可能不需要记录名称的长度——这取决于您是否会更改现有节点中的名称。 (如果你不会,strlen(node->name) 就足够了;如果你愿意,你需要知道是否有足够的 space 用于新名称,当当前名称可能比可用的 space.) 总的来说,我认为 FAM 太复杂了,你本周无法可靠地管理它;你需要比现在更熟悉 C。