Программирование на языке Си: Использование массивов данных в Си. Указатели.

октября 31, 2011  |  Published in Си и C++  |  9 Comments

gnu-cОбъявление массива в Си
Массив (Array) относится к вторичным типам данных. Массив в Си представляет собой коллекция явно определенного размера элементов определенного типа. то есть в отличие от массивов в Ruby массивы в Си являются однотипными (хранят данные только одного типа) и имеют заранее определенную длину (размер).

В Си массивы можно грубо разделить на 2 типа: массив чисел и массив символов. Разумеется, такое деление абсолютно условное ведь символы — это также целые числа. Массивы символов также имеют несколько иной синтаксис. Ниже приведены примеры объявления массивов:

 
 
 
 

int arr[100];
int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
char ch[]  = {'R','u','b','y','D','e','v','.','r','u'};
char ch2[] = "RubyDev.ru";

В первом случае мы объявляем массив целых чисел (4 байта на число) размером в 100 элементов. Точнее мы резервируем память для хранения 100 элементов типа int.

Во втором случае мы определяем массив из 10 целочисленных элементов и сразу же присваиваем элементам массива значения.

В третьем случае мы определяем массив символов. В Си нету строк, но есть массивы символов, которые заменяют строки.

В последнем случае мы также объявляем массив символов с помощью специального — более лаконичного синтаксиса. Массивы ch и ch2 практически идентичны, но есть одно отличие. Когда для создания массива мы используем синтаксис со строковой константой, то в конец массива символов автоматически добавляется символ \0, при использовании стандартного синтаксиса обявления массива мы должны самостоятельно добавлять \0 в качестве последнего элемента массива символов. Символ \0 (null) используется для идентификации конца строки. О страках мы поговорим более подробно в отдельной статье.

Обращение к элементам массива в Си

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

#include <stdio.h>

int main() {
  int arr[100];
  int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  char ch[]  = {'R','u','b','y','D','e','v','.','r','u'} ;
  char ch2[] = "RubyDev.ru";

  printf("%d\n", arr[1]);
  printf("%c\n", ch[0]);
}

Код printf(«%d\n», a[1]); напечатает 2, а не 1 потому, что индексация массивов начинается с 0 и лишнее подтверждение тому строка printf(«%c\n», ch[0]);, которая напечатает символ «R» — нулевой элемент массива ch.

В общем случае объявление массива имеет следующий синтаксис:

тип_данных имя_переменной[<количество_элементов>] = <список, элементов, массива>

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

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

Указатели в Си
Типы данных необходимы для того, чтобы мочь выделить кусок памяти определенного размера для хранения данных и определения того, что это за данные ибо без явного определения непонятно является ли набор нулей и единиц числом, символом или чем-нибудь еще. В этом случае переменная является ссылкой на фрагмент памяти определенного размера и типа, например, int переменная ссылается на определенную область памяти объемом 4 байта, в которой хранится целое число, а char переменная ссылается на область памяти объемом 1 байт в которой хранится символ (код символа).

Чтобы получить адрес на который ссылается переменная мы используем специальный оператор & — оператор адреса (address operator), пример:

#include <stdio.h>

int main() {
  int arr[100];
  int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
  char ch[]  = {'R','u','b','y','D','e','v','.','r','u'} ;
  char ch2[] = "RubyDev.ru";
  int num = 100500;

  printf("%p\n", &arr);
}

Строка printf(«%p\n», &arr); напечатает 0xbfbbe068. 0xbfbbe068 — шестнадцатеричное представление адреса памяти где хранится число 100500.

Указатели — это переменные специального типа, которые хранят не обыкновенные значения, но их адреса в памяти.

#include <stdio.h>

int main() {
  int a, b;

  b = a = 10;

  printf("A: %p\n", &a);
  printf("B: %p\n", &b);
}

$ ./program
A: 0xbfe32008
B: 0xbfe3200c

В примере выше мы присваиваем переменным a и b одинаковое значение — число 10, но переменные a и b ссылаются на две разные области памяти, то есть мы сохраняем в памяти число 10 два раза. Если мы изменим значение переменной b, то оно это не отразится на переменной a и наоборот. Это отличается от того, как мы работаем с переменными в Ruby, где переменные — это ссылки на объекты хранимые в памяти, и при присваивании в стиле a = b = 10 мы получаем один объект — число 10 и две ссылки на него.

Если нам необходима будет еще одна ссылка на ту же область памяти, на которую ссылается переменная a, то мы можем воспользоваться указателем. Пример:

#include <stdio.h>

int main() {
  int a = 10;
  int * b = &a;

  printf("A:\n\taddress: %p\n\tvalue: %d\n",&a, a);
  printf("B:\n\taddress: %p\n\tvalue: %d\n",b, *b);
}

Результат выполнения:

$ ./program
A:
    address: 0xbfed0fa8
    value: 10
B:
    address: 0xbfed0fa8
    value: 10

Указатели и массивы

На самом деле в Си нет массивов в привычном для многих людей понимании. Любой массив в Си — это просто ссылка на нулевой элемент массива. Пример:

#include <stdio.h>

int main() {
  int a[] = {10,20,30};

  printf("a-Address:%p\n",    &a);
  printf("a[0]-Address:%p\n", &a[0]);
  printf("a[0]-Value:%d\n",   a[0]);
  printf("a-Size:%d\n",       sizeof(a));
}

Результат:

$ ./program
a-Address:0xbfc029b4
a[0]-Address:0xbfc029b4
a[0]-Value:10
a-Size:12

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

Когда мы запускаем программу, то операционная система предоставляет программе два особых объема памяти — стек (stack) и кучу (heap). В нашей программе используется только стек. Стек хранит значения упорядочено. Когда мы создаем массив, мы на самом деле создаем указатель ну нулевой элемент коллекции элементов и резервируем память для N-количества элементов. В примере выше мы создали коллекцию из 3 элементов типа int, т.е. каждый элемент занимает 4 байта памяти. Когда мы воспользовались функцией sizeof(), которая возвращает размер в байтах переданного ей аргумента, то получили значение 12 т.е. массив занимает 12 байт памяти: 3элемента * 4 байта. Поскольку для хранения элементов коллекции используется стек — элементы сохраняются по порядку, то есть занимают соседние области стека, а это означает, что мы можем перемещаться по коллекции зная позицию элемента и размер коллекции. Пример:

#include <stdio.h>

int main() {
  int a[] = {10,20,30,40,10}, i;

  for(i = 0; i <= sizeof(a)/sizeof(int); i++)
    printf("a[%d] has %d in %p\n", i, a[i], &a[i]);
}

Результат:

$ ./program
a[0] has 10 in 0xbfbeda88
a[1] has 20 in 0xbfbeda8c
a[2] has 30 in 0xbfbeda90
a[3] has 40 in 0xbfbeda94
a[4] has 10 in 0xbfbeda98
a[5] has 5 in 0xbfbeda9c

Программа напечатала нам информацию о массиве из 5 элементов: номер элемента, значение и адрес в памяти. Обратите внимание на адреса элементов — это то, о чем я вам говорил. Адреса идут подряд и каждый следующий больше предыдущего на 4. В 5 элементе коллекции, которого мы на самом деле не объявляли хранится общее количество элементов коллекции. Самое интересное — это то, что мы можем аналогичным образом использовать и указатели для прохода по массиву. Пример:

#include <stdio.h>

int main() {
  int a[] = {10,20,30,40,10}, i;
  int * b = a;

  for(i = 0; i <= sizeof(a)/sizeof(int); i++)
    printf("a[%d] has %d in %p\n", i, *(b + i), b + i);
}

Примечания

1. Обратите внимание на то, что указателю b мы присваиваем не адрес массива a, а само значение переменной a, ведь a это, по сути и есть указатель.

2. Использование квадратных скобой с указанием индексов элементов массива — это такой синтаксический сахар в Си для более удобного и понятного обращения к элементам коллекции.

3. Как я уже говорил, в Си нету традиционных массивов потому, я называю их коллекциями для того, чтобы подчеркнуть эту особенность Си.

4. Адрес 1 элемента массива больше адреса 0 элемента массива на объем памяти выделяемой под хранение элемента данного типа. Мы работаем с элементами типа int, для хранения каждого из которых используется 4 байта. Адрес элемента массива в памяти и вообще любых данных — это адрес первого байта выделяемой под его хранение памяти.

5. Для упрощения понимания представьте, что память компьютера — это огромный кинотеатр, где места пронумерованы от 0 до, скачем 1_073_741_824. У данных типа char задница нормального размера и они помещаются в одном кресле (занимают один байт), а у толстых посетителей типа long double задницы огромные и каждый из них вмещается только на 10 сидениях. Когда у толстых посетителей кинотеатра спрашивают номер их места, они говорят только номер первого кресла, а количество и номера всех остальных кресел можно легко вычислить исходя из комплекции  посетителя (типа данных). Массивы можно представить в виде групп однотипных посетителей кинотеатра, например группа худеньких балерин типа char из 10 человек займет 10 мест потому, что char вмещается в одном кресле, а группа любителей пива состоящая из 5 человек типа long int займет 40 байт.

6. У операторов & и * имеется несколько популярных названий, но вы можете называть их  хоть Васей и Петей. Главное, что стоит запомнить — это:

& — показывает номер первого занятого посетителем кинотеатра сидения. То есть адрес первого занимаемого байта.

* — позволяет обратиться к посетителю сищящему на определенном месте. То есть позволяет получить значение, что хранится по определенному адресу в памяти.

На этом статья окончена, но не окончена тема массивов и указателей, а тем более изучения всего языка Си.

Лучшая благодарность автору — ваши комментарии!

Tags: , , ,

Responses

  1. anonymouse says:

    октября 31, 2011 at 12:47 (#)

    Проверь, действительно ли эти два массива одинаковы:
    char ch[] = {‘R’,'u’,'b’,'y’,'D’,'e’,'v’,’.’,'r’,'u’};
    char ch2[] = «RubyDev.ru»;
    Во втором случае массив содержит на один элемент больше, /0, который используется как ограничитель при печати, копировании строк и так далее.

  2. admin says:

    октября 31, 2011 at 18:05 (#)

    На самом деле оба массива содержат символ \0 в качестве 10 элемента, по этому они действительно одинаковы, но о символе \0 я расскажу в отдельной статье посвященной символьным массивам и строкам.

  3. anonymouse says:

    ноября 1, 2011 at 09:52 (#)

    Да, ты оказался прав, я написал тот комментарий до того как сам проверил вот этот код в GCC:
    #include
    #include

    int main(void)
    {
    char ch[] = {‘R’,'u’,'b’, ‘y’, ‘D’, ‘e’, ‘v’, ‘.’, ‘r’, ‘u’};
    char ch2[] = «RubyDev.ru»;

    printf(«%x\n», ch[ strlen(ch) ] );

    return 0;
    }
    Печатает ноль.

  4. admin says:

    ноября 1, 2011 at 18:49 (#)

    Самое интересно, что если верить спецификации ANSI C, то ты прав ведь там ничего не сказано об автодобавлении нулевого символа в конец массива символов созданного стандартным для массивов способом (и в K&R это в обоих вариантах делается явно). Думаю, это или отличие в С99 или в компиляторе дело, так как производители компиляторов реализуют возможности С99 в основном частично, а некоторые добавляют что-то свое. Теперь понятно, почему выбор компилятора так важен. Нужно будет над этим вопросом попозже поработать и написать статью о различиях компиляторов Си, поддержке ими С99 и различиях между ANSI C и C 99.

  5. admin says:

    ноября 2, 2011 at 11:14 (#)

    Провел расследование, все таки я дезинформировал тебя. В традиционном синтаксисе \0 не добавляется, это просто такое совпадение, что следующим в стеке идет символ \0, но он не относится к массиву символов. Если использовать strlen() то явно видна разница в 1 символ между традиционным синтаксисом создания массива. где символы просто перечисляются и использованием строковой константы. null-символ добавляется автоматически только в конец массива символов созданного при помощи строковой констранты.

  6. andr says:

    ноября 15, 2011 at 17:46 (#)

    Очень много вводящих в заблуждение высказываний. Такими статьями начинающих программистов только портить. :)
    Например, «В 5 элементе коллекции, которого мы на самом деле не объявляли хранится общее количество элементов коллекции.», — вот это невиданные сказки. В языке С нигде не хранится длина массивов. В этом примере происходит выход за пределы массива ‘a’, т.к. для 5-ти элементов массива последний индекс == 4, а ты его индексируешь до 5-ти. Тем самым обращаешься по адресу переменной i, которую компилятор разместил в памяти сразу после массива, поэтому на последнем цикле (когда i == 5) и получаешь в результате 5-ку на выходе.

    «Как я уже говорил, в Си нету традиционных массивов потому, я называю их коллекциями для того, чтобы подчеркнуть эту особенность Си.» — это совсем что-то непонятное. Что такое «традиционные массивы»? Коллекции, кстати, это более широкий термин. Под определение коллекций подходят массивы, списки, матрицы, стеки и даже хеш-таблицы. Зачем вводить неподходящие термины и вводить в заблуждение читателей?

  7. admin says:

    ноября 15, 2011 at 18:41 (#)

    andr спасибо за замечание. Я только недавно начал изучать Си, и это были мои предположения. Си несколько непривычен для меня, вот и получаются такие ошибки. Скоро поправлю.

  8. faustman says:

    ноября 28, 2011 at 19:02 (#)

    Про худеньких балерин и группу любителей пива хорошо сказал!))

  9. Myname says:

    декабря 6, 2012 at 11:40 (#)

    А у меня gcc a[5], которое, вы говорите, хранит количество элементов, выдает значение 32767.

Leave a Response

Для подсветки кода используйте BB - коды: [language]...[/language], где language может быть: ruby, javascript, css, html.