[C++] Effective C++ : Chapter 5 구현
Effective C++ : Chapter 5 구현
항목 26: 변수 정의를 늦출 수 있는 데까지 늦추는 근성을 발휘하자.
Q. 아래 코드에서 encrypted 변수를 어떻게 초기화하고 있을까요? 아래의 보기 중 옳은 것을 고르시오.
std::string encryptPassword(const std::string& password)
{
std::string encrypted(password);
}
1. string의 기본 생성자에 의해 만들어지고 password 값이 대입된다.
2. string의 복사 생성자에 의해 초기화된다.
답: 2번 ( 185~186쪽 )
불필요한 기본 생성자 호출이 일어나지 않고 변수의 의미가 명확한 상황에서 변수를 정의함과 동시에 복사 생성자에 의해 초기화가 이루어지고 있습니다.
Q. 다음 2개의 코드 중 어떤 생성자를 몇 번씩 호출하는지 추측하시오. (단, password 생성은 고려하지 않는다).
std::string encrypted;
std::string password("abc");
encrypted = password;
std::string password("abc");
std::string encrypted(password);
답: 기본생성자 1 대입연산자 1 , 복사생성자
Q. 아래의 2가지 방법의 비용이 각각 얼마나 드는지 '생성자, 연산자, 소멸자'와 연관지어서 설명하세요.
A.
widget w;
for (int i = 0; i < n; ++i)
{
// w = i에따라 달라지는 값;
}
B.
for (int i = 0; i < n; ++i)
{
// Widget W(i에 따라 달라지는 값);
}
A : 생성자 1번 + 소멸자 1번 + 대입 n번
B : 생성자 n번 + 소멸자 n번
위의 답을 보고 대부분의 상황에서 A방법이 더 좋다고 생각할 수 있습니다. 그러나 A방법을 쓰면 w라는 이름을 볼 수 있는 볌위가 B 방법보다 넓어지기 때문에 프로그램의 이해도와 유지보수성이 떨어질 수 있습니다. 그러니 아래의 두 상황에서는 B방법 채택하는게 좋습니다.
만약 대입에 들어가는 비용이 생성자-소멸자 쌍보다 적게 나오는 경우가 있다면 A가 더 효율적이겠지만, 그렇지 않은 경 우는 B 방식이 더 효율적입니다.
유효범위 측면에서도 한번 생각해 볼 필요가 있습니다. A의 경우는 루프를 포함하는 유효범위를 가지고 있어 프로그램의 이해도와 유지보수성이 좋지 않아질 수 있습니다.
항목 27: 캐스팅은 절약, 또 절약! 잊지 말자
Q. cast를 사용하면 안되는 이유가 무엇일까요?
책을 읽었을 때에는 비용 문제 인 것 같기도 하고, 너무 강력하기 때문에 그런것 같기도 하고,, 아리송 합니다 ! 같이 찾아보고 의논해보면 좋을 거 같아요!
dynamic_cast ⇒ 비용이 높기 때문에 (클래스의 이름 비교를 수행)
static_cast ⇒ 정보 손실, 런타임에 대한 정보를 알 수 없음
reinterpret_cast ⇒ 포인터 변환 문제?
- 클래스 주소로 비교하지 않고 이름으로 비교하는 이유는 ???
- class A ← class B ← class C
Q. static_cast이 위험한 cast일 수 있는 이유를 모두 말해보자
정보 손실
- 부동 소수점 값을 정수로 static_cast한다고 가정할 때, 소숫점 이하의 값이 손실될 수 있다.
포인터 형 변환(역참조)
- int 포인터를 double 포인터로 형 변환할 경우(역참조할 경우) 메모리 오류가 발생할 수 있음
가독성 저하
타입 안전성 감소
안전하지 않은 형 변환
- static_cast는 컴파일 시간에 캐스팅을 진행하기 때문에 런타임 중 변경된 타입이 유효한지 검사하지 않기 때문
- 만약 상위 클래스 포인터가 하위 클래스 객체를 가리키고 있지 않은 경우에 형 변환을 시도하면 런타임에 오류 발생
- 다중 상속의 경우 두 클래스 간의 상속 구조가 복잡해질 수 있다
- 예시코드
Q. 이 코드의 문제점은?
class Window{
public:
virtual void onResize() {...}
...
};
class Special Window :public Window{
public:
virtual void onResize(){
static_cast<Window>(*this).onResize();
}
};
가상 함수를 파생클래스에서 재정의해서 구현할 때 기본 클래스의 버전을 호출하는 문장을 가장 먼저 넣어달라는 요구사 항이 있을 때 케이스이다.
Window 객체로 타입캐스팅을 잘 해서 Window::onResize() 함수를 호출한 것으로 보이지만 실제로는 그렇지 않다. 타입 캐스팅이 될 때 *this 에 대한 사본이 임시적으로 만들어지게 되고 현재 객체가 아닌 사본 객체의 Window::onResize() 함수가 불리기 때문이다.
만약 Window::onResize() 가 객체를 수정하도록 만들어진 경우 현재 객체는 그 수정이 제대로 반영되지 않을 것이다.
수정한 코드
class Window {
public:
virtual void onResize() { }
};
class SpecialWindow :public Window {
public:
virtual void onResize() {
// static_cast<Window>(*this).onResize();
__super::onResize();
// 또는
Window::onResize();
}
};
타입 캐스팅을 하지 않고 Window::onResize() 함수를 부르도록 수정하여 해결이 가능하다.
항목 28: 내부에서 사용하는 객체에 대한 ‘핸들’을 반환하는 코드는 되도록 피하자
Q. 무효 참조 핸들에 대해 설명해보세요.
핸들이 있기는 하지만, 그 핸들을 따라갔을 때 실제 객체의 데이터가 없는 것을 의미합니다.
예를 들어, 핸들 불러왔는데 해당 객체가 이미 소멸됐을 때
danggling handler
Q. 핸들이란 무엇이고, 핸들을 사용할 때 생길 수 있는 문제점은 무엇일까요?
참고: 198~200쪽
다른 객체에 손을 댈 수 있게 하는 매개자로, C++에서 참조자, 포인터, 반복자가 이에 해당됩니다.
*자동차의 핸들처럼 나는 운전을 위해 필요한 엔진 어쩌구.. 이런게 뭔진 모르겠지만 아무튼 핸들만 알면 운전을 할 수 있다* 라고 멘토님께 설명을 들었었습니다!!
예를 들어 operator[] 연산자는 string이나 vector 등의 클래스에서 개개의 원소를 참조할 수 있게 만드는 용도로 제공되고 있는데, 실제로 이 연산자는 내부적으로 해당 컨테이너에 들어 있는 개개의 원소 데이터에 대한 참조자를 반환하는 식으로 (핸들을 반환하는 식으로) 동작합니다.
무효참조 핸들(dangling handle) 이라는 문제가 생길 수 있는데, 핸들이 있기는 하지만 그 핸들을 따라갔을 때 실제 객체의 데이터가 없는 것입니다. 이번 항목에서는 임시 객체 temp가 소멸되면서 나타나는 예시를 들어 설명하고 있습니다.
Q. 아래의 방법으로 객체를 반환받는다면 어떤 문제가 있을 수 있을까?
class Rectangle {...} // upperleft()함수 보유.
class GUIObject {...}
// Rectangle 객체를 생성하여 GUI객체의 사각 테두리 영역을 반환하는 함수
const Rectangle boundingBox(const GUIObject& obj);
GUIObject *pgo;
const Point *pUpperLeft = &(boundingBox(*pgo).upperleft());
예시의 문장이 끝날 때, 문장에서 임시로 생성된 Rectangle가 소멸합니다. 따라서 pUpperLeft가 가리키고 있는 객체는 없는 객체가 되어 무효참조 핸들(dangling Handle) 현상이 발생하게 됩니다.
Q. 아래의 밑줄 친 부분의 코드 다음줄에서 Rectangle의 임시 객체가 소멸되는것이 아닌가?
#include <iostream>
using namespace std;
class Point{
public:
Point() { X = 0; Y = 0; }
Point(int x, int y) : X(x),Y(y) { }
void SetX(int newVal) { X = newVal; }
void SetY(int newVal) { Y = newVal; }
int GetX() const { return X; }
int GetY() const { return Y; }
private:
int X;
int Y;
};
struct RectData{
public:
RectData(){ }
virtual ~RectData(){ }
Point ulhc; // 좌측 상단
Point lrhc; // 우측 상단
};
class Rectangle{
public:
Rectangle()
{
pData = make_shared<RectData>();
}
Rectangle(Point coord1, Point coord2)
{
pData = make_shared<RectData>();
pData->ulhc = coord1;
pData->lrhc = coord2;
}
virtual ~Rectangle() { }
private:
shared_ptr<RectData> pData;
public:
const Point& upperLeft() const { return pData->ulhc; }
const Point& lowerRight() const { return pData->lrhc; }
};
class GUIObject{
public:
GUIObject() { }
GUIObject(Point coord1, Point coord2)
{
rectangle = Rectangle(coord1, coord2);
}
virtual ~GUIObject() { }
const Rectangle& GetRectangle() const { return rectangle; }
private:
Rectangle rectangle;
};
const Rectangle boundingBox(const GUIObject& obj)
{
return obj.GetRectangle();
}
int main() {
Point coord1(50, 50);
Point coord2(100,100);
const Rectangle rec(coord1, coord2);
GUIObject* pgo1 = new GUIObject(coord1, coord2);
const Point* pUpperLeft = &(**boundingBox(*pgo1)**.upperLeft());
cout << pUpperLeft->GetX() << ' ' << pUpperLeft->GetY() << '\\n';
Rectangle temp = boundingBox(*pgo1);
const Point* pUpperLeft2 = &(temp.upperLeft());
bool bk = false;
delete pgo1;
return 0;
}
메모리 블록 중에 하나를 참조를 하고 있는데, 블록이 허용되었던 공간이었다가 허용되지 않은 공간이 되어서 문제가 생기는데, 바로 바로 사용을 할 때는 안보일 수도 있다.
문제가 생길 수 있는 여지가 있는 가능성이 크기 때문에, 사용하지 않는 것을 권장한다.
항목 29: 예외 안전성이 확보되는 그날 위해 싸우고 또 싸우자!
Q. 책에서 강력한 보장을 제공하기 위해서 사용한 방법 2가지는 무엇일까요?
Copy and Swap, 스마트 포인터
Q. 예외 안전성을 확보하기 위한 보장 3가지가 무엇이고 각각 어떤 보장을 뜻 할까요
- Basic Guarantee : 기본적인 보장어떤 객체나 자료구조도 더럽히지 않고, 모든 객체의 상태는 내부적으로 일관성을 유지하고 있다.
- 하지만, 프로그램의 상태가 어떤 지는 정확히 예측되지 않을 수 있다.
- 함수 동작 중 예외가 발생하면 실행 중인 프로그램에 관련된 모든 것을 유효한 상태로 유지하겠다는 보장이다.
- Strong Guarantee : 강력한 보장2가지 프로그램 상태만 존재한다. 예외가 발생하지 않으면 함수가 끝까지 동작하고, 예외가 발생하지 않으면 함수 호출이 없던 것처럼 되돌아 간다.
- 함수 동작 중 예외가 발생하면 프로그램의 상태를 절대로 변경하지 않겠다는 보장이다.
- Nothrow Guarantee : 예외 불가 보장기본제공 타인에 대한 모든 연산은 예외를 던지지 않게 되어 있어 예외불가 보장이 제공된다.
- 예외를 절대로 던지지 않겠다는 보장이다.
Q. copy and swap 에 대해 이야기를 나누어봅시다.
복사 후 맞바꾸기 방법은 객체의 데이터에 대한 사본을 만들어 놓고 그 사본을 변경한 후에 사본과 원본을 바꿔치기 작업을 예외를 던지지 않는 함수 내부에서 하자는 의미입니다.
효율 혹은 복잡성에서 생기는 비용이 있지만, 강력한 보장을 제공
항목 30: 인라인 함수는 미주알고주알 따져서 이해해 두자
Q. 메모리 공간이 적은 기계에서 인라인 함수를 남발할 시 문제점을 상의해보아요!
답
- 프로그램 크기가 그 기계에서 쓸 수 있는 메모리 공간을 넘어버릴 수 있다.
- 가상 메모리 환경에서 인라인 함수로 인해 부풀려진 코드는 성능의 걸림돌이 된다.
- 페이징 횟수가 늘어나 명령 캐시 적중률이 떨어진다.
- ⇒ 반대의 경우, inline 함수의 본문 길이가 매우 짧을 경우 목적코드의 크기도 작아지며 명령어 캐시 적중률이 높아짐.
Q. 인라인 함수의 제외 대상은 무엇일까요?
답
- 컴파일러가 보기에 복잡한 함수 (루프가 들어 있다거나 재귀 함수인 경우)
- 가상 함수 호출
- ⇒ “virtual” : 어떤 함수를 호출할 지 결정을 프로그램 실행 중에 한다. 의미 (즉, 컴파일 시간에 일어나는 인라인 함수와 의미가 상반되기 때문임)
- 함수 포인터를 통해 호출하는 경우
- 생성자 및 소멸자
- 객체 생성, 자동 초기화
- 객체 소멸
- 기본 클래스 부분과 객체의 데이터 멤버들이 자동으로 생성, 객체가 소멸될 대 반대 순서로 이루어짐
- ⇒ C++는 객체가 생성되고 소멸될 때 일어나는 일들에 대해 여러가지 보장을 제공해주기 때문임
Q. inline과 forceinline의 차이는 무엇일까?
답
- inline
- 컴파일러에게 함수를 인라인으로 확장하도록 요청하는 지시자입니다. (함수 호출을 실제 함수의 코드로 대체하도록 권장하는 것).
- forceinline
- forceinline은 컴파일러에게 함수를 반드시 인라인으로 확장하도록 지시하는 지시자.컴파일러는 함수 호출을 무조건 해당 함수의 코드로 대체해야 함.
- 공통된 특징
- 주로 짧은 코드 또는 자주 호출되는 함수에 사용됩니다.
- 함수가 너무 복잡하거나 크기가 너무 클 경우에는 인라인을 수행하지 않을 수 있음.
항목 31: 파일 사이의 컴파일 의존성을 최대로 줄이자
Q. 파일 사이의 컴파일 의존성을 최소화하는 방법에 대해 설명해보세요.
'정의부에 대한 의존성'을 '선언부에 대한 의존성'으로 바꿀 수 있다면 바꿉니다.
- 객체 참조자 똔는 포인터로 충분한 경우에는 객체를 직접 쓰지 않습니다.
- 어떤 타입에 대한 참조자 및 포인터를 정의할때는 그 타입의 선언부만 필요합니다. 반면, 어떤 타입의 객체를 정의할 때는 그 타입의 정의가 준비되어 있어야 합니다.
- 가능하다면 클래스 정의 대신 클래스 선언에 최대한 의존하도록 합니다.
- 함수 선언이 되어 있는 (라이브러리의) 헤더 파일 쪽에 그 부담을 주지 않고 실제 함수 호출이 일어나는 사용자의 소스 파일 쪽에 전가한는 방법을 사용합니다.
- 선언부와 정의부에 대해 별도의 헤더 파일을 제공합니다. 핸들클래스
- 선언부를 위한 헤더 파일, 정의부를 위한 헤더 파일 2개를 만들어 사용합니다. 이 때 두 파일은 관리도 짝 단위로 해야 합니다.
Q. 팩토리 함수(혹은 가상 생성자)에 대해 설명해보세요.
답.
팩토리 함수(Factory Function)는 객체를 생성하는 함수입니다. 일반적으로 객체 생성에 필요한 복잡한 로직이나 초기화를 감추는 데 사용됩니다. 주로 객체 생성을 담당하는 정적 메서드(static method)나 전역 함수로 구현됩니다.
인터페이스 클래스를 사용하기 위해서는 객체 생성 수단이 최소한 하나는 있어야 합니다. 이 때, 파생 클래스의 생성자 역할을 대신하는 팩토리 함수를 만들어 놓고 이 팩토리 함수를 호출하여 객체를 생성합니다.
주어진 인터페이스 클래스의 인터페이스를 지원하는 객체를 동적으로 할당한 후, 그 객체의 포인터를 반환합니다.
팩토리 함수의 장점은 다음과 같습니다:
- 객체 생성의 추상화: 팩토리 함수를 사용하면 클라이언트 코드에서 객체를 생성하는 방식을 추상화할 수 있습니다. 클라이언트 코드는 어떤 클래스의 객체를 생성하는지에 대해 몰라도 됩니다.
- 유연한 객체 생성: 팩토리 함수를 사용하면 객체를 생성할 때 다양한 옵션을 고려할 수 있습니다. 예를 들어, 생성될 객체의 유형이 실행 시간에 결정되거나, 여러 가지 초기화 옵션이 있을 때 유용합니다.
- 객체 생성 과정의 중앙 집중화: 팩토리 함수를 사용하면 객체 생성에 필요한 모든 코드를 중앙 집중화할 수 있습니다. 이로 인해 코드 중복이 줄어들고 유지보수가 쉬워집니다.
예를 들어, 게임에서 여러 유형의 캐릭터를 생성하는 경우, 각 캐릭터 유형에 대한 팩토리 함수를 만들어 사용할 수 있습니다. 클라이언트 코드는 캐릭터를 생성하기 위해 해당 팩토리 함수를 호출하면 됩니다. 이렇게 하면 클라이언트 코드가 각 캐릭터의 생성 방법에 대해 알 필요가 없어지고, 새로운 캐릭터 유형을 추가하거나 기존 캐릭터의 생성 방법을 변경하는 것이 쉬워집니다.
팩토리 메서드 패턴, 팩토리 패턴
https://jeonyeohun.tistory.com/385#:~:text=지금까지 정리한 내용,하게 하는 디자인 패턴이었습니다.
Q. 컴파일 의존성을 줄이는 방법 2가지를 말하고, 간단하게 설명해보아요
1. 핸들 클래스
- 핸들 클래스는 클래스 구현부와 선언부를 분리하는 방법입니다.
- 실제 필요한 구현을 분리함으로써 사용자는 내부에 대해 알지 않아도 되고, 컴파일 의존성을 떨어트려 놓을 수 있습니다.
2. 인터페이스 클래스
- 인터페이스 클래스는 순수 가상함수로 이루어진 클래스 입니다.
- 모두 가상함수로 이루어져있기 때문에, 가상함수테이블에 대한 비용이 든다는 단점을 가지고 있지만 컴파일 의존성을 내려놓을 수 있습니다.
Q. string 타입은 클래스 일까요?
X: 아니다. typedef으로 정의된 타입동의어이다.
O: 맞다. 클래스이다.
String은 클래스가 아니다. typedef로 정의된 타입동의어입니다. 그렇기때문에, 전방선언도 불가능합니다.
basic_string<char>를 typedef한 것입니다.
Q. 핸들 클래스와 인터페이스 클래스 사용시의 장단점은 무엇일까요?
((228쪽 참고))
[ 장점 ]
구현부로부터 인터페이스를 떼어 놓을 수 있어서 파일 간 컴파일 의존성을 완화시킨다.
[ 단점 ]
핸들 클래스의 단점
- 멤버 함수를 호출하면 구현부 객체의 데이터까지 가기 위해 포인터를 타야 한다. (=간접화 연산이 한 단계 증가된다)
- 객체 하나를 저장하는데 필요한 메모리 크기 + 구현부 포인터의 크기가 더해진다.
- 구현부 포인터가 동적 할당된 구현부 객체를 가리키도록 (핸들 클래스의 생성자 안에서) 포인터 초기화가 일어나야 한다. 즉, 동적 메모리 할당과 해제에 따르는 연산 오버헤드가 발생한다.
인터페이스 클래스의 단점
- 호출되는 함수가 가상 함수라서 함수 호출 시 가상 테이블 점프에 따르는 비용이 소모된다.
핸들, 인터페이스 모두의 단점
- 핸들 클래스와 인터페이스 클래스 모두 함수 본문과 구현부를 분리하는 설계라서, 인라인 함수 호출이 불가하다(?)
< 인터페이스를 쓸 때 주의사항 >
- C++은 C#/Java와 달리 시스템 상 인터페이스는 없고 개발자가 사용하는 방법입니다.
- 멤버변수를 선언하면 안 됩니다. 멤버변수를 선언하면 추상클래스가 되버려서 인터페이스 기능의 상실합니다.
※ 참고) 순수 가상 함수를 포함한 클래스를 인스턴스로 만들 수 없다. 그러므로 추상클래스와 인터페이스를 인스턴스로 만들 수 없다.
'⭐ Programming > Effective C++' 카테고리의 다른 글
[C++] Effective C++ : Chapter 7 템플릿과 일반화 프로그래밍 (0) | 2024.05.27 |
---|---|
[C++] Effective C++ : Chapter 6 상속, 그리고 객체 지향 설계 (0) | 2024.05.07 |
[C++] Effective C++ : Chapter 4 설계 및 선언 (0) | 2024.04.17 |
[C++] Effective C++ : Chapter 3 자원관리 (0) | 2024.04.13 |
[C++] Effective C++ : Chapter 2 생성자, 소멸자 및 대입 연산자 (1) | 2024.04.10 |
댓글
이 글 공유하기
다른 글
-
[C++] Effective C++ : Chapter 7 템플릿과 일반화 프로그래밍
[C++] Effective C++ : Chapter 7 템플릿과 일반화 프로그래밍
2024.05.27 -
[C++] Effective C++ : Chapter 6 상속, 그리고 객체 지향 설계
[C++] Effective C++ : Chapter 6 상속, 그리고 객체 지향 설계
2024.05.07 -
[C++] Effective C++ : Chapter 4 설계 및 선언
[C++] Effective C++ : Chapter 4 설계 및 선언
2024.04.17 -
[C++] Effective C++ : Chapter 3 자원관리
[C++] Effective C++ : Chapter 3 자원관리
2024.04.13