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

  • Упражнение 2.12. Какие из приведенных ниже имен недопустимы (если таковые есть)

  • Упражнение 2.14. Допустим ли следующий код Если да, то какие значения он отобразит на экране

  • Упражнение 2.15. Какие из следующих определений недопустимы (если таковые есть)

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


    Скачать 1.85 Mb.
    НазваниеЯзык программирования C Пятое издание
    Дата15.07.2019
    Размер1.85 Mb.
    Формат файлаpdf
    Имя файла620354-www.libfox.ru.pdf
    ТипДокументы
    #84130
    страница4 из 54
    1   2   3   4   5   6   7   8   9   ...   54
    Упражнение 2.10. Каковы исходные значения, если таковые вообще имеются, каждой из следующих переменных?
    Page 56/1103
    std::string global str; int global_int; int main() { int local_int; std::string local_str;
    }
    2.2.2. Объявления и определения переменных
    Для обеспечения возможности разделить программу на несколько логических частей язык
    С++ предоставляет технологию, известную как раздельная компиляция (separate compilation). Раздельная компиляция позволяет составлять программу из нескольких файлов, каждый из которых может быть откомпилирован независимо.
    При разделении программы на несколько файлов необходим способ совместного использования кода этих файлов. Например, код, определенный в одном файле, возможно,
    должен использовать переменную, определенную в другом файле. В качестве конкретного примера рассмотрим объекты std::cout и std::cin. Классы этих объектов определены где-то в стандартной библиотеке, но все же наши программы могут использовать их. Внимание!
    Неинициализированные переменные — причина проблем во время выполнения
    Значение неинициализированной переменной неопределенно. Попытка использования значения неинициализированной переменной является ошибкой, которую зачастую трудно обнаружить. Кроме того, компилятор не обязан обнаруживать такие ошибки, хотя большинство из них предупреждает, по крайней мере, о некоторых случаях использования неинициализированных переменных.
    Что же произойдет при использовании неинициализированной переменной с неопределенным значением? Иногда (если повезет) программа отказывает сразу, при попытке доступа к объекту. Обнаружив место, где происходит отказ, как правило, довольно просто выяснить, что его причиной является неправильно инициализированная переменная.
    Но иногда программа срабатывает, хотя результат получается ошибочным. Возможен даже худший вариант, когда на одной машине результаты получаются правильными, а на другой происходит сбой. Кроме того, добавление кода во вполне работоспособную программу в неподходящем месте тоже может привести к внезапному возникновению проблем.
    Мы рекомендуем инициализировать каждый объект встроенного типа. Это не всегда необходимо, но проще и безопасней предоставить инициализатор, чем выяснять, можно ли в данном конкретном случае безопасно опустить его.
    Для поддержки раздельной компиляции язык С++ различает объявления и определения.
    Объявление (declaration) делает имя известным программе. Файл, который должен использовать имя, определенное в другом месте, включает объявление для этого имени.
    Определение (definition) создает соответствующую сущность.
    Page 57/1103

    Объявление переменной определяет ее тип и имя. Определение переменной — это ее объявление. Кроме задания имени и типа, определение резервирует также место для ее хранения и может снабдить переменную исходным значением.
    Чтобы получить объявление, не являющееся также определением, добавляется ключевое слово extern и можно не предоставлять явный инициализатор. extern int i; // объявить, но не определить переменную i int j; // объявить и определить переменную j
    Любое объявление, которое включает явный инициализатор, является определением. Для переменной, определенной как extern (внешняя), можно предоставить инициализатор, но это отменит ее определение как extern. Объявление внешней переменной с инициализатором является ее определением: extern double pi = 3.1416; // определение
    Предоставление инициализатора внешней переменной в функции является ошибкой.
    Объявлены переменные могут быть много раз, но определены только однажды.
    На настоящий момент различие между объявлением и определением может показаться неочевидным, но фактически оно очень важно. Использование переменной в нескольких файлах требует объявлений, отдельных от определения. Чтобы использовать ту же переменную в нескольких файлах, ее следует определить в одном, и только одном файле. В
    других файлах, где используется та же переменная, ее следует объявить, но не определять.
    Более подробная информация о том, как язык С++ поддерживает раздельную компиляцию,
    приведена в разделах 2.6.3 и 6.1.3. Упражнения раздела 2.2.2
    Упражнение 2.11. Объясните, приведены ли ниже объявления или определения.
    (a) extern int ix = 1024;
    (b) int iy;
    (c) extern int iz; Ключевая концепция. Статическая типизация
    Язык С++ обладает строгим статическим контролем типов (statically typed) данных. Это значит, что проверка соответствия значений заявленным для них типам данных осуществляется во время компиляции. Сам процесс проверки называют контролем соответствия типов (type-checking), или типизацией (typing).
    Как уже упоминалось, тип ограничивает операции, которые можно выполнять с объектом. В
    языке С++ компилятор проверяет, поддерживает ли используемый тип операции, которые с ним выполняют. Если обнаруживается попытка сделать нечто, не поддерживаемое данным
    Page 58/1103
    типом, компилятор выдает сообщение об ошибке и не создает исполняемый файл.
    По мере усложнения рассматриваемых программ будет со всей очевидностью продемонстрировано, что строгий контроль соответствия типов способен помочь при поиске ошибок в исходном коде. Однако последствием статической проверки является то, что тип каждой используемой сущности должен быть известен компилятору. Следовательно, тип переменной необходимо объявить прежде, чем эту переменную можно будет использовать.
    2.2.3. Идентификаторы
    Идентификаторы (identifier) (или имена) в языке С++ могут состоять из символов, цифр и символов подчеркивания. Язык не налагает ограничений на длину имен. Идентификаторы должны начинаться с букв или символа подчеркивания. Символы в верхнем и нижнем регистрах различаются, т.е. идентификаторы языка С++ чувствительны к регистру.
    // определено четыре разных переменных типа int int somename, someName, SomeName, SOMENAME;
    Язык резервирует набор имен, перечисленных в табл. 2.3 и 2.4, для собственных нужд. Эти имена не могут использоваться как идентификаторы.
    Таблица 2.3. Ключевые слова языка С++ alignas continue friend register true alignof decltype goto reinterpret_cast try asm default if return typedef auto delete inline short typeid bool do int signed typename break double long sizeof union case dynamic_cast mutable static unsigned catch else namespace static_assert using char enum new static_cast virtual char16_t explicit noexcept struct void char32_t export nullptr switch volatile class extern operator template wchar_t const false private this while constexpr float protected thread_local const_cast for public throw
    Таблица 2.4. Альтернативные имена операторов языка С++ and bitand compl not_eq or_eq xor_eq and_eq bitor not or xor
    Кроме ключевых слов, стандарт резервирует также набор идентификаторов для использования в библиотеке, поэтому пользовательские идентификаторы не могут содержать два последовательных символа подчеркивания, а также начинаться с символа подчеркивания, непосредственно за которым следует прописная буква. Кроме того,
    идентификаторы, определенные вне функций, не могут начинаться с символа подчеркивания.
    Соглашения об именах переменных
    Существует множество общепринятых соглашений для именования переменных. Применение подобных соглашений может существенно улучшать удобочитаемость кода.
    • Идентификатор должен быть осмысленным.
    • Имена переменных обычно состоят из строчных символов. Например, index, а не Index или
    INDEX.
    • Имена классов обычно начинаются с прописной буквы, например Sales_item.
    Page 59/1103

    • Несколько слов в идентификаторе разделяют либо символом подчеркивания, либо прописными буквами в первых символах каждого слова. Например: student_loan или studentLoan, но не studentloan.
    Самым важным аспектом соглашения об именовании является его неукоснительное соблюдение. Упражнения раздела 2.2.3

    Упражнение 2.12. Какие из приведенных ниже имен недопустимы (если таковые есть)?
    (a) int double = 3.14; (b) int _;
    (с) int catch-22; (d) int 1_or_2 = 1;
    (e) double Double = 3.14;
    2.2.4. Область видимости имен
    В любом месте программы каждое используемое имя относится к вполне определенной сущности — переменной, функции, типу и т.д. Однако имя может быть использовано многократно для обращения к различным сущностям в разных точках программы.
    Область видимости (scope) — это часть программы, в которой у имени есть конкретное значение. Как правило, области видимости в языке С++ разграничиваются фигурными скобками.
    В разных областях видимости то же имя может относиться к разным сущностям. Имена видимы от момента их объявления и до конца области видимости, в которой они объявлены.
    В качестве примера рассмотрим программу из раздела 1.4.2:
    #include <iostream> int main() { int sum = 0;
    // сложить числа от 1 до 10 включительно for (int val = 1; val <= 10; ++val) sum += val; // эквивалентно sum = sum + val std::cout << "Sum of 1 to 10 inclusive is "
    << sum << std::endl; return 0;
    }
    Эта программа определяет три имени — main, sum и val, а также использует имя
    Page 60/1103
    пространства имен std, наряду с двумя именами из этого пространства имен — cout и endl.
    Имя main определено вне фигурных скобок. Оно, как и большинство имен, определенных вне функции, имеет глобальную область видимости (global scope). Будучи объявлены, имена в глобальной области видимости доступны в программе повсюду. Имя sum определено в пределах блока,
    которым является тело функции main(). Оно доступно от момента объявления и далее в остальной части функции main(), но не за ее пределами. Переменная sum имеет область видимости блока (block scope). Имя val определяется в пределах оператора for. Оно применимо только в этом операторе, но не в другом месте функции main().Совет.
    Определяйте переменные при первом использовании
    Объект имеет смысл определять поближе к месту его первого использования. Это улучшает удобочитаемость и облегчает поиск определения переменной. Однако важней всего то, что когда переменная определяется ближе к месту ее первого использования, зачастую проще присвоить ей подходящее исходное значение. Вложенные области видимости
    Области видимости могут содержать другие области видимости. Содержащаяся (или вложенная) область видимости называется внутренней областью видимости (inner scope), а содержащая ее области видимости — внешней областью видимости (outer scope).
    Как только имя объявлено в области видимости, оно становится доступно во вложенных в нее областях видимости. Имена, объявленные во внешней области видимости, могут быть также переопределены во внутренней области видимости:
    #include <iostream>
    //
    Программа предназначена исключительно для демонстрации.
    //
    Использование в функции глобальной переменной, а также определение
    // одноименной локальной переменной - это очень плохой стиль
    // программирования int reused = 42; // reused имеет глобальную область видимости int main()
    { int unique = 0; //
    Page 61/1103
    unique имеет область видимости блока
    // вывод #1; используется глобальная reused; выводит 42 0 std::cout << reused << " " << unique << std::endl; int reused = 0; // новый локальный объект по имени reused скрывает
    // глобальный reused
    // вывод #2: используется локальная reused; выводит 0 0 std::cout << reused << " " << unique << std::endl;
    // вывод #3: явное обращение к глобальной reused; выводит 42 0 std::cout << ::reused << " " << unique << std::endl; return 0;
    }
    Вывод #1 осуществляется перед определением локальной переменной reused. Поэтому данный оператор вывода использует имя reused, определенное в глобальной области видимости. Этот оператор выводит 42 0. Вывод #2 происходит после определения локальной переменной reused. Теперь локальная переменная reused находится в области видимости (in scope). Таким образом, второй оператор вывода использует локальный объект reused, а не глобальный и выводит 0 0. Вывод #3 использует оператор области видимости (см. раздел 1.2)
    для переопределения стандартных правил областей видимости. У глобальной области видимости нет имени. Следовательно, когда у оператора области видимости пусто слева, это обращение к указанному справа имени в глобальной области видимости. Таким образом, это выражение использует глобальный объект reused и выводит 42 0.
    Как правило, определение локальных переменных, имена которых совпадают с именами глобальных переменных, является крайне неудачным решением. Упражнения раздела 2.2.4
    Упражнение 2.13. Каково значение переменной j в следующей программе? int i = 42; int main() { int i = 100; int j = i;
    }

    Упражнение 2.14. Допустим ли следующий код? Если да, то какие значения он отобразит на экране?
    Page 62/1103
    int i = 100, sum = 0; for (int i = 0; i != 10; ++i) sum += i; std::cout << i << " " << sum << std::endl;
    2.3. Составные типы
    Составной тип (compound type) — это тип, определенный в терминах другого типа. У языка
    С++ есть несколько составных типов, два из которых, ссылки и указатели, мы рассмотрим в этой главе.
    У рассмотренных на настоящий момент объявлений не было ничего, кроме имен переменных. Такие переменные имели простейший, базовый тип объявления. Более сложные операторы объявления позволяют определять переменные с составными типами,
    которые состоят из объявлений базового типа.
    2.3.1. Ссылки
    Ссылка (reference) является альтернативным именем объекта. Ссылочный тип "ссылается на"
    другой тип. В определении ссылочного типа используется оператор объявления в форме
    &d, где d — объявляемое имя: int ival = 1024; int &refVal = ival; // refVal ссылается на другое имя, ival int &refVal2; // ошибка: ссылку следует инициализировать
    Обычно при инициализации переменной значение инициализатора копируется в создаваемый объект. При определении ссылки вместо копирования значения инициализатора происходит связывание (bind) ссылки с ее инициализатором. После инициализации ссылка остается связанной с исходным объектом. Нет никакого способа изменить привязку ссылки так, чтобы она ссылалась на другой объект, поэтому ссылки следует инициализировать.
    Новый стандарт ввел новый вид ссылки — ссылка r-значения (r-value reference), которую мы рассмотрим в разделе 13.6.1. Эти ссылки предназначены прежде всего для использования в классах. С технической точки зрения,
    когда мы используем термин ссылка (reference), мы подразумеваем
    Page 63/1103
    ссылку l-значения (l-value reference).Ссылка — это псевдоним
    После того как ссылка определена, все операции с ней фактически осуществляются с объектом, с которым связана ссылка. refVal = 2; // присваивает значение 2 объекту, на который ссылается
    // ссылка refVal, т.е. ival int ii = refVal; // то же, что и ii = ival
    Ссылка — это не объект, а только другое имя уже существующего объекта .
    При присвоении ссылки присваивается объект, с которым она связана. При доступе к значению ссылки фактически происходит обращение к значению объекта, с которым связана ссылка. Точно так же, когда ссылка используется как инициализатор, в действительности для этого используется объект, с которым связана ссылка.
    // ok: ссылка refVal3 связывается с объектом, с которым связана
    // ссылка refVal, т.е. с ival int &refVal3 = refVal;
    // инициализирует i значением объекта, с которым связана ссылка refVal int i = refVal; // ok: инициализирует i значением ival
    Поскольку ссылки не объекты, нельзя определить ссылку на ссылку. Определение ссылок
    В одном определении можно определить несколько ссылок. Каждому являющемуся ссылкой идентификатору должен предшествовать символ &. int i = 1024, i2 = 2048; // i и i2 — переменные типа int int &r = i, r2 = i2; // r — ссылка, связанная с переменной i;
    //
    Page 64/1103
    r2 — переменная типа int int i3 = 1024, &ri = i3; // i3 — переменная типа int;
    // ri — ссылка, связанная с переменной i3 int &r3 = i3, &r4 = i2; // r3 и r4 — ссылки
    За двумя исключениями, рассматриваемыми в разделах 2.4.1 и 15.2.3, типы ссылки и объекта, на который она ссылается, должны совпадать точно. Кроме того, по причинам,
    рассматриваемым в разделе 2.4.1, ссылка может быть связана только с объектом, но не с литералом или результатом более общего выражения: int &refVal4 = 10; // ошибка: инициализатор должен быть объектом double dval = 3.14; int &refVal5 = dval; // ошибка: инициализатор должен быть объектом
    // типа int Упражнения раздела 2.3.1

    Упражнение 2.15. Какие из следующих определений недопустимы (если таковые есть)?
    Почему?
    (a) int ival = 1.01; (b) int &rval1 = 1.01;
    (с) int &rval2 = ival; (d) int &rval3;
    Упражнение 2.16. Какие из следующих присвоений недопустимы (если таковые есть)? Если они допустимы, объясните, что они делают. int i = 0, &r1 = i; double d = 0, &r2 = d;
    (a) r2 = 3.14159; (b) r2 = r1;
    (c) i = r2; (d) r1 = d;
    Упражнение 2.17. Что выводит следующий код? int i, &ri = i; i = 5; ri = 10; std::cout << i << " " << ri << std::endl;
    Page 65/1103

    2.3.2. Указатели
    Указатель (pointer) — это составной тип, переменная которого указывает на объект другого типа. Подобно ссылкам, указатели используются для косвенного доступа к другим объектам.
    В отличие от ссылок, указатель — это настоящий объект. Указатели могут быть присвоены и скопированы; один указатель за время своего существования может указывать на несколько разных объектов. В отличие от ссылки, указатель можно не инициализировать в момент определения. Подобно объектам других встроенных типов, значение неинициализированного указателя, определенного в области видимости блока, неопределенно.
    Указатели зачастую трудно понять. При отладке проблемы, связанные с ошибками в указателях, способны запутать даже опытных программистов.
    Тип указателя определяется оператором в форме *d, где d — определяемое имя. Символ *
    следует повторять для каждой переменной указателя. int *ip1, *ip2; // ip1 и ip2 — указатели на тип int double dp, *dp2; // dp2 — указатель на тип double;
    // dp — переменная типа double Получение адреса объекта
    Указатель содержит адрес другого объекта. Для получения адреса объекта используется оператор обращения к адресу (address-of operator), или оператор &. int ival = 42; int *p = &ival; // p содержит адрес переменной ival;
    // p - указатель на переменную ival
    Второй оператор определяет p как указатель на тип int и инициализирует его адресом объекта ival типа int. Поскольку ссылки не объекты, у них нет адресов, а следовательно,
    невозможно определить указатель на ссылку.
    За двумя исключениями, рассматриваемыми в разделах 2.4.2 и 15.2.3, типы указателя и объекта, на который он указывает, должны совпадать. double dval; double *pd = &dval; // ok: инициализатор - адрес объекта типа double
    Page 66/1103
    double *pd2 = pd; // ok: инициализатор - указатель на тип double int *pi = pd; // ошибка: типы pi и pd отличаются pi = &dval; // ошибка: присвоение адреса типа double
    // указателю на тип int
    Типы должны совпадать, поскольку тип указателя используется для выведения типа объекта,
    на который он указывает. Если бы указатель содержал адрес объекта другого типа, то выполнение операций с основным объектом потерпело бы неудачу. Значение указателя
    Хранимое в указателе значение (т.е. адрес) может находиться в одном из четырех состояний.
    1. Оно может указывать на объект.
    2. Оно может указывать на область непосредственно за концом объекта
    3. Это может быть нулевое значение, означающее, что данный указатель не связан ни с одним объектом.
    4. Оно может быть недопустимо. Любое иное значение, кроме приведенного выше, является недопустимым.
    Копирование или иная попытка доступа к значению по недопустимому указателю является серьезной ошибкой. Как и использование неинициализированной переменной, компилятор вряд ли обнаружит эту ошибку. Результат доступа к недопустимому указателю непредсказуем. Поэтому всегда следует знать, допустим ли данный указатель.
    Хотя указатели в случаях 2 и 3 допустимы, действия с ними ограничены. Поскольку эти указатели не указывают ни на какой объект, их нельзя использовать для доступа к объекту.
    Если все же сделать попытку доступа к объекту по такому указателю, то результат будет непредсказуем. Использование указателя для доступа к объекту
    Когда указатель указывает на объект, для доступа к этому объекту можно использовать оператор обращения к значению (dereference operator), или оператор *. int ival = 42; int *p = &ival; // p содержит адрес ival; p - указатель на ival cout << *p; //
    * возвращает объект, на который указывает p;
    Page 67/1103

    // выводит 42
    Обращение к значению указателя возвращает объект, на который указывает указатель.
    Присвоив значение результату оператора обращения к значению, можно присвоить его самому объекту.
    *p = 0; //
    * возвращает объект; присвоение нового значения
    // ival через указатель p cout << *p; // выводит 0
    При присвоении значения *p оно присваивается объекту, на который указывает указатель p.
    Обратиться к значению можно только по допустимому указателю, который указывает на объект. Ключевая концепция. У некоторых символов есть несколько значений
    Некоторые символы, такие как & и *, используются и как оператор в выражении, и как часть объявления. Контекст, в котором используется символ, определяет то, что он означает. int i = 42; int &r = i; //
    & следует за типом в части объявления; r - ссылка int *p; //
    * следует за типом в части объявления; p - указатель p = &i; //
    & используется в выражении как оператор
    // обращения к адресу
    *p = i; //
    * используется в выражении как оператор
    // обращения к значению int &r2 = *p; //
    & в части объявления; * - оператор обращения к значению
    В объявлениях символы & и * используются для формирования составных типов. В
    Page 68/1103
    выражениях эти же символы используются для обозначения оператора. Поскольку тот же символ используется в совершенно ином смысле, возможно, стоит игнорировать внешнее сходство и считать их как будто различными символами. Нулевые указатели
    Нулевой указатель (null pointer) не указывает ни на какой объект. Код может проверить, не является ли указатель нулевым, прежде чем пытаться использовать его. Есть несколько способов получить нулевой указатель. int *p1 = nullptr; // эквивалентно int *p1 = 0; int *p2 = 0; // непосредственно инициализирует p2 литеральной
    // константой 0, необходимо #include cstdlib int *p3 = NULL; // эквивалентно int *p3 = 0;
    Проще всего инициализировать указатель, используя литерал nullptr, который был введен новым стандартом. Литерал nullptr имеет специальный тип, который может быть преобразован (см. раздел 2.1.2) в любой другой ссылочный тип. В
    качестве альтернативы можно инициализировать указатель литералом 0, как это сделано в определении указателя p2.
    Программисты со стажем иногда используют переменную препроцессора (preprocessor variable) NULL, которую заголовок cstdlib определяет как 0.
    Немного подробней препроцессор рассматривается в разделе 2.6.3, а пока достаточно знать,
    что препроцессор (preprocessor) — это программа, которая выполняется перед компилятором.
    Переменные препроцессора используются препроцессором, они не являются частью пространства имен std, поэтому их указывают непосредственно, без префикса std::.
    При использовании переменной препроцессора последний автоматически заменяет такую переменную ее значением. Следовательно, инициализация указателя переменной NULL
    эквивалентна его инициализации значением 0. Сейчас программы С++ вообще должны избегать применения переменной NULL и использовать вместо нее литерал nullptr.
    Нельзя присваивать переменную типа int указателю, даже если ее значением является 0. int zero = 0; pi = zero; // ошибка: нельзя присвоить переменную типа int указателю Совет. Инициализируйте все указатели
    Неинициализированные указатели — обычный источник ошибок времени выполнения.
    Page 69/1103

    Подобно любой другой неинициализированной переменной, последствия использования неинициализированного указателя непредсказуемы. Использование неинициализированного указателя почти всегда приводит к аварийному отказу во время выполнения. Однако поиск причин таких отказов может оказаться на удивление трудным.
    У большинства компиляторов при использовании неинициализированного указателя биты в памяти, где он располагается, используются как адрес. Использование неинициализированного указателя — это попытка доступа к несуществующему объекту в произвольной области памяти. Нет никакого способа отличить допустимый адрес от недопустимого, состоящего из случайных битов, находящихся в той области памяти, которая была зарезервирована для указателя.
    Авторы рекомендуют инициализировать все переменные, а особенно указатели. Если это возможно, определяйте указатель только после определения объекта, на который он должен указывать. Если связываемого с указателем объекта еще нет, то инициализируйте указатель значением nullptr или 0. Так код программы может узнать, что указатель не указывает на объект. Присвоение и указатели
    И указатели, и ссылки предоставляют косвенный доступ к другим объектам. Однако есть важные различия в способе, которым они это делают. Самое важное то, что ссылка — это не объект. После того как ссылка определена, нет никакого способа заставить ее ссылаться на другой объект. При использовании ссылки всегда используется объект, с которым она была связана первоначально.
    Между указателем и содержащимся в нем адресом нет такой связи. Подобно любой другой
    (нессылочной) переменной, при присвоении указателя для него устанавливается новое значение. Присвоение заставляет указатель указывать на другой объект. int i = 42; int *pi = 0; // указатель pi инициализирован, но не адресом объекта int *pi2 = &i; // указатель pi2 инициализирован адресом объекта i int *pi3; // если pi3 определен в блоке, pi3 не инициализирован pi3 = pi2; // pi3 и pi2 указывают на тот же объект, т.е. на i pi2 = 0; // теперь pi2 не содержит адреса никакого объекта
    Сначала может быть трудно понять, изменяет ли присвоение указатель или сам объект, на который он указывает. Важно не забывать, что присвоение изменяет свой левый операнд.
    Следующий код присваивает новое значение переменной pi, что изменяет адрес, который она хранит: pi = &ival; // значение pi изменено; теперь pi указывает на ival
    Page 70/1103

    С другой стороны, следующий код (использующий *pi, т.е. значение, на которое указывает указатель pi) изменяет значение объекта:
    *pi = 0; // значение ival изменено; pi неизменен Другие операции с указателями
    Пока значение указателя допустимо, его можно использовать в условии. Аналогично использованию арифметических значений (раздел 2.1.2), если указатель содержит значение
    0, то условие считается ложным. int ival = 1024; int *pi = 0; // pi допустим, нулевой указатель int *pi2 = &ival; // pi2 допустим, содержит адрес ival if (pi) // pi содержит значение 0, условие считается ложным
    // ... if (pi2) // pi2 указывает на ival, значит, содержит не 0;
    // условие считается истинным
    // ...
    Любой отличный от нулевого указатель рассматривается как значение true. Два допустимых указателя того же типа можно сравнить, используя операторы равенства (==) и неравенства
    (!=). Результат этих операторов имеет тип bool. Два указателя равны, если они содержат одинаковый адрес, и неравны в противном случае. Два указателя содержат одинаковый адрес (т.е. равны), если они оба нулевые, если они указывают на тот же объект или на область непосредственно за концом того же объекта. Обратите внимание, что указатель на объект и указатель на область за концом другого объекта вполне могут содержать одинаковый адрес. Такие указатели равны.
    Поскольку операции сравнения используют значения указателей, эти указатели должны быть допустимы. Результат использования недопустимого указателя в условии или в сравнении непредсказуем.
    Дополнительные операции с указателями будут описаны в разделе 3.5.3.
    Тип void* является специальным типом указателя, способного содержать адрес любого объекта. Подобно любому другому указателю, указатель void* содержит адрес, но тип объекта по этому адресу неизвестен. double obj = 3.14, *pd = &obj;
    Page 71/1103

    // ok: void* может содержать адрес любого типа данных void *pv = &obj; // obj может быть объектом любого типа pv = pd; // pv может содержать указатель на любой тип
    С указателем void* допустимо немного действий: его можно сравнить с другим указателем,
    можно передать его функции или возвратить из нее либо присвоить другому указателю типа void*. Его нельзя использовать для работы с объектом, адрес которого он содержит,
    поскольку неизвестен тип объекта, неизвестны и операции, которые можно с ним выполнять.
    Как правило, указатель void* используют для работы с памятью как с областью памяти, а не для доступа к объекту, хранящемуся в этой области. Использование указателей void*
    рассматривается в разделе 19.1.1, а в разделе 4.11.3 продемонстрировано, как можно получить адрес, хранящийся в указателе void*. Упражнения раздела 2.3.2
    Упражнение 2.18. Напишите код, изменяющий значение указателя. Напишите код для изменения значения, на которое указывает указатель.
    Упражнение 2.19. Объясните основные отличия между указателями и ссылками.
    Упражнение 2.20. Что делает следующая программа? int i = 42; int *p1 = &i;
    *p1 = *p1 * *p1;
    Упражнение 2.21. Объясните каждое из следующих определений. Укажите, все ли они корректны и почему. int i = 0;
    (a) double* dp = &i; (b) int *ip = i; (c) int *p = &i;
    Упражнение 2.22. С учетом того, что p является указателем на тип int, объясните следующий код: if (p) // ... if (*p) // ...
    Упражнение 2.23. Есть указатель p, можно ли определить, указывает ли он на допустимый объект? Если да, то как? Если нет, то почему?
    Упражнение 2.24. Почему инициализация указателя p допустима, а указателя lp нет? int i = 42; void *p = &i; long *lp = &i;
    2.3.3. Понятие описаний составных типов
    Page 72/1103

    Как уже упоминалось, определение переменной состоит из указания базового типа и списка операторов объявления. Каждый оператор объявления может связать свою переменную с базовым типом отлично от других операторов объявления в том же определении. Таким образом, одно определение может определять переменные отличных типов.
    // i - переменная типа int; p - указатель на тип int;
    // r - ссылка на тип int int i = 1024, *p = &i, &r = i;
    Многие программисты не понимают взаимодействия базового и модифицированного типа,
    который может быть частью оператора объявления. Определение нескольких переменных
    Весьма распространенное заблуждение полагать, что модификатор типа (* или &)
    применяется ко всем переменным, определенным в одном операторе. Частично причина в том, что между модификатором типа и объявляемым именем может находиться пробел. int* p; // вполне допустимо, но может ввести в заблуждение
    Данное определение может ввести в заблуждение потому, что создается впечатление, будто int* является типом каждой переменной, объявленной в этом операторе. Несмотря на внешний вид, базовым типом этого объявления является int, а не int*. Символ * — это модификатор типа p, он не имеет никакого отношения к любым другим объектам, которые могли бы быть объявлены в том же операторе: int* p1, p2; // p1 - указатель на тип int; p2 - переменная типа int
    Есть два общепринятых стиля определения нескольких переменных с типом указателя или ссылки. Согласно первому, модификатор типа располагается рядом с идентификатором: int *p1, *p2; // p1 и p2 — указатели на тип int
    Этот стиль подчеркивает, что переменная имеет составной тип. Согласно второму,
    модификатор типа располагается рядом с типом, но он определяет только одну переменную в операторе: int* p1; // p1 - указатель на тип int int* p2; // p2 - указатель на тип int
    Этот стиль подчеркивает, что объявление определяет составной тип.
    Page 73/1103

    Нет никакого единственно правильного способа определения указателей и ссылок. Важно неукоснительно придерживаться выбранного стиля.
    В этой книге используется первый стиль, знак * (или &) помещается рядом с именем переменной. Указатели на указатели
    Теоретически нет предела количеству модификаторов типа, применяемых в операторе объявления. Когда модификаторов более одного, они объединяются хоть и логичным, но не всегда очевидным способом. В качестве примера рассмотрим указатель. Указатель — это объект в памяти, и, как у любого объекта, у этого есть адрес. Поэтому можно сохранить адрес указателя в другом указателе.
    Каждый уровень указателя отмечается собственным символом *. Таким образом, для указателя на указатель пишут **, для указателя на указатель на указатель — *** и т.д. int ival = 1024; int *pi = &ival; // pi указывает на переменную типа int int **ppi = &pi; // ppi указывает на указатель на переменную типа int
    Здесь pi — указатель на переменную типа int, a ppi — указатель на указатель на переменную типа. Эти объекты можно было бы представить так:
    Подобно тому, как обращение к значению указателя на переменную типа int возвращает значение типа int, обращение к значению указателя на указатель возвращает указатель. Для доступа к основной объекту в этом случае необходимо обратиться к значению указателя дважды: cout << "The value of ival\n"
    << "direct value: " << ival << "\n"
    << "indirect value: " << *pi << "\n"
    << "doubly indirect value: " << **ppi << endl;
    Эта программа выводит значение переменной ival тремя разными способами: сначала непосредственно, затем через указатель pi на тип int и наконец обращением к значению указателя ppi дважды, чтобы добраться до основного значения в переменной ival. Ссылки на указатели
    Ссылка — не объект. Следовательно, не может быть указателя на ссылку. Но поскольку указатель — это объект, вполне можно определить ссылку на указатель. int i = 42; int *p; // p - указатель на тип int int *&r = p; // r - ссылка на указатель p
    Page 74/1103
    r = &i; // r ссылается на указатель;
    // присвоение &i ссылке r делает p указателем на i
    *r = 0; // обращение к значению r дает i, объект, на который
    // указывает p; изменяет значение i на 0
    Проще всего понять тип r — прочитать определение справа налево. Ближайший символ к имени переменной (в данном случае & в &r) непосредственно влияет на тип переменной. Таким образом, становится ясно, что r является ссылкой. Остальная часть оператора объявления определяет тип, на который ссылается ссылка r. Следующий символ,
    в данном случае *, указывает, что тип r относится к типу указателя. И наконец, базовый тип объявления указывает, что r — это ссылка на указатель на переменную типа int.
    Сложное объявление указателя или ссылки может быть проще понять, если читать его справа налево. Упражнения раздела 2.3.3
    Упражнение 2.25. Определите типы и значения каждой из следующих переменных:
    (a) int* ip, &r = ip; (b) int i, *ip = 0; (c) int* ip, ip2;
    2.4. Спецификатор const
    Иногда необходимо определить переменную, значение которой, как известно, не может быть изменено. Например, можно было бы использовать переменную, хранящую размер буфера.
    Использование переменной облегчит изменение размера буфера, если мы решим, что исходный размер нас не устраивает. С другой стороны, желательно предотвратить непреднамеренное изменение в коде значения этой переменной. Значение этой переменной можно сделать неизменным, используя в ее определении спецификатор const (qualifier const): const int bufSize = 512; // размер буфера ввода
    Это определит переменную bufSize как константу. Любая попытка присвоить ей значение будет ошибкой: bufSize = 512; // ошибка: попытка записи в константный объект
    Поскольку нельзя изменить значение константного объекта после создания, его следует инициализировать. Как обычно, инициализатор может быть выражением любой сложности:
    Page 75/1103
    const int i = get_size(); // ok: инициализация во время выполнения const int j = 42; // ok: инициализация во время компиляции const int k; // ошибка: k - неинициализированная константа Инициализация и константы
    Как уже упоминалось не раз, тип объекта определяет операции, которые можно с ним выполнять. Константный тип можно использовать для большинства, но не для всех операций,
    как и его неконстантный аналог. Ограничение одно — можно использовать только те операции, которые неспособны изменить объект. Например, тип const int можно использовать в арифметических выражениях точно так же, как обычный неконстантный тип int. Тип const int преобразуется в тип bool тем же способом, что и обычный тип int, и т.д.
    К операциям, не изменяющим значение объекта, относится инициализация. При использовании объекта для инициализации другого объекта не имеет значения, один или оба из них являются константами. int i = 42; const int ci = i; // ok: значение i копируется в ci int j = ci; // ok: значение ci копируется в j
    Хотя переменная ci имеет тип const int, ее значение имеет тип int. Константность переменной ci имеет значение только для операций, которые могли бы изменить ее значение. При копировании переменной ci для инициализации переменной j ее константность не имеет значения. Копирование объекта не изменяет его. Как только копия сделана, у нового объекта нет никакой дальнейшей связи с исходным объектом. По умолчанию константные объекты локальны для файла
    Когда константный объект инициализируется константой во время компиляции, такой как bufSize в определении ниже, компилятор обычно заменяет используемую переменную ее значением во время компиляции. const int bufSize = 512; // размер буфера ввода
    Таким образом, компилятор создаст исполняемый код, использующий значение 512 в тех местах, где исходный код использует переменную bufSize.
    Чтобы заменить переменную значением, компилятор должен видеть ее инициализатор. При разделении программы на несколько файлов, в каждом из которых используется константа,
    необходим доступ к ее инициализатору. Для этого переменная должна быть определена в каждом файле, в котором используется ее значение (см. раздел 2.2.2). Для обеспечения такого поведения, но все же без повторных определений той же переменной, константные переменные определяются как локальные для файла. Определение константы с тем же
    Page 76/1103
    именем в нескольких файлах подобно написанию определения для отдельных переменных в каждом файле.
    Иногда константу необходимо совместно использовать в нескольких файлах, однако ее инициализатор не является константным выражением. Мы не хотим, чтобы компилятор создал отдельную переменную в каждом файле, константный объект должен вести себя как другие (не константные) переменные. В таком случае определить константу следует в одном файле, и объявить ее в других файлах, где она тоже используется.
    Для определения единого экземпляра константной переменной используется ключевое слово extern как в ее определении, так и в ее объявлениях.
    //
    Файл file_1.cc. Определение и инициализация константы, которая
    // доступна для других файлов extern const int bufSize = fcn();
    //
    Файл file_1.h extern const int bufSize; // та же bufSize, определенная в file_1.cc
    Здесь переменная bufSize определяется и инициализируется в файле file_1.cc. Поскольку это объявление включает инициализатор, оно (как обычно) является и определением. Но поскольку bufSize константа, необходимо применить ключевое слово extern, чтобы использовать ее в других файлах.
    Объявление в заголовке file_1.h также использует ключевое слово extern. В данном случае это демонстрирует, что имя bufSize не является локальным для этого файла и что его определение находится в другом месте.
    Чтобы совместно использовать константный объект в нескольких файлах, его необходимо определить с использованием ключевого слова extern. Упражнения раздела 2.4

    1   2   3   4   5   6   7   8   9   ...   54


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