Технология программирования
Роль вычислительной техники в информационных системах. Компьютеризация учебного процесса. Технологичность программного обеспечения. Особенности отладки и испытания пpогpамм. Операторы языка СИ. Указатели и структуры данных. Основы доступа к файлам.
Рубрика | Программирование, компьютеры и кибернетика |
Вид | тезисы |
Язык | русский |
Дата добавления | 10.05.2015 |
Размер файла | 603,6 K |
Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже
Студенты, аспиранты, молодые ученые, использующие базу знаний в своей учебе и работе, будут вам очень благодарны.
Строка #define определяет символьное имя, или именованную константу, для заданного стринга литер.
#define имя подставляемый_текст
С этого момента при любом появлении имени оно будет заменяться на соответствующий ему подставляемый_текст.
Подставляемый_текст может быть любой последовательностью литер, среди которых могут встречаться не только цифры.
# include <stdio.h>
# define LOWER 0 // нижняя граница таблицы
# define UPPER 300 // верхняя граница
# define STEP 20 // размер шага
/* печать таблицы температур по Фаренгейту и Цельсию */
main ()
{ int f;
for (f=LOWER; f<=UPPER; f=f+STEP)
printf ("%3d %6.1f\n", f, (5.0/9.0)*(f-32));
}
Ввод-вывод литер.
Стандартная библиотека поддерживает очень простую модель ввода-вывода. Текстовый ввод-вывод вне зависимости от того, откуда он исходит или куда направляется, имеет дело с потоком литер. Модель ввода-вывода - это текстовый поток, т.е. последовательность литер, разбитая на строки, каждая из которых содержит нуль или более литер и завершается литерой новая строка (\n).
Простейшими из функций чтения и записи одной литеры являются - getchar и putchar, соответственно чтение следующей литеры ввода из текстового потока и печать содержимого целой переменной в виде литеры.
Например, программа копирования по одной литере с входного потока в выходной поток:
# include <stdio.h> // копирование ввода на вывод
main ()
{ int с;
c=getchar ();
while (c!=EOF) // константа EOF, определенная в <stdio.h>
{ putchar (c);
с=getchar ();
}
}
Для хранения литерных данных специально предназначен тип char, однако для этого также годится и любой целый тип.
Использование типа int, а не char для переменной с связано с одной важной причиной: функция getchar по исчерпании входного потока в качестве своего результата должна выдавать значение, отличное от любой реальной литеры.
В Си любое присваивание, и в частности c=getchar (), трактуется как выражение со значением, равным значению левой части после присваивания. Это значит, что присваивание может встречаться внутри более сложного выражения.
# include <stdio.h> // копирование ввода на вывод
main ()
{ int с;
while ((c=getchar ())!=EOF) // присваивание вместе с проверкой
putchar (c);
}
Следующие программы подсчитывают во входном текстовом потоке литеры, строки и слова.
# include <stdio.h> // подсчет литер
main ()
{ double nc;
for (nc=0; getchar ()!=EOF; ++nc) ; // пустой оператор тела цикла
printf ("%.0f \ n", nc);
}
# include <stdio.h> // подсчет строк
main ()
{ int c, nline=0;
while ((c=getchar ())!=EOF)
if (c= ='\n') ++nline;
printf ("%d\n", nline);
}
# include <stdio.h>
# define IN 1 // внутри слова
# define OUT 0 // вне слова
// подсчет строк, слов и литер
main ()
{ int c, nline, nword, nchar, state;
state=OUT;
nline=nword=nchar=0;
while ((c=getchar ())!=EOF)
{ ++nchar;
if (c= = `\n') ++nline;
if (c= =' ` ¦ ¦ c = = `\n' ¦ ¦ c= ='\t') state=OUT;
else if (state = = OUT)
{ state = IN;
++nword;
}
}
printf ("%d%d%d\n", nline, nword, nchar);
}
Описание. Массивы, функции
Декларация int array[10]; определяет array как массив из 10 значений типа int. В Си элементы массива всегда нумеруются, начиная с нуля. Индексом может быть любое целое выражение, образуемое целыми переменными и целыми константами.
Напишем, программу, подсчитывающую по отдельности каждую цифру, пробельные литеры (пробелы ` ', табуляции `\t' и новые строки - `\n') и все другие литеры. Счетчик цифр храним в массиве.
# include <stdio.h> /* подсчет цифр, пробельных и прочих литер */
main ()
{ int c, i, nwhite, nother;
int ndigit [10];
nwhite=nother=0;
for (i=0; i<10; i++) ndigit [i]=0;
while ((c=getchar())!=EOF)
if (c>='0' && c<='9') // является ли литера цифрой?
++ndigit [c-`0'];
else if (c = = ` `¦ ¦ c = = `\n' ¦ ¦ c = = `\t') ++nwhite;
else ++nother;
printf ("цифры = ");
for (i=0; i<10; i++)
printf ("%d", ndigit [i]);
printf (", пробелы=%d,прочие =%d\n", nwhite, nother);
}
Проверка if (c>='0' && c<='9') определяет, является ли, находящаяся в с литера цифрой. Если это так, то c-`0' есть числовое значение цифры.
По определению значения типа char являются всего лишь малыми целыми, так что переменные и константы типа char в арифметических выражениях идентичны значениям типа int. Например, c-`0' - есть целое выражение с возможными значениями от 0 до 9, которые соответствуют литерам от `0' до `9', хранящимся в переменной с. Таким образом, значение данного выражения является правильным индексом для массива ndigit.
Функции
Функция обеспечивает удобный способ отдельно оформить некоторые вычисление и пользоваться им далее, не заботясь о том, как оно реализовано. Механизм использования функций в Си удобен, легок и эффективен и служит цели - получать более ясную программу.
Определение любой функции имеет следующий вид:
тип_результата имя_функции (список_формальных_параметров)
{ декларации
инструкции
}
Кроме определения функции существует понятие прототипа функции, формат которого имеет следующий вид:
тип_результата имя_функции (список_формальных_параметров);
Такая декларация должна стоять перед main и сообщать компилятору о возможности использования этой функции в программе (само определение функции должно находиться после функции main), а также должна быть согласована с определением и всеми вызовами. Будет ошибкой, если определение функции или вызов не соответствует своему прототипу.
Имена параметров не требуют согласования. Фактически в прототипе они могут быть произвольными или вообще отсутствовать.
Значение, вычисляемое функцией, возвращается в main с помощью инструкции return. За словом return может следовать любое выражение:
return выражение;
Функция не обязательно возвращает какое-нибудь значение. Инструкция return без выражения только передает управление в ту программу, которая ее вызвала, не передавая ей никакого результирующего значения.
Поскольку main есть функция, как и любая другая, она может вернуть результирующее значение тому, кто ее вызвал, - фактически в операционную среду, в которой была запущена программа. Обычно возвращается нулевое значение, что говорит о нормальном завершении счета (return 0;).
Вызов по значению, массивы литер, внешние переменные и область действия
Аргументы, вызов по значению
В Си все аргументы функции передаются "по значению", в отличие от "вызова по ссылке", которые позволяют функции иметь доступ к самим аргументам, а не к их локальным копиям. Поэтому в Си вызываемая функция не может непосредственно изменить переменную вызывающей функции: она может изменить только ее частную, временную копию.
Благодаря этому свойству обычно удается написать более компактную программу.
Если функции требуется изменить переменную в вызывающей программе, тогда последняя должна передать адрес подлежащей изменению переменной (указатель на переменную), а в вызываемой функции соответствующий параметр следует описать как указатель и организовать косвенный доступ через него к этой переменой.
Если аргументом функции является массив (имя массива), то функции передается значение, которое является адресом начало этого массива; никакие элементы массива не копируются.
Массивы литер
Самый распространенный вид массива в Си - массив литер. Использование литерных массивов проиллюстрируем программой чтения текстовых строк и печати самой длиной из них.
Технология программирования предполагает обязательным этап проектирования. Так как язык Си относится к языкам структурного программирования, поэтому будем использовать функциональную декомпозицию, а также представление логики отдельных функции в виде блок-схем с применением основных управляющих конструкций [доп.13].
Схема программы нашего примеры достаточно проста:
ЦИКЛ-ПОКА (есть ли еще строка?)
Если (данная строка длиннее самой длинной из предыдущих)
ТО
Запомнить ее
Запомнить ее длину
ВСЕ-ЕСЛИ
Напечатать самую длинную строку
ВСЕ-ЦИКЛ
Из схемы видно, что программа естественным образом распадается на части. Одна из них получает новую строку, другая проверяет ее, третья запоминает, а остальные управляют процессом вычислений. Поскольку, как правило, процесс четко распадается на части, его хорошо бы так и переводить на Си
# include <stdio.h>
# define MAXLINE 1000 // максимальный размер вводимой строки
int getline (char line [], int maxline); // прототип функции getline ( )
void copy (char to [], char from[]); // прототип
// печать самой длинной строки
main ( )
{ int len ; // длина текущей строки
int max; // длина макс из просмотренных строк
char line [MAXLINE]; // текущая строка
char longest [MAXLINE]; // самая длинная строка
max =0;
while ((len=getline (line, MAXLINE )) >0)
if(len > max)
{ max=len;
copy (longest, line);
}
if (max > 0) // была ли хоть одна строка?
printf("%s", longest);
return 0;
}
// getline: читает строку в s, возвращает длину
int getline (char s [], int lim)
{ int c,i;
for (i=0; i<lim-1 (c=getchar( ))!= EOF c!='\ n'; i++)
s[i] =c;
if(c= ='\n')
{s[i]=c;
++ i;
}
s[i]='\0'; // конец стринга литер
return i ;
}
// copy: копирует из `from 'в `to'
void copy (char to [], char from [])
{ int i=0;
while ((to [i]=from[i])!='\0')
+ + i;
}
Функции main и getline взаимодействуют между собой через пару аргументов и возвращаемое значение. Одни функции возвращают результирующее значение, другие (как copy) нужны только для того, чтобы произвести какие-то действия, не выдавая никакого значения. На месте типа результата в copy стоит void. Это явное указание на то, что никакого значения данная функция не возвращает.
Задание размера массива в определении функции имеет целью резервирование памяти. В самой getline задавать длину массива s нет необходимоcти, так как его размер указан в main.
В конец создаваемого массива литер обязательно помещается литера `\0' (null - литера, кодируемая нулевым байтом), чтобы пометить конец стринга литер.
Внешние переменные и область действия
Переменные line, longest и т.д. локализованы в main, поэтому никакие другие функции прямо к ним обращаться не могут. То же верно и применительно к переменным других функций. Такие переменные называются локальными, которые возникают в момент обращения к этой функции и исчезают после окончание ее работы (выхода из нее).
Такие переменные не сохраняют своих значений от вызова к вызову и должны устанавливаться заново при каждом новом обращении к функции.
Альтернативой локальным (автоматическим - auto) переменным можно определить внешние (extern) переменные, к которым разрешается обращаться по их именам из любой функции.
Внешние переменные можно использовать вместо аргументов для связи между функциями по данным.
Внешние переменные сохраняют свои значения и после возврата из функций, их установивших.
Внешняя переменная должна быть определена, причем только один раз, вне текста любой функции.
Внешняя переменная должна быть описана (декларирована) во всех функциях, которые хотят ею пользоваться.
"Определение" переменной (функции)- это место, где переменная (функция) создается и ей отводится память.
"Описание" помещается там, где фиксируется природа переменной (функции), но никакой памяти для нее не отводится.
Основная литература - 1 [36-41], 3 [85-105].
1[23-29], 3[39-53]. 1[14-22], 2[40-67], 3[13-38].
Контрольные вопросы:
1. Какова структура программы на языке Си?
2. Под элементами языка понимаются его базовые конструкции, используемые при написании программ. Каковы элементы языка Си?
3. Операции - это комбинации символов, специфицирующие действия по преобразованию значений. Каковы операции языка Си?
4. Константа - это число, символ или строка символов. Для чего используются в программе константы и каковы типы констант языка Си?
5. Ключевые слова - это предопределенные идентификаторы, которые имеют специальное значение для компилятора. Каков список ключевых слов языка Си?
6. Отдельные исходные файлы можно объединить в один исходный файл, компилируемый как единое целое, посредством директивы препроцессора #include. Какие директивы нужны для работы с текстовой информацией?
7. Каково назначение функции main ()?
8. Как протестировать программу подсчета слов? Какой ввод вероятнее всего не обнаружит ошибок, если они были допущены?
9. В чем смысл использования именованных констант?
10. Каков приоритет операторов ¦ ¦ и &&, и как вычисляются выражения, связанные этими операторами?
11. Какую роль в Си играют функции?
12. Каким образом обеспечивается способ многопутевого ветвления на языке Си?
13. Каков синтаксис задания типа возвращаемого значения функции?
14. Что происходит, если в процессе вычислений мы выходим на конец функции, отмеченный в тексте последней закрывающей фигурной скобкой?
15. Что такое прототип функции и какую роль он играет с точки зрения интерфейса программы?
16. Что такое передача аргументов "по значению" и "по ссылке"?
17. Если функция в качестве аргумента получает массив, то каким образом она имеет доступ к любому элементу массива?
18. Каковы основные управляющие конструкции в структурном программировании?
19. Что должна делать main, если встретится строка, превышающая допустимый размер (MAXLINE)?
20. В каких случаях оправдано применение локальных и внешних переменных?
Лекция 6. Операторы языка СИ. Условный оператор. Операторы цикла. Оператор выбора
Переменные и константы являются основными объектами, с которыми имеет дело программа.
Тип объекта определяет множество значений, которые этот объект может принимать, и операции, которые над ними могут выполняться.
Всем переменным разумно давать осмысленные имена в соответствии с их назначением. Короткие имена предпочтительны для локальных переменных, особенно для счетчиков циклов, и более длинные для внешних переменных. Обычно в программах на Си малыми (строчными) буквами набирают переменные, а большими (прописными)- именованные константы.
В Си существует всего лишь несколько базовых типов: char, int, float, double. Имеется также несколько квалификаторов, которые можно использовать вместе с указанными базовыми типами: short, long-применяемые к целым, и signed, unsigned-применяемые к типу char и любому целому типу. Тип long double предназначен для арифметики с плавающей точкой повышенной точности.
Именованные константы для всех размеров вместе с другими характеристиками машины и компилятора содержатся в стандартных головных файлах <limits. h> и <float.h>.
Целый тип (включая перечислимый тип) и плавающий тип в совокупности образуют арифметический тип.
Тип void (пустой) имеет специальное назначение. Указание спецификации типа void в объявлении функции означает, что функция не возвращает значений.
Указание типа void в списке объявлений аргументов в объявлении функции означает, что функция не принимает аргументов.
Объявление указателя на тип void означает, что он будет ссылаться на любой, т.е. не - специфицированный тип.
Тип void может быть указан в операции приведения типа. Нельзя объявить переменную тип void .
Область значений - это интервал от минимального до максимального значения, которое может быть представлено в переменной данного типа.
Декларации
Все переменные должны быть декларированы (описаны) раньше, чем будут использоваться. Декларация специфицирует тип и содержит список переменной (ых) этого типа. В своей декларации переменная может быть инициализирована.
Инициализация неавтоматической переменной осуществляется только один раз, перед началом работы программы (инициализатор - константное выражение). Локальная переменная получает начальное значение каждый раз при входе в функцию либо в блок (инициализатор - любое выражение). Внешние и статические переменные по умолчанию получают нулевые значения.
К любой переменной в декларации может быть применен квалификатор const для указания того, что ее значение далее не будет изменяться. Применительно к массиву квалификатор const указывает на то, что ни один из его элементов не будет меняться. Указание const можно также применять к аргументу - массиву, чтобы сообщить, что функция не изменяет этот массив.
Арифметические операторы
Бинарными арифметическими операторами являются:
+, - , *, /, %,
где %-подобный оператору mod языка Паскаль - оператор взятия модуля. Оператор % к операндам типов float и doublе не применяется.
Операторами отношения и сравнения являются:
<, <=, >, >=, = = (равно), != (не равно).
Логические операторы: (конъюнкция) и ¦¦ (дизъюнкция).
По определению численным результатом вычисления выражения отношения или логического является 1 в случае, если оно истинно, и 0-если оно ложно.
Оператор ! обычно используют в конструкциях вида
if (!valid) // не правильный
что эквивалентно if (valid = = 0).
Преобразования типов
Если операнды оператора принадлежат разным типам, то они приводятся к некоторому общему типу, типу с большим диапазоном значений. В общем случае, когда оператор имеет разнотипные операнды, прежде чем операция начнет выполняться, "младший" тип подтягивается к "старшему". Результат будет иметь старший тип. К старшим типам, в порядке уменьшения их приоритета, относятся: long double, double, float, int, long.
Значения типа char-это всего лишь малые целые, и их можно свободно использовать в арифметических выражениях, что значительно облегчает всевозможное манипуляции с литерами. В качестве примера приведет простую реализацию функции atoi, преобразующей последовательность цифр в ее числовой эквивалент.
// atoi: преобразование стринга s в число
int atoi (char s[])
{ int i, n=0;
for (i=0; s[i]>='0' s[i]<='9'; i + +)
n =10*n+(s[i]-`0');
return n;
}
Преобразования имеют место и при присваиваниях: значение правой части присваивания приводится к типу левой части, который и является типом результата.
Так как аргумент в вызове функции есть выражение, при передаче его функции также возможно преобразование типа. При отсутствии прототипа функции аргументы типа char и short переводятся в int, a float- в double. В том случае, когда аргументы описаны в прототипе функции, как и должно быть, при вызове функции нужное преобразование включается автоматически.
Инкрементные и декрементные операторы. Операторы присваивания и выражения
В Си есть два необычных оператора, предназначенных для увеличения и уменьшения переменных: + + - инкрементный оператор добавляет 1 к своему операнду, и - - - декрементный оператор, который уменьшает на 1 значение своего операнда.
Например,
if (c = = `\n') + + nline;
В свою очередь, эти два оператора могут использоваться как префиксные (помещая их перед операндом, например, + + n) , так и как постфиксные (помещая их после операнда, например, n + +). В обоих случаях значение n увеличивается на 1 . Но выражение ++n увеличивает n до того, как его значение будет использовано в выражении, а n+ + - после того. Применение этих операторов к переменным вне выражения не зависит от места их размещения относительно переменной. Поэтому в приведенном выше примере, приращение переменной nline ( + + nline либо nline + + ) будет иметь равный эффект.
Другое дело, когда переменная входит в выражение. В этом случае порядок размещения инкрементных (декрементных) операторов относительно переменных важен при вычислении значения выражения.
Побитовые операции.
В Си имеются шесть операторов для манипулирования с битами. Их можно применять только к операндам типов char, short, int и long, знаковым и беззнаковым.
Для иллюстрации некоторых побитовых операций рассмотрим функцию getbits(x,p,n), которая формирует поле в n бит, вырезанное из х, начиная с позиции р, прижимая его к правому краю. Например, getbits(x,4,3) вернет в качестве результата 4,3 и 2-ой биты значения х, прижимая их к правому краю. Вот эта функция:
//getbits: получает n бит, начиная с р-ой позиции
unsigned getbits (unsigned x, int p, int n)
{ return (x>>(p+1-n)) & ~(~0<<n);
}
Операторы присваивания и выражения
Большинству бинарных операторов т.е. операторов, имеющих левый и правый операнды, соответствуют операторы присваивания op=, где ор - один из операторов + - * / % << >> & ^ ¦.
Например, выражение i=i+2 можно написать в сжатом виде: i+=2.
Помимо краткости операторы присваивания обладают тем преимуществом, что они более соответствуют тому, как человек мыслит. В сложных выражениях запись становится более легкой для понимания. Оператор присваивания может помочь компилятору сгенерировать более эффективный код.
Типом и значением любого выражения присваивания являются тип и значение его левого операнда после завершения присваивания.
Условные выражения
Инструкции
if(a>b) z=a;
else z=b;
пересылают в z максимальное из двух значений, а и b.
В Си существует тернарный оператор "?:", который представляет собой другой способ записи этой и подобных ей конструкций:
z=(a>b) ? a:b; // z=max(a,b)
Условное выражение и в самом деле является выражением, и его можно использовать в любом месте, где допускается выражение.
Условное выражение часто позволяет сократить программу. В качестве примера приведем цикл, обеспечивающий печать n элементов массива по 10 на каждой строке с одним пробелом между колонками; каждая строка цикла, включая последнюю, заканчивается литерой новая - строка:
for (i=0; i<n; i+ +)
printf ("%6d%c", ar [i],(i%10= = 9¦¦i= = n-1)?'\n':' `);
Литера новая-строка посылается после каждого десятого и после n-го элемента. За всеми другими элементами следует пробел.
Управление
Порядок, в котором выполняются вычисления, определяется инструкциями управления.
Любое выражение становится инструкцией, если в конце его поставить точку с запятой, как, например, в записи х=0; i++; printf(…);
В Си точка с запятой является заключающей литерой инструкции, а не разделителем, как в языке Паскаль.
Составная инструкция или блок - это объединение описаний и инструкций с помощью фигурных скобок. Синтаксически, такой блок воспринимается как одна инструкция.
Другой пример объединения инструкций - это круглые скобки, помещенное после if, else, while или for.
После правой закрывающей фигурной скобки в конце блока точка с запятой не ставится.
К инструкциям управления относятся:
if- else - для принятия решения;
else -if- для многоступенчатого принятия решения;
switch - переключатель для выбора одного из многих путей (аналог оператора case в Паскале);
циклы while и for;
цикл do- while;
инструкции break и continue;
инструкции goto и метки.
Приведем некоторые комментарии к данным инструкциям:
- отсутствие в одной из вложенных друг в друга if - конструкций else - части может привести к не однозначному толкованию записи. Эту неоднозначность разрешают тем, что else всегда связывают с ближайшим if , у которого нет своего else;
- правильной интерпретации вложенности конструкций if и else помогает правильная расстановка фигурных скобок, а также с помощью показа отступов в тексте;
- каждая ветвь переключателя switch ( в том числе и после default) должна заканчиваться инструкцией break - выход из переключателя;
- с точки зрения грамматики три компоненты цикла for (выр1; выр2; выр3) представляют собой произвольные выражения, но чаще выр1 и выр3 - это присваивания или вызовы функций, а выр2 - выражение отношения. Любое из этих трех выражений может отсутствовать, но точку с запятой опускать нельзя. При отсутствии выр1 и выр3 считается, что их просто нет в конструкции цикла; при отсутствии выр2 полагается, что его значение как бы всегда истинно;
- в Си индекс цикла for и его предельное значение могут изменяться внутри цикла, и значение индекса цикла после выхода из цикла всегда определено;
- пара выражений, разделенных оператором "запятая", вычисляется слева направо. Типом и значением результата являются тип и значение самого правого выражения, что позволяет в инструкции for в каждой из трех компонент иметь по несколько выражений, например, вести два индекса параллельно (в отличие от запятых, которые разделяют аргументы функции, переменные в описаниях и т.д.);
- повтор в цикле do - while происходит когда условие истинно;
- альтернативой нормальному завершению цикла являются инструкции break и continue: инструкция break вызывает немедленный выход из самого внутреннего из объемлющих ее циклов или переключателей; инструкция continue вынуждает ближайший объемлющий ее цикл ( for, while или do - while) начать следующий шаг итерации.
Основная литература - 1[42-70], 2[135-167], 3[120-132], 3[85-106].
Контрольные вопросы:
1. Что такое тип объекта?
2. Что преследует разрешение помечать объекты квалификатором const?
3. Каково количество значимых литер для внутренних имен и для имен функций и внешних переменных?
4. Что такое эскейп - последовательность? Каков полный набор эскейп - последовательностей?
5. Каковы приоритеты операторов языка Си и порядок их выполнения.
6. Пусть int k=2; Чему будет равно значение переменной к после выполнения оператора присваивания: k=k+ + + + + k;
7. Каким образом происходит заполнение освобождающихся разрядов при применении операторов сдвига <<, >>?
8. Какими преимуществами обладают операторы присваивания в сжатом виде?
9. Каким образом определено правило преобразования для тернарного оператора, в случае если входящие в него выражения принадлежат любым типам?
10. Каков приоритет и порядок вычислений всех операторов языка Си?
11. Так как if просто проверяет числовое значение выражения, условие иногда можно записывать в сокращенном виде. Приведите примеры таких записей.
12. Покажите различные способы разрешения неоднозначности при вложенности друг в друга if - конструкций.
Лекция 7. Функции и структура программы
Функции подразделяют большие вычислительные задачи на более мелкие и позволяют воспользоваться тем, что уже сделано другими разработчиками, а не начинать создание программы каждый раз "с нуля". В выбранных должным образом функциях "упрятаны" (инкапсулированы) несущественные для других частей программы детали их функционирования, что делает программу в целом более ясной и облегчает внесение в нее изменений.
Обычно программы на Си состоят их большого числа небольших функций, а не из немногих больших. Программу можно располагать в одном или нескольких исходных файлах. Эти файлы можно компилировать отдельно, а загружать вместе, в том числе и с ранее откомпилированными библиотечными функциями.
Подразделение задачи на более мелкие решается путем функциональной декомпозиции, результатом которой является структура, содержащая одну главную функцию(main), все выделенные и отношения между ними. Любая программа таким образом есть просто совокупность определений переменных и функций. Связи между функциями осуществляются через аргументы, возвращаемые значения и внешние переменные. В исходном файле функции разрешается располагать в любом порядке; исходную программу можно разбивать на любое число файлов, но так, чтобы ни одна из функций не оказалась разрезанной.
Инструкция return реализует механизм возврата результата от вызываемой к вызывающей функции. За словом return может следовать любое выражение. Если потребуется, выражение будет приведено к типу тип-результата.
Функция main может выдавать в качестве результата работы программы какое - либо число, которое будет доступно в той среде, из которой данная программа была вызвана.
Если тип результата функции опущен, то предполагается, что она возвращает значение типа int.
Функции, возвращающие нецелые значения, в первую очередь должны декларировать об этом через тип возвращаемого значения. Кроме того, важно, чтобы вызывающая программа знала, что вызываемая функция возвращает нецелое значение. Один из способов обеспечить это - явно описать вызываемую функцию в вызывающей. Например:
#include <stdio.h>
#define MAXLINE 100
// примитивный калькулятор
main ( )
{ double sum, atof (char []);
char line [MAXLINE];
int getline (char line [], int max);
sum=0;
while (getline ( line, MAXLINE) >0)
printf ("\t%g\n",sum+=atof(line));
return 0;
}
В декларации double sum, atof (char []); говорится, что sum - переменная типа double, а atof- функция, которая принимает аргумент типа char [] и возвращает результат типа double (преобразует стринг в double).
Если в выражении встретилось имя, нигде ранее не описанное, за которым следует открывающая скобка, то такое имя по контексту считается именем функции, получающей результат типа int; при этом относительно ее аргументов ничего не предполагается.
Внешние переменные.
Программа на Си обычно оперирует с множеством внешних объектом: переменных и функций. Внешние переменные специфицируются вне функций и потенциально доступны для многих функций. Сами функции всегда являются внешними объектами, поскольку в Си запрещено определять функции внутри других функций. По умолчанию одинаковые внешние имена, используемые в разных файлах, ссылаются на один и тот же внешний объект (функцию).
Для любой функции внешняя (external) переменная доступна по ее имени, если это имя было должным образом описано в этой функции.
Внешние переменные существуют постоянно, так что их значения сохраняются в интервалах между обращениями к функциям. Таким образом, если двум функциям приходится пользоваться одними и теми же данными и ни одна и их них не вызывает другую, то часто бывает удобно оформить эти общие данные в виде внешних переменных, а не передавать их в функцию и обратно через аргументы.
Функции: правила областей действия. Статические объекты
Исходный текст Си-программ можно хранить в нескольких файлах, а ранее скомпилированные программы можно загружать из библиотек. В связи с этим возникают следующие вопросы: как писать декларации, в каком порядке их располагать и как организовать. Другими словами, каким образом разбиение программы на несколько файлов скажется на описаниях переменных, связях частей программы нужным образом, инициализации, использовании памяти и т.д.
Областью действия имени считается часть программы, в которой это имя можно использовать. Для автоматических переменных областью действия является функция, в которой они описаны. Одноименные локальные переменные разных функций никак не связаны друг с другом. Тоже утверждение справедливо и в отношении параметров функций, которые фактически являются локальными переменными.
область действия внешней переменной или функции простирается от точки программы, где она декларирована до конца файла, подлежащего компиляции. Например, если main, sp, val, push и pop определены в одном файле в указанном порядке, т.е.
main ( ) { … }
int sp=0;
double val [MAX];
void push (double f) { … }
double pop (void) { … }
то к переменным sp и val можно адресоваться из push и pop просто по их именам; никаких дополнительных деклараций для них не требуется. В main эти имена не видимы, как и сами push и pop.
Если на внешнюю переменную нужно сослаться до того, как она определена, или если она определена в другом файле, то ее декларация должна быть помечена словом extern. Декларация (описание) внешней переменной объявляет свойства переменной (прежде всего ее тип), а определение, кроме того, приводит к определению для нее памяти.
Если строки
int sp;
double val [MAX];
расположены вне всех функций, то они определяют внешние переменные sp и val, т.е. отводят для них память, и, кроме того служат декларациями для остальной части исходного файла. А вот строки
extern int sp;
extern double val [ ];
декларируют для остальной части файла, что sp - переменная типа int и что val - массив типа double (размер которого определен где-то в другом месте); при этом ни переменная ни массив не создаются, и память им не отводится.
На всю совокупность файлов, из которых состоит исходная программа, для каждой внешней переменной должно быть одно-единственное определение; другие файлы, чтобы получить доступ к внешней переменной должны иметь в себе декларацию extern.
В определениях массивов необходимо указывать их размеры, что же касается деклараций extern, то здесь они не обязательны.
Инициализировать внешнюю переменную можно только в определении!
Пусть определим функции push и pop в одном файле, а переменные sp и val - в другом, где их и инициализируем. Тогда для установления связей понадобятся такие определения и декларации:
В файле 1:
extern int sp;
extern double val [ ];
void push (double f) { … }
double pop ( void ) { … }
В файле 2:
int sp=0;
double val [MAX];
Поскольку декларации extern находятся в начале файла 1 и вне определений функций, их действие распространяется на все функции, причем одного набора деклараций достаточно для всего файла 1. Та же организация extern - деклараций необходима и в случае, когда программа состоит из одного файла, но определения sp и val расположены после их использования.
Указание static, примененное к внешней переменной или функции, ограничивает область действия объекта концом файла. Это есть способ скрыть имена от других файлов. Статическая память специфицируется словом static, которое помещается перед обычным описанием. Указание static перед декларациями переменных sp и val, с которыми работают только push и pop, скрывает их от остальных функций.
Обычно имена функций глобальны и видимы из любого места программы.
Если же функция помечена словом static, то ее имя становится невидимым вне файла, в котором она определена.
Декларацию static можно использовать и для внутренних переменных.
Как и автоматические (локальные) переменные, внутренние статические переменные локальны в функциях, но в отличие от автоматических они не возникают только на период активации функции, а существуют постоянно. Это значит, что для внутренних статических переменных отводится постоянная память.
Основная литература - 1[71-93], 2[256-268], 3[133-145].
Контрольные вопросы:
1. Что такое функция?
2. Что специфицирует определение функции?
3. Что задает объявление функции?
4. В определении функции допускается указание спецификации класса памяти static или extern. Что такая спецификация задает?
Лекция 8. Указатели
Указатели и адреса
Указатель - это переменная, содержащая адрес переменной. Указатели широко применяются в Си- отчасти потому что в некоторых случаях без них просто не обойтись, а отчасти потому что программы с ними обычно короче и эффективнее. При соблюдении определенной дисциплины с помощью указателей можно достичь ясности и простоты.
Память типичной машины - массив последовательно пронумерованных и проадресованных ячеек, с которыми можно работать по отдельности или связными кусками. Применительно к любой машине верны следующие утверждения: один байт может хранить значение типа char, двухбайтовые ячейки могут рассматриваться как целое типа short, а четырехбайтовые - как целые типа long. Указатель - это группа ячеек (как правило, две или четыре), в которых может храниться адрес.
Пусть имеет следующие декларации:
int x=1, y=2, z[10];
int *ip; //ip - указатель на int
Декларация int*ip (символ * "прижимается" к идентификатору переменной) гласит: "выражение *ip есть нечто типа int".
Унарный оператор &(амперсанд) выдает адрес объекта. Унарный оператор * есть оператор раскрытия ссылки. Этот оператор применяется только к указателю и выдает содержимое объекта, на который указатель ссылается.
Следующие несколько строк показывают каким образом используются операторы * и &.
ip=&x; //указателю присвоен адрес переменной x, ip указывает на x
y=*ip; //y теперь равен 1, а не 2
*ip=0; //х теперь равен 0
ip=z; //ip указывает на 0-й элемент (начало) массива z
ip = &z [5]; //ip теперь указывает на 5-й элемент массива z
Таким образом, синтаксис декларации переменной указатель "подстраивается" под синтаксис выражений, в которых эта переменная может встретиться. Указанный принцип применим и в отношении описаний функций. Например, запись double *dp, atof(char*);означает, что выражение *dp и atof (s) имеют тип double, а аргумент функции atof есть указатель на char (фактически, указатель на стринг).
Указателю разрешено ссылаться только на объекты заданного типа.
Указатель на void может ссылаться на объекты любого типа, но к такому указателю нельзя применять оператор раскрытия ссылки.
Так как указатели сами являются переменными, в тексте они могут встречаться и без оператора раскрытия ссылки.
Указатели и аргументы функций. Указатели и массивы.
Поскольку функции в Си в качестве своих аргументов получают значение параметров, поэтому прямой возможности, находясь в вызванной функции, изменить переменную вызывающей функции нет.
Чтобы получить желаемый эффект, надо вызывающей программе передать указатели на те значения, которые должны быть изменены. В этом случае формальные параметры вызываемых функций должны быть описаны как указатели, при этом доступ к значениям параметров будет осуществляться через них косвенно. Формальные параметры - указатели позволяют вызываемой функции осуществлять доступ к объектам вызвавшей ее программы и дают возможность изменить эти объекты.
Рассмотрим функцию swap, которая в программе сортировки переставляет местами два неупорядоченных элемента.
Если функция swap определена следующим образом:
void swap (int x, int y)
{ int temp;
temp=x; x=y; y=temp;
}
тогда вызов swap(a,b) никак не повлияет на переменные a и b, поскольку swap получает лишь копии их значений. Для достижения эффекта - перестановки, формальные параметры функции swap должны быть описаны как указатели, а аргументы в вызове функции представлены адресами соответствующих параметров - swap (&a, &b).
void swap (int *px, int *py)
{int temp;
temp=*px; *px=*py; *py=temp;
}
В Си существует связь между указателями и массивами, и связь эта настолько тесная, что эти средства лучше рассматривать вместе. Доступ к элементу массива кроме операции индексирования, может быть выполнен при помощи указателя, и такой вариант работает быстрее.
Декларация int ar[10]; определяет массив ar размера 10, т.е. блок из 10 последовательных объектов с именами ar[0], ar[1],…,ar[9]. Запись ar[i] означает i-й элемент массива.
Если pa есть указатель на int, т.е. определен как int * pa; то в результате присваивания pa=&a[0]; указатель pa будет указывать (ссылаться) на нулевой элемент ar; иначе говоря, pa будет содержать адрес элемента ar[0]. Присваивание x=* pa; будет копировать содержимое ar[0] в x.
Если pa указывает на некоторый элемент массива, то pa+1 по определению указывает на следующий элемент, pa+i - на i-й элемент после pa, а pa-i - на i - й элемент перед pa. Таким образом, если pa указывает на ar[0], то *( pa+1) есть содержимое ar[1], pa+i - адрес ar[i], а *(pa+i) - содержимое ar[i].
Таким образом, элемент массива одинаково разрешается изображать и в виде указателя со смещением и в виде массива с индексом.
Указатель - это переменная, а имя массива это константный указатель, по этой причине можно написать pa=а или ра+ +, а записи типа ar=pa или ar+ + не допускаются.
Если имя массива передается функции. то последняя получает в качестве аргумента адрес его начального элемента. Внутри вызываемой функции этот аргумент является локальной переменной, содержащей адрес. Вообще, функция может рассматривать имя массива в качестве своего аргумента так, как ей удобно - либо как имя массива (char s [ ]), либо как указатель ( char *s), и поступать с ним соответственно. Функция может даже использовать оба вида записи, если это покажется ей уместным.
Адресная арифметика. Литерные указатели и функции.
Если р есть указатель на некоторый элемент массива, то р+ + продвигает р так, чтобы он указывал на следующий элемент, а р+=i увеличивает его, чтобы он указывал на i-й элемент после того, на который он указывал ранее. Эти и подобные конструкции - самые простые примеры арифметики над указателями, называемой так же адресной арифметикой.
Соединение в одном языке Си указателей, массивов и адресной арифметики - одна из сильных его сторон.
Как и любую другую переменную, указатель можно инициализировать, но только такими осмысленными для него значениями, как нуль или выражение, приводящее к некоторому адресу ранее определенных данных, соответствующего типа.
Декларация static char *p=ar; определяет р как указатель на char и инициализирует его адресом массива ar. Указанная декларация могла бы иметь и такой вид:
static char *p=&ar[0]
поскольку имя массива и есть адрес его нулевого элемента.
Отметим несколько важных свойств арифметики с указателями:
- при соблюдении некоторых правил указатели можно сравнить. Если p и q указывают на элементы одного массива, то к ним можно применять операторы отношения = =, !=, <, <=, > и т.д. Например отношение вида p<q истинно, если р указывает на более ранний элемент массива, чем q;
- любой указатель всегда можно сравнить на равенство и неравенство с нулем;
- для указателей, ссылающихся на элементы разных массивов, результат арифметических операций или сравнений не определен;
- указатели и целые можно складывать и вычитывать. Запись p+n означает адрес объекта, занимающего n-ое место после объекта на который указывает р. Это справедливо безотносительно к типу объекта, на который ссылается р; n автоматически домножается на коэффициент, соответствующий размеру (типу) объекта. Информация о размере неявно присутствует в описании р. Если, к примеру, int занимает четыре байта, то коэффициент умножения будет равен четырем;
- допускается также вычитание указателей. Например, если p и q ссылаются на элементы одного массива и р<q, то q-p+1 есть число элементов от р до q включительно;
- арифметика с указателями учитывает тип объекта: если она имеет дело со значениями float, занимающими больше памяти, чем char, и р-указатель на float, то р+ + продвинет р на следующее значение float;
Стринговая константа, написанная в виде "I am a string" есть массив литер. Во внутреннем представлении этот массив заканчивается "пустой" литерой `\0', по которой программа может найти конец стринга.
Доступ к стринговой константе осуществляется через указатель на ее первый элемент.
Обычно стринговые константы используются в качестве аргумента функций, например в printf. Рассмотрим следующее описание
char *pmessage;
Присваивание pmessage="now is the time"; поместим в эту переменную ссылку на литерный массив, при этом сам стринг не копируется, а копируется лишь указатель на него.
Операции для работы со стрингом как единым целым в Си не предусмотрены.
Организовать литерный массив можно двумя способами, как массив последовательности литер с `\0', и с помощью указателя, инициализированного ссылкой на стринговую константу, т.е.
char armessage [ ] = "now is the time";
char *pmessage = "now is the time";
соответственно.
Между этими двумя определениями существует важное различие в силу того, что указатель это переменная, а имя массива - константный указатель.
Основная литература - 1[94-106], 3[65-66]., 2[269-280].
Контрольные вопросы
1.Каков приоритет унарных операторов * и & и порядок их выполнения в выражении?
2. Пусть имеем следующие декларации
int k=2;
int *ip;
Как можно изменить содержимое переменной k на 5 с помощью указателя ip?
3. Есть ли разница в записях (*ip)+ + и *ip+ +?
4. Если *ip+=1увеличивает на единицу то, на что ссылается ip, то какие действия выполняют операторы:
+ +*ip и (*ip)+ +?
5. Если имеем два указателя на int: ip и iq, то к чему приведет оператор присваивания
iq=ip;
6. Что значит передать данные в функцию " по значению"?
7. Изобразите графически механизм доступа к исходным данным, если параметры функции описаны как указатели.
Лекция 9. Использование сложных типов в языке СИ. Описание массивов. Программирование ввода-вывода одномерных массивов. Строки. Обработка строк.
Как и и любые другие переменные, указатели можно группировать в массивы.
В информационных системах широко используются алгоритмы поиска и сортировки данных различной природы и, в частности, текстовых строк произвольной длины. Эффективность алгоритмов поиска и сортировки во многом связана с выбором представления данных. Одним из таких удобных и эффективных представлений является массив указателей на начала текстовых строк произвольной длины.
Поскольку строки в памяти расположены вплотную друг к другу, к каждой отдельной строке доступ просто осуществлять через указатель на ее первую литеру.
Сами же указатели можно организовывать в виде массива. Одна из возможностей сравнить две строки - передать указатели на них функции strcmp. Чтобы поменять местами строки, достаточно будет поменять местами в массиве их указатели ( а не сами строки) .
При этом снимаются сразу две проблемы: одна- связанная со сложностью управления памятью а вторая - с большими накладными расходами при перестановках самих строк.
Процесс сортировки распадается на три этапа:
- чтение всех строк из ввода;
- сортировка введенных строк;
- печать их по порядку.
Программа ввода должна прочитать и запомнить литеры всех строк, а также построить массив указателей на строки. Эта функция также должна подсчитать число введенных строк - эта информация понадобится для сортировки и печати.
Программа вывода занимается только тем, что печатает строки, причем в том порядке, в котором в массиве указателей на них расположены ссылки.
В качестве алгоритма сортировки можно предложить быструю сортировку, предложенную К.А. Хоором в 1962 году.
В Си имеется возможность задавать прямоугольные многомерные массивы и в частности двумерные. Особенность двумерного массива в Си заключается лишь в форме записи, в остальном его можно трактовать почти также, как в других языках. Элементы запоминаются строками, следовательно, при переборе их в том поряке, как они расположены в памяти, чаще будет изменяться самый правый индекс.
Массив инициализируется списком начальных значений, заключенным в фигурные скобки; каждая строка двумерного массива инициализируется соответствующим подсписком.
Если двумерный массив передается функции в качестве аргумента, то декларация соответствующего ему параметра должна содержать количество столбцов; количество строк в данном случае несущественно, поскольку, как и прежде, функции будет передана ссылка на массив строк.
Представим декларацию двумерного массива
int ar[5] [10];
Если массив ar передается некоторой функции f , то эту функцию можно было бы определить следующим образом:
f(int array[5][10]) {…};
Вместо этого можно записать:
f( int array [][10]) {…}
поскольку число строк здесь не имеет значения, или
f( int (*array )[10]) {…}
последняя запись декларирует, что параметр есть указатель на массив из 10 значений типа int . Скобки здесь необходимы, так как квадратные скобки [] имеют более высокий приоритет, чем *. Без скобок (круглых) декларация
int * array[10]
определяет массив из 10 указателей на int. В более общем случае только первое измерение (соответствующее первому индексу) можно не задавать, все другие специфицировать необходимо.
Основная литература - 1[107-111], 2[346-370].
Контрольные вопросы:
1. Каким образом выбор структуры данных для описания понятий предметной области (решаемой задачи) связан с эффективностью алгоритма их обработки. Покажите эту связь на примерах лабораторных работ.
2. Какие проблемы решаются при сортировке текстовых строк с использование указателей?
3. О чем говорит следующая декларация char * lineptr[MAXLINES]?
4. Используя декларацию п.3, что собою представляет следующее выражение *lineptr [i]?
5. Почему при передачи функции двумерного массива количество строк массива может не указываться в параметре функции?
Лекция 10. Инициализация массивов указателей. Указатели вместо многомерных массивов
Покажем механизм инициализации массивов указателей на примере на функции month_name(n), которая возвращает ссылку на стринг литер, содержащий название n-го месяца. Эта функция идеальна для демонстрации использования статического массива. Функция имеет в своем распоряжении массив стрингов, на один из которых она и возвращает ссылку.
month_name: возвращаем имя n-го месяца
char * month _ name (int n)
{ static char * name [] {"Неверный месяц",
"Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"};
return (n<1¦¦ n>12)? name [0]: name [n];
Инициализатором служит список стрингов каждому из которых соответствует определенное место в массиве. Литеры і-го стринга где-то размещены в памяти и указатель на них запоминается в name [і]. Так как размер массива name не специфицирован, компилятор вычислит его по количеству заданных начальных значений (инициализирующих выражений).
В чем разница между двумерным массивом и массивом указателей? Для следующих определений:
int ar [10][20];
int*x[10];
Записи ar[3] [4] и x[3] [4] будет синтаксически правильными ссылками на некоторое значение типа int. Однако только ar является истинно двумерным массивом: для двух сом элементов типа int будет выделена память, а вычисление смещения элемента ar [строка, столбец] от начало массива будет вестись по формуле 20 * строка + столбец, учитывающей его прямоугольную природу.
Для массива x определяются только 10 указателей, причем без инициализации. Инициализация должна задаваться явно - либо статически, либо в процессе счета.
Если каждый элемент x ссылается на пятиэлементный массив, то в результате в памяти будет выделено пространство для размещения 50 значений типа int, и еще 10 ячеек для указателей.
Важное преимущество указателей в том, что строки такого массива могут иметь разные длины.
Подобные документы
Проектирование программного обеспечения для классифицирования выпускников высшего учебного заведения. Выбор системы управления базами данных и языка программирования. Разработка структуры данных, схема базы данных. Реализация программного комплекса.
дипломная работа [2,4 M], добавлен 27.03.2013Цели и задачи дисциплины "Технология программирования". Программные средства ПК. Состав системы программирования и элементы языка. Введение в систему программирования и операторы языка Си. Организация работы с файлами. Особенности программирования на С++.
методичка [126,3 K], добавлен 07.12.2011Проблема надежности программного обеспечения, ее показатели и факторы обеспечения. Методы контроля процесса разработки программ и документации, предупреждение ошибок. Этапы процесса отладки ПО, приемы структурного программирования и принцип модульности.
презентация [379,5 K], добавлен 30.04.2014Постановка задачи автоматизации учебного процесса колледжа и описание предметной области. Работа с базами данных в Delphi: способы, компоненты доступа к данным и работы с ними. Язык запросов SQL. База данных в Microsoft Access и результаты исследований.
дипломная работа [55,6 K], добавлен 16.07.2008Изучение общей структуры языка программирования Delphi: главные и дополнительные составные части среды программирования. Синтаксис и семантика языка программирования Delphi: алфавит языка, элементарные конструкции, переменные, константы и операторы.
курсовая работа [738,1 K], добавлен 17.05.2010Использование средств вычислительной техники в информационных системах. Программно-аппаратные средства, обеспечивающие сбор, обработку и выдачу информации. Модели данных - списки (таблицы), реляционные базы данных, иерархические и сетевые структуры.
реферат [105,1 K], добавлен 08.11.2010Диагностический анализ системы управления предприятия, его организационной и функциональной структуры. Разработка проекта подсистемы учёта средств вычислительной техники, описание технического обеспечения базы данных. Характеристика программного продукта.
дипломная работа [7,2 M], добавлен 28.06.2011История развития языков программирования; создание и распространение языка С++; новый подход к разработке объектно-ориентированного программного обеспечения. Применение моделирования предметных областей для структуризации их информационных отражений.
реферат [29,1 K], добавлен 06.12.2010Анализ методов и средств контроля доступа к файлам. Проблемы безопасности работы с файлами, средства контроля доступа ним. Идеология построения интерфейса, требования к архитектуре. Работа классов системы. Оценка себестоимости программного продукта.
дипломная работа [2,5 M], добавлен 21.12.2012Сущность языка программирования, идентификатора, структуры данных. Хранение информации, алгоритмы их обработки и особенности запоминающих устройств. Классификация структур данных и алгоритмов. Операции над структурами данных и технология программирования.
контрольная работа [19,6 K], добавлен 11.12.2011