Разные языки программирования имеют разное поведение виртуальных функций, когда речь идет о конструкторах и деструкторах.
Неправильное использование виртуальных функций — классическая ошибка разработки на C++, которую мы рассмотрим в этой статье.
Теория
Я предполагаю, что читатель уже знаком с виртуальные функции на C++, поэтому перейду сразу к делу.Когда виртуальная функция вызывается в конструкторе, она работает только внутри базовых классов или текущего созданного класса.
Конструкторы в производных классах еще не вызывались, и поэтому реализованные в них виртуальные функции вызываться не будут. Сначала позвольте мне объяснить это с помощью рисунка.
Пояснения:
- Из класса А класс наследуется Б ;
- Из класса Б класс наследуется С ;
- Функции фу И бар являются виртуальными;
- Функция фу нет реализации в классе Б .
- Функция фу .
Поэтому реализация функции из класса будет называться А .
- Функция бар .
Поэтому функция, принадлежащая текущему классу, называется Б .
Если скомпилировать и запустить этот код, то он напечатает:#include <iostream> class A { public: A() { std::cout << "A()\n"; }; virtual void foo() { std::cout << "A::foo()\n"; }; virtual void bar() { std::cout << "A::bar()\n"; }; }; class B : public A { public: B() { std::cout << "B()\n"; foo(); bar(); }; void bar() { std::cout << "B::bar()\n"; }; }; class C : public B { public: C() { std::cout << "C()\n"; }; void foo() { std::cout << "C::foo()\n"; }; void bar() { std::cout << "C::bar()\n"; }; }; int main() { C x; return 0; }
A()
B()
A::foo()
B::bar()
C()
При вызове виртуальных методов в деструкторах все работает точно так же.
Казалось бы, в чем проблема? Все это описано в книгах по программированию на C++.
Проблема в том, что об этом легко забыть! И учтите, что функции фу И бар будет вызываться из крайнего наследника, т.е.
из класса С .
Вопрос «Почему код работает неожиданным образомЭ» появляется снова и снова на форумах.
Пример: Вызов виртуальных функций внутри конструкторов .
Думаю, теперь понятно, почему в таком коде легко допускать ошибки.
Особенно легко запутаться, если вам доведется программировать на других языках, где поведение иное.
Рассмотрим следующую программу C#: class Program
{
class Base
{
public Base()
{
Test();
}
protected virtual void Test()
{
Console.WriteLine("From base");
}
}
class Derived : Base
{
protected override void Test()
{
Console.WriteLine("From derived");
}
}
static void Main(string[] args)
{
var obj = new Derived();
}
}
Если вы запустите его, он напечатает: From derived
Соответствующая визуальная диаграмма:
Функция в потомке вызывается из конструктора базового класса! При вызове виртуального метода из конструктора учитывается тип времени выполнения создаваемого экземпляра.
На основе этого типа происходит виртуальный звонок.
Несмотря на то, что вызов метода происходит в конструкторе базового типа, фактический тип созданного экземпляра Полученный , что и определяет выбор метода.
Вы можете прочитать больше о виртуальных методах в спецификации .
Стоит отметить, что такое поведение также может быть чревато ошибками.
Например, проблемы могут возникнуть, если виртуальный метод работает с членами производного типа, которые еще не инициализированы в его конструкторе.
Давайте посмотрим на пример: class Base
{
public Base()
{
Test();
}
protected virtual void Test() { }
}
class Derived : Base
{
public String MyStr { get; set; }
public Derived(String myStr)
{
MyStr = myStr;
}
protected override void Test()
=> Console.WriteLine($"Length of {nameof(MyStr)}: {MyStr.Length}");
}
При попытке создать экземпляр типа Полученный произойдет исключение типа Исключение NullReferenceException , даже если значение, отличное от нулевой : новый производный («Привет») .
При выполнении тела конструктора типа База реализация метода будет называться Тест от типа Полученный .
Этот метод обращается к свойству MyStr , который в настоящее время инициализируется значением по умолчанию ( нулевой ), а не параметр, передаваемый конструктору ( мояStr ).
Мы разобрались с теорией.
Теперь я расскажу вам, почему я вообще решил написать эту статью.
Как появилась статья
Все началось с вопроса " Scan-Build для clang-13 не показывает ошибок "на сайте StackOverflow. Хотя точнее было бы сказать, что все началось с обсуждения статьи" О том, как мы с сочувствием смотрим на вопрос на StackOverflow, но молчим ".Вам не обязательно переходить по ссылкам.
Я сейчас кратко перескажу суть истории.
Человек спросил, как искать два типа ошибок с помощью статического анализа.
Первая ошибка касается переменных типа логическое значение , и сейчас нам это не интересно.
Вторая часть вопроса касалась поиска вызовов виртуальных функций в конструкторе и деструкторе.
Если убрать все, что не относится к теме, то задача состоит в том, чтобы идентифицировать вызовы виртуальных функций в этом коде: class M {
public:
virtual int GetAge(){ return 0; }
};
class P : public M {
public:
virtual int GetAge() { return 1; }
P() { GetAge(); } // maybe warn
~P() { GetAge(); } // maybe warn
};
И вдруг оказалось, что не все понимают, чем опасен такой код и почему статические анализаторы кода предупреждают о вызове виртуальных методов в конструкторах/деструкторах.
Появился для публикации на сайте хабра комментарии (RU) следующей формы: Краткий комментарий N1. Значит компилятор прав, ошибки нет. Ошибка только в логике программиста; его пример кода всегда будет возвращать один в первом случае.
И он мог бы даже использовать встроенный код для ускорения кода конструктора и деструктора.
Но для компилятора это всё равно не имеет значения, либо результат функции нигде не используется, функция не использует никаких внешних аргументов — компилятор просто выкинет пример как оптимизацию.
И это логично и правильно.
В результате ошибки просто нет. Краткий комментарий N2. Я вообще не понял вашего юмора по поводу виртуальных функций.
[цитата из книги о виртуальных функциях].
Автор подчеркивает, что ключевое слово virtual используется только один раз.
Далее в книге объясняется, что это передается по наследству.
А теперь, студенты, ответьте на мой вопрос: «Где вы увидели проблему вызова виртуальной функции в конструкторе и деструкторе класса? Дайте ответ отдельно по каждому случаю».
Подразумевается, что вы оба, как недобросовестные ученики, не понимаете вопроса, когда вызывается конструктор и деструктор класса.
И кроме того, они совершенно упустили тему «В каком порядке определяются объекты родительских классов при определении предка и в каком порядке они уничтожаются».
Возможно, прочитав эти комментарии, вы задаетесь вопросом, какое все это имеет отношение к обсуждаемой ранее теме.
Это правильно, что вы в недоумении.
Ответ: вообще не актуально.
Человек, оставивший комментарии, просто понятия не имеет, от какой проблемы хочет защититься человек, задавший вопрос на StackOverflow. Да, стоит признать, что вопрос можно было бы сформулировать лучше.
На самом деле в приведенном выше коде проблемы как таковой нет. Еще нет. Она появится в будущем, когда у классов появятся новые потомки, реализующие функцию GetAge кто что-то делает. Если пример содержал другой класс, унаследовавший п , тогда вопрос стал бы более полным.
Однако любой, кто хорошо знает C++, сразу поймет, о какой проблеме идет речь и зачем нужно искать вызовы функций.
Запрет на вызов виртуальных функций в конструкторах/деструкторах также отражен в стандартах кодирования.
Например, в стандарте кодирования SEI CERT C++ есть правило: ООП50-ЦПП.
Не вызывайте виртуальные функции из конструкторов или деструкторов.
.
Это диагностическое правило реализуется многими анализаторами кода, такими как Parasoft C/C++test, Polyspace Bug Finder, PRQA QA-C++, плагин SonarQube C/C++.
К ним относится PVS-Studio, разработанный нашей командой (диагностика В1053 ).
Что делать, если ошибки нет?
Мы не рассмотрели ситуацию, когда ошибки нет. Другими словами, все работает именно так, как задумано.
В этом случае мы можем явно указать, какие функции планируем вызывать: B() {
std::cout << "B()\n";
A::foo();
B::bar();
};
Такой код обязательно будет правильно понят вашими коллегами.
Статические анализаторы, в свою очередь, тоже все поймут и промолчат.
Заключение
Оцените статический анализ кода.Это поможет выявить потенциальные проблемы в коде, о которых вы и ваши коллеги, возможно, даже не догадываетесь.
Несколько примеров:
- В718 .
Функцию «Foo» не следует вызывать из функции «DllMain».
- В1032 .
Указатель приводится к более строго выровненному типу указателя.
- В1036 .
Потенциально небезопасная блокировка с двойной проверкой.
Однако, как показывают комментарии и вопросы на StackOverflow, эта тема заслуживает внимания и контроля.
Если бы все было очевидно, этой статьи не было бы.
Хорошо, что анализаторы кода способны поддержать программиста в его работе.
Спасибо за внимание и приходите пытаться Анализатор PVS-Studio. Если вы хотите поделиться этой статьей с англоязычной аудиторией, воспользуйтесь ссылкой для перевода: Андрей Карпов.
Вызовы виртуальных функций в конструкторах и деструкторах (C++) .
Теги: #программирование #C++ #c #статический анализ кода #конструктор #cpp #виртуальные функции
-
Программное Обеспечение Dmx Light Player
19 Oct, 24 -
Концепция Безбумажного Офиса
19 Oct, 24 -
Лоран, Огюст
19 Oct, 24 -
Кодигнитер 1.6
19 Oct, 24 -
Iot-Практикум От Microsoft И Мтс
19 Oct, 24