愛伊米

乾貨 | 深度剖析C語言的main函式

乾貨 | 深度剖析C語言的main函式

main的返回值

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

void main()

{

printf(“Hello world\n”);

return;

}

執行結果:g++ test。c

乾貨 | 深度剖析C語言的main函式

main()

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

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

#include

main()

{

printf(“Hello world\n”);

return 0;

}

執行結果:

乾貨 | 深度剖析C語言的main函式

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

int main()

{

printf(“Hello world\n”);

}

執行結果:

乾貨 | 深度剖析C語言的main函式

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

測試main函式返回值的意義

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

#include

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函式傳參時,可選的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

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

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函式肯定是程式執行的第一個函式。那麼,事實果然如此嗎?相信在看了本節之後,會有不一樣的認識。

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

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

#include

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和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

__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

using namespace std;

inline int startup_1()

{

cout

return 0;

}

int static no_use_variable_startup_1 = startup_1();

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

{

cout

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

#include

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 ” );

}

學習IT相關內容,找“職座標線上”