본문 바로가기

KUCC/C언어 세션

[C언어세션][포인터와배열]포인터와 배열의 관계, 포인터 연산, 상수형태의 문자열,포인터 배열

포인터에 대한 이해가 충분히 이루어졌다면, 포인터와 배열 사이의 관계를 살펴보자.

 

 

포인터와 배열의 관계?


우리가 앞서 배열을 배워 본 적이 있다. 근데 왜 갑자기 포인터를 배우다가 배열을 다시 언급하는걸까?

왜냐하면 배열은 주소값이기 때문이다.!

 

배열의 이름은 배열의 시작 주소값을 의미한다.

다음 예제를 통해 더 자세히 살펴보자.

 

#include <stdio.h>

int main(void){

   int arr[3]={ 0, 1, 2 };
   printf("배열의 이름: %p\n", arr);
   printf("첫 번째 요소: %p\n",&arr[0]);
   printf("두 번째 요소: %p\n",&arr[1]);
   printf("세 번째 요소: %p\n",&arr[2]);
  
}

결과를 실행해보면,

배열의 이름: 0012FF50
첫번째 요소: 0012FF50
두번째 요소: 0012FF54
세번째 요소: 0012FF58 

이런식으로 출력된다.

위 코드를 통해 배열과 포인터의 특징을 몇가지 알 수 있다.

첫째, 일단 배열의 이름을 출력하는 코드를 보자. arr이라는 배열의 이름을 출력하는 코드를 작성하면, 실제론 주소값이 출력된다. 그런데 그 주소값이 밑에 첫번째 요소의 주소값과 동일하다. 즉, 배열의 첫번째 요소의 주소값과, 배열의 이름을 출력한 결과는 같다.

 

둘째, int형 배열요소간 주소값의 차이는 4바이트이다. arr[0]과 arr[1]의 주소값, arr[2]와 arr[1]의 주소값 모두 4바이트씩 차이난다. 즉, 모든 배열 요소가 메모리 공간에 나란히 할당되고 있다는 점을 알 수 있다.

 

배열의 이름은 이처럼, 포인터변수의 역할을 한다. 왜냐하면 메모리의 주소값을 나타내는 역할을 하기 때문이다. 하지만 둘의 차이가 있다면, 포인터 변수는 주소 값의 변경이 가능하지만, 배열의 이름은 주소값의 변경이 불가능하다.

 

배열의 이름은 대상의 변경이 불가능한 상수이기 때문에, 배열의 이름을 '상수 형태의 포인터' ,'포인터 상수'라고도 한다.

(참고로 배열의 이름도 포인터이기에, *연산도 가능하다.)

 

지금까지 배열의 이름이 포인터로 사용될 수 있는 상황에 대해 다루어보았는데, 반대로 포인터가 배열의 이름으로 사용될 수 있는 상황에 대해 살펴보자.

사실 배열의 이름과 포인터 변수는 변수냐, 상수냐의 차이가 있을 뿐, 둘다 포인터이기 때문에 배열의 이름으로 할 수 있는 연산은 포인터 변수로도 할 수 있고, 그 반대도 가능하다.

#include <stdio.h>

int main(void){

   int arr[3]={ 15, 25, 35 };
   int * ptr= &arr[0]; 
   
   printf("%d %d\n", ptr[0], arr[0]);
   printf("%d %d\n", ptr[1], arr[1]);
   printf("%d %d\n", ptr[2], arr[2]);
   printf("%d %d\n", *ptr, *arr);
   
   return 0;

}

위 코드의 실행 결과를 예측해보자. ( 참고로, int * ptr = &arr[0] 대신에 int * ptr = arr이라는 표현을 쓸 수 있다.)

 

 

포인터 연산


#include <stdio.h>

int main(void){

   int * ptr1=0x0010;
   double * ptr2=0x0010;
   
   ptr1++;
   ptr1 + = 3;
   ptr2 -= 5;
   ptr2 = ptr1 +2;

}

포인터에 ++ 이나 +=와 같은 증감 연산자를 사용하게 되면 어떻게 될까?

int 자료형의 주소일때, 0x0010에 1을 더하면 0x0011이 되는게 아니라, 실제론 0x0014가 된다. 자료형의 크기 만큼 주소값이 증가하기 때문이다. 마찬가지로, double형일 때에는 포인터를 대상으로 1을 증가시키면 8이 증가한다. 한마디로, type형 포인터를 대상으로 n의 크기만큼 값을 증감시키면 n*sizeof(TYPE)의 크기만큼 주소 값이 증가, 감소한다.

 

 

아래 코드를 살펴보자

int main(void){

   int arr[3]={ 11, 22, 33 };
   int * ptr = arr;
   printf("%d %d %d \n" , *ptr , *(ptr+1), *(ptr+2));

}

ptr이라는 포인터는 배열의 첫번째 값의 주소를 가르키고 있다.

*ptr의 경우, arr의 첫번째 값이 반환 될 것이다. *(ptr+1)의 경우, arr의 첫번째 값 주소에 4만큼 더하면, arr의 두번째 값 주소가 되어, 두번째 값이 반환 될 것이다. 마찬가지로 *(ptr+2)의 경우, arr의 첫번째 값 주소에 8만큼 더하면 arr의 세번째 값 주소가 되어, 세번째 값이 반환 될 것이다.

한마디로 정리하자면, arr[i]는 (arr이라는 배열의 i번째 값은) *(arr+i)으로도 나타낼 수 있다.

 

 

상수형태의 문자열을 가리키는 포인터


문자열을 선언하는 방식에는 크게 2가지가 있다. 배열을 이용한 방식과, char형 포인터 변수를 이용한 방식이다.

 

배열을 기반으로 하는 선언 방식은 다음과 같다.

char str1[] = "Hello";

 

밑은 char형 포인터 변수를 사용한 방식이다.

char * str2 = "Bye";

"Bye"가 메모리 공간에 저장되고, Bye의 첫번째 문자 B의 주소값이 str2에 저장된다.

 

str1과 str2의 차이는 뭘까?

우선, 배열이름 str1은 계속 H를 가리키는 상태여야 하지만, 포인터 변수인 str2은 다른 위치도 가리킬 수 있다. str2 = "Good Bye"라는 코드를 추가하면, str2가 가리키는 대상이 문자열 "Good Bye"로 바뀐다. 한마디로 정리하자면, str1은 상수형태의 포인터이기때문에 가리키는 대상이 바뀔 수 없고, str2은 변수형태의 포인터이기 때문에 가리키는 대상이 바뀔 수 있다.

 

또, 한가지 차이점이 더 있다. str1은 변수형태의 문자열, str2은 상수형태의 문자열이다. 왜? str2은 포인터변수이기 때문에 문자열 그 내용 자체는 변경이 불가능하다. 그렇기에, 포인터 변수의 형태로 선언된 문자열의 경우, 문자열이 변경되지 않는다. 다만, 컴파일러에 따라서 약간의 차이는 있다. 프로그램이 그냥 종료되기도 하고, 어떤 컴파일러들은 변경을 허용하기도 한다. 그렇기에 포인터변수의 형태로 선언된 문자열을 바꾸려고 하지 말자.

 

이해가 안된다면 다음 코드를 보자.

#include <stdio.h>

int main(void){

   char str1[]=" My String";
   char * str2= " Your String";
   printf("%s %s \n", str1, str2);
   
   str2=" Our String";
   printf("%s %s \n", str1, str2);
   
   str1[0]='X';
   str2[0]='X';
   
   printf("%s %s\n",str1,str2);
   return 0;

}

str1[0]='X'는 문자열을 변경하고자하는 시도이다. str1의 경우, 정상적으로 문자열이 변경된다. 하지만 str2의 경우는 다르다. str2는 포인터 변수로 선언한 문자열, 즉 상수형태의 문자열이기 때문에 문자열 그 자체를 변경할 수 없다. 그러므로 str2[0]='X' 부분이 정상적으로 작동되지 않는다.

 

 

 

포인터 배열


포인터 배열은 말그대로 포인터 변수를 요소 지닌 배열이다. 포인터 배열은 int * arr1[20]; 이런식으로 선언한다.

arr1앞에 선언된 int*가 int형 포인터를 의미한다.

 

문자열을 저장하는 포인터 배열을 만들어보자.

#include <stdio.h>

int main(void){

   char * strArr[3] = {"Simple", "String", "Array"};
   
   printf("%s \n",strArr[0]);
   printf("%s \n",strArr[1]);
   printf("%s \n",strArr[2]);
   
   return 0;
}

이를 실행하면, 첫줄에는 Simple, 둘째 줄에는 String, 셋째 줄에는 Array가 출력된다.

 이때 기억해야하는 것은, 큰 따옴표로 묶여서 표현되는 문자열은 메모리 공간에 저장된 후, 그 주소값이 반환된다는 점이다.

char * strArr[3]이라는 문자형 포인터 배열을 선언한 후, "Simple"이라는 문자열이 메모리 공간에 저장되고, 그 "Simple"의 주소값, 즉 문자열의 첫번째 문자의 주소값이 반환된다. 그렇기에 char형(문자형) 포인터 배열에 저장가능한 것이다.

 

이처럼, char형 포인터 배열은 문자열의 주소값을 저장할 수 있는 배열이다. 그래서, 문자열 배열로 불린다.

 

 

포인터와 배열 한번에 이해하기 정말 쉽지 않을 것이다...!

한번 읽어보고 이해하려고 노력해보자~

 

 

 

참고자료:

윤성우. (2010). 윤성우의 열혈 C 프로그래밍. 서울: 오렌지미디어. (문자열 포인터 관련 예시 코드 일부)