프로그래밍 언어/Effective C++

[Effective C++]항목 1~4 : C++ 기초 사고방식

우향우@ 2023. 3. 22. 00:41

항목 1 : C++은 언어들의 연합체이다.

C++은 다중패러다임 프로그래밍 언어로써 절차,객체 지향 프로그래밍, 함수형 프로그래밍, 일반화 프로그래밍등의 다양한 프로그래밍 패러다임을 적용할 수 있다. 이러한 C++을 공부할땐 다소 혼란스러울 수 있는데 이러한 C++을 다음 네가지 영역으로 구분한다면 도움이 될 수 있다.

 

C : 기존 C에서도 제공되던 부분들이며 C의 규칙들을 따른다.

객체 지향 개념의 C++: C++로 넘어오며 추가된 클래스등의 객체지향 개념들이다.

템플릿 C++ : C++의 강력한 기능중 하나인 템플릿을 활용하는 부분들이다. 일반화에 큰 힘을 준다.

STL : 표준 라이브러리로 컨테이너와 알고리즘, 반복자와 함수 객체들로써 돌아간다.

 

C++을 구성하고 있는 이 네가지 부분에 대한 패러다임에 익숙해진다면 C++에 익숙하다고 할 수 있을 것이다.

 

항목 2 : #define 대신 const, enum, inline을 쓰자

흔히들 #define으로 상수나 매크로를 만들어 사용하는 경우가 많다. 이 둘은 단점이 꽤나 있어 const, enum, inline으로 대체할 수 있다면 대체하는 것이 좋다.

 

define 상수의 단점

1. 에러 메세지에 변환된 상수값 자체로 뜨기에 디버깅에 좋지않다.

2. 사용 범위를 조절할 수 없다.

3. 매번 상수 사본이 등장할 수 있어 성능상 좋지않다.

 

따라서 const나 enum으로 이를 대체하는 것이 좋다.

참고) 클래스에서 static const 멤버를 사용할 때는 대부분 외부 정의가 아닌 내부 선언부에 해야되지만 일부 오래된 컴파일러는 정의 위치에 초기화할 것을 요구하는 경우도 있다.

참고2) 위의 구식 컴파일러의 경우 해당 멤버를 다른 배열 멤버의 크기로 사용할 수 없게된다. 이럴땐 enum으로 상수를 생성하면 된다.

 

매크로의 단점

1. 매개변수를 넘겨받는 실제 함수가 아닌 코드에 매크로 인자를 붙여넣는 것이므로 전형적인 함수와는 다른 처리를 보일 때가 있다. (대표적으로 i++를 인자로 쓸 경우, 연산자 우선순위에 의한 경우)

 

따라서 이를 inline으로 대체하는 것이 좋다. inline은 함수의 로직을 그대로 제공해주면서 호출 스택을 사용하지않는다는 점에서 매크로보다 유용하다.

 

항목 3 : const를 쓸 수 있는곳에는 const를 쓰자

변수나 클래스 메소드에 const를 붙일 수 있는 것에는 const를 쓰는 것이 좋다. 이유는 다음과 같다.

1. 메소드가 const라는 것을 명시해주면 사용자가 안심하고 사용할 수 있다.

2. 매개변수가 const라면 사용자가 안심하고 변수를 넘겨줄 수 있다.

3. 사용자가 수정해서는 안되는 데이터를 잘못 수정하지 않도록 막아준다.

4. const로써 넘겨진 객체에서도 const 메소드는 호출 가능하므로 기능을 보강해줄 수 있다.

 

포인터에 대한 const

const char *p 또는 char const *p => 포인터가 가르키는 대상이 상수, 즉 가르키는 대상의 값을 바꿀 수 없다.

char * const p => 포인터 자신이 상수, 즉 가르키는 대상을 다른 대상으로 바꿀 수 없다.

기준은 *보다 const가 앞에 있나 뒤에 있나이며 당연히 두가지를 동시에 쓸 수도 있다.

 

const 오버로드

클래스 메소드는 인자가 같더라도 const유무에 따라 오버로드가 가능하다. 이러면 const 포인터나 레퍼런스로 호출 시 const가, 비 const로 호출 시 비const가 호출된다.\이때 오버로드된 함수들이 리턴값만 const 레퍼런스냐 일반 레퍼런스냐의 차이만 있고 내부 코드는 동일한 경우도 있을 수 있다. 이때 코드 중복을 피하기위해 const메소드를 정상 구현하고 비const 메소드에서 이를 그대로 호출한 뒤 그 리턴값을 const에서 비const레퍼런스로 강제 캐스팅하고 리턴하는 방법을 쓸 수 있다. const를 비const로 강제 캐스팅하는건 그렇게 건강한 방식은 아니지만 해당 상황만 잘 통제할 수 있다면 코드중복을 피할 수 있는 방법이 될 수 있다.

 

물리적 상수성, 논리적 상수성

상수성에는 두 가지가 있다.물리적 상수성은 실제 객체의 멤버들의 데이터 값이 변하지 않는 것을 말한다.논리적 상수성은 사용자 입장에서 해당 객체의 상태가 변하지 않는 것을 말한다. 즉 내부 멤버가 바뀌더라도 사용자 입장에서는 객체의 상태가 변하지 않은 것과 마찬가지라면 논리적 상수성은 성립된다. 내부적으로 캐쉬 구조를 사용하거나 더티 비트등의 로직이 구성되있을때 이에 해당된다.반대로 내부 멤버는 바뀌지 않았더라도 사용자 입장에서 객체의 상태가 변했다면 논리적 상수성은 성립되지 않는다. 객체 멤버 포인터로 외부 데이터를 가르킨 상황이 이에 해당된다. 포인터가 가르키는 대상의 값이 바뀌면 물리적 상수성은 유지되지만 논리적 상수성은 무너진다. 전자의 경우 특정 멤버에 mutable 키워드를 붙이면 해당 멤버는 상수 객체일때도 수정이 가능하다. 이로 전자의 문제를 해결 할 수 있다.하지만 후자의 경우 스스로 클래스 구조를 잘 설계해야할 것이다.

 

항목 4 : 객체를 사용하기 전에는 반드시 초기화하자

C++에서는 모든 객체가 자동으로 초기화되지 않는다. 전역변수등의 일부 상황에서는 자동으로 초기화되지만 되지않는 경우도 많기에 그냥 모든 상황에 초기화를 해주는 편이 좋다.

 

생성자 초기화 목록

객체의 생성자에서 멤버들을 초기화할 때는 초기화 목록을 적극 활용하자. 본문에서 멤버에 값을 넣으면 초기화가 아닌 대입이 되므로 생성과 대입이 별개로 일어나 성능이 좋지 않을 수 있다. 특히 큰 성능을 먹는 객체를 멤버로 가지고 있다면 치명적일 것이다. 그에 비해 초기화 목록에서 초기화한다면 확실히 생성으로 초기화 되므로 성능이 더 좋다.

또한 커스텀 객체의 경우 초기화 목록에 넣지않으면 자동으로 기본생성자가 호출된다. 하지만 기본생성자를 호출하는 것으로 충분한 상황이라해도 가독성측면에서 기본생성자를 생성자 목록에서 명시적으로 호출해주는 것이 좋다.

 

생성 순서와 소멸 순서

객체가 생성되고 소멸될때 멤버들은 다음 순서로 생성된다.

부모 -> 자식

위쪽 멤버 -> 아래쪽 멤버

 

소멸은 반대로

자식 -> 부모

아래쪽 멤버 -> 위쪽 멤버

순서이다.

 

정적 객체 생성 순서

c++에서 정적 객체를 초기화할 때 주의해야할 점이 있다. 바로 지역 정적 객체가 아닌 비지역 정적 객체들은 서로 다른 번역단위(include를 처리한 후의 각 소스파일 단위)에 있을 경우 초기화 순서를 알 수 없다. 만약 이러한 상황에서 한 번역 단위의 정적 객체가 다른 번역 단위의 정적 객체를 이용하여 초기화할 때 필요한 정적 객체가 아직 초기화가 되지않아 문제가 발생할 수 있다.

이를 해결하기 위해서는 지역 전역 객체를 활용하면 된다. 지역 전역 객체의 생성 시점은 해당 지역 전역 객체의 선언문에 처음 도달한 순간이기에 이를 조절할 수 있다. 이를 이용하여 한 지역 전역 객체를 가지고 이를 리턴해주는 싱글톤 객체 반환 함수를 만들고 이 함수를 초기화에 해당 객체가 필요한 쪽에서 호출하여 가져오면 함수 호출에 의해 확실히 객체가 생성이 되고 리턴되기에 문제가 없다.

 

 

 

 

참고서적

Effective C++