2008/10/27

別再掉進DLL地獄的陷阱裡(DLL Hell)~.NET解決之道

資策會數位教育研究所講師 王芳芳


Introduction

DLL 陷阱是一個惡夢, 是一種相當奇怪的問題。

相信很多讀者都有這樣的經驗,如果你的軟體今天原本運作順暢,當你安裝某個新軟體之後,突然間電腦就無法運作了。這絕對不是你的硬體有問題,也不是應用程 式的問題,而是作業系統設計上的缺失,這樣的問題層出不窮,這通常是因為新的應用程式版本覆蓋掉共享的程式庫(DLL),而且往往修改了一些現存應用程式 所必需的「bug」,這個缺失有了一個名字叫做DLL Hell (DLL地獄)。開發人員與系統管理者(以及使用者)面臨最大的挑戰就是版本更新的問題 ,他們花很多時間在 Windows 登錄檔 (Regedit) 上試著解決其問題而吃盡苦頭 。

.在Microsoft .NET的世界裡,軟體元件再也不需要登錄(Registry)了! NET Framework包含了一些功能,可以實際消除「DLL Hell」的問題,一項稱之為「side-by-side」開發模式的新功能。

DLL & DLL Hell

為什麼要使用 DLL (Dynamic Linking Library) - 動態連結檔 ?

微軟當初為Windows設計動態連結檔主要是擷取它的兩項優點:一是動態連結、一是資源共享。資源共享的例子相當顯而易見,例如之前曾經提過 Windows有三個核心的動態連結檔:Kernel主要是負責系統和應用程式的記憶體、行程和執行緒等等的管理工作;User主要負責使用者介面和訊息 的傳遞;GDI則負責系統的任何圖形繪製、顯示等工作。而這些動態連結檔所提供的任何函數都可以在必要的時候,讓每一個Windows環境底下的執行檔使 用。因為DLL具備節省記憶體的特性,因此自從Windows 3.1版以來,它就逐漸成為Windows程式設計的主流


˙動態連結檔可以資源共享

許多大型軟體廠商的眾多軟體產品可能都會有許多可以共用的模組,如果每一套軟體各自擁有一份這些可以共用的模組,不僅會造成磁碟空間的浪費,還會讓維護這 些模組的工作變的既複雜、又凌亂。最好的方法就是僅保持一份程式碼,然後透過共享的方式讓其他自家的應用程式也可以存取其中共用的模組。共用模組的方法之 一就是將模組製作成動態連結檔,然後透過軟體的安裝程式複製到電腦,那麼只要安裝了其中一套軟體之後,其他自家的產品就可以互相共用這一套動態連結檔。

假設有一函數庫X供三個應用程式A、B、C使用,如果函數庫為目的碼或原始程式碼,則程式編譯之後,函數庫X將會各自成為執行檔A、B、C的一部份,而將來如果應用程式A、B、C同時執行,函數庫X也會各自佔用一份記憶體,顯然這是比較浪費記憶體的方式。

如果函數庫為DLL形式,則編譯之後,函數庫並不會成為執行檔的一部分,而將來如果應用程式A、B、C同時被執行,則系統只會載入一份函數庫讓程式A、程式B、程式C共用,如圖。



Figure: 程式與DLL的共用架構圖
 


˙動態連結檔節省記憶體空間

動態連結檔的資源共享可以節省磁碟空間,而動態載入的連結方式則可以節省記憶體空間。動態連結檔採用動態載入的連結方式,動態載入讓程式檔在需要相關的函
數或資源的時候,才載入放置在動態連結檔裡面的函數或資源,這種方式將可以有效地使用記憶體。不論是節省磁碟空間或記憶體空間,都是希望利用動態連結檔所
提供的共享函數與系統資源的方式,降低整個Windows環境對於硬體設備的需求。


DLL的問題 - DLL Hell


˙動態連結檔到底出了什麼問題?

其實DLL的優點(程式碼共用、節省記憶體),正是其缺點的起源。原本是立意良好的DLL,有一天會變成DLL Hell,恐怕是當初DLL的設計者所始料未及的。

而之所以會出現DLL Hell,也是因為動態連結檔可以與其他程式共用函數、共享資源所引起,可謂「成也共用、敗也共用」。此話怎講呢?為了要讓其他程式共用動態連結檔所提供 的函數或資源,動態連結檔的設計者必須相當謹慎地、縝密地考慮到功能的一致性、回溯相容等細節問題,否則一旦程式所使用動態連結檔沒有提供所預期的功能, 那麼使用者就會為此而掉入地獄了。

但是要完全考慮到一致性或回溯相容,實在是困難重重,就算真的要做到,也會讓利用動態連結檔的軟體廠商付出相當的成本;但,有必要付出這些成本嗎?想想現 今的電腦執行環境,與當初微軟設計動態連結檔的時候已經有相當、相當大的變化。現在的硬體比起當初已經便宜太多、太多了,個人電腦的記憶體都是從64MB 起跳,配備128MB記憶體的電腦更是比比皆是,而硬碟容量更是以GB計算。在如此的硬體環境之下,Windows程式設計師還需要這麼刻苦地考慮共用的 問題嗎?而且動態連結檔的動態載入,其實已經替Windows系統節省了不少系統資源,因此微軟也重新調整動態連結檔的設計理念,而且也針對作業系統進行 改善,希望不要再有任何使用者掉入因為共用動態連結檔而起的地獄深淵。

˙數種DLL Hell 的狀況


讓我們想一想,如果某一副程式或物件類別有90%符合我們的需求,卻有10%不符合,怎麼辦呢?對副程式來說,大概只有修改「原始程式碼」一途。

假設程式A會使用物件X,在程式A安裝到系統時,會同時安裝物件X,假設另一個程式B也會使用到物件X,那麼程式B直接複製到硬碟中即可正常運作,因為物 件X已經存在於系統中,這聽起來很好,因為程式A與程式B可以共用物件X。然而對程式A來說,原本在安裝後,執行得好好的,卻可能在未知的一天變成無法執 行,這就是所謂的DLL Hell。以下為描述DLL Hell的兩種狀態。


狀況 1. 動態連結檔沒有善盡回溯相容的責任

如果程式A使用的是1.0版的物件X,而程式B使用的是 2.0 版的物件X(通常是因為程式B開發的時間較晚,使用較新的版本),結果會怎樣呢?結果在程式B被安裝到系統時,物件X 2.0版也必須安裝到系統中,此時系統中 1.0 版的物件X將會被 2.0 版所取代。

在大部分的情況下,物件X 2.0版相容於1.0版,所以程式A依然可以正常運作,但有時候卻會出現 2.0 版及 1.0 版不相容的情況,此時程式A便無法正常執行了。此種DLL Hell的起因則是的設計者,原因在於動態連結檔沒有善盡回溯相容的原則。試著想想A.exe 需要 X.dll 所提供的功能,但是在新版的 X.dl l裡面,功能竟然被取消了,這時候也極可能發生DLL Hell。

但是誠如之前所討論,有時往往很難保證百分之百的回溯相容,而且目前個人電腦的硬體配備已經不再像以前簡陋,因此微軟也提出了所謂Side-By- Side的動態連結檔,讓程式都能擁有自己專屬的動態連結檔,進而減少共用動態連結檔以避免這種DLL Hell的發生。


狀況 2. 動態連結檔善盡回溯相容的責任, 但動態連結檔本身出現bug

另一種情況,物件X的提供者確實考慮到版本相容的問題,而根據物件的規格來看,新舊版也的確相容,但程式A使用新版的物件X就是有問題,畢竟程式A並沒有與新版的物件X一起運作過,誰知道會發生什麼情況?
 



Client

Server DLL (X.DLL)

Server Codes


程式 A 用 X 1.0

(X 1.0) (原始版本)


Public SetValue(ByVal Value
As

Integer)


If Value < color="#0000ff">Then

Value = 0


End
If


m_Value = Value


End

Property


程式 B 用 X 2.0

(X 2.0) (更新版本)


Public SetValue(ByVal Value
As

Integer)


'Fix the bug


If Value < color="#0000ff">Then

Err.Raise Number:=APP_ERROR, Description:="Negative Value "


End
If


m_Value = Value


End

Property



如上表所發生的不幸的事, 縱使 X 2.0 的開發人員小心翼翼透過 VB6.0 的二進位相容模式控制DLL版本,且所有的內部GUID值與方法和參數都完全相同,由於X 1. 0 之中有一個名為 Value 的屬性名稱, 當此一屬性設為負數時, 該屬性就會變成零, 但卻不會出現錯誤訊息。這個做法是錯誤的, X 2.0版將此臭從解決了- 若將Value屬性設為負數, 則會拋出錯誤。

當程式 B 以 X 2.0 散佈時, 這支程式B 當然也可以正常運作。不過, 如果將 X 2.0 安裝在系統之中, 程式A 會當掉。之前程式將Value 屬性設為 -1 不會有問題, 但現在會出現執行時期的錯誤。同時程式 A的開發人員並未在這設計錯誤檢視的機制。

在許多真實的案例中, 要善盡二進位相容模式控制DLL版是非常困難的, 亙何況即使是二進位相容模式控制DLL版本還是有可能造成DLL Hell。

.NET 如何解決DLL Hell 的問題

自我描述的Assembly(Self-describing Assembly)

Assembly(組合)是簡化部署與版本管理的關鍵。Assembly是部署與版本管理的基本單元,其中包含一群資源(Resource)與型別 (Type),以及它們內含的Metadata,同時也包括此組合在建造時所依賴的其他組合的版本資訊。有了獨立完整的組態資訊,同一組合的不同版本也可 安裝在同一部機器上,搭配共通語言執行環境具備根據各組合的組態資訊,載入正確版本的依賴組合(Dependent Assemblies)的能力,安裝與解除安裝的過程,就如同複製檔案與刪除檔案一樣單純,以往因先後安裝彼此覆蓋而產生的所謂“DLL Hell”的版本失控的現象不復存在。




Figure . Self-Describing Assembly 可包括多個檔案

Metadata(定義)是Assembly能自我描述的關鍵。Metadata是編輯器在產生執行碼時,伴隨產生的定義性資訊,舉凡元件所使用的型別、 屬性、方法、事件甚至輔助與備註等資訊都可包含在內,而且保證與執行碼的一致性,完全取代並且超越了傳統分離式的IDL(Interface Definition Language) 檔案與型別庫(Type Library)所扮演的功能,同時元件服務要求與執行所需的資訊皆動態來自Metadata 。.asseembly directive 作為辨識 assembly 本身 , .assembly extern directives 定義此 assembly 所依賴的其他 Assemblies。



Figure . 可使用 IL Disassembler (Ildasm)呈現 DLL 的 metadata


簡化的部署與版本管理

˙Application-Private Assemblies (Isolated Assembly)
Application-Private Assemblies (or 被隔離的 assembly) 只能被一個應用程式所使用- 它不能被其他的應用程式所共用。隔離 assembly 讓程式開發者有著對應用程式絕對的控制權,這也是 .NET應用程式的預設方式。

開發好的Application-Private Assemblies 要在另一個 .NET 環境進行安裝時,手續只有一個,就是Copy And Paste。只要把編譯好的程式,無論是EXE執行檔、DLL元件、ASP.NET的.aspx網頁或Web Service的.asmx檔,全部都是以複製/貼上的方式部署在和應用程式相同的目錄,這些檔案複製完成後,不需額外註冊或設定。.NET程式執行時, 如果需要額外的元件,首先會自本身執行檔下的同一目錄開始尋找,因此,每套應用程式預設都是使用本身同一目錄下的元件,不同應用程式間不會相互干擾,也消 除了DLL Hell 的困擾。(註: The CLR finds these assemblies through a process called probing. Probing is simply a mapping of the assembly name to the name of the file that contains the manifest.)

˙Shared Assemblies - Side by side execution (並排執行)
然而,應用程式共享 assembly 還是有其必要性, 因為讓每個應用程式都有自己一份 copy (如 System.Windowns.Forms, System.Web or a common Web Forms control )是件很奇怪的事 。

為了解決DLL Hell的問題,.NET增加了一種新的技術,稱為Side by side execution,意思是應用程式可以擁有各自版本的DLL,例如程式 A使用版本1.0的物件、而程式B使用版本2.0的物件,1.0版與2.0版的物件可以同時在系統執行。

透過Side by side execution的技術,應用程式只要安裝成功之後,就不用擔心DLL更新版本,或規格的改變,因為就算DLL改朝了,應該程式也不用換代。以下簡述 Side by side execution的過程圖 及應用步驟:



Figure . Process for implementing strong names

step 1. create a key pair using the Strong Name tool (Sn.exe) .

Sn –k MyKey.snk
嚴格名稱(Strong names)是一種在.NET 架構下可減少 .NET DLL 陷阱的功能

(註: The author of an assembly generates a key pair, signs the file containing the manifest with the private key, and makes the public key available to callers by the Strong Name tool. The key pair is passed to the compiler using the custom Assembly attribute )


Step 2. sign an assembly with a strong name and version number:

In Assemblyinfo.vb,


Step 3. Install Assembly to Global assembly cache (GAC)
gacutil /i:myassembly.dll

(註: See the .NET Framework SDK documentation for a full description of the options supported by gacutil.)



Figure : Global Assembly Cache

Step 4. 應用程式與 Assembly 的 版本繫結 (Version Policy)

.NET 提供有組態設定機制可以控制Assembly 的繫結, 故在程式中可以載入相關 Assembly的升級版本。組態設定機制是由XML設定檔來負責,透過這種安全機制可以控制程式的安全、版本以及遠端的功能。每一個應用程式可以有XML程式設定檔來指定應用程式所要繫結Assembly的不同版本

程式設定檔的檔案名稱為程式名稱加上 .config 副檔名, 如 Myapp.exe.config。

的標記用來指是 .NET 載入更新版的 Assembly 。

例如以下範例設定檔會載入MarineCtrl 版本 5.0.0.1 而非版本 5.0.0.0




Summary

The .NET Framework NET Assembly 自我描述與版本管理功能讓 zero-impact 的部署安裝成為可能,同時也終結了DLL Hell 。
Application-Private Assemblies (or 被隔離的 assembly) 只能被一個應用程式所使用 - 它不會被其他的應用程式所影響。 隔離的 assembly 讓程式開發者對應用程式有著絕對的控制權,開發好的Application-Private Assemblies只要部署在和應用程式同一目錄即可。

透過Side by side execution的技術,應用程式只要安裝成功之後,就不用擔心DLL更新版本,或規格的改變, 它允許 一個 assembly 的多個版本在一個機器上同時被安裝並執行, 而且每一個應用程式都可以要求和不同的 Assembly 版本繫結。

The .NET Framework 紀錄應用程式版本資訊,並在執行應用程式時使用此資訊載入應用程式所需依賴的正確版本的 Assemblies。

2008/10/15

C/C++中的日期和時間 time_t與struct tm轉換

作者:吳文力

摘要:
本文從介紹基礎概念入手,探討了在C/C++中對日期和時間操作所用到的數據結構和函數,並對計時、時間的獲取、時間的計算和顯示格式等方面進行了闡述。本文還通過大量的實例向你展示了time.h頭文件中聲明的各種函數和數據結構的詳細使用方法。

關鍵字:UTC(世界標准時間),Calendar Time(日歷時間),epoch(時間點),clock tick(時鐘計時單元)


1.概念
在 C/C++中,對字符串的操作有很多值得注意的問題,同樣,C/C++對時間的操作也有許多值得大家注意的地方。最近,在技術群中有很多網友也多次問到過 C++語言中對時間的操作、獲取和顯示等等的問題。下面,在這篇文章中,筆者將主要介紹在C/C++中時間和日期的使用方法.

通過學習許多C/C++庫,你可以有很多操作、使用時間的方法。但在這之前你需要了解一些「時間」和「日期」的概念,主要有以下幾個:

Coordinated Universal Time(UTC):協調世界時,又稱為世界標准時間,也就是大家所熟知的格林威治標准時間(Greenwich Mean Time,GMT)。比如,中國內地的時間與UTC的時差為+8,也就是UTC+8。美國是UTC-5。

Calendar Time:日歷時間,是用「從一個標准時間點到此時的時間經過的秒數」來表示的時間。這個標准時間點對不同的編譯器來說會有所不同,但對一個編譯系統來說,這個標准時間點是不變的,該編譯系統中的時間對應的日歷時間都通過該標准時間點來衡量,所以可以說日歷時間是「相對時間」,但是無論你在哪一個時區,在同一時刻對同一個標准時間點來說,日歷時間都是一樣的。

epoch:時間點。時間點在標准C/C++中是一個整數,它用此時的時間和標准時間點相差的秒數(即日歷時間)來表示。

clock tick:時鐘計時單元(而不把它叫做時鐘滴答次數),一個時鐘計時單元的時間長短是由CPU控制的。一個clock tick不是CPU的一個時鐘周期,而是C/C++的一個基本計時單位。

我們可以使用ANSI標准庫中的time.h頭文件。這個頭文件中定義的時間和日期所使用的方法,無論是在結構定義,還是命名,都具有明顯的C語言風格。下面,我將說明在C/C++中怎樣使用日期的時間功能。

2. 計時

C/C++中的計時函數是clock(),而與其相關的數據類型是clock_t。在MSDN中,查得對clock函數定義如下:

clock_t clock( void );

這個函數返回從「開啟這個程序進程」到「程序中調用clock()函數」時之間的CPU時鐘計時單元(clock tick)數,在MSDN中稱之為掛鐘時間(wall-clock)。其中clock_t是用來保存時間的數據類型,在time.h文件中,我們可以找到對它的定義:

#ifndef _CLOCK_T_DEFINED
typedef long clock_t;
#define _CLOCK_T_DEFINED
#endif

很明顯,clock_t是一個長整形數。在time.h文件中,還定義了一個常量CLOCKS_PER_SEC,它用來表示一秒鐘會有多少個時鐘計時單元,其定義如下:

#define CLOCKS_PER_SEC ((clock_t)1000)

可以看到可以看到每過千分之一秒(1毫秒),調用clock()函數返回的值就加1。下面舉個例子,你可以使用公式clock()/CLOCKS_PER_SEC來計算一個進程自身的運行時間:

void elapsed_time()
{
printf("Elapsed time:%u secs.\n",clock()/CLOCKS_PER_SEC);
}

當然,你也可以用clock函數來計算你的機器運行一個循環或者處理其它事件到底花了多少時間:

#include 「stdio.h」
#include 「stdlib.h」
#include 「time.h」

int main( void )
{
long i = 10000000L;
clock_t start, finish;
double duration;
/* 測量一個事件持續的時間*/
printf( "Time to do %ld empty loops is ", i );
start = clock();
while( i-- ) ;
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
printf( "%f seconds\n", duration );
system("pause");
}

在筆者的機器上,運行結果如下:

Time to do 10000000 empty loops is 0.03000 seconds

上面我們看到時鐘計時單元的長度為1毫秒,那麼計時的精度也為1毫秒,那麼我們可不可以通過改變CLOCKS_PER_SEC的定義,通過把它定義的大一些,從而使計時精度更高呢?通過嘗試,你會發現這樣是不行的。在標准C/C++中,最小的計時單位是一毫秒。

3.與日期和時間相關的數據結構

在標准C/C++中,我們可通過tm結構來獲得日期和時間,tm結構在time.h中的定義如下:

#ifndef _TM_DEFINED
struct tm {
int tm_sec; /* 秒 – 取值區間為[0,59] */
int tm_min; /* 分 - 取值區間為[0,59] */
int tm_hour; /* 時 - 取值區間為[0,23] */
int tm_mday; /* 一個月中的日期 - 取值區間為[1,31] */
int tm_mon; /* 月份(從一月開始,0代表一月) - 取值區間為[0,11] */
int tm_year; /* 年份,其值等於實際年份減去1900 */
int tm_wday; /* 星期 – 取值區間為[0,6],其中0代表星期天,1代表星期一,以此類推 */
int tm_yday; /* 從每年的1月1日開始的天數 – 取值區間為[0,365],其中0代表1月1日,1代表1月2日,以此類推 */
int tm_isdst; /* 夏令時標識符,實行夏令時的時候,tm_isdst為正。不實行夏令時的進候,tm_isdst為0;不了解情況時,tm_isdst()為負。*/
};
#define _TM_DEFINED
#endif

ANSI C標准稱使用tm結構的這種時間表示為分解時間(broken-down time)。

而日歷時間(Calendar Time)是通過time_t數據類型來表示的,用time_t表示的時間(日歷時間)是從一個時間點(例如:1970年1月1日0時0分0秒)到此時的秒數。在time.h中,我們也可以看到time_t是一個長整型數:

#ifndef _TIME_T_DEFINED
typedef long time_t; /* 時間值 */
#define _TIME_T_DEFINED /* 避免重復定義 time_t */
#endif

大家可能會產生疑問:既然time_t實際上是長整型,到未來的某一天,從一個時間點(一般是1970年1月1日0時0分0秒)到那時的秒數(即日歷時間)超出了長整形所能表示的數的范圍怎麼辦?對time_t數據類型的值來說,它所表示的時間不能晚於2038年1月18日19時14分07秒。為了能夠表示更久遠的時間,一些編譯器廠商引入了64位甚至更長的整形數來保存日歷時間。比如微軟在Visual C++中采用了__time64_t數據類型來保存日歷時間,並通過_time64()函數來獲得日歷時間(而不是通過使用32位字的time()函數),這樣就可以通過該數據類型保存3001年1月1日0時0分0秒(不包括該時間點)之前的時間。

在time.h頭文件中,我們還可以看到一些函數,它們都是以time_t為參數類型或返回值類型的函數:

double difftime(time_t time1, time_t time0);
time_t mktime(struct tm * timeptr);
time_t time(time_t * timer);
char * asctime(const struct tm * timeptr);
char * ctime(const time_t *timer);

此外,time.h還提供了兩種不同的函數將日歷時間(一個用time_t表示的整數)轉換為我們平時看到的把年月日時分秒分開顯示的時間格式tm:

struct tm * gmtime(const time_t *timer);
struct tm * localtime(const time_t * timer);

通過查閱MSDN,我們可以知道Microsoft C/C++ 7.0中時間點的值(time_t對象的值)是從1899年12月31日0時0分0秒到該時間點所經過的秒數,而其它各種版本的Microsoft C/C++和所有不同版本的Visual C++都是計算的從1970年1月1日0時0分0秒到該時間點所經過的秒數。

4.與日期和時間相關的函數及應用
在本節,我將向大家展示怎樣利用time.h中聲明的函數對時間進行操作。這些操作包括取當前時間、算時間間隔、以不同的形式顯示時間等內容。

4.1 獲得日歷時間

我們可以通過time()函數來獲得日歷時間(Calendar Time),其原型為:

time_t time(time_t * timer);

如果你已經聲明了參數timer,你可以從參數timer返回現在的日歷時間,同時也可以通過返回值返回現在的日歷時間,即從一個時間點(例如:1970年 1月1日0時0分0秒)到現在此時的秒數。如果參數為空(NULL),函數將只通過返回值返回現在的日歷時間,比如下面這個例子用來顯示當前的日歷時間:

#include "time.h"
#include "stdio.h"
int main(void)
{
struct tm *ptr;
time_t lt;
lt =time(NULL);
printf("The Calendar Time now is %d\n",lt);
return 0;
}

運行的結果與當時的時間有關,我當時運行的結果是:

The Calendar Time now is 1122707619

其中1122707619就是我運行程序時的日歷時間。即從1970年1月1日0時0分0秒到此時的秒數。

4.2 獲得日期和時間

這裡說的日期和時間就是我們平時所說的年、月、日、時、分、秒等信息。從第2節我們已經知道這些信息都保存在一個名為tm的結構體中,那麼如何將一個日歷時間保存為一個tm結構的對象呢?

其中可以使用的函數是gmtime()和localtime(),這兩個函數的原型為:

struct tm * gmtime(const time_t *timer);
struct tm * localtime(const time_t * timer);

其中gmtime()函數是將日歷時間轉化為世界標准時間(即格林尼治時間),並返回一個tm結構體來保存這個時間,而localtime()函數是將日歷時間轉化為本地時間。比如現在用gmtime()函數獲得的世界標准時間是2005年7月30日7點18分20秒,那麼我用localtime()函數在中國地區獲得的本地時間會比時間標准時間晚8個小時,即2005年7月30日15點18分20秒。下面是個例子:

#include "time.h"
#include "stdio.h"
int main(void)
{
struct tm *local;
time_t t;
t=time(NULL);
local=localtime(&t);
printf("Local hour is: %d\n",local->tm_hour);
local=gmtime(&t);
printf("UTC hour is: %d\n",local->tm_hour);
return 0;
}

運行結果是:

Local hour is: 15
UTC hour is: 7

4.3 固定的時間格式

我們可以通過asctime()函數和ctime()函數將時間以固定的格式顯示出來,兩者的返回值都是char*型的字符串。返回的時間格式為:

星期幾 月份 日期 時:分:秒 年\n\0
例如:Wed Jan 02 02:03:55 1980\n\0

其中\n是一個換行符,\0是一個空字符,表示字符串結束。下面是兩個函數的原型:

char * asctime(const struct tm * timeptr);
char * ctime(const time_t *timer);

其中asctime()函數是通過tm結構來生成具有固定格式的保存時間信息的字符串,而ctime()是通過日歷時間來生成時間字符串。這樣的話,asctime()函數只是把tm結構對象中的各個域填到時間字符串的相應位置就行了,而ctime()函數需要先參照本地的時間設置,把日歷時間轉化為本地時間,然後再生成格式化後的字符串。在下面,如果lt是一個非空的time_t變量的話,那麼:

printf(ctime(<));

等價於:

struct tm *ptr;
ptr=localtime(<);
printf(asctime(ptr));

那麼,下面這個程序的兩條printf語句輸出的結果就是不同的了(除非你將本地時區設為世界標准時間所在的時區):

#include "time.h"
#include "stdio.h"
int main(void)
{
struct tm *ptr;
time_t lt;
lt =time(NULL);
ptr=gmtime(<);
printf(asctime(ptr));
printf(ctime(<));
return 0;
}

運行結果:

Sat Jul 30 08:43:03 2005
Sat Jul 30 16:43:03 2005

4.4 自定義時間格式

我們可以使用strftime()函數將時間格式化為我們想要的格式。它的原型如下:

size_t strftime(
char *strDest,
size_t maxsize,
const char *format,
const struct tm *timeptr
);

我們可以根據format指向字符串中格式命令把timeptr中保存的時間信息放在strDest指向的字符串中,最多向strDest中存放maxsize個字符。該函數返回向strDest指向的字符串中放置的字符數。

函數strftime()的操作有些類似於sprintf():識別以百分號(%)開始的格式命令集合,格式化輸出結果放在一個字符串中。格式化命令說明串 strDest中各種日期和時間信息的確切表示方法。格式串中的其他字符原樣放進串中。格式命令列在下面,它們是區分大小寫的。

%a 星期幾的簡寫
%A 星期幾的全稱
%b 月分的簡寫
%B 月份的全稱
%c 標准的日期的時間串
%C 年份的後兩位數字
%d 十進制表示的每月的第幾天
%D 月/天/年
%e 在兩字符域中,十進制表示的每月的第幾天
%F 年-月-日
%g 年份的後兩位數字,使用基於周的年
%G 年分,使用基於周的年
%h 簡寫的月份名
%H 24小時制的小時
%I 12小時制的小時
%j 十進制表示的每年的第幾天
%m 十進制表示的月份
%M 十時制表示的分鐘數
%n 新行符
%p 本地的AM或PM的等價顯示
%r 12小時的時間
%R 顯示小時和分鐘:hh:mm
%S 十進制的秒數
%t 水平制表符
%T 顯示時分秒:hh:mm:ss
%u 每周的第幾天,星期一為第一天 (值從0到6,星期一為0)
%U 第年的第幾周,把星期日做為第一天(值從0到53)
%V 每年的第幾周,使用基於周的年
%w 十進制表示的星期幾(值從0到6,星期天為0)
%W 每年的第幾周,把星期一做為第一天(值從0到53)
%x 標准的日期串
%X 標准的時間串
%y 不帶世紀的十進制年份(值從0到99)
%Y 帶世紀部分的十制年份
%z,%Z 時區名稱,如果不能得到時區名稱則返回空字符。
%% 百分號

如果想顯示現在是幾點了,並以12小時制顯示,就象下面這段程序:

#include 「time.h」
#include 「stdio.h」
int main(void)
{
struct tm *ptr;
time_t lt;
char str[80];
lt=time(NULL);
ptr=localtime(<);
strftime(str,100,"It is now %I %p",ptr);
printf(str);
return 0;
}

其運行結果為:
It is now 4PM

而下面的程序則顯示當前的完整日期:

#include
#include

void main( void )
{
struct tm *newtime;
char tmpbuf[128];
time_t lt1;
time( <1 );
newtime=localtime(<1);
strftime( tmpbuf, 128, "Today is %A, day %d of %B in the year %Y.\n", newtime);
printf(tmpbuf);
}

運行結果:

Today is Saturday, day 30 of July in the year 2005.

4.5 計算持續的時間長度

有時候在實際應用中要計算一個事件持續的時間長度,比如計算打字速度。在第1節計時部分中,我已經用clock函數舉了一個例子。Clock()函數可以精確到毫秒級。同時,我們也可以使用difftime()函數,但它只能精確到秒。該函數的定義如下:

double difftime(time_t time1, time_t time0);

雖然該函數返回的以秒計算的時間間隔是double類型的,但這並不說明該時間具有同double一樣的精確度,這是由它的參數覺得的(time_t是以秒為單位計算的)。比如下面一段程序:

#include 「time.h」
#include 「stdio.h」
#include 「stdlib.h」
int main(void)
{
time_t start,end;
start = time(NULL);
system("pause");
end = time(NULL);
printf("The pause used %f seconds.\n",difftime(end,start));//<-
system("pause");
return 0;
}

運行結果為:
請按任意鍵繼續. . .
The pause used 2.000000 seconds.
請按任意鍵繼續. . .

可以想像,暫停的時間並不那麼巧是整整2秒鐘。其實,你將上面程序的帶有「//<-」注釋的一行用下面的一行代碼替換:

printf("The pause used %f seconds.\n",end-start);

其運行結果是一樣的。

4.6 分解時間轉化為日歷時間

這裡說的分解時間就是以年、月、日、時、分、秒等分量保存的時間結構,在C/C++中是tm結構。我們可以使用mktime()函數將用tm結構表示的時間轉化為日歷時間。其函數原型如下:

time_t mktime(struct tm * timeptr);

其返回值就是轉化後的日歷時間。這樣我們就可以先制定一個分解時間,然後對這個時間進行操作了,下面的例子可以計算出1997年7月1日是星期幾:

#include "time.h"
#include "stdio.h"
#include "stdlib.h"
int main(void)
{
struct tm t;
time_t t_of_day;
t.tm_year=1997-1900;
t.tm_mon=6;
t.tm_mday=1;
t.tm_hour=0;
t.tm_min=0;
t.tm_sec=1;
t.tm_isdst=0;
t_of_day=mktime(&t);
printf(ctime(&t_of_day));
return 0;
}

運行結果:

Tue Jul 01 00:00:01 1997

現在注意了,有了mktime()函數,是不是我們可以操作現在之前的任何時間呢?你可以通過這種辦法算出1945年8月15號是星期幾嗎?答案是否定的。因為這個時間在1970年1月1日之前,所以在大多數編譯器中,這樣的程序雖然可以編譯通過,但運行時會異常終止。

5.總結

本文介紹了標准C/C++中的有關日期和時間的概念,並通過各種實例講述了這些函數和數據結構的使用方法。筆者認為,和時間相關的一些概念是相當重要的,理解這些概念是理解各種時間格式的轉換的基礎,更是應用這些函數和數據結構的基礎。

2024年React state management趨勢

輕量化 在過去Redux 是 React 狀態管理的首選函式庫。 Redux 提供了強大的功能和靈活性,但也帶來了一定的學習成本和複雜度。 隨著 React 生態的不斷發展,越來越多的開發者開始追求輕量化的狀態管理函式庫。 Zustand 和 Recoil 等庫以其簡單易用、性...