초보 해커를 위한 C언어와 동작 원리/Ch2. Programming

함수

anonkorea4869 2025. 4. 23. 00:16

학습 목표

! 내용은 검토 포스트 입니다. 부정확한 내용이 있을 있으니 양해바랍니다.

  • 함수의 정의(입력값을 넣으면 결과를 반환)을 이해한다.
  • call-by-value와 call-by-reference의 차이를 이해한다.
  • 지역 변수와 전역 변수의 차이점을 학습한다.

1. 함수의 정의

수학에서의 함수는 y = f(x)로 x값이 주어지면 연산을 통해 y값이 나오는것이다. 코딩에서도 마찬가지로 input이 주어지면 일련의 연산결과를 통해 output이 나오게된다. printf()함수에서 서식 문자와 변수라는 input을 넣으면 출력이라는 output이 나오는것과 마찬가지이다. 추가로 printf()함수는 사용자 지정함수가 아니라 내장 함수이기 때문에 stdio.h 헤더 파일에 존재한다.

코딩에서의 함수는 input과 output이 있을수도 없을수도 있다. 이것은 사용자가 코딩하는 것에 따라 다르다.

2. 함수 용어

2.1. 구성요소

함수를 사용하기위한 구성요소는 선언(prototype), 호출(call), 정의(definition)이다.

2.2. Function call

함수 호출 방법은 아래와 같다. 함수에 넣어주는 값을 ‘인자’라고 한다. 인자는 숫자와 같은 상수를 직접 넣을 수도 있고 변수를 넣을 수도 있다. 인자는 0개 이상이 될 수 있지만 반환 값은 최대 1개이다. 대부분의 언어가 반환 값을 최대 1개로 하지만, Python은 2개 이상의 반환도 지원한다.

// 1. 반환값이 (필요) 없을 경우 : 함수명(인자1, 인자2, ...)
printf("%d", 1);
// 2. 반환값이 있을 경우 : 변수 = 함수명(인자1, 인자2, ...)
a = isOddNumber(3);

함수를 사용할때 함수의 인자와 반환값을 알아야할 경우가 있는데. 함수 call할때 마우스를 오버(올려놓으면) 반환값과 인자를 알 수 있다.

2.3. Function definition

함수 정의는 쉽게 설명하자면 함수 그 자체를 구현하는 것이다. 형식은 아래 코드와 같다.

반환 데이터 타입 : 정수형으로 반환해야하면 int, 실수형으로 반환해야하면 double 등 자료형에 맞게 사용하면 된다. 만약 반환할 값이 없다면 void를 해주면 된다.

함수명 : 함수를 호출 할때 사용할 함수의 이름을 지정해주면 된다.

데이터 타입&매개변수 : 함수를 호출 할때 넣어주는 변수들이 인자(Argument)라면 함수가 받는 변수는 매개변수(Parameter)이라고 한다. 인자와 매개변수의 변수명을 같게 작성할 필요는 없다. 인자의 변수명이 calcNumber이더라도 매개변수명은 num이라고 하는 등 오히려 포괄적으로 작성하는 것이 일반적이다.

/*
반환데이터타입 함수명(데이터타입 매개변수) {
    작성할 코드
    return 반환값(반환값이 존재 한다면)
}
*/

int isOddNumber(int num) {
    if(num % 2 == 1) {
        return 1;
    } else {
        return 0;
    }
}

위에 작성한 intOddNumber 함수를 call하는 방법은 아래와 같다.

#include <stdio.h>

int isOddNumber(int num) {
    if(num % 2 == 1) {
        return 1;
    } else {
        return 0;
    }
}

int main(void) {
    int returnValue;

    returnValue = isOddNumber(3);
    printf("%d", returnValue);

    return 0;
}

2.4. Function prototype

함수 프로토타입은 어떤 함수를 사용 할 수 있는지 컴파일러 및 사용자에게 알려주는 것이다. 형식은 Function Definition의 윗부분에 세미콜론(;)을 붙인 형태이다.

// 1. 반환데이터타입 함수명(데이터타입 매개변수);
int isOddNumber(int num);
// 2. 반환데이터타입 함수명(데이터타입);
int isOddNumber(int);

C언어 컴파일러는 위에서 아래로 한줄 씩 읽어 나가는데 아래 코드와 같이 호출되는 함수가 함수를 호출하는 부분보다 아래있으면 함수를 찾지못해 에러가 나오게 된다.

#include <stdio.h>

int main(void) {
    int returnValue;

    returnValue = isOddNumber(3); // error: call to undeclared function 'isOddNumber'
    printf("%d", returnValue);

    return 0;
}

int isOddNumber(int num) {
    if(num % 2 == 1) {
        return 1;
    } else {
        return 0;
    }
}

작은 규모의 프로그램이라면 함수를 모두 main위에 올릴수 있겠으나, 큰 규모의 프로그램일 경우 호출되는 함수가 함수를 호출하는 부분보다 모두 위에 있기에는 복잡할 뿐만 아니라 가독성 측면에서도 떨어진다. 따라서 Function Prototype을 위에 하고 함수는 main아래에 적는다. Funtion Prototype에서는 매개변수의 데이터 타입이 중요할뿐 매개변수명은 중요하지 않기 때문에 적지 않아도 되지만 가독성이 좋게 하기위해서는 변수명도 같이 기입하면 좋다.

#include <stdio.h>

int isOddNumber(int num);

int main(void) {
    int returnValue;

    returnValue = isOddNumber(3);
    printf("%d", returnValue);

    return 0;
}

int isOddNumber(int num) {
    if(num % 2 == 1) {
        return 1;
    } else {
        return 0;
    }
}

2.5. caller & callee

함수를 부르는 함수는 caller, 함수에게 호출당한 함수는 callee라고 한다. main함수가 printf()를 호출할때 caller는 main, callee는 printf이다.

#include <stdio.h>

int main(void) {
    printf("Hello world");
    // printf는 main 함수 입장에서 callee
    // main은 printf 함수 입장에서 caller

    return 0;
}

3. main함수의 매개변수

함수 정의 코드를 보면 우리가 자주 사용하는 main 도 같은 형식이라는것을 알 수 있다. main도 함수인데 main에게 int형의 반환값과 매개변수 공간이 있는것 보면 main도 다른 함수에 의해 불려진 함수라는 것을 알수있다. return 0;를 넘겨주는 것은 main을 호출한 함수에게 0을 임의의 이유로 전달 하는 것을 알수 있다.

int main(void) {
    return 0;
}

3.1. 인자 없이 실행

인자 없이 실행은 우리가 일반적으로 실행버튼을 누르는것과 같다. program.c라는 C언어 코드의 실행 버튼을 누르면 하단 Terminal에서 아래와 같은 명령어를 볼 수 있다. C코드가 있는 폴더로 들어가서 컴파일 한 후 코드를 실행시키는 것이다.

// 이동 "절대주소" && 컴파일 && "절대주소"실행파일명
cd "절대주소" && gcc program.c -o program && "절대주소"program

위 명령어로 만들어진 파일은 아래와 같은 명령어로 실행 시킬 수 있다. 해당 명령어는 현재 위치에 실행파일이 있을 때 실행시키는 명령어이다.

./program

3.2. 인자 있이 실행

보통 main의 매개변수 항상 없는것이라고 생각 할 수 있지만 그렇지 않다. main도 매개변수를 받을 수 있다. 프로그램은 ./program 으로 실행할 수도 있지만 실행시 아래와 같이 매개변수를 포함하여 호출 할 수 있다.

./program argument1 argument2

main의 매개변수는 argc, argv, envp가 있는데 변수의 의미는 아래와 같다.

  • argc(Argument Count) : 프로그램 호출 인자의 갯수 → 반환 값 : 정수형
  • argv(Argument Vector) : 프로그램 호출 인자 문자열 → 반환 값 : 2차원 배열
  • envp(Environment path) : 프로그램 호출 환경 변수 → 반환 값 : 2차원 배열
int main(int argc, char *argv[], char *envp[]) {

}

3.3. argc

프로그램 호출 인자의 갯수로 아래 명령어의 경우 “./program”, “argument1”, “argument2” 총 3개의 문자열이 있으므로 argc는 3개이다.

./program argument1 argument2

아래 코드를 컴파일 한 후 명령어를 입력하면 argc의 값이 나오게 된다.

#include <stdio.h>

int main(int argc, char *argv[], char *envp[]) {

    printf("argc = %d", argc); // argc = 3
    return 0;
}

3.4. argv

프로그램 호출 인자 문자열로 2차원 배열형식이다. char argv[]라고 했지만 char *argv라고 적기도 한다. 아래 명령어의 경우 “./program”, “argument1”, “argument2” 총 3개의 문자열이 2차원 배열에 담겨있다.

./program argument1 argument2

아래 코드를 컴파일 한 후 명령어를 입력하면 argv 문자열들이 값이 나오게 된다.

#include <stdio.h>

int main(int argc, char *argv[], char *envp[]) {

    for(int i = 0 ; i < argc ; i++) {
        printf("%s\n", argv[i]);
    }
    /*
        ./program
        argument1
        argument2
    */

    return 0;
}

명령어로 실행 시킬 때 보통 ./program으로 입력하지만 Wargame 등에서 보안 정책을 우회하기 위해서 같은 의미인 ././././program으로 실행시켜 문자열 크기를 늘리기도 한다.

3.5. envp

포르그램 호출 환경 변수로 OS 실행 환경에 대한 정보를 담고 있다. 잘 안쓰이긴 하지만 출력하면 Linux 명령어 evn 결과의 일부가 나온다.

#include <stdio.h>

int main(int argc, char *argv[], char *envp[]) {

    for(int i = 0 ; i < sizeof(*envp); i++) {
        printf("%s\n", envp[i]);
    }
        /*
            USER=[사용자명]
            COMMAND_MODE=unix2003
            __CFBundleIdentifier=com.microsoft.VSCode
        */
    return 0;
}

4. Call-by-value & Call-by-reference

아래 코드를 실행 시켜보자. myFunc 함수에서 값을 변경했음에도 불구하고 num값이 바뀌지 않는다. 이는 Call-By-value와 Call-by-reference와 관련된것이다.

#include <stdio.h>

void myFunc(int num) {
    num += 1;
    printf("myFunc's num = %d\n", num); // myFunc's num = 2
}

int main(void) {
    int num = 1;

    myFunc(num);
    printf("main's num = %d\n", num); // main's num = 1

    return 0;
}

4.1. Call-by-value

함수를 호출 할 때 값을 넘겨주는 것을 의미한다. 아래 코드에서 넘겨주는 num 값인 1은 임의의 저장공간에 복사된다. main 함수에서의 num 주소와 myFunc 함수의 num의 주소가 다른것을 알 수 있다. myFunc에서 매개변수를 사용할 때는 원본을 변경하는 것이 아닌 복사본을 변경하는 것이므로 함수가 종료된 후에도 main 에서의 num값이 변하지 않는것이다.

#include <stdio.h>

void myFunc(int num) {
    printf("num = %d, &num = %p\n", num, &num); // num = 1, &num = 0x16faef40c
    num += 1;
    printf("myFunc's num = %d\n", num); // myFunc's num = 2
}

int main(void) {
    int num = 1;

    printf("num = %d, &num = %p\n", num, &num); // num = 1, &num = 0x16faef438
    myFunc(num);
    printf("main's num = %d\n", num); // main's num = 1

    return 0;
}

4.2. Call-by-reference

함수를 호출 할 때 주소를 넘겨주는것을 의미한다. main 함수에서의 num 주소와 myFunc 함수의 num의 주소가 같은 것을 알 수 있다. 주소안에 값에 접근 하여 1을 증가시켜 원본 자체를 수정한다. 호출 후의 main 함수를 보면 num의 값이 바뀐 것을 알 수 있다.

#include <stdio.h>

void myFunc(int *num) {
    printf("*num = %d, num = %p\n", *num, num); // *num = 1, num = 0x16f05f438
    *num += 1;
    printf("myFunc's *num = %d\n", *num); // myFunc's *num = 2
}

int main(void) {
    int num = 1;

    printf("num = %d, &num = %p\n", num, &num); // num = 1, &num = 0x16f05f438
    myFunc(&num);
    printf("main's num = %d\n", num); // main's num = 2

    return 0;
}

5. printf & scanf

배열과 포인터를 어느정도 익혔으니 printf와 scanf의 인자의 의미에 대해 알아볼것이다. printf(”%d”, num)과 scanf(”%d”, &num)의 경우 printf 함수는 변수 앞에 &가 없지만 scanf에는 왜 변수 앞에 &가 있는지. scanf(”%s”, arr)의 경우에는 왜 &가 없는지 알아보겠다.

5.1. printf 함수

&는 변수의 주소를 알아내기 위해 사용한다. 변수의 주소를 알아내는것은 변수를 함수로 넘길때 함수에서 값을 바꿀수 있게 하기 위함이다. printf의 경우 인자로 넘겨준 변수가 변하는 일은 없으므로 &를 사용하지 않아도 된다.

#include <stdio.h>

int main(void) {   
    int num = 3;

    printf("%d", num); // num값은 안바뀜

    return 0;
}

5.2. scanf 함수

반대로 scanf는 변수의 값을 바꿔주어야 한다. 따라서 &를 붙여 변수의 주소를 넘긴다.

#include <stdio.h>

int main(void) {   
    int num = 3;

    scanf("%d", &num); // num값은 바뀜

    return 0;
}

문자열 str의 경우 그 자체로 포인터이기 때문에 &를 안써도 된다. 하지만 n번째 인덱스의 값을 바꾸기 위해서는 str[n]은 값이기 때문에 앞에 &를 작성해주어야한다.

#include <stdio.h>

int main(void) {   
    char str[] = "Hello world";

    scanf("%s", str); // str은 주소
    scanf("%c", &str[0]); // str[0]은 값

    return 0;
}

6. 재귀 함수

재귀(Recursive) 함수는 자기 자신을 호출하고 호출 된 함수에서 자기 자신을 다시 호출 하는 함수이다. 대표적인 예제로 n!(Factorial) 구하기가 있다. 3!은 321으로 6이다. 아래 코드는 n을 통해 n-1 값을 구하고 n * (n-1)을 하여 3!을 구하는 예제이다.

#include <stdio.h>

int factorial(int num) {
    if(num == 1) {
        return 1;
    }

    num *= factorial(num - 1);

    return num;
}

int main(void) {   
    int num;

    num = factorial(3);
    printf("3! = %d", num);

    return 0;
}

7. 지역변수와 전역변수

이전 변수에 대하여 학습했는데, 변수는 지역 변수(Local variable)과 전역 변수(Global variable)로 나뉜다. 지역 변수와 전역 변수는 해당 변수를 사용 가능 한 함수의 범위에 따라 구분된다.

7.1. 지역 변수

우리가 여태까지 배웠던 것은 모두 지역변수이다. 지역 변수는 변수가 선언된 함수에서만 사용할 수 있는 변수이다. 아래 코드는 main함수에 num 변수를 생성 한 후 myFunc 함수에서 num변수를 출력하고자 한다. 그 결과 ‘구분할 수 없는 식별자 num’ 에러가 뜨면서 프로그램일 컴파일 되지 않는다.

#include <stdio.h>

void myFunc() {
    printf("myFunc : %d\n", num); // error: use of undeclared identifier 'num'
}
int main(void) {
    int num = 1;
    printf("main : %d\n", num);
      myFunc();

    return 0;
}

아래 코드는 myFunc 함수에 num을 인자로 주어 실행시킨 결과이다. Call-by-value에서 이해했다시피 myFunc의 num 매개변수는 main 함수의 num인자를 복사한것으로, myFunc에서 변화된 값이 main함수에는 적용되지 않는다.

#include <stdio.h>

void myFunc(int num) {
    printf("myFunc : %d\n", num); // myFunc : 2
    num++;
}
int main(void) {
    int num = 1;

    myFunc(num);
    printf("main : %d\n", num); // main : 1

    return 0;
}

아래 코드와 같이 Call-by-reference도 마찬가지이다. 값은 변화할지 언정 Call-by-value와 마찬가지로 복사된 변수이므로 그 주소가 다르다. 결론적으로 두 변수는 다른 변수이다.

#include <stdio.h>

void myFunc(int *num) {
    printf("myFunc : %d, %p\n", *num, &num); // myFunc : 1, 0x16d3533f8
    (*num)++;
}

int main(void) {
    int num = 1;

    myFunc(&num);
    printf("main : %d, %p\n", num, &num); // main : 2, 0x16d353438

    return 0;
}

7.2. 전역 변수

전역 변수는 모든 함수에서 사용 가능한 변수이다. 전역 변수는 일반적으로 함수 위에 위치한다. 아래 코드는 num = 1 전역 변수를 생성하고 main과 myFunc 두 함수에서 값과 변수의 주소를 출력한 결과이다. 값 뿐만 아니라 주소가 같음으로 두 변수는 같은 변수를 사용한다고 할 수 있다.


#include <stdio.h>

int num = 1;

void myFunc() {
    printf("myFunc : %d, %p\n", num, &num); // myFunc : 1, 0x102df8000
}

int main(void) {
    myFunc();
    printf("main : %d, %p\n", num, &num); // myFunc : 1, 0x102df8000

    return 0;
}

7.3. 지역 변수와 전역 변수의 사용 범위

반복문에서 반복문 외부 변수보다 내부 변수의 우선순위가 높듯이, 전역 변수와 지역 변수도 우선순위가 있다. myFunc 함수는 전역변수의 값인 1을 출력하지만 main 함수는 지역변수의 값인 2를 출력함을 알 수 있다. 결론적으로 전역 변수보다 지역 변수가 우선순위가 높다. 하지만 아래와 같이 작성 하게되면 가독성이 매우 떨어지므로 중복되는 변수명은 가급적 사용하지 않는 것이 좋다.

#include <stdio.h>

int num = 1;

void myFunc() {
    printf("myFunc : %d\n", num); // myFunc : 1
}

int main(void) {
    int num = 2;
    printf("main : %d\n", num); // myFunc : 2

    myFunc();

    return 0;
}

7.4. 전역 변수의 초기값

쓰레기값으로 초기화 되는 지역변수와 달리 전역변수는 초기값이 있다. 아래 코드는 전역 변수와 지역 변수를 선언하고 그 값을 출력한 것이다. 지역변수는 쓰레기값에 나오는데 반해 전역 변수는 0이 출력되었다. 전역변수는 그 값이 0으로 초기화 되어있음을 알 수 있다.

#include <stdio.h>

int global_variable;

int main(void) {
    int local_variable;
    printf("%d\n", local_variable); // 80665488 
    printf("%d\n", global_variable); // 0

    return 0;
}

문자열 역시 두 변수는 초기값이 차이가 난다. 아래 코드는 지역 변수와 전역 변수를 크기만큼 반복하여 아스키코드를 출력하는 코드이다. 지역 변수는 임의의 아스키코드가 출력된 반면에 전역 변수는 모두 0으로 출력되어있다.

#include <stdio.h>
#include <string.h>

char global_variable[4];

int main(void) {
    char local_variable[4];

        // 지역 변수 출력
    for(int i = 0 ; i < strlen(local_variable) ; i++) {
        printf("local : %d\n", local_variable[i], local_variable[i]);
    }
    /**
        local : -112
            local : 27
            local : 29
            local : 1
        **/

    // 전역 변수 출력
    for(int j = 0 ; j < strlen(local_variable); j++) {
        printf("global : %d\n", global_variable[j], global_variable[j]); 
    }
    /**
        global : 0
        global : 0
        global : 0
        global : 0
    **/
    return 0;
}

'초보 해커를 위한 C언어와 동작 원리 > Ch2. Programming' 카테고리의 다른 글

구조체  (0) 2025.04.23
문자열 함수  (0) 2025.04.23
배열과 포인터  (2) 2025.04.22
포인터  (0) 2025.04.22
배열  (0) 2025.04.22