본문 바로가기
프로그래밍언어/C++

[Effective C++] 항목1 ~ 4

by 목가 2018. 1. 23.
반응형

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


C++ 이해를 쉽게 하기 위해 단일언어들의 상관관계가 있는 연합체로 보고 각 언어 규칙을 각개격파한다.
- 시각이 단순해지고 기억하기 편함.
- 4가지 단일언어로 나누어 본다.( C, 객체지향 개념의 C++, 템플릿 C++, STL )

: 블록, 문장, 선행 처리자, 기본제공 데이터타입, 배열, 포인터
객체지향 개념의 C++ : 클래스(생성자, 소멸자), 캡슐화, 상속, 다형성, 가상함수(동적 바인딩)
- 템플릿 C++
- STL : Standard template Library

효과적인 프로그래밍을 위해 한 하위 언어에서 다른 하위 언어로 옮겨가면서 대응 전략을 바꿔야 한다.
ex) C에서는 값에 의한 전달이 참조에 의한 전달보다 효율적
객체지향 C++ 에서는 상수 객체 참조자에 의한 전달이 효율적
STL에서는 값에 의한 전달이 효율적



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

문제:
#define ASPECT_RATIO 1.653 이 있을 때,
소스코드가 컴파일러에 넘어가기 전에 선행처리자가 ASPECT_RATIO라는 문자를 숫자로 모두 바꿔버린다. 따라서 컴파일러가
쓰는 기호 테이블에 들어가지 않는다.
숫자로 대체된 코드에서 컴파일 에러가 발생한다면 컴파일 메세지에서 1.653라는 숫자가 어디에서 왔는지 헷갈릴 수 있다.

해결법:
매크로 대신 상수를 사용한다.
const double AspectRatio = 1.653;

AspectRatio 는 컴파일러 눈에도 보이고 기호테이블에도 들어간다.
또, 위처럼 double형의 경우 최종코드 size가 define을 썼을 때 보다 줄어들 수 있다. 선행처리자의 경우 모든 ASPECT_RATIO를 1.653으로 바꿔주기 때문에 목적코드에서 ASPECT_RATIO의 개수만큼 1.653의 사본을 복사한다. 하지만 상수 변수는 아무리 여러번 쓰여도 한 개의 사본만 생성하기 때문이다.

#define을 상수로 교체할 때 주의할 점 2가지.
1.상수 포인터 정의 시 포인터는 const로, 포인터가 가리키는 대상도 const로.(안정성)
ex) const char * const authorName = "Scott Meyer";
    ㄴ대상 상수화  ㄴ포인터 주소 상수화
하지만 char* 보다 string 객체 사용이 좋다. const std::string authorName("Scott Meyer");

2.클래스 멤버로 상수 정의하는 경우
class GamePlayer{
private:
 static const int NumTurns = 5;     //상수 선언(헤더파일)
 int score[NumTurns];
 ...
}

C++에서는 "정의"가 마련되어 있어야 하는게 보통이지만 정적 멤버로 만들어지는 정수류 타입(int, char, bool 등)의 클래스 내부 상수는 예외이다.
단, 클래스 상수 주소를 구한다든지, 컴파일러가 잘못 만들어진 경우는 정의를 제공해야한다.

const int GamePlayer::NumTurns;    //정의(구현파일)
 값이 주어지지 않는 이유 : 클래스 상수의 초기값은 해당 상수가 선언된 시점에서 바로 주어지기 때문.

오래된 컴파일러의 경우는 초기값을 상수 '정의'시점에 준다.
하지만 이 경우 클래스에서 배열 멤버를 선언할 때 문제가 될 수 있는데, '나열자 둔갑술(enum hack)'을 사용.

class GamePlayer{
private:
 enum { NumTurns = 5 };   //enum 사용

 int score[NumTurns];
 ...
}
장점 1. enum의 주소를 취할 수 없으므로, 선언 한 정수상수를 다른 사람이 주소를 얻다거나 참조자를 쓸 수 없도록 가능.
장점 2.const객체 사용 시 컴파일러에 따라 메모리 할당하는 경우가 있음. enum 사용시 불필요한 메모리할당이 없다.


#define 지시자의 오용 사례(매크로 함수)
#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))

int a=5, b=0;
CALL_WITH_MAX(++a, b);     // a가 2번 증가
CALL_WITH_MAX(++a, b+10); // a가 1번 증가

해결책 => inline 함수 이용.
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
 f(a > b? a: b);
}
// +매크로 효율 유지




항목 3: 낌새만 보이면 const를 들이대 보자.
char greeting[] = "Hello";
const char * const p = greeting;
 ㄴ대상 상수화  ㄴ포인터 주소 상수화
== char const * const p = greeting;

void f1(const Widget *pw);  ==  void f2(Widget const * pw);


STL 반복자(iterator)는 포인터를 본뜬 것. const로 선언한다면 포인터를 상수화하는 것과 같다.

std::vector<int> vec;

const std::vector<int>::iterator iter = vec.begin();

*iter = 10; //가능
++iter;     //Error

가리키는 대상의 값을 변경하지 못하도록 하고싶을 경우
std::vector<int>::const_iterator cIter = vec.begin();

*iter = 10; //Error
++iter;     //가능


- operator 함수 반환값을 const로 하기.

class Rational { ... };
const Rational operator* (const Rational& lhs, const Rational& rhs);

Rational a,b,c;

if( a * b = c)  ...// a * b == c 의 실수, operator* 의 반환 타입이 상수이므로 실수를 막는다.


- 상수 멤버 함수

사용 목적
1.클래스의 인터페이스를 이해하기 좋게 하기 위함. 그 클래스로 만든 객체를 변경할 수 있는 함수와 그렇지 않은 함수를 사용자가 알게 하기 위함.
2.상수함수에서 "상수 객체에 대한 참조자"를 매개변수로 받으므로 C++ 프로그램 실행 성능을 높힌다.

예제) 오버로딩 된 상수함수
class TextBlock{
public:
 const char& operator[](std::size_t position) const
{ return text[position];}
 
 char& operator[](std::size_t position)
{ return text[position];}
private:
 std::string text;
};

TextBlock tb("Hello");
const TextBlock ctb("World");

tb[0] = 'x';   //가능
ctb[0] = 'x';  //Error, const operator[]함수에서 반환값이 상수형이기 때문.

반환값 const char (참조자로 반환하는 이유) : 기본 제공 타입형의 반환값을 수정하는 일은 절대로 있을 수 없다.
C++의 특성상 반환 시 값에 의한 반환을 수행. (사본값이 반환됨)

상수 맴버함수의 2가지 상수성
1.비트수준 상수성(물리적 상수성) : 그 객체의 어떤 데이터 멤버의 값도 건드리지 않는다.
2.논리적 상수성 : 참조자를 반환할 경우 참조자를 가지고 값을 변경하지 못하게 한다.

예제)
class CTextBlock{
public:
 char& operator[] (std::size_t position) const
 { return pText[Position]; }
private:
 char *pText;
};

Const CTextBlock cctb("Hello");
char *pc = &cctb[0];

*pc = 'J';     // 가능하다. operator 함수가 물리적 상수성만 지키므로, 참조자를 반환받아서 값을 수정할 경우 가능함.


- mutable : 상수 멤버 함수 내에서 mutable형의 맴버변수들은 수정이 가능하다. 


- 상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법.

class TextBlock {
public:
 const char& operator[] (std::size_t position) const
{
 ...
 return text[position];
}
 char& operator[] (std::size_t position)
{
 ...
 return text[position];
}
private:
 std::string text;
};

=> 코드중복( 컴파일 시간, 유지보수, 코드 크기 부풀림 ...)

해결법 : 비상수 oeprator[]가 상수 버전을 케스팅하여 호출하도록 구현.

class TextBlock {
public:
 const char& operator[] (std::size_t position) const
{
 ...
 return text[position];
}
 char& operator[] (std::size_t position)       //상수버전 operator를 호출하고 끝
{
 return 
const_cast<char&>(                     // const 특성 제거
static_cast<const TextBlock&> // 명시적 형변환 cast 연산자( c style (type)과 다른점? 런타임 <->컴파일타임)
(*this) [position]
);
}
private:
 std::string text;
};



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

int x;

class Point{
 int x,y;
}

초기화 될 때도 있지만 안될 때도 있다.
 - 몇몇 초기화 규칙
(C++의 C부분)만 사용하고 런타임 비용이 소모될 수 있는 상황 : 초기화 보장 X
(C++의 C부분) 배열 각 원소 : 초기화 보장 X
(C++의 STL부분) vector : 초기화 보장 O

따라서 가장 좋은 방법은 항상 초기화 하는 것.

- 기본제공 타입 비맴버 객체
int x = 0;
const char* text = "A C-style string";

double d;
std::cin >> d;  //입력스트림에서 읽으면서 초기화 수행

- 생성자 초기화

ABEntry::ABEntry(const std::string&name. const std::string& address, const std::list<PhonNumber>& phones)
{
 theName = name;
 theAddress = address;        //초기화가 아닌 '대입', 대입 전 이미 초기화 과정을 거침 (초기화 과정 무쓸모)
 thePhones = Phones;
 numTimesConsulted = 0;     
}

대입을 통한 초기화
VS
초기화리스트 사용

ABEntry::ABEntry(const std::string&name. const std::string& address, const std::list<PhonNumber>& phones)
:
 theName(name),
 theAddress(address),
 thePhones (Phones),
 numTimesConsulted(0)         
{}

초기화 리스트 사용한 경우 데이터 멤버를 기본 생성자로 초기화 하고 싶을 경우 () 로 한다.
혹자는 오버가 아닌가 생각할 수 있음. 사용자 정의 타입은 생략해도 기본생성자로 초기화 되기 때문.
하지만 초기화 리스트에 모두 초기화 하는 습관을 기르자. 그래야 빼먹는 실수를 안한다.
(int numTimesConsulted <- 기본제공 타입의 경우 초기화되지 않을 수 있음)

초기화 리스트가 너무 길어질 경우 private 멤버함수에서 처리.

-비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.

번역단위: 컴파일로 하나의 목적파일을 만드는 바탕이 되는 소스코드.(기계어화), (소스파일 + #include 파일)

=>다른 번역단위에 있는 비지역 정적 객체의 초기화 과정에서 서로 의존적이라면 문제 발생 가능.

ex)
문제점: 한 번역 단위에 있는 비지역 정적 객체를 초기화 할 때 다른 번역 단위에 있는 비지역 정적 객체가 사용될 경우

-- 라이브러리 --
class FileSystem {           //라이브러리에 포함된 클래스
public:
 std::size_t numDisks() const;
}

extern FileSystem tfs;

-- 사용자 측 --
class Directory {              //라이브러리의 사용자가 만든 클래스
public:
 Directory(params);
};

Directory::Directory( params )
{
 std::size_t disks = tfs.numDisks();
}

사용
Directory tempDir( params);      //tempDir 생성자 호출 시 tfs가 초기화되어있지 않으면 문제 발생.

해결책: 비지역 정적 객체 -> 지역 정적 객체(싱글톤 패턴)

class FileSystem { ... };
FileSystem& tfs()                  //tfs 객체를 이 함수로 대신함.
{
 static FileSystem fs;
 return fs;
}

class Directory { ... };
Directory::Directory( params )
{
 std::size_t disks = tfs().numDisks();
}
Directory& tempDir()               //tempDir 객체를 이 함수로 대신함. 함수화 하여, 호출할 일이 없다면 호출하지 않아서
{                                     //생성, 소멸 비용도 생기지 않게 함.
 static Directory  td;               
 return td;
}

하지만 다중스레드 시스템에서는 동작 장애가 생길 수 있음.(race condition)
해결: 다중스레드로 돌입하기 전 시동 단계에서 참조자 반환 함수를 전부 손으로 호출.


반응형

'프로그래밍언어 > C++' 카테고리의 다른 글

[Effective C++] 항목 26~28  (0) 2018.04.10
[Effective C++] 항목 22 ~ 25  (0) 2018.01.23
[Effective C++] 항목 18 ~ 21  (0) 2018.01.23
[Effective C++] 항목 13 ~ 17  (0) 2018.01.23
[Effective C++] 항목 5 ~ 12  (0) 2018.01.23

댓글