乾貨!深度剖析C語言的main函數

2021-02-19 嵌入式ARM

main函數的返回值用於說明程序的退出狀態。如果返回0,則代表程序正常退出。返回其它數字的含義則由系統決定。通常,返回非零代表程序異常退出。

void main()

有一些書上的,都使用了void main( ) ,其實這是錯誤的。C/C++ 中從來沒有定義過void main( ) 。

C++ 之父 Bjarne Stroustrup 在他的主頁上的 FAQ 中明確地寫著 「The definition void main( ) { /* … */ } is not and never has been C++, nor has it even been C.」 這可能是因為 在 C 和 C++ 中,不接收任何參數也不返回任何信息的函數原型為「void foo(void);」。

可能正是因為這個,所以很多人都誤認為如果不需要程序返回值時可以把main函數定義成void main(void) 。然而這是錯誤的!main 函數的返回值應該定義為 int 類型,C 和 C++ 標準中都是這樣規定的。

雖然在一些編譯器中,void main() 可以通過編譯,但並非所有編譯器都支持 void main() ,因為標準中從來沒有定義過 void main 。

g++3.2 中如果 main 函數的返回值不是 int 類型,就根本通不過編譯。而 gcc3.2 則會發出警告。所以,為了程序擁有很好的可移植性,一定要用 int main ()。測試如下:

#include <stdio.h>

void main()
{
    printf("Hello world\n");
    return;
}

運行結果:g++ test.c

main()

那既然main函數只有一種返回值類型,那麼是不是可以不寫?規定:不明確標明返回值的,默認返回值為int,也就是說 main()等同於int main(),而不是等同於void main()。

在C99中,標準要求編譯器至少給 main() 這種用法來個警告,而在c89中這種寫法是被允許的。但為了程序的規範性和可讀性,還是應該明確的指出返回值的類型。測試代碼:

#include <stdio.h>

main()
{
    printf("Hello world\n");
    return 0;
}

運行結果:

C和C++的標準

在 C99 標準中,只有以下兩種定義方式是正確的:

int main( void ) 
int main( int argc, char *argv[] ) 

若不需要從命令行中獲取參數,就使用int main(void) ;否則的話,就用int main( int argc, char *argv[] )。當然參數的傳遞還可以有其他的方式,在下一節中,會單獨來講。

main 函數的返回值類型必須是 int ,這樣返回值才能傳遞給程序的調用者(如作業系統),等同於 exit(0),來判斷函數的執行結果。

C++89中定義了如下兩種 main 函數的定義方式:

int main( ) 
int main( int argc, char *argv[] ) 

int main( ) 等同於 C99 中的 int main( void ) ;int main( int argc, char*argv[] ) 的用法也和C99 中定義的一樣。同樣,main函數的返回值類型也必須是int。

return 語句

如果 main 函數的最後沒有寫 return 語句的話,C99 和c++89都規定編譯器要自動在生成的目標文件中加入return 0,表示程序正常退出。

不過,建議你最好在main函數的最後加上return語句,雖然沒有這個必要,但這是一個好的習慣。在linux下我們可以使用shell命令:echo $? 查看函數的返回值。

#include <stdio.h>

int main()
{
    printf("Hello world\n");
}

運行結果:

同時,需要說明的是return的返回值會進行 類型轉換,比如:若return 1.2 ;會將其強制轉換為1,即真正的返回值是1,同理,return 『a』 ;的話,真正的返回值就是97,;但是若return 「abc」;便會報警告,因為無法進行隱式類型轉換。

測試main函數返回值的意義

前文說到,main函數如果返回0,則代表程序正常退出。通常,返回非零代表程序異常退出。在本文的最後,測試一下:  test.c:

#include <stdio.h>

int main()
{
    printf("c 語言\n");
    return 11.1; 
}

在終端執行如下:

➜  testSigpipe git:(master) ✗ vim test.c
➜  testSigpipe git:(master) ✗ gcc test.c
➜  testSigpipe git:(master) ✗ ./a.out && echo "hello world"  #&&與運算,前面為真,才會執行後邊的
c 語言

可以看出,作業系統認為main函數執行失敗,因為main函數的返回值是11

➜  testSigpipe git:(master) ✗ ./a.out 
➜  testSigpipe git:(master) ✗ echo $?
11

若將main函數中返回值該為0的話:

➜  testSigpipe git:(master) ✗ vim test.c
➜  testSigpipe git:(master) ✗ gcc test.c 
➜  testSigpipe git:(master) ✗ ./a.out && echo "hello world" #hello
c 語言
hello world

可以看出,正如我們所期望的一樣,main函數返回0,代表函數正常退出,執行成功;返回非0,代表函數出先異常,執行失敗。


首先說明的是,可能有些人認為main函數是不可傳入參數的,但是實際上這是錯誤的。main函數可以從命令行獲取參數,從而提高代碼的復用性。

函數原形

為main函數傳參時,可選的main函數原形為:

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

參數說明:

①、第一個參數argc表示的是傳入參數的個數 。

②、第二個參數char* argv[],是字符串數組,用來存放指向的字符串參數的指針數組,每一個元素指向一個參數。各成員含義如下:

argv[0]:指向程序運行的全路徑名。

argv[1]:指向執行程序名後的第一個字符串 ,表示真正傳入的第一個參數。

argv[2]:指向執行程序名後的第二個字符串 ,表示傳入的第二個參數。

…… argv[n]:指向執行程序名後的第n個字符串 ,表示傳入的第n個參數。

規定:argv[argc]為NULL ,表示參數的結尾。

③、第三個參數char* envp[],也是一個字符串數組,主要是保存這用戶環境中的變量字符串,以NULL結束。envp[]的每一個元素都包含ENVVAR=value形式的字符串,其中ENVVAR為環境變量,value為其對應的值。

envp一旦傳入,它就只是單純的字符串數組而已,不會隨著程序動態設置發生改變。可以使用putenv函數實時修改環境變量,也能使用getenv實時查看環境變量,但是envp本身不會發生改變;平時使用到的比較少。

注意:main函數的參數char* argv[]和char* envp[]表示的是字符串數組,書寫形式不止char* argv[]這一種,相應的argv[][]和 char** argv均可。

char* envp[]

寫個小測試程序,測試main函數的第三個參數:

#include <stdio.h>

int main(int argc ,char* argv[] ,char* envp[])
{
    int i = 0;

    while(envp[i++])
    {
        printf("%s\n", envp[i]);
    }

    return 0;
}

運行結果:部分截圖

envp[] 獲得的信息等同於Linux下env命令的結果。

常用版本

在使用main函數的帶參版本的時,最常用的就是:**int main(int argc , char* argv[]);**變量名稱argc和argv是常規的名稱,當然也可以換成其他名稱。

命令行執行的形式為:可執行文件名 參數1 參數2 … … 參數n。可執行文件名稱和參數、參數之間均使用空格隔開。

示例程序
#include <stdio.h>

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

    int i;
    printf("Total %d arguments\n",argc);

    for(i = 0; i < argc; i++)
    {
        printf("\nArgument argv[%d]  = %s \n",i, argv[i]);
    }

    return 0;
}

運行結果:

➜  cpp_workspace git:(master) ✗ vim testmain.c 
➜  cpp_workspace git:(master) ✗ gcc testmain.c 
➜  cpp_workspace git:(master) ✗ ./a.out 1 2 3    #./a.out為程序名 1為第一個參數 , 2 為第二個參數, 3 為第三個參數
Total 4 arguments
Argument argv[0]  = ./a.out 
Argument argv[1]  = 1 
Argument argv[2]  = 2 
Argument argv[3]  = 3 
Argument argv[4]  = (null)    #默認argv[argc]為null


可能有的人會說,這還用說,main函數肯定是程序執行的第一個函數。那麼,事實果然如此嗎?相信在看了本節之後,會有不一樣的認識。

為什麼說main()是程序的入口

linux系統下程序的入口是」_start」,這個函數是linux系統庫(Glibc)的一部分,當我們的程序和Glibc連結在一起形成最終的可執行文件的之後,這個函數就是程序執行初始化的入口函數。通過一個測試程序來說明:

#include <stdio.h>

int main()
{
    printf("Hello world\n");
    return 0;
}

編譯:

gcc testmain.c -nostdlib     # -nostdlib (不連結標準庫)

程序執行會引發錯誤:/usr/bin/ld: warning: cannot find entry symbol _start; 未找到這個符號

所以說:

編譯器預設是找 __start 符號,而不是 main

那麼,這個_start和main函數有什麼關係呢?下面我們來進行進一步探究。

_start函數的實現該入口是由ld連結器默認的連結腳本指定的,當然用戶也可以通過參數進行設定。_start由彙編代碼實現。大致用如下偽代碼表示:

void _start()
{
  %ebp = 0;
  int argc = pop from stack
  char ** argv = top of stack;
  __libc_start_main(main, argc, argv, __libc_csu_init, __linc_csu_fini,
  edx, top of stack);
}

對應的彙編代碼如下:

_start:
 xor ebp, ebp //清空ebp
 pop esi //保存argc,esi = argc
 mov esp, ecx //保存argv, ecx = argv

 push esp //參數7保存當前棧頂
 push edx //參數6
 push __libc_csu_fini//參數5
 push __libc_csu_init//參數4
 push ecx //參數3
 push esi //參數2
 push main//參數1
 call _libc_start_main

hlt

可以看出,在調用_start之前,裝載器就會將用戶的參數和環境變量壓入棧中。

main函數運行之前的工作

從_start的實現可以看出,main函數執行之前還要做一系列的工作。主要就是初始化系統相關資源:

Some of the stuff that has to happen before main():

set up initial stack pointer 

initialize static and global data 

zero out uninitialized data 

run global constructors

Some of this comes with the runtime library's crt0.o file or its __start() function. Some of it you need to do yourself.

Crt0 is a synonym for the C runtime library.

1.設置棧指針

2.初始化static靜態和global全局變量,即data段的內容

3.將未初始化部分的賦初值:數值型short,int,long等為0,bool為FALSE,指針為NULL,等等,即.bss段的內容

4.運行全局構造器,類似c++中全局構造函數

5.將main函數的參數,argc,argv等傳遞給main函數,然後才真正運行main函數

main之前運行的代碼

下面,我們就來說說在mian函數執行之前到底會運行哪些代碼:(1)全局對象的構造函數會在main 函數之前執行。

(2)一些全局變量、對象和靜態變量、對象的空間分配和賦初值就是在執行main函數之前,而main函數執行完後,還要去執行一些諸如釋放空間、釋放資源使用權等操作

(3)進程啟動後,要執行一些初始化代碼(如設置環境變量等),然後跳轉到main執行。全局對象的構造也在main之前。

(4)通過關鍵字attribute,讓一個函數在主函數之前運行,進行一些數據初始化、模塊加載驗證等。

示例代碼

①、通過關鍵字attribute

#include <stdio.h>

__attribute__((constructor)) void before_main_to_run() 

    printf("Hi~,i am called before the main function!\n");
    printf("%s\n",__FUNCTION__); 


__attribute__((destructor)) void after_main_to_run() 

    printf("%s\n",__FUNCTION__); 
    printf("Hi~,i am called after the main function!\n");


int main( int argc, char ** argv ) 

    printf("i am main function, and i can get my name(%s) by this way.\n",__FUNCTION__); 
    return 0; 
}

②、全局變量的初始化

#include <iostream>

using namespace std;

inline int startup_1()
{
    cout<<"startup_1 run"<<endl;
    return 0;
}

int static no_use_variable_startup_1 = startup_1();

int main(int argc, const char * argv[]) 
{
    cout<<"this is main"<<endl;
    return 0;
}

至此,我們就聊完了main函數執行之前的事情,那麼,你是否還以為main函數也是程序運行的最後一個函數呢?

結果當然不是,在main函數運行之後還有其他函數可以執行,main函數執行完畢之後,返回到入口函數,入口函數進行清理工作,包括全局變量析構、堆銷毀、關閉I/O等,然後進行系統調用結束進程。

main函數之後執行的函數

1、全局對象的析構函數會在main函數之後執行; 2、用atexit註冊的函數也會在main之後執行。

atexit函數

原形:

int atexit(void (*func)(void)); 

atexit 函數可以「註冊」一個函數,使這個函數將在main函數正常終止時被調用,當程序異常終止時,通過它註冊的函數並不會被調用。

編譯器必須至少允許程式設計師註冊32個函數。如果註冊成功,atexit 返回0,否則返回非零值,沒有辦法取消一個函數的註冊。

在 exit 所執行的任何標準清理操作之前,被註冊的函數按照與註冊順序相反的順序被依次調用。每個被調用的函數不接受任何參數,並且返回類型是 void。被註冊的函數不應該試圖引用任何存儲類別為 auto 或 register 的對象(例如通過指針),除非是它自己所定義的。

多次註冊同一個函數將導致這個函數被多次調用。函數調用的最後的操作就是出棧過程。main()同樣也是一個函數,在結束時,按出棧的順序調用使用atexit函數註冊的,所以說,函數atexit是註冊的函數和函數入棧出棧一樣,是先進後出的,先註冊的後執行。通過atexit可以註冊回調清理函數。可以在這些函數中加入一些清理工作,比如內存釋放、關閉打開的文件、關閉socket描述符、釋放鎖等等。

#include<stdio.h>
#include<stdlib.h>

void fn0( void ), fn1( void ), fn2( void ), fn3( void ), fn4( void );

int main( void )

{
  //注意使用atexit註冊的函數的執行順序:先註冊的後執行
    atexit( fn0 );  
    atexit( fn1 );  
    atexit( fn2 );  
    atexit( fn3 );  
    atexit( fn4 );

    printf( "This is executed first.\n" );
    printf("main will quit now!\n");

    return 0;

}

void fn0()
{
    printf( "first register ,last call\n" );
}

void fn1(
{
    printf( "next.\n" );
}

void fn2()
{
    printf( "executed " );
}

void fn3()
{
    printf( "is " );
}

void fn4()
{
    printf( "This " );
}

相關焦點

  • 乾貨 | 深度剖析C語言的main函數
    在本文的最後,測試一下:  test.c:#include <stdio.h>int main(){    printf("c 語言");    return 11.1; }在終端執行如下:
  • 深度剖析C語言的main函數
    在本文的最後,測試一下:  test.c:#include <stdio.h>int main(){    printf("c 語言");    return 11.1; }➜  testSigpipe git
  • 深度剖析C語言的main函數!
    在本文的最後,測試一下:  test.c:可以看出,作業系統認為main函數執行失敗,因為main函數的返回值是11可以看出,正如我們所期望的一樣,main函數返回0,代表函數正常退出,執行成功;返回非0,代表函數出先異常,執行失敗。首先說明的是,可能有些人認為main函數是不可傳入參數的,但是實際上這是錯誤的。
  • c語言main函數裡的參數argv和argc解析
    總的來說,函數的返回值就是給調用的地方返回一個值。(1)main函數是特殊的,首先這個名字是特殊的;因為在c語言裡面規定了main函數是整個程序的入口;其它函數只有直接或者間接被main函數所調用才能被執行,如果沒用被main函數直接或者間接調用,則這個函數在整個程序中無用。
  • 深入淺出剖析C語言函數指針與回調函數(一)
    我們把函數的指針(地址),這裡也就是add_ret,作為參數int add(int a , int b , int (*add_value)()) , 這裡的參數就是int(*add_value)() , 這個名字可以隨便取,但是要符合C語言的命名規範。當這個指針被用來調用其所指向的函數時,我們就說這是回調函數。
  • C語言簡明教程(一)C語言簡單剖析
    >(一)C語言簡單剖析實驗內容本節課程將簡單介紹 C 語言的發展歷程及前景,並剖析 C 語言的編譯執行過程,寫出經典的 hello world 程序。創建 C 程序C 語言的簡單結構預處理指令main()函數程序框架printf() 函數創建 C 程序C 程序的創建過程有四個基本步驟:編寫;編譯;連結;執行。
  • C語言的main函數到底該怎麼寫
    語言到現在,我們似乎看到了很多個版本的main函數,那麼哪一種才是正確的呢?我們先來看看目前有哪些版本。main函數版本第一種,沒有返回值,沒有入參:main()在C89標準中,這種寫法是可以接受的,但使用現在的編譯器編譯時,會報告警,並且會將其返回值默認為int。實際上,如果函數沒有顯式聲明返回類型,那麼編譯器會將返回值默認為int。
  • 如何寫好 C main 函數 | Linux 中國
    但是不要這麼快就否定 C 語言 —— 它能夠提供很多東西,並且簡潔。如果你需要速度,用 C 語言編寫可能就是你的答案。如果你正在尋找穩定的職業或者想學習如何捕獲空指針解引用,C 語言也可能是你的答案!在本文中,我將解釋如何構造一個 C 文件並編寫一個 C main 函數來成功地處理命令行參數。我:一個頑固的 Unix 系統程式設計師。
  • C語言 main 函數到底怎麼寫是對的?
    各位,C語言中的main函數大家都再熟悉不過了,這是你學習C語言首先就要學習的東西,但是我看過很多人寫的代碼包括我們的一些讀者在main函數的寫法方面版本很多
  • C語言中的scanf函數
    #include <stdio.h>main(){ int n = 5; char c[n]; for(int i = 0; i < n; i++)  c[i] = scanf("%c",&c[i]);  printf(c);return
  • C語言必須寫main函數?最簡單的 Hello world 你其實一點都不懂!
    既然它們的流程是,系統加載進來,然後初始化,再到我們的main方法,那麼這個main方法,肯定是可以變的。為什麼這麼說呢?做過嵌入式開發的應該熟悉,基本上都沒有main函數一說,直接從跳轉入口開始跑就可以的。可以給任意函數,指定成Enter,也就是入口函數,使用連結腳本就可以指定,這塊感興趣的可以搜索gcc連結器參數。
  • C語言必須寫main函數?最簡單的Hello world 你其實一點都不懂!
    既然它們的流程是,系統加載進來,然後初始化,再到我們的main方法,那麼這個main方法,肯定是可以變的。為什麼這麼說呢?做過嵌入式開發的應該熟悉,基本上都沒有main函數一說,直接從跳轉入口開始跑就可以的。可以給任意函數,指定成Enter,也就是入口函數,使用連結腳本就可以指定,這塊感興趣的可以搜索gcc連結器參數。
  • 深入淺出剖析C語言函數指針與回調函數
    gcc main.c -L . –lvendor -o main    我們將編譯動態庫生成的libvendor.so拷貝到/usr/lib後,現在就不需要vendor.c了,此時我們將vendor.c移除,也可以正常的編譯並且執行main函數的結果,這就是回調函數的作用之一。
  • C語言主函數main函數返回值有什麼用?
    使用了這麼多年的C語言,學習工作,開發伺服器程序,始終沒有覺得main函數的返回值有什麼作用。最近一段時間由於開發一個測試軟體,需要對考生提交的C源碼和編譯生成的EXE可執行文件進行檢驗,才讓我狠狠地理解了一下main的返回值。
  • C語言之函數
    一.函數的概念1.C語言中,最簡單的程序模塊就是函數。2.函數被視為程序設計的基本邏輯單位,一個c程序是由一個main()函數和若干其他函數組成。3.程序執行從main()函數開始,main()函數可以調用其他函數,其他函數可以互相調用。
  • 如果 main 函數的末尾沒有 return 語句將會有什麼影響
    原問題為:"c語言中,如果main函數的末尾沒有return語句將會有什麼影響?":我是準大一,學計算機的,剛剛接觸計算機,萌新求解答原回答請參考這裡: https://www.zhihu.com/question/338814178/answer/785578903。
  • C語言裡,main 函數中 return x和 exit(x) 到底有什麼區別?
    問題:C語言裡,main 函數中 return x和 exit(x) 到底有什麼區別 ?
  • 「C語言」int main還是void main?
    從一開始學習C語言剛開始寫代碼就遇到一個有爭議的問題(那就是主函數寫法),雖然剛開始就知道```int main```才是標準的寫法,但一直沒有深刻理解為什麼不能用
  • main( )函數詳解
    C的設計原則是把函數作為程序的構成模塊。main()函數稱之為主函數,一個C程序總是從main()函數開始執行的。    return 0;}int main( int argc, char *argv[] ) /* 帶參數形式 */{    ...    return 0;}int指明了main()函數的返回類型,函數名後面的圓括號一般包含傳遞給函數的信息。void表示沒有給函數傳遞參數。
  • 學習c語言筆記——C庫函數printf()
    c語言中的printf是什麼來的?」。我答:「它是一個函數,主要用來輸出運算結果。」 ,下面就給大家介紹C庫函數printf()使用方法。下面我們通過一個調用c庫函數的c語言案例來說明printf()函數的使用方法,如c語言1。