C언어는 사용자를 믿고 실행시킨다. 즉 많은 책임이 사용자에게 있다. 그렇기 때문에 컴파일 타임에서 에러가 나지 않는다. 반면에 C++에선 컴파일러가 타입 체크를 해주므로 컴파일 타임에서 에러가 나는 것. 이건 개발자에 축복이다!
C++에서의 형 변환
원래 컴파일러는 포인터간의 캐스팅은 이성적이지 않다고 판단한다. 그래서 사용자가 형 변환을 시키지 않으면 암시적 형 변환은 일어나지 않는다. 하지만 void형 포인터에 한해서는 다르다. void 타입을 구체적인 타입으로 캐스팅을 하는 건 이성적인 코드라고 판단한다. void형 포인터는 아무런 연산 (역참조, 덧셈, 뺄셈)을 할 수 없지만 char형 포인터로는 연산이 가능하므로 의미가 있다고 판단하는 것이다.
근데 그렇다고 암시적 형 변환을 해주는 것은 아니다. C++에는 여러가지 형 변환이 있는데…
intmain() { // 0. 명시적 형변환: 컴파일러의 타입 체크 기능을 끔 // 이렇게 짜지말것! 포인터에 대해 컴파일러가 보장해주지 않는 것이기 때문이다. char* p1 = (char*) malloc(1);
// 1. 이성적 형 변환: static_cast // 형 변환이 이성적이라면 캐스팅이 된다. char* p2 = static_cast<char*>(malloc(1));
int x = 0x1;
char* p = &x; // ERROR char* p = static_cast<char*>(&x); // ERROR
// 2. 비이성적 형 변환: char*로 재해석 해달라는 의미. // 컴파일러의 타입 체크 기능을 끄지 않고 형 변환을 강행. // C언어의 대부분의 형 변환을 지원한다. char* p = reinterpret_cast<char*>(&x);
constdouble PI = 3.14; // * 심볼릭 상수: 이름이 있는 상수
double* p = &PI // C언어에서는 에러가 나지 않는다. 즉 런타임에서 상수성을 보장하지 않는다. double* p = reinterpret_cast<double*>(&PI); // ERROR // C언어의 대부분의 형 변환을 지원하지만 이것만은 예외로 지원하지 않는다. // 이러한 캐스팅을 C언어에서 지원하는건 언어의 스펙 때문이지 // 이성적인 판단에 의한 것은 아니다.
// 3. 비상수 형 변환: const_cast // 문법적으로 지원을 한다. 필요한 경우가 있으닌까. // 하지만 대부분의 개발자들은 const_cast를 쓴다는 것은 설계가 잘못된 것이라고 생각한다. double *p = const_cast<double*>(&PI); }
Animal* p2 = static_cast<Animal*>(new Dog); // 이성적 형 변환이니 이렇게 해야... Animal* p2 = new Dog; // 이건 왜 되는거야?: 상속 관계이므로! // upcasting: 형 변환 연산자를 생략할 수 있다. // 컴파일러가 컴파일 타임에 두 클래스 간의 관계를 알고있기 때문.
Dog* pDog = p2; // ERROR: 컴파일 타임에 p2가 어떤 형일지 알 수가 없다. // downcasting을 위해 명시적 형 변환을 하면 컴파일러 기능을 꺼버리므로 문제가 됨. // RTTI를 사용해야 한다.
// RTTI(Runtime Type Information) // : C언어 표준이 아니라 컴파일러가 제공해 주는 기능 // 컴파일러 옵션에 켜는게 있다. // 이 기능을 사용하려면 class 안에 가상 함수가 하나라도 있어야함. // Lookup Table 위에 RTTI 정보가 저장이 된다. (밑에 참조) // 자바는 기본으로 제공이 된다.
// downcasting: dynamic_cast // 만약 캐스팅에 실패하면 return type이 null이 나옴. // 형 변환에 대한 안정성이 보장이 안되기 때문에 값을 확인하고 사용해야한다. Dog* pDog = dynamic_cast<Dog*>(p2); if(p2 == 0) // .. else // .. }
/* // enumeration enum { KOR = 0, ENG, JPN }; // C언어에서는 enum은 완전한 타입이 아니라 int의 호환형이다. // 그래서 int가 인수인 자리에도 enum이 들어갈 수 있다. // 하지만 C++에선 하나의 타입으로 인정받는다. // 대신 태그를 붙여서 선언해야함 enum LANG { KOR = 0, ENG, JPN }; // 하지만 전역에 이렇게 선언해 버리면 네임 스페이스가 오염되므로 // 클래스 안으로 넣어버리는게 더 좋다. */
classCursor { // 1. 객체의 임의 생성을 막기 위해 // 생성자를 private 영역에 정의 Cursor() {}
// 3. 대입과 복사를 금지하기 위해 대입 연산자 함수와 // 복사 생성자 함수를 private 영역에 정의
/* // 이렇게 막아줄 수 있지만... Cursor(const Cursor& c) {} Cursor& operator=(const Cursor& c) {} // 클래스 내부에서의 호출은 못막음 (아래 foo 함수) */
/* // 따라서 Cursor(const Cursor& c); Cursor& operator=(const Cursor& c); // 이렇게 해서 링킹 타임에 에러를 나게 만든다! */
// 근데 이건 가독성이 떨어짐. // 그래서 새 표준에선 다음과 같이 쓴다. Cursor(const Cursor& c) = delete; Cursor& operator=(const Cursor& c) = delete;
static Mutex mutex; // 고프 싱글톤 static Cursor* pInstance; // 이건 단순한 선언에 불과하다. // 초기화 해줘야함!
public: // private에 정의만 하면 내부에서 복사 생성자를 호출 할 수 있다. // 어떻게 막아야 할까? // 컴파일 타임에선 함수가 있고 없고의 문제 보단 제대로 타입이 잘 들어 간건지 타입 체크만 한다. // 링킹 타임에서 함수가 실제로 바인딩 될때 기계어가 없는걸 보고 에러를 나게 해야한다. voidfoo() { Cursor c; Cursor c1 = c; Cursor c2; c2 = c; }
// 2. 유일한 객체를 반환하기 위한 정적 인터페이스 도입 static Cursor* getInstance()// self in Android { /* static Cursor cursor; // 데이터 영역에 선언하는건 마이어's 싱글톤 // 이거보단 고프 싱글톤을 많이 씀. (힙에 선언) return &cursor; */
// mutex.lock(); // 동적할당에 실패하면 예외 발생 // -> unlock 호출이 안됨 // -> Dead Lock 발생
// RAII (Resource Acquisition is Initialization) // : 소멸될 때 자원을 획득하자! (?) AutoLock<Mutex> l(mutex); // 생성자와 파괴자를 통해서 lock/unlock함 // 예외 발생시 스택을 풀면서 나가는 성질을 // 이용한 것이다. (Stack Unwinding) // 스택이 풀리면서 파괴자가 호출됨. // 이를 통해 Exception Safety를 보장 if (pInstance == 0) pInstance = new Cursor; // mutex.unlock();