[2023 방통대 C 스터디] 8. [참조에 의한 호출] 마법!!!!

포인터는 어디에 사용할까요? 포인터는 메모리를 직접 조작할 수 있는 마법과도 같은 강력한 수단이기 때문에 아주 다양한 목적으로 활용할 수 있습니다. 포인터를 활용하는 몇 가지 예시를 소개해보도록 하겠습니다.
값에 의한 호출 #
위의 코드는 변수 a에 0을 저장하고 add() 함수에 변수 a를 넣어 1을 더하도록 의도한 코드입니다. 결과는 어떻게 될까요? 의도한 것과 다르게 0이 출력됩니다.
왜 그럴까요?
이는 함수 add()가 값에 의한 호출 방식으로 실행되기 때문입니다. add() 함수가 실행될 때 매개변수 add_a에는 인자값 a가 복사되어 버립니다. 즉, a와 add_a는 다른 변수인 것이죠.
💡 함수에 대한 햇갈리기 쉬운 용어가 다수 등장합니다.
- 인자값(argument): 함수에 전달하는 변수
- 매개변수(parameter): 인자값으로 전달된 변수를 함수 내에서 사용할 수 있도록 복사한 변수
실제로 함수 안과 밖에서 변수 a와 add_a의 메모리 주소값을 출력해보면 서로 다른 것을 알 수 있습니다. add() 함수 안에서 아무리 add_a 매개변수를 수정해봤자 밖의 변수 a와 메모리 주소가 다르기 때문에 바꾼 내용이 저장되지 않겠죠.
참조에 의한 호출 #
그럼 대신 add() 함수에 변수 a의 메모리 주소번지를 인자값으로 넘기면 어떻게 될까요?
add() 함수의 인자값으로 a 변수의 주소(&a)를 넣기 위해 매개변수를 포인터형으로 수정하고(int *add_a), 역참조 연산자를 이용해 메모리 주소의 데이터에 1을 더하도록(2번 줄) 수정했습니다. 함수 속의 printf()함수 역시 포인터를 출력하도록(3번 줄) 변경합니다.
이렇게 수정한 후 코드를 실행하면 add() 함수에서 변수 a의 메모리를 직접 접근해 1을 더해버리게 됩니다. 결론적으로 add()함수가 실행된 후에는 a에 1이 더해지게 되겠죠.
이것을 바로 참조에 의한 호출이라 합니다.
참조에 의한 호출 응용 - swap() 예시
#
가장 대표적인 참조에 의한 호출 응용으로는 swap() 함수가 있습니다.
c #include <stdio.h>
void swap(int *a_p, int *b_p) { int c; c = *b_p; *b_p = *a_p; *a_p = c; }
int main() { int a = 10, b = 20;
printf("%d, %d\n", a, b);
swap(&a, &b);
printf("%d, %d\n", a, b);
return 0;
}
이 함수는 두 개의 정수를 가리키는 포인터를 전달받아, 두 정수의 값을 서로 교환해주는 역할을 합니다.
void swap(int* a, int* b): 두 개의 포인터를 매개변수로 받는 함수swap을 정의합니다.int temp = *a;:a가 가리키는 값을temp에 저장합니다.a = *b;:b가 가리키는 값을a가 가리키는 곳에 저장합니다.b = temp;:temp에 저장된 값을b가 가리키는 곳에 저장합니다.
이렇게 하면 a와 b가 가리키는 값이 서로 교환됩니다.
참조에 의한 호출 응용 - 배열과 사진의 전달 #
함수로 배열을 전송하고 싶다면 어떻게 해야할까요? 사진은 또 어떻게 보낼 수 있을까요?
간단합니다. 데이터의 시작점 메모리 주소와 데이터의 크기를 전송하면 됩니다.
c #include <stdio.h>
void printArray(int* arr, int size) { for (int i = 0; i < size; i++) { printf("%d ", *(arr + i)); } }
int main() { int arr[] = { 1, 2, 3, 4, 5 }; int size = sizeof(arr) / sizeof(int); printArray(arr, size); return 0; }
위 코드에서 printArray() 함수에 배열 arr과 배열의 크기 size를 인자로 전달하고 있습니다. 이때 arr은 배열의 첫 번째 요소를 가리키는 포인터입니다. 함수 내에서 arr의 값을 역참조하여 배열의 요소를 출력하는 것을 볼 수 있습니다.
사진은 [R, G, B]로 구성된 하나의 픽셀 배열이 카메라의 화소만큼 존재합니다. 당연하게도 2차원 배열을 전달하기만 하면 되죠.
c #include <stdio.h> #include <malloc.h>
typedef struct { char r; char g; char b; } pixel_t;
void process_pixels(pixel_t** pixels, int width, int height) { // do something with the pixel data // 예시로 각 픽셀의 r, g, b 값을 출력해봅시다. for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { printf("(%d, %d, %d) ", pixels[i][j].r, pixels[i][j].g, pixels[i][j].b); } printf("\n"); } }
pixel_t** convert_to_pointer(pixel_t pixels[3][3]) { // 행을 가리키는 포인터들을 동적으로 할당합니다. pixel_t** ptr = malloc(3 * sizeof(pixel_t*)); for (int i = 0; i < 3; i++) { // 각 포인터가 가리키는 주소에 pixels 배열의 값을 복사합니다. ptr[i] = malloc(3 * sizeof(pixel_t)); for (int j = 0; j < 3; j++) { ptr[i][j] = pixels[i][j]; } } return ptr; }
int main() { // 3x3 크기의 픽셀 배열을 만듭니다. pixel_t pixels[3][3] = { {{255, 0, 0}, {0, 255, 0}, {0, 0, 255}}, {{255, 255, 0}, {255, 0, 255}, {0, 255, 255}}, {{128, 128, 128}, {0, 0, 0}, {255, 255, 255}} };
// convert_to_pointer 함수를 사용하여 2차원 배열을 이중 포인터로 변환합니다.
pixel_t** ptr = convert_to_pointer(pixels);
// process_pixels 함수를 호출하여 픽셀 데이터를 전달합니다.
process_pixels(ptr, 3, 3);
// 동적으로 할당한 메모리를 해제합니다.
for (int i = 0; i < 3; i++) {
free(ptr[i]);
}
free(ptr);
return 0;
}
아직 배우지 않은 문법을 조금 사용해 조금 코드가 복잡한데요, process_pixels() 함수에 집중하면 됩니다. 픽셀의 데이터가 저장된 포인터와 가로, 세로 크기를 넘기는게 보이시죠?
이처럼 참조에 의한 호출은 아무리 큰 데이터라 하더라도 항상 8 byte로 고정된 크기의 메모리의 주소값을 전달하기 때문에 (1)데이터의 전달 속도가 아주 빠르고, 데이터를 복사하지 않기 때문에 (2)메모리의 낭비가 방지됩니다.
💡 물론 이러한 이유로 함수에서 포인터의 내용을 삭제(수정)하면 함수 밖에서도 이러한 내용이 반영됩니다. 이러한 개념을 깊은 복사, 얕은 복사라고 합니다.
참조에 의한 호출 응용 - 구조체의 전달 #
구조체 역시 불필요하게 구조체 전체를 복사하지 않고, 구조체의 포인터를 전달할 수 있습니다.
위 코드의 12번 라인을 보면 구조체 변수인 s의 포인터를 print_ascii_data() 변수에 인자값으로 전달합니다. 이렇게 전달받은 인자값은 구조체 포인터인 매개변수로 복사되어 7번 라인에서 역참조 연산자와 함께 사용됩니다.
연산자 우선순위로 인해 매개변수 s 앞에 역참조연산자(*)를 붙인 후 괄호로 감싸(연산자 우선순위) 그 후 도트 연산자(.)를 이용해 멤버 변수를 사용하는것을 볼 수 있습니다.
그런데, 조금 많이 복잡하죠?
이렇게 자주 사용할 것을 아는지 C에서는 이 과정을 한번에 해주는 연산자를 제공합니다. 바로 화살표 연산자(->)입니다. 화살표 연산자를 사용하면 복잡했던 코드가 이렇게 변합니다.
화살표 연산자(->)를 사용하면 구조체 포인터를 역참조한 뒤 화살표 연산자 뒤의 멤버 변수를 가져옵니다.
이처럼 구조체를 함수로 전달할 때 포인터를 활용하면 아주 복잡한 구조체를 빠르고 효율적으로 전달할 수 있습니다.