Как определить метод класса
Перейти к содержимому

Как определить метод класса

  • автор:

Определение класса в языке C++ и создание его объекта

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

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

class имя_класса { список_компонентов };

имя_класса — произвольно выбираемый идентификатор

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

Заключенный в фигурные скобки список компонентов называют телом класса.
Телу класса предшествует заголовок. В простейшем случае заголовок класса включает слово class и имя.
Определение класса всегда заканчивается точкой с запятой.

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

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

имя_класса имя_объекта;

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

Способы доступа к компонентам класса.

Существует несколько уровней доступа к компонентам класса. Рассмотрим основные:

public — члены класса открыты для доступа извне.
private — члены класса закрыты для доступа извне.

По умолчанию все переменные и функции, принадлежащие классу, определены как закрытые (private). Это означает, что они могут использоваться только внутри функций-членов самого класса. Для других частей программы, таких как функция main(), доступ к закрытым членам запрещен. Это, кстати, единственное отличие класса от структуры — в структуре все члены по умолчанию — public.

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

class имя_класса { закрытые переменный и функции; защищенные члены данных; защищенные конструкторы; защищенные методы; public: открытые переменные и функции; общедоступные свойства; общедоступные члены данных; общедоступные конструкторы; общедоступный деструктор; общедоступные методы; } список имен объектов;

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

имя_объекта.имя_члена класса;

Пришло время примера…

# include using namespace std; class Test{ // так как спецификатор доступа не указан // данная переменная будет по умолчанию закрыта // для доступа вне класса (private) int one; // спецификатор доступа public // все члены, идущие после него // будут открыты для доступа извне public: // инициализировать переменные в классе // при создании запрещено, поэтому мы определяем // метод, реализующий данное действие void Initial(int o,int t){ one=o; two=t; } // метод показывающий переменные класса // на экран void Show(){ cout"\n\n"one"\t"two"\n\n"; } int two; }; void main(){ // создается объект с типом Test Test obj; // вызывается функция, инициализирующая его свойства obj.Initial(2,5); // показ на экран obj.Show(); // 2 5 // прямая запись в открытую переменную two // с переменной one такая запись невозможна, так // как доступ к ней закрыт obj.two=45; // снова показ на экран obj.Show(); // 2 45 }

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

Оператор .

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

Оператор ->

Указатели на объекты. Доступ к членам объекта можно осуществлять и через указатель на объект. В этом случае применяется операция стрелка (→).

Оператор ::

Оператор :: называется оператором разрешения области видимости (scope resolution operator). Т.е. создается шаблон класса и в нем шаблоны методов класса, а сами методы описываются вне описания класса.

class stack { int stck[SIZE]; int tos; public: void init(); void push(int i); int pop(); }; void stack::init() { tos = 0; } void stack::push(int i) { if(tos==SIZE) { cout  "Stack is full.\n"; return; } stck[tos] = i; tos++; }

Константный метод объекта

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

Язык С++ позволяет пометить метод как константный. При этом неконстантные методы объекта запрещается использовать в теле помеченного метода, и в контексте этого метода ссылки на сам объект и все его поля будут константны. Для обозначения константности, используется модификатор const.

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

Давайте, рассмотрим пример класса с константными методами:

# include # include using namespace std; class Personal { public: // конструктор с параметрами // мы выделяем здесь память // однако в нашем примере нет // ни деструктора, ни конструктора // копирования - единственная цель, // которую мы преследуем показать // работу константного метода Personal(char*p,char*n,int a){ name=new char[strlen(n)+1]; if(!name){ cout"Error. "; exit(0); } picture_data=new char[strlen(n)+1]; if(!picture_data){ cout"Error. "; exit(0); } strcpy(picture_data,p); strcpy(name,n); age=a; } // Группа константных методов // внутри них невозможно // изменить какое-то из свойств const char*Name()const{ return name; } int Age()const{ return age; } const char*Picture()const{ return picture_data; } void SetName(const char*n){ strcpy(name,n); } void SetAge(int a){ age=a; } void SetPicture(const char*p){ strcpy(picture_data,p); } private: char*picture_data; // путь к фотографии char*name; // имя int age; // возраст }; void main(){ Personal A("C:\\Image\\","Ivan",23); cout"Name: "A.Name()"\n\n"; cout"Age: "A.Age()"\n\n"; cout"Path for picture: "A.Picture()"\n\n"; A.SetPicture("C:\\Test\\"); A.SetName("Leonid"); A.SetAge(90); cout"Name: "A.Name()"\n\n"; cout"Age: "A.Age()"\n\n"; cout"Path for picture: "A.Picture()"\n\n"; }

В данном примере методы Name, Age, Picture объявлены константными. Кроме того, можно наблюдать и использование константных указателей: параметр методов SetName и SetPicture, возвращаемое значение методов Name и Picture. Компилятор обеспечит проверку того, что реализация константных методов не имеет побочных эффектов в виде изменения состояния объекта, реализующего класс Personal. Как только обнаружится попытка выполнить запрещенную операцию, компилятор сообщит об ошибке.

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

Есть базовый класс «publication», производный от него «book» и производной от book — «type». Когда определяю методы внутри класса ошибок нет, но если вывожу их за класс то компилятор ругается. В чём проблема ?

1 2 3 4 5 6 7 8 9
#include #include "publication.h" using namespace std; int main() { return 0; }

publication.h

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
#pragma once #include #include using namespace std; class publication { private: string name; mutable float price; public: publication() :name("none"), price(0) { } publication(string name, float price) :name(name), price(price) { } inline void change()const; inline void put()const; void get(); }; inline void publication::change()const { cout  "New price: "; cin >> price; } inline void publication::put()const  cout  "Name: "  name  " void publication::get() { cout  "Name: "; cin >> name; cout  "Price: "; cin >> price; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
#pragma once #include #include "publication.h" using namespace std; class book : private publication { private: int size; public: book() : publication(), size(0) { } book(string name, float price, int size) : publication(name, price), size(size) { } void change(); void get(); inline void put()const; }; void book::change() { publication::change(); } void book::get() { publication::get(); cout  "Size: "; cin >> size; } inline void book::put()const { publication::put(); cout  "Size: "  size  endl; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
#pragma once #include #include "book.h" using namespace std; class type : private book { private: float time; public: type() : book(), time(0) { } type(string name, float price, int size, float time) : book(name, price, size), time(time) { } inline void put()const; void change(); void get(); }; inline void type::put()const { book::put(); cout  "Time: "  time; } void type::change() { book::change(); cout  "New time: "; cin >> time; } void type::get() { book::get(); cout  "Time: "; cin >> time; }

Как определить метод класса

В языке C++ можно разделять объявление и определение функций в том числе по отношению к функциям, которые создаются в классах. Для этого используется выражение имя_класса::имя_функции(параметры) < тело_функции>.

Например, возьмем следующий класс Person:

class Person < private: std::string name; unsigned age; public: Person(std::string p_name, unsigned p_age) < name = p_name; age = p_age; >void print() < std::cout >;

Разобъем класс, вынеся реализацию его методов во вне:

#include class Person < private: std::string name; unsigned age; public: Person(std::string p_name, unsigned p_age); void print(); >; // конструктор Person::Person(std::string p_name, unsigned p_age) < name = p_name; age = p_age; >void Person::print() < std::cout int main() < Person tom; tom.print(); // Name: Tom Age: 22 >

Теперь функции класса Person (в данном случае конструктор и функция print) в самом классе имеют только объявления. Реализации функций размещены вне класса Person.

Консольный вывод программы:

Name: Tom Age: 38

Подобное разделение упрощает обзор и понимание интрефейса класса, особенно когда функции имеют много кода.

Три такой организации кода также можно делегировать конструктор:

#include class Person < private: std::string name; unsigned age; public: Person(std::string p_name, unsigned p_age); Person(std::string p_name); void print(); >; // конструктор Person::Person(std::string p_name, unsigned p_age) < name = p_name; age = p_age; >// делегирование конструктора Person::Person(std::string p_name) : Person(p_name, 18) < >void Person::print() < std::cout int main() < Person tom; tom.print(); // Name: Tom Age: 22 Person bob; bob.print(); // Name: Bob Age: 18 >

Как определить метод класса

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

Методы

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

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

#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:

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

К слову, в других методах структуры 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++ можно блокировать доступ к полям извне, но сохранять доступ для методов. Для этого введены три области доступа

  1. public — символ в этой области доступен извне
  2. private — символ из этой области доступен лишь собственных в методах
  3. 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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *