Chapter 1. C++에 왔으면 C++의 법을 따릅시다.
항목 1. C++를 언어들의 연합체로 바라보는 안목은 필수
-
C++
- 클래스를 쓰는 C( C with Classes )가 처음 이름
- 오늘날의 C++은 다중패러다임 프로그래밍 언어
- 절차적 프로그래밍, 객체지향, 함수식, 일반화 프로그래밍을 포함해서 메타 프로그래밍 개념까지 지원
- C++을 단일 언어로 바라보지 말고 여러 언어들의 연합체로 봐야함
-
C++의 하위 언어
- C : C++은 C를 기본으로 제공
- 객체 지향 개념의 C++ : 클래스를 쓰는 C에 관한것이 모두 해당 됨(객체 지향 설계의 대부분)
- 템플릿 C++ : C++의 일반화 프로그래밍 부분
- STL : 템플릿 라이브러리, container, iterator, algorithm, function object가 얽혀 돌아감
항목 2. #define을 쓰려거든 const, enum, inline을 떠올리자.
-
#define AAA 1.653
- 컴파일러에게 넘어가기 전에 선행 처리자가 AAA는 밀어버리고 1.653만을 채택
- 컴파일 에러가 발생시 에러 메세지는 1.653로 표시됨 => 디버깅이 어려워짐
-
#define을 const 로 교체하자.
- #define을 사용하면 AAA를 사용한 수 만큼 사본이 생기지만, 상수로 쓰면 사본은 하나만 생기고 참조함.
-
교체할 때의 주의점
-
상수 포인터 정의
- const char * const authorName = "Scott Meyers";
- const std::string authorName("Scott Meyers");
-
클래스 멤버로 상수를 정의
- class GamePlayer{
- private:
- static const int NumTurns = 5;
- int scores[NumTurns];
- ...
- };
-
나열자 둔갑술
- class GamePlayer{
- private:
- enum { NumTurns = 5 };
- int scores[NumTurns];
- ...
- };
-
장점
- enum의 주소를 취하는 것이 불법임
- #define처럼 어떤 형태의 쓸데없는 메모리 할당도 저지르지 않음
-
-
#define의 오용사례
- // a와 b 중에 큰 것을 f에 넘겨 호출합니다.
- #define CALL_WITH_MAX( a,b) f((a)> ? (b) ? (a) : (b) )
- int a = 5, b = 0;
- CALL_WITH_MAX(++a, b); // a가 두 번 증가합니다.
- CALL_WITH_MAX(++a, b + 10); // a가 한 번 증가합니다.
-
해결 방법 : 인라인 함수에 대한 템플릿
- Template<typename T >
- inline void callWithMax( const T& a, const T& b )
- {
- F( a > b : a : b );
- }
- 동일 계열 함수군 : 동일한 타입의 두가지 인자를 받고 둘중에 큰 것을 F로 넘겨서 호출하는 구조
항목 3. 낌새만 보이면 const를 들이대 보자!
-
const 장점
- 외부 변경 불가능
- static으로 선언한 객체에도 const 가능
-
포인터 자체를 상수로, 포인터가 가리키는 데이터를 상수로 지정 가능, 둘다 지정, 둘자 미지정 가능
- char greeting[] = "Hello";
- char *p = greeting; // 비상수 포인터, 비상수 데이터
- const char *p = greeting; // 비상수 포인터, 상수 데이터
- char * const p = greeting; // 상수 포인터, 비상수 데이터
- const char * const p = greeting; // 상수 포인터, 상수 데이터
- *의 왼쪽에 const가 존재 : 포인터가 가리키는 대상이 상수
- *의 오른쪽에 const가 존재 : 포인터 자체가 상수
-
받아들이는 매개변수 타입은 모두 똑같음
- void f1(const Widget *pw); // f1은 상수 Widget 객체에 대한 포인터를 매개변수로 취함
- void f2(Widget const *pw); // f2도 동일
-
STL에서 iterator가 가리키는 대상 자체의 변경을 허용하지 않으려면 const_iterator 사용
- std::vector<int> vec;
- const std::vector<int>::iterator iter = vec.begin();
- *iter = 10; // OK
- ++iter; // 에러, iter는 상수
- std::vector<int>::const_iterator cIter = vec.begin();
- *cIter = 10; // 에러, *cIter는 상수
- ++cIter; // OK
-
함수의 반환값을 const로
- class Rational {...};
- const Rational operator* (const Rational& lhs, const Rational& rhs);
- Rational a,b,c;
- (a * b) = c; // a*b의 결과(const)에 대고 operator=을 호출하다니;;;
-
상수 멤버 함수
- const 키워드의 있고 없고 차이만으로 오버로딩이 가능
- class TextBlock {
- public:
- const char& operator[] (std::size_t position) const // 상수 객체에 대한 operator[]
- { return text[position]; }
- char & operator[] (std::size_t position) // 비상수 객체에 대한 operator[]
- { return text[position]; }
- private:
- std::string text;
- };
- TextBlock tb("Hello");
- std::cout << tb[0]; // TextBlock::operator[]의 비상수 멤버를 호출합니다.
- const TextBlock ctb("World");
- std::cout << ctb[0]; // TextBlock::operator[]의 상수 멤버를 호출합니다.
- void print(const TextBlock& ctb) // 이 함수에서 ctb는 상수 객체로 쓰입니다.
- {
- std::cout << ctb[0]; // TextBlock::operator[]의 상수 멤버를 호출합니다.
- }
- std::cout << tb[0];
- tb[0] = 'x';
- std::cout << ctb[0];
- ctb[0] = 'x';
- operator[]가 char &를 반환하는 이유는 값에 의한 반환이 되면 사본이 리턴되며 값에 대입연산자를 수행하면 컴파일 에러가 나기 때문
-
비트수준 상수성(물리적 상수성)
- 어떤 멤버 함수가 그 객체의 어떤 데이터 멤버도 건드리지 않아야(정적 멤버는 제외) 그 멤버 함수가 'const'임을 인정
- class CTextBlock {
- public:
- char& operator[] (std::size_t position) const // 부적절한(그러나 비트수준 상수성이 있어서 허용되는) operator[]의 선언
- { return pText[position]; }
- private:
- char *pText;
- };
- const CTextBlock cctb("Hello"); // 상수 객체를 선언합니다.
- char *pc = &cctb[0]; // 상수 버전의 operator[]를 호출하여 cctb의 내부 데이터에 대한 포인터를 얻습니다.
- *pc = 'J'; // cctb는 이제 "Jello"라는 값을 갖습니다.
-
논리적 상수성
- 상수 멤버 함수라고 해서 객체의 한 비트도 수정할 수 없는 것이 아니라 일부 몇 비트 정도는 바꿀 수 있되, 그것을 사용자측에서 알아채지 못하게만 하면 상수 멤버 자격이 있다.
- class CTextBlock {
- public:
- std::size_t length() const;
- private:
- char *pText;
- std::size_t textlength; // 바로 직전에 계산한 텍스트 길이
- bool lengthIsValid; // 이 길이가 현재 유효한가?
- };
- std::size_t CTextBlock::length() const
- {
-
if (!lengthIsValid) {
- textLength = std::strlen(pText); // 에러! 상수 멤버 함수 안에서는 textLength
- // 및 lengthIsValid에 대입할 수 없음
- lengthIsValid = true;
- }
- return textLength;
- }
- class CTextBlock {
- public:
- std::size_t length() const;
- private:
- char *pText;
- mutable std::size_t textlength; // 이 데이터 멤버들은 어떤 순간에도 수정가능
- mutable bool lengthIsValid; // 심지어 상수 멤버 함수 안에서도 수정가능
- };
- std::size_t CTextBlock::length() const
-
{
- if (!lengthIsValid) {
- textLength = std::strlen(pText); // 문제없음
- lengthIsValid = true; // 문제없음
- }
- return textlength;
- }
-
상수 멤버 및 비상수 멤버 함수에서 코드 중복 현상을 피하는 방법
-
코드 판박이 괴물
- 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;
- };
- 우리가 원하는 것 : operator[]의 핵심 기능을 한 번만 구현해 두고 이것을 두 번 사용하고 싶음
-
결론 : 캐스팅이 필요하긴 하지만, 안전성도 유지하면서 코드 중복을 피하는 방법
-
비상수 operator[]가 상수 버전을 호출하도록 구현하는 것
- class TextBlock {
- public:
- const char& operator[] ( std::size_t position) const // 이전과 동일
- {
- return text[position];
- }
- char& operator[] (std::size_t position) // 상수 버전 op[]를 호출하고 끝
- {
- // op[]의 반환 타입에 캐스팅을 적용, const를 떼어냅니다.
- // *this의 타입에 const를 붙입니다.
- // op[]의 상수 버전을 호출합니다.
- return const_cast<char&>( static_cast<const TextBlock&>(*this)[position] );
- }
- };
- 상수 멤버와 비상수 멤버가 기능적으로 똑같게 구현되어 있을 경우, 코드 중복을 피하기 위해 비상수 버전이 상수 버전을 호출하게 만든다.
-
-
항목 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자
-
C++ 의 객체 초기화 규칙
- C 부분만 쓰면 값이 초기화된다는 보장이 없음.
- STL의 vector는 초기화가 보장됨
-
best한 방법 : 모든 객체를 사용하기 전에 항상 초기화
- int x = 0; // int의 직접 초기화
- const char * text = "A C-style string"; // 포인터의 직접 초기화
- double d; .. 입력 스트림에서 읽음으로써 "초기화" 수행
- std::cin >> d;
- C++ 생성자에서 지킬 규칙 : 그 객체의 모든 것을 초기화하자!
-
대입과 초기화의 차이
- class PhoneNumber {...};
- class ABEntry { // ABEntry = "Address Book Entry"
-
public:
- ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones);
-
private:
- std::string theName;
- std::string theAddress;
- std::list<PhoneNumber> thePhones;
- int numTimesConsulted;
- };
- ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
-
{
- theName = name; // 지금은 모두 대입을 하고 있음, 초기화가 아님
- theAddress = address;
- thePhones = phones;
- numTimesConsulted = 0;
- }
- theName, theAddress, thePhone은 디폴트 생성자에서 이미 초기화됨
- 기본 제공 타입인 numTimesConsulted은 초기화 되었다는 보장이 없음
- ABEntry::ABEntry(const std::string& name, const std::string& address, const std::list<PhoneNumber>& phones)
-
:
- theName(name), // 이제 이들은 모두 초기화되고 있음
- theAddress(address),
- thePhones(phones),
- numTimesConsulted(0)
- { } // 생성자 본문엔 이제 아무것도 들어가 있지 않음
- 기본 생성자 호출 후에 복사 대입 연산자를 연달아 호출하는 것보다 복사 생성자를 한 번 호출하는 쪽이 더 효율적임
-
데이터 멤버를 기본 생성자로 초기화
- ABEntry::ABEntry()
- : theName(),
- theAddress(),
- thePhones(),
- numTimesConsulted(0)
- {}
- 상수와 참조자는 대입이 불가능 하기 때문에 반드시 멤버 초기화 리스트를 사용해야함
-
객체를 구성하는 데이터의 초기화 순서
- 기본 클래스는 파생 클래스보다 먼저 초기화
- 클래스 데이터 멤버는 그들이 선언된 순서대로 초기화
-
비지역 정적 객체의 초기화 순서는 개별 번역 단위에서 정해진다.
-
정적 객체 : 자신이 생성된 시점부터 프로그램이 끝날 때까지 살아 있는 객체
- 지역 정적 객체 : 함수 안에서 static으로 선언된 객체
- 비지역 정적 객체 : 전역 객체, 네임스페이스 유효범위에서 정의된 객체, 클래스 안에서 static으로 선언된 객체, 파일 유효범위에서 static으로 정의된 객체
- 번역 단위 : 그 파일이 #include 하는 파일들까지 합친 소스파일 하나
- 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);
-
- 해결 방법 : 비지역 정적 객체를 맡는 함수를 준비해서 이 안에 각 개체를 넣음. 함수는 정적객체의 참조자를 반환
-
비지역 정적 객체가 지역 정적 객체가 됨(Singleton 패턴)
- class FileSystem {...};
- FileSystem& tfs()
-
{
- static FileSystem fs;
- return fs;
- }
- class Directory {...};
- Directory::Directory(params)
-
{
- std::size_t disks = tfs().numDisks();
- }
- Directory& tempDir()
-
{
- static Directory td;
- return td;
- }
- Directory tempDir(params);
- 참조자 반환 함수는 정적 객체를 쓰기 때문에 다중 스레드 시스템에서는 문제가 생길 수 있음
-
정리
- 기본 제공 타입은 직접 손으로 초기화
- 생성자에서는 본문에 넣지 않고 멤버 초기화 리스트 사용
- 여러 번역 단위에 있는 비지역 정적 객체들의 초기화 순서 문제를 피해서 설계
History
Last edited on 10/25/2007 18:20 by 켈리
Comments (0)