[Reading] A Tour of C++ (2nd Edition) // Section 2.
A Tour of C++ [Bjarne StrouStrup, 2018]을 읽고 각 섹션별로 중요하다고 생각되는 내용이나, 새로 알게 된 내용들을 정리한 글입니다.
1. Introduce of User-Defined Types
C++에서 기본 제공 타입built-in type은 다음을 지칭한다.
- fundamental types(
int
,char
,double
, etc.) const
한정자- declarator operators(*, &, [], etc.)
이러한 built-in 타입들은 매우 다양하지만 저수준의 동작을 하도록 설계되어 있다. 즉, 하드웨어의 상태나 그 구조를 반영하는 데 초점이 맞춰져 있는 것이다.
그 결과 이러한 타입들과 연산자들은 프로그래머에게 고수준의 어플리케이션 작성을 위한 기능들을 지원하는 데 한계를 가지는데, C++은 이러한 단점을 해소하고자 프로그래머들로 하여금 원하는 고급 기능들을 직접 구현할 수 있는 복잡한 추상 메커니즘(Abstraction Mechanism)을 제공한다.
C++의 추상 메커니즘은 주로 프로그래머들이 적절한 표현과 동작을 이용해 그들에게 필요한 타입을 직접 설계하고 사용할 수 있도록 디자인되어 있다. 이러한 추상 메커니즘을 통해 설계한 타입들을 사용자 정의 타입(user-defined type) 이라 하며, 클래스(class), 또는 열거체(enumerations)가 이에 속한다.
따라서 많은 책들이 이러한 타입들을 설계하고, 사용하는 방법에 집중하는데, 이러한 사용자 정의 타입들은 다음과 같은 이유 덕분에 기본 제공 타입built-in type보다 선호되기도 한다.
- 사용이 간편하다.
- 에러 발생 가능성이 상대적으로 낮다.
- 효율적이며, 때로는 더 좋은 성능을 보인다.
Section 2에서 소개하는 사용자 정의 타입들은 다음과 같다.
- 구조체(Structure)
- 클래스(Class)
- 공용체(Union)
- 열거체(Enumeration)
2. Structure
사용자 정의 타입을 만드는 첫 번째 단계는 해당 타입이 필요로 하는 여러 원소(element)들을 한데 모으는 것이며, 이를 구조체로 구현할 수 있다.
구조체(또는 사용자 정의 타입) 안에 존재하는 여러 원소들을 멤버(member) 라 하는데, C++의 구조체에는 C와 달리 멤버로서 함수도 포함될 수 있다.
#include <cstdarg> // For Ellipsis.
struct Vector
{
int size;
double* elem;
void init(int s)
{
size = s;
elem = new double[size];
}
void setData(int count...)
{
va_list list;
va_start(list, count);
for (int i = 0; i < count; i++)
{
elem[i] = va_arg(list, double);
}
va_end(list);
}
void print() const
{
for (int i = 0; i < size; i++)
{
std::cout << elem[i] << ' ';
}
std::cout << std::endl;
}
};
3. Class
특정한 속성과 기능은 독립적으로 존재할 수 있지만, 일반적으로 현실 세계에서는 이들이 강하게 연결되어 있다(마치 우리 몸의 근육과 운동 수행 능력이 밀접한 연관을 가지는 것 처럼!).
앞서 구조체Struct를 통해 속성과 기능을 묶어서 새로운 타입을 만드는 법을 살펴보았지만, 구조체에 존재하는 멤버 변수들은 어디서나 접근할 수 있다는 단점이 있다.
클래스class는 구조체와 같이 속성(변수)과 기능(함수)를 동시에 포함할 수 있는 사용자 정의 타입이지만, 구조체와 달리 각 멤버들의 접근 수준을 제한할 수 있다.
선풍기를 사용할 때 몇 개의 버튼(인터페이스)만을 이용해 사용하도록 하는 것이 내부의 모든 회로와 부품을 이용할 수 있도록 하는 것 보다 사용자들이 편하게 사용할 수 있는 것처럼, 내부 속성들을 외부에서 직접적으로 접근할 수 없도록 한 후 인터페이스(함수)를 통해서만 간접적으로 이용할 수 있게 하면 훨씬 편리하고 안전한 타입을 정의할 수 있다.
앞서 살펴본 Vector 구조체를 클래스로 나타내면 다음과 같다.
#include <cstdarg> // For Ellipsis
class Vector
{
private: // members below are accessible only Vector scope.
int size;
double* elem;
public: // members below are accessible from external scope.
Vector(int s)
: elem{ new double[s] }, sz{s}
{}
~Vector()
{
delete elem;
}
void setData(int count...)
{
va_list list;
va_start(list, count);
for (int i = 0; i < count; i++)
{
elem[i] = va_arg(list, double);
}
va_end(list);
}
void print() const
{
for (int i = 0; i < size; i++)
{
std::cout << elem[i] << ' ';
}
std::cout << std::endl;
}
};
구조체의 init()
멤버함수가 Vector()
멤버함수로 대치된 것을 확인할 수 있는데, 이처럼 클래스와 동일한 이름을 가지면서 반환형이 명시되어 있지 않은 함수를 생성자constructor라 한다.
생성자는 해당 클래스 타입의 객체가 생성될 때 호출되며, 일반적으로 멤버들을 초기화하는 역할을 한다.
생성자와 비슷하지만 앞에 ~
를 붙인 함수도 존재하는데, 이는 소멸자destructor라 하며, 이는 생성된 객체가 범위를 벗어나거나 프로그램이 종료되는 등의 이유로 메모리에서 해제될 때 호출되는 함수이다.
소멸자는 일반적으로 동적으로 할당된 멤버들을 해제함으로써 메모리를 수거하는 역할을 한다.
C++은 생성자와 소멸자를 이용해 RAII(Resourcs Acquisition Is Initialization) 패턴을 구현한다.
4. Union
공용체Union는 구조체의 일종인데, 구조체는 안에 존재하는 멤버들이 각자의 메모리 영역을 사용하지만 공용체는 모두가 같은 영역을 사용한다. 코드로 나타내면 다음과 같다.
struct Structure
{
int mem1;
int mem2;
int mem3;
};
union Union
{
int mem1;
int mem2;
int mem3;
};
//....
{
std::cout << "size of Structure is " << sizeof(Structure) << "Bytes" <<'\n'
<< "size of Union is " << sizeof(Union) << "Bytes" << std::endl;
}
-----------------------------
Output :
size of Structure is 12Bytes
size of Union is 4Bytes
즉, 공용체는 자신의 멤버 중 가장 큰 크기의 타입을 가지는 멤버와 동일한 크기를 가지게 된다.
이러한 공용체의 특성은 단순히 데이터를 저장하는 데 사용하기보다는 상황에 따라 다른 타입의 데이터가 저장되는 타입을 정의할 때 유용하다.
쉽게 말해 여러 타입의 멤버를 가질 필요는 있지만 그 멤버들이 동시에 사용되지 않는다면 공용체를 사용하는 것이 좋다.
struct Entry
{
std::string name;
enum Type { ptr, num } t;
Node* ptr;
int num;
}
void foo(Entry& pe)
{
if (pe.t == num)
{
std::cout << pe.num << std::endl;
}
else
{
//..
}
}
위 코드에서 Entry 구조체 안의 ptr과 num은 함께 사용되지 않는다. Enum Type 멤버인 t의 상태에 따라 둘 중 하나만을 사용하는데, 이러한 경우 공용체를 통해 메모리를 절약할 수 있다.
union Value
{
Node* ptr;
int num;
}
struct Entry
{
string name;
enum Type { ptr, num } t;
Value v;
}
void foo(Entry& pe)
{
if (pe.t == num)
{
std::cout << pe.v.num << std::endl;
}
else
{
//..
}
}
그런데 이러한 형태의 구조는 t와 v가 독립적으로 수정될 수 있다는 문제점이 있다. 이는 프로그래머의 실수 때문에 t가 num에 해당하는 상수를 가지고 있음에도 ptr형태의 데이터를 사용하는 것과 같은 상황을 유발할 수 있다.
이러한 위험으로부터 실수를 방지하기 위해선 공용체와 해당 공용체의 값의 형태를 지정할 플래그flag를 클래스로 묶는wrapping 것을 권장한다. 클래스는 접근 제한을 지정할 수 있으므로 플래그와 값을 private로 숨긴 후 인터페이스를 통한 간접 접근을 통해 이들을 사용할 수 있도록 함으로써 앞서 언급한 것과 같은 상황을 방지할 수 있다.
C++17에는 std::variant를 이용해 간단하게 래퍼 클래스를 구현할 수 있다.
5. Enumeration
C++은 클래스와 더불어 간단하게 값을 열거Enumerate할 수 있는 열거체eunmeration 타입을 지원한다.
열거체는 다음과 같이 사용한다.
enum class Color { red, green, blue };
enum class TrafficLight { green, yellow, red };
Color colour = Color:: red; // For English. ;)
TrafficLight light = TrafficLight:: green;
열거체 안에 열거된 상수들은 자신이 속한 열거체를 범위로 가지므로, 상수와 대응되는 이름이 같더라도 이를 구분할 수 있으며 문제도 되지 않는다(Color::green과 TrafficLight::green이 서로 구분된다는 뜻이다).
열거체는 일반적으로 적은 개수의 정수 상수들을 표현하기 위해 사용한다. 정수 리터럴 대신 열거체를 이용하면 코드 가독성이 향상되고 실수를 유발할 가능성이 낮아지며, 코드의 유지보수 측면에도 좋은 영향을 끼친다.
사실 위의 enum class 는 엄밀히 말하면 열거체가 아닌 열거 클래스이다.
일반적인 열거체(C부터 존재하던)는 다음처럼 사용할 수 있다.
enum Color { red, green, blue };
Color colour = red;
열거체와 열거 클래스의 차이는 다음과 같이 정리할 수 있다.
- 열거 클래스는 멤버의 범위가 해당 열거 클래스로 범위가 고정된다.
반면 열거체는 멤버의 범위가 선언된 범위와 동일한 범위를 가진다. - 열거 클래스는 열거형 -> 정수로의 형변환이 불가능하다.
반면 열거체는 형변환이 가능하다. - 열거 클래스의 멤버들은 비교, 대입 연산 외 연산이 정의되어 있지 않다(단, 연산자 오버로딩을 통해 직접 정의가 가능하다).
반면 열거체의 멤버들은 일반적인 정수 리터럴과 동일한 연산 집합이 정의되어 있다.
/* enum class */
enum class Color { red, green, blue };
enum class TrafficLight { green, yellow, red };
Color x = red; // Error: which red?
Color y = TrafficLight::red; // Error: that red is not a Color's.
Color z = Color::red // OK
int i = Color::red; // Error: Color::red is not an int.
Color j = Color::red + Color::green; //Error: can't do arithmetic operation
Color c = 2; // Error: can't initialized. 2 is not a Color.
/* enum */
enum Color { red, green, blue };
enum TrafficLight { green, yellow, red }; // Error: green, red is already declared.
Color x = red; // OK
Color y = Color::red; // OK
int i = red; // OK
int j = red + green; // OK(?!)
Color c = 2; // Error: can't initialized. 2 is not a Color.
결론적으로, 열거 클래스의 멤버는 그 타입이 해당 열거 클래스이지만 열거체의 멤버는 정수 타입을 가진다. 따라서 더욱 강하게 타입을 지정하고자 한다면 열거 클래스를 사용하는 것이 좋다.
추가적으로 열거체에 대해 알아야 할 사실들은 다음이 있다.
- 열거체 안에 나열된 멤버들은 별도로 값을 지정하지 않는 한 0부터 순차적으로 정수 값이 매겨진다.
- 정수 -> 열거형 상수로의 형변환은 다음과 같이 할 수 있다.
int n = 1;
Color x = Color{n};
Color y = static_cast<Color>(n);
Color z {n}; // from C++17.
6. Advice
-
기본 제공 타입이 너무 저수준의 역할을 하는 것 같다면 내장 타입보다는 잘 정의된 사용자 정의 타입을 사용하라.
Prefer well-defined user-defined types over built-in types when the built-in types are too low-level.
-
연관된 데이터들은 구조체 또는 클래스 형식으로 조직화하라.
Organize related data into structures(structs or classes).
-
클래스를 이용해 인터페이스와 구현 간의 차이를 표현하라.
Represent the distinction between an interface and an implementation using a class.
(간단히 말하면 정보은닉(information hiding)을 구현하라는 말 같습니다.)
-
구조체는 단순히 모든 멤버가 public
인 클래스이다.
A struct is simply a class with its members public by default.
-
클래스의 초기화를 보장하기 위해 생성자를 정의하라.
Define constructors to guarantee and simplify initialization of classes.
-
구조가 드러나 있는 공용체의 사용을 피하라. 대신 공용체를 타입 필드와 함께 클래스로 묶어라.
Avoid "naked" unions. wrap them in a class together with a type field.
(구조가 드러나 있는 공용체란, 공용체 내 데이터의 타입을 구분할 타입 필드와 공용체가 독립적으로 접근 가능한 상태에 놓인 공용체를 말합니다.)
-
명명된 상수들의 집합을 표현할 때 열거체를 사용하라.
Use enumerations to represent sets of named constants.
-
의도하지 않은 실수를 최소화하기 위해 단순 열거체 대신 열거체 클래스를 사용하라.
Prefer class enums over "plain" enums to minimize surprises.
-
안전하고 간단한 사용을 위해 열거체의 연산을 정의하라.
Define Operations on enumerations for safe and simple use.
Author And Source
이 문제에 관하여([Reading] A Tour of C++ (2nd Edition) // Section 2.), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다
https://velog.io/@hamsik2rang/Reading-A-Tour-of-Cpp-2nd-Edition-Section-2
저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념
(Collection and Share based on the CC Protocol.)
기본 제공 타입이 너무 저수준의 역할을 하는 것 같다면 내장 타입보다는 잘 정의된 사용자 정의 타입을 사용하라.
Prefer well-defined user-defined types over built-in types when the built-in types are too low-level.
연관된 데이터들은 구조체 또는 클래스 형식으로 조직화하라.
Organize related data into structures(structs or classes).
클래스를 이용해 인터페이스와 구현 간의 차이를 표현하라.
Represent the distinction between an interface and an implementation using a class.
(간단히 말하면 정보은닉(information hiding)을 구현하라는 말 같습니다.)
구조체는 단순히 모든 멤버가 public
인 클래스이다.
A struct is simply a class with its members public by default.
클래스의 초기화를 보장하기 위해 생성자를 정의하라.
Define constructors to guarantee and simplify initialization of classes.
구조가 드러나 있는 공용체의 사용을 피하라. 대신 공용체를 타입 필드와 함께 클래스로 묶어라.
Avoid "naked" unions. wrap them in a class together with a type field.
(구조가 드러나 있는 공용체란, 공용체 내 데이터의 타입을 구분할 타입 필드와 공용체가 독립적으로 접근 가능한 상태에 놓인 공용체를 말합니다.)
명명된 상수들의 집합을 표현할 때 열거체를 사용하라.
Use enumerations to represent sets of named constants.
의도하지 않은 실수를 최소화하기 위해 단순 열거체 대신 열거체 클래스를 사용하라.
Prefer class enums over "plain" enums to minimize surprises.
안전하고 간단한 사용을 위해 열거체의 연산을 정의하라.
Define Operations on enumerations for safe and simple use.
Author And Source
이 문제에 관하여([Reading] A Tour of C++ (2nd Edition) // Section 2.), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@hamsik2rang/Reading-A-Tour-of-Cpp-2nd-Edition-Section-2저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)