0 24.7K ru

Типичные вопросы на собеседовании по C++

Зачем нужен виртуальный деструктор?

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

class My
{
public:
    virtual ~My()
    {
        std::cout << "Hello from ~My()" << std::endl;
    }
};

class Derived : public My
{
public:
    virtual ~Derived()
    {
        // Здесь могла бы быть очистка ресурсов
        std::cout << "Hello from ~Derived()" << std::endl;
    }
};

My *obj = new Derived();
delete obj;

Output:

Hello from ~Derived()
Hello from ~My()

Без ключевого слова virtual у родительского класса My деструктор порожденного класса не был бы вызван. Т.е. вызвался бы только ~My().

Что стоит помнить при использовании исключений в конструкторе объекта?

Если исключение не обработано, то 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

Здесь могут спросить: «Как бы вы решили эту проблему при подобной ситуации». Правильный ответ: «Воспользовался бы умными указателями».

 Пример умного указателя:

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, в деструкторе которого и будет закрыт хэндл. 

Изобретать велосипед, конечно, не надо, все уже написано до нас, например в boostLokiATL и т.п.

Для каких целей применяется ключевое слово const?

  1. Позволяет задать константность объекта
  2. Позволяет задать константность указателя
  3. Позволяет указать, что данный метод не модифицирует члены класса, т.е. сохраняет состояние объекта

Учтите что константный метод может изменять члены класса, если они объявлены как mutable

Можете ли вы написать пример какого-нибудь алгоритма сортировки?

Первое что приходит всем на ум это сортировка пузырьком.

Рассмотрим пузырковую сортировку без временных переменных

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 );

Напишите код для реверса строки?

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);

Как защитить объект от копирования?

Сделать 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

В чем разница между struct и class?

Практически ни в чем. В struct модификаторы доступа по умолчанию public, в class private. Также отличается и наследование по умолчанию, у struct — public, у class — private.

Сколько в памяти занимает произвольная структура?

sizeof всех членов + остаток для выравнивания (по умолчанию выравнивание 4 байта) + sizeof указателя на vtable (если есть виртуальные функции) + указатели на классы предков, от которых было сделано виртуальное наследование (размер указателя * количество классов)

 

Как сгенерировать 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()
    {
    }
};

В чем отличие vector от deque?

Здесь вспоминают о наличии у deque методов push_front и pop_front. Но основное отличие в организации памяти, у vector она как у обычного Си-массива, т.е. последовательный и непрерывный набор байт, а у deque это фрагменты с разрывами. За счет этого отличия vector всегда можно привести к обычному массиву или скопировать целиком участок памяти, но зато у deque операции вставки/удаления в начало быстрее (O(1) против O(n)), ввиду того, что не нужно перемещать 

В чем отличие malloc от new?

malloc — выделение блока памяти в стиле Си, опасное с точки зрения приведения типов (non-typesafe), т.к. возвращает void * и требует обязательного приведения. new — выделение блока памяти и последующий вызов конструктора, безопасное с точки зрения приведения типов (typesafe), т.к. тип возвращаемого значения определен заранее.

В чем различия между dynamic_cast и reinterpret_cast?

xxx_cast<type_to>(expression_from)

Динамическое приведение - это безопасное приведение по иерархии наследования, в том числе и для виртуального наследования. Проводит преобразование типа, предварительно убедившись (с помощью RTTI), что объект expression_from в действительности является объектом типа type_to. Если нет: для указателей возвращает NULL.

При reinterpret_cast результат не гарантирован, проверки не осуществляются. Ограничения на expression_from: порядковый тип (логический, символьный, целый, перечисляемый), указатель, ссылка. Ограничения на type_to: для порядкового типа или указателя — порядковый тип или указатель. Для ссылки — ссылка.

Для чего нужен аллокатор и как создать свой собственный аллокатор?

Аллокатор это шаблонный класс, который отвечает за выделение памяти и создание объектов. По умолчанию все контейнера используют std::allocator<T>. 
В языке c++ имеется так же возможность написать свой аллокатор. У своего алокатора должно быть такое объявление:

template <class T> 
    class my_allocator  
    { 
      typedef size_t    size_type; 
      typedef ptrdiff_t difference_type; 
      typedef T*        pointer; 
      typedef const T*  const_pointer; 
      typedef T&        reference; 
      typedef const T&  const_reference; 
      typedef T         value_type; 
 
      pointer allocate(size_type st, const void* hint = 0); 
      void deallocate (pointer p, size_type st); 
      void construct (pointer p, const_reference val); 
      void destroy (pointer p); 
      template <class U>  
      struct rebind { typedef allocator<U> other; }; 
    }; 

В чём суть множественного наследования? Какие проблемы могут возникнуть при его использовании? Как их преодолеть?

Множественное наследование – мощный способ связи классов в с++. С помощью множественного наследования класс может иметь сразу несколько базовых классов, объединяя в себе их свойства. Однако данный метод порождает определенные неудобства, из-за которых множественно наследование отсутствует в таком языке, как Java, к примеру, уберегая программиста от возможных проблем, и вынуждая его строить механизм множественного наследования без порождения выше упомянутых проблем.
Проблемы, собственно говоря, возникают, когда имеет место такая ситуация:
Пусть класс A – базовый, далее классы B и С наследуют A, к классу D применено множественное наследование - для него базовыми являются одновременно B и C. Программа видит эту структуры таким образом:
A(1)      A(2)
|             |
B            C
     \    /
        D
Теперь, вызывая из D метод, расположенный в A программа сталкивается с неоднозначностью: а из «какого» A вызывать? A(1) или A(2)? 
Избежать данной проблемы поможет использование ключевого слова virtual, которое превращает класс A в виртуальный класс, так сказать «класс – шаблон». Теперь никакой неоднозначности нет, и ситуация выглядит вот так:
         A
    /          \
B                C
    \           /
          D

Какая разница между указателями: 

int* i=new int[0]; 
int* j=(int*) malloc(0);

Ответ: 

int* i=new int[0];

возвращает ненулевой указатель, а

int* j=(int*) malloc(0);

возвращает нулевой указатель.

int* i=new int[0]; 

выделит память в GlobalHeap, а

int* j=(int*) malloc(0); 

выделит в LocalHeap. 

Отсюда в оператора new преимущества в памяти.

В чем основное различие между деструктором и оператором delete?

Оператор delete освобождает область памяти зарезервированную ранее с помощью оператора new. При этом для объектов автоматически будет вызван деструктор. Деструктор содержит код, который необходимо выполнить до освобождения памяти, например, освобождение системных ресурсов и т.д. Вызов деструктора вручную не приводит к освобождению памяти, занимаемой объектом.

Чем отличается vector от deque?

Отличие в организации памяти, у vector она как у обычного Си-массива - последовательный и непрерывный набор байт, а у deque это фрагменты с разрывами. За счет этого отличия vector всегда можно привести к обычному массиву или скопировать целиком участок памяти, но зато у deque операции вставки/удаления в начало быстрее (O(1) против O(n)), ввиду того, что не нужно перемещать остальные значения. А также наличие у deque методов push_front и pop_front.

Что можно сказать о delete this?

Это выражение считается выражением плохого тона и нужно стараться избегать его использования, так как оно ведет к ошибкам.
Данная конструкция имеет две ловушки.
Во-первых, если оно выполняется в методе extern, static или automatic объекта, программа скорее всего завершится досрочно вскоре после выполнения оператора delete. Не существует переносимого способа сообщить, что объект создан на куче, таким образом класс не может проверить корректно ли объект создался.
Во-вторых, при таком самоуничтожении, программа может и не узнать об этом. Для нее объект по-прежнему продолжает существовать, несмотря на то, что это не так. Любое обращение к this или к любым данным объекта приведет к катастрофическим последствиям. При этом методы, которые не содержат обращения к полям-данным объекта, выполняются нормально.

В чем разница между массивом и списком?

Массив – это набор однородных элементов, а список – разнородных.
Распределение памяти массива всегда статическое и непрерывное, а в списке все это динамическое и рандомное.

В случае с массивами пользователю не нужно управлять выделением памяти, а при использовании списков придется, из-за ее динамичности.

 

Comments:

Please log in to be able add comments.