본문 바로가기
Language/Golang

Golang (Go언어) 포인터(Pointer)

by HeeWorld 2024. 11. 7.

Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.

 

Golang 마스코트 Gopher(고퍼)

 

포인터(Pointer)

- 메모리 주소를 값으로 갖는 타입

- 포인터도 값을 가지고, Type이라 변수를 만들 수 있음 (값을 받는데 값이 메모리 주소인 것)

포인터

 

✓ var a int라는 변수를 선언하면, 컴퓨터는 메모리에 변수를 저장할 공간(8byte)을 만듦.

    공간이 시작하는 주소(ex.100번지)를 a라는 변수가 가리키게(point)함.

     a = 10 이라고 하면 a라는 공간에 10을 복사해라 라는 의미.

→ a라는 공간은 a가 가르키고 있는 메모리 시작주소 값(ex.100)과 type을 알면 사이즈를 알 수 있고, 해당 공간에 10이라는 값을 복사하는 것

 

* 모든 변수는 다 메모리 공간을 가지고 있고, 메모리 공간은 시작 주소가 있음 (=주소는 숫자값)

   → 주소값은 어떤 변수가 가지고 있을 수 있다는 것

 

예)

var a int
var p *int

p = &a   // a의 메모리 주소를 포인터 변수 p에 대입

 

→ p = &a에서 a에 &를 붙이면 a의 주소를 의미(a라는 변수가 가르키고 있는 메모리 주소값),

    a의 메모리 주소를 p에 복사하라는 의미 = p도 변수이기 때문에 p도 본인만의 메모리 공간을 가지고 있음

    a의 메모리 시작 주소가 100이고, p의 메모리 주소는 20이라고 할 때,

     p= &a를 하면 a의 주소값(100)을 p가 가르키고 있는 공간에 복사하라는 뜻으로, p는 값으로 100(p=100)을 갖게 되는데,

    이는 메모리 주소를 값으로 받은 것 = pointer

 

✓ 포인터는 type앞에 *을 붙여서 표시 함

*int ⇢ int형 메모리 공간, int 타입의 변수의 주소값을 값으로 가질 수 있는 포인터, int타입의 메모리 주소를 값으로 가질 수 있음

 

var f float64

var p *int

p = &f

 

예를 들어 위와 같은 코드가 있을 때, p = &f를 수행할 때 컴파일 에러 발생(=타입 다름)

&f은 f의 주소 → 주소값 type은 *float64이며, p는 타입이 int pointer이기 때문에 서로 타입이 달라서 에러 발생

= 어떤 연산을 하더라도 양변의 타입이 같아야 함

 

→ Type 앞에 *을 붙이면 그 타입의 해당하는 주소 값을 값으로 받을 수 있는 포인터가 됨.

 

p = 20

*p = 20

 

✓ p = 20은 p가 가지고 있는 메모리 공간에 20이란 값을 복사하는 코드인데, 이를 실행하면 error가 발생

→ p는 포인터 타입이고, 20은 int 타입이라 에러가 발생 됨(p가 가르키는 공간에 특정 값을 넣을 수 없음) 

 

★ 포인터 변수의 값은 무조건 어떤 변수, int형 데이터의 메모리 주소값을 넣어야 함.

→ Go에서는 포인터에 특정 값을 넣을 수 없음(C언어에서는 가능)

 

✓ *p=20은 포인터가 가지고 있는 값이 가리키는 공간을 의미

→ 포인터가 가지고 있는 값이 100일 때, 100번지가 가리키고 있는 공간(포인터가 가지고 있는 값이 가르키는 공간의 값을 20으로 바꾸라는 의미

    = p가 가지고 있는 공간에 있는 값인 100이(*100), 가르키고 있는 공간인 100번지의 공간에 20을 넣어라.

 

var a int
var b int
var p *int

a = 10
b = 20
p = &a
*p= 100

a=100  // 포인터 구문으로 인해 a는 100으로 값이 바뀜

b= *p
b= 100

 

p = &a

→ p는 a의 주소이다

*p= 100

→ p가 가리키고 있는 공간의 값을 100으로 바꿔라 = a의 값이 100으로 바뀜, p가 a의 주소값을 가지고 있기 때문

b= *p

→ *p는 공간으로 a의 메모리 공간을 가리킴, 우변(r-value)에 있기 때문에 값으로 동작하니까 p가 가르키고 있는 메모리 공간의 값이라서 b=100이 됨

 

 

여러 포인터 변수가 하나의 변수를 가르킬 수 있다

✓ 어떤 변수 a, b, c가 있다고 할 때, a=100, b=a, c=a라고 하면 각각의 공간은 별도로 존재하고 안의 값은 똑같이 100이 됨.

var a int
var b int
var c int

a = 100
b = a
c = a

변수와 값

 

✓ 포인터도 동일함, 단, 포인터에서는 값을 메모리 주소를 가지고 있어서 어떤 특정 변수를 가르키는 것임.

    → 100은 메모리 주소 값이고, 실제로는 a라는 별도의 메모리 공간을 가리키고 있는 것

         = 여러 개의 포인터 변수가 하나의 변수를 가리킬 수 있음.

var a int
var p1 *int
var p2 *int
var p3 *int

p1 = &a
p2 = p1
p3 = p2

포인터 변수와 별도의 변수

 

package main

import "fmt"

func main() {
	var a int = 500
	var p *int

	p = &a

	fmt.Printf("p의 값: %p\n", p)
	fmt.Printf("p가 가르키는 메모리의 값: %d\n", *p)

	*p = 100
	fmt.Printf("a의 값: %d\n", a)
}

// 결과

p의 값: 0xc000122040
p가 가르키는 메모리의 값: 500
a의 값: 100

 

* 16진수는 10부터 15까지를 영문을 사용하여 a가 10이고, b가 11 .. 이런식으로 f/15까지 영문으로 표현함

 

- p의 값는 a의 메모리 주소의 값을 출력

- p가 가르키는 메모리 값은 a의 공간

- a의 값은 *p=100(포인터 p가 가지고 있는 값이 나타내는 공간)으로 a값이 100으로 바뀜

 

 

포인터 변숫값 비교하기

- 포인터 연산에서 ==를 사용할 수 있음

package main

import "fmt"

func main() {
	var a int = 10
	var b int = 20

	var p1 *int = &a
	var p2 *int = &a
	var p3 *int = &b

	fmt.Printf("p1 == p2: %v\n", p1 == p2)
	fmt.Printf("p2 == p3: %v\n", p2 == p3)
}

// 결과

p1 == p2: true
p2 == p3: false

 

var p1 *int = &a

- p1 은 a의 메모리 공간을 가리킴

 

var p2 *int = &a

- p1 은 a의 메모리 공간을 가리킴

 

var p3 *int = &b

- p3 는 b의 메모리 공간을 가리킴(p3는 다른 메모리 주소를 가지고 있기 때문에 false)

 

 

포인터 변수의 기본값은 nil

- nil = null로, 값은 0이지만 정확한 의미는 어떤 메모리 공간도 가리키고 있지 않음

- 무효한 값을 가르키고 있음(정상적인 메모리 공간이 아님) / nothing

var p *int
if p != nil {
  // p가 nil이 아니라는 것은 p가 유효한 메모리 주소를 가리킨다는 뜻
}

 

 

포인터 사용 이유

package main

import "fmt"

type Data struct {
	value int
	data  [200]int
}

func ChangeData(arg Data) {
	arg.value = 999
	arg.data[100] = 999
}

func main() {
	var data Data

	ChangeData(data)
	fmt.Printf("value = %d\n", data.value)
	fmt.Printf("data[100] = %d\n", data.data[100])
}

// 결과

value = 0
data[100] = 0

 

type Data struct { ~~ }

- value int = 8byte와 data [200]int = 1600byte(200*8byte) 두 값을 합치면 1608byte로 1608byte짜리 data struct를 생성

   = 초기값인 0을 가지고 있음

 

ChangeData(data)

- ChangeData(data)를 호출하여 data 변숫값을 인수로 넣는데, data 변숫값이 모두 복사되기 때문에 ChangeData() 함수의

    매개변수 arg와 data는 서로 다른 메모리 공간을 가짐

   → 함수의 인자로 어떤 변수가 쓰이면 무조건 우변(r-value)로 쓰임 = r-value로 쓰이는 것은 값으로 사용되는 것!

 

- ChangeData(data)를 호출해 ChangeData(arg Data)에 복사가 되었기 때문에 arg와 data는 서로 다른 메모리 공간임

   → arg의 data 값을 바꿔도, data의 값은 바뀌지 않음 = 공간이 다름!!!

 

 - 따라서, data.value를 출력하면 0이 나옴 = 값이 바뀐 적이 없음

 

★ 이를 해결하기 위해 포인터를 사용함! ★

 

package main

import "fmt"

type Data struct {
	value int
	data  [200]int
}

func ChangeData(arg *Data) {
	arg.value = 999
	arg.data[100] = 999
}

func main() {
	var data Data

	ChangeData(&data)
	fmt.Printf("value = %d\n", data.value)
	fmt.Printf("data[100] = %d\n", data.data[100])
}

// 결과

value = 999
data[100] = 999

 

ChangeData(arg *Data)

- arg를 포인터로 받았고, 포인터 변수로 받았다는 건 Data type의 주소를 받는 것 = arg가 Data의 공간을 포인터함

 

arg.value = 999

- (*arg).value = arg가 가리키고 있는 data의 공간에 포함된 value를 999로 바꿈

 

arg.data[100] = 999

- data가 가지고 있는 101번째 공간의 값을 999로 바꿈

 

ChangeData(&data)

- data의 주솟값으로, type이 data의 포인터 타입이 됨.

 

✓ 포인터를 이용하면 data 변수의 메모리 주소만 복사되기 때문에 메모리 주솟값인 8바이트만 복사 됨.

✓ arg 포인터 변수가 data 변수의 메모리 주소를 값으로 가지고 있어, Data 구조체의 내부 필드값을 변경할 수 있음.

* arg는 포인터 변수라서 (*arg).value의 형식으로 사용해야 하지만, Go에서는 arg.value를 사용해도 동작함.

 

 

구조체 포인터 초기화

- 구조체 변수를 별도로 생성하지 않고, 곧바로 포인터 변수에 구조체를 생성해 주소를 초깃값으로 대입할 수 있음

 

<기존 방식> 

var data Data
var p *Data = &data

 

- Data 타입 구조체 변수 data를 선언

- data 변수의 주소를 반환 = &data

 

<구조체를 생성해 초기화하는 방식>

var p *Data = &Data{}

 

- *Data 타입 구조체 변수 p를 선언

- Data 구조체를 만들어 주소를 반환 = &Data{}

 

✓ data 변수의 주소를 p가 값으로 받는 형태로, Data type의 공간주소를 p가 가지고 있음(변수는 없지만 공간은 있음)

    = 다만, 원래 가리키고 있던 변수가 이름만 없으며, 똑같은 크기의 공간이 존재

 

 

인스턴스(Instance)

- 메모리에 할당된 데이터의 실체

var data Data
var p *Data = &data

Data 인스턴스 하나가 만들어지고, 포인터 변수 p가 가리킴

 

var p1 *Data = &Data{}
var p2 *Data = p1
var p3 *Data = p1

 

- Data 인스턴스가 하나 만들어지고, p1이 그 공간을 가리키고 있고 p2,p3가 p1을 가리키고 있어 다 Data라는 공간을 가리키고 있음

p1,p2,p3 모두 Data 인스턴스를 가리킴

 

var data1 Data
var data2 Data = data1
var data3 Data = data1

 

- data1, data2, data3은 각 변수가 생성되어, 인스턴스가 모두 3개가 됨

- data1의 값이 data2, data3에 복사되어 값만 같음

data1, data2, data3은 값만 같고, 서로 다른 인스턴스

 

 

new() 내장함수

- 인수로 타입을 받음

- 타입을 메모리에 할당하고 기본값으로 채워 그 주소를 반환 

 p1 := &Data{}
 var p2 = new(Data)

 

p1 := &Data{}

- &Data{}를 사용할 때, 각 필드 값을 초기화 할 수 있음

 

var p2 = new(Data)

- new를 사용할 때는 값을 넣을 수 없고, 기본 값으로 초기화 됨.

 

 

인스턴스가 사라질 때

- 인스턴스는 아무도 찾지 않을 때 사라짐

func TestFunc() {
  u := &User{}
  u.Age = 30
  fmt.Println(u)
}

 

- 메모리 공간에 User을 만들고, u가 가리키고 있는 공간에 Age 값을 30으로 변경하라는 구문

- fmt.Println(u)를 하여, u를 출력 후 코드가 끝남, 변수는 소속된 {} 중괄호가 끝나면 사라짐 = u 사라짐

- User인스턴스는 있지만, 포인팅하고 있는 변수가 없어서 다음번 가비지 컬렉터 타임에 사라짐.

   = 어떤 인스턴스를 변수, 포인터 등이 가리키고 있으면 쓰임이 있는 것이지만, 아무도 가리키지 않고 / 찾지 않으면 쓰임이 없음.

 

✓ 인스턴스는 메모리에 생성된 데이터의 실체

✓포인터를 이용해 인스턴스를 가리키게 할 수 있음

✓ 함수 호출 시 포인터 인수를 통해 인스턴스를 입력 받고 그 값을 변경할 수 있음

✓ 쓸모 없어진 인스턴스는 가비지 컬렉터가 자동으로 지워줌

 

 

스택 메모리와 힙 메모리

- 대부분 프로그래밍 언어는 메모리를 할당할 때 스택 메모리 영역 또는 힙 메모리 영역을 사용함.

- 이론상 스택 메모리 영역이 힙 메모리 영역보다 훨씬 효율적이기 때문에 스택 메모리 영역에 할당하는 것이 좋지만,

   스택 메모리는 함수 내부에서만 사용가능한 영역임

package main

import "fmt"

type User struct {
	Name string
	Age  int
}

func NewUser(name string, age int) *User {
	var u = User{name, age}
	return &u
}

func main() {
	userPointer := NewUser("AAA", 23)

	fmt.Println(userPointer)
}

// 결과

&{AAA 23}

 

func NewUser(name string, age int) *User {

- NewUser func안에 var u를 만듦

 

var u = User{name, age}

- u는 User type, User 주소 값을 가리킴

 

return &u

- u의 주소를 반환

 

→ u는 u가 속한 {} 중괄호가 끝나면 변수가 사라지게 되는데, 사라진 변수의 주소를 반환하는 구문

     = 없어진 변수의 주소를 반환하는 것을 댕글링(Dangling, 이미 사라진 주소를 사용하려고 함) 오류가 발생해야 함 = C언어

 

userPointer := NewUser("AAA", 23)

→ userPointer는 *User 포인터 타입으로, NewUser의 결과를 받음

    주소 값이 나오는데 주소 값이 무효한 주소(유효하지 않은 주소) 즉, 사라진 주소를 가리키고 있는거라 실체가 없는데,

    출력했을 때 출력이 된 것으로 공간이 사라지지 않았음을 의미

 

✓ C나 C++는 해당 코드를 사용하면 에러가 발생함

→ func NewUser에서 u 변수가 사라졌기 때문에 에러가 발생함

- func내에 존재하는 지역변수는 스택 메모리에 쌓이게 되는데, func이 돌아갈 때 사용되었던 함수들은 pop(사라짐)됨

  그러니 공간이 사라져 무효한 주소가 됨.

- Go에서는 탈출검사(escape analysis)라고, 컴파일러가 코드를 분석해서 어떤 인스턴스가 코드 밖(func)으로 탈출하지 않았는지 분석

   탈출하면 스택에 만들지 않고 힙 영역에 생성함(지역 변수는 스택 메모리 영역에 생성됨)

   힙 영역에 생성된 것들은 쓰임이 다 하면 사라지고, 쓰임이 있으면 사라지지 않음

'Language > Golang' 카테고리의 다른 글

Golang (Go언어) 패키지(Package)  (6) 2024.11.09
Golang (Go언어) 문자열(String)  (4) 2024.11.08
Golang (Go언어) 구조체(Structure)  (9) 2024.11.05
Golang (Go언어) 배열(Array)  (0) 2024.11.02
Golang (Go언어) for문 (반복문)  (4) 2024.10.31