리얼 개발

[Effective C++] 공부 정리 2 본문

C++/효과적인 C++

[Effective C++] 공부 정리 2

econo-my 2024. 7. 13. 18:01

1. C++에 왔으면 C++의 법을 따릅시다.


항목1 : C++를 언어들의 연합체로 바라보는 안목은 필수

C++은 다음 4가지 하위 언어들의 연합체이다.

  • C언어
    • C++은 여전히 C를 기본으로 하고 있다.
    • 블록, 문장, 선행 처리자, 기본제공 데이터 타입, 배열, 포인터 등 모든 것이 C에서 왔다.
  • 객체 지향 개념의 C++
    • 클래스를 쓰는 C
    • 클래스, 캡슐화, 상속, 다형성, 가상 함수 등
  • 템플릿 C++
    • C++의 일반화 프로그래밍
    • 새로운 프로그래밍 패러다임인 템플릿 메타프로그래밍(template metaprograming : TMP)이 파생
  • STL
    • Standard Template Library의 줄임말
    • 컨테이너, 반복자, 알고리즘, 함수 객체

(*) C++을 사용한 효과적인 프로그래밍 규칙은 경우에 따라 달라진다. 그 경우란, 바로 C++의 어떤 부분을 사용하느냐이다.

 

요약

 C++은 오랫동안 발전되며 다양한 기능을 제공하는 강력한 툴이 되었다. 그렇기에 배움에 있어 진입장벽이 높지만 C++을 하위 4개의 언어로 쪼개서 생각할 수 있다면 C++ 이해의 관문에 들어가기 쉬울 것이다.

 

 

 

항목 2 : #define을 쓰려거든 const, enum, inline을 떠올리자.

가급적 선행 처리자보다 컴파일러를 더 가까이하자.

  • 매크로의 경우 컴파일러가 쓰는 기호 테이블에 들어가지 않는다.
  • #define ASPECT_RATIO 1.653
    • 해당 코드가 들어간 부분에서 컴파일 에러가 발생하면 에러 메세지에는 1.653이 나온다.
  • 매크로 대신 상수를 사용하자.
  • 매크로 함수 대신 인라인 함수에 대한 템플릿을 사용하자.
#define ASPECT_RATIO 1.653
	const double AspectRatio = 1.653;

	class GamePlayer
	{
	private:
		static const int NumTurns = 5; //상수 선언, 정의된 것이 아니다.
		int scores[NumTurns];
	};

	//클래스 상수의 정의는 구현파일에 둔다.
	//정의에는 상수의 초기값이 있으면 안된다.
	//왜냐하면 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문이다.
	const int GamePlayer::NumTurns;

	//정수 타입의 정적 클래스 상수에 대한 클래스 내 초기화를 금지하는 구식 컴파일러의 경우
	class GamePlayer2
	{
	private:
		enum { NumTurns = 5 };
		int scores[NumTurns];
	};

#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))

	//매크로 함수 대신 인라인 함수를 우선 생각하자.
	template<typename T>	//T가 정확히 무엇인지 모르기 때문에, 매개변수로 상수 객체에 대한 참조자를 쓴다.
	inline void CallWithMax(const T& a, const T& b)
	{
		f(a > b ? a : b);
	}

	void f(int Value)
	{
		std::cout << Value << std::endl;
	}

	int main()
	{
		int a = 5, b = 0;
		CALL_WITH_MAX(++a, b); //a가 두번 증가하는 괴현상, 인자를 여러번 평가한다.
		return 0;
	}

(*) 단순한 상수를 쓸 때는, #define 보다 const 객체 혹은 enum을 우선 생각하자.

(*) 함수처럼 쓰이는 매크로를 만들려면, #define 매크로보다 인라인 함수를 우선 생각하자.

 

요약

const, enum, inline의 존재를 늘 유념하고 #define을 될 수 있으면 피하자.

 

 

 

항목 3 : 낌새만 보이면 const를 들이대 보자!

  • const는 ‘의미적인 제약’ 을 소스 코드 수준에서 붙인다는 점과 어떤 객체의 내용이 불변이어야 한다는 제작자의 의도를 컴파일러 및 다른 프로그래머와 나눌 수 있는 수단이라고 할 수 있다.

 

  • 포인터
char str[] = "Hello";

char* p = greeting;    //비상수 포인터
											 //비상수 데이터

const char* p = greeting; //비상수 포인터
													//상수 데이터
													
char* const p = greeting; //상수 포인터
													//비상수 데이터
													
const char* const p = greeting; //상수 포인터
															  //상수 데이터											 
	

 

  • 반복자
std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin();
																//iter는 T* const 처럼 동작한다.
*iter = 10;                     //OK, iter가 가리키는 대상을 변경한다.
++iter;                         //에러! iter 은 상수이다.

std::vector<int>::const_iterator cIter = vec.begin();
																//cIter는 const T* 처럼 동작한다.
*cIter = 10;                    //에러! *cIter가 상수이기 때문에 안된다.
++cIter;                        //OK, cIter을 변경하는건 가능하다.

 

  • 상수 멤버 함수
    • 멤버 함수에 붙는 const 는 “해당 멤버 함수가 상수 객체에 대해 호출될 함수이다” 라는 사실을 알려준다. (?)
//멤버 함수의 경우 반환값, 매개변수, 함수 자체에 const를 붙일 수 있다.
const T func(const paramiter) const

//const 키워드가 있고 없고의 차이만 있는 멤버 함수들은 오버로딩이 가능하다.
Test operator*(const Test& test)
{
	return Test(a * test.a, b * test.b);
}

const Test& operator*(const Test& test) const
{
	return *this;
}
  • 컴파일러 쪽에서 보면 비트수준 상수성을 지켜야 하지만, 우리는 개념적인 상수성을 사용해서 프로그래밍 하면 된다.
    • 비트수준 상수성
      • 어떤 멤버 함수가 그 객체의 어던 데이터 멤버도 건드리지 않아야한다.
      • 객체를 구성하는 비트들 중 어떤 것도 바꾸면 안된다.
    • 논리적 상수성
      • 상수 멤버 함수도 객체의 일부 몇 비트를 바꿀 수 있되 사용자 측에서 알아채지 못하도록(객체의 상태에 영향을 주지 않도록) 해야 한다.
      • mutable 키워드 사용, mutable은 비정적 데이터 멤버를 비트수준 상수성의 족쇄에서 풀어준다.

 

  • 상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법
class TextBlock
{
public:
	const char& operator[](std::size_t position) const
	{
		//작업들
		return text[position];
	}
	
	char& operator[](std::size_t position) const
	{
		//operator[]의 반환 타입에 캐스팅을 적용, cosnt를 떼어낸다.
		//*this의 타입에 const를 붙이고 operator[]의 상수 버전을 호출한다.
		return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
	}
}

//반대로 상수 멤버 함수에서 비상수 멤버 함수를 호출하는 일은 할 수 없다.

 

 

요약

const를 붙여 선언하면 컴파일러가 사용상의 에러를 잡아내는 데 도움을 준다. 웬만하면 const를 아낌없이 붙이자.

 

 

 

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

  • 기본제공 타입으로 만들어진 비멤버 객체에 대해서는 초기화를 손수 해야한다. 이 부분을 제외하면 C++ 초기화의 나머지 부분은 생성자로 귀결된다.
  • 대입과 초기화를 헷갈리지 않는 것이 중요하다.
    • 객체의 데이터 멤버는 생성자의 본문이 실행되기 전에 초기화되어야 한다.
    • 기본제공 타입의 경우에는 생성자에서 대입되기 전에 초기화되리란 보장이 없다.
class ABEntry
{
public:
	ABEntry(const std::string name, const std::string& address, const std::list<PhonNumber>& phones);

	std::string GetName() const { return theName; }
	int GetInt() const { return numTimesConsulted; }
private:
	std::string theName;
	std::string theAddress;
	std::list<PhonNumber> thePhones;
	int numTimesConsulted;
};

ABEntry::ABEntry(const std::string name, const std::string& address, const std::list<PhonNumber>& phones)
{
	theName = name;        //지금은 모두 '대입'을 하고 있다.
	theAddress = address;  //'초기화'가 아니다.
	thePhones = phones;
	numTimesConsulted = 0;
	//theName, theAddress, thePhones의 경우 기본 생성자를 호출해서 초기화를 미리 해 놓은 뒤 곧바로 새로운 값을
	//대입하고 있다.
}

ABEntry::ABEntry(const std::string name, const std::string& address, const std::list<PhonNumber>& phones)
	:theName(name), theAddress(address), thePhones(phones), numTimesConsulted(0) //초기화
	//복사 생성자에 의해 초기화되고 끝난다.
{ }
  • 정적 객체(static object) : 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체, 다시 말해 main() 함수의 실행이 끝날 때 정적 객체의 소멸자가 호출된다.
    • 지역 정적 객체(local static object)
      • 함수 안에서 static으로 선언된 객체
    • 비지역 정적 객체(non-local static object)
      • 전역 객체
      • 네임스페이스 유효범위에서 선언된 객체
      • 클래스 안에서 static으로 선언된 객체
      • 파일 유효범위에서 static으로 선언된 객체

 

요약

데이터는 직접 초기화해주는게 웬만한 상황에서 좋다. 멤버 초기화 리스트를 사용하는 습관을 들이자.

 

'C++ > 효과적인 C++' 카테고리의 다른 글

[Effective C++] 공부 정리 5  (0) 2024.10.25
[Effective C++] 공부 정리 4  (0) 2024.08.20
[Effective C++] 공부 정리 3  (0) 2024.07.13
[Effective C++] 공부 정리 1  (1) 2024.07.13