C#

[C# 기본 정리 2] 변수, 힙, 데이터 형식, 오버플로우와 언더플로

sunlight-dby 2025. 4. 3. 00:05

 

'이것이 C#이다 개정판'의 chapter 3 정리한 글입니다.

 


변수 (Variable)

C#에서는 변수의 초기화를 강제합니다.

초기화되지 않은 변수를 사용하면 컴파일러가 에러 메시지를 내면서 실행 파일을 만들어주지 않습니다.

 

 

힙 (Heap)

힙은 스택과 달리 저장된 데이터를 스스로 제거하는 메커니즘을 가지고 잊지 않습니다.

대신 CLR의 가비지 컬렉터(Garbage Collector)가 존재합니다. 더 이상 데이터를 참조하는 곳이 없을 때 가비지 컬렉터가 사용되지 않는 데이터의 메모리를 제거하는 역할을 하게 됩니다.

 

힙을 사용하는 이유를 간단하게 말하자면, 스택 메모리 영역은 코드 블록이 사라지는 시점에 제거되기 때문에, 이러한 한계를 없애고 싶을 때 사용합니다.

프로그래머가 데이터를 원하는 동안 살리고 싶을 때, 또 다른 메모리 영역을 CLR이 제공하는 것입니다.

 

데이터 형식 : 정수형

메모리의 효율적으로 사용하기 위해, 자료형의 크기와 범위를 고려해야 합니다.

데이터 형식 설명 크기(Byte) 담을 수 있는 값의 범위
byte 부호 없는 정수 1 (8bit)   0 ~ 255
sbyte signed byte
정수
1 (8bit)   -128 ~ 127
short 정수 2 (16bit)   -32,768 ~ 32,767
ushort unsigned short
부호 없는 정수
2 (16bit)    0 ~ 65,535
int 정수 4 (32bit)    -2,147,483,648 ~ 2,147,483,647
uint unsigned int
부호 없는 정수
4 (32bit)    0 ~ 4,294,967,295
long 정수 8 (64bit)    -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
ulong unsigned long
부호 없는 정수
8 (64bit)    0 ~ 18,446,744,073,709,551,615
char 유니코드 문자 2 (16bit)  

 

 

[2의 보수법]

1 1 1 1 1 1 1 1

 

위와 같은 비트로 이루어진 sbyte 형식의 변수는 -127과 -1중 어떤 값을 가질지를 알기 위해서는 '2의 보수법'을 알아야 합니다.

 

정답부터 말하자면, -1이 정답입니다.

 

가장 앞의 비트가 부호 비트라고 생각을 하면, 정답은 -127이 되야하지 않나라고 생각할 수 있지만, 그렇지 않습니다.

Signed 변수는 MSB(Most Significant bit)라고 하는 양수, 음수를 표현하는 부호 비트 1bit를 가집니다.

 

하지만, -127이 아닌 이유는 sbyte는가 '2의 보수법'이라는 알고리즘을 채택하여 음수를 표현하기 때문입니다.

※ sbyte뿐 아니라, short, int, long의 경우에도 2의 보수법 알고리즘을 채택한다.

 

[2의 보수법을 이용해서 음수를 표현하는 방법]

-1을 예로 들면,

 

① 먼저 수 부분 비트를 채운다.

0 0 0 0 0 0 0 1

 

② 전체 비트를 반전시킨다.

1 1 1 1 1 1 1 0

 

③ 반전된 비트에 1을 더한다.

1 1 1 1 1 1 1 1

 

이런 식으로 -1을 표현하게 됩니다.

 

[예제]

using System;
using System.Reflection.Metadata.Ecma335;
using static System.Console;

namespace IntegralTypes
{
    class MainApp
    {
        static void Main(string[] args)
        {
            byte a = 255;  // 0b_1111_1111 또한 동일
            sbyte b = (sbyte)a;

            WriteLine(a);  // 255
            WriteLine(b);  // -1
        }
    }
}

 

 

오버플로우(Overflow)와 언더플로우(Underflow)

[오버플로우]

각 데이터 형식의 최대값을 넘어가는 데이터를 저장할 때, 오버플로우가 발생하면서 아래와 같은 현상이 발생합니다.

  • byte의 MaxValue(최대값 255) + 1
1 1 1 1 1 1 1 1 + 1

 

1 || 0 0 0 0 0 0 0 0

 

byte는 8개의 비트만 담을 수 있으므로 넘쳐 흐른 왼쪽의 비트는 버리고, 오른쪽 8개의 비트만 보관합니다.

그래서 최대값을 가진 byte형 변수가 오버플로우가 발생하면 0이 되는 것입니다.

 

[언더플로우]

각 데이터 형식의 최대값을 넘어가는 데이터를 저장할 때, 오버플로우가 발생하면서 아래와 같은 현상이 발생합니다.

  • sbyte의 MinValue(최대값 -128) - 1 → (-128) + -(1)
1 0 0 0 0 0 0 0

 

+

1 1 1 1 1 1 1 1

 

1 || 0 1 1 1 1 1 1 1

 

언더플로우의 상황에서도 역시,
byte는 8개의 비트만 담을 수 있으므로 넘쳐 흐른 왼쪽의 비트는 버리고, 오른쪽 8개의 비트만 보관합니다.

그래서 최소값을 가진 byte형 변수가 언더플로우가 발생하면127이 되는 것입니다.

 

[예시]

using System;
using static System.Console;

namespace IntegralTypes
{
    class MainApp
    {
        static void Main(string[] args)
        {
            uint a = uint.MaxValue;
            WriteLine(a);

            a = a + 1;
            WriteLine(a);


            int b = int.MinValue;
            WriteLine(b);

            b = b - 1;
            WriteLine(b);
        }
    }
}

 

데이터 형식 : 실수형

데이터 형식 설명 크기(Byte) 담을 수 있는 값의 범위
float 단일 정밀도 부동 소수점 형식
(6 ~ 7개의 자릿수만 표현 가능)
4 (32bit)   -3.402823e38 ~ 3.402823e38
double 복수 정밀도 부동 소수점 형식
(15 ~ 16개의 자릿수만 표현 가능)
8 (64bit)   -1.79769313486232e308 ~ 
  1.79769313486232e308
decimal 28 ~ 29자리 데이터를
표현할 수 있는 소수 형식
16 (128bit)   ±1.0 x 10e-28 ~ ±7.9 x 10e28

 

부동 소수점 형식에는 float와 double 두 가지가 있지만, float보다는 double을 사용하는 것을 권합니다.

 

double이 float에 비해 메모리를 두 배로 사용하지만, 그만큼 float에 비해 데이터의 손실이 적기 때문입니다.

(표현 가능한 자릿수를 비교하면, float은 double에 비해 실제 값에 대한 오차가 더 클 수 밖에 없습니다.)

 

double 형식을 사용했는데도 데이터의 손실이 우려된다면, decimal 형식을 사용할 수 있습니다.

 

decimal의 한계마저 넘어서는 데이터를 처리해야 한다면, 그때는 직접 그 데이터를 처리할 수 있는 알고리즘을 담은 복합 데이터 형식을 직접 작성해야 합니다.

 

[예시]

using System;
using static System.Console;

namespace IntegralTypes
{
    class MainApp
    {
        static void Main(string[] args)
        {
            float   a = 3.141_592_653_589_793_238_462_643_383_279f;  // F도 사용 가능
            double  b = 3.141_592_653_589_793_238_462_643_383_279;   // d나 D도 사용 가능
            decimal c = 3.141_592_653_589_793_238_462_643_383_279m;  // M도 사용 가능

            WriteLine(a);  // 3.1415927
            WriteLine(b);  // 3.141592653589793
            WriteLine(c);  // 3.1415926535897932384626433833
        }
    }
}

 

데이터 형식 : 논리형

true나 false를 저장할 때, 실제로 필요한 메모리 크기는 1bit이지만, 컴퓨터가 기본적으로 다루는 데이터의 크기가 바이트 단위이기 때문에 1bit만 저장하려 해도 1byte가 저장이 됩니다.

 

데이터 형식 : object 형식

object 형식은 어떤 데이터이든지 다룰 수 있는 데이터 형식입니다.

그럴 수 있는 모든 데이터 형식(기본 데이터 형식이나 모든 복합 데이터 형식)이 자동으로 object 형식으로부터 상속받게 한 것이다.

따라서 컴파일러는 어떤 형식의 데이터라도 object에 담아 처리할 수 있습니다.

 

하지만, 부호 있는 정수 형식, 부호 없는 형식, 부동 소수점 형식, decimal 형식 등 각자 데이터 형식을 처리하는 방식이 모두 다른데,

object 형식은 어떻게 모든 데이터를 다룰 수 있는지, 이에 대한 의문점이 생길 수 있습니다.

 

이 의문점을 해결하기 위해서, '박싱'과 '언박싱'의 개념을 알아야 합니다.

 

[박싱과 언박싱]

  • 박싱 (Boxing)
    • 값 형식의 데이터를 참조 형식으로 할당(변환)하는 것
    • 스택 메모리에는 데이터의 주소를 참조하고, 실제 값은 힙 메모리에 할당됩니다.
    • 박싱을 통해 값 형식을 메모리에 할당된 객체(Object 타입)로 변환하여 참조 형식처럼 다룰 수 있습니다.
  • 박싱의 작동 구조
  1. 값 형식 데이터를 힙에 할당 : 값 형식 데이터를 힙에 새로운 객체로 할당합니다.
  2. 객체 생성 : 이 객체는 해당 값 형식의 데이터를 포함합니다.
  3. 데이터 복사 : 값 형식의 데이터를 복사하여 힙에 할당된 객체에 저장합니다.
  4. 참조 생성 : 값 형식을 가리키는 참조가 생성됩니다.
  • 언박싱 (Unboxing)
    • 박싱된 객체를 참조 형식에서 다시 값 형식의 데이터로 할당(변환)하는 것
    • 힙에 저장된 객체에서 값을 꺼내와 스택에 저장하고, 이 과정에서 타입 검사와 캐스팅이 필요합니다.
  • 언박싱의 작동 구조
  1. 참조 가져오기 : 박싱된 객체의 참조를 가져옵니다.
  2. 값 추출 : 힙에 저장된 객체에서 값 형식 데이터를 추출합니다.
  3. 데이터 복사 : 이 데이터를 스택에 있는 값 형식 변수에 복사합니다.
  • 유의할 점
    • 단순히 스택에서 힙으로의 값 복사가 박싱은 아니라는 것을 유의해야 합니다.
    • 반대로 참조 형식을 값 형식으로 바꾸븐게 무조건 언박싱은 아니라는 것입니다.
      • 언박싱은 박싱된 데이터를  다시 값 형식으로 돌려놓는 것입니다.
        즉, 박싱이 있어야 언박싱이 있을 수 있다.
  • 장점
    • 유연성 : 값 형식으로 변환하여 다양한 컬렉션이나 메서드에서 사용할 수 있습니다.
  • 단점
    • 성능 저하 : 박싱과 언박싱은 힙 메모리 할당과 가비지 컬렉션을 유발하므로 성능 저하가 발생할 수 있습니다.
    • 런타임 오류 가능성 : 언박싱을 할 때, 타입 불일치로 인한 예외가 발생할 수 있습니다.

[예제]

using System;
using static System.Console;

namespace IntegralTypes
{
    class MainApp
    {
        static void Main(string[] args)
        {
            int a = 123;
            object b = (object)a;  // a에 담긴 값을 박싱해서 힙에 저장
            int c = (int)b;        // b에 담긴 값을 언박싱해서 스택에 저장

            WriteLine(a);  // 123
            WriteLine(b);  // 123
            WriteLine(c);  // 123

            double x = 3.1414213;
            object y = x;          // x에 담긴 값을 박싱해서 힙에 저장
            double z = (double)y;  // y에 담긴 값을 언박싱해서 스택에 저장

            WriteLine(x);  // 3.1414213
            WriteLine(y);  // 3.1414213
            WriteLine(z);  // 3.1414213
        }
    }
}