В строке 58 содержится определение объекта класса Cat с именем Frisky. В конструктор объекта Frisky передается значение 5. В данном случае нет никакой необходимости вызывать функцию-член SetAge(), поскольку объект Frisky создавался с использованием значения 5, присвоенного переменной-члену itsAge, как показано в строке 61. В строке 63 переменной itsAge объекта Frisky присваивается значение 7, на этот раз с помощью функции SetAge(). Новое значение выводится на экран в строке 65.
Рекомендуется:Используйте конструкторы для инициализации объектов.
He рекомендуется:Не пытайтесь с помощью конструктора или деструктора возвращать какое бы то ни было значение.
Не передавайте деструкторам параметры.
Объявление функций-членов со спецификатором const
В языке C++ предусмотрена возможность объявить метод класса таким образом, что такому методу будет запрещено изменять значения переменных-членов класса. Для этого в объявлении функции используется ключевое слово const, стоящее после круглых скобок, но перед точкой с запятой. Например, объявим таким образом функцию-член SomeFunction(), которая не принимает аргументов и возвращает значение типа void:
void SomeFunction() const;
Функции доступа к данным часто объявляются со спецификатором const. В классе Cat есть две функции доступа:
void SetAge(int anAge);
int GetAge();
Функция SetAge() не может быть объявлена со спецификатором const, поскольку она изменяет значение переменной-члена itsAge. А в объявлении функЦии.^Аде() может и даже должен использоваться спецификатор const, поскольку она не должна ничего изменять в классе. Функция GetAge() просто возвращает текущее значение переменной-члена itsAge. Следовательно, объявление этих функций необходимо записать в таком виде:
void SetAge(int anAge);
int GetAge() const;
Если некоторая функция объявлена с использованием спецификатора const, а в ее выполнении происходит изменение какого-либо члена объекта, то компилятор покажет сообщение об ошибке. Например, если записать функцию GetAge() таким образом, чтобы она подсчитывала, сколько раз запрашивался возраст объекта Cat, будет обязательно сгенерирована ошибка компиляции, поскольку при таком подсчете (т.е. при вызове функции GetAge()) происходит изменение объекта Cat.
Примечание:Используйте спецификатор const везде в объявлениях функций-членов, если они не должны изменять объект. Это позволит компилятору лучше отслеживать ошибки и поможет вам при отладке программы.
Использовать const в объявлениях методов, не изменяющих объект, считается хорошим стилем программирования. Это позволяет компилятору лучше отслеживать ошибки еще до запуска программы на выполнение.
Чем отличается интерфейс от выполнения класса
Как уже упоминалось, клиенты - это составные части программы, которые создают и используют объекты вашего класса. Открытый интерфейс класса (объявление класса) можно представить себе в виде соглашения с этими клиентами, в котором указываются способы взаимодействия клиентов с классом.
Например, в объявлении класса Cat указывается, что программа-клиент может инициализировать любой возраст объекта этого класса с помощью функции доступа SetAge() и возвратить это значение с помощью функции доступа GetAge(). При этом гарантируется, что каждый объект класса Cat сможет вывести сообщение Meow на экран с помощью функции-члена Meow(). Обратите внимание, что в открытом интерфейсе класса ничего не говорится о закрытой переменной-члене itsAge, которая используется при выполнении класса и не должна интересовать клиентов. Значение возраста можно возвратить из объекта с помощью GetAge() и установить с помощью SetAge(), но сама переменная itsAge, в которой хранится это значение, скрыта от клиентов.
Если объявить функцию GetAge() со спецификатором const, а именно этого требуют правила хорошего тона программирования, в соглашение также будет внесен пункт о том, что функцию GetAge() нельзя использовать для изменения значений объекта класса Cat.
В языке C++ осуществляется строгий контроль за типами данных, поэтому подобное соглашение между классом и клиентами будет законом для компилятора, который сгенерирует ошибку компиляции в случае нарушения этого соглашения. В листинге 6.5 показан пример программы, которую не удастся скомпилировать из-за нарушения этих самых соглашений.
Предупреждение:Листинг 6.5 не компилируется!
Листинг 6.5. Пример нарушения соглашений интерфейса
1: // Пример ошибки компиляции, связанной
2: // с нарушениями соглашений интерфейса класса
3:
4: #include <iostream.h> // для объекта cout
5:
6: class Cat
7: {
8: public:
9: Cat(int initialAge);
10: ~Cat();
11: int GetAge() const; // метод доступа const
12: void SetAge (int age);
13: void Meow();
14: private:
15: int itsAge;
16: };
17:
18: // конструктор класса Cat
19: Cat::Cat(int initialAge)
20: {
21: itsAge = initialAge;
22: cout << "Cat constructor\n";
23: }
24: Cat::~Cat() // деструктор, который не выполняет никаких действий
25: {
26: cout << "Cat destructor\n";
27: }
28: // функция GetAge объявлена как const,
29: // но мы нарушаем это условие!
30: int Cat::GetAge() const
31: {
32: return (itsAge++); // это нарушение соглашения интерфейса!
33: }
34:
35: // определение функции SetAge как открытого
36: // метода доступа к данным класса
37:
38: void Cat::SetAge(int age)
39: {
40: // присваиваем переменной-члену itsAge
41: // значение переданного парйметра age
42: itsAge = age;
43: }
44:
45: // Определение метода Meow
46: // возвращает void
47: // параметров нет
48: // используется для вывода на экран текста "Meow"
49: void Cat::Meow()
50: {
51: cout << "Meow.\n";
52: }
53:
54: // демонстрирует различные нарушения
55: // интерфейса, что приводит к ошибкам компиляции
56: int main()
57: {
58: Cat Frisky; // не соответствует обьявлению
59: Frisky.Meow();
60: Frisky.Bark(); // Нет, кошки не лают.
61: Frisky.itsAge = 7; // переменная itsAge закрыта
62: return 0;
63: }
Анализ: Как упоминалось выше, эта программа не компилируется. Поэтому и отсутствуют результаты ее работы.
Эту программу было забавно писать, поскольку в нее специально закладывались ошибки.
В строке 11 GetAge( )объявляется как функция доступа к данным-членам класса без права их изменения, на что указывает спецификатор const. Однако в теле функции GetAge(), а именно в строке 32, выполняется приращение переменной-члена itsAge. А поскольку этот метод объявлен как const, он не имеет права изменять значение переменной itsAge. Следовательно, во время компиляции программы на этой строке будет зафиксирована ошибка.
В строке 13 объявляется метод Meow(), в этот раз без использования ключевого слова const. И хотя такое упущение не является ошибкой, это далеко не лучший стиль программирования. Если учесть, что этот метод не должен изменять значения переменных-членов класса Cat, то его следовало бы определить со спецификатором const.
В строке 58 показано определение объекта класса Cat с именем Frisky. В этом варианте программы класс Cat имеет конструктор, который принимает в качестве параметра целочисленное значение. Это означает обязательность передачи параметра заданного типа. Поскольку в строке 58 никакой параметр не передается, компилятор зафиксирует ошибку.
Примечание:Если в классе объявляется какой-либо конструктор, компилятор в этом случае не станет предлагать со своей стороны никакого другого конструктора даже если определение объекта по форме не будет coответствовать объявленному конструктору. В подобных случаях компилятор покажет сообщение об ошибке.
В строке 60 вызывается метод Bark(). Этот метод вообще не был объявлен, следовательно, ни о каком его использовании и речи быть не может.
В строке 61 делается попытка присвоить переменной itsAge значение 7. Поскольку переменная itsAge относится к числу закрытых данных-членов, то при компиляции программы здесь будет зафиксировано покушение на частную собственность класса.
Почему для отслеживания ошибок лучше использовать компилятор
Кажется невероятным написать программу не допуская никаких ошибок. Тем не менее некоторые программисты способны на подобные чудеса, хотя, конечно, таких кудесников очень немного. Большинство из них, как и все нормальные люди делают ошибки. Поэтому нашлись программисты, которые разработали систему, способную помочь в отслеживании ошибок путем перехвата и исправления их на ранней стадии создания программ. Хотя сообщения об ошибках, выявленных компилятором, действуют на нервы, это намного лучше возникновения ошибок при выполнении программы. Если бы компилятор был менее дотошный, то велика вероятность, что ваша программа дала бы сбой в самый неподходящий момент, например во время презентации.
Ошибки компиляции, т.е. ошибки, выявленные на стадии компиляции, гораздо безобиднее ошибок выполнения, которые проявляются после запуска программы. Компилятор будет дотошно и однотипно сообщать об обнаруженной им ошибке. Напротив, ошибка выполнения может не обнаруживать себя до поры до времени, но потом проявиться в самый неподходящий момент. Поскольку ошибки компиляции заявляют о себе при каждом сеансе компиляции, то их легко идентифицировать и исправить, чтобы больше о них не вспоминать. Чтобы добиться создания программ, которые не станут со временем выкидывать фокусы, программист должен помочь компилятору в отслеживании ошибок, используя спецификаторы в объявлениях для предупреждения возможных сбоев.
Где следует распологать в программе объявления классов и определения методов
Каждая функция, объявленная в классе, должна иметь определение. Определение также называется выполнением функции. Подобно другим функциям, определение метода класса состоит из заголовка и тела функции.
Определение должно находиться в файле, который компилятор может легко найти. Большинство компиляторов C++ предпочитают, чтобы такой файл имел расширение .c или .cpp. В этой книге используется расширение .cpp, но вам стоит выяснить предпочтения собственного компилятора.
Примечание: Многие компиляторы полагают, что файлы с расширением .c содержат программы, написанные на языке С, а файлы с расширением .cpp - программы на C++. Вы можете использовать любое расширение, но именно .cpp сведет к минимуму возможные недоразумения в программах на C++.
Объявления классов можно поместить в один файл с программой, но это не считается хорошим стилем программирования. В соглашении, которого придерживаются многие программисты, рекомендуется помещать объявление в файл заголовка, имя которого обычно совпадает с именем файла программы, но в качестве расширения используются такие варианты, как .h, .hp или .hpp. В этой книге для имен файлов заголовков используется расширение .hpp, но вам стоит выяснить предпочтения собственного компилятора.
Например, можно поместить объявление класса Cat в файл с именем CAT, hpp, а определение методов класса - в файл с именем CAT .cpp. Затем нужно включить файл заголовка в код файла с расширением .cpp. Для этого в начале программного кода в файле CAT.cpp используется следующая команда:
#include "Cat.hpp"
Эта команда дает указание компилятору ввести содержимое файла CAT.hpp в данном месте программы. Результат выполнения команды include такой же, как если бы вы переписали с клавиатуры в это место программы полное содержимое соответствующего файла заголовка. Имейте в виду, что некоторые компиляторы чувствительны к регистру букв и требуют точного соответствия написания имен файла в директиве #include и на диске.
Зачем же нужно отделять файл заголовка с расширением .hpp от файла программы с расширением cpp, если мы все равно собираемся вводить содержимое файла заголовка назад в файл программы? Как показывает практика, большую часть времени клиентов вашего класса не волнуют подробности его выполнения. При чтении небольшого файла заголовка они получают всю необходимую информацию и могут игнорировать файл с подробностями выполнения этого класса. Кроме того, не исключено, что содержимое файла заголовка с расширением .hpp вам захочется включить не в один, а в несколько файлов программ.
Примечание:Объявление класса сообщает компилятору, что представляет собой этот класс, какие данные он содержит и какими функциями располагает. Объявление класса называется его интерфейсом, поскольку оно сообщает пользователю, как взаимодействовать с классом. Интерфейс обычно хранится в файле с расширением .hpp, который называется файлом заголовка.
Из определения функции компилятор узнает, как она работает. Определение функции называется выполнением метода класса и хранится в файле с расширением .cpp. Подробности выполнения класса касаются только автора класса. Клиентам же класса, т.е. частям программы, использующим этот класс, не нужно знать, как выполняются функции.
Выполнение с подстановкой
Можно выполнять подстановку методов с помощью ключевого слова inline точно так же, как это делалось с обычными функциями. Для этого ключевое слово inline нужно разместить перед типом возвращаемого значения. Например, определение подставляемой функции-члена GetWeight() имеет следующий вид:
inline intCat::GetWeight()
{
return itsweight; // возвращает переменную-член Weight
}
Можно также поместить определение функции в объявление класса, что автоматически делает такую функцию подставляемой:
class Cat
{
public:
int GetWeight() { return itsWeight; } // подставляемая функция
void SetWeight(int aWeight);
};
Обратите внимание на синтаксис определения функции GetWeight(). Тело подставляемой функции начинается сразу же после объявления метода класса, причем после круглых скобок нет никакой точки с запятой. Подобно определению обычной функции, определение метода начинается с открывающей фигурной скобки и оканчивается закрывающей фигурной скобкой. Как обычно, пробелы значения не имеют, и то же самое определение можно записать несколько иначе:
class Cat
{
public:
int GetWeight() const
{
return itsWeight;
} // подставляемая функция
void SetWeight(int aWeight);
};
В листингах 6.6 и 6.7 вновь создается класс Cat, но в данном случае объявление класса содержится в файле CAT.hpp, а выполнение - в файле CAT.cpp. Кроме того, в листинге 6.7 метод доступа к данным класса и метод Meow() являются подставляемыми.
Листинг 6.6. Объявление класса CAT в файле CAT.hpp
1: #include <iostream.h>
2: class Cat
3; {
4: public:
5: Cat (int initialAge);
6: ~Cat();
7: int GetAge() const { return itsAge;) // подставляемая функция!
8: void SetAge (int age) { itsAge = age;} // подставляемая функция!
9: void Meow() const { cout << "Мяу.\n";} // подставляемая функция!
10: private:
11: int itsAge;
12: };
Листинг 6.7. Выполнение масса Cat в файле CAT.cpp
1: // Пример использования подставляемых функций
2: // и включения файла заголовка
3:
4: #include "cat.hpp" // не забудьте включить файл заголовка!
5:
6:
7: Cat::Cat(int initialAge) //конструктор
8: {
9: itsAge = initialAge;
10: }
11:
12: Cat::~Cat() // деструктор, не выполняет никаких действий
13: {
14: }
15:
16: // Создаем виртуальную кошку, устанавливаем ее возраст, разрешаем
17: // ей мяукнуть, сообщаем ее возраст, затем снова "мяукаем" и изменяем возраст кошки.
18: int main()
19: {
20: Cat Frisky(5);
21: Frisky.Meow();
22: cout << "Frisky is а cat who is ";
23: cout << Frisky.QetAge() << " years old.\n";
24: Frisky.Meow();
25: Frisky.SetAge(7);
26: cout << "Now Frisky is " ;
27: cout << Frisky.GetAge() << " years old.\n";
28: return 0;
29: }
Результат:
Meow.
Frisky is а cat who is 5 years old.
Meow.
Now Frisky is 7 years old.
Анализ: Программа, представленная в листингах 6.6 и 6.7, аналогична программе из листинга 6.4 за исключением того, что три метода класса объявляются подставляемыми, а само объявление класса вынесено в файл заголовка CAT.hpp.
В строке 7 объявляется функция GetAge() и тут же следует определение ее выполнения. Строки 8 и 9 занимают объявления еще двух встроенных функций, но их определения содержатся в другом файле.
В строке 4 листинга 6.7 с помощью команды #include "cat.hpp" в программу включается содержимое файла CAT.hpp. Компилятор получает команду считать содержимое файла cat .hpp и ввести его в данный файл, начиная со строки 5.
Возможность встраивания файлов в другие файлы позволяет хранить объявления классов отдельно от их выполнения и использовать тогда, когда в этом возникает необходимость. Это стандартный прием при создании программ на языке C++. Обычно объявления классов хранятся в файле с расширением .hpp, который затем включается в соответствующий файл .cpp с помощью директивы #include.
В строках 18-29 в точности повторяется тело функции main() из листинга 6.4. Цель этого повторения - показать, что применение подставляемых вариантов функций не внесло изменений в использование этих функций.