C++에서는 멤버 초기화를 어떤 방식으로 할까? 😊

안녕하세요! 두두코딩 널두 🥸 입니다 ✋
오늘은 C++ 초기화 리스트 개념에 대해 알아보겠습니다.

🖇 소스코드에 마우스를 올리고 copy 버튼을 누를 경우 더 쉽게 복사할 수 있습니다!

궁금한 점, 보안점 남겨주시면 성실히 답변하겠습니다. 😁
+ 감상평 댓글로 남겨주시면 힘이됩니다. 🙇

Intro.

이번 포스팅에서는 C++에서 사용하는 초기화 리스트 / 위임 생성자 에 대해 알아보겠습니다.

C++ 객체데이터 초기화

생성자를 통해 멤버 데이터를 초기화를 진행하는데, 보통 아래와 같이 초기화를 진행한다.

#include <iostream>

class Point
{
	int x;
	int y;
public:
	// Point 생성자
	Point() {
		x = 0;
		y = 0;
	}
};

int main()
{
	Point p;
}

위와 같이 생성자 내에서 xy 데이터를 초기화 한다. 하지만 이와 같이 초기화하는 동작은 초기화라고 볼 수 없고, 대입이라고 봐야한다.

아래의 코드를 통해 알아보자.

// 초기화
int x1 = 0;

// 대입
int x2;
x2 = 0;

코드를 보면, 초기화대입은 분명 다른 코드라는 것을 알 수 있다. 초기화 동작을 할 경우 메모리를 할당하면서 해당 메모리 값에 데이터를 미리 넣어줘 만드는 것을 말한다.

대입의 경우 메모리 할당을 받아 쓰레기 값 (임의값) 을 넣어주고, 이후 값을 복사해서 넣는 것이다.

사실 primitive타입 같은 경우 큰 차이 없이 사용가능하다.

우리가 앞서 정의한 Point와 같은 사용자 정의 타입에서 초기화 동작과 대입 동작에 큰 차이를 나타내며, 실제 성능 저하를 유발할 수 있다.

구체적으로 사용자 정의 타입을 초기화 통작으로 수행할 경우 생성자는 오직 1번만 호출 된다.

반면에 대입 동작을 통한 초기화를 할 경우, 쓰레기 값을 채우기 위한 생성자를 1번 호출하고 이후 대입을 위한 대입 연산을 호출하도록 한다.

즉, 초기화 동작을 통해 하는 것이 함수 호출을 1회 줄이는 것이기 때문에 성능적 측면에서도 더 좋다.

그렇다면 우리가 정의한 Point대입 방법이 아닌 초기화를 하기 위해서는 어떻게 해야될까?

이때 사용되는 것이 바로 초기화 리스트이다.

#include <iostream>

class Point
{
	int x;
	int y;
public:
	// Point 생성자
	Point() /* 초기화 리스트 활용 */ : x(0), y(0) {
		// 대입을 통한 초기화 동작이 아님.
		// x = 0;
		// y = 0;
	}
};

int main()
{
	Point p;
}

위의 코드와 같이 초기화 리스트 동작과 함께 초기화를 하면 된다. 초기화 리스트를 사용하기 위해서는 :를 생성자 뒤에 적어주고, 각 멤버데이터를 초기화 하면 된다. 초기화를 희망하는 값을 ( ) 내 적어주면 된다.

초기화와 대입은 다르다!

우리가 처음 배울 때 대입을 너무 당연시 활용해 초기화로 착각하는 경우가 많이 한번더 강조하고자 한다.

초기화 리스트를 활용할 경우 초기화 동작을 수행하도록 하고, 대입을 활용하면 대입을 수행한다고 했다.

코드로서 확인할 수 있는 방법이 없을까?

const& 를 활용하면 코드에서 초기화리스트는 초기화를 담당하는 것을 알 수 있다.

보통 const&는 초기화 시점에 데이터 값이 존재해야한다.

const같은 경우 초기화 한 이후 데이터 변경이 안되기 때문에 대입은 절대 불가능하다.

& 같은 경우 초기화가 필수이다.

이런 특징을 활용해, 초기화리스트의 동작을 살펴보자.

#include <iostream>

class Point
{
	int x;
	int y;
	const int n;
public:
	// Point 생성자
	Point() /* 초기화 리스트 활용 */ : x(0), y(0) {
		n = 10;
	}
};

int main()
{
	Point p;
}

위의 코드와 같이 작성할 경우 const 값을 대입을 통해 변경하는 것으로 판단되어 컴파일 에러가 발생한다. 이 동작을 아래와 같이 초기화 리스트로 옮기면 문제 없이 코드가 동작한다. 초기화 리스트초기화를 위한 동작이기 때문에 const를 초기화하는 것으로 이해한다.

#include <iostream>

class Point
{
	int x;
	int y;
	const int n;
public:
	// Point 생성자
	Point() /* 초기화 리스트 활용 */ : x(0), y(0), n(10) {}
};

int main()
{
	Point p;
}

초기화 리스트 주의사항

아래의 코드를 보고, 실제 x의 값이 어떤 값으로 지정될 것 같은지 생각해보자.

#include <iostream>

class Point
{
public:
	int x;
	int y;
public:
	Point() : y(0), x(y) {}
};

int main()
{
	Point p;

	std::cout << p.x << std::endl;
	std::cout << p.y << std::endl;
}

위의 코드를 봤을 때 p.x의 값은 어떤 것으로 나올 것인가? 0이 출력될 것이라 생각하는가?

정답은 그럴수도 있고 아닐수도 있다이다. 좀 더 구체적으로 이야기하면, x의 값은 쓰레기 값이 출력된다.

쓰레기 값 같은 경우 컴파일러마다 다르기 때문에 어떤 값이 나올지 모른다. 우리가 코드에서 보는것과 같이 y 라는 값이 0을 먼저 넣고, x에 y값을 활용해 초기화하니 문제 없는 것 아닐까? 라고 생각하면 안된다.

초기화 리스트 같은 경우 말그대로 초기화 동작을 위해 사용되는 것이다. 우리가 객체를 초기화할때는 멤버 데이터를 순서대로 메모리에 올리고, 할당받는다. 할당 받은 메모리에 초기화 리스트를 활용해 값을 넣는다.

지금 위의 코드 같은 경우 x, y 순서로 멤버데이터가 놓여있기 때문에 메모리 할당과 초기화 순서도 x->y와 같은 순서로 이루어진다.

따라서, y값을 참조하게 될 경우 아직 초기화 되지 않은 쓰레기 값을 참조해 초기화하게 된다.

이를 해결하기 위해서는 초기화 리스트의 순서를 변경하든, 멤버데이터 순서를 변경하면 된다.

#include <iostream>

class Point
{
public:
	/* 멤버 데이터 순서 변경 */
	int y;
	int x;
public:
			 /* 초기화 리스트 순서 변경해도 됨. */
	Point() : y(0), x(y) {}
};

외부 생성자에서 초기화리스트 사용

우리는 생성자를 꼭 멤버 함수로 구현할 필요는 없다. 즉, 외부로 빼서 생성자를 구현할 수 있다. 이 경우 초기화리스트는 어떻게 작성해야될까?

#include <iostream>

class Point
{
	int x;
	iny y;
public:
	Point(int a = 0, int b = 0);
};

// 외부에 만들 땐 항상 클래스 명을 같이 적어줘야함.
Point::Point(int a, int b) : x(a), y(b)
{  }

위와 같이, 외부에 생성자 구현체를 적을 경우, 내부와같이 초기화리스트를 같이 적어주면된다. 우리가 앞서 배운 default parameter 같은 경우는 선언부에만 적어준다는 걸 기억하자.

위임 생성자

C++11에서 등장하는 위임 생성자 개념에 대해 알아보자.

class Point
{
	int x, y;
public:
	Point()
	{
		Point(0, 0);
	}

	Point(int a, int b) : x(a), y(b) {}
};

int main()
{
	Point p;
}

기본 생성자에서 인자2개를 받는 생성자를 호출하기 위해서는 위와 같이 작성할 수 있다. 위와 같이 작성할 경우 어떤 문제가 발생할까?

기본 생성자에서 Point(0,0); 와 같은 코드를 작성할 경우 내부 인자2개의 생성자를 부르는 것이아니라, 임시 객체를 새롭게 생성한다. 임시객체 같은 경우 조금 난이도가 있는 내용이라 나중에 다루도록 한다.

무튼, 우리가 원하는 인자 2개를 가진 내부 생성자가 point(0,0)과 같은 방식으로는 호출되지 않는다는 점이다.

이를 해결하기 위해서는 꼭 초기화리스트를 통해 부르도록 해야되는데, 아래의 코드와 같이 작성할 경우 우리가 원하는 내부 인자 2개를 가진 생성자를 호출할 수 있다. 이런 행위를 위임 한다라고 해 위임생성자라고 부른다.

class Point
{
	int x, y;
public:
	// 초기화리스트로 위임함. 위임생성자.
	Point() : Point(0,0) { }

	Point(int a, int b) : x(a), y(b) {}
};

int main()
{
	Point p;
}

C++11 부터 추가된 문법으로, 해당 문법이 필요한 이유를 알아보도록 하자.

위임 생성자의 필요성

사용자 정의 타입에서 가지고 있는 멤버 데이터가 사용자 정의 타입일 경우, 객체가 생성될 때 멤버데이터의 사용자 타입도 초기화를 위해 생성자를 호출하도록 한다.

하지만, 아래와 같이 기본 생성자가 정의되어 있지 않은 상태에서 사용자 정의 타입이 호출될 경우는 어떻게 될까?

class Point
{
	int x,y;

public:
	// 기본생성자가 없음. 컴파일러가 만들어주지 않는다.
	// 사용자가 정의했기 때문..
	Point(int a, int b) : x(a), y(b) {}
};

class Rect
{
	// 얘네의 생성자가 불림.
	Point p1;
	Point p2;

public:
	Rect() {}
};

int main()
{
	Rect r;
}

위 코드를 수행할 경우 에러가 발생한다.

Point 객체의 기본 생성자가 없기 때문이다. 이 경우 어떻게 해야할까?

기본 생성자를 만들거나, 아니면 위에서 우리가 학습한 위임 생성자를 활용하면 된다.

초기화 리스트에 위임생성자를 활용하면 문제 없이 동작하는 것을 볼 수 있다.

class Point
{
	int x,y;

public:
	// 기본생성자가 없음. 컴파일러가 만들어주지 않는다.
	// 사용자가 정의했기 때문..
	Point(int a, int b) : x(a), y(b) {}
};

class Rect
{
	// 얘네의 생성자가 불림.
	Point p1;
	Point p2;

public:
	// 위임 생성자 활용
	Rect() : p1(0,0), p2(0,0) {}
};

int main()
{
	Rect r;
}

C++ 사용자 정의 타입 초기화 방법 정리

C++에서 멤버 데이터를 초기화하는 방법은 총 3가지이다.

🌱 대입을통한 방법 (초기화는 아님.)

class Point
{
	int x, y;
public:
	Point() {
		x = 0;
		y = 0;
	}
};

위 코드는 대입을 활용한 초기화를 흉내낸 방법이다. 초기화는 아니라는 점을 기억하자. 실제 할당 다 받고, 대입하는 것이다!

🌱 초기화 리스트를 활용하는 방법

class Point
{
	int x, y;
public:
	Point() : x(0), y(0) {}
};

우리가 앞서 배운 초기화리스트를 활용한 방법이다.

🌱 멤버 데이터 직접 초기화하는 방법

class Point
{
	int x = 0;
	int y = 0;
public:
	Point() {}
};

위의 코드는 멤버 데이터를 직접 초기화 하는 방법이다. 해당 방법은 C++11 부터 등장했으며, 다른 언어 (e.g. Java, C#)와 비슷하게 멤버데이터를 직접 초기화 하는 방법을 위해 만들어졌다고 보면 된다.

이 방법의 문제점은 상수 값만 가능하다는 것이다. 즉, 우리가 위에서 본 것처럼 x(y)와 같은 변수를 활용한 방법이 불가능하다.

C++에서는 모든 초기화를 초기화 리스트로 하는 것을 권장하고 있다. 심지어 primitive 타입도 초기화리스트를 활용하라고 권한다.

군말말고 초기화 리스트를 통해 초기화하는 습관을 가지도록 하자!!

Outtro.

해당 포스팅은 Ecourse의 C++ Basic 강의를 참고해 작성되었습니다.

강의를 참고하실 분은 여기를 클릭해 확인해주세요!