Как создать массив полиморфных объектов c

от admin

Как создать массив полиморфных объектов c

БлогNot. Лекции по C/C++: Классы, часть 3 (полиморфизм)

Лекции по C/C++: Классы, часть 3 (полиморфизм)

Полиморфизмом в общем смысле называется способность функции обрабатывать данные разных типов. Основные области применения полиморфизма в C++ следующие:

1. Перегрузка функций. C++ позволяет определять несколько функций с одинаковым именем в одном пространстве имён. Такие функции называются перегруженными и компилятор различает их по спискам параметров. Перегрузка позволяет использовать универсальные имена для выполнения однотипных действий.

В классическом Си поддержка полиморфизма ограничена, например, функция взятия модуля числа имеет разные названия в зависимости от типа своего аргумента – abs , fabs или labs . В C++ для совместимости эти имена сохранены, но можно и не помнить 3 различных названия функций, а пользоваться полиморфной функцией abs . Компилятор сам выберет, какая конкретная версия функции выполняется:

Приведём пример пользовательской перегрузки функции вывода значения объекта с именем print :

Для работы примера нужно подключить следующие стандартные заголовки:

Многие функции C++-библиотек, таких как .NET или QT, также имеют по несколько перегрузок, работающих с разными типами или разными списками аргументов.

2. Виртуальные методы базовых классов, которые переопределяются в производных классах. То есть, производные классы предоставляют свои собственные определения и реализацию некоего стандартного действия («установить», «нарисовать», «стереть» и т.п.). Во время выполнения, когда клиент вызывает виртуальный метод, выполняется поиск типа объекта и вызывается перезапись виртуального метода. В качестве примера приведём класс Number (число), имеющий потомков IntNumber (целое число) и DoubleNumber (вещественное число):

Как видно из кода, мы обращаемся с объектами разных классов (числами различных типов) похожим образом.

3. Перегрузка операторов в классах. Оператор в С++ — это некоторое действие или функция, обозначенная специальным символом. Для того чтобы распространять эти действия на новые типы данных, при этом сохраняя естественный синтаксис, в С++ была введена возможность перегрузки операторов.

Перегружать можно следующие операторы:

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

Для встроенных типов данных перегружать операторы также нельзя, потому что тип — это набор данных и операций над ними. Изменяя операции, выполняемые над данными типа, мы меняем само определение типа. Также нельзя изменить приоритет существующего оператора или определить новый оператор, отсутствующий в языке.

Переопределяя операторы » , » » && » » || » мы можем потерять их «ленивые» свойства (возможность не вычислять выражение до конца, например, true || условие == true , соответственно, после получения первого значения true условие перестаёт вычисляться).

Операторы » -> «, » [] «, » () «, » = » и » (type) » можно переопределить только как методы класса.

  • вместо синтаксиса get(i) удобнее использовать [i] ;
  • a.plus(b) или plus(a,b) удобнее заменить на a + b ;
  • вместо p.get()->print() удобнее использовать p->print() .

И т.д., например, в классе «матрица» удобно и естественно переопределить оператор » * » для умножения матриц, как сделано в Mathcad.

Следует понимать, что операторы делятся на унарные (применяемые к одному объекту, например, инкремент и декремент) и бинарные (имеющие два операнда, например, сложение или умножение). Единственную в C/C++ тернарную операцию условного оператора (… ? … : …) переопределить также нельзя.

4. Использование шаблонов. Шаблоны функций — это инструкции, согласно которым создаются локальные версии шаблонизированной функции для определенного набора параметров и типов данных. Например, без использования шаблонов для решения типовой задачи вывода в консоль элементов массива нам пришлось бы создать столько функций, сколько типов массивов мы обрабатываем:

Функции отличаются только типом параметров, такой подход крайне нерационален по затратам времени. С помощью ключевого слова template мы можем создать универсальный шаблон функции печати массива с любым типом элементов:

Компилятор сам создаст локальные копии функции-шаблона.

Все шаблоны функций начинаются со слова template , после которого идут угловые скобки, в которых перечисляется список параметров. Каждому параметру должно предшествовать зарезервированное слово class или typename :

Ключевое слово typename говорит о том, что в шаблоне будет использоваться встроенный тип данных, такой как int , double , float , char и т.д. А ключевое слово class сообщает компилятору, что в шаблоне функции в качестве параметра будут использоваться пользовательские типы данных, то есть, прежде всего, классы.

5. Контейнеры из разнотипных объектов. С помощью полиморфизма можно поместить объекты разных классов в один массив с типом базового класса. Например, создадим три класса с двумя методами – невиртуальный метод Info выводит информацию о животном, а виртуальный метод Say сообщает, что это животное «говорит». Виртуальный метод переопределён (override) в классах-наследниках. Невиртуальный метод просто скрыт в наследниках новой реализацией (невиртуальные методы нельзя переопределять).

При создании объекта будет иметь значение, в переменную какого типа записан объект:

А теперь частая ситуация, когда полиморфизм нужен — при создании массива объектов:

Ключевым в понимании полиморфизма является то, что он позволяет вам манипулировать объектами различной степени сложности путём создания общего для них стандартного интерфейса для реализации похожих действий (принцип «один интерфейс – множество методов«). В целом полиморфизм позволяет писать более абстрактные программы и повысить степень повторного использования кода.

  • создание списка, в который могут входить экземпляры как предка, так и потомка (потомков);
  • переопределение операторов в рамках класса.

В описание класса Student из предыдущей лекции добавим следующие элементы (файл student.h ):

Поля в разделе public:

Ключевое слово static в применении к свойству класса означает, что свойство существует в единственном числе для всех экземпляров класса (является статическим). Такие свойства можно использовать, например, как счётчики числа объектов или указатели на их списки.

Вне всех функций, то есть, глобально добавим в файл student.cpp инициализацию статических членов класса. Она выполняется именно таким образом, подобно инициализации глобальных переменных:

Переменная size здесь служебная и предназначена для инициализации максимального размера списка.

Опишем прототипы новых методов, также в разделе public базового класса (файл student.h ):

Функция print , как видно из листинга, также объявлена статической. В частности, это означает, что её можно вызывать без создания экземпляра класса кодом вида Student::print();

Наконец, предусмотрим в нашем классе переопределение операторов. Как сказано выше, переопределённый оператор понимается просто как перегруженная функция, а переопределять можно многие, но не все операторы C++.

При переопределении оператора его приоритет и порядок выполнения операций не меняются, но в ряде случаев действуют специальные правила написания переопределённой функции, например, чтобы отличать перегруженный постфиксный ++ от префиксного.

Общий вид переопределения оператора следующий:

Добавим в публичную секцию описания класса Student типичные примеры функций переопределения операторов:

Здесь мы переопределили унарный оператор ! (отрицание). Его результат — величина типа int , которая будет принимать значения 1 (истина) или 0 (ложь). Это соответствует обычной логике данного оператора. Параметры такой функции не требуются, так как она будет работать с текущим объектом this .

Прототип функции, переопределяющей оператор += , который не будет создавать нового объекта, а лишь изменять текущий, «прицепляя» строку-параметр к свойству Name текущего объекта. Поэтому тип функции указываем void . Это не значит, что нельзя было реализовать функцию += , возвращающую значение некоторого типа. В общем случае нужно помнить о вычислениях «по цепочке» и избегать операторов, которые ничего не возвращают. Например, наш оператор будет работать в синтаксисе

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

Переопределили бинарный оператор сложения, он получает ссылку на прибавляемый объект, стоящий справа от знака » + » ( Student & ) и возвращает новый объект, полученный в результате сложения. Объект слева от знака » + » доступен через this .

В ряде случаев экономичнее возвращать только ссылку на объект класса, а не сам объект ( Student & вместо Student ). Например, если данные изменяемого перегруженным методом экземпляра класса уже существовали до вызова метода, и, соответственно, не пропадут после выхода из него.

В публичной секции описания дочернего класса Hobbit также переопределим 2 операции — префиксный и постфиксный операторы ++ , правила их переопределения видны из кода:

Напишем реализацию новых и изменённых свойств и методов для обоих классов.

Метод add добавляет объект родительского или дочернего класса в единый список (с контролем предельного заполнения списка):

Удалять объект из динамического списка будет деструктор базового класса, соответственно, его реализация изменится:

Деструктор ссылается на новый метод search , который ищет фамилию студента student в текущем списке:

Статический метод печати выводит весь текущий список:

Закомментированное действие лучше выполнять с помощью оператора динамического приведения типа <dynamic_cast> (так как метод showStudent переопределён в дочернем классе и, вообще говоря, может быть перегруженным).

Наконец, напишем реализацию функций, переопределяющих стандартные операторы в наших классах.

Оператор ! мы приспособили для проверки того, есть ли объект, к которому он применяется, в списке:

Оператор += будет добавлять строку, переданную параметром, к фамилии студента:

Напомним, что метод setName должен вызываться только для созданного и проинициализированного объекта, иначе явное освобождение памяти, выполняемое оператором delete [] Name в этом методе, может привести к «преждевременному» удалению объекта, например, при неявном вызове конструктора копирования. Одним из решений проблем, связанных с такими трудноуловимыми ошибками, стала фактическая ликвидация деструкторов в языках Java, PHP и др. Возможность вызова деструктора в этих языках заменяется процедурой «сборки мусора» (garbage collection), периодически выполняемой системой.

Более корректное определение += , работающее по цепочке, было бы, например, таким:

Пример вызова такого метода:

«Сложение» студентов в нашем случае будет означать сцепление их фамилий и сложение номеров групп. Само по себе это действие бессмысленно, но оно иллюстрирует, как переопределять бинарный оператор:

Читать:
Tamper rtc что это

В классе-потомке Hobbit префиксный оператор ++ , который должен возвращать ссылку на объект, увеличивает на 1 код символа, соответствующего свойству Hobby :

Оператор постфиксного инкремента отличается тем, что имеет неиспользуемый параметр типа int . Оператор возвращает временный объект с прежним значением поля Hobby , но всё равно увеличивает значение Hobby текущего объекта this . Именно так будет обеспечена корректная работа постфиксного оператора, срабатывающего после вычисления выражения, в котором он встретился:

Продемонстрируем запрограммированные действия в main.cpp :

Пример 2. Определим небольшой класс комплексных чисел и проиллюстрируем на нём некоторые особенности переопределения операторов.

Пример 3. В классе Class , представляющем собой «обёртку» для обычного целочисленного значения, проиллюстрирована перегрузка таких операторов, как присваивание и круглые скобки, а также показана перегрузка операторов функциями-друзьями класса (на практике не рекомендуется перегружать «друзьями» любые операторы, меняющие состояние объекта).

Примечание. Оператор «круглые скобки» здесь просто для примера, в новых комплияторах может понадобиться другая обработка таких списков аргументов.

Пример 4, совсем простой. Шаблон для класса-массива из элементов любого скалярного типа (например, int, double, char)

Примеры проверены в бесплатной сборке Visual Studio 2010 Express или в Studio 2015.

How to make an array with polymorphism in C++? [duplicate]

I’m trying to create an array of 3 table class. The requirement is I have to use Base object to do that.

look fine to me. But when I tried to access each table, the compiler said it’s access violation. I don’t know what wrong with this code. I’m using Visual Studio 2010 though.

4 Answers 4

In C++, polymorphism and arrays don’t mix.

Since in general the size of the derived class is different to the size of the base class, polymorphism and pointer arithmetic don’t play together nicely. Since array access involves pointer arithmetic, expressions such as pBase[1] don’t work as expected.

One possibility is to have an array of pointers to your objects, perhaps even smart pointers to simplify memory management. But don’t forget to define a virtual destructor in Base1 .

You’re getting the error because the array is statically typed to Base1 . That means that this line:

adds the size of Base1 in bytes to pBase and interprets this as the beginning of another Base1 object, but this actually points someplace into the middle of the first table instance.

If you need an array of polymorphic instances, you must store them in the array (or preferably in a std::vector ) via pointers (or preferably some form of smart pointers).

Agnew’s response was spot on. Let me explain a little more. By augmenting your code, I print out the size of a Base1 and a table object as well as the addresses of the three table objects as they are created by the new operator:

As you can see those objects are spaced 12 bytes apart from each other in memory.

Now, let’s print out the addresses that pBase[0], pBase[1] and pBase[2] give:

Now look at what happens: the pointers we get back are spaced 8 bytes apart. This is because the pointer arithmetic is done on an pointer whose type is Base1 and since Base1 is 8 bytes longs what the compiler does is to translate pBase[n] into pBase + (n * sizeof(Base1)) .

Now you should be able to understand exactly why the first GetRow() works and why you crash on the second.

Как сделать массив с полиморфизмом в C++? [дубликат]

Я пытаюсь создать массив из 3 классов таблиц. Требование состоит в том, что для этого я должен использовать базовый объект.

хорошо выглядеть для меня. Но когда я пытался получить доступ к каждой таблице, компилятор сказал, что это нарушение прав доступа. Я не знаю, что не так с этим кодом. Я использую Visual Studio 2010, хотя.

4 ответы

В C++ полиморфизм и массивы несовместимы.

Поскольку обычно размер производного класса отличается от размера базового класса, полиморфизм и арифметика указателей плохо сочетаются друг с другом. Поскольку доступ к массиву включает арифметику указателя, такие выражения, как pBase[1] не работают должным образом.

Одна из возможностей состоит в том, чтобы иметь массив указатели на ваши объекты, возможно, даже интеллектуальные указатели для упрощения управления памятью. Но не забудьте определить виртуальный деструктор в Base1 .

ответ дан 05 дек ’12, 07:12

Вы получаете ошибку, потому что массив статически типизирован для Base1 . Это означает, что эта строка:

добавляет размер Base1 в байтах к pBase и интерпретирует это как начало другого Base1 объект, но на самом деле он указывает куда-то в середину первого table пример.

Если вам нужен массив полиморфных экземпляров, вы должны хранить их в массиве (или, что предпочтительнее, в std::vector ) с помощью указателей (или, что предпочтительнее, какой-либо формы интеллектуальных указателей).

ответ дан 05 дек ’12, 07:12

Ответ Агнью был точным. Позвольте мне объяснить немного больше. Увеличивая ваш код, я распечатываю размер Base1 и table объекта, а также адреса трех table объекты в том виде, в каком они создаются new оператор:

Как видите, эти объекты расположены в памяти на расстоянии 12 байт друг от друга.

Теперь давайте распечатаем адреса, которые дают pBase[0], pBase[1] и pBase[2]:

Теперь посмотрите, что происходит: указатели, которые мы возвращаем, разнесены на 8 байтов друг от друга. Это связано с тем, что арифметические операции с указателем выполняются для указателя, тип которого Base1 и с тех пор Base1 имеет длину 8 байт, что делает компилятор, так это переводит pBase[n] в pBase + (n * sizeof(Base1)) .

Теперь вы должны быть в состоянии понять, почему первый GetRow() работает, а почему вылетает на второй.

ответ дан 05 дек ’12, 08:12

и, тсссс.. но приведение к правильному типу, а затем вызов метода для приведенного типа предотвратит сбой — Johnathan

Ты имеешь в виду ((table)pBase)[1].GetRow() ? Это сработает, но это какой ужас что нужно сделать, и почти наверняка вернется и укусит вас за задницу. — Ник Бугалис

да я имею в виду именно это. Да, это довольно «ужасно», но вполне допустимый способ уговорить компилятор получить доступ к полиморфным объектам по правильным адресам. — Johnathan

спасибо, ребята, теперь я понимаю больше о поли — Калейдоскоп

@НикБугалис: ((table)pBase)[1].GetRow() не гарантируется работа (даже после исправления опечатки). Ты имеешь в виду dynamic_cast<table*>(pBase)[1].GetRow() ? Это будет работать. — Наваз

Массив полиморфных объектов

Я обычно сталкиваюсь с необходимостью создавать массивы или векторы полиморфных объектов. Я бы предпочел использовать ссылки, а не умные указатели, на базовый класс, потому что они, как правило, проще.

Массивам и векторам запрещено содержать необработанные ссылки, и поэтому вместо этого я использовал умные указатели на базовые классы. Тем не менее, есть также возможность использовать std::reference_wrapper вместо: https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper

Из того, что я могу сказать из документации, это одно из предполагаемых применений, но когда поднимается тема массивов, содержащих полиморфные объекты, обычно советуют использовать умные указатели, а не std::reference_wrapper ,

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

TL: DR; Почему умные указатели, такие как std::unique_ptr казалось бы, предпочтительнее std::reference_wrapper при создании массивов полиморфных объектов?

Решение

В очень простых сроках:

unique_ptr является владельцем объекта. Управляет временем жизни принадлежащего объекта

reference_wrapper оборачивает указатель на объект в памяти. Оно делает НЕ управлять временем жизни обернутого объекта

Вы должны создать массив unique_ptr (или же shared_ptr ) гарантировать освобождение объекта, когда он больше не нужен.

Другие решения

Если вы достаточно мотивированы, вы можете написать poly_any<Base> тип.

poly_any<Base> является any ограничивается только хранением объектов, которые получены из Base и обеспечивает .base() метод, который возвращает Base& к основному объекту.

Очень неполный набросок:

Тогда вам просто нужно перехватить все средства, чтобы положить вещи в poly_any<Base> и хранить get_base указатель на функцию:

Как только вы это сделали, вы можете создать std::vector<poly_any<Base>> и это вектор типов значений, которые полиморфно происходят от Base ,

Обратите внимание, что std::any обычно использует небольшую оптимизацию буфера для внутреннего хранения небольших объектов и больших объектов в куче. Но это деталь реализации.

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

Однако, как указатели и ссылки, reference_wrapper не управляет временем жизни объекта. Это то, что мы используем vector<uniq_ptr<>> а также vector<shared_ptr<>> for: для обеспечения правильного удаления указанных объектов.

С точки зрения производительности, vector<reference_wrapper<T>> должно быть так же быстро и эффективно использовать память vector<T*> , Но оба эти указателя / ссылки могут стать висящими, поскольку они не управляют временем жизни объекта.

Похожие статьи