Классы, объекты, методы
Java является объектно-ориентированным языком, поэтому такие понятия как «класс» и «объект» играют в нем ключевую роль. Любую программу на Java можно представить как набор взаимодействующих между собой объектов.
Шаблоном или описанием объекта является класс (class), а объект представляет экземпляр класса. Можно провести следующую аналогию. У нас у всех есть некоторое представление о машине — наличие двигателя, шасси, кузова и т.д. Есть некоторый шаблон auto — этот шаблон можно назвать классом. Реально же существующий автомобиль auto_solaris (фактически экземпляр данного класса) является объектом этого класса.
Определение класса
Класс определяется с помощью ключевого слова сlass. Вся функциональность класса представлена его членами — полями (полями называются переменные класса) и методами. Например, класс Book мог бы иметь следующее описание :
class Book < public String name; public String author; public int year; public void Info()< System.out.printf("Книга '%s' (автор %s) была издана в %d году \n", name, author, year); >>
Таким образом, в классе Book определены три переменных и один метод Info, который выводит значения этих переменных.
Кроме обычных методов в классах используются также и специальные методы, которые называются конструкторами. Конструкторы нужны для создания нового объекта данного класса и, как правило, выполняют начальную инициализацию объекта. Название конструктора должно совпадать с названием класса :
class Book < public String name; public String author; public int year; Book()< this.name = "неизвестно"; this.author = "неизвестно"; this.year = 0; >Book(String name, String author, int year) < this.name = name; this.author = author; this.year = year; >public void Info()
Класс Book имеет два конструктора. Первый конструктор без параметров присваивает «неопределенные» начальные значения полям. Второй конструктор присваивает полям класса значения, которые передаются через его параметры.
Так как имена параметров и имена полей класса в данном случае у нас совпадают — name, author, year, то мы используем ключевое слово this. Это ключевое слово представляет ссылку на текущий объект. Поэтому в выражении this.name = name; первая часть this.name означает, что name — это поле текущего класса, а не название параметра name. Если бы у нас параметры и поля назывались по-разному, то использовать слово this было бы необязательно.
Мы можем определить несколько конструкторов для установки разного количества параметров и затем вызывать один конструктор класса из другого :
public class Book < public String name; public String author; public int year; Book(String name, String author) < this.name = name; this.author = author; >Book(String name, String author, int year) < // вызов конструктора с двумя параметрами this(name, author); // определение третьего параметра this.year = year; >>
Вызов конструктора класса с двумя параметрами производится с помощью ключевого слова this, после которого в скобках указывается список параметров.
Создание объекта
Чтобы непосредственно использовать класс в программе, надо создать его объект. Процесс создания объекта двухступенчатый: вначале объявляется переменная данного класса, а затем с помощью ключевого слова new и конструктора непосредственно создается объект, на который и будет указывать объявленная переменная :
Book b; // объявление переменной определенного типа/класса b = new Book(); // выделение памяти под объект Book
После объявления переменной Book b; эта переменная еще не ссылается ни на какой объект и имеет значение null. Затем создаем непосредственно объект класса Book с помощью одного из конструкторов и ключевого слова new.
Инициализаторы
Кроме конструктора начальную инициализацию полей объекта можно проводить с помощью инициализатора объекта. Так можно заменить конструктор без параметров следующим блоком :
public class Book < public String name; public String author; public int year; /* начало блока инициализатора */ < name = "неизвестно"; author = "неизвестно"; year = 0; >/* конец блока инициализатора */ Book(String name, String author, int year) < this.name = name; this.author = author; this.year = year; >>
Методы класса
Метод класса в объектно-ориентированном программировании — это функция или процедура, принадлежащая какому-либо классу или объекту.
Как и процедура в процедурном программировании, метод состоит из некоторого количества операторов для выполнения определенного действия и может иметь набор входных параметров.
Различают простые методы и статические методы :
- простые методы имеют доступ к данным объекта конкретного экземпляра (данного класса);
- статические методы не имеют доступа к данным объекта, и для их использования не нужно создавать экземпляры (данного класса).
Методы предоставляют интерфейс, при помощи которого осуществляется доступ к данным объекта некоторого класса, тем самым, обеспечивая инкапсуляцию данных.
Кроме имени и тела (кода) у метода есть ряд других характеристик:
- набор модификаторов;
- тип возвращаемого значения;
- набор аргументов (параметров).
Модификаторы метода — public, protected, private
Модификаторы метода определяют уровень доступа. В зависимости от того, какой уровень доступа предоставляет тот или иной метод, выделяют:
- public : открытый — общий интерфейс для всех пользователей данного класса;
- protected : защищённый — внутренний интерфейс для всех наследников данного класса;
- private : закрытый — интерфейс, доступный только изнутри данного класса.
Такое разделение интерфейсов позволяет сохранять неизменным открытый интерфейс, но изменять внутреннюю реализацию.
Для того чтобы создать статический метод, перед его именем надо указать модификатор static. Если этого не сделать, то метод можно будет вызывать только в приложении к конкретному объекту данного класса (будет нестатическим).
Класс может включать метод main, который должен иметь уровень доступа public; к нему обращается виртуальная машина Java, не являющаяся частью какого-либо пакета.
Абстрактный класс, abstract class
Абстрактный класс в объектно-ориентированном программировании — базовый класс, который не предполагает создания экземпляров. Абстрактные классы реализуют на практике один из принципов ООП — полиморфизм. Абстрактный класс может содержать (и не содержать) абстрактные методы. Абстрактный метод не реализуется для класса, в котором описан, однако должен быть реализован для его неабстрактных потомков. Пример абстрактного класса, включающего две абстрактные функции.
public abstract class Price < public abstract int getPriceCode(); public abstract double summPrice(int days); public int bonusPrice(int days) < return 1; >>
Переопределение метода, Override
В реализации ReleasePrice, наследующего свойства класса Price, «реализуем» абстрактные методы и «переопределяем» метод с использованием аннотации @Override :
public class ReleasePrice extends Price < public int getPriceCode() < return Disk.NEW_RELEASE; >public double summPrice(int days) < double summ = days*3; return summ; >/* * Метод bonusPrice переопределяем. В родительском метод возвращал 1, а здесь уже идет расчет */ @Override public int bonusPrice(int days) < if (days >1) return 2; return 1; > >
Теперь, если в родительском класса Price метод bonusPrice будет удален или переименован, то среда разработки должна выдать соответствующее сообщение. Компилятор также выдаст сообщение об ошибке.
Перегрузка методов, overload
Совокупность имени метода и набора формальных параметров называется сигнатурой метода. Java позволяет создавать несколько методов с одинаковыми именами, но разными сигнатурами. Создание метода с тем же именем, но с другим набором параметров называется перегрузкой. Какой из перегруженных методов должен выполняться при вызове, Java определяет на основе фактических параметров, передаваемых методу.
Пример класса Test с тремя перегруженными методами test :
public class Test < void test() < System.out.println("параметр отсутствует"); >void test(int a) < System.out.println("a = " + String.valueOf(a)); >void test (int a, int b) < System.out.println("a + b = " + String.valueOf(a + b)); >void test(double d) < System.out.println("d = " + String.valueOf(d)); >>
Пример использования класса Test:
public class Testing < public static void main(String args[]) < Test of = new Test(); double result = 30.25; of.test(); of.test(10); of.test(20, 10); of.test(result); >>
Java рекурсия
Рекурсией называется метод (функция), которая внутри своего тела вызывает сама себя.
Рассмотрим пример рекурсивного метода вычисления факториала. Для того чтобы вычислить n!, достаточно знать и перемножить между собой (n-1)! и n. Создадим метод, реализующий описанный способ.
static int fact (int n) < if (n == 1) < return 1; >else if (n == 2) < return 2; >else < return fact(n-1) * n; >>
Указанный рекурсивный метод вычисляет факториал натурального числа.
Рассмотрим пример, вычисляющий через рекурсию n-ое число Фибоначчи. Напомним, как выглядят первые элементы этого ряда: 1 1 2 3 5 8 13 …
static int fib (int n) < if (index else if (n == 1) return 1; else if (n == 2)) return 1; return fib (n-2) + fib (n-1); >
Суперкласс Object
В Java есть специальный суперкласс Object и все классы являются его подклассами. Поэтому ссылочная переменная класса Object может ссылаться на объект любого другого класса. Так как массивы являются тоже классами, то переменная класса Object может ссылаться и на любой массив.
У класса Object есть несколько важных методов:
Метод | Описание |
---|---|
Object clone() | Функция создания нового объекта, не отличающий от клонируемого |
boolean equals(Object object) | Функция определения равенства текущего объекта другому |
void finalize() | Процедура завершения работы объекта; вызывается перед удалением неиспользуемого объекта |
Class getClass() | Функция определения класса объекта во время выполнения |
int hashCode() | Функция получения хэш-кода объекта |
void notify() | Процедура возобновления выполнения потока, который ожидает вызывающего объекта |
void notifyAll() | Процедура возобновления выполнения всех потоков, которые ожидают вызывающего объекта |
String toString() | Функция возвращает строку описания объекта |
void wait() | Ожидание другого потока выполнения |
void wait(long ms) | Ожидание другого потока выполнения |
void wait(long ms, int nano) | Ожидание другого потока выполнения |
Методы getClass(), notify(), notifyAll(), wait() являются «финальными» (final) и их нельзя переопределять.
Проверка принадлежности класса instanceof
Для проверки принадлежности класса какому-либо объекту необходимо использовать ключевого слова instanceof. Иногда требуется проверить, к какому классу принадлежит объект. Это можно сделать при помощи ключевого слова instanceof. Это логический оператор, и выражение foo instanceof Foo истинно, если объект foo принадлежит классу Foo или его наследнику, или реализует интерфейс Foo (или, в общем виде, наследует класс, который реализует интерфейс, который наследует Foo).
Пример с рыбками. Допустим имеется родительский класс Fish и у него есть унаследованные подклассы SaltwaterFish и FreshwaterFish. Необходимо протестировать, относится ли заданный объект к классу или подклассу по имени
SaltwaterFish nemo = new SaltwaterFish(); if(nemo instanceof Fish) < // рыбка Немо относится к классу Fish // это может быть класс Fish (родительский класс) или подкласс типа // SaltwaterFish или FreshwaterFish. if(nemo instanceof SaltwaterFish) < // Немо - это морская рыбка! >>
Данная проверка удобна во многих случаях. Иногда приходится проверять принадлежность класса при помощи instanceof, чтобы можно было бы разделить логику кода:
void checkforTextView(View v) < if(v instanceof TextView) < // Код для элемента TextView >else < // Для других элементов View >>
Импорт класса import
Оператор import сообщает компилятору Java, где найти классы, на которые ссылается код. Любой сложный объект использует другие объекты для выполнения тех или иных функций, и оператор импорта позволяет сообщить о них компилятору Java. Оператор импорта обычно выглядит так:
import packane_name.Class_Name_To_Import;
За ключевым словом следуют класс, который нужно импортировать. Имя класса должно быть полным, то есть включать свой пакет. Чтобы импортировать все классы из пакета, после имени пакета можно поместить ‘.*;’
IDE Eclipse упрощает импорт. При написании кода в редакторе Eclipse можно ввести имя класса, а затем нажать Ctrl+Shift+O. Eclipse определяет, какие классы нужно импортировать, и добавляет их автоматически. Если Eclipse находит два класса с одним и тем же именем, он выводит диалоговое окно с запросом, какой именно класс вы хотите добавить.
Статический импорт
Существует ещё статический импорт, применяемый для импорта статических членов класса или интерфейса. Например, есть статические методы Math.pow(), Math.sqrt(). Для вычислений сложных формул с использованием математических методов, код становится перегружен. К примеру, вычислим гипотенузу.
hypot = Math.sqrt(Math.pow(side1, 2) + Math.pow(side2, 2));
В данном случае без указания класса не обойтись, так как методы статические. Чтобы не набирать имена классов, их можно импортировать следующим образом:
import static java.lang.Math.sqrt; import static java.lang.Math.pow; . hypot = sqrt(pow(side1, 2) + pow(side2, 2));
После импорта уже нет необходимости указывать имя класса.
Второй допустимый вариант, позволяющий сделать видимыми все статические методы класса:
import static java.lang.Math.*;
В этом случае не нужно импортировать отдельные методы. Но данный подход в Android не рекомендуется, так как требует больше памяти.
Класс: базовый синтаксис
В объектно-ориентированном программировании класс – это расширяемый шаблон кода для создания объектов, который устанавливает в них начальные значения (свойства) и реализацию поведения (методы).
На практике нам часто надо создавать много объектов одного вида, например пользователей, товары или что-то ещё.
Как мы уже знаем из главы Конструктор, оператор «new», с этим может помочь new function .
Но в современном JavaScript есть и более продвинутая конструкция «class», которая предоставляет новые возможности, полезные для объектно-ориентированного программирования.
Синтаксис «class»
Базовый синтаксис выглядит так:
class MyClass < // методы класса constructor() < . >method1() < . >method2() < . >method3() < . >. >
Затем используйте вызов new MyClass() для создания нового объекта со всеми перечисленными методами.
При этом автоматически вызывается метод constructor() , в нём мы можем инициализировать объект.
class User < constructor(name) < this.name = name; >sayHi() < alert(this.name); >> // Использование: let user = new User("Иван"); user.sayHi();
Когда вызывается new User(«Иван») :
- Создаётся новый объект.
- constructor запускается с заданным аргументом и сохраняет его в this.name .
…Затем можно вызывать на объекте методы, такие как user.sayHi() .
Методы в классе не разделяются запятой
Частая ошибка начинающих разработчиков – ставить запятую между методами класса, что приводит к синтаксической ошибке.
Синтаксис классов отличается от литералов объектов, не путайте их. Внутри классов запятые не требуются.
Что такое класс?
Итак, что же такое class ? Это не полностью новая языковая сущность, как может показаться на первый взгляд.
Давайте развеем всю магию и посмотрим, что такое класс на самом деле. Это поможет в понимании многих сложных аспектов.
В JavaScript класс – это разновидность функции.
class User < constructor(name) < this.name = name; >sayHi() < alert(this.name); >> // доказательство: User - это функция alert(typeof User); // function
Вот что на самом деле делает конструкция class User <. >:
- Создаёт функцию с именем User , которая становится результатом объявления класса. Код функции берётся из метода constructor (она будет пустой, если такого метода нет).
- Сохраняет все методы, такие как sayHi , в User.prototype .
При вызове метода объекта new User он будет взят из прототипа, как описано в главе F.prototype. Таким образом, объекты new User имеют доступ к методам класса.
На картинке показан результат объявления class User :
Можно проверить вышесказанное и при помощи кода:
class User < constructor(name) < this.name = name; >sayHi() < alert(this.name); >> // класс - это функция alert(typeof User); // function // . или, если точнее, это метод constructor alert(User === User.prototype.constructor); // true // Методы находятся в User.prototype, например: alert(User.prototype.sayHi); // sayHi() < alert(this.name); >// в прототипе ровно 2 метода alert(Object.getOwnPropertyNames(User.prototype)); // constructor, sayHi
Не просто синтаксический сахар
Иногда говорят, что class – это просто «синтаксический сахар» в JavaScript (синтаксис для улучшения читаемости кода, но не делающий ничего принципиально нового), потому что мы можем сделать всё то же самое без конструкции class :
// перепишем класс User на чистых функциях // 1. Создаём функцию constructor function User(name) < this.name = name; >// каждый прототип функции имеет свойство constructor по умолчанию, // поэтому нам нет необходимости его создавать // 2. Добавляем метод в прототип User.prototype.sayHi = function() < alert(this.name); >; // Использование: let user = new User("Иван"); user.sayHi();
Результат этого кода очень похож. Поэтому, действительно, есть причины, по которым class можно считать синтаксическим сахаром для определения конструктора вместе с методами прототипа.
Однако есть важные отличия:
-
Во-первых, функция, созданная с помощью class , помечена специальным внутренним свойством [[IsClassConstructor]]: true . Поэтому это не совсем то же самое, что создавать её вручную. В отличие от обычных функций, конструктор класса не может быть вызван без new :
class User < constructor() <>> alert(typeof User); // function User(); // Error: Class constructor User cannot be invoked without 'new'
Кроме того, строковое представление конструктора класса в большинстве движков JavaScript начинается с «class …»
class User < constructor() <>> alert(User); // class User
Также в дополнение к основной, описанной выше, функциональности, синтаксис class даёт ряд других интересных возможностей, с которыми мы познакомимся чуть позже.
Class Expression
Как и функции, классы можно определять внутри другого выражения, передавать, возвращать, присваивать и т.д.
Пример Class Expression (по аналогии с Function Expression):
let User = class < sayHi() < alert("Привет"); >>;
Аналогично Named Function Expression, Class Expression может иметь имя.
Если у Class Expression есть имя, то оно видно только внутри класса:
// "Named Class Expression" // (в спецификации нет такого термина, но происходящее похоже на Named Function Expression) let User = class MyClass < sayHi() < alert(MyClass); // имя MyClass видно только внутри класса >>; new User().sayHi(); // работает, выводит определение MyClass alert(MyClass); // ошибка, имя MyClass не видно за пределами класса
Мы даже можем динамически создавать классы «по запросу»:
function makeClass(phrase) < // объявляем класс и возвращаем его return class < sayHi() < alert(phrase); >; >; > // Создаём новый класс let User = makeClass("Привет"); new User().sayHi(); // Привет
Геттеры/сеттеры, другие сокращения
Как и в литеральных объектах, в классах можно объявлять вычисляемые свойства, геттеры/сеттеры и т.д.
Вот пример user.name , реализованного с использованием get/set :
class User < constructor(name) < // вызывает сеттер this.name = name; >get name() < return this._name; >set name(value) < if (value.length < 4) < alert("Имя слишком короткое."); return; >this._name = value; > > let user = new User("Иван"); alert(user.name); // Иван user = new User(""); // Имя слишком короткое.
При объявлении класса геттеры/сеттеры создаются на User.prototype , вот так:
Object.defineProperties(User.prototype, < name: < get() < return this._name >, set(name) < // . >> >);
Пример с вычисляемым свойством в скобках [. ] :
Как определить метод класса
Эта статья даст базовое понимание терминов «класс», «метод», «наследование», «перегрузка метода»
Методы
Методы — это функции, объявление которых размещено внутри определения класса или структуры. В список переменных, доступных для метода, неявно попадают все поля структуры или класса, в котором он объявлен. Другими словами, в список областей видимости метода попадает область видимости структуры.
Взгляните на пример:
#include struct Vec2f float x = 0; float y = 0; // Объявление метода с именем getLength // 1) метод - это функция, привязанная к объекту // 2) полное имя метода: "Vec2f::getLength" // Метод имеет квалификатор "const", потому что он не меняет // значения полей и не вызывает другие не-const методы. float getLength() const const float lengthSquare = x * x + y * y; return std::sqrt(lengthSquare); > >;
Методу Vec2f::getLength доступны все символы (т.е. переменные, функции, типы данных), которые были объявлены в одной из трёх областей видимости. При наличии символов с одинаковыми идентификаторами один символ перекрывает другой, т.к. поиск происходит от внутренней области видимости к внешней.
Понять идею проще на схеме. В ней область видимости названа по-английски: scope.
Поднимаясь по схеме от внутренней области видимости к внешней, легко понять, какие имена символов доступны в методе getLength:
- локальная переменная “lengthSquare”
- поля Vec2f под именами “x” и “y”
- всё, что есть в глобальной области видимости
К слову, в других методах структуры Vec2f переменная “lengthSquare” будет недоступна, а поля “x” и “y” будут доступны.
Конструкторы
Конструктор — это специальный метод, который вызывается автоматически при выполнении инструкции объявления переменной. При этом память под переменную уже выделена заранее, т.к. память под все локальные переменные выделяется на стеке программы в момент вызова функции. Конструктор позволяет выполнить сложный код для инициализации переменной.
Посмотрите на простой пример. В нём есть проблема: и поля, и параметры конструктора названы одинаково. В результате в области видимости конструктора доступны только параметры, и своими именами они перекрывают поля!
struct Vec2f float x = 0; float y = 0; // Имя метода-конструктора совпадает с именем типа, возвращаемый тип отсутствует Vec2f(float x, float y) // поля x, y перекрыты, что делать? > >;
Язык C++ предлагает два решения. Первый способ — использовать косвенное обращение к полям через привязанный к методу объект. Указатель на него доступен по ключевому слову this :
struct Vec2f float x = 0; float y = 0; Vec2f(float x, float y) // Обращаемся к полю через указатель this this->x = x; this->y = y; > >;
Второй путь считается более правильным: мы используем специальную возможность конструкторов — “списки инициализации конструктора” (англ. constructor initializer lists). Списки инициализации — это список, разделённый запятыми и начинающийся с “:”. Элемент списка инициализации выглядит как field(expression) , т.е. для каждого выбранного программистом поля можно указать выражение, инициализирующее его. Имя переменной является выражением. Поэтому мы инициализируем поле его параметром:
struct Vec2f float x = 0; float y = 0; Vec2f(float x, float y) : x(x) // Перекрытия имён нет, т.к. согласно синтаксису C++ , y(y) // перед скобками может стоять только имя поля. > >;
Объявление и определение методов
C++ требует, чтобы каждый метод структуры или класса был упомянут в определении этой структуры или класса. Но допускается писать лишь объявление метода, о определение размещать где-нибудь в другом месте:
// Определение структуры Vec2f содержит // - объявление конструктора // - объявление метода getLength struct Vec2f float x = 0; float y = 0; Vec2f(float x, float y); float getLength() const; >; // Определение конструктора (добавлен квалификатор "Vec2f::") Vec2f::Vec2f(float x, float y) : x(x) , y(y) > // Определение метода getLength (добавлен квалификатор "Vec2f::") float Vec2f::getLength() const const float lengthSquare = x * x + y * y; return std::sqrt(lengthSquare); >
Классы и структуры
В C++ есть ключевое слово class — это практически аналог ключевого слова struct . Оба ключевых слова объявляют тип данных, и разница между ними есть только на стыке наследования и инкапсуляции. Других различий class и struct не существует.
Основы инкапсуляции
В C++ можно блокировать доступ к полям извне, но сохранять доступ для методов. Для этого введены три области доступа
- public — символ в этой области доступен извне
- private — символ из этой области доступен лишь собственных в методах
- protected — используется редко, о нём можете прочитать в документации
Давайте сделаем поля типа Vec2f недоступными извне. Также мы заменим ключевое слово struct на class — это не меняет смысла программы, но считается хорошим тоном использовать struct только если все поля доступны публично.
class Vec2f public: // начало списка публичных методов и полей Vec2f(float x, float y) : x(x) , y(y) > float getLength() const // Здесь поля x/y доступны, т.к. это внутренний метод const float lengthSquare = x * x + y * y; return std::sqrt(lengthSquare); > private: // начало списка недоступных извне методов и полей float x = 0; float y = 0; >; // ! ОШИБКА КОМПИЛЯЦИИ ! // Поля x/y недоступны для внешней функции void printVector(const Vec2f& v) std::cout <"[" <v.x <"," <v.y <"]"; >
Запомните несколько хороших правил:
- Используйте struct, если все поля публичные и не зависят друг от друга; используйте class, если между полями должны соблюдаться закономерности (например, поле “площадь” круга должно быть)
Основы наследования
В C++ новый тип может наследовать все поля и методы другого типа. Для этого достаточно указать структуру или класс в списке базовых типов. Такой приём используется в SFML при объявлении классов фигур:
// ! КОД НАМЕРЕННО УПРОЩЁН! // Класс RectangleShape наследует все поля и методы Shape, // но также имеет дополнительные поля и методы. // Финальный список полей и методов составляет компилятор при сборке программы. // Смысл наследования: "прямоугольник является фигурой". class RectangleShape : public Shape public: // Конструктор, принимающий один необязательный аргумент RectangleShape(const Vector2f& size = Vector2f(0, 0)); // Метод с одним аргументом void setSize(const Vector2f& size); // . >; // Класс Shape наследует все поля и методы двух классов: Drawable и Transformable // Смысл наследования: "фигура является сущностью, которую можно нарисовать // и у которой можно задать позицию/масштаб/вращение" class Shape : public Drawable, public Transformable public: // Виртуальный деструктор Shape. // Деструктор вызывается // 1) для локальной переменной - при выходе из области видимости локальной переменной // 2) для параметра, переданного по значению - при выходе из функции // 3) для временных объектов в выражении - при завершении инструкции, в которой находится выражение // 4) для объектов в динамической памяти - при освобождении памяти (например, через delete) // Деструкторы простых типов int, sf::Vector2f и т.д. не делают ничего. // Деструкторы сложных типов освобождают ресурсы (например, удаляют текстуру или вспомогательныю память) virtual ~Shape(); // Метод setTexture, принимает 2 параметра, из которых 1 - необязательный. void setTexture(const Texture* texture, bool resetRect = false); // . >;
Что означает public перед именем базового типа? Во-первых внешний код может передать RectangleShape в функцию, принимающую ссылку на Shape, то есть возможен так называемы upcast от более низкого (и более конкретного) типа RectangleShape к более высокому (и более абстрактному) типу Shape:
// Хотя параметр имеет тип Shape, мы можем передать тип RectangleShape, // потому что RectangleShape является Shape. // Аналогично мы можем передать CircleShape. void drawShape(sf::RenderWindow& window, const Shape& shape) window.draw(shape); > // Параметр имеет тип RectangleShape, и передать Shape или CircleShape нельзя, // потому что ни Shape, ни CircleShape не являются RectangleShape. void drawRect(sf::RenderWindow& window, const RectangleShape& shape) window.draw(shape); >
Во-вторых из-за public наследования все унаследованные поля и методы сохраняют свой уровень доступ: приватные остаются приватными, публичные остаются публичными. А если бы мы наследовали Shape с ключевым словом private, то уровень доступа стал бы ниже: все методы и поля стали бы приватными:
// ! КОД НАМЕРЕННО УПРОЩЁН! // Все поля и методы Shape, даже публичные, стали приватными в RectangleShape class RectangleShape : private Shape public: RectangleShape(const Vector2f& size = Vector2f(0, 0)); void setSize(const Vector2f& size); >;
Контроль уровня доступа полей и методов — хитрый механизм, пройдёт немало времени, прежде чем вы научитесь пользоваться им правильно. В начале просто старайтесь сделать правильный выбор между private и public. Скорее всего поля будут private, а конструктор и все методы будут public. Это позволяет сохранять инвариант класса, то есть держать поля объекта в согласованном состоянии независимо от того, какие методы вызывают извне.
Основы полиморфизма: виртуальные методы и их перегрузка
SFML использует ещё одну идиому C++: виртуальные методы. Ключевые слова virtual , final , override относятся именно к этой идиоме. Например, в SFML определяется класс Drawable, который обозначает “сущность, которую можно нарисовать”. Все рисуемые классы SFML, включая sf::Sprite , sf::RectangleShape , sf::Text , прямо или косвенно наследуются от sf::Drawable .
// ! КОД НАМЕРЕННО УПРОЩЁН! class Drawable public: // Виртуальный деструктор. virtual ~Drawable() <> // Виртуальный метод draw virtual void draw(RenderTarget& target, RenderStates states) const; >;
Зачем это надо? Дело в том, что метод draw класса RenderWindow принимает параметр типа Drawable . Тем не менее, этот метод успешно рисует любые типы объектов: спрайты, фигуры, тексты. Он не выполняет проверок — он просто настраивает состояние рисования (RenderStates) и вызывает метод draw у сущности, которая является Drawable .
void RenderWindow::draw(const Drawable& drawable) RenderStates states = . ; // как-то настраиваем состояние. drawable.draw(*this, states); >
Виртуальный метод вызывается косвенно: если класс 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 у объекта окна.
#pragma once #include // Класс RussianFlag рисует флаг России. // Наследуем его от sf::Drawable, чтобы можно было рисовать флаг проще: // window.draw(flag); class RussianFlag : public sf::Drawable public: // Конструктор принимает два параметра: положение и размер флага. RussianFlag(const sf::Vector2f& position, const sf::Vector2f& size); private: // Метод draw вызывается окном при рисовании флага, // то есть window.draw(flag) косвенно привозит к вызову этого метода. void draw(sf::RenderTarget& target, sf::RenderStates states) const override; sf::RectangleShape m_whiteStrip; sf::RectangleShape m_blueStrip; sf::RectangleShape m_redStrip; >;
Теперь мы можем реализовать конструктор и метод draw. В конструкторе мы должны вычислить и установить позиции и размеры трёх полос на флаге, а в методе draw мы должны их последовательно нарисовать.
#include "RussianFlag.h" RussianFlag::RussianFlag(const sf::Vector2f& position, const sf::Vector2f& size) const sf::Vector2f stripSize = size.x, size.y / 3.f >; m_whiteStrip.setSize(stripSize); m_whiteStrip.setPosition(position); m_whiteStrip.setFillColor(sf::Color(0xFF, 0xFF, 0xFF)); m_blueStrip.setSize(stripSize); m_blueStrip.setPosition(position + sf::Vector2f 0.f, stripSize.y >); m_blueStrip.setFillColor(sf::Color(0, 0, 0xFF)); m_redStrip.setSize(stripSize); m_redStrip.setPosition(position + sf::Vector2f 0.f, 2.f * stripSize.y >); m_redStrip.setFillColor(sf::Color(0xFF, 0, 0)); > void RussianFlag::draw(sf::RenderTarget& target, sf::RenderStates states) const target.draw(m_whiteStrip, states); target.draw(m_blueStrip, states); target.draw(m_redStrip, states); >
Теперь использовать класс RussianFlag извне очень легко!
#include "RussianFlag.h" #include #include // Функция создаёт окно определённого размера с определённым заголовком. void initWindow(sf::RenderWindow& window) sf::VideoMode videoMode(800, 600); const std::string title = "Russian Flag + class derived from sf::Drawable"; sf::ContextSettings settings; settings.antialiasingLevel = 8; window.create(videoMode, title, sf::Style::Default, settings); > int main() sf::RenderWindow window; initWindow(window); RussianFlag flag(100, 50>, 300, 150>); while (window.isOpen()) sf::Event event; while (window.pollEvent(event)) if (event.type == sf::Event::Closed) window.close(); > > // Рисуем flag как обычную фигуру или спрайт: вызовом window.draw. window.clear(); window.draw(flag); window.display(); > >
PS-Group
- PS-Group
- sshambir@gmail.com
- ps-group
- image/svg+xml sshambir
С++ для начинающих Определение методов класса вне класса
В двух предыдущих статьях (С++ для начинающих Классы Первое знакомство и С++ для начинающих Конструктор Класса) функции и конструктор определялись внутри класса. Другими словами- “методы класса были расписаны внутри класса”. Такой подход не всегда удобен, поэтому в C++ существует альтернативный вариант. В альтернативном варианте вовнутрь класса размещается прототип функции, а вне класса пишется сама функция. Чтобы было понятнее, пишу пример.
Код C++. Определение функции (метода) вне класса
class MyClass
int a ; //Переменная а доступна только внутри класса MyClass
public:
MyClass (); //Прототип конструктора
void show_a (); //Прототип функции show_a()
> obj1 ; //obj1 – Переменная типа MyClass. Здесь описание класса закончено, обязательны точка с запятой
//++++++++++++++++++++++++++++++++++++++++++
//Для описания методов вне класса используют оператор глобального разрешения
MyClass :: MyClass () //Класс::Конструктор Конструктор не возвращает никаких значений
cout //В конструкторе выполняется ввод значения в а с клавиатуры
cin >> a ;
>
void MyClass :: show_a () //Тип функции Класс ::Функция. Описываем функцию show_a()
cout //Функция выводит на экран значение a из класса MyClass
>
//Описание методов вне класса закончено
void main ()
clrscr (); //Очистка экрана для очистки при первом запуске
cout
getch (); //Ожидание нажатия Enter
clrscr (); //Так как объекты конструируются до входа в функцию main, делаем генеральную уборку с экрана непосредственно перед самым выходом
return;
>
=========================
Теперь пора пояснить для тех кому очень нужно. Эта программа аналогична такой:
cin>>a;
cout
Только выполнена через класс с использованием конструктора. Так сделано потому что в моем представлении этот примитив вполне может дать понять принцип построения простых классов. Внутри класса описываются детали класса и прототипы методов. Были прописаны прототипы двух методов (Конструктор и функция show_a). Дальше эти методы были прописаны вне класса. Между плюсовыми комментариями как раз идет написание этих обоих методов. Разные классы могут использовать методы с одинаковыми именами, из-за этого в С++ при определении метода класса вне класса используют оператор глобального разрешения (::)
Сначала был прописан конструктор. Конструктор есть особый метод и всегда имеет тоже имя, что у своего класса. Конструктор никогда не возвращает значений, даже void недопустим, поэтому в начале строчки Class::MyClass() отсутствует тип. При этом конструктор может принимать параметры (в скобки можно записывать).
После описания конструктора была описана функция show_a(); Так как функция всегда возвращает значение, то вначале обязательно указывать возвращаемый тип. В примере этот тип void ( void show_a(); )
Сама конструкция определения методов вне классов
class ИмяКласса
ТипФункции Функция(ТипыВозвращаемыхПараметров); //Прототип метода
>;
Тип ИмяКласса :: МетодКласса(Параметры) //Вначале тип метода класса
<
основной код метода
>
Так как класс сам по себе бесполезен, для него был создан объект. Объект представляет из себя переменную – экземпляр класса. В примере это переменная obj1. В функции main через объект obj1 запускается конструктор класса MyClass, внутри конструктора прописано предложение ввести значение и это значение присваивается в a из класса MyClass. После того как объект был собран, выполняется сама функция main. Очищается экран, выполняется метод класса show_a. В этом методе прописано только одно действие – вывод a на экран. Потом происходит ожидание нажатия клавиши Enter, очистка экрана и выход из программы