스콧 마이어스 형님의 Effective c++ 항목1~27까지 읽고 개인적으로 이해한 내용을 키워드를 중심으로 정리한 내용입니다 :)
C++의 효과적인 방법
책의 구성 = 일반적인 설계전략 + C++만의 언어적 특징에 관련된 실전 세부사항
설계이슈 > 접근들 > "선택방법"
"선택방법" < 결정을 Effective 하게 하는 부분들을 알려준다.
공부방법 : 설명 + 항목(제목과 설명) + 숨겨진 근본원리 => 안목이 올라간다.
항목 1 : C++ 언어들의 연합체로 바라보는 안목은 필수
C++ : Multi Paradugm Progrmming Language (다중 패러다임 프로그래밍 언어)
Procedual 기반 -> Obejct Oriented, Functional, Generic, Meta Programming
여러 언어들의 "연합체" : 'C' + 'OOP CPP' + 'Template' + 'STL'
항목 2 : #define을 쓰려거든 const, enum, inline을 떠올리자
#define보다는 상수포인터, 클래스 멤버로 상수 정의하자.
"메크로 함수의 오용"과 "디버깅"을 잡기힘든 경우 등등 문제가 많다.
그래서 const or "enum hack(나열자 둔갑술)" 로 메크로 대체
"inline 함수 템플릿"
inline자체가 #define장단점 보완할려고 쓰인다.
항목 3 : 낌새만 보이면 const를 들이대보자
함수의 반환값이 const인경우? => (a * b) = c; 와 같은 어처구니 없는 실수 방지
기본 제공 타입과의 쓸데없는 비호환성 방지
상수 멤버함수의 const 유무는 "함수 오버로딩"으로 이어짐.
"비트 수준 상수성(물리적 상수성)", "논리적 상수성"
- 비트 수준 상수성 : 어떤 객체 멤버도 건드리면 안되었을 때 const로 인정
- 논리적 상수성 : 상수객체를 초기화 > 상수 멤버함수 > 포인터로 값변경시 발생하는 "황당한" 상황의 "보완"으로 논리적 상수성이 등장 => mutable
상수멤버 및 비상수 멤버함수에서 코드 중복 피하기 : 1. 비상수 멤버에서 static_cast<const char&> 2. const_cast<char&>
반대로 상수멤버 > 비상수 멤버 함수로는 안됨(이유는 상수멤버 함수인데 변경할 여지가있는 비상수 멤버 함수 호출? 안된다)
항목 4 : 객체를 사용하기 전에 반드시 그 객체를 초기화 하자!
초기화 X => "미정의 동작" 높아진다.
초기화 규칙? => 있는데 "조금"복잡
생성자 이니셜라이저 1. 사용, 부모 => 자식 2. 선언된 순서대로 초기화 진행
"비지역 정적 객체 초기화 순서는 개별 번역단위"에서 이루어 진다.
지역정적 객체, 비지역 정적 객체
"번역 단위" :
비지역 정적 객체 => 무엇이 먼저 초기화 되는지 알 수 없다 => "지역정적 객체"로 변경한다 => "Single Tone"
(C++는 .cpp파일끼리 참조를 하지 않기 때문에)
항목 5 : C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자
복사 생성자, 복사 대입 연산자, 소멸자 => 컴파일러가 public으로 자동으로 선언한다. => "배후의 코드"
복사 시리즈는 legal, resonable 해야한다.
복사시리즈를 막고 싶다면 private으로 선언하자.
항목 6 : 컴파일러가 만들어낸 함수가 필요없으면 확실히 이들의 사용을 금해 버리자
compiler가 생성한 모든 함수는 => public으로 선언된다.
부모의 복사시리즈를 private으로 선언하고 싶다면 =>
private으로 부모 클래스에 복사시리즈를 정의한다(90점)
+
부모 private에 CopyCtor(const Fcc&); Fcc& operator = (const Fcc&); 이런식으로 "필수가 아닌" 매개변수를 적어주지 않는다.(10점)
uncopyable 데이터가 없는 클래스를 상속 (가상 소멸자 신경 X) => "공백 기본 클래스 최적화"
부스트의 noncopyable 클래스 상속하는거 좋다.
항목 7 : 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자
TimeKeeper* getTimeKeeper(); => 미정의 동작 발생 높다 => 가상소멸자
가상함수 1개 이상이라도 있다면 => vftable생성 => 이것은 포인터인데 실행환경 32bit, x86(64bit)에 따라 크기가 다르게 정해진다. (이식성)
STL 컨테이너는 가상 소멸자 없다. 상속받으려고 하지말자.
항목 8 : 예외가 소멸자를 떠나지 못하도록 붙들어 놓자
예외가 나오는 것을 소멸자에 그대로 놔두는 것 try (db.close()) {} catch (...) {}
방법 두가지 => 1. std::abort(); 2. catch(...) { 로그작성 }
두가지가 있지만 개선한 방법은 => 인터페이스를 조금더 "잘" 설계 bool값, Close()와 같은 함수를 public으로 선언
항목 9 : 객체 생성 및 소멸 과정중에는 절대로 가상함수를 호출하지 말자
생성자에서 가상함수를 호출? => 자식 클래스 생성자 호출 하자마자 => 부모 클래스 생성자 호출(아직 까지는 자식 클래스 객체로 인식하지 못한다) => 부모 생성자 호출이 완료되고 나면은 자식클래스 생성자를 완료하려 내려감 => 이때 자식 클래스 객체라고 인식하게 된다.
부모 클래스 생성자에서 가상함수 호출시 "절대로" 파생클래스로 안 내려감.(생성자 호출 동안에)
생성자에서 => 비가상함수(가상함수를 호출하는 내용) => 더 안된다(컴파일은 됨) => 사용자는 행복하지 않다 => 대처 방법 : "비가상 멤버함수로 변경하자"
std::string을 반환하는 정적 멤버 변수 createLogString을 생성자의 인자로 넘겨줌(도우미)
=> 안정성이 높아진다. 초기화되지 않은 데이터를 건드려서 "미정의 동작"이 발생하는 확률을 줄여준다)
항목 10 : 대입 연산자는 *this 참조자를 반환하자
"우측 연관(right-associate)"
대입 연산의 특성.
"좌변 참조자를 반환하자"라는 규약 => "관례"이다.
항목 11 : operator = 에서는 자기대입에 대한 처리가 빠지지 않도록 하자
self-assignment => 눈에 잘 띄지 않는다. => 많이 하지는 않지만 가능성이 있고 발생할 경우 문제가 된다.
전통적인 방법 : "일치성 검사" ( 비용이 들어간다 )
따라서, 예외 안정성에만 집중
1. 포인터가 가르키는 객체 복사이후 => 삭제
2. 복사후 맞바꾸기
항목 12 : 객체의 모든 부분을 빠짐없이 복사하자
클래스 데이터 멤버 추가 => 복사함수 다시 작성
상속에서 해당 클래스멤버 전부다 복사, 클래스가 상속한 복사함수도 꼬박꼬박 호출 하자.
항목 13 : 자원 관리에는 객체가 그만!
caller가 자원해제 해야한다. 하지만 "도중 하차"문제가 있다. (continue, goto, etc...)
항상 자원이 delete되게 할려면은 "소멸자"에서 자원을 해제해야한다.
1. RAAI ( RAAI 클래스 : auto_ptr, shared_ptr )
2. 소멸자에서 확실히 해제
항목 14 : 자원 관리 클래스의 복사동작에 대해 진지하게 고찰하자
Mutex, C API
초반에 Leak 클래스 객체생성=> 임계영역 지정 => 아름다워 보이지만, "복사"된다면?
1. 복사금지(noncopyable, 항목6)
2. 자원에 대해 참조 카운팅 방식 사용(shared_ptr), deleter ( 스마트 포인터의 두번째 매개변수에 선택적으로 사용)
항목 15 : 자원 관리 클래스에서 관리되는 자원은 외부에서 접근할 수 있도록 하자
자원 관리 클래스? : RAAI 클래스 auto_ptr, shared_ptr
자원 관리 클래스의 실제 자원이 필요한 경우가 있다.
1. 명시적 변환( .get() )
2. 암시적 변환 ( operator FontHandle() const {} )
암시적 변환보다는 명시적 변환이 좋을 때가 많지만 암시적 변환은 때때로 사용하기 쉽게 해주고 융통성이 높을 때가있다.
실제 자원접근 API 많다. 따라서 RAAI 클래스 자원접근 open 해주고 명시적/암시적을 상황에 맞게 사용하도록 하자.
항목 16 : new 및 delete 사용할 때는 형태를 맞추자
new string[] => delete[] 쌍을 맞추도록 하자.
항목 17 : new로 생성산 객체를 스마트 포인터에 저장하는 코드는 별도의 한 문장으로 만들자
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
a. new Widget
b. priority()
c. std::tr1::shared_ptr<Widget>(new Widget)
a, b, c의 연산/호출 순서가 누가 먼저 오게되는지 확답을 못한다.
a => "예외" => b => c => 메모리 누수 발생할 수 있다.
따라서 shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());
로 별도의 한문장으로 만들자.
항목 18 : 인터페이스 설계는 제대로 쓰기엔 쉽게 엉터리로 쓰기엔 어렵게 하자
C++ Interface => function, class, template ...
비지역 정적객체 문제점 => 반환을 스마트 포인터로 + 생성 시점에서 "함수자" 사용
shared_ptr => 교차 DLL 문제 해결
항목 19 : 클래스 설계는 타입 설계와 똑같이 취급하자
클래스 정의 == 새로운 타입 정의
고려해야할 부분들이 많다. p147~p149
객체 생성/소멸, 초기화/대입?, 제약?, 상속?
항목 20 : 값에 의한 전달보다 상수 객체 참조자에 의한 전달 방식을 택하는게 낫다
복사 생성자 => pass-by-value => 복사생성자 호출이 많아진다.
따라서 상수객체에 대한 참조자로 전달을 하자. => "복사 손실 문제(splicing problem) 싹둑 잘림"
참조자 = 포인터를 사용해서 구현되어있음. 참조자 전달 == 포인터 전달
하지만 기본타입, STL 반복자, 함수객체 들은 pass-by-value가 적절하다.
항목 21 : 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려 하지 말자
"참조자" = 그냥 이름이다. 존재하는 객체에 붙는 다른 이름이다.
operaetor * => 참조자 반환?? 이상하고 생각해봐야한다. => 참조자는 반드시 이미 존재하는 객체이여야 하기 때문에.
지역 객체 참조 반환?? => "핵폭탄"이다. (메모리에서 없어지는데 반환한다?)
정적 객체 참조 반환?? => if ( (a*b) == (c*d) ) 가 항상 true이다. true로 안나오는게 이상하다.
따라서 새로운 객체 반환 => 정당한 비용을 지불하자.
항목 22 : 데이터 멤버가 선언될 곳은 private 영역임을 명시
캡슐화 : 간편해진다. 클래스 불변속성, 사전/사후 조건, 스레딩 환경 동기화 장점이 있다.
캡슐화 되지 않았다? => 바꿀 수 없다.
바뀐다? => 캡슐화 반비례로 낮아진다.
public, protected는 50보 100보이다.
항목 23 : 멤버함수보다는 비멤버 비프렌드 함수와 더 가까워지자
멤버함수가 더 낫다? => 틀렸다.
비멤버 함수 => 패키징 유연성 올라간다.
캡슐화가 높다 => 밖에서 볼 수 있는것이 줄어든다 == 밖에서 볼 수 있는것이 줄어든다 => 유연성이 높아진다.
캡슐화가 높다 => 변경 여유가 높아진다.
namespace안에 비멤버 함수 구현 => 컴파일 의존성이 떨어진다 (긍정적이다) => C++라이브러리가 이런식으로 구현
#include <vevtor>
항목 24 : 타입변환이 모든 매개변수에 대해 적용되어야만 한다면 비멤버 함수를 선언하자
class Rational
{
public:
const Rational operator * (const Rational& rhs) const;
};
A : result = oneHalf * 2;
B : result = 2 * oneHalf;
A의 경우 2가 Rational temp(2)로 임시객체가 만들어지는 "암시적 변환"이 된다.
반면 B의 경우 정수형에 대한 Rational타입의 오버로딩이 안되어 있기 때문에 불가하다.
B의 경우 가능하게 하고싶다면 "비멤버 함수"를 만들어 준다.
항목 25 : 예외를 던지지 않는 swap에 대한 지원도 생각해보자 ( X )
(이부분 어렵고 이해가 제대로 되지는 않았습니다)
std::swap 포인터 하나만 바꾸어야하는 상황 발생한다면?
std::swap "애누리 없이 " 복사 시리즈 호출함.
기존의 std::swap으로 복사시리즈 다 호출하면서 바꾸면 비효율적.
따라서 T 타입에 대해 "특수화"를 진행한다.
std::swap에 template<> 으로 "완전 템플릿 특수화"를 진행
C++ std에서 "완전 템플릿 특수화"만 허용 => 나머지 정의해버리면 "미정의 동작"으로 이어진다.
1. swap으로 납득이 된다 => std::swap사용
2. 납득이 안 간다 => 자신만의 swap을 public으로 정의, namespace or class에 비멤버 swap 만들고 이곳에서 자신만의 swap호출. 만약에 새로운 클래스가 있다면 std::swap을 템플릿 특수화를 진행
3. std::swap 볼 수 있도록 using std::swap선언
항목 26 : 변수 정의는 늦출 수 있는데 까지 늦추는 근성을 발휘하자
string encrypted; 객체의 생성/소멸 비용지불해야한다.
로직이 끝나고 필요하다면 그때 std::string encrypted(password); 와 같은 식으로 정의와 동시에 초기화 => "장점"
"루프"에서는 for문 밖에 정의(A), for문 안에 정의(B)
효율은 당연히 A의 방법이 높지만 이해도와 유지보수가 A가 B보다 더 낮다.
항목 27 : 캐스팅은 절약, 또 절약! 잊지말자 ( X )
cast == "괴물"
C-style => 구형 스타일 캐스트이다.
"포인터 변위"
"요구 사항"(가상함수를 부모에서 정의시 자식에서 호출해야하는)
캐스팅이 땅긴다? => 안좋은 징조
dynamic_cast 느리다 => 최대한 피해라
static_cast<Window>(*this).onSize(); => 동작 제대로 안한다. Window(부모) 기본 클래스 부분에 대한 "사본" 생성 => 원본에 영향 못준다.
'CPP' 카테고리의 다른 글
[C++] 전달참조 (0) | 2023.07.31 |
---|---|
[C++] 오른값 참조 (0) | 2023.07.30 |
[C++] C++ 컴파일 의존성 (2) | 2023.05.28 |
[C++] vftable (0) | 2023.04.21 |
[C++] 복사 생성자와 임시객체 (0) | 2023.04.16 |
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!