Спецификатор mutable применяется только к объектам классов, о которых речь впереди. Остальные спецификаторы мы рассмотрим в этом разделе.
Спецификатор класса памяти auto
Редко используемый спецификатор auto объявляет локальную переменную.
Спецификатор auto объявляет локальную переменную. Но он используется довольно редко (возможно, вам никогда и не доведется применить его), поскольку локальные переменные являются "автоматическими" по умолчанию. Вряд ли вам попадется это ключевое слово и в чужих программах.
Спецификатор класса памяти extern
Все программы, которые мы рассматривали до сих пор, имели довольно скромный размер. Реальные же компьютерные программы гораздо больше. По мере увеличения размера файла, содержащего программу, время компиляции становится иногда раздражающе долгим. В этом случае следует разбить программу на несколько отдельных файлов. После этого небольшие изменения, вносимые в один файл, не потребуют перекомпиляции всей программы. При разработке больших проектов такой многофайловый подход может сэкономить существенное время. Реализовать этот подход позволяет ключевое слово extern.
В программах, которые состоят из двух или более файлов, каждый файл должен "знать" имена и типы глобальных переменных, используемых программой в целом. Однако нельзя просто объявить копии глобальных переменных в каждом файле. Дело в том, что в C++ программа может включать только одну копию каждой глобальной переменной. Следовательно, если вы попытаетесь объявить необходимые глобальные переменные в каждом файле, возникнут проблемы. Когда компоновщик попытается скомпоновать эти файлы, он обнаружит дублированные глобальные переменные, и компоновка программы не состоится. Чтобы выйти из этого затруднительного положения, достаточно объявить все глобальные переменные в одном файле, а в других использовать extern-объявления, как показано на рис. 9.1.
Спецификатор extern объявляет переменную, но не выделяет для нее области памяти.
В файле F1 объявляются и определяются переменные х, у и ch. В файле F2 используется скопированный из файла F1 список глобальных переменных, к объявлению которых добавлено ключевое слово extern. Спецификатор extern делает переменную известной для модуля, но в действительности не создает ее. Другими словами, ключевое слово extern предоставляет компилятору информацию о типе и имени глобальных переменных, повторно не выделяя для них памяти. Во время компоновки этих двух модулей все ссылки на эти внешние переменные будут определены.
До сих пор мы не уточняли, в чем состоит различие между объявлением и определением переменной, но здесь это очень важно. При объявлении, переменной присваивается имя и тип, а посредством определения для переменной выделяется память. В большинстве случаев объявления переменных одновременно являются определениями. Предварив имя переменной спецификатором extern, можно объявить переменную, не определяя ее.
Существует еще одно применение для ключевого слова extern, которое не связано с многофайловыми проектами. Не секрет, что много времени уходит на объявления глобальных переменных, которые, как правило, приводятся в начале программы, но это не всегда обязательно. Если функция использует глобальную переменную, которая определяется ниже (в том же файле), в теле функции ее можно специфицировать как внешнюю (с помощью ключевого слова extern). При обнаружении определения этой переменной компилятор вычислит соответствующие ссылки на нее.
Рассмотрим следующий пример. Обратите внимание на то, что глобальные переменные first и last объявляются не перед, а после функции main().
#include <iostream>
using namespace std;
Int main()
{
extern int first, last; // Использование глобальных переменных.
cout << first << " " << last << "";
return 0;
}
// Глобальное определение переменных first и last.
int first = 10, last = 20;
При выполнении этой программы на экран будут выведены числа 10 и 20, поскольку глобальные переменные first и last, используемые в инструкции cout, инициализируются этими значениями. Поскольку extern-объявление в функции main() сообщает компилятору о том, что переменные first и last объявляются где-то в другом месте (в данном случае ниже, но в том же файле), программу можно скомпилировать без ошибок, несмотря на то, что переменные first и last используются до их определения.
Важно понимать, что extern-объявления переменных, показанные в предыдущей программе, необходимы здесь только по той причине, что переменные first и last не были определены до их использования в функции main(). Если бы их определения компилятор обнаружил раньше определения функции main(), необходимости в extern-инструкции не было бы. Помните, если компилятор обнаруживает переменную, которая не была объявлена в текущем блоке, он проверяет, не совпадает ли она с какой-нибудь из переменных, объявленных внутри других включающих блоков. Если нет, компилятор просматривает ранее объявленные глобальные переменные. Если обнаруживается совпадение их имен, компилятор предполагает, что ссылка была именно на эту глобальную переменную. Спецификатор extern необходим только в том случае, если вы хотите использовать переменную, которая объявляется либо ниже в том же файле, либо в другом.
И еще. Несмотря на то что спецификатор extern объявляет, но не определяет переменную, существует одно исключение из этого правила. Если в extern-объявлении переменная инициализируется, то такое extern-объявление становится определением. Это очень важный момент, поскольку любой объект может иметь несколько объявлений, но только одно определение.
Статические переменные
Переменные типа static — это переменные "долговременного" хранения, т.е. они хранят свои значения в пределах своей функции или файла. От глобальных они отличаются тем, что за рамками своей функции или файла они неизвестны. Поскольку спецификатор static по-разному определяет "судьбу" локальных и глобальных переменных, мы рассмотрим их в отдельности.
Локальные static-переменные
Локальная static-переменная поддерживает свое значение между вызовами функции.
Если к локальной переменной применен модификатор static, то для нее выделяется постоянная область памяти практически так же, как и для глобальной переменной. Это позволяет статической переменной поддерживать ее значение между вызовами функций.(Другими словами, в отличие от обычной локальной переменной, значение static-переменной не теряется при выходе из функции.) Ключевое различие между статической локальной и глобальной переменными состоит в том, что статическая локальная переменная известна только блоку, в котором она объявлена. Таким образом, статическую локальную переменную в некоторой степени можно назвать глобальной переменной, которая имеет ограниченную область видимости.
Чтобы объявить статическую переменную, достаточно предварить ее тип ключевым словом static. Например, при выполнении этой инструкции переменная count объявляется статической.
static int count;
Статической переменной можно присвоить некоторое начальное значение. Например, в этой инструкции переменной count присваивается начальное значение 200:
static int count = 200;
Локальные static-переменные инициализируются только однажды, в начале выполнения программы, а не при каждом входе в функцию, в которой они объявлены.
Возможность использования статических локальных переменных важна для создания независимых функций, поскольку существуют такие типы функций, которые должны сохранять их значения между вызовами. Если бы статические переменные не были предусмотрены в C++, пришлось бы использовать вместо них глобальные, что открыло бы путь для всевозможных побочных эффектов.
Рассмотрим пример использования static-переменной. Она служит для хранения текущего среднего значения от чисел, вводимых пользователем.
/* Вычисляем текущее среднее значение от чисел, вводимых пользователем.
*/
#include <iostream>
using namespace std;
int r_avg(int i);
Int main()
{
int num;
do {
cout << "Введите числа (-1 означает выход): ";
cin >> num;
if(num != -1)
cout << "Текущее среднее равно: " << r_avg(num);
cout << '';
}while(num > -1);
return 0;
}
// Вычисляем текущее среднее.
int r_avg(int i)
{
static int sum=0, count=0;
sum = sum + i;
count++;
return sum / count;
}
Здесь обе локальные переменные sum и count объявлены статическими и инициализированы значением 0. Помните, что для статических переменных инициализация выполняется только один раз (при первом выполнении функции), а не при каждом входе в функцию. В этой программе функция r_avg() используется для вычисления текущего среднего значения от чисел, вводимых пользователем. Поскольку обе переменные sum и count являются статическими, они поддерживают свои значения между вызовами функции r_avg(), что позволяет нам получить правильный результат вычислений. Чтобы убедиться в необходимости модификатора static, попробуйте удалить его из программы. После этого программа не будет работать корректно, поскольку промежуточная сумма будет теряться при каждом выходе из функции r_avg().
Глобальные static-переменные
Глобальная static-переменная известна только для файла, в котором она объявлена.
Если модификатор static применен к глобальной переменной, то компилятор создаст глобальную переменную, которая будет известна только для файла, в котором она объявлена. Это означает, что, хотя эта переменная является глобальной, другие функции в других файлах не имеют о ней "ни малейшего понятия" и не могут изменить ее содержимое. Поэтому она и не может стать "жертвой" несанкционированных изменений. Следовательно, для особых ситуаций, когда локальная статичность оказывается бессильной, можно создать небольшой файл, который будет содержать лишь функции, использующие глобальные static-переменные, отдельно скомпилировать этот файл и работать с ним, не опасаясь вреда от побочных эффектов "всеобщей глобальности".
Рассмотрим пример, который представляет собой переработанную версию программы (из предыдущего раздела), вычисляющей текущее среднее значение. Эта версия состоит из двух файлов и использует глобальные static-переменные для хранения значений промежуточной суммы и счетчика вводимых чисел.
//---------------------Первый файл---------------------
#include <iostream>
using namespace std;
int r_avg(int i);
void reset();
Int main()
{
int num;
do {
cout <<"Введите числа (-1 для выхода, -2 для сброса): ";
cin >> num;
if(num==-2) {
reset();
continue;
}
if(num != -1)
cout << "Среднее значение равно: " << r_avg(num);
cout << '';
}while(num != -1);
return 0;
}
//---------------------Второй файл---------------------
#include <iostream>
static int sum=0, count=0;
int r_avg(int i)
{
sum = sum + i;
count++;
return sum / count;
}
Void reset()
{
sum = 0;
count = 0;
}
В этой версии программы переменные sum и count являются глобально статическими, т.е. их глобальность ограничена вторым файлом. Итак, они используются функциями r_avg() и reset(), причем обе они расположены во втором файле. Этот вариант программы позволяет сбрасывать накопленную сумму (путем установки в исходное положение переменных sum и count), чтобы можно было усреднить другой набор чисел. Но ни одна из функций, расположенных вне второго файла, не может получить доступ к этим переменным. Работая с данной программой, можно обнулить предыдущие накопления, введя число -2. В этом случае будет вызвана функция reset(). Проверьте это. Кроме того, попытайтесь получить из первого файла доступ к любой из переменных sum или count. (Вы получите сообщение об ошибке.)
Итак, имя локальной static-переменной известно только функции или блоку кода, в котором она объявлена, а имя глобальной static-переменной — только файлу, в котором она "обитает". По сути, модификатор static позволяет переменным существовать так, что о них знают только функции, использующие их, тем самым "держа в узде" и ограничивая возможности негативных побочных эффектов. Переменные типа static позволяют программисту "скрывать" одни части своей программы от других частей. Это может оказаться просто супердостоинством, когда вам придется разрабатывать очень большую и сложную программу.
Важно! Несмотря на то что глобальные static-переменные по-прежнему допустимы и широко используются в С++-коде, стандарт C++ возражает против их применения. Для управления доступом к глобальным переменным рекомендуется другой метод, который заключается в использовании пространств имен. Этот метод описан ниже в этой книге.
Регистровые переменные
Возможно, чаще всего используется спецификатор класса памяти register. Для компилятора модификатор register означает предписание обеспечить такое хранение соответствующей переменной, чтобы доступ к ней можно было получить максимально быстро. Обычно переменная в этом случае будет храниться либо в регистре центрального процессора (ЦП), либо в кэше (быстродействующей буферной памяти небольшой емкости). Вероятно, вы знаете, что доступ к регистрам ЦП (или к кэш-памяти) принципиально быстрее, чем доступ к основной памяти компьютера. Таким образом, переменная, сохраняемая в регистре, будет обслужена гораздо быстрее, чем переменная, сохраняемая, например, в оперативной памяти (ОЗУ). Поскольку скорость, с которой к переменным можно получить доступ, определяет, по сути, скорость выполнения вашей программы, для получения удовлетворительных результатов программирования важно разумно использовать спецификатор register.
Спецификатор register в объявлении переменной означает требование оптимизировать код для получения максимально возможной скорости доступа к ней.
Формально спецификатор register представляет собой лишь запрос, который компилятор вправе проигнорировать. Это легко объяснить: ведь количество регистров (или устройств памяти с малым временем выборки) ограничено, причем для разных сред оно может быть различным. Поэтому, если компилятор исчерпает память быстрого доступа, он будет хранить register-переменные обычным способом. В общем случае неудовлетворенный register-запрос не приносит вреда, но, конечно же, и не дает никаких преимуществ хранения в регистровой памяти.
Поскольку в действительности только для ограниченного количества переменных можно обеспечить быстрый доступ, важно тщательно выбрать, к каким из них применить модификатор register. (Только правильный выбор может повысить быстродействие программы.) Как правило, чем чаще к переменной требуется доступ, тем большая выгода будет получена в результате оптимизации кода с помощью спецификатора register. Поэтому объявлять регистровыми имеет смысл управляющие переменные цикла или переменные, к которым выполняется доступ в теле цикла. На примере следующей функции показано, как register-переменная типа int используется для управления циклом. Эта функция вычисляет результат выражения mе для целочисленных значений с сохранением знака исходного числа (т.е. при m = -2 и е = 2 результат будет равен -4).
int signed_pwr(register int m, register int e)
{
register int temp;
int sign;
if(m < 0) sign = -1;
else sign = 1;
temp = 1;
for( ; e; e--) temp = temp * m;
return temp * sign;
}
В этом примере переменные m, е и temp объявлены как регистровые, поскольку все они используются в теле цикла, и потому к ним часто выполняется доступ. Однако переменная sign объявлена без спецификатора register, поскольку она не является частью цикла и используется реже.
Происхождение модификатора register
Модификатор register был впервые определен в языке С. Первоначально он применялся только к переменным типа int и char или к указателям и заставлял хранить переменные этого типа в регистре ЦП, а не в ОЗУ, где хранятся обычные переменные. Это означало, что операции с регистровыми переменными могли выполняться намного быстрее, чем операции с остальными (хранимыми в памяти), поскольку для опроса или модификации их значений не требовался доступ к памяти.
После стандартизации языка С было принято решение расширить определение спецификатора register. Согласно ANSI-стандарту С модификатор register можно применять к любому типу данных. Его использование стало означать для компилятора требование сделать доступ к переменной типа register максимально быстрым. Для ситуаций, включающих символы и целочисленные значения, это по-прежнему означает помещение их в регистры ЦП, поэтому традиционное определение все еще в силе. Поскольку язык C++ построен на ANSI-стандарте С, он также поддерживает расширенное определение спецификатора register.
Как упоминалось выше, точное количество register-переменных, которые реально будут оптимизированы в любой одной функции, определяется как типом процессора, так и конкретной реализацией C++, которую вы используете. В общем случае можно рассчитывать по крайней мере на две. Однако не стоит беспокоиться о том, что вы могли объявить слишком много register-переменных, поскольку C++ автоматически превратит регистровые переменные в нерегистровые, когда их лимит будет исчерпан. (Это гарантирует переносимость С++-кода в рамках широкого диапазона процессоров.)
Чтобы показать влияние, оказываемое register-переменными на быстродействие программы, в следующем примере измеряется время выполнения двух циклов for, которые отличаются друг от друга только типом управляющих переменных. В программе используется стандартная библиотечная С++-функция clock(), которая возвращает количество импульсов сигнала времени системных часов, подсчитанных с начала выполнения этой программы. Программа должна включать заголовок <ctime>.
/* Эта программа демонстрирует влияние, которое может оказать использование register-переменной на скорость выполнения программы.
*/
#include <iostream>
#include <ctime>
using namespace std;