클래스(Class)

클래스(Class)

데이터와 그 데이터를 다루는 동작을 한데 묶어 새 타입을 만들 일이 많아요. C++에서는 그걸 **클래스(class)**로 표현해요. 클래스는 사용자 정의 타입으로, 선언 문법의 decl-specifier-seq에 나오는 class-specifier로 정의돼요.

출처: cppreference

본문

문법

클래스 지정자(class specifier)는 다음 문법을 가져요.

class-key 속성(optional) class-head-name final(optional) base-clause(optional) { member-specification } (1)
class-key 속성(optional) base-clause(optional) { member-specification } (2)
자리 의미
class-key class, struct, union 중 하나. 키워드 classstruct는 기본 멤버 접근과 기본 기반 클래스 접근을 제외하면 동일해요. union이면 그 선언은 공용체(union) 타입을 도입해요.
속성(attr) (C++11부터) 임의 개수의 속성, alignas 지정자를 포함할 수 있음
class-head-name 정의되는 클래스의 이름, 선택적으로 한정될 수 있음
final (C++11부터) 있으면 그 클래스를 상속할 수 없음
base-clause 하나 이상의 기반 클래스와 각각에 사용된 상속 방식의 목록 (파생 클래스 참고)
member-specification 접근 지정자, 멤버 객체와 멤버 함수 선언·정의의 목록 (아래 참고)

전방 선언(Forward declaration)

다음 형태의 선언

class-key 속성 식별자 ;

은 이 스코프에서 나중에 정의될 클래스 타입을 선언해요. 정의가 나타날 때까지 이 클래스 이름은 **불완전 타입(incomplete type)**이에요. 이 덕분에 서로를 참조하는 클래스를 만들 수 있어요.

class Vector; // 전방 선언

class Matrix
{
    // ...
    friend Vector operator*(const Matrix&, const Vector&);
};

class Vector
{
    // ...
    friend Vector operator*(const Matrix&, const Vector&);
};

그리고 특정 소스 파일이 클래스에 대한 포인터와 참조만 사용한다면, #include 의존성을 줄이는 데도 도움이 돼요.

// MyStruct.h 안
#include <iosfwd> // std::ostream의 전방 선언 포함

struct MyStruct
{
    int value;
    friend std::ostream& operator<<(std::ostream& os, const S& s);
    // 정의는 #include <ostream>을 사용하는 MyStruct.cpp 파일에 제공
};

전방 선언이 지역 스코프에 나오면, 같은 이름으로 이전에 선언된 클래스, 변수, 함수, 그리고 둘러싸는 스코프에 나타날 수 있는 다른 모든 선언을 가려요(hide).

struct s { int a; };
struct s; // 아무 것도 하지 않음 (s는 이 스코프에서 이미 정의됨)

void g()
{
    struct s; // 새 지역 struct "s"의 전방 선언
              // 이 블록이 끝날 때까지 전역 struct s를 가림
    
    s* p; // 지역 struct s에 대한 포인터
    
    struct s { char* p; }; // 지역 struct s의 정의
}

새 클래스 이름은 다른 선언의 일부로 나타나는 정교화된 타입 지정자(elaborated type specifier)로도 도입될 수 있어요. 다만 이름 검색이 같은 이름의 이전에 선언된 클래스를 찾지 못할 때만 그래요.

class U;

namespace ns
{
    class Y f(class T p); // 함수 ns::f를 선언하고 ns::T와 ns::Y를 선언

    class U f(); // U는 ::U를 가리킴
    
    // T와 Y에 대한 포인터와 참조를 쓸 수 있음
    Y* p;
    T* q;
}

멤버 지정(Member specification)

멤버 지정, 즉 클래스 정의의 **본문(body)**은 다음 중 임의의 개수를 중괄호로 감싼 시퀀스예요.

속성(optional) decl-specifier-seq(optional) member-declarator-list(optional) ;
자리 의미
속성(attr) (C++11부터) 임의 개수의 속성
decl-specifier-seq 지정자들의 시퀀스. 생성자, 소멸자, 사용자 정의 타입 변환 함수의 선언에서만 선택적이에요.
member-declarator-list init-declarator-list와 비슷하지만, 비트 필드 선언, 순수 지정자(pure-specifier), virt-specifier(override 또는 final, C++11부터)를 추가로 허용하고, direct-non-list-initialization 문법은 허용하지 않아요.

이 선언은 정적·비정적 데이터 멤버와 멤버 함수, 멤버 typedef, 멤버 열거형, 중첩 클래스를 선언할 수 있어요. friend 선언일 수도 있어요.

class S
{
    int d1;             // 비정적 데이터 멤버
    int a[10] = {1, 2}; // 초기화식을 가진 비정적 데이터 멤버 (C++11)
    
    static const int d2 = 1; // 초기화식을 가진 정적 데이터 멤버
    
    virtual void f1(int) = 0; // 순수 가상 멤버 함수
    
    std::string d3, *d4, f2(int); // 데이터 멤버 두 개와 멤버 함수 하나
    
    enum { NORTH, SOUTH, EAST, WEST };
    
    struct NestedS
    {
        std::string s;
    } d5, *d6;
    
    typedef NestedS value_type, *pointer_type;
};
class M
{
    std::size_t C;
    std::vector<int> data;
public:
    M(std::size_t R, std::size_t C) : C(C), data(R*C) {} // 생성자 정의
    
    int operator()(std::size_t r, std::size_t c) const // 멤버 함수 정의
    {
        return data[r * C + c];
    }
    
    int& operator()(std::size_t r, std::size_t c) // 또 다른 멤버 함수 정의
    {
        return data[r * C + c];
    }
};
class S
{
public:
    S();          // public 생성자
    S(const S&);  // public 복사 생성자
    virtual ~S(); // public 가상 소멸자
private:
    int* ptr; // private 데이터 멤버
};
class Base
{
protected:
    int d;
};

class Derived : public Base
{
public:
    using Base::d;    // Base의 protected 멤버 d를 Derived의 public 멤버로 만듦
    using Base::Base; // 모든 기반의 생성자를 상속 (C++11)
};
template<typename T>
struct Foo
{
    static_assert(std::is_floating_point<T>::value, "Foo<T>: T must be floating point");
};
struct S
{
    template<typename T>
    void f(T&& n);
    
    template<class CharT>
    struct NestedS
    {
        std::basic_string<CharT> s;
    };
};

멤버 선언에는 별칭 선언(alias declaration)도 포함돼요. (C++11부터)

template<typename T>
struct identity { using type = T; };

멤버 클래스 템플릿의 추론 가이드(deduction guide)도 포함돼요. (C++17부터)

template<class CharT>
struct NestedS { std::basic_string<CharT> s; };

template<class CharT>
NestedS(std::basic_string<CharT>) -> NestedS<CharT>;

using-enum 선언도 포함돼요. (C++20부터)

enum class color { red, orange, yellow };

struct highlight { using enum color; };

지역 클래스(Local classes)

클래스 선언은 함수 본문 안에도 나타날 수 있는데, 그 경우 **지역 클래스(local class)**를 정의해요. 이런 클래스의 이름은 함수 스코프 안에서만 존재하고, 밖에서는 접근할 수 없어요.

  • 지역 클래스의 멤버는 그 클래스의 정의에서만 선언될 수 있어요. 단, 중첩 클래스인 멤버는 그 클래스의 가장 가까운 둘러싸는 블록 스코프에서도 선언될 수 있어요.
  • 지역 클래스 안에 중첩된 클래스도 지역 클래스예요.
  • 지역 클래스는 정적 데이터 멤버를 가질 수 없어요.
  • 지역 클래스의 멤버 함수는 링키지(linkage)가 없어요.
  • 지역 클래스의 멤버 함수는 클래스 본문 안에서 완전히 정의되어야 해요.
  • 클로저 타입(closure types, C++14부터)을 제외한 지역 클래스는 멤버 템플릿을 가질 수 없어요.
  • 지역 클래스는 friend 템플릿을 가질 수 없어요.
  • 지역 클래스는 클래스 정의 안에서 friend 함수를 정의할 수 없어요.
  • 함수(멤버 함수 포함) 안의 지역 클래스는 그 둘러싸는 함수가 접근할 수 있는 것과 같은 이름에 접근할 수 있어요.

지역 클래스는 템플릿 인자로 쓸 수 없었어요. (C++11 이전)

#include <algorithm>
#include <iostream>
#include <vector>

int main()
{
    std::vector<int> v{1, 2, 3};
    
    struct Local
    {
        bool operator()(int n, int m)
        {
            return n > m;
        }
    };
    
    std::sort(v.begin(), v.end(), Local()); // C++11부터
    
    for (int n : v)
        std::cout << n << ' ';
    std::cout << '\n';
}

출력:

3 2 1

이 예제에서는 함수 안에서 정의한 지역 클래스 Localstd::sort의 비교 함수 객체로 써서 벡터를 내림차순으로 정렬했어요.

결함 보고(Defect reports)

이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 1693 C++98 멤버 선언이 비어 있을 수 없었음 빈 선언 허용
CWG 1930 C++98 decl-specifier-seq이 저장 클래스 지정자나 cv 한정자를 포함하면 member-declarator-list가 비어 있을 수 있었음 목록은 비어 있으면 안 됨
CWG 2890 C++98 중첩 클래스의 멤버를 어디에 선언할 수 있는지가 불명확했음 명확히 함

더 알아보기

  • 파생 클래스(derived class): 상속과 base-clause.
  • 멤버 함수, 생성자, 소멸자: 클래스의 다양한 구성 요소.
  • 공용체(union): union 키워드로 정의하는 특별한 종류의 클래스.