Язык Си. Массивы. Строки

Строки и массивы в языке Си.

Начнем с массивов!
Массив это последовательность однотипных данных, расположенных друг за другом.
Чтобы указать компилятору что нам нужен массив используйте следующую запись:
  // массив из 10 элементов типа int
  int    i_arr[10];     
  
  // массив из 256 элементов типа char
  char   c_buffer[256];
  
  // массив из 360 элементов типа double
  double d_acos[360];
Индексация элементов массива начинается с 0, то есть, чтобы обратиться к первому элементу мы пишем i_arr[0]. Компилятор не проверяет допустимость индексов, выход за пределы массива. Поэтому когда вы обращаетесь к несуществующему элементу поведение программы не определено, поэтому границы нужно контролировать самому, или же написать функции которые будут это делать, и обращаться к элементам массива с помощью этих функций.
С инициализацией обычных переменных вы уже знакомы, массив инициализируется почти так же как и обычные переменные, ключевое слово почти :-) . Мы знаем что массив это последовательность элементов, значит и при инициализизации нам нужно указать значения всех элементов, но это не обязательно, можно инициализировать и нужные нам элементы.
Правила при инициализации массивов, смотрите в примере ниже.
  // массив не проинициализирован
  // значения элементов буду случайными
  int i_a[3];

  // массив проинициализирован
  // все три элемента имеют значения
  // [0] = 1, [1] = 2, [2] = 4
  int i_b[3] = {1, 2, 4};

  // массив проинициализирован
  // указан только первый элемент
  // [0] = 1
  // если же дальше не указаны значения
  // то оставшиеся элементы массива
  // инициализируются значением 0
  int i_c[3] = {1};

  // размер массива будет вычислен
  // при компиляции программы, и будет равен двум
  // так как есть список инициализации
  // [0] = 1, [1] = 2
  int i_d[] = {1,2};

Также, начиная со стандарта С99 есть такая штука как назначенные инициализаторы, удобный механизм, для того чтобы проинициализировать нужный элемент. Следуя вышеописанным правилам, если хотя бы один элемент проинициализирован, то все остальные элементы, которые не указаны, будут проинициализированы значением 0.
  // массив проинициализирован
  // с помощью назначенного инициализатора
  // [0] = 0, [1] = 0, [2] = 10
  int i_e[3] = {[2] = 10};

  // массив проинициализирован
  // с помощью назначенных инициализаторов
  // [0] = 10, [1] = 10, [2] = 10, [3] = 0, [4] = 1, [5] = 1
  int i_f[6] = {[0 ... 2] = 10, [4 ... 5] = 1};

  // [0] = 1, [1] = 2, [2] = 0, [3] = 0, [4] = 9
  int i_g[] = {1, 2, [4] = 9};

  // [0] = 1, [1] = 0, [2] = 0, [3] = 0, [4] = 9, [5] = 10, [6] = 11
  int i_h[] = {1, [4] = 9, 10, 11};

Строки.
Строка в языке Си это массив элементов типа char с завершающим символом '\0' или значением 0. Сначала рассмотрим в примере ниже, способы инициализации строк.
  char str_1[100] = "Hello str1";
  char str_2[]    = "Hello str2";
  char *str_3     = "Hello str3";
Во втором и третьем случае, компилятор автоматически вычисляет размер строки, и выделяет память. В первом случае мы указали что размер строки равен 100, но по факту это 99 потому что последним элементом будет завершающий символ '\0'. 
Во всех трех случаях объявления и инициализации строки, мы использовали строковые литералы("Hello str1", "Hello str2", "Hello str3"), завершающий символ 0 или '\0' будет добавлен в конец автоматически(для случая 2 и 3), ведь из правила массивов(для первого случая) мы знаем, что если был проинициализирован хоть один элемент массива, то все остальные будут проинициализированны значением 0. Во втором и третьем случае, размер массива не известен, но компилятор сам его вычислит и выделит память на 1 байт больше, чтобы в последний записать '\0'.
Если вы хотите использовать строковый буфер то используйте первый пример, если же вам просто нужна строковая константа то второй или третий вариант, но в третьем варианте запрещено изменение данных, только чтение. 
Также, вторая и третья запись отличаются тем что указатель вы можете увеличивать только при третьем способе.
#include <string.h>
#include <stdio.h>
int main()
{
  char str_2[] = "Hello str2";
  char *str_3  = "Hello str3";

  int str_length = 0;

  str_length = strlen(&str_2[0]);
  for(int i = 0; i < str_length; ++i)
    printf("%c", *(str_2++));   // ОШИБКА доступ только так - str_2[i]
  printf("\n");

  str_length = strlen(str_3);
  for(int i = 0; i < str_length; ++i)
    printf("%c", *(str_3++));
  printf("\n");

  return 0;
}

Как уже было сказано, при попытке обратиться с элементу и перезаписать его, для строки str_3 поведение неопределено. Дело в том что при таком способе мы не знаем каким способом будет храниться строка. Как правило третий вариант записывается так:
const char *str_3  = "Hello str3";
Но мы записали без слова const. Давайте разберем случай такой записи но без const. Посмотрите на пример ниже:
  char *str = "Hello!";
  printf("Hello!\n");
  // ...
  printf("Hello! how are you?\n");
Слово "Hello!" здесь в трех участках кода, компилятор может провести опитимизацию, и хранить "Hello!" в одном месте, а код будет ссылаться на один и тот же участок памяти, соответственно если мы изменим какуе-то буковку(элемент массива), это может повлиять на другие части программы. Поэтому подобный способ создания и инициализации строки НАСТОЯТЕЛЬНО РЕКОМЕНДУЮ испольковать только со словом const, потому что в таком случае при попытке изменить элемент строки, компилятор выдаст ошибку, в обратном случае ваша программа может упасть.
  const char *str = "Hello!";
  str[0] = 0;  // логическая ошибка, компилятор ругается

  char *str = "Hello!";
  str[0] = 0;  // логическая ошибка
  // компилятор не ругается, но поведение программы
  // не определено
  // у меня(Ubuntu 64x - GCC 6.3.0 20170406) 
  // программа аварийно завершается



Возможны дополнения!

Комментарии