Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
Упражнение 2.26. Что из приведенного ниже допустимо? Если что-то недопустимо, то почему? (a) const int buf; (b) int cnt = 0; (c) const int sz = cnt; (d) ++cnt; ++sz; 2.4.1. Ссылка на константу Подобно любым другим объектам, с константным объектом можно связать ссылку. Для этого используется Page 77/1103 ссылка на константу (reference to const), т.е. ссылка на объект типа const. В отличие от обычной ссылки, ссылку на константу нельзя использовать для изменения объекта, с которым она связана. const int ci = 1024; const int &r1 = ci; // ok: и ссылка, и основной объект - константы r1 = 42; // ошибка: r1 - ссылка на константу int &r2 = ci; // ошибка: неконстантная ссылка на константный объект Поскольку нельзя присвоить значение самой переменной ci, ссылка также не должна позволять изменять ее. Поэтому инициализация ссылки r2 — это ошибка. Если бы эта инициализация была допустима, то ссылку r2 можно было бы использовать для изменения значения ее основного объекта. Терминология. Константная ссылка — это ссылка на константу Программисты С++, как правило, используют термин константная ссылка (const reference), однако фактически речь идет о ссылке на константу (reference to const). С технической точки зрения нет никаких константных ссылок. Ссылка — не объект, поэтому саму ссылку нельзя сделать константой. На самом деле, поскольку нет никакого способа заставить ссылку ссылаться на другой объект, то в некотором смысле все ссылки — константы. То, что ссылка ссылается на константный или неконстантный тип, относится к тому, что при помощи этой ссылки можно сделать, однако привязку самой ссылки изменить нельзя в любом случае. Инициализация и ссылки на константу В разделе 2.1.2 мы обращали ваше внимание на два исключения из правила, согласно которому тип ссылки должен совпадать с типом объекта, на который она ссылается. Первое исключение: мы можем инициализировать ссылку на константу результатом выражения, тип которого может быть преобразован (см. раздел 2.1.2) в тип ссылки. В частности, мы можем связать ссылку на константу с неконстантным объектом, литералом или более общим выражением: int i = 42; const int &r1 = i; // можно связать ссылку const int& с обычным // объектом int const int &r2 =42; // ok: r1 - ссылка на константу Page 78/1103 const int &r3 = r1 * 2; // ok: r3 - ссылка на константу int &r4 = r * 2; // ошибка: r4 - простая, неконстантная ссылка Простейший способ понять это различие в правилах инициализации — рассмотреть то, что происходит при связывании ссылки с объектом другого типа: double dval = 3.14; const int &ri = dval; Здесь ссылка ri ссылается на переменную типа int. Операции со ссылкой ri будут целочисленными, но переменная dval содержит число с плавающей запятой, а не целое число. Чтобы удостовериться в том, что объект, с которым связана ссылка ri, имеет тип int, компилятор преобразует этот код в нечто следующее: const int temp = dval; // создать временную константу типа int из // переменной типа double const int &ri = temp; // связать ссылку ri с временной константой В данном случае ссылка ri связана с временным объектом (temporary). Временный объект — это безымянный объект, создаваемый компилятором для хранения промежуточного результата вычисления. Программисты С++ зачастую используют слово "temporary" как сокращение термина "temporary object". Теперь рассмотрим, что могло бы произойти, будь инициализация позволена, но ссылка ri не была бы константной. В этом случае мы могли бы присвоить значение по ссылке ri. Это изменило бы объект, с которым связана ссылка ri. Этот временный объект имеет тип не dval. Программист, заставивший ссылку ri ссылаться на переменную dval, вероятно, ожидал, что присвоение по ссылке ri изменит переменную dval. В конце концов, почему произошло присвоение по ссылке ri, если не было намерения изменять объект, с которым она связана? Поскольку связь ссылки с временным объектом осуществляет уж конечно не то, что подразумевал программист, язык считает это некорректным. Ссылка на константу может ссылаться на неконстантный объект Важно понимать, что ссылка на константу ограничивает только то, что при помощи этой ссылки можно делать. Привязка ссылки к константному объекту ничего не говорит о том, является ли сам основной объект константой. Поскольку основной объект может оказаться неконстантным, он может быть изменен другими способами: int i = 42; int &r1 = i; // r1 связана с i Page 79/1103 const int &r2 = i; // r2 тоже связана с i; // но она не может использоваться для изменения i r1 = 0; // r1 - неконстантна; i теперь 0 r2 = 0; // ошибка: r2 - ссылка на константу Привязка ссылки r2 к неконстантной переменной i типа int вполне допустима. Но ссылку r2 нельзя использовать для изменения значения переменной i. Несмотря на это, значение переменной i вполне можно изменить другим способом, Например, можно присвоить ей значение непосредственно или при помощи другой связанной с ней ссылки, такой как r1. 2.4.2. Указатели и спецификатор const Подобно ссылкам, вполне возможно определять указатели, которые указывают на объект константного или неконстантного типа. Как и ссылку на константу (см. раздел 2.4.1), указатель на константу (pointer to const) невозможно использовать для изменения объекта, на который он указывает. Адрес константного объекта можно хранить только в указателе на константу: const double pi = 3.14; // pi - константа; ее значение неизменно double *ptr = π // ошибка: ptr - простой указатель const double *cptr = π // ok: cptr может указывать на тип // const double *cptr = 42; // ошибка: нельзя присвоить *cptr В разделе 2.3.2 упоминалось о наличии двух исключений из правила, согласно которому типы указателя и объекта, на который он указывает, должны совпадать. Первое исключение — это возможность использования указателя на константу для указания на неконстантный объект: double dval = 3.14; // Page 80/1103 dval типа double; ее значение неизменно cptr = &dval; // ok: но изменить dval при помощи cptr нельзя Подобно ссылке на константу, указатель на константу ничего не говорит о том, является ли объект, на который он указывает, константой. Определение указателя как указателя на константу влияет только на то, что с его помощью можно сделать. Не забывайте, что нет никакой гарантии того, что объект, на который указывает указатель на константу, не будет изменяться. Возможно, указатели и ссылки на константы следует рассматривать как указатели или ссылки, "которые полагают , что они указывают или ссылаются на константы".Константные указатели В отличие от ссылок, указатели — это объекты. Следовательно, подобно любым другим объектам, вполне может быть указатель, сам являющийся константой. Как и любой другой константный объект, константный указатель следует инициализировать, после чего изменить его значение (т.е. адрес, который он содержит) больше нельзя. Константный указатель объявляют, расположив ключевое слово const после символа *. Это означает, что данный указатель является константой, а не обычным указателем на константу. int errNumb = 0; int *const curErr = &errNumb; // curErr всегда будет указывать на errNumb const double pi = 3.14159; const double *const pip = π // pip константный указатель на // константный объект Как уже упоминалось в разделе 2.3.3, проще всего понять эти объявления, читая их справа налево. В данном случае ближе всего к имени curErr расположен спецификатор const, означая, что сам объект curErr будет константным. Тип этого объекта формирует остальная часть оператора объявления. Следующий символ оператора объявления, *, означает, что curErr — это константный указатель. И наконец, объявление завершает базовый тип, означая, что curErr — это константный указатель на объект типа int. Аналогично pip — это константный указатель на объект типа const double. Тот факт, что указатель сам является константой, ничто не говорит о том, можем ли мы использовать указатель для изменения основного объекта. Возможность изменения объекта полностью зависит от типа, на который указывает указатель. Например, pip — это константный указатель на константу. Ни значение объекта, на который указывает указатель pip, ни хранящийся в нем адрес не могут быть изменены. С другой стороны, указатель curErr имеет простой, неконстантный тип int. Указатель curErr можно использовать для изменения значения переменной errNumb: *pip = 2.72; // Page 81/1103 ошибка: pip - указатель на константу // если значение объекта, на который указывает указатель curErr // (т.е. errNumb), отлично от нуля if (*curErr) { errorHandler(); *curErr = 0; // обнулить значение объекта, на который // указывает указатель curErr } Упражнения раздела 2.4.2 Упражнение 2.27. Какие из следующих инициализаций допустимы? Объясните почему. (a) int i = -1, &r = 0; (b) int *const p2 = &i2; (c) const int i = -1, &r = 0; (d) const int *const p3 = &i2; (e) const int *p1 = &i2; (f) const int &const r2; (g) const int i2 = i, &r = i; Упражнение 2.28. Объясните следующие определения. Какие из них недопустимы? (a) int i, *const cp; (b) int *p1, *const p2; (c) const int ic, &r = ic; (d) const int *const p3; (e) const int *p; Упражнение 2.29. С учетом переменных из предыдущих упражнений, какие из следующих присвоений допустимы? Объясните почему. (a) i = ic; (b) pi = p3; (с) pi = ⁣ (d) p3 = ⁣ (e) p2 = pi; (f) ic = *p3; 2.4.3. Спецификатор const верхнего уровня Как уже упоминалось, указатель — это объект, способный указывать на другой объект. В результате можно сразу сказать, является ли указатель сам константой и являются ли Page 82/1103 константой объекты, на которые он может указывать. Термин спецификатор const верхнего уровня (top-level const) используется для обозначения того ключевого слова const, которое объявляет константой сам указатель. Когда указатель способен указывать на константный объект, это называется спецификатор const нижнего уровня (low-level const). В более общем смысле спецификатор const верхнего уровня означает, что объект сам константа. Спецификатор const верхнего уровня может присутствовать в любом типе объекта, будь то один из встроенных арифметических типов, тип класса или ссылочный тип. Спецификатор const нижнего уровня присутствует в базовом типе составных типов, таких как указатели или ссылки. Обратите внимание, что ссылочные типы, в отличие от большинства других типов, способны иметь спецификаторы const как верхнего, так и нижнего уровня, независимо друг от друга. int i = 0; int *const pi = &i; // нельзя изменить значение pi; // const верхнего уровня const int ci = 42; // нельзя изменить ci; const верхнего уровня const int *p2 = &ci; // нельзя изменить p2; const нижнего уровня const int *const p3 = p2; // справа const верхнего уровня, слева нет const int &r = ci; // const в ссылочных типах всегда нижнего уровня Различие между спецификаторами const верхнего и нижнего уровней проявляется при копировании объекта. При копировании объекта спецификатор const верхнего уровня игнорируется. i = ci; // ok: копирование значения ci; спецификатор const верхнего // уровня в ci игнорируется p2 = p3; // ok: указываемые типы совпадают; спецификатор const верхнего // Page 83/1103 уровня в p3 игнорируется Копирование объекта не изменяет копируемый объект. Поэтому несущественно, является ли копируемый или копирующий объект константой. Спецификатор const нижнего уровня, напротив, никогда не игнорируется. При копировании объектов у них обоих должны быть одинаковые спецификаторы const нижнего уровня, или должно быть возможно преобразование между типами этих двух объектов. Как правило, преобразование неконстанты в константу возможно, но не наоборот. int *p = p3; // ошибка: p3 имеет const нижнего уровня, а p - нет p2 = p3; // ok: p2 имеет то же const нижнего уровня, что и p3 p2 = &i; // ok: преобразование int* в const int* возможно int &r = ci; // ошибка: невозможно связать обычную int& с // объектом const int const int &r2 = i; // ok: const int& можно связать с обычным int У указателя p3 есть спецификатор const нижнего и верхнего уровня. При копировании указателя p3 можно проигнорировать его спецификатор const верхнего уровня, но не тот факт, что он указывает на константный тип. Следовательно, нельзя использовать указатель p3 для инициализации указателя p, который указывает на простой (неконстантный) тип int. С другой стороны, вполне можно присвоить указатель p3 указателю p2. У обоих указателей тот же тип (спецификатор const нижнего уровня). Тот факт, что p3 — константный указатель (т.е. у него есть спецификатор const верхнего уровня), не имеет значения. Упражнения раздела 2.4.3 Упражнение 2.30. Укажите по каждому из следующих объявлений, имеет ли объявляемый объект спецификатор const нижнего или верхнего уровня. const int v2 = 0; int v1 = v2; int *p1 = &v1, &r1 = v1; const int *p2 = &v2, *const p3 = &i, &r2 = v2; Упражнение 2.31. С учетом объявлений в предыдущем упражнении укажите, допустимы ли следующие присвоения. Объясните, как спецификатор const верхнего или нижнего уровня применяется в каждом случае. Page 84/1103 r1 = v2; p1 = p2; р2 = p1; p1 = p3; p2 = p3; 2.4.4. Переменные constexpr и константные выражения Константное выражение (constant expression) — это выражение, значение которого не может измениться и вычисляется во время компиляции. Литерал — это константное выражение. Константный объект, инициализируемый константным выражением, также является константным выражением. Вскоре мы увидим, что в языке есть несколько контекстов, требующих константных выражений. Является ли данный объект (или выражение) константным выражением, зависит от типов и инициализаторов. Например: const int max_files = 20; // max_files - константное выражение const int limit = max_files + 1; // limit - константное выражение int staff_size = 27; // staff_size - неконстантное выражение const int sz = get_size(); // sz - неконстантное выражение Хотя переменная staff_size инициализируется литералом, это неконстантное выражение, поскольку он имеет обычный тип int, а не const int. С другой стороны, хоть переменная sz и константа, значение ее инициализатора неизвестно до времени выполнения. Следовательно, это неконстантное выражение. Переменные constexpr В большой системе может быть трудно утверждать (наверняка), что инициализатор — константное выражение. Константная переменная могла бы быть определена с инициализатором, который мы полагаем константным выражением. Однако при использовании этой переменной в контексте, требующем константного выражения, может оказаться, что инициализатор не был константным выражением. Как правило, определение объекта и его использования в таком контексте располагаются довольно далеко друг от друга. Согласно новому стандарту, можно попросить компилятор проверить, является ли переменная константным выражением, использовав в ее объявлении ключевое слово constexpr. Переменные constexpr неявно являются константой и должны инициализироваться константными выражениями. constexpr int mf = 20; // Page 85/1103 20 - константное выражение constexpr int limit = mf + 1; // mf + 1 - константное выражение constexpr int sz = size(); // допустимо, только если size() является // функцией constexpr Хоть и нельзя использовать обычную функцию как инициализатор для переменной constexpr, как будет описано в разделе 6.5.2, новый стандарт позволяет определять функции как constexpr. Такие функции должны быть достаточно просты, чтобы компилятор мог выполнить их во время компиляции. Функции constexpr можно использовать в инициализаторе переменной constexpr. Как правило, ключевое слово constexpr имеет смысл использовать для переменных, которые предполагается использовать как константные выражения. Литеральные типы Поскольку константное выражение обрабатывается во время компиляции, есть пределы для типов, которые можно использовать в объявлении constexpr. Типы, которые можно использовать в объявлении constexpr, известны как литеральные типы (literal type), поскольку они достаточно просты для литеральных значений. Все использованные до сих пор типы — арифметический, ссылка и указатель — это литеральные типы. Наш класс Sales_item и библиотечный тип string не относятся к литеральным типам. Следовательно, нельзя определить переменные этих типов как constexpr. Другие виды литеральных типов рассматриваются в разделах 7.5.6 и 19.3. Хотя указатели и ссылки можно определить как constexpr, используемые для их инициализации объекты жестко ограничены. Указатель constexpr можно инициализировать литералом nullptr или литералом (т.е. константным выражением) 0. Можно также указать на (или связать с) объект, который остается по фиксированному адресу. По причинам, рассматриваемым в разделе 6.1.1, определенные в функции переменные обычно не хранятся по фиксированному адресу. Следовательно, нельзя использовать указатель constexpr для указания на такие переменные. С другой стороны, адрес объекта, определенного вне любой функции, является константным выражением и, таким образом, может использоваться для инициализации указателя constexpr. Как будет описано в разделе 6.1.1, функции могут определять переменные, существующие на протяжении нескольких вызовов этой функция. Как и объект, определенный вне любой функции, эти специальные локальные объекты также имеют фиксированные адреса. Поэтому и ссылка constexpr может быть связана с такой переменной, и указатель constexpr может содержать ее адрес. Указатели и спецификатор constexpr Важно понимать, что при определении указателя в объявлении constexpr спецификатор constexpr относится к указателю, а не к типу, на который указывает указатель. const int *p = nullptr; // p - указатель на const int Page 86/1103 constexpr int *q = nullptr; // q - константный указатель на int Несмотря на внешний вид, типы p и q весьма различны; p — указатель на константу, тогда как q — константный указатель. Различие является следствием того факта, что спецификатор constexpr налагает на определяемый объект спецификатор const верхнего уровня (см. раздел 2.4.3). Как и любой другой константный указатель, указатель constexpr может указать на константный или неконстантный тип. constexpr int *np = nullptr; // np - нулевой константный указатель // на int int j = 0; constexpr int i = 42; // типом i является const int // i и j должны быть определены вне любой функции constexpr const int *p = &i; // p - константный указатель // на const int i constexpr int *p1 = &j; // p1 - константный указатель на int j Упражнения раздела 2.4.4 Упражнение 2.32. Допустим ли следующий код? Если нет, то как его исправить? int null = 0, *p = null; 2.5. Работа с типами По мере усложнения программ используемые в них типы также становятся все более сложными. Осложнения в использовании типов возникают по двум причинам. Имена некоторых типов трудно писать по памяти. Написание некоторых их форм утомительно и подвержено ошибкам. Кроме того, формат записи сложного типа способен скрыть его цель или значение. Другой источник осложнений кроется в том, что иногда трудно точно определить необходимый тип. Это может потребовать оглянуться на контекст программы. |