ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 21.12.2021
Просмотров: 978
Скачиваний: 3
На прикладі наведено текст програми з функцією obmen (x, y), яка міняє місцями значення двох цілих величин(рис. 5.8). Так як x, y - адреси змінних a і b, то * x і * y забезпечують непрямий доступ значенням a і b. До сказаного додамо, що використання покажчиків дозволяє нам оминати обмеження мови Сі, згідно з якими функцій може повертати лише одне значення.
Якщо як функції передається ім'я масиву, то воно фактично визначає адресу початку масиву, тобто є покажчиком. Як ілюстрацію напишемо чергову версію функції length, що обчислює довжину рядка. Якщо ім'я масиву передається функції, то в самій функції можна виходити з того, що вона працює з масивом, а можна виходити з того, що вона працює з покажчиком.
Приклад 7.Заміна місцями значення двох цілих величин.
/ * обмін a і b * /
#include <stdio.h>
void zamina (int * x, int * y){
int t;
t =* x;
* x =* y;
* y = t;
}
int main () {
int a, b;
a = 3; b = 7;
zamina (&a, & b);
printf ("a =% d b =% d", a, b);
}
Рисунок 5.8 - Заміна місцями значення двох цілих величин.
На схемі вказано як відбувається обмін між a і b. Суцільними лініями вказано я ми через код бачимо обмін, пунктиром, як відбувається обмін самих даних.
У визначенні функції формальні параметри char s [] і char * s абсолютно ідентичні.
5.4 Операція s++
(приклад 2) збільшення на одиницю поточне значення покажчика, спочатку вказує на перший символ рядка, а операція * s! = '\ 0' порівнює черговий символ ознакою кінця рядка.
Приклад 8 Збільшення на одиницю поточне значення покажчика
/ * довжина рядка * /
int length (char * s) {
int i;
for (i = 0; * s! = '\ 0'; s++)
i++;
return i;}
5.5 Операції ==, !=
Крім раніше розглянутих операцій адресної арифметики, до покажчиків можна застосувати операції порівняння == і! =. Навіть операції відношення <,> = і т.п. працюють правильно, якщо покажчики посилаються на елементи одного і того ж масиву. В останньому випадку можливе навіть віднімання посилань: якщо u і s посилаються на елементи одного масиву, то us є число елементів між u і s. Використовуємо цей факт для складання ще однієї версії функції length (рис. 5.9). Спочатку u вказує на перший символ рядка (char * u = s). Потім у циклі по черзі перевіряється кожен символ, поки зрештою не буде виявлений "\ 0". Різниця us дає саме довжину рядка.
Приклад 9. Функція length
/ * довжина рядка * /
int length (char * s) {
char * u = s;
while (* u! = '\ 0')
u++;
return (u-s);
}
Рисунок 5.9 – Ілюстрація функції length
І для даного випадку u-s = 3(останній нуль-символ не рахується).
5.6 Копіювання рядка
Для ілюстрації основних аспектів застосування покажчиків в Сі розглянемо функцію копіювання рядка s1 в рядок s2. Спочатку наведемо версію, засновану на роботі з масивами. Для порівняння поруч поміщена версія з використанням покажчиків .
Приклад 10. копіювання рядка s1 в рядок s2 звичайним способом.
/ * копія рядка * /
void copy (char s1 [],char s2 []){
int i = 0;
while ((s2[i] = s1[i])!='\0')
i ++;
}
Приклад 11. копіювання рядка s1 в рядок s2 із використанням покажчиків.
/ * копія рядка * /
void copy (char * s1, char * s2){
while ((* s2 =* s1)! = '\ 0')
s2 ++; s1 ++;
}
Тут операція копіювання поміщена безпосередньо в умові, що визначає момент циклу: while ((* s2 =* s1)! = '\ 0'). Просування вздовж масивів аж до тих пір, поки не зустрінеться "\ 0", забезпечують оператори s2++ та s1++. Їх, однак, теж можна помістити в перевірку.
Приклад 12. копіювання рядка s1 в рядок s2 із використанням циклу while
/ * копія рядка * /
void copy (char * s1, char* s2){
while ((*s2 ++ =*s1 ++)!=' \ 0 ');
}
Ще раз нагадаємо, що унарні операції типу * і ++ виконуються справа наліво. Значення *s++ це символ, на який вказує s до його збільшення. Так як значення "\ 0" є нуль, а цикл while провірит, чи не нуль вираз в дужках, то це дозволяє опустити явне порівняння з нулем. Так поступово функція копіювання стає все більш компактною і ... все менш зрозумілою. Але в системному програмуванні перевагу частіше віддають саме компактним і, отже, більш ефективним за швидкодією програмам.
Приклад 13.
/ * копія рядка * /
void copy (char * s1, char* s2){
while (* s2 ++ =* s1 ++);
}
У мові Сі, що деяка символьна стрічка, виражена як "рядок", фактично розглядається як покажчик на нульовий елемент масиву "рядок". Допускається, наприклад, такий цікавий запис:
Приклад 14. Вираження символьної стрічки як "рядок".
char * uk; uk = "ІНФОРМАТИКА";
Останній оператор присвоює покажчиком адресу нульового елемента рядка, тобто символу "І". Виникає питання, де знаходиться масив, що містить символи "ІНФОРМАТИКА"? Ми його ніде не описували. Відповідь така: цей рядок - константа, вона є частиною функції, в якій зустрічається, точно так саме, як ціла константа 4 або символьна константа "А" в операторах i = 4; c = "A".
Приклад 15. Друкування рядка символів у зворотному порядку.
# include <stdio.h>
int main () {
char * uk1, * uk2;
uk1 = uk2 = "ІНФОРМАТИКА";
while (* uk2! = '\ 0')
putchar (* uk2 + +);
putchar ('\ n');
while (--uk2> = uk1)
putchar (* uk2);
putchar ('\ n');
}
На самому початку покажчиками uk1 і uk2 присвоюється початкові адреси рядка "ІНФОРМАТИКА". Потім рядок посимвольно друкується і одночасно покажчик uk2 зміщується вздовж рядка. В кінці висновку uk2 вказує на останній символ вихідного рядка. У другому циклі while все той же покажчик uk2 починає змінюватися в зворотному напрямку, зменшуючись до тих пір, поки він не буде вказувати на нульовий елемент масиву, забезпечуючи видачу рядка в зворотному порядку.
5.7 Посилання та оператор &
Як тільки ми оголошуємо змінну, виділяється необхідний обсяг пам'яті, призначається для неї в певному місці в ОЗП (адреса в пам'яті). Як правило, ми не вибираємо це місце розташування змінної, це завдання автоматично виконується ОС під час виконання. Однак у деяких випадках нам потрібно знати адресу, за якою наша змінна буде зберігатися під час виконання програми.
Використання покажчиків в якості альтернативного способу доступу до змінних таїть в собі небезпеку - якщо було змінено адресу, що зберігається в покажчику, то цей покажчик більше не посилається на потрібне значення.
Мова C пропонує альтернативу для більш безпечного доступу до змінних через покажчики. Оголосивши змінну посилання , можна створити об'єкт, який, як покажчик, посилається на інше значення, але, на відміну від покажчика, постійно прив'язаний до цього значення. Таким чином, посилання на значення завжди посилається на це значення.
Головне що варто знати це те, що при ініціалізації посилання вона повинна бути проініціалізована(!).
Приклад 16. Синтаксис запису.
int &reference (NULL); /*операція неможлива!*/
int &reference = variable; /*правильна ініціалізація */
Приклад 17. Використання посилань(рис. 5.10).
int main(){
int first = 1111; /*Присвоєно значення змінній.*/
int *p = &first; /*Покажчику присвоєно адресу ivar.*/
int &link = first; /*Посилання асоційовано з ivar.*/
int *sp = &link; /*Покажчику присвоєно адресу iref.*/
printf( "first = %i \n", first);
printf("*p = %i \n ", *p);
printf( "link = %i \n ",link );
printf("*sp = %i \n ", *sp );
}
Результат роботи програми:
Рисунок 5.10 - Схема роботи:
Якщо link присвоювати якісь значення(link++, link-=3 …) то будуть змінюватись значення змінної на яку вона вказує.
Якщо функція в якості параметра приймає посилання, то вона змінює безпосередньо ці значення.
Приклад 18. Використання посилань.
void chage (int &c,int &d){
int var=c;
c = d; d = var;
}
int main (){
int a=1,b=0;
change(a, b);
printf(“a = %i, b = %i \n”, a, b);
}
Результат роботи програми:
В якості параметра функції беруться посилання, тобто синоніми змінних a i b, тому виходить ,що функція працює не з копіями, а з самими змінними.
5.9 Посилання в якості результатів функції
Функції можуть повертати посилання на об'єкти за умови, що ці об'єкти існують, коли функція неактивна. Таким чином, функції не можуть повертати посилання на локальні автоматичні змінні. Наприклад, для функції, оголошеної як:
Приклад 19. Синтаксис.
double & rf (int p);
необхідний аргумент цілого типу, і вона повертає посилання на об'єкт double, ймовірно оголошеного десь в іншому місці.
Функція знаходить та повертає посилання на найбільший елемент масиву:
Приклад 20. Пошук посилання на найбільший елемент масиву.
int &max(int n, int d[]){
int a=0;
for (int i=1; i<n; i++)
a = d[a]>d[i]?a:i; /*якщо елемент з індексом im більший чим елемент з індексом і то im присвоюється im (тобто без змін), а якщо твердження не правильне то im присвоюється значення і*/
return d[a];
}
int main (){
int x[]={10, 20, 30, 15};
int n=4;
printf("max(n,x) = %i \n", max(n,x));
max(n,x) = 0;
for (int i=0;i<n;i++)
printf("x[%i]= %i\n",i, x[i]);
system("PAUSE");
return EXIT_SUCCESS;
}
Результат роботи програми:
При першому викликові функції відбувається виведення елемента, при другому, через посилання, цьому елементу присвоюється значення 0.
5.10 Покажчики на функцію
Перш ніж вводити покажчик на функцію, нагадаємо, що кожна функція характеризується типом значення, що повертається, ім'ям та сигнатурою. Нагадаємо, що сигнатура визначається кількістю, порядком проходження і типами параметрів. Іноді кажуть, що сигнатурою функції називається список типів її параметрів.
А тепер шляхом послідовності тверджень прийдемо до обговорення теми:
1. При використанні імені функції без подальших дужок і параметрів ім'я функції виступає в якості покажчика на цю функцію, і його значенням служить адреса розміщення функції в пам'яті.
2. Це значення адреси може бути присвоєно деякого покажчиком, і потім вже цей новий покажчик можна застосовувати для виклику функції.
3. У визначенні нового покажчика має бути той же тип, що і повертається функцією значення, і та ж сигнатура.
4. Покажчик на функцію визначається таким чином:
Приклад 21. Синтаксис.
тип_функціі (* імя_покажчика) (специфікація_параметрів);
Наприклад: int (* func1Ptr) (char); - визначення покажчика func1Ptr на функцію з параметром типу char, що повертає значення типу int.
Примітка: Будьте уважні! Якщо наведену синтаксичну конструкцію записати без перших круглих дужок, тобто у вигляді int * fun (char); то компілятор сприйме її як прототип якоїсь функції з ім'ям fun і параметром типу char, що повертає значення покажчика типу int *.
Другий приклад: char * (* func2Ptr) (char *, int); - визначення покажчика func2Ptr на функцію з параметрами типу покажчик на char і типу int, яка повертає значення типу покажчик на char.
Представлення на практиці.
У визначенні покажчика на функцію тип значення і сигнатура (типи, кількість і послідовність параметрів) повинні збігатися з відповідними типами і сигнатурами тих функцій, адреси яких передбачається привласнювати вводиться покажчиком при ініціалізації або за допомогою оператора присвоювання. В якості найпростішої ілюстрації сказаного наведемо програму з покажчиком на функцію:
Приклад 22. Покажчик на функцію.
void Fone (void) /* Визначення Fone.*/
{
printf("Load Fone() \ n");
}
void Ftwo (void) /* Визначення Ftwo.*/
{
printf("Load Ftwo () \ n");
}
int main (){
void (* ptof) (void); /* ptof - покажчик на функцію.*/
ptof = Ftwo; /* Присвоюється адресу Ftwo ()*/
(*ptof) (); /* Виклик Ftwo () за її адресою.*/
ptof = Fone; /* Присвоюється адресу Fone ()*/
(*ptof) (); /* Виклик Fone () за її адресою.*/
ptof (); /* Виклик еквівалентний (*ptof) ();*/
}
Результат робти::
Тут значенням імені_покажчика служить адреса функції, а за допомогою операції розіменовування * забезпечується звернення за адресою до цієї функції. Проте буде помилкою записати виклик функції без дужок у вигляді * ptof ();. Справа в тому, що операція () має більш високий пріоритет, ніж операція звернення за адресою *. Отже, відповідно до синтаксисом буде спочатку зроблена спроба звернутися до функції ptof (). І вже до результату буде віднесена операція розіменовування, що буде сприйнято як синтаксична помилка.
При визначенні покажчик на функцію може бути ініціалізований. В якості ініціалізуючого значення повине використовуватися адреса функції, тип і сигнатура якої відповідають визначення покажчика.
При присвоєнні покажчиків на функції також необхідно дотримувати відповідність типів значень, що повертаються функцій і сигнатур для покажчиків правої і лівої частин оператора присвоювання. Те ж справедливо і при наступному виклику функцій за допомогою покажчиків, тобто типи і кількість фактичних параметрів, використовуваних при зверненні до функції за адресою, повинні відповідати формальним параметрам, що викликається. Наприклад, тільки деякі з таких операторів будуть допустимі:
Приклад 23. Недопустимі оператори.
char cfc (char) {...} /* Визначення функції.*/
char cfi (int) {...} /* Визначення функції.*/
void vff (float) {...} /* Визначення функції.*/
int * ifc (char *){...} /* Визначення функції.*/
char (* ptof1) (int); /* Покажчик на функцію.*/
char (* ptof2) (int); /* Покажчик на функцію.*/
void (* ptof3) (float) = vff; /*ініціалізувати покажчик.*/
int main ()
{
ptof1 = cfc; /* Помилка - невідповідність сигнатур.*/
ptof2 = vff; /* Помилка - невідповідність типів (значень і сигнатур).*/
ptof1 = ifc; /* Помилка - невідповідність типів.*/
ptof1 = cfi; /* Правильно.*/
ptof2 = pt1; /* Правильно.*/
char c = (* ptof1) (44); /* Правильно.*/
c = (* ptof2) ('\ t'); /* Правильно.*/
}
Приклад 24. Відображення гнучкості механізму викликів функцій за допомогою покажчиків.
/* Функції одного типу з однаковими сигнатурами:*/
#include <stdio.h>
#include <stdlib.h>
int plus (int a, int b) {return a + b;}
int divide(int a, int b) {return a / b;}
int multiple (int a, int b) {return a * b;}
int subtruct (int a, int b) {return a - b;}
int main (){
int (* ptof) (int, int); /*îãîëîøåííÿ âêàç³âíèêà
int var1 = 12, var2 = 4;
char c = '+';
while (c != '\040'){
printf("\nArguments: var1 =%i, var2 =%i", var1, var2);
printf(". Result for c = \'%i\', ", c);
switch (c){
case '+':
ptof = plus;
c = '/';
break;
case '-':
ptof = subtruct;
c = ' ';
break;
case '*':
ptof = multiple;
c = '-';
break;
case '/':
ptof = divide;
c = '*';
break;
}
var1 = (* ptof) (var1, var2 );
printf("var1 = (* ptof) (var1, var2)\n", var1);
}
system("PAUSE");
return EXIT_SUCCESS;
}
Результати роботи:
Цикл триває, поки значенням змінної c не стане пробіл. У кожній ітерації покажчик ptof отримує адресу однієї з функцій, і змінюється значення c. За результатами програми легко простежити порядок виконання її операторів.
Найкраще відобразити суть застосування можна на прикладі передачі функції в функцію (це рідко використовується):
Приклад 25. Передача функції в функцію(рис. 5.11)
int add(int a, int b){ return (a+b); }
int subtraction (int a, int b){ return (a-b); }
int operation (int x, int y, int (*functocall)(int,int))
{
int g;
g = (*functocall)(x,y);
return (g);
}
int main ()
{
int m,n;
int (*minus)(int,int) = subtraction; /*оголошення та ініціалізація покажчика на ф-ю subtraction*/
m = operation (7, 5, addition);
n = operation (20, m, minus);
printf("n= %i, m= %i \n", n, m);}
Виконання:
Змінній m присвоюється значення функції operation в яку передаються три параметри: 7,5 і адреса на функцію addition. В функції operation ми присвоюємо значення виконання функції покажчик і параметрами , якими їй передали при виклику з головної програми:
Рисунок 5.11 – Зображення прикладу 25.
Результат роботи програми:
5.11 Покажчики на void
Покажчики на void –це покажчики спеціального типу. Він являє собою відсутність типу, тобто вказує на зміну що не має типу(невідома довжина і опції розіменовування).
Це дозволяє їм вказувати на будь-які дані: від char до double. Але на заміну вони мають великі обмеження: дані на які він вказує неможуть безпосередньо роіменовуватись(що є логічним , так як ми не маємо типу), з цієї причини ми повинні вказувати тип перд тим як розіменувати дані на які вказує цей покажчик.
Приклад 26. Покажчик на void.