5 분 소요

클래스

데이터와 그 데이터를 다루는 함수를 하나로 묶은 ‘사용자 정의 자료형’
사실 Struct와 Defalut 접근 제어자가 다를 뿐 나머지는 같음

  • Class는 private, Struct 는 Public
  • C에서는 Struct에 변수만을 넣어 다루었기에 C++에서도 Struct를 그렇게 사용하는 편이 많다
class Player {
public:
    int hp;
    void TakeDamage(int dmg) {
        hp -= dmg;
    }
};

이러한 class의 개념이 OOP(Object-Oriented Programming)의 핵심 기능을
구현하는데 도움을 줄 수 있었음

요소 설명
캡슐화 데이터 + 함수 묶기 (private, public 등 접근 제어로 은닉 가능)
상속 기존 클래스를 확장 (class B : public A)
다형성 공통 인터페이스로 다양한 객체 처리 (virtual, override)

상속

기존 클래스(base, 부모 라고도 함)의 속성(Property)과 기능(Function)을
새로운 클래스(Derived, 자식)이 물려받는 것

class Animal {
public:
    void Eat() {}
    virtual void Speak() { std::cout << "Animal sound\n"; }
};

class Dog : public Animal {
public:
    void Speak() override { std::cout << "Bark!\n"; }
};

Animal* a = new Dog();
a->Speak(); // ➜ "Bark!" ← 자식의 것이 호출됨

상속을 사용하는 이유는 다음과 같다

  • 코드 재사용성 증가 : 공통 기능을 부모에서 정의하고, 자식들이 상속받게 함
  • 계층적 구조 성립 : is-a 관계를 표현하여 ‘구현’적인 측면에 도움을 줄 수 있음 (Dog is a Animal)
  • 다형성(OOP)의 기반 : 같은 행동이지만 다르게 동작하는 것
    (같은 Animal이지만 Dog 은 ‘멍멍’, 고양이는 ‘야옹’ 하고 울 수 있듯
    하지만 둘다 ‘운다’라는 행동)

Virtual 과 Override

키워드 사용 위치 설명
virtual 부모 클래스의 함수 선언 해당 함수가 다형성 대상임을 나타냄
override 자식 클래스의 함수 정의 부모의 virtual 함수에 대한 정확한 재정의임을 명시

override 자체는 ‘선택사항’이지만 가독성을 위해 권장됨(휴먼 에러 방지)
(없어도 암묵적으로 처리되긴함)

반대로 Virtual 키워드가 없는데 자식에서 override를 쓰면 컴파일 에러가 발생한다
두 키워드 자체를 쓰지 않는 경우는
클래스 타입을 따라가게 된다

Derived d;
d.Foo();   // Derived::Foo() 호출
Base* b = &d;
b->Foo();  // Base::Foo() 호출 (다형성 없음)

(포인터는 아래 쪽에서 다룰 예정)

  • 가상 함수 테이블(VTable)?
    ‘클래스’마다 존재하는 virtual 함수의 ‘주소 테이블’
    (virtual 함수가 존재해야만 생성)
    다형성이 구현될 수 있는 이유이며, 실제 객체의 클래스 타입을 판별하여
    VTable에 있는 함수 주소로 찾아가 해당 함수를 호출한다
    ex) obj->Func(); → obj->vptr → vtable[“Func”] → 실제 함수 실행

// 다형성의 흔한 예시
class Monster {
public:
    virtual void Attack() = 0; // 순수 가상 함수
};

class Goblin : public Monster {
public:
    void Attack() override { std::cout << "Goblin attacks!\n"; }
};

class Dragon : public Monster {
public:
    void Attack() override { std::cout << "Dragon breathes fire!\n"; }
};

std::vector<Monster*> monsters = { new Goblin(), new Dragon() };
for (auto* m : monsters) {
    m->Attack();  // ➜ Goblin or Dragon에 따라 다른 결과
}

  • 드래곤과 고블린은 모두 ‘Monster’ 클래스를 상속받고 Player를 공격하는 Function이 존재
  • 그럼에도 두 개체 간의 ‘공격’에 대한 세부적 차이가 존재할 수 있기에
    실제 구현은 각각의 ‘클래스’에서 Override 하여 사용

접근 제어자(Access Specifier)

외부에서 ‘접근이 가능한 수준’을 정의하는 키워드
(상속, 멤버 함수, 멤버 변수 등의 위치에 사용된다)

C++의 접근 제어자는 3가지이다

제어자 의미
public 어디서든 접근 가능
protected 해당 클래스 + 파생 클래스에서만 접근 가능
private 해당 클래스 내부에서만 접근 가능

이러한 접근 제어자를 사용함으로서

  • 해당 클래스에 ‘기대한 함수’만을 공개하기에 클래스의 존재를 명확하게 함
  • 외부에서 직접 값을 변경하려는 시도를 막아 잘못된 실수를 막음
  • 내부 구현을 변경함에 따라, 외부 코드의 영향을 최소화함
  • 로직의 내부/외부 가 명확해지기에 시각적인 역할 분리 가능
    => 캡슐화 (외부에 내부 노출을 숨겨 데이터를 보호하며, 외부는 필요한 기능만을 제공받음)
class Player {
private:
    int hp;  // 외부에서 직접 접근 불가

protected:
    int level;  // 자식 클래스에서는 접근 가능

public:
    Player() : hp(100), level(1) {}

    void TakeDamage(int dmg) {
        hp -= dmg;
    }

    int GetHP() const {
        return hp;
    }
};

상속에서도 사용된다

class Derived : public Base    // public 상속
class Derived : protected Base // protected 상속
class Derived : private Base   // private 상속

이 경우 의미가 조금 달라진다

부모의 접근 수준 ↓
상속 방식 →
public 상속 protected 상속 private 상속
public 멤버 public로 유지 protected로 바뀜 private로 바뀜
protected 멤버 protected로 유지 protected로 유지 private로 바뀜
private 멤버 ❌ 접근 불가 (상속은 됨) ❌ 접근 불가 ❌ 접근 불가

Getter/Setter

클래스의 멤버 변수에 안전하게 접근하거나 수정할 수 있도록 하는 함수들
언어적 기능이 아닌 일종의 약속이다
Getter : 값을 읽는 함수(GetHP 등)
Setter : 값을 설정하는 함수 (SetHP 등)

class Player {
private:
    int hp;

public:
    int getHP() const { return hp; }       // getter
    void setHP(int value) { hp = value; }  // setter
};

이러한 구현을 사용하는 이유?

  1. 캡슐화
    직접 접근을 차단하여 잘못된 값 입력 방지 가능
// Setter로 hp 값이 음수로 들어오는 경우를 막는다
void setHP(int value) {
    if (value < 0) value = 0;
    hp = value;
}

  1. 내부 구현의 변경에도 외부 코드의 영향 최소화
// 나중에 hp를 계산식으로 바꿔도
int getHP() const {
    return hp + armor_bonus;
}

  1. 함수이기에 내부에 디버깅,로깅 등의 장치 설정 가능
void setHP(int value) {
    std::cout << "HP 변경됨: " << value << "\n";
    hp = value;
}

포인터

‘주소값’을 저장하는 변수
(사실 연관된 개념이 매우 많기에 가능한 정의와 사용법에 집중하려 한다
메모리와 주소(CS), 업/다운 캐스팅, 동적 메모리, 깊은/얕은 복사,
댕글리 포인터, 스마트 포인터 등등은 나중에 다룰 수 있을때 다룰 예정)

  • ‘*‘를 통해 포인터 타입을 선언하고, 사용할때는 *를 통해 역참조를 하여 그 값에 존재하는 걸 가져온다
  • 포인터는 결국 ‘주소값’이며 이는 0x…. 이런식의 값이기에 어떠한 포인터 타입을 선언해도
    기본적으론 비슷한 바이트수를 가짐
  • 무언가를 ‘가리킨다’ 라는 개념이기에 ‘해당 주소값’을 찾아가서
    가지고 있는 값을 ‘포인터 타입’으로 캐스팅하여 사용한다는 개념
    (ex : 0x… 에 들어있는 값은 결국 2진수로 되어있는 값이기에
    그 값을 어떻게 해석할지를 ‘포인터 타입’으로 규정하고 읽는다)
    (int a = 10; 을 char* cPtr = &a; 로 char 타입으로 읽을 수 있음)
  • 결국 포인터 자신도 포인터 타입이 될 수 있다
    이를 다중 포인터라 한다
    (ex : int** pp; int*** ppp;)
// 대표적인 포인터 예시
int a = 10;
int* p = &a;  // a의 주소를 저장
cout << *p; // p를 역참조한 a 값을 출력

const pointer?

const 자체는 ‘변경할 수 없음’을 보장하는 키워드이며
이는 pointer에도 사용이 가능하다

그런데 어디에 붙이냐에 따라 그 사용방식이 달라지는 점이 point

  1. A* p
    일반 포인터이며 p로 가리킨 A 객체와 포인터 자신인 p 값도 변경 가능
  2. const A* p
    p가 가리키는 A를 변경할 수 없다고 보장한 포인터이다
    다만 p는 여전히 변경 가능
  3. A* const p
    p 자체는 다른 객체를 가리킬 수 없다고 보장한 포인터
    다만 *p 인 A는 변경 가능
  4. const A * const p
    p와 A 모두 변경할 수 없는 포인터

TMI : 모던 C++

C++ 11 부터 모던 C++이라 부르며
여러 기능들이 추가되었음

버전 주요 기능
C++11 auto, 람다, unique_ptr, nullptr, enum class, override
C++14 generic lambda, decltype(auto), make_unique
C++17 if constexpr, structured bindings, std::optional, string_view
C++20 코루틴, Concepts, ranges, consteval, modules
C++23 std::expected, deducing this, multidimensional subscript

이러한 기능들의 추가로 C++은 다양한 강점을 가지게 됨

장점 설명
코드 간결성 반복되는 타입 명시 생략, 추론 활용
성능 개선 move semantics, constexpr, noexcept
버그 줄이기 스마트 포인터, nullptr, enum class
표현력 향상 람다, 커스텀 연산자, ranges, concepts
대형 시스템 적합 모듈화, 템플릿 메타프로그래밍 향상

C++ 은 지속적으로 새로운 시스템을 도입하고 있고,
특유의 강력한 성능과 함께 아직까지 현역으로 존재하는 언어이다

댓글남기기