모두의 코드
씹어먹는 C 언어 - <13 - 3. 마술 상자 함수 3 (function)>

이번 강좌에서는

에 대해서 배웁니다.

씹어먹는 C 언어

와.. 드디어, 함수만 세번째 강의입니다. 아마 이전 강좌에서 배운 내용들 중 어려운 것은 없으리라 생각됩니다. 물론, 이번 강좌의 내용도 이전까지의 내용을 잘 숙지 하셨더라면 무난하게 넘어갈 수 있으리라 생각됩니다.

 지난번 내용을 상기해보며

지난번 내용은 잘 기억하고 있는지요? 다시 한 번 요약해 보자면, "어떠한 함수가 특정한 타입의 변수/배열의 값을 바꾸려면 함수의 인자는 반드시 타입을 가리키는 포인터 형을 이용해야 한다!" 였습니다.사실, 이 문장이 이해가 잘 되지 않았던 분들이 있으리라 생각됩니다. 하지만, 이번 강좌를 보고 난다면 이 문장의 의미를 정확하게 파악할 수 있을 것입니다.

/* 눈 돌아가는 예제. 포인터가 가리키는 변수를 서로 바꾼다.  */
#include <stdio.h>

int pswap(int **pa, int **pb);
int main() {
  int a, b;
  int *pa, *pb;

  pa = &a;
  pb = &b;

  printf("pa 가 가리키는 변수의 주소값 : %x \n", pa);
  printf("pa 의 주소값 : %x \n \n", &pa);
  printf("pb 가 가리키는 변수의 주소값 : %x \n", pb);
  printf("pb 의 주소값 : %x \n", &pb);

  printf(" ------------- 호출 -------------- \n");
  pswap(&pa, &pb);
  printf(" ------------- 호출끝 -------------- \n");

  printf("pa 가 가리키는 변수의 주소값 : %x \n", pa);
  printf("pa 의 주소값 : %x \n \n", &pa);
  printf("pb 가 가리키는 변수의 주소값 : %x \n", pb);
  printf("pb 의 주소값 : %x \n", &pb);
  return 0;
}
int pswap(int **ppa, int **ppb) {
  int *temp = *ppa;

  printf("ppa 가 가리키는 변수의 주소값 : %x \n", ppa);
  printf("ppb 가 가리키는 변수의 주소값 : %x \n", ppb);

  *ppa = *ppb;
  *ppb = temp;

  return 0;
}

  성공적으로 컴파일 하면

여러분의 출력결과는 위 사진과 다를 수 있습니다.

일단, 붉은색으로 박스 친 부분을 잘 살펴보기 바랍니다. pa 가 가리키는 변수의 주소값은 (즉, pa 의 값이지요) 31FBCC 였습니다. 물론, 여러분이 실행했을 때 에는 결과가 다르게 나올 것입니다. (거의 99% 확률로 다르게 나옵니다) pb 가 가리키는 변수의 주소값은 31FBC0 이였습니다. 그런데 말이죠. pswap 함수를 호출하고 나니, pa 가 가리키는 변수의 주소값은 31FBC0 이 되고, pb 가 가리키는 변수의 주소값은 31FBCC 가 되었습니다. 즉, 두 포인터가 가리키는 변수가 서로 뒤바뀐 것이지요.

이 때, 우리는 이와 같은 함수를 만들기 위해서, 인자를 어떤 형식으로 취해야 될까요? 앞서 말했듯이, 특정한 타입의 변수의 값을 바꾸려면, 특정한 타입을 가리키는 포인터로 인자를 취해야 된다고 했습니다. 그런데, 이 예제의 경우, 특정한 타입은 int* 타입입니다. 그렇다면 int* 타입을 가리키는 포인터의 타입은? 음. 강좌를 잘 복습하였다면 int** 타입 이라고 말할 수 있겠지요. (잘 모르겠다면 12-3 강, 포인터는 영희이다!를 보세요)

  따라서, 우리는 위 이야기를 토대로 아래와 같이 함수를 정의하였습니다.

int pswap(int **ppa, int **ppb)

  상당히, 잘한 것이지요. 이제, 함수의 몸체를 봐봅시다.

int pswap(int **ppa, int **ppb) {
  int *temp = *ppa;

  printf("ppa 가 가리키는 변수의 주소값 : %x \n", ppa);
  printf("ppb 가 가리키는 변수의 주소값 : %x \n", ppb);

  *ppa = *ppb;
  *ppb = temp;

  return 0;
}

일단, int* 형의 temp 변수를 만들어서 *ppa 의 값을 저장하고 있습니다. 그런데, *ppa 의 값은 무엇일까요?

만일 우리가 위 예제 처럼 pswap 함수를 호출하였다고 하면, ppapa 를 가리키고 있고, ppbpb 를 가리키고 있겠지요. 따라서, *ppa 라 하면 pa 의 값을 이야기 합니다. 그런데, paint* 형이므로, pa 의 값을 보관하는 변수는 반드시 int* 이여야 되겠지요. 따라서, 우리는 int* 형의 temp 변수를 정의하였습니다. 그 아래의 내용은 이전에 만들어 보았던 swap 함수와 동일합니다.

printf("ppa 가 가리키는 변수의 주소값 : %x \n", ppa);
printf("ppb 가 가리키는 변수의 주소값 : %x \n", ppb);

그렇다면 우리는 위 두개의 printf 문장에서 어떤 결과가 출력될 지 예측 가능합니다. 위 예제에서 ppapa 를 가리키고 있으므로 ppa 의 값을 출력하면 pa 의 주소값이 나오고, ppb 도 마찬가지로 나오겠죠. 위 출력결과에서 실제로 같다는 것을 확인할 수 있습니다. 어때요. pswap 함수가 이해가 되나요?

위 과정을 그림으로 표현하면 아래와 같습니다.

그렇다면, 이번에는 이차원 배열을 인자로 받는 함수에 대해서 생각해 보도록 합시다.

/* 2 차원 배열의 각 원소를 1 씩 증가시키는 함수 */
#include <stdio.h>
/* 열의 개수가 2 개인 이차원 배열과, 총 행의 수를 인자로 받는다. */
int add1_element(int (*arr)[2], int row);
int main() {
  int arr[3][2];
  int i, j;

  for (i = 0; i < 3; i++) {
    for (j = 0; j < 2; j++) {
      scanf("%d", &arr[i][j]);
    }
  }

  add1_element(arr, 3);

  for (i = 0; i < 3; i++) {
    for (j = 0; j < 2; j++) {
      printf("arr[%d][%d] : %d \n", i, j, arr[i][j]);
    }
  }
  return 0;
}
int add1_element(int (*arr)[2], int row) {
  int i, j;
  for (i = 0; i < row; i++) {
    for (j = 0; j < 2; j++) {
      arr[i][j]++;
    }
  }

  return 0;
}

  성공적으로 컴파일 하였다면

역시 잘 실행되는군요. 일단, 함수의 정의부분 부터 살펴봅시다.

int add1_element(int (*arr)[2], int row) {
  int i, j;
  for (i = 0; i < row; i++) {
    for (j = 0; j < 2; j++) {
      arr[i][j]++;
    }
  }

  return 0;
}

이 함수는 인자를 두 개 받고 있는데 하나는 열의 개수가 2 개인 이차원 배열을 가리키는 포인터 이고, 하나는 함수의 행의 수를 받는 인자입니다.

for (i = 0; i < row; i++) {
  for (j = 0; j < 2; j++) {
    arr[i][j]++;
  }
}

우리는 row 를 통해 이 이차원배열의 행의 개수를 알 수 있고, 열의 개수는 이미 알고 있으므로 (배열 포인터에서) 각 원소를 1 씩 증가시키는 작업을 시행할 수 있게됩니다. 위와 같이 말이죠. 우리는 포인터를 잘 배워서 헷갈릴 문제는 없지만 많은 사람들에게 다음과 같이 인자를 받는것이 어렵게 느껴집니다.

int add1_element(int (*arr)[2], int row)

그래서, 오직 함수의 인자의 경우에서만 위 형태의 인자를 다음과 같이도 표현할 수 있습니다.

int add1_element(int arr[][2], int row)

  이는 오직 함수의 인자에서만 적용되는 것입니다. 만일

int parr[][3] = arr;

와 같은 문장을 이용했더라면 컴퓨터는 parr 을 '열의 개수가 3 개이고 행의 개수는 정해지지 않는 배열' 이라 생각해서 오류를 내게 됩니다. (만일 행의 개수를 생략했다면 배열을 정의시 초기화도 해주어야 되는데는 위는 그러지 않으므로) 암튼, 함수의 인자에서만 가능한 형태라는 것을 기억해 주시기 바랍니다.

덧붙여서 응용력을 살짝 이용하면 다차원 배열의 인자도 정의할 수 있습니다. 예를 들어서

int multi(int (*arr)[3][2][5]) {
  arr[1][1][1][1] = 1;
  return 0;
}

  혹은

int multi(int arr[][3][2][5]) {
  arr[1][1][1][1] = 1;
  return 0;
}

  로 하면 됩니다.

 상수인 인자

/* 상수를 인자로 받아들이기 */
#include <stdio.h>
int read_val(const int val);
int main() {
  int a;
  scanf("%d", &a);
  read_val(a);
  return 0;
}
int read_val(const int val) {
  val = 5;  // 허용되지 않는다.
  return 0;
}

컴파일 하게 되면 아래와 같은 오류를 만나게 됩니다.

 error C2166: l-value가 const 개체를 지정합니다.

흠.. 이건 우리가 이전에 상수의 값을 변경하려고 했었을 때 만났던 오류 인것 같습니다. 맞습니다. 우리가 valconst int 로 선언하였기 때문에 함수를 호출 할 때, val 의 값은 인자로 전달된 값으로 초기화 되고 결코 바뀌지 않습니다. 즉, vala 의 값으로 상수로 초기화 된 것입니다. 따라서, 함수 내부에서 val = 5 와 같이 val 의 값을 바꾸려 한다면 오류가 나겠지요. 왜냐하면 val 은 상수이니까요.

상수로 인자를 받아들이는 경우 대부분은 함수를 호출 해도 그 인자의 값이 바뀌지 않는 경우에 자주 사용합니다만, 자세한 내용은 나중에 좀더 다루도록 하겠습니다.

 함수 포인터

아마, '함수 포인터' 라는 말을 들었을 때는 조금 의아하는 감이 있지 않을까 합니다. 함수 포인터라니, 함수를 가리킨다는 것인가? 그럼, 함수가 메모리 상에 있다는 거야? 네. 맞습니다. 사실, 프로그램의 코드 자체가 메모리 상에 존재합니다. 우리는 이전에 컴파일러가 하는 작업이 바로 우리가 '인간에 친숙한 언어' 로 쓰여진 프로그램 코드를 '컴퓨터에 친숙한 언어, 즉 수 데이터들' 로 바꿔주어 실행 파일을 생성한다고 배웠습니다. 이렇게, 바뀐 실행 파일을 실행하게 되면 프로그램의 수 코드가 메모리 상에 올라가게 됩니다. 다시말해, 메모리 상에 함수의 코드가 들어간다는 것입니다. 이 때, 변수를 가리키는 포인터 처럼 함수 포인터는 메모리 상에 올라간 함수의 시작 주소를 가리키는 역할을 하게 됩니다.

그렇다면, 함수 포인터가 함수를 가리키기 위해서는 그 함수의 시작 주소값을 알아야 합니다. 그런데, 배열과 마찬가지로 함수의 이름이 바로 함수의 시작 주소값을 나타냅니다.

/* 함수 포인터 */
#include <stdio.h>

int max(int a, int b);
int main() {
  int a, b;
  int (*pmax)(int, int);
  pmax = max;

  scanf("%d %d", &a, &b);
  printf("max(a,b) : %d \n", max(a, b));
  printf("pmax(a,b) : %d \n", pmax(a, b));

  return 0;
}
int max(int a, int b) {
  if (a > b)
    return a;
  else
    return b;

  return 0;
}

성공적으로 컴파일 했다면

역시 우리가 예상했던 데로 잘 흘러가는 것 같습니다. 함수 포인터는 어떻게 정의하는지 살펴봅시다.

int (*pmax)(int, int);

일단, 위는 함수 포인터 pmax 의 정의 입니다. 위 정의를 보고 다음과 같은 사실을 알 수 있습니다. '이 함수 포인터 pmax 는 함수의 리턴값이 int 형이고, 인자 두 개가 각각 int 인 함수를 가리키는구나!'. 따라서, 우리는 pmax 함수 포인터로 특정한 함수를 가리킬 때, 그 함수는 반드시 pmax 의 정의와 일치해야 합니다. 함수 포인터의 일반적인 정의는 다음과 같습니다.

(함수의 리턴형) (*포인터 이름)(첫번째 인자 타입, 두번째 인자 타입,....)
// 만일 인자가 없다면 그냥 괄호 안을 비워두면 된다. 즉, int (*a)() 와 같이 하면 된다

이제 pmaxmax 를 가리키게 되는 부분을 봅시다.

pmax = max;

max 함수를 살펴보면 pmax 의 정의와 일치하므로, max 함수의 시작 주소값을 pmax 에 대입할 수 있게 됩니다. 이 때, 앞에서도 말했듯이 특정한 함수의 시작 주소값을 알려면 그냥 함수 이름을 넣어주면 됩니다. pmax = &max 와 같은 형식은 틀린 것입니다.

printf("max(a,b) : %d \n", max(a, b));
printf("pmax(a,b) : %d \n", pmax(a, b));

pmax 는 이제 max 함수를 가리키므로 pmax 를 통해 max 함수가 할 수 있었던 모든 작업들을 할 수 있게 됩니다. 이때도 역시 그냥 pmaxmax 처럼 이용하면 됩니다. 이는 배열에서

int arr[3];
int *p = arr;

arr[2];  // p[2] 와 정확히 일치
p[2];

와 같이 arr[2]p[2] 가 동일한 것과 같습니다. 아무튼 max(a,b) 를 하나 pmax(a,b) 를 하나 결과는 똑같이 나오게 됩니다.

/* 함수 포인터 */
#include <stdio.h>

int max(int a, int b);
int donothing(int c, int k);
int main() {
  int a, b;
  int (*pfunc)(int, int);
  pfunc = max;

  scanf("%d %d", &a, &b);
  printf("max(a,b) : %d \n", max(a, b));
  printf("pfunc(a,b) : %d \n", pfunc(a, b));

  pfunc = donothing;

  printf("donothing(1,1) : %d \n", donothing(1, 1));
  printf("pfunc(1,1) : %d \n", pfunc(1, 1));
  return 0;
}
int max(int a, int b) {
  if (a > b)
    return a;
  else
    return b;

  return 0;
}
int donothing(int c, int k) { return 1; }

  성공적으로 컴파일 했다면

일단, 우리는 이전의 예제와 동일한 형태의 함수 포인터 pfunc 을 정의하였습니다.

int (*pfunc)(int, int);

이는 '리턴형이 int 이고 두 개의 인자 각각의 포인터 형이 int 인 함수를 가리킵니다. 그런데, donothing 함수와 max 함수 모두 이 조건을 만족하고 있습니다. 즉, 이들은 인자의 변수들도 다루고 하는 일도 다르지만 리턴값이 int 로 같고 두 개의 인자 모두 int 이므로 pfunc 이 이 두개의 함수를 가리킬 수 있는 것입니다.

pfunc = max;

scanf("%d %d", &a, &b);
printf("max(a,b) : %d \n", max(a, b));
printf("pfunc(a,b) : %d \n", pfunc(a, b));

pfunc = donothing;

printf("donothing(1,1) : %d \n", donothing(1, 1));
printf("pfunc(1,1) : %d \n", pfunc(1, 1));

따라서, 위와 같이 했을 때 pfunc 이, 자기가 가리키는 함수의 역할을 제대로 하고 있다는 것을 알 수 있습니다. 그런데 말이죠. 함수 포인터를 만들 때, 인자의 형이 무엇인지 알기 힘든 경우가 종종 있습니다. 예를 들어 아래와 같은 함수의 원형을 봅시다.

int increase(int (*arr)[3], int row)

흠... 두 번째 인자의 형은 int 라는 것은 알겠는데 첫번째 인자의 형은 도대체 뭘까요? 사실, 간단합니다. 특정한 타입의 인자를 판별하는 일은 단순히변수의 이름만을 빼버리면 됩니다. 따라서, 첫번째 인자의 형은 int (*)[3] 입니다. 즉, increase 함수를 가리키는 함수 포인터의 원형은 아래와 같습니다.

int (*pfunc)(int (*)[3], int);

  간단하지요? 이것을 이전에 이차원 배열을 인자로 받았던 함수에 적용시켜 보면 정확히 작동한다는 것을 알 수 있습니다.

그럼, 이번 강좌는 여기에서 끝을 내도록 하겠습니다. 함수에 관한 강좌는 여기서 막을 내리게 됩니다. 사실, 아직까지도 C 언어를 배우면서 정말로 무언가 할 수 있는 실용적인 프로그램을 만들지 못해서 안타깝습니다. 그래서 이번에 생각해보기로 여러 재미있는 과제들을 내보도록 하죠.

생각해보기

문제 1

사용자로 부터 5 명의 학생의 수학, 국어, 영어 점수를 입력 받아서 평균이 가장 높은 사람 부터 평균이 가장 낮은 사람까지 정렬되어 출력하도록 하세요. 특히, 평균을 기준으로 평균 이상인 사람 옆에는 '합격', 아닌 사람은 '불합격' 을 출력하게 해보세요 (난이도 : 中上).

문제 2

유클리도 호제법을 이용해서 N 개의 수들의 최대공약수를 구하는 함수를 만들어보세요. 유클리드 호제법이 무엇인지 모르신다면, 인터넷 검색을 활용하는 것을 추천합니다. (댓글을 달아도 돼요) (난이도 : 中上)

문제 3

자기 자신을 호출하는 함수를 이용해서 1 부터 특정한 수까지의 곱을 구하는 프로그램을 만들어보세요. (난이도 : 下)

문제 4

계산기를 만들어보세요. 사용자가 1 을 누르면 +, 2 를 누르면 - 와 같은 방식으로 해서 만들면 됩니다. 물론 이전의 계산 결과는 계속 누적되어야 하고, 지우기 기능도 있어야 합니다. (물론 하나의 함수에 구현하는 것이 아니라 여러개의 함수로 분할해서 만들어야겠죠?)  (난이도 : 中)

문제 5

N 진법에서 M 진법으로 변환하는 프로그램을 만들어보세요. (난이도 : 中)

문제 6

에라토스테네스의 체를 이용해서 1 부터 N 까지의 소수를 구하는 프로그램을 만들어보세요. (난이도 : 中)

문제 7

1000 자리의 수들의 덧셈, 뺄셈, 곱셈, 나눗셈을 수행하는 프로그램을 만들어보세요. 나눗셈의 경우 소수 부분을 잘라버리세요. 물론, 소수 부도 1000 자리로 구현해도 됩니다. 1000 자리 수들의 연산 수행 시간은 1 초 미만이여야 합니다. (난이도 : 上)

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <<씹어먹는 C 언어 - <13 - 3. 마술 상자 함수 3 (function)>>> 입니다. 이번 강좌의모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요


 다음 강좌 보러가기
프로필 사진 없음
댓글에 글쓴이에게 큰 힘이 됩니다