티스토리 뷰

* 이전글

2016/05/06 - [C++ 기본] Hello World 시작



* 면접이 조만간 잡히게 될 것 같아 우선적으로 복습이 필요한 내용인 클래스와 상속에 대해서 문법을 간단하게 살펴보고 hackerrank.com이나 기본 소팅 알고리즘을 구현해보는 것을 할 예정이다.



* 클래스 선언

: 클래스 선언은 클래스의 전체적인 구조를 나타내며, 변수에 대한 정의와 함께 함수의 구조를 정의한다. 아래와 같은 구조는 주로 헤더 파일에 들어간다. Car.h에 정의해조자.
class Car {
    private:
        int wheels;
        int price;
    public:
        void setWheels(int);
        void setPrice(int);
        int getWheels(void);
        int getPrice(void);
};


* 클래스 정의

: 클래스 정의는 실제로 실행되는 함수의 내용 등을 표현하는 것을 의미하고 cpp 파일에 정의된다. 클래스 같은 경우에는 함수의 실제로 실행되는 내용에 대해서 넣으면 되고, cpp에는 메인 함수 등과 같이 직접 실행되는 내용들도 들어가게 된다. 아래와 같이 car.cpp를 정의하자.
#include "car.h"

void Car::setWheels(int _wheels) {
    this->wheels = _wheels;
}

void Car::setPrice(int _price) {
    this->price = _price;
}

int Car::getWheels(void) {
    return this->wheels;
}

int Car::getPrice(void) {
    return this->price;
}

: 위의 getter와 setter 함수에서 사용하고 있는 this 키워드는 현재 클래스의 인스턴스를 가리키는 포인터를 의미하며, 포인터이기 때문에 포인터의 속성을 접근할 때 사용하는 -> 연산자로 각 속성 변수들에 접근해야 한다.


* 클래스 시험

: 위의 클래스를 시험하기 위하여 아래와 같이 간단한 함수들을 main 함수에서 호출하도록 하면 된다. cap.cpp의 아래에 main 함수를 넣고 시험하는 코드를 짜보자.

#include <iostream>  

/* 위의 car.cpp 내용 넣기
 * (중략)
 **/

int main(int argc, char* argv[]) {
    Car myCar;
    myCar.setWheels(4);
    myCar.setPrice(10000);

    std::cout << "My car has " << myCar.getWheels() << " wheels.\nIt's price is " << myCar.getPrice() << " dollars" << std::endl;

    return 0;
}


: 위와 같이 실행하면 아래와 같이 정상적으로 변수들이 설정되고 가져와서 출력하는 것을 확인할 수 있다. 정상 동작을 확인하였다.





* 클래스 생성자

: 이번에는 생성자와 소멸자를 아래와 같이 추가로 정의하였다.


/* Car.h */
class Car {
    private:
        int wheels;
        int price;
    public:
        Car();
        ~Car();
        void setWheels(int);
        void setPrice(int);
        int getWheels(void);
        int getPrice(void);
};

: 아래는 car.cpp의 내용이다.

/* car.cpp */
#include <iostream> 
#include "car.h"

Car::Car() {
    this->wheels = 0;
    this->price = 0;
    std::cout << "Instance is created" << std::endl;
}

Car::~Car() {
    std::cout << "Instance is deleted" << std::endl;
}

void Car::setWheels(int _wheels) {
    this->wheels = _wheels;
}

void Car::setPrice(int _price) {
    this->price = _price;
}

int Car::getWheels(void) {
    return this->wheels;
}

int Car::getPrice(void) {
    return this->price;
}

int main(int argc, char* argv[]) {
    Car myCar;
    std::cout << "My car has " << myCar.getWheels() << " wheels for default." << std::endl;
    std::cout << "It's price is " << myCar.getPrice() << " dollars for default." << std::endl;

    myCar.setWheels(4);
    myCar.setPrice(10000);

    std::cout << "My car has " << myCar.getWheels() << " wheels." << std::endl;
    std::cout << "It's price is " << myCar.getPrice() << " dollars." << std::endl;

    return 0;
}

: 위와 같이 생성자와 소멸자를 생성한 다음, 다시 컴파일해서 실행하면 생성자와 소멸자가 자동으로 호출되는 것을 확인할 수 있다. 이렇게 생성자는 초기 값을 설정하는데 사용되며, 소멸자는 클래스 안에서 동적으로 메모리를 할당하는 경우 해제하는 소스를 넣으면 된다.





* 클래스 생성자 오버로딩(Overloading)

- yuet님 댓글로 오버라이딩->오버로딩으로 수정: 오버라이딩은 상속 하위 클래스가 함수를 덮어씌우(?)는 형식을 이야기하고 같은 함수의 이름을 다르게 호출 가능하도록 하는 것이 오버로딩이다.

: C에서 함수 오버로딩이 쉬운 것은 하나의 특징으로 생성자도 오버로딩이 가능하다. 생성자를 오버로딩하게 되면 초기화하는 방법을 다양하게 가져갈 수 있는 장점이 있다. 객체지향에서는 생성자에서부터 초기화를 하는 경우가 많기 때문에 익혀두면 좋다. 먼저 car.h에 새로운 생성자를 정의해야 한다. 
/* car.h */
class Car {
    private:
        int wheels;
        int price;
    public:
	Car();
	Car(int, int); // overriding
	~Car();
        void setWheels(int);
        void setPrice(int);
        int getWheels(void);
        int getPrice(void);
};

: 그 다음에 car.cpp에 해당 생성자의 초기화 소스 코드를 구현하면 된다.

/* car.cpp */ #include <iostream>

#include "car.h" Car::Car() { this->wheels = 0; this->price = 0; std::cout << "Instance is created" << std::endl; } /* declare the overloading constructor */ Car::Car(int _wheels, int _price) { this->wheels = _wheels; this->price = _price; std::cout << "Instance is created with wheels(" << _wheels << "), price(" << _price << ")" << std::endl; } Car::~Car() { std::cout << "Instance is deleted" << std::endl; } void Car::setWheels(int _wheels) { this->wheels = _wheels; } void Car::setPrice(int _price) { this->price = _price; } int Car::getWheels(void) { return this->wheels; } int Car::getPrice(void) { return this->price; } int main(int argc, char* argv[]) { Car myCar; std::cout << "My car has " << myCar.getWheels() << " wheels for default." << std::endl; std::cout << "It's price is " << myCar.getPrice() << " dollars for default." << std::endl; myCar.setWheels(4); myCar.setPrice(10000); std::cout << "My car has " << myCar.getWheels() << " wheels." << std::endl; std::cout << "It's price is " << myCar.getPrice() << " dollars." << std::endl; Car initCar(6, 20000); return 0; }

: 위와 같이 오버로딩하는 생성자를 정의하고, main 함수에서는 Car 변수를 정의할 때 함수 호출하듯이 정의하면 오버로딩된 생성자를 호출하여 객체를 초기화하게 된다. 위에 대한 실행 결과는 아래와 같이 나온다.




* new, delete 키워드

: 위의 경우에는 클래스를 정의한 다음 정적으로 함수 안에서만 로컬 변수로 이용하는 경우 클래스의 인스턴스를 생성하는 경우이고, 클래스의 인스턴스를 동적으로 생성, 해제하려면 new와 delete 키워드를 사용하면 된다. new와 delete는 기존의 클래스는 건드리지 않고 main 함수를 수정해서 시험해볼 수 있다. main 함수에서는 기본 생성자를 통해서 생성하고, 오버로딩 생성자를 통해서도 아래와 같이 객체를 생성할 수 있다. new 키워드에서는 메모리를 동적으로 heap에 할당한 다음 객체 포인터를 리턴하므로 변수형은 Car*과 같이 포인터형이 되어야 한다는 점을 주의 해야 한다. delete 키워드의 입력 인자는 포인터형으로 포인터형이 아닌 로컬변수 등을 넣게 되면 프로그램에 오류가 잘 발생하니 주의하자.
int main(int argc, char* argv[]) {

    Car* newCar = new Car();
    Car* newInitCar = new Car(8, 30000);

    delete newCar;
    delete newInitCar;

    return 0;
}
: 위에 대한 실행 결과는 아래와 같다.


: 이렇게 '수동'으로 new 키워드로 인스턴스를 생성하는 경우에는 '반드시', '반드시' delete로 삭제를 해주어야 한다. 가끔 복잡한 로직을 돌다가 delete가 두번 되면서 프로그램에 런타임 오류가 발생하는 경우가 있어서 초심자들은 delete를 제대로 해주지 않는 경우가 많은데(경험담..), delete를 제대로 해주지 않으면 프로그래밍에 있어서 가장 잡기 힘든 버그 중 하나인 메모리 누수(memory leak)가 발생해서 프로그램이 차지하고 있는 메모리가 점점 커지는 이상 증상이 생기기도 하니, new를 해줄 때부터 인스턴스의 생성 시점과 삭제 시점에 대한 계획을 잘 세워서 미리 작성해두면 좋다. C++을 개발하는 개발자라면 언젠가는 반드시 메모리 누수를 경험하게 될 것이다.

* 클래스 상속

: 객체지향에서 상속은 반드시 알고 넘어가야할 개념으로 부모 클래스를 정의하면 서브클래스는 proctected 항목들과 public 항목들을 자기의 것인 것처럼 사용할 수 있다. 이러한 개념은 간단한 객체지향 프로그래밍에서는 큰 소용이 없지만, 복잡한 객체지향 프로그래밍을 구현하게 되면 반복되거나 중복되는 기능들을 편리하게 관리하고 개발할 수 있다. Car 클래스를 상속하는 SUV 클래스를 정의서 시험해보자.
/* suv.h */

#include "car.h"

class SUV : public Car {
    private:
        int spareWheels;
    public:
        void setSpareWheels(int);
        int getSpareWheels(void);
};

: 위의 경우 보면 car.h를 include 하고 있고, main 함수에서 suv.h를 include하게 되면 전체적으로 : dcar.h는 두 번 로드를 하게 된다. 이렇기 때문에 오류가 일어난다면 한번만 include 될 수 있도록 전처리 단계에서 #ifndef 를 사용하면 좋다.

/* car.h */
#ifndef CAR_H
#define CAR_H

class Car {
    private:
        int wheels;
        int price;
    public:
        Car();
        Car(int, int);
        ~Car();
        void setWheels(int);
        void setPrice(int);
        int getWheels(void);
        int getPrice(void);
};

#endif

: 위와 같이 CAR_H에 대해서  ifndef로 검사하여 정의되지 않았으면 CAR_H를 정의하면서 car.h 파일의 내용들을 선언하고, 만약 CAR_H가 정의되어있다면, #endif로 넘어가서 재선언을 하지 않고 넘어가게 된다. 그러면 이어서 suv.cpp의 예를 살펴보면 아래와 같다.

#include "suv.h" void SUV::setSpareWheels(int _spareWheels) { this->spareWheels = _spareWheels; } int SUV::getSpareWheels(void) { return this->spareWheels; }

: 그리고 main 함수를 수정하여 해당 클래스가 시험이 될 수 있도록 아래와 같이 구현하였다.

int main(int argc, char* argv[]) {
    SUV* newSUV = new SUV();

    newSUV->setSpareWheels(2);
    std::cout << "Spare wheels are " << newSUV->getSpareWheels() << std::endl;

    newSUV->setPrice(5000);
    std::cout << "The price is " << newSUV->getPrice() << std::endl;

    delete newSUV;

    return 0;
}
: 먼저 SUV 클래스의 함수와 변수를 호출해서 시험해보고 부모클래스인 Car 클래스의 setPrice, getPrice 함수도 시험해보고 있다. 이에 대한 실행 결과는 아래와 같다.


: SUV에 있는 함수들과 부모클래스인 Car의 함수들인 setPrice와 getPrice도 정상 동작하고 있는 것을 확인할 수 있다. 여기서 Car클래스의 price 변수는 private으로 정의하였지만, setPrice와 getPrice 함수는 public이기 때문에 자식 클래스인 SUV에서는 price 변수에 직접 접근하지 못하고 public인 getter, setter 함수를 통해서 접근해야 한다. 이러한 식으로 private으로 설정하고 외부에서 중요한 데이터에 접근하지 못하게 하고 접근할 때에는 함수 등의 인터페이스를 제공하는 것이 객체 지향의 중요한 개발방법론이다. 만약 자식 클래스에서 직접 price나 wheels 변수에 접근하고 싶으면 변수의 접근설정을 protected로 바꾸면 된다. 아래는 protected를 사용하여 변수에 직접 접근 가능하도록 한 예이다. SUV 클래스에서 전체 바퀴수를 가져오는 getTotalWheels 함수를 변수에 직접 접근하여 작성하였다.
/* suv.h */
#include "car.h"

class SUV : public Car {
    private:
        int spareWheels;
    public:
        void setSpareWheels(int);
        int getSpareWheels(void);

        int getTotalWheels(void);
};

: 그리고 suv.cpp에 이 함수를 정의하는 부분도 추가하였다.
int SUV::getTotalWheels(void) {
    return this->spareWheels + this->wheels;
}
: 위와 같이 this->wheels로 직접 부모클래스의 wheels에 접근하고 있는 소스를 컴파일해보면 아래와 같은 컴파일 오류가 일어난다.


: 오류 설명을 보면 'Car'의 private 멤버변수인 wheels에 접근하려고 해서 오류가 일어나고 있다. 그러면 여기서 private으로 정의된 변수들을 protected로 바꾸면 오류가 안 일어난다. 아래와 같이 car.h와 시험을 위한 케이스로 car.cpp의 메인 함수를 수정하였다.

/* car.h */
#ifndef CAR_H
#define CAR_H

class Car {
    protected:
        int wheels;
        int price;
    public:
    Car();
    Car(int, int);
    ~Car();
        void setWheels(int);
        void setPrice(int);
        int getWheels(void);
        int getPrice(void);
};
#endif

: main 함수의 시험 케이스는 아래와 같이 구현하였다. SUV를 생성해서 spareWheels를 설정하고 wheels를 설정한 다음, SUV의 getTotalWheels를 호출해보는 순서대로 이루어진다.

int main(int argc, char* argv[]) {
    SUV* newSUV = new SUV();

    newSUV->setSpareWheels(2);
    std::cout << "Spare wheels are " << newSUV->getSpareWheels() << std::endl;

    newSUV->setWheels(4);
    std::cout << "The wheels are " << newSUV->getWheels() << std::endl;

    std::cout << "The total number of wheels are: " << newSUV->getTotalWheels() << std::endl;

    delete newSUV;

    return 0;
}

: 위와 같이 private 변수를 protected로 수정하면 이제 정상적으로 컴파일되고 실행되는 것을 확인할 수 있다.




* 클래스 friend

: 위와 같이 private 변수를 외부에서는 접근을 하지 못하지만, 클래스 안에 friend 키워드를 명시하면 해당 클래스 안의 private 멤버 변수들에 접근할 수 있도록 권한을 부여해줄 수 있다. 아래는 자동차를 점검하는 Engineer라는 새로운 클래스를 만들어서 Car의 wheels에 접근 가능하도록 friend로 지정을 해줄 것이다. 먼저 아래는 engineer.h 파일이다.
/* engineer.h */
#include "car.h"

class Engineer {
    public:
        int getWheelsInCar(Car*);
};
: 그리고 아래는 Engineer 클래스의 정의를 한 engineer.cpp 파일이다. 여기서 Car의 포인터를 받아서 Car의 private 멤버 변수인 wheels에 직접 접근하려고 하고 있다.
/* engineer.cpp */
#include "engineer.h"

int Engineer::getWheelsInCar(Car* car) {
    return car->wheels;
}
: 현재 상태로는 Engineer 클래스는 Car 클래스와 아무런 상관관계가 없으므로 컴파일을 하려고 하면 private 멤버변수에 접근하려고 한다고 하면서 컴파일 오류가 뜰 것이다. 따라서 Car 클래스에서 Engineer 클래스가 private 멤버변수에 접근할 수 있도록 접근 권한을 부여해주어야 한다. 아래와 같이 Engineer 클래스가 friend라는 것을 클래스 안에서 명시하면 된다.
/* car.h */
#ifndef CAR_H
#define CAR_H

class Car {
    protected:
        int wheels;
        int price;
    public:
	Car();
	Car(int, int);
	~Car();
        void setWheels(int);
        void setPrice(int);
        int getWheels(void);
        int getPrice(void);

        friend class Engineer;
};
#endif
: 이렇게 명시하고 컴파일을 하게 되면 더이상 Engineer에서 Car의 private 멤버변수로 접근을 시도하고 있다는 컴파일 오류는 발생하지 않는다. 이것을 시험해보기 위한 main 함수는 아래와 같이 작성하였다.
int main(int argc, char* argv[]) {}
    SUV* newSUV = new SUV();

    newSUV->setWheels(4);
    std::cout << "The wheels are " << newSUV->getWheels() << std::endl;

    Engineer engineer;
    std::cout << "There are " << engineer.getWheelsInCar(newSUV) << " wheels in this car" << std::endl;

    delete newSUV;

    return 0;
}
: 그리고 이 실행결과는 아래와 같이 Engineer 클래스가 Car 클래스의 private 멤버변수에 접근하고 있는 것을 확인할 수 있다.


: 이렇게 private 변수에 접근해야 될 필요성이 있을 때에는 friend 키워드를 사용해서 다른 클래스에 접근 권한을 부여해줄 수 있지만, 이렇게 변수에 직접 접근하도록 하는 것보다는 상속이나 함수를 통해서 변수에 접근할 수 있는 별도의 인터페이스를 제공하는 것이 객체지향 프로그래밍에서는 조금 더 바람직할수도 있다. 그리고 이를 encapsulation이라고도 한다.


* Polymorphism = 다형성

: Polymorphism은 한글로 다형성이라고 하며, 이는 같은 부모의 클래스를 두고 있는 여러 개의 클래스들이 같은 기능을 하는 함수를 오버라이딩해서 밖에서 봤을 때에는 같지만, 내부적으로 동작하는 로직이 다를 때 Polymorphism을 사용한다고 이야기한다. 아래는 간단한 Car를 부모클래스도 사용하는 새로운 함수 Motorcycle 클래스를 추가한 예이다. 먼저 motorcycle.h에서 새로운 클래스를 정의한 예이다. 지금은 사용하지 않을 것이지만, 나중에 beep 함수를 사용할 예정인 beep 함수를 미리 선언 해두었다.

/* motorcycle.h */
#include "car.h"

class Motorcycle : public Car {
    public:
        void beep();
};

: 그리고 아래는 motorcycle.cpp에서 beep 함수를 아래와 같이 정의하였다.

/* motorcycle.cpp */
#include <iostream>
#include "motorcycle.h"

void Motorcycle::beep() {
    std::cout << "BEEP!" << std::endl;
}

: 그리고 이를 시험하기 위한 main함수는 아래와 같이 작성하였다. 기존의 SUV와는 조금 다르게 이번에는 new Motorcycle()로 초기화를 하지만, 그 부모의 포인터 형인 Car*로 받고 있는 것을 확인할 수 있다.

int main(int argc, char* argv[]) {
    SUV* newSUV = new SUV();

    newSUV->setWheels(4);
    std::cout << "The wheels are " << newSUV->getWheels() << std::endl;

    Car* motorcycle = new Motorcycle();
    motorcycle->setWheels(2);
    std::cout << "The wheels are " << motorcycle->getWheels() << std::endl;

    delete newSUV;
    delete motorcycle;

    return 0;
}

: 이렇게 자식 클래스인 Motorcycle 클래스의 인스턴스를 생성하였지만, Car의 포인터로 받을 수 있으며, 이렇게 되면 Car에서 정의한 함수들을 그대로 사용할 수 있다. 이 때에 Car의 포인터이기 때문에 Motorcycle에 있는 함수와 변수들에 대한 정보는 없어서 Motorcycle에 있는 함수, 위에서는 beep 함수에 접근할 수 없다는 것을 참고하면 좋을 것이다. 이에 대한 실행 결과는 아래와 같다.



: 여기서 Car 함수에 beep 함수를 추가해서 Motorcycle의 beep 함수가 오버라이딩하게 하면 아래와 같이 소스에 car.h와 car.cpp에 추가할 수 있을 것이다.

/* car.h */
#ifndef CAR_H
#define CAR_H

class Car {
    protected:
        int wheels;
        int price;
    public:
	Car();
	Car(int, int);
	~Car();

        void setWheels(int);
        void setPrice(int);
        int getWheels(void);
        int getPrice(void);

        void beep(void);

        friend class Engineer;
};
#endif

: 그리고 car.cpp는 아래와 같이 정의를 추가하면 된다.

void Car::beep(void) {
    std::cout << "No horne" << std::endl;
}

: main 함수에서 beep 함수가 어떻게 호출되는지 학인하기 위하여 아래와 같이 구현하였다. 하나의 Motorcycle 인스턴스는 원래의 Motorcycle 포인터에 할당해주고, 또 다른 인스턴스는 Car*에 할당해주었다.

int main(int argc, char* argv[]) {
    Car* polyMotorcycle = new Motorcycle();
    Motorcycle* motorcycle = new Motorcycle();

    polyMotorcycle->beep();
    motorcycle->beep();
    
    delete motorcycle;
    delete polyMotorcycle;

    return 0;
}
: 이렇게 실행한 결과는 아래와 같이 동일하게 인스턴스를 초기화했는데 첫번째 Car*에 할당한 경우 Car 클래스의 beep함수가 호출되었고, Motorcycle*에 할당한 경우에는 Motorcycle의 beep 함수가 호출 되었다.


: 이렇게 함수를 오버라이딩 했는데 부모 클래스의 포인터로 함수를 호출하면 부모클래스의 함수가 호출되는 현상을 해결하고 자식클래스에 정의된 함수가 호출되도록 도와주는 키워드는 virtual 키워드이다.



* virtual 키워드

: virtual 키워드는 부모 클래스에서 함수를 가상으로 정의해 놓고, 실제로 구현은 자식 클래스에서 하고자 하는 경우에 사용하는 것으로 이해하면 편리하다. 이는 다형성을 이용하는데 있어서 반드시 활용하면 아주 유용하게 사용할 수 있을 것이다. virtual 키워드를 사용하기 위해서는 앞선 예에서 그냥 car.h에 Car 클래스 선언에서 beep 함수 앞에 virtual을 아래와 같이 넣으면 된다.
/* car.h */
#ifndef CAR_H
#define CAR_H

class Car {
    protected:
        int wheels;
        int price;
    public:
	Car();
	Car(int, int);
	~Car();

        void setWheels(int);
        void setPrice(int);
        int getWheels(void);
        int getPrice(void);

        virtual void beep(void);

        friend class Engineer;
};
#endif

: 이렇게 하고나서 위의 main 함수를 다시 실행해보면 결과가 바뀌어서 원래 Motorcycle 인스턴스가 생성된대로 Motorcycle 클래스의 beep 함수가 실행되는 것을 확인할 수 있다.




: 지금과 같이 간단한 상황에서는 virtual을 사용해야 하는 이유를 잘 모를 수 있지만, 복잡한 프로그램을 개발할 때 일반적인 말로 "컴포넌트"화 시켜서 모듈들을 관리할 때 이 virtual 키워드를 사용하는 것은 필수일 것이다. 일반적으로 모듈들은 하나의 공통된 부모 클래스를 두고 각각의 종류에 따라서 동작을 특성화시켜서 변경하기 때문에 그럴 때 이 virtual 키워드를 사용하면 되는 것이다.



* 클래스 operator

: 객체지향 프로그래밍을 할 때 자주 사용하는 기능 중 하나는 바로 operator이다. 이 operator는 처음 접하면 잘 모르겠다가도, 일단 익숙해지면 아주 편리해지는 재미있는 기능이다. 가장 많이 사용해야 하는 operator는 아마 = operator로 일반적으로 특정 클래스의 인스턴스에 =이 들어왔을 때 어떻게 동작할지 알려주는 operator 함수이다. 아래와 같이 Car의 = operator를 정의할 수 있다.

/* car.h */
#ifndef CAR_H
#define CAR_H

class Car {
    protected:
        int wheels;
        int price;
    public:
	Car();
	Car(int, int);
	~Car();

        void setWheels(int);
        void setPrice(int);
        int getWheels(void);
        int getPrice(void);

        virtual void beep(void);

        Car& operator=(const Car&);

        friend class Engineer;
};
#endif

: 다른 클래스의 operator=을 선언할 때에도 유사하게 하면 된다. 아래와 같이 내부 로직을 car.cpp에서 정의하였다.

Car& Car::operator=(const Car& rightCar) {
    this->wheels = rightCar.wheels;
    this->price = rightCar.price;
    return *this;
}

: 위의 소스만 보면 쉽게 이해하기 힘들면 아래의 실제 사용되는 상황을 보고 이해하면 된다.

int main(int argc, char* argv[]) {
    Car* myCar = new Car(4,10000);
    Car usedCar = *myCar;

    std::cout << "Used car has " << usedCar.getWheels() << " wheels, and the price is " << usedCar.getPrice() << std::endl;

    return 0;
}

: 위의 Car usedCar = *myCar로 되어있는 부분을 보면 usedCar가 = 연산자의 왼쪽 객체이고, 위의 operator= 함수가 호출되는 주체이다. 따라서, 위의 operator= 함수 안에서 사용하고 있는 this는 usedCar로 보면 된다. 그리고 rightCar의 인자로 들어오는 Car는 operator=의 오른쪽 객체로 *myCar이다. 여기서 myCar는 wheels는 4로, price는 10000로 초기화 되어서 인자로 넘어간다. 그리고 operator= 함수 안에서 오른쪽의 인자로 넘어온 객체에 대한 멤버변수의 값들을 왼쪽 객체로 옮겨주면 operator= 함수에 대한 처리는 끝난다. 이렇게 operator= 함수는 클래스 객체를 대상으로 =을 사용하게 되면 호출되는 것이다. 다른 유용한 operator 함수는 아마 위의 std::cout에서 자주 쓰고 있는 << operator 일 것이다. 이 operator는 =과는 달리 멤버함수로 오버라이트하는 것이 아니라 외부에 operator<< 함수에서 Car를 인자로 받는 함수에서 friend 키워드로 접근하도록 권한을 부여하여 출력이 가능하도록 할 것이다. 아래는 먼저 car.h에서 friend로 operator<<함수를 선언한 것이다.

/* car.h */ #ifndef CAR_H #define CAR_H #include <iostream>

class Car { protected: int wheels; int price; public: Car(); Car(int, int); ~Car(); void setWheels(int); void setPrice(int); int getWheels(void); int getPrice(void); virtual void beep(void); Car& operator=(const Car&); friend std::ostream& operator<<(std::ostream&, const Car&); friend class Engineer; }; #endif

: ostream의 입출력 값을 가지기 때문에 iostream의 include를 추가하였고, 이 선언은 멤버함수로서 선언한다기 보다는 외부에 있는 operator<< 함수가 멤버변수들에 접근 가능하도록 외부 함수에 friend로 권한을 부여한 것이라고 생각하면 된다. car.cpp에는 아래와 같이 operator<< 함수를 정의하였다.

std::ostream& operator<<(std::ostream& os, const Car& car) {
    os << "This car has " << car.wheels << " wheels, and the price is " << car.price;
    return os;
}

: 주의 할점은 Car의 멤버함수로 Car::operator<<를 한 것이 아니라 바로 operator<<로 외부에 있는 함수로 정의한 것이다. 왜냐하면 std::cout 등의 ostream에서 사용해야 하기 때문에 외부 함수로 정의한 것이다. 그리고 이를 시험하기 위하여 아래와 같이 main 함수의 로직을 작성하였다.

int main(int argc, char* argv[]) {
    Car* myCar = new Car(4,10000);
    Car usedCar = *myCar;

    std::cout << usedCar << std::endl;
    delete myCar;
    return 0;
}

: 원래 std::cout 에서 문자열들을 복잡하게 넣었던 내용들이 이제 간단하게 클래스의 인스턴스 자체를 넣고도 출력이 가능하게 되어 개발하는데 있어서 편리하게 활용할 수 있을 것이다. 이렇게 operator에 따라서 =이나 ++과 같은 binary 연산자도 있지만, +나 - 등과 같은 unary 연산자에 대해서 다르게 정의하고 인자를 정의해야 하니 각각에 대해서는 각 operator의 예제를 참고하거나 레퍼런스를 찾아보면 좋다.



* template T 클래스

: template T 클래스는 위의 다형성을 사용하는 경우와는 다르게 특정 클래스가 다른 여러 가지 클래스의 입력을 유동적으로 받아서 관리할 수 있도록 하는 기능을 수행한다. 이에 대하여 다형성과 함께 활용하면 좋기도 하지만, 템플릿 클래스 T는 클래스가 아닌 int나 double 등과 같은 기본형들도 활용할 수 있어서 활용 범위가 조금 더 넓기도 하다. 하지만 소스의 가독성은 조금 떨어질 수도 있어서 정말 필요한 경우가 아니면 많이 사용하는 경우는 없을 수 있다. 이것을 가장 많이 이용하는 경우는 트리나 해쉬맵 등과 같은 데이터구조이고, 일반적으로는 많이 사용하지는 않는다. 그래도 일반적으로 사용되는 경우를 앞서 살펴본 예를 확장해서 보면 아래와 같이 활용할 수 있을 것이다. 예를 들면, 앞서 사용한 Engineer가 다루는 기계를 서로 다른 클래스로 하고자 할 때, Engineer를 template 클래스로 아래와 같이 정의가 가능하다.

/* engineer.h */
#ifndef ENGINEER_H
#define ENGINEER_H

#include "car.h"

template 
class Engineer {
    private:
        T mechanic;
    public:
        int getWheelsInCar(Car*);
        void setMechanic(T);
        T getMechanic(void);
};
#endif

: 위와 같이 template을 클래스 상단에 정의하고 해당 템플릿 클래스와 관련된 변수, 함수들의 정의가 가능하다. engineer.cpp에는 아래와 같이 기존의 함수 정의에 template 키워드를 추가하여 정의가 가능하다.

/* engineer.cpp */
#include "engineer.h"


template 
int Engineer::getWheelsInCar(Car* car) {
    return car->wheels;
}

template 
void Engineer::setMechanic(T _mechanic) {
    this->mechanic = _mechanic;
}

template 
T Engineer::getMechanic(void) {
    return this->mechanic;
};

: 그리고 기존에 Engineer를 friend로 추가했던 Car 클래스도 Engineer가 템플릿 클래스로 변경이 되었으므로 템플릿으로 바꿔줘야 한다. 아래는 수정된 car.h 파일이다.

/* car.h */
#ifndef CAR_H
#define CAR_H

#include <iostream> 

class Car {
    protected:
        int wheels;
        int price;
    public:
	Car();
	Car(int, int);
	~Car();

        void setWheels(int);
        void setPrice(int);
        int getWheels(void);
        int getPrice(void);

        virtual void beep(void);

        Car& operator=(const Car&);
        friend std::ostream& operator<<(std::ostream&, const Car&);

        template  friend class Engineer;
};
#endif

: 이제 준비는 어느 정도 끝났고, Car말고 다른 기계를 다루는 Engineer 클래스를 정의하기 위하여 Computer 클래스를 새로 정의하였다. 아래는 computer.h이다.

/* computer.h */
class Computer {
};

: 이제 main 함수에서 시험해보기 위하여 아래와 같이 구현이 가능하다.

int main(int argc, char* argv[]) {
    Car* myCar = new Car(4,10000);
    Engineer carEngineer;
    carEngineer.setMechanic(*myCar);
    delete myCar;


    Engineer computerEngineer;
    Computer myComputer;
    computerEngineer.setMechanic(myComputer);

    return 0;
}

: 이상이 없어 보이지만, 위의 경우 컴파일을 해보면 링크에러가 일어난다. 왜냐하면 Engineer나 Engineer와 같이 정의가 되면, 해당 정의된 내용에 접근해서 해당 Car나 Computer를 어떻게 처리할지 결정이 가능하다. 하지만 위의 경우에는 Engineer의 정의가 engineer.cpp에 있었기 때문에 이에 대하여 car.cpp에서는 직접 접근이 불가능하여 별도로 main 함수가 있는 car.cpp에 아래와 같이 include를 추가해주면 된다.

#include "engineer.cpp"

: 이렇게 하면 정상적으로 동작하는 것을 확인할 수 있다. 하지만 위와 같이 템플릿을 사용하게 되는 경우에는 일반적으로 T를 이용해서 별도로 부가적인 함수 호출이나 기능 제공을 하기가 어렵고, 앞서 이야기한대로 데이터구조, 데이터 관리, 즉 모델의 관점에서는 유용하게 활용이 가능하나, 비즈니스 로직을 넣기에는 부족함이 많다. 따라서 이러한 템플릿의 사용이 필요한 부분은 대부분 이미 STL 등과 같은 라이브러리에서 제공해주고 있고, 비즈니스 로직에 대한 구현이 필요한 부분은 다형성 정도의 객체지향 개념만 활용해도 충분히 커버 가능할 것이다.


끝.


* 다음편 예고

: 다음에는 C++로 기본적인 자료구조를 구현하는 것과 각 정렬들을 구현해보도록 할 것이고, 그 이후에 다시 C++ 기본에 맞게 문법관련해서 정리할 예정이다. 



* 다음편

2016/05/13 - [C++ 응용] 퀵, 머지, 힙, 버블, 선택, 삽입 정렬 알고리즘 구현

2016/05/19 - [C++ 기본] 변수형과 기본 자료 구조(vector, map, set)


공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함