Пользовательские структуры и классы в C++
Язык С++ обладает большим количеством встроенных типов данных, но на практике этих типов данных мало. Часто программисту требуется иметь возможность создавать сложные типы данных, которые могли бы объединять несколько простых.
Предположим, что необходимо создать тип данных, который описывает человека. Опишем его с помощью структуры:
struct Person { string lastname; string firstname; string middlename; };
Структура Person состоит из 3 полей структуры, каждый из которых является строкой. lastname
, firstname
и middlename
хранят информацию о фамилии, имени и отчестве соответственно. Теперь можно создать запись о человеке:
int main(int argc, char *argv[]) { Person p = Person(); p.lastname = "Pelevin"; p.firstname = "Maksim"; p.middlename = "Sergeevich"; }
Дополнительное задание: усложните структуру, добавив к ней информацию о дате рождения и номере телефона. Дату рождения можно описать через другую структуру, хранящую информацию о дне, месяце и годе.
Точка между переменной и полем в записи p.lastname
является операцией доступа к полю. Обращаю внимание, что для объектов, созданных динамически, эта операция выглядит как ->
.
int main(int argc, char *argv[]) { Person *p = new Person(); p->lastname = "Pelevin"; p->firstname = "Maksim"; p->middlename = "Sergeevich"; p.middlename = "Sergeevich"; // error: reference type 'Person *' is a pointer; maybe you meant to use '->'? delete p; }
6 строчка приведёт к ошибке компиляции.
Добавляем методы
После того, как человек получил фамилию, имя и отчество, хорошо бы его научить отвечать на вопрос, кто он такой. Для этого прямо в структуре можно объявить функцию:
struct Person { string lastname; string firstname; string middlename; string getFullName() { return lastname + " " + firstname + " " + middlename; } };
и теперь спросим его его следующим образом:
int main(int argc, char *argv[]) { Person p = Person(); p.lastname = "Pelevin"; p.firstname = "Maksim"; p.middlename = "Sergeevich"; cout << p.getFullName(); }
В консоли выведется полные ФИО. Отлично. Вдумчивые читатели спросят: «А что делать, если я создаю функцию и передаю в качестве аргумента переменную, по имению совпадающую с полем структуры?». У меня подготовлено 2 ответа на этот вопрос:
Вариант 1
Часто программисты во избежание коллиций имён, в имени поля в конце добавляют символ нижнего подчёркивания, или вначале, или пишут вообще что-то вроде: m_lastname
. Ну вы поняли…
Вариант 2
Синтаксис языка позволяет использовать ключевое слово this
, который даёт ссылку на объект. Запись this->lastname
в методе структуры можно прочитать как «моя фамилия». Таким образом можно избегать коллизий имён, что улучшает читаемость кода, но может привести к неожиданным ошибками. Обращаю внимание, что this
является ссылкой, а следовательно доступ к полям осуществляется через «стрелочку» (->
).
О размере структуры
Вот простенький пример:
struct A { int a; char b; }; int main(int argc, char *argv[]) { cout << sizeof(A); }
и такой же простой вопрос: int занимает в памяти 4 байта, char — 1. Сколько в памяти занимает тип данных A? 4 + 1 = 5
, верно? Так почему же sizeof(A) возвращает число 8? Правильный ответ заключается в том, что внутреннее представление выравнивается таким образом, чтобы оно было кратно 1 машинному слову (м.с.) процессора (напоминмаю, что для 32-битных систем 1 м.с. = 4 байта, а для 64-битных — 8). Вы можете установить собственное количество байтов, по которому будет выравниваться размер структуры. Делается это с помощью специальных директив препроцессора (здесь 1 — 1 байт, можно подставить любое понравившееся число, например 2 и тогда структура будет занимать 6 байт):
#pragma pack(push, 1) struct A { int a; char b; }; #pragma pack(pop)
Классы и основы ООП
Инкапсуляция
А давайте просто заменим слово struct
на слово class
?
class Person { string lastname; string firstname; string middlename; string getFullName() { return lastname + " " + firstname + " " + middlename; } };
Вероятно (да нет, конечно, точно!), вы получили ошибку следующего содержания:
error: 'lastname' is a private member of 'Person' p.lastname = "Pelevin"; ^
Всё дело в том, что когда мы изменили структуру на класс, то автоматически наложили условия доступа к полями и методам. Собственно, именно эта особенность отличает структуру от класса, поэтому в дальнейшем я не стану разделять эти 2 почти одинаковых понятия.
Существует 3 уровня доступа к полям и класса:
- публичный — самый слабый. Доступ к полю или методу может осуществляться откуда угодно;
- приватный — самый сильный. Доступ к полю или методы может осуществляться только из методов самого класса и только оттуда;
- защищённый — доступ к полю или методам осуществляется через методы самого класса или методы его подклассов. О подклассах будет рассказано ниже.
Различные режимы доступа обеспечивают одно из 3 оснований объектно-ориентированной парадигмы программирования — инкапсуляция. Инкапсуляция позволяет скрыть внутреннюю реализацию класса, оставив только необходимые для работы методы. Например для некоторого класса Browser, совершенно не важно, каким образом будет происходить чтение файла с сервера и его передача, вы знаете, что метод string Browse::load(URL url)
по заданному url вернёт вам html-код, упакованный в string.
Напишем код, который позволит создать экземпляр класса Person (полные листинг):
#include <iostream> #include <string.h> using namespace std; class Person { string lastname; string firstname; string middlename; public: Person(string lastname, string firstname, string middlename) { this->lastname = lastname; this->firstname = firstname; this->middlename = middlename; } string getFullName() { return lastname + " " + firstname + " " + middlename; } }; int main(int argc, char *argv[]) { Person p = Person("Pelevin", "Maksim", "Sergeevich"); cout << p.getFullName(); }
На 11 строчке меняется режим доступа на публичный (public), поэтому все поля и методы, следующие после него и до следующего изменения режима доступа или конца файла будут считаться публичными. С 13 по 17 строку объявлен конструктор класса. Конструктор класса вызывается всегда при создании объекта, независимо от области памяти, в которой он создаётся; по сути это просто метод специального вида, названия которого совпадает с именем класса, а возвращаемое значение и вовсе отсутствует. Конструктор, у которого нет аргументов, называется конструктор по-умолчанию; если в классе не объявлено никакого другого конструктора, то конструктор по-умолчанию автоматически генерируется компилятором.
В нашем случае, конструктор принимает в себя значения, которыми будут проинициализированы внутренние поля класса (ещё раз обращаю внимание на использование this). Для инициализации полей класса существует сокращённый синтаксис:
Person(string lastname, string firstname, string middlename) : lastname(lastname), firstname(firstname), middlename(middlename) { cout << "The new person has born!\n"; }
Помимо конструктора существует деструктор, который вызывается, когда объект уничтожается. Записывается в классе он следующим образом: ~Person() {...}
.
Наследование
Тут всё как у Дарвина: один класс может наследовать свойства другого и дополнять их своими. Например, создадим студента, который, несомненно, является человеком, но, помимо этого у него ещё есть возраст; одновременно научим его говорить, сколько же он уже учится, если бы поступил на 1 курс в 18 лет:
class Student : public Person { int age; public: Student(string lastname, string firstname, string middlename, int age) : Person(lastname, firstname, middlename), age(age) { cout << "The student came with age " << age << ".\n"; } int getUniversityTime() { return age - 18; } }; int main(int argc, char *argv[]) { Student p = Student("Pelevin", "Maksim", "Sergeevich", 24); cout << p.getFullName() << " and he's studying for " << p.getUniversityTime() << " years"; }
Строка 1 демонстрируется, каким образом объявляется, что класс является наследником другого класса. public
перед именем суперкласса говорит о том, каким образом будет меняться режим доступа к полям класса. В подавляющем большинстве, это будет публичный режим доступа, поэтому другие виды в данной статье не рассматриваются.
В выделенных строках содержится очень много информации и я сейчас попробую разъяснить их все. В строке 7 объявлена сигнатура конструктора, создающего экземпляр класса Student, что похоже на то, что было уже сделано для класса Person. Строка 9 очень похожа на то, что мы видели, когда инициализировали поля класса у Person; собственно, так оно и есть. Строка 8 — это инициализация суперкласса, то есть класса Person, которому передаются некоторые значения аргументов конструктора Student. Если суперкласс имеет конструкор по умолчанию, то это строка может быть опущена, как и 9-я.
Вообще, при работе программы, когда нужно создать экземпляр определённого класса, создаются вначале все суперклассы, а после этого объект желаемого класса. При разрушении объекта через оператор delete или другим причинам происходит уничтожение объектов в обратном порядке.
Полиморфизм
Полиморфизм позволяет переопределять поведение классов, под которыми понимается в общем-то работа методов классов. Например:
string getFullName() { string fullname = Person::getFullName(); return fullname + " is student"; }
Код Person::getFullName
лишь способ обратиться к методу суперкласса с указанием конкретного суперкласса, т.к. С++ поддерживает множественное наследование, о чём здесь не рассказывается. Таким образом, следуеющий код:
int main(int argc, char *argv[]) { Student p = Student("Pelevin", "Maksim", "Sergeevich", 24); cout << p.getFullName() << " and he's studying for " << p.getUniversityTime() << " years"; }
выведет строку: Pelevin Maksim Sergeevich is student and he's studying for 6 years
.
Перегрузка операции класса
Можно научить наш класс выводить имя, не вызывая каки-либо специальных методов. Здесь я привожу только пример без пояснений, которые можно найти по ссылке:
#include <iostream> #include <string.h> using namespace std; class Person { string lastname; string firstname; string middlename; public: Person(string lastname, string firstname, string middlename) : lastname(lastname), firstname(firstname), middlename(middlename) { cout << "The new person has born!\n"; } friend ostream& operator<< (ostream &out, Person &p); }; ostream& operator<<(ostream& os, Person &p) { os << p.lastname << " " << p.firstname << " " << p.middlename; return os; } int main(int argc, char *argv[]) { Person p = Person("Pelevin", "Maksim", "Sergeevich"); cout << p; }