Форум программистов, компьютерный форум, киберфорум
Наши страницы

Строковые литералы в Си/Си++

Войти
Регистрация
Восстановить пароль
Рейтинг: 4.83. Голосов: 6.

Строковые литералы в Си/Си++

Запись от Evg размещена 15.02.2012 в 22:32
Обновил(-а) Evg 02.08.2016 в 20:46
Метки c++

ВНИМАНИЕ! Вопросы по существу обсуждаемого вопроса просьба задавать здесь или создать тему на форуме и кинуть на неё ссылку в блог или мне в личку.
Объясняю почему

Причин для этого несколько.

Я, как и любой другой автор, всегда могу упустить интересный момент обсуждаемой темы (что подтвердилось на практике). А потому задаваемый вопрос может закрывать пробел в статье. Ответ на конкретный вопрос, как правило, дать несложно. Сложнее его аккуратно сформулировать так, чтобы ответ являлся законченной частью статьи. Поэтому, как правило, на первых порах я ограничиваюсь конкретным ответом на конкретный вопрос, а в статью временно вставляю ссылку на пост, где был дан ответ. А когда дойдут руки, то вместо ссылки пишу нормальное пояснение. Технические возможности блога не позволяют в комментариях пользоваться широкими возможностями, доступными на форуме (то как выделение текста жирным, вставка фрагментов исходников в удобном для чтения виде и т.п.), поэтому будет удобнее, если вопрос и ответ будут опубликованы на форуме

Любая статья является изложением знаний в общем случае. У многих людей мышление устроено так, что прочтя на форуме конкретный вопрос и конкретный ответ на этот вопрос, у них появится бОльшее понимание, чем после прочтения теоретических выкладок (даже если они подкреплены конкретными примерами). Ссылки на такие обсуждения я, как правило, включаю в последний раздел статьи.

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

Исторически сложилось, что раньше (когда ещё не было блога) статьи располагались на форуме и представлены были в виде двух тем. Первая тема создавалась в специально отведённой свалке и представляла собой черновик, который со временем дорабатывался до законченной статьи. После этого статья переезжала во вторую тему в тематическом разделе. А первая тема оставалась дополнительной свалкой для замечаний и мелких вопросов по теме. Ссылку на старое местоположение данной свалки я помещаю в начале статьи. Вопросы, по возможности, прошу создавать в отдельных темах, но если вопрос действительно мелкий, то можно его задать и в указанной свалке.






  • 1. Общие сведения
  • 2. Строковой литерал на позиции инициализатора массива char'ов
  • 3. Строковой литерал внутри оператора sizeof
  • 4. Строковой литерал во всех прочих случаях
  • 5. Какие из всего этого следуют выводы
    5.1. Хранение больших массивов строк
    5.2. Когда можно, а когда нельзя модифицировать строку
    5.3. Возврат строки из функции
    5.4. Переиспользование памяти под неявные объекты для строк
    5.5. Применение оператора [] к строковому литералу
  • 6. Склеивание строковых литералов компилятором
  • 7. Ссылки на темы, где обсуждался данный вопрос






1. Общие сведения

Ещё одна конструкция языков Си и Си++, которая часто приводит в затруднение начинающих - это строковой литерал. Начинающим, как правило, трудно понять, чем принципиально отличаются две следующие конструкции:

C
char str1[] = "abc";
char *str2 = "abc";
Термин "строка" имеет очень много смыслов, с технической стороны различающихся в разных контекстах. В контексте языков типа Бэйсик или Паскаль слово "строка" имеет один смысл, в контексте C'шного заголовочного файла string.h - другой смысл, в контексте C++'ного заголовочного файла string - третий смысл, и т.д. Чтобы не путаться с прочими смыслами, я буду пользоваться техническим термином "строковой литерал", который по смыслу означает "строковая константа". Строковым литералом называется заключённая в двойные кавычки строка.

Семантика строкового литерала в языках C/C++ имеет двоякий смыл и зависит от того, в каком месте кода он (строковой литерал) встретился. И эти два различных случая отражены в вышеидущем примере.






2. Строковой литерал на позиции инициализатора массива char'ов

Первая возможность использования строкового литерала - это инициализация массива char'ов. Данная конструкция возникает только в тех случаях, когда описывается переменная, являющаяся массивом char'ов и через знак "=" написана строка, которая инициализирует данный массив. В таком случае строковой литерал следует трактовать как константный массив-инициализатор, состоящих из элементов типа char. Т.е. запись

C
char str[] = "abc";
эквивалентна записи

C
char str[] = { 'a', 'b', 'c', '\0' };
ВНИМАНИЕ!
В первом комментарии к статье мне любезно сообщили об ошибке. Поведение для Си и для Си++ немного различается. А потому нижеидущий текст (до конца раздела 2) гарантированно справедлив для языка Си, но требует небольшой доработки для языка Си++

При этом есть один очень хитрый момент, касающийся включения в этот инициализатор элемента '\0'. И этот момент иногда вводит в заблуждение даже тех, кто имеет хороший опыт программирования. Если массив задан без указания размера (т.е. с пустыми квадратными скобками), то в инициализатор включается хвостовой символ '\0', как это было указано в вышеидущем примере. Т.е., условно говоря, sizeof от инициализатора (и, соответственно, переменной типа массив) будет равен количеству символов в строке плюс единица. Однако если у массива указан размер, то хвостовой '\0' в инициализатор НЕ включается.

Таким образом, если мы напишем

C
char str[4] = "abc";
то такая запись будет эквивалентной

C
char str[4] = { 'a', 'b', 'c' };
По общим правилам языков C/C++ для инициализации массива в случаях, когда инициализатор имеет меньшее число элементов, чем инициализируемая переменная, хвост инициализатора дописывается нулями соответствующего типа. И в данном случае такую запись можно трактовать как

C
char str[4] = { 'a', 'b', 'c', '\0' };
Хитрость заключается в том, что если мы укажем размер массива равный 3, то хвостового нуля в инициализаторе уже не будет (потому что размер инициализатора совпадает с размером массива). При этом код остаётся абсолютно корректным с точки зрения языка, хотя начинает представлять собой некоторый бардак с точки зрения программиста, который слишком привык к тому, что под строкой подразумевается набор символов с хвостовым нулём.

Если мы напишем такой пример:

C
#include <stdio.h>
 
char a[3] = "abc";
char b[3] = "def";
char c[] = "ghi";
 
int
main (void)
{
  printf ("%s\n", a);
  return 0;
}
то при исполнении на многих компиляторах мы получим код, который при исполнении напечатает нам "abcdefghi". Связано это с тем, что компилятор, как правило, положит три массива "a", "b" и "c" в память подряд друг за другом и таким образом в памяти сформируется набор данных размером 10 байт: 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', '\0'. Когда мы подаём в printf указатель на массив "a", то внутри себя функция printf уже не знает ни о каких переменных и будет работать с этим указателем как со строкой: т.е. с набором символов, который заканчивается нулём.

Конструкция

C
char str[2] = "abc";
является ошибочной, потому как размер инициализатора больше, чем размер массива.

В случае с инициализацией многомерных массивов char'ов имеем всё ровно то же самое:

C
char a[3][6] = { "abc", "defg", "hijklm" };
эквивалентно

C
char a[3][6] =
{
  { 'a', 'b', 'c' },
  { 'd', 'e', 'f', 'g' },
  { 'h', 'i', 'j', 'k', 'l', 'm' }
};
а после применения общего правила заполнения нулями хвоста инициализатора эквивалентно

C
char a[3][6] =
{
  { 'a', 'b', 'c', '\0', '\0', '\0' },
  { 'd', 'e', 'f', 'g', '\0', '\0' },
  { 'h', 'i', 'j', 'k', 'l', 'm' }
};





3. Строковой литерал внутри оператора sizeof

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

C
a = sizeof ("abcdefgh");
эквивалентно

C
char __tmp_obj[] = "abcdefgh";
a = sizeof (__tmp_obj);
и будет возвращать количество символов в строке плюс единица (т.е. с учётом неявного хвостового нуля). Таким образом, здесь у нас работа идёт по тем же правилам, что и в разделе 2






4. Строковой литерал во всех прочих случаях

Во всех прочих случаях строковой литерал трактуется как НЕявно заведённый статический константный объект типа массив char'ов, инициализированный символами данного строкового литерала с включением неявного завершающего нуля, и далее взятие адреса на нулевой элемент данного объекта. Теперь всё это же самое, выраженное в виде программы:

C
"abc"
эквивалентно

C
static char const __tmp_obj[] = "abc";
&__tmp_obj[0]
и в виде более осязаемого конкретного примера:

C
1
2
3
char *str1 = "abc";
char str2[256];
strcpy (str2, "def");
эквивалентно

C
static char const __tmp_obj1[] = "abc";
static char const __tmp_obj2[] = "def";
 
char *str1 = &__tmp_obj1[0];
char str2[256];
strcpy (str2, &__tmp_obj2[0]);
С массивами тут тоже всё просто и построено на тех же самых общих принципах, только нужно правильным образом учесть строковые литералы:

C
char *s[3] = { "abc", "defg", "hijklm" };
эквивалентно

C
static char const __tmp_obj1[] = "abc";
static char const __tmp_obj2[] = "defg";
static char const __tmp_obj3[] = "hijklm";
 
char *s[3] = { &__tmp_obj1[0], &__tmp_obj2[0], &__tmp_obj3[0] };
что после раскрытия строкового литерала на позиции инициализатора эквивалентно:

C
static char const __tmp_obj1[4] = { 'a', 'b', 'c', '\0' },
static char const __tmp_obj2[5] = { 'd', 'e', 'f', 'g', '\0' },
static char const __tmp_obj3[7] = { 'h', 'i', 'j', 'k', 'l', 'm', '\0' }
 
char *s[3] = { &__tmp_obj1[0], &__tmp_obj2[0], &__tmp_obj3[0] };





5. Какие из всего этого следуют выводы

5.1. Хранение больших массивов строк

Посмотрите на коды, которые требуются для хранения массива строк, которые мы использовали в конце раздела 2 и в конце раздела 4. Образ памяти, который получаются в примере из раздела 2 (хранение через двумерный массив char'ов) будет отличаться от образа памяти, полученного в примере из раздела 4 (хранение через массив указателей). При хранении через массив указателей каждая отдельно взятая строка потребляет ровно столько памяти, сколько требуется для её хранения. А в примере с хранением через двумерный массив каждая строка (которая является элементом массива) потребляет одинаковое количество памяти, определяемое последней размерностью массива (фактически самой длинной строкой). Т.е. если мы имеем 1000 строк, 999 из которых занимают 3 байта, а одна строка занимает 500 байт, то в варианте с двумерным массивом мы будем иметь массив размером 500.000 байт, а в варианте работы через массив указателей будем иметь 999*3 + 500 байт, которые потребуются для хранения непосредственно строк, плюс 1000 указателей на строки. И таким образом, вариант хранения через указатели будет более экономным по памяти. А если у нас 1000 строк и все занимают по 500 байт, то в обоих случаях нам потребуется 500.000 байт на хранение непосредственно строк, плюс дополнительные 1000 указателей в варианте хранения через указатели (которые не нужны в варианте с двумерным массивом). Поэтому с точки зрения экономии памяти выбор между двумя вариантами осуществляется на основании сбалансированности строк: если строки равные по длине, то их эффективнее хранить в двумерном массиве. Если строки сильно различаются по длине, то эффективнее хранить их через массив указателей

5.2. Когда можно, а когда нельзя модифицировать строку

После прочтения разделов 2 и 4, как мне кажется, данный вопрос должен отпасть сам собой. Но для закрепления материала всё-таки пройдусь по этому моменту, т.к. не раз видел, что он вызывает затруднение у начинающих.

C
char str[] = "abc";
str[0] = 'q';
Этот вариант является допустимым, т.к. мы имеем обычный массив, в который сначала записали 4 элемента ('a', 'b', 'c', '\0'), а затем в нулевой элемент массива записываем другое значение

C
char *str = "abc";
str[0] = 'q';
А этот вариант является недопустимым, хотя на некоторых компиляторах он может нормально отработать. Недопустимым он является по той причине, что "str" у нас является указателем, смотрящим на константный объект, а любая запись в константный объект запрещена. Если подходить в вопросу абсолютно честно, то уже и первая строка данного примера является ошибочной, потому как в левой части присваивания мы имеем переменную типа "char*", а в правой - выражение типа "const char*". Почему компилятор (или, правильнее сказать, стандарт) допускает такие действия - я затрудняюсь ответить.

При распределении переменных в память многие современные компиляторы константные объекты складывает в отдельные сегменты памяти, которые при исполнении попадают в read-only память и запись в такие переменные будет запрещена средствами операционной системы. То же самое касается и неявных константных объектов, возникающих из строковых литералов. Так, например, при компиляции второго примера из данного раздела компилятором gcc под linux'ом мы получим слом на исполнении из-за попытки записать в read-only память. В то время как при использовании borland'овского компилятора под windows данный пример успешно работает.

В мире всё ещё используется софт, исходники которого написаны очень давно, когда ещё и стандарт Си толком не появился (напомню, что стандарт Си появился лишь через несколько лет после появления языка), когда компиляторы были слабыми, а операционные системы однозадачными. И в таком софте зачастую присутствует код, модифицирующий память, отведённую под строковой литерал в константном случае. Для таких случаев, например, у компилятора gcc есть опция -fwritable-strings, по которой строки от строковых литералов НЕ помещаются в read-only сегменты. Т.е. у gcc есть два режима работы, но по умолчанию выставлен более строгий (и более правильный) режим. Возможно, что и у упомянутого borland'овского компилятора есть два режима, но по умолчанию выставлен менее строгий режим. Но ковыряться в настройках лениво. FIXME надо бы с этим вопросом разобраться

5.3. Возврат строки из функции

FIXME написать
Демонстрирующий пример тут

5.4. Переиспользование памяти под неявные объекты для строк

Как уже говорилось выше, данный код

C
char *str1 = "abc";
char *str2 = "abc";
эквивалентен

C
static char const __tmp_obj1[] = "abc";
static char const __tmp_obj2[] = "abc";
 
char *str1 = &__tmp_obj1[0];
char *str2 = &__tmp_obj2[0];
В этом случае мы имеем два неявных объекта (к которым программист никак не может обратиться напрямую), которые являются константными, статически инициализированными и при этом содержат одинаковые значения элементов массива. Некоторые компиляторы умеют пользоваться этим свойством и экономить память, объединяя несколько одинаковых строк в одну. И таким образом пример становится эквивалентным:

C
static char const __tmp_obj[] = "abc";
 
char *str1 = &__tmp_obj[0];
char *str2 = &__tmp_obj[0];
5.5. Применение оператора [] к строковому литералу

FIXME написать
Речь идёт о выражениях типа "abc"[i]






6. Склеивание строковых литералов компилятором

FIXME написать
Имелось в виду то, что написано тут в разделе 3.3.4 по части конкатенации строковых литералов






7. Ссылки на темы, где обсуждался данный вопрос
Всего комментариев 8

Комментарии

  1. Старый комментарий
    Аватар для D.E.S.P.E.R.O.
    Описанное в пункте 2, о инициализаторе массива с указанием его размера не имеет отношения к языку С++. Это справедливо только для С, а в случае с С++ компилятор выдаст ошибку о том, что инициализатор больше инициализируемого!
    Запись от D.E.S.P.E.R.O. размещена 25.06.2013 в 15:53 D.E.S.P.E.R.O. вне форума
    Обновил(-а) D.E.S.P.E.R.O. 25.06.2013 в 15:54
  2. Старый комментарий
    Аватар для D.E.S.P.E.R.O.
    Пример с оператором printf, чисто теоретически имеет право на существование, но ни gcc ни vs на данный момент не допустят такого извращения даже в отладочном режиме!
    Запись от D.E.S.P.E.R.O. размещена 25.06.2013 в 16:20 D.E.S.P.E.R.O. вне форума
  3. Старый комментарий
    Аватар для Evg
    Спасибо. Надо будет как-то аккуратно это переписать
    Запись от Evg размещена 10.07.2013 в 15:09 Evg вне форума
  4. Старый комментарий

    Не по теме:

    то при исполнении на многих компиляторах мы получим код, который при исполнении напечатает нам "abcdefghi"


    а можем получить и abc и abcXdefXghi"где X любой символ может и не печатный
    все дело в выравнивании
    компилятор может выравнивать адреса структур на кратное степени двойки значение( для 32 обычно кратно 4)
    и все дело где объявлен массив если в глобальной памяти где память обнуляется то "хвост" будет содержать 0 что равнозначно концу строки, а если в автоматической памяти то там будет скорее всего мусор
    пример
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    char a[3]="abc";
    char b[3]="def";
    char c[]="qwe";
     
    int main()
    {
    char al[3]="abc";
    char bl[3]="def";
    char cl[]="qwe";    
    printf("%s",a);
    printf("\n----------------------\n");
    printf("%s",al);
        
     
    return 0;
    }
    но скомпилировать чтобы проверить не смог
    все мои компиляторы дают ошибку что размер массива меньше
    Запись от ValeryS размещена 28.01.2014 в 09:28 ValeryS вне форума
  5. Старый комментарий
    удалось скомпилировать под онлайн компилятор
    http://codepad.org/vYvo3ACw
    но результат прямо противополжный

    вот модифицированый код
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    #include <stdio.h>
     
    char a[3]={'a','b','c'};
    char b[3]={'d','e','f'};
    char c[]={'q','w','e','\0'};
     
    int main()
    {
        char al[3]={'a','b','c'};
        char bl[3]={'d','e','f'};
        char cl[]={'q','w','e','\0'};   
    printf("%s",a);
    printf("\n----------------------\n");
    printf("%s",al);
        
     
    return 0;
    }
    Qt и VS дают абсолютно разные результаты

    это я к тому писал что поправь размер на 4( кратно двойке) тогда результат будет похож
    C
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    
    #include <stdio.h>
     
    char a[4]={'a','b','c','1'};
    char b[4]={'d','e','f','2'};
    char c[]={'q','w','e','\0'};
     
    int main()
    {
        char al[4]={'a','b','c','1'};
        char bl[4]={'d','e','f','2'};
        char cl[]={'q','w','e','\0'};
    printf("%s",a);
    printf("\n----------------------\n");
    printf("%s",al);
     
     
    return 0;
    }
    по крайней мере в области глобальных
    локальные тоже по разному высвечиваются
    Запись от ValeryS размещена 28.01.2014 в 09:45 ValeryS вне форума
  6. Старый комментарий
    Аватар для Evg
    > это я к тому писал что поправь размер на 4( кратно двойке) тогда результат будет похож

    Не "результат будет похож", а "будет таким же с гораздо большей вероятностью". Тест-то некорректный, а потому тут нету "правильного" результата. Размер массива 4 приведёт к тому, что увеличится вероятность того, что пример на разных компиляторах выдаст один и тот же результат.

    На самом деле по честному надо не только в этом тесте заменить 3 на 4, но и во всех вышеидущих примерах, потому что везде идёт объяснение на примере строки из трёх символов. Чтобы не было незаметного перехода от трёх к четырём, из-за которого у многих возникнут непонятки на ровном месте, из-за того, что этот переход не заметили. Поэтому надо аккуратно сесть и по всей статье симметрично всё заменить. Надо будет этим заняться
    Запись от Evg размещена 30.01.2014 в 18:47 Evg вне форума
  7. Старый комментарий
    А в чем отличие
    C++
    1
    
    char *str;
    от
    C++
    1
    
    char* str;
    ?
    Запись от laby размещена 14.07.2016 в 09:04 laby вне форума
  8. Старый комментарий
    Аватар для Nameless One
    В том, что в первом случае пробел стоит перед знаком ¨asterisk¨ (¨звездочка¨), а во втором — после.
    Запись от Nameless One размещена 17.07.2016 в 11:06 Nameless One вне форума
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2017, vBulletin Solutions, Inc.
Рейтинг@Mail.ru