2.1 اشاره‌گر (Pointer)

2.1 اشاره‌گر (Pointer)

اشاره‌گر در واقع متغیری است که، آدرس حافظه یک مقدار را نگه می‌دارد.

1var ex *T

در بین برنامه نویسان زبان Go همیشه این مسئله بوده که کی و کجا باید از Pointer استفاده کنیم؟!

دیدگاه من نسبت به Pointer :

زمانی باید از Pointer استفاده کنید که قصد دارید یک متغیری را در scope ها و توابع مختلف مقدار دهی کنید در اینجا بهتر است از Pointer استفاده کنیم تا جلو کپی شدن متغیر در خانه های مختلف حافظه گرفته شود.

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

حتی این قضیه برای اینکه method تعریف کنیم صدق میکنه که چرا باید متد با Pointer یا بدون Pointer تعریف کنیم.

نکته مهم 1: استفاده از Pointer باید با دقت انجام شود تا از مشکلاتی مانند دسترسی همزمان به متغیرها و اشتباهات مرتبط با حافظه جلوگیری شود.

نکته مهم 2: استفاده از Pointer خیلی خوب و مفید است اما در جای درست چون اگر نتوانیم تشخیص دهیم کی و کجا استفاده کنیم به مرور باعث کاهش عملکرد برنامه خواهد شد.

در مثال بالا ما شیوه تعریف یک متغیر اشاره‌گر را توضیح دادیم. اول کلید واژه ی var بعد اسم متغیر و در آخر هم *T یعنی تایپ متغیر. به مثال زیر توجه کنید:

1var ptr *string

در تعریف اشاره‌گر ‌ها, ما ۲ تا اپراتور داریم که کارکرد هر کدام از این اپراتورها رو در ادامه توضیح میدم:

  • & بهش میگن ampersand با استفاده از این می‌توانیم آدرس حافظه متغیر فرضا x را به متغیر دیگری بدهیم (y := &x)
  • * بهش میگن asterisk با استفاده از این می‌توانیم به مقدار داخل حافظه متغیر فرضا x دسترسی پیدا کنیم (x*)

برای اینکه یک اشاره گر تعریف کنیم ۲ روش وجود دارد:

  1. استفاده از تابع new
  2. استفاده از اپراتور & (آمپرسند)

مثال 1 #

فرض کنید شما 1 متغیر دارید و قصد دارید داخل 3 تابع مختلف مقدارش را بروز کنید و با یک تابع دیگر نمایش دهید:

 1package main  
 2  
 3import "fmt"  
 4  
 5func main() {  
 6    var count int  
 7  
 8    addCount(&count)  
 9  
10    addCount(&count)  
11  
12    addCountWithoutPointer(count)  
13    fmt.Printf("value = %d, address in memory = %p\n", count, &count)  
14  
15    printCount(count)  
16  
17}  
18  
19func addCount(x *int) {  
20    *x++  
21    fmt.Printf("value = %d, address in memory = %p\n", *x, x)  
22}  
23  
24func addCountWithoutPointer(x int) {  
25    x++  
26    fmt.Printf("value = %d, address in memory = %p\n", x, &x)  
27}  
28  
29func printCount(x int) {  
30    fmt.Printf("value = %d, address in memory = %p\n", x, &x)  
31}
1value = 1, address in memory = 0xc000110068
2value = 2, address in memory = 0xc000110068
3value = 3, address in memory = 0xc000110088
4value = 2, address in memory = 0xc000110068
5value = 2, address in memory = 0xc0001100b0

pointer

در کد فوق ما یک متغیر به نام count ساختیم که داخل تابع (scope) main می باشد.

رخداد اول: حال این متغیر را 2 بار بصورت Pointer به تابع addCount پاس دادیم و داخل همان تابع مقدار دهیش کردیم و پس از مقدار دهی در همان تابع print ش کردیم. اتفاقی که افتاد مقدار متغیر در همان خانه حافظه که 0xc0000a6068 هست مقدار دهی شد و عملا بخشی دیگر از حافظه گرفته نشد.

رخداد دوم: متغیر را بدون Pointer به تابع addCountWithoutPointer پاس دادیم و در همان تابع مقدار دهید و print کردیم, اتفاقی که افتاد ما متغیر را اینبار بدون Pointer پاس دادیم یعنی عملا یک کپی از متغیر را به تابع addCountWithoutPointer فرستادیم و اگر به آدرس حافظه مقدار دقت کنید 0xc0000a6088 عملا یک خانه جدید به این کپی تخصیص داده شد و مقدارش در همان خانه بروز شده و اون متغیر x تنها در همان تابع زنده اس و در صورتیکه اگر x را از تابع بازگشت دهید دوباره یک کپی از آن به بیرون منتقل می شود.

مثال 2 #

فرض کنید یک تایپ count دارید که نام مستعار تایپ int می باشد و 3 تا متد (متد را در بخش 2.3 می توانید بخوانید) گیرنده Pointer با نام های increase , decrease و print دارند.

 1package main  
 2  
 3import "fmt"  
 4  
 5type count int  
 6  
 7func main() {  
 8    x := new(count)  
 9    x.increase()  
10    x.increase()  
11    x.decrease()  
12    x.increase()  
13  
14    x.printWithoutPointer()  
15  
16}  
17  
18func (c *count) increase() {  
19    *c++  
20    c.print()  
21}  
22  
23func (c *count) decrease() {  
24    *c--  
25    c.print()  
26}  
27  
28func (c *count) print() {  
29    fmt.Printf("value = %d, address in memory = %p\n", *c, c)  
30}  
31  
32func (c count) printWithoutPointer() {  
33    fmt.Printf("value = %d, address in memory = %p\n", c, &c)  
34}
1value = 1, address in memory = 0xc0000a4068
2value = 2, address in memory = 0xc0000a4068
3value = 1, address in memory = 0xc0000a4068
4value = 2, address in memory = 0xc0000a4068
5value = 2, address in memory = 0xc0000a4088

ما در مثال فوق با استفاده از تابع new اومدیم متغیر x را ایجاد کردیم سپس متد increase برای افزایش مقدار متغیر x و متد decrease را برای کاهش مقدار x و در نهایت print را برای چاپ استفاده کردیم.

در اینجا به دلیل گیرنده Pointer بودن تایپ count توانستیم درهمان خانه حافظه مقدار x را افزایش یا کاهش دهیم و در نهایت با استفاده از متد print اومدیم مقدار و خانه حافظه را چاپ کردیم.

اما یک متد printWithoutPointer داریم که یک کپی از مقدار x را چاپ میکند و عملا مقدار را از یک خانه حافظه جدید را به نمایش میگذارد.

متد printWithoutPointer بدون Pointer می باشد و زمانیکه سایر متدهایتان با یا بدون Pointer هست بهتر است متدهای جدیدتان با Pointer باشد تا جلو سردرگمی گرفته شود. طبق داکیومنت های ارائه شده برای Go چندان لزومی ندارد چنین ترکیبی انجام دهید.

2.1.1 استفاده از تابع new #

یک اشاره‌گر با استفاده از تابع new بصورت مثال زیر تعریف شده است:

1a := new(int)
2*a = 10
3fmt.Println(*a) //Output will be 10

در مثال بالا ما متغیر a را از نوع int اشاره‌گر pointer a تعریف کردیم و سپس داخل آدرس حافظه a مقدار ۱۰ را قرار دادیم.

توجه کنید مقدار پیش‌فرض یک متغیر از نوع اشاره‌گر nil است. اگر جایی شما متغیر از نوع اشاره‌گر را بصورت nil بفرستید ممکن است به panic از نوع nil pointer بر بخورید و اجرای برنامه شما کاملا متوقف شود.

2.1.2 استفاده از اپراتور ‘&’ #

برای دریافت آدرس حافظه یک متغیر از & می‌توان استفاده کرد:

1a := 2
2b := &a
3fmt.Println(*b) //Output will be 2

به مثال زیر توجه کنید:

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6    var b *int
 7    a := 2
 8    b = &a
 9    
10    fmt.Println(b)
11    fmt.Println(*b)
12    b = new(int)
13    *b = 10
14    fmt.Println(*b) 
15}
1$ go run main.go
20xc0000b0018
32
410

در خروجی بالا 0xc0000b0018 آدرس حافظه متغیر a است. در واقع متغیر a ساخته شد و ما آدرس حافظه آن را به متغیر b دادیم. یعنی هر دو متغیر به یک آدرس از حافظه اشاره می‌کنند.

2.1.3 اپراتور * اشاره‌گر #

ما می‌توانیم اپراتور * را برای عملیات‌های زیر به کار ببریم:

  • گرفتن مقدار یک آدرس حافظه که با استفاده از اشاره‌گر ذخیره شده است.
  • تغییر مقدار یک آدرس حافظه.

به مثال زیر توجه کنید: #

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	a := 2
 7	b := &a
 8	fmt.Println(a)
 9	fmt.Println(*b)
10
11	*b = 3
12	fmt.Println(a)
13	fmt.Println(*b)
14
15	a = 4
16	fmt.Println(a)
17	fmt.Println(*b)
18}
1$ go run main.go
22
32
43
53
64
74

در مثال بالا a و b* هر دو دارند به یک آدرس از حافظه اشاره می‌کنند. بنابرین تغییر مقدار یکی از آن‌ها، روی هر دو متغیر تاثیر می‌گذارد.

2.1.4 اشاره‌گر به یک اشاره‌گر (Double Pointers) #

شما می‌‌توانید یک متغیر اشاره‌گر تعریف کنید و متغیر اشاره‌گر دیگری را بهش اختصاص دهید.

1a := 2
2b := &a
3c := &b

array

همانطور که در مثال و عکس بالا می‌بینید، متغیر a مقدارش ۲ و آدرسش در حافظه 0xXXXXXX است. در مقدار متغیر b ما اشاره کردیم به آدرس حافظه متغیر a و در ادامه در متغیر c به آدرس حافظه متغیر b اشاره کردیم.

زمانیکه شما بخواهید مقدار c را چاپ کنید کافیست از c** استفاده کنید تا مقدار ۲ را چاپ کند.

به مثال زیر توجه کنید:

 1package main
 2
 3import "fmt"
 4
 5func main() {
 6	a := 2
 7	b := &a
 8	c := &b
 9
10	fmt.Printf("a: %d\n", a)
11	fmt.Printf("b: %x\n", b)
12	fmt.Printf("c: %x\n", c)
13
14	fmt.Println()
15	fmt.Printf("a: %d\n", a)
16	fmt.Printf("*&a: %d\n", *&a)
17	fmt.Printf("*b: %d\n", *b)
18	fmt.Printf("**c: %d\n", **c)
19
20	fmt.Println()
21	fmt.Printf("&a: %d\n", &a)
22	fmt.Printf("b: %d\n", b)
23	fmt.Printf("&*b: %d\n", &*b)
24	fmt.Printf("*&b: %d\n", *&b)
25	fmt.Printf("*c: %d\n", *c)
26
27	fmt.Println()
28	fmt.Printf("&b: %d\n", &b)
29	fmt.Printf("c: %d\n", c)
30	fmt.Printf("*c: %d\n", *c)
31	fmt.Printf("**c: %d\n", **c)
32	
33}
 1$ go run main.go
 2a: 2
 3b: c000018078
 4c: c00000e028
 5
 6a: 2
 7*&a: 2
 8*b: 2
 9**c: 2
10
11&a: 824633819256
12b: 824633819256
13&*b: 824633819256
14*&b: 824633819256
15*c: 824633819256
16
17&b: 824633778216
18c: 824633778216
19*c:824633819256
20**c:2

توجه کنید در زبان گو علی رغم زبان c استفاده از اشاره‌گر حسابی (Pointer Arithmetic) امکان پذیر نمی‌باشد و در صورت استفاده با خطای زیر مواجه خواهید شد:

1package main
2func main() {
3    a := 1
4    b := &a
5    b = b + 1
6}
1$ go run main.go
2invalid operation: b + 1 (mismatched types *int and int)