3. 게임을 위한 소프트웨어 엔지니어링 기초 - 3

이관중·2023년 7월 30일

3.3.3 킬로바이트 vs 키비바이트

  • 2의 지수 단위로 측정하는 메모리 크기를 국제단위계 킬로바이트나 메가바이트로 표현하는 것은 정확하지 않다.
    • 프로그래머가 킬로바이트라고 말할 경우 대개 1024바이트를 의미.
    • SI단위로 킬로는 10^3 == 1000
  • 국제전기기술위원회 IEC에서 1998년 새로운 SI 형태의 접두사 공표.
  • 책에서는 IEC 단위 사용

3.3.4 선언, 정의, 연결성

3.3.4.1 번역 단위 다시 살펴보기

  • C/C++ 프로그램은 여러 번역 단위로 이뤄진다.
  • 컴파일러가 .cpp 파일 1개를 번역하면 목적 파일 .o또는 .obj 1개가 생긴다.
    • .cpp파일은 컴파일러가 번역할 때 다루는 제일 작은 단위이기 때문에 번역 단위라고 불린다.
  • 목적파일에는 .cpp 파일에 들어있는 것
    • 정의된 모든 함수를 번역한 기계어
    • .cpp 파일 안의 모든 전역 변수
    • .cpp 파일 안의 모든 정적변수
    • .cpp 파일에서 정의된 함수를 가리키는 미확정 참조unresolved references를 담기도 함
  • 컴파일러는 한 번에 1개의 번역 단위만 처리하기 때문에 외부의 전역 변수나 함수를 처리할때면 외부 참조들이 있을 것이라 가정하고 진행할 수 밖에 없음
  • 목적 파일들을 모두 모아서 완성된 실팽 파일로 만드는 것은 링커의 몫
  • 링커는 모든 목적 파일을 읽어들여 미확정 상태인 외부참조가 진짜 어떤 것인지 알아내려고 시도.
    • 이 과정 모두 성공시, 모든 함수, 전역변수, 정적 변수, 번역 단위 간 참조가 전부 포함된 실행 파일 만들어짐.
  • 링커의 주된 역할은 외부 참조 해결. 이와 관련된 링커가 낼 수 있는 에러는 두 가지
    1. extern으로 선언된 외부 참조를 찾아낼 수 없는 경우, '미확정 심볼unresolved symbol' 에러 발생.
    2. 이름이 같은 변수나 함수를 2개 이상 발견한 경우. '중복 정의된 심볼multiply defined symbol' 에러 발생.

3.3.4 선언과 정의의 차이

C/C++에서는 변수나 함수를 사용하기 전에 반드시 선언declaration하고 정의definition해야 한다.

  • 선언:
    데이터 객체나 함수의 형태를 나타낸다. 컴파일러에 이름과 데이터 타입 또는 함수의 서명(리턴 타입과 인자 타입)을 알려 준다.
  • 정의:
    프로그램 안에 고유한 저장 공간을 나타낸다. 이 저장 공간 안에는 변수, 구조체 및 클래스의 인스턴스, 함수의 기계어 등이 들어갈 수 있다.

요약하자면, 선언은 어떤 개념에 대한 언급reference이고 정의는 그 개념의 본체라고 할 수 있다. 정의는 언제나 선언이지만 그 역은 항상 참인 것은 아니다. C/C++에서 정의가 아닌 순수한 선언이 존재한다.

// foo.cpp

// 함수 max()의 정의
int max(int a, int b)
{
    return (a > b) ? a : b;
}

// 함수 min()의 정의
int min(int a, int b)
{
    return (a <= b) ? a : b;
}

정의가 아닌 선언으로 함수를 표현하면 다른 번역 단위에서 참조하거나 같은 번역 단위에서 나중에 사용할 수 있다. 함수 서명 후에 세미콜론을 붙이면 선언이 되는데, 이때 extern 키워드를 앞에 붙일 수도 있다.

// foo.h

extern int max(int a, int b);	// 함수 선언
int min(int a, int b);			// 마찬가지로 선언이다.
								// extern은 붙이지 않았지만 있는것으로 가정한다.

변수나 구조체(클래스)의 인스턴스를 정의할 때는 데이터의 타입 바로 뒤에 이름을 쓰면 되고 배열을 선언하는 경우는 대괄호를 붙인다.

// foo.cpp

// 전부 변수 정의다.
U32 gGlobalInteger = 5;
F32 gGlobalFloatArray[16];
MyClass gGlobalInstance;

다른 번역 단위에서 정의된 전역 변수를 사용할 때는 현재 번역 단위에서 extern 키워드를 앞에 붙여 선언하면 된다.

// foo.h

// 전부 정의가 아닌 선언이다.
extern U32 gGlobalInteger;
extern F32 gGlobalFloatArray[16];
extern MyClass gGlobalInstance;

선언과 정의의 중복

  • C/C++에서는 똑같은 데이터나 객체, 함수에 대한 선언을 여러 번 할 수 있다.
  • 정의는 같은 정의를 한 번 밖에 할 수 없다.
  • 한 번역 단위에서 같은 정의가 여러 번 나오면 컴파일러가 에러를 낸다.
  • 서로 다른 번역 단위에 중복 정의가 있다면 컴파일러는 모르고, 링커에서 중복 정의된 심볼 에러를 낸다.

헤더 파일에 정의하기와 인라인 함수

  • 정의를 헤더 파일에 두는 것은 보통 좋은 생각이 아니다.
    • 정의를 포함한 헤더를 여러 .cpp 파일에서 #include 구문으로 불러들이면 중복 정의된 심볼 에러가 발생하기 때문.
  • 인라인 함수의 정의는 예외.
    • 함수의 코드가 호출된 곳에 직접 복사되기 때문.
    • 두 군데 이상의 번역 단위에서 같은 인라인 함수를 사용하려면 인라인 함수의 정의는 반드시 헤더 파일에 있어야 한다.
    • .h 파일에 함수 선언을 inline으로 하고 함수 구현을 .cpp 파일에 둬도 안된다.
    • 컴파일러가 인라인 함수를 처리하려면 컴파일러가 함수 구현을 볼 수 있어야 한다.
// foo.h

// 이 함수는 올바른 인라인 함수다.
inline int max(int a, int b)
{
    return (a > b) ? a : b;
}

// 이 함수는 인라인 함수가 될 수 없는데
// 컴파일러가 함수 구현을 볼 수 없기 때문이다.
inline int min(int a, int b);
// foo.cpp

// min() 함수의 구현은 컴파일러에게 '감춰진' 거나 다름없기 때문에
// 이 함수는 foo.cpp 안에서만 인라인으로 처리될 수 있다.
int min(int a, int b)
{
    return (a <= b) ? a : b;
}

인라인 키워드는 컴파일러에게는 참조해야 할 힌트일 뿐이다. 컴파일러는 각 인라인 함수에 대해 함수 크기와 인라인화했을 때 얻을 수 있는 효율성 등을 감안해 분석한 후 정말 인라인으로 처리할지 결정한다. 프로그래머가 컴파일러에게 인라인 함수를 만들라고 강제할 수 있는 __forceinline 같은 키워드를 지원하는 컴파일러도 있다.

템플릿과 헤더 파일

  • 템플릿 클래스와 함수의 정의는 그것을 사용하는 모든 번역 단위에서 컴파일러에 보여야 한다.
    • 따라서 2개 이상의 번역 단위에서 템플릿을 쓰려면 템플릿이 헤더 파일에 있어야 한다(인라인 함수의 정의와 마찬가지로).
    • 따라서 템플릿의 선언과 정의는 뗄 수가 없다.
    • 즉, 헤더파일에 템플릿 함수와 클래스의 선언만 넣고, cpp파일에 정의를 감출수 없다.
profile
컴퓨터 그래픽스를 알고싶은 개발자

0개의 댓글