항목1 - 포인터와 참조자를 구분하자.
C언어에서 열심히 사용되던 포인터와 C++에서 포인터를 대신하기 위한 레퍼런스.
비슷하지만 다른 특징을 가진 두 가지 개념을 구분해서 사용하는 것이 이번 정리의 목적이다.
구분을 위해서는 각각의 특징을 정리해보자.
레퍼런스
클래스나 구조체의 값을 접근할 때 '.'연산자를 이용한다.
NULL 값을 저장할 수 없다. ( 메모리 공간을 차지하는 객체를 참조해야 한다. )
선언될 때 반드시 초기화해야 한다. ( 한번 참조하면 다른 객체를 참조 할 수 없다. )
포인터
클래스나 구조체의 값을 접근할 때 '->'연산자를 이용한다.
NULL 값을 저장할 수 있다.
값을 참조하려면 * 연산자를 붙여야 한다.
특징을 통한 사용법
만약 내가 가리킬 객체가 NULL이 될 경우가 있는 경우에는 무조건 포인터를 사용해야 한다.
NULL이 되지 않을 것이 보장된다면 레퍼런스를 사용해도 된다.
반대로 생각하면 포인터의 경우에는 NULL 값인지 체크를 해야 한다.
opeator[]를 리턴할때 포인터면 *을 붙여야 값에 접근할 수 있으니 이럴때는 레퍼런스를 사용하는게 이쁘다.
책에서 NULL값을 가진 포인터를 레퍼런스가 받거나 하는 극단적인 예제들이 나와있는데
현업에서 코드가 복잡할 때 함수 리턴값이나 인자값 혹은 컨테이너들을 이용하다 보면 실수가 있을 수는 있을 것 같다.
만약 이런일이 발생한 다면 크래쉬가 나서 덤프를 확인하면서 힘들게 고치거나 안전하게 스마트 포인터를 이용하자!
항목2 - 가능한 C++ 스타일의 캐스트를 즐겨 쓰자.
C 스타일의 캐스트에 비해서 C++ 스타일의 캐스트는 기능별로 세부적으로 나누어져 있는 편이다.
그렇기 때문에 필요에 맞게 C++ 스타일의 캐스트를 이용하면 실수를 줄일 수 있고 코드를 볼 때 더 명확하게 의도를 파악할 수 있다.
C++ 스타일 캐스트를 이용하는것이 확실히 더 좋아보인다.
대신에 정말 간단한 캐스트이고 명확한 경우에는 기존 C 스타일 캐스트를 이용해도 될 것 같기는 하다.
그래도 가능하면 C++ 캐스트를 이용하자.
C++ 캐스트 간단 설명
const_cast
상수성 (const) / 휘발성 (volatile)을 붙이거나 제거할 때만 사용한다.
dynamic_cast
파생 혹은 형제 클래스로 캐스팅할 때만 사용한다.
가능하지 않으면 NULL을 리턴함으로 NULL 체크가 필요하다.
가상함수가 있는 경우에만 가능하다.
static_cast
상수성 / 휘발성 / 상속관계가 아닌 경우에만 사용한다.
reinterpret_cast
예제상으로는 함수 포인터 캐스팅 정도에 사용한다고 나와있다.
C 캐스팅과 비슷한 느낌이라고 보면 된다.
컴파일러마다 결과가 다를 수 있어서 코드 이식에 문제가 있을 수도 있다고 한다.
항목3 - 배열과 다형성은 같은 수준으로 놓고 볼 것이 아니다.
엄청 중요한 내용은 아니라서 간단히 정리하면...
부모클래스와 자식클래스가 있다고 할때 C++에서는 다형성을 통해
부모클래스 포인터에 자식클래스의 주소를 받을 수 있고 오버라이딩해서 사용할 수 있다.
부모클래스 배열에 자식클래스 인스턴스를 넘겼다고 하자.
이때 직접적으로 배열의 인덱스를 통해 접근한다거나
new []로 부모클래스 배열을 만들고 자식클래스 인스턴스의 주소를 넣고 delete []를 한다거나
하는 직접적으로 주소에 접근할 경우에 배열은 부모클래스의 사이즈만큼 이동할 것이기 때문에 예기치 않은 동작을 할 거라는 내용;;
항목4 - 쓸데 없는 기본 생성자는 그냥 두지 말자.
여기서 말하는 기본 생성자는 아무런 인자를 받지 않고 호출될 수 있는 생성자를 말한다.
초기화 리스트등을 이용해서 디폴트 초기값을 넣어두는 용도로 보통 사용한다.
근데 만약에 디폴트값을 정의할 수 없고 인자를 받아야지만 초기화 할 수 있는 객체라면 인자를 받는 생성자를 만들어줘야 한다.
이렇게 되면 객체를 생성할 때 인자를 넣어줘야 하기 때문에 생성부분의 코드가 살짝 복잡해진다.
class EquipmentPiece { public: EquipmentPiece(int IDNumber); .... };
위와 같은 객체가 있다고 했을 때 배열을 생성하면 조금 귀찮아진다.
EquipmentPiece bestPiece[10]; // error EquipmentPiece bestPiece[] = { EquipmentPiece(0), EquipmentPiece(1), ... }; // OK EquipmentPiece* bestPiece = new EquipmentPiece[10]; // error typedef EquipmentPiece* PEP; PEP bestPiece[10]; for (int i = 0; i < 10; ++i) { bestPiece[i] = new EquipmentPiece(i); // OK }
포인터를 사용하는 것이 깔끔해 보이기는 하는데 포인터를 동적할당해야 해서 메모리를 더 쓰는 문제가 있다.
이를 막기 위해서 그냥 10개짜리 메모리를 할당한다음에 placement new 연산자를 이용해서 미리 잡아놓은 메모리에 객체를 생성하는 방법을 사용해도 된다.
이럴 경우에는 소멸자도 따로 호출해줘야 하고 메모리 해제도 해야되고 마찬가지로 귀찮아진다.
자세한건 다른 항목에 나오니 일단 패스...
다른 문제점은 템플릿으로 구현된 컨테이너의 경우에 기본 생성자만 지원되는 경우가 있을 수 있어서 이런 경우에도 사용하기 힘들다.
다른 생각해볼 점은 가상 기본 클래스를 만들 때 기본 생성자를 지원하지 않는 경우이다.
이럴 경우 상속받는 쪽에서 생성자를 제대로 이해하고 구현해줘야 하는 문제가 있다.
상속받아서 사용하는것 치고는 불편한 느낌이라 잘 설계되었다고 말할 수 없을 것이다.
class EquipmentPiece { public: EquipmentPiece(int IDNumber = UNSPECIFIED); .... private: static const int UNSPECIFIED; };
신선하게 위와 같이 만들어 기본 생성자인척 하게 끔 만들었다고 해보자.
인자를 안 넣어도 UNSPECIFIED가 들어가 지기 때문에 위에 문제들은 모두 해결된다.
하지만 생성만 편해진거고 나머지 구현에서는 IDNumber가 UNSPECIFIED인가를 검사하는 부분들이 들어갈 것이기 때문에
전체적으로 보면 오히려 더 안 좋은 코드가 될 것이다.
그때 그때 다르겠지만...
생성자에서 인자를 받아야만 한다면 그냥 받는 쪽으로 구현하자 ㅠㅠ
'프로그래밍 > C, C++' 카테고리의 다른 글
[C++11/C++17] 유니폼 초기화 (Uniform Initialization) (0) | 2021.07.30 |
---|---|
[C++17] string_view 간단 정리 (0) | 2021.05.07 |
More Effective C++ - 항목5 ~ 항목8 (0) | 2018.04.01 |
warning MSB8004: Output Directory does not end with a trailing slash. (1) | 2011.08.21 |
_vstprintf_s 사용해 printf 함수 만들기 (0) | 2011.08.21 |