Архивы рубрики: Книги

Выражения и операторы в Си

C++ обладает небольшим, но гибким набором различных видов операторов для контроля потока управления в программе и богатым выбором операций для управления данными. То есть программа представляет собой последовательность строк. Каждая строка включает в себя одно
или более выражений, разделенных запятой. Основными элементами выражения служат числа, имена и операции *, -, +, – (унарный и бинарный) и =. Имена не всегда описываются до использования.

Применяемый метод синтаксического анализа обычно именуется рекурсивным спуском; это популярный и простой нисходящий метод. В таком языке, как C++, в котором вызовы функций вполне дешевы, кроме того, данный метод эффективен. Для любого правила вывода грамматики существует функция, вызывающая другие функции. Терминальные символы (например, END, NUMBER, + и –) определяются лексическим анализатором get_token(), а нетерминальные символы определяются функциями синтаксического анализа expr(), term() и prim().

Программа разбора для обнаружения ввода применяет функцию get_token(). Значение вызова get_token() определяется в переменной curr_tok; curr_tok принимает одно из значений перечисления token_value.

В любой функции разбора предполагается, что было обращение к get_token() и в curr_tok располагается очередной символ, подлежащий анализу. Это дает возможность программе разбора заглядывать на один лексический символ вперед и вынуждает функцию разбора читать на одну лексему больше, чем применяется правилом, для обработки которого она была вызвана. Каждая функция разбора определяет «свое» вы-
ражение и возвращает значение. Функ-ция expr() обрабатывает сложение и вычитание; она включает в себя простой цикл, который обнаруживает термы для сложения или вычитания.

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

Обработка ошибок в программах С++ не составляет большого труда. Функция обработки ошибок просто определяет ошибки, пишет сообщение об ошибке и возвращает управление обратно: Возвращение производится потому, что ошибки чаще всего встречаются в середине вычисления выражения, и поэтому следует или полностью прекращать вычисление, или возвращать значение, которое не должно привести к последующим ошибкам. Для обычного калькулятора больше подходит последнее. Если бы get_token() обнаруживала номера строк, то error() сообщала бы, где приблизительно обнаружена ошибка. Это было бы полезно, если бы калькулятор применялся неинтерактивно. Когда все части программы разделены, необходим только драйвер для инициализации и того, что связано с запуском. Например: Принято обычно, что main() возвращает ноль при обычном завершении программы и не ноль в противном случае, поэтому это прекрасно осуществляет возвращение числа ошибок.

 

Ссылка в языке Си

Ссылка — это другое имя объекта. Главное применение ссылок заключается в спецификации операций для типов, определяе-мых пользователем. Их можно также применять как параметры функции. Запись x& представляет собой ссылку на x.

К примеру:

int i = 1;
int& r = i; — r и i теперь ссылаются на один int
int x = r — x = 1
r = 2; — i = 2;

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

К примеру:

register int i;
register point cursor;
register char* p;

Описание register применяют только тогда, когда эффективность действительно важна. C++ позволяет записать значения основных типов: символьных констант, целых констант и констант с плавающей точкой. Также ноль (0) может применяться как константа любого указательного типа, и символьные строки служат константами типа char[]. Можно также определить символические константы. Символическая константа представляет собой имя, значение которого нельзя изменить в его области видимости. В C++ существует три вида символических констант:
1) любому значению любого типа можно присвоить имя и использовать его как константу, добавив к его описанию ключевое слово const;
2) множество целых констант может быть задано как перечисление;
3) любое имя вектора или функции является константой.

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

1) помещение в байт более одного небольшого объекта;
2) применение одного и того же пространства для хранения разных объектов в разное время.

Первое можно осуществить с помощью применения полей, второе — объединений.

 

Разыменование в языке Си

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

char c1 = ‘a’;
char* p = &c1 — в p хранится адрес c1
char c2 = *p; — c2 = ‘a’

Над указателями можно осуществлять определенные арифметические действия. К примеру, функция, подсчитывающая число символов в строке (не считая завершающего 0):

int strlen(char* p)
{
int i = 0;
while (*p++) i++;
return i;
}

Два структурных типа различны, даже когда они имеют одинаковые члены. К примеру:

struct s1 {int a;};
struct s2 {int a;};

являются двумя разными типами, поэтому

s1 x;
s2 y = x; — ошибка: несоответствие типов.

Структурные типы отличаются и от основных типов, поэтому
s1 x;
int i = x; — ошибка: несоответствие типов

Но существует механизм описания нового имени для типа, который не требует введения нового типа. Описание с префиксом typedef вводит не новую переменную данного типа, а новое имя этого типа. К примеру:

typedef char* Pchar;
Pchar p1, p2;
char* p3 = p1;

 

Имена переменных в Си

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

Приведем примеры последовательностей символов, которые не могут применяться как идентификаторы:

012 a fool $sys class 3var
pay.due foo~bar .name if

Буквы в верхнем и нижнем регистрах являются различными, т. е. Count и count — разные имена, но вводить имена, почти не отличающиеся друг от друга, нежелательно. Имена, которые начинаются с подчерка, по традиции применяются для специальных средств среды выполнения, поэтому применять такие имена в прикладных программах не стоит. Каждое имя (идентификатор) в C++ программе обладает ассоциированным с ним типом. Данный тип определяет, какие операции возможно применить к имени, а также как эти операции интерпретируются.

Целый тип char удобнее всего применять для хранения и обработки символов на некотором компьютере; обычно это 8-битовый байт. Размеры объектов C++ выражаются в единицах размера char, т. е. можно записать sizeof(char)==1. Тип unsigned char является беззнаковым, и при
его использовании имеем более переносимые программы, но при применении его вместо char могут появиться значительные потери в эффективности. Тип void (пустой) синтаксически проявляет себя как основной тип. Но применять его следует только как часть производного типа, объектов типа void нет. Он применяется для указания, что функция не возвращает значение, или в качестве базового типа для указателей на объекты неизвестного типа.

void f() — f не возвращает значение
void* pv; — указатель на объект неизвестного типа

Для большинства типов T T* служит типом указатель на T. То есть в переменной типа T* может располагаться адрес объекта типа T. Для указателей на вектора и указателей на функции необходимо пользоваться более сложной записью:

int* pi;
char** cpp; — указатель на указатель на char
int (*vp)[10]; — указатель на вектор из 10 int’ов
int (*fp)(char, char*); — указатель на функцию
— получающую параметры (char, char*)
— и возвращающую int

 

Описание и скрытие имен в языке Си

Описание определяет имя в области видимости.

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

Например:

int x; — глобальное x
f() {
int x; — локальное x прячет глобальное x
x = 1; — присвоить локальному x
{
int x; — прячет первое локальное x
x = 2; — присвоить второму локальному x
}
x = 3; — присвоить первому локальному x
}
int* p = &x — взять адрес глобального x

Скрытие имен обязательно при написании больших программ. Но читающий человек легко может не заметить, что имя скрыто, и некоторые ошибки, которые возникают вследствие этого, очень тяжело обнаружить, в основном потому, что они редкие. Применение для глобальных переменных имен i или x напрашивается на неприятности. С помощью операции разрешения области видимости :: можно применять
скрытое глобальное имя. Например:

int x;
f()
{
int x = 1; — скрывает глобальное x
::x = 2; — присваивает глобальному x
}

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

int x;
f()
{
int x = x; — извращение
}

 

Описание переменных в языке Си

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

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

char ch;
int count = 1;
char* name = «Bjarne»;
struct complex {float re, im;};
complex cvar;
extern complex sqrt(complex);
extern int error_number;
typedef complex point;
float real(complex* p) {return p->re;};
const double pi = 3.1415926535897932385;
struct user;

Большинство описаний служит также определениями; т. е. они к тому же определяют для имени сущность, к которой оно относится. Для ch, count и cvar этой сущностью служит соответствующий объем памяти, который должен применяться как переменная, — эта память будет выделена. Для real это заданная функция, для constant pi это значение 3.1415926535897932385, для complex этой сущностью служит новый тип. Для point это тип complex, поэтому point принимает смысл complex. Только описания extern complex sqrt(complex); extern int error_number;
struct user; не служат одновременно определениями. То есть объект, к которому они относятся, должен быть определен где-то еще. Код (тело) функции sqrt должен определяться определенным описанием, память для переменной error_number типа int должна выделяться другим
описанием, и какое-то другое описание типа user должно определять, что он из себя представляет. В C++ программе всегда должно присутствовать только одно определение каждого имени, но описаний может быть большое количество и все описания должны согласовы-
ваться с типом объекта, которого они касаются, поэтому в этом фрагменте есть две ошибки:
int count;
int count; — ошибка: переопределение

exnern int error_number;
exnern int error_number; — ошибка: несоответствие типов