본문 바로가기

프로그래밍 언어/C

[K.N.K C Programming 정리] Chap 7. Basic Types

K.N.KING C Programming

안녕하세요 여러분! 7단원 Basic Types 가져와봤습니다....

사실 7단원까지가 조금 따분한거 같아요 ㅋㅋㅋ 8단원부터 배열 나오고 함수 나오고 포인터 나오던데 재밌어질것 같습니다 ㅎㅎ

자 그럼 드가봅시다!

 


Chap7. Basic Types

Make no mistake about it : Computers process numbers - not symbols. We measure our understanding (and control) by the extent to which we can arithmetize an activity.


7.1 Integer Types

C는 근본적으로 다른 2개의 숫자 타입을 가지고 있다. 바로 정수와 실수 타입이다.(integer and float types

실수는 소수부분을 가지고 있다. 정수 타입은 부호를 가지는 정수타입과 가지지 않는 정수 타입으로 나뉜다. 

부호가 있는 정수타입의 제일 왼쪽의 비트는 사인 비트(sign bit)라고 불리며, 이 비트가 0이면 0 혹은 양수이고, 1이면 음수를 나타낸다. 따라서 16비트 정수의 가장 큰 수는, 0111111111111111 이다. 이는 10진 정수로 32767(2^15 - 1) 이다.

32비트 정수의 가장 큰 수는 2,147,483,647 (2^31 -1)로 약 21억이다. 만약 제일 왼쪽의 사인 비트까지 숫자의 크기를 결정하는데 쓰인다면, 이는 부호가 없는 정수를 나타낸다. 16비트의 가장 큰 부호 없는 정수는 65,535(2^16 - 1) 이다. 그리고 가장 큰 32비트 부호 없는 정수는 4,294,967,295(2^32 - 1) 이다. 기본적으로, C에서 정수값의 제일 왼쪽 비트는 부호 표시를 위해 예약되어있다. 컴파일러에게 해당 변수가 부호가 없음을 알려주기 위해선 우리는 변수를 unsigned 라고 선언해야한다. 부호없는 정수는 low-level 프로그래밍과 시스템 프로그래밍에서 유용하게 쓰인다. 20장에서 부호없는 정수의 사용예시를 보도록 하자. 일단은 부호없는 정수는 쓰지말자.

 

C의 정수타입은 다양한 크기를 가진다. int 타입은 보통 32비트지만, 옛날 컴퓨터에서는 16비트일것이다. int 에 담기엔 너무 큰 숫자를 저장하기 위해 C는 long 타입을 지원한다. 가끔 우리는 메모리를 아껴쓰기 위해 컴파일러에게 int보다 더 작은 공간에 숫자를 담으라고 명령하고 싶다. 이때 short integer를 사용한다.

우리의 요구에 일치하는 정확한 정수타입을 정하기 위해 우리는 변수가 long, short, signed, unsigned 인지 구체화할 수 있다. 우리는 심지어 이들을 섞어 쓰기도 한다!(long unsigned int). 

사실 다음의 6가지 경우를 제외하고 나머지 조합은 다 똑같은 표현들이다. 

 

short int 

unsigned short int

 

int

unsigned int

 

long

unsigned long

 

이를테면, long singed int 는 long int와 같은 포현이다. 왜냐하면 정수는 항상 unsigned 라고 명시하지 않으면 signed 이기 때문이다. C는 int를 생략하여 적음으로써 정수타입을 더 간결하게 선언할 수 있도록 도와준다. 예를 들면, long int는 그냥 long 으로 축약할 수 있고, unsigned short int 는 unsigned short 로 축약할 수 있다. int를 생략하는 것은 C프로그래머들 사이에서 널리 퍼진 관습이다! 이제 부터는 꼭 필요한 경우가 아니라면 int를 생략하겠다.

 

위에 서술한 6개의 int 값의 범위는 기계마다 다를 수 있다. 따라서 컴파일러는 꼭 지켜야 할 몇가지 규칙을 가지는데, 이를테면 다음과 같다. C표준은 int는 short int 보다 클 것, long int는 int보다 작으면 안될 것 등을 제시한다.

 

C99는 위의 6가지 표준 정수 타입말고도 두 가지를 더 제시한다. 바로 long long int 와 unsigned long long int 이다.

이러한 타입들은 매우 큰 정수의 필요성이 대두됨에 따라, 그리고 64비트 연산을 지원하는 새로운 cpu가 나타나며 등장하게 되었다. 두 long long 타입 모두 최소 64비트크기를 가져야 한다. long long int 의 최댓값은 9,223,372,036,854,775,807(2^63 - 1) 이다. 엄청나게 크다!

이러한 표준 정수타입 이외에도, C99 표준은 정수타입의 확장판, implementation - defined 을 지원한다. 예를 들어, 컴파일러는 128 비트 크기의 signed, unsigned 정수 타입을 제공할 수 있다!

 

Integer Constants

C는 여러 형태의 상수를 제공한다. 10진 정수, 8진 정수, 16진 정수로 선언될 수 있다.

8진 정수는 숫자 0으로 시작하며, 0에서 7 사이의 숫자가 쓰인다.

16진 정수는 숫자 0과 알파벳 x 로 시작한다. 0~9 사이의 숫자와 알파벳 a~f 가 쓰인다. 대문자로 써도되고 소문자로 써도된다. 이 때, 8진수와 16진수는 그저 숫자를 표시하는 방법 중 하나일뿐, 저장되는 방식에는 아무 영향을 끼치지 않는다. 정수 상수의 타입은 보통 int 이다. 그러나, 만약 상수의 값이 int 에 담기에 너무 크다면, 상수는 long int(혹은 long long int) 를 가질것이다. 컴파일러가 상수를 long으로 다루게 하려면, 숫자 끝에 L 이나 l 을 붙여줘야한다. 

이 때 부호를 가지지 않는다는것을 알려주려면, 끝에 U 혹은 u 를 추가로 붙여주면 된다. 

C99에서 LL 이나 ll 로 끝나는 정수상수는 long long int 타입을 가진다. 

 

Integer Overflow

정수의 산술연산이 일어날때, 너무 큰 수가 결과로 나타날 수 있다. 두개의 int 값에 산술연산이 수행된다면, 결과는 반드시 int로 표현될수 있어야 한다. 만약 결과가 너무 큰 수라 int로 표시되지 못한다면, 오버플로우(overflow)가 일어났다고 한다. 정수 오버플로우가 일어났을때 어떤 결과가 발생하는지는 피연산자가 부호있는 정수인지 아니면 부호없는 정수인지에 따라 달라진다. 부호있는 정수에서 오버플로우가 일어날 경우 프로그램의 거동은 정의되지 않았다.(undefined - behavior. 기억나지? 그렇지?) 4.4장에서 undefined behavior 의 결과는 변한다는 점을 상기해보자.

대부분의 경우 연산의 결과는 틀린 것이고, 프로그램이 깨지는 경우도 충분히 발생할 것이다.

만약 부호없는 정수에서 오버플로우가 일어날 경우, 결과는 정해져있다. (When overflow occurs during an operation on unsigned integers, though, the result is defined) n이 결과를 저장하기 위해 쓰이는 비트의 개수라면, 오버플로우의 결과로 우리는 2^n 으로 나눈 나머지를 얻게된다. 이를테면, 16비트짜리 부호없는 정수의 최댓값인 65535(2^16 - 1) 에 1을 더한다면, 결과는 0이 나올것이다. 

 

Reading and Writing Integers

만약 int 값들의 오버플로우 때문에 프로그램이 작동하지 않는다고 해보자. 이 때의 대안은 아마 int 에서 long(혹은 long long) 으로 바꾸는 것 일것이다. 그런데 우리는 이렇게 변수의 타입을 바꾸는것이 프로그램에 어떤 영향을 끼칠지 생각해봐야 한다. 

특히, 우리는 반드시 바꾼 변수가 printf 와 scanf 안에서 쓰이는지 봐야한다. 만약 그렇다면, 함수안의 format string 역시 바뀌어야한다. 왜냐하면, %d 변환 지정자는 오직 int 타입에만 유효하기 때문이다. 

unsigned, short, long 타입은 몇가지 변환지정자를 추가로 필요로한다!

  • 만약 부호없는 정수를 읽거나 쓰고 싶으면 d대신 u, o, x 를 써라! 만약 u 를 쓸 경우, 숫자는 10진 정수로 읽힐것이다. o 는 8진수로 읽힐것이고, x는 16진 정수로 읽힐것이다.
  • short 타입의 정수를 쓰거나 읽을 땐 h 를 d, o, u, x 앞에 붙여라. s가 short 타입 변수라면, printf("%hd", s) 이런식으로 말이다.
  • long 타입의 정수를 쓰거나 읽을 땐, l 을 앞에 붙여라. a 가 long 타입 변수라면, printf("%ld",a) 이런식으로 말이다.
  • long long 타입도 마찬가지로 ll 을 앞에 붙여주면 된다.

7.2 Floating Types

정수 타입만으로 모든 숫자를 쓸 수있는건 아니다. 소수점이하 숫자들이 필요할 때도 있고, 정수로 나타내기엔 매우 큰 숫자들도 있다. 이런 숫자들은 부동 소수점 형식으로 저장된다. C는 세개의 floating types 를 제공한다. 

 

float - 단정밀도 부동소수점(Single - precision floating point)

double : 이중정밀도 부동소수점(Double - precision floating point)

long double : Extended - precision floating point

 

온도를 소수점 이하 한자리까지 측정할때처럼 float는 정밀도가 중요하지 않을 때 쓰인다. double은 매우 정밀한 정확도를 제공한다. long double 은 무결점에 가까운 정밀도를 제공하지만, 거의 쓰이지 않는다. 

한가지 유의해야 할 것은, C표준 자체는 float, double, long double 의 정밀도가 얼마나 정확한지 구체적으로 정의하진 않는다는것이다. 컴퓨터마다 부동소수점 숫자를 저장해놓는 방식이 다르기 때문이다. 다만, 현대의 대부분의 컴퓨터들은 부동소수점 저장 방식의 국제표준, IEEE 754 표준을 따른다.

IEEE 754 표준에서는 단정밀도와 이중정밀도 형식을 정의해놓았다. 숫자들은 과학적 표기법으로 저장되고, 부호(sign), 지수(exponent), 가수(fraction) 세 파트로 나뉘어 저장된다. 지수에 사용되는 비트의 개수는 숫자가 얼마나 크거나 작은지 결정하고, 가수에 사용되는 비트의 개수는 정밀도를 결정한다. 단정밀도 형식에서 지수는 총 8개의 비트를 차지하고, 가수는 23개의 비트를 차지한다. 이 경우 단정밀도 숫자의 최댓값은 약 3.40 X 10^38 이고 정밀도는 소수점 이하 6자리까지 유지된다. 

double 은 최댓값 1.79769 X 10^308 을 가지며, 정밀도는 소수점 이하 15자리까지이다.

IEEE 표준을 따르지 않는 컴퓨터는 위의 수치를 가지지 않을 수 있다. 실수타입의 특성을 정의한 매크로는 

<float.h> 헤더에서 찾아볼 수 있다. 

 

Floating Constants

실수 상수들은 다양한 방식으로 쓰일 수 있다. 57.0 이란 숫자는 다음과 같이 쓸 수있다.

 

57.0    57.    57.0e0    57E0    5.7e1    5.7e+1    .57e2    570.e-1

 

실수 상수는 반드시 . (decimal point) 를 가지거나 exponent(지수) 를 가져야한다. 이때 지수는 10승단위이다. (power of 10) 만약 지수가 있으면, E 나 e 뒤에 써야한다. 

기본값으로, 실수 상수는 이중 정밀도로 저장된다. 바꿔말하면, C 컴파일러가 57.0 이란 실수 상수를 발견하면, 컴파일러는 이를 double 타입과 같은 방식으로 저장한다. 이런 방식은 전혀 문제될것이 없다. 왜냐하면 필요한 경우 자동적으로 float타입으로 바뀌기 때문이다. 

꼭 float 타입으로 저장해야 한다면, f나 F 를 숫자뒤에 붙여주자. Long double 타입으로 저장해야한다면 숫자 끝에 L이나 l을 붙여주자.

 

Reading and Writing Floating-Point Numbers

3장에서 살펴 봤듯이, 단정밀도 타입의 실수를 읽거나 쓸때는 %e %f %g 변환지정자를 사용한다. double과 long double 타입은 조금 다른 변환지정자를 요구한다. 

  • 만약 double 타입을 읽어야할 상황이라면 e,f,g 앞에 l 을 붙여주자. 만약 d가 double 타입변수라면, scanf("%lf", &d) 와 같이 쓸 수 있다.
  • long double 타입을 읽어야한다면, e,f,g 앞에 L을 붙여주자.

7.3 Character Types

나머지 기본 타입은 char 뿐이다. char 타입은 컴퓨터마다 다를 수 있다. 왜냐하면, 컴퓨터마다 다른 문자 세트를 가지고 있을 수 있기 때문이다. 오늘날 가장 유명한 문자 세트는 ASCII 이다. 128개(7비트)의 문자를 나타낼 수 있다. 

아스키는 종종 256개의 문자를 지원하는 Latin-1 세트로 확장된다.

char 타입은 " 가 아닌 '(single quote) 안에 감싸진다는것을 짚고 넘어가자!

 

Operations on Characters

C에서 문자를 다루는 원리는 간단하다. C는 문자를 작은 정수로 다룬다! 따라서 문자들은 모두 이진수 형태로 저장된다. 

예를 들어, 아스키를 보면 문자의 코드가 0000000 에서 1111111 까지 있고, 우리는 이를 0에서 127까지의 숫자로 생각할 수 있다. 'a' 는 97이라는 값을 가지고, 'A'는 65라는 값을 가진다. 만약 문자가 계산 중에 나타나면, C는 단순히 그것의 정수 값을 다룬다. 다음과 같은 코드를 보자.

if('a' <= ch && ch <= 'z')
    ch = ch - 'a' + 'A';

 위 코드는 ch가 만약 소문자라면 대문자로 바꾸는 코드이다.

그런데, 'a' <= ch와 같이 문자를 비교하기 위해 <,<=,>,>= 를 쓰는 코드는 사실 권장되는 코드는 아니다. 왜냐하면, 이런 값들은 모두 문자 세트(character set)에 의존하기 때문에 이런 코드를 포함한 프로그램은 이식성이 낮아진다.(may not be portable)

또한 'a' * 'b' / 'c' 처럼 아무 의미없는 결과나 에러를 발생시킬 수 있다. 

 

Character Handling Functions

우리는 방금 소문자를 대문자로 바꾸는 if문을 살펴봤다. 이는 확실히 최고의 방법은 아니다. 그럼 어떻게 해야할까?

바로 C의 toupper 라이브러리 함수를 호출하는 것이다!

ch = toupper(ch);

만약 toupper 함수가 호출되면, toupper 함수는 자신의 인자(이 경우 ch)가 소문자인지 체크한다. 만약 소문자가 맞다면, 그에 상응하는 대문자를 반환한다. 만약 소문자가 아니라면 인자의 값을 그대로 반환한다(대문자). 

다음과 같이 if문안에 바로 쓸 수있다.

if(toupper(ch) == 'A') ...

이 때, toupper 함수를 쓰려면 #include directive를 상단에 적어줘야 한다.

#include <ctype.h>

toupper 말고도 C 라이브러리에는 문자를 처리해주는 유용한 함수들이 있다. 23.5 장에서 함수들과 사용예시를 다룬다.

 

Reading and Writing Characters using scanf and printf

%c 변환지정자는 scanf 와 printf 에서 하나의 문자를 읽을 수 있도록 합니다. 

 

char ch;
scanf("%c", &ch);
printf("%c", ch);

scanf 는 문자를 읽기 전에 white - space characters 를 스킵하지 않습니다.(이 문장이 이해가 안되면 3단원 정리를 보고오시는 걸 추천드립니다!!) 따라서 만약 읽혀지지 않은 공백이 있다면, 이 경우 ch는 ' ' 값을 갖게 된다. scanf 가 공백을 스킵하게 하려면, format string 안의 %c 앞에 한 칸 공백을 두어야 한다. 

scanf(" %c", &ch);

이렇게 적으면 먼저 공백을 스킵하고나서 ch를 읽는다!

3.2장에서 다루었듯이, scanf 안에서의 공백은 0개, 1개 혹은 여러 개의 white - space characters 를 스킵한다는걸 짚고 넘어가자. 

 

Reading and Writing Characters using getchar and putchar

C는 하나의 문자를 읽고 쓸 여러 방법을 제시한다. 특히, 우리는 getchar 와 putchar 함수를 scanf와 printf 대신 쓸 수있다. putchar는 하나의 문자를 출력해주는 함수이다.

getchar 는 호출될때마다 하나의 문자를 읽고 그것을 반환한다. 반환된 문자를 저장하기 위해선 대입연산자로 변수에 저장해주면 된다.

ch = getchar();

getchar는 사실 char 타입 값이 아니라 int 값을 반환한다. (이유는 이후의 챕터에서 설명할것이다.) 그리고, getchar는 scanf와 마찬가지로 white space characters 를 스킵하지 않는다.

scanf와 printf를 쓰는것보다 getchar 와 putchar 를 사용하는것은 프로그램이 실행될때 시간을 줄여준다.

getchar와 putchar 가 빠른 이유는 다음과 같다. 우선 scanf와 printf 보다 매우 간단하다! scanf와 printf는 여러 타입의 데이터를 읽고 쓰기 위해 만들어졌다는것을 짚고 넘어가자. 또한, getchar 와 putchar는 보통 빠르게 동작하기 위해 매크로를 사용해 구현되기 때문이다. getchar는 또한 scanf에 비해 읽은 값을 바로 반환한다는 특성때문에 여러 C 관용구에서 많이 쓰인다. 특정 문자를 입력으로 받아들이는데 엔터를 치면 종료되는 반복문을 생각해보자.

scanf를 사용하는 경우,

do {
    scanf("%c", &ch);
} while (ch != '\n');

우리는 이제 이를 getchar를 사용해,

while ( (ch = getchar()) != '\n')
;

이렇게 바꿀 수 있다! 사실 변수 ch도 필요없다. 더 축약하면 다음과 같다.

while (getchar() != '\n')
;

이는 아주 잘 알려진 C 관용구이다. 아리송해보이지만 배울만한 가치가 있는 관용구이다!

 

7.4 Type Conversion

컴퓨터가 산술 연산을 수행하려면, 두 피연산자가 같은 크기를 가져야한다. 즉, 같은 개수의 비트를 가져야 하고, 같은 방식으로 저장되야 한다. 이를테면, 16비트 정수와 32비트 정수를 더하거나 32비트 정수와 32비트 실수를 더하는 것은 바로 수행할 수 없을것이다.

그러나, C 에서는  표현식에서 기본 타입들을 섞어써도 괜찮다. 우리는 정수, 실수 심지어 문자형까지도 하나의 표현식 안에 쓸 수 있다. 이 경우, 하드웨어가 표현식을 평가할 수 있게 C컴파일러는 타입이 일치하지 않는 피연산자를 타입이 일치하게 형변환 시켜주는 명령을 만들어 낼것이다.

만약 우리가 16비트 short와 32비트 int를 더한다면, 컴파일러는 short 타입 변수를 32비트로 변환할것이다. 만약 int와 float를 더한다면, 컴파일러는 int를 float로 바꿀것이다. 다만, 이 변환은 다소 복잡하다! int와 float는 다른 방식으로 저장되기 때문이다.

 

이러한 변환들은 프로그래머의 개입 없이 컴파일러가 자동으로 처리하기 때문에 이들은 암시적 형변환(implicit conversions)이라고 불린다. 또한 C는 프로그래머에게 캐스팅 연산자(cast operator)를 통해 명시적 형변환(explicit conversions)을 할 수 있게 해준다. 우선 암시적 형변환부터 다루기로 하자.

암시적 형변환은 약간 복잡하다. C에는 여러 산술 타입이 존재하기 때문이다. 

암시적 형변환은 다음 상황에서 수행된다.

  • 산술 연산이나 논리 표현식에서 피연산자들의 타입이 서로 일치하지 않을때.(이 경우 C 는 usual arithmetic conversions 를 수행한다.)
  • 대입 연산자의 오른쪽 표현식이 왼쪽 표현식과 일치하지 않을때.
  • 함수를 호출할때 인자들의 타입이 그에 해당하는 파라미터의 타입과 일치하지 않을때(when the type of an argument in a function call doesn't match the type of the corresponding parameter)
  • return문에서 표현식의 타입이 함수의 반환형과 일치하지 않을때.

우선 첫 두가지 케이스만 다루기로 하고, 나머지 두 케이스는 9장에서 다루자.

 

The usual Arithmetic Conversions

일반적인 산술 변환(usual arithmetic conversion) 은 산술 연산자, 관계연산자, 동등연산자 등등 거의 대부분의 이항연산자의 피연산자들에 적용된다. 예를 들어, f가 float 타입이고 i가 int 타입이라고 해보자. 이 경우 f + i 가 이에 해당한다. 이 때, float를 int로 바꾸는 것 보다 int를 float로 바꾸는 것이 더 안전하다. 정수는 항상 float로 바뀔 수 있다. 그에 비해 부동소수점 수를 int로 변경하는 것은 숫자의 가수부분을 날려버릴 수 있다! 심지어 형변환 하고자 하는 실수가 int 타입이 가질 수 있는 최댓값 보다 큰 경우 완전히 의미 없는 결과를 불러올 수 있다. 

usual arithmetic conversion 뒤에 숨겨진 전략은 바로 형변환을 할 때, 두 개의 피연산자중 두 값을 모두 안전하게 수용할 수 있는 "좁은(narrowest)" 타입으로 나머지를 변환시킨다는것이다 여기서 좁은(narrowest) 타입이란, 대강 이렇게 받아들이면 된다. - 두 타입 중 어느 한 타입을 저장하는데 더 적은 공간을 차지하면 그 타입을 더 좁은 타입 이라고 칭한다.(The strategy behind the usual arithmetic conversions is to convert operands to the "narrowest" type that will safely accommodate both values. Roughly speaking, one type is narrower than another if it requires fewer bytes to store) 피연산자의 타입은 더 좁은 타입의 피연산자를 나머지 피연산자로 바꿈으로써 짝지어질수 있다. 이러한 행동은 프로모션(promotion) 이라고 불린다. 프로모션 중 가장 흔한 프로모션은 integral promotions 으로 문자 타입이나 short integer 를 int 로 바꾸는 변환이다.

우리는 usual arithmetic conversions 를 (앞으로 이를 일반적인 산술 형변환 이라고 부르겠다.)  두 개의 경우로 나눌 수 있다.

  • 피연산자의 타입 중 하나가 floating 타입인 경우 : 좁은 타입의 피연산자를 형변환하고자 할 때. float >> double >> long double 순으로 변환하면 된다. 만약 피연산자가 long double을 가지면, 나머지 피연산자를 long double로 바꾸면 된다. 그게 아니라면, 만약 한 피연산자가 double 을 가지면 나머지를 double로 바꾸면된다. float 도 마찬가지이다. 이런 룰들이 정수와 실수가 섞인 연산에서도 적용된다는것을 짚고 넘어가라! 만약 한 피연산자의 타입이 long int 이고 나머지 한타입이 double 이라면, long int 타입의 피연산자는 double 로 바뀔것이다.
  • 둘 중 어떤 타입도 floating 타입이 아닌 경우 : 우선 먼저 integral promotion 을 수행한다. 그 후 int >> unsigned int >> long int >> unsinged long int 순으로 좁은 타입의 피연산자를 더 넓은 쪽으로 형변환 시킨다. (long long 추가할 것.) 

Conversion During Assignment

일반적인 산술 형변환은 대입에 적용되지 않는다. 그 대신에 C는 대입 연산자의 오른쪽 표현식이 왼쪽에 위치한 변수의 타입으로 변해야 한다는 간단한 규칙을 따른다. 만약 왼쪽의 변수의 타입이 표현식의 타입보다 넓다면 문제없이 진행될것이다. 이를테면,

char c;
int i;
float f;
double d;

i = c; // c is converted to int
f = i; // i is converted to float
d = f; // f is converted to double

만약 그렇지 않은 경우, 문제가 생길 수 있다. 부동 소수점 숫자를 정수 타입의 변수에 대입하는 것은 숫자의 가수부분을 날릴 수 있다. 

int i;

i = 842.97; // i is now 842
i = -842.97; // i is now -842

 

Implicit Conversions in C99

C99 에서의 암시적 형변환은 C89에서와는 살짝 다르다. 왜냐하면 C99에는 타입들이 추가 되었기때문이다.

C99는 각각의 정수타입에게 정수 변환 랭킹을 부여하였다. 다음은 높은 순위에서 낮은 순위대로 적은것이다.

 

1, long long int, unsigned long int

2, long int, unsigned long int

3, int, unsigned long int

4, short int, unsigned short int

5, char, signed char, unsigned char

6, _Bool

 

C89의 integral promotions 를 대신하여, C99 에서는 "integer promotions" 를 준비하였다. 이는 int 보다 랭크가 낮은 모든 타입은 int 로 변환한다는 것이다.(단, 모든 타입이 int 로 바뀔 수 있다고 가정한다.)

C89에서처럼 C99에서도 일반적인 산술 변환은 두 가지 경우로 나뉜다. 

  • 피연산자의 타입 중 하나가 floating 타입인 경우 : 
  • 둘 중 어떤 타입도 floating 타입이 아닌 경우 : 

 

Casting

C의 암시적 형변환이 편리하긴 해도, 가끔씩 더 높은 수준으로 형변환을 다뤄야 할 때가 온다. 

이런 이유로, C는 캐스팅(cast)을 제공한다. 캐스팅 표현식은 다음과 같은 형식을 지닌다.

 

type - name  )  expression

 

type name 은 표현식의 바뀔 타입을 지정한다. 다음 코드를 살펴보자.

 

float f, frac_part;

frac_part = f - (int) f;

캐스팅 표현식 (int) f 는 f의 int 로의 변환 결과를 나타낸다. C의 일반적인 산술 연산은 (int) f 가 다시 float 로 바뀐 후 뺄셈이 수행될것을 요구한다.( 뺄셈 연산자 에서 float - int 꼴이니까 int를 float 로 바꾼단 소리입니다.)(C's usual arithmetic conversions then require that (int) f be converted back to type float before the subtraction can be performed). f와 (int) f 의 차이는 f의 가수부분이 캐스팅 중에 사라졌냐 아니냐 이다.

 

다음의 예시를 살펴보자.

float quotient;
int dividend, divisor;

quotient = dividend / divisor;

나눗셈의 결과는 정수일것이고, 이는 quotient 에 대입되기전에 먼저 float로 형변환 될것이다. 

하지만 우리가 원하는 정확한 결과는 dividend 와 divisor를 나눗셈이 일어나기 전에 float로 바뀌고 나서의 결과이다.

여기서 캐스팅 연산자를 사용할 수 있다.

 

quotient = (float) dividend / divisor;

dividend에 적용되는 캐스팅이 컴파일러에게 divisor도 float로 캐스팅되게 하기 때문에 divisor는 캐스팅이 필요하지 않다. 또한, C는 캐스팅 연산자를 단항 연산자로 간주한다. 따라서 캐스팅 연산자는 이항 연산자보다 우선순위가 높다

따라서 컴파일러는 (float) dividend / divisor 를 ((float) dividend) / divisor 로 인식할 것이다.

 

오버플로우를 피하기 위해 캐스팅이 필요한 경우가 생긴다. 이를테면, 다음과 같은 경우를 살펴보자.

long i;
int j = 1000;

i = j * j; // overflow may occur (C89 기준! 예시가 잘 안와닿으면, j 값을 20억이라고 생각해보면 편하다)

그냥 딱 보면, 사실 코드 자체는 괜찮아 보인다. i는 long 타입 변수이고 j * j 는 1,000,000 이므로 쉽게 그 값을 저장할 수 있을것 같다. 문제는 두개의 int 값들이 곱해질때, 그 결과역시 int 타입이라는 점이다. 그러나 i * j 의 값은 기계의 종류에 따라 int값에 담기에 너무 크기때문에 오버플로우를 야기한다. 다행히도, 캐스팅이 이런 문제를 막아줄 수 있다.

 

i = (long) j * j;

캐스팅 연산자가 * 보다 우선순위가 앞서기때문에, 첫 번째 j가 먼저 long으로 바뀌고 그로 인해 두번째 j 가 long으로 바뀌게 한다. 

i = (long) (j * j);

위 문장의 문제를 알겠지? 그렇지? 왜냐하면 캐스팅 되기 전에 이미 j * j에서 오버플로우가 일어나기 때문이다.

 

7.5 Type Definitions

5.2장에서 봤듯이 우리는 Boolean 타입의 매크로를 만들기 위해 #define directive 를 사용했었다.

#define BOOL int

 그런데 이보다 더 좋은 방법이 있다. 바로 type definition 을 쓰는것이다.

 

typedef int Bool;

정의되는 타입의 이름이 마지막에 위치한다는것을 짚고 넘어가자. 또한 bool 로 적지 않고 Bool 로 적었다는것도 짚고 넘어가자. 사실 첫 글자를 대문자로 하는것은 필수사항은 아니지만, C프로그래머들이 채택하는 규약(convention) 이다.

typedef 를 사용해 Bool 을 정의하는것은 컴파일러가 갖고있는 타입 목록에 Bool 을 추가했다는 것을 의미한다.

Bool 은 이제 내장형 타입과 똑같이 쓸 수 있다. 이를 테면 캐스팅, 변수 선언, 등등 에서 말이다. 

Bool flag; // same as int flag;

컴파일러는 Bool 을 int와 같은 것으로 본다. 따라서 flag는 사실 그냥 int 변수와 다를게 없다.

 

Advantages of Type Definitions

타입 정의는 프로그램을 좀 더 이해가능하게 만든다(프로그래머가 의미있는 타입 이름을 정했다는 가정하에).

예를 들면, 변수 cash_in 과  cash_out 이 달러의 양을 저장하기 위해 만들어진 변수라고 해보자. 그렇다면,

typedef float Dollars;

그러고나서

Dollars cash_in, cash_out;

이렇게 적는 것이 다음의 예보다 훨씬 유용한 코드이다.

float cash_in, cash_out;

또한 typedef 는 프로그램을 수정하기에 더 용이하도록 만든다. 우리가 만약 Dollars를 double 로 써야할 상황이 온다면, 우리는 딱 한개만 해주면된다. 바로 type definition을 수정하는 것이다.

typedef double Dollars;

 Dollars 변수의 선언은 수정하지 않아도 된다! 만약 typedef 가 없었다면 우리는 모든 달러변수의 선언부를 수정해야 할것이다.

 

Type Definitions and Portability

typedef 는 이식성이 훌륭한 프로그램을 작성하는데 중요한 도구이다. 한 컴퓨터에서 다른 컴퓨터로 프로그램을 옮길 때 발생하는 문제점 중 하나는 타입들이 기계마다 다른 범위를 갖는다는 것이다. 만약 i 가 int 변수라면, 

i = 1000000; 와 같은 대입 연산은 32 - bit 정수에서는 괜찮으나 16 - bit 정수에서는 문제가 발생할 것이다. 

 

Portability tip : For greater portability, consider using typedef to define new names for integer types.

 

0 부터 50000까지 제품의 양을 저장해야 하는 변수를 필요로 하는 프로그램을 작성한다고 해보자.

우리는 long 타입의 변수를 쓸 수 있다. 왜냐하면, long타입을 사용하면 적어도 21억 까지는 보장이 되니 말이다. (C89 기준이라 그런듯 합니다.) 그러나 우리는 int 를 쓸 것이다. 왜냐하면, int 변수의 산술 연산이 long 변수의 산술연산보다 빠르기 때문이다. 또한, int 변수는 공간을 덜 차지한다.

int타입으로 제품의 양을 나타내는 변수를 선언하는것보다 우리는 우리만의 "(제품의)양" 타입을 선언할 수 있다.

typedef int Quantity;

그리고 이런 타입을 변수를 선언할때 쓸 수 있다.

Quantity q;

우리가 만약 더 짧은 정수타입으로 프로그램을 다루는 기계로 프로그램을 옮긴다면, 우리는 Quantity 의 정의를 바꿀 수 있다. 

typedef long Quantity;

이런 기술은 사실 우리의 문제를 전부 해결해주진 않는다. 왜냐하면 Quantity 의 정의를 바꾸는 것은 Quantity 변수가 사용되는 방향에도 영향을 주기 때문이다. 다른건 몰라도 Quantity 변수를 사용하는 printf 와 scanf 는 반드시 바뀌어야 한다. 변환지정자를 %d 에서 %ld로 하면 되겠지? 그렇지?

 

C의 라이브러리는 C의 구현마다 다를 수 있는 타입의 이름들을 만들기 위해 그 자체가 typedef 를 쓴다. 이런 타입들은 prtdiff_t , size_t 혹은 wchar_t 처럼 보통 _t로 끝나는 이름을 가지고 있다. 

(The C library itself uses typedef to create names for types that can vary from one C implementation to another; these types often have names that end with _t. such as ~)

정확한 타입의 정의는 바뀔 수 있지만, 여기 몇가지 예시가 있다.

typedef long int ptrdiff_t
typedef unsigned long int size_t;
typedef int wchar_t;

C99 에서는 <stdint.h> 헤더가 비트의 갯수에 따라 정수타입의 이름을 정의하기 위해 typedef 를 사용한다.

예를 들어, int32_t 는 정확히 32비트를 사용하는 부호있는 정수의 타입이다. 이러한 타입들을 사용하는 것은 이식성이 좋은 프로그램을 만드는데 효율적인 방법이다.

 

7.6 The sizeof Operator

sizeof 연산자는 어떤 특정 타입의 값을 저장하는데 얼마나 많은 메모리가 요구되는지 알려준다.(The sizeof operator allows a program to determine how much memory is required to store values of a particular type)

 

sizeof  (  type - name  )

 

표현식의 값은, type - name 타입의 값을 저장하는데 사용되는 바이트의 개수를 나타내는 부호없는 정수이다. 

sizeof ( char ) 는 항상 1이다. 그러나 다른 타입의 사이즈는 변할 수 있다. 32비트 머신에서는 sizeof(int) 는 보통 4일것이다. sizeof 는 보통의 연산자들과는 다르다는 것을 짚고 넘어가자. 왜냐하면, 컴파일러 그 자체가 sizeof 표현식의 값을 결정하기 때문이다. sizeof 연산자는 상수, 변수, 표현식 모두에 적용될 수 있다. sizeof(i) 와 sizeof(i + j) 모두 32비트 머신에서는 4 일것이다. 사실 표현식에 쓰일때는 소괄호를 필요로 하지 않는다. 다시 말해, sizeof(i) 와 sizeof i 모두 같은 표현이다. 다만 sizeof i + j 이런 경우만 조심하면된다. 이 경우 sizeof 연산자는 단항 연산자이기 때문에 sizeof(i) + j 가 계산되어 엉뚱한 값이 나올 수 있다!

 

sizeof 값을 출력하는 것은 약간의 주의를 필요로 한다. 왜냐하면, sizeof 표현식의 타입이 implementation - defined 타입인 size_t 타입이기 때문이다. 

C89에서는 정수 타입 중 가장 큰 타입인 unsigned long 타입으로 캐스팅 후 %lu 로 출력하는 방식을 썼다.

printf("Size of int : %lu\n", (unsigned long) sizeof(int));

그런데 C99 에서는 위와 같이 할 수가 없다! 왜냐하면 size_t 타입이 unsigned long 보다 커질 수 있기 때문이다. 그런데, C99에서 printf 함수는 size_t 를 캐스팅없이 바로 출력할 수 있다. trick 한가지를 쓰는것인데, 변환지정자(보통 u 를쓴다) 앞에 z를 붙이는 것이다. 

print("Size of int : %zu\n", sizeof(int)); // C99 only

 

Q&A

Q : 값을 담기에 작은 타입의 변수에 큰 값을 넣으면 어떤 일이 벌어지나요?

A : 대강 설명하자면, 만약 넣을 값이 integral 타입이고 변수가 unsigned 타입이면 초과되는 부분의 비트는 다 짤려나가게 된단다. 만약 변수가 signed 타입이라면 결과는 implementation - defined 이다.(이거 이해안되면 2장 보고오기!)

 부동 소수점 숫자를 더 작은 크기의 정수나 실수 타입 변수에 넣게 됬을때 일어나는 일은 undefined - behavior 란다.

프로그램 종료처럼 그 어떠한 일도 충분히 발생할 수 있어! 

 

Q : 매크로와 typedef의 차이점이 뭔가요?

A : 거기엔 두 가지 차이점이 있네! 먼저, type definitions 은 매크로 definition 보다 훨씬 강력하다네. 특히, 배열과 포인터 타입은 매크로로 정의될 수 없네. 만약,

#define PTR_TO_INT int *

이렇게 한다면,

PTR_TO_INT p, q, r;

이 선언은 전처리를 거치면(preprocessing)

int * p, q, r;

이렇게 바뀔걸세. 운나쁘게도, p만 포인터변수라네. q와 r은 그냥 int 타입 변수야. typedef 는 이런 문제점이 전혀 없네.

두 번째로, typedef 의 이름들은 변수의 그것과 동일한 유효범위 규칙을 적용받는다네. 만약 함수안에서 typedef 를 써서 타입이름을 정의한다면, 그 타입의 유효범위는 함수 안이된다네! 매크로는 이와 다르게 어디에 나타나는 전처리기에 의해 바뀌어버리지.


휴.....여러분 7단원 엄청 기네요!!!!! 쓰는데 시간도 많이 걸렸네요 허허 ㅎㅎ

읽어주셔서 감사합니다. 다음 단원에서 뵐게요! ^^