1.1. 원시 데이터 타입

C/C++언어에는 타입이 있어 데이터의 타입을 지정하고 지정한 타입으로 데이터들을 표현, 저장합니다.

타입은 원시 데이터 타입과 복합 데이터 타입으로 나뉠 수 있습니다.


“원시 데이터 타입”(Primitive Data Type) : 언어가 기본적으로 제공하는 정형 데이터 타입이며,

고정 공간을 선언해서 사용하는 형태로 주로 메모리 스택에 들어가는 데이터 타입 입니다.


“복합 데이터 타입”(Composite Data Type) : 언어가 제공하거나 사용자가 임의로 지정하는 비 정형 데이터 타입이며,

임의 지정이나 동적인 할당의 이유로 유동적인 공간을 선언하게 되고 주로 메모리 힙에 들어가는 데이터 타입 입니다.


C/C++에서 대표적인 원시 데이터 타입으로 9 가지가 있습니다.

1). char : 문자 데이터를 표현하는 타입으로, 고정 1바이트 크기를 가지며
부호 있는(-128부터 127까지) 또는 부호 없는(0부터 255까지) 값을 가질 수 있습니다.

C
char a = 'A';

2). int : 정수 데이터를 표현하는 타입으로, 대개 고정 4바이트 크기를 가지며
대부분의 플랫폼에서는 -2,147,483,648부터 2,147,483,647까지의 값을 가질 수 있습니다.

C
int a = 1;

3). float : 4바이트 부동 소수점 숫자를 표현하는 타입으로, 대략 7자리의 정밀도를 가지며
약 -3.4E+38부터 3.4E+38까지의 범위를 가집니다.

C
float a = 1.1;

4). void : 어떤 값도 표현하지 않는 타입으로, 주로 함수의 반환 타입이나 포인터의 타입(void Pointer)으로 사용됩니다.

C
void a() {} // 함수의 리턴을 void로 할 수 있습니다.
// 함수는 이후에 자세하게 설명되어 있습니다.

void *a; // void 포인터는 이후에 자세하게 설명되어 있습니다.

5). double : 8바이트 부동 소수점 숫자를 표현하는 타입으로, 대략 15자리의 정밀도를 가지며
약 -1.7E+308부터 1.7E+308까지의 범위를 가집니다.

C
double a = 1.1;

6). short : 짧은 정수 데이터를 표현하는 타입으로, 대개 고정 2바이트 크기를 가지며
대부분의 플랫폼에서는 -32,768부터 32,767까지의 값을 가질 수 있습니다.

C
short a = 1;

7) long int : 긴 정수 데이터를 표현하는 타입으로, 대개 고정 4바이트 또는 8바이트 크기를 가지며
대부분의 플랫폼에서는 -2,147,483,648부터 2,147,483,647까지의 값을 가질 수 있습니다. (int를 생략해도 동일합니다.)

C
long a = 1;

8) long long int : int, long 보다 긴 정수 데이터를 표현하는 타입으로, 고정 8바이트 크기를 가지며
대부분의 플랫폼에서는 -9,223,372,036,854,775,808부터 9,223,372,036,854,775,807까지의 값을 가질 수 있습니다.
(int를 생략해도 동일합니다.)

C
long long a = 1;

위의 타입에서 unsigned를 접두사로 붙이면, 같은 고정 바이트에 부호비트가 빠지고
값의 범위가 0부터 양수 값의 두 배가 되는 데이터 타입이 됩니다. ( void는 제외 )

C
unsigned char a = 'A';
unsigned int b = 1;
unsigned float c = 1.1;
unsigned double d = 1.1;
unsigned short e = 1;
unsigned long long f = 1;

C++언어는 C언어의 타입을 모두 포함 하면서 추가적으로 정수형에 비트 수를 지정하여 할당 할 수 있습니다.

C++
int8_t a = 0; // 1byte
int16_t b = 0; // 2byte
int32_t c = 0; // 4byte
int64_t d = 0; // 8byte
uint8_t e = 0; // 1byte unsigned
uint16_t f = 0; // 2byte unsigned
uint32_t g = 0; // 4byte unsigned
uint64_t h = 0; // 8byte unsigned

또한, C++언어는 bool 타입을 지원합니다.

C언어도 bool사용이 가능하지만, 헤더 파일<stdbool.h>을 따로 추가 해줘야 합니다. (#include<stdbool.h>)

bool : 이진 데이터를 표현하는 타입으로, 고정 1바이트 크기를 가지며, true(1), false(0) 으로 두 가지 값만 존재합니다.

C++
bool a = true;

1.2. 정수 타입

정수형 타입 int류에 대해 좀 더 자세히 배워봅시다.

C
int a = 11;

예제 코드는 int 타입 변수 a 에 11을 할당하는 문법으로 정수타입은 기본적으로 십진법을 사용합니다.

만약, 십진법이 아닌 2진법 8진법 16진법으로 값을 넣고 싶다면 다음과 같이 작성할 수 있습니다.

C
int a = 0b11; // 2진법 11을 대입
int b = 011; // 8진법 11을 대입
int c = 0x11; // 16진법 11을 대입

정수 타입은 주로 int : 4바이트, long long : 8바이트인 두 가지를 자주 사용합니다.

4바이트 int 는 32비트 이므로 32자리 비트열로 정수를 표현하는데

맨 좌측 1비트는 부호 비트로 0이면 양수이고 1이면 음수를 나타 냅니다.

그래서 수의 범위는 \((2^{31}-1)\) ~ \((-2^{31})\) 이 됩니다.

C
#include<stdio.h>
int main()
{
    int a = 0b00000000000000000000000000000000;
    printf("%d\n",a);

    int b = 0b01111111111111111111111111111111;
    printf("%d\n",b);

    return 0;
}

출력 결과는 다음과 같습니다.
(printf는 이후에 자세하게 설명되어 있습니다. 지금은 a와 b를 콘솔에 출력하는 코드라고 이해하면 충분합니다.)

Bash
0
2147483647

그렇다면, 부호비트가 1인 음수에 대해서는 값이 어떻게 정의 될까요?

위의 내용으로 보아서 부호비트만 1로 두면 -0, 전부 1이면 -2147483647이 될 것 같습니다.

하지만, 실제로 넣어보면 완전히 다르게 정의 되는 것을 볼 수 있습니다.

C
#include<stdio.h>
int main()
{
    int a = 0b10000000000000000000000000000000;
    printf("%d\n",a);

    int b = 0b11111111111111111111111111111111;
    printf("%d\n",b);

    return 0;
}

출력 결과는 다음과 같습니다.

Bash
-2147483648
-1

이유는 음수의 값은 2의 보수로 저장되기 때문에 이 현상이 생기는 것 입니다.

2의 보수를 이해하기 전에
먼저 1의 보수는 어떤 수를 해당 값에 더하여 각 자리수가 모두 1이 되도록 하는 수를 말합니다.
예를 들어 01100111의 1의 보수는 10011000이 됩니다. 즉, 비트를 반전(NOT)한 결과를 1의 보수라고 표현합니다.

그렇다면, 2의 보수는 1의 보수 결과에서 +1을 한 결과가 2의 보수가 됩니다.
위의 예시를 그대로 적용하자면, 01100111의 2의 보수는 10011000 + 1 = 10011001이 됩니다.

그렇다면 -1은 일단 부호비트가 1일 것이고, 남은 31비트는 2의 보수로 저장되므로

C
int a = 0b10000000000000000000000000000001;
// a 에서 부호비트를 제외한 31비트를 2의 보수하면,
a = 0b11111111111111111111111111111110 + 1;
// a 가 되므로, -1은 
a = 0b11111111111111111111111111111111;
// a 가 됩니다.

그래서 모두 1이면 -1이 되고,

부호비트만 1이면, 0값의 2의 보수 이므로 -0이지만, 0은 모두 0인 값으로 정의 되어있으므로,
-0 대신 오버플로우 된 2147483648값의 2의 보수로 생각하여 -2147483648이 됩니다.
(오버플로우는 값이 초과됬다는 의미입니다.)

C
int a = 0b10000000000000000000000000000000;
// a는 0이지만, 값이 초과된 2^32로 보고 31자리를 2의 보수하면,
a = 0b11111111111111111111111111111111 + 1;
// a 가 되므로, 오버플로우가 되어(값이 초과되어)
a = 0b10000000000000000000000000000000;
// a 가 됩니다.

그래서 부호비트만 1이면 -2147483648이 됩니다.


1.3. 실수 타입

실수형 타입 float류에 대해 좀 더 자세히 배워 봅시다.

C
float a = 1.1;

예제 코드는 float 타입 변수 a에 1.1값을 할당하는 문법으로 엄밀하게는 1.1f를 할당합니다.

만약, float 보다 더 큰 범위의 실수값을 대입하려면 1.1L 을 사용하여 값을 할당하면 됩니다.


실수 타입은 float : 4바이트, double : 8바이트 인 두 가지를 사용합니다.

4바이트 float 는 32비트 이므로 32자리 비트열로 실수를 표현하는데

맨 좌측 1비트는 부호 비트로 0이면 양수이고 1이면 음수를 나타 냅니다.

그리고 실수형은 부동 소수점을 사용하여 표현하는데 부동 소수점은 소수점을 고정하는 것이 아니라

소수점의 위치를 정하는 지수부와 소수점에 관계없는 가수부를 나누어 값을 표현합니다.

float의 지수부는 8비트를 사용하고 가수부는 23비트를 사용합니다.

C
#include<stdio.h>
#include<string.h>
int main()
{
    // 좌측부터 1비트는 부호비트, 8비트는 지수부, 23비트는 가수부가 됩니다.
    float a;
    
    unsigned int source_a = 0b00000000000000000000000000000000;
    memcpy(&a, &source_a, sizeof(float));
    
    printf("%f\n", a);
    
    float b;
    
    unsigned int source_b = 0b01111111011111111111111111111111;
    memcpy(&b, &source_b, sizeof(float));
    
    printf("%f\n", b);
    
    return 0;
}

위의 코드는 실수형 타입에 비트열을 그대로 넣는 방법입니다.

같은 크기의 정수형 타입에 값을 먼저 넣고 (정수형 타입은 비트열이 그대로 들어갑니다.)

memcpy( [비트 값을 붙여넣기 할 주소], [비트 값을 복사할 주소], [붙여넣기 할 공간의 크기] ) 함수를 사용하여
비트열을 그대로 넣을 수 있습니다.

만약, 위의 과정없이 float에 바로 할당한다면, 비트 값이 정수형 타입 값으로 바뀌고

해당 정수형 타입 값이 float 타입에 맞게 변환 되므로 의도와는 다른 값이 들어가게 됩니다.

(한 번 직접 넣어서 테스트 해보시는 것을 권장합니다.)

추가로 주소 값에 관해서는 포인터 파트에서 자세하게 다룰 예정이므로 지금은 memcpy 사용방법 정도로 이해하시면 충분합니다.


출력 결과는 다음과 같습니다. ( 동일하게 printf는 a와 b를 콘솔에 출력하는 코드로 이해하시면 충분합니다. )

Bash
0.000000
340282346638528859811704183484516925440.000000

a는 0으로 정의하고, b는 float의 최대 값으로 정의 합니다.

그 이유는 먼저 a의 경우, 일반적으로 지수부는 1이상을 원칙으로 사용하지만, 0인 경우도 정의가 되어 있습니다.

일단 지수부가 1인 경우는 \(2^{bias=-127} * 2^{지수부 = 1} * 1.[가수부]\)가 됩니다.

예를 들어 지수부가 1이고 가수부가 0이면, \(2^{-126}\)이 될 것입니다.

C
#include<stdio.h>
#include<string.h>
int main()
{
    float c;
    unsigned int source_c = 0b00000000100000000000000000000000;
    memcpy(&c, &source_c, sizeof(float));
    printf("%.127f\n", c); // 소수점 아래 127자리까지 출력
    
    return 0;
}

출력 결과는 다음과 같습니다.

Bash
0.0000000000000000000000000000000000000117549435082228750796873653722224567781866555677208752150875170627841725945472717285156250

하지만, 지수부가 0이라면, \(2^{-126} * 0.[가수부]\) 의 공식이 적용됩니다.

예를 들어 지수부가 0이고 가수부가 1이면, \(2^{-126} * ( 0.00000000000000000000001 = 2^{-23} ) = 2^{-149}\) 가 됩니다.

C
#include<stdio.h>
#include<string.h>
int main()
{
    float d;
    unsigned int source_d = 0b00000000000000000000000000000001;
    memcpy(&d, &source_d, sizeof(float));
    printf("%.150f\n", d); // 소수점 아래 150자리까지 출력
    
    return 0;
}

출력 결과는 다음과 같습니다.

Bash
0.000000000000000000000000000000000000000000001401298464324817070923729583289916131280261941876515771757068283889791082685860601486638188362121582031250

위의 예시는 사실 float에서 표현가능한 가장 작은 양의 실수 값입니다.

또한 이를 float의 “ε”(Epsilon)으로 표현하며, 최소단위, 분해능 이라고도 볼 수 있을 것입니다.

그렇다면 위의 a는 \(2^{-126} * 0.0\) 이 되므로 0임을 쉽게 알 수 있습니다.


다음은 b의 경우, 지수부가 \(256-2\)이고, 가수부가 \(2^{23}-1\)인 경우인데

지수부가 \(256-1\)이 되면 (즉, 지수부가 모두 1이 되면) 보다 더 큰 값을 유도 할 수 있을 것 같습니다.

하지만, 실제 값을 대입 해보면 다르게 나오는 것을 볼 수 있습니다.

C
#include<stdio.h>
#include<string.h>
int main()
{
    float e;
    unsigned int source_e = 0b01111111100000000000000000000000;
    memcpy(&e, &source_e, sizeof(float));
    printf("%f\n", e);

    float f;
    unsigned int source_f = 0b01111111100000000000000000000001;
    memcpy(&f, &source_f, sizeof(float));
    printf("%f\n", f);
    
    return 0;
}

출력 결과는 다음과 같습니다.

Bash
inf
nan

이유는 지수부가 \(256-2\) 까지 일 때만 정상적인 실수 값으로 사용하도록 하고

지수부가 \(256-1\) 인 경우 즉, 지수부가 모두 1인 경우는 특수 용도로 쓰는 걸로 약속 했기 때문입니다.

만약, 지수부가 \(256-2\) 인 경우는 \(2^{bias=-127} * 2^{지수부 = 256-2} * 1.[가수부]\) 가 되고,

만약, 지수부가 \(256-1\) 인 경우는 가수부가 0이면 inf를, 가수부가 0이 아닌 값이면 nan이 됩니다.

그렇다면 위의 b는 \(2^{bias=-127} * 2^{ 256-2 } * 1.11111111111111111111111 = 2^{127} * (2-2^{-23})\)

따라서, b 값은 \(2^{128} – 2^{104}\) 이고 float의 가장 큰 값이 됩니다.


\(2^{-149}\)에서 부터 \(2^{128}-2^{104}\)까지 32비트로 표현되면서도 양수와 음수도 표현 가능하고
정수에서 실수까지 표현 가능한 float이 int보다 뛰어난 범위를 제공하는 것 처럼 보입니다.

하지만, float의 경우 제한된 실수 범위의 값을 표현 가능하기 때문에 실제 소수점을 사용하다 보면
오차가 조금씩 눈에 띄게 나는 편입니다.

또한, 튜링머신 특성상 유한집합만을 정확히 표현 할 수 있기 때문에 (가산, 비가산) 무한집합을 정의 해야 할 때,
반드시 한계점이 찾아올 수 밖에 없습니다. ( 무한시간 소모, 유한한 분해능의 문제 )

float의 부호비트 값을 \(a\), 지수부 비트 값을 \(b\), 가수부 비트 값을 \(c\)라고 할 때,

float의 값을 수학적으로 정의 해본다면 다음과 같을 것입니다.

\(float(a, b, c) = \begin{cases} (1-2a)2^{b-127}(1 + 2^{-23}c), & \mbox{if }0<b<255 \\ (1-2a)2^{-149}c, & \mbox{if }b=0 \\ (1-2a)∞ & \mbox{if }b=255 ∧ c=0 \\ (1-2a)ε & \mbox{if }b=255 ∧ c≠0 \end{cases} \)

1.3. 문자 타입

문자형 타입 char류에 대해 좀 더 자세히 배워 봅시다.

C
char a = 'A';

예제 코드는 char 타입 변수 a에 ‘A’ 값을 할당하는 문법입니다.

문자형 타입의 경우 다양한 문자들을 표현하기 위해 다양한 “유니코드”(unicode)를 지원 하지만,

가장 기본적으로 프로그램 코드와 컴파일 문법은
“ASCII”(American Standard Code for Information Interchange)을 사용합니다.

ASCII코드 테이블은 다음과 같습니다.

아래는 0 부터 32 , 127에 대한 설명을 나열 한 것입니다.


문자형 타입 char는 고정 1바이트를 가지고 순서대로 1비트를 부호비트로 남은 7비트를 ASCII코드로 사용합니다.

C
#include<stdio.h>
#include<string.h>
int main()
{
    char a = 0b01000001; // 65 = A
    printf("%c\n", a);

    int b = 0b01100001; // 97 = a
    printf("%c\n", b);

    return 0;
}

출력 결과는 다음과 같습니다. ( 위의 printf는 a와 b를 ASCII문자로 출력하라는 명령으로 이해하시면 충분합니다. )

Bash
A
a

위에서 볼 수 있듯 문자형 타입 char 와 정수형 타입 int는 바이트의 차이를 제외하고
본질적으로 같은 행동을 취할 수 있습니다. ( 그렇기에, char도 정수형과 같이 정수로도 출력 할 수 있을 것입니다. )


int의 범용성을 두고도 char를 쓰는 이유는 당연히 메모리 공간 효율성 측면에서 큰 이득을 벌 수 있기 때문에

“문자”를 쓰는 것이 정해진다면 char를 사용하는 것이 좋습니다. 또한, C/C++는 Type Unsafe 언어이기 때문에

이런 문자형 타입과 정수형 타입의 경계가 모호한 것을 단점으로 바라보는 입장이 대다수입니다.

(과도하게 자유로운 문법사용이 난해한 코드를 만들 수 있고, 이는 버그 수정등에 어려움을 겪을 수 있기 때문입니다.)

필자는 모든 특징에는 장단점이 있으니 경계가 모호함을 인지하고 효율적으로 작성하는 것이 좋다고 생각합니다.


Leave a Reply

Your email address will not be published. Required fields are marked *