Сколько места занимает значение, какие значения в него помещаются и что будет, если не поместятся
sizeofdouble и правильно сравнивать вещественные числаconst, #define и enumprintf: %d означает «вставить сюда целое число», остальные форматы названы рядом. Подробно printf разбирается в разделе 4.Оперативная память — длинный ряд байтов. Байт — 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; /* две переменные в одной строке — так можно, но лучше по одной */
-Wall замечает её не всегда (предупреждение -Wuninitialized).Стандарт 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) | Диапазон со знаком |
|---|---|---|---|
char | 1 | 1 | −128 … 127 |
short | 2 | 2 | −32 768 … 32 767 |
int | 4 | 4 | −2 147 483 648 … 2 147 483 647 (≈ ±2,1 · 109) |
long | 4 | 8 | как int или как long long |
long long | 8 | 8 | ≈ ±9,2 · 1018 |
float | 4 | 4 | вещественный, пункт 04 |
double | 8 | 8 | вещественный, пункт 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).
int; до 9 · 1018 — long long; больше — встроенного целого типа нет, нужен другой подход. Население Земли (8 · 109) в int уже не помещается.У каждого целого типа есть беззнаковая версия: 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), для чисел предметной области — знаковые.Вещественные числа хранятся в форме с плавающей точкой (стандарт IEEE 754): знак, порядок и мантисса — как 1,5 · 103, только по основанию 2.
| Тип | Байт | Значащих десятичных цифр | Наибольшее значение | Формат printf |
|---|---|---|---|---|
float | 4 | 6–7 | ≈ 3,4 · 1038 | %f |
double | 8 | 15–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
%.17g — вывести 17 значащих цифр: так видна погрешность, которую обычный %f (6 знаков после точки) скрывает округлением.0.1 + 0.2 == 0.3 — ложь.fabs(a - b) < EPS, где fabs — модуль из <math.h>, а EPS выбирается по задаче (для денег в рублях — 1e-9 с запасом).float 24 бита мантиссы: целые числа больше 224 = 16 777 216 хранятся уже не все. Поэтому в курсе вещественный тип — double; float — только там, где этого требует библиотека.double. 0,1 ₽ + 0,2 ₽ ≠ 0,3 ₽ — для бухгалтерии это недопустимо. Суммы денег в серьёзных программах хранят целым числом копеек (long long) и переводят в рубли только при выводе. В учебных задачах курса деньги — double, а результат округляется при выводе до копеек.В Linux программа с <math.h> может потребовать при сборке ключ -lm в конце команды — подключить библиотеку математических функций: gcc … float.c -o float -lm. В Windows (MinGW) он не нужен.
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.Когда в выражении встречаются разные типы, 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 / 2 | int | 3 |
7 % 2 | int | 1 |
7.0 / 2 | double | 3.5 |
(double)7 / 2 | double | 3.5 |
(double)(7 / 2) | double | 3.0 |
(int)3.99 | int | 3 |
'a' + 1 | int | 98 |
Литерал — значение, записанное прямо в тексте программы. Его тип определяется записью:
| Литерал | Тип | Замечание |
|---|---|---|
42, -7 | int | десятичная запись |
0x1F | int | шестнадцатеричная: 31 |
017 | int | восьмеричная: 15. Ведущий ноль меняет смысл — не дописывайте нули слева |
5000000000LL, 10u | long long, unsigned | суффиксы типа |
3.5, 1e-9 | double | 1e-9 = 1 · 10−9 |
2.5f | float | суффикс f |
'A', '\n' | int (код символа) | '\n' — один символ «перевод строки» |
"text" | строка, массив char | раздел 11 |
Число, смысл которого не очевиден, нужно назвать. В C для этого три способа:
#define SEATS 45 /* макрос: препроцессор заменит SEATS на 45 до компиляции */
const double VAT_RATE = 0.20; /* константа: переменная, которую нельзя изменить */
enum { MONTHS = 12 }; /* перечисление: целая константа с именем */
#define — замена текста препроцессором (раздел 1, пункт 06). У макроса нет типа, его не видно в отладчике, но им можно задать размер массива. Без точки с запятой и знака равенства.const — переменная только для чтения: VAT_RATE = 0.18; не соберётся (error: assignment of read-only variable). Есть тип, видна в отладчике.enum — именованные целые константы; удобен для набора связанных значений: enum { RED, GREEN, BLUE }; — 0, 1, 2.const для вещественных, enum или #define для целых. Имена констант — заглавными буквами. Числа 0, 1, 2 в формулах вроде «удвоить» или «следующий» именовать не нужно.Все типы этого раздела — простые (скалярные): значение — одно число. Из них строятся остальные:
| Вид | Пример | Что хранит | Раздел |
|---|---|---|---|
| Массив | 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 longdouble хранит большинство дробей с погрешностью; вещественные сравнивают с допускомchar — число; digit - '0' превращает символ цифры в её значение7 / 2 — 3; для вещественного деления — (double)const, enum, #define#define; раздел 2 — модель задачи: величины и их допустимые значения. Нужен для: раздела 4 — операции над этими типами, форматы printf и scanf; раздела 6 — выбор типа накопителя в цикле; раздела 10 — адрес переменной; раздела 12 — двоичная запись целых и битовые операции.