코딩 학습/C와 C++

C++ 기초 - enum class, auto, default, delete, final, override, 람다 함수

이개 2026. 4. 10. 14:07

고정폭 정수형

고정폭 정수형은 정수 자료형의 크기를 보장한다. 표준형에서 쓰인 애매한 표기들을 정확하게 표현하기 위하여 만들어졌다.

int8_t Score = Human01->GetID();

 

C++11에서는 아래와 같은 고정폭 정수형 자료형이 지원된다.

  • int8_t / uint8_t
  • int16_t / uint16_t
  • int32_t / uint32_t
  • int64_t / uint64_t
  • intptr_t / uintptr_t

 

enum

기존 enum은 enum 멤버를 int형 변수에 대입하거나 다른 enum 멤버와의 비교가 가능하여 엉망이 될 수 있었다.

// main.cpp

enum EPosition 
{
    TOP,
    JUNGLE,
    MID,
    SUPPORT,
    BOTTOM
};

enum EChamp 
{
    LEESIN,
    LULU,
    ZED,
    BLITZ,
    MISSFORTUNE
};

int main()
{
    EPosition MyPosition = SUPPORT;
    EChamp MyChamp = BLITZ;
    int Number = MyPosition; // 컴파일 에러 안납니다. enum to int 암시적 형변환 해줌.
    if (MyPosition == ZED)   // 컴파일 에러 안납니다. 서로 다른 enum 끼리 비교 연산 지원.
    {
        /* ... */
    }

    return 0;
}

 

 

enum class

c++ 11부터 지원되는 독자적인 자료형.

  • 정수형으로의 암시적 캐스팅이 없고, 필요하면 프로그래머가 명시적 캐스팅 해야함.
  • enum class에는 기저 정수형을 지정할 수 있음.
    • enum class 열거형명 : uint8_t과 같은 식으로 지정 가능하며 8비트 크기로 제한.
// Main.cpp

enum class EPosition 
{
    TOP,
    JUNGLE,
    MID,
    SUPPORT,
    BOTTOM
};

enum class EChamp 
{
    LEESIN,
    LULU,
    ZED,
    BLITZ,
    MISSFORTUNE
};

int main()
{
    EPosition MyPosition = EPosition::SUPPORT;
    EChamp MyChamp = EChamp::BLITZ; 
    int Number = MyPosition;       // 컴파일 에러. enum class는 int 자료형으로의 암시적 형변환이 안됨.
    if (MyPosition == EChamp::ZED) // 컴파일 에러. 서로 다른 enum class 간의 비교 연산 불가.
    {
        /* ... */
    }
    
    for (int i = EPosition::TOP; i <= EPosition::BOTTOM; ++i)
    {
    }
	    // 컴파일 에러. C언어에서 배운 (int) 형변환 연산자를 쓰거나
	    // static_cast<int>로 우회 가능.

		for (int i = (int)(EPosition::TOP); i <= static_cast<int>(EPosition::BOTTOM); ++i) 
		{
		}
			// 컴파일 성공.

    return 0;
}

 

static_cast<int>를 사용하여 enum을 int로 변경할 수도 있다.

for(int i=static_cast<int>(Eposition::TOP); i <= static_cast<int>(Eposition::BOTTOM); i++)
{
	COUT << static_cast<int>MyPosition << endl;

}

// 또는

for(int i=(int)(Eposition::TOP); i <= (int)(Eposition::BOTTOM); i++)
{
	COUT << (int)MyPosition << endl;

}

 

enum에 할당할 바이트 크기를 정하는 방법

#include <cstdint> // uint8_t를 위한 인클루드

enum class EPosition : uint8_t
{
    TOP,
    JUNGLE,
    MID,
    SUPPORT,
    BOTTOM = 0x100 
	    // 컴파일 경고가 나면서 8비트 부분만 떼서 0으로 저장되거나,
	    // 컴파일 에러가 날 수도 있습니다.
};

 

enum class의 바이트 크기 설정

// Pixel.h

#pragma once

#include <cstdint>

enum class EColor : uint8_t
{
	Red = 0,
	Green = 1,
	Blue = 2,
};

class Pixel
{
public:
	Pixel(const EColor InColor)
		: Color(InColor)
	{
	}

	~Pixel()
	{
	}
	
	inline EColor GetColor() const 
	{ 
		return Color; 
	}

	void PrintColor() const;

private:
	EColor Color;

};
// Pixel.cpp

#include "Pixel.h"

#include <iostream>

void Pixel::PrintColor() const
{
    switch (Color)
    {
    case EColor::Red:
        std::cout << "RED" << std::endl;
        break;
    case EColor::Green:
        std::cout << "GREEN" << std::endl;
        break;
    case EColor::Blue:
        std::cout << "BLUE" << std::endl;
        break;
    default:
        std::cout << "INVALID" << std::endl;
        break;
    }
}
// Main.cpp

#include "Pixel.h"

int main()
{
	Pixel Pixel01(EColor::Red);
	Pixel Pixel02(EColor::Blue);
	Pixel Pixel03(EColor::Green);

	Pixel01.PrintColor();
	Pixel02.PrintColor();
	Pixel03.PrintColor();

	return 0;
}

 

enum 대신 enum class 사용할 것!

 

auto 키워드

컴파일러가 자료형을 자동으로 추론하게 된다. 컴파일 타임에 자료형이 정해져야하므로, 조건을 만족해야 사용 가능하다.

  • auto 변수는 반드시 초기화해야한다.
  • 한번 자료형이 결정되면 더는 자료형을 바꾸지 못한다.
// 1. auto 변수는 반드시 초기화를 해야합니다.
auto X;                   // 컴파일 에러
auto X = "Hello, world!"; // 컴파일 성공.

// 2. 한 번 자료형이 결정되면 더는 자료형을 바꾸지 못합니다.
auto X = "Hello, world!"; // 컴파일 성공. X는 const char* 자료형.
X = true;                 // 컴파일 에러.

 

auto로 포인터와 참조를 받을 수도 있다.

  • 포인터를 받을 때: auto 또는 auto*
  • 참조를 받을 때: auto&
// Main.cpp

Cat* MyCat = new Cat(2, "Calido");
auto MyCatPtr01 = MyCat;
	// 안 좋은 코딩 습관. 포인터인지 아닌지 알 수 없습니다.

auto* MyCatPtr02 = MyCat;
	// 가독성을 위해 포인터를 받을 때는 auto*를 씁시다.
// Main.cpp

Cat MyCat(3, "Calido");
Cat& MyCatRef = MyCat;
auto AnotherCatRef01 = MyCatRef;
	// 이때 AnotherCatRef01은 참조 자료형이 아니고 값 자료형 Cat입니다.
	// 만약 기본 복사 생성자가 동작하고, 하필 또 포인터 자료형의 멤버 변수가 있다면 얕은 복사가 일어납니다.
	// 그럼 같은 메모리 주소를 공유하게 되고, 소멸자에서 메모리 이중 해제가 발생할 수도 있습니다.
	// 포인터 자료형의 멤버 변수가 없다고 해도, 값 자료형이므로 의도치 않은 값복사로 인한 성능저하도 우려됩니다.

auto& AnotherCatRef02 = MyCatRef; 
	// 이렇게 사용하는 것을 추천합니다.

 

 

const 변수는 auto 말고 const auto&로 받는 게 좋다.

const int A = 7;
auto& ARef01 = A;       
	// ARef01의 자료형은 const int&가 됩니다.
	// 그럼에도 auto&에는 const가 안 붙어있기 때문에 프로그래머가 볼 때 헷갈릴 수 있습니다.

const auto& ARef02 = A; 
	// 이렇게 항상 작성합시다.

 

컴파일러가 알아낼 수 있기 때문에 auto는 모든 변수를 받을 수 있지만, 사람의 가독성을 위해서 연산자를 달아준다.

 

auto를 사용하면 좋은 경우

1. 반복자

for (std::map<std::string, std::string>::const_iterator Iter = Map01.begin(); Iter != Map01.end(); ++Iter) 
{ 
	// 너무 길다.
} 

for (auto Iter = Map01.begin(); Iter != Map01.end(); ++Iter) 
{ 
	// 속이 뻥. 단, 본인이 iterator를 잘 이해함과 동시에 자료형 추론이 가능한 경우에만 사용해야 합니다.
}

 

2. 템플릿 매개변수가 두 개 이상인 경우

MyPair<int, std::string>* Pair01 = new MyPair<int, std::string>(110, "Kim"); 
	// 우항에 자료형(MyPair<int, std::string>)이 보이기 때문에 auto를 사용하는 것도 좋은 방법입니다.

auto* Pair02 = new MyPair<int, std::string>(110, "Lee");
	// 자료형이 보이면 auto 키워드를 써도 됩니다.

 

 

default 키워드

생성자를 하나도 작성하지 않으면 암시적 기본생성자가 구현된다.  default 키워드를 사용하면 컴파일러가 기본 생성자, 연산자 및 소멸자의 암시적 버전을 만든다. 그래서 기본 생성자나 소멸자를 직접 구현하지 않고 기본 생성자나 소멸자를 그대로 활용할 수 있다.

// Main.cpp

class Pet
{
public:
	Pet() = default;
		// 컴파일러에게 기본 생성자를 구현하라고 명시합니다.
		// 사람이 볼 때, 명시적으로 기본 생성자가 있구나 추측 가능합니다.
	
	// 또한 다른 생성자를 정의해서 기본 생성자가 생성되지 않는 상황도 피할 수 있습니다.
	// default 키워드를 사용하면 다른 생성자가 정의되었어도 기본 생성자를 만들어냅니다.

};

int main(void)
{
	Pet MyPet;

	return 0;
}

 

 

 

delete 키워드

특정 생성자/연산자를 사용하지 못하게 명시적으로 금지하는 키워드.

private에다 생성자를 추가하는 방법도 있지만, delete를 사용하여 명시적으로 표현해주는 것이 좋다.

class Dog
{
private:
	Dog() {} // 생성자를 호출하지 못하게 하는 방법.

};

 

private 접근 제어자로 특정 생성자 연산자를 숨기면 접근제어자 에러메세지가 난다. delete로 호출을 금지하면 좀 더 올바른 호출 금지 에러 메세지가 뜬다.

// Main.cpp

#include <string>

class Pet
{
public:
	Pet() = default;
	
	Pet(const Pet& InOther) = delete;

};

int main()
{
	Pet MyPet01;
	Pet MyPet02(MyPet01); 
		// 컴파일 에러.
	
	return 0;
}

 

final 키워드

더이상의 상속을 금지하는 키워드. 클래스의 final을 붙이면 상속이 불가능해진다.

가상함수 뒤에 붙이면 더이상 오버라이딩 금지를 의미한다. 가상함수를 자식 클래스에서 오버라이딩 못하도록 해주는 키워드이다.

 

// Main.cpp

#include <iostream>

class Dog
{
public:
	Dog(int InAge)
		: Age(InAge)
	{
	}

	virtual void SetAge(int InAge) final
	{
		Age = InAge;
	}

	virtual int GetAge(void) const final
	{
		return Age;
	}

	virtual void Wag() const = 0;

private:
	int Age;

};

class Corgi final : public Dog
{
public:
	Corgi(int InAge)
		: Dog(InAge)
	{
	}

	//virtual void SetAge(int InAge) override;

	virtual void Wag() const final
	{
		std::cout << "Corgi is now wagging." << std::endl;
	}

};

//class JindoCorgi : public Corgi
//{
//};

int main()
{
	Corgi Choco(10);
	Choco.Wag();

	return 0;
}

 

jave의 final과 다른 점

Java final
→ 값 변경 / 상속 / 오버라이드 금지

C++ final
→ 상속 / 오버라이드만 금지 (값은 상관 없음)

 

java

final int x = 10;
x = 20; // ❌ 불가능
final class A {}
class B extends A {} // ❌
class A {
    final void foo() {}
}

class B extends A {
    void foo() {} // ❌ 오버라이드 금지
}

 

C++

  • 값에는 final 붙일 수 없음.
  • 클래스는 이름 뒤에, 함수에선 선언() 뒤에 붙여야 함
  • override와 같이 사용할 경우 override 뒤에 붙.
class A final {};

class B : public A {}; // ❌

 

class A {
    virtual void foo() final;
};

class B : public A {
    void foo() override; // ❌
}
class B : public A {
    void foo() override final;
};

 

override 키워드

자식 클래스에 가상함수 오버라이딩을 하려고 할 때, override 키워드를 붙여주면 해당 함수의 부모 클래스를 체크해줌.

잘못된 오버라이딩을 막기 위해 사용.

 

override 조건

  1. 부모 함수가 virtual 
  2. 함수 이름 동일
  3. 파라미터 / const / 반환형 동일
// Main.cpp

class Animal
{
public:
	void Move()
	{
	}

	virtual void MakeSound()
	{
	}

	virtual void Eat()
	{
	}

};

class Dog : public Animal
{
public:
	void Move() override
	{
	}
		// 오버라이딩이 맞을까요?

	void MakeSound(int InCount) override
	{
	}
		// 오버라이딩이 맞을까요?

	void Eat() override
	{
	}
		// 오버라이딩이 맞을까요?

};

int main()
{
	

	return 0;
}

 

 

람다 함수(Lambda Function)

이름없는 함수 객체, 또는 익명 함수라고도 많이 부름.

캡쳐 블럭은 람다 함수가 정의 범위(scope) 안에 있는 변수를 람다 함수에 넘겨줄 때 사용

캡쳐의 종류

  • []
    • 캡쳐하지 않음
  • [=]
    • 모든 외부 변수를 캡쳐
    • 값에 의한 캡쳐이므로, 캡쳐해온 원본 변수 수정 불가능
  • [&]
    • 모든 외부 변수를 캡쳐
    • 참조에 의한 캡쳐이므로, 람다 함수 내에서 캡쳐해 온 변수 원본 수정 가능
  • [변수명]
    • 특정 변수를 값에 의한 캡쳐
  • [&변수명]
    • 특정 변수를 참조에 의한 캡쳐
// Main.cpp

#include <iostream>
#include <limits> // std::numeric_limits

int main(void)
{
	{
		auto NoCaptureLambda01 = []() -> void { std::cout << "NoCaptureLambda01" << std::endl; };
		// void (*NoCaptureLambda01)() = []() -> void { std::cout << "NoCaptureLambda01" << std::endl; };
		NoCaptureLambda01();

		[]() -> void { std::cout << "NoCaptureLambda02" << std::endl; }();
	}
	
	{
		float Score01 = 98.f;
		float Score02 = 99.f;

		// auto WrongGetMax = []() -> float { return Score01 < Score02 ? Score02 : Score01; };
		// std::cout << "Max: " << []() -> float { return Score01 < Score02 ? Score02 : Score01; }() << std::endl;
			// 컴파일 에러. 외부 변수 사용 불가. 캡쳐가 필요합니다.

		auto GetMax01 = [Score01, Score02]() -> float { return Score01 < Score02 ? Score02 : Score01; };
		std::cout << "Max: " << GetMax01() << std::endl;

		auto GetMax02 = [=]() -> float { return Score01 < Score02 ? Score02 : Score01; };
		std::cout << "Max: " << GetMax02() << std::endl;
	}

	{
		float Score = 99.f;

		// auto IncreaseScore = [Score]() -> void { Score += 1.f; };
			// 값에 의한 캡쳐이기 때문에 수정은 불가능.

		std::cout << "Before Score: " << Score << std::endl;
		auto IncreaseScore = [&Score]() -> void { Score += 1.f; };
		IncreaseScore();
		std::cout << "After Score: " << Score << std::endl;

		float Score01 = 98.f;
		float Score02 = 99.f;
		float Score03 = 100.f;

		auto IncreaseAllScores = [&]() -> void { Score01 += 1.f; Score02 += 1.f; Score03 += 1.f; };
		
		IncreaseAllScores();
	}

	{
		float MaxScore = std::numeric_limits<float>::lowest();
		float Score01 = 98.f;
		float Score02 = 99.f;
		float Score03 = 100.f;

		std::cout << "Before MaxScore: " << MaxScore << std::endl;
		auto GetMaxScore = [=, &MaxScore]() -> void
			{
				// MaxScore만 참조 캡쳐, 나머지는 값 캡쳐.
				MaxScore = std::max(MaxScore, Score01);
				MaxScore = std::max(MaxScore, Score02);
				MaxScore = std::max(MaxScore, Score03);
			};

		GetMaxScore();

		std::cout << "After MaxScore: " << MaxScore << std::endl;
	}

	return 0;
}

 

매개변수 목록은 생략 가능

auto PrintHelloWorld = [] { cout << "Hello, world!" << endl; }; 
	// () 괄호는 생략할 수도 있습니다. 그럼에도 불구하고 적어두는 것이 좋습니다.
// Main.cpp

#include <iostream>

int main(void)
{
	float Score01 = 100.0f;
	float Score02 = 90.0f;

	auto AddScore = [](float InLHS, float InRHS) -> float { return InLHS + InRHS; };

	std::cout << "Score01 + Score02: " << AddScore(Score01, Score02) << std::endl;

	return 0;
}

 

 

익명 함수와 sort() 함수

// Main.cpp

#include <iostream>
#include <vector>
#include <algorithm> // std::sort()

struct Comparer
{
	bool operator()(float InLHS, float InRHS)
	{
		return (InLHS < InRHS);
	}
};

bool Sort(float InLHS, float InRHS)
{
	return (InLHS > InRHS); 
		// 전역 함수로 Sort() 함수를 선언하고 사용하면 다른 동료도 Main.cpp를 읽다가 Sort() 함수를 쓸 수도 있습니다.
		// 나중에 Sort() 함수를 수정 할 일이 생긴다면, Sort() 함수를 사용 중인 모든 프로그래머들과 의논해야합니다.
		// 다른 사람과 굳이 공유하지 않을 코드라면 숨기는 것이 좋긴 합니다.
}

int main(void)
{
	std::vector<float> Scores01 = { 100.f, 10.f, 90.f, 20.f, 80.f, 30.f, 70.f, 40.f, 60.f, 50.f };

	Comparer MyComparer;
	std::sort(Scores01.begin(), Scores01.end(), MyComparer);

	auto LeftIter01 = Scores01.begin();
	while (LeftIter01 != Scores01.end())
	{
		std::cout << *LeftIter01 << ' ';
		++LeftIter01;
	}
	std::cout << std::endl;

	std::vector<float> Scores02 = { 100.f, 10.f, 90.f, 20.f, 80.f, 30.f, 70.f, 40.f, 60.f, 50.f };

	std::sort(Scores02.begin(), Scores02.end(), Sort);

	auto LeftIter02 = Scores02.begin();
	while (LeftIter02 != Scores02.end())
	{
		std::cout << *LeftIter02 << ' ';
		++LeftIter02;
	}
	std::cout << std::endl;

	return 0;
}

 

익명 함수 활용

// Main.cpp

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

int main(void)
{
	std::vector<float> Scores = { 100.f, 10.f, 90.f, 20.f, 80.f, 30.f, 70.f, 40.f, 60.f, 50.f };

	std::sort(Scores.begin(), Scores.end(), [](float InLHS, float InRHS) -> bool { return InLHS > InRHS; });

	auto LeftIter01 = Scores.begin();
	while (LeftIter01 != Scores.end())
	{
		std::cout << *LeftIter01 << ' ';
		++LeftIter01;
	}
	std::cout << std::endl;

	return 0;
}

 

// STL 알고리즘과 함께
vector<int> v = {3, 1, 4, 1, 5};

// sort에 비교 함수로 전달
sort(v.begin(), v.end(), [](int a, int b) {
    return a > b;  // 내림차순
});

// find_if에 조건 함수로 전달
auto it = find_if(v.begin(), v.end(), [](int x) {
    return x > 3;  // 3보다 큰 첫 번째 요소
});

장점

  • 간단하면서도 로직에 최적화된 함수 작성에 좋음
  • 전역 함수로 작성하면 다른 사람도 호출 가능하므로 유지보수에 좀더 신경써야함.

단점

  • 디버깅 불편
  • 함수 재사용성이 낮음

기본적으로는 이름 있는 함수를 사용하는 게 좋음

자잘한 함수, 예를 들어 한 줄 짜리 함수나 정렬 함수처럼 STL 컨테이너에 매개변수로 전달할 함수같은 경우는 람다로 쓰는 것이 좋음.

 

mutable

일반적으로 값을 복사 캡처([x])하면, 람다는 내부적으로 그 값을 상수(const)로 보관하는데, mutable을 붙이면 이를 해제해준다.

int x = 10;

// mutable이 없다면 x = 99; 에서 컴파일 에러 발생!
auto f = [x]() mutable {
    x = 99; // 이제 수정 가능
    std::cout << "내부 x: " << x << std::endl;
};

f(); // 내부 x: 99 출력
std::cout << "외부 x: " << x << std::endl; // 외부 x: 10 출력 (복사본만 바꿨으므로)

 

  • 외부에는 영향 없음: mutable로 바꾼 값은 람다 내부에 저장된 복사본일 뿐, 람다 밖의 원본 x는 여전히 10.
  • 상태의 지속성: 람다를 다시 호출하면 x는 10으로 초기화되는 게 아니라, 이전에 수정된 99를 기억. 즉, 람다가 '상태'를 가지게 됨.
  • 외부의 값 자체를 바꾸고 싶다면 [&x] 처럼 참조 값을 캡처해야한다. 이때 mutable은 필요없다.

 

람다를 함수 인자로 넘기는 방법

// 람다를 변수처럼 다른 함수에 넘길 수 있다!
void apply(/* 여기에 람다를 받는 타입 */) {
    f(3, 4);  // 넘겨받은 람다 실행
}

apply([](int a, int b) { return a + b; });  // 람다를 인자로 전달
// C++20 이전에는 함수 매개변수에 auto 불가!
void func(auto f) { }  // ❌ C++17까지 컴파일 에러

// C++20부터는 가능
void func(auto f) { }  // ✅ C++20
// 가장 일반적인 방법
template <typename F>
void func(F f) {
    f(3, 4);
}

func([](int a, int b) { return a + b; });  // ✅
func([](int a, int b) { return a * b; });  // ✅
// F가 람다 타입으로 자동 추론됨
// b번: std::function으로 받기
void func(function<int(int, int)> f) { f(3, 4); }
// ✅ 가능하지만 약간 느림 (타입 변환 오버헤드)

// d번: template으로 받기
template <typename F>
void func(F f) { f(3, 4); }
// ✅ 가능하고 더 빠름 (컴파일 타임에 타입 확정)

 

 

function 타입 변수

#include <functional>

// 일반 함수
int add(int a, int b) { return a + b; }

// std::function에 저장
function<int(int, int)> f1 = add;         // 일반 함수
function<int(int, int)> f2 = [](int a, int b) { return a + b; }; // 람다
function<반환타입(매개변수타입들)>

function<int(int, int)>
//       ^^^  ^^^  ^^^
//       반환  인자1 인자2

function<void()>         // 반환 없음, 인자 없음
function<bool(int)>      // bool 반환, int 인자 1개
function<int(int, int)>  // int 반환, int 인자 2개
// auto는 타입을 나중에 바꾸기 어려움
auto f = [](int a, int b) { return a + b; };

// function은 나중에 다른 함수로 교체 가능!
function<int(int, int)> f;
f = [](int a, int b) { return a + b; };
f = [](int a, int b) { return a * b; };  // 교체 가능!

// 함수를 인자로 넘길 때도 유용
void apply(function<int(int, int)> func) {
    cout << func(3, 4) << endl;
}
apply([](int a, int b) { return a + b; });  // 7
apply([](int a, int b) { return a * b; });  // 12