강의/computer science

[ComputerScience] c++의 pointer와 reference 2편 (Dynamic Array, Static Array, Smart Pointers)

하기싫지만어떡해해야지 2024. 9. 30. 16:22

이 게시글은

서울대학교 데이터사이언스대학원

조요한 교수님의

데이터사이언스 응용을 위한 컴퓨팅 강의를

학습을 위해 재구성하였습니다.


pointer와 reference는

관련 내용이 깊고

자세하게 설명해야할게 많아서

나누다보니 3편까지 작성해야할 것 같다

 

1편에서는 pointer의 정의, 사용법,

동적 메모리 할당 등에 대해서 배웠다

 

이버에는 포인터 변수를 이용해서

동적으로 배열을 할당하고

그걸 어떻게 이용하는지 알아보자

 

 

Dynamic Array

 

 

위 코드를 보면

int *arr = new int[3]{1, 2, 3};

이렇게 포인터 변수 arr를 동적으로 할당했다

 

위 코드가 무슨 뜻이냐하면

어떤 메모리 주소에

int가 3개 들어갈 수 있는

공간을 할당하겠다는 뜻이다

 

new로 동적 할당이 끝나면

arr는 첫번째 원소의 위치를 반환하게된다

 

이렇게 동적으로 arr 포인터 변수를 할당하고

array로 저장하게되면

arr[0], arr[1], arr[2]와 같은 방식으로

저장된 원소에 접근할 수 있다

 

또한, 동적으로 할당했기 때문에

arr[0], arr[1], arr[2]의 원소들인

1, 2, 3은 heap에 저장되고

포인터 변수 자체는

컴파일시에 메모리에 할당되어

stack공간에 할당된다

 

 

 

 

그렇다면 이제 arr를 다양하게 출력해보자

 

그냥 arr을 출력하면

가장 첫번째 원소의 메모리주소를 반환한다

 

&arr을 하게 되면

포인터변수 arr가 저장된 메모리주소를 반환한다

 

*arr를 하게되면

가장 첫번째 원소의 값을 반환한다

 

&(*arr)를 하게 되면

가장 첫번째 원소가 저장된 메모리 주소

즉, 그냥 arr과 같은 값을 반환한다

 

 

arr + 1을 하게되면

arr의 첫 번째 주소의 다음 주소를 가리키므로

arr[1]의 주소가 출력된다

 

arr + 2를 하게되면

arr의 첫 번째 주소에서

2번 건너뛴 주소를 반환하므로

arr[2]의 주솟값이 반환된다

 

앞에 *이 붙으면 해당 주솟값에있는

원소를 가리키게 되므로

arr[1] = 2

arr[2] = 3

이 출력되는 것이다

 

 

 

 

이제 이러한 동적 배열을 활용해서

2차원 배열을 만들어보자

 

multi-dimensional을 만들려면

pointer의 pointer가 필요하다

 

약간 복잡하지만

개념이 잘 잡혀있다면

이해할 수 있다

 

위 코드를 보면 우선 int로

nRows와 nCols를 정의해준다

2차원 배열은 행렬을 생각하면 되는데

row는 가로의 개수, col은 세로의 개수다

cin을 통해서 가로는 2, 세로는 3을 넣어준다

 

그리고 이중 포인터 배열인

int** arr = new int*[nRows];

를 선언해준다

 

int** arr는 int*가 들어있는

메모리 주소를 담고있는 포인터 변수이다

 

복잡하게 생각할 필요 없이

포인터변수의 메모리주소를 담고있는

또 하나의 포인터변수인것이다

 

아무튼 int** arr 포인터변수를

nRows의 값인 2만큼 공간을 할당해준다

이 말은 int 데이터가 담긴 메모리 주소를 갖고있는

포인터변수(8바이트)를 2개

할당해달라는 뜻이다

 

따라서 arr[0]과 arr[1]이

heap에 할당된다

 

그다음 for문을 돌면서

arr[0]과 arr[1]에 int 데이터를 nCols의 개수인

3개만큼 담을 int 배열을 할당해준다

 

int는 4바이트를 필요로하므로

arr[0]과 arr[1] 각각

4바이트 3개 총 12바이트가 할당된다

 

그래서 arr[0]에는 arr[0]의

첫번째 원소의 주솟값이 저장되고

arr[1]에는 arr[1]의

첫번째 원소의 주솟값이 저장된다

 

포인터변수 arr는 stack에 저장되고

나머지는 동적으로 할당해줬으므로

heap에 저장된다

 

 

Static Array

 

static array는 heap이 아닌

stack 공간에 할당된다

 

왜냐하면 compile할 때부터

size를 알기 때문에

(fixed size array)

컴파일러가 자동으로

메모리 공간을 할당해주기 때문이다

 

 

array에서 index를 주지않고

그냥 단순 변수명만 사용하는 것은

마치 첫 번째 변수를 가리키는

포인터 변수와 비슷한 역할을 한다

 

위 코드 예시에서

arr는 arr[0]의 주솟값과 동일하고

arr + 1은 arr[0]에서 한 칸 떨어진

arr[1]의 주솟값과 동일하다

 

 

static array에서의 이차원배열

선언법을 알아보자

 

동적 배열보다 단순한데

int arr[2][2] = {1, 2, 3, 4};

와 같이 선언해주면

2x2 형태의 행렬로 배열이

선언되는 것이다

 

값이 할당되는 순서는 차례대로

arr[0][0]과 arr[0][1]에 1과 2가

arr[1][0]과 arr[1][1]에 3과 4가

할당된다

 

위 ppt의 stack 메모리 구조처럼

차곡차곡 쌓이는데

1, 2, 3, 4 순서대로 쌓인다고 생각하면

이해하기 편하다

 

 

Smart Pointer

 

smart pointer에 대해서 알아보자

 

프로그래머들이 포인터를 쓰면서

가장 많이하는 실수는 바로

메모리 공간 할당 후

free를 해주지않아서 

memory leak이 발생하게 하는 것이다

 

혹은 이미 free를 시켜주었는데

실수로 그 공간에 접근을 시도하는 경우도

종종 발생하는데 이를 Dangling Pointer라고 한다고 한다

 

이러한 자주 발생하는 실수를 방지하기위해

smart pointer라는 것이 나왔는데

이는 일반 raw pointer를

Encapsulate해주는 pointer로

메모리 공간을 더이상 사용하지 않을때

자동으로 free를 시켜준다고 한다

 

 

 

c++에서 smart pointer는

std::unique_ptr로 선언한다

 

unique_ptr<vector<int>> vecPtr(new vector<int>());

로 선언해주면

vector<int>의 포인터 변수를

smart pointer로 선언해주게된다

 

또한 unique_ptr을 사용해서

선언해줄 때는

변수명 뒤에 넘겨주는 파라미터 값으로

데이터타입에 맞는 빈 값을 할당시켜줘야한다

위 예시에선 데이터타입이 vector<int>이므로

실제 vector<int>()를 넘겨준 것이다

 

unique_ptr을 쓸 때

한 가지 유의해야할 점은

copy가 안된다는 점이다

 

이 이유는 이 변수를 copy하는 순간

다른 변수들도 vecPtr이 들고있는

메모리 주솟값에 자유롭게 접근이 가능한데

이를 방지하고자하기 위함이다

 

 

이렇게 선언된 unique_ptr은

raw pointer를 쓰듯이 사용 가능하며

main함수가 종료돼서

vecPtr이 scope에서 사라지면

자동으로 메모리 공간을 free시켜준다

 

 

unique_ptr말고

shared_ptr도 있다

 

shared_ptr은 unique_ptr과 다르게

copy가 가능한 smart pointer이다

 

위 코드와 같이

vecPtr1을 선언하고

vecPtr2 = vecPtr1;

과 같이 해주면

vecPtr2와 vecPtr1은

서로 같은 메모리 주소를 가리키기된다

 

따라서 vecPtr1이나 vecPtr2에

push_back을 하면

같은 공간에 값을 넣어주는 것이다

 

shared_ptr은

copy된 값들도 포함해서

마지막 shared_ptr이 scope에서

사라지게 될때 메모리에서 free된다

 

 

위에서 unique_ptr과 shared_ptr 모두

선언이 꽤나 길고 복잡한데

이를 간소화시켜주려고

make_unique와 make_shared가 나왔다

 

make_unique와 make_shared를

이용하면 위 코드와 같이

간단하게 포인터변수 선언이 가능하다

 

하지만 make_unique는 c++14이상

make_shared는 c++11 이상에서만

지원한다고 한다