Мы собрали 15 самых каверзных вопросов с IT-собеседований по C++, на которые не просто желательно, а необходимо знать ответы.
Часто работодатели пытаются ввести нас в заблуждение сложными вопросами и логическими задачами. Это своего рода мозговой штурм, чтобы проверить кандидата не только на умение правильно решать сложные задачи, но и на время, за которое он приходит к правильным ответам.
Чтобы не ударить лицом в грязь, просмотрите нашу подборку из 15 нелегких вопросов с IT-собеседований по C++: она вам обязательно пригодится.
1. Что мы получим на выходе, исходя из условия?
#include<iostream> using namespace std; f(); int x = 9; void main() { f(); cout << x; } f() { ::x = 8; }
Ответ: 8.
Думаю, объяснения излишни, но на всякий случай: «чтение» кода в плюсах происходит строго сверху вниз. Так что как в Java, где можно ниже мейна объявлять сколь угодно методов, а они все равно будут воспроизводиться в указанном порядке, сделать не получится.
2. Что будет выведено и почему?
#include <iostream> int main(int argc, char **argv) { std::cout << 25u - 50; return 0; }
Ответ не -25, не надейтесь.
Ответ (который удивит многих): 4294967271, предполагая 32-битные целые числа.
Почему так происходит?
Существует иерархия: long double, double, float, unsigned long int, long int, unsigned int, int. И когда два операнда определены как 25u (unsigned int) и 50 (int), 50 также будет интерпретироваться как беззнаковое целое число, то есть 50u.
Кроме того, результат операции также будет иметь тип операндов. Следовательно, результат 25u - 50u и сам является беззнаковым целым числом. Таким образом, результат -25 преобразуется в 4294967271.
3. Когда используется виртуальное наследование?
Распространенный вопрос с IT-собеседований. Когда есть класс (класс A), который наследуется от 2 родителей (B и C), оба из которых разделяют родителя (класс D):
#include <iostream> class D { public: void foo() { std::cout << "Foooooo" << std::endl; } }; class C: public D { }; class B: public D { }; class A: public B, public C { }; int main(int argc, const char * argv[]) { A a; a.foo(); }
Если вы не используете виртуальное наследование, то получите две копии D в классе A: один из B и один из C. Чтобы исправить это, вам нужно изменить объявления классов C и B следующим образом:
class C: virtual public D { }; class B: virtual public D { };
4. Что вообще означает модификатор virtual?
В C++ виртуальные функции позволяют поддерживать полиморфизм – одну из ключевых составляющих ООП. С его помощью в классах-потомках можно переопределять функции класса-родителя. Без виртуальной функции мы получаем «раннее связывание», а с ней – «позднюю привязку». То есть, какая реализация метода используется, определяется непосредственно во время выполнения и основывается на типе объекта с указателем на объект, из которого он построен.
5. Приведите пример использования виртуальной функции.
У нас есть 2 класса:
class Animal { public: void eat() { std::cout << "I'm eating generic food."; } }; class Cat : public Animal { public: void eat() { std::cout << "I'm eating a rat."; } };
В основной функции:
Animal *animal = new Animal; Cat *cat = new Cat; animal->eat(); // Outputs: "I'm eating generic food." cat->eat(); // Outputs: "I'm eating a rat."
Теперь сделаем так, что eat() будет вызываться посредством какой-нибудь промежуточной функции:
void func(Animal *xyz) { xyz->eat(); }
В основной функции:
Animal *animal = new Animal; Cat *cat = new Cat; func(animal); // Outputs: "I'm eating generic food." func(cat); // Outputs: "I'm eating generic food."
Как это исправить, если мы захотим добавить больше животных? Просто делаем eat() виртуальной функцией:
class Animal { public: virtual void eat() { std::cout << "I'm eating generic food."; } }; class Cat : public Animal { public: void eat() { std::cout << "I'm eating a rat."; } };
Теперь в основной функции:
func(animal); // Outputs: "I'm eating generic food." func(cat); // Outputs: "I'm eating a rat."
Все работает!
6. Есть ли разница между классом и структурой?
Единственное различие между классом и структурой – это модификаторы доступа. Элементы структуры являются общедоступными по умолчанию, а класса – private. Рекомендуется использовать классы, когда вам нужен объект с методами, а в случае с простым объектом – структуры.
7. В чем проблема следующего фрагмента?
class A { public: A() {} ~A(){} }; class B: public A { public: B():A(){} ~B(){} }; int main(void) { A* a = new B(); delete a; }
Из спецификации (C++11 §5.3.5/3):
Если статический тип подлежащего удалению объекта отличается от его динамического типа, статический тип должен быть базовым классом динамического типа подлежащего удалению объекта и иметь виртуальный деструктор или поведение undefined.
8. Что такое класс хранения?
Класс, который определяет срок существования, компоновку и расположение переменных/функций в памяти.
В C ++ поддерживаются такие классы хранения: auto, static, register, extern и mutable.
Обратите внимание, что register устарел для C++11. Для C++17 он был удален и зарезервирован для будущего использования.
9. Как вызвать функцию C в программе на C++?
Еще один популярный вопрос с IT-собеседований, рассчитанный на новичков, совершенно не представляющих, как такое возможно. На самом же деле возможно, если использовать extern «C»:
//C code void func(int i) { //code } void print(int i) { //code }
//C++ code extern "C"{ void func(int i); void print(int i); } void myfunc(int i) { func(i); print(i); }
10. Что делает ключевое слово const?
Ответ: задает константность объекта, указателя, а также указывает, что данный метод сохраняет состояние объекта (не модифицирует члены класса).
Пример с неизменяемыми членами класса:
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 } };
11. Виртуальный деструктор: что он собой представляет?
Во-первых, он объявляется как virtual (об этом модификаторе мы писали выше). Он нужен, чтобы с удалением указателя на какой-нибудь объект был вызван деструктор данного объекта. Например, у нас есть 2 класса:
class base { public: ~base() { cout << "Вызывается деструктор класса base"; } }; class derived: public base { public: ~derived() { cout << "Вызывается деструктор класса derived"; } };
Выполняем следующее:
base *p; //объявляем указатель на base derived d_ob; p=new derived(); delete p; return 0;
В итоге выполнится деструктор базового класса, а не производного. Это может поспособствовать утечке памяти. Если же до объявления деструкторов установить модификатор virtual, выполнится деструктор производного класса.
12. Виртуальный конструктор: что он собой представляет?
Каверзный вопрос с IT-собеседований, который чаще всего задают именно после виртуальных деструкторов, дабы сбить кандидата с толку. Конструктор не может быть виртуальным, поскольку в нем нет никакого смысла: при создании объектов нет такой неоднозначности, как при их удалении.
13. Сколько раз будет выполняться этот цикл?
unsigned char half_limit = 150; for (unsigned char i = 0; i < 2 * half_limit; ++i) { //что-то происходит; }
Еще один вопрос с подвохом с IT-собеседований. Если бы вы сказали 300, а i был объявлен как int, вы были бы правы. Но поскольку i объявлен как unsigned char, правильный ответ – зацикливание (бесконечный цикл).
Объясняем. Выражение 2 * half_limit будет повышаться до int (на основе правил преобразования C++) и заимеет значение 300. Но так как i – это unsigned char, он пересматривается по 8-битному значению, которое после достижения 255 будет переполняться, поэтому вернется к 0, и цикл будет продолжаться вечно.
14. Каков результат следующего кода?
#include <iostream> class Base { virtual void method() {std::cout << "from Base" << std::endl;} public: virtual ~Base() {method();} void baseMethod() {method();} }; class A : public Base { void method() {std::cout << "from A" << std::endl;} public: ~A() {method();} }; int main(void) { Base* base = new A; base->baseMethod(); delete base; return 0; }
Ответ:
from A from A from Base
Здесь важно отметить порядок уничтожения классов и то, как метод класса Base возвращается к своей реализации после удаления А.
15. Что мы получим на выходе?
#include <iostream> int main(int argc, const char * argv[]) { int a[] = {1, 2, 3, 4, 5, 6}; std::cout << (1 + 3)[a] - a[0] + (a + 1)[2]; }
Ответ: 8.
Объяснение:
- (1 + 3)[a] – то же, что и a[1 + 3] == 5
- a[0] == 1
- (a + 1)[2] – то же, что и a[3] == 4
Суть вопроса заключается в проверке арифметических знаний и понимании всей магии, которая происходит за квадратными скобками.
Комментарии