Мы собрали 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
Суть вопроса заключается в проверке арифметических знаний и понимании всей магии, которая происходит за квадратными скобками.
Комментарии
спасибо
Ошибка в первом же примере или я не знаю что за версию компилятора вы используете.
После ошибки в первом же ответе, читать дальше не стал.. (компилятор ошибку выдаст, нету типа данных возвращаемого функцией)
Это не ошибка, а прикол VS, в ней можно объявить main как void.