-
Notifications
You must be signed in to change notification settings - Fork 1
00_02.Declarations
C언어에서 변수와 함수를 선언 (declaration)하는 것은 매우 중요한데, 우리는 선언을 통해 컴파일러가 프로그램을 컴파일하기 위해 필요한 정보를 제공한다. 또한 변수나 함수를 선언할 때는 auto, const, void, int 등의 선언 지정자 (declaration specifier)를 통해 추가적인 특성을 부여할 수 있다. 변수와 함수의 선언은 다음과 같은 형태로 이루어진다.
[기억 클래스 지정자] [자료형 한정자] (자료형 지정자) <선언자>;
선언 지정자는 우리가 선언하는 변수나 함수의 특징을 설명하는 키워드로, [기억 클래스 지정자], [자료형 한정자]와 (자료형 지정자)가 여기에 해당된다. 또한 선언자 (declarator)는 변수나 함수의 이름 (식별자)의 앞이나 뒤에 [], *나 ()가 추가적으로 붙은 것을 가리키는 단어로, 우리가 선언하는 것이 배열인지, 포인터인지, 함수인지를 나타낸다. 뭔가 이해하기 어렵다고? 아래 예시를 한번 보자.
static const float DELTA_TIME;여기서 static, const, float는 변수의 특징을 설명해주는 키워드이기 때문에, 선언 지정자이다. 또한 DELTA_TIME은 이것이 배열이나 포인터가 아닌 그냥 변수임을 나타내는 선언자임을 알 수 있다.
const char name[MAX_LENGTH] = "6PM";여기서 const, char는 선언 지정자이고, name[MAX_LENGTH]는 선언자이며, "6PM"은 변수를 초기화해주는 역할을 한다.
static void LoadGameData(void);여기서 static과 void는 선언 지정자이고, LoadGameData(void)는 선언자이다.
C언어의 기억 클래스 지정자 (storage class specifier)를 이해하기 위해서는, 변수의 3가지 특성에 대해 먼저 알아야 한다.
- 수명 (lifetime): 모든 변수는 언제 메모리가 할당되고 해제되는지가 정해져 있는데, 이것을 변수의 수명이라고 한다. 일반적으로 우리가 선언한 변수는 그 변수가 속한 블록 (block)이 실행되었을 때 메모리가 할당되고, 블록이 끝날 때 메모리가 해제된다.
void do_something(void) {
// 함수가 호출되면, `value`를 위한 메모리가 할당될 것이다.
int value = 10;
/* ... */
} // 함수 실행이 끝났으므로, 더 이상 `value`에 접근할 수 없다.- 유효 범위 (scope): 우리가 변수를 참조할 수 있는 소스 코드의 일부분을 변수의 유효 범위라고 하며, 변수는 프로그램에 어느 부분에 선언되었는지에 따라 다른 유효 범위를 가진다. 변수가 블록 안에 있으면 프로그램 실행 중에 블록을 벗어났을 때 더 이상 접근할 수 없는데, 이것을 변수가 블록 유효 범위 (block scope)를 가지고 있다고 한다. 또한 변수가 블록 밖에 있는 경우, 즉 전역 변수 (global variable)인 경우에는 그 소스 파일의 모든 곳에서 접근할 수 있는데, 이것을 변수가 파일 유효 범위 (file scope)를 가지고 있다고 한다.
int value = 0;
/* ... */
void do_something(void) {
// `value`는 파일 유효 범위를 가지기 때문에, 모든 함수에서 접근할 수 있다.
value = -1;
/* ... */
}- 연결성 (linkage): 변수의 연결성은 변수가 프로그램의 다른 부분과 얼마만큼 공유될 수 있는지를 결정하는 특성이다. 변수가 외부 연결성 (external linkage)를 가질 경우, 이 변수는 프로그램의 다른 소스 파일에서도 접근할 수 있게 된다. 또한 변수가 내부 연결성 (internal linkage)를 가질 경우에는 그 변수가 속한 소스 파일에서만 변수에 접근할 수 있다. 마지막으로 연결성 특성이 없는 변수는 그 변수가 포함된 함수에만 속하고, 함수 이외의 다른 곳에서 접근할 수 없다.
-
auto기억 클래스 지정자:auto기억 클래스 지정자는 블록 안에 있는 변수에만 붙일 수 있으며,auto로 선언된 변수는 블록을 벗어나면 더 이상 접근할 수 없게 된다. 또한auto변수는 블록 유효 범위를 가지고, 연결성 특성이 없어 그 블록에서만 접근할 수 있다. 기억 클래스 지정자가 따로 없는 변수는 컴파일러가 자동으로auto키워드를 붙여주기 때문에, 변수 선언 시에auto키워드를 따로 붙일 일은 없다고 생각하면 된다.
void do_something(void) {
// `auto int value;`랑 같은 뜻이다.
int value;
/* ... */
}-
static기억 클래스 지정자:static기억 클래스 지정자는 변수가 선언된 위치에 관계없이 사용 가능하며,static으로 선언된 모든 변수는 프로그램이 끝날 때까지 메모리가 해제되지 않고 계속 유지된다. 하지만static변수는 어디에서 선언되었는지에 따라 그 변수의 특성이 조금씩 달라지는데, 블록 밖에서static으로 선언된 변수는 파일 유효 범위와 내부 연결성을 가지지만, 블록 안에서static으로 선언된 변수는 블록 유효 범위를 가지고 연결성 특성은 가지지 않는다.static으로 선언된 변수의 가장 큰 특징은auto로 선언된 일반 변수와 다르게, 프로그램이 실행되기 전에 단 한 번만 초기화된다는 것이다. 또한, 함수를 순환/재귀 호출 (recursion)할 때마다auto로 선언된 변수는 새로 생성이 되지만,static으로 선언된 변수는 모든 함수 호출에서 같은 값을 가지게 된다.
/*
`x`는 파일 유효 범위와 내부 연결성을 가지기 때문에,
이 파일의 모든 곳에서 접근할 수 있다.
*/
static int x;
void do_something(void) {
/*
`value`는 블록 유효 범위를 가지고 연결성 특성은
가지지 않기 때문에, 이 함수 외의 다른 곳에서
`value`에 접근할 수 없다.
*/
static int value;
// `x`에 마음대로 접근이 가능하다.
x = 100;
/* ... */
} -
extern기억 클래스 지정자:extern기억 클래스 지정자는 여러 개의 소스 파일이 변수를 공유할 수 있게 해준다.extern키워드는 컴파일러에게 "이 변수는 다른 소스 파일에 정의되어 있으니, 알아서 찾아줘"라고 알려주는 역할을 한다. 그러면 컴파일러는 "음... 다른 소스 파일에 정의되어 있으니 여기서는 이 변수 선언에 메모리를 할당할 필요가 없겠군!"이라고 판단하게 된다.extern기억 클래스 지정자를 사용할 때 주의할 점은,extern int value;대신extern int value = 10;처럼 그 변수를 초기화하려고 하면 안된다는 것인데, 그 이유는 C언어에서 변수는 여러 번 선언되는 것이 가능하지만, 그 변수의 정의는 단 하나만 존재해야 하기 때문이다.extern으로 선언된 변수는static과 비슷하게 프로그램이 끝날 때까지 그 변수의 메모리가 해제되지 않고 계속 유지되며, 파일 유효 범위 또는 블록 유효 범위를 가진다.
/* main.c */
/*
`extern int value;`는 컴파일러에게 `value`가 다른 소스 파일에
정의되어 있으니 메모리를 할당할 필요가 없다고 알려준다.
*/
extern int value;/* secret.c */
// 실제 `value`의 값은 여기에 정의되어 있다.
int value = 100;-
register기억 클래스 지정자:register기억 클래스 지정자는 컴파일러에게 "이 변수는 메모리에 보관하지 말고, CPU의 레지스터에 저장해줘"라고 부탁하는 기능을 수행한다. 사실 이것은 명령이 아니라 부탁이기 때문에, 컴파일러가 이 변수를 실제로 레지스터에 저장하지 않을 수도 있다.register기억 클래스 지정자는auto와 거의 모든 특성이 똑같지만, CPU의 레지스터는 메모리와는 다르게 주소가 따로 없기 때문에, 변수의 메모리 주소를 얻을 때 사용하는 주소 연산자 (&)를 사용할 수 없다. CPU는 메모리에 저장된 데이터보다 레지스터에 저장된 데이터를 더 빠르게 읽고 쓸 수 있기 때문에, 값이 자주 변경되는 변수에 사용하는 것이 좋다.
/* 길이가 `n`인 배열 `arr`의 모든 원소의 합을 반환한다. */
int GetArraySum(int *arr, int n) {
if (arr == NULL || n <= 0) return 0;
/*
사실... 요즘은 컴파일러 성능이 좋고 최적화도 알아서
잘해주기 때문에 `register` 키워드를 사용할 일이
거의 없기는 하지만... 사용하면 좋을 수도 있음!
*/
register int i = 0;
int result = 0;
for (i = 0; i < n; i++)
result += arr[i];
return result;
}변수뿐만이 아니라 함수를 선언할 때도 기억 클래스 지정자를 사용할 수 있는데, 이때 사용할 수 있는 키워드는 extern과 static, 단 두 개뿐이다. 함수를 선언할 때 extern을 사용하는 것은 변수 선언 시에 auto 키워드를 붙이는 것와 마찬가지로 굳이 할 필요가 없기 때문에, 여기서는 static 키워드가 붙은 함수에 대해서만 알아보도록 하자.
static 기억 클래스 지정자는 내가 만든 함수를 다른 소스 파일에서 사용하지 못하도록 하고 싶을 때 주로 사용한다. 이렇게 static 키워드를 붙여 함수를 선언하면 다른 사람이 우리가 작성한 코드를 수정할 때 이 함수가 수정되더라도 다른 소스 파일에는 영향을 주지 않을 것이라는 사실을 알 수 있다. 또한 static으로 함수를 선언하면 해당 소스 파일에서만 접근이 가능하기 때문에, 다른 소스 파일에서 똑같은 함수 이름을 필요에 따라 재사용할 수 있게 된다.
C언어의 자료형 한정자에는 const, volatile, 그리고 1999년에 제정된 C언어 표준 (C99)에서 추가된 restrict가 있다. volatile은 주로 저수준 또는 임베디드 프로그래밍 (low-level/embedded programming)에서 사용되고, restrict는 우리가 사용할 일이 거의 없기 때문에, const만 살펴보자. const 자료형 한정자는 한번 값이 초기화되면 절대 변하지 않는 상수 (constant)를 만들 때 사용하며, 일반적으로 상수의 이름을 지을 때는 DELTA_TIME, INT_MAX, PI처럼 대문자만을 사용한다.
// `DELTA_TIME`은 상수이기 때문에, 절대 그 값이 변하지 않는다.
static const float DELTA_TIME = 1.0f / 60.0f; const 자료형 한정자를 사용하여 상수를 선언하면 우리가 작성한 프로그램을 읽어보는 사람들에게 이 상수의 값이 절대 변하지 않음을 알릴 수도 있고 컴파일러가 상황에 따라 우리가 작성한 코드를 최적화해줄 수 있으므로, 상수를 선언할 때는 const를 애용하는 것이 좋다.
CPU는 우리가 작성한 함수를 호출할 때 매개 변수의 값을 복사해서 스택 (stack)이라는 메모리 공간에 저장한다. 또한 CPU는 함수를 호출하거나 함수 호출이 끝나고 프로그램의 그 다음 코드를 실행할 때 몇 가지 명령어를 추가로 실행하는데, 이렇게 CPU가 함수를 실행하는 데에 필요한 추가 작업을 오버헤드 (overhead)라고 한다. 함수의 오버헤드는 일반적으로 프로그램 실행에 큰 영향을 미치지 않지만, 물리 엔진 (physics engine)처럼 벡터 연산 함수가 몇십만 번, 또는 몇백만 번 호출되거나 CPU의 성능이 컴퓨터 CPU의 성능보다 비교적 낮은 저수준 또는 임베디드 프로그래밍을 할 때는 영향을 미칠 수 있다.
C99 표준부터는 함수 선언 시에 사용할 수 있는 inline이라는 선언 지정자가 새로 추가되었다. inline 키워드는 컴파일러에게 "이제부터 이 함수가 호출되는 모든 곳을 그 함수 내용에 대응하는 CPU 명령어로 대체해줘"라고 요청하는 역할을 하는데, inline 선언 지정자는 register와 마찬가지로 컴파일러에게 명령이 아닌 요청을 하는 것이며, 컴파일러가 이 선언 지정자를 무시할 수도 있다. 이제 inline 키워드가 무엇인지 이해하기 위해 아래 예제를 살펴보자.
inline 함수를 선언하는 방법에는 두 가지가 있다. 첫 번째 방법은 static inline 키워드를 사용해 그 함수가 선언된 소스 파일에서만 사용하는 방법이고, 두 번째 방법은 헤더 파일에 inline 함수의 정의를 직접 넣는 것이다.
/* source.c */
/* 부동 소수점 값 `a`와 `b`의 합을 반환한다. */
static inline double GetTwoSum(double a, double b) {
return a + b;
}/* example.h */
#ifndef HEADER_H
#define HEADER_H
/* ... */
/* 부동 소수점 값 `a`와 `b`의 합을 반환한다. */
inline double GetTwoSum(double a, double b) {
return a + b;
}
/* ... */
#endif/* example.c */
#include "example.h"
extern double GetTwoSum(double a, double b);- 오픈 소스 소프트웨어란?
- Git과 버전 관리 시스템
- GitHub를 이용한 저장소 호스팅
- 프로젝트의 기여 및 관리
- 라이브러리 소개
- 개발 환경 구축
- 첫 번째 프로그램
- 게임 창과 커서 관리
- 프레임, 시간과 타이머
- 픽셀, 선분과 기본 도형
- 마우스와 키보드 입력
- 벡터 글꼴과 비트맵 글꼴
- 이미지와 텍스처의 사용
- 카메라와 렌더 텍스처
- 충돌 감지와 충돌 해결
- 효과음과 음악 재생
- 그 외 유용한 함수