
사용자 정의 타입
int나 double과 같은 C++에서 정의된 타입들은 기본 타입이라고 하며 즉시 사용이 가능하다
물론 함수, 포인터, 참조, 배열과 같은 기본타입의 확장인 복합 타입도 동일하다
(이미 C++에서 타입 이름과 기호가 의미하는 바를 알고 있기 때문에 즉시 사용이 가능한 것, 프로그래머가 직접 정의할 필요가 없다)
C++에는 대표적으로 크게 두가지 범주의 복합 타입(compoune type)을 이용하여 사용자 정의 타입을 만든다
(열거형(enum), 클래스 타입(class, struct, union))
사용자 정의 타입은 반드시 이름과 정의가 있어야 사용이 가능하다
struct Foo //사용자 정의 타입 Foo 정의
{
int i{ };
};
이러한 사용자 정의 타입을 정의할 때 ;으로 마무리를 해야한다, 생략하면 컴파일러가 다음줄에서 에러를 뱉을 수 있다
보통 사용자 정의 타입의 이름은 대문자 시작, 접미사 사용X로 명명한다 (필수는 아님)
ex)Foo(o), foo(x), foo_t(x)
사용자 정의 타입을 사용하기 위해서는 사용하는 코드 파일에서 해당 사용자 정의 타입의 정의를 include해야 한다, forward declaration으로는 부족하다 (정의가 있어야 컴파일러가 해당 사용자 정의 타입의 객체를 생성할 때 필요한 메모리 크기를 계산할 수 있기 때문)
결국 보통 .h에서 사용자 정의 타입을 정의하고 사용하는 코드 파일에서 #include하여 사용하는것을 권장한다
ODR에 따르면 각각의 함수, 전역 변수는 프로그램 전체에서 단 한번만 정의 될 수 있다, 여기서 사용자 정의 타입은 ODR의 일부 예외 규칙을 적용받는다
사용자 정의 타입은 여러 코드파일에서 반복해서 정의될 수 있다 (.h에 정의하고 여러 코드파일에서 #include해도 ODR규칙 위반이 아님), 이때 한 코드파일에서 한 번만 정의되어야 한다 (이는 헤더가드를 통해 방지가 가능)
추가로 정의된 타입이 모두 동일해야 한다
C++에 존재하는 기본, 복합 타입만 가지고는 다양한 프로그램을 제작하기 힘들다, 따라서 사용자 정의 타입은 필수이다
enum
enum은 특정한 값들의 집합을 정의할 수 있는 타입이다
ex) 가위, 바위, 보 or 색상 등...
enum은 프로그램 정의 타입으로 전방선언만으로 사용이 불가능하고 #include를 통해 정의를 include해야 사용이 가능하다
enum 이름
{
enumerator,
enumerator,
...
};
enum Color
{
red,
green,
blue,
};
Color color{ red };
각각의 enuemrator들은 ,로 구분하고 마지막의 ,는 선택사항이지만 추가하는 것을 권장한다
프로그램 정의 타입이기 때문에 마무리는;으로 해야한다
이렇게 정의한 열거형 타입 변수에는 정의된 enumerator만 저장할 수 있다 (다른 값을 넣으면 compile error)
각각의 enumerator는 소문자로 시작하는것을 권장한다
왜 enum을 사용할까?
Color color{ red }; //가독성 향상
Color color{ rock }; //compile error
각 value값을 이름으로 확인이 가능하기 때문에 디버깅에 도움이 된다
함수의 인자로 사용이 가능하기 때문에 좋다
void foo(Color c); //인자로 Color enumtype만 받을 수 있음
enumerator 정수 변환
각각의 enumerator는 symbolic constant(심볼릭 상수)로 정수타입의 값을 가진다
첫번째 enumerator를 0으로 1씩 증가한 값을 가지게 된다
enum Color
{
red, //0
green, //1
blue, //2
};
이때 명시적으로 정수값을 할당할 수도 있다
enum Color
{
red = -3, //-3
green, //-2
blue = 5; //5
}
중복값 할당도 가능하지만 지양하며 특정 값을 할당하는것 자체도 특별한 이유가 없다면 지양한다
만약 enum type 변수를 0으로 초기화 하면 enumerator에 0으로 초기화된 값이 없어도 0으로 설정된다, 그렇기 때문에 의미없는 값이 enum type 변수에 저장될 가능성이 있다
Color c{ }; //내부 enuemrator에 0이 없어도 0이 기본값으로 초기화 됨
상태를 저장하는 enum이라면 default나 none이라는 enuemrator가 0값으로 되어있도록 설계하는게 좋다
enum State
{
none,
attack,
die,
};
enum타입은 정수타입 값을 저장하지만 enum타입 자체는 복합 타입이기 때문에 정수형이 아니다 하지만 enum type 변수는 정수형으로 암시적 형변환이 된다 (하지만 역으로 정수가 enum타입으로 변환되지는 않는다)
State s{ attack };
std::cout << s; //1이라는 정수로 암시적 형변환
State s{ 1 }; //1을 넣는다고 attack으로 암시적 형변환이 되지 않는다
State s{ static_cast<State>(1) }; //이렇게 명시적 형변환을 해야 가능하다
enumerator는 내부적으로 정수값을 가진다, 이때 C++ 표준에서 특정한 정수 타입을 지정하지 않는다 (컴파일러가 결정한다) 보통은 int를 사용하지만 더 큰 값이 있다면 자동으로 더 큰 정수 타입을 사용하게 된다
이때 프로그래머가 직접 enumerator의 기본 타입을 지정할 수 있다
enum Color : std::int8_t //명시적으로 enum의 타입을 int8_t로 지정한 것
{
};
굉장히 작은 데이터 사용일 때 명시적으로 enumerator의 기본 타입을 지정하면 조금이나마 효율적으로 사용이 가능하다
그렇다면 이러한 enum타입을 문자열로 변환하려면 어떻게 해야할까?
단순히 enum타입 변수를 출력하면 정수형으로 암시적 형변환이 되어 숫자가 출력되는데 이는 확인이 힘들다, 따라서 문자열로 출력하여 확인하는것이 더 이해하기 쉽다
일단 기본적으로 C++에서 enum타입을 문자열로 변환하는 기능은 존재하지 않고 직접 만들어야 한다
enum Color
{
red,
green,
blue,
};
Color color{ red };
constexpr std::string_view getColor(Color color) //constexpr함수로 컴파일 타임 실행, std::string_view로 문자열을 가리키기만 해서 복사 비용 제거
{
switch(color)
{
case red:
return "red";
case green:
return "green";
...
}
}
이렇게 문자열 타입의 값을 switch-case로 return하는 함수를 만들면 된다
또한 std::cin은 열거형을 직접 입력 받을 수 없다 따라서 정수타입으로 받고 static_cast<>()로 명시적 형변환하거나 문자열 타입을 받고 이 입력받은 string을 enumerator문자열과 비교해서 enum으로 바꾸고 enum타입에 할당해야 한다
Color color{ red };
std::cin >> color; //error
이때 C++은 대소문자 구분을 하기 때문에 사용자가 Red를 입력하면 red로 확인이 불가능하다, 따라서 입력값을 소문자로 변환하여 비교하는 방식이 더 좋다
#include <algorithm>
#include <cctype>
#include <string>
#include <string_view>
std::string toLower(std::string_view sv)
{
std::string lower{};
std::transform(sv.begin(), sv.end(), std::back_inserter(lower),
[](char c) { return static_cast<char>(std::tolower(static_cast<unsigned char>(c))); });
return lower;
}
std::transform()을 통해 문자열을 변환한다
std::transform()은 특정 범위에 있는 element들에 특정 함수를 적용하고 그 결과를 다른 컨테이너의 출력 위치에 저장한다
sv.begin()부터 sv.end()까지를 대상으로 lower문자열 변수의 끝에 삽입한다 (std::back_inserter)
이때 변환할 함수는 람다함수로 넣어준다, std::tolower()로 문자를 소문자로 변환한다
따라서 RED가 들어왔으면 R,E,D 순차적으로 begin() ~ end()까지 unsigend char 소문자로 변환 후 lower라는 std::string 타입의 문자열 변수에 삽입한다는 의미이다
그렇다면 바로 std::cout << 으로 enum타입 변수를 문자열로 출력하려면 어떻게 해야할까?
연산자 오버로딩
+,-,*, <<와 같은 연산자들을 사용자 정의 타입에 맞게 변경하여 구현하는걸 연산자 오버로딩이라고 한다
C++에서는 이러한 연산자 오버로딩을 지원하기 때문에 사용자 정의 타입에서도 기존의 연산자를 사용할 수 있게 된다
기본적으로 연산자 오버로딩 함수는 연산자 이름을 함수명으로 사용한다, >>연산자를 오버로딩한다면 >>가 함수 이름이 되는것이다
연산자의 피연산자들을 매개변수에 정의해야 하고 반환형을 지정해야 한다, 해당 연산자를 통한 원하는 연산은 함수 body에서 구현하면 된다
여기서는 >>와 <<를 기준으로 정리해보겠다
enum Color
{
red,
green,
blue,
};
constexpr std::string_view getColorName(Color color)
{
switch (color)
{
case red:
return "red";
...
}
}
std::ostream& operator<<(std::ostream& out, Color color) //>>연산자 오버로딩
{
return out << getColorName(color);
}
int main()
{
Color c{ red };
std::cout << c;
}
여기서 std::ostream&은 std::cout, std::cerr 등과 같은 출력 stream 객체 타입이다
매개변수로 std::ostream&타입과 enum타입을 받아 >> 연산자 오버로딩을 진행한다 (문자열을 return하는 함수를 받아 출력)
std::cin >>도 연산자 오버로딩 함수를 만들어서 enum타입을 직접 입력 받을 수 있도록 구현해보자
constexpr std::optional<Color> getColorFromString(std::string_view sv)
{
if (sv == "red") return red;
return {};
}
std::istream& operator>>(std::istream& in, Color& color)
{
std::string s{};
in >> s;
std::optional<Color> convertColor{ getColorFromString(s) };
if(converColor)
{
color = *converColor;
return in;
}
in.setstate(std::ios_base::failbit);
return in;
}
int main()
{
Color c{};
std::cin >> c;
}
std::cin.setstate()는 입력 상태 플래그를 전송하는 함수이다
()에는 다양한 플래그가 들어가며 입력 실패의미가 전송되면 더 이상 입력을 받지 않게 된다
std::cin.setstate(std::ios_base::goodbit, failbit, badbit, eofbit); //순차적으로 정상, 입력 실패, 심각한 오류, End of File 상태 (입력종료)를 의미하는 플래그이다
보통 잘못된 데이터 입력 시 입력 실패 플래그를 날려서 더 입력받지 못하도록 한다
이러한 입력 스트림 오류 상태를 해제하려면 clear()해주면 된다
std::cin.clear();
추가로 if (std::cin)를 통해 입력 성공 여부를 확인할 수 있다
if (std::cin) ////만약 std::ios_base flag가 정상이 아니라면 false가 나온다
{
}
(위 내용들은 std::cout 출력 스트림에도 마찬가지로 동작함)
cin 입력 스트림 버퍼에서 데이터를 무시하려면 std::cin.ignore()를 사용한다
std::cin.ignore(무시할 최대 문자 수 (기본값 1), 무시할 문자의 구분자 (기본값 '\n'));
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n'); //해당 입력 stream의 최대 크기만큼 or 문자를 '\n'만날때 까지 무시한다(문자 구분자도 지움) (남은 입력 버퍼 안에서!)
std::string name1;
std::string name2;
std::cin >> name1;
std::cin.ignore(3);
std::cin >> name2;
std::cout << name1 << '\n'; //Kelvin
std::cout << name2 << '\n'; //rk
//Kelvin Park을 치게 되면 Kelvin, Park이 나오게 된다 (공백으로 인해 flush), 따라서 남아있는 []Park에서 3글자가 빠져서 rk가 나오게 된다
std::cin.ignore(100, 'r'); //이렇게하면 Kelvin하고 k가 나오게된다 (남은 입력버퍼 데이터 []Park에서 r까지 지운다