Распределяет ли printf () память в C?

Этот простой метод просто создает массив динамического размера n и инициализирует его значениями 0 ... n-1. Он содержит ошибку, malloc () выделяет только n байтов вместо sizeof (int) * n:

int *make_array(size_t n) {
    int *result = malloc(n);

    for (int i = 0; i < n; ++i) {
        //printf("%d", i);
        result[i] = i;
    }

    return result;
}

int main() {
    int *result = make_array(8);

    for (int i = 0; i < 8; ++i) {
        printf("%d ", result[i]);
    }

    free(result);
}

Когда вы проверите вывод, вы увидите, что он напечатает некоторые числа, как и ожидалось, но последние - тарабарщина. Однако, как только я вставил printf () внутрь цикла, результат был странно правильным, даже несмотря на то, что распределение все еще было неправильным! Есть ли какое-то выделение памяти, связанное с printf ()?


person AdHominem    schedule 04.10.2016    source источник
comment
Часто printf() - или многие другие <stdio.h> функции - выделяют буфер, связанный с FILE *, когда буфер требуется впервые, а не при создании файлового потока. Итак, краткий ответ на вопрос заголовка - Да.   -  person Jonathan Leffler    schedule 05.10.2016
comment
Я предполагаю, что, вызвав в первую очередь Демонов неопределенного поведения, вы не должны удивляться, если позже получите еще более неопределенное поведение.   -  person Jongware    schedule 05.10.2016
comment
как только я вставил printf () внутрь цикла .... Где именно вы вставили лишний printf?   -  person AnT    schedule 05.10.2016
comment
malloc (8) возвращает память для 8 байтов или возвращает NULL. Вы пытаетесь сохранить там 8 целых чисел, каждое из которых занимает (в зависимости от системы) 4 байта. Следовательно, C не гарантирует, что произойдет с последними 6 целыми числами, отсюда неопределенное поведение.   -  person hetepeperfan    schedule 05.10.2016
comment
Второй printf, который вы упомянули, //printf("%d", i); Вы просто печатаете i, а не буфер, так что это будет работать должным образом.   -  person Fantastic Mr Fox    schedule 05.10.2016
comment
@AnT Я имею в виду printf (), который закомментирован в коде.   -  person AdHominem    schedule 05.10.2016
comment
@AdHominem: Если вы ссылаетесь на этот printf, то почему вас удивляет, что этот printf все печатает правильно? Этот printf просто печатает i напрямую. Он полностью не зависит от распределения памяти.   -  person AnT    schedule 05.10.2016
comment
Волшебная часть состоит в том, что в результирующем массиве будут правильные числа внутри, printf на самом деле просто печатает каждое число в другой раз. Но это меняет массив   -  person AdHominem    schedule 05.10.2016
comment
@AdHominem В неопределенном поведении нет ничего волшебного. См. мой ответ, где я пытаюсь его объяснить.   -  person Braden Best    schedule 21.10.2016


Ответы (3)


Строго говоря, чтобы ответить на вопрос в заголовке, ответ будет таков, что это зависит от реализации. Некоторые реализации могут выделять память, а другие - нет.

Хотя в вашем коде есть и другие проблемы, о которых я расскажу ниже.


Примечание: изначально это была серия комментариев, которые я сделал по вопросу. Я решил, что это слишком много для комментария, и переместил их к этому ответу.


Когда вы проверите вывод, вы увидите, что он напечатает некоторые числа, как и ожидалось, но последние - тарабарщина.

Я считаю, что в системах, использующих модель сегментированной памяти, выделения «округляются» до определенного размера. Т.е. если вы выделяете X байтов, ваша программа действительно будет владеть этими X байтами, однако вы также сможете (неправильно) пройти мимо этих X байтов на некоторое время, прежде чем ЦП заметит, что вы нарушаете границы, и отправит SIGSEGV.

Скорее всего, именно поэтому ваша программа не дает сбоев в вашей конкретной конфигурации. Обратите внимание, что выделенные вами 8 байтов будут охватывать только два int в системах, где sizeof (int) равно 4. Остальные 24 байта, необходимые для других 6 int, не принадлежат вашему массиву, поэтому что угодно может записывать в это пространство, и когда вы читаете из него space, то вы получите мусор, если ваша программа сначала не выйдет из строя.

Цифра 6 важна. Запомни это на потом!

Волшебная часть состоит в том, что в результирующем массиве будут правильные числа внутри, printf на самом деле просто печатает каждое число в другой раз. Но это меняет массив.

Примечание. Следующее является предположением, и я также предполагаю, что вы используете glibc в 64-битной системе. Я собираюсь добавить это, потому что считаю, что это может помочь вам понять возможные причины, по которым что-то может казаться работающим правильно, хотя на самом деле неверно.

Причина, по которой это "магически правильно", скорее всего, связана с printf получением этих чисел через va_args. printf, вероятно, заполняет область памяти сразу за физической границей массива (поскольку vprintf выделяет память для выполнения операции "itoa", необходимой для печати i). Другими словами, эти «правильные» результаты на самом деле просто мусор, который «кажется правильным», но на самом деле это именно то, что находится в ОЗУ. Если вы попытаетесь изменить int на long, сохранив 8-байтовое распределение, ваша программа с большей вероятностью выйдет из строя, потому что long длиннее, чем int.

Реализация malloc в glibc имеет оптимизацию, при которой она выделяет целую страницу из ядра каждый раз, когда заканчивается куча. Это делает его быстрее, потому что вместо того, чтобы запрашивать у ядра больше памяти при каждом выделении, оно может просто захватить доступную память из «пула» и создать еще один «пул», когда первый заполнится.

При этом, как и стек, указатели кучи malloc, поступающие из пула памяти, имеют тенденцию быть смежными (или, по крайней мере, очень близко друг к другу). Это означает, что вызовы malloc из printf, скорее всего, появятся сразу после 8 байтов, выделенных для вашего массива int. Однако независимо от того, как это работает, суть в том, что независимо от того, насколько «правильными» могут показаться результаты, на самом деле они просто мусор, и вы вызываете неопределенное поведение, поэтому нет никакого способа узнать, что произойдет, или программа будет делать что-то еще при других обстоятельствах, например, сбой или неожиданное поведение.


Итак, я попытался запустить вашу программу с printf и без него, и оба раза результаты были неверными.

# without printf
$ ./a.out 
0 1 2 3 4 5 1041 0 

По какой-то причине ничего не мешало держать в памяти 2..5. Однако что-то мешало удерживать в памяти 6 и 7. Я предполагаю, что это буфер vprintf, используемый для создания строкового представления чисел. 1041 будет текстом, а 0 будет нулевым ограничителем, '\0'. Даже если это не результат vprintf, что-то пишет по этому адресу между заполнением и печатью массива.

# with printf
$ ./a.out
*** Error in `./a.out': free(): invalid next size (fast): 0x0000000000be4010 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x77725)[0x7f9e5a720725]
/lib/x86_64-linux-gnu/libc.so.6(+0x7ff4a)[0x7f9e5a728f4a]
/lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7f9e5a72cabc]
./a.out[0x400679]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf0)[0x7f9e5a6c9830]
./a.out[0x4004e9]
======= Memory map: ========
00400000-00401000 r-xp 00000000 08:02 1573060                            /tmp/a.out
00600000-00601000 r--p 00000000 08:02 1573060                            /tmp/a.out
00601000-00602000 rw-p 00001000 08:02 1573060                            /tmp/a.out
00be4000-00c05000 rw-p 00000000 00:00 0                                  [heap]
7f9e54000000-7f9e54021000 rw-p 00000000 00:00 0 
7f9e54021000-7f9e58000000 ---p 00000000 00:00 0 
7f9e5a493000-7f9e5a4a9000 r-xp 00000000 08:02 7995396                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9e5a4a9000-7f9e5a6a8000 ---p 00016000 08:02 7995396                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9e5a6a8000-7f9e5a6a9000 rw-p 00015000 08:02 7995396                    /lib/x86_64-linux-gnu/libgcc_s.so.1
7f9e5a6a9000-7f9e5a869000 r-xp 00000000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5a869000-7f9e5aa68000 ---p 001c0000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5aa68000-7f9e5aa6c000 r--p 001bf000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5aa6c000-7f9e5aa6e000 rw-p 001c3000 08:02 7999934                    /lib/x86_64-linux-gnu/libc-2.23.so
7f9e5aa6e000-7f9e5aa72000 rw-p 00000000 00:00 0 
7f9e5aa72000-7f9e5aa98000 r-xp 00000000 08:02 7999123                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9e5ac5e000-7f9e5ac61000 rw-p 00000000 00:00 0 
7f9e5ac94000-7f9e5ac97000 rw-p 00000000 00:00 0 
7f9e5ac97000-7f9e5ac98000 r--p 00025000 08:02 7999123                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9e5ac98000-7f9e5ac99000 rw-p 00026000 08:02 7999123                    /lib/x86_64-linux-gnu/ld-2.23.so
7f9e5ac99000-7f9e5ac9a000 rw-p 00000000 00:00 0 
7ffc30384000-7ffc303a5000 rw-p 00000000 00:00 0                          [stack]
7ffc303c9000-7ffc303cb000 r--p 00000000 00:00 0                          [vvar]
7ffc303cb000-7ffc303cd000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]
012345670 1 2 3 4 5 6 7 Aborted

Это интересная часть. Вы не упомянули в своем вопросе, произошел ли сбой вашей программы. Но когда я его запустил, он разбился. Жесткий.

Также неплохо проверить с valgrind, если он у вас есть. Valgrind - полезная программа, которая сообщает, как вы используете свою память. Вот результат valgrind:

$ valgrind ./a.out
==5991== Memcheck, a memory error detector
==5991== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==5991== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==5991== Command: ./a.out
==5991== 
==5991== Invalid write of size 4
==5991==    at 0x4005F2: make_array (in /tmp/a.out)
==5991==    by 0x40061A: main (in /tmp/a.out)
==5991==  Address 0x5203048 is 0 bytes after a block of size 8 alloc'd
==5991==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5991==    by 0x4005CD: make_array (in /tmp/a.out)
==5991==    by 0x40061A: main (in /tmp/a.out)
==5991== 
==5991== Invalid read of size 4
==5991==    at 0x40063C: main (in /tmp/a.out)
==5991==  Address 0x5203048 is 0 bytes after a block of size 8 alloc'd
==5991==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5991==    by 0x4005CD: make_array (in /tmp/a.out)
==5991==    by 0x40061A: main (in /tmp/a.out)
==5991== 
0 1 2 3 4 5 6 7 ==5991== 
==5991== HEAP SUMMARY:
==5991==     in use at exit: 0 bytes in 0 blocks
==5991==   total heap usage: 2 allocs, 2 frees, 1,032 bytes allocated
==5991== 
==5991== All heap blocks were freed -- no leaks are possible
==5991== 
==5991== For counts of detected and suppressed errors, rerun with: -v
==5991== ERROR SUMMARY: 12 errors from 2 contexts (suppressed: 0 from 0)

Как видите, valgrind сообщает, что у вас есть invalid write of size 4 и invalid read of size 4 (4 байта - это размер int в моей системе). Также упоминается, что вы читаете блок размера 0, который следует за блоком размера 8 (блок, который вы заблокировали). Это говорит вам, что вы проходите мимо массива и попадаете в мусорную зону. Еще вы могли заметить, что он сгенерировал 12 ошибок из 2 контекстов. В частности, это 6 ошибок в контексте записи и 6 ошибок в контексте чтения. Ровно столько нераспределенного пространства, сколько я упоминал ранее.

Вот исправленный код:

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

int *make_array(size_t n) {
    int *result = malloc(n * sizeof (int)); // Notice the sizeof (int)

    for (int i = 0; i < n; ++i)
        result[i] = i;

    return result;
}

int main() {
    int *result = make_array(8);

    for (int i = 0; i < 8; ++i)
        printf("%d ", result[i]);

    free(result);
    return 0;
}

А вот вывод valgrind:

$ valgrind ./a.out
==9931== Memcheck, a memory error detector
==9931== Copyright (C) 2002-2015, and GNU GPL'd, by Julian Seward et al.
==9931== Using Valgrind-3.11.0 and LibVEX; rerun with -h for copyright info
==9931== Command: ./a.out
==9931== 
0 1 2 3 4 5 6 7 ==9931== 
==9931== HEAP SUMMARY:
==9931==     in use at exit: 0 bytes in 0 blocks
==9931==   total heap usage: 2 allocs, 2 frees, 1,056 bytes allocated
==9931== 
==9931== All heap blocks were freed -- no leaks are possible
==9931== 
==9931== For counts of detected and suppressed errors, rerun with: -v
==9931== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

Обратите внимание, что он не сообщает об ошибках и результаты верны.

person Braden Best    schedule 20.10.2016

Не указано, выделяет ли printf() какую-либо память в процессе выполнения своей работы. Было бы неудивительно, если бы какая-либо конкретная реализация сделала это, но нет никаких оснований предполагать, что это так. Более того, если работает одна реализация, это ничего не говорит о том, работает ли другая реализация.

То, что вы видите другое поведение, когда printf() находится внутри цикла, ничего вам не говорит. Программа демонстрирует неопределенное поведение, выходя за границы выделенного объекта. После этого все последующее поведение не определено. Вы не можете рассуждать о неопределенном поведении, по крайней мере, с точки зрения семантики C. Программа не имеет семантики C, когда начинается неопределенное поведение. Вот что означает «неопределенный».

person John Bollinger    schedule 04.10.2016

Вы выделяете для массива 8 байтов, но храните 8 int, каждый из которых составляет не менее 2 байтов (возможно, 4), поэтому вы пишете за пределами выделенной памяти. Это вызывает неопределенное поведение.

Когда вы вызываете неопределенное поведение, может случиться все, что угодно. Ваша программа может дать сбой, может показывать неожиданные результаты или может показаться, что она работает правильно. Кажущееся несвязанным изменение может изменить то, какое из вышеперечисленных действий происходит.

Исправьте выделение памяти, и ваш код будет работать должным образом.

int *result = malloc(sizeof(int) * n);
person dbush    schedule 04.10.2016
comment
However, once I inserted the printf() inside the loop, the output was strangely correct Вы должны упомянуть, что printf в цикле просто печатает i, что является четко определенным поведением. - person Fantastic Mr Fox; 05.10.2016
comment
int может быть только одним байтом, если CHAR_BIT не менее 16. - person 12431234123412341234123; 05.10.2016
comment
@ 12431234123412341234123 Проверка фактов: стандарт гарантирует, что int составляет минимум 16 бит (2 байта). int не может быть одним байтом. Если это так, то компилятор, допускающий это, не соответствует стандартам и не должен считаться компилятором C. - person Braden Best; 21.10.2016
comment
@Braden Best: int может быть одним байтом. ANSI-C, C99 и C11 запрещают, чтобы int могло быть только одним байтом. (как я уже писал). CHAR_BIT может быть 16, и в этом случае байт имеет длину 16 бит, а для int требуется только один байт. - person 12431234123412341234123; 02.11.2016
comment
@ 12431234123412341234123, тогда вам нужно снова прочитать стандарт, потому что один байт не может содержать гарантированный диапазон -32767..+32767. - person Braden Best; 02.11.2016
comment
@BradenBest: байт должен иметь не менее 8 бит, если у вас есть реализации, которые имеют 8 бит / байт, в этом случае действительной реализации требуется не менее 2 байтов для int. Но допустимая реализация может иметь больше бит для байта, и если реализация имеет не менее 16 бит для байта, эта реализация может иметь или не иметь одного байта int. - person 12431234123412341234123; 02.11.2016
comment
@ 12431234123412341234123 вот и все. Два байта - это минимальный размер для int. - person Braden Best; 02.11.2016
comment
@BradenBest: (извините, я нажимаю Enter непреднамеренно, прежде чем написать полный комментарий), на этот вопрос уже дан ответ: (stackoverflow.com/questions/1738568/). Минимум 16 бит, а не 2 байта. Байт может иметь 16 бит. Также верно, что байт имеет более 63 бит, а все целочисленные типы имеют размер 1 байт. - person 12431234123412341234123; 02.11.2016
comment
@ 12431234123412341234123 Ты хоронишь вещи в семантике. Весь мир согласен с тем, что байт равен 8 битам. И так было десятилетиями. Нет никаких разумных оснований предполагать, что какой-либо компьютер будет использовать что-то еще. Потому что никто в здравом уме не стал бы развиваться по такой бездумной модели. - person Braden Best; 02.11.2016
comment
@BradenBest Байт не обязательно должен иметь ровно 8 бит. Такие модели действительно существуют. См. stackoverflow.com/ questions / 6971886 / - person dbush; 02.11.2016
comment
Это сломало бы такие вещи, как sizeof, который возвращает значение size_t для размера в байтах. Если байт составляет 16 бит, то что такое sizeof (char)? Вы не можете сказать 1, потому что это было бы неточно. Char составляет 8 бит. Вы не можете сказать 0,5, потому что целочисленные типы не работают, и вы не можете сказать 0, потому что это нарушит существующий код, в котором используется sizeof (char). - person Braden Best; 02.11.2016
comment
@BradenBest По определению, sizeof(char) всегда возвращает 1. Не имеет значения, является ли CHAR_BITS 8, 9, 16 или чем-то еще. Стандарты учитывают такие архитектуры. - person dbush; 02.11.2016
comment
@dbush, но char - 1 байт. Таким образом, хотя он не сломает код, он будет тратить много памяти при выделении для строк, поскольку ascii занимает всего 7-8 бит. И я не могу представить, как эта система справится, скажем, с накопителем на 250 ГБ. Упс, теперь 125гб. - person Braden Best; 02.11.2016
comment
@BradenBest: C ничего не говорите о максимальных размерах, имеет ли это смысл или нет, здесь не по теме. @ dbush, я думаю, вы имеете в виду CHAR_BIT, а не CHAR_BITS. - person 12431234123412341234123; 02.11.2016
comment
@BradenBest Байт определяется как минимальная адресуемая часть памяти. На некоторых архитектурах это может быть 9 или 16 бит. Если так настроено оборудование, вы не можете не тратить впустую в этом смысле. - person dbush; 02.11.2016
comment
@ 12431234123412341234123 Да, ошибка моя, я имел ввиду CHAR_BIT. - person dbush; 02.11.2016
comment
@dbush, и мы используем 8-битные байты более 30 лет. Совершенно разумно предположить, что каждое цифровое устройство в вашем доме использует 8-битные байты. Неразумно предполагать, что поставщик оборудования осмелится нарушить это соглашение за пределами какого-то странного экспериментального домашнего суперкомпьютера. - person Braden Best; 02.11.2016
comment
@BradenBest Сообщение, на которое я ссылался выше, ссылается на устройство Bluetooth с 16-битным байтом, так что это не просто устаревшее оборудование. Дело в том, что существуют архитектуры, хотя и редкие, в которых нет 8-битных байтов. Стандарт должен учитывать эти архитектуры. - person dbush; 02.11.2016
comment
@dbush тебе не кажется немного сломленным, если потенциально sizeof (int) == sizeof (char) может быть правдой? - person Braden Best; 02.11.2016
comment
@BradenBest Было бы необычно, да, но в стандарте этому ничего не мешает. - person dbush; 02.11.2016
comment
@dbush, это был интересный разговор. Я думаю, что числа должны были обратиться к размеру байта, прежде чем спорить о том, сколько байтов в int. Это предотвратило бы множество ненужных разочарований. Кроме того, вам, вероятно, следует отредактировать свой ответ, поскольку вы сказали, что int составляет не менее 2 байтов. - person Braden Best; 02.11.2016