Як працюють покажчики на покажчики в C? Коли ви використовували б їх?
Як працюють покажчики на покажчики в C? Коли ви використовували б їх?
Відповіді:
Припустимо, 8-розрядний комп'ютер з 8-бітовими адресами (і, таким чином, лише 256 байт пам'яті). Це частина цієї пам'яті (цифри вгорі - це адреси):
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
| | 58 | | | 63 | | 55 | | | h | e | l | l | o | \0 | |
+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+----+
Тут ви можете побачити, що з адреси 63 починається рядок "привіт". Тож у цьому випадку, якщо це єдине виникнення "привіт" у пам'яті,
const char *c = "hello";
... визначає cяк вказівник на рядок (тільки для читання) рядка "привіт", і, таким чином, містить значення 63. воно cповинно бути збережене десь: у прикладі вище в місці 58. Звичайно, ми не можемо вказувати лише на символи , а також на інші покажчики. Наприклад:
const char **cp = &c;
Тепер cpвказує на c, тобто містить адресу c(що становить 58). Ми можемо піти ще далі. Поміркуйте:
const char ***cpp = &cp;
Тепер cppзберігається адреса cp. Отже, воно має значення 55 (на підставі наведеного вище прикладу), і ви здогадалися: воно зберігається за адресою 60.
Щодо того, чому користуються вказівниками на покажчики:
t, посилання на масив має тип t *. Тепер розглянемо масив масивів типу t: природно, посилання на цей 2D масив матиме тип (t *)*= t **, а значить, вказівник на покажчик.char **.fпотрібно прийняти аргумент типу, t **якщо він повинен змінити змінну типу t *.Як працюють покажчики на покажчики в C?
Спочатку вказівник - це змінна, як і будь-яка інша змінна, але вона містить адресу змінної.
Вказівник на покажчик - це змінна, як і будь-яка інша змінна, але яка містить адресу змінної. Ця змінна як раз і є вказівником.
Коли ви використовували б їх?
Ви можете використовувати їх, коли вам потрібно повернути вказівник на деяку пам'ять на купі, але не використовувати значення повернення.
Приклад:
int getValueOf5(int *p)
{
*p = 5;
return 1;//success
}
int get1024HeapMemory(int **p)
{
*p = malloc(1024);
if(*p == 0)
return -1;//error
else
return 0;//success
}
І ви називаєте це так:
int x;
getValueOf5(&x);//I want to fill the int varaible, so I pass it's address in
//At this point x holds 5
int *p;
get1024HeapMemory(&p);//I want to fill the int* variable, so I pass it's address in
//At this point p holds a memory address where 1024 bytes of memory is allocated on the heap
Існують і інші способи використання, як і аргумент main () кожної програми C має вказівник на покажчик для argv, де кожен елемент містить масив символів, які є параметрами командного рядка. Ви повинні бути обережними, хоча, коли ви використовуєте покажчики покажчиків для вказівки на двовимірні масиви, краще замість цього використовувати вказівник на двовимірний масив.
Чому це небезпечно?
void test()
{
double **a;
int i1 = sizeof(a[0]);//i1 == 4 == sizeof(double*)
double matrix[ROWS][COLUMNS];
int i2 = sizeof(matrix[0]);//i2 == 240 == COLUMNS * sizeof(double)
}
Ось приклад вказівника на двовимірний масив, виконаний правильно:
int (*myPointerTo2DimArray)[ROWS][COLUMNS]
Ви не можете використовувати вказівник на двовимірний масив, хоча ви хочете підтримувати змінну кількість елементів для ROWS та COLUMNS. Але коли ви знаєте заздалегідь, ви будете використовувати двовимірний масив.
Мені подобається цей "реальний світ" приклад коду вказівника на використання вказівника, в Git 2.0, виконувати 7b1004b :
Колись Лінус сказав:
Я дійсно бажаю, щоб більше людей зрозуміли дійсно кодування низького рівня кодування. Не великі, складні речі, такі як пошук без замкових імен, але просто хороше використання покажчиків на покажчики тощо.
Наприклад, я бачив занадто багато людей, які видаляють запис, пов’язаний окремо із списку, відслідковуючи запис "prev" , а потім видалити запис, зробивши щось подібне
if (prev)
prev->next = entry->next;
else
list_head = entry->next;
і коли я бачу такий код, я просто переходжу "Ця людина не розуміє покажчиків". І це, на жаль, досить часто.
Люди, які розуміють покажчики, просто використовують " вказівник на вхідний покажчик " та ініціалізують його з адресою list_head. А потім, переходячи список, вони можуть видалити запис, не використовуючи жодних умовних умов, просто зробивши
*pp = entry->next

Застосовуючи це спрощення, ми можемо втратити 7 рядків від цієї функції, навіть додаючи 2 рядки коментарів.
- struct combine_diff_path *p, *pprev, *ptmp;
+ struct combine_diff_path *p, **tail = &curr;
Кріс вказує у коментарі до відео у 2016 році " Проблема подвійного покажчика Лінуса Торвальдса " Філіпа Баука .
Кумар вказує в коментарі до блогу " Лінус про розуміння покажчиків ", де Гриша Трубецький пояснює:
Уявіть, що у вас пов'язаний список, визначений як:
typedef struct list_entry {
int val;
struct list_entry *next;
} list_entry;
Потрібно перебрати його від початку до кінця та видалити конкретний елемент, значення якого дорівнює значенню to_remove.
Більш очевидний спосіб це зробити:
list_entry *entry = head; /* assuming head exists and is the first entry of the list */
list_entry *prev = NULL;
while (entry) { /* line 4 */
if (entry->val == to_remove) /* this is the one to remove ; line 5 */
if (prev)
prev->next = entry->next; /* remove the entry ; line 7 */
else
head = entry->next; /* special case - first entry ; line 9 */
/* move on to the next entry */
prev = entry;
entry = entry->next;
}
Ми робимо вище:
- ітерація над списком до вступу
NULL, це означає, що ми дійшли до кінця списку (рядок 4).- Коли ми стикаємося із записом, який ми хочемо видалити (рядок 5),
- ми присвоюємо значення поточного наступного вказівника попередньому,
- таким чином усуваючи поточний елемент (рядок 7).
Вище є окремий випадок - на початку ітерації немає попереднього запису (
prevєNULL), і щоб видалити перший запис у списку, вам слід змінити саму головку (рядок 9).Що сказав Лінус, це те, що вищезазначений код можна спростити, зробивши попередній елемент вказівником на покажчик, а не просто на вказівник .
Потім код виглядає приблизно так:
list_entry **pp = &head; /* pointer to a pointer */
list_entry *entry = head;
while (entry) {
if (entry->val == to_remove)
*pp = entry->next;
pp = &entry->next;
entry = entry->next;
}
Вищевказаний код дуже схожий на попередній варіант, але зауважте, як нам більше не потрібно спостерігати за окремим випадком першого елемента списку, оскільки
ppце неNULLна початку. Простий і розумний.Також хтось із цієї теми прокоментував, що причина цього краща -
*pp = entry->nextце атомна. Це, звичайно, НЕ атомний .
Вищенаведений вираз містить два оператори зриву (*і->) та одне призначення, і жодна з цих трьох речей не є атомною.
Це поширена помилка, але , на жаль , майже нічого в C ніколи не слід вважати атомними ( в тому числі++і--операторів)!
Під час висвітлення покажчиків на курсі програмування в університеті нам дали два підказки, як почати вивчати їх. Першим було переглянути Pointer Fun With Binky . Друге - було подумати про перехід Очних птахів з «Любляни Керролл» через « Зеркальне скло»
"Ви сумуєте", - тривожно промовив Лицар: - Дозвольте заспівати вам пісню, щоб потішити вас.
"Це дуже довго?" - спитала Аліса, бо вона того дня почула багато віршів.
- Давно, - сказав лицар, - але це дуже, дуже красиво. Усі, хто чує мене, це співають - або це приносить сльози їхнім очам, або ж - "
"Або ще що?" - сказала Аліса, бо Лицар зробив раптову паузу.
"Або ще ні, ви знаєте. Назва пісні називається "Зірки очей". "
"О, так називається пісня, чи не так?", - сказала Аліса, намагаючись відчути інтерес.
"Ні, ви не розумієте", - сказав Лицар, трохи розчулено. «Саме так називається ім’я. Назва справді - "У віці людина у віці". "
"Тоді я повинен був сказати:" Ось як називається пісня "?" Аліса поправила себе.
"Ні, ви не повинні: це зовсім інша річ! Пісня називається "Шляхи та засоби": але це лише те, що вона називається, ви знаєте! "
"Ну, що ж це за пісня?" - сказала Аліса, яка до цього часу була зовсім збентежена.
"Я прийшов до цього", - сказав Лицар. "Пісня насправді -" Сидіти біля воріт ": і мелодія - це власний винахід".
Ви можете прочитати це: Покажчики на покажчики
Сподіваюся, що це допомагає з’ясувати деякі основні сумніви.
Коли потрібна посилання на покажчик. Наприклад, коли ви бажаєте змінити значення (адресу, вказану на) змінної вказівника, оголошеної в області викликової функції всередині викликаної функції.
Якщо ви передаєте один вказівник в якості аргументу, ви будете змінювати локальні копії вказівника, а не оригінальний покажчик в області виклику. За допомогою вказівника на вказівник ви змінюєте останнє.
Вказівник на покажчик також називається ручкою . Одне використання для нього часто - це коли об'єкт можна переміщувати в пам'яті або вилучати. Один з них часто відповідає за блокування та розблокування використання об'єкта, щоб він не переміщувався під час доступу до нього.
Він часто використовується в середовищі з обмеженою пам'яттю, тобто в Palm OS.
Розглянемо рисунок і програму нижче, щоб краще зрозуміти це поняття .
Відповідно до рисунка, ptr1 - це єдиний покажчик, який має адресу змінної num .
ptr1 = #
Аналогічно, ptr2 - покажчик на покажчик (подвійний вказівник), який має адресу вказівника ptr1 .
ptr2 = &ptr1;
Вказівник, який вказує на інший покажчик, відомий як подвійний вказівник. У цьому прикладі ptr2 - подвійний покажчик.
Значення з наведеної діаграми:
Address of variable num has : 1000
Address of Pointer ptr1 is: 2000
Address of Pointer ptr2 is: 3000
Приклад:
#include <stdio.h>
int main ()
{
int num = 10;
int *ptr1;
int **ptr2;
// Take the address of var
ptr1 = #
// Take the address of ptr1 using address of operator &
ptr2 = &ptr1;
// Print the value
printf("Value of num = %d\n", num );
printf("Value available at *ptr1 = %d\n", *ptr1 );
printf("Value available at **ptr2 = %d\n", **ptr2);
}
Вихід:
Value of num = 10
Value available at *ptr1 = 10
Value available at **ptr2 = 10
це вказівник на значення адреси вказівника. (це жахливо я знаю)
в основному, це дозволяє передати вказівник на значення адреси іншого вказівника, так що ви можете змінити, де інший вказівник вказує з підфункції, наприклад:
void changeptr(int** pp)
{
*pp=&someval;
}
Вказівник на вказівник - це, ну, вказівник на вказівник.
Важливим прикладом someType ** є двовимірний масив: у вас є один масив, заповнений покажчиками на інші масиви, тому коли ви пишете
dpointer [5] [6]
Ви отримуєте доступ до масиву, який містить вказівники до інших масивів у його 5-му положенні, отримуєте вказівник (нехай fpointer його ім'я), а потім отримують доступ до 6-го елемента масиву, на який посилається цей масив (так, fpointer [6]).
Як це працює: це змінна, яка може зберігати інший покажчик.
Коли ви використовуєте їх: Багато хто використовує один з них, якщо ваша функція хоче побудувати масив і повернути його абоненту.
//returns the array of roll nos {11, 12} through paramater
// return value is total number of students
int fun( int **i )
{
int *j;
*i = (int*)malloc ( 2*sizeof(int) );
**i = 11; // e.g., newly allocated memory 0x2000 store 11
j = *i;
j++;
*j = 12; ; // e.g., newly allocated memory 0x2004 store 12
return 2;
}
int main()
{
int *i;
int n = fun( &i ); // hey I don't know how many students are in your class please send all of their roll numbers.
for ( int j=0; j<n; j++ )
printf( "roll no = %d \n", i[j] );
return 0;
}
Я створив 5-хвилинне відео, яке пояснює, як працюють покажчики:
Там так багато корисних пояснень, але я не знайшов лише короткий опис, так що ..
В основному вказівник - це адреса змінної. Короткий підсумковий код:
int a, *p_a;//declaration of normal variable and int pointer variable
a = 56; //simply assign value
p_a = &a; //save address of "a" to pointer variable
*p_a = 15; //override the value of the variable
//print 0xfoo and 15
//- first is address, 2nd is value stored at this address (that is called dereference)
printf("pointer p_a is having value %d and targeting at variable value %d", p_a, *p_a);
Також корисну інформацію можна знайти в темі, що означає довідник та відсторонення
І я не настільки впевнений, коли корисні покажчики можуть бути корисними, але, як правило, їх потрібно використовувати, коли ви робите певний розподіл пам'яті вручну / динаміку - malloc, calloc тощо.
Тож сподіваюся, що це також допоможе з’ясувати проблематику :)