[new와 placement new]

 

메모리를 할당하면서 객체를 생성하는 new 연산과 달리, placement new는 이미 확보된 메모리 공간에 객체를 생성할 때 사용하는 특수한 new 연산자입니다. 

 

> new

MyClass* obj = new MyClass();

 

new는 위와 같이 실시하며, 동적 메모리에서 공간을 할당한 뒤, 그 공간에서 생성자를 호출합니다.

 

> placement new

char buffer[sizeof(MyClass)];
MyClass obj = new(buffer) MyClass();

 

placement new는 미리 확보된 메모리 공간(위에서는 buffer)가 존재하고, 그 공간을 생성자 호출용으로만 활용합니다. 이 과정에서 새 메모리 할당은 발생하지 않습니다.

 

 

[placement new 문법]

 

연산자 정의는 다음과 같이 이루어져 있습니다.

void* operator new(size_t size, void* ptr) noexcept;
////////////////////////////////////////////////////
T* obj = new(ptr) T(constructor_args...);

 

확보된 ptr 주소를 인자로 받아, 그 위치에 객체를 생성하는 것입니다. 아래는 실제 사용 예시에 대한 코드입니다.

#include <iostream>
#include <new> // placement new를 사용하려면 필요

class MyClass
{
public:
    int Value;

    MyClass(int _Value) : Value(_Value)
    {
        std::cout << "Constructor: " << Value << std::endl;
    }

    ~MyClass()
    {
        std::cout << "Destructor: " << Value << std::endl;
    }
};

int main()
{
    // 메모리만 확보
    alignas(MyClass) char buffer[sizeof(MyClass)];

    // 확보된 메모리에 객체 생성
    MyClass* obj = new(buffer) MyClass(123);

    std::cout << "Value = " << obj->Value << std::endl;

    // 명시적으로 소멸자 호출 (delete X)
    obj->~MyClass();

    return 0;
}

///////////////////////////////////////////
<출력>
Constructor: 123
Value = 123
Destructor: 123

 

 

[사용 이유]

 

placement new의 활용 목적 대부분은 메모리풀에서 활용하기 위함에 있습니다. 미리 큰 버퍼를 만들어두고, placement new를 통해 필요한 위치에 객체를 생성하는 것입니다.

void* pool = malloc(sizeof(MyClass) * 100);
MyClass* obj = new(pool) MyClass();

/////////////////////////////////////////////////
<메모리풀 예시>
class MemoryPool
{
    char* Pool;
    size_t Offset;

public:
    MemoryPool(size_t Size)
    {
        Pool = new char[Size];
        Offset = 0;
    }

    ~MemoryPool() { delete[] Pool; }

    template<typename T, typename... Args>
    T* Allocate(Args&&... args)
    {
        void* Ptr = Pool + Offset;
        Offset += sizeof(T);
        return new(Ptr) T(std::forward<Args>(args)...);
    }
};

 

추가로, 객체의 생성과 소멸 시점을 직접 관리하고 싶은 경우에도 활용합니다. shared_ptr 등에는 내부 구현에 placement new를 활용하고 있습니다.

 

 

[주의 사항]

 

placement new는 실제로 new와 같이 메모리를 할당하는 것이 아니라, 미리 할당해둔 메모리 영역을 활용하는 것이기 때문에 delete 연산자를 호출하기보단 명시적으로 소멸자를 호출하는 것이 좋습니다. 또한 이미 생성된 영역에 대해 다시 생성을 실시하면 같은 영역에 객체가 덮어씌워질 수 있습니다.

빌드 과정은 크게 전처리기, 컴파일, 어셈블러, 링커 순으로 실행됩니다.

 

 

[전처리기]

 

유저가 작성한 코드를 저수준 언어로 변환하기 위한 준비를 실시합니다. 주석, 공백 등 불필요한 요소를 제거하고 매크로 구문을 치환하며, 헤더 파일의 코드 전체를 소스파일 내에 추가하게 됩니다.

 

 

[컴파일]

 

전처리 과정을 거친 코드를 저수준의 어셈블리어로 번역하는 동시에 문법상의 오류를 검출하기도 합니다.

 

 

[어셈블러]

 

어셈블러 과정에서는 어셈블리어를 0과 1로 이루어진 바이너리 코드로 변환합니다. 변환된 바이너리 코드는 여러 개의 오브젝트 파일(.obj)로 저장됩니다.

 

 

[링커]

 

위에서 여러 개로 저장된 오브젝트 파일을 해당 단계에서 하나의 프로그램에서 작동하도록 연결해줍니다. 이 과정에서 정적 라이브러리가 프로그램과 함께 묶이게 됩니다. 하나로 묶인 프로그램은 exe 파일로 저장되며 빌드가 완료됩니다.

[완벽한 전달의 필요성]

 

C++ 11버전 미만에서는 우측값 레퍼런스가 도입되기 전이었기 때문에, 아래와 같이 해결하기 어려운 문제가 있었습니다.

template <typename T>
void Wrapper(T _Value) 
{
  Func(_Value);
}

 

위의 함수는 인자로 받는 _Value를 그대로 Func() 함수에 전달해주는 래퍼 함수입니다. 이러한 형태는 종종 emplace_back()과 같은 함수에서 활용됩니다.

 

 

[잠시 vector의 push_back과 emplace_back 설명]

 

예를 들어 vector가 class temp를 원소로 갖는 경우, 객체를 vector 뒤에 추가하려면 아래와 같이 코드를 적어주면 됩니다.

vec.push_back(temp(1, 2, 3));

 

하지만 이 과정에서 불필요한 이동 또는 복사가 발생하게 됩니다(물론 현재의 push_back은 emplace_back과 같이 최적화됨). 이러한 문제에 대한 해결책으로 emplace_back을 활용하곤 합니다.

vec.emplace_back(1, 2, 3);

 

emplace_back은 전달받은 인자를 활용하여 컨테이너 내부 메모리 공간에 temp의 생성자를 직접 생성한 뒤 원소로 추가하는 동작을 수행합니다.

push_back 객체를 먼저 생성하고 복사/이동 후 요소를 뒤에 삽입(복사, 이동이 발생)
emplace_back 인자만 넘기면 내부에서 직접 생성하여 삽입(복사, 이동이 생략)
#include <iostream>
#include <vector>

class Temp
{
public:
	Temp(int a, int b, int c)
	{
		std::cout << "일반 생성자 호출: " << a << ", " << b << ", " << c << "\n";
	}

	Temp(const Temp& other)
	{
		std::cout << "복사 생성자 호출\n";
	}

	Temp(Temp&& other) noexcept
	{
		std::cout << "이동 생성자 호출\n";
	}
};

int main()
{
	std::vector<Temp> vec;

	std::cout << "\n--- emplace_back(1, 2, 3) ---\n";
	vec.emplace_back(1, 2, 3); // 직접 생성

	std::cout << "\n--- push_back(Temp(1, 2, 3)) ---\n";
	vec.push_back(Temp(1, 2, 3)); // 임시 객체 생성 후 이동 또는 복사
}

 

그렇다면 emplace_back로 전달한 인자들을 활용해 temp 생성자에 전달하며 호출하기 위해선 어떻게 해야할까요?

 

 

[완벽한 전달]

 

완벽한 전달이란, 함수 템플릿에서 인자로 받은 값을 그 값의 성격(좌측값인지, 우측값인지, const인지 등 여부)을 그대로 보존하여 다른 함수에 전달하는 방식입니다. 다시 가장 위의 래퍼 함수를 살펴보겠습니다.

template <typename T>
void Wrapper(T _Value) 
{
  Func(_Value);
}

 

wrapper(T _Value)처럼 값을 전달받으면 인자의 특성이 손실될 수 있습니다. 예를 들어, 우측값이 좌측값으로 변경되거나, const 속성이 무시될 수도 있다는 것입니다. 아래의 코드를 확인해보겠습니다.

#include <iostream>
#include <vector>

template <typename T>
void wrapper(T _Value) 
{
	Func(_Value);
}

class temp {};

void Func(temp& _other) { std::cout << "좌측값 레퍼런스 호출" << std::endl; }
void Func(const temp& _other) { std::cout << "좌측값 상수 레퍼런스 호출" << std::endl; }
void Func(temp&& _other) { std::cout << "우측값 레퍼런스 호출" << std::endl; }

int main() 
{
	temp A;
	const temp constA;

	std::cout << "원본 --------" << std::endl;
	Func(A);
	Func(constA);
	Func(temp());

	std::cout << "Wrapper -----" << std::endl;
	wrapper(A);
	wrapper(constA);
	wrapper(temp());
}

 

template wrapper 함수에 객체를 전달했지만, 원래의 의도와 다르게 _Value에는 복사된 값이 전달되어 좌측값 레퍼런스 호출이 발생하는 것을 볼 수 있습니다. 여기서 완벽한 전달을 활용할 수 있습니다.

 

1. 인자를 레퍼런스로 받는다(T&&; 만능 레퍼런스, forwarding reference )

2. 전달할 때 std::forward로 전달한다.

 

함수를 다음과 같이 수정한 뒤, 결과를 확인해보겠습니다.

template <typename T>
void wrapper(T&& _Value) 
{
    Func(std::forward<T>(_Value));
}

 

std::forward는 _Value가 좌측값을 받았다면 좌측값으로, 우측값을 받았다면 우측값으로, const를 받았다면 const로 정확하게 전달해줍니다. 원래 변수의 성격을 유지해주는 함수입니다.

 

 

[emplace_back과 std::forward]

 

다시 emplace_back을 확인해보겠습니다.

template<class... Args>
reference emplace_back(Args&&... args) {
    // 전달받은 args들을 완벽하게 전달하여 생성자 호출
    construct_at(end(), std::forward<Args>(args)...);
    ++size_;
}

 

emplace_back의 구현 로직을 확인해보면 내부에서 std::forward를 활용하는 것을 확인할 수 있습니다. 따라서 아래의 코드

vec.emplace_back(1, 2, 3);

 

이에 대한 수행은 전달된 인자들이 그대로 temp(int, int, int) 생성자에 전달되는 것 처럼 구현됩니다. std::forward는 vector의 emplace_back 뿐만 아니라, make_unique, make_shared 등 많은 STL 함수에서 활용됩니다.

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

[C++] placement new  (0) 2025.10.13
[C++] 빌드 과정  (0) 2025.09.03
[C++] 이동 생성자와 std::move()  (0) 2025.05.16
[C++] 좌측값과 우측값(lvalue and rvalue)  (0) 2025.05.15
[C++] 복사 생략(Copy Elision)  (0) 2025.05.15

[생성자를 통한 객체 생성]

 

생성자를 통해 객체를 생성하는 방식은 다양합니다. 아래의 코드에서는 일반 생성자, 복사 생성자, 이동 생성자의 활용 예시를 적어두었습니다.

#include <iostream>
#include <string>

class MyString 
{
public:
    std::string Name;
    std::string Tag;

    MyString(const char* _Str) : Name(_Str)
    {
        std::cout << "일반 생성자 호출 : " << _Str << "\n";
    }

    MyString(const MyString& _Other) : Name(_Other.Name)
    {
        std::cout << "복사 생성자 호출 : " << _Other.Name << "\n";;
    }

    MyString(MyString&& _Other) noexcept : Name(std::move(_Other.Name))
    {
        std::cout << "이동 생성자 호출 : " << _Other.Name << "\n";;
        this->Name;
    }

    MyString operator+(const MyString& _Other) const
    {
        std::cout << "operator+ 호출\n";
        return MyString((Name + _Other.Name).c_str());
    }
};

int main() 
{
    MyString str1 = "asdf";
    MyString str2 = "qwer";
    MyString temp = str1 + str2;
    MyString str3 = temp;
    MyString str4 = std::move(temp);
}

 

이제 다음의 코드를 확인해보겠습니다.

MyString temp = str1 + str2;
MyString str3 = temp;

 

첫 번째 줄은 str1 + str2를 통해 객체 하나를 생성한 뒤 temp에 전달하는 방식으로, 복사 생성자가 호출될 것이라 예상됩니다. 하지만 이전에 포스팅했던 대로 복사 생략이 발생합니다.

https://umtimos.tistory.com/144

 

[C++] 복사 생략(Copy Elision)

[일반 생성자와 복사 생성자] C++에서 클래스의 객체를 생성할 때, 임의로 생성자를 따로 선언하지 않을 경우 디폴트 생성자로 객체를 생성합니다. 하지만 생성자를 따로 정의하여 인자를 전달하

umtimos.tistory.com

 

컴파일러는 이미 생성된 str1 + str2가 리턴한 객체를 temp인셈 치고 사용하는 것입니다. 하지만 두 번째 줄의 경우네는 복사 생성자가 호출되는데, 아래 그림과 같은 작업이 이뤄집니다.

temp로부터 값을 전달할 임시 객체를 생성하고, str3는 복사 생성자를 호출하여 빈 공간을 할당한 뒤, 임시 객체의 값을 빈 공간에 복사하고 소거되면서 str3가 완성되는 구조입니다. 이 과정에서는 복사 생략이 발생하지 않습니다.

 

 

[이동 생성자]

 

C++ 11부터는 이런 문제를 해결하고자 이동 생성자를 도입했습니다. 이동 생성자는 기존의 복사 생성자보다 적은 비용을 활용하여 객체를 이전(이동)시기는 방식으로 동작합니다. 임시 객체나 더 이상 사용하지 않을 객체에 대해 자원을 복사하는 대신, 그 자원의 소유권을 넘겨주는 방식으로 동작하는 것입니다.

MyString str4 = std::move(temp);

 

위의 코드에서 사용된 std::move()는 말 그대로 temp를 rvalue로 취급하도록 하여, 이동 생성자의 인자로 전달되도록 해줍니다. 즉, temp는 더 이상 쓸 일이 없음을 명시적으로 알려주어 해당 시점에 MyString 클래스가 이동 생성자를 호출하게 됩니다.

 

+) std::move()는 C++ 11부터 utility 라이브러리를 통해 제공되고 있습니다. 

+) 여기서 한 가지 유의할 점은, 데이터 이동 과정은 이동 생성자나 이동 대입 연산자가 호출되는 시점에 진행되는 것이지, move를 사용한 시점에 수행되는 것이 아니라는 점입니다. 실제로 이동 생성자나 이동 대입 연산자를 따로 선언하지 않고 move() 함수만 활용할 경우 기존보다 더욱 느린 복사 생성자가 호출됩니다.

 

이동 생성자가 호출되면 temp가 가진 내부 자원(여기서는 Name)의 내용이 str4로 이전(이동)외고, temp는 비워진 상태가 됩니다. 이러한 과정에서 불필요한 메모리 복사나 할당이 발생하지 않기 때문에 복사 생성자보다 훨씬 효율적으로 동작할 수 있게 됩니다. 이를 확인하기 위한 간단한 코드로, 아래를 확인해봅시다.

MyString(MyString&& _Other) noexcept : Name(std::move(_Other.Name))
{
    std::cout << "이동 생성자 호출" << "\n";;
    std::cout << "temp의 Name : " << _Other.Name << "\n";;
    std::cout << "str4의 Name : " << this->Name << "\n";;
}

 

이동 연산자가 호출되는 과정에서 std::move()의 인자로 전달된 temp의 Name 내부 버퍼를 str4(this)의 Name이 가져갑니다. 이렇게 되면 원래 객체인 temp는 더 이상 유효한 상태가 아니기 때문에 다시 활용하지 않는 것이 좋습니다.

 

 

[이동 생성자가 유용한 경우]

 

다음의 경우에 이동 생성자가 유용하게 활용될 수 있습니다.

 

- 함수 리턴 값이 객체인 경우

- 컨테이너 등에 값을 emplace_back, push_back 할 때

- 자원을 소유하고 있는 객체를 더이상 사용하지 않을 때

 

추가로, 아래의 경우에도 사용하면 좋습니다.

template <typename T>
void my_swap(T &a, T &b) 
{
  T tmp(std::move(a));
  a = std::move(b);
  b = std::move(tmp);
}

 

C++ 기본 자료형(int, float 등)이 아니라, 위에서 사용한 MyString과 같은 사용자 정의 자료형의 경우에는 위와 같이 사용하면 좋습니다.

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

[C++] 빌드 과정  (0) 2025.09.03
[C++] 완벽한 전달(Perfact Forwarding)과 std::forward  (0) 2025.05.16
[C++] 좌측값과 우측값(lvalue and rvalue)  (0) 2025.05.15
[C++] 복사 생략(Copy Elision)  (0) 2025.05.15
[C++] union  (0) 2025.05.14

[우선정리]

lvalue - 이름과 주소가 있음
- 재참조하거나 수정이 가능
rvalue - 일시적인 값
- 이름이 없고 메모리 주소를 갖지 않음(기본적으로)
- 수식을 통해 생성되는 값
- 수정할 수 없음
- 재참조할 수 없음

 

모든 C++의 표현식은 두 가지 카테고리로 분류가 가능합니다.

 

- 어떤 타입을 가지는가

- 어떤 종류의 값을 가지는가

int a = 10;

 

 

[좌측값(lvalue)]

 

위의 코드에서, a는 메모리상에 존재하는 변수입니다. 메모리상에 존재한다는 것은 주소값을 취한다는 것으로, 이렇게 주소값을 취하는 값을 좌측값(lvalue)이라고 합니다. 이 좌측값이라는 개념은 C에서 정착된 개념이지만, C++에서는 굳이 좌측에 존재한다고 좌측값이 되는 것은 아닙니다.

int a = 10; // 왼쪽(Left)에 존재하는 lvalue
int b = a; // 오른쪽(Right)에 존재하는 lvalue

 

 

[우측값(rvalue)]

 

반면 int a = 10;에서 10의 경우에는 주소값을 취할 수 없는 값입니다. 메모리상에 존재할 수도 있는 a와 다르게 표현식에 잠깐 존재할 뿐, 연산이 끝난 뒤에는 사라지는 값입니다. 이렇게 주소를 취할 수 없는 값을 우측값(rvalue)이라고 합니다.

 

 

[좌측값 레퍼런스]

 

C++에서는 레퍼런스 변수를 지원하기 떄문에, 좌측값에 대해 레퍼런스 변수로 주소값을 받을 수 있습니다.

int a = 10;
int& Refa = a;

 

일반적으로 사용하는 레퍼런스 변수(&가 하나)의 경우 좌측값 레퍼런스라고 합니다.

 

 

[우측값 레퍼런스]

 

하지만 좌측값 레퍼런스와 다르게, 우측값 레퍼런스는 & 연산자 하나로 받을 수 없습니다.

int& RefDigit = 3;

 

이 값을 받기 위해서는 &&를 활용하거나, const T&를 활용해야합니다.

int&& RefDigit = 3;
const int& RefDigit2 = 3;
std::cout << RefDigit << std::endl;
std::cout << RefDigit2 << std::endl;

 

그렇다면 &&와 const T&는 무엇을 의미하는 것일까요?

 

> 임시값

 

두 종류의 변수에 대해 알아보기 전에, 임시값이 무엇인지 먼저 알아보도록 하겠습니다. 보통 코드에서 rvalue는 다음의 특징을 갖습니다.

 

- 이름이 없음(3, 5+2)

- 직접 참조할 수 없음

- 표현식이 끝나면 바로 사라짐

- 메모리에 존재는 하지만, 그 메모리가 레지스터나 스택에 임시로 저장되는 것

 

정리하자면 rvalue는 이름이 없고 금방 사라지는 값이라고 할 수 있습니다.

 

 

1. const T&

 

C++에서는 const T&로 임시값을 안전하게 참조할 수 있습니다. int a = 3;에서, 3이라는 값은 변수처럼 주소를 갖지 않기 때문에 직접 참조할 수 없습니다. 하지만 const T&를 활용하면 예외적으로 우측값을 참조할 수 있게 됩니다.

 

이것이 가능한 이유는 const라는 제약이 존재하기 때문입니다. 컴파일러는 "이 값을 바꾸지 않겠다"고 보장하는 const 참조에 한하여 임시값을 임시 객체로 생성한 뒤 수명을 연장시켜 줍니다

void Print(const std::string& _Str) 
{
    std::cout << _Str << std::endl;
}

printf("Hello");  // 문자열 리터럴 → std::string 임시 객체 → const 참조로 수명 연장

 

해당 참조 방식은 임시 객체를 읽기 전용으로 참조할 수 있게 해주는 안전한 방법이며, 여러 기능에서도 널리 쓰이는 방법입니다.

 

 

2. T&&

 

C++11 버전 이상부터 도입된 기능으로, const T&와 마찬가지로 우측값 참조를 지원해주는 방법입니다. 하지만 읽기 전용 참조 방식이 아닌, 임시값에 바인딩 후 그 값을 수정할 수 있는 권한도 갖게 해줍니다.

int&& Ref = 10;  // OK: rvalue를 rvalue Reference에 바인딩
Ref = 20;        // OK: 수정도 가능

 

해당 특성은 임시 객체를 수정하거나, 자원을 이동(move)시키기 위한 목적으로 설계된 참조 타입입니다. 임시값이라는 것은 금방 소멸될 것이기 때문에, 값을 수정해도 괜찮다는 전제 하에 사용되는 방식입니다.

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

[C++] 완벽한 전달(Perfact Forwarding)과 std::forward  (0) 2025.05.16
[C++] 이동 생성자와 std::move()  (0) 2025.05.16
[C++] 복사 생략(Copy Elision)  (0) 2025.05.15
[C++] union  (0) 2025.05.14
[C++] 클래스 template  (0) 2025.05.13

[일반 생성자와 복사 생성자]

 

C++에서 클래스의 객체를 생성할 때, 임의로 생성자를 따로 선언하지 않을 경우 디폴트 생성자로 객체를 생성합니다. 하지만 생성자를 따로 정의하여 인자를 전달하면서 생성하거나, 다른 객체의 값을 복사하면서 생성이 가능합니다.

#include <iostream>

class Temp
{
    int Value;

public:
    Temp(int _Value) : Value(_Value)
    { 
        std::cout << "일반 생성자 호출" << std::endl; 
    }

    Temp(const Temp& _Other) : Value(_Other.Value)
    {
        std::cout << "복사 생성자 호출" << std::endl;
    }
};

int main()
{
    Temp a(1);
    Temp b(a);
}

 

인자를 전달하면서 생성하는 경우에는 일반적인 생성자에 인자를 전달하는 방식으로 생성되는 것이지만, 다른 객체를 전달받으면서 생성하는 경우에는 복사 생성자를 통해 객체가 만들어집니다.

 

 

[복사 생략(Copy Elision)]

 

그렇다면 아예 객체를 생성함과 동시에, 그것을 인자로 받아서 생성하는 경우에는 어떻게 될까요?

#include <iostream>

class Temp
{
    int Value;

public:
    Temp(int _Value) : Value(_Value)
    { 
        std::cout << "일반 생성자 호출" << std::endl; 
    }

    Temp(const Temp& _Other) : Value(_Other.Value)
    {
        std::cout << "복사 생성자 호출" << std::endl;
    }
};

int main()
{
    Temp a(1); 
    Temp b(a);
    Temp c(Temp(2));
}

 

우리의 예상은 복사 생성자의 호출이지만, 일반 생성자가 호출되는 것을 확인할 수 있습니다. C++ 컴파일러는 Temp c(Temp(2))와 같은 코드를 확인할 경우, 굳이 임시 객체를 만들고, 이를 복사 생성자에 활용할 필요가 없다고 판단하게 됩니다. Temp(2)를 통해 2를 만들고, 다시 Temp c(2)를 할 필요 없이 바로 Temp c(2)를 통해 객체를 생성하면 된다고 판단하기 때문입니다.

 

이러한 최적화는 복사 생략(Copy Elision, or Return Value Optimization(RVO))이라고 하며, 발생할 수도 있지 꼭 발생하는 것은 아닙니다. 경우에 따라서는 복사 생략이 발생할 수도 있을 것 같은 지점에서 복사 생략이 발생하지 않을 수도 있다는 것입니다. C++ 17 이상 버전부터는 함수 내부에 객체를 만들어 return하는 형식에 대해서는 반드시 복사 생략이 발생하긴 합니다.

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

[C++] 이동 생성자와 std::move()  (0) 2025.05.16
[C++] 좌측값과 우측값(lvalue and rvalue)  (0) 2025.05.15
[C++] union  (0) 2025.05.14
[C++] 클래스 template  (0) 2025.05.13
[C++] 함수 template  (0) 2025.05.13

[struct와 union의 메모리 구조 차이]

 

클래스 내부에 이름이 없는 struct를 선언하는 것은, 보통 이 키워드에 해당하는 메모리 구성으로 사용하라는 뜻이 된다.

class A
{
    struct 
    {
        int a;
        int b;
    };
};

 

위의 코드에서 int a와 int b는 struct 키워드에 해당하는 메모리 구성으로 이루어지게 된다. 따라서 다음과 같은 메모리 구조를 갖는다([] = 1byte).

 

a      b

[][][][][][][][]

 

여기서 struct를 union으로 변경해보자.

class A
{
    union
    {
        int a;
        int b;
    };
};

 

이렇게 되면 메모리 구조가 다음과 같이 변경된다.

 

a

b

[][][][]

 

 

[union 메모리 구조]

 

union 키워드 내부의 변수들은 메모리 영역을 공유하게 된다. union 내부에 동일한 형태의 자료형이 존재한다면, 가장 마지막에 호출되어 변경된 메모리 상태를 공유한다.

union Un
{
    int a;
    int b;
};

int main()
{
    UTest1 Un = Un();
    Un.a = 10;
    Un.b = 20;

    int A = Un.a;  // A = 20
    int B = Un.b;  // B = 20
}

 

union 내부에 자료형의 크기가 다를 경우에는 가장 메모리가 큰 변수를 기준으로 메모리 영역을 할당받은 뒤, 메모리 상태를 공유한다.

union
{
    int a;
    int b;
    __int64 c;
};

 

위의 union 내부 자료형들은 다음의 메모리 구조를 갖는다.

 

a

b

c

[][][][][][][][]

 

 

[예시]

 

Windows 정의 자료형 중, LARGE_INTEGER가 union을 활용하는 자료형이다. 다음과 같이 선언되어 있다.

typedef union _LARGE_INTEGER {
    struct 
    {
        DWORD LowPart;
        LONG HighPart;
    } DUMMYSTRUCTNAME;
    struct 
    {
        DWORD LowPart;
        LONG HighPart;
    } u;
    LONGLONG QuadPart;
} LARGE_INTEGER;

 

이걸 64bit C++ 환경에 맞춰 변경하면 다음과 같이 볼 수 있다.

typedef union _LARGE_INTEGER {
    struct 
    {
        int LowPart;
        int HighPart;
    };
    struct 
    {
        int LowPart;
        int HighPart;
    } u;
    __int64 QuadPart;
};

 

QuadPart는 8byte 자료형이기 때문에, _LARGE_INTEGER의 메모리는 다음과 같은 구조를 갖는다.

 

LowPart

                HighPart

QuadPart

[  ][  ][  ][  ][  ][  ][  ][  ]

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

[C++] 좌측값과 우측값(lvalue and rvalue)  (0) 2025.05.15
[C++] 복사 생략(Copy Elision)  (0) 2025.05.15
[C++] 클래스 template  (0) 2025.05.13
[C++] 함수 template  (0) 2025.05.13
[C++] 람다  (0) 2025.05.13

[클래스 template]

 

함수처럼 클래스도 template를 사용할 수 있다.

template<typename DataType>
class NewArr 
{
public:
    DataType Value;
};

int main()
{
    NewArr<int> Test0;
    NewArr<int*> Test1;
    NewArr<bool> Test2;
    NewArr<wchar_t> Test3;
}

 

클래스 template는 인자추론이 불가능하기 때문에, 객체 선언과 동시에 원하는 자료형을 적어줘야 한다. 인자추론이 불가능한 이유는 다음과 같다.

 

1. 클래스의 크기는 main이 컴파일 되는 시점에서 정해져 있어야 한다.

2. 여기서 클래스 템플릿에 자료형을 적지 않으면 문제가 발생한다.

3. 위의 코드의 클래스 내부에 별명으로 자료형이 선언되어 있다.

4. 이러면 해당 클래스의 크기를 컴파일러가 파악할 수 없기 때문에 컴파일이 불가능해진다.

 

 

[클래스 template 특수화]

 

함수 template와 마찬가지로 클래스 template도 특수화가 가능하다. 클래스 template을 특수화하는 이유는 특정 자료형에 대해서는 구분되는 template 클래스를 만들기 위함이다.

template<typename DataType>
class NewArr
{
public:
    DataType Value;
};

template<>
class NewArr<bool>
{
public:
    bool Value;
};

int main()
{
    NewArr<int> Test0;
    NewArr<int*> Test1;
    NewArr<bool> Test2;
    NewArr<wchar_t> Test3;
}

 

 

[부분 클래스 template 특수화]

 

template 생성 시 전달할 자료형으로 여러 개의 typename을 정의한 template 구조의 경우, 일부 인자만 따로 지정할 수 있는 부분 클래스 template 특수화를 지원해주기도 한다.

template<typename T1, typename T2>
class Pair 
{
};

template<typename T>
class Pair<T, int> 
{
    // 두 번째 인자가 int인 경우 특수화
};

 

 

[클래스 template 사용 주의점-1]

 

함수 template도 마찬가지이지만, 클래스 template도 컴파일 시 컴파일러가 전달받은 자료형으로 새로운 클래스들을 만든다. 위의 코드를 컴파일할 경우, 아래와 같이 된다.

template<typename DataType>
class NewArr 
{
public:
    DataType Value;
};
////////////////////컴파일러에 의해 컴파일 시점에 새로 만들어짐/////////////////////
class NewArrint
{
public:
    int Value;
};
class NewArrint*
{
public:
    int* Value;
};
class NewArrbool
{
public:
    bool Value;
};
class NewArrwchar_t
{
public:
    wchar_t Value;
};
//////////////////////////////////////////////////////////////////
int main()
{
    NewArr<int> Test0;
    NewArr<int*> Test1;
    NewArr<bool> Test2;
    NewArr<wchar_t> Test3;
}

 

코드 작성 시간은 줄어들고, 컴파일 시간은 늘어나는 것이다.

 

 

[클래스 template 사용 주의점-2]

 

클래스 template에 대한 함수 구현은 모두 헤더에서 진행해야 한다.

template<typename DataType>
class TestClass
{
public:
	// constrcuter destructer
	TestClass();
	~TestClass();

	// delete Function
	TestClass(const TestClass& _Other) = delete;
	TestClass(TestClass&& _Other) noexcept = delete;
	TestClass& operator=(const TestClass& _Other) = delete;
	TestClass& operator=(TestClass&& _Other) noexcept = delete;

	void Func() 
    {
        <템플릿 클래스의 함수는 헤더에서 구현해야만 한다.>
	}

protected:

private:
	DataType Value;
};


// [클래스 템플릿의 cpp 파일 상황], 가능은 하지만 이런 방식은...
void TestClass<float>::Func()
{

}

void TestClass<int>::Func()
{

}

void TestClass<double>::Func()
{

}

 

이유는 cpp 파일을 컴파일 할 때, 어떤 자료형으로 클래스를 구체화해야하는지 몰라서 Func() 함수 바디 부분이 메모리에 정의되어 있지 않으면 컴파일이 되지 않기 때문이다.

 

- 헤더 파일은 컴파일 하지 않으면, cpp 파일 내에 include한 헤더 파일 내용이 복사될 뿐임

- 이후 cpp 파일들만 컴파일되고 링킹 실시

- 이제 메모리 할당을 실시해야하는데, 메모리를 결정하려면 구체적인 자료형을 알아야함

- 클래스 템플릿을 cpp에 구현했다면, 템플릿 타입만 존재하는 상황이기 때문에 어느 정도로 메모리를 할당해야할지 컴파일러가 파악할 수 없어 Func() 함수 바디 부분이 메모리에 정의되지 못하고 그냥 넘어감

- 따라서 링킹 시 Func() 함수 바디 부분을 알 수 없어 링킹 에러 발생

 

물론 함수를 구현부에 따로 선언할 때 함수명 우측에 자료형을 명시하면 구현부를 만들 수 있긴 하지만, 이러면 template를 사용하는 이유가 없다.

 

 

[클래스 template 사용 주의점-3]

 

vector<int>와 vector<unsigned int>를 선언하고, 대입을 실시해보면 대입이 되지 않는 것을 확인할 수 있다.

#include <vector>

using namespace std;

int main()
{
	vector<int> Arr1 = { 1, 2, 3 };
	vector<unsigned int> Arr2;

	Arr2 = Arr1; // 불가능
}

 

이것은 template로 전달하는 자료형에 따라 완전히 다른 타입의 클래스가 만들어지는 것이기 때문에 두 클래스 간의 복사 생성자, 대입 연산자가 작동하지 않는 것이다. 이것은 타입 안전성을 위한 장치로, template가 서로 다른 타입을 구분하지 않을 때 의도하지 않은 타입 변환이 일어나 오류가 발생하는 것을 방지하기 위함이다.

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

[C++] 복사 생략(Copy Elision)  (0) 2025.05.15
[C++] union  (0) 2025.05.14
[C++] 함수 template  (0) 2025.05.13
[C++] 람다  (0) 2025.05.13
[C++] 캐스팅(Casting)  (0) 2025.05.13

[오버로딩]

 

일반적으로 함수는 이름이 같아도, 전달하는 매개 변수에 차이가 있다면 오버로딩이 이뤄집니다.

#include <iostream>

void Function(int Test)
{
    std::cout << Test;
}

void Function(bool Test)
{
    std::cout << Test;
}

void Function(int* Test)
{
    std::cout << Test;
}

int main()
{
    Function(10);       // 출력 : 10
    Function(true);     // 출력 : true
    int* Ptr = nullptr;
    Function();         // 출력 : 0000000000000000
}

 

 

[함수 template]

 

하지만 전달하는 자료형마다 일일이 함수를 작성해주는 것은 여간 번거로운 작업이 아닙니다. 이를 위해 template가 존재합니다.

#include <iostream>

template<typename DataType>
void Function(DataType Test)
{
    std::cout << Test;
}

int main()
{
    int* Ptr = nullptr;
    Function/*<int*>*/(Ptr);
    Function/*<int>*/(10);
    Function/*<bool>*/(true);
}

 

함수 template를 활용할 경우, 컴파일러가 인자 추론을 실시하여 해당 자료형에 맞는 함수를 컴파일 타임에 만들어줍니다. 인자 추론은 자료형이 확실한 경우(int, float, bool, pointer 등) 가능합니다.

 

 

[함수 template 특수화]

 

모두 동일한 동작을 하도록하지 않고, 특정한 인자값을 받는 경우 이를 다르게 활용하고 싶다면 template 특수화를 활용하면 됩니다. 아래와 같이 bool의 경우 다른 패턴을 활용하고 싶다면 template<> 키워드를 할당한 뒤 함수를 작성해주면 됩니다.

#include <iostream>

template<typename DataType>
void Function(DataType Test)
{
    std::cout << Test;
}
template<>
void Function(bool Test)
{
    std::cout << "bool 자료형 입니다.";
}

int main()
{
    int* Ptr = nullptr;
    Function(Ptr);      // 출력 : 0000000000000000
    Function(10);       // 출력 : 10
    Function(true);     // 출력 : bool 자료형 입니다.
}

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

[C++] union  (0) 2025.05.14
[C++] 클래스 template  (0) 2025.05.13
[C++] 람다  (0) 2025.05.13
[C++] 캐스팅(Casting)  (0) 2025.05.13
[C++] std::enable_shared_from_this와 shared_from_this()  (0) 2025.05.13

[람다]

 

C++에서 함수를 선언할 때는 보통 선언과 구현을 실시한다. 하지만 람다를 사용할 경우 선언과 동시에 바로 함수처럼 활용할 수 있다.

[캡처](매개변수){함수바디}(호출인자) // 형태

[](int _a, int _b){return a + b};          // 함수를 만들기만 한 것
[](int _a, int _b){return a + b}(10, 20);  // 이건 함수를 만들고 바로 쓴 것

 

캡처를 선언하고, 매개 변수를 설정한 뒤 구현부를 등록해주고 필요 시 매개변수를 넣어주면 된다. 매개 변수와 호출인자는 사용하지 않으면 보통 생략할 수 있다.

 

 

[사용처]

 

여러 곳에서 무궁무진하게 활용 가능하겠지만, 작성자의 경우에는 Inline으로 처리될 수 있을 정도의 짧은 함수이면서 굳이 재활용하지 않는 함수일 경우 사용하곤 한다. 일반적인 sort의 경우 오름차순 정렬이 실시되지만, 특정 로직을 추가 실행하거나 할 경우에는 함수 포인터로 원하는 로직이 작성된 함수를 전달해주면 된다. 이때 람다를 사용할 수 있다.

#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

int main()
{
	vector<int> Arr = { 8, 4, 10, 2, 7, 9, 1};
	sort(Arr.begin(), Arr.end(), [](int _Left, int _Right) { return _Left > _Right; });
	
	for (int Value : Arr) cout << Value << endl;

	return 0;
}

 

 

[람다 캡쳐]

 

람다 사용의 주의점은 해당 구현부가 현재의 스택에서 분리된다는 점이다. 예를들어, 아래와 같이 람다를 선언하면 람다의 함수 바디는 main 함수의 스택 영역과 분리된 영역으로 존재하게 된다.

int main()
{
    int Value = 0;
    [](int _value)
    {
       int Value = 0;
    };
}

 

하지만 이를 위해 람다 캡처를 지원해주는데, 람다 캡처에 특정 연산자를 활용하면 스택에 존재하는 값을 활용할 수 있게 된다. 캡처의 종류는 네 가지가 있다.

 

- =

- &

- 단일캡처

- this

 

 

1. =캡처

해당 캡처는 모든 외부 변수를 복사해서 람다 함수 내부에서 사용한다는 뜻이 된다.

int main()
{
	int a = 10;
	int* IntPtr = &a;

	std::cout << "IntPtr : " << IntPtr << std::endl;

	std::function<void()> Ptr = [=]()
		{
			int* RamPtr = nullptr;
			RamPtr = IntPtr;

			std::cout << "IntPtr : " << IntPtr << std::endl;
			std::cout << "RamPtr : " << RamPtr << std::endl;
		};

	Ptr();
}

 

 

하지만 해당 캡처 방식은 단순하게 외부에 존재하는 변수에 대한 대입이 이뤄지지 않고 에러가 발생하는데, 아래의 테스트를 해보면 이유를 확인해볼 수 있다.

// 람다 구현부에서 외부 변수 int a;에 대해 a = 20; 수행이 안되는 이유
int main()
{
	int a = 20;
	__int64 aAddRess = (__int64)&a;

	std::cout << "a : " << a << std::endl;
	std::cout << "aAddRess : " << aAddRess << std::endl;

	std::function<void()> Ptr = [=]()
		{
			__int64 aAddRess = (__int64)&a;

			std::cout << "a : " << a << std::endl;
			std::cout << "aAddRess : " << aAddRess << std::endl;
		};

	Ptr();
}

 

메모리의 주소가 차이난다는 것을 확인할 수 있는데, 이것은 [=캡처]가 단순히 호출한 영역의 메모리를 복사해서 들고있기 때문에 발생하는 것이다.

 

 

2. &캡처

위 현상을 해결하기 위해 [&캡처]가 있다. 이전과 동일하게 테스트해보면 외부 변수와 람다 구현부 변수의 주소값이 동일한 것을 확인할 수 있다.

int main()
{
	int a = 20;
	__int64 aAddRess = (__int64)&a;

	std::cout << "a : " << a << std::endl;
	std::cout << "aAddRess : " << aAddRess << std::endl;

	std::function<void()> Ptr = [&]()
		{
			int Test = 100;
			a = Test;
			__int64 aAddRess = (__int64)&a;

			std::cout << "a : " << a << std::endl;
			std::cout << "aAddRess : " << aAddRess << std::endl;
		};

	Ptr();
}

 

해당 캡처 방식은 호출한 영역의 메모리를 참조해서 활용하는 방식이다. 물론 이 방식은 스택 영역에서 활용할 경우 외부 변수가 파괴될 염려가 있어 사용에 주의해야 한다.

 

 

3. 단일캡처

영역의 메모리 전체를 복사하는 방식이 아니라, 단일 변수에 대해서도 복사가 가능하다.

int main()
{
	int a = 20;
	int b = 30;
	__int64 aAddRess = (__int64)&a;
	__int64 bAddRess = (__int64)&b;

	std::cout << "a : " << a << std::endl;
	std::cout << "b : " << b << std::endl;
	std::cout << "aAddRess : " << aAddRess << std::endl;
	std::cout << "bAddRess : " << bAddRess << std::endl;

	std::function<void()> Ptr = [&a, b]()
		{
			int Test = 100;
			a = Test;
			Test = b;
			__int64 aAddRess = (__int64)&a;
			__int64 bAddRess = (__int64)&b;

			std::cout << "a : " << a << std::endl;
			std::cout << "b : " << b << std::endl;
			std::cout << "aAddRess : " << aAddRess << std::endl;
			std::cout << "bAddRess : " << bAddRess << std::endl;
		};

	Ptr();
}

 

 

4. this캡처

물론 클래스 멤버변수까지 캡처하는 방식도 가능하다. 이때는 호출하는 대상의 this를 캡처에 전달하면 된다. [&캡처]와 유사한 방식으로 동작한다.

class Test
{
public:
	void Call()
	{
		__int64 aAddRess = (__int64)&a;
		__int64 bAddRess = (__int64)&b;

		std::cout << "a : " << a << std::endl;
		std::cout << "b : " << b << std::endl;
		std::cout << "aAddRess : " << aAddRess << std::endl;
		std::cout << "bAddRess : " << bAddRess << std::endl;

		std::function<void()> ClassFunction = [this]()
			{
				int Test = 100;
				a = Test;
				Test = b;
				__int64 aAddRess = (__int64)&a;
				__int64 bAddRess = (__int64)&b;

				std::cout << "a : " << a << std::endl;
				std::cout << "b : " << b << std::endl;
				std::cout << "aAddRess : " << aAddRess << std::endl;
				std::cout << "bAddRess : " << bAddRess << std::endl;
			};

		ClassFunction();
	}

	int a = 0;
	int b = 0;
	__int64 aAddRess = 0;
	__int64 bAddRess = 0;
};

int main()
{
	Test Obj = Test();
	std::function<void()> Function = std::bind(&Test::Call, Obj);
	Function();
}

 

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

[C++] 클래스 template  (0) 2025.05.13
[C++] 함수 template  (0) 2025.05.13
[C++] 캐스팅(Casting)  (0) 2025.05.13
[C++] std::enable_shared_from_this와 shared_from_this()  (0) 2025.05.13
[C++] 스마트 포인터  (0) 2025.03.21

+ Recent posts