Стандартный класс
std::string в C++ имеет несколько механизмов оптимизации для повышения производительности и уменьшения расхода памяти. Рассмотрим основные из них. Если строка маленькая (обычно до 15-23 байт, зависит от реализации),
std::string не выделяет динамическую память, а хранит символы прямо внутри объекта std::string. Обычно
std::string содержит: - Указатель на динамическую память
- Размер строки
- Вместимость (
capacity) Но если строка короткая, память не выделяется в куче (
heap), а данные хранятся прямо внутри объекта std::string. #include <iostream>
#include <string>
int main() {
std::string small = "Hello"; // Использует SSO
std::string large = "This is a very long string that will not fit SSO"; // Динамическая память
std::cout << "Small string: " << small << "\n";
std::cout << "Large string: " << large << "\n";
}
Раньше (до C++11)
std::string использовал технику Copy-on-Write (COW), где несколько строк могли разделять одну и ту же память, пока одна из них не изменялась. Почему отказались?
Не потокобезопасно (если один поток меняет строку, другой может получить неожиданные данные). В C++11 было добавлено
std::move, которое делает копирование дешевле. SSO делает COW менее нужной. std::string a = "Hello";
std::string b = a; // Копия не создается, оба указывают на одну память
b[0] = 'X'; // Теперь создаётся отдельная копия
Вместо дорогостоящего копирования
std::string теперь можно перемещать с помощью std::move(), передавая владение ресурсом без копирования. #include <iostream>
#include <string>
int main() {
std::string a = "Hello, world!";
std::string b = std::move(a); // Перемещение, a становится пустой
std::cout << "b: " << b << "\n"; // "Hello, world!"
}
Если заранее известно, сколько символов потребуется, можно использовать
.reserve(size), чтобы избежать многократных перевыделений памяти. std::string str;
str.reserve(100); // Сразу выделяет память на 100 символов
for (int i = 0; i < 100; i++) {
str += 'a'; // Не перевыделяет память 100 раз
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1💊1
Чтобы взвести (установить) бит в определённое значение (1) в числе, можно использовать побитовую операцию ИЛИ (
|). Эта операция позволяет установить конкретный бит в 1, не изменяя остальные биты числа.Операция ИЛИ (
|) сравнивает каждый бит двух чисел. Если хотя бы один из битов в соответствующей позиции равен 1, результат в этой позиции будет 1. Иначе, результат будет 0.n и нули в остальных позициях. Это можно сделать с помощью выражения 1 << n.x и маской.#include <iostream>
int main() {
int x = 0b00001010; // Число, с которым работаем (10 в десятичной системе)
int n = 3; // Позиция бита, которую хотим установить (начиная с 0)
// Создаём маску с единицей в позиции n
int mask = 1 << n;
// Устанавливаем бит в позиции n
x = x | mask;
// Вывод результата
std::cout << "Результат: " << std::bitset<8>(x) << std::endl; // Двоичный вывод
std::cout << "Результат: " << x << std::endl; // Десятичный вывод
return 0;
}
1 << n создает маску, сдвигая 1 влево на n позиций. Например, если n = 3, результат будет 0b00001000. Операция x | mask устанавливает бит в x на позиции n в 1. Если бит в x на позиции n уже был 1, он останется 1, если он был 0, то станет 1.Если
x было 0b00001010 и n = 3, результат будет:Маска:
0b00001000x | mask: 0b00001010 | 0b00001000 = 0b00001010В результате бит в позиции 3 установлен в 1, и итоговое значение числа в двоичном формате
0b00001010 (десятичное 10).Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
2. Контейнер использует методы разрешения коллизий:
o Связанные списки (chaining): все элементы с одним хешем добавляются в связанный список внутри одного bucket'а.
o Открытая адресация: поиск свободной ячейки для хранения данных.
3. После нахождения bucket'а выполняется проверка на равенство ключей с помощью метода equals.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Используется для обозначения переменных, значение которых может изменяться неожиданно для компилятора. Это может происходить, например, из-за внешних факторов вне контроля программы, таких как аппаратные регистры, прерывания или многопоточные среды.
volatile говорит компилятору, что он не должен оптимизировать код, касающийся этих переменных, и всегда должен считывать их значения непосредственно из памяти.Переменные, связанные с аппаратными устройствами, такими как порты ввода-вывода, часто меняются вне контроля программы. Использование
volatile предотвращает оптимизации, которые могли бы кэшировать значение регистров. volatile int* port = reinterpret_cast<int*>(0x400);
*port = 42; // Запись в аппаратный регистр
В системах реального времени и встроенных системах значения переменных могут изменяться в обработчиках прерываний.
volatile гарантирует, что каждое обращение к такой переменной будет действительным. volatile bool interruptFlag = false;
void interruptHandler() {
interruptFlag = true; // Устанавливается в обработчике прерывания
}
void mainFunction() {
while (!interruptFlag) {
// Ожидание установки флага прерывания
}
// Обработка прерывания
}
Переменные, которые могут изменяться из других потоков, также могут быть помечены как
volatile. Однако следует отметить, что volatile не обеспечивает защиту от гонок данных и не заменяет средства синхронизации, такие как мьютексы или атомарные операции. volatile bool stopThread = false;
void workerThread() {
while (!stopThread) {
// Выполнение работы
}
}
int main() {
std::thread t(workerThread);
// ...
stopThread = true; // Остановка рабочего потока
t.join();
return 0;
}
volatile предотвращает оптимизации компилятором, которые могли бы удалить или кэшировать доступы к переменной.volatile не обеспечивает потокобезопасность. Для обеспечения корректной работы в многопоточной среде следует использовать мьютексы, атомарные операции или другие механизмы синхронизации.Переменные могут быть как
const volatile, если они не должны изменяться программой, но могут изменяться внешними факторами.#include <iostream>
volatile int* hardwareRegister = reinterpret_cast<int*>(0x400);
void writeToRegister(int value) {
*hardwareRegister = value; // Запись в регистр
}
int readFromRegister() {
return *hardwareRegister; // Чтение из регистра
}
int main() {
writeToRegister(100);
std::cout << "Register value: " << readFromRegister() << std::endl;
return 0;
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2🔥1
Forwarded from Идущий к IT
🔥 Записал видос "Как за 3 минуты настроить Автоотклики на вакансии HeadHunter" больше не придется заниматься этой унылой рутиной
📺 Видео: https://youtu.be/G_FOwEGPwlw
Please open Telegram to view this post
VIEW IN TELEGRAM
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
Чтобы класс мог использоваться в качестве ключа в ассоциативных контейнерах (например, в
std::map или std::unordered_map), он должен удовлетворять определённым требованиям. Класс должен иметь определённый оператор сравнения. По умолчанию,
std::map и std::set используют оператор operator< для сравнения ключей. Это необходимо, чтобы контейнер мог упорядочивать ключи. В этом примере класс MyKey имеет перегруженный оператор operator<, что позволяет использовать его в качестве ключа в std::map.#include <iostream>
#include <map>
class MyKey {
public:
int value;
MyKey(int v) : value(v) {}
bool operator<(const MyKey& other) const {
return value < other.value;
}
};
int main() {
std::map<MyKey, std::string> myMap;
myMap[MyKey(1)] = "one";
myMap[MyKey(2)] = "two";
for (const auto& pair : myMap) {
std::cout << pair.first.value << ": " << pair.second << std::endl;
}
return 0;
}
Класс должен быть хэшируемым. Это означает, что для него должна быть определена функция хэширования. В стандартной библиотеке C++ можно определить специализированный шаблон
std::hash для вашего класса.Класс должен иметь определённый оператор
operator==. В этом примере для класса MyKey определён оператор operator== и специализированный шаблон std::hash, что позволяет использовать его в качестве ключа в std::unordered_map.#include <iostream>
#include <unordered_map>
#include <functional>
class MyKey {
public:
int value;
MyKey(int v) : value(v) {}
bool operator==(const MyKey& other) const {
return value == other.value;
}
};
namespace std {
template <>
struct hash<MyKey> {
std::size_t operator()(const MyKey& k) const {
return std::hash<int>()(k.value);
}
};
}
int main() {
std::unordered_map<MyKey, std::string> myMap;
myMap[MyKey(1)] = "one";
myMap[MyKey(2)] = "two";
for (const auto& pair : myMap) {
std::cout << pair.first.value << ": " << pair.second << std::endl;
}
return 0;
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Являются контейнерами из стандартной библиотеки, но они значительно различаются по своей структуре и оптимальности использования в разных ситуациях.
Это динамический массив, который обеспечивает быстрый доступ к элементам по индексу. Вектор хранит свои элементы в непрерывном блоке памяти, что делает возможным обращение к элементам за константное время
O(1). Однако, такое хранение делает вставку и удаление элементов в начало или середину вектора менее эффективными (в среднем O(n), где n — количество элементов), так как может потребоваться перемещение большого количества элементов для освобождения места или заполнения пробела.#include <vector>
#include <iostream>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.push_back(6); // Добавление элемента в конец вектора
std::cout << "Элемент по индексу 3: " << vec[3] << std::endl; // Быстрый доступ к элементам
return 0;
}
В отличие от
std::vector, представляет собой двусвязный список. Каждый элемент содержит данные и указатели на предыдущий и следующий элементы в списке. Это обеспечивает быструю вставку и удаление элементов в любом месте списка, так как требуется только изменение нескольких указателей (O(1)), независимо от размера списка. Однако, доступ к элементам в std::list осуществляется за линейное время O(n), так как для доступа к элементу нужно пройти через указатели от начала или конца списка.#include <list>
#include <iostream>
int main() {
std::list<int> lst = {1, 2, 3, 4, 5};
lst.push_front(0); // Быстрая вставка в начало списка
lst.push_back(6); // Быстрая вставка в конец списка
for (auto it = lst.begin(); it != lst.end(); ++it) {
std::cout << *it << " ";
}
std::cout << std::endl;
return 0;
}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥1
PIMPL расшифровывается как Pointer to IMPLementation (Указатель на реализацию). Это паттерн проектирования, который используется в C++ для разделения интерфейса и реализации с целью скрытия деталей реализации и уменьшения зависимости от заголовочных файлов.
Вместо того чтобы хранить данные прямо в классе, мы используем указатель на структуру реализации (
Impl). Без PIMPL (проблема: утечка зависимостей)
// File: MyClass.h
#include <string> // Подключаем заголовок, влияет на все файлы
class MyClass {
private:
std::string data; // Прямое хранение данных
public:
MyClass();
void print();
};
С PIMPL (скрываем детали реализации)
// File: MyClass.h
#include <memory>
class MyClass {
private:
struct Impl; // Объявляем, но не определяем
std::unique_ptr<Impl> pImpl; // Умный указатель на реализацию
public:
MyClass(); // Конструктор
~MyClass(); // Деструктор
void print();
};
// File: MyClass.cpp
#include "MyClass.h"
#include <iostream>
#include <string>
// Определяем реализацию
struct MyClass::Impl {
std::string data = "Hello, PIMPL!";
void print() { std::cout << data << std::endl; }
};
// Реализация методов
MyClass::MyClass() : pImpl(std::make_unique<Impl>()) {}
MyClass::~MyClass() = default;
void MyClass::print() { pImpl->print(); }
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1🔥1
Методы разрешения коллизий:
1. Цепочки (chaining): элементы с одинаковым хэш-значением хранятся в связанном списке или другой структуре.
2. Открытая адресация (open addressing): ищется следующая доступная ячейка для хранения элемента.
Коллизии снижают производительность, поэтому важно выбирать хорошие хэш-функции.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Range-based for loop – это упрощённый цикл
for, который позволяет перебирать элементы контейнера (std::vector, std::array, std::map, std::set и т. д.) без индексов и итераторов. for (auto element : container) {
// Действие с element
}Простой пример с
std::vector #include <iostream>
#include <vector>
int main() {
std::vector<int> v = {1, 2, 3, 4, 5};
for (int x : v) { // Перебираем все элементы вектора
std::cout << x << " ";
}
}
Вывод
1 2 3 4 5
Как работает этот цикл?
Компилятор превращает его в обычный
for с итератором: for (auto it = v.begin(); it != v.end(); ++it) {
int x = *it; // Копируем элемент
std::cout << x << " ";
}Передача по значению (
=) – создаёт копию элементаfor (int x : v) { // x - копия элемента
x = 100; // НЕ изменит вектор!
}Передача по ссылке (
&) – изменяет оригиналfor (int& x : v) { // x - ссылка на элемент
x *= 2; // Изменит оригинальный вектор!
}С
std::mapstd::map<int, std::string> m = {{1, "one"}, {2, "two"}};
for (const auto& [key, value] : m) { // structured binding (C++17)
std::cout << key << " -> " << value << "\n";
}С
std::setstd::set<int> s = {1, 2, 3, 4};
for (int x : s) { std::cout << x << " "; }for (int x : {10, 20, 30}) {
std::cout << x << " ";
}Вывод
10 20 30
int arr[] = {1, 2, 3};
for (int x : arr) { std::cout << x << " "; }Работает так же, как и с `std::vector`!
Работает с пользовательскими классами (если есть
begin() и end()) class MyContainer {
int data[3] = {10, 20, 30};
public:
int* begin() { return data; }
int* end() { return data + 3; }
};
int main() {
MyContainer c;
for (int x : c) { std::cout << x << " "; }
}Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Чтобы убедиться, что в коде нет багов, используют различные методы и инструменты проверки. Рассмотрим основные подходы:
Перед тем как запускать код, полезно просмотреть его вручную или с коллегами. Это помогает:
Найти логические ошибки
Улучшить читаемость кода
Проверить соответствие код-стайлу
// Опытный разработчик может заметить, что деление на 0 возможно
int divide(int a, int b) {
return a / b; // Потенциальная ошибка, если b == 0
}
Лучше добавить проверку
int divide(int a, int b) {
if (b == 0) {
throw std::runtime_error("Деление на ноль!");
}
return a / b;
}Тесты проверяют каждую функцию отдельно, чтобы убедиться, что она работает правильно. В C++ используют Google Test (gtest), Catch2 или Boost.Test.
#include <gtest/gtest.h>
#include "math_utils.h"
TEST(DivideFunctionTest, HandlesZeroDenominator) {
EXPECT_THROW(divide(10, 0), std::runtime_error);
}
TEST(DivideFunctionTest, NormalCase) {
EXPECT_EQ(divide(10, 2), 5);
}
Если код работает не так, как ожидалось, используют отладчики (GDB, LLDB, Visual Studio Debugger). Они позволяют пошагово исполнять код и смотреть, где происходит ошибка.
g++ -g main.cpp -o program # Компилируем с отладочной информацией
gdb ./program # Запускаем отладчик
Используются специальные инструменты, которые автоматически ищут ошибки в коде:
Clang-Tidy
Cppcheck
SonarQube
cppcheck --enable=all my_code.cpp
Эти инструменты помогают находить утечки памяти, переполнения буфера и другие ошибки.
g++ -fsanitize=address -g main.cpp -o program
./program # Если есть проблемы, они будут найдены
Когда код протестирован отдельно, его проверяют в реальной среде. Это включает тестирование работы программы с базами данных, сетевыми запросами и другими модулями.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
МVC (Model-View-Controller) – это классический паттерн разделения логики приложения, но у него есть недостатки, особенно в сложных проектах.
Взаимодействие между Model, View и Controller может стать сложным. Когда модель изменяется, нужно обновлять все представления (
View), которые её используют. Если контроллер слишком тесно связан с представлением, код становится сложно поддерживать. В сложных GUI-приложениях (например, Qt, MFC) модель может изменяться из разных мест, и контроллеру трудно управлять обновлениями.
MVC не даёт чёткого разделения ответственности, если приложение очень сложное.
Один контроллер может управлять сразу несколькими представлениями, что создаёт запутанный код. Часто приходится создавать "промежуточные" контроллеры для разруливания логики → код становится сложнее.
В сложных веб-приложениях (React, Angular) MVC превращается в "спагетти", потому что представления (View) и контроллеры (Controller) начинают выполнять логические задачи, которые должны быть в модели.
Использовать слоистую архитектуру (Layered Architecture) или Service Layer.
Если бизнес-логика попадает в контроллер, он становится раздутым и сложно поддерживаемым. Контроллер начинает обрабатывать данные, проверять их, а не просто передавать управление.
class UserController {
public:
void login(std::string username, std::string password) {
if (username.empty() || password.empty()) {
std::cout << "Ошибка: пустые данные\n";
return;
}
if (username == "admin" && password == "1234") {
std::cout << "Вход выполнен!\n";
} else {
std::cout << "Неверный логин/пароль\n";
}
}
};Здесь контроллер сам проверяет данные и логику аутентификации, что нарушает принцип разделения ответственности (SRP).
Вынести бизнес-логику в Model или Service.
Использовать слоистую архитектуру (Service Layer, Repository Pattern).
class AuthService {
public:
bool authenticate(const std::string& username, const std::string& password) {
return (username == "admin" && password == "1234");
}
};
class UserController {
AuthService authService;
public:
void login(const std::string& username, const std::string& password) {
if (authService.authenticate(username, password)) {
std::cout << "Вход выполнен!\n";
} else {
std::cout << "Неверный логин/пароль\n";
}
}
};Model тестируется легко, потому что она "чистая" (без UI-кода).
Controller и View сложно тестировать, потому что они зависят от пользовательского ввода и интерфейса.
Unit-тестирование UI-кода практически невозможно, требуется мокирование.
В многопоточных приложениях несколько представлений могут обращаться к одной модели, вызывая гонки данных.
Если контроллер один на несколько потоков, он становится "узким местом" (bottleneck).
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
Если у базового класса есть хотя бы одна виртуальная функция, то у него создается одна таблица виртуальных функций (vtable). У производного класса также создается своя vtable, если он переопределяет виртуальные методы или добавляет новые.
- Если производный класс не добавляет новых виртуальных функций, он использует vtable родительского класса.
- Если переопределяет методы, создается отдельная vtable для производного класса.
Таким образом, в общем случае будет две таблицы vtable – по одной для каждого класса.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥4👍1
В unordered_set, который основан на хеш-таблице, самый худший случай поиска возникает, когда структура данных деградирует до такой степени, что время поиска становится линейным, то есть \(O(n)\), где \(n\) — количество элементов в контейнере. Это происходит по нескольким причинам:
Играет ключевую роль в распределении элементов по "ведрам" (buckets) в
unordered_set. Если хеш-функция плохо разрабатывается и генерирует одинаковые или близкие хеш-коды для большого количества ключей, элементы начинают накапливаться в очень небольшом числе ведер. Это приводит к тому, что большинство операций поиска, вставки и удаления начинают зависеть от числа элементов в самом длинном связном списке, а не от общего количества ведер, что сильно ухудшает производительность.Загрузка хеш-таблицы (
load factor) — это отношение количества элементов в хеш-таблице к количеству ведер. Когда load factor становится слишком высоким, вероятность коллизий увеличивается, что также приводит к увеличению длины цепочек в каждом ведре. Хотя unordered_set автоматически увеличивает количество ведер при увеличении количества элементов, в случаях с экстремально высоким load factor поиск может деградировать до линейного.Вставляются данные, которые неудачно распределяются хеш-функцией, даже если сама функция в целом хороша, это может привести к временной деградации производительности. Например, последовательная вставка элементов с одинаковым хешем приведет к удлинению одной цепочки.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
1. Конструкторы не возвращают значения, поэтому исключения — единственный способ сообщить о неудачной инициализации.
2. Исключения интегрируются с механизмами управления памятью, автоматически освобождая частично инициализированные ресурсы.
3. Они делают код более выразительным, отделяя логику инициализации от обработки ошибок.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM