[C] 함수
C, 함수
이번 챕터의 목표
숫자 5개를 입력받아 오름차순으로 정렬하기
(단, 숫자입력/정렬/출력은 따로 함수 만들기)
함수 기본
예를 들어 사용자로부터 자연수 n을 입력받고,
n+1 을 계산해서 출력하는 프로그램입니다.
#include <stdio.h>
int main(void)
{
int input = 0, result = 0;
printf("자연수 n을 입력하세요: ");
scanf("%d", &input);
putchar('\n');
result = input + 1;
printf("인풋값은 %d입니다.\n", input);
printf("결과값은 %d입니다.\n\n", result);
return 0;
}
/*
자연수 n을 입력하세요: 6
인풋값은 6입니다.
결과값은 7입니다.
*/
여기서 n+1을 구하는 프로세스를
따로 함수를 구현하여 만든다고 하면 다음과 같습니다.
#include <stdio.h>
int Calc(int n)
{
return n+1;
}
int main(void)
{
int input = 0, result = 0;
printf("자연수 n을 입력하세요: ");
scanf("%d", &input);
putchar('\n');
result = Calc(input);
printf("인풋값은 %d입니다.\n", input);
printf("결과값은 %d입니다.\n\n", result);
}
/*
자연수 n을 입력하세요: 76
인풋값은 76입니다.
결과값은 77입니다.
*/
Calc()이란 함수가 제가 직접 만든 함수입니다.
괄호안에 써져있는 대로 매개변수를 받아, 스코프 안에 있는 내용을 실행합니다.
여기선 int n을 받고, n+1을 리턴합니다.
함수명 앞에 써져있는 형식대로 리턴해야합니다.
여기선 CalcSum()앞에 int가 써져있으므로, return값은 int형이어야 하고,
리턴값인 n+1은 int형입니다.
그리고 지금까지 우리가 써왔던 main()도
사용자가 직접 작성하는, 사용자 정의 함수입니다.
c프로그램이 작동할 때, 제일 먼저 실행되는 건 main()함수입니다.
main()함수에서 input값을 입력받고, Calc(input)을 통해, 함수를 호출(실행)합니다.
Calc(input)이라는 말은 Calc()함수의 매개변수 n에 input값을 대입하라는 뜻입니다.
그러면 n+1 값이 리턴되고, 그 값은 result에 저장되게 됩니다.
참고로 함수의 선언과 정의를 분리할 수 있습니다.
예를 들어 위 코드의 Calc()함수를 선언과 정의로 분리한다면 다음과 같습니다.
#include <stdio.h>
// 함수의 선언
int Calc(int n);
int main(void)
{
int input = 0, result = 0;
printf("자연수 n을 입력하세요: ");
scanf("%d", &input);
putchar('\n');
result = Calc(input);
printf("인풋값은 %d입니다.\n", input);
printf("결과값은 %d입니다.\n\n", result);
}
// 함수의 정의
int Calc(int n)
{
return n+1;
}
/*
자연수 n을 입력하세요: 76
인풋값은 76입니다.
결과값은 77입니다.
*/
굳이 함수를 저렇게 분리 안해도 되는데,
코드만 많아지게 왜 분리하는지 이해가 안될수도 있습니다.
그에 대한 이유로는 다양할 수 있습니다.
예를 들어 실제로 개발을 하게 되면, 한두개가 아니라
아마 수없이 많은 함수를 만들어 사용할 것입니다.
이때 함수를 선언과 정의로 나누어 작성한다면,
다른 사람이 제가 작성한 코드를 봤을 때, 한눈에
어떤어떤 함수를 써서 이런 구조로 코드를 작성했구나라는 걸 알 수 있을 것입니다.
그렇다면 이제 함수를 사용하는 이유를 알아야합니다.
위의 n+1을 출력하는 첫번째 코드를 살펴보면
굳이 함수를 만들어 사용하지 않아도 충분히 기능을 구현할 수 있습니다.
함수를 사용하는 이유는 결국 다음 두가지로 압축됩니다.
- 코드 작성의 효율을 높히기 위해(재사용 코드 줄이기)
- 유지보수의 용이
정말 쓸데없는 프로그램이지만 설명을 위해 다음 예제를 살펴보겠습니다.
#include <stdio.h>
int main(void)
{
printf("Repeat code!\n");
printf("Repeat code!!\n");
printf("Repeat code!!!\n");
printf("Repeat code!!!!\n");
printf("Repeat code!!!!!\n\n");
printf("Cute Teemo\n\n");
printf("Repeat code!\n");
printf("Repeat code!!\n");
printf("Repeat code!!!\n");
printf("Repeat code!!!!\n");
printf("Repeat code!!!!!\n\n");
printf("Super Teemo\n\n");
printf("Repeat code!\n");
printf("Repeat code!!\n");
printf("Repeat code!!!\n");
printf("Repeat code!!!!\n");
printf("Repeat code!!!!!\n\n");
printf("Devil Teemo\n\n");
printf("Repeat code!\n");
printf("Repeat code!!\n");
printf("Repeat code!!!\n");
printf("Repeat code!!!!\n");
printf("Repeat code!!!!!\n\n");
return 0;
}
반복되는 printf() 구문이 많습니다.
이걸 함수를 이용하면 다음과 같이 훨씬 간결해집니다.
#include <stdio.h>
void PrintRepeat(void)
{
printf("Repeat code!\n");
printf("Repeat code!!\n");
printf("Repeat code!!!\n");
printf("Repeat code!!!!\n");
printf("Repeat code!!!!!\n\n");
}
int main(void)
{
PrintRepeat();
printf("Cute Teemo\n\n");
PrintRepeat();
printf("Super Teemo\n\n");
PrintRepeat();
printf("Devil Teemo\n\n");
PrintRepeat();
return 0;
}
이게 첫번째 이유였던 재사용 코드 줄이기 목적입니다.
이번엔 “Repeat code”라는 문구를
“Teemo Forever”로 바꾸려는 상황을 가정해보겠습니다.
첫번째 코드에서는 총 20개의 printf()를 변경해야합니다.
그런데 두번째 코드에서는 PrintRepeat()함수안에 5개의 printf()만 바꾸면 됩니다.
이게 두번째 이유였던 유지보수의 용이 목적입니다.
함수 설계 원칙
함수를 만들어 사용하는 법을 알았으면 그 다음 고민하야 할 것은
무엇을(어떤 기능을 수행하는) 함수로 만들어야 하는가입니다.
제일 처음에 n+1을 출력하는 예제를 보면,
n+1을 계산하는 함수를 따로 만들었습니다.
그런데 n+1 계산 정도는 너무 간단하기 때문에
굳이 함수로 안만들어도 될 것 같습니다.
이것에 대한 정답은 없습니다. 코드 설계에 대한 가이드도 여럿 있지만,
개발 회사마다, 개발자 개개인마다 선호하는 스타일이 다를 것이고,
만들고있는 프로그램의 성격에 따라서도 효율적인 설계가 다를 것입니다.
저는 제가 보고있는 책의 저자님께서 말씀하시는 규칙 두가지를 정리해보겠습니다.
UI와 기능의 분리
UI(User Interface)는 인간과 기계가 상호작용할 수 있도록 연결되는 형식을 말합니다.
계산기를 예로 들어보겠습니다.
우리는 계산기 화면(UI)를 통해 수와 연산자를 입력하고, 계산값을 돌려받습니다.
만약 3+4 를 화면을 통해 입력하면, 그걸 계산하는 과정(기능)은 사용자에게 보여지지 않고
결과값만 사용자 화면에 보이게 됩니다.
이런 계산기를 만든다고 하면 실제로 계산되는 부분, 즉 기능적인 부분은
따로 함수로 만들어서 분리하는게 좋다는 것입니다.
다음 코드를 살펴보겠습니다.
// UI와 기능의 분리1 (분리 안된 상황)
// 1부터 입력받은 자연수 n까지의 합 구하는 프로그램
#include <stdio.h>
int CalcSum(int n)
{
while (n <= 0)
{
printf("\n입력된 수는 자연수가 아닙니다. 다시 입력해주세요.\n");
printf("자연수 n 입력: ");
scanf("%d", &n);
}
return n * (n + 1) / 2;
}
int main(void)
{
int input = 0, result = 0;;
printf("1부터 n까지의 합 구하는 프로그램\n\n");
printf("자연수 n 입력: ");
scanf("%d", &input);
result = CalcSum(input);
printf("\n1부터 n까지의 합은 %d입니다.\n\n", result);
return 0;
}
/*
1부터 n까지의 합 구하는 프로그램
자연수 n 입력: 0
입력된 수는 자연수가 아닙니다. 다시 입력해주세요.
자연수 n 입력: -1
입력된 수는 자연수가 아닙니다. 다시 입력해주세요.
자연수 n 입력: 10
1부터 n까지의 합은 55입니다.
*/
이 코드는 자연수 n을 입력받고, 1부터 n까지의 합을 출력하는 프로그램입니다.
그리고 입력받은 수가 자연수가 아닐 경우에는
자연수 입력 때까지 계속 재입력을 요구합니다.
기능 부분이 CalcSum()이란 별도의 함수로 빠져있으므로,
UI와 기능이 분리됬다고 말할 수 있습니다.
하지만 완벽히 분리된 건 아닙니다.
자연수가 입력되지 않았을 때, 재입력을 요구하는 것도
사용자 화면에 보여지므로 UI라고 말할 수 있는데,
이 부분이 기능쪽(CalcSum() 함수)에 구현이 되어있기 때문입니다.
조금더 엄격하게 구분한 코드를 살펴보겠습니다.
// UI와 기능의 분리2 (분리형)
// 1부터 n까지의 합 구하기
#include <stdio.h>
int CalcSum(int n)
{
if (n <= 0) return 0;
return n * (n + 1) / 2;
}
int main(void)
{
int input = 0, result = 0;
printf("1부터 n까지의 합 구하는 프로그램\n\n");
printf("자연수 n입력: ");
scanf("%d", &input);
while ((result = CalcSum(input)) == 0)
{
printf("\n자연수를 입력하지 않았습니다. 자연수를 입력해주세요.\n");
printf("자연수 n입력: ");
scanf("%d", &input);
}
printf("1부터 n까지의 합은 %d입니다.\n\n", result);
return 0;
}
/*
1부터 n까지의 합 구하는 프로그램
자연수 n입력: -5
자연수를 입력하지 않았습니다. 자연수를 입력해주세요.
자연수 n입력: 0
자연수를 입력하지 않았습니다. 자연수를 입력해주세요.
자연수 n입력: 10
1부터 n까지의 합은 55입니다.
*/
이번 코드의 CalcSum() 함수를 살펴보면
정말 기능 자체만 수행하는 것을 볼 수 있습니다.
이렇게 UI와 기능을 명확히 구분해야, 나중에 유지보수가 편합니다.
다음은 일반적인 프로그램에서 사용되는 UI의 기본형 코드입니다.
기능부분은 없고 UI만 존재합니다.
// event loop 구현
#include <stdio.h>
int PrintMenu(void)
{
int input = 0;
printf("1.새로만들기 2.불러오기 3.설정 4.인쇄하기 0.종료하기: ");
scanf("%d", &input);
return input;
}
int main(void)
{
int menu = 0;
while ((menu = PrintMenu()) != 0)
{
switch (menu)
{
case 1:
puts("새로만들기 메뉴입니다.");
break;
case 2:
puts("불러오기 메뉴입니다.");
break;
case 3:
puts("설정 메뉴입니다.");
break;
case 4:
puts("인쇄하기 메뉴입니다.");
break;
default:
puts("잘못 입력 하셨습니다.");
}
printf("\n");
}
puts("\nProgram End\n\n");
return 0;
}
/*
1.새로만들기 2.불러오기 3.설정 4.인쇄하기 0.종료하기: 1
새로만들기 메뉴입니다.
1.새로만들기 2.불러오기 3.설정 4.인쇄하기 0.종료하기: 2
불러오기 메뉴입니다.
1.새로만들기 2.불러오기 3.설정 4.인쇄하기 0.종료하기: 3
설정 메뉴입니다.
1.새로만들기 2.불러오기 3.설정 4.인쇄하기 0.종료하기: 4
인쇄하기 메뉴입니다.
1.새로만들기 2.불러오기 3.설정 4.인쇄하기 0.종료하기: 5
잘못 입력 하셨습니다.
1.새로만들기 2.불러오기 3.설정 4.인쇄하기 0.종료하기: 0
Program End
*/
UI화면 코드를 작성할 때도, 메뉴 입력값을 처리하는 기능은
저렇게 따로 함수를 만들어 처리하는게 일반적입니다.
그리고 이러한 반복 구조를 event loop라 부릅니다.
재사용 가능한 단위 기능의 구현
이 부분은 위의 함수를 사용하는 이유에서 코드와 함께 설명했으므로
자세한 설명은 생략하겠습니다.
다만 재사용 단위를 설정하는 건 개개인의 역량이라 할 수 있습니다.
예를 들어 다음 코드를 살펴보겠습니다.
#include <stdio.h>
int MenuInput(void)
{
int n = 0;
printf("\n1~3 입력(0은 종료): ");
scanf("%d", &n);
return n;
}
int main(void)
{
int input = 0;
while ((input = MenuInput()) != 0)
{
if (input == 1)
{
printf("1번 출력문1\n");
printf("1번 출력문2\n");
}
else if (input == 2)
{
printf("1번 출력문1\n");
printf("1번 출력문2\n");
printf("2번 출력문1\n");
printf("2번 출력문2\n");
}
else if (input == 3)
{
printf("1번 출력문1\n");
printf("1번 출력문2\n");
printf("2번 출력문1\n");
printf("2번 출력문2\n");
printf("3번 출력문1\n");
printf("3번 출력문2\n");
}
else
{
printf("잘못된 입력입니다.\n");
}
}
printf("\nProgram End\n\n");
return 0;
}
/*
1~3 입력(0은 종료): 1
1번 출력문1
1번 출력문2
1~3 입력(0은 종료): 3
1번 출력문1
1번 출력문2
2번 출력문1
2번 출력문2
3번 출력문1
3번 출력문2
1~3 입력(0은 종료): 0
Program End
*/
입력받은 input값에 따라 출력문이 다르지만, 뭔가 반복되는 규칙이 보입니다.
이걸 함수를 만들어서 코드를 좀 줄여보겠습니다.
// 함수 설계(재사용 단위 기능 구현)
#include <stdio.h>
int MenuInput(void)
{
int n = 0;
printf("\n1~3 입력(0은 종료): ");
scanf("%d", &n);
return n;
}
void Print1(void)
{
printf("1번 출력문1\n");
printf("1번 출력문2\n");
}
void Print2(void)
{
printf("2번 출력문1\n");
printf("2번 출력문2\n");
}
void Print3(void)
{
printf("3번 출력문1\n");
printf("3번 출력문2\n");
}
int main(void)
{
int input = 0;
while ((input = MenuInput()) != 0)
{
if (input == 1)
{
Print1();
}
else if (input == 2)
{
Print1();
Print2();
}
else if (input == 3)
{
Print1();
Print2();
Print3();
}
else
{
printf("잘못된 입력입니다.\n");
}
}
printf("\nProgram End\n\n");
return 0;
}
Print1(), Print2(), Print3() 함수를 만들어서
코드를 조금 줄여봤습니다.
그런데 함수를 다음과 같이 만들면 더 짧은 코드를 만들 수 있습니다.
// 함수 설계(재사용 단위 기능 구현)
#include <stdio.h>
int MenuInput(void)
{
int n = 0;
printf("\n1~3 입력(0은 종료): ");
scanf("%d", &n);
return n;
}
void Print(int n)
{
printf("%d번 출력문1\n", n);
printf("%d번 출력문2\n", n);
}
int main(void)
{
int input = 0;
while ((input = MenuInput()) != 0)
{
if (input == 1)
{
Print(1);
}
else if (input == 2)
{
Print(1);
Print(2);
}
else if (input == 3)
{
Print(1);
Print(2);
Print(3);
}
else
{
printf("잘못된 입력입니다.\n");
}
}
printf("\nProgram End\n\n");
return 0;
}
전역변수
보통 함수내에서 선언하는 변수를 지역변수라 부릅니다.
이 지역변수는 해당 함수의 스코프 내에서만 유효합니다.
즉, main()함수에서 선언한 a라는 변수를, 외부함수 Calc()에서 사용할 수 없습니다.
그런데 전역변수(Global Variable)라는게 있습니다.
이름(Global)에서도 알 수 있듯이, 이 전역변수는
특정 스코프에 속하지 않고, 소스 파일 전체에서 접근(파일 스코프)할 수 있는 형태로 선언 및 정의된 변수입니다.
전역변수는 프로그램이 최초 실행될 때(main() 함수가 실행되기도 전)부터 존재하며,
프로그램이 끝날 때까지 사라지지 않고 유지됩니다.
예제 코드를 살펴보겠습니다.
#include <stdio.h>
// 전역변수 n 선언
int n = 77;
void Asdf(void)
{
// 함수 Asdf()의 지역변수 n 선언
int n = 7777;
printf("함수 Asdf 안의 지역변수 n: %d\n\n", n);
printf("지역변수 n값 변경: ");
scanf("%d", &n);
printf("\n지역변수 n 변경수 값: %d\n\n", n);
}
int main(void)
{
printf("초기 전역변수 n값: %d\n", n);
printf("전역변수 n값 변경: ");
scanf("%d", &n);
putchar('\n');
printf("변경 후 전역변수 n값: %d\n\n", n);
Asdf();
printf("Asdf 함수 실행 후 전역변수 n값: %d\n\n", n);
return 0;
}
/*
초기 전역변수 n값: 77
전역변수 n값 변경: 1004
변경 후 전역변수 n값: 1004
함수 Asdf 안의 지역변수 n: 7777
지역변수 n값 변경: 486
지역변수 n 변경수 값: 486
Asdf 함수 실행 후 전역변수 n값: 1004
*/
코드에서 볼 수 있듯이, 전역변수는 특정 스코프 안이 아닌 곳에 선언됩니다.
그리고 이 전역변수는 어디서든지 접근 가능합니다.
main()함수에서는 n이라는 변수를 선언하지 않았지만,
n이라는 변수에 접근하였고, 이는 전역변수 n에 접근한 것입니다.
그런데 Asdf()함수에서는 n이라는 변수를 따로 선언했습니다.
그랬더니 Asdf() 내에서 변수 n에 접근하면,
전역변수 n이 아니라 해당 함수에서 선언한 지역변수 n에 접근합니다.
이름이 똑같은 변수에 접근할 때 우선순위를 정리하면 다음과 같습니다.
- 가장 최근에 형성된 블록 스코프에 속한 지역변수
- 현재 블록 스코프의 외부를 감싸고 있는 상위 스코프(최대 함수 몸체까지 검색)
- 파일 스코프(전역변수)
저 우선순위를 숙지하고 똑같은 이름으로 전역변수와 지역변수를 사용할 순 있으나,
추천하지 않는 방법입니다. 아무래도 변수명이 같으면 실수할 확률이 커지기 때문입니다.
전역변수에는 g_n 처럼 앞에 g를 붙이든지 해서,
변수 이름만 봐도 전역변수임을 알 수 있게 하는게 바람직합니다.
그리고 어디서든 접근 가능하다는 전역변수의 특징때문에 편리하다고 느낄 수 있습니다.
하지만, 전역변수를 오남용하게 되면 각 함수들의 독립성이 보존되지 못하고,
서로 불필요하게 의존하는 관계가 될 수 있습니다.
전역변수는 꼭 필요한 경우에만 사용하는게 좋습니다.
함수의 매개변수가 배열일 경우
함수의 매개변수가 배열일 경우, 포인터를 매개변수로 설정하면 됩니다.
아직 포인터에 대해 살펴보지 않았으니 간단하게 예제만 살펴보겠습니다.
// 매개변수가 '배열'일 경우 포인터 매개변수로 받는다.
// 배열 입력받는 함수
#include <stdio.h>
void InitArr(int *arr, int size)
{
for (int i = 0; i < size; i++) {
printf("%d번째 요소 입력: ", i+1);
scanf("%d", &arr[i]);
}
printf("배열 입력 완료\n\n");
}
void PrintArr(int *arr, int size)
{
printf("배열 출력\n");
for (int i = 0; i < size; i++) printf("%d ", arr[i]);
printf("\n\n");
}
int main(void)
{
int arr[3] = { 0 };
int size = sizeof(arr) / sizeof(int);
// 배열 초기화
InitArr(arr, size);
// 배열 출력
PrintArr(arr, size);
return 0;
}
/*
1번째 요소 입력: 5
2번째 요소 입력: 12
3번째 요소 입력: 7
배열 입력 완료
배열 출력
5 12 7
*/
InitArr()와 PrintArr()에서 배열을 매개변수로 받을 때,
*arr 와 같이 포인터로 받고 있습니다.
Back to the Goal
숫자 5개를 입력받아 오름차순으로 정렬하기
(단, 숫자입력/정렬/출력은 따로 함수 만들기)
// 매개변수가 '배열'일 경우 포인터 매개변수로 받는다.
// 배열을 입력받아, 그 배열을 버블소트을 이용해 오름차순으로 정렬하기
#include <stdio.h>
void InitList(int *numList, int size)
{
printf("%d개의 수 입력\n", size);
for (int i = 0; i < size; i++)
{
printf("%d번째 수: ", i + 1);
scanf("%d", &numList[i]);
}
printf("수 입력 완료\n\n");
}
void SortList(int *numList, int size)
{
int temp = 0;
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - i - 1; j++) {
if (numList[j] > numList[j + 1]) {
temp = numList[j];
numList[j] = numList[j + 1];
numList[j + 1] = temp;
}
}
}
}
void PrintList(int *numList, int size)
{
for (int i = 0; i < size; i++) printf("%d ", numList[i]);
printf("\n\n");
}
int main(void)
{
int arr[5] = { 0 };
// 배열의 크기 계산
int size = sizeof(arr) / sizeof(int);
printf("%d개의 수를 입력받아 오름차순으로 정렬하는 프로그램\n\n", size);
// 배열 초기화
InitList(arr, size);
// 배열 오름차순으로 소트
SortList(arr, size);
// 배열 출력
PrintList(arr, size);
return 0;
}
/*
5개의 수를 입력받아 오름차순으로 정렬하는 프로그램
5개의 수 입력
1번째 수: 77
2번째 수: 1004
3번째 수: 7
4번째 수: -77
5번째 수: -7
수 입력 완료
-77 -7 7 77 1004
*/
main()함수에서 InitList(), SortList(), PrintList()를 호출할 때,
그냥 InitList(arr, 5)와 같이 그냥 크기를 숫자로 안넣고
size값을 또 계산해서 넣은 이유는 유지보수의 용이성 때문입니다.(제 기준…)
만약 배열의 크기를 7로 변경하려 한다면,
그냥 숫자로 썻다면, 바꿔야할 부분이 4군데 입니다.
arr[5] -> arr[7]
InitList(arr, 5) -> InitList(arr, 7)
SortList(arr, 5) -> SortList(arr, 7)
PrintList(arr, 5) -> PrintList(arr, 7)
그런데 위 코드와 같이 size를 계산해서 사용하면 바꿔야할 부분이 1군데 입니다.
arr[5] -> arr[7]