Program Language/C++ 언어

16. C++ STL 알고리즘, 입력과 출력, 예외 처리

parkes811 2024. 7. 15. 10:47

함수 객체

  • 함수 객체(function object)

- STL 알고리즘에 데이터를 전달하기 위해서는 다음과 같은 방법을 사용합니다.

 

   1. 함수 포인터

   2. 함수 객체

   3. 람다 표현식

 

- 많은 STL 알고리즘이 데이터를 처리하기 위해 매개변수로 함수 객체를 받아들입니다.

- 펑크터(functor)라고도 불리는 함수 객체는 호출 연산자(())와 함께 사용할 수 있는 객체를 의미합니다.

- 이러한 함수 객체는 우선 타입을 선언하고, 해당 클래스에서 호출 연산자(())를 오버로딩하여 구현하게 됩니다.

 

  • 함수 객체의 장점

직접적인 함수 호출과 비교하여 함수 객체를 사용하면 다음과 같은 장점을 가집니다.

 

 1. 함수 객체는 상태(state)를 포함할 수 있습니다.

 2. 함수 객체는 타입이므로, 템플릿 인수로 사용할 수 있습니다.

 

 

  • 미리 정의된 함수 객체

- STL은 자주 사용할만한 연산에 대해 몇 가지 기본적인 함수 객체를 미리 정의하여 제공하고 있습니다.

- 이러한 미리 정의된 함수 객체는 함수를 매개변수로 전달받는 STL 함수를 지원하기 위해 제공됩니다.

- 이러한 함수 객체는 functional 헤더 파일에 정의되어 있습니다.

 

STL에서는 다음과 같은 함수 객체를 미리 정의하여 제공하고 있습니다.

 

- 예제

plus<int> add;
equal_to<int> comp;
greater_equal<int> ge;

cout << add(7, 3) << endl;
cout << comp(7, 3) << endl;
cout << ge(7, 3);

// greater와 less 함수 객체를 각각 sort() 함수의 인수로 전달하여 사용하는 예제
vector<int> vc = {20, 40, 10, 30}; // vector 객체의 선언 및 초기화

sort(vc.begin(), vc.end(), greater<int>());
copy(vc.begin(), vc.end(), ostream_iterator<int>(cout, " "));
cout << endl;

sort(vc.begin(), vc.end(), less<int>());
copy(vc.begin(), vc.end(), ostream_iterator<int>(cout, " "));

 


알고리즘

  • STL 알고리즘 함수

- STL은 여러 알고리즘을 STL 알고리즘 함수나 STL 컨테이너의 멤버 함수를 사용하여 구현하고 있습니다.

 

STL 컨테이너는 반드시 필요한 기능만을 포함하고 있으며, 동작하는데 필요한 모든 기능을 가지고 있지는 않습니다.

따라서 STL 컨테이너는 알고리즘을 제공하는 많은 전역 함수와 함께 사용해야만 제 기능을 발휘할 수 있습니다.

이렇게 제공되는 STL 알고리즘 함수는 반복자를 통해 임의의 컨테이너에 같은 방법으로 적용됩니다.

 

대부분의 알고리즘 함수는 algorithm 헤더 파일과 numeric 헤더 파일에 정의되어 있습니다.


  • STL 알고리즘의 분류

STL 알고리즘은 기능별로 다음과 같이 구분할 수 있습니다.

 

  1. 읽기 알고리즘(algorithm 헤더 파일)

  2. 변경 알고리즘(algorithm 헤더 파일)

  3. 정렬 알고리즘(algorithm 헤더 파일)

  4. 수치 알고리즘(numeric 헤더 파일)


  • 읽기 알고리즘

- STL 읽기 알고리즘 함수는 컨테이너를 변경하지 않으며, 컨테이너의 지정된 범위에서 특정 데이터를 읽기만 하는 함수입니다.

 

STL에서 제공하는 대표적인 읽기 알고리즘 함수는 다음과 같습니다.

 

  1. find()

  2. for_each()

 

- find() 함수는 두 개의 입력 반복자로 지정된 범위에서 특정 값을 가지는 첫 번째 요소를 가리키는 입력 반복자를 반환합니다.

 

- for_each() 함수는 두 개의 입력 반복자로 지정된 범위의 모든 요소를 함수 객체에 대입한 후, 대입한 함수 객체를 반환합니다.


  • 변경 알고리즘

- STL 변경 알고리즘 함수는 컨테이너를 변경하지 않으며, 컨테이너의 지정된 범위에서 요소의 값만을 변경할 수 있는 함수입니다.

 

- STL에서 제공하는 대표적인 변경 알고리즘 함수는 다음과 같습니다.

 

  1. copy()

  2. swap()

  3. transform()

 

- copy() 함수는 두 개의 입력 반복자로 지정된 범위의 모든 요소를 출력 반복자가 가리키는 위치에 복사합니다.

- swap() 함수는 두 개의 참조가 가리키는 위치의 값을 서로 교환합니다.

- transform() 함수는 두 개의 입력 반복자로 지정된 범위의 모든 요소를 함수 객체에 대입한 후, 출력 반복자가 가리키는 위치에 복사합니다.


  • 정렬 알고리즘

- STL 정렬 알고리즘 함수는 컨테이너의 지정된 범위의 요소들이 정렬되도록 컨테이너를 변경하는 함수입니다.

- 모든 정렬 알고리즘 함수는 올바른 정렬을 위해 임의 접근 반복자를 사용합니다.

- 따라서 임의 접근이 가능한 컨테이너만이 사용할 수 있습니다.

 

STL에서 제공하는 대표적인 정렬 알고리즘 함수는 다음과 같습니다.

 

  1. sort()

  2. stable_sort()

  3. binary_search()

 

- sort() 함수는 두 개의 임의 접근 반복자로 지정된 범위의 모든 요소를 서로 비교하여, 오름차순으로 정렬합니다.

- stable_sort() 함수는 두 개의 임의 접근 반복자로 지정된 범위의 모든 요소를 서로 비교하여, 값이 서로 같은 요소들의 상대적인 순서는 유지하면서 오름차순으로 정렬합니다.

- binary_search() 함수는 sort() 함수를 사용하여 오름차순으로 정렬한 후에, 전달된 값과 같은 값이 있으면 참(true)을 반환하고 없으면 거짓(false)을 반환합니다.


  • 수치 알고리즘

- 수치 알고리즘 함수는 다른 알고리즘 함수와는 달리 numeric 헤더 파일에 정의되어 있습니다.

 

- 대표적인 수치 알고리즘 함수는 다음과 같습니다.

 

  1. accumulate()

 

- accumulate() 함수는 두 개의 입력 반복자로 지정된 범위의 모든 요소의 합을 반환합니다.


C++ 입출력

 

- 대부분의 C++ 컴파일러는 iostream과 fstream 헤더 파일에 정의되어 있는 클래스 라이브러리를 제공합니다.

iostream과 fstream 클래스 라이브러리의 중요 개념 중 하나가 바로 스트림(stream)입니다.


  • 스트림(stream)

- C++ 프로그램은 파일이나 콘솔의 입출력을 직접 다루지 않고, 스트림(stream)이라는 흐름을 통해 다룹니다.

- 스트림(stream)이란 실제의 입력이나 출력이 표현된 데이터의 이상화된 흐름을 의미합니다.

- 즉, 스트림은 운영체제에 의해 생성되는 가상의 연결 고리를 의미하며, 중간 매개자 역할을 합니다.

 


  • 버퍼(buffer)

- 스트림은 내부에 버퍼(buffer)라는 임시 메모리 공간을 가지고 있습니다.

- 이러한 버퍼를 이용하면 입력과 출력을 좀 더 효율적으로 처리할 수 있게 됩니다.

 

 

버퍼를 사용하면서 얻을 수 있는 장점은 다음과 같습니다.

 

  1. 문자를 하나씩 전달하는 것이 아닌 묶어서 한 번에 전달하므로, 전송 시간이 적게 걸려 성능이 향상됩니다.

  2. 사용자가 문자를 잘못 입력했을 경우 수정을 할 수가 있습니다.

 

하지만 입력 작업에 버퍼를 사용하는 것이 반드시 좋은 것만은 아닙니다.

빠른 반응이 요구되는 게임과 같은 프로그램에서는 키를 누르는 즉시 바로 전달되어야만 합니다.

 

이렇게 버퍼를 사용하는 입력과 버퍼를 사용하지 않는 입력은 서로 다른 용도로 사용됩니다.

따라서 자신의 목적에 맞게 버퍼의 사용 여부를 판단해야 합니다.


try, throw, catch


  • 예외(exception)란?

- 예외(exception)란 컴퓨터 시스템이 동작하는 도중에 예상하지 못한 오류가 발생하여, 실행되고 있던 프로그램이 중지되는 것을 의미합니다.

- 예외 처리(exception handling)는 이러한 예외 상황을 처리할 수 있도록 코드의 흐름을 바꾸는 행위를 의미합니다.

- C++은 언어 차원에서 예외 처리 문법을 제공하여, 예외 처리하는 방식을 확장하고 관리하기 쉽도록 해줍니다.


  • try, throw, catch 문

- C++에서는 예외 처리의 구현을 위해서 try, throw, catch 문을 제공합니다.

 

  1. try 문 : 예외가 발생할 가능성이 있는 코드 블록

  2. throw 문 : try 문에서 발생한 오류에 대한 정보를 전달

  3. catch 절 : 발생한 예외에 대해 예외 핸들러가 처리할 내용을 담은 코드 블록

 

try 문으로 예외를 처리하기 위해서는 try 문 다음에 반드시 하나 이상의 catch 절을 구현해야 합니다.

또한, 각 catch 절은 자신이 처리할 수 있는 예외 타입을 지정할 수 있습니다.

이때 특정 예외 타입 대신에 줄임표(...)를 사용하면, 해당 catch 절은 모든 타입의 예외를 처리하게 됩니다.

하지만 이러한 줄임표를 사용한 catch 절의 위치는 언제나 catch 절 중 맨 마지막에 위치해야 합니다.

 

다음 예제는 사용자가 정확히 양의 정수를 입력했는가를 throw 문으로 검사하는 예제입니다.

#include <iostream>

using namespace std;

int IncreaseNumber(int n) {
    if (n < 0)
        throw n;
    else if (n == 0)
        throw "0은 입력할 수 없습니다.";

    return ++n;
}

int main(void) {

    int num;
    cout << "양의 정수를 하나 입력해주세요 : ";
    
    while (cin >> num) {
        try {
            cout << "입력한 정수에서 1을 증가시킨 값 : " << IncreaseNumber(num) << endl;
        }
        catch (int input) {

            cout << input << "은 양의 정수가 아닙니다." << endl;
            cout << "양의 정수를 다시 입력해주세요 : ";
            continue;
        }

        catch(const char* st) {

            cout << st << endl;
            cout << "양의 정수를 다시 입력해주세요 : ";
            continue;
        }
        break;
    }
    return 0;
}

 

위의 예제처럼 throw 문의 피연산자는 변수와 문자열을 포함한 모든 타입의 예외를 사용할 수 있습니다.

하지만 C++ 표준에서는 throw 문의 피연산자로 std::exception에서 파생되는 예외 타입을 사용하도록 권장하고 있습니다.


  • 예외 메커니즘

C++에서 예외 처리는 다음과 같은 순서로 진행됩니다.

 

  1. try 문에 도달한 프로그램의 제어는 try 문 내의 코드를 실행합니다.

 

  2. 이때 예외가 발생(throw)하지 않으면 프로그램의 제어는 맨 마지막 catch 절 바로 다음으로 이동합니다.

 

  3. 만약 예외가 발생하면 catch 핸들러는 다음과 같은 순서로 적절한 catch 절을 찾게 됩니다.

     3-1. 스택에서 try 문과 가장 가까운 catch 절부터 차례대로 검사합니다.

     3-2. 만약 적절한 catch 절을 찾지 못하면, 바로 다음 바깥쪽 try 문 다음에 위치한 catch 절을 차례대로 검사합니다.

     3-3. 이러한 과정을 가장 바깥쪽 try 문까지 계속 검사하게 됩니다.

     3-4. 그래도 적절한 catch 절을 찾지 못하면, 미리 정의된 terminate() 함수가 호출됩니다.

 

  4. 만약 적절한 catch 절을 찾게 되면, throw 문의 피연산자는 예외 객체의 형식 매개변수로 전달됩니다.

 

  5. 모든 예외 처리가 끝나면 프로그램의 제어는 맨 마지막 catch 절 바로 다음으로 이동합니다.


  • 스택 풀기(stack unwinding)

- 스택 풀기란 예외를 처리하는 영역을 찾지 못해서 해당 예외가 호출된 영역의 상위 함수로 예외가 계속해서 전달되는 현상을 가리킵니다.

void Func03() { throw 0;}
void Func02() { Func03(); }
void Func01() { Func02(); }

int main(void) {
    try {
    	Func01();
    }
    catch (int ex) {
        cout << "예외 처리(main) : " << ex << endl;
    }
    return 0;
}

 

- 위의 예제에서는 연속된 함수 호출을 통해 호출된 Func03() 함수에서 예외가 발생합니다.

- 하지만 Func03() 함수에는 예외를 처리할 catch 절이 없으므로, 프로그램은 Func02() 함수로 예외를 전달합니다.

 

- 하지만 Func02() 함수에서도 예외를 처리할 수 없으므로, 또다시 Func01() 함수로 예외를 전달합니다.

- Func01() 함수에도 예외를 처리할 catch 절이 없기 때문에 마지막으로 Func01() 함수를 호출한 main() 함수로 전달됩니다.

- 따라서 예외의 실제 처리는 main() 함수 내의 catch 절에서 수행되게 됩니다.