A tour of C++ 2장

A tour of C++ 2장

구조체, 클래스, 공용체, 열거형 클래스 등 사용자 정의 타입에 대해 알아보자.

해당 포스팅 및 책의 소스코드 정리는 여기를 클릭해 다운받아 확인해보기 바란다.

C++ 타입

C++ 에서는 기본타입 (int, double etc), const 제한자, 선언 연산으로 생성할 수 있는 내장 타입 과 추상화 메커니즘을 바탕으로 만들어진 사용자 정의 타입 2 가지로 나뉜다. 내장 타입 같은 경우 로우레벨에 가깝게 설계 되어졌으며, 전통적으로 하드웨어의 능력을 직접적이고 효율적으로 사용한다. 반면에 프로그래머로 하여금 진보된 어플리케이션 쉽게 작성하게 해주는 하이레벨 기능을 제공하지 않는다. 사용자 정의 타입 같은 경우 프로그래머가 스스로 적절한 표현방식과 연산을 갖춘 타입을 설계하고 구현하며, 간단하고 편리하게 사용할 수 있게 해준다. 사용자 정의 타입의 대표적인 예로 “클래스” 와 “열거형”을 들 수 있다.

구조체

새로운 타입을 만드는 첫 단계는 필요한 구성요소를 데이터 구조로 조직화 하는 것이다. 데이터 구조를 조직화하는 키워드는 struct가 사용되며 이를 우리는 구조체 타입이라고 부른다.

struct Vector {
  int sz; // 요소의 개수
  double* elem; // 요소를 가리키는 포인터
};

위의 소스코드는 요소의 개수요소를 가리키는 포인터 로 이루어진 Vector 구조체를 나타낸 것이다. Struct는 사용자가 만든 타입으로 아래와 같이, 코드내에서 사용가능하다.

int main ()
{
  Vector v; // (.) 연산 접근
  v.sz;

  Vector* v1; // (->) 연산 접근
  v1->sz;
}

구조체의 요소에 접근하기 위해서는 (.) 연산을 사용하면 된다. 만약 구조체가 pointer 타입으로 정의 될 경우 (->) 연산을 사용해 내부 데이터에 접근이 가능하다.

위의 Vector 구조체 내 존재하는 double 포인터 같은 경우 double 배열을 가리킬 수 있다. double 배열을 가리킬 요소를 추가하는 코드를 보자.

void vector_init(Vector& v, int s)
{
	v.elem = new double[s];
	v.sz =s;
}

vector_init 코드 같은 경우 Vector를 참조연산으로 전달하여, 해당 구조체내 정보를 초기화하는 함수이다. elem 포인터 같은경우 new 연산자를 활용해, Heap 영역에 동적 메모리를 할당받은 후 double 배열을 생성한다. new 연산자로 생성된 배열 혹은 객체는 프로그램이 종요되거나 delete 연산을 통해 소멸할 때까지 유효하다.

// cin을 이용해 s개의 정수를 읽고 그 합을 반환하는 함수
double read_and_sum(int s)
{
  Vector v;
  vector_init(v,s);

  for (int i = 0; i != s; ++i)
    cin >> v.elem[i];

  double sum = 0;

  for (int i = 0; i != s; ++i)
    sum += v.elem[i];

  return sum;
}

위의 코드는 간단하게 숫자를 인자로 전달받아, 전달받은 인자의 갯수만큼 double 버퍼할당하고 채워서 총합을 구하는 함수이다. 우리가 사용할 수 있는 표준 입력 출력을 이용해 간단하게 구현해 볼 수 있다. (직접 작성해서 해보기 바란다.)

클래스

사용자 정의 타입 을 좀 더 실세계와 유사하게 하기 위해서는 데이터를 “표현하는 방식”과 “연산” 사이의 개선이 필요하다. 특히, 사용자가 메모리 표현에 접근하지 못하게 함으로 타입 내 데이터의 일관성을 유지하고 쉽게 사용할 수 있도록 할 수 있다는 장점이 있다. 이를 위해 도입된 사용자 정의 타입의 메커니즘이 “클래스” 이다. 클래스는 “타입의 인터페이스” 와 “연산에 필요한 구현”을 분리한다.

클래스는 데이터 뿐만아니라 함수 타입등을 멤버로 가질 수 있다. 연산에 필요한 구현을 사용하는 부분은 외부에서 접근하지 못하도록 하고 외부에서 값을 읽을 필요가 있는 부분은 인터페이스로 만들어 제공한다.

클래스는 외부의 접근을 제한하기 위해 public, private, protected 라는 접근 제한자 3가지를 제공한다.

클래스 같은 경우 Default 접근 제한자가 private 이고, 구조체 같은 경우 Default 접근 제한자가 public 이다.

class Vector {
public:
  Vector(int s) : elem{new double [s]}, sz{s} {} // Vector 생성
  double& operator[](int i) { return elem[i]; } // 요소 접근 : index연산자 재정의
  int size() { return sz; }
private:
  double* elem;
  int sz;
};

위와 같이 Vector 타입을 클래스로 재정의 할 수 있다. 위와 같이, Vector 객체는 요소를 가리키는 vhdlsxj (elem)와 요소의 개수를 포함하는 Handle이라고 부른다. Vector 내 요소의 개수는 Vector마다 다를 수 있고, 보통 Handle의 크기는 고정되며, elem과 같이 new를 이용해 Heap 영역에 할당해 참조하는 방법을 많이 사용한다.

Vector class는 데이터에 접근 및 사용하기 위해 Vector(), operator[], size() 인터페이스를 이용해야한다. 우리가 이전에 작성한 read_and_sum() 즉, 값을 읽은 뒤 총합을 구하는 함수를 좀 더 간소화해 구현할 수 있다.

double read_and_sum(int s)
{
  Vector v(s);
  for(int i = 0; i != v.size(); i++)
    cin >> v[i];
  double sum = 0;
  for(int i = 0; i != v.size(); i++)
    sum += v[i];

  return sum;
}

Vector class와 동일한 이름을 갖는 함수를 우리는 생성자 (constructor)라 정의한다. 클래스를 객체화할 때 사용하며, 우리가 구조체를 구현할 때 사용한 vector_init()함수를 대체할 수 있다.

생성자 같은 경우 일반함수와 달리 “클래스의 객체를 생성할 때 생성자의 사용이 보장되므로, 변수를 초기화 하지 안흔 실수”를 방지할 수 있다. 위에서 사용중인 생성자를 보자.

Vector(int s) : elem{new double [s]}, sz{s} {} // Vector 생성

Vector의 생성자를 보면, 인자를 전달받아 배열 초기화 및 사이즈를 초기화한다. 만약 인자를 전달받지 않는다면 Compile error를 나타낼 것이며, 사용자는 쉽게 알 수 있다. 또한 해당 문법에서 brace-init 문법을 볼 수 있다. 우리는 보통 “()” 를 이용해 초기화한다. 하지만 modern C++ 에서는 “{}” 를 이용해 초기화하며, 특히 정수 값 같은 경우 많이 사용한다.

”{}”를 활용하지 않을 경우 컴파일러가 자체적으로 casting을 하게 되며, 값에서 오류를 나타내지 않는다. 반면에 “{}” 사용할 경우 컴파일러 단에서 잘못된 것을 User에게 알려주기 때문에 쉽게 파악할 수 있으며 사전에 버그를 방지할 수 있다. 생성자에서는 유용하게 사용가능하다.

우리는 인터페이스로 operator[] 인덱싱 함수를 재정의했다. overriding은 추후에 구체적으로 다루도록 하겠다.

클래스와 구조체의 차이

아무래도 공부하다보면 가장 많이 떠오르는 질문일 수 있다.

구조체와 클래스 사이에는 근본적인 차이가 없다. 구조체에서도 클래스와 동일하게 멤버 함수로 표현이 가능하고 interface처럼 이용할 수 있다. 차이라고 보자면, 기본적인 접근 제한자가 public 이냐 private이냐 정도일 것이다. 코딩을 하다보면 코딩 스타일이나 규약을 읽게 될텐데 해당 코드에 대해 규율에 맞게 정의하고 사용하면 된다고 생각한다. 소모적인 논쟁은 하지 말고 그냥 그렇구나 정도만 생각하고 넘어가도록 하자 :)

공용체 (Union)

Union은 모든 멤버가 같은 메모리 주소에 할당되는 struct이며, 공용체의 크기는 가장 큰 멤버의 크기와 같다.

Union의 구조를 잘 설명한 icarus Union 그림자료가 있어 공유하고자 한다.

uion structure

구조체를 선언하게 된다면, 원래 빨간, 파란, 초록 데이터가 따로따로 메모리 영역에 잡히게 된다. Union을 사용하게 될 경우 가장 큰 영역인 빨간색 데이터만 메모리에 잡히게 되고 파란색과 초록색은 공유해서 사용하게된다. Simple 예시를 코드로 보자.

enum Type { ptr, num };

struct Entry {
	string name;
	Type t;
	Node* p; // Type 이 ptr일 경우 사용
	int i;   // Type 이 num일 경우 사용
};

void foo(Entry pe)
{
	if (pe->t == num) cout << i;
	else cout <<  *p;
}

위의 코드를 보자면, Entry내 Node와 i는 Type에 따라서 사용되는 경우가 다르다. 즉, 동시에 사용되지 않아 메모리 낭비가 발생한다. 이 같은 경우 union을 이용할 수 있다.

union Value {
	Node* p;
	int i;
}; // (;) 붙어야함.

struct Entry {
  string name;
  Type t;
  Value v; // Type 이 ptr일 경우 사용
};

Union 같은 경우 어떤 값을 갖는지 모르기 때문에 프로그래머가 관리해야한다. 우리는 foo() 함수를 이용해 Type을 통해 관리하도록 만들었다. Type을 사용해 프로그래머가 관리하는 방법을 Tagged union 방법이라고 부르며, Tagging을 통해 관리를 용이하게 하는 장점이 있다. Tag를 이용하지 않고 Union을 사용할 경우 버그가 유발될 수 있기 때문에 UnTagged union 방식은 사용하지 않는 것을 권장한다.

하지만, tagged Union같은 경우 사용자가 관리해야하는 귀찮은 부분이 존재한다. 이를 해결하기 위해 표준 라이브러리에서는 union 대신 variant 타입을 제공하고 있으며, 사용자에게 편리성을 제공한다. variant 타입을 자세히 알고 싶은 사람은 여기 를 클릭해서 확인해보자.

열거형 (Enum)

C++는 클래스 외에도 여러 값을 열거할 수 있는 간단한 사용자 타입을 제공한다.

enum class Color { red, blue, green };
enum class Traffic_light { greeb, yellow, red };

Color col = Color::red;
Traffic_light light = Traffic_light::red;

위와 같이 열거형을 사용하는 방법은 enum class로 선언하면 된다. 열거자의 스코프는 해당 enum class로 제한된다. 동일한 이름을 다른 enum class에서 사용해도 혼동없이 사용이 가능하다. 즉, red라는 상수를 사용할 때 class의 scope가 다르면 Color::red, Traffic_light::red 사용가능하다. enum을 사용할 경우 코드의 가독성이 향상되고 기호로 표현함으로 에러의 소지도 줄일 수 있다.

C 언어를 사용해 본 사람이라면 enum을 잘 알고 있을텐데 기본적으로 int형 값으로 변환되서 사용된다. 암묵적으로 형변환이 되기 때문에 버그가 유발 될 수 있다. 따라서, C++에서는 enum class를 제공하며, 암묵적 형변환을 막아주는 장점이 있다.

Color x = red; // red 값이 모호해서 에러 발생
Color y = Traffic_light::red; // 다른 타입으로 인식
Color z = Color::red;

int i = Color::red; //형변환 에러 발생

enum Color_enum_test { red, green, blue };

int col = Color_enum_test::red; // 형변환 가능 --> enum class가 아니라 암묵적 형변환 됨

기본적으로 enum class는 대입과 초기화, 비교연산자에 대해서만 정의되어져있다. 그 외의 코드는 추가로 구현이 가능하다.

조언

  1. 내장 타입이 너무 로우레벨이라고 느껴진다면 잘 정의된 사용자 정의 타입을 사용하라.
  2. 연관된 데이터는 struct or class로 구조화 하라.
  3. class를 이용해 인터페이스와 구현부를 구별하라.
  4. struct는 기본적으로 public class이다.
  5. class 초기화를 보장하고 단순화 할 수 있도록 생성자를 정의하라.
  6. union을 사용할 경우 표준 라이브러리 나 tagged union을 사용하라.
  7. 열거형으로 명시된 상수 집합을 사용하라.
  8. 에러는 최소화 할 수 있도록 scope가 정의된 열거형 (enum class)를 사용하라.
  9. 열거형 (enum) 같은 경우 추가로 연산자를 재정의해 사용하도록 하자.