본문 바로가기
Programming/C++

[Modern C++ 공부 - Day7] 선언과 초기화 Declaration & Initialization

by 롱일스 2023. 6. 16.
반응형

Modern C++ 공부 Day 7입니다.

오늘 공부할 내용은 C++ 변수 선언과 초기화입니다.

 

이전 포스트를 읽고 오시면 본 포스트를 공부하는 데 도움이 됩니다.

2023.06.15 - [Programming/C++] - [Modern C++ 공부 - Day6] C++ Semantics 시맨틱스

 

[Modern C++ 공부 - Day6] C++ Semantics 시맨틱스

Modern C++ 공부 Day 6입니다. 오늘 공부할 내용은 C++ 시맨틱스입니다. 이전 포스트를 읽고 오시면 본 포스트를 공부하는 데 도움이 됩니다. 2023.06.15 - [Programming/C++] - [Modern C++ 공부 - Day5] 상수 참조에

huangdi.tistory.com

 

 

C++ 범용 초기화 Universal Initialization

기본적으로 Modern C++에서 괄호{ }를 사용하면 아래와 같이 어떤 타입의 변수도 초기화할 수 있습니다.

int x{10};			// int x = 10;과 동일
string str{"Hello World"};  	// string str("Hello World");와 동일

vector<int> vec{1, 2, 3, 4, 5};  // std::vector 컨테이너 {1, 2, 3, 4, 5}로 초기화
// vector<int> vec; 
// vec.push_back(1);
// vec.push_back(2);
// ...와 동일

위의 코드에서처럼 이전 버전에서 쓰던 방식으로 변수를 초기화할 수 있지만, { }을 이용해서 일관성있게 모든 타입의 변수를 초기화할 수 있다는 점이 기억하기 더 쉽습니다. 

하지만, 아래와 같이 변수 선언 타입과 초기화하려는 값의 타입이 다른 경우에는 조심해야 합니다.

int x = 3.2;		// 컴파일 성공, 하지만 경고가 뜰 수 있다.
int x{3.2};		// 컴파일 에러.
  •  기존의 방식으로 변수를 초기화할 때, 위와 같이 int 로 선언했지만 double로 초기화하는 경우에는 int 형식이 허용하는 범위보다 큰 데이터는 무시됩니다. 그래도 Modern 컴파일러로 컴파일은 가능합니다. 경고 Warning이 뜰 수 있지만요.
  • 반면에 { }을 이용하여 컴파일하는 경우에는, 변수 선언 타입과 초기화 데이터 타입이 다르면 아예 컴파일이 되지 않습니다. 

 

Vector 초기화

Modern C++에서 많이 사용하는 std::vector를 초기화하는 방식은 아래와 같이 다양합니다.

vector<int> vec1(3);			// vec1 요소가 {0,0,0}
vector<int> vec2(3, 2);			// vec2 요소가 {2,2,2}
vector<int> vec3{3};			// vec3 요소가 {3}
vector<int> vec4{3, 2};			// vec4 요소가 {3,2}

제대로된 코드로 출력해서 다시 한 번 확인해봅시다.

#include <iostream>
#include <string>
#include <vector>
using namespace std;

void print(const vector<int>& vec) {
    for (auto v : vec) {
        cout << v << ", ";
    }
    cout << endl;
}

int main() {
    vector<int> vec1(3);
    vector<int> vec2(3, 2);
    vector<int> vec3{3};
    vector<int> vec4{3, 2};

    cout << "vec1 = ";
    print(vec1);

    cout << "vec2 = ";
    print(vec2);

    cout << "vec3 = ";
    print(vec3);

    cout << "vec4 = ";
    print(vec4);

	return 0;	
}

출력값은 다음과 같습니다.

vec1 = 0, 0, 0,
vec2 = 2, 2, 2,
vec3 = 3,
vec4 = 3, 2,

 

클래스 객체 초기화

다음으로, 클래스의 객체를 선언하고 초기화할 때는 어떻게 할까요? 이 경우에도 { }를 사용할 수 있습니다.

class Test {};

int main() {

    Test test{}; // 클래스 Test의 객체 test 생성 및 초기화
    ...
    Test test(); // 클래스 Test의 객체를 반환하는 함수 test 선언
}
  • Test test{ }; 를 통해서 클래스 Test의 객체 test를 생성하고 기본 생성자를 이용해 초기화.
  • Test test( ); 는 객체 생성이 아닌 test라는 함수 선언.

위 코드에서 { }를 이용해서 객체도 생성할 수 있다는 것을 알 수 있습니다. 여기서 주의할 점은 ( )를 이용하면 객체 생성이 아닌 함수 선언으로 컴파일러가 인식한다는 것입니다.

 

새로운 타입 정의

다음으로, 개발자가 직접 새로운 타입을 정의할 때 사용할 수 있는 문법을 소개해보겠습니다. 이전에는 typedef를 이용해서 다음과 같이 새로운 타입을 정의하곤 했습니다. 

typedef vector<int> IntVec;

int main() {
	IntVec vec;
}

Modern C++에서는 아래와 같이 using을 이용해서 새로운 타입을 정의할 수 있습니다.

using IntVec = vector<int>;

int main() {
	IntVec vec;
}

 

Null Pointer

마지막으로, nullptr 널 포인터 (Null pointer)에 대해서 알아보겠습니다.

  • NULL은 C++에서 사용되는 null 포인터의 오래된 표현입니다. 실제로 NULL은 0으로 정의되며, 이는 C++에서 "null 포인터 상수"로 간주됩니다. NULL은 C++보다 먼저 개발된 C 언어에서 파생되었으며, C++에도 계승되었습니다. 하지만 NULL이 정수 0으로 정의되어 있어서 함수 오버로딩 등의 상황에서 문제가 발생할 수 있습니다.
  • 이에 비해 nullptr은 C++11에서 도입된 새로운 키워드로, 진정한 null 포인터를 나타냅니다. nullptr은 모든 포인터 타입에 대입할 수 있으며, 정수 타입이 아니기 때문에 함수 오버로딩에서 모호함을 방지할 수 있습니다.
  • 따라서, Modern C++에서는 nullptr을 사용하여 null 포인터를 나타내는 것이 권장됩니다.
  • Null pointer를 이용하면 아무 객체도 가리키고 있지 않는 포인터 초기화할 수 있습니다.

 

아래 코드를 보면서 제대로 이해했는지 확인해봅시다.

void func(int);
void func(int *);

func(nullptr)		// func(int *)를 호출
 
func(NULL)		// clang에서는 func(int *) 호출
			// VC++에서는 func(int) 호출
			// gcc에서는 컴파일되지 않음.
  • nullptr은 포인터 타입을 가진 함수에 대한 호출에 대응됩니다. 따라서 func(nullptr)을 호출하면, void func(int*) 버전의 함수가 호출됩니다. nullptr은 모든 타입의 포인터에 대입할 수 있습니다. 
  • func(NULL)은 컴파일러에 따라 다르게 동작합니다. NULL은 기본적으로 정수 0으로 정의되지만, 많은 C++ 컴파일러들은 NULL을 null pointer constant로 취급합니다.
  • 따라서 어떤 컴파일러(예: clang)는 func(NULL)을 func(int*) 함수를 호출하는 것으로 해석하지만, 다른 컴파일러(예: Visual C++)는 func(int) 함수를 호출하는 것으로 해석할 수 있습니다. 그리고 gcc와 같은 컴파일러는 모호함을 피하기 위해 이러한 호출을 허용하지 않을 수 있습니다.
  • 따라서 C++11 이후에서는 nullptr을 사용하는 것이 가장 안전하며, 함수 호출의 모호성을 방지할 수 있습니다. NULL을 사용하는 것은 예상치 못한 결과나 모호성을 초래할 수 있으므로 가능한 한 피하는 것이 좋습니다.

제대로 컴파일해서 출력값을 통해 확인해봅시다.

#include <iostream>

using namespace std;

void func(int i) {
	cout << "func(int) 호출됨." <<endl;
}

void func(int *i) {
	cout << "func(*int) 호출됨." <<endl;
}

int main() {
	func(NULL);
	func(nullptr);
}
  • GNU g++로 컴파일 할 때는 func(NULL);에서 컴파일 에러가 발생합니다. 왜냐하면 g++에서는 NULL은 다른 방식으로 구현되어 있고, 이 두 함수 중 어느 것도 실제로 일치하지 않기 때문입니다.
  • Visual C++로 컴파일 시에는 아래와 같이 출력값이 나옵니다.
func(int) 호출됨.
func(*int) 호출됨.

 

지금까지 Modern C++에서의 선언과 초기화에 대해서 알아봤습니다.

다음 시간에는 C++ Class 클래스에 대해서 복습하는 시간을 가지겠습니다.

728x90
반응형