Переменные и типы данных в Си
мая 2, 2012 | Published in Си и C++
Из серии:
- Введение в программирование на Си
- * Переменные и типы данных в Си
- Операторы, условия и циклы языка Си
Нужно быть полнейшим идиотом для того, чтобы не понять по заголовку о чем идет речь в статье. Очевидно, что читатели RubyDev в совершенстве адекватные люди с IQ не ниже среднего и достаточно широкими энциклопедическими знаниями и кругозором и потому с места мы сразу прыгаем в карьер минуя абсолютно бесполезное разлогое введение в тему статьи.
Переменные
Переменные — это именованные, понятные человеку ссылки на данные. Переменные в языке Си являются статическим и строготипизированными. На практике это проявляется в следующем:
- Переменная должна быть объявлена перед использованием.
- При объявлении переменной должен быть определен ее тип данных, а точнее тип данных, на который она ссылается.
- Тип данных переменной остается неизменным на протяжении всего выполнения программы.
- Переменная бронирует ячейку в памяти, которая соответствует указанному типу данных и не может эту ячейку как-либо расширить. По этой причине необходимо осторожно подбирать тип данных для того, чтобы быть уверенным, что памяти зарезервированной переменной хватит для сохранения данных.
Если сравнивать переменные Си и Ruby, то они весьма отличаются друг от друга. Например переменные в Ruby являются не статическими, но динамическими и не типизированными. Переменная в Ruby может ссылаться на любой тип данных и постоянно менять его. Это связано с тем, что в Ruby переменная не резервирует область памяти, а ссылается на объект в виртуальной таблице объектов. Визуально это можно представить себе как две таблицы, одна содержит два столбца, в одном хранятся все объекты (их данные), а в другом — идентификатор object_id. Во второй таблице у нас имеются также два столбца, в одном хранится object_id, а в другом имя переменной. Когда вы присваиваете переменной другое значение, то во второй таблице напротив имени переменной просто меняется object_id. В Си все совсем иначе. Переменная при объявлении захватывает область памяти, размер которой соответствует типу переменной, например 1 байт. Далее переменная ссылается на эту область данных, а вы можете помещать в нее любое значение.
Типы данных Си
Знакомство с типами данных традиционно принято начинать с типа char. Не будем изменять традициям и начнем с char.
CHAR
char — это тип данных, которые требуют для хранения 1 байт памяти. Этого количества памяти хватает для хранения символа ASCII, или чисел от -128 до +127, или от 0 до +255. Тип char применяется для хранения ASCII последовательностей, но может интерпретироваться и как число.
#include <stdio.h> int main(void) { char a = 'a'; printf("%c -> %d\n", a, a); printf("%d - 5 = %d\n", a, a - 5); return 0; }
В результате выполнения программы будет напечатано следующее:
a -> 97
97 — 5 = 92
Целочисленные INT типы
Тип int используется для хранения целых чисел и занимает не менее 4 байтов памяти. Тип int является, так сказать, семейством типов. int типов существует несколько: short int, long int и long long int (C99). Признаться честно, все типы являются платформа-зависимыми, то есть, говоря, что int занимает 4 байта, а не подразумеваю, что 4 байта и только так. На некоторых платформах int может занимать, например 8 байт памяти, а на других 2. Насколько мне известно, стандарт Си все же регламентирует минимальное значение основных типов и потому, наверное, можно быть уверенным в том, что int зарезервирует минимум 4 байта. Лично на моей машине int вмещает число 2147483647, long int занимает столько же места, как и int. Тип short int занимает минимум 2 байта.
Помимо int существуют short int и long int. Первый занимает минимум 1 байт, а второй обязан быть не менее int, но чаще всего занимает 4 байта.
#include <stdio.h> int main(void) { int a = 2147483647; short int b = 32767; long int c = 2147483647; printf("%d, %d, %li\n", a, b, c); return 0; }
Программа напечатает следующую строку:
2147483647, 32767, 2147483647
Числа с плавающей точкой
Для хранения чисел с плавающей точкой, используются специальные типы данных: float, double и long double. Каждый из них занимает 4, 8 и 12 байт соответственно.
Чтобы узнать достоверно, какой размер памяти занимает каждый тип необходимо воспользоваться функцией sizeof():
#include <stdio.h> int main(void) { printf("\n----- Integer numbers -----\n\n"); printf("type %15s%5s \n", "|", "size"); printf("%s %15s %2d\n", "char", "|", sizeof(char)); printf("%s %10s %2d\n", "short int", "|", sizeof(short int)); printf("%s %16s %2d\n", "int", "|", sizeof(int)); printf("%s %11s %2d\n", "long int", "|", sizeof(long int)); printf("%s %6s %2d\n", "long long int", "|", sizeof(long long int)); printf("\n----- Floating point numbers -----\n\n"); printf("%s %14s %2d\n", "float", "|", sizeof(float)); printf("%s %13s %2d\n", "double", "|", sizeof(double)); printf("%s %8s %2d\n", "long double", "|", sizeof(long double)); return 0; }
Эта программа напечатает следующий результат:
----- Integer numbers ----- type | size char | 1 short int | 2 int | 4 long int | 4 long long int | 8 ----- Floating point numbers ----- float | 4 double | 8 long double | 12
Signed и Unsigned типы
Численные типы данных могут быть signed и unsigned. Signed типы означают, что переменная такого типа может хранить число со знаком, а unsigned, наоборот, что переменная хранит число без знака. Когда я писал выше, что char может хранить числа в диапазоне от -128 до +127, или от 0 до +255, то в первом случае подразумевался тип signed char, а во втором unsigned. Если переменная не должна хранить отрицательное число, то ей необходимо прописать unsigned-тип, так вы более явно укажете, что число может быть только положительным и увеличите диапазон положительных значений в 2 раза (с 127 до 255, например). Вам не обязательно писать signed, если переменная может иметь в качестве значения отрицательное число. Если signed или unsigned не указаны, то переменная по умолчанию объявляется с signed типом.
Строки и массивы
Хочу вас обрадовать. В Си нет строк. Нет строк таких как в Ruby, JavaScript или даже богомерзком PHP. Строки в Си представляют собой массив символов. Единственное чем строки отличаются от массива — это возможность использовать более краткий и лаконичный синтаксис их объявления, а еще строка должна завершаться символом окончания строки — «\0«. Ниже приведен пример специфичного для строк синтаксиса их объявления, а еще «классический» синтаксис создания массива.
#include <stdio.h> int main(void) { char a[] = "string"; char b[] = { 's', 't', 'r', 'i', 'n', 'g', '\0'}; printf("%s == %s\n", a, b); return 0; }
Данная программа напечатает следующее: string == string
Как видите, чтобы объявить массив необходимо также указывать тип его элементов и ставить [] после имени переменной. Да, да, массивы в Си могут хранить коллекцию элементов только одного типа. Ниже приведены некоторые примеры массивов.
int a[100] — массив целых чисел (тип int), размер которого — 100 элементов.
char b[5] — массив символов (или целых чисел, тип char), размер которого — 5 элементов.
float c[] = { 4.5, 6.7, 12.345 } — массив чисел с плавающей точкой (тип float) состоящий из 3 элементов.
Доступ к элементам массива очень прост и должно быть вы не раз сталкивались с подобным синтаксисом:
#include <stdio.h> int main(void) { float c[] = { 4.5, 6.7, 12.345 }; printf("%f\n", c[0]); printf("%f\n", c[1]); printf("%f\n", c[2]); return 0; }
Напечатает:
4.500000
6.700000
12.345000
На самом деле массив представляет собой не более чем указатель на первый его элемент (элемент с индексом 0). Смысл массивов в Си — забронировать N — ячеек в памяти для хранения там элементов массива. Когда вы используете следующий по порядку индекс, то выполняется особая уличная магия арифметика с указателями. Эта арифметика заключается например в том, что 1 + 1 = 4, именно на 4 байта должен переместиться указатель, чтобы получить доступ к следующему элементу типа float. Если быть совсем точным, то перемещается не указатель (есть такой элемент языка Си), а сама программа, которая хочет получить доступ к данным. Указатель остается на месте, а вот программа отсчитывает от него 4 байта (точнее столько, сколько резервирует для себе определенный тип данных) или 1 элемент того же типа. Подробнее об указателях мы поговорим в отдельной главе ибо это достаточно важная, большая и сложная тема.
Несмотря на всю аскетичность Си, массивы в Си могут быть многоуровневыми. Ниже приведен пример создания такого массива:
#include <stdio.h> int main(void) { int a[3][3] = { { 1, 2, 3 }, { 4, 5, 6 }, {7, 8 ,9} }; printf("%d\n", a[0][0]); printf("%d\n", a[0][1]); printf("%d\n", a[1][0]); printf("%d\n", a[2][0]); return 0; }
Данная программа напечатает следующий текст:
1
2
4
7
В программировании на Си, а особенно в работе с массивами имеется один неприятный момент — вы можете легко залезть не туда, куда хотели. Например вы можете повредить данные другого запущенного приложения. Сделать это достаточно просто:
#include <stdio.h> int main(void) { int a[] = { 1, 2, 3, 4 }; float c[] = { 4.5, 6.7, 12.345 }; printf("%f\n", c[0]); printf("%f\n", c[1]); printf("%f\n", c[2]); printf("%d\n", a[5]); return 0; }
Результат:
4.500000
6.700000
12.345000
1087792742
Откуда у a взялся 6й элемент? Разумеется его нет и быть не может. Поскольку данные типа int занимают ячейку в 4 байта, то программа просто отсчитала 5 ячеек по 4 байта и выбрала шестую ячейку размером 4 байта. Разумеется, никакой 6й ячейки в действительно нет, а полученное число 1087792742 может быть просто мусором или данным из какой-нибудь другой программы. Обычно операционная система беспокоится о том, чтобы каждой программе был выделен какой-то объем памяти и чтобы другая программа не могла получить к нему доступа. Если другая программа пытается получить доступ к чужим данным, то возникает ошибка Segmentation fault и программа завершает свою работу.
На этом мы закончим знакомство с типами данных и вернемся к переменных. Мы рассмотрели только самые основные (примитивные) типы данных, но и до остальных доберемся. Дело в том, что оставшиеся типы данных достаточно сложны, чтобы их описывать все вместе в одной статье. Об одних только указателях можно написать целую книгу, а еще есть структуры, объединения и много-много других тем по типам данных.
Свойства переменных
Выше мы уже рассмотрели важное свойство переменных — тип данных, на который те ссылаются (ячейку для которого резервируют). Сейчас же мы рассмотрим три остальных свойства переменных — область видимости, время хранения и модификаторы доступа.
Область видимости переменной может быть следующей: блок кода (и вложенные в него блоки кода), функция, файл и программа в целом.
Пример переменной с глобальной областью видимости (по всему файлу):
#include <stdio.h> void print_file_var(void); char file_var[] = "file scope"; int main(void) { printf("%s\n", file_var); print_file_var(); return 0; } void print_file_var() { printf("%s\n", file_var); }
Данная программа напечатает:
file scope
file scope
Пример переменной с областью видимости — функцией:
#include <stdio.h> void print_function_var(void); int main(void) { char function_var[] = "function scope"; printf("%s\n", function_var); print_function_var(); return 0; } void print_function_var() { printf("%s\n", function_var); }
При компиляции программы возникает ошибка:
$ cc program.c
program.c: In function ‘print_function_var’:
program.c:14:20: error: ‘function_var’ undeclared (first use in this function)
program.c:14:20: note: each undeclared identifier is reported only once for each function it appears in
Она возникает потому, что переменной function_var не видно внутри функции print_var(). Внутри main() переменная function_var видима и может успешно использоваться.
Пример переменной с областью видимости — блоком кода:
#include <stdio.h> int main(void) { char function_var[] = "function scope"; { char function_var[] = "block variable"; printf("%s\n", function_var); } printf("%s\n", function_var); return 0; }
Данная программа напечатает:
block variable
function scope
Спецификаторы времени хранения
Спецификаторы времени хранения еще называют спецификаторами типа. Таких спецификаторов всего четыре: static, register, extern и auto.
auto позволяет объявлять переменные с областью видимости — блоком кода (в том числе и блоком в который помещен код функции). Переменные с спецификатором времени хранением автоматически удаляются после того, как функция или код в блоке завершили свою работу. Вы также можете их повторно инициировать при изменении локации. По умолчанию все переменные блоков кода являются auto и потому явно указывать auto не нужно.
static позволяет объявлять статические переменные, которые могут принадлежать как блоку кода, так и глобальному пространству имен. Отличие переменных объявленных как static заключается в том, что их значение сохраняется после выполнения блока кода. Если блок кода (или функция) вызываются повторно — будет использовано сохраненное в памяти значение переменной.
register пытается поместить переменную в регистр, доступ к которому значительно быстрее, чем к оперативной памяти. В силу того, что регистр располагает малым объемом памяти, спецификатор register не гарантирует того, что переменная будет помещена в регистр. В регистр следует помещать только такие данные, доступ к которым происходит многократно. Поскольку переменная со спецификатором register ссылается не на оперативную память, но на регист CPU, то получить адрес переменной нельзя, а значит нельзя и создать соответствующий указатель.
extern позволяет «наследовать» переменную, то есть сделать ее во истину глобальной. Вам достаточно объявить глобальную (видимую в одном файле) переменную в одном файле исходников вашей программы, а в другом вам достаточно написать:
extern <data type> <variable_name>, чтобы компилятор понял, что значение переменной с именем <variable_name> следует искать в других файлах.
Ниже приведен пример использования спецификаторов времени хранения:
/* auto, register, static, extend */ /* programm.c */ #include <stdio.h> char var[] = "ololo" int main(void) { auto char function_var[] = "function scope"; register int some_var = 100500; for (int i = 1; i <= 5; i++) { static char counter = 0; printf("%d\n", counter ++); } return 0; } /* programm_1.c */ #include <stdio.h> int main(void) { extend char var[]; printf("%s\n", var); return 0; }
Данная программа напечатает следующее:
0
1
2
3
4
Благодаря тому, что переменная counter сохраняет свое значение между вызовами блока кода в цикле for.
Модификаторы класса хранилища
Модификаторы класса хранилища еще называют квалификаторами типа. Таких модификаторов существует целых два: const и volatile.
Модификатор const позволяет объявить переменную, значение которой нельзя изменить. Пример:
#include <stdio.h> int main(void) { const char function_var[] = "function scope"; function_var[3] = 'k'; printf("%s\n", function_var); return 0; }
При попытке скомпилировать получаем ошибку:
$ cc program.c -std=c99
program.c: In function ‘main’:
program.c:5:5: error: assignment of read-only location ‘function_var[3]’
Модификатор volatile позволяет отключить оптимизацию памяти для работы с переменной. По умолчанию компилятор кэширует значение переменной, если оно не меняется, например если вы несколько раз пытаетесь присвоить одинаковое значение. Однако, иногда такую оптимизацию необходимо отключать. Более подробно о viotile рассказать не могу, ибо сам никогда не пользовался, да и вообще это редко применяемый модификатор (квалификатор).
На этом мы заканчиваем наше знакомство с типами данных и переменных!