『Tucker의 Go 언어 프로그래밍』 스터디 요약 노트 : 14강 포인터

2021. 5. 30. 21:39개발/Golang

728x90
반응형

포이넡는 메모리 주소를 값으로 갖는 타입입니다. 포인터를 이용하면 동일한 메모리 공간을 여러 변수가 가리킬 수 있습니다. 그리고 메모리 복사를 줄일 수 있으며 반환값 없이 변숫값을 바꿀수 있습니다.

포인터에 대해 요약해보겠습니다!

 

1. 포인터란?

 

메모리 주소를 값으로 갖는 타입입니다.

예를 들어 int타입 a가 있을 때 a는 메모리에 저장되어 있고 속성으로 메모리 주소를 가지고 있습니다. 변수 a의 주소가 0x0100q번지라고 했을 때 메모리 주솟갑 또한 숫자값이기 때문에 다른 변수의 값으로 사용될 수 있습니다. 이렇게 메모리 주솟값을 변숫값으로 가질 수 있는 변수를 포인터 변수라고 합니다.

 

<사진>

 

 

p = &a
  • 변수 p에 a의 주소를 대입하는 구문
  • 변수 p의 값은 a의 주소인 0x0100이 됩니다. 이것을 ' 포인터 변수 p가 변수 a를 가리킨다 ' 라고 말합니다.
  • 포인터를 이용하면 여러 포인터 변수가 하나의 메모리 공간을 가르킬 수도 있고 포인터가 가리키고 있는 메모리 공간의 값을 읽을 수 도 변경 할 수도 있습니다.

 

 

1) 포인터 변수 선언

var p *int
  • 데이터 타입 앞에 *을 붙여서 선언합니다. 
  • p는 int 타입 데이터의 메모리 주소를 가리키는 포인터 변수입니다. User 구조체를 가르키면 *User라고 선언하면 됩니다.

 

package main

import "fmt"

func main() {
	var a int = 500
    var p *int			// int 포인터 변수 p 선언
    
    p = &a			// a의 메모리 주소를 변수 p의 값으로 대입 (복사)
    
    fmt.Printf("p의 값 : %p\n" , p) //p의 메모리 주소값 출력
    fmt.Printf("p가 가리키는 메모리의 값: %d\n", *p) // p가 가리키는 메모리값 출력
    
    *p = 200
    fmt.Printf("a의 값 : %d\n", a)
}
    

결과값

  • int 타입 메모리 변수 p를 선언하고 a의 메모리 주소를 p의 값에 대입합니다. 
  • p의 메모리 주소를 확인해보니 0xc000100010이 나왔고 거기에 넣은 값은 500이 나옵니다.
  • *p로 접근한 메모리 변수 p에 200으로 변경하니 a의 주소를 가리킨 p의 값때문에 값이 200으로 변경됩니다.

 

2) 포인터 변숫값 비교하기

 

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은 a의 메모리공간을 p2도 a의 메모리공간을 가지고 p3은 b의 메모리공간을 가집니다.
  • p1과 p2는 같은 메모리 주소를 가지기 때문에 p1 == p2는 true가 되고, p2 == p3는 다르기 때문에 false가 됩니다.

 

3) 포인터의 기본값 nil

 

포인터 변숫값을 초기화 하지않으면 기본값은 nil입니다. 이값은 0이지만 정확한 의미는 유효하지 않는 메모리 주솟값 즉 어떤 메모리 공간도 가리키고 있지 않음을 나타냅니다.

 

 

< 유효한 메모리 주소를 가르키는지 검사하는 구문>

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

 

2. 포인터는 왜 쓰나?

 

변수 대입이나 함수 인수 전달은 항상 값을 복사하기 때문에 많은 메모리 공간을 사용하는 문제와 큰 메모리 공간을 복사할 때 발생하는 성능 문제를 안고 있습니다. 또한 다른 공간으로 복사되기 때문에 변경사항이 적용되지도 않습니다. 포인터를 사용하지 않는 예를 살펴보겠습니다.

 

< 예시 >

 

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])
 }

결과값

  • ChangeData() 함수는 Data 타입 구조체를 매개변수로 받습니다. ChangeData()에서 Data함수를 호출하면서 인수로 넣습니다.
  • ChangeData() 함수의 매개변수 arg와 data는 서로 다른 메모리 공간을 갖는 변수입니다.
  • arg 변수값을 변경해도 data 변수와는 다른 메모리 공간을 가지기 때문에 arg값을 변경해도 data값은 출력되지 않습니다.

 

  •  이 예제의 문제점
    • ChangeData() 함수 호출시 data 변숫값이 모두 복사되기 때문에 구조체 크기 만큼 복사된다.
    • Data 구조체의 크기는 1608바이트입니다. ( int = 8byte , [200]int = 1600byte )
    • 함수를 호출 할때마다 1608바이트를 짧은 시간에 많이 호출되면 성능 문제가 발생할 수 있습니다.

→ 이 문제들을 한방에 해결해주는 해결사가 포인터 입니다.

 

< 포인터를 이용한 예제 > 

 

package main

import "fmt"


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

func ChangeData(arg *Data) {		//매개변수로 Data 포인터를 받습니다.
	arg.value = 999
    arg.data[100] = 999
}

func main() {
	var data Data 	// Data 구조체의 변수 data
    
    ChangeData(&data)
    fmt.Printf("value = %d\n", data.value)
    fmt.Printf("data[100] = %d\n", data.data[100])
}
    
    

결과값

  • ChangeData() 함수 매개변수로 Data 구조체의 포인터를 받는것으로 변경했습니다.
  • data는 변숫값이 아니라 data의 메모리 주소를 인수로 전달합니다. 메모리주소는 8바이트 숫자값 ( 64비트 컴퓨터에선 메모리주소는 8바이트 ) 이기에 1608바이트가 복사되는게 아닌 8바이트만 복사 됩니다.
  • arg 포인터 변수가 가리큰 구조체 값을 변경합니다. ( 999의 값 )
  • arg 포인터 값은 main() 함수의 data 구조체 주솟값이기 때문에 arg 포인터가 main() 함수의 data 변수를 가리키게 됩니다. 그래서 data값이 변경됩니다.

 

Data 구조체를 생성해 포인터 변수 초기화하기

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

 

  • Data 타입 포인터 변수 p에 Data구조체를 생성해 그 주소를 대입했습니다.
  • 실제로 있는 구조체 데이터의 실체를 가리키게 되므로 포인터 변수 p만 가지고도 구조체의 필드 값에 접근하고 변경할 수 있습니다.

 

2. 인스턴스

 

 

1) new() 내장 함수

2) 인스턴스는 언제 사라지나?

 

인스턴스는 아무도 찾지 않을 때 사라진다.

 

func TestFunc() {
	u := &User{}	// u 포인터 변수를 선언하고 인스턴스를 생성합니다.
    u.Age = 30
    fmt.Println(u)
}			// 내부 변수 u는 사라집니다. 더불어 인스턴스도 사라집니다.

< 정리 >

  • 인스턴스는 메모리에 생성된 데이터의 실체입니다.
  • 포인터를 이용해서 인스턴스를 가르키게 할 수있습니다.
  • 함수 호출 시 포인터 인수를 통해서 인스턴스를 입력받고 그 값을 변경할 수 있게 됩니다.
  • 쓸모 없어진 인스턴스는 가비지 컬렉터가 자동으로 지워줍니다.

 

3) 스택메모리와 힙메모리

 

  • 프로그래밍 언어는 메모리를 할당할 때 스택메모리 영역 또는 힙 메모리 영역을 사용 
  • 스택메모리는 효율적이지만 스택메모리는 함수 내부에서만 사용가능한 영역
  • 함수 외부로 공개되는 메모리 공간은 힙 메모리영역에서 할당합니다. 
  • Go언어 에서는 탈출 검사를 통해서 변수의 인스턴스가 함수 외부로 공개되는것을 분석해 스텍메모리가 아닌 힙메모리에서 할당하게 됩니다. 

 

 

 ps. 스택메모리와 힙메모리와 인스턴스 탈출 검사는 Tucker님 유튜브나 책을 보시는 걸 추천. 글로 설명이 어렵네요.ㅎ

728x90
반응형