C++ 스터디 #11: 클래스(1)
시간 | 2021년 7월 20일 화요일 20:00 ~ 22:00 |
장소 | ZOOM |
참가자 | - |
1. 객체 지향 프로그래밍
1.1. 개요
현재 많이 사용되는 프로그래밍 기법은 절차 지향 프로그래밍과 객체 지향 프로그래밍으로 나눌 수 있습니다.
절차 지향 프로그래밍(procedural programming)에서 전체 프로그램은 함수들의 집합으로 이루어집니다. 이런 설계 방법은 하향식 설계라고도 불립니다.
하지만 데이터가 함수와 분리되기 때문에 프로그래머들은 함수 작성에만 신경을 쓰게 되는 단점이 있습니다.
이런 문제점을 해결하기 위해 등장한 것이 객체 지향 프로그래밍(object-oriented programming)입니다.
객체 지향 프로그래밍은 데이터와 함수를 하나의 덩어리인 객체로 묶어서 생각하는 방법입니다.
이렇게 묶는 것을 캡슐화(encapsulation)라고 부릅니다.
1.2. 객체의 구성
객체는 상태와 동작을 가지고 있습니다. 객체의 상태(state)는 객체의 속성입니다.
예를 들어 자동차 객체의 경우 속성은 차종, 색상, 기어, 속도, 배기량, 주행 거리, 연비 등을 생각할 수 있습니다.
객체의 동작(behavior)은 객체가 취할 수 있는 동작입니다.
자동차를 예로 들면 출발하기, 멈추기, 가속하기, 방향 전환하기 등이 여기에 해당됩니다.
객체의 상태와 동작은 프로그램에서 변수와 함수로 표현할 수 있습니다.
즉 객체는 변수와 함수로 이루어져 있는 코드의 묶음이라 할 수 있습니다.
- 멤버 변수 : 객체 안의 변수에는 객체의 상태를 저장합니다. 객체 안에 포함된 변수를 일반적인 변수와 구별하기 위하여 특별히 멤버 변수 또는 필드(field)라고 합니다.
- 멤버 함수 : 객체 안의 함수는 특정한 동작을 수행합니다. 일반적인 함수와 구별하기 위해서 객체 안의 함수를 멤버 함수 또는 메소드(method)라고 합니다.
1.3. 캡슐화
새로운 프로그램을 개발할 때 기존에 작성된 코드를 재사용할 수 있다면 편리할 것입니다.
관련 데이터와 알고리즘이 하나의 묶음으로 정리되어 있고, 객체 지향 프로그래밍에서는 이를 캡슐화라고 합니다.
객체가 하나의 캡슐에 해당하고, 클래스의 멤버 변수가 데이터와, 멤버 함수는 알고리즘에 각각 해당합니다.
캡슐화의 목적 1. 관련 데이터와 알고리즘 묶기
서로 관련되어 있는 데이터와 알고리즘이 묶여 있으면 사용하기 매우 편리합니다.
캡슐로 된 약처럼, 캡슐로 감싸져 있지 않으면 안의 내용물들이 흩어지게 되고 복용이 힘들 것입니다.
캡슐화의 목적 2. 정보 은닉: 접근 가능한 것과 접근해서는 안되는 것 분리
외부에서 객체의 세부 사항을 너무 많이 알아도 문제가 될 수 있습니다.
어떤 사람이 TV 채널을 돌리거나 음량을 바꿀 때 리모콘을 사용하지 않고 내부 회로를 마구 조작하면서 사용하다가, 중요한 부분을 잘못 건드리게 되면 TV가 고장이 날 수 있을 뿐더러 고치기도 쉽지 않습니다.
TV 제조사가 안전하게 TV를 사용할 수 있게 리모콘을 제공한 것처럼, 프로그래밍에서도 객체를 사용할 때 접근 가능한 것과 접근해서는 안되는 것을 분리합니다.
2. 클래스
2.1. 클래스의 역할
자동차 객체의 경우 같은 설계도에 의해 각각의 자동차가 만들어집니다. 객체 지향 소프트웨어에서도 같은 객체들이 여러 개 필요한 경우가 있습니다.
이러한 객체들은 모두 하나의 설계도로 만들어집니다. 바로 이 설계도를 클래스(class)라고 합니다.
다시 말하면 클래스란 어떤 종류의 모든 객체에게 공통인 멤버 변수와 멤버 함수를 정의하는 형틀 또는 청사진이라고 할 수 있습니다.
객체 지향 프로그래밍에서는 클래스로부터 만들어지는 객체를 그 클래스의 인스턴스(instance)라고 합니다.
객체가 생성될 때마다 각 객체에 필요한 기억 공간이 할당됩니다. 같은 설계도로 만들어진 자동차라고 해도 각 자동차의 속도, 주행 거리, 기어 등의 상태는 다릅니다.
따라서 같은 클래스로 만들어진 인스턴스라고 해도 각기 다른 상태 값을 가질 수 있습니다. 따라서 이들 상태 값을 저장할 공간이 객체마다 필요합니다. 즉 객체마다 멤버 변수의 값은 달라집니다.
멤버 함수의 경우는 약간 다릅니다. 자동차를 예로 들면 전조등을 켜는 방법은 같은 설계도로 만든 자동차이면 모두 동일합니다.
따라서 객체의 멤버 함수들은 객체마다 저장되는 것이 아니라 하나의 멤버 함수를 공유합니다.
정리하자면 같은 클래스의 인스턴스들은 멤버 함수는 공유하지만 멤버 변수는 각각 가지고 있다고 말할 수 있습니다.
2.2. 클래스 작성
클래스는 다음과 같이 정의됩니다.
class 클래스이름 { 자료형 멤버변수1; 자료형 멤버변수2; 반환형 멤버함수1(); 반환형 멤버함수2(); };
간단한 예시를 작성하면 다음과 같습니다.
class Circle { public: int radius; string color; double calcArea() { return 3.14*radius*radius; } };일반적으로 클래스 이름은 명사로 하며, 첫 글자는 대문자로 합니다. 위 예시에서 클래스의 이름은 Circle입니다.
이어서 등장하는 키워드 public은 외부에서 멤버들을 자유롭게 사용할 수 있음을 의미합니다.
Circle 클래스 안에는 원의 반지름을 나타내는 radius 변수와 원의 색상을 나타내는 color 변수를 선언하였고 멤버 함수로는 원의 반지름을 계산하여 반환하는 calcArea() 함수를 정의했습니다.
클래스의 정의가 끝나면 반드시 ;을 붙어야 합니다.
2.3. 객체 생성
클래스를 선언하였다고 해서 객체가 생성된 것은 아닙니다. 클래스의 정의는 객체를 찍어내는 틀을 만든 것에 불과합니다.
틀을 사용하여 객체를 생성해야 객체를 사용할 수 있습니다.
Circle obj;
위의 문장이 실행되면 obj라는 객체가 생성되게 됩니다. 이처럼 실제로 생성된 객체를 클래스의 인스턴스라고 부릅니다.
Circle이라는 클래스 이름은 자료형의 이름으로 생각할 수 있습니다. obj는 객체의 이름이 됩니다.
2.4. 객체의 멤버 접근
객체 안에 정의된 멤버 변수와 멤버 함수를 사용하려면 도트(.)연산자를 사용해야 합니다.
예를 들어 obj 객체의 변수 radius에 값을 대입하려면 다음과 같이 해야 합니다.
obj.radius = 3;
하나의 클래스에서 많은 객체가 생성될 수 있기 때문에 어떤 객체의 어떤 멤버인지를 적어주는 것입니다.
obj.calcArea();
위와 같이 도트 연산자를 이용하여 멤버 함수도 호출할 수 있습니다.
2.5. 예제
사각형을 클래스 Rectangle로 표현하세요. width, height를 멤버 변수로 가지고 사각형의 면적을 계산하는 calcArea()를 멤버 함수로 가져야 합니다.
그리고 클래스 Rectangle로 obj1, obj2라는 이름의 두 개의 객체를 생성하고 각 객체의 면적을 출력하는 프로그램을 작성하세요.
3. 멤버 함수 중복 정의
#include <iostream> #include <string> 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(1); obj.print(3.14); obj.print("KAsimov"); obj.print(); return 0; }
위의 코드에서 멤버 함수 print()는 정수, 실수, 문자열에 대하여 중복 정의되어 있습니다.
따라서 어떤 자료형의 데이터든지 출력할 수 있습니다. 문자열을 출력하는 print()는 매개 변수가 주어지지 않으면 문자열 “No Data!”를 출력합니다.
4. 클래스의 인터페이스와 구현의 분리
만약 멤버 함수가 매우 복잡해서 100줄이 넘으면 클래스의 정의가 어디서 시작해서 어디서 끝나는지를 분간하기 힘들 것입니다.
이런 경우에는 클래스 외부에 멤버 함수를 정의해야 합니다.
#include <iostream> #include <string> using namespace std; class Circle { public: double calcArea(); int radius; string color; } double Circle::calcArea() { return 3.14*radius*radius; } int main() { Circle obj; obj.radius = 10; cout << obj.calcArea() << endl; return 0; }
클래스 외부에서 calcArea() 함수를 정의할 때 함수 이름 앞에 클래스 이름인 Circle과 ::연산자를 붙인 것을 볼 수 있습니다.
::연산자는 이름공간(name space)를 지정하는 연산자입니다.
5. 생성자
5.1. 생성자 함수
변수를 사용하기 위해서는 초기화를 해주어야 한다.
객체도 이와 마찬가지로 생성한 후 초기화를 해야 사용할 수 있다.
C++에서는 초기화를 담당하는 생성자 함수가 존재한다. 이를 사용하면 객체의 생성과 동시에 초기화를 할 수 있다.
직사각형을 나타내는 클래스 Rectangle을 다음과 같이 정의하자.
#include <iostream> using namespace std; class Rectangle { private: int width, height; public: int calcArea() { return width * height; } };
클래스 Rectangle의 객체를 생성하고 변수에 여러 값들을 저장해보자.
int main() { Rectangle r; // 객체 r을 생성한다. r.width = 4; // 객체 r의 너비를 4, 높이를 3으로 설정한다. r.height = 3; return 0; }
하지만 깜빡하고 객체를 초기화하지 않는다면 문제가 발생할 것이다.
int main() { Rectangle r; cout << r.calcArea(); // 객체를 초기화하지 않아 쓰레기 값이 출력된다. return 0; }
혹은, 멤버 변수가 private이라면 변수에 직접 접근할 수 없어 아까와 같은 방법으로는 값을 저장할 수 없다.
이때는 값을 저장할 수 있는 public인 멤버 함수를 만들어 사용해야 한다.
따라서 객체를 생성할 때 자동으로 객체를 초기화할 수 있다면 매우 편리할 것이다.
이를 생성자 함수라고 한다.
class Rectangle { private: int width, height; public: Rectangle(int w, int h) { width = w; height = h; } int calcArea() { return width * height; } };
생성자는 클래스 이름과 똑같은 멤버 함수이고 반환 형식은 따로 존재하지 않는다.
Rectangle a; // 오류! 초기화값 없음 Rectangle b(4, 3); // OK. 직접 초기화, 예전 방법, 하지만 함수 선언과 혼동할 수 있음 Rectangle c {4, 3}; // OK. 유니폼 초기화, 최신 방법. Rectangle d = {4, 3}; // OK. 약간은 간결하지 않음
생성자를 사용하는 전체 코드는 이렇다.
#include <iostream> using namespace std; class Rectangle { private: int width, height; public: Rectangle(int w, int h) { width = w; height = h; } int calcArea() { return width * height; } }; int main() { Rectangle r {4, 3}; cout << r.calcArea(); // 12 return 0; }
5.2. 생성자 중복 정의
생성자도 멤버 함수의 일종이라고 생각할 수 있고, 따라서 생성자도 함수 오버로딩처럼 중복 정의가 가능하다.
다음은 매개 변수가 없는 생성자와 매개 변수가 있는 생성자를 동시에 정의한 것이다.
#include <iostream> using namespace std; class Rectangle { private: int width, height; public: Rectangle() { width = 1; height = 1; } Rectangle(int w, int h) { width = w; height = h; } int calcArea() { return width * height; } }; int main() { Rectangle a; // 매개 변수가 없는 생성자가 호출된다. 너비와 높이가 1로 설정된다. Rectangle b {4, 3}; // 매개 변수가 있는 생성자가 호출된다. cout << a.calcArea() << endl; // 1 cout << b.calcArea(); // 12 return 0; }
이렇게 매개 변수가 없는 생성자를 기본 생성자라고 한다. 기본 생성자는 객체를 생성할 때 인수를 주지 않으면 자동으로 호출된다.
다만, 기본 생성자를 호출할 때 소괄호를 사용하면 안된다. 컴파일러가 함수를 정의하는 것으로 생각하기 때문이다.
Rectangle a; // OK. 기본 생성자가 호출된다. Rectangle b(); // X. 기본 생성자가 아니다.
예제: 평면 위의 점
2차원 평면 위의 한 점을 나타내는 클래스 Point를 생성자를 포함하여 작성하라.
멤버 변수는 private으로, 멤버 함수는 public으로 설정하라.
따로 좌표를 설정하지 않으면 기본 좌표를 (0, 0)으로 설정하라.
객체의 좌표를 출력하는 멤버 함수는 클래스 외부에 정의하라.
x = 0, y = 0 x = 2, y = 3
답안 예시
#include <iostream> using namespace std; class Point { private: int x, y; public: Point() { x = 0; y = 0; } Point(int a, int b) { x = a; y = b; } void printXY(); }; int main() { Point p; Point q(2, -3); p.printXY(); q.printXY(); return 0; } void Point::printXY() { cout << "x = " << x << ", y = " << y << endl; }
5.3. 디폴트 인수를 사용하는 생성자
생성자의 매개변수는 디폴트 값을 가질 수 있다.
#include <iostream> using namespace std; class Rectangle { private: int width, height; public: Rectangle(int w = 1, int h = 1) { width = w; height = h; } int calcArea() { return width * height; } }; int main() { Rectangle a; Rectangle b{4, 3}; cout << a.calcArea() << endl; // 1 cout << b.calcArea(); // 12 return 0; }
5.4. 멤버 초기화 리스트
앞서 클래스 Rectangle의 생성자에서 멤버 변수를 다음과 같이 초기화했다.
class Rectangle { private: int width, height; public: Rectangle(int w, int h) { width = w; height = h; } };
하지만 이것은 모두 할당이지 초기화를 해준 것이 아니다.
생성자 함수가 실행될 때 멤버 변수인 width와 height가 생성된 후, 생성자 본문이 실행되어 값이 할당된다.
이는 유효한 방법이지만 초기화보다 효율적이지 않다.
게다가 어떤 종류는 선언과 동시에 초기화를 해 주어야 한다.
class Rectangle { private: const int width; int& height; public: Rectangle(int w, int& h) { width = w; // const 변수는 할당할 수 없다. height = h; // 참조 변수는 선언과 동시에 초기화되어야 한다. } };
이런 문제를 해결하기 위해 C++에서는 초기화 리스트를 통해 멤버 변수를 초기화할 수 있다.
Rectangle(int w, int h) : width(w), height(h) {} // width를 w로, height를 h로 초기화한다.
또한, 기본값이 지정되어 있더라도 생성자 멤버 초기화 리스트가 가장 우선시된다.
class Rectangle { public: int width = 1; int height = 1; Rectangle(int w, int h) : width(w), height(h) {} }; int main() { Rectangle r {2}; // 값을 하나만 전달하면 앞에서부터 차례대로 채운다. cout << r.width << endl; // 2 cout << r.height; // 1
예제: 시간
시간을 나타내는 클래스 Time을 작성하라.
시와 분을 입력받아 시간 객체를 생성하라.
멤버 초기화 리스트를 이용하여 객체를 초기화하라.
다음을 생성자 함수에서 수행하라.
- 입력받은 시가 0 이상 24 미만의 정수가 되도록 24를 더하거나 뺄 것
- 입력받은 분이 0 이상 60 미만의 정수가 되도록 60을 더하거나 뺄 것
입력받은 시간을 출력하는 멤버 함수를 작성하라.
시: -33 분: 77 15시 17분
답안 예시
#include <iostream> using namespace std; class Time { private: int hour, minute; public: Time(int h, int m) : hour(h), minute(m) { while (hour < 0) hour += 24; while (hour >= 24) hour -= 24; while (minute < 0) minute += 60; while (minute >= 60) minute -= 60; } void printTime() { cout << hour << "시 " << minute << "분" << endl; } }; int main() { int h, m; cout << "시 : "; cin >> h; cout << "분 : "; cin >> m; Time t(h, m); t.printTime(); return 0; }
6. 접근 제어
6.1. 접근 지정자
접근 제어란 외부에서 특정 멤버 변수나 함수에 접근하는 것을 제어하는 것이다.
public:
을 붙이면 공개 멤버가 되어 클래스 밖에서도 멤버에 직접 접근할 수 있다.
private:
을 붙이면 전용 멤버가 되어 클래스 외부에서는 접근할 수 없고, 클래스의 다른 멤버만 해당 멤버에 접근할 수 있다.
public:
과 private:
을 접근 지정자라고 한다.
클래스의 모든 구성원은 기본적으로 private이므로 외부에서 직접 접근하기 위해서는 public 키워드를 사용해야 한다.
6.2. 접근 제어의 필요성
우리는 앞에서 Rectangle 클래스를 작성했다.
그런데 만약에 width와 height가 모두 public인데 실수로 이 값들을 0 이하로 변경했다면 어떨까?
도형의 변 길이는 0 이하가 될 수 없으므로 이는 잘못된 값이 된다.
이러한 실수를 방지하기 위해 공개해도 되는 멤버와 공개해서는 안되는 멤버를 구분한다.
6.3. 접근자와 설정자
Rectangle 클래스에서 width와 height 변수를 비공개로 설정했다면 객체의 멤버 변수 값을 직접 변경하거나 읽어올 수 없다.
따라서 우리는 private으로 선언된 멤버 변수를 외부로 전달하거나 외부에서 안전하게 멤버 변수의 값을 변경할 수 있는 함수를 만들어 사용한다.
이를 각각 접근자, 설정자라고 부른다.
class Rectangle { private: int width = 1; int height = 1; public: Rectangle(int w, int h) : width(w), height(h) {} int getWidth() {return width} int getHeight() {return height} // 접근자 void setWidth(int w) : width(w) {} void setHeight(int h) : height(h) {} // 설정자 }; int main() { Rectangle r {2}; r.setHeight(3); cout << r.getWidth() << " " << r.getHeight(); // 2 3 return 0; }
예제
다음 빈칸을 채워 자신의 학과, 학번, 이름이 출력되도록 만들어라.
#include <iostream> #include <string> using namespace std; class Student { private: ____________________ ____________________ ____________________ public: Student() : department("미정"), num(2021000000), name("홍길동") {} void setDepartment(__________) { ____________________ } void setNum(__________) { ____________________ } void setName(__________) { ____________________ } string getDepartment() { ____________________ } int getNum() { ____________________ } string getName() { ____________________ } }; int main() { ____________________ ____________________ ____________________ ____________________ cout << "학과: " << ____________________ << endl; cout << "학번: " << ____________________ << endl; cout << "이름: " << ____________________ << endl; }
답안 예시
#include <iostream> #include <string> using namespace std; class Student { private: string department; int num; string name; public: Student() : department("미정"), num(2021000000), name("홍길동") {} void setDepartment(string s) { department = s; } void setNum(int n) { num = n; } void setName(string s) { name = s; } string getDepartment() { return department; } int getNum() { return num; } string getName() { return name; } }; int main() { Student s; s.setDepartment("기계공학부"); s.setNum(2020170716); s.setName("윤호연"); cout << "학과: " << s.getDepartment() << endl; cout << "학번: " << s.getNum() << endl; cout << "이름: " << s.getName() << endl; }