Phương thức main () hoạt động như thế nào trong C?


96

Tôi biết có hai chữ ký khác nhau để viết phương thức chính -

int main()
{
   //Code
}

hoặc để xử lý đối số dòng lệnh, chúng tôi viết nó là-

int main(int argc, char * argv[])
{
   //code
}

Trong C++tôi biết chúng tôi có thể quá tải một phương pháp, nhưng trong Clàm thế nào để trình biên dịch xử lý hai chữ ký khác nhau của mainchức năng?


14
Quá tải đề cập đến việc có hai phương thức có cùng tên trong cùng một chương trình. Bạn chỉ có thể có một mainphương thức trong một chương trình duy nhất trong C(hoặc, thực sự, bằng khá nhiều ngôn ngữ có cấu trúc như vậy).
Kyle Strand

12
C không có phương thức; nó có các chức năng. Các phương thức là sự thực thi phía sau của các hàm "chung chung" hướng đối tượng. Chương trình gọi một hàm với một số đối số đối tượng và hệ thống đối tượng chọn một phương thức (hoặc có thể là một tập hợp các phương thức) dựa trên kiểu của chúng. C không có bất kỳ thứ nào trong số này trừ khi bạn tự mô phỏng nó.
Kaz

4
Để có một cuộc thảo luận sâu sắc về các điểm đầu vào của chương trình - không phải đặc biệt main- tôi giới thiệu cuốn sách kinh điển của John R. Levines "Người liên kết & người tải".
Andreas Spindler, 17/10/13

1
Trong C, biểu mẫu đầu tiên là int main(void)không int main()(mặc dù tôi chưa bao giờ thấy một trình biên dịch nào từ chối int main()biểu mẫu).
Keith Thompson

1
@harper: Biểu ()mẫu đã lỗi thời và không rõ ràng rằng nó thậm chí còn được cho phép main(trừ khi việc triển khai cụ thể hóa nó như một biểu mẫu được phép). Tiêu chuẩn C (xem 5.1.2.2.1 Khởi động chương trình) không đề cập đến ()biểu mẫu, không hoàn toàn tương đương với ()biểu mẫu. Chi tiết quá dài cho nhận xét này.
Keith Thompson

Câu trả lời:


132

Một số tính năng của ngôn ngữ C bắt đầu như là các bản hack đã xảy ra.

Nhiều chữ ký cho danh sách đối số chính, cũng như danh sách đối số có độ dài thay đổi, là một trong những tính năng đó.

Các lập trình viên nhận thấy rằng họ có thể truyền các đối số bổ sung cho một hàm và không có gì xấu xảy ra với trình biên dịch đã cho của họ.

Đây là trường hợp nếu các quy ước gọi như vậy:

  1. Hàm gọi xóa các đối số.
  2. Các đối số ngoài cùng bên trái gần với đầu ngăn xếp hơn hoặc với cơ sở của khung ngăn xếp, để các đối số giả không làm mất hiệu lực của địa chỉ.

Một tập hợp các quy ước gọi tuân theo các quy tắc này là tham số dựa trên ngăn xếp truyền theo đó trình gọi bật các đối số và chúng được đẩy từ phải sang trái:

 ;; pseudo-assembly-language
 ;; main(argc, argv, envp); call

 push envp  ;; rightmost argument
 push argv  ;; 
 push argc  ;; leftmost argument ends up on top of stack

 call main

 pop        ;; caller cleans up   
 pop
 pop

Trong các trình biên dịch mà loại quy ước gọi này là trường hợp, không cần thực hiện gì đặc biệt để hỗ trợ hai loại mainhoặc thậm chí là các loại bổ sung. maincó thể là một hàm không có đối số, trong trường hợp đó, nó không để ý đến các mục đã được đẩy lên ngăn xếp. Nếu đó là một hàm của hai đối số, thì nó sẽ tìm argcargvlà hai mục ngăn xếp trên cùng. Nếu đó là biến thể ba đối số dành riêng cho nền tảng với con trỏ môi trường (tiện ích mở rộng chung), thì điều đó cũng sẽ hoạt động: nó sẽ tìm đối số thứ ba đó là phần tử thứ ba từ đầu ngăn xếp.

Và do đó, một cuộc gọi cố định hoạt động cho mọi trường hợp, cho phép một mô-đun khởi động cố định duy nhất được liên kết với chương trình. Mô-đun đó có thể được viết bằng C, dưới dạng một hàm tương tự như sau:

/* I'm adding envp to show that even a popular platform-specific variant
   can be handled. */
extern int main(int argc, char **argv, char **envp);

void __start(void)
{
  /* This is the real startup function for the executable.
     It performs a bunch of library initialization. */

  /* ... */

  /* And then: */
  exit(main(argc_from_somewhere, argv_from_somewhere, envp_from_somewhere));
}

Nói cách khác, mô-đun bắt đầu này luôn luôn gọi một chính ba đối số. Nếu hàm main không có đối số hoặc chỉ int, char **, nó sẽ hoạt động tốt, cũng như nếu nó không có đối số, do các quy ước gọi.

Nếu bạn làm điều này trong chương trình của mình, nó sẽ là hành vi không thể di chuyển và được coi là không xác định theo ISO C: khai báo và gọi một hàm theo một cách và định nghĩa nó theo cách khác. Nhưng thủ thuật khởi động của trình biên dịch không nhất thiết phải có tính di động; nó không được hướng dẫn bởi các quy tắc cho các chương trình di động.

Nhưng giả sử rằng các quy ước gọi là như vậy mà nó không thể hoạt động theo cách này. Trong trường hợp đó, trình biên dịch phải xử lý mainđặc biệt. Khi nó nhận thấy rằng nó đang biên dịch mainhàm, nó có thể tạo ra mã tương thích với, chẳng hạn như lệnh gọi ba đối số.

Có nghĩa là, bạn viết thế này:

int main(void)
{
   /* ... */
}

Nhưng khi trình biên dịch nhìn thấy nó, về cơ bản nó thực hiện một chuyển đổi mã để hàm mà nó biên dịch trông giống như sau:

int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
   /* ... */
}

ngoại trừ việc các tên __argc_ignorekhông tồn tại theo nghĩa đen. Không có tên nào như vậy được đưa vào phạm vi của bạn và sẽ không có bất kỳ cảnh báo nào về các đối số không được sử dụng. Việc chuyển đổi mã khiến trình biên dịch phát ra mã với liên kết chính xác biết rằng nó phải xóa ba đối số.

Một chiến lược triển khai khác là để trình biên dịch hoặc có thể là trình liên kết tạo __starthàm tùy chỉnh (hoặc bất cứ thứ gì nó được gọi), hoặc ít nhất chọn một từ một số lựa chọn thay thế được biên dịch trước. Thông tin có thể được lưu trữ trong tệp đối tượng về các biểu mẫu được hỗ trợ mainđang được sử dụng. Trình liên kết có thể xem thông tin này và chọn phiên bản chính xác của mô-đun khởi động có chứa lệnh gọi maintương thích với định nghĩa của chương trình. Việc triển khai C thường chỉ có một số nhỏ các dạng được hỗ trợ mainvì vậy cách tiếp cận này là khả thi.

Các trình biên dịch cho ngôn ngữ C99 luôn phải xử lý mainđặc biệt, ở một mức độ nào đó, để hỗ trợ việc hack mà nếu hàm kết thúc mà không có returncâu lệnh, thì hành vi đó giống như return 0được thực thi. Điều này, một lần nữa, có thể được xử lý bằng một phép chuyển đổi mã. Trình biên dịch thông báo rằng một hàm được gọi mainđang được biên dịch. Sau đó, nó kiểm tra xem phần cuối của cơ thể có thể tiếp cận được hay không. Nếu vậy, nó sẽ chèn mộtreturn 0;


34

KHÔNG có quá tải mainngay cả trong C ++. Chức năng chính là điểm đầu vào cho một chương trình và chỉ nên tồn tại một định nghĩa duy nhất.

Đối với tiêu chuẩn C

Đối với môi trường được lưu trữ (đó là môi trường bình thường), tiêu chuẩn C99 cho biết:

5.1.2.2.1 Khởi động chương trình

Chức năng được gọi khi khởi động chương trình được đặt tên main. Việc thực hiện tuyên bố không có nguyên mẫu nào cho chức năng này. Nó sẽ được định nghĩa với kiểu trả về intvà không có tham số:

int main(void) { /* ... */ }

hoặc với hai tham số (ở đây được gọi là argcargv, mặc dù bất kỳ tên nào cũng có thể được sử dụng, vì chúng là cục bộ của hàm mà chúng được khai báo):

int main(int argc, char *argv[]) { /* ... */ }

hoặc tương đương; 9) hoặc theo một số cách thức triển khai khác được xác định.

9) Do đó, intcó thể được thay thế bằng tên typedef được định nghĩa là int, hoặc kiểu của argvcó thể được viết là char **argv, v.v.

Đối với C ++ tiêu chuẩn:

3.6.1 Chức năng chính [basic.start.main]

1 Một chương trình phải chứa một hàm toàn cục được gọi là hàm chính, là hàm khởi động được chỉ định của chương trình. [...]

2 Việc triển khai sẽ không xác định trước chức năng chính. Chức năng này sẽ không bị quá tải . Nó sẽ có kiểu trả về là kiểu int, nhưng nếu không thì kiểu của nó là kiểu thực thi được xác định. Tất cả các triển khai phải cho phép cả hai định nghĩa sau của main:

int main() { /* ... */ }

int main(int argc, char* argv[]) { /* ... */ }

Tiêu chuẩn C ++ nói rõ ràng "Nó [hàm chính] sẽ có kiểu trả về là kiểu int, nhưng nếu không thì kiểu của nó là kiểu thực thi được xác định" và yêu cầu hai chữ ký giống như tiêu chuẩn C.

Trong môi trường được lưu trữ ( môi trường AC cũng hỗ trợ các thư viện C) - Hệ điều hành sẽ gọi main.

Trong môi trường không được lưu trữ (Một dành cho các ứng dụng nhúng), bạn luôn có thể thay đổi điểm vào (hoặc thoát) của chương trình bằng cách sử dụng các lệnh tiền xử lý như

#pragma startup [priority]
#pragma exit [priority]

Trong đó mức độ ưu tiên là một số tích phân tùy chọn.

Khởi động Pragma thực thi chức năng trước chức năng chính (ưu tiên) và thoát pragma thực thi chức năng sau chức năng chính. Nếu có nhiều hơn một chỉ thị khởi động thì quyền ưu tiên sẽ quyết định lệnh nào sẽ thực thi trước.


4
Tôi không nghĩ, câu trả lời này thực sự trả lời câu hỏi làm thế nào trình biên dịch thực sự xử lý tình huống. Theo tôi, câu trả lời được đưa ra bởi @Kaz mang lại cái nhìn sâu sắc hơn.
Tilman Vogel

4
Tôi nghĩ câu trả lời này trả lời tốt hơn câu hỏi của @Kaz. Câu hỏi ban đầu có ấn tượng rằng đang xảy ra quá tải toán tử và câu trả lời này giải quyết điều đó bằng cách hiển thị rằng thay vì một số giải pháp nạp chồng, trình biên dịch chấp nhận hai chữ ký khác nhau. Các chi tiết của trình biên dịch rất thú vị nhưng không cần thiết để trả lời câu hỏi.
Waleed Khan

1
Đối với môi trường đích tự do ("không được lưu trữ"), có rất nhiều điều đang diễn ra ngoài một số #pragma. Có một ngắt thiết lập lại từ phần cứng và đó thực sự là nơi chương trình bắt đầu. Từ đó, tất cả các thiết lập cơ bản được thực thi: ngăn xếp thiết lập, thanh ghi, MMU, ánh xạ bộ nhớ, v.v. Sau đó, sao chép các giá trị init từ NVM sang các biến lưu trữ tĩnh xảy ra (phân đoạn .data), cũng như "zero-out" trên tất cả biến lưu trữ tĩnh phải được đặt thành 0 (phân đoạn .bss). Trong C ++, các hàm tạo của các đối tượng có thời lượng lưu trữ tĩnh được gọi. Và khi tất cả những điều đó được thực hiện, thì main sẽ được gọi.
Lundin

8

Không cần quá tải. Có, có 2 phiên bản, nhưng chỉ có một phiên bản có thể được sử dụng tại thời điểm đó.


5

Đây là một trong những quy tắc bất đối xứng kỳ lạ và đặc biệt của ngôn ngữ C và C ++.

Theo tôi, nó chỉ tồn tại vì lý do lịch sử và không có logic thực sự nghiêm túc đằng sau nó. Lưu ý rằng mainđặc biệt cũng vì các lý do khác (ví dụ: maintrong C ++ không thể đệ quy và bạn không thể lấy địa chỉ của nó và trong C99 / C ++, bạn được phép bỏ qua một returncâu lệnh cuối cùng ).

Cũng lưu ý rằng ngay cả trong C ++, nó không phải là quá tải ... một chương trình có dạng đầu tiên hoặc nó có dạng thứ hai; nó không thể có cả hai.


Bạn cũng có thể bỏ qua returncâu lệnh trong C (kể từ C99).
dreamlax

Trong C, bạn có thể gọi main()và lấy địa chỉ của nó; C ++ áp dụng các giới hạn mà C không áp dụng.
Jonathan Leffler

@JonathanLeffler: bạn nói đúng, đã sửa. Điều thú vị duy nhất về main mà tôi tìm thấy trong thông số kỹ thuật C99 ngoài khả năng bỏ qua giá trị trả về là vì tiêu chuẩn được viết từ IIUC, bạn không thể chuyển giá trị âm cho argckhi đệ quy (5.1.2.2.1 không chỉ định giới hạn về argcargvchỉ áp dụng cho cuộc gọi ban đầu tới main).
6502

4

Điều bất thường mainkhông phải là nó có thể được định nghĩa theo nhiều cách, mà là nó chỉ có thể được định nghĩa theo một trong hai cách khác nhau.

mainlà một chức năng do người dùng định nghĩa; việc triển khai không khai báo một nguyên mẫu cho nó.

Điều tương tự cũng đúng với fooor bar, nhưng bạn có thể xác định các hàm với những tên đó theo bất kỳ cách nào bạn muốn.

Sự khác biệt là nó mainđược gọi bởi việc triển khai (môi trường thời gian chạy), không chỉ bởi mã của riêng bạn. Việc triển khai không giới hạn ở ngữ nghĩa lời gọi hàm C thông thường, vì vậy nó có thể (và phải) đối phó với một vài biến thể - nhưng không bắt buộc phải xử lý vô số khả năng. Biểu int main(int argc, char *argv[])mẫu cho phép các đối số dòng lệnh và int main(void)trong C hoặc int main()trong C ++ chỉ là một sự thuận tiện cho các chương trình đơn giản không cần xử lý các đối số dòng lệnh.

Về cách trình biên dịch xử lý điều này, nó phụ thuộc vào việc thực hiện. Hầu hết các hệ thống có thể có các quy ước gọi làm cho hai biểu mẫu tương thích hiệu quả và bất kỳ đối số nào được chuyển đến một mainđịnh nghĩa không có tham số sẽ bị bỏ qua một cách lặng lẽ. Nếu không, sẽ không khó để trình biên dịch hoặc trình liên kết xử lý mainđặc biệt. Nếu bạn tò mò về cách nó hoạt động trên hệ thống của mình , bạn có thể xem một số danh sách lắp ráp.

Và giống như nhiều thứ trong C và C ++, các chi tiết phần lớn là kết quả của lịch sử và các quyết định độc đoán của các nhà thiết kế ngôn ngữ và người tiền nhiệm của chúng.

Lưu ý rằng cả C và C ++ đều cho phép các định nghĩa do triển khai khác xác định cho main - nhưng hiếm khi có bất kỳ lý do chính đáng nào để sử dụng chúng. Và đối với các triển khai tự do (chẳng hạn như các hệ thống nhúng không có hệ điều hành), điểm vào chương trình được xác định bởi việc triển khai và thậm chí không nhất thiết phải được gọi main.


3

Đây mainchỉ là tên cho một địa chỉ bắt đầu do trình liên kết quyết định, nơimain là tên mặc định. Tất cả các tên hàm trong một chương trình là địa chỉ bắt đầu nơi hàm bắt đầu.

Các đối số của hàm được đẩy / bật vào / từ ngăn xếp vì vậy nếu không có đối số nào được chỉ định cho hàm thì không có đối số nào được đẩy / bật vào / ra khỏi ngăn xếp. Đó là cách main có thể hoạt động cả khi có hoặc không có đối số.


2

Chà, hai chữ ký khác nhau của cùng một hàm main () chỉ xuất hiện trong hình ảnh khi bạn muốn như vậy, ý tôi là nếu chương trình của bạn cần dữ liệu trước khi xử lý thực tế mã của bạn, bạn có thể chuyển chúng bằng cách sử dụng -

    int main(int argc, char * argv[])
    {
       //code
    }

trong đó biến argc lưu trữ số lượng dữ liệu được truyền vào và argv là một mảng các con trỏ tới char trỏ đến các giá trị được truyền từ bảng điều khiển. Nếu không, nó luôn luôn tốt để đi với

    int main()
    {
       //Code
    }

Tuy nhiên, trong mọi trường hợp, có thể có một và chỉ một main () trong một chương trình, vì đó là điểm duy nhất mà từ đó chương trình bắt đầu thực thi và do đó nó không thể có nhiều hơn một. (hy vọng nó xứng đáng)


2

Một câu hỏi tương tự đã được hỏi trước đây: Tại sao một hàm không có tham số (so với định nghĩa hàm thực) lại biên dịch?

Một trong những câu trả lời được xếp hạng hàng đầu là:

Trong C func()có nghĩa là bạn có thể chuyển bất kỳ số lượng đối số nào. Nếu bạn muốn không có đối số thì bạn phải khai báo làfunc(void)

Vì vậy, tôi đoán đó là cách mainđược khai báo (nếu bạn có thể áp dụng thuật ngữ "được khai báo" cho main). Trên thực tế, bạn có thể viết một cái gì đó như thế này:

int main(int only_one_argument) {
    // code
}

và nó sẽ vẫn biên dịch và chạy.


1
Quan sát tuyệt vời! Có vẻ như trình liên kết khá tha thứ main, vì có một vấn đề chưa được đề cập: thậm chí còn nhiều lập luận cho main! Thêm "Unix (nhưng không phải Posix.1) và Microsoft Windows" char **envp(tôi nhớ DOS cũng cho phép điều đó, phải không?), Và Mac OS X và Darwin thêm một con trỏ char * "thông tin tùy ý do hệ điều hành cung cấp" khác. wikipedia
usr2564301

0

Bạn không cần phải ghi đè điều này. Bởi vì chỉ có một chức năng sẽ được sử dụng tại một thời điểm. Có 2 phiên bản khác nhau của chức năng chính

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.