강의/computer science

[ComputerScience] c++의 pointer와 reference 1편 (포인터의 정의, 동적 메모리 할당, 포인터 연산)

하기싫지만어떡해해야지 2024. 9. 29. 20:27

이 게시글은

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

조요한 교수님의

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

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


이번시간에는 c언어의 꽃이라고 할 수 있는

포인터에 대해서 배운 내용을 정리해보려한다

 

포인터는 c언어의 꽃이지만

강의 내용이 c++로 이루어지기에

c++과 함께 포인터 변수에 대한 내용을 정리하고

c++에만 있는 reference에 대해서도

배운 내용을 정리해보려한다

 

 

 

c나 c++에서는 변수가 선언이 될 때

그 변수를 위한 공간이 메모리에 생성이된다

메모리에 특정 변수를 위한 공간이 생성이되면

그 공간에 대한 정보인 주소가 존재할텐데

이 주소를 변수의 이름과 associate를 해서

변수 이름을 이용해 그 메모리 주소를

찾아갈 수 있도록 하는 것이다

이러한 것을 변수 선언이라고 부른다

 

위의 ppt에 있는 코드를 보면

x라는 변수가 선언되면

메모리의 stack에 x를 위한 공간이 할당되고

0x7ff7b...어쩌구 하는 메모리 주소가 존재한다

이 메모리 주소를 x라는 변수명과

associate하는 것이다

 

그렇게 되면 앞으로는

x라는 변수 명으로 해당 주소에

접근을 할 수가 있게 된다

 

int x = 5;에 대해서

앞에 &를 붙여

&x를 출력하면

변수명과 assoicate된 메모리주소를

반환한다

 

 

pointer 변수

 

많은 사람들이 포인터 변수에 대해서

포인터가 나오면 굉장히 어려워하는데

반드시 기억해야할건

포인터는 int나 char와 같은

그냥 평범한 변수라는 것이다

 

int가 숫자, char가 문자 한 개를 갖고있다면

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

그 크기는 8바이트이다

 

데이터타입 뒤에 *를 붙인

T*로 포인터변수를 정의하는데

이는 해당 포인터 변수가 갖고있는

메모리 주소로 접근했을 때

T 타입의 데이터가 들어있다는 뜻이다

int* 로 포인터변수를 정의하면

포인터 변수 안에 있는 주소로 가면

숫자 데이터가 들어있다는 소리이다

 

이렇게 포인터변수 안에 있는

메모리 주소로 접근해서

해당 주소에 있는 데이터를

사용하는 것을

dereference라고 부른다

그리고 이는 포인터변수의 이름 앞에 *를

붙인형태로 주로 사용한다

 

이게 무슨 말이냐하면

5가 저장된 메모리 주소를 담고있는

포인터변수 ptr이 있다고하면

포인터 변수 ptr에 담겨있는

메모리 주소에 접근해 5를 가져오는 것을

dereference라고 하고

*ptr이라고 하면 dereferencing을 해서

5가 나오게 된다

*ptr = 5를 출력하게 되는 것이다

 

 

그렇다면 위 ppt 코드에서

포인터 변수들이 각각

어떻게 출력되는지 보자

 

int x = 5;

가 정의되었고 밑에

int *ptr = &x;

라는 포인터 변수가 정의되었다

 

일반 변수 앞에 &를 붙이면

해당 변수가 저장된 메모리주소를 반환한다고

이전 ppt에서 말했으므로

포인터변수 ptr에는 int변수 x가 저장된

메모리 주소가 저장되어있게된다

 

cout << &x << endl;

에서 &x는 x가 들어있는 주솟값이므로

x가 들어있는 주솟값이 출력되게된다

 

cout << ptr << endl;

은 뭐가 출력이 될까?

위에서 강조했지만 포인터변수는

그냥 단순한 변수임을 기억해야한다

메모리 주소를 담는 단순한 변수이기때문에

아무것도 없이 그냥 ptr만 나온다면

이는 당연히 ptr이 담고있는

메모리 주솟값을 반환한다

그리고 이는 x의 주솟값이므로

&x와 동일한 값이 출력될 것이다

 

cout << &ptr << endl;

그렇다면 &ptr은 뭐가 출력이되게 될까?

차근차근 생각을 해보면 쉽다

앞에서 변수명 앞에 &가 붙으면

해당 변수가 저장된 메모리주소를 반환하다고 했다

그럼 &ptr은 ptr이라는 포인터변수가

저장된 메모리 주소를 가리킬 것이다

 

포인터변수도 단순한 변수이기때문에

포인터변수의 데이터를 저장하고있는

메모리 공간이 존재한다

이 주소를 찾으면된다

 

위 ppt에 보면 x의 메모리 주소를 담고있는

ptr변수가 stack의 위에 정의되어있다

이 메모리 주솟값을 반환할 것이다

 

cout << *ptr << endl;

마지막으로 이것은 무엇을 반환할까?

앞에서 말했듯

*ptr은 dereferencing이다

ptr변수에 저장된 메모리 주소에 접근해

그 데이터를 가져온다

따라서 *ptr은 당연히 5를 출력할 것이다

 

 

 

 

ptr앞에 *이 붙은 *ptr은

마치 변수의 이름처럼 사용할 수 있다

 

위 사진을 잘 보자

포인터변수 ptr은 7번방에 위치해있고

15라는 주솟값을 갖고있다

 

그렇다면 *ptr을 쓰게되면

ptr이 갖고있는 15번방에 접근해

15번방에 들어있는 50이라는

데이터를 반환하게 되는 것이다

 

*ptr을 15번방에 있는 50이라는

데이터의 이름처럼 취급하는 것이다

 

따라서

*ptr = *ptr + 5를 하게 된다면

*ptr은 50이라는 데이터의 변수명처럼

사용되므로

50에서 5를 더한 55가

15번방에 저장되게 되는 것이다

 

 

Dynamic Memory Allocation

 

동적 메모리 할당에 대해 알아보자

 

프로그램이 실행되는 runtime에 

메모리를 할당하는 것을

dynamic memory allocation

한국어로는

동적 메모리 할당이라고 한다

 

메모리에는 대표적으로

Heap과 Stack이 있는데

Heap이 dynamic allocation을 할때

저장하는 부분으로

프로그래머가 작성할 수 있다

stack은 로컬변수같은 것들을

컴파일 할 때 컴파일러가

메모리를 할당하는 곳이다

 

 

 

그렇다면 코드상에서

어떻게 동적 메모리 할당을 하는지 알아보자

 

왼쪽이 c언어이고 오른쪽이 c++이다

 

c언어에서는 malloc을 통해서

heap에 동적으로 메모리를 할당해준다

 

만약 malloc이 제대로 작동을했다면

array 변수에는 메모리의 첫번째 주소가 출력되고

그렇지 않다면 NULL이 반환되게 된다

 

그런 다음 사용이 끝나면

free로 할당을 해제해준다

 

이 과정을 c++에서 해보자

 

c++에서는 header에 new를 선언해주고

malloc대신 new로 동적 할당을 해준다

array = new int[n] 으로 정의할 때

n은 size이다

 

그리고 delete[] array;

와 같은 방식으로

메모리 할당을 해제해준다

 

 

 

제대로 할당이 되지 않더라도 error를 띄우지않고

그냥 Null pointer를 반환하기 때문에

반드시 if문으로 nullptr 여부를 체크해줘야한다

 

 

 

할당 해제에 관한 부분인데

변수에 대한 사용이 끝났으면

반드시 delete를 통해

메모리할당 해제를 해주어야한다

 

해제를 해주지 않으면

계속 그 공간을 잡고있기때문에

memory leak 방지를 위해

메모리 공간을 해제해줘야

계속해서 재사용할 수 있다

 

 

 

class pointer에 관한 내용이다

 

어떤 class를 포인터 변수로 정의했을 때

class 내부 메소드에는 어떻게 접근할까?

 

위 ppt 코드 예시를 보면

vector<int>* myVector = new vector<int>();

와 같은 방식으로

빈 int vector object를 dynamic하게

myVector라는 변수명에 할당시켰다

 

vector는 하나의 object인데

이를 포인터변수로 정의하면

어떻게 사용할 수 있을까

 

방법은 크게 2가지가 있는데

1. *myVector

2. myVector

로 이용하는 방법이다

 

위에서 말했듯이

*myVector와 같은 방식으로쓰면

그 변수의 이름처럼 쓰이기 때문에

class object에 있는 메소드들을 바로 사용 가능하다

 

따라서 (*myVector)[0]과 같은 방식으로

vector의 첫번째 원소에

접근가능하다

또한 (*myVector)은 .(dot)으로

class의 메소드를 이용할 수 있다

 

두 번째는 myVector다음에 ->(화살표)로

접근하는 방식이다

myVector -> push_back(10);

이렇게 vector값에 element를 추가해줄 수 있다

 

첫 번째와 두 번째가 다른 점은

두 번째 방법으로 하면

[0]와 같이 index로 vector에 접근한다던가

++ 메소드는 쓰지못한다

 

 

 

포인터 변수를 dereferencing 할 때는

포인터변수의 타입이 매우 중요해진다

 

왜냐하면 메모리 주소로 가서 타입에 따라

몇 바이트를 가져올지가 중요해지기 때문이다

 

이게 무슨 소리인가하면

int pointer라고 하면

메모리 주소로 가서 4바이트만 읽을 것이다

string이라고 하면 시작주소부터를

string으로 취급할 것이다

 

특히 pointer 산술연산을 할 때

매우 중요해진다

 

intPtr + 2 라고 하면

int는 한 개에 4바이트이기때문에

2개를 더하면 8바이트를 더한 값을 반환한다

 

charPtr + 2라고 하면

char이 1바이트이기 때문에

2개를 더하면 2바이트를 건너뛴 위치를 반환한다

 

 

 

 

int *aPtr = new int(5);

로 heap에 4바이트의 int 공간에

5라는 값을 할당했다

이런 상황에서 다양하게 출력을 해보자

 

aPtr은 포인터변수가 담고있는 주솟값

즉, 5가 저장되어있는 값을 출력할 것이다

 

*aPtr은 당연히 주솟값이 담고있는

변수인 5를 출력할 것이다

 

&aPtr은 aPtr이라는 포인터변수가

저장된 주솟값을 출력할 것이다