Покажчики на С: коли використовувати амперсанд та зірочку?


298

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

Коли я повинен використовувати &і *?


5
Проілюструйте, як ви бачите речі, які іноді працюють по-іншому. Інакше ми мусимо здогадатися, що це вас бентежить.
Дрю Дорманн

1
Погодьтеся з Нілом Баттерворт. Напевно, я отримаю набагато більше інформації, отримуючи її з перших рук з книги, і пояснення K&R цілком зрозуміло.
Том

144
Я не погоджуюся з усіма вами, хто каже, що не дуже гарно ставити такі питання на питаннях. SO стала ресурсом номер 1 при пошуку в Google. Ви не даєте достатньо кредиту на ці відповіді. Прочитайте відповідь Дена Олсона. Ця відповідь справді прониклива і неймовірно корисна для новачків. RTFMє недоцільним і, відверто кажучи, дуже грубим. Якщо у вас немає часу відповісти, то поважайте тих, хто потребує часу, щоб відповісти на ці запитання. Я б хотів, щоб я міг це зробити "anon", але очевидно, що він / вона не має часу зробити внесок у будь-який змістовний спосіб.
SSH Це

18
Що сказав SSH Це абсолютно правда. Деякі люди кричать "Просто Google це", але сьогодні це навпаки: "Подивіться на StackOverflow". Це питання корисне багатьом людям. (Звідси upvotes та novovotes.)
Імператор MC

Відповіді:


610

У вас є вказівники та значення:

int* p; // variable p is pointer to integer type
int i; // integer value

Ви перетворюєте вказівник на значення за допомогою *:

int i2 = *p; // integer i2 is assigned with integer value that pointer p is pointing to

Ви перетворюєте значення в покажчик за допомогою &:

int* p2 = &i; // pointer p2 will point to the address of integer i

Редагувати: У випадку з масивами вони трактуються дуже як вказівники. Якщо ви вважаєте їх покажчиками, ви будете використовувати *для отримання значень всередині них, як пояснено вище, але існує й інший, більш поширений спосіб використання []оператора:

int a[2];  // array of integers
int i = *a; // the value of the first element of a
int i2 = a[0]; // another way to get the first element

Щоб отримати другий елемент:

int a[2]; // array
int i = *(a + 1); // the value of the second element
int i2 = a[1]; // the value of the second element

Отже, []оператор індексації - це особлива форма *оператора, і вона працює так:

a[i] == *(a + i);  // these two statements are the same thing

2
Чому це не працює? int aX[] = {3, 4}; int *bX = &aX;
Пітер

5
Масиви є спеціальними і можуть бути перетворені в покажчики прозоро. Це підкреслює ще один спосіб переходу від вказівника до значення, я додаю його до пояснення вище.
Ден Олсон

4
Якщо я правильно це зрозумів ... приклад int *bX = &aX;не працює, тому що aXвже повертає адресу aX[0](тобто &aX[0]), тож &aXвийде адреса адреси, яка не має сенсу. Це правильно?
Пітер

6
Ви маєте рацію, хоча є випадки, коли ви, можливо, хочете адресу адреси. У цьому випадку ви оголосите це як int ** bX = & aX, але це більш досконала тема.
Ден Олсон

3
@Dan, враховуючи int aX[] = {3,4};, int **bX = &aX;є помилкою. &aXмає тип "покажчик на масив [2] з int", а не "вказівник на масив int". Зокрема, ім'я масиву не розглядається як вказівник на його перший елемент для унарного &. Можна зробити:int (*bX)[2] = &aX;
Алок Сінгал

28

Існує закономірність роботи з масивами та функціями; спочатку просто трохи важко побачити

Маючи справу з масивами, корисно пам’ятати наступне: коли вираз масиву з’являється у більшості контекстів, тип виразу неявно перетворюється з «N-елементного масиву T» в «вказівник на T», а його значення встановлюється щоб вказати на перший елемент масиву. Винятки з цього правила - це коли вираз масиву відображається як операнд &або sizeofоператорів, або коли це рядковий літерал, який використовується як ініціалізатор в декларації.

Таким чином, коли ви викликаєте функцію з виразом масиву як аргумент, функція отримає вказівник, а не масив:

int arr[10];
...
foo(arr);
...

void foo(int *arr) { ... }

Ось чому ви не використовуєте &оператора для аргументів, що відповідають "% s" у scanf():

char str[STRING_LENGTH];
...
scanf("%s", str);

Через неявне перетворення scanf()отримує char *значення, яке вказує на початок strмасиву. Це справедливо для будь-якої функції, яка викликається виразом масиву як аргументу (майже про будь-яку з str*функцій *scanfта *printfфункцій тощо).

На практиці ви, ймовірно, ніколи не викличете функцію з виразом масиву за допомогою &оператора, як у:

int arr[N];
...
foo(&arr);

void foo(int (*p)[N]) {...}

Такий код не дуже поширений; ви повинні знати розмір масиву в оголошенні функції, і функція працює лише з покажчиками на масиви певних розмірів (вказівник на 10-елементний масив T - це інший тип, ніж вказівник на 11-елементний масив з T).

Коли вираз масиву з'являється як операнд для &оператора, тип результуючого виразу є "вказівник на N-елементний масив T", або T (*)[N], який відрізняється від масиву покажчиків ( T *[N]) та вказівника на базовий тип ( T *).

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

Пам'ятайте, що C передає всі аргументи функції за значенням; формальний параметр отримує копію значення фактичного параметра, а будь-які зміни формального параметра не відображаються у фактичному параметрі. Загальний приклад - функція swap:

void swap(int x, int y) { int tmp = x; x = y; y = tmp; }
...
int a = 1, b = 2;
printf("before swap: a = %d, b = %d\n", a, b);
swap(a, b);
printf("after swap: a = %d, b = %d\n", a, b);

Ви отримаєте такий вихід:

перед свопом: a = 1, b = 2
після swap: a = 1, b = 2

Формальні параметри xі yє різними об'єктами від aі b, тому змінюються xі yне відображаються в aі b. Оскільки ми хочемо змінити значення aі b, ми повинні передати їм покажчики на функцію swap:

void swap(int *x, int *y) {int tmp = *x; *x = *y; *y = tmp; }
...
int a = 1, b = 2;
printf("before swap: a = %d, b = %d\n", a, b);
swap(&a, &b);
printf("after swap: a = %d, b = %d\n", a, b);

Тепер ваш вихід буде

перед свопом: a = 1, b = 2
після swap: a = 2, b = 1

Зауважте, що в функції swap ми змінюємо не значення, xа y, а значення, на що xі y вказують . Написання в *xвідрізняється від письма до x; ми не оновлюємо значення xсаме по собі, отримуємо місцезнаходження xта оновлюємо значення в цьому місці.

Це однаково вірно, якщо ми хочемо змінити значення вказівника; якщо ми пишемо

int myFopen(FILE *stream) {stream = fopen("myfile.dat", "r"); }
...
FILE *in;
myFopen(in);

то ми змінюємо значення вхідного параметра stream, а не на що stream вказує , тому зміна streamне впливає на значення in; для того, щоб це працювало, ми повинні перейти вказівник до вказівника:

int myFopen(FILE **stream) {*stream = fopen("myFile.dat", "r"); }
...
FILE *in;
myFopen(&in);

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

int arr[N];
init(arr, N);
...
void init(int *arr, int N) {size_t i; for (i = 0; i < N; i++) arr[i] = i*i;}

Зауважте, що об’єкти масиву можуть не бути призначені; тобто ви не можете зробити щось подібне

int a[10], b[10];
...
a = b;

тому ви хочете бути обережними, коли ви маєте справу з покажчиками на масиви; щось на зразок

void (int (*foo)[N])
{
  ...
  *foo = ...;
}

не буде працювати.


16

Простіше кажучи

  • &означає адресу , ви побачите, що в заповнювачах заповнення функцій для зміни змінної параметра, як у С, змінні параметри передаються за значенням, використовуючи засоби ampersand для передачі посилання.
  • *означає дереференцію змінної вказівника, тобто отримувати значення цієї змінної вказівника.
int foo(int *x){
   *x++;
}

int main(int argc, char **argv){
   int y = 5;
   foo(&y);  // Now y is incremented and in scope here
   printf("value of y = %d\n", y); // output is 6
   /* ... */
}

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

int foo(int x){
   x++;
}

int main(int argc, char **argv){
   int y = 5;
   foo(y);  // Now y is still 5
   printf("value of y = %d\n", y); // output is 5
   /* ... */
}

Ось ілюстрація використання розвідки

int main(int argc, char **argv){
   int y = 5;
   int *p = NULL;
   p = &y;
   printf("value of *p = %d\n", *p); // output is 5
}

Вищеописане ілюструє, як ми отримали адресу y та призначили її змінній вказівника p. Тоді ми знімаємо p відновлення, прикріпивши його *до передньої частини, щоб отримати значення p, тобто *p.


10

Так, це може бути досить складним, оскільки *використовується для багатьох цілей у C / C ++.

Якщо *з'являється перед уже оголошеною змінною / функцією, це означає або що:

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

Якщо *з'являється у змінній або оголошенні функції, це означає, що ця змінна є покажчиком:

int int_value = 1;
int * int_ptr; //can point to another int variable
int   int_array1[10]; //can contain up to 10 int values, basically int_array1 is an pointer as well which points to the first int of the array
//int   int_array2[]; //illegal, without initializer list..
int int_array3[] = {1,2,3,4,5};  // these two
int int_array4[5] = {1,2,3,4,5}; // are identical

void func_takes_int_ptr1(int *int_ptr){} // these two are identical
void func_takes int_ptr2(int int_ptr[]){}// and legal

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

Якщо він &відображається перед уже оголошеною змінною, він повертає адресу цієї змінної

Крім того, ви повинні знати, що, передаючи масив функції, вам також завжди доведеться передавати розмір масиву цього масиву, за винятком випадків, коли масив є чимось на зразок 0-закінченого cstring (char масиву).


1
@akmozo s / func_takes int_ptr2 / func_takes_int_ptr2 / (недійсний пробіл)
PixnBits

4

Коли ви оголошуєте змінну вказівника або параметр функції, використовуйте *:

int *x = NULL;
int *y = malloc(sizeof(int)), *z = NULL;
int* f(int *x) {
    ...
}

Примітка: кожна заявлена ​​змінна потребує свого *.

Коли ви хочете взяти адресу значення, використовуйте &. Коли ви хочете прочитати або записати значення у вказівник, використовуйте *.

int a;
int *b;
b = f(&a);
a = *b;

a = *f(&a);

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

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


4

Я переглядав усі багатослівні пояснення, тому замість цього звернувся до відео з університету Нового Південного Уельсу для порятунку. Ось просте пояснення: якщо у нас є клітина, яка має адресу xта значення 7, непрямим способом запитати адресу значення 7є &7і непрямий спосіб попросити значення за адресою xє *x.so (cell: x , value: 7) == (cell: &7 , value: *x).Another спосіб дивитися на це: Johnсидить 7th seat.the *7th seatвкажуть Johnі &Johnпередасть address/ розташування 7th seat. Це просте пояснення мені допомогло і сподіваюся, що воно допоможе і іншим. Ось посилання на відмінне відео: натисніть тут.

Ось ще один приклад:

#include <stdio.h>

int main()
{ 
    int x;            /* A normal integer*/
    int *p;           /* A pointer to an integer ("*p" is an integer, so p
                       must be a pointer to an integer) */

    p = &x;           /* Read it, "assign the address of x to p" */
    scanf( "%d", &x );          /* Put a value in x, we could also use p here */
    printf( "%d\n", *p ); /* Note the use of the * to get the value */
    getchar();
}

Надбудова: Завжди ініціалізуйте покажчик перед їх використанням. Якщо ні, вказівник вказуватиме на що-небудь, що може призвести до збоїв програми, оскільки операційна система не дозволить вам отримати доступ до пам’яті, яка знає, що ви не володієте. Але просто поставте p = &x;, ми призначаємо вказівник конкретному розташуванню.


3

Насправді у вас це погладжено, нічого більше вам не потрібно знати :-)

Я просто додам наступні біти:

  • дві операції є протилежними кінцями спектра. &приймає змінну і дає вам адресу, *приймає адресу і дає вам змінну (або вміст).
  • масиви "деградують" до покажчиків, коли ви передаєте їх функції.
  • ви можете насправді мати декілька рівнів щодо непрямості ( char **pозначає, що pце вказівник на покажчик на a char.

Що стосується речей, які працюють інакше, а не насправді:

  • масиви, як уже згадувалося, деградують до покажчиків (на перший елемент масиву) при передачі функцій; вони не зберігають інформацію про розмір.
  • в C немає рядків, просто масиви символів, які, за умовою, представляють рядок символів, що закінчується нульовим ( \0) символом.
  • Коли ви передаєте адресу змінної функції, ви можете скасувати посилання на вказівник, щоб змінити саму змінну (як правило, змінні передаються за значенням (крім масивів)).

3

Я думаю, ти трохи розгублений. Ви повинні прочитати хороший підручник / книгу про покажчики.

Цей підручник дуже хороший для початківців (чітко пояснює, що &і що таке *). І так, не забудьте прочитати книгу « Покажчики на С » Кеннета Ріка.

Різниця між &і *дуже зрозуміла.

Приклад:

#include <stdio.h>

int main(){
  int x, *p;

  p = &x;         /* initialise pointer(take the address of x) */
  *p = 0;         /* set x to zero */
  printf("x is %d\n", x);
  printf("*p is %d\n", *p);

  *p += 1;        /* increment what p points to i.e x */
  printf("x is %d\n", x);

  (*p)++;         /* increment what p points to i.e x */
  printf("x is %d\n", x);

  return 0;
}

1

Гаразд, схоже, що ваша публікація була відредагована ...

double foo[4];
double *bar_1 = &foo[0];

Подивіться, як ви можете використати, &щоб отримати адресу початку структури масиву? Наступне

Foo_1(double *bar, int size){ return bar[size-1]; }
Foo_2(double bar[], int size){ return bar[size-1]; }

зробить те саме.


Питання було позначено C не C ++.
Prasoon Saurav

1
І я зняв
кривду
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.