Советы по программированию. При выполнении вариантов заданий придерживайтесь следующих ключевых моментов.
При выполнении вариантов заданий придерживайтесь следующих ключевых моментов.
1. Выражение, стоящее в круглых скобках операторов if, while и do-while, вычисляется по правилам стандартных приоритетов операций.
2. Если в какой-либо ветви вычислений условного оператора или в цикле требуется выполнить два (и более) оператора, то они при помощи фигурных скобок объединяются в блок.
3. Проверка вещественных величин на равенство, как правило, из-за ограниченной разрядности дает неверный результат.
4. Чтобы получить максимальную читаемость и простоту структуры программы, надо правильно выбирать способ реализации ветвлений (с помощью if, switch, или условных операций), а также наиболее подходящий оператор цикла.
5. Выражение в операторе switch и константные выражения в case должны быть целочисленного или символьного типов.
6. Рекомендуется использовать в операторе switch ветвь default.
7. После каждой ветви для передачи управления на точку кода за оператором switch используется оператор break.
8. При построении любого цикла надо не забывать тот факт, что в нем всегда явно или неявно присутствуют четыре основных элемента: начальные установки, код цикла, модификация параметра цикла и проверка условия на продолжение цикла.
9. Если количество повторений цикла заранее не известно (реализуется итерационный процесс), необходимо предусмотреть аварийное завершение цикла при получении достаточно большого количества итераций.
10. При использовании бесконечного цикла обязательно необходима организация выхода из цикла по условию.
ЗАДАНИЕ 2. Разветвляющиеся алгоритмы
Первый уровень сложности
Составить программу нахождения требуемого значения с исходными данными x, y, z. Обозначение: min и max – нахождение минимального и максимального из перечисленных в скобках значений элементов.
1. ; 2. ;
3. ; 4. ;
5. ; 6. ;
7. ; 8. ;
9. ; 10. ;
11. ; 12. ;
13. ; 14. ;
15. .
Второй уровень сложности
Вычислить значение y в зависимости от выбранной функции j(x), аргумент которой определяется из поставленного условия. Возможные значения функции j(x): 2x, x2, х/3. Предусмотреть вывод сообщений, показывающих, при каком условии и с какой функцией производились вычисления у.
1. , где
2. , где
3. , где
4. , где
5. , где
6. , где
7. , где
8. , где
9. , где
10. , где
11. , где
12. , где
13. , где
14. , где
15. , где
ЗАДАНИЕ 3. Циклические алгоритмы
Первый уровень сложности
Составить программу для определения таблицы значений функции у в произвольном диапазоне [a, b] изменения аргумента хс произвольным шагом h. Значения a, b, hвводятся с клавиатуры. Таблица должна содержать следующие столбцы: порядковый номер, значение аргумента x, значение функции, сообщение о возрастании или убывании функции.
Определить максимальное и минимальное значения функции.
1. a = –p; b = p; h = 0,4.
2. a = 0,7; b = 1,8; h = 0,1.
3. a = –0,5; b = 2,5; h = 0,2.
4. a = –0,9; b = 2,7; h = 0,3.
5. a = –2; b = 0,8; h = 0,2.
6. a = –1,9; b = 2,7; h = 0,3.
7. a = –0,4p; b = 0,4p; h = 0,5.
8. a = –0,3p; b = 1,3p; h = p/10.
9. a = –p/2; b = p/2; h = p/10.
10. a = –3; b = 3; h = 0,5.
11. a = –p; b = p; h = p/6.
12. a = –0,9; b = 1, h = 0,3.
13. a = –0,9; b = 2,7; h = 0,3.
14. a = –0,1; b = 2; h = 0,1.
15. a = p; b = 2p; h = p/15.
Второй уровень сложности
Значение аргумента x изменяется от a до b с шагом h. Для каждого x найти значения функции Y(x), суммы S(x) и |Y(x)–S(x)| и вывести в виде таблицы. Значения a, b, h и n вводятся с клавиатуры. Так как значение S(x) является рядом разложения функции Y(x), значения S и Y для заданного аргумента x должны совпадать в целой части и в первых двух-четырех позициях после десятичной точки.
Работу программы проверить для a = 0,1; b = 1,0; h = 0,1; значение параметра n выбрать в зависимости от задания.
1. , .
2. .
3. .
4. .
5. .
6. .
7. .
8. , .
9. , .
10. , .
11. , .
12. , .
13. , .
14. , .
15., .
ГЛАВА 9. Указатели
Определение указателей
При обработке декларации любой переменной, например double x=1.5; компилятор выделяет для переменной участок памяти, размер которого определяется ее типом (double – 8 байт), и инициализирует его указанным значением (если таковое имеется). Далее все обращения в программе к переменной по имени заменяются компилятором на адрес участка памяти, в котором будет храниться значение этой переменной. Разработчик программы на языке Си имеет возможность определить собственные переменные для хранения адресов участков оперативной памяти. Такие переменные называются указателями.
Итак, указатель – это переменная, которая может содержать адрес некоторого объекта. Простейшая декларация указателя имеет формат
тип * ID_указателя;
Например: int *a; double *f; char *w;
Здесь тип может быть любым, кроме ссылки или битового поля, причем тип может быть к этому моменту только декларирован, но еще не определен (следовательно, в структуре, например, может присутствовать указатель на структуру того же типа).
Символ «звездочка» относится непосредственно к ID указателя, поэтому для того, чтобы декларировать несколько указателей, ее нужно записывать перед именем каждого из них.
Например, в декларации:
int *a, *b, с;
определены два указателя на участки памяти для целочисленных данных, а также обычная целочисленная переменная с.
Значение указателя равно первому байту участка памяти, на который он ссылается.
Указатели предназначены для хранения адресов областей памяти. В языке Cи имеются три вида указателей – указатели на объект известного типа, указатель типа void и указатель на функцию. Эти три вида различаются как своими свойствами, так и набором допустимых операций. Указатель не является самостоятельным типом данных, так как всегда связан с каким-либо конкретным типом, т.е. указатель на объект содержит адрес области памяти, в которой хранятся данные определенного типа.
Указатель типа void применяется в тех случаях, когда конкретный тип объекта, адрес которого требуется хранить, не определен (например, если в одной и той же переменной в разные моменты времени требуется хранить адреса объектов различных типов).
Указателю типа void можно присвоить значение указателя любого типа, а также сравнивать его с любыми другими указателями, но перед выполнением каких-либо действий с участком памяти, на которую он ссылается, требуется явно преобразовать его к конкретному типу.
Указатель может быть константой или переменной, а также указывать на константу или переменную.
С указателями-переменными связаны две унарные операции & и *.
Операция & означает «взять адрес» операнда. Операция * имеет смысл – «значение, расположенное по указанному адресу» (операция разадресации).
Таким образом, обращение к объектам любого типа как операндам операций в языке Cи может производиться:
– по имени (идентификатору);
– по указателю (операция косвенной адресации):
ID_указателя=&ID_объекта; – операция разыменования;
*ID_указателя – операция косвенной адресации.
Говорят, что использование указателя означает отказ от именования адресуемого им объекта.
Операция разадресации, или разыменования, предназначена для доступа к величине, адрес которой хранится в указателе. Эту операцию можно использовать как для получения, так и для изменения значения величины (если она не объявлена как константа).
Унарная операция получения адреса & применима к переменным, имеющим имя (ID), для которых выделены участки оперативной памяти. Таким образом, нельзя получить адрес скалярного выражения, неименованной константы или регистровой переменной (типа register).
Отказ от именования объектов при наличии возможности доступа по указателю приближает язык Си по гибкости отображения «объект – память» к языку ассемблера.
Пример 1:
int x, – переменная типа int ;
*y; – указатель на объект типа int;
y = &x; – y – адрес переменной x;
*y=1; – косвенная адресация указателем поля x, т.е. по
указанному адресу записать 1: x = 1.
Пример 2:
int i, j = 8, k = 5, *y;
y=&i;
*y=2; – i = 2
y=&j;
*y+=i; – j += i ® j = j+i ® j = j + 2 = 10
y=&k;
k+=*y; – k += k ® k = k + k = 10
(*y)++; – k++ ® k = k + 1 = 10 + 1 = 11
Как видно из приведенных примеров, конструкцию *ID_указателя можно использовать в левой части оператора присваивания, так как она является L-значением (см. разд. 4.3), т.е. определяет адрес участка памяти. Эту конструкцию часто считают именем переменной, на которую ссылается указатель. С ней допустимы все действия, определенные для величин соответствующего типа (если указатель инициализирован).
Пример 3:
int i1; – целая переменная;
const int i2=1; – целая константа;
int * pi1; – указатель на целую переменную;
const int * pi2; – указатель на целую константу;
int * const pi1=&i1; – указатель-константа на целую переменную;
const int * const pi2=&i2; – указатель-константа на целую константу.
Как видно из примеров, модификатор const, находящийся между ID указателя и символом «звездочка», относится к самому указателю и запрещает его изменение, a const слева от звездочки задает константное значение объекта, на который он указывает. Для инициализации указателей использована операция получения адреса &.
Указатель подчиняется общим правилам определения области действия, видимости и времени жизни.
Операция sizeof
Формат записи:
sizeof ( параметр);
параметр – тип или идентификатор объекта (но не ID функции).
Данная операция позволяет определить размер указанного параметра в байтах (тип результата int).
Если указан идентификатор сложного объекта (массив, структура, объединение), то результатом будет размер всего объекта. Например:
sizeof(int) – результат 2(4) байта;
double b[5];
sizeof(b) – результат 8 байт * 5 = 40 байт.
Динамическое распределение оперативной памяти (ОП) связано с операциями порождения и уничтожения объектов по запросу программы, при котором захват и освобождение памяти производится программно, т.е. в процессе работы программы. При этом в языке Си порождение объектов (захват памяти) и уничтожение объектов (освобождение памяти) выполняются при помощи библиотечных функций.
Инициализация указателей
При декларации указателя желательно выполнить его инициализацию, т.е. присвоение начального значения. Наиболее распространенная из ошибок в программах – непреднамеренное использование неинициализированных указателей.
Инициализатор записывается после ID указателя либо в круглых скобках, либо после знака равенства.
Существуют следующие способы инициализации указателя:
1. Присваивание указателю адреса существующего объекта:
а) используя операцию получения адреса переменной:
int a = 5;
int *p = &а; – указателю p присвоили адрес объекта а;
int *p(&а); – то же самое другим способом;
б) с помощью значения другого инициализированного указателя:
int *g = р;
Указателю-переменной можно присвоить значение другого указателя либо выражения типа указатель с использованием при необходимости операции приведения типа. Приведение типа необязательно, если один из указателей имеет тип void *, например
int i,*x;
char *y;
x = &i; // x – поле объекта int;
y = (char *)x; // y – поле объекта char;
y = (char *)&i; // y – поле объекта char;
в) с помощью идентификаторов массива или функции, которые трактуются как адрес начала участка памяти, в котором размещается указанный объект. Причем следует учитывать тот факт, что ID массивов и функций являются константными указателями. Такую константу можно присвоить переменной типа указатель, но нельзя подвергать преобразованиям, например:
int x[100], *y;
y = x; – присваивание константы переменной;
x = y; – ошибка, т.к. в левой части указатель-константа.
2. Присваивание пустого значения:
int *x1 = NULL;
int *x2 = 0;
В первой строке используется константа NULL, определенная как указатель, равный нулю. Рекомендуется использовать просто цифру 0, так как это значение типа int будет правильно преобразовано стандартными способами в соответствии с контекстом. А так как объекта с нулевым (фиктивным) адресом не существует, пустой указатель обычно используют для контроля, ссылается указатель на конкретный объект или нет.
3. Присваивание указателю адреса выделенного участка динамической памяти:
а) c помощью операции new (см. разд. 16.4):
int *n = new int;
int *m = new int (10);
б) c помощью функции malloc (см. разд. 10.9):
int *p = (int*)malloc(sizeof(int));
Присваивание без явного приведения типов допускается в двух случаях:
– указателям типа void*;
– если тип указателей справа и слева от операции присваивания один и тот же.
Если переменная-указатель выходит из области своего действия, отведенная под нее память освобождается. Следовательно, динамическая переменная, на которую ссылался указатель, становится недоступной. При этом память, на которую указывала сама динамическая переменная, не освобождается. Такая ситуация называется «замусоривание оперативной памяти». Еще одна причина появления «мусора» – когда инициализированному указателю присваивается значение другого указателя. При этом старое значение указателя теряется.
Операции над указателями
Помимо уже рассмотренных операций, с указателями можно выполнять арифметические операции сложения, инкремента (++), вычитания, декремента (--) и операции сравнения.
Арифметические операции с указателями автоматически учитывают размер типа величин, адресуемых указателями. Эти операции применимы только к указателям одного типа и имеют смысл в основном при работе со структурами данных, последовательно размещенными в памяти, например с массивами.
Инкремент перемещает указатель к следующему элементу массива, декремент – к предыдущему.
Указатель, таким образом, может использоваться в выражениях вида
p # iv, ## p, p ##, p # = iv,
p – указатель, iv – целочисленное выражение, # – символ операции '+' или '–'.
Результатом таких выражений является увеличенное или уменьшенное значение указателя на величину iv * sizeof(*p), т.е. если указатель на определенный тип увеличивается или уменьшается на константу, его значение изменяется на величину этой константы, умноженную на размер объекта данного типа.
Текущее значение указателя всегда ссылается на позицию некоторого объекта в памяти с учетом правил выравнивания для соответствующего типа данных. Таким образом, значение p # iv указывает на объект того же типа, расположенный в памяти со смещением на iv позиций.
При сравнении указателей могут использоваться отношения любого вида («>», «<» и т.д.), но наиболее важными видами проверок являются отношения равенства и неравенства («==», «!=»).
Отношения порядка имеют смысл только для указателей на последовательно размещенные объекты (элементы одного массива).
Разность двух указателей дает число объектов адресуемого ими типа в соответствующем диапазоне адресов, т.е. в применении к массивам разность указателей, например, на третий и шестой элементы равна 3.
Очевидно, что уменьшаемый и вычитаемый указатели должны принадлежать одному массиву, иначе результат операции не имеет практической ценности и может привести к непредсказуемому результату. То же можно сказать и о суммировании указателей.
Значение указателя можно вывести на экран с помощью функции printf, используя спецификацию %p (pointer), результат выводится в шестнадцатеричном виде.
Рассмотрим фрагмент программы:
int a = 5, *p, *p1, *p2;
p = &a;
p2 = p1 = p;
++p1;
p2 += 2;
printf(“a = %d , p = %d , p = %p , p1 = %p , p2 = %p .\n”, a, *p, p, p1, p2);
Результат может быть следующим:
a = 5 , *p = 5 , p = FFF4 , p1 = FFF6, p2 = FFF8 .
Графически это выглядит следующим образом (в 16-разрядном процессоре на тип int отводится 2 байта):
FFF5 | FFF7 | FFF9 | |||||||
FFF4 р | FFF6 p1 | FFF8 p2 | FFF10 | ||||||
p = FFF4,
p1 = FFF6 = ( FFF4 + 1*sizeof(*p)) ® FFF4 + 2 (int)
р2 = FFF8 = ( FFF4 + 2*sizeof(*p)) ® FFF4 + 2*2
На одну и ту же область памяти (как видно из приведенного примера), может ссылаться несколько указателей различного типа. Но примененная к ним операция разадресации даст разные результаты.
При смешивании в выражении указателей разных типов явное преобразование типов требуется для всех указателей, кроме void*.
Явное приведение типов указателей позволяет получить адрес объекта любого типа:
type *p;
p = (type*) &object;
Значение указателя p позволяет работать с переменной object как объектом типа type.
ГЛАВА 10. Массивы
Понятие массива
В математике для удобства записи различных операций часто используют индексированные переменные: векторы, матрицы и т.п. Так, вектор представляется набором чисел (c1, c2, ..., cn), называемых его компонентами, причем каждая компонента имеет свой номер, который принято обозначать в виде индекса. Матрица А – это таблица чисел (аij, i=1,..., n; j=1,..., m), i – номер строки, j – номер столбца. Операции над матрицами и векторами обычно имеют короткую запись, которая обозначает определенные, порой сложные действия над их индексными компонентами. Например, произведение двух векторов записывается как . Произведение матрицы на вектор .
Таким образом, если с группой величин одинакового типа требуется выполнять однообразные действия, им дают одно имя, а различают по порядковому номеру.
Введение индексированных переменных в языках программирования также позволяет значительно облегчить реализацию многих сложных алгоритмов, связанных с обработкой массивов однотипных данных.
Например, использование массивов данных позволяет компактно записывать множество операций с помощью циклов.
В языке Си для этой цели используется сложный тип данных – массив, представляющий собой упорядоченную конечную совокупность элементов одного типа. Число элементов массива называют его размером. Каждый элемент массива определяется идентификатором массива и своим порядковым номером – индексом. Индекс – целое число, по которому производится доступ к элементу массива. Индексов может быть несколько. В этом случае массив называют многомерным, а количество индексов одного элемента массива является его размерностью.
Описание массива в программе отличается от описания простой переменной наличием после имени квадратных скобок, в которых задается количество элементов массива. Например, double a [10]; – описание массива из 10 вещественных чисел.
При описании массивов квадратные скобки являются элементом синтаксиса, а не указанием на необязательность конструкции.
Размеры массивов предпочтительнее вводить с клавиатуры как значения целочисленных переменных или задавать с помощью именованных констант, поскольку при таком подходе для ее изменения достаточно скорректировать значение константы всего лишь в одном месте программы.
Одномерные массивы
В программе одномерный массив объявляется следующим образом:
типID_массива [размер] = {список начальных значений};
тип – базовый тип элементов массива (целый, вещественный, символьный); размер – количество элементов в массиве.
Список начальных значений используется при необходимости инициализировать данные при объявлении, он может отсутствовать.
При декларации массива можно использовать также атрибуты «класс памяти» и const.
Размер массива вместе с типом его элементов определяет объем памяти, необходимый для размещения массива, которое выполняется на этапе компиляции, поэтому размер массива задается только константой или константным выражением. Нельзя задавать массив переменного размера, для этого существует отдельный механизм – динамическое выделение памяти.
Пример объявления массива целого типа: int a[5];
Индексы массивов в языке Си начинаются с 0, т.е. в массиве а первый элемент: а[0], второй – а[1], … пятый – а[4].
Обращение к элементу массива в программе на языке Си осуществляется в традиционном для многих других языков стиле – записи операции обращения по индексу [] (квадратные скобки), например:
a[0]=1;
a[i]++;
a[3]=a[i]+a[i+1];
Пример объявления массива целого типа с инициализацией начальных значений:
int a[5]={2, 4, 6, 8, 10};
Если в группе {…} список значений короче, то оставшимся элементам присваивается 0.
Внимание. В языке Си с целью повышения быстродействия программы отсутствует механизм контроля выхода за границы индексов массивов. При необходимости такой механизм должен быть запрограммирован явно.