CPP Module 06

과제를 통해 배울 수 있는 것

기존에 파이썬이나 자바를 쓰던 내 입장에서는 형변환 종류에 명시적 형변환, 그리고 암시적 형변환 정도만 알고 있었다. 그런데 C++ 에서는 단순히 그 두 가지 종류가 아닌, 여러 가지 종류의 형변환을 사용할 수 있었고 이번 과제는 그 다양한 종류의 형변환을 사용하는 것이다.

Exercise 00: Conversion of scalar types

참고: static_cast conversion: 공식 문서
참고: static_cast in C++: geeksforgeeks

단순히 말하면 char, int, double, float끼리의 스칼라 타입을 형변환하는 과제이다.
이들끼리는 명시적으로 형변환 해야한다고 되어있다. 기타 예외 처리 이런 건 제쳐두고 핵심 부분인 형변환만 다루도록 하겠다.

static_cast

Ex00에서 사용할 형변환은 static_cast 이다.

static_cast는 암묵적 및 사용자가 정의한 형변환의 조합을 사용하여 타입 간 형변환을 수행한다… 고 나와있는데, 이게 무슨 소리냐면 그냥 정수 -> 실수, 포인터 -> (void *)로 형변환 할 수 있다는 얘기다.

static_cast는 컴파일 단계에서 형변환이 이루어지기 때문에 컴파일러가 타입을 검증하여 타입 안정성이 보장된다는 장점이 있다. 그리고 당연히 명시적이기도 하다.
반대로 타입 안정성이 보장된다는 얘기는 타입에 있어서 유연하지 않다는 것이기도 하다.

사용 예시는 다음과 같다.

void ScalarConverter::strToChar() {
  std::cout << "char: " << _str[0] << std::endl;
  std::cout << "int: " << static_cast<int>(_str[0]) << std::endl;
  std::cout << "float: " << static_cast<float>(_str[0]) << std::endl;
  std::cout << "double: " << static_cast<double>(_str[0]) << std::endl;
}

ScalarConverter 클래스의 구현 일부분을 가져왔다. 멤버 변수 _str의 앞 글자를 static_cast를 이용하여 원하는 타입으로 형변환 해주는 식으로 구현했다.

std::stringstream

참고: std::stringstream: cpluscplus.com 참고: 씹어먹는 C++ - <7 - 1. C++ 에서의 입출력 (istream, ostream)>

std::stringstream 클래스의 객체는 일련의 문자들이 들어있는 문자열 버퍼를 사용한다. 멤버 변수인 str을 사용하여 문자열 객체로 이 문자들에 직접 접근할 수 있다.

여기서 istream으로부터 상속받아 사용할 수 있는 것 중, operator>>가 있다. 이걸 사용하면 std::cin과 같은 동작을 할 수 있기 때문에 extract formatted input을 얻을 수 있다고 나와있다. 따라서 이를 이용하여 형변환을 할 것이다.

std::stodstd::stoi 등이 c++11 버전인 관계로 std::stringstream을 이용하여 int는 형변환을 따로 해주었다.

void ScalarConverter::printInt() {
  std::stringstream ss(_str);
  int i;

  ss >> i;
  std::cout << "int: " << i << std::endl;
}

Exercise 01: Serialization

직렬화 하는 이유

참고: 데이터 직렬화(serialization)는 무엇이고 왜 필요한가?

디스크에 저장하거나 통신할 때에는 값 형식 데이터만 가능하다. 이때 값 형식 데이터는 스택에 메모리가 쌓이고 직접 접근이 가능한 데이터(ex. int, float, char, …)를 말한다. 참조 형식 데이터는 힙에 할당된 메모리 주소를 가지고 있어서 저장, 통신에 사용할 수 없다.

생각해보면 당연한게, 내 컴퓨터에서 A라는 메모리 공간에 데이터를 할당했다 치면 그 A 주소 값을 다른 컴퓨터에 넘겨봤자 주소가 같을 리가 없다.

그래서 각 주소 값이 가지는 데이터를 전부 모아 값 형식의 데이터로 변환하는, 데이터 직렬화가 필요한 것이다.

reinterpret_cast

참고: reinterpret_cast conversion : 공식 문서
참고: reinterpret_cast 연산자 : Microsoft

(공식 문서 양이 엄청 방대하다…)

reinterpret_cast포인터 <-> 다른 포인터, 정수 계열 <-> 포인터 형식의 변환을 제공한다. 서브젝트에서 요구한 Data * <-> uintptr_t 형변환에 제격이다.

해당 내용을 아래와 같이 적용할 수 있다.

uintptr_t Serializer::serialize(Data *ptr) {
  return reinterpret_cast<uintptr_t>(ptr);
}

Data *Serializer::deserialize(uintptr_t raw) {
  return reinterpret_cast<Data *>(raw);
}

Exercise 02: Identify real type

dynamic_cast

참고: dynamic_cast conversion: 공식 문서 참고: dynamic_cast 연산자: Microsoft

이번 과제는 어떤 객체가 주어질 때, 이 객체의 클래스 타입을 알아내는 내용이다. 이를 위해 dynamic_cast 형변환을 사용했다.

dynamic_cast는 클래스를 가리키는 포인터와 레퍼런스를 부모, 자식, 그리고 형제로 형변환한다. Base 클래스를 상속 받는 A, B, C 클래스가 있다면, A, B, C 끼리는 형제고, 각각 Base와는 부모-자식 관계니까 이 dynamic_cast를 사용하면 된다.

형변환에 실패하면 애초에 그 객체는 형변환 하려는 타입의 객체가 아니었던 거니까, 그 부분을 이용해서 아래와 같이 구현하면 객체의 타입을 알 수 있다.

void identify(Base *p) {
  std::cout << "------------------- IDENTIFY *p -------------------" << std::endl;
  if (dynamic_cast<A *>(p))
    std::cout << "type: A" << std::endl;
  if (dynamic_cast<B *>(p))
    std::cout << "type: B" << std::endl;
  if (dynamic_cast<C *>(p))
    std::cout << "type: C" << std::endl;
}

번외. C++에서 정적 클래스를 만드는 방법

처음에는 단순히 다른 곳에서 했던 것처럼 static class ScalarConverter 이런 식으로 선언했는데, clion에서 그렇게 쓰는 거 아니라고 알려줬다.

슬랙을 조금 뒤져보니까 정적 멤버 변수와 정적 멤버 함수만 가지는 클래스인데, 와중에 생성자들은 객체를 만들 필요가 없기 때문에 private으로 선언한다고 한다.
맨날 Car car = new Car("sonata", "차량 번호"); 이런 식으로 쓰다보니 저런 방식이 새롭긴 하다.

댓글남기기