14. OOP 캡슐화(OOP Encapsulation)
1. 키워드
- OOP 캡슐화(OOP Encapsulation)
- 프렌드(Friend)와
friend
키워드 - 프렌드 클래스(Friend Class)
- 프렌드 멤버(Friend Member)
- 순환 참조(Circular Reference)
- 전방 선언(Forward Declaration)
- 정적 멤버 변수(Static Member Variable)와
static
키워드 - 정적 멤버 함수(Static member Function)와
static
키워드 - 상수 멤버 변수(Constant Member Variable)와
const
키워드 - 상수 멤버 함수(Constant Member Function)와
const
키워드
2. 프렌드
- C++에서 객체의
private
멤버에는 해당 객체의public
멤버 함수를 통해서만 접근할 수 있다. - 하지만 경우에 따라서는 해당 객체의 멤버 함수가 아닌 함수도
private
멤버에 접근해야만 할 경우가 발생한다. - 이럴 때마다 매번
private
멤버에 접근하기 위한 새로운public
멤버 함수를 작성하는 것은 매우 비효율적이다. - 따라서 C++에서는 이러한 경우를 위해 프렌드라는 새로운 접근 제어 키워드를 제공한다.
- 프렌드는 지정한 대상에 한해 해당 객체의 모든 멤버에 접근할 수 있는 권한을 부여해 준다.
- 이러한
friend
키워드는 전역 함수, 클래스, 멤버 함수의 세 가지 형태로 사용할 수 있다.
1) 프렌드 함수 선언
- C++에서 프렌드 함수는
friend
키워드를 사용하여 다음과 같이 선언한다.
- 이렇게 선언된 프렌드 함수는 클래스 선언부에 그 원형이 포함되지만, 클래스의 멤버 함수는 아니다.
- 이러한 프렌드 함수는 해당 클래스의 멤버 함수는 아니지만, 멤버 함수와 같은 접근 권한을 가지게 된다.
friend
키워드는 함수의 원형에서만 사용해야 하며, 함수의 정의에서는 사용하지 않는다.- 그리고 프렌드 선언은 클래스 선언부의
public
,private
,protected
영역 등 어디에나 위치할 수 있으며, 위치에 따른 차이는 전혀 없다.
2) 프렌드의 필요성
- C++에서 클래스에 대해 이항 연산자를 오버로딩할 때 프렌드의 필요성이 자주 발생한다.
- 그 이유는 바로 멤버 함수의 호출 형태에 있다.
- 멤버 함수는 왼쪽 피연산자인 객체가 호출하는 형태이므로, 이항 연산자의 매개변수 순서라든가 타입에 민감해진다.
- 하지만 멤버 함수가 아닌 함수를 사용하면 해당 객체의
private
멤버에 접근할 수 없게 된다. - 따라서 이때 사용하는 것이 바로 프렌드이다.
- 다음의 코드는 사각형을 나타내는
Rect
클래스를 정의하면서, 크기를 조절하기 위해*
(곱셈 연산자)를 오버로딩하는 예제이다.
#include <iostream>
using namespace std;
class Rect {
private:
double height_;
double width_;
public:
Rect(double height, double width); // 생성자
void DisplaySize();
Rect operator*(double mul) const;
};
int main(void) {
Rect origin_rect(10, 20);
Rect changed_rect = origin_rect * 2; // 멤버 함수는 왼쪽 피연산자인 객체가 호출함
// Rect changed_rect = 3 * origin_rect;
changed_rect.DisplaySize();
return 0;
}
Rect::Rect(double height, double width) {
height_ = height;
width_ = width;
}
void Rect::DisplaySize() {
cout << "이 사각형의 높이는 " << this->height_
<< "이고, 너비는 " << this->width_ << "입니다." << endl;
}
Rect Rect::operator*(double mul) const {
return Rect(height_ * mul, width_ * mul);
}
// 이 사각형의 높이는 20이고, 너비는 40입니다.
- 위 코드의
operator*()
함수에서 주석 처리된 구문처럼 피연산자의 순서를 바꾸어 실행하면 오류가 발생할 것이다. - 그 이유는 멤버 함수란 왼쪽 피연산자인 객체가 호출하는 형태가 되어야 하기 때문이다.
- 따라서 두 구문이 모두 정상적으로 동작하기 위해서는 지금의
operator*()
함수뿐만 아니라 매개변수의 순서가 다른 또 하나의operator*()
함수를 객체가 호출하지 않는 전역 함수로 작성해야 한다. - 그 전역 함수가
private
멤버인height_
와width_
에 접근하기 위해서는friend
키워드를 추가해야 한다.
#include <iostream>
using namespace std;
class Rect {
private:
double height_;
double width_;
public:
Rect(double height, double width); // 생성자
void DisplaySize();
Rect operator*(double mul) const;
friend Rect operator*(double mul, const Rect &origin); // 프렌드 함수
};
int main(void) {
Rect origin_rect(10, 20);
Rect rect01 = origin_rect * 2;
Rect rect02 = 3 * origin_rect;
rect01.DisplaySize();
rect02.DisplaySize();
return 0;
}
Rect::Rect(double height, double width) {
height_ = height;
width_ = width;
}
void Rect::DisplaySize() {
cout << "이 사각형의 높이는 " << this->height_
<< "이고, 너비는 " << this->width_ << "입니다." << endl;
}
Rect Rect::operator*(double mul) const {
return Rect(height_ * mul, width_ * mul);
}
Rect operator*(double mul, const Rect &origin) {
return origin * mul;
}
// 이 사각형의 높이는 20이고, 너비는 40입니다.
// 이 사각형의 높이는 30이고, 너비는 60입니다.
- 멤버 함수 원형의 맨 마지막에
const
키워드를 추가하면, 멤버 함수를 상수 멤버 함수로 정의할 수 있다. - 상수 멤버 함수란 자신이 호출하는 객체를 수정하지 않는 읽기 전용 함수를 의미한다.
3. 다양한 프렌드
1) 프렌드 클래스
- C++에서 프렌드는 전역 함수, 클래스, 멤버 함수의 세 가지 형태로 사용할 수 있다.
- 만약 두 클래스가 기능적으로 서로 밀접한 관계에 있고, 상대방의
private
멤버에 접근해야만 한다면 클래스 자체를 프렌드로 선언하는 것이 좋다. - 프렌드 클래스란 해당 클래스의 모든 멤버 함수가 특정 클래스의 프렌드인 클래스르 의미한다.
- C++에서 프렌드 클래스는 다음과 같이 선언한다.
- 만약
Rect
클래스의 선언에 다음과 같은 프렌드 클래스의 선언이 존재한다고 가정해 보자.
- 이때
Display
클래스의 모든 멤버 함수는Rect
클래스에 대한 프렌드 접근 권한을 부여 받게 된다. - 즉,
Display
클래스의 모든 멤버 함수는Rect
클래스의 모든 멤버에 접근할 수 있다.
- 다음의 코드는
Display
클래스의 모든 멤버 함수가Rect
클래스의 모든 멤버에 접근할 수 있도록 선언한 것이다.
#include <cmath>
#include <iostream>
using namespace std;
class Rect {
private:
double height_;
double width_;
public:
Rect(double height, double width); // 생성자
void height() const;
void width() const;
friend class Display; // 프렌드 클래스 선언
};
class Display {
public:
void ShowSize(const Rect &target);
void ShowDiagonal(const Rect &target);
};
int main(void) {
Rect rect01 = Rect(10, 20);
Display rect_display = Display();
rect_display.ShowSize(rect01);
rect_display.ShowDiagonal(rect01);
return 0;
}
Rect::Rect(double height, double width) {
height_ = height;
width_ = width;
}
void Rect::height() const {
cout << "이 사각형의 높이는 " << this->height_ << "입니다." << endl;
}
void Rect::width() const {
cout << "이 사각형의 너비는 " << this->width_ << "입니다." << endl;
}
void Display::ShowSize(const Rect &target) {
target.height();
target.width();
}
void Display::ShowDiagonal(const Rect &target) {
double diagonal;
diagonal = sqrt(pow(target.height_, 2) + pow(target.width_, 2));
cout << "이 사각형의 대각선 길이는 " << diagonal << "입니다." << endl;
}
// 이 사각형의 높이는 10입니다.
// 이 사각형의 너비는 20입니다.
// 이 사각형의 대각선 길이는 22.3607입니다.
2) 프렌드 멤버 함수
- 프렌드 클래스란 해당 클래스의 모든 멤버 함수가 특정 클래스의 프렌드가 된다.
- 하지만 멤버 함수에 따라 특정 클래스의 프렌드 설정이 필요없는 멤버 함수도 있다.
- 앞선 코드에서
ShowDiagonal()
함수는Rect
클래스의private
멤버에 직접 접근하도록 구현되어 있다. - 하지만
ShowSize()
함수는Rect
클래스의public
인터페이스만으로 구현되어 있다. - 따라서
Display
클래스에서Rect
클래스에 대해 프렌드 설정이 필요한 함수는ShowDiagonal()
함수뿐이다.
- 이처럼 프렌드 멤버 함수란 해당 클래스의 특정 멤버 함수만을 프렌드로 지정하는 것을 의미한다.
- 이것은 프렌드 설정이 꼭 필요한 함수에 대해서만 접근을 허락하므로, 정보 은닉 및 캡슐화 개념에 더욱 가깝게 구현할 수 있게 된다.
- 다음의 코드는
Display
클래스의ShowDiagonal()
함수만이Rect
클래스의 모든 멤버에 접근할 수 있도록 선언한 것이다.
#include <cmath>
#include <iostream>
using namespace std;
class Rect;
class Display {
public:
void ShowSize(const Rect &target);
void ShowDiagonal(const Rect &target);
};
class Rect {
private:
double height_;
double width_;
public:
Rect(double height, double width); // 생성자
void height() const;
void width() const;
friend void Display::ShowDiagonal(const Rect &target); // 프렌드 멤버 함수 선언
};
int main(void) {
Rect rect01 = Rect(10, 20);
Display rect_display = Display();
rect_display.ShowSize(rect01);
rect_display.ShowDiagonal(rect01);
return 0;
}
Rect::Rect(double height, double width) {
height_ = height;
width_ = width;
}
void Rect::height() const {
cout << "이 사각형의 높이는 " << this->height_ << "입니다." << endl;
}
void Rect::width() const {
cout << "이 사각형의 너비는 " << this->width_ << "입니다." << endl;
}
void Display::ShowSize(const Rect &target) {
target.height();
target.width();
}
void Display::ShowDiagonal(const Rect &target) {
double diagonal;
diagonal = sqrt(pow(target.height_, 2) + pow(target.width_, 2));
cout << "이 사각형의 대각선 길이는 " << diagonal << "입니다." << endl;
}
// 이 사각형의 높이는 10입니다.
// 이 사각형의 너비는 20입니다.
// 이 사각형의 대각선 길이는 22.3607입니다.
3) 전방 선언
- 앞선 코드에서
Rect
클래스는Display
클래스를 참조하고,Display
클래스는Rect
클래스를 참조하고 있다. - 이처럼 두 클래스의 선언 내에서 서로를 참조하고 있는 상황을 순환 참조라고 한다.
- 이러한 순환 참조를 피하기 위해서는 한 클래스를 다른 클래스의 앞에 미리 선언하는 전방 선언을 사용해야 한다.
- C++에서 전방 선언은 다음과 같이 선언한다.
- 위의 코드에서는 순환 참조를 피하고자
Rect
클래스를 전방 선언했다.
- 또한, 프렌드 멤버 함수를 선언할 때에는 각 클래스의 선언 위치도 신경 써야 한다.
- 앞선 코드에서는
Rect
클래스와Display
클래스를 다음과 같은 순서로 선언했다.
- 만약 다음과 같이 순서를 바꾸면 컴파일러는 오류를 발생시킬 것이다.
Rect
클래스 내에서ShowDiagonal()
함수가 프렌드로 선언된 것을 처리하기 전에, 컴파일러는 이미ShowDiagonal()
함수의 선언을 알고 있어야만 한다.- 따라서 순서를 바꾸게 되면 컴파일러는 프렌드로 선언된
ShowDiagonal()
함수를 알지 못하므로, 오류를 발생시킨다.
4. 정적 멤버와 상수 멤버
1) 정적 멤버 변수
- C++에서 정적 멤버란 클래스에는 속하지만, 객체 별로 할당되지 않고 클래스의 모든 객체가 공유하는 멤버를 의미한다.
- 멤버 변수가 정적(Static)으로 선언되면, 해당 클래스의 모든 객체에 대해 하나의 데이터만이 유지 관리된다.
- 정적 멤버 변수는 클래스 영역에서 선언되지만, 정의는 파일 영역에서 수행된다.
- 이러한 정적 멤버 변수는 외부 연결(External Linkage)을 가지므로, 여러 파일에서 접근할 수 있다.
- 정적 멤버 변수에도 클래스 멤버의 접근 제한 규칙이 적용되므로, 클래스의 멤버 함수나 프렌드만이 접근할 수 있다.
- 하지만 정적 멤버 변수를 외부에서도 접근할 수 있게 하고 싶으면, 정적 멤버 변수를
public
영역에 선언하면 된다.
- 다음의 코드는 모든
Person
객체가 같이 공유하는person_count_
라는 정적 멤버 변수를 선언하는 것이다.
#include <iostream>
#include <string>
using namespace std;
class Person {
private:
string name_;
int age_;
public:
static int person_count_; // 정적 멤버 변수의 선언
Person(const string &name, int age); // 생성자
~Person() { person_count_--; } // 소멸자
void ShowPersonInfo();
};
int Person::person_count_ = 0; // 정적 멤버 변수의 정의 및 초기화
int main(void) {
Person hong = Person("길동", 29);
hong.ShowPersonInfo();
Person lee = Person("순신", 35);
lee.ShowPersonInfo();
return 0;
}
Person::Person(const string &name, int age) {
name_ = name;
age_ = age;
cout << ++person_count_ << "번째 사람이 생성되었습니다." << endl;
}
void Person::ShowPersonInfo() {
cout << "이 사람의 이름은 " << name_
<< "이고, 나이는 " << age_ << "살입니다." << endl;
}
// 1번째 사람이 생성되었습니다.
// 이 사람의 이름은 길동이고, 나이는 29살입니다.
// 2번째 사람이 생성되었습니다.
// 이 사람의 이름은 순신이고, 나이는 35살입니다.
- 이 정적 멤버는
Person
객체가 생성될 때마다1
씩 증가하여, 현재까지 총 몇 개의Person
객체가 생성되었는지를 알려준다.
2) 정적 멤버 함수
- C++에서는 클래스의 멤버 함수도 정적으로 선언할 수 있다.
- 이렇게 선언된 정적 멤버 함수는 해당 클래스의 객체를 생성하지 않고도, 클래스 이름만으로 호출할 수 있다.
- 정적 멤버 함수는 정적 멤버 변수를 선언하는 방법과 같이
static
키워드를 사용하여 선언한다. - 이러한 정적 멤버 함수는 다음과 같은 특징을 가진다.
1] 객체를 생성하지 않고 클래스 이름만으로 호출할 수 있다.
2] 객체를 생성하지 않으므로, this
포인터를 가지지 않는다.
3] 특정 객체와 결합하지 않으므로, 정적 멤버 변수밖에 사용할 수 없다.
#include <iostream>
#include <string>
using namespace std;
class Person {
private:
string name_;
int age_;
public:
static int person_count_; // 정적 멤버 변수의 선언
static int person_count(); // 정적 멤버 함수의 선언
Person(const string &name, int age); // 생성자
~Person() { person_count_--; } // 소멸자
void ShowPersonInfo();
};
int Person::person_count_ = 0; // 정적 멤버 변수의 정의 및 초기화
int main(void) {
Person hong = Person("길동", 29);
Person lee = Person("순신", 35);
cout << "현재까지 생성된 총 인원 수는 "
<< Person::person_count() << "명입니다." << endl;
return 0;
}
Person::Person(const string &name, int age) {
name_ = name;
age_ = age;
cout << ++person_count_ << "번째 사람이 생성되었습니다." << endl;
}
void Person::ShowPersonInfo() {
cout << "이 사람의 이름은 " << name_
<< "이고, 나이는 " << age_ << "살입니다." << endl;
}
int Person::person_count() { // 정적 멤버 함수의 정의
return person_count_;
}
// 1번째 사람이 생성되었습니다.
// 2번째 사람이 생성되었습니다.
// 현재까지 생성된 총 인원 수는 2명입니다.
- 위의 코드에서는 정적 멤버 변수인
person_count_
의 값을 출력하기 위해서 정적 멤버 함수person_count()
를 선언한다.
3) 상수 멤버 변수
- 상수 멤버 변수란 한 번 초기화하면, 그 값을 변경할 수 없는 멤버 변수를 의미한다.
- 이러한 상수 멤버 변수는 변수의 타입 앞에
const
키워드를 사용하여 선언한다.
- 클래스 전체에 걸쳐 사용되는 중요한 상수는 상수 멤버 변수로 정의하여 사용하는 것이 좋다.
4) 상수 멤버 함수
- 상수 멤버 함수란 호출한 객체의 데이터를 변경할 수 없는 멤버 함수를 의미한다.
- 이러한 상수 멤버 함수는 함수의 원형 마지막에
const
키워드를 사용하여 선언한다.
- 호출한 객체의 데이터를 단순히 읽기만 하는 멤버 함수는 상수 멤버 함수로 정의하는 것이 정보 보호 측면에서도 좋다.