7375

Объектно-ориентированное программирование

Книга

Информатика, кибернетика и программирование

Объектно-ориентированное программирование Введение в объектно-ориентированное программирование Объектно-ориентированное программирование (ООП) - основная парадигма программирования 80-90-х годов, которая по всей видимости сохранится и в т...

Русский

2013-01-22

1.32 MB

28 чел.

Объектно-ориентированное программирование

  1.  Введение в объектно-ориентированное программирование

Объектно-ориентированное программирование (ООП) – основная парадигма программирования 80-90-х годов, которая по всей видимости сохранится и в течение текущего десятилетия. Она является результатом тридцатилетнего опыта и практики, которые берут начало в языке Симула 67 и продолжаются в языках Смолток, Эйфель, Турбо Паскаль, Объектный Паскаль, С++, Ява, C# и др. ООП – это стиль, который фиксирует поведение реального мира так, что детали его составных частей скрыты, и тот, кто решает задачу, мыслит в терминах, присущих самой задаче, а не языку программирования. Рассмотрим основные этапы развития программирования, которые помогают лучше понять взаимосвязь структурного подхода, модульного программирования и ООП.

  1.  Структурный подход в программировании

Первые программы для ЭВМ редко превышали объем 1 кбайт. С той поры существенно изменились архитектура и технические характеристики ЭВМ и чрезвычайно снизилась стоимость хранения пересылки и обработки 1 байта информации. Объемы используемых программ и программных систем измеряются десятками и сотнями мегабайтов. Вместе с тем удельная стоимость создания программ (нормированная объемом программы) в предыдущие годы менялась мало. Более того, с ростом объема программы удельная стоимость ее создания могла нелинейно возрастать. Было обнаружено, что время создания сложной программы пропорционально квадрату и даже кубу ее объема. Поэтому неудивительно, что одним из основных факторов, определяющих развитие технологии программирования, является снижение стоимости проектирования и создания программного продукта (ПП), или борьба со сложностью процесса программирования.

Другими важнейшими факторами, влияющими на эволюцию методов проектирования и создания ПП, являются:

• изменение архитектур ЭВМ в интересах повышения производительности, надежности и функциональности программ;

• упрощение взаимодействия пользователей с ЭМВ и интеллектуализация ЭВМ.

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

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

  1.  решаемая задача,
  2.  язык программирования,
  3.  среда выполнения программы,
  4.  процесс коллективной разработки и создания программы,
  5.  стремление к универсальности программы.

От свойства сложности нельзя избавиться, но можно изменять характеристики ее проявления путем организации процесса программирования и использования соответствующих средств программирования.

Сложность программы влияет также на ее способность отвечать ряду принципиальных требований к ней:

  1.  правильность (correctness): точное исполнение программой всех функций, изложенных в ее спецификации;
  2.  устойчивость (robustness): способность программы адекватно реагировать на непредвиденные ситуации (например, защита от дурака);
  3.  расширяемость (extendibility): легкость адаптации к постоянно возрастающим требованиям пользователя (заказчик никогда не знает с самого начала, что он хочет);
  4.  повторная используемость (reusability): способность многократно использоваться в различных приложениях;
  5.  совместимость (compatibility): способность легко сочетаться с другими программами;
  6.  эффективность (efficiency): способность исполняться быстро и оптимально использовать аппаратные ресурсы;
  7.  переносимость (portability): способность быть переносимой на другую аппаратуру или операционную среду;
  8.  легкость использования (ease of use): не предъявлять особых требований к квалификации пользователя.

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

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

Наряду с термином декомпозиция также используется термин структуризация проблемы или программы. Идея разделения программы на относительно самостоятельные крупные части, реализующие определенные ее функции и отражающие определенную иерархию взаимосвязей в программе, нашли отражение в структурном подходе к разработке и созданию программных систем. В программировании структурный подход появился с возникновением первых подпрограмм, процедур и функций, написанных в так называемом процедурно-ориентированном стиле программирования. Данный стиль опирается на простое правило: определить переменные и константы, представляющие обрабатываемые данные, и написать (или использовать существующие) процедуры (подпрограммы) их обработки. В этом стиле данные являются пассивными элементами программы, а процедуры – активными. Отсюда и название стиля.

Теоретическое оформление структурный подход получил в конце 60-х –т начале 70-х годов прошлого столетия в работах Э. Дейкстры, А.П. Ершова, П. Парнаса, Н. Вирта, Э. Йордана, Н. Вирта, Э. Брукса и других теоретиков и практиков программирования. Тогда же появилось и структурное программирование, в котором нашло отражение идея упорядочивания структуры программы путем использования небольшого количества структур управления с ограниченным использованием (или даже вообще без использования) оператора безусловного перехода (goto). Структурное программирование ориентирует программиста на составление программ, структура которых близка к дереву операторов и блоков. Использование структуры типа «дерево» в качестве своеобразного эталона объясняется тем, что она проста как для анализа, так и реализации. Типичным языком программирования, поддерживающим данный стиль программирования, является Паскаль.

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

Такой модуль, содержащий данные и процедуры их обработки, удобен для его автономной разработки и создания. Специализация модулей по видам обработки и наличие в них данных определенных типов – это свойства, которые отражают генетическую связь модульного программирования и ООП. Наиболее известные языки, поддерживающие модульное программирование – Модула-2, Ада и Оберон.

1.2. Роль типов данных в языках программирования

1.2.1. Понятие типа данных

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

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

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

Ранние языки программирования, такие как Фортран и Алгол-60, обеспечивали небольшой набор стандартных типов данных, необходимых для решения математических задач. Коренным образом ситуация изменилась на рубеже 70-х годов, когда применение ЭВМ вышло за рамки математических задач, что потребовало от языков программирования более мощных средств моделирования по сравнению со средствами стандартных типов данных. Поэтому такие языки программирования, как ПЛ-1, Алгол-68 и Паскаль, были оснащены механизмами конструкторов типов данных, позволяющими определять в программе необходимое количество типов массивов, типов записей, типов множеств, типов файлов и т.п.

Типы данных, порождаемые конструкторами типов, обычно называются структурными типами данных. Их использование позволяет упростить работу программиста при отображении структур обрабатываемых данных на структуры, предлагаемые языком программирования и тем самым сократить объем рутинной работы при написании программы. Важно отметить, что при работе со структурными данными конкретного типа данных программист пользуется операциями, предоставляемыми этим типом данных (например, выборка элемента по индексу в массиве или выборка поля по его имени в записи). Тем не менее ограниченное количество конструкторов типов не позволяет адекватно представить в программе любой необходимый тип данных с тем наборам операций, который естественно отражает семантику данного типа данных. Например, оба и тип даты, и тип лица могут быть представлены на Паскале типом записи и тогда программист будет работать с ними, используя операции типа записи. В то же время даты и лица – совершенно разные сущности с совершенно разным поведением (например, можно увеличить дату на единицу, а лицо сочетать браком с другим лицом), учет чего может существенно снизить сложность программы и усилить ее понимаемость.

Чтобы дать программисту возможность определять в программе нужные ему типы данных  (в дополнение к стандартным и структурным типам данных), в 70-х годах была разработана концепция абстрактных типов данных, обладающих интерфейсом и реализацией. Вся необходимая пользователю информация (имена операций, типы их параметров и типы результатов) сосредотачивалась в доступном ему интерфейсе, а детали реализации типа данных (представление его значений посредством значений известных типов данных и тела операций) были скрыты от него. Первым лабораторным языком, обеспечивающим механизм абстрактных типов данных является Клу (1974 год), а первым промышленным языком – Ада.

1.2.2. Этапы становления

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

В течение долгой истории типов данных в языках программирования несколько раз менялось их восприятие и, как следствие, их определение. Для ранних языков, таких как Фортран и Алгол-60, проблемы с типами данных не возникало – задание в описании языка всех типов данных, которые могли использоваться в программах, гарантировало от двусмысленных трактовок этого понятия. Однако уже с созданием Алгола 68 и Паскаля с их обширными (по тем временам) средствами конструирования потенциально неограниченного множества типов данных стало ясно, что простым перечислением их свойств обойтись нельзя. Возникли непростые вопросы эквивалентности типов данных (могут ли быть эквивалентными два типа записи в Паскале?), неявных приведений (можно ли переменной из одного диапазона присвоить значение из другого диапазона?), принадлежности значений тем или иным типам данных (принадлежит ли целое число отрезку целых?) и т. п. Поэтому появилась осознанная необходимость концептуального осмысления понятия типа данных, еще более усилившаяся в связи с созданием языков с абстрактными типами данных.

Первое определение типа данных, просуществовавшее в языках программирования примерно 15 лет звучало следующим образом: тип данных – это множество значений. Действительно, трудно не согласиться, что каждый тип данных определяет какое-то множество значений (иногда даже потенциально бесконечное). Но каким образом формируется это множество? Как убедиться, относится ли данное значение к данному типу данных? Чем характеризуются значения одного типа? Ни на один из этих вопросов первое определение типа данных ответа не дает.

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

Первое определение к типам данных было терпимо в те времена, когда язык программирования предоставлял своим пользователям встроенное в него ограниченное количество стандартных типов данных. В этом случае можно было задать реализацией языка множество значений каждого из стандартных типов и отдельно определить и реализовать необходимый набор операций над ними.

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

Первые попытки решения этой проблемы осуществлялись в рамках привычного подхода: множество значений отдельно, множество операций отдельно. Характерным примером тому является язык Алгол 68, в котором при необходимости образовать новый тип данных сначала отдельно представляется множество его значений посредством одного из простых или структурных типов данных, а затем при необходимости (но необязательно!) пишутся операции над данными этого типа. Такая же философия воплощена и в языка Паскаль. Оказалось, что такой подход к типообразованию имеет по меньшей мере три существенных недостатка.

1. Представление типа данных открыто пользователю. Вследствие этого он может использовать операции представляющего типа в дополнение к специально введенным операциям и тем самым совершить непредусмотренную работу с переменной данного типа. Рассмотрим в качестве простейшего примера организацию средствами Паскаля типа данных Counter с операциями plusone и minusone:

type Counter = Integer;

procedure plusone(var i: Counter) begin i := i +1 end;

procedure minusone(var i: Counter) begin i := i -1 end;

Теперь если мы опишем переменную

var c: Counter;

мы можем выполнять операции

plusone(c) и minusone(c).

Однако в тоже время никто не может нам запретить написать следующий оператор:

с := с + 5;

задав тем самым непредусмотренные действия со счетчиком.

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

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

Неудачи с продолжением традиционной линии (типы – множества значений) и привели к появлению упоминавшейся работы Морриса, после чего в трактовке типа данных стал утверждаться принцип конструктивизма: тип данных характеризуется не столько множеством значений, сколько множеством операций, т.е. для определения типа данных необходимо образовать множество операций, конструирующих и анализирующих его значения. Этим операциям, однако, необходим «строительный материал» для конструирования значений данного типа. Поэтому конструкция языка программирования, служащая для создания нового типа данных, конструктив типа данных, содержит указание данные какого (или каких) из существующих типов являются таким строительным материалом. Такое указание называется представлением типа данных, а используемые для этого типы данных – представляющими типами. Все операции нового типа данных описываются в терминах операций над данными представляющих типов, при этом как представление типа, так и реализация его операций скрываются (инкапсулируются) в его конструктиве таким образом, что на уровень использования выносится «абстрактное» описание операций, содержащее в простейшем случае имена и типы их аргументов и результатов.

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

В результате появилось чисто «операционное» определение: тип данных – это множество операций, задающих интерпретацию элементов универсального пространства значений. Это определение вводит в рассмотрение некое универсальное пространство значений и тем самым приводит к трудному вопросу: принадлежит ли каждое значение универсального пространства значений только одному типу данных или оно может принадлежать нескольким типам? В этом отношении любопытно замечание видного английского ученого Тони Хоара: «Размышлять о типах легче, если придерживаться следующего важного принципа: каждое значение, переменная и выражение принадлежат одному и точно одному типу. Конечно, я не могу доказать этот принцип. Просто все приходит в беспорядок, если вы его твердо не придерживаетесь».

Таким образом, Хоар считает необходимым рассматривать каждое значение принадлежащим только одному типу. Языки программирования, реализующее это свойство, называют мономорфными, а языки, допускающие возможность принадлежности одного значения разным типам данных – полиморфными. Такое разбиение языков является следствием наличия двух точек зрения на проблему, что первично: типы или данные. Вот как описывает ситуацию американский ученый П. Вегнер.

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

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

При конструктивном подходе к типам данных необходимость в операциях-конструкторах очевидна: именно они вырабатывают значения данного типа. Множество операций-анализаторов может быть в принципе пустым. Но тогда такой тип данных не будет представлять никакого интереса. Без возможности сравнивать значения данного типа между собой или анализировать их содержание (в случае со структурными значениями типа записей или массивов) их невозможно различать. Все они будут казаться одинаковыми. Поэтому единственная возможность различать значения какого-то типа данных – это снабжение его рядом операций-анализаторов (в частности, предикатов), показывающих поведение его значений по отношению к другим, известным типам данных (в частности, к Булеву типу).

В качестве примера рассмотрим тип натуральных чисел Nat с двумя операциями-конструкторами zero и succ и одним анализатором eqv. Операция zero является константой (частный случай конструктора), задающей первое значение типа Nat, а операция succ использует в качестве аргумента некоторое значение данного типа и вырабатывает другое значение того же типа. Таким образом, каждое значение типа Nat является результатом работы определенной последовательности конструкторов zero и succ. Анализатор eqv сравнивает два значения этого типа на равенство и вырабатывает результат Булевского типа (false или true). Сопоставляя эти операции с общепринятой нотацией натуральных чисел, можно сказать, что знак 0 обозначает значение, сопоставленное константе zero, 1 – значение, вырабатываемое последовательностью операций succ(zero), 2 – succ(succ(zero)), и т.д. Все возможные последовательности операций-конструкторов данного конструируют все множество его значений. При этом, как правило, никого не интересует, что является строительным материалом для значений (мы можем, конечно, знать, что в ЭВМ непосредственным строительным материалом для целых или вещественных чисел являются биты памяти, но в типизированных языках программирования мы обычно не используем это знание).

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

К сказанному выше следует добавить, что программист в своих программах на языке высокого уровня никогда не оперирует собственно значениями (ими оперирует ЭВМ, когда исполняется оттранслированная программа). Программист всегда оперирует выражениями, или термами, каждый из которых представляет собой композицию имен (знаков) операций тех или иных типов данных. Тип результата самой внешней операции в выражении определяет тип всего выражения. Таким образом, на уровне программы каждое правильно составленное выражение имеет один определенный тип, что позволяет компилятору произвести типовый контроль программы и тем самым проверить ее правильность с этой точки зрения.

  1.  Интерфейс и представление типа данных

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

Интерфейс типа данных предоставляет пользователю информацию об именах и профилях операций. Имя операции – это либо идентификатор, либо – специальный символ (знак). Профиль операции указывает имена типов аргументов и типа результата операции. Например, типичный интерфейс типа данных Integer выглядит следующим образом:

 type Integer =

 [ -_: Integer Integer;

 _+_, _-_, _*_, _/_, _mod_: Integer, Integer Integer;

 _=_, _<>_, _<_, _<=_, _>_, _>=_: Integer, Integer Boolean;

 sqv: Integer Integer]

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

 type Counter =

 [zero: Counter;

 plusOne, minusOne: Counter Counter;

 isZero: Counter Boolean]

 rep Integer;

 zero = 0;

 plusOne = func(var c: Counter): Counter { c := c+1};

 minusOne = func(var c: Counter): Counter { c := c-1};

 isZero = func(c: Counter): Boolean { result c = 0}

 end;

В данном примере значения типа Counter представляются целыми числами. Однако это представление скрыто от пользователя, и он не может манипулировать счетчиками как целыми числами. В то же время в реализации операций счетчики рассматриваются как целые числа, и операции над ними описаны в терминах операций над целыми числами.

Представление встроенного типа всегда выражается каким-то количеством байтов памяти. Поскольку представление пользовательского типа в конечном итоге может быть выражено в байтах, имеет смысл операция определения размера типа данных. Например, в С++ операция sizeof(T) вырабатывает количество байтов, занимаемых значением типа Т.

Как встроенные, так и пользовательские типы данных разделяются, в свою очередь, на конкретные и родовые (параметризованные) типы. Конкретный тип характеризует определенное множество значений, он не требует дальнейшего уточнения и готов к непосредственному использованию. Родовой тип характеризует множество множеств значений, он обладает параметрами и требует конкретизации путем подстановки аргументов. Фактически, родовой тип – это функция высокого уровня (типовая функция), порождающая типы данных с родственными интерфейсами. Другой вариант родового типа данных – это макрос, который в результате макроподстановки превращается в определение конкретного типа данных. По последнему варианту трактуются шаблоны в С++.

1.3. Причины появления объектного подхода

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

Человек обычно мыслит образами или объектами, он знает их свойства и манипулирует ими, сообразуясь с определенными событиями. Говорят, что еще древним грекам принадлежит мысль, что мир можно рассматривать в виде объектов и событий. Рене Декарт отмечал, что люди обычно имеют объектно-ориентированный взгляд на мир. Так, подумав о телефонном аппарате, человек может представить не только его форму и цвет, но и возможность позвонить, характер звучания звонка и ряд других свойств. Очевидно, что любое моделирование реальности будет тем проще и естественнее, чем лучше аппарат моделирования отражает характер восприятия реальности человеком. Из этого следует, что язык программирования как инструмент моделирования реальности и соответствующий стиль программирования будут тем успешнее, чем больше они удовлетворяют этому требованию.

Первые языки программирования ориентировались, с одной стороны, на математические величины, а с другой – на определенную модель вычислителя (ЭВМ архитектуры фон Неймана). Поэтому они содержали такие конструкции как переменная, процедура и функция. Как уже было сказано выше, программисты представляли свои программы в виде взаимодействующих активных подпрограмм (блоков, процедур, функций), обрабатывающих пассивные данные. Соответственно языки (Фортран, Алгол-60, Алгол-68, ПЛ/1, Паскаль, С и др.), поддерживающие этот стиль программирования, называются процедурно-ориентированными.

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

Моделируемый объект всегда представляется человеку чем-то единым, целостным, хотя он может состоять из частей или других объектов. Например, телефонный аппарат состоит из трубки, провода и корпуса с диском, а трубка состоит из микрофона, динамика, проводов и т.п. Целостное представление объекта в виде взаимосвязанной совокупности его компонентов и свойств является базовым принципом объектного подхода.

Объектный подход первоначально был сформулирован еще во второй половине 60-х годов в языке Симула-67, предложившим иерархическую классификацию объектов и наследование свойств объектов при их описании и построении. Однако объектно-ориентированный стиль программирования начал развиваться только в 80-х годах с появлением языка Смолток. Более поздние языки – Эйфель, Турбо Паскаль, Объектный Паскаль, С++, Ява, C# и др. Одной из причин сравнительно медленного становления объектно-ориентированного стиля программирования стало его существенное отличие от господствовавшего процедурно-ориентированного стиля.

  1.   Концепции объектно-ориентированного программирования

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

Такую декомпозицию задачи будем называть объектно-ориентированным анализом предметной области. Для описания результатов объектно-ориентированного анализа и последующего программного синтеза необходимы адекватные языковые средства, построенные на определенных принципах. Они являются составными частями двух взаимосвязанных дисциплин: объектно-ориентированного анализа и объектно-ориентированного программирования соответственно. В данном курсе изучается в основном вторая из них.

Основным понятием ООП является объект. Он обладает уникальным внутренним идентификатором, состоянием и рядом методов, служащих для анализа и изменения его состояния. Следующее важное понятие – класс однотипных объектов. И, наконец, третье фундаментальное понятие – наследование, служащее для отражения иерархии классов и объектов. По этой причине «объектно-ориентированный» обычно определяется как «объект + класс объектов + наследование».

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

1.4.1. Объекты и классы

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

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

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

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

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

самолет

 пассажирский самолет

 военный самолет

  истребитель

  бомбардировщик

  штурмовик

 транспортный самолет

 спортивный самолет

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

1.4.2. Инкапсуляция свойств объекта

Инкапсуляция (дословно «заключение в оболочку») представляет собой локализацию в рамках объекта всех данных об объекте, которые характеризуют его внутреннюю структуру и поведение с запрещением непосредственного доступа к тем данным, которые нецелесообразно (или даже опасно) предоставлять в распоряжение пользователя. Так, например, объект класса счетчик может содержать атрибут целого типа текущее_значение, и, по всей видимости, было бы опасно дать пользователю возможность менять это значение произвольным образом, добавляя к нему любое целое значение или вычитая его. Цитата из руководства по языку TURBO-Паскаль версии 5.5, в которой впервые появляется объектное расширение базового языка: "Яблоко может быть разломлено, но, как только вы это сделали, оно перестает быть яблоком. Отношения частей к целому и к другим частям более ясны, когда все связано вместе, в одной оболочке. Это качество называется   инкапсуляцией ...".

Связывая код и описания данных вместе, объект помогает упорядочить их. Если требуется получение значения одного из атрибутов объекта, то вызывается метод, принадлежащий этому объекту, который возвращает значение нужного атрибута. Разумеется, это возможно только тогда, когда такой метод представлен в описании объектного типа. Аналогично, чтобы задать значение атрибута, вызывается метод, который присваивает этому атрибуту новое значение.

В С++ атрибуты объекта называются элементами данных, или полями, а операции – компонентными функциями, или методами. В Яве и C# они соответственно называются полями и методами. Кроме того, понятие «атрибут» в C# имеет другой смысл. Поэтому в дальнейшем мы будем считать синонимами понятия элемент данных и поле, с одной стороны, и операция, компонентная функция и метод – с другой стороны. Компонентная функция, не вырабатывающая другого результата, кроме изменения состояния объекта, иногда называется процедурой.

Разные объектно-ориентированные языки программирования решают проблему инкапсуляции по-разному. Так, например, в Смолтоке все поля объекта (внутренние переменные) скрыты от пользователя и доступны только в телах методов данного класса, в то же время все методы объекта открыты пользователю. Более гибкая схема реализована в С++, Яве и C#. В этих языках каждый компонент объекта (поле или метод) может помечаться одним из спецификаторов доступа private, protected или public. Компонент, помеченный как private (скрытый), может использоваться только в телах методов данного класса, компонент, помеченный как protected (защищенный), может дополнительно использоваться в наследниках данного класса (т.е. в телах их методов), а компонент, помеченный как public (открытый), может использоваться в любом месте программы. Такое гибкое разграничение доступа к компонентам объекта позволяет избежать нежелательных искажений свойств объекта и допустить эффективный доступ к ним, когда это необходимо (прямой доступ к элементу данных объекта обычно более эффективен, чем доступ к нему посредством метода).

Доступ к открытым компонентам объекта осуществляется посредством одной или нескольких операций выбора компонента. Например, в С++ есть операции выбора «.» и «->». Левым аргументом первой операции является имя объекта, а второй – указатель на объект. В Яве и C# нет различия между этими понятиями, поэтому там есть только одна операция «.». Правый аргумент операции – это всегда имя компонента с возможными аргументами, если этот компонент – функция. Пример (С++).

Определение класса:

class Date {

 private int day, month, year;

 public void set_date (int d, m, y);

 public void increment_date ();

 public int get_day ();

 public int get_month ();

 public int get_year ();

}

Описание переменной и указателя:

Date datevar, *datepointer;

Обращение к компонентам объекта:

 datevar.set_date(1, 1, 2001);

 datevar.get_day();

datepointer->increment_date();

 datepointer->get_year();

Вызов метода данного объекта часто называют посылкой сообщения объекту. При этом сам объект называют получателем сообщения. В данном примере получатели сообщения – переменная datevar и указатель datepointer.

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

1.4.3. Наследование свойств

Наследование есть свойство классов порождать своих потомков и наследовать свойства (атрибуты и операции) своих родителей. Класс-потомок автоматически наследует от родителя все атрибуты и операции, а также может вводить новые атрибуты и операции и даже заменять (переопределять) операции родительского класса. Наследование в ООП позволяет адекватно отражать родственные отношения между объектами предметной области. Если класс В обладает всеми свойствами класса А и еще имеет дополнительные свойства, то класс А называется базовым (родительским), а класс Вподклассом (наследником) класса А.

Процесс построения иерархии классов очень напоминает процесс построения генеалогических деревьев в реальном мире. Наследование свойств только одного базового класса называется одиночным (простым) наследованием, а параллельное наследование свойств сразу нескольких базовых классов называется множественным (кратным) наследованием. В С++ возможны оба варианта наследования классов, а в Яве и C#  – только одиночное наследование, для реализации множественного наследования там применяется другой механизм.

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

Базовые классы, или корни подобных деревьев, часто обладают такими абстрактными свойствами, что сами в программах не используются (например, класс фигура), они необходимы лишь для порождения требуемых классов. Правильный выбор корня обеспечивает удобство «выращивания дерева», т.е. простоту построения библиотеки классов. Применение правила ООП «наследуй и изменяй свойства классов» хорошо согласуется с поэтапным подходом к разработке и созданию больших программных систем. Пример иерархии классов: фигура -> точка -> круг ->цилиндр.

1.4.4. Полиморфизм поведенческих свойств объектов

Свойство какого-либо имени программы одновременно обозначать различные сущности называется полиморфизмом этого имени. В буквальном переводе с греческого это слово означает "много форм". Понятие полиморфизма в языках программирования является одним из основополагающих, и совсем необязательно связывать его только с именами. Правомерно говорить о полиморфных знаках операций — встроенных и/или определяемых. К примеру, в операции "a + b" в зависимости от типов a и b знак операции «+» может обозначать операцию сложения целых, сложения вещественных, объединения множеств или сцепления строк (при желании можно считать, что их имя — знак "+"). Полиморфизм проявляется и в связи с локализацией имен: одно и то же имя из разных контекстов обозначает разные сущности, а неоднозначность понимания имени ликвидируется языковыми правилами соотнесения имени с контекстом (в различных языках возможны различные правила). Полиморфны элементы интерфейса программ – как технические (к примеру, клавиша мыши в зависимости от позиционирования ее курсора имеет разный смысл), так и программируемые (в частности, одинаковые световые кнопки на разных панелях обозначают различные действия). Если в определении полиморфизма отказаться от одновременности, то можно считать полиморфным понятие переменной, поскольку в разные моменты выполнения программы она содержит (обозначает) различные значения.

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

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

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

1.4.5. Типы объектов

Описание класса определяет поведение множества однотипных объектов, из-за чего класс можно назвать типом объектов, или объектным типом. Заметим разницу между типом данных и типом объектов: в то время как тип данных содержит операции по образованию одних значений на основе других значений и тем самым определяет множество значений и множество операций над ними, тип объектов не содержит операций по созданию объектов и потому определяет лишь поведение каким-то образом созданных объектов.

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

Идентификатором объекта обычно является его адрес в куче. В Яве и C# он называется ссылкой, а тип объектов – ссылочным типом. В дальнейшем, употребляя термин «тип», мы будем иметь в виду как тип данных, так и тип объектов.

1.5.5. Основные языки

Смолток (Smalltalk). Язык разрабатывался в Xerox Palo Alto Research Center (США) в течение 70-х годов. Было создано несколько версий: Смолток-72, Смолток-76 и Смолток-80. Считалось, что язык будет единственным средством программирования интерактивной системы программирования, в которой программы характеризуются высокой степенью модульности и динамической расширяемости. Типичный процессор Смолтока позволяет программисту бродить по обширной библиотеке описаний классов, создавать, редактировать, транслировать и отлаживать новые классы и конструировать программы из этих компонентов.

Смолток – очень чистый объектно-ориентированный язык: все оперируемые сущности в нем – объекты. Даже классы объектов являются объектами, а структуры управления рассматриваются как операции соответствующих классов. В результате Смолток использует минимум понятий, но синтаксис языка раздражающий.

С++. Язык был создан Американским ученым Бьярном Строуструпом в первой половине 80-х годов. Задачи:

  •  улучшить С
  •  поддержать абстракцию данных
  •  поддержать объектно-ориентированное программирование
  •  обеспечить максимально возможную эффективность работы программ.

Улучшение С выразилось в:

- контроль типов параметров

- операции работы с памятью (динамич. создание и уничтожение объектов)

- совмещение имен функций и операций

- встраивание функций в код программы.

С был взят в качестве базового языка, потому что он:

- многоцелевой, лаконичный и относительно низкого уровня

- отвечает большинству задач системного программирования

- идет везде и на всем

- может быть использован в среде программирования UNIX.

Абстракция данных есть метод разработки программ с представлением в ней понятий из прикладной области как пользовательских (user-defined) типов данных. Интерфейс типа данных (спецификация) отделяется от его реализации, что

- облегчает понимание программ, позволяя прямо описывать понятие в

 естественных для него терминах

- изменять реализацию типа данных, не задевая пользователей.

С++ обеспечивает абстракцию данных, поддерживая следующие понятия и возможности:

- конструктор пользовательских типов class

- средства управления доступом (public, private)

- абстрактные классы

- гарантированная инициализация и очистка объектов

- пользовательские преобразования типов объектов

- параметризованные (родовые) функции и типы данных

- обработка исключительных ситуаций.

Ключевым понятием в С++ является класс. Классы обеспечивают перечисленные выше возможности.

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

- производный класс (подкласс)

- виртуальная функция (подмена функций).

Концепция класса вместе с производными классами и виртуальными функциями была взята из языка Симула 67. Возможности совмещения имен и расположения описаний везде, где может встретиться оператор, взяты из языка Алгол 68. Родовые конструкции (generics) появились под влиянием языков Клу и Ада. Из Ады взят также механизм обработки исключительных ситуаций.

Ява (Java). Язык был разработан в середине 90-х годов в компании Javasoft, являвшейся оперативным подразделением фирмы Sun Microsystems. В настоящее время Ява является, возможно, самым надежным и простым в использовании объектно-ориентированным языком программирования. В качестве прообраза языка был выбран С++ как наиболее распространенный объектно-ориентированный язык программирования. Однако Ява существенно упрощен по сравнению с С++ путем избавления от указателей, ручного управления памятью, множественного наследования и многих других тонких вещей, необходимых для создания эффективных системных программ.

Достаточно простая и надежная структура Явы обусловлена в первую очередь другой ориентацией языка по сравнению с С++. Ява предназначен в первую очередь для независимого от платформы Интернет-программирования, не требующего высокой эффективности работы программ. С помощью этого языка можно создать приложение, которое может работать сразу на нескольких платформах: Windows, Unix или Macintosh. Основными характеристиками программ, написанных на Яве, являются:

независимость от архитектуры ЭВМ;

работа в распределенных средах;

динамическое исполнение;

интерпретация и компиляция;

многопоточность;

ориентация на сетевое использование;

объектная ориентация;

переносимость;

устойчивость;

безопасность.

C#. Это потомок С++ и Явы, являющийся простым объектно-ориентированным языком программирования, предназначенным для программирования в среде  

.NET. Язык в основном сохраняет философию и структуру Явы. Однако поскольку в отличие от Явы он ориентирован на создание эффективных программ, в нем присутствуют некоторые свойства, характерные для С++. Следующие основные цели преследовались при создании языка:

А. Создать первый компонентно-ориентированный язык в семействе С/С++. Современное программирование все меньше и меньше ориентируется на создание прикладных программ, работающих только на одном конкретном компьютере, и все больше смещается в сторону создания распределенных программ, состоящих из компонентов, передаваемых по сети и подключаемых по мере надобности. К языку, используемому для объединения этих компонентов в одну цельную программу, предъявляются столь специфические требования, что им не отвечает ни один из существующих языков программирования. Этот язык не должен отличаться особой сложностью, поскольку все задачи решаются на уровне компонентов, но должен обладать развитой системой диагностики и повышенным уровнем безопасности, так как программе приходится работать с объектами, совместная работа которых не гарантирована.

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

С. Создать максимально простой язык. Имеется в виду, что язык должен быть намного проще, чем С++.

D. Создать альтернативу Яве. На мой взгляд, это было основной задачей, так как практически все предыдущие задачи (кроме написания эффективных программ) решены в Яве. Поэтому в последующих главах C# будет в основном сопоставляться с Явой.

  1.   Этапы объектно-ориентированного программирования

Программа, решающая некоторую задачу, заключает в себе описание части мира, относящегося к этой задаче, или определенной предметной области. Описание действительности в форме взаимодействующих объектов во многих случаях естественнее, чем в форме иерархии подпрограмм, и поэтому оно облегчает программное моделирование такой предметной области.

В процессе программирования в объектно-ориентированном стиле можно выделить следующие этапы:

  1.  Определение основных  понятий предметной области и соответствующих им классов с соответствующими свойствами. Обоснование способов создания объектов.
  2.  Определение или формулирование принципов взаимодействия классов и взаимодействия объектов в рамках программной системы.
  3.  Установление иерархии взаимосвязи свойств родственных классов.
  4.  Реализация иерархии классов посредством механизма наследования.
  5.  Реализация методов каждого класса.

В результате такой работы создается объектно-ориентированная среда, или библиотека классов, позволяющая решать задачи моделирования в определенной предметной области. Первые три этапа являются объектно-ориентированным анализом предметной области, а последние два – собственно объектно-ориентированным программированием.

Основные достоинства ООП:

• использование более естественных понятий предметной области;

• простота введения новых понятий на основе существующих;

• отображение в библиотеке классов наиболее общих свойств и отношений между объектами моделируемой предметной области;

• естественность отображения пространства решаемой задачи в пространство объектов программы;

• простота внесения изменений в определения классов и программу в целом;

• упрощение составления и понимания программы благодаря полиморфизму;

• упрощение структуры программы вследствие инкапсуляции свойств и поведения объекта.

В качестве недостатков ООП можно отметить следующие:

• снижение быстродействия программ, особенно при необходимости позднего связывания;

• большие затраты на разработку библиотеки классов, поэтому ООП целесообразно применять при создании больших программных систем, а не при написании маленьких единичных программ;

• необходимость анализа всей иерархии классов для правильного использования свойств базовых классов.

Целесообразность применения технологии ООП для создания конкретной программной системы определяется двумя главными факторами:

• спецификой предметной области и решаемой прикладной задачи;

• особенностями используемой системы программирования.

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

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


  1.  Встроенные типы данных и объектов

2.1. Конкретные встроенные типы

2.1.1. С++

В С++ встроенные типы данных называются основными, или базовыми (fundamental). Имеется несколько основных типов и несколько их разновидностей. Они соответствуют наиболее общим и широко используемым основным аппаратным единицам памяти компьютера.

А) Логический тип

Данный тип (bool) определяет два значения, обозначаемые идентификаторами true и false. В типе нет собственных операций над этими значениями. Вместо этого задаются преобразования логических значений в целые числа и наоборот. Поэтому интерфейс этого типа можно представить следующим образом:

 type bool =

 [true, false: bool;

 int: bool int]

где int(false) = 0 и int(true) = 1. В большинстве случаев функция int используется неявно, т.е. в контексте, где требуется целое значение, логическое значение переводится в целое.

Б) Литерные типы

Значением типа литера (char) может быть любая литера из набора литер реализации в кодировке ASCII. На любом компьютере практически всегда литера представляется одним байтом, так что может существовать 256 различных литер. Можно с достаточной уверенностью предположить, что набор литер конкретной реализации включает в себя десятичные цифры, буквы латинского алфавита и основные знаки пунктуации, имеющиеся на клавиатуре.

Литерные значения (символьные литералы) обозначаются изображением соответствующей литеры в одиночных кавычках, например: ‘a’, ‘4’, ‘D’, ‘+’. Некоторые неграфические и специальные литеры изображаются с использованием маркировочного символа ‘\’:

новая строка (new line) NL (LF) \n

 горизонтальная табуляция (horisontal tab) HT \t

 вертикальная табуляция (vertical tab) VT \v

возврат на символ (backspace) BS \b

возврат каретки (carriage return) CR \r

 протяжка страницы (form feed) FF \f

внимание (alert) BEL \a

обратная косая черта (backslash) \ \\

вопросительный знак (question mark) ? \?

одинарная кавычка (single quote) ‘ \’

двойные кавычки (double qoute) « \»

восьмеричное число (octal number) ooo \ooo

шестнадцатиричное число (hex number) hhh \xhhh

Последовательности типа \ооо или \xhhh используется для задании литеры ее восьмеричным или шестнадцатиричным кодом.

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

int: char int,

которая вырабатывает целое число на основе кода данной литеры.

Однако при рассмотрении кода литеры как целого числа возникает проблема дальнейшей интерпретации этого числа: со знаком или нет? 256 значений, представимых 8 битами, можно интерпретировать как значения от 0 до 255 и как значения в диапазоне от –128 до 127. В С++ имеются два способа явного указания диапазона: тип signed char означает диапазон от –128 до 127, а тип unsigned char – диапазон от 0 до 255. К счастью, разница касается только значений вне диапазона 0-127, в который попадают все наиболее употребительные литеры. При уверенной работе только с этими литерами можно пользоваться просто типом char.

В) Целые типы

Тип целых чисел (int) определяет множество целых чисел, представимых на данной ЭВМ. Интерфейс этого типа данных выглядит следующим образом:

type int =

[_+_, _-_, _*_, _/_, _%_, _<<_, _>>_, _&_, _^_, _|_: int, int int;

 ~_, +_, -_: int int;

_==_, _!=_, _<_, _>_, _<=_, _>=_, _&&_, _||_: int, int bool;

 !_: int bool]

Операции «+», «-», «*», «/» и «%» служат для сложения, вычитания, умножения, деления и получения остатка от деления двух целых чисел.

Операции «<<» и «>>» осуществляют побитовый сдвиг левого операнда влево или соответственно вправо на количество разрядов, определяемое значением правого операнда, которое не может быть отрицательным. Операция «<<» освобождаемые биты всегда заполняет нулями. Операция «>>» заполняет нулями освобождаемые биты только в случае сдвига неотрицательных чисел; в противном случае результат зависит от реализации.

Результатами побитовых операций «&», «^» и «|» являются соответственно побитовое И, побитовое исключающее ИЛИ или побитовое обычное ИЛИ операндов.

Унарная операция «~» производит обратный код своего операнда (т.е. каждый бит операнда меняет свое значение на противоположное), а операции «+» и «-» имеют обычный смысл.

Операции «==», “!=», «<», «>», «<=», «>=» служат для сравнения двух чисел на равенство, неравенство, меньше, больше, меньше или равно, больше или равно соответственно.

Операции «&&», «||» являются соответственно логическими И и ИЛИ, проверяющими, отличны ли операнды от нуля. У первой операции второй операнд не вычисляется, если первый операнд равен нулю (результат false), а у второй операции второй операнд не вычисляется, если первый операнд отличен от нуля (результат true). Унарная операция логического отрицания «!» производит false если операнд отличен от нуля и true – в противном случае.

Существует три вида целых типов: short int (или просто short), int и long int (или просто long). Все они обладают одним и тем же интерфейсом, описанным выше. Разница между ними заключается в диапазоне представимых чисел. Представление зависит от реализации, но обычно длинные целые занимают не меньше памяти, чем короткие целые. Естественно, что короткое целое может быть преобразовано в более длинное целое. Поэтому мы можем считать, что в целом типе есть дополнительная операция

int: short int;

а в типе long – дополнительная операция

long: int long.

Кроме того, в типе short есть операция

short: char short;

преобразующая литеру в короткое целое.

Целые, объявленные как unsigned, служат для представления натуральных чисел. Поскольку int - это по умолчанию тип со знаком, знаковые целые есть просто синонимы обычных целых. Последовательное применение (обычно неявное) цепочки операций позволяет преобразовывать значение несколько раз. При этом цепочки безопасных преобразований выглядят следующим образом:

 signed char short int long;

 unsigned char unsigned short unsigned int unsigned long.

Типы char, int всех размеров, а также перечисления (2.4) называются целочисленными (integral) типами.

Целое число (константа) изображается последовательностью десятичных цифр, начинающейся не с нуля. Последовательность цифр без 8 и 9, начинающаяся с нуля, рассматривается как восьмеричное число. Последовательность цифр, начинающаяся с 0х или 0Х и возможно включающая буквы от А (а) до F (f), рассматривается как шестнадцатиричное число. Пример:

двенадцать:  12 014 0XC

Тип целой константы зависит от ее формы, значения и суффикса. Если она является десятичной и не содержит суффикса, то имеет первый из следующих типов, в котором может быть представлено ее значение:

 int,  long int,  unsigned long int.

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

 int,  unsigned int,  long int,  unsigned long int.

Если константа содержит суффикс u или U, она имеет первый из следующих типов, в котором может быть представлено ее значение:

 unsigned int,  unsigned long int.

Если константа содержит суффикс l или L, она имеет первый из следующих типов, в котором может быть представлено ее значение:

 long int,  unsigned long int.

Если константа имеет один из суффиксов ul, lu, uL, Lu, Ul, lU, UL, LU, ее типом является unsigned long int.

Отметим, что результат функции sizeof, определяющей размер (в байтах) объекта любого типа, имеет тип size_t, который является беззнаковым целочисленным типом, определенным в <cstddef>.

Г) Вещественные типы

Существует три вида вещественных типов: float, double и long double. Тип double обеспечивает не меньшую точность представления, чем float, а тип long double - не меньшую, чем double. Общий интерфейс всех трех типов (на примере типа float) выглядит следующим образом:

type float =

[_+_, _-_, _*_, _/_: float, float float;

 +_, -_: float float;

 _==_, _!=_, _<_, _>_, _<=_, _>=_, _&&_, _||_: float, float bool;

 !_: float bool]

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

 double: float double;

long_double: double long double.

Изображение вещественного числа состоит из целой части, десятичной точки и дробной части, литеры e или E, возможного целого показателя степени с возможным знаком и возможного суффикса. Либо целая часть, либо дробная часть (но не обе сразу) может быть опущена. Вещественное число имеет тип double, если только он не задан явно суффиксом. Суффиксы f и F задают тип float, суффиксы l и L задают тип long double. Примеры:

 double: 2.0,  2.,  0.2e1,   .2E1

 float: 2.0F,  20e-1F

 long double: 2.0L

В середине вещественной константы не может быть пробела. Например,

65.43 е-21 рассматривается как четыре отдельные лексемы:

 65.43  е  -  21

и считается синтаксической ошибкой.

Целочисленные и вещественные вместе составляют арифметические типы. Типичные размеры встроенных типов в реализации (в байтах):

bool 1

char 1

 short int 2

int 2 или 4

long int 4 или 8

float 4

double 8

long double 12 или 16

Во всех арифметических операциях с операндами разных типов, операнд более короткого типа всегда преобразуется (если, конечно, находится цепочка таких преобразований) в значение типа более длинного операнда и используется операция этого типа.

Д) Тип void.

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

Е) Тип выражение.

В некоторых операциях операндом является не значение определенного типа, а выражение определенного типа, которое вычисляется по мере надобности. Поэтому будем считать (хотя в языке этого в явном виде нет), что тип выражение Т определяет множество выражений типа Т. Типичной операцией, использующей в качестве операндов не только значения, но и выражения, является операция построения условного выражения. Обозначим через number любой арифметический тип. Тогда операция

_?_:_ : number, expression T, expression T T

выработает значение второго операнда, если значение первого операнда отлично от нуля, и третьего – в противном случае.

Выражение типа void обычно называется оператором. Таким образом, условное выражение становится оператором, если Т – это void.

2.1.2. Ява

В этом языке встроенные типы данных называются примитивными. К ним относятся следующие типы:

boolean true или false

char  16-разрядный символ в кодировке Unicode 1.1

byte  8-разрядное целое (со знаком)

short  16-разрядное целое (со знаком)

int  32-разрядное целое (со знаком)

long  64-разрядное целое (со знаком)

float  32-разрядное вещественное

double  64-разрядное вещественное

А) Логический тип

В отличие от С++ в Яве логический тип обладает всеми естественными операциями:

 type boolean =

 [true, false: boolean;

 _&&_, _||_, _&_, _^_, _|_ : boolean, boolean boolean;

 !_ : boolean boolean;

 _==_, _!=_ : boolean, boolean boolean]

Здесь знаки операций имеют тот же смысл, что и соответствующие знаки операций типа int в С++. При этом поразрядные операции отличаются от своих логических аналогов тем, что всегда вычисляются значения обоих операндов.

Б) Символьный тип

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

В) Целые типы

Типы byte и short не имеют собственных операций (они используются только для экономного хранения коротких целых чисел), и потому их значения всегда преобразуются в значения типа int перед выполнением любых вычислений. Интерфейс целого типа можно представить следующим образом:

type int =

  [_+_, _-_, _*_, _/_, _%_, _<<_, _>>_, _>>>_, _&_, _^_, _|_:

   int, int int;

 ~_, +_, -_: int int;

 _==_, _!=_, _<_, _>_, _<=_, _>=_: int, int boolean;

 int: byte int;

 int: short int]

В отличии от С++ здесь операция «>>» всегда заполняет освобождающиеся биты значением знакового разряда, а операция «>>>» – нулями. Все остальные операции имеют тот же смысл.

Интерфейс типа long выглядит аналогичным образом с добавлением операции перевода целых в длинные целые.

Изображаются целые числа практически так же как и в С++ (за исключением отсутствия суффиксов беззнаковых типов).

Г) Вещественные типы

Интерфейс типа float имеет следующий вид:

 type float =

 [_+_, _-_, _*_, _/_, _%_: float, float float;

 +_, -_: float float;

 _==_, _!=_, _<_, _>_, _<=_, _>=_ : float, float boolean]

Интерфейс типа double включает дополнительно операцию преобразования значения типа float в значение типа double. Как видим, по сравнению с С++ в вещественных типах допускается операция нахождения остатка от деления.

Изображаются вещественные числа так же как и в С++.

Д) Тип void имеет в Яве тот же смысл, что и в С++.

2.1.3. C#

В этом языке встроенные типы данных называются типами значений (value types), или размерными типами. Последний термин подразумевает, что данные этих типов имеют размер, который учитывается при отведении памяти под переменную.

Важными встоенными ссылочными типами являются object и string. При этом считается, что object является корнем иерархии всех типов, как размерных, так и ссылочных.

Ниже приводится список простых встроенных типов в C# и показано, как изображаются их литералы.

Тип

Описание

Пример

object

Корени иерархии всех типов

object o = null;

string

Строковый тип; строка – это последовательность символов в кодировке Unicode 

string s = "hello";

sbyte

8-разрядный байт со знаком

sbyte val = 12;

short

16-разрядное целое со знаком

short val = 12;

int

32-разрядное целое со знаком

int val = 12;

long

64-разрядное целое со знаком

long val1 = 12;
long val2 = 34L;

byte

8-разрядный байт без знака

byte val1 = 12;

ushort

16-разрядное целое без знака

ushort val1 = 12;

uint

32-разрядное целое без знака

uint val1 = 12;
uint val2 = 34U;

ulong

64-разрядное целое без знака

ulong val1 = 12;
ulong val2 = 34U;
ulong val3 = 56L;
ulong val4 = 78UL;

float

32-разрядное вещественное число

float val = 1.23F;

double

64-разрядное вещественное число

double val1 = 1.23;
double val2 = 4.56D;

bool

true или false

bool val1 = true;
bool val2 = false;

char

16-разрядный символ в кодировке Unicode

char val = 'h';

decimal

128-разрядное число в двоично-десятичном коде

decimal val = 1.23M;

Операции перечисленных типов данных аналогичны операциям соответствущих типов данных Явы.

2.1.4. Понятие объекта в С++, Яве и C#

Во всех этих языках объект – это данное, размещенное в памяти и снабженное адресом. Этот адрес становится в дальнейшем уникальным идентификатором объекта (в Яве и C# он называется ссылкой), а данное, размещенное в данный момент по этому адресу – его состоянием. Типы объектов в Яве и C# называются ссылочными типами (reference types), и переменная такого типа всегда содержит либо ссылку на объект, либо специальное значение null. В С++ для реализации такого способа работы с объектами необходимо применять механизм указателей.

Следует отметить, что в C# значение размерного типа также может быть преобразовано в объект и обратно в значение. Этот механизм называется упаковкой (boxing) и распаковкой (unboxing). Любой объект считается экземпляром встроенного класса Object, который является корнем иерархии классов. Таким образом, если описана переменная х типа Object и ей присваивается значение переменной размерного типа, то будет образован объект с данным значением в качестве его состояния и переменной х будет присвоена ссылка на этот объект. Пример:

int x = 42 // переменная размерного типа

object bar = x // переменная х упаковывается в bar

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

int y = (int)bar // распаковка и приведение к типу int

В С++ адресом также обладает функция. Выражение, вырабатывающее адрес объекта или функции, принято называть l-значением (l-value). L-значение, если это не имя функции, массива или константы, модифицируемо и может быть употреблено в левой части оператора присваивания. Все, что не обладает адресом, называется r-значением (r-value). Естественно, что l-значение может употребляться везде, где требуется r-значение (используется объект, расположенный по данному адресу).

В дальнейшем мы обозначаем l-значение типа Т как l-value Т, а модифицируемое l-значение типа Т как ml-value T.

  1.   Производные типы данных

В С++ существует потенциально бесконечное число производных типов, полученных, как правило, конкретизацией родовых встроенных типов посредством основных типов следующими способами:

 массивы (arrays) объектов заданного типа;

 перечисления (enumerations) конкретных значений;

 функции (functions) с параметрами заданных типов, возвращающие значение

 указанного типа;

 указатели (pointers) на объекты или функции заданного типа;

 ссылки (references) на объекты или функции заданного типа;

 константы (constants), являющиеся значениями заданного типа;

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

 разных типов, набор функций для работы с этими объектами и

 набор ограничений на доступ к объектам и функциям;

 структуры (structures), которые являются классами без ограничений на доступ

 к объектам и функциям-членам структуры;

 объединения (unions), являющиеся структурами, способными содержать

 объекты разных типов в разные моменты времени;

 указатели на компоненты класса (pointers to class members), которые

 идентифицируют компоненты заданного типа среди объектов данного

 класса.

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

В C# есть типы массивов, похожие на типы массивов в Яве, и два производных типа: перечисления и структуры.

В данном разделе мы рассмотрим производные типы в С++ и C# и массивы в Яве и C#.

2.4.1. Перечисления

В С++ тип перечисления, построенный посредством конструктора enum, задает набор значений, определяемый пользователем. После определения, перечисление рассматривается как отдельный целочисленный тип с поименованными константами. Его имя становится именем-перечисления, т.е. зарезервированным словом в своей области действия. Пример:

enum color {red, orange, yellow, green, blue};

Этот тип задает пять целых значений, к которым применимы все операции целого типа.

Идентификаторы в списке перечисления рассматриваются как константы и могут употребляться везде, где требуются константы. По умолчанию первой константе в списке перечисления присваивается значение 0, а значение каждого следующего элемента увеличивается на единицу. Эта процедура может быть изменена путем явной инициализации элементов перечисления: элемент перечисления со знаком «=» и последующим выражением придает идентификатору указанное значение; последующие идентификаторы без «=» продолжают прогрессию от присвоенного значения. Это значение должно быть целым или неявно приводимым к целому. Пример:

 enum { a, b, c = 0 };

enum { d, e, f = e+2 };

Здесь определено, что a, c и d равны 0, b и e равны 1, f равно 3.

Заметим, что разным элементам перечисления может быть присвоено одно и то же значение (т.е. они являются синонимами).

Имена элементов перечисления должны быть отличны от имен обычных переменных и других элементов перечисления с той же областью действия. Например, при наличии определения color следующее определение

enum fruit {apple, pear, orange, kiwi};

будет неверным из-за повторного описания константы orange.

 

Фактически перечисление определяет несколько целочисленных констант, которые могли бы быть определены и явным образом. Например,

enum { a, b, c = 0 };

эквивалентно

 const int a = 0;

const int b = 1;

 const int c = 0;

Таким образом, перечисление не является «чистым» типом данных в смысле трактовки, введенной в разделе 2.1. Более того, перечисление может даже не быть типом, а лишь блоком, вводящем несколько констант. Тем не менее, когда задается конкретный тип перечисления, для него могут быть определены дополнительные операции.

В C# определение перечислимого типа также вводится посредством конструктора enum, за которым следуют имя типа, указание доступности, базовый тип и имена значений определяемого типа согласно следующему систаксису:

enum-declaration:
enum-modifiers
opt   enum   identifier   enum-baseopt   enum-body   

enum-base:
:   integral-type

enum-body:
{   enum-member-declarationsopt   }
{   enum-member-declarations   ,   }

У каждого типа перечисления есть базовый тип.   Им может быть один из следующих интегральных типов: byte, sbyte, short, ushort, int, uint, long или ulong. Если базовый тип не указан, им по умолчанию становится тип int. Пример:

enum Color: long
{
Red,
Green,
Blue
}

В определении типа могут использоваться один или несколько следующих модификаторов (enum-modifiers): new, public, protected, internal, private.Они имеют тот же смысл, что и соответствующие модификаторы классов (будут рассмотрены позже).

Тело (enum-body) определения типа перечисления содержит имена элементов этого типа, которым могут задаваться константные значения базового типа. Как и в С++, несколько элементов могут иметь одно и то же значение, например:  

enum Color
{
Red,
Green,
Blue,

Max = Blue
}
.

Если элемент не инициализирован, ему присваивается значение на единицу большее значения предыдущего элемента (если первый элемент не инициализирован, ему присваивается ноль базового типа). Таким образом, тип перечисления в этом языке является расширением типа перечисления в С++.

2.4.2. Указатели

Понятие указателя в С++ ничем не отличается от аналогичного понятия в процедурных языках; при этом допускается большее количество операций над указателями. Интерфейс типа указателя на объекты типа Т в С++ можно представить следующим образом:

 type pointer T =

 [ new: pointer T;

 int: pointer T int;

 &_ : l-value T pointer T;

 *_: pointer T l-value T;

 _[_] : pointer T, int l-value T;

 _+_, _-_ : pointer T, int pointer T;

 _-_ : pointer T, pointer T int;

 !_ : pointer T bool;

 _==_, _!=_, _<_, _>_, _<=_, _>=_, _&&_, _||_:

  pointer T, pointer T bool]

Операция new порождает новый объект типа Т в свободной (динамической) памяти и вырабатывает значение указателя на него.

Операция int позволяет преобразовать значение указателя в целое число.

Операция «&», примененная к объекту или функции, вырабатывает адрес этого объекта в качестве значения указателя.

Операция «*» производит разыменование указателя, т.е. поставляет объект, указываемый данным указателем.

Операция индексации «_[_]» служит для доступа к элементам массива. Выражение p[i] аналогично выражению *(p + i), т.е. значение указателя увеличивается на величину индекса, и результат разыменовывается.

Операции «+» и «-» соответственно добавляют целое число к указателю и вычитают его из него. Как правило, указатель-операнд указывает на элемент массива, результат также будет указывать на элемент массива, находящийся выше или ниже данного указателя на количество позиций, задаваемое вторым операндом (т.е. если p указывает на, скажем, 7-й элемент массива, то p+5 будет указывать на 12-й элемент массива, а p-2 – на 3-й). Результат не определен, если результирующий указатель выходит за пределы массива.

Вторая операция «-» дает разность двух указателей на объекты одного и того же типа; результатом является целое со знаком, представляющее количество объектов между этими двумя указателями. Значение результата не определено, если указатели не нацелены на элементы одного и того же массива.

Операция «!» выработает значение false, если указатель не нулевой, и true в противном случае.

Остальные операции имеют тот же смысл, что и для целых чисел.

Тип указателя на объекты типа Т в С++ изображается как Т*. Например:

 char c = ‘a’;

char* p = &c;

К сожалению, запись указателей на массивы и функции сложнее (будет рассмотрена ниже).

В Яве и ядре C# указателей нет. Однако, в С# возможно объявление и использование указателей С++ в так называемых «небезопасных» (unsafe) участках текста программы, помеченных служебным словом unsafe.

2.4.3. Массивы и строки

В С++ для данного типа T тип T[size], где size – константное выражение, есть тип «массива из size элементов типа Т».

Выражение size задает число элементов массива. Если оно равно N, массив имеет N элементов, пронумерованных от 0 до N-1.

Массив может состоять из элементов любого основного типа кроме void, из указателей некоторого типа, из указателей на компоненты класса, из объектов класса, из элементов типа перечисления или из массивов какого-либо типа.

Когда несколько спецификаций вида «массив из» являются смежными, создается многомерный массив (массив массивов); при этом константные выражения, задающие границы массива, могут быть опущены для первой размерности.

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

float fa[17], *afp[17];

объявляют массив чисел типа float и массив указателей на числа типа float.

static int x3d[3][5][7];

объявляет статический трехмерный массив целых чисел размером 3*5*7.

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

В С++ невозможно описание размерностей многомерных массивов через запятую, как это делается в большинстве языков программирования. Поэтому ошибочно объявление

 int bad[5,2];

По этой причине невозможно также обращение к элементам массива путем перечисления значений индексов через запятую, т.е. следеут писать

x3d[2][3][1]

вместо

x3d[3,5,7]

Указатели и массивы в С++ связаны очень тесно. По сути дела в С++ нет собственно типа массива со своими операциями. Имя массива всегда можно использовать как указатель на свой первый элемент. Когда к указателю р типа Т* применяется арифметическая операция, предполагается, что он указывает на элемент массива из Т; р+1 означает следующий элемент массива, а р-1 - предыдущий элемент.

Продолжая изучать массивы и указатели, рассмотрим конструкцию

 type* имя;

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

long arlong[] = {100,200,300,400}; //описали и инициализировали массив

long* arlon = arlong;  // описали указатель и связали его с массивом

int* arint = new int[4];//описали указатель и связали с ним участок памяти

Имя массива является константой, потому присваивание массиву невозможно.

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

 int v1[] = {1, 2, 3, 4};

char v2[] = {'a', 'b', 'c'}

вводят массивы типов int[4] и char[3] соответственно, а описание

int v3[10] = {1, 2, 3}

вводит массив типа int[10], в котором первые три компонента инициализированы, как указано, а остальные – нулями.

Вложенные фигурные скобки можно опускать:

 float y[4][3] = {

 {1, 3, 5},

 {2, 4, 6},

 {3, 5, 7},

};

эквивалентно

 float y[4][3] = {1, 3, 5, 2, 4, 6, 3, 5, 7};

В обоих случаях y[3] инициализируется нулями.

Указатель на массив описывается посредством символа «*», помещенным перед именем массива в скобках, например:

int (*vp)[10]; // указатель на массив из 10 целых

Отсутствие скобок в данном описании трактовалось бы как описание массива из 10 указателей на целые числа.

В Яве тип массива является одним из встроенных классов. Это означает, что каждый массив является объектом, снабженным собственным внутренним идентификатором. При объявлении переменной-массива размер не указывается (переменная может в принципе ссылаться на массив любого размера). Количество элементов конкретного объекта-массива задается при его создании операцией new и не может измениться в дальнейшем. Например,

int[] ia = new int[3];

объявляет переменную ia как массив целых, создает массив из 3-х целых и присваивает идентификатор этого массива переменной. Заметим, что в Яве квадратные скобки, указывающие, что определяется массив, обычно следуют сразу за типов компонентов массива в отличие от С++, где они следуют за именем массива (хотя возможна и такая запись); это дает возможность четкого разделения типа и имени во всех контекстах.

Как и в С++, первый элемент массива имеет индекс 0, а последний – размер-1. Основная операция над массивами – выбор элемента по индексу. При использовании индекса, выходящего за эти пределы, возбуждается исключение IndexOutOfBounds. Размер массива может быть получен из значения поля length.

При создании многомерных массивов (т.е. массив массивов) обязательно указывать первую размерность, следующие могут указываться по мере надобности. Таким образом, массив может состоять из массивов разных размеров. Пример:

float [][] mat = new float[4][];

Инициализируются массивы в Яве так же, как и в С++. Обращение к элементам массива массивов также производится так же, как и в С++.

Все сказанное относительно массивов в Яве справедливо и для C#. Дополнительной особенностью в этом языке является возможность объявлять многомерные массивы. Примеры:

 int [,] m1;   // двумерный массив

 int [] [,,] m2 // массив трехмерных массивов

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

 m1[2,3]

 m2[0][3,2,1]

При инициализации многомерного массива используются вложенные друг в друга агрегаты, как в С++, например:

 int[,] b = {{0, 1}, {2, 3}, {4, 5}, {6, 7}, {8, 9}};

Массив литер в С++ называется строкой, и он может инициализироваться литерной строкой. Литерная строка представляет собой последовательность из одной или нескольких литер, заключенных в двойные кавычки, например, «...». В реализации строка всегда заканчивается дополнительной литерой ‘\0’. Размер строки равен числу ее литер, включая завершающий нуль. Например,

sizeof("asdf") == 5;

Пустая строка записывается как "" и имеет тип char[1]. В следующем примере

char msg[] = "Syntax error on line %s\n";

поскольку ‘\n’ - это одна литера и добавляется завершающий 0, вводится массив типа char[25].

В Яве и C# строки изображаются так же как и в С++. Однако в отличие от С++ массив литер не является строкой и не содержит завершающей литеры ‘\0’. В стандартных библиотеках С++ и Явы имеются классы строк (string и String соответственно), которые обеспечивают разнообразные операции над строками. В частности оба языка обеспечивают операции конкатенации (обозначаются символом «+») и сравнения строк на равенство.  В C# строки – элементы ссылочного типа string, который представляет собой класс, аналогичный соответствующему классу Явы. Подробно эти классы будут рассмотрены позже.

2.4.4. Константы

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

Чтобы объявить тип константным в С++, необходимо поставить перед ним ключевое слово const. Так как константе нельзя присваивать значения, определение константного объекта должно сопровождаться его инициализацией.

Например, определения

 const int ci = 10; const int *pc  = &ci; const int *const cpc = pc;

int i; int *p; int *const cp = &i;

объявляют:

1) ci как целую константу,

2) pc как указатель на целую константу (эквивалентно const int *pc  = &ci),

3) cpc как константный указатель на целую константу,

4) i как целую переменную,

5) p как указатель на целую переменную (эквивалентно int *p),

6) cp как константный указатель на целое (эквивалентно int *const cp = &i).

Значения ci, cp, cpc не могут быть изменены после инициализации. Значение pc может быть изменено, как и значение объекта, указываемого cp. Примеры допустимых действий:

 i = ci;  *cp = ci;   pc++;  pc = cpc;  pc = p;

Примеры недопустимых действий:

 ci = 1;  ci++;  *pc = 2;  cp = &ci;  cpc++;  p = pc;

Наличие типа константы const позволяет описать четыре варианта типов указателей. Примеры:

 char c, c1;

const char d = 'x';

 

char* pv; // переменный указатель на переменную

 pv = &c; // OK

*pv = 'z' // OK

 const char* pc; // переменный указатель на константу

pc = &d // OK

*pc = 'z' // ошибка: присваивание константе

char *const cp = &c; // константный указатель на переменную

cp = &c1; // ошибка: присваивание константе

*cp = 'y'; // OK

const char *const cpc = &c; // константный указатель на константу

cpc = &d; // ошибка: присваивание константе

*cpc = 'a' // ошибка: присваивание константе

Указателю на константу можно присваивать адрес переменной, потому что вреда от этого быть не может. Например:

pc = &c; // OK

Это, в частности, полезно при описании параметров функции для запрещения модификации аргумента, например:

 void strcopy(char* p, const char* q) // не может изменять q*

Нельзя, однако, присвоить адрес константы указателю на переменную, ибо это позволило бы изменить значение константы, например:

pv = &d // ошибка

В Яве типа константы нет. Поэтому константы, как и в других языках программирования вводятся по средством специальных ключевых слов. В данном случае для этого используются два ключевых слова: static и final. Пример:

static final double = 3.1416;

Как результат, константа становится компонентом класса, а не отдельного объекта.

В C# также нет типа константы, и для описания константы используется общепринятое ключевое слово const. Как и в Яве, константа становится компонентом класса, но при этом в ее описании не нужно (и нельзя) употреблять служебное слово static. Примечательно, что при описании константы необходимо указывать ее тип, которым может быть sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool, string,  тип-перечисления или ссылочный тип. Примеры:

const int Y = 10;

const double X = 1.0, Y = 2.0, Z = 3.0;

2.4.5. Ссылки

Ссылка в С++ является другим именем объекта. Главное применение ссылок состоит в описании параметров (здесь они играют ту же роль, что и параметры-переменные Паскаля) и возврате значений для функций вообще и для одинаково названных операций в частности. Запись T& означает ссылку на Т. Примеры:

 int i = 1;

int& r = i; // r и i ссылаются теперь на один int

 int x = r; // x = 1

r = 2; // i =2

void f(double& a) {a += 3.14;} // параметр - ссылка

 // ...

double d = 0;

f(d); // прибавление 3.14 к d

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

 int ii = 0;

int& rr = ii;

rr++; // ii увеличивается на 1

Чтобы получить указатель на объект помеченный ссылкой rr, можно написать &rr, что эквивалентно &ii.

Не существует ссылки на ссылки, массивов ссылок и указателей на ссылки.

Ссылки на константы играют важную роль в качестве параметров функций.

В Яве и C# нет конструкции, аналогичной ссылке в С++. Термин «ссылка» в этих языках означает уникальный внутренний идентификатор объекта.

2.4.6. Структуры

Тип структуры (тип записи в других языках) реализует математическую операцию декартова произведения множеств. Соответственно структура – это набор элементов (в общем случае) различных типов. В простейшем виде определение типа структуры в С++ и C# имеет следующий вид:

struct имя-типа-структуры {T1 p1; … ; Tn pn;};

где Т1, … , Tn – имена типов данных, а p1, … , pn – имена проектировочных функций (полей структуры). Такое определение можно считать сокращенной запись следующего интерфейса:

 type T =

 [create: T1, … , Tn T;

 p1: l-value T l-valueT1;

  . . .

 pn: l-value T l-value Tn];

Пример:

 Struct address {

 char* name; // “Jim Dandy”

 int number; // 61

 char* street; // “south str”

 char* town; // “New Providence”

 char state[2]; // ‘N’ ‘J’

 int zip; // 7974

};

Обращение к проектировочным функциям (полям структуры) осуществляется, как обычно, через точку. Таким образом, если addr – переменная типа address, то addr.street – это стилизованная запись вызова функции street(addr). При этом, если аргумент проектировочной функции – ml-значение, то и результат – ml-значение, т.е. его можно использовать в левой части оператора присваивания.

Операция create для структуры изображается в виде агрегата, перечисляющего в фигурных скобках значения полей структуры, например:

 address jd = {

 “Jim Dandy”,

 61, “South Str”,

 “New Providence”, {‘N’, ‘J’}, 7974

};

Если ptr – указатель на структуру, то для доступа к полям структуры через этот указатель осуществляется посредством операции “->”: например:

 address ptr = &jd;

ptr->name; // поле структуры name

 ptr->street; // поле структуры street

Нетрудно видеть, что запись p->a является сокращенной формой записи (*p).a.

Другие операции над структурами не определены. Однако структура является частным случаем класса, и потому в каждом типе структуры могут дополнительно определяться любые необходимые операции.

Два типа структур всегда являются разными типами, даже если у них одинаковы поля. Например,

 struct S1 {int a;};

struct S2 {int a;};

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

В Яве нет отдельного типа структуры, поэтому там структуры представляются классами.

В C# тип структуры есть и в простейшем виде он аналогичен типу структуры в С++. Он так же является частным случаем класса в том смысле, что в типе структуры можно определять практически все виды компонентов, которые можно определять в классе. В отличие от объектов класса, однако, структура является значением, т.е. она не обладает внутренним идентификатором и размещается всегда в стеке по месту ее использования.

2.4.7. Объединения

Тип объединения реализует математическую операцию размеченного объединения множеств. Поэтому объединение является структурой, у которой в любой момент времени существует значение только одного из компонентов (все компоненты размещаются в памяти по одному и тому же адресу). По этой причине при хранении объекта-объединения достаточно выделить память под максимальный компонент. Определение типа объединения в С++ имеет следующий вид:

union имя-типа-объединения {T1 p1; … ; Tn pn;};

где Т1, … , Tn – имена типов данных, а p1, … , pn – имена инъектировочных функций (вариантов объединения). Такое определение можно считать сокращенной запись следующего интерфейса:

 type T =

 [p1: T1 T;

  . . .

 pn: Tn T;

 get_p1: l-value T l-value T1;

  . . .

 get_pn: l-value T l-value Tn]

В С++ обращения к инъектировочным функциям и функциям get осуществляется так же, как к полям структуры, т.е. либо через точку с использованием объединения и имени компонента объединения либо через стрелку использованием указателя на объединение и имени компонента объединения. Пример:

 union tok_val {

 char* p; // строка

 char v[8]; // массив из 8 символов 

 long i; // целое

 double d; // вещественное двойной точности

};

tok_val uv *puv;

uv.p = “string” // присваивание строки

uv.d = 2.5; // присваивание вещественного значения

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

 void strange (int i)

{

 tok_val x:

 if (i)

  x.p = "2";

 else

  x.d = 2;

 sqrt(x.d); // ошибка, если i != 0

};

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

Объединение вида

 union {список-компонентов}

называется безымянным (anonymous); оно определяет неименованный объект (а не тип). Имена компонентов безымянного объединения должны отличаться от других имен из его области действия и используются непосредственно, т.е. без префиксации, обычной для доступа к компонентам объединения. Пример:

 void f()

{

 union {int a; char* p};

 a = 1;

 // . . .

 p = "Jenny";

 // . . .

}

Здесь а и р употребляются как обычные переменные, но, поскольку они являются компонентами объединения, имеют один и тот же адрес.

Объединение может иметь компонентные функции (в том числе конструкторы и деструкторы), но не виртуальные функции. Объединение не может иметь базовых классов и само не может быть базовым классом; оно не может также иметь статических компонентных данных.

В Яве и C# нет конструкции, аналогичной объединениям С++.

  1.   Объявления

2.5.1. Объявления и описания

Объявление вводит в программу одно или несколько имен. Каждое имя должно быть обязательно объявлено, прежде чем оно будет использовано, так как компилятору нужно знать, с чем он имеет дело. В С++ объявление является описанием за исключением следующих случаев:

1) оно объявляет функцию без спецификации тела,

2) содержит спецификатор extern при отсутствии инициализатора и тела

 функции,

3) является объявлением статического компонента в объявлении класса,

4) является объявлением имени класса,

5) является объявлением имени типа (typedef).

Примеры описаний:

 int a;

extern const c = 1;

int f(int x) {return x+a;};

struct S {int a; int b;};

 enum {up, down};

Примеры чисто объявлений:

 extern int a;

extern const c;

int g(int);

struct T;

 typedef int Int;

Как видно из примеров, описания делают нечто большее, чем объявления, а именно, они определяют некоторую сущность, с которой будет оперировать компилятор или программа. Например, для переменной а этой сущностью является количество выделяемой памяти в программе, для функции f – тело функции, для структуры S – количество выделяемой памяти в компиляторе.

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

В Яве и C# все объявления являются и описаниями.

2.5.2. Структура объявления

В С++ объявление состоит из 4-х частей: необязательного спецификатора, базового типа, объявляющей части и, возможно, инициализатора. За исключением объявлений функций и пространств имен объявление заканчивается точкой с запятой. Пример:

char* kings[] = {“Antigon”, “Selevk”, “Ptolemeo”};

В этом примере базовым типом является char, объявляющей частью – * kings[], а инициализатором –  ={…}.

В качестве спецификатора могут выступать ключевые слова как, например, virtual и extern. Они описывают характеристики, не связанные с типом.

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

* указатель префикс

*const константный указатель префикс

& ссылка  префикс

[] массив суффикс

() функция суффикс

Использование модификаторов типа значительно упростилось бы, если бы все они были префиксами или суффиксами. Однако модификаторы типа «*», «[]» и «()» разрабатывались так, чтобы отражать их смысл в выражениях. Суффиксные модификаторы типа «крепче связаны» с именем, чем префиксные. Следовательно, *kings[] означает массив указателей на какие-то объекты, а для определения типа «указатель на массив» необходимо использовать скобки: (*kings)[].

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

сonst int  =1, d = 2;

а две символьные переменные – следующим образом:

 char x, y;

Обратим внимание, что модификаторы типа относятся только к ближайшему имени и не относятся к соседним именам в объявлении. Пример:

 int* p, y;  // int* p; int y;

int x, q;  // int x; int q;

 int v[10], pv; // int v[10], int pv; 

В Яве и С# объявление всегда является компонентом описания класса и также состоит из 4-х частей: необязательного спецификатора, типа, списка идентификаторов и, возможно, инициализатора. Заметим, что в отличие от С++ здесь нет чего-либо подобного модификаторам типа. Тип задается всегда полностью в одном месте. Примеры на Яве:

 static final double = 3.1416;

String[] dangers = {“Lions”, Tigers”, “Bears”};

Заметим, что последнее объявление равносильно следующему фрагменту:

 String[] dangers = new String[3];

Dangers[1] = “Lions”;

 Dangers[2] = “Tigers”;

Dangers[3] = “Bears”;

Если инициализатор переменной не задан, во всех языках ей присваивается «нулевое» значение соответствующего типа. Например, для Явы это выглядит следующим образом:

 boolean false

char ‘\u0000’

целое (byte, short, int, long) 0

 float +0.0f

double +0.0d

ссылка на объект идентификатор null.

В C# значение по умолчанию формируется нулевыми значениями всех битов. Таким образом:

  •  Для sbyte, byte, short, ushort, int, uint, long и ulongэто 0.
  •  Для char – это '\x0000'.
  •  Для float – это 0.0f.
  •  Для double – это 0.0d.
  •  Для decimal – это 0.0m.
  •  Для bool – это false.
  •  Для типа перечисления E – это 0.
  •  Для ссылочного типа – это значение null.
  •  Для стуктурного типа значение по умолчанию формируется из нулевых значений его полей.

2.5.3. Определение нового имени типа данных

В С++ объявление, начинающееся с ключевого слова typedef, вводит новое имя (синоним) для существующего типа данных. Например:

 typedef char* Pchar;

Pchar p1, p2; // p1 и p2 имеют тип char*

char* p3 = p1;

Целью такого объявления часто является назначение короткого синонима для часто используемого типа. Другое использование typedef – свести в одно место все непосредственные ссылки на какой-то тип, например:

 typedef int int32;

 typedef short int16;

Имея такие определения типов и используя int32 везде, где могут потребоваться большие числа, мы можем перенести нашу программу на машину с sizeof(int) == 2, просто заменив единственную строчку исходного кода с int:

typedef long int32;

В Яве и C# аналогичной конструкции нет.

  1.   Преобразования типов

И С++, и Ява, и C# являются языками с сильной типизацией – это означает, что во время компиляции программы практически всегда осуществляется проверка на совместимость типов операндов операций. Языки предотвращают неверные присваивания, запрещая все сколько-нибудь сомнительные операции, и поддерживают механизмы приведения типов (вернее, преобразования значений одних типов в значения других типов), когда совместимость может быть проверена только на стадии выполнения программы. В большинстве случаев такие приведения происходят автоматически.

2.6.1. Целочисленные расширения и преобразования

В С++ значения типов char и short int, элементы перечисления, и целые битовые поля могут употребляться везде, где употребляются целые. Если int может представить все значения исходного типа, значение приводится к int, в противном случае оно преобразуется в unsigned int. Этот процесс называется целочисленным расширением (integral promotion).

Когда целое преобразуется к беззнаковому типу, получается значение наименьшего целого без знака, конгруэнтного целому со знаком. Когда значение преобразуется к знаковому типу, оно остается неизменным, если оно может быть представлено в новом типе, в противном случае результат зависит от реализации.

Точно так же в Яве и C# числовое значение более короткого типа может быть преобразовано в значение более длинного типа. В Яве тип char может использоваться везде, где допускается использование int, а в C# где допускается использование ushort.

2.6.2. Вещественные типы одинарной и двойной точности

Для выражений типа float используется вещественная арифметика одинарной точности, а типа double – двойной точности. Одно вещественное число может быть неявно преобразовано в другое вещественное число независимо от его типа. Когда значение менее точного вещественного типа преобразуется к равному или более точному вещественному типу, значение не меняется. Множество значений, которые могут быть представлены как float является подмножеством множества double, которое в свою очередь является подмножеством множества long double. Поэтому преобразования от float к double и от double к long double всегда безопасны.

Когда в С++ значение более точного вещественного типа преобразуется к менее точному вещественному типу и оно находится в границах представления, результат может быть следующим большим или предшествующим меньшим представимым значением. В Яве и C# такое приведение неявно сделать нельзя.

2.6.3. Вещественные и целочисленные типы.

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

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

2.6.4. Арифметические преобразования

1. Если один операнд имеет тип long double, другой операнд приводится к long double.

2. В противном случае, если один операнд имеет тип double, другой операнд приводится к double.

3. В противном случае, если один операнд имеет тип float, другой операнд приводится к float.

4. В противном случае, над обоими операндами производятся целочисленные расширения.

4а. В этом случае, если один из операндов - unsigned long, другой преобразуется к unsigned long.

4б. В противном случае, если один операнд имеет тип long int, а другой - unsigned int, то, если long int может представить все значения unsigned int, последний преобразуется к long int; иначе оба операнда преобразуются к unsigned long int.

4в. В противном случае, если один операнд имеет тип long, другой операнд приводится к long.

4г. В противном случае, если один операнд имеет тип unsigned, другой операнд приводится к unsigned.

4д. В противном случае оба операнда принадлежат к типу int.

Система безопасных преобразований в С++ может быть представлена следующими цепочками:

signed char -> short -> int -> long

unsigned char -> unsigned short -> unsigned int -> unsigned long

float -> double -> long double

Любая реализация обычно имеет большее число безопасных преобразований.

В C# такие цепочки выглядят следующим образом:

  •  из sbyte в short, int, long, float, double или decimal.
  •  из byte в short, ushort, int, uint, long, ulong, float, double или decimal.
  •  из short в int, long, float, double или decimal.
  •  из ushort в int, uint, long, ulong, float, double или decimal.
  •  из int в long, float, double или decimal.
  •  из uint в long, ulong, float, double или decimal.
  •  из long в float, double или decimal.
  •  из ulong в float, double или decimal.
  •  из char в ushort, int, uint, long, ulong, float, double или decimal.
  •  из float в double.

2.6.5. Преобразования указателей

Над указателями С++ при присваивании, инициализации, сравнении и другом использовании могут быть выполнены следующие преобразования:

1. Константное выражение, равное нулю, преобразуется к пустому указателю (null pointer). Гарантируется, что этот указатель отличается от указателя на любой объект.

2. Указатель на переменный объект может быть преобразован к void*.

3. Указатель на функцию можно преобразовать к void*, если void* имеет достаточный размер, чтобы представить его.

4. Указатель на объект некоторого класса можно преобразовать к указателю на объект доступного базового класса, если преобразование однозначно; базовый класс считается доступным, если доступны его открытые (public) компоненты. Пустой указатель преобразуется в себя.

5. Выражение типа «массив элементов типа Т» можно преобразовать к указателю на начальный элемент этого массива.

6. Выражение типа «функция с результатом типа Т» можно преобразовать к указателю на «функцию с результатом типа Т», за исключением случая, когда оно является операндом операции взятия адреса или операции вызова функции.

В Яве и C# указателей нет. Вместо этого есть понятие ссылки на объект (внутренний идентификатор объекта). Ссылка на объект производного класса может использоваться везде, где требуется ссылка на объект базового класса или реализуемого интерфейса.

2.6.6. Преобразования ссылок (С++)

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

2.6.7. Явные преобразования типов

Когда значение одного типа не может быть неявно преобразовано в значение другого типа, довольно часто можно воспользоваться явным приведением типов (cast). Явное приведение требует, чтобы преобразованное значение нового типа как можно лучше соответствовало старому значению бывшего типа. Преобразование осуществляется путем снабжения данного выражения явным указанием нужного типа. В С++ это указание может быть представлено в двух видах:

(новый-тип) выражение  новый-тип (выражение)

В Яве и C# допускается только первая форма приведения, например:

 

 double d = 7.99;

long l = (long) d;

Значение любого типа, которое может быть преобразовано в значение другого типа посредством неявного преобразования, может быть преобразовано явно. В Яве и С# таким образом могут быть преобразованы значения более длинных типов в значения более коротких типов (в С++ такие преобразования могут быть сделаны и неявно). При этом возможны потеря точности и другие неприятные явления, если прежнее значение не умещается в памяти, отведенной под новое значение. Примеры:

 float f;

char* p;

// . . .

long ll = long(p); // преобразует p в long

int i = int(f); // преобразует f в int

В C# в отличие от С++ практически все преобразования типов перечислений осуществляются явным образом (единственное исключение – преобразование целочисленного нуля в значение типа перечислений). Допустимы следующие явные преобразования:

  •  из sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double или decimal в любой тип перечислений;
  •  из любого типа перечислений в sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double или decimal;
  •  из любого типа перечислений в любой другой тип перечислений.

Преобразование осуществляется путем преобразования соответствующих базисных типов. Например, перевод значения типа перечисления E с базовым типом int в значение типа byte производится путем явного преобразования из int в byte, а преобразование из byte в E производится путем неявного преобразования из byte в int.

В С++ указатель может быть явно преобразован к значению любого целочисленного типа, достаточно для этого вместительного; функция преобразования определяется реализацией. Значение целочисленного типа может быть преобразовано в указатель. Указатель, преобразованный в целое и обратно должен иметь первоначальное значение. Пример:

 char* f(char* p)

{

 int i = (int)p;

 return (char*)i;  // f(arg) == arg?

 }

Указатель на объект одного типа может быть явно преобразован в указатель на объект другого типа.

Указатель на объект класса Base может быть явно преобразован в указатель на объект класса Derived, для которого Base является прямым или косвенным базовым классом, если существует однозначное преобразование от Derived к Base. Такое приведение базового класса к производному предполагает, что объект базового класса является подобъектом объекта производного класса; результирующий указатель будет нацелен на объемлющий объект производного класса. Если объект базового класса не является подобъектом объекта производного класса, приведение может вызвать исключительную ситуацию.

То же самое происходит и в Яве и C#. Здесь также ссылка на объект базового класса может быть преобразована в ссылку на объект производного класса, хотя такое преобразование небезопасно. Предположим, что у нас есть следующая иерархия объектов:

 Person

 

Student Professor

Ссылка типа Person не обязательно относится к типу Student – объект может иметь тип Professor. Следовательно, неверно, вообще говоря, ставить ссылку на объект типа Person там, где требуется ссылка на объект типа Student. Подобное приведение называется сужением (narrowing), или понижающим приведением в иерархии классов. Иногда его также называют ненадежным приведением (unsafe casting), поскольку оно не всегда допустимо.

Но иногда мы можем совершенно точно знать, что объект типа Person на самом деле является экземпляром класса Student. В этом случае можно совершать явное понижающее приведение, например:

 Student jack

Person john = jack; //неявное повышающее приведение

Student mike = (Student) john // явное понижающее приведение

Если такое приведение сработает (т.е. ссылка john действительно указывает на объект класса Student), то ссылка mike будет указывать на тот же объект, что и john, но с ее помощью можно будет получить доступ к дополнительным функциям, предоставляемым классом Student. Если же преобразование окажется недопустимым (ссылка john указывает на объект класса Professor), то в Яве будет возбуждено исключение ClassCastException.

Чтобы избежать риска возбуждения исключения, в Яве есть специальная функция instanceof, которая позволяет проверить, принадлежит ли в данный момент указываемый объект данному производному типу, например:

 if (john instanceof Student) {

Student mike = (Student) john;

 // использование методов класса Student

}

В C# для этой цели служит операция is, и приведенный выше пример на этом языке выглядит следующим образом:

 

 if (john is Student) {

Student mike = (Student) john;

 // использование методов класса Student

}

Для указания явного преобразования типа на этапе компиляции программы в текущей версии С++ введены специальные операции:

 static_cast<имя_типа>(выражение)

reinterpret_cast<имя_типа>(выражение)

const_cast<имя_типа>(выражение)

Все три операции преобразуют значение выражения в значение типа, указанного именем-типа. Первая операция осуществляет преобразование родственных типов, например, указателя на один тип к указателю на другой тип в той же иерархии классов или вещественного типа в целочисленный. Вторая операция управляет преобразованиями между несвязанными типами, например, целых в указатели или указателей одного типа в указатели другого типа. Благодаря этим различиям, компилятор может осуществлять минимальную проверку типов при static_cast, а программист – легче обнаружить опасные преобразования при reinterpret_cast. Операция const_cast аннулирует действие спецификатора const, т.е. позволяет считать константный объект переменным.

2.6.8. Строковое приведение в Яве

В Яве класс String отличается от всех остальных неявным преобразованием значений других типов в строки в операции конкатенации строка: при выполнении конкатенации преобразуется в строку все, что не является строковым значением. Подобные приведения определены для всех примитивных типов и осуществляются вызовом метода toString соответствующего класса.

Если преобразовать в строку пустую ссылку, то результатом будет строка “null” Если для данного класса метод toString не определен, то используется метод, унаследованный от класса Object, возвращающий строковое представление объекта.

  1.   Сводка операций

В следующей сводке операций С++ после каждой операции приведено ее название и пример использования. В этих примерах class_name это имя класса, member  имя компонента, object  выражение, дающее в результате объект класса, pointer  выражение, дающее в результате указатель, expr  выражение, а lvalue  выражение, дающее модифицируемый объект. Type может быть произвольным именем типа, только когда он стоит в скобках, во всех остальных случаях существуют ограничения. Постфиксные операции и операции присваивания правоассоциативны, все остальные - левоассоциативны. Это значит, что a=b=c означает a=(b=c), a+b+c означает (a+b)+c, и *p++ означает *(p++), а не (*p)++.

В каждой очерченной части находятся операции с одинаковым приоритетом. Операция, расположенная в более высокой части, имеет приоритет больший, чем операция, расположенная в более низкой части.

-------------------------------------------------------------------------------------------------

:: разрешение области видимости class_name::member

:: глобальное ::name

-------------------------------------------------------------------------------------------------

. выбор компонента object.member

-> выбор компонента pointer->member

[] индексация pointer[expr]

() вызов функции expr(expr-list)

() построение значения type(expr-list)

sizeof размер объекта sizeof expr

sizeof размер типа sizeof(type)

------------------------------------------------------------------------------------------

++ приращение после lvalue++

++ приращение до ++lvalue

-- уменьшение после lvalue--

-- уменьшение до --lvalue

~ дополнение ~expr

! не !expr

- унарный минус -expr

+ унарный плюс +expr

& адрес объекта &lvalue

* разыменование *expr

new создание (размещение) new type

delete уничтожение (освобождение) delete pointer

() приведение (преобразование типа) (type)expr

------------------------------------------------------------------------------------------

.* выбор компонента object.*pointer-to-member

->* выбор компонента pointer.->*pointer-to-member

-------------------------------------------------------------------------------------------

* умножение expr*expr

/ деление expr/expr

% взятие по модулю expr%expr

-------------------------------------------------------------------------------------------

+ сложение (плюс) expr+expr

- вычитание (минус) expr-expr

-------------------------------------------------------------------------------------------

<< сдвиг влево expr << expr

>> сдвиг вправо expr >> expr

--------------------------------------------------------------------------------------------

< меньше expr < expr

> больше expr > expr

<= меньше или равно expr <= expr

>= больше или равно expr >= expr

--------------------------------------------------------------------------------------------

== равно expr == expr

!= не равно expr != expr

---------------------------------------------------------------------------------------------

& побитовое И expr & expr

---------------------------------------------------------------------------------------------

^ побитовое исключающее ИЛИ expr ^ expr

---------------------------------------------------------------------------------------------

| побитовое включающее ИЛИ expr | expr

---------------------------------------------------------------------------------------------

&& логическое И expr && expr

---------------------------------------------------------------------------------------------

|| логическое включающее ИЛИ expr || expr

---------------------------------------------------------------------------------------------

_?_:_ арифметический IF expr ? expr : expr

---------------------------------------------------------------------------------------------

= простое присваивание lvalue = expr

*= умножить и присвоить lvalue *= expr

/= разделить и присвоить lvalue /= expr

%= взять по модулю и присвоить lvalue %= expr

+= сложить и присвоить lvalue += expr

-= вычесть и присвоить lvalue -= expr

<<= сдвинуть влево и присвоить lvalue <<= expr

>>= сдвинуть вправо и присвоить lvalue >>= expr

&= И и присвоить lvalue &= expr

|= включающее ИЛИ и присвоить lvalue |= expr

^= исключающее ИЛИ и присвоить lvalue |= expr

---------------------------------------------------------------------------------------------

, запятая expr, expr

Ниже следует сводка операций языка C# (в порядке понижения приоритета).

Категория

Знаки операций

Первичные

x.y  f(x)  a[x]  x++  x--  new

typeof  checked  unchecked

Унарные

+  -  !  ~  ++x  --x  (T)x

Мультиплик-ные

*  /  %

Аддитивные

+  -

Сдвиги

<<  >>

Отношения

<  >  <=  >=  is  as

Равенства

==  !=

Логическое AND

&

Логическое XOR

^

Логическое OR

|

Условное AND

&&

Условное OR

||

Условное

?:

Присваивания

=  *=  /=  %=  +=  -=  <<=  >>=  &=  ^=  |=

Как было показано в предыдущих разделах, многие знаки операций обозначают схожие операции в различных типах. Например, операция сравнения на равенство «==» в Яве и C# работает следующим образом в различных встроенных типах:

а) два выражения целого типа считаются равными, если они вырабатывают одно

и то же значение;

в) два выражения типа object считаются равными, если оба вырабатывают

  ссылку на один и тот же объект или значение  null;

c) два выражения типа string считаются равными, если оба вырабатывают

ссылки на идентичные строки или значение  null.


  1.  Области действия объектов и классы памяти

3.1. Области действия

В С++ существует 5 разновидностей областей действия имен: локальная, функция, файл, пространство имен и класс.

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

Функция: Метки, объявленные в теле функции, могут быть использованы повсюду в теле функции.

Файл: Имя, объявленное вне всех блоков, классов и пространств имен, можно использовать в транслируем файле, содержащем это объявление, начиная с места объявления. Такое имя называется глобальным (global). Объявление имени в блоке может скрыть объявление того же имени в охватывающем блоке, пространстве имен или файле. После выхода из блока имя восстанавливает свой первоначальный смысл. Пример:

int x  // глобальная переменная х

void f()

{

int x; // локальная переменная х, скрывает глобальную х

x = 1; // присваивание локальной х

{

 int x; // новая локальная х, скрывает предыдущую х

 x = 2; // присваивание второй локальной х

}

x = 3; // присваивание первой локальной х

}

int* p = &x; // взятие адреса глобальной переменной х

Класс: Имя компонента класса локализуется в этом классе и может быть использовано только:

1) в компонентной функции этого класса,

2) после операции «.» (точка), примененной к объекту этого класса

 или его производного класса,

3) после операции ->, примененной к указателю на объект этого класса

 или производного от него,

4) после операции ::, примененной к имени этого класса или

 производного от него.

Имя, объявленное как дружественное (friend), считается глобальным; то же самое справедливо для имени класса, впервые объявленного в качестве типа результата функции или типа параметра.

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

int g = 99;

f(int g) {return g ? g : ::g} //в последнем случае исп-ся глобальное g

Пространство имен (namespace) представляет собой некоторую область, служащую для локализации имен с целью избежания конфликтов при их использовании. Пространства имен рассматриваются в конце курса.

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

  1.  локальные переменные, объявленные в блоке, цикле for или среди параметров обработчика исключений;
  2.  параметры метода, если имя встретилось в методе;
  3.  компоненты данного класса, включая все унаследованные;
  4.  импортированные типы с явным именованием;
  5.  другие типы, объявленные в том же пакете;
  6.  импортированные типы с неявным именованием;
  7.  прочие пакеты, доступные системе.

В C#, как и в С++, существуют пространства имен (будут рассмотрены вместе с пространствами имен С++). Для определения значения имени поиск производится примерно в том же порядке, что и для Явы.

3.2. Программа и сборка (С++)

Программа на С++ состоит из одного или нескольких файлов, собранных вместе. Файл состоит из последовательности объявлений. Глобальное имя, объявленное как статическое (static), локализовано в своей единице трансляции и может быть использовано в других файлах для именования других объектов и т.п. Такое имя называется внутренним (internal linkage). Имя, объявленное как встраиваемое (inline) или как const но без extern, также локализовано в своей единице трансляции.

Каждое объявление глобального имени, не являющегося внутренним, приводит в многофайловой программе к одному и тому же объекту, функции или классу. Такое имя называется внешним (external linkage). Внешним является также имя, явно объявленное как extern.

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

3.3. Компоновка (С++)

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

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

 // file1.c:

int a = 1;

int f() { /* что-то делает */}

// file2.c:

 extern int a;

int f();

void g() { a = f(); }

Переменная а и функция f(), которые использует функция g() в файле file2.c, -- такие же, как и те, что определены в файле file1.c. Ключевое слово extern указывает, что объявление а в file2.c является только объявлением, а не описанием. Объект в программе должен описываться только один раз, объявляться он может много раз, но все объявления должны быть согласованы. Пример:

 // file1.c:

int a = 1;

int b = 1;

 extern int c;

// file2.c:

int a;

 extern double b;

 extern int c;

Здесь три ошибки: а описано дважды (int a; является описанием, которое означает int a = 0;), b объявлено дважды с разными типами, а с объявлено дважды, но не описано.

Другая картина при использовании описателя static:

 // file1.c:

 static int a = 6;

 static int f() { /* . . . */}

// file2.c:

 static int a = 7;

 static int f() { /* . . . */}

Поскольку каждое a и f объявлено как static, программа правильна.

3.3.1. Заголовочные файлы

Одним из простых способов достижения согласованности определений различных файлов является включение в исходные файлы программы заголовочных файлов с интерфейсной информацией. Директива

#include "to-be-included"

замещает себя содержимым файла "to-be-included", которым должен быть исходный текст на С++. Такое включение обычно делается препроцессором С++. Для включения файлов из стандартного каталога вместо кавычек используются угловые скобки "<" и ">". Пример:

 #include <stream.h> // из стандартного каталога

#include "myheader.h" // из текущего каталога

В заголовочном файле могут содержаться:

Определения типов struct point {int x, y;};

Параметризованные типы template <class T> class V;

Объявления функций extern int strlen(const char*);

Описания встраиваемых

функций inline char get() {return *p++};

Объявления данных extern int a;

Описания констант const float pi = 3.14.1593;

Перечисления enum bool {false, true};

Объявления имен class Matrix;

Директивы включения #include <signal/h>

Определения макросов #define Case break; case

Комментарии /* проверка на конец файла */

Заголовочные файлы обычно имеют суффикс  .h, а файлы, содержащие функции или определения данных - суффикс  .с.

3.3.2. Старт и завершение

Программа на С++ должна содержать функцию с именем main(). Эта функция предназначается для входа в программу. Она не может быть вызвана из программы. Нельзя также брать ее адрес и объявлять ее как inline или static.

Вызов функции void exit(int), объявленной в <stdlib.h>, завершает программу. Значение параметра возвращается окружению как результат работы программы. Оператор return в main() приводит к вызову exit() с возвращаемым значением как фактическим параметром.

Обычно программа осуществляет какой-либо вывод. Пример:

 #include <iostream.h>

int main()

{

 cout << "Hello, world!\n";

 return 0;

}

Здесь строка #include <iostream.h> сообщает компилятору о необходимости использования стандартных средств ввода-вывода, находящихся в файле iostream.h. Операция "<<" ("поместить в") переписывает второй аргумент в первый (в данном случае, строку "Hello, world!\n" в стандартный поток cout. Значение типа int, возвращаемое функцией main(), программа передает окружающей среде. Если ничего не случается, среда получает случайное значение.

3.4. Компоновка программы Явы

Программа на Яве состоит из набора пакетов, каждый из которых представляет единицу компиляции, формально определяемую следующим образом:

 единица-компиляции:

 описание-пакетаopt описание-импортаopt описания-типов

Здесь описание-пакета (если оно есть) формирует новый пакет, а описание-импорта (если оно есть), имеющее следующий вид:

using имя-пакета

позволяет подключиться к существующим пакетам. Подробнее пакеты рассматриваются в конце курса.

В программе должен быть хотя бы один класс с методом main, который служит для запуска программы (см. раздел 6.7.1).

3.5. Компоновка программы C#

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

единица-компиляции:
using-
директивыopt описания-компонентов-пространства-имен

Здесь using-директива – это директива вида

 using имя-пространства-имен

позволяющая «видеть» в данной единице компиляции имена, описанные в указанном пространстве имен. Существует предопределенное пространство имен с именем System, которое будет использоваться в дальнейших примерах. Описания-компонентов-пространства-имен состоят из описаний подпространств имен и типов данных, основными из которых являются описания классов. Подробнее пространства имен рассматриваются в конце курса.

В программе должен быть хотя бы один класс в методом Main, который служит для запуска программы (см. раздел 6.7.1).

3.6. Классы памяти

В С++ имеется два объявляемых класса памяти: автоматический и статический.

 Автоматические (или локальные) объекты локализованы относительно

каждого входа в блок.

 Статические объекты существуют и сохраняют свои значения в течение

всего времени выполнения программы.

Локальные объекты инициализируются всякий раз при исполнении их описаний и разрушаются при выходе из содержащих их блоков.

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

  1.  Динамическое распределение памяти: new, delete, new[], delete[].

Время жизни именованной переменной определяется ее областью видимости. С другой стороны, часто возникает потребность в создании объектов, которые существуют вне зависимости от области видимости, в которой они были созданы. В С++ такие объекты создаются посредством операции new и уничтожаются посредством операции delete. Говорят, что место под объекты, созданные операцией new, выделяются в свободной, или динамической памяти, которая еще называется кучей. Такими объектами обычно бывают вершины деревьев или элементы связанных списков, являющиеся частью большой структуры данных, размер которой не известен на стадии компиляции.

В общем случае операция new осуществляет следующее:

1. Отведение памяти под объект указанного типа.

2. Инициализация объекта (если она задана).

3. Возвращение указателя на объект.

Пример:

int* p = new int;

Здесь создается именованный объект (переменная) p типа «указатель на целое» и создается в куче неинициализированный объект типа «целое», указателем на который инициализируется p. Для инициализации динамического объекта необходимо указать его значение в скобках, например:

int* p = new int(6);

Время жизни объекта, созданного операцией new, не ограничено областью действия, в которой он был создан, и объект будет существовать до тех пор, пока он не будет удален посредством операции delete. Например,

delete p;

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

 int x;

int* p = &x;

delete p;

Эффект такого применения операции delete не определен и может быть катастрофическим.

Реализация С++ не гарантирует наличие сборщика мусора, осуществляющего поиск объектов, на которые отсутствуют ссылки, и делающую эту память доступной для повторного использования. По этой причине в программе на С++ следует удалять неиспользуемые объекты из динамической памяти вручную посредством операции delete.

Посредством операции new[] можно создавать массивы объектов. В этом случае в квадратных скобках после имени типа может быть указан размер создаваемого массива. В случае многомерного массива все измерения, кроме первого, должны быть заданы константными выражениями, первое измерение может быть переменным выражением. В качестве результата операция возвращает указатель на первый элемент массива. Примеры:

char* s = new char[10]; // выделяется память под строку из 10 символов

int (*ar)[20][30];  // указатель на массив из 20*30 чисел

 . . .

ar = new int[20][30]; //создается массив ячеек под 20*30 целых чисел

Для освобождения массивов используется форма

delete[] выражение

где выражение указывает на массив. Последствия выполнения над массивом операции delete без [] не определены, также как не определены последствия уничтожения отдельного объекта операцией delete[]. Примеры:

 delete [] s;

delete [] ar;

В Яве и C# явных указателей нет. Вместо этого каждая переменная, имеющая тип некоторого класса, является указателем (ссылкой) на какой-то объект этого класса. Операция new, сопровождаемая именем класса и возможным инициализатором (списком значений), выделяет память (в куче) под объект, инициализирует его (если инициализация задана) и возвращает ссылку на него. Пример:

 Point p = new Point();

Complex z = new Complex(1.0, 2.0);

int[] iar = new int[3];

В Яве и C# нет операции delete. В противовес С++ здесь предполагается обязательное наличие сборщика мусора в реализации языка. Поэтому неиспользуемые объекты программы автоматически уничтожаются сборщиком мусора, который берет на учет каждый объект, на который не остается ссылок, и уничтожает его в подходящий момент времени. После этого данная память может использоваться для размещения других объектов. Автоматическая сборка мусора означает, что программисту никогда не придется беспокоиться о проблеме «зависших указателей». К примеру, в С++ на объект в свободной памяти могут смотреть несколько указателей. В таком случае при удалении объекта по одному указателю, остальные указатели будут продолжать указывать на эту область памяти, что может привести к непредсказуемым последствиям. Ява и C# решают эту проблему за программиста, поскольку объект, на который имеется хотя бы одна ссылка, никогда не будет уничтожен сборщиком мусора.


  1.  Операторы

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

В C# есть понятие конечной точки и достижимости оператора. Под конечной точкой (end point) данного оператора понимается точка, расположенная за его концом. Если при каком нибудь значении переменных программы, оператор может быть выполнен, он называется достижимым (reachable), иначе он называется недостижимым (например, недостижим непомеченный оператор, расположенный непосредственно за оператором goto; недоступными являются также конечные точки операторов return и break). Компилятор генерирует предупреждающее сообщение, если он находит недостижимый оператор, но программа не считается при этом ошибочной.

  1.  Помеченный оператор

Во всех языках оператор может быть помечен произвольной меткой, а некоторые операторы имеют специальные метки:

 помеченный-оператор:

 идентификатор : оператор

 case константное-выражение : оператор

 default : оператор

Идентификатор объявляет идентификатор метки, который может употребляться в операторах перехода. Областью действия метки является блок, в которой она объявлена. Метка может быть использована до ее объявления. Метки образуют свое собственное пространство имен и не конфликтуют с другими идентификаторами. Метки case и default могут появляться только в операторе выбора.

В С++ и Яве меткой может быть помечен любой оператор, а в C#  только блок (нельзя пометить вложенный в него оператор, даже если это – блок).

  1.  Оператор-выражение

Большинство операторов в программе обычно являются операторами-выражениями, которые имеют вид:

 оператор-выражение:

 выражениеopt;

Чаще всего операторы-выражения представляют собой присваивания и вызовы функций.

Во всех языках имеется несколько операций присваивания; все они группируются справа налево и требуют l-значения в качестве левого операнда. Типом результата является тип левого операнда. Результатом является значение, запомненное в левом операнде сразу после выполнения операции и рассматриваемое далее как l-значение.

 выражение-присваивания:

 выражение операция-присваивания выражение

операция-присваивания: ОДНА ИЗ  =  *=  /=  %=  +=  -=  >>=  <<=  &=  ~=  |=

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

Любая арифметическая или поразрядная операция может быть объединена со знаком «=» для образования присваивания. Поэтому выражение вида Е1 op= Е2 эквивалентно выражению Е1 = Е1 op Е2, за исключением того, что Е1 вычисляется только один раз.

Операция «++», употребленная перед операндом, увеличивает его на единицу и поставляет это значение; операнд должен быть l-значением арифметического или указательного типа. Выражение ++x эквивалентно выражению x+=1. Аналогичный смысл имеет операция «--»,употребленная перед операндом.

Все побочные выражения проявляются до выполнения следующего оператора. Оператор-выражение с опущенным выражением называется пустым оператором.

Вызовы функций будут рассмотрены при обсуждении функций.

  1.  Составной оператор

Составной оператор используется для задания нескольких операторов там, где по синтаксису ожидается один.

 составной-оператор:

 { список-операторовopt }

список-операторов:

 оператор

 список-операторов оператор

Заметим, что объявление тоже считается оператором, поэтому в составном операторе могут объявляться локальные переменные. Составной оператор с локальными объявлениями обычно называется блоком.

  1.  Выбирающие операторы

Выбирающие операторы выбирают один из нескольких путей исполнения программы.

 выбирающий-оператор:

 if (выражение) оператор

 if (выражение) оператор else оператор

 switch (выражение) оператор

Оператор в выбирающем операторе не может быть объявлением.

4.4.1. Условный оператор (оператор if)

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

"==", "!=", "<", ">", "<=", ">="

возвращают true (которое преобразуется в целое 1), если сравнение истинно, иначе возвращают false, которое преобразуется в 0. Поэтому выражение вычисляется и, если результат не равен нулю (равен true в Яве и C#), выполняется первый оператор. В противном случае, если есть часть else, выполняется ее оператор. Из этого следует, что if(a) эквивалентно if(a != 0).

Логические операции "&&" и "||" наиболее часто используются в условиях. Они не вычисляют второй операнд, если этого не требуется. Например,

if (p && 1 < p->count)

сначала проверит, является ли р нулем, и только если это не так, проверит

1 < p->count.

Некоторые условные операторы могут быть с удобством заменены условными выражениями, например:

 if (a <= b)

 max = b

else max = a:

лучше выражается так:

max = (a <= b) ? b : a;

4.4.2. Оператор выбора (оператор switch)

Оператор выбора передает управление одному из нескольких операторов в зависимости от значения выражения, которое должно быть числом или объектом класса, для которого существует однозначное преобразование к числу. Любой оператор может быть помечен меткой case:

 case константное-выражение:

где константное-выражение приводится к типу выражения из оператора выбора.

В одном операторе выбора может быть одна метка вида default

Выполнение оператора во всех языках заключается в вычислении выражения и сравнении его значения с метками выбора. Если одна из меток равна значению выражения, управление передается оператору, следующему за меткой. Если ни одна из меток выбора не равна значению выражения, управление передается оператору, помеченному меткой default, если есть таковой. Пример:

 switch (val) {

 case 1: f(); break;

 case 2: g(); break;

 default: h(); break;

}

Во всех языках необходимо заботиться о завершении каждой ветви, но в C# отсутствие завершения трактуется отлично от С++ и Явы. В С++ и Яве отсутствие завершения означает переход к исполнению следующей ветви. Например,

 switch (val) {

// осторожно!

 case 1: cout << "case1 \n";

 case 2: cout << "case2 \n";

 default: cout << "default: case не найден \n;

}

при val = 1 напечатает

 case1

case2

default: case не найден

В C# данный оператор будет считаться ошибочным, Чтобы выполнить ту же самую работу, он должен быть написан следующим образом:

switch (val) {

 case 1: cout << "case1 \n"; goto case 2;

 case 2: cout << "case2 \n"; goto default;

 default: cout << "default: case не найден \n;

}

Таким образом, в C# каждая ветвь должна заканчиваться либо каким-либо оператором передачи управления.

  1.  Операторы цикла

Операторы цикла задают многократное исполнение. Следующие три оператора имеют место во всех трех языках:

 оператор-цикла:

 while (логическое выражение) оператор

 do оператор while (логическое выражение)

 for (оператор-иниц-for условиеopt; приращениеopt) оператор

оператор-иниц-for:

 оператор-выражение

 оператор-объявление

приращение:

 выражение

Оператор в операторе-цикла не может быть объявлением. Заметим, что оператор-иниц-for всегда заканчивается точкой с запятой, потому что оба его варианта заканчиваются точкой с запятой.

4.5.1. Оператор ПОКА (while)

В операторе ПОКА входящий в него оператор повторно выполняется до тех пор, пока значение выражения не становится равным нулю (false). Проверка производится перед каждым исполнением оператора. Значение выражения должно быть логическим (в С++ также числом, указателем или объектом класса, для которого существует однозначное преобразование к числу или указателю).

4.5.2. Оператор ПОВТОРИТЬ (do)

В операторе ПОВТОРИТЬ входящий в него оператор повторно выполняется до тех пор, пока значение выражения не становится равным нулю (false). Проверка производится после каждого исполнения оператора. Значение выражения должно быть логическим (в С++ также числом, указателем или объектом класса, для которого существует однозначное преобразование к числу или указателю).

4.5.3. Оператор итерации for

Оператор итерации

 for (оператор-иниц-for условие opt; приращение opt) оператор

эквивалентен оператору

 оператор-иниц-for

 while (условие) {

 оператор

 приращение;

}

за исключением того, что оператор ПРОДОЛЖИТЬ (continue) в операторе приводит к выполнению приращения и далее очередному вычислению условия. Таким образом:

 оператор-иниц-for задает инициализацию, необходимую для выполнения

цикла,

 условие определяет проверку, выполняемую перед каждым повторением,

так что цикл завершается, когда условие становится ложным (равным нулю),

 приращение обычно осуществляет приращение переменных цикла после

каждого повторения. Пример:

 const int sz = 24;

int ia[sz];

for (int i = 0; i<sz; ++i) ia[i] = i;

Значение условия должно быть логическим (в С++ также числом, указателем или объектом класса, для которого существует однозначное преобразование к числу или указателю). Пропуск условия подразумевает, что фраза while в эквивалентном представлении сводится к while(true).

Если оператор-иниц-for является объявлением, область действия объявленных имен простирается до конца блока, содержащего оператор for. Этот оператор, как и приращение, может представлять собой список выражений, разделенных запятой. Вычисление этих выражений, как и в других операторах, происходит слева направо. Например, чтобы посредством двух индексов перебрать все элемента массива arr в противоположных направлениях, можно на Яве написать:

for (i = 0, j = arr.length-1; j >= 0; i++, j--) { . . . }

4.5.4. Оператор итерации foreach

Этот оператор присутствует только в C# и используется для последовательного доступа к элементам коллекций, для каждого из которых выполняется вложенный оператор. Он имеет следующий вид:

оператор-foreach:
foreach (тип-элемента  идентификатор  in   выражение )  оператор

Выражение, стоящее за служебным словом in, должно иметь тип коллекции. Таким типом является тип массива, а также любой тип C, который содержит открытый метод  E GetEnumerator(), где E – это тип структуры, класс или интерфейс. При этом тип E должен содержать открытый метод bool MoveNext() и открытое свойство Current типа тип-элемента, которое позволяет читать текущее значение элемента коллекции.

 

При работе оператора с переменной идентификатор посредством вызова метода MoveNext() последовательно связываются элементы коллекции, являющейся значением выражения, и исполняется оператор до тех пор, пока метод MoveNext() не выработает значение false. Пример:

double[,] values = {
{1.2, 2.3, 3.4, 4.5},
{5.6, 6.7, 7.8, 8.9}
};

foreach (double elementValue in values)
 Console.Write("{0} ", elementValue);
 Console.WriteLine();

Результат: 1.2  2.3  3.4  4.5  5.6  6.7  7.8  8.9

  1.  Операторы перехода

Операторы перехода передают управление безусловно. Наборы этих операторов в С++, Яве и C# немного различаются.

С++ и C#:

 оператор-перехода:

 break

 continue

 return выражениеopt

 goto метка

Ява:

 оператор-перехода:

 break меткаopt

 continue меткаopt

 return выражениеopt

При выходе из области действия вызываются деструкторы для всех сконструированных в области действия объектов, которые еще не были уничтожены к этому моменту. Это относится как к явно объявленным, так и временным объектам.

4.6.1. Оператор завершения (break)

В С++ и C# этот оператор может употребляться только в операторе цикла или операторе выбора и приводит к завершению наименьшего объемлющего оператора цикла или оператора выбора; управление передается оператору, следующему за завершенным.

Пример:

 switch (val) {

 case 1: f(); break;

 case 2: g(); break;

 default: h(); break;

}

В Яве этот оператор в принципе может употребляться для выхода из любого блока, хотя чаще всего, как и в С++, он используется для выхода из оператора цикла или оператора выбора. При этом break без метки завершает работу непосредственно охватывающего его цикла или выбора. Чтобы выйти из внешнего оператора, необходимо снабдить его меткой и указать ее в операторе break. Пример:

 float[][] Matrix;

boolean workOnFlag(float flag) {

 int x, y;

 boolean found = false;

search:

 for (y = 0; y < Matrix.length; y++) {

  for (x = 0; x < Matrix[y].length; x++) {

   if (Matrix[y][x] == flag) {found = true; break search;}

  }

 }

 if (!found) return false;

  // сделать что-нибудь с найденным элементом массива

 return true;

}

4.6.2. Оператор продолжения (continue)

Оператор может употребляться только в операторе цикла и приводит к передаче управления на вычисление управляющего условия (приращения в цикле for). Более точно, в каждом из операторов

 while (foo) { do { for (;;) {

 // ...  // ...  // ...

 contin: ;  contin: ;  contin: ;

}  } while (foo)  }

оператор continue, если он не содержится во вложенном операторе цикла, эквивалентен goto contin. Пример:

 while (cin) {

 // . . .

 if (cur_tok == PRINT) continue;

 cout << expr() << '\n';

}

эквивалентно

 while (cin) {

 // . . .

 if (cur_tok == PRINT) goto end_of-loop;

 cout << expr() << '\n';

end_of-loop: ;

}

В Яве в таком операторе может указываться метка внешнего цикла. В этом случае continue относится к этому циклу, а не тому, в который он непосредственно вложен.

4.6.3. Оператор возврата (return)

Функция возвращает управление в точку вызова посредством оператора return. Оператор return без выражения можно употреблять только в функциях, не возвращающих значения, в конструкторах и деструкторах.

Оператор return с выражением можно употреблять только в функциях, возвращающих значение; оно передается в точку вызова и, если необходимо, преобразуется к требуемому функцией типу.

4.6.4. Оператор перехода (goto)

В С++ и C# этот оператор передает управление оператору, помеченному указанным идентификатором. В C# кроме того в качестве метки может использоваться метка выбора. В программировании высокого уровня оператор имеет мало применений и его следует избегать. Одно из немногих разумных его применений состоит в выходе из вложенного цикла или переключателя (break прекращает выполнение только самого внутреннего охватывающего его цикла или переключателя). Пример:

 void f() {

 int i, j;

 for (int i = 0; i < n; i++)

  for (int j = 0; j < m; j++)

   if (nm[i][j] == a) goto found; // найдено

   // не найдено

 // . . .

 found: // найдено

 // nm[i][j] == a

}

В Яве этого оператора нет.

  1.  Оператор-объявление

Оператор-объявление вводит в текущем блоке новый идентификатор.

 оператор-объявление:

 объявление

Если идентификатор, введенный объявлением, был ранее объявлен во внешнем блоке, внешнее объявление скрывается в оставшейся части блока и восстанавливается при выходе из него.


  1.  Функции

В программе на С++ можно объявить любое количество функций. В программе на Яве или C#  функция может быть только методом класса.

  1.  Описание функции

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

 описание-функции:

 спецификаторыopt описатель тело-функции

описатель:

 тип-результатаopt идентификатор (список-параметровopt) cv-описательopt

список-параметров:

 описание-параметра , список параметров

описание-параметра:

 тип идентификаторopt

cv-описатель:

 const

 volatile

тело-функции:

 составной оператор

Пример:

 int max(int a, int b, int c)

{

 int m = (a > b) ? a : b;

 return (m > c) ? m : c;

}

Здесь int – тип результата, max имя функции, (int a, int b, int c) – список параметров, остальное – тело функции.

Объявление функции не содержит тела функции. Типы в объявлении и описании функции должны совпадать, например:

 extern void swap(int*, int*);// объявление

void swap(int* p, int* q)  // описание

{

 int t = *p;

 *p = *q;

 *q = *t;

}

Типы параметров, тип результата и cv-описатель являются частями типа функции. При этом cv-описатель может быть частью только объявления или описания нестатической компонентной функции или указателя на компонентную функцию класса.

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

 f(),

 *fpi(int),

 (*pif)(const char*, const char*);

Здесь объявляются функция f без параметра с целым результатом, функция fpi с целым параметром и указателем на целое как результатом и указатель pif на функцию, которая имеет два параметра – указатели на константные литеры – и целый результат.

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

 void print (int a, int)

{ printf(«a = %d\n», a) }

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

В С++ функцию можно определить со спецификатором inline. Такая функция называется встроенной. Пример:

inline int fac(int n) {return (n <2)? 1: n*fac(n-1);}

Спецификатор inline указывает компилятору, что он должен пытаться встраивать код тела функции по месту ее вызова. Обычно такими объявляются функции с очень простым телом, так что встройка кода по месту вызова оказывается экономичнее организации вызова функции как подпрограммы. Компилятор, однако может и не встраивать функцию, если она, например, рекурсивна, слишком велика или определяется указатель на нее. В Яве и C# аналогичной конструкции нет.

В С++ в объявлении параметра может быть задано выражение, оно воспринимается как значение параметра по умолчанию. При этом все следующие параметры также должны иметь значения по умолчанию, заданные в этом или предыдущих объявлениях функции. Значения по умолчанию подставляются в вызов функции при отсутствии в нем соответствующих фактических параметров. Пример. Функция, объявленная как

point(int = 3, int = 4);

может быть вызвана следующими способами:

point(1, 2);  point(1);   point();

Локальная переменная функции создается заново (в стеке) при каждом вызове функции и инициализируется соответственно ее типу или значением инициализатора, если есть таковой. Однако, если локальная переменная объявлена как static, при всех вызовах функции будет использоваться единственный статически размещаемый в памяти объект, который будет проинициализирован только один раз. Пример:

 void f (int n) {

 while (n--) {

  static int n = 0; // инициализируется один раз

  int x = 0; // инициализируется при каждом вызове f

  cout << “n==” << n++ << “, x ==” << x++ ‘\n’;

 }

}

int main() {

 f(3);

}

В результате работы main будет напечатано:

 n == 0, x == 0

n == 1, x == 0

n == 2, x == 0

Таким образом, статическая переменная позволяет функции «помнить о прошлом», не создавая при этом глобальной переменной, к которой могли бы обратиться (и испортить ее) другие функции.

Имя функции является константой, потому присваивание функции невозможно.

В Яве описание функции (метода) состоит из заголовка функции и тела функции. Заголовок функции состоит из возможных модификаторов доступа (будут рассмотрены при обсуждении классов), типа результата, имени функции, возможного списка формальных параметров и возможного опиcателя throws. Для каждого формального параметра указывается возможный описатель final, тип параметра и его идентификатор. Тело функции – это блок. Описатель final указывает, что соответствующий параметр не может быть модифицирован в теле функции. Примеры:

void windowClosing(WindowEvent e) {тело};

public int compare( final Object o1, final Object o2 ) {тело};

В Яве существует понятие сигнатуры метода, состоящей из его имени и типов параметров. Путем сопоставления сигнатур методов определяется, могут ли в классе быть описаны несколько методов с данным именем. Например, сигнатуры предыдущих методов выглядят следующим образом:

windowClosing(WindowEvent);

compare(Object, Object);

В C# описание метода аналогично описанию метода в Яве. Основной особенностью является разграничение видов параметров. Перед каждым из них может стоять одно из служебных слов ref, out или params. Таким образом, в языке существует четыре вида формальных параметров:

  1.  передаваемые по значению (нет служебного слова перед типом параметра);
  2.  передаваемые по ссылке (служебное слово ref перед типом параметра);
  3.  выходные параметры (служебное слово out перед типом параметра);
  4.  параметры переменной длины (служебное слово params перед типом параметра).

Сигнатура метода в C# строится так же, как и в Яве, но при этом отмечаются и виды параметров (значение, ссылка или выходной); при этом параметр, помеченный как params, считается параметром-значением.

  1.  Вызов функции и подстановка параметров

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

Благодаря наличию типа ссылки, в С++ возможна модификация фактического параметра. Рассмотрим функцию

 void f(int val, int& ref)

{

 val++; ref++;

}

В данном примере первый параметр передается значением. А второй – ссылкой. Когда вызывается f, выражение val++ увеличивает локальную копию первого фактического параметра, тогда как ref++ увеличивает второй фактический параметр. Использование функций, которые изменяют фактические параметры, может сделать программу трудно понимаемой и потому таких параметров лучше избегать. Однако передача большого объекта по ссылке может быть эффективнее, чем его передача по значению. В этом случае можно объявить такой параметр как const, чтобы не позволить функции изменить значение подставляемого объекта. Пример:

 void f(const large& arg)

{

 // значение "arg" не может быть изменено

}

Аналогично, если параметр, являющийся указателем, описывается как const, подразумевается, что функция не меняет значение указываемого объекта. Пример:

void strcopy(char* to, const char* from);

Если в качестве параметра функции используется массив, подставляется указатель на его первый элемент. Пример:

int strlen(const char*);

void f()

{

 char v[] = "an array";

 strlen(v);

 strlen("Nicolas");

};

Иначе говоря, Т[] преобразуется к Т*, когда он передается функции. Следовательно, присваивание элементу параметра-массива меняет значение фактического параметра-массива. Таким образом, массив не подставляется (и не может подставляться) значением.

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

void compute(int* vec_prt, int vec_size);

В Яве все параметры передаются по значению. Другими словами, формальным параметрам метода присваиваются значения фактических параметров при его вызове. Таким образом, если фактическим параметром является переменная, то она не будет изменена при работе метода, даже если в нем осуществляется присваивание соответствующему формальному параметру.

Однако, если параметр метода имеет тип класса объектов, то в качестве фактического параметра передается ссылка на объект, а не сам объект! Следовательно, можно менять в методе состояние того объекта, на который она ссылается, значение ссылки остается прежним.

В C# параметр-значение имеет ту же семантику, что и в Яве, а параметр-ссылка – ту же, что и параметр типа ссылки в С++. Основное требование к фактическому параметру этого вида: это должна быть инициализированная переменная. Таким образом, в этом языке может быть изменена переменная простого или структурного типа, подставляемая в качестве фактического параметра. Пример:

void Swap(ref int x, ref int y) {
 int temp = x;
 x = y;
 y = temp;
} 

Данный метод имеет следующую сигнатуру:

Swap(ref int, ref int).

Дополнительной особенностью C# являются выходные параметры, унаследованные из Ады. Здесь так же, как и в предыдущем случае, фактическим параметром должна быть переменная, но не требуется ее предварительная инициализация. Поэтому в теле метода такой параметр считается неинициализированным и должен быть инициализирован до его первого использования. Выходные параметры используются в том случае, когда функция должна возвращать несколько значений. Пример:

void SplitPath(string path, out string dir, out string name) {
int i = path.Length;
while (i > 0) {
 char ch = path[i – 1];
 if (ch == '\\' || ch == '/' || ch == ':') break;
  i--;
}
dir = path.Substring(0, i);
name = path.Substring(i);
}

Данный метод имеет следующую сигнатуру:

SplitPath(string, out string, out string).

Если одним из аргументов метода может быть одномерный массив произвольной длины, то такой параметр описывается последним в списке как тип массива с видом params. Заметим, что это может быть и массив ссылок, то есть допустимы параметры типов int[] и int[][], но не могут описываться параметры типа int[,]. При вызове метода с таким параметром возможны два варианта передачи аргументов:

  1.  Выражение, имеющее тип массива с типом элементов, неявно преобразуемых к указанному типу элементов. В этом случае имеет место обычная подстановка значением (т.е., подставляется ссылка на аргументный массив).
  2.  Аргументом является список выражений (т.е., несколько аргументов), тип которых неявно преобразуем к указанному типу элементов массива. В этом случае при вызове метода создается временный массив соответствующей длины, в который копируются (преобразованные) значения выражений, и ссылка на этот массив передается функции в качестве аргумента. Пример:

void F(params int[] args) {
Console.Write("Array contains {0} elements:", args.Length);
foreach (int i in args)
 Console.Write(" {0}", i);
Console.WriteLine();
}

Теперь при наличии описания переменной

int[] arr = {1, 2, 3};

возможны следующие вызовы функции:

F(arr);
F(10, 20, 30, 40);
F();

Заметим, что в последнем случае функция вызывается с аргументным массивом нулевой длины.

Данный метод имеет следующую сигнатуру:

F(int[]).

В предыдущих примерах ситаксис вызова функции в C# был аналогичен синтаксису вызова функции в С++ и Яве. Это, однако, справедливо только для параметров-значений и параметров-массивов. Если же в описании функции формальный параметр помечен как ref или out, то так же должен быть помечен и соответствующий фактический параметр. Пусть мы имеем следующие описания переменных:

int xx = 1, yy = 2;

string s1 = “qwert”, s2, s3;

Тогда возможны следующие вызовы ранее описанных функций:

Swap(ref xx, ref yy);

SplitPath(s1, out s2, out s3);

  1.  Возврат значения

Функция, объявленная не как void, должна возвращать значение. Например:

 int f() {} // ошибка

void g() {} // все в порядке

Возвращаемое значение указывается в операторе return. Например:

int fac(int n) { return (n >1) ? n*fac(n-1) : 1 }

В функции может быть больше одного оператора return, например:

 int fac(int n)

{

 if  (n >1)  return n*fac(n-1)

 else return 1

}

Как и семантика подстановки параметров, семантика возврата значения идентична семантике инициализации: тип возвращаемого значения проверяется на согласованность с объявленным типом результата и выполняются необходимые преобразования типа. Пример:

 double f()

{

 // . . .

 return 1; // неявно преобразуется к double(1)

}

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

 int* f() {

 int local = 1;

 // . . .

 return &local; // ошибка

}

Аналогичная ситуация при использовании ссылок:

 int& f() {

 int local = 1;

 // . . .

 return local; // ошибка

}

В Яве и C# таких проблем нет.

Функция, объявленная как void,  может не содержать оператора return вообще, в этом случае возврат происходит при достижении конца блока функции.

  1.  Совмещение имен функций

Когда несколько функций выполняют сходную работу над объектами разных типов, может быть удобно дать им одно и то же имя. Этот механизм называется совмещением имен (overloading). Пример:

 void print(int)

void print(const char*)

Когда вызывается функция с совмещенным именем f, компилятор должен понять, к какой из функций с именем f следует обратиться. Для этого типы всех фактических параметров в вызове функции сравниваются с типами формальных параметров всех функций с именем f. Задача заключается в том, чтобы вызвать функцию, которая лучше всех (по типам параметров) соответствует вызову или выдать сообщение об ошибке, если подходящей функции нет. Пример:

 void print(double);

void print(long);

void f() {

 print(1L); // print(long)

 print(1.0); // print(double)

 print(1); // ошибка: неясно, print(long) или print(double)

 };

В упрощенном виде правила согласования типов параметров применяются в следующем порядке:

1) Точные совпадения, т.е. согласования, которые совсем не используют или  используют преобразования, без которых нельзя обойтись (например,  преобразование имени массива к указателю, имени функции к указателю на  функцию и Т к const T).

2) Согласования, использующие приведение к арифметическому типу,  например, char в int, short в int и их беззнаковых аналогов unsigned, а

также float в double, int во float и т.д.

3) Согласования, которые используют преобразования, определенные  пользователем.

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

void print(int)

void print(const char*)

void print(double);

void print(long);

void print(char);

void h(char c, int i, short s, float f) {

 print(c); // точное согласование, вызывается print(char)

 print(i) // точное согласование, вызывается print(int)

 print(s) // приведение к числовому типу, вызывается

    // print(int)

 print(f) // приведение к числовому типу, вызывается

    // print(double)

 print('a') // точное согласование, вызывается print(char)

 print(49) // точное согласование, вызывается print(int)

 print("a") // точное согласование, вызывается print(const char*)

 }

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

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

  1.  Найти все методы, к которым может относиться данный вызов, т.е. построить список всех методов с данным именем, данным количеством параметров таких типов, что параметрам могут быть присвоены значения аргументов (с возможными преобразованиями). Если находится всего один такой метод, вызывается именно он.
  2.  Если остается список методов, каждый из которых подходит для вызова, то первый метод в списке сопоставляется с остальными. Если есть в списке еще один метод с более точным согласованием типов параметров, первый метод исключается из списка. Эта процедура повторяется до тех пор, пока остается возможность удаления методов из списка.
  3.  Если остался ровно один метод, то совпадение для него оказалось наиболее точным, и он вызывается. Если же осталось несколько методов, то вызов является неоднозначным и, следовательно, недопустимым.

В C# используется похожий алгоритм со следующими дополнениями:

  1.  в списке кандидатов могут быть только функции, у которых вид каждого формального параметра совпадает с видом соответствующего фактического параметра;
  2.  у фактического и формального параметра, имеющего вид ref или out, должен быть один и тот же тип (невозможны преобразования типов).

  1.  Указатель на функцию в С++

С функцией в С++ можно делать только две вещи: вызывать ее и брать ее адрес. Указатель, полученный взятием адреса функции, можно затем использовать для ее вызова. Пример:

 void error(char* p) { /* . . . */};

void (*efct)(char*);  // указатель на функцию

void f()

{

 efct = &error;  // efct указывает на error

 (*efct)("error");  // вызов error через efct

}

Чтобы вызвать функцию через указатель, его надо сначала разыменовать. Поскольку операция вызова функции () имеет более высокий приоритет, чем операция разыменования "*", нельзя написать просто *efct("error"), что означает *(efct("error")). Можно, однако, написать efct("error"), и компилятор обнаружит, что efct - это указатель и правильно вызовет функцию.

Часто бывает полезен массив указателей на функции.


  1.  Классы и объекты

Классы служат для определения типов объектов. Объект отличается от значения тем, что он обладает состоянием, которое может меняться в течение жизни объекта. В то время как тип данных определяет множество значений посредством множества операций, нельзя сказать то же самое о типе объектов. Никакой тип объектов не определяет множества объектов. Скорее тип объектов определяет множество состояний объекта посредством множества методов. Среди методов объекта мы можем выделить конструкторы, служащие для инициализации объектов, мутаторы, служащие для изменения состояния объектов, и анализаторы, служащие для анализа состояния объекта. В С++ методы называются компонентными функциями (member functions).

В С++ не делается различия между типами данных и типами объектов, типы объектов в нем – это просто пользовательские типы данных. В Яве и C# это различие проводится, так что объект всегда имеет внутренний идентификатор (ссылку), а значение любого типа данных идентифицирует само себя. В Яве в дополнение к типам объектов (классам), определяемым программистом на этом языке, существует встроенный тип объектов для каждого из встроенных типов данных. В C# значение любого типа данных может быть преобразовано («упаковано») в объект, если оно используется там, где требуется объект. В дальнейшем, употребляя термин «тип», мы будем иметь в виду либо тип данных, либо тип объекта.

  1.  Описание класса

6.1.1. Компоненты класса

Рассмотрим реализацию работы с датами посредством структуры Date и набора функций:

 

 struct Date {

 int day, month, year;

};

void set(Date&, int, int, int);

void add_year(Date&, int);

void add_month(Date&, int);

void add_day(Date&, int);

В данном примере нет явной связи между типом данных и функциями. Такую связь можно установить, объявив функции компонентами структуры:

 struct date {

 int day, month, year;

 void set(int, int, int);

 void add_year(int);

 void add_month(int);

 void add_day(int);

};

Переменные класса (в С++ структура является одним из видов класса) обычно называются полями, в приведенном выше примере – это day, month и year. Каждый экземпляр класса (объект) обладает отдельным экземпляром своих полей. Текущие значения полей определяют текущее состояние данного объекта.

Во всех языках спецификатор static в объявлении поля означает, что поле относится скорее к классу целиком, чем к отдельному объекту. В Яве спецификатор final, а в C# – спецификатор readonly, в объявлении поля указывает, что значение переменной присваивается только раз, при ее инициализации. Наличие обоих спецификаторов и в том, и в другом языке означает объявление константы.

Заметим, что в C# константа класса может быть объявлена двумя способами: либо путем использования спецификатора const, либо путем использования спецификаторов static и readonly. Различие между ними заключается в том, что в первом случае константа инициализируется компилятором (будет выдано сообщение об ошибке, если это невозможно), а во втором – в процессе выполнения программы (различие может быть важным при перекомпиляции отдельных модулей программы). Более того, в первом случае могут определяться константы только примитивных типов, а во втором – любых типов.

Функции, объявленные внутри объявления класса, называются компонентными функциями в С++ и методами в Яве и C# , и их можно вызывать только для переменной соответствующего типа с использованием стандартного синтаксиса доступа к компонентам структуры, например:

 Date my_birthday;

void f() {

 Date today;

 today.set(16, 10, 2000);

 my_birthday.set(12, 2, 1943);

 Date tomorrow = today;

 tomorrow.add_day(1);

  . . .

}

В Яве и C# компонентная функция (метод) описывается полностью внутри класса (вспомним, что в этих языках нет разницы между объявлением и описанием). В С++ компонентная функция может описываться как внутри класса, так и вне его. Так как различные классы могут иметь компонентные функции с одним и тем же именем, то в последнем случае описание функции уточняется именем класса, например:

 void Date::set(int d, int m, int y) {

 day = d;

 month = m;

 year = y;

}

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

В теле компонентной функции имена компонентов того же класса можно использовать без указания объекта, к которому они относятся: считается, что имя относится к компоненту того объекта, для которого вызвана функция. Например, если Date::set() вызывается для today, то month = m означает today.month = m.

Следующий вид компонентов класса во всех трех языках – конструкторы, служащие для инициализации объектов данного класса. В С++ и C# существуют также деструкторы, служащие для уничтожения существующих объектов.

В C# имеются три дополнительных вида компонентов класса: свойства, представляющие собой дополнительные (как правило, встраиваемые) функции, служащие для доступа к хранимым компонентам объекта,  события, служащие для посылки извещений объектом или классом, и индексаторы, позволяющие работать с объектами как с массивами.

6.1.2. Управление доступом к компонентам класса

Объявление типа Date в предыдущем разделе предоставляет набор функций для работы с объектами этого типа. Однако оно не указывает, что только эти функции могут осуществлять доступ к полям объекта этого типа. Эти ограничения в С++ можно отразить, воспользовавшись в ключевым словом class вместо struct (в Яве есть только class):

 class Date {

 int day, month, year;

 public

 void set(int, int, int);

 void add_year(int);

 void add_month(int);

 void add_day(int);

};

Метка доступа public разделяет тело класса на две части. Компоненты, перед которыми нет метки доступа, считаются помеченными меткой private (ее можно указать и явным образом) и называются скрытыми компонентами. Эти компоненты могут использоваться только в телах методов классов. Компоненты с меткой доступа public называются открытыми (к ним может обращаться любая функция) и образуют интерфейс класса. Структура в С++ является на самом деле классом, в котором все компоненты открыты по умолчанию. Компонент класса может еще быть помеченным как protected. Такой компонент называется защищенным, и он может быть также использован в телах методов производных классов.

В Яве правила доступа к компонентам класса несколько отличаются от приведенных выше. В этом языке существуют четыре вида компонентов:

  1.  открытый (public) компонент доступен в любом месте, в котором доступен сам класс;
  2.  закрытый (private) компонент доступен только в методах своего класса;
  3.  защищенный (protected) компонент доступен также в методах производного класса (подкласса) и в функциях, входящих в тот же пакет;
  4.  пакетный (без метки доступа) компонент доступен только в том пакете, в который входит данный класс.

В C# существуют пять видов компонентов:

  1.  открытый (public) компонент доступен в любом месте, в котором доступен сам класс;
  2.  защищенный (protected) компонент доступен также в методах производного класса (подкласса);
  3.  внутренний (internal) компонент доступен в пределах текущей единицы трансляции (аналогичен пакетному компоненту Явы);
  4.  защищенный внутренний (protected internal) компонент доступен в пределах текущей единицы трансляции и в методах производного класса (аналогичен защищенному компоненту Явы);
  5.   закрытый (private) компонент доступен только в методах своего класса;

В языке допускается отсутствие метки доступа; в этом случае, как и в С++, по умолчанию предполагается private.

Ограничение доступа к компонентам класса явно объявленным списком методов обеспечивает инкапсуляцию состояния объекта и абстрактное его представление для внешнего мира. Благодаря этому, например, ошибка, в результате которой некоторая дата приняла неверное значение (например, 36 декабря 2000 года) может быть вызвана только неверным кодом соответствующего метода.  Из этого следует, что первая стадия отладки – локализация ошибки – может быть завершена еще до запуска программы. При этом, если нам потребуется изменить представление класса (т.е. состав и типы его полей), нам потребуется только изменить методы класса. Код пользователя непосредственно зависит только от интерфейса класса, и его не придется переписывать при изменении представления класса (хотя может потребоваться его перекомпиляция). Другое преимущество заключается в том, что для работы с классом пользователю достаточно ознакомиться только с его интерфейсом. Заметим, что в Яве пределах одного пакета открытые, защищенные и пакетные компоненты равноправны с точки зрения их доступности.

6.1.3. Управление доступом к классу

В С++ любой класс по определению считается открытым и может использоваться любой программой. Не так обстоит дело в Яве и C#. В Яве класс, описанный без метки доступа, может использоваться только в своем пакете, Для того, чтобы он мог использоваться в других пакетах, перед его описанием необходимо поместить метку public.  Точно так же в C# класс может быть помечен меткой public или internal с той же самой семантикой (класс может использоваться только в данной единице компиляции при отсутствии метки доступа).

6.1.4. Константные компонентные функции

Определенный выше класс Date предоставляет функции, которые изменяют состояние объектов этого класса. К сожалению, мы не обеспечили способа проверки состояния объектов. Эту проблему легко решить, добавив функции, возвращающие значения дня, месяца и года:

 class Date {

  int day, month, year;

 public:

  int day() const {return day;}

  int month() const {return month;}

  int year() const;

}

В С++ служебное слово const после списка (в данном случае пустого) параметров указывает, что функция не изменяет состояние объекта. Такая функция называется константной. Естественно, компилятор обнаружит нарушение этого намерения (не менять состояние) и выдаст сообщение об ошибке. Пример:

 inline int Date::year() const {

 return year++; // ошибка: попытка изменить значение поля 

}

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

 inline int Date::year() const  // правильно

{ return year;  

}

Константную компонентную функцию можно вызывать как для константного, так и для переменного объекта, в то время как обычную функцию нельзя вызывать для константного объекта (она может его изменить, что недопустимо). Примеры:

 void f(Date& d, const Date& cd) {

 int i = d.year(); // правильно

 d.add_year(1); // правильно

 int i = cd.year(); // правильно

 cd.add_year(1); // ошибка: нельзя изменить значение константы

}

В Яве и C# нет константных объектов и потому нет константных методов: любой метод может менять состояние объекта, к которому он применен.

  1.  Создание и использование объектов и их компонентов

6.2.1. Объекты и указатели на них

После описания класса можно объявить одну или несколько переменных и констант, содержащих объекты данного класса, например:

Date date1, date2;

После обработки такого объявления в С++ каждой переменной выделяется память для хранения объекта (т.е. всех полей) данного класса. В Яве и C# такой переменной выделяется память под хранение ссылки на объект (уникального идентификатора объекта). Для дальнейшего доступа к открытым полям и методам объекта используется операция «.» (точка), аналогичная операции доступа к полям структуры, например:

date1.set(1, 1, 2000);

Если Date – это класс Явы или C#, тогда, прежде чем работать с переменной, надо сначала создать объект, а затем его инициализировать, например:

 date1 = new Date;

date1.set(1, 1, 2000);

В С++ также можно описать переменную-указатель на объект:

Date* pd;

Такая переменная сначала инициализируется указателем на конкретный объект после чего с этим объектом можно оперировать посредством операции «->», например:

 pd = new Date;

pd->set(1, 1, 2000);

x = pd->day(); // подразумевается, что х – это целая переменная

6.2.2. Указатели на компоненты класса

В С++ можно также объявить указатель на компонент класса. В упрощенном виде такое объявление имеет следующий вид:

тип имя-класса :: * cv-описательopt идентификатор

Тип идентификатора в таком объявлении есть «cv-описательный указатель на компонент класса имя-класса типа тип». Пример:

 class X {

public:

 void f(int);

 int a;

};

int X ::* pmi = &X::a;

void (X::* pmf)(int) = &X::f;

Здесь pmi и pmf объявлены (переменными) указателями на компоненты класса Х типа int и void(int) соответственно и инициализированы ссылками на соответствующие компоненты. Для работы с такими указателями используются операции «.*» и «->*». У первой операции слева должен стоять объект данного класса, а у второй – указатель на объект. Справа у обеих операций стоит указатель на компонент класса. Их можно использовать следующим образом:

 X obj, *prt;

// ...

obj.*pmi = 7; // присваивает 7 компоненту класса типа int в obj

 (obj.*pmf)(9); // вызов компонентной функции с параметром 9 для obj

prt->*pmi = 7; // присваивает 7 компоненту класса типа int в prt

(prt->*pmf)(9) // вызов компонентной функции с параметром 9 для prt

Указатель на компонент класса не может указывать на статический компонент класса.

Значением указателя на компонент класса в реализации обычно является смещение (количество байтов) данного компонента относительно начала объекта. При работе операций «.*» и «->*» это смещение добавляется к адресу объекта. Указатели на компоненты класса могут быть полезны в качестве компонентов массивов.

6.2.3. Указатель this

Функции-мутаторы состояния add_year, add_month и add_day были определены выше как не возвращающие значения. При использовании подобных связанных функций иногда возникает желание выстроить операции в цепочку. Для этого требуется, чтобы функции возвращали ссылку на измененный объект. Например, мы могли бы написать на С++:

 void f (Date& d) {

  . . .

 d.add_day(1).add_month(1).add_year(1);

  . . .

}

или примерно то же самое на Яве и C#:

 void f (Date d) {

  . . .

 d.add_day(1).add_month(1).add_year(1);

  . . .

}

чтобы добавить к d один день, один месяц и один год. Для этого надо, чтобы функции возвращали ссылку на Date. Их объявления в таком случае на С++:

 class Date {

  . . .

 public Date& add_year(int n); // прибавить n лет

 public Date& add_month(int n); // прибавить n месяцев

 public Date& add_day(int n); // прибавить n дней

  . . .

 }

и на Яве:

 class Date {

  . . .

 public Date add_year(int n){/* тело функции */};

 public Date add_month(int n){/* тело функции */};

 public Date add_day(int n){/* тело функции */};

  . . .

 }

Каждая из этих функций должна возвращать объект, для которого она вызвана. Этой цели служат указатель this в C++ и ссылка this в Яве и C#. Каждый из них указывает на объект, к которому применена функция. Например функция add_year может быть описана на С++ следующим образом:

 Date& Date::add_year(int n) {

 if (d == 29 && m == 2 && !leapyear(y+n)) //leapyear – високосный год

 {

  d = 1;

  m = 3;

 }

 y += n;

 return *this;

 }

На Яве и C# в этом случае возвращается просто this.

Таким образом, для класса Х в С++ this имеет тип Х*, а в Яве и C# Х. Однако, это не обычная переменная: нельзя получить ее адрес или присвоить ей значение (т.е. ее можно считать неименованной константой). В константной компонентной функции класса Х (С++) this имеет тип const X* для предотвращения модификации самого объекта. Пример на С++:

 struct s {

 int a;

 int f() const;

 int g() {return a++;};

 int h() const {return a++} // ошибка

}

int s::f() const {return a}

Выражение а++ в теле функции s::h недопустимо, поскольку оно изменяет часть а объекта, для которого вызывается s::h(). Это как раз делать нельзя в константной компонентной функции класса, где this  (а –  короткая запись для this->a) есть указатель на const, т.е. *this – константа.

Как видно из предыдущего примера, при ссылке на компонент использование this излишне. В C++ он главным образом используется при написании компонентных функций, работающих непосредственно с указателями. При этом указатель на объект, для которого вызвана компонентная функция, является ее скрытым параметром. Типичный пример - функция, вставляющая звено в двусвязный список:

 struct dlink {

 dlink* pre;

 dlink* suc;

 void append(dlink* p);

};

void dlink::append(dlink* p)

{ if (suc) suc->pre = p;   // то есть, this->suc->pre = p

 p->suc = suc;  // то есть, p->suc = this->suc

 p->pre = this;  // явное использование this

 suc = p;   // то есть, this->suc = p

};

 dlink* list_head;

void f(dlink* a, dlink*b) {

 list_head->append(a);

 list_head->append(b);

};

  1.  Конструкторы и деструкторы

6.3.1. Конструкторы

Использование функций типа set для инициализации объектов класса неэлегантно и подвержено ошибкам. Так как нигде не сказано, что объект должен быть проинициализирован, программист может забыть об этом или сделать это дважды (часто с одинаково разрушительными последствиями). Лучшим подходом является предоставление программисту возможности создать функцию, имеющую явное назначение – инициализация объектов. Такая функция во всех языках называется конструктором (на самом деле ничего она не конструирует, вернее было бы назвать ее инициализатором). Конструктор в отличие от метода не может возвращать значения. Он распознается тем, что имеет то же имя, что и класс, в котором он определен. Пример:

 class Date {

  . . .

 Date(int d, int m, int y);

};

Если класс имеет конструктор, то объект этого класса при его создании будет проинициализирован. Если конструктору требуются аргументы, они должны быть предоставлены. Примеры на С++:

 Date today = Date(12, 8,2000);

Date xmas(25, 12, 2001); // сокращенная форма

Date my_birthday;  // ошибка: отсутствует инициализация

Date release_0(10, 12) // ошибка: отсутствует третий аргумент

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

class Date {

 int day, month, year;

public:

 Date(int, int, int); // день, месяц, год

 Date(int, int); // день, месяц, текущий год

 Date(int); // день, текущие месяц и год

 Date(); // дата по умолчанию -- сегодня

 Date (const char*); // дата в строковом представлении

};

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

 Date today(4);

Date july4(“July 4, 2000”);

Date now;

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

При создании в динамической памяти экземпляра класса, имеющего конструктор, также задаются аргументы конструктора, например:

 Date ptr1 = new Date(12, 8,2000);

Date ptr2 = new Date(“July 4, 2000”);

Таким образом, инициализация посредством конструктора является более компактной и более надежной, чем без него.

Поскольку в Яве и C# объекты всегда создаются в свободной памяти, инициализация объекта посредством конструктора имеет тот же вид, что и инициализация объектов свободной памяти в С++. Правила написания конструкторов остаются практически теми же самыми. Другой конструктор этого же класса может быть вызван в теле конструктора через ссылку this. Пример:

 class Body {

 public long idNum;

 public String name = “<unnamed>”;

 public Body orbits = null;

 private static long nextId = 0;

 Body() { idNum = nextID++; };

 Body(String bodyName, Body orbitsAround) {

  this();

  name = bodyName;

  orbits = orbitsAround;

 }

}

6.3.2. Конструктор по умолчанию

Конструктор, который может быть вызван без аргументов, называется конструктором по умолчанию. Пример:

 class Name {

 const char* s;

  . . .

};

 

class Table {

 Name* p;

 int sz;

public:

 Table(int s = 15) {p = new Name[sz = s];}

  . . .

};

В данном примере число 15 является аргументом по умолчанию, поэтому Table::Table(int) является конструктором по умолчанию. Если в классе объявлен конструктор по умолчанию, то он и будет вызван при отсутствии аргументов. В противном случае (и если в классе не объявлено других конструкторов) компилятор сгенерирует собственный конструктор по умолчанию. Такой конструктор неявно вызывает конструкторы по умолчанию для полей класса и конструкторы по умолчанию базовых классов (при создании объекта производного класса). Пример:

 struct Tables {

 int i;

 int vi[10];

 Table t1;

 Table vt[10];

};

Tables tt;

В этом примере tt будет проинициализирована сгенерированным конструктором по умолчанию, который вызовет Table(15) для tt.t1 и каждого элемента массива tt.vt. С другой стороны, tt.i и элементы массива tt.vi останутся не проинициализированными, потому что их тип не является классом. Причина различной обработки классов и встроенных типов заключается в требовании совместимости с С.

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

 struct X {

 const int a;

 int& r;

};

X x;  // ошибка: нет конструктора по умолчанию для Х

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

В классах C# также возможны конструкторы по умолчанию с той же семантикой, что и в С++.

6.3.3. Деструкторы

Как было сказано выше, конструктор инициализирует объект. Другими словами, они создают среду, в которой работают методы. Иногда создание такой среды подразумевает захват каких-то ресурсов (например, файлы или участки динамической памяти), которые должны быть освобождены после их использования. В результате некоторым классам требуется функция, которая будет гарантированно вызвана при уничтожении объекта, аналогично конструктору, который гарантированно вызывается при создании объекта. Такая функция в С++ и C# называется деструктором. Как правило, деструктор очищает память и освобождает ресурсы. В С++ деструктор вызывается неявно, когда автоматическая переменная выходит из области видимости, удаляется объект из свободной памяти и т.п. Только в очень необычных ситуациях пользователю приходится уничтожать объект явным образом. В C# деструктор всегда вызывается неявно сборщиком мусора, когда объект становится недоступным. Поэтому в этом языке он главным образом используется для освобождения таких ресурсов как файлы (он может освобождать и оперативную память, если вызывается в так называемом небезопасном режиме, в котором может использоваться код языка С).

В С++ деструктор чаще всего используется для освобождения памяти, выделенной конструктором. Рассмотри простую таблицу элементов типа Name. Конструктор класса Table должен выделить память для хранения элементов таблицы. После того, как таблица уничтожена каким-то образом, мы должны быть уверены, что эта память будет освобождена для дальнейшего использования. Этого можно добиться, реализовав деструктор. Как и конструктор, он имеет то же имя, что и класс, но перед ним ставится символ «~». Пример:

 class Table {

 Name* p;

 int sz;

public:

 Table(int s = 15) {p = new Name[sz = s];}

 ~Table() { delete[] p; }

 Name* lookup(const char*);

 bool insert(Name*); };

Здесь при удалении объекта этого класса будет запущен деструктор, который освободит память, отведенную под поле p.

В Яве деструкторов нет. Однако для освобождения ресурсов (например, файлов) при автоматическом уничтожении объекта в классе может быть описан метод finalize(), который будет запускаться каждый раз перед уничтожением объекта данного класса. Другим вариантом как в Яве, так и в C# является использование ветви finally в блоке с контролем (см. главу 12).

6.3.4. Конструкторы копирования и присваивания

В С++ если t1 и t2 являются объектами класса Table, выражение t2 = t1 по умолчанию означает поэлементное копирование t1 в t2. При наличии у объекта полей, являющихся указателями, такое копирование может вызвать неожиданный (и часто нежелательный) эффект. Пример:

 void h() {

Table t1;

Table t2 = t1; // копирующая инициализация – проблема

Table t3;

t3 = t2;   // копирующее присваивание – проблема

}

В этом примере конструктор Table будет по умолчанию вызван дважды – по одному разу для t1 и t3. Он не будет вызван для t2, потому что эта переменная проинициализирована посредством присваивания. Однако при выходе из функции деструктор ~Table будет вызван три раза, по одному разу для t1, t2 и t3! По умолчанию копирование интерпретируется как поэлементное, поэтому t1, t2 и t3 к концу работы функции будут содержать указатели на массив имен, выделенный в свободной памяти при инициализации t1. Не осталось указателя на массив, заведенный при инициализации t3, потому что он будет перезаписан при выполнении присваивания t3 = t2. В результате эта память будет потеряна. С другой стороны, массив, созданный для t1, будет удаляться трижды! Результат непредсказуем и, вероятно, приведет к катастрофе.

Чтобы избежать подобной ситуации, необходимо объявить в классе конструктор копирования и конструктор присваивания. Пример:

 class Table {

  . . .

 Table(const Table&); // конструктор копирования

 Table& operator= (const Table&) // конструктор присваивания

};

Программист может определить любой подходящий смысл для этих конструкторов, например:

 Table::Table(const Table& t) {

 p = new Name[sz = t.sz];

 for (int i = 0; i < sz; i++) p[i] = t.p[i];

}

Table& Table::operator= (const Table& t) {

 if (this != &t) {  //

  delete[] p;

  p = new Name[sz = t.sz];

  for (int i = 0; i < sz; i++) p[i] = t.p[i];

 }

 return *this;

}

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

В Яве при исполнении оператора присваивания ov1 = ov2, где ov1 и ov2 – переменные одного и того же класса, происходит просто поэлементное копирование значений полей ov2 в соответствующие поля ov1. Для правильного копирования, подобного осуществляемому конструктором присваивания, приведенным выше, в классе следует реализовать метод copy(). Тогда при исполнении ov1.copy(ov2) копирование произойдет надлежащим образом. Если метод copy() в классе не реализован, то при исполнении ov1.copy(ov2) вызовется некий стандартный copy(), который произведет поэлементное копирование.

Для осуществления действий, подобных конструктору копирования, необходимо реализовать метод (или воспользоваться стандартным вариантом) clone(). При работе этого метода создается новый объект, поля которого инициализируются как предписано методом (поэлементно в стандартном случае). Если в классе реализован clone(), тогда он вызывается следующим образом: S ov1 = ov2.clone(), где S – имя класса. В противном случае следует осуществить приведение типов: ov1 = (S) ov2.clone(). Подробнее работа метода clone()будет рассмотрена позже.

6.3.5. Преобразования посредством конструкторов и специальных функций

В С++ конструктор с одним параметром, если он не объявлен как explicit, задает преобразование типа своего параметра к типу своего класса. Пример:

 class X {

 // . . .

public

 X(int);

 X(const char*, int = 0);

};

void f(X arg) {…};

 X a = 1;  // a =X(1)

X b = "Jessie";  // b = X("Jessie", 0)

a = 2;   // a = X(2)

f(3);   // f(X(3))

Использование конструктора для задания преобразования типа удобно, но имеет нежелательные следствия:

  1.  неявное преобразование из пользовательского типа в базовый тип невозможно;
  2.  нельзя задать преобразование из нового типа в существующий;
  3.  в ранних версиях С++ было невозможно, определив конструктор с одним параметром, не получить при этом преобразования.

Последняя проблема решена в текущей версии языка путем использования спецификатора explicit в объявлении конструктора, что исключает применение конструктора для неявного преобразования типа.

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

 operator имя-типа() тело-функции

определяет преобразование из Х в тип, заданный именем-типа. Пример:

 class X {

 // . . .

public:

 operator int();

};

void f(X a)

{

 int i = int(a);

 i = (int)a;

 i = a;

};

Во всех трех случаях присваиваемое значение будет преобразовано посредством функции X::operator int().

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

6.3.6. Порядок инициализации полей и локальных объектов

Когда объект содержит поля, типы которых – классы (т.е. локальные объекты), при конструировании объекта сначала вызываются конструкторы этих полей. Соответственно при уничтожении объекта вызываются деструкторы этих полей, а затем уж – деструктор самого объекта. Деструкторы локальных объектов всегда вызываются в порядке, противоположном их конструированию. То же самое происходит и с локальными объектами функции. Пример:

void f(int i) {

 Table aa;

 Table bb;

 if (i > 0) { Table cc; . . . }

 Table dd;

  . . .

}

В этом примере при каждом вызове f() переменные aa, bb и dd создаются именно в таком порядке и уничтожаются при выходе из функции в порядке dd, bb, aa. Если во время работы функции выполнится условие, то переменная cc будет создана после bb и уничтожена до создания dd.

  1.  Статические компоненты классов

6.4.1. Статические переменные и константы

Переменная (константа), которая является частью класса, но не является частью объекта, называется статическим компонентом класса. Такой компонент вводится посредством спецификатора static, который означает, что для всего класса заводится только один экземпляр данного компонента. Таким образом, этот компонент становится как бы глобальным компонентом программы, но видимым только в пределах данного класса, что исключает его непредвиденное использование другими функциями и классами.

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

// Initializing static arrays in classes

class Values { // static integral consts are initialized in-place: 

static const int scSize = 100;

static const long scLong = 100;

static const int scInts[];

static const long scLongs[];

static const float scTable[];

static const char scLetters[];

static int size;

static const float scFloat;

static float table[];

static char letters[];};

int Values::size = 100;

const float Values::scFloat = 1.1;

const int Values::scInts[] = { 99, 47, 33, 11, 7};

const long Values::scLongs[] = { 99, 47, 33, 11, 7};

const float Values::scTable[] = { 1.1, 2.2, 3.3, 4.4};

const char Values::scLetters[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g'};

float Values::table[4] = { 1.1, 2.2, 3.3, 4.4};

char Values::letters[8] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'};

Статические объекты пользовательских типов (классов) также нициализируются на уровне файла:

class X {

 int i;

public:

 X(int ii) : i(ii) {}

};

class Stat {

  // Следующая инициализация незаконна! 

//static const X x(100);

 // как костантные, так и обычные статические объекты

// инициализируются вне класса

static X x2;

static X xTable2[];

static const X x3;

static const X xTable3[];

};

X Stat::x2(100);

X Stat::xTable2[] = {  X(1), X(2), X(3), X(4) };

const X Stat::x3(100);

const X Stat::xTable3[] = {  X(1), X(2), X(3), X(4) };

В следующем примере на Яве метод unset может быть уверен, что статическая переменная UNSET инициализирована.

 class Value {

 public static double UNSET = Double.NaN;

 private double v;

 public void unset() { v = UNSET; }

  . . .

}

За пределами класса обращение к открытому статическому компоненту производится путем уточнения имени этого компонента именем класса. При этом в Яве и C# используется символ «.», а в С++ символ «::», например:

 Value.UNSET // Ява

Value::UNSET // С++

Можно также обращаться к этим компонентам через объект, используя стандартную нотацию. Заметим, однако, что оба варианта обращения дают один и тот же эффект.

6.4.2. Статические методы

Статический метод вызывается для целого класса, а не для отдельного его объекта. Подобные методы также называют методами класса. Статический метод выполняет работу, общую для всех объектов данного класса. Такому методу не передается ссылка this, поэтому он не может работать на прямую с полями объекта, однако, объект может быть передан ему в качестве параметра. Напрямую статический метод может работать со статическими полями класса и методами класса. В качестве примера рассмотрим реализацию на С++ конструктора по умолчанию в классе Date, который использует текущую дату.

 class Date {

 int day, month, year;

 static Date default_date;

public:

 Date (int d = 0, int m = 0, int y = 0);

   . . .

 static void set_default(int, int, int);

};

Мы можем описать конструктор Date следующим образом:

 Date::Date(int d, int m, int y) {

 day = d? d : default_date.day;

 month = m? m : default_date.month;

 year = y? y : default_date.year;

}

Дата default_date является скрытым статическим полем класса Date. Поэтому она может быть установлена только компонентной функцией этого класса:

 void Date::set_default(int d, int m, int y) {

 default_date = Date(d, m, y);

}

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

Date::set_default(1, 1, 2000);

Заметим, что статический метод в отличие от обычного может быть вызван, когда еще не создано ни одного объекта данного класса.

6.4.3. Блоки статической инициализации (Ява)

В Яве класс может содержать блоки статической инициализации, которые присваивают значения статическим полям или выполняют другую необходимую работу. Статический инициализатор наиболее полезен в тех случаях, когда простая инициализация поля невозможна. Например, создание статического массива часто должно дополняться его алгоритмической инициализацией. Пример:

 class Primes {

 private static int[] knownPrimes = new int[4];

 static {

  knownPrimes[0] = 2;

  for (int i = 1; i < knowPrimes.length; i++)

   knownPrimes[i] = nextPrime();

 }

};

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

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

6.4.4. Статические конструкторы (C#)

Аналогом блоков статической инициализации Явы в C# являются статические конструкторы. Такой конструктор описывается по правилам обычного конструктора, но с предшествующим спецификатором static. Естественно, что в нем нельзя употреблять нестатические компоненты классов. Статический конструктор всегда вызывается неявно перед первым использованием статического компонента класса или перед первым созданием объекта данного класса. По этой причине  в его описании не указываются метки доступа. Пример:

class Test
{
static void Main() {
 A.F();
 B.F();
}
}

class A
{
static A() {
 Console.WriteLine("Init A");
}
public static void F() {
 Console.WriteLine("A.F");
}
}

class B
{
static B() {
 Console.WriteLine("Init B");
}
public static void F() {
 Console.WriteLine("B.F");
}
}

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

 

Init A
A.F
Init B
B.F

потому что исполнение статического конструктора класса A инициируется вызовом A.F, а исполнение статического конструктора класса В инициируется вызовом В.F.  

  1.  Друзья класса (С++)

Обычное объявление компонентной функции гарантирует три логически различные вещи:

  1.  функция имеет доступ к скрытой части объявления класса;
  2.  функция находится в области видимости класса;
  3.  функция должна вызываться для объекта (имеется указатель this).

Объявив функцию как static, мы придаем ей только первые два свойства. В С++, объявив функцию как friend, мы наделяем ее только первым свойством. Таким образом, друг класса – это функция, которая не является компонентом класса, но которой разрешается использовать его защищенные и скрытые компоненты. Друга класса нельзя вызвать посредством операции доступа к компоненту класса, кроме случая, когда друг является компонентом другого класса. Пример:

 class X {

 int a;

 friend void friend_set(X*, int);

 public void member_set(int);

};

void friend_set(X* p, int i) {p->a = i;};

void X::member_set(int i) {a = i};

void f {

 X obj;

 friend_set(&obj, 10);

 obj.member_set(10);

};

Механизм друзей важен потому, что функция может быть другом двух классов и потому – гораздо эффективнее обычной функции, оперирующей объектами обоих классов. Например, мы могли бы захотеть определить функцию, которая умножает матрицу на вектор. Естественно, что классы matrix и vector скрывают свое представление и обеспечивают полный набор операций для манипулирования объектами своих типов. Однако наша функция не может быть членом обоих классов. Если ее описать как обычную функцию с использованием компонентных функций обоих классов, мы можем получить не очень эффективный код. Выход – в объявлении этой функции другом обоих классов:

 class matrix; // объявили класс матриц

class vector {

 float v[4];

 // . . .

 friend vector multiply(const matrix&, const vector&);

};

class matrix {

 vector v[4];

 // . . .

 friend vector multiply(const matrix&, const vector&);

};

Мы можем теперь написать функцию умножения, которая использует элементы векторов и матриц непосредственно:

vector multiply(const matrix& m, const vector& v) {

 vector r;

 for (int i = 0; i < 4; i++) {  // r[i] = m[i] * v

  r.v[i] = 0;

  for (int j = 0; j < 4; j++) r.v[i] += m.v[i][j] * v.v[j];

 };

 return r;

}

Объявление друзей можно поместить как в открытой, так и закрытой части класса – не имеет значения, где именно. Так же, как и компонентные функции, друзья явно указываются в объявлении класса, Поэтому они являются часть интерфейса класса в той же мере, что и компонентные функции. В то же время дружественная функция вызывается как обычная функция. Она не получает указателя this и конкретный объект может передаваться ей только через механизм параметров.

Компонентная функция одного класса может быть другом другого:

class X {

 // . . .

 void f();

};

class Y {

 // . . .

 friend void X::f();

};

Если сразу все компонентные функции класса Х должны быть объявлены друзьями класса Y, то можно применить более короткую форму записи:

class Y {

 // . . .

 friend class X;

};

Функция, первое объявление которой содержит спецификатор friend, считается также внешней. Из сказанного следует:

static void f() { /* . . . */};

class X { friend g(); }; // подразумевает и extern g()

class Y {

 friend void f(); // верно: f() имеет теперь внутренне связывание

};

static g() { /* . . . */}; // ошибка: несовместимое связывание

6.5.1. Поиск друзей

Так же как и объявление компонента класса, объявление друга не добавляет нового имени в охватывающую область видимости. Пример:

 class matrix {

 friend class Xform;

 friend matrix invert (const matrix&);

  . . .

};

Xform x;  // ошибка: в текущей области видимости нет имени Xform

matrix (*p)(const matrix&) = &invert; // аналогичная ошибка

Класс-друг должен быть предварительно объявлен в охватывающей области видимости или определен в пространстве имен, непосредственно охватывающим класс, объявивший его другом, например:

class X { . . . }; // друг класса Y

namespace N {

 class Y {

  friend class X;

  friend class Z;

  friend class AE;

 };

 class Z { . . . };   // друг класса Y

}

class AE { . . . }; // не является другом класса Y

Поиск дружественной функции осуществляется так же, как и обычной функции.

  1.  Вложенные и локальные классы.

6.6.1. Вложенные классы

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

int x, y;

class enclose {

public:

 int x;

 static int s;

 class inner {

  void f(int i) {

   x = i;  // ошибка: присваивание enclose::x

   s = 1; // верно: присваивание enclose::s

   ::x = 1; // верно: присваивание глобальной x

   y = i; // верно: присваивание глобальной y

  };

  void g(enclose* p, int i) {

   p->x = i; // верно: присваивание enclose::x

  };

 };

};

 inner* p = 0;  // ошибка: "inner' вне области действия

Компонентные функции как объемлющего класса, так и вложенного класса подчиняются обычным правилам доступа, т.е. они могут быть открытыми, защищенными или закрытыми. Пример:

class E {

 int x;

 class I {

  int y;

  void f(E* p, int i)

  {

   p->x = i;  // ошибка: E::x скрытый компонент класса

  };

 };

 int g(I* p)

 {

  return p->y;  // ошибка: I::y скрытый компонент класса

 };

};

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

class X {

 struct M1 {int m};

public:

 struct M2 {int m};

 M1 f(M2);

};

void f() {

 M1 a;  // ошибка: имя "М1" невидимо

 M2 b;  //  ошибка: имя "М2" невидимо

 X::M1 c; // ошибка: "Х::М1" скрытый компонент класса

 X::M2 d; // все в порядке

}

Компонентные функции и статические поля вложенного класса С++ могут быть описаны в глобальной области действия, например:

class enclose {

class inner {

 static int x;

 void f(int i);

};

};

typedef enclose::inner ei;

int ei::x = 1;

void enclose::inner::f(int i) { . . . }

В последних версиях Явы также разрешены вложенные классы. Так же как и вложенные классы С++, они подчиняются обычным правилам доступа к компонентам класса, т.е. могут быть открытыми, защищенными, скрытыми, пакетными и т.д.

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

 public class WhichChar {

 private BitSet used = new BitSet();  // BitSet – класс битовых

         // векторов

 

private class Enumerate implements Enumeration {

  private int pos = 0;

  private int setSize = used.size();

  public boolean hasMoreElements() {

   while (pos < setSize && !used.get(pos))

    pos++;

   return (pos < setSize);

  }

     . . .

 }

 public Enumeration characters() {return new Enumerate();}

}

Здесь вложенный класс Enumerate – скрытый, так что он служит исключительно целям реализации своего объемлющего класса. Заметим, что Enumerate осуществляет доступ к полю used своего объекта. При поиске объявления идентификатора used компилятор Явы проверит сначала, не объявлен ли он во вложенном классе, а затем перейдет к охватывающему классу. Свой объект передается методам вложенного класса через ссылку this, поставленную тому методу, который создал объект вложенного класса. В приведенном примере каждый раз, когда внутри конкретного объекта класса WhichChar создается объект класса Enumerate, ему передается ссылка на объемлющий объект. (В общем случае здесь применяется та же техника, что и при работе с вложенными друг в друга процедурами в блочных языках программирования.) Это означает, что невозможно создать объект вложенного нестатического класса в статическом контексте (в статическом методе, статическом блоке и т.п.). Доступ к объемлющему объекту из внутреннего объекта может быть осуществлен через имя класса. Например, код класса Enumerate мог бы получить доступ к объемлющему объекту через WhichChar.this.

В C# также возможны описания вложенных классов. Пример:

class A
{
class B
{
 static void F() {
  Console.WriteLine("A.B.F");
 }
}
}

Здесь класс B  вложенный, потому что он описан в классе A.

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

public class List
{
// скрытый класс
private class Node
{
 public object Data;
 public Node Next;
 public Node(object data, Node next) {
  this.Data = data;
  this.Next = next;
 }
}

private Node first = null;
private Node last = null;

// Public interface
public void AddToFront(object o) {…}
public void AddToBack(object o) {…}
public object RemoveFromFront() {…}
public object AddToFront() {…}
public int Count { get {…} }
}

6.6.2. Локальные объявления классов

В С++ и Яве класс, описанный в блоке функции, называется локальным. Объявления в локальном классе могут использовать из объемлющей области действия только имена типов, статические переменные, внешние переменные и элементы перечисления (разрешение использования автоматических переменных объемлющей функции предполагало бы допустимость вложенных функций). Пример:

int x;

void f() {

 static int s;

 int x;

 extern int g();

 struct local {

  int g() {return x;} // ошибка: "x" автоматическая переменная

  int h() {return s;}  // верно

  int k() {return ::x;} // верно

  int l() {return g();} // верно

 };

};

local* p = 0; // ошибка: "local" вне области действия

Компонентные функции локального класса должны быть описаны в нем. Локальный класс не может иметь статических компонентных данных, так как их невозможно описать на уровне файла.

В Яве локальные классы напоминают локальные классы С++. В C# локальных классов нет.

6.6.3. Локальные имена типов данных

Имена типов данных подчиняются тем же правилам области действия, что и другие имена. Тем самым, имена типов, определенных внутри объявления класса, нельзя употреблять без уточнения вне этого класса. Пример на С++:

class X {

public:

 typedef int I;

 class Y { ... };

 I a;

};

I b;  // ошибка 

Y c;  // ошибка

X::Y d;  // нормально

Имя класса, описанное имя типа или имя константы, использованное в имени типа, не может переопределяться в объявлении класса после его использования в нем. Пример:

typedef int c;

enum { i =1};

 

class X {

 char v[i];

 int f() {return sizeof(c)};

 char c; //ошибка: имя типа переопределяется после использования

 enum {i =2}; // ошибка: константа "i" переопред-ся после исп-ния

};

typedef char* T;

struct Y {

 T a;

 typedef long T; // ошибка: Т уже использован

 T b;

};

  1.  Характерные классы и методы в Яве и C#

6.7.1. Метод main

Программа на Яве и C# строится на основе классов (а не файлов, как в С++). Детали запуска программы могут различаться в разных системах, но всегда необходимо указать имя класса, который управляет работой программы. В этом классе обязательно должен быть метод с именем main в Яве и с именем Main в C#. В Яве он должен быть описан как public, static и void, и ему должен передаваться один аргумент типа String[] (массив строк). В C# он иметь одну из следующих расширенных сигнатур:

void Main() {…}
static void Main(string[] args) {…}
static int Main() {…}
static int Main(string[] args) {…}

При запуске программы система находит указанный класс и запускает этот метод. Пример метода main, выводящего значения своих параметров:

class Echo {

 public static void main(String[] args) {

  for (int i = 0; i , args.length; i++)

   System.out.print(args[i] + “ “);

  System.out.println();

 }

}

В массиве строк содержатся аргументы программы. Чаще всего они вводятся пользователем при запуске программы. Например, в Unix программа Echo может быть вызвана следующим образом:

java Echo in here

В этой команде java является именем Ява-системы, Echo – имя класса с методом main, а остальные параметры представляют собой аргументы для main. Результатом работы программы будет напечатанная строка

 

in here

Программа может содержать несколько классов с методом main (Main), тем не менее в ней используется только один указанный main. Наличие нескольких методов main (Main), может иметь положительный эффект: каждый класс может отлаживаться автономно.

Целое значение, возвращаемое методом Main в C#, обычно содержит код ошибки. Пример:

static int Main(string[] args)

{

if (args.Length == 0)

{

 Console.WriteLine(“please enter an argument.”);

 return 1;

}

. . .

return 0;

}

6.7.2. Строки

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

System.out.println(“Hellow, world”);

Компилятор Явы создает экземпляр класса String, присваивает ему значение этого литерала и передает его в качестве параметра методу println.

Одной из основных операций над строками является конкатенация строк, объявление которой в нотации С++ выглядит следующим образом:

String operator+(const String);

В Яве и С#, как и в С++, используется специальная (привычная людям) нотация вызова методов, обозначенных знаками операций. Поэтому если str1 и str2 – два  объекта класса  String,  то вместо канонического  str1.+(str)  пишут str1 + str, что произведет в данном случае новый строковый объект, в котором за символами строки str1 будут следовать символы строки str2. Если одим из операндов оказывается null, используется пустая строка. При использовании операции конкатенации в операторе присваивания может использоваться, как и во многих других случаях, специальный знак операции с присваиванием, в данном случае «+=».

В классе Явы String имеется два конструктора:

 public String() // конструирует новый объект со значением “”

public String(String s) // конструирует клон объекта s

Другие важные методы:

public int length() // возвращает количество символов в строке

public char charAt(int i) // возвращает символ в позиции i; i не

 // должно быть отрицательным или больше length()-1 (символы

 // строки нумеруются от 0 до length)

 public int indexOf(char ch) // возвращает первую позицию символа ch

      // или -1 

public boolean equals(String s)  // возвращает true, если

     // содержимое обоих объектов одно и то же

public int compareTo(String s)  // возвращает положительное число,

 // нуль или отрицательное число в зависимости от того будет ли

 // данная строка больше (лексикографическое сравнивание в

 // Unicode), равна или меньше s.

В классе имеется также большое количество других полезных методов. Похожие методы определены и в классе string в C#, Ниже приведены объявления некоторых его методов:

public class String: ICloneable, IComparable, Ienumerable {

public String(char[] value, int startIndex, int length);

public String(char[] value);

public String(char c, int count);

public static readonly string Empty;

public object Clone();

public static int Compare(string strA, string strB);

public static string Concat(object arg0, object arg1);

public static string Copy(string str);

public static bool Equals(string a, string b);

public int IndexOf(char value);

public string Insert(int startIndex, string value);

public static bool operator ==(String a, String b);

public static bool operator !=(String a, String b);

public string Remove(int startIndex, int count);

public string Replace(char oldChar, char newChar);

public string Replace(string oldValue, string newValue);

public char this[int index] { get; }

public int Length { get; }

}

Для работы с изменяемыми строками в Яве имеется класс StringBuffer.

6.7.3. Метод toString

Если класс содержит метод  String toString(), то этот метод автоматически вызывается каждый раз, когда объект этого типа встречается вместо строки в операции «+» или «+=». Рассмотрим снова класс Body, предназначенный для хранения сведений о небесных телах:

class Body {

 public long idNum;

 public String name = “<unnamed>”;

 public Body orbits = null;

 private static long nextId = 0;

 Body() { idNum = nextID++; };

 Body(String bodyName, Body orbitsAround) { . . . };

 public String toString() {

  String desc = idNum + “ (“ + name + “) ”;

  if (orbits != null) desc += “ orbits ” + orbits;

  return desc;

 }

}

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

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

 Body sun = new Body(); // значение idNum равно 0

sun.name = “Sol”;

Body earth = new Body(); // значение idNum равно 1

earth.name = “Earth”;

earth.orbits = sun;

Тогда в следующих операторах:

 System.out.println(“Body “ + sun);

System.out.println(“Body “ + earth);

происходит косвенный вызов метода toString для объектов sun и earth, приводящий к печати следующих результатов:

 Body 0 (Sol)

Body 1 (Earth) orbits 0 (Sol)

Заметим, что значения всех примитивных типов данных автоматически преобразуются в объекты класса String при их использовании в подобных выражениях.

В C# также используется метод ToString, если один из операндов операции конкатенации не строка или выводится один нестроковый аргумент. Пример:

class Test
{
 static void Main() {
 string s = null;
 Console.WriteLine("s = >" + s + "<"); // выводит s = ><
 int i = 1;
 Console.WriteLine("i = " + i); // выводит i = 1
 float f = 1.2300E+15F;
 Console.WriteLine("f = " + f);  // выводит f = 1.23E+15
 decimal d = 2.900m;
 Console.WriteLine("d = " + d); // выводит d = 2.900
}
}

6.7.4. Делегаты в C#

Делегаты используются в этом языке в тех случаях, когда в С++ используются указатели на функцию, а в Паскале – типы функций, но при этом делегат может инкапсулировать и некоторый объект, и некоторый его метод. Описание делегата определяет класс, производный от класса System.Delegate. Делегат (объект соответствующего типа) содержит один или несколько методов, называемых вызываемыми сущностями. Для нестатического метода вызываемая сущность состоит из некоторого объекта и этого метода, для статического метода она состоит только из этого метода. Для данного делегата и подходящего набора аргументов можно вызвать все его методы с данным набором аргументов.

Определение делегата имеет следующий вид:

модификаторы-делегатаopt delegate тип-результата   идентификатор   

       (   формальные-параметрыopt   )   ;

где идентификатор есть тип делегата, а модификаторы-делегата – это одна из меток доступа или служебное слово new. Важным свойством делегата является то, что с ним могут быть связаны методы любого класса, важно лишь, чтобы они были совместимы с его типом, т.е.:

а) тип делегата и метод имеют одно и то же количество формальных параметров

с одними и теми же типами в том же самом порядке и тех же самых видов;

в) типы результатов одни и те же.

Пример:

delegate int D1(int i, double d);

class A
{
public static int M1(int a, double b) {…}
}

class B
{
delegate int D2(int c, double d);
public static int M1(int f, double g) {…}
public static void M2(int k, double l) {…}
public static int M3(int g) {…}
public static void M4(int g) {…}
}

Типы делегатов D1 и D2 совместимы с методами A.M1 и B.M1, так как у них один и тот же тип результата и одни и те же типы формальных параметров. Типы делегатов D1 и D2 несовместимы с методами  B.M2, B.M3 и B.M4, так как у них либо разные типы результата, либо разные типы параметров.

Создаются делегаты как же, как и другие объекты, посредством операции new, но при этом с делегатом связывается некоторый конкретный метод. С  созданным делегатом работают как с обычным методом. Пример:

delegate void SimpleDelegate();

class Test
{
static void F() {
 System.Console.WriteLine("Test.F");
}

static void Main() {
 SimpleDelegate d = new SimpleDelegate(F);
 d();
}
}

Здесь описан тип делегата без параметров SimpleDelegate, создан делегат этого типа d, с которым связан метод F, и затем вызван этот метод посредством вызова делегата d(). Конечно, нет большого смысла в связывании метода с делегатом и немедленном вызове его, проще было бы вызвать метод обычным способом. Делегаты полезны, когда они вызываются анонимно, обеспечивая аналог подстановки функций (В С++ в этом случае использовался бы указатель на функцию). Пример:

void MultiCall(SimpleDelegate d, int count) {
for (int i = 0; i < count; i++)
 d();
}
}

Здесь при разных вызовах функции MultiCall могут подставляться различные делегаты в качестве d и тем самым вызов d() будет запускать разные методы.

Однотипные делегаты могут объединяться посредством операций «+» и «+=», делегат может быть сокращен посредством операций  «-» и «-=» и делегаты могут сравниваться на равенство. Пример:

delegate void D(int x);
class Test
{
public static void M1(int i) {…}
public static void M2(int i) {…}

public void M3(int i){…}

}

class Demo
{
static void Main() {
 D cd1 = new D(Test.M1); // M1
 D cd2 = new D(Test.M2); // m2
 D cd3 = cd1 + cd2;  // M1 + M2
 D cd4 = cd3 + cd1;   // M1 + M2 + M1
 D cd5 = cd4 + cd3;   // M1 + M2 + M1 + M1 + M2

     Test t = new Test();
 D cd6 = new D(t.M3);  // нестатический метод

}
}

Если делегат содержит несколько вызываемых сущностей (в результате применения операции «+»), то при вызове делегата последовательно исполняется каждая из них с одним и тем же набором аргументов. Если методы в этих сущностях вырабатывают результат, то учитывается результат, выработанный последним методом.

6.8. Дополнительные виды компонентов в C#

6.8.1. Свойства

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

В общем виде описание свойства имеет следующий синтаксис:

описание-свойства:

модификаторы-свойстваopt тип имя-свойства  {описания-доступов}

Описания-доступов – это либо описание читателя:

 get блок

либо описание писателя: