본문 바로가기
~ 2024.03/C 언어

[C 언어 기초] 포인터와 배열

by Monett 2023. 12. 13.
반응형

K.N.King 의 C Programming - A Modern Approach 를 공부하며 내용을 정리한 글 입니다.
현재 블럭의 내용은 작성자의 의견 혹은 생각이며, 틀린 내용이 있을 수 있습니다. 지적 감사드립니다.

포인터 산술

포인터는 배열의 원소를 가리킬 수 있다.

int a[10];
int *p = &a[0];

 

위 코드를 통해 포인터 변수 p로 a[0]에 접근할 수 있다.

예를 들어, a[0]에 5라는 값을 저장하고 싶으면 다음과 같이 해주면 된다.

*p = 5;

 

p를 a의 첫번째 원소를 가리키게 만드는건 그렇게 대단한 일은 아니다.

하지만 p에 포인터 산술(pointer arithmetic)을 사용하게 되면 a의 다른 원소에 접근이 가능하다.

 

C는 세 가지의 포인터 산술을 지원한다.

  • 포인터에 정수 더하기
  • 포인터에 정수 빼기
  • 포인터에 다른 포인터 빼기

이제부터 나오는 예제 코드는 다음 선언을 했음을 가정한다.

int a[10];
int *p;
int *q;
int i;

포인터에 정수 더하기

정수 j를 포인터 p에 더하게 되면 p가 현재 가리키는 원소에서부터 j만큼 떨어진 원소를 가리키게 된다.

다시 말해, p가 a[i]를 가리키고 있다면 p+j는 a[i+j]를 가리킨다.

포인터에 정수 빼기

더하기와 동일하게, p가 a[i]를 가리킨다면 p - j는 a[i-j]를 가리킨다.

포인터끼리 빼기

포인터끼리 뺄셈의 결과는 포인터간의 거리이다.

그러므로 p는 a[i]를 가리키고, q는 a[j]를 가리킨다고 하면 p - q 는 i - j와 같다.

포인터간 비교

포인터도 관계 연산자와 동등 연산자를 사용해서 비교할 수 있다.

p = &a[5];
q = &a[1];

p > q; // 1
p < q; // 0

포인터를 통한 배열 처리

개인적으로 재밌다고 생각한 부분이다.
#define N (10)
…
int a[N];
int sum;
int* p;
…
sum = 0;
for (p = &a[0]; p < &a[N]; ++p) {
    sum += *p;
}
++p 는 p = p + 1과 동일한 효과이기 때문에, p가 현재 가리키고 있는 원소의 다음 원소를 가리킬 수 있도록 해준다.

 

p < &a[N] 조건은 a[N]이 존재하지 않음에도 우리는 a[N]에 주소 연산자를 써줄 수 있다. 

루프 본문은 p가 &a[0] 부터 &a[N - 1]와 같을 때 계속 실행되고, p가 &a[N]과 같아지는 순간 종료된다.


* 연산자와 ++ 연산자 섞어 쓰기

C프로그래머들은 배열 원소를 처리하는 구문에 * 연산자와 ++ 연산자를 섞어 쓰곤 한다.

 

배열 참조를 사용한다면 다음과 같이 작성할 것이다.

a[i++] = j;

 

만약 p가 배열의 원소를 가리키고 있다면 아래 구문은 위와 동일하다.

*p++ = j;

 

++ 후위 연산자는 *보다 우선순위를 갖기 때문에 컴파일러는 이를 다음과 같이 인식한다.

*(p++) = j;

 

위 코드 뿐만 아니라, 이런 코드도 가능하다

(*p)++;

 

이는 p가 가리키는 오브젝트(p가 int를 가리킨다면 정수 그 자체)를 반환하고 해당 오브젝트를 증가시켜준다.

 

아래 표는 이러한 연산에 대한 정리이다.

표현식 의미
*p++ 혹은 *(p++) 증가되기 이전의 표현식의 값은 *p이다; 그리고 p가 증가된다
(*p)++ 증가되기 이전의 표현식의 값은 *p이다; 그리고 *p가 증가된다
*++p 혹은 *(++p) p가 증가된다; 증가된 이후의 표현식의 값은 *p이다
++*p 혹은 ++(*p) *p가 증가된다; 증가된 이후의 표현식의 값은 *p이다

배열 이름 포인터로서 쓰기

배열 이름은 배열의 첫번째 원소를 가리키는 포인터로서 사용할 수 있다.

 

예를 들어,

int a[10];

a라는 이름의 배열을 선언 했을때, 

*a = 7;

a를 배열의 첫번째 원소를 가리키는 포인터로서 a[0]의 값을 바꿀 수 있다.

 

물론 아래 코드도 가능하다.

*(a + 1) = 12;

이는 a[1]의 값을 12로 바꿔준다.

 

정리하자면, a + i는 &a[i]와 같은 것이며(둘 다 a의 i번째 원소를 가리키는 포인터를 의미한다) *(a+i)는 a[i]와 같다. 다시 말해 배열 첨자는 일종의 포인터 산술의 한 형태로 볼 수 있다는 것이다.

 

배열의 이름을 포인터로 사용할 수 있다는 사실은 배열에 대한 루프를 작성하는데 큰 도움이 된다.

// before
for (p = &a[0]; p < &a[N]; ++p
{
	sum += *p;
}

// after
for(p = a; p < a + N; ++p)
{
	sum += *p;
}

배열 입력변수

배열의 이름이 함수에 전달되는 순간 그 이름은 포인터로서 취급되어진다.

int find_largest(int array[], int size)
{
    int i;
    int max;

    max = array[0];
    for (i = 1; i < size; ++i) {
        if (array[i] > max) {
            max = array[i];
        }
    }

    return max;
}

 

 

위 함수를 다음과 같이 호출했다고 가정해보자.

largest = find_largest(b, N);

 

 

위의 경우 array에 b의 첫번째 원소를 가리키는 포인터가 할당된다. 즉, 배열 그 자체가 복사되지 않는다.

 

배열 입력변수가 포인터로서 취급이 되기에 여러가지 중요한 점들을 파악할 수 있다.

  • 일반적인 변수가 함수에 전달되면 그 값이 복사된다. => 해당 매개변수에 대한 수정사항은 변수에 영향을 미치지 않는다.
  • 반대로, 배열이 전달되면 복사본이 전달되는 것이 아닌 포인터가 할당되는 것이기 때문에 값이 변하면 원본이 변한다.
  • 원본이 바뀌지 않게 해주려면 선언부에 const를 추가해주면 된다.

포인터 배열 이름으로서

만약 배열의 이름이 포인터로서 쓸 수 있다면, 반대로 포인터를 마치 배열 이름인척 쓸 수 있을까?

#define N (10)
…
int array[N];
int i;
int sum = 0;
int* ptr_array = array;
…
for (i = 0; i < N; ++i) {
    sum += ptr_array[i];
}

 

당연히 가능하다. 컴파일러는 ptr_array[i]를 *(ptr_array + i)처럼 대할 것이다.

 

이는 지극히 정상적인 포인터 산술 방법이다.

반응형

'~ 2024.03 > C 언어' 카테고리의 다른 글

[C 언어 기초] 동적 메모리 할당  (0) 2023.12.13
[C 언어 기초] 문자열 문자마다 읽기  (0) 2023.12.13
[C 언어 기초] 포인터  (0) 2023.12.13
[C 언어 기초] 선택문  (0) 2023.12.13
[C 언어 기초] 표현식  (0) 2023.12.13