close

Вход

Забыли?

вход по аккаунту

?

ОП Ответы

код для вставкиСкачать

Определение алгоритма
Классификация языков
Модификаторы доступа
Объявление переменных
Локальные переменные
Формальные параметры
Глобальные переменные
Спецификаторы хранения
Оператор присваивания
Инициализация переменных
Константы
Операторы
Оператор ?
Операторы указания & и *
Оператор sizeof
Операторы выбора
Оператор "запятая"
Выражения
Преобразования типов в выражениях
Принудительные преобразования типов
Циклы
Оператор break
Оператор continue
Метки и goto
Одномерный массив
Создание указателя на массив
Строки
Двумерные массивы
Массивы строк
Многомерные массивы
Индексация с помощью указателей
Размещение массивов
Функции
Оператор return
Правила видимости для функций
Аргументы функции. Передача по значению и передача по указателю
Передача массивов в функции
Аргументы функции main()
Возврат указателей
Рекурсия
Указатели на функции
Структуры
Доступ к членам структуры
Присваивание структур
Массивы структур
Указатели на структуры
Битовые поля
Объединения
Перечисления
Использование typedef
Ввод, вывод, потоки и файлы
Форматированный консольный ввод-вывод Модификаторы формата
Файловая система ANSI C
Запись и чтение символа
fclose( ) Использование feof()
Работа со строками: fgets() и fputs()
fread( ) и fwrite( )
Указатели
Односвязные списки
Обход односвязного списка
Бинарное дерево поиска. Вставка и поиск элемента по ключу в бинарном дереве поиска.
Бинарное дерево поиска. Удаление элемента из бинарного дерева поиска.
Обход бинарного дерева
Балансировка бинарного дерева поиска
ВОПРОС 1
Алгоритм - точное предписание, которое задаёт вычислительный процесс, начинающийся с произвольного исходного данного (из некоторой совокупности возможных для данного алгоритма данных) и направленный на получение полностью определяемого этим исходным данным результата.
Как правило, для данного алгоритма можно выделить 7 характеризующих его параметров: совокупность возможных исходных данных; совокупность возможных результатов; совокупность возможных промежуточных результатов; правило начала; правило непосредственной переработки; правило окончания; правило извлечения результата.
Исходный кодТекст программы, который можно читать. Обычно подразумевается сама программа. Исходный код вводится в компилятор.Объектный кодРезультат перевода исходного кода в машинный. Объектный код вводится в компоновщик.КомпоновщикПрограмма, компонующая отдельные откомпилированные функции в одну программу. Объединяет функции стандартной библиотеки С с кодом, написанным программистом. БиблиотекаФайл, содержащий стандартные функции, которые могут использоваться программой. Данные функции содержат все операции ввода-вывода, а также другие полезные подпрограммы.Время компиляцииСобытия, возникающие при компиляции программыВремя выполненияСобытия, возникающие при выполнении программы
Вопрос 2
Существуют различные классификации языков программирования.
По наиболее распространенной классификации все языки программирования, в соответствии с тем, в каких терминах необходимо описать задачу, делят на языки низкого и высокого уровня.
Если язык близок к естественному языку программирования, то он называется языком высокого уровня, если ближе к машинным командам, - языком низкого уровня.
Высокий уровень Ада
Модула-2
Паскаль
COBOL
Фортран
Бейсик Средний уровень С++
С
FORTH
Макро Ассемблер Низкий уровень Ассемблер Структурированные
Паскаль
Ада
С++
С
Модула-2 Неструктурированные
Фортран
Бейсик
COBOL http://www.c-cpp.ru/books/strukturirovannyy-yazyk
Структурированный язык
(Структурированный язык позволяет объявлять процедуры или функции внутри других процедур или функций. Поскольку С не позволяет создавать функции внутри функции, он не является полностью структурированным языком.
Отличительной особенностью структурированного языка является разделение кода и данных. Разделение является способностью языка отделять и прятать от оставшейся части программы всю информацию и инструкции, необходимые для выполнения некоторых задач. Одним из способов достижения разделения является использование подпрограмм, применяющих локальные переменные. Используя локальные переменные, программист может создавать подпрограммы таким образом, что события, возникающие в них, не будут влиять на оставшуюся часть программы. Данная возможность позволяет легко осуществлять разделение кода в С-программах. При разработке такого рода функции все, что нужно знать - это что делает функция, а не как она это делает. Следует помнить, что излишнее использование глобальных ,может приводить к ошибкам и ужасающим эффектам в программах
Структурированный язык предоставляет множество возможностей программирования. Он поддерживает несколько типов циклов, таких как while, do-while и for. Вопрос 3
Модификаторы доступа
С имеет 2 типа модификаторов, которые используются для контроля за способом доступа или модификации переменных. Эти модификаторы называются const и volatile.
Переменные типа const не могут изменяться операторами программы. Например:
const int a;
создаст целочисленную переменную, называемую а, которая не может быть модифицирована в программе. Она может использоваться в других типах выражений. Переменная с модификатором const получает своё значение или при инициализации, или каким-либо аппаратно-зависимым способом. Например, нижеприведённая строка присваивает count значение 100:
const int count = 100;
Помимо инициализации константа не может быть модифицирована программой.
Модификатор volatile используется для сообщения компилятору о возможности изменения значения способами, не определёнными в программе. Это важно, поскольку компилятор автоматически оптимизирует некоторые выражения, делая предположения, что содержимое переменных не изменяется в выражениях. Также некоторые виды оптимизации могут изменять порядок вычисления выражений во время процесса компиляции. Модификатор volatile предотвращает возникновение данных изменений.
Возможно использование этих модификаторов вместе. Например, если предполагается, что 0х30 является адресом порта, содержимое которого изменяется внешним устройством, то следующее объявление предохранит от побочных эффектов:
const volatile unsigned char *port = 0x30;
Вопрос 4 Объявление переменных
Все переменные должны объявляться перед использованием.
тип список_переменных;
Вопрос 5
Локальные переменные
Переменные, объявляемые внутри функций, называются локальными переменными. С локальными переменными могут работать только операторы, находящиеся в блоке, где данные переменные объявлены.
Вопрос 6
Формальные параметры
/* возвращает 1, если с является частью строки s; в противном случае - 0 */
int is_in(char *s, char c)
{
while(*s)
if(*s==c) return1;
else s++;
return 0;
}
пункт 8,2
Вопрос 7
Глобальные переменные
В противоположность локальным переменным глобальные переменные видны всей программе и могут использоваться любым участком кода. Они хранят свои значения на протяжении всей работы программы. Глобальные переменные создаются путём объявления вне функции. К ним можно получить доступ в любом выражении, независимо от того, в какой функции находится данное выражение.
В следующей программе переменная count объявлена вне функций. Общепринятым является объявление глобальных переменных в начале программы.
#include <stdio.h>
void func1(void), func2(void);
int count;
int main(void)
{
count = 100;
func1();
return 0;
}
void func1(void)
{
func2();
printf("count is %d", count); /* выведет 100 */
}
void func2(void)
{
int count;
for(count=1; count<10; count++)
putchar(' ');
}
Хотя ни main(), ни func1() не объявляют переменную count, но они оба могут её использовать, func2() объявляет локальную переменную count. Когда func2() обращается к count, она обращается только к локальной переменной, а не к глобальной.
Вопрос 8
Спецификаторы хранения
Имеется 4 спецификатора хранения, поддерживаемые в С. Это
extern
static
register
auto
Они говорят компилятору, как должны храниться переменные. Спецификаторы предшествуют объявлению переменной. В общем случае это выглядит так:
спецификатор_хранения тип имя_переменной;
extern
Поскольку С позволяет выполнять раздельную компиляцию модулей для большой программы, в целях ускорения компиляции и помощи управлению большими проектами, должны быть способы передачи информации о глобальных переменных файлам программы. Решение заключается в объявлении всех глобальных переменных в одном файле и использовании при объявлении в других файлах слова extern.
Файл 1Файл 2int x, y;
char ch;
int main(void)
{
...
}
void func1(void)
{
x = 23;
}extern int x, y;
extern char ch;
void func22(void)
{
x = y/10;
}
void func23(void)
{
y = 10;
} Имеется другой вариант использования extern. Когда используется глобальная переменная внутри функции, находящейся в том же файле, где происходит объявление глобальной переменной, то можно объявлять её как extern.
static
Статические переменные являются долговременными переменными, существующими на протяжении функции или файла. Они отличаются от глобальных переменных, поскольку не известны за пределами функции или файла, но могут хранить свои значения между вызовами.
Статические локальные переменные
Когда static применяется к локальной переменной, это приводит к тому, что компилятор создаёт долговременную область для хранения переменной почти таким же способом, как это делается для глобальной переменной. Ключевое различие между статической локальной и глобальной переменными заключается в том, что статическая локальная переменная остаётся известной только в том блоке, в котором она была объявлена.
Примером функции, требующей использования статических локальных переменных, является генератор последовательности чисел, создающий новое число, основываясь на старом.
int series(void)
{
static int series_num;
series_num = series_num+23;
return(series_num);
}
В данном примере переменная series_num существует между вызовами функций вместо того, чтобы каждый раз создаваться и уничтожаться как обычная локальная переменная. Это означает, что каждый вызов series( ) может создать новый член серии, основываясь на последнем члене без глобального объявления переменной.
Статические глобальные переменные
Когда оператор static применяется к глобальной переменной, он сообщает компилятору о необходимости создания глобальной переменной, которая будет известна только в файле, где она объявлена. Это означает, что, даже если переменная является глобальной, другие подпрограммы в других файлах не будут знать о ней. Таким образом, не возникает побочных эффектов.
auto
Спецификатор auto может быть задан только при определении объектов блока, например, в теле функции. Этим объектам память выделяется при входе в блок и освобождается при выходе из него. Вне блока объекты класса auto не существуют.
register
Спецификатор register просит, чтобы компилятор сохранил переменную способом, позволяющим осуществлять наибыстрейший доступ. Для целых чисел и символов это обычно подразумевает размещение не в памяти, а в регистрах процессора. Для других типов переменных компилятор может использовать другие способы для уменьшения времени доступа. Компилятор может просто проигнорировать данную просьбу.
Нельзя применять register к глобальным переменным. Также, поскольку регистровая переменная может быть сохранена в регистре процессора, нельзя получить адрес регистровой переменной. (Данное ограничение присутствует только в С, но не в С++.)
Вопрос 9
Оператор присваивания
Общий вид оператора присваивания следующий:
имя_переменной = выражение;
где выражение может быть как простой одиночной константой, так и сложной комбинацией переменных, операторов и констант. В левой части оператора присваивания должна стоять переменная, а не функция или константа.
Вопрос 10
Инициализация переменных
Во время объявления переменных можно сообщить им значение путём помещения знака равенства и константы после имени переменной. Этот процесс называется инициализацией и в общем случае имеет вид:
тип имя_переменной = константа;
Ниже приведено несколько примеров
char ch = 'a';
int first = 0;
float balance = 123.23;
Глобальные и статические глобальные переменные инициализируются только при запуске программы. Локальные переменные инициализируются каждый раз при входе в блок, где они были объявлены. Статические локальные переменные инициализируются только один раз, а не каждый раз при входе в блок. Глобальные и статические локальные переменные инициализируются 0, если не указано инициализационное значение. Неинициализированные нестатические локальные и регистровые переменные будут иметь неопределённое значение.
Вопрос 11
Тип данныхПример константыchar
int
long int
short int
float
double'a' '\n' '9'
1 123 21000 -234
35000L -34L
10 -12 90
123.23F 4.34e-3F
123.23 12312.333 -0.987654
Строковые константы
C поддерживает ещё один тип констант в дополнение к предопределённым типам данных. Это строковые константы. Все строковые константы заключаются в двойные кавычки, например: "this is a test". Не следует путать строковые константы с символами. Одиночный символ заключается в одинарные кавычки, как например: 'a'. Строки мы рассмотрим позже.
Символьные константы с обратным слэшем
Заключение символов в одинарные кавычки применимо для большинства печатаемых символов, но некоторые, как например звонок, невозможно ввести с клавиатуры. С этой целью С использует специальные константы с обратным слэшем, показанные в таблице.
Код Значение\bЗабой\fПеревод формата\nНовая строка\rВозврат каретки\tГоризонтальная табуляция\"Двойная кавычка\'Одинарная кавычка\0Нулевой символ\\Обратный слэш\vВертикальная табуляция\aЗвонок\NВосьмеричная константа (N - значение)\xNШестнадцатеричная константа (N - значение) Символы с обратным слэшем следует использовать таким же образом, как и обычные символы.
Вопрос 12
Операторы
С имеет множество встроенных операторов. Оператор - это символ, который сообщает компилятору о необходимости выполнения некоторых математических или логических действий. Имеется три больших класса операторов: арифметические, отношения, логические и битовые. Кроме этого, С имеет несколько специальных операторов для частных задач.
Арифметические операторы
Приоритет выполнения арифметических операций следующий:
высший +(унарный плюс) - (унарный минус) ++  * / %
низший+  (бинарные операторы)
***
Унарная операция - это операция над одним операндом (побитовое отрицание, унарный минус - меняет знак числа).
Бинарная операция - математическая операция, принимающая два аргумента и возвращающая один результат (то есть с арностью два).
Терна́рная усло́вная опера́ция - во многих языках программирования операция, возвращающая свой второй или третий операнд в зависимости от значениялогического выражения, заданного первым операндом.
В следующем примере вычисляется минимальное из чисел a и b:
min = (a < b) ? a : b; Унарные операции выполняются справа налево. В отличие от унарных, бинарные операции выполняются слева направо. ***
Операторы, обладающие одним приоритетом, вычисляются компилятором слева направо. Круглые скобки в С устанавливают наивысший приоритет операций.
Операторы отношения и логические операторы
Ключевой концепцией операторов отношения и логических операторов является понятие истины и лжи. В С истине соответствует любое значение кроме 0, лжи - 0. Выражение, использующее операторы отношения или логические операторы, возвращает 0 для лжи и 1 для истины.
Операция ЗначениеПример! Логическое НЕ!EOF= = Равноvalue==0!= Не равноvalue!=0< Меньшеi<count> Большеi>count> = Больше или равноi>=count< = Меньше или равноi<=count|| Логическое ИЛИ!a || b&& Логическое Иa>8 && c<5 (НЕ НАДО таблицу внизу, дай мне пару минут и диктуй дальше!!!)Таблица истинности для логических операторов образована с использованием на входах 1 и 0:
Pqp&&qp||q!p00001010111001011110 Как операторы отношения, так и логические операторы имеют более низкий приоритет по сравнению с арифметическими операторами. Это означает, что выражение 10 > 1 + 12 вычисляется как 10 > (1 + 12). Результатом будет ложь.
Ниже показаны приоритеты выполнения операторов отношения и логических операторов
высший!
> >= < <=
== !=
&&
низший||
Как и в арифметических выражениях, возможно использование круглых скобок для изменения естественного порядка вычисления операторов отношения или логических операторов. Например:
!1 && 0
даст в результате 0, поскольку ! вычисляется первым, а затем вычисляется &&. Если расставить скобки следующим образом:
!(1 && 0)
то получится истина.
Надо помнить, что все выражения с операторами отношения и логическими операторами дают в результате 0 или 1. Поэтому следующий фрагмент программы не только корректен, но и печатает число 1:
int x;
x =100;
printf("%d", x > 1);
Битовые операторы
С поддерживает все существующие битовые операторы. Битовые операции - это считывание, установка или сдвиг битов в байте или слове, которые соответствуют стандартным типам языка С char и int. Битовые операции не могут использоваться с float, double, long double, void и другими сложными типами.
ОператорДействие&И|ИЛИ^Исключающее ИЛИ~Дополнение>>Сдвиг вправо<<Сдвиг влево Битовые операторы И, ИЛИ, НЕ используют ту же таблицу истинности, что и их логические эквиваленты, за тем исключением, что они работают побитно. Исключающее ИЛИ имеет следующую таблицу истинности:
pqp^q
000
011
101
110
Битовое И чаще всего используется для обнуления битов. То есть любой бит установленный в 0, вызывает установку соответствующего бита в другом операнде в также в 0. Например, следующая функция читает символы из порта модема, используя функцию read_modem(), и сбрасывает бит чётности в 0.
char get_char_from_modem(void)
{
char ch;
ch = read_modem();
return (ch & 127);
}
Чётность отображается старшим битом, который устанавливается в 0 с помощью битового И, поскольку 7 младших бит равны 1.
Вопрос 13
Оператор ?
С имеет очень мощный оператор, который можно использовать вместо if-else. Оператор ? имеет следующий вид:
выражение1 ? выражение2 : выражение3
Оператор ? работает следующим образом: вычисляется выражение1, если оно истинно, то вычисляется выражение2 и всё выражение получает это значение, если ложно, то вычисляется выражение 3 и всё выражение получает это значение. Например:
x = 10;
y = x>9 ? 100 : 200;
В данном примере y получает значение 100. Если бы х было меньше либо равно 9, то у получила бы значение 200.
Вопрос 14
Операторы указания & и *
Указатель - это адрес переменной в памяти. Указатель на переменную - это переменная, специально созданная для хранения указателя на объект определённого типа. Зная адрес переменной, можно существенно упростить работу некоторых программ. Указатели имеют три главных назначения в С:
Предоставляют быстрое обращение к элементам массива.
Позволяют функциям модифицировать передаваемые параметры.
Поддерживают динамические структуры данных.
Первый оператор - &. Это унарный оператор, возвращающий адрес операнда в памяти. Например:
m = &count;
помещает в m адрес переменной count. Второй оператор - это *, дополняющая &. Это унарный оператор, возвращающий значение переменной по указанному адресу. Например:
q = *m;
Переменные, содержащие адреса или указатели, должны объявляться путём помещения * перед именем переменной. Например, для объявления указателя ch на символ, следует написать
char *ch;
Здесь ch  это не символ, а указатель на символ. Тип данных, на который указывает указатель, называется базовым типом указателя.
Ниже операторы * и & используются для занесения числа 10 в переменную target:
#include <stdio.h>
int main(void)
{
int target, source;
int *m;
source = 10;
m = &source;
target = *m;
printf("%d", target);
return 0
}
Вопрос 15
Оператор sizeof
sizeof - это унарный оператор, возвращающий длину в байтах переменной или типа, помещённого в скобки. Например:
float f;
printf("%f ", sizeof f);
printf("%d", sizeof(int));
выдаёт 4 4. (Предполагается использование 32-битных целых).
ПУНКТ 3,9 в учебнике
Вопрос 16
Операторы выбора С поддерживает два типа оператора выбора: if и switch. Кроме того, оператор ? является иногда альтернативой if.
Условный оператор if
Синтаксис полной формы условного оператора:
if ( логическое выражение ) оператор1;
else оператор2;
Если логическое выражение истинно, т.е. не равно нулю, то выполняется оператор1, иначе выполняется оператор2.
Синтаксис сокращенной формы условного оператора:
if ( логическое выражение ) оператор;
Оператор выполняется только в том случае, если логическое выражение не равно нулю, т.е. истинно.
Под логическим выражением понимается совокупность операций отношений и логических операций.
Часто, каждая из альтернативных возможностей оператора if требует выполнения более одного оператора. В этом случае необходимо заключить группу операторов в фигурные скобки { }.
Список операторов, заключенный в фигурные скобки, называется блоком. Лесенка if-else-if
Типичной программной конструкцией является лесенка if-else-if. Она выглядит следующим образом:
if(выражение)
оператор;
else if(выражение)
оператор;
else if(выражение)
оператор;
...
else
оператор;
Условия вычисляются сверху вниз. Когда обнаруживается истинное условие, то выполняется оператор, связанный с этим условием, а остальная часть конструкции игнорируется. Если не найдено ни одного истинного условия, выполняется оператор, соответствующий последнему else.
switch
Синтаксис оператора switch:
switch (выражение) {
case константное выражение: оператор или группа операторов;
break; case константное выражение: оператор или группа операторов;
break;
case константное выражение: оператор или группа операторов;
break;
. . . . . . . .
defaulf: оператор или группа операторов;
};
Результат вычисленного выражения сравнивается с каждым из константных выражений. Если находится совпадение, то управление передается оператору, связанному с данным case. Исполнение продолжается до конца тела оператора switch или пока не встретится оператор break, который передает управление из тела switch оператору, следующему за switch. Оператор или группа операторов, стоящий после default, выполняется, если выражение не соответствует ни одному из константных выражений в case. Константные выражения должны быть целого или символьного типа. Если нескольким константным выражениям соответствует один и тот же оператор, то возможна следующая запись:
switch (выражение)
{
case константное выражение:
case константное выражение:
case константное выражение: оператор или группа операторов;
break;
};
ПУНКТ 4,5 в учебнике
Вопрос 17
Оператор "запятая"
Оператор "запятая" используется для связки нескольких выражений. Левая сторона оператора "запятая" вычисляется как не выдающая значения. Значение выражения, находящегося с правой стороны, станет значением разделенного запятыми выражения. Например:
x=(y=3, y+1);
Сначала присваивается 3 переменной y, а затем 4 переменной x. Скобки нужны, поскольку оператор "запятая" имеет более низкий приоритет, чем оператор присваивания.
Вопрос 18
Выражения
Выражения в С - это любая допустимая комбинация операторов, констант и переменных.
Преобразования типов в выражениях. Компилятор преобразует все выражения к типу большего операнда.
1. Все переменные типа char short int преобразуются к типу int. Все переменные типа float - к типу double.
2. Если один из пары операндов имеет тип long double, другой операнд также преобразуется к типу long double. Иначе, если один из операндов имеет тип double, другой операнд также преобразуется к double. Иначе, если один из операндов имеет тип long, другой операнд также преобразуется к типу long. Иначе, если один из операторов имеет тип unsigned, другой операнд также преобразуется к типу unsigned.
Вопрос 19
Преобразования типов в выражениях. Компилятор преобразует все выражения к типу большего операнда.
1. Все переменные типа char short int преобразуются к типу int. Все переменные типа float - к типу double.
2. Если один из пары операндов имеет тип long double, другой операнд также преобразуется к типу long double. Иначе, если один из операндов имеет тип double, другой операнд также преобразуется к double. Иначе, если один из операндов имеет тип long, другой операнд также преобразуется к типу long. Иначе, если один из операторов имеет тип unsigned, другой операнд также преобразуется к типу unsigned.
В УЧЕБНИКЕ ПУНКТ 3,4,1
Вопрос 20
Принудительные преобразования типов
Имеется возможность заставить выражение принять определённый тип с помощью оператора принудительных преобразований. Эта операция имеет следующий вид:
(тип) выражение;
где тип - это один из стандартных типов данных С или определяемый пользователем тип. Например, если необходимо, чтобы выражение x/2 имело тип float (частное со знаками после запятой), следует написать:
(float) x / 2
В УЧЕБНИКЕ 3,4,2
Вопрос 21
Циклы
В С и других языках программирования циклы позволяют выполнять набор инструкций до тех пор, пока не выполниться некоторое условие. Определены три разных оператора цикла:
Итерационный цикл for
Стандартный вид цикла for следующий:
for (инициализация_цикла; выражение-условие; список_выражений) оператор;
Оператор for имеет три главные части:
инициализация_цикла - это место, где обычно находится оператор присваивания, используемый для установки начального значения переменной цикла.
выражение-условие - это выражение, определяющее условие работы цикла.
список_выражений - это место где определяется характер изменения переменной цикла на каждой итерации.
Эти три важные части должны разделяться точкой с запятой. Цикл for работает до тех пор, пока условие истинно. Когда условие становится ложным, выполнение программы продолжается с оператора, следующего за циклом for.
Пример
#include <stdio.h>
int main(void)
{
int x;
for(x=1; x<=100; x++) printf("%d ",x);
return 0;
}
В данном примере x является переменной цикла, которая изменяется и проверяется на каждой итерации цикла. Ниже приведён пример цикла for, повторяющего несколько операторов:
for(x=100; x!=65; x-=5) {
z = sqrt(x);
printf("The square root of %d, %f", x, z);
}
Как sqrt(), так и printf(), вызываются и выполняются пока x не равно 65.
В цикле for проверка условия выполняется в самом начале. Это означает, что код в цикле может вообще не выполняться, если условие изначально ложно.
Вариации цикла for
Одна из наиболее типичных вариаций достигается с использованием оператора "запятая", тем самым позволяя иметь две или более переменных цикла.
Например, функция reverse(). Она предназначена для копирования содержимого первого строкового аргумента во второй строковый аргумент в обратном порядке.
void reverse(char *s, char *r)
{
int i, j;
for(i=strlen(s)-1, j=0; i>=0; j++, i--)
r[i]=s[j];
r[j] = '\0';
}
Бесконечный цикл
Одним из наиболее интересных аспектов использования цикла for является создание бесконечного цикла. Поскольку не требуется ни одна из частей, можно создать бесконечный цикл путём удаления части проверки условия. Например:
for( ; ; ) printf("this loop will run forever.\n");
while
while (выражение-условие)
тело цикла
Тело цикла - это или пустой, или простой, или составной оператор. Выражением-условием может быть любое выражение, имеющее в качестве истины ненулевое значение. Цикл выполняется, пока условие истинно. Когда условие становится ложным, выполняется строка, следующая за циклом.
Следующий пример показывает процедуру, обрабатывающую ввод с клавиатуры, работающую пока не будет введён символ 'A':
void wait_for_char(void)
{
char ch;
ch = '\0';
while(ch!='A') ch = getchar();
}
Как и цикл for, цикл while сначала выполняет проверку, то есть тело цикла может вообще не выполняться. Когда необходимо несколько различных условий для окончания цикла while, как правило, используют одну переменную, являющуюся результатом действия этих условий, причём значение этой переменной может устанавливаться в различных частях цикла. Например:
void func1(void)
{
int working = 1;
while(working) {
working = procces1();
if (working)
working = process2();
if (working)
working = process3();
}
}
do/while
Цикл do/while проверяет условие в конце. То есть, цикл do/while всегда выполняется, хотя бы один раз. Стандартный вид цикла do/while следующий:
do{
последовательность операторов;
}while (выражение-условие);
Хотя в фигурных скобках нет необходимости при наличии одного оператора, они обычно используются для улучшения читабельности и устранения недоразумений (у читателя, а не у компилятора) по поводу цикла.
Возможно, наиболее типичным использованием цикла do/while является процедура выбора пунктов меню. Когда набран корректный ответ, она возвращает значение функции. Неправильный ответ приводит к повторному вводу. Пример:
void menu(void)
{
char ch;
printf("1. Check Spelling\n");
printf("2. Correct Spelling Errors\n");
printf("3. Display Spelling Errors\n");
printf(" Enter your choice: ");
do{
ch = getchar();
switch(ch) {
case '1':
check_spelling();
break;
case '2':
correct_errors();
break;
case '3':
display_errors();
break;
}
} while(ch!='1'&&ch!='2'&&ch!='3');
}
Во всех циклах тело цикла не может быть описанием или определением. Это либо отдельный оператор, который всегда завершается точкой с запятой, либо составной оператор, либо блок (заключается в фигурные скобки). Вопрос 22
Оператор break
Оператор break имеет два назначения. Первое - это окончание работы оператора switch. Второе - это принудительное окончание цикла, минуя стандартную проверку условия.
Когда оператор break встречается в теле цикла, цикл завершается и выполнение программы переходит на строку, следующую за циклом.
Пример
#include <stdio.h>
int main(void)
{
int x;
for(x=1; x<100; x++) {
printf("%d ",x);
if(x==10) break;
}
return 0;
}
Данная программа выводит числа от 0 до 10 включительно и заканчивает работу, поскольку break вызывает немедленный выход из цикла, минуя условие x<100.
Вопрос 23
Оператор continue
Работа оператора continue чем-то похоже на работу оператора break. Но вместо форсированного окончания continue переходит к следующей итерации цикла, пропуская оставшийся код тела цикла. Например, следующая процедура выводит только положительные числа:
do{
scanf("%d", &x);
if(x<0) continue;
printf("%d ", x);
} while(x!=100);
Вопрос 24
Метки и goto
Хотя goto уже давно не рекомендуют использовать, он по-прежнему используется в программах. goto требует наличия меток для работы. Метка - это корректный идентификатор С, завершаемый двоеточием. Метка должна находиться в той же функции, что и goto.
УЧЕБНИК 4,8
Вопрос 25
Массивы
Массив - это совокупность переменных одного типа, к которым обращаются с помощью общего имени. Доступ к отдельному элементу массива может осуществляться с помощью индекса. В С все массивы состоят из соприкасающихся участков памяти. Наименьший адрес соответствует первому элементу. Наибольший адрес соответствует последнему элементу. Массивы могут иметь одну или несколько размерностей.
Массивы тесно связаны с указателями.
Одномерный массив
Стандартный вид объявления одномерного массива следующий:
тип имя_переменной[размер]; В С массивы должны определяться однозначно, чтобы компилятор мог выделить под них место в памяти. Здесь тип определяет базовый тип массива и является типом каждого элемента массива. Параметр размер определяет, сколько элементов содержит массив. В одномерном массиве полный размер массива в байтах вычисляется следующим образом:
общее число байт = sizeof (базовый тип)*число элементов
У всех массивов первый элемент имеет индекс 0. Поэтому, если написать
char p[10];
то будет объявлен массив символов из 10 элементов, причём эти элементы адресуются индексом от 0 до 9. Следующая программа загружает целочисленный массив числами от 0 до 9 и выводит его:
#include <stdio.h>
int main(void)
{
int x[10];
int t;
for(t=0; t<10; ++t) x[t] = t;
for(t=0; t<10; ++t) printf("%d ", x[t]);
return 0;
}
В С отсутствует проверка границ массивов. Можно выйти за один конец массива и записать значение в какую-либо переменную, не относящуюся к массиву, или даже в код программы. Работа по предоставлению проверки границ возлагается на программиста. Вопрос 26
Создание указателя на массив
Можно создать указатель на первый элемент массива, указав имя массива без индекса. Пусть имеется int sample[10];
Можно создать указатель на первый элемент, используя имя sample. Следовательно, следующий фрагмент присваивает переменной p адрес первого элемента sample:
int *p;
int sample[10];
p = sample;
Можно также получить адрес первого элемента массива с помощью оператора &. Например, sample и &sample[0] приводят к одинаковому результату. Тем не менее в профессиональных программах нет почти нигде &sample[0].
Вопрос 27
Строки
Наиболее типичным представителем одномерного массива являются строки. Хотя С не определяет строкового типа, имеется поддержка строковых операций с помощью функций, имеющихся в любом языке. В С строка определяется как символьный массив произвольной длины, оканчивающийся нулевым символом. Нулевой символ определяется как '\0'. Поэтому необходимо объявлять символьные массивы на один символ больше, чем требуется для хранения самой длинной строки. Например, если необходимо объявить массив s, содержащий десятисимвольную строку, следует написать:
char s[11];
В результате этого выделяется место в конце строки для нулевого символа.
Не надо добавлять нулевой символ в конец строковой константы, поскольку компилятор С выполняет это автоматически.
С поддерживает множество функций для работы со строками. Наиболее типичными являются strcpy(), strcat(), strlen(), strcmp() со следующими прототипами:
char *strcpy(char *s1, const char *s2);
char *strcat(char *s1, const char *s2);
size_t strlen(const char *s1);
int strcmp(const char *s1, const char *s2);
Все функции используют заголовочный файл string.h. Функция strcpy() копирует строку, на которую указывает s2, в строку на которую указывает s1. Функция возвращает s1. Функция strcat() выполняет конкатенацию строки, на которую указывает s1, и строки на которую указывает s2. Она также возвращает s1. Функция strlen() возвращает длину строки, на которую указывает s1. Тип данных size_t определён стандартом для адресов и в настоящее время для большинства машин определяется как беззнаковое длинное целое. Функция strcmp() сравнивает строки s1 и s2. Она возвращает 0, если две строки эквивалентны, значение меньше 0, если строка s1 при лексикографическом порядке идет раньше s2 и значение больше 0, если строка s1 при лексикографическом порядке идет после s2.
Следующая программа демонстрирует использование данных строковых функций:
#include <string.h>
#include <stdio.h>
int main()
{
char s1[80], s2[80];
gets(s1); gets(s2);
printf("lengths: %d %d\n", strlen(s1), strlen(s2));
if(!strcmp(s1, s2)) printf("The strings are equal\n");
strcat(s1, s2);
printf("%s\n", s1);
return 0;
}
Если при запуске программы ввести строки "hello" и "hello", в результате получится:
lengths: 5 5
The strings are equal
hellohello
Для многих компиляторов существуют безопасные версии функций strcpy() и strcat() - это strcpy_s() и strcat_s(). Чтобы ими полноценно пользоваться необходимо подключить <string.h> и <errno.h>.
errno_t strcpy_s(char *strDestination, size_t sizeInBytes, const char *strSource );
errno_t strcat_s(char *strDestination, size_t sizeInBytes, const char *strSource);
Вторым параметром в этих функциях идет максимальная длина целевой строки. Функции возвращают значение ноль, если параметры корректны и не ноль, если в параметрах содержится ошибка.
Вопрос 28
Двумерные массивы
С позволяет создавать многомерные массивы. Простейшим видом многомерного массива является двумерный массив. Двумерный массив - это массив одномерных массивов. Двумерный массив объявляется следующим образом:
тип имя_массива[размер второго измерения][размер первого измерения];
В следующем примере в двумерный массив заносятся числа от 1 до 12, после этого массив выводится на экран.
#include <stdio.h>
int main()
{
int t, i, num[3][4];
/* загрузка чисел */
for(t=0; t<3; ++t)
for(i=0; i<4; ++i)
num[t][i] = (t*4)+i+1;
/* вывод чисел */
for(t=0; t<3; ++t) {
for(i=0; i<4; ++i)
printf("%d ", num[t][i]);
printf("\n");
}
return 0;
}
В данном примере num[0][0] имеет значение 1, num[0][1] имеет значение 2, num[0][2] имеет значение 3 и так далее. num[2][3] имеет значение 12.
Вопрос 29
Массивы строк
В программировании типично использование массивов строк. Например, процессор ввода в базу данных может проверять команды пользователя в строковом массиве. Для создания массива строк используется двумерный массив символов. Левый индекс определяет число строк, а правый индекс - максимальное число символов в каждой строке. Данный фрагмент кода объявляет массив из 30 строк, каждая может содержать до 79 символов включительно:
char str_array [30] [80];
Доступ к отдельным строкам очень прост - необходимо написать имя массива только с левым индексом. Например:
gets(str_array[2]);
Данная строка эквивалентна
gets(&str_array[2][0]);
но предыдущий вариант более типичен при написании профессиональных программ.
Рассмотрим программу, использующую массив как основу простейшего текстового редактора.
#include <stdio.h>
#define MAX 100
#define LEN 255
char text[MAX][LEN];
/* простейший текстовый редактор*/
int main()
{
register int t, i, j;
for(t=0; t<MAX; t++) {
printf("%d: ", t);
gets(text[t]);
if(!*text[t]) break;/*выход по пустой строке*/
}
/*посимвольный вывод текста*/
for(i=0; i<t; i++) {
for(j=0; text[i][j]; j++)
printf("%c",text[i][j]);
printf("%c", '\n');
}
getchar();
return 0;
}
Данная программа осуществляет ввод текста, пока не встретится пустая строка. Затем она отображает каждую строку. В целях иллюстрации она выводит текст посимвольно, с использованием правого индекса.
Вопрос 30
Многомерные массивы
С позволяет создавать массивы с размерностями больше двух. Многомерный массив объявляется следующим образом:
тип имя[размерN]...[размер2][ размер1];
Вопрос 31
Индексация с помощью указателей
Указатели и массивы очень тесно связаны между собой. Имя массива без индекса - это указатель на первый элемент массива. Пусть имеется массив
char p[10];
тогда следующие значения идентичны:
p
&p[0]
Выражение p == &p[0]
выдаёт истину, поскольку адрес первого элемента и адрес массива совпадают.
Справедливо и обратное. Любой указатель может быть проиндексирован, как будто это массив. Например,
int *p, i[10];
p = i;
p[5] = 1000;
*(p+5) = 1000;
Оба оператора присваивания помещают значение 1000 в шестой элемент i. Первый оператор использует индексацию с р, а второй - арифметику указателей. Результат одинаков.
Данные способы индексации совершенно справедливы для массивов размерности 2 и более. Предположим, что а - это целочисленный массив 10 на 10. Тогда нижеприведенные операторы эквивалентны:
(int *)a
&a[0][0]
К элементу 0, 4 массива а можно обратиться или с помощью индексации массива - a[0][4], или с помощью указателя - *((int *)a+4). Аналогично к элементу 1, 2 можно обратиться или с помощью индексации массива - a[1][2], или с помощью указателя - *((int *)a+12).
Указатели иногда используются для обращения к массивам, поскольку арифметика указателей чаще всего выполняется быстрее, чем индексация массивов. Преимущество наиболее заметно, когда осуществляется последовательный доступ к массиву. В данной ситуации указатель может увеличиваться или уменьшаться с помощью эффективных операторов увеличения или уменьшения. Если же доступ к массиву происходит случайным образом, то лучше использовать индексацию массива, а не указатели.
Двумерные массивы подобны массивам указателей на строки. Поэтому использование отдельных указателей является одним из лёгких способов доступа к элементам двумерного массива.
Следующая функция выводит содержимое указанной строки глобального целочисленного массива num:
int num[10][10];
...
void pr_row(int j)
{
int *p, t;
p = num[j];
for(t=0; t<10; ++t) printf("%d ", *(p+t));
}
Данный код может быть обобщён, если передавать в качестве аргумента номер строки, длину строки и указатель на первый элемент массива:
void pr_row(int j, int row_dimension, int *p)
{
int t;
p = p + (j * row_dimension);
for(t=0; t< row_dimension; ++t)
printf("%d ", *(p+t));
}
Вопрос 32
Размещение массивов
Во многих ситуациях невозможно знать размер массива. В С++ можно динамически выделять и освобождать память при помощи операторов new и delete. Если необходимо выделить память под целочисленный массив длины 100, то можно воспользоваться следующим кодом:
int *p;
try{ p = new int[100]; }
catch(...){ puts("Cannot allocate"); exit(1); }
В дальнейшем доступ осуществляется, как при помощи указателя. Например, доступ к 6 элементу:
p[5]
Для освобождения памяти необходимо пользоваться оператором delete.
delete [] p;
Оператор delete должен освобождать память, только ранее выделенную оператором new, иначе это может привести к краху программы.
Вопрос 33
Функции
Функции - это базовые блоки С, в которых выполняются все операции. Стандартный вид функций следующий:
спецификатор_типа имя_функции(список параметров)
{
тело функции
}
Спецификатор_типа определяет тип возвращаемого функцией значения с помощью оператора return. Это может быть любой допустимый тип. Список параметров - это разделённый запятыми список переменных, получающих значения аргументов при вызове функции. Функция может быть без параметров и в таком случае список параметров состоит из ключевого слова void.
Вопрос 34
Оператор return
Оператор return имеет два назначения. Во-первых, немедленный выход из функции. То есть он осуществляет выход в вызывающий функцию код. Во-вторых, может использоваться для возврата значения. Рассмотрим оба назначения.
Выход из функции
Имеется два способа окончания работы функции и передачи управления вызывающему коду. Первый способ предполагает достижение последнего оператора функции, после чего работа функции заканчивается при встрече }. Например, следующая функция выводит на экран строку в обратном порядке:
void pr_reverse(char *s)
{
register int t;
for(t=strlen(s)-1; t>-1; t--) printf("%c", s[t]);
}
После отображения строки функция ничего не делает, поэтому управление передаётся вызвавшему её коду.
Большинство функций использует оператор return для окончания исполнения с целью возвращения значения или упрощения кода функции и увеличения его эффективности путём создания нескольких точек выхода. Например, функция, показанная ниже, возвращает или индекс первого появления подстроки, указываемой в s2, в строке, указываемой в s1, или -1, если не обнаружено совпадений:
int find_substr(char *s1, char *s2)
{
register int t;
char *p, *p2;
for(t=0; s1[t]; t++) {
p = &s1[t];
p2 = s2;
while(*p2 && *p2==*p) {
p++;
p2++;
}
if(!*p2) return t;
}
return -1;
}
Надо обратить внимание, как два оператора return упрощают функцию.
Возвращаемые значения
Все функции, кроме функций типа void, возвращают значения, Данное значение определяется в операторе return. Если функция не определена как void, она может использоваться в качестве операнда в любом корректном выражении. Следовательно, каждое из следующих выражений корректно:
x = power(y);
if(max(x, y) > 100) printf("greater");
for(ch=getchar(); isdigit(ch);) ...;
Функция не может стоять с левой стороны оператора присваивания. Оператор типа
swap(x, y) = 100;
неправилен. Компилятор выдаст ошибку.
Если функция определена как void, она не может использоваться в выражениях. Предположим, что f() объявлена как void. Следующие операторы не будут компилироваться:
int t;
t = f();
f() + f();
Если не указано, чему присваивается возвращаемое значение, то оно просто отбрасывается. Рассмотрим следующую программу, использующую mul():
#include <stdio.h>
int mul(int a, int b);
int main(void)
{
int x, y, z;
x = 10; y = 20;
z = mul(x, y);/* 1 */
printf("%d ", mul(x, y)); /* 2 */
mul(x, y);/* 3 */
return 0;
}
int mul(int a, int b)
{
return a*b;
}
Строка 1 присваивает возвращаемое функцией mul() значение переменной z. В строке 2 возвращаемое значение используется функцией printf(). В строке 3 возвращаемое значение теряется.
Значения, возвращаемые функцией main()
Когда используется оператор return в main(), программа возвращает код завершения вызывавшему процессу. Возвращаемое значение должно быть целого типа. Большинство ОС трактуют 0 как нормальное завершение программы. Остальные значения воспринимаются как ошибки.
Вопрос 35
Правила видимости для функций
Каждая функция в С - это блок кода. Код функции является собственностью функции, и к нему нельзя получить доступ с помощью оператора или другой функции, помимо вызова данной функции.
Переменные, определённые в функциях, называются локальными переменными. Локальные переменные создаются при входе в функцию и уничтожаются при выходе из неё. Поэтому локальные переменные не могут содержать значения между вызовами функций. Единственным исключением из этого правила являются переменные, объявленные со спецификатором static.
Все функции в С находятся на одном уровне видимости. То есть невозможно определить функцию в функции.
Вопрос 36
Аргументы функции
Если функция использует аргументы, она должна объявлять переменные, получающие значения аргументов. Данные переменные называются формальными параметрами функции. Они ведут себя так же, как и локальные переменные. Объявление параметров происходит после имени функции, но перед открывающей скобкой функции.
Передача по значению и передача по указателю
Обычно функции могут передавать аргументы двумя способами. Первый называется передачей по значению. Данный метод копирует содержимое аргумента в формальный параметр функции. Изменения, сделанные в параметре, не влияют на значение переменной, используемой при вызове.
Передача по указателю является вторым способом передачи аргументов. В данном методе копируется адрес аргумента. В функции адрес используется для доступа к настоящему аргументу, используемому при вызове. То есть, изменения, сделанные в параметре, влияют на содержимое переменной, используемой при вызове.
Рассмотрим следующую функцию:
#include <stdio.h>
int sqr(int x);
int main(void)
{
int t=10;
printf("%d %d", sqr(t), t);
return 0;
}
int sqr(int x)
{
x = x*x;
return x;
}
На экране появится 100 10.
Создание передачи по указателю
Хотя, как правило, передача параметров происходит по значению, можно передать параметр по ссылке. Поскольку в данном случае происходит передача адреса аргумента, возможно изменение значения аргумента, находящегося вне функции.
Указатели передаются в функции, как и обычные значения. Конечно, необходимо объявлять параметр типа указатель. Пример - функция, меняющая значение двух целочисленных аргументов.
void swap(int *x, int *y)
{int temp;
temp = *x;
*x = *y;
*y = temp;
}
Оператор * используется для доступа к переменным, на которые указывают операнды. Важно помнить, что любая функция, использующая указатели на параметры должна вызываться с адресами аргументов. Следующая программа демонстрирует правильный способ вызова swap():
#include <stdio.h>
void swap(int *x, int *y);
int main(void)
{
int x, y;
x = 10;
y = 20;
swap(&x, &y);
printf("%d %d", x, y);
return 0;
}
В данном примере переменной х присваивается значение 10, а переменной у - 20. Затем вызывается swap() с адресами х и у. Унарный оператор & используется для получения адресов переменных. Поэтому в функцию swap() передаются адреса х и у, а не их значения.
Вопрос 37
Передача массивов в функции
Когда массив используется в качестве аргумента функции, передаётся только адрес массива, а не копия всего массива. При вызове функции с именем массива в функцию передаётся указатель на первый элемент массива. Параметр должен иметь тип, совместимый с указателем. Имеется три способа объявления параметра, предназначенного для получения указателя на массив. Во-первых, он может быть объявлен как массив. Пример:
#include <stdio.h>
void display(int num[10]);
int main(void)
{int t[10], i;
for(i = 0; i < 10; ++i) t[i] = i;
display(t);
return 0;
}
void display(int num[10])
{int i;
for(i=0; i<10; ++i) printf("%d ", num[i]);
}
Хотя параметр num объявляется как целочисленный массив из 10 элементов, С автоматически преобразует его указателю на целочисленный тип данных.
Следующий способ состоит в объявлении параметра для указания на безразмерный массив, как показано ниже:
void display(int num[ ])
{int i;
for(i=0; i<10; ++i) printf("%d ", num[i]);
}
где num объявлен как целочисленный массив неизвестного размера. Поскольку С не предоставляет проверку границ массива, настоящий размер массива не имеет никакого отношения к параметру.
Последний способ, которым может быть объявлен num, - это наиболее типичный способ, применяемый при написании профессиональных программ,  через указатель. Пример:
void display(int *num)
{int i;
for(i=0; i<10; ++i) printf("%d ", num[i]);
}
Важно понять, что при использовании массива в качестве аргумента функции происходит передача в его функцию его адреса. Это означает, что код внутри функции может изменять значение массива, используемого при вызове.
При передаче многомерных массивов функции следует определить все размерности, кроме самой левой, это нужно компилятору для корректной индексации массива. Например, если массив определён как int m[4][5][6][7];
то функция func1(), получающая m, может выглядеть следующим образом:
int func1(int d[][5][6][7])
{
...
}
ВОпрос 38
Аргументы функции main()
По стандарту поддерживаются два агрумента main() - это argc и argv. Они позволяют передавать аргументы командной строки в программу. Аргументы командной строки - это информация, следующая за именем программы в командной строке операционной системы.
Параметр argc содержит число аргументов командной строки и является целым числом. Он всегда больше либо равен 1, поскольку имя программы квалифицируется как первый аргумент. Параметр argv - это указатель на массив символьных указателей. Каждый элемент данного массива указывает на аргумент командной строки. Все аргументы командной строки - это строки, все числа конвертируются программой во внутренний формат. Следующая программа выводит "Hello", а затем имя пользователя, если его набрать прямо за именем программы:
#include <stdio.h>
int main(int argc, char *argv[])
{if(argc!=2) {
printf("You forgot to type your name\n");
return 1;
}
printf("Hello%s", argv[1]);
return 0;
}
Аргументы командной строки должны отделяться пробелами или табуляциями. Запятые, точки с запятыми и им подобные символы не рассматриваются как разделители. Если необходимо передать строку, содержащую пробелы или табуляции в виде одного аргумента, то её следует заключить в двойные кавычки. Например:
"this is a test"
Пример по использованию аргументов командной строки. Программа отсчитывает в обратном порядке от значения, указанного в командной строке и подаёт сигнал. Если в качестве второго аргумента присутствует строка "display", то на экране будет отображаться счётчик.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{int disp, count;
if(argv<2) { printf("You must enter the length of count\n");
printf("on command line. Try again.\n");
return 1;
}
if(argc == 3 && !strcmp(argv[2],"display")) disp = 1;
else disp = 0;
for(count=atoi(argv[1]); count; --count)
if (disp) printf("%d ", count);
printf("%c", '\a');
return 0;
}
Использование прототипов функций
Прототипы функций служат двум целям: во-первых, они определяют тип возвращаемого функцией значения, во-вторых, они определяют тип и число аргументов, используемых функциями. Прототип имеет следующий вид:
тип имя_функции(список параметров);
Прототип помещается в начало программы.
Формально при создании прототипа функции не требуется включать имена параметров. Например, следующие два варианта корректны и равнозначны:
char func(char *, int);
char func(char *str, int count);
Тем не менее, если включить имена параметров, компилятор использует имена для выдачи сообщений о несоответствии типов.
В УЧЕБНИКЕ 8,3
Вопрос 39
Возврат указателей
Функции могут возвращать указатели также как и любые другие типы. Важно помнить, что указатели - это не беззнаковые целые. Это адрес памяти некоторого типа данных, у них другая арифметика.
Пример - функция, возвращающая указатель на символ в месте, где найдено первое вхождение символа в строке:
char *match(char c, char *s)
{register int count;
count = 0;
while(c!=s[count] && s[count]) count++;
if(s[count]) return (&s[count]);
else return NULL;
}
Функция match() пытается вернуть указатель на позицию в строке, где первый раз найден символ. Если не найдено соответствие, возвращается указатель, содержащий NULL.
Вопрос 40
Рекурсия
В С функции могут вызывать сами себя. Простой пример.
int factr(int n)
{int answer;
if(n = = 1) return 1;
answer = factr(n-1)*n;
return (answer);
}
Задача о ханойских башнях.
#include "stdafx.h"
#include <stdio.h>
void solveTowers(int count, char sourse,char destination, char spare)
{
if (count == 1)
{
printf("Move top ring from %c to %c\n", sourse, destination);
}
else {
solveTowers(count-1, sourse, spare,destination);
solveTowers(1, sourse, destination, spare);
solveTowers(count-1, spare, destination,sourse);
}
}
int main()
{
solveTowers(4,'A','B','C');
getchar();
return 0;
}
Вопрос 41
Указатели на функции
Хотя функция - это не переменная, она имеет физическое положение в памяти, которое может быть присвоено указателю. Адрес, присвоенный указателю, является входной точкой в функцию. Указатель может использоваться вместо имени функции. Он также позволяет передавать функции как обычные аргументы в другие функции.
Адрес функции получается при использовании имени функции без скобок и аргументов. Пример:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#define PI 3.1415
double del2(double a, double b, double eps, double (*f)(double));
int main(void)
{
double (*p)(double);
p = sin;
double a = -PI/2, b = PI/2, eps = 0.0001;
printf("Root = %lf\n", del2(a,b,eps,p));
printf("Root = %lf\n", del2(a,b,eps,sin));
return 0;
}
double del2(double a, double b, double eps, double (*f)(double))
{
if(b<a) {
puts("Left bigger than right");
exit(0);
}
if(eps <= 0) {
puts("Eps <= 0");
exit(0);
}
if(f(a) == 0) return a;
if(f(b) == 0) return b;
while(b-a >= eps) {
double x=(a+b)/2;
if(f(a)*f(x) < 0.) a=x;
else if(f(x) == 0.) return x;
else b=x;
}
return (a+b)/2;
}
Когда вызывается del2(), ей передаются числа с плавающей точкой двойной точности и один указатель на функцию. Скобки вокруг *f необходимы для правильной интерпретации компилятором данного выражения.
При объявлении указателя на функцию можно использовать прототип, оставив имена параметров пустыми.
Рассмотрим работу функции sin() в функции del2(). Оператор
if(f(a) == 0) return a;
осуществляет вызов функции, в данном случае sin(), с помощью f, который указывает на данную функцию. Вызов происходит с аргументом a. Бывают моменты, когда выгодно передавать функции в процедуры или хранить массивы функций. Пример:
#include <stdlib.h>
#include <stdio.h>
#include <conio.h>
#include <string.h>
void enter(void), del(void); void review(void), quit(void);
int menu(void);
void (*options[])(void) = {
enter, del,
review,
quit
};
int main(void)
{
int i;
i = menu();
(*options[i])();
getchar();
return 0;
}
int menu(void)
{
char ch;
do {
printf("1. Enter\n");
printf("2. Delete\n");
printf("3. Review\n");
printf("4. Quit\n");
printf("Select a number:");
ch = getche();
printf("\n");
}while(!strchr("1234", ch));
return ch-49;
}
void enter(void)
{printf("In enter.");}
void del(void)
{printf("In del.");}
void review(void)
{printf("In review.");}
void quit(void)
{
printf("In quit.");
exit(0);
}
Вопрос 42
Структуры
Структура - это совокупность переменных, объединённых одним именем, предоставляющая общепринятый способ совместного хранения информации. Объявление структуры приводит к образованию шаблона, используемого для создания объектов структуры. Переменные, образующие структуру, называются членами структуры. (Члены структуры также часто называются элементами или полями.)
Фрагмент кода объявляет структуру, определяющую имя и адрес. Ключевое слово struct сообщает компилятору об объявлении структуры.
struct addr {
char name[30];
char street[40];
char city[20];
char region[3];
unsigned long int zip;
};
Объявление завершается точкой с запятой, поскольку объявление структуры - это оператор. Имя структуры addr идентифицирует структуру данных и является спецификатором типа. Имя структуры часто используют как ярлык.
Пока не создано никакой переменной. Определена только форма данных. Для объявления переменной, соответствующей данной структуре, следует написать:
addr addr_info; //C++
struct addr addr_info;//C
Когда объявлена структурная переменная, компилятор автоматически выделяет необходимый участок памяти для размещения всех её членов.
При объявлении структуры можно одновременно объявить одну или несколько переменных. Например:
struct addr {
char name[30];
char street[40];
char city[20];
char region[3];
unsigned long int zip;
} addr_info, binfo, cinfo;
объявляет структуру addr и переменные addr_info, binfo, cinfo данного типа.
Если необходима только одна структурная переменная, то нет необходимости в ярлыке структуры. Это означает, что
struct {
char name[30];
char street[40];
char city[20];
char region[3];
unsigned long int zip;
} addr_info;
объявляет одну переменную addr_info с типом определённым предшествующей ей структурой.
Стандартный вид объявления структуры следующий:
struct ярлык{
тип имя_переменной;
тип имя_переменной;
тип имя_переменной;
...
} структурные переменные;
Вопрос 43
Доступ к членам структуры
Доступ к отдельным членам структуры осуществляется с помощью оператора . ("точка"). Например, следующий фрагмент кода присваивает члену zip структурной переменной addr_info значение 12345:
addr_info.zip = 12345;
Ко всем членам структуры доступ осуществляется точно таким же способом. Стандартный вид доступа следующий:
имя_структуры.имя_члена
Таким образом, массив символов addr_info.name может использоваться в fgets():
fgets(addr_info.name,30,stdin);
Для доступа к отдельным элементам addr_info.name можно использовать индекс. Например:
register int t;
for(t=0; addr_info.name[t]; ++t) putchar(addr_info.name[t]);
Вопрос 44
Присваивание структур
Информация, содержащаяся в одной структуре, может быть присвоена другой структуре того же типа с помощью одиночного оператора присваивания, то есть не нужно присваивать значение каждого члена по отдельности. Следующая программа демонстрирует присваивание структур:
#include <stdio.h>
int main()
{
struct ab{
int a;
int b;
} x, y;
x.a = 10;
x.b = 20;
y = x;
printf("Contents of y: %d %d.", y.a, y.b);
return 0;
}
После присваивания y.a и y.b будут содержать значения 10 и 20 соответственно.
Вопрос 45
Массивы структур
Для объявления массива структур следует сначала определить структуру, а затем объявить массив переменных данного типа. Например, для объявления 100-элементного массива структур типа addr, которая была ранее определена в данной лекции, следует написать:
struct addr addr_info[100];
Для доступа к отдельным структурам массива addr_info следует проиндексировать имя массива. Например, для вывода содержимого поля zip третьей структуры, следует написать:
printf("%ld", addr_info[2].zip);
Как и массивы переменных, массивы структур индексируются с 0.
Передача структур в функции
При передаче членов структур в функции фактически передаётся значение члена. Следовательно, передаётся обычная переменная.
Когда же структура используется как аргумент функции, передается вся структура с помощью стандартной передачи по значению. Это означает, что любые изменения, внесенные в содержимое структуры внутри функции, не повлияют на структуру, используемую в качестве аргумента.
Если структуру нужно использовать как параметр, то лучше всего определить структуру глобально, а затем использовать ярлык для объявления необходимых структурных переменных и параметров. Например,
#include <stdio.h>
struct struct_type {
int a, b;
char ch;
};
void f1(struct struct_type parm);
int main(void)
{
struct struct_type arg;
arg.a = 1000;
f1(arg);
return 0;
}
void f1(struct struct_type parm)
{
printf("%d", parm.a);
}
Данная программа выводит число 1000 на экран.
Вопрос 46
Указатели на структуры
Язык С позволяет создавать указатели на структуры так же, как и на другие типы переменных.
Для доступа к членам структуры с помощью указателя на структуру следует использовать оператор "стрелка" ->.
Пример:
struct bal {
float balance;
char name[80];
} person;
struct bal *p;
p = &person;
p->balance = 0f;
Вопрос 47 пункт 9,2 в учебнике. первые два предложения оттуда. и пару слов об использовании(2 красная строка в учебнике).
Битовые поля
Если ограничено место для хранения информации, можно сохранить несколько переменных в одном байте.
Некоторые интерфейсы устройств передают информацию, закодировав биты в один байт.
Некоторым процедурам кодирования необходимо получить доступ к отдельным битам в байте.
Стандартный вид объявления битовых полей следующий:
struct имя структуры {
тип имя1:длина;
тип имя2:длина;
...
тип имяN:длина;};
Пример
struct device {
unsigned active : 1;
unsigned ready : 1;
unsigned xmt_error : 1;
} dev_code;
Битовые поля имеют некоторые ограничения. Нельзя получить адрес переменной битового поля. Переменные битового поля не могут помещаться в массив. Переходя с компьютера на компьютер, нельзя быть уверенным в порядке следования бит (слева направо или справа налево). Любой код, использующий битовые поля, зависит от компьютера.
Вопрос 48
ПУНКТ 9,3 учебника
union union_type{
int i;
char ch[4];
};
Вопрос 49
Перечисления
Перечисления - это набор именованных целочисленных констант, определяющий все допустимые значения, которые может принимать переменная.
enum ярлык {список перечислений} список переменных;
enum coin{ penny, nickel, dime, quarter, half_dollar, dollar};
coin money;
В перечислениях каждому имени ставится в соответствие целочисленное значение и поэтому перечисления могут использоваться в любых целочисленных выражениях.
printf("The value of quarter is %d ", quarter);
Если явно не проводить инициализацию, значение первого имени перечисления будет 0, второго 1 и т.д.
Можно определить значение одного или нескольких символов, использую инициализатор. Это делается путём помещения за символом знака равенства и целочисленного значения. При использовании инициализатора, имена, следующие за инициализированным, получают значение большее на 1, чем указанное перед этим. enum coin{ penny, nickel, dime, quarter=100,
half_dollar, dollar};
Теперь имена получат следующие значения:
penny0
nickel1
dime2
quarter100
half_dollar101
dollar102
9,4 учебник
Вопрос 50
Использование typedef
читай пункт 9,1,2
typedef тип имя;
typedef float balance;
Вопрос 51
Ввод, вывод, потоки и файлы
Потоки и файлы
Система ввода-вывода С поддерживает постоянный интерфейс независимо от устройства, к которому обращается программист. То есть система ввода-вывода С предоставляет уровень абстракции между программистом и аппаратурой. Данная абстракция называется потоком, а собственно устройство называется файлом. Все потоки схожи своим поведением. Существует два типа потоков: текстовые и двоичные.
Текстовые потоки
Текстовые потоки - это последовательности символов. В текстовых потоках некоторые символы могут преобразовываться согласно требованиям среды.
Двоичные потоки
Двоичные потоки - это последовательности байт, имеющих однозначное соответствие с байтами во внешнем устройстве. Тем не менее, может добавляться некоторое количество нулевых байт к двоичному потоку. Эти нулевые байты могут использоваться для унификации представления информации, например для заполнения сектора диска.
Файлы
В С файлы - это логическая концепция, применимая ко всему, начиная от дисковых файлов и заканчивая терминалами. Поток связывается с конкретным файлом с помощью операции открытия. Если файл открыт, то может осуществляться обмен между файлом и программой.
Не все файлы имеют одинаковые возможности. В начале работы программы по стандарту ANSI C открыты три предопределённых текстовых потока: stdin, stdout, stderr.
ПотокУстройствоstdinКлавиатураstdoutЭкранstderrЭкранКонсольный ввод-вывод
Чтение и запись символов
Чтение и запись строк
ФункцияОперацияgetchar( )Читает символ с клавиатуры, ожидает нажатия вводаgetche( )Читает символ с эхом, не ожидает нажатия ввода, не определена в стандарте ANSI C, но является типичным расширениемgetch( )Читает символ без эха, не ожидает нажатия ввода, не определена в стандарте ANSI C, но является типичным расширениемputchar( )Выводит символ на экранputs( )Выводит строку на экранУказатель на файл
Указатель на файл - это указатель на информацию, определяющую различные параметры файла, включая его имя, состояние и текущую позицию. Указатель на файл идентифицирует конкретный дисковый файл и используется потоком для выполнения операций ввода-вывода. Указатель на файл - это переменная-указатель типа FILE. Для создания файловой переменной-указателя используется оператор:
FILE *fp;
Открытие файла
FILE *fopen(const char *имя_файла, const char *режим);
где режим указывает на строку, содержащую желаемый режим открытия файла.
Таблица 1
РежимЗначение"r"Открывает файл для чтения. (По умолчанию как текстовый файл.)"w"Создаёт файл для записи. (По умолчанию как текстовый файл.)"a"Присоединяет к файлу. (По умолчанию как текстовый файл.)"rb"Открывает двоичный файл для чтения."wb"Открывает двоичный файл для записи."ab"Присоединяет к двоичному файлу."r+"Открывает файл для чтения и записи. (По умолчанию как текстовый файл.)"w+"Создаёт файл для чтения и записи. (По умолчанию как текстовый файл.)"a+"Присоединяет или создаёт файл для чтения и записи. (По умолчанию как текстовый файл.) Если необходимо открыть файл с именем test для записи, то следует написать:
fp = fopen("test", "w");
где fp - переменная типа FILE *. Тем не менее, обычно можно увидеть следующее:
if( (fp = fopen("test" , "w")) == NULL) {
puts("Cannot open file.");
exit(1);
}
Если fopen( ) используется для открытия файла на запись, то любой ранее существующий файл с указанным именем будет удалён. Если файла с указанным именем не существует, он будет создан. Если необходимо дописать информацию в конец файла, следует использовать режим "а". Если файл не существует, он будет создан.
Вопрос 52
Форматированный консольный ввод-вывод
Стандартная библиотека С содержит две функции, выполняющие форматированный ввод и вывод стандартных типов данных: printf() и scanf(). Термин форматированный подразумевает, что эти функции могут писать и читать данные в разных форматах, которыми можно управлять. Функция printf() используется для вывода данных на консоль, scanf() - для чтения данных с консоли. Как printf(), так и scanf() могут работать с любыми стандартными типами и строками. Функция printf( ) имеет следующий прототип:
int printf(const char *форматная строка, ...);
Первый аргумент форматная строка определяет способ вывода последующих аргументов. Он содержит два типа элементов: символы, выводимые на экран, и спецификаторы формата, определяющие способ вывода аргументов, следующих за форматной строкой. Спецификаторы формата начинаются со знака процент, за которым следует код формата.
Таблица 1
Код
%c
%d
%i
%e
%E
%f
%lf
%g
%G
%o
%s
%u
%x
%X
%p
%n
%%Формат
Символ
Знаковое десятичное целое число
Знаковое десятичное целое число
Научная нотация (e - строчная)
Научная нотация (E - прописная)
Десятичное число с плавающей точкой
Десятичное число с плавающей точкой двойной точности
Использует или %e, или %f, соответствующее более короткому представлению
Использует или %E, или %f, соответствующее более короткому представлению
Беззнаковое восьмеричное число
Строка символов
Беззнаковое десятичное число
Беззнаковое шестнадцатиричное число (строчные буквы)
Беззнаковое шестнадцатиричное число (прописные буквы)
Вывод указателя
Ассоциированный аргумент - это указатель на целое, в которое записывается число уже выведенных символов
Выводит знак %
Должно быть полное соответствие между числом аргументов и числом спецификаторов формата, а также спецификаторы формата и аргументы должны соответствовать друг другу.
Функция printf( ) возвращает число напечатанных символов. В случае ошибки она возвращает EOF.
Печать символов
Для печати отдельных символов используется %c. Для печати строки %s.
Вывод чисел
Пример
#include <stdio.h>
int main(void)
{
double f;
for(f=1.0; f<1.0e+10; f=f*10)
printf("%g ", f);
getchar();
unsigned int num;
for(num=0; num<255; num++) {
printf("%o ", num);
printf("%x ", num);
printf("%X ", num);
}
getchar();
return 0;
}
Вывод адресов
Пример
#include <stdio.h>
int main(void)
{
/* Вывод адресов */
int sample;
printf("%p ", &sample);
getchar();
return 0;
}
Спецификатор %n
Пример
#include <stdio.h>
int main(void)
{
int sample;
/* Спецификатор %n */
printf("This%n is a test\n", &sample);
printf("%d\n", sample);
getchar();
return 0;
}
Опциональные префиксы для целых чисел
Таблица 2
КодЗначениеhКороткое целое shortlДлинное целое longllДважды длинное целое long longI32Тип __int32I64Тип __int64
Вопрос 53
Модификаторы формата
Спецификатор минимума ширины поля
Целое число, помещаемое между знаком % и кодом формата, рассматривается как спецификатор минимума ширины поля.
Пример #include <stdio.h>
int main(void)
{
double item;
item = 10.12304;
printf("%f\n", item);
printf("%10f\n", item);
printf("%012f\n",item);
return 0;
}
Спецификатор точности
Спецификатор точности состоит из точки, за которой следует целое число.
#include <stdio.h>
int main(void)
{
printf("%.4f\n", 123.1234567);
printf("%3.8d\n", 1000);
printf("%10.15s\n", "This is a simple test");
return 0;
}
В результате получим
123.1235
00001000
This is a simpl
Выровненый вывод
Модификаторы * и #
Помещение # перед g, G, f, e или E обеспечивает наличие десятичной точки в случае отсутствия дробной части. Если перед спецификатором o поместить #, то выведенное число будет иметь в начале 0. Если перед спецификаторами формата x или X поставить #, то шестнадцатеричное число будет печататься с префиксом 0x. Нельзя применять # к другим спецификаторам формата.
Помимо констант, спецификаторы минимальной ширины поля и точности могут предоставляться аргументами printf(). Для реализации этого следует использовать *.
printf("%*.*f", 10, 4, 123.4)
scanf( )
Стандартный ввод с консоли, как правило, осуществляется с помощью scanf(). Она читает все стандартные типы данных и автоматически преобразует числа к правильному внутреннему формату. Стандартный вид scanf() следующий:
int scanf(const char * форматная_строка, ...);
форматная_строка определяет способ чтения значений в переменные, на которые указывает список аргументов. Форматная строка состоит из трёх типов символов:
Спецификаторы формата
Специальные символы
Стандартные символы
Функция scanf( ) возвращает число введённых полей. Она возвращает EOF, если обнаруживается преждевременный конец файла. Спецификаторы формата перечислены в следующей таблице:
Код
%c
%d
%i
%e
%f
%lf
%g
%o
%s
%x
%p
%n
%u
%[]Значение
Читает одиночные символы
Читает десятичное число
Читает десятичное число
Читает число с плавающей запятой
Читает число с плавающей запятой
Читает число с плавающей запятой двойной точности
Читает число с плавающей запятой
Читает восьмеричное число
Читает строку
Читает шестнадцатеричное число
Читает указатель
Получает целочисленное значение, равное числу прочитанных символов
Читает беззнаковое целое
Сканирует множество символов
В scanf( ) следует передавать адреса. Это означает, что все аргументы должны быть указателями на переменные. Например, для чтения целого в переменную count надо использовать следующий вызов scanf():
scanf("%d", &count);
Во избежание ошибок при вводе из-за несовпадения форматов, можно пользоваться значением возвращаемым функцией scanf(). Например:
#include <stdio.h>
#include <clocale>
int main()
{
setlocale(LC_CTYPE,"Russian");
int x, flag = 0;
printf("Введите число ");
do{
flag = scanf("%d", &x);
if(flag != 1) {
puts("Вы попытались ввести не число,");
puts("попробуйте ещё раз");
fflush(stdin);
}
}while(flag != 1);
printf("Введённое Вами число %d\n", x);
return 0;
}
Вопрос 54
Файловая система ANSI C
Заголовочный файл stdio.h предоставляет прототипы для функций ввода-вывода и определяет три типа: size_t, fpos_t и FILE. Типы size_t и fpos_t - зависят от адресного пространства (обычно unsigned long). stdio.h также определяет несколько макросов. Наиболее значимые NULL, EOF, FOPEN_MAX, SEEK_SET, SEEK_CUR, SEEK_END.
ИмяФункцияfopen()
fclose()
putc()
fputc()
getc()
fgetc()
fseek()
fprintf()
fscanf()
feof()
ferror()
rewind()
remove()
fflush()Открывает файл
Закрывает файл
Записывает символ в файл
Аналогично putc()
Читает символ из файла
Аналогично getc()
Переходит к указанному байту в файле
Делает то же в файл, что и printf() на консоль
Делает то же с файлом, что и scanf() c консолью
Возвращает истину при достижении конца файла
Возвращает истину при обнаружении ошибки
Сбрасывает индикатор позиции файла на начало файла
Стирает файл
Очищает буфер файла
Вопрос 55
Запись символа
Система ввода-вывода ANSI С определяет две эквивалентные функции, выводящие символ - putc() и fputc(). Поддержка двух идентичных функций необходима для совместимости со старыми версиями С. Функция putc() используется для записи символов в поток, ранее открытый функцией fopen(). Прототип для putc() следующий:
int putc(int ch, FILE *fp);
где fp - это указатель на файл, возвращённый fopen( ), а ch - выводимый символ.
Если putc( ) выполнена успешно, она возвращает выведенный символ. В противном случае возвращает EOF.
Чтение символа
Имеется две эквивалентные функции для ввода символа - getc() и fgetc(). Функция getc( ) используется для чтения символа из потока, открытого на чтение с помощью fopen( ). Прототип getc( ) следующий:
int getc(FILE *fp);
где fp - это указатель на файл типа FILE *, возвращенный fopen(). Функция getc( ) возвращает EOF при достижении конца файла. Для чтения текстового файла до конца следует использовать следующий код:
ch = getc(fp);
while( ch != EOF) {ch = getc(fp);}
Вопрос 56
fclose( )
Функция fclose( ) используется для закрытия потока, ранее открытого с помощью fopen( ). Она сохраняет в файл данные, находящиеся в дисковом буфере, и выполняет операцию системного уровня по закрытию файла.
Функция fclose( ) имеет прототип:
int fclose(FILE *fp);
где fp - это указатель на файл возвращённый fopen( ). Если возвращён 0, то операция закрытия была выполнена успешно, а если EOF, то, значит, была ошибка.
Вопрос 57
Использование feof( )
Когда файл открыт для двоичного ввода, может быть прочитано значение равное EOF. В результате getc( ) будет отображать состояние конца файла, хотя конец файла не достигнут. Для разрешения этой проблемы в С имеется функция feof( ).
Её прототип:
int feof(FILE *fp);
где fp - идентифицирует файл. Функция feof() возвращает не 0, если достигнут конец файла, в противном случае она возвращает 0. Следующий код читает двоичный файл, пока не встретится конец файла:
while(!feof(fp)) ch = getc(fp);
Следующая программа копирует файл любого типа. Файлы открываются в двоичном режиме, и feof() используется для проверки наличия конца файла:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
FILE *in, *out;
char ch, p[1025], p1[1025];
puts("Input filename");
fgets(p,1025,stdin);
if(p[strlen(p)-1] == '\n') p[strlen(p)-1]='\0';
printf("Length of filename now = %lu\n", strlen(p));
puts("Output filename");
fgets(p1,1025,stdin);
if(p1[strlen(p1)-1] =='\n') p1[strlen(p1)-1]='\0';
printf("Length of filename now = %lu\n", strlen(p1));
if((in=fopen(p, "rb")) == NULL) {
printf("Cannot open source file.");
return 1;
}
if((out=fopen(p1, "wb")) == NULL) {
printf("Cannot open destination file.");
return 1;
}
while(!feof(in)) {
ch = getc(in);
if(!feof(in)) putc(ch, out);
}
fclose(in);
fclose(out);
return 0;
}
Вопрос 58
Работа со строками: fgets() и fputs()
Файловая система ввода-вывода С содержит две функции, которые могут читать или писать строки в поток - fgets() и fputs(). Они имеют следующие прототипы:
int fputs(const char *str, FILE *fp);
char *fgets(char *str, int длина, FILE *fp);
Функция fputs() подобна puts(), за исключением того, что она пишет строку в указанный поток. Функция fgets() читает строку из указанного потока, пока не встретится символ новой строки или не будет прочитано (длина-1) символов. Если прочитан символ новой строки, то он станет частью строки. В обоих случаях результирующая строка завершается нулевым символом. Функция возвращает str в случае успеха и нулевой указатель - в случае ошибки.
Можно использовать fgets() для чтения строки с клавиатуры. Чтобы сделать это, надо использовать stdin как указатель на файл. Например:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
char s[80];
printf("Enter a string: ");
fgets(s, 80, stdin);
if(s[strlen(s)-1] == '\n') s[strlen(s)-1]='\0';
puts(s);
return 0;
}
Вопрос 59
fread( ) и fwrite( )
Файловая система предоставляет две функции, позволяющие читать и писать блоки данных - fread( ) и fwrite( ). Их прототипы:
size_t fread(void *буфер, size_t число_байт, size_t объём, FILE *fp);
size_t fwrite(const void *буфер, size_t число_байт, size_t объём, FILE *fp);
В случае fread() буфер - это указатель на область памяти, которая получает данные из файла. В случае fwrite() буфер - это указатель на информацию, записываемую в файл. Длина каждого элемента в байтах определяется в число_байт. Аргумент объём определяет, сколько элементов (каждый длиной число_байт) будет прочитано или записано. fp - это указатель на ранее открытый поток.
Функция fread( ) возвращает число прочитанных элементов. Данное значение может быть меньше, чем объём, если раньше был достигнут конец файла или произошла ошибка. Функция fwrite( ) возвращает число записанных элементов. Данное значение равно объём, если не возникла ошибка.
Пример
#include <stdio.h>
struct bal{
char name[80];
float cash;
};
int main(void)
{
register int i;
FILE *fp;
bal balance[100];
// открытие на запись
if((fp=fopen("balance","wb"))==NULL) {
printf("Cannot open file.\n");
return 1;
}
for(i=0;i<100;i++) {
balance[i].name[0]='a'+(i%26);
balance[i].name[1]='a'+(i/26);
balance[i].name[2]='\0';
balance[i].cash=(float)i;
}
// сохранение массива balance
fwrite(balance,sizeof balance,1,fp);
fclose(fp);
//обнуление массива
for(i=0;i<100;i++) {balance[i].cash=0.0;}
if((fp=fopen("balance","rb"))==NULL) {
printf("Cannot open file.\n");
return 1;
}
//чтение массива
fread(balance, sizeof balance,1,fp);
//вывод содержимого массива
for(i=0; i<100; i++) {
printf("%s = ", balance[i].name);
printf("%9f\n",balance[i].cash);
}
printf("\n");
fclose(fp);
return 0;
}
Вопрос 60
Указатели
Указатели предоставляют способ, позволяющий функциям модифицировать передаваемые аргументы.
Указатели используются для динамического выделения памяти.
Использование указателей может повысить эффективность работы некоторых программ.
Указатели, как правило, используются для поддержки некоторых структур данных типа связанных списков и двоичных деревьев.
Указатели - это адреса.
Указатель содержит адрес памяти. Как правило, данный адрес содержит местоположение какой-либо переменной в памяти. Если одна переменная содержит адрес другой, то говорят, что первая переменная указывает на вторую.
Переменные-указатели
Если переменная должна содержать указатель, она объявляется следующим образом:
тип *имя;
где тип - это любой допустимый тип (базовый тип указателя), а имя - это имя переменной-указателя. Все арифметические действия с указателями выполняются применительно к базовому типу. Поэтому базовый тип указателя играет важную роль.
Операторы для работы с указателями
Имеется два специальных оператора для работы с указателями - * и &. Оператор & - это унарный оператор, возвращающий адрес операнда. Например:
p=&num;
помещает адрес переменной num в p.
Оператор * - это унарный оператор, возвращающий значение переменной, находящейся по указанному адресу. Например, если р содержит адрес памяти переменной num, то
q=*p;
поместит значение num в q.
Пример:
#include <stdio.h>
int main(){
int num, q;
int *p;
num = 100;
p=&num;
q=*p;
printf("%d\n", q);
return 0;
}
Арифметические действия с указателями
К указателям могут применяться только две арифметические операции: сложение и вычитание. Помимо прибавления или вычитания к указателю целых чисел, можно вычитать из одного указателя другой. Нельзя умножать или делить указатели, нельзя складывать указатели, нельзя добавлять или вычитать типы float и double.
Вопрос 61
Односвязные списки
Организация данных, способ их расположения в памяти, зависит от алгоритма, который будет использоваться.
Односвязный список - это способ соединения объектов, таким образом, когда каждый объект указывает на следующий объект в этом списке. Порядок расположения объектов зависит от приложения, и обычно это просто порядок, в котором программа встречает данные. Списки особенно полезны тогда, когда порядок элементов в них не играет роли. Списки особенно полезны в тех случаях, когда данные в памяти необходимо запоминать сразу же, а мы заранее не знаем, сколько данных можем получить, или когда требуется переставлять данные произвольным образом.
Хотя структура данных не может содержать экземпляр самой себя, она может содержать указатель на экземпляр самого себя. Если используется этот указатель, чтобы указывать на следующий элемент в списке, можно быстро и просто создавать списки, а также добавлять новые элементы в любое место списка, в том числе и в начало. Потребуется "сигнальное" значение для обозначения конца списка, и значение NULL подходит для этой цели превосходно.
Рис. 1
Структуры и функции для односвязных списков
Для создания односвязного списка, содержащего числа типа int, прежде всего, требуется подходящая структура данных.
Листинг 1
struct LIST
{
int Object;
struct LIST *Next;
};
Добавление элементов
Новый элемент можно поместить в начале, в конце или где-нибудь в середине списка. Начнем с функции, добавляющей элемент где-то в середину списка; две другие функции являются фактически частными случаями данной функции.
Листинг 2
int LAdd(LIST **Item, int Object)
{
LIST *NewItem;
if(Item == NULL) {
printf("Error argument function LAdd\n");
exit(1);
}
try{NewItem = new LIST;}
catch(...){
puts("Allocation error!\n");
return 0;
}
NewItem->Object = Object;
if(NULL == *Item) /* Обработать пустой список */
{
NewItem->Next = NULL;
*Item = NewItem;
}
else /* Вставить сразу после текущего элемента */
{
NewItem->Next = (*Item)->Next;
(*Item)->Next = NewItem;
}
return 1;
}
Данная функция вставляет элемент в список после того элемента, адрес которого ей передаётся. Хотелось бы также иметь возможность вставлять новый элемент перед тем элементом, адрес которого передаётся в функцию. Эту задачу выполняет следующая функция. Листинг 3
int LFront(LIST **Item, int Object)
{
int Result = 1;
LIST *p = NULL;
if(Item == NULL) {
printf("Error argument function LFront\n");
exit(1);
}
Result = LAdd(&p, Object);
if(1 == Result) {
p->Next = *Item;
*Item = p;
}
return Result;
}
Однако для односвязных списков это сопряжено с некоторыми трудностями. Можно сделать это только в начале списка, так как невозможно добраться до предыдущего элемента, чтобы изменить его указатель Next. Добавлять элементы в начало списка выгодно, так как это выполняется быстро. Однако иногда может потребоваться поместить новые элементы в конец списка.
Листинг 4
int LAppend(LIST **Item, int Object)
{
int Result = 1;
LIST *EndSeeker;
if(Item == NULL) {
printf("Error argument function LAppend\n");
exit(1);
}
if (NULL == *Item)
{
Result = LAdd(Item, Object);
}
else
{
EndSeeker = *Item;
while(EndSeeker->Next != NULL)
{
EndSeeker = EndSeeker->Next;
}
Result = LAdd(&EndSeeker, Object);
}
return Result;
}
Удаление элемента
Элемент удаляется из списка достаточно просто в том случае, если требуется только освободить память. Однако это не всё, что нужно сделать. Проблема заключается в целостности списка. Можно легко удалить элемент, стоящий после того, который нам указан. Но поскольку у нас нет указателя на предыдущий элемент, можно удалить тот, что указан, и вернуть указатель на следующий элемент.
Листинг 5
LIST *LDeleteThis(LIST *Item)
{
LIST *NextNode = NULL;
if(Item != NULL)
{
NextNode = Item->Next;
delete Item;
}
return NextNode;
}
Следующая функция удаляет элемент, следующий за указанным.
Листинг 6
void LDeleteNext(LIST *Item)
{
if(Item != NULL && Item->Next != NULL)
{
Item->Next = LDeleteThis(Item->Next);
}
}
Очередь
В основных чертах очереди похожи на стеки, но подчиняются другому набору правил - первым пришёл, первым ушёл. При таком режиме можно добавлять элементы только в конец очереди и удалять элементы только из начала очереди. Эта очередь функционирует точно так же, как и обычная очередь в магазине или на почте.
Очереди часто используются при моделировании. Очереди могут быть также полезными в системном программировании. Например, многозадачные операционные системы хранят события и сообщения в очереди или, чаще, во множестве очередей - по одной для каждого текущего процесса.
Программировать очередь с помощью односвязного списка не так удобно, как стек, но это можно осуществить. Хитрость заключается в том, что используются два указателя: один на начало очереди, а другой - на её конец
Вопрос 62
Обход односвязного списка
Алгоритм обхода односвязного списка посещает каждый пункт списка. При этом над каждым пунктом выполняется какая-либо операция, например, вывод содержания на экран или изменение данных.
p = List;
while(p != NULL)
{
printf("%d ", p->Object);
p = p->Next;
}
Можно выполнить этот обход и при помощи цикла for:
for(p = List; p != NULL; p = p->Next) printf("%d ", p->Object);
Вопрос 63
1.Деревья
Дерево в теории графов - это связный неориентированный граф Г, не содержащий циклов. Дерево с одной выделенной вершиной называется корневым деревом.
В корневых деревьях можно построить иерархию. Поскольку из корня можно попасть в любую вершину графа и маршрут только один, то если вершины А и В лежат на одном пути и А встречается раньше В, А называется предком В, а В потомком А. Не все вершины могут быть связаны отношениями предок-потомок.
Вершина, у которой нет потомков, называется листом.
Поддеревом корневого дерева называется любая вершина со всеми её потомками.
2.Бинарные деревья
Бинарное дерево - это корневое дерево с множеством вершин V, таких что:
множество V пусто, или
множество V распадается на три непересекающихся подмножества:
- отдельная вершина r, корень;
- два, возможно пустых поддерева, являющихся бинарными деревьями.
Высота дерева - это количество вершин, расположенных на самом длинном пути от корня к листу.
Бинарное дерево поиска
Поле, по которому производится поиск, называется поисковым ключом, или просто ключом.
Для каждого узла N бинарное дерево поиска удовлетворяет следующим трём условиям:
Значение поискового ключа узла N больше всех значений поисковых ключей, содержащихся в левом поддереве TL.
Значение поискового ключа узла N меньше всех значений поисковых ключей, содержащихся в правом поддереве TR.
Деревья TL и TR являются бинарными деревьями поиска.
Алгоритм симметричного обхода бинарного дерева поиска посещает узлы в порядке, определённом их поисковыми ключами.
Вставка элемента в бинарное дерево поиска
Поиск элемента по ключу
Вопрос 64
ПУНКТ 1 и 2 из вопроса 63
Удаление элемента
Три варианта:
Узел N является листом
Узел N имеет только один дочерний узел
Узел N имеет два дочерних узла
В третьем случае необходимо найти симметричного преемника поискового ключа узла N. Тогда задачу можно свести к первым двум вариантам.
Вопрос 65
ПУНКТ 1 и 2 из вопроса 63
Обход бинарного дерева
Алгоритм обхода бинарного дерева посещает каждую вершину. При этом над каждой вершиной выполняется какая-либо операция, например, вывод содержания на экран или изменение данных. Для определённости будем считать, что при посещении вершины на экран просто выводятся записанные в неё данные.
Рекурсивный алгоритм выглядит следующим образом.
traverse(BinaryTree binTree)
{
if(дерево binTree не пусто)
{
traverse(левое поддерево корня дерева binTree)
traverse(правое поддерево корня дерева binTree)
}
}
Здесь не указаны инструкции, выполняющие вывод на экран данных, находящихся в корне. При обходе любого бинарного дерева алгоритм имеет три возможности посещения корня r. Он может посетить корень r перед обходом обоих поддеревьев, после обхода левого поддерева, но перед обходом правого, или после обхода обоих поддеревьев. Такой порядок называется прямым, симметричным и обратным, соответственно.
Вопрос 66
Балансировка бинарного дерева поиска
Алгоритмы вставки элемента в бинарное дерево поиска, поиска элемента по ключу, удаления элемента из бинарного дерева поиска требуют количества операций линейно зависящее от высоты дерева h.
При вставке элементов в бинарное дерево поиска дерево может иметь как вид а) так и вид б).
а)б)
Очевидно, что в случае а) высота дерева равна числу узлов. Поэтому возникает задача приведения бинарного дерева поиска к виду с высотой как можно меньше.
Введём понятие уровня узла А.
Если узел А является корнем дерева Т, он принадлежит первому уровню.
В противном случае уровень узла на единицу больше чем уровень его предка.
Бинарное дерево Т, имеющее высоту h, является совершенным, если выполняются следующие условия:
все узлы, начиная с уровня h-2 и выше, имеют по два дочерних узла;
если узел, находящийся на уровне h-1, имеет дочерние узлы, то все узлы, находящиеся на этом же уровне слева от него, имеют по два дочерних узла;
если узел, находящийся на уровне h-1, имеет один дочерний узел, то он является его левым дочерним узлом.
При симметричном обходе бинарного дерева поиска узлы посещаются в порядке возрастания ключа.
Если есть достаточное количество памяти, то можно воспользоваться следующим методом приведения бинарного дерева поиска к дереву с минимальной высотой.
Записать все элементы из бинарного дерева поиска в массив при симметричном обходе, при этом массив будет упорядочен по возрастанию ключа. Удалить исходное дерево.
Создать пустое бинарное дерево поиска. Вставлять элементы из массива длины n при помощи следующей рекурсивной функции (псевдокод).
Insert_from_array(BinaryTree *binTree, Type_elem *a, int n)
{
if(n == 0) return;
else{
Insert(binTree, a[n/2]);
Insert_from_array(binTree, a, (n/2));
Insert_from_array(binTree, &a[n/2+1],(n-(n/2)-1));
}
}
Здесь Insert(BinaryTree *binTree, Type_elem a) - функция вставляющая элемент в бинарное дерево поиска.
012...[n/2]...n-1u_0u_1u_2...u_[n/2] ...u_(n-1) Для действительного числа x обозначим ⌈x⌉ - верхнюю грань, т.е. минимальное число не меньшее x. Для дерева T обозначим h(T) его высоту. Докажем, что построенное таким образом бинарное дерево, с числом вершин n≥1, имеет высоту равную ⌈log_2⁡(n+1) ⌉.
Доказательство по индукции.
При n=1, ⌈log_2⁡(1+1) ⌉=1 утверждение верно.
Предположим, что оно верно при всех n<k.
Докажем для n=k. Высота полученного дерева равна h(T)==1+max┬⁡{h(T_L ),h(T_R )}. Поскольку левое поддерево содержит не меньше вершин, чем правое, то h(T)=1+⌈log_2⁡〖(⌊n/2⌋+1)〗 ⌉=⌈log_2⁡(n+1) ⌉.
Утверждение доказано.
В случае, когда нет возможности приводить дерево к дереву с минимальной высотой, приводят дерево к виду с высотой близкой к минимальной. Бинарное дерево поиска называется сбалансированным по высоте, или просто сбалансированным, если высота правого поддерева любого его узла отличается от высоты левого поддерева не больше чем на 1.
Сбалансированное бинарное дерево поиска с n узлами имеет высоту между log_2⁡〖(n+1)〗 и 〖1.4404×log〗_2⁡〖(n+2)-0.328〗 (см. D. Knuth, The Art of Computer Programming, v.3).
Методы балансировки бинарных деревьев, требующие существенно меньших затрат памяти, чем изложенный, не рассматриваются в данном курсе.
ДОПОЛНИТЕЛЬНОЕ
fseek( ) и произвольный доступ
Можно выполнять операции произвольного чтения и записи, используя систему буферизированного ввода-вывода, с помощью функции fseek(), устанавливающей текущую файловую позицию. Её прототип:
int fseek(FILE *fp, long число_байт, int начало);
где *fp - это указатель на файл, число_байт - это длинное целое, содержащее число байт от начала до позиции маркера, а начало - это одно из макроопределений:
МакроопределениеСмыслSEEK_SETНачало файлаSEEK_CURТекущая позицияSEEK_ENDКонец файла Макроопределения есть целочисленные значения, причём SEEK_SET равен 0, SEEK_CUR 1, SEEK_END 2. Функция fseek() возвращает 0 в случае удачи или ненулевое значение в случае ошибки.
Пример
Чтение 234-ого байта из файла test.
int func(void)
{
FILE *fp;
if ((fp = fopen("test", "rb") == NULL) {
printf("Cannot open file.\n");
exit(1);}
fseek(fp, 234L, 0);
return getc(fp);
}
Другой пример - программа, позволяющая просмотреть содержимое файла, как в ASCII, так и шестнадцатеричном формате.
#include <stdio.h>
#include <ctype.h>
#define SIZE 128
void display(int numread);
char buf[SIZE];
int main(int argc, char *argv[])
{
FILE *fp;
int sector, numread;
if(argc!=2) {
printf("Usage: dump filename");
return 1;
}
if((fp=fopen(argv[1], "rb"))==NULL) {
printf("Cannot open file.");
return 1;
}
do {
printf("Enter sector: ");
scanf("%d", &sector);
if(sector >= 0) {
if(fseek(fp, sector*SIZE, SEEK_SET)) {
printf("seek error");
}
if((numread=fread(buf, 1, SIZE, fp))!=SIZE)
printf("EOF reached.");
display(numread);
}
}while(sector >= 0);
return 0;
}
void display(int numread)
{
int i, j;
for(i=0; i<numread/16; i++) {
for(j=0; j<16; j++) printf("%3X", buf[i*16+j]);
printf(" ");
for(j=0; j<16; j++) {
if(isprint(buf[i*16+j]))
printf("%c",buf[i*16+j]);
else printf(".");
}
printf("\n");
}
}
Результат действия программы на файл с текстом программы:
Функция ferror( )
Функция ferror() используется для определения, привела ли выполненная операция к ошибке.
int ferror(FILE *fp);
Она возвращает истину, если в результате выполнения предыдущей операции произошла ошибка.
fprintf() и fscanf()
Помимо основных функций ввода-вывода, система буферизированного ввода-вывода содержит fprintf() и fscanf(). Данные функции ведут себя также, как и printf() и scanf(), за тем исключением, что работают с дисковыми файлами. Они имеют следующие прототипы:
int fprintf(FILE *fp, const char *форматная_строка,...);
int fscanf(FILE *fp, const char *форматная_строка,...);
где fp - это указатель на файл, возвращённый fopen().
Например, для чтения из файла длинных целых чисел в десятичной записи, разделенных пробелами и знаками перехода на новую строку, можно пользоваться следующим кодом:
#include <stdio.h>
int main()
{
long x;
int flag = 0;
FILE *fp;
if(NULL == (fp = fopen("test_in.txt","r"))){
puts("Cannot open file!");
return 1;
}
while(!feof(fp)){
flag = fscanf(fp, "%ld", &x);
if(flag == 1) printf("Input = %ld\n", x);
else if(flag == EOF) break;
else if(ferror(fp)){
puts("File error!");
return 1;
}
else {
puts("Format error!");
return 1;
}
}
fclose(fp);
return 0;
}
Функция rewind( )
Функция rewind() сбрасывает маркер файла на начало для файла, указанного в аргументе. Её прототип:
void rewind(FILE *fp);
где fp - допустимый файловый указатель.
Удаление файлов
Функция remove() удаляет файлы. Её прототип:
int remove(const char *имя_файла);
В случае удачного выполнения она возвращает 0, а в случае неудачного не 0.
Директива #include
Директива #include предлагает компилятору включить другой исходный файл, имя которого указывается после директивы. Имя файла заключается в двойные кавычки или в треугольные скобки <>.
Подключаемые файлы также могут иметь директивы #include. Если подключаемый файл будет указан в <>, то поиск будет происходить в стандартных каталогах, предназначенных для хранения заголовочных файлов. Если подключаемый файл заключен в двойные кавычки, поиск будет происходить в текущем рабочем каталоге. Если файл не найден, то поиск продолжится в стандартных каталогах.
Директивы условной компиляции
Существует несколько директив условной компиляции, позволяющих изменять порядок компиляции программы в зависимости от определенных макросов. Данный процесс называется условной компиляцией и широко используется.
Директивы #if, #else, #elif, #endif
Если после #if константное выражение принимает истинное значение, то код между #if и #endif компилируется, в противном случае код пропускается. Директива #endif используется для обозначения конца блока #if.
Стандартный вид #if следующий
#if константное выражение
последовательность операторов
#endif
Пример:
#include <stdio.h>
#define SIZE 100
int main(void)
{
#if SIZE>99
puts("Compiled for array greater than 99.");
#endif
return 0;
}
Данная программа выводит сообщение на экран, поскольку SIZE имеет значение большее 99. Данный пример демонстрирует важный момент: выражение следующее за #if, вычисляется на этапе компиляции. Следовательно, оно должно содержать ранее определенные идентификаторы и константы, а не переменные.
Действие #else во многом похоже на действие оператора else - она предоставляет альтернативный вариант, если #if содержит ложное выражение.
Пример:
#include <stdio.h>
#define SIZE 10
int main(void)
{
#if SIZE>99
puts("Compiled for array greater than 99.");
#else
puts("Compiled for small array.");
#endif
return 0;
}
В данном случае SIZE определяется так, чтобы значение было меньше 99, в результате чего компилируется код, соответствующий #else. Следовательно, выводится "Compiled for small array.".
Обратите внимание, что #else используется для обозначения конца блока #if и начала блока #else. Это делается потому, что может быть только один #endif, связанный с#if.
#elif означает "иначе, если" и используется для построения лесенки if-else-if с целью определения различных опций компиляции. За #elif следует константное выражение. Если выражение истинно, то блок кода компилируется и остальные выражения не проверяются. В противном случае рассматривается следующий блок. Стандартный вид #elif следующий:
#if выражение 1
последовательность операторов
#elif выражение 2
последовательность операторов
#elif выражение 3
последовательность операторов
...
#elif выражение N
последовательность операторов
#else
последовательность операторов
#endif
Пример
#include <stdio.h>
#include <locale.h> #define GB 0
#define RUSSIA 7
#define GERMANY 10
#define ACTUAL_COUNTRY 7
int main(void)
{
#if ACTUAL_COUNTRY==RUSSIA
puts(setlocale(LC_ALL,"Russian"));
puts("Установлена локализация для России");
#elif ACTUAL_COUNTRY==GB
puts(setlocale( LC_ALL, "English" ));
#else puts(setlocale(LC_ALL, "German"));
#endif
printf("%g\n",1234000000.0);
return 0;
}
#if и #elif могут быть вложенными. Если это имеет место, то каждый #endif, #else или #elif ассоциируется с ближайшим #if или #elif.
Директивы #ifdef и #ifndef
Директивы #ifdef и #ifndef предписывают соответственно "компилировать, если определено" и "компилировать, если не определено".
Стандартный вид #ifdef следующий:
#ifdef имя_макроса
последовательность операторов
#endif
Если имя макроса определено ранее в #define, то последовательность операторов, стоящих между #ifdef и #endif, будет компилироваться.
Стандартный вид #ifndef следующий:
#ifndef имя_макроса
последовательность операторов
#endif
Если имя макроса не определено ранее в #define, то последовательность операторов, стоящих между #ifndef и #endif, будет компилироваться.
Как #ifdef, так и #ifndef могут использовать оператор #else, но не #elif.
Директива #undef Директива #undef используется для снятия определения макроса. Имеет следующий вид:
#undef имя_макроса
Автор
xak93
Документ
Категория
Без категории
Просмотров
523
Размер файла
534 Кб
Теги
ответы
1/--страниц
Пожаловаться на содержимое документа