C언어 (3)
1. 함수(Function)
1-1. 함수란?
함수는 특정 작업을 수행하는 코드 묶음 이다. 반복되는 코드를 하나로 묶어 이름을 붙이고, 필요할 때마다 호출해서 사용한다.
JavaScript에서 function으로 함수를 만들어 쓰는 것과 같은 개념이다. 다만 C에서는 반환 타입을 명시해야 하고, 매개변수에도 타입을 지정해야 한다는 차이가 있다.
함수를 사용하는 이유는 크게 세 가지다.
코드 재사용 — 같은 로직을 여러 번 작성할 필요 없이, 한 번 정의하고 호출만 하면 된다. 가독성 — 복잡한 프로그램을 작은 단위로 쪼개면 코드를 읽고 이해하기 쉬워진다. 유지보수 — 로직을 수정할 때 함수 하나만 고치면 호출하는 곳 전체에 반영된다.
1-2. 함수의 형태
C언어의 함수는 반환타입, 함수이름, 매개변수, 함수 본체 로 구성된다.
반환타입 함수이름(매개변수) {
// 함수 본체
return 반환값;
}
실제 예시를 보면 이렇다.
int add(int a, int b) {
return a + b;
}
int 는 이 함수가 정수를 반환한다는 뜻이고, add 는 함수 이름이다. int a, int b 는 매개변수로, 함수를 호출할 때 전달받는 값이다. return a + b 로 결과를 돌려준다.
반환할 값이 없으면 반환타입을 void 로 지정한다. 이 경우 return 은 생략하거나 return; 만 쓴다.
void printHello(void) {
printf("Hello!\n");
}
매개변수가 없을 때는 괄호 안에 void 를 쓰거나 비워둔다. C에서는 void 를 명시하는 것이 정확한 표현이다. 괄호를 비워두면 "매개변수가 정해지지 않았다"는 의미가 되어 의도와 다를 수 있다.
1-3. 함수의 사용방법
함수를 사용하려면 선언(프로토타입), 정의(구현), 호출 세 단계를 알아야 한다.
#include <stdio.h>
int add(int a, int b); // 함수 선언 (프로토타입)
int main(void) {
int result = add(3, 5); // 함수 호출
printf("%d\n", result); // 8
return 0;
}
int add(int a, int b) { // 함수 정의 (구현)
return a + b;
}
함수 선언(프로토타입) 은 컴파일러에게 "이런 함수가 있을 것이다"라고 미리 알려주는 것이다. 반환타입, 함수이름, 매개변수 타입만 적고 세미콜론으로 끝낸다. main 보다 아래에 함수를 정의할 때 필요하다.
함수 정의 는 함수가 실제로 무슨 일을 하는지 구현하는 것이다.
함수 호출 은 정의된 함수를 실행하는 것이다. 함수 이름 뒤에 괄호를 쓰고 인자를 전달하면 된다.
만약 함수를 main 위에 정의하면 프로토타입 선언 없이도 사용할 수 있다.
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main(void) {
printf("%d\n", add(3, 5));
return 0;
}
C에서 함수의 인자 전달은 값에 의한 전달(call by value) 이다. 함수에 변수를 넘기면 원본이 아니라 복사본 이 전달된다. 함수 안에서 매개변수 값을 바꿔도 원본 변수에는 영향이 없다.
void change(int x) {
x = 100; // 복사본만 바뀜
}
int main(void) {
int a = 5;
change(a);
printf("%d\n", a); // 여전히 5
return 0;
}
원본을 수정하려면 포인터를 사용해서 주소를 넘겨야 한다. 이건 포인터를 배울 때 다시 다루게 된다.
1-4. 함수의 범위
함수의 범위란 함수가 어디서 호출될 수 있는지 를 의미한다.
기본적으로 같은 소스 파일 안에서는 어디서든 함수를 호출할 수 있다. 단, 호출하는 시점에서 컴파일러가 그 함수의 존재를 알고 있어야 한다. 그래서 프로토타입 선언을 위에 적거나, 함수 정의 자체를 호출보다 위에 두는 것이다.
여러 소스 파일로 나뉜 프로젝트에서는 헤더 파일(.h) 에 함수 프로토타입을 모아두고, 각 소스 파일에서 #include 로 포함시켜 사용한다.
// math_utils.h
int add(int a, int b);
int subtract(int a, int b);
// math_utils.c
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
// main.c
#include <stdio.h>
#include "math_utils.h"
int main(void) {
printf("%d\n", add(3, 5));
printf("%d\n", subtract(10, 4));
return 0;
}
함수 앞에 static 을 붙이면 해당 소스 파일 안에서만 사용할 수 있는 내부 함수 가 된다. 다른 파일에서는 호출할 수 없다.
static int helper(int x) {
return x * 2;
}
2. 변수
변수는 선언되는 위치와 키워드에 따라 생존 범위(scope) 와 생존 기간(lifetime) 이 달라진다.
2-1. 지역변수 (Local Variable)
함수나 블록({}) 안에서 선언된 변수다. 해당 블록 안에서만 사용할 수 있고, 블록이 끝나면 메모리에서 사라진다.
void foo(void) {
int x = 10; // 지역변수
printf("%d\n", x);
} // 여기서 x는 소멸
int main(void) {
foo();
// printf("%d\n", x); // 에러! x는 foo 안에서만 존재
return 0;
}
지역변수는 스택(stack) 메모리 에 할당된다. 함수가 호출되면 생성되고, 함수가 끝나면 자동으로 해제된다. 초기화하지 않으면 쓰레기값 이 들어있다.
같은 이름의 지역변수가 서로 다른 함수에 있어도 완전히 별개의 변수다.
void funcA(void) {
int x = 10;
printf("A: %d\n", x); // 10
}
void funcB(void) {
int x = 20;
printf("B: %d\n", x); // 20
}
if, for, while 같은 블록 안에서 선언한 변수도 지역변수다. 해당 블록이 끝나면 사라진다.
for (int i = 0; i < 5; i++) {
int temp = i * 2; // 매 반복마다 생성/소멸
}
// i와 temp 모두 여기서 사용 불가
2-2. 전역변수 (Global Variable)
함수 바깥에서 선언된 변수다. 프로그램이 시작될 때 생성되고, 프로그램이 종료될 때까지 유지된다. 모든 함수에서 접근할 수 있다.
#include <stdio.h>
int count = 0; // 전역변수
void increment(void) {
count++;
}
void printCount(void) {
printf("count: %d\n", count);
}
int main(void) {
increment();
increment();
increment();
printCount(); // count: 3
return 0;
}
전역변수는 데이터 영역(data segment) 에 할당된다. 초기화하지 않으면 지역변수와 달리 자동으로 0으로 초기화 된다.
편리해 보이지만 전역변수의 남용은 위험하다. 어디서든 값을 바꿀 수 있기 때문에, 프로그램이 커지면 어떤 함수가 값을 변경했는지 추적하기 어려워진다. 버그의 원인이 되기 쉽고 유지보수도 힘들어진다. 꼭 필요한 경우가 아니라면 지역변수를 사용하고, 함수 간 데이터 전달은 매개변수와 반환값으로 처리하는 것이 좋다.
전역변수와 지역변수의 이름이 같으면 지역변수가 우선 한다.
int x = 100; // 전역변수
void foo(void) {
int x = 5; // 지역변수가 전역변수를 가림
printf("%d\n", x); // 5
}
2-3. 정적변수 (Static Variable)
지역변수에 static 키워드를 붙이면 정적변수가 된다. 지역변수처럼 해당 함수 안에서만 접근 할 수 있지만, 함수가 끝나도 값이 사라지지 않고 유지 된다.
void counter(void) {
static int count = 0; // 최초 호출 때만 초기화됨
count++;
printf("호출 횟수: %d\n", count);
}
int main(void) {
counter(); // 호출 횟수: 1
counter(); // 호출 횟수: 2
counter(); // 호출 횟수: 3
return 0;
}
일반 지역변수였다면 함수가 호출될 때마다 count가 0으로 초기화되어 항상 1이 출력될 것이다. 하지만 static 으로 선언했기 때문에 초기화는 처음 한 번만 실행되고, 이후에는 이전 값이 그대로 유지된다.
정적변수는 전역변수와 마찬가지로 데이터 영역 에 저장된다. 초기화하지 않으면 자동으로 0이 된다. 다만 접근 범위는 선언된 함수 안으로 제한되므로, 전역변수의 단점(어디서든 접근 가능)을 피하면서도 값을 유지할 수 있다.
정리하면 이렇다.
지역변수 는 함수 안에서만 접근 가능하고, 함수가 끝나면 사라진다. 전역변수 는 어디서든 접근 가능하고, 프로그램이 끝날 때까지 유지된다. 정적변수 는 함수 안에서만 접근 가능하지만, 프로그램이 끝날 때까지 값이 유지된다. 지역변수의 스코프와 전역변수의 수명을 합쳐놓은 것이라고 보면 된다.
마무리
함수는 코드를 구조화하는 가장 기본적인 단위이고, 변수의 범위는 프로그램의 데이터 흐름을 결정한다. 함수와 변수의 스코프를 정확히 이해하면, 코드가 길어져도 데이터가 어디에 있고 어디까지 영향을 미치는지 파악할 수 있다. 이 개념은 이후 포인터와 동적 메모리 할당을 배울 때 더욱 중요해진다.