728x90

실제로 os는 메모리를 페이지 맵핑을 하기 때문에 프로세스에서 관리하는 논리적 주소와 실제 물리적 주소는 다르다. 운영체제에게 메모리를 요청하면 최소 페이지 사이즈(4kb)로 할당 해준다. 프로세스에서 접근하는 메모리 주소는 맵핑된 주소기 때문에 절대 다른 프로세스와 중복되지 않으니 걱정하지 않고 써도 된다.

 

우리는 이 페이지 단위로 반환해주는 메모리 주소값에 데이터를 올려놓고 쓰면 된다.

 

https://jhnyang.tistory.com/290

 

[운영체제 OS] 메모리 관리기법 - 페이징 (paging)이란? 내부 단편화(Internal Fragmentatoin)에 대해 알아

[운영체제 완전정복 링크 모음] 안녕하세요 양햄찌 블로그 입니당. 오늘은 드디어 운영체제에서 중요한 한 섹션을 차지하고 있는 페이징(paging)에 대해 살펴보려고 해요. 오늘 진행하려는 포스팅

jhnyang.tistory.com

 

그럼 페이지기법은 왜 나왔을까?

 

실제로 프로세스 메모리를 조작하기 위해서는 프로세스 메모리가 linear 한 상태가 되어야 한다.

그리고 물리적 메모리도 linear하게 매핑해서 쓰다보니 메모리 할당 반납하는 과정에서 메모리 파편화가 일어난다. 물리적 주소의 메모리 파편화를 external 파편화라고 한다. 그래서 중간에 페이지 맵핑 테이블을 두고 프레임(물리적 주소에서는 페이징을 프레임이라고 함)단위로 아무대나 할당하고 맵핑  테이블을 이용해서 프로세스에서는 linear한 생태로 사용하겠금한다.

 

반대로 이렇게 하면 논리적 메모리는 linear를 유지하기 떄문에 이쪽에도 파편화가 생길 수 있는데 이를 internal fragment라고 하고 이는 external fragment 보다 작기 때문에 페이징 방식을 쓴다.

 

결론

이후 단계를 보기 위해 이해해야 하는 내용은 다음과 같다.

- 논리주소를 liner한 상태로 유지하고 물리주소의 external 파편화를 줄이기 위해 페이징 단위로 테이블 참고하여 맵핑 하는 과정을 거친다.

- 뿐만 아니라 페이지 단위로 메모리에 속성을 부여하기도 한다(ex. 읽기용, 쓰기용 등) 왜냐하면 1바이트 단위로 메모리 속성을 지정하는 것은 그 속성 정보를 저장하기 위해서 그만큼의 메모리가 또 필요하기 때문에 비효율적.

728x90
728x90

이 글은 [인프런-게임서버 만들기 by rookiss]를 내용을 토대로 개인적으로 학습한 내용을 종합하여 정리 했습니다.

 

 

앞서 [CustomAllocator] 1단계_new, delete overloading에서 new, delete 오버로딩 하는 방법에 대해 학습했습니다.

 

오버로딩 하는 방법에는 크게 두 가지가 있습니다.

- 전역 오버로딩

- 클래스별 오버로딩

 

전역 오버로딩의 경우 모든 new, delete 연산자에 적용 되므로 위험한 방식 입니다. 그렇다면 메모리 관리를 하기 위해서는 클래스별 오버로딩을 쓰면 될까요? 가능합니다. 하지만 메모리 관리를 좀더 범용적으로 하려면 클래스별로 오버로딩을 작성하는 일도 여간 귀찮은 작업일 것 입니다. 매크로 등을 이용해서 조금은 개선 시킬 수 있지만요.

 

그래서 여기서는 저희만의 new, delete를 정의해서 사용하는 방법을 소개합니다. xnew, xdelete로 구현해 봅시다.

 

template<typename Type, typename... Args>
Type* xnew(Args&&... args)
{
	Type* memory = static_cast<Type*>( ::malloc(sizeof(Type) ); // 1단계 메모리 할당

	new(memory)Type(::forward<Args>(args)...); // 2단계 생성자 호출

	return memory; // 3단계 메모리 주소 반환
}

template<typename Type>
void xdelete(Type* obj)
{
	obj->~Type();    // 1단계 소멸자 호출
	::free(obj); // 2단계 메모리 반환
}

new, delete 동작 방식을 이해하고 있다면 위 코드를 이해하는 큰 어려움이 없습니다. 템플릿 코드로 바꿔준게 전부니까요. 이 코드를 이해 하기 위해서 new, delete 동작 방식 이외에 선수 지식이라면 템플릿 기초 문법, 템플릿 가변 매개변수, 전달참조(+forward) 정도 입니다. 위 코드가 이해되지 않는다면 해당 내용을 먼저 학습하고 위 코드를 보시면 어렵지 않게 이해할 수 있습니다.

 

본격적으로 메모리풀링을 하기 앞서 new, delete 연산자 흐름 가로채서 custom new, xdelete를 정의 했습니다. 이제 다음 단계에서 위 코드에 보이는 malloc 과 free 부분 역시 저희만의 메모리 할당기/해제기를 만들어 대체 해보도록 합시다.

728x90

'Programming Language > C++' 카테고리의 다른 글

CustomAllocator] 4단계_StompAllocator_1  (0) 2022.02.14
CustomAllocator] 3단계_메모리 페이징  (0) 2022.02.14
[CustomAllocator] 1단계_new, delete overloading  (0) 2022.02.13
::/스코프 지정자  (0) 2022.02.11
std::function  (0) 2022.02.10
728x90

이 글은 [인프런-게임서버 만들기 by rookiss]를 내용을 토대로 개인적으로 학습한 내용을 종합하여 정리 했습니다.

 

CustomAllocator를 만들기 앞서 핵심이 되는 new, delete 연산자 오버로딩에 대한 설명이 필요하다.

new, delete 연산자는 메모리를 할당, 해제하는 연산자로 이 연산자를 오버로딩 하여 메모리를 효과적으로 관리할 수 있다. 

 

하지만 다양한 메모리 할당 전략(컴파일러에 의한)을 모른다면 오히려 독이 될 수도 있다. 그러니 다양한 메모리 할당, 해제 원리나 정책을 이해하고 사용 했을 때 new, delete 오버로딩은 빛을 발할 것이다. 하지만 효율적인 메모리 관리 또는 순수한 학습용이 아니라도 디버깅(개발시)에 메모리 이슈를 해결하기 위한 안전 도구를 만드는데도 유용할 것으로 보인다. 실제로 이 글은 메모리를 효과적으로 관리하기 위한 기법메모리 이슈를 쉽게 잡아 내기 위한 기법 두가지를 설명하는 것을 목표로 한다.

 

자, 그럼 new, delete 연산자의 오버로딩에 대해서 알아보자.

 

new 와 delete의 구체적인 동작 방식

new 표현식

  Knight* knight = new Knight();

위 문장을 new-표현식이라고 합니다. 이 문장은 두가지 일을 합니다. 먼저 operator new를 호출하여 Knight 객체에 대한 메모리를 할당하고, 객체의 생성자를 호출합니다. 생성자 실행이 끝나고 나서야 객체에 대한 포인터가 리턴 됩니다.

 

실제로 어셈블러를 통해서 'operator new 호출 -> 생성자 호출 -> 객체 주소 리턴' 되는 과정을 알 수 있습니다.

흥미로운 점은 operator new가 실제로 void*를 반환하는데 이때 바로 객체의 포인터를 포인터 변수에 대입하지 않고 생성자 부터 호출되네요. 그리고 생성자가 호출되기 전에 cmp 비교를 통해서 분기가 나뉘는 것도 볼 수 있네요... 아직은 제가 이해하기 힘든 영역 입니다... 

 

delete 표현식

delete knight;

위 문장은 delete-표현식이라고 합니다. 이 문장 역시 new-표현식과 마찬가지로 두가지 일을 합니다. 먼저 소멸자를 호출하고 operator delete를 호출해서 메모리르 해제 합니다.

 

new 표현식과 다르게 'scalar deleting destructor'를 호출 하고 scalar deleting destructor 내부에서 소멸자와 operator delete를 호출하는 것을 확인 할 수 있습니다. scalar deleteting destructor는 컴파일러가 사용하는 함수라고 하는데 왜 new-표현식과 다르게 동작하는지는 잘 모르겠습니다.. 추측하자면 어떤 버전의 operator delete 연산자가 호출 될지는 호출된 operator new 버전에 따라 바뀌기 때문에 어떤 operator delete를 호출해야 할지에 대한 정보가 필요하기 때문에.. 이런 과정을 거치는게 아닐까 생각 해봅니다... 

 

요약하자면,

new-표현식은 ==> '메모리 할당 -> 생성자 호출'

delete-표현식은 ==>  '소멸자 -> 메모리 해제'

 

여기서 또 한가지 중요한 부분은 new, delete 표현식 자체는 오버라이딩 할 수 없습니다. 단지 new, delete 연산자를 오버로딩 하는 것 입니다. 즉 메모리 할당, 해제를 커스터마이징 할 수 있는 것이지요.

 

new 표현식과 operator new

new-표현식에는 여섯가지 종류가 있고, 각 버전마다 적용되는 operator new가 따로 있다.

 

그중 2개는 배치 new 연산자로 placement new operator라 부른다. 이 연산자를 사용하면 기존에 확보된 메모리에서 객체를 생성하고(생성자를 호출) 한다. 이는 메모리풀을 만들 때 유용하다. 또한 이 배치 new 연산자는 오버로딩이 금지되어 있다.

//일반
void* operator new(size_t size);
void* operator new[](size t_size);

//익센션
void* operator new(size_t size, const std::nothrow_t&) noexcept;
void* operator new[](size_t size, const std::nothrow_t&) noexcept;

//placement new operator(오버로딩 금지)
void* operator new(size_t size, void* p) noexcept;
void* operator new[](size_t size, void* p) noexcept;

 

delete 표현식과 operator delete

delete-표현식은 두가지 종류가 있다. 하지만 operator delete는 operator new와 동일하게 6종류가 있다. 왜 일까?

operator delete는 operator new와 항상 대응되어 호출된다. operator new 종류를 보면 일반버전 2개, 익센션버전2개, 배치버전 2개가 있다. operator new의 익센션 버전이 호출된단다면 operator delete 익센션 버전이 호출된다. 하지만 이는 개발자가 예측하고 처리하는 부분이 아니다. 따라소 익센션 버전 delete-표현식은 존재하지 않는다. 배치버전 delete-표현식 역시 이와 동일한 이유다.

// 일반
void operator delete(void* ptr) noexcept;
void operator delete[](void* ptr) noexcept;

// 익셉션
void operator delete(void* ptr, const std::nothrow_t&) noexcept;
void operator delete[](void* ptr, const std::nothrow_t&) noexcept;

// placement delete operator
void operator delete(void* p, void*) noexcept;
void operator delete[](void* p, void*) noexcept;

 

operator new와 operator delete 오버로딩 하기

특정 new, delete 연산자를 오버로딩하여 내부적으로는 전역 new,delete 연산자를 호출하는 예제입니다.

#include <iostream>
using namespace std;

class MemoryDemo
{
public:
    virtual ~MemoryDemo() = default;

    //normal.
    void* operator new(size_t size) { cout << "operator new" << endl; return ::operator new(size); }
    void operator delete(void* ptr) noexcept { cout << "operator delete" << endl; ::operator delete(ptr); }

    void* operator new[](size_t size) { cout << "operator new[]" << endl; return ::operator new[](size); }
    void operator delete[](void* ptr) { cout << "operator delete[]" << endl; ::operator delete[](ptr); }

    //exception.
    void* operator new(size_t size, const std::nothrow_t&) noexcept { cout << "operator new nothrow" << endl; return ::operator new(size, nothrow); }
    void operator delete(void* ptr, const std::nothrow_t&) noexcept { cout << "operator delete nothrow" << endl; ::operator delete(ptr, nothrow); }

    void* operator new[](size_t size, const std::nothrow_t&) noexcept { cout << "operator new[] nothrow" << endl; return ::operator new[](size, nothrow); }
    void operator delete[](void* ptr, const std::nothrow_t&) noexcept { cout << "operator delete[] nothrow" << endl; ::operator delete[](ptr, nothrow); }

    //placement new 는 오버로딩 불가능.
};



int main()
{
    MemoryDemo* mem = new MemoryDemo();
    delete mem;
    mem = new MemoryDemo[10];
    delete[] mem;
    mem = new (nothrow) MemoryDemo();
    delete mem;
    mem = new (nothrow) MemoryDemo[10];
    delete[] mem;
}

 

new, delete 연산자 오버로딩시 주의할 점

- 전역 operator new와 delete 는 교체하지 말 것. 최소한 교체 했다면 그 내부에서 또다시 new를 호출 하지 말 것. 무한 재귀가 될 수도 있다.

- operator new를 오버로딩 하면 반드시 대응되는 operator delete도 오버로딩 할 것. 대응되게 하지 않으면 기본 c++메모리 정책을 따르므로 할당-해제 정책이 달라지는 결과 초래한다.

- operator new, delete를 오버로딩 했다면 default, delete를 사용하여 operator new, delete 여러 타입들의 사용, 미사용 여부를 명확하게 할 것. 메모리 할당, 해제 정책의 일관성을 유지하기 위해서.

 

operator new와 operator delete에 매개변수를 추가하여 오버로딩하기

#include <iostream>
using namespace std;

class MemoryDemo
{
public:
    virtual ~MemoryDemo() = default;

    void operator delete(void* ptr) noexcept 
    { 
        cout << "operator delete" << endl; 
        ::operator delete(ptr); 
    }

    //custom
    void* operator new(size_t size, int extra)
    { 
        cout << "operator new with extra int : " << extra << endl; 
        return ::operator new(size); 
    }

    // 이 delete를 직접 호출할 순 없다. operator new호출 할때 그객체의 생성자에서 익셉션을 던져야 호출된다.
    void operator delete(void* ptr, int extra) noexcept  
    { 
        cout << "operator delete with extra int : " << extra << endl; 
        ::operator delete(ptr);
    }
};



int main()
{
    MemoryDemo* mem = new (__LINE__) MemoryDemo();
    delete mem;
}

짝꿍으로 custom 오버로딩한 operator delete는 직접 호출되지 않는다. 그래서 위 코드에서는 기본 delete가 호출된다.

이 부분은 아직 잘 이해를 못하겠다. 기본 delete를 정의하지 않으면 delete 키워드는 적절한 operator delete를 찾을 수 없다고 불평을 한다. 

 

매개변수를 추가하여 메모리 누수가 발생한 지점의 파일 이름, 줄번호 등을 받아서 로그를 남길 수도 있다.

 

관련 C 매크로

__FILE__ :: 파일이름

__LINE__ :: 줄번호

__FUNCTION__ or __func__ :: 함수 이름

 

operator delete에 메모리 크기를 매개변수로 전달 하도록 오버로딩 하기

주의 할점은 매개변수를 받지 않는 operator delete를 사용하면 매개변수 없는 버전이 먼저 호출 되므로 매개변수 있는 타입을 쓰려면 그 버전만 정의해라.

#include <iostream>
using namespace std;

class MemoryDemo
{
public:
    virtual ~MemoryDemo() = default;

    //custom
    void* operator new(size_t size)
    { 
        cout << "operator new " << endl; 
        return ::operator new(size); 
    }

    // 이 delete를 직접 호출할 순 없다. operator new호출 할때 그객체의 생성자에서 익셉션을 던져야 호출된다.
    void operator delete(void* ptr, size_t size)  
    { 
        cout << "operator delete with extra int : " << size << endl;
        ::operator delete(ptr);
    }
};



int main()
{
    MemoryDemo* mem = new MemoryDemo();
    delete mem;
}

 

 

728x90

'Programming Language > C++' 카테고리의 다른 글

CustomAllocator] 3단계_메모리 페이징  (0) 2022.02.14
CustomAllocator] 2단계_BaseAllocator  (0) 2022.02.14
::/스코프 지정자  (0) 2022.02.11
std::function  (0) 2022.02.10
Modern C++/lambda/람다  (0) 2022.02.09
728x90

:: 만 덜렁 쓰였을 때 의미는?

 

1. ::는 스코프 지정자.

 

2. 네임스페이스도 스코프의 일종.

 

3. 스코프 이름 없이 :: 만 쓰이면 글로벌 네임스페이스 스코프를 지정했음을 의미.

 

4. 글로벌 네임스페이스는 이름이 없다.

728x90

'Programming Language > C++' 카테고리의 다른 글

CustomAllocator] 2단계_BaseAllocator  (0) 2022.02.14
[CustomAllocator] 1단계_new, delete overloading  (0) 2022.02.13
std::function  (0) 2022.02.10
Modern C++/lambda/람다  (0) 2022.02.09
{} 중괄호 초기화  (0) 2022.02.06
728x90

함수는 함수 포인터 변수로만 보관할 수 있고, 함수자(functor)는 객체 타입으로만 보관이 가능합니다.

std::function을 사용하면 모든 collable 들을 객체형태로 보관할 수 있습니다. 심지어 람다식도 보관할 수 있습니다.

여기서 collable은 소괄호를 이용하여 호출 가능한 모든형태를 말합니다.

 

물론 auto를 통해서도 collable 들을 보관할 수도 있습니다. auto와 std::function의 차이는 뒤에 설명하겠습니다. 

일단, 간단한 사용 예시를 봅시다.

 

간단한 사용 예시

#include <iostream>
#include <functional>
using namespace std;

int func(const string& a)
{
    cout << "func1 " << a << endl;
    return 0;
}

struct functor
{
    void operator() (char c)
    {
        cout << "functor : " << c << endl;
    }
};


int main()
{
    function<int(const string&)> f1 = func;
    function<void(char c)> f2 = functor();
    function<void(void)> f3 = []() { cout << "lamda func" << endl; };

    f1("hi");
    f2('k');
    f3();
}

 

 

C에서의 함수 포인터에서도 멤버변수를 보관할 떄 문법이 약간 달라졌던 것 처럼 std::function 을 이용할 때도 동일합니다. 당연히 멤버함수는 객체 의존적인 함수이기 떄문입니다. 멤버함수는 암무적으로 자신을 호출한 객체를 인자로 암묵적으로 받고 있기 때문에 std::functio에 타입에 멤버함수의 클래스타입을 첫번째 인자로 명시해야 합니다.

 

함수 포인터와 마찬가지로 멤버함수를 보관할 때는 명시적으로 멤버함수 앞에 &를 통해 함수 주소를 반환하고 있습니다. 이게 FM 이죠. 정적, 전역함수의 경우 컴파일러에 의해서 암묵적으로 함수이름이 함수주소를 반환해주고 있었던거니 까요.

 

멤버함수를 보관하는 std::function 객체

#include <iostream>
#include <functional>
using namespace std;


class Knight
{
public:
    void AttackByFist() const
    {
        cout << "AttackByFist" << endl;
    }

    void AttackBySword(int addDamage)
    {
        cout << "AttackBySword" << endl;
    }
};

int main()
{
    function<void(const Knight&)> attackByFist = &Knight::AttackByFist; // <void(Knight&)> 형태로도 const 멤버 함수를 보관됩니다. 이유는 모르겠습니다.
    function<void(Knight&, int)> attackBySword = &Knight::AttackBySword;

    Knight k;

    attackByFist(k);
    attackBySword(k, 100);
}



mem_fn

mem_fn 함수는 멤버함수 function 객체를 반환합니다.

언제 mem_fn 함수가 유용할 할까요?

 

먼저 문제가 되는 상황을 살펴 보겠습니다.

#include <iostream>
#include <functional>
using namespace std;


class Knight
{
public:
    Knight(string name) : _name(name) {};
public:
    void Print()
    {
        cout << "I am Knight :: " << _name << endl;
    }

private:
    string _name;
};

template<typename Func>
void func(Func f)
{
    Knight k("by func");
    f(k);
}


int main()
{
	func(&Knight::Print);   // 컴파일 에러
}

이 경우 '이름()' 형태로 호출되지 않기 때문에 func 함수 내부에서 'f(k)'는 동작하지 않습니다. 따라서 collable 형태로 사용 가능하도록 function 객체로 넘겨 주어야 합니다.

 

        function<void(Knight&)> f = &Knight::Print;
        func(f);

이처럼 funtion 객체로 받고 func 인자로 넘겨주면 collable 형태로 호출이 가능하기 떄문에 정상 작동합니다. 이 과정을 대신 할수 있는 방법에는 두가지가 있습니다. std::mem_fn을 사용하거나 람다식을 이용하는 방법입니다. 

 

mem_fn 사용

        func(mem_fn(&Knight::Print));

람다식 사용

        func([](Knight& k) {
            k.Print();
        });

 

개인적으로는 mem_fn 코드는 더 짧지만 functional 헤더를 추가 해야하고 람다는 함수객체를 선언과 동시에 collabe 형태로 전달도 가능합니다.

람다는 auto와 같이 사용될 때 막강한 편의성을 제공합니다.

std::function을 이용하여 collabe 타입을 명시적으로 지정하고 특정 타입의 collabe만 받는 함수객체를 관리하고 싶을 때 유용한것 같습니다.

 

std::bind

함수객체 생성과 동시에 디볼트 인자값을 지정할 수 있습니다. 사용빈도가 떨어지는 것 같아 생략.

 

728x90

'Programming Language > C++' 카테고리의 다른 글

[CustomAllocator] 1단계_new, delete overloading  (0) 2022.02.13
::/스코프 지정자  (0) 2022.02.11
Modern C++/lambda/람다  (0) 2022.02.09
{} 중괄호 초기화  (0) 2022.02.06
using - type alias declaration  (0) 2022.02.06
728x90

함수 객체는 STL에 유용하게 사용된다.

C++11에서는 이런 함수 객체를 손쉽게 작성하는 '람다' 라는 기능을 제공한다.

 

람다에 의해 만들어진 실행시점 객체를 클로저(closure)라고 합니다.

 

사용법

    float ratio = 0.3;;

    auto addLamda = [&ratio](int a, int b) -> float {
        float ret = (a + b) / ratio;
        return ret;
    };

    float ret = addLamda(3, 4);  // 2.3333

 

[캡쳐](인자) -> 타입 { 구현부 }

캡쳐(capture)

함수 객체 내부에 변수를 저장하는 개념과 유사하다. '구현부'에서 외부 변수를 사용할 때 방식을 지정할 수 있다.

방식에는 복사방식, 참조방식이 있다. 

예)

[=] : 모든 값을 구현부에서 복사 방식으로 사용합니다.

[&] : 모든 값을 구현부에서 참조 방식으로 사용합니다.

[a, &b] : b변수를 참조 방식으로 사용합니다. a변수는 복사 방식으로 사용합니다.

 

인자

함수 매개변수와 동일한 형태로 매개변수를 받습니다.

예) [](int, int)

 

타입

'-> bool' 과 같은 형태로 지정하지만 실제로 구현부 return 값을 통해 자동 추론 되기 때문에 생략 가능 합니다.

 

구현부

일반 함수 구현부와 동일하게 작성됩니다.

 

주의점

MS에서는 캡쳐방식에 [=], [&] 사용을 지양합니다. 이는 명시적인 캡쳐 방식을 지정하지 않을 경우에 문제를 인식하기 쉽지 않기 때문입니다.

 

문제가 되는 경우를 아래 코드를 통해서 살펴 봅시다.

class Knight
{
    public:
        auto ResetHpJob()
        {
            // this 포인터가 복사된 형태
            // 캡터 부분만 봐서는 한눈에 알아보기 힘들다.
            auto f = [=]()
            {
                _hp = 200;
            };

            //auto f = [this]()
            //{
            //    this->_hp = 200;
            //};
            
            return f;
        }

    public:
        int _hp = 100;
};

 

 

해당 코드는 클래스 내부에서 람다를 정의하고 있습니다. 그리고 ResetHpJob 함수에서는 멤버변수를 접근하는 람다를 반환하고 있습니다. 이 람다를 외부에서 호출하게 되면 문제가 됩니다.

왜냐하면 람다 정의부분은 객체에 의존적인 변수에 접근하고 있고 람다가 호출되는 시점에 객체가 존재한다는 보장 이 없기 때문입니다.

 

이런 코드 작성을 미리 방지하려면 어떻게 해야 할까요??

 

클래스 내부이기 때문에 멤버변수를 의심없이 접근하고 있지만 실제로는 'this->멤버변수' 와 같은 형태로 접근 되어야 합니다. 따라서 캡쳐 방식을 지정해야 합니다. 이때 [=] 와 같은 방식으로 지정하게 된다면 '='가 this 포인터를 복사하기 위함인지 한눈에 파악하기 어렵습니다.

 

이런 이유로 캡쳐 방식을 지정할 때는 변수별로 별도로 지정하는 것이 코드 가독성을 높이고 이는 곧 디버깅을 쉽게 합니다.

728x90

'Programming Language > C++' 카테고리의 다른 글

::/스코프 지정자  (0) 2022.02.11
std::function  (0) 2022.02.10
{} 중괄호 초기화  (0) 2022.02.06
using - type alias declaration  (0) 2022.02.06
auto  (0) 2022.02.06
728x90

다양한 초기화 방법

C++ 에서는 =,(),{} 와 같은 다양한 초기 방법이 있다.

(일반 자료형과 객체 타입 초기화 방식에는 약간의 차이가 있다.)

 

장점

- 축소 변환 방지(컴파일 에러)

- 배열의 초기화 문법 {}와 같이 생성자를 정의할 수 있다.

 

단점

- initializer_list<T> 생성자를 이용하여 stl container vector와 같은 방식으로 초기화 할 수 있다. 이때 일반적인 생성자 시그니처와 initailizer 초기화 방식의 시그니처가 동일한 경우 (), {}에 따라 다른 생성자가 호출된다. 이는 초기화 호출방식에 대한 혼란을 야기시킨다.

 

결론

  • 소괄호 초기화
    • 전통적인 초기화 방식 → 거부가 없음
    • vector 등 특이한 케이스에 대해서면 {} 사용
  • 중괄호 초기화 {}
    • 초기화 문법 일치화(intializer_list<T> 생성자를 이용) ⇒ 100% 대응하진 못함.
    • 축소 변환 방지
  • 개인 생각
    • 중괄호 초기화 방식의 축소변환은 확실히 개발단계에서 장점이지만 이고 개인의 취향에 따른 선택사항이지만 중괄호 초기화로 인해 생성자 호출 방식에 있어서 분기점이 생긴다. 이는 팀단위 개발에서는 코드 가독성을 떨어뜨릴 것 같다. {}초기화는 약속된 특정 클래스에서만 합의하에 사용하는게 좋을 것 같다. 애초에 ms도 그렇게 사용하는 것(?) 같다.
#include <iostream>
#include <vector>

using namespace std;

class Knight
{
public:
	Knight()
	{
		cout << "Knight 기본 생성자" << endl;
	}

	Knight(const Knight& k)
	{
		cout << "Knight 복사 생성자" << endl;
	}


	Knight& operator = (const Knight& k)
	{
		cout << "Knight 대입 연산자" << endl;
		hp = k.hp;
		return *this;
	}

public:
	int hp = 100;

};

class Mage
{
public:
	Mage()
	{

	}

	Mage(int a, int b)
	{
		cout << "Mage(int int)" << endl;
	}

	Mage(initializer_list<int> li)
	{
		cout << "Mage(initailizer_list)" << endl;
	}
};

Knight Func()
{
	return Knight();
}

int main()
{
	// 다양한 초기 방법
	{
		// 일반 자료형
		int a = 0;

		int b(0);
		int c = (0);

		int d{ 0 };
		int e = { 0 };

		// 객체 타입
		Knight k1; // k1 기본 생성자
		Knight k2 = k1; // k2 복사 생성자

		Knight k3; // k3 기본 생성자
		k3 = k1; // k3 대입 연산자

		Knight k4{ k2 }; // k4 복사 생성자.
	}

	// !주의!
	{
		Knight k5(); // knight k5; 이 기본생성자를 호출하는 거고 knight k5()는 함수 선언문이다. void형 매개변수에 리턴값이 knight인 k5.
		Knight k6{}; // 이건 기본생성자 버전.
	}

	// {} 초기화 장점 - 축소 변환 방지
	{
		// 일반 초기화시 축소됨
		double x = 1.34;
		int y = x; // 컴파일에서 경고
		int z = { x }; // 컴파일에서 에러
	}

	// {} 초기화 장점 - 배열과 같은 방식을 객체 초기화 가능.
	{
		// 번거로운 초기화 방식
		vector<int> v1;
		v1.push_back(1);
		v1.push_back(2);
		v1.push_back(3);

		// {}를 이용한 초기화 방식. 마치 배열 초기화 방식과 유사
		int arr[] = { 1,2,3,4,5 };

		v1 = { 1,2,3,4 };

	}

	// {} 문제점 - 아래 두 초기화 방식은 다른 생성자를 호출한다. - why?
	{
		vector<int> v1{10 , 1}; // 벡터 크기를 10으로 지정하고 모든 요소를 1로 초기하는 생성자 호출.
		vector<int> v2(10, 1); // 벡터에 10, 1 두 값을 초기화하는 생성자 initalizer_list 생성자 호출.
	}


	// {} 문제점 재현
	{
		Mage m{ 3,4 }; // initailizer 생성자가 정의되어 있으면	Mage(int a, int b) 생성자는 호출되지 않습니다.
	}

	return 0;
}

공부한 내용을 개인적으로 복습하기 쉬운 형태로 정리했습니다.

잘못된 내용이 있는 경우 언제든 댓글 혹은 이메일로 지적해주시면 감사드립니다.

728x90

'Programming Language > C++' 카테고리의 다른 글

std::function  (0) 2022.02.10
Modern C++/lambda/람다  (0) 2022.02.09
using - type alias declaration  (0) 2022.02.06
auto  (0) 2022.02.06
함수 객체(함수자, Functor)  (0) 2022.02.06
728x90

특징

- typedef 보다 직관적인 사용 방식

- 탬블릿 문법과 사용시 typedef 보다 더 궁합이 잘 맞는다.

- typedef 장점은.. 없다. 그냥 using 쓰세요..

 

사용 예제

#include <iostream>
#include <vector>
#include <list>
using namespace std;


// 직관성 - 가독성
typedef __int64 id;
using id2 = int; 

// 직관성 - 함수 포인터
typedef void (*MyFunc)(); // 매개변수와 리턴값이 없는 함수 포인터
using MyFunc2 = void(*)();

// tmeplate과의 궁합 - using (ok)
template<typename T>
using List = ::list<T>;

// tmeplate과의 궁합 - typedef (ok) - struct나 class로 typedef를 감싸는 등의 방법이 있긴하다.
//template<typename T>
//typedef list<T> List;

// typedef + template의 흉측한 모습
template<typename T>
struct List2
{
	typedef std::list<T> type;
};

int main()
{
	List<int> li;				// using + template => 깔끔
	List2<int>::type li2;		// typedef + template => 흉측

	return 0;
}

 


공부한 내용을 개인적으로 복습하기 쉬운 형태로 정리했습니다.

잘못된 내용이 있는 경우 언제든 댓글 혹은 이메일로 지적해주시면 감사드립니다.

728x90

'Programming Language > C++' 카테고리의 다른 글

Modern C++/lambda/람다  (0) 2022.02.09
{} 중괄호 초기화  (0) 2022.02.06
auto  (0) 2022.02.06
함수 객체(함수자, Functor)  (0) 2022.02.06
함수 포인터  (0) 2022.02.06

+ Recent posts