1. 배열은 포인터다.
정확히 말하면, 배열의 이름은 그 값을 바꿀 수 없는 상수 포인터다.
앞에서 본 포인터 변수는 값을 바꿀 수 있었다.
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <stdio.h>
int main()
{
int num1, num2;
int* ptr = &num1;
ptr = &num2;
...
}
|
cs |
이렇게.
하지만 배열의 경우 값을 바꿀 수 없다.
1
2
3
4
5
6
7
8
9
10
11
|
#include <stdio.h>
int main()
{
int arr[3] = { 0, 1, 2 };
arr = &arr[2]; //에러 발생!
...
}
|
cs |
배열은 시작 주소를 기준으로 해서 각 요소들의 주소를 지정한다.
배열 arr이 0x0030F780에 할당되었고, int 배열이라고 하자.
그럼 &arr[0]은 배열의 시작 주소이므로 0x0030F780,
&arr[1]은 0x0030F784, &arr[2]는 0x0030F788이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <stdio.h>
int main()
{
int arr[3] = { 0, 1, 2 };
printf("arr: %p\n", arr);
printf("arr[0]의 주소: %p\n", &arr[0]);
printf("arr[1]의 주소: %p\n", &arr[1]);
printf("arr[2]의 주소: %p\n", &arr[2]);
return 0;
}
|
cs |
여기서 arr의 출력 결과가 &arr[0], 그러니까 배열의 첫 번째 바이트 주소와 같은 것에 주목하자.
이는 배열의 이름이 그 배열의 시작 주소를 나타낸다는 것을 의미한다.
각 요소의 주소는 배열의 시작 주소에 인덱스*자료형 크기(byte)의 곱을 더한 값이다.
여기서는 int형 배열이므로 &arr[1]의 값은 0x0030F780 + 1*4 = 0x0030F784인 것이다.
메모리에는 다음과 같은 형태로 할당된다.
따라서 다음의 세 가지 결론을 내릴 수 있다.
① 배열의 이름은 그 배열의 시작 주소(&arr[0])다
② 배열의 이름에는 주소값이 저장되므로 배열의 이름도 일종의 포인터다
③ 배열의 이름은 가리키는 대상을 변경할 수 없기 때문에 상수 형태의 포인터라고 할 수 있다
④ 배열 요소 간 주소 값의 차는 그 배열의 자료형의 바이트 크기다
2. 배열 이름을 대상으로 한 연산
int arr[4];
여기서 배열의 이름 arr 이 가리키는 것은 배열의 첫 번째 요소다.
그런데 이 요소는 int형이다. 따라서 arr 은 int형 포인터라고 할 수 있다.
당연히 double형 배열의 이름은 double형 포인터다.
1차원 배열 이름의 포인터 형은 포인터 변수와 마찬가지로 가리키는 대상을 기준으로 정한다.
배열의 이름 역시 포인터고, 포인터 형도 정해지므로 * 연산을 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#include <stdio.h>
int main()
{
int arr1[3] = { 0, 1, 2 };
double arr2[4] = { 0.1, 1.1, 2.1, 3.1 };
printf("arr1[0]: %d\n", arr1[0]);
printf("arr2[0]: %f\n\n", arr2[0]);
(*arr1) += 3;
(*arr2) += 4.0;
printf("arr1[0]: %d\n", arr1[0]);
printf("arr2[0]: %f\n", arr2[0]);
return 0;
}
|
cs |
배열의 이름 또한 포인터 이므로 간접 지정 연산자 *를 사용할 수 있다.
배열의 이름이 가리키는 대상은 배열의 첫 번째 요소이므로,
*arr1 에 대한 연산은 arr1[0] 에 대한 연산이고
*arr2 에 대한 연산은 arr2[0] 에 대한 연산이라 할 수 있다.
따라서 arr1[0], arr1[1], arr1[2] 등의 표현은 포인터를 통해 접근한 것이다.
배열 이름 arr1은 int형 포인터이니, 이 포인터가 가리키는 대상을 통해 배열의 요소에
접근하는 것이다.
그렇다면 포인터 변수로도 배열의 요소에 접근할 수 있을까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
#include <stdio.h>
int main()
{
int arr[3] = { 0, 1, 2 };
int* ptr = arr;
printf("arr: %p\n", arr);
printf("ptr: %p\n\n", ptr);
printf("arr[0]: %d\n", arr[0]);
printf("ptr[0]: %d\n\n", ptr[0]);
printf("arr[1]: %d\n", arr[1]);
printf("ptr[1]: %d\n\n", ptr[1]);
printf("arr[2]: %d\n", arr[2]);
printf("ptr[2]: %d\n", ptr[2]);
return 0;
}
|
cs |
연산의 형태만 보면 포인터 변수와 배열의 이름을 구분할 수 없다.
이처럼 포인터 변수를 통해 수행할 수 있는 연산과 배열의 이름을 통해 수행할 수 있는 연산은 동일하다.
하지만 포인터 변수를 배열의 이름처럼 사용하는 경우도, 그 반대의 경우로 많지 않다.
그럼에도 소개하는 이유는, 이것이 포인터를 이해하는 데에 중요한 부분이기 때문이다.
3. 포인터를 대상으로 하는 증감 연산
포인터라고 해서 * 연산만 가능한 것은 아니다. 증감 연산도 가능하다.
다만 중요한 것은 연산의 결과다.
포인터는 주소값을 저장한다. 이 주소값에 대해 증감 연산을 한다면 어떻게 될까?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#include <stdio.h>
int main()
{
int num1 = 3;
int* ptr1 = &num1;
double num2 = 5;
double* ptr2 = &num2;
printf("ptr1: %p\n", ptr1);
printf("ptr2: %p\n\n", ptr2);
ptr1 += 1;
ptr2 += 2;
printf("ptr1 += 1: %p\n", ptr1);
printf("ptr2 += 2: %p\n\n", ptr2);
printf("ptr1 + 1: %p\n", ptr1 + 1);
printf("ptr2 + 1: %p\n\n", ptr2 + 1);
return 0;
}
|
cs |
int형 포인터 ptr1 의 값을 1 증가시킨 결과 4 만큼 늘어났고,
double형 포인터 ptr2 의 값을 2 증가시킨 결과 16 만큼 늘어났다.
따라서 포인터를 대상으로 한 연산은 다음과 같이 이루어진다고 할 수 있다.
TYPE 형 포인터를 대상으로 n 증가: n × sizeof(TYPE) 만큼 증가
TYPE 형 포인터를 대상으로 n 감소: n × sizeof(TYPE) 만큼 감소
TYPE 형 포인터의 값을 n 증가시킬 때 주소값은 n × sizeof(TYPE)만큼 증가한다면,
이를 배열 요소에 접근할 때도 이용할 수 있지 않을까?
배열의 각 요소 간 주소 차이는 자료형의 바이트 크기이니 말이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
#include <stdio.h>
int main()
{
int arr[5] = { 11, 22, 33 ,44 ,55 };
int* ptr = arr; //int* ptr = &arr[0];과 동일
printf("*ptr: %d\n", *ptr);
printf("*(ptr + 1): %d\n", *(ptr + 1));
printf("*(ptr + 2): %d\n", *(ptr + 2));
printf("*(ptr + 3): %d\n", *(ptr + 3));
printf("*(ptr + 4): %d\n\n", *(ptr + 4));
printf("*ptr: %d\n", *ptr);
++ptr;
printf("*ptr: %d\n", *ptr);
++ptr;
printf("*ptr: %d\n", *ptr);
++ptr;
printf("*ptr: %d\n", *ptr);
--ptr;
printf("*ptr: %d\n", *ptr);
return 0;
}
|
cs |
위 예제를 통해 많은 것을 알 수 있다.
우선 ptr + i 는 다음과 같음을 알 수 있다.
그리고 ++ptr 과 --ptr 은 다음과 같이 이루어짐을 알 수 있다.
마지막으로, 다음의 결론을 도출할 수 있다.
*(arr + i)와 arr[i]는 같다.
arr 에는 포인터 변수도 올 수 있고 배열의 이름도 올 수 있다. 어느 것이든 식은 성립한다.
4. 문자열 포인터
다음 두 문장의 차이는 무엇일까?
char str1[] = "string";
char* str2 = "string";
전자는 str1 이라는 배열에 "string" 이라는 문자열 리터럴이 저장되는 형태고
후자는 str2 라는 포인터가 "string" 을 단순히 가리키는 형태다.
그러니까 전자는 변수 형태의 포인터고, 후자는 상수 형태의 포인터라는 것이다.
물론 이름이 문자 s 의 주소를 의미한다는 것은 같지만, 메모리에 저장되는 형태가 다르다.
큰 따옴표로 묶여서 표현되는 문자열은 메모리 공간에 저장된 후 그 주소가 반환된다.
그러니까 실제로 str2는 다음과 같은 형태로 초기화되는 것이다.
char* str2 = 0x00352f4d;
때문에 다음과 같은 차이점이 있다.
str1: 문자열의 일부를 변경할 수 있지만, 다른 위치를 가리킬 수 없다.
str2: 문자열의 일부를 변경할 수 없지만, 다른 위치를 가리킬 수 있다.
앞에서 언급했듯이 배열은 상수 포인터이기 때문에, 가리키는 위치를 변경할 수 없다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#include <stdio.h>
int main()
{
char str1[] = "string";
char* str2 = "string";
printf("str1: %s\n", str1);
printf("str2: %s\n", str2);
str1[0] = 'S';
str2[0] = 'S';
printf("str1: %s\n", str1);
printf("str2: %s\n", str2);
//Error: str1 = "beyond";
str2 = "beyond";
printf("str1: %s\n", str1);
printf("str2: %s\n", str2);
return 0;
}
|
cs |
컴파일은 된다. 하지만 실행 시 12행에서 문제가 발생해 그 뒤의 코드는 실행되지 않는다.
컴파일러에 따라 발생하는 문제의 형태는 다르다. 이를 허용하는 컴파일러도 있다.
하지만 코드의 범용성을 위해 모든 컴파일러에서 동작하는 코드가 아니라면 지양하는 것이 좋다.
참고로 위 코드에서 12행을 주석 처리하면 다음과 같이 출력된다.
주석처리 하지 않았을 때도 printf 함수가 호출되지 않았을 뿐이지
str1 의 첫 글자는 변경되었음을 알 수 있다.
5. 포인터 배열
포인터도 일종의 자료형이기 때문에, 포인터로 이루어진 배열도 선언할 수 있다.
여타 배열과 마찬가지로, 다음과 같이 선언하면 된다.
int* arr[3] = { &n1, &n2, &n3 };
포인터라고 해서 어렵게 생각할 것 없다.
int형 배열이나 double형 배열처럼 값을 저장할 수 있는 변수들을 나열한 것에 불과하다.
다만 그 값이 주소값일 뿐이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#include <stdio.h>
int main()
{
int n1 = 1, n2 = 2, n3 = 3;
int* arr[3] = { &n1, &n2, &n3 };
printf("n1: %d\n", n1);
printf("*arr[0]: %d\n\n", *arr[0]);
printf("n2: %d\n", n2);
printf("*arr[1]: %d\n\n", *arr[1]);
printf("n3: %d\n", n3);
printf("*arr[2]: %d\n\n", *arr[2]);
return 0;
}
|
cs |
이를 메모리 구조로 보면 다음과 같다,
위 코드와 같은 정수 배열은 물론, 실수나 문자열 배열도 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <stdio.h>
int main()
{
char* arr[3] = { "Apple", "Banana", "Cat" };
printf("arr[0]: %s\n", arr[0]);
printf("arr[1]: %s\n", arr[1]);
printf("arr[2]: %s\n", arr[2]);
return 0;
}
|
cs |
리터럴 형태의 문자열은 주소를 반환한다.
따라서 다음과 같은 형태로 초기화된다.
char* arr[3] = { 0x008FFB40, 0x008FFB40, 0x008FFB40 };
각각 A , B , C 의 주소를 나타낸다.
따라서 메모리 구조로 나타내면 다음과 같다.
int* 배열과 다를 게 없다.
물론 가리키는 대상이 정수인가, 문자열인가의 차이는 있지만
메모리 공간 상의 위치를 가리킨다는 점에서는 동일하다.