Семестр 2

Практическое занятие 1

Программировать – значит понимать.

Кристин Нюгард

Что нужно предварительно знать для выполнения задания:

1. Последовательность этапов генерации исполняемого кода

Она представлена на рисунке. Исходные тексты содержатся обычно в нескольких файлах с расширением *.cpp (их автором является прикладной программист) а также в так называемых заголовочных файлах, имеющих обычно расширение *.h (от английского header – заголовок). В заголовочные файлы выносится информация, которая должна присутствовать в нескольких исходных *.cpp – файлах. Во всех таких *.cpp файлах делается ссылка на нужные заголовочные файлы с помощью директивы #include.

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

Текстовый редактор – с его помощью программист создает исходный текст программы или вносит в него исправления. Рекомендуется, внеся какое-либо изменение в один из файлов, проверить работоспособность приложения и только затем вносить следующее изменение (естественно, если изменения не взаимосвязаны).

Препроцессор – он выполняет ряд действий над исходным текстом, в частности выполняет директивы #include и #define, которые просто состоят в замене одного текста другим (например, строки #include <cmath> текстом заголовочного файла сmath). Препроцессор всегда запускается автоматически перед компиляцией файла. Результат обработки препроцессором исходного файла называется единицей трансляции.

Компилятор – преобразует единицу трансляции в так называемый объектный формат (файл с расширением *.obj).Объектный модуль содержит уже машинные коды команд, но еще не содержит окончательно сформированных адресов операндов, так как они зависят от содержимого других объектных модулей. Если изменения были внесены только в один модуль, достаточно перекомпилировать только его – это может заметно уменьшить время, затрачиваемое на получение исполняемой программы с очередным изменением (по сравнению с полным построением проекта – см. далее). Для запуска процедуры компиляции файла используется команда меню BuildàCompile или горячая клавиша Ctrl-F7. Запуск одной только компиляции (без компоновки) имеет смысл, поскольку на этом этапе транслятор проверяет наличие ошибок в конкретном (компилируемом) файле и выводит сообщения, если ошибки были обнаружены

НЕ нашли? Не то? Что вы ищете?

Компоновщик (редактор связей, линкер) – «собирает» исполняемую программу (создает файл с расширением *.exe или *.dll) путем объединения нескольких объектных модулей, при этом вычисляет и добавляет к кодам команд адреса операндов. Кроме того, компоновщик по требованию программиста может в *.exe-файле добавить к исполняемому коду дополнительную отладочную информацию, такую например, как соответствие символических имен переменных их машинным адресам. Эта информация используется программой-отладчиком и позволяет производить отладку на уровне исходного текста программы. Если вы произвели какие-либо изменения в исходном тексте и хотите посмотреть, как они «будут работать», то после компиляции обязательно следует выполнить компоновку. Компиляция файла с последующим автоматическим запуском компоновки выполняется по команде меню BuildàBuild или горячей клавишей F7.

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

Последовательность этапов генерации исполняемого кода.

2. Структура исходного текста программы на языке Cи.

2.1. Функции

Любая С программа состоит из одной и более функций, каждая из которых имеет вид:

<тип_возвращаемого_значения> <имя_функции>(<список_параметров>)

{

<строки исходного текста программы>

}

Одна (и только одна) из этих функций должна иметь стандартное имя – main. Это – “точка входа” в Вашу программу (то есть первая команда этой функции будет первой выполняемой командой Вашей программы). Другие функции могут быть вызваны из функции main или из какой либо другой функции в процессе выполнения программы. Функции могут находиться в разных файлах.

2.2. Блоки кода.

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

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

void main()

{

}

Таким образом, функция main() может содержать внутри себя:

блоки и вызовы других функций, а эти последние также могут содержать внутри себя

блоки и вызовы функций.

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

2.3. Комментарии.

Для документирования программы на языке С используются комментарии двух видов:

// это комментарий первого вида. Его действие распространяется на текущую строку текста.

/*

А это комметарий второго вида. Компилятор рассматривает все строки внутри /*...*/ скобок как комментарий. Вложенные комментарии такого вида не допускаются.

*/

3. Директивы препроцессора.

Получение исполняемого модуля из исходного текста происходит в несколько этапов. На первом этапе с исходным текстом программы работает специальная программа – препроцессор. Основная цель препроцессора – закончить формирование исходного текста программы на С (то есть выполнение директив препроцессора). Затем окончательный текст программы подвергается компиляции. Директивы препроцессора позволяют программисту:

1)  включать текст из других текстовых файлов (в частности содержащих стандартную библиотеку функций).

2)  описывать макроподстановки.

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

3.1. Включение файлов. Директива #include.

Директива препроцессора #include позволяет включать в исходный текст программы, поступающий на компиляцию, любые текстовые файлы. Существуют следующие формы директивы:

#include <спецификация_файла>

#include “спецификация_файла”

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

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

Например:

#include <iostream> //указание препроцессору искать файл iostream в стандартной включаемой директории

#include “my. h” //указание препроцессору искать файл my. h сначала в текущей директории, а потом в каталогах по умолчанию

#include “c:\myhdr\my. h” //указание препроцессору искать файл my. h только в c:\myhdr и нигде больше

3.2. Директива #define

Во времена ранних версий С считалось, что употребление макросов делает исходный текст программы более “читабельным”. Но в связи с развитием языка и появлением новых стандартов С++ считается (Бьерн Страуструп – создатель С++), что употребление макросов не является хорошим стилем программирования. Несмотря на авторитетное мнение, предлагаю рассмотреть применение данной директивы, так как, заглянув в. h – файлы стандартных библиотек С, API и MFC (что бывает очень полезным), Вы будете встречать эту директиву довольно часто. Поэтому, даже не употребляя ее согласно советам гуру, Вы должны иметь о ней представление.

1)  #define <идентификатор_макро> <тело_макро>

Препроцессор просматривает исходный текст и заменяет каждое вхождение лексемы “идентификатор_макро” на лексему (совокупность лексем) “тело_макро”. Такие замещения называются макрорасширениями

Например:

#define HI “Hello, darling!”

#define FIRST 1

void main()

{

cout<<HI; //препроцессор заменит HI на “Hello, darling!”

...

if(n == FIRST) ... // препроцессор заменит FIRST на 1

}

2)  После первой подстановки выполняется просмотр уже «расширенного» макросами текста, что позволяет организовывать вложенные макро. Например:

#define YES 0

#define NO! YES

void main()

{

if(i == YES)... //препроцессор заменит на if(i == 0)...

if(i == NO)... // a) препроцессор заменит при первом просмотре на if(i == !YES),
б) при втором просмотре на if(i == !0)

}

3)  Можно описать макро с параметрами:

#define <идентификатор_макро>(список аргументов) <тело_макро>

Например:

#define SQUARE(x)( (x) * (x) )

void main()

{

int z = 2;

y = SQUARE(z); //препроцессор заменит на (z)*(z)

}

Замечание: существенным моментом при написании тела макро с параметрами является заключение параметра в скобки. Подумайте, каковы были бы результаты макрорасширения в следующем примере в случае (1) и (2)?

#define SQUARE(x)( (x) * (x) )(1) или #define SQUARE(x)( x * x )(2)

void main()

{

int z = 2;

y = SQUARE(z-1);

}

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

4)  #define EMPTY //пример пустого тела макро

Макро с пустым телом харатеризует определенное (для компилятора) состояние данного идентификатора (с именем EMPTY). В основном такие макроопределения применяются для условной компиляции кода.

5)  С помощью комбинации ## в макро с параметрами можно «слить» два формальных параметра в одну лексему. Так, например, можно компоновать идентификаторы из отдельных частей:

#define VAR(i, j) i##_##j

int first_second, first_third, second_third;

VAR(first, second) = 1; //присвоит переменной first_second значение 1

VAR(second, third) = 2; //присвоит переменной second_third значение 2

3.3. Директива #undef.

Если идентификатор «упомянут» в директиве #define, компилятор считает его определенным и действующим до тех пор, пока в файле не встретится директива #undef. Поэтому один и тот же идентификатор может означать для компилятора разные лексемы, то есть одному и тому же идентификатору макро можно поставить в соответствие разные тела. Можно отменить действие макроопределения с помощью директивы #undef. Например:

#define MAX_NUMBER 80

char buffer[MAX_NUMBER]; //препроцессор подставит char buffer[80]; объявлен массив из 80 элементов

#undef MAX_NUMBER //отмена действия макро. Идентификатор макрокоманды становится неопределенным

#define MAX_NUMBER 128

int nLength = MAX_NUMBER * 4; // препроцессор подставит int nLength=128*4;

Замечание: идентификаторы могут также определяться и становиться неопределенными при помощи опций командной строки компилятора (что эквивалентно вставке в текст программы директив #define и #undef).

3.4. Директивы #ifdef, #ifndef и #endif

Условные директивы #ifdef, #ifndef используются для проверки: является ли в данный момент указанный идентификатор определенным. Например, попытка с помощью директивы #define переопределить уже определенный идентификатор приведет к выводу предупреждающего сообщения. Предпочтительно для уникального описания идентификаторов использовать следующую стратегию:

#ifndef MAX_NUMBER

#define MAX_NUMBER 512 //эта строка не выполняется, если MAX_NUMBER в данный момент определен

#endif

3.5. Директивы #if, #elif, #else.

Условные операторы #if, #elif, #else и #endif работают также, как обычные условные операторы Си, только действуют на стадии компиляции, а не выполнения. Условная компиляция позволяет в зависимости от значения тех или иных идентификаторов включать в текст программы разные части кода. Пример использования директив условной компиляции:

#if defined(CREDIT) //если определен идентификатор CREDIT,

credit(); //компилируется вызов функции credit().

#elif defined(DEBIT) //иначе если определен идентификатор DEBIT,

debit(); //компилируется вызов функции debit().

#else //если ни один идентификатор не определен,

error(); //компилируется вызов функции error()

#endif

Оператор препроцессора defined предоставляет по сравнению с директивами #ifdef и #ifndef альтернативный, более гибкий способ оценки – является ли идентификатор определенным. Его использование допустимо только с директивами #if, #elif. Выражение defined (идентификатор) имеет значение true (1), если идентификатор в данный момент определен. Таким образом, директива

#if defined(CREDIT) (1) эквивалентна директиве #ifdef CREDIT (2).

Преимущество использования (1) в том, что такой способ позволяет многократно использовать оператор defined в сложных выражениях, следующих за директивой #if, например:

#if defined(MY_SYSTEM) && !defined(YOUR_SYSTEM)

...

Замечание: часто в качестве идентификатора в операторе defined используются переменные, определяющие тип процессора или операционную систему. Это позволяет учесть особенности архитектуры или операционной системы на этапе компиляции, что в свою очередь позволяет программисту без изменений адаптировать исходный текст программы к разным средам.

4. Базовые типы данных (представление, преобразование, контроль)

1)  Целочисленные типы: различные ключевые слова (char, short, int, long – signed и unsigned), используемые для определения целых типов данных, определяют диапазон значений, размер памяти, выделяемой компилятором для хранения переменной данного типа и те машинные инструкции, которые будут использоваться для операций с переменными данного типа. Ключевые слова signed и unsigned указывают, как интерпретируется старший бит машинного представления переменной. Если указано ключевое слово unsigned, то старший бит рассматривается как часть числа. Если же указано ключевое слово signed, то старший бит рассматривается как знак числа. По умолчанию все переменные целого типа считаются signed.

Диапазон значений целых типов

char

short

int

long

signed

от –128 до 127

от –32768 до 32767

системо-зависим (в нашем случае эквивалентен long)

от -2,147,483,648 до 2,147,483,647

unsigned

от 0 до 255

от 0 до 65535

от 0 до 4,294,967,295

Замечание: тип char обычно используется для представления кода символа (ASCII).

2)  Логический тип bool (boolean) – может принимать одно из двух значений: истина (true) или ложь (false). Логические переменные используются для выражения результатов логических операций. Например:
i!= 0 - выражение принимает значение true или false в зависимости от величины i.
bool b = x==y; //если x равен y, то b примет значение true, в противном случае false.
При преобразовании к целому типу true имеет значение 1, а false – 0. Например:
int n = true; //n примет значение 1
И наоборот, целые можно неявно преобразовать в логические значения. При этом ненулевые целые преобразуются в true, нулевые – в false. Например:
bool b = 2; //b принимает значение true

3)  Типы с плавающей точкой: float, double, long double. Действия над числами с плавающей точкой выполняются специальным функциональным блоком – сопроцессором. Числа представляются в стандарте IEEE 754, который предусматривает три формата: 32(float), 64(double) для “внешнего” представления и 80(long double) бит для внутреннего представления чисел в сопроцессоре. Использование 80-битового формата на уровне языка Си не реализовано.

4)  Пустой тип – void. Если тип void используется в качестве возвращаемого функцией типа, это означает, что функция ничего не возвращает. Использование void в качестве списка параметров функции означает, что функция не принимает никаких параметров.
Пример:
void Func(void); //функция Func ничего не принимает и ничего не возвращает
Если объявлен указатель на тип void, это означает, что объявлен «универсальный» указатель на любой тип данных.
Нельзя объявить переменную типа void, так как компилятор «не знает», сколько нужно зарезервировать памяти.

5)  *enum (перечисление)– задает набор целых значений, определяемый программистом. Используется для создания более «читабельных» программ, так как позволяет использовать мнемонические идентификаторы для набора констант. Рассматривается в данном разделе как дополнение к базовым типам данных, так как при работе с перечислениями компилятор осуществляет дополнительный контроль (т. е. не только типа, но и) диапазона значений.
Пример:

//В качестве элементов перечисления можно определить именованные целые константы

enum{ZERO, ONE, TWO}; //определяет три целые константы и присваивает им значения 0,1,2

//Перечислению можно присвоить имя (или объявить тип):

enum number{ZERO, ONE, TWO}; //number является типом.

number num1; //объявление переменной типа number подсказывает программисту и компилятору, как предполагается использовать данную переменную (она может принимать значения только из списка инициализации)

//Список инициализирующих значений

enum DAYS{MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY}; // При отсутствии явных инициализаторов перечисляемые константы принимают значения: MONDAY==0, TUESDAY==1, WEDNESDAY==2...

enum DAYS{MONDAY=1, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY=10, SUNDAY= SATURDAY+5}; // MONDAY==1, TUESDAY==2 ..., //SATURDAY==10, SUNDAY==15

enum DAYS{MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY=10, SUNDAY=10}; // значения констант не обязаны быть уникальными: MONDAY==0, TUESDAY==1..., SATURDAY==10, SUNDAY==10

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

Пример:

DAYS week_day; //переменная типа DAYS

week_day = MONDAY; //корректно

MONDAY = 4; //ошибка, так как MONDAY – это константа

int n= MONDAY; //n будет присвоено значение 0. Любой целочисленной переменной может быть присвоено значение из инициализирующего списка

week_day = 0; //ошибка, несмотря на то, что MONDAY==0

week_day = (DAYS) 0; // week_day примет значение MONDAY, но непонятно: зачем могла бы понадобиться такая запись, поэтому лучше этим не пользоваться

week_day = DAYS (0); //то же, что и в предыдущей строке

5. Константы.

Данные, обрабатываемые программой, - это константы и переменные. Тип константы распознается компилятором по ее записи в тексте программы. Например:

1) целочисленные константы:

int n = 10; //без префикса - десятичная константа (n=10)

int n = 010; //префикс «0» - восьмеричная константа (n=8)

int n = 0x10; // префикс «0x» - шестнадцатиричная константа (n=16)

2) вещественные константы: 456.789, .123, 1., 18e-3.

3) символьные константы (заключенные в одиночные кавычки): ‘A’, ‘a’, ‘ ‘, ‘?’.

4) управляющие или ESC-последовательности (для графического представления неграфических символов используется символ обратного слэша – «\»). Например:

‘\n’ – новая строка

‘\f’ – перевод формата

Если после «\» следует восьмеричное или шестнадцатиричное число, то компилятор интерпретирует его как управляющий код, соответствующий этому значению, или как представление символа его числовым эквивалентом (кодом ASCII). Например:

‘\x63’ – представление управляющего кода Ctrl+C

‘\x3f’ – ASCII представление символа «?»

5) строковые константы (заключенные в двойные кавычки): «Пример строковой константы». Следует помнить, что компилятор при генерации такой константы добавляет последний нулевой байт.

6. Свойства переменных.

6.1. Объявление переменной

Все переменные в Си-программе должны быть объявлены (явно описаны) до их первого использования.

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

[спецификатор_класса_памяти] тип идентификатор [=начальное значение];

Рекомендация (Бьерн Страуструп): не объявляйте переменную, пока она Вам не потребуется! Это сделает Вашу программу более «читабельной», и в большинстве случаев позволит сразу же переменную инициализировать.

6.2. Имя / идентификатор

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

Для повышения читабельности текста программы приняты некоторые (необязательные, но рекомендуемые) соглашения об использовании имен переменных. В начало имени переменной добавляется префикс, который соответствует типу данных и записывается маленькими буквами. Само имя начинается с большой буквы. Для указателей добавляется префикс «p» (от «pointer»). Имена функций могут состоять из нескольких слов, которые пишутся слитно, но каждое из них начинается с большой буквы. Пример:

int iNumber; //целое

int* piPointer; //указатель на целое

void MyFunc(void); //функция

Нельзя в качестве идентификатора использовать ключевые слова языка С++. Ключевые слова – это предопределенные идентификаторы, которые имеют специальное значение для компилятора, такие как char, if, void, while и другие.

6.3. Время существования

– это интервал выполнения программы, в течение которого переменная существует.

Статическое время существования – объекты существуют на всем протяжении выполнения программы - extern, static. Место под такие переменные отводится в области памяти данных.

Локальное время существования - создаются при входе в окружающий их объявление блок и освобождают память при выходе из блока - register, auto. Для локального объекта при каждом входе в блок распределяется новая область памяти. Место под локальные переменные отводится в стеке или регистрах.

Динамическое время существования - создаются при вызове программистом определенных функций типа malloc() или оператора new и разрушаются при вызове free() или delete. Для таких переменных память выделяется из специального буфера памяти, называемого heap, а продолжительность существования определяется программистом.

6.4. Область действия

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

6.5. Область видимости

-обычно ( но не всегда) совпадает с областью действия (локальная и глобальная) и не может быть больше последней (бывают случаи, когда глобальный объект временно скрыт локальным объектом с тем же идентификатором, тогда для доступа к глобальному объекту требуется специальный оператор разрешения области видимости – “::”). Например:

int AAA; //глобальная переменная

void main()

{

AAA = 5; //глобальная переменная принимает значение 5

int AAA=1; //локальная переменная = 1. С этого момента времени компилятор под именем AAA «видит» локальную переменную

::AAA++; //а глобальную AAA можно «увидеть» с помощью оператора разрешения области видимости

}

Замечание: для вложенных локальных переменных с одинаковым именем не существует способа обращения к скрытой локальной переменной:

void main()

{

int BBB = 1; // локальная переменная принимает значение 1

{ //вложенный блок

int BBB=4; //локальная переменная = 4. С этого момента времени вложенная переменная BBB «скрывает» переменную с аналогичным именем во внешнем блоке. Пока блок не закончится, нет возможности доступа к скрытой локальной переменной

}

}

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

// файл – my. cpp

int version;

// другой файл – other. cpp

int version;

В такой ситуации при сборке проекта компоновщик выдаст ошибку –«множественное определение имени», хотя под этим именем по замыслу программиста две совершенно разных переменных. Для того, чтобы избежать таких ситуаций в современных версиях языка С++ введено понятие пространства имен – namespace. Пример объявления:

// файл – my. cpp

namespace one //”one” – это имя пространства имен

{

int version;

}

// другой файл – other. cpp

namespace other //”other” – это имя другого пространства имен

{

int version;

}

Пространство имен присоединяет дополнительный идентификатор ко всем именам, перечисленным в фигурных скобках, что обеспечивает некоторую защиту от того, что где-то в другом месте в программе употребляется то же имя. Каждое имя становится уникальным в своем пространстве имен и добраться до него можно только с помощью указания самого имени в качестве квалификатора и оператора разрешения области видимости - «::». Пример использования:

{

...

one::version = 1;

other::version = 2;

}

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

Посредством пространства имен С++ также позволяет объединять данные и функции «по смыслу»:

namespace Version

{

int version;

int previousVersion;

SetVersion(int ver);

int GetVersion();

...

}

Если имя, требующее квалификатора, используется часто, довольно утомительно каждый раз писать этот квалификатор. Такое неудобство можно устранить с помощью директивы – “using”. Директива using делает доступными имена из данного пространства имен, как если бы они были объявлены глобально.

Пример использования средств ввода-вывода стандартной библиотеки (стандартная библиотека определена в пространстве имен std):

1)  без директивы using:

#include <iostream>

void main()

{

std::cout<<”Hello, World!\n”; //стандартная библиотека определена в пространстве имен std

}

2)  С помощью директивы using:

#include <iostream>

void main()

{

using namespace std; //все имена из std объявляю глобальными

cout<<”Hello, World!\n”; //теперь квалификатор std не нужен

}

Замечание: одно пространство имен можно сделать “видимым” из другого с помощью директивы using следующим способом:

namespace Inner{

enum Token_value{NAME1, NAME2, NAME3};

Token_value current_token;

...

}

namespace Outer{

...

using namespace Inner; //делает доступными все имена из Inner

using Inner:: current_token; //если требуется иметь доступ только к какому-то одному имени из Inner

}

6.6. Размещение в памяти (спецификаторы класса памяти).

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

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

auto – класс памяти для внутренних (объявленных внутри блока) переменных по умолчанию, поэтому обычно этот спецификатор явно не указывается. Место для такой переменной отводится в стеке. Переменная с классом памяти auto «видима» только в том блоке, в котором объявлена. Память для такой переменной выделяется при входе в блок и освобождается при выходе из блока.

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

extern – объявление переменной со спецификатором класса памяти extern определяет тип переменной и информирует компилятор о том, что память для этой переменной распределяется где-то в другом месте программы (память выделяется только один раз!). Память под такую переменную отводится в области данных.

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

7. Правила приведения типов.

7.1. Неявное приведение типов.

При использовании в арифметическом выражении переменных разных типов компилятор в ходе вычисления перед выполением действия делает внутренние преобразования (неявное приведение типов):

а)операнды разных типов, участвующие в одной операции при вычислении выражения (справа от знака =), приводятся к «старшему», то есть более длинному типу. Типы, начиная от старшего в порядке убывания старшинства:

long double, double, float, unsigned long, long, unsigned int, int, unsigned short, short, unsigned char, char.

Замечание 1: «короткие» целые типы (short, char) для повышения эффективности вычислений приводятся к типу int (с учетом «знаковости»)

Замечание 2: при преобразовании младшего целого типа к старшему целому типу (“расширенные”) старшие биты могут заполняться либо нулями, либо знаковым (старшим) битом преобразуемого значения (распространение знака, sign extention). Общее правило состоит в следующем: при преобразовании младшего signed типа к старшему (signed или unsigned) типу “расширенные” старшие биты заполняются знаковым разрядом преобразуемого значения; при преобразовании младшего unsigned типа к старшему (signed или unsigned) типу “расширенные” старшие биты заполняются нулями.

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

б) при выполнении операции присваивания результат приводится к типу переменной слева от знака операции. В этом случае может возникнуть ситуация преобразования старшего типа к младшему!

Преобразование старших целых типов в младшие выполняется отбрасыванием старших байтов.

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

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

7.2. Явное приведение типа:

При явном приведении типов вся ответственность за результат ложится на программиста, который должен учитывать правила, перечисленные выше, для простых типов данных, а также особенности приведения типов для указателей.

Не рекомендуется без оправданной необходимости (особенно начинающим программистам) пользоваться явным преобразованием типа.

Явные преобразования типа в стиле С:

int x=2, y=1;

double Result = (double)x/y; //достаточно явного приведения к типу double только одного из слагаемых – второе будет приведено к типу double неявным приведением типов компилятором

стали нежелательны после введения операторов приведения в новом стиле в С++=> там где необходимо явное преобразование, следует пользоваться операторами static_cast, reinterpret_cast, const_cast, dynamic_cast или их сочетаниями. Хотя эти операторы предназначены для обеспечения некоторой защиты от «злостного» программиста при явных приведениях типа, использованием const_cast, reinterpret_cast тоже злоупотреблять не стоит (они «недалеко ушли» от старого стиля С, а введены скорее для единообразия с static_cast и dynamic_cast в новом стиле).

static_cast <type> (выражение)

reinterpret_cast <type> (выражение)

const_cast <type> (выражение)

dynamic_cast <type> (выражение)

преобразование с проверкой во время компиляции

преобразование без проверки

константное преобразование

преобразование с проверкой во время выполнения

осуществляет преобразование базовых типов и указатель void* к указателю на любой тип

осуществляет преобразование не связанных между собой типов

аннулирует действие модификаторов const и volatile

осуществляет преобразование указателей (ссылок) на объекты базовых-производных классов

Примеры применения операторов явного преобразования:

typedef unsigned char BYTE;

void f()

{

int i = 65;

float f = 2.5;

char ch = static_cast<char>(i); // int to char

double dbl = static_cast<double>(f); // float to double

...

BYTE uch = static_cast<BYTE>(ch);

...

}

8. Модификаторы const и volatile

1)  Объявление переменной с ключевым словом const создает переменную только для чтения. Такая переменная должна быть инициализирована при объявлении. Любая попытка изменить ее значение вызовет ошибку компиляции. Например:

const int nMax=1000; // корректно

int nX = nMax; //корректно

const int nMin; // ошибка компилятора

nMax = 2000; //ошибка компилятора

Рекомендуется объявление переменных с модификатором const для обозначения констант вместо директивы #define, так как в первом случае компилятор осуществляет контроль типов.

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

volatile int nTicks; // счетчик тиков таймера

#define INTERVAL 1000

Программа_обработки_прерываний_от_таймера()

{

nTicks++; //увеличение значения счетчика тиков на единицу

}

Программа_ожидания_истечения_интервала_времени()

{

nTicks = 0;

while(nTicks < INTERVAL); //пока счетчик тиков меньше INTERVAL, ничего не //делать

}

9. Спецификатор типа typedef

Спецификатор typedef не создает новых типов данных, он просто создает полезные мнемонические синонимы для существующих типов. Например:

typedef long BIGGY; //определен синоним для типа long. Объект не создается!

BIGGY myBig; //создается объект с типом BIGGY, то есть с типом long

Особенно большое значение имеет использование typedef для упрощения сложных определений.

Замечание: нельзя использовать спецификатор typedef с другими спецификаторами типов данных:

unsigned BIGGY yourBig; //ошибка компилятора

Методические указания по выполнению работы:

Типовая последовательность действий, которой рекомендуется следовать, выполняя каждое задание:

1.  Создать свой индивидуальный рабочий каталог

2.  Запустить ИСР, создать новый проект, для которого указать путь к созданному рабочему каталогу (при этом будет автоматически создан вложенный новый рабочий каталог проекта)

3.  Скопировать в рабочий каталог проекта файл-заготовку (файлы) для выполнения работы и затем включить этот файл в проект

После этого можно редактировать, компилировать и отлаживать.

Замечание1: в рамках интегрированной среды разработки понятие проект включает в себя совокупность файлов, содержащих исходные тексты программы, а также конфигурационную информацию, нужную ИСР для правильной генерации исполняемого кода. Информацию о проекте ИСР хранит в файле проекта, имеющем расширение *.dsp.

Замечание2: понятие Workspace объединяет конфигурацию открытых окон и различные средства просмотра этих окон– закладки внизу Project Workspace-окна позволяют разными способами посмотреть на свой проект. Доступные в данном проекте способы просмотра зависят от того, какие продукты Developer Studio инсталлированы Вами и с какого рода проектом Вы работаете. Способы просмотра проекта: FileView, ClassView, ResourceView. Информацию о представлении проекта ИСР хранит в файле проекта, имеющем расширение *.dsw.

Как создать новый проект:

Для создания нового проекта откройте пункт меню «File à New» и на закладке «Projects» выберите пункт «Win32 Console Application». В поле ввода «Location» укажите путь к Вашему рабочему каталогу, а в поле «Project name» - придуманное Вами имя проекта.

Чтобы включить в проект нужный файл, нужно воспользоваться пунктом меню «ProjectàAdd To ProjectàFiles...». В появившемся окне стандартного диалога выберите файлы, которые хотите включить в проект.

В результате выполнения задания слушатель должен :

1) Получить навыки работы в интегрированной среде:

создание проекта с требуемыми свойствами в требуемой директории;

включение файлов в проект;

редактирование исходного текста программы;

получение исполняемого кода (compile, build, build all);

отладка (breakpoints, run, step, go to cursor, как посмотреть значения переменных);

получение справки;

2) На практике закрепить:

структура программы – основные понятия,

некоторые свойства переменных разных типов,

использование директив препроцессора,

формирование условных выражений,

поразрядная арифметика,

применение инструкций циклов.