🧠Задача о максимальной сумме подмассива (алгоритм Кадане) С++
Дан целочисленный массив, найдите в нем непрерывный подмассив с наибольшей суммой.
Например:
Мы можем легко решить эту задачу за линейное время, используя Алгоритм Кадане.
Идея состоит в том, чтобы поддерживать максимальный (с положительной суммой) подмассив, “заканчивающийся” на каждом индексе данного массива. Этот подмассив либо пуст (в этом случае его сумма равна нулю), либо состоит на один элемент больше, чем максимальный подмассив, оканчивающийся на предыдущем индексе.
Алгоритм может быть реализован следующим образом на C++:
результат:
The maximum sum of a contiguous subarray is 6
Временная сложность приведенного выше решения равна O(n) и не требует дополнительного места, где n это размер ввода.
Приведенный выше код не обрабатывает случай, когда все элементы массива отрицательные. Если массив содержит все отрицательные значения, ответом является максимальный элемент. Мы можем легко разместить эту проверку перед тем, как продолжить алгоритм. Реализацию можно увидеть ниже на C++:
Продолжение
@cpluspluc
Дан целочисленный массив, найдите в нем непрерывный подмассив с наибольшей суммой.
Например:
Input: {-2, 1, -3, 4, -1, 2, 1, -5, 4}
Output: Subarray with the largest sum is {4, -1, 2, 1} with sum 6.
Мы можем легко решить эту задачу за линейное время, используя Алгоритм Кадане.
Идея состоит в том, чтобы поддерживать максимальный (с положительной суммой) подмассив, “заканчивающийся” на каждом индексе данного массива. Этот подмассив либо пуст (в этом случае его сумма равна нулю), либо состоит на один элемент больше, чем максимальный подмассив, оканчивающийся на предыдущем индексе.
Алгоритм может быть реализован следующим образом на C++:
#include <iostream>
#include <vector>
using namespace std;
// Функция для нахождения максимальной суммы непрерывного подмассива
// в заданном целочисленном массиве
int kadane(vector<int> const &arr)
{
// сохраняет максимальный суммарный подмассив, найденный на данный момент
int max_so_far = 0;
// сохраняет максимальную сумму подмассива, заканчивающегося на текущей позиции
int max_ending_here = 0;
// обход заданного массива
for (int i = 0; i < arr.size(); i++)
{
// обновить максимальную сумму подмассива, "заканчивающегося" на индексе "i" (путем добавления
// текущий элемент до максимальной суммы, заканчивающейся на предыдущем индексе 'i-1')
max_ending_here = max_ending_here + arr[i];
// если максимальная сумма отрицательна, устанавливаем ее в 0 (что представляет
// пустой подмассив)
max_ending_here = max(max_ending_here, 0);
// обновить результат, если текущая сумма подмассива окажется больше
max_so_far = max(max_so_far, max_ending_here);
}
return max_so_far;
}
int main()
{
vector<int> arr = { -2, 1, -3, 4, -1, 2, 1, -5, 4 };
cout << "The maximum sum of a contiguous subarray is " << kadane(arr);
return 0;
}
результат:
The maximum sum of a contiguous subarray is 6
Временная сложность приведенного выше решения равна O(n) и не требует дополнительного места, где n это размер ввода.
Приведенный выше код не обрабатывает случай, когда все элементы массива отрицательные. Если массив содержит все отрицательные значения, ответом является максимальный элемент. Мы можем легко разместить эту проверку перед тем, как продолжить алгоритм. Реализацию можно увидеть ниже на C++:
Продолжение
@cpluspluc
📌 Что выведет код ?
▪Ответ
▪Деструкторы в С++
В приведенной выше программе id - статическая переменная, и она увеличивается при каждом создании объекта. Объект a[0] создается первым, но первым уничтожается объект a[2]. Объекты всегда уничтожаются в порядке, обратном их созданию. Причина обратного порядка заключается в том, что объект, созданный позже, может использовать ранее созданный объект.
@cpluspluc
#include <iostream>
using namespace std;
class A
{
int id;
static int count;
public:
A() {
count++;
id = count;
cout << "constructor for id " << id << endl;
}
~A() {
cout << "destructor for id " << id << endl;
}
};
int A::count = 0;
int main() {
A a[3];
return 0;
}
▪Ответ
▪Деструкторы в С++
@cpluspluc
Скомпилируйте библиотеку как статическую и включите ее в свой проект, включите файл ZTG.h
#include "ZTG/ZTG.H"
▪Github@cpluspluc
Please open Telegram to view this post
VIEW IN TELEGRAM
В чем принципиальное отличие ссылки от указателя в С++? Какие ограничения есть у первых, а какие у вторых?
Вот некоторые из отличий::
nullptr
:void f(int* num, int& num2)
{
if(num != nullptr) // if nullptr ignored algorithm
{
}
// can't check num2 on need to use or not
}
const
#include <iostream>
int main()
{
std::cout << "Hello, world!\n";
const int v = 10;
//int& const r = v; // Ошибка
const int& r = v;
enum
{
is_const = std::is_const<decltype(r)>::value
};
if(!is_const)
std::cout << "const int& r is not const\n";
else
std::cout << "const int& r is const\n";
}
@cpluspluc
Please open Telegram to view this post
VIEW IN TELEGRAM
#include <iostream>
#include <vector>
int main() {
std::vector<int> a{0,1,2,3,4,5};
int b = 0;
std::cout << a[b + 1] << std::endl; // подчеркивает 'b+1'
return 0;
}
возникает проблема:
Арифметическое переполнение: использование оператора "+" на байтовом значении 4 и приведение результата к байтовому значению 8. Приведите значение к более широкому типу перед вызовом оператора "+", чтобы избежать переполнения (io.2)
Почему возникает такое предупреждение?
b+1
имеет тип int
. Если предположить, что в b
будет максимальное значение, представимое типом int
, то получим UB. Решение: привести хотя бы один из операндов к 64-битному типу.
В общем, компилятор ругается здесь не на конкретные значения, а на типы, используемые в выражении.
@cpluspluc
Please open Telegram to view this post
VIEW IN TELEGRAM
C и C++ имеют свои собственные встроенные генераторы случайных чисел. Они реализованы в двух отдельных функциях, которые находятся в заголовочном файле
cstdlib
:srand()
устанавливает передаваемое пользователем значение в качестве стартового. srand()
следует вызывать только один раз — в начале программы (обычно в верхней части функции main()
)rand()
генерирует следующее случайное число в последовательности. Оно будет находиться в диапазоне от 0
до RAND_MAX
(константа в cstdlib
, значением которой является 32767
).Вот пример программы, в которой используются обе эти функции:
#include <iostream>
#include <cstdlib> // для функций rand() и srand()
int main()
{
srand(4541); // устанавливаем стартовое значение - 4 541
// Выводим 100 случайных чисел
for (int count=0; count < 100; ++count)
{
std::cout << rand() << "\t";
// Если вывели 5 чисел, то вставляем символ новой строки
if ((count+1) % 5 == 0)
std::cout << "\n";
}
}
// 14867 24680 8872 25432 21865
// ...
В C++ 11 добавили тонну нового функционала для генерации случайных чисел, включая алгоритм Вихрь Мерсенна, а также разные виды генераторов случайных чисел (например, равномерные, генератор Poisson и пр.). Доступ к ним осуществляется через подключение заголовочного файла
random
. Вот пример генерации случайных чисел в C++11 с использованием Вихря Мерсенна на картинке.
Вихрь Мерсенна генерирует случайные 32-битные целые числа
unsigned
(а не 15-битные целые числа, как в случае с rand()
), что позволяет использовать гораздо больший диапазон значений. Существует также версия (
std::mt19937_64
) для генерации 64-битных целых чисел unsigned
@cpluspluc
Please open Telegram to view this post
VIEW IN TELEGRAM
C и C++ имеют свои собственные встроенные генераторы случайных чисел. Они реализованы в двух отдельных функциях, которые находятся в заголовочном файле
cstdlib
:srand()
устанавливает передаваемое пользователем значение в качестве стартового. srand()
следует вызывать только один раз — в начале программы (обычно в верхней части функции main()
)rand()
генерирует следующее случайное число в последовательности. Оно будет находиться в диапазоне от 0
до RAND_MAX
(константа в cstdlib
, значением которой является 32767
).Вот пример программы, в которой используются обе эти функции:
#include <iostream>
#include <cstdlib> // для функций rand() и srand()
int main()
{
srand(4541); // устанавливаем стартовое значение - 4 541
// Выводим 100 случайных чисел
for (int count=0; count < 100; ++count)
{
std::cout << rand() << "\t";
// Если вывели 5 чисел, то вставляем символ новой строки
if ((count+1) % 5 == 0)
std::cout << "\n";
}
}
// 14867 24680 8872 25432 21865
// ...
В C++ 11 добавили тонну нового функционала для генерации случайных чисел, включая алгоритм Вихрь Мерсенна, а также разные виды генераторов случайных чисел (например, равномерные, генератор Poisson и пр.). Доступ к ним осуществляется через подключение заголовочного файла
random
. Вот пример генерации случайных чисел в C++11 с использованием Вихря Мерсенна на картинке.
Вихрь Мерсенна генерирует случайные 32-битные целые числа
unsigned
(а не 15-битные целые числа, как в случае с rand()
), что позволяет использовать гораздо больший диапазон значений. Существует также версия (
std::mt19937_64
) для генерации 64-битных целых чисел unsigned
@cpluspluc
Please open Telegram to view this post
VIEW IN TELEGRAM
#include "include/cef_app.h"
int main(int argc, char* argv[]) {
CefMainArgs main_args(argc, argv);
CefSettings settings;
// выполняем инициализацию
CefInitialize(main_args, settings, nullptr, nullptr);
// создаем окно с UI
CefWindowInfo window_info;
CefBrowserHost::CreateBrowser(window_info, new SimpleHandler(), "file:///your_ui.html", CefBrowserSettings(), nullptr);
// цикл обработки сообщений
CefRunMessageLoop();
// Cef для окончания работы
CefShutdown();
}
Замените
"file:///your_ui.html"
на путь к вашему HTML-файлу. Вуаля! Ваш интерфейс на HTML/CSS уже встроен в приложение на C++.@cpluspluc
Please open Telegram to view this post
VIEW IN TELEGRAM
=default
(для конструктора по умолчанию)X::X() {}
// и
X::X() = default;
#include <iostream>
#include <type_traits>
struct X {
//X() {}
X() = default;
int a;
int b;
};
int main( ) {
X x { 1, 2 }; // ошибка, если X - не тривиальный класс.
std::cout << std::boolalpha <<
std::is_trivial<X>::value << "\n";
}
При определении конструктора как
= default
тривиальность класса сохраняется, если она была до этого. В данном случае, это равносильно отсутствию явного упоминания конструктора в определении класса.Если конструктор по умолчанию определен как
= default
вне определения класса, всё равно будет считаться, что конструктор предоставлен пользователем, и это тоже делает класс нетривиальным.@cpluspluc
Please open Telegram to view this post
VIEW IN TELEGRAM
C++ Requests — это простая обертка вокруг libcurl, вдохновленная Python Requests.
Вот так можно отправить GET-запрос при помощи cpr:
#include <cpr/cpr.h>
int main(int argc, char** argv) {
cpr::Response r = cpr::Get(cpr::Url{"https://api.github.com/repos/libcpr/cpr/contributors"},
cpr::Authentication{"user", "pass", cpr::AuthMode::BASIC},
cpr::Parameters{{"anon", "true"}, {"key", "value"}});
r.status_code; // 200
r.header["content-type"]; // application/json; charset=utf-8
r.text; // JSON text string
}
@cpluspluc
Please open Telegram to view this post
VIEW IN TELEGRAM
🔓Что выведет следующий код?
🔢Варианты ответа:
A)
B)
C)
D)
✅Правильный ответ: B
💡Почему?
В момент вызова конструктора Base, объект ещё не стал Derived. Виртуальная функция вызывается в контексте Base.
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base constructor: ";
call();
}
virtual void call() { cout << "Base::call\n"; }
};
class Derived : public Base {
int x = init();
int init() {
cout << "Derived::init\n";
return 42;
}
public:
Derived() {
cout << "Derived constructor\n";
}
void call() override {
cout << "Derived::call, x = " << x << "\n";
}
};
int main() {
Derived d;
return 0;
}
🔢Варианты ответа:
A)
Derived::init
Derived constructor
B)
Derived::init
Derived constructor
C)
Base constructor: Derived::call, x = 42
Derived constructor
D)
Derived::call, x = <undefined>
Derived::init
Derived constructor
✅
💡Почему?
В момент вызова конструктора Base, объект ещё не стал Derived. Виртуальная функция вызывается в контексте Base.
#include <iostream>
#include <vector>
struct Tracer {
Tracer(const char* name) : name(name) {
std::cout << "Constructing " << name << "\n";
}
~Tracer() {
std::cout << "Destructing " << name << "\n";
}
const char* name;
};
struct Example {
Tracer t1{"t1"};
std::vector<Tracer> list;
Tracer t2{"t2"};
Example() : list{Tracer("temp1"), Tracer("temp2")} {
std::cout << "Inside constructor\n";
}
};
int main() {
Example e;
std::cout << "End of main\n";
return 0;
}
---
ОТВЕТ:
Constructing t1
Constructing temp1
Constructing temp2
Inside constructor
Constructing t2
End of main
Destructing t2
Destructing temp2
Destructing temp1
Destructing t1
---
Почему так:
• Поля инициализируются в порядке объявления в структуре, а не в списке инициализации
• std::initializer_list создаёт временные объекты, которые копируются в вектор
• t2 создаётся после строки Inside constructor
• Деструкторы вызываются в обратном порядке: t2 → temp2 → temp1 → t1
Хитрость — в порядке инициализации, временных объектах и destructuring-порядке!
@cpluspluc
Please open Telegram to view this post
VIEW IN TELEGRAM
Реализуйте структуру данных — очередь (`queue`), поддерживающую три операции:
-
enqueue(x)
— добавить элемент в конец очереди (должно работать за O(1));-
dequeue()
— удалить элемент из начала очереди и вернуть его (должно работать за O(1));-
remove(x)
— удалить первое вхождение элемента x
из очереди за O(1).Ограничения:
- Элементы могут повторяться.
- Если элемент отсутствует,
remove(x)
не делает ничего.- Операции должны оставаться эффективными на больших объемах данных (миллионы элементов).
- Можно использовать стандартные структуры данных STL (`list`,
unordered_map
, unordered_set
и т.д.).▪️ Дополнительные вопросы к кандидату:
- Как обеспечить O(1) удаление произвольного элемента из очереди?
- Как избежать утечек памяти или "битых" итераторов после удаления элементов?
- Что будет, если очередь будет содержать множество одинаковых элементов?
- Как бы вы изменили решение, если бы нужно было удалять все вхождения элемента, а не только первое?
---
▪️ Разбор возможного решения:
Основная архитектура:
- Используем
std::list<int> data
для хранения реальной очереди.- Почему
list
? Потому что позволяет за O(1) удалять элементы по итератору.- Используем
std::unordered_map<int, std::list<std::list<int>::iterator>> index
.- Для каждого значения храним список итераторов на его вхождения в
data
.Операции:
- enqueue(x):
- Добавляем
x
в конец data
.- Сохраняем итератор на него в
index[x]
.- Все действия — за O(1).
- dequeue():
- Удаляем элемент из начала
data
.- Находим соответствующий итератор в
index
, удаляем его.- Если в списке итераторов для этого значения больше нет элементов, удаляем ключ из
index
.- remove(x):
- Если в
index[x]
есть итераторы:- Берем первый итератор.
- Удаляем его из
data
и из списка итераторов.- Если список стал пустым — удаляем запись
x
из index
.- Все действия — за O(1).
---
▪️ Возможные подводные камни:
- ❗ Не проверять, пустой ли
index[x]
, перед удалением итератора.- ❗ Не удалять ключи из
unordered_map
, что приводит к накоплению "пустых" списков в памяти.- ❗ Использовать
vector
вместо list
— тогда удаление из середины будет занимать O(n).---
▪️ Мини-пример интерфейса класса:
#include <list>
#include <unordered_map>
class FastQueue {
private:
std::list<int> data;
std::unordered_map<int, std::list<std::list<int>::iterator>> index;
public:
void enqueue(int x) {
auto it = data.insert(data.end(), x);
index[x].push_back(it);
}
int dequeue() {
if (data.empty()) throw std::out_of_range("Queue is empty");
int val = data.front();
auto it = index[val].front();
index[val].pop_front();
if (index[val].empty()) {
index.erase(val);
}
data.pop_front();
return val;
}
void remove(int x) {
if (index.count(x) == 0) return;
auto it = index[x].front();
data.erase(it);
index[x].pop_front();
if (index[x].empty()) {
index.erase(x);
}
}
};
▪️ Дополнительные вопросы на усложнение:
- Что если нужно сделать
removeAll(x)
— удаление всех вхождений элемента?- Как изменится решение, если в очереди будут сложные объекты вместо
int
?- Как минимизировать использование памяти, если очередь очень большая?
- Как обеспечить безопасность потоков (thread-safety) для многопоточного варианта очереди?
@cpluspluc
Please open Telegram to view this post
VIEW IN TELEGRAM
🎯 ▪ Задача: "Числа-близнецы с кастомным компаратором" (C++ для продвинутых)
У вас есть массив
1.
2.
3. Если таких пар несколько, вернуть все, отсортированные по A, затем по B
▪ Ограничения:
-
-
▪ Пример:
Вход:
Вывод:
✅ Минимальная разница = 1
🧠 Уловки задачи:
Наивное сравнение O(N²) слишком медленно
Нужно использовать сортировку + два указателя или бинарный поиск
✍️ Решение:
Идея решения — сначала разделить все числа на четные и нечетные, отсортировать их, а затем с помощью двух указателей найти пары с минимальной разницей. После этого собрать все такие пары и отсортировать результат по условию.
▪ Код:
```cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
using namespace std;
int main() {
int N;
cin >> N;
vector<int> even, odd;
for (int i = 0; i < N; ++i) {
int x;
cin >> x;
if (x % 2 == 0) even.push_back(x);
else odd.push_back(x);
}
sort(even.begin(), even.end());
sort(odd.begin(), odd.end());
vector<pair<int, int>> result;
int min_diff = INT_MAX;
int i = 0, j = 0;
while (i < even.size() && j < odd.size()) {
int a = even[i];
int b = odd[j];
int diff = abs(a - b);
if (diff < min_diff) {
min_diff = diff;
result.clear();
result.emplace_back(a, b);
} else if (diff == min_diff) {
result.emplace_back(a, b);
}
if (a < b) i++;
else j++;
}
sort(result.begin(), result.end());
for (auto& p : result) {
cout << "(" << p.first << "," << p.second << ")\n";
}
return 0;
}```
▪ 🔍 Пошаговый разбор:
✅ Считываем входные данные
✅ Разделяем числа на четные и нечетные
✅ Сортируем оба массива
✅ Используем два указателя, чтобы найти минимальную разницу за O(N)
✅ Если нашли пару с меньшей разницей — сбрасываем результат и добавляем её
✅ Если нашли пару с такой же разницей — просто добавляем
✅ Финально сортируем все найденные пары по A, затем по B
✨ Почему задача хитрая: ✅ Нужно оптимальное решение вместо лобового перебора
✅ Включает тонкую работу с индексами
✅ Требует правильно обрабатывать несколько минимальных пар
✅ Объединяет знания сортировки, двух указателей и поиска пар
💪 Challenge для профи: 👉 Попробуйте реализовать эту задачу с помощью multiset + lower_bound / upper_bound, чтобы избежать сортировки массивов и упростить логику поиска ближайших чисел.
У вас есть массив
N
целых чисел. Нужно найти все пары чисел (A, B), таких что:1.
|A - B|
минимально среди всех пар в массиве2.
A
— четное, а B
— нечетное3. Если таких пар несколько, вернуть все, отсортированные по A, затем по B
▪ Ограничения:
-
1 <= N <= 10^5
-
-10^9 <= A[i] <= 10^9
▪ Пример:
Вход:
arr = [4, 7, 9, 2, 8, 3]
Вывод:
(2,3)
(4,3)
(4,7)
(8,7)
(8,9)
✅ Минимальная разница = 1
🧠 Уловки задачи:
Наивное сравнение O(N²) слишком медленно
Нужно использовать сортировку + два указателя или бинарный поиск
✍️ Решение:
Идея решения — сначала разделить все числа на четные и нечетные, отсортировать их, а затем с помощью двух указателей найти пары с минимальной разницей. После этого собрать все такие пары и отсортировать результат по условию.
▪ Код:
```cpp
using namespace std;
int main() {
int N;
cin >> N;
vector<int> even, odd;
for (int i = 0; i < N; ++i) {
int x;
cin >> x;
if (x % 2 == 0) even.push_back(x);
else odd.push_back(x);
}
sort(even.begin(), even.end());
sort(odd.begin(), odd.end());
vector<pair<int, int>> result;
int min_diff = INT_MAX;
int i = 0, j = 0;
while (i < even.size() && j < odd.size()) {
int a = even[i];
int b = odd[j];
int diff = abs(a - b);
if (diff < min_diff) {
min_diff = diff;
result.clear();
result.emplace_back(a, b);
} else if (diff == min_diff) {
result.emplace_back(a, b);
}
if (a < b) i++;
else j++;
}
sort(result.begin(), result.end());
for (auto& p : result) {
cout << "(" << p.first << "," << p.second << ")\n";
}
return 0;
}```
▪ 🔍 Пошаговый разбор:
✅ Считываем входные данные
✅ Разделяем числа на четные и нечетные
✅ Сортируем оба массива
✅ Используем два указателя, чтобы найти минимальную разницу за O(N)
✅ Если нашли пару с меньшей разницей — сбрасываем результат и добавляем её
✅ Если нашли пару с такой же разницей — просто добавляем
✅ Финально сортируем все найденные пары по A, затем по B
✨ Почему задача хитрая: ✅ Нужно оптимальное решение вместо лобового перебора
✅ Включает тонкую работу с индексами
✅ Требует правильно обрабатывать несколько минимальных пар
✅ Объединяет знания сортировки, двух указателей и поиска пар
💪 Challenge для профи: 👉 Попробуйте реализовать эту задачу с помощью multiset + lower_bound / upper_bound, чтобы избежать сортировки массивов и упростить логику поиска ближайших чисел.
🦾 Задача с подвохом: Виртуальные функции и конструкторы
Условие:
Что выведет следующий код и почему?
❓ Вопрос:
Что будет выведено? Почему результат может удивить даже опытных C++ разработчиков?
🔍 Разбор:
1️⃣ Мы создаём объект.
Это вызывает конструктор , но сначала выполняется конструктор `Base` (по правилам иерархии).
2️⃣ В конструкторе есть вызов .
Может показаться, что поскольку объект на самом деле , вызовется .
Но! Вот главный подвох:
➡️ В C++, когда вы вызываете виртуальную функцию из конструктора (или деструктора), она не виртуальна для объекта, который ещё не полностью сконструирован.
На момент вызова объект всё ещё только Base, потому что ещё не инициализирован.
✅ Пошаговое выполнение:
- Вызов конструктора :
```
Base constructor
```
- Вызов внутри конструктора :
Это вызовет Base::foo(), а не :
```
Base foo
```
- После завершения конструктора , вызывается конструктор :
```
Derived constructor
```
✅ Итоговый вывод:
```
Base constructor
Base foo
Derived constructor
```
💥 Подвох:
• Многие ожидают, что виртуальные функции работают «магически» всегда.
• Но при вызове из конструктора (или деструктора) виртуальные функции не полиморфны, потому что объект ещё не «стал» Derived полностью.
🛡️ Что нужно помнить:
Никогда не полагайтесь на вызовы виртуальных функций в конструкторах/деструкторах для вызова методов производных классов. Это источник трудноуловимых багов.
✅ Вывод:
C++ строго следует правилам объектной модели: пока объект конструируется (или разрушается), он считается экземпляром того класса, конструктор которого выполняется в данный момент. Это поведение важно помнить при проектировании иерархий классов!
@cpluspluc
Условие:
Что выведет следующий код и почему?
#include <iostream>
class Base {
public:
Base() {
std::cout << "Base constructor\n";
foo();
}
virtual void foo() {
std::cout << "Base foo\n";
}
};
class Derived : public Base {
public:
Derived() {
std::cout << "Derived constructor\n";
}
void foo() override {
std::cout << "Derived foo\n";
}
};
int main() {
Derived d;
return 0;
}
❓ Вопрос:
Что будет выведено? Почему результат может удивить даже опытных C++ разработчиков?
🔍 Разбор:
1️⃣ Мы создаём объект
Derived d;
Это вызывает конструктор
Derived
2️⃣ В конструкторе
Base
foo();
Может показаться, что поскольку объект на самом деле
Derived
Derived::foo()
Но! Вот главный подвох:
➡️ В C++, когда вы вызываете виртуальную функцию из конструктора (или деструктора), она не виртуальна для объекта, который ещё не полностью сконструирован.
На момент вызова
foo()
Derived
✅ Пошаговое выполнение:
- Вызов конструктора
Base
```
Base constructor
```
- Вызов
foo()
Base
Это вызовет Base::foo(), а не
Derived::foo()
```
Base foo
```
- После завершения конструктора
Base
Derived
```
Derived constructor
```
✅ Итоговый вывод:
```
Base constructor
Base foo
Derived constructor
```
💥 Подвох:
• Многие ожидают, что виртуальные функции работают «магически» всегда.
• Но при вызове из конструктора (или деструктора) виртуальные функции не полиморфны, потому что объект ещё не «стал» Derived полностью.
🛡️ Что нужно помнить:
Никогда не полагайтесь на вызовы виртуальных функций в конструкторах/деструкторах для вызова методов производных классов. Это источник трудноуловимых багов.
✅ Вывод:
C++ строго следует правилам объектной модели: пока объект конструируется (или разрушается), он считается экземпляром того класса, конструктор которого выполняется в данный момент. Это поведение важно помнить при проектировании иерархий классов!
@cpluspluc
🧠 Задача с подвохом для продвинутых C++ разработчиков
🔹 Уровень: Advanced
🔹 Темы:
📌 Условие
Рассмотрим следующий код:
❓ Вопросы
1. Что будет выведено на экран?
2. Почему
3. Что изменится, если заменить
🔍 Разбор
✅ Ожидаемый вывод:
🔧 Почему так происходит
-
-
- Временный объект уничтожается (вызывается `Dtor`).
-
🔄 Если заменить `reserve(3)` на `resize(3)`
-
-
- Это может привести к копированию или перемещению уже созданных элементов.
⚠️ Подвох
Многие ошибочно считают, что
🧠 Вывод
-
-
- Не путай эти методы — от этого зависит и производительность, и семантика.
📌 Совет:
Если не хочешь лишнего перемещения, используй
@cpluspluc
🔹 Уровень: Advanced
🔹 Темы:
std::vector
, управление памятью, конструкторы/деструкторы, reserve()
vs resize()
📌 Условие
Рассмотрим следующий код:
#include <iostream>
#include <vector>
struct Foo {
Foo() { std::cout << "Ctor\n"; }
~Foo() { std::cout << "Dtor\n"; }
Foo(const Foo&) { std::cout << "Copy\n"; }
Foo(Foo&&) noexcept { std::cout << "Move\n"; }
};
int main() {
std::vector<Foo> v;
v.reserve(3); // Резервируем место под 3 элемента
std::cout << "--- Pushing ---\n";
for (int i = 0; i < 3; ++i) {
v.push_back(Foo());
}
std::cout << "--- Done ---\n";
}
❓ Вопросы
1. Что будет выведено на экран?
2. Почему
reserve()
не предотвращает конструкторы копирования/перемещения? 3. Что изменится, если заменить
reserve(3)
на resize(3)
?🔍 Разбор
✅ Ожидаемый вывод:
--- Pushing ---
Ctor
Move
Dtor
Ctor
Move
Dtor
Ctor
Move
Dtor
--- Done ---
Dtor
Dtor
Dtor
🔧 Почему так происходит
-
Foo()
создаёт временный объект. -
push_back()
вызывает перемещающий конструктор Move
. - Временный объект уничтожается (вызывается `Dtor`).
-
reserve(3)
выделяет память, но не создаёт объектов.🔄 Если заменить `reserve(3)` на `resize(3)`
-
resize(3)
создаст 3 объекта Foo через конструктор по умолчанию. -
push_back(Foo())
добавит четвёртый, возможно вызовет realocation. - Это может привести к копированию или перемещению уже созданных элементов.
⚠️ Подвох
Многие ошибочно считают, что
reserve(n)
создаёт n
объектов. Но это не так — reserve()
только выделяет память, не вызывая конструкторы. Именно поэтому внутри push_back
всё равно происходит перемещение или копирование.🧠 Вывод
-
reserve()
— экономия на реаллокациях, без создания объектов. -
resize()
— создаёт n
объектов, вызывает конструкторы. - Не путай эти методы — от этого зависит и производительность, и семантика.
// Резервирует память, не создаёт объекты
v.reserve(10);
// Создаёт 10 объектов Foo
v.resize(10);
📌 Совет:
Если не хочешь лишнего перемещения, используй
emplace_back()
:
v.emplace_back(); // Вызывает конструктор Foo напрямую внутри вектора
@cpluspluc
🧠 Хитрая задача на C++: "Исчезающее число среди строк"
Условие
Дан список строк, представляющих числа от 1 до 100 включительно. Одного числа нет.
Твоя задача — найти, какого именно числа не хватает.
Нельзя использовать сортировку,
Пример:
Массив: ["1", "2", "3", ..., "98", "99"] (без "100")
Ответ: 100
Формат:
Решение с XOR
Пример использования:
Почему работает:
XOR — идеальное решение, когда нужно найти одну потерянную величину среди уникальных значений.
(XOR всех от 1 до 100) ^ (XOR из данных) = отсутствующее число.
@cpluspluc
Условие
Дан список строк, представляющих числа от 1 до 100 включительно. Одного числа нет.
Твоя задача — найти, какого именно числа не хватает.
Нельзя использовать сортировку,
std::accumulate
, std::unordered_map
, std::stoi
можно.Пример:
Массив: ["1", "2", "3", ..., "98", "99"] (без "100")
Ответ: 100
Формат:
int findMissingNumber(const std::vector<std::string>& data);
Решение с XOR
#include <iostream>
#include <vector>
#include <string>
int findMissingNumber(const std::vector<std::string>& data) {
int xor_full = 0;
int xor_data = 0;
for (int i = 1; i <= 100; ++i) {
xor_full ^= i;
}
for (const auto& s : data) {
xor_data ^= std::stoi(s);
}
return xor_full ^ xor_data;
}
Пример использования:
int main() {
std::vector<std::string> data;
for (int i = 1; i <= 100; ++i) {
if (i != 57) { // Удалим 57
data.push_back(std::to_string(i));
}
}
std::cout << "Пропущено: " << findMissingNumber(data) << std::endl;
return 0;
}
Почему работает:
XOR — идеальное решение, когда нужно найти одну потерянную величину среди уникальных значений.
a ^ a = 0
, 0 ^ b = b
. Поэтому: (XOR всех от 1 до 100) ^ (XOR из данных) = отсутствующее число.
@cpluspluc
🔍 C++ Задача для профи: кто вызовется?
У тебя есть следующий код:
❓ Вопросы:
1. Что напечатает программа?
2. Почему вызов
3. Что произойдёт, если передать
🧠 Подвох:
-
-
- У тебя может возникнуть разный вывод в зависимости от перегрузки
✅ Разбор:
- `process(42)` → `Integral: 42` ✅
- `process(3.14)` → `Floating-point: 3.14` ✅
- `process('A')` → `Integral: 65` (ASCII код символа!) ⚠️
- `process(true)` → `Integral: 1` (да, это `bool`) ✅
🎯 Что проверяет задача:
- Понимание `SFINAE` и `enable_if`
- Типовую систему C++ (что именно считается integral)
- Разницу между `char`, `bool`, `int`, `float` в шаблонах
- Предсказание поведения перегрузки на уровне типов
@cpluspluc
У тебя есть следующий код:
#include <iostream>
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
process(T value) {
std::cout << "Integral: " << value << std::endl;
}
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
process(T value) {
std::cout << "Floating-point: " << value << std::endl;
}
int main() {
process(42); // (1)
process(3.14); // (2)
process('A'); // (3)
}
❓ Вопросы:
1. Что напечатает программа?
2. Почему вызов
process('A')
может удивить?3. Что произойдёт, если передать
true
?🧠 Подвох:
-
char
— это integral type, а не string
и не отдельный класс-
true
— это тоже int`-подобный тип: `std::is_integral<bool>::value == true
- У тебя может возникнуть разный вывод в зависимости от перегрузки
✅ Разбор:
- `process(42)` → `Integral: 42` ✅
- `process(3.14)` → `Floating-point: 3.14` ✅
- `process('A')` → `Integral: 65` (ASCII код символа!) ⚠️
- `process(true)` → `Integral: 1` (да, это `bool`) ✅
🎯 Что проверяет задача:
- Понимание `SFINAE` и `enable_if`
- Типовую систему C++ (что именно считается integral)
- Разницу между `char`, `bool`, `int`, `float` в шаблонах
- Предсказание поведения перегрузки на уровне типов
@cpluspluc
🔍 C++ Задача для профи: кто вызовется?
У тебя есть следующий код:
❓ Вопросы:
1. Что напечатает программа?
2. Почему вызов
3. Что произойдёт, если передать
🧠 Подвох:
-
-
- У тебя может возникнуть разный вывод в зависимости от перегрузки
✅ Разбор:
-
-
-
-
🎯 Что проверяет задача:
- Понимание
- Типовую систему C++ (что именно считается integral)
- Разницу между
- Предсказание поведения перегрузки на уровне типов
@cpluspluc
У тебя есть следующий код:
#include <iostream>
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
process(T value) {
std::cout << "Integral: " << value << std::endl;
}
template<typename T>
typename std::enable_if<std::is_floating_point<T>::value, void>::type
process(T value) {
std::cout << "Floating-point: " << value << std::endl;
}
int main() {
process(42); // (1)
process(3.14); // (2)
process('A'); // (3)
}
❓ Вопросы:
1. Что напечатает программа?
2. Почему вызов
process('A')
может удивить?3. Что произойдёт, если передать
true
?🧠 Подвох:
-
char
— это integral type, а не string
и не отдельный класс-
true
— это тоже int`-подобный тип: `std::is_integral<bool>::value == true
- У тебя может возникнуть разный вывод в зависимости от перегрузки
✅ Разбор:
-
process(42)
→ Integral: 42
✅-
process(3.14)
→ Floating-point: 3.14
✅-
process('A')
→ Integral: 65
(ASCII код символа!) ⚠️-
process(true)
→ Integral: 1
(да, это `bool`) ✅🎯 Что проверяет задача:
- Понимание
SFINAE
и enable_if
- Типовую систему C++ (что именно считается integral)
- Разницу между
char
, bool
, int
, float
в шаблонах- Предсказание поведения перегрузки на уровне типов
@cpluspluc
⚙️ Задача для C++ разработчиков: «Непонятная ошибка, которая портит данные»
🎯 Цель: Найти и объяснить причину скрытого неопределённого поведения, которое проявляется не сразу
📍 Ситуация:
Ты разрабатываешь кроссплатформенное приложение на C++17, которое обрабатывает массивы бинарных данных.
На тестах — всё работает. Но у части пользователей (особенно на Linux) возникают:
- Повреждённые файлы после сериализации
- Непредсказуемые вылеты при больших объёмах данных
- Валидация данных случайно "съезжает" (байты путаются)
Вот фрагмент кода:
🔍 Визуально всё нормально. В unit-тестах — ок. На CI — ок.
Но на проде данные иногда повреждены, и никто не может воспроизвести баг стабильно.
🧩 Задача:
1. Почему
2. Что может отличаться на разных платформах и влиять на поведение?
3. Как бы ты безопасно сериализовал структуру в
4. Как это можно поймать с помощью
5. Как написать cross-platform-safe сериализацию?
💡 Подсказка:
В C++ `struct Packet` может иметь **padding** и **alignment**, которые отличаются на архитектурах. `memcpy` по `sizeof(Packet)` может захватить лишние или мусорные байты.
🛠 Решение:
1. `struct Packet` не является POD-структурой с гарантированным layout — в ней может быть **неинициализированный padding**, который `memcpy` тоже копирует.
2. Проблема усиливается на системах с разным выравниванием: x86 vs ARM, GCC vs MSVC.
3. Более безопасный способ — сериализовать поля по отдельности:
std::vector<uint8_t> serialize(const Packet& p) {
std::vector<uint8_t> buffer;
buffer.insert(buffer.end(), reinterpret_cast<const uint8_t*>(& p.id ),
reinterpret_cast<const uint8_t*>(& p.id ) + sizeof( p.id ));
buffer.insert(buffer.end(), p.data , p.data + sizeof( p.data ));
return buffer;
}
4. Или использовать `std::ostringstream` / `std::span` / `protobuf` / `flatbuffers`.
5. Проверка с `-fsanitize=undefined` даст warning:
```
memcpy: reading padding bytes from stack frame
```
📌 **Вывод:**
В C++ `memcpy` на структуру — это **ловушка**, если ты не контролируешь padding. Никогда не сериализуй структуры напрямую через память, если это не `#pragma pack` и не строго определённый layout.
💬 Это вопрос для собеседования на позицию C++ системного разработчика с уклоном в безопасность и низкоуровневую разработку.
@cpluspluc
🎯 Цель: Найти и объяснить причину скрытого неопределённого поведения, которое проявляется не сразу
📍 Ситуация:
Ты разрабатываешь кроссплатформенное приложение на C++17, которое обрабатывает массивы бинарных данных.
На тестах — всё работает. Но у части пользователей (особенно на Linux) возникают:
- Повреждённые файлы после сериализации
- Непредсказуемые вылеты при больших объёмах данных
- Валидация данных случайно "съезжает" (байты путаются)
Вот фрагмент кода:
#include <vector>
#include <cstring>
struct Packet {
uint32_t id;
char data[64];
};
std::vector<uint8_t> serialize(const Packet& p) {
std::vector<uint8_t> buffer(sizeof(Packet));
std::memcpy(buffer.data(), &p, sizeof(Packet));
return buffer;
}
🔍 Визуально всё нормально. В unit-тестах — ок. На CI — ок.
Но на проде данные иногда повреждены, и никто не может воспроизвести баг стабильно.
🧩 Задача:
1. Почему
memcpy
здесь небезопасен, хотя кажется логичным? 2. Что может отличаться на разных платформах и влиять на поведение?
3. Как бы ты безопасно сериализовал структуру в
std::vector<uint8_t>
? 4. Как это можно поймать с помощью
valgrind
/ asan
/ -fsanitize=undefined
? 5. Как написать cross-platform-safe сериализацию?
💡 Подсказка:
🛠 Решение:
1. `struct Packet` не является POD-структурой с гарантированным layout — в ней может быть **неинициализированный padding**, который `memcpy` тоже копирует.
2. Проблема усиливается на системах с разным выравниванием: x86 vs ARM, GCC vs MSVC.
3. Более безопасный способ — сериализовать поля по отдельности:
std::vector<uint8_t> buffer;
buffer.insert(buffer.end(), reinterpret_cast<const uint8_t*>(&
reinterpret_cast<const uint8_t*>(&
buffer.insert(buffer.end(),
return buffer;
}
4. Или использовать `std::ostringstream` / `std::span` / `protobuf` / `flatbuffers`.
5. Проверка с `-fsanitize=undefined` даст warning:
```
memcpy: reading padding bytes from stack frame
```
📌 **Вывод:**
В C++ `memcpy` на структуру — это **ловушка**, если ты не контролируешь padding. Никогда не сериализуй структуры напрямую через память, если это не `
💬 Это вопрос для собеседования на позицию C++ системного разработчика с уклоном в безопасность и низкоуровневую разработку.
@cpluspluc