1. 개요
C++에서 클래스를 설계하는 것은 새로운 타입을 만드는 것과 똑같습니다.
프로그래머는 클래스 설계자를 넘어 Type을 설계하는 막강한 힘을 가지고 있다는 것입니다.
언어 설계자가 기본 제공 타입을 만들때 만큼이나 큰 정성이 필요한 작업입니다
이번 항목은 클래스를 설계할때 깊게 고민해볼만 질문을 소개하는 항목입니다.
2. 질문
2-1. 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?
이 부분에 따라서 생성자, 소멸자의 설계가 바뀌게 되고 메모리 할당, 해제 함수의 설계에도 큰 영향을 미칩니다.
https://yeoul0714.tistory.com/35
[Effective C++] 항목 8: 예외가 소멸자를 떠나지 못하도록 붙들어 놓자
1. 여러개의 예외는 처리가 곤란하다.만약 이러한 클래스가 있고 벡터에서 10개의 Yeoul객체를 가지고 있다고 생각해보자.class Yeoul{public: ~Yeoul(){}};void DoSomething(){ std::vectorv;} 벡터 v가 소멸될때에
yeoul0714.tistory.com
2-2. 객체 초기화는 객체 대입과 어떻게 달라야 하는가?
생성자, 대입 연산자의 동작과 차이점을 결정하는 중요한 질문입니다.
초기화와 대입은 해당되는 함수 호출이 완전히 다르기에 헷갈리지 않는 것이 중요합니다.
https://yeoul0714.tistory.com/30
[Effective C++] 항목 4: 객체를 사용하기 전에 반드시 그 객체를 초기화하자
1. 초기화 여부를 파악하긴 난해하다.C++의 변수 초기화가 중구난방은 아니다. 초기화가 보장되는 경우와 아닌경우가 분명하게 존재한다. 그러나 그 조건을 전부 알고있기엔 너무나 복잡하다. 기
yeoul0714.tistory.com
2-3. 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?
어떤 타입에 대해 값에 의한 전달을 구현하는 것은 복사 생성자입니다.
이렇게 복사가 될때 어떠한 의미를 지니는지 명확히 해야합니다.
MyClass a;
MyClass b = a; // 이 복사가 어떤 의미인지?
이렇게 복사가 될때 참조 or 복사가 될 가능성이 있습니다.
// 값 의미론: std::vector
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = v1; // 완전한 복사, v1과 v2는 독립적
// 참조 의미론: std::shared_ptr
std::shared_ptr<int> p1(new int(42));
std::shared_ptr<int> p2 = p1; // 같은 int 객체를 가리킴
이처럼 새로운 클래스를 만들때 복사하는 부분에서 어떤식으로 작동할지 고민을 하는 과정이 필수적입니다.
2-4. 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가?
class Date {
private:
int month; // 1~12만 유효
int day; // 1~31만 유효 (월에 따라 다름)
int year; // 어떤 범위만 유효?
};
이러한 Date클래스가 있다과면 month, day, year은 유효한 범위가 분명 존재합니다.
이것 외에도 분수 클래스에서 분모가 0이 아니어야 한다던지 이진 탐색 트리는 항상 정렬된 상태여야 한다는
유효 조건이 있습니다.
이러한 유효한 조합을 클래스의 불변속성이라고 합니다.
불변속성에 따라서 생성자, 대입연산자, Setter등에서 처리해 주는 루틴이 바뀌게 됩니다.
https://yeoul0714.tistory.com/59
[Effective C++] 항목 18: 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자
1. 개요 우리는 C++로 개발을 하면서 정말 수많은 인터페이스를 설계하게 됩니다. 그리고 우리가 개발한 인터페이스가 올바르게 쓰여지길 바랍니다. 만약 올바르게 쓰여지지 않았다면 최소한의
yeoul0714.tistory.com
항목 18에서 이러한 부분을 제한하는 방법들을 봤을 것입니다.
클래스를 설계할 때는 이러한 부분을 충분히 고민해야 합니다.
2-5. 기존의 클래스 상속 계통망(inheritance graph)에 맞출 것인가?
만약 기존의 클래스를 상속받아서 만든다고 하면 기존 클래스의 제약을 많이 받게 됩니다.
멤버 함수가 virtual이냐 아니냐에 따라서 많은 부분이 갈립니다. (항목 34,36)
만약 새로 설계하는 클래스가 누군가에게 상속해줄 가능성에 따라서 virtual 함수 여부가 결정됩니다.
특히 소멸자가 그렇습니다.
https://yeoul0714.tistory.com/33
[Effective C++] 항목 7: 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자
1. 부모 클래스 소멸자를 virtual로 해야하는 이유우리는 기본 클래스의 소멸자는 반드시 virtual로 선언해주어야 합니다. 아래 예시를 통해서 그 이유를 알아보도록 합시다.// Effective C++ 항목 7: 다
yeoul0714.tistory.com
2-6. 어떤 종류의 타입 변환을 허용할 것인가?
새로 만든 클래스(타입)은 기존의 클래스(타입)들과 함께 어울려야 합니다.
그래서 타입 변환에 대해서 충분한 고려가 필요합니다
T1클래스를 T2로 암시적으로 변환하고 싶다면
T1 클래스에 변환함수를 넣거나
// Kilometers를 Miles로 변환하는 예시
class Kilometers {
private:
double value;
public:
Kilometers(double km) : value(km) {}
double getValue() const { return value; }
// T1(Kilometers)에 변환 함수 정의
operator Miles() const {
return Miles(value * 0.621371);
}
};
class Miles {
private:
double value;
public:
Miles(double mi) : value(mi) {}
double getValue() const { return value; }
};
// 사용 예시
void main() {
Kilometers seoul_busan(325);
Miles distance = seoul_busan; // 암시적 변환: Kilometers::operator Miles() 호출
std::cout << "서울-부산 거리: " << distance.getValue() << " 마일" << std::endl;
// 출력: 서울-부산 거리: 201.946 마일
}
인자 하나로 호출되는 비명시호출 생성자를 T2에 넣어야 합니다.
class Celsius {
private:
double temperature;
public:
Celsius(double temp) : temperature(temp) {}
double getTemperature() const { return temperature; }
};
class Fahrenheit {
private:
double temperature;
public:
// T2(Fahrenheit)에 비명시호출 생성자 정의
Fahrenheit(const Celsius& celsius) {
temperature = celsius.getTemperature() * 9/5.0 + 32;
}
Fahrenheit(double temp) : temperature(temp) {}
double getTemperature() const { return temperature; }
};
// 사용 예시
void main() {
Celsius seoul_summer(30);
Fahrenheit hot_day = seoul_summer; // 암시적 변환: Fahrenheit::Fahrenheit(const Celsius&) 호출
std::cout << "서울 여름 기온: " << hot_day.getTemperature() << "°F" << std::endl;
// 출력: 서울 여름 기온: 86°F
}
만약 명시적 타입 변환만 허용하고 싶다면 해당 변환을 맡는 별도의 함수를 만들고 타입 변환 연산자와
비명시호출 생성자는 만들지 말아야 합니다.
class Radians {
private:
double angle;
public:
Radians(double rad) : angle(rad) {}
double getAngle() const { return angle; }
// 변환 함수 대신 일반 멤버 함수 사용
Degrees toDegrees() const {
return Degrees(angle * 180.0 / 3.14159);
}
// 변환 연산자는 정의하지 않음!
// operator Degrees() const { ... } // 이런 코드는 없음!
};
class Degrees {
private:
double angle;
public:
Degrees(double deg) : angle(deg) {}
double getAngle() const { return angle; }
// 생성자에 explicit 키워드 추가 (비명시호출 생성자 방지)
explicit Degrees(const Radians& rad) {
angle = rad.getAngle() * 180.0 / 3.14159;
}
};
// 사용 예시
void main() {
Radians angle_rad(1.5708);
// Degrees angle_deg = angle_rad; // 컴파일 오류! 암시적 변환 불가
// 명시적 변환 방법:
Degrees angle_deg1 = angle_rad.toDegrees(); // 방법 1: 변환 함수 호출
Degrees angle_deg2 = Degrees(angle_rad); // 방법 2: 명시적 생성자 호출
std::cout << "각도: " << angle_deg1.getAngle() << "°" << std::endl;
// 출력: 각도: 90°
}
2-7. 어떤 연산자와 함수를 두어야 의미가 있을까?
우리가 클래스안에 선언할 함수들이 이 고민을 통해서 결정될 것입니다.
어떤 것들은 멤버 함수로 적당하고 아닌 것들도 고려해야합니다.
(23,24,46)
2-8. 표준 함수들 중 어떤 것을 허용하지 말 것인가?
private로 선언해야 하는 함수가 여기에 해당됩니다.
https://yeoul0714.tistory.com/32
[Effective C++] 항목 6: 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자
1. 개요블로그의 주인장인 여울은 게임을 사랑하는 개발자이다. 여울은 게임을 소개하는 프로그램을 만들었데 이 프로그램에는 게임을 나타내는 클래스가 있다.class Game {}; 그런데 이세상에 똑
yeoul0714.tistory.com
2-9. 새로운 타입의 멤버에 대한 접근권한을 어느 쪽에 줄 것인가?
멤버들을 private, protected, public어디에 둘지 결정하는데 도움을 주는 고민입니다.
friend로 만들어야 할 클래스와 함수를 정할때도 도움이 되고
// Vector 클래스와 VectorIterator 클래스는 서로 밀접한 관계
class Vector {
private:
int* data;
size_t size;
public:
// Vector의 private 멤버에 VectorIterator가 접근할 수 있도록 허용
friend class VectorIterator;
};
class VectorIterator {
// 이 클래스는 Vector의 private 멤버에 접근 가능
};
중첩 클래스를 만들때도 도움이 됩니다.
class FileSystem {
public:
// File은 FileSystem에서만 의미가 있는 클래스
class File {
private:
std::string path;
int fileHandle;
public:
bool open();
void close();
};
File openFile(std::string path);
};
// 사용 예시
FileSystem fs;
FileSystem::File myFile = fs.openFile("data.txt");
- private: 클래스의 구현 세부사항 (대부분의 데이터 멤버)
- protected: 자식 클래스에게 제공할 기능
- public: 클래스의 공개 인터페이스
- friend: 특별히 신뢰하는 클래스/함수에게 private 접근 허용
- 중첩 클래스: 특정 클래스와 강하게 연관된 보조 클래스 정의
2-10. 선언되지 않은 인터페이스로 무엇을 둘 것인가?
우리가 만든 타입이 제공할 보장이 어떤 종류일까에 대한 질문입니다.
선언되지 않은 인터페이스란 클래스의 코드에 명시적으로 선언되지 않았지만
클래스가 암묵적으로 제공하는 약속이나 보장을 말합니다.
보장 가능한 부분은 수행 성능, 예외 안정성, 자원 사용입니다. (29)
보장하도록 결정한 결과는 클래스 구현에서 제약으로 작용합니다.
다시 말하면 코드 사용자에게 편의성을 제공하기 위해서 추가로 구현해야하는 부분이나 제약을 말합니다.
자원사용에 대해 예시를 들자면 shared_ptr을 써야하는 제약이 생기는 것이 예시입니다.
2-11. 새로 만드는 타입이 얼마나 일반적인가?
클래스를 만들 때는 특정 타입 하나가 아니라 여러 관련 타입들을 한꺼번에 다루는 설계를 생각해야 할 수도 있습니다.
다양한 타입에 동일한 동작을 제공하려면 구체적인 클래스 하나보다는 일반화된 템플릿을 만드는 것이 좋습니다.
따라서 설계 시 이 기능이 특정 타입에만 필요한가, 아니면 여러 타입에 필요한가?를 고려하여
템플릿 사용 여부를 결정해야 합니다.
언제 템플릿을 사용해야 하는가?
- 여러 데이터 타입에 동일한 로직 적용: 같은 알고리즘이 다른 타입에 적용될 때
- 코드 중복 감소: 비슷한 클래스를 여러 번 만드는 대신 하나의 템플릿으로
- 타입 안전성 유지: 컴파일 시간에 타입 검사
템플릿의 단점
- 컴파일 시간 증가: 템플릿 인스턴스화가 많을수록 느려짐
- 오류 메시지 복잡: 템플릿 관련 컴파일 오류는 이해하기 어려움
- 구현 복잡성: 모든 타입에 작동하도록 만들기가 더 어려움
2-12. 정말로 꼭 필요한 타입인가?
기존 클래스 기능이 아쉬워서 새로운 클래스를 만든다면 비멤버 함수나 템플릿을 추가 생성하는게 낫습니다.
// 이렇게 새 클래스를 만드는 대신
class EnhancedString : public std::string {
public:
bool containsNumber() const;
std::string removeSpaces() const;
};
// 사용
EnhancedString str = "Hello 123";
if (str.containsNumber()) { /*...*/ }
// 이렇게 비멤버 함수로 만들 수 있음
bool containsNumber(const std::string& str);
std::string removeSpaces(const std::string& str);
// 사용
std::string str = "Hello 123";
if (containsNumber(str)) { /*...*/ }
// 이렇게 여러 클래스를 만드는 대신
class IntPair {
int first, second;
public:
IntPair(int a, int b) : first(a), second(b) {}
int getMax() const { return first > second ? first : second; }
};
class DoublePair {
double first, second;
public:
DoublePair(double a, double b) : first(a), second(b) {}
double getMax() const { return first > second ? first : second; }
};
// 하나의 템플릿으로 해결
template <typename T>
class Pair {
T first, second;
public:
Pair(T a, T b) : first(a), second(b) {}
T getMax() const { return first > second ? first : second; }
};
// 사용
Pair<int> intPair(5, 10);
Pair<double> doublePair(3.14, 2.71);
3. 결론
이것만은 잊지 말자!
- 클래스 설계는 타입 설계입니다.
새로운 타입을 정의하기 전에, 이번 항목에 나온 모든 고려사항을 빠짐없이 점검해 보십시오.
책에는 짧게 나오고 넘어간 질문들이 하나하나 곱씹어보니 정말 깊게 생각해볼만한 질문들입니다.
다양한 예시를 보고 충분히 고민하지 않으면 책의 질문 자체를 이해하기 어려웠습니다.
책을 보며 조금씩 고민해봤지만 실제로 현업이나 프로젝트에서 클래스를 설계하게 된다면 깊은 고민이 필요한
유용한 질문들이 있던 유익한 항목이었습니다.
책에서 다른 항목에 대한 참조도 많이 언급을 하였는데 해당 부분까지 공부해본 뒤 다시 와서 읽어보면
감회가 새로울 것 같습니다. (2025/05/18)
https://yeoul0714.tistory.com/64
[Effective C++] 항목 20: '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이
1. 개요C++에서는 함수로부터 객체를 받거나 함수에 객체를 전달할때 값에 의한 전달(pass-by-value)를 사용하기도 합니다. 특별한 경우가 아니면 함수의 매개변수는 실제 인자의 '사본'을 받게 됩니
yeoul0714.tistory.com
'Effective C++ > Chapter 4: 설계 및 선언' 카테고리의 다른 글
[Effective C++] 항목 22: 데이터 멤버가 선언될 곳은 private 영역임을 명심하자 (0) | 2025.05.30 |
---|---|
[Effective C++] 항목 21: 함수에서 객체를 반환해야 할 경우에 참조자를 반환하려고 들지 말자 (0) | 2025.05.29 |
[Effective C++] 항목 20: '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달' 방식을 택하는 편이 대개 낫다 (0) | 2025.05.29 |
[Effective C++] 항목 18: 인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자 (0) | 2025.05.17 |