Category (Click)
개발보드 덕질하기
🪄

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

2023 방통대 C 스터디 강의노트
한국방송통신대학교 컴퓨터과학과의 C언어 스터디에 사용한 자료입니다.
배움은 끝이 없기에 강의노트라 하더라도 오류가 있을 수 있으며, 이 점에 대해서는 둥글게 지적 부탁드립니다.
읽기 전 아래 내용을 숙지하여주시기 바랍니다.
강의 노트에서 중요한 부분은 이렇게 노란색 전구()로 강조합니다.
한국어로 번역했을 때 뜻이 명확하지 않은 일부 용어는 의미를 분명히 하기 위해 한국어로 뜻을 번역하지 않고 영어 원문을 그대로 사용하거나 말음을 한국어로 표기합니다.
강의 노트는 스터디 내용을 대체할 수 없으며 이해를 위한 자세한 내용은 스터디에서 언급합니다.
강의 노트는 Light 테마에서 보시는것을 권장합니다.
스터디는 Windows 11, Visual Studio IDE 환경에서 진행합니다.
mac인 경우 패러렐즈를 이용해 Windows 11 ARM을 설치하여 동일하게 진행할 수 있습니다.
모든 강의 노트의 저작권은 작성자 차완기에게 있으며 무단 복제를 금합니다.

#방통대 2023 C 스터디 강의노트 태그 전체 글보기

포인터는 어디에 사용할까요? 포인터는 메모리를 직접 조작할 수 있는 마법과도 같은 강력한 수단이기 때문에 아주 다양한 목적으로 활용할 수 있습니다. 포인터를 활용하는 몇 가지 예시를 소개해보도록 하겠습니다.

값에 의한 호출

위의 코드는 변수 a에 0을 저장하고 add() 함수에 변수 a를 넣어 1을 더하도록 의도한 코드입니다. 결과는 어떻게 될까요? 의도한 것과 다르게 0이 출력됩니다.
왜 그럴까요?
이는 함수 add()값에 의한 호출 방식으로 실행되기 때문입니다. add() 함수가 실행될 때 매개변수 add_a에는 인자값 a가 복사되어 버립니다. 즉, aadd_a는 다른 변수인 것이죠.
함수에 대한 햇갈리기 쉬운 용어가 다수 등장합니다.
인자값(argument): 함수에 전달하는 변수
매개변수(parameter): 인자값으로 전달된 변수를 함수 내에서 사용할 수 있도록 복사한 변수
실제로 함수 안과 밖에서 변수 aadd_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() 함수가 있습니다.
#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; }
C
복사
이 함수는 두 개의 정수를 가리키는 포인터를 전달받아, 두 정수의 값을 서로 교환해주는 역할을 합니다.
void swap(int* a, int* b): 두 개의 포인터를 매개변수로 받는 함수 swap을 정의합니다.
int temp = *a;: a가 가리키는 값을 temp에 저장합니다.
a = *b;: b가 가리키는 값을 a가 가리키는 곳에 저장합니다.
b = temp;: temp에 저장된 값을 b가 가리키는 곳에 저장합니다.
이렇게 하면 ab가 가리키는 값이 서로 교환됩니다.

참조에 의한 호출 응용 - 배열과 사진의 전달

함수로 배열을 전송하고 싶다면 어떻게 해야할까요? 사진은 또 어떻게 보낼 수 있을까요?
간단합니다. 데이터의 시작점 메모리 주소와 데이터의 크기를 전송하면 됩니다.
#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; }
C
복사
위 코드에서 printArray() 함수에 배열 arr과 배열의 크기 size를 인자로 전달하고 있습니다. 이때 arr은 배열의 첫 번째 요소를 가리키는 포인터입니다. 함수 내에서 arr의 값을 역참조하여 배열의 요소를 출력하는 것을 볼 수 있습니다.
사진은 [R, G, B]로 구성된 하나의 픽셀 배열이 카메라의 화소만큼 존재합니다. 당연하게도 2차원 배열을 전달하기만 하면 되죠.
#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; }
C
복사
아직 배우지 않은 문법을 조금 사용해 조금 코드가 복잡한데요, process_pixels() 함수에 집중하면 됩니다. 픽셀의 데이터가 저장된 포인터와 가로, 세로 크기를 넘기는게 보이시죠?
이처럼 참조에 의한 호출은 아무리 큰 데이터라 하더라도 항상 8 byte로 고정된 크기의 메모리의 주소값을 전달하기 때문에 (1)데이터의 전달 속도가 아주 빠르고, 데이터를 복사하지 않기 때문에 (2)메모리의 낭비가 방지됩니다.
물론 이러한 이유로 함수에서 포인터의 내용을 삭제(수정)하면 함수 밖에서도 이러한 내용이 반영됩니다. 이러한 개념을 깊은 복사, 얕은 복사라고 합니다.

참조에 의한 호출 응용 - 구조체의 전달

구조체 역시 불필요하게 구조체 전체를 복사하지 않고, 구조체의 포인터를 전달할 수 있습니다.
위 코드의 12번 라인을 보면 구조체 변수인 s의 포인터를 print_ascii_data() 변수에 인자값으로 전달합니다. 이렇게 전달받은 인자값은 구조체 포인터인 매개변수로 복사되어 7번 라인에서 역참조 연산자와 함께 사용됩니다.
연산자 우선순위로 인해 매개변수 s 앞에 역참조연산자(*)를 붙인 후 괄호로 감싸(연산자 우선순위) 그 후 도트 연산자(.)를 이용해 멤버 변수를 사용하는것을 볼 수 있습니다.
그런데, 조금 많이 복잡하죠?
이렇게 자주 사용할 것을 아는지 C에서는 이 과정을 한번에 해주는 연산자를 제공합니다. 바로 화살표 연산자(->)입니다. 화살표 연산자를 사용하면 복잡했던 코드가 이렇게 변합니다.
화살표 연산자(->)를 사용하면 구조체 포인터를 역참조한 뒤 화살표 연산자 뒤의 멤버 변수를 가져옵니다.
이처럼 구조체를 함수로 전달할 때 포인터를 활용하면 아주 복잡한 구조체를 빠르고 효율적으로 전달할 수 있습니다.