Вопросы и ответы с собеседований по C++
Вопросы и ответы с собеседований C++ разработчиков. Определение указателей, встраивание C-кода, структуры, классы, методы, ключевое слово const, потребление и выделение памяти, обработка исключений, практические вопросы и многое другое.
Какой код выполняется до main?
Конструкторы глобальных объектов.
Как вызвать функцию C в коде C++?
Это возможно, если использовать 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);
}
Что такое указатель?
Указатель — это переменная, которая хранит адрес памяти объекта. Указатели широко используются в C и C++ для трех основных целей: 1. Для выделения новых объектов в куче, 2. Для передачи функций другим функциям 3. Для итерации элементов в массивах или других структурах данных.
Раскажите про ключевое слово const
1. Позволяет задать константность объекта
2. Позволяет задать константность указателя
3. Позволяет указать, что данный метод не модифицирует члены класса, т.е. сохраняет состояние объекта
Не можем изменить значение объекта:
const int i = 1;
i = 2; // error C3892: 'i' : you cannot assign to a variable that is const
Не можем изменить указатель на объект:
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
Не можем изменить члены класса:
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
.
Как запретить копирование объекта?
Для того, чтобы запретить копирование объекта, нужно сделать 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
Зачем нужен модификатор virtual?
В C++ виртуальные функции позволяют поддерживать полиморфизм – одну из ключевых составляющих ООП. С его помощью в классах-потомках можно переопределять функции класса-родителя. Без виртуальной функции мы получаем «раннее связывание», а с ней – «позднее». То есть, какая реализация метода используется, определяется непосредственно во время выполнения и основывается на типе объекта с указателем на объект, из которого он построен.
Что такое чисто виртуальный метод и абстрактный класс?
// Абстрактный класс
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
Чисто виртуальный метод — это метод, у которого отсутствует реализация.
Абстрактный класс — это класс, имеющий хотя бы один чисто виртуальный метод. Как следствие, экземпляр подобного класса не может быть создан т. к. отсутствует реализация виртуального метода.
Что такое виртуальный деструктор?
Чтобы избежать возможной утечки ресурсов или другого неконтролируемого поведения объекта, в логику работы которого включен вызов деструктора.
Пример:
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;
Вывод:
Hello from ~Derived()
Hello from ~Base()
Без ключевого слова virtual
у родительского класса Base
деструктор порожденного класса не был бы вызван. Т.е. вызвался бы только ~Base()
.
Что такое виртуальный конструктор?
Каверзный вопрос, который чаще всего задают именно после виртуальных деструкторов, дабы сбить кандидата с толку. Конструктор не может быть виртуальным, поскольку в нем нет никакого смысла: при создании объектов нет такой неоднозначности, как при их удалении.
Разница между struct и class?
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'
Практически ни в чём.
В struct
модификаторы доступа по умолчанию public
, в class
- private
.
Так же отличается и наследование по умолчанию, у struct
— public
, у class
— private
.
Разница между struct и union?
В struct
все её члены хранятся одновременно, и к ним есть доступ.
В union
существует только один тип, который в данный момент используется.
Обращение к остальным вызовет undefined behavior
.
Сколько памяти занимает произвольная структура?
Можно рассчитать по формуле: sizeof
всех членов
+ остаток для выравнивания (по умолчанию выравнивание 4 байта)
+ sizeof
указателя на vtable
(если есть виртуальные функции)
+ указатели на классы предков, от которых было сделано виртуальное наследование (размер указателя * количество классов)
Пример:
struct Foo
{
int i;
char a;
};
int size = sizeof(Foo); // 8 байт, хотя int + char = 5. Все дело в дефолтном выравнивании равном 4, т.е. размер должен быть кратен блоку в 4 байта.
// Установим выравнивание в 1 байт
struct Foo
{
int i;
char a;
};
int size = sizeof(Foo); // 5 байт
Различия std::array и std::vector?
std::array
- это обёртка вокруг обычного массива фиксированного размера, а std::vector
- это динамически расширяемый массив.
std::array
хранит свои данные в стековой памяти, а std::vector
выделяет память в куче (динамическая память).
У std::array
в параметрах шаблона указывается тип хранимых элементов и размер массива. А у std::vector
- тип элементов и ещё возможно указать аллокатор, который нужно использовать при выделении памяти (с помощью new
).
std::array
чуть лучше по производительности в случае небольшого массива, чем std::vector
, который содержит в себе несколько указателей, которые указывают на выделенный в куче кусок памяти.
В чем отличие vector от deque?
У deque
имеются методы push_front
и pop_front
.
Но основное отличие в организации памяти: у vector
она, как у обычного Си-массива, т.е. последовательный и непрерывный набор байт, а у deque
это фрагменты с разрывами.
За счет этого отличия vector
всегда можно привести к обычному массиву или скопировать целиком участок памяти, но зато у deque
операции вставки/удаления в начало быстрее (O(1)
против O(n)
), ввиду того, что не нужно перемещать остальные значения.
В чем отличие malloc от new?
malloc
— выделение блока памяти в стиле Си, опасное с точки зрения приведения типов (non-typesafe
), т.к. возвращает void
и требует обязательного приведения.
new
— выделение блока памяти и последующий вызов конструктора; безопасное с точки зрения приведения типов (typesafe
), т.к. тип возвращаемого значения определен заранее.
Различия delete и delete[]?
class Foo
{
};
Foo *pFoo = new Foo();
delete pFoo;
Foo *pFooArray = new Foo[10]();
delete[] pFoo;
delete
предназначен для уничтожения объектов, память под которые выделена при помощи new()
.
delete[]
предназначен для объектов, выделенных при помощи оператора new[]()
.
При неправильном использовании оператора delete
(например, delete
вместо delete[]
) результат будет:
undefined behavior
Как добавить объект в качестве ключа в ассоциативные контейнеры?
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
Т.к. значения в ассоциативных контейнерах хранятся отсортированными, объект должен реализовывать оператор сравнения <
, а остальные операторы сравнения могут быть выражены через него.
Что дают разные модификаторы при наследовании?
Изменяют зону видимости членов базового класса.
При private
наследовании protected
и public
члены становятся private
.
При protected
наследовании public
становится protected
.
А при public
ничего не изменяется.
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
Для чего используется ключевое слово volatile?
volatile int i = 1; // Независимо от прочего кода, данная переменная не будет оптимизирована.
Для указания компилятору, что доступ к переменной может осуществляться из мест, неподконтрольных ему. А как следствие, что работу с данной переменной не нужно подвергать разного рода оптимизациям.
Т.е. если volatile
присутствует в каком-то условии, которое не меняется со временем, то компилятор может оптимизировать его, чтобы избежать ненужных проверок.
При использовании volatile
компилятор скорее всего не будет этого делать.
while (1)
{
if(i == 1)
{
// Какой-то код, не изменяющий i
}
}
// Если бы volatile отсутствовало, то компилятор мог бы переделать код на что-то аля:
if(i == 1) // Нет необходимости проверять i все время, если и так известно, что оно не изменяется
{
while (1)
{
// Какой-то код, не изменяющий i
}
}
Для чего используется вызов throw без аргументов?
try
{
//....
try
{
// Call something
}
catch(const std::exception& )
{
// Make/Check something..
throw; // Пересылаем исключение следующему обработчику
}
//...
}
catch(const std::exception& e)
{
std::cout << e.what() << std::endl;
}
Для повторного возбуждения предыдущего исключения и направления его следующему обработчику.
Как сгенерировать 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()
{
}
};
Какие нюансы у auto_ptr?
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'
Так как данный умный указатель реализует подход разрушающего копирования, то при присвоении его другому умному указателю оригинальный потеряет свое значение. А так же его нельзя использовать в стандартных STL контейнерах.
Также ввиду того, что в деструкторе auto_ptr
вызывается оператор delete
, нельзя хранить объекты, созданные при помощи new[]()
.
Именно из-за этого нюанса boost
предоставляет умные указатели в двух вариантах, для просто объекта и их коллекции, например, shared_ptr
и shared_array
.
Бывает такое, что оператор new не выделяет память?
Да, бывает, когда new
передаётся указатель на уже выделенную память (например, с помощью malloc
).
Это называется placement new
. И оператор new
без изменения возвращает второй параметр - указатель. Это используется для создания объектов в выделенном "хранилище" или после malloc
.
Пример:
struct A{
A(){ std::cout << "I'm constructor\n"; }
~A(){ std::cout << "I'm destructor\n"; }
};
int main() {
void *pv = malloc(sizeof(A)); // или char *pv = new char[sizeof(A)];
A *pa = new(pv) A;
std::cout << std::hex << reinterpret_cast<A*>(pv) << " = " << pa << '\n';
pa->~A();
free(pv); // или delete[] pv;
pv = nullptr;
}
Важно! В этом случае деструктор нужно вызывать самостоятельно!
Когда вызывается конструктор копирования, а когда копирующий оператор присваивания?
Конструктор копирования вызывается, когда объект ещё не создан и на основе другого объекта создается текущий, а копирующий оператор присваивания - когда объект уже создан и происходит копирование всех элементов из одного объекта в другой.
Пример:
class A{
public:
A(){ std::cout << "Constructor\n"; }
A(const A&){ std::cout << "Copy constructor\n"; }
A& operator=(const A&){
std::cout << "Copy assignment operator\n";
return *this;
}
~A(){ std::cout << "Destructor\n"; }
};
int main() {
A a1;
A a2;
A a3 = a1; // вызывается копирующий конструктор A(const A&), так как объект a3 не создан
a2 = a1; // вызывается оператор копирующего присваивания: A& operator=
}
Constructor
Constructor
Copy constructor
Copy assignment operator
Destructor
Destructor
Destructor
Можно ли использовать ссылку при возвращении из функции?
Нельзя: - никогда нельзя возвращать ссылку на локальную переменную, созданную внутри функции, потому что она разрушится сразу после выхода из функции; - нельзя возвращать ссылку на выделенную память, потому что её нельзя будет очистить. Можно: - когда возвращается константная ссылка на член класса; - когда вы пишете оператор <<, оператор = и оператор индексирования, они должны возвращать ссылку на объект. Возвращать ссылку - это хорошая практика, потому что не вызывается лишний копирующий конструктор для возвращаемого значения, но иногда лучше и правильнее возвращать по значению.
Что будет выведено на экран?
int a=4;
int &f(int x){
a = a + x;
return a;
}
int main(void){
int t = 5;
std::cout << f(t) << std::endl; // 9
f(t) = 20;
std::cout << f(t) << std::endl; // 25
t = f(t);
std::cout << f(t) << std::endl; // 60
}
В первом случае на печать выведется
9
, потому что функция f
принимает параметр t = 5
, значение которого внутри функции прибавляется к глобальной переменной a
. Теперь a = 9
. А f
возвращает ссылку на неё и печатается значение a
, то есть 9
.
Далее не важно, что происходит внутри функции, важно, что возвращаемой ссылке на a
присваивается 20
. Значит, a = 20
. Переменная t
не поменяла своё значение, так как в функцию она передается по значению, а не по ссылке.
Затем опять вызываем f(5)
, при этом a = 20
. Выполняем a = 20 + 5
, на печати увидим число 25
.
Теперь присваиваем t
значение a
(в этот момент a = 25 + 5
), значит, t
станет равно 30
.
И, наконец, последняя печать. Вызываем f(30)
. Выполняется a = 30 + 30
и возвращается значение 60
.