함수는 인자를 전달받도록 정의할 수 있다. (함수는 원래 인자 전달과 값의 반환이 가능)
배열 대상의 인자를 전달하는 원리를 알기 위해서는 인자 전달원리에 대한 확실한 이해가 필요하다.
"함수호출 시 전달되는 인자의 값은 매개변수에 복사 된다."
함수가 호출되면 복사가 되기 때문에 전달되는 인자와 매개변수는 별개가 된다.⭐
int SimpleFunc(int num)
{
...
}
int main(void)
{
int age = 17;
SimpleFunc(age);
}
위 예제를 보면 SimpleFunc 함수의 호출을 통해서 인자로 age를 전달하고 있다.
이 때, 실제로 전달되는 것은 age가 아닌 age에 저장된 값
이다.⭐
그리고 그 값이 매개변수 num에 복사되는 것이다.
그렇다면 SimpleFunc 함수 내에서 매개변수 num에 저장된 값을 1 증가시킬 경우, 변수 age의 값은 얼마 증가할까? 정답은 '무관하다.'이다.
num과 age는 별개의 변수이기 때문이다.
함수 호출 시 인자로 배열을 통째로 전달하는 방법이 있을까?
아쉽게도 통째로 넘겨주는 방버은 없다. 매개변수로 배열을 선언할 수 없기 때문이다.
배열을 통째로 넘겨받으려면 매개변수로 배열을 선언할 수 있어야 한다.
다만, 함수 내에서 배열에 접근할 수 있도록 배열의 주소 값을 전달하는 것은 가능하다.
배열의 주소 값을 인자로 전달해서 이를 통해서 접근하도록 유도할 수 있다.
(아파트를 보고 싶어하는 사람에게 아파트를 통채로 복사해 놓을 수 없으니, 아파트 주소를 알려주는 것 처럼😂)
int arr[3] = {1, 2, 3};
int * ptr = arr;
SimpleFunc(arr); // SimpleFunc 함수를 호출하면서 배열 arr의 주소 값 전달
위 예제처럼 SimpleFunc 함수에 배열의 주소 값을 전달할 수 있다.
SimpleFunc 함수는 어떻게 작성되어야할까?
void SimpleFunc(int * param)
{
....
}
위와 같이 int형 포인터 변수가 함수의 매개변수로 선언되어야한다.
이 모든 내용을 합쳐 하나의 예제로 확인해보자!
#include <stdio.h>
void ShowArayElem(int* param, int len)
{
int i;
for (i = 0; i < len; i++)
printf("%d ", param[i]);
printf("\n");
}
int main()
{
int arr1[3] = { 1, 2, 3 };
int arr2[5] = { 4, 5, 6, 7, 8 };
ShowArayElem(arr1, sizeof(arr1) / sizeof(int));
ShowArayElem(arr2, sizeof(arr2) / sizeof(int));
return 0;
}
> 출력
1 2 3
4 5 6 7 8
이 예제에서는 ShowArayElem 함수 내에서 외부에 선언된 배열에 접근하여 그 값을 출력하였다.
값의 출력 뿐만 아니라 값의 변경도 주소 값만 알면 가능하다~!
#include <stdio.h>
void ShowArayElem(int* param, int len)
{
int i;
for (i = 0; i < len; i++)
printf("%d ", param[i]);
printf("\n");
}
void AddArayElem(int* param, int len, int add)
{
int i;
for (i = 0; i < len; i++)
param[i] += add;
}
int main()
{
int arr[3] = { 1, 2, 3 };
AddArayElem(arr, sizeof(arr) / sizeof(int), 1);
ShowArayElem(arr, sizeof(arr) / sizeof(int));
AddArayElem(arr, sizeof(arr) / sizeof(int), 2);
ShowArayElem(arr, sizeof(arr) / sizeof(int));
AddArayElem(arr, sizeof(arr) / sizeof(int), 3);
ShowArayElem(arr, sizeof(arr) / sizeof(int));
return 0;
}
> 출력
2 3 4
4 5 6
7 8 9
배열의 주소 값만 안다면 어디서든 배열에 접근하여 저장된 값을 참조하고 변경할 수 있다.⭐
위 두 예제에서 함수에 전달되는 매개변수가 포인터 변수인 int * param
으로 선언되었다.
이를 int param []
으로 선언하는 것도 가능하다.
두 case는 동일한 선언이나 후자가 배열이 인자로 전달된다는 느낌을 더 강하게 주는 선언이다.
하지만 이 둘이 같은 선언으로 간주되는 경우 매개변수의 선언으로 제한된다.
예를 들어 main 함수에서 int * ptr = arr
는 가능하나 int ptr[] = arr
로 대체될 순 없다.
또한, 함수 내에서는 인자로 전달된 배열의 길이를 계산할 수 없다.
이는 매개변수로 배열을 전달할 때 배열 자체를 전달하는 것이 아닌 포인터 변수를 전달하기 때문이다.
따라서, 매개변수를 대상으로 sizeof 연산을 할 경우 배열의 크기는 반환되지 않고 포인터 변수의 크기가 반환된다.
배열의 크기나 길이정보도 인자로 함께 전달해야한다.
Call-by-value
와 Call-by-reference
는 함수의 호출 방식을 의미한다.
함수를 호출할 때 단순히 값을 전달하는 형태의 함수호출을 가리켜 Call-by-value
라 한다.
다음 예제에서 Call-by-value 방식을 확인해보자.
#include <stdio h>
void Swap(int n1, int n2) // 값만 전달받아 함수 내에서 계산.
{
int temp = n1;
n1 = n2;
n2 = temp;
printf("n1 n2: %d %d \n", n1, n2); // 함수 내에서는 값이 변했지만 main 함수에는 적용 x.
}
int main()
{
int num1 = 10;
int num2 = 20;
printf("num1 num2: %d %d \n", num1, num2);
Swap(num1, num2);
printf("Call by Value num1 num2: %d %d \n", num1, num2); // 값이 바뀌지 않음.
return 0;
}
> 출력
num1 num2: 10 20
n1 n2: 20 10
Call by Value num1 num2: 10 20
Swap 함수에서는 두 수의 값을 변경한다.
Swap 함수 이후에 오는 printf문에서 num1과 num2의 값이 변경되길 기대했다면 틀렸다.
n1과 n2는 저장된 값을 변경시키는 매개 변수일 뿐, num1과 num2에 저장된 값의 변경으로까지는 이어지지 않는다.
![]() | ![]() |
---|
메모리의 접근에 사용되는 주소 값을 전달하는 형태의 함수 호출을 가리켜 Call-by-reference
라 한다.
앞에서 본 예제에서 num1과 num2의 값을 변환하기 위해선 어떻게 하면 할 수 있을까?
Call-by-referece 방식을 이용해 num1과 num2의 주소값을 Swap함수로 전달해서 Swap함수 내에서 num1과 num2에 직접 접근하는 것이다.
#include <stdio h>
void swap(int* ptr1, int* ptr2) // 주소 값을 전달받아 계산.
{
int temp = *ptr1;
*ptr1 = *ptr2;
*ptr2 = temp; // 함수 밖, main 함수에도 값이 전달되어 계산 적용.
}
int main()
{
int num1 = 10;
int num2 = 20;
printf("num1 num2: %d %d \n", num1, num2);
swap(&num1, &num2);
printf("Call by Reference num1 num2: %d %d \n", num1, num2); // 값이 바뀜.
return 0;
}
> 출력
num1 num2: 10 20
n1 n2: 20 10
Call by Reference num1 num2: 20 10
위 함수는 변수의 주소 갑슬 인자로 받아서 해당 변수에 직접 접근하는 형태를 띤다.
swap 함수의 매개변수인 ptr1과 ptr2는 각각 num1과 num2를 가리키고,
*ptr1
은 num1을, *ptr2
는 num2를 의미하게 되어 결과적으로는 num1에 저장된 값과 num2에 저장된 값이 바뀐다.
이제 scanf 함수호출 시 &연산자를 넣는 이유를 알 수 있다!
scanf 함수호출이 완료되면 변수에는 값이 채워진다. 변수의 주소 값을 알아야 해당 변수에 값을 채워 넣을 수 있기 때문에 변수 앞에 &가 붙는 것이다.
배열이 변수로 들어가면 (문자열을 입력받으면) 배열은 그 자체로 배열의 주소 값이기 때문이다!
const 선언은 앞서 Chapter 5에서 소개가 된 적이 있다.
const 선언은 포인터 변수를 대상으로도 선언이 가능하다.
int main(void)
{
int num = 20;
const int * ptr = #
*ptr = 30; // 컴파일 에러
num = 30; // 컴파일 성공
}
const 선언이 포인터 변수 앞에 사용하게 되면 의미가 아래와 같다.
"포인터 변수 ptr을 이용해서 ptr이 가리키는 변수에 저장된 값을 변경하는 것을 허용하지 않는다."
따라서 *ptr이 가리켜서 변수에 저장된 값을 바꾸는 것은 되지 않고,
변수에 직접 저장된 값을 바꾸는 것은 가능하다.
const 선언이 포인터 변수명 바로 앞에 오는 경우도 있다.
이 때는 포인터 변수 ptr은 상수가 되어 한번 주소 값이 저장되면 그 값의 변경이 불가능해진다. 한번 가리키기 시작한 변수는 끝까지 가리켜야 한다는 뜻이다.
int main(void)
{
int num1 = 20;
int num2 = 30;
int * const ptr = &num1;
ptr = &num2; // 컴파일 에러
*ptr = 40; // 컴파일 성공
}
위 코드를 보면 ptr 포인터 변수가 num1의 주소 값을 가리키다가 num2의 주소 값으로 변경되어 가리키려하니 컴파일 에러가 발생한다.
다만, 그 주소 값에 저장되어 있는 값을 바꾸는 것은 허용이 된다.
포인터 변수를 대상으로 이 두 가지 형태의 const 선언을 동시에 할 수도 있다.
const int * const ptr = #
이렇게 선언이 되면 *ptr=20;
선언이나, ptr=&age
의 선언이 불가능해진다.
만약 원의 넓이를 계산하여 출력하는 함수가 있다.
원주율에 해당하는 값을 PI라는 변수에 넣어두었다고 생각해보자.
그런데 이 PI값은 변하면 안된다.
하지만 const 선언을 하지 않으면 이 PI 변수의 값을 마음대로 바꿀 수 있으므로 컴파일러가 이러한 문제점을 발견 못하고 오류를 발생시킬 수 있다.
이러한 변수에 대한 안정성을 확보하기 위해서 const 선언이 중요하다.
<Review>
이렇듯 포인터 변수와 함수에 대해 알아보았다.
확실히 C는 작게 하나하나 모든 경우에 대해 생각하고 초반에 만들었던 언어인만큼 세심한 부분이 많다는 것을 알 수 있다...
다음엔 재밌는 파트인 도전! 프로그래밍 2!다.🙋🏻♀️
알고리즘 문제도 매일 조금씩 풀고 있는데 더 열심히 정진해야겠다.🤗
모두 화이티잉~~👍