اش

اشاره‌گر در C

{one: "۱ تمرین", other: "%{count} تمرین"}

درباره‌ی اشاره‌گر

«اشاره‌گر» متغیری یا ثابتی است که مقدارش نشانی متغیر یا ثابت دیگری است. اشاره‌گرها ویژگی‌ای قدرتمند و بالقوه خطرناک در C هستند. درک اشاره‌گرها می‌تواند چالش‌برانگیز باشد.

int i = 5;

وقتی مقداری را به یک متغیر اختصاص می‌دهیم، آن‌گاه می‌توانیم دو پرسش بپرسیم.

شما کی هستید؟

من i هستم.

چه دارید؟

من عدد صحیح 5 را دارم.

پرسش سومی هم هست که می‌توانیم بپرسیم.

کجا هستید؟

من در 0x6dfed4 هستم.

صبر کنید، چی؟

متغیرها و ثابت‌ها مقدارشان را در یک مکان حافظه ذخیره می‌کنند. آن مکان در حافظه به‌عنوان نشانی متغیر یا ثابت شناخته می‌شود.

عملگر نشانی و عملگر غیرمستقیم (بخش ۱)

برای به دست آوردن نشانی یک متغیر، از «عملگر نشانی» استفاده می‌کنیم که & است.

int i = 5;
int *i_address = &i;

صبر کنید، آن * پیش از i_address چه معنایی دارد؟ آیا معنایش ضرب نیست؟

یک نوع داده که پس از آن یک * و سپس یک شناسه بیاید، روش اعلان اشاره‌گر به متغیری از آن نوع است. * به‌عنوان «عملگر غیرمستقیم» شناخته می‌شود، چون به‌طور غیرمستقیم به مقداری اشاره می‌کند که اشاره‌گر نشانی آن را نگه می‌دارد.

بسیار خوب، اما چرا متغیری بسازیم که فقط نشانی متغیر دیگری را نگه دارد؟

اصلاً چرا باید برایمان مهم باشد که نشانی یک متغیر چیست؟

خب، وقتی متغیرها در یک آرایه‌اند، برایمان مهم است که کجا قرار دارند.

آرایه و اشاره‌گر

وقتی آرایه‌ای به این شکل می‌سازیم

int my_ints[] = {1, 2};

مقدار my_ints در واقع نشانی آن است که تصادفاً با نشانی اولین عنصرش هم یکسان است. نشانی دومین عنصر آن، نشانی اولین عنصر به‌علاوه‌ی اندازه‌ی یک int روی سیستم خواهد بود.

برای اینکه این را بهتر ببینید، اجرای کد زیر را امتحان کنید

#include <stdio.h>

int main() {
    // my_int's value is its address
    int my_ints[] = {1, 2};
    
    // since my_ints is already an address
    // we don't need the address operator to make a pointer from it.
    // a pointer to my_ints holds the address of my_ints
    int *my_ptr = my_ints;
    
    // the address of my_ints and the value of my_ptr are the same
    printf("my_ints is %p. my_ptr is %p.\n", my_ints, my_ptr);
    
    // the address of the first element is the same as the values in the line above
    printf("&my_ints[0] is %p. my_ptr + 0 is %p.\n", &my_ints[0], my_ptr + 0);
    
    // the address of the second element has increased by the size of an int
    printf("&my_ints[1] is %p. my_ptr + 1 is %p.\n", &my_ints[1], my_ptr + 1); // 
    
    return 0;
}

حساب اشاره‌گر، یا چرا اندیس آرایه از ۰ شروع می‌شود

مثال پیشین نشان می‌دهد چرا اندیس‌گذاری آرایه از 0 شروع می‌شود و نه از 1. اندیس درون آرایه، اندیس را در اندازه‌ی نوع ضرب می‌کند و آن را به نشانی شروع آرایه می‌افزاید. چون اولین عنصر با ابتدای آرایه هم‌راستا است، برای رسیدن به اولین عنصر نوع را در 0 ضرب می‌کنیم و به نشانی شروع می‌افزاییم. برای رسیدن به دومین عنصر نوع را در 1 ضرب می‌کنیم و به نشانی شروع می‌افزاییم. اگر طول نوع چهار بایت باشد، نشانی دومین عنصر چهار بایت بالاتر از اولین عنصر خواهد بود.

همچنین به همین دلیل است که اشاره‌گرها از یک نوع مشخص اعلان می‌شوند. اگر اشاره‌گر نمی‌دانست به چه نوعی اشاره می‌کند، نمی‌دانست هنگام افزودن 1 به آن چند بایت جلو برود. افزودن 1 به یک اشاره‌گر لزوماً به معنای افزودن یک بایت به نشانی نیست. به این معناست که 1 ضربدر طول نوع آن بر حسب بایت را اضافه می‌کنیم.

اما صبر کنید، ادامه دارد! فکر نمی‌کردید که کارمان با * تمام شده، مگر نه؟

عملگر غیرمستقیم (بخش ۲)

حالا که می‌دانیم نشانی‌ها با اندیس‌گذاری آرایه چگونه کار می‌کنند، اگر مقدار ذخیره‌شده در یک نشانی را به دست نیاوریم، چه فایده‌ای دارد؟ چطور این کار را می‌کنیم؟ برای به دست آوردن مقدار ذخیره‌شده در یک نشانی از عملگر غیرمستقیم استفاده می‌کنیم که شاید یادتان باشد * است. در این حالت، عملگر غیرمستقیم را گاهی «عملگر اشاره‌زدایی» هم می‌نامند، چون مقداری را اشاره‌زدایی می‌کند که اشاره‌گر نشانی آن را دارد. نامیدن آن با عنوان عملگر اشاره‌زدایی هنگام اشاره‌زدایی، آن را از عملگر غیرمستقیم برای تعریف اشاره‌گر متمایز می‌کند، اما * در هر دو کاربرد از نظر فنی عملگر غیرمستقیم نامیده می‌شود.

نه بابا! شوخی می‌کنید؟ * چند کاربرد دارد؟! ضرب، اعلان اشاره‌گر و حالا هم اشاره‌زدایی! گیج‌کننده است! و اصلاً اشاره‌زدایی یعنی چه؟

مثالی نشان می‌دهد چگونه از عملگر غیرمستقیم برای اشاره‌زدایی استفاده کنیم

#include <stdio.h>

int main() {
    // define the variable
    int my_ints[] = {1, 2};
    
    // define the pointer from the address of the variable
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *(my_ptr + 0) is 1.
    printf("my_ints[0] is %u. *(my_ptr + 0) is %u.\n", my_ints[0], *(my_ptr + 0));
    
    // prints: my_ints[1] is 2. *(my_ptr + 1) is 2.
    printf("my_ints[1] is %u. *(my_ptr + 1) is %u.\n", my_ints[1], *(my_ptr + 1));
    
    return 0;
}

استفاده از پرانتز می‌تواند به رفع ابهام در نیت هنگام حساب اشاره‌گر کمک کند. مثال زیر ممکن است آنچه برنامه‌نویس قصد داشته چاپ نکند

#include <stdio.h>

int main() {
    int my_ints[] = {1, 42};
    int *my_ptr = my_ints;
    
    // prints: my_ints[0] is 1. *++my_ptr is 42.
    printf("my_ints[0] is %u. *++my_ptr is %u.\n", my_ints[0], *++my_ptr);
    
    // prints: my_ints[1] is 43. ++*my_ptr++ is 43.
    printf("my_ints[1] is %u. ++*my_ptr++ is %u.\n", my_ints[1], ++*my_ptr);

    return 0;
}

هم * و هم ++ یک اولویت دارند، اما از راست به چپ ارزیابی می‌شوند. در خط اول، my_ptr ابتدا به دومین عنصر افزایش می‌یابد و سپس اشاره‌زدایی می‌شود. در خط دوم، my_ptr که در خط اول به دومین عنصر افزایش یافته بود، ابتدا اشاره‌زدایی می‌شود و سپس مقدار اشاره‌زدایی‌شده افزایش می‌یابد. اگر این خط را به مثال بالا اضافه کنیم

// prints: my_ints[1] is 43. *++my_ptr is (some garbage value).
printf("my_ints[1] is %u. *++my_ptr is %u.\n", my_ints[1], *++my_ptr);

در آن خط، اشاره‌گر را تا فراتر از آرایه افزایش داده‌ایم. از آرایه فراتر رفته‌ایم و به چهار بایت بعدی در حافظه اشاره می‌کنیم که اشاره‌گر آن‌ها را یک int تفسیر می‌کند، اما در واقع آن بایت‌ها می‌توانند به متغیر یا متغیرهای دیگری تعلق داشته باشند. آن چهار بایت ممکن است بخشی از یک double باشند یا چهار char باشند. مشکل این است که اصلاً نباید با اشاره‌گرمان به آن‌ها نگاه کنیم.

می‌توانستیم از این استفاده کنیم

// prints: my_ints[2] is (some garbage value).
printf("my_ints[2] is %u", my_ints[2]);

چون اندیس‌گذاری آرایه با یک اشاره‌گر پیاده‌سازی می‌شود، همان خطر فراتر رفتن از آرایه هم وجود دارد.

و چون یک رشته آرایه‌ای از charهاست، اشاره‌گرها را می‌توان با رشته‌ها هم به کار برد. غالباً یک رشته را با نحوه‌ی نگارش اشاره‌گر اعلان می‌کنند، به این شکل

char *my_string = "This is my string";
// instead of
// char my_string[] = "This is my string";

اما یک پرسش. چرا ستاره را کنار شناسه می‌گذاریم؟ چرا آن را کنار نوع نگذاریم، چون اشاره‌گری به آن نوع است؟ چون این کد

int* my_ptr1, my_ptr2_that_is_really_an_int;

دو اشاره‌گر به یک int اعلان نمی‌کند، بلکه یک اشاره‌گر به int و یک int اعلان می‌کند. برای اعلان دو اشاره‌گر به یک int به این شکل عمل می‌کنیم

int *my_ptr1, *my_ptr2;

بنابراین خوب است * را نزدیک شناسه بگذاریم تا ابهام به کمترین حد برسد. یا در هر خط فقط یک شناسه اعلان کنیم. و اگر هنوز از * سیر نشده‌اید، خوش آمدید به...

اشاره‌گر به اشاره‌گر

غالب است که آرایه‌های رشته به این شکل نمایش داده شوند

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    printf("%s\n", *passed_in_strings);
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char *my_strings[] = {"One string", "Two string"};
    print_strings(my_strings);
    return 0;
}

ممکن است واضح نباشد، اما آرایه‌ی رشته‌ها به‌عنوان اشاره‌گری به آرایه‌ای از آرایه‌های char اعلان و مقداردهی اولیه می‌شود. وقتی آرایه‌ی آرایه‌های char به print_strings() پاس داده می‌شود، امضا آن را اشاره‌گری به اشاره‌گر(ها) به char توصیف می‌کند.

پس چطور می‌دانیم که char ** واقعاً اشاره‌گری به آرایه‌ای از اشاره‌گرها به آرایه‌های char است، و نه اشاره‌گری به اشاره‌گر یک char واحد؟

نمی‌دانیم.

این یکی از موارد متعددی است که می‌تواند استفاده از اشاره‌گرها را گیج‌کننده کند. کد زیر ممکن است کامپایل شود

#include <stdio.h>

void print_strings(char **passed_in_strings) {
    // prints: A (followed by gibberish)
    printf("%s\n", *passed_in_strings);
    // prints gibberish
    printf("%s\n", *(passed_in_strings + 1));
}

int main() {
    char my_char = 'A';
    char *my_ptr = &my_char;
    char **my_ptr_to_ptr = &my_ptr;
    print_strings(my_ptr_to_ptr);
    return 0;
}

اما از print_strings() آن‌طور که در نظر بود استفاده نمی‌شود. استفاده از امضای متفاوت کمکی نمی‌کند. تغییر امضا به این شکل

void print_strings(char *passed_in_strings[]) {
    // code snipped
}

ممکن است باز هم کامپایل شود و باز هم چرند چاپ کند. اما پاس دادن اشاره‌گری به اشاره‌گر یک char واحد واقعاً ارزشی ندارد. به احتمال زیاد، وقتی char ** را می‌بینید، نمایانگر اشاره‌گری به آرایه‌ای از اشاره‌گرها به آرایه‌های char است.

دیدیم که اشاره‌گری به یک تابع پاس داده می‌شود. اما چرا اشاره‌گری را به تابع پاس دهیم؟

ارسال اشاره‌گر به تابع

در C، آرگومان‌ها با مقدار به توابع پاس داده می‌شوند. یعنی مقدار از آرگومان کپی می‌شود و آن کپی محلی تابع است. وقتی تابع پایان می‌یابد، مقدار کپی‌شده هم پایان می‌یابد.

ببینید وقتی می‌خواهیم مقادیر را درون یک تابع تغییر دهیم چه اتفاقی می‌افتد

#include <stdio.h>

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    //prints:  In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", a, b);
}

int main() {
    int a = 3, b = 4;
    swap(a, b);
    // prints: After 'swap', a is 3 and b is 4
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

این به این دلیل است که، هرچند متغیرها اسم یکسان دارند، a و b در swap() همان a و b در main() نیستند. آن‌ها کپی‌هایی از مقادیر a و b هستند. آیا می‌توانیم swap را تغییر دهیم تا آن‌طور که در نظر داریم کار کند؟

بله می‌توانیم! می‌توانیم نشانی a و b را به‌عنوان اشاره‌گر پاس دهیم. اما این چه فایده‌ای دارد، وقتی تابع اشاره‌گرها را کپی می‌کند؟ فایده دارد، چون نشانی کپی‌شده همچنان همان نشانی است. با اشاره‌زدایی از نشانی می‌توانیم به مقدار اصلی آن‌طور که در main() تعریف شده دسترسی پیدا کنیم.

#include <stdio.h>

// pass in pointers to a and b
void swap(int *a, int *b) {
    // set temp to value at a's dereferenced address
    int temp = *a;

    // set a's value to value at b's dereferenced address
    *a = *b;

    // set b's value to temp
    *b = temp;

    // prints: In 'swap', a is 4 and b is 3.
    printf("In 'swap', a is %d and b is %d.\n", *a, *b);
}

int main() {
    int a = 3, b = 4;
    swap(&a, &b);
    // prints: After 'swap', a is 4 and b is 3.
    printf("After 'swap', a is %d and b is %d.\n", a, b);
    return 0;
}

الان *های زیادی در swap هست، اما حالا آن‌طور که در نظر بود کار می‌کند.

یک روش برای خواندن عملگر غیرمستقیم «مقدار واقع در» است. بنابراین *a مقدار واقع در a است. این با رشته‌ها چقدر خوب کار می‌کند؟

اشاره‌گر به char

می‌دانیم که مقدار یک متغیر آرایه، نشانی جایی است که آرایه از آن‌جا شروع می‌شود. و می‌دانیم که می‌توانیم یک رشته را یا به‌صورت آرایه‌ی char، char my_string[]، یا به‌صورت اشاره‌گری به char، char *، تعریف کنیم. چه تفاوتی دارند؟

در این کد، یک آرگومان char * به describe_me() پاس می‌دهیم. آرگومان adjective به‌صورت آرایه‌ی char تعریف شده و تابع آن را به‌عنوان اشاره‌گری به char می‌پذیرد.

#include <stdio.h>
#include <string.h>

// takes pointer to char
void describe_me(char *adjective){
    strcpy(adjective, "evil");
    // prints: I am evil.
    printf("I am %s.\n", adjective);
}

int main() {
    // defined as a char array
    char adjective[] = "good";
    describe_me(adjective);
    // prints: I am really evil.
    printf("I am really %s.\n", adjective);
    
    return 0;
}

می‌توانیم تغییری ماندگار در adjective ایجاد کنیم، که نشان می‌دهد آرایه‌ی char تعریف‌شده را می‌توان به‌عنوان اشاره‌گری به char پذیرفت.

حالا بیایید adjective را مستقیماً به‌صورت اشاره‌گری به char تعریف کنیم.

#include <stdio.h>
#include <string.h>

void describe_me(char *adjective){
    // segmentation fault on this line when program is executed
    strcpy(adjective, "evil");
    printf("I am %s.\n", adjective);
}

int main() {
    char *adjective = "good";
    describe_me(adjective);
    printf("I am really %s.\n", adjective);
    
    return 0;
}

یادتان هست که اشاره‌گرها قدرتمند، اما بالقوه خطرناک‌اند؟ اما چرا کار نمی‌کند؟ یک آرایه یک اشاره‌گر است و یک اشاره‌گر با آرایه‌ی char کار کرد! دلیلش این است که پیش‌تر، adjective به‌صورت آرایه‌ی char تعریف شده بود که متغیری متعلق به main() است. حالا adjective به‌صورت یک رشته‌ی literal تعریف شده، یعنی charsها در حافظه‌ی ایستای فقط‌خواندنی نوشته می‌شوند که متعلق به کل برنامه است. هر دو اشاره‌گرند، اما به مکان‌های متفاوتی اشاره می‌کنند. تلاش برای تغییر مقدار یک رشته‌ی literal مانند تلاش برای تغییر مقدار literal 5 است. این کار مجاز نیست. پس مشکل واقعاً این نیست که اشاره‌گر است. مشکل این است که به کجا اشاره می‌کند.

ویرایش از طریق GitHub این پیوند در پنجره یا زبانه‌ی جدیدی باز می‌شود