Class

OOP(Object Oriented Programming)의 특성

  • 추상화(Abstraction) : 공통의 기능이나 속성을 묶어 클래스로 정의한다.
  • 캡슐화(Encapsulation) : 객체의 세부 구현을 내부로 감추고 최소한의 인터페이스만 객체 외부로 노출한다.
  • 상속(Inheritance) : 자식 클래스가 부모 클래스의 멤버를 물려받아 확장이 가능하도록 구현한다.
  • 다형성(Polymorphism) : 같은 요청을 받아도 객체에 타입에 다르게 동작하도록 구현한다.

클래스(Class)와 구조체(Struct)의 차이

  • C++에서 classstruct 지정자는 거의 동일하다.
  • 유일한 차이는 class의 디폴트 접근 지시자(Access Identifier)가 private인 반면 struct의 디폴트 접근 지시자는 public이다.
  • Google Style Guide에서는 데이터를 옮기는 목적의 passive object(다른 객체와 상호작용하지 않는 객체)에만 struct를 사용한다고 한다.

Object In Memory(Object Alignment)

  • 클래스 내부 멤버변수를 어떻게 배치하는지에 따라 메모리 크기가 달라진다.
  • 객체 정렬 법칙(Object Alignment Rules)
    • 각 멤버 변수들은 자신들의 사이즈의 배수의 위치에서 시작되어야 한다. (4byte면 4의 배수에서)
    • 클래스의 전체 사이즈는 멤버 변수 중 가장 큰 사이즈의 배수에서 끝이 나야한다. (8byte면 8의 배수에서)
  • 해당 규칙을 맞추기 위해 메모리 사이에 빈 공간(padding)이 존재할 수 있다.
  • 최적화를 위해서(병렬 프로그래밍을 위해 객체의 사이즈가 32의 배수가 되어야할 경우) alignas 지정자를 사용해서 객체의 사이즈를 정해줄 수 있다. (padding을 추가)
class Cat {
public:
  void speak();

private:
  double a; // 8byte
  int b;    // 4byte 
  int c;    // 4byte
};

Cat cat;
sizeof(cat); // 이때 사이즈는 16byte
// (8byte) - (4byte) - (4byte) : 16byte
class Cat {
public:
  void speak();

private:
  int b;    // 4byte
  double a; // 8byte 
  int c;    // 4byte
};

Cat cat;
sizeof(cat); // 이때 사이즈는 24byte
// (4byte) - (4byte padding) - (8byte) - (4byte) - (4byte padding) : 24byte

정적 멤버 함수(Static Member Function)

  • 정적 멤버 함수는 객체를 생성하지 않아도 호출할 수 있다.
  • 기본적으로 클래스의 멤버 변수, 함수들은 객체 자신의 주소를 가리키는 this 키워드와 바인딩 되어 있다.
  • 하지만 static 지정자가 있는 멤버 함수는 this 키워드와 바인딩되어 있지 않아서 객체 생성 없이 사용 가능하다.
  • 대신 내부에서 this 키워드를 사용해야 쓸 수 있는 멤버 변수, 멤버 함수를 사용하지 못한다.
class Cat {
public:
  void speak() {
    std::cout << "speak" << std::endl;
  }
  static void staticSpeak() {
    std::cout << "static speak" << std::endl;
  }

private:
  int _age;
}

Cat::staticSpeak(); // 객체를 생성하지 않고 호출 가능

Cat cat;
cat.speak();
cat.staticSpeak(); // 멤버 함수니까 당연히 객체에서도 호출할 수 있다.

정적 멤버 변수(Static Member Variable)

  • 멤버 변수를 static 선언하면 이는 해당 클래스로 생성되는 객체들이 모두 공유할 수 있는 변수라는 뜻이다.
  • 스택이나 힙 영역에 저장되는 것이 아닌 데이터 영역에 저장된다.
class Cat {
public:
  void speak() {
    count++;
    std::cout << count << std::endl;
  }

private:
  static int count; // public으로 두면 외부에서 변경이 가능하다.
  int _age;
}

Cat::count = 0; // 프로그램이 실행되기 전에 초기화를 시켜줘야 한다.

Cat kitty;
Cat nabi;
kitty.speak(); // 1
nabi.speak();  // 2

// 이렇게 하면 멤버 변수에 포함시킬 필요없이 static 변수를 사용할 수 있다.
// 초기화 시점은 달라질 수 있다.
class Cat {
public:
  void speak() {
    static int count = 0;
    count++;
    std::cout << count << std::endl;
  }

private:
  int _age;
}

생성자 초기화 리스트(Member Initialized List)

  • 생성자 호출과 동시에 멤버 변수를 초기화 하는 방식
  • (생성자 이름) : var1(arg1), var2(arg2) {}
  • 생성자 초기화 리스트를 사용하면 생성과 초기화를 동시에 수행한다.
  • 상수와 참조자는 생성과 동시에 초기화 되어야 하기 때문에 사용하기 적합하다.
  • 멤버 변수가 클래스일 경우 불필요한 디폴트 생성을 할 필요가 없는 장점이 있다.
class Marine {
public:
  Marine();

private:
  int hp;
  int coord_x, coord_y;
  const int damage;
  bool is_dead;
  Cat cat;
}


// 기존 생성자
// 생성을 하고 그 다음 멤버 변수에 대입
Marine::Marine() {
  hp = 50;
  coord_x = coord_y = 0;
  damage = 5;
  is_dead = false;
  // 생성자가 호출되기 전에 이미 cat은 디폴트 생성자를 호출한다.
}

// 생성자 초기화 리스트
// 생성과 초기화를 동시에 수행
// cat은 디폴트 생성자를 호출하지 않고 바로 5를 인자로 하는 생성자를 호출한다.
Marine::Marine() : hp(50), coord_x(0), coord_y(0), damage(5), is_dead(false), cat(5) {}

중괄호 초기화(Uniform Initialization)

  • C++11부터 생성자 호출과 함수 정의를 혼돈하지 않기 위해 () 대신 {}를 사용하는 중괄호 초기화를 지원한다.
  • 중괄호 초기화 사용 시 장점
    • 새로운 객체 생성 표현 가능
      • e.g. std::vector에서 사용하는 std::initialize_list 생성자
    • non-static 멤버 변수 초기화
      • C++11부터는 =로 초기화해도 문제 없다.
    • 복사할 수 없는 객체 초기화
      • e.g. std::atomic도 초기화할 수 있다.
    • narrow conversion 방지
      • 암시적 형변환을 컴파일 에러로 감지한다.
    • most vexing parse에서 자유로움
      • e.g. A a();와 같이 객체를 생성하는 것인지 함수를 정의하는 것인지 혼동을 주지 않는다.
  • 중괄호 초기화 사용 시 주의할 점
    • auto와 함께 사용하면 std::initializer_list로 타입이 추론된다.
    • 생성자에 std::initializer_list가 매개변수로 존재하면 오버로딩 우선순위가 매우 높다.
    • std::initializer_list 생성자와 다른 생성자가 다르게 구현되었을 경우에 대해 고려해야 한다.
A a(); // A 클래스 타입을 반환하는 a 함수 정의
A a{}; // A 클래스 타입을 가지는 a 객체 생성


// vector에서는 생성자에 std::initializer_list를 써서 {} 사용에 주의해야 한다.
std::vector<int> v1(10, 20); // 10의 값을 가지는 20개의 원소 생성
std::vector<int> v2{10, 20}; // 10, 20 값을 가지는 2개의 원소 생성

초기화 리스트(Initializer List)

  • C++11부터 중괄호 초기화를 사용할 때 {} 안의 데이터를 인자로 받을 수 있는 std::initializer_list가 추가되었다.
A(std::initializer_list<int> n) {}
A a = {1, 2, 3};

복사 생성자(Copy Constructor)

  • 복사 생성자의 목적은 객체의 멤버 변수 값들을 복사하여 새로 메모리를 할당하는 것이다.
  • 복사 생성자는 T(const T& a); 구조를 가지고 있다.
  • 만약 클래스가 멤버 변수로 포인터를 가지고 있으면 디폴트 복사 생성자를 사용할 경우 문제가 발생할 수 있다.
    • 디폴트 복사 생성자를 사용하면 복사된 객체가 동적으로 할당한 변수의 주소값만 복사하여 같은 메모리를 두 개의 포인터가 가리키게 된다.
    • 이는 객체가 파괴될 때 메모리 해제가 두 번 발생하는 문제가 발생 한다.
    • 이렇게 단순히 대입만 해주는 복사를 얕은 복사(Shallow Copy)라고 한다.
  • 이를 해결하기 위해서는 복사 생성자를 만들어 변수에 새로 메모리를 할당해 주면 된다.
    • 이를 깊은 복사(Deep Copy)라고 한다.
// 빈 공간을 할당해서 복사를 해야된다. (임시 객체 메모리는 소멸자로 해제한다.)
MyString::MyString(const MyString &str) {
  std::cout << "복사 생성자 호출 ! " << std::endl;
  string_length = str.string_length;
  memory_capacity = str.string_length;
  string_content = new char[string_length];

  for (int i = 0; i != string_length; ++i)
    string_content[i] = str.string_content[i];
}

이동 생성자(Move Constructor)

  • 이동 생성자의 목적은 객체의 멤버 변수 주소값을 가져오는 것이다. (말그대로 옮기는 것)
  • 이동 생성자는 T(T&& a); 구조를 가지고 있다.
  • 멤버 변수로 포인터가 있으면 포인터 주소를 넘겨주고 넘겨준 객체는 포인터가 nullptr가 되어야 한다.
  • 즉, 소유권을 이전한다고 표현할 수 있다.
  • 객체를 C++ 컨테이너(ex. std::vector)에 넣을 때는 이동 생성자에 반드시 noexcept로 명시해야 한다.
    • 안그러면 컨테이너 내부에서 재할당이 발생할때 이동 연산이 아닌 복사 연산을 수행되어 성능이 떨어진다.
// 문자열 전체를 복사할 필요 없이 임시 객체가 기리키고 있던 문자열로 바꿔주면 된다.
MyString::MyString(MyString&& str) {
  std::cout << "이동 생성자 호출 !" << std::endl;
  string_length = str.string_length;
  string_content = str.string_content;
  memory_capacity = str.memory_capacity;

  // 임시 객체 소멸 시에 메모리를 해제하지 못하도록 포인터를 뺏어야 한다.
  str.string_content = nullptr;
}

복사 할당 연산자(Copy Assignment Operator)

  • =(등호)를 객체를 생성할 때 쓰면 생성자가 호출되고 이미 생성된 객체끼리 쓰면 할당 연산자가 호출한다.
  • 복사 할당 연산자는 T& operator=(const T&); 구조를 가지고 있다.
  • 자기 자신을 복사 할당하는 상황은 막아야 한다.
class Cat {
public:
  Cat& operator=(const Cat& cat) {
    if(&cat == this){
      return *this;
    }
    _name = cat._name;
    _age = cat._age;
    std::cout << "Copy Assignment" << std::endl;
    return *this;
  }

private:
  std::string _name;
  int _age;
};

이동 할당 연산자(Move Assignment Operator)

  • 이동 할당 연산자는 T& operator=(T&&); 구조를 가지고 있다.
  • 자기 자신을 이동 할당하는 상황은 막아야 한다.
class Cat {
public:
  Cat& operator=(Cat&& cat) noexcept {
    if(&cat == this){
      return *this;
    }
    _name = std::move(cat._name);
    _age = cat._age;
    std::cout << "Move Assignment" << std::endl;
    return *this;
  }

private:
  std::string _name;
  int _age;
};

특수 멤버 함수(Special Member Function)

  • 특수 멤버 함수는 컴파일러가 자동으로 생성해주는 멤버 함수이다.
  • 특수 멤버 함수의 자동 생성 조건은 다음과 같다. (Rule of three/five/zero)
    • 디폴트 생성자
      • 클래스에 사용자가 직접 선언한 생성자가 없는 경우
    • 소멸자
      • 클래스에 사용자가 직접 선언한 소멸자가 없는 경우
    • 복사 생성자와 복사 대입 연산자
      • 클래스에 사용자가 직접 선언한 이동 생성자 혹은 이동 대입 연산자가 없는 경우
      • 복사 생성자와 복사 대입 연산자 중 하나를 선언해도 컴파일러가 나머지도 생성한다.
    • 이동 생성자와 이동 대입 연산자
      • 클래스에 사용자가 직접 선언한 소멸자, 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자가 없는 경우 (이 중 하나라도 있으면 자동 생성되지 않는다.)
      • 이동 생성자와 이동 연산자 중 하나를 선언하면 컴파일러가 나머지를 생성해주지 않는다.
  • 자동 생성 조건이 아니더라도 = default를 사용하여 특수 멤버 함수를 명시적으로 사용할 수 있다.

연산자 오버로딩(Operation Overloading)

  • 연산자를 재정의해서 사용하는 방식 (C에서는 불가능했는데 C++에서 가능)
  • 클래스의 멤버 함수거나 전역 함수이어야 한다.
  • 멤버 함수로 오버로드된 연산자의 첫번째 인자는 무조건 해당 객체 타입이어야 한다.
class Cat {
public:
  // 멤버 함수 연산자 오버로딩
  bool operator==(const Cat& cat) const{
    return this->getAge() && cat.getAge();
  }
  int getAge() const {
    return _age;
  };

private:
  string _name;
  int _age;
};

// 전역 함수 연산자 오버로딩
bool operator==(const Cat& in, const Cat& out) {
    return in.getAge() == out.getAge();
}

명시적(explicit) 캐스팅

  • static_cast : 일반적인 타입 변환
  • const_cast : 객체의 constness를 없애는 타입 변환
  • dynamic_cast : 파생 클래스 사이에서 다운 캐스팅
  • reinterpret_cast : 서로 관련 없는 포인터들끼리 캐스팅


Inheritance

상속(Inheritance)

  • 자식 클래스가 부모 클래스의 멤버를 물려받아 확장이 가능한 구조를 말한다.
class A {
 public:
  int x;
 protected:
  int y;
 private:
  int z;
};

class B : public A {
  // x is public
  // y is protected
  // z is not accessible from B
};

class C : protected A {
  // x is protected
  // y is protected
  // z is not accessible from C
};

// 'private' is default for classes
class D : private A {
  // x is private
  // y is private
  // z is not accessible from D
};

상속 관계에서 생성자/소멸자 순서

  • 부모 클래스의 생성자 → 자식 클래스 생성자 → 자식 클래스 소멸자 → 부모 클래스 소멸자

함수 재정의(overriding) 조건

  • 부모 클래스 함수가 반드시 가상 함수이어야 한다.
  • 부모 클래스 함수와 자식 클래스 함수의 이름이 동일해야 한다. (소멸자는 예외)
  • 부모 클래스 함수와 자식 클래스의 함수의 매개변수 타입이 동일해야 한다.
  • 부모 클래스 함수와 자식 클래스 함수의 상수성(constness)가 동일해야 한다.
  • 부모 클래스 함수와 자식 클래스 함수의 반환 타입과 예외 지정이 동일해야 한다.
  • 부모 클래스 함수와 자식 클래스 함수의 참조 한정사(reference qualifier)가 동일해야 한다.

동적 다형성(Dynamic Polymorphism)

  • 부모 클래스의 포인터를 만들고 런타임에 부모 클래스나 자식 클래스를 동적 할당하는 방식이다.
    • 하나의 클래스 포인터로 여러 클래스를 가리킬 수 있다. (동적 다형성)
    • 컴파일 타임이 아닌 런타임에 클래스를 정할 수 있다.
  • 상속과 virtual 지정자가 필수이다.
    • 컴파일러는 포인터 타입을 보고 호출할 함수를 결정하기 때문에 부모 클래스에 virtual 지정자를 사용하여 가상 테이블(virtual table)을 가리키는 포인터를 생성해야 한다.
      • 가상 테이블 : 컴파일러가 호출해야 하는 함수들의 주소가 들어있는 테이블이다.
      • 가상 테이블이 없으면, 부모 클래스 포인터로 자식 클래스를 가리킬 때 자식 클래스의 메서드나 소멸자를 호출할 수 없다.
class Animal {
public:
  virtual void eat() {};
  virtual void sleep() {};
  virtual ~Animal() = default;

private:
  std::string _name;
}

class Cat : public Animal {
public:
  void eat() override {};
  void sleep() override {};
  void jump();

private:
  int _age;
}

// Animal 데이터와 virtual table 포인터를 가진 크기
Animal* ani = new Animal();
ani->eat();
ani->sleep();

// Animal 데이터 + Cat 데이터, virtual table 포인터를 가진 크기
// Animal 포인터를 사용하기 때문에 Animal 클래스에 없는 메서드는 호출할 수 없다.
Animal* cat = new Cat();
cat->eat();
cat->sleep();
cat->jump(); // 불가능, Animal 포인터는 해당 메서드를 모른다.

추상 클래스(Abstract Class)

  • virtual 지정자를 붙이고 뒤에 = 0을 붙이면 순수 가상 함수(pure virtual function)라 부른다.
  • 순수 가상 함수가 있는 클래스를 추상 클래스라고 부른다.
  • 추상 클래스는 객체 생성을 못한다. (상속을 통해 사용해야 한다.)

인터페이스(Interface)

  • C++에서는 Interface 지정자가 없다. 따라서 순수 가상 함수만 가지고 있는 추상 클래스를 인터페이스라 부르는 경우도 있다.
  • 좋은 인터페이스는 멤버 변수와 구현이 없는 것이 좋다.

다중 상속(Multiple Inheritance)

  • C++에서는 클래스를 다중 상속 받을 수 있다.

다이아몬드 상속(Diamond Inheritance)

  • 다중 상속이 가능하면 당연히 같은 부모 클래스를 가진 두 자식 클래스를 만들고 이 두 자식 클래스를 다중 상속받는 자식 클래스를 만들어 다이아몬드 상속을 받을 수 있다.
  • 이때 의도하지 않게 제일 위의 부모 클래스가 두번 호출될 수 있다.
  • 이러한 문제를 막기 위해 같은 부모 클래스를 가지는 두 자식 클래스에 virtual 지정자를 붙여서 부모 클래스가 두번 호출되는 걸 막을 수 있다.

Dynamic Cast

  • 업 캐스팅(Up Casting) : 자식 클래스에서 부모 클래스로 변환
  • 다운 캐스팅(Down Casting) : 부모 클래스에서 자식 클래스로 변환
  • dynamic_cast를 통해 클래스를 변환할 수 있다.
  • dynamic_cast를 사용하면 virtual table을 확인해서 해당 타입으로 변환이 가능한지 확인하고 불가능하면 nullptr을 반환한다.
  • 이러한 캐스팅을 RTTI(Run Time Type Information)이라고 하는데 이를 사용하지 않는 것이 좋다고 한다.
// Animal이라는 부모 클래스를 Dog, Cat이라는 자식 클래스가 상속 받았다고 가정한다.
// Animal은 공통 함수 walk()을 가지고 있고 Dog, Cat은 각각 bark(), meow() 함수가 있다고 가정한다.
Animal* animal = new Dog();
animal->walk();
animal->bark(); // 불가능, 포인터 형이 Animal

// 이렇게 가능하지만 매우 위험한 방법
Dog* dog = static_cast<Dog*>(animal);
dog->walk();
dog->bark();

// dynamic cast를 쓰면 부적절한 변환으면 nullptr을 반환한다.
// Animal 객체를 동적 할당해놓고 Cat, Dog로 다운 캐스팅하거나
// Dog 객체를 동적 할당해놓고 Cat으로 캐스팅하면 nullptr 
Cat* cat = dynamic_cat<Cat*>(animal);
if(cat == nullptr) {
  std::cout << "error" << std::endl;
}

상속에서 발생할 수 있는 문제점

  • 이 두 가지 문제는 인터페이스(순수 가상 함수로만 이루어진 클래스)로 구현하면 발생하지 않는다.
  • Object Slicing
    • 자식 클래스 객체를 생성하고 부모 클래스 레퍼런스로 가리키면 문제가 없는데 그냥 부모 클래스로 =(할당)을 해버리면 자식 클래스에 있는 내용이 잘려나간다.
    • 그 이유는 부모 클래스가 자식 클래스를 복사 생성하는 과정에서 부모 클래스가 가질 수 있는 정보만 복사되기 때문이다. (virtual table은 복사되지 않는다.)
    • 이를 방지하기 위해 복사 생성자를 delete하는 방법이나 protected로 보내는 방법이 있다. (문제는 같은 클래스 간의 복사 생성도 막는다.)
// Cat은 meow()라는 함수를 가지고 있다.
Cat cat = new Cat();

Animal& aniR = cat;
ani->meow();

Animal aniC = cat; // 복사 생성자가 호출되어 Animal 정보만 복사한다.
ani->meow(); // 에러 발생
  • Operator Overloading
    • 만약 부모 클래스에 대한 연산자만 정의되어 있으면 자식 클래스끼리 연산을 하면 알아서 암시적 형 변환되어 부모 클래스 연산자를 사용하여 의도치 못한 결과를 만든다.
    • 추상 클래스에서 연산자를 구현하지 않고 상속 받은 클래스에서 구현하는 것을 추천한다.
bool operator==(Animal& first, Animal& second) {
  return first.eyes == second.eyes;
}

Cat cat1 = new Cat{10};
Cat cat2 = new Cat{9};

if(cat1 == cat2) cout << "same" << endl; // 호출된다. (둘다 상속받은 부모 클래스 변수가 같다.)
// 자식 클래스 연산자가 없어 암시적 형변환되어 부모 클래스 연산자를 사용한다.

// 자식 클래스에 대한 연산자도 추가해줘야 한다.
bool operator==(Cat& first, Cat& second) {
  return first.age == second.age;
}


카테고리:

업데이트: