C언어 이론 정리 - 05

Lee Pil Ung·2021년 12월 5일
0

C

목록 보기
5/6
post-thumbnail

📖 C언어 이론 정리 - 05

C언어 이론 정리 파트 05
자료 참조 : TCP School.com

- C언어 함수

함수란?

프로그래밍에서 함수(function)란 하나의 특별한 목적의 작업을 수행하기 위해 독립적으로 설계된 프로그램 코드의 집합으로 정의할 수 있다.

C 프로그램은 이러한 함수들로 구성되며, 포함된 함수들을 사용하여 프로그램의 목적을 달성하게 된다.

C언어에서 함수는 크게 표준 함수사용자 정의 함수로 구분할 수 있다.

함수를 사용하는 이유

함수를 사용하는 가장 큰 이유는 바로 반복적인 프로그래밍을 피할 수 있기 때문이다.

프로그램에서 특정 작업을 여러 번 반복해야 할 때는 해당 작업을 수행하는 함수를 작성하면 된다.

그리고서 프로그램이 필요할 때마다 작성한 함수를 호출하면 해당 작업을 반복해서 수행할 수 있다.

또한, 프로그램을 여러 개의 함수로 나누어 작성하면, 모듈화로 인해 전체적인 코드의 가독성이 좋아진다.

그리고 프로그램에 문제가 발생하거나 기능의 변경이 필요할 때에도 손쉽게 유지보수를 할 수 있다.

  • 🚩 함수의 크기에 대해서 정확히 명시된 규칙은 없으나, 대략 하나의 기능을 하나의 함수로 만드는 것이 가장 좋다.

함수의 정의

C언어에서 사용자 정의 함수를 정의하는 방법은 다음 그림과 같다.

  1. 반환 타입(return type) : 함수가 모든 작업을 마치고 반환하는 데이터의 타입을 명시한다.

  2. 함수 이름 : 함수를 호출하기 위한 이름을 명시한다.

  3. 매개변수 목록(parameters) : 함수 호출 시에 전달되는 인수의 값을 저장할 변수들을 명시한다.

  4. 함수 몸체 : 함수의 고유 기능을 수행하는 명령문의 집합이다.

함수 호출 시에는 여러 개의 인수를 전달할 수 있지만, 함수가 반환할 수 있는 값은 1개를 넘지 못한다.

또한, 함수의 특성에 따라 인수나 반환값이 하나도 없는 함수도 존재할 수 있다.

📝 예제

다음 예제에서는 인수로 전달받은 두 수 중에서 더 큰 수를 반환하는 bigNum()함수를 정의하여 사용한다.

#include <stdio.h>

int bigNum(int num01, int num02) // 함수의 정의
{
    if (num01 >= num02)
    {
        return num01;
    }
    else
    {
        return num02;
    }
}

int main(void)  // 위에서 정의한 함수 활용
{
    int result;
    result = bigNum(3, 5); // 함수의 호출
    printf("두 수 중 더 큰수는 %d입니다.\n", result);
    result = bigNum(3, 1); // 함수의 호출
    printf("두 수 중 더 큰수는 %d입니다.\n", result);
    result = bigNum(7, 5); // 함수의 호출
    printf("두 수 중 더 큰수는 %d입니다.\n", result);
    return 0;
}
// 실행 결과
두 수 중 더 큰수는 5입니다.
두 수 중 더 큰수는 3입니다.
두 수 중 더 큰수는 7입니다.

함수의 원형 선언

C언어에서 함수를 정의할 때는 그 위치가 매우 중요하다.

📝 예제

#include <stdio.h>

int main(void)
{
    int result;
    result = bigNum(3, 5); // 함수의 호출
    printf("두 수 중 더 큰수는 %d입니다.\n", result);
    result = bigNum(3, 1); // 함수의 호출
    printf("두 수 중 더 큰수는 %d입니다.\n", result);
    result = bigNum(7, 5); // 함수의 호출
    printf("두 수 중 더 큰수는 %d입니다.\n", result);
    return 0;
}
int bigNum(int num01, int num02) // 함수의 정의
{
    if (num01 >= num02)
    {
        return num01;
    }
    else
    {
        return num02;
    }
}
// 실행 결과 ( 에러 발생 )
warning: implicit declaration of function ‘bigNum’ [-Wimplicit-function-declaration]
    6 |     result = bigNum(3, 5); // 함수의 호출
      |              ^~~~~~

C언어에서는 가장 먼저 main() 함수가 컴파일러에 의해 컴파일된다.

위의 예제에서 컴파일러는 main()함수에 등장하는 bigNum()함수를 아직 알지 못하기 때문에 컴파일 오류를 발생시킨다.

따라서 컴파일러에 bigNum() 함수는 나중에 정의되어 있다고 알려줘야 한다.

그 역할을 하는 것이 바로 함수의 원형(prototype)선언이다.

위와 같이 차례대로 한 번에 컴파일하는 방식을 단일 패스(one pass)컴파일 방식이라고 한다.

하지만 하드웨어의 발달로 컴파일러에 따라 여러 번에 걸쳐 컴파일하는 다중 패스(multi-pass)방식이 많아지고 있다.

다중 패스 방식의 컴파일러에서는 함수의 원형을 선언하지 않아도 컴파일 오류를 발생시키지 않는다.

하지만 오래된 컴파일러는 대부분 단일 패스 방식으로 컴파일하므로, C 표준에서는 여전히 함수의 원형을 정의하고 있다.

함수의 원형(prototype) 선언은 다음과 같은 방식으로 선언된다.

✅ 문법

반환타입 함수이름(매개변수타입);

📝 예제

다음 예제는 앞서 살펴본 예제에 함수의 원형(prototype) 선언을 추가한 예제이다.

이렇게 함수의 원형(prototype)은 main() 함수 앞에 미리 선언되어야 한다.

#include <stdio.h>

int bigNum (int, int); // 함수의 원형 선언

int main(void)
{
    int result;

    result = bigNum(3, 5); // 함수의 호출
    printf("두 수 중 더 큰수는 %d입니다.\n", result);
    result = bigNum(3, 1); // 함수의 호출
    printf("두 수 중 더 큰수는 %d입니다.\n", result);
    result = bigNum(7, 5); // 함수의 호출
    printf("두 수 중 더 큰수는 %d입니다.\n", result);
    return 0;
}

int bigNum(int num01, int num02) // 함수의 정의
{
    if (num01 >= num02)
    {
        return num01;
    }
    else
    {
        return num02;
    }
}
// 실행 결과
두 수 중 더 큰수는 5입니다.
두 수 중 더 큰수는 3입니다.
두 수 중 더 큰수는 7입니다.

실행 결과 오류 없이 정상 작동함을 알 수 있다.

변수의 유효 범위(variable scope)

C언어에서는 변수의 선언 위치에 따라 해당 변수의 유효 범위, 메모리 반환 시기, 초기화 여부, 저장되는 장소 등이 변경된다.

C언어에서 변수는 위와 같은 특징들을 기준으로 다음과 같이 나눌 수 있다.

  1. 지역 변수(local variable)

  2. 전역 변수(global variable)

  3. 정적 변수(static variable)

  4. 레지스터 변수(register variable)

메모리의 구조

컴퓨터의 운영체제는 프로그램의 실행을 위해 다양한 메모리 공간을 제공한다.

C 프로그램이 운영체제로부터 할당받는 대표적인 메모리 공간은 다음과 같다.

  1. 코드(code) 영역

  2. 데이터(data) 영역

  3. 스택(stack) 영역

  4. 힙(heap) 영역

지역 변수(local variable)

지역 변수란 '블록' 내에서 선언된 변수를 의미한다.

지역 변수는 변수가 선언된 블록 내에서만 유효하며, 블록이 종료되면 메모리에서 사라진다.

이러한 지역 변수는 메모리상의 스택(stack)영역에 저장되며, 초기화하지 않으면 의미 없는 값(쓰레기값)으로 초기화된다.

함수의 매개변수 또한 함수 내에서 정의되는 지역 변수로 취급된다.

📝 예제

#include <stdio.h>

void local(void);

int main(void)
{
    int i = 5;
    int var = 10;

    printf("main() 함수 내의 지역 변수 var의 값은 %d입니다.\n", var);
    if (i < 10)
    {
        local();
        int var = 30;
        printf("if 문 내의 지역 변수 var의 값은 %d입니다.\n", var);
    }
    printf("현재 지역 변수 var의 값은 %d입니다.\n", var);
    return 0;
}

void local(void)
{
    int var = 20;
    printf("local() 함수 내의 지역 변수 var의 값은 %d입니다.\n", var);
}
// 실행 결과
main() 함수 내의 지역 변수 var의 값은 10입니다.
local() 함수 내의 지역 변수 var의 값은 20입니다.
if 문 내의 지역 변수 var의 값은 30입니다.
현재 지역 변수 var의 값은 10입니다.

위의 예제에서 변수 var는 한 번은 main() 함수 내에서, 또 한 번은 if 문에서, 마지막은 local() 함수 내에서 선언된다.

이처럼 같은 이름의 변수 var은 모두 다른 중괄호({}) 영역에서 선언되었으며, 이러한 중괄호 영역을 블록(block)이라고 한다.

이렇게 변수의 유효 범위변수가 선언된 블록을 기준으로 설정되며, 해당 블록이 끝나면 모든 지역 변수는 메모리에서 사라지게 된다.

  • 🚩 위의 예제에서처럼 변수의 이름으로 같은 이름을 여러 번 사용하는 것은 구문 상 에러를 발생시키지는 않지만, 바람직하지 못한 방식이므로 지양하는것이 좋다.
  • 🚩 한 블록 내에서 같은 이름의 변수를 또다시 선언하려고 하면 컴파일러는 오류를 발생시킨다.

전역 변수(global variable)

전역 변수란 함수의 외부에서 선언된 변수를 의미한다.

전역 변수는 프로그램의 어디에서나 접근할 수 있으며, 프로그램이 종료되어야만 메모리에서 사라진다.

이러한 전역 변수는 메모리상의 데이터(data) 영역에 저장되며, 직접 초기화하지 않아도 0으로 자동 초기화된다.

📝 예제

#include <stdio.h>

void local(void);

int var; // 전역 변수 선언

int main(void)
{
    printf("전역 변수 var의 초깃값은 %d입니다.\n", var);
    int i = 5;
    int var = 10; // 지역 변수 선언
    printf("main() 함수 내의 지역 변수 var의 값은 %d입니다.\n", var);
    if (i < 10)
    {
        local();
        printf("현재 변수 var의 값은 %d입니다.\n", var); // 지역 변수에 접근
    }
    printf("더 이상 main() 함수에서는 전역 변수 var에 접근할 수가 없습니다.\n");
    return 0;
}

void local(void)
{
    var = 20; // 전역 변수의 값 변경
    printf("local() 함수 내에서 접근한 전역 변수 var의 값은 %d입니다.\n", var);
}

// 실행 결과
전역 변수 var의 초기값은 0입니다.
main() 함수 내의 지역 변수 var의 값은 10입니다.
local() 함수 내에서 접근한 전역 변수 var의 값은 20입니다.
현재 변수 var의 값은 10입니다.
더 이상 main() 함수에서는 전역 변수 var에 접근할 수가 없습니다.

위의 예제에서 전역 변수 var와 같은 이름의 지역 변수 var가 main() 함수 내부에서 선언된다.

이 지역 변수가 선언되기 전까지는 main() 함수에서도 전역 변수 var에 접근할 수 있다.

하지만 지역 변수 var가 선언된 후에는 main() 함수에서 전역 변수 var로 접근할 방법이 없어진다.

왜냐하면, 블록 내에서 선언된 지역 변수는 같은 이름의 전역 변수를 덮어쓰기 때문이다.

  • 🚩 따라서 이처럼 전역 변수와 같은 이름으로 지역 변수를 선언하는 것은 좋지 않다.

또한, 여러 개의 파일로 구성된 프로그램에서 외부 파일의 전역 변수를 사용하기 위해서는 extern 키워드를 사용해 다시 선언해야 줘야한다.

정적 변수(static variable)

C언어에서 정적 변수static 키워드로 선언한 변수를 의미한다.

이렇게 선언된 정적 변수지역 변수전역 변수의 특징을 모두 가지게 된다.

함수 내에서 선언된 정적 변수는 전역 변수처럼 단 한 번만 초기화되며(초기화는 최초 실행 시 단 한번만 수행됨), 프로그램이 종료되어야 메모리상에서 사라진다.

또한, 이렇게 선언된 정적 변수는 지역 변수처럼 해당 함수 내에서만 접근할 수 있다.

📝 예제

#include <stdio.h>

void local(void);
void staticVar(void);

int main(void)
{
    int i;

    for (i = 0; i < 3; i++)
    {
        local();
        staticVar();
    }
    return 0;
}

void local(void)
{
    int count = 1;
    printf("local() 함수가 %d 번째 호출되었습니다.\n", count);
    count++;
}

void staticVar(void)
{
    static int static_count = 1;  // ① 라인
    printf("staticVar() 함수가 %d 번째 호출되었습니다.\n", static_count);
    static_count++;
}

// 실행 결과
local() 함수가 1 번째 호출되었습니다.
staticVar() 함수가 1 번째 호출되었습니다.
local() 함수가 1 번째 호출되었습니다.
staticVar() 함수가 2 번째 호출되었습니다.
local() 함수가 1 번째 호출되었습니다.
staticVar() 함수가 3 번째 호출되었습니다.

위의 예제는 지역 변수로 선언된 count와 정적 변수로 선언된 static_count를 서로 비교하는 예제이다.

지역 변수인 count는 함수의 호출이 끝날 때마다 메모리상에서 사라진다.

하지만 정적 변수인 static_count는 함수의 호출이 끝나도 메모리상에서 사라지지 않고, 다음 함수 호출 때 이전의 데이터를 그대로 가지고 있다.

정적 변수 static_count의 초기화를 수행하는 ①번 라인의 코드는 최초로 실행될 때 단 한 번만 수행되며, 두 번째부터는 수행되지 않는다.

또한, static_count는 전역 변수와는 달리 자신이 선언된 staticVar() 함수 이외의 영역에서는 호출할 수 없다.

레지스터 변수(register variable)

레지스터 변수란 지역 변수를 선언할 때 register 키워드를 붙여 선언한 변수를 의미한다.

이렇게 선언된 레지스터 변수는 CPU의 레지스터(register) 메모리에 저장되어 빠르게 접근할 수 있게 된다.

하지만 컴퓨터의 레지스터는 매우 작은 크기의 메모리이므로, 이 영역에 변수를 선언하기 힘든 경우도 많다.

그럴 때 C 컴파일러는 해당 변수를 그냥 지역변수로 선언하게 된다.

🏷 변수의 종류

변수 종류키워드선언 위치유효 범위
지역 변수auto함수/블록의 내부함수/블록의 내부
전역 변수extern함수의 외부프로그램 전체
정적 변수static함수/블록의 내부함수/블록의 내부
레지스터 변수register함수/블록의 내부함수/블록의 내부

변수 종류메모리 소멸 시기초깃값저장 장소
지역 변수함수 종료시초기화되지 않음.스택(stack) 영역
전역 변수프로그램 종료시0으로 초기화됨.데이터(data) 영역
정적 변수프로그램 종료시0으로 초기화됨.데이터(data) 영역
레지스터 변수함수 종료시초기화되지 않음.CPU의 레지스터(register)

- 재귀 호출(recursive call)

함수란 하나의 작업을 수행하기 위해 독립적으로 설계된 프로그램 코드의 집합이다.

C 프로그램은 이러한 함수들로 구성되며, 포함된 함수들을 사용하여 프로그램의 목적을 달성하게 된다.

재귀 호출(recursive call)이란 함수 내부에서 함수가 자기 자신을 또다시 호출하는 행위를 의미한다.

이러한 재귀 호출은 자기가 자신을 계속해서 호출하므로, 끝없이 반복되게 된다.

따라서 함수 내에 재귀 호출을 중단하도록 조건이 변경될 명령문을 반드시 포함해야 한다.

프로그래밍을 처음 접하는 사람들은 이러한 재귀 호출이 왜 필요한가에 대해 이해하기 힘들 수도 있다.

하지만 재귀 호출은 알고리즘이나 자료 구조론에서는 매우 중요한 개념 중 하나이다.

또한, 재귀 호출을 사용하면 복잡한 문제도 매우 간단하게 논리적으로 접근하여 표현할 수 있다.

재귀 호출의 개념

재귀 호출의 개념을 파악하기 위해서 우선 재귀 호출을 사용하지 않고 1부터 n까지의 합을 구하는 함수를 만들어 보자.

📝 예제

int sum(int n) {
    int i;
    int result = 0;

    for (i = 1; i <= n; i++)
    {
        result += i;
    }
    return result;
}  

위의 예제에서 sum() 함수는 재귀 호출을 사용하지 않고 만든 함수다.

이러한 함수는 그냥 봐서는 그 목적을 바로 알 수 없으며, 코드를 해석해야 무슨 목적으로 만든 함수인지 알 수 있다.

즉 변수 i와 result는 왜 정의됐으며, for 문은 왜 사용되었는지 바로 알 수가 없다.


이제부터 재귀 호출을 사용하여 1부터 n까지의 합을 구하는 rSum() 함수를 만들어 보자

우선 1부터 4까지의 합을 구하는 알고리즘은 다음과 같다.

  1. 1부터 4까지의 합은 1부터 3까지의 합에 4를 더하면 됩니다.

  2. 1부터 3까지의 합은 1부터 2까지의 합에 3을 더하면 됩니다.

  3. 1부터 2까지의 합은 1부터 1까지의 합에 2를 더하면 됩니다.

  4. 1부터 1까지의 합은 그냥 1입니다.

위와 같이 논리적인 재귀 알고리즘을 구상하고 나면, 그것을 토대로 의사 코드를 작성할 수 있다.

위의 알고리즘을 의사 코드(pseudo code)로 작성하면 다음과 같다.

🏷 의사 코드

시작
    1. n이 1이 아니면, 1부터 (n-1)까지의 합에 n을 더한 값을 반환함.
    2. n이 1이면, 그냥 1을 반환함.
끝
  • 🚩 의사 코드(pseudo code)란 특정 프로그래밍 언어의 문법에 맞춰 작성된 것이 아닌, 일반적인 언어로 알고리즘을 표현한 코드를 의미한다.

작성한 의사코드를 재귀호출을 이요한 예제로 구현해 보자.

#include <stdio.h>

int rSum(int);

int main(void)
{
	int n = 5;
	
	printf("1부터 %d까지의 합은 %d입니다.\n", n, rSum(n));
	return 0;
}

int rSum(int n)
{
	if (n == 1)					// n이 1이면, 그냥 1을 반환함. 
	{
		return 1;
	}
	return n + rSum(n-1);	// n이 1이 아니면, n을 1부터 (n-1)까지의 합과 더한 값을 반환함.
}
// 실행 결과
1부터 5까지의 합은 15입니다.

위의 예제에서 if 문이 존재하지 않으면, 이 프로그램은 실행 직후 스택 오버플로우(stack overflow)에 의해 종료된다.

스택 오버플로우(stack overflow)란?

  스택 오버플로우(stack overflow)는 메모리 구조 중 스택(stack) 영역에서 해당 프로그램이 사용할 수 있는 메모리 공간 이상을 사용하려고 할 때 발생한다.

따라서 if 문처럼 재귀 호출을 중단하기 위한 조건문을 반드시 포함해야 한다.

이처럼 재귀 호출은 다양한 알고리즘을 표현한 의사 코드를 그대로 코드로 옮길 수 있게 해주고 직관적인 프로그래밍을 하는 데 많은 도움을 준다.

그러나 재귀 호출은 비재귀 호출보다 실행 시간이 오래 걸리는 단점을 가지고 있다.

재귀의 장, 단점

  • 👍 재귀호출의 장점 : 코드의 간결함

  • 👎 재귀호출의 단점 : 무한 재귀호출의 위험성, 성능 상의 문제

profile
Frontend SoftWare Engineer(2022.06.27 ~)

0개의 댓글