본문 바로가기
휴지통/C 프로그래밍

포인터와 함수에 대한 이해

by 신재권 2021. 1. 21.

1. 함수의 인자로 배열 전달하기

함수는 인자를 전달받도록 정의할 수 있다. 함수라는 이름이 붙은 이유도 인자의 전달과 값의 반환이 가능하기 때문이다.

-인자전달의 기본방식은 값의 복사이다.

"함수호출시 전달되는 인자의 값은 매개변수에 복사가 된다"

위의 문장에서 가장 중요한 단어는 "복사"이다. 즉 복사가 되는 것 뿐이기 때문에 함수가 호출되고 나면 전달되는 인자와 매개변수는 별개가 된다.

int SimpleFunc(int num){..}
int main(void){
int age =17;
SimpleFunc(age);   //age에 저장된 값이 매개변수 num에 복사됨
}

위 코드의 SimpleFunc 함수의 호출을 통해서 인자로 age를 전달하고 있다. 그러나 실제로 전달하는 것은 age가 아닌, age에 저장된 값이다.(즉 복사 된다) 그리고 그 값이 매개변수 num에 복사되는 것이다. 따라서 age와 num은 값을 주고 받은 사이일 뿐 그 이상은 아무런 관계가 아니다.

num의 값이 증가했다고 하여 변수 age의 값이 변화될리는 없다. num과 age는 별개의 변수이기 때문이다.

함수를 호출하면서 매개변수에 배열을 통째로 넘겨주는 방법은 존재하지 않는다.

"매개 변수로 배열을 선언할 수 없다"

배열을 통째로 넘겨받으려면 매개변수로 배열을 선언할 수 있어야 한다. 하지만 이것이 허용되지 않으니 배열을 통째로 넘기는 것은 불가능한 일이다. 대신에 함수 내에서 배열에 접근할 수 있도록 배열의 주소값을 전달하는 것은 가능하다.

-배열의 함수의 인자로 전달하는 방식

배열을 통째로 전달하는 것이 불가하다면, 배열의 주소값을 인자로 전달해서 이를 통해서 접근하도록 유도하는 방법을 생각할 수 있다.

int arr[3] = {1 ,2 ,3};

다음의 형태로 함수를 호출하면서 배열의 주소값을 전달할 수 있다.

SimpleFunc(arr); //SimpleFunc 함수를 호출하면서 배열 arr의 주소 값 전달

그렇다면 SimpleFunc함수의 매개변수는 어떻게 선언되어야 하는가?

int main(void){
int arr[3] = { 1, 2, 3};
int *ptr = arr;   //배열 이름 arr은 int형 포인터
}

따라서 SimpleFunce함수의 매개변수는 다음과 같이 int형 포인터 변수로 선언되어야 한다.

void SimpleFunc(int *param){ .. }

그럼 매개변수 param을 이용해서 배열에는 어떻게 접근해야하는가?

포인터 변수를 이용해도 배열의 형태로 접근이 가능하니 , 다음과 같이 접근이 가능하다.

printf("%d %d" , param[1], param[2]); //두번째, 세번째 요소 출력

#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(void){
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;
}

위 예제에서는 ShowArayElem 함수 내에서 외부에 선언된(main함수 내에 선언된) 배열에 접근하여 그 값을 출력하였다.

주소값만 알면 해당 메모리 공간에 얼마든지 접근이 가능하기 때문에 값의 변경도 가능하다.

배열의 주소값만 안다면 어디서든 배열에 접근하여 저장된 값을 참조하고 변경할 수 있다.

-배열을 함수의 인자로 전달받는 함수의 또 다른 선언

void a(int *param, int len) { ...};

void b(int *param, int len, int add) { .. };

두 함수에는 int형 배열의 주소 값을 인자로 전달받을 수 있도록 int형 포인터 변수가 선언되어 있다.

그런데 이를 대신하여 다음과 같이 선언하는 것도 가능하다.

void a(int param[], int len) { ...};

void b(int param[], int len, int add) { .. };

즉 int param[]과 int*param은 완전히 동일한 선언이다. 일반적으로 배열의 주소 값이 인자로 전달될 때에는 int paramp[]형태의 선언을 주로 많이 사용한다. 하지만 이 둘이 같은 선언으로 간주되는 것은 매개변수의 선언으로 제한된다.

int *ptr =arr; // int ptr[] = arr; 로 대체 불가능

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

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

함수를 호출할 때 단순히 값을 전달하는 형태의 함수 호출을 가리켜 Call -by -value라 하고 , 메모리의 접근에 사용되는 주소 값을 전달하는 형태의 함수호출을 가리켜 Call-by-reference라 한다. 즉, Call-by-value와 call-by-reference를 구분하는 기준은 함수의 인자로 전달되는 대상에 있다.

void a(int *param, int len) // call by reference함수이다.

이 함수는 첫번째 인자로 배열의 주소 값을 전달받도록 정의되었으니, 이 함수의 호출 형태는 call by reference이다.

예를 들어 두개의 인자를 전달받아 두 인자 위치를 바꾸는 함수가 있을 때, 메인함수에는 따로 두개의 인자가 선언될 것이다. 하지만 앞서 말했듯이 메인함수와 함수의 매개변수들은 별개이다.

만약 void Swab(int a, int b) ; 이런식으로 선언했으면 함수내에서는 값의 변경이 되지만 , 메인함수로 넘어가면 적용이 안되기때문에 값을 바꾸지 못한다. 즉 Swab함수 내 매개변수 사이에만 값의 반환이 일어난 것이다.

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

위와 같은 예로 void Swab 함수를 선언하는데 이번에는 주소값을 전달할 수 있도록 할 것이다.

void Swab(int *a, int *b); 이런식으로 선언시 함수 내에서 메인함수에 있는 인자의 주소값을 받아 함수 내에서 직접접근이 가능하도록 된다.

-이제는 scanf 함수호출시 &연산자를 붙이는 이유를 알 수 있다.

scanf 함수 호출시 &연산자를 붙이는 이유를 알 수 있을 것이다.

int main(void){
int n;
scanf("%d", &n);    //변수 num의 주소값을 scanf 함수에 전달
}

위의 scanf함수 호출이 완료되면 변수 n에는 값이 채워진다. 즉 프로그램 사용자로부터 값을 입력 받아서 변수 n에 그 값을 채우는 일을 scanf함수가 하는 것이다. 그리고 이를 위해서 scanf함수는 변수 n의 주소값을 알아야 한다. 그래야 변수 n에 접근해서 값을 채워넣을 수 있기 때문이다.

그래서 scanf함수 호출시에 변수 n의 주소값을 전달하는 것이다.

scanf함수의 호출도 call by reference 형태의 함수호출에 해당한다.

그렇다면 문자열을 입력받을 때에는 왜 &연산자를 붙이지 않는가?

int main(void){
char str[30];
scanf("%s", str);    //scanf("%S", &str); 는 잘못된 문장이다.
}

이전에 보였듯이 문자열은 같은 방식으로 입력받는다. 그리고 문자열이 저장될 배열의 이름 str의 앞에는 & 연산자를 붙이지 않는다.

그 이유는 str은 그 자체로 배열의 주소 값이다. 그냥 str을 전달하면 배열의 주소값이 전달된다. 그래서 &연산자를 붙일 필요가 없다.

3. 포인터 대상의 const 선언

-포인터 변수가 참조하는 대상의 변경을 허용하지 않는 const선언

const는 변수를 상수화 하는 목적으로도 사용가능하지만 포인터 변수를 대상으로도 const선언을 할 수 있다.

const의 선언 위치가 가장 중요하다.

const int * ptr = &n;

이렇게 const가 맨 앞부분에 선언되면

"포인터 변수 ptr을 이용해서 ptr이 가리키는 변수에 저장된 값을 변경하는 것을 허용하지 않는다" 라는 뜻이다.

즉 *ptr =30 ; //n에 저장되어 있는 값을 30으로 변경시도하였지만 컴파일 에러 발생 , 값 변경 불가

그렇다고 해서 포인터 변수 ptr이 가리키는 변수 num이 상수화 되는 것은 아니다. 따라서 다음과 같이 변수 n에 저장된 값을 변경하는 것은 허용이 된다. num =40;

이렇듯 위의 const 선언은 값을 변경하는 방법에 제한을 두는 것이지 , 무엇인가를 상수로 만드느 ㄴ선언은 안디ㅏ.

-포인터 변수의 상수화

int *const ptr =&n; //이렇게 const선언은 포인터 변수의 이름 앞에도 올 수 있다.

이렇게 되면 포인터 변수 ptr은 상수가 된다. 포인터 변수 ptr이 상수라는 뜻은 한번 주소값이 저장되면 그 값이 변경이 불가능하다는 뜻이며, 이는 한번 가리키기 시작한 변수를 끝까지 가리켜야 한다.

ptr = &n2; //컴파일 에러 (가리키는 곳 변경 불가)

*ptr= 40; //컴파일 성공 (*ptr이 가리키는 n의 값을 변경성공)

위의 코드를 보면 ptr 이 n을 가리키고 있는데 , 가리키는 대상을 n2로 바꿀라는 연산을 진행하고 있따. 하지만 ptr은 상수 이기 때문에 이 부분에서 컴파일 에러가 발생한다. 물론 ptr은 상수일 뿐이니, 가리키는 대상의 저장된 값을 변경하는 연산은 문제가 되지 않는다.

다음과 같이 하나의 포인터 변수를 대상으로 이 두가지 형태의 const 선언을 동시에 할 수도 있다.

const int *const ptr = &num;

이렇게 선언이되면 const선언으로 인해 *ptr=? ; 과 ptr = &? ; 의 연산이 불가능해진다. 즉 어떠한 것도 변경이 불가하다.

-const선언이 갖는 의미

const선언이 특별한 기능을 제공하는 것이 아니기 때문에 그 중요성을 인식하지 못하는 경우가 많다.

const는 C++에서 존재하던 키워드인데 C언어의 일부가 되었다. 그만큼 중요하고 유용하다는 뜻이다.

const 선언을 많이 하면 그 만큼 프로그램 코드의 안전성은 높아진다.

실수로 값을 변경을 시도했을시 컴파일러가 이러한 문제점을 발견하지 못한다.

그래서 const를 선언하면 실수로 값을 변경을 했을 시 컴파일 과정중 에러가 발생하기 때문에 실수를 알수 있다.

'휴지통 > C 프로그래밍' 카테고리의 다른 글

포인터의 포인터  (0) 2021.01.21
다차원 배열  (0) 2021.01.21
포인터와 배열  (0) 2021.01.21
포인터의 이해  (0) 2021.01.21
1차원 배열  (0) 2021.01.21