Lection C - pointer _SorochakRoman.pptx
- Количество слайдов: 13
Вказівники в мові С Поняття вказівника та його використання
Що таке вказівник? • • • Оперативну пам’ять варто розглядати як масив послідовно пронумерованих комірок, з якими можна працювати по роздільності чи зв’язаними ділянками. Вказівник – це змінна, що містить адресу початку певної ділянки оперативної пам ’яті. Вказівник – це фізична сутність, а отже під нього виділяється пам’ять як і під будь-яку змінну. 1. 2. char *p; std: : cout << &p; //вказівник як і будь-яка змінна має свою адресу розміщення Розмір вказівника визначається розміром машинного слова заданої платформи. 1. 2. char *p; sizeof(p); //4//поверне розмір самого вказівника Оголошення вказівника : char *p. C; int *p. I; До будь-якої зміннлї можна застосувати операція взяття адреси - &; • • • char a; &a; 1. 2. 3. 4. 5. 6. //Оголошення вказівника р та змінної с char *p, c; //Запис до p адресу змінної с p = &c; //операція взяття адреси //звернення до с через вказівник р *p= 10; //розіменування вказівника – – • 1. 2. 1) кількість байт, що потрібно взяти від адреси-початку 2) як цю послідовність байт трактувати( цілочисельний тип, раціональний, структура, …) Вказівник може бути будь-якого типу. По-суті тип вказівника нам говорить про: Також є особливий тип вказівника void*. Вказівник такого типу містить лише адресу і до нього не може бути застосовано операцію розіменування. Для цього ми повинні використати явне привденення типів. 1. 2. 3. • //оператор & поверне адресу розміщення змінної a Наступні рядки коду описують сутність вказівника. void *p. V = &c; (char)p. V = 5; //приведення до типу char та звернення до ділянки, що містить с sizeof(p. V); //розмір безтипового вказівника як і будь-якого = 4 байти Висячий вказівник – вказівник, який посилається на неіснуючу змінну. Рекомендується присвоювати таким вказівникам NULL.
Операції над вказівниками. Адресна арифметика • Над вказівниками можна здійснити наступні операції: 1. Розіменування (*) – звернення до ділянки памяті за адресою, що міститься у вказівнику. *ptr = 5; //значення lvalue є посилання на комірку памяті, яке адресується цим вказівником sizeof(*ptr); //операція визначення розміру поверне кількість байт обєкту на який посилається ptr sizeof(ptr); // операція визначення розміру поверне розмір заданого вказівника 2. Присвоєння (=) – побітове копіювання вмістимого вказівника. При цьому здійснюється перевірка типів цих вказівників. • Важливо зазначити, що на етапі компіляції перевіряються типи лівої та правої частин відносно =, тобто char a, *p. C=&a; int *p. I; pi = p. C; призведе до помилки. Тут потрібне явне приведення до типу. • Обійти це обмеження простим копіюванням вмістимого вказівників, використавши функцію void memcpy(void *, const void *, size_t); Важливо зазначити, що ми копіюємо вмістиме вказівників, а не ділянки ОП, адреси яких вміщує один із вказівників. Тому ми передаємо адреси самих вказівників memcpy(&p. I, &p. C, sizeof(p. I)); 3. Порівняння (>, <, >=, <=, ==, !=) – результатом будь-якої операції є ненульове значення в разі істинності. if(p. I == (int*)p. C) { std: : cout << “p. I == p. C”} //потрібне явне приведення типу • Можна використати функцію int memcmp(const void*, vonst void*, size_t); , що порівнює побайтово і повертає, яка ділянка більша або 0 у разі рівності. if(!memcmp(&p. C, &p. I, 4) { printf(“p. C == p. I”); } //порівняння вмістимого вказівників без приведення типів 4. Збільшення/зменшення адрес – до вказівника можна додати чи відняти довільне ціле число ptr = ptr + 3; // ptr = ptr + 3*sizeof(*ptr) 5. Віднімання вказівників – різниця двох вказівників дасть кількість об’єктів, що може бути розміщено між об’єктами, на які посилаються задані вказівники. char arr[5], *p 1=mas, *p 2=&mas[3] printf(“%d”, ptr 2 – ptr 1); // 3
Кваліфікатор доступу const та volatile • Вказівники можуть бути незмінними ( const ), тобто можуть посилатися лише на один об’єкт, так і посилатися на константний об’єкт, тобто значенння обєкту не можна змінити за допомогою такого вказівника. int i, а; const int *p 1 = &i; //вказівник на константну змінну //*p 1 = 5; //помилка, вказівник не може змінити значення змінної, на яку посилається int * const p 2 = &i; //константний вказівник на змінну //int const *p 2; //помилка, обов’язково повинен бути проініцілізованим //p 2 = &a; //помилка; константний вказівник адресує лише одну змінну *p 2 = 3; • Вказівники на константні змінні використовують перш за все при передачі праметрів до функцій. В тілі функції заборонено змінювати змінні, на які послаються дані вказівники. void f(const int *p); //по суті передасться копія вказівника, що буде адресувати ту ж змінну типу • • Має зміст і такий код const int * const p=&a; , що оголошує константний вказівник на константну змінну. Якщо потрібно відмінити оптимізацію для якогось вказівника, то його можна оголосити як volatile. int * volatile p. V = &a; //такий вказівник не буде оптимізовано, а тому до нього буде доступ із інших потоків • Можна заборонити оптимізовувати змінну, на яку посилається заданий вказівник. volatile int b; volatile int * pv = &b; • • //volatile змінна //вказівник на volatile змінну Має зміст і такий код volatile int * volatile p; , що говорить, що не можна оптимізовувати ні сам вказівник ні змінну, на яку він посилається. Важливо зауважити, що const стосується можливості явного перезапису змінної. Volatile же стосується заборони оптимізації змінної. Тому наступний код не є суперечливим. const volatile int * const volatile p = &a; //константний неоптимізуючий вказівник на константну неоптимізуючу змінну типу int
Вказівники та масиви (1) • В С++, як і в ANSI C, ім'я масиву є адресою його першого елемента, тобто: – char ach[30]; // тут ach є адресою &ach[0]; • Це робить простою організацію вказівників на масиви даних Нехай задано int a[10]; – – – • Процес організаціі доступу до елементів масиву через вказівник складається з двох етапів: 1. 2. – • ініціалізація вказівника адресою першого чи останнього елемента масиву; використання циклічного оператора для доступу до елементів масива або маніпуляції адресою, яку містить вказівник. Зокрема, якщо j=&ach[0], то наступні звертання до ³-го елемента масиву ach є рівносильними ach[i] = j[i] = *(j+i); Зауважимо: хоча операції * i & мають найвищий пріоритет, проте операція *j++ приведе до значення елемента масиву з індексом збільшеним на одиницю (читається даний вираз справа наліво). Тобто, при умові j=&ach[0] – • int* pa; // перший варіант організації вказівника j на масив ach. Array pa=&a[0]; // другий варіант організації вказівника j на масив ach. Array pa=a; *j++ = ach. Array[1] = *(j+1) = *(j++). Для того щоб збільшити на одиницю значення елемента масиву, треба записати (*j)++.
Вказівники та масиви (2) • Звертання до елемента масиву за допомогою оператора індексації ach[i] вимагає проведеннянаступних операцій: 1. 2. • Обчислення вказівника (a+i) Розіменування отриманого вказівника *(a+i) Як наслідок звернення до елемента масиву можна провести за один крок. Нище показано фрагмент коду сортування бульбашкою. При кожній наступній ітерації вказівники вже посилатимуться на потрібні елементи. 1. 2. 3. 4. 5. 6. 7. 8. 9. for(int i = 0; i < n; i++) { for(int *p 1 = arr, *p 2 = p 1+1, j = 0; j < n-i-1; p 1++, p 2++, j++) { if (*p 1 > *p 2) { //arr[j] > arr[j+1] *p 1 += *p 2; //arr[j] += arr[j+1]; *p 2 = *p 1 - *p 2; //arr[j+1] = arr[j] - arr[j+1]; *p 1 = *p 1 - *p 2; //arr[j+1] = arr[j] - arr[j+1]; } } } • Рядкова константа “I’m a string” є масивом символів із додатковим термінальним нулем у кінці ‘ ’. • Потрібно відрізняти: char *pmessage = "now is the time"; /*вказівник*/ char amessage[] = "now is the time"; /* массив */ У першому випадку до pmessage запишеться адреса константного рядка. У другому ж створиться масив символів. Відповідно запис: *pmessage = ‘S’; призведе до помилки.
Вказівники і двовимірні масиви • Доступ до матриці (двовимірного масиву) організовується як масив вказівників на рядки, або як вказівник на вказівник, наприклад. 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. • #include
Приклад передавання подвійного масиву у функцію 1. #include
Приклад використання подвійного масиву Проблемний код 1. void print. Array(int** ttt, int dim 1, int dim 2) 2. { 3. int w, k; 26. 27. 28. } 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. cout <<"----------"<
Вказівники і структури • С++ допускає створення вказівників на структури і організовувати доступ до полів структури, незалежно від того чи це змінна чи функція, за допомогою операції доступу -> "стрілка", наприклад 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. • • struct { main() { My. Strt int i; int Mas[10]; // int Mas[10] = {1, 2}; My. Strt My; My. Strt* p=&My; (*p). i=1; for(int i = 0; i < 10; i++) p->Mas[i] = i; Заборонена дія Організація доступу через оператор розіменування «*-множення» та оператор доступу «. -крапка» . Дужки обов’язкові. Організація доступу через оператор доступу «-> - стрілка» } Якщо замість вказівника використовувати посилання, то оператор доступу -> "стрілка" треба замінити на оператор доступу. "крапка". Подібно до структур організовуються вказівники на класи та об'єднання.
Вказівники на функції • • • C++ підтримує мехазнім організації вказівників, які зберігають адресу функції, тобто адрес першого виконавчого оператора. Загальний синтаксис оголошення вказівника на функцію має вигляд – type (*pointer. Function)(param. List)[=function. Address]; Тоді загальний синтаксис виклику функції через вказівник виглядає так – (*pointer. Function)(param. List); При ініціалізації вказівників на функції іменем конкретної функції треба забезпечити відповідність типу, який повертає функція, i cписку її параметрів до оголошення вказівника. Після ініціалізації вказівника на функцію його можна використовувати для виклику цих функцій, наприклад 1. 2. int funct 1(int, double); int funct 2(int, double); 3. 4. 5. 6. 7. 8. 9. 10. 11. main() { int (*p)(int, double); p=funct 1; int i=p(1, 2); p=funct 2; i=p(1, 2); i=(*p)(1, 2); } 12. 13. 14. 15. int funct 1(int i 1, double i 2) { std: : cout << “nfunct 1: n”; return (i 1+int(i 2)); } 16. 17. 18. 19. int funct 2(int i 1, double i 2) { std: : cout << “nfunct 2: n”; return (i 1+int(i 2)); } // прототипи функцій // // // оголошення вказівника на функцію організація вказівника на funct 1 виклик funct 1 через вказівник переадресація вказівника на funct 2 виклик funct 2 через вказівник виклик в старому форматі // оголошення функцій
Використання вказівників для накладання інтерфейсів • • • Накласти інтерфейс на деяку ділянку пам’яті означає певним чином страктувати її, тобто до накладання інтерфейсу ми сприймаємо цю ділянку як послідовність байт, а після як послідовність змінних, що мають певний тип. Важливо зауважити, що ми можемо накладати інтерфейси вже на існуючі змінні. Для цього використовують вказівники. Наприклад: 1. 2. 3. 4. 5. • Для прикладу змінимо значення константної змінної 1. 2. 3. 4. 5. 6. • int * I=3; //змінна типу int std: : cout << I; //3 char *p. C = (char*)&I; //накладання інтерфейсу на перший байт змінної I *p. C = 5; //доступ до першого байта змінної і std: : cout << (int)*p. C; //5 const volatile int i = 7; std: : cout << i; int * p. I = (int*)&i; //I = 8; *p. I = 8; std: : cout << *p. I; //volatile потрібен для заборони оптимізації константи //7 //накладання інтерфейсу на існуючу змінну i //помилка. Значення константи заборонено змінювати явно! //доступ до ділянки ОП, де розташована змінна і та трактування її іншим інтерфейсом - int //8 Важливо зауважити, що даний код вимагає явного приведення типів (const int* до int*). Цього можна уникнути за допомогою функції void memcpy(void* p 1, const void*p 2, size_t size); , що просто перезаписує size байт ділянки ОП, на яку вказує p 1, із ділянки , на яку вказує p 2. Тут не виконується жодна з перевірок правильності приведення типів. 1. 2. const volatile int * p = &I; //вказівник на неоптимізуючу константу memcpy(&p. I, &p, sizeof(p)); //запис адреси, що містить p, до вказівника p. I
Динамічні змінні • У мові С є можливість виділяти пам’ять під час виконання програми. Ця можливість забезпечується функціями : – – – • • • Результатом роботи кожної із цих функцій є виділення деякої ділянки динамічної пам’мяті та повернення безтипового вказівника на початок цієї ділянки. При цьому це всього лиш набір байт без будь-якого типу. Для доступу до цієї пам’яті та її трактування потрібно мати хоча б один вказівник певного типу. Для прикладу: – – – • void* malloc(size_t size); //виділення динамічної пам’яті розміром size та повернення вказівника на цю область void* calloc(size_t num, size_t size); //виділення динамічної пам’яті розміром size*num для розміщення масиву із num едемнтів void* realloc(void *ptr, size_t newsize); //зміна обсягу ділянки динамічної пам’яті int *i = malloc(sizeof(*i)); *i = 5; char *arr = calloc(10, sizeof(*arr)); arr[5] = 6; std: : cout << *(arr+5); //розіменування //виділення дин. пам. розміром 4 байти та накладання на неї інтерфейсу типу int //звернення до 5 елемента масиву за допомогою оператора розіменування //виділення дин. пам. під масив типу char із 10 елементів //звернення до 5 елемента масиву за допомогою оператора індексації //звернення до 5 елемента масиву за допомогою адресної арифметики оператора Маючи один лише вказівник на будь-який тип, можливо і не відомий, можна виділити динамічну пам’ять під змінну цього типу. 1. 2. 3. 4. 5. 6. – struct{ //оголошення анонімної структури int a; char b; } *p; void *p. Tmp = malloc(sizeof(*p)); //виділення потрібної кількості байт під зберігання об’кту memcpy(&p, &p. Tmp, sizeof(p)); //накладння інтерфейсу на щойно виділену пам’ять Тут важливо зазначити, що не можна використати оператор = (присвоювання), оскільки нам потрібно явно вказати тип приведення, якого в нас немає.