Coding/Unreal, C++

[C++ Primer Plus] 9. Memory Models and Namespaces

Seperate Compilation

  • 1챕터에서 말했듯, C++는 각 파일마다 컴파일을 따로(.c → .o)할 수 있다. 이런 방식덕분에 소스코드 하나를 수정하면 그거만 컴파일하고 링킹을 해서 실행파일을 만들 수 있다. Unix나 Linux의 make가 이런 관리를 도와준다.
  • 한 소스 코드의 내용을 나눠야 한다고 생각해보자. 예를 들어, 클래스 구현부와 main()을 나누는 것이다. 이런 상황에서는 main에서 클래스 구현부를 알 필요가 있다. 이런 상황을 해결해주는 것이 #include 기능이다. 즉, 총 3개의 파일로 나눠질 것이다.
    1. 클래스의 정의와 메소드의 프로토타입을 담은 .hpp 파일
    2. 클래스 구현 코드를 담은 .cpp 파일
    3. 클래스 메소드를 사용하는 .cpp 파일
  • 헤더 파일에는 주로 다음의 내용을 담는다.
    • Function protypes
    • Symbolic constants defined using #define or const
    • Structure declarations
    • Class declarations
    • Template declarations
    • Inline functions
  • 함수의 구체적인 알고리즘 자체를 쓰는 것은 Template과 Inline 함수다. 왜냐하면 Template과 Inline 함수는 실제로 Instantiate 되지 않기 때문이다. (=STL이 헤더에 다 정의된 이유)반대 이유로 non-templae function은 헤더에 정의하면 안된다. 그러면 다른 파일에서 같은 헤더를 include하면 같은 name을 가진 함수가 2개 instantiate 되기 때문이다.
  • #include에서 <>와 ""는 다른 의미를 가진다. <>가 더 좁은 의미를 가진다고 이해하면 된다. <>는 컴파일러에게 이 헤더 파일이 표준 헤더 파일이니까 파일 시스템에서 찾아보라고 알려주는 것이다. 반대로 ""로 감싼 헤더는 컴파일러가 먼저 현재 디렉토리나 소스코드 디렉토리를 찾아보게 한다. 이래도 없으면 표준 헤더 파일을 찾아본다.
    • <> : 표준 헤더 폴더
    • "" : 현재 폴더 + 소스코드 폴더 → 표준 헤더 폴더 

Multiple Library Linking

  • C++ 컴파일러는 적절한 상황에 쓰여야 하는 함수를 빠르게 찾기 위해 name decoration 또는 name mangling이라는 작업을 수행한다.
  • 예를 들어, long MyFunctionFoo(int, float)를 ?MyFunctionFoo@@YAYH로 만든다. 문제는 이런 name decoration 방식이 컴파일러마다 다르다는 것이다.
  • 즉 컴파일된 라이브러리를 사용할 때는 같은 컴파일러로 컴파일을 했는지 확인해야 한다. 다른 컴파일러로 컴파일됐다면 name decoration 규칙이 다르기 때문에 제대로 링킹을 할 수 없다.

Storage Duration, Scope, and Linkage

  • C++은 (11 기준) 메모리를 네가지로 나누어 관리한다.
    1. Automatic storage duration
      자신이 정의된 블록이 시작될 때 생겨나서 블록이 끝나면 사라진다.
    2. Static storage duration
      프로그램이 시작될 때 생겨나서 프로그램이 끝날 때 사라진다.
    3. Thread storage duration (C++11)
      쓰레드가 시작될 때 생겨나서 쓰레드가 끝날 때 사라진다.
    4. Dynamic storage duration
      new 연산자로 생겨나고 delete 연산자로 사라진다.

Scope and Linkage

  • Scope는 name이 file 안에서 어디까지 보이는 지(visible)를 결정한다.
  • Linkage는 name이 각 단위(unit)에서 어떻게 공유될 수 있는지 결정한다. 예를 들어, external linkage는 file 사이에서 공유되고, internal linkage는 같은 파일, 다른 함수 사이에서 공유된다.

Automatic Storage Duration

  • 함수의 매개변수나 함수 안에서 정의된 변수는 기본적으로 automatic storage duration을 갖는다. 또한 local scope에 no linkage다.
int main()
{ // teledeli allocated
	int teledeli = 5;
    { // websight allocated
    	cout << "Hello\n";
        int websight = -2;
        cout << "websight" << ' ' << teledeli << endl;
    } // websight expires
} // teledeli expires
  • 프로그램은 automatic storage duration을 가진 변수들을 관리하기 위해 stack이라는 메모리 관리 기법을 사용한다. 그래서 애초에 이 메모리 공간 자체를 stack이라고 부른다. stack의 크기는 각기 다르지만 컴파일러에 플래그를 줘서 조정할 수 있다.
  • stack은 LIFO(Last In First Out) 특성을 가지고 있다. 즉, 마지막으로 정의된 변수가 가장 먼저 나간다. 보통 함수가 시작되면 그 매개변수를 스택에 가장 먼저 쌓고 top을 올린다.

Static Duration Variables

  • C++의 Static Duration Variables는 3가지 linkage를 갖는다.
    1. external linkage (across files)
    2. internal linkage (across functions in a single file)
    3. no linkage (accessible to just one function or block)
  • 컴파일러는 모든 static 함수를 정해진 메모리 블록에 함께 적재해놓는다. 그리고 따로 static 함수를 초기화하지 않으면 0으로 초기화해준다.
int global = 1000; // static duration, external linkage
static int one_file = 50; // static duration, internal linkage

int main()
{
	...
}

void func1(int n)
{
	static int count = 0; // static duration, no linkage
    int llama = 0;
}

void func2(int q)
{
	...
}
  • static variable은 세가지 초기화 방법을 가지고 있다.
    1. zero initialization
    2. constant-expression initialization
    3. dynamic initialization
#include <cmath>
int x; // zero
int y = 5; // constant-expression
long z = 13 * 13; // constant-expression
const double pi = 4.0 * atan(1.0); // dynamic

Static Duration, External Linkage

  • external linkage를 가진 변수를 external variables 또는 global variables라고 부른다.
  • 'One Definition Rule'에 따라 정의는 빌드 범위 내에서 단한번 수행되어야 한다.
double up;
extern int blem; // 다른 곳에서 blem을 정의했을 것이다. 그 값을 갖다 쓴다.
extern char gr = 'z'; // gr은 이 파일에서 정의된다. 다른 곳에서는 정의하면 안된다.
  • 참고로 no linkage를 가진 static duration variable은 재귀 함수 등에서 쓰면 굳이 매개변수로 넘겨줄 필요가 없어서 편하다.

Specifiers and Qualifiers

  • storage class specifier와 cv-qualifiers가 있다. 이를 구분해서 알아둬야 한다.
  • storage class specifiers
    • register
    • static
    • extern
    • thread_local
    • mutable
  • cv-qualifiers
    • const
    • volatile
  • cv는 const와 volatile을 의미한다. const는 한번 정의된 이후로는 해당 프로그램에서 값이 수정되는 일이 없다는 것을 의미한다. volatile은 완전 반대다. 해당 프로그램에서 수정하지 않았음에도 불구하고 값이 바뀔 수 있음을 의미한다. 예를 들어, 하드웨어에 의해 값이 바뀔 수 있다.
  • mutable은 const struct임에도 불구하고 값을 수정할 수 있는 멤버를 의미한다.
struct data
{
	char name[30];
    mutable int accesses;
};

const data veep = {"Hyun", 0};
veep.accesses++; // Allowed
  • const는 storage class specifier를 조금 바꿀 수 있다. 예를 들어, static duration을 가진 변수를 const로 선언하면 기본적으로 static을 붙인 것과 같다. 즉, external linkage를 가지지 않는다. external linkage를 가지려면 extern specifier를 앞에 또 써주면 된다.
const int fingers = 10; // internal linkage, same as static const int fingers
extern const int fingers = 10; // external linkage

Functions and Linkage

  • 함수도 Linkage를 가진다. 하지만 변수에 비해 폭이 아주 좁다.
  • 기본적으로 함수는 external linkage를 가진다. 그래서 extern specifier를 함수 앞에 붙여줄 수 있지만 붙이나 마나 똑같다.
  • 함수에 internal linkage를 주려면 static specifier를 쓰면 된다.
static int private(double x); // interal linkage

Language Linking

  • Linker는 각 함수마다 symbolic name이 필요하다. 문제는 C와 C++이 symbolic name을 만드는 방법이 다르다는 것이다.
    • spiff(int)는 C에서 _spiff로, C++에서 _spiff_i로 바뀐다. C++에는 함수 오버로딩이 있기 때문이다.
  • 때문에 특정 라이브러리가 C로 만들어졌으면 제대로 linking 할 수가 없다. 그래서 look-up convention을 써줄 수 있다.
extern "C" void spiff(void); // C 방식의 이름을 찾으라고 링커에게 지시
extern void spoff(int); // C++ 방식의 이름을 찾으라고 링커에게 지시
extern "C++" void spiff(int); // C++ 방식의 이름을 찾으라고 링커에게 지시

Storage Schemes and Dynamic Allocation

  • new 연산자에서 초기화를 함께 할 수 있다.
int *pi = new int (6);
double * pd = new double (99.99);

// C++11 way
struct where {double x; double y; double z;};
where * one = new where {2.5, 5.3, 7.2};
int * ar = new int [4] {2,4,6,7};
  • Placement new 를 사용해서 특정 메모리 공간을 할당받을 수 있다.
#include <new> # placement new는 new 헤더를 include해야 한다.
struct chaff
{
	char dross[20];
    int slag;
};

char buffer1[50];
char buffer2[500];

int main()
{
	chaff *p1, *p2;
    int *p3, *p4;
    
    p1 = new chaff; // heap에서 할당받음
    p3 = new int[20]; // heap에서 할당받음
    
    p2 = new (buffer1) chaff; // buffer1의 공간을 할당받음. new (sizeof(chaff), buffer1) 호출
    p4 = new (buffer2) int[20]; // buffer2의 공간을 할당받음
}

Namespaces

  • 프로그램이 커지고 복잡해지면서 같은 이름이 충돌하는 namespace problem이 부상했다. 이를 해결해주는 것이 Namespace다.
namespace Jack {
	double pail;
    void fetch();
    int pal;
    struct Well { ... };
};

namespace Jill {
	double bucket(double n) { ... }
    double fetch;
    int pal; // Jack의 pal 변수와 이름이 겹치지만 namespace가 다르므로 괜찮다.
    struct Hill { ... };
};

using Declarations and using Directives

  • using declaration은 특정 네임스페이스의 특정 이름을 쉽게 쓸 수 있게 해주고, using directive는 특정 네임스페이스 전체를 바로 접근할 수 있게 해준다.
  • using declaration 예제
int main()
{
	using Jill::fetch; // Jill::fetch를 local namespace로 옮긴다.
    double fetch; // ERROR! 이름이 겹친다.
}

///

using Jill::fetch; // Jill::fetch를 global namespace로 옮긴다.

int main()
{
	cin >> fetch;
}
  • using directive 예제
using namespace Jack;

int main()
{
	pal = 4; // 알아서 Jack::pal을 의미함
}
  • 이 예시만 보면 using declaration을 전부 적용한 것이 using directive가 아니냐고 할 수 있는데, using directive는 그보다는 scope resolution(::)을 전체적으로 적용한 것에 가깝다.
  • 즉,  해당 namespace를 global namespace로 끌고 내려와주는 것이다. 그만큼 읽는 입장에서 혼동이 온다.
  • namespace가 여러개 겹치면 길어질 수 있는데, 이를 간단하게 만들기 위해 alias를 설정할 수도 있다.
namespace MEF = myth::elements::fire;

References

  • C++ Prime Plus 6th, Chapter 9