코가손의 블로그

[C++/기초] ModernC++ 본문

C++/문법

[C++/기초] ModernC++

Cogason 2021. 12. 19. 20:05
중괄호 초기화 { }
더보기
더보기

리스트 초기화를 간편하게 할 수 있다.

클래스에도 적용이 가능하다.

class Knight
{
public:
    Knight()
    {
    
    }
    
    Knight(initializer_list<int> li)
    {
        cout << "Knight(initializer_lit)" << endl;
    }
};

int main()
{
    Knight knight{1, 2, 3, 4, 5};

    return 0;
}
using
더보기
더보기
// 1. 직관성이 더 좋다
typedef __int64 id;
using id2 = int;

typedef void (*MyFunc)();
using MyFunc2 = void(*)();

// 2. 템플릿에 활용된다

template<typename T>
using List = std::list<T>;
override, final
더보기
더보기
class Player
{
public:
    virtual void Attack()
    {
        cout << "Player!" << endl;
    }
};

class Knight : public Player
{
public:
    void Attack() // 최초로 가상함수를 사용했는지? 확인 어려움
    {
        cout << "Knight!" << endl;
    }
    
};

int main()
{
    Player* player = new Knight();

    return 0;
}

자식 클래스에서 최초로 가상함수를 사용했는지 확인하기 어려울 때가 있다.

그리고 시그니쳐가 달라지면 부모에게서 내려온 매서드로 인식하지 못한다.

그래서 재정의(Override)할 때 재정의한 메서드는 override를 붙여 가독성을 확보하고

부모에게서 재정의된 매서드라고 확정한다.

 

추가적으로 이번 자식 클래스에서만 마지막으로 정의하겠다는 뜻으로 final을 붙여주면

다음 자식부터 Override를 막을 수 있다.

오른값 참조 (rvalue reference)
더보기
더보기

C++11에 들어서서 오른값 참조 방식이 등장했다.

일반적인 상황보다는 코어 라이브러리에서 자주 사용된다.

모던 C++의 꽃이라고 할 수 있을 만큼, 속도 향상에서 엄청난 도약을 이루었다.

 

왼값(lvalue) vs 오른값(rvalue)

왼값 : 단일식을 넘어서 계속 지속되는 개체, 개체를 다시 사용/접근

오른값 : 왼값이 아닌 나머지 (임시 값, 열거형, 람다, i++ 등), 임시적으로 스택 메모리에 생성되었다가 소멸됨

 

class Knight
{
public:

public:
    int _hp = 100;
    Pet _pet;
};

void TestKnight_Copy(Knight knight) { }
void TestKnight_LValueRef(Knight& knight) { }
void TestKnight_ConstLValueRef(const Knight& knight) { }
void TestKnight_RValueRef(Knight&& knight) { }

int main()
{
    Knight k1;
    
    TestKnight_LValueRef(Knight());      // Knight()는 오른값, 오류 발생, const참조는 가능함
    TestKnight_ConstLValueRef(Knight()); // 가능함, 다만 readonly
    
    Knight k2;
    k2 = std::move(k1); // rvalue_cast
    TestKnight_RValueRef(k2);
    
    // 주의
    // 오른값 : 왼값이 아니다 -> 오른값은 단일식에서 벗어나면 사용하지 못한다.
    // 오른값 참조 : 오른값만 참조할 수 있기 때문에, 오른값으로 캐스팅이 되었더라도
    // 단일식에서 벗어나 사용할 수 있으면 왼값이다. 왼값은 참조하지 못한다.
    
    return 0;
}

 

왜 굳이 오른값 참조를 사용해야 될까?

원본을 유지하지 않으면 어떤 장점이 있을지 생각해보자

원본 객체를 넘겨 받고 그 원본 객체는 어차피 사라질 임시 객체 이니 아무렇게 사용해도 된다.

// 복사 대입 연산자
void operator=(const Knight& knight)
{
    // 깊은 복사
    _hp = knight._hp;
    
    if (knight._pet)
        _pet = new Pet(*knight._pet);
}

// 이동 대입 연산자
void operator=(Knight&& knight) noexcept
{
    // 얕은 복사
    _hp = knight._hp;
    _pet = knight._pet;
    
    knight._pet = nullptr;
}
전달 참조 (forwarding reference)
더보기
더보기

&&는 오른값 참조로 쓰일 때도 있지만 전달 참조로 쓰일 때가 있다.

전달 참조는 template나 auto와 같이 형식 연역(type deduce)가 일어날 때(만) 발생한다.

왼값을 넣어주면 왼값으로 동작을 하고 오른값을 넣어주면 오른값으로 동작하는 것이 전달참조 이다.

 

주의할 점은 오른값을 넘겨줄 때, 오른값을 넘겨받는 쪽은 오른값을 왼값으로 사용하게 되는 것이기 때문에

넘겨받은 오른값(왼값이 되버림)을 다시 오른값으로 캐스팅 해주어야 원하는 동작을 실행할 수 있다.

 

class Knight
{
public:
    Knight() { cout << "기본 생성자" << endl; }
    Knight(const Knight&) { cout << "복사 생성자" << endl; }
    Knight(Knight&&) noexcept { cout << "이동 생성자" << endl; }
};

void Test_Copy(Knight k)
{

}

template<typename T>
void Test_ForwardingRef(T&& param) // 전달 참조
{
    // 왼값 참조라면 복사, 오른값 참조라면 이동
    Test_Copy(std::forward<T>(param));
}

int main()
{
    Knight k1;
    
    Test_ForwardingRef(std::move(k1));
    Test_ForwardingRef(k1);               // 왼값 참조 가능함
    
    auto&& k2 = k1;               // k2는 왼값
    auto&& k3 = std::move(k1);    // k3는 오른값
    
    return 0;
}
람다
더보기
더보기

함수 객체를 간편하게 만들 수 있다.

enum class ItemType
{
    None,
    Armor,
    Weapon,
    Jewelry,
    Consumable
};

enum class Rarity
{
    Common,
    Rare,
    Unique
};

class Item
{
public:
    Item() { }
    Item(int itemId, Rarity rarity, ItemType type)
        : _itemId(itemId), _rarity(Rarity), _type(type)
    {
        
    }
    
public:
    int _itemId = 0;
    Rarity _rarity = Rarity::Common;
    ItemType _type = ItemType::None;
};

int main()
{
    vector<Item> v;
    v.push_back(Item(1, Rarity::Common, ItemType::Weapon));
    v.push_back(Item(2, Rarity::Common, ItemType::Armor));
    v.push_back(Item(3, Rarity::Rare, ItemType::Jewelry));
    v.push_back(Item(4, Rarity::Unique, ItemType::Weapon));
    
    auto findIt = std::find_if(v.begin(), v.end(), /*람다식*/);

    return 0;
}

람다표현식 : [캡처](매개변수 부분) {구현부}

캡처(capture) : 함수 객체 내부에 변수를 저장하는 개념과 유사 

캡처 모드 : 복사 방식[=](기본), 참조 방식[&], 변수마다 캡처 모드 지정 가능(선호됨)

int itemId = 4;

auto findByItemIdLambd = [=](Item& item)
{
    return item._itemId == itemId;
};

auto findIt = std::find_if(v.begin(), v.end(), findByItemIdLambda);
스마트포인터
더보기
더보기

그냥 기본 포인터만 사용하게 되면 여러 객체가 서로를 가리키고 있는 포인터의 상황에서

메모리 오염이 일어날 확률이 매우 높다.

포인터를 알맞는 정책에 따라 관리하는 객체를 스마트 포인터 라고 한다. (포인터를 래핑해서 사용한다)

shared_ptr, weak_ptr, unique_ptr 이 있다.

 

shared_ptr : 포인터와 참조 카운터로 관리, 참조 카운터가 0이 되면 자동으로 메모리 할당을 해제해준다.

그러나 shared_ptr끼리 서로 참조를 물고 있으면 메모리가 절대 삭제되지 않을 가능성이 생긴다.

// 기본 사용법
shared_ptr<Class> cp = make_shared<Class>();

// 사이클 문제
shared_ptr<Knight> k1 = make_shared<Knight>();

// k1 [ count: 1]
{
    shared_ptr<Knight> k2 = make_shared<Knight>();
    // k1 [count: 1]
    // k2 [count: 1]
    
    k1->_target = k2;
    k2->_target = k1;
    // k1 [count: 2]
    // k2 [count: 2]
}
// k1 [count : 2]
// k2 [count : 1]

// k1은 k2를 주시하고 k2는 k1을 순환구조로 주시해서 
// 참조카운트가 0으로 떨어지지 않게 되어 영원히 메모리를 차지하게된다

weak_ptr : shared_ptr의 사이클 문제를 해결, 참조 카운트가 refCount와 weakCount로 나누어 구성됨. 다만

사용하기전에 .expired() == false로 유효한지 체크를 해야 하고 포인터를 다시 shared_ptr로 바꿔써야되는 번거로움이 있음.

 

unique_ptr : 유일한 객체를 가리키고 싶을 때 사용. 다른 포인터에게 넘겨줄 수 없다. 다른 포인터에게 넘겨주려면 오른값 참조를 이용해야 한다.

 

 

'C++ > 문법' 카테고리의 다른 글

[C++/기초] using vs typedef  (0) 2021.11.11
C++ 중괄호 초기화 {}  (0) 2021.11.11
C++ 콜백 함수  (0) 2021.11.11
C++ 캐스팅(static_cast, dynamic_cast, const_cast, reinterpret_cast)  (0) 2021.11.11
C++ 문자와 문자열  (0) 2021.11.11
Comments