Tại sao C / C ++ chính argv được khai báo là Hồi char * argv [] và thay vì chỉ là char char * argv?


21

Tại sao được argvkhai báo là "con trỏ trỏ đến chỉ mục đầu tiên của mảng", thay vì chỉ là "con trỏ tới chỉ mục đầu tiên của mảng" ( char* argv)?

Tại sao khái niệm "con trỏ đến con trỏ" được yêu cầu ở đây?


4
"một con trỏ tới con trỏ tới chỉ mục đầu tiên của mảng" - Đó không phải là một mô tả chính xác về char* argv[]hoặc char**. Đó là một con trỏ tới một con trỏ tới một ký tự; cụ thể là con trỏ bên ngoài trỏ đến con trỏ đầu tiên trong một mảng và các con trỏ bên trong trỏ đến các ký tự đầu tiên của các chuỗi kết thúc nul. Không có chỉ số liên quan ở đây.
Sebastian Redl

12
Làm thế nào bạn có được đối số thứ hai nếu nó chỉ là char * argv?
gnasher729

15
Cuộc sống của bạn sẽ trở nên dễ dàng hơn khi bạn đặt không gian đúng chỗ. char* argv[]đặt không gian sai vị trí Nói char *argv[], và bây giờ rõ ràng điều này có nghĩa là "biểu thức *argv[n]là một biến loại char". Đừng để bị cuốn vào việc cố gắng tìm ra con trỏ là gì và con trỏ của con trỏ là gì, v.v. Tuyên bố là cho bạn biết những hoạt động bạn có thể thực hiện trên điều này.
Eric Lippert

1
Tinh thần so sánh char * argv[]với cấu trúc C ++ tương tự std::string argv[], và có thể dễ dàng phân tích cú pháp hơn. ... Đừng bắt đầu thực sự viết nó theo cách đó!
Justin Time 2 Tái lập lại

2
@EricLippert lưu ý rằng câu hỏi cũng bao gồm C ++ và ở đó bạn có thể có ví dụ như char &func(int);không &func(5)có loại nào char.
Ruslan

Câu trả lời:


59

Argv về cơ bản là như thế này:

nhập mô tả hình ảnh ở đây

Bên trái là chính đối số - những gì thực sự được chuyển qua làm đối số cho chính. Nó chứa địa chỉ của một mảng các con trỏ. Mỗi điểm đó đến một nơi nào đó trong bộ nhớ chứa văn bản của đối số tương ứng được truyền trên dòng lệnh. Sau đó, ở cuối mảng đó, đảm bảo là một con trỏ null.

Lưu ý rằng bộ lưu trữ thực tế cho các đối số riêng lẻ ít nhất có khả năng được phân bổ riêng biệt với nhau, do đó địa chỉ của chúng trong bộ nhớ có thể được sắp xếp khá ngẫu nhiên (nhưng tùy thuộc vào cách mọi thứ được viết, chúng cũng có thể nằm trong một khối liền kề duy nhất bộ nhớ - đơn giản là bạn không biết và không nên quan tâm).


52
Bất cứ công cụ bố trí nào đã vẽ sơ đồ đó cho bạn đều có lỗi trong thuật toán thu nhỏ chéo của chúng!
Eric Lippert

43
@EricLippert Có thể cố ý nhấn mạnh rằng các điểm có thể không liền kề cũng không theo thứ tự.
jamesdlin

3
Tôi muốn nói đó là cố ý
Michael

24
Đó chắc chắn là có chủ ý - và tôi đoán Eric có thể đã hiểu điều đó, nhưng (chính xác là IMO) nghĩ rằng bình luận đó là buồn cười.
Jerry Coffin

2
@JerryCoffin, người ta cũng có thể chỉ ra rằng ngay cả khi các đối số thực tế nằm liền kề trong bộ nhớ, chúng có thể có độ dài tùy ý, do đó, người ta vẫn cần các con trỏ riêng biệt để có thể truy cập argv[i]mà không cần quét qua tất cả các đối số trước đó.
ilkkachu

22

Bởi vì đó là những gì hệ điều hành cung cấp :-)

Câu hỏi của bạn là một chút vấn đề đảo ngược gà / trứng. Vấn đề không phải là chọn những gì bạn muốn trong C ++, vấn đề là cách bạn nói trong C ++ những gì HĐH mang lại cho bạn.

Unix vượt qua một mảng "chuỗi", mỗi chuỗi là một đối số lệnh. Trong C / C ++, một chuỗi là "char *", do đó, một chuỗi các chuỗi là char * argv [] hoặc char ** argv, theo sở thích.


13
Không, đó chính xác là "vấn đề chọn những gì bạn muốn trong C ++". Ví dụ, Windows cung cấp dòng lệnh dưới dạng một chuỗi và các chương trình C / C ++ vẫn nhận được argvmảng của chúng - bộ thực thi đảm nhiệm việc token hóa dòng lệnh và xây dựng argvmảng khi khởi động.
Joker_vD

14
@Joker_vD Tôi nghĩ theo một cách xoắn, đó về những gì HĐH mang lại cho bạn. Cụ thể: Tôi đoán C ++ đã làm theo cách này vì C đã làm theo cách này và C đã làm theo cách này bởi vì tại thời điểm đó, C và Unix đã liên kết chặt chẽ với nhau và Unix đã làm theo cách này.
Daniel Wagner

1
@DanielWagner: Vâng, đây là từ di sản Unix của C. Trên Unix / Linux, tối thiểu _startcác cuộc gọi mainchỉ cần chuyển mainmột con trỏ đến argvmảng hiện có trong bộ nhớ; Nó đã ở đúng định dạng. Nhân sao chép nó từ đối số argv sang lệnh execve(const char *filename, char *const argv[], char *const envp[])gọi hệ thống đã được thực hiện để bắt đầu một tệp thực thi mới. (Trên Linux, argv [] (chính mảng) và argc nằm trên ngăn xếp khi nhập quy trình. Tôi cho rằng hầu hết các Unix đều giống nhau, vì đó là một nơi tốt cho nó.)
Peter Cordes

8
Nhưng quan điểm của Joker ở đây là các tiêu chuẩn C / C ++ để lại cho việc triển khai các đối số đến từ đâu; họ không cần phải đi thẳng từ hệ điều hành. Trên một hệ điều hành vượt qua chuỗi phẳng, việc triển khai C ++ tốt phải bao gồm mã thông báo, thay vì cài đặt argc=2và chuyển toàn bộ chuỗi phẳng. (Tiếp theo bức thư của tiêu chuẩn là không đủ để thể hữu ích , nó cố tình để lại rất nhiều chỗ cho sự lựa chọn thực hiện.) Mặc dù một số chương trình Windows sẽ muốn trích dẫn điều trị đặc biệt, triển khai rất thật làm cung cấp một cách để có được chuỗi bằng phẳng, quá
Peter Cordes

1
Câu trả lời của Basile là khá nhiều điều này + @ Joker chỉnh sửa và nhận xét của tôi, với nhiều chi tiết hơn.
Peter Cordes

15

Đầu tiên, như một khai báo tham số, char **argvgiống như char *argv[]; cả hai đều ngụ ý một con trỏ tới (một mảng hoặc tập hợp một hoặc nhiều con trỏ) có thể thành chuỗi.

Tiếp theo, nếu bạn chỉ có "con trỏ tới char" - ví dụ như char *- thì để truy cập mục thứ n, bạn sẽ phải quét các mục n-1 đầu tiên để tìm mục bắt đầu của mục thứ n. (Và điều này cũng sẽ áp đặt yêu cầu rằng mỗi chuỗi được lưu trữ liên tục.)

Với mảng con trỏ, bạn có thể lập chỉ mục trực tiếp cho mục thứ n - vì vậy (trong khi không thực sự cần thiết - giả sử các chuỗi là liền kề), nó thường thuận tiện hơn nhiều.

Để minh họa:

./program xin chào thế giới

argc = 3
argv[0] --> "./program\0"
argv[1] --> "hello\0"
argv[2] --> "world\0"

Có thể là, trong một mảng các ký tự được cung cấp bởi os:

            "./program\0hello\0world\0"
argv[0]      ^
argv[1]                 ^
argv[2]                        ^

nếu argv chỉ là một "con trỏ tới char" bạn có thể thấy

       "./program\0hello\0world\0"
argv    ^

Tuy nhiên (mặc dù có thể theo thiết kế của os) không có gì đảm bảo thực sự rằng ba chuỗi "./program", "hello" và "world" là liền kề nhau. Hơn nữa, loại "con trỏ đơn tới nhiều chuỗi liền kề" này là cấu trúc kiểu dữ liệu khác thường hơn (đối với C), đặc biệt là so với mảng con trỏ tới chuỗi.


Điều gì nếu thay vì, argv --> "hello\0world\0"bạn có argv --> index 0 of the array(xin chào), giống như một mảng bình thường. Tại sao điều này không thể thực hiện được? sau đó bạn tiếp tục đọc argcthời gian mảng . sau đó bạn truyền argv chính nó và không phải là một con trỏ để argv.
một người dùng

@auser, đó là những gì argv -> "./program\0hello\0\world\0" là: một con trỏ tới char đầu tiên (tức là ".") Nếu bạn đưa con trỏ đó qua \ 0 đầu tiên, thì bạn có một con trỏ tới "hello \ 0" và sau đó là "world \ 0". Sau thời gian argc (nhấn \ 0 "), bạn đã hoàn thành. Chắc chắn, nó có thể được thực hiện để hoạt động, và như tôi đã nói, một cấu trúc khác thường.
Erik Eidt

Bạn đã quên nói rằng trong ví dụ của bạn argv[4]NULL
Basile Starynkevitch

3
Có một đảm bảo rằng (ít nhất là ban đầu) argv[argc] == NULL. Trong trường hợp này argv[3], không argv[4].
Miral

1
@Hill, vâng, cảm ơn bạn vì tôi đã cố gắng nói rõ ràng về các dấu chấm hết ký tự null (và bỏ lỡ cái đó).
Erik Eidt

13

Tại sao argv chính của C / C ++ được khai báo là đá char * argv []

Một câu trả lời có thể là bởi vì tiêu chuẩn C11 n1570 (trong khởi động chương trình §5.1.2.2.1 ) và tiêu chuẩn C ++ 11 n3337 (trong chức năng chính §3.6.1 ) yêu cầu điều đó đối với môi trường được lưu trữ (nhưng lưu ý rằng tiêu chuẩn C đề cập cũng §5.1.2.1 môi trường tự do ) Xem thêm điều này .

Câu hỏi tiếp theo là tại sao các tiêu chuẩn C và C ++ lại chọn mainint main(int argc, char**argv)chữ ký như vậy ? Lời giải thích là phần lớn lịch sử: C được phát minh với Unix , trong đó có một vỏglobbing trước khi thực hiện fork(mà là một cuộc gọi hệ thống để tạo ra một quá trình) và execve(đó là cuộc gọi hệ thống để thực hiện một chương trình), và điều đó execveđã truyền một mảng của các đối số chương trình chuỗi và có liên quan đến mainchương trình được thực hiện. Đọc thêm về triết lý Unix và về ABI .

Và C ++ đã cố gắng làm theo các quy ước của C và tương thích với nó. Nó không thể xác định mainlà không tương thích với truyền thống C.

Nếu bạn đã thiết kế một hệ điều hành từ đầu (vẫn có giao diện dòng lệnh) và ngôn ngữ lập trình cho nó từ đầu, bạn sẽ được tự do phát minh ra các quy ước bắt đầu chương trình khác nhau. Và các ngôn ngữ lập trình khác (ví dụ Common Lisp hoặc Ocaml hoặc Go) có các quy ước bắt đầu chương trình khác nhau.

Trong thực tế, mainđược gọi bởi một số mã crt0 . Lưu ý rằng trên Windows, việc tạo hình có thể được thực hiện bởi mỗi chương trình tương đương với crt0 và một số chương trình Windows có thể bắt đầu thông qua điểm nhập WinMain không chuẩn . Trên Unix, globalbing được thực hiện bởi shell (và crt0đang điều chỉnh ABI và bố trí ngăn xếp cuộc gọi ban đầu mà nó đã chỉ định, để gọi các quy ước về việc thực hiện C của bạn).


12

Thay vì nghĩ về nó như là "con trỏ tới con trỏ", nó giúp nghĩ về nó như là "mảng của chuỗi", với []biểu thị mảng và char*biểu thị chuỗi. Khi bạn chạy một chương trình, bạn có thể truyền cho nó một hoặc nhiều đối số dòng lệnh và chúng được phản ánh trong các đối số thành main: argclà số lượng đối số và argvcho phép bạn truy cập các đối số riêng lẻ.


2
+1 này! Trong nhiều ngôn ngữ - bash, PHP, C, C ++ - argv là một chuỗi các chuỗi. Về điều này bạn phải suy nghĩ khi bạn nhìn thấy char **hoặc char *[], đó là như nhau.
rexkogitans

1

Trong nhiều trường hợp, câu trả lời là "bởi vì đó là một tiêu chuẩn". Để báo giá tiêu chuẩn C99 :

- Nếu giá trị của argc lớn hơn 0, các thành viên mảng argv [0] đến bao gồm argv [argc-1] sẽ chứa các con trỏ tới các chuỗi , được đưa ra các giá trị được xác định bởi môi trường máy chủ trước khi khởi động chương trình.

Tất nhiên, trước khi nó được chuẩn hóa, nó đã được K & R C sử dụng trong các triển khai Unix ban đầu, với mục đích lưu trữ các tham số dòng lệnh (thứ bạn phải quan tâm trong hệ vỏ Unix như /bin/bashhoặc /bin/shkhông phải trong các hệ thống nhúng). Để trích dẫn phiên bản đầu tiên của "Ngôn ngữ lập trình C" của K & R (trang 110) :

Đầu tiên (thông thường được gọi là argc ) là số lượng đối số dòng lệnh mà chương trình được gọi với; thứ hai ( argv ) là một con trỏ tới một chuỗi các chuỗi ký tự có chứa các đối số, mỗi chuỗi trên một chuỗi.

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.