All
[Effective C++] 14. 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자
[Effective C++] 14. 자원 관리 클래스의 복사 동작에 대해 진지하게 고찰하자
2022.05.22항목 13에서 힙 기반 자원을 해제하는 법을 알아봤는데, 힙에 할당되지 않는 자원이라면 우리는 스스로 자원 관리 클래스를 만들어야 한다. 예를 들어 Mutex 타입의 객체를 조작하는 C API를 사용 중이라고 가정해보자. 해당 API에서 제공하는 함수 중엔 lock, unlock함수를 제공한다. void lock(Mutex* pm); // pm이 가리키는 뮤텍스에 잠금을 건다. void unlock(Mutex* pm); // pm이 가리키는 해당 뮤텍스의 잠금을 해제한다. 우리는 배운 것처럼 자원을 획득하고, 소멸 시에 그 자원을 해제해야 하니 이전에 걸어 놓은 뮤텍스 잠금을 잊지 않고 풀어 줄 목적인 뮤텍스 잠금을 관리하는 클래스를 하나 만든다고 생각해보자. class Lock { public: exp..
[Effective C++] 13. 자원 관리에는 객체가 그만!
[Effective C++] 13. 자원 관리에는 객체가 그만!
2022.05.20우리는 동적 할당한 객체를 사용할 일이 더 이상 없을 때 해제해줘야 한다는 사실을 알고 있다. 아래의 예시를 보자. void f() { Investment *pInv = createInvestment(); ... delete pInv; } 이렇게 하면 f를 벗어날 때 Investment객체를 해제하기 때문에 문제가 없어 보이지만, 중간에 예외가 발생하거나 return 되는 경우 등에 대한 고려는 없음을 알 수 있다. 위와 같은 경우를 막기 위한 방법으로는 자원을 객체에 넣고 그 자원 해제를 소멸자가 맡도록 하고, 소멸자는 실행 제어가 f를 떠날 때 호출되도록 만들면 되는데 이러한 내용을 템플릿 클래스로 만든 것이 스마트 포인터이다. 스마트 포인터 포인터처럼 동작하는 클래스 템플릿으로 사용이 끝난 메모리를..
[Effective C++] 12. 객체의 모든 부분을 빠짐없이 복사하자
[Effective C++] 12. 객체의 모든 부분을 빠짐없이 복사하자
2022.05.18객체의 모든 부분을 빠짐없이 복사하자 객체의 안쪽 부분을 캡슐화된 객체 지향 시스템 중 설계가 잘 된 것들을 보면 복사 생성자와 복사 대입 연산자 두 가지가 있고, 이 둘을 통틀어 객체 복사 함수라고 부른다. 이렇게 컴파일러가 생성한 복사 함수는 객체의 모든 부분을 빠짐없이 복사해주지만, 개발자가 직접 복사 함수를 선언한다면 구현한 복사 함수가 틀린 경우에도 알려주지 않는다.(ex. 모든 객체의 복사가 이루어지지 않는 경우..) 예제 void logCall(const std::string& funcName); // 로그 기록을 만든다. class Customer { public: Customer(const Customer& rhs); Customer& operator=(const Customer& rhs..
[Effective C++] 11. operator=에서는 자기 대입에 대한 처리가 빠지지 않도록 하자
[Effective C++] 11. operator=에서는 자기 대입에 대한 처리가 빠지지 않도록 하자
2022.05.18자기 대입(self assignment) 어떤 객체가 자기 자신에 대해 대입 연산자를 적용하는 것을 뜻한다. class Widget {...}; Widget w; w = w; // 자기에 대한 대입 자기 대입이 생기는 이유는 여러 곳에서 하나의 객체를 참조하는 상태(중복 참조)가 일어나기 때문인데, 따라서 같은 클래스의 객체를 다룰 때는 중복 참조를 고려하여 설계해야 한다. 그리고 이때 조심해야 하는 것이 있는데 밑의 코드를 봐보자. class Bitmap {...}; class Widget { private: Bitmap *pb; // 힙에 할당한 객체를 가리키는 포인터 } Widget& Widget::operator=(const Widget& rhs) // 안전하지 않게 구현된 operator= { ..
[Effective C++] 10. 대입 연산자는 *this의 참조자를 반환하게 하자
[Effective C++] 10. 대입 연산자는 *this의 참조자를 반환하게 하자
2022.05.14C++의 대입 연산은 여러 개가 사슬처럼 엮일 수 있는 성질을 가지고 있다. int x, y, z; x = y = z = 10; 대입 연산이 가진 또 하나의 특성은 우측 연관(right-associative) 연산이라는 점이다. 즉, 위의 대입 연산 사슬은 다음과 같이 우측 z부터 순서대로 대입된다. x = (y = (z = 10)); 대입 연산이 사슬처럼 엮이려면 대입 연산자가 좌변 인자에 대한 참조자를 반환하도록 구현되어 있다. 아래의 예시를 보자. class Widget { public: Widget& operator+=(const Widget& rhs) // +=, -=, *= 등에도 동일한 규약이 적용된다. { return *this; // 좌변 객체(의 참조자)를 반환. 반환 타입은 현재 클래..
[Effective C++] 9. 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자
[Effective C++] 9. 객체 생성 및 소멸 과정 중에는 절대로 가상 함수를 호출하지 말자
2022.05.14먼저 이 단원에서 얘기하고 싶은 내용을 얘기하자면 객체 생성 및 소멸 과정에서 가상 함수를 호출하면 "미정의 동작"을 할 수 있기 때문에 호출하면 안 된다는 것이다. 이런 현상이 발생하는 원인에 관해서 알아보자. 다형성과 가상 함수 base클래스에서 가상 함수 function()을 선언하고, derived클래스에서 가상 함수 function을 오버라이딩 해준 구조를 생각해보자. 이제 메인에서 base b = new drived(); 이후 b.function(); 호출 시 derived의 함수가 호출되는데, 이게 바로 C++의 다형성의 특징이다. 근데 base의 생성자에서 가상 함수인 function을 호출하게 되면 어떻게 될까? 일단 예제를 확인해보자. class Transaction { public: ..
[Effective C++] 8. 예외가 소멸자를 떠나지 못하도록 붙들어 놓자
[Effective C++] 8. 예외가 소멸자를 떠나지 못하도록 붙들어 놓자
2022.05.14제목의 의미는 소멸자에서 예외가 빠져나오지 않도록 처리하자는 것을 의미한다. 소멸자가 호출되는 경우 정상적으로 객체가 종료되었을 때 예외처리 메커니즘에 의해 객체가 소멸될 때 위에서 예외처리 메커니즘에 의해 객체가 소멸될 때 또 예외가 발생한다면, terminate함수가 호출되어 프로그램이 종료된다. 따라서 try~catch로 예외를 소멸자 내에 묶어 두어야 한다. 아래의 코드를 보면, class DBConnection { public: static DBConnection create(); // DB 생성 void close(); // 연결 닫기 } ---- class DBConn { public: ~DBConn() { db.close(); // DB 연결 닫기 } private: DBConnection..
[Effective C++] 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자
[Effective C++] 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자
2022.05.13C++의 다형성을 통해 우리는 공통 기능을 가진 하나의 클래스를 기본 클래스로 만들고 적절한 용도에 따라 파생시켜 사용할 수 있으므로, 이렇게 파생시킨 객체를 쉽게 사용하기 위해 이 객체에 대한 포인터를 가져오는 용도로 팩토리 함수를 사용하면 좋을 것 같다. 그럼 팩토리 함수부터 알아보자. 팩토리 함수 새로 생성된 파생 클래스 객체에 대한 기본 클래스 포인터를 반환하는 함수로 파생 클래스의 객체를 new로 할당하기 때문에 메모리를 적절히 삭제해야 한다. 하지만 잘 못 사용하면 문제가 될 수 있는데, 아래의 예시를 보고 문제가 되는 부분을 찾아보자. class TimeKeeper { public: TimeKeeper(); ~TimeKeeper(); } class AtomicClock: public Time..
[Effective C++] 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자
[Effective C++] 6. 컴파일러가 만들어낸 함수가 필요 없으면 확실히 이들의 사용을 금해 버리자
2022.05.09복사 생성자와 복사 대입 연산자를 사용하고 싶지 않을 때 항목 5에서 컴파일러가 은근슬쩍 만들어내는 함수들에 대해 알았으니, 이러한 함수들을 사용하고 싶지 않을 때에 대해서 알아보자. 첫 번째 해결의 열쇠는 다음과 같다. 컴파일러가 생성하는 함수들은 모두 public멤버가 되지만, 직접 선언할 때는 public멤버로 선언해야 한다고 요구하는 부분은 없다는 점이다. 즉, 복사 생성자 및 복사 대입 연산자를 private 멤버로 선언하면 클래스 멤버 함수가 명시적으로 선언되기 때문에 컴파일러는 자신의 기본 버전을 만들 수 없음 private의 접근성을 가지므로 외부로부터 호출도 불가능하여 복사를 할 수 없음 두 가지 이유로 인해 외부에서 복사는 불가능하게 할 수 있다. 하지만 private멤버 함수는 그 클..
[Effective C++] 5. C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자
[Effective C++] 5. C++가 은근슬쩍 만들어 호출해 버리는 함수들에 촉각을 세우자
2022.05.08비어있지만, 비어있지 않은 class C++에서는 직접 선언하지 않아도 복사 생성자(copy constructor), 복사 대입 연산자(copy assignment operator), 소멸자(destructor), 생성자를 기본형으로 선언해준다. 이들은 모두 public멤버이며 inline 함수이다. 즉, 아래의 클래스를 선언했더라도 class Empty {}; 생성자와 소멸자, 복사 생성자, 복사 대입 연산자들이 암시적으로 선언되어 있는 것과 같다. class Empty { public: Empty() {...} // 기본 생성자 Empty(const Empty& rhs) {...} // 복사 생성자 ~Empty() {...} // 소멸자 Empty& operator=(const Empty& rhs) ..
[Effective C++] 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자
[Effective C++] 4. 객체를 사용하기 전에 반드시 그 객체를 초기화하자
2022.05.07C++의 초기화 C++은 객체의 값의 초기화를 보장하지 않는다. 초기화되지 않은 값을 내버려 두면 정의되지 않은 동작이 그대로 나올 수 있기 때문에 항상 초기화를 해주는 것이 좋다. 기본 제공 타입으로 만들어진 비멤버 객체에 대해서 초기화는 아래처럼 할 수 있으며 int x = 0; // int의 직접 초기화 const char * text = "A C-style string"; // 포인터의 직접 초기화 double d; std::cin>> d; // 입력 스트림에서 읽음으로써 초기화 수행 이런 부분을 제외한 C++의 초기화 나머지 부분은 생성자로 귀결되기 때문에 생성자에서 지킬 규칙은 간단하다. 그 객체의 모든 것을 초기화하면 된다. 이때, 대입(assignment)을 초기화(initializatio..
[Effective C++] 3. 낌새만 보이면 const를 들이대 보자!
[Effective C++] 3. 낌새만 보이면 const를 들이대 보자!
2022.05.07const의 매력 '의미적인 제약'을 소스코드 수준에서 붙인다는 점 컴파일러가 이 제약을 단단히 지켜준다는 점 즉, 어떤 값이 불변이어야 한다는 제작자의 의도를 컴파일러 및 다른 프로그래머와 나눌 수 있는 수단이 된다는 점이 매력적이라 볼 수 있다. const는 팔방미인 클래스 바깥에서 전역 혹은 네임스페이스 유효 범위의 상수를 선언(정의)하는 데 사용할 수 있으며 파일, 함수, 블록 유효 범위에서 static으로 선언 한 객체에도 const를 붙일 수 있다. 클래스 내부에서는 정적 멤버 및 비정적 데이터 멤버 모두를 상수로 선언할 수 있다. Q. 포인터는 어떨까? char greeting[] = "Hello"; char* p = greeting; // 비상수 포인터, 비상수 데이터 const char*..