ВУЗ: Не указан

Категория: Не указан

Дисциплина: Не указана

Добавлен: 30.03.2021

Просмотров: 351

Скачиваний: 1

ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.

Еще о типах данных и операторах


Прежде чем переходить к более сложным средствам C++, имеет смысл подробнее познакомиться с некоторыми типами данных и операторами. Кроме уже рассмотренных нами типов данных, в C++ определены и другие. Одни из них состоят из модификаторов, добавляемых к уже известным вам типам. Другие включают перечисления, а третьи используют ключевое слово typedef. C++ также поддерживает ряд операторов, которые значительно расширяют область действия языка и позволяют решать задачи программирования в весьма широком диапазоне. Речь идет о поразрядных операторах, операторах сдвига, а также операторах "?" и sizeof. Кроме того, в этой главе рассматриваются такие специальные операторы, как new и delete. Они предназначены для поддержки С++-системы динамического распределения памяти.


Спецификаторы типа const и volatile

Спецификаторы типа const и volatile управляют доступом к переменной.

В C++ определено два спецификатора типа, которые оказывают влияние на то, каким образом можно получить доступ к переменным или модифицировать их. Это спецификаторы const и volatile. Официально они именуются cv-спецификаторами и должны предшествовать базовому типу при объявлении переменной.


Спецификатор типа const

Переменные, объявленные с использованием спецификатора const, не могут изменить свои значения во время выполнения программы. Однако любой const-переменной можно присвоить некоторое начальное значение. Например, при выполнении инструкции


const double version = 3.2;

создается double-переменная version, которая содержит значение 3.2, и это значение программа изменить уже не может. Но эту переменную можно использовать в других выражениях. Любая const-переменная получает значение либо во время явно задаваемой инициализации, либо при использовании аппаратно-зависимых средств. Применение спецификатора const к объявлению переменной гарантирует, что она не будет модифицирована другими частями вашей программы.

Спецификатор const предотвращает модификацию переменной при выполнении программы.


Спецификатор const имеет ряд важных применений. Возможно, чаще всего его используют для создания const-параметров типа указатель. Такой параметр-указатель защищает объект, на который он ссылается, от модификации со стороны функции. Другими словами, если параметр-указатель предваряется ключевым словом const, никакая инструкция этой функции не может модифицировать переменную, адресуемую этим параметром. Например, функция code() в следующей короткой программе сдвигает каждую букву в сообщении на одну алфавитную позицию (т.е. вместо буквы 'А' ставится буква 'Б' и т.д.), отображая таким образом сообщение в закодированном виде. Использование спецификатора const в объявлении параметра не позволяет коду функции модифицировать объект, на который указывает этот параметр.


#include <iostream>

using namespace std;


void code(const char *str);

int main()

{

 code("Это тест.");

 return 0;

}


/* Использование спецификатора const гарантирует, что str не может изменить аргумент, на который он указывает.

*/

void code(const char *str)

{

 while(*str) {

  cout << (char) (*str+1);

  str++;

 }

}


Поскольку параметр str объявляется как const-указатель, у функции code() нет никакой возможности внести изменения в строку, адресуемую параметром str. Но если вы попытаетесь написать функцию code() так, как показано в следующем примере, то обязательно получите сообщение об ошибке, и программа не скомпилируется.


// Этот код неверен.

void code(const char *str)

{

 while(*str) {

  *str = *str + 1; // Ошибка, аргумент модифицировать нельзя.

  cout << (char) *str;

  str++;

 }

}


Поскольку параметр str является const-указателем, его нельзя использовать для модификации объекта, на который он ссылается.


Спецификатор const можно также использовать для ссылочных параметров, чтобы не допустить в функции модификацию переменных, на которые ссылаются эти параметры. Например, следующая программа некорректна, поскольку функция f() пытается модифицировать переменную, на которую ссылается параметр i.


// Нельзя модифицировать const-ссылки.

#include <iostream>

using namespace std;

void f(const int &i);

int main()

{

 int к = 10;

 f(к);

 return 0;

}

// Использование ссылочного const-параметра.

void f (const int &i)

{

 i = 100; // Ошибка, нельзя модифицировать const-ссылку.

 cout << i;

}


Спецификатор const еще можно использовать для подтверждения того, что ваша программа не изменяет значения некоторой переменной. Вспомните, что переменная типа const может быть модифицирована внешними устройствами, т.е. ее значение может быть установлено каким-нибудь аппаратным устройством (например, датчиком). Объявив переменную с помощью спецификатора const, можно доказать, что любые изменения, которым подвергается эта переменная, вызваны исключительно внешними событиями.


Наконец, спецификатор const используется для создания именованных констант. Часто в программах многократно применяется одно и то же значение для различных целей. Например, необходимо объявить несколько различных массивов таким образом, чтобы все они имели одинаковый размер. Когда нужно использовать подобное "магическое число", имеет смысл реализовать его в виде const-переменной. Затем вместо реального значения можно использовать имя этой переменной, а если это значение придется впоследствии изменить, вы измените его только в одном месте программы. Следующая программа позволяет попробовать этот вид применения спецификатора const "на вкус".


#include <iostream>

using namespace std;

const int size = 10;

int main()

{

 int A1[size], A2[size], A3[size];

 // . . .

}


Если в этом примере понадобится использовать новый размер для массивов, вам потребуется изменить только объявление переменной size и перекомпилировать программу. В результате все три массива автоматически получат новый размер.


Спецификатор типа volatile

Спецификатор volatile информирует компилятор о том, что данная переменная может быть изменена внешними (по отношению к программе) факторами.


Спецификатор volatile сообщает компилятору о том, что значение соответствующей переменной может быть изменено в программе неявным образом. Например, адрес некоторой глобальной переменной может передаваться управляемой прерываниями подпрограмме тактирования, которая обновляет эту переменную с приходом каждого импульса сигнала времени. В такой ситуации содержимое переменной изменяется без использования явно заданных инструкций программы. Существуют веские основания для того, чтобы сообщить компилятору о внешних факторах изменения переменной. Дело в том, что С++-компилятору разрешается автоматически оптимизировать определенные выражения в предположении, что содержимое той или иной переменной остается неизменным, если оно не находится в левой части инструкции присваивания. Но если некоторые факторы (внешние по отношению к программе) изменят значение этого поля, такое предположение окажется неверным, в результате чего могут возникнуть проблемы.

Например, в следующем фрагменте программы предположим, что переменная clock обновляется каждую миллисекунду часовым механизмом компьютера. Но, поскольку переменная clock не объявлена с использованием спецификатора volatile, этот фрагмент кода может иногда работать недолжным образом. (Обратите особое внимание на строки, обозначенные буквами "А" и "Б".)


int clock, timer;

 // ...

timer = clock; // строка A

// ... Какие-нибудь действия.

cout << "Истекшее время " << clock-timer; // строка Б


В этом фрагменте переменная clock получает свое значение, когда она присваивается переменной timer в строке А. Но, поскольку переменная clock не объявлена с использованием спецификатора volatile, компилятор волен оптимизировать этот код, причем таким способом, при котором значение переменной clock, возможно, не будет опрошено в инструкции cout (строка Б), если между строками А и Б не будет ни одного промежуточного присваивания значения переменной clock. (Другими словами, в строке Б компилятор может просто еще раз использовать значение, которое получила переменная clock в строке А.) Но если между моментами выполнения строк А и Б поступят очередные импульсы сигнала времени, то значение переменной clock обязательно изменится, а строка Б в этом случае не отразит корректный результат.

Для решения этой проблемы необходимо объявить переменную clock с ключевым словом volatile.


volatile int clock;


Теперь значение переменной clock будет опрашиваться при каждом ее использовании.

И хотя на первый взгляд это может показаться странным, спецификаторы const и volatile можно использовать вместе. Например, следующее объявление абсолютно допустимо. Оно создает const-указатель на volatile-объект.


const volatile unsigned char *port = (const volatile char *) 0x2112;

В этом примере для преобразования целочисленного литерала 0x2112 в const-указатель на volatile-символ необходимо применить операцию приведения типов.




Спецификаторы классов памяти

C++ поддерживает пять спецификаторов классов памяти:


auto


extern


register


static


mutable

Спецификаторы классов памяти определяют, как должна храниться переменная.

С помощью этих ключевых слов компилятор получает информацию о том, как должна храниться переменная. Спецификатор классов памяти необходимо указывать в начале объявления переменной.

Спецификатор 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 << "\n";

 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-объявление становится определением. Это очень важный момент, поскольку любой объект может иметь несколько объявлений, но только одно определение.