На уроке №37 мы узнали, что фундаментальные типы данных (int, double, char и т.д.) можно сделать константными, используя ключевое слово const, и что все константные переменные должны быть инициализированы во время объявления. В случае с константными фундаментальными типами данных инициализация может быть копирующей, прямой или uniform:
1 2 3 |
const int value1 = 6; // копирующая инициализация const int value2(8); // прямая инициализация const int value3 { 11 }; // uniform-инициализация (C++11) |
Константные объекты классов
Объекты классов можно сделать константными (используя ключевое слово const). Инициализация выполняется через конструкторы классов:
1 2 3 |
const Date date1; // инициализация через конструктор по умолчанию const Date date2(12, 11, 2018); // инициализация через конструктор с параметрами const Date date3 { 12, 11, 2018 }; // инициализация через конструктор с параметрами в C++11 |
Как только константный объект класса инициализируется через конструктор, то любая попытка изменить переменные-члены объекта запрещена, так как это нарушает принципы константности объекта. Запрещается как изменение переменных-членов напрямую (если они являются public), так и вызов методов (сеттеров), с помощью которых можно установить значения переменным-членам. Рассмотрим следующий класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Anything { public: int m_value; Anything(): m_value(0) { } void setValue(int value) { m_value = value; } int getValue() { return m_value ; } }; int main() { const Anything anything; // вызываем конструктор по умолчанию anything.m_value = 7; // ошибка компиляции: нарушение const anything.setValue(7); // ошибка компиляции: нарушение const return 0; } |
Строки №16-17 вызовут ошибки компиляции, так как они нарушают принципы константности объекта, пытаясь напрямую изменить переменную-член и вызывая сеттер для изменения значения переменной-члена.
Константные методы классов
Теперь рассмотрим следующую строку кода:
1 |
std::cout << anything.getValue(); |
Удивительно, но это также вызовет ошибку компиляции, хотя метод getValue() не делает ничего для изменения переменной-члена! Оказывается, константные объекты класса могут явно вызывать только константные методы класса, а getValue() не указан, как константный метод. Константный метод — это метод, который гарантирует, что не будет изменять объект или вызывать неконстантные методы класса (поскольку они могут изменить объект).
Чтобы сделать getValue() константным, нужно просто добавить ключевое слово const к прототипу функции после списка параметров, но перед телом функции:
1 2 3 4 5 6 7 8 9 10 11 12 |
class Anything { public: int m_value; Anything() { m_value= 0; } void resetValue() { m_value = 0; } void setValue(int value) { m_value = value; } int getValue() const { return m_value; } // ключевое слово const находится после списка параметров, но перед телом функции }; |
Теперь getValue() является константным методом. Это означает, что мы можем вызывать его через любой константный объект.
Для методов, определенных вне тела класса, ключевое слово const должно использоваться как в прототипе функции (в теле класса), так и в определении функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class Anything { public: int m_value; Anything() { m_value= 0; } void resetValue() { m_value = 0; } void setValue(int value) { m_value = value; } int getValue() const; // обратите внимание на ключевое слово const здесь }; int Anything::getValue() const // и здесь { return m_value; } |
Кроме того, любой константный метод, который пытается изменить переменную-член или вызвать неконстантный метод класса, также приведет к ошибке компиляции, например:
1 2 3 4 5 6 7 |
class Anything { public: int m_value ; void resetValue() const { m_value = 0; } // ошибка компиляции, константные методы не могут изменять переменные-члены класса }; |
В этом примере метод resetValue() был установлен константным, но он пытается изменить значение m_value
. Это вызовет ошибку компиляции.
Обратите внимание, конструкторы не могут быть константными. Это связано с тем, что они должны иметь возможность инициализировать переменные-члены класса, а константный конструктор этого не может сделать. Следовательно, в языке С++ константные конструкторы запрещены.
Стоит отметить, что константный объект класса может вызывать конструктор, который будет инициализировать все или некоторые переменные-члены, или же не будет их инициализировать вообще!
Правило: Делайте все ваши методы, которые не изменяют данные объекта класса, константными.
Константные ссылки и классы
Еще одним способом создания константных объектов является передача объектов в функцию по константной ссылке.
На уроке №98 мы рассмотрели преимущества передачи аргументов по константной ссылке, нежели по значению. Если вкратце, то передача аргументов по значению создает копию значения (что является медленным процессом). Большую часть времени нам не нужна копия, а ссылка уже указывает на исходный аргумент и является более эффективной, так как избегает создания и использования ненужной копии. Мы обычно делаем ссылку константной для гарантии того, что функция не изменит значение аргумента и сможет работать с r-values (например, с литералами).
Можете ли вы определить, что не так со следующим кодом?
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 |
#include <iostream> class Date { private: int m_day; int m_month; int m_year; public: Date(int day, int month, int year) { setDate(day, month, year); } void setDate(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } int getDay() { return m_day; } int getMonth() { return m_month; } int getYear() { return m_year; } }; // Примечание: Мы передаем объект date по константной ссылке, дабы избежать создания копии объекта date void printDate(const Date &date) { std::cout << date.getDay() << "." << date.getMonth() << "." << date.getYear() << '\n'; } int main() { Date date(12, 11, 2018); printDate(date); return 0; } |
Ответ заключается в том, что внутри функции printDate(), объект date
рассматривается как константный. И через этот константный date
мы вызываем методы getDay(), getMonth() и getYear(), которые являются неконстантными. Поскольку мы не можем вызывать неконстантные методы через константные объекты, то здесь мы получим ошибку компиляции.
Решение простое — сделать getDay(), getMonth() и getYear() константными:
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 |
class Date { private: int m_day; int m_month; int m_year; public: Date(int day, int month, int year) { setDate(day, month, year); } // Метод setDate() не может быть const, так как изменяет значения переменных-членов void setDate(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } // Все следующие геттеры могут быть const int getDay() const { return m_day; } int getMonth() const { return m_month; } int getYear() const { return m_year; } }; |
Теперь в функции printDate() константный date
сможет вызывать getDay(), getMonth() и getYear().
Перегрузка константных и неконстантных функций
Хотя это делается не очень часто, но функцию можно перегрузить таким образом, чтобы иметь константную и неконстантную версии одной и той же функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#include <string> class Anything { private: std::string m_value; public: Anything(const std::string &value="") { m_value= value; } const std::string& getValue() const { return m_value; } // getValue() для константных объектов std::string& getValue() { return m_value; } // getValue() для неконстантных объектов }; |
Константная версия функции будет вызываться для константных объектов, а неконстантная версия будет вызываться для неконстантных объектов:
1 2 3 4 5 6 7 8 9 10 |
int main() { Anything anything; anything.getValue() = "Hello!"; // вызывается неконстантный getValue() const Anything anything2; anything2.getValue(); // вызывается константный getValue() return 0; } |
Перегрузка метода и разделение его на константную и неконстантную версии обычно выполняется, когда возвращаемое значение должно отличаться по константности (когда требуется константа, и когда она не требуется). В примере, приведенном выше, неконстантная версия getValue() будет работать только с неконстантными объектами, но эта версия более гибкая, так как мы можем использовать её как для чтения, так и для записи m_value
(что мы, собственно, и делаем, присваивая строку Hello!
).
Но, когда мы не изменяем данные объекта класса, тогда вызывается константная версия getValue().
Заключение
Любой метод, который не изменяет данные объекта класса, должен быть const!
В последнем примере строка
Вот никогда бы не подумал, что таким извращённым способом можно присвоить значение внутренней переменной, которая ещё и private. И этот геттер возвращает m_value по значению, то есть копию переменной. Почему это тогда внутренняя переменная объекта должна измениться при присваивании чего-то этой копии? Да и само присваивание значения вызову метода — тоже ломает мозг. Не ожидал, что вызов метода может быть выражением l-value. Неужели это где-то используется?
Этот геттер возвращает не по значению, а неконстантную ссылку (следовательно lvalue):
Вот так геттер стал сеттером.
Трансгеттер!
Меня к такому жизнь не готовила.
Мои полномочия все…
хех, орнул) Поэтому все методы get надо делать const)
Хочу просто уточнить.
Как я вижу тут выходит, что идет инициализация только не в переменную. В константный метод геттер инициализируется возврат, то ли вычислений, то ли еще каких данных.
Грубо говоря мы предоставили файл, который используется, не меняя его, инициализировали, в памяти 80Кб, просто указываем на это "значение" не теряя больше память.
Как-то так, я хочу понимать как работают сложные приложения. Игры те же, на плюсах написанное всегда очень хорошо работает и требования всегда снижены.
Когда-нибудь такие конструкции меня добьют 🙂
Это точно ))), когда приходишь с логикой пхп, например. То эти плюшки, ломают мозг )))
Я вообще с JS пришел. Капец сложно переучится )
Тут просто своя логика, если читать и перечитывать, то все понятно, я сейчас ушел в JS )). После C++ становятся многие вещи более понятными, например ссылки на функции и иногда не хватает типизации. Вообще базу на плюсах полезно пройти, многое потом становится понятным, ты начинаешь понимать как под капотом.
Поясните, пожалуйста, почему нужно еще спереди указать const? Как я понял это связано с тем, что функция возвращает ссылку.
Попробуй не указывать const вначале функции. Исходя их моих личных тестов, перегрузка работает с const в конце, а const спереди ни на что не влияет. Имхо.
Спасибо! нашлось другое — более симпатичное решение одной проблемки. Примерная суть:
допустим у меня есть метод write для объекта с заголовком:
и возвратом return *this (удобно так иногда делать — можно цепочкой точечек пользоваться)
Его можно использовать для перегрузки оператора << :
или
Первое работает и для констант, но выполняет копирование, что не есть гуд.
написать:
нельзя — работать не будет. read требует "живого" объкта
Полностью менять метод read — нехорошо. Не совсем понятно становится, почему именно этот метод не возвращает this. Создавать метод с другим именем или перегружать с управляющими параметрами тоже не айс. А вот сделать константную перегрузку — самое оно
еще раз спасибо 🙂
Мало того! можно не дублировать код функции. Пусть определена:
Тогда неконстантная перегрузка может быть такой:
🙂
Вопрос: можно ли специфицировать вызов именно константной версии как-то проще, чем в коде выше?