4.1  Краткая характеристика объектов

Как уже указывалось, лексемы, группируясь и объединяясь вместе, образуют выражения, операторы и другие смысловые конструкции, т.е. конструкции следующей, более высокой ступени иерархии языка. Одним из основных смысловых конструкций языка C++ является понятие «объект».

Рис. 4.1. Объекты и их свойства

Объектом назовем идентифицируемую область памяти, которая может содержать фиксированное значение одной переменной или их набор (используемое в данном случае слово "объект" не следует путать с общим термином, используемым в объектно-ориентированных языках). Примерами объектов могут быть переменные, массивы, а также объекты, созданные на основе структур, объединений, перечислений, классов и т.д. Каждый объект имеет связанное с ним имя и тип (рис. 4.1).

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

Объявления определения создают объекты, присваивают им имена (идентификаторы), размещают их в оперативной памяти и, при необходимости, их инициализируют. Например:

int i; //создан объект (переменная i), в памяти под него выделено машинное слово.

float x=5.3; //создан объект, выделено 4 байта, инициализирован значением (5.3).

Объявления ссылки не создают объекты, а просто делают их имена известными компилятору (используя ключевое слово extern). Например:

extern int i; // Информирует компилятор о том, что

// объект используется в рассматриваемом файле, а создан он

/ в другом файле.

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

const float  pi     = 3.1415926;

const int max = 32767;

char  *const str    = "Константный указатель"; // константный указатель

char  const  *str1  = "Указатель на константу"; // указатель на константу

С учетом этого, следующие операторы являются недопустимыми, например:

pi  = 3.0;           // присвоение значения константе

i   = max++;      // инкремент константы

str = "Изменение константного указателя";  // модификация указателя str

Однако вызов функции strcpy (str," Константный указатель ") является допустимым, поскольку он выполняет посимвольное копирование строкового литерала "Константный указатель" в адрес памяти, указываемый str (о функции strcpy () см. в приложении 2, о константах см. в разделе 2).

Имя используется для доступа к объекту. Именем может быть простой идентификатор либо выражение, «указывающее» на адрес объекта.

Идентификатор – это произвольное имя любой длины (см. раздел 2). Стандартные библиотеки C++, а также модули и заголовки файлов, разработанные пользователем, должны иметь уникальные идентификаторы (или формируемые из них выражения) и типы, в этом случае компилятор C++ может однозначно обеспечивать доступ, интерпретировать и модифицировать каждый активный объект разрабатываемой программы.

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

Язык C++ поддерживает многие встроенные (стандартные) и определяемые пользователем типы объектов. Имеется также возможность объявлять указатели на эти объекты. Специальная операция sizeof() позволяет определить во время компиляции размер в байтах любого стандартного или определяемого пользователем типа данных.

Встроенные типы могут быть целочисленными, знаковыми (signed) и беззнаковыми (unsigned), с плавающей и фиксированной точкой (float, double, long double), различной точности. Используемые ключевые слова для определения встроенных (основных) типов объектов представлены в табл. 4.1.

Таблица 4.1 Ключевые слова для определения встроенных (основных) типов объектов

Целые типы

Плавающие типы

Прочие

char                     

float              

enum

int                        

double            

 void

short  

long  double 

 near pointer

long  

    

 far pointer

signed

 

unsigned

 

Для определения объектов целого типа используются различные ключевые слова, которые определяют диапазон значений и размер области памяти, выделяемой под переменные. Ключевое слово char используется для представления символа (из массива символов) или для объявления строковых литералов. Значением объекта типа char является код (размером 1 байт), соответствующий представляемому символу. Для представления символов русского алфавита тип объекта должен быть unsigned char, так как коды русских букв превышают значение 127.

Ключевые слова int, short, long используются для объявления объектов целого типа. Стандарт ANSI C не  устанавливает  размеры  памяти этих  типов,  за исключением того, что размеры данных short, int и  long образуют неубывающую  последовательность  «short  £  int  £  long».  Все  три  типа могут быть одинаковыми. Это существенно для написания мобильных кодов, предназначенных для переноса на  другую  аппаратную базу.

В самом деле, размер памяти для переменной типа signed int определяется длиной машинного слова, которое имеет различный размер на разрядных интерфейсах. Так, на 16-разрядных интерфейсах размер слова равен 2 байта, на 32-разрядных – соответственно 4 байта. То есть тип int эквивалентен типам short int, или long int в зависимости от архитектуры используемой персональной ЭВМ. Таким образом, одна и та же программа может правильно работать на одном компьютере и неправильно на другом. Для определения размера памяти, занимаемой объектом, можно использовать операцию sizeof(), возвращающую значение размера в байтах указанного типа.

Ключевые слова signed и unsigned указывают, как интерпретируется знаковый бит объявляемого объекта, т.е., если указано ключевое слово unsigned, то знаковый бит интерпретируется как часть объекта, в противном случае он интерпретируется как знак. В случае отсутствия ключевого слова unsigned объект целого типа считается знаковым. В том случае, когда типом является signed или unsigned и далее следует идентификатор объекта, то он будет рассматриваться как объект типа int. На рис. 4.2 приведены форматы основных типов для 16 – разрядного интерфейса:

Рис. 4.2. Форматы основных типов объектов

Для объектов, представляющих числа с плавающей точкой используются следующие ключевые слова : float, double, long double (в некоторых реализациях языка С long double  отсутствует).

Тип float занимает 4 байта. Из них 1 бит отводится для знака, 8 бит для экспоненты и 23 бита для мантиссы. Старший бит мантиссы всегда равен 1, поэтому он не заполняется, в связи с этим диапазон значений переменной с плавающей точкой приблизительно равен от 3.14E-38 до 3.14E+38.

Тип double занимает 8 байт памяти. Его формат аналогичен формату float. Биты памяти распределяются следующим образом: 1 бит для знака, 11 бит для экспоненты и 52 бита для мантиссы. С учетом опущенного старшего бита мантиссы диапазон значений равен от 1.7E-308 до 1.7E+308.

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

формат 1:

enum [имя_тега_перечисления] {список_перечисления}

описатель[,описатель…];

· формат 2:

enum имя_тега_перечисления описатель [,описатель…];

Объявление перечисления задает тип объекта перечисления и определяет список именованных констант, называемый списком_перечисления. Значением каждого имени списка является некоторое целое число.

Объект типа перечисления может принимать значения одной из именованных констант списка. Именованные константы списка имеют тип int. Таким образом, память, соответствующая переменной перечисления, – это память, необходимая для размещения значения типа int.

В формате 1 имена и значения перечисления задаются в списке перечислений. Необязательное имя_тега_перечисления, – это идентификатор, который именует тег перечисления, определенный списком перечисления. Описатель именует переменную перечисления. В объявлении может быть задана более чем одна переменная типа перечисления. Список_перечисления содержит одну или несколько конструкций вида:

идентификатор [= константное_выражение]

Каждый идентификатор именует элемент перечисления. Все идентификаторы в списке enum должны быть уникальными. В случае отсутствия константного выражения первому идентификатору соответствует значение 0, следующему идентификатору – значение 1 и т.д. Имя константы перечисления эквивалентно ее значению.

Идентификатор, связанный с константным_выражением, принимает значение, задаваемое этим константным выражением. Константное выражение должно иметь тип int и может быть как положительным, так и отрицательным. Следующему идентификатору в списке присваивается значение, равное константному выражению плюс 1, если этот идентификатор не имеет своего константного выражения.

Использование элементов перечисления должно подчиняться следующим правилам:

1) переменная может содержать повторяющиеся значения;

2) идентификаторы в списке перечисления должны быть отличны от всех других идентификаторов в той же области видимости, включая имена обычных переменных и идентификаторы из других списков перечислений;

3) имена типов перечислений должны быть отличны от других имен типов перечислений, структур и объединений в этой же области видимости;

4) значение может следовать за последним элементом списка перечисления.

Примеры перечислимых объектов можно найти в разделе 2.

В зависимости от размещения объявления в исходном коде объект может иметь разные: контекст, класс памяти, продолжительность, видимость, компоновку.

Контекст – это часть программы, в которой данный объект существует, и к нему можно обращаться. Существует пять категорий контекста:

1) блок (или локальный);

2) функция;

3) прототип функции;

4) файл;

5) класс (только для С++).

Контекст зависит от того, как и где объявлены идентификаторы.

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

Единственными идентификаторами, имеющими контекст функции, являются метки операторов. Имена меток могут быть использованы в операторах goto в любой точке функции, где объявлена данная метка. Метки объявляются неявно; для этого записывается имя_метки, затем идет двоеточие (:) и за ним оператор. Имена меток в пределах функции должны быть уникальными. Например:

strart: puts(“Начало блока”);

goto start;

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

int func(int i, float f, double d);

int func(int, float, double);

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

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

Чтобы различать идентификаторы объектов различного рода компилятор языка С++ устанавливает более сложный контекст, называемый «Пространства имен». Во избежание противоречий имена идентификаторов внутри одного пространства имен должны быть уникальными. В языке С++ имя должно быть объявлено до момента его первого использования в выражении.

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

Пространства имен помогают справиться с проблемой засорения более удобным

способом. Для их создания используется ключевое слово namespace, после которого записывается имя пользовательского пространства имен, а за ним следует блок в фигурных скобках, содержащий различные объявления. Каждое такое пространство представляет собой отдельную область видимости. Все объекты, объявленные внутри такого пространства имен, называются его членами.

Для доступа к члену созданного пространства используется оператор области видимости (::). Например, если создано пространство имен namespace abc и оно имеет объект х, то для доступа к этому объекту нужно обратиться abc :: x. Оператор области видимости может использоваться для ссылки на член глобального пространства имен, не имеющий имени, например, :: y.

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

Программы, в которых используются пространства имен, обычно организуют следующим образом:

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

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

Псевдоним пространства имен используется для задания короткого синонима имени пространства. Объявление псевдонима начинается ключевым словом namespace, за которым следует короткий псевдоним, а за ним – знак равенства и исходное полное имя пространства.

Имеется механизм, позволяющий обращаться к членам пространства имен, используя их имена без имени пространства имен. Для этого применяются using-объявления. Using-объявление начинается ключевым словом using, за которым следует имя члена пространства. Using-объявление ведет себя подобно любому другому объявлению: оно имеет область видимости, и имя, введенное им, которые можно употреблять начиная с места объявления и до конца области видимости. Using-объявление может находиться в определенной области видимости, и, значит, можно точно указать, в каком месте программы те или иные члены разрешается употреблять без дополнительной квалификации.

Чтобы заставить программу работать с новой библиотекой помогают using-директивы, облегчающие переход на новую версию библиотеки, где впервые стали применяться пространства имен. Using-директива начинается ключевым словом using, за которым следует ключевое слово namespace, а затем имя некоторого пространства имен. Это имя должно ссылаться на определенное ранее пространство, иначе компилятор выдаст ошибку. Using-директива позволяет сделать все имена из этого пространства видимыми в неквалифицированной форме. Using-директивы очень полезны при переводе приложений на новые версии библиотек, использующие пространства имен.

Все компоненты стандартной библиотеки языка С++ находятся в пространстве имен std. Поэтому, при использовании одного пространства имен можно в исходный  код ввести директиву:

using namespace std;

Для связи идентификаторов с объектами требуется, чтобы каждый идентификатор имел, кроме имени и типа, еще два атрибута: класс памяти и продолжительность. Компилятор языка C++ определяет эти атрибуты по явным или неявным объявлениям в исходном коде программы.

Класс памяти задает размещение объекта (в сегменте данных (static, extern), в регистре (register), в куче (heap) или стеке (auto)) и продолжительность, т.е. время его существования (все время работы программы, либо же при  выполнении некоторых конкретных блоков кода). Класс памяти может быть установлен синтаксисом объявления, его расположением в исходном коде или обоими этими факторами.

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

Объектам со статической продолжительностью выделяется память сразу же в начале выполнения программы; память сохраняется до конца работы программы. Объекты со статической продолжительностью обычно размещаются в фиксированных сегментах данных. Все функции независимо от того, где они определены, являются объектами со статической продолжительностью. Также статическую продолжительность имеют все объекты с файловым контекстом. Другим переменным может быть задана статическая продолжительность путем использования явных спецификаторов класса памяти static или extern. Объекты со статической продолжительностью инициализируются в ноль (или пустое значение) при отсутствии явного спецификатора или в языке С++ конструктора.

 Объекты с локальной продолжительностью всегда имеют локальный контекст, поскольку они не существуют вне своего объемлющего блока. Объекты с локальной продолжительностью, иначе называемые динамическими локальными переменными, имеют меньшую продолжительность. Они создаются в стеке (или в регистре) при входе в объемлющий их блок или функцию. При выходе программы из такого блока или функции они уничтожаются. Объекты с локальной продолжительностью должны быть инициализированы явно; в противном случае их начальное значение непредсказуемо. При объявлении переменных локальной продолжительности можно использовать спецификатор класса памяти auto, однако он является избыточным, поскольку auto для переменных, объявленных в блоке, всегда является умолчанием.

При объявлении переменных (например, int, char, float) с использованием спецификатора класса памяти register также подразумевается auto, однако компилятору при этом передается запрос (или рекомендация) о том, что желательно данный объект разместить в регистре. Если имеется свободный регистр, то компилятор языка C++ может для локальной переменной или переменной типа указатель выбрать его в качестве памяти. Если свободных регистров нет, то переменная распределяется как auto, или динамический локальный объект, без выдачи предупреждения и генерации ошибки.

Объекты с динамической продолжительностью создаются и уничтожаются специальными функциями при выполнении программы. Им выделяется память из специального резерва, называемого кучей (heap), при помощи либо стандартных библиотечных функций, как, например malloc(), либо при помощи операции new в языке С++. Соответствующая отмена распределения выполняется при помощи функций free() или операции delete. Например:

#include<stdio.h>

const double pi=3.1415926; // класс памяти – static, размещается в сегменте

 //  данных,

// продолжительность – с точки объявления до конца 

// файла

void main(void)

{

float r=10;  //   класс памяти – auto, размещается в стеке, продолжительность –

//  с точки объявления до конца блока

char * ptr=malloc(10); //  класс памяти – heap, указывает на область памяти

//  heap, продолжительность – с точки объявления

// до конца блока

free(ptr);

}

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

Видимость не может выходить за пределы контекста; но контекст может превышать видимость. Например:

{

int i; char ch;  // автоматическое распределение по умолчанию

i = 3;                // объекты int i и char ch в контексте и видимы

{

double i;       // объект double i в контексте и видим

i = 2.0e3;      // объект int i в контексте, но скрыт

ch = ‘A';         // объект char ch в контексте и видим

}                         // объект double i вне контекста

i += 1;                         // объект int i видим и равен 4

                        // объект char ch в контексте, видим  и равен ‘A’

}

                          // int i и char ch вне контекста

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

Компоновка – это процесс, который позволяет правильно связать каждое вхождение идентификатора с одним конкретным объектом или функцией. Все идентификаторы имеют один из трех атрибутов компоновки, тесно связанных с их контекстом: внешнюю компоновку, внутреннюю компоновку или отсутствие компоновки. Эти атрибуты  определяются местоположением и форматом объявлений, вместе с явным (или неявным по умолчанию) использованием спецификатора класса памяти static или extern.