모노산달로스의 행보

[C programming] 포인터를 통한 배열 값 접근, 배열 포인터와 포인터 배열 차이점, 값에 의한 호출과 참조에 의한 호출 차이점 본문

ProgrammingLanguage/C

[C programming] 포인터를 통한 배열 값 접근, 배열 포인터와 포인터 배열 차이점, 값에 의한 호출과 참조에 의한 호출 차이점

모노산달로스 2024. 4. 10. 22:33

C programming - 포인터 배열

 

 

리눅스 환경에서 네트워크 프로그래밍을 공부하기 위해서 C언어를 다시 복습해야 할 필요성을 느꼈습니다. 따라서 이번 기회에 배열부터 전처리기까지 내용들을 정리하겠습니다.


포인터와 1차원 배열

#include <stdio.h>
int main(void)
{
    int array[3]={10, 20, 30};
    int *p = NULL;

    p = array;

    printf("%x %x %x\n", p, p+0, &p[0]);
    printf("%x %x\n", p+1, &p[1]);
    printf("%x %x \n", p+2, &p[2]);
    
    printf("%d %d %d\n", *p, *(p+0), *(&p[0]));
    printf("%d %d\n", *(p+1), *&p[1]);
    printf("%d %d\n", *(p+2), *&p[2]);
    
    printf("size of array : %d size of pointer : %d \n", sizeof(array), sizeof(p));

    return 0;
}

예제 코드의 출력 결과

 

포인터에 배열의 시작 주소를 저장하면 위와 같이 1차원 배열의 주소에 접근하는 것이 가능합니다. 배열의 주소 참조 방법과 동일하게 이루어집니다. 값의 참조도 마찬가지로 *을 붙여 접근이 가능합니다.

 

또한, 배열의 크기포인터 변수의 크기는 차이가 존재합니다. 선언된 배열의 자료형은 int로서 요소 하나당 4의 크기를 가지게 됩니다. 배열의 길이가 3으로 선언되었으므로 배열의 크기는 12가 됩니다. 하지만 이를 가리키는 포인터 변수의 크기는 8이 출력됩니다. (64bit 환경에서는 8이 출력되고 32bit 환경에서는 4가 출력됩니다.)

 

즉, 포인터 변수의 크기는 고정되어 있지만 배열의 크기는 배열 길이에 따라 가변적으로 정해집니다.

 


포인터와 2차원 배열

#include <stdio.h>
int main(void)
{
    int array[2][3] = {10, 20, 30, 40, 50 ,60};
    int* p = NULL;

    p = array;

    printf("%x %x %x \n", &p[0], &p[1], &p[2]);
    printf("%x %x %x \n", &p[3], &p[4], &p[5]);

    printf("%d %d %d\n", p[0], p[1], p[2]);
    printf("%d %d %d\n", p[3], p[4], p[5]);

    return 0;
}

예제 코드의 출력 결과

 

2차원 배열의 이름은 시작 주소를 나타낸다는 것을 이전에 정리했었습니다. 포인터로 2차원 배열을 참조하기 위해 배열의 시작 주소를 저장합니다. 코드에서 p[0], p[1], p[2] ... 와 같이 1차원 포인터 변수 p가 2차원 배열을 1차원으로 접근하고 있습니다.

 

즉 아래와 같은 표현은 잘못되었습니다.

printf("%d %d %d \n", p[0][0], p[0][1], p[0][2]); // error
printf("%d %d %d \n", p[1][0], p[1][1], p[1][2]); // error

 

그렇다면 배열을 2차원으로 접근하기 위해 어떻게 해야 할까요? 여기서 우리는 배열 포인터를 사용할 수 있습니다.

 


배열 포인터

int (*p) [3];

 

기존의 포인터를 선언하는 방식과 동일하지만 *연산자와 포인터의 이름괄호로 묶어줍니다. 그리고 포인터가 가리키는 배열의 열의 길이를 지정해줍니다.

 

#include <stdio.h>
int main(void)
{
    int array[2][3] = {10, 20, 30, 40, 50, 60};
    int (*p)[3] = NULL;

    p = array;

    printf("%d %d %d \n", p[0][0], p[0][1], p[0][2]);
    printf("%d %d %d \n", p[1][0], p[1][1], p[1][2]);

    return 0;
}

예제 코드의 출력 결과

 

배열 포인터를 선언할 때는 꼭 가리키는 배열의 열의 길이를 지정해 주는 것에 유의합시다. 배열 포인터를 사용해 2차원으로 접근하면 에러가 발생하지 않는 것을 확인할 수 있습니다.

 

printf("%d %d %d \n", *(p[0]+0),*(p[0]+1), *(p[0]+2));
printf("%d %d %d \n", *(*(p+1)+0), *(*(p+1)+1), *(*(p+1)+2));

 

위와 같은 표현도 같은 결과가 출력되는 것 또한 기억해 둡시다.

 


포인터 배열

방금 정리한 배열 포인터와 헷갈릴 수 있는 포인터 배열이 존재합니다. 배열 포인터는 배열을 가리키는 포인터를 의미하고, 포인터 배열은 주소를 저장하는 배열을 의미합니다.

int* pointer[3];

 

기존에 배열을 선언하는 방식과 동일합니다. 하지만 자료형을 표현하는 부분에 *연산자를 붙여 포인터 배열임을 알려줍니다.

 

#include <stdio.h>
int main(void)
{
    int a = 10, b = 20, c = 30;
    int* ap[3] = {NULL, NULL, NULL};

    ap[0]=&a;
    ap[1]=&b;
    ap[2]=&c;

    printf("%x %x %x \n", &a, &b, &c);
    printf("%x %x %x \n", ap[0], ap[1], ap[2]);
    printf("%x %x %x \n", *(ap+0), *(ap+1), *(ap+2));

    printf("%d %d %d \n", *&a, *&b, *&c);
    printf("%d %d %d \n", *ap[0],*ap[1],*ap[2] );
    printf("%d %d %d \n", **(ap+0),**(ap+1),**(ap+2) );

    return 0;
}

예제 코드의 출력 결과

포인터 변수의 수가 증가하게 되면 주소 관리가 어려워질 수 있습니다. 포인터 배열을 사용하면 위와 같이 주소를 체계적으로 관리할 수 있습니다.

 


인수 전달 방법

C언어에서는 값에 의한 호출(call by value)참조에 의한 호출(call by reference)이 존재합니다.

#include <stdio.h>
void swap(int x, int y);
int main(void)
{
    int a = 100, b = 200;

    swap(a, b);
    printf("a: %d, b: %d\n", a, b);

    return 0;
}
void swap(int x, int y) {
    int tmp;
    
    tmp = x;
    x = y;
    y = tmp;
}

예제 코드의 출력 결과

 

위 코드는 값에 의한 호출에 대한 예제입니다. swap이라는 함수는 두 변수의 값을 교환하는 기능을 수행합니다. 그런데 두 변수 a와 b의 값을 분명 swap 함수를 통해 교환하였음에도 출력 값은 기존과 동일하게 나타납니다.

 

왜냐하면 swap에 사용된 값은 변수 a와 b의 값을 복사한 것이기 때문입니다. 즉, 그것이 교환이 이루어진 대상이 실제 변수 a와 b를 의미하는 것이 아닙니다.

 

이러한 문제를 해결하기 위해서 포인터를 통해 참조에 의한 호출을 사용할 수 있습니다.

#include <stdio.h>
void swap(int *x, int *y);
int main(void)
{
    int a = 100, b = 200;

    swap(&a, &b);
    printf("a: %d, b: %d\n", a, b);

    return 0;
}
void swap(int *x, int *y) {
    int tmp;
    
    tmp = *x;
    *x = *y;
    *y = tmp;
}

예제 코드의 출력 결과

 

이번에는 swap 함수에 변수 a와 b의 주소 값이 전달됩니다. 이제 함수에서 복사된 값이 아닌 주소에 존재하는 실제 변수에 접근하는 것입니다. 즉, 참조에 의한 호출이 이루어진 것입니다.

 

#include <stdio.h>
void func(int a[], int size);
int main(void)
{
    int a[3] = {1, 2, 3};
    printf("%2d %2d %2d \n", a[0], a[1], a[2]);
    func(a, 3);
    printf("%2d %2d %2d \n", a[0], a[1], a[2]);

    return 0;
}
void func(int a[], int size) {
    for(int i=0; i<size; i++) {
        a[i] = a[i] * 10;
    }
}

예제 코드의 출력 결과

 

배열의 경우는 포인터를 사용하지 않더라도 참조에 의한 호출이 이루어집니다.

 

#include <stdio.h>
void func(int *a, int size);
int main(void)
{
    int a[3] = {1, 2, 3};
    int *ptr = a;
    printf("%2d %2d %2d \n", a[0], a[1], a[2]);
    func(ptr, 3);
    printf("%2d %2d %2d \n", a[0], a[1], a[2]);

    return 0;
}
void func(int *a, int size) {
    for(int i=0; i<size; i++) {
        a[i] = a[i] * 10;
    }
}

 

물론 포인터를 사용하는 방법 또한 가능합니다.

 

void func(int (*a)[3], int row_size, int col_size);
void func(int a[][3], int row_size, int col_size);

 

2차원 배열을 함수에 사용하는 경우 위와 같은 표현식을 사용할 수 있습니다. 배열의 열의 길이는 꼭 표시해 주어야 합니다.