[C언어] #8 가변인자함수, 올바른 오류처리 전략

Ilhoon·2022년 2월 19일
1
post-thumbnail

가변인자 함수

  • 정해지지 않은 수의 매개변수(가변 인자)를 허용하는 함수

  • 최소한 한개의 정해진 자료형의 매개변수가 필요하다.

  • ex. printf() scanf() 같은 것

int add_ints(cont size_t count, ...){
    va_list ap;			// 가변 인자 목록, va_start(), va_arg(), va_end() 매크로 함수를 사용할 때 필요한 정보 포함
    int sum;
    size_t i;
    
    sum = 0;
    va_start(ap, count);
    {
        for (i = 0; i<count; i++){
            sum += va_arg(ap, int);
        }
    }
    va_end(ap);
    return sum;
}
  • va_start()

    • 매크로 함수
    • 함수 매개변수로 들어온 가변인자들에 접근하기 전에 반드시 호출해야함
    • 가변인자가 스택 메모리의 어디서부터 시작하는지 찾아냄
  • va_end()

    • 매크로 함수
    • 함수 매개변수로 들어온 가변인자들에 접근이 끝난 뒤 반드시 호출해야 함.
    • 사용 했던 가변인자 항목을 정리함
  • va_arg()

    • 매크로 함수
    • 가변 인자 목록으로부터 다음 가변인자를 가져옴
    • 가져올 가변인자의 자료형은 두번째 매개변수로 알려줌
      • 모든 정수형은 int 모든 실수형은 double
      • printf()함수에서 모든 실수형이 %f서식으로 출력가능했던 이유와도 같다 (어차피 double형으로 들어오니까)

가변인자 함수가 데이터를 읽어오는 방식

  • 가변인자 함수도 호출시 매개변수를 입력하기 때문에 그 만큼 스택프레임에 넣어주면된다 (일반 함수와 동일)

  • BUT, 가변인자 함수 내에서 매개변수를 읽어올때 몇 개가 들어올지 알 수 없기 때문에 이 부분이 문제

    • 첫 번째 매개변수의 위치와 자료형을 알기 때문에 데이터 접근 가능!

    • va_start(ap, count) : 가변인자 시작전 마지막 데이터의 메모리 주소 계산

      • va_start(ap, count)						// 코드
        ap.data = (char*)&count + sizeof(count)	// 매크로 내부 구현 예상 (전처리기가 변환할 코드)
    • va_arg(ap, int) : int 크기만큼 더해가면서 읽을 위치를 변경

      • val = *(int*)ap.data++;

매크로함수?

  • 스택프레임을 만들지도 않고
  • 매개변수도 없고
  • 함수주소로 점프하지도 않고
  • 전처리기가 매크로함수의 구현코드로 대체

오류 처리

C에는 예외처리가 없지만 가장 안정적인 프로그램들을 만든 언어 (운영체제)

예외처리가 반드시 소프트웨어 품질을 높이는 것은 아니다.

  • 예외처리가 없는 언어에서 문제가 발생하면?

    • 프로그램 크래시 (뻗어버림...)
    • 그냥 넘어갈 수가 없다.
  • 예외처리가 있는 언어라면 그냥 넘어갈 수 있다

예외처리의 유무보다 오류 상황을 대비하고 처리하는 전략이 중요하다.


안좋은 예

스왑함수를 호출하는데 오류가 났다면?

  • // 원본
    void swap (int* a, int* b){
    	int tmp;
        
        tmp = *a;
        *a = *b;
        *b = tmp;
    }

주먹구구식 오류처리

  • // 주먹구구식 오류처리
    void swap (int* a, int* b){
    	int tmp;
        
        if(a==NULL || b==NULL){
            return;
        }
        /* */
        tmp = *a;
        *a = *b;
        *b = tmp;
    }
  • 이게 왜 문제인가

    • 함수는 호출되었는데 NULL이 들어와서 그냥 리턴한건지 제대로 실행된건지 사용자는 확인하기가 어렵다.

    • 이런 식의 함수가 여러개라면? 문제가 생겼을 때 일일히 다 확인해봐야되는 문제

    • 이 함수에 NULL이 들어와도 되는가를 먼저 검증해야하지 않을까?

      • 진짜 NULL포인터가 허용되는 경우라면 함수나 변수이름에 명시하는 것도 좋다.

      • 이렇게 하면 함수를 유지보수하거나 사용하는 사람도 오류확률을 줄일 수 있다.

        • moster_t* spawn_monster_or_null(const monster_t* special_monster_or_null)

문제는 한 군데서만 찾는게 더 효율적 (집중의 문제)

  • 문제를 빨리 발견하고 빨리 찾을 수 있다
  • 데이터가 외부에서 들어오는 경우라면 그 경계부분에서만 철통보안.
    • 경계부분의 함수는 오류코드를 반환하자

#### 버그와 오류

버그

  • 아예 발생해서는 안되는 상황
  • 선조건과 후조건을 만족하는지 (assert)

오류

  • 실행 중 일어날 수 있는 예측가능한 상황
  • 오류 처리는 버그 상황을 모두 잡았다는 가정 하고 처리하는 것
    • 내 함수에 들어오는 데이터는 모두 유효한 데이터
      • 그런데 외부 입력, 외부 파일 등에서 유효하지 않은 데이터가 들어올 수도 있잖아?
      • 경계부분에서 걸러줘야한다. (ex. 데이터 유효 검증함수 실행 후 프로그램 실행)
  • 프로그램이 대처해서 처리해야한다.

assert

주어진 조건을 만족하는지 확인하는 함수 (버그 상황 확인)

만족하지 않으면 코어 덤프와 함께 오류메시지 출력하고 프로그램 종료

  • 코어덤프 : 실행중인 프로그램에 대한 정보를 core라는 이름으로 파일로 저장하는 것.

사용자에게 제공되는 프로그램의 실제 코드 내부에서는 제거해줘야한다.

unsigned int deposit (unsigned int deposit_amount){
    unsigned int before_total;
    unsigned int after_total;
    
    assert(deposit_amount > 0);	// 예금 금액이 0인 상황은 존재할 필요가 없다.
    
    /* 코드 생략 */
    
    assert(before_total < after_total);
    return after_total;
}

올바른 오류처리 전략 정리

  1. 기본적으로 내가 작성하는 모든 함수에 들어오는 데이터는 유효하다 가정하고 어서트를 많이 쓸 것
  2. 그렇지 않은 함수는 매개변수나 함수 이름에서 그렇지 않다는 사실을 명백히 표시할 것
  3. 오류 상황을 정리하는 부분은 최소한으로 할 것
  4. 어떤 함수가 오류 처리를 한다는 사실을 반환형을 통해 확실히 보여줄 것
profile
꾸준하게!

0개의 댓글