9. Technicalities: Classes, etc.

9.1. User-defined types

C++ 언어는 char, int, double 등의 빌트인 타입을 지원한다. 빌트인 타입이 아닌 타입은 사용자 정의 타입이라 불린다. 이는 표준 라이브러리 타입 (std::vector, std::string, std::ostream 등)일 수도 있으며, 사용자가 만든 타입일 수도 있다. 많은 사용자 정의 타입은 해당하는 연산자까지 같이 정의되는 경우도 있다: std::string의 + 연산자, std::ostream의 << 연산자 등. 이러한 타입을 정의하는 것은 우리의 아이디어를 코드에 표현하는 데 있어서 유용하다. 오브젝트에 필요한 데이터를 어떻게 표현할지, 어떤 연산이 데이터에 적용될 수 있는지를 정의할 수 있다.

C++은 2가지의 사용자 정의 타입을 제공한다: 클래스와 열거형. 클래스는 타입의 오브젝트가 어떻게 표현되는지, 어떻게 생성되는지, 어떻게 사용되는지, 어떻게 소멸되는지를 특정하는 사용자 정의 타입이다. 클래스는 큰 프로그램을 이루는 아주 유용한 벽돌이 된다.

9.2. Classes and members

클래스는 사용자 정의 타입으로, 빌트인 타입이나 다른 사용자 정의 타입이나 함수들로 이루어진다. 클래스를 이루는 부분들을 멤버라 한다. 클래스는 0개 이상의 멤버를 가질 수 있다. 멤버는 데이터 멤버일 수도 있고 함수일 수도 있다. 멤버는 object.member 로 접근한다. 멤버 함수 내에서는 var.m이 아니라 m으로 다른 멤버를 직접 접근할 수 있다.

class X {
public:
    int m; // data member
    int mf(int v) { int old = m; m = v; return old; } // function member
};

X var; // var is a variable of type X
var.m = 7; // assign to var's data member m
int x = var.mf(9); // call var's member function mf()

9.3. Interface and implementation

클래스는 인터페이스와 구현의 합이다. 인터페이스는 사용자가 직접적으로 접근하는 선언부이다. 구현은 클래스 사용자가 인터페이스를 통해서만 간접적으로 접근할 수 있는 선언부이다. 퍼블릭 인터페이스는 public: 라벨을 붙이고 구현부는 private: 라벨을 붙인다.

class X { // this class's name is X
public:
    // public members: the interface to users (accessible to all)
    // functions, types, data (often best kept private)
private:
    // private members: the implementation details (used by members of this class only)
    // functions, types, data
};

클래스 멤버는 기본적으로 private:이다. 사용자는 직접적으로 private 멤버에 접근할 수 없고, 그를 사용하는 public 함수를 통해서만 접근할 수 있다. struct는 클래스의 한 종류로서, 기본적으로 public: 공개 범위를 갖는 class이다. 보통은 자료 구조 등에 쓰인다.

9.4. Evolving a class

9.4.1. struct and functions

날짜를 어떻게 표현할까? 다음의 struct Date를 생각해 볼 수 있다.

// simple Date (too simple?)
struct Date {
    int y;       // year
    int m;     // month in year
    int d;      // day of month
};

이의 문제는 잘못된 입력 (y = -3 등)에 대한 조건이 없다는 것이다. 다음과 같은 초기화 함수를 둠으로써 함수 내에서 조건을 체크할 수 있다.

// helper functions:

void init_day(Date& dd, int y, int m, int d) {
    // check that (y,m,d) is a valid date
    // if it is, use it to initialize dd
}

void add_day(Date& dd, int n) {
    // increase dd by n days
}

클래스 내 연산들은 항상 클래스가 만족해야 하는 조건들을 체크해야 한다.

9.4.2. Member functions and constructors

체크 함수는 제대로 쓰지 않으면 의미가 없다. 그래서 초기화시킬 때부터 클래스 데이터의 정당성을 체크하는 것은 중요하다. 이를 위해서 생성자를 쓴다.

// simple Date
// guarantee initialization with constructor
// provide some notational convenience
struct Date {
    int y, m, d;                            // year, month, day
    Date(int y, int m, int d);      // check for valid date and initialize
    void add_day(int n);           // increase the Date by n days
};

생성자가 있는 클래스는 다음과 같이 초기화할 수 있다.

Date my_birthday;                                  // error: my_birthday not initialized
Date today {12,24,2007};                        // oops! run-time error
Date last {2000,12,31};                            // OK (colloquial style)
Date next = {2014,2,14};                         // also OK (slightly verbose)
Date christmas = Date{1976,12,24};      // also OK (verbose style)
Date last(2000,12,31);            // OK (old colloquial style)

9.4.3. Keep details private

Date의 날짜는 add_day를 통해서만 수정될 수 있어야 할 것이다. 이를 위해 private:로 직접 접근으로부터 보호한다.

// simple Date (control access)
class Date {
    int y, m, d;                                          // year, month, day
public:
    Date(int y, int m, int d);                   // check for valid date and initialize
    void add_day(int n);                        // increase the Date by n days
    int month() { return m; }
    int day() { return d; }
    int year() { return y; }
};

Date birthday {1970, 12, 30};                 // OK
birthday.m = 14;                                      // error: Date::m is private
std::cout << birthday.month() << '\n';         // we provided a way to read m

private로 직접 접근으로부터 보호하는 이유는 데이터가 올바른 상태에 있음을 보장하기 위해서이다. 멤버 함수, 생성자 등을 통해 데이터를 생성하고 수정할 때마다 올바른 상태에 있음을 보장할 수 있다.

9.4.4. Defining member functions

멤버 함수를 정의할 때, 클래스 바깥에서는 class_name::member_name 표기법을 쓴다.

Date::Date(int yy, int mm, int dd) : y{yy}, m{mm}, d{dd} {         // constructor with member initializers
}
void Date::add_day(int n) {
    // ...
}
int month() {                        // oops: we forgot Date::
    return m;                 // not the member function, can’t access m
}

큰 멤버 함수들은 보통 클래스 정의부 안이 아니라 바깥으로 분리한다. 이는 세부 구현과 인터페이스를 분리하기 위함이다. 간단한 함수는 클래스 정의부 안으로 넣기도 한다.

클래스 정의부 안으로 멤버 함수를 넣는 것은 인라이닝의 효과도 있는데, 이는 컴파일러가 클래스 안의 진입 지점을 통해 함수를 호출하는 것이 아니라 함수를 직접 호출함으로써 성능을 향상시킬 수 있게 된다. 인라인된 함수의 내용을 바꾸면 클래스를 사용하는 부분 전체가 재컴파일되어야 한다는 단점이 있으며, 클래스 정의부의 몸집이 커진다는 단점도 있다. 컴파일러는 큰 함수들을 인라이닝하지 않으므로 작은 함수들만 클래스 정의부 안으로 넣자.

9.4.5. Referring to the current object

클래스 멤버 함수가 클래스 멤버를 호출할 때에는 해당 클래스 오브젝트의 멤버를 자동적으로 호출한다.

9.4.6. Reporting errors

올바르지 못한 날짜를 생성하려 시도하면 어떻게 해야 할까? 생성할 때부터 그런 날짜는 생성되지 않게 해야 하므로, 생성자에서 예외를 던지는 방식으로 해결한다.

// simple Date (prevent invalid dates)
class Date {
public:
    class Invalid { };                       // to be used as exception
    Date(int y, int m, int d);          // check for valid date and initialize
          // . . .
private:
    int y, m, d;                               // year, month, day
    bool is_valid();                        // return true if date is valid
};

Date::Date(int yy, int mm, int dd) : y{yy}, m{mm}, d{dd} {  // initialize data members
    if (!is_valid()) throw Invalid{};          // check for validity
}

bool Date::is_valid() {                                   // return true if date is valid
    if (m < 1 || 12 < m) return false;
    // ...
}

이렇게 해 두면 다음과 같이 예외를 처리할 수 있다.

void f(int x, int y)
try {
      Date dxy {2004,x,y};
      std::cout << dxy << '\n';              // see §9.8 for a declaration of <<
     dxy.add_day(2);
} catch(Date::Invalid) {
          throw std::runtime_error("invalid date");
}

9.5. Enumerations

enum은 열거형으로서 어떤 값들을 상징적 상수로 연결시켜 나열한 것이다.

enum class Month {
    jan=1, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
};

이렇게 하면 Month::jan으로 1월을 나타낼 수 있다. 열거형의 경우 값을 지정해 준 값 이후의 값들은 1씩 증가되면서 적용되고, 값을 지정해주지 않으면 가장 처음 값이 0으로 지정되고 그 이후의 값들은 1씩 증가된다.

Month m = Month::feb;

Month m2 = feb;                     // error: feb is not in scope
m = 7;                                         // error: can’t assign an int to a Month
int n = m;                                   // error: can’t assign a Month to an int
Month mm = Month(7);         // convert int to Month (unchecked)
Month bad = 9999;     // error: can’t convert an int to a Month

열거형 클래스와 int는 직접 상호 호환이 불가능하다. Month(999)나 Month{999}같은 초기화도 사용할 수 없다. 999는 Month로 사용할 수 없는 값이기 때문이다.

열거형 클래스는 올바른 생성을 보장하는 생성자가 없으므로 변환 함수에서 체크하는 수밖에 없다.

Month int_to_month(int x) {
    if (x < int(Month::jan) || int(Month::dec) < x) throw std::runtime_error("bad month");
    return Month(x);
}

void f(int m) {
    Month mm = int_to_month(m);
    // ...
}

9.5.1. “Plain” enumerations

C++11 이전에는 class가 안 붙은 enum이 있었다. 이는 단순히 정수형 상수 표현식에 대한 새로운 이름을 붙이는 역할만 했으므로 다른 enum끼리의 충돌이나 int와의 충돌로부터 막지 못했다. 이를 쓰지 말고 enum class를 쓰자.

9.6. Operator overloading

대부분의 C++ 연산자는 클래스와 열거형에 오버로딩될 수 있다. 이를 연산자 오버로딩이라 한다.

enum class Month {
    Jan=1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
};

Month operator++(Month& m) {                                 // prefix increment operator
    m = (m == Dec) ? Jan : Month(int(m) + 1);       // “wrap around”
    return m;
}

Month m = Sep;
++m;       // m becomes Oct
++m;       // m becomes Nov
++m;       // m becomes Dec
++m;       // m becomes Jan (“wrap around”)

하지만 없는 연산자를 새로 만들어낼 순 없다. 또한 연산자의 인자 수를 부자연스럽게 조정하는 것도 불가능하다. (단항 <= 등) 오버로딩된 연산자는 최소 하나의 사용자 정의 타입을 피연산자로 가져야 한다.

int operator+(int,int);      // error: you can’t overload built-in +
Vector operator+(const Vector&, const Vector &);      // OK
Vector operator+=(const Vector&, int);                          // OK

연산자 오버로딩은 코드의 직관성을 늘리지 않는다면 하지 않는 것이 낫다. 또한 의미에 맞게 사용하라.

9.7. Class interfaces

  • 인터페이스를 완전하게 유지하라.
  • 인터페이스를 최소한으로 유지하라.
  • 생성자를 제공하라.
  • 복사를 지원하든가 막아라.
  • 인자의 타입을 잘 체킹하라.
  • 멤버 변수를 수정하지 않는 멤버 함수를 식별하라.
  • 소멸자에서 자원을 해제하라.

9.7.1. Argument types

생성자에서 인자 타입의 일치를 컴파일 타임에 체크하라. 컴파일 타임에 체크할 수 있는 것은 컴파일 타임에 체크하는 것이 좋지만 인자 타입의 정당성은 런타임에 체크할 수밖에 없다.

9.7.2. Copying

사용자 정의 타입에 대해 컴파일러가 기본적으로 대입과 복사를 제공하지만 이를 원하지 않을 때는 (이것은 대체로 좋은 생각이 아니다) 이것을 커스터마이징할 수 있다.

9.7.3. Default constructors

생성자를 만들지 않으면 컴파일러가 기본 생성자를 제공한다.

std::string s1;                         // default value: the empty string " "
std::vector<std::string> v1;        // default value: the empty vector; no elements
std::string s2 = std::string{}; // default value: the empty string " "
std::vector<std::string> v2 = std::vector<std::string>{};     // default value: the empty vector; no elements

사용자 정의 타입을 만들었을 때는 기본 생성자가 적절한 기본값을 가짐을 보장해야 한다.

Date::Date() : y {2001}, m {Month::jan}, d {1} {}

또는 클래스 내 static 변수를 만들어 기본값을 담당하게 할 수도 있다.

const Date& default_date() {
    static Date dd {2001,Month::jan,1};
    return dd;
}

Date::Date() :y{default_date().year()}, m{default_date().month()},          d{default_date().day()} {}

std::vector<Date> birthdays(10);      // ten elements with the default Date value, Date{}

9.7.4. const member functions

함수 내 클래스 인자가 const로 선언되었는데 멤버 함수가 const가 아닐 경우에는 컴파일러는 그 멤버 함수가 클래스 인자를 수정할 수 있다고 가정하므로 컴파일 에러를 띄운다. 이를 막기 위해서 멤버 변수를 수정하지 않는 멤버 함수는 const로 놓는다.

class Date {
public:
          // . . .
          int day() const;                         // const member: can’t modify the object
          Month month() const;           // const member: can’t modify the object
          int year() const;                       // const member: can’t modify the object

          void add_day(int n);              // non-const member: can modify the object
          void add_month(int n);        // non-const member: can modify the object
          void add_year(int n);            // non-const member: can modify the object
private:
          int y;                                         // year
          Month m;
          int d;                                        // day of month
};

Date d {2000, Month::jan, 20};
const Date cd {2001, Month::feb, 21};

std::cout << d.day() << " — " << cd.day() << '\n';      // OK
d.add_day(1);                                                             // OK
cd.add_day(1);                                                           // error: cd is a const

9.7.5. Members and “helper functions”

클래스의 정당성을 보장하기 위해서 헬퍼 함수를 만들되, 코드의 유지보수와 이해의 용이성을 위해 그 개수를 최소화하라.

9.8. The Date class

이로써 Date 클래스가 만들어진다. C++ 표준 라이브러리에서는 <chrono>에서 더 나은 기능을 제공한다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중