본문 바로가기

포인터

1. 포인터 변수

 

포인터 변수(pointer variable)는 주소값을 저장하는 변수다.

메모리 공간은 1바이트 단위로 구분되어 있고, 단위에는 숫자가 매겨져 있다.

이 숫자를 주소, 번지 또는 어드레스(address)라 한다.

 

다음과 같이 변수가 선언되어 있다고 하자.

 

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main()
{
    char ch = 'S';
    int num1 = 6, num2 = 3;
    
    ...
}
cs

 

실제 메모리 공간에는 다음과 같이 저장된다.

(다음과 같이 나란히 저장될 수도, 그렇지 않을 수도 있다.)

 

참고로 주소값을 2진수로 나타내려면 32자리를 나타내야 하기 때문에 보통 16진수로 나타낸다. 

 

 

S, 6, 3은 각각 0xffffdded, 0xffffddee, 0xffffddf2에 저장된 것이다.

따라서 다음과 같이 말할 수 있다.

 

 ch 는 0xffffdded에 저장되었다.

 num1 은 0xffffddee부터 0xffffddf1까지 4바이트에 걸쳐 저장되었다.

 num2 는 0xffffddf2부터 0xffffddf4까지 5바이트에 걸쳐 저장되었다.

 

 

이렇게 되면 다음과 같은 의문이 생긴다.

 

 num1 이 0xffffddee, 0xffffddef, 0xffffddf0, 0xffffddf1의 4바이트에 걸쳐 저장된다면,

 &num1 의 값은 무엇인가?

 

답은 0xffffddee다.

C언어에서 위치는 시작 주소만으로 표현하기 떄문이다.

그러니까 다음과 같이 말해도 무방하다.

 

 num1 은 0xffffddee에 할당되어 있다.

 

여기서 0xffffddee라는 것은 주소이다.

값이라는 것은 변수에 저장할 수 있다는 것이다.

이 변수가 바로 포인터 변수다.

 

포인터 변수는 다음과 같이 선언한다.

 

1
2
3
int* ptr1;
int *ptr2;
int * ptr3;
cs

 

*은 자료형과 변수 이름 사이에만 있다면 어디든 상관 없다.

필자는 1행과 같이 쓴다.

 

위 선언문은 다음을 의미한다.

 

 int* :  int형 변수의 주소값을 저장하는 포인터 변수

 ptr1 :  그 변수의 이름은 ptr1  

 

그러니까 double형 변수의 주소값을 저장하고 싶다면  double* ptr; 과 같이 선언하면 된다.

 

 

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main()
{
    int num = 3;
    int* ptr = &num;    
 
    ...
}
cs

 

위 코드를 잘 보자.

int형 변수의 주소값을 저장하는 포인터 변수  ptr 은 int형 변수인  num 의 주소값을 저장한다.

이를 그림으로 나타내면 다음과 같다.

 

 

물론 실제로는 다음과 같이 저장된다.

 

하지만 보통 다음과 같이 나타낸다.

 

 

이런 상황을 다음과 같이 표현한다.

 

 ptr 이  num 을 가리키고 있다.

 

이는 다음을 의미한다.

 

포인터 변수  ptr 에  num 의 주소값이 저장되어 있다.

 

 

그런데 한 가지 이상한 점이 있다.

앞의 그림에서는 포인터 변수의 크기를 4바이트로 표현했는데,

double형 포인터와 int형 포인터는 크기가 같을까, 다를까?

 

포인터라는 것은 주소값을 저장하는 것이다.

주소값이라는 건 변수의 위치를 나타낸다.

따라서 double형이 8바이트라고 해서 주소값이 8바이트가 되는 것도,

char형이 1바이트라고 해서 주소값이 1바이트가 되는 것도 아니다.

주소값은 char형 변수든, double형 변수든 int형 변수든 자료형에 관계 없이 같다.

 

하지만 운영체제에 따라서는 달라진다.

64비트 OS의 경우 0x00000000 00000000 ~ 0xFFFFFFFF FFFFFFFF의 주소를 할당할 수 있고,

32비트 OS의 경우 0x00000000 ~ 0xFFFFFFFF의 주소를 할당할 수 있다.

따라서 32비트 OS에서는 4바이트고, 64비트 OS에서는 8바이트다.

범위로 따지면 32비트 OS에서는 unsigned int, 64비트 OS에서는 unsigned long long이다.

 

현재 많은 컴퓨터들이 64비트 시스템을 채택하고 있지만

32비트 OS라고 가정해도 포인터를 이해하는 데에는 문제 되지 않으므로, 32비트 OS로 가정한다.

 

 

 

 

2. 포인터 형

 

앞에서 int형 변수를 가리키려면  int* , double형 변수를 가리키려면  double* 로 선언하면 

된다고 했다.

이처럼 포인터에도 포인터 형(pointer type)이 있다. 변수의 자료형처럼 포인터에도 타입이 

있는 것이다.

 

포인터 형은 기본적으로 다음과 같은 형식으로 표기한다.

 

TYPE* ptr;

 

예를 들면 다음과 같다.

 

int* ptr1;

double* ptr2;

char* ptr3;

 

 

 

 

3. * 연산자와 & 연산자의 관계

 

여기서의 * 연산자는 곱셈 연산자가 아니라, 간접지정 연산자다. 

곱셈 연산자는 이항 연산자지만, 포인터에서 * 연산자는 단항 연산자. 위치에 따라 달라진다.

& 연산자는 알다시피 피연산자의 주소값을 반환하는 연산자다.

 

* 연산자는 다음과 같이 사용한다.

 

*(주소값)

*(주소값이 저장된 포인터 변수)

 

* 연산자는 피연산자에 주소값이 온다.

그리고 그 주소에 있는 값을 반환한다.

포인터 변수가 올 경우 그 포인터 변수가 가리키는 변수를 반환한다. 

 

int num = 3;

int* ptr = &num;

 

그러니까, 위 코드에서  *ptr  num 이다.

따라서 다음 두 문장은 같은 문장이다.

 

*ptr = 3;

num = 3;

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
int main()
{
    int num = 3;
    int* ptr = &num;
 
    printf("num: %d\n", num);
    printf("*ptr: %d\n"*ptr);
 
    return 0;
}
cs

 

 

포인터 변수  ptr 에는 int형 변수  num 의 주소값이 저장되어 있다.

따라서  *ptr 은  ptr 에 저장된 위치에 있는 값,  num 의 주소값에 저장되어 있는 값, 

즉 변수  num 에 저장된 값  3 을 반환하게 된다.

 

 

예를 들어 다음은

 

*ptr = 3;

 

다음과 같다.

 

num = 3;

 

 

여기서 한 가지 알아두면 좋을 점이 있다.

다음 식을 보자(대입 연산자가 아닌 등호로 보자).

 

① ptr = &num

② *ptr = num

 

ptr = &num이다. 따라서 식 ②는 다음과 같이 바꿔 쓸 수 있다.

 

*(&num) = num

 

뭔가 보이지 않는가?

그렇다.  * 연산자와 & 연산자는 서로 상쇄된다.

& 연산자는 주소를,  * 연산자는 그 주소에 있는 값을 반환하니, 어찌 보면 당연한 이야기다.

 

 

 

 

4. 포인터 형이 필요한 이유

 

앞에서 * 연산자를 통해 주소에 있는 값을 반환한다고 했다.

그런데 포인터 형이 없다면 여기서 문제가 있다.

C언어에서 주소는 시작 주소로 표시한다. 따라서 * 연산에는 다음의 정보가 필요하다.

 

① 어디에 있는 값을 반환할 것인가?

② 그 주소부터 어디까지가 반환할 데이터인가?

③ 그 데이터는 정수형인가, 실수형인가?

 

만약에 포인터 변수 ptr에 0xffffde34라는 주소값이 저장되어 있다고 하자.

그리고 *ptr을 출력한다고 하자.

 

여기서 문제가 생긴다.

0xffffde34부터 읽어와서 출력해야 된다는 건 알겠는데, 얼마나(몇 바이트를) 읽어야 하는지,

또 정수인지 실수인지가 문제다.

 

이를 위해 포인터 형을 명시하는 것이다.

ptr이 int*형이라면 4바이트를 읽어와 정수로 해석하면 되고,

double*형이라면 8바이트를 읽어와 실수로 해석하면 된다.

 

그러니까 포인터의 형은 메모리 공간을 참조하는 기준이 된다.

따라서 자료형과 포인터 형은 일치해야 한다.

 

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    int num = 3;
    double* ptr = &num;
 
    printf("num: %d\n"*ptr);
 
    return 0;
}
cs

 

위 코드의 출력 결과를 예측할 수 있겠는가?

 

사실 컴파일은 된다. 경고는 있어도 에러는 발생하지 않기 때문이다.

하지만 출력 결과는 예측할 수 없다.

 

 

출력하려는 범위는 0xffffddee~0xffffddf1인데,

실제로 출력되는 것은 0xffffddee~0xffffddf5다.

하지만 0xffffddf2~0xffffddf5에는 쓰레기값이 채워져 있기 때문에, 출력값을 예측할 수 없다.

 

정리하면, 포인터 형은 포인터 변수가 가리키는 변수의 값을 참조하는 기준이라는 것이다.

이 기준이 없으면 정확한 값을 출력할 수 없다.

 

 

 

 

5. 쓰레기값

 

변수를 선언하기 전의 메모리에는 ⒜의미없는 값들이 들어있다.

하지만 변수를 선언하고 나면 쓰레기값으로 찬다.

아직 이야기하기 이르지만, 이는 스택을 할당하기 위함이다.

변수가 선언되면 그 변수의 앞뒤를 0xCC로 채우게 되는데, 이 0xCC가 쓰레기값이다.

 

아직 몰라도 되니, 변수를 초기화하지 않으면 쓰레기값이라는 이상한 게 들어있다고만 알아두자.

 

 

 

 

 

 

 

 

⒜의미없는 값: 사실은 그 주소를 사용한 흔적, 그러니까 원래 그 주소에 있던 값.

정확히 말하면 초기화 없이 주소만 할당해주기 때문에 원래 그 위치에 있던 값이 남아있는 것이다.

'Programming > C' 카테고리의 다른 글

매개변수  (0) 2019.03.24
포인터와 배열의 관계  (0) 2019.03.17
1차원 배열  (0) 2019.03.15
변수  (0) 2019.03.07
함수  (0) 2019.03.06