C 나 C ++에있는 많은 새롭고 오래된 대학 수준의 학생들에게 포인터가 왜 혼란의 주요 요인입니까? 변수, 함수 및 수준을 넘어서 포인터가 작동하는 방식을 이해하는 데 도움이되는 도구 나 사고 과정이 있습니까?
전체 개념에 얽매이지 않고 누군가를 "아하, 알았다"수준으로 끌어 올리기 위해 할 수있는 좋은 방법은 무엇입니까? 기본적으로 유사한 시나리오를 드릴하십시오.
C 나 C ++에있는 많은 새롭고 오래된 대학 수준의 학생들에게 포인터가 왜 혼란의 주요 요인입니까? 변수, 함수 및 수준을 넘어서 포인터가 작동하는 방식을 이해하는 데 도움이되는 도구 나 사고 과정이 있습니까?
전체 개념에 얽매이지 않고 누군가를 "아하, 알았다"수준으로 끌어 올리기 위해 할 수있는 좋은 방법은 무엇입니까? 기본적으로 유사한 시나리오를 드릴하십시오.
답변:
포인터는 많은 사람들이 특히 포인터 값을 복사하고 여전히 동일한 메모리 블록을 참조 할 때 혼동 될 수 있다는 개념입니다.
가장 좋은 비유는 포인터를 집 주소가있는 종이 조각으로 간주하고 실제 블록으로 참조하는 메모리 블록을 고려하는 것입니다. 따라서 모든 종류의 작업을 쉽게 설명 할 수 있습니다.
아래에 Delphi 코드를 추가하고 적절한 경우 주석을 추가했습니다. 다른 주요 프로그래밍 언어 인 C #은 동일한 방식으로 메모리 누수와 같은 것을 나타내지 않기 때문에 Delphi를 선택했습니다.
높은 수준의 포인터 개념 만 배우려면 아래 설명에서 "메모리 레이아웃"이라고 표시된 부분을 무시해야합니다. 그것들은 작업 후 메모리가 어떻게 보일 수 있는지에 대한 예를 제공하기 위해 의도되었지만 더 낮은 수준입니다. 그러나 버퍼 오버런의 실제 작동 방식을 정확하게 설명하기 위해이 다이어그램을 추가하는 것이 중요했습니다.
면책 조항 : 모든 의도와 목적을 위해이 설명과 예제 메모리 레이아웃은 크게 단순화되었습니다. 낮은 수준의 메모리를 처리해야하는 경우 알아야 할 오버 헤드와 세부 정보가 더 많습니다. 그러나 메모리와 포인터를 설명하기 위해 충분히 정확합니다.
아래에 사용 된 THouse 클래스가 다음과 같다고 가정 해 봅시다.
type
THouse = class
private
FName : array[0..9] of Char;
public
constructor Create(name: PChar);
end;
하우스 오브젝트를 초기화하면 생성자에 지정된 이름이 개인 필드 FName에 복사됩니다. 고정 크기 배열로 정의 된 이유가 있습니다.
메모리에서는 하우스 할당과 관련된 약간의 오버 헤드가있을 것입니다.이를 아래와 같이 설명하겠습니다.
--- [ttttNNNNNNNNNN] --- ^ ^ | | | +-FName 배열 | +-오버 헤드
"tttt"영역은 오버 헤드이며, 일반적으로 8 또는 12 바이트와 같은 다양한 유형의 런타임 및 언어에 대해 더 많은 부분이 있습니다. 이 영역에 저장된 값이 메모리 할당 자나 코어 시스템 루틴 이외의 다른 것으로 변경되지 않아야합니다. 그렇지 않으면 프로그램이 충돌 할 위험이 있습니다.
메모리 할당
기업가에게 집을 짓고 집 주소를 알려주십시오. 실제와 달리 메모리 할당은 어디에 할당해야하는지 알 수 없지만 충분한 공간이있는 적절한 지점을 찾고 할당 된 메모리에 주소를 다시보고합니다.
다시 말해, 기업가는 그 자리를 선택할 것입니다.
THouse.Create('My house');
메모리 레이아웃 :
--- [ttttNNNNNNNNNN] --- 1234 우리집
주소로 변수를 유지
새 집 주소를 종이에 적는다. 이 문서는 집을 참조 할 것입니다. 이 종이가 없으면 잃어 버렸으며 이미 집에 있지 않는 한 집을 찾을 수 없습니다.
var
h: THouse;
begin
h := THouse.Create('My house');
...
메모리 레이아웃 :
h V --- [ttttNNNNNNNNNN] --- 1234 우리집
포인터 값 복사
새 종이에 주소를 쓰십시오. 이제 두 개의 별도 용지가 아닌 동일한 집으로가는 두 개의 종이가 있습니다. 한 종이의 주소를 따르고 그 집의 가구를 재 배열하려고 하면 다른 집도 같은 방식으로 수정 된 것처럼 보이지만 실제로는 한 집이라는 것을 명백하게 감지 할 수 없다면 말입니다.
참고 이것은 일반적으로 사람들에게 설명하는 데 가장 문제가 많은 개념이며, 두 개의 포인터가 두 개의 객체 또는 메모리 블록을 의미하지는 않습니다.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1 V --- [ttttNNNNNNNNNN] --- 1234 우리집 ^ h2
메모리 확보
집을 철거하십시오. 그런 다음 나중에 원할 경우 신문을 새 주소로 재사용하거나 더 이상 존재하지 않는 집 주소를 잊어 버리도록 지울 수 있습니다.
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
h := nil;
여기에서 먼저 집을 짓고 주소를 붙잡습니다. 그런 다음 집에 뭔가를하고 (독자에게 연습으로 남겨둔 ... 코드를 사용하십시오) 그런 다음 무료로 사용하십시오. 마지막으로 변수에서 주소를 지 웁니다.
메모리 레이아웃 :
h <-+ v +-무료 전 --- [ttttNNNNNNNNNN] --- | 1234 내 집 <-+ h (지금은 아무데도) <-+ +-무료 후 ---------------------- | (메모리는 여전히 xx34 내 집 <-+에 데이터가 들어 있습니다)
매달려있는 포인터
당신은 기업가에게 집을 파괴하라고 지시하지만, 종이에서 주소를 지우는 것을 잊어 버립니다. 나중에 종이 조각을 보면 집이 더 이상 존재하지 않으며 방문하지 않고 결과가 실패한다는 것을 잊었습니다 (아래 잘못된 참조에 대한 부분도 참조하십시오).
var
h: THouse;
begin
h := THouse.Create('My house');
...
h.Free;
... // forgot to clear h here
h.OpenFrontDoor; // will most likely fail
h
부름을받은 후에 사용하면 효과 가 .Free
있을 수 있지만 그것은 단지 행운입니다. 중요한 작업 중에는 고객 위치에서 실패 할 가능성이 높습니다.
h <-+ v +-무료 전 --- [ttttNNNNNNNNNN] --- | 1234 내 집 <-+ h <-+ 무료 후 v +- ---------------------- | xx34 나의 집 <-+
보시다시피 h는 여전히 메모리에있는 데이터의 나머지를 가리 킵니다. 그러나 데이터가 완전하지 않기 때문에 이전과 같이 사용하지 못할 수 있습니다.
메모리 누수
종이 한 장을 잃어 집을 찾을 수 없습니다. 집은 여전히 어딘가에 서 있으며 나중에 새 집을 짓고 싶을 때 그 자리를 재사용 할 수 없습니다.
var
h: THouse;
begin
h := THouse.Create('My house');
h := THouse.Create('My house'); // uh-oh, what happened to our first house?
...
h.Free;
h := nil;
여기서 우리 h
는 새 집의 주소로 변수 의 내용을 덮어 썼지 만 오래된 집은 여전히 어딘가에 서 있습니다. 이 코드가 끝나면 그 집에 갈 수있는 방법이 없으며, 그대로 남아있을 것입니다. 다시 말해, 할당 된 메모리는 응용 프로그램이 닫힐 때까지 할당 된 상태를 유지하며,이 시점에서 운영 체제가이를 해제합니다.
첫 번째 할당 후 메모리 레이아웃 :
h V --- [ttttNNNNNNNNNN] --- 1234 우리집
두 번째 할당 후 메모리 레이아웃 :
h V --- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN] 1234 나의 집 5678 나의 집
이 방법을 얻는 더 일반적인 방법은 위와 같이 덮어 쓰지 않고 해제하는 것을 잊어 버리는 것입니다. 델파이 용어로 이것은 다음과 같은 방법으로 발생합니다.
procedure OpenTheFrontDoorOfANewHouse;
var
h: THouse;
begin
h := THouse.Create('My house');
h.OpenFrontDoor;
// uh-oh, no .Free here, where does the address go?
end;
이 방법을 실행 한 후에는 변수에 집 주소가 존재하지만 집은 여전히 존재하지 않습니다.
메모리 레이아웃 :
h <-+ 포인터를 잃기 전에 v +- --- [ttttNNNNNNNNNN] --- | 1234 내 집 <-+ h (지금은 아무데도) <-+ +-포인터를 잃은 후 --- [ttttNNNNNNNNNN] --- | 1234 내 집 <-+
보다시피, 이전 데이터는 메모리에 그대로 남아 있으며 메모리 할당 자에 의해 재사용되지 않습니다. 할당자는 사용 된 메모리 영역을 추적하며 해제하지 않으면 재사용하지 않습니다.
메모리를 비우지 만 (현재 유효하지 않은) 참조는 유지
집을 철거하고, 종이 한 장을 지울 수 있지만, 오래된 주소를 가진 또 다른 종이가 있습니다. 주소로 갈 때 집을 찾을 수는 없지만 폐허와 비슷한 것을 찾을 수 있습니다 하나의.
아마도 당신은 심지어 집을 찾을 것입니다, 그러나 그것은 당신이 원래 주소를 받았던 집이 아니기 때문에, 당신이 속한 것처럼 그것을 사용하려는 시도는 끔찍하게 실패 할 수 있습니다.
때로는 이웃 주소에 3 개의 주소 (메인 스트리트 1 ~ 3)를 차지하는 다소 큰 집이 설정되어 있고 주소가 집 가운데로 이동하는 경우도 있습니다. 큰 3 주소 집의 일부를 하나의 작은 집으로 취급하려는 시도도 끔찍하게 실패 할 수 있습니다.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := h1; // copies the address, not the house
...
h1.Free;
h1 := nil;
h2.OpenFrontDoor; // uh-oh, what happened to our house?
여기에 집에서 참조를 통해, 철거되었다 h1
,하는 동안 h1
뿐만 아니라 지워졌습니다, h2
여전히 이전, 아웃 오브 날짜, 주소가 있습니다. 더 이상 서 있지 않은 집에 대한 접근은 작동하지 않거나 작동하지 않을 수 있습니다.
위의 매달려있는 포인터의 변형입니다. 메모리 레이아웃을 참조하십시오.
버퍼 오버런
당신은 이웃 집이나 마당에 쏟아져 들어갈 수있는 것보다 더 많은 물건을 집으로 옮깁니다. 그 이웃 집의 소유자가 나중에 집으로 돌아 오면, 자신이 고려할 모든 종류의 물건을 찾을 수 있습니다.
이것이 내가 고정 크기 배열을 선택한 이유입니다. 무대를 설정하기 위해, 우리가 할당 한 두 번째 집은 어떤 이유로 메모리의 첫 번째 집 앞에 위치한다고 가정하십시오. 즉, 두 번째 집은 첫 번째 집보다 주소가 낮습니다. 또한 서로 바로 옆에 할당됩니다.
따라서이 코드는 다음과 같습니다.
var
h1, h2: THouse;
begin
h1 := THouse.Create('My house');
h2 := THouse.Create('My other house somewhere');
^-----------------------^
longer than 10 characters
0123456789 <-- 10 characters
첫 번째 할당 후 메모리 레이아웃 :
h1 V ----------------------- [ttttNNNNNNNNNN] 내 집
두 번째 할당 후 메모리 레이아웃 :
h2 h1 vv --- [ttttNNNNNNNNNN] ---- [ttttNNNNNNNNNN] 1234 어딘가에있는 다른 집 ^ --- +-^ | +-덮어 쓰기
가장 자주 충돌을 일으키는 부분은 저장된 데이터의 중요한 부분을 덮어 쓰는 경우 실제로 임의로 변경해서는 안됩니다. 예를 들어, 프로그램 충돌과 관련하여 h1 하우스 이름의 일부가 변경되었지만 문제가되지는 않지만 깨진 개체를 사용하려고 할 때 개체의 오버 헤드를 덮어 쓰면 충돌이 발생할 가능성이 높습니다. 객체의 다른 객체에 저장된 링크를 덮어 씁니다.
연결된 목록
종이 한 장의 주소를 따라 가면 집에 도착하고 그 집에는 체인의 다음 집 등을 위해 새 주소를 가진 또 다른 종이가 있습니다.
var
h1, h2: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
여기서 우리는 우리 집과 오두막으로 연결되는 링크를 만듭니다. 우리는 집이 NextHouse
참조 가 없을 때까지 체인을 따라갈 수 있습니다 . 모든 집을 방문하기 위해 다음 코드를 사용할 수 있습니다.
var
h1, h2: THouse;
h: THouse;
begin
h1 := THouse.Create('Home');
h2 := THouse.Create('Cabin');
h1.NextHouse := h2;
...
h := h1;
while h <> nil do
begin
h.LockAllDoors;
h.CloseAllWindows;
h := h.NextHouse;
end;
메모리 레이아웃 (다음 다이어그램에서 객체에 링크로 NextHouse 추가, 아래 다이어그램에서 4 개의 LLLL로 표시됨) :
h1 h2 vv --- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL] 1234 홈 + 5678 캐빈 + | ^ | + -------- + * (링크 없음)
기본적으로 메모리 주소는 무엇입니까?
메모리 주소는 기본적으로 숫자입니다. 메모리를 큰 바이트 배열로 생각하면 첫 번째 바이트의 주소는 0이고 다음 바이트의 주소는 1입니다. 이것은 간단하지만 충분합니다.
따라서이 메모리 레이아웃 :
h1 h2 vv --- [ttttNNNNNNNNNN] --- [ttttNNNNNNNNNN] 1234 나의 집 5678 나의 집
다음 두 주소를 가질 수 있습니다 (가장 왼쪽-주소 0).
위의 링크 된 목록이 실제로 다음과 같이 보일 수 있습니다.
h1 (= 4) h2 (= 28) vv --- [ttttNNNNNNNNNNLLLL] ---- [ttttNNNNNNNNNNLLLL] 1234 홈 0028 5678 카빈 0000 | ^ | + -------- + * (링크 없음)
일반적으로 주소가 0 인 주소를 주소 0으로 저장하는 것이 일반적입니다.
기본적으로 포인터 란 무엇입니까?
포인터는 메모리 주소를 보유한 변수 일뿐입니다. 일반적으로 프로그래밍 언어에 번호를 제공하도록 요청할 수 있지만 대부분의 프로그래밍 언어 및 런타임은 숫자 자체가 실제로 의미를 갖지 않기 때문에 아래에 숫자가 있다는 사실을 숨기려고합니다. 포인터를 블랙 박스로 생각하는 것이 가장 좋습니다. 실제로 작동하는 한 실제로 구현되는 방법을 알거나 신경 쓰지 않습니다.
첫 번째 Comp Sci 수업에서는 다음과 같은 연습을했습니다. 물론, 여기에는 약 200 명의 학생이있는 강의실이 있습니다.
교수는 칠판에 다음과 같이 씁니다. int john;
존이 일어
교수는 다음과 같이 썼다. int *sally = &john;
샐리는 일어 서서 요한을 가리킨다
교수: int *bill = sally;
빌이 일어서 요
교수: int sam;
샘은 일어
교수: bill = &sam;
Bill은 이제 Sam을 가리 킵니다.
나는 당신이 아이디어를 얻는다고 생각합니다. 포인터 할당의 기본 사항을 살펴볼 때까지 한 시간 정도 걸렸다 고 생각합니다.
포인터를 설명하는 데 도움이되는 유추는 하이퍼 링크입니다. 대부분의 사람들은 웹 페이지의 링크가 인터넷의 다른 페이지를 가리킴을 이해하고 해당 하이퍼 링크를 복사하여 붙여 넣을 수 있으면 둘 다 동일한 원본 웹 페이지를 가리 킵니다. 원래 페이지로 이동하여 편집 한 경우 해당 링크 (포인터) 중 하나를 따라 가면 새 업데이트 페이지가 표시됩니다.
int *a = b
는 않습니다 (두 개의 복사본을 만들지 않는 것처럼 *b
).
포인터가 너무 많은 사람들을 혼동하는 것처럼 보이는 이유는 대부분 컴퓨터 아키텍처에 대한 배경 지식이 거의 없거나 전혀 없기 때문입니다. 많은 사람들이 컴퓨터 (기계)가 실제로 어떻게 구현되는지에 대한 아이디어를 가지고 있지 않기 때문에 C / C ++에서 작업하는 것은 외계인처럼 보입니다.
포인터 조작 (로드, 저장, 직접 / 간접 주소 지정)에 중점을 둔 명령 세트를 사용하여 간단한 바이트 코드 기반 가상 머신 (선택한 언어로 파이썬이 가장 효과적 임)을 구현하도록 요청하는 것이 좋습니다. 그런 다음 해당 명령 세트에 대한 간단한 프로그램을 작성하도록 요청하십시오.
단순한 추가보다 약간 더 필요한 것은 포인터를 포함 할 것이고 그것을 얻을 것이라고 확신합니다.
C / C ++ 언어를 사용하는 새롭고 나이 많은 대학 수준의 학생들에게 포인터가 왜 혼란의 주요 요인입니까?
가치에 대한 자리 표시 자의 개념-변수-은 학교에서 우리가 가르치는 무언가-대수에 매핑됩니다. 컴퓨터 내에서 실제로 메모리가 배치되는 방식을 이해하지 않고 그릴 수있는 기존의 병렬 요소는 없으며 C / C ++ / 바이트 통신 수준에서 낮은 수준의 문제를 처리 할 때까지는 이런 종류의 것을 생각하지 않습니다. .
변수, 함수 및 수준을 넘어서 포인터가 작동하는 방식을 이해하는 데 도움이되는 도구 나 사고 과정이 있습니까?
주소 상자. BASIC을 마이크로 컴퓨터로 프로그래밍하는 법을 배웠을 때, 게임에 담긴이 예쁜 책들이 있었으며 때로는 특정 주소에 가치를 쏟아야했습니다. 그들은 0, 1, 2로 점진적으로 레이블이 붙은 많은 상자 그림을 가지고 있었으며 작은 상자 (1 바이트)만이 상자에 들어갈 수 있으며 많은 컴퓨터가 있다고 설명했습니다. 일부 컴퓨터 65535만큼이나 있었다! 그들은 서로 옆에 있었고 모두 주소를 가지고있었습니다.
전체 개념에 얽매이지 않고 누군가를 "아하, 알았다"수준으로 끌어 올리기 위해 할 수있는 좋은 방법은 무엇입니까? 기본적으로 유사한 시나리오를 드릴하십시오.
훈련? 구조체 만들기 :
struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';
char* my_pointer;
my_pointer = &mystruct.b;
cout << 'Start: my_pointer = ' << *my_pointer << endl;
my_pointer++;
cout << 'After: my_pointer = ' << *my_pointer << endl;
my_pointer = &mystruct.a;
cout << 'Then: my_pointer = ' << *my_pointer << endl;
my_pointer = my_pointer + 3;
cout << 'End: my_pointer = ' << *my_pointer << endl;
C를 제외하고 위와 동일한 예 :
// Same example as above, except in C:
struct {
char a;
char b;
char c;
char d;
} mystruct;
mystruct.a = 'r';
mystruct.b = 's';
mystruct.c = 't';
mystruct.d = 'u';
char* my_pointer;
my_pointer = &mystruct.b;
printf("Start: my_pointer = %c\n", *my_pointer);
my_pointer++;
printf("After: my_pointer = %c\n", *my_pointer);
my_pointer = &mystruct.a;
printf("Then: my_pointer = %c\n", *my_pointer);
my_pointer = my_pointer + 3;
printf("End: my_pointer = %c\n", *my_pointer);
산출:
Start: my_pointer = s
After: my_pointer = t
Then: my_pointer = r
End: my_pointer = u
아마도 그것은 예제를 통해 몇 가지 기본 사항을 설명합니까?
처음에 포인터를 이해하는 데 어려움을 겪었던 이유는 많은 설명에 참조로 전달하는 것에 대한 많은 쓰레기가 포함되어 있기 때문입니다. 이 모든 것이 문제를 혼동하기 만합니다. 포인터 매개 변수를 사용하면 여전히 값으로 전달됩니다. 그러나 값은 int가 아닌 주소입니다.
다른 사람이 이미이 자습서에 연결했지만 포인터를 이해하기 시작한 순간을 강조 할 수 있습니다.
C의 포인터와 배열에 대한 튜토리얼 : 3 장-포인터와 문자열
int puts(const char *s);
현재
const.
전달 된 매개 변수puts()
는 포인터입니다. 포인터 값은 포인터의 값입니다 (C의 모든 매개 변수는 값으로 전달되므로). 포인터의 값은 포인터가 가리키는 주소입니다. , 주소. 따라서puts(strA);
우리가 본 것처럼 쓰면 strA [0]의 주소를 전달하게됩니다.
이 단어를 읽는 순간 구름이 갈라지고 햇빛 광선이 포인터를 이해하게했습니다.
VB .NET 또는 C # 개발자이고 (현재 그대로) 안전하지 않은 코드를 사용하지 않더라도 포인터의 작동 방식을 이해하는 것이 가치가 있거나 객체 참조의 작동 방식을 이해하지 못합니다. 그런 다음 객체 참조를 메서드에 전달하면 객체가 복사된다는 일반적인 실수가 발생합니다.
Ted Jensen의 "C의 포인터 및 배열에 대한 자습서"는 포인터에 대한 학습을위한 훌륭한 리소스입니다. 그것은 포인터가 무엇인지 (그리고 무엇을 위해 무엇인지) 설명하고 함수 포인터로 마무리하는 10 개의 수업으로 나뉩니다. http://home.netcom.com/~tjensen/ptr/cpoint.htm
거기에서 Beej 's Guide to Network Programming은 유닉스 소켓 API를 가르치며, 여기에서 정말 재미있는 일을 시작할 수 있습니다. http://beej.us/guide/bgnet/
포인터의 복잡성은 우리가 쉽게 가르 칠 수있는 것 이상입니다. 학생들이 서로를 가리 키도록하고 집 주소와 함께 종이 조각을 사용하는 것은 훌륭한 학습 도구입니다. 그들은 기본 개념을 소개하는 훌륭한 일을합니다. 실제로 기본 개념을 배우는 것은 포인터를 성공적으로 사용 중요 합니다. 그러나 프로덕션 코드에서는 이러한 간단한 데모가 캡슐화 할 수있는 것보다 훨씬 더 복잡한 시나리오를 사용하는 것이 일반적입니다.
다른 구조물을 가리키는 구조물이 다른 구조물을 가리키는 시스템에 관여했습니다. 이러한 구조 중 일부에는 추가 구조에 대한 포인터가 아니라 내장 구조가 포함되어 있습니다. 이것은 포인터가 실제로 혼란스러워하는 곳입니다. 여러 수준의 간접 참조가 있고 다음과 같은 코드로 끝나기 시작합니다.
widget->wazzle.fizzle = fazzle.foozle->wazzle;
정말 빨리 혼란 스러울 수 있습니다 (더 많은 줄과 잠재적으로 더 많은 레벨을 상상하십시오). 포인터 배열과 노드 대 노드 포인터 (트리, 링크 된 목록)를 던져 넣으면 여전히 나빠집니다. 나는 그러한 시스템에서 일하기 시작하면 정말 좋은 개발자들이 길을 잃는 것을 보았습니다.
포인터의 복잡한 구조는 코딩이 제대로 표시되지 않을 수도 있습니다. 컴포지션은 훌륭한 객체 지향 프로그래밍의 중요한 부분이며, 원시 포인터가있는 언어에서는 필연적으로 다층 간접 처리로 이어질 것입니다. 또한 시스템은 종종 스타일이나 기법이 서로 일치하지 않는 구조의 타사 라이브러리를 사용해야합니다. 그러한 상황에서는 복잡성이 자연스럽게 발생할 것입니다 (물론 우리는 가능한 한 많이 싸워야합니다).
학생들이 포인터를 배우도록 돕기 위해 대학이 할 수있는 최선의 방법은 포인터 사용이 필요한 프로젝트와 함께 훌륭한 데모를 사용하는 것입니다. 하나의 어려운 프로젝트는 수천 개의 데모보다 포인터 이해에 더 많은 역할을합니다. 데모를 통해 이해를 깊게 할 수 있지만 포인터를 깊이 이해하려면 실제로 사용해야합니다.
나는이 목록에 비유를 추가하여 컴퓨터 과학 교사로서 포인터를 설명 할 때 매우 유용하다고 생각했다. 먼저 :
무대를 설정 :
3 개의 공간이있는 주차장을 고려하십시오.이 공간에는 번호가 매겨집니다.
-------------------
| | | |
| 1 | 2 | 3 |
| | | |
어떤 방식으로, 이것은 메모리 위치와 같으며 순차적이고 연속적입니다. 일종의 배열과 같습니다. 지금은 자동차가 없으므로 빈 배열과 같습니다 ( parking_lot[3] = {0}
).
데이터 추가
주차장은 오랫동안 빈 상태로 유지되지 않습니다. 만약 그렇게한다면 아무 의미가없고 아무도 만들지 않을 것입니다. 하루가 이동함에 따라 3 대의 자동차, 파란 차, 빨간 차 및 녹색 차가 가득 차 있다고 가정 해 봅시다.
1 2 3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |
이 차는 모든 생각하는 한 가지 방법은 우리의 자동차 데이터의 어떤 종류 (AN 말 것을 그래서 동일한 유형 (자동차)입니다 int
)하지만 그들은 서로 다른 값이 ( blue
, red
, green
, 색이 될 수 있었다 enum
)
포인터를 입력
이 주차장에 당신을 데려 가서 파란 차를 찾도록 요청하면, 손가락 하나를 뻗어 그 자리에서 파란 차를 가리 키도록 사용하십시오. 이것은 포인터를 가져다가 메모리 주소에 할당하는 것과 같습니다. (int *finger = parking_lot
)
당신의 손가락 (포인터)은 내 질문에 대한 답이 아닙니다. 상대 에 손가락은 나에게 아무것도 알 수 없지만, 난 당신이있는 거 손가락이 위치를 보면 가리키는 (포인터를 역 참조), 내가 찾고 있던 차 (데이터를) 찾을 수 있습니다.
포인터 재 지정
이제 빨간 차를 찾아달라고 요청할 수 있으며 손가락을 새 차로 리디렉션 할 수 있습니다. 이제 포인터 (이전과 동일한 것)가 동일한 유형 (차)의 새로운 데이터 (빨간 차를 찾을 수있는 주차 장소)를 보여줍니다.
포인터는 물리적으로는 여전히 변경하지 않은 당신의 손가락, 그것은 나를 변화 보여주는 된 데이터 만. ( 「주차장」주소)
이중 포인터 (또는 포인터에 대한 포인터)
이것은 둘 이상의 포인터에서도 작동합니다. 빨간 차를 가리키는 포인터가 어디에 있는지 물어볼 수 있으며 다른 손을 사용하고 손가락으로 첫 번째 손가락을 가리킬 수 있습니다. (이것은int **finger_two = &finger
)
이제 파란 차가 어디에 있는지 알고 싶다면 첫 번째 손가락의 방향을 따라 두 번째 손가락, 차 (데이터)를 따라갈 수 있습니다.
매달려있는 포인터
이제 동상과 매우 흡사하다고 말하고 빨간 차를 무기한으로 향하는 손을 잡으려고합니다. 그 빨간 차가 도망 가면 어떨까요?
1 2 3
-------------------
| o=o | | o=o |
| |B| | | |G| |
| o-o | | o-o |
포인터는 여전히 빨간 차 어디를 가리키는 없었다 하지만 더 이상. 새 차가 들어 오면 ... 오렌지 차가 있다고합시다. "빨간 차는 어디에 있습니까?"라는 질문을 다시해도 여전히 거기에있는 것입니다. 그것은 빨간 차가 아니라 주황색입니다.
포인터 산술
그래, 여전히 두 번째 주차 지점을 가리키고 있습니다 (오렌지 자동차가 점령)
1 2 3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |
글쎄요, 지금 새로운 질문이 있습니다 ... 다음 주차 장소 에서 차의 색을 알고 싶습니다 . 2 지점을 가리키고 있음을 알 수 있으므로 1을 추가하고 다음 지점을 가리키고 있습니다. ( finger+1
), 이제 데이터가 무엇인지 알고 싶었으므로 손가락이 아닌 해당 지점을 확인해야 포인터 ( *(finger+1)
)를 연기하여 녹색 자동차가 있는지 확인할 수 있습니다 (해당 위치의 데이터) )
"without getting them bogged down in the overall concept"
높은 수준의 이해로 질문을 읽습니다 . 그리고 당신의 요점 : "I'm not sure that people have any difficulty understanding pointers at the high level of abstraction"
-당신은 이 수준까지 포인터를 이해 하지 못하는 사람들 이 얼마나 놀랐는지 – 놀라서
좋은 다이어그램 세트가있는 튜토리얼의 예는 포인터를 이해하는 데 크게 도움이됩니다 .
Joel Spolsky는 게릴라 인터뷰 가이드 기사 에서 포인터 이해에 대해 다음과 같이 좋은 점을 지적합니다 .
어떤 이유로 든 대부분의 사람들은 포인터를 이해하는 두뇌의 일부없이 태어난 것처럼 보입니다. 이것은 기술적 인 것이 아니라 적성적인 것입니다. 어떤 사람들은 할 수 없다는 복잡한 형태의 이중 간접 사고가 필요합니다.
포인터의 문제는 개념이 아닙니다. 실행과 관련된 언어입니다. 교사들이 전문 용어가 아니라 어려운 포인터의 개념이라고 생각하거나 복잡한 C 및 C ++가 개념을 만든다고 가정 할 때 혼란이 추가로 발생합니다. 개념을 설명하는 데 많은 노력이 필요하지 않습니다 (이 질문에 대한 대답과 같이). 나는 이미 그 모든 것을 이해하기 때문에 나와 같은 누군가에게 낭비됩니다. 문제의 잘못된 부분을 설명하고 있습니다.
내가 어디에서 왔는지 알기 위해 포인터를 완벽하게 이해하는 사람이며 어셈블러 언어로 유능하게 사용할 수 있습니다. 어셈블러 언어에서는 포인터라고하지 않습니다. 이를 주소라고합니다. C에서 프로그래밍하고 포인터를 사용할 때 많은 실수를하고 혼란스러워합니다. 나는 아직도 이것을 정리하지 않았다. 예를 들어 보겠습니다.
API가 말하면 :
int doIt(char *buffer )
//*buffer is a pointer to the buffer
무엇을 원합니까?
그것은 원할 수 있습니다 :
버퍼의 주소를 나타내는 숫자
(그것을주기 위해 doIt(mybuffer)
, 또는 말 doIt(*myBuffer)
합니까?)
주소의 주소를 버퍼의 주소로 나타내는 숫자
(즉 doIt(&mybuffer)
하거나 doIt(mybuffer)
또는 doIt(*mybuffer)
?)
주소의 주소를 버퍼의 주소로 나타내는 숫자
(아마의 그 doIt(&mybuffer)
. 또는 그 것이다 doIt(&&mybuffer)
? 또는doIt(&&&mybuffer)
)
그리고 "x는 주소를 y로 유지합니다"와 "나를 의미하지 않습니다." 이 함수는 y "에 주소가 필요합니다. 답은 "mybuffer"가 무엇으로 시작해야하는지, 그리고 그것과 함께 무엇을 하려는지에 달려 있습니다. 이 언어는 실제로 발생하는 중첩 수준을 지원하지 않습니다. 새 포인터를 만드는 함수에 "포인터"를 넘겨야 할 때와 마찬가지로 버퍼의 새 위치를 가리 키도록 포인터를 수정합니다. 실제로 포인터 또는 포인터에 대한 포인터를 원하므로 포인터의 내용을 수정할 위치를 알고 있습니다. 대부분의 경우 "
"포인터"가 너무 오버로드되었습니다. 포인터가 값의 주소입니까? 또는 주소를 값으로 보유하는 변수입니까? 함수가 포인터를 원할 때 포인터 변수가 보유한 주소를 원합니까 아니면 포인터 변수에 주소를 원합니까? 혼란 스러워요.
double *(*(*fn)(int))(char)
, 평가 결과 *(*(*fn)(42))('x')
는 double
입니다. 중간 평가 유형을 이해하기 위해 평가 계층을 제거 할 수 있습니다.
(*(*fn)(42))('x')
그때 평가 한 결과는 무엇입니까 ?
x
평가 *x
하면 두 배가 되는 물건 을 얻습니다.
fn
입니다 보다 당신이 할 수있는 측면에서 할 로fn
나는 포인터를 이해하는데 주된 장벽은 나쁜 교사라고 생각합니다.
거의 모든 사람들은 포인터에 관해 거짓말을한다 : 그것들은 메모리 주소에 지나지 않으며 , 임의의 위치 를 가리킬 수있다 .
물론 이해하기 어렵고 위험하며 반 마술 적입니다.
어느 것도 사실이 아닙니다. C ++ 언어가 말하는 것에 충실하고 실제로 "작동하는"것으로 밝혀 지지만, 언어에 의해 보장되지는 않는 한, 포인터는 실제로 매우 단순한 개념 입니다. 실제 포인터 개념의 일부가 아닙니다.
몇 달 전에이 블로그 게시물 에 이것에 대한 설명을 쓰려고 노력했습니다 .
C ++ 표준은 포인터 가 메모리 주소를 나타낸다고 말하지만 "포인터는 메모리 주소이며 메모리 주소 외에는 메모리와 상호 교환 가능하게 사용되거나 생각 될 수있다"고 말하는 것은 아닙니다. 주소 ". 구별이 중요합니다)
이해하기 어려운 이유는 어려운 개념 때문 이 아니라 구문이 일관성이 없기 때문 입니다.
int *mypointer;
먼저 변수 작성의 가장 왼쪽 부분이 변수의 유형을 정의한다는 것을 배웁니다. C 및 C ++에서는 포인터 선언이 이와 같이 작동하지 않습니다. 대신 그들은 변수가 왼쪽의 유형을 가리키고 있다고 말합니다. 이 경우 *
mypointer 가 int를 가리키고 있습니다.
C #에서 안전하지 않은 포인터를 사용하려고 할 때까지 포인터를 완전히 파악하지 못했습니다. 정확하고 동일한 방식으로 논리적이고 일관된 구문으로 작동합니다. 포인터는 유형 자체입니다. 여기서 mypointer 는 int에 대한 포인터입니다.
int* mypointer;
함수 포인터를 시작하지 않아도됩니다 ...
int *p;
간단한 의미가 있습니다 : *p
정수입니다. int *p, **pp
의미 *p
하고 **pp
정수입니다.
*p
하고 **pp
있습니다 하지 당신이 초기화되지 않기 때문에, 정수 p
또는 pp
또는 *pp
아무것도 지점. 나는 왜 일부 사람들이 문법을 고수하는 것을 선호하는지 이해합니다. 특히 일부 경우와 복잡한 경우에는 그렇게해야합니다 (그러나 여전히 알고있는 모든 경우에 그 문제를 해결할 수는 있습니다) ... 그러나 나는 올바른 조정을 가르치는 것이 초보자들에게 오도한다는 사실보다 그 경우가 더 중요하다고 생각하지 않습니다. 못생긴 종류는 말할 것도 없습니다! :)
나는 집 주소 비유를 좋아하지만 항상 주소가 사서함 자체에 있다고 생각했습니다. 이렇게하면 포인터 참조 해제 (사서함 열기)의 개념을 시각화 할 수 있습니다.
예를 들어 링크 된 목록을 따르는 경우 : 1) 주소가있는 용지로 시작 2) 용지의 주소로 이동 3) 다음 주소가있는 새 용지를 찾으려면 우편함을 엽니 다.
선형 연결 목록에서 마지막 사서함에는 목록이 없습니다 (목록 끝). 순환 연결 목록에서 마지막 사서함에는 첫 번째 사서함의 주소가 있습니다.
3 단계는 역 참조가 발생하는 위치이며 주소가 유효하지 않은 경우 충돌하거나 잘못 될 수 있습니다. 잘못된 주소의 사서함으로 걸어 갈 수 있다고 가정하면 블랙홀이나 그 안에 세상을 뒤집는 무언가가 있다고 상상해보십시오. :)
사람들이 어려움을 겪는 주된 이유는 일반적으로 흥미롭고 매력적인 방식으로 가르치지 않기 때문이라고 생각합니다. 나는 강사가 군중으로부터 10 명의 자원 봉사자를 얻고 그들에게 각각 1 미터의 통치자를주고, 특정 구성으로 서서 서로를 가리 키도록합니다. 그런 다음 사람들을 움직여서 (그리고 통치자를 가리키는 위치) 포인터 산술을 보여주십시오. 그것은 역학에 너무 얽매이지 않고 개념을 보여주는 간단하지만 효과적 (그리고 무엇보다도 기억에 남는) 방법 일 것입니다.
C와 C ++에 도달하면 일부 사람들에게는 더 힘들어 보입니다. 이것이 그들이 실제로 제대로 이해하지 못하거나 포인터 조작이 본질적으로 그 언어에서 어렵다는 이론을 제시하고 있기 때문에 확실하지 않습니다. 나는 내 자신의 전환을 잘 기억하지 못하지만 파스칼의 포인터를 알고 C로 이동하여 완전히 잃어 버렸습니다.
실제로 구문 문제 일 수 있습니다. 포인터의 C / C ++ 구문은 일관성이없고 필요 이상으로 복잡해 보입니다.
아이러니하게도, 실제로 포인터를 이해하는 데 도움이 된 것은 C ++ 표준 템플릿 라이브러리 에서 반복기의 개념에 직면했다 . 반복자가 포인터의 일반화로 생각되었다고 가정 할 수 있기 때문에 역설적입니다.
때로는 나무를 무시하는 법을 배울 때까지 숲을 볼 수 없습니다.
(*p)
이었을 것입니다 (p->)
, 따라서 우리는이 것 p->->x
대신 모호한의*p->x
a->b
단순히 의미 (*a).b
합니다.
* p->x
수단 * ((*a).b)
반면 *p -> x
수단 (*(*p)) -> x
. 접두사와 접미사 연산자를 혼합하면 모호한 구문 분석이 발생합니다.
1+2 * 3
9이어야한다고 말하는 것과 같습니다.
혼란은 "포인터"개념으로 함께 혼합 된 다중 추상화 계층에서 비롯됩니다. 프로그래머는 Java / Python에서 일반적인 참조로 혼동하지 않지만 포인터는 기본 메모리 아키텍처의 특성을 노출한다는 점에서 다릅니다.
추상화 계층을 깨끗하게 분리하는 것이 좋은 원칙이며 포인터는 그렇게하지 않습니다.
foo[i]
것은 특정 지점으로 가고 특정 거리를 앞으로 나아가고 거기에 무엇이 있는지 보는 것을 의미합니다. 문제를 복잡하게 만드는 것은 컴파일러의 이익을 위해 순수하게 표준에 의해 추가 된 훨씬 더 복잡한 추가 추상화 계층이지만 프로그래머 요구와 컴파일러 요구에 적합하지 않은 방식으로 모델링합니다.
내가 설명하는 방식은 배열과 색인의 관점에서였습니다. 사람들은 포인터에 익숙하지 않을 수도 있지만 일반적으로 색인이 무엇인지 알고 있습니다.
따라서 RAM이 배열이라고 가정하십시오 (및 10 바이트의 RAM 만 있음).
unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };
그런 다음 변수에 대한 포인터는 실제로 RAM에서 해당 변수의 첫 번째 바이트입니다.
따라서 pointer / index가있는 경우 unsigned char index = 2
값은 분명히 세 번째 요소 또는 숫자 4입니다. 포인터에 대한 포인터는 해당 숫자를 가져 와서 인덱스 자체로 사용하는 위치입니다RAM[RAM[index]]
입니다.
나는 종이 목록에 배열을 그리고 같은 메모리를 가리키는 많은 포인터, 포인터 산술, 포인터 포인터 등과 같은 것들을 보여주기 위해 그것을 사용합니다.
우체국 박스 번호.
다른 정보에 액세스 할 수있는 정보입니다.
(우체국 사서함 번호에서 산술을 수행하면 문자가 잘못된 상자에 들어가기 때문에 문제가 발생할 수 있습니다. 누군가가 전달 주소가없는 다른 상태로 이동하면 매달린 포인터가 있습니다. 반면 우체국에서 메일을 전달하면 포인터에 대한 포인터가 있습니다.)
반복자를 통해 그것을 잡는 나쁜 방법은 아니지만 .. 계속해서 알다시피 Alexandrescu가 그들에 대해 불평하기 시작할 것입니다.
많은 ex-C ++ 개발자 (이터레이터가 언어를 덤프하기 전에 현대적인 포인터라는 것을 결코 이해하지 못했던)는 C #으로 이동하여 여전히 알맞은 반복자가 있다고 생각합니다.
흠, 문제는 모든 반복자가 런타임 플랫폼 (Java / CLR)이 달성하려고하는 것, 즉 새롭고 간단한 모든 사람 사용법과 완전히 일치하지 않는다는 것입니다. 어느 것이 좋을지 모르지만 그들은 보라색 책에서 한 번 말했고 C 전과 전에도 말했습니다.
우회.
매우 강력한 개념이지만 항상 그렇게한다면 결코 그렇지 않습니다. 반복자는 알고리즘의 추상화, 또 다른 예를 돕는 데 유용합니다. 그리고 컴파일 타임은 매우 간단한 알고리즘의 장소입니다. 코드 + 데이터 또는 다른 언어 C #을 알고 있습니다.
IEnumerable + LINQ + Massive Framework = 300MB의 런타임 페널티 간접 처리로 간접 참조 형식의 인스턴스를 통해 앱을 드래그합니다.
"Le Pointer는 싸다."
위의 답변 중 일부는 "포인터가 정말 어렵지 않다"고 주장했지만 "포인터가 어려운 곳"을 직접 다루지는 않았습니다. 에서 오는. 몇 년 전 저는 CS 학생들에게 첫 해를 가르쳤으며 (1 년 동안 분명히 빨 랐기 때문에) 포인터에 대한 아이디어 가 어렵지 않다는 것이 분명했습니다 . 어려운 이유는 포인터를 원하는 이유와시기를 이해 하는 것입니다. 입니다.
포인터를 사용해야하는 이유와시기에 대해 더 넓은 소프트웨어 엔지니어링 문제를 설명하는 것으로부터이 질문을 이혼 할 수 있다고 생각하지 않습니다. 모든 변수가 전역 변수가 아니 어야 하는 이유와 비슷한 코드를 함수에 포함시켜야하는 이유 는 무엇입니까 (즉, 포인터 를 사용 하여 콜 사이트에 대한 동작을 특수화).
포인터에 대해 너무 혼란스러워하는 것을 보지 못했습니다. 메모리의 위치, 즉 메모리 주소를 저장합니다. C / C ++에서는 포인터가 가리키는 유형을 지정할 수 있습니다. 예를 들면 다음과 같습니다.
int* my_int_pointer;
my_int_pointer에 int가 포함 된 위치의 주소가 포함되어 있다고합니다.
포인터의 문제점은 메모리의 위치를 가리 키므로 사용자가 포함하지 않아야 할 위치로 쉽게 넘어갈 수 있다는 것입니다. 버퍼 오버플로에서 C / C ++ 응용 프로그램의 수많은 보안 허점을 살펴보십시오 (포인터 증가). 할당 된 경계를지나).
일을 좀 더 혼동시키기 위해 때로는 포인터 대신 핸들로 작업해야합니다. 핸들은 포인터에 대한 포인터이므로 백엔드는 메모리에서 항목을 이동하여 힙 조각 모음을 수행 할 수 있습니다. 포인터가 중간에 바뀌면 결과를 예측할 수 없으므로 먼저 핸들을 잠 가서 아무데도 이동하지 않도록해야합니다.
http://arjay.bc.ca/Modula-2/Text/Ch15/Ch15.8.html#15.8.5 나보다 좀 더 일관되게 이야기합니다. :-)
모든 C / C ++ 초보자에게는 동일한 문제가 있으며 "포인터를 배우기가 어렵 기"때문이 아니라 "누가 어떻게 그리고 어떻게 설명해야하는지"때문에 문제가 발생합니다. 일부 학습자는 시각적으로 일부를 시각적으로 수집하고이를 설명하는 가장 좋은 방법은 "트레이닝"예제 를 사용하는 것입니다 (언어 및 시각적 예에 적합)를 사용하는 것입니다.
여기서 "기관차" 는 아무것도 보유 할 수없는 포인터 이고 "왜건" 은 "기관차"가 당기려고하는 것입니다. 그런 다음 "수레"자체를 분류하고 동물, 식물 또는 사람 (또는 이들의 혼합)을 보유 할 수 있습니까?