Почему я могу переместить указатель, переданный в качестве аргумента функции, но не могу переназначить ему новое значение после `realloc`?

В этом вопросе я имею в виду другой вопрос stackoverflow, потому что у меня недостаточно баллов, чтобы прокомментировать его.


См.: Valgrind сообщает о недействительном Realloc

Вкратце: почему инструкция buffer = trim(buffer); должна быть написана вне функции? Почему это нельзя записать внутри функции как phrase = (char*)realloc(phrase, strlen(phrase)+1);?


Подробно: если я передам любой указатель на функцию, например const *str;, то, выполнив str++;, я смогу создать побочный эффект и изменить начальную позицию строки.

Но почему я не могу просто переназначить другое значение функции с помощью функций управления динамической памятью, таких как malloc и realloc?

Почему я не могу просто поставить str = (char*) realloc(str, strlen(str) + 1 * sizeof(char));?

Чем этот побочный эффект отличается от другого? Разве я не могу гипотетически просто двигаться str куда захочу?

C передается по значению. Функция не имеет доступа к переменной char * вызывающего объекта, а только к ее копии. Вы можете изменить значение указателя (надеюсь, на действительное новое значение), но для realloc требуется значение, которое было возвращено malloc/calloc/realloc, а не просто то, которое куда-то указывает, даже не внутри динамически выделенного блока.

Avi Berger 11.11.2022 01:26

@AviBerger Но если это «передача по значению», почему я могу создавать эти побочные эффекты и просто увеличивать их (извините, я пришел из программирования на Java)

sueszli 11.11.2022 01:32

Аргумент phrase в trim строго локален для этой функции. Звонящий не видит. Любые изменения phrase изнутри trim будут скрыты от вызывающего абонента. Вот почему trim возвращает новое значение указателя. Если бы это было не так, оно было бы потеряно, когда trim вернется.

Tom Karzes 11.11.2022 01:37

Вы можете изменить копию. Это не имеет прямого отношения к оригиналу. Java отличается от C. У меня есть для вас мантра программирования на C: «Нет такой вещи, как ссылочный тип. Нет такой вещи, как ссылочный тип».

Avi Berger 11.11.2022 01:39

realloc — это функция, освобождающая память, на которую указывает ptr. Если освобождаемая память является памятью стека, а не управляемой памятью, realloc не работает. Если вы хотите просто КОПИРОВАТЬ память str, используйте другую функцию.

david 11.11.2022 01:43

@AviBerger @TomKarez @david Большое спасибо за вашу помощь, ребята! Извините, но мне все еще очень трудно обдумать это. Если это строго локальная копия - то почему я могу изменить ее операцией инкремента, чтобы мои изменения были видны извне? например: C static void sideEffect(char *str) { str++; // works fine! This instruction changes the actual value - not it's copy! str = (char*) malloc(strlen(str) + 1 * sizeof(char)); // doesn't work for some reason :( }

sueszli 11.11.2022 01:54

@sueszli: str — это копия любого значения, которое было передано функции sideEffect. Поэтому str++ изменяет значение локальной копии, а не значение исходной переменной, которая использовалась для вызова функции sideEffect. То же самое относится и к str = (char*) malloc(strlen(str) + 1 * sizeof(char));.

Andreas Wenzel 11.11.2022 02:00

Java также передается по значению. Передайте int функции, и если вы измените это значение, вызывающая сторона не увидит изменений. void test(char **str) { (*str)++; } тогда предположим char *fubar = "hello world"; test(&fubar); ... поэкспериментируйте с этим. Кроме того, K&R2e. Читай, делай упражнения по мере того, как натыкаешься на них...

autistic 11.11.2022 02:03

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

autistic 11.11.2022 02:05

В Java у вас есть переменная, которая связана с динамически выделяемым объектом. В C вы имеете дело непосредственно с сущностями, которые не обязательно распределяются динамически. Вы можете (но не всегда) иметь дескрипторы этих сущностей. Это собственные объекты, классифицируемые как указатели. В sideEffect str++ изменяет локальный str (указатель). Это не влияет на сущность, которая была передана в fcn. Он также не изменяет буфер, на который указывает, а только место в этом буфере, на которое он указывает.

Avi Berger 11.11.2022 02:07

@sueszli: Эта демонстрационная программа может помочь вам понять. Как видите, значение исходной переменной сохраняет свое значение, изменяется только копия.

Andreas Wenzel 11.11.2022 02:08

@autistic Woah, вы правы — мое первоначальное предположение, что я могу даже создать побочный эффект с помощью str++, было неверным. Я попробовал еще раз и понял, что - Спасибо за объяснение.

sueszli 11.11.2022 11:09

@AviBerger - Этот контраст с Java действительно заставил меня понять различия. Большое спасибо - теперь я действительно понял это :)

sueszli 11.11.2022 11:09

@AndreasWenzel Не знал, что существует компилятор браузера! Спасибо за демонстрацию. Действительно полезно!

sueszli 11.11.2022 11:14

@sueszli мы можем объявить наши аргументы T *fubar вместо T fubar, а затем оперировать (*fubar), а не fubar. В вашем случае T — это char *, поэтому T * — это char **. Мы можем использовать одну и ту же идиому для любого типа, чтобы вызывающий мог видеть обновления, которые мы вызываем. realloc вместо этого передает эту информацию через возвращаемое значение (как указано в ответе). Я решил объяснить это, чтобы прояснить ваши убеждения о передаче по значению, поскольку вы, похоже, смущены этим. Как и в Java, необходимо передать ссылочный тип (конечно, по значению), чтобы вызывающая сторона могла видеть обновления того, на что указывает аргумент.

autistic 13.11.2022 05:47
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
15
67
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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

Мы можем «эмулировать» передачу по ссылке, если передаем указатель. Разыменовав указатель, мы можем получить доступ к «ссылочному» объекту, находящемуся вне вызываемой функции. Но этот указатель по-прежнему передается по значению, то есть является копией аргумента, инициализирующего параметр.

Примечание. Ссылки на C++ и другие языки не что иное, как указатели внутри. Однако есть дополнительная семантика. Возможно, вы захотите посмотреть на сгенерированный машинный код.

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

Проблема вопроса, на который вы ссылаетесь, может быть сведена к следующему:

char* called(char* pointer)
{
    return realloc(pointer, /* some irrelevant value */);
}

void caller(void)
{
  char* buffer = malloc(/* some irrelevant value */);

  /* ignore returned pointer */ called(buffer);

  free(buffer); /* here Valgrind reports an error */
}

Нам нужно различать несколько случаев для realloc() здесь.

  1. realloc() возвращает NULL, так как для выполнения запроса недостаточно памяти. Прежний адрес по-прежнему действителен.
  2. realloc() возвращает тот же адрес, потому что таким образом он может удовлетворить запрос. Поскольку возвращенный и прежний адреса равны, оба действительны.
  3. realloc() возвращает новый адрес. Прежний адрес теперь недействителен.

Из них третий случай является общим и приводит к задокументированной проблеме.

Поскольку buffer в caller() не изменяется called(), просто потому, что called() не может получить к нему доступ, он по-прежнему содержит прежний адрес. Теперь, когда free() вызывается с этим недопустимым адресом, обнаруживается ошибка.

Чтобы исправить эту ошибку, caller() необходимо использовать возвращаемое значение. Правильный способTM сделать это:

void caller(void)
{
  char* buffer = malloc(/* some irrelevant value */);

  char* new_buffer = called(buffer);
  if (new_buffer != NULL) {
    buffer = new_buffer;
  } else {
    /* handle the re-allocation error, the address in buffer is still valid */
  }

  free(buffer);
}

Альтернативой является передача указателя на buffer в called() и изменение buffer корректно. Но этот тип перенаправления часто генерирует хуже читаемый код. Однако для удобства вы можете решить пойти по этому пути.

Вау, большое вам спасибо - это был такой красноречивый ответ, и он ДЕЙСТВИТЕЛЬНО ДЕЙСТВИТЕЛЬНО помог мне понять, почему realloc ведет себя так, а не иначе, и почему моя ментальная модель была ошибочной.

sueszli 11.11.2022 11:47

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