• 4.7.1 열거형 구현 방식 안 좋은 예
  • 4.7.2 Enumerated Types
  • 4.7.3 열거형 타입 사용 시 주의사항

 


4.7.1 열거형 구현 방식 안 좋은 예

 

어떤 특성에 대해 여러 종류로 분류하는 경우가 굉장히 많다. 실무에서도 입자의 성분을 분류하거나 계산 축을 설정하기 위해 정수형으로 나열해서 쓰곤 했다. 그냥 하드코딩으로 된 것도 있으며, 매크로를 사용하는 경우도 많았다. 아래의 예시를 보자.

int computeDamage(int weapon_id)
{
	if (weapon_id == 0) // sword
	{
		return 1;
	}

	if (weapon_id == 1) // hammer
	{
		return 2;
	}

	// ... 이런식으로 구현하면 힘들어진다.
}

무기별로 데미지를 다르게 해서 구현을 하려 했다. 0을 sword로 생각하고 구현했으며, 주석을 달아서 나름 친절하게 구현을 했다고 볼 수 있겠다. 하지만 무기 특성을 데미지 계산뿐만 아니라 다른 곳에서도 쓴다면 계속 위처럼 조건문을 작성해야 하는데 모든 곳에 주석을 달기엔 비효율적이다.


4.7.2 Enumerated Types

 

위와 같은 고민을 해결해줄 수 있는 것이 열거형 타입인 enum이 되겠다. 바로 적용된 버전의 코드를 보자.

enum Weapon
{
	WEAPON_SWORD,
	WEAPON_HAMMER,	// 마지막 쉼표 있어도 된다.
};

int computeDamage(int weapon_id)
{
	if (weapon_id == WEAPON_SWORD)
	{
		return 1;
	}

	if (weapon_id == WEAPON_HAMMER)
	{
		return 2;
	}

	// ...
}

아까보다 주석이 줄었으며, 가독성도 좋아졌다. enum의 경우 직접 수치를 입력하지 않을 경우 0부터 1씩 커지는 숫자가 enum 안에 들어간다. 여기서 WEAPON_SWORD가 0이고 다음이 1이 들어가 있다. enum은 헤더에 넣어두고 include하는 방식으로 사용하며, enum 마지막엔 ;으로 끝맺음을 해줘야 한다.

 

열거형 타입을 반드시 0부터 시작되게 사용하지 않아도 된다. 프로그래머가 값을 입력하면 그 값을 입력한 다음 항목에 대해서는 1씩 증가하게 구현이 된다.

#include <iostream>
#include <typeinfo>

enum Color
{
	COLOR_BLACK = -3,
	COLOR_RED,
	COLOR_BLUE = 5,
	COLOR_GREEN,
	COLOR_SKYBLUE,
};

int main()
{
	using namespace std;

	// 초기화 방식은 다른 타입들과 같다.
	Color paint = COLOR_BLACK;
	Color house(COLOR_BLUE);
	Color apple{ COLOR_RED };

	Color my_color = COLOR_BLACK;

	cout << my_color << " " << COLOR_BLACK << endl;

	return 0;

위의 예시는 순서대로 -3, -2, 5, 6, 7이 할당된다. 이처럼 사용자가 할당할 수는 있지만 권장하지 않는 방식이다.


4.7.3 열거형 타입 사용 시 주의사항

 

하나의 enum에 같은 수를 배정할 수 있지만 의도와는 다르게 작동할 수 있다. 아래의 예시를 수행해보면 조건문이 참으로 되고 두 개가 다른 항목이지만 같다고 판단하는 꼴이 된다.

#include <iostream>

enum Color
{
	COLOR_BLACK,
	COLOR_RED,
	COLOR_BLUE = 5,
	COLOR_GREEN = 5,
	COLOR_SKYBLUE,
};

int main()
{
	using namespace std;

	if (COLOR_BLUE == COLOR_GREEN)
		cout << "equal" << endl;

	return 0;
}

 

다른 enum일지라도 같은 이름을 가지면 안된다. 아래 예시처럼 int 자료형에도 저장이 가능하기 때문에 같은 이름인 BLUE를 저장할 경우 C2365 에러가 생긴다. 만약 같은 이름을 다른 열거형에서 사용하고싶다면 C++11부터 생긴 enum class를 활용하자.

#include <iostream>

enum Color
{
	COLOR_BLACK,
	COLOR_RED,
	COLOR_BLUE,
	COLOR_GREEN,
	COLOR_SKYBLUE,
	BLUE
};
enum Feeling
{
	HAPPY,
	JOY,
	TIRED,
	BLUE,
};

int main()
{
	using namespace std;

	int color_id = BLUE;

	cout << color_id << endl;

	return 0;
}

BLUE가 중복되었다.

 

위에서처럼 int 자료형에 enum을 넣는 것은 가능하지만 반대는 안된다. 아래 주석 처리된 부분은 바로 에러가 뜬다. static_cast를 사용하면 넣을 수 있겠지만 애초에 숫자로 넣는 모호한 방식을 사용하지 않기 위해 사용된 것이 열거형이므로 이런 식으로 사용하는 경우는 없을 것으로 생각된다.

enum Color
{
	COLOR_BLACK,
	COLOR_RED,
	COLOR_BLUE,
	COLOR_GREEN,
	COLOR_SKYBLUE,
};

int main()
{
	using namespace std;

	int color_id = COLOR_BLUE;
	cout << color_id << endl;

	// 그냥 넣는건 안된다. 애초에 안그러기 위해 만들어진게 enum
	//Color test = color_id;
	//Color test = 3;

	Color my_color = static_cast<Color>(3);

	return 0;
}

 

cin으로 바로 enum 자료형에 값을 넣는 것은 안된다. 입력 받으려면 우회해야 한다. 입력을 정수로 받고 해당 정수와 enum의 숫자를 일치하는지 조건을 체크하여 구현하는 방법이 최선일 것이다.

#include <iostream>
#include <typeinfo>

enum Color // user-defined data types
{
	COLOR_BLACK,
	COLOR_RED,
	COLOR_BLUE,
	COLOR_GREEN,
	COLOR_SKYBLUE,
};

int main()
{
	using namespace std;

	Color my_color = static_cast<Color>(3);

	//cin >> my_color;

	// 우회하는 방법
	int in_number;
	cin >> in_number;

	if (in_number == 0) my_color = COLOR_BLACK;
	// ...

	return 0;
}
  • 4.6.1 C 스타일 문자열
  • 4.6.2 string library header
  • 4.6.3 string 자료형 사용 시 유의사항

4.6.1 C 스타일 문자열

 

이미 C 언어를 학습한 사람이라면 C 스타일의 문자열을 다룬 기억이 있을 것이다. C 스타일 문자열은 char 자료형을 배열로 지니는 형태이며, 문자열이 담길 경우 마지막에 널 문자('\0')가 들어간다.

#include <iostream>

int main()
{
	using namespace std;

	// 커서를 가져다 올리면 13 길이 배열이라고 표시됨.
	cout << "Hello, World" << endl;
	const char my_strs[] = "Hello, World";

	return 0;
}

문자열의 길이를 확인해보면 12가 아닌 13이라는 것을 알 수 있다.

보이는 길이보다 1 길다.


4.6.2 string library header

 

C++에서는 string 라이브러리를 활용하여 문자열을 다룬다. string은 프로그래머의 편의를 위해 만들어진 사용자 정의 자료형으로 볼 수 있다.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	const string my_hello = "Hello, World";
	//const string my_hello("Hello, World");
	//const string my_hello{ "Hello, World" };

	cout << my_hello << endl;

	return 0;
}

입력받는 방법은 위에 나열된 3가지 모두 가능하다.

 

숫자를 문자열로 취급하여 받을 수도 있는데, string은 정수를 암시적으로 형 변환을 해주진 않는다. 위에서 언급하였듯이 너무 친숙하여 기본 자료형처럼 보이지만 사용자 정의 자료형이기 때문에 암시적 형 변환이 불가능하다.

#include <string>

int main()
{
	using namespace std;

	// ""를 삭제하고 넣으면 에러가 뜬다.
	const string my_ID = "123";

	return 0;
}

 

또한 string 자료형은 + 연산자에 대한 오버로딩이 구현되어 있어서 문자열을 이어주는 것도 쉽게 수행할 수 있다. 이처럼 문자열을 붙여주는 것을 append라고 하며, 프로그래밍을 공부하다 보면 자주 만나게 될 용어이다. string으로 표현하는 문자열과 C 스타일 문자열의 가장 큰 차이점은 바로 문자열의 길이이다. string 문자열은 널 문자를 가지고 있지 않기 때문에 같은 문자열이라도 길이가 C 스타일 문자열보다 1 짧게 된다.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	string a("Hello, ");
	string b("World");

	string hw = a + b; // append

	// 12 길이(C 스타일과 길이가 1 차이남.)
	cout << hw.length() << endl;

	hw += " I'm good";
	cout << hw << endl;


	return 0;
}

4.6.3 string 자료형 사용 시 유의사항

 

string을 입력 받을 때는 생각했던 대로 작동하지 않는 경우가 종종 있다. 다음의 예시들을 보자. 아래 예시에서 name 부분에 입력할 문자열을 띄어쓰기가 포함된 문자열을 입력해보자.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	cout << "Your name ? : ";
	string name;
	cin >> name; // 빈칸이 있으면 이미 다받은 것으로 판단.

	cout << "Your age ? : ";
	string age;
	cin >> age;

	cout << name << " " << age << endl;

	return 0;
}

출력도 해주지 않고 끝나버린다.

문자열을 입력할 때 cin은 띄어쓰기를 만나는 순간 입력이 끝난 줄 알고 저장해준 뒤 다음으로 넘어간다. 이를 해결하는 방법은 여러 가지가 있는데, 그중 하나는 바로 getline을 활용하는 것이다.

 

getline은 enter를 치기 전까지 전부 입력을 받아준다. 이를 이용하면 문자열에 띄어쓰기가 있는 것까지 온전히 받아낼 수 있다.

#include <iostream>
#include <string>

int main()
{
	using namespace std;

	cout << "Your name ? : ";
	string name;
	getline(cin, name);

	cout << "Your age ? : ";
	string age;
	getline(cin, age);

	cout << name << " " << age << endl;

	return 0;
}

 

정수를 먼저 받으면 생기는 오작동도 있다. 이번에는 나이를 먼저 받게 구현을 하면 나이를 입력하자마자 바로 프로그램이 종료되는 것을 볼 수가 있다. int 자료형에 저장하는 경우엔 getline을 사용할 수 없게 되면서 cin으로 사용할 수밖에 없게 되고 숫자를 입력 후 enter를 치는 행위가 getline을 통과하는 조건으로 수행되면서 이러한 문제가 생긴다.

#include <iostream>
#include <string>
#include <limits>

int main()
{
	using namespace std;

	cout << "Your age ? : ";
	int age;
	cin >> age;
	
	// 해결법
	//cin.ignore(32767, '\n'); // 2 byte 가장 큰 signed 정수 32767
	//cin.ignore(numeric_limits<streamsize>::max(), '\n');

	cout << "Your name ? : ";
	string name;
	getline(cin, name); // enter칠때까지 입력 받을 수 있음.

	cout << name << " " << age << endl;

	return 0;
}

이를 해결하기 위해 ignore라는 함수를 활용할 수 있다. ignore 함수의 첫 번째 인자가 입력 받는 길이가 되며, 두 번째 인자가 입력받으면 끝낼 문자가 되겠다. 입력받는 수가 얼마나 길지 모르기 때문에 2byte 가장 큰 signed 정수인 32767을 넣어주었다. 32767이라는 남들이 보기에 이해 못 할 수 있는 숫자를 넣는 것은 다른 사람들을 힘들게 하기 때문에 limit 라이브러리에 있는 streamsize max를 이용해도 되겠다.

  • 4.5.1 typeinfo library header
  • 4.5.2 암시적 형 변환(Implicit Type Conversion(coersion))
  • 4.5.3 명시적 형 변환(Explicit Type Conversion(casting))

4.5.1 typeinfo library header

 

형 변환에 대해서 다루기 전에 유용한 라이브러리 하나를 소개하고자 한다.

#include <iostream>
#include <typeinfo>

int main()
{
	using namespace std;

	cout << typeid(0.0).name() << endl;
	cout << typeid(0).name() << endl;

	bool variable = true;
	cout << typeid(variable).name() << endl;

	return 0;
}

typeinfo 라이브러리에 있는 함수 중 하나인 typeid().name()는 변수나 리터럴이 어떤 타입인지 알려주는 함수이다. 형 변환하면서 제대로 형 변환이 되었는지 체크하기 위해 쓰면 유용한 함수이다.


4.5.2 암시적 형 변환(Implicit Type Conversion(coersion))

 

암시적 형 변환은 컴파일러가 알아서 강제로 형 변환을 해주며, 프로그래머가 아무것도 하지 않아도 자동으로 수행하는 것이다. 아래의 예제에서는 프로그램이 실행되지만 다음과 같은 에러 메시지가 뜬다.

#include <iostream>
#include <typeinfo>

int main()
{
	using namespace std;

	int a = 123.0;
    
	cout << typeid(a).name() << endl;

	return 0;
}
warning C4244: 'initializing': conversion from 'double' to 'int', possible loss of data

C4244 에러는 데이터 손실이 있다고 언급을 하지만 프로그램이 실행되도록 둔다. 개발자가 실수로 값을 잘못 넣는 경우에 발생하니 발견하면 해당 변수에 대해 파악을 하고 수정해두는 것이 좋겠다.

 

빌드는 되지만 신경 쓰이는 에러 메시지가 뜬다.

 

암시적 형변환 중 numeric promotion이라 불리는 형 변환이 있다. numeric promotion은 작은 메모리를 가지는 데이터가 큰 메모리에 담기는 경우를 의미한다. 아래의 예시는 float 자료형이 double로 numeric promotion되었다. 우리말로 숫자 승격이라고 쓸 수 있겠다.

#include <iostream>
#include <typeinfo>

int main()
{
	using namespace std;

	float	f = 1.0f;
	double	d = f;

	cout << typeid(f).name() << endl;
	cout << typeid(d).name() << endl;

	return 0;
}

 

또 다른 암시적 형 변환으로 numeric conversion이 존재한다. 숫자 변환이란 의미를 가지는 이 암시적 형 변환은 서로 다른 자료형 간에 변환이나 큰 메모리를 가지는 데이터가 작은 메모리에 담기는 경우를 말한다. 큰 메모리에 담긴 데이터일지라도 작은 메모리가 감당 가능한 범위의 값이라면 컴파일러가 충분히 처리해준다.

#include <iostream>
#include <typeinfo>
#include <iomanip>

int main()
{
	using namespace std;

	float	d = 3;
	short	s = 2;
	
	cout << typeid(d).name() << endl;
	cout << typeid(s).name() << endl;

	int		i = 30000; // short 범위를 넘어가면 문제지만 안넘어가면 괜찮다.
	char		c = i;

	cout << static_cast<int>(c) << endl; // 48

	double	dd = 0.123456789;
	float		ff = dd;

	cout << setprecision(12) << dd << endl;
	cout << setprecision(12) << ff << endl;

	return 0;
}

4.5.3 명시적 형 변환(Explicit Type Conversion(casting))

 

명시적 형 변환은 프로그래머가 변환하겠다는 강력한 의사표현을 하는 것이다. 아래와 같이 C 스타일과 C++ 스타일, 그리고 최근 자주 쓰는 스타일이 있다.

int main()
{
	using namespace std;

	int i = int(4.0);			// c++ style
	i = (int)4.0;			// c style
	i = static_cast<int>(4.0);	// 최신
	
	return 0;
}

'Programming Language > C++' 카테고리의 다른 글

Section 4.7. 열거형 타입 enum  (0) 2021.11.28
Section 4.6. string 문자열 사용  (0) 2021.11.27
Section 4.4. using namespace  (0) 2021.11.13
Section 4.3. 외부 연결  (0) 2021.11.11
Section 4.2. 전역 변수와 정적 변수  (0) 2021.11.10

C++11에서 auto 키워드가 생겼다. 우리가 기존에 명시해주던 자료형에 대하여 저장하는 데이터 형태를 보고 자료형을 추론해내어 알아서 저장한다. 프로그래밍하면서 굉장히 유용하게 쓸 수 있으니 숙지해두면 좋을 개념이다.


일반적인 변수 초기화

 

일반적으로 하는 변수 초기화를 생각해보자. 변수 이름 앞에 자료형을 기입해주고 초기화해주는 r-value를 해당 자료형에 맞게 입력해준다. 이 경우 해당 변수는 양 옆으로 똑같이 자료형에 대한 정보를 주는 상황이 된다. 즉, 같은 정보가 중복되어서 입력된 것이다. 물론 double로 선언하고 3을 넣는 사람도 있지만 3.0을 넣는게 올바른 프로그래밍이겠다.

#include <iostream>

int main()
{
	using namespace std;

	int a = 123;

	return 0;
}

int 자료형과 r-value 123(int) 정보가 중복되어 들어오는 예시이다.


auto 키워드

 

앞에서 언급한 것처럼 정보가 중복되어 들어오는 것을 비효율적이라 생각하여 auto 키워드가 생겼다. 정보 입력은 하나만 있어도 충분하기 때문이다. auto 키워드를 사용하여 선언을 하는 경우 반드시 초기화도 함께 해주어야 한다. 저장되는 데이터를 컴파일러가 추론해서 어느정도의 메모리를 사용하여 어떤 기본 자료형으로 저장할지 선택해야 하는데, 애초에 저장할 데이터를 안 주면 추론 자체가 불가능하니 당연하겠다.

#include <iostream>

int main()
{
	using namespace std;

	int a = 123;

	auto b = 123123123123123123;
	auto d = 123.0;
	auto c = 1 + 2.0;

	return 0;
}

이렇게 auto로 초기화한 변수에 마우스 커서를 올리면 어떤 자료형으로 추론이 되었는지 알 수 있다.

long long으로 추론되었다.

이렇게 한 번 추론이 되면 추론된 자료형으로 취급하여 다루면 되겠다.


함수에서의 auto 키워드

 

auto 키워드는 함수를 다룰 때도 사용할 수 있다.

int add(int x, int y)
{
	return x + y;
}

int main()
{
	using namespace std;

	auto result = add(1, 2);

	return 0;
}

이처럼 함수에서 return 값을 auto로 받아낼 수 있겠다. 위의 코드에서 add 함수를 아래 두가지로 변경해서 넣어보자.

auto add(int x, int y)
{
	return x + y;
}

auto add(int x, int y)
{
	return x + (double)y;
}

정의부터 auto를 써서 사용한다. 애초에 return에 놓인 결과값도 int이기 때문에 추론이 가능한 형태이다. 두 번째 add 함수는 return하기 전에 double로 형 변환을 수행하였다. 이럴 경우 return 값이 double로 변하고 해당 함수의 return 값을 받아야 하는 변수도 double이 되어야 한다. 전부 auto로 연결되어서 문제없이 작동하는 것을 확인할 수 있다.

 

매개 변수 위치에는 auto 키워드를 쓸 수 없다. 매개 변수에 auto 키워드를 쓰는 목적을 생각해보면 함수 오버로딩을 안 하고 한 번에 정의하기 위함일 듯하다. 그러한 목적을 충족시켜주는 template 개념이 이미 존재하기 때문에 template을 활용하여 사용하면 되겠다.

 

auto 키워드가 생기면서 function trailing return type이라는 개념으로 프로그래밍이 가능해졌다. 결과가 되는 자료형을 아래처럼 함수 뒤에 적는 것이다. 굳이 적을 필요가 있냐는 생각이 들 수 있지만 추후 다루게 될 template과 사용할 때 효과를 발휘한다. 또한 함수의 결과가 무엇인지 좌에서 우로 순차적인 형태가 되기 때문에 C++이 익숙하지 않은 프로그래머에게 가독성을 높이는 코딩이라 할 수 있겠다.

auto add(int x, int y) -> double;

int main()
{
	using namespace std;

	auto result = add(1, 2);

	return 0;
}

auto add(int x, int y) -> double
{
	return x + (double)y;
}

Reference

+ Recent posts