2010년 1월 30일 토요일
C로 개발을 할땐...
2010년 1월 29일 금요일
가변인수 : stdarg.h
2. 함수 정의에서 va_list형 변수를 만든다.
3. 인수 리스트에 대한 변수를 초기화하기 위해서 매크로를 사용한다.
4. 인수 리스트에 접근하는데 매크로를 이용한다.
5. 지우기 위해 매크로를 이용한다.
int f2(int n, const char *s, ...); //유효
char f3(char 1, ..., char c2); //무효
double f3(); // 무효
첫번째 선언의 맨오른쪽 인수인 "..."은 표준 명칭으로 parmN 이라고 한다.
parmN은 첫째는 n이 되고, 두번째는 s가 된다. 다음과 같이 사용될 수 있다.
f(4, 13, 117, 18, 23); // 4개의 추가 인수
인수의 맨앞 숫자는 뒤 따라올 인수들의 수를 나타낸다.
{
va_list ap; // 생략된 인수를 담을 객체를 선언한다.
...
}
stdargs.h에 선언된 va_start()매크로를 사용해서 선언된 va_list변수에 인수 리스트를 복사한다.
다음으로 넘겨준 인수를 가져와야 할 것이다. 그래야 지지고 볶고 할것 아닌가..
va_arg()를 사용하여 데이터에 엑세스 한다.
double tic;
tic = va_arg(ap, double);
toc = va_arg(ap, int)
인수의 형식은 va_arg()의 두번째 인수인 규격에 정확히 맞아야 한다.
마지막으로 리스트 선언된 객체를 지운다
인수를 해제하기전 데이터를 백업 받아야 한다면 다음 매크로를 사용하면된다.
va_list ap;
va_list apcopy;
double
double tic;
int toc;
...
va_start(ap, lim); // 인수 리스트로 ap를 초기화한다.
va_copy(apcopy, ap); // ap를 ap copy에 복사한다.
tic = va_arg(a, double); // 첫 번째 인수를 검색한다.
toc = va_arg(ap, int); // 두 번째 인수를 검색한다.
2 #include <stdarg.h>
3 double sum(int, ...);
4
5 int main(void)
6 {
7 double s, t;
8 s = sum(3,1.1,2.2,3.3);
9 t = sum(6,1.1,2.1,3.1,4.1,5.1,6.1);
10 printf("%g %g\n", s, t);
11 return 0;
12 }
13
14 double sum(int lim, ...)
15 {
16 va_list ap;
17 double tot = 0;
18 int i;
19 va_start(ap, lim);
20 for(i=0;i<lim;i++)
21 tot += va_arg(ap, double);
22 va_end(ap);
23 return tot;
24 }
va_arg는 별도의 카운터를 하지 않아도 다음 값을 가져오는 걸로 확인되었다.
memcpy(), memmove()
void * memmove(void *s1, const oid *s2, size_t n);
두 함수 모두 s2에 의해 포인트된 위치에서 s1에 포인트된 위치로 n바이트를 복사하여 s1값으로 봔환한다. 키워드 restrict(코드 최적화)로 나타나듯이, 이 두 함수의 다른 점은 memcpy()는 두개의 메모리 범위간의 중복이 없다고 가정할 수 있다는 것이다. memmove()함수는 그런 전제를 하지 않으므로 모든 바이트가 마지막 수신지에 복사되기 전에 임시 버퍼에 먼저 복사되는 것처럼 복사가 이루어진다. 만약 중복되는 범위가 있을때 memcpy()함수를 사용한다면 어떻게 될 것인가? 이 행위는 정의 되지 않는다. 즉, 실핼할 수도 있고 안 할 수도 있다는 뜻이다. 컴파일러는 memcpy()함수를 사용하지 말아야 할 때 사용하게 되면 그것을 멈출 수 없다. 따라서, 사용시 범위가 중복되지 않도록 확인하는 것은 사용자의 책임이다. 이것이 바로 프로그래머에게는 또하나의 부담이 될것이다.
이러한 함수들은 모든 데이터형과 작동하도록 설계되어 있기 때문에, 두개의 포인터 인수는 void형 포이터이다. C는 void *의 포인터에 어떤 포인터 형식도 할당할 수 있도록 해준다. 그러나 이 폭넓은 수용은 이러한 함수들이 어떤 종류의 데이터가 복사되는지 알지 못한다는 단점이 있다. 그러므로 이런 함수들은 복사될 바이트 수를 나타내기 위해 세 번째 인수를 사용한다. 일반적으로 배열의 경우, 바이트 수는 요소의 수가 아니라는 것을 주의한다. 따라서 10 double값의 배열을 복사했다면 세 번째 인수로 10이 아닌 10*sizeof(double)를 사용해야 할 것이다.
2 #include <string.h>
3 #include <stdlib.h>
4 #define SIZE 10
5
6 void show_array(const int ar[], int n);
7
8 int main(void)
9 {
10 int values[SIZE] = {1,2,3,4,5,6,7,8,9,10};
11 int target[SIZE];
12 double curious[SIZE / 2] = {1.0, 2.0, 3.0, 4.0, 5.0};
13 int * pi;
14
15 memcpy(target, values, SIZE * sizeof(int));
16 puts("target:");
17 show_array(target, SIZE);
18 pi = (int *)malloc(SIZE * sizeof(int));
19 memcpy(pi, values, SIZE * sizeof(int));
20 puts("pi:");
21 show_array(pi, SIZE);
22 memmove(values + 2, values, 5);
23 puts("values");
24 show_array(values, SIZE);
25 memcpy(target,curious, (SIZE /2) * sizeof(double));
26 puts("target:");
27 show_array(target, SIZE);
28
29 return 0;
30 }
31
32 void show_array(const int ar[], int n)
33 {
34 int i;
35 for(i = 0; i < n; i++)
36 printf("%d ", ar[i]);
37 putchar('\n');
38 }
target:
1 2 3 4 5 6 7 8 9 10
pi:
1 2 3 4 5 6 7 8 9 10
values
1 2 1 2 5 6 7 8 9 10
target:
0 1072693248 0 1073741824 0 1074266112 0 1074790400 0 1075052544
memcpy()에 대한 마지막 호출은 double형에서 int형 배열까지의 데이터를 복사한다. 이것은 memcpy()가 데이터형을 알지 모사며 그것에 대해 상관하지도 않는다는 것을 본여준다. 즉, 하나의 위치에서 다른 위치로 바이트를 복사할 뿐이다(예를들어, 구조체에서 문자 배열로 바이트를 복사할 수 있다.) 데이터 변환 또한 없다. 요소마다 지정하는 루프를 가지면 할당하는 동안 double형 값은 int형으로 변환될지도 모른다. 이경우에 프로그램은 바이트를 있는 그대로를 복사한 다음 그것이 int형인 것처럼 비트 패턴을 번역한다.
디버깅에는 assert()!!!
다음 예제를 보자
#include <math.h>
#include <assert.h>
int main()
{
double x, y, z;
puts("Enter a pair of numbers (0 0 to quit): ");
while(scanf("%lf%lf", &x, &y) == 2 && (x != 0 || y != 0))
{
z = x * x - y * y;
assert(z >= 0);
printf("Answer is %f\n", sqrt(z));
puts("Next pair of numbers: ");
}
return 0;
}
결과
Enter a pair of numbers (0 0 to quit):
4 3
Answer is 2.645751
Next pair of numbers:
5 3
Answer is 4.000000
Next pair of numbers:
3 5
assert: assert.c:12: main: Assertion `z >= 0' failed.
일반적으로 인수는 관계식이거나 논리식이다. assert()가 프로그램을 강제로 중지시키면, 먼저 실패한 테스트를 출력하고, 다음으로 테스트를 포함하는 파일명, 라인 번호를 표시나다. 관계식이 false가되면 해당 코드와 경로 및 파일명, 그리고 라인번호를 같이출력한다.
더 멋진 장점은 선언부분에 아래의 정의를 추가한다.
#define NDEBUG
위 의 선언을 정의하면 모든 asert()함수가 비활성화 된다. 당연히 디버깅이 더 필요할시에는 주석처리하고 다시 컴파일 하면 된다. 디버깅용으로 아주 좋을 것이다.
exit(), atexit()
#include <stdlib.h>
void sign_off(void);
void too_bad(void);
int main(void)
{
int n;
atexit(sign_off);
puts("Enter an integer:");
if(scanf("%d",&n) != 1)
{
puts("That's no integer!");
atexit(too_bad);
exit(EXIT_FAILURE);
}
printf("%d is %s.\n", n, (n % 2 == 0) ? "even" : "odd");
return 0;
}
void sign_off(void)
{
puts("Thus terminates another magnificent program from");
puts("SeeSaw Software!");
}
void too_bad(void)
{
puts("SeeSaw Sftware extends its heartfelt condolences");
puts("to you upon the failure of your program.");
}
Enter an integer:
1 // 숫자 입력시
1 is odd.
Thus terminates another magnificent program from
SeeSaw Software!
Press ENTER or type command to continue
Enter an integer:
q // 문자 입력시
That's no integer!
SeeSaw Sftware extends its heartfelt condolences
to you upon the failure of your program.
Thus terminates another magnificent program from
SeeSaw Software!
- atexit
: 이 함수는 exit()가 호출될때 실행되는 함수 리스트에 해당 함수를 등록한다. atexit()에 등록되는건 함수 포인터 형식이다. 함수명 자체가 주소값이므로 그냥 함수명을 인수로 사용하면 된다. ANSI에서는 32개까지의 등록을 보장한다. 스택처럼 맨처음 추가된 함수가 맨 나중에 호출이 된다. FILO(First In Last Out). 보통 프로그램 모니터링 파일을 업데이트 하거나 환경 변수를 재설정하는 등의 업무를 실행한다. exit()는 별도로 호출하지 않더라도 프로그램이 종료될때 exit()함수를 무조건 호출하므로 위와 같은경우 적어도 sign_off함수는 반드시 출력된다. 이 함수는 exit()가 호출된 후 실행된다.
- exit()
: atexit()로 지정된 함수는 exit() 이후에 실행한 후 스스로 정리한다. exit()는 모든 출력 스트림을 플러쉬(flush)하고, 개방된 모든 스트림을 닫고, 표준 I/O함수 tmpfine()의 호출로 만들어진 임시 파일을 닫는다. 그런 다음 exit()는 호스트 환경으로 제어를 반환하고, 가능하면 그 환경에 종료 상황을 보고한다. 전통적으로 UNIX프로그램은 성공적인 종료를 표시하는데 0을 사용하고 실패를 표시하는데 0이아닌 수를 사용하고 있다. exit()함수는 return문을 사용하는 것과 동일하다. 다른점은 exit()는 mail()함수외의 다른 함수에서 사용되면 종료가 된다. return은 값을 반환하겠지만 말이다..
인라인 함수
#include <stdio.h>inline void eatline() //인라인 함수 정의{while(getchar() != '\n')continue;}int main(void){...eatline();...}
#include <stdio.h>inline void eatline() //인라인 함수 정의{while(getchar() != '\n')continue;}int main(void){...while(getchar() != '\n') // 대체된 함수 호출continue;...}
//file1.cinline double square(double); // 인라인 함수선언double square(double x){return x * x;} // 외부 링키지 선언//file2.cextern double square(double); // 외부 함수 선언double square(double x) {it y; y = x*x; return y;}//file3.cextern double square(double); // 외부 함수 선언
extern double square(double); // 외부함수로 선언inline double square(double); // 인라인 함수로 선언double square(double x){return x * x;} // 외부링키지로 선언int main(){double q = square(1.3) + square(1.5); // 어떤 square를 써야하나??return 0;}
2010년 1월 28일 목요일
전처리문(펌)
실질적인 컴파일 이전에 미리 처리되는 문장으로 선행처리기라고도 한다.
컴파일러는 사용자가 작성한 코드를 컴파일하기에 앞서 전처리문에서 정의해 놓은 작업들을 먼저 수행한다.
종류로는 #define, #if, #ifdef, #ifndef, #defined, #undef 등이 있다.
이것은 기존에 있는 방대한 소스 코드를 지우지 않고 활성화와 비활성화하는 데에 가장 많이 이용된다.
즉, 기존에 있는 소스 코드를 건드리지 않고 부분적인 컴파일을 하는 것이다.
C의 전처리문이 오는 줄(Line)의 첫 문자는 항상 '#'으로 시작한다.
ANSI 표준에 따른 C의 전처리문의 종류
- 파일 처리를 위한 전처리문 : #include
- 형태 정의를 위한 전처리문 : #define, #undef
- 조건 처리를 위한 전처리문 : #if, #ifdef, #ifndef, #else, #elif, #endif
- 에러 처리를 위한 전처리문 : #error
- 디버깅을 위한 전처리문 : #line
- 컴파일 옵션 처리를 위한 전처리문 : #pragma
조건 처리를 위한 전처리문은 어떤 조건에 대한 검사를 하고 그 결과를 참(0 이 아닌 값) 또는 거짓(0)으로 돌려준다.
#if : ...이 참이라면
#ifdef : ...이 정의되어 있다면
#else : #if나 #ifdef에 대응된다.
#elif : 'else + if'의 의미
#endif : #if, #ifdef, #infdef이 끝났음을 알린다.
#include
헤더 파일과 같은 외부 파일을 읽어서 포함시키고자 할 때 사용된다. 이때의 파일은 이진 파일(Binary file)이 아닌 C의 소스 파일과 같은 형태의 일반 문서 파일을 말한다:
#include <stdio.h> /* 이 위치에 stdio.h라는 파일을 포함시킨다. */
#include "text.h" /* 이 위치에 text.h라는 파일을 포함시킨다. */
<...>
을 사용할 때와 ...을 사용할 때의 차이점은 <...>은 컴파일러의 표준 포함 파일 디렉토리(또는 사용자가 별도로
지정해 준)에서 파일을 찾는 것을 기본으로 한다. 그리고 ...을 사용했을 때는 현재의 디렉토리를 기본으로 파일을 찾게 된다.
아예 디렉토리를 같이 지정할 수도 있다:
#include <C:\MYDIR\MYHEAD.H>
#include "C:\MYDIR\MYHEAD.H"
#define
상수 값을 지정하기 위한 예약어로 구문의 상수로 치환한다. 또한 #define은 함수 역활과 비슷하게 아래와 같이 쓰일 수 있다:
#define SUM(x) ((x) = (x) + (x))
#define으로 정의할 수 있는 것은 숫자만이 아니다:
#define MYNAME "Young Hee"
이렇게 #define으로 정의된 것은 일반적인 변수와는 다르다. 그 차이는 명백하다:
#define MYNAME "Turbo"
char my_name[] = "Turbo"
MYNAME은 전처리문으로 my_name은 문자형 배열 변수로 정의되었다:
printf(MYNAME);
printf(MYNAME);
printf(my_name);
printf(my_name);
이것을 전처리한 상태는 다음과 같이 될 것이다:
printf("Turbo");
printf("Turbo");
printf(my_name);
printf(my_name);
이
런 결과에서 우리가 유추해 볼 수 있는 것은 전처리 명령을 사용했을 경우 "Turbo"라는 동일한 동작에 대해서 두개의 똑같은
문자열이 사용됐고, 변수를 사용했을 경우에는 하나의 문자열을 가지고 두번을 사용하고 있다는 것이다. 결과적으로 이런 경우에는
전처리문을 사용했을 경우 메모리 낭비를 가져 온다는 것을 알 수 있다.
#undef
#define으로 이미 정의된 매크로를 무효화한다:
#define ADD(a, b) (a + b)
#undef ADD(a, b)
앞으로 사용되는 ADD(...)는 undefined symbol이 되어 에러 처리된다.
#if ~ #endif
#if 구문은 if랑 아주 비슷하다. 이것은 어떠한 구문을 컴파일 할지 안할지를 지정할 수 있다:
#define A 1
#if A
source code ...
#endif
위
source code 부분은 컴파일이 된다. if문에서와 같이 참, 거짓을 구분하여 컴파일이 된다. 위에서 A값은 1 즉 0보다
큰 수이기 때문에 참인 것이다. 직접 아래와 같이 하면 거짓이기 때문에 source code 부분은 컴파일이 되지 않는다:
#if 0
source code ...
#endif
#ifdef ~ #endif
컴파일 할 때
#define MYDEF /* MYDEF는 값은 가지지 않았지만 어쨋든 정의는 되었다 */
#ifdef YOURDEF /* 만약 YOURDEF가 정의되어 있다면... */
#define BASE 10 /* BASE == 10 */
#elif MYDEF /* 그외에 MYDEF가 정의되었다면... */
#define BASE 2 /* BASE == 2 */
#endif
BASE는 상수 2로 치환되어 전처리기가 컴파일러에게 넘겨준다.
#ifndef 헤더명_H__ ~ #endif
헤
더 파일이 겹치는 것을 막기 위한 일종의 매크로이다. 예를 들어, 헤더 파일에 어떤 클래스의 인터페이스 선언을 넣었다고 하자.
이 클래스 인터페이스에서 다른 파일의 프로토타입이 필요해서 다른 A 파일을 include 하고 있는데 이 헤더 파일을
include 하는 파일에서 A라는 헤더 파일을 이미 include 하고 있다면 두번 define한 것이 된다. 그러면
SYNTEX 에러가 난다. 그래서 그런 것을 막는 방법의 하나로 #ifndef을 사용한다. 이전에 include되어 있으면
#endif 쪽으로 점프해 버려 결국 한번 선언되는 것이다:
#include
#include
이렇게 두번 썼다고 하자. 그런데 앞에 이미 include를 했는데 밑에 또 한다면 문제가 된다. 컴파일러가 검사해야할 코드량도 많아진다. 그래서 stdio.h에는
#ifndef STDIO_H__
#define STDIO_H__
가
선언되어 있다. 만약 STDIO_H가 선언되어 있지 않다면 선언한다는 뜻이다. 그 뒤 (b)에서는 이미 (a)쪽에서
STDIO_H__ 을 선언한 상태이기 때문에 전처리기 쪽에서 무시해버린다. 그러므로 컴파일러는 (a)만 검사한다.
#defined
define이 여러 개 되어 있는지를 검사할 때 쓴다. 이것은 여러 개를 동시에 검사 할 수 있다:
#if (defined A) || (defined B)
#ifdef와 #if defined의 차이
#ifdef은 정의가 되어 있는지를 테스트 하기 때문에, 한번에 여러 개를 사용할 수 없다:
#ifdef name
여러 개가 정의되어 있는지를 테스트 하기 위해서 #if defined를 사용할 수 있다:
#if defined(MACRO1) || defined(MACRO2)
#if는 ||로 중첩해서 사용할 수 있다. 형식이, #if expression이므로 C 표현이 올 수 있다:
#if (MACRO1) || (MACRO2)
#error
소스 라인에 직접 에러 메세지를 출력한다. 전처리기가 #error 문을 만나면 그 즉시 컴파일을 중단하고 다음과 같은 에러 메시지를 출력한다:
ERROR : XXXXX.c ########: Error directive: 내용
- XXXXX.c --> 현재 컴파일 중인 파일 명
- ####### --> 전처리기가 #error 문을 만난 시점에서의 행 번호(헤더 포함)
#ifdef __LARGE__
#error This program must be compiled in LARGE memory model!
#endif
이 내용은 만일 프로그램이 LARGE 모델이 아니라면 "#error" 뒤에 표시된 메세지를 출력하고 컴파일을 중지하게 된다.
#line
이 명령은 소스 코드의 행 번호를 지정하기 위한 것으로 주로 컴파일러에 의해 미리 정의된 __LINE__과 함께 사용된다.
__LINE__과 __FILE__을 각각 행 번호와 파일 명으로 변경한다:
#include <stdio.h>
#define DEBUG
void main(void)
{
int count = 100;
#line 100 /* 다음 줄번호를 100으로 설정한다 */
/* <-- 이 줄의 번호가 100이다 */
#ifdef DEBUG /* <-- 이 줄의 번호가 101이다 */
printf("line:%d, count = %d\n", __LINE__, count);
#endif
count = count * count - 56;
#ifdef DEBUG
printf("line:%d, count = %d\n", __LINE__, count);
#endif
count = count / 2 + 48;
#ifdef DEBUG
printf("line:%d, count = %d\n", __LINE__, count);
#endif
}
#pragma
컴
파일 옵션의 지정. 컴파일러 작성자에 의해서 정의된 다양한 명령을 컴파일러에게 제공하기 위해 사용되는 지시어이다. 컴파일러의
여러 가지 옵션을 명령행에서가 아닌 코드에서 직접 설정한다. #pragma는 함수의 바로 앞에 오며 그 함수에만 영향을 준다.
Turbo C는 9개의 #pragma 문(warn, inline, saveregs, exit, argsused, hdrfile, hdrstop, option, startup)을 지원하고 있다:
#pragma inline
컴파일할 때 어셈블러를 통해서 하도록 지시한다. 즉, 인라인 어셈블리 코드가 프로그램에 있음을 알려준다(명령행 상에서 '-B' 옵션).
#pragma saveregs
이 홉션은 휴즈 메모리 모델에 대해 컴파일된 함수에게 모든 레지스터를 저장하도록 한다.
#pragma warn
이 지시어는 Turbo C에게 경고 메시지 옵션을 무시하도록 한다.
#pragma warn -par
이는 매개 변수(parAMETER)가 사용되지 않았다고 경고(warnING)를 내지 못하도록 한다. 이와 반대되는 표현은
#pragma warn +par
경
고의 내용 앞에 (+)를 사용하면 경고를 낼 수 있도록 설정하고 (-)를 사용하면 경고를 내지 못하도록 하는 것은 모든 경고에
대해 동일하다. 명령 행에서는 "-wxxx"로 경고를 설정하고 "-w-xxx"로 경고를 해제한다. 경고의 종류는 무척 많은데
자주 사용되는 것을 아래에 나타냈다. 모든 것을 알고 싶다면 컴파일러 User's Guide의 명령행 컴파일러 부분을 참고하기
바란다:
par : 전해진 파라미터가 사용되지 않음
rvl : void 형이 아닌 함수에서 리턴 값이 없음
aus : 변수에 값을 할당했으나 사용하지 않았음
voi : void 형 함수에서 리턴 값이 사용되었음
sig : 부호 비트(MSB)가 고려되지 않은 형 변환(type-castion)에서 부호 비트를 소실할 수 있음
Standard C pre-defined symbols
| __FILE__ | a string that holds the path/name of the compiled file |
| __LINE__ | an integer that holds the number of the current line number |
| __DATE__ | a string(Mmm dd yyyy) that holds the current system date |
| __TIME__ | a string(hh:mm:ss) that holds the current system time |
| __STDC__ | defined as the value '1' if the compiler conforms with the ANSI C standard |
| __cplusplus | determines if your compiler is in C or C++ mode. Usually used in headers |
#include <stdio.h>
void main(void)
{
printf("The path/name of this file is %s\n", __FILE__);
printf("The current line is %d\n", __LINE__);
printf("The current system date is %s\n", __DATE__);
printf("The current system time is %s\n", __TIME__);
#ifdef __STDC__
printf("The compiler conforms with the ANSI C standard\n");
#else
printf("The compiler doesn't conform with the ANSI C standard\n");
#endif
#ifdef __cplusplus
printf("The compiler is working with C++\n");
#else
printf("The compiler is working with C\n");
#endif
}
프 로그래머들 마다 코딩 스타일(암시적 약속)이 있다. 보통 매크로, const 변수는 대문자로 적는 것이 원칙이다. 매크로 함수와 일반 함수, 매크로 대상체(object-like macro)와 일반 변수를 구분하기 쉽게 해주는 것이기 때문이다.
#define STDIO_H_
왜 뒤에 _를 붙였을까? 이것도 하나의 암시적 약속이다. 컴파일러 제작 회사는 매크로를 정의할 때 사용자들과 이름이 충돌이 나지 않게 하기 위해서 대부분 _를 뒤어 덧붙인다. 또한 _를 하나 혹은 두 개 연속으로 시작하는 것은 컴파일러 내부에서 사용하는 매크로라는 성격이 강하다. 물론 강제적인 뜻은 없으며 단지 관습상 그렇다. 왜 이것이 관습이 되었나 하면 보통 매크로 변수 이름이나 함수 이름을 지을 때 뒤에 _를 붙이지 않기 때문이다. 그래서 함수 제작자들이 _를 단골로 붙였다.