Величины

Типы данных

Язык С, как впрочем и любой другой язык программирования, требует от программиста описания величины, на основании которого компилятор выделяет под нее память и определяет, какие операции с данной величиной являются допустимыми. Описание в С — это сложная конструкция, состоящая из описателей, имеющих различный смысл. В языке С для полного описания переменной указывается ее тип и класс памяти. Начнем с простого перечисления описателей (табл. 2.1).

Таблица 2.1. Базовые типы языка С

Тип Длина в битах Интервал значений
unsigned char 8 0 — 255
char 8 –128 — 127
unsigned int 16 0 — 65 535
short 16 –32 768 — 32 767
int 16 –32 768 — 32 767
unsigned long 32 0 — 4 294 967 295
long 32 –2 147 483 648 — 2 147 483 647
float 32 3,4?10–38 — 3,4?1038
double 64 1,7?10–308 — 1,7?10308
long double 80 3,4?10–4932 — 1,1?10–4932

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

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

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

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

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

Переменная, объявленная как переменная класса static, хранится в статической памяти (не стековой), и ее значения сохраняются и по завершению работы составного оператора. Для иллюстрации рассмотрим простой пример (листинг 2.3).

Листинг 2.3

#include <iostream.h>

#include <conio.h>

int f(int);

void main()

{ clrscr();

  cout << f(1)<< " ";

  cout << f(1);

}

int f(int t)

{ static int a=t;

  a++;

  return a;

}

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

static a=t;

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

Необходимо отметить, что глобальные переменные являются переменными класса static, даже если они и не объявляются, как таковые.

Расположение объявлений

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

if  (y==5) float a=8.1;

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

if (y==5) {float a=8.1;}

Данный нюанс предупреждает казус. Отсутствие объявления до первого использования есть синтаксическая ошибка. Но рассмотрим следующий фрагмент:

{ if (i!=7) int y=8;

  int sum+=y;

}

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

{ int y;

  if (i!=7) y=8;

  int sum+=y;

}

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

Инициализация переменных

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

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

Примечание

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

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

int a;

a=8;

Это же самое  возможно сделать так:

int a=8;

Впрочем, если подходить более строго, то в первом случае инициализации нет вовсе, а оператор a=8 — это просто оператор присваивания. Однако здесь может возникнуть вопрос: если эти ситуации неразличимы, то зачем введен термин "инициализация"? Новый термин должен содержать в себе новый смысл. Новый смысл действительно есть. Заключается он в задании исходного значения переменной. Однако для автоматических переменных различие имеет косметический характер, просто инициализировать переменные в момент объявления удобно, это позволяет немного сократить текст.

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

Листинг 2.4

#include <iostream.h>

#include <conio.h>

int f(int);

void main()

{ clrscr();

  cout << f(1)<< "  ";

  cout << f(1);

}

int f(int t)

{ static int a;

  a=t;

  a++;

  return a;

}

Разница заключается в том, что переменная объявлена, а первое значение получено одним оператором позже. Различие принципиальное. Дело в том, что все операторы функции f выполняются при каждом ее вызове, а инициализация переменной класса static выполняется только один раз при вхождении в тело функции. Поэтому в измененном примере величина a после своего объявления каждый раз получает значение 1, и результат работы функции оба раза равен 2.

Более подробно о времени жизни и области видимости

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

{ int x=1;

  { int x=2;}

  cout << x;

}

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

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

Игнорирование этого правила может привести к очень грубым ошибкам. Приведем примеры организации вложенного цикла (табл. 2.2).

Таблица 2.2. Примеры организации вложенного цикла

Блок без ошибки Блок с ошибкой
for (int i=1; i<=5; i++)

 { for (int i=1; i<=5; i++)

 int x:=i*i;

 }

for (int i=1; i<=5; i++)

 { for (i=1; i<=5; i++)

 int x:=i*i;

 }

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

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

Листинг 2.10

int x;

void main();

 { { int x=2;  // Присваивание локальному х //

       :: x=3; // Присваивание глобальному х //

    }

 }

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

{ static int x;

  { ::x=8;

  }

} 

Причем она ошибочна, даже не глядя на то, что переменная x объявлена как переменная класса static. Этого не достаточно. Существенно важен именно глобальный характер области видимости, а переменная, объявленная внутри составного оператора, имеет все же локальную видимость.

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

Классы памяти

Язык С позволяет описывать для переменных не только имена и типы, но и так называемые классы памяти, определяющие самый общий способ хранения значений переменных. Ранее мы уже рассматривали свойства класса памяти static. Это не единственный класс памяти. Их в языке С целых четыре, и сейчас мы рассмотрим их подробнее:

q        auto;

q        static;

q        register;

q        extern.

Объявления внутреннего уровня

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

Переменная, объявленная как переменная класса static — это переменная с глобальным временем жизни. Но ее область видимости ограничена тем составным оператором, в котором она объявлена. Глобальное время жизни означает, что значение переменной сохраняется при выходе из оператора. Это же самое означает, что операция инициализации таких переменных выполняется только один раз, в момент первого вхождения в составной оператор, и повторной инициализации уже не происходит. Здесь ограничение по памяти определяется двумя существенными факторами. Во-первых, операционной системой, под которую создан компилятор, и, во-вторых, выбранной моделью памяти для компиляторов, работающих под управлением MS-DOS.

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

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

Объявления внешнего уровня

Переменные, объявляемые на внешнем уровне, могут быть только двух классов: static и extern. Если класс переменной не указан, то по умолчанию он static. Время жизни глобальных переменных равно времени выполнения программы, область видимости для переменной класса static — данный файл, для переменной класса extern — все файлы программы. Класс памяти для функций объявляется  так же как и для переменных. Отличия выражаются лишь тем обстоятельством, что для функции единица размещения есть файл, а для переменной единица размещения есть составной оператор.

О преобразованиях типов

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

int a=8;

char r=a;

вполне может иметь место. Для языка С такое присвоение вполне законно, поэтому чтобы обеспечить корректность выполнения присвоения, необходимо что-то сделать с лишним байтом целой величины. Вполне возможна и обратная операция:

char a='g';

int t =a;

В этом случае длины символьной величины недостаточно, чтобы заполнить целую, и здесь противоположная проблема: что делать с недостающим байтом? Решение этих двух проблем — где взять недостающие байты и что делать с лишними — и есть преобразование типов.

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

 

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

float a=6.897;

cout << int(a);

В этом примере показан прием явного выполнения преобразования.

Объявления вида typedef

Язык С позволяет создавать объявления следующего вида:

typedef int example;

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

example n;

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

typedef struct example

{ char a[100];

  int s;

  double sum;

};

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

 

Ввод/вывод

Форматный ввод/вывод

Прежде всего, необходимо сказать, что средства ввода/вывода не являются составной частью языка. Все, что мы будем рассматривать в этой главе — это операции, поддерживаемые специальными библиотеками, разработанными для конкретных реализаций языка С.

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

Функции форматного ввода и вывода имеют следующий синтаксис:

q        функция вывода

int printf(const char *format [, argument, ...]);

q        функция ввода

int scanf(const char *format [, address, ...]);

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

Таблица 3.1. Описатели формата

Символ Ожидаемый ввод Формат вывода
d Целое Целое десятичное со знаком
i Целое Целое десятичное со знаком
o Целое Беззнаковое восьмеричное целое
u Целое Беззнаковое десятичное целое
x Целое Беззнаковое шестнадцатеричное целое с цифрами (a, b, c, d, e, f)
X Целое Беззнаковое шестнадцатеричное целое с цифрами (A, B, C, D, E, F)
f Действительное Число со знаком в форме
[-]dddd.dddd
e Действительное Число со знаком в форме [-]d.dddd или e[+/-]ddd
e Действительное То же что и e. В экспоненциальной форме
c Символ Одиночный символ
s Указатель на строку Печатаются символы до тех пор, пока не будет достигнут терминальный ноль

В примере из листинга 3.1 вводятся две переменные, находится их сумма и выводится на экран. И для ввода и для вывода используется только один описатель — описатель целого числа.

Листинг 3.1

#include <stdio.h>

#include <conio.h>

void main(void)

{ clrscr();

  int a,b;

  scanf("%d%d",&a,&b);

  int sum=a+b;

  printf("Summa %d",sum);

}

Следующий пример демонстрирует, что значение числа не связано жестко с числовой системой (листинг 3.2). Число можно ввести как десятичное и использовать его как десятичное, а в выводе определить его формат как-то иначе.

Листинг 3.2

#include <stdio.h>

#include <conio.h>

void main(void)

{ clrscr();

  int r=29997;

  printf("Десятичное %d Восьмеричное %o Шестнадцатеричное %x",

          r,r,r);

}

Функция printf выводит одно и то же число в трех различных форматах: как десятичное число, как восьмеричное и как шестнадцатеричное. Необходимо, конечно, помнить, что формат вывода и формат представления числа — это не одно и то же.

Потоковый ввод/вывод

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

Поток ввода управляется командой cin, поток вывода управляется командой cout. Количество вводимых структур для одного объявления ввода или вывода не ограничено. Поэтому вполне допустимы следующие записи:

cout << a << b << c << d;

cin  >> a >> b >> c >> d;

Операции в С

Список операций

Язык С предлагает большой набор арифметических операций. Его основа, конечно, обычные арифметические операции:

q        * — умножение;

q        / — деление;

q        + — сложение;

q        - — вычитание.

Арифметические  выражения строятся по обычным математическим правилам. Умножение и деление имеют более высокий приоритет, чем сложение и вычитание, операции в скобках имеют более высокий приоритет, чем за скобками.

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

Таблица 4.1. Операции в С

Операция Действие
= Простое присваивание
+= Вычисление выражения, записанного в правой части с последующим суммированием
-= Вычисление выражения, записанного в правой части с последующим вычитанием
*= Вычисление выражения, записанного в правой части с последующим умножением
/= Вычисление выражения, записанного в правой части с последующим делением
%= Вычислить остаток и присвоить
<<= Сдвинуть влево и присвоить
>>= Сдвинуть вправо и присвоить
&= Выполнить операцию И, а затем присвоить
|= Выполнить включающее ИЛИ, а затем присвоить
^= Выполнить исключающее ИЛИ, а затем присвоить
++ Приращение до или приращение после
-- Уменьшение до или уменьшение после
+ Унарный плюс (также сложение)
- Унарный минус (также вычитание)
* Умножение
/ Деление
% Вычисление остатка
< Меньше
<= Меньше или равно
> Больше
>= Больше или равно
== Равно
!= Не равно
& Побитовое И
^ Побитовое исключающее ИЛИ
| Побитовое включающее ИЛИ
&& Логическое И
|| Логическое включающее ИЛИ
! Логическое отрицание
?: Арифметический if
:: Разрешение области видимости
. Выбор члена
-> Выбор члена
.* Выбор члена
->* Выбор члена
[] Индексация
() Построение выражения или вызов функции
Sizeof Размер объекта или типа
& Адрес объекта
* Разыменование
New Создание, размещение объекта
Delete Уничтожение объекта (освобождение области памяти)
delete[] Уничтожение массива
() Преобразование типа
<< Сдвиг влево
>> Сдвиг вправо
, Следование

 

Старшинство и порядок вычисления

В приводимой табл. 4.4 сведены правила старшинства и ассоциативности всех операций, включая и те, которые мы еще не обсуждали. Операции, расположенные в одной строке, имеют один и тот же уровень старшинства; строки расположены в порядке убывания старшинства. Так, например, операции *, / и % имеют одинаковый уровень старшинства, который выше, чем уровень операций + и -.

Таблица 4.4. Приоритет операций

Операции Порядок вычисления
(), [], ->, . Слева направо
!, \, ^, ++, --, -, (type), *, &, sizeof Справа налево
*, /, % Слева направо
+, - Слева направо
<<, >> Слева направо
<, <=, >, >= Слева направо
==, != Слева направо
& Слева направо
^ Слева направо
&& Слева направо
?: Справа налево
=, +=, -= Справа налево
, Слева направо

 

Операторы языка С

Циклы

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

Однако необходимо заметить, что название "цикл с параметром" — не совсем точное название для той конструкции, которую дает в наше распоряжение С. Те, кто имеет опыт программирования на языках Бейсик или Паскаль, очень быстро заметят разницу. Классический цикл с параметром — скорее, частный случай того, что предлагает язык С. Рассмотрим пример:

for ( int i=1;i<=N;i++) cin >> A[i];

Здесь есть тело цикла, состоящее из одной команды ввода, и заголовок, в котором записано начальное значение параметра, его конечное значение и шаг изменения. Это действительно не более, чем частный случай, потому как трем частям заголовка присвоен конкретный смысл, привязанный к термину "параметр". Чтобы лучше понять сказанное рассмотрим синтаксис:

for ([<выражение1>]; [<выражение2>]; [<выражение3>])

  <команда>

Здесь <команда> выполняется до тех пор, пока <выражение2> не примет значение 0 (в смысле логического значения).

<выражение1> вычисляется перед выполнением первого шага цикла. Обычно оно используется для инициализации цикла, но это лишь частный случай.

После каждого шага цикла вычисляется <выражение3>. Обычно это выражение используется для изменения параметра цикла, но и это не обязательно.

Обратите внимание, что все три выражения заключены в квадратные скобки []. Это означает, что все три выражения не являются обязательными и даже цикл for (;;;) с полностью пустым заголовком будет синтаксически правильным. Такое обобщенное понимание компонентов цикла дает большую свободу. Например, выражения, отвечающие за инициализацию и изменения в ходе работы цикла, могут быть сложными. Например, такими:

for (i=0, t=50; i < 40 && t>0; i++, t--)

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

Примечание об инициализации

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

Цикл с предусловием

Синтаксис:

while <условие>  <команда>

Здесь <команда> может быть как одной командой, так и составным оператором. <условие> — любое арифметическое выражение, значение которого воспринимается, как логическое. Как уже было сказано в начале главы, тело цикла этой формы выполняется после проверки условия, следовательно, если перед первым шагом цикла условие окажется ложным, тело цикла не будет выполнено ни одного раза. Цикл работает до тех пор, пока условие истинно. Напомним, что условие (так, как его понимает С) — это арифметическое выражение, следовательно, условием может быть почти все, что угодно, в том числе и сложное выражение, состоящее из нескольких элементарных, перечисленных через запятую (об операторе следование см. далее в этой главе).

Цикл с постусловием

Синтаксис:

do

{ <последовательность_команд>

}

while <условие>

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

Условный оператор

Синтаксис:

if (<условие>) <оператор1>; [else <оператор2>;]

В случае истинности проверяемого условия выполняется <оператор1>, в случае его ложности выполняется <оператор2>. Ключевое слово else не обязательно. Вполне допустима конструкция

if (<условие>) <оператор1>;

ifПредположим, необходимо присвоить некоей переменной (пусть ее имя b) значение 2, но только в том случае, если другая переменная (пусть ее имя d) равна 1, и в то же время третья переменная (например, r) равна 5. Это условие возможно записать так:

if (d==1)

  if (r==5) b=2;

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

if (d==1 && r==5) b=2;

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

 if (d==1)

  if (r==5) b=2;

  else b=3;

О чем говорит эта конструкция? О том, что для определения значения b величина d обязательно должна иметь значение 1. В противном случае наша конструкция из двух вложенных условий вообще не окажет никакого влияния на вычисление величины b. А если переменная d все-таки равна 1, то тогда выбор между двумя значениями осуществляется в зависимости от значения величины r. Если же мы вложенный выбор заменим одним со сложным условием, то смысл проверяемого условия существенно измениться.

if (d==1 && r==5) b=2;  else b=3;

Новое выражение говорит о том, что выбор между двумя значениями будет сделан обязательно в зависимости от того, ложно или истинно сложное условие. Здесь величина d не имеет особенной роли, она равноправна с величиной r. В то время, как в предыдущей конструкции при величине d, не равной единице, значение r не имеет никакого значения.

Вероятно, уже понятно, что ключевое слово else принадлежит ближайшей конструкции if. Далее для иллюстрации приведем чуть более сложный пример:

if (d==1)

   if (r==5) b=2;

   else b=3;

else b=4;

Здесь отступами показана сопринадлежность if и else. В этом примере значение величины b определяется так: если d не равно единице, то b=4. Если же равно, то значение b определяется из сравнения r==5. Если r равно 5, то выполняется b=2. Если же нет, то выполняется b=3.

Поставим теперь такую задачу. Пусть переменная d также играет определяющее значение и таким же образом участвует в определении величины b. Пусть также необходимо, чтобы при r=5 переменная b принимала значение 2. А при r<>5 величина b пусть имеет то же самое значение, которое она имела до входа в конструкцию выбора. Иными словами, необходимо выбросить else, входящий во внутреннюю конструкцию. То есть, сделать так:

if (d==1)

   if (r==5) b=2;

else b=4;

Но это совсем не то, что нам нужно. В таком варианте запись

else b=4;

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

if  (d==1)

  { if (r==5) b=2; }

else b=4;

Внутренняя конструкция условного оператора превращается в составной оператор, а запись else b=4; остается составной частью внешней конструкции выбора.

И последнее, и <оператор1>, и <оператор2>, указанные в синтаксисе, — это один, но возможно составной оператор.

Оператор-переключатель

Далее рассмотрим еще один оператор организации выбора, называемый оператором-переключателем. Начнем с примера работающей программы (листинг 5.5).

Листинг 5.5

#include <iostream.h>

#include <conio.h>

void main()

{ clrscr();

  int n;

  cin >>n;

  switch (n)

  { case 1: cout << "Первый";

    case 2: cout << "Второй";

    case 3: cout << "Третий";

  }

}

Значение переменной n, указанной в круглых скобках после ключевого слова switch, определяет точку передачи управления. Ключевые слова case с указанным числовым значением — это точки, в которые управление передается. Работает конструкция так: оператор определяет значение величины n, находит точку, значение которой равно значению n, и передает управление на найденную точку. Далее выполняются все операторы от точки, на которую было передано управление, и до закрывающей скобки оператора-переключателя, если выполнение не прерывается каким-либо образом. Следовательно, при n=1 программа выполнит все три оператора вывода. При n=2 два последних, а при n=3 только один последний. Сам оператор-переключатель не дает возможности выполнить только группу операторов, непосредственно определенных за точкой передачи управления. Но это можно сделать, воспользовавшись оператором прерывания. Вот так:

switch (n)

  { case 1: cout << "Первый";break;

    case 2: cout << "Второй";break;

    case 3: cout << "Третий";

  }

Новая конструкция case сработает несколько иначе. А именно, при n=1 отработает только первый оператор вывода, при n=2 — только второй, и, соответственно, при n=3 в сравнении с предыдущей программой ничего не изменится. После третьего оператора вывода оператор break не записан, т. к. на этом этапе работа конструкции case завершается.

Два рассмотренных выше примера должны были дать достаточное представление о сущности конструкции выбора. Рассмотрим точное описание ее синтаксиса.

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

{

  case <константа>: <последовательность_команд>

  [default: <последовательность_команд>]

}

Здесь <выражение> — это арифметическое выражение, значение которого сравнивается с константами. Ключевое слово default необходимо для пометки последовательности команд, выполняемой в том случае, когда значение выражения не совпало ни с одной константой. default не является необходимой частью конструкции, и если оно будет опущено, то при отсутствии подходящей константы в конструкции case не будет выполнено никаких команд.

Оператор break

Использование данного оператора уже рассматривалось ранее, но посвятим ему еще немного времени.

Оператор прерывает выполнение самого внутреннего из объемлющих его операторов do, for, switch, while. Управление передается оператору, следующему за тем оператором, который располагается за прерванным.

Составной оператор

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

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

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

do

{

   while(условие)

      {

}

while (условие);

      }

Здесь очевидное пересечение двух составных операторов, и этот фрагмент не получится даже откомпилировать.

Оператор продолжения continue

Данный оператор позволяет проигнорировать группу операторов, если в их выполнении уже нет необходимости. Используется внутри циклов for, do, while. Действие его заключается в передаче управления на следующий шаг цикла.

Оператор-выражение

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

void main()

{ int n;

  n+1;

}

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

Оператор goto

Это оператор, которым следует пользоваться очень аккуратно. Его чрезмерное участие в программе может сделать логику слишком запутанной и непрозрачной. Как правило, его можно убрать из программы, немного изменив ее структуру. С этим оператором тесным образом связано понятие метки. А именно, оператор передает управление на оператор с меткой. В примере из листинга 5.9 показано, как  это делается.

Листинг 5.9

#include <iostream.h>

void main()

{ int sum=0;

  for (int i=1;i<=10;i++)

   { sum+=i;

     if (i==5) goto metka;

   }

  cout <<"Вывод";

  metka:

  cout <<sum;

}

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

Пустой оператор ;

Точка с запятой в языке С является оператором, который не выполняет никаких действий.

Оператор возврата return

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

 

Константы

Простые константы

Константы — это объекты, значение которых не может быть изменено в ходе работы программы. Константа — достаточно обычное понятие, хорошо известное из математики. Но в программировании термин "константа" понимается несколько шире, чем в математике. Именно поэтому выше было сказано, что константа — это объект, а не просто число. Язык С предоставляет в распоряжение программиста числовые константы, а также символьные и строковые.

Говоря о константах, необходимо разделять два немного различных понятия: это именованная константа, т. е. имеющая имя и значение, и константа-значение, т. е. просто число, просто символ или просто строка. Просто число или символ может, например, появиться в выражении, неименованная строковая константа может появиться, например, в операции вывода.

cout << "Пример строковой константы";

В основном глава посвящена именованным константам. Числовые константы могут быть десятичными, восьмеричными и шестнадцатеричными.

Восьмеричная константа начинается цифрой ноль. Примеры: 0567, 0433, 01232.

Шестнадцатеричная константа начинается символами 0x или 0X. Примеры: 0x5643, 0x6757, 0xab454f, 0X6757, 0XAB657F.

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

cout <<12+03+0xa12;

Константы этого фрагмента заданы в трех системах счисления. Результат их сложения равен 2593 в десятичной системе счисления. Это число мы и увидим.

В листинге 6.1 приведен пример программы, в которой созданы 4 константы: целое число, действительное число, символ, строка символов.

Листинг 6.1

#include <iostream.h>

#include <conio.h>

void main()

{ clrscr();

  const int a=45;

  const float b=6.7;

  const char r='g';

  const char e[]="12345";

  cout <<a<<" "<<b<<" "<<r<<" "<<e;

}

Особое внимание в этом примере обратите на объявление строковой константы Квадратные скобки обязательны.  Строка символов в С — это фактически символьный массив, поэтому константа и объявляется как символьный массив. Указывать же длину этого массива как раз не обязательно. Так как это константа, то ее длина вполне может быть определена на этапе компиляции. Указание типа, является обязательным.

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

Можно сформулировать три правила описания констант:

q        значение любого типа можно использовать как константу, если задать ему имя и добавить ключевое слово const;

q        для описание множества целых констант допустимо использовать перечисление;

q        имя массива является константой.

Последнее утверждение требует небольшого пояснения. Рассмотрим любое объявление массива, например такое: int a[100]. Невозможность изменения этого имени означает, что в квадратных скобках нельзя использовать переменную величину, т. е. нельзя написать так:

int a[n];

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

const char a[]="123456"

"7890"

окажется строка "1234567890".

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

const char a[]="123""\0""45677"

Но, скорее всего, ваша программа будет считать, что за нулем нет больше символов. Обратите внимание, как вставлен в строку-константу терминальный ноль. Если это сделать вот так:

const char a[]="123\045677"

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

Перечисления

Перечисления можно рассматривать как способ объявления констант. Действие перечисления ограничено целыми числами, и существует две возможности объявления: во-первых, можно не указывать значения перечислителей, тогда по умолчанию первый перечислитель будет равен 0, второй — 1 и т. д. Во-вторых, значения перечислителей можно задать явным образом. В примере из листинга 6.5 показаны обе возможности.

Листинг 6.5

#include <iostream.h>

#include <conio.h>

void main()

{ clrscr();

  enum example1{a1,a2,a3};

  cout <<a1<<" "<<a2<<" "<<a3<<"\n";

  enum example2{a4=100,a5=200,a6=300};

  cout <<a4<<" "<<a5<<" "<<a6;

}

Константы, данные в перечислении, — это обязательно целые числа, поэтому перечисление следующего вида:

enum example{f=6,h=8.9};

будет незаконным. Константа h объявлена ошибочно.

 

Массивы

Общие правила работы с массивами

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

Синтаксис объявления массива таков:

Тип <имя_массива>[верхняя_граница_индекса]

Индекс изменяется от нуля до верхней границы минус единица. Таким образом, если дано объявление

int a[3];

это означает, что задан массив целых (двухбайтных) чисел с номерами: 0, 1, 2. Элемент a[3] уже выходит за определенные границы.

Есть возможность определить многомерный массив. Для этого достаточно определить несколько индексов. Вот так: float mas[5][12] — это двумерный массив, в котором первый индекс изменяется до четырех и второй до 11.

В листинге 7.1 приведен пример программы, выполняющей следующие действия:

1.         Вводится целочисленный массив.

2.         Положительные числа переписываются в массив b.

3.         Отрицательные числа переписываются в массив c.

Листинг 7.1

#include <conio.h>

#include <iostream.h>

void main()

{ int a[100],b[100],c[100],na,nb=-1,nc=-1;;

  clrscr();

  cin>>na;

  for (int i=0;i<na;i++) cin >>a[i];

  for (i=0;i<na;i++)

   if (a[i]>0) {b[++nb]=a[i];} else {c[++nc]=a[i];}

  cout<<"\n";

  for (i=0;i<=nb;i++) cout <<b[i]<<" ";

  cout <<"\n";

  for (i=0;i<=nc;i++) cout <<c[i]<<" ";

}

В программе заданы три массива целого типа и зарезервирована память на 100 элементов для каждого. Реально можно ввести и меньшее количество чисел. Больше нельзя. Но память, выделенная под 100 элементов, будет недоступна для других данных независимо от того, нужен этот массив или нет. Поэтому массив — очень полезная структура, но и очень затратная по памяти. Планируя использование массива,  вы должны точно указать верхнюю границу, и это обязательно должна быть константа. Заметьте, что благодаря собственным индексам, введенным для вспомогательных массивов, память как бы экономится, т. к. массивы b и c используются частично. Но это кажущаяся экономия. Ведь неиспользованные элементы объявленных массивов вернуть в свободную память нельзя. Для реальной экономии памяти надо было бы определить количество отрицательных и положительных чисел и уже потом объявлять массивы, но этого сделать нельзя. Попытка обойти это ограничение следующим образом:

int n;

n = реально необходимое число;

int mas[n];

будет отвергнута компилятором.

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

Листинг 7.4

#include <iostream.h>

#include <conio.h>

void main()

{ clrscr();

  int a[5]={1,2,3,4,5};

  for (int i=0;i<=4;i++)

   cout <<a[i]<<" ";

  cout <<"\n";

  int b[3][3]=

  { {1,2,3},

    {4,5,6},

    {7,8,9}

  };

  for (i=0;i<=2;i++)

   for (int j=0;j<=2;j++)

     cout <<b[i][j]<<" ";

}

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

char a[3]={'q','w','e'}

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

char a[4]={'q','w','e','\0'}

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

 

Многомерные массивы в языке С, если говорить точно, — это массивы массивов. То есть, объявление int a[5][5] читать, как двумерный массив, будет не верно. Правильнее сказать, что объявлено пять массивов по пять элементов в каждом. Такая разница в прочтении — совсем не игра слов. Если мы говорим о многомерном массиве, то имеется в виду, что как единое целое выступает только один объект — это весь двумерный массив, состоящий из элементов. Объявляя массив массивов, мы объявляем не один целостный объект (многомерный массив), а массив целостных объектов (одномерные массивы), а это уже большая разница.

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

Листинг 7.6

#include <conio.h>

#include <iostream.h>

void main()

{ clrscr();

  struct example{char a;int b;};

  example mass[2]=

   { {'q',1},

     {'h',2}

   };

  cout<<mass[1].a<<" "<<mass[1].b;

}

 

Структуры и объединения

Структуры

Рассмотрим пример (листинг 8.1).

Листинг 8.1

#include <iostream.h>

#include <conio.h>

void main()

{ clrscr();

  struct man{int value;char name;};

  man my;

  my.value=45;

  my.name='a';

  cout <<my.name<<"="<<my.value;

}

В этом примере переменная my содержит в себе две величины: величину целого типа с именем value и величину символьного типа с именем name. Для доступа к этим величинам необходимо указать общее имя переменной, под которым они объединены, в данном случае это имя my, а затем через точку — имя переменной, к которой необходим доступ.

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

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

struct my_struct{int a; char b;} my;

Результатом выполнения этой конструкции будет объявление переменной my типа my_struct. Описания структур могут быть очень сложными. Они могут содержать в своем описании массивы и даже описания иных структур.

Замечание об инициализации

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

struct example

 { int a=1;

   char b='a';

 }

Инициализация величин, имеющих тип структуры, возможна точно также, как и инициализация, например, массивов.

Объединения

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

struct example{ int a; char g;};

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

Относительно хорошее решение проблемы предлагает объединение. Это тоже структура, содержащая несколько полей, но на момент объявления переменной заданного типа объединение создает только одно поле из объявленных — именно то, которое нужно. Структура, записанная выше, будет выглядеть так:

union example { int a; char g;};

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

Замечание об инициализации объединений

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

union name

  {

    char a[5];

    int b;

    char c[10];

  };

 name my={5};

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

union name

  {

    int b;

    char a[5];

    char c[10];

  };

 name my={5};

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

 

 

Строки

Нультерминальные строки С

В современных компиляторах есть классы для самых разных типов строк, но любой компилятор языка С/С++ понимает так называемые нультерминальные строки. Фактически, строки языка С — это не новый тип данных, а символьный массив, для которого оговорено только одно специальное условие — такая строка должна завершаться символом \0, именуемым терминальным нулем, поэтому они и называются нультерминальными. Если это небольшое условие не соблюдать, то некоторые свойства строки у такого массива останутся, но лучше все же терминальный ноль не игнорировать, т. к. это даст в ваше распоряжение достаточно мощный набор функций, работающих со строками, как единым целым.

Как уже было сказано, типа "нультерминальная строка" в языке С нет. Но такую строку можно организовать, как указатель на динамический символьный массив. В листинге 10.1 приведен пример программы, в которой строка создается, вводится с клавиатуры и выводится на экран монитора.

Листинг 10.1

#include <iostream.h>

#include <conio.h>

void main()

{ clrscr();

  char *s;        // Объявление строки неопределенной длины

  s=new char[10]; // Создание строки в 10 символов

  cin >> s;

  cout << s;

}

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

cin >> *s;

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

Оператор

s=new char[10];

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

char *a="пример инициализации строки";

char b[]="еще один пример инициализации строки";

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

Набор функций, используемых для обработки нультерминальных строк, наверное, сильно зависит от реализации. Поэтому мы рассмотрим некоторые стандартные функции, используемые в реализации Turbo C. А затем посмотрим, технику работы с такими строками при условии, что ни одна из стандартных функций не известна.

Первый пример представлен в листинге 10.2.

Листинг 10.2

#include <iostream.h>

#include <conio.h>

#include <string.h>

void main()

{ clrscr();

  char *s;

  s=new char[10];

  cin >> s;

  for (int i=0;i<=strlen(s);i++)

  cout << s[i]<<"  ";

}

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

В следующем примере попробуем сложить две строки (листинг 10.3). Это достаточно распространенная операция, и нам необходимо научиться ее выполнять.

Листинг 10.3

#include <string.h>

#include <stdio.h>

void main(void)

{  char *dest;

   dest=new char[25];

   char *blank = " ", *c = " Язык С [А.С.1] , *turbo = "Turbo";

   strcpy(dest, turbo);

   strcat(dest, blank);

   strcat(dest, c);

   printf("%s\n", dest);

}

Эту же программу можно переписать в таком варианте (листинг 10.4).

Листинг 10.4

#include <string.h>

#include <stdio.h>

void main(void)

{  char dest[25];

   char *blank = " ", *c = " Язык С ", [А.С.2] *turbo = "Turbo";

   strcpy(dest, turbo);

   strcat(dest, blank);

   strcat(dest, c);

   printf("%s\n", dest);

}

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

Теперь попробуем переписать первый пример (см. листинг 10.2) без использования функции вычисления длины (листинг 10.5).

Листинг 10.5

#include <string.h>

#include <iostream.h>

#include <conio.h>

void main(void)

{  clrscr();

   char *s;

   s=new char[20];

   cin >> s;

   int num=0;

   while (s[num]!='\0') num++;

   cout << num;

}

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

Следующая программа выполняет операцию конкатенации. А именно, добавляет две строки, вводимые с клавиатуры к третьей пустой (листинг 10.6).

Листинг 10.6

#include <string.h>

#include <iostream.h>

#include <conio.h>

void main(void)

{  clrscr();

   char *a,*b,*c;

   a=new char[20];

   cin >> a;

   b=new char[20];

   cin >> b;

   c=new char[40];

   char *w;

   w=c;

   while (*a!='\0')

    {*c=*a;c++;a++;}

   while (*b!='\0')

    {*c=*b;c++;b++;}

   *c='\0';

   c=w;

   cout << c;

}

Еще одна стандартная операция над строками — это копирование части строки определенной длины, начиная с конкретной позиции (листинг 10.7).

Листинг 10.7

#include <string.h>

#include <iostream.h>

#include <conio.h>

void main(void)

{  clrscr();

   char *a,*b,*w;

   a=new char[100];

   b=new char[100];

   cin >> a;

   int n,count;

   cin >>n>>count;

   for (int i=1;i<n;i++) a++;

   w=b;

   for (i=1;i<=count;i++)

    { *b=*a;

      a++;b++;

    }

  *b='\0';

  b=w;

  cout << b;

}

Указатель a показывает на начало строки. Нам необходимо передвинуть его на n позиций. Эту операцию выполняет первый цикл for. После его завершения указатель a находится на позиции, с которой требуется начинать копирование, а указатель b на начале строки b. Во втором цикле указатель a проходит последовательно count элементов, каковые записываются в строку b. По завершению цикла к строке b добавляется терминальный ноль и указателю b присваивается его исходное значение, сохраненное в специальном указателе w.

Файлы данных

Неформатный ввод/вывод

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

Ввод/вывод в файл, как и ввод/вывод на консоль, возможен в двух вариантах: форматный и неформатный. Мы начнем изучение файлового ввода/вывода  с неформатного метода.

Сущность метода заключается в указании:

q        объекта, из которого считывается или в который записывается информация;

q        имени файла, хранящего информацию;

q        размер порции информации (в байтах);

q        количество порций, считываемых за один раз.

Таким образом работают две функции: fread — чтение данных из файла и fwrite — запись данных в файл. Далее приведен их точный синтаксис.

size_t fread(void *ptr, size_t size, size_t n, FILE *stream);

size_t fwrite(const void *ptr, size_t size, size_t n,

              FILE*stream);

Здесь:

q        size_t — тип, используемый для возврата информации о памяти, занимаемой объектом. Для дальнейших примеров не нужен, и в записи функций чтения/записи его можно игнорировать;

q        *ptr — адрес переменной, являющейся источником или, соответственно, местом назначения для читаемых (записываемых) байтов;

q        size — количество считываемых (записываемых) байтов;

q        n — количество повторений операции чтения (записи);

q        *stream — указатель на файл.

В примере, приведенном в листинге 12.1, источником данных является символьный массив, содержащий две буквы. Этот массив записывается в файл с именем file 10 раз, для чего в теле цикла используется функция записи, в которой указывается имя массива (как известно, имя массива содержит его адрес), указание на то, что записывать необходимо 2 байта. Количество операций — одна, и файловый указатель имеет имя — f.

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

FILE *fopen(const char *filename, const char *mode);

Функции требуется немногое: только имя файла и способ его открытия. Список возможных способов:

q        r — файл открывается только для чтения;

q        w — открывается для записи. Если файл с таким именем уже существует, то он будет переписан;

q        a — файл открывается для добавления данных к концу файла, а если такой файл не существует, то открывается для записи;

q        r+ — открывается существующий файл для обновления (чтения и записи);

q        w+ — открывается существующий файл для обновления (чтения и записи). Если такой файл уже существует, то он будет переписан;

q        a+ — открывается для обновления, начиная с конца файла. Если такой файл уже существует, то он будет переписан.

И последняя функция, используемая в данной программе, fclose — это функция, закрывающая файл. Ее синтаксис, видимо, ясен из примера.

Листинг 12.1

#include <stdio.h>

#include <iostream.h>

#include <conio.h>

void main()

{ FILE *f;

  clrscr();

  char a[2];

  a[0]='a';a[1]='b';

  f=fopen("file","w");

  for (int i=1;i<=10;i++)

    fwrite(a,2,1,f);

  char buf[2];

  fclose(f);

  f=fopen("file","r");

  for (i=1;i<=10;i++)

   { fread(buf,2,1,f);

     buf[2]='\0';

     cout <<buf;

   }

}

Отдельно стоит пояснить объявление FILE *f. FILE. Это структура данных, содержащаяся в файле stdio.h, и ее цель — описание величин, управляющих процессом ввода/вывода. Как именно она устроена, можно посмотреть в справочной системе компилятора. Мы же здесь подробно рассматривать эту структуру не будем, т. к. практически для любой задачи ввода/вывода достаточно объявления, принятого по умолчанию.

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

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

Листинг 12.3

#include <stdio.h>

#include <iostream.h>

#include <conio.h>

void main()

{ struct data{char a;int t;};

  data my,my1;

  my.a='a';

  my.t=45;

  FILE *f;

  clrscr();

  f=fopen("file","w");

  fwrite(&my,sizeof(my),1,f);

  fclose(f);

  f=fopen("file","r");

  fread(&my1,sizeof(my),1,f);

  cout <<my1.a<<" "<<my1.t;

}

Форматный файловый ввод/вывод

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

В первом примере (листинг 12.5) записывается, а затем считывается набор строк. Для этого используются функции форматного ввода/вывода. Для ввода по формату достаточно указать тип формата. В нашем примере указано, что записывается строка. Управляющий символ \n необходим для того, что указать в файле точку, в которой текущая строка завершается.

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

Листинг 12.5

#include <iostream.h>

#include <conio.h>

#include <stdio.h>

void main()

{ clrscr();

  char *y;

  y=new char[50];

  FILE *f=fopen("proba","w");

  for (int i=1;i<=5;i++)

   { scanf("%s",y);

     fprintf(f,"%s\n",y);

   }

  fclose(f);

  f=fopen("proba","r");

  for (i=1;i<=5;i++)

   { fscanf(f,"%s",y);

     printf("%s",y);

   }

}

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

Листинг 12.6

#include <iostream.h>

#include <conio.h>

#include <stdio.h>

void main()

{ clrscr();

  char y='h';

  FILE *f=fopen("proba","w");

  for (int i=1;i<=5;i++)

    fprintf(f,"%c%d\n",y,i);

  fclose(f);

  int t;

  f=fopen("proba","r");

  for (i=1;i<=5;i++)

   { fscanf(f,"%c%d\n",&y,&t);

     cout<<y<<t<<"  ";

   }

}

 

Hosted by uCoz