이번 포스팅에서 우리가 배워볼 것은 다음과 같다.
- 생성자(constructor)
- 생성자가 필요한 이유
- 생성자를 호출하는 방법
- 멤버 초기화 리스트
- 소멸자
- 접근 제어(access control)
- 접근자와 설정자
- 객체와 함수의 관계
1. 생성자(constructor)
객체가 생성된 후에는 반드시 객체를 초기화하여야 한다. 동적 메모리 공간이나 자원들을 할당 하는 것도 초기화에 포함되는데 생성자(constructor)는 그 초기화를 담당하는 함수라 보면 된다.
2. 생성자가 필요한 이유
아래 예제를 보면서 생성자가 왜 필요한지 알아보자.
#include <iostream>
using namespace std;
class Time {
public:
int hour; // 0-23이 가능한 시를 나타낸다.
int minute; // 0-59가 가능한 분을 나타낸다.
void init_time(Time& obj, int h, int m) {
obj.hour = h;
obj.minute = m;
}
void print() {
cout << hour << ":" << minute << endl;
}
};
int main() {
Time a; // 객체 a 생성
// 이렇게 객체의 데이터를 저장하면 되나, 값들을 하나씩 저장해야 하기 때문에 그다지 편한 방법이 아니다.
a.hour = 10;
a.minute = 25;
a.print();
// 아래와 같이 값을 설정하면 내가 예상했던대로는 돌아가지 않는 오류가 발생한다.
Time b;
b.hour = 26;
b.minute = 70;
b.print();
// c의 경우 init_time() 호출이나 값을 초기화하지 않아서 잘못된 값이 들어간다. 그래서 쓰레기값이 출력된다.
Time c;
c.print();
return 0;
}
이렇듯이 원치 않는 값이 들어가거나, 값을 초기화하지 않아서 쓰레기 값이 나오는 등 오류를 범하기 쉽다. 그리고 값을 설정한다 해도 일일히 설정해야 하기 때문에 번거로움도 발생한다. 이러한 이유로 우리는 생성자를 사용한다.
이제 아래 예시를 통해 생성자의 예를 알아보자.
#include <iostream>
using namespace std;
class Time {
public:
int hour; // 0-23이 가능한 시를 나타낸다.
int minute; // 0-59가 가능한 분을 나타낸다.
// 생성자 정의(기본 생성자에 의한 초기화)
Time() {
hour = 0;
minute = 0;
}
// 생성자 중복 정의(매개변수가 있는 생성자)
Time(int h, int m) {
hour = h;
minute = m;
}
void print() {
cout << hour << ":" << minute << endl;
}
};
int main() {
Time a; // 기본 생성자에 의한 초기화
Time b(10, 25); // 매개변수가 있는 생성자
a.print();
b.print();
return 0;
}
- 이렇듯이 객체를 생성할 때 객체를 초기화하는 함수가 자동으로 호출될 수 있다면 매우 편리할 것이다. 이렇게 객체를 생성할 때 객체를 초기화 하는 함수가 생성자(constructor)이다.
- 생성자 중복 정의 - 위 코드와 같이 매개변수가 없는 생성자와 매개변수가 있는 생성자를 동시에 정의할 수 있다. 즉, 생성자도 멤버 함수의 일종이라고 생각할 수 있으므로 중복 정의가 가능하다.(생성자의 중복 정의)
- 디폴트 생성자 - 생성자도 함수의 일종이기 때문에 매개 변수는 디폴트 값을 가질 수 있다. 아래 코드를 참고하자.
#include <iostream> using namespace std; class Time { public: int hour; // 0-23이 가능한 시를 나타낸다. int minute; // 0-59가 가능한 분을 나타낸다. // 디폴트 생성자 Time(int h = 0, int m = 0) { // 디폴트 인수 사용 hour = h; minute = m; } void print() { cout << hour << ":" << minute << endl; } }; int main() { Time a; a.print(); // 0:0 Time b{ 10, 25 }; b.print(); // 10:25 return 0; }
3. 생성자를 호출하는 방법
- 생성자명은 클래스 이름과 같은 멤버 함수로 만들면 된다.
- 인수가 있는 생성자가 정의되어 있는데 프로그래머가 까먹고 인자를 주지 않으면 컴파일러가 오류를 발생시킨다.
- 아래와 같이 생성자를 호출할 수 있다.
Time a; // 오류 발생 - Time 생성자를 호출하는데 필요한 인수들을 제공하지 않았음.
Time b(10, 25); // OK - 하지만 () 기호는 예전 버전에서 사용하는 방법
Time c{10, 25}; // OK - 최신의 방법
Time d = {10, 25}; // OK - 다만 '=' 기호로 인해 간결하지 않을 뿐
4. 멤버 초기화 리스트
생성자를 좀 더 쉽게 작성할 수 있는 방법을 초기화 리스트(initializer list) 라 한다. 아래 예제를 참고하자.
// 방법1
Time(int h, int m) : hour{h}, minute{m} {}
// 방법2
Time(int h, int m) : hour(h), minute(m) {}
// 방법3
Time(int h = 0, int m = 0) : hour{h}, minute{m} {} // 초기화 리스트와 디폴트 인수를 동시에 사용 가능
5. 소멸자
- 객체가 소멸될 때 호출되는 함수를 소멸자(destructor)라고 한다.
- 클래스 이름에 물결표(~) 접두사를 붙여서 만든다.
- 이 소멸자는 파일을 닫거나 메모리를 반환하는 작업과 같이 프로그램을 종료하기 전에 자원을 반납하는데 유용하게 사용된다.
- 소멸자는 생성자와 마찬가지로 값을 반환하거나 매개변수를 사용할 수 있다.
예제1. 생성자와 소멸자
#include <iostream>
#include <string.h>
using namespace std;
class MyString {
private:
char* s;
int size;
public:
MyString(const char* c) {
size = strlen(c) + 1;
s = new char[size];
strcpy_s(s, size, c);
cout << s << endl;
}
// 소멸자
~MyString() {
delete[] s;
}
};
int main() {
MyString str("abcdefghijk");
return 0;
}
6. 접근 제어(access control)
- 외부에서 특별한 멤버 변수나 멤버 함수에 접근을 제어하는 것을 접근 제어(access control) 이라고 한다.
- Private나 Public 등의 접근 지정자를 멤버 변수나 멤버 함수 앞에서 붙여서 접근을 제한한다.
접근제어 종류
- 전용멤버(private member)
- 클래스 내부에서만 접근이 허용된다.
- 멤버 정의시 앞에 private:를 붙여주면 이후에 정의되는 모든 멤버는 private member가 된다.
- 접근 지정자가 생략되었다면 자동으로 private 변수가 된다.
- 정보은닉의 취지에 따라서 멤버 변수는 private 변수로 선언하는 것이 좋다.
- 어떤 개발자가 의미가 담겨진 변수에 불가능한 의미나 값을 집어넣을지도 모르기 때문에, 이런 실수 방지를 위해 private와 같은 접근 지정자가 필요하다.
- 공용멤버(public member)
- 외부에서도 접근 가능한 접근 지정자이다.
접근제어 예제
#include <iostream>
using namespace std;
class Time {
private:
int hour;
int minute;
public:
Time(int h, int m);
void inc_hour();
void print();
};
Time::Time(int h, int m) {
hour = h;
minute = m;
}
void Time::inc_hour() {
++hour;
if (hour > 23) {
hour = 0;
}
}
int main() {
Time a{ 24, 59 };
++a.hour; // hour 변수는 a 객체의 private 멤버 변수이므로 접근 불가능하다.(오류 발생)
return 0;
}
7. 접근자와 설정자
- Private로 선언된 멤버 변수들을 읽어서 외부로 전달해주는 함수를 접근자(getter)라고 한다.
- 안전하게 멤버 변수들을 변경할 수 있는 함수를 설정자(setter)라고 한다.
접근자와 설정자 예제
#include <iostream>
using namespace std;
class Time {
private:
int hour; // 0-23
int minute; // 0-59
public:
Time(int h, int m);
// 접근자 정의
int getHour() { return hour; }
int getMinute() { return minute; }
// 설정자 정의
void setHour(int h) { hour = h; }
void setMinute(int m) { minute = m; }
};
Time::Time(int h, int m) {
hour = h;
minute = m;
}
int main() {
Time a{ 24, 59 };
a.setHour(0);
a.setMinute(10);
cout << a.getHour() << ":" << a.getMinute() << endl;
return 0;
}
8. 객체와 함수의 관계
어떤 피자 체인점에서 미디엄 크기의 피자를 주문하면 무조건 라지 피자로 변경해준다고 하자.
다음 3가지 경우로 나누어서 각각 코드를 만들어본 후, 피자의 크기가 커지는지 비교해보자.
1. 객체가 함수의 매개 변수로 전달되는 경우
#include <iostream>
using namespace std;
class Pizza {
public:
Pizza(int s) : size(s) {};
int size; // 단위 : 인치
};
void makeDouble(Pizza p) {
p.size *= 2;
}
int main() {
Pizza pizza(10);
makeDouble(pizza);
cout << pizza.size << "인치 피자" << endl;
return 0;
}
피자의 크기는 변하지 않는다. 원본값을 복사해서 매개변수로 넘어가기 때문에 그 복사본만 값이 변경되고 원본값은 그대로 유지된다.
2. 객체의 참조자가 함수의 매개 변수로 전달되는 경우
#include <iostream>
using namespace std;
class Pizza {
public:
Pizza(int s) : size(s) {};
int size; // 단위 : 인치
};
void makeDouble(Pizza &p) { // 참조자 & 사용
p.size *= 2;
}
int main() {
Pizza pizza(10);
makeDouble(pizza);
cout << pizza.size << "인치 피자" << endl;
return 0;
}
이번에 makeDouble() 함수 매개변수 앞에 &라는 기호를 사용하였더니 10인치였던 피자가 20인치 피자로 두 배가 되었다. 매개변수에 있던 p 객체는 pizza 객체의 주소를 가리키게 되는 것이다. 이렇게 원본값을 그대로 바꾸기 위해서는 참조자를 사용하면 된다.참조자란 변수에다가 하나의 이름을 더 주는 것이며, & 기호를 이용하여 정의한다.
int i; // 변수 정의
int& j = i; // 변수 정의
참조자를 통하여 변수의 값을 변경하면 원본 변수를 변경하는 것이나 마찬가지이다.
3. 함수가 객체를 반환하는 경우
함수가 객체를 반환할 때도 객체의 내용이 복사될 뿐 원본이 전달되지는 않는다.
#include <iostream>
using namespace std;
class Pizza {
public:
Pizza(int s) : size(s) {};
int size; // 단위 : 인치
};
Pizza createPizza(){
Pizza p{ 10 };
return p; // return되는 순간 이 p는 메모리상에서 죽어버리고
}
int main() {
Pizza pizza = createPizza(); // return p 되는 순간 createPizza에 있던 p 객체 데이터가 copy되어 pizza 객체에 값이 들어간다.
cout << pizza.size << "인치 피자" << endl;
return 0;
}
'Develop > C, C++' 카테고리의 다른 글
[C/C++] 복사생성자와 정적 멤버 (0) | 2023.12.26 |
---|---|
[C/C++] 객체 지향 프로그래밍과 클래스 (0) | 2023.12.25 |
[C++] 객체 배열과 벡터 (0) | 2023.12.22 |
[C/C++] 상속(Inheritance) (0) | 2023.12.19 |
[C/C++] 동적할당(Dynamic Memory Allocation) (0) | 2023.12.18 |