C++ 클래스


데이터 구조체의 확장형, 구조체처럼 멤버 변수를 가질 수 있지만 나아가서 함수도 멤버로 가질 수 있다. (객체 지향의 정수)

클래스의 인스턴스를 ‘Object‘라고 부르며 일반적인 변수처럼 클래스 자체를 타입으로 선언하고 다룰 수 있다.

class class_name { // class_name: 클래스의 식별자 
 // 선언하는 Body 부분에 구조체처럼 멤버 변수를 가질 수 있다.
  access_specifier_1:
    member1;
  access_specifier_2:
    member2;
  ...
} object_names; // 초기화 하면서 오브젝트도 선언할 수 있다.

기본적으로 일반적인 데이터 구조체와 같은 형식이지만 추가로 함수를 넣을 수 있고 접근 지정자를 사용한다.

접근 지정자가 의미하는 것은 아래와 같다.

private: 같은 클래스나 "friend"에서만 사용할 수 있다.
기본적으로 이걸로 정해진다.
protected: 상속한 클래스와 "friend"까지.
public: 모든 곳에서 접근 가능

멤버 함수의 경우는 클래스 선언부에서 직접 구현하거나 프로토타입만 선언하고 구문 밖에서 스코프 연산자 (‘::‘)로 클래스를 지정하고 함수를 마저 구현할 수 있다.

스코프 연산자는 지정한 클래스나 네임스페이스에 포함시킨 것 같이 범위 속성을 부여한다.

// classes example
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    void set_values (int,int); // 함수의 프로토타입 선언
    int area() {return width*height;} // 또는 이렇게 그냥 구현할 수도 있다.
};

void Rectangle::set_values (int x, int y) { // 여기서 함수 구현 
  width = x;
  height = y;
}

int main () {
  Rectangle rect;
  rect.set_values (3,4);
  cout << "area: " << rect.area(); // area: 12
  return 0;
}

컴파일러의 최적화 면에서 멤버 함수를 정의부에서 구현하는 것은 인라인 멤버 함수로 간주하여 처리한다. 지금처럼 밖에서 구현하는 것은 인라인으로 처리되지 않는다.

생성자 Constructor

클래스가 생성될 때, 자동으로 호출되는 함수

보통 멤버 변수의 초기화나 할당 작업에 쓰인다.
클래스와 같은 이름의 반환 타입이 없는 멤버 함수로 선언하고 인자가 있는 경우 오브젝트 생성 시기에 넣어서 사용할 수 있다.

// example: class constructor
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle (int,int);
    int area () {return (width*height);}
};

Rectangle::Rectangle (int a, int b) {
  width = a;
  height = b;
}

int main () {
  Rectangle rect (3,4); // 생성자 호출
  Rectangle rectb (5,6); // 생성자 호출
  cout << "rect area: " << rect.area() << endl; // 12
  cout << "rectb area: " << rectb.area() << endl; // 30
  return 0;
}

생성자는 일반 멤버 함수처럼 어느 때나 사용 가능한 것이 아닌 오브젝트 생성 시, 딱 한 번만 실행하고 값을 반환하지 않는다.

또한 다른 함수처럼 오버로딩 할 수 있다.

// overloading class constructors
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle ();        // 생성자 1
    Rectangle (int,int); // 생성자 2
    int area (void) {return (width*height);}
};

Rectangle::Rectangle () { // 기본 호출자
  width = 5;
  height = 5;
}

Rectangle::Rectangle (int a, int b) {
  width = a;
  height = b;
}

int main () {
  Rectangle rect (3,4);
  Rectangle rectb;
  cout << "rect area: " << rect.area() << endl;
  cout << "rectb area: " << rectb.area() << endl;
  return 0;
}

위의 예제에서 rectb와 같이 매개변수를 나열하는 괄호 자체가 없는 경우에 기본 생성자가 호출된다. (괄호만 있고 매개변수가 없는 경우는 객체 선언이 아닌 함수 선언으로 취급하여 기본 생성자가 호출되진 않는다.)

사실 생성자 자체는 다른 구문으로도 호출될 수 있다.
우선 하나의 매개변수만을 가진 생성자라면 변수 초기화 방식을 따라할 수 있다.
class_name object_name = initialization_value;

최근 버전의 C++(확인 필요)에서는 괄호가 아닌 중괄호 { }를 사용하여 여러 개의 매개변수를 가진 생성자도 호출할 수 있다.
class_name object_name { value, value, value, ... }
class_name object_name = { value, value, value, ... }
// { 전에 할당 연산자도 붙일 수 있다.

// classes and uniform initialization
#include <iostream>
using namespace std;

class Circle {
    double radius;
  public:
    Circle(double r) { radius = r; }
    double circum() {return 2*radius*3.14159265;}
};

int main () {
  Circle foo (10.0);   // functional form
  Circle bar = 20.0;   // assignment init.
  Circle baz {30.0};   // uniform init.
  Circle qux = {40.0}; // POD-like

  cout << "foo's circumference: " << foo.circum() << '\n';
  return 0;
}

// 괄호와는 다르게 중괄호는 함수 호출과 혼동될 수 없는 장점이 있어 괄호에서 안되던 기본 생성자 호출이 가능하다.
Rectangle rectb;   // default constructor called
Rectangle rectc(); // function declaration (default constructor NOT called)
Rectangle rectd{}; // default constructor called 

Most existing code currently uses functional form, and some newer style guides suggest to choose uniform initialization over the others, even though it also has its potential pitfalls for its preference of initializer_list as its type. (확인 필요)

또한 생성자에서 멤버 변수를 초기화 할 때, 단순히 값을 삽입만 한다면 다르게 적어볼 수 있다.

// "멤버 변수"가 일반적인 타입이라면 이렇게도 적을 수 있다.
// "멤버 오브젝트"가 콜론 이후에 없었다면 기본 생성된다.

Rectangle::Rectangle (int x, int y) { width=x; height=y; }
Rectangle::Rectangle (int x, int y) : width(x) { height=y; }

Rectangle::Rectangle (int x, int y) : width(x), height(y) { }

멤버 오브젝트의 초기화는 아래와 같이 예외가 있을 수 있다.

// member initialization
#include <iostream>
using namespace std;

class Circle {
    double radius;
  public:
    Circle(double r) : radius(r) { }
    double area() {return radius*radius*3.14159265;}
};

class Cylinder {
    Circle base;
    double height;
  public:
    // Cylinder(double r, double h) : base (r), height(h) {} // base의 타입인 Circle의 생성자 중에 기본 생성자가 없어서 호출되지 않는다.	
    Cylinder::Cylinder (double r, double h) : base{r}, height{h} { } // 중괄호를 사용해 다른 생성자를 지정할 수 있게된다. (앞서 언급했듯 괄호는 함수 호출로 혼동될 수 있다.)

    double volume() {return base.area() * height;}
};

int main () {
  Cylinder foo (10,20);

  cout << "foo's volume: " << foo.volume() << '\n';
  return 0;
}

충격적인 사실은 구조체와 union도 클래스처럼 사용될 수 있다.
구조체의 경우는 클래스와 마찬가지로 멤버 함수까지 가질 수 있다. 단, 접근자는 기본적으로 public이다.
union은 단 하나의 데이터 멤버를 가지지만 놀랍게도 이것도 하나의 클래스로써 멤버 함수를 가질 수 있다. 이것도 접근자는 기본적으로 public이다.

연산자 오버로딩

클래스에 생성과 할당 외에도 C++의 여러가지 연산자와 상호작용할 수 있도록 덮어쓸 수 있다.

오버로딩 가능한 연산자들
+ - * / = < > += -= *= /= << >> <<= >>= == != <= >= ++ -- % & ^ ! | ~ &= ^= |= && || %= [] () , ->* -> new delete new[] delete[]

오버로딩은 operator 키워드에 연산자를 붙인 특별한 함수로 덮어쓸 수 있다.

type operator sign (parameters) { /*... body ...*/ }

class CVector {
  public:
    int x,y;
    CVector () {};
    CVector (int a,int b) : x(a), y(b) {} // 생성자
    CVector operator + (const CVector&);
};

CVector CVector::operator+ (const CVector& param) {
  CVector temp;
  temp.x = x + param.x; // 예제기준으로 foo.x와 bar.x가 더해진다.
  temp.y = this.y + param.y; // this 키워드는 현재 스코프의 객체를 명시적으로 지정한다.
  return temp; // CVector를 반환한다.


 // 근데 이거 굳이 temp로 복사하지 않고 래퍼런스로 반환해도 되지 않나?
 // return *this;
}
/*
CVector operator+ (const CVector& a, const CVector& param) {
  CVector temp;
  temp.x = a.x + param.x;
  temp.y = a.y + param.y;
  return temp;
}

*/
int main () {
  CVector foo (3,1);
  CVector bar (1,2);
  CVector result;
  result = foo + bar; // CVector::operator+가 호출된다.
// result  = foo.operator+ (bar);
 // 이런 형태로 호출되며 명시적으로 똑같이 호출할 수도 있다.
  cout << result.x << ',' << result.y << '\n';
  return 0;
}

연산자마다 다른 멤버 함수는 아래에서 확인할 수 있다.
위의 예제와는 다르게 클래스의 멤버 함수가 아닌 형태로도 선언할 수 있다.

표현식연산자멤버 함수멤버 함수 아님
@a+ - * & ! ~ ++ --A::operator@()operator@(A)
a@++ --A::operator@(int)operator@(A,int)
a@b+ - * / % ^ & | < > == != <= >= << >> && || ,A::operator@(B)operator@(A,B)
a@b= += -= *= /= %= ^= &= |= <<= >>= []A::operator@(B)
a(b,c...)()A::operator()(B,C...)
a->b->A::operator->()
(TYPE) aTYPEA::operator TYPE()

정적 변수

클래스 내부의 데이터나 함수를 정적으로 선언할 수 있다.
정적으로 선언된 멤버는 모든 오브젝트에 공유되어 멤버 함수가 아닌 듯이 다룰 수 있다.

보통 그 클래스의 모든 오브젝트에서 공유하는 값이 되므로 ‘클래스 변수’라고도 부른다.

// static members in classes
#include <iostream>
using namespace std;

class Dummy {
  public:
    static int n;
    Dummy () { n++; };
};

int Dummy::n=0;

int main () {
  Dummy a;
  Dummy b[5];
  cout << a.n << '\n';
  Dummy * c = new Dummy;
  cout << Dummy::n << '\n'; // 모든 오브젝트에게 값이 공유되므로 오브젝트 이름이 아닌 그냥 클래스 이름으로도 값을 다룰 수 있다. (정적 멤버만 해당)
  delete c;
  return 0;
}

정적으로 선언된 함수의 경우는 멤버 함수가 아닌 것으로 취급되어 해당 스코프 내에 인스턴스가 없는 것으로 취급된다. (즉, this 키워드를 쓸 수 없다.)

const 멤버 함수

const 오브젝트로 선언된 클래스 인스턴스는 클래스 바깥에서는 멤버 변수를 읽는 것만 할 수 있다. (생성자는 여전히 호출되고 값을 초기화한다.)

// constructor on const object
#include <iostream>
using namespace std;

class MyClass {
  public:
    int x;
    MyClass(int val) : x(val) {}
    int get() {return x;}
};

int main() {
  const MyClass foo(10);
// foo.x = 20;            // not valid: x cannot be modified
  cout << foo.x << '\n';  // ok: data member x can be read
  return 0;
}

이 때, 멤버 함수는 해당 오브젝트가 const로 선언된 상태여야 호출될 수 있다.
따라서 위의 예제에서 get 함수를 사용하려면 아래와 같이 바꿔야 한다.

int get() const {return x;} // 키워드를 함수 매개변수와 보디의 중간 부분에 붙인다.

const로 선언된 멤버 함수는 정적 멤버와 const로 선언된 다른 멤버 함수 외에는 호출할 수 없어서 본질적으로 오브젝트의 상태를 바뀌게 하지 않는다.

또한 멤버 함수를 오버로드하여 const(일관성)을 덮어쓸 수 있다.
같은 함수를 const가 있는 것, 없는 것을 나눠서 작성하면 오브젝트의 const 속성에 따라 호출되는 함수가 정해진다.

그러면 포인터로 멤버 변수를 강제로 지정한다면 const 속성을 무시할 수 있나?

const로 선언되지 않은 오브젝트라면 const 선언 유무와 관계없이 모든 멤버 함수에 접근할 수 있다.

클래스 탬플릿

함수 탬플릿을 만드는 것 처럼 클래스 선언부에도 탬플릿을 넣을 수 있다.

template <class T>
class mypair {
    T a, b;
  public:
    mypair (T first, T second) : a(first), b(second) { } 
    T getmax ();
};

mypair<int> myobject (115, 36);
mypair<double> myfloats (3.0, 2.18); 

// 멤버 함수가 클래스 선언부 바깥이었다면 여기서도 탬플릿을 선언해야 한다.
template <class T>
T mypair<T>::getmax () // <T>를 클래스 이름 뒤에 붙여야 한다. 탬플릿의 매개변수도 클래스의 탬플릿 매개변수임을 지정한다.
{
  T retval;
  retval = a>b? a : b;
  return retval;
}

또한 탬플릿 특화 (Template specialization)를 통해 탬플릿 매개변수에 특정한 타입이 온 경우를 따로 구현할 수 있다.

// template specialization
#include <iostream>
using namespace std;

// class template:
template <class T>
class mycontainer {
    T element;
  public:
    mycontainer (T arg) {element=arg;}
    T increase () {return ++element;}
};

// class template specialization:
template <> // 여기서는 모든 타입이 밝혀져있어 탬플릿 매개변수가 필요하지 않지만 탬플릿 선언 자체는 필요하다.
class mycontainer <char> { // char로 선언된 경우
    char element;
  public:
    mycontainer (char arg) {element=arg;}
    char uppercase ()
    {
      if ((element>='a')&&(element<='z'))
      element+='A'-'a';
      return element;
    }
};

int main () {
  mycontainer<int> myint (7);
  mycontainer<char> mychar ('j');
  cout << myint.increase() << endl;
  cout << mychar.uppercase() << endl;
  return 0;
}

탬플릿 특화된 클래스는 원본 클래스에서 상속하지 않으므로 모든 멤버를 다시 선언해야 한다.

특별한 멤버 함수들

클래스의 멤버로써 암시적으로 선언된 함수들이다.

멤버 함수클래스 C를 통해 선언한다면..:
기본 생성자C::C();
소멸자C::~C();
복사 생성자C::C (const C&);
복사 할당자C& operator= (const C&);
이동 생성자C::C (C&&);
이동 할당자C& operator= (C&&);

기본 생성자는 아무런 매개변수 없이 클래스 오브젝트가 생성될 때 호출된다.
클래스에 아무런 생성자가 선언되어있지 않았다면 컴파일러에서 암시적으로 이 기본 생성자가 있는 것으로 추정한다.

class Example {
  public:
    int total;
    void accumulate (int x) { total += x; }
};

Example ex; // 암시적으로 생성자가 추가되어 정상적으로 호출됨

그런데 여기서 매개변수를 사용하는 생성자가 하나라도 선언되었다면 이 암시적으로 생성되던 기본 생성자는 생기지 않는다. 따라서 매개변수 없이 호출하는 것이 불가능해지게 된다.

class Example2 {
  public:
    int total;
    Example2 (int initial_value) : total(initial_value) { };
    void accumulate (int x) { total += x; };
};

Example2 ex (100); // 정상적으로 호출된다.
Example2 ex; // 이번엔 없는 함수를 참고하므로 에러가 나온다. 이 경우에는 기본 생성자를 직접 작성하여 선언해야 한다.

소멸자

생성자와 정반대로 이번에는 클래스 오브젝트가 해체될 때 호출되는 함수

따라서 호출되는 시기는 오브젝트가 선언된 함수의 범위가 끝나는 경우인데 만약에

클래스 사용이 끝나면 내부 리소스 해체를 위해 필요할 수 있다. 특히 동적 할당을 통해 사용하던 리소스라면 여기서 delete를 통해 지워야 한다.

// destructors
#include <iostream>
#include <string>
using namespace std;

class Example4 {
    string* ptr;
  public:
    // constructors:
    Example4() : ptr(new string) {}
    Example4 (const string& str) : ptr(new string(str)) {}
    // destructor:
    ~Example4 () {delete ptr;}
    // access content:
    const string& content() const {return *ptr;}
};

int main () {
  Example4 foo;
  Example4 bar ("Example");

  cout << "bar's content: " << bar.content() << '\n';
  return 0;
}

복사 생성자

오브젝트에서 자신과 같은 타입의 다른 오브젝트를 할당했거나 매개변수로 생성자를 호출했다면 아래와 같은 복사 생성자로 취급한다. (기본 생성자와 마찬가지로 다른 복사, 이동, 할당 생성자가 없다면 암시적으로 생성된다.)

MyClass::MyClass (const MyClass&);

// 멤버 변수가 있는 경우
class MyClass {
  public:
    int a, b; string c;
};
// 아래와 같이 선언된다. 이 때, 클래스 자체의 멤버만 복사하며 포인터가 섞여있었다면 좀 복잡해질 수 있다. (얕은 복사)
MyClass::MyClass(const MyClass& x) : a(x.a), b(x.b), c(x.c) {}


MyClass foo;
MyClass bar = foo; // 복사 생성자 호출

멤버들을 복사하는데 포인터가 섞여있는 경우면 같은 포인터를 참고하게 되므로 실수로 한쪽에서 해체하면 런타임에 치명적인 상황을 낳게 된다.
이를 피하기 위한 깊은 복사를 수행하려면 새로운 복사 생성자를 추가해야 한다.

// copy constructor: deep copy
#include <iostream>
#include <string>
using namespace std;

class Example5 {
    string* ptr;
  public:
    Example5 (const string& str) : ptr(new string(str)) {}
    ~Example5 () {delete ptr;}
    // 새로운 복사 생성자
    Example5 (const Example5& x) : ptr(new string(x.content())) {}
    // access content:
    const string& content() const {return *ptr;}
};

int main () {
  Example5 foo ("Example");
  Example5 bar = foo;

  cout << "bar's content: " << bar.content() << '\n';
  return 0;
}

복사 할당자

오브젝트가 생성되는 때에만 다른 오브젝트에서 복사할 수 있는 것은 아니다.

MyClass foo;
MyClass bar (foo);       // 오브젝트 생성: 복사 생성자 호출
MyClass baz = foo;       // 오브젝트 생성: 복사 생성자 호출
foo = bar;               // 복사 할당자 호출됨 

복사, 이동 할당자가 따로 없다면 기본적으로 아래와 같은 복사 할당자가 호출된다.

MyClass& operator= (const MyClass&); // *this를 반환한다.

복사 생성자와 마찬가지로 멤버들을 복사하는 얕은 복사를 수행하므로 멤버 중에 동적 할당으로 다루는 것이 있다면 좀 복잡해진다.
보통 이때도 깊은 복사로 해결할 수 있다.

Example5& operator= (const Example5& x) {
  delete ptr;                      // 원래 다루고 있던 공간 해체 
  ptr = new string (x.content());  // 새로운 문자열 공간을 위한 
  return *this;
}

// 또는 상수가 아닌 경우면 같은 오브젝트를 그냥 다시 쓸 수도 있다.
Example5& operator= (const Example5& x) {
  *ptr = x.content();
  return *this;
}

이동 생성자 / 할당자

임시로 쓰이는 오브젝트에 할당 연선자를 사용하는 경우에 이동이 발생한다. 그리고 복사랑은 다르게 ‘이동’은 원본이 목적지로 넘어가고나서 없어진다.

임시로 쓰이는 오브젝트는 기본적으로 함수의 반환이나 타입 캐스트 등으로 생기는 별도의 식별자가 없는 오브젝트를 의미하는데 이들은 보통 다시 사용되지 않으므로 굳이 이 값들을 ‘복사’할 필요는 없다.

MyClass fn();            // function returning a MyClass object
MyClass foo;             // default constructor
MyClass bar = foo;       // copy constructor
MyClass baz = fn();      // move constructor
foo = bar;               // copy assignment
baz = MyClass();         // move assignment 

멤버 함수 형태로 보면 아래와 같다.

MyClass (MyClass&&);             // move-constructor
MyClass& operator= (MyClass&&);  // move-assignment

여기서 &&는 임시 값을 참조하는 rvalue reference다. 이제 이 참조를 이용해 ‘이동’을 구현하면 아래와 같다.

// move constructor/assignment
#include <iostream>
#include <string>
using namespace std;

class Example6 {
    string* ptr;
  public:
    Example6 (const string& str) : ptr(new string(str)) {}
    ~Example6 () {delete ptr;}
    // move constructor
    Example6 (Example6&& x) : ptr(x.ptr) {x.ptr=nullptr;}
    // move assignment
    Example6& operator= (Example6&& x) {
      delete ptr; 
      ptr = x.ptr;
      x.ptr=nullptr;
      return *this;
    }
    // access content:
    const string& content() const {return *ptr;}
    // addition:
    Example6 operator+(const Example6& rhs) {
      return Example6(content()+rhs.content());
    }
};


int main () {
  Example6 foo ("Exam");
  Example6 bar = Example6("ple");   // move-construction ("ple") -> 생성자의 반환값인 임시 string 오브젝트를 이용한다.

  foo = foo + bar;                  // move-assignment (foo + bar) -> 임시 오브젝트 (operator+ 함수)

  cout << "foo's content: " << foo.content() << '\n';
  return 0;
}

보통 컴파일러에서 이동 생성자가 필요한 상황을 최적화 한다. (Return Value Optimization) 또한 함수에서 반환된 값으로 오브젝트를 초기화한다면 이동 할당자는 호출되지 않는다. (확인 필요)

그리고 rvalue reference의 사용은 이동 생성자 외에는 딱히 쓸만한 곳이 없다. 사용 자체는 모든 함수의 매개변수로 쓸 수는 있지만 작동 방식이 달라서 마구잡이로 남발하다가는 에러가 일어나는 곳을 찾는게 쉽지 않아진다.

암시적 멤버

위의 특별한 멤버 함수들은 C와 초기 버전 C++과의 역호환성을 위해 암시적으로 아래와 같은 상황에 작동된다.

멤버 함수 암시적으로 이 상황에..이렇게 작동됨
기본 생성자다른 생성자가 없는 경우아무것도 안함
소멸자소멸자가 없는 경우 아무것도 안함
복사 생성자다른 이동 생성자나 이동 할당자가 없는 경우모든 멤버 복사
복사 할당자다른 이동 생성자나 이동 할당자가 없는 경우모든 멤버 복사
이동 생성자소멸자나 다른 복사 생성자 그리고 복사, 이동 할당자가 없는 경우모든 멤버 이동
이동 할당자소멸자나 다른 복사 생성자 그리고 복사, 이동 할당자가 없는 경우모든 멤버 이동

그리고 이런 암시적 멤버 함수를 명시적으로 기본적으로 작동하는 것을 설정하거나 지울 수 있다.

function_declaration = default;
function_declaration = delete;
// default and delete implicit members
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle (int x, int y) : width(x), height(y) {}
    Rectangle() = default; // 원래라면 다른 생성자가 있어 기본 생성자가 없어야하지만 지정하여 생겨났다.
    Rectangle (const Rectangle& other) = delete; // 복사 생성자 불가
    // 만약 이 함수를 default로 바꾼다면 원래 암시적으로 쓰일
    // Rectangle::Rectangle (const Rectangle& other) : width(other.width), height(other.height) {}
 형태로 쓰일 수 있게 된다.
    int area() {return width*height;}
};

int main () {
  Rectangle foo;
  Rectangle bar (10,20);

  cout << "bar's area: " << bar.area() << '\n';
  return 0;
}

보통 추후의 호환성을 위해 클래스에 명시적으로 하나의 복사/이동 생성자 또는 복사/이동 할당자를 만든다. 여기서 사용하지 않는 특별 함수들은 명시적으로 선언하는 것이 추천된다.

Friend

private, protect 멤버는 클래스 바깥에서 접근할 수 없지만 이걸 무시할 수 있는 friend 선언이 있다. friend 키워드를 함수나 클래스에 붙여 선언하면 클래스의 멤버 함수는 아닌 채로 값을 사용할 수 있다.

// friend functions
#include <iostream>
using namespace std;

class Rectangle {
    int width, height;
  public:
    Rectangle() {}
    Rectangle (int x, int y) : width(x), height(y) {}
    int area() {return width * height;}
    friend Rectangle duplicate (const Rectangle&); // friend 선언됨 
};

Rectangle duplicate (const Rectangle& param) // 근데 이거 왜 클래스 스코프 없음?
{
  Rectangle res;
  res.width = param.width*2; // param의 멤버 변수는 원래 접근될 수 없지만 friend 선언으로 사용할 수 있게 된다.
  res.height = param.height*2;
  return res;
}

int main () {
  Rectangle foo;
  Rectangle bar (2,3);
  foo = duplicate (bar);
  cout << foo.area() << '\n';
  return 0;
}

friend 함수는 보통 서로 다른 두 클래스의 멤버에 접근하기 위해 쓰인다.

또한 클래스 자체에 friend 클래스를 선언하여 대상 클래스가 자신의 클래스 멤버들을 사용할 수 있도록 할 수 있다.

// friend class
#include <iostream>
using namespace std;

class Square; // Rectangle::convert 함수에서 Square가 읽히기 전에 쓰이므로 프로토타입 선인 필요

class Rectangle {
    int width, height;
  public:
    int area ()
      {return (width * height);}
    void convert (Square a);
};

class Square {
  friend class Rectangle; // Rectangle에서 Square 멤버를 사용할 수 있도록 함
  private:
    int side;
  public:
    Square (int a) : side(a) {}
};

void Rectangle::convert (Square a) {
  width = a.side;
  height = a.side;
}
  
int main () {
  Rectangle rect;
  Square sqr (4);
  rect.convert(sqr);
  cout << rect.area();
  return 0;
}

friend 관계는 한쪽만 또는 양쪽으로 선언할 수 있다.

클래스 간의 상속

한 클래스를 기반으로 그 특성을 유지하여 다른 클래스에 ‘상속’할 수 있다.
상속된 클래스는 기반 클래스의 멤버를 모두 물려받고 자신의 특성에 따라 여기서 덧붙여 추가할 수 있다.

class derived_class_name: public base_class_name // public이 아닌 다른 지정자도 가능하다. (기반 클래스 멤버의 최대 접근 레벨 지정)
// 구조체도 상속이 가능하다. 지정자가 없으면 private로 취급된다.
{ /*...*/ };

위의 코드에서 상속된 클래스의 멤버 접근은 기반 클래스 멤버의 접근 수준보다 높게 설정될 수 없다. (예를 들어, 기반에서 public인 멤버는 상속된 클래스에서 private로 바꿀 수 있지만 반대로는 안된다.)

// derived classes
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b;}
 };

class Rectangle: public Polygon {
  public:
    int area ()
      { return width * height; }
 };

class Triangle: public Polygon {
  public:
    int area ()
      { return width * height / 2; }
  };
  
int main () {
  Rectangle rect;
  Triangle trgl;
  rect.set_values (4,5);
  trgl.set_values (4,5);
  cout << rect.area() << '\n'; // 20
  cout << trgl.area() << '\n'; // 10
  return 0;
}
접근publicprotectedprivate
같은 클래스의 멤버yesyesyes
상속된 클래스의 멤버yesyesno
멤버가 아님yesnono

보통 기반 클래스에 다른 접근 수준이 필요하다면 그냥 멤버 변수로 표현하는게 나을 수 있다.

상속하는 경우, 기반 클래스에서 아래와 같은 멤버들을 가져온다.

  • 생성자, 소멸자
  • 할당 연산자 오버로딩 함수 (operator=) (확인 필요)
  • friend
  • 모든 멤버 변수들

생성자의 경우는 상속된 클래스에서 기반 클래스의 기본 생성자를 호출한다. 다른 생성자를 지정하여 호출하는 것도 가능하다.

derived_constructor_name (parameters) : base_constructor_name (parameters) {...}

class Son : public Mother {
  public:
    Son (int a) : Mother (a)
      { cout << "Son: int parameter\n\n"; }
};

여러 개의 클래스에서 다중 상속하는 것도 가능하다.

class Rectangle: public Polygon, public Output;
class Triangle: public Polygon, public Output;

다형성 Polymorphism

기반 클래스로의 포인터

클래스 상속의 주요 기능 중 하나로써, 상속된 클래스의 포인터를 기반 클래스의 타입으로 변환하도록 할 수 있다.

// pointers to base class
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
};

class Rectangle: public Polygon {
  public:
    int area()
      { return width*height; }
};

class Triangle: public Polygon {
  public:
    int area()
      { return width*height/2; }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon * ppoly1 = &rect; // 기반 클래스인 polygon으로 변경
  Polygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  cout << rect.area() << '\n';
  cout << trgl.area() << '\n';
  return 0;
}

이렇게 기반 클래스 포인터로 바뀌게 되면 상속된 클래스가 아닌 기반 클래스의 멤버만 사용할 수 있게 된다.

가상 멤버 Virtual members

상속된 클래스에서 다시 선언할 수 있도록 하는 멤버 함수 (중요한 다형성 기능)
기반 클래스에서 멤버 함수 선언 앞에 virtual 키워드를 넣어 선언할 수 있고 상속된 클래스에서 같은 이름으로 평범하게 멤버함수를 선언하면 된다.

// virtual members
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area ()
      { return 0; }
};

class Rectangle: public Polygon {
  public:
    int area ()
      { return width * height; }
};

class Triangle: public Polygon {
  public:
    int area ()
      { return (width * height / 2); }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon poly;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  Polygon * ppoly3 = &poly;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  ppoly3->set_values (4,5);
  cout << ppoly1->area() << '\n'; // 20
  cout << ppoly2->area() << '\n'; // 10
  cout << ppoly3->area() << '\n'; // 0
  return 0;
}

기반 클래스와 같은 이름의 멤버함수를 virtual 키워드 없이 선언할 수 있지만 이 경우에는 기반 클래스의 함수만 우선적으로 호출된다.

위의 예제의 경우는 기반 클래스 포인터지만 해당 원래 객체의 비주얼 멤버 함수로 제대로 호출되게 된다.

또한 가상 함수의 구현을 하지 않고 선언만 하여 해당 기반 클래스에 상속될 모든 클래스에서 따로 구현하도록 강제 할 수 있다. (순수 가상 함수)

// abstract class CPolygon
class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area () =0;
 // 함수 선언 옆에 = 0을 붙여 선언한다.
    void printarea()
      { cout << this->area() << '\n'; } // 이때는 역참조 연산자로 포인팅하게 한다.
};

// 이렇게 순수 가상함수가 있는 클래스를 '추상 기본 클래스'라 부른다.
Polygon mypolygon;   // 그리고 이런 '추상 기본 클래스'는 오브젝트에 인스턴스화 할 수 없게 된다. 

// 다만, 여전히 상속된 클래스를 포인터로 가르켜서 멤버들을 다룰 순 있다.
Polygon * ppoly1;
Polygon * ppoly2;

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다