Проблемы и подвохи умных указателей
Умные указатели (smart pointers) — unique_ptr, shared_ptr, weak_ptr — это мощный инструмент для управления памятью в C++, но они имеют собственные проблемы и подводные камни, которые нужно знать.
Проблема 1: Циклические ссылки (circular references)
Это самая опасная проблема с shared_ptr — циклические ссылки приводят к утечкам памяти:
struct Node {
int data;
std::shared_ptr<Node> next;
std::shared_ptr<Node> prev; // ⚠️ Циклические ссылки!
};
// Утечка памяти
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1; // Циклическая ссылка!
// node1 и node2 имеют счётчики ссылок > 1
// Когда выходят из scope, не удаляются!
// Утечка: оба объекта живут вечно
Решение: использовать weak_ptr:
struct Node {
int data;
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // ✅ weak_ptr не увеличивает счётчик
};
Ключевое слово override в C++
Override — одно из самых важных ключевых слов в современном C++11+, которое спасает от множества ошибок при переопределении виртуальных функций.
Проблема до override
class Base {
public:
virtual void process(int x);
virtual void calculate();
};
class Derived : public Base {
public:
// ОШИБКА 1: Опечатка в имени функции
void proccess(int x); // Не переопределяет Base::process!
// ОШИБКА 2: Неправильная сигнатура
void calculate(double x); // Не переопределяет, создаёт новую!
// ОШИБКА 3: Забыл const
void process(int x) const; // Не переопределяет!
};
// Программист думает, что переопределил, но нет!
Base* ptr = new Derived();
ptr->process(5); // Вызовет Base::process, не Derived::process!
Решение: override
Зачем нужен виртуальный деструктор
Виртуальный деструктор — это критическое требование для полиморфных классов, обеспечивающее корректное удаление объектов через базовый указатель. Без него программа страдает от утечек памяти и корупции памяти.
Проблема: деструктор без virtual
Когда производный класс наследует базовый без virtual деструктора:
class Base {
public:
~Base() { std::cout << "Base destructor\n"; }
};
class Derived : public Base {
private:
std::string buffer; // Требует очистки
public:
~Derived() {
std::cout << "Derived destructor\n";
// buffer автоматически очистится
}
};
Base* ptr = new Derived();
delete ptr; // ПРОБЛЕМА!
Что происходит:
Решение: virtual деструктор
class Base {
public:
virtual ~Base() { std::cout << "Base destructor\n"; }
};
Разница между классом и структурой в C++
В C++ различие между class и struct минимально, но стилистически и концептуально они используются по-разному.
Синтаксическое различие: Видимость по умолчанию
struct: public по умолчанию
struct Point {
int x; // ✅ public по умолчанию
int y; // ✅ public
void move() {} // ✅ public по умолчанию
};
Point p;
p.x = 10; // ✅ OK — public
p.move(); // ✅ OK
class: private по умолчанию
class Point {
int x; // ❌ private по умолчанию
int y; // ❌ private
void move() {} // ❌ private по умолчанию
};
Point p;
p.x = 10; // ❌ COMPILATION ERROR — private
p.move(); // ❌ COMPILATION ERROR — private
Наследование
struct Base { int x; };
struct Child : Base {};
// наследование PUBLIC по умолчанию
static_assert(std::is_same_v<decltype(Child().x), int>); // ✅ x доступен
RAII: Resource Acquisition Is Initialization
RAII — это фундаментальный паттерн в C++, который привязывает управление ресурсами к жизненному циклу объектов. Это одна из самых мощных инноваций языка, обеспечивающая безопасность памяти и надёжность.
Основная идея
Ресурсы захватываются в конструкторе, освобождаются в деструкторе.
Этот паттерн гарантирует:
Базовый пример: File RAII
// ❌ БЕЗ RAII (C-style, опасно)
void process_file_bad() {
FILE* f = fopen("data.txt", "r");
// Если здесь исключение — файл не закроется!
if (some_error_condition) {
throw std::runtime_error("Oops!");
// fclose(f) НИКОГДА не вызовется
}
// А что если раньше возвращаемся?
if (file_empty) {
return; // утечка файлового дескриптора!
}
fclose(f); // может быть, может не быть
}
Мой опыт разработки на C/C++
Обладаю более чем 10-летним опытом разработки высокопроизводительных backend-систем на C/C++. Специализирую на архитектуре масштабируемых приложений, где каждая деталь имеет значение.
Ключевые области знаний
Стандартная библиотека (STL)
Глубокое понимание контейнеров (vector, map, unordered_map, deque, list), алгоритмов и итераторов. Практический опыт оптимизации выбора структур данных в зависимости от паттернов доступа:
vector для последовательного доступа с минимальными выделениямиunordered_map для O(1) поиска с правильным хешированиемmove semantics и perfect forwarding для минимизации копированийУправление памятью
Работал с разными подходами:
new/delete (legacy code)smart pointers: unique_ptr для исключительного владения, shared_ptr для разделённого владенияRAII паттерн для гарантированного освобождения ресурсовКонструкторы как шаблонные функции
Да, конструктор может быть шаблонной функцией (template constructor). Это мощный инструмент для написания обобщённого кода, позволяющий создавать объекты с различными типами данных через единый интерфейс.
1. Базовый пример
#include <iostream>
#include <type_traits>
template <typename T>
class Container {
private:
T value;
public:
// Обычный конструктор
Container() : value() {}
// Шаблонный конструктор
template <typename U>
Container(U val) : value(static_cast<T>(val)) {
std::cout << "Template constructor called\n";
}
};
int main() {
Container<int> c1(42); // int -> int
Container<double> c2(3.14f); // float -> double
Container<std::string> c3("hello"); // const char* -> std::string
return 0;
}
2. Шаблонный конструктор для конвертации типов
Атомарная блокировка двух мьютексов
Атомарная блокировка двух мьютексов — критическая проблема в многопоточном программировании, особенно для предотвращения deadlock ситуаций. Рассмотрим основные подходы и механизмы.
Проблема классического подхода
Если заблокировать мьютексы последовательно, возникает риск deadlock:
// Опасно!
mutex1.lock(); // Поток 1 берет mutex1
mutex2.lock(); // Может быть заблокирован
// Если другой поток имеет mutex2 и ждет mutex1 — deadlock
Решение 1: std::lock (C++11)
Самый надежный и рекомендуемый механизм:
#include <mutex>
#include <thread>
mutex mu1, mu2;
// Правильный способ
std::lock(mu1, mu2); // Атомарно блокирует оба мьютекса
// Безопасная работа с защищенными ресурсами
mu1.unlock();
mu2.unlock();
Каким образом можно создать файл только если его нет?
В C++ есть несколько способов создать файл только если он не существует. Выбор зависит от платформы и требований.
1. POSIX: open() с флагами O_CREAT и O_EXCL
Это атомарная операция — либо создаёт файл, либо ошибка:
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
int createFileIfNotExists(const char* filename) {
// O_CREAT: создать если не существует
// O_EXCL: ошибка если существует
// O_WRONLY: открыть на запись
int fd = open(filename, O_CREAT | O_EXCL | O_WRONLY, 0644);
if (fd == -1) {
if (errno == EEXIST) {
std::cerr << "File already exists\n";
} else {
perror("open failed");
}
return -1;
}
// fd содержит файловый дескриптор
close(fd);
return 0;
}
Преимущества: Атомарный, работает везде, низкий уровень.
Недостатки: Нужно проверять errno, низкоуровневый API.
2. C++17: std::filesystem
Что требуется для использования структуры в качестве ключа unordered_map?
Использование пользовательских типов в качестве ключей в unordered_map требует специальной подготовки. Это частая задача при разработке backend-приложений, где нужны составные ключи (например, (user_id, date) или (source_ip, port)).
Основное требование: Hash функция
unordered_map использует хеширование вместо сравнения, поэтому нужно определить hash функцию для структуры:
#include <unordered_map>
#include <string>
#include <functional>
struct User {
int id;
std::string email;
};
// Вариант 1: Специализация std::hash
namespace std {
template<>
struct hash<User> {
size_t operator()(const User& user) const {
// Комбинируем хеши полей
size_t h1 = hash<int>()(user.id);
size_t h2 = hash<std::string>()(user.email);
// Используем XOR и сдвиг (FNV-like)
return h1 ^ (h2 << 1);
}
};
}
Источники использованные для обучения
За 10+ лет работы в C/C++ backend разработке я использовал разнообразные источники обучения, от классических книг до современных онлайн-ресурсов. Вот основные из них:
Классические книги
Это трёх-томная серия остаётся библией для C++ разработчиков:
Эти книги учат не синтаксису, а философии и best practices. Каждый совет обоснован и показывает грубые ошибки, которые совершают даже опытные разработчики.
Официальный гайд от создателя C++. Массивный том, но содержит:
Особенности процесса запуска ядра Linux
Процесс загрузки ядра Linux — это критически важный этап, который должен понимать каждый системный программист и backend-разработчик, работающий с низкоуровневым кодом, оптимизацией систем и kernel-space приложениями.
Этапы загрузки ядра
1. BIOS/UEFI Phase
2. Bootloader Phase
Какая асимптотическая сложность удаления в vector?
Краткий ответ
Удаление элемента из std::vector имеет сложность O(n) в худшем случае, где n — количество элементов в vector.
Почему именно O(n)?
std::vector — это динамический массив. Его элементы хранятся в памяти последовательно. Когда вы удаляете элемент, все элементы после него нужно сдвинуть на позицию влево, чтобы заполнить "дыру".
std::vector<int> vec = {10, 20, 30, 40, 50};
// 0 1 2 3 4
// Удаление элемента на позиции 1 (значение 20)
vec.erase(vec.begin() + 1);
// Процесс:
// Было: [10, 20, 30, 40, 50]
// Шаг 1: Удаляем 20
// Шаг 2: Сдвигаем 30 на позицию 1
// Шаг 3: Сдвигаем 40 на позицию 2
// Шаг 4: Сдвигаем 50 на позицию 3
// Итог: [10, 30, 40, 50]
Количество операций сдвига: n - i - 1, где i — позиция удаляемого элемента и n — размер вектора.
Анализ разных случаев
Мой уход из последней должности
Отправляюсь из компании Yandex.Cloud по стратегическим причинам, а не из-за критических проблем.
Причины ухода
Необходимость более широкого опыта
В течение 5 лет работал глубоко в одной экосистеме — облачная инфраструктура и микросервисы. Организация отлично структурирована, но я достиг плато в части экспериментирования с новыми технологиями и подходами. Хотелось попробовать себя в:
Стремление к лидерству
В текущей компании я был strong individual contributor (senior engineer), но не имел возможности влиять на архитектурные решения на уровне компании. Хочу присоединиться к команде, где я смогу:
Что нужно для работы с дескриптором в неблокируемом режиме?
Для работы с файловым дескриптором (file descriptor) в неблокируемом (non-blocking) режиме необходимо выполнить несколько шагов и обработать особенности такой работы.
1. Установка флага O_NONBLOCK
Способ 1: При открытии файла/сокета
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
// Открыть файл в неблокируемом режиме
int fd = open("file.txt", O_RDONLY | O_NONBLOCK);
if (fd == -1) {
perror("open");
return 1;
}
// Для сокетов
#include <sys/socket.h>
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
perror("socket");
return 1;
}
Способ 2: Установка флага на уже открытый дескриптор
#include <fcntl.h>
#include <unistd.h>
int fd = open("file.txt", O_RDONLY);
// Получить текущие флаги
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
perror("fcntl F_GETFL");
return 1;
}
Со своей ли техникой нужно работать?
Краткий ответ
Нет, вы должны работать с компьютером компании. Это стандартная практика в IT-индустрии по соображениям безопасности, юридической ответственности и организационной логики. Однако есть исключения и нюансы, которые зависят от политики конкретной компании.
Почему компаниям нужна своя техника
1. Безопасность и контроль
2. Защита интеллектуальной собственности
3. Юридическая ответственность
Нет, ссылке нельзя присвоить другой адрес памяти после инициализации. Это одно из ключевых различий между ссылками и указателями в C++.
Ссылка связана с объектом навсегда
Когда вы инициализируете ссылку, она привязывается к конкретному объекту. После инициализации эта связь неразрывна:
int a = 10;
int b = 20;
int& ref = a; // ссылка привязана к переменной a
// ref теперь всегда указывает на a
ref = b; // ЭТО НЕ переприсваивает ссылку!
// Это присваивает значение b переменной a
// a становится равна 20, ref всё ещё указывает на a
cout << a << endl; // выведет 20
cout << ref << endl; // выведет 20 (потому что это а)
Почему нельзя переприсвоить
Это архитектурное решение языка C++:
Сложность поиска в бинарном дереве поиска (BST) будет не логарифмической в следующих случаях:
1. Несбалансированное бинарное дерево (вырождение в список)
Когда дерево становится несбалансированным, оно может вырождаться в односвязный список. В худшем случае все элементы находятся только в правом (или только в левом) поддереве:
// Вырожденное дерево O(n)
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
};
// Пример вырождения: вставляем уже отсортированный массив
TreeNode* root = nullptr;
for (int x : {1, 2, 3, 4, 5}) {
// Вставляем - получаем цепочку (right, right, right, ...)
}
// Поиск любого элемента требует O(n) операций
Это происходит когда:
2. Не является деревом поиска
Плюсы и минусы многопоточности в backend-разработке
Многопоточность — критический инструмент в современной backend-разработке, но она сложна и требует глубокого понимания. Давайте разберём обе стороны.
Плюсы многопоточности
Использование многоядерных процессоров — это главное преимущество. На 16-ядерном сервере многопоточный код может одновременно выполняться на всех ядрах, давая 16-кратное увеличение пропускной способности. Однопоточное приложение использует только одно ядро.
Блокирующие операции — IO (сетевые запросы, работа с БД, файлы) требует времени. Многопоточность позволяет одному потоку ждать IO, пока другие обрабатывают вычисления или служат другим клиентам. Без потоков сервер будет в простое во время каждого запроса к БД.
Масштабируемость — при правильной реализации многопоточное приложение может обслуживать тысячи одновременных подключений. Например, веб-сервер может иметь пул потоков, каждый обрабатывает свой клиент.
Чем отличается malloc от new в C++?
malloc и new — это два разных способа динамического выделения памяти. Хотя оба выделяют память в heap, они работают по-разному и предназначены для разных целей.
Основные различия
| Параметр | malloc | new |
|---|---|---|
| Тип | Функция C | Оператор C++ |
| Заголовок | <cstdlib> | встроено в язык |
| Возвращаемое значение | void* | Типизированный указатель |
| Конструкторы | НЕ вызывает | Вызывает |
| Деструкторы | НЕ вызывает (free) | Вызывает (delete) |
| Инициализация | Нет | Есть (в скобках) |
| Обработка ошибок | Возвращает NULL | Выбрасывает исключение |
| Пользовательская аллокация | Нет (только в C++11+) | Можно перегрузить |
| Memory pool / Arena | Сложно | Легко (placement new) |
1. malloc — выделение памяти без инициализации
Input/Output (I/O) операции
Input/Output — это процесс обмена данными между программой и внешним миром (файловая система, сеть, консоль, устройства и т.д.). Это фундаментальная концепция в программировании и системном дизайне.
Определение
Input (Ввод) — получение данных из внешних источников:
Output (Вывод) — отправка данных во внешние места:
1. I/O в C++ — Streams (потоки)
C++ использует концепцию streams (потоков) для абстракции I/O операций. Все I/O операции работают через специальные объекты.
Основные потоки:
#include <iostream>
#include <fstream>
Что критично в новой работе для C/C++ Backend Developer
Выбор нового места работы — важное решение. Вот что я считаю критичным:
1. Технический стек и архитектура
Критично:
Важно:
Красный флаг:
2. Команда и культура
Принципы объектно-ориентированного программирования (ООП)
ОООП — парадигма программирования, основанная на концепции объектов, которые содержат данные (состояние) и методы (поведение). В C++ это реализуется через классы, структуры и наследование.
1. Инкапсуляция (Encapsulation)
Инкапсуляция — принцип скрытия внутренних деталей реализации и предоставления контролируемого интерфейса для взаимодействия. Это достигается через модификаторы доступа: private, protected, public.
class BankAccount {
private:
double balance; // скрыто от внешнего доступа
public:
void deposit(double amount) {
if (amount > 0) balance += amount;
}
double getBalance() const { return balance; }
};
Преимущества: защита инвариантов объекта, возможность изменить внутреннюю реализацию без влияния на код клиентов.
2. Наследование (Inheritance)
Какие знаешь итераторы?
Итераторы — это одна из центральных абстракций в C++ STL. Это обобщение указателей, позволяющее единообразно работать с разными контейнерами.
Категории итераторов
Итераторы организованы в иерархию по мощности:
Позволяет произвольный доступ, как обычный указатель: доступ по индексу O(1), арифметика, сложение с числом, сравнение.
Контейнеры: std::vector, std::deque, std::array, std::string
Можно идти вперёд и назад. Но NO random access.
Контейнеры: std::list, std::set, std::map, std::multiset, std::multimap
Только вперёд, одна позиция за раз.
Контейнеры: std::forward_list, std::unordered_set, std::unordered_map
Можно только читать, одна позиция за раз. Примеры: std::istream_iterator, std::istreambuf_iterator.
Разница между ссылкой и указателем в C++
Это одна из самых важных различий в C++. Оба работают с адресами памяти, но имеют фундаментально разное поведение.
Основные различия
Указатель (pointer)
int value = 42;
int* ptr = &value;
std::cout << *ptr; // 42
ptr = nullptr; // OK
Ссылка (reference)
int value = 42;
int& ref = value;
std::cout << ref; // 42 (без *)
ref = nullptr; // ОШИБКА компиляции
Ключевые различия в коде
Переприсвоение
int a = 10, b = 20;
int* ptr = &a;
ptr = &b; // ✅ Указатель переприсвоен на b
Placement New: конструирование объектов в предвыделенной памяти
Placement new — это продвинутая техника C++, которая позволяет создавать (конструировать) объекты в уже выделенной памяти, вместо выделения новой памяти. Это не выделяет новую память, а вызывает конструктор объекта в уже существующей области памяти.
Основная синтаксис
// Обычный new — выделяет память И вызывает конструктор
MyClass* obj = new MyClass(args);
// Placement new — вызывает конструктор в существующей памяти
MyClass* obj = new (ptr) MyClass(args); // ptr — адрес памяти
Практический пример 1: Использование стека
class Point {
public:
int x, y;
Point(int x = 0, int y = 0) : x(x), y(y) {
std::cout << "Constructor called\n";
}
~Point() {
std::cout << "Destructor called\n";
}
};
Что такое Mock в тестировании
Mock — это имитация реального объекта, которая используется для изоляции компонента при тестировании. Это ключевой инструмент для написания быстрых, надёжных unit тестов.
Основное определение
Mock — это объект-заместитель, который:
Различие между Mock, Stub, Fake и Spy
Это часто путают, но это разные инструменты:
// Интерфейс реальной зависимости
class DatabaseInterface {
public:
virtual ~DatabaseInterface() = default;
virtual User getUser(int id) = 0;
virtual void saveUser(const User& user) = 0;
virtual void deleteUser(int id) = 0;
};
Процесс загрузки BIOS
BIOS (Basic Input/Output System) — это первая программа, запускаемая при включении компьютера. Он подготавливает железо для загрузки операционной системы.
Этап 1: POST (Power-On Self-Test)
При включении питания процессор выполняет следующее:
Если обнаружены критические ошибки — выводится звуковой сигнал (beep codes), загрузка прерывается.
Этап 2: Инициализация оборудования
BIOS инициализирует:
Возвращаемое значение std::find_if при неудаче
std::find_if возвращает итератор на конец диапазона (end iterator) в случае, если элемент, удовлетворяющий условию, не найден.
Сигнатура функции
template<class InputIt, class UnaryPredicate>
InputIt find_if(InputIt first, InputIt last, UnaryPredicate p);
Функция ищет первый элемент в диапазоне [first, last), для которого предикат p возвращает true.
Поведение при успехе и неудаче
#include <algorithm>
#include <vector>
#include <iostream>
Плюсы и минусы MFC (Microsoft Foundation Classes)
MFC — один из старейших C++ фреймворков для разработки Windows приложений. Несмотря на возраст, используется в legacy и enterprise-проектах.
Плюсы MFC
1. Огромное сообщество и документация MFC существует с 1992 года, поэтому в сети огромное количество примеров, туториалов и решений. Разработчик легко найдёт ответ на любой вопрос.
2. Зрелость и стабильность Фреймворк прошёл проверку временем, используется в тысячах production-приложений. Баги давно найдены, API стабилен и не меняется.
3. Полная интеграция с Windows API MFC — тонкая обёртка над WinAPI, позволяет легко добраться до низкоуровневых функций системы без ненужных абстракций.
4. Производительность Приложения очень быстрые — нативный код без лишних слоёв абстракции. Минимальные накладные расходы.
5. Встроенные компоненты Включает поддержку диалогов, меню, базовых контролов, документов, drag-and-drop и других UI элементов из коробки.
Спецификаторы методов в C++
Спецификаторы методов — ключевые слова, которые определяют поведение, доступность и особенности работы методов класса. Это критические инструменты для создания безопасного и предсказуемого кода.
1. const спецификатор
const метод — метод, который не изменяет состояние объекта. Гарантирует, что на члены-данные не будут изменены.
class String {
public:
int length() const { // const метод
return str.size();
}
void setContent(const std::string& s) { // non-const метод
str = s;
}
private:
std::string str;
};
const String immutable = "hello";
immutable.length(); // OK - const метод
immutable.setContent("world"); // ERROR - non-const метод!
Преимущества:
2. virtual спецификатор
Как работает виртуальность в C++
Основная идея
Виртуальные функции позволяют вызвать нужный метод в зависимости от реального типа объекта, а не от типа указателя/ссылки.
Это основа полиморфизма в C++.
Простой пример
class Animal {
public:
virtual void speak() {
std::cout << "Some sound\n";
}
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "Woof!\n";
}
};
class Cat : public Animal {
public:
void speak() override {
std::cout << "Meow!\n";
}
};
int main() {
Dog dog;
Cat cat;
Animal* animals[] = {&dog, &cat};
for (auto a : animals) {
a->speak(); // Вызвать нужный speak() в зависимости от реального типа
}
// Output:
// Woof!
// Meow!
}
Внутреннее устройство: Virtual Method Table (VMT)
Компилятор использует таблицу виртуальных методов для реализации полиморфизма.
Умные указатели в C++
Умные указатели (smart pointers) — это шаблонные классы, которые обёртывают сырые указатели и автоматизируют управление памятью через RAII паттерн. Это одна из ключевых инноваций Modern C++, избавляющая от необходимости вручную вызывать delete.
Основные типы
std::unique_ptr
Уникальный указатель с эксклюзивным владением ресурсом:
std::unique_ptr<MyClass> ptr1(new MyClass());
std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
// ptr1 теперь nullptr, ptr2 владеет объектом
// При выходе ptr2 из области видимости объект удаляется
Особенности:
std::shared_ptr
Разделённое владение ресурсом через подсчёт ссылок (reference counting):
Опишите TCP 3-way handshake
TCP 3-way handshake (трёхсторонний процесс установления соединения) — это последовательность из трёх пакетов, которую выполняют два хоста для установления надёжного TCP-соединения. Этот процесс гарантирует, что оба хоста готовы к обмену данными и синхронизирует начальные номера последовательности (sequence numbers).
Этапы 3-way handshake
Этап 1: SYN (Synchronize)
Клиент → Сервер
├─ Flag: SYN
├─ SEQ: x (начальный номер последовательности клиента)
├─ ACK: 0 (нет подтверждения)
└─ Состояние клиента: SYN_SENT
Этап 2: SYN-ACK (Synchronize + Acknowledge)
Сервер → Клиент
├─ Flag: SYN + ACK
├─ SEQ: y (начальный номер последовательности сервера)
├─ ACK: x + 1 (подтверждение SEQ клиента)
└─ Состояние сервера: SYN_RECEIVED
Этап 3: ACK (Acknowledge)
Клиент → Сервер
├─ Flag: ACK
├─ SEQ: x + 1
├─ ACK: y + 1 (подтверждение SEQ сервера)
└─ Состояние клиента: ESTABLISHED
└─ Состояние сервера: ESTABLISHED
Что такое процесс?
Процесс — это экземпляр программы, находящийся в состоянии выполнения в операционной системе. Это не просто код программы, а активный субъект, который имеет собственное адресное пространство, системные ресурсы и состояние.
Основные компоненты процесса
Процесс включает:
Жизненный цикл процесса
Процесс может находиться в состояниях:
Протокол FTP (File Transfer Protocol)
FTP — один из самых старых и всё ещё используемых протоколов для передачи файлов. Хотя во многом устаревший, знание FTP важно для backend-разработчика, работающего с legacy системами.
Что такое FTP?
FTP (File Transfer Protocol) — это протокол прикладного уровня (Layer 7 OSI) для передачи файлов между компьютерами в сети. Он работает на TCP/IP и использует два отдельных соединения: одно для команд, другое для данных.
Основная идея: клиент подключается к FTP серверу, аутентифицируется (или входит как anonymous), затем может загружать, скачивать, удалять, переименовывать файлы.
Архитектура: два соединения
Это ключевое отличие FTP от HTTP:
Control connection (Port 21)
Что происходит при выбрасывании исключения из конструктора?
Краткий ответ
Когда из конструктора выбрасывается исключение:
Детальный процесс
class Resource {
public:
Resource(int id) {
std::cout << "Конструктор начался\n";
if (id < 0) {
throw std::invalid_argument("ID не может быть отрицательным");
}
this->id = id;
std::cout << "Конструктор завершился успешно\n";
}
~Resource() {
std::cout << "Деструктор вызван\n";
}
private:
int id;
};
Выбор структуры данных для хранения целых чисел с частым добавлением
При выборе структуры данных для большого количества целых чисел с частыми операциями добавления нужно анализировать несколько факторов: частоту операций, объем памяти, паттерны доступа и требования к производительности. Рассмотрю лучшие варианты.
1. std::vector (Динамический массив)
Лучший выбор в большинстве случаев:
#include <vector>
std::vector<int> numbers;
// Добавление элементов
for (int i = 0; i < 1000000; i++) {
numbers.push_back(i); // Амортизированная O(1)
}
// Резервирование памяти заранее
numbers.reserve(1000000); // Избегаем лишних переаллокаций
// Итерирование очень быстро
for (int num : numbers) {
// Доступ: O(1)
}
Преимущества:
Какой контейнер лежит в основе map?
map в C++ Standard Library строится на основе красно-чёрного дерева (Red-Black Tree). Это самобалансирующееся бинарное дерево поиска, которое обеспечивает эффективность операций благодаря поддержанию баланса.
Почему именно красно-чёрное дерево?
Красно-чёрное дерево выбрано потому, что оно гарантирует:
Структура узла красно-чёрного дерева
struct Node {
Key key;
Value value;
Node* left;
Node* right;
Node* parent;
Color color; // RED или BLACK
};
Правила красно-чёрного дерева
Устройство std::set
std::set — это контейнер стандартной библиотеки C++, который хранит уникальные элементы в отсортированном порядке. Понимание его внутреннего устройства критично для написания эффективного кода.
Внутренняя структура: красно-чёрное дерево
std::set реализован на основе красно-чёрного дерева (red-black tree) — сбалансированного бинарного дерева поиска:
// Упрощённая структура узла
template<typename T>
struct Node {
T value;
Node* left;
Node* right;
Node* parent;
Color color; // RED или BLACK
};
Свойства красно-чёрного дерева
Эти свойства гарантируют, что высота дерева всегда O(log n).
Сложность операций
std::set<int> s;
Как устроен Map в C++
std::map — это упорядоченный контейнер, который хранит пары ключ-значение в отсортированном порядке по ключам. Это один из фундаментальных контейнеров стандартной библиотеки C++, и понимание его внутреннего устройства критично для написания эффективного кода.
Внутренняя структура: Red-Black Tree
std::map реализует свои данные с помощью красно-чёрного дерева (Red-Black Tree) — это самобалансирующееся двоичное дерево поиска:
#include <map>
#include <iostream>
int main() {
// Внутренняя структура map:
// [5]
// / \
// [3] [7]
// / \ / \
// [1][4][6][8]
std::map<int, std::string> map;
map[5] = "five";
map[3] = "three";
map[7] = "seven";
// Элементы хранятся в отсортированном порядке
for (auto& [key, value] : map) {
std::cout << key << ": " << value << std::endl; // 3, 5, 7
}
}
Свойства Red-Black Tree
Ответ: Сборка C++ - многоэтапный процесс от источника к исполняемому файлу
Процесс сборки (build) C++ проекта состоит из нескольких этапов: препроцессинг, компиляция, оптимизация и линковка.
Этап 1: Препроцессинг (Preprocessing)
// main.cpp
#include <iostream>
#define MAX 100
#ifdef DEBUG
#define PRINT(x) std::cout << x << std::endl
#else
#define PRINT(x)
#endif
int main() {
PRINT("Hello");
return 0;
}
Препроцессор (обычно cpp):
g++ -E main.cpp > main.i # Выводит результат препроцессинга
Результат: файл main.i с развёрнутыми include и макросами.
Этап 2: Компиляция (Compilation)
Компилятор (gcc, clang) преобразует исходный код в ассемблер:
g++ -S main.cpp # Создаёт main.s (ассемблер)
Как послать сигнал процессу
Что такое сигналы
Сигналы — это способ асинхронной коммуникации между процессами в Unix/Linux. Это программное прерывание, которое может заставить процесс выполнить определённое действие.
Основные сигналы:
1. Команда kill из shell
Простейший способ — использовать kill:
# Отправить SIGTERM (15) процессу с PID 1234
kill 1234
# Отправить конкретный сигнал
kill -SIGTERM 1234
kill -15 1234
# Принудительное завершение (SIGKILL)
kill -9 1234
# Остановить процесс
kill -SIGSTOP 1234
# Возобновить процесс
kill -SIGCONT 1234
Способы передачи параметров в функцию на C++
В C++ существует несколько способов передачи параметров в функцию, каждый из которых имеет свои преимущества и недостатки. Выбор правильного способа критически важен для производительности и безопасности кода.
1. Передача по значению (Pass by Value)
Параметр копируется при вызове функции. Изменения внутри функции не влияют на оригинальный параметр.
void process_value(int x) {
x = 100; // Меняем копию, не оригинал
}
int main() {
int a = 5;
process_value(a);
std::cout << a << std::endl; // 5 (не изменилось)
}
Преимущества:
Недостатки:
Когда использовать:
2. Передача по ссылке (Pass by Reference)
Предпочтение: Командная работа
С 10+ годами опыта я предпочитаю работать в команде. Это основано на глубоком понимании того, как достигаются лучшие результаты в разработке ПО.
Почему командная работа эффективнее
Разнообразие перспектив
Каждый разработчик видит проблему по-своему. Code review выявляет баги, которые я мог пропустить. Архитектурные решения обсуждаются и улучшаются благодаря разным точкам зрения.
Распределение знания
Нет одного узкого места (single point of failure). Если я в отпуске, проекты не замораживаются. Knowledge sharing позволяет разработчикам учиться друг у друга.
Повышение качества кода
Peer review значительно снижает количество ошибок. Code review — это возможность учиться и развиваться.
Роль опытного разработчика в команде
Опыт работы в командах
За мою карьеру из более чем 10 лет в системном программировании и разработке backend-систем на C/C++ я имел удовольствие работать в разнообразных командах различных масштабов и направлений.
Высоконагруженные системы (HighLoad)
Текущий проект — это разработка высоконагруженного backend'а для платформы обработки больших данных. Наша команда состоит из 8 инженеров, и мы работаем над:
В этом проекте я занимался оптимизацией критических путей, снижением latency с 500ms до 50ms через переписание горячих функций и использование SIMD инструкций.
Микросервисная архитектура
Работал в команде из 15 человек на проекте микросервисной архитектуры для финтеха. Мой вклад:
Полиморфизм в ООП
Полиморфизм — это ключевое понятие объектно-ориентированного программирования, которое означает "многоформность". Это способность объектов разных типов отвечать на один и тот же запрос (вызов метода), но выполняя разные действия в соответствии с их типом. Полиморфизм позволяет писать более гибкий и переиспользуемый код.
Типы полиморфизма
В C++ существует два основных вида полиморфизма:
Перегрузка функций — одна из форм статического полиморфизма, когда несколько функций имеют одно имя, но разные параметры:
void print(int x) { std::cout << "Int: " << x << std::endl; }
void print(double x) { std::cout << "Double: " << x << std::endl; }
void print(const std::string& x) { std::cout << "String: " << x << std::endl; }
print(42); // Вызовет print(int)
print(3.14); // Вызовет print(double)
print("Hello"); // Вызовет print(string)
shared_ptr в C++
shared_ptr — это один из наиболее важных инструментов современного C++ для управления динамической памятью. Это умный указатель из стандартной библиотеки, реализующий концепцию разделяемого владения памятью.
Что такое shared_ptr?
shared_ptr — это шаблонный класс, который автоматически управляет жизненным циклом объекта, используя подсчёт ссылок (reference counting). Объект удаляется из памяти только когда на него нет ни одной ссылки.
Как это работает?
#include <memory>
// Создание shared_ptr
std::shared_ptr<int> ptr1(new int(42));
std::shared_ptr<int> ptr2 = ptr1; // Копирование
std::shared_ptr<int> ptr3 = std::make_shared<int>(42); // Лучший способ
// Счётчик ссылок = 3 (ptr1, ptr2, ptr3)
ptr1.reset(); // Счётчик = 2
// Объект удаляется только когда счётчик = 0
Ключевые особенности
Выбор контейнеров STL
Правильный выбор контейнера критичен для производительности. Вот детальный путеводитель с примерами.
Последовательные контейнеры
std::vector
Используй: В 90% случаев это твой выбор по умолчанию.
// Отлично для:
// - Случайный доступ O(1)
// - Стабильная память (iterator invalidation predictable)
// - Удалению в конце (амортизированная O(1))
std::vector<int> v = {1, 2, 3};
v.push_back(4); // O(1) амортизированная
int x = v[500]; // O(1)
v.erase(v.end() - 1); // O(1)
v.erase(v.begin()); // O(n) - плохо!
Избегай: Удалений в начале/середине часто. Память: Динамический массив, capacity растёт 1.5x или 2x.
std::deque
Используй: Когда нужны операции O(1) в обоих концах.
// Отлично для:
// - push_back/pop_back O(1)
// - push_front/pop_front O(1) (vector не может!)
// - Случайный доступ O(1)
Что такое выполнение рукопожатия в HTTPS?
HTTPS handshake (рукопожатие) — это процесс установления защищённого соединения между клиентом и сервером перед передачей данных. Это многоэтапный протокол (TLS/SSL), который обеспечивает аутентификацию, согласование алгоритмов шифрования и обмена ключами.
Этапы TLS 1.2 handshake
1. Client Hello
2. Server Hello
3. Проверка сертификата