수업을 들으며 노트에 정리한 것 (함수)
함수 개요
| 상황 | 해법 |
|---|---|
| 하는 일은 같은데 받을 값의 타입이 여러 가지임 | 오버로딩 |
| 넘긴 변수 자체를 함수가 바꿔 주길 원함 | ref |
| 돌려줄 값이 둘 이상임 | out · 튜플 |
| 넘길 값의 개수가 그때그때 다름 | params |
| 같은 일을 점점 작은 문제로 줄여 가며 풀어야 함 | 재귀 |
오버로딩 규칙
- 오버로딩이 성립하려면 매개변수 목록이 달라야 한다.
| 다른 점 | 예 |
|---|---|
| 개수 | Add(int) · Add(int, int) |
| 타입 | Add(int, int) · Add(double, double) |
| 순서 | Draw(string, int) · Draw(int, string) |
- 실행문 사이의 함수는 오버로딩이 불가하다.
기본값으로 대신할 수 있는 경우
- 매개변수 개수만 다른 오버로딩은 기본값으로 대신할 수 있는 경우가 많다.
방식 | 알맞은 상황 |
|---|---|
기본값 | 하는 일이 완전히 같고 값 하나만 생략하는 경우 |
오버로딩 | 받는 타입이 다르거나, 경우마다 하는 일이 다른 경우 |
ref 원본을 직접 다루기
- 매개변수와 인수 양쪽에
ref를 붙이면 복사본이 아니라 원본 변수 자체가 전달된다. ref를 사용할 때 지켜야할 것 2가지
| 규칙 | 이유 |
|---|---|
호출할 때도 ref를 적음 | 원본이 바뀔 수 있다는 사실이 호출부에 드러남 |
| 넘기는 변수를 미리 초기화해 둠 | 함수가 그 값을 읽을 수 있어야 함 |
- 배열 요소도
ref로 넘길 수 있다.
out 값을 내보내는 통로
out은ref와 비슷하지만 쓰임이 반대다. 함수가 값을 채워 내보내기 위한 매개변수이다.ref와의 차이는 3개가 있다.
ref | out | |
|---|---|---|
| 넘기기 전 초기화 | 필요함 | 필요 없음 |
| 함수 안에서 값 할당 | 선택 | 반드시 해야 함 |
| 쓰임 | 값을 주고받음 | 값을 내보냄 |
out변수를 호출 자리에 선언하기
out인수로 쓸 변수는 호출하는 자리에서 바로 선언할 수 있다. 미리 선언해 두는 방식과 결과가 같다.
매개변수 모디파이어 정리
- in이라는 모디파이어도 있다. 참조로 넘기되 함수 안에서 못 바꾸게 막는 것이며, 큰 값을 복사 없이 읽기만 할 때 쓴다.
| 모디파이어 | 넘기기 전 초기화 | 함수 안에서 | 원본 |
|---|---|---|---|
| (없음) | 필요함 | 복사된 매개변수를 바꿀 수 있음 | 매개변수 재대입은 원본 변수에 반영되지 않음 |
ref | 필요함 | 바꿔도 됨 | 바뀜 |
out | 필요 없음 | 반드시 값을 넣음 | 바뀜 |
⚠️ ref를 남용하지 말 것: 단순 결과는 반환값으로 표현하고, 호출자의 변수 자체를 변경해야 할 때
ref를 사용함. 변경할 변수가 둘 이상이어야 한다는 문법 제한은 없음. 참조 타입 인수를 값으로 전달해도 그 객체 내부는 변경할 수 있음
개수가 정해지지 않은 인수
parmas
- 받은 인수의 개수를 미리 정할 수 없을 때
parmas를 붙인 배열 매개변수를 쓴다. parmas는 맨 뒤에 하나만 올 수 있다. 앞쪽에는 일반 매개변수를 둔다.
여러 값을 한 번에 반환하기
듀플로 묶어서 돌려주기
out으로도 여러 값을 내보낼 수 있지만, 값 여러 개를 하나로 묶어 반환하는 방법이 따로 있다.
반환 타입 자리에 괄호로 타입을 나열하면 된다.
using System;
var result = GetMinMax(new int[] { 5, 2, 8, 1, 9, 3 });
Console.WriteLine($"최솟값: {result.Item1}, 최댓값: {result.Item2}");
(int, int) GetMinMax(int[] numbers)
{
int min = numbers[0];
int max = numbers[0];
foreach (int number in numbers)
{
if (number < min) min = number;
if (number > max) max = number;
}
return (min, max);
}
이렇게 값 여러 개를 괄호로 묶은 것을 튜플(tuple) 이라고 함.
return (min, max); 처럼 괄호에 값을 나열해 돌려주고, 받는 쪽은
Item1 · Item2 로 꺼냄.
재귀
자기 자신을 부르는 함수
- 함수 안에서 자기 자신을 호출할 수 있다. 이것을 재귀(recursion)라고 한다.
using System;
CountDown(5);
void CountDown(int n)
{
if (n <= 0)
{
Console.WriteLine("발사!");
return;
}
Console.WriteLine(n);
CountDown(n - 1);
}
5
4
3
2
1
발사!
종료 조건이 없으면 맞추지 않는다.
- 재귀 함수에는 더 이상 자기를 부르지 않고 끝나는 조건이 반드시 있어야 한다. 이것을 종료 조건이라고 한다.
| 확인할 것 | 앞의 CountDown |
|---|---|
| 끝나는 조건이 있는가 | n <= 0 |
| 호출마다 그 조건에 가까워지는가 | n - 1이므로 매번 1씩 줄어듦 |
팩토리얼
- 재귀가 잘 들어 맞는 문제는 큰 문제가 같은 모양의 작은 문제로 줄어드는 경우임. 팩토리얼이 그런 예다.
5! = 5 × 4!이고 4! = 4 × 3!이므로, n!은 n × (n-1)!로 쓸 수 있음.
using System;
Console.WriteLine($"5! = {Factorial(5)}");
Console.WriteLine($"4! = {Factorial(4)}");
Console.WriteLine($"1! = {Factorial(1)}");
int Factorial(int n)
{
if (n <= 1)
{
return 1;
}
return n * Factorial(n - 1);
}
반복문으로도 되는 경우가 많다.
- 같은 계산을 반복문으로도 쓸 수 있다.
using System;
Console.WriteLine($"재귀: {FactorialRecursive(5)}");
Console.WriteLine($"반복: {FactorialLoop(5)}");
int FactorialRecursive(int n)
{
if (n <= 1) return 1;
return n * FactorialRecursive(n - 1);
}
int FactorialLoop(int n)
{
int result = 1;
for (int i = 2; i <= n; i++)
{
result *= i;
}
return result;
}
- 둘 다 같은 답을 내지만 이런 경우에는 반복문 쪽이 빠르고 안전하다. (함수 호출이 쌓이지 않기 때문이다.)
재귀가 비싸질 수 있다.
- 피보나치 수열을 재귀로 사용하면 정의 그대로여서 읽기 좋다.
- 다만 재귀로 쓸 수 있다는 것과 재귀로 쓰는 것이 좋다는 것은 다르다.
정렬과 검색
선택 정렬
- 정렬(sort) 은 데이터를 순서대로 늘어놓는 것이다. 여러 방법이 있는데, 그중 이해하기 쉬운 것이 선택 정렬(selection sort) 이다. 방법은 단순함. 아직 정리되지 않은 부분에서 가장 작은 값을 찾아 맨 앞과 바꾸는 것을 반복한다.
순차 검색
- 검색(search) 은 찾는 값이 어디 있는지 알아내는 것이다. 가장 단순한 방법은 앞에서부터 하나씩 보는 것이다.
이진 검색
- 정렬되어 있다면 훨씬 빠른 방법을 쓸 수 있다. 가운데 값을 보고 찾는 값이 그보다 큰지 작은지에 따라 한쪽 절반을 통째로 버리는 것이다.
⚠️ 전제 조건: 이진 검색은 정렬된 배열에서만 맞는 답을 냄. 정렬되지 않은 배열에 쓰면 있는 값을 못 찾고 -1 을 돌려줄 수 있음
두 검색의 비교
using System;
int[] data = new int[1000];
for (int i = 0; i < data.Length; i++)
{
data[i] = i * 2; // 0, 2, 4, ... 1998 (정렬된 상태)
}
Console.WriteLine($"순차 검색 비교 횟수: {CountSequential(data, 1998)}");
Console.WriteLine($"이진 검색 비교 횟수: {CountBinary(data, 1998)}");
int CountSequential(int[] values, int target)
{
int count = 0;
for (int i = 0; i < values.Length; i++)
{
count++;
if (values[i] == target) break;
}
return count;
}
int CountBinary(int[] values, int target)
{
int count = 0;
int low = 0;
int high = values.Length - 1;
while (low <= high)
{
count++;
int mid = (low + high) / 2;
if (values[mid] == target) break;
if (values[mid] < target) low = mid + 1;
else high = mid - 1;
}
return count;
}
순차 검색 비교 횟수: 1000
이진 검색 비교 횟수: 10
| 순차 검색 | 이진 검색 | |
|---|---|---|
| 정렬 필요 | 필요 없음 | 반드시 정렬돼 있어야 함 |
| 칸이 1,000개일 때 | 최악 1,000번 | 약 10번 |
| 칸이 100만 개일 때 | 최악 100만 번 | 약 20번 |
- 정렬하는 데 드는 비용이 있으므로, 한 번만 찾을 것이라면 순차 검색이 나을 수도 있다.
여러 번 찾을 데이터라면 한 번 정렬해 두고 이진 검색을 쓰는 편이 유리하다.
오늘도 어려운 함수 수업에 대해 정리해 보았다. 다만 아직 정리만 해둔 것일 뿐 머릿속에 다 들어와 있는 상태는 아니다.
문자열에서 살짝 자신감을 얻었다가 다시 함수로 돌아와서 머리가 포화 상태이다. 못다한 복습을 마저 진행하려고 한다.
2026년 10월 6일 기준으로 수업에서 텍스트 게임(카드 뒤집어 짝 맞추기)을 제작하는 시간을 가졌다.
해당 내용은 따로 디벨로그에 작성하는 것이 좋을 것 같다.



