Переменные и типы данных в Си

мая 2, 2012  |  Published in Си и C++

gnu-c

Из серии:

  1. Введение в программирование на Си
  2. Переменные и типы данных в Си
  3. Операторы, условия и циклы языка Си

Нужно быть полнейшим идиотом для того, чтобы не понять по заголовку о чем идет речь в статье. Очевидно, что читатели 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 рассказать не могу, ибо сам никогда не пользовался, да и вообще это редко применяемый модификатор (квалификатор).

На этом мы заканчиваем наше знакомство с типами данных и переменных!

Tags: , , , ,

Leave a Response

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