이전 포스팅에서 객체를 생성하고 객체의 멤버변수 초기화를 목적으로 함수를 정의하고 호출하였습니다. 정보은닉을 목적으로 멤버변수들을 private 으로 선언했습니다. 생성자라는 것을 이용하면 객체도 생성과 동시에 초기화할 수 있습니다.
생성자의 이해
클래스의 이름과 함수의 이름이 동일합니다. 반환형이 선언되어 있지 않으며, 실제로 반환하지 않습니다. 이러한 유형이 함수를 가르켜 생성자라고 합니다. 객체 생성시 딱 한번 호출된다는 특징을 가지고 있습니다. 이전에 생성자를 정의하지 않았을 때, 우리는 다음과 같은 방식으로 객체를 생성하였습니다.
SimpleClass sc;
SimpleClass * ptr = new SimpleClass;
생성자가 정의되었으니, 객체생성과정에서 자동으로 호출되는 생성자에게 전달할 인자의 정보를 다음과 같이 추가해야 합니다. 생성자도 함수의 일종이니 오버로딩이 가능합니다. 생성자도 함수의 일종이니 매개변수에 디폴트 값을 설정할 수 있습니다.
#include <iostream> using namespace std; class SimpleClass { private: int num1; int num2; public: SimpleClass() { num1 = 0; num2 = 0; } SimpleClass(int n) { num1 = n; num2 = 0; } SimpleClass(int n1, int n2) { num1 = n1; num2 = n2; } /* SimpleClass(int n1=0, int n2=0) { num1=n1; num2=n2; } */ void ShowData() const { cout << num1 << ' ' << num2 << endl; } }; int main(void) { SimpleClass sc1; sc1.ShowData(); SimpleClass sc2(100); sc2.ShowData(); SimpleClass sc3(100, 200); sc3.ShowData(); return 0; } |
컴파일 및 실행결과를 통해서 생성자의 오버로딩이 가능하다는 사실을 확인할 수 있습니다. 디폴트 처리 되어 있는 함수의 주석을 해제하고 컴파일 및 실행하면, 매개변수의 디폴트 값 설정도 가능함을 확인할 수 있습니다. 가장 처음 생성자는 int형 데이터를 인자로 요구하므로, 이 생성자를 이용해서 객체를 생성하려면 다음과 같이 문장을 구성해야 합니다.
SimpleClass sc2(100);
SimpleClass * ptr2 = new SImpleClass(100);
#include <iostream> using namespace std; class SimpleClass { private: int num1; int num2; public: SimpleClass(int n1=0, int n2=0) { num1 = n1; num2 = n2; } void ShowData() const { cout << num1 << ' ' << num2 << endl; } }; int main(void) { SimpleClass sc1(); SimpleClass mysc = sc1(); mysc.ShowData(); return 0; } SimpleClass sc1() { SimpleClass sc(20, 30); return sc; } |
main 함수 내에 지역적으로 함수의 원형이 선언되었습니다. 이는 정의되어 있는 함수의 호출을 위한 것입니다. sc1 함수를 호출하여, 이 때 반환되는 객체의 값으로 mysc 객체를 초기화하고 있습니다. 보통 함수의 원형은 전역적으로(함수 밖에) 선언하지만, 위 예제에서 보이듯이 함수내에 지역적으로도 선언 가능합니다. SimpleClass sc1(); 은 함수의 원형 선언에 해당합니다. 이 문장을 void형 생성자의 호출문으로 인정해 버리면, 컴파일러는 이러한 문장을 만났을 떄, 이것이 객체생성문이닞 함수의 원형 선언인지 구분할 수 없게 됩니다. 그래서 이러한 유형의 문장은 객체생성이 아닌, 함수의 원형선언에만 사용하기로 약속하였습니다.
이전 예제에 대한 생성자의 활용
#include <iostream> using namespace std; class FruitSeller { private: int APPLE_PRICE; int numOfApples; int myMoney; public: FruitSeller(int price, int num, int money) { APPLE_PRICE = price; numOfApples = num; myMoney = money; } int SaleApples(int money) { int num = money / APPLE_PRICE; numOfApples -= num; myMoney += money; return num; } void ShowSalesResult() const { cout << "remaining apples: " << numOfApples << endl; cout << "sales profit: " << myMoney << endl << endl; } }; class FruitBuyer { private: int myMoney; int numOfApples; public: FruitBuyer(int money) { myMoney = money; numOfApples = 0; } void BuyApples(FruitSeller &seller, int money) { numOfApples += seller.SaleApples(money); myMoney -= money; } void ShowBuyResult() const { cout << "current money: " << myMoney << endl; cout << "apple numbers: " << numOfApples << endl << endl; } }; int main(void) { FruitSeller seller(1000, 20, 0); FruitBuyer buyer(5000); buyer.BuyApples(seller, 2000); cout << "seller situation" << endl; seller.ShowSalesResult(); cout << "buyer situation " << endl; buyer.ShowBuyResult(); return 0; } |
클래스 내에 public 바로 아래에 생성자가 추가되었습니다. 셀러와 바이어 변수들에 대해 초기화 하였습니다. 생성자는 멤버변수의 초기화를 목적으로 정의가 되니, 객체 생성과정에서의 생성자 호출은 객체의 초기화를 한결 수월하게 합니다.
멤버 이니셜라이저(Member Initialize)를 이용한 멤버 초기화
#ifndef __POINT_H_ #define __POINT_H_ class Point { private: int x; int y; public: Point(const int &xpos, const int &ypos); int GetX() const; int GetY() const; bool SetX(int xpos); bool SetY(int ypos); }; #endif
|
#ifndef __RECTANGLE_H_ #define __RECTANGLE_H_ #include "Point.h" class Rectangle { private: Point upLeft; Point lowRight; public: Rectangle(const int &x1, const int &y1, const int &x2, const int &y2); void ShowRecInfo() const; }; #endif |
#pragma warning(disable:4996) #include <iostream> #include "Point.h" using namespace std; Point::Point(const int &xpos, const int &ypos) { x = xpos; y = ypos; } int Point::GetX() const { return x; } int Point::GetY() const { return y; } bool Point::SetX(int xpos) { if (0 > xpos || xpos > 100) { cout << "error" << endl; return false; } x = xpos; return true; } bool Point::SetY(int ypos) { if (0 > ypos || ypos > 100) { cout << "error" << endl; return false; } y = ypos; return true; } |
#include <iostream> #include "Rectangle.h" using namespace std; Rectangle::Rectangle(const int &x1, const int &y1, const int &x2, const int &y2) :upLeft(x1, y1), lowRight(x2, y2) { //empty } void Rectangle::ShowRecInfo() const { cout << "left upper: " << '[' << upLeft.GetX() << ", "; cout << upLeft.GetY() << ']' << endl; cout << "right lower: " << '[' << lowRight.GetX() << ", "; cout << lowRight.GetY() << ']' << endl << endl; } |
#include<iostream> #include "Point.h" #include "Rectangle.h" using namespace std; int main(void) { Rectangle rec(1, 1, 5, 5); rec.ShowRecInfo(); return 0; } |
클래스다운 모습을 갖추었습니다. 멤버 이니셜라이저를 사용하다 보면, 생성자의 몸체 부분이 그냥 비는 일이 종종 발생합니다. 마지막으로 우리는 객체의 생성과정을 다음과 같이 정리할 수 있습니다. 1단계로 메모리 공간의 할당, 2단계로 이니셜라이저를 이용한 멤버변수(객체)의 초기화 그리고 3단계로 생성자의 몸체부분 실행입니다. C++의 모든 객체는 위의 3가지 과정을 순서대로 거쳐서 생성이 완성됩니다. 물론 이니셜라이저가 선언되지 않았따면, 메모리 공간의 할당과 생성자의 몸체 부분의 실행으로 객체생성은 완성이 됩니다. 생성자는 이니셜라이저처럼 선택적으로 존재하는 대상이 아닙니다. 우리가 처음에 정의했던 클래스에는 생성자가 존재하지 않았습니다. 따라서 생성자는 있을 수도 없을 수도 있는 것으로 생각하기 쉽다. 하지만 생성자는 반드시 호출이 됩니다. 우리가 생성자를 정의하지 않으면, 디폴트 생성자라는 게 자동으로 삽입되어 호출이 됩니다.
멤버 이니셜라이저를 이용한 변수 및 상수(변수) 초기화
클래스에서 보이듯이 객체가 아닌 멤버변수도 이니셜라이저를 통해서 초기화가 가능합니다. 이니셜라이저의 다음 문장은 num1(n1) num1을 n1의 값으로 초기화하라는 뜻이 됩니다. 따라서 프로그래머는 생성자의 몸체에서 초기화 하는 방법과 이니셜라이저를 이용하는 초기화 방법 중에서 선택이 가능합니다. 일반적으로 멤버변수의 초기화에 있어서는 이니셜라이저를 선호하는 편입니다. 초기화의 대상을 명확히 인식할 수 있다라는 장점과 성능에 약간의 이점이 있기 때문입니다. 이니셜라이저를 이용하면 선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성됩니다. 반면, 생성자의 몸체부분에서 대입연산을 통한 초기화를 진행하면, 선언과 초기화를 각각 별도의 무낮엥서 진행하는 형태로 바이너르 코드가 생성됩니다. const 멤버 변수도 이니셜라이저를 이용하면 초기화가 가능합니다.
const 멤버변수가 이니셜라이저를 통해서 초기화가 가능하다는 사실을 확인하면 됩니다. const 변수는 상수화시키는 키워드입니다. 따라서 const 선언에 의해서 상수화 된 변수를 가리켜 const 변수라고 하고, const 상수라고도 합니다. 이 둘은 같은 의미로 사용이 됩니다.
이니셜라이저의 특징은 멤버변수로 참조자를 선언할 수 있게 합니다.
const 변수와 마찬가지로 참조자도 선언과 동시에 초기화가 이뤄져야 합니다. 따라서 이니셜라이저를 이용하면 참조자도 멤버변수로 선언될 수 있습니다.
#include <iostream> using namespace std; class AAA { public: AAA() { cout << "empty object" << endl; } void ShowYourName() { cout << "I'm class AAA" << endl; } }; class BBB { private: AAA &ref; const int # public: BBB(AAA &r, const int &n) :ref(r), num(n) { } void ShowYourName() { ref.ShowYourName(); cout << "and" << endl; cout << "I ref num" << num << endl; } }; int main(void) { AAA obj1; BBB obj2(obj1, 20); obj2.ShowYourName(); return 0; } |
객체 생성시 실제로 생성자가 호출되는지 확인하기 위해서 삽입한 출력문입니다. 참조자가 멤버변수로 선언되었습니다. 따라서 이니셜라이저를 통해서 초기화를 해야 합니다. const 참조자가 선언되었습니다. 따라서 이니셜라이저를 통해서 정수형 상수로도 초기화가 가능합니다.
디폴트 생성자
메모리 공간의 할당 이후에 생성자의 호출까지 완료되어야 객체라 할 수 있습니다. 즉, 객체가 되기 위해서는 반드시 하나의 생성자가 호출되어야 합니다. 이러한 기준에 예외를 두지 않기 위해서 생성자를 정의하지 않는 클래스에는 C++ 컴파일러에 의해서 디폴트 생성자라는 것이 자동으로 삽입됩니다. 따라서, 모든 객체는 한번의 생성자 호출을 동반합니다. 이는 new 연산자를 이용한 객체의 생성에도 해당하는 이야기입니다. 즉, 위의 클래스ㄴ를 다음의 형태로 생성해도 객체의 생성과정에서 생성자가 호출됩니다. malloc 함수호출 시, 실제로 AAA 클래스의 크기정보만 바이트 단위로 전달되기 때문에 생성자가 호출될 리 없습니다. 따라서 객체를 동적으로 할당하려는 경우에는 반드시 new 연산자를 이용해야 합니다.
생성자 불일치
매개 변수가 void 형으로 선언되는 디폴트 생성자는, 생성자가 하나도 정의되어 있지 않을 때에만 삽입이 됩니다. 즉, 다음과 같이 정의된 클래스에는 디폴트 생성자가 삽입되지 않습니다.
Private 생성자
객체의 생성이 클래스의 외부에서 진행되기 떄문에 생성자는 public 으로 선언되어야 합니다. 그럼 클래스 내부에서 객체를 생성한다면, 생성자가 private 으로 선언되어도 됩니다. 그래서 클래스 내부에서만 객체의 생성을 허용하려는 목적으로 생성자를 private 으로 선언하기도 합니다.
#include <iostream> using namespace std; class AAA { private: int num; public: AAA() : num(0) {} AAA& CreateInitObj(int n) const { AAA * ptr = new AAA(n); return *ptr; } void ShowNum() const { cout << num << endl; } private: AAA(int n) : num(n) {} }; int main(void) { AAA base; base.ShowNum(); AAA &obj1 = base.CreateInitObj(3); obj1.ShowNum(); AAA &obj2 = base.CreateInitObj(12); obj2.ShowNum(); delete &obj1; delete &obj2; return 0; } |
public 생성자가 정의되었습니다. 따라서 클래스 외부에서는 이 생성자를 기반으로 객체를 생성해야 합니다. 함수 내에서, 17행에 정의된 private 생성자를 이용해서 AAA 객체를 생성 및 반환하고 있습니다. 힙 영역에 생서된 객체를 참조의 형태로 반환하고 있습니다. 힙에 할당된 메모리 공간은 변수로 간주하여, 참조자를 통한 참조가 가능하다 라는 사실을 다시 한번 확인시켜 줍니다. 단순히 private으로 선언된 생성자를 통해서도 객체의 생성이 가능함을 보였습니다. 객체의 생성방법을 제한하고자 하는 경우에는 매우 유용하게 사용 됩니다.
소멸자의 이해와 활용
객체 생성시 반드시 호출되는 것이 생성자라면, 객체 소멸시 반드시 호출되는 것은 소멸자 입니다. 소멸자는 다음의 형태를 갖습니다. 클래스의 이름 앞에 ~가 붙은 형태의 이름을 갖습니다. 반환형이 선언되어 있지 않으며, 실제로 반환하지 않습니다. 매개변수는 void형으로 선언되어야 하기 때문에 오버로딩도 디폴트 값 설정도 불가능합니다. 소멸자는 객체소멸 과정에서 자동으로 호출이 됩니다. 프로그래머가 직접 소멸자를 정의하지 않으면, 디폴트 생성자와 마찬가지로 아무런 일도 하지 않는 디폴트 소멸자가 자동으로 삽입됩니다. 소멸자는 대게 생성자에서 할당한 리소스의 소멸에 사용됩니다. 생성자 내에서 new 연산자를 이용해서 할당해 놓은 메모리 공간이 있다면, 소멸자에서는 delete 연산자를 이용해서 이 메모리 공간을 소멸합니다.
#pragma warning(disable:4996) #include <iostream> #include <cstring> using namespace std; class Person { private: char * name; int age; public: Person(char * myname, int myage) { int len = strlen(myname) + 1; name = new char[len]; strcpy(name, myname); age = myage; } void ShowPersonInfo() const { cout << "name : " << name << endl; cout << "age : " << age << endl; } ~Person() { delete[]name; cout << "called destructor~" << endl; } }; int main(void) { Person man1("Lee dong woo", 29); Person man2("Jang dong gun", 41); man1.ShowPersonInfo(); man2.ShowPersonInfo(); return 0; } |
불피요한 메모리 공간의 낭비 또는 부족을 막기 위해서 문자열의 길이만큼 메모리 공간을 동적 할당하고 있습니다. 소멸자에는 생성자에서 할당한 메모리 공간의 소멸에 대한 코드가 삽입되어 있습니다. 소멸자를 통해서 객체소멸과정에서 처리해야 할 일들을 자동으로 처리할 수 있습니다.
클래스와 배열 그리고 this 포인터
객체 배열 및 객체 포인터 배열은 구조체 배열과 구조체 포인터 배열과 유사합니다. 객체 기반의 배열은 다음의 형태로 선언합니다. SoSimple arr[10]; 이를 동적으로 할당하는 경우에는 다음의 형태로 선언합니다. SoSimple * ptrArr = new SoSimple[10]; 이러한 형태로 배열을 선언하면, 열 개의 SoSimple 객체가 모여서 배열을 구성하는 형태가 됩니다. 이렇듯 구조체 배열의 선언과 차이가 없습니다. 하지만 배열을 선언하는 경우에도 생성자는 호출이 됩니다. 단, 배열의 선언과정에서는 호출할 생성자를 별도로 명시하지 못합니다. 즉, 위의 형태로 배열이 생성되려면 다음 형태의 생성자가 반드시 정의되어 있어야 합니다.
#pragma warning(disable:4996) #include <iostream> #include <cstring> using namespace std; class Person { private: char * name; int age; public: Person(char * myname, int myage) { int len = strlen(myname) + 1; name = new char[len]; strcpy(name, myname); age = myage; } Person() { name = NULL; age = 0; cout << "called Person()" << endl; } void SetPersonInfo(char * myname, int myage) { name = myname; age = myage; } void ShowPersonInfo() const { cout << "name: " << name << ", "; cout << "age: " << age << endl; } ~Person() { delete[]name; cout << "called destructor~" << endl; } }; int main(void) { Person parr[3]; char namestr[100]; char * strptr; int age; int len; for (int i = 0; i < 3; i++) { cout << "name: "; cin >> namestr; cout << "age: "; cin >> age; len = strlen(namestr) + 1; strptr = new char[len]; strcpy(strptr, namestr); parr[i].SetPersonInfo(strptr, age); } parr[0].ShowPersonInfo(); parr[1].ShowPersonInfo(); parr[2].ShowPersonInfo(); return 0; } |
배열 생성시 필요한 생성자를 추가하였습니다. 원하는 데이터로의 초기화를 목적으로 정의하였습니다. 반복문 안에서 이름과 나이 정보를 입력 받앗, 객체를 초기화하고 있습니다. 위의 실행결과를 통해서 객체 배열 생성시 void형 생성자가 호출됨을 확인할 수 있습니다. 배열 소멸시에도 그 배열을 구성하는 객체의 소멸자가 호출됨을 확인 할 수 있습니다.
객체 포인터 배열
객체 배열이 객체로 이뤄진 배열이라면, 객체 포인터 배열은 객체의 주소 값 저장이 가능한 포인터 변수로 이뤄진 배열입니다.
#pragma warning(disable:4996) #include <iostream> #include <cstring> using namespace std; class Person { private: char * name; int age; public: Person(char * myname, int myage) { int len = strlen(myname) + 1; name = new char[len]; strcpy(name, myname); age = myage; } Person() { name = NULL; age = 0; cout << "called Person()" << endl; } void SetPersonInfo(char * myname, int myage) { name = myname; age = myage; } void ShowPersonInfo() const { cout << "name: " << name << ", "; cout << "age: " << age << endl; } ~Person() { delete[]name; cout << "called destructor~" << endl; } }; int main(void) { Person * parr[3]; char namestr[100]; int age; for (int i = 0; i < 3; i++) { cout << "name: "; cin >> namestr; cout << "age: "; cin >> age; parr[i] = new Person(namestr, age); } parr[0]->ShowPersonInfo(); parr[1]->ShowPersonInfo(); parr[2]->ShowPersonInfo(); delete parr[0]; delete parr[1]; delete parr[2]; return 0; } |
포인터 배열이 선언되었습니다. 이는 객체의 주소 값 3개를 저장할 수 있는 배열입니다. 객체를 생성해서 이 객체의 주소 값을 배열에 저장하고 있습니다. 총 3회에 걸쳐서 new 연산을 진행하였으니, 총 3회에 걸쳐서 delete 연산을 진행합니다. 객체를 저장할 때에는 위의 예제에서 보인 두 가지 방법 중 하나를 택해야 합니다. 즉, 저장의 대상을 객체로 하느냐, 객체의 주소 값으로 하느냐를 결정해야 합니다. 객체 배열과 객체 포인터 배열의 차이점을 정확히 이해하기 바랍니다.
this point 의 이해
멤버함수 내에서는 this 라는 이름의 포인터를 사용할 수 있는데, 이는 객체 자신을 가리키는 용도로 사용되는 포인터입니다.
#include <iostream> #include <cstring> using namespace std; class SoSimple { private: int num; public: SoSimple(int n) : num(n) { cout << "num=" << num << ", "; cout << "address=" << this << endl; } void ShowSImpleData() { cout << num << endl; } SoSimple * GetThisPointer() { return this; } }; int main(void) { SoSimple sim1(100); SoSimple * ptr1 = sim1.GetThisPointer(); cout << ptr1 << ", "; ptr1->ShowSImpleData(); SoSimple sim2(200); SoSimple * ptr2 = sim2.GetThisPointer(); cout << ptr2 << ", "; ptr2->ShowSImpleData(); return 0; } |
this를 반환하고 있습니다. 이는 이 문장을 실행하는 객체의 포인터를 반환하라는 의미입니다. 그래서 반환형도 SoSimple* 형으로 선언되어 있습니다. 객체 sim1에 의해 반환된 this 를 ptr1에 저장하고 있습니다. 이 때 this는 SoSimple 의 포인터이므로 SoSimple형 포인터 변수에 저장해야 하니다. ptr1에 저장된 주소 값을 출력하고 있습니다. ptr1이 가리키는 객체의 ShowSimpleData 함수를 호출하고 있습니다.
소스코드와 실행결과를 통해서 this는 객체자신의 주소 값을 의미한다는 사실을 확인 및 이해했을 것입니다. 이렇듯 this 포인터는 그 주소 값을 의미한다는 사실을 확인 및 이해했을 것입니다. 이렇듯 this 포인터는 그 주소 값과 자료형이 정해져 있지 않은 포인터입니다. 0x12FF번지에 할당된 SoSimple 객체 내에서 사용이 되면, this 는 SoSimpe형의 포인터이면서 그 값은 0x12FF를 의미하게 되고, 0x52AA번지에 할당된 SoComplex 객체 내에서 사용이 되면, this 는 SoComplex 형의 포인터이면서 그 값은 0x52AA번지를 의미하게 됩니다.
This 포인터의 활용
클래스에서 함수의 매개변수 이름은 num 이다. 그런데 이 일므은 멤버변수의 이름과 동일하기 때문에 함수 내에서의 num은 매개변수 num을 의미하게 됩니다. 따라서 변수의 이름만 참조하는 방법으로 함수 내에서 맴버변수 num에 접근이 불가능합니다. 그러나 this 포인터를 활용하면 가능합니다. 위 문장에서 this는 객체를 참조하는 포인터입니다. 그럼 객체의 포인터를 가지고 접근하는 변수 num은 멤버변수 num인가? 지역변수 num인가요? 객체의 포인터를 가지고는 지역변수에 접근이 불가능합니다. 따라서 이는 멤버변수 num을 의미하는 것입니다. 이러한 특성을 이용하면, 매개변수의 이름을 멤버변수의 이름과 달리하기 위해서 고민할 필요가 없습니다.
#include <iostream> using namespace std; class TwoNumber { private: int num1; int num2; public: TwoNumber(int num1, int num2) { this->num1 = num1; this->num2 = num2; } /* TwoNumber(int num1, int num2) :num1(num1), num2(num2) { } */ void ShowTwoNumber() { cout << this->num1 << endl; cout << this->num2 << endl; } }; int main(void) { TwoNumber two(2, 4); two.ShowTwoNumber(); return 0; } |
this -> num1, this -> num2 는 멤버변수를, num1, num2는 매개변수를 의미하니 매개변수 um1, num2를 통해서 전달된 값이 멤버변수 num1, num2 에 저장됩니다. 멤버 이니셜라이저에서는 this 포인터를 사용할 수 없습니다. 대신에 저장하는 변수는 멤버변수로, 저장되는 값은 (소괄호 안의 변수 및 상수는) 매개변수로 인식을 하기 때문에 여기서 보이는 형태의 문장 구성이 가능합니다. this 포인터를 사용함으로써, 멤버변수에 접근함을 명확히 하였습니다. 일반적으로 이러한 경우에는 this 포인터를 생략해서 표현합니다.
변수의 이름을 짓는 것은 의외로 신경이 쓰이는 일입니다. 특히 유사한 성격을 지니는 두 변수의 이름을 구별되게 짓는 것은 고민스럽기까지 합니다. 그래서 위 예제에서 보이듯이 멤버변수와 매개변수의 이름을 동일하게 짓고, this 포인터를 이용해서 이 둘을 구분하는 것을 좋아하는 프로그래머들도 있습니다.
Self-Reference 의 반환
객체 자신을 참조할 수 있는 참조자를 의미합니다. 우리는 this 포인터를 이용해서, 객체가 자신의 참조에 사용할 수 있는 참조자의 반환문을 구성할 수 있습니다.
#include <iostream> using namespace std; class SelfRef { private: int num; public: SelfRef(int n) :num(n) { cout << "obj constructor" << endl; } SelfRef& Adder(int n) { num += n; return *this; } SelfRef& ShowTwoNumber() { cout << num << endl; return *this; } }; int main(void) { SelfRef obj(3); SelfRef &ref = obj.Adder(2); obj.ShowTwoNumber(); ref.ShowTwoNumber(); ref.Adder(1).ShowTwoNumber().Adder(2).ShowTwoNumber(); return 0; } |
함수 Adder 에서는 선언된 반환형과 반환의 내용을 함께 살펴야 합니다. 반환의 내용은 *this 인데, 이는 이 문장을 실행하는 객체 자신의 포인터가 아닌, 객체 자신을 반환하겠다는 의미가 됩니다. 반환형이 참조형 SelfRef&으로 선언되었습니다. 따라서 객체 자신을 참조할 수 있는 참조의 정보가 반환됩니다. 위의 정의된 함수와 마찬가지로 객체 자신을 참조할 수 있는 참조 값을 반환하도록 정이 되어 있습니다. 객체 obj의 Adder 함수를 호출하였습니다. 이 함수는 객체 자신의 참조 값을 반환합니다. 즉, 객체 obj의 참조 값을 반환합니다. 따라서 참조자 ref 는 객체 obj를 참조하게 됩니다. 두 함수 Adder 와 ShowTwoNumber가 객체의 참조 값을 반환하기 때문에 구성이 가능한 문장입니다. 실행결과에서 보이듯이, 하나의 객체만 생성하였습니다. 다만, 그 객체가 반환하는 참조 값을 대상으로 다양한 방법으로 멤버변수를 호출했을 뿐입니다.
참조의 정보에 대한 이해
참조의 정보 또는 참조 값이라는 표현을 사용하였습니다. num에 저장된 정수 값은 아닙니다. 변수 num을 참조할 수 있는 참조의 정보가 전달됩니다. 변수 num을 참조할 수 있는 참조 값이 참조자 ref에 전달되어, ref가 변수 num을 참조하게 됩니다. 대입 연산자의 왼편에 참조자의 선언이 오거나, 반환형으로 참조형이 선언되면, 그 때 전달되는 정보를 표현하기 위해서 참조의 정보 또는 참조 값이라는 표현을 사용한 것입니다.