C# - 값 타입 (Value Type), 참조 타입 (Reference Type)

2025. 9. 10. 17:43·C#

이전 글에서 배열에 대해 다루다가 값 타입과 참조 타입에 대한 이야기가 나왔었다.

원래 이번 글에서는 함수에 대한 포스팅을 할 예정이었으나 값 타입(Value Type)과 참조 타입(Reference Type)에 엮인 내용이 많아 따로 짚고 넘어가는 것이 좋을 거 같다.

 

값 타입 / 참조 타입 복습

  • 값 타입 (Value Type)
    • bool, byte, int, double, decimal, char // struct, enum
    • 대입 / 인수 전달시 값 자체가 복사
    • 일반적으로 스택(Stack)에 저장되지만, 예외적으로 박싱(boxing)되거나 참조 타입의 필드로 존재하면 힙(Heap)에 배치될 수 있음.
  • 참조 타입 (Reference Type)
    • class, string, array, delegate, object
    • 변수에는 객체 주소(참조)가 저장되고, 실제 객체는 보통 힙(Heap)에 있다.
    • 대입 / 인수 전달 시 참조(주소)가 복사 ( = 같은 객체를 가리키는 주소 )

참고 : string은 참조 타입이지만 불변성 때문에 값 타입 처럼 다루는 느낌을 줌


스택(Stack) / 힙(Heap)

메모리 배치와 수명

  • 스택 (Stack)
    • 메서드 호출 시 생성되는 지역 변수/매개변수의 저장소
    • 후입선출 구조 (LIFO: Last In First Out)
    • 스코프가 끝나면 자동 해제
    • 접근 비용이 낮고 빠름
  • 힙 (Heap)
    • 동적 할당 영역
    • new로 생성된 객체의 실제 데이터 저장
    • GC(Garbage Collector)가 접근 불가능해진 객체를 수거
    • 동적 할당/해제로 유연하지만 관리 비용 존재

값 복사 / 참조 복사

값 타입을 넘기면 값 자체가 복사 된다

참조 타입을 넘기면 참조 주소가 값처럼 복사되어 같은 원본을 가리킨다.

 

말로만 하면 이해가 잘 되지 않을 수 있으니 코드로 살펴보자.

 

값 타입의 경우

struct Point { public int X, Y; }

void MoveX(Point p)      // p는 호출 시 '값 복사본'
{
    p.X += 10;           // 복사본만 변경
}

var p0 = new Point { X = 1, Y = 2 };
MoveX(p0);				// p0.X == 1 (원본 변경 없음)

 

참조 타입의 경우

class Person { public string Name; }

void Rename(Person p)
{
    p.Name = "Bob";      // 같은 객체의 필드 변경 → 호출자에 반영됨
}

void Reassign(Person p)
{
    p = new Person { Name = "Carol" }; // 매개변수 복사본의 참조만 바뀜
}

var a = new Person { Name = "Alice" };
Rename(a);    // a.Name == "Bob"
Reassign(a);  // a.Name == "Bob" (여전히 같은 객체)

 

배열(Array)은 참조 타입

void ZeroFirst(int[] arr)
{
    arr[0] = 0;          // 같은 배열 인스턴스의 요소 변경 → 호출자에 반영
}

void ReassignArray(int[] arr)
{
    arr = new int[] { 9, 9, 9 }; // 매개변수 참조만 교체, 호출자에 영향 없음
}

int[] xs = { 1, 2, 3 };
ZeroFirst(xs);     // xs == [0, 2, 3]
ReassignArray(xs); // xs == [0, 2, 3]

 

string은 참조 타입이지만 불변

void Upper(string s)
{
    s = s.ToUpperInvariant();	// 새 문자열 생성, 매개변수 참조만 변경
}

string t = "hello";
Upper(t);			// t == "hello"  (원본 불변)

 

문자열을 수정하려면 새 문자열을 반환하거나 StringBuilder를 사용하는 등 다른 방식을 생각해보아야 한다.


얕은 복사 (Shallow) / 깊은 복사 (Deep)

 

얕은 복사 : 객체의 필드 값을 1단계만 복사. 필드가 참조 타입이면 참조(주소)만 공유.

깊은 복사 : 객체가 가진 중첩 참조까지 새 인스턴스를 만들어 완전 독립 복제함

 

얕은 복사 예시

class Address { public string City; }

class Person
{
    public string Name;
    public Address Addr;

    // MemberwiseClone()은 protected이므로 클래스 내부에서만 호출 가능
    public Person ShallowCopy() => (Person)this.MemberwiseClone();
}

var p1 = new Person { Name = "Alice", Addr = new Address { City = "Seoul" } };
var p2 = p1.ShallowCopy();       // 얕은 복사
p2.Name = "Bob";
p2.Addr.City = "Busan";

// 결과
// p1.Name == "Alice"         (값 타입/문자열 참조 자체는 교체되어 독립)
// p1.Addr.City == "Busan"    (중첩 참조(Address)는 공유 → 내부 변경이 반영)

 

깊은 복사 예시

class Address { public string City; }

class Person
{
    public string Name;
    public Address Addr;

    public Person DeepCopy()
    {
        return new Person
        {
            Name = this.Name,                           // string은 불변 → 복사로 충분
            Addr = new Address { City = this.Addr.City } // 중첩 참조를 '새로' 생성
        };
    }
}

var p1 = new Person { Name = "Alice", Addr = new Address { City = "Seoul" } };
var p2 = p1.DeepCopy();
p2.Addr.City = "Busan";

// 결과
// p1.Addr.City == "Seoul"   (완전 독립 복제)

 

배열, 컬렉션에서의 복사

Array.Clone(), Array.Copy(...), new List<T>(old) 는 요소를 대입해서 채움

요소가 값 타입이면 사실상 깊은 복사와 동일한 효과

요소가 참조 타입이면 얕은 복사

class Node { public int V; }

var a1 = new[] { new Node{ V = 1 }, new Node{ V = 2 } };
var a2 = (Node[])a1.Clone();              // 얕은 복사(배열 인스턴스만 새 것, 요소 참조는 공유)
a2[0].V = 99;

// a1[0].V == 99 (요소 객체는 동일 참조)

var a3 = a1.Select(n => new Node { V = n.V }).ToArray(); // 깊은 복사
a3[0].V = 7;
// a1[0].V == 99 (완전 독립)

 


박싱 (Boxing) / 언박싱 (Unboxing)

 

박싱 (Boxing) : 값 타입을 참조 타입으로 힙에 포장하는 것

언박싱 (Unboxing) : 박싱된 참조에서 원래 값 타입으로 캐스팅하는 것

int n = 10;
object boxed = n;       // Boxing (값 타입 → 힙 객체)
int m = (int)boxed;     // Unboxing (참조 → 값 타입)

 

박싱이 발생하는 경우

1. 비제네릭 컬렉션에 값 타입을 넣을 때

var list = new System.Collections.ArrayList();
list.Add(42);             // Boxing
int x = (int)list[0];     // Unboxing

 

2. 값 타입을 object/인터페이스 파라미터로 넘길 때

interface IHasArea { double Area(); }

struct Rect : IHasArea
{
    public double W, H;
    public double Area() => W * H;
}

IHasArea r = new Rect { W = 3, H = 4 }; // 인터페이스 변수에 담는 순간 Boxing
double a = r.Area();

 

불필요한 박싱은 힙 할당 + GC 압박 + 캐스팅 비용을 유발한다고 하니 사용시 주의하자.

 

 

 

이렇게 참조타입과 관련된 키워드 몇가지에 대해 간단히 알아봤다.

개인적으로 나머지는 다 어렵지 않은 내용 같은데 박싱/언박싱 개념이 잘 와닿지 않는 것 같다.

이후 기회가 된다면 다시 자세히 다루어 보도록 하자.

 

'C#' 카테고리의 다른 글

C# - 배열 심화 문제 - 2차원 배열 swap  (0) 2025.09.11
C# - 함수 (Function)  (0) 2025.09.10
C# - 배열 ( Array )  (0) 2025.09.08
C# - 콘솔로 별 찍기 (다이아몬드 편)  (0) 2025.09.08
C# - 콘솔로 별 찍기 (삼각형 편)  (0) 2025.09.04
'C#' 카테고리의 다른 글
  • C# - 배열 심화 문제 - 2차원 배열 swap
  • C# - 함수 (Function)
  • C# - 배열 ( Array )
  • C# - 콘솔로 별 찍기 (다이아몬드 편)
ellug
ellug
Web / App / Game Dev
  • ellug
    dev.ellug
    ellug
  • 전체
    오늘
    어제
    • 분류 전체보기 (95)
      • Javascript (React, Next) (1)
      • C# (66)
      • Graphics (1)
      • Unity (25)
      • 프로그래머스 (0)
        • Lv.0 (0)
        • Lv.1 (0)
        • Lv.2 (0)
        • Lv.3 (0)
      • AI Agent (0)
      • StableDiffusion (0)
      • ETC (1)
      • 잡담 (0)
      • 유머 (0)
  • 블로그 메뉴

    • 홈
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    클래스
    struct
    console
    list
    콘솔
    추상 클래스
    class
    unity
    Photon
    네트워크
    firebase
    delegate
    터미널
    function
    Queue
    inputsystem
    Network
    큐
    Design Pattern
    Abstract
    게임
    유니티
    최적화
    자료구조
    useState
    interface
    알고리즘
    스택
    stack
    C#
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.4
ellug
C# - 값 타입 (Value Type), 참조 타입 (Reference Type)
상단으로

티스토리툴바