Справочник по C, Полный справочник по C
Скачать 393.9 Kb.
|
312 Модуль 7. Еще о типах данных и операторах << running_avg(num); cout << '\n'; } while(num > -1); return 0; } // Вычисляем текущее среднее. int running_avg(int i) { static int sum = 0, count = 0; // Поскольку переменные // sum и count являются статическими, // они сохраняют свои значения между // вызовами функции running_avg(). sum = sum + i; count++; return sum / count; } Здесь обе локальные переменные sum и count объявлены статическими и инициализированы значением 0. Помните, что для статических переменных ини- циализация выполняется только один раз (при первом выполнении функции), а не при каждом входе в функцию. В этой программе функция running_avg() используется для вычисления текущего среднего значения от чисел, вводимых пользователем. Поскольку обе переменные sum и count являются статическими, они поддерживают свои значения между вызовами функции running_avg(), что позволяет нам получить правильный результат вычислений. Чтобы убедить- ся в необходимости модификатора static, попробуйте удалить его из програм- мы. После этого программа не будет работать корректно, поскольку промежуточ- ная сумма будет теряться при каждом выходе из функции running_avg(). Глобальные static -переменные Если модификатор static применен к глобальной переменной, то компиля- тор создаст глобальную переменную, которая будет известна только для файла, в котором она объявлена. Это означает, что, хотя эта переменная является гло- бальной, другие функции в других файлах не имеют о ней “ни малейшего поня- С++: руководство для начинающих 313 Еще о типах данных и операторах 7 тия” и не могут изменить ее содержимое. Поэтому она и не может стать “жертвой” несанкционированных изменений. Следовательно, для особых ситуаций, когда локальная статичность оказывается бессильной, можно создать небольшой файл, который будет содержать лишь функции, использующие глобальные static- переменные, отдельно скомпилировать этот файл и работать с ним, не опасаясь вреда от побочных эффектов “всеобщей глобальности”. Рассмотрим пример, который представляет собой переработанную версию программы (из предыдущего подраздела), вычисляющей текущее среднее значе- ние. Эта версия состоит из двух файлов и использует глобальные static-пе- ременные для хранения значений промежуточной суммы и счетчика вводимых чисел. В эту версию программы добавлена функция reset(), которая обнуляет (“сбрасывает”) значения глобальных static-переменных. // --------------------- Первый файл --------------------- #include using namespace std; int running_avg(int i); void reset(); int main() { int num; do { cout << "Введите числа (-1 для выхода, -2 для сброса): "; cin >> num; if(num==-2) { reset(); continue; } if(num != -1) cout << "Среднее значение равно: " << running_avg(num); cout << '\n'; } while(num != -1); return 0; 314 Модуль 7. Еще о типах данных и операторах } // --------------------- Второй файл --------------------- static int sum=0, count=0; // Эти переменные известны // только в файле, в котором // они объявлены. int running_avg(int i) { sum = sum + i; count++; return sum / count; } void reset() { sum = 0; count = 0; } В этой версии программы переменные sum и count являются глобально ста- тическими, т.е. их глобальность ограничена вторым файлом. Итак, они исполь- зуются функциями running_avg() и reset(), причем обе они расположены во втором файле. Этот вариант программы позволяет сбрасывать накопленную сумму (путем установки в исходное положение переменных sum и count), чтобы можно было усреднить другой набор чисел. Но ни одна из функций, расположен- ных вне второго файла, не может получить доступ к этим переменным. Работая с данной программой, можно обнулить предыдущие накопления, введя число –2. В этом случае будет вызвана функция reset(). Проверьте это. Кроме того, по- пытайтесь получить из первого файла доступ к любой из переменных sum или count . (Вы получите сообщение об ошибке.) Итак, имя локальной static-переменной известно только функции или блоку кода, в котором она объявлена, а имя глобальной static-переменной — только файлу, в котором она “обитает”. По сути, модификатор static позволяет перемен- ным существовать так, что о них знают только функции, использующие их, тем са- мым “держа в узде” и ограничивая возможности негативных побочных эффектов. Переменные типа static позволяют программисту “скрывать” одни части своей программы от других частей. Это может оказаться просто супердостоинством, ког- да вам придется разрабатывать очень большую и сложную программу. С++: руководство для начинающих 315 Еще о типах данных и операторах 7 Вопрос. Я слышал, что некоторые C++-программисты не используют глобальные static -переменные. Так ли это? Ответ. Несмотря на то что глобальные static -переменные по-прежнему допу- стимы и широко используются в C++-коде, стандарт C++ не предусма- тривает их применения. Для управления доступом к глобальным перемен- ным рекомендуется другой метод, который заключается в использовании пространств имен. Этот метод описан ниже в данной книге. При этом гло- бальные static -переменные широко используются C-программистами, поскольку в C не поддерживаются пространства имен. Поэтому вам еще долго придется встречаться с глобальными static -переменными. Спросим у опытного программиста ВАЖНО! 7.4. Регистровые переменные Возможно, чаще всего используется спецификатор класса памяти register . Для компилятора модификатор register означает предписание обеспечить та- кое хранение соответствующей переменной, чтобы доступ к ней можно было по- лучить максимально быстро. Обычно переменная в этом случае будет храниться либо в регистре центрального процессора (ЦП), либо в кэш-памяти (быстродей- ствующей буферной памяти небольшой емкости). Вероятно, вы знаете, что до- ступ к регистрам ЦП (или к кэш-памяти) принципиально быстрее, чем доступ к основной памяти компьютера. Таким образом, переменная, сохраняемая в ре- гистре, будет обслужена гораздо быстрее, чем переменная, сохраняемая, напри- мер, в оперативной памяти (ОЗУ). Поскольку скорость, с которой к переменным можно получить доступ, определяет, по сути, скорость выполнения вашей про- граммы, для получения удовлетворительных результатов программирования важно разумно использовать спецификатор register. Формально спецификатор register представляет собой лишь запрос, кото- рый компилятор вправе проигнорировать. Это легко объяснить: ведь количество регистров (или устройств памяти с малым временем выборки) ограничено, причем для разных сред оно может быть различным. Поэтому, если компилятор исчерпает память быстрого доступа, он будет хранить register-переменные обычным спо- собом. В общем случае неудовлетворенный register-запрос не приносит вреда, но, конечно же, и не дает никаких преимуществ хранения в регистровой памяти. Как правило, программист может рассчитывать на удовлетворение register-за- проса по крайней мере для двух переменных, обработка которых действительно будет оптимизирована с точки зрения максимально возможной скорости. 316 Модуль 7. Еще о типах данных и операторах Поскольку быстрый доступ можно обеспечить на самом деле только для огра- ниченного количества переменных, важно тщательно выбрать, к каким из них применить модификатор register. (Лишь правильный выбор может повысить быстродействие программы.) Как правило, чем чаще к переменной требуется до- ступ, тем большая выгода будет получена в результате оптимизации кода с помо- щью спецификатора register. Поэтому объявлять регистровыми имеет смысл управляющие переменные цикла или переменные, к которым выполняется до- ступ в теле цикла. На примере следующей функции показано, как используются register-пе- ременные для повышения быстродействия функции summation(), которая вы- числяет сумму значений элементов массива. Здесь как раз и предполагается, что реально для получения скоростного эффекта будут оптимизированы только две переменные. // Демонстрация использования register-переменных. #include { int vals[] = { 1, 2, 3, 4, 5 }; int result; result = summation(vals, 5); cout << "Сумма элементов массива равна " << result << "\ n"; return 0; } // Функция возвращает сумму int-элементов массива. int summation(int nums[], int n) { register int i; // Эти переменные оптимизированы для register int sum = 0; // для получения максимальной скорости. С++: руководство для начинающих 317 Еще о типах данных и операторах 7 for(i = 0; i < n; i++) sum = sum + nums[i]; return sum; } Здесь переменная i, которая управляет циклом for, и переменная sum, к ко- торой осуществляется доступ в теле цикла, определены с использованием спе- цификатора register. Поскольку обе они используются внутри цикла, можно рассчитывать на то, что их обработка будет оптимизирована для реализации бы- строго к ним доступа. В этом примере предполагалось, что реально для получе- ния скоростного эффекта будут оптимизированы только две переменные, поэто- му nums и n не были определены как register-переменные, ведь доступ к ним реализуется не столь часто, как к переменным i и sum. Но если среда позволяет оптимизацию более двух переменных, то имело бы смысл переменные nums и n также объявить с использованием спецификатора register, что еще больше по- высило бы быстродействие программы. Вопросы для текущего контроля 1. Локальная переменная, объявленная с использованием модификатора static , ________ свое значение между вызовами функции. 2. Спецификатор extern используется для объявления переменной без ее определения. Верно ли это? 3. Какой спецификатор служит для компилятора запросом на оптимизацию обработки переменной с целью повышения быстродействия программы?* 1. Локальная переменная, объявленная с использованием модификатора static , сохраняет свое значение между вызовами функции. 2. Верно, спецификатор extern действительно используется для объявления пере- менной без ее определения. 3. Запросом на оптимизацию обработки переменной с целью повышения быстродей- ствия программы служит спецификатор register. 318 Модуль 7. Еще о типах данных и операторах ВАЖНО! 7.5. Перечисления В C++ можно определить список именованных целочисленных констант. Та- кой список называется перечислением (enumeration). Эти константы можно затем использовать везде, где допустимы целочисленные значения (например, в цело- численных выражениях). Перечисления определяются с помощью ключевого слова enum , а формат их определения имеет такой вид: enum имя_типа { список_перечисления } список_переменных; Под элементом список_перечисления понимается список разделенных запятыми имен, которые представляют значения перечисления. Элемент спи- сок_переменных необязателен, поскольку переменные можно объявить позже, используя имя_типа перечисления. В следующем примере определяется перечисление transport и две перемен- ные типа transport с именами t1 и t2. enum transport { car, truck, airplane, train, boat } t1, t2; Определив перечисление, можно объявить другие переменные этого типа, ис- пользуя имя типа перечисления. Например, с помощью следующей инструкции объявляется одна переменная how перечисления transport. transport how; Вопрос. Когда я добавил в программу спецификатор register, то не заметил ни- каких изменений в быстродействии. В чем причина? Ответ . Большинство компиляторов (благодаря прогрессивной технологии их разработки) автоматически оптимизируют программный код. Поэтому во многих случаях внесение спецификатора register в объявление переменной не ускорит выполнение программы, поскольку обработка этой переменной уже оптимизирована. Но в некоторых случаях исполь- зование спецификатора register оказывается весьма полезным, так как он позволяет сообщить компилятору, какие именно переменные вы считаете наиболее важными для оптимизации. Это особенно ценно для функций, в которых используется большое количество переменных, и ясно, что всех их невозможно оптимизировать. Следовательно, несмотря на прогрессивную технологию разработки компиляторов, спецификатор register по-прежнему играет важную роль для эффективного про- граммирования. Спросим у опытного программиста С++: руководство для начинающих 319 Еще о типах данных и операторах 7 Эту инструкцию можно записать и так. enum transport how; Однако использование ключевого слова enum здесь излишне. В языке C (ко- торый также поддерживает перечисления) обязательной была вторая форма, по- этому в некоторых программах вы можете встретить подобную запись. С учетом предыдущих объявлений при выполнении следующей инструкции переменной how присваивается значение airplane. how = airplane; Важно понимать, что каждый символ списка перечисления означает целое число, причем каждое следующее число (представленное идентификатором) на единицу больше предыдущего. По умолчанию значение первого символа пере- числения равно нулю, следовательно, значение второго — единице и т.д. Поэтому при выполнении этой инструкции cout << car << ' ' << train; на экран будут выведены числа 0 и 3. Несмотря на то что перечислимые константы автоматически преобразуются в целочисленные, обратное преобразование автоматически не выполняется. На- пример, следующая инструкция некорректна. how = 1; // ошибка Эта инструкция вызовет во время компиляции ошибку, поскольку автомати- ческого преобразования целочисленных значений в значения типа transport не существует. Откорректировать предыдущую инструкцию можно с помощью операции приведения типов. fruit = (transport) 1; // Теперь все в порядке, // но стиль не совершенен. Теперь переменная how будет содержать значение truck, поскольку эта transport -константа связывается со значением 1. Как отмечено в комментарии, несмотря на то, что эта инструкция стала корректной, ее стиль оставляет желать лучшего, что простительно лишь в особых обстоятельствах. Используя инициализатор, можно указать значение одной или нескольких перечислимых констант. Это делается так: после соответствующего элемента списка перечисления ставится знак равенства и нужное целое число. При исполь- зовании инициализатора следующему (после инициализированного) элементу списка присваивается значение, на единицу превышающее предыдущее значение инициализатора. Например, при выполнении следующей инструкции константе airplane присваивается значение 10. enum transport { car, truck, airplane = 10, train, boat }; 320 Модуль 7. Еще о типах данных и операторах Теперь все символы перечисления transport имеют следующие значения. car 0 truck 1 airplane 10 train 11 boat 12 Часто ошибочно предполагается, что символы перечисления можно вводить и выводить как строки. Например, следующий фрагмент кода выполнен не будет. // Слово "train" на экран таким образом не попадет. how = train; cout << how; Не забывайте, что символ train — это просто имя для некоторого целочислен- ного значения, а не строка. Следовательно, при выполнении предыдущего кода на экране отобразится числовое значение константы train, а не строка "train". Конечно, можно создать код ввода и вывода символов перечисления в виде строк, но он выходит несколько громоздким. Вот, например, как можно отобразить на экране названия транспортных средств, связанных с переменной how. switch(how) { case car: cout << "Automobile"; break; case truck: cout << "Truck"; break; case airplane: cout << "Airplane"; break; case train: cout << "Train"; break; case boat: cout << "Boat"; break; } Иногда для перевода значения перечисления в соответствующую строку можно объявить массив строк и использовать значение перечисления в каче- стве индекса. Например, следующая программа выводит названия трех видов транспорта. С++: руководство для начинающих 321 Еще о типах данных и операторах 7 // Демонстрация использования перечисления. #include "Automobile", "Truck", "Airplane", "Train", "Boat" }; int main() { transport how; how = car; cout << name[how] << '\n'; how = airplane; cout << name[how] << '\n'; how = train; cout << name[how] << '\n'; return 0; } Вот результаты выполнения этой программы. Automobile Airplane Train Использованный в этой программе метод преобразования значения пере- числения в строку можно применить к перечислению любого типа, если оно не содержит инициализаторов. Для надлежащего индексирования массива строк перечислимые константы должны начинаться с нуля, быть строго упорядочен- 322 Модуль 7. Еще о типах данных и операторах ными по возрастанию, и каждая следующая константа должна быть больше пред- ыдущей точно на единицу. Из-за того, что значения перечисления необходимо вручную преобразовывать в удобные для восприятия человеком строки, они, в основном, используются там, где такое преобразование не требуется. Для примера рассмотрите перечисление, используемое для определения таблицы символов компилятора. ВАЖНО! 7.6. Ключевое слово typedef В C++ разрешается определять новые имена типов данных с помощью ключе- вого слова typedef. При использовании typedef-имени не создается новый тип данных, а лишь определяется новое имя для уже существующего типа. Благодаря typedef -именам можно сделать машинозависимые программы более переноси- мыми: для этого иногда достаточно изменить typedef-инструкции. Это сред- ство также позволяет улучшить читабельность кода, поскольку для стандартных типов данных с его помощью можно использовать описательные имена. Общий формат записи инструкции typedef таков. typedef тип имя; Здесь элемент тип означает любой допустимый тип данных, а элемент имя — но- вое имя для этого типа. При этом заметьте: новое имя определяется вами в каче- стве дополнения к существующему имени типа, а не для его замены. Например, с помощью следующей инструкции можно создать новое имя для типа float. typedef float balance; Эта инструкция является предписанием компилятору распознавать иденти- фикатор balance как еще одно имя для типа float. После этой инструкции мож- но создавать float-переменные с использованием имени balance. balance over_due; Здесь объявлена переменная с плавающей точкой over_due типа balance, ко- торый представляет собой стандартный тип float с другим названием. |