Tucker의 Go 언어 프로그래밍 책과 유튜브를 통해 학습 중입니다.
포인터(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
var p1 *Data = &Data{}
var p2 *Data = p1
var p3 *Data = p1
- Data 인스턴스가 하나 만들어지고, p1이 그 공간을 가리키고 있고 p2,p3가 p1을 가리키고 있어 다 Data라는 공간을 가리키고 있음
var data1 Data
var data2 Data = data1
var data3 Data = data1
- data1, data2, data3은 각 변수가 생성되어, 인스턴스가 모두 3개가 됨
- 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 |