C++ 클래스 문법
C++의 클래스는 데이터와 함수를 하나로 묶어서 새로운 자료형을 만드는 문법이다.
이 문서는 클래스의 가장 기본적인 형태부터 시작해서 생성자, 접근 제어, 캡슐화, 상속, virtual 키워드까지 단계별로 학습한다.
0. 학습 목표
이 문서를 끝까지 학습하면 다음 내용을 설명하고 사용할 수 있어야 한다.
class를 정의하고 객체를 만들 수 있다.- 멤버 변수와 멤버 함수를 구분할 수 있다.
public,private,protected의 차이를 이해한다.- 생성자와 소멸자의 실행 시점을 안다.
this,const멤버 함수,static멤버를 사용할 수 있다.- 상속을 사용해 기존 클래스를 확장할 수 있다.
virtual,override, 순수 가상 함수, 가상 소멸자의 필요성을 이해한다.
1. 클래스의 가장 기본 형태
#include <iostream>
using namespace std;
class Student {
public:
string name;
int age;
void introduce() {
cout << "이름: " << name << ", 나이: " << age << '\n';
}
};
int main() {
Student s;
s.name = "Yoma";
s.age = 20;
s.introduce();
}핵심
class 클래스이름 {
public:
멤버 변수
멤버 함수
};class Student는Student라는 새로운 자료형을 만든다.name,age는 멤버 변수이다.introduce()는 멤버 함수이다.- 객체를 만든 뒤
.연산자로 멤버에 접근한다.
Student s;
s.name = "Jamcoding";
s.introduce();확인 문제
Car클래스를 만들고brand,speed멤버 변수를 추가해 보자.showInfo()멤버 함수를 만들어 자동차 정보를 출력해 보자.
2. class와 struct의 차이
C++에서는 struct도 멤버 함수와 생성자를 가질 수 있다.
struct Point {
int x;
int y;
};
class Player {
int hp;
};가장 큰 기본 차이는 접근 권한의 기본값이다.
| 문법 | 기본 접근 권한 |
|---|---|
struct | public |
class | private |
struct A {
int value; // public
};
class B {
int value; // private
};정리
- 단순히 데이터를 묶는 용도라면
struct를 자주 사용한다. - 데이터 보호와 기능을 함께 설계할 때는
class를 자주 사용한다.
3. 접근 지정자
클래스 내부의 멤버는 접근 범위를 정할 수 있다.
class BankAccount {
private:
int balance;
public:
void deposit(int money) {
if (money > 0) {
balance += money;
}
}
int getBalance() {
return balance;
}
};접근 지정자 종류
| 접근 지정자 | 의미 |
|---|---|
public | 클래스 외부에서 접근 가능 |
private | 클래스 내부에서만 접근 가능 |
protected | 클래스 내부와 자식 클래스에서 접근 가능 |
왜 private를 사용할까?
다음 코드는 위험하다.
class BankAccount {
public:
int balance;
};
int main() {
BankAccount account;
account.balance = -100000;
}잔액이 음수가 되는 이상한 상태를 막을 수 없다.
그래서 보통 멤버 변수는 private으로 숨기고, 멤버 함수를 통해 안전하게 변경한다.
class BankAccount {
private:
int balance = 0;
public:
void deposit(int money) {
if (money <= 0) return;
balance += money;
}
bool withdraw(int money) {
if (money <= 0) return false;
if (balance < money) return false;
balance -= money;
return true;
}
int getBalance() const {
return balance;
}
};확인 문제
HpPotion클래스를 만들고amount를private으로 선언해 보자.use()함수를 통해서만 포션을 사용할 수 있게 만들어 보자.
4. 생성자
생성자는 객체가 만들어질 때 자동으로 호출되는 함수이다.
class Player {
private:
string name;
int hp;
public:
Player(string playerName, int playerHp) {
name = playerName;
hp = playerHp;
}
void show() const {
cout << name << " HP: " << hp << '\n';
}
};
int main() {
Player p("Knight", 100);
p.show();
}생성자의 특징
- 클래스 이름과 같은 이름을 가진다.
- 반환형이 없다.
- 객체 생성 시 자동 호출된다.
Player p("Knight", 100);이 줄에서 Player(string playerName, int playerHp) 생성자가 호출된다.
5. 생성자 초기화 리스트
C++에서는 생성자 본문에서 대입하는 방식보다 초기화 리스트를 자주 사용한다.
class Player {
private:
string name;
int hp;
public:
Player(string playerName, int playerHp)
: name(playerName), hp(playerHp) {
}
};다음 두 코드는 비슷해 보이지만 동작 방식이 다르다.
Player(string playerName, int playerHp) {
name = playerName;
hp = playerHp;
}Player(string playerName, int playerHp)
: name(playerName), hp(playerHp) {
}초기화 리스트는 멤버 변수를 처음부터 원하는 값으로 생성한다.
초기화 리스트가 꼭 필요한 경우
const 멤버 변수는 생성 후 값을 바꿀 수 없으므로 초기화 리스트가 필요하다.
class Item {
private:
const int id;
public:
Item(int itemId)
: id(itemId) {
}
};6. 기본 생성자와 생성자 오버로딩
매개변수가 없는 생성자를 기본 생성자라고 한다.
class Monster {
private:
string name;
int hp;
public:
Monster()
: name("Unknown"), hp(10) {
}
Monster(string monsterName, int monsterHp)
: name(monsterName), hp(monsterHp) {
}
void show() const {
cout << name << " HP: " << hp << '\n';
}
};
int main() {
Monster a;
Monster b("Slime", 30);
a.show();
b.show();
}핵심
같은 이름의 생성자를 여러 개 만들 수 있다. 매개변수의 개수나 타입이 다르면 C++이 적절한 생성자를 선택한다.
7. 소멸자
소멸자는 객체가 사라질 때 자동으로 호출되는 함수이다.
class Resource {
public:
Resource() {
cout << "자원 획득\n";
}
~Resource() {
cout << "자원 해제\n";
}
};
int main() {
Resource r;
}소멸자의 특징
- 클래스 이름 앞에
~를 붙인다. - 반환형이 없다.
- 매개변수를 가질 수 없다.
- 객체가 스코프를 벗어나면 자동 호출된다.
~Resource() {
cout << "자원 해제\n";
}언제 중요할까?
동적 메모리, 파일, 네트워크 연결처럼 직접 해제해야 하는 자원을 관리할 때 중요하다.
8. this 포인터
this는 현재 객체 자기 자신을 가리키는 포인터이다.
class Player {
private:
string name;
int hp;
public:
Player(string name, int hp) {
this->name = name;
this->hp = hp;
}
};매개변수 이름과 멤버 변수 이름이 같을 때 this->를 사용하면 구분할 수 있다.
this->name = name;this->name: 멤버 변수name: 매개변수
초기화 리스트를 사용하면 this를 자주 쓰지 않아도 된다.
Player(string name, int hp)
: name(name), hp(hp) {
}9. const 멤버 함수
객체의 값을 바꾸지 않는 멤버 함수는 뒤에 const를 붙인다.
class Player {
private:
string name;
int hp;
public:
Player(string name, int hp)
: name(name), hp(hp) {
}
int getHp() const {
return hp;
}
void damage(int amount) {
hp -= amount;
}
};int getHp() const {
return hp;
}이 함수는 객체의 멤버 변수를 변경하지 않겠다는 의미이다.
왜 필요할까?
const 객체는 const 멤버 함수만 호출할 수 있다.
void printHp(const Player& player) {
cout << player.getHp() << '\n';
}getHp()에 const가 없으면 위 함수에서 호출할 수 없다.
10. 클래스 안과 밖에서 함수 구현하기
작은 예제에서는 클래스 안에 함수를 바로 구현해도 된다.
class Player {
public:
void attack() {
cout << "공격!\n";
}
};코드가 길어지면 클래스 안에는 선언만 두고, 밖에서 구현할 수 있다.
class Player {
public:
void attack();
};
void Player::attack() {
cout << "공격!\n";
}핵심 문법
반환형 클래스이름::함수이름() {
// 구현
}::는 범위 지정 연산자이다.
11. static 멤버
static 멤버는 객체마다 따로 존재하지 않고, 클래스에 하나만 존재한다.
class Player {
private:
static int count;
string name;
public:
Player(string name)
: name(name) {
count++;
}
static int getCount() {
return count;
}
};
int Player::count = 0;
int main() {
Player a("A");
Player b("B");
cout << Player::getCount() << '\n';
}핵심
count는 모든Player객체가 공유한다.static멤버 함수는 객체 없이 클래스 이름으로 호출할 수 있다.
Player::getCount();12. 복사 생성자와 대입 연산자
객체는 다른 객체로부터 복사될 수 있다.
class Player {
private:
string name;
int hp;
public:
Player(string name, int hp)
: name(name), hp(hp) {
}
void show() const {
cout << name << " " << hp << '\n';
}
};
int main() {
Player a("Knight", 100);
Player b = a; // 복사 생성
b.show();
}C++은 기본적으로 멤버 변수를 하나씩 복사하는 복사 생성자를 만들어 준다.
직접 복사 생성자 만들기
class Player {
private:
string name;
int hp;
public:
Player(string name, int hp)
: name(name), hp(hp) {
}
Player(const Player& other)
: name(other.name), hp(other.hp) {
cout << "복사 생성자 호출\n";
}
};복사 생성자와 대입 연산자 차이
Player a("A", 100);
Player b = a; // 복사 생성자
Player c("C", 50);
c = a; // 대입 연산자처음 만들어지는 객체에 다른 객체를 넣으면 복사 생성자이다. 이미 만들어진 객체에 값을 덮어쓰면 대입 연산자이다.
13. 캡슐화
캡슐화는 데이터를 숨기고, 정해진 함수로만 접근하게 만드는 방식이다.
class Character {
private:
int hp;
public:
Character(int hp)
: hp(hp) {
}
int getHp() const {
return hp;
}
void takeDamage(int damage) {
if (damage < 0) return;
hp -= damage;
if (hp < 0) {
hp = 0;
}
}
};좋은 점
- 객체가 이상한 상태가 되는 것을 막을 수 있다.
- 클래스 내부 구현을 나중에 바꿔도 외부 코드 영향을 줄일 수 있다.
- 코드의 의도를 함수 이름으로 드러낼 수 있다.
character.takeDamage(30);이 코드는 character.hp -= 30보다 의미가 분명하다.
14. 상속 기본 문법
상속은 기존 클래스의 기능을 물려받아 새로운 클래스를 만드는 문법이다.
class Animal {
protected:
string name;
public:
Animal(string name)
: name(name) {
}
void eat() const {
cout << name << " eats\n";
}
};
class Dog : public Animal {
public:
Dog(string name)
: Animal(name) {
}
void bark() const {
cout << name << " barks\n";
}
};
int main() {
Dog dog("Mango");
dog.eat();
dog.bark();
}핵심 문법
class 자식클래스 : public 부모클래스 {
};| 용어 | 의미 |
|---|---|
| 부모 클래스 | 상속해 주는 클래스 |
| 자식 클래스 | 상속받는 클래스 |
| 기반 클래스 | 부모 클래스와 같은 의미 |
| 파생 클래스 | 자식 클래스와 같은 의미 |
15. protected
private 멤버는 자식 클래스에서도 직접 접근할 수 없다.
class Animal {
private:
string name;
};
class Dog : public Animal {
public:
void bark() {
// cout << name; // 오류
}
};자식 클래스에서 접근해야 하는 멤버는 protected를 사용할 수 있다.
class Animal {
protected:
string name;
};
class Dog : public Animal {
public:
void bark() {
cout << name << " barks\n";
}
};주의
protected는 자식 클래스에게 내부 구현을 열어 주는 것이다.
무조건 많이 쓰기보다는, 필요한 경우에만 사용한다.
16. 상속에서 생성자 호출 순서
자식 객체가 만들어질 때는 부모 생성자가 먼저 호출된다.
class Animal {
public:
Animal() {
cout << "Animal 생성자\n";
}
};
class Dog : public Animal {
public:
Dog() {
cout << "Dog 생성자\n";
}
};
int main() {
Dog dog;
}출력 순서:
Animal 생성자
Dog 생성자소멸자는 반대로 호출된다.
Dog 소멸자
Animal 소멸자매개변수가 있는 부모 생성자 호출
class Animal {
protected:
string name;
public:
Animal(string name)
: name(name) {
}
};
class Dog : public Animal {
public:
Dog(string name)
: Animal(name) {
}
};자식 생성자의 초기화 리스트에서 부모 생성자를 호출한다.
17. 함수 재정의
자식 클래스에서 부모 클래스의 함수를 새로 정의할 수 있다.
class Animal {
public:
void speak() const {
cout << "Animal sound\n";
}
};
class Dog : public Animal {
public:
void speak() const {
cout << "Woof\n";
}
};
int main() {
Dog dog;
dog.speak();
}출력:
Woof하지만 다음 코드는 기대와 다르게 동작한다.
Animal* animal = new Dog();
animal->speak(); // Animal sound
delete animal;포인터 타입이 Animal*이기 때문에 Animal::speak()가 호출된다.
이 문제를 해결할 때 virtual이 필요하다.
18. virtual 키워드
virtual은 부모 클래스 포인터나 참조로 자식 객체를 다룰 때, 실제 객체의 함수를 호출하게 만드는 키워드이다.
class Animal {
public:
virtual void speak() const {
cout << "Animal sound\n";
}
};
class Dog : public Animal {
public:
void speak() const override {
cout << "Woof\n";
}
};
class Cat : public Animal {
public:
void speak() const override {
cout << "Meow\n";
}
};
int main() {
Animal* a = new Dog();
Animal* b = new Cat();
a->speak();
b->speak();
delete a;
delete b;
}출력:
Woof
Meow핵심
virtual void speak() const;부모 클래스의 함수 앞에 virtual을 붙이면, 실행 중 실제 객체 타입에 맞는 함수가 호출된다.
이것을 동적 바인딩 또는 런타임 다형성이라고 한다.
19. override
override는 자식 클래스의 함수가 부모 클래스의 가상 함수를 제대로 재정의하고 있는지 검사하게 만든다.
class Animal {
public:
virtual void speak() const {
cout << "Animal sound\n";
}
};
class Dog : public Animal {
public:
void speak() const override {
cout << "Woof\n";
}
};다음 코드는 실수이다.
class Dog : public Animal {
public:
void speak() override {
cout << "Woof\n";
}
};부모 함수는 void speak() const인데, 자식 함수는 const가 없다.
override를 붙이면 컴파일러가 이 실수를 잡아 준다.
습관
가상 함수를 재정의할 때는 거의 항상 override를 붙이는 것이 좋다.
20. 다형성
다형성은 같은 코드가 실제 객체에 따라 다르게 동작하는 성질이다.
class Monster {
public:
virtual void attack() const {
cout << "Monster attack\n";
}
};
class Slime : public Monster {
public:
void attack() const override {
cout << "Slime body attack\n";
}
};
class Dragon : public Monster {
public:
void attack() const override {
cout << "Dragon fire breath\n";
}
};
void runAttack(const Monster& monster) {
monster.attack();
}
int main() {
Slime slime;
Dragon dragon;
runAttack(slime);
runAttack(dragon);
}출력:
Slime body attack
Dragon fire breathrunAttack() 함수는 Monster를 받지만, 실제로는 Slime, Dragon의 attack()이 호출된다.
21. 순수 가상 함수와 추상 클래스
부모 클래스에서 함수의 구체적인 동작을 정하지 않고, 자식 클래스가 반드시 구현하게 만들 수 있다.
class Shape {
public:
virtual double area() const = 0;
};= 0이 붙은 가상 함수를 순수 가상 함수라고 한다.
순수 가상 함수를 하나 이상 가진 클래스는 추상 클래스이다.
class Circle : public Shape {
private:
double radius;
public:
Circle(double radius)
: radius(radius) {
}
double area() const override {
return 3.141592 * radius * radius;
}
};
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double width, double height)
: width(width), height(height) {
}
double area() const override {
return width * height;
}
};추상 클래스의 특징
Shape shape; // 오류추상 클래스는 직접 객체를 만들 수 없다. 대신 부모 타입의 참조나 포인터로 자식 객체를 다룰 수 있다.
void printArea(const Shape& shape) {
cout << shape.area() << '\n';
}22. 가상 소멸자
상속 구조에서 부모 클래스 포인터로 자식 객체를 삭제할 수 있다면, 부모 클래스의 소멸자는 virtual이어야 한다.
잘못된 예
class Animal {
public:
~Animal() {
cout << "Animal 소멸자\n";
}
};
class Dog : public Animal {
public:
~Dog() {
cout << "Dog 소멸자\n";
}
};
int main() {
Animal* animal = new Dog();
delete animal; // Dog 소멸자가 호출되지 않을 수 있음
}올바른 예
class Animal {
public:
virtual ~Animal() {
cout << "Animal 소멸자\n";
}
};
class Dog : public Animal {
public:
~Dog() override {
cout << "Dog 소멸자\n";
}
};
int main() {
Animal* animal = new Dog();
delete animal;
}출력:
Dog 소멸자
Animal 소멸자핵심 규칙
부모 클래스를 다형적으로 사용할 계획이라면 소멸자를 가상 소멸자로 만든다.
virtual ~Base() = default;23. final
final은 더 이상 상속하거나 재정의하지 못하게 막는다.
클래스 상속 금지
class Boss final {
};
// class FinalBoss : public Boss {
// }; // 오류함수 재정의 금지
class Animal {
public:
virtual void speak() const {
cout << "Animal sound\n";
}
};
class Dog : public Animal {
public:
void speak() const final {
cout << "Woof\n";
}
};Dog를 상속한 클래스는 speak()를 다시 재정의할 수 없다.
24. 상속을 사용할 때 생각할 점
상속은 강력하지만 항상 좋은 선택은 아니다.
상속은 보통 is-a 관계일 때 자연스럽다.
Dog is an Animal
Circle is a Shape하지만 다음 관계는 상속보다 멤버 변수로 포함하는 것이 더 자연스럽다.
Car has an Engine
Player has an Inventory이런 관계는 has-a 관계라고 한다.
class Engine {
public:
void start() {
cout << "Engine start\n";
}
};
class Car {
private:
Engine engine;
public:
void drive() {
engine.start();
cout << "Drive\n";
}
};정리
is-a관계이면 상속을 고려한다.has-a관계이면 멤버 변수로 포함하는 방식을 고려한다.
25. 전체 예제
아래 예제는 클래스, 생성자, 상속, virtual, override, 가상 소멸자를 함께 사용한다.
#include <iostream>
#include <memory>
#include <string>
#include <vector>
using namespace std;
class Character {
protected:
string name;
int hp;
public:
Character(string name, int hp)
: name(name), hp(hp) {
}
virtual ~Character() = default;
string getName() const {
return name;
}
int getHp() const {
return hp;
}
virtual void attack() const = 0;
void takeDamage(int damage) {
if (damage < 0) return;
hp -= damage;
if (hp < 0) {
hp = 0;
}
}
};
class Warrior : public Character {
public:
Warrior(string name)
: Character(name, 120) {
}
void attack() const override {
cout << name << " swings a sword\n";
}
};
class Mage : public Character {
public:
Mage(string name)
: Character(name, 80) {
}
void attack() const override {
cout << name << " casts fireball\n";
}
};
int main() {
vector<unique_ptr<Character>> party;
party.push_back(make_unique<Warrior>("Arthur"));
party.push_back(make_unique<Mage>("Merlin"));
for (const auto& character : party) {
character->attack();
}
}사용된 문법
| 문법 | 사용 위치 |
|---|---|
| 클래스 | Character, Warrior, Mage |
| 생성자 | Character(string name, int hp) |
| 상속 | class Warrior : public Character |
protected | name, hp |
virtual | attack(), 소멸자 |
| 순수 가상 함수 | virtual void attack() const = 0; |
override | 자식 클래스의 attack() |
| 다형성 | vector<unique_ptr<Character>> |
26. 실습 문제
문제 1. 기본 클래스 만들기
Book 클래스를 만들어 보자.
조건:
title,author,price를 멤버 변수로 가진다.- 멤버 변수는
private으로 선언한다. - 생성자로 값을 초기화한다.
showInfo()함수로 정보를 출력한다.
문제 2. 캡슐화 연습
Wallet 클래스를 만들어 보자.
조건:
money는private이다.deposit(int amount)로 돈을 넣는다.withdraw(int amount)로 돈을 뺀다.- 잔액보다 많이 출금할 수 없다.
getMoney() const로 잔액을 확인한다.
문제 3. 상속 연습
Vehicle 클래스를 만들고 Car, Bike가 상속받게 만들어 보자.
조건:
Vehicle은brand를 가진다.Vehicle은move()함수를 가진다.Car는drive()함수를 가진다.Bike는ride()함수를 가진다.
문제 4. virtual 연습
Animal 클래스를 만들고 Dog, Cat, Bird가 상속받게 만들어 보자.
조건:
Animal에는virtual void speak() const가 있다.Dog는"Woof"를 출력한다.Cat은"Meow"를 출력한다.Bird는"Tweet"을 출력한다.vector<Animal*>또는vector<unique_ptr<Animal>>에 담아서 반복문으로speak()를 호출한다.
문제 5. 추상 클래스 연습
Payment 추상 클래스를 만들어 보자.
조건:
virtual void pay(int amount) const = 0;을 가진다.CreditCardPayment,CashPayment가 상속받는다.- 각 클래스는 서로 다른 결제 메시지를 출력한다.
- 부모 클래스 포인터나 참조로 결제 함수를 호출한다.
27. 빠른 요약
class ClassName {
private:
int value;
public:
ClassName(int value)
: value(value) {
}
int getValue() const {
return value;
}
};class Child : public Parent {
public:
Child()
: Parent() {
}
};class Base {
public:
virtual ~Base() = default;
virtual void run() const = 0;
};
class Derived : public Base {
public:
void run() const override {
cout << "Derived run\n";
}
};기억할 규칙
- 클래스의 멤버 변수는 보통
private으로 둔다. - 외부에서 필요한 동작은
public함수로 제공한다. - 객체를 만들 때 초기값이 필요하면 생성자를 사용한다.
- 멤버 초기화에는 생성자 초기화 리스트를 우선 사용한다.
- 객체를 변경하지 않는 함수에는
const를 붙인다. - 상속은
is-a관계일 때 사용한다. - 부모 포인터나 참조로 자식 객체를 다룰 때는
virtual이 필요하다. - 가상 함수를 재정의할 때는
override를 붙인다. - 다형적으로 사용할 부모 클래스의 소멸자는
virtual로 만든다.