본문 바로가기

C 언어

c언어 포인터와 1차원 배열의 관계

 

포인터와 배열은 C 언어에서 매우 중요한 개념으로, 특히 배열과 포인터의 관계는 많은 이들에게 혼란을 줄 수 있다. 이 글에서는 포인터의 기본 개념부터 1차원 배열과의 관계까지 쉽게 설명해보겠다.

포인터의 기본 개념

포인터는 두 가지만 기억하면, 별(*)이 아무리 많이 붙어도 헷갈리지 않을 수 있다. 

첫 번쨰, 포인터는 간단히 말해 "주소를 저장하는 변수"다.

포인터와 일반 변수의 차이가 뭘까? 

int main(void){
	int a; 
	int* p;  
}

 

위 코드에서 a라는 변수는 int형 정수 값을 저장한다.즉 a는 4byte 크기의 정수 값을 저장한다.

반면, p라는 포인터는 8byte의 메모리 블록에 어떤 변수의 주소값을 저장하는데, 포인터가 저장하는 주소는 메모리에서 특정 변수가 저장된 위치의 첫 번째 주소이다. 

 

64bit 시스템에서 포인터로 선언된 변수는 8바이트 크기의 주소 값을 저장한다. 왜 8바이트를 저장하냐면, 64비트 시스템에서는 메모리 주소를 표현하는 데 64비트가 필요한데, 이는 2^64개의 메모리 위치를 나타낼 수 있다는 의미이다. 64비트는 8바이트(1byte == 8bit)이므로, 메모리 주소를 저장하는 포인터 변수는 8byte 크기를 가진다.

 

여기서 중요한 점은 별(*)이 붙었을 때, 변수의 용도가 '정수 저장'에서 '주소 저장'으로 바뀐다는 것이다.


예를 들어 아래와 같은 코드가 있다고 해보자

int a = 5;
int *p = &a;


위 코드에서 p는 a의 주소를 저장하는 포인터다. 이 때, a는 int 자료형인데  int는 4바이트니까 4개의 블록 메모리에 할당되고 각각의 블록은 주소값을 가지고 있다. 여기서 p는 a 가 할당된 메모리 블록 중 첫 번째 블록 주소의 값을 저장하게 된다. 

 

그렇다면 왜 포인터를 선언할 때 포인터 앞에 자료형이 나와야하는지 알 수 있다.

64bit 시스템에서,포인터는 주소값을 저장하기 떄문에 변수의 자료형이 int, char, double 이든 상관없이 8바이트를 할당한다. 그래서 포인터 앞에 자료형을 붙여주지 않으면, 나중에 포인터 역참조로 주소값이 아닌 변수의 메모리에 접근하여 값을 가져와야 할 때 몇 바이트를 가져와야 하는지 모르니까 문제가 발생하게 된다. 

 

즉,  int *p와 double *p는 같은 주소 값을 가리키더라도, 메모리에서 읽어들이는 크기와 방법이 다르다.

64비트 시스템에서는 모든 포인터가 8바이트 크기를 가지지만, 가리키는 변수의 자료형에 따라 메모리에서 읽어오는 크기가 달라진다. int 포인터는 4바이트씩, double 포인터는 8바이트씩 이동하며 메모리 값을 읽는다.

 

두번째, 포인터의 asterisk(*) 는 연산자이다.

포인터를 선언 할 때 아래와 같이 선언한다고 해보자.

int main(void){
  int a = 5; 
  int b = 10;
  int* p = &a;
}


이후 선언한 포인터 p 값에 a가 아닌 b 변수의 주소 값을 가리키는 것으로 변경하고 싶다. 둘 중 뭐가 맞을까?

//1번 
p = &b; 

//2번 
*p = &b;


정답은 1번이다. 종종 포인터를 선언한 뒤에도 계속해서 *p로 적는 사람들이 있는데, 만약 *p = &b 라고 적는다면 오류가 발생할 것이다. *p는 p가 가리키는 메모리 위치의 값(10)을 나타내기 때문에, &b와 같은 주소값을 저장하려면 int* 타입의 변수에 저장해야 한다. 하지만 *p는 int 타입의 값을 기대하는데 여기에 8바이트 크기의 주소를 저장하려고 시도하는 것은 잘못된 시도이기 때문이다. 따라서 주소값을 저장하려면 p = &b;와 같이 포인터에 직접 저장해야 한다. 

 

세번째, 포인터의 asterisk(*) 는 역참조할 수 있는 작대기 개수라고 생각하자 

포인터에 별이 하나 더 붙을 때마다 포인터는 한 단계 더 깊은 주소를 가리키게 된다.

그래서 포인터의 별 갯수는 역참조할 수 있는 작대기 개수라고 생각하면 쉽다. 

예를 들어, int*는 int형의 주소를, int**는 int*의 주소를, int***는 int**의 주소를 가리킨다.

 

예제 코드를 보자. 

int a = 5;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;

 


int* p1은 int의 주소를 저장하는 포인터 p1이라는 뜻이다.
int** p2는 int 포인터 p1의 주소를 저장하는 포인터 p2이다.
int*** p3는 int 더블포인터 p2의 주소를 저장하는 포인터 p3이다. 

이처럼,포인터는 군대에서 계급장을 달듯이, 주소를 저장하려는 변수보다 별이 하나 더 있어야 원하는 변수의 주소를 저장할 수 있다. 


별 1개짜리는 별 2개짜리가 받을 수 있고, 
별 2개짜리는 별 3개짜리가 받을 수 있고,
별 99개짜리는 별 100개짜리가 받을 수 있는 것이다. 

이는 역참조와도 관련이 있는데, 결국 * 의 갯수는 가능한 역참조의 횟수와도 같다. 즉, 
*이 1개면, 역참조는 한번만 연결해서 a의 메모리에 접근하여 5을 출력할 수 있고 
*이 2개면, 역참조를 두번연결해서 a의 메모리에 접근하여 5을 출력할 수 있다. 
*이 3개면, 역참조를 세번 연결해서 a의 메모리에 접근하여 5를 출력할 수 있다. 

int a = 5;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;

printf("%d\n", a) // 5 
printf("%d\n", *p1 ) // 5
printf("%d\n", *p2)  // a의 주소값
printf("%d\n", **p2) // 5
printf("%d\n", *p3)  // p1의 주소값
printf("%d\n", **p3) // a의 주소값
printf("%d\n", ***p3) // 5

 

1차원 배열과 포인터의 관계

이제 1차원 배열과 포인터와 관계에 대해 알아보자. 배열은 연속적인 메모리 공간이 덩어리 형태로 이루어진 자료구조이다. 그리고 배열의 이름은 그 배열의 첫 번째 요소의 주소를 가리키는 포인터 역할을 한다. 예를 들어, 배열의 이름이 a 라면 a는 배열의 첫번째 주소와 같다. 즉, p == &a[0]가 성립한다.

int a[] = {1, 2, 3, 4, 5};
int *p = a; 

printf("%d\n",p); 
printf("%d\n",a);
printf("%d\n",*p); 
printf("%d\n",*a);

출력
0x001000
0x001000
1
1


그런데 왜 배열의 이름과 배열의 시작 주소값은 같은걸까?  왜냐하면 배열 자체가 포인터로 이루어졌다고 볼 수 있기 때문이다.(포인터처럼 메모리 블록 하나당 8바이트씩 할당되는건 아니지만) 그리고 배열의 첫번째 원소를 우리는 a[0]이라하고, 두 번째 원소는 a[1].. 등으로 표시하는데 여기서 "[ ]" 인덱스가 의미하는 바는 사실 역참조이다.

 

  • a[0]는 *(a + 0)와 같다.
  • a[1]는 *(a + 1)와 같다.
  • a[2]는 *(a + 2)와 같다.

즉, 포인터(*)와 인덱스([ ])는 같은 기능을 하는 것이다. 그래서 보통 인덱스는 배열에서만 쓸 수 있다고 생각하는데, 사실 배열이 아니더라도 역참조의 기능과 동일하게 사용할 수도 있다. 

int a = 5; 
int *p = &a; 

printf("%d\n",p[0]); 
printf("%d\n",*p); 

출력
5
5

 

이렇듯, 인덱스"[ ]"는 꼭 배열에서만 사용하지 않아도 역참조처럼 출력되는 것을 확인할 수 있다. 즉 *과 인덱스는 완전히 동일한 기능을 한다.  

 

이제 배열과 포인터의 관계를 잘 이해했는지 확인하기 위해, 아래 예제 코드가 이해되는지 확인해보자.

#include<stdio.h>

int main() {
	int a[5] = { 10,20,30,40,50 };
	int* p;
	p = a;

	printf("1. %d\n", a); //배열의 첫번째 주소 
    printf("2. %d\n", *a); //10
	printf("3. %d\n", *p); //10 
	printf("4. %d\n", *(p+1)); //20
	printf("5. %d\n", p[2]); //30
	printf("6. %d\n", *(a+2)); //30

	p = p + 2; //a[2]

	printf("7. %d\n", a); //배열의 첫번째 주소 
	printf("8. %d\n", *p); //30
	printf("9. %d\n", *(p+2)); //50
	printf("10. %d\n", p-1); //배열의 두번째 주소 
	printf("11. %d\n", a[2]); //30
	printf("12. %d\n", p[2]); //50
	printf("12. %d\n", p[-1]); //20
	printf("13. %d\n", *p+2); //32  <- 역참조 연산 후 +2를 한다. 

}