티스토리 뷰

C, C++

[ C언어 ] 16. 포인터

RiKang 2018.01.02 21:59

Table of Contents


개요

기본 문법

간접 참조

오프셋

포인터와 배열




1. 개요


 포인터는 다른 프로그래밍 언어에서는 찾아보기 힘든 C언어의 문법입니다. 또한 고급 언어이면서도 저급 언어에 가까운 C언어의 특징을 잘 나타내주는 요소이기도 합니다. 특이하게도 메모리의 주소를 다루기 위한 문법이기 때문입니다.


 포인터를 공부하시는 분이라면, 지금까지 C언어를 공부하면서 메모리에 여러가지 변수들을 저장하고 사용해 오셨을 겁니다. 그런데 변수들을 저장하는 컴퓨터의 메모리는 4byte, 8byte 쯤은 모래알처럼 느껴질 만큼 방대한 용량을 가지고 있습니다. 어떻게 C 컴파일러는 그 방대한 메모리에서 변수의 값을 정확하게 읽어내는 것일까요? 그건 메모리의 각 칸에 '주소(address)'가 있기 때문입니다. 마치 우리가 아파트의 각 집에 1404호 같은 번호를 붙여놓은 것과 비슷한 이유이지요.


 포인터 변수란 이러한 주소를 저장하기 위한 변수입니다.




2. 기본 문법


 일반적인 변수의 선언 형식이 변수형 변수이름;라고 한다면, 포인터 변수를 선언할 때는 변수형 *변수이름;으로 선언할 수 있습니다. int 변수를 저장할 메모리의 주소로서 사용하려면 int *변수이름;이고 char 변수를 저장할 메모리의 주소로서 사용하려면 char *변수이름;이 되는 식입니다. 이제 아래의 프로그램을 따라서 코딩하고 출력 결과를 확인해보시길 바랍니다.


1
2
3
4
5
6
7
8
9
10
11
//code by RiKang, weeklyps.com
#include <stdio.h>
 
int main() {
    int a=1;
    int *p; // 포인터 변수의 선언
    p = &a; // p에 '변수 a의 메모리에서의 주소'를 저장
    printf("주소 = %p\n",p); // p를 16진수로 출력
    printf("값 = %d\n",*p);  // p에 저장되어 있는 값을 출력
    return 0;
}
cs

출력

주소 = 0x7ffc38f5f784

값 = 1


 p = &a;

 이미 많이 사용하셨을 scanf("%d",&a); 같은 구문에서 &a 는 변수a의 주소를 나타냅니다. &는 변수 이름을 통해 변수의 주소를 반환하는 일종의 연산자인 것입니다. (주소 연산자라고 합니다.) 따라서 p=&a; 라는 구문을 통해 정수형 변수 a의 주소를 포인터 변수 p에 저장할 수 있습니다.


 printf("주소 = %p\n",p);


 %p는 포인터 변수를 출력하기 위한 서식 문자입니다. 포인터 변수를 받은 %p 는 그 값을 16진수로 표현해 줍니다. 출력 결과에 나오는 0x는 뒤에 나오는 숫자가 16진수임을 알려주는 표식입니다. (16진수는 한 자리에 0 ~ 15의 수가 들어갈 수 있습니다. 그런데 10부터 15까지의 숫자는 한 번에 표현할 방법이 딱히 없으므로 a,b,c,d,e,f 의 알파벳으로 표기합니다. 따라서 a=10, b=11, .., f=15를 나타냅니다.)



 각 칸을 1byte로 표현한 메모리의 상태를 나타내면 위와 같이 됩니다. (저장 방식에 따라 0001 이 아니라 1000으로 저장되어 있을 수 있습니다.)


 정수형 변수는 4byte이기 때문에 4칸을 차지하고 있으며, 변수 이름은 a로 설정되어 있습니다. 이 변수의 첫 번째 byte 주소는 0x7ffc38f5f784, 마지막 byte 주소는 0x7ffc38f5f787로서 이 사이에 있는 4개의 byte가 할당되어 있는 것입니다.


 &a를 저장한 포인터 p에는 첫 번째 byte의 주소가 저장되어 있습니다.




3. 간접 참조


 간접 참조란 포인터 변수가 저장하고 있는 주소를 찾아가서 그 주소에 있는 변수를 사용하는 연산자입니다. &가 변수를 통해 주소를 찾는 연산자라면, 간접 참조 연산자는 반대로 주소를 통해 변수를 찾는 연산자라고 볼 수 있습니다. C언어에는 간접 참조 연산자로 *이 존재합니다.


1
2
3
4
5
6
7
8
9
10
11
//code by RiKang, weeklyps.com
#include <stdio.h>

int main() {
    int a=13;
    int *= &a;
    printf("주소 = %p, a = %d, *p = %d\n",p,a,*p);
    *p=11;                                          // 간접 참조
    printf("주소 = %p, a = %d, *p = %d\n",p,a,*p);
    return 0;
}
cs

출력

주소 = 0x7ffdd7550b54, a = 13, *p = 13
주소 = 0x7ffdd7550b54, a = 11, *p = 11


 이 프로그램을 직접 돌려보시면 *p가 a와 같은 결과를 출력함을 알 수 있습니다. p에 a의 주소가 담겨있기 때문에, p에 써진 주소를 따라가면 a가 나오는 건 당연한 이치입니다. 따라서 *연산자를 통해 *p를 사용하면 마치 a를 사용한 것과 동일한 효과를 볼 수 있습니다.




4. 오프셋


 컴퓨터의 메모리라는 것은 '정수형 변수를 저장하기 위한 메모리', '실수형 변수를 저장하기 위한 메모리' 이런 식으로 나누어져 있지 않습니다. 같은 메모리에 정수형 변수를 저장하기도 하고, 실수형 변수를 저장하기도 할 뿐이지요. 그런데 이상하게도 포인터 변수를 선언할 때 보면 int *p, char *p 이런 식으로 p가 어떤 변수형의 주소를 저장하는 것인지 써주게 되어 있습니다. 어차피 똑같은 메모리의 주소인데 왜 써주게 되어 있을까요? 그런 이유 중 하나는 변수형마다 오프셋(offset)이 다르기 때문입니다.


 오프셋이란 메모리의 간격을 의미합니다.


 위와 같이 메모리에 int 변수 2개와 char 변수 2개가 저장되어 있다고 해보면 int 변수의 오프셋은 4byte가 되고, char 변수의 오프셋은 1byte가 됩니다. int 변수의 크기는 4byte이고 char 변수의 용량은 1byte이기 때문이지요. 이제 아래의 코드를 출력해 보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//code by RiKang, weeklyps.com
#include <stdio.h>
 
int main() {
    int a=13;
    int *= &a;
    printf("%p\n",p);      // 4씩 증가하는 주소
    printf("%p\n",p+1);
    printf("%p\n",p+2);
    printf("%p\n",p+3);
    printf("-----------------\n");
    char b='a';
    char *= &b;
    printf("%p\n",q);      // 1씩 증가하는 주소
    printf("%p\n",q+1);
    printf("%p\n",q+2);
    printf("%p\n",q+3);
    return 0;
}
cs

출력

0x7fff98cf6764
0x7fff98cf6768
0x7fff98cf676c
0x7fff98cf6770
-----------------
0x7fff98cf6763
0x7fff98cf6764
0x7fff98cf6765
0x7fff98cf6766


 이 결과를 보면 신기하게도 int *p;로 선언한 p는 +1할 때마다 4가 증가한 결과가 나오고 char *q;로 선언한 q는 +1할 때마다 1이 증가한 결과가 나옵니다. 포인터 변수를 +1하는 연산은 단순히 +1 이 아니라, 포인터 변수가 다음 변수의 주소를 가리키도록 하는 연산입니다. 따라서 해당 변수의 오프셋만큼 증가시키게 됩니다. 그래야 +1한 이후에 다음 변수를 가리킬 수 있기 때문이지요.




5. 포인터와 배열


 문자열에서 나오듯이, 배열 이름은 '첫 번째 변수의 주소'와 동일한 효과를 냅니다. 여기부터 생각을 전개해 나가면 사실, 포인터와 배열은 상당히 유사한 점이 많습니다.


1
2
3
4
5
6
7
8
9
10
//code by RiKang, weeklyps.com
#include <stdio.h>
 
int main() {
    int a[5]={6,7,8,9,10};
    int *= a;
    for(int i=0; i<5; i++)
        printf("%d ",*(p+i));
    return 0;
}
cs

출력

6 7 8 9 10


 위의 포로그램을 실행시켜 보시면 배열을 순회한 것과 똑같은 결과가 나오게 됩니다. 이는 배열로 선언된 변수들이 메모리에서 붙어있기 때문에 가능한 출력 결과입니다.


 int *p = a;


 p에 배열 이름을 넣었기 때문에 &a[0] 을 넣은 것과 동일한 상황입니다. 따라서 *p 와 a[0] 은 동일한 효과를 냅니다.


 printf("%d ",*(p+i));


 이제, *(p+1)을 생각해 보겠습니다. int 변수의 오프셋은 4이기 때문에 p+1은 p보다 4byte 떨어진 주소를 나타냅니다. 그리고 그곳에는 a[1]이 저장되어 있지요. 따라서 *(p+1)은 a[1]와 동일한 효과를 냅니다. *(p+2), *(p+3) 등도 마찬가지 원리입니다.




'C, C++' 카테고리의 다른 글

[ C언어 ] 18. 함수  (0) 2018.01.04
[ C언어 ] 17. 문자열 관련 함수  (0) 2018.01.03
[ C언어 ] 16. 포인터  (0) 2018.01.02
[ C언어 ] 15. 형 변환  (2) 2017.12.07
[ C언어 ] 14. 문자열  (0) 2017.12.04
[ C언어 ] 13. 연산자 우선순위  (0) 2017.11.27