Sugar

[C#] Object Oriented Programming#01 | 객체 지향, 복사와 참조, 스택과 힙

by Sugar0810

※ 객체 지향의 특징 및 절차 지향과 차이점

  • 절차 지향 - 코드가 전부 함수로 구성.
    • 프로그램 규모가 커지면 복잡해진다.(유지 보수 면에서 불리)
      • 함수 호출 순서에 종속적이기 때문에 호출 순서 타고타고 올라가 보며 코드를 읽어보아야 해서 불편하다.
    • 함수를 계속 새로 만들고 만들고 해야해서 복잡하다.
      • 같은 Fight 이라도 플레이어 vs 플레이어, 보스 vs 플레이어 등등 많은 버전이 있어야 하므로 오버로딩을 많이 해야 해서 복잡해진다.
  • 객체 지향 - 모든 것을 객체 위주로 생각.
    • 속성(멤버 변수)와 기능(멤버 함수)로 나뉜다.
    • ‘플레이어’의 이름, 공격력, HP, 직업 같은 속성들과 Fight 같은 기능을 추상화 하여 Player라는 이름의 클래스로 묶는다.
      • 이 클래스(붕어빵틀, 설계도)를 가지고 세상에 존재하는 객체(실제 붕어빵)로 찍어낼 수 있다.
      class Knight
      {
          public int hp;
          public int attack;
      
          public void Move() { Console.Write("Kight Move"); }
          public void Attack() { Console.Write("Kight Attack"); }
      }
      
      class Program
      {
          static void Main(string [] args)
          {
              Knight knight = new Knight();
      
              knight.hp = 100;
              knight.attack = 10;
              knight.Move();
              knight.Attack();
          }
      }
      • Knight 클래스 = 설계도
        • 속성 : hp, attack -> 실존하는 모든 Knight 타입 객체들은 이 속성을 가진다.
        • 기능 : Move(), Attack() -> 실존하는 모든 Knight 타입 객체들은 이 기능을 가진다.
      • 클래스는 단순 설계도일 뿐 이 클래스로 찍어낸 Knight 객체들은 속성 값이 제각각 다를 수 있다.
      • new
        • C++과 달리 C# 에선 객체 생성시 new를 사용하여 생성해야 한다.
        • Kignt knight만으로는 메모리 할당이 되어 있지 않아 객체 생성이 안됨

 

※ 복사와 참조

구조체와 클래스의 차이

구조체(Struct) -> Call by Value 방식으로 작업한다. (복사)

  • 예를 들어 이름이 mageMage타입의 구조체(Struct)가 있다면 Mage mage2 = mage 하여도 magemage2는 별개의 메모리다. mage2mage의 값들을 깊은 복사해 온 사본일 뿐이다.
  • 객체와 달리 구조체는 Mage mage; 만 해도 메모리가 할당이 된다.

클래스(Class) -> Call by Reference 방식으로 작업한다. (참조)

  • 예를 들어 이름이 knightKnight 클래스 타입의 객체가 있다면 Knight knight2 = knight 하면 knight2knight는 동일한 메모리를 참조하게 된다. 즉 이 둘은 동일한 객체를 접근하다. 데이터를 복사해준 것이 아닌 얕은 복사하여 동일한 객체에 대한 주소를 내부적으로 복사해준 것 뿐이다.
    • C++로 따지면 두 변수가 동일한 메모리에 소유권을 모두 가지도록 shared_ptr를 만들고 리턴해준 상황과 같다.
  • C#에선 Knight knight;만으로는 메모리 할당이 되지 않는다. new를 사용하여야 메모리가 할당이 된다.(=실존하게 된다.)
    • new를 사용하여 할당했다는 것은 힙 메모리에 동적으로 할당 받았다는 것이다. C# 에서 클래스 객체는 C++과 달리 무조건 힙 메모리에 올라간다.
    • C++은 구조체나 클래스 객체나 다 스택, 힙 양쪽 다 사용이 가능하다.
    • 힙과 다르게 스택 메모리는 유효 범위가 계속 달라지므로 스택의 주소를 참조하는건 좀 위험한 일이다.

객체 복사시 참조가 아닌 깊은 복사를 하고 싶다면

새로운 별개의 객체를 생성하여 자기 자신의 속성 값들을 복사하고 이를 리턴하는 함수 Clone()을 만든다. 직접 Call by Value (복사)를 구현하는 방식.

class Knight
    {
        public int hp;
        public int attack;

        public Knight Clone()
        {
            Knight knight = new Knight();
            knight.hp = hp;
            knight.attack = attack;
            return knight;
        }
        public void Move() { Console.Write("Kight Move"); }
        public void Attack() { Console.Write("Kight Attack"); }
    }

    ...

    // knight을 깊은 복사하여 knight2을 만든다. 두 객체는 별개의 객체이다. 
    Knight knight2 = knight.Clone();

 

※ 스택과 힙

스택(Stack) 메모리

  • 불완전하고 일시적으로 사용하는 메모리(=메모장)
  • 함수 내부에서만 살아 있다가 함수 끝나면 사라지는 지역 변수, 연쇄적인 함수들의 호출 위치 등등 잠깐 있다 사라질 것들은 스택 메모리에 올라온다.
    • 스택 자료구조처럼 쌓이는 구조고 가장 나중에 쌓인게 가장 먼저 빠진다.(=FIFO, 선입선출)
  • 반면에 객체와 같은 Call by Reference 는 스택 메모리에 그 데이터 자체가 아닌 그 데이터가 있는 주소가 올라간다.
    • 실제 데이터는 힙 메모리에 있다. 즉 스택 메모리에 저장되는건 본체 데이터가 위치한 힙 메모리의 주소다.

힙(Heap) 메모리

  • new 등등 프로그램 실행 中 실시간으로 할당 된 것들이 올라간다.
    • 실행 전부터 메모리를 잡고 실행하는 것이 아니라 프로그램 실행 중에 그때 그때 필요한 메모리를 할당할 땐 힙 메모리에서 가져다 쓴다.
  • 특별히 해제해주는 작업이 없다면 프로그램 내내 힙 메모리에 안정적으로 남아있게 된다.
    • C++ 에선 개발자가 반드시 직접 delete 로 일일이 해제 해주어 힙 메모리 누수를 막아야 했었다.
    • C# 에선 아무도 참조하지 않고 자리만 차지하는 힙 메모리는 C# 시스템 자체의 GC(가비지 콜렉터)가 알아서 해제해준다.
  • Call by Reference 를 실현하는 ref 변수가 참조하는 데이터는 스택 메모리일 수도 있다.
    • ref int a 에서 참조하는 메모리는 스택 메모리 일 수도 있는 것. a엔 참조 중인 스택 메모리의 주소가 스택 메모리에 속한 a에 올라가게 된다.
    • Call by Reference 라고 해서 무조건 힙에 저장되는건 아니라는 것이다. 착각하지 말기!

 

※ 참고 사이트

블로그의 정보

Sugar

Sugar0810

활동하기