Tôi hiểu con trỏ ngăn xếp là gì - nhưng nó được dùng để làm gì?


11

Con trỏ ngăn xếp chỉ vào đỉnh của ngăn xếp, nơi lưu trữ dữ liệu trên cơ sở chúng ta gọi là cơ sở "LIFO". Để đánh cắp sự tương tự của người khác, nó giống như một chồng các món ăn mà bạn đặt và lấy các món ăn ở trên cùng. Con trỏ ngăn xếp, OTOH, trỏ đến "món ăn" trên cùng của ngăn xếp. Ít nhất, điều đó đúng với x86.

Nhưng tại sao máy tính / chương trình "quan tâm" những gì con trỏ ngăn xếp trỏ đến? Nói cách khác, mục đích nào có con trỏ ngăn xếp và biết nơi nó chỉ phục vụ?

Một lời giải thích dễ hiểu bởi các lập trình viên C sẽ được đánh giá cao.


Bởi vì bạn không thể nhìn thấy đỉnh của ngăn xếp trong ram như bạn có thể thấy đỉnh của một chồng đĩa.
tkausl


8
Bạn không lấy một món ăn từ dưới cùng của một chồng. Bạn thêm một cái trên đầu và người khác lấy nó từ đầu . Bạn đang nghĩ về một hàng đợi ở đây.
Kilian Foth

@Snowman Chỉnh sửa của bạn dường như thay đổi ý nghĩa của câu hỏi. moonman239, bạn có thể xác minh xem sự thay đổi của Snowman có chính xác không, cụ thể là việc thêm "mục đích này thực sự phục vụ mục đích gì, trái ngược với việc giải thích cấu trúc của nó?"
8bittree

1
@ 8bittree Vui lòng xem mô tả chỉnh sửa: Tôi đã sao chép câu hỏi như được nêu trong dòng chủ đề vào phần thân của câu hỏi. Tất nhiên, tôi luôn cởi mở với khả năng tôi đã thay đổi một cái gì đó và tác giả ban đầu luôn tự do quay lại hoặc chỉnh sửa bài đăng.

Câu trả lời:


23

Mục đích nào ngăn xếp này thực sự phục vụ, trái ngược với việc giải thích cấu trúc của nó?

Bạn có nhiều câu trả lời mô tả chính xác cấu trúc của dữ liệu được lưu trữ trên ngăn xếp, mà tôi lưu ý là ngược lại với câu hỏi bạn đã hỏi.

Mục đích mà ngăn xếp phục vụ là: ngăn xếp là một phần của sự hợp nhất hóa tiếp tục trong một ngôn ngữ không có coroutines .

Hãy giải nén nó.

Tiếp tục chỉ đơn giản là đặt câu trả lời cho câu hỏi "điều gì sẽ xảy ra tiếp theo trong chương trình của tôi?" Tại mọi thời điểm trong mỗi chương trình, một cái gì đó sẽ xảy ra tiếp theo. Hai toán hạng sẽ được tính toán, sau đó chương trình tiếp tục bằng cách tính tổng của chúng và sau đó chương trình tiếp tục bằng cách gán tổng cho một biến, và sau đó ... và cứ thế.

Reization chỉ là một từ highfalutin để thực hiện cụ thể của một khái niệm trừu tượng. "Chuyện gì xảy ra tiếp theo?" là một khái niệm trừu tượng; cách ngăn xếp được đặt ra là một phần của cách mà khái niệm trừu tượng đó được biến thành một cỗ máy thực sự thực sự tính toán mọi thứ.

Coroutines là các chức năng có thể nhớ vị trí của chúng, kiểm soát năng suất cho một coroutine khác trong một thời gian, và sau đó tiếp tục nơi chúng rời đi sau đó, nhưng không nhất thiết phải ngay sau khi sản lượng coroutine được gọi là. Hãy nghĩ đến "lợi nhuận mang lại" hoặc "chờ đợi" trong C #, phải nhớ vị trí của chúng khi mục tiếp theo được yêu cầu hoặc hoạt động không đồng bộ hoàn tất. Các ngôn ngữ có coroutines hoặc các tính năng ngôn ngữ tương tự đòi hỏi các cấu trúc dữ liệu nâng cao hơn so với ngăn xếp để thực hiện tiếp tục.

Làm thế nào để một ngăn xếp thực hiện tiếp tục? Những câu trả lời khác nói như thế nào. Ngăn xếp lưu trữ (1) giá trị của các biến và thời gian có tuổi thọ được biết là không lớn hơn kích hoạt của phương thức hiện tại và (2) địa chỉ của mã tiếp tục được liên kết với kích hoạt phương thức gần đây nhất. Trong các ngôn ngữ có xử lý ngoại lệ, ngăn xếp cũng có thể lưu trữ thông tin về "tiếp tục lỗi" - đó là, chương trình sẽ làm gì tiếp theo khi xảy ra tình huống đặc biệt.

Hãy để tôi nhân cơ hội này để lưu ý rằng ngăn xếp không cho bạn biết "tôi đến từ đâu?" - mặc dù nó thường được sử dụng trong gỡ lỗi. Ngăn xếp cho bạn biết nơi bạn sẽ đến tiếp theogiá trị của các biến kích hoạt sẽ là gì khi bạn đến đó . Thực tế là trong một ngôn ngữ không có coroutines, nơi bạn sẽ đến gần như luôn luôn là nơi bạn đến từ đó làm cho loại gỡ lỗi này dễ dàng hơn. Nhưng không có yêu cầu rằng trình biên dịch lưu trữ thông tin về nơi kiểm soát đến từ nếu nó có thể thoát khỏi mà không làm như vậy. Tối ưu hóa cuộc gọi đuôi ví dụ phá hủy thông tin về nơi điều khiển chương trình đến từ đâu.

Tại sao chúng ta sử dụng ngăn xếp để thực hiện tiếp tục trong các ngôn ngữ mà không có coroutines? Bởi vì đặc điểm của kích hoạt đồng bộ các phương thức là mô hình "tạm dừng phương thức hiện tại, kích hoạt phương thức khác, tiếp tục phương thức hiện tại biết kết quả của phương thức được kích hoạt" khi được cấu thành một cách hợp lý một chồng các kích hoạt. Tạo một cấu trúc dữ liệu thực hiện hành vi giống như ngăn xếp này là rất rẻ và dễ dàng. Tại sao nó rẻ và dễ dàng? Bởi vì các bộ chip đã được thiết kế đặc biệt trong nhiều thập kỷ để làm cho loại lập trình này dễ dàng cho các nhà văn biên dịch.


Lưu ý rằng trích dẫn mà bạn tham chiếu đã bị thêm nhầm trong một chỉnh sửa bởi một người dùng khác và từ đó đã được sửa, làm cho câu trả lời này không hoàn toàn giải quyết được câu hỏi.
8bittree

2
Tôi khá chắc chắn một lời giải thích được cho là để tăng sự rõ ràng. Tôi không hoàn toàn tin rằng "ngăn xếp là một phần của sự thống nhất tiếp tục trong một ngôn ngữ không có coroutines" thậm chí gần với điều đó :-)

4

Việc sử dụng stack cơ bản nhất là lưu trữ địa chỉ trả về cho các hàm:

void a(){
    sub();
}
void b(){
    sub();
}
void sub() {
    //should i got back to a() or to b()?
}

và từ quan điểm C này là tất cả. Từ quan điểm của trình biên dịch:

  • tất cả các đối số chức năng được thông qua bởi các thanh ghi CPU - nếu không có đủ các thanh ghi, các đối số sẽ được đưa vào ngăn xếp
  • sau khi hàm kết thúc (hầu hết) các thanh ghi sẽ có cùng giá trị như trước khi nhập nó - vì vậy các thanh ghi đã sử dụng được sao lưu trên ngăn xếp

Và theo quan điểm của hệ điều hành: chương trình có thể bị gián đoạn bất cứ lúc nào vì vậy sau khi hoàn thành nhiệm vụ hệ thống, chúng tôi phải khôi phục trạng thái CPU, vì vậy hãy lưu trữ mọi thứ trên ngăn xếp

Tất cả điều này hoạt động vì chúng tôi không quan tâm có bao nhiêu mục đã có trong ngăn xếp hoặc bao nhiêu mục mà người khác sẽ thêm trong tương lai, chúng tôi chỉ cần biết chúng tôi đã di chuyển con trỏ ngăn xếp bao nhiêu và khôi phục nó sau khi hoàn thành.


1
Tôi nghĩ chính xác hơn khi nói rằng các đối số được đẩy trên ngăn xếp, mặc dù thường là một thanh ghi tối ưu hóa được sử dụng thay thế trên các bộ xử lý có đủ các thanh ghi miễn phí cho tác vụ. Đó là một nit, nhưng tôi nghĩ nó phù hợp hơn với cách các ngôn ngữ đã phát triển trong lịch sử. Các trình biên dịch C / C ++ sớm nhất hoàn toàn không sử dụng các thanh ghi cho việc này.
Gort Robot

4

LIFO vs FIFO

LIFO là viết tắt của Last In, First Out. Như trong, mục cuối cùng được đưa vào ngăn xếp là mục đầu tiên được lấy ra khỏi ngăn xếp.

Những gì bạn mô tả với sự tương tự các món ăn của bạn (trong phiên bản đầu tiên ), là một hàng đợi hoặc FIFO, First In, First Out.

Sự khác biệt chính giữa hai loại, đó là LIFO / stack đẩy (chèn) và bật (loại bỏ) từ cùng một đầu, và một hàng đợi / hàng đợi làm như vậy từ các đầu đối diện.

// Both:

Push(a)
-> [a]
Push(b)
-> [a, b]
Push(c)
-> [a, b, c]

// Stack            // Queue
Pop()               Pop()
-> [a, b]           -> [b, c]

Con trỏ ngăn xếp

Chúng ta hãy xem những gì đang xảy ra dưới mui xe của ngăn xếp. Đây là một số bộ nhớ, mỗi hộp là một địa chỉ:

...[ ][ ][ ][ ]...                       char* sp;
    ^- Stack Pointer (SP)

Và có một con trỏ ngăn xếp chỉ xuống dưới cùng của ngăn xếp hiện tại trống rỗng (việc ngăn xếp tăng lên hay phát triển xuống không đặc biệt liên quan ở đây vì vậy chúng tôi sẽ bỏ qua điều đó, nhưng tất nhiên trong thế giới thực, điều đó xác định thao tác nào thêm vào và trừ đi SP).

Vì vậy, hãy đẩy a, b, and cmột lần nữa. Đồ họa ở bên trái, hoạt động "mức cao" ở giữa, mã giả C-ish ở bên phải:

...[a][ ][ ][ ]...        Push('a')      *sp = 'a';
    ^- SP
...[a][ ][ ][ ]...                       ++sp;
       ^- SP

...[a][b][ ][ ]...        Push('b')      *sp = 'b';
       ^- SP
...[a][b][ ][ ]...                       ++sp;
          ^- SP

...[a][b][c][ ]...        Push('c')      *sp = 'c';
          ^- SP
...[a][b][c][ ]...                       ++sp;
             ^- SP

Như bạn có thể thấy, mỗi lần chúng ta push, nó sẽ chèn đối số vào vị trí con trỏ ngăn xếp hiện đang trỏ và điều chỉnh con trỏ ngăn xếp để trỏ đến vị trí tiếp theo.

Bây giờ hãy bật:

...[a][b][c][ ]...        Pop()          --sp;
          ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'c'
          ^- SP
...[a][b][c][ ]...        Pop()          --sp;
       ^- SP
...[a][b][c][ ]...                       return *sp; // returns 'b'
       ^- SP

Popngược lại push, nó điều chỉnh con trỏ ngăn xếp để trỏ đến vị trí trước đó và xóa mục đã có (thường để trả lại cho bất kỳ ai được gọi pop).

Bạn có thể nhận thấy rằng bcvẫn còn trong bộ nhớ. Tôi chỉ muốn đảm bảo với bạn rằng đó không phải là lỗi chính tả. Chúng tôi sẽ trở lại ngay.

Cuộc sống không có con trỏ ngăn xếp

Hãy xem điều gì xảy ra nếu chúng ta không có con trỏ ngăn xếp. Bắt đầu với việc đẩy lại:

...[ ][ ][ ][ ]...
...[ ][ ][ ][ ]...        Push(a)        ? = 'a';

Er, hmm ... nếu chúng ta không có con trỏ ngăn xếp, thì chúng ta không thể di chuyển thứ gì đó đến địa chỉ mà nó trỏ đến. Có lẽ chúng ta có thể sử dụng một con trỏ trỏ đến cơ sở thay vì trên cùng.

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[a][ ][ ][ ]...        Push(a)        *bp = 'a';
    ^- bp
// No stack pointer, so no need to update it.
...[b][ ][ ][ ]...        Push(b)        *bp = 'b';
    ^- bp

À ồ. Vì chúng tôi không thể thay đổi giá trị cố định của cơ sở của ngăn xếp, chúng tôi chỉ ghi đè lên abằng cách đẩy bđến cùng một vị trí.

Chà, tại sao chúng ta không theo dõi số lần chúng ta đã đẩy. Và chúng ta cũng cần theo dõi thời gian chúng ta xuất hiện.

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);
                                         int count = 0;

...[a][ ][ ][ ]...        Push(a)        bp[count] = 'a';
    ^- bp
...[a][ ][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Push(a)        bp[count] = 'b';
    ^- bp
...[a][b][ ][ ]...                       ++count;
    ^- bp
...[a][b][ ][ ]...        Pop()          --count;
    ^- bp
...[a][b][ ][ ]...                       return bp[count]; //returns b
    ^- bp

Chà nó hoạt động, nhưng nó thực sự khá giống với trước đây, ngoại trừ *pointerrẻ hơn pointer[offset](không có số học thêm), chưa kể nó ít để gõ. Điều này có vẻ như là một mất mát đối với tôi.

Hãy thử lại lần nữa. Thay vì sử dụng kiểu chuỗi Pascal để tìm phần cuối của bộ sưu tập dựa trên mảng (theo dõi có bao nhiêu mục trong bộ sưu tập), hãy thử kiểu chuỗi C (quét từ đầu đến cuối):

...[ ][ ][ ][ ]...                       char* bp; // "base pointer"
    ^- bp                                bp = malloc(...);

...[ ][ ][ ][ ]...        Push(a)        char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       *top = 'a';
    ^- bp    ^- top

...[ ][ ][ ][ ]...        Pop()          char* top = bp;
    ^- bp, top
                                         while(*top != 0) { ++top; }
...[ ][ ][ ][a]...                       --top;
    ^- bp       ^- top                   return *top; // returns '('

Bạn có thể đã đoán được vấn đề ở đây. Bộ nhớ chưa được khởi tạo không được đảm bảo là 0. Vì vậy, khi chúng ta tìm kiếm vị trí hàng đầu a, chúng ta sẽ bỏ qua một loạt các vị trí bộ nhớ không sử dụng có rác ngẫu nhiên trong đó. Tương tự như vậy, khi chúng ta quét lên trên cùng, cuối cùng chúng ta sẽ bỏ qua vượt quá mức achúng ta vừa đẩy cho đến khi cuối cùng chúng ta tìm thấy một vị trí bộ nhớ khác xảy ra 0, và di chuyển trở lại và trả lại rác ngẫu nhiên ngay trước đó.

Điều đó đủ dễ để khắc phục, chúng ta chỉ cần thêm các thao tác vào PushPopđể đảm bảo đỉnh của ngăn xếp luôn được cập nhật để được đánh dấu bằng a 0và chúng ta phải khởi tạo ngăn xếp với một đầu cuối như vậy. Tất nhiên, điều đó cũng có nghĩa là chúng ta không thể có 0(hoặc bất kỳ giá trị nào chúng ta chọn làm dấu kết thúc) làm giá trị thực trong ngăn xếp.

Trên hết, chúng tôi cũng đã thay đổi các hoạt động O (1) thành các hoạt động O (n).

TL; DR

Con trỏ ngăn xếp theo dõi đỉnh của ngăn xếp, nơi tất cả các hành động xảy ra. Có nhiều cách để loại bỏ nó ( bp[count]topvề cơ bản vẫn là con trỏ ngăn xếp), nhưng cả hai đều phức tạp và chậm hơn là chỉ có con trỏ ngăn xếp. Và không biết vị trí trên cùng của ngăn xếp có nghĩa là bạn không thể sử dụng ngăn xếp.

Lưu ý: Con trỏ ngăn xếp chỉ vào "đáy" của ngăn xếp thời gian chạy trong x86 có thể là một quan niệm sai lầm liên quan đến toàn bộ ngăn xếp thời gian chạy bị lộn ngược. Nói cách khác, cơ sở của ngăn xếp được đặt tại một địa chỉ bộ nhớ cao và đầu của ngăn xếp phát triển thành các địa chỉ bộ nhớ thấp hơn. Con trỏ ngăn xếp không trỏ đến đỉnh của ngăn xếp nơi tất cả các hành động xảy ra, chỉ là mũi là tại một địa chỉ bộ nhớ thấp hơn so với các cơ sở của ngăn xếp.


2

Con trỏ ngăn xếp được sử dụng (với con trỏ khung) cho ngăn xếp cuộc gọi (theo liên kết đến wikipedia, nơi có một hình ảnh tốt).

Ngăn xếp cuộc gọi chứa các khung cuộc gọi, chứa địa chỉ trả về, các biến cục bộ và dữ liệu cục bộ khác (đặc biệt là nội dung bị tràn của các thanh ghi; biểu mẫu).

Cũng đọc về các cuộc gọi đuôi (một số cuộc gọi đệ quy đuôi không cần bất kỳ khung cuộc gọi nào), xử lý ngoại lệ (như setjmp & longjmp , chúng có thể liên quan đến việc bật nhiều khung ngăn xếp cùng một lúc), tín hiệu & ngắtliên tục . Xem thêm gọi các quy ướcgiao diện nhị phân ứng dụng (ABI), đặc biệt là ABI x86-64 (định nghĩa rằng một số đối số chính thức được thông qua bởi các thanh ghi).

Ngoài ra, mã một số hàm đơn giản trong C, sau đó sử dụng gcc -Wall -O -S -fverbose-asm để biên dịch nó và xem xét .s tệp trình biên dịch được tạo .

Appel đã viết một bài báo cũ năm 1986 tuyên bố rằng bộ sưu tập rác có thể nhanh hơn phân bổ ngăn xếp (sử dụng Kiểu liên tục trong trình biên dịch), nhưng điều này có thể sai trên các bộ xử lý x86 ngày nay (đáng chú ý là do hiệu ứng bộ đệm).

Lưu ý rằng các quy ước gọi, ABI và bố cục ngăn xếp khác nhau trên 32 bit i686 và trên 64 bit x86-64. Ngoài ra, các quy ước gọi (và người chịu trách nhiệm phân bổ hoặc bật khung cuộc gọi) có thể khác với các ngôn ngữ khác nhau (ví dụ: C, Pascal, Ocaml, SBCL Common Lisp có các quy ước gọi khác nhau ....)

BTW, các tiện ích mở rộng x86 gần đây như AVX đang áp đặt các ràng buộc căn chỉnh ngày càng lớn trên con trỏ ngăn xếp (IIRC, khung cuộc gọi trên x86-64 muốn được căn chỉnh thành 16 byte, tức là hai từ hoặc con trỏ).


1
Bạn có thể muốn đề cập rằng việc căn chỉnh tới 16 byte trên x86-64 có nghĩa là gấp đôi kích thước / căn chỉnh của một con trỏ, điều này thực sự thú vị hơn so với đếm byte.
Ded

1

Nói một cách đơn giản, chương trình quan tâm bởi vì nó sử dụng dữ liệu đó và cần theo dõi nơi tìm thấy nó.

Nếu bạn khai báo các biến cục bộ trong một hàm, ngăn xếp là nơi chúng được lưu trữ. Ngoài ra, nếu bạn gọi một chức năng khác, ngăn xếp là nơi lưu trữ địa chỉ trả về để nó có thể quay lại chức năng bạn đã ở khi chức năng bạn gọi đã kết thúc và chọn nơi nó rời đi.

Không có SP, lập trình có cấu trúc như chúng ta biết về cơ bản là không thể. (Bạn có thể làm việc xung quanh việc không có nó, nhưng nó sẽ đòi hỏi phải thực hiện phiên bản của riêng bạn, vì vậy điều đó không có nhiều khác biệt.)


1
Khẳng định của bạn rằng lập trình có cấu trúc mà không có ngăn xếp sẽ là không thể. Các chương trình được biên dịch thành kiểu truyền tiếp tục không tiêu tốn ngăn xếp, nhưng chúng là các chương trình hoàn toàn hợp lý.
Eric Lippert

@EricLippert: Có lẽ đối với các giá trị "hoàn toàn hợp lý", vô nghĩa mà chúng bao gồm đứng trên đầu của một người và xoay người từ trong ra ngoài, có lẽ. ;-)
Mason Wheeler

1
Với việc tiếp tục truyền , có thể không cần một ngăn xếp cuộc gọi nào cả. Thực tế, mỗi cuộc gọi là một cuộc gọi đuôi và goto chứ không phải trả lại. "Khi CPS và TCO loại bỏ khái niệm trả về hàm ẩn, việc sử dụng kết hợp của chúng có thể loại bỏ sự cần thiết của ngăn xếp thời gian chạy."

@MichaelT: Tôi nói "về cơ bản" không thể vì một lý do. Về mặt lý thuyết, CPS có thể thực hiện được điều này, nhưng trong thực tế, việc viết mã trong thế giới thực về bất kỳ sự phức tạp nào trong CPS trở nên vô cùng khó khăn, như Eric đã chỉ ra trong một loạt các bài đăng trên blog về chủ đề này .
Mason Wheeler

1
@MasonWheeler Eric đang nói về các chương trình được biên dịch thành CPS. Ví dụ: trích dẫn blog của Jon Harrop : In fact, some compilers don’t even use stack frames [...], and other compilers like SML/NJ convert every call into continuation style and put stack frames on the heap, splitting every segment of code between a pair of function calls in the source into its own separate function in the compiled form.Điều đó khác với "triển khai phiên bản [ngăn xếp] của riêng bạn".
Doval 4/12/2015

1

Đối với ngăn xếp bộ xử lý trong bộ xử lý x86, sự tương tự của một chồng bát đĩa thực sự không chính xác.
Vì nhiều lý do (chủ yếu là lịch sử), ngăn xếp bộ xử lý phát triển từ đỉnh bộ nhớ xuống phía dưới bộ nhớ, do đó, một sự tương tự tốt hơn sẽ là chuỗi các liên kết chuỗi treo trên trần nhà. Khi đẩy thứ gì đó lên ngăn xếp, một liên kết chuỗi sẽ được thêm vào liên kết thấp nhất.

Con trỏ ngăn xếp đề cập đến liên kết thấp nhất của chuỗi và được bộ xử lý sử dụng để "xem" vị trí của liên kết thấp nhất đó, để các liên kết có thể được thêm hoặc xóa mà không phải di chuyển toàn bộ chuỗi từ trần xuống.

Theo một nghĩa nào đó, bên trong bộ xử lý x86, ngăn xếp lộn ngược nhưng ngưỡng thuật ngữ ngăn xếp thông thường được sử dụng, do đó liên kết thấp nhất được gọi là đỉnh của ngăn xếp.


Các liên kết chuỗi mà tôi đã đề cập ở trên thực sự là các ô nhớ trong máy tính và chúng được sử dụng để lưu trữ các biến cục bộ và một số kết quả tính toán trung gian. Các chương trình máy tính quan tâm vị trí trên cùng của ngăn xếp (nghĩa là nơi liên kết thấp nhất bị treo), bởi vì phần lớn các biến mà hàm cần truy cập tồn tại gần với nơi con trỏ ngăn xếp đề cập đến và việc truy cập nhanh vào chúng là mong muốn.


1
The stack pointer refers to the lowest link of the chain and is used by the processor to "see" where that lowest link is, so that links can be added or removed without having to travel the entire chain from the ceiling down.Tôi không chắc đây là một sự tương tự tốt. Trong liên kết thực tế không bao giờ được thêm hoặc loại bỏ. Con trỏ ngăn xếp giống như một chút băng bạn sử dụng để đánh dấu một trong các liên kết. Nếu bạn bị mất băng đó, bạn sẽ không có một cách nào để biết đó là từ dưới hầu hết các liên kết mà bạn sử dụng ở tất cả ; du lịch chuỗi từ trần xuống sẽ không giúp bạn.
Doval 4/12/2015

Vì vậy, con trỏ ngăn xếp cung cấp một điểm tham chiếu mà chương trình / máy tính có thể sử dụng để tìm các biến cục bộ của hàm?
moonman239

Nếu đó là trường hợp, thì làm thế nào để máy tính tìm thấy các biến cục bộ? Có phải nó chỉ cần tìm kiếm mọi địa chỉ bộ nhớ từ dưới lên?
moonman239

@ moonman239: Không, khi biên dịch, trình biên dịch sẽ theo dõi nơi mỗi biến được lưu trữ liên quan đến con trỏ ngăn xếp. Bộ xử lý hiểu địa chỉ tương đối như vậy để cấp quyền truy cập trực tiếp vào các biến.
Bart van Ingen Schenau

1
@BartvanIngenSchenau À, được rồi. Kiểu như khi bạn ở giữa một nơi xa xôi và bạn cần sự giúp đỡ, vì vậy bạn đưa ra cho 911 ý tưởng về nơi bạn có liên quan đến cột mốc. Con trỏ ngăn xếp, trong trường hợp này, thường là "mốc" gần nhất và do đó, có lẽ, là điểm tham chiếu tốt nhất.
moonman239

1

Câu trả lời này đề cập cụ thể tới các con trỏ ngăn xếp của thread hiện hành (thực hiện) .

Trong các ngôn ngữ lập trình thủ tục, một luồng thường có quyền truy cập vào ngăn xếp 1 cho các mục đích sau:

  • Kiểm soát luồng, cụ thể là "ngăn xếp cuộc gọi".
    • Khi một chức năng gọi một chức năng khác, ngăn xếp cuộc gọi sẽ nhớ nơi quay trở lại.
    • Một ngăn xếp cuộc gọi là cần thiết bởi vì đây là cách chúng tôi muốn "chức năng gọi" hoạt động - "chọn nơi chúng tôi rời đi" .
    • Có các kiểu lập trình khác không có lệnh gọi hàm ở giữa thực thi (ví dụ: chỉ được phép chỉ định hàm tiếp theo khi kết thúc hàm hiện tại) hoặc không có lệnh gọi hàm nào cả (chỉ sử dụng lệnh nhảy goto và có điều kiện ). Những kiểu lập trình này có thể không cần ngăn xếp cuộc gọi.
  • Chức năng gọi tham số.
    • Khi một chức năng gọi một chức năng khác, các tham số có thể được đẩy lên ngăn xếp.
    • Người gọi và callee cần phải tuân theo quy ước tương tự như người chịu trách nhiệm xóa các tham số khỏi ngăn xếp, khi cuộc gọi kết thúc.
  • Các biến cục bộ sống trong một lệnh gọi hàm.
    • Lưu ý rằng một biến cục bộ thuộc về một người gọi có thể được truy cập tới một callee bằng cách chuyển một con trỏ tới biến cục bộ đó tới callee.

Lưu ý 1 : dành riêng cho việc sử dụng của chủ đề, mặc dù nội dung của nó hoàn toàn có thể đọc được - và có thể phá vỡ - bởi các chủ đề khác.

Trong lập trình lắp ráp, C và C ++, cả ba mục đích có thể được thực hiện bằng cùng một ngăn xếp. Trong một số ngôn ngữ khác, một số mục đích có thể được thực hiện bằng các ngăn xếp riêng biệt hoặc bộ nhớ được phân bổ động.


1

Đây là một phiên bản cố ý quá mức của những gì ngăn xếp được sử dụng cho.

Hãy tưởng tượng ngăn xếp như một đống thẻ chỉ mục. Con trỏ ngăn xếp chỉ vào thẻ trên cùng.

Khi bạn gọi một chức năng:

  • Bạn viết địa chỉ của mã ngay sau dòng gọi hàm trên thẻ và đặt nó vào đống. (Tức là bạn tăng con trỏ ngăn xếp lên một và ghi địa chỉ vào nơi nó trỏ đến)
  • Sau đó, bạn ghi lại các giá trị có trong sổ đăng ký vào một số thẻ và đặt chúng vào đống. (tức là bạn tăng con trỏ ngăn xếp theo số lượng thanh ghi và sao chép nội dung thanh ghi vào vị trí nó trỏ tới)
  • Sau đó, bạn đặt một thẻ đánh dấu trên đống. (tức là bạn lưu con trỏ ngăn xếp hiện tại.)
  • Sau đó, bạn viết giá trị của từng tham số mà hàm được gọi với, một đến một thẻ và đặt nó vào đống. (tức là bạn tăng con trỏ ngăn xếp theo số lượng tham số và ghi các tham số vào vị trí con trỏ ngăn xếp trỏ đến.)
  • Sau đó, bạn thêm một thẻ cho mỗi biến cục bộ, có khả năng ghi giá trị ban đầu vào nó. (tức là bạn tăng con trỏ ngăn xếp theo số lượng biến cục bộ.)

Tại thời điểm này, mã trong hàm chạy. Mã được biên dịch để biết nơi mỗi thẻ có liên quan đến đầu. Vì vậy, nó biết rằng biến xlà thẻ thứ ba từ trên cùng (tức là con trỏ ngăn xếp - 3) và tham số ylà thẻ thứ sáu từ trên xuống (tức là con trỏ ngăn xếp - 6.)

Phương pháp này có nghĩa là địa chỉ của từng biến hoặc tham số cục bộ không cần phải được nướng vào mã. Thay vào đó, tất cả các mục dữ liệu này được xử lý liên quan đến con trỏ ngăn xếp.

Khi hàm trả về, thao tác ngược lại chỉ đơn giản là:

  • Tìm kiếm thẻ đánh dấu và ném tất cả các thẻ trên nó đi. (tức là đặt con trỏ ngăn xếp đến địa chỉ đã lưu.)
  • Khôi phục các thanh ghi từ các thẻ đã lưu trước đó và ném chúng đi. (tức là trừ một giá trị cố định từ con trỏ ngăn xếp)
  • Bắt đầu chạy mã từ địa chỉ trên thẻ trên đầu và sau đó ném nó đi. (tức là trừ 1 từ con trỏ ngăn xếp.)

Ngăn xếp bây giờ trở lại trạng thái trước khi hàm được gọi.

Khi bạn xem xét điều này, hãy lưu ý hai điều: phân bổ và phân bổ địa phương là một thao tác cực kỳ nhanh vì nó chỉ cần thêm một số vào hoặc trừ một số từ con trỏ ngăn xếp. Cũng lưu ý làm thế nào tự nhiên điều này làm việc với đệ quy.

Điều này là quá đơn giản cho mục đích giải thích. Trong thực tế, các tham số và cục bộ có thể được đặt trong các thanh ghi dưới dạng tối ưu hóa và con trỏ ngăn xếp thường sẽ được tăng và giảm theo kích thước từ của máy chứ không phải một. (Để đặt tên cho một vài điều.)


1

Các ngôn ngữ lập trình hiện đại, như bạn đã biết, hỗ trợ khái niệm các lệnh gọi chương trình con (thường được gọi là "các hàm gọi"). Điều này có nghĩa rằng:

  1. Ở giữa một số mã, bạn có thể gọi một số chức năng khác trong chương trình của bạn;
  2. Hàm đó không biết rõ nó được gọi từ đâu;
  3. Tuy nhiên, khi công việc của nó được thực hiện và nó return, điều khiển quay trở lại điểm chính xác nơi cuộc gọi được bắt đầu, với tất cả các giá trị biến cục bộ có hiệu lực như khi cuộc gọi được bắt đầu.

Làm thế nào để máy tính theo dõi điều đó? Nó duy trì một bản ghi liên tục về các chức năng đang chờ đợi các cuộc gọi trở lại. Bản ghi này là một stack stack và vì nó là một thứ quan trọng như vậy, nên chúng ta thường gọi nó stack.

Và vì mẫu cuộc gọi / trả lại này rất quan trọng, CPU từ lâu đã được thiết kế để cung cấp hỗ trợ phần cứng đặc biệt cho nó. Con trỏ ngăn xếp là một tính năng phần cứng trong CPU. Một thanh ghi dành riêng để theo dõi đỉnh ngăn xếp và được các hướng dẫn của CPU sử dụng để phân nhánh vào chương trình con và quay trở lại từ nó.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.