Basic Programming(기초 프로그래밍)/C#

[C# 입문] 데이터 형과 변수 (상)

Master_Worm 2021. 4. 7. 14:00

 

Hello, World! 코드 분석해보기! 지난 강의에서 2개의 프로그래밍 문제를 드렸습니다. 이 문제에 대한 답부터 살펴보도록 하겠습니다. 

 

첫 번째 문제에 대한 해답은

using System;

namespace MyFirstApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("안녕하세요. 저는 OO입니다.");
            Console.WriteLine("앞으로 우리의 프로그램을 재밋게 해보도록 하겠습니다.");
            Console.WriteLine("다음에 뵙겠습니다!");
        }
    }
}

 두번째 문제에 대한 해답은

using System;

namespace MyFirstApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("이 줄은 첫번째 줄입니다.");
            Console.WriteLine("");
            Console.WriteLine("이 줄은 세번째 줄입니다.");
            Console.WriteLine("");
            Console.WriteLine("이 줄은 다섯번째 줄 입니다.");
        }
    }
}

 

로 볼 수 있겠습니다. 이 코드와 다르게 쓰셨다고 해도 틀린 것은 아닙니다. 다양한 방법이 있으며 여기에는 이러한 방법이 있다고 볼 수 있습니다. 출력된 결과가 같으면 정답이라 생각하시면 됩니다. 

 


 

 

  이번 포스팅에서는 C#에서 사용되는 데이터 종류에 대해 살펴보도록 할 예정입니다. C#에서 정의된 상태를 이야기하는 것이다 보니 글 자체가 지루해질 우려가 있지만, 반드시 필요한 부분이기도 하니 한 번쯤 쭈욱 읽어보신 다음에 하나하나 직접 쳐보시면서 익혀보는 시간을 갖도록 하시면 됩니다. 

 

  정말 단순한 내용이지만, 단순한 내용이기에 오히려 어려워질 수 있는 단원입니다. 꼭 한번은 끝까지 읽어보시기 바랍니다. 

 


  앞서 이야기햇듯이 컴퓨터는 데이터의 종류를 딱딱 알아맞히지 못합니다. (물론 어느 정도는 컴퓨터 스스로가 판단할 수 있도록 설계되어 있기는 합니다.) 예를 들어, 12와 "12"는 다른 형태로 받아들입니다. 그냥 12는 정수로 받아들여지며, "12"는 문자열로 쓰입니다. 인간은 이를 한 번에 알아들을 수 있도록 12라는 숫자로 자동으로 변환해서 생각합니다. 하지만 컴퓨터는 아니죠.

 

  그래서 필요한 것이 데이터형이 필요합니다. 컴퓨터가 어느정도 추측할 수도 있지만, 이러한 추측은 프로그래머 입장에서 설계의 혼란이 오기 마련입니다. 예를 들어, 숫자 12로 저장하려고 했는데, 컴퓨터의 추측으로 12라는 문자로 입력되면 이후에 오류가 나기 마련입니다. 

 

  이러한 수많은 데이터에 대한 통일을 하기 위해서는 반드시 명시적인 데이터형이 정의되어 있어야 합니다. 

 

  C#의 데이터 형식은 두 가지를 기본 근간으로 이루고 있습니다. 기본 데이터 형식복합 데이터 형식으로 크게 나눌 수 있습니다. 그리고 이러한 데이터 형식은 값 형식참조 형식으로 나누어 집니다.당장에는 기본 데이터 형식에 대해서 살펴보도록 하고 나중에 복합 데이터 형식에 대해 살펴보고자 합니다.

 


기본 데이터 형식

데이터 형식 .NET 형식(Type) 비트 범위
byte System.Byte 부호 없는 정수 8 0 ~ 255
sbyte System.SByte 부호 있는 정수 8 -128 ~ 127
short System.Int16 부호 있는 정수 16 -32,768 ~ 32,767
ushort System.UInt16 부호 없는 정수 16 0 ~ 65,535
int System.Int32 부호 있는 정수 32 -2,147,483,648 ~ 2,147,4283,647
uint System.UInt32 부호 없는 정수 32 0 ~ 4,294,967,295
char System.Char 유니코드 문자 16 -
string System.String 문자 시퀀스 - -
bool System.Boolean 논리 형식 8 true, false
object System.Ojbect 모든 형의 기본 형식 - -
long System.Int64 부호 있는 정수 64 -922,337,203,685,477,508 ~ 922,337,203,685,477,507
ulong System.UInt64 부호 없는 정수 64 0 ~ 18,446,744,073,709,551,615
float System.Single 단정밀도 부동 소수점 형식 32 -3.402823e38 ~ 3.402823e38
double System.Double 배정밀도 부동 소수점 형식 64 -1.79769313486232e308 ~ 1.79769313486232e308
decimal System.Decimal 10진수 부동 소수점 숫자 128 -3.402823e38 ~ 3.402823e38

  여기서 1.3e2 의 형태로 쓰인 수는 1.3 × 10의 제곱 형태를 의미합니다. 우리가 고등학교 때, 배운 로그에서와 비슷한 형태를 의미합니다. 수의 과학적 표기 기법에 따라 표시한 수를 의미합니다. 큰 수를 표시할 때는 e는 에러의 e가 아니라 지수의 Exponent의 e입니다. 

 

이러한 수의 범위를 직접 확인할 수 있는데, 그 방법은 int.MaxValue와 int.MinValue를 사용하는 것입니다.

 

using System;

namespace MyFirstApp
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("byte의 최댓값 : " + byte.MaxValue);
            Console.WriteLine("byte의 최솟값 : " + byte.MinValue);

            Console.WriteLine("sbyte의 최댓값 : " + sbyte.MaxValue);
            Console.WriteLine("sbyte의 최솟값 : " + sbyte.MinValue);

            Console.WriteLine("char의 최댓값 : " + char.MaxValue);
            Console.WriteLine("char의 최솟값 : " + char.MinValue);
            
            Console.WriteLine("float의 최댓값 : " + float.MaxValue);
            Console.WriteLine("float의 최솟값 : " + float.MinValue);

            Console.WriteLine("double의 최댓값 : " + double.MaxValue);
            Console.WriteLine("double의 최솟값 : " + double.MinValue);

        }
    }
}

 알아보고자 하는 데이터형에다가 .MaxValue를 써주면 됩니다. 위에 코드는 몇 개만 살펴보았지만, 위에서 소개한 데이터형을 더 넣어봄으로써 각각의 값을 확인해보시기 바랍니다. 

 

  실제 실행하면

이러한 형태가 나오는데, char의 값은 ?로 나오는 것을 확인할 수 있습니다. 저장된 값에 따라 값이 변하기 때문에 이러한 형태가 됩니다. 지금은 알 수 없는 값이기에 값이 나오지 않고 있습니다. 심지어 문자열을 저장하는 데이터형인 string은 출력해보려고 하면 오류가 나옵니다. 

 

 


값 형식(Primitive Data Type)과 참조 형식 (Reference Type)

  기본 데이터와 복합 데이터 형에는 각각 값 형식과 참조 형식이 존재합니다. 값 형식은 실제 값을 변수를 통해 데이터를 저장하며, 참조 형식은 참조할 위치에 대한 주소를 저장하는 역할을 합니다. 조금 더 자세히 살펴보면 값 형식은 스택에 의해 저장되며, 참조 형식은 힙과 스택에 의해 할당됩니다. 스택에는 참조하고자 하는 주소지가 저장되어 있는 것이지요. (그리고 가비지 컬렉터에 의해서 할당된 값이 해제됩니다.) 이는 나중에 다시 이야기하도록 하겠습니다.  (이 부분에 대해 자세히 이야기하기 위해서는 스택과 힙이 필요한데, 자료구조에 대한 이야기이므로 나중에 자세히 살펴보도록 하겠습니다.) 

 

* 기본 데이터 형식에는 숫자 형식, 논리 형식, 문자열 형식, 오브젝트 형식으로 크게 나누어 집니다. 여기서 문자열 형식과 오브젝트 형식만 참조형식이며 숫자 형식, 논리 형식은 값 형식입니다.

 

* 스택은 책을 쌓아둔 것을 생각하시면 됩니다. 그래서 가장 먼저 들어간 것이 가장 나중에 나오는 선입 후출의 형태를 가집니다. 

(좌) 힙, (우) 스택

* 힙은 이진 트리라 불리는 형태를 가집니다. 가지가 2개 짜리인 나무 구조를 연상시키는 형태를 의미합니다. 이러한 트리 형태는 컴퓨터에서 자주 쓰이는 자료 구조입니다. 나중에 알아보도록 하겠습니다. 


변수

변수는 값을 저장할 수 있는 공간을 의미합니다. 

int a;
char b;
string c;

  이러한 a, b, c를 변수라 부릅니다. 컴퓨터에서 메모리에 '일정한 공간 만큼을 할당하라'라고 명령을 내리는 역할을 합니다. 그리고 위에서 본 것처럼, 각 데이터형에 따라 얼마만큼의 비트를 할당할지 결정하게 됩니다. a라는 변수는 32비트만큼의 메모리를 차지하고 있습니다. 마치 우리가 살고 있는 현실 세계에서 특정한 땅만큼을 배정해주고, a씨네 집이라고 이름 짓는 것과 같습니다. 

 

   실제 주소는 서울시 코딩서로 200번 길 32처럼 있지만, a씨네 집이라고 별칭을 지어주는 것이지요. 메모리에서도 같은 일이 일어납니다. a라는 변수에는 메모리 주소가 저장되며, a라는 별칭을 통해 이 메모리에 접근하자 라고 약속을 해두는 것입니다. C#에서는 메모리에 대해 크게 신경 쓰지 않아도 되도록 가비지 컬렉터가 존재합니다. 이 부분은 다시 나중에 이야기하도록 하겠습니다. 다만 변수를 선언하면 메모리에 올라가며, 메모리에 직접적으로 접근하는 것은 불가능하며, 변수라는 메모리 주소의 별칭을 통해 접근한다 정도로 이해하시면 되겠습니다.

그림으로 그려보자면 이러한 느낌입니다. 

int a = 10; int라는 데이터 형을 가진 a라는 변수에는 10이 저장되어 있다. 그리고 그 주소는 1000이다. int는 32비트이지만 바이트로 표현하면 4바이트이다. 그래서 주소지도 4씩 증가하는 형태로 표현되어 있다! 

 

※ 변수는 메모리에서 특정한 자리를 차지하는 영역에 대한 별칭을 의미한다! 정도가 변수의 의미라 생각할 수 있겠습니다. 

 

초기 프로그래밍 언어에서는 변수를 선언한 뒤에 반드시 초기화가 필요했습니다. 초기화를 하지 않으면 '쓰레기 값'이라 불리는 임의의 아무 값이 들어가기 때문이죠. 이러한 쓰레기 값으로 인해 프로그램이 오류가 생길 가능성이 있었어서 초기에는 초기화가 필수가 되었습니다. C#에는 초기화를 하지 않으면 컴파일 수준에서 오류를 발생시킵니다. 초기화는 일반적으로 정수 값이라면 0을 넣습니다. 

 


리터럴(Literal)

리터럴이라 하면 문자 그대로의 값을 의미합니다. 실제 사전 상의 의미도 "문자 그대로의"라는 의미가 있습니다. 다음의 코드를 살펴보도록 하겠습니다.

int a = 100;

이라는 코드에서 100은 리터럴입니다. 100이라는 의미 그대로를 가지고 있기 때문입니다. 100이라는 숫자가 쓰이기 위해서는 어떤 부분에는 저장이 되어야 할 것입니다. 이러한 형태로 100이라는 문자 그대로의 의미를 가지는 진짜 숫자가 저장된 형태가 리터럴이라 부릅니다. 고등학교 수학에서는 상수라 불리던 것이 리터럴과 같은 의미를 가집니다.

 


이제 간단한 예제를 실습해보면서 변수를 사용해보도록 하겠습니다. 

 

using System;

namespace MyFirstApp
{
    class Program
    {
        static void Main(string[] args)
        {
            int a = 1_000_000_000;      // 자릿수를 구분할 때 _를 사용합니다.
            float b = 3.1734F;          // F는 float를 의미하며 이러한 리터럴을 만들 때 사용
            double c = 3.1734;          // double 형태는 F를 넣지 않아도 에러가 생기지 않는다.
            string d = "안녕 나야";

            Console.WriteLine(a);
            Console.WriteLine(b);
            Console.WriteLine(c);
            Console.WriteLine(d);

        }
    }

숫자를 사용할 때는 구분자인 '_'를 사용함으로써 사람이 보기 편하게 만들 수 있습니다.

1000000000 = 1_000_000_000 과 같이 쓰는 것이죠. 구분자를 어디에 둘지는 프로그래머가 정할 수 있지만, 통상적으로 3개의 자릿수 마다 구분자를 둡니다.

 

만약에 데이터형의 범위를 넘어가는 값을 대입하려고 한다면 컴파일러가 오류를 발생시킵니다. 

위에 코드에서 추가로

short e = 500000000;

을 넣게 되면 오류가 발생합니다. 

 

컴파일러가 잡지 못하는 오류에 대해서, 데이터형을 넘쳐서 데이터가 변형된 형태를 오버플로우라 부르며, 데이터형이 부족해서 생긴 변형을 언더플로우라 부릅니다. 

 

우리는 둘 중에서 오버플로우에 대한 예제만 살펴보도록 하겠습니다.

using System;

namespace MyFirstApp
{
    class Program
    {
        static void Main(string[] args)
        {
            short a = 32767;      // short의 최댓값은 32,767
            a++;                  // 1만큼 증가시킨다.

            Console.WriteLine(a);

        }
    }
}

실제 실행하게 되면, 32767이 나오는 것이 아닌 -32768이 나오게 됩니다. 데이터가 저장할 수 있는 한계선을 넘어감으로써 우리가 원하는 값이 아닌 다른 데이터 값을 표현하게 된 것입니다. 이 부분에 대해서도 나중에 저장 구조()에 대해 살펴보고 그 다음에 설명드리도록 하겠습니다. 

 

위의 구조를 조금 더 바꿔보도록 하겠습니다. 바로 전에 배웠던 short.MaxValue를 활용해보도록 하겠습니다. 

using System;

namespace MyFirstApp
{
    class Program
    {
        static void Main(string[] args)
        {
            short a = short.MaxValue;      // short의 최댓값은 32,767
            a++;                  // 1만큼 증가시킨다.

            Console.WriteLine(a);

        }
    }
}

언더플로우까지 나오게 한다면, 

using System;

namespace MyFirstApp
{
    class Program
    {
        static void Main(string[] args)
        {
            short a = short.MaxValue;      // short의 최댓값은 32,767
            a++;                  // 1만큼 증가시킨다.

            Console.WriteLine(a);

            a = short.MinValue;
            a--;
            Console.WriteLine(a);
        }
    }
}

로 바꿔 볼 수 있습니다. 

 


실수를 담은 데이터 형 float, double, decimal

  지금까지는 거의 정수형에 대한 이야기를 해왔습니다. 그렇다면 소수를 표현하는 수단은 어떻게 표현해야 하는가? 바로 float, double, decimal 형태로 표현합니다. 세 개의 데이터 형의 차이점은 데이터를 어디까지 표현할 수 있는가 입니다. 

 

  가장 많은 수를 표현할 수 있는 것은 float < double < decimal의 순으로 정밀도가 높습니다. 정밀도는 소수점을 표현할 수 있는 선을 이야기합니다. C#은 기본적으로 double형을 장려합니다. float보다 더 많은 데이터를 차지하지만, 데이터 손실을 안고 가는 것보다는 double형을 쓰면서 얻을 데이터를 확보하자는 것이죠. 하지만 더 높은 정밀도를 가진 decimal은 너무 과도한 정밀도 일수도 있기에 기본으로 쓰이는 것은 double형을 사용합니다. 

 

  실제로 예제를 통해 정밀도가 어디까지 표현되는지에 대해 살펴보도록 하겠습니다. 

using System;

namespace MyFirstApp
{
    class Program
    {
        static void Main(string[] args)
        {
            float a = 3.1415926535_8979323846_2643383279_5028841971f;   // float형을 사용하려면 숫자 뒤에 f가 필요하다.
            double b = 3.1415926535_8979323846_2643383279_5028841971;   // 
            decimal c = 3.1415926535_8979323846_2643383279_5028841971m; // decimal형을 사용하려면 숫자 뒤에 m이 필요하다.

            Console.WriteLine(a);
            Console.WriteLine(b);
            Console.WriteLine(c);
        }
    }
}

 

입력한 수는 파이를 입력해보았습니다. 10자리 마다 구분지어서 넣어보았습니다. 실제 실행 결과를 보면 float과 double, decimal에 따라 소수점 아래에 숫자를 표현하는 정도가 다름을 확인할 수 있습니다. (정밀도가 다른 것이죠)

 


글이 길어지는 관계로 다음 포스팅으로 넘어가서 글을 이어가도록 하겠습니다! 

감사합니다.