Главная страница
Навигация по странице:

  • Каков порядок (см. раздел 6.6.1) преобразований в каждом из следующих обращений

  • Язык программирования C Пятое издание


    Скачать 1.85 Mb.
    НазваниеЯзык программирования C Пятое издание
    Дата15.07.2019
    Размер1.85 Mb.
    Формат файлаpdf
    Имя файла620354-www.libfox.ru.pdf
    ТипДокументы
    #84130
    страница18 из 54
    1   ...   14   15   16   17   18   19   20   21   ...   54
    Упражнение 6.49. Что такое функция-кандидат? Что такое подходящая функция?
    Упражнение 6.50. С учетом приведенных в начале раздела объявлений функции f()
    перечислите подходящие функции для каждого из следующих вызовов. Укажите наилучше соответствие, или если его нет, то из-за отсутствия соответствия или неоднозначности вызова?
    (a) f(2.56, 42) (b) f(42) (с) f(42, 0) (d) f(2.56, 3.14)
    Упражнение 6.51. Напишите все четыре версии функции f(). Каждая из них должна выводить собственное сообщение. Проверьте свои ответы на предыдущее упражнение. Если ответы были неправильными, перечитайте этот раздел и выясните, почему вы ошиблись.
    6.6.1. Преобразование типов аргументов
    Чтобы определить наилучшее соответствие, компилятор ранжирует преобразования,
    применяемые для приведения типа аргумента к типу соответствующего ему параметра.
    Преобразования ранжируются в порядке убывания следующим образом.
    1. Точное соответствие. Типы аргумента и параметра совпадают в случае, если:
    • типы аргумента и параметра идентичны;
    • аргумент преобразуется из типа массива или функции в соответствующий тип указателя.
    (Указатели на функции рассматриваются в разделе 6.7);
    • аргумент отличается наличием или отсутствием спецификатора const верхнего уровня.
    2. Соответствие в результате преобразования констант (см. раздел 4.11.2).
    3. Соответствие в результате преобразования (см. раздел 4.11.1).
    4. Соответствие в результате арифметического преобразования (см. раздел 4.11.1) или преобразования указателя (см. раздел 4.11.2).
    5. Соответствие в результате преобразования класса (раздел 14.9). Соответствие,
    требующее приведения и (или) целочисленного преобразования
    В контексте соответствия функций приведение и преобразование встроенных типов может привести к удивительным результатам. К счастью, в хорошо разработанных системах редко используют функции с параметрами, столь похожими, как в следующих примерах.
    При анализе вызова следует помнить, что малые целочисленные типы всегда преобразуются в тип int или больший целочисленный тип. Рассмотрим две функции, одна из которых получает тип int, а вторая тип short, версия short будет вызвана только со значениями типа short. Даже при том, что меньшие целочисленные значения могли бы быть ближе к соответствию, эти значения преобразуются в тип int, тогда как вызов версии short потребовал бы преобразования: void ff(int);
    Page 316/1103
    void ff(short); ff('a'); // тип char приводится к int, поэтому применяется f(int)
    Все целочисленные преобразования считаются эквивалентными друг другу. Преобразование из типа int в unsigned int, например, не имеет преимущества перед преобразованием типа int в double. Рассмотрим конкретный пример. void manip(long); void manip(float); manip(3.14); // ошибка: неоднозначный вызов
    Литерал 3.14 имеет тип double. Этот тип может быть преобразован или в тип long, или в тип float. Поскольку возможны два целочисленных преобразования, вызов неоднозначен.
    Соответствие функций и константные аргументы
    Когда происходит вызов перегруженной функции, различие между версиями которой заключается в том, указывает ли параметр (или ссылается) на константу, компилятор способен различать, является ли аргумент константным или нет:
    Record lookup(Account&); // функция, получающая ссылку на Account
    Record lookup(const Account&); // новая функция, получающая ссылку на
    // константу const Account а;
    Account b; lookup(а); // вызов lookup(const Account&) lookup(b); // вызов lookup(Account&)
    В первом вызове передается константный объект а. Нельзя связать простую ссылку с константным объектом. В данном случае единственная подходящая функция — версия,
    получающая ссылку на константу. Кроме того, этот вызов точно соответствует аргументу а.
    Во втором вызове передается неконстантный объект b. Для этого вызова подходят обе функции. Аргумент b можно использовать для инициализации ссылки константного или неконстантного типа. Но инициализация ссылки на константу неконстантным объектом требует преобразования. Версия, получающая неконстантный параметр, является точным
    Page 317/1103
    соответствием для объекта b. Следовательно, неконстантная версия предпочтительней.
    Параметры в виде указателя работают подобным образом. Если две функции отличаются только тем, указывает ли параметр на константу или не константу, компилятор на основании константности аргумента вполне может решить, какую версию функции использовать: если аргумент является указателем на константу, то вызов будет соответствовать версии,
    получающей тип const*; в противном случае, если аргумент — указатель на не константу,
    вызывается версия, получающая простой указатель. Упражнения раздела 6.6.1
    Упражнение 6.52. Предположим, что существуют следующие объявления: void manip(int, int); double dobj;

    Каков порядок (см. раздел 6.6.1) преобразований в каждом из следующих обращений?
    (a) manip('a', 'z'); (b) manip(55.4, dobj);
    Упражнение 6.53. Объясните назначение второго объявления в каждом из следующих наборов. Укажите, какие из них (если они есть) недопустимы.
    (a) int calc(int&, int&); int calc(const int&, const int&);
    (b) int calc(char*, char*); int calc(const char*, const char*);
    (c) int calc(char*, char*); int calc(char* const, char* const);
    6.7. Указатели на функции
    Указатель на функцию (function pointer) содержит адрес функции, а не объекта. Подобно любому другому указателю, указатель на функцию имеет вполне определенный тип. Тип функции определен типом ее возвращаемого значения и списком параметров. Имя функции не является частью ее типа.
    // сравнивает длины двух строк bool lengthCompare(const string &, const string &);
    Эта функция имеет тип bool(const string&, const string&). Чтобы объявить указатель,
    способный указывать на эту функцию, достаточно расположить указатель вместо имени функции:
    // pf указывает на функцию, получающую две константные ссылки
    Page 318/1103

    // на строки и возвращающую значение типа bool bool (*pf)(const string &, const string &); // не инициализирован
    Просматривая объявление с начала, можно заметить, что имени pf предшествует знак *,
    следовательно, pf — указатель. Справа расположен список параметров, означая, что pf указывает на функцию. Глядя влево, можно заметить, что возвращаемым типом функции является bool. Таким образом, указатель pf указывает на функцию, которая имеет два параметра типа const string& и возвращает значение типа bool.
    Круглые скобки вокруг части *pf необходимы. Без них получится объявление функции pf(),
    возвращающей указатель на тип bool:
    // объявление функции pf(), возвращающей указатель на тип bool bool *pf(const string &, const string &); Использование указателей на функцию
    При использовании имени функции как значения функция автоматически преобразуется в указатель. Например, адрес функции lengthCompare() можно присвоить указателю pf следующим образом: pf = lengthCompare; // pf теперь указывает на функцию lengthCompare pf = &lengthCompare; // эквивалентное присвоение: оператор обращения к
    // адресу необязателен
    Кроме того, указатель на функцию можно использовать для вызова функции, на которую он указывает. Это можно сделать непосредственно, обращение к значению указателя там не обязательно: bool b1 = pf("hello", "goodbye"); // вызов lengthCompare bool b2 = (*pf)("hello", "goodbye"); // эквивалентный вызов bool b3 = lengthCompare("hello", "goodbye"); // эквивалентный вызов
    Преобразование указателя на один тип функции в указатель на другой тип функции невозможно. Однако для обозначения того, что указатель не указывает на функцию, ему можно присвоить nullptr (см. раздел 2.3.2) или целочисленное константное выражение,
    Page 319/1103
    означающее нуль: string::size_type sumLength(const string&, const string&); bool cstringCompare(const char*, const char*); pf = 0; // ok: pf не указывает на функцию pf = sumLength; // ошибка: разные типы возвращаемого значения pf = cstringCompare; // ошибка: разные типы параметров pf = lengthCompare; // ok: типы функции и указателя совпадают точно Указатели на перегруженные функции
    Как обычно, при использовании перегруженной функции применяемую версию должен прояснить контекст, в котором она используется. Вот объявление указателя на перегруженную функцию: void ff(int*); void ff(unsigned int); void (*pf1)(unsigned int) = ff; // pf1 указывает на ff(unsigned)
    Компилятор использует тип указателя для выявления используемой версии перегруженной функции. Тип указателя должен точно соответствовать одной из версий перегруженной функции: void (*pf2)(int) = ff; // ошибка: нет версии с точно таким списком
    // параметров double (*pf3) (int*) = ff; // ошибка: тип возвращаемого значения
    // функций ff и pf3 не совпадают Указатель на функцию как параметр
    Подобно массивам (см. раздел 6.2.4), нельзя определить параметры типа функции, но можно создать параметр, являющийся указателем на функцию. Как и в случае с массивами, можно создать параметр, который выглядит как тип функции, но обрабатывается как указатель:
    //
    Page 320/1103
    третий параметр имеет тип функции и автоматически обрабатывается как
    // указатель на функцию void useBigger(const string &s1, const string &s2, bool pf(const string&, const string&));
    // эквивалентное объявление: параметр явно определен как указатель
    // на функцию void useBigger(const string &s1, const string &s2, bool (*pf)(const string&, const string&));
    При передаче функции как аргумента это можно сделать непосредственно. Аргумент будет автоматически преобразован в указатель:
    // автоматическое преобразование функции lengthCompare в указатель
    // на нее useBigger(s1, s2, lengthCompare);
    Как можно заметить в объявлении функции useBigger(), написание указателей на тип функций быстро становится утомительным. Псевдонимы типа (см. раздел 2.5.1), а также спецификатор decltype (см. раздел 2.5.3) позволяют упростить код, который использует указатели на функции:
    //
    Func и Func2 имеют тип функции typedef bool Func(const string&, const strings); typedef decltype(lengthCompare) Func2; // эквивалентный тип
    //
    FuncP и FuncP2 имеют тип указателя на функцию typedef bool(*FuncP)(const string&, const string&); typedef decltype(lengthCompare) *FuncP2; // эквивалентный тип
    Page 321/1103

    Здесь при определении типов использовано ключевое слово typedef. И Func, и Func2
    являются типами функций, тогда как FuncP и FuncP2 — типы указателя. Следует заметить,
    что спецификатор decltype возвращает тип функции; автоматического преобразования в указатель не происходит. Поскольку спецификатор decltype возвращает тип функции, при необходимости получить указатель следует добавить символ *. Можно повторно объявить функцию useBigger(), используя любой из этих типов:
    // эквивалентные объявления useBigger с использованием псевдонимов типа void useBigger(const string&, const string&, Func); void useBigger(const string&, const string&, FuncP2);
    Оба объявления объявляют ту же функцию. В первом случае компилятор автоматически преобразует тип функции, представленный именем Func, в указатель. Возвращение указателя на функцию
    Подобно массивам (см. раздел 6.3.3), нельзя возвратить тип функции, но можно возвратить указатель на тип функции. Точно так же тип возвращаемого значения следует писать как тип указателя; компилятор не будет автоматически рассматривать тип возвращаемого значения функции как соответствующий тип указателя. Как и при возвращении массива, безусловно,
    проще всего объявить функцию, которая возвращает указатель на функцию, при помощи псевдонима типа: using F = int(int*, int); //
    F - тип функции, а не указатель using PF = int(*)(int*, int); //
    PF - тип указателя
    Здесь для определения F как типа функции и PF как указателя на тип функции было использовано объявление псевдонима типа (см. раздел 2.5.1). Имейте в виду, что в отличие от параметров, имеющих тип функции, тип возвращаемого значения не преобразуется автоматически в тип указателя. Следует явно определить, что тип возвращаемого значения является типом указателя:
    PF f1(int); // ok: PF - указатель на функцию; f1 возвращает указатель
    // на функцию
    F f1(int); // ошибка: F - тип функции; f1 не может возвратить функцию
    F *f1(int); // ok: явное определение типа возвращаемого значения как
    //
    Page 322/1103
    указателя на функцию
    Конечно, функцию f1() также можно объявить непосредственно: int (*f1(int))(int*, int);
    Читая это объявление изнутри наружу, можно заметить у f1 список параметров, таким образом, f1 — это функция. Имени f1 предшествует знак *, следовательно, функция f1()
    возвращает указатель. У типа самого указателя тоже есть список параметров, таким образом,
    указатель указывает на функцию. Эта функция возвращает тип int.
    И наконец, следует обратить внимание на то, что объявления функций, которые возвращают указатель на функцию, можно упростить при помощи замыкающего типа возвращаемого значения (см. раздел 6.3.3): auto f1(int) -> int (*)(int*, int); Использование спецификаторов auto и decltype для типов указателей на функции
    Если известно, какую функцию (функции) следует возвратить, можно использовать спецификатор decltype для упрощения записи типа возвращаемого значения в виде указателя на функцию. Предположим, например, что имеются две функции, обе возвращают тип string::size_type и имеют два параметра типа const string&. Можно написать третью функцию, которая получает параметр типа string и возвращает указатель на одну из следующих двух функций следующим образом: string::size_type sumLength(const string&, const string&); string::size_type largerLength(const string&, const string&);
    // в зависимости от значения строкового параметра функция getFcn
    // возвращает указатель на sumLength или largerLength decltype(sumLength) *getFcn(const string &);
    Единственная сложность в объявлении функции getFcn() — это необходимость помнить, что при применении спецификатора decltype к функции она возвращает тип функции, а не указатель на тип функции. Чтобы получить указатель, а не функцию, следует добавить знак *.
    Упражнения раздела 6.7
    Упражнение 6.54. Напишите объявление функции, получающей два параметра типа int, и возвращающей тип int. Объявите также вектор, элементами которого является тип указателя на эту функцию.
    Упражнение 6.55. Напишите четыре функции, которые добавляют, вычитают, умножают и делят два значения типа int. Сохраните указатели на эти значения в векторе из предыдущего упражнения.
    Упражнение 6.56. Обратитесь к каждому элементу вектора и выведите результат.
    Резюме
    Функции представляют собой именованные блоки действий, применяемые для структурирования даже небольших программ. При их определении указывают тип
    Page 323/1103
    возвращаемого значения, имя, список параметров (возможно, пустой) и тело функции. Тело функции — это блок операторов, выполняемых при вызове функции. Переданные функции при вызове аргумента должны быть совместимы с типами соответствующих параметров.
    В языке С++ функции могут быть перегружены. То есть одинаковое имя может быть использовано при определении разных функций, отличающихся количеством или типами параметров. На основании переданных при вызове аргументов компилятор автоматически выбирает наиболее подходящую версию функции. Процесс выбора правильной версии из набора перегруженных функций называют подбором функции с наилучшим соответствием.
    Термины
    Автоматический объект (automatic object). Объект, являющийся для функции локальным.
    Автоматические объекты создаются и инициализируются при каждом обращении и удаляются по завершении блока, в котором они были определены.
    Аргумент (argument). Значение, предоставляемое при вызове функции для инициализации соответствующих параметров.
    Аргумент по умолчанию (default argument). Значение, определенное для использования, когда аргумент пропущен при вызове функции.
    Бесконечная рекурсия (recursion loop). Когда у рекурсивной функции отсутствует условие остановки, она вызывает сама себя до исчерпания стека программы.
    Встраиваемая функция (inline function). Функция, тело которой встраивается по месту обращения, если это возможно. Встраиваемые функции позволяют избежать обычных дополнительных затрат, поскольку их вызов заменяет код тела функции.
    Вызов по значению (call by value).
    См . передача по значению.
    Вызов по ссылке (call by reference).
    См . передача по ссылке.
    Исполняемый файл (executable file). Файл, содержащий программный код, который может быть выполнен операционной системой.
    Класс initializer_list. Библиотечный класс, представляющий разделяемый запятыми список объектов одинакового типа, заключенный в фигурные скобки.
    Компоновка (link). Этап компиляции, на котором несколько объектных файлов объединяются в исполняемую программу.
    Локальная переменная (local variable). Переменные, определенные в блоке.
    Локальный статический объект (local static object). Локальный объект, который создается и инициализируется только один раз перед первым вызовом функции, в которой используется ее значение. Значение локального статического объекта сохраняется на протяжении всех вызовов функции.
    Макрос assert. Макрос препроцессора, который получает одно выражение, используемое в
    Page 324/1103
    качестве условия. Если переменная препроцессора NDEBUG не определена, макрос assert проверяет условие. Если оно ложно, макрос assert выводит сообщение и завершает программу.
    Макрос препроцессора (preprocessor macro). Средство препроцессора, ведущее себя как встраиваемая функция. Кроме макроса assert, современные программы С++ очень редко используют макросы препроцессора.
    Наилучшее соответствие (best match). Функция, выбранная для вызова из набора перегруженных версий. Если наилучшее соответствие существует, выбранная функция лучше остальных подходит по крайней мере для одного аргумента вызова и не хуже остальных версий для оставшейся части аргументов.
    Неоднозначный вызов (ambiguous call). Ошибка времени компиляции, происходящая при поиске подходящей функции, когда две или более функции обеспечивают одинаково хорошее соответствие для вызова.
    Объектный код (object code). Формат, в который компилятор преобразует исходный код.
    Объектный файл (object file). Файл, содержащий объектный код, созданный компилятором из предоставленного файла исходного кода. Исполняемый файл создается в результате компоновки одного или нескольких объектных файлов.
    Оператор (). Оператор вызова. Запускает функцию на выполнение. Круглые скобки,
    следующие за именем функции или указателем на функцию, заключают разделяемый запятыми список аргументов, который может быть пуст.
    Отсутствие соответствия (no match). Ошибка времени компиляции, происходящая при поиске подходящей функции, когда не обнаружено ни одной функции с параметрами, которые соответствуют аргументам при данном вызове.
    Параметр (parameter). Локальная переменная, объявляемая в списке параметров функции.
    Параметры инициализируются аргументами, предоставляемыми при каждом вызове функции.
    Перегруженная функция (overloaded function). Функция, которая имеет то же имя, что и по крайней мере одна другая функция. Перегруженные функции должны отличаться по количеству или типу их параметров.
    Передача по значению (pass by value). Способ передачи аргументов параметрам не ссылочного типа. Не ссылочный параметр — это копия значения соответствующего аргумента.
    Передача по ссылке (pass by reference). Способ передачи аргументов параметрам ссылочного типа. Ссылочные параметры работают так же, как и любая другая ссылка;
    параметр связан со своим аргументом.
    Замыкающий тип возвращаемого значения (trailing return type). Тип возвращаемого значения,
    определенный после списка параметров.
    Подбор функции (function matching). Процесс, в ходе которого компилятор ассоциирует вызов функции с определенной версией из набора перегруженных функций. При подборе функции используемые в обращении аргументы сравниваются со списком параметров каждой версии перегруженной функции.
    Подходящая функция (viable function). Подмножество перегруженных функций, которые могли бы соответствовать данному вызову. У подходящих функций количество параметров
    Page 325/1103
    совпадает с количеством переданных при обращении аргументов, а тип каждого аргумента может быть преобразован в тип соответствующего параметра.
    Поиск перегруженной функции (overload resolution).
    См . подбор функции.
    Продолжительность существования объекта (object lifetime). Каждый объект характеризуется своей продолжительностью существования. Нестатические объекты, определенные в блоке,
    существуют от момента их определения и до конца блока, в котором они определены.
    Глобальные объекты создаются во время запуска программы. Локальные статические объекты создаются прежде, чем выполнение впервые пройдет через определение объекта.
    Глобальные объекты и локальные статические объекты удаляются по завершении функции main().
    Прототип функции (function prototype). Синоним объявления функции. В прототипе указано имя, тип возвращаемого значения и типы параметров функции. Чтобы функцию можно было вызвать, ее прототип должен быть объявлен перед точкой обращения.
    Раздельная компиляция (separate compilation). Способность разделить программу на несколько отдельных файлов исходного кода.
    Рекурсивная функция (recursive function). Функция, которая способна вызвать себя непосредственно или косвенно.
    Скрытое имя (hidden name). Имя, объявленное в области видимости, но скрытое ранее объявленной сущностью с тем же именем, объявленным вне этой области видимости.
    Тело функции (function body). Блок операторов, в котором определены действия функции.
    Тип возвращаемого значения (return type). Часть объявления функции, определяющее тип значения, которое возвращает функция.
    Функция constexpr. Функция, способная возвратить константное выражение. Функция constexpr неявно является встраиваемой.
    Функция (function). Именованный блок действий.
    Функция-кандидат (candidate function). Одна из функций набора, рассматриваемая при поиске соответствия вызову функции. Кандидатами считаются все функции, объявленные в области видимости обращения, имя которых совпадает с используемым в обращении.
    Глава 7
    Классы
    Классы в языке С++ используются для определения собственных типов данных.
    Определение типов, отражающих концепции решаемых задач, позволяет существенно упростить написание, отладку и модификацию программ.
    В этой главе будет продолжено описание классов, начатое в главе 2. Основное внимание здесь уделяется важности абстракции данных, позволяющей отделять реализацию объекта от операций, в которых объект может участвовать. В главе 13 будет описано, как
    Page 326/1103
    контролировать происходящее при копировании, перемещении, присвоении и удалении объекта, а в главе 14 рассматривается определение собственных операторов.
    Фундаментальными идеями , лежащими в основе концепции классов (class), являются абстракция данных (data abstraction) и инкапсуляция (encapsulation). Абстракция данных — программный подход, полагающийся на разделение интерфейса (interface) и реализации (implementation). Интерфейс класса состоит из операций, которые пользователь класса может выполнить с его объектом. Реализация включает переменные-члены класса,
    тела функций, составляющих интерфейс, а также любые функции, которые нужны для определения класса, но не предназначены для общего использования.
    Инкапсуляция обеспечивает разделение интерфейса и реализации класса. Инкапсулируемый класс скрывает свою реализацию от пользователей, которые могут использовать интерфейс,
    но не имеют доступа к реализации класса.
    Класс, использующий абстракцию данных и инкапсуляцию, называют абстрактным типом данных (abstract data type). Внутренняя реализация абстрактного типа данных заботит только его разработчика. Программисты, которые используют этот класс, не обязаны ничего знать о том, как внутренне работает этот тип. Они могут рассматривать его как абстракцию .
    7.1. Определение абстрактных типов данных
    Класс Sales_item, использованный в главе 1, является абстрактным типом данных. При использовании объекта класса Sales_item задействовался его интерфейс (т.е. операции,
    описанные в разделе 1.5.1). Мы не имели доступа к переменным-членам, хранящимся в объекте класса Sales_item. На самом деле нам даже не было известно, какие переменные-члены имеет этот класс.
    Наш класс Sales_data (см. раздел 2.6.1) не был абстрактным типом данных. Он позволяет пользователям обращаться к его переменным-членам и вынуждает пользователей писать собственные операции. Чтобы сделать класс Sales_data абстрактным типом, необходимо определить операции, доступные для его пользователей. Как только класс Sales_data определит собственные операции, мы сможем инкапсулировать (т.е. скрыть) его переменные-члены.
    7.1.1. Разработка класса Sales_data
    В конечном счете хочется, чтобы класс Sales_data поддержал тот же набор операций, что и
    Page 327/1103
    класс Sales_item. У класса Sales_item была одна функция-член (member function) (см. раздел 1.5.2) по имени isbn, а также поддерживались операторы +, =, +=, << и >>.
    Определение собственных операторов рассматривается в главе 14, а пока определим обычные (именованные) функции для этих операций. По причинам, рассматриваемым в разделе 7.1.5, функции, осуществляющие сложение и операции ввода-вывода, не будут членами класса Sales_data. Мы определим эти функции как обычные. Функция, выполняющая составное присвоение, будет членом класса, и по причинам, рассматриваемым в разделе
    7.1.5, наш класс не должен определять присвоение.
    Таким образом, интерфейс класса Sales_data состоит из следующих операций.
    • Функция-член isbn(), возвращающая ISBN объекта.
    • Функция-член combine(), добавляющая один объект класса Sales_data к другому.
    • Функция add(), суммирующая два объекта класса Sales_data.
    • Функция read(), считывающая данные из потока istream в объект класса Sales_data.
    • Функция print(), выводящая значение объекта класса Sales_data в поток ostream. Ключевая концепция. Различие в ролях программистов
    Пользователями (user) программисты обычно называют людей, использующих их приложения. Аналогично разработчик класса реализует его для пользователей класса. В данном случае пользователем является другой программист, а не конечный пользователь приложения.
    Когда упоминается пользователь , имеющееся в виду лицо определяет контекст употребления термина. Если речь идет о пользовательском коде или пользователе класса Sales_data, то подразумевается программист, который использует класс. Если речь идет о пользователе приложения книжного магазина, то подразумевается менеджер склада,
    использующий приложение.
    Говоря о пользователях , программисты С++, как правило, имеют в виду как пользователей приложения, так и пользователей класса.
    В простых приложениях пользователь класса вполне может быть и его разработчиком. Но даже в таких случаях имеет смысл различать роли. Разрабатывая интерфейс класса, следует думать о том, чтобы его было проще использовать. При использовании класса не нужно думать, как именно он работает.
    Авторы хороших приложений добиваются успеха потому, что понимают и реализуют потребности пользователей. Точно так же хорошие разработчики класса обращают пристальное внимание на потребности программистов, которые будут использовать их класс.
    У хорошо разработанного класса удобный, интуитивно понятный интерфейс, а его
    Page 328/1103
    реализация достаточно эффективна для решения задач пользователя. Использование пересмотренного класса Sales_data
    Прежде чем думать о реализации нашего класса, обдумаем то, как можно использовать функции его интерфейса. В качестве примера использования этих функций напишем новую версию программы книжного магазина из раздела 1.6, работающую с объектами класса
    Sales_data, а не Sales_item:
    Sales_data total; // переменная для хранения текущей суммы if (read(cin, total)) { // прочитать первую транзакцию
    Sales_data trans; // переменная для хранения данных следующей
    // транзакции while(read(cin, trans)) { // читать остальные транзакции if (total.isbn() == trans.isbn()) // проверить isbn total.combine(trans); // обновить текущую сумму else { print(cout, total) << endl; // отобразить результаты total = trans; // обработать следующую книгу
    }
    } print(cout, total) << endl; // отобразить последнюю транзакцию
    } else { // ввода нет cerr << "No data?!" << endl; //
    Page 329/1103
    уведомить пользователя
    }
    Сначала определяется объект класса Sales_data для хранения текущей суммы. В условии оператора if происходит вызов функции read() для чтения в переменную total первой транзакции. Это условие работает, как и другие написанные ранее циклы с использованием оператора >>. Как и оператор >>, наша функция read() будет возвращать свой потоковый параметр, который и проверяет условие (см. раздел 4.11.2). Если функция read()
    потерпит неудачу, сработает часть else, выводящая сообщение об ошибке.
    Если данные прочитаны успешно, определяем переменную trans для хранения всех транзакций. Условие цикла while также проверяет поток, возвращенный функцией read(). Пока операции ввода в функции read() успешны, условие выполняется и обрабатывается следующая транзакция.
    В цикле while происходит вызов функции-члена isbn() объектов total и trans, возвращающей их
    ISBN. Если объекты total и trans относятся к той же книге, происходит вызов функции combine(), добавляющей компоненты объекта trans к текущей сумме, хранящейся в объекте total. Если объект trans представляет новую книгу, происходит вызов функции print(),
    выводящей итог по предыдущей книге. Поскольку функция print() возвращает ссылку на свой потоковый параметр, ее результат можно использовать как левый операнд оператора <<.
    Это сделано для того, чтобы вывести символ новой строки после результата, созданного функцией print(). Затем объект trans присваивается объекту total, начиная таким образом обработку записи следующей книги в файле.
    По исчерпании ввода следует не забыть вывести данные последней транзакции. Для этого после цикла while используется еще один вызов функции print(). Упражнения раздела 7.1.1
    Упражнение 7.1. Напишите версию программы обработки транзакций из раздела 1.6 с использованием класса Sales_data, созданного для упражнений в разделе 2.6.1.
    7.1.2. Определение пересмотренного класса Sales_data
    У пересмотренного класса будут те же переменные-члены, что и у версии, определенной в разделе 2.6.1: член типа string по имени bookNo, представляющий ISBN, член типа unsigned по имени units_sold, представляющий количество проданных экземпляров книги, и член типа double по имени revenue, представляющий общий доход от этих продаж.
    Как уже упоминалось, у класса будут также две функции-члена, combine() и isbn(). Кроме того,
    предоставим классу Sales_data другую функцию-член, чтобы возвращать среднюю цену, по которой были проданы книги. Эта функция, назовем ее avg_price(), не предназначена для общего использования. Она будет частью реализации, а не интерфейса.
    Функции-члены определяют (см. раздел 6.1) и объявляют (см. раздел 6.1.2) как обычные функции. Функции-члены должны быть объявлены в классе, но определены они могут быть непосредственно в классе или вне тела класса. Функции, не являющиеся членами класса, но являющиеся частью интерфейса, как функции add(), read() и print(), объявляются и определяются вне класса.
    Page 330/1103

    С учетом вышеизложенного напишем пересмотренную версию класса Sales_data: struct Sales_data {
    // новые члены: операции с объектами класса Sales_data std::string isbn() const { return bookNo; }
    Sales_data& combine(const Sales_data&); double avg_price() const;
    // те же переменные-члены, что и в p. 2.6.1 std::string bookNo; unsigned units_sold = 0; double revenue = 0.0;
    };
    // функции интерфейса класса Sales_data, не являющиеся его членами
    Sales_data add(const Sales_data&, const Sales_data&); std::ostream &print(std::ostream&, const Sales_data&); std::istream &read(std::istream&, Sales_data&);
    Функции, определенные в классе, неявно являются встраиваемыми (см. раздел 6.5.2).
    Определение функций-членов
    Хотя каждый член класса должен быть объявлен в самом классе, тело функции-члена можно определить либо в, либо вне тела класса. Функция isbn() определяется в классе Sales_data, а функции combine() и avg_price() вне его.
    Сначала рассмотрим функцию isbn(), возвращающую строку и имеющую пустой список параметров: std::string isbn() const { return bookNo; }
    Как и у любой функции, тело функции-члена является блоком. В данном случае блок содержит один оператор return, возвращающий значение переменной-члена bookNo объекта класса Sales_data. Интересно, как эта функция получает объект, член bookNo которого следует выбрать? Указатель this
    Давайте снова рассмотрим вызов функции-члена isbn(): total.isbn()
    Здесь для вызова функции-члена isbn() объекта total используется точечный оператор (см.
    раздел 4.6).
    Page 331/1103

    За одним исключением, рассматриваемым в разделе 7.6, вызов функции-члена осуществляется от имени объекта. Когда функция isbn() обращается к члену класса
    Sales_data (например, bookNo), она неявно обращается к членам того объекта, из которого была вызвана. В этом вызове функции isbn(), когда она возвращает значение члена bookNo,
    речь идет о члене total.bookNo.
    Функция-член способна обратиться к тому объекту, из которого она была вызвана, благодаря дополнительному неявному параметру this. Когда происходит вызов функции-члена,
    указатель this инициализируется адресом объекта, из которого была вызвана функция.
    Рассмотрим следующий вызов: total.isbn()
    Здесь компилятор присваивает адрес объекта total указателю this и неявно передает его как параметр функции isbn(). Компилятор как бы переписывает этот вызов так:
    // псевдокод, в который преобразуется вызов функции-члена
    Sales_data::isbn(&total)
    Этот код вызывает функцию-член isbn() класса Sales_data, передав адрес объекта total.
    В функции-члене можно обратиться непосредственно к членам объекта, из которого она была вызвана. Для использования членов объекта, на который указывает указатель this, можно не использовать оператор доступа к члену. Любое непосредственное использование члена класса подразумевает использование указателя this. Таким образом, когда функция isbn()
    использует переменную bookNo, она неявно использует член объекта, на который указывает указатель this. Это аналогично синтаксису this->bookNo.
    Параметр this определяется неявно и автоматически. Кроме того, определить параметр или переменную по имени this самому нельзя, но в теле функции-члена его использовать можно.
    Вполне допустимо, хоть и не нужно, определить функцию isbn() так: std::string isbn() const { return this->bookNo; }
    Поскольку указатель this всегда предназначен для обращения к "этому" объекту, он является константным (см. раздел 2.4.2). Нельзя изменить адрес, хранящийся в указателе this.
    Константные функции-члены
    Еще одним важным моментом функции-члена isbn() является ключевое слово const,
    расположенное за списком параметров. Оно применяется для модификации типа неявного указателя this.
    По умолчанию указатель this имеет тип константного указателя на неконстантную версию типа класса. Например, типом по умолчанию указателя this в функции-члене Sales_data является Sales_data *const. Хоть указатель this и неявен, он подчиняется обычным правилам инициализации, согласно которым (по умолчанию) нельзя связать указатель this с константным объектом (см. раздел 2.4.2). Следствием этого факта, в свою очередь, является невозможность вызвать обычную функцию-член для константного объекта.
    Если бы функция isbn() была обычной и если бы указатель this был обычным параметром типа указателя, то мы объявили бы его как const Sales_data *const. В конце концов, тело функции isbn() не изменяет объект, на который указывает указатель this; таким образом, эта функция стала бы гибче, если бы указатель this был указателем на константу (см. раздел
    6.2.3).
    Page 332/1103

    Однако указатель this неявный и не присутствует в списке параметров, поэтому нет места,
    где можно было бы указать, что он должен быть указателем на константу. Язык решает эту проблему, позволяя разместить ключевое слово const после списка параметров функции-члена. Это означает, что указатель this является указателем на константу.
    Функции-члены, использующие ключевое слово const таким образом, являются константными функциями-членами (const member function).
    Тело функции isbn() можно считать написанным так:
    // псевдокод, иллюстрирующий использование неявного указателя
    // этот код недопустим: нельзя самому явно определить этот указатель
    // обратите внимание, что это указатель на константу, поскольку isbn()
    // является константным членом класса std::string Sales_data::isbn(const Sales_data *const this)
    { return this->isbn; }
    Тот факт, что this является указателем на константу, означает, что константные функции-члены не могут изменить объект, для которого они вызваны. Таким образом,
    функция isbn() может читать значения переменных- членов объектов, для которых она вызывается, но не изменять их.
    Константные объекты, ссылки и указатели на константные объекты могут вызывать только константные функции-члены Область видимости класса и функции-члены
    Помните, что класс сам является областью видимости (см. раздел 2.6.1). Определения функций-членов класса находятся в области видимости самого класса. Следовательно,
    использованное функцией isbn() имя bookNo относится к переменной-члену, определенной в классе Sales_data.
    Следует заметить, что функция isbn() может использовать имя bookNo, несмотря на то, что оно определено после функции isbn(). Как будет описано в разделе 7.4.1, компилятор обрабатывает классы в два этапа — сначала объявления членов класса, затем тела функций-членов, если таковые вообще имеются. Таким образом, тела функций-членов могут использовать другие члены своих классов, независимо от того, где именно в классе они определены.Определение функции-члена вне класса
    Подобно любой другой функции, при определении функции-члена вне тела класса ее определение должно соответствовать объявлению. Таким образом, тип возвращаемого значения, список параметров и имя должны совпадать с объявлением в теле класса. Если член класса был объявлен как константная функция, то в определении после списка параметров также должно присутствовать ключевое слово const. Имя функции-члена,
    Page 333/1103
    определенное вне класса, должно включить имя класса, которому она принадлежит: double Sales_data::avg_price() const { if (units_sold) return revenue/units_sold; else return 0;
    }
    Имя функции, Sales data::avg_price(), использует оператор области видимости (см. раздел
    1.2), чтобы указать, что определяемая функция по имени avg_price объявлена в пределах класса Sales_data. Как только компилятор увидит имя функции, остальная часть кода интерпретируется как относящаяся к области видимости класса. Таким образом, когда функция avg_price() обращается к переменным revenue и units_sold, она неявно имеет в виду члены класса Sales_data. Определение функции, возвращающей указатель this на объект
    Функция combine() должна действовать как составной оператор присвоения +=. Объект, для которого вызвана эта функция, представляет собой левый операнд присвоения. Правый операнд передается как аргумент явно:
    Sales_data& Sales_data::combine(const Sales_data &rhs) { units_sold += rhs.units_sold; // добавить члены объекта rhs revenue += rhs.revenue; // к членам объекта 'this' return *this; // возвратить объект, для которого вызвана функция
    }
    Когда наша программа обработки транзакций осуществляет вызов total.combine(trans); // обновить текущую сумму адрес объекта total находится в неявном параметре this, а объект trans связан с параметром rhs. Таким образом, при вызове функции combine() выполняется следующий оператор: units_sold += rhs.units_sold; // добавить члены объекта rhs
    В результате произойдет сложение переменных total.units_sold и trans.units_sold, а сумма должна сохраниться снова в переменной total.units_sold.
    Самым интересным в этой функции является тип возвращаемого значения и оператор return.
    Обычно при определении функции, работающей как стандартный оператор, она должна
    Page 334/1103
    подражать поведению этого оператора. Стандартные операторы присвоения возвращают свой левый операнд как l-значение (см. раздел 144). Чтобы возвратить l-значение, наша функция combine() должна возвратить ссылку (см. раздел 6.3.2). Поскольку левый операнд —
    объект класса Sales_data, тип возвращаемого значения — Sales_data&.
    Как уже упоминалось, для доступа к члену объекта, функция-член которого выполняется,
    необязательно использовать неявный указатель this. Однако для доступа к объекту в целом указатель this действительно нужен: return *this; // возвратить объект, для которого вызвана функция
    Здесь оператор return обращается к значению указателя this, чтобы получить объект, функция которого выполняется. Таким образом, для этого вызова возвращается ссылка на объект total. Упражнения раздела 7.1.2
    Упражнение 7.2. Добавьте функции-члены combine() и isbn() в класс Sales_data, который был написан для упражнений из раздела 2.6.2.
    Упражнение 7.3. Пересмотрите свою программу обработки транзакций из раздела 7.1.1 так,
    чтобы использовать эти функции-члены.
    Упражнение 7.4. Напишите класс по имени Person, представляющий имя и адрес человека.
    Используйте для содержания каждого из этих членов тип string. В последующих упражнениях в этот класс будут добавлены новые средства.
    Упражнение 7.5. Снабдите класс Person операциями возвращения имени и адреса. Должны ли эти функции быть константами? Объясните свой выбор.
    7.1.3. Определение функций, не являющихся членом класса, но связанных с ним
    Авторы классов нередко определяют такие вспомогательные функции, как наши функции add(), read() и print(). Хотя определяемые ими операции концептуально являются частью интерфейса класса, частью самого класса они не являются.
    Мы определяем функции, не являющиеся членом класса, как любую другую функцию, т.е. ее объявление обычно отделено от определения (см. раздел 6.1.2). Функции, концептуально являющиеся частью класса, но не определенные в нем, как правило, объявляются (но не определяются) в том же заголовке, что и сам класс. Таким образом, чтобы использовать любую часть интерфейса, пользователю достаточно включить только один файл.
    Обычно функция, не являющаяся членом класса, но из состава его интерфейса объявляется в том же заголовке, что и сам класс. Определение функций read() и print()
    Функции read() и print() выполняют ту же задачу, что и код в разделе 2.6.2, поэтому и не удивительно, что тела этих функций очень похожи на код, представленный там:
    // введенные транзакции содержат ISBN, количество проданных книг и
    //
    Page 335/1103
    цену книги istream &read(istream &is, Sales_data &item) { double price = 0; is >> item.bookNo >> item.units_sold >> price; item.revenue = price * item.units_sold; return is;
    } ostream &print(ostream &os, const Sales_data &item) { os << item.isbn() << " " << item.units_sold << " "
    << item.revenue << " " << item.avg_price(); return os;
    }
    Функция read() читает данные из предоставленного потока в заданный объект. Функция print()
    выводит содержимое предоставленного объекта в заданный поток.
    В этих функциях, однако, следует обратить внимание на два момента. Во- первых, обе функции получают ссылки на соответствующие объекты классов ввода и вывода. Классы ввода и вывода — это типы, не допускающие копирования, поэтому их передача возможна только по ссылке (см. раздел 6.2.2). Кроме того, чтение и запись в поток изменяют его,
    поэтому обе функции получают обычные ссылки, а не ссылки на константы.
    Второй заслуживающий внимания момент: функция print() не выводит новую строку. Обычно функции вывода осуществляют минимальное форматирование. Таким образом,
    пользовательский код может сам решить, нужна ли новая строка. Определение функции add()
    Функция add() получает два объекта класса Sales_data и возвращает новый объект класса
    Sales_data, представляющий их сумму:
    Sales_data add(const Sales_data &lhs, const Sales_data &rhs) {
    Sales_data sum = lhs; // копирование переменных-членов из lhs в sum sum.combine(rhs); // добавить переменные-члены rhs в sum return sum;
    }
    В теле функции определяется новый объект класса Sales_data по имени sum,
    предназначенный для хранения суммы двух транзакций. Инициализируем объект sum копией
    Page 336/1103
    объекта lhs. По умолчанию копирование объекта класса подразумевает копирование и членов этого объекта. После копирования у членов bookNo, units_sold и revenue объекта sum будут те же значения, что и у таковых объекта lhs. Затем происходит вызов функции combine(),
    суммирующей значения переменных-членов units_sold и revenue объектов rhs и sum в последний. По завершении возвращается копия объекта sum. Упражнения раздела 7.1.3
    Упражнение 7.6. Определите собственные версии функций add(), read() и print().
    Упражнение 7.7. Перепишите программу обработки транзакций, написанной для упражнений в разделе 7.1.2, так, чтобы использовать эти новые функции.
    Упражнение 7.8. Почему функция read() определяет свой параметр типа Sales_data как простую ссылку, а функция print() — как ссылку на константу?
    Упражнение 7.9. Добавьте в код, написанный для упражнений в разделе 7.1.2, операции чтения и вывода объектов класса Person.
    Упражнение 7.10. Что делает условие в следующем операторе if? if (read(read(cin, data1), data2))
    7.1.4. Конструкторы
    Каждый класс определяет, как могут быть инициализированы объекты его типа. Класс контролирует инициализацию объекта за счет определения одной или нескольких специальных функций-членов, известных как конструкторы (constructor). Задача конструктора
    — инициализировать переменные-члены объекта класса. Конструктор выполняется каждый раз, когда создается объект класса.
    В этом разделе рассматриваются основы определения конструкторов. Конструкторы —
    удивительно сложная тема. На самом деле мы сможем больше сказать о конструкторах в разделах 7.5, 15.7 и 18.1.3, а также в главе 13.
    Имя конструкторов совпадает с именем класса. В отличие от других функций, у конструкторов нет типа возвращаемого значения. Как и другие функции, конструкторы имеют (возможно пустой) список параметров и (возможно пустое) тело. У класса может быть несколько конструкторов. Подобно любой другой перегруженной функции (см. раздел 6.4), конструкторы должны отличаться друг от друга количеством или типами своих параметров.
    В отличие от других функций-членов, конструкторы не могут быть объявлены константами
    (см. раздел 7.1.2). При создании константного объекта типа класса его константность не проявится, пока конструктор не закончит инициализацию объекта. Таким образом,
    конструкторы способны осуществлять запись в константный объект во время его создания.
    Синтезируемый стандартный конструктор
    Хотя в нашем классе Sales_data не определено конструкторов, использующие его программы компилировались и выполнялись правильно. Например, программа из раздела 7.1.1
    определяла два объекта класса Sales_data:
    Sales_data total; // переменная для хранения текущей суммы
    Page 337/1103

    Sales_data trans; // переменная для хранения данных следующей
    // транзакции

    1   ...   14   15   16   17   18   19   20   21   ...   54


    написать администратору сайта