[C언어] 함수

김민정·2024년 8월 2일
0
post-thumbnail

Chapter 09. C언어의 핵심! 함수!

C언어의 핵심을 포인터로 생각하는 경우가 많다.
하지만 C언어의 핵심은 포인터가 아닌 '함수'다.
함수를 잘 정의하는 것도 중요하고, 잘 정의된 함수를 가져다 쓰는 것도 중요하다.

09-1 "함수를 정의하고 선언하기"

프로그램의 구현은 복잡한 문제를 해결하는 것에 비유할 수 있다.
크고 복잡한 문제를 작게 나워서 하나씩 해결해 나가는 것이 보다 빠르게, 그리고 정확히 문제를 해결하는 원칙이 된다.

"Divide"Divide andand Conquer!"Conquer!"

프로그램을 작은 크기의 함수들로 나눠서 구현하게 되면, 문제의 발생 및 프로그램의 요구사항 변경으로 인한 소스코드의 변경이 필요한 경우에, 변경의 범위를 축소 및 제한할 수 있다.

함수의 입력과 출력: printf 함수 반환

이전 Chapter 2에서 입력(전달인자)와 그에 따른 적절한 출력(반환 값)이 존재하는 것이 함수라고 했다.
그러나 C언어의 함수는 전달인자가 없거나 반환 값이 없는 경우도 있다.
printf 함수를 예로 들면 전달되는 인자정보들을 참조하여 데이터를 모니터에 출력하는 기능을 지닌다. 따라서 굳이 값을 반환할 필요가 없다.
따라서, 함수는 "입력 또는 출력이 없는 함수도 존재하며 만들 수 있다."
실제로 printf 함수는 값을 반화하지 않을까? 아래 예제를 살펴보자.

#include <stdio.h>
int main()
{
	int num1, num2;
	num1 = printf("12345\n");	// printf함수를 먼저 실행하고 그 반환하는 값을 num1에 저장.
	num2 = printf("I love my home\n");
	printf("%d %d \n", num1, num2);
	return 0;
}

> 출력
12345
I love my home
6 15

printf 함수\n문자를 포함하여 모니터에 출력한 문자열의 길이를 반환한다.

함수의 형태

전달인자의 유무와 반환 값의 유무에 따라서 함수를 네 개의 형태로 나눈다.

유형 1 : 전달 인자 O, 반환 값 O
유형 2 : 전달 인자 O, 반환 값 X
유형 3 : 전달 인자 X, 반환 값 O
유형 4 : 전달 인자 X, 반환 값 X
(반환 값이 없다 = return이 없다)

전달 인자와 반환값이 모두 있는 함수 정의

가장 일반적인 형태의 함수다.
예시로 덧셈 기능을 기지는 함수를 정의해보겠다.
전달인자는 2개가 되어야하고 덧셈한 이후 결과를 보여줘야하기 때문에 반환 값도 존재한다.
따라서 구현할 함수의 특징을 정리하자면, 1) 전달인자는 int형 정수 둘, 덧셈 진행 (기능), 2) 덧셈 결과 반환, 반환형도 int로 선언 (출력), 3) 함수 이름 설정

#include <stdio.h>

int Add(int num1, int num2)	// 인자 전달 O, 반환 값 X
{
	return num1 + num2;	// 덧셈이 선 진행되고 그 결과가 반환됨.
}

그럼 만든 함수를 호출해보자.

int main()
{
	int result;
	result = Add(3, 4);	// = 대입 전에 Add 함수 호출 먼저 진행 되고 그 값이 result에 저장.
	printf("덧셈결과1: %d \n", result);
	result = Add(5, 8);
	printf("덧셈결과2: %d \n", result);
	return 0;
}

> 출력
덧셈결과1: 7
덧셈결과2: 13

Add(3, 4)Add(5, 8)에서 함수의 호출문이 반환 값으로 대체되는 것을 알 수 있다.

전달 인자와 반환값이 존재하지 않는 함수 정의

1. 반환값이 존재하지 않는 함수
반환값이 없기 때문에 반환형을 지정할 필요가 없다. 그리고 반환하는 return문도 없다.

void ShowAddResult(int num)	// 인자 전달 O, 반환 값 X
{
	printf("덧셈결과 출력: %d \n", num);
}

2. 전달 인자가 존재하지 않는 함수
전달 인자가 없어 매개 변수 부분이 void다.

int ReadNum(void)	// 인자 전달 X, 반환 값 O
{
	int num;
	scanf_s("%d", &num);
	return num;
}

3. 전달 인자, 반환 값 모두 존재하지 않는 함수
단순히 메시지를 전달하는 함수로 전달 인자나 반환 값 모두 없다.

void HowToUseThisProg(void)	// 인자 전달 X, 반환 값 X
{
	printf("두 개의 정수를 입력하시면 덧셈결과가 출력됩니다. \n");
	printf("자! 그럼 두 개의 정수를 입력하세요. \n");
}

위에 나온 함수 3가지를 모두 실행해본다면 아래와 같다.

int main()
{
	int result, num1, num2;
	HowToUseThisProg();
	num1 = ReadNum();
	num2 = ReadNum();
	result = Add(num1, num2);
	ShowAddResult(result);
	return 0;
}

> 출력
두 개의 정수를 입력하시면 덧셈결과가 출력됩니다.! 그럼 두 개의 정수를 입력하세요.
12 24
덧셈결과 출력: 36

*return의 의미**

키워드 return은 '값을 반환하면서 함수를 빠져나간다'라는 의미로 사용된다.
간혹 반환형이 void로 선언된 함수에서는 return문을 사용할 수 없는 것으로 하는데, 사용할 수 있다! 반환값을 적어주지 않으면 된다.

void NoReturnType(int num)
{
	if (num < 0)
		return;	// 값을 반환하지 않는 return 문!
}

함수의 정의와 그에 따른 원형의 선언

함수의 위치를 결정할 때도 주의를 기울여야 한다.

컴파일이 위에서 아래로 진행되기 때문에 컴파일 이전에 함수를 정의해 이후에 호출을 해줘야한다.
하지만 무조건 함수를 정의한 다음에 호출할 수 있는 것은 아니다.

함수를 앞서 선언한 이후 함수를 정의해도 가능하다.
아래는 그 순서다.

참고로 함수의 정의가 뒤에 이루어지는 경우, 함수의 선언에는 매개변수의 갯수 및 자료형 정보만 포함되면 되기 때문에 매개변수의 이름을 생략해서 선언하는 것이 가능하다.

int Increment(int);	// 함수의 선언

하나의 함수 내에 둘 이상의 return문이 존재하는 경우

조건문이 있는 경우 하나의 함수 내에서 return 문이 2개 존재한다.
return문을 통해 값을 반환하면서 함수를 빠져나가는 기본 원칙을 이용한 것이다.

#include <stdio.h>
int NumberCompare(int num1, int num2);

int main()
{
	printf("3과 4중에서 큰 수는 %d 이다.\n", NumberCompare(3, 4));	// 값이 아니라 함수 호출문이 온 경우
	printf("7과 2중에서 큰 수는 %d 이다.\n", NumberCompare(7, 2));	// 함수에서 반환하는 값이 들어가게 된다. 
	return 0;
}

int NumberCompare(int num1, int num2)
{
	if (num1 > num2)
		return num1;
	else
		return num2;
}

> 출력
34중에서 큰 수는 4 이다.
72중에서 큰 수는 7 이다.

이번엔 절댓값의 크기를 비교하는 함수를 정의해 실행해보도록 하겠다.

int AbsoCompare(int num1, int num2);
int GetAbsoValue(int num);

int main()
{
	int num1, num2;
	printf("두 개의 정수 입력: ");
	scanf_s("%d %d", &num1, &num2);
	printf("%d와 %d중 절댓값이 큰 정수: %d \n",
		num1, num2, AbsoCompare(num1, num2));
	return 0;
}

int AbsoCompare(int num1, int num2)	// 절댓값 비교하는 함수
{
	if (GetAbsoValue(num1) > GetAbsoValue(num2))
		return num1;
	else
		return num2;
}

int GetAbsoValue(int num)	// 절댓값으로 변환하는 함수
{
	if (num < 0)
		return num * (-1);
	else
		return num;
}

> 출력
두 개의 정수 입력: 5 -9
5-9중 절댓값이 큰 정수: -9

위에서 보면 main 함수가 아닌 AbsoCompare이란 함수에서도 GetAbsoValue 함수를 호출하여 사용한 것을 알 수 있다. 다른 함수에서도 필요에 따라서 얼마든지 함수를 호출할 수 있다.


09-2 "변수의 존재기간과 접근범위 1: 지역변수"

변수의 선언위치와 함수는 깊은 관계가 있다.
변수는 선언되는 위치에 따라서 크게 전역 변수지역 변수로 나뉜다.
이 둘은 다음 두 가지에 대해서 차이점을 보인다.

  1. 메모리 상에 존재하는 기간
  2. 변수에 접근할 수 있는 범위

지역변수

지역 변수란? 중괄호 내에 선언되는 변수다. 선언된 지역 내에서만 유효하다는 특성이 있다.

#include <stdio.h>
int SimpleFuncOne()
{
	int num = 10;	// 이후부터 SimpleFuncOne의 num 유효
	num++;
	printf("SimpleFuncOne Num: %d \n", num);
	return 0;	// SimpleFuncOne 의 num이 유효한 마지막 문장. 이후 num 메모리 소멸
}

int SimpleFuncTwo()
{
	int num1 = 20;	// 이후부터 num1 유효.
	int num2 = 30;	// 이후부터 num2 유효.
	num1++, num2--;
	printf("num1 & num2: %d %d \n", num1, num2);
	return 0;	// num1, num2 유효한 마지막 문장. 이후 메모리 소멸
}

int main()
{
	int num = 17;	// 이후부터 main의 num 유효
	SimpleFuncOne();
	SimpleFuncTwo();
	printf("main num: %d \n", num);
	return 0;	// main의 num이 유효한 마지막 문장. 소멸
}

> 출력
SimpleFuncOne Num: 11
num1 & num2: 21 29
main num: 17

SimpleFuncOne 함수에서 선언된 변수 num은 함수의 중괄호 안에 선언되었으므로 지역변수다.
따라서 이 변수는 SimpleFuncOne 함수를 빠져나가가기 직전까지 유효하며 해당 지역을 벗어나면 자동으로 소멸된다.
또한, 지역 변수는 선언된 지역 내에서만 유효하기 때문에 선언된 지역이 다르면 변수명이 같아도 상관없다.

지역변수의 할당과 소멸 과정
지역변수 자체를 이해하는데 있어 지역변수의 생성 및 소멸의 과정을 이해하는 것은 중요하다.
Chapter 25에서는 메모리 구조에 대해 자세히 배울 예정이다. 아래는 지역변수가 할당되는 메모리의 특징이다.

  • 지역변수는 '스택(stack)'이라는 메모리 영역에 할당된다.
  • 지역변수는 접시 쌓듯이 할당된다.

위 예제에서 지역변수의 할당과 소멸 순서는 다음과 같다.

  1. main 함수의 호출
    : 변수 num이 main함수 내 선언되어 main 함수가 종료될 때까지 메모리상에 존재한다.

  2. SimpleFuncOne 함수의 호출과 종료(반환)
    : SimpleFuncOne 함수가 호출되어 실행되면 SimpleFuncOne 함수 내에서 변수 num이 할당되고 초기화 된다. return문을 실행하며 main 함수로 돌아가게 되면 SimpleFuncOne 함수에서 선언된 변수 num은 메모리 공간에서 사라지게 된다.

  3. SimpleFuncTwo 함수의 호출과 종료(반환)
    : SimpleFuncTwo 함수가 호출되어 실행되면 SimpleFuncTwo 함수 내에서 변수 num1과 num2가 할당되고 초기화된다. SimpleFuncTwo 함수도 SimpleFuncOne 함수와 마찬가지고 return문이 실행되면서 main 함수로 돌아가게 되고 선언된 변수들은 메모리 공간에서 사라지게 된다.

  4. main 함수의 종료(반환)
    : main 함수의 변수 num이 마지막으로 소멸된다.

이러한 과정을 통해 알 수 있는 것은
지역변수는 해당 선언문이 실행될 때 메모리 공간에 할당되었다가, 선언문이 존재하는 함수가 종료(반환)를 하면 메모리 공간에서 소멸한다.

지역변수는 반복문이나 조건문에서도 선언이 가능하다.

#include <stdio.h>
int main()
{
	int cnt;
	for (cnt = 0; cnt < 3; cnt++)
	{
		int num = 0;
		num++;
		printf("%d번째 반복, 지역변수 num은 %d. \n", cnt + 1, num);
	}
	if (cnt == 3)
	{
		int num = 7;
		num++;
		printf("if문 내에 존재하는 지역변수 num은 %d. \n", num);
	}
	return 0;
}

> 출력
1번째 반복, 지역변수 num은 1.
2번째 반복, 지역변수 num은 1.
3번째 반복, 지역변수 num은 1.
if문 내에 존재하는 지역변수 num은 8.


for문의 중괄호 내에 선언되었기 때문에 for문 내에서 유효한 지역변수가 된다.
그리고 for문에 의한 반복은 중괄호 내에서 이뤄지는 것이 아닌, 중괄호의 진입과 탈출을 반복하면서 이뤄지는 것을 확인할 수 있다. 따라서 1~3번째 반복에서 num은 계속 1인 것이다.

if문의 중괄호 내에 변수도 if문 내에서만 유효한 지역변수가 된다.

다음 예제는 지역변수가 외부에서 선언된 동일한 이름의 변수를 가리게 되는 것을 확인할 수 있다.

#include <stdio.h>
int main()
{
	int num = 1;
	if (num == 1)
	{
		int num = 7;	// 이 행 주석 처리시 출력 : 11, 11 / 냅둘시 출력: 17, 1
		num += 10;
		printf("if문 내 지역변수 num: %d \n", num);
	}
	printf("main 함수 내 지역변수 num: %d \n", num);
	return 0;
}

> 출력
// 주석 처리 X
if문 내 지역변수 num: 17
main 함수 내 지역변수 num: 1

// 주석 처리 O
if문 내 지역변수 num: 11
main 함수 내 지역변수 num: 11

if문 내에서 num을 새로 할당하고 초기화했기 때문에 main 함수의 num이 가려진다.

매개변수

함수를 정의할 때 선언하는 매개변수도 지역변수의 일종이다.
따라서 매개변수 역시 다음 특성을 가진다.

  • 선언된 함수 내에서만 접근이 가능하다.
  • 선언된 함수가 반환을 하면, 지역변수와 마찬가지로 소멸이 된다.

이 둘의 관계는 '매개변수 ∈ 지역변수'가 된다.
+) 지역변수를 자동변수(automatic variable)이라고도 한다.


09-3 "변수의 존재기간과 접근범위 2: 전역변수, static 변수, register 변수"

전역변수

전역변수란? 처음 실행되면 메모리 공간에 할당되어 프로그램이 종료될 때까지 메모리 공간에 남아있는 변수다.
전역변수는 지역변수와 달리 어디서든 접근이 가능한 변수이기 때문에 중괄호 내에 선언되지 않는다.

#include <stdio.h>
void Add(int val);
int num;	// 전역변수

int main()
{
	printf("num: %d \n", num);
	Add(3);
	printf("num: %d \n", num);
	num++;
	printf("num: %d \n", num);
	return 0;
}

void Add(int val)
{
	num += val;	// 전역변수 num의 값 val만큼 증가.
}

/*예상 출력
0
3
4
*/

> 출력
num: 0
num: 3
num: 4

예제를 통해 알 수 있듯이 전역변수는 다음의 특징을 지닌다.

  1. 프로그램의 시작과 동시에 메모리 공간에 할당되어 종료 시까지 존재한다.
  2. 별도의 값으로 초기화하지 않으면 0으로 초기화된다.
  3. 프로그램 전체 영역 어디서든 접근이 가능하다.

만약에 전역변수와 동일한 이름의 지역변수가 선언되면 어떻게 될까?
해당 지역에서는 전역변수가 가려지고, 지역변수로서의 접근이 이루어진다.

다음 예제를 통해 확인해보자.

#include <stdio.h>
int Add(int val);
int num = 1;

int main()
{
	int num = 5;	// 지역 변수 선언 및 초기화
	printf("num: %d \n", Add(3));
	printf("num: %d \n", num + 9);
	return 0;
}

int Add(int val)
{
	int num = 9;
	num += val;
	return num;
}

/*예상 출력
12
14
*/

> 출력
num: 12
num: 14

예제를 통해서도 동일한 이름의 지역변수는 해당 지역에서는 전역변수가 가려지고, 지역변수로서의 접근이 이루어진다는 것을 확인할 수 있다.
하지만 가급적이면 변수의 이름을 달리해서 지역변수와 전역변수를 헷갈릴만한 상황을 만들지 않는 것이 좋다.

전역변수는 중간에 메모리상에서 소멸되지도 않고 어느 지역이든 접근이 가능하니 무적의 변수로 보이기도 한다. 하지만 전역변수의 선언은 가급적 제한해야한다. 왜냐하면 전역변수를 많이 사용하게 된다면 프로그램의 구조를 복잡하게 만들기 때문이다.
아래 그림과 같이 총 26개의 함수를 사용하는데 모두 전역변수라면 생기는 함수의 전역변수 접근 형태다.

이런 구조에서 전역변수의 변경은 다수의 다른 함수까지 변경, 혹은 전체 프로그램의 변경을 요구한다. (위와 같은 코드를 '스파게티 코드(spaghetti Code)'라고 한다.)
따라서 전역변수의 수가 증가하면 그만큼 프로그램은 복잡해지며, 좋은 구조의 프로그램과 거리가 멀어지게 되기 때문에 전역변수의 선언은 그만큼 신중해야 한다.

static 변수

static 변수란? 전역변수와 지역변수 앞에 static 선언을 한 변수다.
(지역변수의 static 선언에 대해 설명하고 전역변수의 static 선언은 추후에 배울 예정이다.)

지역변수에 static 선언이 붙게 되며 특성은 다음과 같다.

  1. 선언된 함수 내에서만 접근이 가능하다. (지역변수 특성)
  2. 딱 1회 초기화되고 프로그램 종료시까지 메모리 공간에 존재한다. (전역변수 특성)

예제를 통해 특성을 확인해보자.

#include <stdio.h>

SimpleFunc()
{
	static int num1 = 0;
	int num2 = 0;
	num1++, num2++;
	printf("static: %d, local: %d \n", num1, num2);
}

int main()
{
	int i;
	for (i = 0; i < 3; i++)
		SimpleFunc();
	return 0;
}

/*예상 출력
s: 1, l: 1
s: 2, l: 1
s: 3, l: 1
*/

> 출력
static: 1, local: 1
static: 2, local: 1
static: 3, local: 1

예제를 통해 static으로 선언된 지역변수는 전역변수와 동일한 시기에 할당되고 소멸된다는 것을 알 수 있다.
단, 지역변수와 마찬가지로 선언된 함수 내에서만 접근이 가능하다.

static 지역변수는 전역변수보다 좋다(안정적이다).
전역변수와 마찬가지로 프로그램이 종료될 때까지 메모리 공간에 남아있지만, 접근할 수 있는 범위를 하나의 함수로 제한했기 때문이다.
static 지역변수를 전역변수로 대체할 일은 없어야하고, 반대로 전역변수를 static 지역변수로 대체할 수 있다면 대체해서 프로그램의 안정성을 높여야한다.
(static 지역변수를 static 변수라고도 한다.)

register 변수

register 변수란? 지역변수에 register라는 선언을 추가한 변수인데 이는 '레지스터'라는 메모리 공간에 저장될 확률이 높아진다.

// register 변수 예시
int SoSimple()
{
	register int num=3;
    ...
}

레지스터는 CPU내에 존재하는 크기가 매우 작은 메모리다. 하지만 CPU내에 존재하기 때문에 이 메모리에 저장된 데이터를 대상으로 하는 연산은 매우 빠르다.
컴파일러는 이 register 선언을 힌트로 보고 레지스터의 활용여부를 결정한다.
register 선언을 추가해도 컴파일러가 합당하지 않다고 판단하면 레지스터에 할당되지 않는다.
전역변수에는 register 선언을 추가할 수 없다.


09-4 "재귀함수에 대한 이해"

재귀함수의 기본적인 이해

재귀함수란? 함수 내에서 자기 자신을 다시 호출하는 함수다.

위 그림과 같이 재귀함수는 함수 내에서 본인을 다시 호출하는데 처음으로 다시 되돌아가서 실행되는 것이 아닌, 함수가 호출되면 해당 함수의 복사본을 만들어서 실행하는 구조다.
위 그림 예시는 '재귀의 탈출조건'이 없는데 이를 추가하여 예제를 확인해보자.

#include <stdio.h>

void Recursive(int num)
{
	if (num <= 0)	// 재귀의 탈출 조건
		return;	// 재귀의 탈출!
	printf("Recursive Call! %d \n", num);
	Recursive(num - 1);
}
int main()
{
	Recursive(3);
	return 0;
}

> 출력
Recursive Call! 3
Recursive Call! 2
Recursive Call! 1

이 예제에서 재귀의 탈출 조건은 num이 0보다 같거나 작아졌을 때다.
그렇다면 탈줄 조건이 있을 때 재귀함수의 실행 흐름은 어떻게 될까?

호출은 원래순서대로 가다가 탈출 조건을 마주쳤을 때 역순으로 반환된다.
이렇듯 재귀함수를 정의하는데 있어서 탈출조건을 구성하는 것은 매우 중요한 일이다.

재귀함수의 디자인 사례

재귀함수의 특징을 사용한 예시가 팩토리얼(factorial)값을 반환하는 함수다.

팩토리얼 식은 아래와 같고,

수학적 표현은 아래와 같다.

이를 코드로 표현하면 아래와 같다.

if(n==0)
	return 1;
else
	return n * Factorial(n-1);

실제로 정확히 계산하는지 함수로 표현하여 확인해보자.

#include <stdio.h>

int Factorial(int n)
{
	if (n == 0)
		return 1;
	else
		return n * Factorial(n - 1);
}

int main()
{
	printf("1! = %d \n", Factorial(1));
	printf("2! = %d \n", Factorial(2));
	printf("3! = %d \n", Factorial(3));
	printf("4! = %d \n", Factorial(4));
	printf("9! = %d \n", Factorial(9));
	return 0;
}

> 출력
1! = 1
2! = 2
3! = 6
4! = 24
9! = 362880

<Review>

사실 함수를 처음 배웠을 때 이해가 가질 않았다.
수학의 함수를 개념이 살짝 다른 느낌,,,?
수학의 함수는 한줄로 정의할 수 있었지만 이건 여러 계산을 통해 내가 원하는 결과를 반환해야해서 처음엔 이 구조가 이해가 잘 되지 않았다.

하지만 파이썬을 배우고 프로젝트들을 해보면서 이해할 수 있게 되었다...!
이렇게라도 다시 C언어의 함수를 공부할 수 있게 되어 다행이라 생각한다.

다음 Chapter엔 도전! 프로그래밍! 이다 ㅎㅎㅎ
처음에 엄청 쫄면서 풀었던 기억이✨ 앞으로도 화이팅~!

profile
백엔드 코린이😁

0개의 댓글

관련 채용 정보