15 популярных вопросов с IT-собеседований по языку C++

Мы собрали 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

Суть вопроса заключается в проверке арифметических знаний и понимании всей магии, которая происходит за квадратными скобками.

Вас также могут заинтересовать другие статьи по теме:

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ

matyushkin
29 марта 2020

ТОП-10 книг по C++: от новичка до профессионала

Книги по C++ на русском языке с лучшими оценками. Расставлены в порядке воз...