Может ли быть массив свойством класса?
Если может быть, то как на примере массива строк это все можно организовать?
![]()
Малость добавлю от себя. Если вы пишете код в Visual Studio, то очень удобно пользоваться так называемыми снипетами. Для создания свойств существует несколько снипетов:
- наберите prop и дважды нажмите на клавишу Tab у вас появится заготовка свойства public int MyProperty < get; set; >между int и MyProperty можно переключаться с помощью все той же Tab . Так вот, когда вы воспользуетесь этим снипетом, то курсор у вас будет стоять в позиции int сразу начинайте вводить List<string> после нажмите Tab и вы сможете дать нужное вам название свойству, чтобы закончить нажмите клавишу ввода.
- propg даст public int MyProperty
![]()
как и для всего остального
![]()
из комментария @Vladislav Khapin
Дизайн сайта / логотип © 2023 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2023.3.13.43308
Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.
Выделение динамической памяти в конструкторе класса.
Деструктор класса, освобождение динамически выделенной памяти в деструкторе.
Динамически выделяемая память всегда требует особого внимания по сравнению с автоматической памятью.
Динамически создаваемые объекты и массивы объектов требуют операции выделения памяти, а в тот момент как этот объект или массив объектов становится ненужным, требуется явный вызов операции высвобождения выделенной памяти, в этом заключается принцип работы с динамической памятью, а именно весь процесс по управлению возлагается на программиста.
Если забыть выполнить выделение или высвобождение памяти, то это рано или поздно приведет к краху программы.
Работа с динамической памятью осуществляется через указатели, если забыть выделить память, то работая через не проинициализированный указатель можно попортить чужую память.
А если забывать освобождать динамически выделенную память на момент утраты необходимости в ней, то можно очень скоро получить невозможность выделить новую область памяти под другие объекты, так как вся доступная процессу память забита ненужными, висячими объектами или массивами.
Прежде чем вдаваться в подробности обработки динамических полей класса, вспомним как осуществляется выделение и высвобождение динамической памяти.
Оператор new выделяет запрошенное количество памяти и возвращает указатель на выделенную область памяти.
Оператор delete высвобождает память из под динамического объекта.
Оператор delete [] высвобождает память из под динамического массива.
Каких особых телодвижений требуют динамические поля класса?
Список того, что требуют динамические поля, следующий:
— в момент создания объекта-экземпляра класса, должно происходит выделение динамического блока памяти.
— в классе должно быть предусмотрено дополнительно поле в котором будет храниться размерность выделенного блока (обычно, количество элементов динамического массива, но иногда и количество байтов).
— в момент удаления объекта, связанная с ним динамическая память должна быть высвобождена.
Для реализации первого и второго пункта из представленного выше списка, необходимо в классе предусмотреть конструктор, в котором выполняется выделение требуемого объема динамической памяти и описать два обязательных поля: указатель на динамическую память и целочисленное поле для хранения кол-ва элементов динамического массива (поля).
Рассмотрим пример описания такого класса, класса SmartArray:
Динамическое поле необязательно должно быть типа char *, Вы вольны использовать любой другой тип.
Конструктор данного класса должен быть с параметром, это требуется по той причине, что при создании экземпляра, динамическому полю требуется знать сколько именно элементов ему хранить.
Конечно, есть возможность описать конструктор по умолчанию для данного класса, следующим образом:
В таком случае, динамическому полю присваивается значение nullptr (пустой указатель), а поле — размерность массива, обнуляется. Так как был описан конструктор по умолчанию, то для работы с экземплярами данного класса требуется предусмотреть метод для проверки динамического поля на пустоту, для этого и был реализован метод is_empty.
Осталось только предусмотреть правильное высвобождение памяти из под динамического поля в момент удаления экземпляра класса.
И как же перехватывать момент удаления экземпляра?
А для этого служит специальный метод — деструктор класса, он такой же особенный как и конструктор, его не нужно вызывать, он вызывается автоматом в момент удаления объекта и вызывается для конкретного экземпляра только один раз.
В данном методе описывается правильное высвобождение динамической памяти, если же все поля класс автоматические, то тело деструктора остаётся пустым.
Пример описания деструктора для нашего класса SmartArray:
Обратите внимание на синтаксис описания деструктора, у него не указывается тип возвращаемого параметра, имя деструктора состоит из символа тильда
и имени класса (без пробела между тильдой и именем класса), у деструктора список входных параметров всегда пуст.
Любой класс может содержать только один деструктор!
В самом деструкторе, как Вы можете наблюдать, содержится вызов метода delete[], который и выполняет высвобождение занятой памяти из под динамического поля.
Заметка
В описанном деструкторе класса можно было перед вызов оператора delete[] выполнить проверку: «а не является ли массив пустым?»:
Но этого не было сделано по причине того, что оператор delete[], вызванный для указателя равного nullptr, будет выполнен правильно, без формирования какой либо ошибки.
Мы рассмотрели все основные моменты обработки динамических полей, осталось научиться обращаться к отдельным элементам динамического поля.
Обращение к элементам динамического поля можно реализовать через метод?
Можно, но для этого лучше воспользоваться специальным оператором [].
Как ранее было сказано, класс позволяет описать тип в котором содержится набор полей, методов и операторов, так что, нет ничего сверхъестественного в описание оператора [] для нашего класса.
Необходимо знать следующее:
— в классе можно описать (переопределить) только поддерживаемые в С++ операторы, своих напридумывать нельзя
— переопределение операторов (в отличии от реализации пользовательских методов) производится четко, в соответствии с правилами стандарта С++
Синтаксис переопределения оператора [] следующий:
тип_элемента_массива& opeator[] (int index)* тело оператора */>
Оператор является функцией-членом класса, он возвращает ссылку на объект (на элемент массива), тип которого соответствует типу элемента динамического массива (поля).
Оператор [] принимает только один аргумент, данный аргумент может быть любого типа (int, size_t и т.д.) и обязан определять индекс целевого элемента динамического поля (массива).
При описании данного оператора, в его теле можно реализовать проверку выхода заданного (в качестве аргумента) индекса за границы массива, как это сделано в следующем примере:
Как Вы можете заметить, в класс добавилось дополнительное поле err_value, это поле хранит значение, которое будет возвращаться оператором [] в случае выхода индекса за пределы массива.
Никто не запрещает убрать проверку выхода за границы массива из оператора [], для данного примера это будет даже лучше, так как избавляет нас от описания костылика в виде дополнительного поля err_value, но при этом, проверку придется выполнять во внешних функциях, в которых осуществляется обработка экземпляров данного класса.
Не забудьте в данном классе реализовать метод size(), который будет возвращать кол-во элементов динамического поля.
Пример создания класса для работы с одномерными массивами
Для практики программирования Вам были предложены Задачи 2, содержащие работу со строками, одномерными и двухмерными массивами.
Полезно эти программы писать в стиле ООП, то есть задается некоторый исходный класс, назовем его ArrayIntOne, с использованием которого далее решаются поставленные задачи.
Вы можете добавлять в этот класс новые методы, либо, наследуя от него дочерний класс, создать новый.
Решение
1. Каждый класс будем сохранять как отдельный файл в общем пространстве имен, например, namespace ArrayIntOne, либо через подключение через директиву using.
2. Исходный класс назовем ArrayInt1, который будет содержать некоторые поля, конструкторы и методы. Естественно, что полями класса будут длина массива n и собственно одномерный массив a[]. Конструкторы класса необходимы для создания массивов разными способами (через ввод с клавиатуры, через генераторы случайных чисел). Методы класса — самые простые (задать или извлечь элемент массива, найти максимальный/минимальный элемент, отсортировать массив). тогда получим
Исходный класс — ArrayInt1
3. Предположим, что нам нужно изменить или что-либо добавить в этот класс. Но им уже пользуются в других проектах, поэтому эти изменения сделаем в дочернем классе, который назовем ArrayIntOne.Три конструктора оставим без изменений, добавим конструктор для чтения из текстового файла.
Пусть файл данных имеет вид:
7
2
4
1
7
6
5
8
где первое число задает количество элементов массива, а далее в каждой строке по одному записываются эти элементы. Присвойте файлу имя, например, M1.txt, разместите его в папке с программой (exe-файлом). Тогда при запросе имени файла Вам будет достаточно указать его имя без расширения — M1.
Для примера также выполним перегрузку метода Get_Element(int k). Тогда получим класс ArrayIntOne, как дочерний класс от ArrayInt1:
Наследование класса — ArrayIntOne
Замечу, что для чтения файла понадобились директивы
4. В решениях задач по основам C# вы часто использовали различные алгоритмы контроля ввода целых чисел. Ранее был написан класс Restricted Integer Number (RIN) — «Ограниченное целое число», который удобно использовать в нашей тестовой программе:
Класс RIN — ограниченное целое число
NEW: Наш Чат, в котором вы можете обсудить любые вопросы, идеи, поделиться опытом или связаться с администраторами.
Класс Object и массивы
Давайте обсудим допустимость преобразований между классами-массивами и классом Object . Понятно, что существует неявное преобразование объекта любого класса в объект класса Object , так что переменной типа оbject всегда можно присвоить переменную типа массив. Обратное такое преобразование также существует, но оно должно быть явным. Как всегда, при проведении явных преобразований не гарантируется успешность их выполнения.
В этой лекции и ранее обсуждался вопрос о создании универсальных процедур, которые могли бы работать с данными разных типов. Серьезный разговор об универсализации классов еще предстоит, сейчас же лишь напомню, что уже рассматривался такой прием, как перегрузка метода. У клиента, использующего перегруженный метод, создается впечатление, что он вызывает универсальный метод, работающий с аргументами разного типа. Создатель перегруженного метода должен, конечно, написать множество реализаций для поддержки такой универсальности. Другой уже обсуждавшийся прием состоит в том, что формальный аргумент метода принадлежит родительскому классу, тогда методу при вызове может быть передан аргумент любого из потомков.
Приведу в качестве примера многострадальную процедуру печати объектов, многократные варианты которой уже были рассмотрены. На этот раз формальный аргумент процедуры будет иметь тип оbject — прародителя всех классов. Разберем, как можно выяснить, что в процедуру передается массив, как определить его тип и работать с ним уже как с массивом, а не как с переменной класса Object . Вот текст этой процедуры, названной PrintObject :
public static void PrintObj(object A)
Console.WriteLine(«A.GetType()=<0>)», A.GetType()); if (A.GetType()==typeof(System.Int32[]))
int[] temp; temp = (int[])A;
for(int i = 0; i<temp.GetLength(0);i++) Console.Write(«\t temp[<0>]=<1>«, i,temp[i]);
Console.WriteLine(«Аргумент не является массивом целых»); >//PrintObject
Несколько замечаний к реализации.
Метод GetType , примененный к аргументу, возвращает не тип Object , а реальный тип фактического аргумента. Поэтому можно проанализировать, какому классу принадлежит объект, переданный в процедуру.
На каждой ветви разбора можно создать временный объект нужного типа и скопировать в него переданный аргумент. В данном примере рассматривается только одна ветвь, в которой создается целочисленный одномерный массив temp .
Заметьте, при присваивании значения переменной temp выполняется явное
преобразование из класса Object в класс Int[] .
При наличии переменной temp , выполнение нужных действий над массивом не представляет никаких трудностей.
Приведу два примера вызова этой процедуры:
//работа с процедурой PrintObject //Корректный и некорректный вызовы
Вот какой вывод порождается этим фрагментом кода:
Рис. 12.4. Результаты работы процедуры PrintObj
Массивы объектов
Во всех рассмотренных примерах этой главы нам встречались массивы, элементы которых имели только простые значимые типы. В реальных программах массивы объектов и других ссылочных типов встречаются не менее часто. Каков бы ни был тип элементов, большой разницы при работе с массивами нет. Но один важный нюанс все же есть, и его стоит отметить. Он связан с инициализацией элементов по умолчанию. Уже говорилось о том, что компилятор не следит за инициализацией элементов массива и доверяет инициализации, выполненной конструктором массива по умолчанию. Но для массивов ссылочного типа инициализация по умолчанию присваивает ссылкам значение Null . Это означает, что создаются только ссылки, но не сами объекты. По этой причине, пока не будет проведена настоящая инициализация с созданием объектов и заданием ссылок на конкретные объекты, работать с массивом ссылочного типа будет невозможно.
Рассмотрим детали этой проблемы на примере. Определим достаточно простой и интуитивно понятный класс, названный Winners , свойства которого задают имя победителя и его премию, а методы позволяют установить размер премии для каждого победителя и распечатать его свойства. Приведу код, описывающий этот класс:
/// Класс победителей с именем и премией
public class Winners
//поля класса string name; int price;
//статическое или динамическое поле rnd? //static Random rnd = new Random(); Random rnd = new Random();
public void SetVals(string name)
this.price = rnd.Next(5,10)* 1000; >//SetVals
public void PrintWinner(Winners win)
» его премия — <1>«, win.name, win.price); >//PrintWinner
Коротко прокомментирую этот текст.
1. Свойство name описывает имя победителя, а свойство price — величину его премии.
2. Свойство rnd необходимо при работе со случайными числами.
3. Метод SetVals выполняет инициализацию. Он присваивает полю name значение, переданное в качестве аргумента, и полю price — случайное значение.
4. Метод PrintWinner — метод печати свойств класса. Без подобного метода не обходится ни один класс.
5. В классе появится еще один статический метод InitAr , но о нем скажу чуть позже.
Пусть теперь в одном из методов нашего тестирующего класса Testing предполагается работа с классом Winners , начинающаяся с описания победителей. Естественно, задается массив, элементы которого имеют тип Winners . Приведу начало тестирующего метода, в котором дано соответствующее объявление:
public void TestWinners()
//массивы объектов int nwin = 3;
Winners[] wins = new Winners[nwin];
В результате создан массив wins , состоящий из объектов класса Winners . Что произойдет, если попытаться задать значения полей объектов, вызвав специально созданный для этих целей метод SetVals ? Рассмотрим фрагмент кода, осуществляющий этот вызов:
//создание значений элементов массива for(int i=0; i < wins.Length; i++)
На этапе выполнения будет сгенерировано исключение — нулевая ссылка. Причина понятна: хотя массив wins и создан, но это массив ссылок, имеющих значение null . Сами объекты, на которые должны указывать ссылки, не создаются в момент объявления массива ссылочного типа. Их нужно создавать явно. Ситуация аналогична объявлению массива массивов. И там необходим явный вызов конструктора для создания каждого массива на внутреннем уровне.
Как же создавать эти объекты? Конечно, можно возложить эту обязанность на пользователя, объявившего массив wins , — пусть он и создаст экземпляры для каждого элемента массива. Правильнее все-таки иметь в классе соответствующий метод. Метод должен быть статическим, чтобы его можно было вызывать еще до того, как созданы экземпляры класса, поскольку метод предназначен для создания этих самых экземпляров. Так в нашем классе появился статический метод InitAr :
public static Winners[] InitAr(Winners[] Winar)
for(int i=0; i < Winar.Length; i++) Winar[i] = new Winners();
Методу передается массив объектов, возможно, с нулевыми ссылками. Он возвращает тот же массив, но уже с явно определенными ссылками на реально созданные объекты. Теперь достаточно вызвать этот метод, после чего можно спокойно вызывать и метод SetVals . Вот как выглядит правильная последовательность вызовов методов класса
//создание значений элементов массива for(int i=0; i < wins.Length; i++)
wins[i].SetVals(winames[i]); //печать значений элементов массива for(int i=0; i < wins.Length; i++) wins[i].PrintWinner(wins[i]);
Теперь все корректно, массивы создаются, элементы заполняются нужными значениями, их можно распечатать:
Рис. 12.5. Печать элементов массива wins
Обратите внимание, что всем победителям назначена одна и та же премия. Хотя понятно, что дело в программной ошибке, но в ней можно видеть и знак свыше. Коль скоро для победителей выбраны такие имена, почитаемые всеми программистами, то негоже пытаться расставить их по ранжиру даже в примере.
Что же касается ошибки, то она связана с тем, что в данном случае свойство rnd следует сделать статическим, чтобы оно было одно на все экземпляры класса. В тексте описания варианта класса приведены оба варианта объявления свойства, один из которых закомментирован.
Массивы. Семантика присваивания
Преобразования между классами массивов и родительскими классами Array и Object уже рассматривались. А существуют ли другие преобразования между классами массивов? Что происходит при присваивании x=e ; (передаче аргументов в процедуру), если x и e — это массивы разных классов? Возможно ли присваивание? Ответ на этот вопрос положительный, хотя накладываются довольно жесткие ограничения на условия, когда такие преобразования допустимы. Известно, например, что между классами Int и Object существуют взаимные преобразования — в одну сторону явное, в другую неявное. А вот между классами Int[] и Object[] нет ни явных, ни неявных преобразований . С другой стороны, такое преобразование существует между классами String[] и Object[] . В чем же тут дело, и где логика? Запомните, главное ограничение на возможность таких преобразований состоит в том, что элементы массивов должны иметь ссылочный тип. А теперь притянем сюда логику. Крайне желательно обеспечить возможность проведения преобразований между массивами, элементы которых
принадлежат одному семейству классов, связанных отношением наследования . Такая возможность и была реализована. А вот для массивов с элементами значимых типов подобную же возможность не захотели или не смогли реализовать.
Сформулируем теперь точные правила, справедливые для присваивания и передачи аргументов в процедуру. Для того, чтобы было возможным неявное преобразование массива с элементами класса S в массив с элементами класса T , T необходимо выполнение следующих условий:
• классы S и T T должны быть ссылочного типа;
• размерности массивов должны совпадать;
• должно существовать неявное преобразование элементов класса S в элементы класса T . T
Заметьте, если S — это родительский класс, а T T — его потомок, то для массивов одной размерности остальные условия выполняются. Вернемся теперь к примеру с классами Int[] , String[] и Object[] . Класс Int не относится к ссылочным классам, и потому преобразования класса Int[] в Object[] не существует. Класс string является ссылочным классом и потомком класса Object , а потому существует неявное
преобразование между классами String[] и Object[] .
Правило для явного преобразования можно сформулировать, например, так. Если существует неявное преобразование массива с элементами класса S в массив с элементами класса T , T то существует явное преобразование массива с элементами класса T T в массив с элементами класса S .
Для демонстрации преобразований между массивами написана еще одна процедура печати. Вот ее текст:
public static void PrintArObj(string name,object[] A)
Console.WriteLine(name); foreach (object item in A )
Console.Write(«\t <0>«, item); Console.WriteLine();
Как видите, формальный аргумент этой процедуры принадлежит классу Object[] . При ее вызове фактическими аргументами могут быть массивы, удовлетворяющие выше указанным условиям. Вот пример кода, в котором вызывается эта процедура. В этом же фрагменте показаны и присваивания массива одного класса другому, где выполняются явные и неявные преобразования массивов.
public void TestMas()
string[] winames = <"Т. Хоар", "Н. Вирт", "Э. Дейкстра">; Arrs.PrintArObj(«winames», winames);
object[] cur = new object[5]; cur = winames; Arrs.PrintArObj(«cur», cur); winames = (string[])cur;
Arrs.PrintArObj(«winames», winames); >//TestMas
Взгляните на результаты работы этой процедуры.
Рис. 12.6. Семантика присваивания и преобразования массивов
Приступая к описаниям массивов, я полагал, что 10 страниц одной лекции будет вполне достаточно. Оказалось, что массивы C# более интересны. Надеюсь, с этим согласятся и читатели.