C++ 이것저것 1
클래스
데이터와 그 데이터를 다루는 함수를 하나로 묶은 ‘사용자 정의 자료형’
사실 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”] → 실제 함수 실행
- 가상 함수 테이블(VTable)?
// 다형성의 흔한 예시
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
};
이러한 구현을 사용하는 이유?
-
- 캡슐화
- 직접 접근을 차단하여 잘못된 값 입력 방지 가능
- 캡슐화
// Setter로 hp 값이 음수로 들어오는 경우를 막는다
void setHP(int value) {
if (value < 0) value = 0;
hp = value;
}
- 내부 구현의 변경에도 외부 코드의 영향 최소화
// 나중에 hp를 계산식으로 바꿔도
int getHP() const {
return hp + armor_bonus;
}
- 함수이기에 내부에 디버깅,로깅 등의 장치 설정 가능
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
-
- A* p
- 일반 포인터이며 p로 가리킨 A 객체와 포인터 자신인 p 값도 변경 가능
- A* p
-
- const A* p
- p가 가리키는 A를 변경할 수 없다고 보장한 포인터이다
다만 p는 여전히 변경 가능
- const A* p
-
- A* const p
- p 자체는 다른 객체를 가리킬 수 없다고 보장한 포인터
다만 *p 인 A는 변경 가능
- A* const p
-
- const A * const p
- p와 A 모두 변경할 수 없는 포인터
- const A * const p
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++ 은 지속적으로 새로운 시스템을 도입하고 있고,
특유의 강력한 성능과 함께 아직까지 현역으로 존재하는 언어이다
댓글남기기