본 포스팅은 위 링크의 글을 참조하여 정리한 글입니다.
우리는 이전에 생성자의 초기화 리스트 문법을 보았다.
바로 이 녀석인데
#include <iostream>
using namespace std;
class Rectangle{
private:
int x;
int y;
int width;
int height;
public:
Rectangle(int _x, int _y);
Rectangle(int _x, int _y ,int _width, int _height);
void print_rectangle();
};
Rectangle::Rectangle(int _x, int _y) : x(_x), y(_y) , width(5), height(5) {}
Rectangle::Rectangle(int _x, int _y ,int _width, int _height) : x(_x), y(_y) , width(_width), height(_height){}
void Rectangle::print_rectangle(){
cout << "x : " << x << "y : " << y << " width : " << width << " height : " << height << endl;
}
int main(){
Rectangle rectangle1(10,20);
rectangle1.print_rectangle();
Rectangle rectangle2(10,20,30,40);
rectangle2.print_rectangle();
}
x : 10y : 20 width : 5 height : 5
x : 10y : 20 width : 30 height : 40
생성자 부분을 보면, 재밌게도 본문( {}
)에 아무것도 없다.
함수 이름 옆에 : 변수(매개변수), 변수(매개변수)
로 값을 생성자 본문을 대체하는 것이다. 만약, 따로 입력 받는 매개변수가 없다면 초기값도 위의 30, 40처럼 설정할 수 있다.
재밌게도 생성자의 초기화 리스트는 대입이 아니라, 선언과 동시에 초기화가 되는 개념이다.
즉, 위의 코드는 다음이 아니라,
int x;
int y;
int width;
int height;
x = _x;
y = _y;
width = _width;
height = _height;
이것과 같다.
int x = _x;
int y = _y;
int width = _width;
int height = _height;
선언과 동시에 초기화
된다라는 특성은 어디에 적용할 수 있을까??
대표적으로 참조자와 상수는 반드시 선언과 동시에 초기화
되어야 한다. 따라서, 클래스 내에 참조자나 상수를 넣고 싶다면 반드시 생성자의 초기화 리스트에 넣어주어야 한다.
해당 도형이 어떤 도형인지를 알려주는 DIAGRAM_ID를 추가해보자, 이는 한 번 설정되면 바뀌지 않도록 하기위해 const로 설정해주도록 하자
class Rectangle{
private:
int x;
int y;
int width;
int height;
const int DIAGRAM_ID;
public:
Rectangle(int _x, int _y);
Rectangle(int _x, int _y ,int _width, int _height);
void print_rectangle();
};
Rectangle::Rectangle(int _x, int _y) : x(_x), y(_y) , width(5), height(5), DIAGRAM_ID(1) {}
Rectangle::Rectangle(int _x, int _y ,int _width, int _height) : x(_x), y(_y) , width(_width), height(_height) , DIAGRAM_ID(1){}
x : 10y : 20 width : 5 height : 5
x : 10y : 20 width : 30 height : 40
다음과 같이 모든 생성자에서 const나 참조자로 된 변수의 기본값을 받아 설정하면 된다.
데이터 메모리 영역 참고
https://dlrow-olleh.tistory.com/19
data segmentation은 4개의 영역으로 나뉜다.
코드 영역, 데이터 영역, 힙, 스택
코드 영역은 목적 코드 형태의 프로그램(즉, 코드를 목적 파일(.obj)로 만든 것)과 상수 데이터를 저장하는 곳이다. 다른 공간과는 달리 읽기 전용이다.
데이터 영역은 전역 변수와 정적 변수(static)이 저장되는 공간이다.
따라서, static 변수의 경우는 객체가 소멸하거나, 함수가 종료할 때 사라지는 것이 아닌 프로그램이 종료될 때 소멸하게 된다.
또한, static 변수의 경우 클래스에서 사용할 경우, 모든 객체(인스턴스)들이 공유하는 변수이다.
즉, 객체가 10개 100개가 있더라도, 참조하는 static 변수는 1개이다.
static 키워드를 이용하여 객체가 몇 개 만들어졌는지 계산해보도록 하자
#include <iostream>
using namespace std;
class Rectangle{
private:
int x;
int y;
int width;
int height;
const int DIAGRAM_ID;
static int count;
public:
Rectangle(int _x, int _y);
Rectangle(int _x, int _y ,int _width, int _height);
~Rectangle();
void print_rectangle();
void print_count();
};
int Rectangle::count = 0;
Rectangle::Rectangle(int _x, int _y) : x(_x), y(_y) , width(5), height(5), DIAGRAM_ID(1) {
count++;
}
Rectangle::Rectangle(int _x, int _y ,int _width, int _height) : x(_x), y(_y) , width(_width), height(_height) , DIAGRAM_ID(1){
count++;
}
Rectangle::~Rectangle(){
count--;
}
void Rectangle::print_rectangle(){
cout << "x : " << x << "y : " << y << " width : " << width << " height : " << height << endl;
}
void Rectangle::print_count(){
cout << "total count : " << count << endl;
}
int main(){
Rectangle rectangle1(10,20);
rectangle1.print_rectangle();
Rectangle rectangle2(10,20,30,40);
rectangle2.print_rectangle();
rectangle1.print_count();
}
x : 10y : 20 width : 5 height : 5
x : 10y : 20 width : 30 height : 40
total count : 2
static은 맴버 변수로 static int count;
처럼 사용하면 된다.
여기서, 초기화 방법이 두 가지가 있는데, 하나는 다음과 같다.
int Rectangle::count = 0;
이는 마치 맴버 함수들을 구현해줄 때와 비슷한데, count는 private임에도 불구하고 예외적으로 위와 같이 초기화가 가능하다.
두 번째는 const static
한 맴버 변수에서만 가능한 초기화이다.
class Rectangle{
private:
int x;
int y;
int width;
int height;
const int DIAGRAM_ID;
static int count;
const static int MAX_ANGLE = 360;
public:
Rectangle(int _x, int _y);
Rectangle(int _x, int _y ,int _width, int _height);
~Rectangle();
void print_rectangle();
void print_count();
};
다음과 같이 const static int MAX_ANGLE = 360;
맴버 변수가 있는 곳에 바로 초기값을 넣어줄 수 있는데, 이는 const static
한 값만 가능하므로 알아두자
또, static 함수도 만들 수 있다. 단, static 함수의 경우에, 데이터 영역에 할당되는 것들이므로 아직, 인스턴스가 만들어지기 전이다.
따라서, 인스턴스의 맴버 변수들을 참조할 수는 없다.
class CalcRect{
private:
public:
static int calcArea(Rectangle &rectangle);
};
int CalcRect::calcArea(Rectangle &rectangle){
return rectangle.getWidth() * rectangle.getHeight();
}
int main(){
Rectangle rectangle1(10,20);
rectangle1.print_rectangle();
cout << "area : " << CalcRect::calcArea(rectangle1);
}
CalcRect
클래스를 만들고 static 함수로 calcArea
를 만들었다. 해당 함수에 rectangle1 인스턴스를 넣으면 자동으로 계산이 되도록 한다.
static 함수이기 때문에 함수 호출은 클래스에서 해주어야 한다. 따라서 CalcRect::calcArea
로 해주어야 한다.
호출을 일반화하면
클래스이름::static함수();
다음과 같다.
this는 객체 자신을 가리키는 포인터 역할
을 한다. 포인터라는 것을 알아두도록 하자
따라서, 객체 안에서만 의미가 있으므로 static같이 클래스 안에 정의된 함수에서는 this는 의미가 없다.
int Rectangle::getWidth(){
return this->width; // *this.width, width
}
int Rectangle::getHeight(){
return this->height;
}
그래서 다음처럼, getter, setter를 만들 수 있다. 물론 width, height로 써도 문제없이 반환된다.
만약, 인자에 들어가는 매개변수와 맴버 변수의 이름이 같다고 하자,
int Rectangle::setWidth(int width){
width = width;
}
이는 매우 어색해보이고, 읽기 좋아보이지 않는다. 따라서 this를 이용하여 변경하면 다음과 같이 구분할 수 있다.
int Rectangle::setWidth(int width){
this->width = width;
}
this->width
는 객체(인스턴스)의 맴버변수임을 알 수 있다. width
는 매개변수임이 구분된다.
이밖에도 this를 이용한 체이닝을 구현할 수 있는데, 이는 레퍼런스를 리턴하는 함수를 통해 구현이 가능하다.
이전에 참조자를 반환하는 함수를 만들었었다. 그런데 참조자를 반환한다는 것이 무슨 의미이고, 값을 반환하는 것과 어떤 차이가 있을까?
#include <iostream>
using namespace std;
class Repository{
int data;
public:
Repository(int _data);
int& getRefData();
int getData();
void printData();
};
Repository::Repository(int _data) : data(_data) {}
int& Repository::getRefData(){
return data;
}
int Repository::getData(){
return data;
}
void Repository::printData(){
cout << "Repo : " << data << endl;
}
int main(){
Repository repository(10);
repository.printData();
int& refData = repository.getRefData();
refData = 200;
repository.printData();
}
Repo : 10
Repo : 200
다음의 예제에서 Repo의 data참조값을 참조형 refData가 받고, refData에 200을 넣어주면, 클래스의 data도 바뀐다.
이전에 참조형은 하나의 별명일 뿐이다. 라고 했다. 이는 메모리에 따로 참조형 데이터를 만드는 것이 아니라, 매크로로 마냥 컴파일러가 참조형 데이터를 동치시킬 수 있다는 것이다.
따라서 int& Repository::getRefData()
의 경우, 반환값이 참조형이므로 컴파일러가 그냥 data를 빼다 박는다고 할 수 있다.
즉, 다음과 같다는 것이다.
int& refData = data; // int& refData = repository.getRefData();
그럼, 어차피 왼쪽의 참조형 변수가 있으니까 값을 반환해주는 것과 무엇이 다른가이다.
즉, int Repository::getData()
로 참조형 변수에 값을 넣어주는 것과 무엇이 다르냐는 것이다.
int &data = repository.getData();
data = 200;
repository.printData();
prog.cpp:35:35: error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’
하지만 위 코드는 실제로 컴파일 에러가 발생한다. 이유는 lvalue, rvalue라는 말들이 나오는데 이는 뒤에가서 더 자세히 배우도록 하자.
참조형은 컴파일러가 알아서 data를 빼다 박아주었다. 이는 참조형의 특징이다. (사실 훨씬 더 복잡한 내용이 숨어있지만, 지금은 그러려니 하자)
그런데, getData()
로 값을 리턴해주는 것은 왜 참조형 변수인 int &data
에 넣어지지 않을까??
https://stackoverflow.com/questions/9029368/is-return-value-always-a-temporary
이는 값을 return할 때의 특징인데, 값을 넘겨줄 때는 임시로 메모리에 하나의 변수를 만들어 값을 복사한 후, 해당 값을 넘겨준다.
따라서, 리턴할 때의 임시값이 사라지면서 참조할 것이 없어 컴파일 에러가 발생하는 것이다.
참조자 리턴은 이렇게 data를 바로 넘겨주는 기능을 하고, 또한 참조자이기 때문에 대입도 가능하다.
즉 다음이 가능하다.
repository.getRefData() = 300;
뭐 이런게 되나 싶은데, 아까도 말했듯이 참조자는 가리키는 값을 빼다박을 수 있다고 했다.
그래서 다음과 같이 된다.
data = 300;
이러한 참조자의 특성을 이용하여 체이닝을 할 수 있다.
#include <iostream>
using namespace std;
class Repository{
int data;
int price;
int id;
string name;
public:
Repository& setData(int _data);
Repository& setPrice(int _price);
Repository& setId(int _id);
Repository& setName(string _name);
void printRepoInfo();
};
Repository& Repository::setData(int _data){
data = _data;
return *this;
}
Repository& Repository::setPrice(int _price){
price = _price;
return *this;
}
Repository& Repository::setId(int _id){
id = _id;
return *this;
}
Repository& Repository::setName(string _name){
name = _name;
return *this;
}
void Repository::printRepoInfo(){
cout << " data : " << data << " price : " << price << " id : " << id << " name : " << name << endl;
}
int main(){
Repository repo;
repo.setData(10).setId(1).setName("hello").setPrice(20);
repo.printRepoInfo();
}
data : 10 price : 20 id : 1 name : hello
*this
의 반환 타입으로 Repository&
을 해준 것이 보인다. 즉, 참조자를 반환함으로서 값을 그대로 내보냈다고 생각하면 된다.
그러면 뒤의 체이닝으로 이어진 코드는 자신의 메서드를 계속 부르는 것과 같다.
repo.setData(10).setId(1).setName("hello").setPrice(20);
repo.setData(20)
이 처리되면 *this
가 호출되므로 자기 자신이 나온다, 따라서 다음과 같이 쓸 수 있다.
repo.setId(1).setName("hello").setPrice(20);
이렇게 하나하나 줄여 나가다 보면
repo.setPrice(20);
이렇게 된다.
이것이 빌더 패턴이다. 물론 빌더 패턴은 이것과는 약간 다르지만 어느정도 비슷하다.
C++에서는 변수들의 값을 바꾸지 않고, 읽기만하는 마치 상수 같은 맴버 함수를 상수 함수
로 선언할 수 있다.
매개변수든, 클래스의 맴버 변수든 어떠한 상태를 바꾸는 속성이 있어서는 안된다. 단, 지역 변수는 바꿀 수 있다.
그냥 값만 읽어들여야 한다. 어떨 때 이런 함수가 쓰이냐면 비교 함수를 만들 때 자주쓰인다.
우리는 위의 예제에서 printRepoInfo()
가 읽기만 한다는 것을 확인하였다.
이에따라 printRepoInfo
를 const함수로 만들어보자
class Repository{
int data;
int price;
int id;
string name;
public:
Repository& setData (int _data);
Repository& setPrice(int _price);
Repository& setId(int _id);
Repository& setName(string _name);
void printRepoInfo() const;
};
먼저 선언부에도 const를 붙여주어야 한다. const의 위치는 매개변수 괄호 ()
와 바디 {}
사이에 넣어주면 된다.
void Repository::printRepoInfo() const{
cout << " data : " << data << " price : " << price << " id : " << id << " name : " << name << endl;
}
구현부에도 다음과 같이 const를 매개변수 괄호와 바디사이에 넣어주면 된다.
만약 setter 중에 하나라도 const를 넣으면 에러를 볼 수 있다.