2025.01.28 (화)

윤혜진·2025년 1월 28일
0

TIL

목록 보기
13/42

📍오늘의 학습 키워드

  • 3주차 강의 수강
    • 클래스와 객체
      • 클래스의 구성 요소
      • 접근 제한자(public, private, protected)
      • 프로퍼티
    • 상속과 다형성
      • 상속의 특징
      • 가상 메서드
      • 추상 클래스와 메서드
      • 오버라이딩과 오버로딩
    • 고급 문법 및 기능
      • 제너릭
      • out, ref

📍학습 내용

  • 구조체와 클래스

    • 구조체
      • 스택에 할당
      • 상속을 받을 수 없음
      • 작은 크기의 데이터 저장이나 단순한 데이터 구조에 적합
    • 클래스
      • 힙에 할당
      • 단일 상속 및 다중 상속이 가능
      • 더 복잡한 객체를 표현하고 다양한 기능을 제공하기 위해 사용됨
  • 클래스의 구성 요소

    • 필드: 클래스 내부에 선언되어 있는 변수(데이터)
    • 메서드: 클래스 내부에 선언되어 있는 함수(행동)
    • 생성자: 객체가 생성될 때 자동으로 호출. 객체를 초기화 하는 역할.
    • 소멸자: 객체가 소멸될 때 호출되는 메서드. 메모리나 리소스의 해제 작업을 수행.
  • 접근 제한자

    • 클래스, 필드, 메서드 등의 접근 가능한 범위를 지정하는 키워드
    • public: 외부에서 자유롭게 접근이 가능합니다.
    • private: 같은 클래스 내부에서만 접근 가능합니다.
    • protected: 같은 클래스 내부와 상속받은 클래스에서만 접근 가능합니다.
  • 프로퍼티 (Property)

    • 객체의 필드 값을 private로 은닉화를 해두면 외부에서는 접근할 수 없게 되는데, 프로퍼티는 그렇게 은닉된 필드값을 외부에서 접근할 수 있도록 매개 역할을 함.
    • get은 값을 가져오는 역할을 하고, set은 값을 설정하는 역할을 한다.

      🍊  집을 지키는 경비원이라고 생각하면 쉽게 이해 가능 :

      • 직접 집 안(필드)에 들어가지는 못하고, 경비원에게 부탁해 물건을 가져오거나 넣어달라고 요청해야 함.
      • 경비원(프로퍼티)은 요청을 처리하고, 필요한 경우 집에 있는 물건(필드)을 수정하거나 가져다줄 수 있음.
  • 프로퍼티를 쓰는 이유

    • 데이터를 안전하게 보호하기 위해 :
      • 필드를 public 으로 직접 노출한다면, 클래스 외부에서 아무런 제어 없이 값을 읽거나 수정할 수 있게 됨.
      • 그러나 프로퍼티를 통하면 값을 설정(set)할 때 조건을 넣어 잘못된 데이터를 막을 수 있다. (유효성 검사)
    • 읽기와 쓰기 권한을 제어하기 위해 :
      • 필드를 공개하면 클래스 외부에서 읽기만 해야 하는 값도 수정이 가능하게 됨.
      • 읽기만 해야 하는 값은 get만 설정하는 식으로 읽기 전용 프로퍼티로 만들 수 있음. (반대의 경우도 가능)
  • 정적 할당과 동적 할당

    • 정적 할당 (Static Allocation)
      • 미리 정해진 크기의 공간을 할당하는 방식
      • 스택이라는 메모리 공간을 사용
      • 메모리를 스스로 해제할 필요가 없음. (메서드가 끝나면 스택에서 자동으로 정리된다.)
    • 동적 할당 (Dynamic Allocation)
      • 크기가 고정되지 않고, 프로그램이 실행되는 동안에도 원하는 만큼 공간을 사용할 수 있다.
      • 힙이라는 메모리 공간을 사용
      • 동적 할당한 메모리는 직접 관리해주어야 함. (단, .NET 사용시 가비지 컬렉터가 자동으로 정리해줌)
    • 정적 할당 vs 동적 할당
      특징정적 할당동적 할당
      크기고정됨유동적 (실행 중에 변경 가능)
      메모리 위치스택(Stack)힙(Heap)
      성능빠름 (컴파일 타임에 관리)느림 (런타임에 관리)
      메모리 관리자동으로 정리직접 해제하거나 가비지 컬렉터 의존
      유연성제한적매우 유연함
  • 상속의 특징

    1. 부모 클래스의 멤버에 접근:
      • 자식 클래스는 상속받은 부모 클래스의 멤버에 접근 가능
      • 이를 통해 부모 클래스의 기능을 재사용할 수 있다.
        //부모 클래스
        public class Animal
        {
            public string Name { get; set; }
            public int Age { get; set; }	
        }
        
        //자식 클래스 1
        public class Dog : Animal
        {
            public void Bark()
            {
                Console.WriteLine($"{Name} is bark");
            }
        }
        
        //자식 클래스 2
        public class Cat : Animal
        {
        
        }
        
        static void Main(String[] args)
        {
        		//부모 클래스에 Name이 있기 때문에
        		//따로 선언하지 않아도 Name 이용 가능
        
        		Dog dog = new Dog();
        		dog.Name = "Bobby";
        		dog.Age = 3;
        		
        		dog.Bark();
        }
        [결과]
        Bobby is bark
    2. 매서드 재정의:
      • 자식 클래스는 부모 클래스의 메서드를 재정의하여 자신에게 맞게 수정할 수 있다.
      • 이를 통해 다형성(Polymorphism)을 구현할 수 있음.
        //부모 클래스
        public class Animal
        {
            public string Name { get; set; }
            public int Age { get; set; }
            
            public void Eat()
        		{
        		    Console.WriteLine("Animal is eating.");
        		}	
        }
        
        //자식 클래스 1
        public class Dog : Animal
        {
        		//부모 클래스의 메서드 재정의
            public void Eat()
        		{
        		    Console.WriteLine($"{Name} is eating.");
        		    Console.WriteLine($"{Name} looks very happy!");
        		}	
        }
        
        //자식 클래스 2
        public class Cat : Animal
        {
        
        }
        
        static void Main(String[] args)
        {
        		Dog dog = new Dog();
        		dog.Name = "Bobby";
        		dog.Age = 3;
        		
        		dog.Eat();   //Dog에서 재정의한 Eat()이 실행됨
        }
        [결과]
        Bobby is eating.
        Bobby looks very happy!
    • 그러나 위와 같은 방법으로 코드를 짜면 부모 클래스의 메서드가 숨겨지게 되는데, 사실 이런 식으로 재정의를 하는 것은 좋은 방법이 아님. (다른 방법은 후술하겠음)
    1. 상속의 깊이:
      • 클래스는 다수의 계층적인 상속 구조를 가질 수 있다.
      • 즉, 부모 클래스가 또 다른 클래스의 자식 클래스가 될 수도 있음. (부모 클래스를 상속 받는 자식 클래스, 자식 클래스를 상속 받는 손자 클래스…)
      • 상속의 깊이가 깊어질수록 클래스 간의 관계가 복잡해질 수 있으므로 적절한 선으로 이용하는 것이 중요.
  • 다형성

    • 한 가지 형태의 코드가 여러 가지 모양으로 동작할 수 있는 것을 의미
    • 사실 위의 예시처럼 코드를 짜면 부모 클래스의 메서드가 숨겨지게되는데, 이런 식으로 재정의를 숨기면서 진행을 하는 건 좋은 방법이 아님.
    • 상속을 하면서도 다형성을 유지하는 방법은 가상 메서드, 추상 메서드, 오버라이드오버로딩이 있다.
  • 가상 메서드를 사용해야 하는 이유

    • 우리가 Cat을 Cat으로, Dog을 Dog로 사용한다면 숨겨서 사용해도 큰 문제는 없음.
    • 그러나 Dog와 Cat의 갯수가 많아져 그 둘을 부모 클래스(Animal)로 한 번에 관리해주게 된다면, 다음과 같은 문제가 생김:
      List<Animal> animalList = new List<Animal>();
      animalList.Add(new Dog());
      animalList.Add(new Cat());
      
      foreach (Animal animalList in list)
      {
          animalList.Eat();
      }
      
      [결과]
      Animal is eating.
      Animal is eating.
    • 위처럼 코딩을 하면 우리는 하나는 수정하지 않은 Eat, 하나는 Dog에서 재정의한 Eat이 실행될 거라고 기대하지만, 실제로 실행하면 수정하지 않은 Eat 메서드만 두 번 실행됨.
    • 이유: List는 Dog가 아닌 Animal을 참조하고 있기 때문에, Dog에서 따로 재정의한 Eat 메서드 보다 Animal의 Eat 메서드 쪽이 더 가깝기 때문.
    • 이런 문제를 해결하기 위해서는 가상 메서드가 필요하다.
  • 가상 메서드(Virtual Method)

    • 부모 메서드에 virtual을 붙임으로서 날 상속한 자식들이 재정의 할 수 있는 메서드인 것을 미리 알려줄 수 있음.
    • 상속 받은 메서드를 자식 클래스에서 재정의 할 때는 override를 붙여줌으로서 내가 이 메서드를 재정의 했음을 알려주어야 함.
    • 아래는 가상 메서드를 적용한 코드:
      //부모 클래스
      public class Animal
      {
          public string Name { get; set; }
          public int Age { get; set; }
          
          //자식들이 재정의를 할 수도 있다고 virtual로 알려줌
          public virtual void Eat()
      		{
      		    Console.WriteLine("Animal is eating.");
      		}	
      }
      
      //자식 클래스 1
      public class Dog : Animal
      {
      		//재정의 한 메서드임을 override로 알려줌
          public override void Eat()
      		{
      		    Console.WriteLine($"{Name} is eating.");
      		    Console.WriteLine($"{Name} looks very happy!");
      		}	
      }
      
      //자식 클래스 2
      public class Cat : Animal
      {
      
      }
      
      static void Main(String[] args)
      {
      		List<Animal> animalList = new List<Animal>();
      		animalList.Add(new Dog());
      		animalList.Add(new Cat());
      		
      		foreach (Animal animalList in list)
      		{
      		    animalList.Eat();
      		}
      }
      
      [결과]
      Bobby is eating.
      Bobby looks very happy!
      Animal is eating.
  • 추상클래스와 메서드 (Abstract Class, Abstract Method)

    • 직접적으로 인스턴스를 생성할 수 없는 클래스
    • 주로 상속을 위한 베이스 클래스로 사용된다.
    • 추상 클래스는 abstract키워드를 사용하여 선언되며, 추상 메서드를 포함할 수 있다.
    • 추상 메서드는 구현부가 없는 메서드로, 자식 클래스에서 반드시 구현되어야 한다.
    • 구현하지 않으면 아래처럼 오류가 남 :

  • 가상메서드와 추상메서드의 차이

    • 가상메서드(virtual):
      • 구현이 되어 있을 수도 있고 없을 수도 있음
      • 구현이 되어있다면 자식의 메서드를 사용하고, 아니면 부모메서드를 사용.
    • 추상메서드의(abstract):
      • 무조건 재정의 되어있음
      • 부모 클래스에는 빈 메서드만 있으므로 무조건 자식의 메서드를 사용.
  • 오버라이딩과 오버로딩

    • 오버라이딩

      • 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 재정의 하는 것을 의미.
      • 가상메서드와 추상메서드를 사용한 코드들이 이에 속한다. (Animal과 Dog와 Cat)
      • 상속 관계에 있는 클래스 간에 발생하며, 메서드의 이름, 매개변수 및 반환 타입이 동일해야 함.
      • 오버라이딩을 통해 자식 클래스는 부모 클래스의 메서드를 재정의하여 자신에게 맞는 동작을 구현할 수 있다.
    • 오버로딩

      • 동일한 메서드 이름을 가지고 있지만, 매개변수의 개수, 타입 또는 순서가 다른 여러 개의 메서드를 정의하는 것을 의미한다.
      • 오버로딩을 통해 동일한 이름을 가진 메서드를 다양한 매개변수 조합으로 호출할 수 있다.
      • 우리가 자주 사용하는 WriteLine도 오버로딩을 사용한 메서드.
      • WriteLine이 오버로딩 되어있기 때문에 우리는 다양한 작성 형식을 사용할 수 있다.
      • 정수형 변수를 넣는다던가, 문자열 변수를 넣는다던가, “ ”를 통해 문자열을 직접 입력한다던가, +를 이용해 둘 다 넣는다던가… 모두 오버로딩이 되어있기 때문에 사용할 수 있는 것.
  • 제너릭(Generic)

    • 클래스나 메서드를 일반화시켜 다양한 자료형에 대응할 수 있는 기능 (입력 받는 값에 따라 자료형이 결정된다는 점에서 var와 비슷한 느낌!)
    • 제너릭 클래스나 메서드에서 사용할 자료형은 선언 시점이 아닌 사용 시점에 결정됨.
    • 선언 할 때는 <T> 형태의 키워드를 이용하여 제너릭을 선언.
    • 사용할 때는 <T> 대신 구체적인 자료형을 넣어준다.
    • 아래 코드는 Stack 클래스를 제너릭을 이용하여 만든 뒤, Main에서 int와 string 형식으로 사용하는 코드이다 :
      class Stack<T>
      {
          private T[] elements;
          private int top;
      
          public Stack()
          {
              elements = new T[100];
              top = 0;
          }
      
          public void Push(T item)
          {
              elements[top++] = item;
          }
      
          public T Pop()
          {
              return elements[--top];
          }
      
      }//Stack<T>
      
      static void Main(String[] args)
      {
          Stack<int> intStack = new Stack<int>();
          intStack.Push(1);
          intStack.Push(2);
          intStack.Push(3);
          Console.WriteLine(intStack.Pop());
      
          Stack<string> stringStack = new Stack<string>();
          stringStack.Push("하나");
          stringStack.Push("둘");
          stringStack.Push("셋");
          Console.WriteLine(stringStack.Pop());
      
      }//Main
      
      [결과]
      3
    • 컬렉션의 선언 방식을 다시 한 번 살펴보면, 각 컬렉션 또한 제너릭을 이용하는 것을 알 수 있다.
      List<int> numbers = new List<int>();
      Dictionary<string, int> scores = new Dictionary<string, int>();
      Stack<int> stack1 = new Stack<int>();
      Queue<int> queue1 = new Queue<int>();
  • out, ref 키워드
    • 변수를 참조 형태로 전달하는데 사용. (C++의 포인터와 유사)
    • 두 키워드를 사용하면 변수의 값이 메서드 안에서 변경될 수 있음.
    • 둘의 차이점은 다음과 같다 :
      특징refout
      초기화 여부호출 전에 반드시 초기화해야 함호출 전에 초기화할 필요 없음
      메서드 내 초기화 여부초기화는 선택 사항반드시 초기화해야 함
      사용 목적값을 수정하고 다시 가져올 때 사용값을 전달받기 위해 사용
    • out
      • 변수를 전달하기 전, 변수를 초기화 하지 않아도 됨.
      • 대신 메서드 안에서 반드시 변수에 값을 넣어주어야 함. (메서드 안이 비어있거나 받은 변수에 값을 할당하지 않으면 오류가 난다.)
      • “빈 그릇을 줄 테니, 여기에 값을 넣어서 가져와라!” 라는 느낌.
    • ref
      • 변수를 전달하기 전, 반드시 변수 안에 값이 있어야 함. (전달값이 있어야 함)
      • 대신 메서드 안을 비워두어도 오류가 나지 않음.
      • "이미 준비된 물건을 가져와서 고치겠다!"는 느낌.

📍겪은 어려움

  • 이름 ‘Console.WriteLine’이(가) 현재 컨텍스트에 없습니다.

    • abstract를 이용한 실습 중이었는데, 다음과 같은 오류가 일어남.
    • 검색했을 때 Console이나 System을 소문자로 입력하면 나타나는 오류라고 나왔는데, 나는 해당사항이 없었음.
    • 알고보니 부모 클래스에 있는 Draw메서드를 재정의 해주어야 하는 상황에서 실수로 Console.WriteLine부분만 입력해 일어난 오류였다.

📍회고 및 반성

  • 나중에 내가 배운걸 잊었을 때 다시 보고 참고할 수 있도록 최대한 정성껏 정리했다…
  • 후반으로 갈 수록 모르는 개념들이 많아져서 이해하는데 시간이 오래걸리는 것 같음.
  • 아직은 생소하게 느껴지는 개념들이지만, 빠르게 익숙해질 수 있도록 배운 걸 많이 사용해볼 것! 아자!

0개의 댓글

관련 채용 정보