공부/C#

C# 기본 구조, 식별자, 키워드, 리터럴, 연산자, 타입 기초, 숫자 타입, 연산자, bool 타입, 연산자, 문자, 문자열, 배열, 변수, 메모리구조, 값 전달, 참조 타입 값 전달, ref, out, params

월러비 2025. 8. 6. 17:39

 

C# 기본 구조

  • 가장 단순한 예제
int x = 12 * 30;        // Statement 1
Console.WriteLine(x);   // Statement 2

  • 문(또는 문장이라고 부름)(Statement)은 프로그램의 기본 실행 단위
  • 각 문은 세미콜론(;)으로 끝난다.
  • 주석은 // 뒤에 작성하며, 컴파일러는 이를 무시한다.
    • /**/ 도 있다.
  • 문자열이나 숫자와 같은 값을 리터럴이라고 한다.
  • x : 변수(또는 식별자 라고 부른다.)
    • : 산술연산자
  • WriteLine은 콘솔에 값을 출력하는 메서드다.
    • 자동으로 개행(n\)을 해준다.
    • Write 함수도 있다. : 개행(n\)이 없는 함수다.

C++과의 차이점

  • C#은 메모리 관리를 프로그래머가 하지 않도록 만들어졌다
    • C++ 메모리 제어 직접 가능
    • C# : 백그라운드에서 메모리를 관리해준다.
  • 프로그램 진입점이 다르다.
    • C++ : main의 내부가 시작점이다.
    • C# : 최상위문 사용 하냐 안하냐에 따라 달라진다.
  • C#은 헤더파일과 cpp 파일이 분리되어있지 않는다.
    • 클래스 내에 어딘가에 선언만 되어있으면 어디서든 쓸 수 있다.

최상위문

  • 프로그램의 시작점을 의미한다.
    • C++의 main 함수 역할이다.
  • C# 9.0부터는 Program 클래스와 Main 메서드 없이도 프로그램을 작성할 수 있다.
using System;  // System 네임스페이스 사용 선언

// 네임스페이스 사용 전
System.Console.WriteLine("Hello!");  // 정규화된 이름 사용

// 네임스페이스 사용 후
Console.WriteLine("Hello!");        // 간단한 이름 사용 가능

//흔히 사용하는 방식
static void Main()
{

}
  • Unity에서는 최상위문을 사용할 수 없다.
  • 임의로 main 함수를 만들어서 사용한다.
    • C#은 전역함수가 없다.
    • 전부 클래스의 멤버 함수여야 한다.
    • 모든 것이 ‘객체’로 이루어져야하기 때문이다.
  • 대소문자 구분을 잘 해야한다.
  • static으로 선언해야한다.
    • 클래스마다 ‘하나만’ 가지고 있어야하기 때문이다.

메서드 사용

using System;

// 메서드 호출
Console.WriteLine(FeetToInches(30));   // 360
Console.WriteLine(FeetToInches(100));  // 1200

// 메서드 정의
int FeetToInches(int feet)
{
    int inches = feet * 12;
    return inches;
}
  • 같은 계산을 반복해야 할 때는 메서드를 정의하여 코드를 재사용할 수 있다.
  • 반환 타입 (int) - 메서드가 돌려주는 값의 형식
  • 메서드 이름 (FeetToInches) - 메서드를 호출할 때 사용
  • 매개변수 목록 (int feet) - 메서드에 전달되는 입력 값
  • 메서드 본문 - 중괄호로 둘러싸인 실행할 코드
  • return 문 - 계산 결과를 호출한 곳으로 반환

매개변수 없는 메서드

  • void : 메서드가 아무 값도 반환하지 않음을 나타낸다.
  • return 문을 생략하거나 return; 형태로 사용할 수 있다.
  • DateTime : 시간 관련 데이터형
    • Now : DateTime형을 반환한다.
  • C#은 멤버접근이든 전역 접근이든 .(연산자)으로 통일이다.
  • $ : 서식화 해주는 역할이다.
    • 문자열 보간 이라고 한다.
    • %d의 역할이다.
  • : → 문자열을 의미한다.

컴파일 과정과 IL 코드

  • HW 위에 붙은것 : OS
  • 프로그래머가 명령을 OS에게 내리고, OS가 HW에서 실행하게 한다.

컴파일과 IL 코드 생성

  • C# 코드가 실행 파일이 되는 과정:
  1. C# 소스 코드(.cs)를 컴파일러가 IL(Intermediate Language) 코드로 변환
  2. IL 코드는 CPU에 독립적인 중간 언어로, .NET의 공통 언어다.
    1. 실행환경에서 돌릴 수 있는 결과물
    2. C++은 OS에서 바로 돌린다.
    3. IL은 OS에서 실행할 수 없다.
    4. 다만, IL은 어떤 개발 환경에서도 동일하게 동작할 수 있다.
  3. IL 코드는 실행 시점에 JIT(Just-In-Time) 컴파일러가 기계어로 변환

어셈블리

  • .NET의 배포 단위를 의미한다.
    • solution : 프로젝트의 집합
    • 프로젝트 집합을 지금 어셈블리로 사용하는 것이다.
  • IL 코드를 포함한 실행 파일(.exe) 또는 라이브러리(.dll)로 빌드할 수 있다.
    • 우리는 실행파일로 프로젝트를 세팅해서 사용하고있는 것이다.
  • 메타데이터(형식 정보, 버전 정보 등)
  • 여러 개의 소스 파일을 하나의 어셈블리로 컴파일 가능함

컴파일 시간과 실행 시간(런타임)

  • const로 선언된 상수의 값이 계산됨
    • const : 정해진 값이 있다면 실행 중 변경이 되지 않게한다.
  • 문법 오류를 검사함 - 가장 중요
  • 타입 검사를 수행함
    • C++이랑 C#이랑 통일하게 강타입(타입을 빡빡하게 검사)이다.
  • 최적화를 수행함
    • 중복 코드, 같은 값 할당 같은 일이 일어난 경우 수행하지 않고 넘어가고 다음 코드를 실행한다.

실행 시간(런타임)에 발생하는 일:

  • 변수의 값이 계산됨
  • 메서드가 호출됨
  • 객체가 생성됨
  • 예외가 발생할 수 있음
// 컴파일 시간에 발생하는 동작들
const int a = 10 * 20;  // 컴파일러가 200으로 직접 치환함
const int max = int.MaxValue + 1;  // 컴파일 시 오류: 상수 값이 int 범위를 벗어남

// 실행 시간에 발생하는 동작들
int b = 10 * 20;       // 프로그램 실행 중에 계산됨
int userInput = int.Parse(Console.ReadLine());  // 실행 중에만 알 수 있는 값
int result = checked(2000000000 * 2);  // 실행 중에 OverflowException 발생 가능
  • int.MaxValue : int 클래스의 MaxValue 변수에 접근하는 것이다.
  • int.Parse : int 형의 static 함수를 호출하는 것이다.
    • checked :

C# 구문

식별자

  • 프로그래머가 지정하는 클래스, 메서드, 변수 등의 이름이다.

식별자 규칙

  • 문자나 밑줄로 시작해야 한다.
  • Unicode 문자, 숫자, 밑줄로 구성된다.
  • 대소문자를 구분한다.
  • C# 키워드와 동일한 이름은 사용할 수 없다.
    • return, void 등이 키워드다.
using System;

int x = 12 * 30;
Console.WriteLine (x);
  • System : 네임스페이스 식별자
  • Console : 클래스 식별자
  • x : 변수 식별자
  • WriteLine : 메서드 식별자

명명 관례

  • 매개변수, 지역 변수 : 카멜 케이스(camelCase) 사용
    • 시작을 소문자로, 띄어쓰기 나오면 대문자로
  • 그 외 모든 식별자 : 파스칼 케이스(PascalCase) 사용
    • 시작을 대문자로, 나머지는 소문자로
    • 띄어쓰기가 나올때마다 대문자로 바꾼다.
// 카멜 케이스 - 매개변수, 지역 변수
void ProcessData(string firstName, int maxCount)
{
    int currentValue = 0;
    string tempName = firstName;
}

// 파스칼 케이스 - 클래스, 메서드
public class CustomerOrder
{
    public void ProcessPayment() { }
}

키워드

예약 키워드

  • 컴파일러에서 특별한 의미를 가지는 단어다.
  • 식별자로 사용할 수 없다.
  • abstract, as, base, bool 등이 있다.
// 예약 키워드 사용 예시
public class MyClass        // class 키워드
{
    private int number;     // private 키워드

    public void Test()      // public, void 키워드
    {
        if (number > 0)     // if 키워드
        {
            return;         // return 키워드
        }
    }
}

문맥 키워드

  • 특정 문맥에서만 특별한 의미를 가짐
  • 식별자로도 사용 가능함
    • 문맥에서만 유효한 키워드이기 때문이다.
  • get, set, value 등이 있음
// 문맥 키워드 사용 예시
public class Temperature
{
    private float temp;

    // get, set은 속성에서 문맥 키워드로 사용됨
    public float Celsius
    {
        get { return temp; }
        set { temp = value; }  // value는 setter에서 문맥 키워드
    }

    // get을 일반 식별자로 사용 가능
    public void get() { }  // 메서드 이름으로 사용
}
  • 프로퍼티 : get set
    • 프로퍼티 안에서만 유효만 문맥 키워드다.
    • 밖에서는 일반 식별자로 사용 가능하다.
    • 하지만, 웬만해선 겹치게 쓰지 않는게 좋다.

@ 식별자

  • 키워드를 식별자로 사용해야 할 경우 @ 기호를 앞에 붙임
  • 이것도 쓰지 않는게 좋다.

리터럴, 구두점과 연산자

  • 프로그램에 직접 삽입된 데이터 값이다.
  • 숫자(12, 30), 문자열("Hello") 등이 있다.
// 정수 리터럴
int dec = 42;        // 10진수
int hex = 0x2A;      // 16진수
int bin = 0b101010;  // 2진수

// 실수 리터럴
double d1 = 123.456;    // 소수점 표기
double d2 = 1.23e2;     // 지수 표기
decimal m = 123.456m;   // decimal 리터럴

// 문자와 문자열 리터럴
char ch = 'A';
string s1 = "Hello";
string s2 = @"C:\\\\Program Files";  // 축자 문자열 리터럴

// 불리언 리터럴
bool t = true;
bool f = false;
  • 리터럴 앞에 접두사 또는 접미사가 있을 수 있다.
    • 0b : 2진수
    • 0x : 16진수
  • 소수점 표기는 default가 double형이다.
  • e2 : 지수표기
  • decimal :
    • f붙이면 float 형 리터럴이 된다.
  • string : 문자열 데이터형
    • C#은 string이 문자열 기본 데이터형이다.

구두점

  • 세미콜론(;): 문장의 끝을 나타낸다.
int x = 12 * 30;
Console.WriteLine(x);

  • 중괄호({}): 문장 블록을 구분한다.
if (x > 0)
{
    Console.WriteLine("Positive");
}

  • 마침표(.): 멤버 접근에 사용된다.
Console.WriteLine("Hello");

  • 괄호(): 메서드 호출과 매개변수 목록에 사용된다.
Math.Max(5, 10);

문장 래핑(Wrapping)

  • 문장은 여러 줄에 걸쳐 작성할 수 있다.
Console.WriteLine(
    1 + 2 + 3 + 4 + 5 +
    6 + 7 + 8 + 9 + 10
);

연산자

  • 단항 연산자(Unary operator): 하나의 피연산자에 작용 (예: -x)
  • 이항 연산자(Binary operator): 두 개의 피연산자에 작용 (예: x + y)
  • 삼항 연산자(Ternary operator): 세 개의 피연산자에 작용
  • 대부분의 이항 연산자는 중위 표기법(Infix notation) 사용
    • 이항 연산자를 기준으로 한다.
// 연산자 예시
-x              // 단항 연산자
x + y           // 이항 연산자(중위 표기법)
condition ? x : y  // 삼항 연산자

주석(Comments)

  • // 기호 뒤의 텍스트를 주석 처리함
// Statement 1
int x = 12 * 30;

여러 줄 주석

  • /* \*/ 사이의 모든 텍스트를 주석 처리함
/* This is a comment that
   spans two lines */
int x = 3;

C# 타입 (자료형, 데이터타입)

  • 타입(Type)은 값의 청사진을 정의한다.
    • 해당 값이 어떤 종류의 데이터인지 지정한다.
    • 값이 차지하는 메모리 크기를 결정한다.
    • 해당 값으로 수행할 수 있는 연산을 정의한다.
  • 변수는 타입의 인스턴스를 저장하는 메모리 공간이다.
    • 프로그램에서 데이터를 임시로 저장하는 용도로 사용한다.
    • 선언된 타입의 값만 저장할 수 있다.
    • 프로그램 실행 중에 값을 변경할 수 있다.
int x = 12 * 30;        // x는 변수, 12와 30은 int 타입의 값
Console.WriteLine(x);   // 360 출력

변수와 상수의 차이

  • 변수(Variable): 시간에 따라 다른 값을 가질 수 있는 저장소다.
  • 상수(Constant): 한 번 값이 정해지면 변경할 수 없다.
const int y = 360;  // 상수 선언
y = 100;           // 컴파일 오류: 상수는 변경 불가

미리 정의된 타입

  • 기본 데이터 형을 의미한다.

int 타입 사용 예제

  • 정수형 타입(Integer Type)인 int는 가장 기본적인 숫자 타입임
    • 32비트(4바이트) 메모리를 사용함
    • 2,147,483,648부터 2,147,483,647 범위의 정수를 저장할 수 있음
    • 덧셈(+), 뺄셈(-), 곱셈(*), 나눗셈(/) 등의 산술 연산 가능
    • 프로그램에서 숫자를 다룰 때 가장 많이 사용되는 타입임

string 타입 사용 예제

  • 문자열(String) 타입은 텍스트 데이터를 저장하는 참조 타입임
    • 문자들의 시퀀스(연속된 나열)를 저장함
    • 텍스트는 유니코드(Unicode) 문자로 저장되어 전 세계의 문자를 표현할 수 있음
    • 불변(Immutable)타입으로, 한번 생성된 문자열은 변경할 수 없음
      • 메모리에 저장된 값이 변경되는게 아니라 할당될때 다른 메모리가 생성되어서 그 메모리를 가리키게 되는것이다.
        • 본래라면 메모리 누수가 일어나지만 C#에는 가비지 컬렉터가 있다.
      • 장점 : 구현이 간단해진다.
    • 문자열 조작을 위한 다양한 메서드(ToUpper(), ToLower(), Substring() 등)를 제공함
    • \+ 연산자를 사용하여 문자열을 연결할 수 있음
  • ToString() 메서드
    • 모든 타입에서 사용 가능한 메서드임
    • 객체를 문자열로 표현하는 방법을 정의함
    • 숫자, 날짜 등을 문자열로 변환할 때 사용함
string message = "Hello world";
string upperMessage = message.ToUpper();
Console.WriteLine(upperMessage); // HELLO WORLD 출력

int x = 2022;
message = message + x.ToString();
Console.WriteLine(message); // Hello world2022 출력

bool 타입과 조건문 예제

  • 논리형(Boolean) 타입은 참/거짓을 표현하는 가장 단순한 타입임
    • true 또는 false 두 가지 값만 가질 수 있음
    • 1바이트의 메모리를 사용함 (실제로는 1비트만 필요하지만 CPU 처리 효율성을 위해 1바이트 사용)
    • 조건문(if)이나 반복문(while)의 조건식에서 주로 사용됨
    • 비교 연산자(\==, !=, <, >, <=, >=)의 결과값으로 사용됨
bool simpleVar = false;
if (simpleVar)  // false이므로 실행되지 않음
    Console.WriteLine("This will not print");

int x = 5000;
bool lessThanAMile = x < 5280;  // 비교 연산의 결과를 저장
if (lessThanAMile)  // true이므로 실행됨
    Console.WriteLine("This will print");

사용자 정의 타입

  • C#에서는 필요한 타입을 직접 정의할 수 있음
  • 클래스(class)나 구조체(struct)를 사용하여 새로운 타입을 만듦
    • C++은 접근지정자를 제외하고 다른게 없다.
    • C#은 클래스와 구조체에 따라서 많이 다르다.
  • 사용자 정의 타입은 데이터와 해당 데이터를 처리하는 메서드를 하나의 단위로 캡슐화함

타입의 구성 요소

  • 데이터 멤버(필드)
    • 타입의 상태를 저장하는 변수 - 멤버 변수
    • public, private 등의 접근 제한자로 보호 수준 지정
      • 변수, 함수 뿐만 아니라 클래스도 접근지정자를 작성해야한다.
      • 생략하면 기본 접근 권한이 설정된다.
        • 클래스 : interval
        • 멤버 : private
        • 구조체 : public
    • 인스턴스 필드와 정적(static) 필드로 구분됨
  • 함수 멤버(메서드)
    • 타입의 행동을 정의하는 코드 블록
    • 데이터 멤버를 조작하는 연산 수행
    • 매개변수를 받아 결과를 반환할 수 있음
public class UnitConverter
{
    private int ratio;  // 데이터 멤버(필드) - 변환 비율 저장

    // 함수 멤버(생성자) - 객체 초기화
    public UnitConverter(int unitRatio)
    {
        ratio = unitRatio;  // 필드 초기화
    }

    // 함수 멤버(메서드) - 단위 변환 수행
    public int Convert(int unit)
    {
        return unit * ratio;
    }
}
  • ratio 필드는 변환 비율을 저장하는 데이터 멤버임
  • 생성자는 객체가 생성될 때 ratio 필드를 초기화함
  • Convert 메서드는 입력값에 비율을 곱해 변환된 값을 반환함
// 인치 변환기 (1피트 = 12인치)
UnitConverter feetToInchesConverter = new UnitConverter(12);

// 피트 변환기 (1마일 = 5280피트)
UnitConverter milesToFeetConverter = new UnitConverter(5280);

Console.WriteLine(feetToInchesConverter.Convert(30));    // 360 (30피트 = 360인치)
Console.WriteLine(milesToFeetConverter.Convert(1));      // 5280 (1마일 = 5280피트)

// 체이닝: 1마일을 인치로 변환
Console.WriteLine(feetToInchesConverter.Convert(
    milesToFeetConverter.Convert(1)));                   // 63360 (1마일 = 63360인치)
  • C#은 구조체는 무조건 정적할당으로 쓰이고, 클래스로 만든 인스턴스는 무조건 동적할당으로 쓰인다.
  • new : 동적할당할때 쓴다.
    • C++과의 차이점 : C++에서는 delete를 해줘야 했지만, C#은 가비지 컬렉터가 있어 자동으로 삭제된다.

인스턴스 멤버와 정적 멤버

  • 멤버의 소속에 따라 두 가지로 구분됨:
    • 인스턴스 멤버: 객체 각각에 속하는 멤버
    • 정적(static) 멤버: 타입 자체에 속하는 멤버
Panda p1 = new Panda("Pan Dee");  // 첫 번째 판다 객체
Panda p2 = new Panda("Pan Dah");  // 두 번째 판다 객체

Console.WriteLine(p1.Name);         // Pan Dee - 인스턴스 멤버 접근
Console.WriteLine(p2.Name);         // Pan Dah - 인스턴스 멤버 접근
Console.WriteLine(Panda.Population);// 2 - 정적 멤버 접근 (총 판다 수)

public class Panda
{
    public string Name;            // 인스턴스 필드 - 각 판다의 이름
    public static int Population;  // 정적 필드 - 전체 판다의 수

    public Panda(string n)
    {
        Name = n;                  // 각 판다의 이름 설정
        Population = Population + 1;// 전체 판다 수 증가
    }
}

  • C#은 초기화 안하면 데이터형의 기본값이 들어간다.
    • string : null, int : 0 등
  • 클래스로 만드는 변수는 모두 ‘참조형’이다. - string 등
    • 포인터나 레퍼런스처럼
  • 구조체로 만드는 변수는 참조형이 아니다. - int 등
  • 인스턴스 멤버의 특징:
    • 객체마다 별도의 복사본을 가짐
    • 객체 참조를 통해 접근 (예: p1.Name)
    • 객체의 고유한 상태를 표현할 때 사용
  • 정적 멤버의 특징:
    • 모든 객체가 공유하는 하나의 복사본만 존재
    • 타입 이름을 통해 직접 접근 (예: Panda.Population)
    • 객체 생성 없이도 사용 가능
    • 모든 객체가 공유해야 하는 데이터에 사용
    • C++과는 다르게 반드시 ‘클래스 명으로’ 접근해서 호출해야한다.

네임스페이스 정의

  • 네임스페이스(Namespace)는 타입을 조직화하는 방법임
    • 타입 이름의 충돌을 방지함
    • 관련된 타입을 논리적으로 그룹화함
    • 코드의 모듈화와 재사용성을 향상시킴
using System;      // System 네임스페이스 사용 선언
using Animals;     // Animals 네임스페이스 사용 선언

Panda p = new Panda("Pan Dee");  // Panda 타입 사용
//Animals.Panda p = new Panda("Pan Dee"); //원래 형태
Console.WriteLine(p.Name);       // System.Console 타입 사용

//아래의 Panda와는 다른 데이터형이 된다.
public class Panda  // Animals.Panda가 전체 이름이 됨
{
    public string Name { get; set; }

    public Panda(string name)
    {
        Name = name;
    }
}

namespace Animals  // Animals 네임스페이스 정의
{
    public class Panda  // Animals.Panda가 전체 이름이 됨
    {
        public string Name { get; set; }

        public Panda(string name)
        {
            Name = name;
        }
    }
}

타입과 형변환

  • 형변환(Type Conversion)은 한 타입의 값을 다른 타입의 값으로 변환하는 것임

암시적 형변환(Implicit Conversion)

  • 데이터 손실의 위험이 없는 안전한 변환
    • ex) 업캐스팅
  • 컴파일러가 자동으로 수행
  • 작은 크기의 타입에서 큰 크기의 타입으로 변환할 때 사용
int x = 12345;      // int는 32비트 정수
long y = x;         // 암시적으로 64비트 정수로 변환 (안전)
float f = x;        // int에서 float로 암시적 변환 (안전)

int x = 1234567890;
float f = x;  // 정밀도 손실 가능성 있음
Console.WriteLine(f);  // 1.234568E+09 출력 (정밀도 손실 발생)

명시적 형변환(Explicit Conversion)

  • 데이터 손실 가능성이 있는 변환
    • ex) 다운캐스팅
  • 프로그래머가 명시적으로 지정해야 함
  • 캐스트 연산자 () 사용
int x = 12345;
short z = (short)x;   // 명시적으로 16비트 정수로 변환 (데이터 손실 가능)
int i = (int)3.99;    // double에서 int로 변환 (소수점 이하 손실)

값 타입과 참조 타입

값 타입(Value Types)

  • 값 자체를 직접 저장함
  • 변수가 선언된 메모리에 직접 저장됨
  • 할당 시 값이 복사됨
    • 값만 같고 다른 메모리 공간이 된다.
  • struct로 선언된 사용자 정의 타입임
public struct Point { public int X, Y; }

Point p1 = new Point();
p1.X = 7;

Point p2 = p1;             // p1의 값이 p2로 복사됨
Console.WriteLine(p1.X);   // 7
Console.WriteLine(p2.X);   // 7

p1.X = 9;                  // p1만 변경
Console.WriteLine(p1.X);   // 9
Console.WriteLine(p2.X);   // 7 (p2는 영향 받지 않음)

  • 값의 참조(메모리 주소)를 저장함
  • 실제 데이터는 힙(heap) 메모리에 저장됨
  • 할당 시 참조가 복사됨
  • class로 선언된 사용자 정의 타입임
public class Point { public int X, Y; }

Point p1 = new Point();
p1.X = 7;

Point p2 = p1;             // p1의 참조가 p2로 복사됨
Console.WriteLine(p1.X);   // 7
Console.WriteLine(p2.X);   // 7

p1.X = 9;                  // 같은 객체를 가리키므로
Console.WriteLine(p1.X);   // 9
Console.WriteLine(p2.X);   // 9 (p2도 영향 받음)

  • null은 "참조 없음"을 나타내는 특별한 값임
  • 참조 타입만 null 값을 가질 수 있음
  • null인 객체의 멤버에 접근하면 NullReferenceException 발생
Point p = null;  // 참조가 없는 상태
Console.WriteLine(p == null);  // True
Console.WriteLine(p.X);        // 실행 시점 오류: NullReferenceException

  • Nullable\<T> || T?
int? nullableInt = null;
if (nullableInt == null)
  Console.WriteLine("값이 없습니다.");

  • nullable 형이라고 부른다.

기본값

  • 모든 타입은 자동 초기화되는 기본값을 가짐
  • 기본값은 해당 메모리를 0으로 초기화한 결과임
  • 참조 타입의 경우 기본값은 null
// 숫자 타입의 기본값은 0
Console.WriteLine(default(int));     // 0
Console.WriteLine(default(double));  // 0.0

// bool 타입의 기본값은 false
Console.WriteLine(default(bool));    // False

// char 타입의 기본값은 '\\\\0'
Console.WriteLine(default(char));    // '\\0'

// 참조 타입의 기본값은 null
Console.WriteLine(default(string));  // null

  • default 키워드로 모든 타입의 기본값을 얻을 수 있음
  • C# 7.1부터는 타입 추론이 가능한 경우 타입을 생략할 수 있음
int x = default;     // default(int)와 동일
string s = default;  // default(string)과 동일

미리 정의된 타입의 분류

 

  • 숫자 타입
    • 부호 있는 정수
      • sbyte: 8비트, -128 ~ 127
      • short: 16비트, -32,768 ~ 32,767
      • int: 32비트, -2,147,483,648 ~ 2,147,483,647
      • long: 64비트, -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
    • 부호 없는 정수
      • byte: 8비트, 0 ~ 255
      • ushort: 16비트, 0 ~ 65,535
      • uint: 32비트, 0 ~ 4,294,967,295
      • ulong: 64비트, 0 ~ 18,446,744,073,709,551,615
    • 실수
      • float: 32비트 부동소수점
      • double: 64비트 부동소수점
      • decimal: 128비트 정밀 소수점
  • 논리 타입
    • bool: true/false 값
  • 문자 타입
    • char: 16비트 유니코드 문자

참조 타입 (Reference Types) - 클래스

  • string
    • 문자열을 나타내는 특별한 참조 타입
    • 불변(immutable) 타입임
  • object
    • 모든 타입의 기본 클래스
    • 모든 타입은 object로부터 직접 또는 간접적으로 상속됨

C# 타입과 CLR 타입의 관계

  • C# 키워드는 .NET의 타입에 대한 별칭임
// 다음 두 선언은 동일함
int i = 5;                  // C# 키워드 사용
System.Int32 i = 5;        // .NET 타입 이름 사용

// 다음 두 선언도 동일함
string s = "Hello";        // C# 키워드 사용
System.String s = "Hello"; // .NET 타입 이름 사용

  • int == Int32 이다.
  • 프리미티브 타입(Primitive Types)
    • CLR에서 특별히 지원하는 기본 타입들임
    • 컴파일된 코드에서 특별한 지원을 받음
    • decimal을 제외한 모든 숫자 타입은 IL에서 직접 지원되는 프리미티브 타입임

C# 숫자 타입과 연산자

C#의 수치 데이터형아래 표는 C#의 기본 제공 수치 데이터형을 보여줌:C# 타입 .NET 타입 접미사 크기 범위

부호있는 정수        
sbyte SByte - 8비트 -2⁷ ~ 2⁷-1
short Int16 - 16비트 -2¹⁵ ~ 2¹⁵-1
int Int32 - 32비트 -2³¹ ~ 2³¹-1
long Int64 L 64비트 -2⁶³ ~ 2⁶³-1
부호없는 정수        
byte Byte - 8비트 0 ~ 2⁸-1
ushort UInt16 - 16비트 0 ~ 2¹⁶-1
uint UInt32 U 32비트 0 ~ 2³²-1
ulong UInt64 UL 64비트 0 ~ 2⁶⁴-1
실수        
float Single F 32비트 ±(~10⁻⁴⁵ ~ 10³⁸)
double Double D(생략가능) 64비트 ±(~10⁻³²⁴ ~ 10³⁰⁸)
decimal Decimal M 128비트 ±(~10⁻²⁸ ~ 10²⁸)
  • .NET 타입 앞에 System.을 생략하고 작성한것이다.
  • decimal : 정밀 소수점 데이터형

수치 데이터형의 분류와 특징

  • 정수형(Integral Types)
    • int와 long이 가장 많이 사용됨
    • 다른 정수형은 메모리 효율이나 호환성을 위해 사용됨
    • 기본 정수 리터럴은 int로 해석됨
  • 부동소수점형(Floating-Point Types)
    • float와 double은 과학/공학 계산용
    • double이 기본 실수 리터럴 타입임
    • 2진수 기반으로 저장되어 10진 소수 표현에 오차 있음
  • 고정소수점형(Fixed-Point Type)
    • decimal은 금융 계산용
    • 10진수 기반으로 저장되어 정확한 소수 계산 가능
    • 다른 수치 타입보다 처리 속도가 느림

CLR 타입과의 관계

  • CLR : 실행환경의 얘기다.
  • C# 키워드는 .NET의 타입에 대한 별칭임
// 다음 두 선언은 동일함
int i = 5;
System.Int32 i = 5;

숫자 리터럴(Numeric Literals)

리터럴의 기본 형식

  • 정수 리터럴은 10진수 또는 16진수로 표현 가능함:
int x = 127;     // 10진수
long y = 0x7F;   // 16진수 (0x 접두사)
  • 가독성을 위해 밑줄(_) 사용 가능:
int million = 1_000_000;
  • 2진수 표현은 0b 접두사 사용:
var b = 0b1010_1011_1100_1101_1110_1111;

리터럴 타입 추론

  • 컴파일러는 다음 규칙으로 타입을 추론함:
    • 소수점이나 지수(E)가 있으면 double로 추론
    • 정수는 int, uint, long, ulong 순서로 맞는 타입 선택
Console.WriteLine(1.0.GetType());         // Double
Console.WriteLine(1E06.GetType());        // Double
Console.WriteLine(1.GetType());           // Int32
Console.WriteLine(0xF0000000.GetType());  // UInt32
Console.WriteLine(0x100000000.GetType()); // Int64

  • 모든 자료형은 object 클래스를 항속한것이다.

숫자 접미사(Suffix)

  • 리터럴의 타입을 명시적으로 지정할 수 있음:
    • F: float
    • D: double
    • M: decimal
    • U: uint
    • L: long
    • UL: ulong
float f = 1.0F;
double d = 1D;
decimal m = 1.0M;
uint i = 1U;
long l = 1L;
ulong ul = 1UL;

  • 주의사항:
    • 접미사는 대소문자를 구분하지 않음 (1F와 1f는 동일)
    • 실수 리터럴에서 f나 F 접미사는 필수임 (float 타입)
    • d나 D 접미사는 생략 가능 (실수의 기본 타입이 double)

숫자 형변환(Numeric Conversions)

  • 더 큰 범위로의 변환은 암시적으로 가능
  • 더 작은 범위로의 변환은 명시적 캐스팅 필요
int x = 12345;       // int는 32비트 정수
long y = x;          // 암시적 변환 가능 (32비트 → 64비트)
short z = (short)x;  // 명시적 변환 필요 (32비트 → 16비트)

부동소수점 간의 변환

  • float → double: 암시적 변환 가능
  • double → float: 명시적 변환 필요

정수와 부동소수점 간의 변환

  • 모든 정수 타입은 부동소수점으로 암시적 변환 가능
  • 부동소수점에서 정수로는 명시적 변환 필요
int i = 1;
float f = i;      // 암시적 변환
int i2 = (int)f;  // 명시적 변환 필요

// 주의: 큰 정수를 float로 변환 시 정밀도 손실 가능
int i1 = 100000001;
float f = i1;          // 정밀도는 유지되나 정확도 손실
int i2 = (int)f;       // 100000000

산술 연산자(Arithmetic Operators)

  • 기본 산술 연산자:
    • + (덧셈)
    • - (뺄셈)
    • * (곱셈)
    • / (나눗셈)
    • % (나머지)

증가/감소 연산자

  • ++: 값을 1 증가
  • --: 값을 1 감소
  • C++ C# 차이
  • 전위와 후위 형식의 차이:
int x = 0, y = 0;
Console.WriteLine(x++);    // 출력: 0, x는 1
//x++ + x++ => 출력 : 1 x++(바뀌기 전) + x++(바뀐 후)
Console.WriteLine(++y);    // 출력: 1, y는 1

정수 타입의 특수 연산

  • 정수 나눗셈은 소수부분을 버림
  • 0으로 나누면 DivideByZeroException 발생
int a = 2 / 3;    // 결과: 0
int b = 0;
int c = 5 / b;    // DivideByZeroException 발생

오버플로우(Overflow)

  • 표현할 수 있는 값의 범위를 넘어갈때 발생한다.
  • 기본적으로 오버플로우는 무시됨
  • checked 연산자로 검사 가능
int a = int.MinValue;
a--;                     // 오버플로우: int.MaxValue가 됨

checked
{
    int c = int.MaxValue;
    c++;                 // OverflowException 발생
}

// 단일 식에 대한 검사
int y = checked(x + 1);

unchecked 연산자

  • 컴파일러 수준의 확인을 비활성화함
int x = int.MaxValue;
int y = unchecked(x + 1);  // 오버플로우 허용

비트 연산자(Bitwise Operators)

  • C#은 다음과 같은 비트 연산자를 제공함:

연산자 의미 예제 결과

~ 보수(Complement) ~0xfU 0xfffffff0U
& AND 0xf0 & 0x33 0x30
    OR 0xf0
^ XOR(베타적 OR) 0xff00 ^ 0x0ff0 0xf0f0
<< 왼쪽 시프트 0x20 << 2(비트의 갯수) 0x80
>> 오른쪽 시프트 0x20 >> 1 0x10
  • 시프트 연산을 2로 곱하거나 나누는것과 같은 연산의 결과가 나온다.

비트 연산자 활용 예제

  • byte : 부호가 없는 정수형

비트 연산자 활용 - ARGB 쉬프트 연산주의사항

  • 시프트 연산 시 비트 수가 타입의 크기를 넘어가면 무시됨
  • 부호 있는 타입의 오른쪽 시프트(>>)는 부호를 유지함

8비트와 16비트 정수 타입

  • byte, sbyte, short, ushort는 자체 산술 연산자가 없음
  • 연산 시 자동으로 int로 변환됨
    • 정수의 승격 이라고 부른다.
short x = 1, y = 1;
short z = x + y;         // 컴파일 오류
short z = (short)(x + y);// 정상 동작

특수한 부동소수점 값특수값 설명 Double 상수 Float 상수

NaN Not a Number double.NaN float.NaN
+∞ 양의 무한대 double.PositiveInfinity float.PositiveInfinity
-∞ 음의 무한대 double.NegativeInfinity float.NegativeInfinity
-0 음의 0 -0.0 -0.0f
Console.WriteLine(double.NegativeInfinity);  // -Infinity
Console.WriteLine(1.0 / 0.0);                // Infinity
Console.WriteLine(-1.0 / 0.0);               // -Infinity
Console.WriteLine(1.0 / -0.0);               // -Infinity
Console.WriteLine(-1.0 / -0.0);              // Infinity

  • 실수는 0으로 나눠도 컴파일 오류가 생기지 않는다.

NaN(불능) 관련 연산

  • 0을 0으로 나누거나 무한대에서 무한대를 뺄 때 발생
  • NaN 값과의 비교는 항상 false
  • IsNaN : 불능 확인 함수
  • Equals : 같은지 확인하는 함수
Console.WriteLine(0.0 / 0.0);                // NaN
Console.WriteLine((1.0/0.0) - (1.0/0.0));    // NaN

// NaN 비교
Console.WriteLine(0.0 / 0.0 == double.NaN);   // False
Console.WriteLine(double.IsNaN(0.0 / 0.0));   // True

// object.Equals 사용 시
Console.WriteLine(object.Equals(0.0/0.0, double.NaN)); // True

double과 decimal 비교항목 double decimal

내부 표현 2진수 기반 10진수 기반 (96비트 정수 + 스케일)
값 표현 방식 2의 거듭제곱의 합 (1/2, 1/4, 1/8, ...) 10의 거듭제곱의 합 (1/10, 1/100, ...)
0.1 표현 근사값 (0.000110011001100... 무한반복) 정확한 값
주요 용도 과학/공학 계산 금융 계산
정밀도 15-16자리 (10진수) 28-29자리 (10진수)
처리 방식 CPU 하드웨어 가속 (IEEE 754) 소프트웨어 처리
연산 속도 빠름 느림 (약 10배 차이)

반올림 오차 문제

  • float/double은 이진 소수점으로 인한 오차 발생:
float x = 0.1f;
Console.WriteLine(x + x + x + x + x + x + x + x + x + x);
// 1.0000001 출력

decimal m = 1M / 6M;  // 0.1666666666666666666666666667M
double d = 1.0 / 6.0; // 0.16666666666666666

// 반복 연산에서의 오차
decimal notQuiteWholeM = m + m + m + m + m + m;
// 1.0000000000000000000000000002M

double notQuiteWholeD = d + d + d + d + d + d;
// 0.99999999999999989

// 비교 연산에서의 문제
Console.WriteLine(notQuiteWholeM == 1M);  // False
Console.WriteLine(notQuiteWholeD < 1.0);  // True

C# 불 타입과 연산자

불 타입의 기초

  • 불리언(Boolean) 타입은 true/false 두 가지 값만 가질 수 있는 논리값을 표현하는 타입임
  • C#의 bool 키워드는 System.Boolean 타입의 별칭임
  • 메모리 상에서는 1비트만 필요하지만, 실제로는 1바이트를 사용함
    • CPU가 효율적으로 처리할 수 있는 최소 단위가 1바이트이기 때문임

배열에서의 메모리 효율

  • 많은 수의 불리언 값을 저장할 때는 BitArray 클래스 사용을 고려해야 함
    • System.Collections 네임스페이스에 정의되어 있음
    • 각 불리언 값당 1비트만 사용하므로 메모리 효율적임

불리언 형변환

  • bool 타입과 숫자 타입 사이의 직접적인 형변환은 불가능함
  • 명시적(explicit) 캐스팅도 허용되지 않음
int i = 1;
bool b = (bool)i;  // 컴파일 오류

동등성과 비교 연산자

  • == : 두 값이 같은지 비교
  • != : 두 값이 다른지 비교
  • 동등성 연산은 항상 bool 값을 반환함
int x = 1;
int y = 2;
int z = 1;
Console.WriteLine (x == y); // False
Console.WriteLine (x == z); // True

참조 타입의 동등성

  • 값형 → 값을 비교 / 참조형 → 같은 ‘객체’를 참조하고 있는지 비교
  • 참조 타입의 경우 기본적으로 참조(reference)의 동등성을 비교함
  • 같은 내용이라도 다른 객체이면 false를 반환함
  • class가 아니라 struct를 비교하는것은 참조 비교가 아니라 값을 비교하는 것이다.
Dude d1 = new Dude ("John");
Dude d2 = new Dude ("John");
Console.WriteLine (d1 == d2); // False - 다른 객체임

Dude d3 = d1;
Console.WriteLine (d1 == d3); // True - 같은 객체를 참조함

public class Dude
{
    public string Name;
    public Dude (string n) { Name = n; }
}

조건 연산자

  • && : 조건부 AND 연산자 (둘 다 true일 때만 true)
  • || : 조건부 OR 연산자 (하나라도 true면 true)
  • ! : NOT 연산자 (true를 false로, false를 true로)
static bool UseUmbrella (bool rainy, bool sunny, bool windy)
{
    return !windy && (rainy || sunny);
}

단락 평가(Short-circuiting)

  • && 연산자는 첫 번째 조건이 false면 두 번째 조건을 평가하지 않음
  • || 연산자는 첫 번째 조건이 true면 두 번째 조건을 평가하지 않음
  • 이를 통해 null 체크 등을 안전하게 수행할 수 있음
// sb가 null이면 Length 속성 접근 전에 평가 중단
if (sb != null && sb.Length > 0) { ... }

비 단락 연산자(&와 |)

  • & : 논리 AND (항상 양쪽 피연산자 평가)
  • | : 논리 OR (항상 양쪽 피연산자 평가)
  • 상태 변경이 필요한 경우가 아니면 &&와 || 사용을 권장함
return !windy & (rainy | sunny);  // 양쪽 모두 평가됨

조건(삼항) 연산자

  • ? : 연산자는 세 개의 피연산자를 가지는 유일한 연산자임
  • 조건식 ? 참일때값 : 거짓일때값 형식으로 사용함
static int Max (int a, int b)
{
    return (a > b) ? a : b;
}

문자 타입과 리터럴

  • 문자(char) 타입은 유니코드 문자를 나타내는 2바이트(UTF-16) 크기의 타입임
  • 문자 리터럴은 작은따옴표로 표현함:
char c = 'A';   // 단순 문자

이스케이프 시퀀스(Escape Sequence)

  • 직접 표현하기 어려운 문자는 이스케이프 시퀀스로 표현함
  • 역슬래시(\\)와 특별한 의미를 가진 문자의 조합으로 구성됨
char newLine = '\\n';     // 줄바꿈
char backSlash = '\\\\';   // 역슬래시

  • 주요 이스케이프 시퀀스 목록:

시퀀스 의미

\' 작은따옴표
\" 큰따옴표
\\ 역슬래시
\0 널 문자
\b 백스페이스
\f 폼피드
\n 줄바꿈
  • 유니코드 문자는 \u 또는 \x를 사용하여 16진수로 표현할 수 있음:
char copyrightSymbol = '\\u00A9';  // © 문자
char omegaSymbol = '\\u03A9';      // Ω 문자
char newLine = '\\u000A';          // 줄바꿈

문자 형변환

  • char는 unsigned short와 같은 크기를 가짐
  • char보다 큰 숫자 타입으로의 암시적 변환이 가능함
  • 작은 크기의 타입으로 변환할 때는 명시적 캐스팅이 필요함

문자열 타입

  • 문자열(string)은 불변(immutable)한 유니코드 문자의 시퀀스를 나타내는 참조 타입임
  • 문자열 리터럴은 큰따옴표로 표현함:
string a = "test";
string b = "test";
Console.WriteLine(a == b);  // True - 값 비교

  • string은 == 연산자 재정의가 되어있다.
    • 객체 비교가 아닌 값 비교로 이루어진다.
  • 문자 이스케이프 시퀀스를 문자열에서도 사용할 수 있음:
string a = "Here's a tab:\\t";

  • 역슬래시가 많이 포함된 경우 가독성이 떨어짐:
string path = "\\\\\\\\server\\\\fileshare\\\\helloworld.cs";

축자 문자열(Verbatim String)

  • @ 기호로 시작하는 문자열은 이스케이프 시퀀스를 처리하지 않음:
    • \가 일반 문자열이 된다.
string path = @"\\server\\fileshare\\helloworld.cs";

  • 여러 줄의 문자열을 직접 표현할 수 있음:
string escaped = "First Line\\r\\nSecond Line";
string verbatim = @"First Line
Second Line";

  • 줄바꿈도 같이 출력된다.
  • 축자 문자열에서 큰따옴표는 두 번 연속 사용:
string xml = @"<customer id=""123""></customer>";

  • 2개의 큰 따옴표가 하나의 큰따옴표 문자로 출력된다.

문자열 연산

  • 연산자로 문자열을 연결할 수 있음:
string s = "a" + "b";  // "ab"

  • 다른 타입의 값도 자동으로 ToString() 호출하여 연결:
string s = "a" + 5;    // "a5"

문자열 보간법(String Interpolation)

  • $ 기호로 시작하는 문자열에서 중괄호 안의 식을 값으로 대체함:
int x = 4;
Console.WriteLine($"A square has {x} sides");  // "A square has 4 sides"

  • 중괄호 안의 식은 모든 C# 식이 가능하며 ToString()이 자동 호출됨
  • 형식 지정자를 콜론 뒤에 추가할 수 있음:
string s = $"255 in hex is {byte.MaxValue:X2}";  // "255 in hex is FF"

  • X : 16진수
    • X2 : 16진수 2자리를 찍어라
  • 조건 연산자 등에서 콜론이 필요한 경우 전체 식을 괄호로 묶음:
bool b = true;
Console.WriteLine($"The answer is {(b ? 1 : 0)}");

  • 보간 문자열은 한 줄에 작성해야 하지만, @를 함께 사용하면 여러 줄 가능:
int x = 2;
string s = $@"this interpolation
spans {x} lines";

  • 문자열에서 중괄호를 표현하려면 중괄호를 두 번 사용함

배열

배열의 기초

  • 배열은 고정된 개수의 같은 타입 변수들을 순차적으로 저장하는 자료구조임
  • 배열의 요소들은 메모리에 연속적으로 저장되어 빠른 접근이 가능함
  • 모든 배열은 System.Array 클래스를 상속받음

배열의 선언과 사용

  • C#의 배열은 []를 ‘인덱서’라고 부르며 재정의가 되어있다.

기본 선언과 접근

  • 배열 타입은 요소 타입 뒤에 대괄호([])를 붙여 표현함:
// 5개의 문자를 저장할 수 있는 배열 선언
char[] vowels = new char[5];

// 인덱스로 배열 요소에 접근 (0부터 시작)
vowels[0] = 'a';
vowels[1] = 'e';
vowels[2] = 'i';
vowels[3] = 'o';
vowels[4] = 'u';

Console.WriteLine(vowels[1]); // 출력: e

  • 배열도 참조형이기에 null을 할당할 수 있다.
  • 반복문으로 배열의 모든 요소에 접근할 수 있음:
// Length 속성으로 배열 크기 확인
for (int i = 0; i < vowels.Length; i++)
    Console.Write(vowels[i]); // 출력: aeiou

배열 초기화식

  • 선언과 동시에 배열을 초기화할 수 있음:
// 초기화식 사용
char[] vowels = new char[] {'a','e','i','o','u'};

// 더 간단한 형태 - 이렇게만 적어도 가능하다.
char[] vowels = {'a','e','i','o','u'};

배열의 기본값 초기화

  • 배열 생성시 모든 요소는 해당 타입의 기본값으로 초기화
int[] a = new int[1000];
Console.WriteLine(a[123]); // 출력: 0

  • 값 타입 배열의 경우 각 요소가 배열의 일부로 할당됨:
Point[] a = new Point[1000]; //포인트 객체가 1000개 만들어진다.
int x = a[500].X; // 0
public struct Point { public int X, Y; }

  • 참조 타입 배열의 경우 각 요소는 null로 초기화됨:
Point[] a = new Point[1000];
int x = a[500].X; // 런타임 에러: NullReferenceException
public class Point { public int X, Y; }

// 참조 타입 배열을 사용하려면 각 요소를 초기화해야 함
Point[] a = new Point[1000];
for (int i = 0; i < a.Length; i++)
    a[i] = new Point();

인덱스와 범위 (C# 8.0 이상)

  • ^ 연산자를 사용하여 배열의 끝에서부터 요소에 접근할 수 있음:
char[] vowels = new char[] {'a','e','i','o','u'};
char lastElement = vowels[^1];    // 'u'
char secondToLast = vowels[^2];   // 'o'

  • Index 타입을 사용할 수도 있음:
Index first = 0;
Index last = ^1;
char firstElement = vowels[first]; // 'a'
char lastElement = vowels[last];   // 'u'

범위 연산자(..)

  • .. 연산자로 배열의 일부분을 선택할 수 있음:
char[] firstTwo = vowels[..2];     // 'a', 'e'
char[] lastThree = vowels[2..];    // 'i', 'o', 'u'
char[] middleOne = vowels[2..3];   // 'i'

  • ^ 연산자와 함께 사용 가능:
char[] lastTwo = vowels[^2..];     // 'o', 'u'

  • Range 타입 사용 가능:
Range firstTwoRange = 0..2;
char[] firstTwo = vowels[firstTwoRange]; // 'a', 'e'

다차원 배열

  • 쉼표로 구분된 차원 수를 지정:
int[,] matrix = new int[3,3];

// GetLength() 메서드로 각 차원의 길이 확인
for (int i = 0; i < matrix.GetLength(0); i++)
    for (int j = 0; j < matrix.GetLength(1); j++)
        matrix[i,j] = i * 3 + j;

// 초기화식 사용
int[,] matrix = new int[,]
{
    {0,1,2},
    {3,4,5},
    {6,7,8}
};

  • int[,] : 2차원 배열

가변 배열(Jagged Array)

  • 배열의 배열을 나타내며, 각 하위 배열의 길이가 다를 수 있음:
int[][] matrix = new int[3][];

// 각 하위 배열을 개별적으로 초기화
//matrix.Length ; 3
for (int i = 0; i < matrix.Length; i++)
{
    matrix[i] = new int[3];
    for (int j = 0; j < matrix[i].Length; j++)
        matrix[i][j] = i * 3 + j;
}

// 초기화식 사용
int[][] matrix = new int[][]

    new int[] {0,1,2},
    new int[] {3,4,5},
    new int[] {6,7,8,9}
};

경계 검사

  • 모든 배열 접근은 실행 시점에 경계 검사를 수행함
  • 유효하지 않은 인덱스 사용시 IndexOutOfRangeException 발생:
int[] arr = new int[3];
arr[3] = 1; // IndexOutOfRangeException 발생

변수와 메모리 구조

변수의 개념

  • 변수(Variable)는 수정 가능한 값을 저장하는 메모리 공간임
  • 프로그램 실행 중에 다른 값으로 변경될 수 있음
  • 다음과 같은 종류가 있음:
    • 지역 변수(Local variable): 메서드 내에서 선언된 변수
    • 매개변수(Parameter): value, ref, out, in 형태로 메서드에 전달되는 변수
    • 필드(Field): 클래스나 구조체에 속한 변수(인스턴스 또는 정적)
    • 배열 요소(Array element): 배열의 각 항목을 저장하는 변수

스택과 힙의 메모리 구조

  • 변수가 저장되는 두 가지 주요 메모리 영역이 있음:
    • 스택(Stack): 메서드 호출과 관련된 데이터를 저장하는 영역
    • 힙(Heap): 객체가 저장되는 영역

스택 메모리의 특징

  • 메서드가 호출될 때마다 스택 프레임이 생성됨
  • 지역 변수매개변수가 스택에 저장됨
  • 메서드 종료시 자동으로 정리됨
  • 후입선출(LIFO: Last In First Out) 구조임
static int Factorial(int x)
{
    // 매개변수 x와 반환값이 스택에 저장됨
    if (x == 0) return 1;
    return x * Factorial(x-1);  // 재귀 호출마다 새로운 스택 프레임 생성
}

// 사용 예
Console.WriteLine(Factorial(4));  // 출력: 24

힙 메모리의 특징

  • 객체(참조 타입의 인스턴스)가 저장되는 영역임
    • C++의 동적할당을 생각하면 된다.
    • 한번 만들어지면 계속 유지된다.
  • 가비지 컬렉터(GC)가 자동으로 메모리를 관리함
    • 메모리 릭(누수)이 발생하지 않는다.
    • 삭제되는 타이밍 : 참조하는 변수가 없는 메모리가 생길때 가비지 컬렉터가 자동으로 메모리를 삭제해준다.
      • 삭제는 정해진 타이밍이 없고 가비지 컬렉터가 작동하는 시기에 삭제되기에 삭제되는 타이밍을 사용할 수 없다.
    • 단점 : 성능에 손해가 발생한다.
      • 프로그래머가 원할때 메모리 해제가 불가능하다.
        • A씬에서 명시적으로 해제하고 B씬으로 넘어가고 싶지만 그런것은 불가능해진다.
  • 참조되지 않는 객체는 GC에 의해 제거됨
using System.Text;

StringBuilder ref1 = new StringBuilder("object1");
Console.WriteLine(ref1);        // 출력: object1
// 이 시점에서 ref1이 참조하는 객체는 곧 GC 대상이 됨

StringBuilder ref2 = new StringBuilder("object2");
StringBuilder ref3 = ref2;      // ref2와 ref3가 같은 객체를 참조
Console.WriteLine(ref2);        // 출력: object2
Console.WriteLine(ref3);        // 출력: object2
// 두 참조가 같은 객체를 가리키므로 아직 GC 대상이 아님

  • ref1 : 스택 영역에 올라가는 변수
  • new StringBuilder("object1") : 힙 영역에 저장되는 메모리공간

명확한 할당(Definite Assignment)

  • C#은 엄격한 변수 할당 규칙을 가짐
  • 이는 초기화되지 않은 변수 사용으로 인한 버그를 방지함
  • 주요 규칙:
    1. 지역 변수는 사용하기 전에 반드시 할당되어야 함
    2. 메서드 매개변수는 호출 시점에 값이 제공되어야 함
    3. 필드와 배열 요소는 자동으로 기본값으로 초기화됨
int x;
Console.WriteLine(x);         // 컴파일 오류: x가 할당되지 않음

int[] ints = new int[2];
Console.WriteLine(ints[0]);   // 출력: 0 (배열 요소는 자동 초기화)

class Test {
    public static int X;      // 필드는 자동으로 0으로 초기화
}
Console.WriteLine(Test.X);    // 출력: 0

기본값(Default Values)

  • 자동 초기화되는 경우의 기본값:
    • 숫자 타입: 0
    • bool 타입: false
    • char 타입: '\0'
    • 참조 타입: null
    • 구조체: 모든 필드가 기본값으로 초기화된 상태

매개변수 수정자(Parameter Modifiers)

  • C#의 기본 매개변수 전달 방식임
  • 값의 복사본이 메서드에 전달됨
  • 메서드 내부에서 매개변수를 수정해도 원본은 변경되지 않음
int x = 8;
Foo(x);                    // x의 복사본이 전달됨
Console.WriteLine(x);      // 출력: 8 (원본 값은 변경되지 않음)

static void Foo(int p)
{
    p = p + 1;            // 복사본만 수정됨
    Console.WriteLine(p); // 출력: 9
}

참조 타입의 값 전달

  • 참조의 복사본이 전달됨
  • 객체의 멤버는 수정 가능하지만 참조 자체는 변경 불가
StringBuilder sb = new StringBuilder();
Foo(sb);
Console.WriteLine(sb.ToString());    // 출력: test

static void Foo(StringBuilder fooSB)
{
    fooSB.Append("test");           // 원본 객체 수정 가능
    fooSB = null;                   // 참조 복사본만 변경됨
}

ref 수정자(Reference Modifier) - 가장 제약이 없다

  • 참조로 매개변수를 전달함
  • 메서드가 원본 변수의 저장 공간을 직접 사용함
  • 호출자와 메서드가 같은 메모리 위치를 공유함
string x = "Penn";
string y = "Teller";
Swap(ref x, ref y);              // ref 키워드로 호출
Console.WriteLine(x);            // 출력: Teller
Console.WriteLine(y);            // 출력: Penn

static void Swap(ref string a, ref string b)
{
    string temp = a;
    a = b;
    b = temp;
}

out 수정자

  • ref와 유사하지만 초기값이 필요 없음
  • 메서드는 반드시 out 매개변수에 값을 할당해야 함
    • null 할당도 가능하다
  • 여러 값을 반환할 때 유용함
string a, b;
Split("Stevie Ray Vaughan", out a, out b);
Console.WriteLine(a);           // 출력: Stevie Ray
Console.WriteLine(b);           // 출력: Vaughan

void Split(string name, out string firstNames, out string lastName)
//void Split(string name, out string firstNames, out string_) //이것도 가능하다
{
    int i = name.LastIndexOf(' ');
    firstNames = name.Substring(0, i);
    lastName = name.Substring(i + 1);
}

out 변수 선언과 무시

  • C# 7부터 out 매개변수를 호출 시점에 선언 가능
  • 불필요한 out 매개변수는 _로 무시 가능
// 호출 시점에 변수 선언
Split("Stevie Ray Vaughan", out string a, out string b);

// 두 번째 매개변수 무시
Split("Stevie Ray Vaughan", out string a, out _);

// 여러 개의 out 매개변수 중 일부만 사용
SomeBigMethod(out _, out _, out _, out int x, out _, out _, out _);

in 수정자

  • 참조로 전달하되 수정을 방지함
  • 큰 구조체를 효율적으로 전달할 때 유용함
  • 메서드 내에서 매개변수 수정 시도 시 컴파일 오류 발생
// in 매개변수 정의
void Foo(in SomeBigStruct a) { ... }

// 호출 방법
SomeBigStruct x = ...;
Foo(x);      // in 키워드 생략 가능
Foo(in x);   // in 키워드 명시적 사용

params 수정자 - 가변 매개변수

  • 메서드의 마지막 매개변수에 사용됨
  • 가변 개수의 인수를 배열로 전달받을 수 있음
  • 배열 타입으로 선언해야 함
int total = Sum(1, 2, 3, 4);
Console.WriteLine(total);            // 출력: 10

// 배열로도 전달 가능
int total2 = Sum(new int[] { 1, 2, 3, 4 });
Console.WriteLine(total2);           // 출력: 10

int Sum(params int[] ints)
{
    int sum = 0;
    for (int i = 0; i < ints.Length; i++)
        sum += ints[i];
    return sum;
}

 

  • params 위치에 인수가 없으면 빈 배열이 생성됨
  • 일반 배열을 params 매개변수로 전달할 수도 있다