目前而言,在編程領域中,C語言的運用非常之多,它兼顧了高級語言的匯編語言的優點,相較于其它編程語言具有較大優勢。
作者 | Martin Sebor
譯者 | 蘇本如,責編 | 劉靜
出品 | CSDN(ID:CSDNnews)
以下為譯文:
在所有標準C語言<string.h>頭文件中聲明的字符串處理函數中,最常用的是那些用來復制和連接字符串的函數。這兩組函數都將字符從一個對象復制到另一個對象,并且都返回它們的第一個參數:指向目標對象的起始指針。這種返回值的方式是導致函數效率低下的一個原因,而這正是本文要探討的主題。
本文中展示的示例代碼僅僅用于說明目的。它們可能包含細微的錯誤,不應該被視為最佳代碼實踐。
標準解決方案
這種返回函數的第一個參數的設計,有時候會被不明白其用途的用戶所質疑。這樣的例子在StackOverflow網站上有不少,例如關于strcpy返回值,或者C語言的strcpy為什么返回它的參數?的討論。簡單的答案是,這是一個歷史性的意外。函數的第一個子集是由Unix第七版在1979年引入的,它由strcat、strncat、strcpy和strncpy函數組成。盡管這四個函數都在Unix的各種版本中使用,但通常情況下,對這些函數的調用卻沒有使用它們的返回值。盡管這些函數可以同樣很容易地定義為返回一個指針來指向最后一個復制的字符(或它的后一位),而且事實證明這種做法也非常有用。
兩個或多個字符串的連接操作的最佳復雜度和字符數量成線性關系。但是,如上所述,讓函數返回指向目標字符串的指針會導致操作的效率明顯低于最佳效率。該函數遍歷源字符串序列和目標字符串序列,并獲取指向這兩個序列末尾的指針。該指針指向函數(strncpy除外)附加到目標序列上的字符串結束符NUL('>兩個或多個字符串的連接操作的最佳復雜度和字符數量成線性關系。但是,如上所述,讓函數返回指向目標字符串的指針會導致操作的效率明顯低于最佳效率。該函數遍歷源字符串序列和目標字符串序列,并獲取指向這兩個序列末尾的指針。該指針指向函數(strncpy除外)附加到目標序列上的字符串結束符NUL('\0')處或它的后一位。但是,如果返回的指針指向第一個字符而不是最后一個字符(或它的下一個字符),NUL結束符的位置會丟失,必須在需要時重新計算。這種做法的低效率可以在將兩個字符串s1和s2連接到目標緩沖區d中的示例中得到說明。將一個字符串添加到另一個字符串的慣用方法(雖然遠非理想)是調用strcpy和strcat函數,如下所示:<')處或它的后一位。但是,如果返回的指針指向第一個字符而不是最后一個字符(或它的下一個字符),NUL結束符的位置會丟失,必須在需要時重新計算。這種做法的低效率可以在將兩個字符串s1和s2連接到目標緩沖區d中的示例中得到說明。將一個字符串添加到另一個字符串的慣用方法(雖然遠非理想)是調用strcpy和strcat函數,如下所示:
strcat (strcpy (d, s1), s2);
為了執行這個連接操作,除了同時發生的相應地在d上的傳遞之外,一次在s1的傳遞和一次在s2上的傳遞是必須要執行的操作,但是上面的調用在s1上進行了兩次傳遞。讓我們把這些調用分成兩個語句。
char *d1 = strcpy (d, s1); // pass 1 over s1
strcat (d1, s2); // pass 2 over the copy of s1 in d
因為strcpy返回其第一個參數d的值,所以d1的值與d相同。為簡單起見,在后面的示例中我們將使用d,而不是將返回值存儲在d1中并使用它。在strcat調用中,我們遍歷剛剛復制到d1的字符串以確定最后一個字符的位置,這個成本和第一個字符串s1的長度是線性關系。這個成本乘以每個要連接的字符串。因而最終整個連接操作的成本相當于連接數和所以字符串長度的乘積,趨于一種二次方的關系。這種低效率是如此的臭名昭著,以至于為自己贏得了一個名字:畫師施萊米爾算法。(另見http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2349.htm#sad-string)
必須指出的是,除了效率低下之外,strcat和strcpy還因其緩沖區溢出的問題而臭名昭著,因為它們都對復制字符的數量不做任何限制。
克服局限性的嘗試
當源字符串的長度未知且目標字符串大小固定時,遵循一些流行的安全編碼準則來將連接結果限制為目標區大小實際上會導致兩個冗余的傳遞。例如,按照CERT關于安全使用strncpy和strncat 的建議,并且目標區的大小是dsize字節,我們可能會得到以下代碼。
strncpy (d, s1, dsize - 1); // pass 1 over s1 plus over d up to dsize - 1
d[dsize - 1] = '>d[dsize - 1] = '\0'; // remember to nul-terminate<'; // remember to nul-terminate
size_t n = strlen (d); // pass 2 over copy of s1 in d
strncat (d, s2, dsize - n - 1); // pass 3 over copy of s1 in d
注意,與對strncat的調用不同,當s1的長度大于d的大小時,上面對strncpy的調用不會將NUL('>注意,與對strncat的調用不同,當s1的長度大于d的大小時,上面對strncpy的調用不會將NUL('\0')結束符追加到d上。它是一個常見的想當然的錯誤。此外,當s1短于dsize-1時,strncpy函數將所有剩余的字符填滿為NUL('\0'),這也被視為一種浪費的,因為隨后對strncat的調用將覆蓋掉它們。<')結束符追加到d上。它是一個常見的想當然的錯誤。此外,當s1短于dsize-1時,strncpy函數將所有剩余的字符填滿為NUL('>注意,與對strncat的調用不同,當s1的長度大于d的大小時,上面對strncpy的調用不會將NUL('\0')結束符追加到d上。它是一個常見的想當然的錯誤。此外,當s1短于dsize-1時,strncpy函數將所有剩余的字符填滿為NUL('\0'),這也被視為一種浪費的,因為隨后對strncat的調用將覆蓋掉它們。<'),這也被視為一種浪費的,因為隨后對strncat的調用將覆蓋掉它們。
為了避免一些冗余,程序員有時會選擇先計算字符串長度,然后使用memcpy,如下所示。這種方法仍然效率不高,而且更容易出錯,并且代碼難以閱讀和維護。
size_t s1len = strlen (s1); // pass 1 over s1
if (dsize <= s1len)
s1len = dsize - 1; // no need to nul-terminate
memcpy (d, s1, s1len); // pass 2 over s1
size_t s2len = strlen (s2); // pass 1 over s2
if (dsize - s1len <= s2len)
s2len = dsize - s1len - 1;
memcpy (d + s1len, s2, s2len); // pass 2, over s2
d[s1len + s1len] = '>d[s1len + s1len] = '\0'; // nul-terminate result<'; // nul-terminate result
使用sprintf和snprintf進行連接
出于對代碼復雜性和可讀性的擔心,程序員們有時會使用snprintf函數進行字符串連接。
snprintf (d, dsize, "%s%s", s1, s2);
這樣做代碼的可讀性非常好,但是,由于snprintf的開銷相當大,它的低效率導致它可能比使用字符串函數慢幾個數量級。snprintf的開銷不僅是由于解析格式字符串,而且還由于格式化I/O函數實現中通常固有的復雜性。
一些編譯器(如GCC和Clang)試圖通過將非常簡單的sprintf和snprintf調用轉換為strcpy或memcpy調用以提高效率,避免了對I/O函數的某些調用的開銷(請參閱這個在線示例https://godbolt.org/z/RaWkyd)。然而,由于C庫中沒有等價的字符串函數,而只有當snprintf調用被證明不會導致輸出的截斷時,轉換才會完成,因此對snprintf的相應轉換很少能夠發生。memcpy本身不合適,因為它復制的字節數與指定的字節數完全相同,strncpy也不適合,因為它把目標字符串的最后的NUL結束符之后的位數都覆蓋了。
由于字符串的冗余傳遞次數,將snprintf調用轉換為strlen和memcpy調用序列產生的額外開銷,也被視為得不償失。在這個頁面上,標題為Better builtin string functions部分列出了GCC優化器在這方面的一些限制,以及改進它的一些折中措施。
POSIX的stpcpy和stpncpy函數
為了幫助解決這個問題,在過去很多年里出現了很多超出標準C的庫解決方案。POSIX標準包括stpcpy和stpncpy函數,這兩個函數的實現方法是如果找到NUL結束符,則返回指向該字符的指針。這些函數可以用來緩解上面提到的麻煩和低效率。
const char* stpcpy (char* restrict, const char* restrict);
const char* stpncpy (char* restrict, const char* restrict, size_t);
特別是,在不考慮緩沖區溢出的情況下,可以像下面這樣調用stpcpy來連接字符串:
stpcpy (stpcpy (d, s1), s2);
然而,當字符串副本必須以目標大小為邊界時,等效地使用stpncpy并不會消除將第一個NUL字符之后的剩余目標位置清零并直到邊界指定的最大字符位置的開銷。
char *ret = stpncpy (d, dsize, s1); // zeroes out d beyond the end of s1
dsize -= (ret - d);
stpncpy (d, dsize, s2); // again zeroes out d beyond the end
所以,這個函數仍然效率低下,因為對它的每次調用都會將目標中剩余的空間以及復制的字符串的末尾的空間清零。因此,這個操作的復雜性仍然是二次方的。效率低下的嚴重程度隨著目標的大小成比例地增加,而與被連接的字符串的長度成反比增加。
OpenBSD的strlcpy和strlcat函數
為了應對針對strcpy和strcat函數的弱點以及上面討論的strncpy和strncat的一些缺點的緩沖區溢出攻擊,OpenBSD項目在20世紀90年代末引入了一對替代API(strlcpy和strlcat),旨在使字符串復制和連接更加安全(http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2349.htm)。
size_t strlcpy (char* restrict, const char* restrict, size_t);
size_t strlcat (char* restrict, const char* restrict, size_t);
strncpy和strlcpy函數之間的主要區別在于返回值:前者返回指向目標的指針,后者則返回復制的字符數。另一個區別是strlcpy函數總是在目標中只存儲一個NUL結束符。要連接s1和s2,可以按以下方式使用strlcpy函數:
size_t n = strlcpy (d, s1, dsize);
dsize -= n;
d += n;
strlcpy (d, s2, dsize);
這使得strlcpy在使用性和簡單性方面都可以與snprintf相提并論(當然snprintf的開銷雖然恒定,但要大得多)。
除了OpenBSD以外,strlcpy和strlcat函數在其他系統上也可用,包括Solaris和Linux(在BSD兼容庫中)。但是由于這些系統不是由POSIX指定的,所以這兩個函數在那些系統中并不總是存在。
POSIX的memccpy函數
POSIX還定義了另一個函數memccpy,該函數具有上面討論過的所有理想屬性,可以用來解決上面的問題。
void* memccpy (void* restrict dst, const void* restrict src, int c, size_t n);
這個函數結合了memcpy、memchr的特性以及上面討論的API的最佳方面的特性。
和memchr一樣,它會掃描源序列以查找由其參數之一指定的字符的第一次出現。字符可以是任何值,包括零。
和strlcpy一樣,它最多將指定數量的字符從源序列復制到目標序列,而不會寫入超出其范圍。這解決了有關strncpy和stpncpy的低效率的報怨。
和stpcpy和stpncpy類似(盡管不完全相同),它返回一個指針,該指針指向指定字符的副本(如果存在)的后一位。(回想一下stpcpy和stpncpy返回一個指向復制的NUL的指針。)這避免了strcpy和strncpy固有的低效性。
因此,可以使用memccpy重寫上面的第一個示例(strcat(strcpy(d,s1,s2))以避免在字符串上進行任何冗余傳遞,如下所示。請注意,這里使用SIZE_MAX作為大小限制,這個重寫無法避免原始示例中存在的目標緩沖區溢出的風險,因此應避免。
memccpy (memccpy (d, s1, '>memccpy (memccpy (d, s1, '\0', SIZE_MAX) - 1, s2, '\0', SIZE_MAX);<', SIZE_MAX) - 1, s2, '>memccpy (memccpy (d, s1, '\0', SIZE_MAX) - 1, s2, '\0', SIZE_MAX);<', SIZE_MAX);
為了避免緩沖區溢出的風險,需要為每個調用確定適當的大小限制并作為參數提供。因此,像在snprintf(d, dsize, "%s%s", s1, s2)函數中那樣限制目標大小的連接調用,可以像下面這樣計算目標大小:
char *p = memccpy (d, s1, '>char *p = memccpy (d, s1, '>char *p = memccpy (d, s1, '\0', dsize);<', dsize);<', dsize);
dsize -= (p - d - 1);
memccpy (p - 1, s2, '>memccpy (p - 1, s2, '\0', dsize);<', dsize);
選擇一個解決方案
如果字符串函數返回指向最后一個存儲字符或它的后面一位的指針,而不是返回其第一個參數的值,則上面討論的效率問題可以得到解決。然而,在現有函數使用了接近半個世紀后,對其進行更改是不太可行的。
盡管解決現有C標準字符串函數的問題是不可行的,但是可以通過添加一個或多個不受相同限制的函數來在新代碼中緩解這個問題。由于C標準的章程正在對現有的實踐進行編纂整理,所以C語言標準化委員有義不容辭的責任調查這種功能是否已經存在于流行的實現中,如果已經存在,則應該考慮采納它們。如上文提到的這幾種解決方案。
在上面提到的解決方案中,memccpy函數是最通用和最高效的,它由ISO 標準支持。即使在POSIX標準實現之外,它的應用范圍最廣,爭議最小。
相比之下,stpcpy和stpncpy函數的通用性較差,stpncpy函數會產生不必要的開銷,因此無法達到既定的目標。這些函數在C2X中仍然值得采用,以提高移植性。詳情請參閱N2352–將stpcpy和stpncpy添加到C2X中的提案。
OpenBSD的strlcpy和strlcat函數雖然是最優的,但是它們的通用性較差,支持范圍也較低,而且沒有得到ISO標準的指定。
memccpy函數不僅存在于Unix實現的子集中,它還由另一個名為ISO/IEC 9945的ISO標準指定。ISO/IEC 9945還有另外一個名字,也即大家熟知的IEEE Std 1003.1, 2017版,或者簡言之- POSIX: memccpy,在那里它是作為XSI擴展提供給C的。這個函數可以追溯到System V接口定義第1版(SVID1),最初于1985年發布。
memccpy甚至可以用于UNIX和POSIX以外的實現,例如:
安卓系統中的memccpy函數,
蘋果Mac OS X中的memccpy函數,
BlackBerry Native SDK 的memccpy函數,
Compaq Run-Time Library for VAX中的memccpy函數,
微軟Visual Studio C Runtime Library中的 memccpy 函數,
IBM z/OS 中的memccpy函數.
下面提供了一個簡單(但是效率低下)的memccpy參考實現:
void* memccpy (void* restrict dst, const void* restrict src, int c, size_t n)
{
void *pc = memchr (src, c, n);
void *ret;
if (pc)
{
n = (char*)pc - (char*)src + 1;
ret = (char*)dst + n;
}
else
ret = 0;
memcpy (dst, src, n);
return ret;
}
這個函數的一個更優化的實現可能如下。
void* memccpy (void* restrict dst, const void* restrict src, int c, size_t n)
{
const char *s = src;
for (char *ret = dst; n; ++ret, ++s, --n)
{
*ret = *s;
if ((unsigned char)*ret == (unsigned char)c)
return ret + 1;
}
return 0;
}
借助于memccpy的性能優化,編譯器將能夠把對snprintf (d, dsize, "%s", s)函數的簡單調用轉換為對memccpy(d, s, '>借助于memccpy的性能優化,編譯器將能夠把對snprintf (d, dsize, "%s", s)函數的簡單調用轉換為對memccpy(d, s, '\0', dsize)的最佳有效調用。通過以代碼大小換取速度,激進的優化器甚至可以將符合下列條件的snprintf函數調用(其格式字符串由多個%s指令組成,這些指令中間穿插有普通字符,如%s/%s)轉換成一系列的此類memccpy函數調用:如下所示 <', dsize)的最佳有效調用。通過以代碼大小換取速度,激進的優化器甚至可以將符合下列條件的snprintf函數調用(其格式字符串由多個%s指令組成,這些指令中間穿插有普通字符,如%s/%s)轉換成一系列的此類memccpy函數調用:如下所示
char *p = memccpy (d, s1, '\0', dsize);
if (p)
{
--p;
p = memccpy (p, "/", '>p = memccpy (p, "/", '\0', dsize - (p - d));<', dsize - (p - d));
if (p)
{
--p;
p = memccpy (p, s2, '>p = memccpy (p, s2, '\0', dsize - (p - d));<', dsize - (p - d));
}
}
if (!p)
d[dsize - 1] = '>d[dsize - 1] = '\0';<';
2019年4月WG14會議后的更新
將memccpy函數和本文討論的其他標準函數(除了strlcpy和strlcat),以及另外兩個標準函數納入下一個C編程語言修訂版的提議于2019年4月提交給了C語言標準化委員會(見 3, 4, 5和 6)。委員會最終決定采納memccpy函數,但否決了其余提案。
原文:https://developers.redhat.com/blog/2019/08/12/efficient-string-copying-and-concatenation-in-c/
本文為 CSDN 翻譯,轉載請注明來源出處。
【END】
新聞媒體】揭秘!jquery自動抓取鏈接內容的神奇技術
近日,一項名為jquery自動抓取鏈接內容的技術引起了廣泛關注。這項技術能夠讓網頁開發者輕松實現從網頁中自動獲取鏈接內容的功能,為用戶提供更便捷的瀏覽體驗。下面,我們將為大家揭秘這項技術的原理和應用方法。
【1.背景介紹】
在互聯網時代,信息爆炸的背景下,人們對于獲取信息的需求越來越迫切。然而,傳統的網頁瀏覽方式存在著繁瑣的操作和耗費時間的問題。針對這一問題,jquery自動抓取鏈接內容技術應運而生。
【2.原理解析】
jquery自動抓取鏈接內容技術利用了jquery庫中強大的選擇器和DOM操作功能。通過選擇器定位到指定的鏈接元素,然后使用DOM操作方法獲取鏈接地址,并發送請求獲取鏈接內容。最后,將獲取到的內容展示在用戶界面上。
【3.實戰案例】
為了更好地理解jquery自動抓取鏈接內容技術的實際應用,我們以一個新聞類網站為例進行演示。假設我們需要從該網站的首頁抓取最新的新聞標題和摘要,并在我們自己的網頁中展示出來。
首先,我們使用jquery選擇器定位到新聞鏈接元素,獲取鏈接地址。然后,通過AJAX請求獲取鏈接內容。接著,解析返回的HTML文檔,提取出新聞標題和摘要等關鍵信息。最后,將這些信息動態添加到我們自己的網頁中。
通過這樣的操作,我們就能夠實現自動抓取鏈接內容,并將其展示在我們自己的網頁上。這樣一來,用戶就可以在不離開我們的網頁的情況下獲取到最新的新聞內容,大大提高了用戶體驗。
【4.注意事項】
在使用jquery自動抓取鏈接內容技術時,需要注意以下幾點:
(1)尊重知識產權:在抓取鏈接內容時,務必遵守相關法律法規,尊重原創作者的版權。
(2)合理使用技術:技術是為人類服務的工具,在使用過程中應當合理使用,并不得用于非法用途。
(3)兼容性考慮:不同瀏覽器對于jquery庫和DOM操作方法的支持有所差異,在開發過程中應當考慮兼容性問題。
【5.總結】
jquery自動抓取鏈接內容技術為網頁開發者提供了一種便捷的方式來獲取鏈接內容。通過選擇器和DOM操作方法,我們可以輕松實現自動抓取鏈接內容,并將其展示在用戶界面上。然而,在使用過程中需要注意合法合規的原則,保護知識產權,并兼顧瀏覽器兼容性。
【6.展望未來】
隨著互聯網技術的不斷發展,jquery自動抓取鏈接內容技術有望進一步完善和應用。未來,我們可以期待更多基于這項技術的創新應用,為用戶提供更加便捷、高效的網頁瀏覽體驗。
以上就是jquery自動抓取鏈接內容技術的相關介紹和應用方法。相信隨著這項技術的進一步普及和推廣,我們將能夠享受到更加便捷、高效的網頁瀏覽體驗。讓我們拭目以待吧!
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>妹妹</title>
</head>
<body>
<a href='https://www.baidu.com'>
<!-- 展示一張圖片 img-->
<img src='image/1.jpg' alt='小甜甜'/><br>
</a>
<a href='列表標簽.html' target='_blank'>列表標簽</a>
</body>
</html>
*請認真填寫需求信息,我們會在24小時內與您取得聯系。