Почему переменным, объявленным с одинаковыми именами в разных областях, назначаются одинаковые адреса памяти? - PullRequest
6 голосов
/ 08 мая 2019

Я знаю, что объявление переменной char[] в цикле while ограничено, увидев этот пост: Переопределение переменных в C .

Проходя учебник по созданию простого веб-сервера в C, я обнаружил, что мне нужно вручную очистить память, назначенную responseData в приведенном ниже примере, в противном случае содержимое index.html просто непрерывно добавляется к ответу и ответ содержит дублированное содержимое из index.html:

while (1)
{
  int clientSocket = accept(serverSocket, NULL, NULL);
  char httpResponse[8000] = "HTTP/1.1 200 OK\r\n\n";
  FILE *htmlData = fopen("index.html", "r");
  char line[100];
  char responseData[8000];
  while(fgets(line, 100, htmlData) != 0)
  {
      strcat(responseData, line);
  }
  strcat(httpResponse, responseData);
  send(clientSocket, httpResponse, sizeof(httpResponse), 0);
  close(clientSocket);
}

Исправить по:

while (1)
{
  ...
  char responseData[8000];
  memset(responseData, 0, strlen(responseData));
  ...
}

Исходя из JavaScript, это было удивительно. Зачем мне объявлять переменную и иметь доступ к содержимому памяти переменной, объявленной в другой области с тем же именем? Почему бы C просто не сбросить эту память за кулисами?

Также ... Почему переменным с одинаковым именем, объявленным в разных областях, назначаются одинаковые адреса памяти?

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

Ответы [ 3 ]

6 голосов
/ 08 мая 2019

Не совсем правильно.Вам не нужно очищать весь массив redponseData - достаточно просто очистить его первый байт:

 responseData[0] = 0;

Как отмечает Габриэль Пеллегрино в комментарии , более идиоматическое выражение

 responseData[0] = '\0';

Он явно определяет символ через его кодовую точку с нулевым значением, тогда как первый использует int постоянный ноль.В обоих случаях правый аргумент имеет тип int, который неявно преобразуется (усекается) в тип char для присваивания.(Пункт зафиксировал thx в комментарии pmg .)

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

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

Используйте sizeof всякий раз, когда вы memset массиве!

memset(responseData, 0, sizeof(responseData));

РЕДАКТИРОВАТЬ

Выше я пытался объяснить, как исправить проблему с вашим кодом, но я не ответил на ваши вопросы.Вот они:

  1. Почему переменным (...) в разных областях назначаются одинаковые адреса памяти?

В отношении выполнениякаждая итерация цикла while(1) { ... } действительно создает новую область видимости.Однако каждая область видимости завершается до создания новой области, поэтому компилятор резервирует соответствующий блок памяти в стеке, и цикл повторно использует его в каждой итерации.Это также упрощает скомпилированный код: каждая итерация выполняется точно таким же кодом, который просто переходит с конца на начало.Все инструкции в цикле, которые обращаются к локальным переменным, используют одну и ту же адресацию (относительно стека) в каждой итерации.Таким образом, каждая переменная в следующей итерации имеет точно такое же место в памяти, как и во всех предыдущих итерациях.

Я обнаружил, что мне нужно вручную очистить память

Да, автоматические переменные, расположенные в стеке, не инициализированы в Cпо умолчанию.Нам всегда нужно явно присваивать начальное значение, прежде чем мы его используем - в противном случае значение не определено и может быть неправильным (например, переменная с плавающей запятой может отображаться не как число, массив символов может показаться не завершенным, *Переменная 1062 * может иметь значение вне определения enum, переменная-указатель может не указывать на допустимое, доступное местоположение и т. Д.).

в противном случае содержимое (...) просто непрерывно добавляется

На этот вопрос был дан ответ выше.

Исходя из JavaScript, это было удивительно

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

Кроме того, рассмотрите два последовательных цикла:

void test()
{
    int i;

    for (i=0; i<5; i++) {
        char buf1[10];
        sprintf(buf1, "%d", i);
    }

    for (i=0; i<1; i++) {
        char buf2[10];
        printf("%s\n", buf2);
    }
}

Первыйодин печатает однозначное символьное представление пяти чисел в массив символов, перезаписывая его каждый раз - следовательно, последнее значение buf1[] (в виде строки) равно "4".

Какой выход вы ожидаете от второго цикла? Вообще говоря, мы не можем знать, что будет содержать buf2[], и printf -ing вызывает UB. Однако мы можем предположить, что один и тот же набор переменных (а именно один массив символов из 10 элементов) из обеих непересекающихся областей будет распределен одинаково в одной и той же части стека. Если это так, мы получим цифру 4 в качестве вывода из (формально неинициализированного) массива.

Этот результат зависит от конструкции компилятора и должен рассматриваться как совпадение. Не надейтесь на это, так как это UB!

  1. Почему бы C просто не сбросить память за кулисами?

Потому что это не сказано. Язык был создан для компиляции в эффективный, компактный код. Это делает как можно меньше «за кадром». Среди прочих вещей, которые он не делает, не инициализирует автоматические переменные, если это не указано. Это означает, что вам нужно добавить явный инициализатор в объявление локальной переменной или добавить инструкцию инициализации (например, присваивание) перед первым использованием. (Это не относится к глобальным переменным области модуля; по умолчанию они инициализируются нулями.)

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

6 голосов
/ 08 мая 2019

С этой строкой:

char responseData[8000];

Вы говорите своему компилятору: Эй, большой C, дай мне кусок размером 8000 байт и назови его responseData.

Во время выполнения, если вы не укажете, никто никогда не очистит и не даст вам «совершенно новый» кусок памяти. Это означает, что блок 8000 байтов, который вы получаете в каждом отдельном выполнении, может содержать все возможные перестановки битов в этих 8000 байтах. Нечто необычное, что может случиться, заключается в том, что при каждом выполнении вы получаете одну и ту же область памяти и, таким образом, те же самые биты в этих 8000 байтах, которые ваш большой C дал вам в первый раз. Так что, если вы не чистите, у вас создается впечатление, что вы используете одну и ту же переменную, но это не так! Вы просто используете ту же (никогда не очищенную) область памяти.

Я бы добавил, что в обязанности программиста входит очистка, при необходимости, памяти, которую вы выделяете, динамическим или статическим способом.

3 голосов
/ 08 мая 2019

Зачем мне объявлять переменную и иметь доступ к содержимому памяти переменной, объявленной в другой области с тем же именем?Почему бы C просто не сбросить эту память за кулисами?

Объекты с auto продолжительностью хранения (т. Е. Переменные области блока) не инициализируются автоматически - их начальное содержимое неопределенное.Помните, что C - продукт начала 1970-х годов, и он ошибается в сторону скорости выполнения по сравнению с удобством.Философия Си заключается в том, что программист лучше всех знает, нужно ли что-то инициализировать до известного значения или нет, и достаточно умен, чтобы делать это самостоятельно, если это необходимо.

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

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

void bletch( void )
{
  if ( some_condition )
  {
    int foo = some_function();
    printf( "%d\n", foo );
  } 
  else
  {
    int bar = some_other_function();
    printf( "%d\n", bar );
  }

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

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

char responseData[8000] = {0}; 


As MMуказывает в комментарии, это не относится к массивам переменной длины (и, возможно, к другим переменно измененным типам) - пространство для этих равно , отведено по мере необходимости, хотя откуда берется это пространство, нетопределяется определением языка.Однако для всех остальных типов обычной практикой является выделение всего необходимого пространства для входа в функцию.
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...