Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Основы программирования на языке С. Учебное пособие

.pdf
Скачиваний:
1
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
{return n<m?m:n;}
int cube(int); //прототип функции вычисления куба целого
числа
void main (void) { int i,j,m; scanf("%d%d", &i, &j); m=cube(max(i,j));// вложенные вызовы функций – m – куб // максимального из двух чисел printf("\nmax=%d",m); getch(); }
int cube(int x)// Определение функции вычисления //куба целого числа {return x*x*x;}
Пример 2. Определить функцию, подсчитывающую количество ну­лей в троичной записи натурального числа. Найти все пары натуральных чисел в диапазоне [n1,n2], разность между которыми равна 4 и в троичной записи которых равное число нулей. Например, такой парой является пара
6(20
) и 10(1013) (в троичной записи по одному нулю).
3
# include <stdio.h>
# include <conio.h>
int oct (int a)// определение функции
{int sum=0;
while (a>0)
{
if (a%3==0) sum++;
a/=3;
}
return sum;
}
void main()
{
int n1,n2;
printf("Enter n1,n2 ");
scanf("%d%d",&n1,&n2);
for (int i=n1;i<=n2-4;i++)// Проверяем все пары чисел,
//разность между которыми равна 4
if (oct(i)==oct(i+4) && oct(i)>0)// условие, содержащие
//три вызова функции oct
printf("%d%s%d%s", i," ",i+4,"\n");
getch();
}
51
Особое место в ряду функций занимают так называемые рекурсив­ные функции, причем разделают прямую и косвенную рекурсию. Функция называется косвенно рекурсивной в том случае, если она содержит обра­щение к другой функции, содержащий вызов данной функции. Если же в теле функции имеется вызов самой этой функции, то речь идет о прямой рекурсии, а такую функцию называют рекурсивной. Рекурсивные алго­ритмы эффективны в тех задачах, где рекурсия используется в определе­нии данных. Если у задачи есть очевидное итерационное решение, то ре­курсии следует избегать. Поэтому серьезное изучение рекурсивных мето­дов нужно проводить, вводя динамические структуры данных. Сейчас же рассмотрим только принципиальные возможности
С для организации ре-
курсивных алгоритмов.
Пример 3. Напишите рекурсивную функцию, вычисляющую x
n
(n–целое, неотрицательное).
float step(float x, int n)
{
if (n==0) return 1;
return x*step(x,n-1); //рекурсивный вызов
}
void main()
{
float x;
int n;
printf("Enter x,n ");
scanf("%f%d",&x,&n);
printf("\nStepen(%f%s%d%s%f",x,",",n,")= ",step(x,n));
getch();
}
Для n=0 функция возвращает значение 1. В противном случае вызы­вается та же функция с уменьшенным на 1 значением параметра степени. Таким образом, организуется последовательность вычислений x
n–1
x
=x*x
n–2
…, x2=x*x1, x1=x*x0, x0=1. Обратите внимание, что последова-
n
=x*x
n–1
,
тельность рекурсивных обращений к функции прерывается только при вызове функции step(x,0). Таким образом, важным моментом при состав­лении любой рекурсивной функции является организация выхода их ре­курсии. Каждая задача, решаемая рекурсивным образом, при некоторых наборах данных должна иметь элементарное нерекурсивное решение, например, в нашем примере x
0
=1.
Больше значение в современном программировании играет не только умение создавать собственные функции, но и грамотное использование уже созданных библиотечных функций. Использование любой стандарт­ной библиотечной функции предполагает ее тщательное предварительное изучение с помощью справочника по языку программирования или спра­вочной системы интегрированной среды программирования. В качестве
52
примера рассмотрим некоторые из библиотечных функций языка С для ра­боты со стоками и файлами. Функции для работы со строками описаны в заголовочном файле string.h. Как было отмечено в главе 6, строка в языке С задается как массив символов и строковые функции работают именно с символьными массивами, завершающимися символом ‘\0’, причем вся ответственность за
переполнение массивов ложится на плечи программи­ста. Так как имя массива является указателем-константой на его первый элемент, то параметрами строковых функций чаще всего являются именно указатели-константы на объект типа char (const char *). Рассмотрим описа­ние некоторых функций библиотеки string.h
1.
# include <string.h> unsigned int strlen(char * str)
Функция strlen() возвращает длину строки str. Завершающий нулевой
символ не учитывается.
Пример использования функции:
char s[255]; gets(s); printf("Длина строки равна %d",strlen(s));
2.
#include <string.h> int strcmp(const char *str1, const char* str2);
Функция strcmp() сравнивает в лексикографическом порядке две строки и возвращает целое значение, зависящее от результата сравнения следующим образом: значение меньше нуля, если str1<str2; ноль, если str1=str2; больше нуля, если str1>str2. Приведем пример использования этой функции – фрагмент программы, проверяющий упорядочены ли два слова по алфавиту:
char s1[20], s2[20];
scanf("%s%s",s1,s2);
int k=strcmp(s1,s2);
if (k<0) printf("Слова упорядочены по алфавиту");
else if (k==0) printf("Слова одинаковы");
else printf("Слова не упорядочены по алфавиту");
3.
#include <string.h>
char * strcpy (char *str1, const char* str2);
Функция strcpy() копирует содержимое строки str2 в строку str1 и возвращает значение указателя str1. Если заданные символьные массивы перекрываются, поведение функции не определено. Приведем пример ис­пользования этой функции – ввести в клавиатуры слово и скопировать его в новую строку.
char str1[20],str2[20];
scanf("%s%s",str1, str2);
strcpy(str2,str1);
printf("%s%s%s", str1,"\n",str2);
53
Пример. Вывести слова введенной строки, начинающиеся с гласной буквы, на экран столбиком без разделительных знаков (пробел, табуляция, точка, запятая и т.п.).
Для выделения слов из строки будем использовать библиотечную функцию
char * strtok(char *str1, const char *str2),
которая ищет в строке str1 лексемы, выделенные символами из строки str2, и возвращает указатель на выделенную лексему, причем при отсутствии лексемы возвращается NULL (нулевой указатель, то есть указатель, отличный от указателя на любой объект). Чтобы разделить некоторую строку на лексемы, при первом вызове функции strtok() па­раметр str1 должен указывать на начало этой строки. При последующих вызовах функции в качестве параметра str1 нужно использовать нулевой указатель.
Для проверки принадлежности первой буквы слова к гласным бук­вам будем использовать функцию
char * strchr (const char * str, int ch),
которая возвращает указатель на первое вхождение младшего байта параметра ch в строке str. Если совпадение не обнаружено, возвращается нулевой указатель.
# include <stdio.h>
# include <conio.h>
# include <string.h>
# define dlin 80 // директива позволяет в программе вместо
//числа 80 использовать идентификатор dlin
void main()
{
printf("Enter string: ");
char stroka[dlin];
gets(stroka);
char *slova[dlin/2]; //массив указателей для хранения
//слов строки
char *razd=".,:;!? ";// указатель на строку,
//состоящую из разделительный символов
char *glas="aeoiuy"; // указатель на строку,
//состоящую из гласных
int k=0; //определили индекс для массива slova
slova[k]=strtok(stroka,razd); // выделили первое слово
while (slova[k])// пока выделенная из строки
//лексема не равна NULL
{
if (strchr(glas,*slova[k])!=NULL)
printf("%s%s",slova[k],"\n");//проверили, является ли первая
// буква слова s1[k] гласной
54
slova[++k]=strtok(NULL,razd); // выделили очередное //слово. }
getch(); }
В данном решении задачи для хранения слов мы использовали мас­сив, взяв за его размерность максимально возможную – размерность ис­ходной строки, деленную пополам, то есть предполагая, что все слова строки состоят из одной буквы и разделены одним разделителем. Однако в данной задаче не предполагается дальнейшая обработка выделенных из строки слов,
поэтому нет необходимости сохранять их в массиве. Поэтому целесообразнее определять массив указателей char * s1[dlin/2], а опреде­лить лишь один указатель char *s1, который и использовать для обработки очередного выделенного слова.
Функции для работы с файлами описаны в заголовочном файле stdio.h. В языке не предусмотрены никакие предопределенные структуры фалов: все файлы рассматриваются как последовательности, потоки бай­тов. Для файла определен маркер (указатель чтения/записи), который определят текущую позицию, к которой осуществляется доступ. В про­грамме возможно открывать потоки ввода-вывода и связывать их либо с файлами на диске либо с физическими устройствами (например, принте­ром), записывать в них или считывать из них информацию. Доступ к пото­ку
осуществляется с помощью типа FILE, определенного в файле stdio.h
следующим образом:
FILE * идентификатор;
Описанный указатель можно связывать с конкретным файлом в мо­мент открытия данного файл. Это осуществляется с помощью функции
FILE * fopen(const char *fname, const char * mode);
Функция fopen() открывает файл, имя которого задает параметр fname и возвращает поток, связанный с этим файлом. Строка fname
должна представлять собой имя файла, которое разрешено определен­ными в данной операционной системе правилами. Если указанный файл не удается открыть, функция возвращает нулевой указатель. Типы опе­раций, которые разрешено выполнять с файлом, определяются парамет­ром mode.
Существует таблица значений параметра mode. Наиболее ча-
сто используемые значения:
“r” – открывает текстовый файл для чтения;
“w” – создает текстовый файл для записи;
“a” – открывает текстовый файл для записи в конец файла;
“r+” – открывает текстовый фал для чтения и записи;
“w+” – создает текстовый файл для чтения и записи;
“a+” – открывает текстовый файл для чтения и записи
в конец файла.
55
Приведем фрагмент программы, иллюстрирующий корректный спо-
соб открытия файла.
FILE *fp=fopen("1.txt","r"); if (fp==NULL) {printf("Не удается открыть файл ");
exit(1);}
else {….}
В данном примере происходит попытка открыть файл с именем “1.txt” в текущем каталоге для чтения. Если такого файла не существует, на экран выдается соответствующее сообщение и с помощью функции exit(), описанной в заголовочном файле stdlib.h, происходит завершение
программы. При открытии файла для записи с параметром “w” также ре­комендуется делать проверку, так как диск
может быть защищен от записи
или заполнен.
По окончании работы с файлом он должен быть закрыт с помощью функции
int fclose (FILE * stream);
После обращения к функции fclose() указатель stream больше не свя­зан с конкретным файлом. При успешном выполнении функции возвраща­ется ноль, в противном случае возвращается значение EOF. Попытка за­крыть уже закрытый файл расценивается как ошибка.
Опишем назначения основных функций ввода и ввода данных.
Функция
int fgetc(FILE * stream); считывает из входного по-
тока streаm следующий символ, соответствующий текущей позиции, при достижении конца файла возвращает значение EOF.
Функция
char * fgets (char *str, int num, FILE * stream);
считывает из входного потока stream не более num–1 и помещает их в мас­сив, адресуемый указателем str. Символы читаются, пока не будет прочи­тан символ новой строки или конец файла, либо пока не будет достигнут заданный предел. По завершении чтения символов сразу за последним прочитанным символом размещается нулевой символ. Символ новой стро­ки сохраняется
и остается частью строки. При успешном выполнении
функции возвращается значение str, при неуспешном – нулевой указатель.
Функция
int fscanf (FILE * stream, const char * format, …);
читает информацию из потока stream. Управляющая строка, задаваемая параметром format, состоит из символов трех категорий: спецификаторы формата, пробельные символы и символы, отличные от пробельных. Спе­цификаторы формата – им предшествует знак процента – сообщают, како­го типа данные будут прочитаны. Например, спецификатор %d прочитает целое значение, а спецификатор % с прочитает символ. Пробельные сим­волы заставляют функцию
пропустить один или несколько пробельных символов во входном потоке. Не пробельный символ заставляет функцию прочитать и отбросить соответствующий символ из входного потока. Сим­вол *, стоящий после знака % и перед кодом формата прочитает данные
56
заданного типа, но запретит их присваивание. В качестве неопределенных параметров функции могут использовать параметры зависимости от управляющей строки. Например, функция
fcsanf (fp, “%d%d”, &a, &b);
прочитает очередные два целых числа из файлового потока fp в пе-
ременные a, b, а функция
fcsanf (fp, “%d%*d”, &a);
считает два целый числа, но в переменной а сохранит только первое из них. Функция возвращает количество аргументов, которым действи­тельно присвоены значения.
Функция
int fputc(int ch, FILE * stream); записывает символ
ch в заданный поток stream в текущую позицию файла, а затем передвигает
индикатор позиции файла. Возвращает значение записанного символа, а в случае ошибки значение EOF.
Функция
int fputs(const char *str, FILE* stream); записывает
в заданный поток stream содержимое строки, адресуемой указателем str. При этом завершающий нулевой символ не записывается. При успешном выполне­нии возвращается неотрицательное значение, при неудачном – значение EOF.
Функция
…); записывает в файловый поток stream значения параметров из заданно-
int fprintf (FILE * stream, const char *format,
го списка параметров в соответствии со строкой форматирования format. Например, функция fprintf(fp, “%d”, a) запишет значение целой перемен­ной a в файловый поток fp. Функция возвращает количество реально выве­денных символов, а при возникновении ошибки– отрицательное значение.
Для проверки, достигнут ли конец файла используют функцию
int feof(FILE *stream);
Если индикатор позиции файла, связанного с потоком stream, распо­ложен в конце файла возвращается ненулевое значение, в противном слу­чае – нуль.
Для изменения индикатора позиции файла используют функцию
int fseek ( FILE* stream, long offset, int origin);
Функция устанавливает индикатор позиции файла, связанного с по­током stream, в соответствии со значением смещения offset (количество байтов) и исходного положения origin. Параметр origin может принимать одно их следующих значений: 0 – начало файла, 1 – текущая позиция, 2 – конец файла. При успешном выполнении функция возвращает нулевое значение, при возникновении сбоя – ненулевое. Функция очищает признак конца файла. Вообще говоря,
функцию fseek() следует использовать только при работе с двоичными файлами. При использовании же с текстовыми файлами параметр origin должен иметь значение 0, а параметр offset – зна­чение, полученное в результате вызова функции
long ftell (FILE* stream);
которая возвращает текущее значение индикатора позиции файла.
57
Приведем фрагмент программы, считывающей из файлового потока
fp каждый третий символ
char a; while ( fseek(fp,2,1)==0 && (a=fgetc(fp))!=EOF) printf("%c%c",a,' ');
Если необходимо считать каждое второе число из файла (предпола-
гается, что числа разделены пробелами) программа примет следующий
int a; while(fseek(fp,ftell(fp),0)==0&&fscanf(fp,"%d",&a)!=EOF &&
fscanf(fp, "%d", &a)!=EOF)
printf("%d%c",a,' ');
вид:
Пример. Дан текстовый файл, строки которого содержат не более m (m<=20) символов. Считать строки и записать их в файл result.txt в порядке считывания, причем каждая строка должна быть «перевернута».
#include <stdio.h>
void main ()
{
char filename[255];
printf("Введите имя файла: ");
scanf("%s",filename);
FILE *fp=fopen(filename,"r"); //открываем файл для чтения
if (fp!=NULL) // Если файл успешно открыт
{
FILE * fp1=fopen("result.txt","w");//Открываем для файл
// для записи
char s[20];// определяем массив для хранения
//считанной строки
while(fgets(s,21,fp)) //Цикл считывания строк
{
int l=strlen(s);
if (s[l-1]!='\n') fputc(s[l-1],fp1);
/*цикл записи символов строки в обратном порядке*/
for (int j=l-2; j>=0; j--) fputc(s[j],fp1); //
fputc('\n',fp1);//запись символа новой строки
}
fclose(fp);
fclose (fp1);
printf("Файл записан");
}
else printf("Не существует такого файла!");
getch();
}
Заметим, что запись считанной строки s в файл происходит, начиная с символа с индексом strlen(s)–2, так как последний символ строки это ноль–символ, а предпоследний – символ новой строки, который записыва­ется в строку при ее чтении из файла функций fgets(). Однако исключение
58
может составлять последняя строка файла, после которой в исходном фай­ле может не стоять символа новой строки. Именно для последней строки в цикле введена проверка условия.
Каждому студенту рекомендуется выполнить хотя бы одно из
упражнений 1-12 заданий 16-19.
Задание 16. Определение и вызов функций
1. Определить функцию, проверяющую, является ли целое число со­вершенным. Совершенное число равно сумме всех своих делителей, вклю­чая единицу и не включая себя. Например, 6=1+2+3 – совершенное число,
81+2+2+2 – несовершенное. Выяснить, сколько совершенных чисел нахо­дится в диапазоне [n..m] (n<m), вывести их на экран.
2. Определить функции, переводящую число в двоичную систему счисления и проверяющую, является ли двоичная запись числа симмет­ричной последовательностью
нулей и единиц, начинающейся единицей. Напечатать все числа, не превосходящие n, двоичная запись которых есть симметричная последовательность.
3. Определить функцию, посчитывающую количество инверсий в последовательности цифр натурального числа, то есть количество таких пар соседних цифр, в которых большая находится слева от меньшей. Из массива целых чисел, генерируемом случайным образом, вывести на
экран сначала все числа, в записи которых нет инверсий, затем числа, в записи которых 1 инверсия и т.д. до чисел, имеющих максимальное число инвер­сий в данном массиве. Например, элементы массива {3564, 123, 24, 87, 981,
9871, 54} должны быть выведены следующим образом: 0 инверсий: 123, 24; 1 инверсия: 3564, 87,54; 2 инверсии: 981; 3 инверсии: 9871.
4. Определить функцию, проверяющую, является ли заданная дробь
несократимой. (Дробь
задается двумя натуральными числами – числителем
и знаменателем). Найти все несократимые дроби, заключенные между 0 и 1, знаменатели которых не превышают заданное число n.
5. Определить функцию, проверяющую является ли число простым. Распечатать четверки простых чисел, не превосходящих n, принадлежащих одному десятку. Например, для числа 112 надо напечатать четверки 2 3 5
7; 11 13 17 19 ; 101 103 107 109.
6. Определить функцию, возвращающую сумму простых
делителей натурального числа. Найти все пары “дружественных” чисел в диапазоне [n1,n2]. Два натуральных числа называются “дружественными”, если одно из них равно сумме простых делителей другого.
7. Определить функцию, проверяющую, является ли данное число
простым. Составить программу для проверки гипотезы Гольдбаха о том, что каждое четное число, большее 2, можно представить
суммой двух про-
59
стых чисел. (Для любого четного k, не превосходящего n, выдать либо пару простых слагаемых, либо сообщение, что такого разложения нет).
8. Определить функцию, проверяющую, является ли данное число простым, и функцию, вычисляющую количество нулей в двоичной записи натурального числа. (Считается, что первая цифра двоичного числа всегда
1). Среди простых чисел, не превосходящих
n, найти первое такое, в дво-
ичной записи которого максимальное количество нулей.
9. Определить функцию, проверяющую, является ли данное число
простым, и функцию, подсчитывающую количество единиц в двоичной записи натурального числа. Найти все пары простых чисел, не превосхо­дящих n, сумма единиц в двоичной записи которых совпадает. Например, такой парой является
пара 3 (11) и 5 (101).
10. Определить функцию для нахождения наименьшего общего
кратного (НОК) и наибольшего общего делителя (НОД) двух натуральных чисел. Определить НОК и НОД для n введенных натуральных чисел.
11. Определить функцию, проверяющую, является ли данное число простым, и функцию, вычисляющую количество единиц в двоичной запи­си натурального числа. (Считается, что первая
цифра двоичного числа все­гда 1). Среди простых чисел, не превосходящих n, найти такие, в двоичной записи которых количество единиц не превосходит m.
12. Определить функцию, которая преобразует строку цифр (от дво­ичных до шестнадцатеричных) в эквивалентное десятичное число, и функ­цию, которая преобразует целое десятичное число в эквивалентную строку цифр (от
двоичных до шестнадцатеричных). Вычислить сумму двух вве­денных двоичных строк и вывести результат сложения на экран в десятич­ном и двоичном виде.
Задание 17. Рекурсивные функции
1. Записать алгоритм Евклида вычисления наибольшего общего де­лителя (НОД) как рекурсивную функцию. Алгоритм основан на том факте, что если a=qb+r, где 0<r<b
, то HOD(a,b)=HOD(b,r). В процессе вычисле­ний выводить на экран текущие выражение a=qb+r в численном виде, то есть 37=2*17+3, 17=5*3+2 и т.д. В конце вывести НОD.
2. Записать алгоритм, проверяющий является ли заданное число про-
стым как рекурсивную функцию. Вывести на экран все простые числа,
не
превосходящие n.
3. Записать алгоритм разложения произвольного числа на простые
множители как рекурсивную функцию с выводом на экран сомножителей.
4. Записать алгоритм нахождения канторового разложения произ-
вольного числа а – вектора
21 n
в виде рекурсивной функции. Канто-
),...,,(
aaa
ровым разложением положительного числа а называется запись вида:
,
!...!2!1
naaaa
21
n
i
.
nina
,1,0
60
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]