Си — это язык программирования, который был разработан в начале 1970-х годов. Он стал основой для многих других языков, таких как C++, Java и Python. Си известен своей эффективностью и низким уровнем абстракции, что позволяет программистам работать ближе к аппаратному обеспечению.
Где сейчас используется Си?
Си по-прежнему широко используется в различных областях, включая:
- Разработка операционных систем (например, Linux)
- Встраиваемые системы (например, прошивки для микроконтроллеров)
- Игровая индустрия (например, Unreal Engine)
- Научные вычисления (например, библиотеки для численных методов)
- Системное программирование (например, драйверы устройств)
- Разработка компиляторов и интерпретаторов
- Сетевое программирование (например, реализация протоколов)
- Разработка баз данных (например, SQLite)
- Разработка графических интерфейсов (например, GTK+)
Почему вообще нужно изучать эту древность?
На самом деле у каждого человека свои причины изучить Си. Лично для себя я выделил несколько причин:
- Си является основой для многих современных языков программирования, таких как C++, Java и Python. Изучение Си поможет мне лучше понимать как работают другие языки программирования;
- Си это низкоуровневый язык, который позволяет мне работать ближе к аппаратному обеспечению. Это полезно для понимания работы компьютера и операционных систем;
Среда разработки
Для того чтобы писать на Си вам нужен любой редактор (практически все редакторы сходу поддерживают Си) и компилятор. Во многих системах компилятор для Си уже установлен. Проверить установлен ли компилятор можно с помощью следующей команды:
gcc -v
В случае если компилятор не установлен, мы можем установить его:
# MacOS
brew install gcc
# Ubuntu/Debian
sudo apt install build-essential
# Fedora/RHEL
sudo dnf groupinstall "Development Tools"
sudo dnf group install development-tools # Fedora 41+
# Arch
sudo pacman -S base-devel
# Windows
choco install mingw -y
Структура программы
Программа на Си состоит из функций. Каждая программа должна содержать функцию main
, которая является точкой входа в программу. Вот пример простой программы на Си:
Hello, World!
Давайте разберем данный кусок кода по частям:
Переменные и типы данных
Си сам по себе достаточно минималистичный и маленький язык программирования, у него есть необходимый минимум возможностей, но не более.
В Си есть несколько основных типов данных:
Категория | Описание | Примеры типов |
---|---|---|
Целые числа (Integers) | Целые числа, могут быть положительными или отрицательными. | char , int , short , long , long long |
Беззнаковые целые (Unsigned integers) | Целые числа, могут быть только положительными (или ноль). | unsigned char , unsigned int , unsigned short , unsigned long , unsigned long long |
Числа с плавающей точкой (Floating point numbers) | Вещественные числа (с дробной частью). Используются для точных вычислений. | float , double |
Структуры (Structures) | Пользовательские типы, объединяющие разные переменные под одним именем. Будут рассмотрены далее. | struct MyStruct (будет объяснено позже) |
Пустой тип данных | Данный тип указывает на отсутствие каких-либо данных | void |
В Си нет типа данных для строк, но строки можно представлять как массивы символов. Например, строка “Hello” может быть представлена как массив char
.
Тип | Размер (байт) | Диапазон значений |
---|---|---|
char | 1 | -128 до 127 |
int | 4 | -2,147,483,648 до 2,147,483,647 |
float | 4 | ±1.2E-38 до ±3.4E+38 |
double | 8 | ±2.3E-308 до ±1.7E+308 |
Сам синтаксис объявления переменных в Си выглядит следующим образом:
тип_данных имя_переменной [?= значение];
Вот пример объявления переменных:
int a;
int b;
В примере вверху мы просто объявили две переменные без каких-либо значений. Мы можем объявить переменные и сразу же инициализировать их, как это сделать показано внизу:
15
printf
На протяжении данного туториала мы будем использовать функцию printf
, данная функция выводит данные в стандартный поток вывода.
Для того чтобы использовать данную функцию, мы должны импортировать стандартную библиотеку для ввода и вывода:
#include <stdio.h>
После того как мы импортировали стандартную библиотеку для ввода и вывода мы можем использовать printf
. В саму функцию мы не можем передать случайные данные, ибо первым аргументом всегда должна быть строка:
#include <stdio.h>
int main() {
printf(213); // Вызовет ошибку компиляции
printf(""); // Сработает нормально
}
В первом аргументе на самом деле заключена не просто строка, а строка форматирования. В нее мы можем передавать специальные знаки, которые будут заменяться данными, такие знаки называются спецификаторами формата.
Спецификатор | Значение |
---|---|
%d | Выводит целое число в десятичном формате (знак учитывается) |
%i | То же, что и %d, целое число в десятичном формате |
%u | Выводит беззнаковое целое число в десятичном формате |
%f | Выводит число с плавающей запятой (десятичное дробное число) |
%c | Выводит одиночный символ |
%s | Выводит строку символов (массив char, заканчивающийся \0) |
%x | Выводит целое число в шестнадцатеричном формате (строчные буквы) |
%X | Выводит целое число в шестнадцатеричном формате (прописные буквы) |
%o | Выводит целое число в восьмеричном формате |
%p | Выводит указатель (адрес в памяти) |
%% | Выводит символ % |
Мы можем выводить значения с помощью спецификаторов формата:
10, 2.20000, c
Операторы
В Си есть все необходимые операторы для работы с числовыми данными и не только, внизу перечислен каждый из них:
Оператор | Название | Пример | Описание |
---|---|---|---|
+ | Сложение | a + b | Складывает два значения |
- | Вычитание | a - b | Вычитает второе значение из первого |
* | Умножение | a * b | Умножает два значения |
/ | Деление | a / b | Делит первое значение на второе |
% | Остаток от деления | a % b | Возвращает остаток от целочисленного деления |
Размерность типа данных
В Си есть специальный оператор sizeof
, который позволяет узнать размер типа данных в байтах.
В будущем нам все время нужно будет работать с их размерностью, этот оператор будет нам очень полезен:
Тайп-кастинг (приведение типов)
Си не обладает возможностью автоматического приведения типов, мы можем рассмотреть это на следующем примере:
Для того чтобы преобразовать типы на лету, мы можем указать нужный нам тип в круглых скобочках:
У приведения типов, естественно, есть ограничения, среди которых:
- Приведение типов может привести к потере данных. Например, если вы преобразуете
double
вint
, дробная часть будет отброшена. - Приведение типов может вызвать переполнение или потерю значений. Например, преобразование большого
long
числа вshort
может привести к обрезанию данных. - Приведение типов может привести к неопределенному поведению. Например, если вы преобразуете указатель (о них мы поговорим позже) на один тип в указатель на другой тип, это может привести к ошибкам доступа к памяти.
- Приведение типов может быть неэффективным. Например, преобразование
float
вint
может потребовать дополнительных вычислений и замедлить выполнение программы. - Приведение типов может привести к ошибкам компиляции. Например, если вы пытаетесь преобразовать указатель на функцию в указатель на другой тип функции, это приведет к ошибке компиляции.
- Приведение типов может нарушить выравнивание данных. Например, преобразование указателя на структуру с одним выравниванием в указатель на структуру с другим выравниванием может вызвать ошибки при доступе к данным.
- Приведение типов может затруднить отладку. Ошибки, связанные с некорректным приведением типов, часто сложно обнаружить и исправить, особенно в больших проектах.
Массивы и строки
В Си строки тесно связаны с массивами. Массивы же, в свою очередь, тесно связаны с типами данных и размерностями
На этом этапе мы научимся объявлять и инициализировать одномерные и двумерные массивы, эффективно итерироваться по ним, а также работать со строками и базовыми функциями из стандартной библиотеки string.h
.
Массивы
Массив — это базовая структура данных, предназначенная для хранения фиксированного количества элементов одного типа. Все элементы массива располагаются в памяти последовательно, что обеспечивает быстрый доступ по индексу. Массивы позволяют удобно представлять и обрабатывать наборы данных, таких как список чисел, координаты, таблицы и другие последовательности.
Вот пример инициализации и использования массивов:
Для того чтобы изменить значение в ячейке массива или достать определенное значение из массива, мы должны использовать индекс. Индексы в Си начинаются с нуля, мы можем обратиться к определенной ячейке массива указав индекс в квадратных скобках:
3
20
Строки
Строки в Си не являются отдельным типом данных — они реализованы как массивы символов, завершаемые специальным нулевым символом '\0'
. Этот символ служит маркером конца строки и позволяет стандартным функциям определять её длину, копировать, сравнивать и объединять строки.
char str[] = "Hello"; // то же самое, что {'H', 'e', 'l', 'l', 'o', '\0'}
Для того чтобы работать со строками в Си, нам предоставлена библиотека string.h
:
#include <stdio.h>
#include <string.h>
int main() {
char name[20] = "John";
char greeting[50];
// Копирование значения в строчный массив
strcpy(greeting, "Hello, ");
// Конкатенация строк
strcat(greeting, name);
printf("%s\n", greeting); // Выведет "Hello, John"
}
В string.h
реализованы следующие функции:
strlen(char[])
— данная функция возвращает длину строки не учитывая\\0
;strcpy(dest char[], s char[])
— данная функция копирует строкуs
в строкуdest
;strcat(dest char[], s char[])
— данная функция конкатенирует строкиdest
иs
;strcmp(s1 char[], s2 char[])
— сравнивает строки и возвращает0
, если они идентичны;
Сравнивать строки напрямую с помощью оператора ==
нельзя — это сравнение адресов в памяти, а не содержимого.
Для корректного сравнения используется функция strcmp
.
Управляющие конструкции
Си предоставляет разработчику полный набор управляющих конструкций, необходимых для построения логики выполнения программы.
К основным управляющим конструкциям в C относятся:
- Условные операторы — применяются для выбора направления выполнения программы в зависимости от заданного условия;
- Циклы — используются для повторного выполнения блока кода до тех пор, пока соблюдаются определённые условия;
- Оператор выбора — предоставляет способ обработки различных значений одного выражения и исполнения соответствующего кода для каждого случая, часто применяясь в структурах меню или при множественном ветвлении;
- Операторы прерывания — позволяют досрочно завершить выполнение цикла или пропустить текущую итерацию;
- Оператор перехода — позволяет осуществлять переход к произвольной метке в пределах функции;
Условия
Условия в C реализованы с помощью операторов if
, else if
и else
. Данные операторы позволяют выполнять определенные блоки кода в зависимости от истинности или ложности условия.
Истинными условиями в Си являются все значения, кроме нуля.
Обычно в других языках программирования существуют булев тип данных, который может принимать только два значения: true
и false
. В Си же все немного иначе, в нем нет булевого типа данных, но мы можем использовать целые числа для представления истинности или ложности.
Булев тип данных и директивы препроцессора
Директива препроцессора #define
используется для определения макросов — именованных констант или шаблонов кода, которые заменяются на этапе препроцессинга перед компиляцией.
Например, мы создадим макрос, который будет заменять все значения tokiory
на “Даниил”, вот как это будет выглядеть:
Мы можем использовать #define
для определения булевых значений:
Циклы
В Си реализованы несколько типов циклов, которые можно использовать с помощью ключевых слов while
, do ... while
и for
.
Цикл с предусловием
Начнем с простого, while
это цикл, который будет выполнять код до тех пор, пока условие внутри скобочек истинно:
Текущий индекс: 1
Текущий индекс: 2
Текущий индекс: 3
Текущий индекс: 4
Оператор ++
в сниппете выше, увеличивает значение i
на 1
. Это необходимо для того, чтобы мы не попали в вечный цикл.
Если же мы специально хотим попасть в вечный цикл, то это можно сделать просто пробросив в скобки условие, которое всегда будет истинно:
int i = 0;
while (1) {
printf("Текущий индекс: %d", i);
i++;
}
Текущий индекс: 1
Текущий индекс: 2
Текущий индекс: 3
Текущий индекс: 4
Текущий индекс: 5
Текущий индекс: 6
Текущий индекс: 7
… (до бесконечности или пока i не переполнится)
Цикл с постусловием
Цикл do while
занимается примерно тем же, что и while
. Их различие состоит только в том, что do while
всегда выполняется хотя бы один раз.
Использование данного цикла распространено в геймдеве, где определенному действию нужна хотя бы одна итерация:
Цикл с параметрами
Цикл for
является самым подробным циклом, он позволяет объявить и инициализировать переменную, указать условие в рамках которого цикл будет работать и указать что и как будет изменяться после каждой итерации цикла. Рассмотрим пример:
Текущая итерация: 2
Текущая итерация: 3
Текущая итерация: 4
Текущая итерация: 5
int i = 0
является инициализацией, в данном блоке мы можем объявить переменную с любым названием для использования в цикле. После того как цикл закончится, переменная перестанет существовать;i < 5
является условием цикла, по которому будет работать цикл, пока данное условие истинно – цикл будет продолжаться;i++
является выражением итерации, данное выражение будет выполняться после каждой итерации;
Операторы прерывания
В любом из вышеперечисленных циклов можно использовать операторы прерывания. Данные операторы нужны для того чтобы прерывать выполнение итераций и в зависимости от указанного оператора продолжать цикл со следующей итерации или выходить из цикла.
Всего в Си два ключевых слова для прерывания циклов:
break
— ключевое слово, которое используется для того чтобы немедленно завершить цикл;continue
— ключевое слово, которое используется для того чтобы пропустить текущую итерацию;
Текущая итерация: 2
Текущая итерация: 4
Текущая итерация: 6
Текущая итерация: 8
Оператор выбора
Конструкцией выбора в Си является блок кода, где используется ключевое слово switch
. Данный оператор позволяет выбрать один из блоков кода в зависимости от выполнения условия:
switch
— это ключевое слово, которое указывает на то, что мы сейчас будем выбирать один из вариантов выполнения кода.case
— ключевое слово, которое говорит нам о том, чему должна быть равна переменная или выражение в скобках возлеswitch
для того чтобы выполнился блокcase
;default
— это блок кода, который выполнится, если ни одно из условий изcase
не было удовлетворено;
Примечательно, что тут используется оператор прерывания break
, который мы рассматривали до этого.
Если мы не укажем его, то при удовлетворении условий одного из case
, все последующие блоки case
тоже будут выполнены:
В данном случае a равно трём
В данном случае a равно чему-то другому
Оператор перехода
Оператор перехода — это низкоуровневая управляющая конструкция, позволяющая передавать управление не по логике программы, а непосредственно в заданное место кода.
Использовать оператор перехода стоит только в очень крайних случаях, когда кровь из носа как нужно пропустить определенный блок кода.
Но почему мне не стоит его использовать?
Использование данного оператора сильно усложнит вам и вашим коллегам жизнь при отладке кода, да и в целом оно сильно усложняет логику.
Оператор перехода в Си реализован через ключевое слово goto
и метки:
Закончили вычисление
Функции
Функции — это базовые строительные блоки программы, позволяющие структурировать код, повысить читаемость, переиспользуемость и модульность. Функции в Си могут принимать аргументы и отдавать определенные значения.
В Си функции не являются объектами первого класса, то есть вы не можете передавать функции в другие функции через аргументы, но об этом мы поговорим позже, когда доберемся до указателей.
Функции в Си объявляются с помощью следующего синтаксиса:
[возвращаемый тип данных] [название функции]([?аргументы]) {
[код]
}
Давайте напишем простую функцию, заачей которой будет просто выводить слово "Привет!"
, данная функция не будет принимать никаких входных данных и ничего не будет отдавать:
Для того чтобы вызвать нашу функцию, мы должны указать её имя и поставить круглые скобочки.
В круглые скобки можно будет передавать аргументы согласно параметрам, об этом мы поговорим чуть-чуть позже. Пока что мы можем просто указать пустые скобки (то есть ничего в них не передавать), для того чтобы просто вызвать функцию без аргументов:
Привет!
Привет!
Привет!
Аргументы и возвращаемые значения
Функции в Си могут получать аргументы и отдавать возвращаемые значения. При объявлении функции мы указываем тип, который наша функция должна вернуть, данный тип называется типом возвращаемого значения. Мы можем вернуть значение из функции с помощью ключевого слова return
:
double get_five() {
return 5.12;
}
int main() {
printf("%f", get_five()); // 5.12000
}
Например, функция main
, которую мы уже не один раз писали должна возвращать число. Мы все время возвращали его с помощью return 0
, однако,
если мы явно не укажем число возвращаемое число с помощью ключевого слова return
, то функция будет возвращать нулевое значение.
В случае с возвращаемым типом int
будет возвращаться 0
(даже без использования ключевого слова return
):
Параметры функции — это переменные, которые объявляются в определении функции и служат для получения данных, с которыми функция будет работать. Они указываются в круглых скобках после имени функции и позволяют сделать функцию универсальной, чтобы она могла выполнять действия с разными входными данными.
Давайте создадим простую функцию, которая будет складывать три числа:
В данном случае числа 12, 24, 36
— являются аргументами функции. Тип аргументов, которые мы передаем в функцию всегда должен соответствовать типам указанных параметров при объявлении функции.
Прототипы функций
Прототип функции — это объявление функции, которое указывает её имя, тип возвращаемого значения и типы параметров, позволяя компилятору заранее проверить корректность вызовов до фактической реализации функции.
void say_hello();
В Си мы не можем использовать функцию до её объявления, потому что компилятор не будет знать какие именно типы данных функция принимает в параметрах и какой тип данных функция будет возвращать. Следующий пример кода работать не будет:
int main() {
say_hello();
}
void say_hello() {
printf("Привет!\n");
}
error: implicit declaration of function ‘say_hello’
[-Wimplicit-function-declaration]
4 | say_hello();
В случае если нам нужно использовать функцию до её объявления мы можем использовать прототип функции. Его можно указать вверху файла или в отдельном .h
файле.
Второй способ является предпочтительным, так как помогает отделить реализацию функции от её интерфейса:
// Прототип функции
void say_hello();
Привет!
Область видимости
Блочная область видимости — это область действия переменной, ограниченная телом блока, заключённого в фигурные скобки {}
. Переменные с такой областью видимы и доступны только внутри этого блока и уничтожаются при выходе из него.
По умолчанию все переменные ограничены блочной областью видимости.
b=20
a=10
Функциональная область видимости — это область действия параметров функции, которые видимы и доступны только внутри тела этой функции.
#include <stdio.h>
void foo() {
int a = 0;
}
// Здесь a более не доступна
Глобальная область видимости — это область действия переменных, объявленных вне всех функций и блоков. Такие переменные доступны во всём исходном файле, а при использовании ключевого слова extern
— и в других файлах проекта.
// Доступна только в main.c
int a = 0;
// Доступна в рамках всей программы
extern int b = 2;
Статические переменные
Если нам нужно хранить какое-либо состояние между вызовами функции, то мы можем сделать это двумя способами. Первый способ заключается в объявлении переменной в глобальной области видимости:
2
Второй способ заключается в использовании статической переменной, она объявляется с помощью ключевого слова static
.
Данное ключевое слово позволяет нам назначить переменной статический адрес. При вызове функции адрес переменной не будет меняться, сама же переменная не будет реинициализирована, в случае если инициализация уже произошла:
2
Указатели
Указатели — это переменные, которые хранят адрес другой переменной, то есть ссылку на её расположение в памяти. Вместо того чтобы работать с данными напрямую, указатели позволяют работать с их адресами, что открывает огромные возможности для более эффективной работы с памятью и гибкости программирования.
Для начала давайте разберемся с адресами. У каждой переменной, которую вы объявили есть адрес, мы можем получить его с помощью оператора &
:
Value is: 12
Address is: a0ce480c
Для того чтобы объявить указатель, нужно указать тип данных, на который он будет ссылаться, а затем использовать символ *
:
int *ptr; // Указатель на целое число
double *dptr; // Указатель на число с плавающей запятой
char *cptr; // Указатель на символ
Мы можем инициализировать указатели с помощью специального значения NULL
(будет означать что указатель никуда не ссылается) или с помощью оператора ссылки:
int a = 5;
int *a_ptr;
a_ptr = &a; // Теперь a_ptr ссылается на a
a_ptr = NULL; // Теперь a_ptr никуда не ссылается
После того, как в нашем указателе есть какая-то ссылка, мы можем изменить значение переменной, адрес которой хранится в нашем указателе:
int a = 5;
int *a_ptr;
a_ptr = &a;
*a_ptr = 15;
Обратите внимание на использование оператора *
. В случае с изменением ссылки, на которую ссылается указатель – мы не используем оператор разыменовывания (*
), а в случае когда мы устанавливаем значение переменной, ссылка которой находится в нашем указателе – мы используем *
.
Если же мы попытаемся установить значение переменной без использования разыменовывания, то мы изменим адрес, на который ссылается наш указатель, а не значение:
*a_ptr = 10; // Меняем значение по адресу
a_ptr = 10; // Меняем сам адрес
Массивы
Массив в Си — это структура, представляющая собой последовательность однотипных элементов, хранящихся в непрерывной области памяти. При этом имя массива само по себе выражается как указатель на его первый элемент. Это поведение лежит в основе всей семантики взаимодействия массивов и указателей.
Если мы разыменуем переменную массива, то получим первый элемент, по сути выражения *arr
и arr[0]
— эквиваленты:
Когда мы прибалвяем число к указателю, мы сразу же прибавляем количество необходимых байтов для перехода на следующую ячейку. Это достигается за счет того, что у указателя тоже есть размерность и тип.
Под капотом Си берёт тип данных указателя и смотрит его размерность. При добавлении/вычитании от указателя, он берет переданное число после арифметической операции и умножает его на размерность типа данных:
Тип данных: double
Размерность: 8
Изначальная позиция: 0
Выражение: ptr + 5
Получаемое выражение: ptr(адрес) + (5 * 8)
Строки
Мы можем объявить строки двумя способами:
- Через массив символов
char[]
; - Через указатель на первый символ
char*
;
char foo[] = "str"; // Данный тип превратится в foo[3]
char *bar = "Dynamic String"; // Данный тип превратится в char* (&"Dynamic String"[0])
Второй способ ищет и занимает память в разделе “только для чтения” и помещает туда массив из char
.
При использовании такого способа инициализации строки мы не можем изменять саму строку, так как строка расположена в сегменте .rodata
(Read Only Data).
При использовании фиксированного массива строки изменять можно:
char foo[] = "Hello";
foo[0] = 'h';
printf("%s\n", foo); // hello
Указатель на константу
В Си существуют указатели на константу, такие указатели объявляются через const
, мы можем изменять данные указатели, но не можем изменять значения, на которые данные указатели ссылаются. Отличным примером таких указателей являются указатели на строки:
const char *fooPtr = "Hello";
Указатель на функцию
В C каждая функция располагается в определённой области памяти. Имя функции на самом деле является адресом начала этой области — аналогично имени массива, которое указывает на его первый элемент. Это означает, что мы можем объявить переменную, которая будет хранить этот адрес, и вызывать функцию через неё.
Именно это и называется указателем на функцию — переменная, содержащая адрес функции с определённой сигнатурой (тип возвращаемого значения и список параметров).
Форма записи указателя на функцию примерно следующая:
возвращаемый_тип (*имя)(аргументы);
Давайте создадим функцию add
, которая будет складывать два числа и поместим её в указатель:
Динамическая память
Ранее мы работали только с данными, у которых фиксированная длина. Для того чтобы работать с динамической память в Си есть специальные функции, которые позволяют нам выделять память в куче.
Динамическая память — это область памяти (так называемая куча, или heap), которая выделяется программой во время выполнения. Управление этой памятью осуществляется вручную: программист сам отвечает за выделение и освобождение ресурсов.
Всё управление динамической памятью в C осуществляется с помощью набора функций из заголовочного файла <stdlib.h>
:
Функция | Назначение |
---|---|
malloc | Выделяет заданное количество байт |
calloc | Выделяет память и инициализирует её нулями |
realloc | Изменяет размер ранее выделенного блока |
free | Освобождает ранее выделенную память |
Давайте рассмотрим легкий пример с выделением памяти. Мы реализуем динамический массив состоящий из int
:
#include <stdio.h>
#include <stdlib.h>
int main() {
int n;
printf("Введите размер массива: ");
scanf("%d", &n);
// Выделяем память для n целых чисел
int *arr = (int *)malloc(n * sizeof(int));
// Проверяем успешность выделения
if (arr == NULL) {
printf("Ошибка: не удалось выделить память.\n");
return 1;
}
// Инициализируем массив
for (int i = 0; i < n; i++) {
arr[i] = i * 10;
}
// Выводим массив
printf("Массив:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
// Освобождаем память
free(arr);
return 0;
}
Теперь давайте рассмотрим более подробно данный сниппет:
Реаллоцирование памяти
Иногда во время выполнения программы возникает необходимость изменить размер уже выделенного блока памяти.
Для этого предусмотрена функция realloc
, которая позволяет перераспределить ранее выделенный участок памяти и сохранить уже записанные в него данные.
void* realloc(void* ptr, size_t new_size);
ptr
— указатель на ранее выделенный блок памяти с помощью malloc, calloc или realloc.new_size
— новый требуемый размер в байтах.
realloc
возвращает новый указатель на блок памяти. В случае неудачи возвращается NULL
, при этом исходный блок остаётся доступным.
Если новый размер больше предыдущего, старые данные сохраняются, а оставшаяся часть остаётся неинициализированной (при использовании malloc и realloc). Если меньше — данные обрезаются:
#include <stdlib.h>
#include <stdio.h>
int main() {
// Аллоцируем массив
int SIZE = 10;
int *ptr = (int*)malloc(SIZE * sizeof(int));
if (ptr == NULL) {
printf("Не удалось выделить память\n");
}
// Реаллоцируем массив
ptr = (int*)realloc(ptr, (SIZE + 2) * sizeof(int));
if (ptr == NULL) {
printf("Не удалось перевыделить память\n");
}
free(ptr);
return 0;
}
Выделение памяти с инициализацией
Иногда требуется не только выделить память, но и сразу инициализировать её нулями. Для этого в Cи предусмотрена функция calloc
, которая объединяет в себе действия malloc
и инициализации.
void* calloc(size_t num, size_t size);
num
— количество элементов;size
— размер одного элемента в байтах.
В отличие от malloc
, которая просто выделяет память (с произвольным содержимым), calloc
инициализирует все выделенные байты нулями. Это полезно, когда важно, чтобы переменные изначально имели значение 0
:
Структуры
Структура — это агрегированный тип данных, который позволяет объединить несколько переменных разного типа под одним именем. Это основа для создания пользовательских типов, приближённых к объектам в ООП.
struct Point {
int x;
int y;
};
Здесь Point
— структура, содержащая две переменные типа int
.
Структуры полезны, когда необходимо логически сгруппировать связанные данные: координаты точки, сведения о студенте, параметры конфигурации и т.д.
Использование структур
Мы можем использовать нашу структуру как тип переменной с помощью ключевого слова struct
и названия структуры:
struct Point p1;
p1.x = 10;
p1.y = 20;
Или же мы можем создать новый тип, который по сути будет алиасом на тип структуры:
typedef struct {
int x, y;
} Point;
Point p2 = {1, 2};
Перечисления
Перечисление — это способ задать набор именованных целочисленных констант. Они используются для описания фиксированных множеств значений, что делает код более читаемым.
enum Color { RED, GREEN, BLUE };
enum Status { OK = 200, NOT_FOUND = 404, SERVER_ERROR = 500 };
Мы можем использовать перечисления следующим образом:
enum Color { RED, GREEN, BLUE };
int main() {
enum Color c = GREEN;
if (c == RED) {
// ...
} else if (c == GREEN) {
// ...
}
return 0;
}