생명주기는 스코프 안에 존재할시 살아있다.
괄호 안 = 스코프
스코프는 변수를 나누는 가장 중요한 선이다
참조 : 어떤이름으로 불리건 나라는 본질은 변하지 않음
struct : 각 메모리 영역이 생성, 메모리 공간이 다 다름
c#에서는 포인터는 없지만 스마트 포인터 같이 작동하는 레퍼런스가 있고 그게 바로 class data = new class 하는 것과 동일하게 작동한다,
힙(heap) 메모리영역 - 동적 메모리 할당
- 클래스에서 생성한 인스턴스가 들어감
- struct를 가진 class는 힙 안에서 자기만의 stack 공간을 점유하여 사용함
stack 메모리 영역 - 정적 메모리 할당
- struct data
파라미터, 아규먼트
파라미터 (Parameter): 함수 정의 시 명시하는 변수로, 함수가 호출될 때 전달되는 값을 받을 준비
아규먼트 (Argument): 함수가 호출될 때 실제로 전달되는 값입니다. 이는 파라미터에 대응
함수
함수는 반복적인 행위를 기능 단위로 묶는 것을 의미합니다. 이를 통해 코드의 재사용성을 높이고 가독성을 향상시킬 수 있다.
함수가 출력하는값(return 되는 값)이 정수(int)이면 함수를 void(반환값 없는 함수) 대신 "int 함수명()"으로 선언
실습
함수 학습을 위한 min값에서 부터 max값까지 +1씩 증가하는 코드
배열
- array에 프로그래밍은 0부터 시작
- 4번째인것을 찾아라 했을때 Console.WriteLine(array[3]); 으로 쓰면 된다 (찾을값 -1)
랜덤 엑세스의 특징은 - 시간복잡도에서 이 알고리즘, 수식이 얼마나 빠른가에 대해 / 다이렉트로 엑세스에 접근할 수 있다.
Console.WriteLine(array[7]); 이렇게 하면 인덱스가 범위를 벗어났다 라고 뜬다.
이렇게 작성하면 랜덤 엑세스에대해 보다 쉽게 알아볼 수 있다.
cmd창에서 아무키나 입력할 시 범위 내 까지 숫자가 나온다.
index = 10 과같이 할 경우 해당 array배열 범위를 벗어남으로 해당값이 출력되지 않는다.
재귀함수를 활용한 간단한 예시
* 배열은 요소(element)를 가지고 있고 저 배열은 6개의 요소를 가지고 있다.
* array.length는 요소의 갯수를 알려주는 속성이다.
- SumArray 메서드:
- 메서드는 배열과 배열의 길이를 받아들여 재귀적으로 배열의 합을 계산
- 함수의 재귀적 호출은 배열의 합이 0일 때까지 진행된다. → 재귀함수는 자기 자신을 호출하는 함수라서 이 함수는 계속 반복되고 그 반복이 오류의 범위로 가거나 필요 이상의 범위로 넘어가기 전에 필요한 부분에서 return 0를 해줘 탈출시켜야 한다.
- 재귀 호출이 종료되면 합계를 반환한다.
- Main 메서드:
- 배열을 정의하고, SumArray 메서드를 호출하여 배열의 합을 계산
- 배열의 길이를 인수로 전달
- 계산된 합계를 콘솔에 출력
재귀함수 사용하는 이유
- 문제 해결이 자연스럽게 재귀적인 구조를 갖는 경우: 어떤 문제를 해결하는 과정이 자연스럽게 재귀적인 구조를 갖는 경우에 재귀를 사용. 예를 들어, 이진 트리를 순회하거나 분할 정복 알고리즘을 구현할 때 재귀를 사용하는 것이 간편하고 직관적이게 되는 것이다.
- 작업이나 계산을 간결하고 이해하기 쉽게 만드는 경우: 때로는 반복문을 사용하여 작업을 수행하는 것보다 재귀 함수를 사용하는 것이 코드를 간결하고 이해하기 쉽게 만들 수 있다. 특히, 문제의 해결 방법이 반복적이지 않고 재귀적인 경우에는 재귀 함수를 사용하는 것이 더 효율적이다.
재귀 함수의 호출 스택은 호출의 깊이에 따라 계속해서 쌓이기 때문에, 너무 많은 재귀 호출이 발생하면 스택 오버플로우가 발생하고, 재귀를 사용할 때 메모리 소비나 성능 문제가 발생할 수 있기 때문이다. 재귀를 사용하기 전에 잘 고려하고, 반복문이나 다른 방법을 고려하게된다. 즉, 현업에서는 잘 사용하지 않는다.
- 재귀 함수가 에러 발생을 많이 시켜 분기처리를 제대로하지 않으면 무한으로 돌 수도 있고 메모리 초과(stackoverflow) 에러도 발생시킬 수 있다. 가독성 측면에서는 최악인 것이다.
재귀함수를 이용해 피보나치 수열 만들기
피보나치수열
- static int Fibonacci(int n): 재귀적으로 피보나치 수를 계산
- n이 1 이하일 때는 그대로 n을 반환
- 그렇지 않으면 n-1과 n-2의 피보나치 수를 재귀적으로 호출하여 더한 값을 반환
- static void Main(): 프로그램의 시작점.
- nterms라는 변수를 정의하여 계산할 피보나치 수열의 항의 개수를 지정
- 0부터 nterms - 1까지의 값을 반복하여 각 항에 대한 피보나치 값을 계산하고 출력
연산자 학습
& bit AND결과
- 00000101 + 00000100 = 00000100 곂치는 부분만 1로됨
- 불리언을 대체해서 사용 가능
- 조건문에서 많이 사용
bit OR결과
- 00000101 + 00000100 = 00000101 둘중 하나라도 1이면 True
bit XOR
- 00000101 + 00000100 = 00000001 둘중 하나만 1이어야 1
bit OR / bit XOR 연산자가 현업에서 제일 많이 사용된다.
= 할당 연산자 += 할당과 더하기 연산자 -= 할당과 빼기 연산자 *= 할당과 곱하기 연산자 /= 할당과 나누기 연산자 %= 할당과 나머지 연산자 &= 할당과 and 연산자 |= 할당과 or 연산자 ^= 할당과 xor 연산자 <<= 할당과 left shift연산자 >>= 할당과 right shift연산자 |
클래스 (Class)
- 참조 타입: 클래스는 참조 타입(reference type)입니다. 클래스 객체는 힙(heap)에 저장되고, 변수는 객체의 참조를 저장한다.
- 참조 복사: 클래스 객체를 다른 변수에 할당하면, 두 변수는 동일한 객체를 참조한다.
dog1과 dog2는 동일한 객체를 참조하므로, 하나의 객체에서 변경된 내용이 다른 객체에도 반영된다.
구조체 (Struct)
- 값 타입: 구조체는 값 타입(value type). 구조체 변수는 스택(stack)에 저장되고, 변수 자체가 데이터를 저장
- 값 복사: 구조체 변수를 다른 변수에 할당하면, 데이터의 복사본이 생성됩니다. 각 변수는 독립적인 값을 가진다.
cat1과 cat2는 각각 독립적인 구조체 인스턴스를 가집니다. 하나의 인스턴스에서 변경된 내용이 다른 인스턴스에 영향을 주지 않습니다.
- 클래스 (Class)
- 참조 타입(reference type)
- 객체는 힙(heap)에 저장되며, 변수가 객체를 참조함
- 변수에 객체를 할당하면 참조가 복사됨 (동일 객체 참조)
- 객체의 데이터는 여러 변수가 공유함
- 예제에서 dog1과 dog2는 동일한 객체를 참조
- 하나의 형태를 정의할 때 사용
- 구조체 (Struct)
- 값 타입(value type), 복사 (다른객체로 인식)
- 변수 자체가 데이터를 저장하며, 스택(stack)에 저장됨
- 변수에 구조체를 할당하면 값이 복사됨 (독립적인 데이터)
- 각 구조체 인스턴스는 독립적인 값을 가짐
- 예제에서 cat1과 cat2는 서로 다른 독립적인 구조체 인스턴스
C#에서 상속, 다형성, override, virtual 키워드는 객체 지향 프로그래밍(OOP)의 중요한 개념이다.
상속 (Inheritance)
상속은 한 클래스가 다른 클래스의 특성과 행동을 상속받아 확장하거나 수정할 수 있게 한다.
상속을 통해 코드 재사용성이 높아지고, 클래스 간의 관계를 정의할 수 있다.
- Dog 클래스는 Animal 클래스를 상속받아 Eat 메서드를 사용할 수 있다
- Dog 클래스는 추가로 Bark 메서드를 정의하여 자신의 특성을 확장한다
다형성 (Polymorphism)
다형성은 같은 메서드 호출이 객체의 실제 타입에 따라 다르게 동작하도록 하는 기능입니다. 일반적으로 상속과 인터페이스를 통해 구현됩니다.
- MakeSound 메서드는 virtual 키워드로 선언되어 자식 클래스에서 재정의될 수 있다
- Dog와 Cat 클래스는 MakeSound 메서드를 override 키워드를 사용해 재정의한다.
- Animal 타입의 변수 myDog와 myCat는 각각 Dog와 Cat 객체를 참조하며, 실제 객체 타입에 따라 적절한 MakeSound 메서드가 호출된다.
인터페이스 (Interface)
인터페이스는 C#에서 함수들의 정의를 내려주는 개념으로, 특정 클래스가 반드시 구현해야 하는 메서드들을 강제하기 위해 사용된다. 인터페이스를 통해 함수의 통일성을 유지하고, 클래스 간의 일관성을 보장할 수 있다.
인터페이스의 주요 특징:
- 함수들의 정의: 인터페이스는 함수의 시그니처만 정의하며, 구현은 하지 않는다. 이를 구현하는 클래스는 반드시 인터페이스에 정의된 모든 함수를 구현해야 한다. 여기서 출력은 반듯이 되어야하는 것은 아니다.
- 강제 구현: 인터페이스를 구현하는 클래스는 인터페이스에 선언된 모든 메서드를 구현해야 한다.. 이를 통해 강제적인 구조를 만들 수 있는 것이다.
- 다중 상속: 클래스는 여러 인터페이스를 구현할 수 있다. 이는 C#에서 클래스 다중 상속을 허용하지 않는 대신, 인터페이스를 통해 다중 상속의 효과를 낼 수 있게 한다.
- 객체화 불가능: 인터페이스 자체로는 인스턴스를 생성할 수 없다. 오직 인터페이스를 구현한 클래스를 통해서만 객체를 생성할 수 있다.
인터페이스의 주요 용도
- 함수 통일화: 인터페이스를 사용하면 서로 다른 클래스들이 동일한 메서드를 구현하도록 강제할 수 있고, 이를 통해 함수의 이름, 파라미터, 반환 타입을 일관성 있게 유지할 수 있게 된다.
- 다형성 구현: 인터페이스를 사용하면 같은 인터페이스를 구현한 클래스들은 동일한 메서드 집합을 가지므로, 다형성을 구현할 수 있다.
- 유연한 설계: 새로운 클래스가 추가되어도 인터페이스를 구현하기만 하면 기존 코드와 쉽게 통합될 수 있습니다.
인터페이스 구현 시 주의 사항
- 인터페이스를 구현하는 클래스는 반드시 모든 인터페이스 메서드를 구현해야 합니다.
- 만약 하나라도 구현하지 않으면 컴파일 에러가 발생한다.
- 인터페이스는 메서드의 구현을 제공하지 않으므로, 실제 로직은 이를 구현하는 클래스에서 작성해야 한다.
- 인터페이스 자체로는 인스턴스를 생성할 수 없다. 인터페이스를 구현한 클래스를 통해서만 객체를 생성할 수 있다.
정리하면 interface -> 함수들의 정의를 내려주는 개념 / 이것은 무저껀 있어야 하는 개념
- 함수를 통일화하기 위해서 사용함.
- 인터페이스를 받고 있으면 인터페이스에 선언된 함수들은 무조건 구현되어야한다 (강제상속)
- 인터페이스는 오버라이딩을 꼭 해주어야한다.
그 함수를 상속 받아서 객체화시키지 않으면 그 객체를 활성화하지 못함
이 함수를 오버라이드 해서 써야해 할때 이 함수를 사용함, 객체화도 불가능
<수업중 코드>
namespace _20240530 { public struct Cat { public string name; public int age; public void PrintMyInfo() { Console.WriteLine("name : " + name + " age : " + age); } } public class Dog { public string name; public int age; public void PrintMyInfo() { Console.WriteLine("name : " + name + " age : " + age); } } |
- Cat 구조체와 Dog 클래스가 정의되어 있다. 둘 다 name과 age라는 두 개의 필드를 가지고 있으며, PrintMyInfo 메서드를 통해 자신의 정보를 출력한다.
- struct와 class의 주요 차이점은 struct가 값 형식인 반면, class는 참조 형식이라는 것이다
internal class Program { static void Function1(int k) { for (int i = k; i < 10; i++) { Console.WriteLine(i); } } static void Main(string[] args) { |
- Function1은 k부터 10까지의 숫자를 출력하는 간단한 함수
- Main 메서드는 프로그램의 진입점으로, 실행되는 첫 번째 메서드
Dog a = new Dog(); a.name = "멍멍이"; a.age = 1; a.PrintMyInfo(); Dog b = new Dog(); b.name = "깡깡이"; b.age = 3; b.PrintMyInfo(); Dog c = a; c.PrintMyInfo(); a.name = "셜록이"; c.PrintMyInfo(); |
- Dog 클래스의 인스턴스 a와 b를 생성하고 각각의 이름과 나이를 설정한 후, PrintMyInfo 메서드를 호출하여 정보를 출력
- Dog 객체 c는 a를 참조합니다. c는 a와 동일한 객체를 참조하므로, a의 이름을 변경하면 c의 이름도 변경된 것을 볼 수 있다. 이는 class가 참조 형식이기 때문
Cat d = new Cat(); d.name = "야옹이"; d.age = 5; Cat e = d; d.name = "곱슬이"; e.PrintMyInfo(); d.PrintMyInfo(); } } } |
- Cat 구조체의 인스턴스 d를 생성하고 이름과 나이를 설정
- Cat 구조체 e는 d를 복사합니다. 이때 struct는 값 형식이므로 e는 d의 복사본이 됩니다. 따라서 d의 이름을 변경해도 e의 이름은 영향을 받지 않는다
- PrintMyInfo 메서드를 호출하여 각각의 정보를 출력
<전체코드>
namespace _20240530 { public struct Cat { public string name; public int age; public void PrintMyInfo() { Console.WriteLine("name : " + name + " age : " + age); } } public class Dog { public string name; public int age; public void PrintMyInfo() { Console.WriteLine("name : " + name + " age : " + age); } } internal class Program { static void Function1(int k) { for (int i = k; i < 10; i++) { Console.WriteLine(i); } } static void Main(string[] args) { Dog a = new Dog(); a.name = "멍멍이"; a.age = 1; a.PrintMyInfo(); Dog b = new Dog(); b.name = "깡깡이"; b.age = 3; b.PrintMyInfo(); Dog c = a; c.PrintMyInfo(); a.name = "셜록이"; c.PrintMyInfo(); //////////////////strcut/////////////////////// Cat d = new Cat(); d.name = "야옹이"; d.age = 5; Cat e = d; d.name = "곱슬이"; e.PrintMyInfo(); d.PrintMyInfo(); } } } |
전체 코드의 동작
- Main 메서드가 실행되면서 Dog 객체 a와 b가 생성.
- a와 b의 이름과 나이가 설정되고, PrintMyInfo 메서드로 각각의 정보를 출력
- c는 a를 참조합니다. a의 이름을 변경하면 c의 출력 결과도 변경된 이름을 반영
- Cat 구조체 d가 생성되고, d의 값을 e에 복사한다. d와 e는 독립적인 복사본을 가지므로, d의 이름을 변경해도 e의 값에는 영향을 주지 않는다.
- PrintMyInfo 메서드를 통해 Cat 구조체의 정보가 출력된다.
이 코드는 struct와 class의 차이점, 특히 값 형식과 참조 형식의 동작 방식을 명확히 이해하는 데 도움이 된다.
네임스페이스와 인터페이스
namespace _20240530 { public interface iSignal { void Send(); void Recv(); } |
_20240530 네임스페이스 내에 iSignal 인터페이스가 정의되어 있다. 이 인터페이스는 Send와 Recv라는 두 메서드를 선언하고 있는데 인터페이스는 이를 구현하는 클래스가 두 메서드를 정의하도록 강제하게 된다.
public class human { virtual public void Shot() { } } |
human 클래스는 Shot이라는 가상 메서드를 정의하며, 가상 메서드는 파생 클래스에서 재정의할 수 있다.
public class marin : human, iSignal { virtual public void Send() { } virtual public void Recv() { } override public void Shot() { } } public class firebat : human { override public void Shot() { } } |
- marin 클래스는 human 클래스를 상속받고 iSignal 인터페이스를 구현한다. Shot, Send, Recv 메서드를 재정의한다.
- firebat 클래스는 human 클래스를 상속받고 Shot 메서드를 재정의한다.
또다른 인터페이스와 클래스 정의
public interface AnimalActing { void PrintMyInfo(); } public class Test1 { } public class Test2 { } public class Animal : Test1, AnimalActing { public string name; public int age; virtual public void PrintMyInfo() { Console.WriteLine("name : " + name + " age : " + age); } public void NewFunction() { Console.WriteLine("원본이지렁"); } } |
- AnimalActing 인터페이스는 PrintMyInfo 메서드를 선언
- Animal 클래스는 Test1 클래스를 상속받고 AnimalActing 인터페이스를 구현
- PrintMyInfo와 NewFunction 메서드를 정의.
public class Cat : Animal { override public void PrintMyInfo() { Console.WriteLine("난 고양이지렁"); } new public void NewFunction() { Console.WriteLine("재정의 되었지렁"); } } public class Dog : Animal { } public class T : Dog { } |
- Cat 클래스는 Animal 클래스를 상속받고 PrintMyInfo를 재정의하며, NewFunction을 새로 정의
- Dog 클래스는 Animal 클래스를 상속
- T 클래스는 Dog 클래스를 상속받음
Main 메서드와 프로그램 실행
internal class Program { public static iSignal signal; static void Function1(int k) { for (int i = k; i < 10; i++) { Console.WriteLine(i); } } static void Main(string[] args) { signal = new marin(); for (int i = 0; i < 10; i++) { if (signal != null) { signal.Send(); } signal = null; } |
- Main 메서드는 프로그램의 진입점입니다. signal은 iSignal 타입
- Function1은 k부터 10까지의 숫자를 출력하는 메서드
- Main 메서드에서 marin 객체를 signal로 설정한 후, Send 메서드를 호출하고 signal을 null로 설정
Dog 클래스의 사용 및 출력
Dog a = new Dog(); a.name = "멍멍이"; a.age = 1; a.PrintMyInfo(); Dog b = new Dog(); b.name = "깡깡이"; b.age = 3; b.PrintMyInfo(); Dog c = a; c.PrintMyInfo(); a.name = "셜록이"; c.PrintMyInfo(); |
- Dog 객체 a, b를 생성하고 각각의 정보를 출력
- c는 a를 참조하며, a의 이름을 변경한 후 c의 정보를 다시 출력
Cat 클래스의 사용 및 출력
Cat d = new Cat(); d.name = "야옹이"; d.age = 5; Cat e = d; d.name = "곱슬이"; e.PrintMyInfo(); d.PrintMyInfo(); Cat k = new Cat(); k.PrintMyInfo(); Animal animal = k; animal.PrintMyInfo(); animal.NewFunction(); k.NewFunction(); ((Cat)animal).NewFunction(); Animal animal2 = new Animal(); |
- Cat 객체 d, e를 생성하고 각각의 정보를 출력
- Animal 타입의 animal 변수는 Cat 객체 k를 참조하며, PrintMyInfo와 NewFunction 메서드를 호출
- NewFunction 메서드는 Cat 객체에서 재정의된 메서드가 호출
marin과 firebat 객체의 사용
marin marinObj = new marin(); firebat firebatObj = new firebat(); human[] armys = { marinObj, firebatObj }; marinObj.Shot(); firebatObj.Shot(); for (int i = 0; i < armys.Length; i++) { ((iSignal)armys[i]).Send(); } } } } |
- marin과 firebat 객체를 생성하여 각각 Shot 메서드를 호출
- armys 배열에 marin과 firebat 객체를 저장하고, 각 요소에 대해 Send 메서드를 호출합니다. marin 클래스만 iSignal 인터페이스를 구현하므로, 캐스팅 시 firebat 객체에서 런타임 오류가 발생할 수 있다.
핵심 키워드
- 업캐스팅(upcasting): Cat 객체를 Animal 타입의 변수로 참조
- 다형성(polymorphism): 부모 클래스 타입의 변수로 자식 클래스의 오버라이드된 메서드를 호출
- 메서드 숨기기(method hiding): new 키워드를 사용하여 부모 클래스의 메서드를 숨길 수 있습니다. 이 경우, 변수의 타입에 따라 호출되는 메서드가 달라진다.
- 캐스팅(casting): 부모 클래스 타입의 변수를 자식 클래스 타입으로 캐스팅하면, 자식 클래스의 숨겨진 메서드를 호출할 수 있다.
집중적으로 설명하셨던 내용
Animal animal = k; // k는 animal 변수에 참조 되지만 실제 객체는 cat이기 때문에 |
- k는 Cat 클래스의 인스턴스
- animal 변수는 Animal 타입이지만, 실제로 Cat 객체를 참조
- 이는 업캐스팅(upcasting)이라고 불리며, 자식 클래스의 인스턴스를 부모 클래스 타입의 변수로 참조하는 것
animal.PrintMyInfo(); // 함수를 cat이 오버라이드 해서 printmyinfo가 cat에 있는게 실행된다. |
- PrintMyInfo 메서드는 Cat 클래스에서 오버라이드
- animal 변수는 Animal 타입이지만, 실제 객체는 Cat이므로 Cat 클래스의 PrintMyInfo 메서드가 호출
- 이는 다형성(polymorphism)의 예시로, 참조 변수의 타입이 아니라 실제 객체의 타입에 따라 메서드가 호출
animal.NewFunction(); // new 키워드로 자식에서 덮어 썼지만 얘는 오버라이드 되는게 되는게 아니라 |
- NewFunction 메서드는 Cat 클래스에서 new 키워드를 사용하여 숨겨져있다.
- animal 변수는 Animal 타입이므로, Animal 클래스의 NewFunction 메서드가 호출
- new 키워드는 메서드 숨기기를 의미하며, 이는 오버라이딩과 다르다. 오버라이딩은 부모 클래스의 메서드를 재정의하는 것이고, 메서드 숨기기는 부모 클래스의 메서드를 가리는 것이다.
k.NewFunction(); // k를 실행시켜보면 k의 함수가 실행된다. |
- k는 Cat 타입이므로, Cat 클래스의 NewFunction 메서드가 호출
- 이는 new 키워드로 숨겨진 메서드가 실제로 호출되는 상황을 보여준다.
((Cat)animal).NewFunction(); // Cat으로 변환 된 오브젝트에 한해서만 new 키워드로 선언한 함수가 사용된다. |
- animal 변수를 Cat 타입으로 캐스팅하면, Cat 클래스의 NewFunction 메서드가 호출
- 이는 animal이 실제로 Cat 객체이기 때문에 가능한 것이다.
- 캐스팅 후에는 Cat 클래스의 NewFunction 메서드가 호출된다.