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

Начало работы с C++

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

Первый рабочий маршрут за один вечер

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

  1. Написать main.cpp с одним std::cout.
  2. Разделить код на main.cpp, calc.hpp, calc.cpp.
  3. Собрать сначала одной командой компилятора, затем через CMake.
  4. Добавить одну намеренную ошибку линковки и разобрать сообщение.
  5. Вернуть рабочее состояние и добавить первый тест.

Так закрепляется главный принцип C++: код, сборка и архитектура файла изучаются вместе.


Типичные ошибки в начале пути

ОшибкаЧто происходитКак исправить
Реализация функции в заголовке без inlineduplicate symbol на линковкеперенести реализацию в .cpp
Ручной new без deleteутечка памятииспользовать std::unique_ptr или контейнер
using namespace std; в .hконфликты имён в проектеоставлять std:: явно
Смешение Debug и Release библиотекнестабильное поведениесобирать все модули в одной конфигурации

См. также


Кейс из практики — первая программа растёт до мини-проекта

Старт: один файл main.cpp с простым выводом.

Через 2-3 шага появляется реальная структура:

  • main.cpp — точка входа;
  • calc.hpp — объявления;
  • calc.cpp — реализация;
  • CMakeLists.txt — единый сценарий сборки.

Что это даёт:

  • читаемую архитектуру файлов;
  • меньше ошибок линковки;
  • готовую базу для тестов и CI.

Главная мысль: переход к модульной структуре полезно делать рано, пока код ещё маленький.


Начало работы с C++

Как работает компиляция в C++

Play ITЗагрузка интерактивного демо…

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

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

Компиляция в C++ проходит через четыре фундаментальных этапа:

  • препроцессинг,
  • компиляция,
  • ассемблирование,
  • линковка.

Каждый этап выполняет свою специфическую задачу по трансформации данных. Пропуск любого из этих этапов делает невозможным создание работающей программы.


Этап 1. Препроцессинг

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

Препроцессор обрабатывает специальные команды, начинающиеся с символа решетки #. Эти команды называются директивами.

Директива #include сообщает компилятору вставить содержимое другого файла прямо в текущий файл на месте команды. Если программа использует стандартные библиотеки или собственные заголовочные файлы, их текст подставляется в код.

Директива #define создает макросы. Макрос — это текстовая замена. Когда компилятор встречает имя макроса, он заменяет его на заданный текст.

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

Конструкция #ifdef позволяет включать или исключать части кода в зависимости от условий компиляции. Это полезно для настройки программы под разные платформы или режимы работы.

На выходе этого этапа получается чистый исходный код без директив препроцессора. Он готов к анализу синтаксиса.


Этап 2. Компиляция

Второй этап выполняет сам компилятор. Он читает результат работы препроцессора и проверяет соответствие кода правилам языка. Компилятор анализирует синтаксис и семантику кода.

Если в коде есть ошибки, например, использование несуществующей переменной или несоответствие типов данных, компилятор выводит сообщение об ошибке. Программа не будет собрана до устранения ошибок.

Если ошибок нет, компилятор генерирует объектный код. Объектный код — это набор машинных инструкций, сохраненный в файле. В Windows такие файлы имеют расширение .obj, в Linux и macOS — .o.

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


Этап 3. Ассемблирование

Третий этап переводит сгенерированный ассемблерный код в машинные инструкции.

Ассемблер — это низкоуровневый язык, близкий к машинному коду процессора.

В некоторых реализациях этот этап объединен с компилятором и происходит автоматически. Логически же он существует отдельно. Ассемблер формирует бинарный код, который может быть загружен в память процессора.

Итогом работы ассемблера является объектный файл, содержащий полностью сформированные машинные инструкции для конкретной архитектуры процессора.

--


Этап 4. Линковка

Финальный этап выполняет линковщик. Его задача собрать все созданные объектные файлы в единый исполняемый файл.

Главная проблема линковки — разрешение внешних ссылок. Если в коде вызывается функция printf, которая находится в стандартной библиотеке, линковщик находит эту функцию в соответствующей библиотеке и подставляет ссылку на неё в итоговый файл.

Линковщик также решает проблемы адресации. Он распределяет память для переменных и функций, создавая конечный файл формата .exe (Windows), ELF (Linux) или Mach-O (macOS).

Только после прохождения всех четырех этапов программа готова к запуску.


Производительность и низкоуровневый контроль

Производительность программ на C++ достигается за счет такой архитектуры компиляции. Поскольку весь код анализируется и оптимизируется до запуска, компилятор имеет полную картину всей программы.

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

Эта оптимизация происходит один раз при сборке. Время выполнения программы минимально. Отсутствие необходимости интерпретировать код во время работы дает значительное преимущество в скорости.

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

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

Возможность работать непосредственно с памятью означает, что разработчик сам отвечает за выделение и освобождение ресурсов. Это дает гибкость, но накладывает ответственность за корректность кода.


Архитектура программы и компоненты сборки

Программа на C++ состоит из модулей, каждый из которых выполняет определенную роль. Понимание структуры файлов необходимо для правильной организации проекта.


Исходные файлы (.cpp)

Исходный файл с расширением .cpp содержит реализации функций, классов и глобальных переменных. Эти файлы содержат тело кода, которое выполняет конкретные действия.

Файл .cpp должен содержать хотя бы одну функцию main. Именно эта функция служит точкой входа для операционной системы. При запуске программы система начинает выполнение с первой команды внутри функции main.

Остальной код программы вызывает функции, обращается к данным и управляет потоком выполнения. Один проект может состоять из множества файлов .cpp.


Заголовочные файлы (.h / .hpp)

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

Заголовочные файлы не содержат реализации тел функций. Они лишь сообщают компилятору о существовании этих элементов и их сигнатурах.

Это позволяет разделять интерфейс и реализацию. Когда другой файл включает заголовок с помощью директивы #include, он получает доступ к объявленным элементам. Тело функции остается в исходном файле .cpp.

Такой подход обеспечивает инкапсуляцию и упрощает поддержку кода. Изменение реализации функции не требует перекомпиляции всех файлов, использующих её интерфейс.


Библиотеки (.lib / .a / .so)

Библиотеки представляют собой коллекции предварительно скомпилированного кода. Они делятся на статические и динамические.

Статические библиотеки имеют расширение .lib в Windows и .a в Unix-системах. При линковке содержимое такой библиотеки копируется внутрь исполняемого файла.

Это увеличивает размер программы, но гарантирует, что все необходимые функции будут доступны локально. Программа работает автономно без дополнительных файлов.

Динамические библиотеки имеют расширение .dll в Windows и .so в Linux. Они хранятся отдельно от исполняемого файла и загружаются в оперативную память только при запуске.

Это экономит дисковое пространство. Несколько программ могут использовать одну и ту же библиотеку одновременно. Обновление функциональности возможно без перекомпиляции основного приложения.


Структура сборки проекта

При сборке проекта компилятор обрабатывает каждый .cpp файл отдельно, создавая объектные файлы. Затем линковщик объединяет все объектные файлы и подключаемые библиотеки.

Если в одном файле используется функция, определенная в другом файле того же проекта, линковщик связывает эти ссылки. Если функция находится в сторонней библиотеке, линковщик ищет её там.

Ошибки линковки возникают, когда ссылка не может быть разрешена. Функция может быть нигде не определена или определена несколько раз.

Работа с заголовочными файлами требует соблюдения правил защиты от повторного включения. Для этого используются макропрепроцессоры #ifndef, #define и #endif.

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

Пример структуры проекта выглядит следующим образом:

Project/
├── main.cpp # Точка входа
├── calculator.h # Объявление класса Calculator
├── calculator.cpp # Реализация класса Calculator
├── utils.h # Утилитные функции
├── utils.cpp # Реализация утилит
└── libmath.lib # Внешняя библиотека

Разбор:

  • Project/ показывает минимальную модульную структуру: отдельно интерфейсы (.h) и реализации (.cpp).
  • main.cpp служит входной точкой процесса, где ОС начинает исполнение с main().
  • Пара calculator.h/calculator.cpp реализует разделение "контракт/код", что снижает связность и упрощает сопровождение.
  • utils.* выделяет общие функции в переиспользуемый модуль и предотвращает дублирование.
  • libmath.lib демонстрирует внешний бинарный артефакт, который будет подключаться на этапе линковки.

Файл main.cpp включает заголовки:

#include <iostream>
#include "calculator.h"
#include "utils.h"

int main() {
// Логика программы
}

Разбор:

  • #include <iostream> подключает декларации стандартного ввода/вывода (std::cout, std::cin).
  • #include "calculator.h" и #include "utils.h" подключают пользовательские интерфейсы из проекта.
  • int main() объявляет точку входа, а тип int задает код завершения процесса.
  • Тело функции оставлено как шаблон, куда добавляется оркестрация вызовов модулей.

Файл calculator.h:

#ifndef CALCULATOR_H
#define CALCULATOR_H

class Calculator {
public:
int add(int a, int b);
};

#endif

Разбор:

  • #ifndef / #define / #endif образуют include-guard и защищают от повторного включения заголовка.
  • class Calculator определяет публичный интерфейс класса без деталей реализации.
  • int add(int a, int b); объявляет метод и фиксирует сигнатуру для всех пользователей класса.
  • Здесь нет тела функции, поэтому компоновщик ожидает определение в .cpp.

Файл calculator.cpp:

#include "calculator.h"

int Calculator::add(int a, int b) {
return a + b;
}

Разбор:

  • #include "calculator.h" гарантирует совпадение реализации с объявленной сигнатурой.
  • Calculator::add использует оператор :: для указания принадлежности методу класса.
  • Параметры a и b принимаются по значению, чего достаточно для примитивного типа int.
  • return a + b; реализует чистую функцию без побочных эффектов, удобную для тестирования.

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


Базовый синтаксис C++

Синтаксис языка определяет правила записи кода. Знание базовых конструкций необходимо для написания первых программ.


Директива #include

Директива препроцессора #include используется для подключения заголовочных файлов в исходный код. Она сообщает компилятору вставить содержимое указанного файла в текущее место перед началом компиляции.

Существует два способа указания пути к файлу: угловые скобки <...> и кавычки "...". Использование угловых скобок указывает компилятору искать файл в системных директориях, где расположены стандартные библиотеки.

Кавычки указывают поиск в текущей директории проекта и директориях, указанных в настройках компилятора. Стандартная библиотека ввода-вывода подключается как #include <iostream>. Пользовательские заголовки подключаются как #include "my_header.h".


Точка входа main()

Функция main() является обязательной точкой входа для любой программы на C++. Без неё программа не сможет запуститься. Подпись функции обычно выглядит как int main().

Возвращаемый тип int означает, что функция возвращает целочисленный код завершения программы в операционную систему. Значение 0 сигнализирует об успешном завершении. Любое другое значение указывает на ошибку.

Внутри фигурных скобок {} располагается тело функции, где выполняется основной код программы.


Пространство имён std

Пространство имён std содержит все элементы стандартной библиотеки C++. Сюда входят функции ввода-вывода, контейнеры, алгоритмы и типы данных.

Чтобы обратиться к элементу из этого пространства, используют оператор разрешения области видимости ::. Запись std::cout означает обращение к объекту вывода потока внутри пространства std.

Директива using namespace std; подтягивает все имена из std в текущую область видимости — в учебных .cpp иногда допустимо, в заголовках (.h) почти никогда: при #include такого файла конфликты имён распространятся на весь проект. В репозиториях и библиотеках пишут std::cout, std::vector и т.д.; для длинных имён — точечно using std::string; внутри .cpp.


Оператор вставки <<

Оператор вставки << используется для записей данных в поток вывода. В контексте std::cout << "Текст" оператор направляет строку "Текст" в стандартный поток вывода, который отображается на экране терминала.

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

Аналогичный оператор >> используется для чтения данных из потока ввода std::cin.


Указатели * и ссылки &

Указатели * и ссылки & являются механизмами работы с адресами памяти. Указатель хранит адрес переменной в памяти. Объявление указателя осуществляется с помощью звездочки: int* ptr;.

Переменная типа указатель может хранить адрес переменной int. Ссылка & является альтернативным именем для существующей переменной. Объявление ссылки: int& ref = var;.

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


Оператор разрешения области видимости ::

Оператор разрешения области видимости :: используется для доступа к глобальным элементам, членам классов или пространствам имён. Запись ClassName::member обращается к члену member класса ClassName.

Запись ::globalVar обращается к глобальной переменной globalVar, игнорируя любые локальные переменные с тем же именем. В пространстве имён std::vector<int> оператор отделяет имя пространства от имени контейнера.

Пример базового кода:

#include <iostream>

int main() {
int number = 42;
int* ptr = &number; // ptr хранит адрес number

std::cout << "Значение: " << number << std::endl;
std::cout << "Адрес: " << ptr << std::endl;

return 0;
}

Разбор:

  • #include <iostream> подключает потоковый вывод в консоль через std::cout.
  • int number = 42; выделяет локальную переменную на стеке и инициализирует ее значением.
  • int* ptr = &number; берет адрес переменной оператором & и сохраняет в указателе.
  • std::cout << number печатает значение, а std::cout << ptr показывает адрес в памяти.
  • return 0; передает ОС сигнал успешного завершения.

В этом примере #include <iostream> подключает библиотеку для работы с вводом-выводом. main() является точкой входа. int number объявляет целочисленную переменную. int* ptr объявляет указатель. &number берет адрес переменной number. std::cout использует пространство имён std. << вставляет данные в поток вывода. return 0 завершает программу успешно.


Управление памятью в C++

Память компьютера делится на две основные области: стек и куча. Каждая область имеет свои особенности использования и правила управления.


Стек (Stack)

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

Стек работает по принципу LIFO (последний вошел — первый вышел). Размер стека ограничен и относительно мал, обычно составляет несколько мегабайт. Переменные в стеке доступны только в пределах своей области видимости.

Размер переменных в стеке должен быть известен на этапе компиляции. Стек быстро выделяется и освобождается, так как управление им осуществляет процессор.


Куча (Heap)

Куча — это область динамического выделения памяти. Здесь хранятся объекты, размер которых неизвестен на этапе компиляции или которые должны существовать дольше, чем вызов функции.

Выделение памяти в куче производится явно с помощью оператора new. Освобождение памяти происходит с помощью оператора delete. Разработчик несет полную ответственность за правильное использование памяти в куче.

Неосвобожденная память приводит к утечкам. Преждевременное освобождение ведет к ошибкам доступа.


Динамическая память и операторы new / delete

Динамическая память позволяет создавать объекты произвольного размера во время выполнения программы. Оператор new запрашивает блок памяти нужного размера в куче и возвращает указатель на него.

Оператор delete освобождает этот блок. Пример использования:

int* ptr = new int(10); // Выделение памяти под одно целое число
// Работа с *ptr
delete ptr; // Освобождение памяти
ptr = nullptr; // Установка указателя в нулевое состояние

Разбор:

  • new int(10) выделяет память в куче и инициализирует объект значением 10.
  • ptr хранит адрес выделенного объекта, а доступ к самому значению идет через разыменование *ptr.
  • delete ptr освобождает выделенный блок и завершает жизненный цикл объекта.
  • ptr = nullptr разрывает "висячую" ссылку и снижает риск повторного удаления.
  • Для массивов используется отдельная пара new[]/delete[], иначе поведение некорректно.

Отсутствие автоматического управления памятью в C++ дает высокую производительность, но требует строгой дисциплины. Разработчик должен гарантировать, что каждый new сопровождается парным delete (для массивов — delete[]).

Нарушение этого правила ведет к утечкам памяти или повреждению данных. В прикладном коде пару new/delete обычно заменяют std::make_unique / контейнерами — см. раздел умные указатели ниже и статью Управление памятью.


Умные указатели

Умные указатели — это классы-обертки над обычными указателями, которые автоматизируют управление памятью. Они используют принцип RAII (Resource Acquisition Is Initialization).

Ресурс выделяется при создании объекта и освобождается при его уничтожении. Наиболее распространенные виды умных указателей: std::unique_ptr и std::shared_ptr.

std::unique_ptr обладает эксклюзивным правом владения объектом. Один уникальный указатель может указывать на объект, и ни один другой не может. При передаче уникального указателя другому владельцу право владения переносится.

При уничтожении уникального указателя объект автоматически удаляется. Это предотвращает утечки и двойное удаление.

std::shared_ptr поддерживает подсчет ссылок. Несколько указателей могут указывать на один объект. Счетчик ссылок увеличивается при копировании указателя и уменьшается при удалении.

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

Пример использования умных указателей:

#include <memory>

void example() {
std::unique_ptr<int> unique = std::make_unique<int>(42);
// Автоматическое удаление при выходе из функции

std::shared_ptr<int> shared1 = std::make_shared<int>(100);
std::shared_ptr<int> shared2 = shared1;
// Объект удалится только когда исчезнут оба указателя
}

Разбор:

  • <memory> предоставляет умные указатели и фабричные функции make_unique/make_shared.
  • std::unique_ptr<int> задает эксклюзивное владение объектом: копирование запрещено, разрешено перемещение.
  • std::make_unique<int>(42) безопасно создает объект и избегает ручного delete.
  • std::shared_ptr<int> хранит счетчик ссылок в control block и освобождает объект при счетчике 0.
  • Присваивание shared2 = shared1 делит владение одним объектом между двумя указателями.

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


Сравнение C++ с современными языками

C++ существенно отличается от современных языков, таких как Python, JavaScript, Java и C#. Эти различия определяют порог входа и сценарии применения каждого языка.


Python и JavaScript

Python и JavaScript являются интерпретируемыми языками с автоматической сборкой мусора (Garbage Collection). Разработчик не управляет памятью вручную.

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

Такая автоматизация накладывает накладные расходы на производительность. Время выполнения программы в Python и JavaScript обычно выше, чем в C++, так как интерпретатор тратит ресурсы на анализ кода и управление памятью во время выполнения.


Java и C#

Java и C# находятся посередине между C++ и скриптовыми языками. Они компилируются в байт-код, который выполняется виртуальной машиной (JVM для Java, CLR для C#).

Оба языка используют автоматическую сборку мусора. Разработчик создает объекты, но не обязан вызывать delete или free. Система сама отслеживает живые объекты и освобождает мертвые.

Это повышает безопасность и удобство разработки, но сохраняет некоторую зависимость от времени выполнения и накладных расходов виртуальной машины. Производительность Java и C# ниже, чем у нативного C++, но выше, чем у чистых интерпретируемых языков.


C++ и управление памятью

C++ требует явного управления памятью через new и delete. Разработчик должен следить за жизненным циклом объектов. Это дает максимальный контроль и производительность, но увеличивает сложность кода и риск ошибок.

Ошибки в управлении памятью в C++ приводят к утечкам, сбоям или неопределенному поведению. В языках с GC такие ошибки практически отсутствуют.


Синтаксис и уровень абстракции

Синтаксис C++ более строгий и многословный по сравнению с Python и JavaScript. В Python код часто короче и читабельнее благодаря отсутствию обязательных типов данных и фигурных скобок.

В C++ необходимо явно объявлять типы переменных, использовать пространства имён и указывать детали реализации. Это делает код более громоздким, но и более прозрачным для компилятора.

Строгая типизация C++ позволяет проводить глубокую оптимизацию на этапе компиляции.


Модель исполнения

Модель исполнения C++ — компиляция в нативный машинный код. Программа работает напрямую на процессоре без промежуточного слоя.

В Python и JavaScript код выполняется интерпретатором или виртуальной машиной. В Java и C# код выполняется в среде виртуальной машины.

Это различие определяет скорость запуска и выполнения. C++ запускается мгновенно и работает быстрее, так как нет необходимости в загрузке среды выполнения.


Типизация

Типизация в C++ является строгой и статической. Типы переменных определяются на этапе компиляции и не могут меняться. В Python и JavaScript типизация динамическая: тип определяется в момент выполнения.

В Java и C# типизация статическая, как в C++, но с возможностью вывода типов в некоторых случаях. Строгая типизация C++ позволяет обнаруживать ошибки на этапе компиляции, повышая надежность программы.


Область применения

Экосистема C++ ориентирована на системное программирование, высокопроизводительные вычисления и игры. Python и JavaScript доминируют в веб-разработке, науке о данных и скриптинге.

Java и C# широко применяются в корпоративной разработке, мобильных приложениях и enterprise-системах. Выбор языка зависит от требований проекта.

Если нужна максимальная скорость и контроль — выбирают C++. Если важна скорость разработки и простота — выбирают Python или JavaScript. Если нужен баланс — Java или C#.

Порог входа в C++ выше из-за сложности управления памятью, жесткого синтаксиса и необходимости понимания устройства компьютера. Изучение C++ требует времени и терпения, но оно дает глубокое понимание принципов работы программ и оборудования.

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


Содержание