정수형 타입들과 혼동할 수 있는 동작

기술노트 12582

아키텍처:

All

컴포넌트:

general

업데이트:

2021-05-27 오전 6:43

소개

ISO/ANSI C 표준이 가끔 혼동할 만한 동작을 일으키는 경우가 있습니다. 다음 본문에서 표준을 참고하고, 몇가지 예시와 간략한 기술적인 배경에 대해 소개합니다.

정수 승격(Integral Promotion)

6.2.1.1에 따르면, "char, short int, 또는 int 비트-필드, 이들의 signed, unsigned 형태들, 또는 열거형(enumeration)"은 표현식에 사용될 때 정수 승격(integral promotion)에 의해 int형 또는 unsigned int형으로 변환이 일어난다. 대부분의 경우 표현식으로부터 수학적으로 예상할 수 있는 결과를 가져다준다. 예:

char c1,c2,c3;
c1=0xA
c2=0xB;
c3=c1+c2;

정의에 의해, 16비트 int형을 가지고 컴파일러는 int형 덧셈을 진행한 뒤, 결과 값을 절삭하여 char형으로 변환한다. 0x0A + 0x0B --> 0x000A+ 0x000B --> 0x0015 --> 0x15.

예시에서 보여주듯 결과값은 목적 자료형으로 잘 맞습니다. 이것이 아닌 경우, 상황은 조금 더 복잡해집니다. 컴파일러 최적화와 관련하여, 대부분의 유사한 경우에서 낮은 정밀도로 작업이 수행될 것입니다. (이것이 결과에 영향을 미치지 않는 상황에서, 아래의 크기 최적화를 참고하세요.)

정수형 상수 타입(Integer Constant types)

6.1.3.2에 따르면, 비접미사 정수형 상수에 대해서, 타입(type)은 실제 값과 쓰여진 그 숫자대로 정해진다. 16진수형 상수에 대해선, 타입은 다음 목록에서 그 값을 나타낼 수 있는 첫 번째 형식입니다: int, unsigned int, long int, unsigned long int. 흥미로운 점은 가장 작은 타입이 char이 아니라 int라는 점입니다. 예:

char c1;
c1=0xA

16비트 int형에 대해 컴파일러는, 대입을 할때 절삭이 필요합니다. 0xA는 실제로 0x000A이고 char에 대입하기 위해 잘려야 합니다. 다시, 컴파일러 최적화와 관련하여, 대부분의 유사한 경우에서 낮은 정밀도로 작업이 수행될 것입니다. (이것이 결과에 영향을 미치지 않는 상황에서, 아래의 크기 최적화를 참고하세요.)

크기 최적화(Size optimizations)

결과가 동일하는 한, 컴파일러는 실제 표현식을 평가할 때 다른 정밀도를 사용하는데 여유롭습니다. 섹션 5.1.2.3과 비교하여, "The least requirements on a conforming implementation are:" 문장 다음에 오는 리스트에 주목 바랍니다. 최소 요구 사항에 대한 개요가 드러나는 곳입니다. 예제 리스트 또한 주목하기 바랍니다.

혼동할 수 있는 동작(Possibly confusing behavior)

정수형 타입의 규칙과 혼동할 만한 동작을 일으키는 정수형 변환에 대한 상황이 있습니다. 주의할 상황은 다른 크기의 타입 및/또는 논리 연산(특히 논리 부정)을 수반하는 과제 및/또는 조건입니다(표현식을 테스트). 여기에는 상수의 타입도 포함됩니다.

일부 경우 경고가 발생할 수 있습니다. (예, "constant conditional", "pointless comparison"), 다른 이에게는 기대했던 것과는 다른 결과일 뿐입니다. 만약 컴파일러가 일정 조건의 몇몇 인스턴스를 식별하기 위해 최적화에 의존한다면, 어떤 상황에서는 컴파일러가 더 높은 최적화에서만 경고할 수도 있습니다. 예,

예제 1:

8비트 char을 16비트 int로 추정, 2의 보수.

void f1(unsigned char c1)
{
if (c1 == ~0x80)
;
}

이 테스트는 항상 false 입니다!

설명 : 오른편(Right Hand Side): 0x80는 실제로는 0x0080입니다. 또한 ~0x0080 연산의 결과는 0xFF7F입니다. 왼편(LHS): c1은 8비트의 unsigned character형이므로, 255보다 클 수 없습니다. 또한 음수도 될 수 없는데, 정수 승격 값으로 상위 8비트를 설정할 수 없습니다.

예제 2:

8비트 char을 16비트 int로 추정, 2의 보수.

void f2(void)
{
char c1;
c1= ~0x80;
if (c1 == ~0x80)
;
}

다시 언급하자면, 대입 연산시 논리부정은 int형 객체에서만 수행됩니다. 이 값은 char에 할당 됩니다. 예, char는 (양수의)0x7F 값을 가질 것입니다. 0xFF7F와 비교해봤을 때, 이러한 조건에선 0x007F로 정수 승격이 일어납니다. - 역시 테스트는 실패. 만약 char 자체가 signed 라면, 만약 상수가 깨끗한 상위 비트를 가진다면(0x00-0x7F 범위의 어떤 값이든), 비트 논리부정 값은 음수로 바뀌고, 테스트는 성공적이였을 것입니다(아래 참조).

예제 3:

8비트 char을 16비트 int로 추정, 2의 보수.

void f3(void)
{
signed char c1;
c1= ~0x70;
if (c1 == ~0x70)
;
}

다시 언급하자면, 대입 연산시 논리부정은 int형 객체에서만 수행됩니다,
예, ~(0x0070) --> 0xFF8F. 이 값은 char에 할당 됩니다, 예, char형은 0x8F 값을 가질 것입니다. 이러한 조건에서 당시의 0xFF8F로의 정수 승격/부호 확장과 일치하고 테스트가 작동합니다.

예제 4:

8비트 char을 16비트 int로 추정, 2의 보수.

void f4(void)
{
signed char c1;
signed int i1;
i1= 0xFF;
c1= 0xFF;
if (c1 == i1)
;
}

첫 할당에서 i1은 255가 되고, 반면 두 번째 할당의 c1은 255가 목적 타입 signed char에 맞지 않습니다. 따라서 이 테스트를 성공시키려고 하지 마시길 바랍니다. c1은 명시적 signed char로서 이 문제에선 분명하지만, 순수한 char형이 사용되었는지, 기본적으로 signed char인지 탐지하기는 더 어렵습니다.

예제 5:

void CompilerTest(void)
{
int64_t a,b;
int16_t c,d,e;
c=0x789F;
d=0x7FFF;
e=0x7777;

a=c*d*e; //Wrong result: 0xFFFFFFFFC52A8517
b=c*d;
b=b*e; //Correct result: 0x00001C24C52A8517

}

정수 승격이후 '(c*d)*e' 표현식은 '((int) c * (int) d) * (int) e'로 바뀌게 됩니다. 이 표현식의 결과는 'a'에 저장되기 이전에 묵시적으로 'long long'으로 변환됩니다.
'long long'연산으로 강제하는 한 방법은 내부 피연산자중 하나를 명시적으로 바꾸는데, 'd'를 예로 들면 '(c*(long long)d)*e'로 바꿉니다. 내부 연산을 'long long'으로 수행하게 강제하여 'long long'으로 결과를 생성합니다. 결과는 외부 연산에 대한 피연산자가 되고, 따라서 외부 연산 'long long'으로 수행됩니다.

 

모든 제품 이름은 해당 소유자의 상표 또는 등록 상표입니다.

죄송하지만, 당사 사이트에서는 Internet Explorer를 지원하지 않습니다.보다 편안한 사이트를 위해 Chrome, Edge, Firefox 등과 같은 최신 브라우저를 사용해 주시길 부탁드립니다.