Книга: С++ — это просто. — 3-е изд.: Пер. с англ.
Назад: Оглавление
Дальше: Деструкторы

4500.75

Но будьте осторожны!

Не пытайтесь вернуть локальную переменную по ссылке. Это связано с тем, что

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

возвращает значение. Таким образом, вы возвращаете ссылку на переменную, которой больше не существует, а вызывающая функция ссылается на несущест-вующую переменную.

Некоторые компиляторы C++ выдают предупреждение, когда встречают код, воз-вращающий ссылки на локальные переменные. Проигнорировав такое предупреждение, вы получите непредсказуемые результаты. Иной раз кажется, что программа

корректно работает, поскольку ячейка в стеке, где находилась локальная переменная, не задействуется при использовании ссылки. Работая в одних случаях, программа может дать сбой в других из-за прерываний устройства или многозадачного

режима с использованием стека.

Спецификатор const

Переменная может быть объявлена с использованием спецификатора типа const (от

«константа»), и тогда ее значение не сможет изменяться во время выполнения программы. Любая попытка модифицировать значение переменной, инициированная

посредством спецификатора const, приведет к тому, что компилятор выдаст сообщение об ошибке. Спецификатор const, как правило, применяется вместо символи-ческих констант, представленных при помощи директивы #define.

Спецификатор const не позволит вашей программе изменить значение переменной, которой вы отвели роль константы. Это также служит указанием всем, кто читает

программу, что переменная не предназначена для модификации. Имена const-переменных часто записываются в верхнем регистре, как напоминание о том, что

они являются именованными константами. Следующая программа демонстрирует

применение спецификатора const:

#include <iostream>

using namespace std ;

int main( )

{

float r, a ;

const float PI = 3.14f ;

cin >> r ;

a = PI * r * r ;

cout << endl << "Площадь круга равна " << a << endl ; return 0 ;

}

Переходим на C++

53

Спецификатор типа const предпочтительнее директивы #define, поскольку областью видимости const-переменной можно управлять, размещая ее либо внутри

функции, либо вне всех функций. Так, если const-переменная помещена внутрь

функции, область ее действия будет локализована для данной функции, а если она

размещена вне всех функций, то область ее действия будет глобальной. При помощи директивы #define невозможно добиться столь точной регулировки.

Const-указатели

Начнем сразу с программы:

#include <iostream>

using namespace std ;

void xstrcpy ( char *, char * ) ;

int main( )

{

char str1[ ] = "Nagpur" ;

char str2[ 10 ] ;

xstrcpy ( str2, str1 ) ;

cout << str2 << endl ;

return 0 ;

}

void xstrcpy ( char *t, char *s )

{

while ( *s != '\0' )

{

*t = *s ;

t++ ;

s++ ;

}

*t = '\0' ;

}

Данный фрагмент кода просто копирует содержимое строки str1[ ] в строку str2[ ], используя функцию xstrcpy( ). Что произойдет, если мы добавим следующие строки после последней инструкции xstrcpy( )?

s = s - 6 ;

*s = 'K' ;

Исходная строка изменится на «Kagpur». Можем ли мы быть уверены в том, что

исходная строка не изменится даже случайно при работе xstrcpy( )? Можем, если

предварим ключевым словом const параметр-указатель, изменив прототип функции на:

void xstrcpy ( char *, const char * ) ;

54

Глава 2

Соответственно ее объявление изменится на:

void xstrcpy ( char *t, const char *s )

{

// код программы

}

Следующий фрагмент кода более подробно представит работу спецификатора

const:

char *p = "Hello" ; // указатель — переменная, строка — константа

*p = 'M' ; // ошибка

p = "Bye" ; // работает

const char *q = "Hello" ; // указатель — переменная, строка — константа

*q = 'M' ; // ошибка

q = "Bye" ; // работает

char const *s = "Hello" ; // указатель — переменная, строка — константа

*s = 'M' ; // ошибка

s = "Bye" ; // работает

char * const t = "Hello" ; // указатель и строка — константы

*t = 'M' ; // работает

t = "Bye" ; // ошибка

const char * const u = "Hello" ; // указатель и строка — константы

*u = 'M' ; // ошибка

u = "Bye" ; // ошибка

Const-ссылки

Ранее мы узнали, что можем передать ссылку на переменную в функцию, если нам

требуется модифицировать переменную в функции без использования указателей.

И это не единственное применение передачи по ссылке.

Если мы передадим переменную по значению, то она будет собрана в другую переменную. Тем самым мы создадим еще одну переменную. Сколько переменных

передается по значению, столько создается дополнительных переменных. Этого

можно избежать, передавая переменные по ссылке. Так мы повысим эффектив-ность программного кода. Тем не менее возникает вероятность того, что переменные будут случайно изменены внутри функции. Этого можно избежать, объявив их

константами, как показано в следующем примере:

#include <iostream>

using namespace std ;

void change ( const int & ) ;

Переходим на C++

55

int main( )

{

int i = 32 ;

change ( i ) ;

cout << endl << i ;

return 0 ;

}

void change ( const int &j )

{

j = 45 ;

}

Передача переменной i по ссылке позволяет избежать создания новой переменной.

В то же время спецификатор const предотвращает изменение j (и, в свою очередь, i). Любая попытка модифицировать j вызовет сообщение об ошибке — «Невозможно изменить объект-константу» (‘Cannot modify a constant object’).

А теперь рассмотрим несколько примеров, которые подробнее проясняют концепцию спецификатора const и ссылок:

#include <iostream>

using namespace std ;

int main( )

{

const int i = 10 ;

int &j = i ;

cout << "i = " << i << " j = " << j ; j = 20 ;

cout << "i = " << i << " j = " << j ; return 0 ;

}

В этом примере мы связали j с i. Таким образом, изменение j не должно изменять i, т. к. мы объявили переменную i константой, и потому ее нельзя модифицировать, даже через ссылку на нее. Рассуждая таким образом, мы ожидаем, что программа

выдаст следующее:

i = 10

j = 10

i = 10

j = 20

Однако в ходе компиляции этого фрагмента мы получим сообщение об ошибке, гласящее: «Невозможно преобразовать const int в int &» (‘Cannot convert from const int to int &’). Ошибка возникает из-за того, что мы объявили j ссылкой на int, а не

на const int.

Возьмем другой похожий случай с const-ссылкой:

#include <iostream>

using namespace std ;

56

Глава 2

int main( )

{

int i = 10 ;

const int &j = i ;

cout << i << j ;

j = 20 ;

cout << i << j ;

return 0 ;

}

Эту программу компилятор тоже не пропустит. Что неудивительно, ведь мы опять

делаем что-то совсем не то. Здесь мы инициализировали j в качестве константы, а затем пытаемся изменить ее значение через j = 20.

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

как только мы объявляем переменную или ссылку в качестве константы, мы уже не

можем изменить ее значение.

Как вы думаете, каким будет результат в примере ниже?

#include <iostream>

using namespace std ;

int main( )

{

int i = 10 ;

const int &j = i ;

i = 20 ;

cout << endl << "i = " << i << " j = " << j << endl ; return 0 ;

}

Программа выдаст:

i = 20

j = 20

Но почему? Ссылка зависит от переменной, с которой она связана. Если мы изме-ним значение переменной, значение ссылки изменится автоматически, будь то константа или что-либо иное.

Можем ли мы использовать ссылки для строк, как мы делаем это с целыми числами? Разумеется! И вот каким образом:

#include <iostream>

using namespace std ;

int main( )

{

char *str1 = "Снова, снова дождик льет" ;

char * &str2 = str1 ;

cout << endl << str1 << endl << str2 ;

*str1 = 'M' ;

cout << endl << str1 << endl << str2 ;

Переходим на C++

57

*str2 = 'P' ;

cout << endl << str1 << endl << str2 ;

return 0 ;

}

Здесь str1 — это указатель на строковую константу «Снова, снова дождик льет».

А str2 — это указатель, действующий как ссылка на str1. Таким образом, str1 и

str2 указывают на одну и ту же строку. Далее мы пробуем изменить строковую

константу сначала через str1, затем через str2. Обе попытки приводят к ошибке

выполнения кода, т. к. в них мы пытаемся модифицировать строку, являющуюся

константой.

Возврат значений const-переменных

Функция может возвращать указатель на строковую константу, как показано ниже:

#include <iostream>

using namespace std ;

const char *fun( ) ;

int main( )

{

const char *p ;

p = fun( ) ;

*p = 'A' ; // Ошибка

cout << p ;

return 0 ;

}

const char *fun( )

{

return "Дождик" ;

}

Здесь, поскольку функция fun( ) возвращает строковую константу, мы не можем

воспользоваться указателем p для ее модификации. Более того, следующие операции также недопустимы:

Функция main( ) не может присвоить возвращаемое значение указателю, если

тот не указывает на строковую константу.

Функция main( ) не может передать возвращаемое значение функции, если та не

ожидает указатель на строковую константу.

Функции-члены типа const

В языке C структуры могут содержать только типы данных, тогда как в C++ они

могут содержать и функции. Помимо этого, в C++ есть еще одна сущность, име-нуемая классом, который также может содержать члены данных и функции-члены.

58

Глава 2

Спецификатор const может использоваться для определения функций-членов

в структуре или классе. О таком применении const мы поговорим в главе 5.

Логический тип данных (bool)

Логические (булевы) типы данных названы в честь Джорджа Буля, английского

математика XIX века, разработавшего теорию применения логических операторов

со значениями "верно/истина" (true) и "неверно/ложь" (false). Логические значения

часто называют булевы значения.

Этот тип данных может принимать только два значения, "верно" (true) или "неверно" (false). Чаще всего он используется для хранения результатов сравнений, например:

bool x, y ;

int a = 10, b = 20, c = 30 ;

x = a < b ;

y = b >= c ;

Здесь x принимает значение true, а y — значение false.

При преобразовании в целое число true имеет значение 1, а false — значение 0.

И наоборот, целые числа могут быть имплицитно преобразованы в булевы значения — ненулевые целые числа преобразуются в true, а 0 — в false. Логический тип

можно использовать в арифметических и логических выражениях. Там они преобразуются в целые числа, и над этими целыми числами выполняются целочисленные

арифметические и логические операции. Результат этих операций может быть

преобразован обратно в логический тип: 0 преобразуется в false, а ненулевое значение — в true. Небольшая программа и ее выдача помогут вам понять механизм

этих преобразований:

#include <iostream>

using namespace std ;

int main( )

{

bool b = 32 ;

int i = false ;

cout << endl << b << endl << i ;

int j = b + b ;

bool k = b + b ;

cout << endl << j << endl << k << endl ; return 0 ;

}

Результат работы программы:

1

0

2

1

Переходим на C++

59

Логический тип bool также может использоваться как возвращаемый тип функции, обычно указывающий на удачное либо неудачное выполнение назначенного задания. Старые компиляторы C++, такие как Turbo С++ 3.0, не распознают логический тип данных.

1. Укажите, верны ли следующие утверждения:

В C++ структура может содержать члены данных, а также функции, способные

работать с членами данных.

В C++ объединение может содержать члены данных, а также функции, способные работать с членами данных.

Можно создать массив ссылок.

Как только происходит связывание ссылки с переменной, ссылка уже не может

быть связана с другой переменной.

Переменная может быть связана с несколькими ссылками.

В C++ вызов функции может происходить даже в левой части оператора присваивания.

Небезопасно возвращать локальную переменную по ссылке.

cin и cout являются объектами.

C++ позволяет использовать неименованные объединения.

Указатель иного типа может быть назначен указателю void без необходимости

приведения типов.

Два следующих объявления взаимозаменимы:

enum grade g ;

grade g ;

Три следующих объявления дают одинаковый результат:

int a = 10 ;

int a ( 10 ) ;

int a { 10 } ;

Два следующих объявления дают одинаковый результат:

bool a ;

BOOL a ;

Три следующих объявления дают одинаковый результат:

cout << "\n" ;

cout << '\n' ;

cout << endl ;

60

Глава 2

2. Каков будет результат работы следующих программ:

(A)

#include <iostream>

using namespace std ;

int main( )

{

int i = 5 ;int &j = i ;

int p = 10 ;

j = p ;

cout << i << endl << j << endl ;

p = 20 ;

cout << i << endl << j << endl ;

return 0 ;

}

(B)

#include <iostream>

using namespace std ;

int main( )

{

char *p = "Привет!" ;

char *q = p ;

cout << p << endl << q << endl ;

q = "До встречи!" ;

cout << p << endl << q << endl ;

return 0 ;

}

(C)

#include <iostream>

using namespace std ;

int i = 20 ;

int main( )

{

int i = 5 ;

cout << i << endl << ::i << endl ;

return 0 ;

}

(D)

#include <iostream>

using namespace std ;

Переходим на C++

61

int i = 20 ;

int main( )

{

int i = 5 ;

cout << i << endl << ::i << endl ;

{

int i = 10 ;

cout << i << endl << ::i << endl ;

}

return 0 ;

}

(E)

#include <iostream>

using namespace std ;

const int i = 10 ;

int main( )

{

const int i = 20 ;

cout << i << endl << ::i << endl ;

cout << &i << endl << &::i << endl ; return 0 ;

}

(F)

#include <iostream>

using namespace std ;

int main( )

{

int i ;

cout << sizeof ( i ) << endl << sizeof ( 'i' ) << endl ; return 0 ;

}

3. Укажите ошибки, если таковые имеются, в следующих фрагментах программного кода:

(A)

#include <iostream>

using namespace std ;

int main( )

{

for ( int i = 1 ;i <= 10 ;i++ ) cout << i << endl ;

cout << i ;

return 0 ;

}

62

Глава 2

(B)

#include <iostream>

using namespace std ;

int main( )

{

int i = 5 ;

int &j = i ;

int &k = j ;

int &l = i ;

cout << i << j << k << l ;

return 0 ;

}

(C)

#include <iostream>

using namespace std ;

int main( )

{

int a = 10, b = 20 ;

long int c ;

c = a * long int ( b ) ;

cout << c ;

return 0 ;

}

(D)

#include <iostream>

using namespace std ;

const int i = 10 ;

int main( )

{

const int i = 20 ;

cout << &i endl << &::i ;

return 0 ;

}

(E)

#include <iostream>

using namespace std ;

int main( )

{

char *p = "Здравствуй" ;

p = "Привет" ;

Переходим на C++

63

*p = 'G' ;

cout << p ;

return 0 ;

}

(F)

#include <iostream>

using namespace std ;

int main( )

{

enum result { first, second, third } ;

result a = first ;

int b = a ;

result c = 1 ;

result d = result ( 1 ) ;

return 0 ;

}

(G)

#include <iostream>

using namespace std ;

int a = 10 ;

int main( )

{

int a = 20 ;

{

int a = 30 ;

cout << a << ::a << ::::a ;

}

return 0 ;

}

(H)

#include <iostream>

using namespace std ;

struct emp

{

char name[ 20 ] ;

int age ;

float sal ;

} ;

emp e1 = { "Amol", 21, 2345.00 } ;

emp e2 = { "Ajay", 19, 2300.00 } ;

emp &fun( ) ;

int main( )

64

Глава 2

{

fun( ) = e2 ;

cout << endl << e1.name << endl << e1.age << endl << e1.sal ; return 0 ;

}

emp &fun( )

{

emp e3 = { "Aditya", 21, 3300.75 } ;

return e3 ;

}

(I)

#include <iostream>

using namespace std ;

int main( )

{

char t[ ] = "Строковые функции - это просто!" ;

int l = strlen ( t ) ;

cout << l ;

return 0 ;

}

4. Ответьте на следующие вопросы:

Как бы вы объявили q в следующей программе, если первый cout должен дважды выводить «Интернет», а второй cout — дважды выводить «Интранет»?

#include <iostream>

using namespace std ;

int main( )

{

char *p = "Интернет" ;

cout << p << q << endl ;

q = "Интранет" ;

cout << p << q << endl ;

return 0 ;

}

Если employee является структурой, REGS — союзом, а maritalstatus — пере-числением, то есть ли какой-либо иной способ, которым можно сделать следующие объявления?

struct employee e ;

union REGS i ;

enum maritalstatus m ;

Могут ли следующие инструкции быть записаны иным способом:

employee *p ;

p = ( employee * ) malloc ( sizeof ( e ) ) ;

Переходим на C++

65

float q ;

int a, b ;

q = ( float ) a / b ;

5. Создайте четыре целых числа, четыре указателя на эти числа и четыре

ссылки на них. Сохраните эти указатели и ссылки в двух массивах и распе-чатайте значения четырех целых чисел, используя эти массивы.

6. Завершите следующую программу, определив функцию swapb( ) и ее прототип таким образом, чтобы результат выполнения программы был равен

(20, 10):

#include <iostream>

using namespace std ;

void swapa ( int &, int & ) ;

int main( )

{

int a = 10, b = 20 ;

swapa ( a, b ) ;

cout << a << b ;

return 0 ;

}

void swapa ( int &x, int &y )

{

swapb ( x, y ) ;

}

7. Ответьте на вопросы ниже:

В каких случаях мы должны делать вызов по ссылке?

Почему использовать спецификатор const лучше, чем директиву #define?

Каковы преимущества cout и cin по сравнению с printf( ) и scanf( )?

Допустим ли такой комментарий?

// Это не /* допустимый */ комментарий

На что указывает следующий прототип:

const char *fun ( char const*, const char * ) ;

Любая программа на языке C также является корректной программой на C++.

С++98, С++11, С++14 и С++17 являются различными версиями С++.

C++ поддерживает два типа комментариев. Как правило, стиль // служит для

создания однострочных, а стиль /* .. */ — для многострочных комментариев.

66

Глава 2

cout является объектом класса ostream. Он используется для вывода на экран.

endl используется для вывода на экран '\n'.

<< является оператором вывода данных.

cin является объектом класса istream. Он используется для считывания ввода

с клавиатуры.

>> является оператором ввода данных.

Операторы << и >> могут каскадироваться.

Классы ostream и istream объявлены в заголовочном файле iostream.

Объекты cout и cin определены в пространстве имен с именем std.

Чтобы использовать cout и cin, необходимо включить файл istream и использовать оператор using namespace в начале программы.

Если оператор using namespace не используется, то cout и cin должны иметь

префикс std::.

cin и cout лучше, чем printf( ) и scanf( ), по двум причинам:

Не нужно запоминать и указывать спецификаторы формата.

Их можно использовать для ввода/вывода множества значений в объекте.

Переменную можно инициализировать тремя способами:

int i = 20 ;

int j ( 20 ) ;

int k { 30 } ;

Можно использовать ключевое слово auto для вывода типа переменной по присваиваемому ей значению.

Можно использовать ключевое слово decltype для вывода типа переменной без

присвоения ей значения.

Для объявления переменной структуры, объединения или перечисления не обязательно использовать ключевые слова struct, union или enum.

Неименованные объединения и перечисления обеспечивают прямой доступ

к своим элементам, не требуя создания переменных объединения или перечисления.

Неименованные объединения удобно применять, когда они вложены в структуру.

Можно объявлять переменные непосредственно перед их использованием в программном коде.

Если переменная объявлена в цикле, то она теряет свое значение, когда управление из него выходит.

Допустимы обе операции приведения типов:

n = ( y - 1 ) * ( long ) j ;

n = ( y - 1 ) * long ( j ) ;

Переходим на C++

67

Любой тип указателя может быть назначен пустому указателю (void).

Чтобы присвоить пустой указатель любому другому типу указателя, он должен

быть соответствующим образом приведен к типу.

Оператор расширения области :: используется для доступа к глобальной переменной с тем же именем, что и у локальной переменной.

Ссылки — это постоянные указатели, которые автоматически разыменовываются.

Ссылка может быть связана только с одной переменной.

Допускается множество ссылок на одну переменную.

Не допускается ссылка на ссылку.

Не допускается массив ссылок.

C++ поддерживает три типа вызовов функций:.

Вызов по значению — передача значений фактических аргументов.

Вызов по адресу — передача адресов фактических аргументов.

Вызов по ссылке — передача адресов фактических аргументов.

При вызове по ссылке изменение формальных аргументов в функции не приводит к изменению фактических аргументов.

При вызове по адресу и вызове по ссылке с использованием формальных аргументов фактические аргументы могут подвергаться модификации.

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

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

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

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

В ходе выполнения программного кода значение const-переменной нельзя изменить программными средствами.

Спецификатор const лучше, чем директива #define, поскольку область видимости const может быть как локальной, так и глобальной.

У директивы #define всегда глобальная область видимости.

Спецификатор const также может использоваться для создания const-указателей, const-ссылок и строковых констант.

Если ссылка объявлена как константа, то при ее использовании переменная, с которой связана ссылка, не может быть изменена.

68

Глава 2

Структура и класс могут содержать функции-члены. Они также могут быть заданы при помощи спецификатора const.

Логический (булев) тип данных может принимать только два значения — true («верно/истина») или false («неверно/ложь»).

Если в арифметических операциях используется логический тип, то значение

true обрабатывается как 1, а false — как 0.

Если в логическом выражении используется целое число, то ненулевое значение

рассматривается как true, а 0 — как false.

Функции

Разработка программ на C строилась вокруг функций.

С++ выводит их на абсолютно новый уровень. Функции в С++

эффективнее и мощнее. Помимо обычных функций, С++ также

поддерживает экземплярные, статические, дружественные

и виртуальные функции. Эта глава поможет вам освоить

и по достоинству оценить мощь функций в С++

70

Глава 3

Строгая проверка типов

Исходные значения для аргументов функции

Перегрузка функций

• Разница в типе возвращаемого значения

• Можно ли задать разные типы данных при помощи typedef?

• Можно ли задать разные типы данных при помощи const?

• Разные задачи, одно имя

Перегрузка операторов

• FAQ по перегрузке операторов

Встраиваемые функции

• Зачем полагаться на компилятор?

• А где гарантия?

• Когда ими пользоваться?

Новый синтаксис возвращаемого типа

Функции instance, static, virtual и friend

Упражнения

Важное

Функции

71

ункции (или методы) служат основным структурным компонентом, вокруг

которого строятся программы на C. С++ выводит их на совершенно новый

Фуровень. Функции С++ мощнее и гибче, причем во многих отношениях.

Эти усовершенствования были нацелены на то, чтобы сделать программы на С++

более безопасными и читабельными, чем их предшественники на C.

Некоторые средства, добавленные в функции С++, актуальны лишь для тех случаев, когда мы задействуем объектно-ориентированные возможности С++, тогда как

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

В этой главе мы ограничимся теми улучшениями функций С++, которые применимы к программе любого типа. К числу таких средств относятся:

Строгая проверка прототипа.

Значения по умолчанию для аргументов функции.

Перегрузка функций.

Перегрузка операторов.

Встраиваемые функции.

Рассмотрим каждое из них по отдельности.

Строгая проверка типов

Прототип функции — это объявление, определяющее имя функции, ее аргументы и

тип возвращаемого значения. Ниже приведен ряд примеров объявлений прототипов

функций:

float square ( float ) ;

char * strConvert ( char *, int ) ;

double nthRoot ( float, float ) ;

Каждое из этих объявлений четко указывает имя функции, количество, порядок и

тип аргументов, которые каждая функция будет получать во время вызова, а также

тип значения, которое каждая функция будет возвращать.

Ни одна функция С++ не может быть вызвана, если ее прототип не доступен для

компилятора. Компилятор использует прототип функции для перекрестной проверки на совпадение типов аргументов и типов возвращаемого значения в вызове и

определении. Это называется строгой проверкой типов. В С она отсутствует. Обнаружив несовпадение типа, компилятор С++ незамедлительно указывает на ошибку.

Кроме того, в C проверка типов относительно нестрогая. К примеру, там допустимо

передавать значение как двойное число (double), а возвращать как число с плавающей точкой (float). С++ выдаст на это ошибку или предупреждение.

Задание прототипов функций может означать чуть больше работы на начальном

этапе написания программы. Тем не менее прототипы являются бесценными инст-рументами для обнаружения трудноуловимых ошибок. Язык С++ был разработан

в стремлении предотвратить множество проблем, вызванных небрежностью программистов, в том числе связанных с передачей неправильных типов данных функ-

72

Глава 3

циям. Если вам действительно требуется передать число типа double как float, это

можно сделать при помощи приведения типов. В таком случае вы явным (а не слу-чайным) образом передаете значение одного типа в качестве аргумента параметру

другого типа.

Исходные значения для аргументов функции

Функциям в С++ можно назначать исходные значения аргументов (значения по

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

Рассмотрим, как это происходит, на примере программы:

#include <iostream>

using namespace std ;

int sum ( int n1, int n2, int n3 = 0, int n4 = 0 ) ;

int main( )

{

int s1, s2, s3 ;

s1 = sum ( 10, 20 ) ;

s2 = sum ( 10, 20, 30 ) ;

s3 = sum ( 10, 20, 30, 40 ) ;

cout << s1 << endl << s2 << endl << s3 << endl ; return 0 ;

}

int sum ( int n1, int n2, int n3, int n4 )

{

return ( n1 + n2 + n3 + n4 ) ;

}

Обратите внимание на объявление прототипа функции sum( ). В нем указано, что 0

будет использоваться как значение параметров n3 и n4, если мы не передаем их

в вызове. Таким образом, при первом вызове sum( ) n3 и n4 устанавливаются рав-ными 0. Во втором вызове n3 присваивается значение 30, тогда как n4 принимает

значение 0. В последнем вызове n3 и n4 присваиваются значения 30 и 40. Так, если

значения не передаются в вызове, то для параметров функции применяются значения по умолчанию.

Исходные значения для аргументов функции подчиняются ряду правил:

Значение по умолчанию для аргументов указывается лишь в прототипе функции, а не в ее определении. Для построения вызова компилятор использует информацию из прототипа функции, а не из ее определения.

Значения по умолчанию могут быть назначены только для конечных аргументов, но не для ведущих аргументов или аргументов в середине списка. Таким

образом, следующее объявление неверно:

int sum ( int n1, int n2, int n3 = 0, int n4 ) ;

Функции

73

Значением по умолчанию для аргумента может быть глобальная константа, глобальная переменная или даже вызов функции. Например, приведенный ниже

прототип функции является допустимым:

int myfunc ( int flag = display( ) ) ;

В данном примере, если myfunc( ) вызывается без аргумента, то значение, возвращаемое функцией display( ), будет рассматриваться как флаговое значение по

умолчанию.

Исходные значения для аргументов функции полезны в двух случаях:

Если аргументы функции при ее вызове почти всегда имеют одно и то же значение.

Если после написания программы мы хотим расширить возможности функции, добавив еще один аргумент. При использовании предустановленных аргументов

уже существующие вызовы функции могут продолжать использовать прежнее

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

число аргументов.

Перегрузка функции

В C каждая функция в программе должна иметь уникальное имя. Иногда это начинает раздражать. К примеру, если нам требуется привести к абсолютной величине

значение числового аргумента, мы должны определить следующие три функции

для каждого числового типа данных:

int abs ( int i ) ;

long labs ( long l ) ;

double fabs ( double d ) ;

Все эти функции выполняют одну и ту же задачу, поэтому присваивать каждой из

них свое имя кажется излишним. Для разрешения такой ситуации С++ позволяет

программисту создавать три разные функции с одним именем.

Такой процесс называется перегрузкой функции.

Рассмотрим следующий пример:

#include <iostream>

using namespace std ;

int abso ( int ) ;

long abso ( long ) ;

double abso ( double ) ;

int main( )

{

int i = -25, j ;

long l = -100000L, m ;

double d = -12.34, e ;

74

Глава 3

j = abso ( i ) ;

m = abso ( l ) ;

e = abso ( d ) ;

cout << j << endl << m << endl << e << endl ; return 0 ;

}

int abso ( int ii )

{

return ( ii > 0 ? ii : ii * -1 ) ;

}

long abso ( long ll )

{

return ( ll > 0 ? ll : ll * -1 ) ;

}

double abso ( double dd )

{

return ( dd > 0 ? dd : dd * -1 ) ;

}

Откуда компилятору С++ известно, какую из функций abso( ) следует выбирать

при вызове? Он выбирает по типу аргумента, передаваемого при вызове функции.

К примеру, если передается целочисленный тип (int), то вызывается целочисленная

версия abso( ), а если передается число двойной точности (double), то вызывается

версия abso( ) для этого типа и т. д. Согласитесь, всё достаточно логично.

А что произойдет, если мы сделаем такие вызовы?

char ch = abso ( 'A' ) ;

float f = abso ( 3.14f ) ;

Мы не объявляли функцию abso( ) для обработки типов char или float. И, как следствие, компилятор С++ вызывает версию int в первом случае и версию double во

втором. Если бы мы определили соответствующие функции, то вызывались бы они.

Заметьте, что мы не использовали имя abs( ), т. к. оно конфликтует с одноименной

функцией, присутствующей в стандартной библиотеке С++.

А теперь перейдем к ряду специфичных вопросов, связанных с перегрузкой функций.

Разница в типе возвращаемого значения

Рассмотрим следующую программу для преобразования числовой строки в число:

#include <iostream>

using namespace std ;

int stringToNumber ( char * ) ;

long int stringToNumber ( char * ) ;

Функции

75

int main( )

{

int n1 ;

long int n2 ;

char *ptr1 = "155" ;

char *ptr2 = "400000" ;

n1 = stringToNumber ( ptr1 ) ;

n2 = stringToNumber ( ptr2 ) ;

cout << n1 << endl << n2 ;

return 0 ;

}

Даже если мы зададим перегруженные функции stringToNumber( ), компилятор

все равно выдаст ошибку.

Дело в том, что эти функции будут получать аргументы одного и того же типа и

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

Можно ли задать разные типы данных

при помощи typedef?

Можем ли мы обмануть компилятор С++, представив ему два типа данных как различные, если переименуем один из них при помощи typedef-инструкции? Нет.

Ключевое слово typedef просто присваивает еще одно имя в качестве дополнения

к существующему имени типа, но не образует новый тип.

Поэтому следующий фрагмент программного кода вызовет ошибку:

typedef int INT;

void display ( int ) ;

void display ( INT ) ;

Здесь INT является лишь еще одним именем для int. Следовательно, компилятор не

сможет различить две версии функции display( ).

Можно ли задать разные типы данных

при помощи const?

Следующая программа предназначена для отображения двух строк:

#include <iostream>

using namespace std ;

void display ( char * ) ;

void display ( const char * ) ;

76

Глава 3

int main( )

{

char *ch1 = "Hello" ;

const char *ch2 = "Пока!" ;

display ( ch1 ) ;

display ( ch2 ) ;

return 0 ;

}

void display ( char *p )

{

cout << p << endl ;

}

void display ( const char *p )

{

cout << p << endl ;

}

Функция display( ) здесь построена корректно, поскольку типы char * и const char

* различны. Так, первая версия display( ) получает указатель на строку, тогда как

вторая получает указатель на константную строку.

Разные задачи, одно имя

Однако создавать перегруженные функции для выполнения разных видов задач —

не самая удачная идея в программировании; функции с одинаковыми именами

должны иметь одно общее назначение. К примеру, если мы напишем функцию

abso( ), которая возвращает квадратный корень числа, это будет нелогично и лишь

создаст путаницу.

Поэтому применять перегруженные функции следует благоразумно. Их предназна-чение — предоставить общее имя для набора «схожих, но несколько отличающихся» функций. Злоупотребление перегруженными функциями может сделать программный код трудночитаемым.

Перегрузка операторов

Перегрузка операторов — одна из самых потрясающих возможностей С++. Благодаря ей сложноорганизованные и неудобочитаемые листинги программ преобра-жаются в интуитивно понятный исходный код. Такие операторы, как +, *, -, <=, >=, обычно применяются лишь к стандартным типам данных, в частности, к целым

числам (int), числам с плавающей точкой (float) и т. п. Однако в С++ мы можем

перегрузить эти операторы, чтобы они работали даже с пользовательскими типами

данных.

Предположим, мы хотим выполнить вычисление комплексных чисел. Как известно, не существует стандартного типа данных для представления комплексного числа.

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

Функции

77

struct Complex

{

double real, imag ;

} ;

Затем мы можем создавать переменные этого типа и выполнять общие операции, такие как сложение, вычитание и умножение, как показано в следующем фрагменте

кода:

Complex a, b, c, d, e ;

c = add ( a, b ) ;

d = subtract ( a, b ) ;

e = multiply ( a, b ) ;

Вместо определения функций add( ), subtract( ) или multiply( ) следующая форма

будет интуитивно более понятной:

Complex a, b, c, d, e ;

c = a + b ;

d = a - b ;

e = a * b ;

Но С++ не знает, как обрабатывать +, - или * для пользовательского типа Complex.

Следовательно, нам нужно обучить его, перегрузив эти операторы. Как это сделать, показано в следующей программе:

# include <iostream>

using namespace std ;

struct Complex

{

double real, imag ;

} ;

Complex setComplex ( double r, double i ) ;

void printComplex ( Complex c ) ;

Complex operator + ( Complex c1, Complex c2 ) ;

Complex operator - ( Complex c1, Complex c2 ) ;

int main( )

{

Complex a, b, c, d ;

a = setComplex ( 1.0, 1.0 ) ;

b = setComplex ( 2.0, 2.0 ) ;

c = a + b ;

d = b + c - a ;

cout << "c = " ;printComplex ( c ) ;cout << "d = " ; printComplex ( d ) ;

78

Глава 3

return 0 ;

}

Complex setComplex ( double r, double i )

{

Complex temp ;

temp.real = r ;

temp.imag = i ;

return temp ;

}

void printComplex ( Complex t )

{

cout << "( " << t.real << ", " << t.imag << ")" << endl ;

}

Complex operator + ( Complex c1, Complex c2 )

{

Complex temp ;

temp.real = c1.real + c2.real ;

temp.imag = c1.imag + c2.imag ;

return temp ;

}

Complex operator - ( Complex c1, Complex c2 )

{

Complex temp ;

temp.real = c1.real - c2.real ;

temp.imag = c1.imag - c2.imag ;

return temp ;

}

В этой программе операторы + и - обучаются работать с пользовательским типом

данных Complex. Достигается это путем объявления функции с использованием

ключевого слова operator и самого оператора, предназначенного для перегрузки, как показано ниже.

Complex operator + (Complex c1, Complex c2 ) ;

Это объявление сообщает компилятору, что функция перегруженного оператора

operator + () получает два аргумента типа Complex и возвращает тип Complex. Эта

функция будет вызываться, если операция сложения выполняется над операндами

типа Complex.

В действительности перегрузка операторов позволяет существенно расширить возможности операторов С++. Перегрузка операторов проявляет себя во всей красе

в сочетании с классами в С++. Мы продемонстрируем это в главе 4. Уверен, что

тогда вы сможете по-настоящему оценить концепцию перегрузки операторов.

Функции

79

FAQ по перегрузке операторов

Прежде чем закрыть эту тему, позвольте мне ответить на три наиболее часто зада-ваемых вопроса о перегрузке операторов.

Какие операторы не подлежат перегрузке?

Нельзя перегрузить операторы ., :: и ? :.

Если у оператора есть унарная и бинарная формы (например, операторы + или &), то обе могут быть перегружены.

Каков приоритет операторных функций?

Операторные функции имеют тот же приоритет, что и собственные операции

с тем же оператором. К примеру, оператор * всегда имеет более высокий уровень старшинства, чем оператор +. Приоритет оператора изменить невозможно.

Можно ли переопределить встроенные операторы?

На самом деле вы спрашиваете о том, возможно ли создать свой собственный

оператор для сложения пары целых чисел. Ответ — нет. Если позволить изменять поведение собственных операций, то любая программа может стать нечи-таемой.

Встраиваемые функции

Одним из значительных преимуществ применения функций является то, что они

позволяют экономить место в памяти. Поскольку при каждом вызове функции

выполняется один и тот же код, тело функции не нужно дублировать в памяти.

Представьте ситуацию, когда небольшая функция вызывается в программе несколько раз. При каждом вызове функции возникает определенная непроизводи-тельная нагрузка: время тратится на передачу значений, передачу управления, возврат значения и возврат управления. В программе на языке C этих нагрузок можно

избежать, задав вместо функции эквивалентный ей макрос. Это повышает скорость

выполнения кода, однако у макросов есть ряд недостатков, особенно при наличии

аргументов. Рассмотрим, к примеру, следующий макрос:

#define SQUARE( x ) ( x * x )

Если использовать его в выражении

int y = SQUARE ( 3 + 1 ) ;

он преобразуется в y = 3 + 1 * 3 + 1 вместо ожидаемого y = (3 + 1) * (3 + 1).

Во избежание таких нежелательных результатов в C++ была введена концепция

встраиваемой функции.

При ее использовании обращения ко встраиваемой функции заменяются определенными в ней инструкциями. За отсутствием вызовов функции снимается и свя-занная с ними лишняя нагрузка. Следующий листинг демонстрирует работу

встраиваемой функции:

80

Глава 3

#include <iostream>

using namespace std ;

inline void reportError ( char *str )

{

cout << endl << str ;

exit ( 1 ) ;

}

int main( )

{

// код открытия исходного файла

if ( fileOpeningFailed )

reportError ( "Невозможно открыть исходный файл" ) ;

// код открытия целевого файла

if (fileOpeningFailed )

reportError ( "Невозможно открыть целевой файл" ) ;

// код копирования содержимого исходного файла в целевой файл

return 0 ;

}

Заметьте, что функция должна быть объявлена встраиваемой (с помощью ключевого слова inline) до ее вызова. При компиляции содержимое функции reportError( ) будет вставлено в две части нашей программы, туда, где вызывается функция

reportError( ).

Зачем полагаться на компилятор?

У вас наверняка возник вопрос: зачем использовать компилятор, чтобы встроить

код функции в код другой программы, когда мы легко можем сделать это сами?

Проблема с многократной вставкой одного и того же кода заключается в том, что

вы теряете преимущества четкого и ясного построения программы, которые прив-носит использование функций.

Программа может работать быстрее и занимать меньше пространства в памяти, но

листинг будет длиннее и сложнее. И напротив, если мы помещаем код во встраиваемую функцию, исходный файл остается хорошо организованным и легко читаемым, поскольку функция отображается как отдельный объект. А при компиляции

в объектный код тело функции размещается непосредственно в тех позициях, где

происходит ее вызов.

А где гарантия?

Заметьте, что при определении встраиваемой функции нет гарантии, что ее код

будет размещен там, где выполняется вызов. Происходит это потому, что мы всего

лишь делаем запрос к компилятору. Язык C++ не регламентирует, при каких усло-

Функции

81

виях компилятор может игнорировать запрос. В силу данной неоднозначности

в спецификации языка разработчики компиляторов могут достаточно свободно интерпретировать эти требования.

Когда ими пользоваться?

Использовать спецификатор встраиваемой функции inline следует лишь для небольших функций. При большом размере правильнее предпочесть обычные функции, т. к. экономия места в памяти стоит сравнительно небольшой потери в скорости выполнения.

Новый синтаксис возвращаемого типа

Начиная с версии С++11, был введен новый синтаксис, называемый хвостовой возвращаемый тип, для указания типа значения, возвращаемого функцией. Рассмотрим, к примеру, следующее объявление прототипа:

int addNum ( int x, int y ) ;

Это объявление может быть также записано следующим образом:

auto addNum ( int x, int y ) -> int ;

Здесь ключевое слово auto никак не связано с определением типа. Это всего лишь

часть синтаксиса для хвостового возвращаемого типа.

В С++14 ключевое слово auto также может определять тип возвращаемого значения функции. Например, в следующей функции, раз сумма x + y вычисляется как

целое число, компилятор решает, что тип возвращаемого значения этой функции —

int

auto addNum ( int x, int y )

{

return x + y ;

}

По моему убеждению, такой формы записи следует избегать, т. к. при отсутствии

определенного типа возвращаемого значения вызывающая сторона может непра-вильно его интерпретировать, что, в свою очередь, может привести к непредска-зуемым ошибкам.

Функции instance, static, virtual и friend

В C++ существует еще четыре типа функций: экземплярные (instance), статические (static), виртуальные (virtual) и дружественные (friend). Однако все они связаны с классами. Поэтому их обсуждение мы продолжим, когда познакомимся

с классами. Экземплярные функции (instance) объясняются в главе 4, статические

функции (static) — в главе 6, виртуальные функции (virtual) — в главе 8 и дружественные функции (friend) — в главе 10.

82

Глава 3

Имея за плечами столько знаний, мы уже вполне готовы погрузиться в объектно-ориентированный мир... что мы и сделаем уже в следующей главе, но пока несколько упражнений.

1. Укажите, верны ли следующие утверждения:

Обязательно объявлять прототип вызываемой функции.

Две функции могут быть перегружены, если их аргументы похожи, но возвра-щаемые значения различны.

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

Если для четырех аргументов в прототипе функции указаны значения по умолчанию, мы можем вызвать эту функцию и передать ей первый и четвертый

аргументы.

Функция может быть перегружена сколько угодно раз.

Оператор присваивания не может быть перегружен.

Когда мы определяем функцию как встраиваемую (inline), нет гарантии, что ее

код будет вставлен в то место, где выполняется вызов.

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

2. Укажите ошибки, если таковые имеются, в следующих фрагментах программного кода:

(A)

int main( )

{

int a = 30 ;

fun( ) ;

return 0 ;

}

void fun( )

{

int b = 20 ;

}

(B)

#include <iostream> using namespace std ;

void fun( )

{

cout << "Привет" ;

}

Функции

83

int main( )

{

fun( ) ;return 0 ;

}

(C)

#include <iostream>

using namespace std ;

int fun ( int, int ) ;

int fun ( int, int ) ;

int main( )

{

int a ;

a = fun ( 10, 30 ) ;

cout << a ;

return 0 ;

}

void fun ( int x, int y )

{

return x + y ;

}

(D)

#include <iostream>

using namespace std ;

int main( )

{

void fun1 ( void ) ;

void fun2 ( void ) ;

fun1( ) ;

return 0 ;

}

void fun1 ( void )

{

fun2( ) ;

cout << endl << "Привет!...Доброго" ;

}

void fun2 ( void )

{

cout << endl << "вам дня" ;

}

(E)

#include <iostream>

using namespace std ;

84

Глава 3

void fun ( int, float ) ;

int main( )

{

fun( ) ;

return 0 ;

}

void fun ( int i = 10, float a = 3.14 )

{

cout << i << a ;

}

(F)

#include <iostream>

using namespace std ;

void fun ( int = 10, int = 20, int = 30 ) ;

void fun ( int, int ) ;

int main( )

{

fun ( 1, 2 ) ;

return 0 ;

}

void fun ( int x, int y, int z )

{

cout << endl << x << endl << y << endl << z ;

}

void fun ( int x, int y )

{

cout << endl << x << endl << y ;

}

3. Решите следующие задачи и ответьте на вопросы:

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

очищать весь экран. За максимальный размер экрана примите 25 строк и

80 столбцов.

Предположим, что задана функция со следующим прототипом:

void fun ( int = 10, int = 20, int = 30, int = 40 ) ;

Если эта функция вызывается с передачей ей двух аргументов, то как нам удостовериться в том, что эти аргументы обрабатываются как первый и третий, тогда как второй и четвертый принимаются по умолчанию.

Составьте программу, задающую функцию display( ), прототип которой приведен ниже:

void display ( char ch = '*', int num = 80 ) ;

Функции

85

Вызовите эту функцию для вывода следующих строк:

*****

-------

=====

Напишите перегруженные функции для преобразования int в строку и для преобразования float в строку.

Напишите перегруженные функции для преобразования строки в int и преобразования строки в float.

Объявите структуру с именем matrix, содержащую массив целых чисел 3 x 3.

Перегрузите оператор +, чтобы выполнить сложение двух матриц.

Составьте прототипы для:

• функции, которая получает числа типов int и float, а возвращает число типа

double.

• функции, которая получает указатель типа int и ссылку типа float, а возвращает указатель типа int.

• функции, которая ничего не получает и ничего не возвращает.

• функции, которая получает массив чисел типа int и ссылку типа float и ничего не возвращает.

Какие операторы нельзя перегрузить?

Можно ли изменить приоритет операторов, перегрузив их?

Требуется объявлять прототип вызываемой функции.

Компилятор C++ использует строгую проверку прототипов для корректной

передачи вызова функции.

При объявлении прототипа значения по умолчанию могут быть назначены только для конечных аргументов функции.

Можно перегрузить несколько функций, выполняющих схожие задачи, но отли-чающихся аргументами.

Перегруженные функции имеют одинаковое имя, но их аргументы различаются

по количеству, порядку или типу.

Различие в возвращаемых типах значений не является достаточным основанием

для перегрузки функций.

Различия в аргументах перегруженных функций не могут быть основаны на

переименовании типа при помощи ключевого слова typedef.

86

Глава 3

Операторы могут быть перегружены для того, чтобы сделать операции с пользовательскими типами интуитивно понятными.

Приоритет операторов нельзя изменить при помощи их перегрузки.

Нельзя перегрузить операторы ., :: и ? :.

Перегрузка операторов невозможна для фундаментальных типов, таких как int или float.

Встраиваемые функции сочетают преимущества функции и макроса.

Вызов встраиваемой функции заменяется кодом, содержащимся в ее определении.

Определение функции как встраиваемой не гарантирует, что компилятор C++

выполнит ее вставку.

Классы и объекты

Классы и объекты играют в ООП ту же роль,

что и функции в процедурном программировании.

Добротные базовые знания о классах и объектах

сделают вас умелым программистом на С++

88

Глава 4

Структуры и классы

Классы и конструкторы

Деструкторы

Класс Complex

Указатель this

Перегрузка унарных операторов

Объекты и память

Еще раз о структурах и классах

Идеальная организация программы

Упражнения

Важное

Классы и объекты

89

ознакомившись с необъектно-ориентированными расширениями С++ (ис-

ходными значениями для аргументов функций, перегруженными функция-

Пми, перегруженными операторами, вызовом по ссылке, спецификатором

const), пришло время перейти к объектно-ориентированным функциям C++.

Сначала познакомимся со структурами и классами.

Структуры и классы

Структура в C — это набор похожих или различающихся типов данных. C++ рас-ширяет возможности структур, позволяя включать в структуры функции. Объединение данных и функций, их обрабатывающих, в единое целое является краеугольным камнем объектно-ориентированного программирования.

В C++ к структурам добавляется ещё один элемент: класс, который также может

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

программистов на C++ используют структуры для хранения данных, а классы для

хранения данных и функций.

Начнем с программы, демонстрирующей синтаксис и общие свойства класса:

#include <iostream>

using namespace std ;

class Rectangle

{

private : int len, br ;

public : void getData( )

{

cout << endl << "Введите длину и ширину " ;cin >> len >> br ;

}

void setData ( int l, int b )

{

len = l ;br = b ;

}

void displayData( )

{

cout << endl << "длина = " << len ;cout << endl << "ширина = " << br ;

}

void areaPeri ( )

{

int a, p ;

a = len * br ;

p = 2 * ( len + br ) ;

cout << endl << "площадь = " << a ;

cout << endl << "периметр = " << p << endl ;

}

} ;

90

Глава 4

int main( )

{

Rectangle r1, r2, r3 ; // определение трех объектов класса Rectangle r1.setData ( 10, 20 ) ; // присвоение данных элементам объекта

r1.displayData( ) ; // отображение данных, присвоенных в setData( ) r1.areaPeri( ) ; // расчет и вывод площади и периметра

r2.setData ( 5, 8 ) ;

r2.displayData( ) ;

r2.areaPeri( ) ;

r3.getData( ) ; // получение данных с клавиатуры

r3.displayData( ) ;

r3.areaPeri( ) ;

return 0 ;

}

Взгляните на объявление класса Rectangle в нашей программе:

Слово class ключевое, за ним следует слово Rectangle, являющееся именем

класса. Аналогично структуре тело класса заключается в фигурные скобки и

ограничивается точкой с запятой.

Переменные, объявленные в классе, называются членами данных, а функции, объявленные в нем, называются функциями-членами (компонентными функциями) или методами.

Класс Rectangle включает два члена данных, len и br, а также четыре функции-члена — setData( ), getData( ), displayData( ) и areaPeri( ).

Класс содержит два незнакомых нам ключевых слова — private и public. Они

управляют доступом к членам класса. Закрытые члены (private members) недоступны за пределами класса, а открытые члены (public members) доступны.

Хотя это и не правило, обычно члены данных объявляются закрытыми (private), а функции-члены — открытыми (public). Таким образом, члены данных хранятся

в безопасности. К ним нельзя получить доступ или преобразовывать их извне класса. Если же нам потребуется это сделать, то мы должны обращаться к ним через

открытые функции-члены класса: в пределах класса любой его член может получить доступ к любому другому члену этого класса.

Эта концепция обеспечения безопасности членов данных и предоставления им доступа через функции-члены называется скрытием данных. Поскольку функции-члены обеспечивают систематический доступ к скрытым членам данных, их нередко называют интерфейсом класса.

Не путайте скрытие данных с методами безопасности, применяемыми для защиты

компьютерных данных. Методы безопасности предотвращают неавторизированный

доступ к информации злоумышленников, тогда как скрытие данных используется

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

Классы и объекты

91

Посмотрим теперь, как функция main( ) задействует объявленный класс. Для этого

нам требуется создать объекты этого класса. Так, в main( ) мы создали три объекта

r1, r2 и r3 типа Rectangle.

Для сравнения взглянем на следующие объявления:

int i ;

Rectangle r1 ;

В этом примере int — встроенный тип данных, а Rectangle — пользовательский.

Элементы i и r1 являются переменными типов int и Rectangle соответственно.

В терминологии ООП r1 называется объектом класса Rectangle.

Объект также часто называют экземпляром (instance) класса, а операция создания

объекта называется инстанцированием (instantiation), созданием экземпляра.

В нашей программе мы определили три объекта — r1, r2 и r3. У каждого объекта

есть собственные члены данных len и br. Эти объекты схематично представлены на

рис. 4.1.

Рис. 4.1. Схема объектов

Следующая инструкция

r1.setData ( 10, 20 ) ;

вызывает компонентную функцию setData( ). Поскольку setData( ) — это функция-член (или метод) класса Rectangle, она всегда должна обращаться к объекту этого

класса. Это обусловлено тем, что метод можно вызывать лишь относительно заданного объекта.

Операция точки (.) связывает имя объекта и функцию-член. Синтаксис вызова аналогичен тому, как мы ссылаемся на элементы структуры, при этом круглые скобки

указывают на то, что мы обращаемся к функции-члену, а не к полю данных. Оператор «точка» также называется «оператором доступа к члену класса».

Первый вызов метода setData( ):

r1.setData ( 10, 20 ) ;

выполняет функцию-член setData( ) объекта r1. Эта функция присваивает переменным len и br объекта r1 значения 10 и 20 соответственно. Таким же образом

второй вызов setData( ) устанавливает значения переменных во втором объекте.

Метод displayData( ) отображает значения переменных len и br заданного объекта.

Аналогичным образом функция areaPeri( ) вычисляет и выводит на экран площадь

и периметр, используя значения переменных len и br заданного объекта.

92

Глава 4

Если мы попытаемся произвести вызов:

setData ( 10, 20 ) ;

компилятор сообщит об ошибке, т. к. мы не указали, с данными какого объекта мы

собираемся работать.

Классы и конструкторы

В предыдущем разделе мы познакомились с классами. Напомню, класс содержит

члены данных (поля класса) и функции-члены. Давайте продвинемся еще на шаг

вперед. Внимательно изучите следующую программу:

#include <iostream>

using namespace std ;

class Integer

{

private :

int i ;public :

void getData( )

{

cout << endl << "Введите любое целое число " ;cin >> i ;

}

void setData ( int j )

{

i = j ;

}

Integer( ) // конструктор без аргументов

{

}

Integer ( int j ) // конструктор с одним аргументом

{

i = j ;

}

void displayData( )

{

cout << endl << "значение i = " << i << endl ;

}

} ;

int main( )

{

Integer i1 ( 100 ), i2, i3 ;

i1.displayData( ) ;

i2.setData ( 200 ) ;

i2.displayData( ) ;

Классы и объекты

93

i3.getData( ) ;

i3.displayData( ) ;

return 0 ;

}

Наша программа демонстрирует три способа, при помощи которых мы можем

предварительно присвоить значения членам данных объекта:

Первый способ — посредством функции setData( ), которой мы передаем уста-навливаемое значение.

Второй способ — получение значений с клавиатуры, как показано на примере

функции getData( ).

Это подводит нас к третьему методу, в котором используется новый элемент, именуемый конструктором. Это специальная компонентная функция, позволяющая

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

объект.

Назад: Оглавление
Дальше: Деструкторы