본문 바로가기
휴지통/C 프로그래밍

포인터와 배열

by 신재권 2021. 1. 21.

1. 포인터와 배열의 관계

배열의 이름도 포인터이다. 가장 중요한 것은 '배열이름의 포인터 형'을 이해하는 것이다.

-배열의 이름은 무엇을 의마하는가?

배열의 이름은 포인터이다. 단 ,그 값을 바꿀수 없는 '상수형태의 포인터'이다.

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]);    //%p는 주소를 출력하는 서식문자

다음의 실행결과는 이렇다.

배열의 이름 : 0012FF50

첫 번째 요소 : 0012FF50

두번째요소: 0012FF54

세번째요소 : 0012FF58 //위 주소값은 임의로 지정한 것이다.

실행결과를 통해 우리는 다음 사실을 알 수 있다.

"int형 배열요소간 주소 값의 차는 4바이트 이다."

선언한 배열이 int형 배열이므로 각 요소 별로 할당되는 메모리 공간의 크기는 4바이트이다. 따라서 배열요소간 주소 값의 차가 4바이트임을 보이는 위의 실행결과는 "모든 배열요소가 메모리 공간에 나란히 할당된다"라는 사실을 증명한다.

그리고 다음과 같은 사실도 확인이 가능하다.

"배열의 첫 번째 바이트의 주소 값이 0012FF50인데 ,배열의 이름을 출력한 결과도 이와 같다."

즉 "배열의 이름은 배열의 시작 주소 값을 의미하며, 그 형태는 값의 저장이 불가능한 상수이다."

 

비교조건 | 비교대상

포인터 변수

배열의 이름

이름이 존재하는가?

존재한다

존재한다

무엇을 나타내거나 저장하는가?

메모리의 주소 값

메모리의 주소 값

주소 값의 변경이 가능한가?

가능하다

불가능하다

위의 표에서 보이듯이 배열의 이름이나 포인터 변수나 둘 다 이름이 존재하며, 특정 메모리 공간의 주소 값을 지닌다. 다만 포인터 변수는 그 이름이 의미하듯이 변수지만, 배열의 이름은 가리키는 대상의 변경이 불가능한 상수라는 점에서만 차이를 보인다.

즉 배열의 이름은 "상수 형태의 포인터"이다. 그래서 배열의 이름을 가리켜 '포인터 상수'라 부르기도 한다.

배열의 이름도 포인터이기 때문에 배열의 이름을 피연산자로 하는 *연산이 가능하다.

*연산의 가능성을 말하기에 앞서 포인터형을 확인해야한다. 포인터 형에 따라서 *연산의 결과가 결정되기 때문이다.

-1차원 배열이름의 포인터 형과 배열이름을 대상을 하는 *연산

int arr1[5]; //arr1은 int형 포인터 상수

배열의 이름 arr1이 가리키는 것은 배열의 첫번째 요소이다. 그런데 배열의 첫번째 요소가 int형 변수이니, arr1은 'int형 포인터(int *)라는 결론이 나온다.

1차원 배열이름의 포인터 형은 배열의 이름이 가리키는 대상을 기준으로 결정하면 된다.

즉 *arr1은 첫번째 요소를 가리킨다. arr1자체는 배열의 주소이자 첫번째 요소의 주소이다 . 꼭 포인터형이 붙어야 첫번째 원소를 가리킨다.

int main(void){
int arr[3] = {1, 2,3};
arr[0] +=5;    // arr[0] = arr[0] + 5
arr[1] +=7;
arr[2] +=9;   //포인터를 대상으로 이 문장을 구성한 것
}

위의 코드에서 배열이름 arr는 int형 포인터이니, 포인터를 대상으로 배열의 모든 요소를 arr[0], arr[1], arr[2]로 접근한 셈이다.

그렇다면 포인터 변수를 대상으로도 이러한 형태의 접근이 가능할까? 배열의 이름이나 포인터 변수나 둘 다 포인터이니까

실제로 포인터 변수는 배열의 이름처럼 사용할 수 있다. 즉, 포인터 변수 ptr을 대상으로 ptr[0], ptr[1], ptr[2]와 같이 배열의 형태로 메모리 공간에 접근이 가능하다.

-포인터를 배열의 이름처럼 사용할 수 있다.

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

#include <stdio.h>

int main(void){
int arr[3] = {15, 25, 35};
int *ptr = &arr[0];      //int *ptr = arr; 과 동일한 문장

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;
}
==========================================================================
실행결과
==========================================================================
15 15
25 25
35 35
15 15

참고로 위와같이 포인터 변수를 배열의 이름처럼 사용하는 경우는 거의 없다. 마찬가지로 배열의 이름을 포인터 변수처럼 사용하는 겨웅도 거의 없다. 하지만 이러한 일이 가능하다는 사실은 알고 있어야한다.

2. 포인터 연산

포인터를 대상으로 메모리 접근을 위한 *연산 이외에도 증가 및 감소연산도 가능하다. 그런데 중요한 것은 증가 및 감소연산이 가능하다는 사실이 아니고 연산의 결과이다.

-포인터를 대상으로 하는 증가 및 감소연산

int main(void){
int *ptr1 = ;
int *ptr2 = ...;
ptr1++;
ptr1+=3;
ptr2 +=5;
ptr2= ptr1 +2;
}

"int형 포인터를 대상으로 1을 증가시키면 4가 증가하고(주소값) , double형 포인터를 대상으로 1을 증가시키면 8이 증가한다."

즉, 포인터를 대상으로 하는 증가연산의 결과는 다음과 같다.

int형 포인터를 대상으로 n증가 , n x sizeof(int)의 크기만큼 증가

즉, type형 포인터를 대상으로 n 증가 및 감소 , n x sizeof(type)의 크기만큼 증가 및 감소한다.

감소 연산도 똑같은 크기만큼 감소한다.

포인터의 연산특성으로 인해서 배열접근이 가능하다.

#include <stdio.h>
int main(void) {
int arr[3] = {11, 22, 33};
int *ptr = arr;     //int *ptr =&arr[0]과 같은 문장
printf("%d %d %d \n , *(ptr+0), *(ptr+1), *(ptr+2);   //arr[0], arr[1], arr[2]

printf("%d ", *ptr); ptr++;  //printf함수 호출 후 , ptr++실행 , 즉 arr[0]을 출력 후 arr[0]을 가리키던 값을 arr[1]로 변경
printf("%d ", *ptr); ptr++; // arr[1]을 출력 후 arr[1]을 가리키던 값을 arr[2]로 변경
printf("%d ", *ptr); ptr--;  //arr[2]을 출력 후 arr[2]을 가리키던 값을 arr[1]로 변경
printf("%d ", *ptr); ptr--;  //arr[1]을 출력 후 arr[1]을 가리키던 값을 arr[0]로 변경
printf("%d ", *ptr); printf("\n");   //arr[0] 출력 
return 0;
}
==========================================================================================
실 행 결 과 
==========================================================================================
11 22 33    //5행
11 22 33 22 11 //7~11행

4행에 선언된 포인터 변수 ptr은 int형 포인터 이므로 값을 1 증가 시키는 연산을 할 때마다 실제로는 4가 증가한다. 따라서 배열 arr이 할당된 위치의 주소값을 0x001000이라 가정할 때, (ptr+1)과 (ptr+2)는 4찍 증가해 0x001004와 0x001008을 가리키게 된다.

*ptr, *(ptr+1), *(ptr+2)의 참고결과 출력시 arr[0], arr[1], arr[2]에 저장된 요소가 출력된다.

아래 두 연산의 차이점

*(++ptr) = 20; //ptr에 저장된 값 자체를 변경 . (ptr이 가리키는 곳. 즉, 주소)

*(ptr+1) =20; //ptr에 저장된 값은 변경되지 않음 (ptr이 가리키는 곳,즉, 주소)

위의 두 문장 모두 현재 ptr이 가리키는 위치에서 4바이트 떨어진 메모리 공간에 20을 저장하는 문장이다. 하지만 연산 이후 포인터 변수 ptr의 상태에는 차이가 있다. 첫 번째 문장의 경우 ++연산의 결과로 인해 포인터 변수 ptr에 저장된 값이 4만큼 증가한다. 하지만 두번째 문장의 +연산으로 인해서는 ptr에 저장된 값이 증가하지 않는다. 다만 증가된 값을 연산의 결과로 얻어서 *연산을 진행할 뿐이다.

-중요한 결론 , arr[i] == *(arr+i)

배열의 이름과 포인터 변수는 상수냐 변수냐의 차이만 있을 뿐, 사실상 동일하다. ptr에 저장된 값이 arr의 주소 값이다.

따라서 우리는 식을 도출할 수 있다.

arr[i] == *(arr+i) //arr[i]는 *(arr + i)와 같다.

위의 식에서 arr은 배열의 이름이어도 성립하고 포인터 변수이어도 성립한다.

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

널 문자가 삽입되는 문자열의 선언방식에는 두 가지가 있다. 하나는 배열을 이용한 방식이고, 다른 하나는 char형 포인터 변수를 이용하는 방식이다.

-두 가지 형태의 문자열 표현

char str1[ ] = "My string"; //배열의 길이는 자동으로 계산

이는 배열을 기반으로 하는 '변수 형태의 문자열'선언이다. 변수라 하는 이유는 문자열의 일부를 변경할 수 있기 때문이다. 반면 다음과 같이 포인터를 기반으로 문자열을 선언하는 것도 가능하다.

char *str2= "your String";

이렇게 선언을 하면 메모리 공간에 문자열 "your String"이 저장되고, 문자열의 첫 번째 문자 Y의 주소값이 반환된다.

그리고 그 반환 값이 포인터 변수 str2에 저장된다. 그래서 str2를 char형 포인터로 선언한 것이다. char형 문자 Y의 주소 값이 저장되기 때문이다.

str1은 그 자체로 문자열 전체를 저장하는 배열이고, str2는 메모리상에 자동으로 저장된 문자열 "your string"의 첫 번째 문자를 단순히 가리키고만 있는 포인터 변수이다. 그러나 배열이름 str1이 의미하는 것도 실제로는 문자 M의 주소 값이기 때문에 str1도 str2도 문자열의 시작 주소값을 담고 있다는 측면에서는 동일하지만, 다음의 차이만 있을 뿐이다.

"배열이름 str1은 계속에서 문자 M이 저장된 위치를 가리키는 상태이어야 하지만 포인터 변수 str2는 다른 위치를 가리킬 수 있다.

char *str = "Your team";
str = "Our team";    //str이 가리키는 대상을 문자열 "Our team"으로 변경

하지만 배열 이름인 str1은 상수형태의 포인터이기 때문에 가리키는 대상을 변경할 수 없다.

또 한가지 중요한 차이점이 있는데, 다음과 같이 선언이 되면 애초에 문자열은 배열에 저장이된다. 그리고 배열을 대상으로는 값의 변경이 가능하기 때문에 다음과 같이 선언되며는 문자열을 가리켜 '변수 형태의 문자열'이라 한다.

char str1[ ] = "My string";

반면, 다음과 같은 선언되는 문자열은 '상수 형태의 문자열'이라 한다.

char *str2= "Your string";

실제로 포인터 변수 str2가 가리키는 문자열은 그 내용의 변경이 불가하다.

예를 들어 위와 같이 상수 형태 ,변수 형태의 문자열을 선언하고 어느 특정 부분 문자열을 변경할라 하면 변수형태의 문자열만 바꿀 수 있다.

ex)

str1[4] = 'O' //문자열 변경 성공

str2[4] = 'O' //문자열 변경 실패

가리키는 대상 변경

값을 변경

변수형태문자열(배열선언)

X

O

상수형태문자열(포인터선언)

O

X

-어디서든 선언할 수 있는 상수 형태 문자열

다음과 같이 선언되는 문자열은 상수 형태의 문자열이라 하였다.

char * str= "Const String";

그렇다면 어떠한 순서를 거쳐서 이문장이 처리가 될까?

문장이 실행되면 먼저 문자열이 메모리 공간에 저장이 되고, 그리고 그 메모리의 주소 값이 반환된다. 즉 문자열이 0x1234번지에 저장되었다고 가정하면, 위의 문장은 문자열이 저장된 이후에 다음의 형태가 된다.

char *str = 0x1234;

그리하여 포인터 변수 str에는 문자열의 주소 값 0x1234가 저장되는 것이다. 그렇다면 함수의 호출과정에서 선언이 되는 문자열은 어떻게 처리가 될까?

printf("Show your string");

이경우도 마찬가지 이다. 큰 따옴표로 묶어서 표현되는 문자열은 그 형태에 상관없이 메모리 공간에 저장이 된 후 그 주소값이 반환된다. 따라서 위의 함수호출 문장도 메모리 공간에 저장된이후 다음의 형태가 된다(문자열이 0x1111번지에 저장되어 있다고 가정).

printf(0x1111);

이렇듯 printf함수는 문자열을 통째로 전달받는 함수가 아닌, 문자열의 주소 값을 전달받는 함수이다.

4. 포인터 변수로 이뤄진 배열 : 포인터 배열

포인터 변수는 변수이니 이를 대상으로 배열을 선언할 수 있다.

-포인터배열의 이해

포인터 변수로 이루어진, 그래서 주소 값이 저장이 가능한 배열을 가리켜 '포인터 배열'이라 한다.

int *arr1[20]; //길이가 20인 int형 포인터 배열 arr1

double * arr2[30]; //길이가 30인 double형 포인터 배열 arr2

위 문장에서 보이듯 포인터 배열의 선언방식은 기본 자료형 배열의 선언방식과 동일하다. 배열의 이름앞에 배열 요소의 자료형을 정보를 선언하면 된다. 즉 배열 이름 arr1앞에 선언된 int *가 int형 포인터를 의미한다.

#include <stdio.h>

int main(void){
int num1= 10, num2 =20, num3 = 30;
int *arr[3]= {&num1, &num2, &num3};
printf("%d %d %d \n", *arr[0], *arr[1], *arr[2]);
return 0;
}
=====================================================================
실행결과
=====================================================================
10 20 30 

이렇듯 포인터 배열도 기본 자료형 배열과 별반 다르지 않다. 다만 주소 값을 저장할 수 있도록 포인터 변수를 대상으로 선언된 배열일 뿐이다.

-문자열을 저장하는 포인터 배열

문자열의 주소값을 저장할 수있는 배열로서 사실상 char형 포인터 배열이다. 즉

char * strArr[3] ; //길이가 3인 char형 포인터 배열

위에 선언에서 보이듯이 , char형 포인터 배열은 문자열의 주소 값을 저장 할 수 있는 배열이다 보니 문자열 배열로 불릴 뿐이다.

#include <stdio.h>

int main(void){
char *strArr[3] = {"Simple", "String", "Array"};
printf("%s %s %s \n", strArr[0], strArr[1], strArr[2]);
return 0;
}
=========================================================
실행결과
=========================================================
Simple String Array

위 의 코드에서 4행에는 문자열 배열이 선언되어 있다.

"큰 따옴표로 묶어서 표현되는 문자열은 그 형태에 상관 없이 메모리 공간에 저장된 후 그 후 주소 값이 반환된다"

즉 4행무장을 실행하면 초기화 리스트에 선언된 문자열들은 각각 메모리 공간에 저장되고, 그 위치에 저장된 문자열의 주소 값이 반환된다. 따라서 문자열이 저장된 이후에는 다음의 형태가 된다.

char * strArr[3] = {0x1004, 0x1048, 0x2012}; //임의 주소값

그런데 반환된 주소 값은 문자열의 첫번째 문자의 주소 값이니, 이렇듯, char형 포인터 배열에 저장이 가능한 것이다.

'휴지통 > C 프로그래밍' 카테고리의 다른 글

포인터의 포인터  (0) 2021.01.21
다차원 배열  (0) 2021.01.21
포인터와 함수에 대한 이해  (0) 2021.01.21
포인터의 이해  (0) 2021.01.21
1차원 배열  (0) 2021.01.21