Что происходит с памятью после '\ 0' в строке C?

Удивительно простой/глупый/основной вопрос, но я понятия не имею. Предположим, я хочу вернуть пользователю мою функцию C-строку, длина которой я не знаю в начале функции. С самого начала я могу разместить только верхнюю границу длины и, в зависимости от обработки, размер может уменьшаться.

Вопрос в том, есть ли что-то неправильное при распределении достаточного пространства кучи (верхняя граница), а затем завершение строки, которая значительно меньше, чем во время обработки? т.е. если я вставляю '\ 0' в середину выделенной памяти, то (a.) free() все еще работает правильно, и (b.) делает ли пространство после "\ 0" несущественным? Как только добавляется "\ 0", возвращается ли память только в том случае, если она сидит там, пока не появится free()? Это вообще плохой стиль программирования, чтобы оставить это пространство для подвешивания там, чтобы сохранить некоторое предварительное время программирования, вычисляя необходимое пространство перед вызовом malloc?

Чтобы дать этот контекст, скажем, я хочу удалить последовательные дубликаты, например:

ввод "Привет, ооо!" → вывод "Helo oOo!"

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

char* RemoveChains(const char* str)
{
    if (str == NULL) {
        return NULL;
    }
    if (strlen(str) == 0) {
        char* outstr = (char*)malloc(1);
        *outstr = '\0';
        return outstr;
    }
    const char* original = str; // for reuse
    char prev = *str++;       // [prev][str][str+1]...
    unsigned int outlen = 1;  // first char auto-counted

    // Determine length necessary by mimicking processing
    while (*str) {
        if (*str != prev) { // new char encountered
            ++outlen;
            prev = *str; // restart chain
        }
        ++str; // step pointer along input
    }

    // Declare new string to be perfect size
    char* outstr = (char*)malloc(outlen + 1);
    outstr[outlen] = '\0';
    outstr[0] = original[0];
    outlen = 1;

    // Construct output
    prev = *original++;
    while (*original) {
        if (*original != prev) {
            outstr[outlen++] = *original;
            prev = *original;
        }
        ++original;
    }
    return outstr;
}

Ответ 1

Если я вставляю '\ 0' в середину выделенной памяти,

(a.) free() все еще работают правильно, а

Да.

(b.) ли пространство после '\ 0' становится несущественным? Как только добавляется "\ 0", возвращается ли память только, или она сидит там, где будет пустое место, пока не будет вызван вызов free()?

Зависит. Часто, когда вы выделяете большое количество кучи пространства, система сначала выделяет виртуальное адресное пространство - по мере того, как вы пишете на страницы, какая-то фактическая физическая память назначается для ее возврата (а затем может быть заменена на диск, когда ваша ОС имеет виртуальную память поддержка). Известно, что это различие между расточительным распределением виртуального адресного пространства и фактической физической/swap-памятью позволяет разреженным массивам быть достаточно эффективной для памяти на таких ОС.

Теперь гранулярность этой виртуальной адресации и поискового вызова находится в размерах страниц памяти - это может быть 4k, 8k, 16k...? У большинства ОС есть функция, которую вы можете позвонить, чтобы узнать размер страницы. Таким образом, если вы выполняете множество небольших распределений, округление до размеров страниц является расточительным, и если у вас ограниченное адресное пространство относительно объема памяти, которое вам действительно нужно использовать, то в зависимости от виртуальной адресации описанным выше способом не будет масштабироваться (например, 4 ГБ ОЗУ с 32-разрядной адресацией). С другой стороны, если у вас есть 64-битный процесс, который работает с 32 ГБ ОЗУ, и он делает относительно немного таких распределений строк, у вас есть огромное количество виртуального адресного пространства для игры и округления до размера страницы, t много.

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

Также стоит отметить, что во многих операционных системах память кучи не может быть возвращена в операционную систему до тех пор, пока процесс не завершится: вместо этого, malloc/free library уведомляет ОС о необходимости наращивать кучу (например, используя sbrk() в UNIX или VirtualAlloc() в Windows). В этом смысле память free() бесплатна для повторного использования вашего процесса, но не бесплатна для других процессов. Некоторые операционные системы оптимизируют это - например, используя отдельную и независимо освобождаемую область памяти для очень больших распределений.

Как правило, это плохой стиль программирования, чтобы оставить это пространство для подвешивания, чтобы сохранить некоторое предварительное время программирования, вычисляя необходимое пространство перед вызовом malloc?

Опять же, это зависит от того, сколько таких распределений вы имеете в виду. Если в вашем виртуальном адресном пространстве/ОЗУ очень много, то вы хотите явно указать библиотеке памяти, что не все изначально запрошенной памяти действительно необходимы с помощью realloc(), или вы даже можете использовать strdup() для выделения нового блокировать более жестко на основе реальных потребностей (тогда free() оригинал) - в зависимости от вашей реализации библиотеки malloc/free, которая может работать лучше или хуже, но очень мало приложений будет сильно затронуто любой разницей.

Иногда ваш код может находиться в библиотеке, где вы не можете догадаться, сколько строковых экземпляров будет обрабатываться вызывающим приложением - в таких случаях лучше обеспечить более медленное поведение, которое никогда не становится слишком плохим... поэтому склоняйтесь к сокращению блоки памяти для соответствия строковым данным (заданное количество дополнительных операций, поэтому не влияет на эффективность большого вывода), а не на то, что неизвестная доля исходного буфера строки теряется (в патологическом случае - ноль или один символ, используемый после сколь угодно большого распределения). В качестве оптимизации производительности вы можете беспокоиться о возврате памяти, если неиспользуемое пространство >= используемое пространство - настройте на вкус или сделайте его настраиваемым.

Вы прокомментируете другой ответ:

Итак, все зависит от того, будет ли realloc занимать больше времени или определить размер предварительной обработки?

Если производительность - ваш главный приоритет, тогда да - вы хотите профиль. Если вы не привязываетесь к ЦП, тогда как общее правило берет "препроцессинг" и делает правильное распределение - там меньше фрагментации и беспорядка. Противодействие этому, если вам нужно написать специальный режим предварительной обработки для какой-либо функции - это дополнительная "поверхность" для ошибок и кода для поддержки. (Это компромиссное решение обычно необходимо при реализации вашего собственного asprintf() от snprintf(), но, по крайней мере, вы можете доверять snprintf() действовать как документально и лично не поддерживать его).

Ответ 2

Как только добавляется '\ 0', возвращается ли только память или она сидит ли там пробел до тех пор, пока не будет вызван свободный()?

В \0 нет ничего волшебного. Вы должны вызвать realloc, если хотите "сжать" выделенную память. В противном случае память будет сидеть там, пока вы не назовете free.

Если я вставляю '\ 0' в середину выделенной памяти, делает (a.) free() все еще работают нормально

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

Ответ 3

\0 - это еще один символ с точки зрения malloc и free, им все равно, какие данные вы вставляете в память. Таким образом, free будет по-прежнему работать, добавляете ли вы \0 в середине или вообще не добавляете \0. Дополнительное выделенное пространство все равно будет там, оно не будет возвращено обратно в процесс, как только вы добавите \0 в память. Я лично предпочел бы выделять только необходимый объем памяти вместо выделения на некоторой верхней границе, поскольку это просто растрачивает ресурс.

Ответ 4

Как только вы получите память из кучи, вызвав malloc(), память будет использована вами. Вставка \0 похожа на вставку любого другого символа. Эта память останется в вашем распоряжении, пока вы ее не освободите или пока ОС не вернет ее обратно.

Ответ 5

\0 - это чистое соглашение для интерпретации массивов символов как укусов - оно не зависит от управления памятью. Если вы хотите вернуть свои деньги, вы должны позвонить realloc. Строка не заботится о памяти (что является источником многих проблем безопасности).

Ответ 6

malloc просто выделяет кусок памяти. Его нужно использовать, как вы хотите, и вызывать свободную от исходной позиции указателя... Вставка "\ 0" в середине не имеет последствий...

Чтобы быть конкретным, malloc не знает, какой тип памяти вы хотите (он возвращает onle указатель void)..

Предположим, вы хотите выделить 10 байт памяти, начиная с 0x10 до 0x19..

char * ptr = (char *)malloc(sizeof(char) * 10);

Вставка нуля в 5-ое положение (0x14) не освобождает память 0x15 и далее...

Однако свободный от 0x10 освобождает весь фрагмент в 10 байт.

Ответ 7

  • free() по-прежнему будет работать с байтом NUL в памяти

  • пробел останется впустую до тех пор, пока не будет вызываться free(), или если вы впоследствии не сократите выделение

Ответ 8

Как правило, память - это память. Ему все равно, что вы пишете в нем. НО он имеет гонку, или если вы предпочитаете вкус (malloc, new, VirtualAlloc, HeapAlloc и т.д.). Это означает, что сторона, которая выделяет часть памяти, также должна предоставлять средства для ее освобождения. Если ваш API входит в DLL, тогда он должен предоставлять какую-либо свободную функцию. Это, конечно, накладывает бремя на вызывающего? Итак, почему бы не поставить нагрузку WHOLE на вызывающего? ЛУЧШИЙ способ справиться с динамически распределенной памятью - это НЕ выделить его самостоятельно. Попросите абонента выделить его и передать его вам. Он знает, какой аромат он выделил, и он несет ответственность за его освобождение, когда он это делает.

Как вызывающий абонент знает, сколько нужно выделить? Как и многие API Windows, ваша функция возвращает требуемое количество байтов при вызове, например. с указателем NULL, затем выполните задание, если он снабжен указателем не-NULL (используя IsBadWritePtr, если он подходит для вашего случая, чтобы проверить доступность).

Это также может быть намного более эффективным. Распределение памяти COST много. Слишком много распределений памяти вызывают фрагментацию кучи, а затем затраты выделяются еще больше. Поэтому в режиме ядра мы используем так называемые "списки поиска". Чтобы минимизировать количество распределений памяти, мы повторно используем блоки, которые мы уже выделили, и "освобождены", используя службы, которые NT-ядро предоставляет авторам драйверов. Если вы передадите ответственность за выделение памяти своему абоненту, он может передать вам дешевую память из стека (_alloca) или передать вам одну и ту же память снова и снова без каких-либо дополнительных распределений. Вы, конечно, не заботитесь, но вы разрешаете своему абоненту отвечать за оптимальную работу с памятью.

Ответ 9

Чтобы уточнить использование терминатора NULL в C: Вы не можете выделить "C-строку", вы можете выделить массив char и сохранить в ней строку, но malloc и free просто видят его как массив требуемой длины.

Строка C не является типом данных, а соглашением для использования массива char, где нулевой символ '\ 0' рассматривается как ограничитель строки. Это способ передачи строк без необходимости передавать значение длины в качестве отдельного аргумента. Некоторые другие языки программирования имеют явные строковые типы, которые сохраняют длину вместе с символьными данными, чтобы позволить передавать строки в одном параметре.

Функции, которые документируют свои аргументы как "строки C", передаются массивами char, но не имеют способа узнать, насколько велик массив без нулевого терминатора, поэтому, если это не так, все будет идти ужасно неправильно.

Вы увидите функции, которые ожидают массивы char, которые не обязательно обрабатываются как строки, всегда будут иметь параметр длины буфера, который должен быть передан. Например, если вы хотите обрабатывать данные char, где нулевой байт является допустимым значением, вы не можете использовать "\ 0" в качестве символа терминатора.

Ответ 10

Вы можете сделать то, что делают некоторые из API MS Windows, где вы (вызывающий) передаете указатель и размер выделенной памяти. Если этого недостаточно, вам сообщается, сколько байтов выделяется. Если этого достаточно, используется память, а результат - количество используемых байтов.

Таким образом, решение о том, как эффективно использовать память, предоставляется вызывающему. Они могут выделять фиксированные 255 байтов (общие при работе с путями в Windows) и использовать результат вызова функции, чтобы узнать, нужны ли больше байтов (не в случае с путями из-за MAX_PATH, равным 255 без обхода Win32 API), или же большинство байтов можно игнорировать... Вызывающий может также передавать ноль в качестве размера памяти и точно знать, сколько нужно выделять - не так эффективно обрабатывать, но может быть более эффективным по размеру.

Ответ 11

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

Выполнение двух проходов также прекрасное.

Вы задали правильные вопросы о компромиссах.

Как вы решаете?

Сначала используйте два прохода, потому что:

1. you'll know you aren't wasting memory.
2. you're going to profile to find out where
   you need to optimize for speed anyway.
3. upperbounds are hard to get right before
   you've written and tested and modified and
   used and updated the code in response to new
   requirements for a while.
4. simplest thing that could possibly work.

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

char* copyWithoutDuplicateChains(const char* str)
    {
    if (str == NULL) return NULL;

    const char* s = str;
    char prev = *s;               // [prev][s+1]...
    unsigned int outlen = 1;      // first character counted

    // Determine length necessary by mimicking processing

    while (*s)
        { while (*++s == prev);  // skip duplicates
          ++outlen;              // new character encountered
          prev = *s;             // restart chain
        }

    // Construct output

    char* outstr = (char*)malloc(outlen);
    s = str;
    *outstr++ = *s;               // first character copied
    while (*s)
        { while (*++s == prev);   // skip duplicates
          *outstr++ = *s;         // copy new character
        }

    // done

    return outstr;
    }