Перейти к основному содержимому

5.16. Типы данных

Разработчику Архитектору

Типы данных

Язык программирования Си строится на понятии типов данных — фундаментальных категорий, определяющих, какие значения может принимать переменная, сколько памяти она занимает и какие операции над ней допустимы. В Си типы данных делятся на две большие группы: примитивные (или базовые) и производные. Примитивные типы предоставляются самим языком и не требуют дополнительного определения. Производные типы создаются на основе примитивных и позволяют моделировать более сложные структуры.

Примитивные типы данных в Си — это int, char, float, double и void. Каждый из них играет свою роль в организации данных и логики программы.


Целочисленный тип: int

Тип int предназначен для хранения целых чисел — значений без дробной части. Это один из самых часто используемых типов в программах на Си. Размер типа int зависит от архитектуры процессора и компилятора, но в большинстве современных систем он занимает 4 байта (32 бита), что позволяет хранить значения от -2 147 483 648 до 2 147 483 647.

Переменная типа int объявляется с помощью ключевого слова int, за которым следует имя переменной:

int age = 25;

Этот код создаёт переменную с именем age, присваивает ей значение 25 и резервирует в памяти место, достаточное для хранения одного целого числа.

Простой пример

#include <stdio.h>

int main() {
int score = 100;
printf("Ваш счёт: %d\n", score);
return 0;
}

Программа выводит:

Ваш счёт: 100

Здесь используется спецификатор формата %d, предназначенный для вывода целых чисел.

Сложный пример

#include <stdio.h>

int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}

int main() {
int number = 7;
int result = factorial(number);
printf("Факториал числа %d равен %d\n", number, result);
return 0;
}

Эта программа вычисляет факториал числа с помощью рекурсивной функции. Все переменные и возвращаемые значения имеют тип int, что подчёркивает его универсальность в арифметических вычислениях.


Символьный тип: char

Тип char предназначен для хранения одного символа. Внутри компьютера символы представлены числовыми кодами согласно таблице ASCII или её расширениям. Размер типа char всегда равен 1 байту, что позволяет ему хранить значения от -128 до 127 (если char знаковый) или от 0 до 255 (если беззнаковый). По умолчанию поведение зависит от компилятора, но чаще всего char рассматривается как знаковый.

Объявление переменной типа char выглядит так:

char letter = 'A';

Символ заключается в одинарные кавычки, чтобы отличать его от строки.

Простой пример

#include <stdio.h>

int main() {
char initial = 'T';
printf("Ваша инициал: %c\n", initial);
return 0;
}

Вывод:

Ваша инициал: T

Используется спецификатор %c для вывода одного символа.

Сложный пример

#include <stdio.h>

int main() {
char buffer[10];
buffer[0] = 'H';
buffer[1] = 'e';
buffer[2] = 'l';
buffer[3] = 'l';
buffer[4] = 'o';
buffer[5] = '\0'; // завершающий нуль — признак конца строки

printf("Сообщение: %s\n", buffer);
return 0;
}

Хотя здесь используется массив символов, каждый элемент массива имеет тип char. Этот пример демонстрирует, как отдельные символы собираются в строку — важнейшую концепцию в Си, где строки реализованы как последовательности значений типа char.


Вещественные типы: float и double

Для работы с числами, имеющими дробную часть, в Си предусмотрены два типа: float и double. Оба представляют числа с плавающей запятой, но отличаются точностью и размером.

  • Тип float занимает 4 байта и обеспечивает точность около 6–7 десятичных цифр.
  • Тип double занимает 8 байт и обеспечивает точность около 15–16 десятичных цифр.

По умолчанию вещественные литералы в Си имеют тип double. Чтобы явно указать float, к числу добавляют суффикс f.

Простой пример с float

#include <stdio.h>

int main() {
float price = 19.99f;
printf("Цена: %.2f рублей\n", price);
return 0;
}

Вывод:

Цена: 19.99 рублей

Спецификатор %.2f ограничивает вывод двумя знаками после запятой.

Простой пример с double

#include <stdio.h>

int main() {
double pi = 3.141592653589793;
printf("Число Пи: %.10f\n", pi);
return 0;
}

Вывод:

Число Пи: 3.1415926536

Сложный пример: сравнение точности

#include <stdio.h>

int main() {
float a = 1.0f / 3.0f;
double b = 1.0 / 3.0;

printf("float: %.10f\n", a);
printf("double: %.10f\n", b);
return 0;
}

Вывод может быть таким:

float:  0.3333333433
double: 0.3333333333

Разница в точности становится заметной при высоких требованиях к вычислениям — например, в научных расчётах или графике.


Специальный тип: void

Тип void означает «отсутствие значения». Он не может использоваться для объявления переменных, потому что не представляет никаких данных. Однако void играет важную роль в контексте функций и указателей.

  • Функция, возвращающая void, ничего не возвращает вызывающему коду.
  • Указатель типа void* может указывать на данные любого типа, что делает его универсальным инструментом для работы с памятью.

Простой пример: функция без возврата

#include <stdio.h>

void greet() {
printf("Привет, мир!\n");
}

int main() {
greet();
return 0;
}

Функция greet объявлена с возвращаемым типом void, потому что её задача — только вывести сообщение, а не вернуть результат.

Сложный пример: универсальный указатель

#include <stdio.h>
#include <stdlib.h>

int main() {
int number = 42;
void* ptr = &number; // указатель на любые данные

// Чтобы прочитать значение, нужно привести тип
int* int_ptr = (int*)ptr;
printf("Значение через void*: %d\n", *int_ptr);

return 0;
}

Указатель void* часто используется в стандартной библиотеке Си — например, в функциях malloc и memcpy, которые работают с блоками памяти независимо от их содержимого.


Производные типы данных

Если примитивные типы — это кирпичи, то производные типы — это здания, построенные из этих кирпичей. Они позволяют организовывать данные в более сложные и осмысленные формы, адаптированные под конкретные задачи программы. В Си к производным типам относятся: массивы, указатели, структуры (struct) и объединения (union). Все они создаются на основе примитивных типов, но обладают собственными правилами и возможностями.


Массивы

Массив — это упорядоченная последовательность элементов одного и того же типа, хранящихся в смежных участках памяти. Каждый элемент доступен по индексу, начиная с нуля. Массивы в Си имеют фиксированный размер, который определяется на этапе компиляции (если не используется динамическое выделение памяти).

Объявление массива включает тип элементов, имя массива и количество элементов в квадратных скобках:

int numbers[5]; // массив из пяти целых чисел

После объявления можно присваивать значения отдельным элементам:

numbers[0] = 10;
numbers[1] = 20;
// ... и так далее

Простой пример

#include <stdio.h>

int main() {
int days[7] = {1, 2, 3, 4, 5, 6, 7};
printf("Первый день недели: %d\n", days[0]);
return 0;
}

Программа выводит:

Первый день недели: 1

Инициализация массива при объявлении позволяет задать начальные значения всем или части его элементов.

Сложный пример: работа с двумерным массивом

#include <stdio.h>

int main() {
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", matrix[i][j]);
}
printf("\n");
}
return 0;
}

Вывод:

1 2 3 
4 5 6
7 8 9

Двумерный массив — это массив массивов. Он часто применяется для представления таблиц, изображений или координатных сеток.


Указатели

Указатель — это переменная, которая хранит адрес другой переменной в памяти. Указатели лежат в основе многих механизмов языка Си: передача аргументов в функции по ссылке, динамическое выделение памяти, работа со строками и реализация сложных структур данных.

Тип указателя определяет, на данные какого типа он может указывать. Например, int* — это указатель на целое число, char* — на символ.

Объявление указателя:

int value = 42;
int* ptr = &value; // & — оператор взятия адреса

Чтобы получить значение по адресу, используется оператор разыменования *:

int copy = *ptr; // copy получит значение 42

Простой пример

#include <stdio.h>

int main() {
int number = 100;
int* p = &number;

printf("Значение: %d\n", *p);
printf("Адрес: %p\n", (void*)p);
return 0;
}

Программа выводит значение переменной и её адрес в памяти.

Сложный пример: изменение переменной через указатель

#include <stdio.h>

void increment(int* x) {
(*x)++;
}

int main() {
int counter = 5;
printf("До: %d\n", counter);
increment(&counter);
printf("После: %d\n", counter);
return 0;
}

Вывод:

До: 5
После: 6

Функция increment принимает указатель на целое число и увеличивает значение по этому адресу. Без указателей такая модификация была бы невозможна, потому что Си передаёт аргументы в функции по значению.


Структуры (struct)

Структура — это составной тип данных, объединяющий несколько переменных разных типов под одним именем. Это мощный инструмент для моделирования реальных объектов: например, человека, автомобиля или записи в базе данных.

Объявление структуры начинается с ключевого слова struct, за которым следует имя структуры и список полей в фигурных скобках:

struct Person {
char name[50];
int age;
float height;
};

После объявления можно создавать переменные этого типа:

struct Person alice;
strcpy(alice.name, "Алиса");
alice.age = 30;
alice.height = 165.5f;

Простой пример

#include <stdio.h>
#include <string.h>

struct Book {
char title[100];
int pages;
};

int main() {
struct Book b;
strcpy(b.title, "Война и мир");
b.pages = 1225;

printf("Книга: %s, страниц: %d\n", b.title, b.pages);
return 0;
}

Вывод:

Книга: Война и мир, страниц: 1225

Сложный пример: массив структур и функция обработки

#include <stdio.h>
#include <string.h>

struct Student {
char name[30];
int grade;
};

void print_top_students(struct Student students[], int count) {
for (int i = 0; i < count; i++) {
if (students[i].grade >= 90) {
printf("Отличник: %s (оценка: %d)\n", students[i].name, students[i].grade);
}
}
}

int main() {
struct Student group[] = {
{"Иван", 85},
{"Мария", 95},
{"Анна", 92}
};
int size = sizeof(group) / sizeof(group[0]);

print_top_students(group, size);
return 0;
}

Вывод:

Отличник: Мария (оценка: 95)
Отличник: Анна (оценка: 92)

Этот пример демонстрирует, как структуры позволяют группировать связанные данные и эффективно работать с коллекциями таких объектов.


Объединения (union)

Объединение — это специальный составной тип, все поля которого разделяют одну и ту же область памяти. Размер объединения равен размеру его самого большого поля. В каждый момент времени объединение может хранить значение только одного из своих полей.

Объявляется объединение с помощью ключевого слова union:

union Data {
int i;
float f;
char str[20];
};

Если записать в str, то предыдущие значения i и f будут утеряны, потому что вся память переиспользуется.

Простой пример

#include <stdio.h>
#include <string.h>

union Value {
int integer;
float real;
};

int main() {
union Value v;
v.integer = 42;
printf("Целое: %d\n", v.integer);

v.real = 3.14f;
printf("Вещественное: %.2f\n", v.real);
// Значение integer теперь недействительно
return 0;
}

Вывод:

Целое: 42
Вещественное: 3.14

Сложный пример: интерпретация байтов

#include <stdio.h>

union ByteView {
unsigned int num;
unsigned char bytes[4];
};

int main() {
union ByteView data;
data.num = 0x12345678;

printf("Байты (младший к старшему): ");
for (int i = 0; i < 4; i++) {
printf("%02X ", data.bytes[i]);
}
printf("\n");
return 0;
}

На машине с архитектурой little-endian вывод будет:

Байты (младший к старшему): 78 56 34 12 

Объединения позволяют безопасно исследовать внутреннее представление данных без приведения типов указателей, что особенно полезно в системном программировании и работе с протоколами.


Строки в Си: массивы символов

В языке Си нет встроенного строкового типа. Вместо этого строки представляются как массивы символов, завершающиеся специальным нулевым символом \0 (null terminator). Этот символ сигнализирует о конце строки и обязателен для корректной работы стандартных функций, таких как printf, strlen, strcpy.

Например:

char message[] = "Привет";

Компилятор автоматически добавляет \0 в конец, поэтому фактический размер массива — 7 байт (6 букв + 1 завершающий нуль).

Простой пример

#include <stdio.h>

int main() {
char greeting[] = "Здравствуйте!";
printf("%s\n", greeting);
return 0;
}

Вывод:

Здравствуйте!

Сложный пример: ручное управление строкой

#include <stdio.h>

int string_length(char* s) {
int len = 0;
while (*s != '\0') {
len++;
s++;
}
return len;
}

int main() {
char text[] = "Си — язык близкий к железу";
int len = string_length(text);
printf("Длина строки: %d символов\n", len);
return 0;
}

Эта программа реализует аналог функции strlen вручную, демонстрируя, как строки обрабатываются через указатели на символы и завершающий нуль.