상속

부모 클래스라 불리는 기존의 클래스를 확장하거나 재사용할 수 있는 자식 클래스라 불리는 새로운 클래스를 생성하는 것.

자식 클래스는 부모 클래스의 맴버를 상속받을 수 있으며, 부모 클래스의 기능을 확장하거나 새로운 클래스로 재정의하는 것이 가능하다. 코드를 재사용할 수 있고 계층 구조로 표현할 수 있으며 유지 보수가 증가한다.

  • 단일 상속: 부모 클래스 하나, 자식 클래스 하나
  • 다중 상속: 부모 클래스가 여러 개 (C#에서는 지원X)
  • 인터페이스 상속: 인터페이스를 상속할 때만 다중 상속 가능. 하나의 클래스와 여러 개의 인터페이스를 상속한다.
    // 부모 클래스
    public class Animal
    {
        public string Name { get; set; }
        public int Age { get; set; }

        public void Eat()
        {
            Console.WriteLine("Animal is eating.");
        }

        public void Sleep()
        {
            Console.WriteLine("Animal is sleeping.");
        }
    }

    // 자식 클래스
    public class Dog : Animal
    {
        public void Bark()
        {
            Console.WriteLine("Dog is bark.");
        }
    }

    public class Cat : Animal
    {
        public void Meow()
        {
            Console.WriteLine("Cat is meow.");
        }

        public void Sleep() // 부모 클래스인 Animal의 Sleep을 숨긴다
        {
            Console.WriteLine("Cat Sleep!");
        }
    }

    static void Main(string[] args)
    {
        Dog dog = new Dog();
        dog.Name = "Bobby";
        dog.Age = 3;

        dog.Eat();
        dog.Sleep();
        dog.Bark();

        Cat cat = new Cat();
        cat.Name = "Kkami";
        cat.Age = 10;

        cat.Eat();
        cat.Sleep();
        cat.Meow();
    }
}

 

 

다형성

같은 타입이지만 다양한 동작을 수행할 수 있는 능력

 

가상 메서드

부모 클래스에서 정의되고 자식 클래스에서 재정의된다.

virtual 키워드를 사용하여 선언되며, 자식 클래스에서 필요에 따라 재정의 될 수 있다. 이를 통해 자식 클래스에서 부모 클래스의 메서드를 변경하거나 확장 가능하다.

// 가상 메서드
public class Unit
{
    public virtual void Move() // virtual -> 자신을 상속한 자식들이 재구현 할 수 있음을 선언. 실형태가 다를 수 있음을 확인해라
    {
        Console.WriteLine("두발로 걷기");
    }

    public void Attack()
    {
        Console.WriteLine("Unit 공격");
    }
}

public class Marine : Unit
{

}

public class Zergling : Unit
{
    public override void Move() // override로 재정의
    {
        Console.WriteLine("네발로 걷기");
    }
}

// 가상 메서드 호출
// #1 참조형태와 실형태가 같을때
Marine marine = new Marine();
marine.Move();
marine.Attack();

Zergling zergling = new Zergling();
zergling.Move();
zergling.Attack();

// Unit 클래스로 관리
// #2 참조형태와 실형태가 다를때
List<Unit> list = new List<Unit>();
list.Add(new Marine());
list.Add(new Zergling());

foreach (Unit unit in list)
{
    unit.Move();
}

 

추상 클래스와 메서드

직접적으로 인스턴스를 생성할 수 없다. 주로 이걸 기점으로 만든다는 느낌으로 베이스로 사용된다.

abstract 키워드를 사용하여 선언되며, 추상 메서드는 추상 메서드를 포함할 수 있다. 선언만 하고 구현하지 않기 때문에 자식 클래스에서 반드시 구현되어야 한다.

// 추상클래스
abstract class Shape
{
    public abstract void Draw();
}

class Circle : Shape
{
    public override void Draw() // 추상 메서드를 상속 받았기 때문에 반드시 구현
    {
        Console.WriteLine("Drawing a circle");
    }
}

class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}

class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a triangle");
    }
}

// 추상 메서드 호출
// Shape shape = new Shape(); // 인스턴스를 만들 수 없음
List<Shape> list = new List<Shape>();
list.Add(new Circle());
list.Add(new Square());
list.Add(new Triangle());

foreach (Shape shape in list)
{
    shape.Draw();
}

 

 

오버라이딩과 오버로딩

 

오버라이딩

부모 클래스에 이미 정의되어 있는 메서드를 자식이 재정의 하는 것.(덮어쓰기) 이때 메서드의 이름과 매개변수의 반환 타입이 똑같아야 한다.

부모의 클래스에 정의되어 있는 동작이 아니라 새로운 동작을 재구현 할 수 있다.

 

오버로딩

매개변수의 갯수나 타입, 순서가 다른 동일한 이름의 메서드들이 여러 개 있는 것.

함수를 읽어올 때 골라 읽어올 수 있다.

 

 

객체지향 프로그래밍
  • 캡슐화 - 관련된 데이터와 기능을 하나의 단위로 묶는 것. 정보를 은닉하고 외부에서 직접적인 접근을 제한하여 안정성과 유지보수성을 높힌다.
  • 상속 - 부모 클래스의 특성과 동작을 자식 클래스가 상속받아 재사용한다. 코드의 중복을 줄이고 코드의 구조화와 유지보수를 용이하게 한다.
  • 다형성 - 오버로딩, 오버라이딩을 통해 하나의 메서드 이름으로 다양한 동작을 할 수 있다. 코드의 가독성과 재사용성을 높힌다.
  • 추상화 - 복잡한 기능을 단순화하여 필요한 기능에 집중한다.(세부 구현을 감추고 개념을 구현)
  • 객체 - 데이터와 메서드를 가지고 있으면서 실제로 상호작용하며 프로그램이 동작하는 것. 클래스로 구현되어 모듈화나 재사용성이 높다.

 

 

클래스

객체를 생성하기 위한 템플릿 또는 설계도. 데이터와 메서드를 하나로 묶은 사용자 정의 타입. 속성이나 동작에 대한 정의가 되어 있다. 클래스를 통해 인스턴스(객체)를 만들 수 있다.

- 구성요소: 필드(맴버변수), 메서드(맴버함수), 생성자(객체가 형성될 때 자동으로 호출되는 메서드), 소멸자(메모리에서 소멸될 때 자동으로 생성되는 메서드)

 

객체

클래스로 생성된 인스턴스. 클래스의 실체화된 형태. 독립적인 상태로 고유의 데이터를 가지고 있다.

 

// 클래스 선언
class Person
{
    public string Name;
    public int Age;

    public void PrintInfo()
    {
        Console.WriteLine("Name: " + Name);
        Console.WriteLine("Age: " + Age);
    }
}

Person p = new Person(); // 레퍼런스 타입
p.Name = "John";
p.Age = 30;
p.PrintInfo(); // 출력: Name: John, Age: 30

 

구조체와 클래스

  • 구조체와 클래스 모두 사용자 정의 형식. 우리가 원하는 자료형과 기능들을 뭉쳐 놓은 상태이다.
  • 구조체는 값 형식이며, 스택이라는 메모리에 각자 자동으로 할당된다.
  • 클래스는 참조 형식이며, 힙 영역에 동적 할당된다.
  • 구조체는 상속을 받을 수 없지만 클래스는 단일 상속과 다중상속이 가능하다.
  • 구조체는 작은 크기의 데이터나 단순한 구조를 작성할 때 적합하고 복잡한 경우에 클래스가 훨씬 더 사용하기 편하다.

 

접근 제한자

  • public: 외부에서 자유롭게 접근 가능
  • private: 같은 클래스 내부의 메서드나 필드에서 접근 가능
  • protected: 같은 클래스 내부와 상속 받은 자식까지만 접근 가능

 

필드

클래스 내부에 선언된 변수(맴버 변수). 데이터를 저장한다.

객체의 특징이나 속성들을 표현하기 위해 사용하며, 일반적으로 외부에서 접근하지 못하도록  private로 접근 제한자를 사용한다.

class Player
{
    // 필드 선언
    private string name;
    private int level;
}

 

메서드

클래스 내부에 선언된 함수(맴버 함수). 클래스의 동작을 정의한다.

클래스나 구조체에 대한 기능들을 정리하며, 일반적으로 외부에서 호출할 수 있도록 public으로 만들어준다.

class Player
{
    // 필드
    private string name;
    private int level;

    // 메서드
    public void Attack()
    {
        // 공격 동작 구현
    }
}

Player player = new Player();  // Player 클래스의 인스턴스 생성
player.Attack();  // Attack 메서드 호출

 

생성자

객체를 생성할 때 호출되는 메서드. 객체를 초기화하고 필요한 초기값을 설정하는 역할을 한다.

  • 클래스와 동일한 이름을 가지고 있으며, 반환 타입이 없다.
  • 여러 개를 정의 할 수 있고 매개변수의 개수와 타입에 따라 다른 생성자를 호출 할 수 있다.(오버로딩)
  • 기본적으로 매개변수가 없는 디폴트 생성자가 자동으로 생성되지만, 최소 하나라도 생성자를 만들었다면 디폴트 생성자는 더 이상 만들어지지 않는다.
class Person
{
    private string name;
    private int age;

    public void PrintInfo()
    {
        Console.WriteLine($"Name: {name}, Age: {age}");
    }
}



Person person1 = new Person();

 

소멸자

객체가 소멸이 될 때 호출되는 메서드.

  • 클래스와 동일한 이름을 가지고 있으며, 이름 앞에 ~ 기호를 붙인다.
  • 가비지 컬렉터(Garbage Collector)가 클래스가 아무데도 사용되지 않아서 메모리를 해제할 때 소멸자가 자동으로 호출된다.
  • 파일 핸들, 네트워트 연결, 데이터베이스 연결 등 외부적인 파일들에 대한 자원을 해제할 수 있다.
  • 배열이나 다른 클래스들의 선언을 인위적으로 해제할 수 있다.
  • 소멸이 언제 되는지에 대한 로깅과 디버깅을 할 수 있다.
  • 오버로딩 할 수 없다.
class Person
{
    private string name;

    public Person(string newName)
    {
        name = newName;
        Console.WriteLine("Person 객체 생성");
    }

    ~Person()
    {
        Console.WriteLine("Person 객체 소멸");
    }
}

 

프로퍼티(Property)

은닉화한 필드값(private 등)을 외부에서도 접근할 수 있는 중간 매개 역할을 만들어주는 것.

프로퍼티를 사용하여 데이터 유효성이나 제한을 거는 등 필드 접근을 제한하면 코드의 안정성과 가독성을 높힐 수 있다.

class Person
{
    private string name;
    private int age;

    public string Name
    {
        get { return name; }
        set { name = value; }
    }

    public int Age
    {
        get { return age; }
        set { age = value; }
    }
}

Person person = new Person();
person.Name = "John";   // Name 프로퍼티에 값 설정
person.Age = 25;        // Age 프로퍼티에 값 설정

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // Name과 Age 프로

보통 변수와 동일한 이름을 사용한다.(완전히 동일하면 안되기 때문에 대문자로 변환)

 

프로퍼티 접근 제한자 적용과 유효성 검사 예제

class Person
{
    private string name;
    private int age;

    public string Name
    {
        get { return name; }
        private set { name = value; } // 프로퍼티 자체는 외부에서 접근 가능하지만 세팅은 클래스 내부에서 하겠다는 제한
    }

    public int Age
    {
        get { return age; }
        set
        {
            if (value >= 0) // 대입하려는 값이 유효한가(나이에 음수가 들어오지 못하도록 제한)
                age = value;
        }
    }
}

Person person = new Person();
person.Name = "John";     // 컴파일 오류: Name 프로퍼티의 set 접근자는 private입니다.
person.Age = -10;         // 유효성 검사에 의해 나이 값이 설정되지 않습니다.

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // Name과 Age 프로퍼티에 접근하여 값을 출력합니다.

 

자동 프로퍼티

프로퍼티를 간단히 정의하고 사용할 수 있도록 한다. 필드의 선언과 접근자 메서드의 구현을 컴파일러가 자동으로 처리한다.

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Person person = new Person();
person.Name = "John";     // 값을 설정
person.Age = 25;          // 값을 설정

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // 값을 읽어 출력

 

 

최대값, 최소값 찾기

사용자로부터 일련의 숫자 5개를 입력받아, 그 중에서 최대값과 최소값을 찾는 프로그램 작성하시오.

 

<나의 풀이 과정>

int[] num = new int[5]; // 사용자에게 입력 받을 배열 초기화

int max = num[0]; // 초기화
int min = num[0]; // 초기화

for (int i = 0; i < 5; i++) // 0~4 인덱스 안의 숫자 비교
{
    Console.Write("숫자를 입력하세요: ");
    num[i] = int.Parse(Console.ReadLine()); // 사용자에게 입력 받기

    if (num[i] > max) // num[i]가 max보다 크다면
    {
        max = num[i];
    }
    if (num[i] < min) // num[i]가 min보다 작다면
    {
        min = num[i];
    }
}
Console.WriteLine("최대값: " + max);
Console.WriteLine("최소값: " + min);

초기화가 min이 초기화 된 상태인 0의 값으로 비교되므로 늘 최소값이 0으로 나오게 된다.

 

int[] num = new int[5];

for (int i = 0; i < 5; i++)
{
    Console.Write("숫자를 입력하세요: ");
    num[i] = int.Parse(Console.ReadLine());
}

int max = num[0];
int min = num[0];

for (int i = 0;i < 5; i++) 
{
    if (num[i] > max)
    {
        max = num[i];
    }
    if (num[i] < min)
    {
        min = num[i];
    }
}
Console.WriteLine("최대값: " + max);
Console.WriteLine("최소값: " + min);

사용자가 입력한 숫자를 for문을 통해 5회 반복하여 배열에 넣어준 뒤, 또다시 for문을 통해 5개의 숫자를 비교한다.

 

 

소수 판별하기

주어진 숫자가 소수인지 판별하는 함수와 코드를 만드시오.

 

<나의 풀이 과정>

// 주어진 숫자가 소수인지 판별하는 함수
static bool IsPrime(int num)
{
    if (num <= 1) // 1보다 작거나 같으면 소수가 아니다
    {
        return false;
    }
    for (int i = 2; i < num; i++) // 2보다 크고 자기 자신과 같은 숫자가 되기 전까지 1씩 증가
    {
        if (num % i == 0) // 나누어 떨어지면 소수가 아니다
        {
            return false;
        }
    }

    return true; // 소수
}

static void Main()
{
    Console.Write("숫자를 입력하세요: "); // 사용자에게 숫자 입력 요청
    int num = int.Parse(Console.ReadLine()); // 사용자가 입력한 값을 정수로 변환하여 저장

    if (IsPrime(num)) // 입력 받은 숫자가 소수라면
    {
        Console.WriteLine(num + "은 소수입니다."); // 소수임을 출력
    }
    else // 소수가 아니라면
    {
        Console.WriteLine(num + "은 소수가 아닙니다."); // 소수가 아님을 출력
    }
}

이렇게 하면 작은 수일 때는 괜찮지만 큰 수를 입력했을 때 메모리를 많이 잡아먹게 된다.

static bool IsPrime(int num)
{
    if (num <= 1)
    {
        return false;
    }
    for (int i = 2; i <= num / 2; i++) // 2부터 자기 자신의 절반까지만 확인
    {
        if (num % i == 0)
        {
            return false;
        }
    }

    return true;
}

약수는 자기 자신을 제외하면 절반까지가 최대이기 때문에 2로 나눠주면 메모리 사용량을 줄일 수 있다. 그러나 49, 81 등 제곱된 수의 약수는 절반까지 도달하지 않아도 소수가 아님을 확인 할 수 있다.

 

static bool IsPrime(int num)
{
    if (num <= 1)
    {
        return false;
    }
    for (int i = 2; i * i <= num; i++) // 2부터 숫자의 제곱근까지만 확인
    {
        if (num % i == 0)
        {
            return false;
        }
    }

    return true;
}

 

 

 

숫자 맞추기

컴퓨터는 1에서 100까지의 숫자를 임의로 선택하고, 사용자는 이 숫자를 맞춘다. 사용자가 입력한 숫자가 컴퓨터의 숫자보다 크면 "너무 큽니다!"라고 알려주고, 작으면 "너무 작습니다!"라고 알려준다. 사용자가 숫자를 맞출 시, 게임 종료.

 

Random random = new Random();
int numberToGuess = random.Next(1, 101);
int userNum = 0;
int numberTry = 0;
bool playAgain = true;

Console.WriteLine("숫자 맞추기 게임을 시작합니다. 1에서 100까지의 숫자 중 하나를 맞춰보세요.");
while (playAgain)
{
    while (numberToGuess != userNum)
    {
        Console.Write("숫자를 입력하세요: ");
        userNum = Convert.ToInt32(Console.ReadLine());

        if (numberToGuess > userNum)
        {
            Console.WriteLine("너무 작습니다!");
        }
        else if (numberToGuess < userNum)
        {
            Console.WriteLine("너무 큽니다!");
        }
        numberTry++;
    }
    Console.WriteLine("축하합니다! {0}번 만에 숫자를 맞추었습니다.", numberTry);
    playAgain = false;
}

 

 

틱택토(콘솔)

 

<나의 풀이 과정>

 

메서드

중복되는 코드를 뭉쳐놓은 하나의 블록. 코드를 재사용하거나 모듈화 할 수 있게 만들어주는 것.

  • 코드의 재사용성
  • 모듈화
  • 가독성과 유지보수성
  • 중복 제거
  • 추상화

 

메서드의 구조

[접근 제한자] [리턴 타입] [메서드 이름]([매개변수])
{
    // 메서드 실행 코드
}
  • 접근 제한자(Access Modifier): 메서드에 접근할 수 있는 범위 지정. (public, private, protected 등)
  • 리턴 타입(Return Type): 메서드가 반환하는 값의 데이터 타입 지정. (반환 값이 없을 경우 void 사용)
  • 메서드 이름(Method Name): 메서드를 호출할 때 사용하는 이름.
  • 매개변수(Parameters): 메서드에 전달되는 입력 값. 0개 이상의 매개변수를 정의할 수 있다.
  • 메서드 실행에 코드(Method Body): 중괄호({}) 안에 메서드가 수행하는 작업을 구현하는 코드 작성.

 

매개변수와 반환값

static void PrintLine()
{
    for (int i = 0; i < 10; i++)
    {
        Console.Write("=");
    }
    Console.WriteLine();
}

static void PrintLine2(int count)
{
    for (int i = 0; i < count; i++)
    {
        Console.Write("=");
    }
    Console.WriteLine();
}

static int Add(int a, int b)
{
    return a + b;
}

static void Main(string[] args)
{
    // 사용 예시
    PrintLine();
    PrintLine2(20);

    int result = Add(10, 20);
    Console.WriteLine(result);
}

 

 

오버로딩

매개변수 목록이 다중 정의된 것. 매개변수의 개수, 타입, 순서가 다르면 다른 메서드로 취급한다. 메서드의 기능이나 작업은 동일하지만 입력값에 따라 다르게 동작해야 할 때 사용된다.

 

// 오버로딩
static int AddNumbers(int a, int b)
{
    return a + b;
}

static float AddNumbers(float a, float b)
{
    return a + b;
}

static int AddNumbers(int a, int b, int c)
{
    return a + b + c;
}


static void Main(string[] args)
{
    // 오버로딩 메서드 호출
    int sum1 = AddNumbers(10, 20);  // 두 개의 정수 매개변수를 가진 메서드 호출
    float sum3 = AddNumbers(10, 20); // 두 개의 실수 매개변수를 가진 메서드 호출
    int sum2 = AddNumbers(10, 20, 30);  // 세 개의 정수 매개변수를 가진 메서드 호출

 

 

재귀 호출

메서드가 자기 자신을 호출하는 것. 호출 스택에 호출된 메서드의 정보를 순차적으로 쌓고, 메서드가 반환되면서 스택에서 순차적으로 제거되는 방식이다. 메모리 사용량이 더 크고 실행 속도가 느릴 수 있으며,  무한 루프에 빠질 수 있기 때문에 주의해야한다.

 

// 재귀 호출
static void CountDown(int n)
{
    if (n <= 0)
    {
        Console.WriteLine("Done");
    }
    else
    {
        Console.WriteLine(n);
        CountDown(n - 1);  // 자기 자신을 호출
    }
}

static void Main(string[] args)
{
    // 재귀 호출
    CountDown(5);
}

 

 

구조체

여러 개의 데이터를 묶어서 하나의 사용자 정의 형식으로 만들기 위한 방법. 값 형식(대입하거나 값을 할당할 때 복사되는 것)으로 분류되며 struct 키워드를 사용하여 선언한다.

*자세한 내용은 class와 비교하며 언급

 

 

+ Recent posts