티스토리 뷰

C, C++

[ C언어 ] 18. 함수

RiKang 2018. 1. 4. 19:53
반응형

Table of Contents


개요

기본 문법

함수의 정의

함수의 호출

함수의 선언

전역 변수와 지역 변수

호출 방식

문제




1. 개요


 C언어로 만든 프로그램은 운영체제가 main() 함수를 호출하며 시작하고, main() 함수가 0을 반환하면 종료하게 됩니다. 따라서 지금까지는 main() 함수 안에 모든 명령문을 작성하는 식으로 프로그래밍을 해왔습니다. 하지만, 복잡한 프로그램을 만들게 되면 이런 방식은 곧 한계에 부딪힙니다.


 예를 들기 위해, 앞글에서 살펴본 strlen() 함수가 존재하지 않는다고 가정하면 어떻게 되는지 생각해보겠습니다. 그러면 일단 strlen()의 효과를 얻을 수 있는 3줄 정도의 명령문을 작성하게 될 것입니다. 물론 여기까지는 1줄로 끝낼 수 있는 작업이 3줄 정도로 늘어난 것이니, 불편함이 느껴지지 않을 수 있습니다. 그런데 만일 main() 함수 안에서 문자열의 길이를 구하는 작업이 10번 필요하다면 어떻게 될까요? 아마도 전체 길이가 30줄이나 늘어나게 되는 결과가 초래될 것입니다. 굉장히 비효율적인 복사 붙여넣기 작업이지요.


 함수를 이용하면 이런 문제점들을 줄일 수 있습니다. 여러 명령문을 하나의 함수로 묶어서 관리할 수 있기 때문입니다. 지금까지는 C언어에서 지원해주는 함수들만 사용해 왔지만, 이제부터는 함수를 직접 만들고 사용하게 될 것입니다.




2. 기본 문법


 함수를 사용할 때, 기본적으로 알아야 할 문법으로는 함수의 정의, 호출, 그리고 선언이 있습니다. 함수의 정의란, 함수가 어떤 명령들을 수행해야 하는지를 프로그래밍하는 것입니다. 따라서 지금까지 했던 프로그래밍들은 모두 main() 함수를 정의하는 과정이었다고 볼 수 있습니다. 함수 호출은 정의해놓은 함수를 실행하는 법이고, 함수의 선언은 함수를 실행할 때 어떤 형식을 맞춰줘야 하는지 컴파일러에 알려주는 작업입니다. 이제 '두 정수를 입력받아 두 수 중 큰 수를 반환해주는 함수'를 통해 기본 문법을 알아보겠습니다.




 1. 함수의 정의


 함수를 정의할 때 사용하는 형식을 알아보겠습니다.


1
2
3
4
5
반환값의_형식  함수_이름  (매개변수){
    명령문1;
    명령문2;
    return 반환값;
}
cs


1
2
3
4
5
//strlen 예시
정수형변수 strlen (문자열){
    문자열의 길이를 구하기 위한 명령문
    return 문자열의_길이;
}
cs


 반환값의 형식 ) 이 함수를 호출한 함수로 반환하는 값의 자료형입니다. main 함수에서는 정수형인 0을 return 하기 때문에, 이 부분에 int 를 사용하였습니다.


 함수 이름 ) 말 그대로 함수의 이름, 즉, 식별자입니다.


 매개변수 ) 이 함수를 호출한 함수로부터 받은 값입니다. 예를 들어 printf()함수를 printf("hello world!"); 으로 사용하였다고 하면 이 "hello world!" 부분을 매개변수로 받아야 합니다.


 명령문 ) 지금까지 main() 함수에 명령문들을 작성하셨던 것처럼 프로그래밍하는 부분입니다.


 return 반환값 ) 이 함수를 호출한 함수로 반환값을 반환합니다. strlen() 함수를 예로 들면, 매개변수로 받은 문자열의 길이를 반환해야 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//code by RiKang, weeklyps.com
#include <stdio.h>
 
int get_ans(int v1, int v2){
    int ret;
    if(v1>v2)
        ret = v1;
    else
        ret = v2;
    return ret;
}
 
int main() {
    return 0;
}
cs


 get_ans() 함수는 두 개의 정수를 매개변수로 받아, 최대값을 반환해주는 함수입니다. 이를 통해 함수 정의의 기본 형식를 확인할 수 있습니다.


 반환값의 형식 = int

 함수 이름 = get_ans

 매개변수 = int v1, int v2 (이 함수를 호출할 때에는 두 개의 정수형 변수를 넘겨줘야 합니다.)

 명령문 = 정수형 변수 ret 에 v1, v2 중 최대값을 저장합니다.

 return 반환값 = return ret; => 명령문으로 얻은 최대값인 ret을 넘겨줍니다.


 get_ans() 나 strlen() 처럼 반환값이 존재하는 함수라면, return 을 해줘야 합니다. 그렇지 않으면 에러가 발생하게 되지요. 하지만 꼭 기본 형식처럼 마지막 줄에서 return을 할 필요는 없습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
//code by RiKang, weeklyps.com
#include <stdio.h>
 
int get_ans(int v1, int v2){
    if(v1>v2)
        return v1;
    else
        return v2;
}
 
int main() {
    return 0;
}
cs


 이런식으로 get_ans()를 구성하여도 같은 효과를 얻을 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
//code by RiKang, weeklyps.com
#include <stdio.h>
 
int get_ans(int v1, int v2){
    if(v1>v2)
        return v1;
    return v2;
}
 
int main() {
    return 0;
}
cs


 함수는 return과 동시에 종료하게 됩니다. 따라서 return 이후의 명령문들은 실행하지 않습니다. 그러므로 굳이 else 를 붙이지 않더라도 v1이 리턴되었을 땐 return v2;가 실행되지 않습니다. 위의 프로그램처럼 코딩해도 정확히 v1, v2 중 최대값을 반환해 줄 수 있습니다.


1
2
3
4
5
6
7
8
9
10
//code by RiKang, weeklyps.com
#include <stdio.h>
 
void printf_error(void){
    printf("에러가 발생하였습니다.");
}
 
int main() {
    return 0;
}
cs


 만약 printf();처럼 반환값이 없어도 되는 함수를 작성하고 싶으면, 반환값의 형식으로 void를 넣어주면 됩니다. void를 사용하면 반환값이 없다는 의미가 되며, 따라서 return 이 없더라도 에러가 발생하지 않습니다.




 2. 함수의 호출


 C언어에서 main 함수이외의 함수들은 모두 다른 함수의 호출을 통해 실행됩니다. 그동안 사용하셨던 scanf(), printf() 등도 main() 함수에서 호출하여 실행시킨 것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//code by RiKang, weeklyps.com
#include <stdio.h>
 
int get_ans(int v1, int v2){
    if(v1>v2)
        return v1;
    return v2;
}
 
int main() {
    int a=6, b=4;
    printf("a,b 중 최대값은 %d입니다.\n",get_ans(a,b));
    
    int c = get_ans(a,b);
    printf("c = %d\n",c);
    return 0;
}
cs

출력

a,b 중 최대값은 6입니다.

c = 6


 main() 함수에서 get_ans()를 호출한 예시입니다. get_ans(6,4) 는 6을 반환하기 때문에, 그 자리에 6을 쓴 것과 동일한 효과가 확인됩니다.


1
2
3
4
5
6
7
8
9
10
11
//code by RiKang, weeklyps.com
#include <stdio.h>
 
void printf_error(void){
    printf("에러가 발생하였습니다.");
}
 
int main() {
    printf_error();
    return 0;
}
cs

출력

에러가 발생하였습니다.


 반환값이 없는 함수라면 이처럼 단독으로 사용할 수 있습니다.




 3. 함수의 선언


 사실 저희는 이미 함수의 정의와 호출만으로도 함수를 만들고 실행시키는데 성공하였습니다. 그렇다면 함수의 선언이란 건 왜 필요한 것일까요? 알아보기 위해 아래와 같은 프로그램을 만들어서 실행시켜 보시길 바랍니다.


1
2
3
4
5
6
7
8
9
10
11
//code by RiKang, weeklyps.com
#include <stdio.h>
 
int main() {
    printf_error(); // 컴파일 경고 발생, 컴파일러에 따라 에러 발생 가능
    return 0;
}
 
void printf_error(void){
    printf("에러가 발생하였습니다.");
}
cs


 이 프로그램은 컴파일러 버전에 따라 컴파일 경고 혹은 에러가 발생하게 됩니다. 단지 printf_error()를 main() 함수 밑으로 내린 것 뿐인데 어째서 경고나 에러가 발생한 것일까요? 그 이유는 컴파일러가 printf_error() 함수를 인지하기 전에, printf_error()를 사용했기 때문입니다. printf_error()함수의 정의가 main() 함수 위에 있을 때는, main() 함수를 시작하기 전에 printf_error()함수를 인지했기 때문에 이런 문제가 발생하지 않았던 것입니다. 컴파일러가 main()함수를 만나기 전까지는 소스의 1행부터 분석해 나가기 때문이지요.


 함수의 선언을 이용하면, 이런 문제를 없앨 수 있습니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
//code by RiKang, weeklyps.com
#include <stdio.h>
 
void printf_error(void); // 함수의 선언
 
int main() {
    printf_error();
    return 0;
}
 
void printf_error(void){ // 함수의 정의
    printf("에러가 발생하였습니다.");
}
cs


 함수의 선언은 변환값의_형식  함수_이름  (매개변수)까지 적어주면 됩니다. 단, 매개변수는 형식만 적어줘야 합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//code by RiKang, weeklyps.com
#include <stdio.h>
 
int get_ans(intint); // 함수의 선언
 
int main() {
    int a=6, b=4;
    printf("a,b 중 최대값은 %d입니다.\n",get_ans(a,b));  // 함수의 호출 (getans() 호출 + printf() 호출)
    return 0;
}
 
int get_ans(int v1, int v2){ // 함수의 정의
    if(v1>v2)
        return v1;
    return v2;
}
cs




3. 전역 변수와 지역 변수


 저희는 함수에 대해 다루기 이전까지, main() 함수 내부에서 변수를 선언하고 사용했습니다. 마치 프로그램이 시작할 때 메모리에 변수가 할당되고, 프로그램이 끝날 때 메모리에서 사라지는 것처럼 말이지요. 하지만 사실 변수는 프로그램 중간에 할당되고, 또 중간에 삭제되기도 합니다. 변수가 삭제된 이후에 가져다 쓰려고 하면 당연히 에러가 발생하게 됩니다. 따라서 프로그래머는 각각의 변수가 어디부터 어디까지 존재하고 있는지를 인지하고 있어야 합니다.


 1. 전역 변수


 전역 변수란, 프로그램의 모든 영역에서 사용할 수 있는 변수를 의미합니다. 전역 변수를 사용하기 위해선 변수를 어떤 중괄호 안에도 포함되지 않은 위치, 그리고 해당 변수를 사용할 함수들보다 위쪽에 선언해야 합니다. 따라서 전처리기 이후에 선언하는 경우가 대부분입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//code by RiKang, weeklyps.com
#include <stdio.h>
 
int a; // 전역 변수의 선언, 전역 변수는 0을 써주지 않더라도 0으로 초기화
 
void plus1(void){
    a = a+1// 여기에서도 a 사용 가능
}
 
int main() {
    for(int i=0; i<3; i++){
        printf("a = %d\n",a);  // 여기에서도 a 사용 가능
        plus1();
    }
    return 0;
}
cs

출력

a = 0

a = 1

a = 2


 이처럼 전역변수를 선언하면 main() 함수와 plus1() 함수 모두에서 변수를 사용할 수 있습니다. 전역변수는 프로그램이 종료될 때 메모리에서 해제됩니다. 또 다른 특징으로는 전역변수로 선언한 변수는 굳이 초기화를 하지 않더라도 0으로 초기화된다는 점이 있습니다.


 2. 지역 변수


 지역 변수는 함수 내부에서 선언되며, 기본적으로 변수가 선언된 함수에서만 접근할 수 있도록 되어있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//code by RiKang, weeklyps.com
#include <stdio.h>
 
 
void plus1(void){
    a = a+1// 여기에서 에러 발생!
}
 
int main() {
    int a=0// main() 함수에서 사용할 수 있는 지역 변수 선언
    for(int i=0; i<3; i++){
        printf("a = %d\n",a);  // 여기에서는 a 사용 가능
        plus1();
    }
    return 0;
}
cs

결과 : 컴파일 에러


 이 프로그램을 실행해보면 Line 6에서 에러가 발생합니다. main() 함수에서만 통용되는 a를 plus1()에서 사용하려 했기 때문이지요. plus1()에는 a라는 변수가 없으므로 에러가 난 것입니다.


 그렇다면, plus1()에서도 a를 선언해주면 어떻게 될까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//code by RiKang, weeklyps.com
#include <stdio.h>
 
 
void plus1(void){
    int a;
    a = a+1// plus1()의 a만 증가!
}
 
int main() {
    int a=0// main() 함수에서 사용할 수 있는 지역 변수 선언
    for(int i=0; i<3; i++){
        printf("a = %d\n",a);  // main()의 a는 그대로!
        plus1();
    }
    return 0;
}
 
cs

출력

a = 0

a = 0

a = 0


 위의 출력 결과에서 알 수 있듯이, main() 함수의 a는 증가하지 않습니다. main() 함수의 a와 plus1() 함수의 a는 서로 다른 존재이기 때문이지요. 이 두 변수는 이름만 같을 뿐이지, 메모리 상에서 서로 다른 공간을 차지하고 있기 때문에, 서로 연산 결과를 공유하지 않습니다.




4. 호출 방식


 호출하는 함수와 호출받은 함수는 서로 매개변수를 주고 받습니다. 이 매개변수를 전달하는 방식으로는 두 가지, 값 호출 방식과 참조 호출 방식이 있습니다.


 1. 값 호출 방식 ( Call by value )


 값 호출 방식은 매개변수로 값을 넘기는 방법입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//code by RiKang, weeklyps.com
#include <stdio.h>
 
void printf_and_plus(int v){
    printf("v = %d\n",v);
    v = v+1;
    printf("v = %d\n",v);
}
 
int main() {
    int a=0;
    printf_and_plus(a);
    printf("a = %d\n",a);
    return 0;
}
 
cs

출력

v = 0

v = 1

a = 0


 앞서 함수의 정의에서 예시로 든 형식이 바로 이 값 호출 방식입니다. printf_and_plus(a); 로 a를 넘기지만, 사실 a의 값인 0만 넘어가게 됩니다. 이 0 을 받은 int v를 출력해보면 0이 출력되지요.


 문제는 v=v+1; 부분입니다. 이 명령으로 인해 v는 1이 됩니다. 하지만 출력 결과를 보면 main() 함수의 a는 그대로 0입니다. 값 호출 방식은 말 그대로 값인 0만을 보내기 때문에 v가 증가하든 말든 a와는 상관없는 일인 것입니다. v와 a는 서로 다른 변수이기 때문입니다. 결국 값 호출 방식을 사용하면 원본은 유지되게 됩니다.


 2. 참조 호출 방식 ( Call by reference )


 참조 호출 방식은 매개변수로 변수의 주소를 넘기는 방법입니다. 받은 주소를 통해 원본의 메모리를 조작할 수 있기 때문에, 사실상 원 변수를 넘기는 것과 같은 효과를 볼 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//code by RiKang, weeklyps.com
#include <stdio.h>
 
void printf_and_plus(int *v){ // 포인터 변수로 받습니다.
    printf("*v = %d\n",*v);
    *= *v+1;
    printf("*v = %d\n",*v);
}
 
int main() {
    int a=0;
    printf_and_plus(&a); //a가 아니라 &a를 넘깁니다.
    printf("a = %d\n",a);
    return 0;
}
 
cs

출력

*v = 0

*v = 1

a = 1


 값 호출 방식의 출력 결과와 비교해보시면 차이점을 알 수 있습니다. a가 1이 된 점이 다르지요. 이는 printf_and_plus()에서 사용한 *v가 main() 함수의 a와 같은 변수이기 때문입니다.


 매개변수로 a의 주소를 보냈으며, 이를 포인터 변수로 받았기 때문에 v 는 곧 main() 함수의 a의 주소를 가리키게 됩니다. 따라서 *v의 메모리 구간과 main() 함수의 a의 메모리 구간이 동일합니다. 사실상 *v와 a가 동일한 것입니다. 따라서 *v를 변화시키면 a도 변화하게 됩니다. 결국 참조 호출 방식을 사용하면, 원본 데이터를 그대로 조작할 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//code by RiKang, weeklyps.com
#include <stdio.h>
 
void printf_arr(int *p, int n){ // 포인터 변수로 받습니다.
    for(int i=0; i<n; i++)
        printf("%d ",*(p+i));
    printf("\n");
    for(int i=0; i<n; i++)
        printf("%d ",p[i]);
    printf("\n");
}
 
int main() {
    int a[5= {5,4,3,2,-1};
    printf_arr(&a[0],5);
    printf_arr(a,5);
    return 0;
}
cs
출력
5 4 3 2 -1 
5 4 3 2 -1 
5 4 3 2 -1 
5 4 3 2 -1 

 이 참조 호출 방식을 사용하면 배열을 넘기는 것도 쉽게 가능힙니다. 값 호출 방식으로 배열을 넘기려고 하면 배열 안의 모든 값을 넘겨줘야 하지만, 참조 호출 방식을 사용하면 배열의 첫 번째 주소만 넘겨주면 되기 때문입니다.




5. 문제


 함수의 숙련도 향상을 위해, 아래의 문제들은 함수를 사용해서 해결하시는 걸 추천합니다. 해결하기 힘들다면 풀이 모음을 보시는 걸 추천합니다.


 (0) [BOJ 1110] 더하기 사이클


 (1) [BOJ 1076] 저항


 (2) [BOJ 4673] 셀프 넘버


 풀이 모음

반응형

'C, C++' 카테고리의 다른 글

[ C언어 ] 19. 구조체  (0) 2018.01.05
[ C언어 ] 17. 문자열 관련 함수  (0) 2018.01.03
[ C언어 ] 16. 포인터  (0) 2018.01.02
[ C언어 ] 15. 형 변환  (0) 2017.12.07
[ C언어 ] 14. 문자열  (0) 2017.12.04