C/C++編譯原理

来源:https://www.cnblogs.com/xujinzhong/archive/2018/03/19/8599539.html
-Advertisement-
Play Games

轉自:http://m.blog.csdn.net/blog/business122/21722039 http://m.blog.csdn.net/blog/business122/21722151 C/C++編譯就是要將C/C++的代碼映射到相應的機器碼,以及討論其中的記憶體管理模式,包括記憶體的分 ...


轉自:http://m.blog.csdn.net/blog/business122/21722039    http://m.blog.csdn.net/blog/business122/21722151

C/C++編譯就是要將C/C++的代碼映射到相應的機器碼,以及討論其中的記憶體管理模式,包括記憶體的分配,如何使用等等,整型、數組、指針等這些在記憶體中的實現機制。

C/C++的編譯包括幾個部分,分別是編譯,彙編和鏈接。

1. 編譯,就是將相應的高級語言代碼映射到彙編語言,處理define,include等命令,載入外部的代碼;

2. 彙編,就是將彙編語言映射到機器碼;

3. 鏈接,形成相應的動態和靜態鏈接庫。動態連接庫是在程式運行時動態的載入,靜態連接庫是直接拷貝進入程式,在程式執行時,這些靜態連接庫載入進來。

第一篇:     
首先是預編譯,這一步可以粗略的認為只做了一件事情,那就是“巨集展開”,也就是對那些#***的命令的一種展開。

      例如define MAX 1000就是建立起MAX和1000之間的對等關係,好在編譯階段進行替換。

      例如ifdef/ifndef就是從一個文件中有選擇性的挑出一些符合條件的代碼來交給下一步的編譯階段來處理。這裡面最複雜的莫過於include了,其實也很簡單,就是相當於把那個對應的文件裡面的內容一下子替換到這條include***語句的地方來。

      其次是編譯,這一步很重要,編譯是以一個個獨立的文件作為單元的,一個文件就會編譯出一個目標文件。(這裡插入一點關於編譯的文件的說明,編譯器通過尾碼名來辨識是否編譯該文件,因此“.h”的頭文件一概不理會,而“.cpp”的源文件一律都要被編譯,我實驗過把.h文件的尾碼名改為.cpp,然後在include的地方相應的改為***.cpp,這樣一來,編譯器就會編譯許多不必要的頭文件,只不過頭文件里我們通常只放置聲明而不是定義,因此最後鏈接生成的可執行文件的大小是不會改變的)

      清楚編譯是以一個個單獨的文件為單元的,這一點很重要,因此編譯只負責本單元的那些事,而對外部的事情一概不理會,在這一步里,我們可以調用一個函數而不必給出這個函數的定義,但是要在調用前得到這個函數的聲明(其實這就是include的本質,不就是為了給你提前提供個聲明而好讓你使用嗎?至於那個函數到底是如何實現的,需要在鏈接這一步里去找函數的入口地址。因此提供聲明的方式可以是用include把放在別的文件中的聲明拿過來,也可以是在調用之前自己寫一句void max(int,int);都行。),編譯階段剩下的事情就是分析語法的正確性之類的工作了。好啦,總結一下,可以粗略的認為編譯階段分兩步:    

        第一步,檢驗函數或者變數是否存在它們的聲明;

        第二步,檢查語句是否符合C++語法。

最後一步是鏈接,它會把所有編譯好的單元全部鏈接為一個整體文件,其實這一步可以比作一個“連線”的過程,比如A文件用了B文件中的函數,那麼鏈接的這一步會建立起這個關聯。鏈接時最重要的我認為是檢查全局空間裡面是不是有重覆定義或者缺失定義。這也就解釋了為什麼我們一般不在頭文件中出現定義,因為頭文件有可能被釋放到多個源文件中,每個源文件都會單獨編譯,鏈接時就會發現全局空間中有多個定義了。

標準C和C++將編譯過程定義為9個階段(Phases of Translation):

1.字元映射(Character Mapping)

    文件中的物理源字元被映射到源字元集中,其中包括三字元運算符的替換、控制字元(行尾的回車換行)的替換。許多非美式鍵盤不支持基本源字元集中的一些字元,文件中可用三字元來代替這些基本源字元,以??為前導。但如果所用鍵盤是美式鍵盤,有些編譯器可能不對三字元進行查找和替換,需要增加-trigraphs編譯參數。在C++程式中,任何不在基本源字元集中的字元都被它的通用字元名替換。

2.行合併(Line Splicing)

    以反斜杠/結束的行和它接下來的行合併。

3.標記化(Tokenization)

    每一條註釋被一個單獨的空字元所替換。C++雙字元運算符被識別為標記(為了開發可讀性更強的程式,C++為非ASCII碼開發者定義了一套雙字元運算符集和新的保留字集)。源代碼被分析成預處理標記。

4.預處理(Preprocessing)

    調用預處理指令並擴展巨集。使用#include指令包含的文件,重覆步驟1到4。上述四個階段統稱為預處理階段。

5.字元集映射(Character-set Mapping)

    源字元集成員、轉義序列被轉換成等價的執行字元集成員。例如:'/a'在ASCII環境下會被轉換成值為一個位元組,值為7。

6.字元串連接(String Concatenation)

    相鄰的字元串被連接。例如:"""hahaha""huohuohuo"將成為"hahahahuohuohuo"。

7.翻譯(Translation)

    進行語法和語義分析編譯,並翻譯成目標代碼。

8.處理模板

    處理模板實例。

9.連接(Linkage)

    解決外部引用的問題,準備好程式映像以便執行。

第二篇:

一、C++編譯模式
通常,在一個C++程式中,只包含兩類文件——.cpp文件和.h文件。其中,.cpp文件被稱作C++源文件,裡面放的都是C++的源代碼;而.h文件則被稱作C++頭文件,裡面放的也是C++的源代碼。
C+ +語言支持“分別編譯”(separate compilation)。也就是說,一個程式所有的內容,可以分成不同的部分分別放在不同的.cpp文件里。.cpp文件里的東西都是相對獨立的,在編 譯(compile)時不需要與其他文件互通,只需要在編譯成目標文件後再與其他的目標文件做一次鏈接(link)就行了。比如,在文件a.cpp中定義 了一個全局函數“void a() {}”,而在文件b.cpp中需要調用這個函數。即使這樣,文件a.cpp和文件b.cpp並不需要相互知道對方的存在,而是可以分別地對它們進行編譯, 編譯成目標文件之後再鏈接,整個程式就可以運行了。
這是怎麼實現的呢?從寫程式的角度來講,很簡單。在文件b.cpp中,在調用 “void a()”函數之前,先聲明一下這個函數“void a();”,就可以了。這是因為編譯器在編譯b.cpp的時候會生成一個符號表(symbol table),像“void a()”這樣的看不到定義的符號,就會被存放在這個表中。再進行鏈接的時候,編譯器就會在別的目標文件中去尋找這個符號的定義。一旦找到了,程式也就可以 順利地生成了。
註意這裡提到了兩個概念,一個是“定義”,一個是“聲明”。簡單地說,“定義”就是把一個符號完完整整地描述出來:它是變 量還是函數,返回什麼類型,需要什麼參數等等。而“聲明”則只是聲明這個符號的存在,即告訴編譯器,這個符號是在其他文件中定義的,我這裡先用著,你鏈接 的時候再到別的地方去找找看它到底是什麼吧。定義的時候要按C++語法完整地定義一個符號(變數或者函數),而聲明的時候就只需要寫出這個符號的原型了。 需要註意的是,一個符號,在整個程式中可以被聲明多次,但卻要且僅要被定義一次。試想,如果一個符號出現了兩種不同的定義,編譯器該聽誰的?
這 種機制給C++程式員們帶來了很多好處,同時也引出了一種編寫程式的方法。考慮一下,如果有一個很常用的函數“void f() {}”,在整個程式中的許多.cpp文件中都會被調用,那麼,我們就只需要在一個文件中定義這個函數,而在其他的文件中聲明這個函數就可以了。一個函數還 好對付,聲明起來也就一句話。但是,如果函數多了,比如是一大堆的數學函數,有好幾百個,那怎麼辦?能保證每個程式員都可以完完全全地把所有函數的形式都 準確地記下來並寫出來嗎?

二、什麼是頭文件
很顯然,答案是不可能。但是有一個很簡單地辦法,可以幫助程式員們省去記住那麼多函數原型的麻煩:我們可以把那幾百個函數的聲明語句全都先寫好,放在一個文件里,等到程式員需要它們的時候,就把這些東西全部copy進他的源代碼中。
這 個方法固然可行,但還是太麻煩,而且還顯得很笨拙。於是,頭文件便可以發揮它的作用了。所謂的頭文件,其實它的內容跟.cpp文件中的內容是一樣的,都是 C++的源代碼。但頭文件不用被編譯。我們把所有的函數聲明全部放進一個頭文件中,當某一個.cpp源文件需要它們時,它們就可以通過一個巨集命令 “#include”包含進這個.cpp文件中,從而把它們的內容合併到.cpp文件中去。當.cpp文件被編譯時,這些被包含進去的.h文件的作用便發 揮了。
舉一個例子吧,假設所有的數學函數只有兩個:f1和f2,那麼我們把它們的定義放在math.cpp里:
/* math.cpp */
double f1()
{
//do something here....
return;
}
double f2(double a)
{
//do something here...
return a * a;
}
/* end of math.cpp */
並把“這些”函數的聲明放在一個頭文件math.h中:
/* math.h */

double f1();
double f2(double);
/* end of math.h */
在另一個文件main.cpp中,我要調用這兩個函數,那麼就只需要把頭文件包含進來:
/* main.cpp */
#include "math.h"
main()
{
int number1 = f1();
int number2 = f2(number1);
}
/* end of main.cpp */
這 樣,便是一個完整的程式了。需要註意的是,.h文件不用寫在編譯器的命令之後,但它必須要在編譯器找得到的地方(比如跟main.cpp在一個目錄下)。 main.cpp和math.cpp都可以分別通過編譯,生成main.o和math.o,然後再把這兩個目標文件進行鏈接,程式就可以運行了。

三、#include
#include 是一個來自C語言的巨集命令,它在編譯器進行編譯之前,即在預編譯的時候就會起作用。#include的作用是把它後面所寫的那個文件的內容,完完整整地、 一字不改地包含到當前的文件中來。值得一提的是,它本身是沒有其它任何作用與副功能的,它的作用就是把每一個它出現的地方,替換成它後面所寫的那個文件的 內容。簡單的文本替換,別無其他。因此,main.cpp文件中的第一句(#include "math.h"),在編譯之前就會被替換成math.h文件的內容。即在編譯過程將要開始的時候,main.cpp的內容已經發生了改變:
/* ~main.cpp */
double f1();
double f2(double);
main()
{
int number1 = f1();
int number2 = f2(number1);
}
/* end of ~main.cpp */
不多不少,剛剛好。同理可知,如果我們除了main.cpp以外,還有其他的很多.cpp文件也用到了f1和f2函數的話,那麼它們也通通只需要在使用這兩個函數前寫上一句#include "math.h"就行了。

四、頭文件中應該寫什麼
通 過上面的討論,我們可以瞭解到,頭文件的作用就是被其他的.cpp包含進去的。它們本身並不參與編譯,但實際上,它們的內容卻在多個.cpp文件中得到了 編譯。通過“定義只能有一次”的規則,我們很容易可以得出,頭文件中應該只放變數和函數的聲明,而不能放它們的定義。因為一個頭文件的內容實際上是會被引 入到多個不同的.cpp文件中的,並且它們都會被編譯。放聲明當然沒事,如果放了定義,那麼也就相當於在多個文件中出現了對於一個符號(變數或函數)的定 義,縱然這些定義都是相同的,但對於編譯器來說,這樣做不合法。
所以,應該記住的一點就是,.h頭文件中,只能存在變數或者函數的聲明, 而不要放定義。即,只能在頭文件中寫形如:extern int a;和void f();的句子。這些才是聲明。如果寫上int a;或者void f() {}這樣的句子,那麼一旦這個頭文件被兩個或兩個以上的.cpp文件包含的話,編譯器會立馬報錯。(關於extern,前面有討論過,這裡不再討論定義跟 聲明的區別了。)
但是,這個規則是有三個例外的。
一,頭文件中可以寫const對象的定義。因為全局的const對象默 認是沒有extern的聲明的,所以它只在當前文件中有效。把這樣的對象寫進頭文件中,即使它被包含到其他多個.cpp文件中,這個對象也都只在包含它的 那個文件中有效,對其他文件來說是不可見的,所以便不會導致多重定義。同時,因為這些.cpp文件中的該對象都是從一個頭文件中包含進去的,這樣也就保證 了這些.cpp文件中的這個const對象的值是相同的,可謂一舉兩得。同理,static對象的定義也可以放進頭文件。
二,頭文件中可 以寫內聯函數(inline)的定義。因為inline函數是需要編譯器在遇到它的地方根據它的定義把它內聯展開的,而並非是普通函數那樣可以先聲明再鏈 接的(內聯函數不會鏈接),所以編譯器就需要在編譯時看到內聯函數的完整定義才行。如果內聯函數像普通函數一樣只能定義一次的話,這事兒就難辦了。因為在 一個文件中還好,我可以把內聯函數的定義寫在最開始,這樣可以保證後面使用的時候都可以見到定義;但是,如果我在其他的文件中還使用到了這個函數那怎麼辦 呢?這幾乎沒什麼太好的解決辦法,因此C++規定,內聯函數可以在程式中定義多次,只要內聯函數在一個.cpp文件中只出現一次,並且在所有的.cpp文 件中,這個內聯函數的定義是一樣的,就能通過編譯。那麼顯然,把內聯函數的定義放進一個頭文件中是非常明智的做法。

三,頭文件中可以寫類 (class)的定義。因為在程式中創建一個類的對象時,編譯器只有在這個類的定義完全可見的情況下,才能知道這個類的對象應該如何佈局,所以,關於類的 定義的要求,跟內聯函數是基本一樣的。所以把類的定義放進頭文件,在使用到這個類的.cpp文件中去包含這個頭文件,是一個很好的做法。在這裡,值得一提 的是,類的定義中包含著數據成員和函數成員。數據成員是要等到具體的對象被創建時才會被定義(分配空間),但函數成員卻是需要在一開始就被定義的,這也就 是我們通常所說的類的實現。一般,我們的做法是,把類的定義放在頭文件中,而把函數成員的實現代碼放在一個.cpp文件中。這是可以的,也是很好的辦法。 不過,還有另一種辦法。那就是直接把函數成員的實現代碼也寫進類定義裡面。在C++的類中,如果函數成員在類的定義體中被定義,那麼編譯器會視這個函數為 內聯的。因此,把函數成員的定義寫進類定義體,一起放進頭文件中,是合法的。註意一下,如果把函數成員的定義寫在類定義的頭文件中,而沒有寫進類定義中, 這是不合法的,因為這個函數成員此時就不是內聯的了。一旦頭文件被兩個或兩個以上的.cpp文件包含,這個函數成員就被重定義了。

五、頭文件中的保護措施
考 慮一下,如果頭文件中只包含聲明語句的話,它被同一個.cpp文件包含再多次都沒問題——因為聲明語句的出現是不受限制的。然而,上面討論到的頭文件中的 三個例外也是頭文件很常用的一個用處。那麼,一旦一個頭文件中出現了上面三個例外中的任何一個,它再被一個.cpp包含多次的話,問題就大了。因為這三個 例外中的語法元素雖然“可以定義在多個源文件中”,但是“在一個源文件中只能出現一次”。設想一下,如果a.h中含有類A的定義,b.h中含有類B的定 義,由於類B的定義依賴了類A,所以b.h中也#include了a.h。現在有一個源文件,它同時用到了類A和類B,於是程式員在這個源文件中既把 a.h包含進來了,也把b.h包含進來了。這時,問題就來了:類A的定義在這個源文件中出現了兩次!於是整個程式就不能通過編譯了。你也許會認為這是程式 員的失誤——他應該知道b.h包含了a.h——但事實上他不應該知道。
使用"#define"配合條件編譯可以很好地解決這個問題。在一 個頭文件中,通過#define定義一個名字,並且通過條件編譯#ifndef...#endif使得編譯器可以根據這個名字是否被定義,再決定要不要繼 續編譯該頭文中後續的內容。這個方法雖然簡單,但是寫頭文件時一定記得寫進去。

[轉]C++編譯器與鏈接器工作原理

這裡並沒不是討論大學課程中所學的《編譯原理》,只是寫一些我自己對C++編譯器及鏈接器的工作原理的理解和看法吧,以我的水平,還達不到講解編譯原理(這個很複雜,大學時幾乎沒學明白)。

要明白的幾個概念:

    1、編譯:編譯器對源文件進行編譯,就是把源文件中的文本形式存在的源代碼翻譯成機器語言形式的目標文件的過程,在這個過程中,編譯器會進行一系列的語法檢查。如果編譯通過,就會把對應的CPP轉換成OBJ文件。

    2、編譯單元:根據C++標準,每一個CPP文件就是一個編譯單元。每個編譯單元之間是相互獨立並且互相不可知。

    3、目標文件:由編譯所生成的文件,以機器碼的形式包含了編譯單元里所有的代碼和數據,還有一些期他信息,如未解決符號表,導出符號表和地址重定向表等。目標文件是以二進位的形式存在的。

    根據C++標準,一個編譯單元(Translation Unit)是指一個.cpp文件以及這所include的所有.h文件,.h文件裡面的代碼將會被擴展到包含它的.cpp文件里,然後編譯器編譯該.cpp文件為一個.obj文件,後者擁有PE(Portable Executable,即Windows可執行文件)文件格式,並且本身包含的就是二進位代碼,但是不一定能執行,因為並不能保證其中一定有main函數。當編譯器將一個工程里的所有.cpp文件以分離的方式編譯完畢後,再由鏈接器進行鏈接成為一個.exe或.dll文件。

下麵讓我們來分析一下編譯器的工作過程:

我們跳過語法分析,直接來到目標文件的生成,假設我們有一個A.cpp文件,如下定義:

    int n = 1;

    void FunA()

    {

        ++n;

    }

    它編譯出來的目標文件A.obj就會有一個區域(或者說是段),包含以上的數據和函數,其中就有n、FunA,以文件偏移量形式給出可能就是下麵這種情況:

    偏移量    內容    長度

    0x0000    n       4

    0x0004    FunA    ??

    註意:這隻是說明,與實際目標文件的佈局可能不一樣,??表示長度未知,目標文件的各個數據可能不是連續的,也不一定是從0x0000開始。

    FunA函數的內容可能如下:

    0x0004 inc DWORD PTR[0x0000]

    0x00?? ret

    這時++n已經被翻譯成inc DWORD PTR[0x0000],也就是說把本單元0x0000位置的一個DWORD(4位元組)加1。

    有另外一個B.cpp文件,定義如下:

    extern int n;

    void FunB()

    {

        ++n;

    }

    它對應的B.obj的二進位應該是:

    偏移量    內容    長度

    0x0000    FunB    ??

    這裡為什麼沒有n的空間呢,因為n被聲明為extern,這個extern關鍵字就是告訴編譯器n已經在別的編譯單元里定義了,在這個單元里就不要定義了。由於編譯單元之間是互不相關的,所以編譯器就不知道n究竟在哪裡,所以在函數FunB就沒有辦法生成n的地址,那麼函數FunB中就是這樣的:

    0x0000 inc DWORD PTR[????]

    0x00?? ret

    那怎麼辦呢?這個工作就只能由鏈接器來完成了。

    為了能讓鏈接器知道哪些地方的地址沒有填好(也就是還????),那麼目標文件中就要有一個表來告訴鏈接器,這個表就是“未解決符號表”,也就是unresolved symbol table。同樣,提供n的目標文件也要提供一個“導出符號表”也就是exprot symbol table,來告訴鏈接器自己可以提供哪些地址。

    好,到這裡我們就已經知道,一個目標文件不僅要提供數據和二進位代碼外,還至少要提供兩個表:未解決符號表和導出符號表,來告訴鏈接器自己需要什麼和自己能提供些什麼。那麼這兩個表是怎麼建立對應關係的呢?這裡就有一個新的概念:符號。在C/C++中,每一個變數及函數都會有自己的符號,如變數n的符號就是n,函數的符號會更加複雜,假設FunA的符號就是_FunA(根據編譯器不同而不同)。

    所以,

    A.obj的導出符號表為

    符號    地址

    n       0x0000

    _FunA   0x0004

    未解決符號為空(因為他沒有引用別的編譯單元里的東西)。

    B.obj的導出符號表為

    符號    地址

    _FunB   0x0000

    未解決符號表為

    符號    地址

    n       0x0001

    這個表告訴鏈接器,在本編譯單元0x0001位置有一個地址,該地址不明,但符號是n。

    在鏈接的時候,鏈接在B.obj中發現了未解決符號,就會在所有的編譯單元中的導出符號表去查找與這個未解決符號相匹配的符號名,如果找到,就把這個符號的地址填到B.obj的未解決符號的地址處。如果沒有找到,就會報鏈接錯誤。在此例中,在A.obj中會找到符號n,就會把n的地址填到B.obj的0x0001處。

    但是,這裡還會有一個問題,如果是這樣的話,B.obj的函數FunB的內容就會變成inc DWORD PTR[0x000](因為n在A.obj中的地址是0x0000),由於每個編譯單元的地址都是從0x0000開始,那麼最終多個目標文件鏈接時就會導致地址重覆。所以鏈接器在鏈接時就會對每個目標文件的地址進行調整。在這個例子中,假如B.obj的0x0000被定位到可執行文件的0x00001000上,而A.obj的0x0000被定位到可執行文件的0x00002000上,那麼實現上對鏈接器來說,A.obj的導出符號地地址都會加上0x00002000,B.obj所有的符號地址也會加上0x00001000。這樣就可以保證地址不會重覆。

    既然n的地址會加上0x00002000,那麼FunA中的inc DWORD PTR[0x0000]就是錯誤的,所以目標文件還要提供一個表,叫地址重定向表,address redirect table。

    總結一下:

    目標文件至少要提供三個表:未解決符號表,導出符號表和地址重定向表。

    未解決符號表:列出了本單元里有引用但是不在本單元定義的符號及其出現的地址。

    導出符號表:提供了本編譯單元具有定義,並且可以提供給其他編譯單元使用的符號及其在本單元中的地址。

    地址重定向表:提供了本編譯單元所有對自身地址的引用記錄。

    鏈接器的工作順序:

    當鏈接器進行鏈接的時候,首先決定各個目標文件在最終可執行文件里的位置。然後訪問所有目標文件的地址重定義表,對其中記錄的地址進行重定向(加上一個偏移量,即該編譯單元在可執行文件上的起始地址)。然後遍歷所有目標文件的未解決符號表,並且在所有的導出符號表裡查找匹配的符號,併在未解決符號表中所記錄的位置上填寫實現地址。最後把所有的目標文件的內容寫在各自的位置上,再作一些另的工作,就生成一個可執行文件。

    說明:實現鏈接的時候會更加複雜,一般實現的目標文件都會把數據,代碼分成好向個區,重定向按區進行,但原理都是一樣的。

    明白了編譯器與鏈接器的工作原理後,對於一些鏈接錯誤就容易解決了。

    下麵再看一看C/C++中提供的一些特性:

    extern:這就是告訴編譯器,這個變數或函數在別的編譯單元里定義了,也就是要把這個符號放到未解決符號表裡面去(外部鏈接)。

    static:如果該關鍵字位於全局函數或者變數的聲明前面,表明該編譯單元不導出這個函數或變數,因些這個符號不能在別的編譯單元中使用(內部鏈接)。如果是static局部變數,則該變數的存儲方式和全局變數一樣,但是仍然不導出符號。 

    預設鏈接屬性:對於函數和變數,預設鏈接是外部鏈接,對於const變數,預設內部鏈接。

外部鏈接的利弊:外部鏈接的符號在整個程式範圍內都是可以使用的,這就要求其他編譯單元不能導出相同的符號(不然就會報duplicated external symbols)。

內部鏈接的利弊:內部鏈接的符號不能在別的編譯單元中使用。但不同的編譯單元可以擁有同樣的名稱的符號。

    為什麼頭文件里一般只可以有聲明不能有定義:頭文件可以被多個編譯單元包含,如果頭文件裡面有定義的話,那麼每個包含這頭文件的編譯單元都會對同一個符號進行定義,如果該符號為外部鏈接,則會導致duplicated external symbols鏈接錯誤。

    為什麼公共使用的內聯函數要定義於頭文件里:因為編譯時編譯單元之間互不知道,如果內聯被定義於.cpp文件中,編譯其他使用該函數的編譯單元的時候沒有辦法找到函數的定義,因些無法對函數進行展開。所以如果內聯函數定義於.cpp里,那麼就只有這個.cpp文件能使用它。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 主從伺服器 1. 將讀操作和寫操作分離到不同的資料庫上,避免主伺服器出現性能瓶頸; 2. 主伺服器進行寫操作時,不影響查詢應用伺服器的查詢性能,降低阻塞,提高併發; 3. 數據擁有多個容災副本,提高數據安全性,同時當主伺服器故障時,可立即切換到其他伺服器,提高系統可用性; 讀寫分離的基本原理就是讓主 ...
  • -(void)didReceiveMemoryWarning{ [super didReceiveMemoryWarning]; } 在這裡你需要釋放掉所有占用了很大記憶體的對象,如果你忽略了這個警告,應用有可能直接閃退。iOS設備沒有虛擬記憶體或者交換分區,記憶體用完了就是真的用完了,沒有任何迴旋的餘地 ...
  • 簡要:本系列文章講會對expo進行全面的介紹,本人從2017年6月份接觸expo以來,對expo的研究斷斷續續,一路走來將近10個月,廢話不多說,接下來你看到內容,講全部來與官網 我猜去全部機翻+個人修改補充+demo測試的形式,對expo進行一次大補血!歡迎加入expo興趣學習交流群:597732 ...
  • 簡要:本系列文章講會對expo進行全面的介紹,本人從2017年6月份接觸expo以來,對expo的研究斷斷續續,一路走來將近10個月,廢話不多說,接下來你看到內容,講全部來與官網 我猜去全部機翻+個人修改補充+demo測試的形式,對expo進行一次大補血!歡迎加入expo興趣學習交流群:597732 ...
  • Android #Android開發環境搭建 1.下載:Google在國服的官網 https://developer.android.google.cn/index.html 1.點擊首頁 “ 獲取 Android Studio” 2.跳轉以後等待網頁載入完(左上角顯示X則是未載入完)再點擊綠色的“ ...
  • ##init腳本的啟動 linux內核載入完成後,運行init.rc腳本 /system/bin/app_process Zygote服務啟動的進程名 --start-system-server 表明Zygote啟動完成之後,要啟動System進程。 socket zygote stream 666 ...
  • SnackBar是 Android Support Library 22.2.1 裡面新增提供的一個控制項,我們可以簡單的把它理解成一個加強版的Toast,或者是一個輕量級的Dialog。 特點: 看一下SnackBar的使用方法: 三個參數:View ,內容文本,顯示時長 和Toast的使用類似 T ...
  • 簡要:本系列文章講會對expo進行全面的介紹,本人從2017年6月份接觸expo以來,對expo的研究斷斷續續,一路走來將近10個月,廢話不多說,接下來你看到內容,講全部來與官網 我猜去全部機翻+個人修改補充+demo測試的形式,對expo進行一次大補血!歡迎加入expo興趣學習交流群:597732 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...