Как перезаписать строковый буфер, не изменяя его ранее использовавшиеся

Недавно я начал изучать C и пытаюсь реализовать структуру данных хэш-карты. В настоящее время я реализую свои сегменты как списки с динамическим изменением размера на основе массива. При тестировании функции Bucket_remove я допустил ошибку в тестовом коде, которую пытаюсь понять.

Вот моя new_entry функция:

struct Entry *new_entry(char *id)
{
    struct Entry *e = (struct Entry *)malloc(sizeof(struct Entry));
    e->id = id;
    return e;
}

И вот моя test_remove функция:

static char *test_remove()
{
    struct Bucket *b = new_bucket();

    for (int i = 0; i < 5; i++)
    {
        char id[8];
        snprintf(id, 8, "entry-%d", i);
        struct Entry *e = new_entry(id);
        bucket_add(b, e);
    }

    printf("Entry 0 id: %s.\n", b->_entries[0]->id);
    printf("Entry 1 id: %s.\n", b->_entries[1]->id);

    // ... rest of code omitted for clarity
}

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

Entry 0 id: entry-4.
Entry 1 id: entry-4.

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

Вот как я понимаю код в цикле:

  1. char id[8]; — в стеке выделяется память на 8 символов и присваивается адрес id.
  2. snprintf(id, 8, "entry-%d", i); — указатель на буфер id передается в snprintf, который заполняет его символами созданной строки.
  3. struct Entry *e = new_entry(id); — указатель на буфер id передается в new_entry, где в куче выделяется новая структура Entry, а ее члену id присваивается id (указатель).
  4. bucket_add(b, e); — указатель на сегмент и указатель на запись передаются в bucket_add, а запись (указатель) добавляется в массив записей сегмента.

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

Я был бы признателен, если бы кто-нибудь прояснил мое понимание, пожалуйста.

e->id = id; сохраняет переданный указатель, который находится в одном и том же локальном буфере для каждой записи.
Weather Vane 15.04.2024 23:09

@WeatherVane Я считал, что это так, но не является ли локальный буфер новым буфером на каждой итерации цикла, поскольку он выходит за рамки?

Kyohei Kaneko 15.04.2024 23:12

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

Barmar 15.04.2024 23:13

Как элемент стека, вполне вероятно, что это будет тот же адрес. Но, как вы говорите, это выходит за рамки, поэтому печать результата вне цикла — это UB.

Weather Vane 15.04.2024 23:13

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

Barmar 15.04.2024 23:14

@WeatherVane Ага, это имеет смысл. Оригинал не перезаписался, но оригинал теперь может указывать на что угодно? И в моем случае происходит повторное использование того же адреса, который только что стал доступен.

Kyohei Kaneko 15.04.2024 23:15

Да, вызов printf() за пределами этого блока кода может привести к уничтожению данных, а может и не привести к их потере. В противном случае все записи будут указывать на самые последние данные.

Weather Vane 15.04.2024 23:16

Спасибо обоим. Если кто-то захочет написать ответ, описывающий, что вместо локального буфера необходимо создать и сохранить динамическую копию, я могу принять ответ.

Kyohei Kaneko 15.04.2024 23:19

@Barmar: Re: «Если он выходит за рамки, любые указатели на него становятся недействительными»: это не правило. В static void foo(const char *p) { puts(p); } int main(void) { const char a[] = "Hello, world."; foo(a); }a не входит в область действия foo, но указатель на него все еще действителен.

Eric Postpischil 15.04.2024 23:27

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

Stack Exchange Supports Israel 15.04.2024 23:38

@EricPostpischil a в вашем примере не вышел за рамки. Это не было и не входит в область действия функции foo(), но все еще находится в области действия вызывающего объекта.

Weather Vane 15.04.2024 23:49

TL;DR: вызывающая сторона new_entry всегда передает одно и то же значение для id. Чтобы это исправить, в new_entry замените e->id = id; на e->id = strdup(id);.

Craig Estey 15.04.2024 23:59

@WeatherVane, я думаю, Эрик предпочел бы терминологию вроде «когда выполнение самого внутреннего блока, содержащего определение id, завершается, любые указатели на него становятся недействительными». Если он в особенно раздраженном настроении, то может даже настоять на том, чтобы «стать неопределенным». Технически он прав, хотя мы все знали, что имел в виду Бармар, возможно, даже ОП.

John Bollinger 16.04.2024 00:04

@JohnBollinger: Я не знаю, почему люди думают, что студенты смогут устранить неточности или двусмысленности. Откуда взяться информация, необходимая для определения правильного значения? Характер обучения означает, что у человека еще нет желаемой информации. У какого-то конкретного учащегося может оказаться достаточно информации для решения проблемы в одном случае, но у некоторых его наверняка не будет. Следует исходить из того, что студентам необходимо делать правильные однозначные утверждения.

Eric Postpischil 16.04.2024 00:21
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
14
98
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы передаете указатель на new_entry(char *id)

Для компьютера, создающего в стеке переменную с адресом памяти строки идентификатора.

Когда вы делаете e->id = id; в new_entry(), вы сохраняете e->id адрес памяти строки идентификатора, а не самой строки.

При повторном вызове snprintf(id, ...); вы переписываете строку, сохраняете в своем char id[8];.

И когда вы читаете свой идентификатор, вы читаете указатель на char id[8] и читаете последнее внесенное вами изменение.

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

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

struct Entry
{
    char *id;
};

struct Entry *new_entry(char *id)
{
    struct Entry *e = malloc(sizeof(struct Entry));
    if (e == NULL)
        return (NULL);
    e->id = id;
    return e;
}

int main(void)
{
    struct Entry * b[2];

    for (int i = 0; i < 2; i++)
    {
        char id[8];
        id[0] = '0' + i;
        id[1] = '\0';
        struct Entry *e = new_entry(id);
        if (e == NULL)
            return (-1);
        b[i] = e;
    }

    printf("Entry 0 id: %s.\n", b[0]->id);
    printf("Entry 1 id: %s.\n", b[1]->id);
    printf("Adress 0 id: %p.\n", b[0]->id);
    printf("Adress 1 id: %p.\n", b[1]->id);
    free(b[0]);
    free(b[1]);
    return (0);
}

Результат:

➜  test ./a.out
Entry 0 id: 1.
Entry 1 id: 1.
Adress 0 id: 0x7ffd7496bd30.
Adress 1 id: 0x7ffd7496bd30.

У нас есть время для одного и того же адреса, чтобы синхронизировать одну и ту же строку. У нас также есть проблема, потому что char id[8] больше нет в стеке при печати. И это может привести к повреждению стека:

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

struct Entry
{
    char *id;
};

struct Entry *new_entry(char *id)
{
    struct Entry *e = malloc(sizeof(struct Entry));
    if (e == NULL)
        return (NULL);
    e->id = id;
    return e;
}

int main(void)
{
    struct Entry * b[2];

    for (int i = 0; i < 2; i++)
    {
        char id[8];
        id[0] = '0' + i;
        id[1] = '\0';
        struct Entry *e = new_entry(id);
        if (e == NULL)
            return (-1);
        b[i] = e;
    }

    char test[8];

    test[0] = ';';
    test[1] = '\0';
(void)test;

    printf("Entry 0 id: %s.\n", b[0]->id);
    printf("Entry 1 id: %s.\n", b[1]->id);
    printf("Adress 0 id: %p.\n", b[0]->id);
    printf("Adress 1 id: %p.\n", b[1]->id);
    free(b[0]);
    free(b[1]);
    return (0);
}

Результат:

➜  test ./a.out
Entry 0 id: ;.
Entry 1 id: ;.
Adress 0 id: 0x7ffc08207880.
Adress 1 id: 0x7ffc08207880.

Если вы хотите сохранить копию строки, вам нужно вызвать strdup (дублировать строку) или strcpy (копировать строку):

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

struct Entry
{
    char *id;
};

struct Entry *new_entry(char *id)
{
    struct Entry *e = malloc(sizeof(struct Entry));
    if (e == NULL)
        return (NULL);
    e->id = strdup(id);
    if (e->id == NULL)
    {
        free(e);
        return (NULL);
    }
    return e;
}

int main(void)
{
    struct Entry * b[2];

    for (int i = 0; i < 2; i++)
    {
        char id[8];
        id[0] = '0' + i;
        id[1] = '\0';
        struct Entry *e = new_entry(id);
        if (e == NULL)
            return (-1);
        b[i] = e;
    }

    printf("Entry 0 id: %s.\n", b[0]->id);
    printf("Entry 1 id: %s.\n", b[1]->id);
    printf("Adress 0 id: %p.\n", b[0]->id);
    printf("Adress 1 id: %p.\n", b[1]->id);
    free(b[0]->id);
    free(b[1]->id);
    free(b[0]);
    free(b[1]);
    return (0);
}

Результат:

➜  test ./a.out
Entry 0 id: 0.
Entry 1 id: 1.
Adress 0 id: 0x556eb8f522c0.
Adress 1 id: 0x556eb8f52300.

А еще кастинг своего malloc()вообще плохая идея.

И не забывайте, что вызов функции, такой как malloc() или strdup, должен иметь результат free() и может привести к сбою, и вы несете ответственность за его обработку.

Привет, Вон, спасибо за твой ответ. Однако, похоже, первая часть нуждается в пояснении. В частности, этот бит: «Когда вы повторно вызываете snprintf(id, ...); вы переписываете строку, сохраняете ее в своем идентификаторе char [8];. И когда вы читаете свой идентификатор, вы читаете указатель на идентификатор char [8], и вы читаете последнее внесенное вами изменение». Похоже, что память перезаписана, потому что я повторно использовал указатель, хотя технически я этого не делал. На самом деле я получил новый указатель, но поскольку старый указатель и его базовая память вышли за пределы области действия и были освобождены, мне снова был назначен тот же адрес указателя...

Kyohei Kaneko 16.04.2024 11:15

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

Kyohei Kaneko 16.04.2024 11:16
Ответ принят как подходящий

Благодаря комментариям к моему вопросу, я теперь понимаю проблему.

У меня возникла ошибка «использование стека после области». Эта ошибка возникает, когда указатель на память в стеке используется после завершения области, в которой была выделена память.

В приведенном ниже коде я создал буфер char id[8] в цикле for, придав ему локальную область действия внутри блока. Это означает, что id не только выходит из области видимости в конце каждой итерации (что большинство программистов распознает в других языках программирования), но, что особенно важно, сама базовая память также помечается как доступная.

for (int i = 0; i < 5; i++)
{
    char id[8];
    snprintf(id, 8, "entry-%d", i);
    struct Entry *e = new_entry(id);
    bucket_add(b, e);
}

На каждой итерации моего цикла for из стека запрашиваются новые 8 байтов и присваиваются новой переменной с локальной областью действия под названием id. Мой компилятор повторно использует 8 байтов в стеке, которые только что стали доступны в предыдущей итерации, поэтому id получает тот же адрес памяти в стеке, что и в предыдущей итерации. Однако это зависит от реализации и не всегда может работать таким образом.

Это само по себе нормально и ожидаемо, как и обычные правила определения области действия. Ошибка возникает потому, что я беру указатель на стековую память id и копирую этот указатель в кучу памяти здесь:

struct Entry *new_entry(char *id)
{
    struct Entry *e = (struct Entry *)malloc(sizeof(struct Entry));
    e->id = id;
    return e;
}

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

Итак, когда я получил доступ к данным за пределами локальной области, в которой они были объявлены:

printf("Entry 0 id: %s.\n", b->_entries[0]->id);
printf("Entry 1 id: %s.\n", b->_entries[1]->id);

Выход:

Entry 0 id: entry-4.
Entry 1 id: entry-4.

Неопределённое поведение проявлялось в том, что каждая запись в моей корзине имела указатель id на одну и ту же ячейку памяти и, следовательно, на одно и то же значение. Чем позже будет использоваться указатель, тем хуже будет, потому что вскоре эта память будет снова перезаписана.

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

Для этого я решил использовать функцию strdup:

#include <string.h>

struct Entry *new_entry(char *id)
{
    struct Entry *e = (struct Entry *)malloc(sizeof(struct Entry));
    e->id = strdup(id);
    return e;
}

Вывод показывает, что ошибка устранена:

Entry 0 id: entry-0.
Entry 1 id: entry-1.

Другие вопросы по теме