嵌入式C第一次任務學習記錄
一、位運算
位運算主要有六種,如下圖所示:
位運算子作用於位,並逐位執行操作。(個人見解:位運算和數值計算不同,位運算針對位進行運算,而一般的C語言中數值計算是直接進行賦值運算的)&、 | 和 ^ (三個主要的位運算)的真值表如下所示:
p | q | p & q | p | q | p ^ q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
個人而言覺得可以這麼理解:位運算與相當於乘法,位運算或相當於加法,位運算異或相當於一個比較(如果相同,則值為0,若不同,則值為1),位運算取反的話就相當於一種全部變換(0變成1,1變成0),位運算左移右移本質上是一樣的,相當於將原來的編碼進行整體向左平移或者向右平移。
如下是一個非常便於理解的例子:
假設如果 A = 60,且 B = 13,現在以二進位制格式表示,它們如下所示:
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A|B = 0011 1101
A^B = 0011 0001
~A = 1100 0011
下表顯示了 C 語言支援的位運算子。假設變數A的值為 60,變數B的值為 13,則:
運算子 | 描述 | 例項 |
---|---|---|
& | 按位與操作,按二進位制位進行"與"運算。運算規則: | (A & B) 將得到 12(十進位制數值),即為 0000 1100 |
| | 按位或運算子,按二進位制位進行"或"運算。運算規則: | (A | B) 將得到 61(十進位制數值),即為 0011 1101 |
^ | 異或運算子,按二進位制位進行"異或"運算。運算規則: | (A ^ B) 將得到 49(十進位制數值),即為 0011 0001 |
~ | 取反運算子,按二進位制位進行"取反"運算。運算規則: | (~A ) 將得到 -61(十進位制數值),即為 1100 0011,一個有符號二進位制數的補碼形式。 |
<< | 二進位制左移運算子。將一個運算物件的各二進位制位全部左移若干位(左邊的二進位制位丟棄,右邊補0)。 | A << 2 將得到 240(十進位制數值),即為 1111 0000 |
>> | 二進位制右移運算子。將一個數的各二進位制位全部右移若干位,正數左補0,負數左補1,右邊丟棄。 | A >> 2 將得到 15(十進位制數值),即為 0000 1111 |
例項:
#include <stdio.h>
int main()
{
unsigned int a = 60; /* 60 = 0011 1100 */
unsigned int b = 13; /* 13 = 0000 1101 */
int c = 0;
c = a & b; /* 12 = 0000 1100 */
printf("Line 1 - c 的值是 %d\n", c );
c = a | b; /* 61 = 0011 1101 */
printf("Line 2 - c 的值是 %d\n", c );
c = a ^ b; /* 49 = 0011 0001 */
printf("Line 3 - c 的值是 %d\n", c );
c = ~a; /*-61 = 1100 0011 */
printf("Line 4 - c 的值是 %d\n", c );
c = a << 2; /* 240 = 1111 0000 */
printf("Line 5 - c 的值是 %d\n", c );
c = a >> 2; /* 15 = 0000 1111 */
printf("Line 6 - c 的值是 %d\n", c );
}
輸出結果如下:
Line 1 - c 的值是 12
Line 2 - c 的值是 61
Line 3 - c 的值是 49
Line 4 - c 的值是 -61
Line 5 - c 的值是 240
Line 6 - c 的值是 15
參考網址:https://www.runoob.com/cprogramming/c-operators.html
二、static變數
個人感覺:static就是定義一個靜態變數,使之不能夠被全域性呼叫,只起一次作用。
定義:
我們知道在函式內部定義的變數,當程式執行到它的定義處時,編譯器為它在棧上分配空間,函式在棧上分配的空間在此函式執行結束時會釋放掉,這樣就產生了一個問題: 如果想將函式中此變數的值儲存至下一次呼叫時,如何實現? 最容易想到的方法是定義為全域性的變數,但定義一個全域性變數有許多缺點,最明顯的缺點是破壞了此變數的訪問範圍(使得在此函式中定義的變數,不僅僅只受此函式控制)。static 關鍵字則可以很好的解決這個問題。另外,在 C++ 中,需要一個數據物件為整個類而非某個物件服務,同時又力求不破壞類的封裝性,即要求此成員隱藏在類的內部,對外不可見時,可將其定義為靜態資料。
作用:
- (1)在修飾變數的時候,static 修飾的靜態區域性變數只執行初始化一次,而且延長了區域性變數的生命週期,直到程式執行結束以後才釋放。
- (2)static 修飾全域性變數的時候,這個全域性變數只能在本檔案中訪問,不能在其它檔案中訪問,即便是 extern 外部宣告也不可以。
- (3)static 修飾一個函式,則這個函式的只能在本檔案中呼叫,不能被其他檔案呼叫。static 修飾的變數存放在全域性資料區的靜態變數區,包括全域性靜態變數和區域性靜態變數,都在全域性資料區分配記憶體。初始化的時候自動初始化為 0。
- (4)不想被釋放的時候,可以使用static修飾。比如修飾函式中存放在棧空間的陣列。如果不想讓這個陣列在函式呼叫結束釋放可以使用 static 修飾。
- (5)考慮到資料安全性(當程式想要使用全域性變數的時候應該先考慮使用 static)。
比較:
靜態全域性變數有以下特點:
- (1)靜態變數都在全域性資料區分配記憶體,包括後面將要提到的靜態區域性變數;
- (2)未經初始化的靜態全域性變數會被程式自動初始化為0(在函式體內宣告的自動變數的值是隨機的,除非它被顯式初始化,而在函式體外被宣告的自動變數也會被初始化為 0);
- (3)靜態全域性變數在宣告它的整個檔案都是可見的,而在檔案之外是不可見的。
優點:靜態全域性變數不能被其它檔案所用;其它檔案中可以定義相同名字的變數,不會發生衝突。
全域性變數和全域性靜態變數的區別
- (1)全域性變數是不顯式用 static 修飾的全域性變數,全域性變數預設是有外部連結性的,作用域是整個工程,在一個檔案內定義的全域性變數,在另一個檔案中,通過 extern 全域性變數名的宣告,就可以使用全域性變數。
- (2)全域性靜態變數是顯式用 static 修飾的全域性變數,作用域是宣告此變數所在的檔案,其他的檔案即使用 extern 宣告也不能使用。
靜態區域性變數有以下特點:
- (1)該變數在全域性資料區分配記憶體;
- (2)靜態區域性變數在程式執行到該物件的宣告處時被首次初始化,即以後的函式呼叫不再進行初始化;
- (3)靜態區域性變數一般在宣告處初始化,如果沒有顯式初始化,會被程式自動初始化為 0;
- (4)它始終駐留在全域性資料區,直到程式執行結束。但其作用域為區域性作用域,當定義它的函式或語句塊結束時,其作用域隨之結束。
一般程式把新產生的動態資料存放在堆區,函式內部的自動變數存放在棧區。自動變數一般會隨著函式的退出而釋放空間,靜態資料(即使是函式內部的靜態區域性變數)也存放在全域性資料區。全域性資料區的資料並不會因為函式的退出而釋放空間。
例項
#include <stdio.h>
#include <stdlib.h>
int k1 = 1;
int k2;
static int k3 = 2;
static int k4;
int main()
{
static int m1 = 2, m2;
int i = 1;
char*p;
char str[10] = "hello";
char*q = "hello";
p = (char *)malloc(100);
free(p);
printf("棧區-變數地址 i:%p\n", &i);
printf("棧區-變數地址 p:%p\n", &p);
printf("棧區-變數地址 str:%p\n", str);
printf("棧區-變數地址 q:%p\n", &q);
printf("堆區地址-動態申請:%p\n", p);
printf("全域性外部有初值 k1:%p\n", &k1);
printf(" 外部無初值 k2:%p\n", &k2);
printf("靜態外部有初值 k3:%p\n", &k3);
printf(" 外靜無初值 k4:%p\n", &k4);
printf(" 內靜態有初值 m1:%p\n", &m1);
printf(" 內靜態無初值 m2:%p\n", &m2);
printf(" 文字常量地址:%p, %s\n", q, q);
printf(" 程式區地址:%p\n", &main);
return 0;
}
輸出結果如下:
補充:在類中,static 可以用來修飾靜態資料成員和靜態成員方法。
靜態資料成員
-
(1)靜態資料成員可以實現多個物件之間的資料共享,它是類的所有物件的共享成員,它在記憶體中只佔一份空間,如果改變它的值,則各物件中這個資料成員的值都被改變。
-
(2)靜態資料成員是在程式開始執行時被分配空間,到程式結束之後才釋放,只要類中指定了靜態資料成員,即使不定義物件,也會為靜態資料成員分配空間。
-
(3)靜態資料成員可以被初始化,但是隻能在類體外進行初始化,若未對靜態資料成員賦初值,則編譯器會自動為其初始化為 0。
-
(4)靜態資料成員既可以通過物件名引用,也可以通過類名引用。
靜態成員函式
-
(1)靜態成員函式和靜態資料成員一樣,他們都屬於類的靜態成員,而不是物件成員。
-
(2)非靜態成員函式有 this 指標,而靜態成員函式沒有 this 指標。
-
(3)靜態成員函式主要用來方位靜態資料成員而不能訪問非靜態成員。
總結:
-
(1)靜態成員函式中不能呼叫非靜態成員。
-
(2)非靜態成員函式中可以呼叫靜態成員。因為靜態成員屬於類本身,在類的物件產生之前就已經存在了,所以在非靜態成員函式中是可以呼叫靜態成員的。
-
(3)靜態成員變數使用前必須先初始化(如int MyClass::m_nNumber = 0;),否則會在 linker 時出錯。
參考網址:https://www.runoob.com/w3cnote/cpp-static-usage.html
三、extern變數
個人感覺:extern變數和static變數正好是相反的,static多用於定義靜態變數,使之不能被全域性呼叫,只能單次在一個檔案裡被呼叫;而extern則多用於定義全域性變數,是指可以被全域性呼叫甚至跨檔案呼叫,多次被呼叫
定義:
在定義變數的時候,這個extern居然可以被省略(定義時,預設均省略);在宣告變數的時候,這個extern必須新增在變數前,所以有時會讓你搞不清楚到底是宣告還是定義。或者說,變數前有extern不一定就是宣告,而變數前無extern就只能是定義。注:定義要為變數分配記憶體空間;而宣告不需要為變數分配記憶體空間。
(1)變數
例項:
extern int a; // 宣告一個全域性變數 a
int a; // 定義一個全域性變數 a
extern int a =0 ; // 定義一個全域性變數 a 並給初值。
int a =0; // 定義一個全域性變數 a, 並給初值,
第四個等於第三個,都是定義一個可以被外部使用的全域性變數,並給初值。
糊塗了吧,他們看上去可真像。但是定義只能出現在一處。也就是說,不管是int a;還是extern int a=0;還是int a=0;都只能出現一次,而那個extern int a可以出現很多次。
當你要引用一個全域性變數的時候,你就必須要宣告,extern int a; 這時候extern不能省略,因為省略了,就變成int a;這是一個定義,不是宣告。注:extern int a; 中型別 int 可省略,即 extern a; 但其他型別則不能省略。
(2)函式
對於函式也一樣,也是定義和宣告,定義的時候用extern,說明這個函式是可以被外部引用的,宣告的時候用extern說明這是一個宣告。 但由於函式的定義和宣告是有區別的,定義函式要有函式體,宣告函式沒有函式體(還有以分號結尾),所以函式定義和宣告時都可以將extern省略掉,反正其他檔案也是知道這個函式是在其他地方定義的,所以不加extern也行。兩者如此不同,所以省略了extern也不會有問題。
例項:
/*某cpp檔案*/
int fun(void)
{
return 0;
}
很好,我們定義了一個全域性函式:
/*另一cpp檔案*/
int fun(void);
我們對它做了個宣告,然後後面就可以用了, 加不加extern都一樣, 我們也可以把對 fun 的宣告放在一個頭檔案裡,最後變成這樣:
/*fun.h*/
int fun(void); //函式宣告,所以省略了extern,完整些是extern int fun(void);
/*對應的fun.cpp檔案*/
int fun(void)
{
return 0;
}//一個完整的全域性函式定義,因為有函式體,extern同樣被省略了。
然後,一個要使用你的fun的客戶,把這個標頭檔案包含進去,ok,一個全域性的宣告。沒有問題。但是,,如果是這個客戶要使用全域性變數,那麼要extern 某某變數,不然就成了定義了。
總結:
對變數而言,如果你想在本原始檔(例如檔名A)中使用另一個原始檔(例如檔名B)的變數,方法有2種:(1)在A檔案中必須用extern宣告在B檔案中定義的變數(當然是全域性變數);(2)在A檔案中新增B檔案對應的標頭檔案,當然這個標頭檔案包含B檔案中的變數宣告,也即在這個標頭檔案中必須用extern宣告該變數,否則,該變數又被定義一次。
對函式而言,如果你想在本原始檔(例如檔名A)中使用另一個原始檔(例如檔名B)的函式,方法有2種:(1)在A檔案中用extern宣告在B檔案中定義的函式(其實,也可省略extern,只需在A檔案中出現B檔案定義函式原型即可);(2)在A檔案中新增B檔案對應的標頭檔案,當然這個標頭檔案包含B檔案中的函式原型,在標頭檔案中函式可以不用加extern。
對上述總結換一種說法:
(a)對於一個檔案中呼叫另一個檔案的全域性變數,因為全域性變數一般定義在原檔案.c中,我們不能用#include包含原始檔而只能包含標頭檔案,所以常用的方法是用extern int a來宣告外部變數。 另外一種方法是可以是在a.c檔案中定義了全域性變數int global_num ,可以在對應的a.h標頭檔案中寫extern int global_num ,這樣其他原始檔可以通過include a.h來宣告她是外部變數就可以了。
(b)還有變數和函式的不同舉例int fun();和extern int fun();都是宣告(定義要有實現體)。 用extern int fun()只是更明確指明是宣告而已。而int a;是定義extern int a;是宣告。
(c)此外,extern修飾符可用於C++程式中呼叫c函式的規範問題。
比如在C++中呼叫C庫函式,就需要在C++程式中用extern "C"宣告要引用的函式。這是給連結器用的,告訴連結器在連結的時候用C函式規範來連結。主要原因是C++和C程式編譯完成後在目的碼中命名規則不同。
C++語言在編譯的時候為了解決的多型問題,會將名和引數聯合起來生成一箇中間的名稱,而c語言則不會,因此會造成連結時找不到對應的情況,此時C就需要用extern "C"進行連結指定,這告訴編譯器,請保持我的名稱,不要給我生成用於連結的中間名。
參考網址:https://www.runoob.com/w3cnote/extern-head-h-different.html
四、關鍵詞const
const 是 constant 的縮寫,本意是不變的,不易改變的意思。在 C++ 中是用來修飾內建型別變數,自定義物件,成員函式,返回值,函式引數。C++ const 允許指定一個語義約束,編譯器會強制實施這個約束,允許程式設計師告訴編譯器某值是保持不變的。如果在程式設計中確實有某個值保持不變,就應該明確使用const,這樣可以獲得編譯器的幫助。(個人感覺:const主要就是設定不變數,不變的量可以是值、指標、返回值)
(1)const修飾普通型別的變數
const int a = 7;
int b = a; // 正確
a = 8; // 錯誤,不能改變
a 被定義為一個常量,並且可以將 a 賦值給 b,但是不能給 a 再次賦值。對一個常量賦值是違法的事情,因為 a 被編譯器認為是一個常量,其值不允許修改。
接著看如下的操作:
例項一
#include<iostream>
using namespace std;
int main(void)
{
const int a = 7;
int *p = (int*)&a;
*p = 8;
cout<<a;
system("pause");
return 0;
}
對於 const 變數 a,我們取變數的地址並轉換賦值給 指向 int 的指標,然後利用 *p = 8; 重新對變數 a 地址內的值賦值,然後輸出檢視 a 的值。
從下面的除錯視窗看到 a 的值被改變為 8,但是輸出的結果仍然是 7。
從結果中我們可以看到,編譯器然後認為 a 的值為一開始定義的 7,所以對 const a 的操作就會產生上面的情況。所以千萬不要輕易對 const 變數設法賦值,這會產生意想不到的行為。
如果不想讓編譯器察覺到上面到對 const 的操作,我們可以在 const 前面加上 volatile 關鍵字。
Volatile 關鍵字跟 const 對應相反,是易變的,容易改變的意思。所以不會被編譯器優化,編譯器也就不會改變對 a 變數的操作。
(2)const 修飾指標變數。
const 修飾指標變數有以下三種情況。
-
A: const 修飾指標指向的內容,則內容為不可變數。
-
B: const 修飾指標,則指標為不可變數。
-
C: const 修飾指標和指標指向的內容,則指標和指標指向的內容都為不可變數。
對於 A:
const int *p = 8;
則指標指向的內容 8 不可改變。簡稱左定值,因為 const 位於 * 號的左邊。
對於 B:
int a = 8;
int* const p = &a;
*p = 9; // 正確
int b = 7;
p = &b; // 錯誤
對於 const 指標 p 其指向的記憶體地址不能夠被改變,但其內容可以改變。簡稱,右定向。因為 const 位於 * 號的右邊。
對於 C:則是 A 和 B的合併
int a = 8;
const int * const p = &a;
這時,const p 的指向的內容和指向的記憶體地址都已固定,不可改變。
對於 A,B,C 三種情況,根據 const 位於 * 號的位置不同,我總結三句話便於記憶的話:"左定值,右定向,const修飾不變數"。
(3)const引數傳遞和函式返回值。
對於 const 修飾函式引數可以分為三種情況。
A:值傳遞的 const 修飾傳遞,一般這種情況不需要 const 修飾,因為函式會自動產生臨時變數複製實參值。
例項
#include<iostream>
using namespace std;
void Cpf(const int a)
{
cout<<a;
// ++a; 是錯誤的,a 不能被改變
}
int main(void)
{
Cpf(8);
system("pause");
return 0;
}
B:當 const 引數為指標時,可以防止指標被意外篡改。
例項
#include<iostream>
using namespace std;
void Cpf(int *const a)
{
cout<<*a<<" ";
*a = 9;
}
int main(void)
{
int a = 8;
Cpf(&a);
cout<<a; // a 為 9
system("pause");
return 0;
}
C:自定義型別的引數傳遞,需要臨時物件複製引數,對於臨時物件的構造,需要呼叫建構函式,比較浪費時間,因此我們採取 const 外加引用傳遞的方法。並且對於一般的 int、double 等內建型別,我們不採用引用的傳遞方式。
例項
#include<iostream>
using namespace std;
class Test
{
public:
Test(){}
Test(int _m):_cm(_m){}
int get_cm()const
{
return _cm;
}
private:
int _cm;
};
void Cmf(const Test& _tt)
{
cout<<_tt.get_cm();
}
int main(void)
{
Test t(8);
Cmf(t);
system("pause");
return 0;
}
結果輸出8。
對於 const 修飾函式的返回值。
Const 修飾返回值分三種情況。
A:const 修飾內建型別的返回值,修飾與不修飾返回值作用一樣。
例項
#include<iostream>
using namespace std;
const int Cmf()
{
return 1;
}
int Cpf()
{
return 0;
}
int main(void)
{
int _m = Cmf();
int _n = Cpf();
cout<<_m<<" "<<_n;
system("pause");
return 0;
}
B: const 修飾自定義型別的作為返回值,此時返回的值不能作為左值使用,既不能被賦值,也不能被修改。
C: const 修飾返回的指標或者引用,是否返回一個指向 const 的指標,取決於我們想讓使用者幹什麼。
(4)const修飾類成員函式
const 修飾類成員函式,其目的是防止成員函式修改被呼叫物件的值,如果我們不想修改一個呼叫物件的值,所有的成員函式都應當宣告為 const 成員函式。
注意:const 關鍵字不能與 static 關鍵字同時使用,因為 static 關鍵字修飾靜態成員函式,靜態成員函式不含有 this 指標,即不能例項化,const 成員函式必須具體到某一例項。
下面的 get_cm()const; 函式用到了 const 成員函式:
例項
#include<iostream>
using namespace std;
class Test
{
public:
Test(){}
Test(int _m):_cm(_m){}
int get_cm()const
{
return _cm;
}
private:
int _cm;
};
void Cmf(const Test& _tt)
{
cout<<_tt.get_cm();
}
int main(void)
{
Test t(8);
Cmf(t);
system("pause");
return 0;
}
如果 get_cm() 去掉 const 修飾,則 Cmf 傳遞的 const _tt 即使沒有改變物件的值,編譯器也認為函式會改變物件的值,所以我們儘量按照要求將所有的不需要改變物件內容的函式都作為 const 成員函式。
如果有個成員函式想修改物件中的某一個成員怎麼辦?這時我們可以使用 mutable 關鍵字修飾這個成員,mutable 的意思也是易變的,容易改變的意思,被 mutable 關鍵字修飾的成員可以處於不斷變化中,如下面的例子。
例項
#include<iostream>
using namespace std;
class Test
{
public:
Test(int _m,int _t):_cm(_m),_ct(_t){}
void Kf()const
{
++_cm; // 錯誤
++_ct; // 正確
}
private:
int _cm;
mutable int _ct;
};
int main(void)
{
Test t(8,7);
return 0;
}
這裡我們在 Kf()const 中通過 ++_ct; 修改 _ct 的值,但是通過 ++_cm 修改 _cm 則會報錯。因為 ++_cm 沒有用 mutable 修飾。