KoreanFoodie's Study
모던 C++ 입문 1 - C++ 기초 (초기화, 리터럴, 예외, 포인터) 본문
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 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가 더 이상 데이터를 참조하지 않는 즉시 메모리를 자동으로 해제한다.
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는 공유하더라도 소유권을 주장하지 않는다. 자세한 내용은 이 글을 참고하자.
'Tutorials > C++ : Beginner' 카테고리의 다른 글
C++ 기초 개념 4-1 : 객체 지향 프로그래밍, 객체와 클래스 (0) | 2021.12.20 |
---|---|
C++ 기초 개념 3 : new와 delete (0) | 2021.12.20 |
C++ 기초 개념 2 : 참조자(레퍼런스) (0) | 2021.12.20 |
C++ 기초 개념 1 : 이름공간(namespace) (0) | 2021.12.20 |
C++ 정리 및 리마인드 (모던 C++를 중심으로) (0) | 2021.11.17 |