이번 포스팅에서 우리가 배워볼 것은 다음과 같다.
- 절차 지향 프로그래밍 VS 객체 지향 프로그래밍
- 클래스
- 객체의 구성
- 객체 생성하기
- 접근 지정자
- 객체의 멤버 접근
- 클래스와 인터페이스의 분리
- 이름 공간
- 클래스의 선언과 클래스의 정의 분리
- 클래스 예제
1. 절차 지향 프로그래밍 VS 객체 지향 프로그래밍
현재 우리가 많이 사용되는 프로그래밍 기법은 절차 지향 프로그래밍과 객체 지향 프로그래밍으로 나눈다.
1. 절차 지향 프로그래밍(procedural programming)
절차 지향 프로그래밍(procedural programming)은 프로시저(procedure)를 기반으로 하는 프로그래밍 방법이다. 즉, 절차 지향 프로그래밍에서 전체 프로그램은 함수들의 집합으로 이루어져 있다.
여기서 프로시저는 일반적으로 함수를 의미하므로 아마 '프로시저 지향 프로그래밍'이라는 용어가 더 이해하기에는 와닿을 것이다.
절차 지향 프로그래밍의 문제점
절차 지향 프로그래밍에서는 여러 면에서 단점들이 존재한다.
- 과도한 전역 변수의 사용
- 데이터를 접근하는 것을 통제할 수 없기에 어떤 함수든지 쉽게 데이터를 변경 가능하다.
- 따라서 전역 변수는 모든 함수에 대하여 개방되어 있기 때문에 잘못 데이터가 설정될 가능성이 높아진다.
- 프로그램의 이해가 어려워짐.
- 인간이 동시에 이해할 수 있는 함수 숫자는 제한적이다. 따라서 수백 개의 함수로 이루어진 코드가 있다면 그것을 바로 이해하기에는 쉽지 않을 것이다.
- 변경하고 확장하기 어려움.
- 프로그램의 복잡도가 어느 정도 이상으로 커지게 되면 하나의 변수를 수정할 때 다른 함수들의 영향을 받기 때문에 프로그램을 변경하기가 어려워진다.
이를 보완하기 위해 객체 지향 프로그래밍이라는 개념이 도입된다.
2. 객체 지향 프로그래밍(OOP: object-oriented programming)
객체 지향 프로그래밍(OOP: object-oriented programming)은 우리가 살고 있는 실제 세계가 객체(object)들로 구성되어 있는 것과 비슷하게, 소프트웨어도 객체로 구성하는 방법이다.
여기서 데이터와 함수를 하나의 덩어리(객체)로 묶는 것을 캡슐화(encapsulation) 이라고 한다.
객체 지향 프로그래밍의 개념들
지금 나오는 코드의 예시들은 아직 클래스를 배우지 않았으니 모르는 사람은 클래스 파트 읽고나서 다시 한번 예시들을 보는 것을 추천한다.
- 캡슐화(encapsulation)
- 서로 관련된 데이터와 알고리즘을 하나의 묶음으로 정리한 것
- 캡슐화 되어 있지 않은 데이터와 코드는 상대적으로 사용하기 어렵다.
- 정보은닉(information-hiding)
- 캡슐화의 한 개념으로 객체의 실제 구현 내용을 외부에 감추는 것
- 내부 데이터, 내부 연산을 외부에서 접근하지 못하도록 은닉(hiding) 혹은 격리(isolation)시키는 것
- 보통 프로그램에서 데이터들은 공개되지 않고 몇 개의 메소드 만이 외부로 공개한다.
- 정보은닉이 잘 안된 예시
- 정보은닉이 잘 된 예시
-
class Rectangle { private: int width, height; int calcArea() { return width * height; } };
-
- 상속(inheritance)
- 기존 코드를 재활용하기 위한 기법으로 이미 작성된 클래스(부모 클래스)를 이어 받아서 새로운 클래스(자식 클래스)를 생성하는 기법
- 자식 클래스는 부모 클래스의 모든 속성과 동작을 물려받는다.(이 때 필요한 기능이 있다면 추가 또는 변경 가능하다.)
- 자세한 내용은 상속 파트에서 다루겠다.
- 다형성(polymorphism)
- 객체가 취하는 동작이 상황에 따라서 달라지는 것 = 같은 이름을 사용하여 다른 기능을 구현하는 것을 의미한다.
- 예를 들어, 서로 다른 자료형에 속하는 객체들이 같은 이름의 멤버 함수에 응답하여서 서로 다른 동작을 보여주는 것이 가능하다.
- 다음은 다형성의 예시 중 하나인 오버라이딩 예시이다.
-
#include <iostream> // 기본 클래스 class Shape { public: void calculateArea() const { std::cout << "도형의 면적을 계산합니다." << std::endl; } }; // 사각형 클래스 class Rectangle : public Shape { public: void calculateArea() const override { std::cout << "사각형의 면적을 계산합니다." << std::endl; } }; // 삼각형 클래스 class Triangle : public Shape { public: void calculateArea() const override { std::cout << "삼각형의 면적을 계산합니다." << std::endl; } }; int main() { Shape* shapes[] = {new Shape(), new Rectangle(), new Triangle()}; for (int i = 0; i < 3; ++i) { shapes[i]->calculateArea(); // 다형성에 의해 각 도형 객체의 calculateArea() 호출 } for (int i = 0; i < 3; ++i) { delete shapes[i]; } return 0; }
- main() 함수에서는 Shape 포인터 배열을 사용하여 다양한 도형 객체들을 동적으로 생성하고, 반복문을 통해 각 객체의 calculateArea() 함수를 호출한다. 이 때, 각 객체의 실제 타입에 따라 호출되는 함수가 다르므로 다형성에 의해 각 도형 객체는 서로 다른 동작을 수행한다.
-
- 다음 다형성의 예시인 오버로딩의 예시이다.
- 다음은 다형성의 예시 중 하나인 오버라이딩 예시이다.
class Rectangle {
public:
int width, height;
int calcArea() { return width * height; }
};
#include <iostream>
// 함수 오버로딩
void printValue(int x) {
std::cout << "정수: " << x << std::endl;
}
void printValue(double y) {
std::cout << "실수: " << y << std::endl;
}
int main() {
int intValue = 5;
double doubleValue = 3.14;
printValue(intValue); // 정수형 printValue() 호출
printValue(doubleValue); // 실수형 printValue() 호출
return 0;
}
2. 클래스
객체 지향 소프트웨어에서는 같은 객체들이 여러 개가 필요한 경우가 있다. 이러한 객체들은 모두 하나의 설계도로 만들어지는데, 바로 이 설계도를 클래스(class)라고 한다.
실제 컴파일러 입장에서는 클래스를 사용자-정의 자료형(UDT: user-defined type)으로 취급한다. 자세한 건 객체의 생성 부분에서 다루겠다.
클래스 정의
class 클래스이름 {
// 멤버 변수
자료형 멤버변수1;
자료형 멤버변수2;
// 멤버 함수 선언부
반환형 멤버함수1();
반환형 멤버함수2();
}
멤버변수와 멤버 함수에 대해서는 '4. 객체의 구성'에서 다루겠다.
클래스 작성의 예
class Circle {
public: // 접근 지정자
// 멤버 변수
int radius;
string color;
// 멤버 함수
double calcArea() {
return 3.14 * radius * radius;
}
};
여기까지만 작성한 것이 설계도(와플의 틀)만 작성한 것이다. 이제 와플(객체)를 생성해야 한다. 객체에 대해서도 알아보자.
3. 객체의 구성
일반적으로 클래스에 의해 모든 객체(클래스가 아니더라도 일상생활에서의 객체)는 상태와 동작을 가지고 있다.
- 객체의 상태(state) : 객체의 속성
- ex) 자동차의 색상, 속도, 기어
- 객체의 동작(behavior) : 객체가 취할 수 있는 동작
- ex) 자동차 출발하기, 정지하기, 가속하기, 감속하기
이러한 상태와 동작을 소프트웨어에서는 멤버 변수와 멤버 함수로 구성되어진다.
- 멤버 변수(member variable)(또는 필드(field)) : 객체 안에 포함된 변수이며 일반적인 변수와 구별하기 위하여 이 용어를 사용한다. 객체 안의 변수에는 객체의 상태를 저장한다.
- 멤버 함수(member function)(또는 메소드(method)) : 객체 안의 함수이며 일반적인 함수와 구별하기 위하여 이 용어를 사용한다. 객체 안의 함수에는 특정한 동작(작업)을 수행한다.
4. 객체 생성하기
아까 위에서 언급했다시피 실제 컴파일러 입장에서는 클래스를 사용자-정의 자료형으로 취급한다고 했다. 따라서 앞에 클래스명을 쓰고 그 다음에 객체의 이름을 적어주면 된다.
// 객체 생성하기
Circle obj; // obj는 Circle 자료형의 변수이다.
// Circle : 클래스 이름; 자료형의 이름으로 생각할 수 있다.
// obj : 객체의 이름
이렇게 해서 만든 obj는 물리적인 실체를 가지게 된 객체라고 하며 이것을 클래스의 인스턴스(instance)라고도 부른다.
아래 그림은 클래스와 객체간의 관계를 나타낸다.
5. 접근 지정자
- private 멤버 : 클래스 안에서만 접근(사용) 가능하다.(Information Hiding)
- protected 멤버 : 클래스 안과 상속된 클래스에서만 접근이 가능하다.
- public 멤버 : 어디서나 접근 가능하다. 내가 프로그램이나 개발을 혼자 한다면 상관없으나 타인이 쓰게 되면 이상한 값을 넘어 오류가 발생할 수 있으니 미연의 방지를 위해 public를 자제한다.
6. 객체의 멤버 접근
- 객체의 멤버에 접근하기 위해서는 도트(.) 연산자를 사용한다.
obj.radius = 3; // obj의 멤버 변수인 radius에 3을 저장한다.
- 하나의 클래스에 많은 객체가 생성될 수 있기 때문에 어떤 객체의 어떤 멤버인지 반드시 적어줘야 한다.
- 객체 지향의 관점에서 클래스 안의 멤버 변수를 직접 사용하는 것은 보통 캡슐화 원칙을 위배할 수 있어 바람직하지 않다. 캡슐화 원칙은 클래스 내부의 상태를 외부에서 직접 조작하는 것을 피하고, 대신 클래스의 메서드를 통해 상태에 접근하도록 하는 것을 권장한다. 자세한 건 객체 지향의 4가지 개념에서 다루겠다.
7. 클래스와 인터페이스의 분리
- 클래스 내부에서 멤버 함수를 정의하는 경우
- inline(프로그래밍 언어에서 함수나 메서드를 호출하는 과정에서, 해당 함수의 코드를 호출하는 곳에 직접 삽입하는 기법)과 동일하게 작동된다.
- 클래스 외부에서 멤버 함수를 정의하는 경우
- 복잡한 클래스의 경우에는 멤버 함수를 클래스 외부에서 정의하여 정리할 수 있다.
- 클래스 외부에서 멤버 함수를 정의할 경우에는 일반 함수와 같이 호출된다. 이 때 stack 공간 메모리를 사용하게 된다.
- 외부에서 정의할 때는 '클래스 이름::'으로 클래스의 멤버 함수임을 나타낸다.
-
#include <iostream> using namespace std; class Circle { public: double calcArea() {}; int radius; // 반지름 string color; // 색상 }; // 클래스 외부에서 외부 함수 정의. double Circle::calcArea() { return 3.14 * radius * radius; } int main() { Circle c1; c1.radius = 10; cout << c1.calcArea() << endl; return 0; }
8. 이름 공간
- 이름 공간(namespace)는 식별자(자료형, 함수, 변수 등의 이름)의 영역이다.
- 이름 공간은 코드를 논리적 그룹으로 구성하고 특히 코드에 여러 라이브러리가 포함되어 있을 때 발생할 수 있는 이름 충돌을 방지하는 데 사용한다.
이름 공간을 사용하는 경우
#include <iostream>
using namespace std;
class Circle {
public:
double calcArea() {};
int radius; // 반지름
string color; // 색상
};
// 클래스 외부에서 멤버 함수들이 정의된다.
double Circle::calcArea() {
return 3.14 * radius * radius;
}
int main() {
Circle c1;
c1.radius = 10;
cout << c1.calcArea() << endl;
return 0;
}
이름 공간을 사용하지 않는 경우
아래 코드와 같이 string, cout, endl 앞에 std를 붙여줘야 한다.
#include <iostream>
class Circle {
public:
double calcArea() {};
int radius; // 반지름
std::string color; // 색상
};
// 클래스 외부에서 멤버 함수들이 정의된다.
double Circle::calcArea() {
return 3.14 * radius * radius;
}
int main() {
Circle c1;
c1.radius = 10;
std::cout << c1.calcArea() << std::endl;
return 0;
}
9. 클래스의 선언과 클래스의 정의 분리[미완성]
아래와 같이 클래스의 선언(car.h)과 클래스의 정의(car.cpp)를 파일별로 분리하여 코드를 좀 더 효율적으로 관리할 수 있다.
car.h
car.h에는 클래스의 선언이 들어간다.
#include <iostream>
using namespace std;
class Car {
int speed; // 속도
int gear; // 기어
string color; // 색상
public:
int getSpeed();
void setSpeed(int s);
};
car.cpp
멤버 함수의 몸체는 별도의 소스 파일인 car.cpp에서 정의를 하면 된다. 주의할 점은 car.cpp에서 car.h를 반드시 포함해야 한다.
#include "car.h"
int Car::getSpeed() {
return speed;
}
void Car::setSpeed(int s) {
speed = s;
}
main.cpp
참고로 main.cpp 뿐만 아니라 다른 소스 파일에서 클래스를 사용하려면 그 클래스가 선언된 헤더 파일만 포함하면 된다.
#include "car.h"
using namespace std;
int main() {
Car myCar;
myCar.setSpeed(80);
cout << "현재 속도는 " << myCar.getSpeed() << endl;
return 0;
}
10. 클래스 예제
1. 여러 개의 객체 생성
#include <iostream>
using namespace std;
class Circle {
public:
int radius; // 반지름
string color; // 색상
double calcArea() {
return 3.14 * radius * radius;
}
};
int main() {
Circle pizza1, pizza2;
pizza1.radius = 100;
pizza1.color = "yellow";
pizza2.radius = 200;
pizza2.color = "white";
cout << "피자1의 반지름 : " << pizza1.radius << " 피자의 색깔 : " << pizza1.color << " 피자의 면적 = " << pizza1.calcArea() << "\n";
cout << "피자2의 반지름 : " << pizza2.radius << " 피자의 색깔 : " << pizza2.color << " 피자의 면적 = " << pizza2.calcArea() << "\n";
return 0;
}
- 위 예제를 통해서 각 객체의 멤버 변수 값은 서로 다르는 것을 알 수 있다.
- 모든 객체마다 멤버 함수는 동일하다. 따라서 멤버 함수는 객체 안에 별도로 저장될 필요가 없다.
2. 사각형 클래스 만들기
아래 사각형 클래스를 가지고 하나의 사각형 객체를 생성하는 프로그램을 작성해보자.
class Rectangle {
public:
int width, height;
int calcArea() {
return width * height;
}
};
정답 코드)
#include <iostream>
using namespace std;
class Rectangle {
public:
int width, height;
int calcArea() {
return width * height;
}
};
int main() {
Rectangle rect;
rect.width = 5;
rect.height = 10;
cout << "사각형의 넓이 : " << rect.calcArea() << endl;
return 0;
}
3. 원 객체 그리기
다음 원 그리는 코드를 보고 원의 클래스를 만들고, 원을 그려보자.
int main() {
HDC hdc = GetWindowDC(GetForegroundWindow());
Ellipse(hdc, 100, 100, 180, 180);
return 0;
}
정답 코드)
#include <iostream>
#include <Windows.h>
using namespace std;
class Circle {
public:
float x, y, radius; // 원의 중심점과 반지름
string color; // 원의 색상
double calcArea() {
return 3.14 * radius * radius;
}
void drawCircle() {
HDC hdc = GetWindowDC(GetForegroundWindow());
Ellipse(hdc, x - radius, y - radius, x + radius, y + radius);
}
};
int main() {
Circle c1, c2;
c1.x = 600;
c1.y = 400;
c1.radius = 100;
c2.x = 300;
c2.y = 400;
c2.radius = 50;
c1.drawCircle();
c2.drawCircle();
return 0;
}
4. Car 클래스 작성
Car의 상태인 speed, gear, color과 Car의 동작인 speedUp(), speedDown() 이 들어있는 Car 클래스를 만들어보자.
정답코드)
#include <iostream>
using namespace std;
class Circle {
public:
// 멤버 변수 선언
int speed, gear;
string color;
// 멤버 함수 선언
void speedUp() { // 속도 증가 멤버 함수
speed += 10;
};
void speedDown() { // 속도 감소 멤버 함수
speed -= 10;
}
};
int main() {
Car myCar;
myCar.speed = 100;
myCar.gear = 3;
myCar.color = "red";
myCar.speedUp();
myCar.speedDown();
return 0;
}
5. 멤버 함수 중복 정의(오버로딩)
멤버 함수는 생성자처럼 들어오는 매개변수의 종류에 따라 중복 정의할 수 있다.
#include <iostream>
using namespace std;
class PrintData {
public:
void print(int i) { cout << "정수형 매개변수 : " << i << endl; };
void print(double f) { cout << "실수형 매개변수 : " << f << endl; };
void print(string s = "No Data!") { cout << "문자형 매개변수 : " << s << endl; };
};
int main() {
PrintData obj;
obj.print(5);
obj.print(5.0);
obj.print(5.12);
obj.print("C++ 배우기 쉽다.");
obj.print();
return 0;
}
6. 원들의 경주
두 개의 원을 생성한 후에 난수를 발생하여 원을 움직이게 만들자. 원을 화면에 그리는 draw() 함수와 난수를 발생하여 원을 움직이는 함수 move()를 클래스에 추가한다. 이 때 클래스와 멤버함수를 분리시키자.
#include <iostream>
#include <Windows.h>
using namespace std;
class Circle {
public:
void init(int xval, int yval, int r);
void draw();
void move();
private:
int x, y, radius;
};
// 아직 생성자를 학습하지 않았기 때문에 init() 함수를 사용
void Circle::init(int xval, int yval, int r) {
x = xval;
y = yval;
radius = r;
}
void Circle::draw() {
HDC hdc = GetWindowDC(GetForegroundWindow());
Ellipse(hdc, x - radius, y - radius, x + radius, y + radius);
}
void Circle::move() {
x += rand() % 50;
}
int main() {
Circle c1;
Circle c2;
c1.init(100, 100, 50);
c2.init(100, 200, 40);
for (int i = 0; i < 20; i++) {
c1.move();
c1.draw();
c2.move();
c2.draw();
Sleep(1000);
}
return 0;
}
11. UML(Unified Modeling Language)
객체 지향 프로그래밍에서도 프로그래머들은 애플리케이션을 구성하는 클래스들 간의 관계를 그리기 위하여 클래스 다이어그램(class diagram)을 사용한다. 가장 대표적인 클래스 다이어그램 표기법은 UML(Unified Modeling Language) 이다.
'Develop > C, C++' 카테고리의 다른 글
[C/C++] 복사생성자와 정적 멤버 (0) | 2023.12.26 |
---|---|
[C/C++] 생성자(constructor) & 접근 제어(access control) (0) | 2023.12.23 |
[C++] 객체 배열과 벡터 (0) | 2023.12.22 |
[C/C++] 상속(Inheritance) (0) | 2023.12.19 |
[C/C++] 동적할당(Dynamic Memory Allocation) (0) | 2023.12.18 |