포인터를 이해하는 데 장애물은 무엇이며 이를 극복하기 위해 무엇을 할 수 있습니까?[닫은]

StackOverflow https://stackoverflow.com/questions/5727

  •  08-06-2019
  •  | 
  •  

문제

포인터가 C 또는 C++를 배우는 많은 신입생, 심지어 기존 대학생들에게 혼동을 일으키는 주요 요인인 이유는 무엇입니까?포인터가 변수, 함수 및 그 이상의 수준에서 작동하는 방식을 이해하는 데 도움이 되는 도구나 사고 과정이 있습니까?

전체 개념에 얽매이지 않고 누군가를 "아하, 알겠습니다" 수준으로 끌어올릴 수 있는 좋은 연습 방법은 무엇입니까?기본적으로 시나리오와 같은 드릴입니다.

도움이 되었습니까?

해결책

포인터는 많은 사람들에게 처음에는 혼란스러울 수 있는 개념입니다. 특히 포인터 값을 복사하고 여전히 동일한 메모리 블록을 참조하는 경우에는 더욱 그렇습니다.

가장 좋은 비유는 포인터를 집 주소가 적힌 종이 조각으로 간주하고 포인터가 참조하는 메모리 블록을 실제 집으로 간주하는 것입니다.따라서 모든 종류의 작업을 쉽게 설명할 수 있습니다.

아래에 몇 가지 Delphi 코드를 추가하고 적절한 경우 몇 가지 설명을 추가했습니다.저는 다른 주요 프로그래밍 언어인 C#이 메모리 누수와 같은 현상을 같은 방식으로 나타내지 않기 때문에 Delphi를 선택했습니다.

포인터의 높은 수준의 개념만 배우고 싶다면 아래 설명에서 "메모리 레이아웃"이라고 표시된 부분을 무시해야 합니다.이는 작업 후 메모리가 어떤 모습일 수 있는지에 대한 예를 제공하기 위한 것이지만 본질적으로 더 낮은 수준입니다.그러나 버퍼 오버런이 실제로 어떻게 작동하는지 정확하게 설명하려면 이러한 다이어그램을 추가하는 것이 중요했습니다.

부인 성명:모든 의도와 목적을 위해이 설명과 예제 메모리 레이아웃은 크게 단순화됩니다.낮은 수준으로 메모리를 처리 해야하는지 알아야 할 더 많은 오버 헤드와 더 많은 세부 사항이 있습니다.그러나 기억과 포인터를 설명하려는 의도의 경우 충분히 정확합니다.


아래에 사용된 THouse 클래스가 다음과 같다고 가정해 보겠습니다.

type
    THouse = class
    private
        FName : array[0..9] of Char;
    public
        constructor Create(name: PChar);
    end;

집 객체를 초기화하면 생성자에 지정된 이름이 프라이빗 필드 FName에 복사됩니다.고정 크기 배열로 정의된 이유가 있습니다.

기억상으로는 주택 할당과 관련된 약간의 오버헤드가 있을 것입니다. 아래에서 이를 다음과 같이 설명하겠습니다.

---[ttttNNNNNNNNNN]---
     ^   ^
     |   |
     |   +- the FName array
     |
     +- overhead

"tttt" 영역은 오버헤드입니다. 일반적으로 8바이트 또는 12바이트와 같은 다양한 유형의 런타임 및 언어에 대해 이 영역이 더 많습니다.이 영역에 저장된 모든 값은 메모리 할당자나 핵심 시스템 루틴 이외의 다른 것에 의해 변경되지 않아야 합니다. 그렇지 않으면 프로그램이 충돌할 위험이 있습니다.


메모리 할당

집을 지을 기업가를 구하고 집 주소를 알려주세요.실제 세계와 달리 메모리 할당은 어디에 할당할지 알 수 없지만 충분한 공간이 있는 적절한 지점을 찾아 할당된 메모리에 주소를 다시 보고합니다.

즉, 기업가가 그 자리를 선택할 것입니다.

THouse.Create('My house');

메모리 레이아웃:

---[ttttNNNNNNNNNN]---
    1234My house

주소가 포함된 변수 유지

종이에 새 집의 주소를 적어 두십시오.이 종이는 귀하의 집에 대한 참고 자료가 될 것입니다.이 종이가 없으면 길을 잃으며 이미 그 집에 있지 않으면 집을 찾을 수 없습니다.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...

메모리 레이아웃:

    h
    v
---[ttttNNNNNNNNNN]---
    1234My house

포인터 값 복사

새 종이에 주소를 적으면 됩니다.이제 당신은 두 개의 별도 집이 아닌 같은 집으로 갈 수 있는 두 장의 종이를 갖게 되었습니다.한 신문의 주소를 따라가서 그 집의 가구를 재배치하려는 모든 시도는 다른 집 실제로는 단지 하나의 집이라는 것을 명시적으로 감지할 수 없는 한 동일한 방식으로 수정되었습니다.

메모 이것은 일반적으로 사람들에게 설명하기 가장 어려운 개념입니다. 두 개의 포인터는 두 개의 객체나 메모리 블록을 의미하지 않습니다.

var
    h1, h2: THouse;
begin
    h1 := THouse.Create('My house');
    h2 := h1; // copies the address, not the house
    ...
    h1
    v
---[ttttNNNNNNNNNN]---
    1234My house
    ^
    h2

메모리 해제

집을 철거하세요.원하는 경우 나중에 새 주소에 대해 종이를 재사용하거나 더 이상 존재하지 않는 집 주소를 잊어버리도록 종이를 지울 수 있습니다.

var
    h: THouse;
begin
    h := THouse.Create('My house');
    ...
    h.Free;
    h := nil;

여기서 나는 먼저 집을 짓고 그 주소를 알아낸다.그런 다음 집에 뭔가를 합니다. (그것을 사용하세요, ...독자를 위한 연습용으로 남겨둔 코드) 그런 다음 해제합니다.마지막으로 변수에서 주소를 지웁니다.

메모리 레이아웃:

    h                        <--+
    v                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after free
----------------------          | (note, memory might still
    xx34My house             <--+  contain some data)

매달린 포인터

당신은 기업가에게 집을 파괴하라고 말했지만 종이에서 주소를 지우는 것을 잊어버렸습니다.나중에 그 종이를 볼 때 그 집이 더 이상 거기에 없다는 사실을 잊어버리고 방문하러 갔지만 결과는 실패했습니다(아래의 유효하지 않은 참조에 대한 부분도 참조하십시오).

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                           +- before free
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h                        <--+
    v                           +- after free
----------------------          |
    xx34My house             <--+

보시다시피, 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]---
    1234My house

두 번째 할당 후 메모리 레이아웃:

                       h
                       v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

이 방법을 얻는 더 일반적인 방법은 위와 같이 덮어쓰는 대신 무언가를 해제하는 것을 잊어버리는 것입니다.Delphi 용어로 이는 다음 방법으로 발생합니다.

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                           +- before losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

    h (now points nowhere)   <--+
                                +- after losing pointer
---[ttttNNNNNNNNNN]---          |
    1234My house             <--+

보시다시피, 기존 데이터는 메모리에 손상되지 않으며 메모리 할당 자에 의해 재사용되지 않습니다.할당자는 메모리가 사용 된 영역을 추적하고 자유롭게 해제하지 않으면 재사용하지 않습니다.


메모리를 해제하지만 (지금은 유효하지 않은) 참조를 유지합니다.

집을 부수고 종이 한 장을 지웠지만 예전 주소가 적힌 또 다른 종이도 남아 있습니다. 주소로 가면 집은 없지만 폐허와 비슷한 것을 찾을 수 있습니다. 하나의.

아마도 당신은 집을 찾을 수도 있지만, 그 집은 당신이 원래 주소를 받았던 집이 아니므로, 그 집이 당신 소유인 것처럼 사용하려는 시도는 끔찍하게 실패할 수도 있습니다.

때로는 이웃 주소에 세 개의 주소(Main Street 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]
                        5678My house

두 번째 할당 후 메모리 레이아웃:

    h2                  h1
    v                   v
---[ttttNNNNNNNNNN]----[ttttNNNNNNNNNN]
    1234My other house somewhereouse
                        ^---+--^
                            |
                            +- overwritten

가장 자주 충돌을 일으키는 부분은 저장된 데이터의 중요한 부분을 실제로 무작위로 변경해서는 안되는 부분을 덮어 쓰는 것입니다.예를 들어 프로그램 충돌 측면에서 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;

메모리 레이아웃 (아래 다이어그램의 4 개의 llll과 함께 언급 된 객체의 링크로 NexThouse를 추가) :

    h1                      h2
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home       +        5678Cabin      +
                   |        ^              |
                   +--------+              * (no link)

기본적으로 메모리 주소란 무엇입니까?

메모리 주소는 기본적으로 숫자일 뿐입니다.메모리를 큰 바이트 배열로 생각하면 첫 번째 바이트에는 주소 0, 다음 주소 1 등이 위쪽에 있습니다.이것은 단순화되었지만 충분합니다.

따라서 이 메모리 레이아웃은 다음과 같습니다.

    h1                 h2
    v                  v
---[ttttNNNNNNNNNN]---[ttttNNNNNNNNNN]
    1234My house       5678My house

다음 두 주소가 있을 수 있습니다(가장 왼쪽 - 주소 0).

  • h1 = 4
  • h2 = 23

이는 위의 연결 목록이 실제로 다음과 같을 수 있음을 의미합니다.

    h1 (=4)                 h2 (=28)
    v                       v
---[ttttNNNNNNNNNNLLLL]----[ttttNNNNNNNNNNLLLL]
    1234Home      0028      5678Cabin     0000
                   |        ^              |
                   +--------+              * (no link)

"아무데도 없는" 주소를 0 주소로 저장하는 것이 일반적입니다.


기본적으로 포인터란 무엇입니까?

포인터는 단지 메모리 주소를 담고 있는 변수일 뿐입니다.일반적으로 프로그래밍 언어에 숫자를 제공하도록 요청할 수 있지만 대부분의 프로그래밍 언어와 런타임은 숫자 자체가 실제로 의미를 유지하지 않기 때문에 아래에 숫자가 있다는 사실을 숨기려고합니다.포인터를 블랙박스로 생각하는 것이 가장 좋습니다.당신은 그것이 작동하는 한 실제로 어떻게 구현되는지에 대해 알거나 신경 쓰지 않습니다.

다른 팁

나의 첫 Comp Sci 수업에서 우리는 다음과 같은 연습을 했습니다.물론 이곳은 학생이 200명쯤 되는 강의실이었는데…

교수는 칠판에 이렇게 썼다. int john;

존은 일어섰다.

교수는 다음과 같이 씁니다. int *sally = &john;

Sally가 일어나서 John을 가리킵니다.

교수: int *bill = sally;

Bill은 일어나서 John을 가리킵니다.

교수: int sam;

샘은 일어섰다.

교수: bill = &sam;

Bill은 이제 Sam을 가리킵니다.

나는 당신이 아이디어를 얻을 것 같아요.포인터 할당의 기본 사항을 살펴볼 때까지 이 작업을 수행하는 데 약 한 시간이 소요된 것 같습니다.

포인터를 설명하는 데 도움이 되는 비유는 하이퍼링크입니다.대부분의 사람들은 웹 페이지의 링크가 인터넷의 다른 페이지를 '가리킨다'는 것을 이해할 수 있으며, 해당 하이퍼링크를 복사하여 붙여넣을 수 있으면 둘 다 동일한 원본 웹 페이지를 가리킬 것입니다.원본 페이지로 이동하여 편집한 다음 해당 링크(포인터) 중 하나를 따라가면 새로 업데이트된 페이지가 표시됩니다.

포인터가 많은 사람들을 혼란스럽게 하는 이유는 포인터가 대부분 컴퓨터 아키텍처에 대한 배경 지식이 거의 또는 전혀 없기 때문입니다.많은 사람들이 컴퓨터(기계)가 실제로 어떻게 구현되는지 알지 못하는 것 같기 때문에 C/C++로 작업하는 것은 낯설게 보입니다.

훈련은 포인터 작업(로드, 저장, 직접/간접 주소 지정)에 초점을 맞춘 명령어 세트를 사용하여 간단한 바이트코드 기반 가상 머신(선택한 어떤 언어로든 Python이 이에 적합함)을 구현하도록 요청하는 것입니다.그런 다음 해당 명령 세트에 대한 간단한 프로그램을 작성하도록 요청하십시오.

단순한 추가보다 약간 더 필요한 것은 포인터가 필요하며 포인터는 확실히 얻을 수 있습니다.

포인터가 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의 "Tutorial on Pointers and Arrays in C"가 포인터에 대해 배울 수 있는 훌륭한 자료라는 것을 알았습니다.포인터가 무엇인지(및 그 용도)에 대한 설명으로 시작하여 함수 포인터로 마무리되는 10개의 단원으로 구성되어 있습니다. http://home.netcom.com/~tjensen/ptr/cpoint.htm

거기에서 나아가서 Beej의 네트워크 프로그래밍 가이드에서는 정말 재미있는 일을 시작할 수 있는 Unix 소켓 API를 가르칩니다. http://beej.us/guide/bgnet/

포인터의 복잡성은 우리가 쉽게 가르칠 수 있는 것 이상입니다.학생들이 서로를 가리키도록 하고 집 주소가 적힌 종이를 사용하는 것은 모두 훌륭한 학습 도구입니다.그들은 기본 개념을 소개하는 데 훌륭한 역할을 합니다.사실 기본 개념을 익히는 것은 필수적인 포인터를 성공적으로 사용하려면그러나 프로덕션 코드에서는 이러한 간단한 데모가 캡슐화할 수 있는 것보다 훨씬 더 복잡한 시나리오에 들어가는 것이 일반적입니다.

저는 다른 구조를 가리키는 다른 구조를 가리키는 구조가 있는 시스템에 참여해 왔습니다.이러한 구조 중 일부에는 추가 구조에 대한 포인터가 아닌 내장 구조도 포함되어 있습니다.포인터가 정말 혼란스러워지는 곳입니다.여러 수준의 간접 참조가 있고 다음과 같은 코드로 끝나기 시작하는 경우:

widget->wazzle.fizzle = fazzle.foozle->wazzle;

정말 빨리 혼란스러울 수 있습니다(더 많은 라인과 잠재적으로 더 많은 레벨을 상상해 보세요).포인터 배열과 노드 간 포인터(트리, 연결 목록)를 추가하면 상황은 더욱 악화됩니다.나는 정말 훌륭한 개발자들이 그러한 시스템에서 작업을 시작하고 나면 길을 잃는 것을 본 적이 있습니다. 심지어 기본을 아주 잘 이해하고 있는 개발자들도 마찬가지입니다.

포인터의 복잡한 구조가 반드시 잘못된 코딩을 나타내는 것은 아닙니다(그럴 수도 있지만).구성은 좋은 객체 지향 프로그래밍의 핵심 부분이며 원시 포인터가 있는 언어에서는 필연적으로 다층 간접 참조로 이어집니다.또한 시스템은 스타일이나 기술이 서로 일치하지 않는 구조를 가진 타사 라이브러리를 사용해야 하는 경우가 많습니다.그러한 상황에서는 자연스럽게 복잡성이 발생하게 됩니다(물론, 우리는 가능한 한 많이 싸워야 합니다).

학생들이 포인터를 배울 수 있도록 대학에서 할 수 있는 가장 좋은 일은 포인터 사용이 필요한 프로젝트와 결합하여 좋은 데모를 사용하는 것이라고 생각합니다.하나의 어려운 프로젝트는 수천 번의 시연보다 포인터 이해에 더 많은 도움이 됩니다.시연을 통해 얕은 이해를 얻을 수 있지만 지침을 깊이 파악하려면 실제로 지침을 사용해야 합니다.

나는 이 목록에 컴퓨터 과학 교사로서 (그 당시) 포인터를 설명할 때 매우 도움이 되었던 비유를 추가하고 싶다고 생각했습니다.먼저 다음을 수행해 보겠습니다.


무대를 마련하다:

3개의 공간이 있는 주차장을 생각해 보세요. 이 공간에는 번호가 매겨져 있습니다.

-------------------
|     |     |     |
|  1  |  2  |  3  |
|     |     |     |

어떤 면에서는 이것은 기억의 위치와 비슷하며 순차적이고 연속적입니다.일종의 배열과 같습니다.지금은 그 안에 자동차가 없으므로 빈 배열과 같습니다(parking_lot[3] = {0}).


데이터 추가

주차장은 오래도록 비어있지 않습니다...만약 그렇다면 그것은 무의미할 것이고 아무도 그것을 만들지 않을 것입니다.그럼 하루가 지나면서 주차장이 파란색 자동차, 빨간색 자동차, 녹색 자동차 등 세 대의 자동차로 가득 차게 된다고 가정해 보겠습니다.

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |R| | |G| |
| o-o | o-o | o-o |

이 자동차는 모두 같은 유형(자동차)이므로 이를 생각하는 한 가지 방법은 자동차가 일종의 데이터(예: int) 그러나 값은 서로 다릅니다(blue, red, green;그게 색깔일 수도 있지 enum)


포인터를 입력하세요

이제 제가 여러분을 이 주차장으로 데려가 파란색 차를 찾아달라고 하면 여러분은 손가락 하나를 펴서 1번 지점에 있는 파란색 차를 가리킵니다.이는 포인터를 가져와 메모리 주소(int *finger = parking_lot)

당신의 손가락(포인터)은 내 질문에 대한 답이 아닙니다.찾고 ~에 네 손가락은 나에게 아무것도 말해주지 않지만, 내가 네 손가락이 어디에 있는지 보면 가리키는 (포인터를 역참조), 내가 찾고 있던 자동차(데이터)를 찾을 수 있습니다.


포인터 재할당

이제 대신 빨간 차를 찾아달라고 요청할 수 있고 손가락을 새 차로 바꿀 수 있습니다.이제 포인터(이전과 동일)가 동일한 유형(자동차)의 새로운 데이터(빨간색 자동차를 찾을 수 있는 주차 장소)를 보여줍니다.

포인터는 물리적으로 변경되지 않았으며 여전히 변경되었습니다. 당신의 손가락으로 보여주었던 데이터만 변경되었습니다.("주차장" 주소)


이중 포인터(또는 포인터에 대한 포인터)

이는 둘 이상의 포인터에서도 작동합니다.빨간 차를 가리키는 포인터가 어디에 있는지 물을 수 있고, 다른 손을 사용하여 첫 번째 손가락을 가리킬 수 있습니다.(이것은 마치 int **finger_two = &finger)

이제 파란 차가 어디에 있는지 알고 싶다면 첫 번째 손가락의 방향을 따라 두 번째 손가락, 즉 자동차(데이터)로 갈 수 있습니다.


매달린 포인터

이제 당신이 조각상과 매우 흡사한 기분이 들고 빨간 차를 가리키는 손을 무한정 잡고 싶다고 가정해 보겠습니다.저 빨간 차가 가버리면 어떡하지?

   1     2     3
-------------------
| o=o |     | o=o |
| |B| |     | |G| |
| o-o |     | o-o |

당신의 포인터는 여전히 빨간 차가 있는 곳을 가리키고 있습니다. ~였다 하지만 더 이상은 아닙니다.거기에 새 차가 들어온다고 해보자.오렌지 자동차.이제 내가 다시 "빨간 차는 어디에 있습니까?"라고 묻는다면, 당신은 여전히 ​​그곳을 가리키고 있지만 지금은 틀렸습니다.저건 빨간 차가 아니고 주황색이에요.


포인터 연산

알겠습니다. 여전히 두 번째 주차 공간(현재는 Orange 차량이 차지하고 있음)을 가리키고 계십니다.

   1     2     3
-------------------
| o=o | o=o | o=o |
| |B| | |O| | |G| |
| o-o | o-o | o-o |

그런데 이제 새로운 질문이 생겼습니다...현재 차량 색상을 알고 싶습니다. 다음 주차 공간.지점 2를 가리키고 있는 것을 볼 수 있으므로 1을 추가하면 다음 지점을 가리키게 됩니다.(finger+1), 이제 거기에 어떤 데이터가 있는지 알고 싶었기 때문에 포인터를 참조할 수 있도록 해당 지점(손가락뿐만 아니라)을 확인해야 합니다(*(finger+1)) 거기에 녹색 자동차가 있는지 확인합니다(해당 위치의 데이터).

나는 개념으로서의 포인터가 특별히 까다롭다고 생각하지 않습니다. 대부분의 학생들의 정신 모델은 이와 같은 것에 매핑되며 몇 가지 빠른 상자 스케치가 도움이 될 수 있습니다.

적어도 과거에 내가 경험했고 다른 사람들이 다루는 것을 본 어려움은 C/C++에서 포인터 관리가 불필요하게 복잡해질 수 있다는 것입니다.

좋은 다이어그램 세트가 포함된 튜토리얼의 예는 포인터를 이해하는 데 큰 도움이 됩니다..

Joel Spolsky는 자신의 저서에서 포인터 이해에 대한 몇 가지 좋은 점을 설명합니다. 게릴라 인터뷰 가이드 기사:

어떤 이유에서인지 대부분의 사람들은 포인터를 이해하는 뇌 부분이 없이 태어나는 것 같습니다.이것은 기술이 아니라 적성입니다. 일부 사람들은 할 수 없는 복잡한 형태의 이중 간접적 사고가 필요합니다.

포인터를 이해하는 데 가장 큰 장벽은 나쁜 교사라고 생각합니다.

거의 모든 사람이 포인터에 대한 거짓말을 배웁니다.그들은 메모리 주소 외에는 아무것도 없습니다, 또는 이를 통해 다음을 가리킬 수 있습니다. 임의의 위치.

물론 그것들은 이해하기 어렵고 위험하며 반마법적입니다.

어느 것도 사실이 아닙니다.포인터는 실제로 매우 간단한 개념입니다. C++ 언어가 말하는 내용을 고수하는 한 그리고 "보통" 실제로 작동하는 것으로 판명되었지만 그럼에도 불구하고 언어에 의해 보장되지 않아 포인터의 실제 개념의 일부가 아닌 속성을 속성에 부여하지 마세요.

나는 몇 달 전에 이에 대한 설명을 쓰려고 했습니다. 이 블로그 게시물 - 누군가에게 도움이 되기를 바랍니다.

(누군가가 나에게 현학적이라고 말하기 전에, 그렇습니다. C++ 표준에서는 포인터가 대표하다 메모리 주소.그러나 "포인터는 메모리 주소이며 메모리 주소일 뿐이며 메모리 주소와 상호 교환적으로 사용되거나 간주될 수 있습니다"라고 말하지 않습니다.구별이 중요합니다)

포인터의 문제는 개념이 아닙니다.관련된 실행과 언어입니다.교사가 전문 용어나 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"가 무엇으로 시작하는지, 그리고 그것을 사용하여 무엇을 하려는지에 따라 달라집니다.언어는 실제로 발생하는 중첩 수준을 지원하지 않습니다.새 버퍼를 생성하는 함수에 "포인터"를 전달해야 하는 경우와 마찬가지로 버퍼의 새 위치를 가리키도록 포인터가 수정됩니다.실제로 포인터를 원합니까, 아니면 포인터에 대한 포인터를 원합니까? 그래서 포인터의 내용을 수정하기 위해 어디로 가야 하는지 알 수 있습니다.대부분의 경우 나는 "포인터"가 무엇을 의미하는지 추측해야 하며 대부분의 경우 추측에 대한 경험이 얼마나 많은지에 관계없이 틀렸습니다.

"포인터"는 너무 과부하되었습니다.포인터는 값에 대한 주소입니까?아니면 값에 대한 주소를 보유하는 변수입니까?함수가 포인터를 원할 때 포인터 변수가 보유하는 주소를 원합니까, 아니면 포인터 변수에 대한 주소를 원합니까?혼란스러워요.

포인터를 배우기 어렵게 만드는 것은 포인터가 나올 때까지는 "이 메모리 위치에 int, double, 문자 등을 나타내는 비트 집합이 있다"는 생각에 익숙하기 때문이라고 생각합니다.

포인터를 처음 볼 때 해당 메모리 위치에 무엇이 있는지 실제로 알 수 없습니다."무슨 말이야? 주소?"

나는 "당신이 그것을 얻거나 얻지 못한다"는 개념에 동의하지 않습니다.

실제 용도를 찾기 시작하면(예: 큰 구조를 함수에 전달하지 않는 등) 이해하기가 더 쉬워집니다.

이해하기 어려운 이유는 개념이 어려워서가 아니라, 구문이 일관되지 않습니다.

   int *mypointer;

변수 생성의 가장 왼쪽 부분이 변수 유형을 정의한다는 것을 먼저 배웠습니다.C 및 C++에서는 포인터 선언이 이와 같이 작동하지 않습니다.대신 변수가 왼쪽 유형을 가리키고 있다고 말합니다.이 경우: *마이포인터 가리키고 있다 int에.

C#에서 (안전하지 않은) 포인터를 사용해 볼 때까지 포인터를 완전히 파악하지 못했습니다. 포인터는 정확히 동일한 방식으로 작동하지만 논리적이고 일관된 구문으로 작동합니다.포인터는 유형 자체입니다.여기 마이포인터 ~이다 int에 대한 포인터.

  int* mypointer;

함수 포인터에 대해 시작하지도 마십시오 ...

C++만 알았을 때 포인터 작업을 할 수 있었습니다.나는 시행착오를 통해 어떤 경우에는 무엇을 해야 할지, 무엇을 하지 말아야 할지 알고 있었습니다.하지만 나에게 완전한 이해를 준 것은 어셈블리 언어였습니다.당신이 작성한 어셈블리 언어 프로그램으로 심각한 명령어 수준의 디버깅을 수행한다면 많은 것을 이해할 수 있을 것입니다.

나는 집 주소 비유를 좋아하지만 항상 주소가 우편함 자체라고 생각했습니다.이렇게 하면 포인터 역참조(메일함 열기) 개념을 시각화할 수 있습니다.

예를 들어 연결된 목록을 따르는 경우:1) 주소로 종이로 시작하여 2) 종이의 주소로 이동 3) 사서함을 열어 다음 주소가있는 새 용지를 찾으십시오.

선형 연결 목록에서 마지막 사서함에는 아무 것도 없습니다(목록 끝).순환 연결 목록에서는 마지막 사서함에 첫 번째 사서함의 주소가 포함됩니다.

3단계에서는 역참조가 발생하고 주소가 유효하지 않을 때 충돌이 발생하거나 잘못되는 단계입니다.잘못된 주소의 우편함으로 걸어갈 수 있다고 가정하면, 거기에 세상을 뒤집는 블랙홀이나 무언가가 있다고 상상해 보세요 :)

사람들이 어려움을 겪는 주된 이유는 일반적으로 흥미롭고 매력적인 방식으로 가르치지 않기 때문이라고 생각합니다.나는 강사가 군중 중에서 10명의 자원자를 뽑아 각각 1미터짜리 자를 주고 그들이 특정 구성으로 서서 자를 사용하여 서로를 가리키게 하는 것을 보고 싶습니다.그런 다음 사람들을 움직여(그리고 눈금자를 가리키는 곳을) 포인터 연산을 보여줍니다.이는 역학에 너무 얽매이지 않고 개념을 보여주는 간단하지만 효과적인(그리고 무엇보다도 기억에 남는) 방법이 될 것입니다.

C와 C++에 익숙해지면 일부 사람들에게는 더 어려워지는 것 같습니다.이것이 그들이 제대로 파악하지 못한 이론을 마침내 실천에 옮기기 때문인지, 아니면 포인터 조작이 본질적으로 해당 언어에서 더 어렵기 때문인지는 확실하지 않습니다.나는 내 자신의 전환을 잘 기억하지 못하지만, 알고 있었다 Pascal에서 포인터를 사용하다가 C로 옮겨서 완전히 길을 잃었습니다.

나는 포인터 자체가 혼란스럽다고 생각하지 않습니다.대부분의 사람들은 그 개념을 이해할 수 있습니다.이제 얼마나 많은 포인터를 생각할 수 있는지 또는 얼마나 많은 수준의 간접 지시가 편한지 생각해 보세요.사람들을 한계에 두는 데 너무 많은 시간이 걸리지 않습니다.프로그램의 버그로 인해 실수로 변경될 수 있다는 사실로 인해 코드에 문제가 있을 때 디버그하기가 매우 어려워질 수도 있습니다.

실제로 구문 문제일 수도 있다고 생각합니다.포인터에 대한 C/C++ 구문은 일관성이 없고 필요한 것보다 더 복잡해 보입니다.

아이러니하게도 포인터를 이해하는 데 실제로 도움이 된 것은 C++에서 반복자 개념을 접한 것이었습니다. 표준 템플릿 라이브러리.반복자가 포인터의 일반화로 생각되었다고만 가정할 수 있기 때문에 아이러니합니다.

때때로 당신은 나무를 무시하는 법을 배울 때까지 숲을 볼 수 없습니다.

혼란은 "포인터" 개념에 함께 혼합된 여러 추상화 계층에서 비롯됩니다.프로그래머는 Java/Python의 일반적인 참조와 혼동하지 않지만 포인터는 기본 메모리 아키텍처의 특성을 노출한다는 점에서 다릅니다.

추상화 레이어를 깔끔하게 분리하는 것이 좋은 원칙이지만 포인터는 그렇게 하지 않습니다.

제가 즐겨 설명하는 방식은 배열과 인덱스에 관한 것이었습니다. 사람들은 포인터에 익숙하지 않을 수도 있지만 일반적으로 인덱스가 무엇인지 알고 있습니다.

따라서 RAM이 배열이라고 상상해 보십시오(그리고 RAM은 10바이트만 있습니다).

unsigned char RAM[10] = { 10, 14, 4, 3, 2, 1, 20, 19, 50, 9 };

그러면 변수에 대한 포인터는 실제로 RAM에 있는 해당 변수의 인덱스(첫 번째 바이트)일 뿐입니다.

따라서 포인터/인덱스가 있는 경우 unsigned char index = 2, 이면 값은 분명히 세 번째 요소 또는 숫자 4입니다.포인터에 대한 포인터는 해당 숫자를 가져와서 인덱스 자체로 사용하는 곳입니다. RAM[RAM[index]].

나는 종이 목록에 배열을 그리고 그것을 사용하여 동일한 메모리를 가리키는 많은 포인터, 포인터 산술, 포인터 대 포인터 등과 같은 것을 표시합니다.

우체국 사서함 번호입니다.

이는 다른 것에 접근할 수 있게 해주는 정보입니다.

(그리고 우체국 사서함 번호를 계산하면 편지가 잘못된 상자에 들어가기 때문에 문제가 발생할 수 있습니다.그리고 누군가 전달 주소가 없는 다른 상태로 이동하면 포인터가 매달립니다.반면에 우체국이 메일을 전달하는 경우 포인터에 대한 포인터가 있습니다.)

반복자를 통해 그것을 파악하는 나쁜 방법은 아닙니다.하지만 계속 살펴보면 Alexandrescu가 그들에 대해 불평하기 시작하는 것을 볼 수 있습니다.

많은 전직 C++ 개발자(언어를 덤프하기 전에 반복자가 최신 포인터라는 사실을 전혀 이해하지 못한)는 C#으로 이동하면서도 여전히 괜찮은 반복자가 있다고 믿습니다.

흠, 문제는 모든 반복자가 런타임 플랫폼(Java/CLR)이 달성하려는 목표와 완전히 일치하지 않는다는 것입니다.새롭고 간단하며 모두가 개발자입니다.좋을 수도 있지만 보라색 책에서 한 번 말했고 C 이전에도 말했습니다.

우회.

매우 강력한 개념이지만 끝까지 수행하면 결코 그렇지 않습니다..반복자는 또 다른 예인 알고리즘의 추상화에 도움이 되므로 유용합니다.그리고 컴파일 타임은 매우 간단한 알고리즘을 위한 장소입니다.코드 + 데이터 또는 다른 언어 C#을 알고 있습니다.

IEnumerable + LINQ + Massive Framework = 참조 유형의 인스턴스 힙을 통해 앱을 드래그하는 형편없는 간접 참조로 300MB의 런타임 페널티..

"르 포인터는 싸다."

위의 일부 답변은 "포인터는 정말 어렵지 않다"고 주장했다. 그러나 "포인터가 어려운 곳"을 직접 해결하지는 않았다. 에서 오는.몇 년 전 저는 CS 1학년 학생들을 가르쳤습니다(분명히 푹 빠져 있었기 때문에 단 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++ 초보자는 동일한 문제를 안고 있으며 그 문제는 "포인터를 배우기 어렵기 때문"이 아니라 "포인터가 누구에게 어떻게 설명되는지" 때문에 발생합니다.일부 학습자는 이를 시각적으로 또는 말로 수집하며 이를 설명하는 가장 좋은 방법은 다음을 사용하는 것입니다. "기차" 예 (구두 및 시각적 예에 적합)

어디 "기관차" 포인터는 할 수 없다 무엇이든 잡고 "왜건" "기관차"가 당기거나 가리키는 것입니다.그런 다음 "마차" 자체를 분류할 수 있으며 동물, 식물 또는 사람(또는 이들의 혼합)을 수용할 수 있습니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top