KoreanFoodie's Study

모던 C++ 입문 1 - C++ 기초 (초기화, 리터럴, 예외, 포인터) 본문

Tutorials/C++ : Beginner

모던 C++ 입문 1 - C++ 기초 (초기화, 리터럴, 예외, 포인터)

GoldGiver 2021. 11. 14. 13:18

double square_root(double x) noexcept {...}

'모던 C++ 입문'을 읽으며 내용을 정리하고 중요한 부분을 기록하는 글입니다.


모던 C++ 입문 1 - C++ 기초 (초기화, 리터럴, 예외, 포인터)

 

 

변수

변수는 가능한 늦게 선언하라. 일반적으로 초기화를 하기 전에는 선언하지 않는 것이 좋다.

 

 

리터럴

값 뒤에 문자를 붙여 리터럴임을 표시할 수 있다.

리터럴 타입
2 int
2u unsigned
2l long
2ul unsigned long
2.0 double
2.0f float
2.0l long double

유용함 : 표준 라이브러리는 복소수를 위한 타입을 제공한다.

std::complex<float> z(1.3, 2.4), z2;

불행하게도 float이외의 타입과 연산이 불가능하다. 즉 2.0 * z 가 불가능하다는 뜻. 2.0f * z 는 가능하다.

정확함 : 0.333333333333333333 이라는 숫자는 long double 타입으로 선언했을 때 자릿수를 잃어버릴 수 있다. 이 때 0.333333333333333333l로 정의를 하면 정확한 자릿수를 보존할 수 있다.

C++ 14 이후에는 숫자 앞에 0, 0x, 0b를 붙여 각각 8진수, 16진수, 2진수 값을 표현할 수 있다. 또한 999666333의 경우, 999'666'333처럼 가독성을 위해 ' 를 붙여줄 수 있다.

 

 

축소하지 않는 초기화

C++ 11에서는 값들이 축소되지 않음(Nor Narrowed)를 확인하는 초기화를 도입한다. 이를 위해 유니폼 초기화(Uniform Initialization)이나 중괄호 초기화(Braced Initialization)을 사용한다.

int a = 3.14;	// 축소하지만 컴파일된다 (위험)
int b = {3.14}; // 축소 오류 : 소수 부분이 사라짐

unsigned u1 = -3;	// 축소하지만 컴파일된다 (위험)
unsigned u2n = {-3}; // 축소 오류 : 음수를 가질 수 없다.

 

 

연산자

연산자 순서를 외울 필요는 없다. 괄호를 할용해 모호함을 없애라. 다만 ++i 가 i++보다 빠르다는 것을 기억해라. 후위 연산자는 새로운 임시객체를 만들기 때문에 느리고 에러 발생의 가능성이 있다.

 

 

함수

값에 의한 호출 : 복사본을 생성한다. 일반적으로 느리다.

레퍼런스에 의한 호출 : 매개변수를 수정할 수 있다. 또한 큰 자료 구조일때는 레퍼런스 호출을 사용하여 주소값을 전달해야 한다.

인라인 : 함수 호출 비용을 줄이기 위해 컴파일러는 함수 호출을 인라인하여 함수에 포함된 연산을 미리 코드로 대체해 놓는다.

 

 

오류 처리

C++는 단정(Assertion)과 예외(Exception)으로 예기치 않은 동작을 처리한다.

단정 : <cassert> 헤더의 assert( ) 함수를 이용, 괄호 안의 값이 false면 즉각 종료한다.

assert의 장점은 #define NDEBUG 를 통해 모든 단정을 비활성화할 수 있다는 것이다.

 

 

예외

일반적으로 try - catch 블록을 활용한다.

try {
...
} catch (e1_type& e1) {

} catch (e2_type& e2) {

} catch (...) { // 다른 모든 예외들을 처리

}

C++ 11 에서는 함수에서 예외를 던지지 않아야 한다는 걸 지정하는 새로운 지정자가 있다.

 

 

I/O

I/O를 사용할 때는 다음과 같이 파일이 열렸는지 체크를 해 주어야 한다.

 

스트림은 예외 처리보다 먼저 나왔기에 그동안 작성된 소프트웨어를 깨뜨리지 않도록 만들려고 했기 때문이다. 

int main()
{
    std::ifstream infile;
    std::string filename("test.txt");
    infile.open(filename);
    if (infile.good())
    {
    	// do something
    }
	else
    {
    	std::cout << "The file " << filename << " doesn't exist!" << std::endl;
    }
}

 

 

포인터와 배열

배열을 함수의 인자로 전달할 때는 포인터로 전달하는 것으로 변환된다. 사실 arr[i]라는 표현에서 [ ]은 연산자로, arr라는 배열의 포인터에서 i-1 만큼을 더한 주소가 가리키는 값을 리턴한다. 

포인터를 선언할 때 무작위 값이 할당되므로 초기화를 해 주는 것이 좋다.

// c++11 이후
int *ptr = nullptr;
int *ptr2{};

// c++11 이전
int *ptr3 = 0;
int *ptr4 = NULL;

 

 

스마트 포인터

C++11의 새로운 스마트 포인터 3가지를 알아보자. (unique_ptr, shared_ptr, weak_ptr). 모든 스마트 포인터는 <memory> 헤더에 정의되어 있다.

 

unique_ptr

unique_ptr는 참조한 데이터의 고유 소유권(Unique Ownership)을 나타낸다. unique_ptr의 경우, 포인터가 만료되면 메모리를 자동으로 해제하므로 동적으로 할당하지 않은 주소를 할당하면 버그가 발행한다.

#include <memory>

int main()
{
    unique_ptr<double> dp{new double};
    *dp = 7;
    // ...
    
    double dd;
    unique_ptr<double> dp2{&dd}; // Error
    
    // get pointer from unique_ptr
    double *raw_dp = dp.get();
    
    // 다른 unique_ptr에 할당할 수도 없다.
    unique_ptr<double>dp3{dp}; // Error
    dp3 = dp; // Error
    
    // unique_ptr는 오직 이동만 가능하다
    unique_ptr<double> dp4{move(dp)}, dp5;
    dp5 = move(dp4);
}

 

shared_ptr

shared_ptr은 여러 파티가 공통으로 사용하는 메모리를 관리한다. shared_ptr가 더 이상 데이터를 참조하지 않는 즉시 메모리를 자동으로 해제한다.

unique_ptr과 달리 shared_ptr은 원하는 만큼 자주 복사할 수 있다.

#include <iostream>
#include <memory>

using namespace std;

shared_ptr<double> f()
{
	shared_ptr<double> p1{new double};
	shared_ptr<double> p2{new double}, p3 = p2;
	cout << "p3.use_count() = " << p3.use_count() << endl;
	return p3;
}

int main()
{
	shared_ptr<double> p = f();
	cout << "p.use_count() = " << p.use_count() << endl;
}

가능하다면 make_shared를 사용해서 shared_ptr를 만들어야 한다.

shared_ptr<double> p1 = make_shared<double>();

 

weak_ptr

shared_ptr에서 발생할 수 있는 문제는 메모리 해제를 방해하는 순환 참조(Cycle Reference)다. 이러한 순환은 weak_ptr를 통해 중단할 수 있다. weak_ptr는 공유하더라도 소유권을 주장하지 않는다. 자세한 내용은 이 글을 참고하자.

Comments