确定适当的预定义数组大小在C?

时间:2019-03-31 22:02:51

标签: c

在以下代码中,我将数组大小设置为20。在Valgrind中,代码测试干净。但是,一旦我将大小更改为30,就会出现错误(如下所示)。令我感到困惑的部分是我可以将值更改为40,并且错误消失了。将其更改为50,再次出现错误。然后进行60次测试,以此类推。一直这样。所以我希望有人能够向我解释一下。尽管尽我最大的努力来解决这个问题,但对于我来说,这还不是很清楚。这些错误很难确定,因为从各个方面来看代码都是有效的。

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

struct record {
    int number;
    char text[30];
};

int main(int argc, char *argv[])
{
    FILE *file = fopen("testfile.bin", "w+");
    if (ferror(file)) {
        printf("%d: Failed to open file.", ferror(file));
    }

    struct record rec = { 69, "Some testing" };

    fwrite(&rec, sizeof(struct record), 1, file);
    if (ferror(file)) {
        fprintf(stdout,"Error writing file.");
    }

    fflush(file);
    fclose(file);
}

Valgrind错误:

valgrind --leak-check=full --show-leak-kinds=all\
                --track-origins=yes ./fileio
==6675== Memcheck, a memory error detector
==6675== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==6675== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==6675== Command: ./fileio
==6675== 
==6675== Syscall param write(buf) points to uninitialised byte(s)
==6675==    at 0x496A818: write (in /usr/lib/libc-2.28.so)
==6675==    by 0x48FA85C: _IO_file_write@@GLIBC_2.2.5 (in /usr/lib/libc-2.28.so)
==6675==    by 0x48F9BBE: new_do_write (in /usr/lib/libc-2.28.so)
==6675==    by 0x48FB9D8: _IO_do_write@@GLIBC_2.2.5 (in /usr/lib/libc-2.28.so)
==6675==    by 0x48F9A67: _IO_file_sync@@GLIBC_2.2.5 (in /usr/lib/libc-2.28.so)
==6675==    by 0x48EEDB0: fflush (in /usr/lib/libc-2.28.so)
==6675==    by 0x109288: main (fileio.c:24)
==6675==  Address 0x4a452d2 is 34 bytes inside a block of size 4,096 alloc'd
==6675==    at 0x483777F: malloc (vg_replace_malloc.c:299)
==6675==    by 0x48EE790: _IO_file_doallocate (in /usr/lib/libc-2.28.so)
==6675==    by 0x48FCBBF: _IO_doallocbuf (in /usr/lib/libc-2.28.so)
==6675==    by 0x48FBE47: _IO_file_overflow@@GLIBC_2.2.5 (in /usr/lib/libc-2.28.so)
==6675==    by 0x48FAF36: _IO_file_xsputn@@GLIBC_2.2.5 (in /usr/lib/libc-2.28.so)
==6675==    by 0x48EFBFB: fwrite (in /usr/lib/libc-2.28.so)
==6675==    by 0x10924C: main (fileio.c:19)
==6675==  Uninitialised value was created by a stack allocation
==6675==    at 0x109199: main (fileio.c:11)
==6675== 
==6675== 
==6675== HEAP SUMMARY:
==6675==     in use at exit: 0 bytes in 0 blocks
==6675==   total heap usage: 2 allocs, 2 frees, 4,648 bytes allocated
==6675== 
==6675== All heap blocks were freed -- no leaks are possible
==6675== 
==6675== For counts of detected and suppressed errors, rerun with: -v
==6675== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

3 个答案:

答案 0 :(得分:4)

问题是结构中存在填充,以使int a 在内存中始终对齐4,即使在struct record s数组中也是如此。现在,20 + 4可被4整除,40 + 4和60 + 4也是如此。但是30 + 4和50 + 4不是。因此,需要添加2个填充字节以使sizeof (struct record)被4整除。

运行具有数组大小34的代码时,sizeof (struct record) == 36以及字节35和36包含不确定的值-即使struct record否则已完全初始化。更糟糕的是,写不确定值的代码会泄漏敏感信息-Heartbleed bug是一个很好的例子。

解决方案实际上是使用fwrite编写结构。而是单独编写成员-这也提高了可移植性。性能上也没有太大差异,因为fwrite 缓冲写入操作,fread也是如此。


P.S。挤满了struct的道路铺天盖地,您想要避免它们像通用代码中的瘟疫一样。


P.P.S。 ferror(file)几乎肯定不会在fopen之后才为真-在正常故障中,fopen将返回NULL,而ferror(NULL)可能会导致崩溃。

答案 1 :(得分:3)

[编辑]

我的回答与OP代码的弱点有关,但是Valgrind write(buf) points to uninitialized byte(s)是由于其他原因回答的。


打开失败时,ferror(file)未定义行为(UB)。

if (ferror(file))不是确定公开成功的正确测试。

FILE *file = fopen("testfile.bin", "w+");
// if (ferror(file)) {
//    printf("%d: Failed to open file.", ferror(file));
// }
if (file == NULL) {
    printf("Failed to open file.");
    return -1;  // exit code, do not continue
}

我没有看到其他明显的错误。


ferror(file)对于测试I / O结果(而不是打开文件)很有用。

答案 2 :(得分:0)

我最初误解了valgrind输出,因此@chux应该被接受。我会尽力将最好的答案组合在一起。

检查错误

第一个错误(我没有立即考虑过)是用fopen(3)检查ferror(3)返回的值。 fopen(3)调用会在错误时返回NULL(并设置errno),因此用NULL检查ferror(3)是错误的。

序列化文件上的结构。

通过初始化,您可以写出结构的所有字段,但是并没有初始化它覆盖的所有内存。例如,您的编译器可能在结构中保留了一些填充,以便在访问数据时获得更好的性能。当您在文件上写入整个结构时,实际上是在将未初始化的数据传递到fwrite(3)函数。

通过更改数组的大小,可以更改Valgrind的行为。可能是由于编译器更改了内存中结构的布局,并且使用了不同的填充。

尝试用rec擦拭memset(&rec, 0, sizeof(rec));变量,Valgrind应该停止抱怨。不过,这只会解决症状:由于您要序列化二进制数据,因此应将struct record标记为__attribute__((packed))

初始化内存

您的原始初始化很好。

初始化数据的另一种方法是使用strncpy(3)。 Strncpy将接受指向要写入的目标的指针,指向源内存块(应从中获取数据的指针)和可用写入大小作为参数。

通过使用strncpy(&rec.text, "hello world", sizeof(rec.text),您可以在rec.text缓冲区上写入“ hello world”。但是您应该注意字符串的终止:strncpy不会超出给定的大小写,并且如果源字符串长于该长度,则不会有任何字符串终止符。

Strncpy可以如下安全使用

strncpy(&rec.text, "hello world", sizeof(rec.text) - 1);
rec.text[sizeof(rec.text) - 1] = '\0';

第一行将“ hello world”复制到目标字符串。 sizeof(rec.text) - 1作为大小传递,因此我们为\0终止符留出了空间,终止符明确地写为最后一个字符,以涵盖sizeof(rec.text)比“ hello world”短的情况。

山雀

最后,错误通知应该转到stderr,而stdout是为了获取结果。