
Готовы погрузиться с головой в дивный мир программирования? Хотите узнать как непредсказуемо могут повести себя несколько простых строк кода?
Если ваш ответ "Да!" — добро пожаловать под кат.
Вас будут ждать несколько занимательных задачек на С или С++.
Правильный ответ с объяснением всегда будет спрятан под спойлером.
Удачи!
Про самую короткую программу
main;
Что будет если скомпилировать эту программу компилятором языка C?
- Не cкомпилируется.
- Не слинкуется.
- Скомпилируется и слинкуется.
Это валидный код на языке C.
Почему? В C можно опустить тип возвращаемого значения у функций и при объявлении переменных, по-умолчанию он будет int-ом. А ещё в С нет различия между функциями и глобальными переменными при линковке. В этом случае линковщик думает что под именем main находится функция.
Про fork
#include <iostream> #include <unistd.h> int main() { for(auto i = 0; i < 1000; i++) std::cout << "Hello world!\n"; fork(); }
Сколько раз будет напечатано "Hello world!"?
- 1000
- меньше
- больше
IO операции буферизуется для улучшения производительности.
Вызов fork() породит новый процесс, с copy-on-write дубликатом адресного пространства.
Буферизованные строчки будут напечатаны в каждом процессе.
Про индексы
#include <iostream> int main() { int array[] = { 1, 2, 3 }; std::cout << (4, (1, 2)[array]) << std::endl; }
Что напечатет этот код?
- 1
- 2
- 3
- 4
- Ошибка компиляции
- Не определено стандартом.
Порграмма напечатает 3.
Почему так?
Сначала посмотрим на индекс: array[index] == *(array + index) == *(index + array) == index[array]
Дальше мы имеем дело с бинарным оператором запятая. Он отбрасывает свой левый аргумент и возвращает значение правого.
Про регулярное выражение
#include <regex> #include <iostream> int main() { std::regex re { "(.*|.*)*O" }; std::string str { "0123456789" }; std::cout << std::regex_match(str, re); return 0; }
За какое минимальное время точно заматчится эта регулярка?
- ~ 1 мс.
- ~ 100 мс.
- ~ 1 cек.
- ~ 1 мин.
- ~ 1 час.
- ~ 1 год.
- больше времени жизни вселенной.
Ха-ха! Вот и не угадали. Зависит от компилятора.
На моём ноутбуке clang показывает результат примерно 100 мс.
GCC 57 секунд! Минута! Серьёзно?!
Почему так?
Есть 2 подхода для реализации регулярных выражений.
Один — превратить регулярное выражение в конечный автомат за O(n**2), для регулярного выражения длиной n символов.
Сложность сопоставления co строкой из m символов — O(m). Такое регулярное выражение не поддерживает backtracking.
Второй — что-то вроде жадного перебора с поиском в глубину. Поддерживает backtracking.
А ещё, сложность операций с регулярными выражениями в STL никак не определена. Хорошо хоть, что за минуту управились.
Про move и лямбду
#include <iostream> struct Foo { Foo() { std::cout << "Foo()\n"; } Foo(Foo&&) { std::cout << "Foo(Foo&&)\n"; } Foo(const Foo&) { std::cout << "Foo(const Foo&)\n"; } }; int main() { Foo f; auto a = [f = std::move(f)]() { return std::move(f); }; Foo f2(a()); return 0; }
Какую строчку программа напечатает последней?
Foo()Foo(Foo&&)Foo(const Foo&)
Foo(const Foo&). По умолчанию лямбды иммутабельны. Ко всем значениям указанным в [] неявно добавляется const.
Это позволяет лямбдам вести себя как обычным функциям. При одних и тех же аргументах возвращать одни и те же значения.
Что же происходит в этом случае? Когда мы пытаемся сделать move f из функции, у нас получается const Foo&&.
Это очень странная штука, компилятор не умеет с ней работать и копирует Foo. Можно починить объявив mutable лямбду:
auto a = [f = std::move(f)]() mutable { return std::move(f); };
Или сделать конструктор от Foo(const Foo&&).
Про x и bar
#include <iostream> int x = 0; int bar(int(x)); int main() { std::cout << bar; }
Что произойдёт если попытаться скомпилирвать и запустить это?
- напечатает
0 - напечатает
1 - напечатает
0x0 - не скомпилируется
- не слинкуется
Программа напечатает 1.
Почему так?
int bar(int(x)); — это объявление функции, оно эквивалентно int bar(int x);.
Если вы хотите приведение типа, надо писать вот так int bar((int(x)));.
Затем мы пытаемся вывести адрес функции, он будет неявно приведён к bool, адрес функции не может быть нулём, т.е. true.
Функция bar() не используется. Поэтому при линковке не будет unreferenced symbol.
Про inline
#include <iostream> inline size_t factorial(size_t n) { if (n == 0) return 1; return n * factorial(n - 1); } int main() { std::cout << factorial(5) << std::endl; }
Программа компилируется и линкуется без ошибок вот так g++ -c main.cpp -o main.o && g++ foo.cpp -o foo.o && g++ foo.o main.o -o test. Что произойдёт если ее запустить?
- Напечатается 120.
- Может произойти что угодно.
Может произойти что угодно. Это же С++.
Весь подвох в слове inline. Это лишь указание компилятору.
Он может просто вкомпилировать эту функцию в объектный файл (скорее всего, он так и сделает для рекурсивных функций).
Линковщик умеет выкидывать дубликаты не встроенных в код inline-функций.
В итоговый файл обычно попадает тот вариант, который встретился в первом объектном файле.
Программа выведет 0 если в foo.cpp:
#include <cstddef> inline size_t factorial(size_t n) { if (n == 0) return 0; return 2 * n * factorial(n - 1); } int foo(size_t n) { return factorial(n); }
Про конструкторы
#include <iostream> struct Foo { Foo() { std::cout << "Foo()\n"; } Foo(const Foo&) { std::cout << "Foo(const Foo&)\n"; } Foo(int) { std::cout << "Foo(int)\n"; } Foo(int, int) { std::cout << "Foo(int, int)\n"; } Foo(const Foo&, int) { std::cout << "Foo(const Foo&, int)\n"; } Foo(int, const Foo&) { std::cout << "Foo(int, const Foo&)\n"; } }; void f(Foo) {} struct Bar { int i, j; Bar() { f(Foo(i, j)); f(Foo(i)); Foo(i, j); Foo(i); Foo(i, j); } }; int main() { Bar(); }
Какая строчка будет напечатана последенй?
Foo(int, int)Foo(const Foo&, int)Foo(int, const Foo&)Foo(int)
Последней строчкой будет Foo(const Foo&, int).
Foo(i) — объявелние переменной, оно эквивалентно Foo i, а значит поле класса i пропадёт из области видимости.
Заключение
Надеюсь, вы никогда не увидите это в реальном коде.
