Готовы погрузиться с головой в дивный мир программирования? Хотите узнать как непредсказуемо могут повести себя несколько простых строк кода?
Если ваш ответ "Да!" — добро пожаловать под кат.
Вас будут ждать несколько занимательных задачек на С или С++.
Правильный ответ с объяснением всегда будет спрятан под спойлером.
Удачи!
Про самую короткую программу
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
пропадёт из области видимости.
Заключение
Надеюсь вы никогда не увидите это в реальном коде.
Автор: shaggyboo