Билеты 44 - 54

44. Чем отличается new int(10) от new int[10]?

Кратки ответ

Первый аллоцирует число int{10} и возвращает указатель на этот элемент, второй вариант сложнее, сначала он записывает размер массива, который вы пытаетесь разместить, затем все элементы этого массива поочередно.

operator new !пытается!** аллоцировать какую-то память в динамической куче (в оперативке). Зачем это нужно: выделяя память на куче с помощью new, вы берете ответственность за ее освобождение, так как время жизни этого объекта контролируется вами. Соответственно существует два способа освободить эту память (в рамках C++), а именно operator delete и operator delete[], соответственно каждый из них нужно вызывать в соответствии с тем, какой оператор вы использовали для выделения памяти. Так как оператор delete освобождает указанную ему по указателю память и больше ничего, а delete[] ищет тот самый размер до указателя, который мы туда должны были записать оператором new[], и только по нему далее удаляет столько памяти, сколько нужно.

Вывод:

Для new используйте delete, для new[] используйте delete[], иначе пупа получит за лупу.

45. Можно ли делать delete ptr; после auto ptr = new T[5]; и почему?

Кратки ответ

Нельзя, это утечка памяти и UB.

Смотри выше! Если так сделать, то это UB. Такой код конечно может запуститься и ничего страшного не произойдет, ведь после завершения программы вся память, которую она выделила, очищается автоматически, но в целом такое поведение вызывает утечку памяти, которая плоха тем, что бывает трудно обнаружима и может влечь внезапные ошибки в рандомный момент.
Offtop: В том числе для решения такой проблемы придумали умные указатели.

46. Что такое placement new (размещающее new)?

Кратки ответ

Аналогично обычному оператору new позволяет выделять объект в куске памяти, но на этот раз не только на куче, но и на любом буфере, в том числе статическом.

int main() {
    void* buffer = ...;
    T* obj = new (buffer) T();
    obj->~T();
    return 0;
}

Память которую мы используем для буфера должна быть достаточно неинициализированной памяти для размещения объектов; также она должна быть выровнена. Стоит заметить, что мы должны самостоятельно вызывать деструктор объекта, который выделили, так как он не будет вызван автоматически из-за специфики хранения. Бывает полезно, если мы не хотим много раз ходить к ядру и просить память или хотите использовать статическую память для данных манипуляций, что может отразиться на производительности.

47. Как обеспечить корректное удаление объекта, созданного при помощи размещающего new?

Кратки ответ

Явно вызвать детсрутктор размещенного объекта.

Смотри выше! Вырезка из базы: You must manually call the object's destructor if its side effects are depended by the program.

48. Undefined, unspecified and implementation-defined behavior

Кратки ответ

В сущности эти три понятия характеризуют поведение программы в случаях, когда стандарт языка не дает прямого ответа. Undefined behavior - может произойти все что угодно. Unspecified behavior - стандарт не заставляет документировать как делать, но органичивает возможные варианты. Implementation-defined behavior - компилятор должен явно задокументировать что он делает.

Примеры соответствующих поведений описаны в ссылках в названии:

  1. Undefined behavior
    Стандарт не несет ответственности за то, что произойдет; произойти может все что угодно. Чаще всего конечно происходит далеко не все что угодно и никто форматировать ваш диск не будет; но в сущности поведение программы в данных ситуациях бывает крайне непредсказуемым.

  2. Unspecified behavior
    Ситуация когда компилятор может выбирать что делать; так как стандарт определил возможные варианты но не заставил указать как именно — например передача аргументов в функцию когда порядок вычисления аргументов не установлен стандартом; однако все аргументы должны быть вычислены; соответственно разные компиляторы выбирают разные способы.

  3. Implementation-defined behavior
    Здесь в отличие от пункта выше компилятор должен четко указать как именно он реализует это поведение; разработчики могут реализовывать платформопереносимые программы.

49. Что такое наблюдаемое поведение программы (observable behaviour), зачем нужно это понятие?

Кратки ответ

Класс поведения, не состоящий в классах поведения, таких как: Undefined behavior, Unspecified behavior и Implementation-defined behavior. Нужно для возможности остлеживания сторонними программами выполнения нашей программы.

Итак, в предыдущем билете затрагивались классы поведения программ; соответственно если программа не находится ни в одном из этих классов она является программой с наблюдаемым поведением (так в стандарте определено это понятие). Основной смысл и польза состоит в том что выполнение программы достаточно корректно и предсказуемо; мы можем анализировать ее выполнение с помощью других программ что дает нам большие возможности для анализа кода как для разработчика.

50. Что такое this? Можно ли делать присваивание в this и почему?

this — это константный указатель на экземпляр класса метод которого был вызван. Так как указатель константный то присвоить ему другой объект невозможно; но можно получать доступ ко всем его методам и полям как у указателя.

51. Можно ли делать delete this, почему?

Это выражение вполне возможно использовать; если объект был выделен с помощью new, оно будет освобождать текущий экземпляр this. После выполнения этой операции любые обращения будут недействительными. Такое даже используется, например в shared_ptr.

52. Что такое явное/неявное преобразование типов?

Кратки ответ

В C++ преобразование типов — это процесс, при котором значение одного типа данных преобразуется в значение другого типа. Это может происходить явно(самим человеком, например static_cast) или неявно (автоматически компилятором).

Неявное преобразование типов:

Компилятор сам выполняет преобразование, если считает это безопасным и логичным.

int a = 10;
double b = a; // неявное преобразование int → double

int значение a автоматически преобразуется в double, потому что double может без потерь представить любое int.

Когда обычно происходит неявное преобразование типов:

Присваиваем один тип другому:

double d = 3; // int → double

В арифметике:

int + double → double

В аргументах функции:

void foo(double); foo(5); // int → double

Явное преобразование типов(Ручное):

Указываешь сам, во что преобразовать.

double b = (double)10;               // C-style
double b = static_cast<double>(10);  // C++-style (лучше)

Главные отличия в том, что явное ты делаешь сам, а неявное делает за тебя компилятор, но он может сделать приведение типо не так как тебе хочется -> сломается логика твеой программы. Так что с неявными приведениями нужно быть осторожнее.

53. Какие есть варианты явного приведения типов (cast) в C++ и чем отличаются?

Кратки ответ_

Все варианты приведения типов в С++ являются частями просиходящего в C - style касте и разбиваются на const_cast, static_cast, reinterpret_cast. Также начиная с С++11 существет dynamic_cast для динамеческого приведения типов по иерархии наследования "вверх, вниз и вбок".

Итак, В C++ есть 4 основных вида явного приведения типов (кастов). Каждый имеет своё назначение и уровень безопасности.

Виды кастов в C++

📋 Таблица кастов в C++

КастНазначениеБезопасностьПрименениеОтличие от других кастов
static_castСтандартные преобразования типовБезопасенint → float, базовый класс → производный (вверх по иерархии)Компилируется без runtime-проверок, используется для логичных преобразований
dynamic_castПреобразование в иерархии классов (с virtual)Проверка в runtimeТолько для указателей/ссылок с полиморфизмомПроверяет тип в runtime, безопасен для downcast в иерархии с виртуальными функциями
const_castУдаление или добавление constОсторожноУбрать const, передать в неконстантную функциюЕдинственный каст, работающий только с const/volatile
reinterpret_castПобитовое преобразование между типамиОпасенУказатели, чтение/запись памяти через другой типПозволяет «обмануть» компилятор, интерпретируя данные как другой тип (может привести к UB)

Подробные примеры кода:

static_cast

int a = 10;
double b = static_cast<double>(a); // int → double

dynamic_cast

class Base { public: virtual ~Base() {} };
class Derived : public Base {};

Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // безопасный downcast

const_cast

void print(char* msg) {}

const char* text = "hello";
print(const_cast<char*>(text)); // убираем const

reinterpret_cast

int x = 65;
char* c = reinterpret_cast<char*>(&x); // читаем int как char

54. Что такое макросы? В чем отличия от функций (для макросов в духе #define MAX(a, b))?

Кратки ответ

Макросы — это текстовые подстановки, которые обрабатываются препроцессором до компиляции. Объявляются с помощью #define. Отличаются от функций тем, что просто подставляются в месте вызова и не являются объектами.

Например:

#define MAX(a, b) ((a) > (b) ? (a) : (b))

При компиляции,MAX(a, b) заменяется во всех местах на ((a) > (b) ? (a) : (b)).

Макросы vs Функции в C++

ХарактеристикаМакрос (#define)Функция
Обрабатывается на стадииПрепроцессора (до компиляции)Компиляции
Типы данных❌ Нет проверки типов✅ Есть — зависит от сигнатуры
Отладка❌ Нельзя отлаживать (нет кода в дебаге)✅ Можно пошагово пройти
Побочные эффекты❌ Возможны (например, двойной вызов арг.)✅ Аргументы вызываются один раз
Инлайнинг✅ По сути всегда inline (подстановка текста)⚠ Может быть inline, но не обязан
Безопасность❌ Небезопасны, легко допустить ошибку✅ Безопаснее, соблюдают правила C++
Гибкость❌ Не поддерживает перегрузку, шаблоны и т.п.✅ Поддерживает перегрузку, шаблоны и ООП

Вывод: нужно быть аккуратным с использованием макросов, так как код макроса легко можно сломать из-за приоритетов (оборачивайте макрос в скобочки).