
지금까지 사용했던 printf 함수, scanf 함수 등은 C 언어에서 자체적으로 제공하는 수많은 표준함수 중 일부다. 표준함수의 원형은 헤더 파일에 정의되어 있으며, 실체는 라이브러리 파일에 수록되어 있다. 따라서 표준함수를 사용하려면 헤더 파일을 #include해야 한다.
✏️ 원형(prototype, 프로토타입)
원래의 형태를 뜻하며, 무언가를 만드는 과정에서 시험용으로 미리 제작해보는 '시제품'이라는 의미도 가지고 있다.
함수에서는 기본적인 중요 정보만 미리 간단히 적어놓은 것이 원형이다.
| 헤더 파일 | 선언된 함수 | 함수 예 |
|---|---|---|
| stdio.h | 입출력함수 파일 관련 함수 | printf(), scanf(), getchar(), putchar(), ... fopen(), fclose(), fprintf(), ... |
| conio.h | 콘솔 입출력함수 | putch(), cputs(), cprintf(), getch(), getche(), cscanf(), ... |
| string.h | 문자열 처리 함수 | strcat(), strcmp(), strcpy(), strlen(), strncat(), strncmp(), strncpy(), ... |
| math.h | 수학함수 | sqrt(), sin(), cos(), tan(), log(), exp(), pow(), abs(), asin(), acos(), atan(), cosh(), ... |
| ctype.h | 문자 형태 판별 함수 문자 변환 함수 | isalpha(), isdigit(), islower(), ... tolower(), toupper() |
| stdlib.h | 수치 변환 함수 난수 관련 함수 정렬/검색 함수 | atoi(), itoa() rand(), srand() qsort(), lfind() |
지금껏 해왔던 것은 main 함수라는 하나의 블럭 안에서 표준함수들을 이용하여 프로그램을 만드는 형태였다. 그러나 한 블럭의 길이가 너무 길어지면 복잡하고 이해하기 어려워지게 된다. 이 방법보다 전체 프로그램을 짧은 길위의 단위 프로그램으로 나누는 편이 훨씬 가독성도 좋고 편리하다. 이 단위 프로그램을 사용자가 함수로 정의해서 사용할 수 있다.
함수가 수행하는 일을 기술하는 것을 "함수를 정의한다"고 표현한다. 머리(헤더)부분과 몸체(바디)부분으로 나눌 수 있다.
함수 헤더 : 반환자료형 함수명 (자료형 매개변수1, 자료형 매개변수2, ...)
함수 바디 : 헤더 {
중괄호 안 내용 실행
}
함수가 수행하는 작업에 대한 구체적인 코드가 중괄호 안에 기술된다.
이를 통해 지금까지 써왔던 main 함수도 사실 위의 형태에 맞게 정의한 것임을 알 수 있다. 다만 표준함수나 사용자 정의 함수와 다른 점은 main 함수는 프로그램의 시작과 종료를 의미한다는 것으로, 프로그램에 반드시 하나가 존재해야 한다.
함수의 기능을 사용하는 것이 호출이다. 방법은 함수명(전달인자1, 전달인자2); 의 형식으로 작성하는 것으로, 괄호 안에는 매개변수의 형과 개수에 맞춰서 전달인자 값을 넣어야 한다(매개변수가 없을 때는 빈 괄호만 작성).
✏️ 전달인자, 인수(argument)
함수가 필요로 하는 값이다. 함수 호출 문장의 전달인자는 함수의 매개변수에 전달, 저장된다.
전달인자를 실 매개변수(actual parameters),
함수의 매개변수를 형식 매개변수(formal parameters)라고도 한다.
⌨️ 예제 1
// 게임 속에서 아이템을 구매하는 프로그램
#include <stdio.h>
int main() {
int itemCnt = 0; // 현재 보유중인 아이템 개수(0개에서 시작)
int money = 100; // 현재 보유중인 잔액
// 아이템 구매
itemCnt++; // 아이템 소지 개수 1 증가
money -= 10; // 잔액 감소
printf("아이템을 구매했습니다.\n");
printf(" 아이템 개수 : %d\n", itemCnt);
printf(" 잔액 %d\n", money);
/* 중략 */
// 아이템 구매
itemCnt++; // 아이템 소지 개수 1 증가
money -= 10; // 잔액 감소
printf("아이템을 구매했습니다.\n");
printf(" 아이템 개수 : %d\n", itemCnt);
printf(" 잔액 %d\n", money);
}
🖥️ 실행 결과
아이템을 구매했습니다.
아이템 개수 : 1
잔액 90
아이템을 구매했습니다.
아이템 개수 : 2
잔액 80
이렇게 아이템 구매 코드 5줄을 매번 살 때마다 적어야 한다면 너무 길어질 뿐만 아니라 나중에 가격을 수정할 경우 모든 붙여넣었던 곳을 체크해서 직접 바꿔줘야 한다. 이 코드를 묶어서 하나의 함수로 정의하면 더욱 간단해지고, 필요할 때마다 재사용하기 용이하다.
🛠️ 코드 수정
#include <stdio.h>
/* 지역변수 itemCnt와 money를 전역변수로 변경
(main 함수 안의 변수는 buyItem 함수에서 인식 못하므로 밖으로 뺀 것) */
int itemCnt = 0;
int money = 100;
int cost; // 아이템 가격을 전역변수로 선언
void buyItem() { // buyItem 함수 선언(이름만 봐도 역할을 알 수 있도록 함수명 짓기)
itemCnt++;
money -= cost; // 아이템 구매시 감소하는 잔액을 cost로 설정
printf("아이템을 구매했습니다.\n");
printf(" 아이템 개수 : %d\n", itemCnt);
printf(" 잔액 %d\n", money);
}
int main() {
cost = 30; // 아이템 가격을 30으로 설정
buyItem(); /* main 함수 밖에 정의된 buyItem 함수를 호출
아이템 가격이 30일 때 구매 */
/* 중략 */
cost = 50; // 아이템 가격을 50으로 변경
buyItem(); // 아이템 가격이 50인 상태에서 호출되어 구매
}
함수와 전역변수를 사용해서 구매를 간편하게 할 뿐만 아니라 가격도 그때 그때 원하는 대로 조정할 수 있게 됐다.
⌨️ 예제 2
// 예제 1에 매개변수를 사용하여 수정한 코드
#include <stdio.h>
int itemCnt = 0;
int money = 100;
void buyItem(int cost, int cnt) { // 괄호 안에 매개변수 2개 선언(쉼표로 구분하고 세미콜론 안 씀)
itemCnt += cnt; // 소지한 아이템 개수가 cnt만큼 증가
money -= cost * cnt; // "아이템의 가격 × 구매한 아이템 개수"를 잔액에서 뺌
printf("아이템을 구매했습니다.\n");
printf(" 아이템 개수 : %d\n", itemCnt);
printf(" 잔액 %d\n", money);
}
int main() {
buyItem(20, 2); // 가격이 20인 아이템을 2개 구매
/* 중략 */
buyItem(10, 3); // 가격이 10인 아이템을 3개 구매
}
🖥️ 실행 결과
아이템을 구매했습니다.
아이템 개수 : 2
잔액 60
아이템을 구매했습니다.
아이템 개수 : 5
잔액 30
매개변수도 지역변수와 마찬가지로 해당 함수 내에서만 사용 가능(cost 자체를 main 함수에서 사용 불가능)
⌨️ 예제 3
#include <stdio.h>
int main() {
int a;
a = printf("aaa\n");
printf("%d\n", a);
}
🖥️ 실행 결과
aaa
4
printf 함수가 출력 기능 뿐 아니라, 함수 자체가 하나의 값이 될 수 있음을 볼 수 있다.
출력된 문자의 개수 4가 a에 대입됐다('a' 3개 + '\n' 1개).
⌨️ 예제 4
// 함수의 반환
#include <stdio.h>
int plus(int a, int b) {
return a + b; // a + b의 값을 반환
}
int main() {
int sum = plus(3, 5); // plus 호출 → 8 반환 → plus 값이 8이 됨 → sum = 8
printf("%d\n", sum);
return 0; // 프로그램 종료
}
🖥️ 실행 결과
8
함수에서는 void형이 아닌 이상, 리턴값을 꼭 가져야 한다. 하지만 특별히 main 함수는 끝까지 실행했는데도 리턴값이 명시되지 않았을 경우, 자동으로 0을 반환한다. 0인 이유는 일반적으로 return 값이 0일 경우, 시스템이 이 프로그램은 에러 없이 정상 종료 되었다고 판단하기 때문이다. 때문에 main 함수 맨 마지막에는 return 0;을 적어줘도, 적어주지 않아도 무방하다.
⌨️ 예제 5
#include <stdio.h>
int noMeaning() { // 함수 선언
printf("first\n");
return 1;
printf("second\n");
return 2;
}
int main() {
int a;
a = noMeaning(); // 함수 호출
printf("반환된 값 : %d\n", a);
}
🖥️ 실행 결과
first
반환된 값 : 1
값 1과 2를 반환하려 했으나 1만 반환됐다. 이는 함수 실행 도중 return을 만나는 순간 즉시 종료하고 함수값을 반환해버리기 때문이다.
⌨️ 예제 6
// 예제 2에 return문을 더해서 수정
#include <stdio.h>
int itemCnt = 0;
int money = 100;
int buyItem(int cost, int cnt) { // return값 받기 위해 반환 자료형을 int로 변경
if(money <= cost * cnt) { // 구매 실패를 먼저 조건문으로 설정
printf("잔엑이 부족합니다.\n");
return -1; // 구매 실패시 -1 반환하고 즉시 함수 종료
}
// 잔액이 충분한 경우 return -1;이 실행 안 되서 여기부터 실행됨 → else문 쓸 필요 없음
itemCnt += cnt;
money -= cost * cnt;
printf("아이템을 구매했습니다.\n");
printf(" 아이템 개수 : %d\n", itemCnt);
printf(" 잔액 %d\n", money);
return 0; // 구매 성공 시 0을 반환
}
int main() {
int result;
result = buyItem(200, 2); // result에 0 또는 -1이 저장됨
if(result == -1) { // 저장했던 return값을 사용
printf("수량을 조정하시거나 다음에 이용해 주세요.\n");
}
/* 중략 */
buyItem(10, 3);
}
🖥️ 실행 결과
잔엑이 부족합니다.
수량을 조정하시거나 다음에 이용해 주세요.
아이템을 구매했습니다.
아이템 개수 : 3
잔액 70
예제 2의 코드대로 할 경우, 만약 소지한 금액보다 아이템 가격이 비싸도 잔액이 음수가 될 뿐 여전히 구매는 가능한 문제가 있기 때문에 return문으로 보완했다.
⌨️ 예제 7
// 정수 하나를 매개변수로 받아서 그 수가 짝수이면 0, 홀수이면 1을 반환하는 함수
#include <stdio.h>
int parity(int n) {
return (n % 2 + 2) % 2;
}
int main() {
printf("%d\n", parity(5));
printf("%d\n", parity(-3));
printf("%d\n", parity(6));
}
🖥️ 실행 결과
1
1
0
if(n % 2 == 0)문을 사용해도 되지만, "n % 2"의 값 자체를 return하는 것으로 간단히 할 수 있다.
다만 컴퓨터는 "음수 % 양수 = 음수"로 표현하기 때문에, n이 음수 홀수일 경우 -1이라는 결과가 나온다. 때문에 "n % 2"에 2를 더해서 양수로 만든 후, 그 값을 2로 나눈 나머지를 구하는 식으로 변경한다.
⌨️ 예제 8
// 5개의 슬롯에 각각 저장된 아이템들의 누적 사용 횟수를 출력하는 프로그램
#include <stdio.h>
int useCnt[5] = {0}; // 슬롯 5개(길이 5짜리 배열)의 사용 횟수를 0으로 초기화
void useItem(int); // 함수 useItem의 원형 선언
int main() {
useItem(3);
useItem(0);
useItem(1);
useItem(3);
useItem(2);
for(int i = 0; i < 5; i++) {
printf("슬롯%d의 아이템을 %d번 사용했습니다.\n", i, useCnt[i]);
}
}
void useItem(int itemNum) { // 슬롯의 번호를 매개변수로 받는 함수 정의
useCnt[itemNum]++;
}
🖥️ 실행 결과
슬롯0의 아이템을 1번 사용했습니다.
슬롯1의 아이템을 1번 사용했습니다.
슬롯2의 아이템을 1번 사용했습니다.
슬롯3의 아이템을 2번 사용했습니다.
슬롯4의 아이템을 0번 사용했습니다.
useCnt[5] = {0};
| useCnt[0] | useCnt[1] | useCnt[2] | useCnt[3] | useCnt[4] |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
인수(실 매개변수)의 값을 형식 매개변수에 그대로 복사하는 방법이다.
⌨️ 예제 9
// call-by-value로 두 변수의 값을 서로 교환하기
#include <stdio.h>
void swap(int x, int y) { // main 함수에서 쓰이는 변수 a, b와 안 겹치는 변수명 짓기
int tmp = x; // a 값을 tmp에 임시로 저장해 두기
x = y; // b 값을 a에 대입
y = tmp; // tmp에 저장해뒀던 원래 a 값을 b에 대입
printf("swap 함수 내부 : x = %d, y = %d\n", x, y);
}
int main() {
int a, b;
scanf("%d%d", &a, &b);
printf("swap 함수 실행 전 : a = %d, b = %d\n", a, b);
swap(a, b); // 형식 매개변수에 각각 a, b의 값을 넘겨줌
printf("swap 함수 실행 후 : a = %d, b = %d\n", a, b);
}
🖥️ 실행 결과
2 3 [Enter↵]
swap 함수 실행 전 : a = 2, b = 3
swap 함수 내부 : x = 3, y = 2
swap 함수 실행 후 : a = 2, b = 3
변수 a, b의 값을 넘겨받은 변수 x, y의 값은 swap 함수 내에서 서로 바뀌었지만, main 함수 안의 a, b는 별개의 것이기 때문에 값이 그대로다(복사본을 아무리 수정해도 원본에는 영향이 가지 않는 것과 같음).
인수(실 매개변수)의 주소값을 형식 매개변수에 전달하는 방법이다. 값에 의한 자료 전달 방법과 달리 실 매개변수와 형식 매개변수를 포인터 변수로 선언해야 한다.
⌨️ 예제 10
// call-by-reference로 두 변수의 값을 서로 교환하기
#include <stdio.h>
void swap(int *x, int *y) { // 주소값을 저장하는 포인터 변수로 선언
int tmp = *x; // *x는 a의 시작 주소를 저장 → tmp에 a 값 대입
*x = *y; // a = b와 같아짐
*y = tmp; // *y는 b의 시작 주소를 저장 → tmp에 b 값 대입
printf("swap 함수 내부 : x = %d, y = %d\n", *x, *y);
}
int main() {
int a, b;
scanf("%d%d", &a, &b);
printf("swap 함수 실행 전 : a = %d, b = %d\n", a, b);
swap(&a, &b); // 값이 아니라 주소값을 전달하기 위해 & 붙임
printf("swap 함수 실행 후 : a = %d, b = %d\n", a, b);
}
🖥️ 실행 결과
2 3 [Enter↵]
swap 함수 실행 전 : a = 2, b = 3
swap 함수 내부 : x = 3, y = 2
swap 함수 실행 후 : a = 3, b = 2
함수는 사용되기 전 먼저 선언되어야 하기 때문에, main 함수 앞에서 정의가 되어 있어야 비로소 main 함수 안에서 호출할 수 있다. 정의할 함수가 많을 경우에는 호출 순서가 꼬이지 않게 순서도 잘 맞춰야 한다.
그러나 main 함수를 프로그램 맨 앞부분에 두는 경우, 프로그램을 여러 파일로 나누어 작성하는 경우, 서로가 서로를 호출하는 함수들이 있을 경우 순서대로 함수를 선언하는 것이 불가능하다.
이럴 때는 원형(프로토타입)을 함수 사용 전에 선언하여 호출될 함수의 모양을 미리 알려주면 된다. 원형을 선언하기 위해선 함수 정의의 헤더 부분에 세미콜론(;)만 붙여주면 된다. 일반적으로 함수 원형은 main 함수 정의 이전에 두고 함수 정의는 main 함수 뒤에 둔다.
⌨️ 예제 11
// 걷고 도는 동작을 하는 로봇 프로그램을 간략화 한 코드
#include <stdio.h>
// 2. 함수 원형 선언
void walk(int distance); // 함수 원형을 통해 함수의 기본적인 정보 파악
void drawSquare(); // 함수 원형끼리 순서 바뀌어도 됨
void rotate(int); // 매개변수명 생략(매개변수명은 함수 원형에 영향없음)
int main() {
// 3. 함수 호출
drawSquare();
}
// 1. 함수 정의
void walk(int distance) { // 걷기
printf("%dcm를 걸었습니다.\n", distance);
}
void rotate(int angle) { // 돌기
printf("%d도 회전했습니다.\n", angle);
}
void drawSquare() { // 정사각형을 그리며 걷기(걷기와 돌기의 조합)
for(int i = 0; i < 4; i++) {
walk(10);
rotate(90);
}
}
🖥️ 실행 결과
10cm를 걸었습니다.
90도 회전했습니다.
10cm를 걸었습니다.
90도 회전했습니다.
10cm를 걸었습니다.
90도 회전했습니다.
10cm를 걸었습니다.
90도 회전했습니다.
재귀(recursion)함수는 어떤 함수의 정의 안에 해당 함수 자기자신이 호출된 함수다. 액자 안에 또 액자가 들어가는 것이 무한히 반복되는 형태다.
⌨️ 예제 12
#include <stdio.h>
void rec(int n) {
printf("n = %d\n", n);
rec(n + 1); // 함수 rec가 끝나기 전에 rec가 또 실행됨
}
int main() {
rec(1);
}
🖥️ 실행 결과
n = 1
n = 2
n = 3
…
계속 출력되다가 중간에 오류가 나서 멈추게 된다.
🛠️ 코드 수정
#include <stdio.h>
void rec(int n) {
if(n > 5) return; // 리턴값 반환할 조건문 추가
printf("n = %d\n", n);
rec(n + 1);
}
int main() {
rec(1);
}
🖥️ 실행 결과
n = 1
n = 2
n = 3
n = 4
n = 5
if문과 return문으로 중간에 무한 출력을 끊을 수 있다.
⌨️ 예제 13
// 재귀함수를 이용해서 팩토리얼 값을 출력하는 프로그램
#include <stdio.h>
int factorial(int n) { // n을 매개변수로 → n!을 return
if(n == 1) return 1; // n이 1일 때 멈춰서 음수로 무한히 내려가는 것 방지
return n * factorial(n - 1);
}
int main() {
printf("%d\n", factorial(5)); // 5!의 값 출력하기
}
🖥️ 실행 결과
120
팩토리얼(!)의 값은 for문을 사용해도 되지만, "n! = n * (n - 1)!"인 성질이 있기 때문에 재귀함수로 구할 수 있다.
⌨️ 예제 14
#include <stdio.h>
void rec(int n) {
if(n == 0) return;
printf("%d", n); // n 출력
rec(n - 1); // rec(n - 1) 호출 → 여기서 n은 rec(n)의 n과 기억공간이 다름
printf("%d", n); // 다시 n 출력
}
int main() {
rec(5);
printf("\n");
}
🖥️ 실행 결과
5432112345
n = 5
5 출력 ↴
n = 4
4 출력 ↴
n = 3
3 출력 ↴
n = 2
2 출력 ↴
n = 1
1 출력 ↴
n = 0 → (출력 없이 바로 종료)
1 출력 → (n = 1 종료)
2 출력 → (n = 2 종료)
3 출력 → (n = 3 종료)
4 출력 → (n = 4 종료)
5 출력 → (n = 5 종료)
⌨️ 예제 15
// 일차원 배열을 매개변수로 받은 함수
#include <stdio.h>
void printArr(int arr[4]) { // arr[4] == *arr
for(int i = 0; i < 4; i++) {
arr[i] *= 2; // arr[i] == *(arr + i) == i가 0일 때 *arr
}
}
int main() {
int arr[4] = {1, 2, 3, 4}; // 지역변수로 배열 선언
printArr(arr); // arr == &arr[0]
for(int i = 0; i < 4; i++) { // 함수 실행 후의 배열 출력
printf("%d ", arr[i]);
}
}
🖥️ 실행 결과
2 4 6 8
만약 배열을 매개변수로 하는 함수 printArr가 call-by-value라면, 배열이 main 함수 안에서 선언됐기 때문에 함수 내에서 어떤 연산을 하던 출력 결과는 원래 배열과 동일하게 나왔을 것이다.
그러나 출력 결과를 보면 각 요소에 2씩 곱해졌기 때문에, 이 함수는 call-by-reference라는 것을 알 수 있다.
⌨️ 예제 16
// 이차원 배열을 매개변수로 받은 함수
#include <stdio.h>
void printArr(int arr[2][3]) { // arr[2][3] == (*arr)[3]
for(int i = 0; i < 2; i++) {
for(int j = 0; j < 3; j++) {
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
int main() {
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
printArr(arr); // arr == &arr[0] (0번째 행 전체를 가리킴)
}
🖥️ 실행 결과
1 2 3
4 5 6
이차원 배열도 역시 주소값을 넘겨주기 때문에 call-by-reference이다.
⌨️ 예제 17
// 문자열을 매개변수로 받은 후, 그 문자열에서 공백을 제거하여 출력하는 함수
#include <stdio.h>
void print_noSpace(char str[]) { // str[] == *str 둘 다 가능
for(int i = 0; str[i] != '\0'; i++) { // str[i]가 null 문자가 아닐 동안만 반복문 실행
if(str[i] != ' ') {
printf("%c", str[i]);
}
}
}
int main() {
print_noSpace("Hello, World!\n");
print_noSpace("How are you today?\n");
}
🖥️ 실행 결과
Hello,World!
Howareyoutoday?
문자열의 마지막은 항상 null 문자라는 것을 이용해서 반복문의 종료 조건을 설정했다.