Конструкции языка C
В этом посте разбираю базовые конструкции языка C: переменные, типы, операторы, условия, циклы, массивы, строки и ввод/вывод.
Конструкции языка C в стандарте C23
Этот материал ориентирован на C23. Для проверки примеров используйте режим стандарта: -std=c23. Официальные ссылки по режимам языка и поддержке компилятора: GCC Standards и GCC C status.
На странице GCC C status есть важная формулировка: C23 mode is the default since GCC 15
. Это означает, что в новых версиях GCC C23 уже базовый режим, но при написании обучающих примеров всё равно полезно явно указывать -std=c23, чтобы поведение было предсказуемым на разных машинах.
Структура программы на Си
Минимальная программа на C состоит из подключений заголовков, объявлений/определений функций и точки входа main. Подробно про структуру программы в официальном GNU-руководстве: Complete Program.
#include <stdio.h>
int square(int x) {
return x * x;
}
int main(void) {
int value = 7;
printf("square(%d) = %d\n", value, square(value));
return 0;
}#include <stdio.h> подставляет объявления стандартного ввода/вывода; функция square сначала объявлена и определена, затем вызывается из main; return 0; возвращает код завершения в ОС. Такой порядок помогает компилятору корректно проверять типы и сигнатуры вызовов.
Для C23 особенно важно использовать полноценные прототипы функций и избегать старых неявных стилей. Это снижает риск ошибок при преобразованиях аргументов и на этапе линковки.
Переменные
Переменная в C имеет тип, имя, область видимости и время жизни. Официальный вводный раздел: GNU Variables. В практическом коде переменные лучше инициализировать сразу, иначе легко получить неопределенное поведение.
#include <stdio.h>
int main(void) {
int age = 21;
double temperature = 36.6;
char grade = 'A';
printf("age = %d\n", age);
printf("temperature = %.1f\n", temperature);
printf("grade = %c\n", grade);
age = 22;
temperature = 37.0;
grade = 'B';
printf("updated -> age = %d, temperature = %.1f, grade = %c\n", age, temperature, grade);
return 0;
}Ожидаемый вывод:
age = 21
temperature = 36.6
grade = A
updated -> age = 22, temperature = 37.0, grade = BВ этом примере все переменные явно инициализируются. Если убрать инициализацию локальной переменной и сразу прочитать ее значение, программа формально становится некорректной с точки зрения стандарта.
Типы данных
Базовые типы: char, int, float, double плюс модификаторы short, long, unsigned. Размеры типов платформо-зависимы, поэтому в переносимом коде их проверяют через sizeof.
#include <stdio.h>
int main(void) {
printf("sizeof(char) = %zu\n", sizeof(char));
printf("sizeof(short) = %zu\n", sizeof(short));
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(long) = %zu\n", sizeof(long));
printf("sizeof(unsigned int) = %zu\n", sizeof(unsigned int));
printf("sizeof(float) = %zu\n", sizeof(float));
printf("sizeof(double) = %zu\n", sizeof(double));
return 0;
}Пример вывода:
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
sizeof(long) = 8
sizeof(unsigned int) = 4
sizeof(float) = 4
sizeof(double) = 8C23 bool, true, false
В C23 логический тип можно использовать напрямую, без прежней зависимости от <stdbool.h>. Это делает условия в коде читаемее и ближе к естественной логике предметной области.
#include <stdio.h>
int main(void) {
bool is_admin = true;
bool has_2fa = false;
printf("is_admin = %d\n", is_admin);
printf("has_2fa = %d\n", has_2fa);
printf("is_admin && !has_2fa = %d\n", is_admin && !has_2fa);
return 0;
}Ожидаемый вывод:
is_admin = 1
has_2fa = 0
is_admin && !has_2fa = 1Здесь видно, что логические выражения по-прежнему печатаются как 0/1, но семантика кода становится прозрачнее: условие описывает смысл напрямую, а не через целочисленные суррогаты.
C23 nullptr и nullptr_t
C23 вводит nullptr и nullptr_t. Это снимает старую неоднозначность, когда нулевой указатель часто записывали как 0 или NULL, что смешивало целочисленные и указательные контексты.
#include <stdio.h>
#include <stddef.h>
int main(void) {
int *p = nullptr;
nullptr_t np = nullptr;
printf("p == nullptr -> %d\n", p == nullptr);
printf("np == nullptr -> %d\n", np == nullptr);
return 0;
}Ожидаемый вывод:
p == nullptr -> 1
np == nullptr -> 1Эта запись полезна в ревью и сопровождении: по самому коду видно, что автор имеет в виду именно нулевой указатель, а не число.
size_t: размеры и индексы
Для размеров и индексов в C применяется size_t. В официальной документации GNU это прямо сформулировано как always an unsigned integer type
для результата sizeof: см. Type Size.
Практическое следствие: если вы считаете количество элементов, длину буфера, размер блока памяти или индексируете массив, size_t, а не int.
#include <stdio.h>
int main(void) {
int numbers[] = {10, 20, 30, 40, 50};
size_t n = sizeof(numbers) / sizeof(numbers[0]);
size_t i;
int sum = 0;
for (i = 0; i < n; i++) {
sum += numbers[i];
}
printf("n = %zu\n", n);
printf("sum = %d\n", sum);
return 0;
}Ожидаемый вывод:
n = 5
sum = 150Консольный вывод. Функция printf
Форматная строка printf состоит из обычных символов и спецификаторов конверсии. В GNU libc это сформулировано так: Characters in the template string ... are printed as-is
, а спецификаторы задают преобразование аргументов. Источник: Output Conversion Syntax.
В man-page синтаксис указан явно: %[argument$][flags][width][.precision][length modifier]conversion
. Это самая практичная схема для чтения любого сложного формата: printf(3).
Разбор по частям:
argument$: позиционная нумерация аргументов (например,%2$d). Полезно в локализации и форматах, где один и тот же аргумент используется несколько раз.flags: модификаторы поведения. `-` — выравнивание влево, `+` — всегда печатать знак для знаковых чисел, пробел — печатать пробел вместо `+` для положительных, `#` — альтернативная форма (`0x`, `0`, обязательная точка у некоторых float-форматов), `0` — заполнение нулями. В POSIX/glibc также встречаются locale-флаги'иI.width: минимальная ширина поля. Можно числом (`%10d`) или через `*` (`%*d`, ширина берется из аргумента типа `int`). Отрицательная ширина эквивалентна флагу `-`..precision: точность. Для целых это минимум цифр, для float — число цифр после точки (`f/e/E`) или число значащих цифр (`g/G`), для строк (`%s`) — максимум символов.length modifier: размер аргумента. Стандартные: `hh`, `h`, `l`, `ll`, `j`, `z`, `t`, `L`. Например, `%zu` для `size_t`, `%jd` для `intmax_t`, `%lld` для `long long`, `%Lf` для `long double`.conversion: вид представления значения. Частые: `d/i`, `u`, `o`, `x/X`, `f/F`, `e/E`, `g/G`, `a/A`, `c`, `s`, `p`, `n`, `%`.
По документации glibc у %n есть отдельная семантика: он stores the number of characters printed so far
и ничего не печатает. Источник: Other Output Conversions.
Критически важно подбирать формат под точный тип аргумента. Ошибка в спецификаторе при varargs-вызове может привести к неопределенному поведению.
#include <inttypes.h>
#include <stdio.h>
int main(void) {
int i = -42;
unsigned int u = 42;
double pi = 3.1415926535;
char text[] = "C23";
size_t n = 123;
long long big = 9223372036854775807LL;
intmax_t jm = -9000;
void *ptr = text;
int printed = 0;
printf("signed with sign+zero: [%+08d]\n", i);
printf("unsigned left aligned: [%-10u]\n", u);
printf("hex with #: [%#x]\n", u);
printf("oct with #: [%#o]\n", u);
printf("fixed precision: [%.3f]\n", pi);
printf("string precision: [%.*s]\n", 2, text);
printf("size_t with %%zu: [%zu]\n", n);
printf("long long with %%lld: [%lld]\n", big);
printf("intmax_t with %%jd: [%jd]\n", jm);
printf("pointer %%p: [%p]\n", ptr);
printf("percent sign: [%%]\n");
printf("abc%nXYZ\n", &printed);
printf("printed before %%n = %d\n", printed);
return 0;
}Ожидаемый вывод:
signed with sign+zero: [-0000042]
unsigned left aligned: [42 ]
hex with #: [0x2a]
oct with #: [052]
fixed precision: [3.142]
string precision: [C2]
size_t with %zu: [123]
long long with %lld: [9223372036854775807]
intmax_t with %jd: [-9000]
pointer %p: [0x...]
percent sign: [%]
abcXYZ
printed before %n = 3Константы
В C обычно используют три формы: const для типизированных констант времени выполнения, #define для макроподстановки на этапе препроцессора и enum для именованных целочисленных значений. Это разные инструменты, и у каждого своя зона ответственности.
Формальные правила
const-квалифицированный объект не является modifiable lvalue: менять его через это lvalue нельзя.- Перечислители (
enum) являются целочисленными константными выражениями и могут использоваться, например, вcase-метках. #defineработает на этапе препроцессора и не создает типизированный объект языка.- В контекстах, где требуется constant expression, значение должно удовлетворять правилам constant expressions стандарта.
Источник: N3096 (C23 draft).
#include <stdio.h>
#define MAX_USERS 100
enum Weekday {
MONDAY = 1,
TUESDAY,
WEDNESDAY
};
int main(void) {
const double pi = 3.1415926535;
int users = MAX_USERS;
enum Weekday day = TUESDAY;
printf("pi = %.5f\n", pi);
printf("MAX_USERS = %d\n", users);
printf("TUESDAY = %d\n", day);
return 0;
}Ожидаемый вывод:
pi = 3.14159
MAX_USERS = 100
TUESDAY = 2Если вам нужна строгая типизация и проверка компилятором, выбирайте const или enum. Макросы лучше оставлять для простых compile-time выражений и конфигурационных флагов.
Арифметические операции
Арифметика в C строится вокруг операторов +, -, *, /, %. Перед вычислением компилятор часто приводит типы операндов к общему типу. В GNU manual это сформулировано так: convert their operands to the common type before operating on them
. Источник: Common Type.
Семантика каждой операции:
a + b: складывает значения после приведения к общему типу.a - b: вычитает правый операнд из левого.-a: унарное отрицание значения.a * b: умножение после приведения типов.a / b: деление. Для целых - целочисленное деление.a % b: остаток от целочисленного деления.
Про деление в GNU manual есть прямая формулировка: The result is always rounded towards zero.
. Источник: Division and Remainder.
Важные ограничения:
- Деление на ноль для целых - неопределенное поведение.
- Переполнение знаковых целых - неопределенное поведение.
- Для
unsignedарифметика выполняется по модулю2^N.
Формальные правила
- Бинарные арифметические операторы сначала приводят операнды через usual arithmetic conversions.
- Для целочисленных
/и%частное округляется к нулю; остаток связан с частным стандартным соотношением деления с остатком. - Деление на ноль - неопределенное поведение.
- Переполнение signed integer в арифметических выражениях - неопределенное поведение.
Источник: N3096 (C23 draft).
#include <stdio.h>
int main(void) {
int a = 17;
int b = 5;
int neg = -17;
double x = 17.0;
double y = 5.0;
printf("a + b = %d\n", a + b);
printf("a - b = %d\n", a - b);
printf("a * b = %d\n", a * b);
printf("a / b (int) = %d\n", a / b);
printf("a %% b = %d\n", a % b);
printf("-17 / 5 (int) = %d\n", neg / b);
printf("-17 %% 5 (int) = %d\n", neg % b);
printf("x / y (double) = %.2f\n", x / y);
printf("2 + 3 * 4 = %d\n", 2 + 3 * 4);
printf("(2 + 3) * 4 = %d\n", (2 + 3) * 4);
return 0;
}Ожидаемый вывод:
a + b = 22
a - b = 12
a * b = 85
a / b (int) = 3
a % b = 2
-17 / 5 (int) = -3
-17 % 5 (int) = -2
x / y (double) = 3.40
2 + 3 * 4 = 14
(2 + 3) * 4 = 20Этот пример показывает целочисленное деление, знак остатка и влияние приоритета операций.
Условные операции
Условные выражения в C делятся на две группы: сравнения (==, !=, <, <=, >, >=) и логические операторы (!, &&, ||).
GNU manual формулирует результат логических выражений так: The result of a logical expression is always 1 or 0.
Источник: Logical Operators.
Подробная семантика операторов:
a == b: истина, если значения равны.a != b: истина, если значения различны.a < b,a <= b,a > b,a >= b: отношения порядка над числами.!x: возвращает 1, еслиxравно нулю, иначе 0.x && y: логическое И, правая часть вычисляется только если левая истинна.x || y: логическое ИЛИ, правая часть вычисляется только если левая ложна.
Важные граничные случаи:
- Для
doubleсNaN: сравнения==,<,>ложны, а!=истинно. - Порядковые сравнения указателей корректны только в пределах одного массива (или one-past).
Формальные правила
- Реляционные и equality-операторы возвращают целое значение 0 или 1.
&&и||вычисляют левый операнд первым; правый вычисляется только если это нужно для результата (short-circuit).!возвращает 1 для нуля и 0 для ненулевого значения.- Для указателей порядковые сравнения определены только в допустимых стандартом случаях (например, внутри одного массива).
Источник: N3096 (C23 draft).
#include <math.h>
#include <stdio.h>
int side_effect(void) {
puts("side effect happened");
return 1;
}
int main(void) {
bool can_enter = true;
int age = 20;
int balance = 50;
double nan = NAN;
int arr[3] = {10, 20, 30};
int *p = &arr[0];
int *q = &arr[2];
printf("age >= 18 -> %d\n", age >= 18);
printf("age == 21 -> %d\n", age == 21);
printf("age != 21 -> %d\n", age != 21);
printf("age < 30 -> %d\n", age < 30);
printf("age <= 20 -> %d\n", age <= 20);
printf("age > 30 -> %d\n", age > 30);
printf("(age >= 18 && can_enter) -> %d\n", age >= 18 && can_enter);
printf("(balance > 100 || can_enter) -> %d\n", balance > 100 || can_enter);
printf("!(age < 18) -> %d\n", !(age < 18));
printf("nan == nan -> %d\n", nan == nan);
printf("nan != nan -> %d\n", nan != nan);
printf("p < q (same array) -> %d\n", p < q);
printf("0 && side_effect() -> %d\n", 0 && side_effect());
printf("1 || side_effect() -> %d\n", 1 || side_effect());
return 0;
}Ожидаемый вывод:
age >= 18 -> 1
age == 21 -> 0
age != 21 -> 1
age < 30 -> 1
age <= 20 -> 1
age > 30 -> 0
(age >= 18 && can_enter) -> 1
(balance > 100 || can_enter) -> 1
!(age < 18) -> 1
nan == nan -> 0
nan != nan -> 1
p < q (same array) -> 1
0 && side_effect() -> 0
1 || side_effect() -> 1В последних двух строках функция side_effect() не вызовется ни разу: это и есть short-circuit семантика && и ||.
Поразрядные операции
Поразрядные операции работают с битами целого числа: &, |, ^, ~, <<, >>. GNU manual формулирует это кратко: Bitwise operators operate on integers, treating each bit independently.
. Источник: Bitwise Operations.
Семантика каждой операции:
~x: инвертирует каждый бит операнда.x & y: бит равен 1 только если он 1 в обоих операндах.x | y: бит равен 1, если он 1 хотя бы в одном операнде.x ^ y: бит равен 1, если биты различны.x << n: сдвиг влево наnбитов.x >> n: сдвиг вправо наnбитов.
Важные ограничения:
- Перед вычислением применяются integer promotions.
- Количество сдвига должно быть в диапазоне
0 .. (width-1); отрицательный сдвиг или сдвиг на ширину типа и больше - неопределенное поведение. - Для
unsignedсдвиги обычно предсказуемее; для отрицательных signed-значений правый сдвиг может быть implementation-defined.
Формальные правила
- Перед поразрядными операциями применяются integer promotions.
- Для сдвигов правый операнд должен быть неотрицательным и меньше ширины левого операнда в битах.
- Сдвиг signed-значения с выходом за диапазон может приводить к неопределенному поведению.
- Правый сдвиг отрицательных signed-значений может быть implementation-defined.
Источник: N3096 (C23 draft).
#include <stdio.h>
int main(void) {
unsigned int flags = 5; // 00000101
unsigned int mask = 4; // 00000100
unsigned int read_bit2 = (flags & mask) != 0;
unsigned int set_bit1 = flags | 2; // 00000111
unsigned int clear_bit0 = flags & ~1u; // 00000100
unsigned int toggle_bit2 = flags ^ 4; // 00000001
printf("flags & mask = %u\n", flags & mask);
printf("flags | mask = %u\n", flags | mask);
printf("flags ^ mask = %u\n", flags ^ mask);
printf("(unsigned char)~flags = %u\n", (unsigned int)(unsigned char)~flags);
printf("flags << 1 = %u\n", flags << 1);
printf("flags >> 1 = %u\n", flags >> 1);
printf("read bit2 = %u\n", read_bit2);
printf("set bit1 = %u\n", set_bit1);
printf("clear bit0 = %u\n", clear_bit0);
printf("toggle bit2 = %u\n", toggle_bit2);
return 0;
}Ожидаемый вывод:
flags & mask = 4
flags | mask = 5
flags ^ mask = 1
(unsigned char)~flags = 250
flags << 1 = 10
flags >> 1 = 2
read bit2 = 1
set bit1 = 7
clear bit0 = 4
toggle bit2 = 1Операции присваивания
Составные присваивания +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= сокращают запись и делают шаги обновления состояния явными.
Формальные правила
- Левая часть присваивания должна быть modifiable lvalue.
- При простом присваивании правая часть приводится к типу левой.
- Составное присваивание
E1 op= E2эквивалентноE1 = E1 op E2с тем отличием, чтоE1вычисляется только один раз. - Выражение присваивания имеет значение присвоенного результата после выполнения присваивания.
Источник: N3096 (C23 draft).
#include <stdio.h>
int main(void) {
int n = 10;
n += 5;
n -= 3;
n *= 2;
n /= 4;
n %= 3;
printf("n = %d\n", n);
unsigned int bits = 1; // 0001
bits <<= 2;
bits |= 2;
bits &= 7;
bits ^= 1;
bits >>= 1;
printf("bits = %u\n", bits);
return 0;
}Ожидаемый вывод: n = 0, bits = 3. Такой стиль особенно удобен в циклах и state-machine логике.
Преобразование типов
В C преобразования бывают неявные (automatic conversions) и явные (cast). Это одна из самых важных тем, потому что именно тут чаще всего появляются «тихие» ошибки.
В GNU manual базовое правило сформулировано так: C converts values from one data type to another automatically
. Источник: Type Conversions.
Практический порядок неявных преобразований в выражениях:
- Value transformations: lvalue-to-rvalue, array-to-pointer, function-to-pointer.
- Integer promotions: типы меньше
int(например,char,short) обычно поднимаются доintилиunsigned int. - Usual arithmetic conversions: два операнда приводятся к общему типу.
- Assignment/parameter conversions: при присваивании и передаче аргумента значение приводится к типу приемника.
Формальные правила
- В выражениях применяются value transformations, затем integer promotions, затем usual arithmetic conversions.
- При присваивании и передаче аргумента значение приводится к типу приемника.
T*иvoid*преобразуются взаимно; квалификаторы типа нельзя неявно отбрасывать.- Некоторые преобразования имеют ограничения: например, значение вне диапазона целевого signed-целого при cast из floating может дать неопределенное поведение.
Источник: N3096 (C23 draft).
Что обычно можно и корректно делать:
- Числовые преобразования между целыми и вещественными типами (с учетом возможной потери точности).
- Преобразование
T*вvoid*и обратно. - Преобразование нулевого указателя (
nullptr) к любому типу указателя. - Неявное добавление квалификаторов, например
int*вconst int*.
Что нельзя или опасно:
- Нельзя неявно отбросить квалификатор
const(например,const int*вint*). - Преобразование
doubleвintпри значении вне диапазона целевого типа ведет к неопределенному поведению. - Преобразование произвольного целого в указатель implementation-defined и часто непереносимо.
- Приведение указателя к несовместимому типу и последующее разыменование может нарушить aliasing/alignment правила.
#include <stdint.h>
#include <stdio.h>
#include <stddef.h>
int main(void) {
char c = 120;
short s = 1000;
int promoted = c + s; // char/short -> int
double mix = promoted + 0.25; // int -> double
int narrowed = (int)mix; // явное сужение
int a = 7;
int b = 2;
double wrong_division = a / b; // 3.0
double correct_division = (double)a / b; // 3.5
int truncated = (int)3.99; // 3 (дробная часть отброшена)
int value = 123;
void *vp = &value; // int* -> void*
int *back = (int *)vp; // void* -> int*
uintptr_t raw = (uintptr_t)back;
int *ptr = nullptr;
const int *pc = &value; // int* -> const int* (разрешено)
// int *bad = pc; // нельзя: отброс const без явного приведения
printf("promoted = %d\n", promoted);
printf("mix = %.2f\n", mix);
printf("narrowed = %d\n", narrowed);
printf("wrong_division = %.1f\n", wrong_division);
printf("correct_division = %.1f\n", correct_division);
printf("truncated = %d\n", truncated);
printf("*back = %d\n", *back);
printf("raw pointer as integer = %zu\n", (size_t)raw);
printf("ptr == nullptr -> %d\n", ptr == nullptr);
return 0;
}Этот пример показывает сразу цепочку преобразований: promotions, usual arithmetic conversions, явные сужающие cast и работу с указателями.
Условные конструкции
Основные конструкции управления ветвлением: if / else if / else, switch-case и тернарный оператор ?:. Каждая нужна в своем контексте: от простых проверок до ветвления по дискретным кодам состояния.
Формальные правила
- У
ifусловие должно иметь scalar type. - В
switchуправляющее выражение должно иметь integer type;case-метки должны быть целочисленными константными выражениями. - После преобразования к типу управляющего выражения значения
caseне должны дублироваться. - В
E1 ? E2 : E3вычисляется только одна из ветвейE2/E3, в зависимости от результатаE1.
Источник: N3096 (C23 draft).
if / else if / else
#include <stdio.h>
int main(void) {
int score = 82;
if (score >= 90) {
printf("grade A\n");
} else if (score >= 75) {
printf("grade B\n");
} else {
printf("grade C\n");
}
return 0;
}switch-case
#include <stdio.h>
int main(void) {
int day = 3;
switch (day) {
case 1:
printf("Monday\n");
break;
case 2:
printf("Tuesday\n");
break;
case 3:
printf("Wednesday\n");
break;
default:
printf("Unknown day\n");
break;
}
return 0;
}Тернарный оператор
#include <stdio.h>
int main(void) {
int number = 9;
const char *parity = (number % 2 == 0) ? "even" : "odd";
printf("%d is %s\n", number, parity);
return 0;
}Если ветвление небольшое и возвращает одно выражение, тернарный оператор делает код короче. Если логика сложная, лучше оставить обычный if ради читаемости.
Циклы
В C есть три базовых цикла: for, while, do...while. Дополнительно break завершает цикл досрочно, а continue пропускает текущую итерацию.
Формальные правила
whileпроверяет условие перед каждой итерацией.do...whileпроверяет условие после тела, поэтому тело выполняется минимум один раз.- В
for(init; cond; iter)этапы выполняются в порядке init → cond → body → iter. breakзавершает ближайший цикл/switch,continueпередает управление на следующую итерацию ближайшего цикла.
Источник: N3096 (C23 draft).
for
#include <stdio.h>
int main(void) {
int sum = 0;
for (int i = 1; i <= 5; i++) {
sum += i;
}
printf("sum = %d\n", sum);
return 0;
}while + break/continue
#include <stdio.h>
int main(void) {
int i = 0;
while (i < 10) {
i++;
if (i % 2 != 0) {
continue;
}
if (i > 6) {
break;
}
printf("%d ", i);
}
printf("\n");
return 0;
}Ожидаемый вывод: 2 4 6.
do...while
#include <stdio.h>
int main(void) {
int n = 0;
do {
printf("n = %d\n", n);
n++;
} while (n < 3);
return 0;
}Ключевая особенность do...while: тело выполняется минимум один раз, потому что условие проверяется после выполнения блока.
Массивы и строки
Про массивы в официальном руководстве есть точная формулировка: An array is a data object that holds a series of elements, all of the same data type.
Источник: GNU Arrays.
Для строк официальное правило: строка должна быть terminated with the null character
. Источник: GNU Strings. Это правило важно при вводе, копировании и печати строк.
#include <stdio.h>
int main(void) {
int numbers[] = {10, 20, 30, 40, 50};
size_t n = sizeof(numbers) / sizeof(numbers[0]);
int total = 0;
for (size_t i = 0; i < n; i++) {
total += numbers[i];
}
char name[] = "Alice"; // {'A','l','i','c','e','\0'}
printf("n = %zu\n", n);
printf("total = %d\n", total);
printf("name = %s\n", name);
printf("name length by sizeof = %zu\n", sizeof(name) - 1);
return 0;
}Ожидаемый вывод:
n = 5
total = 150
name = Alice
name length by sizeof = 5Важно: для массивов не хранится отдельная длина, поэтому ее обычно вычисляют формулой sizeof(arr) / sizeof(arr[0]) в той же области, где массив доступен как массив, а не как указатель.
Ввод в консоли. Функция scanf
Для scanf в официальной документации есть ключевая фраза: The return value is normally the number of successful assignments.
Источник: Formatted Input Functions.
Это правило нужно использовать всегда: проверяйте возвращаемое значение scanf, иначе программа может продолжить работу с неинициализированными данными после частично неудачного ввода.
#include <stdio.h>
int main(void) {
int age;
double height;
char name[32];
size_t copies;
printf("Enter age, height, name, copies: ");
int read = scanf("%d %lf %31s %zu", &age, &height, name, &copies);
if (read != 4) {
printf("Input error: expected 4 values, got %d\n", read);
return 1;
}
printf("age = %d\n", age);
printf("height = %.2f\n", height);
printf("name = %s\n", name);
printf("copies = %zu\n", copies);
return 0;
}Почему %31s: это ограничитель длины для буфера name[32]. Почему %zu: переменная copies имеет тип size_t, и формат должен точно соответствовать типу.
Пример сессии ввода/вывода:
Enter age, height, name, copies: 25 181.4 Alex 3
age = 25
height = 181.40
name = Alex
copies = 3