Можно ли в методах использовать классы
Содержание статьи
Урок №113. Классы, Объекты и Методы
Обновл. 28 Сен 2020 |
Хотя язык C++ предоставляет ряд фундаментальных типов данных (например, char, int, long, float, double и т.д.), которых бывает достаточно для решения относительно простых проблем, для решения сложных проблем функционала этих простых типов может не хватать.
Классы
Одной из наиболее полезных особенностей языка C++ является возможность определять собственные типы данных, которые будут лучше соответствовать в решении конкретных проблем. Вы уже видели, как перечисления и структуры могут использоваться для создания собственных пользовательских типов данных. Например, структура для хранения даты:
struct Struct { int day; int month; int year; }; |
Перечисления и структуры — это традиционный (не объектно-ориентированный) мир программирования, в котором мы можем только хранить данные. В C++11 мы можем создать и инициализировать структуру следующим образом:
Struct today { 12, 11, 2018}; // используем uniform-инициализацию |
Для вывода даты на экран (что может понадобиться выполнить и не раз, и не два) хорошей идеей будет написать отдельную функцию, например:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #include <iostream> struct Struct { int day; int month; int year; }; void (Struct &) { std::cout << .day<< «/» << .month << «/» << .year; } int main() { Struct today { 12, 11, 2018}; // используем uniform-инициализацию today.day = 18; // используем оператор выбора члена для выбора члена структуры (today); return 0; } |
Результат выполнения программы:
18/11/2018
В объектно-ориентированном программировании типы данных могут содержать не только данные, но и функции, которые будут работать с этими данными. Для определения такого типа данных в языке C++ используется ключевое слово class. Использование ключевого слова class определяет новый пользовательский тип данных — класс.
В языке C++ классы очень похожи на структуры, за исключением того, что они обеспечивают гораздо большую мощность и гибкость. Фактически, следующая структура и класс идентичны по функционалу:
struct Struct { int day; int month; int year; }; class Class { public: int m_day; int m_month; int m_year; }; |
Единственным существенным отличием здесь является public — ключевое слово в классе (о нем мы поговорим детально на соответствующем уроке).
Так же, как и объявление структуры, объявление класса не приводит к выделению какой-либо памяти. Для использования класса нужно объявить переменную этого типа класса:
Class today { 12, 11, 2018 }; // инициализируем переменную класса Class |
В языке C++ переменная класса называется экземпляром (или «объектом») класса. Точно так же, как определение переменной фундаментального типа данных (например, int x) приводит к выделению памяти для этой переменной, так же и создание объекта класса (например, Class today) приводит к выделению памяти для этого объекта.
Методы классов
Помимо хранения данных, классы могут содержать и функции! Функции, определенные внутри класса, называются методами. Методы могут быть определены, как внутри, так и вне класса. Пока что мы будем определять их внутри класса (для простоты), как определить их вне класса — рассмотрим несколько позже.
Класс с методом вывода даты:
class Class { public: int m_day; int m_month; int m_year; void () // определяем функцию-член { std::cout << m_day << «/» << m_month << «/» << m_year; } }; |
Точно так же, как к членам структуры, так и к членам (переменным и функциям) класса доступ осуществляется через оператор выбора членов (.):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <iostream> class Class { public: int m_day; int m_month; int m_year; void () { std::cout << m_day << «/» << m_month << «/» << m_year; } }; int main() { Class today { 12, 11, 2018 }; today.m_day = 18; // используем оператор выбора членов для выбора переменной-члена m_day объекта today класса Class today.(); // используем оператор выбора членов для вызова метода () объекта today класса Class return 0; } |
Результат выполнения программы:
18/11/2018
Обратите внимание, как эта программа похожа на вышеприведенную программу, где используется структура. Однако есть несколько отличий. В версии Struct нам нужно было передать переменную структуры непосредственно в функцию () в качестве параметра. Если бы мы этого не сделали, то функция () не знала бы, какую переменную структуры Struct выводить. Нам тогда бы пришлось явно ссылаться на члены структуры внутри функции.
Методы класса работают несколько иначе: все вызовы методов должны быть связаны с объектом класса. Когда мы вызываем today.(), то мы сообщаем компилятору вызвать метод () объекта today.
Рассмотрим определение метода () еще раз:
void () // определяем метод { std::cout << m_day << «/» << m_month << «/» << m_year; } |
На что фактически ссылаются m_day, m_month и m_year? Они ссылаются на связанный объект today (который определен caller-ом).
Поэтому, при вызове today.(), компилятор интерпретирует:
m_day, как today.m_day;
m_month, как today.m_month;
m_year, как today.m_year.
Если бы мы вызвали tomorrow.(), то m_day ссылался бы на tomorrow.m_day.
По сути, связанный объект неявно передается методу. По этой причине его часто называют неявным объектом.
Детально о том, как передается неявный объект методу, мы поговорим на соответствующем уроке. Ключевым моментом здесь является то, что для работы с функциями, не являющимися членами класса, нам нужно передавать данные в эту функцию явно (в качестве параметров). А для работы с методами у нас всегда есть неявный объект класса!
Использование префикса m_ (англ. «m» = «members») для переменных-членов помогает различать переменные-члены от параметров функции или локальных переменных внутри методов класса. Это полезно по нескольким причинам:
во-первых, когда мы видим переменную с префиксом m_, то мы понимаем, что работаем с переменной-членом класса;
во-вторых, в отличие от параметров функции или локальных переменных, объявленных внутри функции, переменные-члены объявляются в определении класса. Следовательно, если мы хотим знать, как объявлена переменная с префиксом m_, мы понимаем, что искать нужно в определении класса, а не внутри функции.
Обычно программисты пишут имена классов с заглавной буквы.
Правило: Пишите имена классов с заглавной буквы.
Вот еще один пример программы с использованием класса:
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 | #include <iostream> #include <string> class Employee { public: std::string m_name; int m_id; double m_wage; // Метод вывода информации о работнике на экран void () { std::cout << «Name: » << m_name << «nId: » << m_id << «nWage: $» << m_wage << ‘n’; } }; int main() { // Определяем двух работников Employee john { «John», 5, 30.00 }; Employee max { «Max», 6, 32.75 }; // Выводим информацию о работниках на экран john.(); std::cout<<std::endl; max.(); return 0; } |
Результат выполнения программы:
Name: John
Id: 5
Wage: $30
Name: Max
Id: 6
Wage: $32.75
В отличие от обычных функций, порядок, в котором определены методы класса, не имеет значения!
Примечание о структурах в C++
В языке Cи структуры могут только хранить данные и не могут иметь связанных методов. После проектирования классов (используя ключевое слово class) в языке С++, Бьёрн Страуструп размышлял о том, нужно ли, чтобы структуры (которые были унаследованы из языка Си) имели связанные методы. После некоторых размышлений он решил, что нужно. Поэтому в программах, приведенных выше, мы также можем использовать ключевое слово struct, вместо class, и всё будет работать!
Многие разработчики (включая и меня) считают, что это было неправильное решение, поскольку оно может привести к проблемам, например, справедливо предположить, что класс выполнит очистку памяти после себя (например, класс, которому выделена память, освободит её непосредственно перед моментом уничтожения самого класса), но предполагать то же самое при работе со структурами — небезопасно. Следовательно, рекомендуется использовать ключевое слово struct для структур, используемых только для хранения данных, и ключевое слово class для определения объектов, которые требуют объединения как данных, так и функций.
Правило: Используйте ключевое слово struct для структур, используемых только для хранения данных. Используйте ключевое слово class для объектов, объединяющих как данные, так и функции.
Заключение
Оказывается, Стандартная библиотека C++ полна классов, созданных для нашего удобства. std::string, std::vector и std::array — это всё типы классов! Поэтому, когда вы создаете объект любого из этих типов, вы создаете объект класса. А когда вы вызываете функцию с использованием этих объектов, вы вызываете метод:
#include <string> #include <array> #include <vector> #include <iostream> int main() { std::string s { «Hello, world!» }; // создаем экземпляр класса string std::array<int, 3> a { 7, 8, 9 }; // создаем экземпляр класса array std::vector<double> v { 1.5, 2.6, 3.7 }; // создаем экземпляр класса vector std::cout << «length: » << s.length() << ‘n’; // вызываем метод return 0; } |
Ключевое слово class позволяет создать пользовательский тип данных в языке C++, который может содержать как переменные-члены, так и методы. Классы — это основа объектно-ориентированного программирования!
Тест
Задание №1
Создайте класс Numbers, который содержит два целых числа. Этот класс должен иметь две переменные-члены для хранения этих двух целых чисел. Вы также должны создать два метода:
метод set(), который позволит присваивать значения переменным;
метод (), который будет выводить значения переменных.
Выполнение следующей функции main():
int main() { Numbers n1; n1.set(3, 3); // инициализируем объект n1 значениями 3 и 3 Numbers n2{ 4, 4 }; // инициализируем объект n2 значениями 4 и 4 n1.(); n2.(); return 0; } |
Должно выдавать следующий результат:
Numbers(3, 3)
Numbers(4, 4)
Ответ №1
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 | #include <iostream> class Numbers { public: int m_first; int m_second; void set(int first, int second) { m_first = first; m_second = second; } void () { std::cout << «Numbers(» << m_first << «, » << m_second << «)n»; } }; int main() { Numbers n1; n1.set(3, 3); Numbers n2{ 4, 4 }; n1.(); n2.(); return 0; } |
Задание №2
Почему для Numbers должен использоваться класс, а не структура?
Ответ №2
Класс Numbers содержит как переменные-члены, так и методы, поэтому мы должны использовать класс. Мы не должны использовать структуры с объектами, которые имеют методы.
Оценить статью:
Загрузка…
Источник
Классы в C++ — урок 10
Весь реальный мир состоит из объектов. Города состоят из районов, в каждом районе есть свои названия улиц, на каждой улице находятся жилые дома, которые также состоят из объектов.
Практически любой материальный предмет можно представить в виде совокупности объектов, из которых он состоит. Допустим, что нам нужно написать программу для учета успеваемости студентов. Можно представить группу студентов, как класс языка C++. Назовем его Students.
class Students { // Имя студента std::string name; // Фамилия std::string last_name; // Пять промежуточных оценок студента int scores[5]; // Итоговая оценка за семестр float average_ball; };
Основные понятия
Классы в программировании состоят из свойств и методов. Свойства — это любые данные, которыми можно характеризовать объект класса. В нашем случае, объектом класса является студент, а его свойствами — имя, фамилия, оценки и средний балл.
У каждого студента есть имя — name и фамилия last_name . Также, у него есть промежуточные оценки за весь семестр. Эти оценки мы будем записывать в целочисленный массив из пяти элементов. После того, как все пять оценок будут проставлены, определим средний балл успеваемости студента за весь семестр — свойство average_ball.
Методы — это функции, которые могут выполнять какие-либо действия над данными (свойствами) класса. Добавим в наш класс функцию calculate_average_ball(), которая будет определять средний балл успеваемости ученика.
- Методы класса — это его функции.
- Свойства класса — его переменные.
class Students { public: // Функция, считающая средний балл void calculate_average_ball() { int sum = 0; // Сумма всех оценок for (int i = 0; i < 5; ++i) { sum += scores[i]; } // считаем среднее арифметическое average_ball = sum / 5.0; } // Имя студента std::string name; // Фамилия std::string last_name; // Пять промежуточных оценок студента int scores[5]; private: // Итоговая оценка за семестр float average_ball; };
Функция calculate_average_ball() просто делит сумму всех промежуточных оценок на их количество.
Модификаторы доступа public и private
Все свойства и методы классов имеют права доступа. По умолчанию, все содержимое класса является доступным для чтения и записи только для него самого. Для того, чтобы разрешить доступ к данным класса извне, используют модификатор доступа public. Все функции и переменные, которые находятся после модификатора public, становятся доступными из всех частей программы.
Закрытые данные класса размещаются после модификатора доступа private. Если отсутствует модификатор public, то все функции и переменные, по умолчанию являются закрытыми (как в первом примере).
Обычно, приватными делают все свойства класса, а публичными — его методы. Все действия с закрытыми свойствами класса реализуются через его методы. Рассмотрим следующий код.
class Students { public: // Установка среднего балла void set_average_ball(float ball) { average_ball = ball; } // Получение среднего балла float get_average_ball() { return average_ball; } std::string name; std::string last_name; int scores[5]; private: float average_ball; };
Мы не можем напрямую обращаться к закрытым данными класса. Работать с этими данными можно только посредством методов этого класса. В примере выше, мы используем функцию get_average_ball() для получения средней оценки студента, и set_average_ball() для выставления этой оценки.
Функция set_average_ball() принимает средний балл в качестве параметра и присваивает его значение закрытой переменной average_ball. Функция get_average_ball() просто возвращает значение этой переменной.
Программа учета успеваемости студентов
Создадим программу, которая будет заниматься учетом успеваемости студентов в группе. Создайте заголовочный файл students.h, в котором будет находиться класс Students.
/* students.h */ #include <string> class Students { public: // Установка имени студента void set_name(std::string student_name) { name = student_name; } // Получение имени студента std::string get_name() { return name; } // Установка фамилии студента void set_last_name(std::string student_last_name) { last_name = student_last_name; } // Получение фамилии студента std::string get_last_name() { return last_name; } // Установка промежуточных оценок void set_scores(int student_scores[]) { for (int i = 0; i < 5; ++i) { scores[i] = student_scores[i]; } } // Установка среднего балла void set_average_ball(float ball) { average_ball = ball; } // Получение среднего балла float get_average_ball() { return average_ball; } private: // Промежуточные оценки int scores[5]; // Средний балл float average_ball; // Имя std::string name; // Фамилия std::string last_name; };
Мы добавили в наш класс новые методы, а также сделали приватными все его свойства. Функция set_name() сохраняет имя студента в переменной name, а get_name() возвращает значение этой переменной. Принцип работы функций set_last_name() и get_last_name() аналогичен.
Функция set_scores() принимает массив с промежуточными оценками и сохраняет их в приватную переменную int scores[5].
Теперь создайте файл main.cpp со следующим содержимым.
/* main.cpp */ #include <iostream> #include «students.h» int main() { // Создание объекта класса Student Students student; std::string name; std::string last_name; // Ввод имени с клавиатуры std::cout << «Name: «; getline(std::cin, name); // Ввод фамилии std::cout << «Last name: «; getline(std::cin, last_name); // Сохранение имени и фамилии в объект класса Students student.set_name(name); student.set_last_name(last_name); // Оценки int scores[5]; // Сумма всех оценок int sum = 0; // Ввод промежуточных оценок for (int i = 0; i < 5; ++i) { std::cout << «Score » << i+1 << «: «; std::cin >> scores[i]; // суммирование sum += scores[i]; } // Сохраняем промежуточные оценки в объект класса Student student.set_scores(scores); // Считаем средний балл float average_ball = sum / 5.0; // Сохраняем средний балл в объект класса Students student.set_average_ball(average_ball); // Выводим данные по студенту std::cout << «Average ball for » << student.get_name() << » » << student.get_last_name() << » is » << student.get_average_ball() << std::endl; return 0; }
В самом начале программы создается объект класса Students. Дело в том, что сам класс является только описанием его объекта. Класс Students является описанием любого из студентов, у которого есть имя, фамилия и возможность получения оценок.
Объект класса Students характеризует конкретного студента. Если мы захотим выставить оценки всем ученикам в группе, то будем создавать новый объект для каждого из них. Использование классов очень хорошо подходит для описания объектов реального мира.
После создания объекта student, мы вводим с клавиатуры фамилию, имя и промежуточные оценки для конкретного ученика. Пускай это будет Вася Пупкин, у которого есть пять оценок за семестр — две тройки, две четверки и одна пятерка.
Введенные данные мы передаем set-функциям, которые присваивают их закрытым переменным класса. После того, как были введены промежуточные оценки, мы высчитываем средний балл на основе этих оценок, а затем сохраняем это значение в закрытом свойстве average_ball, с помощью функции set_average_ball().
Скомпилируйте и запустите программу.
Отделение данных от логики
Вынесем реализацию всех методов класса в отдельный файл students.cpp.
/* students.cpp */ #include <string> #include «students.h» // Установка имени студента void Students::set_name(std::string student_name) { Students::name = student_name; } // Получение имени студента std::string Students::get_name() { return Students::name; } // Установка фамилии студента void Students::set_last_name(std::string student_last_name) { Students::last_name = student_last_name; } // Получение фамилии студента std::string Students::get_last_name() { return Students::last_name; } // Установка промежуточных оценок void Students::set_scores(int scores[]) { for (int i = 0; i < 5; ++i) { Students::scores[i] = scores[i]; } } // Установка среднего балла void Students::set_average_ball(float ball) { Students::average_ball = ball; } // Получение среднего балла float Students::get_average_ball() { return Students::average_ball; }
А в заголовочном файле students.h оставим только прототипы этих методов.
/* students.h */ #pragma once /* Защита от двойного подключения заголовочного файла */ #include <string> class Students { public: // Установка имени студента void set_name(std::string); // Получение имени студента std::string get_name(); // Установка фамилии студента void set_last_name(std::string); // Получение фамилии студента std::string get_last_name(); // Установка промежуточных оценок void set_scores(int []); // Установка среднего балла void set_average_ball(float); // Получение среднего балла float get_average_ball(); private: // Промежуточные оценки int scores[5]; // Средний балл float average_ball; // Имя std::string name; // Фамилия std::string last_name; };
Такой подход называется абстракцией данных — одного из фундаментальных принципов объектно-ориентированного программирования. К примеру, если кто-то другой захочет использовать наш класс в своем коде, ему не обязательно знать, как именно высчитывается средний балл. Он просто будет использовать функцию calculate_average_ball() из второго примера, не вникая в алгоритм ее работы.
Над крупными проектами обычно работает несколько программистов. Каждый из них занимается написанием определенной части продукта. В таких масштабах кода, одному человеку практически нереально запомнить, как работает каждая из внутренних функций проекта. В нашей программе, мы используем оператор потокового вывода cout, не задумываясь о том, как он реализован на низком уровне. Кроме того, отделение данных от логики является хорошим тоном программирования.
В начале обучения мы говорили о пространствах имен (namespaces). Каждый класс в C++ использует свое пространство имен. Это сделано для того, чтобы избежать конфликтов при именовании переменных и функций. В файле students.cpp мы используем оператор принадлежности :: перед именем каждой функции. Это делается для того, чтобы указать компилятору, что эти функции принадлежат классу Students.
Создание объекта через указатель
При создании объекта, лучше не копировать память для него, а выделять ее в в куче с помощью указателя. И освобождать ее после того, как мы закончили работу с объектом. Реализуем это в нашей программе, немного изменив содержимое файла main.cpp.
/* main.cpp */ #include <iostream> #include «students.h» int main() { // Выделение памяти для объекта Students Students *student = new Students; std::string name; std::string last_name; // Ввод имени с клавиатуры std::cout << «Name: «; getline(std::cin, name); // Ввод фамилии std::cout << «Last name: «; getline(std::cin, last_name); // Сохранение имени и фамилии в объект класса Students student->set_name(name); student->set_last_name(last_name); // Оценки int scores[5]; // Сумма всех оценок int sum = 0; // Ввод промежуточных оценок for (int i = 0; i < 5; ++i) { std::cout << «Score » << i+1 << «: «; std::cin >> scores[i]; // суммирование sum += scores[i]; } // Сохраняем промежуточные оценки в объект класса Student student->set_scores(scores); // Считаем средний балл float average_ball = sum / 5.0; // Сохраняем средний балл в объект класса Students student->set_average_ball(average_ball); // Выводим данные по студенту std::cout << «Average ball for » << student->get_name() << » » << student->get_last_name() << » is » << student->get_average_ball() << std::endl; // Удаление объекта student из памяти delete student; return 0; }
При создании статического объекта, для доступа к его методам и свойствам, используют операция прямого обращения — «.» (символ точки). Если же память для объекта выделяется посредством указателя, то для доступа к его методам и свойствам используется оператор косвенного обращения — «->».
Конструктор и деструктор класса
Конструктор класса — это специальная функция, которая автоматически вызывается сразу после создания объекта этого класса. Он не имеет типа возвращаемого значения и должен называться также, как класс, в котором он находится. По умолчанию, заполним двойками массив с промежуточными оценками студента.
class Students { public: // Конструктор класса Students Students(int default_score) { for (int i = 0; i < 5; ++i) { scores[i] = default_score; } } private: int scores[5]; }; int main() { // Передаем двойку в конструктор Students *student = new Students(2); return 0; }
Мы можем исправить двойки, если ученик будет хорошо себя вести, и вовремя сдавать домашние задания. А на «нет» и суда нет 🙂
Деструктор класса вызывается при уничтожении объекта. Имя деструктора аналогично имени конструктора, только в начале ставится знак тильды ~. Деструктор не имеет входных параметров.
#include <iostream> class Students { public: // Деструктор ~Students() { std::cout << «Memory has been cleaned. Good bye.» << std::endl; } }; int main() { Students *student = new Students; // Уничтожение объекта delete student; return 0; }
Следующий урок: Конструкторы и деструкторы классов в C++ →.
Источник