#319_Cpp_IF
ОБЯЗАТЕЛЬНО К ПРОЧТЕНИЮ!!!
Интервью Бьярне Страуструпа в котором он поделился как и для чего был придуман С++.
1 января 1998 года Бьёрн Страуструп дал интервью журналу IEEE Computer. Естественно, редакторы ожидали от него ретроспективный обзор семи лет объектно-ориентированного проектирования, использующего созданный Страуструпом язык.
К концу интервью они получили даже больше, чем рассчитывали, и впоследствии решили скрыть его содержание «на благо отрасли». Но, как и бывает в таких случаях, произошла утечка.
Вот полный текст того, что было сказано во время интервью. Оно не отредактировано и не было подготовлено заранее, поэтому не такое складное, какими бывают запланированные интервью.
— Каково это — быть тем, кто изменил мир проектирования программного обеспечения? Прошло уже несколько лет с тех пор. Что можете сказать, оглядываясь назад?
— Вообще-то перед самым интервью я думал о том времени. Помните, все писали на С? Проблема была в том, что они чертовски хорошо это делали. И в университетах тоже очень хорошо этому учили. Настолько хорошо, что там с феноменальной скоростью подготавливали компетентных — я подчёркиваю, "компетентных" — выпускников. Это и привело к проблеме.
— К проблеме?
— Да, к проблеме. Помните, все писали на COBOL?
— Конечно. И я тоже.
— Вначале эти парни были как полубоги. У них была высокая зарплата, и с ними обращались как с членами королевской семьи.
— Вот было время!
— Да. Но что произошло? IBM это надоело, и они стали вкладывать миллионы в обучение программистов, пока тех не стало как собак нерезаных.
— Я поэтому и ушёл. Зарплата за год упала так, что выгоднее стало работать журналистом.
— Точно. То же самое произошло с программистами на С.
— Да? Ну и что?
— Однажды мне в голову пришла небольшая схема, которая немного восстановила бы равновесие. Я подумал: «Интересно: а что, если бы существовал язык настолько сложный, настолько трудный для изучения, что никто и никогда не смог бы наводнить рынок программистами?» На самом деле некоторые идеи я взял из X10, т. е. X-Windows. Это была такая плохая графическая система, что работала только на этих штуках Sun 3/60! Там были все нужные мне ингредиенты: смехотворно сложный синтаксис, непонятные функции и псевдообъектно-ориентированная структура. Даже сейчас никто не пишет сырой код на X-Windows. Почему? Это единственный путь, если вы хотите сохранить душевное здоровье.
— Вы шутите?
— Нисколько. На самом деле была ещё одна проблема. Unix писался на С, то есть любой программист на С очень легко мог стать системным программистом. Помните, сколько зарабатывал программист мейнфрейм-систем?
— Ну ещё бы! Я же им работал.
— Так вот этот новый язык должен был отделиться от Unix, скрыв все системные вызовы, которые так хорошо связывали их вместе. Это позволило бы неплохо зарабатывать и тем парням, которые знают только DOS.
— Не верю, что вы это произнесли…
— Прошло ведь уже достаточно времени. Думаю, большинство людей сами поняли, что C++ — это пустая трата времени. Только пришли они к этому намного позже, чем я надеялся.
— И как именно вы это сделали?
— Это должно было быть всего лишь шуткой. Никогда не думал, что люди воспримут книгу всерьёз. Любой, у кого ещё есть мозги, понимает: объектно-ориентированное программирование не является интуитивно понятным, оно нелогично и неэффективно.
— Что?
— А повторно используемый код? Вы когда-нибудь слышали, что какая-нибудь компания повторно использует свой код?
— Никогда не слышал, но…
— Вот видите. Правда, некоторые пытались на первых порах. Была эта компания из Орегона (кажется, Mentor Graphics), которая сильно простудилась, пытаясь переписать всё на C++ в 1990 или 1991 году. Мне, правда, было очень жаль их, но я думал, что они будут учиться на своих ошибках.
— Очевидно, ошибки их не научили?
— Нисколько. Проблема в том, что большинство компаний замалчивают все свои основные грубые ошибки, ведь нелегко объяснить акционерам убытки в 30 миллионов долларов. Однако, отдадим им должное, в конце концов у них всё получилось.
ОБЯЗАТЕЛЬНО К ПРОЧТЕНИЮ!!!
Интервью Бьярне Страуструпа в котором он поделился как и для чего был придуман С++.
1 января 1998 года Бьёрн Страуструп дал интервью журналу IEEE Computer. Естественно, редакторы ожидали от него ретроспективный обзор семи лет объектно-ориентированного проектирования, использующего созданный Страуструпом язык.
К концу интервью они получили даже больше, чем рассчитывали, и впоследствии решили скрыть его содержание «на благо отрасли». Но, как и бывает в таких случаях, произошла утечка.
Вот полный текст того, что было сказано во время интервью. Оно не отредактировано и не было подготовлено заранее, поэтому не такое складное, какими бывают запланированные интервью.
— Каково это — быть тем, кто изменил мир проектирования программного обеспечения? Прошло уже несколько лет с тех пор. Что можете сказать, оглядываясь назад?
— Вообще-то перед самым интервью я думал о том времени. Помните, все писали на С? Проблема была в том, что они чертовски хорошо это делали. И в университетах тоже очень хорошо этому учили. Настолько хорошо, что там с феноменальной скоростью подготавливали компетентных — я подчёркиваю, "компетентных" — выпускников. Это и привело к проблеме.
— К проблеме?
— Да, к проблеме. Помните, все писали на COBOL?
— Конечно. И я тоже.
— Вначале эти парни были как полубоги. У них была высокая зарплата, и с ними обращались как с членами королевской семьи.
— Вот было время!
— Да. Но что произошло? IBM это надоело, и они стали вкладывать миллионы в обучение программистов, пока тех не стало как собак нерезаных.
— Я поэтому и ушёл. Зарплата за год упала так, что выгоднее стало работать журналистом.
— Точно. То же самое произошло с программистами на С.
— Да? Ну и что?
— Однажды мне в голову пришла небольшая схема, которая немного восстановила бы равновесие. Я подумал: «Интересно: а что, если бы существовал язык настолько сложный, настолько трудный для изучения, что никто и никогда не смог бы наводнить рынок программистами?» На самом деле некоторые идеи я взял из X10, т. е. X-Windows. Это была такая плохая графическая система, что работала только на этих штуках Sun 3/60! Там были все нужные мне ингредиенты: смехотворно сложный синтаксис, непонятные функции и псевдообъектно-ориентированная структура. Даже сейчас никто не пишет сырой код на X-Windows. Почему? Это единственный путь, если вы хотите сохранить душевное здоровье.
— Вы шутите?
— Нисколько. На самом деле была ещё одна проблема. Unix писался на С, то есть любой программист на С очень легко мог стать системным программистом. Помните, сколько зарабатывал программист мейнфрейм-систем?
— Ну ещё бы! Я же им работал.
— Так вот этот новый язык должен был отделиться от Unix, скрыв все системные вызовы, которые так хорошо связывали их вместе. Это позволило бы неплохо зарабатывать и тем парням, которые знают только DOS.
— Не верю, что вы это произнесли…
— Прошло ведь уже достаточно времени. Думаю, большинство людей сами поняли, что C++ — это пустая трата времени. Только пришли они к этому намного позже, чем я надеялся.
— И как именно вы это сделали?
— Это должно было быть всего лишь шуткой. Никогда не думал, что люди воспримут книгу всерьёз. Любой, у кого ещё есть мозги, понимает: объектно-ориентированное программирование не является интуитивно понятным, оно нелогично и неэффективно.
— Что?
— А повторно используемый код? Вы когда-нибудь слышали, что какая-нибудь компания повторно использует свой код?
— Никогда не слышал, но…
— Вот видите. Правда, некоторые пытались на первых порах. Была эта компания из Орегона (кажется, Mentor Graphics), которая сильно простудилась, пытаясь переписать всё на C++ в 1990 или 1991 году. Мне, правда, было очень жаль их, но я думал, что они будут учиться на своих ошибках.
— Очевидно, ошибки их не научили?
— Нисколько. Проблема в том, что большинство компаний замалчивают все свои основные грубые ошибки, ведь нелегко объяснить акционерам убытки в 30 миллионов долларов. Однако, отдадим им должное, в конце концов у них всё получилось.
— Получилось? Вот видите: это доказывает, что объектно-ориентированное программирование работает.
— Ну, почти. Исполняемый файл был настолько огромным, что для загрузки на рабочую станцию HP со 128 Мб оперативной памяти потребовалось пять минут. А затем он очень медленно выполнялся. Я думал, это станет большой проблемой и за мной придут в течение недели, но никого это не волновало. В Sun и HP были только рады продать невероятно мощные коробки с огромными ресурсами, годившимися лишь для запуска простейших программ. Знаете, когда у нас в AT&T появился первый компилятор C++, я скомпилировал «Hello World» и не мог поверить в то, что размер исполняемого файла был 2,1 Мб.
— Да? Но с тех пор компиляторы далеко продвинулись.
— Неужели? Попробуйте последнюю версию g++ — вы не получите больших изменений от половины мегабайта. Кроме того, есть несколько совсем недавних примеров со всего мира. У British Telecom была крупная авария, к счастью, им удалось выбросить всё это и начать сначала. Им повезло больше, чем Australian Telecom. Теперь я слышу, что Siemens создаёт динозавра, и мне всё тревожнее, ведь размер аппаратного обеспечения под исполняемые файлы растёт. Что уж говорить о множественном наследовании.
— Да, но C++ — в принципе надёжный язык.
— Вы правда в это верите? Вы когда-нибудь работали над проектом на C++? Вот что происходит: во-первых, я заложил достаточно подводных камней, чтобы только самые простые проекты работали с первого раза. Возьмите перегрузку операторов. В конце проекта она есть почти каждом модуле, потому что ребятам кажется, что так и должно быть, ведь так было в их учебном курсе. Один и тот же оператор значит что-то совершенно другое в каждом модуле. Попробуйте собрать всё это вместе, когда у вас примерно сотня модулей. А сокрытие данных… Боже мой, иногда не могу удержаться от смеха, когда слышу о проблемах компаний, заставляющих свои модули разговаривать друг с другом. Думаю, слово «синергетический» специально придумали, чтобы добавить мучений руководителю проекта.
— Должен сказать, всё это весьма шокирует. Вы говорите, что сделали это, чтобы повысить зарплату программистам? Это отвратительно.
— Ну что Вы?! У каждого есть выбор. Я не ожидал, что всё так выйдет из-под контроля. В любом случае я, в принципе, добился своего: C++ умирает, но ведь программисты получают высокие зарплаты. Особенно те бедолаги, которым приходится поддерживать всю эту околесицу. Понятно же, что невозможно поддерживать большой программный модуль на C++, если вы его не писали?
— Как это?
— А Вы не знаете? Помните typedef?
— Да, конечно.
— Помните, сколько времени уходило на прощупывание заголовочных файлов, а потом обнаруживалось, что RoofRaised — это число двойной точности? А представьте, сколько времени требуется, чтобы найти все неявно определённые типы во всех классах в крупном проекте.
— И как вы поняли, что достигли своего?
— Помните продолжительность среднего по размеру проекта на С? Около полугода. Недостаточно долго, чтобы обеспечить жене и детям достойный уровень жизни. А возьмите тот же проект и разрабатывайте его на C++. Что вы получите? Я Вам скажу. Один-два года. Разве не здорово? И это гарантированное рабочее место — всего лишь из-за одной ошибки в суждениях. И ещё кое-что. В университетах так давно не преподавали С, что сейчас не хватает приличных программистов на С. Особенно тех, кто что-нибудь знает о программировании систем Unix. Многие ли знают, что делать с malloc, когда все эти годы они использовали new и никогда не удосуживались проверить код возврата? На самом деле большинство программистов на C++ выбрасывают код возврата. А что случилось со старым добрым –1? По крайней мере вы знали, что у вас ошибка, не увязнув во всех этих throw, catch и try.
— Ну, почти. Исполняемый файл был настолько огромным, что для загрузки на рабочую станцию HP со 128 Мб оперативной памяти потребовалось пять минут. А затем он очень медленно выполнялся. Я думал, это станет большой проблемой и за мной придут в течение недели, но никого это не волновало. В Sun и HP были только рады продать невероятно мощные коробки с огромными ресурсами, годившимися лишь для запуска простейших программ. Знаете, когда у нас в AT&T появился первый компилятор C++, я скомпилировал «Hello World» и не мог поверить в то, что размер исполняемого файла был 2,1 Мб.
— Да? Но с тех пор компиляторы далеко продвинулись.
— Неужели? Попробуйте последнюю версию g++ — вы не получите больших изменений от половины мегабайта. Кроме того, есть несколько совсем недавних примеров со всего мира. У British Telecom была крупная авария, к счастью, им удалось выбросить всё это и начать сначала. Им повезло больше, чем Australian Telecom. Теперь я слышу, что Siemens создаёт динозавра, и мне всё тревожнее, ведь размер аппаратного обеспечения под исполняемые файлы растёт. Что уж говорить о множественном наследовании.
— Да, но C++ — в принципе надёжный язык.
— Вы правда в это верите? Вы когда-нибудь работали над проектом на C++? Вот что происходит: во-первых, я заложил достаточно подводных камней, чтобы только самые простые проекты работали с первого раза. Возьмите перегрузку операторов. В конце проекта она есть почти каждом модуле, потому что ребятам кажется, что так и должно быть, ведь так было в их учебном курсе. Один и тот же оператор значит что-то совершенно другое в каждом модуле. Попробуйте собрать всё это вместе, когда у вас примерно сотня модулей. А сокрытие данных… Боже мой, иногда не могу удержаться от смеха, когда слышу о проблемах компаний, заставляющих свои модули разговаривать друг с другом. Думаю, слово «синергетический» специально придумали, чтобы добавить мучений руководителю проекта.
— Должен сказать, всё это весьма шокирует. Вы говорите, что сделали это, чтобы повысить зарплату программистам? Это отвратительно.
— Ну что Вы?! У каждого есть выбор. Я не ожидал, что всё так выйдет из-под контроля. В любом случае я, в принципе, добился своего: C++ умирает, но ведь программисты получают высокие зарплаты. Особенно те бедолаги, которым приходится поддерживать всю эту околесицу. Понятно же, что невозможно поддерживать большой программный модуль на C++, если вы его не писали?
— Как это?
— А Вы не знаете? Помните typedef?
— Да, конечно.
— Помните, сколько времени уходило на прощупывание заголовочных файлов, а потом обнаруживалось, что RoofRaised — это число двойной точности? А представьте, сколько времени требуется, чтобы найти все неявно определённые типы во всех классах в крупном проекте.
— И как вы поняли, что достигли своего?
— Помните продолжительность среднего по размеру проекта на С? Около полугода. Недостаточно долго, чтобы обеспечить жене и детям достойный уровень жизни. А возьмите тот же проект и разрабатывайте его на C++. Что вы получите? Я Вам скажу. Один-два года. Разве не здорово? И это гарантированное рабочее место — всего лишь из-за одной ошибки в суждениях. И ещё кое-что. В университетах так давно не преподавали С, что сейчас не хватает приличных программистов на С. Особенно тех, кто что-нибудь знает о программировании систем Unix. Многие ли знают, что делать с malloc, когда все эти годы они использовали new и никогда не удосуживались проверить код возврата? На самом деле большинство программистов на C++ выбрасывают код возврата. А что случилось со старым добрым –1? По крайней мере вы знали, что у вас ошибка, не увязнув во всех этих throw, catch и try.
— Но ведь наследование помогает сэкономить много времени.
— Действительно? Вы когда-нибудь замечали разницу между планом проекта на C и на C++? Стадия планирования проекта на C++ в три раза длиннее именно для того, чтобы то, что должно наследоваться, наследовалось, а что не должно — нет. И потом, его до сих пор неправильно понимают. Кто-нибудь слышал об утечках памяти в программе на С? Теперь их поиск — целая индустрия. Большинство компаний сдаются и отправляют продукт на сторону (зная, что он протекает, как решето), просто чтобы избежать затрат на отслеживание утечек.
— Есть инструменты…
— Большинство из которых написаны на C++.
— Если мы опубликуем это, вас могут линчевать, вы это понимаете?
— Сомневаюсь. Как я уже сказал, C++ давно прошёл свой пик, и ни в одной компании, будучи в здравом уме, не начнут проект на C++ без пилотного испытания. Оно должно убедить их, что это путь к катастрофе. Если нет, то они заслуживают всего того, что получат. Знаете, я пытался убедить Денниса Ричи переписать Unix на C++.
— Боже мой! Что он сказал?
— К счастью, у него хорошее чувство юмора. Думаю, что и он, и Брайан понимали тогда, что я делаю, просто не подавали виду. Он сказал, что поможет мне написать версию DOS на C++, если мне будет интересно.
— И что Вы?
Страуструп: Вообще-то я написал DOS на C++, я дам вам демоверсию по окончании интервью. У меня она запускается на Sparc 20 в компьютерном зале. На четырёх ядрах работает, как ракета, и занимает всего 70 Мб на диске.
— А как на компьютере?
— А теперь Вы шутите. Вы что, никогда не видели Windows 95? Я считаю это своим самым большим успехом: она произвела эффект разорвавшейся бомбы прежде, чем я был к этому готов.
— Вы знаете, эта идея с Unix++ заставила меня задуматься. Кто-то ведь попробует это сделать.
Страуструп: Да, но не после прочтения этого интервью.
— Простите, но я не думаю, что мы опубликуем что-либо из этого.
— Но это история века. Я лишь хочу, чтобы мои коллеги-программисты запомнили меня за то, что я для них сделал. Вы же знаете, сколько сейчас можно получать на C++?
— Насколько я знаю, лучшие получают 70–80 долларов в час.
— Видите? И он этого полностью заслуживает. Нелёгкая работа — отслеживать все те подводные камни, которые я заложил в C++. И, как я уже говорил, каждый программист на C++ чувствует себя связанным каким-то мистическим обещанием использовать каждый элемент языка в каждом проекте. На самом деле меня это иногда очень раздражает. Спустя столько лет мне почти нравится этот язык.
— То есть раньше он Вам не нравился?
— Я его ненавидел. Он даже выглядит неуклюже, не находите? Но когда начали поступать гонорары от книги… Ну, Вы понимаете.
— Минуточку. А как насчёт ссылок? Вы должны признать, что улучшили указатели "С".
— Хм… Я всегда задавался этим вопросом. Сначала я думал, что улучшил. Но однажды я обсуждал это кое с кем, кто с самого начала писал на C++. Он сказал, что не запоминает, были ли его переменные ссылочными или разыменованными, поэтому всегда использует указатели. Он сказал, что маленькая звёздочка всегда напоминала ему.
— На этом месте я обычно говорю «большое спасибо», но сейчас это вряд ли уместно.
— Обещайте, что опубликуете это. Совесть не даёт мне покоя.
— Я дам вам знать. Но, кажется, знаю, что скажет мой редактор.
— Да и кто в это поверит? Хотя… пришлите мне копию этой записи.
— Это можно.
— Действительно? Вы когда-нибудь замечали разницу между планом проекта на C и на C++? Стадия планирования проекта на C++ в три раза длиннее именно для того, чтобы то, что должно наследоваться, наследовалось, а что не должно — нет. И потом, его до сих пор неправильно понимают. Кто-нибудь слышал об утечках памяти в программе на С? Теперь их поиск — целая индустрия. Большинство компаний сдаются и отправляют продукт на сторону (зная, что он протекает, как решето), просто чтобы избежать затрат на отслеживание утечек.
— Есть инструменты…
— Большинство из которых написаны на C++.
— Если мы опубликуем это, вас могут линчевать, вы это понимаете?
— Сомневаюсь. Как я уже сказал, C++ давно прошёл свой пик, и ни в одной компании, будучи в здравом уме, не начнут проект на C++ без пилотного испытания. Оно должно убедить их, что это путь к катастрофе. Если нет, то они заслуживают всего того, что получат. Знаете, я пытался убедить Денниса Ричи переписать Unix на C++.
— Боже мой! Что он сказал?
— К счастью, у него хорошее чувство юмора. Думаю, что и он, и Брайан понимали тогда, что я делаю, просто не подавали виду. Он сказал, что поможет мне написать версию DOS на C++, если мне будет интересно.
— И что Вы?
Страуструп: Вообще-то я написал DOS на C++, я дам вам демоверсию по окончании интервью. У меня она запускается на Sparc 20 в компьютерном зале. На четырёх ядрах работает, как ракета, и занимает всего 70 Мб на диске.
— А как на компьютере?
— А теперь Вы шутите. Вы что, никогда не видели Windows 95? Я считаю это своим самым большим успехом: она произвела эффект разорвавшейся бомбы прежде, чем я был к этому готов.
— Вы знаете, эта идея с Unix++ заставила меня задуматься. Кто-то ведь попробует это сделать.
Страуструп: Да, но не после прочтения этого интервью.
— Простите, но я не думаю, что мы опубликуем что-либо из этого.
— Но это история века. Я лишь хочу, чтобы мои коллеги-программисты запомнили меня за то, что я для них сделал. Вы же знаете, сколько сейчас можно получать на C++?
— Насколько я знаю, лучшие получают 70–80 долларов в час.
— Видите? И он этого полностью заслуживает. Нелёгкая работа — отслеживать все те подводные камни, которые я заложил в C++. И, как я уже говорил, каждый программист на C++ чувствует себя связанным каким-то мистическим обещанием использовать каждый элемент языка в каждом проекте. На самом деле меня это иногда очень раздражает. Спустя столько лет мне почти нравится этот язык.
— То есть раньше он Вам не нравился?
— Я его ненавидел. Он даже выглядит неуклюже, не находите? Но когда начали поступать гонорары от книги… Ну, Вы понимаете.
— Минуточку. А как насчёт ссылок? Вы должны признать, что улучшили указатели "С".
— Хм… Я всегда задавался этим вопросом. Сначала я думал, что улучшил. Но однажды я обсуждал это кое с кем, кто с самого начала писал на C++. Он сказал, что не запоминает, были ли его переменные ссылочными или разыменованными, поэтому всегда использует указатели. Он сказал, что маленькая звёздочка всегда напоминала ему.
— На этом месте я обычно говорю «большое спасибо», но сейчас это вряд ли уместно.
— Обещайте, что опубликуете это. Совесть не даёт мне покоя.
— Я дам вам знать. Но, кажется, знаю, что скажет мой редактор.
— Да и кто в это поверит? Хотя… пришлите мне копию этой записи.
— Это можно.
#320_Cpp_PPPO
Что такое синглтон Мейерса?
Синглтон Мейерса — одна из реализаций паттерна "синглтон" в C++, предложенная Скоттом Мейерсом.
Этот подход позволяет создавать безопасный и потокобезопасный синглтоны без использования блокировок при многопоточном доступе.
Основная идея заключается в том, чтобы использовать статическую переменную внутри функции, которая будет инициализирована только один раз при первом вызове этой функции.
В стандарте C++11 и выше такая реализация автоматически становится потокобезопасной благодаря гарантии стандарта о том, что локальные статические переменные инициализируются только один раз и безопасно даже в многопоточной среде.
Пример реализации синглтона Мейерса выглядит так:
Основные особенности:
Инициализация по требованию — объект создается только тогда, когда он впервые запрашивается через метод getInstance.
Потокобезопасность — благодаря гарантиям стандарта C++11 и новее, локальные статические переменные инициализируются атомарно и безопасно в многопоточных средах.
Ограничение доступа — конструкторы и деструкторы класса объявлены как приватные, а операции копирования и присваивания запрещены, что предотвращает создание нескольких экземпляров этого класса.
Таким образом, этот способ является эффективным и безопасным способом реализации паттерна "синглтон" в современных версиях C++.
Что такое синглтон Мейерса?
Синглтон Мейерса — одна из реализаций паттерна "синглтон" в C++, предложенная Скоттом Мейерсом.
Этот подход позволяет создавать безопасный и потокобезопасный синглтоны без использования блокировок при многопоточном доступе.
Основная идея заключается в том, чтобы использовать статическую переменную внутри функции, которая будет инициализирована только один раз при первом вызове этой функции.
В стандарте C++11 и выше такая реализация автоматически становится потокобезопасной благодаря гарантии стандарта о том, что локальные статические переменные инициализируются только один раз и безопасно даже в многопоточной среде.
Пример реализации синглтона Мейерса выглядит так:
class Singleton {
public:
static Singleton& getInstance() {
/* Локальная статическая переменная инициализируется только один раз */
static Singleton instance;
return instance;
}
private:
// Конструктор приватен
Singleton() {}
// Деструктор также приватен
~Singleton() {}
// Запрещаем копирование
Singleton(const Singleton&) = delete;
// Запрещаем присваивание
void operator=(const Singleton&) = delete;
};Основные особенности:
Инициализация по требованию — объект создается только тогда, когда он впервые запрашивается через метод getInstance.
Потокобезопасность — благодаря гарантиям стандарта C++11 и новее, локальные статические переменные инициализируются атомарно и безопасно в многопоточных средах.
Ограничение доступа — конструкторы и деструкторы класса объявлены как приватные, а операции копирования и присваивания запрещены, что предотвращает создание нескольких экземпляров этого класса.
Таким образом, этот способ является эффективным и безопасным способом реализации паттерна "синглтон" в современных версиях C++.
#321_Cpp_PkS
В каких случаях не будет сгенерирован конструктор копирования?
Конструктор копирования в C++ может не быть сгенерированным компилятором в следующих случаях:
Если явно объявлен собственный конструктор копирования — если программист определил конструктор копирования, то компилятор не создаст его автоматически:
Если класс содержит член, который не имеет конструктора копирования — если какой-либо член вашего класса сам не поддерживает копирование (например, если у него нет конструктора копирования), то компилятор не сможет создать конструктор копирования для всего класса:
Если класс наследует от базового класса, который не имеет конструктора копирования — если базовый класс не предоставляет конструктор копирования, то и производный класс не получит его автоматически:
Если класс использует std::unique_ptr или другой ресурс, который не поддерживает копирование — std::unique_ptr не имеет конструктора копирования, поэтому если ваш класс содержит такой указатель, то компилятор не сможет сгенерировать конструктор копирования для всего класса:
Если конструктор копирования был удалён (=delete) — если явно запретить генерацию конструктора копирования, используя ключевое слово delete:
Если класс содержит члены, которые используют ссылочные типы — компилятор не может генерировать конструктор копирования для классов, содержащих ссылки, потому что они должны быть инициализированы во время конструирования и не могут быть изменены позже:
Эти случаи охватывают основные ситуации, когда компилятор не генерирует конструктор копирования.
В каких случаях не будет сгенерирован конструктор копирования?
Конструктор копирования в C++ может не быть сгенерированным компилятором в следующих случаях:
Если явно объявлен собственный конструктор копирования — если программист определил конструктор копирования, то компилятор не создаст его автоматически:
class MyClass {
public:
MyClass(const MyClass &other) { /* ваш код */ }
// Другой код...
};Если класс содержит член, который не имеет конструктора копирования — если какой-либо член вашего класса сам не поддерживает копирование (например, если у него нет конструктора копирования), то компилятор не сможет создать конструктор копирования для всего класса:
struct NoCopy {
NoCopy(const NoCopy&) = delete;
};
class MyClass {
private:
NoCopy member;
};Если класс наследует от базового класса, который не имеет конструктора копирования — если базовый класс не предоставляет конструктор копирования, то и производный класс не получит его автоматически:
class Base {
protected:
Base(const Base&) = delete;
};
class Derived : public Base {};Если класс использует std::unique_ptr или другой ресурс, который не поддерживает копирование — std::unique_ptr не имеет конструктора копирования, поэтому если ваш класс содержит такой указатель, то компилятор не сможет сгенерировать конструктор копирования для всего класса:
#include <memory>
class MyClass {
private:
std::unique_ptr<int> ptr;
};
Если конструктор копирования был удалён (=delete) — если явно запретить генерацию конструктора копирования, используя ключевое слово delete:
class MyClass {
public:
MyClass(const MyClass&) = delete;
};Если класс содержит члены, которые используют ссылочные типы — компилятор не может генерировать конструктор копирования для классов, содержащих ссылки, потому что они должны быть инициализированы во время конструирования и не могут быть изменены позже:
class MyClass {
private:
int &ref;
};
Эти случаи охватывают основные ситуации, когда компилятор не генерирует конструктор копирования.
#322_Cpp_PkS
Чем отличается конструктор копирования от оператора присваивания?
Конструктор копирования и оператор присваивания в C++ выполняют схожие задачи, но между ними есть важные различия;
1. Синтаксис вызова.
Конструктор копирования — вызывается при создании нового объекта на основе уже существующего, обычно используется в следующих ситуациях:
— при передаче объекта в функцию по значению;
— при возврате объекта из функции по значению;
— при инициализации одного объекта другим.
Пример:
Оператор присваивания — вызывается, когда нужно присвоить значение одного объекта другому объекту того же типа.
Обычно это происходит после того, как оба объекта уже были созданы:
2. Семантическое различие.
Конструктор копирования — cоздает новый объект на основе существующего, что означает, что память под новый объект выделяется заново, и затем данные копируются из старого объекта в новый.
Оператор присваивания — изменяет состояние уже существующего объекта. То есть объект, которому производится присваивание, уже существует, и его содержимое заменяется содержимым другого объекта.
3. Реализация.
Конструктор копирования имеет следующую сигнатуру:
Здесь other — ссылка на исходный объект, который используется для копирования данных.
Оператор присваивания имеет следующую сигнатуру:
Здесь this указывает на текущий объект, которому выполняется присваивание, а other — объект, чье состояние копируется.
4. Управление памятью.
Конструктор копирования — обычно занимается выделением памяти для новых объектов и копированием данных. Он не должен заботиться об освобождении старой памяти, поскольку она еще не была выделена.
Оператор присваивания — может потребовать освобождения старой памяти перед копированием новой информации.
Например, если объект управляет динамической памятью, необходимо сначала освободить ранее выделенное место, прежде чем выделить новое.
5. Возвращаемое значение.
Конструктор копирования — не возвращает ничего, так как его цель — просто создать новый объект.
Оператор присваивания — возвращает ссылку на объект, которому было выполнено присваивание, что позволяет выполнять цепочки присваиваний:
Пример конструктора копирования:
Чем отличается конструктор копирования от оператора присваивания?
Конструктор копирования и оператор присваивания в C++ выполняют схожие задачи, но между ними есть важные различия;
1. Синтаксис вызова.
Конструктор копирования — вызывается при создании нового объекта на основе уже существующего, обычно используется в следующих ситуациях:
— при передаче объекта в функцию по значению;
— при возврате объекта из функции по значению;
— при инициализации одного объекта другим.
Пример:
// Обычный конструктор
MyClass obj1;
// Вызов конструктора копирования
MyClass obj2(obj1);
/* Также вызов конструктора копирования */
MyClass obj3 = obj1;
Оператор присваивания — вызывается, когда нужно присвоить значение одного объекта другому объекту того же типа.
Обычно это происходит после того, как оба объекта уже были созданы:
MyClass obj1;
MyClass obj2;
// Вызов оператора присваивания
obj2 = obj1;
2. Семантическое различие.
Конструктор копирования — cоздает новый объект на основе существующего, что означает, что память под новый объект выделяется заново, и затем данные копируются из старого объекта в новый.
Оператор присваивания — изменяет состояние уже существующего объекта. То есть объект, которому производится присваивание, уже существует, и его содержимое заменяется содержимым другого объекта.
3. Реализация.
Конструктор копирования имеет следующую сигнатуру:
MyClass(const MyClass& other);
Здесь other — ссылка на исходный объект, который используется для копирования данных.
Оператор присваивания имеет следующую сигнатуру:
MyClass& operator=(const MyClass& other);
Здесь this указывает на текущий объект, которому выполняется присваивание, а other — объект, чье состояние копируется.
4. Управление памятью.
Конструктор копирования — обычно занимается выделением памяти для новых объектов и копированием данных. Он не должен заботиться об освобождении старой памяти, поскольку она еще не была выделена.
Оператор присваивания — может потребовать освобождения старой памяти перед копированием новой информации.
Например, если объект управляет динамической памятью, необходимо сначала освободить ранее выделенное место, прежде чем выделить новое.
5. Возвращаемое значение.
Конструктор копирования — не возвращает ничего, так как его цель — просто создать новый объект.
Оператор присваивания — возвращает ссылку на объект, которому было выполнено присваивание, что позволяет выполнять цепочки присваиваний:
obj1 = obj2 = obj3;
Пример конструктора копирования:
#include <iostream>
class MyClass {
private:
int* data;
public:
MyClass(int value) {
data = new int(value);
std::cout << "Constructor called\n";
}
// Конструктор копирования
MyClass(const MyClass& other) {
data = new int(*other.data);
std::cout << "Copy constructor called\n";
}
~MyClass() {
delete data;
std::cout << "Destructor called\n";
}
friend std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
return os << *obj.data;
}
};
int main() {
MyClass obj1(10);
// Вызов конструктора копирования
MyClass obj2(obj1);
std::cout << "obj2: " << obj2 << "\n";
}
Пример оператора присваивания:
Основное отличие между конструктором копирования и оператором присваивания заключается в том, что первый создает новый объект на основе существующего, а второй изменяет состояние уже созданного объекта.
#include <iostream>
class MyClass {
private:
int* data;
public:
MyClass(int value) {
data = new int(value);
std::cout << "Constructor called\n";
}
// Конструктор копирования
MyClass(const MyClass& other) {
data = new int(*other.data);
std::cout << "Copy constructor called\n";
}
// Оператор присваивания
MyClass& operator=(const MyClass& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
std::cout << "Assignment operator called\n";
}
return *this;
}
~MyClass() {
delete data;
std::cout << "Destructor called\n";
}
friend std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
return os << *obj.data;
}
};
int main() {
MyClass obj1(10);
MyClass obj2(20);
// Вызов оператора присваивания
obj2 = obj1;
std::cout << "obj2: " << obj2 << "\n";
}
Основное отличие между конструктором копирования и оператором присваивания заключается в том, что первый создает новый объект на основе существующего, а второй изменяет состояние уже созданного объекта.
#323_Cpp_PkS
При каких условиях в конструкторе можно выбросить exception?
Выброс исключения (exception) в конструкторе допустим и полезен в тех случаях, когда невозможно корректно завершить процесс создания объекта.
Исключения позволяют передать информацию о возникшей ошибке внешнему коду, который пытается создать экземпляр класса.
Рассмотрим ситуации, когда выбрасывание исключений в конструкторе оправдано:
Невозможность выделения ресурсов — если в процессе создания объекта требуется выделение ресурсов (память, файловые дескрипторы, сетевые соединения и т.д.), и эта операция завершается неудачно, исключение может сообщить об этом вызвавшему коду:
Недопустимые параметры конструктора — если параметры конструктора недопустимы или нарушают инварианты класса, можно выбросить исключение, чтобы предотвратить создание неправильного объекта:
Ошибки инициализации членов класса — если некоторые члены класса не могут быть корректно инициализированы, например, если конструктор члена класса тоже бросает исключение, это исключение должно быть передано наружу:
Логические ошибки — иногда логика программы требует проверки некоторых условий до завершения процесса создания объекта. Если эти условия не выполняются, можно выбросить исключение:
Важно помнить:
Ресурсы должны освобождаться — если в конструкторе выделяются ресурсы (например, память или файлы), важно убедиться, что они будут корректно освобождены в случае возникновения исключения.
Для этого используются RAII (Resource Acquisition Is Initialization) идиомы, такие как умные указатели (std::unique_ptr, std::shared_ptr), которые освобождают ресурсы автоматически.
Исключения не должны скрывать проблемы — исключение должно использоваться для передачи информации о критической ошибке, а не для сокрытия проблем, которые могут быть решены иначе.
Когда НЕ стоит выбрасывать исключения:
Для контроля потока выполнения: Исключения предназначены для обработки ошибок, а не для управления обычным потоком выполнения программы. Например, не следует использовать исключения вместо условных операторов или циклов.
Когда ошибка легко исправима: Если проблема может быть решена без прерывания работы программы, лучше обработать её непосредственно в конструкторе, не прибегая к исключениям.
Таким образом, исключения в конструкторах полезны для информирования о серьёзных проблемах, связанных с созданием объекта, и позволяют внешним компонентам программы корректно реагировать на такие ситуации.
При каких условиях в конструкторе можно выбросить exception?
Выброс исключения (exception) в конструкторе допустим и полезен в тех случаях, когда невозможно корректно завершить процесс создания объекта.
Исключения позволяют передать информацию о возникшей ошибке внешнему коду, который пытается создать экземпляр класса.
Рассмотрим ситуации, когда выбрасывание исключений в конструкторе оправдано:
Невозможность выделения ресурсов — если в процессе создания объекта требуется выделение ресурсов (память, файловые дескрипторы, сетевые соединения и т.д.), и эта операция завершается неудачно, исключение может сообщить об этом вызвавшему коду:
class FileHandler {
public:
FileHandler(const std::string& filename) {
file.open(filename);
if (!file.is_open()) {
throw std::runtime_error("Failed to open file");
}
}
private:
std::ifstream file;
};Недопустимые параметры конструктора — если параметры конструктора недопустимы или нарушают инварианты класса, можно выбросить исключение, чтобы предотвратить создание неправильного объекта:
class PositiveNumber {
public:
PositiveNumber(int value) {
if (value <= 0) {
throw std::invalid_argument("Value must be positive");
}
this->value = value;
}
private:
int value;
};Ошибки инициализации членов класса — если некоторые члены класса не могут быть корректно инициализированы, например, если конструктор члена класса тоже бросает исключение, это исключение должно быть передано наружу:
class ComplexObject {
public:
ComplexObject(int param) : member(param) {}
private:
MemberClass member;
};
/* Предположим, что конструктор MemberClass может бросить исключение */Логические ошибки — иногда логика программы требует проверки некоторых условий до завершения процесса создания объекта. Если эти условия не выполняются, можно выбросить исключение:
class DatabaseConnection {
public:
DatabaseConnection(const std::string& connectionString) {
if (connectionString.empty()) {
throw std::logic_error("Connection string cannot be empty");
}
// Продолжение логики подключения
}
};Важно помнить:
Ресурсы должны освобождаться — если в конструкторе выделяются ресурсы (например, память или файлы), важно убедиться, что они будут корректно освобождены в случае возникновения исключения.
Для этого используются RAII (Resource Acquisition Is Initialization) идиомы, такие как умные указатели (std::unique_ptr, std::shared_ptr), которые освобождают ресурсы автоматически.
Исключения не должны скрывать проблемы — исключение должно использоваться для передачи информации о критической ошибке, а не для сокрытия проблем, которые могут быть решены иначе.
Когда НЕ стоит выбрасывать исключения:
Для контроля потока выполнения: Исключения предназначены для обработки ошибок, а не для управления обычным потоком выполнения программы. Например, не следует использовать исключения вместо условных операторов или циклов.
Когда ошибка легко исправима: Если проблема может быть решена без прерывания работы программы, лучше обработать её непосредственно в конструкторе, не прибегая к исключениям.
Таким образом, исключения в конструкторах полезны для информирования о серьёзных проблемах, связанных с созданием объекта, и позволяют внешним компонентам программы корректно реагировать на такие ситуации.
#324_Cpp_PkS
Что такое конструктор по умолчанию?
Для чего нужны default и delete?
Конструктор по умолчанию (default constructor) — специальный метод класса, который вызывается при создании объекта без передачи аргументов.
Если программист не определил ни одного конструктора для своего класса, компилятор автоматически сгенерирует конструктор по умолчанию.
Зачем нужен конструктор по умолчанию?
Инициализация объектов — конструктор по умолчанию используется для создания объектов класса без необходимости передавать параметры.
Это особенно полезно, когда у класса нет специфических требований к инициализации полей.
Автоматическая генерация компилятором — компилятор генерирует конструктор по умолчанию только если пользователь не определил никаких других конструкторов, что помогает избежать ошибок, связанных с отсутствием возможности создать объект без параметров.
Обеспечение совместимости — некоторые шаблоны и библиотеки требуют наличия конструктора по умолчанию для корректной работы с классами.
Например, контейнеры стандартной библиотеки C++ могут требовать наличие конструктора по умолчанию для правильной работы с объектами.
Ключевые слова default и delete.
Ключевое слово default — позволяет явно указать компилятору сгенерировать функцию-член по умолчанию.
Оно используется, чтобы явно показать, что функция должна быть сгенерирована компилятором даже после того, как были определены другие функции-члены:
Ключевое слово delete — позволяет запретить использование определенной функции-члена, что может быть полезно, например, для предотвращения копирования или перемещения объектов, а также для запрета вызова конструктора по умолчанию:
Конструктор по умолчанию важен для обеспечения возможности создавать объекты без явного указания параметров.
Ключевое слово default используется для явного указания компилятору сгенерировать стандартную реализацию функции-члена.
Ключевое слово delete применяется для запрещения использования определенных функций-членов, таких как конструктор по умолчанию, конструктор копирования или оператор присваивания.
Что такое конструктор по умолчанию?
Для чего нужны default и delete?
Конструктор по умолчанию (default constructor) — специальный метод класса, который вызывается при создании объекта без передачи аргументов.
Если программист не определил ни одного конструктора для своего класса, компилятор автоматически сгенерирует конструктор по умолчанию.
Зачем нужен конструктор по умолчанию?
Инициализация объектов — конструктор по умолчанию используется для создания объектов класса без необходимости передавать параметры.
Это особенно полезно, когда у класса нет специфических требований к инициализации полей.
Автоматическая генерация компилятором — компилятор генерирует конструктор по умолчанию только если пользователь не определил никаких других конструкторов, что помогает избежать ошибок, связанных с отсутствием возможности создать объект без параметров.
Обеспечение совместимости — некоторые шаблоны и библиотеки требуют наличия конструктора по умолчанию для корректной работы с классами.
Например, контейнеры стандартной библиотеки C++ могут требовать наличие конструктора по умолчанию для правильной работы с объектами.
Ключевые слова default и delete.
Ключевое слово default — позволяет явно указать компилятору сгенерировать функцию-член по умолчанию.
Оно используется, чтобы явно показать, что функция должна быть сгенерирована компилятором даже после того, как были определены другие функции-члены:
class MyClass {
public:
/* Явное указание компилятору генерировать конструктор по умолчанию */
MyClass() = default;
};Ключевое слово delete — позволяет запретить использование определенной функции-члена, что может быть полезно, например, для предотвращения копирования или перемещения объектов, а также для запрета вызова конструктора по умолчанию:
class MyClass {
public:
/* Запрет на вызов конструктора по умолчанию */
MyClass() = delete;
};Конструктор по умолчанию важен для обеспечения возможности создавать объекты без явного указания параметров.
Ключевое слово default используется для явного указания компилятору сгенерировать стандартную реализацию функции-члена.
Ключевое слово delete применяется для запрещения использования определенных функций-членов, таких как конструктор по умолчанию, конструктор копирования или оператор присваивания.
#325_Cpp_PkS
Чем отличается интерфейс от абстрактного класса?
Интерфейсы и абстрактные классы — два разных механизма, которые используются для реализации полиморфизма и абстракции в ООП.
Оба они помогают определять структуру классов, но имеют разные цели и особенности.
Интерфейс определяет контракт поведения, который должен быть реализован классами, имплементирующими этот интерфейс.
В интерфейсе можно определить только сигнатуры методов (название метода, параметры и возвращаемый тип), а также константы.
Реализация этих методов должна быть предоставлена в классе, который реализует данный интерфейс.
Особенности:
— только сигнатуры методов — в интерфейсах нельзя реализовать методы;
— все методы по умолчанию являются абстрактными (то есть без тела).
— не содержит состояния — в интерфейсе не могут быть поля с состоянием (переменные экземпляра);
— можно объявлять статические финальные переменные (константы).
Множественное наследование — класс может реализовать несколько интерфейсов одновременно, что позволяет ему поддерживать множественные контракты поведения.
Модификаторы доступа — все члены интерфейса по умолчанию считаются public и abstract, поэтому их явно указывать не нужно.
Пример использования — интерфейсы часто применяются для определения общих контрактов между различными реализациями, например, в случае паттерна "Стратегия" или при работе с коллекциями:
Абстрактный класс — представляет собой частичную реализацию класса, которая включает как абстрактные методы (без реализации), так и обычные методы с реализацией.
Классы, наследующиеся от абстрактного класса, должны переопределить его абстрактные методы, если они хотят быть конкретными (неабстрактными) классами.
Особенности:
Частичная реализация — в отличие от интерфейса, абстрактный класс может содержать как абстрактные методы, так и методы с полной реализацией.
Состояние и поведение — может иметь поля данных (состояния) и конструкторы, а также методы с телом.
Ограниченное наследование — класс может наследоваться только от одного абстрактного класса (в Java, C# и других языках с поддержкой одиночного наследования).
Модификаторы доступа — методы и поля могут иметь различные модификаторы доступа (private, protected, public), в зависимости от необходимости.
Пример использования — абстрактные классы полезны, когда необходимо предоставить базовую функциональность, которую потом будут расширять подклассы, например, в случае паттернов проектирования "Шаблонный метод".
Основные отличия:
Реализация методов — интерфейсы содержат только сигнатуры методов, тогда как абстрактные классы могут содержать как абстрактные методы, так и методы с реализацией.
Наследование — класс может реализовать множество интерфейсов, но может наследовать только один абстрактный класс.
Конкретная реализация — интерфейсы не предоставляют никакой конкретной реализации, в то время как абстрактные классы могут предоставлять частично готовую реализацию.
Состояние — интерфейсы не могут содержать состояние (поля данных), в то время как абстрактные классы могут.
Таким образом, выбор между использованием интерфейса или абстрактного класса зависит от того, насколько гибким должно быть ваше решение и какие именно функциональные возможности вы хотите передать через общую структуру.
Чем отличается интерфейс от абстрактного класса?
Интерфейсы и абстрактные классы — два разных механизма, которые используются для реализации полиморфизма и абстракции в ООП.
Оба они помогают определять структуру классов, но имеют разные цели и особенности.
Интерфейс определяет контракт поведения, который должен быть реализован классами, имплементирующими этот интерфейс.
В интерфейсе можно определить только сигнатуры методов (название метода, параметры и возвращаемый тип), а также константы.
Реализация этих методов должна быть предоставлена в классе, который реализует данный интерфейс.
Особенности:
— только сигнатуры методов — в интерфейсах нельзя реализовать методы;
— все методы по умолчанию являются абстрактными (то есть без тела).
— не содержит состояния — в интерфейсе не могут быть поля с состоянием (переменные экземпляра);
— можно объявлять статические финальные переменные (константы).
Множественное наследование — класс может реализовать несколько интерфейсов одновременно, что позволяет ему поддерживать множественные контракты поведения.
Модификаторы доступа — все члены интерфейса по умолчанию считаются public и abstract, поэтому их явно указывать не нужно.
Пример использования — интерфейсы часто применяются для определения общих контрактов между различными реализациями, например, в случае паттерна "Стратегия" или при работе с коллекциями:
interface IAnimal {
void makeSound();
}
class Dog implements IAnimal {
@Override
public void makeSound() {
System.out.println("Woof!");
}
}Абстрактный класс — представляет собой частичную реализацию класса, которая включает как абстрактные методы (без реализации), так и обычные методы с реализацией.
Классы, наследующиеся от абстрактного класса, должны переопределить его абстрактные методы, если они хотят быть конкретными (неабстрактными) классами.
Особенности:
Частичная реализация — в отличие от интерфейса, абстрактный класс может содержать как абстрактные методы, так и методы с полной реализацией.
Состояние и поведение — может иметь поля данных (состояния) и конструкторы, а также методы с телом.
Ограниченное наследование — класс может наследоваться только от одного абстрактного класса (в Java, C# и других языках с поддержкой одиночного наследования).
Модификаторы доступа — методы и поля могут иметь различные модификаторы доступа (private, protected, public), в зависимости от необходимости.
Пример использования — абстрактные классы полезны, когда необходимо предоставить базовую функциональность, которую потом будут расширять подклассы, например, в случае паттернов проектирования "Шаблонный метод".
abstract class Animal {
abstract void makeSound();
public void breathe() {
System.out.println("Inhale... Exhale...");
}
}
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("Meow!");
}
}Основные отличия:
Реализация методов — интерфейсы содержат только сигнатуры методов, тогда как абстрактные классы могут содержать как абстрактные методы, так и методы с реализацией.
Наследование — класс может реализовать множество интерфейсов, но может наследовать только один абстрактный класс.
Конкретная реализация — интерфейсы не предоставляют никакой конкретной реализации, в то время как абстрактные классы могут предоставлять частично готовую реализацию.
Состояние — интерфейсы не могут содержать состояние (поля данных), в то время как абстрактные классы могут.
Таким образом, выбор между использованием интерфейса или абстрактного класса зависит от того, насколько гибким должно быть ваше решение и какие именно функциональные возможности вы хотите передать через общую структуру.
#326_Cpp_PkS
Какие виды полиморфизма в С++?
В C++ существует несколько видов полиморфизма, каждый имеет свои особенности и применение.
Основные типы полиморфизма в C++:
Компилируемый (статический) полиморфизм — происходит во время компиляции программы. Компилятор знает заранее, какой метод будет вызван, основываясь на типе объекта. Существует два основных вида компилируемого полиморфизма:
— перегрузка функций (Function Overloading) — означает наличие нескольких функций с одинаковым именем, но разными сигнатурами (разное количество параметров или типы параметров). Компилятор выбирает нужную функцию на основе типов аргументов:
— перегрузка операторов (Operator Overloading) — процесс предоставления новых значений для существующих операторов (например, +, -, *) для пользовательских типов данных, что позволяет использовать операторы с объектами пользовательского типа так же, как с встроенными типами:
Динамический (рантаймовый) полиморфизм — реализуется во время выполнения программы и основан на использовании виртуальных функций и указателей/референций к базовым классам.
Динамическое связывание обеспечивает возможность вызова различных версий метода в зависимости от реального типа объекта:
— полиморфизм через виртуальные функции (Virtual Functions) — использование ключевого слова virtual перед методом в базовом классе указывает, что этот метод может быть переопределен в производных классах.
При вызове такого метода через указатель или ссылку на базовый класс, вызывается версия метода, соответствующая реальному типу объекта:
— чистые виртуальные методы (Pure Virtual Function) — определяются в базовом классе с помощью = 0 и обязаны быть переопределены в производном классе, иначе он останется абстрактным (нельзя создать экземпляр такого класса):
.
Какие виды полиморфизма в С++?
В C++ существует несколько видов полиморфизма, каждый имеет свои особенности и применение.
Основные типы полиморфизма в C++:
Компилируемый (статический) полиморфизм — происходит во время компиляции программы. Компилятор знает заранее, какой метод будет вызван, основываясь на типе объекта. Существует два основных вида компилируемого полиморфизма:
— перегрузка функций (Function Overloading) — означает наличие нескольких функций с одинаковым именем, но разными сигнатурами (разное количество параметров или типы параметров). Компилятор выбирает нужную функцию на основе типов аргументов:
#include <iostream>
using namespace std;
void print(int x) {
cout << "int: " << x << endl;
}
void print(double x) {
cout << "double: " << x << endl;
}
int main() {
print(42);
print(3.14);
return 0;
}
— перегрузка операторов (Operator Overloading) — процесс предоставления новых значений для существующих операторов (например, +, -, *) для пользовательских типов данных, что позволяет использовать операторы с объектами пользовательского типа так же, как с встроенными типами:
#include <iostream>
using namespace std;
class Complex {
private:
double real;
double imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// Перегружаем оператор +
Complex operator+(const Complex &other) const {
return Complex(real + other.real, imag + other.imag);
}
};
int main() {
Complex c1(3, 7);
Complex c2(5, 2);
Complex result = c1 + c2;
cout << "(" << result.real << ", " << result.imag << ")" << endl;
return 0;
}
Динамический (рантаймовый) полиморфизм — реализуется во время выполнения программы и основан на использовании виртуальных функций и указателей/референций к базовым классам.
Динамическое связывание обеспечивает возможность вызова различных версий метода в зависимости от реального типа объекта:
— полиморфизм через виртуальные функции (Virtual Functions) — использование ключевого слова virtual перед методом в базовом классе указывает, что этот метод может быть переопределен в производных классах.
При вызове такого метода через указатель или ссылку на базовый класс, вызывается версия метода, соответствующая реальному типу объекта:
#include <iostream>
using namespace std;
class Shape {
public:
virtual void draw() { cout << "Shape::draw()\n"; }
};
class Circle : public Shape {
public:
void draw() override { cout << "Circle::draw()\n"; }
};
class Rectangle : public Shape {
public:
void draw() override { cout << "Rectangle::draw()\n"; }
};
int main() {
Shape *shape1 = new Circle();
Shape *shape2 = new Rectangle();
// Вызывает Circle::draw()
shape1->draw();
// Вызывает Rectangle::draw()
shape2->draw();
delete shape1;
delete shape2;
return 0;
}
— чистые виртуальные методы (Pure Virtual Function) — определяются в базовом классе с помощью = 0 и обязаны быть переопределены в производном классе, иначе он останется абстрактным (нельзя создать экземпляр такого класса):
#include <iostream>
using namespace std;
class Shape {
public:
// Чисто виртуальная функция
virtual void draw() = 0;
};
class Circle : public Shape {
public:
void draw() override { cout << "Circle::draw()\n"; }
};
/* Класс Rectangle остается абстрактным, т.к. не переопределяет draw() */
class Rectangle : public Shape {};
int main() {
Circle circle;
// Вызываем Circle::draw()
circle.draw();
/* Ошибка компиляции: невозможно создать объект Rectangle */
// Rectangle rectangle;
return 0;
}
.
Параметрический полиморфизм (Generics) — достигается за счет шаблонов (templates) в C++.
Шаблоны позволяют создавать обобщённые функции и классы, которые работают с различными типами данных:
— шаблоны функций (Function Templates) — функция-шаблон принимает аргументы любого типа и работает одинаково для всех типов данных:
— шаблоны классов (Class Templates) — класс-шаблон позволяет создавать классы, работающие с различными типами данных:
Эти три вида полиморфизма широко используются в C++, чтобы обеспечить гибкость кода и упрощение разработки сложных систем.
Шаблоны позволяют создавать обобщённые функции и классы, которые работают с различными типами данных:
— шаблоны функций (Function Templates) — функция-шаблон принимает аргументы любого типа и работает одинаково для всех типов данных:
#include <iostream>
using namespace std;
template<typename T>
T max(T a, T b) {
return (a > b) ? a : b;
}
int main() {
int i = 5, j = 10;
double x = 6.8, y = 9.2;
cout << "Max of ints: " << max(i, j) << endl;
cout << "Max of doubles: " << max(x, y) << endl;
return 0;
}
— шаблоны классов (Class Templates) — класс-шаблон позволяет создавать классы, работающие с различными типами данных:
#include <iostream>
using namespace std;
template<typename T>
class Pair {
private:
T first;
T second;
public:
Pair(const T& f, const T& s) : first(f), second(s) {}
T getFirst() const { return first; }
T getSecond() const { return second; }
};
int main() {
Pair<int> p1(3, 7);
Pair<double> p2(3.14, 2.71);
cout << "Pair of ints: (" << p1.getFirst() << ", " << p1.getSecond() << ")\n";
cout << "Pair of doubles: (" << p2.getFirst() << ", " << p2.getSecond() << ")\n";
return 0;
}
Эти три вида полиморфизма широко используются в C++, чтобы обеспечить гибкость кода и упрощение разработки сложных систем.
#327_Cpp_PkS_PPPO
Почему синглтон считается антипаттерном?
Паттерн Singleton (синглтон) — один из самых известных и часто используемых паттернов проектирования, но многие разработчики считают его антипаттерном, поскольку его использование может привести к ряду проблем:
Трудности тестирования — одним из главных недостатков синглтона является сложность его тестирования.
Поскольку синглтон гарантирует существование единственного экземпляра класса, его использование делает модульные тесты сложнее:
— зависимости — синглтон скрывает зависимости, делая код менее очевидным и затрудняя тестирование отдельных компонентов системы;
— глобальное состояние — глобальное состояние, которое создается синглтоном, может приводить к непредсказуемому поведению тестов, особенно если порядок выполнения тестов влияет на результаты;
— невозможность замены — в тестах зачастую требуется заменить реальный объект на mock-объект, однако с синглтоном сделать это затруднительно, потому что у вас нет прямого контроля над созданием объекта.
Нарушение принципа единственной ответственности (SRP) — принцип единственной ответственности гласит, что каждый класс должен иметь одну причину для изменения, но используя синглтон, класс становится ответственным не только за свою основную логику, но и за управление своим жизненным циклом (создание и уничтожение экземпляра), что нарушает SRP и усложняет поддержку кода.
Проблемы многопоточности — если проект использует многопоточное выполнение, синглтон может стать источником ошибок синхронизации.
Даже если вы используете механизмы блокировки для создания экземпляра синглтона, это всё равно добавляет сложности и потенциальные проблемы производительности.
Скрытые зависимости — когда класс напрямую обращается к синглтону, это создает скрытую зависимость, которая неявно передается другим компонентам системы, что затрудняет понимание архитектуры приложения и усложняет рефакторинг.
Усложнение поддержки и масштабируемости — поскольку синглтон управляет собственным жизненным циклом, это ограничивает возможности масштабирования и настройки приложения.
Например, вам может понадобиться создать несколько экземпляров класса в разных контекстах, но синглтон этому препятствует.
Монолитность и сцепленность — синглтон увеличивает сцепленность между модулями системы, что приводит к монолитной архитектуре.
Это снижает гибкость и возможность повторного использования кода.
Альтернативы синглтону.
Существует несколько альтернатив использованию синглтона, которые могут решить описанные выше проблемы:
Dependency Injection (DI) — этот подход позволяет передавать зависимости через конструктор или методы класса, что делает код более тестопригодным и гибким.
Контейнеры зависимостей — использование контейнеров зависимостей (например, Spring в Java или DI-контейнеров в .NET) позволяет управлять жизненными циклами объектов централизованно, избегая необходимости вручную управлять ими внутри каждого класса.
Локализованные объекты — вместо глобального синглтона можно использовать локализацию объектов в пределах конкретного контекста (например, в рамках HTTP-запроса или потока).
Хотя паттерн Singleton был популярен долгое время, его недостатки стали более заметны с развитием современных подходов к разработке ПО.
Использование Dependency Injection и контейнеров зависимостей помогает избежать многих проблем, связанных с синглтонами, обеспечивая лучшую тестопригодность, гибкость и поддерживаемость кода.
Почему синглтон считается антипаттерном?
Паттерн Singleton (синглтон) — один из самых известных и часто используемых паттернов проектирования, но многие разработчики считают его антипаттерном, поскольку его использование может привести к ряду проблем:
Трудности тестирования — одним из главных недостатков синглтона является сложность его тестирования.
Поскольку синглтон гарантирует существование единственного экземпляра класса, его использование делает модульные тесты сложнее:
— зависимости — синглтон скрывает зависимости, делая код менее очевидным и затрудняя тестирование отдельных компонентов системы;
— глобальное состояние — глобальное состояние, которое создается синглтоном, может приводить к непредсказуемому поведению тестов, особенно если порядок выполнения тестов влияет на результаты;
— невозможность замены — в тестах зачастую требуется заменить реальный объект на mock-объект, однако с синглтоном сделать это затруднительно, потому что у вас нет прямого контроля над созданием объекта.
Нарушение принципа единственной ответственности (SRP) — принцип единственной ответственности гласит, что каждый класс должен иметь одну причину для изменения, но используя синглтон, класс становится ответственным не только за свою основную логику, но и за управление своим жизненным циклом (создание и уничтожение экземпляра), что нарушает SRP и усложняет поддержку кода.
Проблемы многопоточности — если проект использует многопоточное выполнение, синглтон может стать источником ошибок синхронизации.
Даже если вы используете механизмы блокировки для создания экземпляра синглтона, это всё равно добавляет сложности и потенциальные проблемы производительности.
Скрытые зависимости — когда класс напрямую обращается к синглтону, это создает скрытую зависимость, которая неявно передается другим компонентам системы, что затрудняет понимание архитектуры приложения и усложняет рефакторинг.
Усложнение поддержки и масштабируемости — поскольку синглтон управляет собственным жизненным циклом, это ограничивает возможности масштабирования и настройки приложения.
Например, вам может понадобиться создать несколько экземпляров класса в разных контекстах, но синглтон этому препятствует.
Монолитность и сцепленность — синглтон увеличивает сцепленность между модулями системы, что приводит к монолитной архитектуре.
Это снижает гибкость и возможность повторного использования кода.
Альтернативы синглтону.
Существует несколько альтернатив использованию синглтона, которые могут решить описанные выше проблемы:
Dependency Injection (DI) — этот подход позволяет передавать зависимости через конструктор или методы класса, что делает код более тестопригодным и гибким.
Контейнеры зависимостей — использование контейнеров зависимостей (например, Spring в Java или DI-контейнеров в .NET) позволяет управлять жизненными циклами объектов централизованно, избегая необходимости вручную управлять ими внутри каждого класса.
Локализованные объекты — вместо глобального синглтона можно использовать локализацию объектов в пределах конкретного контекста (например, в рамках HTTP-запроса или потока).
Хотя паттерн Singleton был популярен долгое время, его недостатки стали более заметны с развитием современных подходов к разработке ПО.
Использование Dependency Injection и контейнеров зависимостей помогает избежать многих проблем, связанных с синглтонами, обеспечивая лучшую тестопригодность, гибкость и поддерживаемость кода.
👍1
#328_Cpp_CMPL_PkS_PPPO
Как реализовано наследование в большинстве компиляторов?
Наследование — одна из ключевых концепций ООП, позволяющая одному классу (производному) заимствовать свойства и методы другого класса (базового).
Большинство компиляторов используют схожие подходы для реализации наследования, хотя детали могут варьироваться в зависимости от ЯП и особенностей компилятора.
Рассмотрим основные аспекты реализации наследования на примере C++ и Java, так как они представляют две популярные парадигмы компиляции: компиляцию в машинный код (C++) и компиляцию байт-кода (Java).
C++ — статическая компиляция — компиляторы преобразуют исходный код в машинный код, оптимизируя его под целевую платформу.
Наследование в C++ реализовано следующим образом:
— составление структуры памяти — при создании объекта производного класса память выделяется для хранения полей как базового, так и производного классов.
Память распределяется последовательно — сначала идут данные базового класса, затем — данные производного:
Для объекта Derived структура памяти будет выглядеть примерно так:
— виртуальные таблицы (vtable) — для поддержки динамического полиморфизма (через виртуальные функции) компилятор генерирует виртуальную таблицу (vtable) для каждого класса.
Vtable содержит указатели на адреса виртуальных функций.
Для каждого объекта создается указатель на соответствующую vtable:
Vtable для Base и Derived будет содержать указатели на соответствующие версии func():
Каждый объект будет хранить указатель на свою vtable, что позволяет вызывать правильные версии виртуальных функций во время выполнения.
— доступ к членам класса — компилятор обрабатывает доступ к членам класса, добавляя смещения для корректного обращения к данным.
Например, если есть указатель на базовый класс, который фактически указывает на объект производного класса, компилятор будет учитывать смещение для правильного доступа к данным производного класса:
— конструкторы и деструкторы — при создании объекта производного класса сначала вызываются конструкторы базовых классов (рекурсивно), начиная с самого верхнего уровня иерархии.
Аналогично, при уничтожении объекта сначала вызываются деструкторы производных классов, а затем базовых:
Результат выполнения программы:
Java — компилирование в байт-код — компиляторы преобразуют исходный код в байт-код, который выполняется виртуальной машиной JVM.
Наследование в Java реализовано следующим образом:
— организация памяти — объекты в Java хранятся в куче (heap), где каждая запись содержит информацию о типе объекта и его полях.
Объекты базового и производного классов организованы аналогично тому, как это делается в C++: сначала идет информация о базовом классе, затем — о производном:
Как реализовано наследование в большинстве компиляторов?
Наследование — одна из ключевых концепций ООП, позволяющая одному классу (производному) заимствовать свойства и методы другого класса (базового).
Большинство компиляторов используют схожие подходы для реализации наследования, хотя детали могут варьироваться в зависимости от ЯП и особенностей компилятора.
Рассмотрим основные аспекты реализации наследования на примере C++ и Java, так как они представляют две популярные парадигмы компиляции: компиляцию в машинный код (C++) и компиляцию байт-кода (Java).
C++ — статическая компиляция — компиляторы преобразуют исходный код в машинный код, оптимизируя его под целевую платформу.
Наследование в C++ реализовано следующим образом:
— составление структуры памяти — при создании объекта производного класса память выделяется для хранения полей как базового, так и производного классов.
Память распределяется последовательно — сначала идут данные базового класса, затем — данные производного:
class Base {
public:
int baseField;
};
class Derived : public Base {
public:
int derivedField;
};Для объекта Derived структура памяти будет выглядеть примерно так:
| Base::baseField | Derived::derivedField |
— виртуальные таблицы (vtable) — для поддержки динамического полиморфизма (через виртуальные функции) компилятор генерирует виртуальную таблицу (vtable) для каждого класса.
Vtable содержит указатели на адреса виртуальных функций.
Для каждого объекта создается указатель на соответствующую vtable:
class Base {
public:
virtual void func() { /* ... */ }
};
class Derived : public Base {
public:
void func() override { /* ... */ }
};Vtable для Base и Derived будет содержать указатели на соответствующие версии func():
Base::vtable: [ &Base::func ]
Derived::vtable: [ &Derived::func ]
Каждый объект будет хранить указатель на свою vtable, что позволяет вызывать правильные версии виртуальных функций во время выполнения.
— доступ к членам класса — компилятор обрабатывает доступ к членам класса, добавляя смещения для корректного обращения к данным.
Например, если есть указатель на базовый класс, который фактически указывает на объект производного класса, компилятор будет учитывать смещение для правильного доступа к данным производного класса:
Base *ptr = new Derived;
ptr->baseField = 42; /* Доступ к полю baseField через указатель на Base */
— конструкторы и деструкторы — при создании объекта производного класса сначала вызываются конструкторы базовых классов (рекурсивно), начиная с самого верхнего уровня иерархии.
Аналогично, при уничтожении объекта сначала вызываются деструкторы производных классов, а затем базовых:
class Base {
public:
Base() { cout << "Base constructor\n"; }
~Base() { cout << "Base destructor\n"; }
};
class Derived : public Base {
public:
Derived() { cout << "Derived constructor\n"; }
~Derived() { cout << "Derived destructor\n"; }
};
int main() {
Derived obj;
return 0;
}Результат выполнения программы:
Base constructor
Derived constructor
Derived destructor
Base destructor
Java — компилирование в байт-код — компиляторы преобразуют исходный код в байт-код, который выполняется виртуальной машиной JVM.
Наследование в Java реализовано следующим образом:
— организация памяти — объекты в Java хранятся в куче (heap), где каждая запись содержит информацию о типе объекта и его полях.
Объекты базового и производного классов организованы аналогично тому, как это делается в C++: сначала идет информация о базовом классе, затем — о производном:
class Base {
int baseField;
}
class Derived extends Base {
int derivedField;
}— таблицы методов (Method Tables) — для каждого загруженного в JVM класса создаются таблицы методов, содержащие ссылки на методы данного класса.
Эти таблицы позволяют быстро находить нужный метод при выполнении программы:
Метод func() будет представлен в таблицах методов обоих классов, позволяя JVM выбирать правильную версию метода в зависимости от типа объекта.
— конструкторы и деструкторы — как и в C++, при создании объекта производного класса сначала вызываются конструкторы базовых классов, начиная с самого верхнего уровня иерархии.
В Java нет явных деструкторов, вместо них используется сборщик мусора (Garbage Collector), который автоматически освобождает память, когда объект больше не нужен:
Результат выполнения программы:
Несмотря на различия в деталях реализации, большинство компиляторов придерживаются общей стратегии при обработке наследования: организация памяти для хранения данных базовых и производных классов, создание таблиц методов для поиска нужных функций, вызов конструкторов и деструкторов в определенном порядке.
Различия связаны с особенностями конкретных ЯП и моделей исполнения (статическая компиляция vs интерпретация байт-кода).
Эти таблицы позволяют быстро находить нужный метод при выполнении программы:
class Base {
void func() { /* ... */ }
}
class Derived extends Base {
@Override
void func() { /* ... */ }
}Метод func() будет представлен в таблицах методов обоих классов, позволяя JVM выбирать правильную версию метода в зависимости от типа объекта.
— конструкторы и деструкторы — как и в C++, при создании объекта производного класса сначала вызываются конструкторы базовых классов, начиная с самого верхнего уровня иерархии.
В Java нет явных деструкторов, вместо них используется сборщик мусора (Garbage Collector), который автоматически освобождает память, когда объект больше не нужен:
class Base {
Base() { System.out.println("Base constructor"); }
}
class Derived extends Base {
Derived() { System.out.println("Derived constructor"); }
}
public class Main {
public static void main(String[] args) {
Derived obj = new Derived();
}
}Результат выполнения программы:
Base constructor
Derived constructor
Несмотря на различия в деталях реализации, большинство компиляторов придерживаются общей стратегии при обработке наследования: организация памяти для хранения данных базовых и производных классов, создание таблиц методов для поиска нужных функций, вызов конструкторов и деструкторов в определенном порядке.
Различия связаны с особенностями конкретных ЯП и моделей исполнения (статическая компиляция vs интерпретация байт-кода).
#329_Cpp_PkS_PPPO
Множественное наследование: за и против?
Множественное наследование — возможность класса одновременно наследовать свойства и методы от нескольких базовых классов.
Этот механизм поддерживается многими объектно-ориентированными ЯП, такими как C++.
Плюсы множественного наследования:
Гибкость — множественное наследование позволяет создавать классы с более сложной структурой, комбинируя функциональность разных базовых классов, что может быть полезно при разработке сложных систем, где нужно объединить несколько независимых функциональностей.
Повторное использование кода — возможность использовать сразу несколько готовых решений (базовых классов) для создания нового класса без необходимости переписывать код заново.
Модульность — позволяет разбивать сложную логику на отдельные компоненты, которые можно потом комбинировать по мере необходимости.
Унификация интерфейсов — в некоторых случаях множественное наследование помогает унифицировать интерфейс различных компонентов системы, что упрощает их взаимодействие.
Минусы множественного наследования:
Проблема ромба (diamond problem) — если два базовых класса имеют общий предок, а класс-наследник наследует оба этих класса, возникает проблема неоднозначности: какой метод общего предка будет вызван?
Эта ситуация требует дополнительных механизмов разрешения конфликта, таких как виртуальное наследование в C++.
Сложность понимания и поддержки — с увеличением количества базовых классов возрастает сложность структуры программы. Разработчикам становится сложнее понимать и поддерживать такой код, особенно если базовые классы не были изначально спроектированы под совместное использование.
Неоднозначные зависимости — при использовании множества базовых классов могут возникнуть скрытые зависимости между ними, что усложняет понимание поведения программы и затрудняет внесение изменений.
Переусложнение архитектуры — избыточная гибкость иногда приводит к созданию слишком запутанных архитектур, когда вместо решения одной задачи создается сложная система взаимосвязей между классами.
Конфликты имен методов — если у двух базовых классов есть одноименные методы, возникает необходимость явно указывать, какой именно метод должен использоваться в классе-наследнике, что также увеличивает сложность кода.
Альтернативы:
Вместо использования множественного наследования часто применяются другие подходы:
Композиция — вместо того чтобы наследоваться от нескольких классов, можно создать объект одного класса внутри другого. Это делает архитектуру более прозрачной и управляемой.
Интерфейсы и абстрактные классы — в языках вроде Java и C# можно использовать интерфейсы и абстрактные классы для определения общих контрактов, оставляя реализацию конкретным классам.
Микросервисы — разделение большой системы на независимые сервисы, каждый из которых отвечает за свою часть функционала, также является хорошей альтернативой.
Множественное наследование имеет свои плюсы и минусы. Оно предоставляет гибкие возможности для проектирования сложных систем, но также несет риски, связанные со сложностью и возможными конфликтами.
Важно тщательно взвешивать все "за" и "против", прежде чем применять этот подход в своих проектах.
Множественное наследование: за и против?
Множественное наследование — возможность класса одновременно наследовать свойства и методы от нескольких базовых классов.
Этот механизм поддерживается многими объектно-ориентированными ЯП, такими как C++.
Плюсы множественного наследования:
Гибкость — множественное наследование позволяет создавать классы с более сложной структурой, комбинируя функциональность разных базовых классов, что может быть полезно при разработке сложных систем, где нужно объединить несколько независимых функциональностей.
Повторное использование кода — возможность использовать сразу несколько готовых решений (базовых классов) для создания нового класса без необходимости переписывать код заново.
Модульность — позволяет разбивать сложную логику на отдельные компоненты, которые можно потом комбинировать по мере необходимости.
Унификация интерфейсов — в некоторых случаях множественное наследование помогает унифицировать интерфейс различных компонентов системы, что упрощает их взаимодействие.
Минусы множественного наследования:
Проблема ромба (diamond problem) — если два базовых класса имеют общий предок, а класс-наследник наследует оба этих класса, возникает проблема неоднозначности: какой метод общего предка будет вызван?
Эта ситуация требует дополнительных механизмов разрешения конфликта, таких как виртуальное наследование в C++.
Сложность понимания и поддержки — с увеличением количества базовых классов возрастает сложность структуры программы. Разработчикам становится сложнее понимать и поддерживать такой код, особенно если базовые классы не были изначально спроектированы под совместное использование.
Неоднозначные зависимости — при использовании множества базовых классов могут возникнуть скрытые зависимости между ними, что усложняет понимание поведения программы и затрудняет внесение изменений.
Переусложнение архитектуры — избыточная гибкость иногда приводит к созданию слишком запутанных архитектур, когда вместо решения одной задачи создается сложная система взаимосвязей между классами.
Конфликты имен методов — если у двух базовых классов есть одноименные методы, возникает необходимость явно указывать, какой именно метод должен использоваться в классе-наследнике, что также увеличивает сложность кода.
Альтернативы:
Вместо использования множественного наследования часто применяются другие подходы:
Композиция — вместо того чтобы наследоваться от нескольких классов, можно создать объект одного класса внутри другого. Это делает архитектуру более прозрачной и управляемой.
Интерфейсы и абстрактные классы — в языках вроде Java и C# можно использовать интерфейсы и абстрактные классы для определения общих контрактов, оставляя реализацию конкретным классам.
Микросервисы — разделение большой системы на независимые сервисы, каждый из которых отвечает за свою часть функционала, также является хорошей альтернативой.
Множественное наследование имеет свои плюсы и минусы. Оно предоставляет гибкие возможности для проектирования сложных систем, но также несет риски, связанные со сложностью и возможными конфликтами.
Важно тщательно взвешивать все "за" и "против", прежде чем применять этот подход в своих проектах.
#330_Cpp_PkS_PPPO
Виртуальное наследование и порядок конструирования?
Виртуальное наследование и порядок конструирования объектов тесно связаны друг с другом, особенно в контексте ЯП, поддерживающих множественное наследование, например, C++.
Виртуальное наследование используется для предотвращения дублирования данных в случае так называемой проблемы ромба:
Здесь класс D наследуется от обоих классов B и C, которые, в свою очередь, наследуются от A. Таким образом, в классе D оказывается две копии переменной x — одна через B, другая через C.
Чтобы избежать этой проблемы, применяется виртуальное наследование:
Теперь класс D содержит только одну копию объекта A, которая разделяется между B и C.
Порядок конструирования объектов.
При создании объекта производного класса происходит вызов конструкторов всех его предков. Порядок вызова конструкторов следующий:
— cначала вызываются конструкторы базового класса (или классов), начиная с самого верхнего уровня иерархии;
— затем вызывается конструктор каждого виртуального базового класса;
— после этого вызываются конструкторы остальных базовых классов, в порядке их объявления в списке наследования;
— наконец, вызывается конструктор самого производного класса.
В этом примере вывод будет следующим:
Обратите внимание, что сначала вызывается конструктор класса A, поскольку он является общим предком для B и C, затем конструкторы B и C, и наконец, конструктор D.
Важные моменты:
Порядок вызова конструктора виртуальных базовых классов — конструктор виртуального базового класса всегда вызывается перед конструкторами любых других базовых классов.
Инициация членов — члены виртуального базового класса инициализируются до того, как будут вызваны конструкторы любого из производных классов.
Единственный экземпляр — в случае виртуального наследования создается только один экземпляр виртуального базового класса, который совместно используется всеми классами-потомками.
Таким образом, виртуальное наследование и порядок конструирования объектов тесно связаны и требуют внимательного подхода при проектировании иерархий классов.
Виртуальное наследование и порядок конструирования?
Виртуальное наследование и порядок конструирования объектов тесно связаны друг с другом, особенно в контексте ЯП, поддерживающих множественное наследование, например, C++.
Виртуальное наследование используется для предотвращения дублирования данных в случае так называемой проблемы ромба:
class A {
public:
int x;
};
class B : public A {};
class C : public A {};
// Проблема ромба!
class D : public B, public C {};Здесь класс D наследуется от обоих классов B и C, которые, в свою очередь, наследуются от A. Таким образом, в классе D оказывается две копии переменной x — одна через B, другая через C.
Чтобы избежать этой проблемы, применяется виртуальное наследование:
class B : virtual public A {};
class C : virtual public A {};
// Теперь только одна копия A!
class D : public B, public C {};Теперь класс D содержит только одну копию объекта A, которая разделяется между B и C.
Порядок конструирования объектов.
При создании объекта производного класса происходит вызов конструкторов всех его предков. Порядок вызова конструкторов следующий:
— cначала вызываются конструкторы базового класса (или классов), начиная с самого верхнего уровня иерархии;
— затем вызывается конструктор каждого виртуального базового класса;
— после этого вызываются конструкторы остальных базовых классов, в порядке их объявления в списке наследования;
— наконец, вызывается конструктор самого производного класса.
#include <iostream>
class A {
public:
A() { std::cout << "Constructor of A\n"; }
};
class B : virtual public A {
public:
B() { std::cout << "Constructor of B\n"; }
};
class C : virtual public A {
public:
C() { std::cout << "Constructor of C\n"; }
};
class D : public B, public C {
public:
D() { std::cout << "Constructor of D\n"; }
};
int main() {
D obj;
}
В этом примере вывод будет следующим:
Constructor of A
Constructor of B
Constructor of C
Constructor of D
Обратите внимание, что сначала вызывается конструктор класса A, поскольку он является общим предком для B и C, затем конструкторы B и C, и наконец, конструктор D.
Важные моменты:
Порядок вызова конструктора виртуальных базовых классов — конструктор виртуального базового класса всегда вызывается перед конструкторами любых других базовых классов.
Инициация членов — члены виртуального базового класса инициализируются до того, как будут вызваны конструкторы любого из производных классов.
Единственный экземпляр — в случае виртуального наследования создается только один экземпляр виртуального базового класса, который совместно используется всеми классами-потомками.
Таким образом, виртуальное наследование и порядок конструирования объектов тесно связаны и требуют внимательного подхода при проектировании иерархий классов.
#331_Cpp_PkS_PPPO
Зачем использовать override?
Ключевое слово override в языке C++ используется для явного указания, что метод в производном классе переопределяет метод базового класса.
Ключевое слово override было введено в стандарт C++11 и служит нескольким важным целям:
Предотвращение ошибок — когда нужно переопределить метод базового класса, важно убедиться, что сигнатуры метода совпадают.
Например, если вы случайно измените имя метода или тип аргумента, компилятор не сможет распознать, что вы пытаетесь переопределить существующий метод.
Использование override заставляет компилятор проверять соответствие сигнатур и сигнализирует об ошибке, если метод не соответствует методу базового класса.
Если не указывать ключевое слово override в дочернем классе и при этом будет изменена сигнатура метода, то произойдет не переопределение, а перегрузка метода, поэтому можно сказать, что override запрещает перегрузку методов в дочернем классе:
Компилятор выдаст ошибку, потому что метод func в классе Derived пытается переопределить метод func из класса Base, но имеет другую сигнатуру.
Ясность кода — использование override делает намерение разработчика явным.
Когда другой программист читает ваш код, он сразу видит, что данный метод предназначен для переопределения метода базового класса, что улучшает читаемость и понятность кода.
Совместимость с будущими изменениями — если в будущем изменится сигнатура метода в базовом классе, компилятор автоматически обнаружит несоответствие и сообщит о нем.
Без использования override такие изменения могли бы остаться незамеченными, приводя к неожиданному поведению программы.
Поддержка автоматического тестирования — многие инструменты статического анализа кода и автоматические тесты используют информацию о том, какие методы являются переопределениями. Использование override облегчает работу таких инструментов и повышает качество кода:
В этом примере использование override гарантирует, что методы makeSound() в классах Dog и Cat действительно переопределяют соответствующий метод из базового класса Animal.
Использование override — хорошая практика, которая помогает предотвратить ошибки, улучшить читаемость кода и обеспечить совместимость с будущими изменениями.
Зачем использовать override?
Ключевое слово override в языке C++ используется для явного указания, что метод в производном классе переопределяет метод базового класса.
Ключевое слово override было введено в стандарт C++11 и служит нескольким важным целям:
Предотвращение ошибок — когда нужно переопределить метод базового класса, важно убедиться, что сигнатуры метода совпадают.
Например, если вы случайно измените имя метода или тип аргумента, компилятор не сможет распознать, что вы пытаетесь переопределить существующий метод.
Использование override заставляет компилятор проверять соответствие сигнатур и сигнализирует об ошибке, если метод не соответствует методу базового класса.
Если не указывать ключевое слово override в дочернем классе и при этом будет изменена сигнатура метода, то произойдет не переопределение, а перегрузка метода, поэтому можно сказать, что override запрещает перегрузку методов в дочернем классе:
class Base {
public:
virtual void func(int a);
};
class Derived : public Base {
public:
void func(float b) override; /* Ошибка! Тип параметра отличается */
};Компилятор выдаст ошибку, потому что метод func в классе Derived пытается переопределить метод func из класса Base, но имеет другую сигнатуру.
Ясность кода — использование override делает намерение разработчика явным.
Когда другой программист читает ваш код, он сразу видит, что данный метод предназначен для переопределения метода базового класса, что улучшает читаемость и понятность кода.
Совместимость с будущими изменениями — если в будущем изменится сигнатура метода в базовом классе, компилятор автоматически обнаружит несоответствие и сообщит о нем.
Без использования override такие изменения могли бы остаться незамеченными, приводя к неожиданному поведению программы.
Поддержка автоматического тестирования — многие инструменты статического анализа кода и автоматические тесты используют информацию о том, какие методы являются переопределениями. Использование override облегчает работу таких инструментов и повышает качество кода:
class Animal {
public:
virtual void makeSound() const = 0;
};
class Dog : public Animal {
public:
void makeSound() const override { std::cout << "Woof!" << std::endl; }
};
class Cat : public Animal {
public:
void makeSound() const override { std::cout << "Meow!" << std::endl; }
};В этом примере использование override гарантирует, что методы makeSound() в классах Dog и Cat действительно переопределяют соответствующий метод из базового класса Animal.
Использование override — хорошая практика, которая помогает предотвратить ошибки, улучшить читаемость кода и обеспечить совместимость с будущими изменениями.
#332_Cpp_PkS_PPPO_UB
Расскажите обо всех возможных способах использования ключевого слова static в С++.
Что такое static initialization order fiasco?
Ключевое слово static в языке C++ имеет множество применений, каждое из которых связано с различными аспектами управления временем жизни и областью видимости переменных и функций.
Способы использования static в C++:
Статический член класса — cтатическими членами называются члены класса, которые принадлежат классу в целом, а не отдельным экземплярам этого класса. Они существуют независимо от количества созданных экземпляров класса и делятся ими всеми.
Статическая функция-член класса — cтатические функции-члены класса могут обращаться к другим статическим членам класса, но не имеют доступа к нестатическим членам, так как они не связаны с каким-либо конкретным объектом.
Статическая локальная переменная — локальные переменные, объявленные как static, сохраняются между вызовами функции, в которой они определены. Их начальное значение устанавливается только один раз, при первом входе в функцию.
Статическая глобальная переменная — глобальные переменные, объявленные как static, имеют внутреннюю область видимости, т.е. доступны только в пределах файла, в котором они определены, что помогает избежать конфликтов имен между файлами.
Статическая функция — функции, объявленные как static, имеют внутреннюю область видимости и доступны только в пределах файла, в котором они определены, что предотвращает конфликты имен между разными файлами.
Статическое связывание — ключевое слово static также используется для указания способа связывания функций и переменных в контексте внешней и внутренней области видимости.
Внешнее связывание означает, что символ доступен во всем проекте, внутреннее — только в текущем модуле (файле).
Static Initialization Order Fiasco (SIOF) — проблема, возникающая при инициализации статических переменных, которые зависят друг от друга и находятся в разных единицах трансляции (трансляционных единицах, TU).
В C++ нет гарантии порядка инициализации статических переменных между разными трансляционными единицами, что может привести к неопределенному поведению.
Расскажите обо всех возможных способах использования ключевого слова static в С++.
Что такое static initialization order fiasco?
Ключевое слово static в языке C++ имеет множество применений, каждое из которых связано с различными аспектами управления временем жизни и областью видимости переменных и функций.
Способы использования static в C++:
Статический член класса — cтатическими членами называются члены класса, которые принадлежат классу в целом, а не отдельным экземплярам этого класса. Они существуют независимо от количества созданных экземпляров класса и делятся ими всеми.
class MyClass {
public:
// Объявление статической переменной
static int count;
/* Увеличение счетчика при каждом создании объекта */
MyClass() { ++count; }
};
/* Определение статической переменной вне класса */
int MyClass::count = 0;
int main() {
MyClass obj1, obj2;
std::cout << MyClass::count << std::endl; /* Выведет 2 */
}Статическая функция-член класса — cтатические функции-члены класса могут обращаться к другим статическим членам класса, но не имеют доступа к нестатическим членам, так как они не связаны с каким-либо конкретным объектом.
class MyClass {
public:
static void printCount() {
std::cout << count << std::endl;
}
private:
static int count;
};
int MyClass::count = 0;
int main() {
// Вызов статической функции
MyClass::printCount();
}Статическая локальная переменная — локальные переменные, объявленные как static, сохраняются между вызовами функции, в которой они определены. Их начальное значение устанавливается только один раз, при первом входе в функцию.
void myFunction() {
/* Инициализируется только при первом вызове */
static int counter = 0;
counter++;
std::cout << "Counter: " << counter << std::endl;
}
int main() {
myFunction(); // Counter: 1
myFunction(); // Counter: 2
}Статическая глобальная переменная — глобальные переменные, объявленные как static, имеют внутреннюю область видимости, т.е. доступны только в пределах файла, в котором они определены, что помогает избежать конфликтов имен между файлами.
// file1.cpp
static int globalVar = 42;
// file2.cpp
extern int globalVar; /* Ошибка: globalVar недоступен извне file1.cpp */
Статическая функция — функции, объявленные как static, имеют внутреннюю область видимости и доступны только в пределах файла, в котором они определены, что предотвращает конфликты имен между разными файлами.
// file1.cpp
static void myStaticFunction() {
std::cout << "This is a static function." << std::endl;
}
// file2.cpp
extern void myStaticFunction(); // Ошибка: myStaticFunction недоступна извне file1.cpp
Статическое связывание — ключевое слово static также используется для указания способа связывания функций и переменных в контексте внешней и внутренней области видимости.
Внешнее связывание означает, что символ доступен во всем проекте, внутреннее — только в текущем модуле (файле).
// file1.cpp
// Внешняя область видимости
extern int externalVariable;
// Внутренняя область видимости
static int internalVariable;
Static Initialization Order Fiasco (SIOF) — проблема, возникающая при инициализации статических переменных, которые зависят друг от друга и находятся в разных единицах трансляции (трансляционных единицах, TU).
В C++ нет гарантии порядка инициализации статических переменных между разными трансляционными единицами, что может привести к неопределенному поведению.
Пример SIOF:
В этом примере возможны следующие сценарии:
— если file1.cpp инициализируется первым, то foo будет корректно создан до создания bar, и программа выполнится правильно.
— если file2.cpp инициализируется первым, то попытка обратиться к foo в конструкторе Bar приведет к неопределенному поведению, так как foo еще не был инициализирован.
Для устранения SIOF можно воспользоваться следующими методами:
Инициализация на уровне функций — переместить инициализацию статических переменных внутрь функций, чтобы гарантировать правильный порядок выполнения.
Использование шаблона Meyers Singleton — этот паттерн обеспечивает безопасную инициализацию и доступ к синглтону.
Использование динамической инициализации — вместо статической инициализации можно использовать динамическую, гарантирующую выполнение кода в нужном порядке.
Ключевое слово static в C++ обладает множеством применений, включая управление областью видимости, временем жизни и способом связывания переменных и функций.
Понимание всех возможностей static и знание о проблеме SIOF помогут вам писать более безопасный и предсказуемый код.
// file1.cpp
struct Foo {
Foo() { std::cout << "Foo constructed.\n"; }
};
// Статическая переменная в file1.cpp
Foo foo;
// file2.cpp
extern Foo foo;
struct Bar {
Bar() { std::cout << "Bar constructed. Using Foo: " << &foo << "\n"; }
};
// Статическая переменная в file2.cpp
Bar bar;
В этом примере возможны следующие сценарии:
— если file1.cpp инициализируется первым, то foo будет корректно создан до создания bar, и программа выполнится правильно.
— если file2.cpp инициализируется первым, то попытка обратиться к foo в конструкторе Bar приведет к неопределенному поведению, так как foo еще не был инициализирован.
Для устранения SIOF можно воспользоваться следующими методами:
Инициализация на уровне функций — переместить инициализацию статических переменных внутрь функций, чтобы гарантировать правильный порядок выполнения.
Foo& getFoo() {
static Foo instance;
return instance;
}
struct Bar {
Bar() { std::cout << "Bar constructed. Using Foo: " << &getFoo() << "\n"; }
};Использование шаблона Meyers Singleton — этот паттерн обеспечивает безопасную инициализацию и доступ к синглтону.
class Singleton {
public:
static Singleton& getInstance() {
static Singleton instance;
return instance;
}
private:
// Приватный конструктор
Singleton() {}
};Использование динамической инициализации — вместо статической инициализации можно использовать динамическую, гарантирующую выполнение кода в нужном порядке.
Foo* getFoo() {
static Foo* instance = new Foo();
return instance;
}
struct Bar {
Bar() { std::cout << "Bar constructed. Using Foo: " << getFoo() << "\n"; }
};Ключевое слово static в C++ обладает множеством применений, включая управление областью видимости, временем жизни и способом связывания переменных и функций.
Понимание всех возможностей static и знание о проблеме SIOF помогут вам писать более безопасный и предсказуемый код.
#333_Cpp_PkS
Что делает вызов throw; в блоке catch?
В языке C++ оператор throw; в блоке catch выполняет повторное выбрасывание исключения, которое было поймано этим блоком.
Это позволяет передать исключение дальше вверх по стеку вызовов, давая возможность другим обработчикам попытаться обработать его.
Пример, иллюстрирующий использование throw; в блоке catch:
В этом примере функция functionThatThrows выбрасывает исключение типа std::runtime_error.
Функция anotherFunction ловит это исключение, выводит сообщение об ошибке и повторно выбрасывает его с помощью throw;. В итоге исключение обрабатывается в функции main.
Зачем использовать throw;?
Логика обработки исключений — можно частично обработать исключение в одном месте программы, а затем передать его дальше для дополнительной обработки или завершения программы.
Разделение ответственности — различные уровни приложения могут отвечать за разные аспекты обработки исключений.
Например, низкоуровневый модуль может просто зафиксировать ошибку, а высокоуровневая логика решит, что делать дальше.
Отладка и журналирование — часто бывает полезно зарегистрировать факт возникновения исключения перед тем, как передать его дальше.
Важные моменты:
Тип исключения — когда вы используете throw; без указания конкретного исключения, оно сохраняет исходный тип исключения.
Это значит, что если в блоке catch вы поймали std::exception, то при повторном выбросе будет выброшено то же самое исключение, а не новое.
Без параметров — убедитесь, что после throw нет никаких параметров. Иначе это будет считаться созданием нового исключения, а не повторным выбросом старого.
Обработка исключений — всегда старайтесь обрабатывать исключения таким образом, чтобы минимизировать влияние на работу программы и предоставить полезную информацию пользователям или администраторам.
Оператор throw; в блоке catch позволяет повторно выбрасывать пойманное исключение, передавая его обработку на более высокий уровень.
Это инструмент для разделения обязанностей по обработке ошибок и обеспечения согласованного поведения программы в случае исключительных ситуаций.
Что делает вызов throw; в блоке catch?
В языке C++ оператор throw; в блоке catch выполняет повторное выбрасывание исключения, которое было поймано этим блоком.
Это позволяет передать исключение дальше вверх по стеку вызовов, давая возможность другим обработчикам попытаться обработать его.
Пример, иллюстрирующий использование throw; в блоке catch:
void functionThatThrows() {
throw std::runtime_error("Error in functionThatThrows");
}
void anotherFunction() {
try {
functionThatThrows();
} catch (const std::exception& e) {
std::cerr << "Caught exception: " << e.what() << std::endl;
/* Повторное выбрасывание исключения */
throw;
}
}
int main() {
try {
anotherFunction();
} catch (const std::exception& e) {
std::cerr << "Exception caught in main: " << e.what() << std::endl;
}
return 0;
}В этом примере функция functionThatThrows выбрасывает исключение типа std::runtime_error.
Функция anotherFunction ловит это исключение, выводит сообщение об ошибке и повторно выбрасывает его с помощью throw;. В итоге исключение обрабатывается в функции main.
Зачем использовать throw;?
Логика обработки исключений — можно частично обработать исключение в одном месте программы, а затем передать его дальше для дополнительной обработки или завершения программы.
Разделение ответственности — различные уровни приложения могут отвечать за разные аспекты обработки исключений.
Например, низкоуровневый модуль может просто зафиксировать ошибку, а высокоуровневая логика решит, что делать дальше.
Отладка и журналирование — часто бывает полезно зарегистрировать факт возникновения исключения перед тем, как передать его дальше.
Важные моменты:
Тип исключения — когда вы используете throw; без указания конкретного исключения, оно сохраняет исходный тип исключения.
Это значит, что если в блоке catch вы поймали std::exception, то при повторном выбросе будет выброшено то же самое исключение, а не новое.
Без параметров — убедитесь, что после throw нет никаких параметров. Иначе это будет считаться созданием нового исключения, а не повторным выбросом старого.
Обработка исключений — всегда старайтесь обрабатывать исключения таким образом, чтобы минимизировать влияние на работу программы и предоставить полезную информацию пользователям или администраторам.
Оператор throw; в блоке catch позволяет повторно выбрасывать пойманное исключение, передавая его обработку на более высокий уровень.
Это инструмент для разделения обязанностей по обработке ошибок и обеспечения согласованного поведения программы в случае исключительных ситуаций.