2022. 12. 2. 07:31ㆍC언어 복습
1. 함수 선언에서 매개변수 리스트를 작성할 때, 빈 칸 vs void
함수 2개를 아래와 같이 선언했다고 가정하자.
int FuncA();
int FuncB(void);
필자는 이 2개의 선언이 같은 뜻이라고 생각했다.... 착각이었다. C89를 기준으로 엄밀하게 말하면 아래와 같다.
1)은 정의문에서 매개변수가 어떤 것이 나오게 될 지 모른다는 뜻이다. 즉, 정의문에서 매개변수가 존재해도 문법 위반이 아니다!
2)는 말 그대로 void라고 명시를 했기 때문에 매개변수가 존재하지 않는다는 뜻이다.
여기서 선언과 정의를 명확하게 구분해서 글을 읽고 이해하는데 헷갈리지 않도록 하자!
결론: 매개변수 리스트가 없다면 반드시 void를 넣어주자.
2. 주석을 다는 방법
C89를 기준으로 주석을 다는 방법은 /* */이 있다. 이전 글에서도 썼지만 C89를 기준으로는 //를 지원하지 않는다.
C라는 언어 자체가 워낙 옛날에 나온 언어인데 한 줄 주석이 따로 등장한 것은 C89가 등장하고 시간이 조금 지나서 등장했다. 그래서 옛날 컴파일러에서 코드를 작성한다면 //가 먹히지 않을 수 있다...
결론: 모든 환경에서 안전하게 돌아가게 만들고 싶다면 /* */를 쓰자.
3. 자료형 signed / unsigned
C에서 제공하는 자료형은 아래와 같은 것들이 있다.
char, short, int, long, float, double, long double
보통 C를 처음 배울 때, 각 자료형의 바이트가 몇 바이트인지 배울텐데 필자는 아래와 같이 배웠다.
char - 1바이트
short - 2바이트
int - 4바이트
long - 4바이트
float - 4바이트
double - 8바이트
long double - 8바이트
그냥 단순히 어떤 것이 몇 바이트다. 이렇게만 배웠다. 하지만...
틀렸다!
실제 강의에서는 필자가 배워온 모든 근간을 부정하듯 이 역시 틀린 내용이라고 말하고 있었다. 사실 ANSI는 각 자료형의 크기를 명시적으로 확정지어 놓은 적이 없다. 그럼 엄밀하게 말해보자.
자료형과 포팅
각 자료형을 알아보기 전에 포팅과 관련해서 알게 된 내용을 적으려고 한다. 우리가 알고 있는 정수 자료형은 2의 보수를 사용한다. 그래서 어떤 자료형이 가질 수 있는 값의 범위는 일반적으로 아래와 같다.
- (n + 1) ~ n
이쯤되면 살짝 억까라고 느껴질 수 있는데 엄청 옛~~날 기계는 1의 보수를 사용할 수 있다고 한다. 그래서 진짜 엄청 아주 엄밀하게 이야기를 하면 그런 기계와도 포팅을 고려했을 때 안전한 값의 범위는 아래와 같다고 한다.
-n ~ n
근데 솔직히 어지간하면 모든 기계라고 말해도 될 정도로 대부분의 기계가 2의 보수를 사용하기 때문에 배제를 해도 될 정도라고 생각하긴 한다. 이런 것까지 알아야 하나 싶지만... 그래도 몰랐던 부분이니 적어봤다.
그리고 추가로 우리가 자료형을 배울 때는 그 크기를 배우게 되는데 단순히 몇 바이트다 이런 식으로 배웠을 것이다. 하지만 엄밀하게 말하면 그렇지 않다! ANSI는 자료형의 크기를 명시적으로 정하지 않고 있기 때문이다. 그래서 자료형의 크기에 대해서 이야기할 때에는 다음과 같이 말하는 것이 엄밀하다고 말할 수 있다.
"자료형 A의 크기는 n비트 이상이고 자료형 B이상의 크기를 가지고 있다."
char - 최소 8비트 이상, 대부분 8비트
signed / unsigned 디폴트는 알 수 없다. (ANSI에서 정하지 않음)
그래서 char를 문자가 아니라 숫자를 표현하기 위해서 쓴다고 한다면 signed와 unsigned를 명시적으로 표기하는 것이 좋다.
라고 솔직히 현존하는 대부분의 기계에서 signed이긴 하다. 다만 안드로이드에서 gcc를 돌리면 unsgiend가 나온다고 하니 이건 주의를 해야겠다.
여담으로 C를 기준으로 1바이트는 단순히 8비트가 아니라 char의 메모리 크기를 의미한다. 그래서 누가 컴파일러를 만들 때 char를 얼마든지 8비트가 아니라 16비트, 100만비트 등으로 만드는 것이 가능하며 이런 경우에는 1바이트가 8비트가 아니게 될 수가 있는 것이다...
C에서 char라고 하는 것은 이 기계에서 내가 돌릴 수 있는 가장 작은 단위의 메모리 크기를 의미한다. 그리고 이것을 1바이트라고 한다.
short - 최소 16비트 이상, char의 크기 이상, 대부분 16비트
값의 범위는 적으나 정말 엄~~~청나게 많은 양의 정수를 한꺼번에 저장할 때 쓰인다고 한다. 다만 우리가 체감할 일이 거의 없지만 기본적으로 정수를 표현할 때 쓰는 int보다는 연산의 속도가 느리다.
int - 최소 16비트 이상, short의 크기 이상, 대부분 32비트
int라는 자료형은 CPU에서 정수를 연산할 때 가장 빠르게 처리할 수 있는 가장 최적화되어 있는 데이터 형태를 의미한다.
CPU에는 ALU라는 레지스터가 있는데 이런 장치가 처리하는 기본 데이터가 워드이며 이것의 크기가 int의 크기이다. 다시 말해서 원래 int의 크기는 CPU마다 다르며 옛날에는 16비트 컴퓨터가 흔했기에 int가 16비트였다.
"어? 지금 64비트 컴퓨터쓰는데 그럼 int는 64비트여야 하는거 아님?"
맞다! 원칙적으로는 그렇다. 하지만 다른 언어들과 마찬가지로 지금까지 너무나 많이 32비트를 사용해왔고 그것에 맞춰서 코드가 작성되어 왔다. 그래서 ANSI 표준에 의하면 int는 CPU가 이해하는 메모리 크기를 따라가야 하지만 그러기에는 32비트로 된 정수를 표현할 방법이 없어지며 short를 32비트로 올리기에는 또 16비트 정수를 표현할 수 없는 노릇이라 int 하나만 건들기가 매우 힘들다. 이런 딜레마뿐만 아니라 64비트로 올린다고 성능이 좋아지는 것이 무조건은 아니라고 한다.
그래서 거의 다 int는 32비트에 머물러 있다.
여담으로 unsigned 변수에 값을 넣는 경우에 대해서도 이야기를 했는데 보통 아래와 같은 코드로 값을 넣는다.
unsigned int a = 2147483648;
여기서 오른쪽에 있는 숫자를 우리는 리터럴이라고 부른다. 근데 이렇게 되면 경고가 나온다. 왜냐하면 오른쪽에 있는 숫자는 signed int의 범위를 넘어선 숫자이기 때문이다. 그래서 뒤에 접미사를 붙여서 이 숫자는 unsigned라고 명시해야 한다.
unsigned int a = 2147483648u; /* 접미사로 U로 대문자를 써도 된다. */
long - 최소 32비트 이상, int의 크기 이상, 대부분 32비트
C에서는 long은 32비트이지만 다른 언어에서는 64비트인 경우가 꽤 있다. 또한 C89에서는 최소 64비트인 기본 정수 자료형이 존재하지 않는다...
여담으로 long 데이터를 의미하는 접미사는 숫자 뒤에 'L', ' l ' 을 붙일 수 있다. 단, 숫자 1과 알파벳 소문자 ' l ' 이 헷갈릴 수 있기 때문에 대문자 'L'을 쓰는 경우를 많이 볼 수 있다. 어쨋든 헷갈리지 않게 주의하자.
float - 최소 char의 크기 이상, 대부분 32비트
대부분의 소프트웨어 공학에서 실수를 표현하는 방식은 IEEE 754 부동소수점으로 되어있다. 그러나 C는 그 전부터 존재했기 때문에 ANSI 표준에서는 C의 실수 표현 방식을 정의하고 있지 않아서 컴퓨터에 따라 IEEE 754가 아닐 수 있다... 근데 솔직히 이 부분은 배제를 해도 상관이 없지 않을까 싶긴 하다. 어쨋든 알아놓자.
또한 float과 같은 실수 자료형은 signed / unsigned가 존재하지 않는다.
숫자 뒤에 붙일 수 있는 접미사는 'f', 'F'가 있는데 대부분 'f'를 사용한다.
double - 최소 float의 크기 이상, 대부분 64비트
원래의 의미는 float의 2배 크기라는 의미로 double이라고 이름을 지었지만 ANSI 표준을 원칙으로 말하면 CPU가 이해하는 데이터 즉, WORD의 크기를 따라가게끔 말하고 있다. 즉, CPU가 가장 빠르게 연산할 수 있는 실수의 형태를 double이라고 정의한다.
그래서 리터럴 형태로 실수를 쓰면 그것은 double형 데이터를 의미한다.
(솔직히 이 부분은 알고는 있었는데 char도 쓰는 김에 다른 자료형 안나오자니 좀 그래서 다 씀)
long double - 최소 double의 크기 이상
double과 비교했을 때 더 높은 정밀도를 요구할 때 사용한다고 한다.
대부분 몇 비트인지를 써야 했는데 그러지 못한 이유는 대부분이라고 말하기에는 그 비율이 너무 중구난방이다. 어떤 곳은 double과 같이 64비트인데 어떤 곳에서는 double의 2배 크기를 가질 수 있다고 말한다. 실제 강의에서도 대부분 몇 비트라는 발언이 없다... 실제로 지금 이 글을 쓰고있는 와중에 우분투 리눅스에서 gcc로 long double을 바이트 크기를 찍어봤는데 16이 찍혔다. 즉, 우분투 리눅스에서 gcc를 기준으로 하면 long double은 16바이트가 되는 것이다. 근데 상당히 자주 쓰이는 Visual Studio를 기준으로 하면 8바이트가 찍힌다. 즉, 대부분 몇 바이트라고 말할 수 없다.
여담으로 sizeof를 이용해서 long double의 크기르 찍으면서 알게 된 것인데 현재 필자는 우분투 리눅스 환경에서 gcc 컴파일러로 최대한 에러와 경고를 잡아주게끔 설정한 상태로 코드를 짜고 있다. 그런 와중에 아래와 같은 코드에서 에러가 떴다.
printf("%d\n", sizeof(long double);
이게 왜? 라고 할 수 있지만 sizeof의 반환은 size_t인데 이것은 int가 아니다. 정정해서 말하면 필자가 사용중인 우분투 리눅스 gcc 컴파일러 기준으로 int가 아니다. sizeof의 반환형인 size_t는 부호없는 정수이며 unsigned int가 될지 unsigned long이 될지 모른다. 단, C89에서 sizeof의 반환형의 메모리 크기를 ANSI 에서 정하지 않아서 다들 암묵적으로 최소 16비트 이상으로 개발을 했지만 C99는 아예 최소 16비트 이상의 부호없는 정수여야 한다고 못을 박아버린다.
여담으로 size_t의 _t는 typedef이라는 것을 뜻하며 필자가 사용중인 우분투 리눅스 gcc 컴파일러는 sizeof의 반환형인 size_t가 unsigned long이다. 또한 표준에 따르면 stddef.h 라는 헤더파일을 include시켜야 하지만 컴파일러마다 그냥 쓰게 해주는 경우도 있다고 한다.
자료형 정리
특정 소형기기를 다룬다고 하면 그 기기의 자료형이 각각 얼마의 크기를 가지는지 메뉴얼을 확인해야 한다.
여기저기에 쓰일 완벽하게 포팅가능한 코드를 짜고 싶다면 포팅이 보장되는 값의 범위만 사용해야 한다.
번 외
for에서 반복문을 돌릴 때, 다음과 같은 코드를 작성하는 경우가 많다.
int n = 10;
printf("반복 시작!\n");
for (int i = 0; i < n; i++) { /* 반복 내용 */ }
위와 같이 코드를 작성하는 것은 C89를 기준으로 컴파일이 되지 않는다. C에서는 어떤 실행\문이 오기 전에 변수 선언을 미리 해놔야 한다. 그래야 컴파일 에러가 발생하지 않는다. 필자는 Visual Studio에서 C++을 위주로 쓰는데 그 때는 이런 코드를 짜도 실행에 아무 지장이 없었다. 하지만 C89는 그렇지 않아서 주의해야 하며 혹시 모르니 그냥 맨 위에 변수를 전부 선언하는 습관을 들이는 것이 좋을 것 같다.
또한 조건식을 쓸 때 카운터 역할을 하는 변수를 그냥 쓰는 경우가 있다. 예를 들면 이렇게 말이다.
if (count) { }
이것은 좋지 않다고 하니 if (count != 0) 이런 식으로 코딩을 하는 습관을 들이는 것이 좋겠다.
'C언어 복습' 카테고리의 다른 글
POCU C언어 정주행 6회차 - 댕글링 포인터, 포인터 연산, 캐스팅, ASLR (0) | 2022.12.10 |
---|---|
POCU C언어 정주행 4회차 - scope(범위), goto문, const (2) | 2022.12.08 |
POCU C언어 정주행 3회차 - 평가 순서, Sequence Point (0) | 2022.12.08 |
POCU C언어 정주행 2회차 - 함수 전방 선언, 빌드 과정, 라이브러리 (0) | 2022.12.08 |
POCU C언어 정주행 - Intro (0) | 2022.12.02 |