Недавно я начал изучать 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
. Каждая запись должна получать указатель на одну и ту же память. Но я не могу понять, почему это так.
Вот как я понимаю код в цикле:
char id[8];
— в стеке выделяется память на 8 символов и присваивается адрес id
.snprintf(id, 8, "entry-%d", i);
— указатель на буфер id
передается в snprintf
, который заполняет его символами созданной строки.struct Entry *e = new_entry(id);
— указатель на буфер id
передается в new_entry
, где в куче выделяется новая структура Entry, а ее члену id
присваивается id
(указатель).bucket_add(b, e);
— указатель на сегмент и указатель на запись передаются в bucket_add
, а запись (указатель) добавляется в массив записей сегмента.Когда происходит новая итерация, это выглядит так, как будто выделяется новый раздел памяти и его новый адрес памяти назначается новой переменной в локальной области видимости. Так как же предыдущая память перезаписывается?
Я был бы признателен, если бы кто-нибудь прояснил мое понимание, пожалуйста.
@WeatherVane Я считал, что это так, но не является ли локальный буфер новым буфером на каждой итерации цикла, поскольку он выходит за рамки?
Если он выходит за пределы области видимости, любые указатели на него становятся недействительными. Вам нужно сделать динамическую копию строки и сохранить указатель на нее в хэш-ключе.
Как элемент стека, вполне вероятно, что это будет тот же адрес. Но, как вы говорите, это выходит за рамки, поэтому печать результата вне цикла — это UB.
Вероятно, происходит то, что каждый раз, когда вы вызываете функцию, адрес локального буфера остается одним и тем же.
@WeatherVane Ага, это имеет смысл. Оригинал не перезаписался, но оригинал теперь может указывать на что угодно? И в моем случае происходит повторное использование того же адреса, который только что стал доступен.
Да, вызов printf()
за пределами этого блока кода может привести к уничтожению данных, а может и не привести к их потере. В противном случае все записи будут указывать на самые последние данные.
Спасибо обоим. Если кто-то захочет написать ответ, описывающий, что вместо локального буфера необходимо создать и сохранить динамическую копию, я могу принять ответ.
@Barmar: Re: «Если он выходит за рамки, любые указатели на него становятся недействительными»: это не правило. В static void foo(const char *p) { puts(p); } int main(void) { const char a[] = "Hello, world."; foo(a); }
a
не входит в область действия foo
, но указатель на него все еще действителен.
Когда массив выходит за пределы области видимости, он освобождается и становится доступным для повторного использования. В большинстве реальных компиляторов из-за способа распределения памяти следующий массив идентификаторов получает точно такой же участок памяти.
@EricPostpischil a
в вашем примере не вышел за рамки. Это не было и не входит в область действия функции foo()
, но все еще находится в области действия вызывающего объекта.
TL;DR: вызывающая сторона new_entry
всегда передает одно и то же значение для id
. Чтобы это исправить, в new_entry
замените e->id = id;
на e->id = strdup(id);
.
@WeatherVane, я думаю, Эрик предпочел бы терминологию вроде «когда выполнение самого внутреннего блока, содержащего определение id
, завершается, любые указатели на него становятся недействительными». Если он в особенно раздраженном настроении, то может даже настоять на том, чтобы «стать неопределенным». Технически он прав, хотя мы все знали, что имел в виду Бармар, возможно, даже ОП.
@JohnBollinger: Я не знаю, почему люди думают, что студенты смогут устранить неточности или двусмысленности. Откуда взяться информация, необходимая для определения правильного значения? Характер обучения означает, что у человека еще нет желаемой информации. У какого-то конкретного учащегося может оказаться достаточно информации для решения проблемы в одном случае, но у некоторых его наверняка не будет. Следует исходить из того, что студентам необходимо делать правильные однозначные утверждения.
Вы передаете указатель на 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], и вы читаете последнее внесенное вами изменение». Похоже, что память перезаписана, потому что я повторно использовал указатель, хотя технически я этого не делал. На самом деле я получил новый указатель, но поскольку старый указатель и его базовая память вышли за пределы области действия и были освобождены, мне снова был назначен тот же адрес указателя...
... Итак, хотя похоже, что я повторно использовал указатель, это зависит от реализации и на самом деле является неопределенным поведением. В остальном ваш ответ очень хорош, так что спасибо!
Благодаря комментариям к моему вопросу, я теперь понимаю проблему.
У меня возникла ошибка «использование стека после области». Эта ошибка возникает, когда указатель на память в стеке используется после завершения области, в которой была выделена память.
В приведенном ниже коде я создал буфер 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.
e->id = id;
сохраняет переданный указатель, который находится в одном и том же локальном буфере для каждой записи.