[C++] 초기화
Uniform Initialization
균일 초기화 : 객체의 형태에 상관 없이 중괄호 {} 를 사용해서 초기화 하는 것
struct Point
{
int x, y;
};
class Rational
{
public:
Rational(int n, int d) : numerator(n), denominator(d) {}
private:
int numerator, denominator;
};
int main()
{
int n = { 0 };
int m[2] = { 1, 2 };
Point p = { 1,2 };
Rational r = { 1,2 };
/* 같은 표현
int n{ 0 };
int m[2]{ 1, 2 };
Point p{ 1,2 };
Rational r{ 1,2 };
*/
//암시적 타입 변환들을 허용하지 않는다.
//정확히 데이터 손실이 있는 변환을 허용하지 않는다.
int n2 = 3.4; //컴파일 가능
int n3 = { 3.4 }; //error
double n4 = { 3 }; //컴파일 가능
}
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;
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
}
#include <iostream>
#include <cstdio>
struct Data
{
int age; //나이
char name[100]; //이름
int balance; //지갑 속의 용돈
};
struct Test
{
Test(int a) {}
};
void Print(Data user)
{
std::cout << user.age << ", " << user.name << ", 잔액 : " << user.balance << std::endl;
}
int main()
{
// 1. 대입 연산자를 사용하여 리터럴을 대입하는 방식으로 변수의 생성과 초기화는 구조체나 클래스 타입의 경우 허용하지 않는다.
// 컴파일 시 int 타입에서 구조체로 타입을 변환시킬 수 없다는 에러가 발생한다.
// Data user01 = 21;
// 이것은 내 생각 -> 위의 구절은 인자가 하나인 생성자를 염두에 두지 않고 선생님께서 쓰신 말같다.
Test test = 1;
// 2. 구조체 내 변수의 순서에 맞추어 리터럴을 함수의 인수로 사용하여 변수를 초기화한다면,
// 다음과 같은 에러가 발생한다. 이 문제의 해결 방안은 구조체의 생성자를 만들어 제공하면 된다.
// Data user02(33, "이순신", 200000);
// 3. 그러나 아래와 같이 중괄호를 사용한다면, 초기화가 혀용된다.
// 입력된 인수는 순서적으로 멤버 변수에 할당된다.
Data user03{ 53, "강감찬", 20000 }; // -std=c+11 이후부터 사용이 가능하다.
Print(user03);
// 4. 무명 변수를 만들어 무명 변수의 값을 변수에 대입시켜 초기화한다. 앞에서 보았던 2. 와 동일한
// 에러가 발생한다. 이 문제 역시 생성자를 만들어주면 해결된다.
// Data user04 = Data(45, "이성계", 304040);
// 5. 아래처럼 함수 호출 연산자와 함께 변수를 생성한다면, 컴파일 에러가 발생한다.
// 무명변수는 허용되지만 안정성의 이유로 아래와 같이 직접 변수를 생성하는 방식은 ISO에서 금지하는 방식이다.
// Data user05();
// 6. 아래와 같이 Data 타입의 무명 변수를 생성하고 변수에 대입한다면, 변수가 생성되는
// 동시에 모든 멤버 변수는 타입에 따라 초기화된다.
Data user06 = Data();
Print(user06);
// 7. 구조체와 클래스는 다음과 같이 나열된 값으로 변수에 순서대로 할당하여 초기화시킬 수 있다.
// 이 방식은 C 언어에서 유래된 방식이다.
Data user07 = { 21, "홍길동", 10000 };
Print(user07);
// 8. 초기화없이 구조체의 변수를 선언한다면 멤버 변수에 알 수 없는 값이 들어간다.
Data user08;
Print(user08);
// new 라는 키워드를 사용하여 메모리 저장소를 생성하여 변수에 할당한다.
// 이 경우 역시 멤버 변수에 알 수 없는 값이 들어간다.
Data* user_08 = new Data;
Print(*user_08);
// 9. new 지정자와 함께 아래처럼 변수를 생성하면 모두 자동으로 초기화된다.
Data* user09 = new Data();
Print(*user09);
}
C++ 17 책의 초기화 부분을 읽어보며 궁금한 점이 몇 개 생겼다.
new 연산자를 이용해 변수를 초기화할 때 () 를 붙였을 때와 안 붙였을 때 차이가 뭘까
컴파일러가 만들어주는 기본 생성자가 그 이유인 것 같은데 정확히는 모르겠으니 테스트 해보자.
#include <iostream>
struct Data
{
int a;
std::string b;
char c;
};
class Data2
{
int a;
std::string b;
char c;
public:
int d;
std::string e;
char f;
};
int main()
{
Data data{ 0, "a", 'c' };
//Data data2(1, "a", 'c');
Data* data3 = new Data;
Data* data4 = new Data();
//Data* data5 = new Data(1, "a", 'c');
Data* data6 = new Data{ 6, "3", 'c' };
//Data* data7 = new Data{ "a", 3, 'c'};
Data2* data8 = new Data2;
Data2* data9 = new Data2();
//Data2* data10 = new Data2{ 6, "3", 'c' };
std::cout << "Data 0번 : " << data.a << std::endl;
std::cout << "Data 3번 : " << data3->a << std::endl;
std::cout << "Data 4번 : " << data4->a << std::endl;
std::cout << "Data 6번 : " << data6->a << std::endl;
}
// 출력 결과
Data 0번 : 0
Data 3번 : -842150451
Data 4번 : 0
Data 6번 : 6
data3, data4 를 봐보자
확실히 출력 결과를 보면 data3 객체의 멤버 변수에는 쓰레기 값이 들어가있는 것으로 보인다.
그럼 생성자를 만들어서 로그를 찍어보자.
#include <iostream>
#define LOG(x) std::cout << x << std::endl;
struct Data
{
Data() { LOG("기본 생성자"); }
int a;
std::string b;
char c;
};
class Data2
{
int a;
std::string b;
char c;
public:
int d;
std::string e;
char f;
};
int main()
{
//Data data{ 0, "a", 'c' };
//Data data2(1, "a", 'c');
Data* data3 = new Data;
Data* data4 = new Data();
//Data* data5 = new Data(1, "a", 'c');
//Data* data6 = new Data{ 6, "3", 'c' };
//Data* data7 = new Data{ "a", 3, 'c'};
Data2* data8 = new Data2;
Data2* data9 = new Data2();
//Data2* data10 = new Data2{ 6, "3", 'c' };
//std::cout << "Data 0번 : " << data.a << std::endl;
std::cout << "Data 3번 : " << data3->a << std::endl;
std::cout << "Data 4번 : " << data4->a << std::endl;
//std::cout << "Data 6번 : " << data6->a << std::endl;
}
// 출력 결과
기본 생성자
기본 생성자
Data 3번 : -842150451
Data 4번 : -842150451
Data 구조체에 기본 생성자를 만드니 유니폼 초기화로 초기화한 Data 들에서 오류가 나 주석 처리했다.
-> data3 과 data4 의 차이를 살펴보고 이것도 알아보자.
출력 결과를 보니까 둘 다 쓰레기 값이 들어가 있다.
결론적으로
1. 컴파일러가 기본으로 만들어주는 생성자는 모든 멤버 변수를 기본값으로 초기화 해준다.
2. 초기화할 때 () 를 붙이는 것은 기본 생성자를 호출 해준다.
인터넷을 뒤져보며 안건데 C++ 은 POD란 개념이 있다.
복잡하지 않은 사용자 정의형 데이터를 진짜 '데이터' 그 자체로 보는 것이다.
자세한 사항은 https://haedallog.tistory.com/78 이 분이 잘 써주셨다.
이제 위에서 유니폼 초기화가 왜 오류가 났는지 살펴보자.
결론은 brace-initialization(중괄호 초기화)와 사용자 정의 생성자(user-defined constructor)가 충돌하기 때문이다.
유니폼 초기화 == aggregate initialization
aggregate initialization은 aggregate 타입에만 허용된다.
구조체 또는 클래스가 아래 조건을 만족하면 aggregate:
- 모든 멤버가 public
- 사용자 정의 생성자 없음
- 가상 함수 없음
- 상속 없음
따라서 생성자를 만들면 { } 기호를 사용해 멤버를 직접 초기화 할 수 없다.