포인터 / 재귀에 대해 너무 어려운 것은 무엇입니까? [닫은]


20

자바 학교위험 속에서 Joel은 Penn에서의 경험과 "세그먼트 결함"의 어려움에 대해 이야기합니다. 그는 말한다

"당신이 할 때까지 segfaults는 어렵다" "심호흡을하고 당신의 마음이 동시에 두 개의 다른 추상화 수준에서 작동하도록 노력하십시오."

segfault 의 일반적인 원인 목록을 감안할 때, 2 단계 추상화에서 어떻게 작업해야하는지 이해할 수 없습니다.

어떤 이유로 Joel은 이러한 개념을 프로그래머가 추상화하는 데 핵심이라고 생각합니다. 너무 많이 가정하고 싶지 않습니다. 그렇다면 포인터 / 재귀에 대해 너무 어려운 것은 무엇입니까? 예가 좋을 것입니다.


31
Joel이 당신에 대해 어떻게 생각하는지 걱정하지 마십시오. 재귀를 쉽게 찾으면 좋습니다. 다른 사람이하는 것은 아닙니다.
FrustratedWithFormsDesigner

6
재귀는 정의 (자체를 호출하는 함수)에 의해 쉽지만, 언제 사용하는지, 어떻게 작동시키는지를 아는 것은 어려운 부분입니다.
JeffO

9
Fog Creek에서 일자리를 신청하고 어떻게 진행되는지 알려주십시오. 우리는 모두 당신의 자기 승진에 매우 관심이 있습니다.
Joel Etherton

4
@ P.Brian.Mackey : 우리는 오해하지 않습니다. 질문은 실제로 아무 것도 요구하지 않습니다. 뻔뻔스러운 자기 홍보입니다. Joel이 포인터 / 재귀에 대해 무엇을 요구하는지 알고 싶으면 다음과 같이 물어보십시오. team@stackoverflow.com
Joel Etherton

19
질문 이 중복 되었습니까?
ozz

답변:


38

나는 대학에서 포인터와 재귀가 어렵다는 것을 처음 알았습니다. 나는 몇 가지 전형적인 첫해 과정을 밟았습니다 (하나는 C와 어셈블러 였고 다른 하나는 스킴에있었습니다). 두 과정 모두 수백 명의 학생들로 시작했으며, 그 중 다수는 수년간 고등학교 수준의 프로그래밍 경험을 가지고있었습니다 (일반적으로 그 당시 BASIC 및 Pascal). 그러나 C 코스에 포인터가 소개되고 Scheme 코스에 재귀가 도입 되 자마자 많은 수의 학생들이 아마도 완전히 독감에 걸렸습니다. 이들은 이전에 많은 코드를 작성했지만 전혀 문제가 없었지만 포인터와 재귀를 칠 때인지 능력 측면에서 벽에 부딪 쳤습니다.

내 가설은 포인터와 재귀가 동시에 두 개의 추상화 수준을 유지해야한다는 점에서 동일하다는 것입니다. 여러 수준의 추상에 대해서는 어떤 사람들은 결코 가질 수없는 정신적 소질이 필요합니다.

  • 포인터를 사용하여 "두 가지 추상화 수준"은 "데이터, 데이터 주소, 데이터 주소 등"또는 전통적으로 "값 대 참조"라고합니다. 훈련받지 않은 학생에게는 xx 자체 의 주소 차이를보기가 매우 어렵 습니다 .
  • 재귀를 통해 "두 가지 추상화 수준"은 함수가 어떻게 자신을 호출 할 수 있는지 이해하고 있습니다. 재귀 알고리즘은 때때로 사람들이 "희망적 사고에 의한 프로그래밍"이라고 부르는 것입니다. 문제를 해결하기 위해 따르는보다 자연스러운 "단계 목록"대신 "기본 사례 + 귀납적 사례"라는 관점에서 알고리즘을 생각하는 것은 매우 부자연 스럽습니다. " 재귀 알고리즘을보고있는 훈련받지 않은 학생에게는 알고리즘이 질문을하는 것처럼 보입니다 .

나는 또한 누군가에게 포인터 및 / 또는 재귀를 가르 칠 수 있다는 것을 완벽하게 기꺼이 받아 들일 것입니다 ... 나는 어떤 방법 으로든 증거가 없습니다. 나는 경험적으로이 두 개념을 실제로 이해할 수 있다는 것이 일반적인 프로그래밍 능력을 매우 잘 예측할 수 있으며, 학부 CS 연수 과정에서이 두 개념은 가장 큰 장애물 중 하나라는 것을 알고 있습니다.


4
""기본 사례 + 귀납적 사례 "의 관점에서 알고리즘을 생각하는 것은 매우 부자연 스럽습니다. 나는 그것이 부자연스럽지 않다고 생각합니다. 그것은 단지 아이들이 그에 따라 훈련되고 있지 않다는 것입니다.
Ingo

14
자연 스러우면 훈련을받을 필요가 없습니다. : P
Joel Spolsky

1
좋은 지적 :), 그러나 우리는 수학, 논리, 물리 등의 훈련이 필요하지는 않습니다. 흥미롭게도 언어 구문에 문제가있는 프로그래머는 거의 없지만 재귀로 가득합니다.
Ingo

1
우리 대학에서 첫 번째 과정은 거의 즉시 돌연변이 등을 도입하기 전에 기능 프로그래밍과 재귀로 시작했습니다. 나는 학생들의 일부 발견 없는 경험을 더 가진 것보다 재귀를 이해 몇 가지 경험. 즉, 수업의 최상위 는 많은 경험을 가진 사람들로 구성되었습니다 .
Tikhon Jelvis

2
포인터와 재귀를 이해하지 못하는 것은 a) 전반적인 IQ 수준과 b) 나쁜 수학 교육과 관련이 있다고 생각합니다.
quant_dev

23

재귀는 단순히 "자체를 호출하는 함수"가 아닙니다. 당신은 재귀 하강 파서에 무엇이 잘못되었는지 알아 내기 위해 스택 프레임을 그릴 때까지 왜 재귀가 어려운지를 알지 못할 것입니다. 종종 상호 재귀 함수 (함수 A는 함수 B를 호출하고 함수 C는 함수 A를 호출 할 수 있음)를 호출합니다. 상호 재귀적인 일련의 함수에서 N 스택 프레임의 깊이에있을 때 무엇이 ​​잘못되었는지 파악하기가 매우 어려울 수 있습니다.

포인터의 경우, 포인터의 개념 은 메모리 주소를 저장하는 변수 인 매우 간단합니다. 그러나 void**다른 노드를 가리키는 포인터 의 복잡한 데이터 구조에 문제가 발생 하면 포인터 중 하나가 가비지 주소를 가리키는 이유를 파악하기가 어려워 질 수 있습니다.


1
재귀 괜찮은 파서를 구현하는 것은 재귀에 다소 관심이 있다고 느꼈을 때였습니다. 당신이 말한 것처럼 포인터는 높은 수준에서 이해하기 쉽습니다. 포인터가 복잡한 이유를 알 수있는 포인터를 다루는 구현의 너트와 볼트에 들어갈 때까지는 아닙니다.
Chris

많은 함수 간의 상호 재귀는 기본적으로와 동일합니다 goto.
starblue

2
각 스택 프레임이 로컬 변수의 새 인스턴스를 생성하기 때문에 @starblue, 실제로는 아닙니다.
Charles Salvia

맞습니다. 꼬리 재귀 만과 같습니다 goto.
starblue

3
@wnoise int a() { return b(); }는 재귀 적 일 수 있지만의 정의에 따라 다릅니다 b. 그래서 그 것처럼 간단하지 않습니다 ...
대안

14

Java는 포인터 (참조라고 함)를 지원하고 재귀를 지원합니다. 표면에서 그의 주장은 무의미 해 보입니다.

그가 실제로 말하고있는 것은 디버깅하는 능력입니다. Java 포인터 (err, reference)는 유효한 객체를 가리 킵니다. AC 포인터가 아닙니다. 그리고 valgrind 와 같은 도구를 사용하지 않는다고 가정 할 때 C 프로그래밍의 요령은 포인터를 어디에 고정했는지 정확하게 찾는 것입니다 (스택 추적에서 거의 발견되지 않습니다).


5
포인터 자체가 세부 사항입니다. Java에서 참조를 사용하는 것은 C에서 로컬 변수를 사용하는 것보다 더 복잡하지 않습니다. Lisp 구현이 수행하는 방식으로 원자를 혼합하더라도 (원자는 제한된 크기의 정수이거나 문자 또는 포인터 일 수 있음) 어렵지 않습니다. 언어가 다른 종류의 구문으로 동일한 종류의 데이터를 로컬 또는 참조 할 수있게되면 더 어려워지고, 언어가 포인터 산술을 허용 할 때는 실제로 털이 많습니다.
David Thornley

@David-음, 이것이 내 응답과 어떤 관련이 있습니까?
Anon

1
Java 지원 포인터에 대한 의견.
David Thornley

"포인터를 조이는 곳 (스택 트레이스에서 발견되는 지점은 거의 없음)" 운이 좋으면 stacktrace를 얻을 수 있습니다.
오메가 센타 우리

5
나는 David Thornley에 동의합니다. Java는 포인터에 대한 포인터를 int에 대한 포인터로 만들 수 없으면 포인터를 지원하지 않습니다. 어쩌면 나는 각각 다른 것을 참조하는 4-5 클래스처럼 만들 수 있다고 생각하지만 실제로는 포인터입니까, 아니면 추악한 해결책입니까?
대안

12

포인터와 재귀의 문제는 반드시 이해하기 어렵다는 것이 아니라 특히 C 또는 C ++와 같은 언어와 관련하여 잘못 배운다는 것입니다 (주로 언어 자체가 잘못 배운 것이므로). 누군가가 "배열은 단지 포인터"라고들을 때마다 읽거나 읽을 때마다 조금씩 죽습니다.

마찬가지로, 누군가가 피보나치 기능을 사용하여 재귀를 설명 할 때마다 비명을 지르고 싶습니다. 반복 버전이 더 열심히 작성하는 것입니다 때문에 나쁜 예 없습니다 는 재귀 하나보다 적어도뿐만 아니라 또는 더 나은 수행하고 그것은 당신에게 재귀 솔루션을 유용하거나 바람직 할 이유에 대한 진정한 통찰력을 제공합니다. 퀵 정렬, 트리 순회 등은 멀리 있습니다. 은 재귀의 이유와 방법에 대한 좋은 예입니다.

포인터로 뭉쳐야한다는 것은 포인터를 노출시키는 프로그래밍 언어로 작업하는 결과입니다. Fortran 프로그래머 세대는 전용 포인터 유형 (또는 동적 메모리 할당)이 없어도 목록과 트리, 스택 및 대기열을 작성하고 있었으며 Fortran이 장난감 언어라고 비난 한 적이 없습니다.


나는 실제 포인터를보기 전에 몇 년 동안 Fortran을 가지고 있었기 때문에 언어 / 컴파일러가 나를 위해 할 수있는 기회를 갖기 전에 이미 같은 일을하는 내 자신의 방식을 사용하고 있음에 동의합니다. 또한 주소 / 주소에 저장된 값의 개념이 매우 간단하지만 포인터 / 주소와 관련된 C 구문이 매우 혼란 스럽다고 생각합니다.
Omega Centauri

Fortran IV에서 구현 된 Quicksort에 대한 링크가 있다면 그것을보고 싶습니다. 실제로 30 년 전에 BASIC으로 구현할 수는 없지만 말할 수는 없습니다.
Anon

나는 Fortran IV에서 일한 적이 없지만 Fortran 77의 VAX / VMS 구현에서 재귀 알고리즘을 구현했습니다 (goto의 대상을 특별한 종류의 변수로 저장할 수있는 후크가 있으므로 쓸 수 있습니다 GOTO target) . 그래도 우리는 우리 자신의 런타임 스택을 만들어야한다고 생각합니다. 오래전에 더 이상 세부 사항을 기억할 수 없었습니다.
John Bode

8

포인터에는 몇 가지 어려움이 있습니다.

  1. 별칭 다른 이름 / 변수를 사용하여 개체의 값을 변경할 가능성.
  2. 비 지역성 (local-locality ) 선언 된 것과 다른 컨텍스트에서 객체 값을 변경할 가능성 (참조에 의해 전달 된 인수에서도 발생)
  3. 수명 불일치 포인터의 수명은 가리키는 객체의 수명과 다를 수 있으며 이는 잘못된 참조 (SEGFAULTS) 또는 가비지로 이어질 수 있습니다.
  4. 포인터 산술 . 일부 프로그래밍 언어는 포인터를 정수로 조작 할 수있게 해주므로 포인터가 어디든지 가리킬 수 있습니다 (버그가있을 때 가장 예상치 못한 위치 포함). 포인터 산술을 올바르게 사용하려면 프로그래머가 가리키는 객체의 메모리 크기를 알고 있어야하며, 더 생각해야합니다.
  5. 유형 캐스트 포인터를 한 유형에서 다른 유형으로 캐스트 하는 기능을 사용하면 의도 한 것과 다른 객체의 메모리를 덮어 쓸 수 있습니다.

그렇기 때문에 프로그래머는 포인터를 사용할 때 더 철저하게 생각해야합니다 ( 두 가지 추상화 수준에 대해서는 모르겠습니다 ). 이것은 초보자가 저지른 일반적인 실수의 예입니다.

Pair* make_pair(int a, int b)
{
    Pair p;
    p.a = a;
    p.b = b;
    return &p;
}

위와 같은 코드는 포인터 개념이 아닌 함수 프로그래밍 언어와 같은 이름 (참조), 객체 및 값 중 하나와 가비지 수집 기능이있는 언어 (Java, Python)가있는 언어에서 완벽하게 합리적입니다. .

재귀 함수의 어려움은 충분한 수학적 배경이없는 사람들 (재귀가 일반적이며 필요한 지식이있는 사람들)이 함수가 이전에 호출 된 횟수에 따라 다르게 동작한다고 생각할 때 발생합니다 . 재귀 함수 는 실제로 이해 하는 방식 으로 생각해야하는 방식으로 생성 될 있기 때문에이 문제는 더욱 악화 됩니다.

데이터 구조가 내부에서 수정되는 Red-Black Tree 의 절차 적 구현과 같이 포인터가 전달되는 재귀 함수를 생각하십시오 . 기능적인 상대 보다 생각하기가 더 어렵다 .

이 질문에는 언급되어 있지 않지만 초보자가 어려움을 겪는 다른 중요한 문제는 동시성 입니다.

다른 사람들이 언급했듯이 일부 프로그래밍 언어 구문에는 개념이 아닌 추가적인 문제가 있습니다. 이해하더라도 이러한 구문에 대한 단순하고 정직한 실수는 디버깅하기가 매우 어려울 수 있습니다.


해당 함수를 사용하면 유효한 포인터가 반환되지만 변수가 함수를 호출 한 범위보다 높은 범위에 있으므로 malloc을 사용할 때 포인터가 무효화 될 수 있습니다.
rightfold

4
@ Radek S : 아뇨. 일부 환경 에서는 다른 곳이 덮어 쓸 때까지 작동 하는 잘못된 포인터를 반환 합니다. (실제로, 이것은 힙이 아닌 스택이 될 것입니다. malloc()다른 기능보다 더 이상 가능성이 없습니다.)
wnoise

1
@Radeck 샘플 함수에서 포인터는 함수가 반환되면 프로그래밍 언어 (이 경우 C)가 보장하는 메모리를 가리 킵니다. 따라서 반환 된 포인터는 garbage를 가리 킵니다 . 가비지 콜렉션이있는 언어는 오브젝트가 컨텍스트에서 참조되는 한 오브젝트를 활성 상태로 유지합니다.
Apalala

그런데 Rust에는 포인터가 있지만 이러한 문제는 없습니다. (안전하지 않은 상황에 있지 않을 때)
Sarge Borsch

2

포인터와 재귀는 두 개의 분리 된 짐승이며 각각 "어려운"것으로 간주되는 다른 이유가 있습니다.

일반적으로 포인터에는 순수한 변수 할당과 다른 정신 모델이 필요합니다. 포인터 변수가있을 때, 그것은 단지 다음과 같습니다. 다른 객체에 대한 포인터, 그것이 포함하는 유일한 데이터는 그것이 가리키는 메모리 주소입니다. 예를 들어 int32 포인터가 있고 직접 값을 할당하면 int 값을 변경하지 않고 새로운 메모리 주소를 가리키고 있습니다 (이 작업으로 할 수있는 많은 트릭이 있습니다) ). 더 흥미로운 것은 포인터에 대한 포인터를 갖는 것입니다 (C #에서 Ref 변수를 함수 매개 변수로 전달하면 발생합니다. 함수는 완전히 다른 개체를 매개 변수에 할당 할 수 있으며 해당 값은 여전히 ​​함수의 범위에 있습니다) 종료합니다.

재귀는 자체 학습 측면에서 함수를 정의하기 때문에 처음 학습 할 때 약간의 정신적 도약이 필요합니다. 처음 접했을 때 그것은 야생의 개념이지만 일단 아이디어를 이해하면 그것은 제 2의 본성이됩니다.

그러나 다시 주제로 돌아갑니다. Joel의 주장은 그 자체로 포인터 나 재귀에 관한 것이 아니라 컴퓨터가 실제로 작동하는 방식에서 학생들이 더 멀리 떨어져 있다는 사실입니다. 이것은 컴퓨터 과학의 과학입니다. 프로그램 학습과 프로그램 작동 방식에는 뚜렷한 차이가 있습니다. 많은 CS 프로그램이 영광스런 무역 학교가되고 있다고 주장하면서 "나는 이런 방식으로 배웠으므로 모두가 이런 식으로 배워야한다"는 문제가 아니라고 생각합니다.


1

나는 P. Brian에게 +1을 주었다. 왜냐하면 재귀는 약간의 어려움을 겪는 사람이 맥 도널드에서 일자리를 찾는 것을 더 잘 고려해야하는 근본적인 개념이지만, 심지어 재귀도있다.

make a burger:
   put a cold burger on the grill
   wait
   flip
   wait
   hand the fried burger over to the service personel
   unless its end of shift: make a burger

물론 이해력 부족은 학교와도 관련이 있습니다. 여기서 Peano, Dedekind 및 Frege와 같은 자연수를 소개해야하므로 나중에 많은 어려움을 겪지 않을 것입니다.


6
그것은 꼬리 회귀이며, 아마도 반복적입니다.
Michael K

6
죄송합니다, 루핑은 아마도 꼬리 재귀입니다 :)
Ingo

3
@Ingo : :) 기능적인 광신자!
Michael K

1
@Michael-사실 그는, 재귀가 더 근본적인 개념이라고 주장 할 수 있다고 생각합니다.
Ingo

@ Ingo : 실제로 할 수 있습니다 (예는 잘 보여줍니다). 그러나 어떤 이유로 인간은 프로그래밍에서 어려움을 goto top겪습니다. 우리는 어떤 이유로 IME 를 추가로 원하는 것 같습니다 .
Michael K

1

나는 문제가 여러 단계의 추상화에서 생각하는 것이라고 Joel에 동의하지 않는다. 나는 포인터와 재귀가 사람들이 프로그램 작동 방식에 대한 정신 모델의 변화를 요구하는 두 가지 좋은 예라고 생각한다.

포인터는 설명하기 가장 간단한 경우라고 생각합니다. 포인터를 다루려면 프로그램이 실제로 메모리 주소와 데이터를 처리하는 방식을 설명하는 프로그램 실행의 정신 모델이 필요합니다. 필자의 경험은 종종 프로그래머가 포인터에 대해 배우기 전에 이것에 대해 생각조차하지 않는 경우가 많았습니다. 비록 그들이 추상적 인 의미로 알고 있다고해도, 프로그램의 작동 방식에 대한인지 모델에 그것을 채택하지 않았습니다. 포인터가 도입되면 코드 작동 방식에 대한 기본적인 방식의 전환이 필요합니다.

재귀는 이해하기에 두 개의 개념적 블록이 있기 때문에 문제가됩니다. 첫 번째는 머신 레벨에 있으며, 포인터가 프로그램과 실제로 어떻게 저장되고 실행되는지를 잘 이해함으로써 극복 할 수 있습니다. 재귀의 또 다른 문제는 사람들이 재귀 문제를 비 재귀 문제로 해체하려고 시도하는 자연스러운 경향이 있다는 것입니다. 재귀 함수는 재게 기능으로서 이해합니다. 이것은 수학적 배경이 불충분 한 사람들이나 수학적 이론을 프로그램 개발과 연관시키지 않는 정신 모델에 문제가 있습니다.

문제는 포인터와 재귀가 불충분 한 정신 모델에 갇힌 사람들에게 문제가되는 유일한 두 영역이라고 생각하지 않습니다. 병렬 처리는 일부 사람들이 단순히 갇히고 정신 모델을 설명하는 데 어려움을 겪는 또 다른 영역 인 것처럼 보입니다. 면접에서 포인터와 재귀를 테스트하기 쉬운 경우가 종종 있습니다.


1
  DATA    |     CODE
          |
 pointer  |   recursion    SELF REFERENTIAL
----------+---------------------------------
 objects  |   macro        SELF MODIFYING
          |
          |

자기 참조 데이터와 코드의 개념은 각각 포인터와 재귀의 정의에 기초합니다. 불행히도, 명령형 프로그래밍 언어에 대한 광범위한 노출로 인해 컴퓨터 과학 학생들은 언어의 기능적 측면에 대한이 신비를 신뢰해야 할 때 런타임의 작동 동작을 통해 구현을 이해해야한다고 믿게되었습니다. 모든 수를 100까지 합산하면 하나에서 시작하여 순서대로 다음에 추가하고 순환 자기 참조 기능을 사용하여 거꾸로하는 것은 간단한 것으로 보입니다. 순수한 기능.

자체 수정 데이터 및 코드의 개념은 각각 객체 (예 : 스마트 데이터) 및 매크로의 정의에 기초합니다. 나는 포인터의 트리를 사용하여 재귀 적 괜찮은 파서를 구현하는 객체 세트를 생성하는 매크로와 같은 네 가지 개념의 조합에서 런타임에 대한 운영상의 이해가 예상되는 경우 특히 이해하기가 더 어려워 언급합니다. . 명령형 프로그래머는 한 번에 모든 추상화 계층을 통해 단계별로 프로그램 상태의 전체 작동을 추적하는 대신 변수가 순수한 함수 내에서 한 번만 할당되고 동일한 순수한 함수를 반복적으로 호출한다는 것을 알아야합니다. Java와 같이 불순한 기능을 지원하는 언어에서도 동일한 인수를 사용하면 항상 동일한 결과 (참조 투명도)를 얻을 수 있습니다. 실행 후 서클에서 돌아 다니는 것은 무의미한 노력입니다. 추상화가 단순화되어야합니다.


-1

Anon의 답변과 매우 유사합니다.
초보자에게는인지 장애 이외에도 포인터와 재귀는 매우 강력하며 암호 방식으로 사용할 수 있습니다.

큰 힘의 단점은, 당신에게 미묘한 방법으로 프로그램을 망치는 큰 힘을 준다는 것입니다.
가짜 값을 일반 변수에 저장하는 것은 충분하지 않지만 포인터에 가짜를 저장하면 모든 종류의 지연된 치명적인 일이 발생할 수 있습니다.
더 나쁜 것은 기괴한 프로그램 동작의 원인을 진단 / 디버그하려고 할 때 이러한 효과가 변경 될 수 있습니다. 그러나 어떤 일이 미묘하게 잘못되면 어떤 일이 일어나고 있는지 파악하기가 어려울 수 있습니다.

재귀와 비슷합니다. 까다로운 데이터 구조 (스택)에 까다로운 것을 채워서 까다로운 것을 구성하는 매우 강력한 방법이 될 수 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.