Студопедия.Орг Главная | Случайная страница | Контакты | Мы поможем в написании вашей работы!  
 

Масиви структур



Як і звичайними масивами простих типів, так само можна оперувати масивами структур, елементи якого мають структурований тип. Розглянемо наочний зразок, який ілюструє оголошення масиву структур:

typedef struct Date

{

int d; /* день */

int m; /* мiсяць */

int y; /* рiк */

} Date;

Date arr[100];

Вище було оголошено масив arr, що складається із 100 елементів, кожний з яких має тип Data. Кожний елемент масиву - це окрема змінна типу Data, що складається із трьох цілих елементів - d, m, y.

Доступ до полів структури аналогічний доступу до звичайних змінних, плюс використання індексу номеру елементу у квадратних дужках:

arr[25].d=24;

arr[12].m=12;

Запропонуємо програму, в якій реалізується концепція структурованого типу Data. Окремими функціями реалізуємо ініціалізацію елементів структури, додавання нового значення, виведення дати на екран, визначення високосного року.

#include<stdio.h>

#include<conio.h>

typedef struct Date

{

int d; /* день */

int m; /* мiсяць */

int y; /* рiк */

} Date;

void set_date_arr(Date *arr,Date value,int n)

{

int i;

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

{

arr[i].d=value.d;

arr[i].m=value.m;

arr[i].y=value.y;

}

}

void print_date_arr(Date *arr,int n)

{

int i;

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

{

printf("%d.%d.%d\n",arr[i].d,arr[i].m,arr[i].y);

}

}

void print_date(Date &d)

/* виведення на екран дати */

{

printf("%d.%d.%d\n",d.d,d.m,d.y);

}

void init_date(Date &d,int dd,int mm,int yy)

/* iнiцiалiзацiя структури типу Date */

{

d.d=dd;

d.m=mm;

d.y=yy;

}

int leapyear(int yy)

/* визначення, чи високосний рiк */

{

if ((yy%4==0&&yy%100!=0)||(yy%400==0)) return 1;

else return 0;

}

void add_year(Date &d,int yy)

/* додати yy рокiв до дати */

{

d.y+=yy;

}

void add_month(Date &d,int mm)

/* додати mm мiсяцiв до дати */

{

d.m+=mm;

if (d.m>12)

{

d.y+=d.m/12;

d.m=d.m%12;

}

}

void add_day(Date &d,int dd)

/* додати dd днiв до дати */

{

int days[]={31,28,31,30,31,30,31,31,30,31,30,31};

d.d+=dd;

if (leapyear(d.y)) days[1]=29;

while ((d.d>days[d.m-1]))

{

if (leapyear(d.y)) days[1]=29;

else days[1]=28;

d.d-=days[d.m-1];

d.m++;

if (d.m>12)

{

d.y+=d.m%12;

d.m=d.m/12;

}

}

}

void main(void)

{

Date date1,date2;

Date array[10]={{12,11,1980},{15,1,1982},{8,6,1985},{8,8,1993},{20,12,2002},{10,1,2003}};

clrscr();

init_date(date1,15,12,2002);

add_day(date1,16);

print_date(date1);

puts("");

init_date(date2,1,1,2003);

add_month(date2,10);

print_date(date2);

puts("");

print_date_arr(array,6);

}

1.13.3 Бітові поля

Бітові поля (bit fields) - особливий вид полів структури. Вони дають можливість задавати кількість бітів, в яких зберігаються елементи цілих типів. Бітові поля дозволяють раціонально використовувати пам'ять за допомогою зберігання даних в мінімально потрібній кількості бітів.

При оголошенні бітового поля вслід за типом елемента ставиться двокрапка (:) і вказується цілочисельна константа, яка задає розмір поля (кількість бітів). Розмір поля повинен бути константою в діапазоні між 0 і заданим загальним числом бітів, яке використовується для зберігання даного типу даних.

struct bit_field {

int bit_1: 1;

int bits_2_to_5: 4;

int bit_6: 1;

int bits_7_to_16: 10;

} bit_var;

1.14 Об'єднання (union)

Об'єднання дозволяють в різні моменти часу зберігати в одному об'єкті значення різного типу. В процесі оголошення об'єднання з ним асоціюється набір типів, які можуть зберігатися в даному об'єднанні. В кожний момент часу об'єднання може зберігати значення тільки одного типу з набору. Контроль за тим, значення якого типу зберігається в даний момент в об'єднанні покладається на програміста.

Синтаксис:

union [ім'я_об'єднання]

{

тип1 елемент1;

тип2 елемент2;

........................

типN елементN;

} [список описів];

Пам'ять, яка виділяється під змінну типу об'єднання, визначається розміром найбільш довгого з елементів об'єднання. Всі елементи об'єднання розміщуються в одній і тій же області пам'яті з однієї й тієї ж адреси. Значення поточного елемента об'єднання втрачається, коли іншому елементу об'єднання присвоюється значення.

Приклад 1:

union sign

{

int svar;

unsigned uvar;

} number;

Приклад 2:

union

{

char *a,b;

float f[20];

} var;

В першому прикладі оголошується змінна типу об'єднання з ім'ям number. Список оголошень елементів об'єднання містить дві змінні: svar типу int і uvar типу unsigned. Це об'єднання дозволяє запам'ятати ціле значення в знаковому або в без знаковому вигляді. Тип об'єднання має ім'я sign.

В другому прикладі оголошується змінна типу об'єднання з ім'ям var. Список оголошень елементів містить три оголошення: покажчика a на значення типу char, змінної b типу char і масиву f з 20 елементів типу float. Тип об'єднання не має імені. Пам'ять, що виділяється під змінну var, рівна пам'яті, необхідної для зберігання масиву f, так як це найдовший елемент об'єднання.

1.15 Файлові потоки

В мові Сі та Сі++ файл розглядається як потік (stream), що представляє собою послідовність байтів, що записуються чи зчитуються. При цьому потік "не знає", що і в якій послідовності в нього записано. Розшифровка змісту написаних у ньому байтів лежить на програмі.

Таблиця 1.13. Значення аргументу mode функції fopen()

"r" відкриття файлу без дозволу на модифікацію, файл відкривається лише для читання.
"w" створення нового файлу тільки для запису, якщо файл із вказаним ім'ям вже існує, то він перезапишеться.
"a" відкриття файлу тільки для додавання інформації в кінець файлу, якщо файл не існує, він створюється.
"r+" відкриття існуючого файлу для читання та запису.
"w+" створення нового файлу для читання та запису, якщо файл із вказаним ім'ям вже існує, то він перезаписується.
"a+" відкриває файл у режимі читання та запису для додавання нової інформації у кінець файлу; якщо файл не існує, він створюється.

Класичний підхід, прийнятий в Сі, полягає в тому, що інформація про потік заноситься в структуру FILE, яка визначена у файлі stdio.h. Файл відкривається за допомогою функції fopen, яка повертає покажчик на структуру типу FILE.

typedef struct

{

short level; /*рівень буферу*/

unsigned flags; /*статус файлу */

char fd; /*дескриптор файла*/

char hold; /*попередній символ,якщо немає буферу*/

short bsize; /*розмір буферу*/

unsigned char *buffer; /*буфер передавання даних*/

unsigned char *curp; /*поточний активний покажчик*/

short token; /*перевірка коректності*/

} FILE;

Синтаксис функції fopen ():

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

Дана функція відкриває файл із заданим ім'ям і зв'язує з ним потік. Аргумент mode вказує режим відкриття файла (таблиця 1.13).

До вказаних специфікаторів в кінці або перед символом "+" може додаватися символ "t" (текстовий файл), або "b" (бінарний, двійковий файл).

1.15.1 Текстові файли

Розглянемо спочатку роботу з текстовими файлами. Відкриття текстового файлу test.txt може мати вигляд:

#include<stdio.h>

void main()

{

FILE *f;

if ((f=fopen("test.txt", "rt"))==NULL)

{

printf("Файл не вдалося відкрити.\n");

return;

}

fclose(f);

}

В даному прикладі змінна f зв'язується з файлом "test.txt", який відкривається як текстовий тільки для читання.

З відкритого таким чином файлу можна читати інформацію. Після закінчення роботи з файлом, його необхідно закрити за допомогою функції fclose ().

Якщо файл відкривався би за допомогою fopen ("test.txt", "rt+");, то можна було б не тільки читати, але й записувати в нього інформацію.

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

Записування символу в файловий потік здійснюється функцією putc ().

int putc(int ch, FILE *f);

Читання рядка здійснюється за допомогою функції fgets ().

char *fgets(char *s,int n,FILE *stream);

У виклику функції fgets (): s - покажчик на буфер, в який читається рядок, n - кількість символів. Читання символу в рядок проходить або до появи символу кінця рядка "\n", або читається n-1 символ. В кінці прочитаного рядка записується нульовий символ.

#include<stdio.h>

#include<string.h>

void main()

{

char s[80];

FILE *f;

if ((f=fopen("1.cpp", "rt"))==NULL)

{

printf("There are an error\n");

return;

}

do

{

fgets(s,80,f);

printf("%s",s);

} while (!feof(f));

fclose(f);

}

Функція feof () перевіряє, чи не прочитаний символ завершення файла. Якщо такий символ прочитаний, то feof () повертає ненульове значення і цикл завершується.

Читання з текстового файлу форматованих даних може здійснюватися функцією fscanf (). Синтаксис:

int fscanf(FILE *stream, const char *format[, address, …]);

Параметр format визначає рядок форматування аргументів, які задаються своїми адресами.

При форматованому читанні можуть виникати помилки у зв'язку з досягненням завершення файлу або невірним форматом записаних у файлі даних. Перевірити, чи успішно пройшло читання даних можна за значенням, яке повертає функція fscanf (). При успішному читанні вона повертає кількість прочитаних полів. Тому читання даних можна організовувати наступним чином:

if (fscanf(f,"%d%d%d",&a,&b,&c)!=3)

{

printf("Помилка читання!\n");

};

Існує також і ряд функцій для запису даних у текстовий файл. Найчастіше використовуються функції fgetc (), fputs () та fprintf ().

Функція fgetc () використовується для читання чергового символу з потоку, відкритого функцією fopen ().

int fgetc(FILE *f);

Синтаксис функції fprintf ():

int fprintf(FILE *stream, const char *format[,argument,…]);

Вона працює майже мак само, як і функція printf (), але їй потрібний додатковий аргумент для посилання на файл. Він є першим у списку аргументів. Наводимо приклад, який ілюструє звертання до наведених вище функцій:

#include<stdio.h>

void main()

{

FILE *fi;

int age;

fi=fopen("age.txt","r"); /* відкриття файла для читання */

fscanf(fi,"%d",&age); /*читання з файла числового значення */

fclose(fi); /* закриття файла */

fi=fopen("data.txt", "a"); /* відкриття файла для додавання інформації в кінець */

fprintf(fi, "Age==%d.\n",age); /* запис рядка в файл */

fclose(fi); /* закриття файла */

}

1.15.2 Двійкові файли

Тепер розглянемо роботу з двійковими файлами. Двійковий файл представляє собою просто послідовність символів. Що саме і в якій послідовності зберігається в двійковому файлі - повинна знати програма.

Двійкові файли мають переваги, порівняно з текстовими при зберіганні числових даних. Операції читання і запису з такими файлами виконуються набагато швидше, ніж з текстовими, так як відсутня необхідність форматування (переведення в текстове представлення та навпаки). Двійкові файли зазвичай мають менший розмір, ніж аналогічні текстові файли. В двійкових файлах можна переміщуватися в будь-яку позицію і читати або записувати дані в довільній послідовності, в той час, як в текстових файлах практично завжди виконується послідовна обробка інформації.

Про те, як відкривати двійкові файли було згадано раніше. Запис і читання в двійкових файлах виконується відповідно функціями fwrite і fscanf.

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

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

В обидві функції повинен передаватися покажчик ptr на дані, які вводяться або виводяться. Параметр size задає розмір в байтах даних, які читаються або записуються.

#include<stdio.h>

#include<conio.h>

struct mystruct

{

int i;

char ch;

};

int main(void)

{

FILE *stream;

struct mystruct s;

if ((stream = fopen("test.txt", "wb")) == NULL)

{

fprintf(stderr, "Неможливо відкрити файл\n");

return 1;

}

s.i = 0;

s.ch = 'A';

fwrite(&s, sizeof(s), 1, stream);

fclose(stream);

return 0;

}

Тепер розглянемо особливості записування і читання рядків.

char s[10];

strcpy(s, "Example");

fwrite(s,strlen(s)+1,sizeof(char),stream);

Записування рядків відбувається посимвольно. В даному прикладі число символів, які записуються - strlen(s)+1 (одиниця додається на нульовий символ в кінці). Читається рядок аналогічно:

fread(s,strlen(s)+1,sizeof(char),stream);

При цьому читання проходить теж посимвольно.

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

int i=strlen(s)+1;

fwrite(&i,1,sizeof(int),stream);

fwrite(s,i,1,stream);

fread(&i,1,sizeof(int),stream);

fread(s,i,1,stream)

В усіх наведених вище прикладах читання даних проходило послідовно. Але, працюючи з двійковими файлами, можна організувати читання даних в довільному порядку. Для цього використовується "покажчик файла" (курсор), який визначає поточну позицію у файлі. При читанні даних курсор автоматично зміщується на число прочитаних байтів. Отримати поточну позицію курсору файла можна за допомогою функції ftell ().

long ftell(FILE *stream);

А встановлюється поточна позиція курсору у файлі за допомогою функції fseek ():

int fseek(FILE *stream, long offset, int whence);

Ця функція задає зміщення на число байтів offset від точки відліку, яка визначається параметром whence. Цей параметр може приймати значення 0, 1, 2 (таблиця 1.14).

Таблиця 1.14. Можливі значення параметра whence функції fseek

Константа whence Точка відліку
SEEK_SET   Початок файлу
SEEK_CUR   Поточна позиція
SEEK_END   Кінець файлу

Якщо задане значення whence=1, то offset може приймати як додатне, так і від'ємне значення, тобто зсув вперед або назад.

Функція rewind () переміщує курсор на початок файлу.

void rewind(FILE *stream);

Те ж саме можна зробити за допомогою функції fseek ():

fseek(stream, 0L, SEEK_SET);

Приклад програми, в якій використовуються описані вище функції:

#include <stdio.h>

long filesize(FILE *stream);

int main(void)

{

FILE *stream;

stream = fopen("test.txt", "w+");

fprintf(stream, "This is a test");

printf("Розмір файла test.txt рівний %ld байт\n",

filesize(stream));

fclose(stream);

return 0;

}

long filesize(FILE *stream)

{

long curpos, length;

curpos = ftell(stream);

fseek(stream, 0L, SEEK_END);

length = ftell(stream);

fseek(stream, curpos, SEEK_SET);

return length;

}

1.15.3 Використання дескрипторів файлів

В мові Сі передбачений ще один механізм роботи з файлами - використання дескрипторів. Файли, які відкриваються таким чином не розраховані на роботу з буферами та форматованими даними.

На початку роботи будь-якої програми відкриваються п'ять стандартних потоків зі своїми дескрипторами.

Таблиця 1.15. Дескриптори стандартних потоків введення-виведення

потік дескриптор  
stdin   стандартний вхідний потік
stdout   стандартний вихідний потік
stderr   стандартний потік повідомлень про помилки
stdaux   стандартний потік зовнішнього пристрою
stdprn   стандартний потік виведення на принтер

Але будь-яка програма може і явним чином відкривати будь-які файли з дескрипторами.

Функції, які працюють з дескрипторами файлів, описані в модулі io.h.

Файли відкривається функцією open (), яка повертає дескриптор файлу:

int open(const char *path, int access [, unsigned mode ]);

Параметр path задає ім'я файлу відкриття. Параметр access визначає режим доступу до файлу.

mode є не обов'язковим та задає режим відкриття файла.

Параметр access формується за допомогою операції АБО (|) з переліку прапорців.

O_RDONLY тільки для читання
O_WRONLY тільки для запису
O_RDWR для читання і запису
O_CREAT створення нового файлу
O_TRUNC якщо файл існує, то він стає порожнім
O_BINARY двійковий файл
O_TEXT текстовий файл

Параметр mode може приймати наступні значення

S_IWRITE дозволити запис
S_IREAD дозволити читання

Використання функції fopen () демонструє наступний приклад:

#include <string.h>

#include <stdio.h>

#include <fcntl.h>

#include <io.h>

int main(void)

{

int handle;

char msg[] = "Hello world";

if ((handle = open("TEST.TXT", O_CREAT | O_TEXT)) == -1)

{

perror("Error:");

return 1;

}

write(handle, msg, strlen(msg));

close(handle);

return 0;

}

Як видно з прикладу, файл, відкритий функцією open () повинен бути закритий за допомогою функції close ().

int close(int handle);

Читання і запис даних при роботі з файлами, що визначаються дескрипторами handle, здійснюється функціями write () і read ().

int read(int handle, void *buf, unsigned len);

int write(int handle, void *buf, unsigned len);

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

Буферизація потоків. В мові Сі існує ряд функцій, які дозволяють керувати буферизацією потоків.

Функція setbuf () дозволяє користувачу встановлювати буферизацію вказаного потоку stream. Синтаксис функції setbuf ():

void setbuf(FILE *stream, char *buf);

Значення аргументу stream повинне відповідати стандартному або вже відкритому потоку.

Якщо значення аргументу buffer рівне NULL, то буферизацію буде відмінено. Інакше, значення аргументу buffer буде визначати адресу масиву символів довжини BUFSIZ, де BUFSIZ - розмір буфера (константа, визначена в stdio.h).

Визначений користувачем буфер використовується для буферизованого введення/виведення для вказаного потоку stream замість буферу, що виділяється системою по замовчуванню.

Потоки stderr і stdout по замовчуванню небуферизовані, але для них можна встановлювати буферизацію засобами setbuf.

Примітка. Наслідки буферизації будуть непередбаченими, якщо тільки функція setbuf () не викликана зразу вслід за функцією fopen () або fseek () для заданого потоку.

В мові Сі для керування буферизацією потоків існує ще одна функція: setvbuf (). Вона дозволяє користувачу керувати буферизацією та розміром буфера потоку stream. Синтаксис:

int setvbuf(FILE *stream, char *buf, int type, size_t size);

Потік stream повинен відноситися до відкритого потоку.

Якщо значення параметру buf не NULL, то масив, адреса якого задається значенням параметра buf буде використовуватися в якості буфера.

Якщо потік буферизується, значення параметра type визначає тип буферизації. Тип буферизації може бути або _IONBF, або _IOFBF, або _IOLBF.

Якщо тип рівний _IOFBF або _IOLBF, то значення параметра size використовується як розмір буфера.

Якщо тип рівний _IONBF, то потік небуферизований, і значення параметрів size і buf ігноруються.

Допустиме значення параметра size: більше 0 і менше, ніж максимальний розмір цілого (int).

Значення констант _IONBF, _IOFBF та _IOLBF визначені у файлі stdio.h.

_IOFBF 0 /* буферизація на повний об'єм буфера */

_IOLBF 1 /* порядкова буферизація */

_IONBF 2 /* потік не буферизується */

Для примусового виштовхування буферу можна використовувати функцію fflush (). Її синтаксис:

int fflush(FILE *stream);

Дана функція виштовхує вміст буфера, зв'язаного з потоком stream. Потік залишається відкритим. Якщо потік небуферизований, то виклик функції fflush () не викличе ніяких ефектів.

Буфер потоку автоматично виштовхується, коли він заповнюється, коли закривається потік або коли програма завершує своє виконання.

Приклад 1.

#include <stdio.h>

#include<conio.h>

char outbuf[BUFSIZ];

int main(void)

{

clrscr();

setbuf(stdout, outbuf);

puts("This is a test of buffered output.\n\n");

puts("This output will go into outbuf\n");

puts("and won't appear until the buffer\n");

puts("fills up or we flush the stream.\n");

getch();

fflush(stdout);

getch();

return 0;

}

Приклад 2.

#include <stdio.h>

int main(void)

{

FILE *input, *output;

char bufr[512];

input = fopen("file.in", "r+b");

output = fopen("file.out", "w");

if (setvbuf(input, bufr, _IOFBF, 512)!= 0)

printf("Помилка встановлення буферизацiї для вхiдного файла\n");

else

printf("Для вхiдного файла встановлено буферизацiю\n");

if (setvbuf(output, NULL, _IOLBF, 132)!= 0)

printf("Помилка встановлення буферизацiї для вихiдного файла\n");

else printf("Буфер для вихiдного файла встановлено\n");

fclose(input);

fclose(output);

return 0;

}

1.16 Функціональний підхід

Як визначити термін "програма"? Взагалі це послідовність операцій над структурами даних, що реалізують алгоритм розв'язання конкретної задачі. На початку проектування задачі ми розмірковуємо відносно того, що повинна робити наша програма, які конкретні задачі вона повинна розв'язувати, та які алгоритми при цьому повинні бути реалізовані. Буває, і це характерно для більшості задач, вихідна задача досить довга та складна, у зв'язку з чим програму складно проектувати та реалізовувати, а тим більше супроводжувати, якщо не використовувати методів керування її розмірами та складністю. Для цього потрібно використати відомі прийоми функціонально-модульного програмування для структурування програм, що полегшує їх створення, розуміння суті та супровід.

Розв'язання практичної задачі проходить у кілька етапів, зміст яких подає таблиця 1.16.

Організація програми на Сі досить логічна. Мова Сі надає надзвичайно високу гнучкість для фізичної організації програми. Нижче наведена типова організація невеликого програмного проекту на Сі:

Нижче піде мова про процедурне (функціональне) програмування на Сі. Існують досить добре розвинуті методи процедурного програмування, що базуються на моделі побудови програми як деякої сукупності функцій. Прийоми програмування пояснюють, як розробляти, організовувати та реалізовувати функції, що складають програму.

Структура кожної функції співпадає зі структурою головної функції програми main (). Функції іноді ще називають підпрограмами.

Основу процедурного програмування на будь-якій мові програмування складає процедура (походить від назви) або функція (як різновид, що саме відповідає мові програмування Сі).

Функція - модуль, що містить деяку послідовність операцій. Її розробка та реалізація у програмі може розглядатися як побудова операцій, що вирішують конкретну задачу (підзадачу). Однак взагалі функція може розглядатися окремо як єдина абстрактна операція, і, щоб її використовувати, користувачеві необхідно зрозуміти інтерфейс функції - її вхідні дані та результати виконання. Легко буде зрозуміти ту функцію, що відповідає абстрактним операціям, необхідним для рішення задачі. Функцію та її використання у програмі можна у такому разі представляти у термінах задачі, а не в деталях реалізації. Припустимо, необхідно розробити функціональний модуль, що розв'язує наступне завдання: існує вхідний список певних даних, який необхідно відсортувати, переставляючи його елементи у визначеному порядку. Ця функція може бути описана, як абстрактна операція сортування даних, що може бути частиною вирішення деякої підмножини задач. Функція, що реалізує цю операцію, може бути використана у багатьох програмах, якщо вона створена як абстракція, що не залежить від реалізації (контексту програми).

Таблиця 1.16. Типові етапи розв'язання задач

Етапи Опис
Постановка задачі та її змістовний аналіз 1. Визначити вхідні дані, які результати необхідно отримати і в якому вигляді подавати відповіді. 2. Визначити за яких умов можливо отримати розв'язок задачі, а за яких - ні. 3. Визначити, які результати вважатимуться вірними.
Формалізація задачі, вибір методу її розв'язання. (математичне моделювання задачі) 1. Записати умову задачі за допомогою формул, графіків, рівнянь, нерівностей, таблиць тощо. 2. Скласти математичну модель задачі, тобто визначити зв'язок вихідних даних із відповідними вхідними даними за допомогою математичних співвідношень з урахуванням існуючих обмежень на вхідні, проміжні та вихідні дані, одиниці її виміру, діапазон зміни тощо. 3. Вибрати метод розв'язку задачі.
Складання алгоритму розв'язання задачі Алгоритм більшою мірою визначається обраним методом, хоча один і той самий метод може бути реалізований за допомогою різних алгоритмів. Під час складання алгоритму необхідно враховувати всі його властивості.
Складання програми Написання програми на мові програмування
Тестування і відлагодження програми Перевірка правильності роботи програми за допомогою тестів і виправлення наявних помилок. Тест - це спеціально підібрані вхідні дані та результати, отримані в результаті обробки програмою цих даних.
Остаточне виконання програми, аналіз результатів Після остаточного виконання програми необхідно провести аналіз результатів. Можлива зміна самого підходу до розв'язання задачі та повернення до першого етапу для повторного виконання усіх етапів.

Функції мають параметри, тому їх операції узагальнені для використання будь-якими фактичними аргументами відповідного типу. Що є вхідними даними для функції? Вхідними даними для неї є аргументи та глобальні структури даних, що використовуються функцією. Вихідними даними є ті значення, які функція повертає, а також зміни глобальних даних, модифікації.

Функціональний модуль, що не використовує глобальні дані, параметризується вхідними параметрами. Функція - це операція над будь-якими аргументами відповідного типу, адже вона не оперує конкретними об'єктами у програмі. Тому її можна використовувати безліч разів з різними параметрами, і не тільки в одній програмі, а й в інших із структурами даних того ж типу. Інтерфейс буде зрозумілий з опису прототипу функції, а об'єкти даних, описані в його реалізації, зрозумілі з локальних оголошень функції. Тому при параметризації входу та локалізації описів функція представляє собою тип самодокументованого модуля, який легко використовувати. Крім цього, функціям притаманна модульність. Її широко використовують для надання функціям більшої ясності, можливості повторного використання, що, таким чином, допомагає скоротити витрати, пов'язані з її реалізацією та супроводом.

1.16.1 Функції

Як було сказано вище, функції можуть приймати параметри і повертати значення. Будь-яка програма на мові Сі складається з функцій, причому одна з яких обов'язково повинна мати ім'я main().

Синтаксис опису функції має наступний вигляд:

тип_поверт_значення ім'я_функції ([список_аргументів])

{

оператори тіла функції

}

Рис. 1.20. Синтаксис опису функції

Слід чітко розрізняти поняття опису та представлення функцій.

Опис функції задає її ім'я, тип значення, що повертається та список параметрів. Він дає можливість організувати доступ до функції (розташовує її в область видимості), про яку відомо, що вона external (зовнішня). Представлення визначає, задає дії, що виконуються фунцією при її активізації (виклику).

Оголошенню функції можуть передувати специфікатори класу пам'яті extern або static.

extern - глобальна видимість у всіх модулях (по замовчуванню);

static - видимість тільки в межах модуля, в якому визначена функція.

Тип значення, яке повертається функцією може бути будь-яким, за виключенням масиву та функції (але може бути покажчиком на масив чи функцію). Якщо функція не повертає значення, то вказується тип void.

1.16.2 Функції, що не повертають значення

Функції типу void (ті, що не повертають значення), подібні до процедур Паскаля. Вони можуть розглядатися як деякий різновид команд, реалізований особливими програмними операторами. Оператор func(); виконує функцію void func(), тобто передасть керування функції, доки не виконаються усі її оператори. Коли функція поверне керування в основну програму, тобто завершить свою роботу, програма продовжить своє виконання з того місця, де розташовується наступний оператор за оператором func().

/*демонстраційна програма*/

#include<stdio.h>

void func1(void);

void func2(void);

main()

{

func1();

func2();

return 0;

}

void func1(void)

{

/* тіло */

}

void func2(void)

{

/* тіло */

}

Звернемо увагу на те, що текст програми починається з оголошення прототипів функцій - схематичних записів, що повідомляють компілятору ім'я та форму кожної функції у програмі. Для чого використовуються прототипи? У великих програмах це правило примушує Вас планувати проекти функцій та реалізовувати їх таким чином, як вони були сплановані. Будь-яка невідповідність між прототипом (оголошенням) функції та її визначенням (заголовком) призведе до помилки компіляції. Кожна з оголошених функцій має бути визначена у програмі, тобто заповнена операторами, що її виконують. Спочатку йтиме заголовок функції, який повністю співпадає з оголошеним раніше прототипом функції, але без заключної крапки з комою. Фігурні дужки обмежують тіло функції. В середині функцій можливий виклик будь-яких інших функцій, але неможливо оголосити функцію в середині тіла іншої функції. Нагадаємо, що Паскаль дозволяє працювати із вкладеними процедурами та функціями.

Надалі розглянемо приклад програми, що розв'язує відоме тривіальне завдання - обчислює корені звичайного квадратного рівняння, проте із застосуванням функціонального підходу:

#include <stdio.h>

#include <stdlib.h>

#include <conio.h>

#include <math.h>

float A,B,C;

/*функція прийому даних*/

void GetData()

{

clrscr();

printf("Input A,B,C:");

scanf("%f%f%f",&A,&B,&C);

}

/*функція запуску основних обчислень*/

void Run()

{

float D;

float X1, X2;

if ((A==0) && (B!=0))

{

X1 = (-C)/B;

printf("\nRoot: %f",X1);

exit(0);

}

D = B*B - 4*A*C;

if (D<0) printf("\nNo roots...");

if (D==0)

{

X1=(-B)/(2*A);

printf("\nTwo equal roots: X1=X2=%f",X1);

}

if (D>0)

{

X1 = (-B+sqrt(D))/(2*A);

X2 = (-B-sqrt(D))/(2*A);

printf("\nRoot X1: %f\nRoot X2: %f",X1,X2);

}

}

/*головна функція програми/

void main()

{

GetData();

Run();

}

Якщо в описі функції не вказується її тип, то по замовчуванню він приймається як тип int. У даному випадку обидві функції описані як void, що не повертають значення. Якщо ж вказано, що функція повертає значення типу void, то її виклик слід організовувати таким чином, аби значення, що повертається, не використовувалося б.

Просто кажучи, таку функцію неможливо використовувати у правій частині виразу. В якості результату функції остання не може повертати масив, але може повертати покажчик на масив. У тілі будь-якої функції може бути присутнім вираз return; який не повертає значення. І, насамкінець, усі програмні системи, написані за допомогою мови Сі, повинні містити функцію main (), що є вхідною точкою будь-якої системи. Якщо вона буде відсутня, завантажувач не зможе зібрати програму, про що буде отримано відповідне повідомлення.

1.16.3 Передача параметрів

Усі параметри, за винятком параметрів типу покажчик та масивів, передаються за значенням. Це означає, що при виклику функції їй передаються тільки значення змінних. Сама функція не в змозі змінити цих значень у викликаючій функції. Наступний приклад це демонструє:

#include<stdio.h>

void test(int a)

{

a=15;

printf(" in test: a==%d\n",a);

}

void main()

{

int a=10;

printf("before test: a==%d\n",a);

test(a);

printf("after test: a==%d\n",a);

}

При передачі параметрів за значенням у функції утворюється локальна копія, що приводить до збільшення об'єму необхідної пам'яті. При виклику функції стек відводить пам'ять для локальних копій параметрів, а при виході з функції ця пам'ять звільняється. Цей спосіб використання пам'яті не тільки потребує додаткового її об'єму, але й віднімає додатковий час для зчитування. Наступний приклад демонструє, що при активізації (виклику) функції копії створюються для параметрів, що передаються за значенням, а для параметрів, що передаються за допомогою покажчиків цього не відбувається. У функції два параметри - one, two - передаються за значенням, three - передається за допомогою покажчика. Так як третім параметром є покажчик на тип int, то він, як і всі параметри подібного типу, передаватиметься за вказівником:

#include <stdio.h>

void test(int one, int two, int * three)

{

printf("\nАдреса one дорівнює %р", &one);

printf("\nАдреса two дорівнює %р", &two);

printf("\nАдреса three дорівнює %р", &three);

*three+=1;

}

main()

{

int a1,b1;

int c1=42;

printf("\nАдреса a1 дорівнює %р", &a1);

printf("\nАдреса b1 дорівнює %р", &b1);

printf("\nАдреса c1 дорівнює %р", &c1);

test(a1,b1,&c1);

printf("\nЗначення c1 = %d\n",c1);

}

На виході ми отримуємо наступне:

Адреса а1 дорівнює FEC6

Адреса b1 дорівнює FEC8

Адреса c1 дорівнює FECA

Адреса one дорівнює FEC6

Адреса two дорівнює FEC8

Адреса three дорівнює FECA

Значення c1 = 43

Після того, як змінна *three в тілі функції test збільшується на одиницю, нове значення буде присвоєно змінній c1, пам'ять під яку відводиться у функції main ().

У наступному прикладі напишемо програму, що відшукує та видаляє коментарі з програми на Сі. При цьому слід не забувати коректно опрацьовувати рядки у лапках та символьні константи. Вважається, що на вході - звичайна програма на Сі. Перш за все напишемо функцію, що відшукує початок коментарю (/*):

/*функція шукає початок коментарю */

void rcomment(int c)

{

int d;

if (c=='/')

if ((d=getchar())=='*')

in_comment();

else

if (d=='/')

{

putchar(c);

rcomment(d);

}

else

{

putchar(c);

putchar(d);

}

else

if (c=='\''|| c=='"') echo_quote(c);

else putchar(c);

}

Функція rcomment(int c) відшукує початок коментарю, а коли знаходить, викликає функцію in_comment(), що відшукує кінець коментарю. Таким чином, гарантується, що перша процедура дійсно ігноруватиме коментар:

/*функція відшукує кінець коментарю */

void in_comment(void)

{

int c,d;

c=getchar();

d=getchar();

while (c!='*'|| d!='/')

{

c=d;

d=getchar();

}

}

Крім того, функція rcomment(int c) шукає також одинарні та подвійні дужки, та якщо знаходить, викликає echo_quote(int c). Аргумент цієї функції показує, зустрілась одинарна або подвійна дужка. Функція гарантує, що інформація всередині дужок відображається точно та не приймається помилково за коментар:

/*функція відображає інформацію без коментарю */

void echo_quote(int c)

{

int d;

putchar(c);

while ((d=getchar())!=c)

{

putchar(d);

if (d=='\\')

putchar(getchar());

}

putchar(d);

}

До речі, функція echo_quote(int c) не вважає лапки, що слідують за зворотною похилою рискою, заключними. Будь-який інший символ друкується так, як він є насправді. А на кінець текст функції main() даної програми, що відкривається переліком прототипів визначених нами функцій:

/* головна програма */

#include <stdio.h>

void rcomment(int c);

void in_comment(void);

void echo_quote(int c);

main()

{

int c,d;

while ((c=getchar())!=EOF)

rcomment(c);

return 0;

}

Програма завершується, коли getchar() повертає символ кінця файлу. Це був типовий випадок проектування програми із застосуванням функціонального підходу.

1.16.4 Функції із змінним числом параметрів

Інколи у функції потрібно передати деяке число фіксованих параметрів та невизначене число додаткових. В цьому випадку опис функції буде мати вигляд:

тип ім'я_функції(список параметрів,...)

Список аргументів включає в себе скінченне число обов'язкових параметрів (цей список не може бути порожнім), після якого на місці невизначеного числа параметрів ставиться три крапки. Для роботи з цими параметрами у файлі stdarg.h визначений тип списку va_list і три макроси: va_start, va_arg, va_end.

Макрос va_start має синтаксис:

void va_start(va_list ap, lastfix);

Цей макрос починає роботу зі списком, встановлюючи його покажчик ap на перший аргумент зі списку аргументів з невизначеним числом.

Макрос va_arg має синтаксис:

void va_arg(va_list ap, type);

Цей макрос повертає значення наступного (чергового) аргументу зі списку. Перед викликом va_arg значення ap повинне бути встановлене викликом va_start або va_arg. Кожний виклик va_arg переводить покажчик на наступний аргумент.

Макрос va_end має синтаксис:

void va_end(va_list ap);

Даний макрос завершує роботу зі списком, звільняючи пам'ять.

#include <stdio.h>

#include <stdarg.h>

void sum(char *msg,...)

{

int total = 0;

va_list ap;

int arg;

va_start(ap, msg);

while ((arg = va_arg(ap,int))!= 0)

{

total += arg;

}

printf(msg, total);

va_end(ap);

}

int main(void)

{

sum("Сума 1+2+3+4 рівна %d\n", 1,2,3,4,0);

return 0;

}

1.16.5 Рекурсивні функції

Рекурсія - це спосіб організації обчислювального процесу, при якому функція в ході виконання операторів звертається сама до себе.

Функція називається рекурсивною, якщо під час її виконання можливий повторний її виклик безпосередньо (прямий виклик) або шляхом виклику іншої функції, в якій міститься звертання до неї (непрямий виклик).

Прямою (безпосередньою) рекурсією називається рекурсія, при якій всередині тіла деякої функції міститься виклик тієї ж функції.

void fn(int i)

{

/*... */

fn(i);

/*... */

}

Непрямою рекурсією називається рекурсія, що здійснює рекурсивний виклик функції шляхом ланцюга викликів інших функцій. При цьому всі функції ланцюга, що здійснюють рекурсію, вважаються також рекурсивними.

void fnA(int i);

void fnB(int i);

void fnC(int i);

void fnA(int i)

{

/*... */

fnB(i);

/*... */

}

void fnB(int i)

{

/*... */

fnC(i);

/*... */

}

void fnC(int i)

{

/*... */

fnA(i);

/*... */

}

Якщо функція викликає сама себе, то в стеку створюється копія значень її параметрів, як і при виклику звичайної функції, після чого управління передається першому оператору функції. При повторному виклику цей процес повторюється.

В якості прикладу розглянемо функцію, що рекурсивно обчислює факторіал. Як відомо, значення факторіала обчислюється за формулою: , причому і . Факторіал також можна обчислити за допомогою простого рекурентного співвідношення . Для ілюстрації рекурсії скористаємося саме цим співвідношенням.

#include<stdio.h>

#include<conio.h>

double fact(int n)

{

if (n<=1) return 1;

return (fact(n-1)*n);

}

void main()

{

int n;

double value;

clrscr();

printf("N=");

scanf("%d",&n);

value=fact(n);

printf("%d! = %.50g",n,value);

getch();

}

Роботу рекурсивної функції fact () розглянемо на прикладі n=6! За рекурентним співвідношенням: . Таким чином, щоб обчислити 6! ми спочатку повинні обчислити 5!. Використовуючи співвідношення, маємо, що , тобто необхідно визначити 4!. Продовжуючи процес, отримаємо:

1).

2).

3).

4).

5).

6).

В кроках 1-5 завершення обчислення кожний раз відкладається, а шостий крок є ключовим. Отримане значення, яке визначається безпосередньо, а не як факторіал іншого числа. Відповідно, ми можемо повернутися від 6-ого кроку до 1-ого, послідовно використовуючи значення:

6).1!=1

5).2!=2

4).3!=6

3). 4!=24

2). 5!=120

1). 6!=720

Важливим для розуміння ідеї рекурсії є те, що в рекурсивних функціях можна виділити дві серії кроків.

Перша серія - це кр оки рекурсивного занурення функції в саму себе до тих пір, поки вибраний параметр не досягне граничного значення. Ця важлива вимога завжди повинна виконуватися, щоб функція не створила нескінченну послідовність викликів самої себе. Кількість таких кроків називається глибиною рекурсії.

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

1.16.6 Покажчики на функції

Як згадувалося раніше, на функцію, як і на інший об'єкт мови Сі можна створити покажчик.

float (*func)(float a, float b); /* покажчик на функцію, що приймає два параметри типу float і повертає значення типу float */

Покажчики на функції широко використовується для передачі функцій як параметрів іншим функціям.

За означенням покажчик на функцію містить адресу першого байта або слова виконуваного коду функції. Над покажчиками на функцію заборонені арифметичні операції.

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

#include<stdio.h>

#include<conio.h>

void main(void)

{

void (*efct)(char *s); /* змінній-покажчику виділена ОП, але efct не містить значення адреси ОП для функції */

efct("Error"); /* груба помилка - спроба працювати з неініціалізованим покажчиком*/

}

Для того, щоб можна було використовувати покажчик на функцію потрібно спочатку присвоїти йому значення адреси пам'яті, де розташована функція, як це зроблено в наступному прикладі.

#include<stdio.h>

#include<conio.h>

void print(char *s)

{

puts(s);

}

void main(void)

{

void (*efct)(char *s);

efct=&print; /* efct=print */

(*efct)("Function Print!"); /* efct("Function Print!"); */

}

Для отримання значення адреси функції необов'язково використовувати операцію &. Тому наступні присвоювання будуть мати однаковий результат:

1). efct=&print;

2). efct=print;

Операція розіменування покажчика на функцію * також є необов'язковою.

1). (*efct)("Function Print!");

2). efct("Function Print!");

Покажчикам на функції можна присвоювати адреси стандартних бібліотечних функцій.

#include<stdio.h>

#include<conio.h>

#include<math.h>

void main(void)

{

double (*fn)(double x);

float y,x=1;

fn=sin;

y=fn(x);

printf("sin(%g)==%g\n",x,y);

fn=cos;

y=fn(x);

printf("cos(%g)==%g\n",x,y);

}

Покажчики на функції можуть також виступати в якості аргументів функцій.

#include<stdio.h>

#include<conio.h>

#include<math.h>

double fn(double (*pfn)(double x),double x)

{

double y=pfn(x);

printf("y==%g\n",y);

return y;

}

double sin_cos(double x)

{

return sin(x)*cos(x);

}

void main(void)

{

fn(sin,1);

fn(&cos,1);

fn(&sin_cos,1);

}

1.16.7 Класи пам'яті

Будь-яка змінна та функція, описана y програмі на Сi, належить до конкретного класу пам'яті, що визначає час її існування та область видимості. Час існування змінної - це період, протягом якого змінна існує в пам'яті, а область видимості (область дії) - це частина програми, в якій змінна може використовуватися.

В мові Сі існує чотири специфікатори класу пам'яті: auto, register, extern і static.

Таблиця 1.17. Область дії та час існування змінних різних класів пам'яті

Клас пам'яті Ключове слово Час існування Область дії
Автоматичний auto тимчасово блок
Регістровий register тимчасово блок
Статичний локальний static постійно блок
Статичний глобальний static постійно файл
Зовнішній extern постійно програма

Клас пам'яті для функції завжди external, якщо перед її описом не стоїть специфікатор static. Клас пам'яті конкретної змінної залежить або від місця розташування її опису, або задається явно за допомогою спеціального специфікатору класу пам'яті, що розташовується перед описом функції. Усі змінні Сі можна віднести до одного з наступних класів пам'яті:





Дата публикования: 2015-01-23; Прочитано: 1693 | Нарушение авторского права страницы | Мы поможем в написании вашей работы!



studopedia.org - Студопедия.Орг - 2014-2024 год. Студопедия не является автором материалов, которые размещены. Но предоставляет возможность бесплатного использования (0.17 с)...