본문 바로가기

Programming/C++

[C++] 5. 클래스의 완성 (정보은닉/캡슐화/생성자와 소멸자/클래스와 배열/This 포인터)

이 포스팅은 윤성우님의 열혈 C++ 프로그래밍 책을 기반으로 작성되었습니다.

정보 은닉

앞선 포스팅에서 설명하였듯이,  C++부터는 접근제어지시자를 통해서 정보를 외부로부터 보호할 수 있게 되었고 이와 같이 멤버 변수의 외부접근을 막는 프로그래밍을 정보은닉이라고 한다. 이를 통해 우리는 객체 내의 멤버변수가 실수로나 아니면 외부 해커들로부터 조작되는 것을 막을 수 있다.
만약 private:이나 protected:를 통해서 정보은닉을 하였다면, 이와 관련하여 접근하기 위한 접근 함수들을 생성하게 될텐데 이를 잘 작성해주는 것이 정보은닉의 핵심이며, 한번만 잘 정의되면 잘못된 접근은 대부분 차단할 수 있다고 볼 수 있다.
또한 이러한 멤버함수들을 const로 선언함으로써, 해당 함수 내에서는 멤버변수의 값을 변경할 수 없게 설정할 수도 있는데, 이 때에는 이 함수와 관련된 다른 함수들 역시 const로 설정해주어야 한다.
왜냐하면 const가 함수는 const가 아닌 함수를 호출 할 수 없기 때문이다.(const가 아닌 함수가 멤버변수의 변경을 시도할 수 있기 때문에.)

캡슐화(Encapsulation)

캡슐화는 말 그대로 관련된 모든 함수를 하나의 클래스로 묶는 것을 의미한다.
예를 들어 A,B,C클래스가 있고 이들이 캡슐화가 잘 안되어 있다는 것은 A와 관련된 함수나 변수들이 B나 C에도 있다는 것이다.
이는 후에 A에 문제가 생겼을 때나 무엇을 수정해야할 때, B나 C도 다 함께 건드리는 대대적인 공사로 이어질 수 있다.
따라서 좋은 프로그래머는 상황과 목적에 따라 Class를 잘 캡슐화하는 사람이며, 100% 완벽한 캡슐화는 없다고 볼 수 있다.
매번 상황이나 목적에 따라 어떻게 그룹핑하느냐가 달라지기 때문이다.

생성자와 소멸자(Constructor & Destructor)

생성자는 객체 생성 시 딱 한번 멤버 변수나 설정들을 초기화하는 함수이다.
생성자 역시 함수이기 때문에 매개변수의 타입이나 숫자에 따라 오버로딩 할 수 있으며, 디폴트 값 설정이 가능하다.
생성자는 객체의 이름과 똑같이 설정해주어야 하며, 이니셜라이저를 활용하여 멤버변수를 초기화 할 수도 있다.
예시. (백준 2884번 문제)

#include <iostream>
using namespace std;

class Clock
{
private:
    int time;
    int hour;
    int min;
public:
    Clock(int h, int m);
    void set_clock(int m=45);
    void show();
};

int main(){
    int x,y;
    cin >> x >> y;
    Clock sangeun(x,y);
    return 0;
}

Clock::Clock(int h, int m){
    time = 60*h + m;
    hour = h;
    min = m;
}
//이니셜라이져를 사용해본다면 이렇게도 가능할 것이다.
Clock::Clock(int h, int m):hour(h), min(m)
{
	time = 60*h + m;
}

괜히 한번 생성자를 써보고 싶어서 이렇게 클래스로 구조화해서 풀어보았다.
클래스 선언에서 똑같은 이름으로 생성자를 만들어준 뒤, 알맞은 매개변수를 넣어주고, 뒤에 정의에서 멤버변수들을 세팅하였다.
이니셜라이져에서 역시 멤버변수들을 세팅 할 수 있다. 단, 이니셜라이져는 함수의 정의 부에서 사용하도록 한다.
이 때, 멤버변수로 클래스를 사용할 경우 역시 위의 방법으로 세팅이 가능하다.
이니셜라이져는 선언과 동시에 초기화되는 개념이기에, 멤버변수로 const로 선언된 변수가 있다하더라도 초기화할 수 있다.
마찬가지 이유로 멤버변수로 선언된 참조자 역시 초기화가 가능하다.

만약 생성자를 생성하지 않으면 컴파일러에 의해 자동으로 디폴트 생성자가 생성된다.
디폴트 생성자는 아무 하는 일이 없으며 위의 코드 경우에는 만약 생성자가 없다면 Clock(){} 와 같이 생성될 것이다.

이상하게 들릴수도 있는데, 생성자는 private으로도 설정될 수 있다.
이러한 경우에는 객체가 외부에서 생성되는 것이 아닌 내부 함수에서 객체를 생성할 때 이렇게 사용한다.

소멸자의 경우 객체 소멸시 자동으로 호출되며 Destructor라고도 한다.
주로 정의는 생성자와 비슷하지만 앞에 ~를 붙여서 정의된다.
예를 들어 위의 Clock 객체의 소멸자는 ~Clock(){...}의 형태를 띄게 될 것이다.
이 역시 아무도 정의하지 않으면 디폴트 소멸자는 ~Clock(){}와 같이 아무일도 하지 않으며 객체가 소멸한다.
이는 주로 객체 내에서 사용된 동적 메모리 공간을 소멸시키기 위해 사용된다.
예를 들어 생성자에서 int * ptr = new int[3]과 같이 동적메모리할당을 하였다면 이를 직접 소멸하지 않으면 이는 객체가 소멸되어도 메모리공간을 잡아먹은 채로 남아있게 된다. 이 때 우리는 ~Clock(){ delete []ptr }과 같은 형태로 사용된 동적메모리를 소멸시킬 수 있다.

객체 배열과 객체 포인터 배열

객체 역시 배열로 구성될 수 있다.
또한 포인터가 배열을 이룰 수 있는 것처럼 객체의 포인터 역시 배열을 이룰 수 있다.
아래의 예제 코드를 보자

Clock arr[3]; //객체 배열 생성
Clock *digital = new Clock[3]; //동적 객체배열

Clock *arr[3]; //다음과 같이 객체 포인터를 배열로 만들 수도 있다.
arr[0] = new Clock(x1,y1);
arr[1] = new Clock(x2,y2);
arr[2] = new Clock(x3,y3);

 

따라서 우리는 객체 배열을 생성하기 전에 객체 포인터를 배열로 만들 지, 객체를 배열로 만들지 생각해볼 필요가 있다.

This 포인터의 이해

사실 This 포인터는 굉장히 쉬운 개념이다.
단지, this 포인터가 사용된 객체 자신의 주소를 가리키고 있는 포인터이다.
이를 활용하여 우리는 주로 객체의 멤버변수에 접근한다.
(함수의 매개변수와 멤버변수가 같은 이름으로 설정되었을 때, this->변수이름: 멤버변수, 그냥 변수이름: 매개변수)
그러면 * this는 무엇일까?
매우 당연하게도, this가 객체 자신의 주소를 가리키는 포인터이기에 *this는 객체 자신이다.
따라서 *this.멤버함수()와 같은 형태의 호출이 가능한 것이다.