C++ 기초 1 (스트림, 서식지정, 포인터, 스마트포인터, 레퍼런스)
일반적인 스트림 개념
스트림은 화면, 키보드 및 파일에만 국한하지 않는다.
모든 클래스는 istream, ostream, iostream에서 파생할 경우 스트림으로 사용할 수 있으며 해당 클래스의 함수에 대한 구현을 제공한다.
ostream의 레퍼런스를 인수로 사용해 모든 종류의 출력 스트림을 허용하는 출력 함수를 작성할 수 있다.
#include <iostream>
#include <fstream>
#include <sstream>
void write_something(std::ostream& os)
{
os << "Hi stream, did you know that 3 * 3 = " << 3 * 3 << std::endl;
}
int main(int argc, char* argv[])
{
std::ofstream myfile("example.txt");
std::stringstream mysstream;
write_something(std::cout);
write_something(myfile);
write_something(mysstream);
std::cout << "mysstream is: " << mysstream.str(); // 개행을 포함하고 있다.
}
실행결과

서식 지정
I/O 스트림은 헤더 파일 <iomanip>에 있는 소위 I/O 조작기로 서식을 지정할 수 있다.
기본적으로 C++는 부동소수점의 숫자 몇 자리만 출력한다.
따라서 우리는 정밀도를 높이려고 한다.
#include <iostream>
#include <iomanip> // iomanip 헤더 추가
using namespace std;
int main()
{
double pi = M_PI;
std::cout << "pi is " << pi << '\n';
std::cout << "pi is " << setprecision(16) << pi << '\n';
}
이렇게 하면
3.14159 -> 3.141592653589793 으로 나온다는데..
실습할 때 'M_PI' was not declared in this scope가 나왔다
chatGPT에게 물어보니 C++에서는 M_PI 상수는 직접 정의되어 있지 않다고 한다..
헤더에 #include <cmath> 추가하면 실행된다..
값을 왼쪽 정렬하고 빈 공간을 선택한 문자로 채우려면
std::cout << "pi is " << setfill('-') << left << setw(30) << pi << '\n';
std::cout << "pi is " << setfill('-') << left << setw(30) << pi << '\n';
프로그래머는 더 이상 필요하지 않을 때 메모리를 해제해야 할 책임이 있다.
delete[] v;
포인터 관련 오류를 최소화하는 전략 세 가지
1. 표준 컨테이너를 사용하라 : 표준 라이브러리나 유효성이 검증된 라이브러리를 사용하라.
표준 라이브러리의 std::vector는 크기 조정 및 범위 검사를 포함한 동적 배열의 모든 기능을 제공하여 메모리를 자동으로 해제한다.
2. 캡슐화 : 클래스에서의 동적 메모리 관리. 따라서 클래스당 한 번만 처리해야 한다.
개체를 소멸할 대 개체가 할당한 모든 메모리를 해제하면 메모리 할당 빈도는 더 이상 중요하지 않다.
이러한 원칙을 RAII(Resource Acquisition Is Intialization)
3. 스마트 포인터를 사용하라
포인터는 두 가지 용도로 사용한다
- 개체를 참조함
- 동적 메모리를 관리함
소위 원시 포인터의 문제점은 포인터가 데이터를 참조하고 있는지
아니면 더 이상 필요하지 않아 메모리를 해제해야 하는지에 대한 개념이 없다는 점이다.
이러한 구분을 타입 수준에서 명시하고 싶다면 스마트 포인터를 사용하면 된다.
스마트 포인터
모든 스마트 포인터는 <memory> 헤더에 정의되어 있다.
1. unique_ptr
이 포인터의 이름은 참조한 데이터의 고유 소유권을 나타낸다.
기본적으로 일반 포인터와 같이 사용할 수 있다.
원시 포인터와의 주요 차이점은 포인터가 만료되면 메모리가 자동으로 해제한다는 점이다.
따라서 동적으로 할당하지 않은 주소를 할당하면 버그가 발생한다.
unique_ptr는 다른 포인터 타입에 할당되거나 암시적으로 변환할 수 없다.
원시 포인터에서 포인터의 데이터를 얻고 싶다면 멤버 함수 get을 사용하면 된다.
double* raw_dp = dp.get();
다른 unique_ptr에 할당할 수 없으며, 오직 이동만 가능하다.
unique_ptr<double> dp2{move(dp)}, dp3;
dp3 = move(dp2);
복사는 데이터를 복제하는 반면 이동은 원본에서 대상으로 데이터를 전송한다.
이 예에서는 참조한 메모리의 소유권을 먼저 dp에서 dp2로 전달한 다음 dp3로 전달한다.
dp와 dp2는 이후에 nullptr가 되고, dp3의 소멸자는 메모리를 해제한다.
같은 방식으로, unique_ptr를 함수에서 반환할 때 메모리의 소유권을 전달한다.
다음 예제에서 dp3은 f()에 할당한 메모리를 대신 사용한다.
std::unique_ptr<double> f()
{ return std::unique_ptr<double>{new double}; }
int main()
{
unique_ptr<double> dp2;
dp3 = f();
}
이 경우 함수의 결과는 이동될 임시 값이므로 move()가 필요하지 않다.
unique_ptr에는 배열을 위한 특별한 구현이 있다.
이 작업은 (delete[]와 함께) 메모리를 적절히 해제할 때 필요하다.
또한, 특수화를 통해 배열처럼 요소에 접근할 수 있는 기능을 제공한다.
unique_ptr<double[]> da{new double[3]};
for (unsigned i=0; i<3; ++i)
da[i] = i + 2;
그 대신, 연산자 *는 배열에 사용할 수 없다.
unique_ptr의 중요한 이점은 원시 포인터에 비해 시간과 메모리에 대한 오버헤드가 전혀 없다는 점이다.
2. shared_ptr
여러 파티 (각 파티가 포인터를 갖고 있음)에서 공통으로 사용하는 메모리를 관리한다.
shared_ptr가 더 이상 데이터를 참조하지 않는 즉시 메모리를 자동으로 해제한다.
이렇게 하면 프로그램을 상당히 단순하게 만들 수 있다.
특히 복잡한 데이터 구조의 경우 더욱 그렇다.
매우 중요한 애플리케이션 영역은 바로 동시성이다.
즉, 모든 스레드가 스레드에 대한 접근이 끝나면 메모리를 자동으로 해제한다.
unique_ptr과 달리 shared_ptr은 원하는 만큼 자주 복사할 수 있다.
shared_tpr<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;
}
두 double 값을 저장하는 메모리를 p1과 p2에 할당
포인터는 p3는 p2를 복사해 아래와 같이 메모리를 가리킴

use_count의 출력 결과를 통해 확인 가능
p3.use_count() = 2
p.use_count() = 1
함수가 반환하면 포인터를 파괴하고 p1이 참조하는 메모리를 (사용하지 않고) 해제한다.
두 번째로 할당한 메모리 블록은 main 함수의 p가 여전히 이를 참조하기 때문에 계속 존재한다.
가능하다면 make_shared를 사용해서 shared_ptr를 만들어야 한다.
shared_ptr<double> p1 = make_shared<double>();
이렇게 하면 메모리에 함께 관리 및 비즈니스 데이터를 저장하며 메모리 캐싱이 보다 효율적이다.
make_shared는 shared_ptr를 반환하기 때문에, 단순화를 위해 자동 타입 추론을 사용할 수 있다.
shared_ptr에는 메모리와 실행 시간에 약간 오버헤드가 있다.
하지만 약간의 오버헤드로 프로그램을 단순화할 수 있다.
3. weak_ptr
shared_ptr에서 발생할 수 있는 문제는 메모리 해제를 방해하는 순환 참조다.
이러한 순환은 weak_ptr를 통해 중단할 수 있다.
weak_ptr는 공유하더라도 소유권을 주장하지 않는다.
동적으로 메모리를 관리하기 위해 포인터 대신 사용할 수 있는 수단은 없다.
다른 개체만을 참조하고 싶다면 레퍼런스라는 다른 언어 기능을 사용하면 된다.
레퍼런스
int i = 5;
int& j = i;
j = 4;
std::cout << "i = " << i << '\n';
레퍼런스를 정의할 때마다 포인터와는 달리 어떤 변수를 참조할 것인지를 직접 선언해야 한다.
나중에 다른 변수를 참조할 수는 없다.
레퍼런스는 함수 인수, 다른 개체의 부분 참조 및 뷰 구축에 매우 유용하다.
C++11 표준에서는 포인터와 레퍼런스 사이의 절충안으로 레퍼런스와 비슷하게 동작하지만
일부 제한을 피하는 reference_wrapper 클래스를 제공한다.
포인터와 레퍼런스 비교
레퍼런스에 비해 포인터가 갖는 주요 이점은 동적 메모리 관리 및 주소 계산 기능이다.
반면에 레퍼런스는 기존 위치를 참조해야 한다.
따라서 레퍼런스는 (정말로 악의적인 의도를 갖고 트릭을 사용하지 않는 한) 메모리 누수를 남기지 않고,
참조한 개체와 같은 표기법을 사용한다.
레퍼런스의 컨테이너를 만드는 건 거의 불가능.
레퍼런스는 오류에 안전하지 않지만 오류 발생 가능성이 훨씬 적다.
포인터와 레퍼런스 비교
특징 | 포인터 | 레퍼런스 |
정의된 위치 참조 | O | |
초기화 필수 | O | |
메모리 누수 방지 | O | |
개체와 같은 표기법 | O | |
메모리 관리 | O | |
주소 계산 | O | |
컨테이너 만들기 | O |
어렵다 ㅜㅜ