💡 생성자, 소멸자, 생성자 오버로딩, 초기화 리스트, 함수 오버로딩, 연산자 오버로딩, const
생성자를 사용하는 이유
클래스를 정의한 후 클래스 객체를 생성하게 되면 메모리에 할당된다. 이때 클래스의 멤버 변수는 초기화되지 않은 상태이므로 사용할 수 없다. 일일이 멤버 변수에 접근하여 초기화 후 사용해도 되지만 만약 private처럼 외부 접근이 불가능한 상태라면 불가능하고 public설정이 된 멤버 함수를 이용하여 초기화해야 할 것이다. 이럴 경우에는 객체를 사용하기 전에 무조건 해당 함수를 실행하고 객체를 사용해야 한다. 따라서 C++에서는 객체의 생성과 동시에 멤버 변수를 초기화해주는 멤버 함수인 생성자(constructor)를 제공한다.
클래스의 객체 생성 시(인스턴스 생성 시)에 private 멤버를 초기화
생성자 이름 == 클래스 이름
객체 생성 시 딱 한 번 호출됨
디폴트값 설정 가능
: 동일한 클래스 타입의 다른 객체에 대한 참조(reference)를 인자로 받아 생성하는 객체를 초기화하도록 하는 깊은 복사(객체가 가진 멤버의 값과 형식 자체를 복사해 객체 자체를 복사).
class TestClass {
private:
int num1;
int num2;
public:
//기본 생성자
TestClass(int a, int b) {
num1 = a;
num2 = b;
}
//복사 생성자
TestClass(TestClass& original) {
num1 = original.num1;
num2 = original.num2;
}
void printNumber() const {
std::cout << "num1 : " << num1 << std::endl;
std::cout << "num2 : " << num2 << std::endl;
}
};
int main() {
//기본 생성자로 객체 생성
TestClass test1(12, 06);
//복사 생성자로 객체 생성
TestClass test2(test1);
test2.printNumber();
}
멤버 변수를 초기화하는 방법 중 하나(다른 하나는 생성자를 통한 멤버 변수 초기화)
const 멤버의 경우 생성과 동시에 초기화되어야하기 때문에 const 멤버 변수는 이니셜라이저를 통해 초기화해야함.
TestClass():num1(12), num2(06) { //추가 초기화 }
💡 변수의 종류
일반 변수 | 값을 저장하기 위해 메모리에 공간을 할당받아 직접 저장하는 함수 |
---|---|
포인터 변수 | 다른 변수의 주소값을 저장하는 변수 |
참조 변수 | 다른 객체나 다른 객체에 대한 별명 |
: 크키가 큰 객체를 함수에 인자로 전달할 때 주로 사용. 객체간의 연결고리 ?
//자료타입& 참조변수명 = 변수명
int original = 1206
int& refer = original
여기서의 &
연산자는 주소연산자와는 다르다.
참조 변수는 저장하는 변수와 같은 메모리를 참조한다
참조 변수는 선언과 동시에 초기화돼야함
null값 참조 불가
const는 const로, non-const는 non-const로 참조
한번 참조한 변수는 재참조할 수 없다.
함수의 매개변수로의 사용
void swapValue(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 10;
int y = 20;
std::cout << "x: " << x << " y: " << y << std::endl;
swapValue(x, y);
std::cout << "x: " << x << " y: " << y << std::endl;
}
기존이라면 swapValue의 매개변수를 포인터로 받아 x, y의 주소값을 넘겨줬겠지.
인자에 x, y가 전달되면 매개변수 a, b가 x, y에 대한 참조로 선언된다.
int main() {
int num = 1206;
//num의 주소값을 ptr에 저장
int *ptr = #
//참조변수 선언 후 num 저장
int& ref = num;
std::cout << *ptr << std::endl;
std::cout << ref << std::endl;
}
//OUTPUT
1206
1206
따라서 *ptr과 ref는 동일하게 취급됨.
참조 변수는 선언과 동시에 초기화돼야 하고, null로 초기화하거나 참조를 변경할 수 없으므로 포인터보다 안전하게 취급받는다.
this pointer를 사용하는 이유
모든 멤버 함수는 자신만의 this 포인터를 가지고 있음.
this pointer : 멤버 함수를 호출한 객체 자신을 가리키는 포인터(주소값을 갖고 있음. 포인터니까)
이를 사용해 호출된 멤버 함수는 자신을 호출한 객체가 어떤 객체인지 알 수 있다.
생성자도 일종의 함수라 오버로딩 가능
주로 클래스에 동적할당을 통해 메모리를 할당받는 멤버 변수가 존재할 경우 메모리 할당해제를 위해 사용됨
💡 **각 영역별로 메모리를 할당받고 사용 종료 후, 할당받은 메모리가 해제되는 시기**스택 영역 | 해당 스코프 ( {} ) 가 끝날 때 |
---|---|
힙 영역 | delete 명령을 통해 메모리를 해제할 때 |
데이터 영역 | 프로그램이 종료될 때 (전역, 정적 변수 등 프로그램 전체에서 사용되므로) |
따라서 힙 영역에 메모리를 할당받은 동적 할당의 경우 소멸자를 통해 메모리를 해제해줘야 함
: 매개변수의 개수나 형태를 다르게 설정하여 동일한 이름의 함수를 여러개 만드는 것
함수 오버로딩을 쓰는 이유
인자 2개를 받아 두 수의 합을 구하는 함수를 구현한다고 하자.
자연수, 정수, 실수 등 다양한 수의 합을 구하려면 그때마다 아래와 같이 새로운 이름의 새로운 함수를 생성해야 한다.
void printIntAdd(int x, int y) {
std::cout << "합 : " << x + y << std::endl;
}
void printDouubleAdd(double x, double y) {
std::cout << "합 : " << x + y << std::endl;
}
int main() {
printIntAdd(12, 6);
printDouubleAdd(12.06, 19.94);
return (0);
}
이 경우 함수 오버로딩을 사용해 다양한 자료형에 대해 동일한 기능을 수행하는 함수를 생성해 불필요한 코드의 복잡성을 방지할 수 있다.
void printAdd(int x, int y) {
std::cout << "합 : " << x + y << std::endl;
}
void printAdd(double x, double y) {
std::cout << "합 : " << x + y << std::endl;
}
int main() {
printAdd(12, 6);
printAdd(12.06, 19.94);
return (0);
}
말그대로 연산자 오버로딩
//난 Point(좌표) class를 만들어서, Point 객체끼리의 합(좌표 덧셈)을 하고싶어
class Point {
private :
int x;
int y;
public :
Point(int a = 0, int b = 0) : x(a), y(b) {};
void print() {
std::cout << "x좌표: " << x << " y좌표: " << ㅛ << std::endl;
}
//매개변수로 참조 변수를 받으니까 값을 조작할 수 있겠지?
Point operator+(Point& p) {
Point point;
//연산자도 매개변수를 받는 함수같은 것임. 즉 p1 + p2면 p1이 +를 호출해 인자로 p2를 넘길거다.
point.x = this->x + p.x;
point.y = this->y + p.y;
return point;
}
};
int main() {
//객체(인스턴스) 생성과 초기화
Point p1(1, 3), p2(2, 7);
Point result = p1 + p2;
result.print();
return (0);
}
: 객체의 멤버를 모두 복사해주는 연산자