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 вручную, демонстрируя, как строки обрабатываются через указатели на символы и завершающий нуль.