Главная страница

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


Скачать 1.85 Mb.
НазваниеЯзык программирования C Пятое издание
Дата15.07.2019
Размер1.85 Mb.
Формат файлаpdf
Имя файла620354-www.libfox.ru.pdf
ТипДокументы
#84130
страница6 из 54
1   2   3   4   5   6   7   8   9   ...   54
Page 87/1103

2.5.1. Псевдонимы типов
Псевдоним типа (type alias) — это имя, являющееся синонимом имени другого типа.
Псевдонимы типа позволяют упростить сложные определения типов, облегчая их использование. Псевдонимы типа позволяют также подчеркивать цель использования типа.
Определить псевдоним типа можно одним из двух способов. Традиционно он определяется при помощи ключевого слова typedef: typedef double wages; // wages - синоним для double typedef wages base, *p; // base - синоним для double, a p - для double*
Ключевое слово typedef может быть частью базового типа в объявлении (см. раздел 2.3).
Объявления, включающие ключевое слово typedef, определяют псевдонимы типа, а не переменные. Как и в любое другое объявление, в это можно включать модификаторы типа,
которые определяют составные типы, включающие базовый тип.
Новый стандарт вводит второй способ определения псевдонима типа при помощи объявления псевдонима (alias declaration) и знака =. using SI = Sales_item; //
SI - синоним для Sales_item
Объявление псевдонима задает слева от оператора = имя псевдонима типа, который расположен справа.
Псевдоним типа — это имя типа, оно может присутствовать везде, где присутствует имя типа.
wages hourly, weekly; // то же, что и double hourly, weekly;
SI item; // то же, что и Sales_item item Указатели, константы и псевдонимы типа
Объявления, использующие псевдонимы типа, представляющие составные типы и константы,
могут приводить к удивительным результатам. Например, следующие объявления используют тип pstring, который является псевдонимом для типа char*. typedef char *pstring; const pstring cstr = 0; // cstr - константный указатель на char const pstring *ps; //
Page 88/1103
ps - указатель на константный указатель
// на тип char
Базовым типом в этих объявлениях является const pstring. Как обычно, модификатор const в базовом типе модифицирует данный тип. Тип pstring — это указатель на тип char, a const pstring — это константный указатель на тип char, но не указатель на тип const char.
Заманчиво, хоть и неправильно, интерпретировать объявление, которое использует псевдоним типа как концептуальную замену псевдонима, соответствующим ему типом: const char *cstr = 0; // неправильная интерпретация const pstring cstr
Однако эта интерпретация неправильна. Когда используется тип pstring в объявлении,
базовым типом объявления является тип указателя. При перезаписи объявления с использованием char*, базовым типом будет char, а * будет частью оператора объявления. В
данном случае базовый тип — это const char. Перезапись объявляет cstr указателем на тип const char, а не константным указателем на тип char.
2.5.2. Спецификатор типа auto
Нет ничего необычного в желании сохранить значение выражения в переменной. Чтобы объявить переменную, нужно знать тип этого выражения. Когда мы пишем программу, может быть на удивление трудно (а иногда даже невозможно) определить тип выражения. По новому стандарту можно позволить компилятору самому выяснять этот тип. Для этого используется спецификатор типа auto. В отличие от таких спецификаторов типа, как double, задающих определенный тип, спецификатор auto приказывает компилятору вывести тип из инициализатора. Само собой разумеется, у переменной, использующей спецификатор типа auto, должен быть инициализатор.
// тип item выводится из типа результата суммы val1 и val2 auto item = val1 + val2; // item инициализируется результатом val1 + val2
Здесь компилятор выведет тип переменной item из типа значения, возвращенного при применении оператора + к переменным val1 и val2. Если переменные val1 и val2 — объекты класса Sales_item (см. раздел 1.5), типом переменной item будет класс Sales_item. Если эти переменные имеют тип double, то у переменной item будет тип double и т.д.
Подобно любому другому спецификатору типа, используя спецификатор auto, можно определить несколько переменных. Поскольку объявление может задействовать только один базовый тип, у инициализаторов всех переменных в объявлении должны быть типы,
совместимые друг с другом.
Page 89/1103
auto i = 0, *p = &i; // ok: i - int, а p - указатель на int auto sz = 0, pi = 3.14; // ошибка: несовместимые типы у sz и pi Составные типы, const и auto
Выводимый компилятором тип для спецификатора auto не всегда точно совпадает с типом инициализатора. Компилятор корректирует тип так, чтобы он соответствовал обычным правилам инициализации.
Во-первых, как уже упоминалось, при использовании ссылки в действительности используется объект, на который она ссылается. В частности, при использовании ссылки как инициализатора им является соответствующий объект. Компилятор использует тип этого объекта для выведения типа auto. int i = 0, &r = i; auto a = r; // a - int (r - псевдоним для i, имеющий тип int)
Во-вторых, выведение типа auto обычно игнорирует спецификаторы const верхнего уровня
(см. раздел 2.4.3). Как обычно в инициализациях, спецификаторы const нижнего уровня учитываются в случае, когда инициализатор является указателем на константу. const int ci = i, &cr = ci; auto b = ci; // b - int (const верхнего уровня в ci отброшен) auto с = cr; // с - int (cr - псевдоним для ci с const верхнего
// уровня) auto d = &i; // d - int* (& объекта int - int*) auto e = &ci; // e - const int* (& константного объекта - const нижнего
// уровня)
Если необходимо, чтобы у выведенного типа был спецификатор const верхнего уровня, его следует указать явно. const auto f = ci; // выведенный тип ci - int; тип f - const int
Page 90/1103

Можно также указать, что необходима ссылка на автоматически выведенный тип. Обычные правила инициализации все еще применимы. auto &g = ci; // g - const int&, связанный с ci auto &h = 42; // ошибка: нельзя связать простую ссылку с литералом const auto &j = 42; // ok: константную ссылку с литералом связать можно
Когда запрашивается ссылка на автоматически выведенный тип, спецификаторы const верхнего уровня в инициализаторе не игнорируются. Как обычно при связывании ссылки с инициализатором, спецификаторы const не относятся к верхнему уровню.
При определении нескольких переменных в том же операторе важно не забывать, что ссылка или указатель — это часть специфического оператора объявления, а не часть базового типа объявления. Как обычно, инициализаторы должны быть совместимы с автоматически выведенными типами: auto k = ci, &l = i; // k - int; l - int& auto &m = ci, *p = &ci; // m - const int&; p - указатель на const int
// ошибка: выведение типа из i - int;
// тип, выведенный из &ci - const int auto &n = i, *p2 = &ci; Упражнения раздела 2.5.2
Упражнение 2.33. С учетом определения переменных из этого раздела укажите то, что происходит в каждом из этих присвоений. а = 42; b = 42; с = 42; d = 42; е = 42; g = 42;
Упражнение 2.34. Напишите программу, содержащую переменные и присвоения из предыдущего упражнения. Выведите значения переменных до и после присвоений, чтобы проверить правильность предположений в предыдущем упражнении. Если они неправильны,
изучите примеры еще раз и выясните, что привело к неправильному заключению.
Упражнение 2.35. Укажите типы, выведенные в каждом из следующих определений. Затем напишите программу, чтобы убедиться в своей правоте. const int i = 42;
Page 91/1103
auto j = i; const auto &k = i; auto *p = &i; const auto j2 = i, &k2 = i;
2.5.3. Спецификатор типа decltype
Иногда необходимо определить переменную, тип которой компилятор выводит из выражения,
но не использовать это выражение для инициализации переменной. Для таких случаев новый стандарт вводит спецификатор типа decltype, возвращающий тип его операнда. Компилятор анализирует выражение и определяет его тип, но не вычисляет его результат. decltype(f()) sum = x; // sum имеет тот тип,
// который возвращает функция f
Здесь компилятор не вызывает функцию f(), но он использует тип, который возвратил бы такой вызов для переменной sum. Таким образом, компилятор назначает переменной sum тот же тип, который был бы возвращен при вызове функции f().
Таким образом, спецификатор decltype учитывает спецификатор const верхнего уровня и ссылки, но несколько отличается от того, как работает спецификатор auto. Когда выражение,
к которому применен спецификатор decltype, является переменной, он возвращает тип этой переменной, включая спецификатор const верхнего уровня и ссылки. const int ci = 0, &cj = ci; decltype(ci) x = 0; // x имеет тип const int decltype(cj) y = x; // y имеет тип const int& и связана с x decltype(сj) z; // ошибка: z - ссылка, она должна быть инициализирована
Поскольку cj — ссылка, decltype (cj) — ссылочный тип. Как и любую другую ссылку, ссылку z следует инициализировать.
Следует заметить, что спецификатор decltype — единственный контекст, в котором переменная определена, поскольку ссылка не рассматривается как синоним объекта, на который она ссылается. Спецификатор decltype и ссылки
Когда спецификатор decltype применяется к выражению, которое не является переменной,
получаемый тип соответствует типу выражения. Как будет продемонстрировано в разделе
4.1.1, некоторые выражения заставят спецификатор decltype возвращать ссылочный тип. По правде говоря, спецификатор decltype возвращает ссылочный тип для выражений,
результатом которых являются объекты, способные стоять слева от оператора присвоения.
Page 92/1103

// decltype выражение может быть ссылочным типом int i = 42, *p = &i, &r = i; decltype(r + 0) b; // ok: сложение возвращает тип int; b имеет тип int
//
(не инициализирована) decltype(*p) с; // ошибка: с имеет тип int& и требует инициализации
Здесь r — ссылка, поэтому decltype(r) возвращает ссылочный тип. Если необходим тип, на который ссылается ссылка r, можно использовать ее в таком выражении, как r + 0, поскольку оно возвращает значение не ссылочного типа.
С другой стороны, оператор обращения к значению — пример выражения, для которого спецификатор decltype возвращает ссылку. Как уже упоминалось, при обращении к значению указателя возвращается объект, на который он указывает. Кроме того, этому объекту можно присвоить значение. Таким образом, decltype(*p) выведет тип int&, а не просто int.
Еще одно важное различие между спецификаторами decltype и auto в том, что выведение,
осуществляемое спецификатором decltype, зависит от формы данного выражения . Не всегда понимают то, что включение имени переменной в круглые скобки влияет на тип, возвращаемый спецификатором decltype. При применении спецификатора decltype к переменной без круглых скобок получается тип этой переменной. Если заключить имя переменной в одни или несколько круглых скобок, то компилятор будет рассматривать операнд как выражение. Переменная — это выражение,
которое способно быть левым операндом присвоения. В результате спецификатор decltype для такого выражения возвратит ссылку.
// decltype переменной в скобках - всегда ссылка decltype((i)) d; // ошибка: d - int& и должна инициализироваться decltype(i) e; // ok: e имеет тип int (не инициализирована)
Помните, что спецификатор decltype(( переменная )) (обратите внимание на парные круглые скобки) всегда возвращает ссылочный тип, а спецификатор decltype( переменная ) возвращает ссылочный тип, только если
Page 93/1103
переменная является ссылкой.Упражнения раздела 2.5.3
Упражнение 2.36. Определите в следующем коде тип каждой переменной и значения,
которые будет иметь каждая из них по завершении. int а = 3, b = 4; decltype(а) с = а; decltype((b)) d = а;
++c;
++d;
Упражнение 2.37. Присвоение — это пример выражения, которое возвращает ссылочный тип.
Тип — это ссылка на тип левого операнда. Таким образом, если переменная i имеет тип int, то выражение i = x имеет тип int&. С учетом этого определите тип и значение каждой переменной в следующем коде: int а = 3, b = 4; decltype(а) с = а; decltype(а = b) d = а;
Упражнение 2.38. Опишите различия выведения типа спецификаторами decltype и auto.
Приведите пример выражения, где спецификаторы auto и decltype выведут тот же тип, и пример, где они выведут разные типы.
2.6. Определение собственных структур данных
На самом простом уровне структура данных (data structure) — это способ группировки взаимосвязанных данных и стратегии их использования. Например, класс Sales_item группирует ISBN книги, количество проданных экземпляров и выручку от этой продажи. Он предоставляет также набор операций,
таких как функция isbn() и операторы >>, <<, + и +=.
В языке С++ мы создаем собственные типы данных, определяя класс. Такие библиотечные типы, как string, istream и ostream, определены как классы, подобно типу Sales_item в главе 1.
Поддержка классов в языке С++ весьма обширна, фактически части III и IV в значительной степени посвящены описанию средств, связанных с классами. Хотя класс Sales_item довольно прост, мы не сможем определить его полностью, пока не узнаем в главе 14, как писать собственные операторы.
2.6.1 Определение типа Sales_data
Несмотря на то что мы еще не можем написать свой класс Sales_item полностью, уже вполне можно создать достаточно реалистичный класс, группирующий необходимые элементы
Page 94/1103
данных. Стратегия использования этого класса заключается в том, что пользователи будут получать доступ непосредственно к элементам данных и смогут самостоятельно реализовать необходимые операции.
Поскольку создаваемая структура данных не поддерживает операций, назовем новую версию
Sales_data, чтобы отличать ее от типа Sales_item. Определим класс следующим образом: struct Sales_data { std::string bookNo; unsigned units_sold = 0; double revenue = 0.0;
};
Класс начинается с ключевого слова struct, сопровождаемого именем класса и (возможно пустым) телом класса. Тело класса заключено в фигурные скобки и формирует новую область видимости (см. раздел 2.2.4). Определенные в классе имена должны быть уникальны в пределах класса, но вне класса они могут повторяться.
Ближайшие фигурные скобки, заключающие тело класса, следует сопроводить точкой с запятой. Точка с запятой необходима, так как после тела класса можно определить переменные: struct Sales_data { /* ... */ } accum, trans, *salesptr;
// эквивалентно, но лучше определять эти объекты так struct Sales_data { /* ... */ };
Sales data accum, trans, *salesptr;
Точка с запятой отмечает конец (обычно пустого) списка объявления операторов. Обычно определение объекта в составе определения класса — это не лучшая идея. Объединение в одном операторе определений двух разных сущностей (класса и переменной) ухудшает читабельность кода.
Забытая точка с запятой в конце определения класса — довольно распространенная ошибка начинающих программистов. Переменные-члены класса
В теле класса определены члены (member) класса. У нашего класса есть только переменные-члены (data member). Переменные-члены класса определяют содержимое объектов этого класса. Каждый объект обладает собственным экземпляром переменных-членов класса. Изменение переменных-членов одного объекта не изменяет данные в любом другом объекте класса Sales_data.
Переменные-члены определяют точно так же, как и обычные переменные: указывается базовый тип, затем список из одного или нескольких операторов объявления. У нашего класса будут три переменные-члены: член типа string по имени bookNo, член типа unsigned по имени units_sold и член типа double по имени revenue. Эти три переменные-члены будут у каждого объекта класса Sales_data.
Page 95/1103

По новому стандарту переменной-члену можно предоставить внутриклассовый инициализатор (in-class initializer). Он используется для инициализации переменных-членов при создании объектов. Члены без инициализатора инициализируются по умолчанию (см. раздел 2.2.1). Таким образом, при определении объектов класса Sales_data переменные-члены units_sold и revenue будут инициализированы значением 0, а переменная-член bookNo — пустой строкой.
Внутриклассовые инициализаторы ограничены формой их использования (см. раздел 2.2.1):
они должны либо быть заключены в фигурные скобки, либо следовать за знаком =. Нельзя определить внутриклассовый инициализатор в круглых скобках.
В разделе 7.2 указано, что язык С++ обладает еще одним ключевым словом, class, также используемым для определения собственной структуры данных. В этом разделе используем ключевое слово struct, поскольку пока еще не рассмотрены приведенные в главе 7
дополнительные средства, связанные с классом. Упражнения раздела 2.6.1
Упражнение 2.39. Откомпилируйте следующую программу и посмотрите, что будет, если не поставить точку с запятой после определения класса. Запомните полученное сообщение,
чтобы узнать его в будущем. struct Foo { /* пусто */ } //
Примечание: нет точки с запятой int main() { return 0;
}
Упражнение 2.40. Напишите собственную версию класса Sales_data.
2.6.2. Использование класса Sales_data
В отличие от класса Sales_item, класс Sales_data не поддерживает операций. Пользователи класса Sales_data должны сами писать все операции, в которых они нуждаются. В качестве примера напишем новую версию программы из раздела 1.5.2, которая выводила сумму двух транзакций. Программа будет получать на входе такие транзакции:
0-201-78345-X 3 20.00 0-201-78345-X 2 25.00
Каждая транзакция содержит ISBN, количество проданных книг и цену, по которой была продана каждая книга. Суммирование двух объектов класса Sales_data
Поскольку класс Sales_data не предоставляет операций, придется написать собственный код,
осуществляющий ввод, вывод и сложение. Будем подразумевать, что класс Sales_data определен в заголовке Sales_data.h. Определение заголовка рассмотрим в разделе 2.6.3.
Так как эта программа будет длиннее любой, написанной до сих пор, рассмотрим ее по частям. В целом у программы будет следующая структура:
Page 96/1103

#include <iostream>
#include <string>
#include "Sales_data.h" int main() {
Sales_data data1, data2;
// код чтения данных в data1 и data2
// код проверки наличия у data1 и data2 одинакового ISBN
// если это так, то вывести сумму data1 и data2
}
Как и первоначальная программа, эта начинается с включения заголовков, необходимых для определения переменных, содержащих ввод. Обратите внимание, что, в отличие от версии
Sales_item, новая программа включает заголовок string. Он необходим потому, что код должен манипулировать переменной-членом bookNo типа string. Чтение данных в объект класса Sales_data
Хотя до глав 3 и 10 мы не будем описывать библиотечный тип string подробно, упомянем пока лишь то, что необходимо знать для определения и использования члена класса,
содержащего ISBN. Тип string содержит последовательность символов. Он имеет операторы
>>, << и == для чтения, записи и сравнения строк соответственно. Этих знаний достаточно для написания кода чтения первой транзакции. double price = 0; // цена за книгу, используемая для вычисления
// общей выручки
// читать первую транзакцию:
//
ISBN, количество проданных книг, цена книги std::cin >> data1.bookNo >> data1.units_sold >> price;
// вычислить общий доход из price и units_sold data1.revenue = data1.units_sold * price;
1   2   3   4   5   6   7   8   9   ...   54


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