C++ 고급 문법 테크닉 - C++ 객체 초기화[5]
C++11의 등장으로 객체를 초기화 하는 방법에는 많은 변화가 생겼습니다. 기존 초기화 방식의 문제점을 살펴 보고, “uniform initialization”, “direct vs copy”, “defalue vs value” 등 새로운 초기화 방법에 대해 살펴보겠습니다.
C++98/03 초기화의 문제점
- 객체의 종류에 따라 초기화 방법이 다름
- 클래스의 일반 멤버로 있는 배열을 초기화 할 수 없음
- 동적 메모리 할당으로 만들어진 배열을 초기화 할 수 없음
- STL 컨테이너를 초기화 하는 편리한 방법이 없음
int main()
{
// 1. 객체(변수) 마다 초기화 방법이 상이함
int n1 = 0;
int n2(0);
int ar[3] = { 1,2,3 };
Point p = { 1,2 }; // 구조체
complex c(1, 2); // 클래스
// 2. 내부 배열 초기화 불가
class Test
{
int x[10];
};
// 3. 동적 할당 배열 초기화 불가
int* p = new int[3];
// 4. STL 컨테이너 간단하게 초기화 불가
vector<int> v;
for (int i = 0; i < 10; i++)
v.push_back(1);
}
Member initializer 관련 유의사항
#include <iostream>
using namespace std;
int cnt = 0;
class Test
{
public:
// int data = 0; // member initializer
int data = ++cnt; // 생성자에서 초기화 할 경우 이 초기화 표현 적용 되지 않음
Test() {}
Test(int n) {} : data(n) {} // 생성자에서 멤버 초기화
};
int main()
{
Test t1; // data = 1
Test t2(3); // data = 3
cout << cnt << endl; // 1
cout << t1.data << endl;
cout << t2.data << endl;
}
Uniform Initialization
일관된 초기화 : 객체의 형태에 상관 없이 중괄호({})를 사용해서 초기화 하는 것
struct Point
{
int x, y;
};
class Complex
{
int re, im;
public:
Complex(int r, int i) : re(r), im(i) {}
};
int main()
{
// 예전 초기화 방식
// int n = 0;
// int x[2] = { 1,2 };
// Point p = { 1,2 };
// Complex c(1, 2);
int n = { 0 };
int x[2] = { 1,2 };
Point p = { 1,2 };
Complex c = { 1, 2 };
/* 이런 식으로 표현해도 동작은 같음
int n{ 0 };
int x[2]{ 1,2 };
Point p{ 1,2 };
Complex c{ 1, 2 };
*/
int n2 = 3.4; // ok
//int n3 = { 3.4 }; // error. 데이터 손실은 오류로 처리(prevent narrow)
//char c1{ 300 }; // error. 1 byte를 초과함으로 오류로 처리
char c2{ 100 }; // ok
}
Direct(직접 초기화) vs Copy(복사 초기화)
- 직접 초기화 : 초기화 시에 =을 사용하지 않는 것
- 복사 초기화 : 초기화시 =을 사용 하는 것
int main()
{
int n1 = 0; // copy initialization
int n2(0); // direct initialization
int n3 = { 0 };
int n4{ 0 };
}
- 둘의 차이점
class Point
{
int x, y;
public:
// explicit : 변환 생성자로 사용될수 없다.
// 또하나의 의미, copy initialization 될수 없다.
explicit Point() : x(0), y(0) {}
explicit Point(int a) : x(a), y(0) {}
explicit Point(int a, int b) : x(a), y(b) {}
};
int main()
{
Point p1(5); // ok.
//Point p2 = 5; // ok. 하지만 생성자 explicit 라면 error.
Point p3(1, 1); // ok
//Point p4 = (1, 1); // error.
Point p5{ 1,1 }; // ok
//Point p6 = { 1,1 }; // ok. 하지만 생성자 explicit 라면 error.
Point p7;
Point p8{}; // direct
//Point p9 = {}; // copy. error
}
Default(디폴트 초기화) vs Value(기본값 초기화)
- 기본 설명
#include <iostream>
using namespace std;
int main()
{
// direct initialization
int n1(0);
int n2{ 0 };
// copy initialization
int n3 = 0;
int n4 = { 0 };
//---------------------------
int n5; // defalut initialization. 쓰레기값
int n6{}; // value initialization. 0으로 초기화
cout << n5 << endl; // 쓰레기값
cout << n6 << endl; // 0
int n7(); // 주의. 초기화가 아닌 함수 선언 표현
}
- 사용자 정의 타입(클래스 등) 일 경우 주의사항
#include <iostream>
using namespace std;
class Point
{
public:
int x;
int y;
Point() {}
//Point() = default;
};
int main()
{
Point p1; // default initialization
Point p2{}; // value initialization
cout << p1.x << endl; // 쓰레기 값
cout << p2.x << endl; // 쓰레기 값, 값 초기화를 의도 하였을 경우 Point() = default; 초기화를 사용 해야함
}
- 디폴트 초기화 관련 규칙 더 알아보기
#include <iostream>
using namespace std;
int main()
{
int n1; // default. 쓰레기값
int n2{}; // value. 0
int n3(); // 함수선언.
int* p1 = new int; // default. 쓰레기값.
int* p2 = new int();// value. 0
int* p3 = new int{};// value. 0
cout << *p1 << endl; // 쓰레기 값
cout << *p2 << endl; // 0
cout << *p3 << endl; // 0
}
initializer_list
중괄호 초기화 표현 사용시 값이 2개 이상 나열 되어 있을 경우
#include <iostream>
#include <initializer_list>
using namespace std;
int main()
{
// brace-init-list
// VC++ : int* first, *last
// g++ : int* first, count
initializer_list<int> s = { 1, 2, 3, 4, 5 };
auto p = begin(s); // 상수를 가르키는 반복자
//*p = 20; // 대입은 대상이 상수성을 띄므로 error
cout << *p << endl; // 꺼내 쓰는 것은 OK
}
- 함수 인자로 활용
#include <iostream>
#include <initializer_list>
using namespace std;
void foo( initializer_list<int> e )
{
auto p1 = begin(e);
auto p2 = end(e);
for (auto n : e)
cout << n << " ";
cout << endl;
}
int main()
{
initializer_list<int> e1 = { 1,2,3 }; // copy init도 가능
initializer_list<int> e2{ 1,2,3,4,5 }; // direct init
foo(e1);
foo({ 1,2,3,4 });
foo({ 1,2,3,4,5,6,7 });
}
- 클래스 생성자에서의 활용
#include <iostream>
#include <initializer_list>
using namespace std;
class Point
{
int x, y;
public:
// 1번 생성자
Point(int a, int b) { cout << "int, int" << endl; }
// 2번 생성자
Point(initializer_list<int> ) { cout << "initializer_list<int>" << endl; }
};
int main()
{
Point p1(1, 1); // 1번, 없다면 error
Point p2( { 1, 1 } ); // 2번. {1,1} 변환생성자를 사용해서 임시객체생성. 복사생성자를 사용해서 p2복사
// 없으면 error지만, explicit 가 아닐경우. 변환 생성.
Point p3{ 1,1 }; // 2. 없으면 1번
Point p4 = { 1,1 }; // 2. 없으면 1번.
Point p5(1, 2, 3); // error
Point p6{ 1,2,3 }; // 2번.
Point p7 = { 1,2,3 };
vector<int> v = { 1,2,3,4 };
}
- STL 컨테이너(vector) 초기화 예제
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v = { 1,2,3,4,5 }; // ok. 편리해 졌다
vector<int> v1(10, 3); // 1번. 10개를 3으로 초기화
vector<int> v2{ 10,3 }; // 2번, 2개의 요소를 10, 3으로 초기화
cout << v1.size() << endl; // 10
cout << v2.size() << endl; // 2
}
aggregate initialization
구조체, 배열을 생성자 없이 중괄호 초기화로 초기화 가능
#include <iostream>
using namespace std;
// aggregate type : {} 로 초기화 가능한것. 구조체, 배열.
struct Point
{
int x, y;
// 만약 가상함수를 포함 할 경우 클래스로 취급되어 aggregate type이 아니게됨
// virtual void foo() {}
};
int main()
{
Point p1; // 1. ok
Point p2 { 1,2 }; // 2. ok
}
위의 C++98/03 초기화 문제점 해결
- 중괄호 초기화로 통일성이 생김
#include <iostream>
#include <vector>
using namespace std;
int main()
{
// 1.
int n1{ 0 };
int n2{ 0 };
int ar[3]{ 1,2,3 };
Point p = { 1,2 }; // 구조체
complex c{ 1, 2 }; // 클래스
// 2.
class Test
{
int x[10]{ 1,2,3,4,5,6,7,8,9,10 };
};
// 3.
int* p = new int[3]{ 1,2,3 };
// 4.
vector<int> v{ 1,2,3 };
}