스터디/C방장

정수형 type 이야기 [C언어 파헤치기 1]

SW PLAN B 2020. 12. 27. 03:33
728x90

제가 알고 있는 C에 관한 지식을 나눕니다.

카카오톡 오픈 채팅방에서 질문도 받습니다.

C방장 오픈채팅방 QR코드

C방장한테 질문하러 가기


■ QUIZ 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(void)
{
    int a = 0;
    int b = 1;
    if (a-< sizeof(a))
    {
        printf("Q1: %d\n", a-b);
    }
    else
    {
        printf("Q1: %d\n", b-a);
    }
 
    return 0;
}
cs
더보기
1
Q1: 1
cs

예상과 다르신가요? 그렇다면 끝까지 읽어주세요.

이 퀴즈를 완전히 이해하기 위해서는 몇 가지 알아야 할 것들이 있습니다.

1. type에 대한 이해

2. 형변환에 대한 이해

3. 다른 타입들 사이의 연산

차근차근 알아보겠습니다.

■ C언어의 정수형 타입

표준 C언어 타입

C99 기준에서 C언어의 표준 정수형(standard integer types)다섯 개의 부호있는 정수형 (signed char, signed short int, signed int, signed long int, signed long long int) 그리고 여섯 개의 부호 없는 정수형 (unsigned char, unsigned short int, unsigned int, unsigned long int, unsigned long long int, _Bool) 이 있습니다.

extended integer types는 MSVC(Microsoft Visual C) 에서 "__int32" 처럼 시스템과 컴파일러에 따라 구현된 확장된 정수형입니다. extended integer types에 대한 내용은 생략하고 넘어가겠습니다.

"character types" 에서 char, signed, unsigned char 가 있다는 점이 특이합니다. 그리고 char는 정수형이 아니고 unsigned char, signed char는 정수형입니다. 이것에 대한 이야기는 다음 포스팅에서 하도록 하고 이번에는 정수형 type에 대해서 이야기해보겠습니다.

■ 표준 C에서 정수형 타입의 범위

표준 C언어 정수형 타입 표현 범위

위의 표는 정수형 타입들이 보장해야 할 최소한의 범위최소한의 사이즈입니다.

괄호의 의미는 생략 가능한 keyword입니다. 예를 들어 signed int, signed, int는 동일한 타입입니다. 마찬가지로 signed long long, long long, long long int, signed long long int는 같은 타입입니다.

signed와 int는 공통적으로 생략 가능하다는 것을 보실 수 있습니다.

자세히 관찰하신 분은 signed int의 min size가 16 bits, 즉 2B라는 것을 눈치채셨을 겁니다. 보통 int의 사이즈는 4B라고 많이 알고 계실 겁니다. 하지만 표준 C에서 int의 최소 사이즈는 2B입니다.

C는 지금처럼 32bit, 64bit 컴퓨터가 상용화되기 전부터 사용되어 왔습니다. 그리고 C는 PC 같은 시스템 외에 임베디드 시스템에 널리 사용되는 언어입니다. 따라서 표준 C는 최소한으로 보장해야 할 범위와 사이즈를 정해놓고 그보다 큰 범위와 사이즈는 C언어의 구현에 따라 달라지는 것을 허용합니다. 다만, 대부분의 환경에서 int의 사이즈는 4B이기 때문에 int의 사이즈를 4B라고 해도 큰 무리가 없겠습니다.

■ 표준 C언어의 음수 정수 표현 방식

C언어 음수 정수 표현 방식

2진수로 음수를 표현하는 방식은 세 가지 방식이 있습니다. "부호 및 크기 방식(signed magnitued)", "1의 보수 방식(1's complement) 그리고 "2의 보수 방식(2's complement)입니다. 

signed magnitued 방식은 최상위 비트(MSB, Most Significant Bit)가 부호를 의미하고 나머지 비트들은 크기를 나타내는 방식입니다. signed magnitude의 문제점은 0의 표현 방식이 두 가지라는 점입니다. 

8bit의 상수에서 1000_0000b와 0000_0000b 모두 0을 의미하게 됩니다. 그리고 덧셈 구현이 불편해집니다.

1's complement 방식은 양수의 표현에서 모든 bit를 뒤집은 값을 음수로 표현하는 방식입니다. 1's complenent 방식은 signed magnitued에 비해 사칙연산 구현이 상대적으로 쉽지만 0의 표현이 두 가지 존재한다는 단점이 여전히 남아있습니다. (1's complement방식에서 0000_0000b와 1111_1111b 모두 0을 나타냅니다.)

2's complement 방식은 1의 보수에 1을 더한 값입니다. 2's complement 방식은 0의 표현이 하나이고 사칙연산 구현이 다른 방식에 비해 간단하기 때문에 대부분의 시스템에서 사용하는 방식입니다.

그렇다면 C의 음수 정수 표현 방식은 어떤 방식을 채택하였을까요?

표준 C는 음수 정수 표현 방식을 제한하지 않았습니다. 시스템에 따라 음수의 표현 방식은 달라지게 됩니다.

대부분의 시스템은 2's complement를 사용하기 때문에 이어지는 내용들은 2's complement 사용을 전제합니다.

■ 형변환 (conversion)

형변환은 암시적 형변환(implicit conversion)명시적 형변환(explicit conversion)으로 나누어 설명합니다. 

서로 다른 타입끼리 연산이 일어날 때 암시적 형변환은 일어납니다. 이때 연산은 '+', '-' 같은 연산과 '=' 같은 대입, '<' 같은 비교 연산을 포함하며 함수의 parameter로 전달될 때 argument는 parameter의 형식으로 암시적 형변환이 일어납니다.

1
2
3
4
5
6
7
8
9
10
11
12
signed char sum(signed char a, signed char b)
{
    return a + b;
}
 
int main(void)
{
    printf("%d", sum(12));
 
    return 0;
}
 
cs

위 예에서 정수형 상수 1과 2는 argument이고 sum 함수에 전달되면 signed char a와 signed char b parameter의 형식으로 암시적 형변환이 일어납니다.

1
2
3
4
5
6
7
8
9
10
11
12
signed char sum(signed char a, signed char b)
{
    return a + b;
}
 
int main(void)
{
    printf("%d", sum((signed char)1, (signed char)2));
 
    return 0;
}
 
cs

명시적 형변환은 "Cast Operators"로 수행됩니다. 함수에 전달하기 전에 (signed char) 연산을 통해 명시적 형변환을 해주었습니다.

■ 형변환(conversion)

□ 사이즈가 큰 타입에서 작은 타입으로 형변환

C언어 형변환

큰 사이즈에서 작은 사이즈로 형변환이 일어날 경우 작은 사이즈 타입의 크기만큼만 데이터가 남고 나머지 데이터는 버려집니다(truncation).

□ 사이즈가 작은 타입에서 사이즈가 큰 타입으로 형변환

C언어 형변환

사이즈가 작은 타입에서 큰 타입으로 형변환이 일어날 경우 source의 type이 중요합니다. 형변환이 일어날 경우 부호가 유지되는 방향으로 데이터가 채워지기 때문입니다.

destination의 타입이 unsigned 또는 signed 인지는 데이터가 채워지고 나서 해석하게 됩니다.

타입의 순위 (Rank)와 형변환

'+' 연산 또는 비교 연산 등에서 일어나는 암시적 형변환은 피 연산자들 중 Rank가 낮은 타입의 피연산자가 Rank가 높은 타입으로 형변환된 후 연산이 진행됩니다.

이때 정수형 타입들의 Rank는 사이즈가 클수록 큰 Rank를 갖습니다. 예를 들어 signed short 는 unsigned char 보다 rank가 높습니다.

어떤 unsigned integer type과 대응하는 signed integer type은 rank가 같습니다. 예를들어 unsigned char와 signed char의 type은 같습니다.

같은 부호를 갖는 정수 타입은 사이즈가 큰 타입으로 형변환 됩니다.

부호가 같고 크기가 같다면 unsigned 타입으로 형변환됩니다. 예를 들어 signed int와 unsigned int의 연산은 unsigned int로 형변환되어서 수행됩니다.

더 자세한 내용은 별도의 포스팅에서 다루도록 하겠습니다.

■ 정수 승격 (integer promotion)

int와 unsigned int 보다 낮은 rank의 타입 간의 연산은 각각 int로 형변환을 한 후 연산을 하게 됩니다.

다음 정수 승격 예시 코드를 보겠습니다.

1
2
3
4
5
6
7
int main(void)
{
    signed char a = 0xf0;
    unsigned char b = 0xf0;
    int c = a - b;
    printf("0x%x\n", c);
}
cs

출력 결과

0xffffff00

왜 이런 결과가 나왔을까요?

a - b 연산 순서

1. signed char a 가 int 타입으로 형변환 되면서 0xfffffff0가 됩니다.

2. unsigned char b가 int 타입으로 형변환 되면서 0x000000f0가 됩니다.

3. 형변환된 두 값의 차이는 0xffffff00 이 int 타입 변수 c에 대입됩니다.

결국 위 결과로 출력 결과는 0xffffff00이 됩니다. 같은 값처럼 보이지만 연산 결과는 정말 엉뚱한 값이 되었습니다. 안전한 개발을 위해 되도록이면 서로 다른 타입의 변수들의 연산은 피하고 불가피하다면 명시적 형변환을 해주는 것이 예상 범위 내의 결과를 얻을 수 있겠습니다.

■ sizeof() 연산

sizeof() 연산의 결과는 unsigned int입니다.

■ QUIZ1 해설

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int main(void)
{
    int a = 0;
    int b = 1;
    if (a-< sizeof(a))
    {
        printf("Q1: %d\n", a-b);
    }
    else
    {
        printf("Q1: %d\n", b-a);
    }
 
    return 0;
}
cs

지금까지 내용을 이해했다면 이제 위의 결과가 왜 1이 출력되는지 이해되셨으리라 믿습니다.

위 프로그램의 연산 순서는 다음과 같습니다.

□ a - b

1. a, b는 같은 타입이기 때문에 형변환이 일어나지 않고 연산되고 그 결과는 (signed) int 타입의 -1입니다.

□ sizeof(a)

1. int 타입인 a의 사이즈는 4입니다. (MSVC에서)

2. sizeof()의 연산 결과는 unsigned int 이므로 sizeof(a)는 unsigned int 타입의 4 입니다.

□ a - b < sizeof(a)

1. 좌변의 타입은 signed int이고 우변의 타입은 unsigned int 이기 때문에 unsigned int로 형변환되어 비교 연산이 수행됩니다.

2. signed int -1이 unsigned int로 형변환 되면 0xfffffff가 되고 4와 비교했을때 더 큰 값입니다.

3. if (a-b < sizeof(a))는 거짓이 되고 b-a 즉, 1이 출력됩니다.

이번 퀴즈에서 교훈은 sizeof()와 음수 비교는 위험하다는 것입니다. 실수를 방지하기 위해 되도록이면 sizeof() 연산 결과를 비교하는 데 사용하는 것을 지양해야 합니다.

■ Note

사실 실제 개발을 하면서 이런 복잡한 타입, 형 변환을 고려해서는 안됩니다. 이런 복잡한 코드를 짠다면 그것은 자기 과시에 지나지 않다고 생각합니다. 

우리는 타입과 형변환이 복잡하다는 것을 알고 이런 복잡한 것을 고려하지 않아도 되는 코드를 짜는 것이 더 중요할 것 같습니다.


C언어 글 목록 보기

 

'스터디/C방장' 카테고리의 글 목록

일상의 모든것

sw-daily.tistory.com



읽어주셔서 감사합니다.

#태그를 이용하시면 제 블로그에 있는 비슷한 주제의 글들을 보실 수 있습니다.

반응형