헤더파일

C++ 정리2 본문

C++

C++ 정리2

헤더파일 2019. 10. 30. 13:50

익명 클래스 형식

 

Microsoft C 확장을 사용하면 이름을 지정하지 않고 다른 구조체 내에서 구조체 변수를 선언할 수 있습니다. 이러한 중첩된 구조체를 익명 구조체라고 합니다. C++에서는 익명 구조체를 허용하지 않습니다.

 

// anonymous_structures.c
#include <stdio.h>

struct phone
{
    int  areacode;
    long number;
};

struct person
{
    char   name[30];
    char   gender;
    int    age;
    int    weight;
    struct phone;    // Anonymous structure; no name needed
} Jim;

int main()
{
    Jim.number = 1234567;
    printf_s("%d\n", Jim.number);
}
//Output: 1234567

 

멤버에 대한 포인터

 

void (Base ::* bfnPrint)() = &Base :: Print;

int main()
{
    Base   *bPtr;
    Base    bObject;
    Derived dObject;
    bPtr = &bObject;    // Set pointer to address of bObject.
    (bPtr->*bfnPrint)();// Print base function
    bPtr = &dObject;    // Set pointer to address of dObject.
    (bPtr->*bfnPrint)();//Print Derive Function
}

 

const char * (Window::*pfnwGC)() = &Window::GetCaption;
bool (Window::*pfnwSC)( const char * ) = &Window::SetCaption;

int main()
{
	Window wMainWindow;
	Window *pwChildWindow = new Window;
	char   *szUntitled    = "Untitled -  ";
	int    cUntitledLen   = strlen( szUntitled );

	strcpy_s( wMainWindow.*pwCaption, cUntitledLen, szUntitled );
	(wMainWindow.*pwCaption)[cUntitledLen - 1] = '1';     //same as
	//wMainWindow.SzWinCaption [cUntitledLen - 1] = '1';
	strcpy_s( pwChildWindow->*pwCaption, cUntitledLen, szUntitled );
	(pwChildWindow->*pwCaption)[cUntitledLen - 1] = '2'; //same as //pwChildWindow->szWinCaption[cUntitledLen - 1] = '2';
}

 

위의 예에서 pwCaption 클래스의 모든 멤버에 대 한 포인터 Window 형식이 있는 char*합니다. pwCaption의 형식은 char * Window::*입니다. 다음 코드에서는 SetCaption  GetCaption 멤버 함수에 대한 포인터를 선언합니다.

참조하는 객체 형식에 따라 . 과 *을 결정합니다.

 

멤버에 대한 포인터 제한

 

정적 멤버의 주소는 멤버에 대한 포인터가 아닙니다. 정적 멤버의 주소는 정적 멤버의 인스턴스 하나에 대한 일반적인 포인터입니다. 일반적인 주소 지정된 된 클래스의 모든 개체에 대한 정적 멤버의 인스턴스가 하나만 존재 하기 때문에 ( & ) 및 역참조 (*) 연산자를 사용할 수 있습니다.

 

멤버 및 가상 함수에 대한 포인터

 

멤버 포인터 함수를 통해 가상 함수를 호출하는 것은 함수가 직접 호출된 것과 같으며, v-table에서 올바른 함수가 조회된 다음 호출됩니다.

 


한정자의 의미

 

const 멤버 데이터를 변경할 수 없습니다. 되지 않는 멤버 함수를 호출할 수 없습니다 const합니다.
volatile 액세스할 때마다 메모리에서 멤버 데이터를 로드하고 특정 최적화를 비활성화합니다.

비트 필드

 

// bit_fields1.cpp
// compile with: /LD
struct Date {
   unsigned short nWeekDay  : 3;    // 0..7   (3 bits)
   unsigned short nMonthDay : 6;    // 0..31  (6 bits)
   unsigned short nMonth    : 5;    // 0..12  (5 bits)
   unsigned short nYear     : 8;    // 0..100 (8 bits)
};

 

상수-식 구조에서 멤버가 차지 하는 비트 수를 지정 합니다. 익명 비트 필드, 식별자가 없는 비트 필드 멤버를 안쪽 여백에 사용할 수 있습니다. 다음 그림에서는 Date 형식 개체의 개념적 메모리 레이아웃을 보여 줍니다.


날짜 개체의 메모리 레이아웃

사실은 nYear 길이가 8 비트 이며 선언 된 형식의 단어 경계를 벗어납니다 unsigned short합니다. 따라서 새로운 시작 부분에서 시작 됩니다. 모든 비트 필드가 기본 형식의 한 개체에 맞아야 할 필요는 없습니다. 선언에서 요청된 비트 수에 따라 새 스토리지 단위가 할당됩니다.

 

// bit_fields2.cpp
// compile with: /LD
struct Date {
   unsigned nWeekDay  : 3;    // 0..7   (3 bits)
   unsigned nMonthDay : 6;    // 0..31  (6 bits)
   unsigned           : 0;    // Force alignment to next boundary.
   unsigned nMonth    : 5;    // 0..12  (5 bits)
   unsigned nYear     : 8;    // 0..100 (8 bits)
};

 

다음과 같이 비트 필드의 멤버를 선언하는 자료형보다 큰 비트 수는 지정할 수 없습니다.

struct Flags {

      unsigned int a : 37; // 컴파일 에러. unsigned int보다 큰 비트 수는 지정할 수 없음

}

 


멤버 액세스 제어(C++)


기본 클래스의 멤버 액세스

 

컴파일 타임에 오류 결정.

 

액세스 제어 및 정적 멤버

// access_control.cpp
class Base
{
public:
    int Print();             // Nonstatic member.
    static int CountOf();    // Static member.
};

// Derived1 declares Base as a private base class.
class Derived1 : private Base
{
};
// Derived2 declares Derived1 as a public base class.
class Derived2 : public Derived1
{
    int ShowCount();    // Nonstatic member.
};
// Define ShowCount function for Derived2.
int Derived2::ShowCount()
{
   // Call static member function CountOf explicitly.
    int cCount = Base::CountOf();     // OK.

   // Call static member function CountOf using pointer.
    cCount = this->CountOf();  // C2247. Conversion of
                               //  Derived2 * to Base * not
                               //  permitted.
    return cCount;
}

Derive1에서 Base로의 변환은 가능하지만 Derive2에서 Base로 변환은 허용안됨. -> Derive1이 private으로 Base을 상속받았기 때문에.

 

가상 함수에 대한 액세스

 

액세스 제어에 적용할 가상 함수는 함수 호출에 사용 되는 형식에 의해 결정 됩니다. 함수의 선언 재정의는 지정된 형식에 대한 액세스 제어에 영향을 주지 않습니다. 예를 들어:

// access_to_virtual_functions.cpp
class VFuncBase
{
public:
    virtual int GetState() { return _state; }
protected:
    int _state;
};

class VFuncDerived : public VFuncBase
{
private:
    int GetState() { return _state; }
};

int main()
{
   VFuncDerived vfd;             // Object of derived type.
   VFuncBase *pvfb = &vfd;       // Pointer to base type.
   VFuncDerived *pvfd = &vfd;    // Pointer to derived type.
   int State;

   State = pvfb->GetState();     // GetState is public.
   State = pvfd->GetState();     // C2248 error expected; GetState is private;
}

 

함수 자체는 vtable을 따라 파생클래스의 GetState 함수가 불립니다.

 

 


Friend (C++)


Friend 선언

 

전역 범위를 갖는 함수는 프로토타입 이전에 friend로 선언될 수 있지만, 멤버 함수는 전체 클래스 선언이 나타나기 전에 friend로 선언될 수 없습니다. 다음 코드에서는 이러한 작업이 실패하는 이유를 보여 줍니다.

 

class ForwardDeclared;   // Class name is known.
class HasFriends
{
    friend int ForwardDeclared::IsAFriend();   // C2039 error expected
};

 

 

C++11부터는 두 가지 형태의 클래스의 friend 선언을 지원합니다.

 

friend class F; - 아직 클래스 정의 전일 떄.

friend F; - 클래스가 정의되어 있을때.

 

namespace NS
{
    class M
    {
        class friend F;  // Introduces F but doesn't define it
    };
}


namespace NS
{
    class M
    {
        friend F; // error C2433: 'NS::F': 'friend' not permitted on data declarations
    };
}

템플릿을 이용해 친구 관계를 설정할 수도 있다.

template <typename T>
class my_class
{
	friend T;
	//...


	private:
	int num = 3;
};


class Wow
{
public:
	void Print(my_class<Wow> c)
	{
		cout << c.num;
	}
};

int main()
{
	my_class<Wow> a;
	Wow w;
	w.Print(a);
}

 


친구 관계의 의미

상속에 의해 작동하지 않고 명시적으로 지정되지 않은 경우 상호적이 아닙니다. Base클래스는 aFriend 클래스의 멤버변수에 접근할 수 없습니다.

 


constexpr


1) 변수에서의 사용

 

const와 constexpr의 주요 차이점은 const 변수의 초기화를 런타임까지 지연시킬 수 있는 반면, constexpr 변수는 반드시 컴파일 타임에 초기화가 되어 있어야 한다.

초기화가 안 되었거나, 상수가 아닌 값으로 초기화 시도시 컴파일이 되지 않는다.

 

  1. constexpr float x = 42.f;    // OK

  2. constexpr float y { 108.f }; // OK

  3. constexpr int i;             // error C2737: 'i': 'constexpr' 개체를 초기화해야 합니다.

  4. int j = 0;

  5. constexpr int k = j + 1;     // error C2131 : 식이 상수로 계산되지 않았습니다.

 

변수에 constexpr 사용시 const 한정자를 암시한다.

 

2) 함수에서의 사용

 

constexpr을 함수 반환값에 사용할 때는 다음의 제약들이 따른다.

  • 가상으로 재정의된 함수가 아니어야 한다.
  • 반환값의 타입은 반드시 LiteralType이어야 한다.

 

함수에 constexpr을 붙일 경우 inline을 암시한다.

즉, 컴파일 타임에 평가하기 때문이며, inline 함수들과 같이 컴파일된다.

 

C++11에서는 함수 본문에 지역변수를 둘 수 없고, 하나의 반환 표현식만이 와야 하는 제약이 있었으나, C++14부터는 이러한 제약이 사라졌다.

 

반환값을 컴파일 때 확인 할 수 있다는 장점!

constexpr int factorial(int n)
{
    return n <= 1 ? 1 : (n * factorial(n - 1));
}

이런 재귀함수가 있을 때 런타임때 재귀하지 않고 미리 값을 평가해 상수값으로 설정한다.

 

3) 생성자 함수에서의 사용

 

LiteralType 클래스를 생성할 때 constexpr 생성자를 사용할 수 있다.

이 때의 제약은 다음과 같다.

  • 모든 생성자 함수의 매개변수들 역시 LiteralType들이어야 한다.
  • 어떤 클래스로부터 상속받지 않아야 한다.

constexpr이 적용된 함수의 매개변수는 반드시 LiteralType이어야 한다.

 

객체는 컴파일 타임에 만들어질 수 있고 다른 constexpr 함수의 매개변수로서 작동할 때 활용할 수 있습니다.

 

class ConstString
{
public:
    template<std::size_t N>
    constexpr ConstString(const char(&a)[N])
    : p(a), sz(N - 1)
    {
    }
};



constexpr std::size_t CountLowercase(ConstString s, std::size_t n = 0, std::size_t c = 0)
{
    return n == s.size() ?
                    c :
                    s[n] >= 'a' && s[n] <= 'z' ?
                        CountLowercase(s, n+1, c+1) :
                        CountLowercase(s, n+1, c);
}

 

앞서 constexpr 함수 내부에서 불가능한 작업으로 리터럴(Literal) 타입이 아닌 변수의 정의라고 이야기 하였습니다. 리터럴 타입은 쉽게 생각하면 컴파일러가 컴파일 타임에 정의할 수 있는 타입이라고 생각하시면 됩니다. C++ 에서 정의하는 바로는;

  • void 

  • 스칼라 타입 (char, int, bool, long, float, double) 등등

  • 레퍼런스 타입

  • 리터럴 타입의 배열

  • 혹은 아래 조건들을 만족하는 타입

    • 디폴트 소멸자를 가지고

    • 다음 중 하나를 만족하는 타입

      • 람다 함수

      • Arggregate 타입 (사용자 정의 생성자, 소멸자가 없으며 모든 데이터 멤버들이 public)
        쉽게 말해 pair 같은 애들을 이야기함

      • constexpr 생성자를 가지며 복사 및 이동 생성자가 없음


생성자

 

 

생성자는 필요에 따라 멤버 초기화 목록을 사용할 수 있습니다. 이는 생성자 본문에서 값을 할당 하는 것 보다 더 효율적으로 클래스 멤버를 초기화 하는 방법입니다. 다음 예제에서는 오버로드 된 세 개의 생성자를 사용 하는 클래스 Box를 보여 줍니다. 마지막 두 가지 사용 멤버 초기화 목록은 다음과 같습니다.

 

int a; a = 10; 과 int a =10; 의 차이입니다. 메모리 접근을 한 번 줄일 수 있습니다.

class Box {
public:
    // Default constructor
    Box() {}

    // Initialize a Box with equal dimensions (i.e. a cube)
    explicit Box(int i) : m_width(i), m_length(i), m_height(i) // member init list
    {}

    // Initialize a Box with custom dimensions
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}

    int Volume() { return m_width * m_length * m_height; }

private:
    // Will have value of 0 when default constructor is called.
    // If we didn't zero-init here, default constructor would
    // leave them uninitialized with garbage values.
    int m_width{ 0 };
    int m_length{ 0 };
    int m_height{ 0 };
};

기본값이 아닌 생성자가 선언된 경우 컴파일러는 기본 생성자를 제공하지 않습니다.

 

class Box {
public:
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height){}
private:
    int m_width;
    int m_length;
    int m_height;

};

int main(){

    Box box1(1, 2, 3);
    Box box2{ 2, 3, 4 };
    Box box3; // C2512: no appropriate default constructor available
}

클래스에 기본 생성자가 없는 경우 대괄호 구문만 사용하여 해당 클래스의 개체 배열을 생성할 수 없습니다. 예를 들어 이전 코드 블록에서 다음과 같이 Box 배열을 선언할 수 없습니다. 하지만 초기화 목록을 이용한 초기화는 가능합니다.

Box boxes[3]; // C2512: no appropriate default constructor available

Box boxes[3]{ { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 } }; //OK

 


복사 생성자

 

복사 생성자 는 동일한 형식의 개체에서 멤버 값을 복사 하여 개체를 초기화 합니다. 클래스 멤버가 스칼라 값과 같은 모든 단순 형식인 경우에는 컴파일러에서 생성 된 복사 생성자로 충분하며 사용자가 직접 정의 하지 않아도 됩니다. 클래스에 더 복잡한 초기화가 필요한 경우 사용자 지정 복사 생성자를 구현 해야 합니다. 예를 들어 클래스 멤버가 포인터인 경우 새 메모리를 할당하고 깊은 복사를 하려면 복사 생성자를 정의해야 합니다. 컴파일러에서 생성된 복사 생성자는 새 포인터가 다른 메모리 위치를 계속 가리키도록 포인터를 복사 하기만 합니다.

 

복사 생성자는 다음 형식중 하나입니다.

    Box(Box& other); // Avoid if possible--allows modification of other.
    Box(const Box& other);
    Box(volatile Box& other);
    Box(volatile const Box& other);

    // Additional parameters OK if they have default values
    Box(Box& other, int i = 42, string label = "Box");

 

 

이동 생성자

 

이동 생성자 는 원래 데이터를 복사 하지 않고 기존 개체의 데이터 소유권을 새 변수로 이동 하는 특수 멤버 함수입니다. 첫 번째 매개 변수로 rvalue 참조를 사용 하고 추가 매개 변수에는 기본값이 있어야 합니다. 이동 생성자는 큰 개체를 전달할 때 프로그램의 효율성을 크게 향상 시킬 수 있습니다.

 

Box(Box&& other);

 

소멸 되려고 하는 동일한 형식의 다른 개체에서 개체를 초기화 하고 더 이상 해당 리소스를 필요로 하지 않는 특정 상황에서 컴파일러는 이동 생성자를 선택 합니다. 다음 예제에서는 오버로드 확인으로 이동 생성자를 선택하는 한 가지 사례를 보여 줍니다.출 하는 생성자에서 반환 된 값은 xvalue 값 (만료 값)입니다. 이는 변수에 할당 되지 않으므로 범위를 벗어나는 것입니다. 이 예제에 대한 동기를 제공 하기 위해 콘텐츠를 나타내는 문자열의 많은 벡터를 제공 해 보겠습니다. 벡터와 해당 문자열을 복사 하는 대신 이동 생성자가 만료 값 "box"에서 이를 이동하여 이제 벡터가 새 개체에 속하도록 합니다. string 클래스가 자체 이동 생성자를 구현 하기 때문에 std::move에 대한 호출이 모두 필요 합니다.

class Box {
public:
    Box() { std::cout << "default" << std::endl; }
    Box(int width, int height, int length)
       : m_width(width), m_height(height), m_length(length)
    {
        std::cout << "int,int,int" << std::endl;
    }
    Box(Box& other)
       : m_width(other.m_width), m_height(other.m_height), m_length(other.m_length)
    {
        std::cout << "copy" << std::endl;
    }
    Box(Box&& other) : m_width(other.m_width), m_height(other.m_height), m_length(other.m_length)
    {
        m_contents = std::move(other.m_contents);
        std::cout << "move" << std::endl;
    }
    int Volume() { return m_width * m_height * m_length; }
    void Add_Item(string item) { m_contents.push_back(item); }
    void Get_Contents()
    {
        for (const auto& item : m_contents)
        {
            cout << item << " ";
        }
    }
private:
    int m_width{ 0 };
    int m_height{ 0 };
    int m_length{ 0 };
    vector<string> m_contents;
};

Box get_Box()
{
    Box b(5, 10, 18); // "int,int,int"
    b.Add_Item("Toupee");
    b.Add_Item("Megaphone");
    b.Add_Item("Suit");

    return b;
}

int main()
{
    Box b; // "default"
    Box b1(b); // "copy"
    Box b2(get_Box()); // "move"
    cout << "b2 contents: ";
    b2.Get_Contents(); // Prove that we have all the values

    char ch;
    cin >> ch; // keep window open
    return 0;
}

 

클래스가 이동 생성자를 정의 하지 않는 경우 사용자가 선언 된 복사 생성자, 복사 할당 연산자, 이동 할당 연산자 또는 소멸자가 없으면 컴파일러가 암시적 항목을 생성 합니다. 명시적 또는 암시적 이동 생성자가 정의 되지 않은 경우 이동 생성자를 사용 하는 작업은 복사 생성자를 대신 사용 합니다. 클래스가 이동 생성자 또는 이동 할당 연산자를 선언 하는 경우 암시적으로 선언 된 복사 생성자가 삭제 된 것으로 정의 됩니다.

암시적으로 선언 된 이동 생성자는 클래스 형식인 멤버가 소멸자를 누락 하거나 컴파일러가 이동 작업에 사용할 생성자를 결정할 수 없는 경우 삭제 된 것으로 정의 됩니다.

 

int a = 3;

 

위 표현식에서 먼저 'a' 를 살펴보도록 합시다. 우리는 a 가 메모리 상에서 존재하는 변수 임을 알고 있습니다. 즉 'a' 의 주소값을 & 연산자를 통해 알아 낼 수 있다는 것입니다. 우리는 보통 이렇게 주소값을 취할 수 있는 값을 좌측값 (lvalue) 라고 부릅니다. 그리고 좌측값은 어떠한 표현식의 왼쪽 오른쪽 모두에 올 수 있습니다 (왼쪽에만 와야 하는게 아닙니다).

반면에 오른쪽에 있는 '3' 을 살펴보도록 합시다. 우리가 '3' 의 주소값을 취할 수 있나요? 아닙니다. '3' 은 왼쪽의 'a' 와는 다르게, 위 표현식을 연산할 때만 잠깐 존재할 뿐 위 식이 연산되고 나면 사라지는 값입니다. 즉, '3' 은 실체가 없는 값입니다.

이렇게, 주소값을 취할 수 없는 값을 우측값 (rvalue) 라고 부릅니다. 이름에도 알 수 있듯이, 우측값은 식의 오른쪽에만 항상 와야 합니다. 좌측값이 식의 왼쪽 오른쪽 모두 올 수 있는반면, 우측값은 식의 오른쪽에만 존재해야 합니다.

 

int a; // a 는 좌측값 
int& l_a = a; // l_a 는 좌측값 레퍼런스 
int& r_b = 3; // 3 은 우측값. 따라서 오류

여태까지 우리가 다루어왔던 레퍼런스는 '좌측값' 에만 레퍼런스를 가질 수 있습니다. 예를 들어서, a 의 경우 좌측값 이기 때문에, a 의 좌측값 레퍼런스인 l_a 를 만들 수 있습니다. 반면에 3 의 경우 우측값이기 때문에, 우측값의 레퍼런스인 r_b 를 만들 수 없습니다. 따라서 이 문장은 오류가 발생하게 됩니다. 이와 같이 & 하나를 이용해서 정의하는 레퍼런스를 좌측값 레퍼런스 (lvalue reference) 라고 부르고, 좌측값 레퍼런스 자체도 좌측값이 됩니다.

그럼 다른 예제를 살펴보도록 합시다.

MyString str3 = str1 + str2;

//를 다시 살펴보도록 합시다. 위 문장은

MyString str3(str1.operator+(str2));

//와 동일합니다. 그런데, operator+ 의 정의를 살펴보면,


MyString MyString::operator+(const MyString &s)

//로 우측값을 리턴하고 있는데, 이 우측값이 어떻게 좌측값 레퍼런스를 인자로 받는,

MyString(const MyString &str);

//를 호출 시킬 수 있었을까요? 이는 & 가 좌측값 레퍼런스를 의미하지만, 예외적으로

const T&

그렇다면 앞서 MyString 에서 지적한 문제를 해결할 생성자의 경우 어떠한 방식으로 작동해야 할까요?

위와 같이 간단합니다. str3 생성 시에 임시로 생성된 객체의 string_content 가리키는 문자열의 주소값을 str3  string_content 로 해주면 됩니다. 문제는 이렇게 하게 되면, 임시 객체가 소멸 시에 string_content 를 메모리에서 해제하게 되는데, 그렇게 되면 str3 가 가리키고 있던 문자열이 메모리에서 소멸되게 됩니다. 따라서 이를 방지 하기 위해서는, 임시 생성된 객체의 string_content  nullptr 로 바꿔주고, 소멸자에서 string_content  nullptr 이면 소멸하지 않도록 해주면 됩니다.

 

MyString::MyString(MyString&& str) {
  std::cout << "이동 생성자 호출 !" << std::endl;
  string_length = str.string_length;
  string_content = str.string_content;
  memory_capacity = str.memory_capacity;

  // 임시 객체 소멸 시에 메모리를 해제하지
  // 못하게 한다.
  str.string_content = nullptr;
}

 

Vector나 STL 컨테이너에 경우 이동 생성하는 과정에서 예외가 발생했더라면, 꽤나 골치아파집니다. 복사 생성을 하였을 경우 새로 할당한 메모리를 소멸시켜 버려도, 기존의 메모리에 원소들이 존재하기 때문에 상관 없지만, 이동 생성의 경우 기존의 메모리에 원소들이 모두 이동되어서 사라져버렸기에, 새로 할당한 메모리를 섯불리 해제해버릴 수 없기 때문입니다. 따라서 vector 의 경우 이동 생성자에서 예외가 발생하였을 때 이를 제대로 처리할 수 없습니다. 이는 C++ 의 다른 컨테이너들도 동일합니다. 이 때문에 vector 는 이동 생성자가 noexcept 가 아닌 이상 이동 생성자를 사용하지 않습니다.

 

class MyString {
	char *string_content;  // 문자열 데이터를 가리키는 포인터
	int string_length;     // 문자열 길이

	int memory_capacity;  // 현재 할당된 용량

public:
	MyString();

	// 문자열로 부터 생성
	MyString(const char *str);

	// 복사 생성자
	MyString(const MyString &str);

	// 이동 생성자
	MyString(MyString &&str) noexcept;

	~MyString();
};

MyString::MyString() {
	std::cout << "생성자 호출 ! " << std::endl;
	string_length = 0;
	memory_capacity = 0;
	string_content = NULL;
}

MyString::MyString(const char *str) {
	std::cout << "생성자 호출 ! " << std::endl;
	string_length = strlen(str);
	memory_capacity = string_length;
	string_content = new char[string_length];

	for (int i = 0; i != string_length; i++) string_content[i] = str[i];
}
MyString::MyString(const MyString &str) {
	std::cout << "복사 생성자 호출 ! " << std::endl;
	string_length = str.string_length;
	string_content = new char[string_length];

	for (int i = 0; i != string_length; i++)
		string_content[i] = str.string_content[i];
}
MyString::MyString(MyString &&str) noexcept {
	std::cout << "이동 생성자 호출 !" << std::endl;
	string_length = str.string_length;
	string_content = str.string_content;
	memory_capacity = str.memory_capacity;

	// 임시 객체 소멸 시에 메모리를 해제하지
	// 못하게 한다.
	str.string_content = nullptr;
}
MyString::~MyString() {
	if (string_content) delete[] string_content;
}

int main() {
	MyString s("abc");
	std::vector<MyString> vec;
	vec.resize(0);

	std::cout << "첫 번째 추가 ---" << std::endl;
	vec.push_back(s);
	std::cout << "두 번째 추가 ---" << std::endl;
	vec.push_back(s);
	std::cout << "세 번째 추가 ---" << std::endl;
	vec.push_back(s);
}

호출결과
생성자 호출 !
첫 번째 추가 ---
복사 생성자 호출 !
두 번째 추가 ---
복사 생성자 호출 !
이동 생성자 호출 !
세 번째 추가 ---
복사 생성자 호출 !
이동 생성자 호출 !
이동 생성자 호출 !

추상 클래스


추상 클래스는 보다 구체적인 클래스가 파생될 수 있는 일반 개념의 식 역할을 합니다. 추상 클래스 형식의 개체를 만들 수는 없지만, 추상 클래스 형식에 대한 포인터와 참조를 사용할 수 있습니다. 순수 가상 함수가 하나 이상 포함된 클래스는 추상 클래스로 간주됩니다. 추상 클래스에서 파생된 클래스는 순수 가상 함수를 구현해야 합니다. 이렇게 하지 않으면 파생된 클래스도 추상 클래스가 됩니다. 예제에 표시 되는 것이 좋습니다. Account 클래스의 용도는 일반적인 기능을 제공하는 것이지만, Account 형식의 개체는 너무 일반적이어서 유용하게 사용하기가 어렵습니다. 따라서 Account는 적합한 추상 클래스 후보입니다.

// deriv_AbstractClasses.cpp
// compile with: /LD
class Account {
public:
   Account( double d );   // Constructor.
   virtual double GetBalance();   // Obtain balance.
   virtual void PrintBalance() = 0;   // Pure virtual function.
private:
    double _balance;
};

추상 클래스에 대한 제한

 

추상 클래스는 다음 용도로 사용할 수 없습니다.

  • 변수 또는 멤버 데이터

  • 인수 형식

  • 함수 반환 형식

  • 명시적 변환 형식

또 다른 제한 사항은 추상 클래스에 대한 생성자가 직/간접적으로 순수 가상 함수를 호출하는 경우 결과가 정의되지 않는다는 것입니다. 하지만 추상 클래스의 생성자 및 소멸자는 다른 멤버 함수를 호출할 수 있습니다.

 


이동할당연산자

 


다음 예제와 같이 클래스 형식에 대한 rvalue 참조를 매개 변수로 사용하고 클래스 형식에 대한 참조를 반환하는 빈 할당 연산자를 정의합니다. 이동 할당 연산자에서 개체를 자체에 할당하려는 경우 작업을 수행하지 않는 조건문을 추가합니다. 조건문에서 할당될 개체로부터 모든 리소스(예: 메모리)를 해제합니다. 다음 예제에서는 할당될 개체로부터 _data 멤버를 해제합니다.

 

// Move constructor.
MemoryBlock(MemoryBlock&& other)
   : _data(nullptr)
   , _length(0)
{
   std::cout << "In MemoryBlock(MemoryBlock&&). length = "
             << other._length << ". Moving resource." << std::endl;

   // Copy the data pointer and its length from the
   // source object.
   _data = other._data;
   _length = other._length;

   // Release the data pointer from the source object so that
   // the destructor does not free the memory multiple times.
   other._data = nullptr;
   other._length = 0;
}

// Move assignment operator.
MemoryBlock& operator=(MemoryBlock&& other)
{
   std::cout << "In operator=(MemoryBlock&&). length = "
             << other._length << "." << std::endl;

   if (this != &other)
   {
      // Free the existing resource.
      delete[] _data;

      // Copy the data pointer and its length from the
      // source object.
      _data = other._data;
      _length = other._length;

      // Release the data pointer from the source object so that
      // the destructor does not free the memory multiple times.
      other._data = nullptr;
      other._length = 0;
   }
   return *this;
}

 

다음 예제에서는 이동 의미 체계를 통해 애플리케이션의 성능을 향상시키는 방법을 보여 줍니다. 이 예제에서는 벡터 개체에 두 요소를 추가한 다음 기존의 두 요소 사이에 새 요소를 삽입합니다. vector 클래스는 이동 의미 체계를 복사 하는 대신 벡터의 요소를 이동 하 여 삽입 작업을 효율적으로 수행 합니다.

// rvalue-references-move-semantics.cpp
// compile with: /EHsc
#include "MemoryBlock.h"
#include <vector>

using namespace std;

int main()
{
   // Create a vector object and add a few elements to it.
   vector<MemoryBlock> v;
   v.push_back(MemoryBlock(25));
   v.push_back(MemoryBlock(75));

   // Insert a new element into the second position of the vector.
   v.insert(v.begin() + 1, MemoryBlock(50));
}
출력
In MemoryBlock(size_t). length = 25.
In MemoryBlock(MemoryBlock&&). length = 25. Moving resource.
In ~MemoryBlock(). length = 0.
In MemoryBlock(size_t). length = 75.
In MemoryBlock(MemoryBlock&&). length = 25. Moving resource.
In ~MemoryBlock(). length = 0.
In MemoryBlock(MemoryBlock&&). length = 75. Moving resource.
In ~MemoryBlock(). length = 0.
In MemoryBlock(size_t). length = 50.
In MemoryBlock(MemoryBlock&&). length = 50. Moving resource.
In MemoryBlock(MemoryBlock&&). length = 50. Moving resource.
In operator=(MemoryBlock&&). length = 75.
In operator=(MemoryBlock&&). length = 50.
In ~MemoryBlock(). length = 0.
In ~MemoryBlock(). length = 0.
In ~MemoryBlock(). length = 25. Deleting resource.
In ~MemoryBlock(). length = 50. Deleting resource.
In ~MemoryBlock(). length = 75. Deleting resource.

강력한 프로그래밍

 

리소스 누수를 방지하려면 항상 이동 할당 연산자에서 메모리, 파일 핸들 및 소켓과 같은 리소스를 해제합니다.

리소스의 복구할 수 없는 소멸을 방지하려면 이동 할당 연산자에서 자체 할당을 적절하게 처리합니다.

사용자 클래스에 이동 생성자와 이동 할당 연산자를 둘 다 제공하는 경우 이동 할당 연산자를 호출하는 이동 생성자를 작성하여 중복 코드를 제거할 수 있습니다. 다음 예제에서는 이동 할당 연산자를 호출하는 이동 생성자의 수정된 버전을 보여 줍니다.

// Move constructor.
MemoryBlock(MemoryBlock&& other)
   : _data(nullptr)
   , _length(0)
{
   *this = std::move(other);
}

복사할당연산자


ClassName& operator=(const ClassName& x)

ClassName( const ClassName& );

 

복사 생성자를 선언하지 않으면 컴파일러는 자동으로 멤버 단위 복사 생성자를 생성합니다. 복사 할당 연산자를 선언하지 않으면 컴파일러는 자동으로 멤버 단위 복사 할당 연산자를 생성합니다. 복사 생성자의 선언은 컴파일러에서 생성된 복사 할당 연산자를 숨기지 않으며 그 반대의 경우에도 마찬가지입니다. 둘 중 하나를 구현하는 경우 다른 하나도 구현하여 코드의 의미를 분명히 하는 것이 좋습니다.

 

컴파일러에서 생성된 복사 생성자

 

사용자 정의 복사 생성자와 같은 컴파일러 생성 복사 생성자가 형식의 단일 인수 "에 대 한 참조 클래스 이름." 모든 기본 클래스와 멤버 클래스 형식의 단일 인수를 선언 하는 복사 생성자가 있는 경우는 예외입니다 상수 클래스 이름을&합니다. 이 경우 컴파일러 생성 복사 생성자의 인수는 또한 const합니다.

복사 생성자에 대 한 인수 형식이 없는 경우 const를 복사 하 여 초기화를 const 개체에 오류가 발생 합니다. 그 반대는 불가능 합니다. 인수가 상수, 하지 않은 개체를 복사 하 여 초기화할 수 있습니다 const합니다.

컴파일러에서 생성 된 할당 연산자 관련해 서 동일한 패턴을 따릅니다 const입니다. 형식의 단일 인수 클래스 이름 & 형식의 인수를 수행 하는 모든 기본 및 멤버 클래스의 할당 연산자 const 클래스 이름&합니다. 이 경우 클래스의 생성 된 할당 연산자는 한 const 인수입니다.

 

 

Return value optimization(RVO)

RVO란 함수가 특정 값을 반환할 때 객체를 생성하고 복사, 소멸시키는 삼중 오버헤드를 단순히 객체 생성 한 번으로 끝내도록 만드는 최적화이다. 우선 아래의 코드를 보자.

A wow(A& a)
{
	return a;
}

A wow2()
{
	return A();
}

int main() {
	A b;
	A c = wow(b);
        A d = wow2();
}

c같은 경우는 어차피 레퍼런스 된 a를 리턴하는 연산이므로 함수를 실행조차 하지 않고 복사생성자로 퉁쳐버린다. 함수안에서 하는 일은 복사생성자에서 하겠거니 한다. 왜냐면 레퍼런스인 a의 상태가 바뀌고 복사생성하면 c의 상태도 a의 상태와 동일할 것이기 때문이다.

d같은 경우는 d를 기본생성자로 생성하는 경우와 같으므로 기본생성자로 퉁쳐버린다.

'C++' 카테고리의 다른 글

캐스팅 정리  (0) 2019.11.17
스레드 프로그래밍  (0) 2019.11.06
C++ 정리  (0) 2019.10.29
문자열 함수  (0) 2018.06.11
알고리즘 함수  (0) 2018.05.31
Comments