Pointer (포인터)

C언어 프로그래밍 공부를 힘들게 하는 원인 중의 하나다.

개념은 간단하다.

포인터는 주소를 다룬다.

주소란 무엇인가? 

예를 들어보자.

int a;

a = 4;

Integer(정수)형 변수 a를 만들고, 거기에다가 값 4를 저장한다.

이는 변수a를 위해 메모리에 저장공간을 만들고, 

공간 'a'에 값 4를 저장한 것이다.

메모리의 저장공간은 모두 고유의 숫자주소를 가지고 있다.

사람들이 고유의 숫자 주민번호를 가지고 있듯이.


프로그래밍할 때 저장공간을 쉽게 기억하기 위해 'a'라는 이름을 쓰지만

사실은 'a'도 숫자 주소를 가지고 있다.

'a'의 주소를 알고 싶다고?

printf ("%d", &a);

&a는 a와 매칭된 메모리 저장공간의 주소를 의미한다.

어느 변수든지 기호 &를 앞에 붙이면 해당 저장공간의 주소가 된다.


주소를 다루기 위해 포인터라는 것을 만들었다.

포인터는 주소를 저장할 수 있도록 한다.

int *b;

포인터를 만드는 방법은 변수이름 앞에 기호 *를 붙이면 된다.

이제 변수 b에 저장되는 값은 주소로 인식된다.

100을 저장한다. 주소 100번지가 된다.

1000을 저장한다. 주소 1000번지가 된다.


b = &a;

b에 변수 a에 매칭된 저장공간의 주소를 저장하는 것이다.


왜? 굳이 포인터라는 걸 만들었나?

포인터를 통해 복잡한 일을 쉽게 하기 위해서다.

어떤 복잡한 일인가?

'주소 000번지에 가서, 그 번지에 저장되어 있는 값을 읽거나 쓰는 일이다.'


b = 100; 

변수 b는 주소 100번지를 저장한다.


printf ("%d", *b);

변수 b가 저장한 값 100을 주소로 인식하고, 그 100번지에 저장되어 있는 값을 읽어오는 것이다.

포인터 변수 앞에 기호 *를 쓰면, 그 번지에 저장된 값을 읽거나 쓸 수 있다.


단, 헷갈리지 말자.

int *b; 이건 변수 b가 포인터라는 것을 정의하는 것이다. 

포인터 변수를 만들 때도 기호 *를 사용한다.


*b = 10;

변수 b가 저장한 값 100을 주소로 인식하고, 그 100번지에 값 10을 쓰는 것이다.

기호 *을 붙여 값을 쓰는 용도로 사용한 것이다.


배운 것을 정리해보자.

1. a = 10;

2. b = &a;

3. *b = 20;

4. printf ("%d", a);


라인 1. 변수 a에 값 10을 저장한다.

라인 2. 변수 a에 매칭된 저장공간의 주소를 b에 저장한다.

라인 3. b에 저장된 주소는 a것이므로, a에다가 20을 저장한다.

라인 4. 위에서 a에 저장된 값이 20으로 바뀌었으므로 10이 아닌, 20을 출력한다.


포인터는 어디에 필요한가?

컴퓨터를 생각해보자.

키보드, 디스크, 비디오카드, USB메모리, CPU 등 여러 가지 장치들이 있다.

프로그램으로 이 장치 들에게 명령을 내리고 싶다면, 어떻게 할 수 있을까?

각 장치들은 고유의 주소를 가지고 있다.

이 주소들을 이용하여 명령을 내릴 수 있다.

예를 들어, 비디오카드가 100번지에 있다면, 100번지에 값을 읽거나, 써서 비디오카드에게 명령한다.

이를 가능하게 하는 것이 포인터다.

따라서 포인터를 아는 것은 시스템을 내 휘하에 두고 이러저러 명령을 내릴 수 있는 힘을 얻는 것이나 마찬가지다. 


int *a

포인터 변수를 선언한 것이다.

하나하나 뜯어서 살펴보자.

a는 포인터 변수의 이름이다.

*: a 앞에 붙은 기호 *은 변수 a가 포인터임을 나타낸다. 기호 *이 없으면 포인터가 아니다.

int: 이것의 의미를 잘 이해해야 한다. 포인터 변수 a를 이용하면 어떤 주소 위치에 값을 쓸 수도 있고, 읽어올 수도 있다. 이 때 그 값의 형(type)을 나타낸다. 즉 변수 a를 이용하면 읽거나 쓰는 값은 int이고, 따라서 4바이트 단위로 읽거나 쓸 수 있게 된다. (32비트 운영체제일 경우에 말이다. 64비트 운영체제이고 int가 64비트이면 8 바이트 단위로 동작한다.)


int **a

포인터 변수이긴 하다.

여기에는 기호 *이 두 개가 붙었다.

이중 포인터라는 것이다.

이것의 의미도 하나하나 뜯어보자.

a: 포인터 변수의 이름이다.

* (오른쪽에서 첫 번째): 변수 a가 포인터임을 나타낸다.

int * (오른쪽에서 두 번째 *): 변수 a를 이용하여 읽고 쓸 수 있는 값의 데이터 형이 int *, 즉 정수 포인터

 

예제프로그램


변수 a를 만들고, 거기에다가 10을 넣었다.

포인터 변수 b를 선언했다.

포인터 변수 c를 선언했다.

b를 이용해서는 int값을 읽고 쓸 수 있으므로, b에는 변수 a의 주소를 넣는다.

c를 이용해서는 int *값을 읽고 쓰기 때문에, c에는 int 포인터 변수 b의 주소를 넣는다.

출력결과는 모두 10, 10, 10

첫 번째 10: 변수 a의 값을 출력한 것


두 번째 10: 포인터 변수 b를 이용하여, a의 주소를 이용해서 int값을 읽어서 출력했다.

세 번째 10: 이중 포인터 변수 c를 이용했다. **c == *(*c)로 이해하면 쉽다. *c는 b에 저장된 a의 주소를 읽는다. *(*c)는 a의 주소를 이용하여 a에 저장된 값을 읽는다.


malloc

메모리를 할당받는 데 사용하는 함수이다.

원하는 크기의 메모리 공간을 할당받을 수 있다.

이 함수의 인자는 할당받기를 원하는 크기이다.

크기는 바이트 단위이다.

malloc(100) : 100 바이트 크기의 공간할당을 요청하는 것이다.

이 함수의 반환값은 주소이다.

할당된 공간의 시작주소이다.

반환값이 주소이므로 이 주소는 포인터 변수에 저장해야 한다.



malloc casting

malloc함수의 원형은 아래와 같이 생겼다.

 void * malloc ( int size)

반환형이 독특하게 생겼다.

void *

생긴 것을 봐서는 포인터이다. 

왜냐면 *이 붙었으니까.

이것으로부터 유추해보면, 포인터 즉 주소값을 반환한다는 뜻이다.

좀 더 추리해보면, 할당받은 공간과 관련된 주소일 것이다.

정확하게 말하자면, 할당받은 공간의 시작주소가 반환값이 된다.

그러면 void는 무슨 뜻인가?

void의 사전적인 의미는 '비어있는' '없는' '무효' 이다.

데이터 형이 없다는 뜻으로 생각하면 된다.


왜 void, 데이터형을 없는 것으로 해 놨을까?

그건 공간을 마음대로 활용할 수 있도록 해 주기 위해서이다.

공간에 int 값을 쓰거나 읽을 용도로 할당받는 경우도 있고,

char 값 용도로 쓸때도 있다.

그러니 데이터형을 일단 없다고 하고, 

실제로 할당받는 순간에 그 형을 정해서 쓰기 위해서 void로 해 놓은 것이다.


공간을 할당받는 순간에 데이터형을 결정하다. casting

int *k;

k = (int *)malloc(100);

char *m;

m = (char *)malloc(100);

둘 다 100 바이트 공간을 할당받는다.

하지만 위의 것은 int형 값들을 읽고 쓰는 공간으로 만들기 위해 (int *)로 캐스팅한다.

아래는 char형 값을 위한 것이다.


할당받은 공간에 값을 저장해보자.

*k = 1;

값 1을 쓰는 것이다.

int형 1을 쓰는 것이다.

int형은 4바이트이므로,

할당받은 공간 100바이트 중 4바이트에다가 1을 쓰는 것이다.

*m = 1;

역시 값 1을 쓰는 것이다.

하지만 이번에는 char형 1을 쓰는 것이다.

char형은 1바이트이므로,

1바이트에다가 1을 쓰는 것이다.


free

컴퓨터의 메모리공간은 유한한다.

malloc으로 할당받은 공간을 다 썼으면 반환해야 한다.

이 때 사용하는 함수가 free이다.

이 함수는 인자로 반환하고자 하는 공간의 시작주소를 받는다.

malloc했을 때 할당받은 공간의 시작주소를 얻을 수 있으니,

이것을 잘 간직해 두었다고,

free할 때 쓰면된다.

free(k);

free(m);

k와 m은 각각 할당받은 공간의 시작주소를 가지고 있으니,

이렇게 하면 되는 것이다.


free를 안하면 어떻게 될까?

메모리 공간이 모자르니 다른 프로그램이 손해를 떠안아야 한다.

프로그램이 아예 실행이 안 될 수도 있고,

실행되는 도중에 '더 이상 메모리를 얻을 수 없습니다.'하고,

중간에 종료되기도 한다.

그러니 malloc으로 할당받은 공간 중에 필요없게 된 것들은

그 때 그 때 free해서 반환하는 것이 에티켓이다.


프로그램 종료는 자동 free된다.

프로그램이 끝나면,

수행되면서 할당받았던 공간들이 자동 반환된다.

그러니, 종료되고 나서, '아차, free 안했네' 걱정할 필요없다. 




반응형
LIST


구조체 (Struct)

C언어에서 구조체를 이용하면,

데이터형이 다른 여러 데이터를 묶어서

새로운 데이터형을 만들 수 있다.


예를들어, 

int와 char 데이터를 하나씩 묶어서

새로운 데이터 타입을 만드는 것은

아래와 같이 할 수 있다.

struct A

{

   int a;

   char c;

};


이렇게 하면, 

새로운 데이터형 struct A가

생겨나게 된다.


이를 사용하는 방법은 다음과 같다.

struct A x;

x.a = 10;

x.c = 'h'; 

sturct A가 데이터형이므로,

x는 struct A형의 변수가 된다.

그리고,

x 안에는 int를 저장하는 a가 있고,

또 char를 저장하는 c가 있는 것이다.

따라서,

각각을 x.a와 x.c로 부르고,

그곳에 값을 넣거나, 읽는 것은 일반 변수와 

똑같이 하면 된다.


구조체변수 대입( Assignment)

여기까지는 일반적인

구조체 정의와 사용법에 대한 얘기이니

별로 새롭지는 않다.

그런데, 아래 프로그램은 조금 생소할 수도 있다.


line 11:

구조체 변수 m과 n을 만들었다.

line 12-13.

m에 데이터를 채워넣는다. n에는 아무 것도 안 들어간 상태이다

line 14:

m을 그대로 n에 대입하면, 구조체 속의 a는 a로, 

c는 c로 자동 대입된다.

line 15:

대입된 결과를 n을 출력해 봄으로써 알 수 있다.


구조체변수 비교 (Comparison): 이건 불가능

대입(assignment)는 당연하게 느껴지겠지만,

비교도 당연히 되리라고 생각해서는 안된다.

아래 프로그램을 보자.

구조체 변수 m과 n이 서로 같은지 

line 16에서 비교하려고 하고 있다.

그런데, 컴파일 과정에서 "이러시면 안됩니다."하고 오류 메시지가 발생한다.


구조체변수끼리의 비교를 하려면,

멤버변수 별로 해야 한다.

아래 프로그램처럼 말이다.

line 16을 보면

구조체 변수 n과 m이 서로 같은지를 비교하기 위해

멤버 a는 a끼리,

멤버 c는 c끼리 비교하고 있다.


자기참조 구조체 (Self Referential Struct)

이것이 무엇인고,

말도 안되는 거 하나를 보자.

struct A

{

   int k;

   struct A a;

};

무엇이 이상한가 하면,

struct A를 정의하고 있는데,

그 안에 자기자신을 포함하고 있다.

아직 자기자신이 다 생기지도 않았는데, 

자기자신이 그 안에 들어있으면,

어쩌라는 말이냐,, 이건 마치 무한 recursion과 같은 일이다.

결론부터 말하면, 이런 구조체는 생길 수가 없다.


그런데,,,

,,,

,,, 기대하시라,,

이런 건 가능하다.

struct A

{

   int k;

   struct A *a;

};

무엇이 달라졌는가?

자기자신을 포함하는 대신에,

자기자신처럼 생긴 것에 대한 주소를 저장하고 있다.

즉,

변수 a는 포인터다.

struct A 타입의 데이터가 저장된 곳의 주소를 저장하는 포인터 변수이다.

갑자기 철학적 질문이 된다.

나는 나를 품지 못하되,

나처럼 생긴 애가 어디있는지 가리키는 손가락은 가질 수 있구나.

이런 것을 소위

자기참조 구조체 (Self referential struct)라고 한다.

이것은 앞으로 데이터구조에서

아주 많이 사용되니 개념을 잘 이해하고 넘어가야 한다.


혹시 이해에 도움이 될까 싶어,

발로 그린 그림이지만, 올려본다.

포인터 변수 a가 다른 struct A형의 데이터의 주소를 가리키는 것을 보여준다.



구조체에 대해서 설명합니다.



반응형
LIST


배열 (Array)에 대한 복습

데이터구조에서 

배열을 많이 사용하기 때문에

이쯤에서 한 번쯤

복습해 보는 것이 좋겠다.


배열은,

같은 데이터형 (data type)을 갖는 

여러 개의 변수들을 손 쉽게 만들 수 있는 방법이다.


int a[6];

배열을 선언할 때는

맨 앞에 데이터형을 쓰고,; 위에서는 int

그 다음에 그 배열의 이름을 붙이고; 위에서는 a

마지막에 몇 개를 만들 것인가를 [ ] 안에 지정한다.; 위에서는 6개


이 때 생기는 6개 배열변수들의

이름은 아래와 같다.

이렇게 번호를 이름삼아 부르는 데, 이 번호를 index 라고 한다.

a[0], a[1], ... a[5]


index는 0부터 시작

이걸 절대 잊지 말아야 한다.

사람들이 물건을 셀 때는

첫 번째, 두 번째, ... 이렇게 1을 기준으로 하지만,

C언어에서는

0  번째, 1 번째,... 이렇게 0이 기준이 된다.


index는 n-1에서 끝

이것도 절대 잊지 말아야 한다.

배열 크기가 n일 때,

마지막 index는 당연히 n-1이 된다.

왜냐면, 0부터 시작했으니.


배열 (Array) 해부

배열은 메모리 상의 연속된 공간에 위치하게 된다.

쉽게 얘기하면,

크기 n인 배열을 선언하면,

n개의 배열변수들이

메모리 상의 연속된 위치에 생기게 된다는 것이다.

서로 서로 떨어질 수가 없다.

일심동체인 것이다.

아래 프로그램을 보자.

크기 6인 int 배열을 만들고,

각 배열변수들의 주소를 출력해보면

'4' 바이트 간격으로 모두 연속된 것을 볼 수 있다.

예를 들어,

a[0]는 16진수 주소 279138에 있고, 4 바이트를 차지한다. 왜냐면 int니까.

그러면

a[1]은 16진수 주소 27913c부터 시작한다. 이런 식이다.


이번에는 char배열을 예로 프로그램을 보자

크기 6인 char배열을 만들었고,

각 배열변수의 주소를 이번에는 10진수로 출력해 보았다. 

1바이트 단위로 증가하는 것을 볼 수 있다.

왜냐면, char가 1바이트이니까.


정리하자면,

배열을 선언하면, 각 배열변수들은 연속된 메모리 공간에 위치한다.

바꿔 말하면,

배열은 메모리에 그 만한 연속공간이 있을 때만 만들 수 있다. 


다차원 배열 (Multi-dimensional Array)

2차원 배열을 생각해보면

다차원 배열이 뭔지 금방 알 수 있다.

2차원 배열은 지도를 떠올리면 금방 이해된다.

또는 지뢰찾기 같은거,,,



2차원 배열은 행과 열 개수만큼의 공간이 만들어진다.

예를 들어, 2x2 배열이면 모두 4개의 배열변수가 만들어진다.

이 때도 index는 0부터 시작이다.

그리고 각 배열변수의 이름은 

두 개 index의 조합으로 나타내어 진다.

int a[2][2];

a[0][0], a[0][1], a[1][0], a[1][1]


3차원 배열

이거는 큐브를 생각하면 된다.

그리고,

3차원 배열은 x, y, z축으로 이루어진 공간을

생각하면 되는데,

예를 들어, 2x2x2 배열이면 모두 8 개의 배열변수가

만들어지고,

배열변수는 3개 index의 조합으로 이름이 지어진다.


int a[2][2][2];

a[0][0][0], a[0][0][1], ...., a[1][1][1] : 모두 8개


다차원배열의 비밀

2차원 배열이면 메모리 상에 2차원으로 생길 줄 알았지?

3차원 배열이면 메모리 상에 3차원이 만들어 지고,,

그러면 4차원은 어떻게 하려고,,


다차원 배열은 

다 뻥이다.

몇 차원이고 간에

메모리상에는 1차원 배열처럼 생긴다.

증명이 필요하다구? 


아래 프로그램을 보자.

3차원 배열 a[2][2][2]를 만들었다.

만약, 이게 진짜로 3차원이면,

주소가 연속이면 안되고, 위층, 아래층 이렇게 나와야 한다.

그런데,

결과를 보면 모두 1바이트 단위로 인접해 있음을 알 수 있다.


다차원 배열, 다 소용없는기라.

그기 다 사기인기라.

결국 1차원 짜릴, 그리 뻥치고 다닌기라.





배열에 대해서 설명합니다.



반응형
LIST

+ Recent posts