[C] C lang 가변 인자 함수
가변 인자 함수
printf() 같은 함수는 어떻게 만드나.
printf() 또는 scanf()에 대해 의구심
얘들은 왜 출력/입력할 데이터를 자료형 한개나 그 이상을 넣을 수 있는건가?
심지어는 그 자료형이 달라도 됨.
가변인자 함수
- 정해지지 않은 수의 매개변수(가변인자)를 허용하는 함수
- 2개 넣어도 된다. 그 이상도 가능
- 반드시 최소 한개의 정해진 자료형의 매개변수가 필요
- 가변인자는 …로 표현
가변 인자 함수는 언제 쓰나
- 아주 많이 사용되진 않지만 가끔은 쓴다.
- 반환명, 함수명 까지는 동일
- 첫번째 매개변수는 일반적인 매개변수를 넣어야 한다.
- 그 다음 어떤 데이터의 형인 매개변수라도 받을 수 있는 가변변자라고 해준다.
- va_list는 가변인자 목록이라 그러고 얘를 사용하려면 stdarg.h를 인클루드 해줘야 한다.
- 이 매개변수를 va_start에 넣어주고 변수 ap와 매개변수 count를 va_start의 매개변수로 넣어준다.
처음 보는 건 이 4개다.
va_로 시작하는 매크로 함수들
va_list
- 가변 인자 목록
- va_start(), va_arg(), va_end() 매크로 함수를 사용 할 떄 필요한 정보가 포함
- 명시되지 않은 자료형(구현마다 다름)
va_list ap
va_start()
- 매크로 함수
함수 매개변수로 들어온 가변 인자들에 접근 하기 전에 반드시 호출해야 한다.
- va_list에 필요한 초기화를 수행
- 특히 가변 인자가 스택 메모리의 어디서 부터 실행하는 지 찾아냄
- 그래서 두번째 매개 변수가 필요
va_end()
- 매크로 함수
- 함수 매개변수로 들어온 가변인자들에 대한 접근이 끝나고 반드시 호출해야 함
- 사용한 가변인자 목록을정리함
- 더 이상 가변 인자 목록을 사용하 수 없도록 가변 인자 목록의 값을 수정함
va_arg()
- 매크로 함수
- 가변 인자 목록으로 부터 다음 가변인자를 가져옴
- 가져올 가변 인자의 자료형은 두번쨰 매개변수로 알려줌
예전 표준상의 문제로 가변 인자의 목록의 기본 자료형 인자들은 다음과 같이 승격(promotion) 됨
- 모든 정수형은 int로
- 모든 부동 소수점은 double로
- 따라서 매개변수에는 int나 double을 쓸 것
구조체를 넣어줄 수도 있고 실제 va_arg에서 이런식으로 불러오는 법도 있다. 그리고 구조체도 다른 변수형이랑 똑같이 돈다
va_args()는 어떻게 인자목록에서 알아서 데이터를 읽어주나?
- C에서 실행중에 자동으로 자료형을 판단하는 기능이 없다
따라서 컴파일러가 이와 비슷한 코드를 컴파일러 하게 해줘야 한다.
데이터 블록을 일단 가져온다. 스택메모리 어딘가를 가리킨다고 가정. 그럼 거기서 int 하나 읽어오고 얘는 void형이므로 int로 캐스팅 한 뒤 그 값을 읽어옴 그럼 이제 실제 값임. 컴파일러가 읽을 수 있어야 됨.
- 컴파일러가 컴파일 하게 미리 코드를 만든다.
- 전처리기
va_arg()는 매크로 함수
- 함수처럼 보이나 엄밀한 의미의 함수는 아니다.
- 스택 프레임 만들지도,
- 매개변수 전달하지도
- 함수 주소로 점프하지도 않음
- 그 대신 전처리기가 매크로 함수의 구현코드로 대체시켜줌
가변 인자 함수가 인자를 읽어오는 방법
어떻게 다른 수의 매개변수를 스택에 넣나
- 어떤 함수 호출 할 떄매다 순서대로 스택에 넣어줌.
몇개의 매개변수가 오는지 모르지만 호출자는 매개변수 몇개를 스택에 넣어야 하는지 분명히 안다.그래서 크게 문제가 아님. 근데 호출을 받는 애 , 가변인자 함수 내부에서 나한테 몇개가 들어온 지 모르기 때문에 가져다 쓰는게 문제일 뿐
- 정확히 형이 정해진 매개변수. 가변인자가 아닌 매개변수. 이건 어디 있는지 안다. 스택 프레임 전에 있는 첫번째 매개변수 있는데서 가져오면 된다. 이런식
가변 인자 함수가 인자를 읽어오는 법
va_start하고 마지막 인자 가변인자가 아닌거 인자. 이걸 다르게 표현하면 가변인자가 시작하기 직전에 있는 매개변수. 이렇게 두번쨰 넣어주면 가변인자가 아닌 거에 제일 마지막 인자가 어느 위치에 있는지 안다. 그리고 데이터 형도 안다.
그러나 그거 다음이 가변인자 시작이구나 하고 메모리 주소를 계산해 줄수 있다.
그래서 실행하면 이게 다음에 가변인자로 이제 시작해야할 메모리 주소는 이 count가 들어왔다. 그럼 count의 주소에서 이 count의 길이만큼 바이트 수를 옮기면 되겠구나.
이 count의 길이는 뭐지. sizeof(int) 왜냐면 int형이 들어온 걸 알기 떄문
실제 코드에선 sizeof(count)를 쓸 일이 많다. 그래서 실제 코드는 sizeof(int)대신 sizeof(count) 를 쓸 일이 더 많다.
이 주소를 char 포인터로 변환한 이유는(캐스팅 한 이유는) 그냥 sizeof(Int)를 더해버리면 count는 int였으니까 int 4개 들어갈 만큼 옮김.
그럼 바이트가 궁금한 것이므로 바이트로 이동하기 위해 이렇게 했다. 이제 ap.data가 이 주소를 가리키게 된다. 이걸 하는 순간 va_list형의 ap라는 변수는 가변인자가 시작하는 메모리 주소, 스택상의 메모리 주소에 주소를 가리킨다. 그 메모리 주소를 저장한다.
2 . 그럼 이제 매개변수를 읽는다. 가변인자 매개변수들
ap는 매개변수 가짐. 가변인자 매개변수가 첫번쨰 매개변수가 시작되는 그 위치 기억. 그럼 이걸 호출 시마다 한번에 하나씩 int 크기만큼 더해가면 된다. 그리고 읽을 위치를 변경
va_arg로 읽어온 걸 val에 대입한다.
함수에서 매개변수로 가변 인자만 받을 수 있을까?
함수의 첫 번째 매개변수로 가변인자를 못 쓴다.
가변 인자(…) 앞에 자료형이 특정된 매개변수가 반드시 있어야 한다.
- 가변 인자 뒤에 자료형이 정해진 매개변수가 있으면 안된다.
- 함수가 정확히 어떤 오프에서 읽어와야 하는지 컴파일 중에 특정 불가
- 즉, 가변인자 아닌 것을 순서대로 읽음.
그 뒤, 가변인자는 va_arg()가 시키는 대로 하나씩 주소를 늘려가며 읽는 것.
가변인자 시작하기 전 바로 전 마지막 인자. 그걸 넣어 줘야지만 가변인자가 어디서 시작할 지 알 수 있다
똑같이 가변인자가 중간네 나오고 또 인자 받는다? 그럼 가변인자가 어디 들어갈 지 모르는 데 어떻게 스택에 들어가는지 아는가.
그래서 가변인자 아닌 거 다 먼저 인자로 받고 그 뒤에 가변인자가 올 수 밖에 없다.
- 그 이유는 va_start같은 게 메모리 위치를 특정해야 되기 때문이다.
이유?
1 . 가변인자가 몇 개인지 가변 인자 함수는 모른다
- 실제 가변인자가 몇개 들어온 지 호출된 함수는 모른다
- 그래서 앞의 예에선 매개변수 count로 제어
단, 스택 메모리 어느 위치부터 가변인자가 시작 되는지는 안다.
2. 가변 인자 자료형을 가진 인자 함수는 모름
- 어떤 형의 가변 인자인지는 실행 중 결정
- 그래서 가변 인자 함수 자체는 스택의 어떤형으로 읽어야 할지 모름
- 따라서 , 정해진 자료형으로 넘겨주는 매개변수로부터 알아야 함
데이터를 잘못 읽으면?
오류처리
C에서의 오류처리
C 언어는 예외(exception)를 지원하지 않는다.
그럼 어떻게 예외처리?
예외처리가 언제나 좋은 것은 아니다.
- 예외처리는 인간이 완벽히 하게엔 거의 불가능
- 그리고 예외처리 기능이 존재하는 언어는 오히려 프로그래머를 게으르게 하기도 한다.
일반적인 사람들의 사고방식
- 여러 단계 일을 설계시 최상의 경우(happy path)만 고려
- 내가 작성하는 코드 한줄 한줄이 잘못 될수 있다는 생각 안하고 기능 우선 쭉쭉 작성함
- 그런다고 크래시 나지 않으므로 모른척 하는 경우도 대다수
- 그래서 실제로는 버그가 많으나 어떻게든 동작하는 프로그램이 나오기도 함.
크래시가 나면 다른 방법이 없다
- 예외 처리가 없는 언어에서 문제가 발생하면?
- 제대로 대처 안하면 크래시가 남
- 즉, 프로그램이 뻗고 고객이 항의
- 덕분에 오히려 예외 상황이 발생하면 빠르게 대처한다.
- 프로그램이 돌다 뻗어서 아무것도 못하니 제대로 고칠 수 밖에
- 참고로 말하면 운영체제도 대다수가 C기반이다.
안 좋은 오류 처리의 예
이런 코딩 스타일은 안 좋다
일단 아무 생각 없이 코드가 무조건 돈다고 생각하고 코드작성
그 뒤 특별한 원칙없이 버그가 나올 때 마다 떔빵으로 고치면 어느 순간 이해하기 힘들어진다.
- 예: 매개변수가 널 포인터인지 확인시
대다수가 그냥 널이면 반환함 세상 모든 함수에 널 체크하는 경우가 있다.
근데 널포인터 아니면 널포인터 크래시 안나고 리턴하네.
근데 크래시 안나서 리턴했는데 나중 보면 swap이 안된 경우가 있다. 이 경우는 널포인터 들어갔기 떄문
어떤 물물교환 사이트에서 돈, 물건 스왑하는데 어떤 실수든 간에 널로 들어오면 함수 호출됨. 근데 내부적으로 문제 없으니까 함수는 도는데 스왑이 안된다.
- 스왑이 안되는데 왜 ?
이 함수에 NULL이 들어와도 되는가?
포인터는 널 포인터 받을 수 있지만 내 의도가 아닌 것에도 크래시가 안나도록 저런 널포인터 검사해서 반환하는게 맞나?
- 원칙상 맞는얘기
예를 들어 두꺼비집은 전기 끊게 만든다.(퓨즈 아예 타버리거나 개폐기 내려감)
근데 만약 전선 하나당 차단기 한개면? 차단기 내려간 거 자체도 헷갈린다. 그런 복잡한 가정 안함.
이론상은 훌륭한 개념이지만 인간이 구현하기엔 많이 어려운 부분
프로그래밍에서는
- 오류 처리 할 떄도 원칙이 있어야 한다
- 다른 언어에서 예외 처리 할 떄도 마찬가지
- 생각 없이 무조건 작동한다고 코드짜는 건 일단은 ok
- 인간의 한계..
- 전자레인지 등 전기 엄청 먹는 걸 한 콘센트에 3,4개 이상 연결하듯이..
- 근데 이 문제를 찾는 곳은 최소한인게 좋다.
- 이 원칙에 따라 올바른 오류처리를 논해야한다.
우선, 버그와 오류는 다르다.
버그는 정상적인 프로그램 작동 방식. 예외적인 상황에서도 존재하면 안되는 것이 버그다.
무조건 함수를 호출 한 뒤, 반드시 보장되어야 할 조건들.
그게 틀리면 아예 버그다. 그건 프로그램에서 더 코드를 작성해서 이런 상황이 발생 시 프로그램이 올바르게 돌게 만들어야지 이렇게 하는게 아니다.
그건 이미 프로그램 자체가 돌 수 있는 최대 상황. 최악의 상황에서 그걸 넘어가는 상황이다.
프로그램 자체의 문제. 내 코드를 아예 잘못 짠 것. 그건 코드를 고쳐서 그런 문제가 안 발생하게끔 만들어서 다시 코드를 만들어야 하는 것.
오류는 실행 도중에 충분히 발생할 수 있는 상황
그건 프로그램이 대처해줘야 하는것. 실행하다 특정 상황이 나면 어떻게 처리해서 프로그램이 계속 진행 될 수 있는 상황. 이 2가지를 구분 해야 한다.
버그에 assert 넣음. 어서트는 딱 도는 순간 실행 안되고 멈춤. 디버그떄는 프로그램 실행 멈추는데 , 릴리즈떄는 배포용 버전은 프로그램 실행하다 어서트 만족하지 않으면 어디로 갈지 모른다. 그러면 그떄 찾아서 코드를 수정해서 크래시가 안나게 고치겠다는 뜻.
선 조건과 후 조건
- 함수 이름이나 변수 이름으로 유추 가능해야함
- 불가능하다면 주석으로 설명
- 두 조건이 참인지 검사하는 어서트를 충분히 넣을 것.
어서트의 문제는 실행 해야만 보인 다는 점.
- C89에서는 컴파일 중 판단 가능한 것도 모두 실행해야 보인다.
- 예: 구조체의 크기. 이런 건 컴파일 도중 결정된다.
- C11은 정적 어서트로 (static assert) 이러한 한게를 극복
- 컴파일 중 어서트 조건을 판단해서 컴파일 오류를 던져줌
이 함수는 0원을 넘는 금액만 들어온다 만약 들어오면 어서트로 오류냄.
후조건은 그전 잔액보다 지금 잔액이 높다.
이런식으로 어서트 넣고 확인 이런 방법들도 있다.
숨겨진 버그는 무수히 많다.
실행중 오류는 어떻게 처리하나
- 버그는 잡았다 가정
가정했다는 건 함수에 들어오는 데이터는 유효
- 유효하지 않은 데이터가 들어오면 어디선가 걸러줘야 함
그 어딘가의 ‘경계’라고 부름
- 오류처리 경계 : 내가 컨트롤 할 수 있는 부분. 그래서 여기는 반드시 유효한 영역. 모든 데이터가 유효한 곳.
- 그 바깥은 잘 모르는 데이터
내 프로그램에 내 배열 사이즈 이건 내가 정확히 안다. 내 프로그램 안의 데이터 유효.
- 근데 만약 내가 파일을 읽어온다고 가정하면 파일에 뭐가 저장 되어있는 지 모른다. 다른 프로그램에서 열어서 파일 바꿀 수도 있는데, 그럼 외부 데이터다. 그럼 파일 시스템과 내 프로그램 사이에 있는 경계.
이런식으로 경계확인만 제대로 되면 프로그램에 들어오는 데이터들은 다 유효한 것. 그 순간에 널 포인터도 있으면 안되는 거고, swap함수 같은 경우 실제 널 포인터 쓰는 거 있을 텐데 이런거 제외하고 선조건 후조건에 적합한 지 확인
널 포인터를 허용한다면 함수나 변수에 명시
- 코딩 표준
- 함수의 매개변수가 널 포인터를 허용한다면, 매개변수 이름 끝에 ‘_or_null’을 붙인다.
- 함수도 마찬가지
오류코드를 반환하자
- 두번쨰 방법
- 오류를 처리해주는 함수/코드 에서 오류가 있음을 알려줘야함
- 가장 좋은 방법은 함수에서 곧바로 오류 코드를 반환하는 것.(현 실무에서 쓰는 방법)
모든 오류 코드를 하나의 enum으로 만들자.
- 구조체로 반환도 가능하다 C에서 많이 쓰는 방법은 아니다.
- 아마 용량 떄문
- 그래도 좋은 방법
- 오류 코드 만들 떄는 해당 라이브러리에서 제공 할 수 있는 모든 오류 코드를 enum으로 정의 하는 게 좋다.
함수마다 enum 만드는 건 좋지 않다.
- C#의 enum과 다르게 C의 enum은 서로 비교 및 대입이 가능
- c의 enum은 그냥 정수에 별명 붙인것
- 두 함수에서 서로 다른 오류인데 오류 코드의 값이 겹치면
- enum 끼리 서로 대입되서 실수도 가능
- 비교 시 다른 enum과 비교도 가능
이전에 사용한 errno도 별로다
전에 본 어떤 함수가 내부적으로 errno에 저장하는 방법도 있다.
이건 아주 훌륭한 방법은 아님
오류가 발생했다는 걸 그걸로 찾으려면 문제가 있다.
올바른 오류 처리 전략 정리
1 . 기본적으로 내가 작성하는 모든 함수에 들어오는 데이터는 유효하다 가정하고 어서트를 많이 쓸 것
2 . 그렇지 않은 함수는 매개변수나 함수 이름에서 그렇지 않다는 사실을 명백히 표시 할것
3 . 오류 상황을 처리하는 장소(경계부분)는 최소한으로 할 것. 이 부분만 집중해서 보게 해야 다른 사람들도 이 분을 집중해서 본다.
4 . 어떤 함수가 오류 처리를 한다는 사실을 반환형 등을 통해 확실히 보여줄 것(반환형 같은 데서 오류코드 보여주는게 좋다. 이미 오류 반환하는 걸 알기 때문). 다른 언어에 예외 던지는 거 대부분 어떤 함수 호출해놓고 예외처리 안하는 거와 비교됨. 근데 오류코드 원래 반환하는 거 알면 그 고민은 안함. 이미 오류코드 반환하는 거 알기 떄문
- 사람이 이해할수 있고 사람의 생각 방식에 맞는 오류처리는 이 4가지로 요약 가능
오류 처리 후에도 발생하는 예외 상황
만약 위 방법대로 해도 프로그램 오작동하면?
- 버그다.
- 프로그램을 다시 고쳐야 함
- 소프트웨어의 품질은 테스트/QA 프로세스의 문제
- 어떤 경우에는 크래시도 남.
- 널 포인터 역참조 등
- 테스트 프로세스가 좋고 QA프로세스가 좋으면 품질이 좋아짐.
- 여기에 집중해야 됨.
이런 상황을 캐치해서 또 대비하고 싶다면?
- C표준에서는 방법이 없다.
- try/catch 문 같은게 없다.
- 운영체제에서 이런 문제를 찾아서 SEH나 시그널을 주기는 한다.
운영체제의 예외 처리
- 함수 포인터를 등록하고 OS가 보내는 예외처리를 받아올 수는 있다.
- 근데 받아와도 어떻게 대처할지 애매한 경우들이 종종 있음
- 동적 메모리 할당에서 더 이상 메모리를 못 받아오면 NULL을 반환
- 해결하려면..?
- 반드시 있어야 하는 파일 열다가 실패하는 경우엔 어떻게 해결해야..?
- 동적 메모리 할당에서 더 이상 메모리를 못 받아오면 NULL을 반환
알아야 할 점
위 두개는 당장 어디서 쓸모 있다기 보다는 알아두면 좋고 컴퓨터 내부적으로 알기 도움이 됨.
1 . 가변인자 함수
함수 호출방법, 스택 메모리에 들어가는 이 방법을 제대로 쓸 수 없는 호환이 안되는 이상한 방법.
근데 속을 내려다 보고 그 매크로 함수 쓰는 모양을 보다보니 이런식으로 작동할수밖에 없는 걸 깨닫게 된다.
실제 작동하는 방법 보면 호출자는 스택에 마구잡이로 푸시한다.
실제 함수 안에서 매개변수 들어온 거, 가변 인자 전에 들어온 마지막 매개변수의 위치를 이용해서 가변인자 매개변수가 어디서 시작하는지 찾아내서 읽어옴.
그 데이터형을 알 수 있는 특정한 방법들을 고안했다.
- 하나는 함수 호출시 정수 들어온다 가정
- 두번쨰는 서식 지정자. 이걸 통해 매개변수가 몇개인지 무슨 자료형인지 정한다.
2 . 올바른 오류처리방법
항상 예외처리가 소프트웨어 품질을 높인다는 보장은 아니다.
그것보다는 올바른 테스트 프로세스가 있어야 하고 사람을 이해해서 프로그래머가 실수를 잘 잡고 고칠수 있게, 그리고 문제가 있으면 그걸 잡을수 있는 부분이 최소화 되게. 그런 원칙을 가지고 프로그램을 작성해야 한다.
이런건 실제로 업무를 많이 해봐야 알수 있는 점. 이론이나 학교상에서는 안 가르침. 예외도 엄청 많이 써보고, 예외 아닌 것도 엄청 써봐야 한다.
100명 사람들 다 실수하는 거 본 다음에 어느정도 감이 오는 것.
그 원칙은 내가 작성하는 코드는 일단은 다 올바른 데이터가 들어온다가 원칙이였다.
그 올바른 데이터가 들어오지 않는 상황들은 경계에서 다 처리해줘야 한다.
사람이 주석이나 문서 안 읽을 수 있으므로 함수에 명시적으로 적어주는게 좋을수 있다.