C++

C++ 기초 1 (스트림, 서식지정, 포인터, 스마트포인터, 레퍼런스)

서니션 2023. 5. 23. 15:29
728x90
반응형

일반적인 스트림 개념

스트림은 화면, 키보드 및 파일에만 국한하지 않는다.

모든 클래스는 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  

 

어렵다 ㅜㅜ

728x90
반응형