[C언어] 포인터와 함수에 대한 이해

김민정·2024년 8월 14일
0
post-thumbnail

Chapter 14. 포인터와 함수에 대한 이해

함수는 인자를 전달받도록 정의할 수 있다. (함수는 원래 인자 전달과 값의 반환이 가능)
배열 대상의 인자를 전달하는 원리를 알기 위해서는 인자 전달원리에 대한 확실한 이해가 필요하다.

14-1 "함수의 인자로 배열 전달하기"

인자전달의 기본방식

"함수호출 시 전달되는 인자의 값은 매개변수에 복사 된다."
함수가 호출되면 복사가 되기 때문에 전달되는 인자와 매개변수는 별개가 된다.⭐

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 연산을 할 경우 배열의 크기는 반환되지 않고 포인터 변수의 크기가 반환된다.
배열의 크기나 길이정보도 인자로 함께 전달해야한다.


14-2 "Call-by-value vs. Call-by-reference"

Call-by-valueCall-by-reference는 함수의 호출 방식을 의미한다.

Call-by-value: 값을 전달하는 형태의 함수 호출

함수를 호출할 때 단순히 값을 전달하는 형태의 함수호출을 가리켜 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에 저장된 값의 변경으로까지는 이어지지 않는다.

Swap 함수 내에서의 값의 교환 Swap 함수 내에서의 값의 교환은 외부에 영향을 주지 X

Call-by-reference: 주소 값을 전달하는 형태의 함수 호출

메모리의 접근에 사용되는 주소 값을 전달하는 형태의 함수 호출을 가리켜 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 함수호출 시 &연산자를 넣는 이유를 알 수 있다!
scanf 함수호출이 완료되면 변수에는 값이 채워진다. 변수의 주소 값을 알아야 해당 변수에 값을 채워 넣을 수 있기 때문에 변수 앞에 &가 붙는 것이다.
배열이 변수로 들어가면 (문자열을 입력받으면) 배열은 그 자체로 배열의 주소 값이기 때문이다!


14-3 "포인터 대상의 const 선언"

const 선언은 앞서 Chapter 5에서 소개가 된 적이 있다.
const 선언은 포인터 변수를 대상으로도 선언이 가능하다.

int main(void)
{
	int num = 20;
    const int * ptr = &num;
    *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 = &num;
이렇게 선언이 되면 *ptr=20;선언이나, ptr=&age의 선언이 불가능해진다.

const 선언이 갖는 의미

만약 원의 넓이를 계산하여 출력하는 함수가 있다.
원주율에 해당하는 값을 PI라는 변수에 넣어두었다고 생각해보자.
그런데 이 PI값은 변하면 안된다.
하지만 const 선언을 하지 않으면 이 PI 변수의 값을 마음대로 바꿀 수 있으므로 컴파일러가 이러한 문제점을 발견 못하고 오류를 발생시킬 수 있다.
이러한 변수에 대한 안정성을 확보하기 위해서 const 선언이 중요하다.


<Review>

이렇듯 포인터 변수와 함수에 대해 알아보았다.
확실히 C는 작게 하나하나 모든 경우에 대해 생각하고 초반에 만들었던 언어인만큼 세심한 부분이 많다는 것을 알 수 있다...
다음엔 재밌는 파트인 도전! 프로그래밍 2!다.🙋🏻‍♀️
알고리즘 문제도 매일 조금씩 풀고 있는데 더 열심히 정진해야겠다.🤗
모두 화이티잉~~👍

profile
백엔드 코린이😁

0개의 댓글

관련 채용 정보