C·Java·Python 코드 실행 추적법
연산자, 함수 호출, 객체와 컬렉션의 상태를 줄 단위로 추적해 코드형 문제를 푸는 방법입니다.
C의 식과 포인터
C 코드는 선언으로 자료형과 초기값을 적은 뒤, 각 문장의 대입 결과를 별도 표에 기록해야 합니다. 포인터는 주소를 저장하고 *p는 그 주소의 값을 읽거나 바꾸므로 p, *p, &p를 같은 값으로 취급하면 안 됩니다.
- 후위 증감은 현재 식에서 기존 값을 사용한 뒤 값을 바꾼다.
- 배열 이름은 많은 식에서 첫 원소 주소로 변환되지만 배열 자체와 포인터 변수는 동일하지 않다.
- 정수 나눗셈은 소수부를 버리므로 피연산자 자료형을 먼저 확인한다.
Java의 객체와 다형성
참조 변수의 선언형은 호출 가능한 멤버를 제한하고, 실제 객체형은 오버라이딩된 인스턴스 메서드의 실행 대상을 결정합니다. 필드와 static 메서드는 다형적 디스패치와 다르게 해석될 수 있어 표로 분리해야 합니다.
- 오버로딩은 매개변수 목록, 오버라이딩은 상속 관계와 같은 시그니처가 기준이다.
- finally는 일반적으로 예외 여부와 관계없이 실행되지만 강제 종료 상황은 예외다.
- equals와 ==는 객체의 논리적 동등성과 참조 동일성을 구분한다.
Python의 컬렉션과 범위
Python은 들여쓰기가 블록을 만들며 슬라이스의 끝 인덱스는 포함하지 않습니다. 리스트는 변경 가능하고 튜플은 변경 불가능하므로 함수에 전달한 뒤 원본이 바뀌는지 판단할 때 객체의 변경 가능성을 확인합니다.
- range(start, stop, step)의 stop은 포함되지 않는다.
- and와 or는 항상 bool만이 아니라 평가를 결정한 피연산자를 반환할 수 있다.
- 얕은 복사는 중첩 객체를 공유할 수 있다.
코드를 상태표로 바꿔 실행 결과 추적하기
코드 결과 문제는 한 줄씩 실행하면서 변수 값, 조건식 결과, 출력 내용을 표로 기록해야 연산자 우선순위와 증감 연산의 함정을 피할 수 있습니다. 참조형 자료는 값 자체가 아니라 같은 객체를 가리키는지까지 표시합니다.
- 후위 증감은 현재 값을 식에 사용한 뒤 변수를 변경하고 전위 증감은 먼저 변경한다.
- 정수 나눗셈·형 변환·오버플로 규칙은 언어별로 확인한다.
- 재귀 호출은 매 호출의 매개변수와 반환 위치를 스택 프레임처럼 적는다.
메모리·예외·언어별 평가 규칙
코드 추적에서는 값의 변화뿐 아니라 변수의 생존 범위와 객체의 공유 여부를 기록해야 합니다. 예외가 발생하면 현재 문장의 남은 부분이 실행되는지, 어떤 catch가 선택되는지, finally 이후 반환값이 어떻게 결정되는지를 순서대로 확인합니다.
- C의 배열 경계를 벗어난 접근과 초기화되지 않은 값 사용은 정의되지 않은 동작을 만들 수 있다.
- Java의 문자열은 불변이며 StringBuilder는 같은 객체의 내용을 변경한다.
- Python의 기본 인수는 함수 정의 시 한 번 평가되므로 변경 가능한 객체를 기본값으로 둘 때 주의한다.
- 단락 평가는 결과가 결정되면 뒤 피연산자를 실행하지 않아 부수효과 발생 여부가 달라질 수 있다.
C 포인터·배열·구조체·재귀 추적
C 문제는 선언에서 자료형과 포인터 단계 수를 표시하고 배열 인덱스·포인터 산술·함수 인수 전달을 한 문장씩 추적합니다. 실행 순서가 정의되지 않았거나 배열 범위를 벗어나는 코드는 임의의 한 출력값을 정답으로 계산하면 안 됩니다.
- 배열 식별자는 많은 식에서 첫 원소 포인터로 변환되지만 sizeof 배열과 &배열처럼 배열 전체 형식이 유지되는 문맥이 있다.
- p+1은 p가 가리키는 자료형 크기만큼 이동하고 *p++는 연산자 결합에 따라 *(p++)로 해석된다.
- C의 함수 인수는 값으로 전달되므로 호출자 변수를 바꾸려면 그 주소를 전달해 역참조한다.
- 구조체의 .는 객체 멤버, ->는 구조체 포인터가 가리키는 객체의 멤버 접근에 사용한다.
- 재귀는 종료조건과 각 호출의 지역 상태·반환 후 남은 연산을 스택 순서대로 기록한다.
Java·Python 객체와 컬렉션 변화
Java는 참조 변수의 선언형과 실제 객체형, 인스턴스·정적 멤버를 나누고 Python은 이름이 가리키는 객체와 변경 가능성을 나눠 봅니다. 두 언어 모두 같은 객체를 여러 변수가 공유하면 한쪽 변경이 다른 쪽 관찰 결과에 반영될 수 있습니다.
- Java 인스턴스 메서드 오버라이딩은 실제 객체형, 오버로딩 선택은 컴파일 시점의 선언형과 인수 형식을 중심으로 결정된다.
- Java String은 불변이고 equals는 논리적 내용, ==는 참조 동일성을 비교하는 상황을 구분한다.
- Python 리스트 슬라이스는 얕은 새 리스트를 만들 수 있지만 내부의 변경 가능한 원소 객체는 공유될 수 있다.
- Python and·or는 단락 평가 후 결과를 결정한 피연산자 자체를 반환할 수 있고 range·slice의 끝 값은 포함하지 않는다.
- 예외 문제는 throw·raise 지점 → 일치하는 처리기 → finally → 반환 또는 전파 순으로 추적한다.
시험에 바로 쓰는 비교표
| 구분 | 먼저 확인할 것 | 대표 함정 | 검산 방법 |
|---|---|---|---|
| C | 자료형·초기값·연산 순서 | 정수 나눗셈, 포인터 역참조 | 변수별 값 표 작성 |
| Java | 상속 관계·선언형·실제형 | 오버로딩과 오버라이딩 | 호출 가능한 멤버와 실행 메서드 분리 |
| Python | 자료형·변경 가능성·범위 | 슬라이스 끝, 얕은 복사 | 각 반복 직후 컬렉션 상태 기록 |
지문 표현을 판단 기준으로 바꾸기
- 기존 값을 식에 사용한 뒤 1 증가 → 후위 증가
- 실제 객체의 재정의 메서드 실행 → 동적 바인딩
- 끝 인덱스를 포함하지 않는 부분 추출 → 슬라이싱
정보처리기사 실기 · Java 상속
같은 객체인데 필드와 메서드의 결과가 다른 이유
선언된 타입과 실제 객체를 나누어 보면 상속 코드가 읽힙니다. 세 줄의 출력을 추적하고 super까지 확인합니다.
01조건과 코드 읽기
Base 타입의 변수 item이 Child 객체를 가리킵니다. points는 인스턴스 필드이고 value()와 total()은 인스턴스 메서드입니다. static 메서드나 오버로딩은 없는 예제입니다.
| 구분 | 상태 |
|---|---|
| item의 선언 타입 | Base |
| 생성한 실제 객체 | Child |
| Base가 선언한 points | 4 |
| Child가 선언한 points | 9 |
class Base {
int points = 4;
int value() {
return points + 1;
}
int total() {
return value() + points;
}
}
class Child extends Base {
int points = 9;
@Override
int value() {
return points * 2;
}
}
public class DispatchTrace {
public static void main(String[] args) {
Base item = new Child();
System.out.println(item.points);
System.out.println(item.value());
System.out.println(item.total());
}
}먼저 예상해보세요item.points, item.value(), item.total()은 각각 무엇을 출력할까요? 모든 줄에 같은 선택 규칙을 적용하지 않는 것이 핵심입니다.
실행 결과와 풀이펼쳐보기접기
확인 결과
4 18 22
| 표현 | 선택 기준 | 실행·계산 | 출력 |
|---|---|---|---|
| item.points | 선언 타입 Base의 필드 | Base.points | 4 |
| item.value() | 실제 객체의 오버라이딩 | Child.value(): 9 × 2 | 18 |
| item.total() | Base 본문 + 동적 메서드 호출 | Child.value() + Base.points | 22 |
item.points는 변수의 선언 타입 Base를 기준으로 필드를 선택해 4입니다. Child의 같은 이름 필드는 Base 필드를 덮어쓰지 않습니다. 반면 item.value()는 오버라이딩된 인스턴스 메서드이므로 실제 객체 Child의 구현을 실행해 9 × 2 = 18입니다.
total()은 Child가 오버라이딩하지 않아 Base.total()의 본문을 실행합니다. 본문 안의 value()도 같은 Child 객체에 대한 호출이므로 18을 반환합니다. 하지만 그 본문에 쓰인 points는 Base 필드 4이므로 합계는 22입니다.
02조건을 바꿔 다시 풀기
확인 1변수의 선언 타입만 Child로 바꾸면?
main의 변수 선언만 아래와 같이 바꾸고 나머지 코드는 유지합니다. 세 줄의 출력 중 무엇이 바뀔까요?
Child item = new Child();확인 1 정답과 이유펼쳐보기접기
확인 결과
9 18 22
item.points는 Child 타입의 필드를 선택하므로 9로 바뀝니다. 실제 객체는 여전히 Child여서 value()는 18입니다. Base.total()의 본문 자체는 바뀌지 않았으므로 그 안의 points는 여전히 Base 필드 4이고 total()은 22입니다.
확인 2super.value()를 호출하면?
기본 예제의 Base item 선언으로 돌아갑니다. Child의 value()만 아래 코드로 교체할 때 세 줄의 출력을 구하세요.
@Override
int value() {
return super.value() + points;
}확인 2 정답과 이유펼쳐보기접기
확인 결과
4 14 18
super.value()는 Base의 구현을 직접 호출해 Base.points + 1 = 5를 반환합니다. 여기에 Child.value() 본문의 points 9를 더해 14입니다. total()에서는 이 14에 Base.points 4를 더하므로 18입니다. super가 부모 객체를 새로 만드는 것은 아닙니다.
이것만 기억하세요필드는 선언된 타입과 코드 위치를, 오버라이딩된 인스턴스 메서드는 실제 객체를 봅니다. 상속받은 메서드 안에서도 두 규칙을 구분해서 적용하세요.
직접 풀어보는 예제와 단계별 해설
아래 Python 3 예제는 이름과 객체, 인덱스와 값, 호출과 반환을 각각 분리해 추적하는 연습입니다. 직접 설계한 학습 예제이며 실제 기출이나 사용자 오답 통계가 아닙니다.
같은 리스트를 가리키는 이름과 복사본
각 대입이 새 리스트를 만드는지 먼저 판단한 뒤 두 출력값을 구해 보세요. 리스트의 원소는 정수로 한정합니다.
Python 3
a = [2, 4, 6]
b = a
c = a[:]
b[1] += 3
c.append(a[1])
print(a)
print(c)| 실행 직후 | a와 b가 가리키는 리스트 | c가 가리키는 리스트 |
|---|---|---|
| c = a[:] | [2, 4, 6] | [2, 4, 6] (별도 객체) |
| b[1] += 3 | [2, 7, 6] | [2, 4, 6] |
| c.append(a[1]) | [2, 7, 6] | [2, 4, 6, 7] |
풀이 순서
- b = a는 원소를 복사하지 않고 같은 리스트에 이름을 하나 더 붙입니다. 따라서 b[1]을 바꾸면 a[1]도 7로 관찰됩니다.
- a[:]는 새 바깥 리스트를 만듭니다. 이 예제의 정수 원소를 교체하는 동작은 c에 영향을 주지 않습니다. 마지막 줄은 현재 a[1]인 7을 c 끝에 붙입니다.
확인 결과
[2, 7, 6]
[2, 4, 6, 7]틀리기 쉬운 지점: 슬라이스를 무조건 깊은 복사라고 외우면 안 됩니다. 원소가 다시 리스트인 중첩 구조에서는 내부 객체가 공유될 수 있습니다.
반복 인덱스가 아니라 원소 값을 더하는 경우
range의 세 인자와 배열 접근을 서로 다른 단계로 적습니다.
Python 3
values = [3, 8, 2, 7, 4, 9]
total = 0
for i in range(1, len(values), 2):
total += values[i] - i
print(total)| i | values[i] | 더하는 값 | total |
|---|---|---|---|
| 1 | 8 | 8 − 1 = 7 | 7 |
| 3 | 7 | 7 − 3 = 4 | 11 |
| 5 | 9 | 9 − 5 = 4 | 15 |
풀이 순서
- range(1, 6, 2)는 끝값 6을 포함하지 않으므로 1, 3, 5를 만듭니다.
- 원소 값의 합은 24, 인덱스의 합은 9입니다. 이 코드는 두 합의 차이 15를 출력하므로 24나 9를 답으로 쓰면 계산 대상 자체를 바꾼 셈입니다.
확인 결과
15틀리기 쉬운 지점: 반복 횟수가 세 번이라는 사실만으로 답은 나오지 않습니다. 매 반복의 피연산자를 표에 넣어야 합니다.
재귀는 호출 순서와 반환 순서를 나누어 쓰기
f(5)의 호출 사슬을 먼저 쓰고, 가장 안쪽 호출부터 결과를 올려 보세요.
Python 3
def f(n):
if n <= 1:
return 2
return f(n - 2) + n
print(f(5))| 호출 | 대기 중인 계산 | 반환값 |
|---|---|---|
| f(1) | 기저 조건 n <= 1 | 2 |
| f(3) | f(1) + 3 | 5 |
| f(5) | f(3) + 5 | 10 |
풀이 순서
- 호출은 f(5) → f(3) → f(1) 순서입니다. f(1)은 다시 호출하지 않고 2를 돌려줍니다.
- 반환은 반대 순서로 계산됩니다. 2 + 3 = 5를 받은 바깥 호출이 5를 더해 10이 됩니다.
확인 결과
10틀리기 쉬운 지점: 기저 조건을 n = 0이라고 바꾸어 읽거나 모든 n을 1씩 감소시키면 다른 프로그램을 풀게 됩니다.
검증 방법과 참고 문서
예제 코드의 출력과 표의 계산을 로컬 자동 테스트로 대조합니다. C·Java에 Python의 리스트 복사나 나눗셈 규칙을 그대로 적용하지 않습니다.
예제·데이터·풀이표: 정처LAB 자체 설계 · AI 보조 작성 및 로컬 검증 · 예제 수정일 2026-09-15
자주 틀리는 판단
- 증감 연산의 사용 시점과 실제 값 변경 시점을 혼동
- Java 참조 변수형과 실제 객체형을 같은 역할로 판단
- Python 슬라이스의 끝 인덱스를 포함한다고 계산
개념을 확인했다면 실제 문제에서 판단 기준을 적용해보세요.
첫 관련 문제 풀기