Здравствуйте!
Те, кто занимается программированием рано или поздно сталкивается с необходимостью прохождения технического собеседования у потенциального работодателя.
О том, что спрашивают на собеседовании у C++ программистов, а также об ответах на эти вопросы и пойдет речь в данном посте.
Небольшая вводная часть в виде вопрос-ответ, чтобы разъяснить некоторые моменты и постараться ответить на возможные вопросы по этому посту.
Зачем все это?
Хочется собрать реальные вопросы и ответы на них в одном посте. Т.к. меня не устраивают те, что я вижу в интернете, они порой слишком размазанные (например, 75 страниц на геймдеве), или рассматривающие всего несколько примеров, или с ошибками, которые уже никто не исправит, или с ссылками на уже мертвые топики, или какие-то надуманные,… список можно продолжать.
В общем хочется иметь список с самыми популярными вопросами по данной теме, и если, вдруг, понадобится освежить знания, чтобы не приходилось лезть в какие-то записки, запускать IDE или гуглить, а можно было открыть одно место и там все прочитать.
Наиболее интересной данная тема должна показаться тем, кто еще только начинает свои шаги в этом безграничном море под названием C++.
Откуда будут вопросы?
Из небольшого моего опыта, опыта людей, которых я знаю, а также, я надеюсь, что много интересного предложите вы, из своего опыта или опыта ваших знакомых.
Какая будет структура этих вопросов?
Линейный список. Вопросы будут добавляться по мере поступления, я постараюсь не забрасывать данный раздел и обновлять его. В конце я буду ставить дату последнего обновления, чтобы было понятно когда список меняли последний раз.
Сейчас я планирую написать первые 20 вопросов, а если эта тема будет востребована, то буду постепенно добавлять новые.
У вас ошибка в коде, как вы ее проглядели?
Людям свойственно ошибаться и я не считаю себя гуру способному писать безошибочно. И это C++, здесь вероятность ошибиться еще больше.
Если вы видите ошибку, напишите о ней, и я обязательно ее исправлю.
Ответ: Чтобы избежать возможной утечки ресурсов или другого неконтролируемого поведения объекта, в логику работы которого включен вызов деструктора.
Пример:
Output:
Без ключевого слова virtual у родительского класса Base деструктор порожденного класса не был бы вызван. Т.е. вызвался бы только ~Base():
Output:
Более подробно об этом написал GooRoo здесь. Наглядный пример возможной утечки в следующем вопросе.
Ответ: Если исключение не обработано, то c логической точки зрения разрушается объект, который еще не создан, а с технической, так как он еще не создан, то и деструктор этого объекта не будет вызван.
Пример:
Output:
Я немного модифицировал предыдущий пример, чтобы проблема была наглядней. Здесь объект m_hFile (если был открыт) утечет т.к. до CloseHandle() выполнение не дойдет. Т.е. имеем такие же проблемы как в первом примере: возможная утечка ресурсов или другие проблемы из-за нарушения логики работы класса.
Здесь могут спросить: «Как бы вы поступили при подобной ситуации». Правильный ответ: «Воспользовался бы умными указателями». Простой пример умного указателя:
Теперь и без вызова деструктора Base хэндл будет закрыт, т.к. при уничтожении класса Base будет уничтожен объект m_hFile класса CHandle, в деструкторе которого и будет закрыт хэндл.
Изобретать велосипед, конечно, не надо, все уже написано до нас, это пример который можно написать на бумажке при соответствующем вопросе. А так есть boost, Loki, ATL и т.п., где это уже реализовано.
Ответ:
Пример 2. Не можем изменить указатель на объект:
Пример 3. Не можем изменить члены класса:
Дополнение: константный метод может изменять члены класса, если они объявлены как mutable. Подробнее про mutable можно прочитать у Алены CPP здесь.
Ответ: Пузырьковая сортировка, для контейнеров, без временных переменных:
Ответ: Код переворота строки для контейнеров, без временных переменных, не осуществляющий прохода по всей строке:
Ответ: Сделать private конструктор копирования и оператор =.
Пример:
Ответ: Практически ни в чем. В struct модификаторы доступа по умолчанию public, в class private. Также отличается и наследование по умолчанию, у struct — public, у class — private.
Пример:
Ответ: Т.к. значения в ассоциативных контейнерах хранятся отсортированными, то объект должен реализовывать оператор сравнения <, а остальные операторы сравнения могут быть выражены через него.
Пример:
Без реализации operator < объект класса Foo нельзя было бы добавить в set, т.к. был бы не определен их порядок внутри set'а.
Для нахождения элемента в контейнеры STL сам определяет недостающие операторы из оператора меньше. Так например, чтобы вычислить нужный элемент STL проверяет меньше ли он текущего, далее больше ли, и если оба условия ложны значит элемент эквивалентен искомому.
Помимо этого, для контейнеров может быть определен класс сравнения (Comparison class), в котором будет определена логика сравнения объектов, реализованная через тот же оператор меньше.
Ответ: sizeof всех членов + остаток для выравнивания (по умолчанию выравнивание 4 байта) + sizeof указателя на vtable (если есть виртуальные функции) + указатели на классы предков, от которых было сделано виртуальное наследование (размер указателя * количество классов)
Пример:
Ответ: Нужно вызвать чисто виртуальный метод в конструкторе родительского класса т.е. до создания дочернего, в котором этот метод реализован. Т.к. современный компилятор не даст это сделать напрямую, то нужно будет использовать промежуточный метод.
Пример:
Ответ: Здесь вспоминают о наличии у deque методов push_front и pop_front. Но основное отличие в организации памяти, у vector она как у обычного Си-массива, т.е. последовательный и непрерывный набор байт, а у deque это фрагменты с разрывами. За счет этого отличия vector всегда можно привести к обычному массиву или скопировать целиком участок памяти, но зато у deque операции вставки/удаления в начало быстрее (O(1) против O(n)), ввиду того, что не нужно перемещать остальные значения.
Ответ: Изменяют зону видимости членов базового класса.
Пример:
При private наследовании protected и public члены становятся private. При protected наследовании public становится protected. А при public ничего не изменяется. Более подробно об этом Алена писала вот здесь.
Ответ (Wiki): Инкапсуляция — свойство языка программирования, позволяющее объединить и защитить данные и код в объектe и скрыть реализацию объекта от пользователя (прикладного программиста). При этом пользователю предоставляется только спецификация (интерфейс) объекта.
Пользователь может взаимодействовать с объектом только через этот интерфейс. Реализуется с помощью ключевого слова: public.
Пользователь не может использовать закрытые данные и методы. Реализуется с помощью ключевых слов: private, protected.
Ответ (Wiki): Возможность объектов с одинаковой спецификацией иметь различную реализацию.
Пример:
Т.е. есть базовый класс фигура, и от него унаследованы два класса квадрат и круг, имеющие собственную реализацию метода Draw().
Ответ: malloc — выделение блока памяти в стиле Си, опасное с точки зрения приведения типов (non-typesafe), т.к. возвращает void * и требует обязательного приведения. new — выделение блока памяти и последующий вызов конструктора, безопасное с точки зрения приведения типов (typesafe), т.к. тип возвращаемого значения определен заранее.
Ответ: Чисто виртуальный метод — это метод, у которого отсутствует реализация. Абстрактный класс — это класс имеющий хотя бы один чисто виртуальный метод. Как следствие, экземпляр подобного класса не может быть создан т.к. отсутствует реализация виртуального метода.
Пример:
Ответ: Для повторного возбуждения предыдущего исключения и направления его следующему обработчику.
Пример:
Ответ: delete предназначен для уничтожения объектов, память под которые выделена при помощи new(). delete[] для объектов выделенных при помощи оператора new[]().
Пример:
При неправильном использовании оператора delete (например, delete вместо delete[]) результат будет: undefined behavior.
Ответ: Так как данный умный указатель реализует подход разрушающего копирования, то при присвоении его другому умному указателю оригинальный потеряет свое значение. А так же его нельзя использовать в стандартных STL контейнерах.
Пример:
Также ввиду того, что в деструкторе auto_ptr вызывается оператор delete, нельзя хранить объекты созданные при помощи new[](). Именно из-за этого нюанса boost предоставляет умные указатели в двух вариантах, для просто объекта и их коллекции, например, shared_ptr и shared_array.
Ответ: Для указания компилятору, что доступ к переменной может осуществляться из мест, неподконтрольных ему. А как следствие, что работу с данной переменной не нужно подвергать разного рода оптимизациям.
Пример:
Т.е. если volatile присутствует в каком-то условии, которое не меняется со временем, то компилятор может оптимизировать его, чтобы избежать ненужных проверок, при использовании volatile компилятор скорее всего не будет этого делать.
Пример:
Обновлено 24.04 в 13.35
Те, кто занимается программированием рано или поздно сталкивается с необходимостью прохождения технического собеседования у потенциального работодателя.
О том, что спрашивают на собеседовании у C++ программистов, а также об ответах на эти вопросы и пойдет речь в данном посте.
Предисловие
Небольшая вводная часть в виде вопрос-ответ, чтобы разъяснить некоторые моменты и постараться ответить на возможные вопросы по этому посту.
Зачем все это?
Хочется собрать реальные вопросы и ответы на них в одном посте. Т.к. меня не устраивают те, что я вижу в интернете, они порой слишком размазанные (например, 75 страниц на геймдеве), или рассматривающие всего несколько примеров, или с ошибками, которые уже никто не исправит, или с ссылками на уже мертвые топики, или какие-то надуманные,… список можно продолжать.
В общем хочется иметь список с самыми популярными вопросами по данной теме, и если, вдруг, понадобится освежить знания, чтобы не приходилось лезть в какие-то записки, запускать IDE или гуглить, а можно было открыть одно место и там все прочитать.
Наиболее интересной данная тема должна показаться тем, кто еще только начинает свои шаги в этом безграничном море под названием C++.
Откуда будут вопросы?
Из небольшого моего опыта, опыта людей, которых я знаю, а также, я надеюсь, что много интересного предложите вы, из своего опыта или опыта ваших знакомых.
Какая будет структура этих вопросов?
Линейный список. Вопросы будут добавляться по мере поступления, я постараюсь не забрасывать данный раздел и обновлять его. В конце я буду ставить дату последнего обновления, чтобы было понятно когда список меняли последний раз.
Сейчас я планирую написать первые 20 вопросов, а если эта тема будет востребована, то буду постепенно добавлять новые.
У вас ошибка в коде, как вы ее проглядели?
Людям свойственно ошибаться и я не считаю себя гуру способному писать безошибочно. И это C++, здесь вероятность ошибиться еще больше.
Если вы видите ошибку, напишите о ней, и я обязательно ее исправлю.
Q & A
1. Зачем нужен виртуальный деструктор?
Ответ: Чтобы избежать возможной утечки ресурсов или другого неконтролируемого поведения объекта, в логику работы которого включен вызов деструктора.
Пример:
class Base { public: virtual ~Base() { std::cout << "Hello from ~Base()" << std::endl; } }; class Derived : public Base { public: virtual ~Derived() { // Здесь могла бы быть очистка ресурсов std::cout << "Hello from ~Derived()" << std::endl; } }; Base *obj = new Derived(); delete obj;
Output:
Hello from ~Derived()
Hello from ~Base()Без ключевого слова virtual у родительского класса Base деструктор порожденного класса не был бы вызван. Т.е. вызвался бы только ~Base():
Output:
Hello from ~Base()Более подробно об этом написал GooRoo здесь. Наглядный пример возможной утечки в следующем вопросе.
2. Что стоит помнить при использовании исключений в конструкторе объекта?
Ответ: Если исключение не обработано, то c логической точки зрения разрушается объект, который еще не создан, а с технической, так как он еще не создан, то и деструктор этого объекта не будет вызван.
Пример:
class Base { private: HANDLE m_hFile; public: Base() { std::cout << "Hello from Base()" << std::endl; m_hFile = ::CreateFileA(...); // Вызываем код, который в ходе своего выполнения бросает исключение SomeLib.SomeFunc(...); } virtual ~Base() { std::cout << "Hello from ~Base()" << std::endl; // Здесь мы планировали закрыть хэндл ::CloseHandle(m_hFile); } }; try { Base b; } catch(const std::exception &e) { std::cout << "Exception message: " << e.what() << std::endl; }
Output:
Hello from Base()
Exception message: Something failedЯ немного модифицировал предыдущий пример, чтобы проблема была наглядней. Здесь объект m_hFile (если был открыт) утечет т.к. до CloseHandle() выполнение не дойдет. Т.е. имеем такие же проблемы как в первом примере: возможная утечка ресурсов или другие проблемы из-за нарушения логики работы класса.
Здесь могут спросить: «Как бы вы поступили при подобной ситуации». Правильный ответ: «Воспользовался бы умными указателями». Простой пример умного указателя:
class Base { private: class CHandle { public: ~CHandle() { ::CloseHandle(m_handle); } private: HANDLE m_handle; public: // Для полноценного smart pointer'а перегрузки одной операции // не достаточно, но для нашего примера и понимания вполне хватит void operator = (const HANDLE &handle) { m_handle = handle; } }; CHandle m_hFile; public: Base() { std::cout << "Hello from Base()" << std::endl; m_hFile = ::CreateFileA(...); // Вызываем код, который в ходе своего выполнения бросает исключение SomeLib.SomeFunc(...); } virtual ~Base() { std::cout << "Hello from ~Base()" << std::endl; } ...
Теперь и без вызова деструктора Base хэндл будет закрыт, т.к. при уничтожении класса Base будет уничтожен объект m_hFile класса CHandle, в деструкторе которого и будет закрыт хэндл.
Изобретать велосипед, конечно, не надо, все уже написано до нас, это пример который можно написать на бумажке при соответствующем вопросе. А так есть boost, Loki, ATL и т.п., где это уже реализовано.
3. Для каких целей применяется ключевое слово const?
Ответ:
- Позволяет задать константность объекта
- Позволяет задать константность указателя
- Позволяет указать, что данный метод не модифицирует члены класса, т.е. сохраняет состояние объекта
const int i = 1; i = 2; // error C3892: 'i' : you cannot assign to a variable that is const
Пример 2. Не можем изменить указатель на объект:
int i = 1; int* const j(&i); int k = 2; *j = k; // Ok j = &k; // error C3892: 'j' : you cannot assign to a variable that is const
Пример 3. Не можем изменить члены класса:
class Foo { private: int i; public: void func() const { i = 1; // error C3490: 'i' cannot be modified because it is being accessed through a const object } };
Дополнение: константный метод может изменять члены класса, если они объявлены как mutable. Подробнее про mutable можно прочитать у Алены CPP здесь.
4. Можете ли вы написать пример какого-нибудь алгоритма сортировки?
Ответ: Пузырьковая сортировка, для контейнеров, без временных переменных:
template <typename T > void bubble_sort( T &a ) { for( T::size_type i = 0; a.size() && i < a.size() - 1; ++i ) { for( T::size_type j = i; j + 1 > 0; --j ) { if( a[j] > a[j+1] ) std::swap( a[j], a[j+1] ); } } } std::vector<int> v; v.push_back( 7 ); v.push_back( 1000 ); v.push_back( 134 ); v.push_back( 23 ); v.push_back( 1 ); bubble_sort( v );
5. Можете ли вы написать код для переворота строки?
Ответ: Код переворота строки для контейнеров, без временных переменных, не осуществляющий прохода по всей строке:
template <typename T > void invert_string( T &a ) { T::size_type length = a.size(); for( T::size_type i = 0; i < (length/2); ++i ) { std::swap( a[i], a[length - i - 1] ); } } std::string str = "abcdefg"; invert_string(str);
6. Как защитить объект от копирования?
Ответ: Сделать private конструктор копирования и оператор =.
Пример:
class NonCopyable { public: NonCopyable(){} private: NonCopyable(NonCopyable&){} private: void operator=(const NonCopyable&){} }; NonCopyable a; NonCopyable b = a; // error C2248: 'NonCopyable::NonCopyable' : cannot access private member a = b; // error C2248: 'NonCopyable::operator =' : cannot access private member
7. В чем разница между struct и class?
Ответ: Практически ни в чем. В struct модификаторы доступа по умолчанию public, в class private. Также отличается и наследование по умолчанию, у struct — public, у class — private.
Пример:
struct Foo { int i; }; class Bar { int i; }; Foo a; a.i = 1; // Ok Bar b; b.i = 1; // error C2248: 'Bar::i' : cannot access private member declared in class 'Bar'
8. Каким свойством должен обладать объект, чтобы его можно было добавить в ассоциативные контейнеры в качестве ключа?
Ответ: Т.к. значения в ассоциативных контейнерах хранятся отсортированными, то объект должен реализовывать оператор сравнения <, а остальные операторы сравнения могут быть выражены через него.
Пример:
struct Foo { int i; }; inline bool operator < (const Foo & lhs, const Foo & rhs) { return lhs.i < rhs.i; } std::set<Foo> data; Foo a, b, c; a.i = 7; b.i = 1; c.i = 6; data.insert( a ); data.insert( b ); data.insert( c ); // Теперь в data элементы находятся в последовательности 1 6 7
Без реализации operator < объект класса Foo нельзя было бы добавить в set, т.к. был бы не определен их порядок внутри set'а.
Для нахождения элемента в контейнеры STL сам определяет недостающие операторы из оператора меньше. Так например, чтобы вычислить нужный элемент STL проверяет меньше ли он текущего, далее больше ли, и если оба условия ложны значит элемент эквивалентен искомому.
Помимо этого, для контейнеров может быть определен класс сравнения (Comparison class), в котором будет определена логика сравнения объектов, реализованная через тот же оператор меньше.
9. Сколько в памяти занимает произвольная структура?
Ответ: sizeof всех членов + остаток для выравнивания (по умолчанию выравнивание 4 байта) + sizeof указателя на vtable (если есть виртуальные функции) + указатели на классы предков, от которых было сделано виртуальное наследование (размер указателя * количество классов)
Пример:
struct Foo { int i; char a; }; int size = sizeof(Foo); // 8 байт, хотя int + char = 5. Все дело в дефолтном выравнивании равном 4, т.е. размер должен быть кратен блоку в 4 байта. // Установим выравнивание в 1 байт #pragma pack(push, 1) struct Foo { int i; char a; }; #pragma pack(pop) int size = sizeof(Foo); // 5 байт
10. Как сгенерировать pure virtual function call исключение?
Ответ: Нужно вызвать чисто виртуальный метод в конструкторе родительского класса т.е. до создания дочернего, в котором этот метод реализован. Т.к. современный компилятор не даст это сделать напрямую, то нужно будет использовать промежуточный метод.
Пример:
class Base { public: Base() { base_func(); } void base_func() { func(); // pure virtual function call exception } virtual void func() = 0; }; class Derived : public Base { public: virtual void func() { } };
11. В чем отличие vector от deque?
Ответ: Здесь вспоминают о наличии у deque методов push_front и pop_front. Но основное отличие в организации памяти, у vector она как у обычного Си-массива, т.е. последовательный и непрерывный набор байт, а у deque это фрагменты с разрывами. За счет этого отличия vector всегда можно привести к обычному массиву или скопировать целиком участок памяти, но зато у deque операции вставки/удаления в начало быстрее (O(1) против O(n)), ввиду того, что не нужно перемещать остальные значения.
12. Что дают разные модификаторы при наследовании?
Ответ: Изменяют зону видимости членов базового класса.
Пример:
class Base { public: int i; }; class Derived : private Base { // i теперь имеет модификатор доступа private }; class Derived2 : private Derived { public: Derived2() { i = 2; // error C2247: 'Base::i' not accessible because 'Derived' uses 'private' to inherit from 'Base' } }; Derived d; d.i; // error C2247
При private наследовании protected и public члены становятся private. При protected наследовании public становится protected. А при public ничего не изменяется. Более подробно об этом Алена писала вот здесь.
13. Что такое инкапсуляция?
Ответ (Wiki): Инкапсуляция — свойство языка программирования, позволяющее объединить и защитить данные и код в объектe и скрыть реализацию объекта от пользователя (прикладного программиста). При этом пользователю предоставляется только спецификация (интерфейс) объекта.
Пользователь может взаимодействовать с объектом только через этот интерфейс. Реализуется с помощью ключевого слова: public.
Пользователь не может использовать закрытые данные и методы. Реализуется с помощью ключевых слов: private, protected.
14. Что такое полиморфизм?
Ответ (Wiki): Возможность объектов с одинаковой спецификацией иметь различную реализацию.
Пример:
class Figure { ... void Draw() const; ... }; class Square : public Figure { ... void Draw() const; ... }; class Circle : public Figure { ... void Draw() const; ... };
Т.е. есть базовый класс фигура, и от него унаследованы два класса квадрат и круг, имеющие собственную реализацию метода Draw().
15. В чем отличие malloc от new?
Ответ: malloc — выделение блока памяти в стиле Си, опасное с точки зрения приведения типов (non-typesafe), т.к. возвращает void * и требует обязательного приведения. new — выделение блока памяти и последующий вызов конструктора, безопасное с точки зрения приведения типов (typesafe), т.к. тип возвращаемого значения определен заранее.
16. Что такое чисто виртуальный метод и абстрактный класс?
Ответ: Чисто виртуальный метод — это метод, у которого отсутствует реализация. Абстрактный класс — это класс имеющий хотя бы один чисто виртуальный метод. Как следствие, экземпляр подобного класса не может быть создан т.к. отсутствует реализация виртуального метода.
Пример:
// Абстрактный класс class Foo { public: // Чисто виртуальный метод virtual void func() = 0; }; class Bar : public Foo { public: virtual void func() { } }; Foo f; // error C2259: 'Foo' : cannot instantiate abstract class Bar b; // Ok
17. Для чего используется вызов throw без аргументов?
Ответ: Для повторного возбуждения предыдущего исключения и направления его следующему обработчику.
Пример:
try { //.... try { // Call something } catch(const std::exception& ) { // Make/Check something.. throw; // Пересылаем исключение следующему обработчику } //... } catch(const std::exception& e) { std::cout << e.what() << std::endl; }
18. В чем различия между delete и delete[]?
Ответ: delete предназначен для уничтожения объектов, память под которые выделена при помощи new(). delete[] для объектов выделенных при помощи оператора new[]().
Пример:
class Foo { }; Foo *pFoo = new Foo(); delete pFoo; Foo *pFooArray = new Foo[10](); delete[] pFoo;
При неправильном использовании оператора delete (например, delete вместо delete[]) результат будет: undefined behavior.
19. Что стоит учитывать при использовании auto_ptr?
Ответ: Так как данный умный указатель реализует подход разрушающего копирования, то при присвоении его другому умному указателю оригинальный потеряет свое значение. А так же его нельзя использовать в стандартных STL контейнерах.
Пример:
std::auto_ptr<Foo> a(new Foo); std::auto_ptr<Foo> b; b = a; // a больше не ссылается на Foo std::vector<std::auto_ptr<Foo>> v; v.push_back(b); // error C2558: class 'std::auto_ptr<_Ty>' : no copy constructor available or copy constructor is declared 'explicit'
Также ввиду того, что в деструкторе auto_ptr вызывается оператор delete, нельзя хранить объекты созданные при помощи new[](). Именно из-за этого нюанса boost предоставляет умные указатели в двух вариантах, для просто объекта и их коллекции, например, shared_ptr и shared_array.
20. Для чего используется ключевое слово volatile?
Ответ: Для указания компилятору, что доступ к переменной может осуществляться из мест, неподконтрольных ему. А как следствие, что работу с данной переменной не нужно подвергать разного рода оптимизациям.
Пример:
volatile int i = 1; // Независимо от прочего кода, данная переменная не будет оптимизирована.
Т.е. если volatile присутствует в каком-то условии, которое не меняется со временем, то компилятор может оптимизировать его, чтобы избежать ненужных проверок, при использовании volatile компилятор скорее всего не будет этого делать.
Пример:
while (1) { if(i == 1) { // Какой-то код не изменяющий i } } // Если бы volatile отсутствовало, то компилятор мог бы переделать код на что-то аля: if(i == 1) // Нет необходимости проверять i все время, если и так известно, что оно не изменяется { while (1) { // Какой-то код не изменяющий i } }
Обновлено 24.04 в 13.35
