C#) — Реализация классов в c#


Содержание

Классы

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

Классы и структуры — это, по сути, шаблоны, по которым можно создавать объекты. Каждый объект содержит данные и методы, манипулирующие этими данными.

Общая форма определения класса

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

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

Данные-члены

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

Поля (field)

Это любые переменные, ассоциированные с классом.

Константы

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

События

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

Функции-члены

Функции-члены — это члены, которые обеспечивают некоторую функциональность для манипулирования данными класса. Они включают методы, свойства, конструкторы, финализаторы, операции и индексаторы:

Методы (method)

Это функции, ассоциированные с определенным классом. Как и данные-члены, по умолчанию они являются членами экземпляра. Они могут быть объявлены статическими с помощью модификатора static.

Свойства (property)

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

Конструкторы (constructor)

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

Финализаторы (finalizer)

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

Операции (operator)

Это простейшие действия вроде + или -. Когда вы складываете два целых числа, то, строго говоря, применяете операцию + к целым. Однако C# позволяет указать, как существующие операции будут работать с пользовательскими классами (так называемая перегрузка операции).

Индексаторы (indexer)

Позволяют индексировать объекты таким же способом, как массив или коллекцию.

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

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

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

В данном примере определяется новый пользовательский класс UserInfo, который содержит 4 поля и 1 метод, которые являются открытыми (т.е. содержат модификатор доступа public). В методе Main() создаются два экземпляра этого класса: myInfo и myGirlFriendInfo. Затем инициализируются поля данных экземпляров и вызывается метод writeInConsoleInfo().

Прежде чем двигаться дальше, рассмотрим следующий основополагающий принцип: у каждого объекта имеются свои копии переменных экземпляра, определенных в его классе. Следовательно, содержимое переменных в одном объекте может отличаться от их содержимого в другом объекте. Между обоими объектами не существует никакой связи, за исключением того факта, что они являются объектами одного и того же типа. Так, если имеются два объекта типа UserInfo, то у каждого из них своя копия переменных Name, Family, Age и Adress, а их содержимое в обоих объектах может отличаться:

Введение в ООП с примерами на C#. Часть четвёртая. Абстрактные классы

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

Что такое абстрактные классы

В плане терминологии давайте доверимся MSDN:

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

Абстрактные классы в действии

Итак, попробуем создать абстрактный класс:

Попытаемся скомпилировать этот код:

Compile time error: Cannot create an instance of the abstract class or interface ‘InheritanceAndPolymorphism.ClassA’

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

Описание методов в абстрактном классе

Попробуем добавить в наш абстрактный класс немного кода:

С кодом класса никаких проблем нет, но скомпилировать снова не получается, потому что нельзя создавать экземпляры абстрактных классов.

Использование абстрактного класса в качестве базового

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

Вау. Теперь всё спокойно компилируется.

Что нужно запомнить: Мы можем унаследовать обычный класс от абстрактного.

Что нужно запомнить: Мы можем создать экземпляр обычного класса, унаследованного от абстрактного, с помощью ключевого слова new .

Декларация методов в абстрактном классе

Теперь попробуем сделать вот так:

И… мы получаем ошибку компиляции:

Compile time error: ‘InheritanceAndPolymorphism.ClassA.YYY()’
must declare a body because it is not marked abstract, extern, or partial

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

…и снова получим ошибку компиляции:

Compiler error: ‘InheritanceAndPolymorphism.ClassB’ does not implement
inherited abstract member ‘InheritanceAndPolymorphism.ClassA.YYY()’

Что нужно запомнить: Если мы хотим объявить метод в абстрактном классе, но не реализовывать его, к методу нужно добавить ключевое слово abstract .

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

Реализация абстрактного метода в производном классе

Так, давайте тогда попробуем реализовать метод YYY() в классе ClassB :

На первый взгляд всё отлично, правда? Но на этот раз мы получим сразу две ошибки компиляции:

Compile time error: ‘InheritanceAndPolymorphism.ClassB’ does not implement
inherited abstract member ‘InheritanceAndPolymorphism.ClassA.YYY()’

Compile time warning: ‘InheritanceAndPolymorphism.ClassB.YYY()’ hides
inherited member ‘InheritanceAndPolymorphism.ClassA.YYY()’.

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

Ура! ^_^ У нас наконец-то нет никаких ошибок!

Абстрактный метод базового класса и метод с override класса-наследника должны быть одинаковы

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

То в консоли увидим следующую ошибку:

Compile time error: ‘InheritanceAndPolymorphism.ClassB.YYY()’: return type must be ‘void’
to match overridden member ‘InheritanceAndPolymorphism.ClassA.YYY()’

Инициализация переменных в абстрактных классах

В примерах выше, переменная int a будет обладать значением по умолчанию (0). Мы можем изменить его на нужное нам прямо в абстрактном классе — с этим не связано никаких особенностей.

Абстрактные методы в неабстрактных классах

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

Compiler error: ‘InheritanceAndPolymorphism.ClassA.YYY()’ is abstract
but it is contained in non-abstract class ‘InheritanceAndPolymorphism.ClassA’

Что нужно запомнить: Абстрактные методы могут быть объявлены только в абстрактных классах.

Вызов абстрактного метода родителя

Compile time error : Cannot call an abstract base member:
‘InheritanceAndPolymorphism.ClassA.YYY()’

Разумеется, мы не можем исполнить код, которого не существует.

Абстрактный класс, который наследуется от другого абстрактного класса

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

Может ли абстрактный класс быть sealed

И получим ошибку:

Compile time error: ‘InheritanceAndPolymorphism.ClassA’:
an abstract class cannot be sealed or static

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

Что нужно запомнить: Абстрактный класс не может иметь модификатор sealed .

Что нужно запомнить: Абстрактный класс не может иметь модификатор static .

Что мы узнали сегодня:

Что нужно запомнить

  • Мы не можем создать экземпляр абстрактного класса с помощью ключевого слова new ;
  • Мы можем унаследовать обычный класс от абстрактного;
  • Мы можем создать экземпляр обычного класса, унаследованного от абстрактного, с помощью ключевого слова new ;
  • Если мы хотим объявить метод в абстрактном классе, но не реализовывать его, к методу нужно добавить ключевое слово abstract ;
  • Если мы объявляем абстрактный метод в абстрактном классе, то этот метод должен реализовываться в неабстрактных наследниках этого класса;
  • Абстрактные методы могут быть объявлены только в абстрактных классах;
  • Абстрактный класс не может иметь модификатор sealed ;
  • Абстрактный класс не может иметь модификатор static .

Часть IV

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

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

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

В главе 15 мы расскажем о перегрузке операторов, которая позволяет использовать операнды типа класса со встроенными операторами, описанными в главе 4. Таким образом, работа с объектами типа класса может быть сделана столь же понятной, как и работа со встроенными типами. В начале главы 15 представлены общие концепции и соображения, касающиеся проектирования перегрузки операторов, а затем рассмотрены конкретные операторы, такие, как присваивание, взятие индекса, вызов, а также специфичные для классов операторы new и delete. Иногда необходимо объявить перегруженный оператор, как друга класса, наделив его специальными правами доступа, в данной главе объясняется, зачем это нужно. Здесь же представлен еще один специальный вид функций-членов – конвертеры, которые позволяют программисту определить стандартные преобразования. Конвертеры неявно применяются компилятором, когда объекты класса используются в качестве фактических аргументов функции или операндов встроенного либо перегруженного оператора. Завершается глава изложением правил разрешения перегрузки функций с учетом аргументов типа класса, функций-членов и перегруженных операторов.

Тема главы 16 – шаблоны классов. Шаблон – это предписание для создания класса, в котором один или несколько типов параметризованы. Например, vector может быть параметризован типом элементов, хранящихся в нем, а buffer – типом элементов в буфере или его размером. В этой главе объясняется, как определить и конкретизировать шаблон. Поддержка классов в C++ теперь рассматривается иначе – в свете наличия шаблонов, и снова обсуждаются функции-члены, объявления друзей и вложенные типы. Здесь мы еще раз вернемся к модели компиляции шаблонов, описанной в главе 10, чтобы показать, какое влияние оказывают на нее шаблоны классов.

13. Классы

Механизм классов в C++ позволяет пользователям определять собственные типы данных. По этой причине их часто называют пользовательскими типами. Класс может наделять дополнительной функциональностью уже существующий тип. Так, например, IntArray, введенный в главе 2, предоставляет больше возможностей, чем тип «массив int». С помощью классов можно создавать абсолютно новые типы, например Screen (экран) или Account (расчетный счет). Как правило, классы используются для абстракций, не отражаемых встроенными типами адекватно.

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

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

13.1. Определение класса

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

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

Каждое определение вводит новый тип данных. Даже если два класса имеют одинаковые списки членов, они все равно считаются разными типами:

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

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

  • написать ключевое слово class, а после него – имя класса. В предыдущем примере объект obj1 класса First объявлен именно таким образом;
  • указать только имя класса. Так объявлен объект obj2 класса Second из приведенного примера.

Оба способа сослаться на тип класса эквивалентны. Первый заимствован из языка C и остается корректным методом задания типа класса; второй способ введен в C++ для упрощения объявлений.

13.1.1. Данные-члены

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

Поскольку мы решили использовать строки для внутреннего представления объекта класса Screen, то член _screen имеет тип string. Член _cursor – это смещение в строке, он применяется для указания текущей позиции на экране. Для него использован переносимый тип string::size_type. (Тип size_type рассматривался в разделе 6.8.)

Необязательно объявлять два члена типа short по отдельности. Вот объявление класса Screen, эквивалентное приведенному выше:

Член класса может иметь любой тип:

Описанные данные-члены называются нестатическими. Класс может иметь также и статические данные-члены. (У них есть особые свойства, которые мы рассмотрим в разделе 13.5.)

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

Данные-члены класса инициализируются с помощью конструктора класса. (Мы рассказывали о конструкторах в разделе 2.3; более подробно они рассматриваются в главе 14.)

13.1.2. Функции-члены

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

Функции-члены класса объявляются в его теле. Это объявление выглядит точно так же, как объявление функции в области видимости пространства имен. (Напомним, что глобальная область видимости – это тоже область видимости пространства имен. Глобальные функции рассматривались в разделе 8.2, а пространства имен – в разделе 8.5.) Например:

Определение функции-члена также можно поместить внутрь тела класса:

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

Функции-члены отличаются от обычных функций следующим:

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

(в разделе 13.9 область видимости класса обсуждается более детально);

  • функции-члены имеют право доступа как к открытым, так и к закрытым членам класса, тогда как обычным функциям доступны лишь открытые. Конечно, функции-члены одного класса, как правило, не имеют доступа к данным-членам другого класса.
  • Функция-член может быть перегруженной (перегруженные функции рассматриваются в главе 9). Однако она способна перегружать лишь другую функцию-член своего класса. По отношению к функциям, объявленным в других классах или пространствах имен, функция-член находится в отдельной области видимости и, следовательно, не может перегружать их. Например, объявление get(int, int) перегружает лишь get() из того же класса Screen:

    (Подробнее мы остановимся на функциях-членах класса в разделе 13.3.)

    13.1.3. Доступ к членам

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

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

    Если бы данные-члены класса Screen были открыты и доступны любой функции внутри программы, как отразилось бы на пользователях изменение внутреннего представления этого класса?

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

    Сокрытие информации – это формальный механизм, предотвращающий прямой доступ к внутреннему представлению типа класса из функций программы. Ограничение доступа к членам задается с помощью секций тела класса, помеченных ключевыми словами public, private и protected – спецификаторами доступа. Члены, объявленные в секции public, называются открытыми, а объявленные в секциях private и protected соответственно закрытыми или защищенными.

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

    В следующем определении класса Screen указаны секции public и private:

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

    13.1.4. Друзья

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

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

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

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

    13.1.5. Объявление и определение класса

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

    Можно объявить класс, не определяя его. Например:

    Это объявление вводит в программу имя Screen и указывает, что оно относится к типу класса.

    Тип объявленного, но еще не определенного класса допустимо использовать весьма ограниченно. Нельзя определять объект типа класса, если сам класс еще не определен, поскольку размер класса в этом момент неизвестен и компилятор не знает, сколько памяти отвести под объект.

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

    Член некоторого класса можно объявить принадлежащим к типу какого-либо класса только тогда, когда компилятор уже видел определение этого класса. До этого объявляются лишь члены, являющиеся указателями или ссылками на такой тип. Ниже приведено определение StackScreen, один из членов которого служит указателем на Screen, который объявлен, но еще не определен:

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

    Пусть дан класс Person со следующими двумя членами:

    и такие функции-члены:

    Какие члены вы объявили бы в секции public, а какие – в секции private? Поясните свой выбор.

    Объясните разницу между объявлением и определением класса. Когда вы стали бы использовать объявление класса? А определение?

    13.2. Объекты классов

    Определение класса, например Screen, не приводит к выделению памяти. Память выделяется только тогда, когда определяется объект типа класса. Так, если имеется следующая реализация Screen:

    выделяет область памяти, достаточную для хранения четырех членов Screen. Имя myScreen относится к этой области. У каждого объекта класса есть собственная копия данных-членов. Изменение членов myScreen не отражается на значениях членов любого другого объекта типа Screen.

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

    Тип Screen объявлен в глобальной области видимости, тогда как объект mainScreen – в локальной области функции main().

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

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

    Указатели и ссылки на объекты класса также можно объявлять. Указатель на тип класса разрешается инициализировать адресом объекта того же класса или присвоить ему такой адрес. Аналогично ссылка инициализируется l-значением объекта того же класса. (В объектно-ориентированном программировании указатель или ссылка на объект базового класса могут относиться и к объекту производного от него класса.)

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

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

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

    Для получения высоты и ширины экрана isEqual() должна пользоваться функциями-членами height() и width() для чтения закрытых членов класса. Их реализация тривиальна:

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

    можно переписать так:

    Результат будет одним и тем же.

    13.3. Функции-члены класса

    Функции-члены реализуют набор операций, применимых к объектам класса. Например, для Screen такой набор состоит из следующих объявленных в нем функций-членов:

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

    При вызове функции home() для объекта myScreen происходит обращение к его члену _cursor. Когда же эта функция вызывается для объекта groupScreen, то она обращается к члену _cursor именно этого объекта, причем сама функция home() одна и та же. Как же может одна функция-член обращаться к данным-членам разных объектов? Для этого применяется указатель this, рассматриваемый в следующем разделе.

    13.3.1. Когда использовать встроенные функции-члены

    Обратите внимание, что определения функций home(), get(), height() и width() приведены прямо в теле класса. Такие функции называются встроенными. (Мы говорили об этом в разделе 7.6.)

    Функции-члены можно объявить в теле класса встроенными и явно, поместив перед типом возвращаемого значения ключевое слово inline:

    Определения home() и get() в приведенных примерах эквивалентны. Поскольку ключевое слово inline избыточно, мы в этой книге не пишем его явно для функций-членов, определенных в теле класса.

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

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

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

    Функция get(int, int) объявляется встроенной с помощью слова inline:

    Определение функции следует после объявления класса. При этом слово inline можно опустить:

    Так как встроенные функции-члены должны быть определены в каждом исходном файле, где они вызываются, то встроенную функцию, не определенную в теле класса, следует поместить в тот же заголовочный файл, в котором определен ее класс. Например, представленные ранее определения move() и get() должны находиться в заголовочном файле Screen.h после определения класса Screen.

    13.3.2. Доступ к членам класса

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

    • в определении функции-члена могут быть обращения к любым членам класса, открытым или закрытым, и это не нарушает ограничений доступа;
    • когда функция-член обращается к членам класса, операторы доступа «точка» и «стрелка» не необходимы.
    Цукерберг рекомендует:  Двойная рамка с использованием CSS

    Хотя _screen, _height, _width и _cursor являются закрытыми членами класса Screen, функция-член copy() работает с ними напрямую. Если при обращении к члену отсутствует оператор доступа, то считается, что речь идет о члене того класса, для которого функция-член вызвана. Если вызвать copy() следующим образом:

    то параметр sobj внутри определения copy() соотносится с объектом s1 из функции main(). Функция-член copy() вызвана для объекта s2, стоящего перед оператором “точка”. Для такого вызова члены _screen, _height, _width и _cursor, при обращении к которым внутри определения этой функции нет оператора доступа, – это члены объекта s2. В следующем разделе мы рассмотрим доступ к членам класса внутри определения функции-члена более подробно и, в частности, покажем, как для поддержки такого доступа применяется указатель this.

    13.3.3. Закрытые и открытые функции-члены

    Функцию-член можно объявить в любой из секций public, private или protected тела класса. Где именно это следует делать? Открытая функция-член задает операцию, которая может понадобиться пользователю. Множество открытых функций-членов составляет интерфейс класса. Например, функции-члены home(), move() и get() класса Screen определяют операции, с помощью которых программа манипулирует объектами этого типа.,/p>

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

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

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

    Далее следуют определения функций:

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

    Представленные до сих пор функции-члены были открытыми, их можно вызывать из любого места программы, а закрытые вызываются только из других функций-членов (или друзей) класса, но не из программы, обеспечивая поддержку другим операциям в реализации абстракции класса. Примером может служить функция-член remainingSpace класса Screen(), использованная в set(const string&).

    (Детально защищенные функции-члены будут рассмотрены в главе 17.)

    Следующая программа предназначена для тестирования описанных к настоящему моменту функций-членов:

    Откомпилировав и запустив эту программу, мы получим следующее:

    13.3.4. Специальные функции-члены

    Существует специальная категория функций-членов, отвечающих за такие действия с объектами, как инициализация, присваивание, управление памятью, преобразование типов и уничтожение. Такие функции называются конструкторами. Они вызываются компилятором неявно каждый раз, когда объект класса определяется или создается оператором new. В объявлении конструктора его имя совпадает с именем класса. Вот, например, объявление конструктора класса Screen, в котором заданы значения по умолчанию для параметров hi, wid и bkground:

    Определение конструктора класса Screen выглядит так:

    Каждый объявленный объект класса Screen автоматически инициализируется конструктором:

    (В главе 14 конструкторы, деструкторы и операторы присваивания рассматриваются более подробно. В главе 15 обсуждаются конвертеры и функции управления памятью.)

    13.3.5. Функции-члены со спецификаторами const и volatile

    Любая попытка модифицировать константный объект из программы обычно помечается компилятором как ошибка. Например:

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

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

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

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

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

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

    Модифицировать _text нельзя, но это объект типа char*, и символы, на которые он указывает, можно изменить внутри константной функции-члена класса Text. Функция-член bad() демонстрирует плохой стиль программирования. Константность функции-члена не гарантирует, что объекты внутри класса останутся неизменными после ее вызова, причем компилятор не поможет обнаружить такую ситуацию.

    Константную функцию-член можно перегружать неконстантной функцией с тем же списком параметров:

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

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

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

    13.3.6. Объявление mutable

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

    Если мы хотим прочитать символ, находящийся в позиции (3,4), то попробуем сделать так:

    // прочитать содержимое экрана в позиции (3,4)

    // Увы! Это не работает

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

    Обратите внимание, что move()изменяет член класса _cursor, следовательно, не может быть объявлена константной.

    Но почему нельзя модифицировать _cursor для константного объекта класса Screen? Ведь _cursor – это просто индекс. Изменяя его, мы не модифицируем содержимое экрана, а лишь пытаемся установить позицию внутри него. Модификация _cursor должна быть разрешена несмотря на то, что у класса Screen есть спецификатор const.

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

    Теперь любая константная функция способна модифицировать _cursor, и move() может быть объявлена константной. Хотя move() изменяет данный член, компилятор не считает это ошибкой.

    // move() — константная функция-член

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

    Отметим, что изменчивым объявлен только член _cursor, тогда как _screen, _height и _width не имеют спецификатора mutable, поскольку их значения в константном объекте класса Screen изменять нельзя.

    Объясните, как будет вести себя copy() при следующих вызовах:

    К дополнительным перемещениям курсора можно отнести его передвижение вперед и назад на один символ. Из правого нижнего угла экрана курсор должен попасть в левый верхний угол. Реализуйте функции forward() и backward().

    Еще одной полезной возможностью является перемещение курсора вниз и вверх на одну строку. По достижении верхней или нижней строки экрана курсор не перепрыгивает на противоположный край; вместо этого подается звуковой сигнал, и курсор остается на месте. Реализуйте функции up() и down(). Для подачи сигнала следует вывести на стандартный вывод cout символ с кодом ‘007’.

    Пересмотрите описанные функции-члены класса Screen и объявите те, которые сочтете нужными, константными. Объясните свое решение.

    13.4. Неявный указатель this

    У каждого объекта класса есть собственная копия данных-членов. Например:

    У объекта myScreen есть свои члены _width, _height, _cursor и _screen, а у объекта bufScreen – свои. Однако каждая функция-член класса существует в единственном экземпляре. Их и вызывают myScreen и bufScreen.

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

    Если функция move() вызывается для объекта myScreen, то члены _width и _height, к которым внутри нее имеются обращения, – это члены объекта myScreen. Если же она вызывается для объекта bufScreen, то и обращения производятся к членам данного объекта. Каким же образом _cursor, которым манипулирует move(), оказывается членом то myScreen, то bufScreen? Дело в указателе this.

    Каждой функции-члену передается указатель на объект, для которого она вызвана, – this. В неконстантной функции-члене это указатель на тип класса, в константной – константный указатель на тот же тип, а в функции со спецификатором volatile указатель с тем же спецификатором. Например, внутри функции-члена move() класса Screen указатель this имеет тип Screen*, а в неконстантной функции-члене List – тип List*.

    Поскольку this адресует объект, для которого вызвана функция-член, то при вызове move() для myScreen он указывает на объект myScreen, а при вызове для bufScreen – на объект bufScreen. Таким образом, член _cursor, с которым работает функция move(), в первом случае принадлежит объекту myScreen, а во втором – bufScreen.

    Понять все это можно, если представить себе, как компилятор реализует объект this. Для его поддержки необходимо две трансформации:

      Изменить определение функции-члена класса, добавив дополнительный параметр:

    В этом определении использование указателя this для доступа к членам _width и _cursor сделано явным.

  • Изменение каждого вызова функции-члена класса с целью передачи одного дополнительного аргумента – адреса объекта, для которого она вызвана:
  • Программист может явно обращаться к указателю this внутри функции. Так, вполне корректно, хотя и излишне, определить функцию-член home() следующим образом:

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

    13.4.1. Когда использовать указатель this

    Наша функция main() вызывает функции-члены класса Screen для объектов myScreen и bufScreen таким образом, что каждое действие – это отдельная инструкция. У нас есть возможность определить функции-члены так, чтобы конкатенировать их вызовы при обращении к одному и тому же объекту. Например, все вызовы внутри main() будут выглядеть так:

    Именно так интуитивно представляется последовательность операций с экраном: очистить экран myScreen, переместить курсор в позицию (2,2), записать в эту позицию символ ‘*’ и вывести результат.

    Операторы доступа «точка» и «стрелка» левоассоциативны, т.е. их последовательность выполняется слева направо. Например, сначала вызывается myScreen.clear(), затем myScreen.move() и т.д. Чтобы myScreen.move() можно было вызвать после myScreen.clear(), функция clear() должна возвращать объект myScreen, для которого она была вызвана. Мы уже видели, что доступ к объекту внутри функции-члена класса производится в помощью указателя this. Вот реализация clear():

    // объявление clear() находится в теле класса

    // в нем задан аргумент по умолчанию bkground = ‘#’

    Обратите внимание, что возвращаемый тип этой функции-члена – Screen& – ссылка на объект ее же класса. Чтобы конкатенировать вызовы, необходимо также пересмотреть реализацию move() и set(). Возвращаемый тип следует изменить с void на Screen&, а в определении возвращать *this.

    Аналогично функцию-член display() можно написать так:

    А вот реализация reSize():

    // объявление reSize() находится в теле класса

    // в нем задан аргумент по умолчанию bkground = ‘#’

    Работа указателя this не исчерпывается возвратом объекта, к которому была применена функция-член. При рассмотрении copy() в разделе 13.3 мы видели и другой способ его использования:

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

    Указатель this можно использовать для модификации адресуемого объекта, а также для его замены другим объектом того же типа. Например, функция-член assign() класса classType выглядит так. Можете ли вы объяснить, что она делает?

    classType – это имя деструктора. Оператор new выглядит несколько причудливо, но мы уже встречались с подобным в разделе 8.4.

    Как вы относитесь к такому стилю программирования? Безопасна ли эта операция? Почему?

    13.5. Статические члены класса

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

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

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

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

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

    Почему _interestRate сделан статическим, а _amount и _owner нет? Потому что у всех счетов разные владельцы и суммы, но процентная ставка одинакова. Следовательно, объявление члена _interestRate статическим уменьшает объем памяти, необходимый для хранения объекта Account.

    Хотя текущее значение _interestRate для всех счетов одинаково, но со временем оно может изменяться. Поэтому мы решили не объявлять этот член как const. Достаточно модифицировать его лишь один раз, и с этого момента все объекты Account будут видеть новое значение. Если бы у каждого объекта была собственная копия, то пришлось бы обновить их все, что неэффективно и является потенциальным источником ошибок.

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

    // явная инициализация статического члена класса

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

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

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

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

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

    Так как name – это массив (и не целого типа), его нельзя инициализировать в теле класса. Попытка поступить таким образом приведет к ошибке компиляции:

    Член name должен быть инициализирован вне определения класса.

    Обратите внимание, что член nameSize задает размер массива name в определении, находящемся вне тела класса:

    nameSize не квалифицирован именем класса Account. И хотя это закрытый член, определение name не приводит к ошибке. Как такое может быть? Определение статического члена аналогично определению функции-члена класса, которое может ссылаться на закрытые члены. Определение статического члена name находится в области видимости класса и может ссылаться на закрытые члены, после того как распознано квалифицированное имя Account::name. (Подробнее об области видимости класса мы поговорим в разделе 13.9.)

    Статический член класса доступен функции-члену того же класса и без использования соответствующих операторов:

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

    // мы используем ссылочный и указательный параметры,

    // чтобы проиллюстрировать оба оператора доступа

    Как ac1._interestRate, так и ac2->_interestRate относятся к статическому члену Account::_interestRate.

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

    // доступ к статическому члену с указанием квалифицированного имени

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

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

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

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

    13.5.1. Статические функции-члены

    Функции-члены raiseInterest() и interest() обращаются к глобальному статическому члену _interestRate:

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

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

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

    Такой функции-члену указатель this не передается, поэтому явное или неявное обращение к нему внутри ее тела вызывает ошибку компиляции. В частности, попытка обращения к нестатическому члену класса неявно требует наличия указателя this и, следовательно, запрещена. Например, представленную ранее функцию-член dailyReturn() нельзя объявить статической, поскольку она обращается к нестатическому члену _amount.

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

    Пусть дан класс Y с двумя статическими данными-членами и двумя статическими функциями-членами:

    Инициализируйте _xval значением 20, а _callsXval значением 0.

    Используя классы из упражнения 13.8, реализуйте обе статические функции-члена для класса Y. callsXval() должна подсчитывать, сколько раз вызывалась xval().

    Какие из следующих объявлений и определений статических членов ошибочны? Почему?

    13.6. Указатель на член класса

    Предположим, что в нашем классе Screen определены четыре новых функции-члена: forward(), back(), up() и down(), которые перемещают курсор соответственно вправо, влево, вверх и вниз. Сначала мы должны объявить их в теле класса:

    Функции-члены forward() и back() перемещают курсор на один символ. По достижении правого нижнего или левого верхнего угла экрана курсор переходит в противоположный угол.

    end() перемещает курсор в правый нижний угол экрана и является парной по отношению к функции-члену home():

    Функции up() и down() перемещают курсор вверх и вниз на одну строку. По достижении верхней или нижней строки курсор остается на месте и подается звуковой сигнал:

    row() – это закрытая функция-член, которая используется в функциях up() и down(), возвращая номер строки, где находится курсор:

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

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

    В более общей реализации параметр op заменяется параметром типа указателя на функцию-член класса Screen. Теперь repeat() не должна сама устанавливать, какую операцию следует выполнить, и всю инструкцию switch можно удалить. Определение и использование указателей на члены класса – тема последующих подразделов.

    13.6.1. Тип члена класса

    Указателю на функцию нельзя присвоить адрес функции-члена, даже если типы возвращаемых значений и списки параметров полностью совпадают. Например, переменная pfi – это указатель на функцию без параметров, которая возвращает значение типа int:

    Если имеются глобальные функции HeightIs() и WidthIs() вида:

    то допустимо присваивание pfi адреса любой из этих переменных:

    В классе Screen также определены две функции доступа, height() и width(), не имеющие параметров и возвращающие значение типа int:

    Однако попытка присвоить их переменной pfi является нарушением типизации и влечет ошибку компиляции:

    // неверное присваивание: нарушение типизации

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

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

    Синтаксис объявления указателя на функцию-член должен принимать во внимание тип класса. То же верно и в отношении указателей на данные-члены. Рассмотрим член _height класса Screen. Его полный тип таков: член класса Screen типа short. Следовательно, полный тип указателя на _height – это указатель на член класса Screen типа short:

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

    Переменную ps_Screen можно инициализировать адресом _height:

    или присвоить ей адрес _width:

    Переменной ps_Screen разрешается присваивать указатель на _width или _height, так как они являются членами класса Screen типа short.

    Несоответствие типов указателя на данные-члены и обычного указателя также связано с различием в их представлении. Обычный указатель содержит всю информацию, необходимую для обращения к объекту. Указатель на данные-члены следует сначала привязать к объекту или указателю на него, а лишь затем использовать для доступа к члену этого объекта. (В книге “Inside the C++ Object Model” ([LIPPMAN96a]) также описывается представление указателей на члены.)

    Указатель на функцию-член определяется путем задания типа возвращаемого функцией значения, списка ее параметров и класса. Например, следующий указатель, с помощью которого можно вызвать функции height() и width(), имеет тип указателя на функцию-член класса Screen без параметров, которая возвращает значение типа int:

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

    Использование typedef может облегчить чтение объявлений указателей на члены. Например, для типа “указатель на функцию-член класса Screen без параметров, которая возвращает ссылку на объект Screen”, т.е.

    Следующий typedef определяет Action как альтернативное имя:

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

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

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

    13.6.2. Работа с указателями на члены класса

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

    требуют скобок, поскольку приоритет оператора вызова () выше, чем приоритет взятия указателя на функцию-член. Без скобок

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

    Указатели на данные-члены используются аналогично:

    Приведем реализацию функции-члена repeat(), которую мы обсуждали в начале этого раздела. Теперь она будет принимать указатель на функцию-член:

    Параметр op – это указатель на функцию-член, которая должна вызываться times раз.

    Если бы нужно было задать значения аргументов по умолчанию, то объявление repeat() выглядело бы следующим образом:

    А ее вызовы так:

    Определим таблицу указателей. В следующем примере Menu – это таблица указателей на функции-члены класса Screen, которые реализуют перемещение курсора. CursorMovements – перечисление, элементами которого являются номера в таблице Menu.

    Можно определить перегруженную функцию-член move(), которая принимает параметр CursorMovements и использует таблицу Menu для вызова указанной функции-члена. Вот ее реализация:

    У оператора взятия индекса ([]) приоритет выше, чем у оператора указателя на функцию-член (->*). Первая инструкция в move() сначала по индексу выбирает из таблицы Menu нужную функцию-член, которая и вызывается с помощью указателя this и оператора указателя на функцию-член. move() можно применять в интерактивной программе, где пользователь выбирает вид перемещения курсора из отображаемого на экране меню.

    13.6.3. Указатели на статические члены класса

    Между указателями на статические и нестатические члены класса есть разница. Синтаксис указателя на член класса не используется для обращения к статическому члену. Статические члены – это глобальные объекты и функции, принадлежащие классу. Указатели на них – это обычные указатели. (Напомним, что статической функции-члену не передается указатель this.)

    Объявление указателя на статический член класса выглядит так же, как и для указателя на объект, не являющийся членом класса. Для разыменования указателя никакой объект не требуется. Рассмотрим класс Account:

    Тип &_interestRate – это double*:

    Определение указателя на &_interestRate имеет вид:

    Этот указатель разыменовывается так же, как и обычный, объект класса для этого не требуется:

    Однако, поскольку _interestRate и _amount – закрытые члены, необходимо иметь статическую функцию-член interest() и нестатическую amount().


    Указатель на interest() – это обычный указатель на функцию:

    Определение указателя и косвенный вызов interest() реализуются так же, как и для обычных указателей:

    К какому типу принадлежат члены _screen и _cursor класса Screen?

    Определите указатель на член и инициализируйте его значением Screen::_screen; присвойте ему значение Screen::_cursor.

    Определите typedef для каждой из функций-членов класса Screen.

    Указатели на члены можно также объявлять как данные-члены класса. Модифицируйте определение класса Screen так, чтобы оно содержало указатель на его функцию-член того же типа, что home() и end().

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

    Определите перегруженный вариант repeat(), который принимает параметр типа cursorMovements.

    13.7. Объединение – класс, экономящий память

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

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

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

    1. Ключевое слово int.
    2. Идентификатор i.
    3. Оператор =
    4. Константа 0 типа int.
    5. Точка с запятой.

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

    (Тип ИД Присваивание Константа Точка с запятой)

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

    Для Assign и Semicolon дополнительной информации не нужно, так как у них может быть только одно значение: соответственно := и ;

    Таким образом, в представлении лексемы могло бы быть два члена – token и value. token – это уникальный код, показывающий, что лексема имеет тип Type, ID, Assign, Constant или Semicolon, например 85 для ID и 72 для Semicolon.value содержит конкретное значение лексемы. Так, для лексемы ID в предыдущем объявлении value будет содержать строку «i», а для лексемы Type – некоторое представление типа int.

    Представление члена value несколько проблематично. Хотя для любой отдельной лексемы в нем хранится всего одно значение, их типы для разных лексем могут различаться. Для лексемы ID в value хранится строка символов, а для Constant – целое число.

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

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

    Если самым большим типом среди всех членов TokenValue является dval, то размер TokenValue будет равен размеру объекта типа double. По умолчанию члены объединения открыты. Имя объединения можно использовать в программе всюду, где допустимо имя класса:

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

    Члены объединения можно объявлять открытыми, закрытыми или защищенными:

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

    Для объединения разрешается определять функции-члены, включая конструкторы и деструкторы:

    Цукерберг рекомендует:  Sql server - Как правильно сделать шринк лога транзакций MS SQL 2008

    Вот пример работы объединения TokenValue:

    Объект типа Token можно использовать так:

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

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

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

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

    Существует анонимное объединение – объединение без имени, за которым не следует определение объекта. Вот, например, определение класса Token, содержащее анонимное объединение:

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

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

    13.8. Битовое поле – член, экономящий память

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

    После идентификатора битового поля следует двоеточие, а за ним – константное выражение, задающее число битов. К примеру, modified – это поле из одного бита.

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

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

    Вот простой пример использования битового поля длиной больше 1 (примененные здесь побитовые операции рассматривались в разделе 4.11):

    Обычно для проверки значения битового поля-члена определяются встроенные функции-члены. Допустим, в классе File можно ввести члены isRead() и isWrite():

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

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

    В стандартной библиотеке C++ имеется шаблон класса bitset, который облегчает манипуляции с битовыми множествами. Мы рекомендуем использовать его вместо битовых полей. (Шаблон класса bitset и определенные в нем операции рассматривались в разделе 4.12.)

    Перепишите примеры из этого подраздела так, чтобы в классе File вместо объявления и прямого манипулирования битовыми полями использовался класс bitset и его операторы.

    13.9. Область видимости класса A

    Тело класса определяет область видимости. Объявления членов класса внутри тела вводят их имена в область видимости класса.

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

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

    Порядок объявления членов класса в его теле важен: нельзя ссылаться на члены, которые будут объявлены позже. Например, если объявление оператора operator[]() находится раньше объявления typedef index_type, то приведенное ниже объявление operator[]() оказывается ошибочным, поскольку в нем используется еще неизвестное имя index_type:

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

    Разрешение имен в определениях встроенных функций-членов происходит в два этапа. Сначала объявление функции (т.е. тип возвращаемого значения и список параметров) обрабатывается в том месте, где оно встретилось в определении класса. Затем тело функции обрабатывается во всей области видимости, сразу после того, как были просмотрены объявления всех членов. Посмотрим на наш пример, в котором оператор operator[]() определен как встроенный внутри тела класса:

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

    Обратите внимание, что член _string объявлен в теле класса после определения operator[](). Это правильно, и _string не является в теле operator[]() необъявленным именем. Имена в телах функций-членов просматриваются на втором шаге разрешения имен в определениях встроенных функций-членов. Этот этап выполняется во всей области видимости класса, как если бы тела функций-членов обрабатывались последними, прямо перед закрытием тела класса, когда все его члены уже объявлены.

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

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

    то имя аргумента по умолчанию разрешается нестатическим членом bkground, а это считается ошибкой.

    Определения членов класса, появляющиеся вне его тела, – это еще один пример части программы, которая находится в области видимости класса. В ней имена членов распознаются несмотря на то, что оператор доступа или оператор разрешения области видимости при обращении к ним не применяется. Как же разрешаются имена в определениях членов?

    Как правило, если такое определение появляется вне тела, то часть программы, следующая за именем определяемого члена, считается находящейся в области видимости класса вплоть до конца определения члена. Вынесем определение оператора operator[]() из класса String:

    Обратите внимание, что в списке параметров встречается typedef index_type без квалифицирующего имени класса String. Текст, следующий за именем члена String::operator[] и до конца определения функции, находится в области видимости класса. Объявленные в этой области типы рассматриваются при разрешении имен типов, использованных в списке параметров функции-члена.

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

    Инициализатор _interestRate вызывает статическую функцию-член Account::initInterest() несмотря на то, что ее имя не квалифицировано именем класса.

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

    Хотя член nameSize не квалифицирован именем класса Account, определение name не является ошибкой, так как оно находится в области видимости своего класса и может ссылаться на его члены после того, как компилятор прочитал Account::name.

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

    // Money должно быть квалифицировано именем класса Account::

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

    13.9.1. Разрешение имен в области видимости класса

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

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

    1. Просматриваются объявления членов класса, появляющиеся перед употреблением имени.
    2. Если на шаге 1 разрешение не привело к успеху, то просматриваются объявления в пространстве имен перед определением класса. Напомним, что глобальная область видимости – это тоже область видимости пространства имен. (О пространствах имен речь шла в разделе 8.5.)

    Сначала компилятор ищет объявление Money в области видимости класса Account. При этом учитываются только те объявления, которые встречаются перед использованием Money. Поскольку таких объявлений нет, далее поиск ведется в глобальной области видимости. Объявление глобального typedef Money найдено, именно этот тип и используется в объявлениях _interestRate и initInterest().

    Имя, встретившееся в определении функции-члена класса, разрешается следующим образом:

    1. Сначала просматриваются объявления в локальных областях видимости функции-члена. (О локальных областях видимости и локальных объявлениях говорилось в разделе 8.1.)
    2. Если шаг 1 не привел к успеху, то просматриваются объявления для всех членов класса.
    3. Если и этого оказалось недостаточно, просматриваются объявления в пространстве имен перед определением функции-члена.

    Имена, встречающиеся в теле встроенной функции-члена, разрешаются так:

    В поисках объявления имени _height, которое встретилось в определении конструктора Screen, компилятор просматривает локальную область видимости функции и находит его там. Следовательно, это имя относится к объявлению параметра.

    Если бы такое объявление не было найдено, компилятор начал бы поиск в области видимости класса Screen, просматривая все объявления его членов, пока не встретится объявление члена _height. Говорят, что имя члена _height скрыто объявлением параметра конструктора, но его можно использовать в теле конструктора, если квалифицировать имя члена именем его класса или явно использовать указатель this:

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

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

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

    Имя, встретившееся в определении статического члена класса, разрешается следующим образом:

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

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

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

    К каким объявлениям относится имя Type при использовании в теле класса Exersise и в определении его функции-члена setVal()? (Напоминаем, что разные вхождения могут относиться к разным объявлениям.) К каким объявлениям относится имя initVal при употреблении в определении функции-члена setVal()?

    Определение функции-члена setVal() ошибочно. Можете ли вы сказать, почему? Внесите необходимые изменения, чтобы в классе Exercise использовался глобальный typedef Type и глобальная функция initVal().

    13.10. Вложенные классы A

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

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

    Для вложенного класса допустимы такие же виды членов, как и для невложенного:

    Закрытым называется член, который доступен только в определениях членов и друзей класса. У объемлющего класса нет права доступа к закрытым членам вложенного. Чтобы в определениях членов List можно было обращаться к закрытым членам ListItem, класс ListItem объявляет List как друга. Равно и вложенный класс не имеет никаких специальных прав доступа к закрытым членам объемлющего класса. Если бы нужно было разрешить ListItem доступ к закрытым членам класса List, то в объемлющем классе List следовало бы объявить вложенный класс как друга. В приведенном выше примере этого не сделано, поэтому ListItem не может обращаться к закрытым членам List.

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

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

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

    Конструктор ListItem не задан как встроенный внутри определения класса и, следовательно, должен быть определен вне него. Но где именно? Конструктор класса ListItem не является членом List и, значит, не может быть определен в теле последнего; его нужно определить в глобальной области видимости – той, которая содержит определение объемлющего класса. Когда функция-член вложенного класса не определяется как встроенная в теле, она должна быть определена вне самого внешнего из объемлющих классов.

    Вот как могло бы выглядеть определение конструктора ListItem. Однако показанный ниже синтаксис в глобальной области видимости некорректен:

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

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

    Если бы внутри ListItem был объявлен статический член, то его определение также следовало бы поместить в глобальную область видимости. Имя этого члена могло бы выглядеть так:

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

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

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

    Пока компилятор не увидел определения вложенного класса, разрешается объявлять лишь указатели и ссылки на него. Объявления членов list и at_end класса List правильны несмотря на то, что ListItem определен в глобальной области видимости, поскольку оба члена – указатели. Если бы один из них был объектом, то его объявление в классе List привело бы к ошибке компиляции:

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

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

    Если бы ListItem не был объявлен перед определением класса Ref, то объявление члена pli было бы ошибкой.

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

    При использовании нестатических членов класса компилятор должен иметь возможность идентифицировать объект, которому принадлежит такой член. Внутри функции-члена класса ListItem указатель this неявно применяется лишь к его членам. Благодаря неявному this мы знаем, что член value относится к объекту, для которого вызван конструктор. Внутри конструктора ListItem указатель this имеет тип ListItem*. Для доступа же к функции-члену init() нужен объект типа List или указатель типа List*.

    Следующая функция-член mf() обращается к init() с помощью параметра-ссылки. Таким образом, init() вызывается для объекта, переданного в аргументе функции:

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

    pFunc, ListStatus и ListItem – все это вложенные имена типов в области видимости объемлющего класса List. К ним, а также к элементам перечисления ListStatus можно обращаться в области видимости класса ListItem даже без квалификации:

    Вне области видимости ListItem и List при обращении к статическим членам, именам типов и элементам перечисления объемлющего класса требуется оператор разрешения области видимости:

    При обращении к элементам перечисления мы не пишем:

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

    13.10.1. Разрешение имен в области видимости вложенного класса

    Посмотрим, как разрешаются имена в определениях вложенного класса и его членов.

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

    1. Просматриваются члены вложенного класса, расположенные перед употреблением имени.
    2. Если шаг 1 не привел к успеху, то просматриваются объявления членов объемлющего класса, расположенные перед употреблением имени.
    3. Если и этого недостаточно, то просматриваются объявления, расположенные в области видимости пространства имен перед определением вложенного класса.

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

    Если вложенный класс ListItem определен в глобальной области видимости, вне тела объемлющего класса List, то все члены List уже были объявлены:

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

    Имя, встретившееся в определении функции-члена вложенного класса, разрешается следующим образом:

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

    Какое объявление относится к имени list в определении функции-члена check_status() в следующем фрагменте кода:

    Весьма вероятно, что при использовании list внутри check_status() программист имел в виду глобальный объект:

    • и value, и глобальный объект list имеют тип int. Член List::list объявлен как указатель и не может быть присвоен value без явного приведения типа;
    • ListItem не имеет прав доступа к закрытым членам объемлющего класса, в частности list;
    • list – это нестатический член, и обращение к нему в функциях-членах ListItem должно производиться через объект, указатель или ссылку.

    Однако, несмотря на все это, имя list, встречающееся в функции-члене check_status(), разрешается в пользу члена list класса List. Напоминаем, что если имя не найдено в области видимости вложенного ListItem, то далее просматривается область видимости объемлющего класса, а не глобальная. Член list в List скрывает глобальный объект. А так как использование указателя list в check_status() недопустимо, то выводится сообщение об ошибке.

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

    Если бы функция-член check_status() была определена как встроенная в теле класса ListItem, то последнее объявление привело бы к выдаче сообщения об ошибке из-за того, что имя list не объявлено в глобальной области видимости:

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

    В главе 11 был приведен пример программы, использующей класс iStack. Измените его, объявив классы исключений pushOnFull и popOnEmpty открытыми вложенными в iStack. Модифицируйте соответствующим образом определение класса iStack и его функций-членов, а также определение main().

    13.11. Классы как члены пространства имен A

    Представленные до сих пор классы определены в области видимости глобального пространства имен. Но их можно определять и в объявленных пользователем пространствах. Имя класса, определенного таким образом, доступно только в области видимости этого пространства, т.е. оно не конфликтует с именами, объявленными в других пространствах имен. Например:

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

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

    Как происходит разрешение имени в определении члена, которое находится в объявленном пользователем пространстве? Например, как будет разрешено someVal:

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

    Определение пространства имен cplusplus_primer не является непрерывным. Определения класса List и объекта someVal размещены в первом его разделе, который находится в заголовочном файле primer.h. Определение функции calc() появляется в определении пространства имен, расположенном в файле реализации primer.C. Использование calc() внутри action() ошибочно, так как она объявлена после использования. Если calc() – часть интерфейса cplusplus_primer, ее следовало бы объявить в той части данного пространства, которая находится в заголовочном файле:

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

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

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

    Квалификаторы cplusplus_primer::List::ListItem:: записаны в порядке, обратном тому, в котором просматриваются имена областей видимости классов и пространств имен. Сначала поиск ведется в области ListItem, затем продолжается в объемлющем классе List и наконец в пространстве cplusplus_primer, предшествующем той области, в которой находится определение action(). Во время поиска в любой области видимости класса просматриваются все объявления членов, а в любом пространстве имен – только те объявления, которые встречались перед определением члена.

    Класс, определенный в области видимости пространства имен, потенциально виден во всей программе. Если заголовочный файл primer.h включен в несколько исходных файлов, то имя cplusplus_primer::List везде относится к одному и тому же классу. Класс – это сущность, для которой в программе может быть более одного определения. Определение класса должно присутствовать один раз в каждом исходном файле, где определяются или используются сам класс или его члены. Однако оно должно быть одинаковым во всех файлах, где встречается, поэтому его следует помещать в заголовочный файл, например primer.h. Затем такой файл можно включать в любой исходный, где определяются или используются члены класса. Это предотвратит несоответствия в случае, когда определение класса записывается более одного раза.

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

    Используя класс iStack, определенный в упражнении 13.21, объявите классы исключений pushOnFull и popOnEmpty как члены пространства имен LibException:

    а сам iStack – членом пространства имен Container. Модифицируйте соответствующим образом определение данного класса и его функций-членов, а также определение main().

    13.12. Локальные классы A

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

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

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

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

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

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

    Как всегда, если первое найденное объявление таково, что употребление имени оказывается некорректным, поиск других объявлений не производится. Несмотря на то что использование val в fooBar() выше является ошибкой, глобальная переменная val не будет найдена, если только ее имени не предшествует оператор разрешения глобальной области видимости.

    Спасибо за материал

    Отличный текст и всё нужно в одном месте. Часто пользуюсь! Спасибо.

    Урок №122. Классы и заголовочные файлы

    Обновл. 16 Июн 2020 |

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

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

    Отделение объявления от реализации

    К счастью, C++ предоставляет способ отделить «объявление» от «реализации». Это делается путём определения методов вне тела самого класса. Для этого просто определите методы класса, как если бы они были обычными функциями, но в качестве префикса добавьте к имени функции имя класса с оператором разрешения области видимости : : (то же, что используется с пространствами имён).

    Вот наш класс Date с конструктором Date и методом setDate(), определёнными вне тела класса. Обратите внимание, прототипы этих функций всё ещё находятся внутри тела класса, но их фактическая реализация находится за его пределами:

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

    Вот ещё один пример класса с конструктором, определённым извне, со списком инициализации членов:

    Классы и заголовочные файлы

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

    Цукерберг рекомендует:  Php - PHP и чекбоксы,- помогите.

    Вот наш Date, но уже разбитый на файлы .cpp и .h:

    Теперь любой другой файл .h или .cpp, который захочет использовать класс Date, сможет просто #include «Date.h» . Обратите внимание, Date.cpp также необходимо добавить к компиляции в проект, который использует Date.h, чтобы линкер смог разобраться с реализацией класса Date.

    Вопрос №1: Разве определение класса в заголовочном файле не нарушает правило одного определения?

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

    Вопрос №2: Разве определения методов класса в заголовочном файле не нарушает правило одного определения?

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

    Методы, определённые вне тела класса, рассматриваются как обычные функции и подчиняются правилу одного определения. Поэтому эти функции должны быть определены в файле .cpp, а не внутри .h. Единственное исключение: шаблоны функций, но об этом уже в следующей главе.

    Параметры по умолчанию

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

    Библиотеки

    Разделение объявления класса и его реализации очень распространено с библиотеками, которые используются для расширения возможностей вашей программы. Вы также подключали заголовочные файлы из стандартной библиотеки, такие как iostream, string, vector, array и другие. Обратите внимание, вы не добавляли iostream.cpp, string.cpp, vector.cpp или array.cpp в ваши проекты. Ваша программа нуждается только в объявлениях из заголовочных файлов, чтобы компилятор мог проверить корректность вашего кода в соответствии с правилами синтаксиса C++. Однако, реализации классов, находящихся в стандартной библиотеке, содержатся в предварительно скомпилированном файле, который добавляется на этапе линкинга. Вы никогда не видите этот код.

    Вне программ с открытым исходным кодом (где предоставляются оба файла: .h и .cpp), большинство сторонних библиотек предоставляют только заголовочные файлы вместе с предварительно скомпилированным файлом библиотеки. На это есть несколько причин:

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

    Защита интеллектуальной собственности (создатели не хотят, чтобы другие просто «крали» их код).

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

    Заключение

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

    Во-первых, как упоминалось выше, это приведёт к загромождению определения вашего класса.

    Во-вторых, функции, определённые внутри класса, являются неявно встроенными. Большие функции, которые вызываются из многих файлов, могут способствовать, таким образом, к «раздуванию» вашего кода.

    В-третьих, если вы измените что-либо в заголовочном файле, то вам нужно будет перекомпилировать каждый файл, содержащий этот заголовок. Это может иметь «эффект бабочки», когда одно незначительное изменение заставит перекомпилировать всю программу (что может быть достаточно медленно и долго). Если же вы изменили код в файле .cpp, то вам необходимо перекомпилировать только этот .cpp файл!

    Поэтому рекомендуется следующее:

    Классы, используемые только в одном файле, и которые повторно не используются, определяйте непосредственно в файле .cpp, где они используются.

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

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

    Нетривиальные методы определяйте в файле .cpp с тем же именем, что у класса.

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

    Как реализовать класс В C?

    предполагая, что я должен использовать C (без C++ или объектно-ориентированных компиляторов), и у меня нет динамического выделения памяти, какие методы я могу использовать для реализации класса или хорошего приближения класса? Это всегда хорошая идея, чтобы изолировать «класс» в отдельный файл? Предположим, что мы можем предварительно выделить память, предположив фиксированное число экземпляров или даже определив ссылку на каждый объект как константу до времени компиляции. Не стесняйтесь делать предположения о том, какая концепция ООП Мне нужно будет реализовать (он будет меняться) и предложить лучший метод для каждого.

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

    19 ответов:

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

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

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

    если вы хотите несколько различных конструкторов, вам придется «украсить» имена функций, вы не можете иметь более одного rectangle_new() функция:

    вот основной пример, показывающий использование:

    Я надеюсь, что это дает вам некоторые идеи, по крайней мере. Для успешных и богатых объектно-ориентированного программирования в с++, посмотрите на Глеба-х GObject библиотека.

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

    Я должен был сделать это один раз тоже для домашнего задания. Я следовал этому подходу:

    1. определите элементы данных в a структура.
    2. определите члены функции, которые возьмите указатель на вашу структуру как первый аргумент.
    3. сделайте это в одном заголовке и одном c. Заголовок для определения структуры & объявления функций, C для реализации.

    простой пример был бы такой:

    если вы хотите только один класс, использовать массив struct s как «объекты» данных и передать указатели на них в функции «член». Вы можете использовать typedef struct _whatever Whatever перед объявлением struct _whatever чтобы скрыть реализацию от клиентского кода. Нет никакой разницы между таким «объектом» и стандартной библиотекой C FILE «объект».

    если вы хотите более одного класса с наследованием и виртуальными функциями, то обычно указатели на функции являются членами структуры или a общий указатель на таблицу виртуальных функций. Элемент GObject библиотека использует как это, так и трюк typedef, и широко используется.

    есть также книга о методах для этого доступны в интернете -объектно-ориентированное программирование с ANSI C.

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

    C интерфейсы и реализации: методы создания многоразового программного обеспечения, Дэвид Р. Хэнсон

    эта книга делает отличную работу по освещению вашего вопроса. Это в профессиональной вычислительной серии Addison Wesley.

    основная парадигма выглядит примерно так:

    Миро Самек разработал объектно-ориентированную структуру C для своей структуры государственной машины:http://sourceforge.net/projects/qpc/. и он также написал об этом книгу:http://www.state-machine.com/psicc2/.

    использовать struct для имитации элементов данных класса. С точки зрения области метода вы можете имитировать частные методы, поместив частная прототипы функций в .c файл и общественные функции .H-файл.

    Я приведу простой пример того, как ООП должно быть сделано в C. Я понимаю, что это thead с 2009 года, но хотел бы добавить это в любом случае.

    основная концепция предполагает размещение «унаследованного класса» в верхней части структуры. Таким образом, доступ к первым 4 байтам в структуре также обращается к первым 4 байтам в «унаследованном классе» (Asuming non-crazy optimalisations). Теперь, когда указатель структуры приводится к «унаследованному классу», «унаследованный класс» может получить доступ «унаследованные значения» точно так же, как он обычно обращается к своим членам.

    это и некоторые соглашения об именах для конструкторов, деструкторов, распределения и функций deallocarion (я рекомендую init, clean, new, free) помогут вам пройти долгий путь.

    Что касается виртуальных функций, используйте указатели функций в структуре, возможно, с Class_func(. ); обертка тоже. Что касается (простых) шаблонов, добавьте параметр size_t для определения размера, требуйте указателя void* или требуйте «класса» введите только те функции, которые вас волнуют. (например, int GetUUID (Object *self); GetUUID(&p);)

    в вашем случае хорошим приближением класса может быть ADT. Но все равно это будет не то же самое.

    • определите весь код для класса в отдельном файле
    • определите все интерфейсы для класса в отдельном заголовочном файле
    • все функции-члены принимают «ClassHandle», который стоит в качестве имени экземпляра (вместо o. foo (), вызов foo(oHandle)
    • конструктор заменяется функцией void ClassInit (ClassHandle h, int x, int y. ) Или ClassHandle ClassInit(int x, int y. ) в зависимости от памяти стратегия распределения
    • все переменные-члены хранятся как член статической структуры в файле класса, инкапсулируя его в файл, предотвращая доступ к нему внешних файлов
    • объекты хранятся в массиве статической структуры выше, с предопределенными дескрипторами( видимыми в интерфейсе) или фиксированным пределом объектов, которые могут быть созданы
    • если это полезно, класс может содержать открытые функции, которые будут проходить через массив и вызывать функции все экземпляры объектов (RunAll () вызывает каждый Run (oHandle)
    • функция Deinit(ClassHandle h) освобождает выделенную память (индекс массива) в динамической стратегии выделения

    кто-нибудь видит какие-либо проблемы, дыры, потенциальные ловушки или скрытые преимущества/недостатки в любом варианте этого подхода? Если я изобретаю метод проектирования (и я предполагаю, что я должен быть), Можете ли вы указать мне на его название?

    есть очень обширная книга на эту тему, которую, возможно, стоит проверить:

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

    скрытие данных с помощью функций get / set является легко реализовать в C, но остановиться на этом. Я видел несколько попыток в этом во встроенной среде и в конце концов это всегда проблема обслуживания.

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

    может быть этой онлайн-книги может помочь вам решить вашу проблему.

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

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

    первый компилятор c++ на самом деле был препроцессором, который перевел код C++ В C.

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

    GTK полностью построен на C и использует множество концепций ООП. Я прочитал исходный код GTK, и это довольно впечатляет, и, безусловно, легче читать. Основная концепция заключается в том, что каждый «класс» — это просто структура и связанные с ней статические функции. Все статические функции принимают структуру «экземпляр» в качестве параметра, делают все, что нужно, и при необходимости возвращают результаты. Например, у вас может быть функция «GetPosition(CircleStruct obj)». Функция будет просто копать через структуру извлеките номера позиций, вероятно, постройте новый объект PositionStruct, вставьте x и y в новую PositionStruct и верните его. GTK даже реализует наследование таким образом, внедряя структуры внутри структур. очень умно.

    вы хотите виртуальные методы?

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

    Если вы хотите иметь виртуальные методы, это становится более сложным. В основном вам нужно будет реализовать свою собственную таблицу VTable для каждой структуры и назначить указатели функций для таблицы vtable в зависимости на котором вызывается функция. Затем вам понадобится набор указателей функций в самой структуре, которые, в свою очередь, вызывают указатель функции в таблице VTable. Это, по сути, с++.

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

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

    кроме того, если вы можете использовать C, вы можете использовать C++ и получить классы.

    C#) — Реализация классов в c#

    Эта статья даст базовое понимание терминов «класс», «метод», «наследование», «перегрузка метода»

    Содержание

    Методы

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

    Взгляните на пример:

    Методу Vec2f::getLength доступны все символы (т.е. переменные, функции, типы данных), которые были объявлены в одной из трёх областей видимости. При наличии символов с одинаковыми идентификаторами один символ перекрывает другой, т.к. поиск происходит от внутренней области видимости к внешней.

    Понять идею проще на схеме. В ней область видимости названа по-английски: scope.

    Поднимаясь по схеме от внутренней области видимости к внешней, легко понять, какие имена символов доступны в методе getLength:

    1. локальная переменная “lengthSquare”
    2. поля Vec2f под именами “x” и “y”
    3. всё, что есть в глобальной области видимости

    К слову, в других методах структуры Vec2f переменная “lengthSquare” будет недоступна, а поля “x” и “y” будут доступны.

    Конструкторы

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

    Посмотрите на простой пример. В нём есть проблема: и поля, и параметры конструктора названы одинаково. В результате в области видимости конструктора доступны только параметры, и своими именами они перекрывают поля!

    Язык C++ предлагает два решения. Первый способ — использовать косвенное обращение к полям через привязанный к методу объект. Указатель на него доступен по ключевому слову this :

    Второй путь считается более правильным: мы используем специальную возможность конструкторов — “списки инициализации конструктора” (англ. constructor initializer lists). Списки инициализации — это список, разделённый запятыми и начинающийся с “:”. Элемент списка инициализации выглядит как field(expression) , т.е. для каждого выбранного программистом поля можно указать выражение, инициализирующее его. Имя переменной является выражением. Поэтому мы инициализируем поле его параметром:

    Объявление и определение методов

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

    Классы и структуры

    В C++ есть ключевое слово class — это практически аналог ключевого слова struct . Оба ключевых слова объявляют тип данных, и разница между ними есть только на стыке наследования и инкапсуляции. Других различий class и struct не существует.

    Основы инкапсуляции

    В C++ можно блокировать доступ к полям извне, но сохранять доступ для методов. Для этого введены три области доступа

    1. public — символ в этой области доступен извне
    2. private — символ из этой области доступен лишь собственных в методах
    3. protected — используется редко, о нём можете прочитать в документации

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

    Запомните несколько хороших правил:

    • Используйте struct, если все поля публичные и не зависят друг от друга; используйте class, если между полями должны соблюдаться закономерности (например, поле “площадь” круга должно быть)

    Основы наследования

    В C++ новый тип может наследовать все поля и методы другого типа. Для этого достаточно указать структуру или класс в списке базовых типов. Такой приём используется в SFML при объявлении классов фигур:

    Что означает public перед именем базового типа? Во-первых внешний код может передать RectangleShape в функцию, принимающую ссылку на Shape, то есть возможен так называемы upcast от более низкого (и более конкретного) типа RectangleShape к более высокому (и более абстрактному) типу Shape:

    Во-вторых из-за public наследования все унаследованные поля и методы сохраняют свой уровень доступ: приватные остаются приватными, публичные остаются публичными. А если бы мы наследовали Shape с ключевым словом private, то уровень доступа стал бы ниже: все методы и поля стали бы приватными:

    Контроль уровня доступа полей и методов — хитрый механизм, пройдёт немало времени, прежде чем вы научитесь пользоваться им правильно. В начале просто старайтесь сделать правильный выбор между private и public. Скорее всего поля будут private, а конструктор и все методы будут public. Это позволяет сохранять инвариант класса, то есть держать поля объекта в согласованном состоянии независимо от того, какие методы вызывают извне.

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

    SFML использует ещё одну идиому C++: виртуальные методы. Ключевые слова virtual , final , override относятся именно к этой идиоме. Например, в SFML определяется класс Drawable, который обозначает “сущность, которую можно нарисовать”. Все рисуемые классы SFML, включая sf::Sprite , sf::RectangleShape , sf::Text , прямо или косвенно наследуются от sf::Drawable .

    Зачем это надо? Дело в том, что метод draw класса RenderWindow принимает параметр типа Drawable . Тем не менее, этот метод успешно рисует любые типы объектов: спрайты, фигуры, тексты. Он не выполняет проверок — он просто настраивает состояние рисования (RenderStates) и вызывает метод draw у сущности, которая является Drawable .

    Виртуальный метод вызывается косвенно: если класс Shape , унаследованный от Drawable , переопределил метод, а потом был передан как параметр типа Drawable , то вызов метода draw всё равно приведёт к вызову переопределённого метода Shape::draw , а не метода Drawable::draw ! С обычными (не виртуальными) методами такого не происходит: если бы мы убрали слово virtual из объявления draw , то вызов метода draw у параметра типа Drawable всегда приводил бы к вызову Drawable::draw , даже если реальный тип объекта, скрытого за этим параметром, совсем другой.

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

    Другими словами, RenderWindow и RectangleShape не знают, что они работают друг с другом, но тем не менее каждый вызывает правильный метод другого класса!

    Когда вы просто вызываете window.draw(shape) , повышение класса происходит дважды: сначала конкретный класс фигуры повышается до более ограниченного класса Drawable, затем конкретный класс RenderWindow повышается до абстрактного RenderTarget. Всё это не требует времени при выполнении: просто компилятор выполняет проверки типов данных ещё при компиляции, не более того.

    Как унаследовать Drawable: практический пример

    Мы создадим класс, который рисует флаг России. Он будет унаследован от Drawable, чтобы использовать для рисования обычный метод draw у объекта окна.

    Теперь мы можем реализовать конструктор и метод draw. В конструкторе мы должны вычислить и установить позиции и размеры трёх полос на флаге, а в методе draw мы должны их последовательно нарисовать.

    Теперь использовать класс RussianFlag извне очень легко!

    Реализация класса c#

    Имеется 3 формы, на каждой по Chart.

    Логично написать класс, реализующий функционал настройки данных Chart и рисования на них. Проблема в том, что из методов класса естественно не видно контролов форм(private всё-таки). Настройка и рисование сопровождаются довольно большим набором условий, которые для всех трёх форм абсолютно разные. В том числе и по типам и количеству проверяемых/принимаемых значений.

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

    1. Внешним классом с передачей нужного Chart непосредственно в метод.
    2. Классом для Chart с интерфейсом (соответственно будет 3 объекта,только не очень понятно как их отличать).
    3. Как-то ещё.

    Есть подозрение, что вопрос глуп и ответ на него элементарен, но прозрения пока не приходит.

    Пока остановился на варианте:

    Общий класс для всех форм, в нем и будут определены все нужные методы и в его объект передана ссылка на нужный Chart, где рисовать.

    drawer — тот, кто рисует, наследуется от класса DrawClass, в котором хранятся и вызываются все необходимые методы для настройки и рисования (в главном методе MainFunc). func — переопределенная функция абстрактного класса, которая будет различна для каждой формы (и привязанного к ней класса Drawer). Приходится передавать в конструктор класса ссылку на текущую форму, чтобы получать доступ к контролам (label). Имеет ли сие творение право на жизнь или выкинуть, забыть и face-vs-table?

    Классы c#

    Всем доброго времени суток. На связи Алексей Гулынин. В прошлой статье мы с вами разобрали конструкцию switch case c#. В данной статье я бы хотел рассказать, что такое классы в C#. Классы — это фундамент языка C#, в нем заключена вся суть объектов. Внутри класса определяются данные и действия, выполняемые над этими данными. Класс — это обертка (шаблон), которая определяет форму объектов. Объекты, в свою очередь, — это экземпляры класса. Определяя класс, мы создаем данные. В данном случае данные — это переменные-экземпляры класса. Над данными должен выполняться определенный код. Реализация кода находится в методах класса.

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

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

    1) private . Данный уровень выставлен по умолчанию. Если вы ничего не напишите, то данный член класса будет иметь уровень доступа private . Это закрытый уровень доступа. Обращаться к данному члену класса, можно только из самого класса.
    2) public . Это самый открытый уровень доступа. Члены данного класса могут быть доступны из любой части программы, а не только из класса.
    3) protected . Это нечто среднее между public и private . К членам с данным уровнем доступа можно обращаться из самого класса, и из классов-наследников (одна из трех парадигм объектно-ориентированного программирования — это наследование, об этом поговорим в одной из следующих статей).

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

    Класс — это нечто абстрактное, он не создает реальность. А вот объект — это уже нечто реальное. Давайте создадим объект класса Room :

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

    Т.е. мы видим, что обращение к переменным-членам класса идёт через оператор «.». Давайте сообщим пользователю объем комнаты и имеет ли данная комната окна:

    Можно создавать сколько угодно объектов от одного класса. И все эти объекты могут иметь различные значения переменных.
    Пока в нашем классе содержатся только переменные-экземпляры. Давайте добавим в него пару методов. Сразу отмечу, что в хорошей программе один метод выполняет только одну задачу. Методу можно присвоить любое имя, за исключением ключевых слов C#, а также Main() , так как данный метод служит точкой входа в программу. Реализуем метод, который возвращает площадь комнаты, а также реализуем метод, который возвращает объём комнаты (так сказать рефакторинг кода):

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

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

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

    На связи был Алексей Гулынин, оставляйте свои комментарии, увидимся в следующих статьях.

    C#) — Реализация классов в c#

    Цель: пассивный доход 100 тыс. руб. в мес.

    Также в блоге: изучаем и разрабатываем что-то новое, делимся опытом.

    Новый язык программирования, для разработки сайтов и приложений, подробнее.

    Sitev.ru
    Социальная сеть программистов, дизайнеров, предпринимателей . и обычных пользователей Sitev.ru/post
    Блог программиста, аналог Хабра C++ фреймворк
    Комплекс кроссплатформенных библиотек для разработки веб, мобайл, десктоп-приложений, игр. Sitev C++ CMS
    Быстрая, надежная, масштабируемая CMS для разработки сайтов на C++ полный список задач.

    Классы в C++. Урок 8

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

    Зачем нужны классы в C++?

    Алан Кей, который придумал ООП, был по образованию не только математиком, но и биологом.

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

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

    Понятия: класс и объект в C++

    Для чего нужны классы? Приведём аналогии. Представим, что нам нужно написать программу учета успеваемости студентов. Понятие студента абстрактное — это общее описание человека, обучающегося в учебном заведении. Студент — это класс ООП. Экземпляры этого класса, конкретные люди: Аня, Маша и Наташа — объекты. В этом случае класс — это описание объектов.

    Или другая аналогия, например, автомобиль. Автомобиль — это объект, который был построен согласно чертежам, эти самые чертежи и играют роль классов. В этом случае классы — это чертежи, по которым создаются объекты.

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

    Вот так класс Student (студент) описывается в С++:

    А вот пример класса Car (автомобиль) на C++:

    Для упрощения, привожу типы auto и void .

    Класс в С++ — это абстракция описывающая свойства и методы, ещё не существующих объектов. Объект — конкретное представление абстракции, имеющее свои свойства и методы.

    Классы описали, как теперь создать объекты? Да очень просто!

    В статической памяти:

    В динамической памяти:

    Три кита ООП

    В ООП существует три основных принципа построения классов:

    • Инкапсуляция (лат. in capsula — в оболочке) — объединяем в классе данные и методы, скрываем детали реализации от пользователя
    • Наследование — порождаем новый класс-потомок на основе уже существующего, все свойства и методы класса-родителя присваиваются классу-потомку
    • Полиморфизм — методы с одним названием для объектов различных классов имеют различную реализацию

    Свойство color и метод paint() размещены вместе в классе Shape — это инкапсуляция. Порождаем класс Circle от Shape и Rectagle от Shape — это наследование. Метод с одним название paint() для различных классов Circle и Rectagle имеет различную реализацию — это полиморфизм.

    Модификаторы доступа

    Создавая классы, как мы это делали выше, свойства и методы никому, кроме этого класса недоступны. Мы какбы по-умолчанию применяли модификатор доступа private .

    Теперь мы хотим чтобы свойство color было доступно только потомкам класса Shape , а метод paint() — всем, не разбирая, кому угодно. Для этого применяются модификаторы protected и public соответственно.

    Геттеры и Сеттеры

    Инкапсуляция в C++ предполагает, что мы не только объединяем в классе данные и методы, но и скрываем детали реализации от пользователя. Под этим подразумевается, что мы не должны переменные помещать в public .

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

    Для этого и применяются геттеры и сеттеры. Эти страшные названия означают лишь функции, начинающиеся на get и set.

    Напишем пример нашего чайника:

    Конструктор и деструктор класса в C++

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

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

    . Деструктор не имеет входных параметров.

    Ну вот, пожалуй, и всё! В эту статью, я, можно сказать, вложил всю свою душу и весь свой многолетний опыт (плюс понадёргал информацию из других источников :) ), так как являюсь ярым поклонником парадигмы ООП! Читайте внимательно, пытайтесь понять, осмыслить, переосмыслить, не бойтесь перечитывать по нескольку раз — ну очень пригодится в дальнейшем при разработки программ!

    Сравнение классов и структур в C++

    Классы в С++ определяются ключевым словом class . Они представляют собой форму структуры, у которой спецификация доступа по умолчанию – private , то есть

    есть сокращенная запись

    Хотя такое определение класса и справедливо, базовым в языке С++ является понятие класса. В С++ принято считать, что структура struct – это просто класс, все члены которого общие, то есть

    Структуры необходимо использовать в тех случаях, когда сокрытие данных неуместно.
    В качестве иллюстрации, представим абстрактный тип данных для комплексных чисел с помощью структуры и с помощью класса. Единственное отличие — в использовании ключевых слов: private и public .
    С использованием структуры:

    С использованием класса:

    Чаще всего структуры содержат только поля и не содержат методов.
    Если предполагается создание объектов, для которых будут реализованы методы их использования, то целесообразно использовать класс.

    Понравилась статья? Поделиться с друзьями:
    Все языки программирования для начинающих