1. 程式人生 > >深入淺出 C++:main()

深入淺出 C++:main()

main() 是 C/C++ 程式執行的進入點,作業系統執行程式時,首先會執行 Runtime Library 內的函式進行必要的初始化,接著才呼叫 main() 轉移控制權,當 main() 返回時,再根據 main() 的返回值呼叫 exit() 結束程式。

main() 的標準函式原型 (Function Prototype)

第一種標準寫法,是不帶引數的:

#include <cstdlib>

int main()
{
  return EXIT_SUCCESS;
}

main() 的返回值代表程式的結束狀態。使用 cstdlib 是為了引入 EXIT_SUCCESS 這個 macro,回傳它或 0 代表程式成功執行,而相對的,回傳 EXIT_FAILURE 則代表由錯誤發生。C++ 標準並未定義 EXIT_SUCCESS 對應哪個整數。

值得留意的是,C++ 嚴格規定,如果函式宣告時有返回值型別 (return type),就必須有程式碼明確給出返回值。但 main() 例外,它可以省略 return EXIT_SUCCESS 那一行,此時效果成功結束。

第二種 main() 的寫法帶有兩個引數 (parameter),允許使用者在命令列模式執行程式時,指定一至數個引數 (argument):

#include <cstdlib>
#include <iostream>

int main(int argc, char* argv[])
{
  for (int i = 0 ; i < argc ; ++i)
    std
::cout << "parameter " << i << " : " << argv[i] << std::endl; return EXIT_SUCCESS; }

引數 argc 記錄當程式執行時,傳入的引數個數,argv 則是型別為 char** 的指標 (pointer),指向一個字串陣列 (string array),該陣列長度為 argc + 1。argv[0] 至 argv[argc - 1] 為執行程式時傳入的引數;argv[argc] 為 0,代表空字串。語法上,第二個引數的宣告,也可以寫成 char** argv,但較常見的寫法仍是 char* argv[],因為它明確表達了 argv 指向一個字串陣列。

上面程式執行結果如下:

[email protected]:~/cpp/c1$ clang++ -std=c++17 -stdlib=libc++ --pedantic-errors -pthread -o main main.cpp
[email protected]:~/cpp/c1$ ./main -s -t --version "test" \"test\" 'test' \'test\'
Argument 0 : ./main
Argument 1 : -s
Argument 2 : -t
Argument 3 : --version
Argument 4 : test
Argument 5 : "test"
Argument 6 : test
Argument 7 : 'test'

有些程式設計師,認為 argv[0] 必為程式的名稱,但實際上在 c++ 標準中,argv[0] 可能是程式名、也可能是空字串。故撰寫跨平臺程式時,切勿犯此錯誤。

如果你熟悉 C++11 的返回型別後置語法 (trailing return type),也可以裝逼這麼寫:

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

Trailing return type 在 C++ template 開發有舉足輕重的作用,但在 main() 這裡,意義與效果與前面的版本完全相同,純粹是換個寫法。

誰呼叫了 main()

本文開頭提到,main() 並非程式第一個執行的函式。我們可以用 gdb 探索究竟是誰呼叫了它。

首先,我們沿用前面的 main.cpp,但在 compile 時,加上 -g 選項以包含 debug 資訊:

[email protected]:~/cpp/c1$ clang++ -std=c++17 -g -stdlib=libc++ --pedantic-errors -pthread -o main main.cpp

接著使用 gdb 除錯:

[email protected]:~/cpp/c1$ gdb -q main
Reading symbols from main...done.

將中斷點 (breakpoint) 設在程式的第 6 行,也就是 for 迴圈那裡:

(gdb) b 6
Breakpoint 1 at 0x400e06: file main.cpp, line 6.

執行程式後,就會顯示停止在第 6 行:

(gdb) r
Starting program: /home/sora/cpp/c1/main 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".

Breakpoint 1, main (argc=1, argv=0x7fffffffdf58) at main.cpp:6
6     for (int i = 0 ; i < argc ;++i)

執行 where 指令,此時 gdb 預設會忽略 main() 之前的函式,所以看不到什麼:

(gdb) where
 #0  main (argc=1, argv=0x7fffffffdf58) at main.cpp:6

輸入以下指令,即可讓 gdb 顯示 main() 之前的函式:

(gdb) set backtrace past-main

再執行一次 where,即可讓 gdb 顯示 main() 之前的函式,可看到 C++ Runtime Library 先執行了 _start()、再呼叫到 __libc_start_main() 函式,最後才呼叫到 main():

(gdb) where
 #0  main (argc=1, argv=0x7fffffffdf58) at main.cpp:6
 #1  0x00007ffff6d3cb97 in __libc_start_main (
    main=0x400df0 <main(int, char**)>, argc=1, argv=0x7fffffffdf58, 
    init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, 
    stack_end=0x7fffffffdf48) at ../csu/libc-start.c:310
 #2  0x0000000000400d2a in _start ()

在 Unix-like 系統中,當 compile 後的程式與 glibc link 成 ELF 格式的可執行檔案,_start 就是程式執行初始化的入口函式。在 ELF 格式中,.init 代表 main() 執行前需要被執行的程式碼,.fini 代表 main() 離開後、程式結束前需要執行的程式碼,這些都傳入了 __libc_start_main() 函式,以下是刪減過後的版本,省略了初始化其他部分的程式碼:

// glibc/csu/libc-start.c

STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
                 int argc, char **argv,
#ifdef LIBC_START_MAIN_AUXVEC_ARG
                 ElfW(auxv_t) *auxvec,
#endif
                 __typeof (main) init,
                 void (*fini) (void),
                 void (*rtld_fini) (void), void *stack_end)
{
  /* Result of the 'main' function.  */
  int result;

  /* Call the initializer of the program, if any.  */
  if (init)
    (*init) (argc, argv, __environ MAIN_AUXVEC_PARAM);

  /* Register the destructor of the dynamic linker if there is any.  */
  if (__glibc_likely (rtld_fini != NULL))
    __cxa_atexit ((void (*) (void *)) rtld_fini, NULL, NULL);

  /* Register the destructor of the program, if any.  */
  if (fini)
    __cxa_atexit ((void (*) (void *)) fini, NULL, NULL);

  result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
  exit (result);
}

.init 與 .fini 兩個 section 對 C++ 至關重要,.init 幫 C++ 程式在 main() 執行之前,呼叫了所有 global object 的 constructor,而 .fini 則是執行他們的 destructor。

最後執行 quit 指令離開除錯,結束 gdb 執行:

(gdb) quit
A debugging session is active.

    Inferior 1 [process 25264] will be killed.

Quit anyway? (y or n) y

常見的非標準 main() 函式原型

void main()
{
}

這種返回值為 void 的寫法,來自 Microsoft Visual C++,由於 Windows 平臺上,許多 C++ 初學者選用 Visual C++ 當作入門的練習工具,所以常會誤以為這種寫法是標準所允許的。gcc 與 clang 都將這種寫法視為錯誤。

另一種非標準、但也很常見的形式為:

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

這種非標準寫法帶有第三個引數 envp,用來接收目前系統的環境變數。下面程式可以打印出系統所有的環境變數:

#include <iostream>

int main(int argc, char* argv[], char* envp[])
{
  for (std::size_t i = 0 ; envp[i] != nullptr ; ++i)
    std::cout << "envp[" << i << "] : " << envp[i] << std::endl;
}

以筆者執行的 Ubuntu 18.04 為例,執行結果如下,可看到 env 裡每個字串,其實是環境變數裡 key、value 的對應:

envp[0] : CLUTTER_IM_MODULE=xim
envp[1] : LD_LIBRARY_PATH=:/home/sora/Temp/clang/build/lib
...
envp[62] : OLDPWD=/home/sora/Temp/clang/llvm/projects
envp[63] : _=./main_envp

C++ 標準中,可以使用 <cstdlib> 的 getenv() 函式取得某個環境變數內容,這種方式,比藉由 main() 傳遞第三個引數來的安全且標準:

char* getenv(const char* env_var);

以下是範例:

#include <iostream>
#include <cstdlib>

int main()
{
  if (const char* p = std::getenv("PATH"); p)
    std::cout << "PATH=" << p << std::endl;

  if (const char* p = std::getenv("TERM"); p)
    std::cout << "TERM=" << p << std::endl;
}

輸出結果如下:

[email protected]:~/cpp/c1$ clang++ -std=c++17 -stdlib=libc++ --pedantic-errors -pthread -o getenv getenv.cpp
[email protected]:~/cpp/c1$ ./getenv
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
TERM=xterm-256color

早期的 C 式 main()

以下這兩種 main() 函式,在早期的 C 程式碼還可見到。這些寫法已無法通過目前 C++ compiler 的檢查。列在本文裡,純粹是因為筆者的考古情懷。如果你有機會接觸到這些古老的程式,記得瞻仰下先聖先賢。

main()
{
}

這種連返回值型別都沒寫的形式,是早期 C 所允許的。沒寫並不代表沒有返回值,C compiler 會將這種情況視為 int main()。

另一種古老的形式如下:

int main()
  int    argc;
  char** argv;
{
  return 0;
}

這種寫法稱為 K&R 形式。1978年,由 Brian Kernighan 與 Dennis Ritchie 合著的 The C Programming Language 第一版,書中即採用這種格式,由於這本書太出名了,計算機界以兩位作者首字母縮寫簡稱這本書為 K&R,而這個函式定義就稱為 K&R 形式。隨後美國國家標準化協會 (American National Standards Institute) 於 1989 年制定的 ANSI C 標準 (常稱為 C89) 已不採用這種函式定義,而 1988 出版的 K&R 第二版亦以 ANSI C 格式改寫。不過如果讀者有幸接觸一些二十幾年錢的 C 程式,仍可看到這種寫法。這種語法一樣無法通過目前 gcc 與 clang 的檢查。

相關推薦

深入淺出 C++main()

main() 是 C/C++ 程式執行的進入點,作業系統執行程式時,首先會執行 Runtime Library 內的函式進行必要的初始化,接著才呼叫 main() 轉移控制權,當 main() 返回時,再根據 main() 的返回值呼叫 exit() 結束程式。

C++Main函式引數列表及引數型別轉換

有三個問題待解決: 1、C/C++ main函式引數意義 2、怎麼向main函式傳參 3、傳進來的引數型別是什麼?怎麼型別轉換? 下面來分別分析! 首先,看程式碼,然後分析。 int main(in

深入淺出 C++與程式終止相關的函式 PART 3

Markdown 編輯器真是不好用,這個文章裡,好幾個程式輸出的地方,# 開頭的都被識別成標題了。如果在 # 前面加上 \,看起來似乎能解決,但好幾行一改,又變成能在文章內看到 \ # 開頭了。哎,試了半個小時,懶得再試了,客官們擔待些,反正對理解正文沒影響便是

深入淺出 C++與程式終止相關的函式 PART 1

C/C++ 程式,一般是藉由 main() 的返回值呼叫 exit() 函式以正常結束程式。除了程式崩潰、或使用者強制結束程式外,C++ 亦提供數個函式,允許呼叫以立即終止程式,本文將一一介紹這些函式。 不過,在進入主題前,需提醒讀者:撰寫程式時,儘可能使程式

深入淺出 C++與程式終止相關的函式 PART 2

quick_exit() 與 at_quick_exit() (C++11新增) [[noreturn]] void quick_exit(int status) noexcept; quick_exit() 為 C++11 引入的函式,如果程式有特殊理

深入淺出 C++#include Directive PART 1

除了基本語法外,使用 C++ 提供的標準庫、型別定義等,都需要使用 #include 引入 header file,寫法如下: #include <iostream> #include <vector> #include <s

C++從零開始區塊鏈main函式的一種實現

前面已經把各種業務邏輯都寫好了,main函式怎麼呼叫就隨便了,這裡只是其中一種實現方法 int main(int argc, char **argv) { if (argc < 2) { std::cout << "argc error!

初遇C#一個簡單的小程序(圓形周長,面積計算器)

編碼 雙精度 崩潰 輸入 面向對象 窗口 語句 readline 面向對象的語言 作為一個面向對象的語言,與用戶的交互很關鍵! 在此,我們可以先分析一下我們這個小程序要與用戶交互的內容:1.命名很重要,讓用戶看見這個程序就知道這個程序的作用。 2.當用戶打開這個程序時,提示

初遇C#健康計算器

最小值 () 標準 最大 兩個 選擇 bsp min 用戶 上次寫了一個簡單的圓形的周長和面積計算器,這個我們來寫一個對大家都很重要的健康計算器,畢竟健康是福嘛,有了健康,什麽都會有!所以我們都要保持健康! 編程開始: Console.Write("請輸入你的性別(男或

C++順序表類實現約瑟夫問題_密碼不同

class josephus main clu 定義 void seq esp while //.h #pragma once#include <iostream>using namespace std;#define MAXSIZE 100 template

深入淺出講解php的socket通信

刪除 不一定 電話鈴 例子 通過 另一個 一次 函數返回 ima 對TCP/IP、UDP、Socket編程這些詞你不會很陌生吧?隨著網絡技術的發展,這些詞充斥著我們的耳朵。那麽我想問:1. 什麽是TCP/IP、UDP?2. Socke

深入淺出CSSDiv(一)

指定 增加 src 深入 lock alt 舉例 gin width 這個系列是學習筆記,簡明記錄結論性的知識。 新建一個層時,border為零,margin為0,padding為0,如果不指定寬度(width),則自動100%填充父元素。 三、層與父元素的關系 1.

C/C++函數調用規則__stdcall,__cdecl,__pascal,__fastcall

this 返回 但是 寄存器 表示 使用 自動 sta borland __cdecl __cdecl 是 C Declaration 的縮寫,表示 C 語言默認的函數調用方法:所有參數從右到左依次入棧,這些參數由調用者清除,稱為手動清棧。被調用函數不會要求調用者傳遞多少

深入理解Objective-CCategory

fix 忽略 DDU 相關 情況 內存布局 先生 們的 ntc https://tech.meituan.com/DiveIntoCategory.html 摘要 無論一個類設計的多麽完美,在未來的需求演進中,都有可能會碰到一些無法預測的情況。那怎麽擴展已有的類呢?一般而言

C++UNREFERENCED_PARAMETER用法

禁用 我想 解釋 一行 .com under 必須 配置 級別 原文地址:http://www.cnblogs.com/kex1n/archive/2010/08/05/2286486.html 作用:告訴編譯器,已經使用了該變量,不必檢測警告! 在VC編譯器下,如果您用最

【Java並發編程實戰】—–“J.U.CReentrantLock之二lock方法分析

b2c check 條件 維護 box 抽象 post eight 若是 前一篇博客簡介了ReentrantLock的定義和與synchronized的差別,以下尾隨LZ的筆記來扒扒ReentrantLock的lock方法。我們知道ReentrantLock有公平鎖、非

C++構造函數1——普通構造函數

創建 c++編譯 clu namespace 我們 這一 () 一次 ret 前言:構造函數是C+中很重要的一個概念,這裏對其知識進行一個簡單的總結 一、構造函數的定義 1.類中的構造函數名與類名必須相同 2.構造函數沒有函數的返回類值型說明符 [特別註意]: a.構造函數

C++引用的簡單理解

傳遞 技術 ren ring 知識 cout 進行 表達 並且 前言:引用是C++一個很重要的特性,最近看了很多有關引用的資料和博客,故在此對引用的相關知識進行總結 一、什麽是引用 引用,顧名思義是某一個變量或對象的別名,對引用的操作與對其所綁定的變量或對象的操作完全等價

C++析構函數

原則 efault main函數 內存空間 log 文件 student 功能 namespace 一、什麽是析構函數 析構函數是類中一種特殊的成員函數,但其功能和構造函數是相反的,當對象結束其生命周期時,系統會自動調用該對象的析構函數進行清理工作(如釋放內存中分配給該對象

C++類中兩個易被忽略的默認函數

ont names namespace tor img c++編譯 style div 顯式 C++的自定義類中有六個默認的函數,即如果用戶沒有顯式定義這些函數時,C++編譯器會類中生成這些函數的默認形式。除了大家所熟知的構造函數、拷貝構造函數、賦值函數和析構函數外,C++