얕은 복사

 

class CPlayer {
public:
    CPlayer(const char* name)
    {
        mName = static_cast<char*>(malloc(strlen(name) + 1));

        strcpy_s(mName, strlen(name) + 1, name);
    }

    ~CPlayer() {
        free(mName);
    }

    const char* GetName() {
        return mName;
    }

private:
    char* mName;
};

int main()
{
    CPlayer player("devhun");

    CPlayer p = player;

    std::cout << p.GetName();

    return 1;
}

 

  • 위 코드는 default 복사 생성자로 인해서 얕은 복사가 발생됩니다. 복사 생성자를 별도로 정의하지 않으면 멤버변수를 그대로 복사하는데, 그럴경우 malloc으로 할당받은 메모리를 가리키는 포인터가 2개로 늘어나는 문제가 발생됩니다.




깊은 복사

 

#include <iostream>
#include <Windows.h>

class CPlayer {
public:
    CPlayer(const char* name)
    {
        mName = static_cast<char*>(malloc(strlen(name) + 1));

        strcpy_s(mName, strlen(name) + 1, name);
    }

    CPlayer(const CPlayer& player) {

        mName = static_cast<char*>(malloc(strlen(player.mName) + 1));

        strcpy_s(mName, strlen(player.mName) + 1, player.mName);
    }

    ~CPlayer() {
        free(mName);
    }

    const char* GetName() {
        return mName;
    }

private:
    char* mName;
};

int main()
{
    CPlayer player("devhun");

    CPlayer p = player;

    std::cout << p.GetName();

    return 1;
}

 

  • 얕은 복사로 인해서 포인터가 복사되는 현상을 없애기 위해서는 복사 생성자를 직접 정의하여 깊은 복사를 하도록 구현해야 합니다. 깊은 복사는 인자로 전달된 인스턴스의 포인터 부분을 동적할당하여 직접 초기화하는 작업을 말합니다.




RAII( Resource Acquisition Is Initialization )이란?


  • RAII( Resource Acquisition Is Initialization )는 리소스 획득은 초기화다 라는 의미를 가지고 있습니다. RAII 기법은 생성자에서 리소스를 획득하고 해당 인스턴스가 스코프를 벗어나면 자동으로 소멸자가 호출되면서 리소스를 해제하는 기법을 말합니다. RAII 패턴이 있기 때문에 C++의 창시자 비야네 스트롭스트룹은 다른 언어에서 사용되는 try/catch에서 finally를 C++에 도입하지 않는다고 하였습니다.




RAII 사용 예시


#include <iostream>
#include <Windows.h>

CRITICAL_SECTION sc;

class CSC {
public:

    CSC(CRITICAL_SECTION& sc)
        :mSC(sc)
    {
        std::cout << "Enter~\n";
        EnterCriticalSection(&mSC);
    }

    ~CSC()
    {
        std::cout << "Leave~\n";
        LeaveCriticalSection(&mSC);
    }

private:

    CRITICAL_SECTION& mSC;
};

int main()
{
    InitializeCriticalSection(&sc);

    {
        CSC sc(sc);
    }

    return 1;
}


  • 위와 같이 동기화 객체를 사용할 때 RAII 디자인 패턴을 사용하면 프로그래머의 실수로 동기화 객체를 해제하는 것을 깜빡하는 실수를 없앨 수 있습니다.

  • 스마트 포인터 또한 RAII 디자인 패턴을 바탕으로 만들어졌습니다.




상속 관계에서 호출할 부모 생성자를 지정하는 방법

#include <iostream>

class CParent {
public:

    CParent()
    {
        std::cout << "Parent\n";
    }

    CParent(const char* comment)
    {
        std::cout << comment;
    }
};

class CChild :private CParent {
public:

    CChild()
    {
        std::cout << "Child\n";
    }

    CChild(const char* comment)
        :CParent(comment)
    {
    }

};

int main()
{
    CChild child("Hello");

    return 1;
}

 

  • 위와같이 상속 관계에서 자식 생성자에서 부모 클래스의 어떤 생성자를 호출할지를 이니셜라이저를 통해 지정할 수 있습니다. 만약, 지정하지 않는 다면은 인자를 아무것도 받지 않는 부모 생성자가 호출됩니다.




const 멤버 함수


#include <iostream>

class CPlayer {
public:

    CPlayer(int age)
        :mAge(age)
    {}

    ~CPlayer() = default;

    void SetAge(int age) {
        mAge = age;
    }

    int GetAge() const {
        return mAge;
    }

private:

    int mAge;
};


int main()
{
    const CPlayer p(20);

    std::cout << p.GetAge();

    return 1;
}

  • const 객체는 const 함수로 정의된 멤버함수만을 호출할 수 있습니다. 또한 const 멤버 함수 내부에서는 멤버 변수를 수정할 수 없습니다.

  • 만약, 멤버 변수가 정의될 때 mutable 키워드를 사용했다면 const 멤버 함수 내부에서도 수정할 수 있습니다.




매크로 함수란?

 

#include <iostream>
#define SQUARE(x) (x * x)

int main()
{
    std::cout << SQUARE(3);
}

 

  • 매크로 함수는 전처리 단계에서 호출부가 정의한 형식 그대로 치환됩니다. 그렇기 때문에 함수 호출로 인한 추가적인 명령어 처리가 없기 때문에 성능상 이점이 있을 수 있습니다.




매크로 함수의 단점

 

#include <iostream>
#define SQUARE(x) (x * x)

int main()
{
    std::cout << SQUARE(++3);

    std::cout << SQUARE("ASDF");
}

 

  • 매크로 함수는 인자로 전달된 형태 그대로 치환하고 데이터 타입을 지정할 수 없기 때문에 사용자가 의도하지 않은 결과가 return 될 수 있습니다.




inline 함수란?

 

#include <iostream>

inline int SQUARE(int x) {
    return  x * x;
}

int main()
{
    std::cout << SQUARE(10);
}

 

  • 매크로 함수를 이용한 함수의 인라인화는 전처리기에 의해서 처리되지만, inline 키워드를 통해 함수를 정의할 경우 컴파일러에 의해서 인라인 처리가 됩니다. 덕분에 데이터 타입을 지정할 수 있고 매크로 함수처럼 형태 그대로 치환되는 형식이 아니기 때문에 매크로 함수보다 이점이 있습니다.




inline 함수의 단점

 

  • inline 키워드를 사용한다고 해서 무조건적으로 inline 처리가 되지 않습니다. 컴파일러 판단에 의해서 inline에 이점이 있을 때 inline 처리가 됩니다.

 

  • 만약, 무조건적으로 inline 처리가 된다 하면 메모리 사용량이 증가되어 관리해야할 페이지 개수가 늘어나고 명령어 캐시 hit율이 떨어지기 때문에 성능이 오히려 떨어질 수 있습니다. 그렇기 때문에 코드양이 적은 함수만이 inline 처리 되는것이 성능에 유리합니다.




volatile 키워드란?

 

  • volatile 키워드는 volatile 키워드를 사용한 변수에 대한 컴파일 최적화에서 제외시키기 위해 사용하는 키워드입니다.




컴파일 최적화란?

 

  • 컴파일러가 프로그래머가 작성한 소스 코드를 컴파일 할 때 불필요한 코드를 개선하거나 제거하여 성능 및 메모리 사용량을 최적화하는 기능을 말합니다.




volatile 키워드와 최적화 컴파일 on/off

 

 

  • 컴파일 옵션에서 최적화 컴파일 '사용 안 함'으로 설정할 경우 모든 코드를 대상으로 컴파일 최적화를 수행하지 않기 때문에 컴파일 최적화를 끈 환경에서는 volatile 키워드는 어떤 기능도 수행하지 않습니다.




volatile과 비순차적 명령어 처리( OOOE : Out-of-order Execution )

 

  • 컴파일러 최적화에 의한 명령어 코드 최적화랑 CPU가 명령어 파이프라인의 최적화를 위해서 사용하는 OOOE는 서로 완전히 다른 기능입니다. 컴파일 최적화는 컴파일러가 진행하고 OOOE는 CPU 내부에서 제공하는 기능이기 때문에 volatile을 사용한다고 해서 OOOE를 막을 수는 없습니다.




volatile 키워드를 사용하는 이유

 

최적화된 코드

 

volatile 키워드 사용한 코드

 

  • 컴파일 최적화로 인해서 이전에 레지스터에 저장된 주소를 재활용하여 명령어를 수행하거나 멀티 쓰레드 환경이나 임베디드 환경에서 프로그래머가 의도하지 않는 로직으로 수행될 수 있기 때문에 필요에 따라서 volatile을 사용해야합니다.




#define, typedef, using 차이


#define


  • 전처리 단계에서 해당 키워드를 사용자가 지정한 값으로 치환합니다.




사용 예시


#define int Number

  • 전처리 단계에서 Number로 작성된 코드를 int로 치환합니다.




typedef


  • 이미 정의된 데이터 타입을 컴파일 타임에 사용자가 지정한 코드로 추가로 정의 합니다.




사용 예시


typedef int Number;

  • 컴파일 단계에서 Number를 int로 정의합니다.




using


  • typedef와 기능은 같으나 typedef가 할 수 없는 템플릿 타입에 대한 추가 정의도 가능합니다.




using Number = int;

template<class T>
using v = std::vector<T>;

  • Number를 int 데이터 타입으로 추가 정의할 수 있고, 템플릿 인자가 필요한 std::vector를 v로 추가 정의가 가능합니다.




typedef vs using

  • typedef와 using은 템플릿 인자 추가 정의를 제외하고는 기능은 똑같습니다. typedef가 존재하는 이유는 하위 호환성을 유지시키기 위해서 typedef를 사용한 추가 정의는 현재도 가능합니다.




'if-else'문과 'switch-case'문의 공통점


  • 'if-else'와 'switch-case'문의 공통점은 조건에 따른 분기를 구분하여 로직을 수행할 수 있습니다.




'if-else'문과 'switch-case'문의 차이점


'if-else'문



  • 'if-else'문은 맞는 조건을 찾을 때 까지 모든 조건을 비교하며 검사합니다.

  • 비교 연산자등을 사용하여 다양하고 구체적인 조건을 요구할 수 있습니다.


'switch-case'문



  • 'switch-case'문은 C/C++에서 switch 테이블을 만들어서 해당하는 조건에 랜덤 액세스로 접근이 가능하기 때문에 일일이 조건을 비교하지 않아도 됩니다.

  • 비교 연산자등을 사용할 수 없습니다.




'if-else'문과 'switch-case'문 결론


  • 분기문이 짧고 구체적이고 다양한 조건을 걸어야할 경우 'if-else'를 사용하는 것이 좋습니다. 만약, 분기문이 길고 다양한 조건문을 사용할 필요가 없다면은 일일이 조건을 검사하지 않고 switch 테이블을 이용해 랜덤 액세스로 접근하는 'switch-case'문을 사용하는 편이 성능상 이점이 있습니다.

래퍼런스( Reference )란?

 

  • 변수의 메모리를 참조할 수 있는 식별자를 추가하는 역할을 합니다.



사용법 예시

int num = 30;

// &(앰퍼샌드)를 이용해 초기화를 합니다.
int &ref = &num;
ref = 40;

// 출력되는 값은 40
std::cout << num;




래퍼런스( Reference )와 포인터( Pointer )의 차이

 

래퍼런스( Reference )

 

int num1 = 10;
int num2 = 30;
int &ref = num2;

// ref 값이 30으로 변경될뿐 참조하는 메모리는 동일
ref = num2;
  • 선언과 동시에 반드시 초기화가 필요합니다. 초기화 이후에는 참조하는 대상을 변경할 수 없습니다.




포인터( Pointer )

 

int num = 10;
int*p = &num;
p = nullptr;
  • 선언 이후 언제든지 참조하는 메모리를 변경할 수 있습니다.




래퍼런스와 포인터의 내부 동작 원리

 

래퍼런스 어셈블리 코드

 

포인터 어셈블리 코드

 

  • 위 어셈블리 코드에서 나온것처럼 포인터와 래퍼런스는 사용 방법과 참조하는 대상을 변경할 수 있는지 여부만 다를뿐 내부 동작 원리는 동일합니다.

포인터 ( Pointer ) 와 const

  • 포인터는 래퍼런스( & )와 달리 참조하는 대상을 언제든지 수정할 수 있지만, const 를 이용해서 참조 대상을 변경하거나 가리키는 값을 수정하는 것을 방지할 수 있습니다.




const 위치의 따른 기능 차이

상수 포인터 ( * const )

int num = 100;

int * const ptr = &num;
  • 상수 포인터는 래퍼런스와 동일하게 초기화 이후 가리키는 대상을 변경할 수가 없습니다.
  • 역참조하여 값을 수정하는건 가능합니다.




상수에 대한 포인터 ( const * )

int num = 100;

int const * ptr1 = &num;

const int * ptr1 = &num;
  • * 의 왼쪽에 const 가 있다면은 포인터가 가리키는 값을 수정할 수 없습니다.
  • 프로그래머 스타일에 따라서 데이터 타입 왼쪽 또는 오른쪽에 const를 작성하며, * 의 왼쪽에만 있으면 기능은 동일합니다.




상수에 대한 상수 포인터 ( const * const )

int num = 100;

int const * const ptr = &num;
  • 참조하는 대상 그리고 역참조 값 또한 수정할 수 없습니다.

+ Recent posts