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


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

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

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

вход " Привет ооооо !!"-->выход " Helo oOo !"

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

char* RemoveChains(const char* str)
{
    if (str == NULL) {
        return NULL;
    }
    if (strlen(str) == 0) {
        char* outstr = (char*)malloc(1);
        *outstr = '';
        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] = '';
    outstr[0] = original[0];
    outlen = 1;

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

11 ответов:

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

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

да.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

вставить значение null в 5-й позиции (0x14) не освобождает память 0x15 года...

однако свободный от 0x10 освобождает весь кусок 10 байт..

  1. free() все равно будет работать с нулевым байтом в памяти

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

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

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

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

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

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

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

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

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

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

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

сделать два прохода тоже нормально.

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

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

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

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;
    }