Раздел 3 — Типы данных и память

Сколько места занимает значение, какие значения в него помещаются и что будет, если не поместятся

Прогресс курса Раздел 3 из 28

Что вы освоите в этом разделе

2 академических часа теории, практической работы в разделе нет; раздел заканчивается тестом на портале. Программы страницы выводят значения функцией printf: %d означает «вставить сюда целое число», остальные форматы названы рядом. Подробно printf разбирается в разделе 4.
01

Память, байт, переменная

Оперативная память — длинный ряд байтов. Байт — 8 бит, каждый бит — 0 или 1, поэтому в байте 28 = 256 разных значений. У каждого байта есть номер — адрес. Всё, что хранит программа — числа, текст, картинки, — записано в памяти байтами.

Переменная — именованное место в памяти. У неё четыре характеристики:

int count = 25;

  имя       count
  тип       int — 4 байта, целое со знаком
  адрес     номер первого из 4 байтов, выбирается при запуске (раздел 10)
  значение  25, записано в этих 4 байтах в двоичном виде

Тип определяет, сколько байт занимает значение, какие значения в них помещаются и какие операции с ними можно делать. Одни и те же 4 байта 00 00 C0 3F — это целое 1 069 547 520, если тип int, и вещественное 1.5, если тип float: память не знает, что в ней лежит, это знает только тип переменной.

Переменную нужно объявить до использования. Объявление без начального значения оставляет в памяти то, что там было раньше — «мусор»:

int total;              /* значение не определено */
int sum = 0;            /* значение 0 */
int a = 1, b = 2;       /* две переменные в одной строке — так можно, но лучше по одной */
Правило курса: переменная получает начальное значение в строке объявления, если это значение известно. Чтение переменной без значения — ошибка; gcc с -Wall замечает её не всегда (предупреждение -Wuninitialized).
02

Целые типы и их размеры

Стандарт C не задаёт точных размеров целых типов — только минимальные. Фактический размер зависит от компилятора и ОС и узнаётся операцией sizeof:

/* Размеры типов в байтах */
#include <stdio.h>

int main(void)
{
    printf("char      %zu\n", sizeof(char));
    printf("short     %zu\n", sizeof(short));
    printf("int       %zu\n", sizeof(int));
    printf("long      %zu\n", sizeof(long));
    printf("long long %zu\n", sizeof(long long));
    printf("float     %zu\n", sizeof(float));
    printf("double    %zu\n", sizeof(double));
    return 0;
}

%zu — формат для результата sizeof (тип size_t, беззнаковое целое).

ТипWindows x64 (MinGW-w64)Linux x64 (gcc)Диапазон со знаком
char11−128 … 127
short22−32 768 … 32 767
int44−2 147 483 648 … 2 147 483 647 (≈ ±2,1 · 109)
long48как int или как long long
long long88≈ ±9,2 · 1018
float44вещественный, пункт 04
double88вещественный, пункт 04

Ловушка — long: в Windows он 4 байта, в Linux 8. Программа, где в long хранятся числа больше двух миллиардов, работает в Linux и ломается в Windows. Поэтому в курсе long не используется: нужно больше int — берите long long.

Когда размер важен точно (формат файла, сетевой протокол), используют типы из <stdint.h> — у них размер в имени: int8_t, int16_t, int32_t, int64_t и беззнаковые uint8_t … uint64_t. Границы типов — константы из <limits.h>:

/* Границы целых типов */
#include <stdio.h>
#include <limits.h>

int main(void)
{
    printf("INT_MIN   = %d\n", INT_MIN);
    printf("INT_MAX   = %d\n", INT_MAX);
    printf("UINT_MAX  = %u\n", UINT_MAX);
    printf("LLONG_MAX = %lld\n", LLONG_MAX);
    return 0;
}
INT_MIN   = -2147483648
INT_MAX   = 2147483647
UINT_MAX  = 4294967295
LLONG_MAX = 9223372036854775807

%u — беззнаковое целое, %lld — long long. Формат должен совпадать с типом значения: gcc с -Wall проверяет это и предупреждает о несовпадении (раздел 1, пункт 07).

Как выбрать целый тип: оцените наибольшее значение, которое может получиться в задаче, — не только во вводе, но и в промежуточных результатах (сумма, произведение). До 2 · 109 — int; до 9 · 1018 — long long; больше — встроенного целого типа нет, нужен другой подход. Население Земли (8 · 109) в int уже не помещается.
03

Знаковые и беззнаковые, переполнение

У каждого целого типа есть беззнаковая версия: unsigned int, unsigned long long и т. д. Отрицательных значений у неё нет, а наибольшее вдвое больше: unsigned int — от 0 до 4 294 967 295.

Отрицательные числа хранятся в дополнительном коде. Для 8 бит: 0 — 00000000, 1 — 00000001, 127 — 01111111; −1 — 11111111, −128 — 10000000. Старший бит — признак знака. Одни и те же 8 бит 11111111 — это −1 в int8_t и 255 в uint8_t.

Переполнение — результат не помещается в тип. Для беззнаковых стандарт определяет результат: значение «заворачивается» по кругу, берётся остаток от деления на 2N, где N — число бит:

/* Переполнение беззнакового типа — по кругу */
#include <stdio.h>
#include <limits.h>

int main(void)
{
    unsigned int u = 0;
    u = u - 1;
    printf("0 - 1   = %u\n", u);

    unsigned int m = UINT_MAX;
    m = m + 1;
    printf("MAX + 1 = %u\n", m);
    return 0;
}
0 - 1   = 4294967295
MAX + 1 = 0

Для знаковых типов переполнение — неопределённое поведение (undefined behavior, UB): стандарт не говорит, что произойдёт. На практике чаще всего получается заворот (INT_MAX + 1 → INT_MIN), но компилятор при оптимизации вправе считать, что переполнения не бывает, и выбросить проверки, которые на него рассчитывают. Программа с UB может работать при отладке и ломаться в релизной сборке.

Знаковое и беззнаковое в одном выражении. При сравнении int с unsigned int знаковое значение переводится в беззнаковое: -1 < 1u ложно, потому что −1 становится 4 294 967 295. gcc с -Wall -Wextra предупреждает: comparison of integer expressions of different signedness [-Wsign-compare]. Правило курса: беззнаковые типы — только для размеров (size_t) и битовых масок (раздел 12), для чисел предметной области — знаковые.
04

Вещественные типы и погрешность

Вещественные числа хранятся в форме с плавающей точкой (стандарт IEEE 754): знак, порядок и мантисса — как 1,5 · 103, только по основанию 2.

ТипБайтЗначащих десятичных цифрНаибольшее значениеФормат printf
float46–7≈ 3,4 · 1038%f
double815–16≈ 1,8 · 10308%f, %g

Главное свойство: большинство десятичных дробей хранится неточно. 0,1 в двоичной записи — бесконечная периодическая дробь 0,000110011001100…, как 1/3 в десятичной. В double хранится ближайшее представимое число:

/* Погрешность double */
#include <stdio.h>
#include <math.h>

int main(void)
{
    double a = 0.1 + 0.2;
    printf("0.1 + 0.2 = %.17g\n", a);
    printf("0.3       = %.17g\n", 0.3);
    printf("a == 0.3        : %d\n", a == 0.3);
    printf("|a - 0.3| < 1e-9: %d\n", fabs(a - 0.3) < 1e-9);

    float f = 16777217.0f;
    printf("float 16777217 = %.1f\n", f);
    return 0;
}
0.1 + 0.2 = 0.30000000000000004
0.3       = 0.29999999999999999
a == 0.3        : 0
|a - 0.3| < 1e-9: 1
float 16777217 = 16777216.0
Деньги в double. 0,1 ₽ + 0,2 ₽ ≠ 0,3 ₽ — для бухгалтерии это недопустимо. Суммы денег в серьёзных программах хранят целым числом копеек (long long) и переводят в рубли только при выводе. В учебных задачах курса деньги — double, а результат округляется при выводе до копеек.

В Linux программа с <math.h> может потребовать при сборке ключ -lm в конце команды — подключить библиотеку математических функций: gcc … float.c -o float -lm. В Windows (MinGW) он не нужен.

05

Символы: char — это число

Тип char — целое в 1 байт. Символ хранится своим кодом по таблице ASCII: 'A' — 65, 'a' — 97, '0' — 48, пробел — 32. Символьный литерал пишется в одинарных кавычках и равен числу: 'A' == 65 истинно.

/* Символ и его код */
#include <stdio.h>

int main(void)
{
    char letter = 'A';
    printf("%c = %d\n", letter, letter);
    printf("%c\n", letter + 2);          /* следующие буквы идут подряд */

    char digit = '7';
    int value = digit - '0';              /* символ цифры → число */
    printf("'%c' - '0' = %d\n", digit, value);
    return 0;
}
A = 65
C
'7' - '0' = 7

%c выводит число как символ, %d — как число. Цифры '0'…'9' в таблице идут подряд (коды 48–57), поэтому digit - '0' даёт значение цифры. Это используется при разборе строк (раздел 11).

Буква '7' и число 7 — разные значения: 55 и 7. Кириллица в таблицу ASCII не входит; в кодировке UTF-8 русская буква занимает 2 байта, то есть два char. Поэтому программы курса выводят текст латиницей, а работа с кириллицей в строках разбирается отдельно в разделе 11.
06

Преобразования типов

Когда в выражении встречаются разные типы, C приводит их к общему по правилам неявного преобразования: char и short → int; целое и вещественное → double; знаковое и беззнаковое того же размера → беззнаковое (пункт 03).

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

int sum = 7, count = 2;
double avg = sum / count;               /* 3.000000: деление целых — целое */
double avg2 = (double)sum / count;     /* 3.500000 */

(double)sum — явное преобразование (приведение типа): значение sum превращается в double до деления, и деление становится вещественным. Скобки (double)(sum / count) не помогут: сначала выполнится целое деление, а преобразуется уже 3.

Вещественное → целое отбрасывает дробную часть (не округляет): (int)3.99 равно 3, (int)-3.99 равно −3. Для округления к ближайшему есть функция round из <math.h>: (int)round(3.99) равно 4. Если вещественное значение не помещается в целый тип — неопределённое поведение.

ВыражениеТипЗначение
7 / 2int3
7 % 2int1
7.0 / 2double3.5
(double)7 / 2double3.5
(double)(7 / 2)double3.0
(int)3.99int3
'a' + 1int98
07

Константы

Литерал — значение, записанное прямо в тексте программы. Его тип определяется записью:

ЛитералТипЗамечание
42, -7intдесятичная запись
0x1Fintшестнадцатеричная: 31
017intвосьмеричная: 15. Ведущий ноль меняет смысл — не дописывайте нули слева
5000000000LL, 10ulong long, unsignedсуффиксы типа
3.5, 1e-9double1e-9 = 1 · 10−9
2.5ffloatсуффикс f
'A', '\n'int (код символа)'\n' — один символ «перевод строки»
"text"строка, массив charраздел 11

Число, смысл которого не очевиден, нужно назвать. В C для этого три способа:

#define SEATS 45                /* макрос: препроцессор заменит SEATS на 45 до компиляции */
const double VAT_RATE = 0.20;   /* константа: переменная, которую нельзя изменить */
enum { MONTHS = 12 };          /* перечисление: целая константа с именем */
Правило курса: число из условия задачи, у которого есть смысл (ставка, вместимость, граница диапазона), — именованная константа: const для вещественных, enum или #define для целых. Имена констант — заглавными буквами. Числа 0, 1, 2 в формулах вроде «удвоить» или «следующий» именовать не нужно.
08

Простые, производные и структурированные типы

Все типы этого раздела — простые (скалярные): значение — одно число. Из них строятся остальные:

ВидПримерЧто хранитРаздел
Массивint marks[30];несколько значений одного типа подряд8, 9
Указательint *p;адрес другого значения10
Строкаchar name[50];массив символов с нулём в конце11
Структураstruct student { … };несколько значений разных типов под одним именем13
Функцияint gcd(int a, int b)именованный алгоритм7

Массив, указатель и функция — производные типы: они строятся из другого типа («массив int», «указатель на double»). Массив и структура — структурированные: в одной переменной несколько значений. Своего типа «строка», как в Pascal или Python, в C нет — строка это массив char.

Ключевые выводы раздела

Запомните главное

  • Переменная — имя, тип, адрес и значение; тип определяет размер, диапазон и операции
  • Размеры типов зависят от ОС; long — 4 байта в Windows и 8 в Linux, в курсе вместо него long long
  • Тип выбирается по наибольшему значению задачи, включая промежуточные результаты
  • Беззнаковые при переполнении идут по кругу; переполнение знаковых — неопределённое поведение
  • double хранит большинство дробей с погрешностью; вещественные сравнивают с допуском
  • char — число; digit - '0' превращает символ цифры в её значение
  • Тип результата определяется операндами: 7 / 2 — 3; для вещественного деления — (double)
  • Числа со смыслом — именованные константы: const, enum, #define
Связи раздела. Опирается на раздел 1 — сборка, предупреждения, препроцессор и #define; раздел 2 — модель задачи: величины и их допустимые значения. Нужен для: раздела 4 — операции над этими типами, форматы printf и scanf; раздела 6 — выбор типа накопителя в цикле; раздела 10 — адрес переменной; раздела 12 — двоичная запись целых и битовые операции.
Тест по разделу 3 — на учебном портале. Курс на portal.nevabit.ru. Учётную запись выдаёт преподаватель.
Раздел 2: Алгоритм и его запись Все разделы Раздел 4: Выражения, ввод и вывод