выделение памяти (язык C)

Вопросы написания собственного программного кода (на любых языках)

Модератор: Olej

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 17 янв 2014, 10:11

Olej писал(а):Я так понимаю (предполагаю), что динамические массивы VLA стандарт C99 вводит вместо плохо стандартизованного (между компиляторами, платформами) вызова alloca(). Что есть почти одно и то же (или совершенно одно и то же?). Только, естественно, нельзя одновременно использовать и один и другой механизм, потому что вместе, не зная друг о друге, они просто разрушат стек.
Вот очень интересный пример... 1-й - это то, что уже показывалось раньше с alloca(), а 2-й - это подобное, переписанное с VLA ... оба примера примерно приведены к похожему виду:

Код: Выделить всё

#define N 6

void test053( void ) {             // alloca() данных в блоке
   void *X[ N ];
   int i;
   void *p = &p;                   // дно кадра стека
   printf( "alloca() в блоке:\n" );
   printf( "дно стека = %p\n", p );
   for( i = 0; i < N; i ++ ) {
      X[ i ] = alloca( sizeof( short ) );
      *(short*)X[ i ] = i + 1;
      printf( "%p[%03d]->%d%s",
              X[ i ], p - (void*)X[ i ], *(short*)X[ i ],
              ( N / 2 - 1 ) == i % ( N / 2 ) ? "\n" : " ,\t" );
   }
}

Код: Выделить всё

void test055( void ) {             // динамические массивы (C99)
   short *X[ N ];
   int i;
   void *p = &p;                   // дно кадра стека
   printf( "VLA в блоке:\n" );
   printf( "дно стека = %p\n", p );
   for( i = 0; i < N; i ++ ) {
      short V[ i + 1 ];
      V[ 0 ] = i + 1;
      X[ i ] = &V[ 0 ];
      printf( "%p[%03d]->%d%s",
              X[ i ], p - (void*)X[ i ], *X[ i ],
              ( N / 2 - 1 ) == i % ( N / 2 ) ? "\n" : " ,\t" );
   }
}
Прежде всего, здесь интересно, что VLA выделяется не в функции, а просто в блоке внутри кода (для меня такая возможность была неожиданной!).

Но особенно интересно выполнение:

Код: Выделить всё

olej@notebook:~/2014_WORK/AntonG/examples.draft$ ./function 6 7
06 ---------------------------------------
alloca() в блоке:
дно стека = 0xbfe9135c
0xbfe91330[044]->1 ,	0xbfe91310[076]->2 ,	0xbfe912f0[108]->3
0xbfe912d0[140]->4 ,	0xbfe912b0[172]->5 ,	0xbfe91290[204]->6
07 ---------------------------------------
VLA в блоке:
дно стека = 0xbfe9135c
0xbfe91334[040]->1 ,	0xbfe91334[040]->2 ,	0xbfe91334[040]->3
0xbfe91334[040]->4 ,	0xbfe91334[040]->5 ,	0xbfe91334[040]->6
------------------------------------------
Всё на месте, массивы как массивы, [0]-элементы их промаркированы порядковым номером и там всё как надо лежит...
Но!
- массивы, создаваемые alloca() внутри блока for, не уничтожаются при завершении блока (выходе за блок), каждый новый массив размещается по новому адресу, освобождены они будут только по завершению функции в которой выполняются, т.е. при переразметке стекового кадра...
- а точно так же создаваемые массивы VLA (и почти там же лежащие) уничтожаются при достижении конца блока, и каждый новый массив в цикле размещается по тому же адресу, где был и 1-й массив.

Вот вам и разница!
Вложения
examples.tgz
(21.64 КБ) 295 скачиваний

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 17 янв 2014, 10:34

Olej писал(а): Всё на месте, массивы как массивы, [0]-элементы их промаркированы порядковым номером и там всё как надо лежит...
Но!
- массивы, создаваемые alloca() внутри блока for, не уничтожаются при завершении блока (выходе за блок), каждый новый массив размещается по новому адресу, освобождены они будут только по завершению функции в которой выполняются, т.е. при переразметке стекового кадра...
- а точно так же создаваемые массивы VLA (и почти там же лежащие) уничтожаются при достижении конца блока, и каждый новый массив в цикле размещается по тому же адресу, где был и 1-й массив.

Вот вам и разница!
Но совершенно ошеломляюще это выглядит, если тот же неизменно код скомпилировать на GCC а Clang:

Код: Выделить всё

olej@notebook:~/2014_WORK/AntonG/examples.draft$ ./functionl 6 7
06 ---------------------------------------
alloca() в блоке:
дно стека = 0xbfdc3dc4
0xbfdc3dc2[002]->1 ,	0xbfdc3dc0[004]->2 ,	0xbfdc3dbe[006]->3
0xbfdc3dbc[008]->4 ,	0xbfdc3dba[010]->5 ,	0xbfdc3db8[012]->6
07 ---------------------------------------
VLA в блоке:
дно стека = 0xbfdc3dc0
0xbfdc3dbe[002]->1 ,	0xbfdc3dba[006]->2 ,	0xbfdc3db4[012]->3
0xbfdc3dac[020]->4 ,	0xbfdc3da2[030]->5 ,	0xbfdc3d96[042]->6
------------------------------------------
- такое впечатление, что локальные массивы теперь вообще выделяются не в стеке (а где?), а в стеке лежат только какие-то указатели на эти массивы (потому, что сами значения элементов массивов - совершенно корректны)...
- но и это ещё не всё! при alloca() эти адреса выравниваются на 2 байта, а при VLA - на 4 байта...
- но я ещё никогда (специально на несовместимости примеры искал!!!) не видел такого разительного расхождения результатов, когда один и тот же код компилируется GCC и Clang.

P.S. Совершенно не исключено, что эти эффекты вызваны ошибками в записи кода - всё это делалось наскоро ... особенно в выражениях printf() - что там выводится? Но в любом случае, почему даже в таком случае так по-разному толкуются одни и те же выражения?

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 17 янв 2014, 11:24

Ещё одно очень интересное место в выделении памяти в C - это неявное выделение выполнением strdup(), клонированием строки. Меня всегда удивляло, почему в литературе так мало этому уделяют внимания, особенно при том, что строки - главный тип данных в программах?

Особенно интересно это работает, когда из функции нужно возвратить строку, которая там каким-то образом формируется... Это совсем не такая тривиальная операция. Пример:

Код: Выделить всё

char* strrep( const char* s, int r ) {
   int n = strlen( s ) * r;
   char buf[ n + 1 ];
   strcpy( buf, s );
   while( --r > 0 )
      strcat( buf, s );
   return strdup( buf );
}

void test043( void ) {
   int i;
   printf( "возврат дубликата строки:\n" );
   for( i = 0; i < 3; i++ ) {
      char *s = strrep( "строка! ", i + 1 );
      printf( "%s\n", s );
      free( s );
   }
}
Функция strrep() возвращает строку, повторённую r раз.
Вот как это выглядит:

Код: Выделить всё

olej@notebook:~/2014_WORK/AntonG/examples.draft$ ./function 4 
04 ---------------------------------------
возврат дубликата строки:
строка! 
строка! строка! 
строка! строка! строка! 
------------------------------------------
Неприятность такого решения состоит только в том, что нужно не забыть free() во внешней вызывающей функции... Ну и ещё то, что при прямом функциональном использовании такого вызова, по типу:

Код: Выделить всё

      printf( "%s\n", strrep( "строка! ", i + 1 ) );
- это гарантировано "потерянный" объект, с отсутствием доступа как по имени, так и по указателю... имеем утечку памяти.

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 17 янв 2014, 11:34

Olej писал(а): Особенно интересно это работает, когда из функции нужно возвратить строку, которая там каким-то образом формируется... Это совсем не такая тривиальная операция.
Другой способ такое сделать - статическая строка-буфер. Как-то так:

Код: Выделить всё

char* strplus( const char* s1, const char* s2 ) {
   static char buf[ 160 ];
   strcpy( buf, s1 );
   strcat( buf, " " );
   strcat( buf, s2 );
   return buf;
}

void test045( void ) {
   printf( "возврат статической строки:\n" );
   printf( "%s\n", strplus( "первая", "строка" ) );
   printf( "%s + %s\n", strplus( "первая", "строка" ), strplus( "вторая", "строка" ) );
}
Но тут есть тоже одно смешное место: при нескольких вызовах (второй printf()) результат будет зависеть от порядка вычислений, заданного компилятором, и это может быть совсем не то, что вы хотите:

Код: Выделить всё

olej@notebook:~/2014_WORK/AntonG/examples.draft$ make function
gcc -Wall -lm -O 4.c -o function
olej@notebook:~/2014_WORK/AntonG/examples.draft$ ./function 5
05 ---------------------------------------
возврат статической строки:
первая строка
первая строка + первая строка
------------------------------------------

Смешно? :mrgreen:

Ещё смешнее станет, если это скомпилировать Clang:

Код: Выделить всё

olej@notebook:~/2014_WORK/AntonG/examples.draft$ make functionl
clang -xc -Wall -lm -O 4.c -o functionl
olej@notebook:~/2014_WORK/AntonG/examples.draft$ ./functionl 5
05 ---------------------------------------
возврат статической строки:
первая строка
вторая строка + вторая строка
------------------------------------------
Т.е. в printf() GCC вычисляет аргументы справа налево, а Clang это делает наоборот - слева направо. :-o

P.S. Обновлённый вариант архива примеров!
Вложения
examples.tgz
(22.02 КБ) 309 скачиваний

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: выделение памяти (язык C)

Непрочитанное сообщение Olej » 17 янв 2014, 19:51

Olej писал(а): P.S. Совершенно не исключено, что эти эффекты вызваны ошибками в записи кода - всё это делалось наскоро ... особенно в выражениях printf() - что там выводится? Но в любом случае, почему даже в таком случае так по-разному толкуются одни и те же выражения?
Конечно же ошибка! :evil: Поскольку в каком-то старом варианте мне в alloca() интересовал только 1 первый элемент массива, то только он один и размещался. GCC при этом делал какие-то дикие выравнивания, а Clan тупо укладывал в стек подряд :shock:

Теперь оба варианта, да ещё приведенные к максимально близкому виду (близнецы-братья) для сравнения, будут выглядеть так:

Код: Выделить всё

void test053( void ) {             // alloca() данных в блоке
   short *X[ N ];
   int i;
   void *p = &p;                   // дно кадра стека
   printf( "alloca() в блоке:\n" );
   printf( "дно стека = %p\n", p );
   for( i = 0; i < N; i ++ ) {
      X[ i ] = (short*)alloca( sizeof( short ) * ( i + 1 ) );
      *(short*)X[ i ] = i + 1;
      printf( "%p[%03d]->%d%s",
              X[ i ], p - (void*)X[ i ], *X[ i ],
              ( N / 2 - 1 ) == i % ( N / 2 ) ? "\n" : " ,\t" );
   }
}

Код: Выделить всё

void test055( void ) {             // динамические массивы VLA (C99) в блоке
   short *X[ N ];
   int i;
   void *p = &p;                   // дно кадра стека
   printf( "VLA в блоке:\n" );
   printf( "дно стека = %p\n", p );
   for( i = 0; i < N; i ++ ) {
      short V[ i + 1 ];
      V[ 0 ] = i + 1;
      X[ i ] = &V[ 0 ];
      printf( "%p[%03d]->%d%s",
              X[ i ], p - (void*)X[ i ], *X[ i ],
              ( N / 2 - 1 ) == i % ( N / 2 ) ? "\n" : " ,\t" );
   }
}
Выполнение (в скобках [] - смещение начала массива вглубь от дна кадра стека)...

- GCC:

Код: Выделить всё

olej@notebook:~/2014_WORK/AntonG/examples.draft$ ./function 8 9
08 ---------------------------------------
alloca() в блоке:
дно стека = 0xbfde3c9c
0xbfde3c70[044]->1 ,	0xbfde3c50[076]->2 ,	0xbfde3c30[108]->3
0xbfde3c10[140]->4 ,	0xbfde3bf0[172]->5 ,	0xbfde3bd0[204]->6
09 ---------------------------------------
VLA в блоке:
дно стека = 0xbfde3c9c
0xbfde3c74[040]->1 ,	0xbfde3c74[040]->2 ,	0xbfde3c74[040]->3
0xbfde3c74[040]->4 ,	0xbfde3c74[040]->5 ,	0xbfde3c74[040]->6
------------------------------------------
-Clang:

Код: Выделить всё

olej@notebook:~/2014_WORK/AntonG/examples.draft$ ./functionl 8 9
08 ---------------------------------------
alloca() в блоке:
дно стека = 0xbfecee94
0xbfecee92[002]->1 ,	0xbfecee8e[006]->2 ,	0xbfecee88[012]->3
0xbfecee80[020]->4 ,	0xbfecee76[030]->5 ,	0xbfecee6a[042]->6
09 ---------------------------------------
VLA в блоке:
дно стека = 0xbfecee90
0xbfecee8e[002]->1 ,	0xbfecee8a[006]->2 ,	0xbfecee84[012]->3
0xbfecee7c[020]->4 ,	0xbfecee72[030]->5 ,	0xbfecee66[042]->6
------------------------------------------
Теперь цифры правильные. Хорошо видно, что:
- GCC укладывает очередной VLA нового размера - на место старого (затирает)
- Clang создаёт новое размещение вслед предыдущему, точно так, как это делает alloca()

Аватара пользователя
Olej
Писатель
Сообщения: 21338
Зарегистрирован: 24 сен 2011, 14:22
Откуда: Харьков
Контактная информация:

Re: язык C в Linux: вопросы начального уровня

Непрочитанное сообщение Olej » 20 янв 2014, 21:31

Olej писал(а): Вот как-то так:

Код: Выделить всё

typedef struct vararr {
   int n, data[ 0 ];
} vararr_t;
Кстати (и это важно достаточно, чтобы об этом написать), то что показано - это расширение GCC, существующее там давно ... ну, года с 2002 это точно, называемое: массивы нулевой длины (внутри структур).

Но стандарт C99 определяет подобное расширение, называя его: массив с переменными границами. Т.е. это уже становится стандартом языка.
При этом структура vararr в показанном раньше примере может быть записана (без каких либо изменений в остальном коде) так:

Код: Выделить всё

typedef struct vararr {
   int n, data[]];
} vararr_t;
Это совершенно динамические структуры (или массивы) произвольного размера, но размещаться они должны, конечно, malloc().

Ответить

Вернуться в «Программирование»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 9 гостей