C++ 04 : 클래스 사용법, this의 활용, 생성자와 소멸자, 멤버 변수 초기화 방법

728x90

C++ 04 : 클래스 사용법, this의 활용, 생성자와 소멸자, 멤버 변수 초기화 방법

 

클래스와 객체

붕어빵 틀을 이용해서 붕어빵을 대량 생산함

붕어빵 틀 -> 클래스

붕어빵 -> 객체

 

클래스는 객체 지향 프로그래밍(OOP)에서 특정 객체를 생성하기 위해 변수와 메서드를 정의하는 틀과 같음.

클래스 내에서 상태 값 역할을 하는 멤버 변수와, 동작을 제어하는 메서드(함수)의 역할로 객체를 정의할 수 있다.

이렇게 만들어진 객체들의 조합으로 프로그래밍하는 방식을 객체 지향 프로그래밍이라고 한다.

 

클래스는 기존의 C에서 쓰이는 구조체에서 발전된 것으로, 접근 제어 지시자의 사용과 함수를 포함할 수 있는 특징이 있다.

 

class 클래스 이름{
    접근 제어 지시자:
        멤버 변수:
        멤버 함수:
};
#include <iostream>
using namespace std;

class Circle {
    public:
        int radius;   // 멤버 변수
        double getArea() {   // 멤버 함수
            return 3.14 * radius * radius;
        }
};

int main() {
    Circle pizza;   // 객체 변수
    pizza.radius = 10;
    cout << "pizza 면적은 " << pizza.getArea() << endl;
}

 

위는 피자의 면적을 출력해내는 클래스이다.

메인에서 Circle 클래스의 객체로 pizza를 선언했으므로, pizza 객체는 '.'을 통해 Circle 내에 있는 멤버 변수와 멤버 함수에 접근할 수 있게 되었다.

따라서 pizza 객체의 멤버변수 radius = 10으로 지정되었고, 

pizza.getArea()를 통해 3.14 * 10 * 10에 대한 값을 출력하게 되었다.

 

클래스명은 숫자와 _도 가능하다.

클래스 내부에 있는 변수를 멤버변수, 클래스 내부에 있는 함수를 멤버 함수라고도 한다.

 

this의 활용

#include <iostream>
using namespace std;

class Circle {
    public:
        int radius;  // 멤버 변수
        double getArea() {  // 멤버 함수
            return 3.14 * radius * radius;
        }
        void plus() {  // 멤버 함수
            this -> radius += 10;
        }
};

int main() {
    Circle pizza;
    pizza.radius = 10;
    cout << "pizza 면적은 " << pizza.getArea() << endl;
    pizza.plus();
    cout << "pizza 면적은 " << pizza.getArea() << endl;
}

 

this는 클래스 내의 멤버 변수를 의미한다. 

this는 클래스의 멤버함수를 호출할 때 사용하는 포인터이다.

 

pizza.plus()는 인수가 없는 것처럼 보이지만, 사실 pizza.plus(&pizza)가 전달된다.(pizza 인스턴스의 주소 전달)

따라서 this -> radius += 10;은 pizza -> radius += 10;과 결과적으로 같은 값이 된다.

this가 호출된 객체(인스턴스)를 가리키게 되는 것이다.

 

this는 함수 매개 변수에 불과하므로 클래스에 메모리 사용량을 추가하지 않는다.(함수가 실행되는 동안에만 스택에 매개 변수가 쌓이기 때문)

 

this를 명시적으로 사용했을 때의 이점은 다음과 같다.

 

1 : 멤버 변수와 이름이 매개 변수를 가진 생성자(또는 멤버 함수)가 있는 경우, 각각의 값을 구분 가능함.

#include <iostream>
using namespace std;

class Circle {
    public:
        int radius = 10;  # 멤버 변수
        void plus(int radius) {   # 매개  변수
            this -> radius += radius;  // this -> radius는 멤버변수, 맨 뒤 radius는 매개 변수
        }
};

int main() {
    Circle pizza;
    pizza.plus(10);
    cout << pizza.radius;
}

 

멤버 변수 이름과 매개 변수 이름이 같지만 this를 사용해 두 값을 구분할 수 있다. 그러나 처음부터 두 값을 구분하는 것이 좋다. 멤버 변수에 m_과 같은 접두사를 이용해 중복을 방지하기도 한다.(m은 member의 약자를 나타내는 코딩 관행)

 

2 : 함수 체이닝 : 함수를 연속적으로 사용한다.

함수의 반환 값을 해당 인스턴스의 레퍼런스로 전달해 연속해서 함수를 사용할 수 있도록 만들 수 있다.

멤버 함수가 *this를 반환하도록 한다.

#include <iostream>
using namespace std;

class Cal {
    public:
        int m_num = 0;    // 멤버 변수
        Cal& plus(int num) {  // 멤버 함수
            m_num += num;
            return *this;
        }
        Cal& minus(int num) {  // 멤버 함수
            m_num -= num;
            return *this;
        }
};

int main() {
    Cal cal;
    cal.plus(10).minus(5);
    cout << cal.m_num;
}

 

멤버 함수가 *this를 반환하는 것은 객체 자기 자신을 반환하는 것과 같기 때문에 연속해서 함수를 적용할 수 있다.

 

(* this포인터는 상수 포인터 자료형이므로 포인터 자체가 다른 것을 가리키도록 할 수 없다.)

 

 

생성자

객체가 생성되는 시점에서 자동으로 호출된다.(딱 한 번만 실행된다.)

객체의 초기화 역할을 한다.

이름은 클래스 이름과 동일해야하며, 여러 개를 선언할 수 있다. (생성자 오버 로딩)

만약 개발자가 생성자를 명시하지 않으면 컴파일러 지가 알아서 텅 빈 생성자를 자동으로 생성한다.

class Circle {  // 생성자 선언부
    Circle();
    Circle(int r);
    // 생성자를 중복으로 선언했으나 매개변수 부분을 다르게 선언하여 구분했다. 이를 생성자 오버 로딩이라고 한다.
};

// 생성자 구현부
Circle::Cirle() {
...
}
Circle::Circle(int r) {
...
}

 

생성자나 멤버 함수는 클래스 선언부 내에서 함수의 기능을 명시하지 않고 구현부에서 명시하는 방법으로 선언부와 구현부를 분리할 수 있다.

 

또한, 생성자는 멤버 함수와 달리 리턴 타입을 명시하지 않는다.

생성자의 주 역할은 멤버 변수의 초기화이기 때문이다.

그러나 멤버 함수를 선언하는 경우에는 반드시 리턴 타입을 명시해야만 하고, 만약 반환할 것이 없더라도 void로 명시해야 한다.

 

#include <iostream>
using namespace std;

class Circle {
    public:
        int radius;
        Circle();
        Circle(int r);
        double getArea();
};
Circle::Circle() {    # 생성자 오버로딩
    radius = 1;
    cout << "반지름 " << radius << endl;
}
Circle::Circle(int r) {    # 생성자 오버로딩
    radius = r;
    cout << "반지름 " << radius << endl;
}
double Circle::getArea() {
    return 3.14*radius*radius;
}

int main() {
    Circle donut;
    double area = donut.getArea();
    cout << "donut 면적은 " << area << endl;

    Circle pizza(30);
    area = pizza.getArea();
    cout << "pizza 면적은 " << area << endl;
}

Circle donut은 매개변수 없는 생성자를 호출한다.

생성자는 호출과 동시에 생성자 함수를 실행하도록 한다.

따라서 구현부의 Circle::Circle() 함수 부분을 실행한다.

 

Circle pizza(30)은 매개 변수 있는 생성자를 호출한다.

구현부에 있는 Circle::Circle(int r) 부분의 함수를 실행하도록 한다.

 

 

소멸자

객체가 소멸되는 시점에서 자동으로 호출됨.

소멸자는 생성자와 다르게 오버 로딩이 없고 오직 하나만 존재한다. (따라서 매개 변수 있는 소멸자 선언이 불가능하다.)

소멸자를 명시하지 않으면 컴파일러가 기본 형태의 소멸자를 만드는 부분은 생성자와 동일하다.

class Circle {
    Circle();
    Circle(int r);
    ...
    ~Circle();    // 소멸자 함수 선언
};
Circle::~Circle() {
... // 소멸자 함수 구현부
}

 

생성자와 소멸자 실행 순서

 

*지역 객체와 전역 객체 생성 소멸 순서 예시

#include <iostream>
using namespace std;

class Circle {
    public:
        int radius;
        Circle();
        Circle(int r);
        ~Circle();
        double getArea();
};
Circle::Circle(){
    radius = 1;
    cout << "반지름 " << radius << "원 생성" << endl;
}
Circle::Circle(int r){
    radius = r;
    cout << "반지름 " << radius << "원 생성" << endl;
}
Circle::~Circle(){
    cout << "반지름 " << radius << "원 소멸" << endl;
}
double Circle::getArea(){
    return 3.14*radius*radius;
}
Circle globalDonut(1000);
Circle globalPizza(2000);

void f() {
    Circle fDonut(100);
    Circle fPizza(200);
}

int main() {
    Circle mainDonut;
    Circle mainPizza(30);
    cout << "----------" << endl;
    f();
    cout << "----------" << endl;
}

 

소멸자의 실행은 중괄호가 닫힐 때 역순으로 실행된다고 생각하면 이해하기 편하다.

f() 함수가 끝나면 생성된 객체를 역순으로 소멸자가 실행되므로 200원 소멸, 100원 소멸 순으로 출력된다.

'----------'이 출력된 후 메인 함수가 종료되었으므로

메인 함수 내의 객체들이 역순으로 소멸된다.

따라서 30원, 1원 순서로 출력된다.

 

최종적으로, 가장 먼저 메모리에 자리 잡았던 전역 객체 부분이 역순으로 소멸된다.

따라서 2000, 1000원 순으로 출력된다.

 

생성 소멸 과정을 간단하게 그림으로 나타내면 다음과 같다.

 

멤버 변수 초기화 방법

728x90