많은 사람들이 프로그래밍을 배우기 위해 언어를 선택할 때 요즘은 주로 파이썬(Python)을 배우지만 과거에 프로그래밍을 접했거나 좀 더 클래식하게 근본을 위해 C부터 시작하는 사람도 많을 것이다 그러게 되면 보통 C만 배우고 끝내지 않고 C++까지 배우는 경우도 많은데
보통 C를 배우다가 C++를 배우다보면 대부분 C와 유사하지만 C에는 없는 C++의 특별한 기능들을 발견할 수 있다 그렇기에 이번 글에선 C에는 없는 C++의 클래스 개념에 대해 알아보려고 한다!
클래스란 무엇일까?
쉽게 설명해 C++에서 클래스란 C와 마찬가지로 사용자 정의 데이터 타입을 만들기 위한 구조체라고 이해하면 되는데 클래스는 구조체와 달리 데이터 멤버(변수)와 멤버 함수(메서드)를 결합할 수 있다
즉 C 언어의 구조체는 데이터만을 포함하지만 C++의 클래스는 데이터와 함수를 모두 포함할 수 있다 알다싶이 C는 절차 지향 프로그래밍 언어지만 C++은 객체 지향 프로그래밍이기에 클래스기에 이러한 특별한 구조체인 클래스가 있는 것이다!
객체 지향 프로그램이란?
잠시 길을 틀어서 그렇다면 객체 프로그래밍이란 무엇이기에 C의 절차 지향과 다른 걸까? 객체 지향 프로그래밍에서는 모든 데이터를 객체(object)로 취급하며 객체가 바로 프로그래밍의 중심이 되는 프로그래밍 법으로
여기서 객체란 간단히 이야기하자면 실생활에서 우리가 인식할 수 있는 사물로 이해할 수 있는데 이러한 객체의 상태와 행동을 구체화하는 형태의 프로그래밍이 바로 객체 지향 프로그래밍이다 또한 객체 지향에서 중요한 클래스가 이와 같은 객체를 만들어 내기 위한 틀과 같은 개념이다
이러한 인스턴스는 독립된 메모리 공간에 저장된 자신만의 멤버 변수를 가지지만 멤버 함수는 모든 인스턴스가 공유하는 구조를 가지고 있다
클래스의 특징
다음으로 클래스와 객체 지향의 특징을 살펴보면 크게 5가지 있는데 바로 캡슐화, 상속, 다형성, 추상화, 은닉성이다 이 5가지 특징에 대해 살펴보면
캡슐화 캡슐화는 클래스가 데이터 멤버와 멤버 함수를 하나의 단위로 묶는 과정으로 이를 통해 데이터의 무결성을 보장하고 외부 접근으로부터 보호할 수 있다
상속 클래스는 다른 클래스로부터 속성과 행동을 상속받을 수 있다 이를 통해 코드의 재사용성이 증가하고 계층적인 클래스 구조를 만들 수 있다
다형성 다형성은 같은 인터페이스나 함수가 다른 객체에서 다양한 형태로 구현될 수 있음을 의미하는 것으로 예를 들어 부모 클래스의 함수를 자식 클래스에서 다르게 정의할 수 있다
추상화 클래스를 사용하면 복잡한 현실 세계의 개념을 단순화된 형태로 모델링할 수 있다 이를 통해 더 복잡한 문제에 집중할 수 있게 된다
은닉성 : 클래스 내부의 구체적인 데이터와 구현 방법은 외부에 공개되지 않기에 클래스의 인터페이스만을 사용하여 객체와 상호작용하게 하며 내부 구현 변경 시 외부 코드에 영향을 미치지 않는다
클래스를 알게 된 지 얼마 안된 지금은 잘 이해되지 않을 수 있으나 클래스를 계속 사용하고 조금 더 공부하다보면 이해가 될 것이기에 조급해하지 않아도 된다
C++의 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;
class Car {
public:
string brand;
int year;
// 생성자
Car(string b, int y) {
brand = b;
year = y;
}
// 멤버 함수
void displayInfo() {
cout << "브랜드: " + brand << ", 연도: " << year << endl;
}
};
int main() {
// 객체 생성 및 초기화
Car myCar("Hyundai", 2022);
myCar.displayInfo();
return 0;
}
위 코드는 Car 클래스에 생성자를 추가하여 객체 생성 시 brand와 year를 초기화하는 코드로 여기서 생성되는 객체인 myCar를 Car타입의 인스턴스라고 불린다
우선 이를 조각내서 천천히 알아보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;
class Car {
public:
// 데이터 멤버
string brand;
int year;
// 생성자
Car(string b, int y) : brand(b), year(y) {}
// 메서드
void displayInfo() {
cout << "브랜드: " + brand << ", 연도: " << year << endl;
}
};
클래스 정의를 먼저 살펴보면 클래스는 데이터 멤버(변수)와 멤버 함수(메서드)를 포함하며 여기서 생성자는 객체가 생성될 때 자동으로 호출되는 특별한 함수로 주로 멤버 변수의 초기화에 사용된다
다음으로
1
2
3
4
5
6
7
int main() {
// 객체 생성 및 초기화
Car myCar("Hyundai", 2022);
myCar.displayInfo();
return 0;
}
이 부분은 객체 생성 및 초기화하는 부분으로 아까 살펴본 생성자를 통해 brand와 year 변수를 생성자를 통해 집어넣은 Hyundai와 2022로 초기화하는 부분이다
그리고 그렇게 생성한 myCar 인스턴스를 통해 Car 클래스의 displayInfo 메서드에 접근하여 사용하고 있다
이번에는 클래스의 특징에서 배운 정보 은닉과 캡슐화를 살펴보면
1
2
3
4
5
6
7
8
9
10
11
class Rectangle {
private:
int width, height;
public:
Rectangle(int w, int h) : width(w), height(h) {}
int area() {
return width * height;
}
};
이렇게 private를 사용하여 Private 멤버를 만들 수 있는데 private 멤버는 클래스 내부에서만 접근 가능한 멤버로 이를 통해 데이터를 숨기고 안전한 인터페이스를 만들 수 있다 여기서 public은 외부에서 접근 가능한 멤버들을 뜻한다
다음으로 클래스의 꽃이라고 볼 수도 있는 상속에 대해 알아보겠다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Vehicle {
public:
string brand = "Ford";
void honk() {
cout << "투우, 투우!" << endl;
}
};
class Car : public Vehicle {
public:
string model = "Mustang";
};
int main() {
Car myCar;
myCar.honk(); // 투우, 투우!
cout << myCar.brand + " " + myCar.model; // Ford Mustang
}
상속은 한 클래스가 다른 클래스의 속성과 메서드를 물려받는 것으로 이를 통해 코드 재사용성이 증가하고 불필요한 코드를 없앨 수 있다 이러한 상속은 C++에서 : public Vehicle 이런 식으로 상속받을 자식 클래스에다가 : 뒤에 접근 지정자와 상속할 멤버들을 가지고 있는 부모 클래스를 넣어서 상속을 할 수 있다
추가적으로 C++에서 클래스 상속 시 사용할 수 있는 접근 지정자는 public, protected, private 3가지로 접근 지정자는 상속받는 멤버의 접근성에 영향을 미친다
각 지정자에 따른 차이점을 예제 코드와 함께 설명하면
우선 Public 상속은 부모 클래스의 public 멤버는 자식 클래스에서 public으로 유지되며 protected 멤버는 protected로 유지된다 하지만 private 멤버는 상속받지만 자식 클래스에서 접근할 수 없다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Vehicle {
public:
int maxSpeed;
protected:
int numWheels;
private:
string brand;
public:
Vehicle() : maxSpeed(0), numWheels(0), brand("Unknown") {}
};
class Car : public Vehicle {
public:
void setWheels(int w) {
numWheels = w; // 가능 (protected 멤버)
}
void setBrand(string b) {
// brand = b; // 불가능 (private 멤버)
}
};
코드로는 이렇게 나타낼 수 있고
다음은 Protected 상속
Protected 상속은 부모 클래스의 모든 public과 protected 멤버가 자식 클래스에서 protected로 상속되며 마찬가지로 private 멤버는 상속받지만 접근할 수 없다
코드로는
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Vehicle {
public:
int maxSpeed;
// ... 같은 멤버 변수와 생성자
};
class Car : protected Vehicle {
public:
void setMaxSpeed(int s) {
maxSpeed = s; // 가능 (protected 멤버)
}
// numWheels와 brand의 접근은 이전 예제와 동일
};
이렇게 나타낼 수 있다
마지막으로 Private 상속은 부모 클래스의 모든 public과 protected 멤버가 자식 클래스에서 private으로 상속된다 즉 부모 클래스의 멤버는 자식 클래스 외부에서 접근할 수 없게 된다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Vehicle {
public:
int maxSpeed;
// ... 같은 멤버 변수와 생성자
};
class Car : private Vehicle {
public:
void setMaxSpeed(int s) {
maxSpeed = s; // 가능 (private 멤버)
}
// numWheels와 brand의 접근은 이전 예제와 동일
};
코드로는 이렇다
근데 이렇게 설명하면 private는 확실히 다른 걸 알겠는데 protected와 public 차이를 명확히 이해가 안될 수 있다
쉽게 비교하자면 이렇다
1
2
3
4
class MyClass {
public:
int publicVar; // 클래스 외부에서도 접근 가능
};
public 멤버는 클래스의 객체를 통해 클래스 외부에서도 직접 접근할 수 있기에 이를 통해 클래스의 기능을 외부에 제공할 수 있다
하지만 protected는
1
2
3
4
class MyClass {
protected:
int protectedVar; // 클래스 외부에서는 접근 불가, 상속받은 클래스에서는 접근 가능
};
클래스 외부에서 직접 접근할 수 없지만 해당 클래스를 상속받는 자식 클래스에서는 접근 가능하다
상속 시 지정할 수 있는 접근 제어자 차이에 대해 정리하면
- Public 상속: 자식 클래스가 부모 클래스의 모든 public과 protected 멤버를 동일한 접근 수준으로 상속받으며 가장 일반적으로 사용된다
- Protected 상속: 자식 클래스가 부모 클래스의 public과 protected 멤버를 protected로 상속받는다
- Private 상속: 자식 클래스가 부모 클래스의 모든 멤버를 private으로 상속받는다
이렇게 나타낼 수 있다
이어서 설명할 것은 다형성과 가상 함수 부분으로 여기서 다형성은 같은 인터페이스에 대해 다른 객체들이 다른 형태의 구현을 제공할 수 있는 능력이고 가상 함수는 파생 클래스에서 재정의할 수 있는 멤버 함수를 말한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Animal {
public:
virtual void speak() {
cout << "동물이 소리를 냅니다." << endl;
}
};
class Dog : public Animal {
public:
void speak() override {
cout << "멍멍!" << endl;
}
};
class Cat : public Animal {
public:
void speak() override {
cout << "야옹!" << endl;
}
};
int main() {
Animal* myAnimal = new Dog();
myAnimal->speak(); // 출력: 멍멍!
myAnimal = new Cat();
myAnimal->speak(); // 출력: 야옹!
delete myAnimal;
return 0;
}
이 코드처럼 virtual을 붙여서 가상 함수로 만들 수 있고 override 을 통해 재정의를 할 수 있다
마지막으로 이런식으로
1
2
3
4
5
6
7
8
9
class Point {
public:
int x, y;
Point(int _x, int _y) : x(_x), y(_y) {}
Point operator + (const Point& p) {
return Point(x + p.x, y + p.y);
}
};
연산자 오버로딩을 통해 클래스에 대해 표준 연산자를 사용자 정의 방식으로 작동하도록 할 수 있다