일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- placement new
- base from member
- delete function
- new&delete
- virtual destructor
- std::ostream
- return by reference
- std::cout
- virtual function table
- suffix return type
- 더 지니어스 양면포커
- constructor
- increment operator
- vector capacity
- conversion constructor
- c++ basic practice
- vector size
- diamond inheritance
- operator overloading
- C++
- discord bot
- c++ multi chatting room
- std::vector
- pointer to member data
- member function pointer
- std::endl
- virtual function
- this call
- virtual inheritance
- dynamic_cast
- Today
- Total
I'm FanJae.
[C++] Member initializer list, Default member initializer 본문
※ 본 포스트는 코드누리 C++ Basic 강의 내용을 보고 정리한 포스트입니다.
1. 멤버 데이터를 초기화 하는 3가지 방법
- 멤버 데이터를 초기화 하는 방법은 크게 3가지가 존재한다.
① 지금까지 사용한 방법
class Point
{
int x;
int y;
public:
Point(int a, int b)
{
x = a;
y = b;
}
};
- 우리가 알고 있는 가장 일반적인 방법이다.
② Member initializer list (C++98)
class Point
{
int x;
int y;
public:
Point(int a, int b) : x{a}, y{b}
{
}
};
③ Default member initializer (C++11)
class Point
{
int x{0};
int y{0};
};
- 오늘은 Case 2번과 Case 3번에 대해 알아보고자 한다.
2. Member initializer list
#include <string>
class People1
{
std::string name;
int age;
public:
People1(const std::string &s, int a)
{
name = s;
age = a;
}
};
class People2
{
std::string name;
int age;
public:
People2(const std::string &s, int a) : name{s}, age{a}
{
}
};
- People1과 People2는 초기화 방법만 제외하면 동일해 보인다.
- 하지만 이 둘은 '대입'과 '초기화'라는 차이가 있다.
People1(const std::string &s, int a)
{
name = s; // 대입
age = a;
}
People2(const std::string &s, int a) : name{s}, age{a} // 초기화
{
}
- 한쪽은 대입, 한쪽은 초기화를 의미한다.
- 초기화(initialization)이란, 객체를 생성하면서 초기값을 전달하는 것이다.
- 이에 반면, 대입은 객체를 생성한 후, 값을 전달한다.
std::string name;
name = s; // 대입 연산자가 호출 된다.
- 즉, 대입의 경우 name 객체를 생성하는 과정에서 디폴트 생성자가 한번 호출되고, 대입 연산자가 호출된다는 것이다.
- 대입 연산자에 대해서는 추후 포스트에서 더 정리해보겠다.
2-1. People1과 People2 객체 생성의 차이
People1 p1(s, 20); // People1 객체 생성
std::string name;
int age;
name = s; // string 내부에 구현된 대입 연산자가 호출 됨.
age = a;
- 즉, string의 대입 연산자의 호출이 일어나게 된다.
People2 p2(s, 20); // People2 객체 생성
std::string name{s}; // 내부에 구현된 인자가 한 개인 생성자가 호출
int age = a;
- 반면 People2는 내부에 구현된 인자가 한 개인 생성자가 호출되어, 대입 연산자 호출 과정이 일어나지 않는다.
2-2. std::string 클래스의 정체
- 원래 std::string은 "basic_string이라는 클래스 템플릿이나, 사용이 번거로워서 using을 이용해서 string으로 사용할 수 있게 만들었다.
template <class _Elem, class _Traits = char_traits<_Elem>, class _Alloc = allocator<_Elem> >
class basic_string
using string = std::basic_string<char, std::char_traits<char>, std::allocator<char> >
People1에 대한 어셈블리 코드
- Compiler Explorer 로 확인해보면, People1에서는 string에 대한 디폴트 생성자와 대입 연산자를 부르는 것이 확인된다.
- 그리고, basic_string의 소멸자가 불리는 것도 확인이 가능하다.
People2에 대한 어셈블리 코드
- People2 쪽이 훨씬 더 간단하게 끝나는 것이 확인가능하다.
※ 물론, 최근 컴파일러는 이것에 대한 최적화가 상당히 잘 이루어져서 People1도 효율적인 코드가 나올 수 있겠지만, People2 쪽이 일반적으로 훨씬 더 효율적인 기계어 코드를 만들 수 있다는 것을 의미한다.
※ 정리하면, 생성자에서 멤버 데이터를 초기화 할때는 멤버 초기화 리스트(member initializer list)를 사용하는 것이 좋다.
3. Member initializer list의 사용 예시
① 상수와 참조 변수가 멤버 데이터로 있는 경우
class Object
{
const int c;
int& r;
public:
Object( int n, int& x)
{
c = n;
r = x;
}
};
int main()
{
int num = 10;
Object obj(10, num);
}
- 상수와 참조 변수는 반드시 초기화를 진행해야 한다. 생성자 블록 안에 대입했을때 상황은 아래와 같다.
const int c;
int& r;
c = n; // 상수는 대입 될 수 없다.
r = x; // 참조도 이는 동일하다.
- 따라서, 멤버 초기화 리스트를 사용해야 한다.
class Object
{
const int c;
int& r;
public:
Object( int n, int& x) : c{n}, r{x}
{
}
};
int main()
{
int num = 10;
Object obj(10, num);
}
② 디폴트 생성자가 없는 타입이 멤버로 있는 경우
class Point
{
int x;
int y;
public:
Point(int a, int b) : x{a}, y{b}
{
}
};
class Rect
{
Point leftTop;
Point rightBottom;
public:
Rect(int x1, int y1, int x2, int y2)
{
}
};
int main()
{
Rect r(1,1,10,10);
}
- 현재 Point 클래스는 디폴트 생성자가 없는 상태이다.
- 기본적으로 객체를 생성할땐 모든 멤버 데이터의 생성자도 호출된다.
Point leftTop;
Point rightBottom;
- 위 부분은 누가봐도 디폴트 생성자의 호출로 보인다.
- 이때, 특정 객체를 만들때 '어떻게 초기화' 할 지를 명시해 줄 수 있도록 멤버 초기화 리스트를 사용하면 된다.
Rect(int x1, int y1, int x2, int y2) : leftTop(x1, y1), rightBottom{x2,y2}
- 생성자를 호출시 멤버 데이터에 있는 객체를 어떻게 초기화 해줄지 명시가 가능하다.
- 괄호,중괄호 표기는 둘다 사용 가능함을 보여주기 위함이지만 일관성 있게 하는것이 좋다.
※ Member initializer list를 사용해야 하는 경우는 크게 2가지라고 할 수 있다.
① 상수와 참조 변수가 멤버 데이터로 있는 경우
② 디폴트 생성자가 없는 타입이 멤버로 있는 경우
4. Member initializer list의 사용시 주의 사항
① 멤버 초기화 리스트 코드에서는 멤버 데이터가 선언된 순서대로 초기화를 진행하라.
class Point
{
public:
int x;
int y;
Point() : y{10}, x{y} // 미정의(undefined) 동작 발생.
{
}
}
- 멤버 데이터 순서상 x가 먼저 정의되어 있기 때문에 초기화 코드가 놓인 순서가 아닌 멤버 데이터가 선언된 순서대로 초기화가 된다. 즉, y가 먼저 입력되었지만, x가 먼저 선언되었기에 x먼저 초기화 하고 위 행위는 잘못된 행위이다.
- 멤버 초기화 리스트의 코드를 작성시에는 멤버 데이터가 선언된 순서대로 초기화 코드를 작성하는게 좋다.
② 선언과 구현 파일을 나눌때 유의 사항
// Point.h
class Point
{
int x;
int y;
public:
Point(int x, int y);
};
// Point.cpp
#include "Point.h"
Point::Point(int x, int y) : x{x}, y{y}
{
// this->x = x;
}
- 멤버 초기화 리스트는 구현 파일에 작성 해야 한다.
- 함수(생성자)의 인자 이름이 멤버 데이터의 이름과 동일한 경우, 생성자 블록 안에서는 멤버를 접근하려면, this->멤버 이름을 사용해야하지만, 멤버 초기화 리스트 코드에서는 this가 필요없다. this는 별도 포스트로 다루고자 한다.
5. Default member initializer (C++11)
#include <iostream>
class Point
{
public:
int x{0};
int y{0};
Point() {}
Point(int a) : x{a} {}
Point(int a, int b) : x{a}, y{b} {}
};
int main()
{
Point pt1; // 0, 0
Point pt2(1); // 1, 0
std::cout << pt1.x << ", " << pt1.y << std::endl;
std::cout << pt2.x << ", " << pt2.y << std::endl;
}
- C++11부터 멤버 데이터에 초기값 지정이 가능하다.
- 원리는 컴파일러가 사용자가 위와 같이 만든 코드를 아래와 같이 변경하는 것이다.
#include <iostream>
class Point
{
public:
int x{0};
int y{0};
Point() : x{0}, y{0} {}
Point(int a) : x{a}, y{0} {}
Point(int a, int b) : x{a}, y{b} {}
};
- 즉, 변환의 과정이 일어남을 유의해야 한다.
#include <iostream>
int g = 0;
class Point
{
public:
// int x{0}; // int x = 0;
int x{++g}; // 예시를 보여주기 위한 것이므로 절대 사용하면 안됨.
int y{0};
Point() {}
Point(int a) : x{a} {}
Point(int a, int b) : x{a}, y{b} {}
};
int main()
{
Point pt1; // 1, 0
Point pt2(1); // 1, 0
std::cout << pt1.x << ", " << pt1.y << std::endl;
std::cout << pt2.x << ", " << pt2.y << std::endl;
std::cout << g << std::endl; // 1
}
- 본 예제는 컴파일러가 Default member initializer를 어떤식으로 변환하는지 보여주는 과정이다.
- 실제 코드는 가급적 이렇게 작성하진 말자. 아래와 같이 변환이 된다.
#include <iostream>
int g = 0;
class Point
{
public:
int x{g++};
int y{0};
Point() : x{g++}, y{0} {}
Point(int a) : x{a}, y{0} {}
Point(int a, int b) : x{a}, y{b} {}
};
int main()
{
Point pt1; // 0, 0
Point pt2(1); // 1, 0
std::cout << pt1.x << ", " << pt1.y << std::endl;
std::cout << pt2.x << ", " << pt2.y << std::endl;
std::cout << g << std::endl; // 1
}
- pt1의 경우는 첫번째 생성자가 호출 될 것이고, 이에 따라서 pt1의 x는 0이지만 g의 값은 1이 증가한다.
- 반면 pt2는 두번째 생성자가 호출되기 때문에, g의 값은 증가하지 않고 1이 되는것을 확인할 수 있다.
'C++ > Basic' 카테고리의 다른 글
[C++] Static Member (0) | 2024.08.16 |
---|---|
[C++] Explicit Constructor (0) | 2024.08.14 |
[C++] Vector I (0) | 2024.08.13 |
[C++] Constructor / Destructor (0) | 2024.08.13 |
[C++] OOP(Object Oriented Programming) I (0) | 2024.08.12 |