본문 바로가기

포인터와 배열의 관계

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= { 012 };
    
    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= { 012 };
 
    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= { 012 };
    double arr2[4= { 0.11.12.13.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= { 012 };
    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= { 112233 ,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* 배열과 다를 게 없다.

물론 가리키는 대상이 정수인가, 문자열인가의 차이는 있지만 

메모리 공간 상의 위치를 가리킨다는 점에서는 동일하다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'Programming > C' 카테고리의 다른 글

다중 포인터  (0) 2019.03.24
매개변수  (0) 2019.03.24
포인터  (0) 2019.03.16
1차원 배열  (0) 2019.03.15
변수  (0) 2019.03.07