Если вы пришли в мир C++ из других миров со статической типизацией, то первое, что необходимо себе уяснить, что массивы в C++ — это не просто тип, это семейство типов. В этой статье постараемся наиболее полно охватить понятие си-массивов. И для большей наглядности рассмотрим работу с массивами на примере c-style строк, которые представляют собой простые символьные массивы.
Поскольку это статья рассчитана больше на начинающих, то начнем с самого начала, а именно с объявления и инициализации их:
Объявление:
Строки в си стиле — они же массивы типа char — можно объявить разными способами.
Способ 1:
Явно задать размерность:
char a2[7] = "a1";
— таким образом мы объявили и инициализировали массив размерностью 7 байт; первые три байта будут заполнены заданными в коде программе символами
'a'
и '1'
и символом конца с-строки '\0'
. И у нас остается про запас 4 символа с неинициализированными значениями, которые могут иметь любые значения*.
Способ 2:
Мы можем не задавать размер массива явно:
char a1[] = "a1234";
— таким образом автоматически будет создан массив размерностью 6 байт, заполненный 5-ю заданными символами + 1 символом конца строки.
Лирическое отступление:
В обоих случаях для строк будет выделена память в куче, которая будет аннулирована после того, как функция, в которой они объявлены, не завершит свою работу. Для иллюстрации вышесказанного рассмотрим следующий код:
#include <iostream> using namespace std; void ViewArray(char* arr){ cout<<endl<< "ViewArray:" << endl; int i=0; while(arr[i]!='\0'){ cout << arr[i++] << endl ; } cout << (int)*(arr+i) << endl ; cout<< endl; } char* CreateArray(){ char a1[7] = "a11111"; cout << "Created:" << &a1; return (char*)&a1; } int main(int argc, char** argv) { char a2[7] = "a11111"; ViewArray(a2); char* c= CreateArray(); ViewArray(c); }
Здесь мы объявили функцию ViewArray, которая посимвольно выводит в консоль содержание любой си-строки, переданной ей в качестве параметра. Обращу ваше внимание, что в C++ нельзя просто так передать значение голого массива по значению в функцию, но об этом чуть позже.
Помимо ViewArray мы так же объявили функцию CreateArray, которая инициализирует строку-массив `a11111` и возвращает указатель на него.
В функции main мы так же создаем строку-массив `a11111`, а так же записываем результат выполнения CreateArray в переменную c
и выводим их обе на экран через ViewArray.
—
И как видите, получаем несколько странный результат. Дело в том, что после завершения выполнения CreateArray память, выделенная на стэке, уничтожается перестает быть валидной и может быть перезаписана другими значениями, а может — и нет. В C/C++ это называется UB (undefined behavior) — неопределенное поведение.
Как же это преодолеть?
Чтобы вернуть массив из функции, необходимо выделить для него память в куче. Для этого перепишем нашу функцию CreateArray таким образом:
char* CreateArray(){ char *a1 = new char[7]{'a','1','1','1','1','1','\0'}; return a1; }
Здесь внутри CreateArray мы выделяем в куче память размером 7 байт и посимвольно заполняем ее. Обратите внимание на последний элемент a1[6]='\0';
— это символ признака конца строки. В случае такой инициализации его следует задать явно. На выходе мы получим точно такую же строку, как и при инициализации a11111
.
К сожалению, в C++ нет способа инициализировать память в куче с си-строкой через обычную строку, как на стеке. Согласитесь, что такая посимвольная инициализация массива все-таки менее удобна, нежели стековая:
char a1[] = "a11111";
Но другого способа нет: если вы хотите заполнить строку в куче без перечисления каждого символа в отдельной кавычке, можно, например, скопировать си-строку со стека и вернуть ее:
char a2[7] = "a11111"; char *a1 = new char[7]; return (char*)memcpy (a1, a2, sizeof(a2));
Хотя этот код будет несколько избыточен, он позволяет работать с обычной строкой для заполнения си-строк в куче.
Думаю, с undefined behavior на стеке мы разобрались. Это было лирическое отступление, и мы переходим к следующему способу:
Способ 3:
char* a2 = "a11111";
На данный момент такой способ не поддерживается стандартом. Но мы не можем его пропустить, поскольку именно его, как правило, используют новички, не понимая, что происходит после его компиляции. Тем более, что их код в большинстве случаев не работает, как нужно.
В целом, я бы не сказал, что этот подход неправильный, он скорее нерекомендуемый. Но если вы знаете что делаете, то… Дело в том, что undefined behavior для C++ в целом, может быть вполне себе defined для конкретного компилятора под конкретную платформу. И вышеприведенная строчка кода будет скомпилирована большинством компиляторов, которые мне известны, но, но как правило, с предупреждениями.
Если вникать в подробности, несмотря на схожее поведение с предыдущими способами инициализации, этот работает совсем не так: для большинства известных компиляторов строка «a11111» станет константой компиляции и будет загружена в память вместе с образом программы. А указатель `char* a2` будет инициализирован адресом хранения этой строки. И казалось бы, все здорово: мы имеем валидную си-строку, точнее указатель на нее. Но противоречие заключается в том, что эту строку нельзя изменить и если вы попытаетесь это сделать, С++ не предупредит вас при компиляции — вы получите ошибку о попытке записи в защищенную память уже на этапе выполнения. И именно поэтому делать так не рекомендуется.
Как правильно?
По сути такая запись эквивалентна:
const char *c= "a11111";
В отличие от первой последняя проверяет, чтобы не было попыток записи по адресу через константный указатель на этапе компиляции, и следовательно способствует более быстрому устранению ошибок и предотвращению их попаданию в продакшн. Если хотите проверить:
const char *c= "a11111"; c[0]='b';
не откомпилируется и вам выдаст ошибку о попытке записи в readonly-память. Конструкция const char*
говорит о том, что указатель указывает на readonly память, которую нельзя менять в процессе выполнения программы.
Передача массивов в качестве аргументов
Существует несколько способов передать массив в качестве аргумента в функцию. Выше мы использовали для этого указатель. Но это не единственный способ. Рассмотрим, как у нас есть варианты:
void ViewArray(char arr[]);
void ViewArray(char *arr);
void ViewArray(char (&arr)[7]);
Первые два способа практически идентичны. В обоих случаях в функцию передается указатель, несмотря на то, что в первом объявлении мы указали массив в качестве аргумента. Даже если мы вставим первой строчкой проверку typeid для первого объявления ViewArray, на выходе получим всего лишь указатель. Здесь, я думаю, стоит коснуться темы про различие указателей и массивов. Дело в том, что новички из-за неявного преобразования массивов в указатели думают, что это одно и то же.
Массивы и указатели
Это очень важная и непростая тема для новичков. Для начала запустим небольшой код:
#include <iostream> #include <typeinfo> using namespace std; int main(int argc, char** argv) { char b2[] = "b2"; char *b1 = b2; cout << typeid(b2).name() << endl; cout << typeid(b1).name() << endl; }
И мы увидим, что b1 и b2 — разные типы. b1
— указатель, Pc
, а b2
— это массив, A3_c
. 3
в названии обозначает размер массива, c
— тип char
. Но тем не менее, передав любой из них в ViewArray, мы получим одинаковое поведение. Потому что при передаче массива в качестве аргумента он всегда преобразуется в указатель на первый его элемент. Это вызывает путаницу. Но не только это. Массив — это уникальный тип данных в C++. Он преобразуется к указателю на первый свой элемент почти при любых операциях с ним. Для иллюстрации этого изменим код выше на:
char b2[] = "b2"; char *b1 = b2; cout << typeid(b2+0).name() << endl; cout << typeid(b1).name() << endl;
И запустим. В итоге мы получим два указателя. Что? Всего лишь добавили нуль к массиву и получили указатель на его первый элемент.
Среди операций, которые неявно преобразуют массив к указателю на 1й элемент, можно отметить
- арифметические операторы сложения и вычитания
+
,-
, - Операторы разыменования
[]
, * - Операторы сравнения:
==
, <, > , - Передача в качестве аргумента в функцию
Есть и исключения — проще запомнить исключения — например, оператор взятия адреса & вернет указатель не на первый элемент массива и, как ожидалось бы, не на указатель на указатель на 1-й элемент массива, а на весь массив:
char (*b3)[3] = &b2;
Попытка же сделать char* b3 = &b2
приведет вас к ошибке. Кроме того, отличать массив от указателя умеют такие функции, как sizeof()
и typeid()
.
Но есть и запретные операции, которые не преобразуют массив к указателю неявно и не работают с массивом напрямую — это возврат из функции и конструктор копирования (оператор =
). Это делает массивы в С++ особым типом данных.
На этом маленький экскурс по различию указателей от массивов закончим и если есть желание углубиться в эту тему, рекомендую почитать вот эту статью с комментариями к ней. Так же буду рад комментариям и замечаниям по данному вопросу к этой статье. А теперь вернемся к третьему объявлению функции: