九、結構體、共同體和枚舉
1、結構體的基本概念
結構體是用戶自定義的類型,可以將多種數據的表示合并到一起,描述一個完整的對象。
使用結構體有兩個步驟:1)定義結構體描述(類型);2)創建結構體變量。
1)定義結構體描述
定義結構體描述的語法:
struct 結構體名{成員一的數據類型 成員名一;成員二的數據類型 成員名二;成員三的數據類型 成員名三;......成員n的數據類型 成員名n;};
注意:
-
結構體名是標識符。
-
結構體的成員可以是任意數據類型。
-
定義結構體描述的代碼可以放在程序的任何地方,一般放在main函數的上面或頭文件中。
-
結構體成員可以用C++的類(如string),但是不提倡。
-
在C++中,結構體中可以有函數,但是不提倡。
-
在C++11中,定義結構體的時候可以指定缺省值。
2)創建結構體變量
創建結構體變量的語法:
struct 結構體名 結構體變量名;
也可以為結構體成員賦初始值。
struct 結構體名 結構體變量名={成員一的值, 成員二的值,......, 成員n的值};
C++11可以不寫等于號。
如果大括號內未包含任何東西或只寫一個0,全部的成員都將被設置為0。
struct 結構體名 結構體變量名={0};
注意:
在C++中,struct關鍵字可以不寫。
可以在定義結構體的時候創建結構體變量。
3)使用結構體
在C++程序中,用成員運算符(.)來訪問結構體的每個成員。結構體中的每個成員具備普通變量的全部特征。
語法:結構體變量名.結構體成員名;
4)占用內存的大小
用sizeof運算符可以得到整個結構體占用內存的大小。
注意:整個結構體占用內存的大小不一定等于全部成員占用內存之和。
內存對齊:#pragma pack(字節數)
合理使用內存對齊規則,某些節省內存的做法可能毫無意義。
5)清空結構體
創建的結構體變量如果沒有初始化,成員中有垃圾值。
用memset()函數可以把結構體中全部的成員清零。(只適用于C++基本數據類型)
bzero()函數也可以。
6)復制結構體
用memcpy()函數把結構體中全部的元素復制到另一個相同類型的結構體(只適用于C++基本數據類型)。
也可以直接用等于號(只適用于C++基本數據類型)。
示例:
#include <iostream> // 包含頭文件。
using namespace std; // 指定缺省的命名空間。
#pragma pack(8)// 超女基本信息結構體st_girl,存放了超女全部的數據項。
struct st_girl
{char name[21]; // 姓名。int age; // 年齡。double weight; // 體重(kg)。char sex; // 性別:X-女;Y-男。bool yz; // 顏值:true-漂亮;false-不漂亮。
};int main()
{st_girl stgirl{"西施",26,33.8,'X',true}; // 創建結構體變量。cout << "sizeof(st_girl)=" << sizeof(st_girl) << endl; memset(&stgirl, 0, sizeof(stgirl));cout << "姓名:" << stgirl.name << ",年齡:" << stgirl.age << ",體重:" << stgirl.weight<< ",性別:" << stgirl.sex << ",顏值:" << stgirl.yz << endl;
}
2、結構體指針
結構體是一種自定義的數據類型,用結構體可以創建結構體變量。
1)基本語法
在C++中,用不同類型的指針存放不同類型變量的地址,這一規則也適用于結構體。如下:
struct st_girl girl; // 聲明結構體變量girl。struct st_girl *pst=&girl; // 聲明結構體指針,指向結構體變量girls。
通過結構體指針訪問結構體成員,有兩種方法:
(*指針名).成員變量名 // (*pst).name和(*pst).age
或者:
指針名->成員變量名 // pst->name和*pst->age
在第一種方法中,圓點.的優先級高于,(指針名)兩邊的括號不能少。如果去掉括號寫成(指針名).成員變量名,那么相當于(指針名.成員變量名),意義就完全不一樣了。
在第二種方法中,->是一個新的運算符。
上面的兩種方法是等效的,程序員通常采用第二種方法,更直觀。
注意:與數組不一樣,結構體變量名沒有被解釋為地址。
2)用于函數的參數
如果要把結構體傳遞給函數,實參取結構體變量的地址,函數的形參用結構體指針。
如果不希望在函數中修改結構體變量的值,可以對形參加const約束。
3)用于動態分配內存
用結構體指針指向動態分配的內存的地址。
#define _CRT_SECURE_NO_WARNINGS // 如果要使用C標準庫的字符串函數,需要加上這一行代碼
#include <iostream> // 包含頭文件。
using namespace std; // 指定缺省的命名空間。struct st_girl
{char name[21]; // 姓名。int age; // 年齡。double weight; // 體重(kg)。char sex; // 性別:X-女;Y-男。bool yz; // 顏值:true-漂亮;false-不漂亮。
};void func(const st_girl* pst)
{cout << "姓名:" << pst->name << ",年齡:" << pst->age << ",體重:" << pst->weight<< ",性別:" << pst->sex << ",顏值:" << pst->yz << endl;
}int main()
{// st_girl stgirl={"西施",26,33.8,'X',true}; // 創建結構體變量。st_girl* stgirl = new st_girl({ "西施",26,33.8,'X',true });// memset(stgirl, 0, sizeof(st_girl));cout << "姓名:" << stgirl->name << ",年齡:" << stgirl->age << ",體重:" << stgirl->weight<< ",性別:" << stgirl->sex << ",顏值:" << stgirl->yz << endl;func(stgirl);cout << "姓名:" << stgirl->name << ",年齡:" << stgirl->age << ",體重:" << stgirl->weight<< ",性別:" << stgirl->sex << ",顏值:" << stgirl->yz << endl;delete stgirl;
}
3、結構體數組
結構體可以被定義成數組變量,本質上與其它類型的數組變量沒有區別。
聲明結構體數組的語法:struct 結構體類型 數組名[數組長度];
初始化結構體數組,要結合使用初始化數組的規則和初始化結構體的規則。
struct st_girl girls[2]={{"西施",26,43.8,'X',true},{"西瓜",25,52.8,'X',false}};
使用結構體數組可以用數組表示法,也可以用指針表示法。
4、結構體中的指針
如果結構體中的指針指向的是動態分配的內存地址:
-
對結構體用sizeof運算可能沒有意義。
-
對結構體用memset()函數可能會造成內存泄露。
-
C++的字符串string中有一個指針,指向了動態分配內存的地址。
struct string{? char *ptr; // 指向動態分配內存的地址。? ......}
5、共同體
共同體(共用體、聯合體)是一種數據格式,它能存儲不同的數據類型,但是,在同一時間只能存儲其中的一種類型。
聲明共同體的語法:
union 共同體名{成員一的數據類型 成員名一;成員二的數據類型 成員名二;成員三的數據類型 成員名三;......成員n的數據類型 成員名n;};
注意:
-
共同體占用內存的大小是它最大的成員占用內存的大小(內存對齊)。
-
全部的成員使用同一塊內存。
-
共同體中的值為最后被賦值的那個成員的值。
-
匿名共同體沒有名字,可以在定義的時候創建匿名共同體變量(VS和Linux有差別),也可以嵌入結構體中。
應用場景:
-
當數據項使用兩種或更多種格式(但不會同時使用)時,可節省空間(嵌入式系統)。
-
用于回調函數的參數(相當于支持多種數據類型)。
示例一:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream> // 包含頭文件。
using namespace std; // 指定缺省的命名空間。union udata // 聲明共同體udata。
{int a;double b;char c[25];
} ;int main()
{udata data; // 定義共同體變量。cout << "sizeof(data)=" << sizeof(data) << endl; //輸出32,存在內存對齊,為8的整數倍cout << "data.a的地址是:" << (void*)&data.a << endl;cout << "data.b的地址是:" << (void*)&data.b << endl;cout << "data.c的地址是:" << (void*)&data.c << endl;data.a = 3;data.b = 8.8;strcpy(data.c, "我是一只傻傻鳥。");cout << "data.a=" << data.a << endl;cout << "data.b=" << data.b << endl;cout << "data.c=" << data.c << endl;
}
sizeof(data)=32
data.a的地址是:00000054A0F8F7E8
data.b的地址是:00000054A0F8F7E8
data.c的地址是:00000054A0F8F7E8
data.a=-943009074
data.b=-1.92562e-20
data.c=我是一只傻傻鳥。
6、枚舉
枚舉是一種創建符號常量的方法。
枚舉的語法:
enum 枚舉名 { 枚舉量1 , 枚舉量2 , 枚舉量3, ......, 枚舉量n };
例如:
enum colors { red , yellow , blue };
這條語句完成了兩項工作:
-
讓colors成了一種新的枚舉類型的名稱,可以用它創建枚舉變量。
-
將red、yellow、blue作為符號常量,默認值是整數的0、1、2。
注意:
用枚舉創建的變量取值只能在枚舉量范圍之內。
枚舉的作用域與變量的作用域相同。
可以顯式的設置枚舉量的值(必須是整數)。
enum colors {red=1,yellow=2,blue=3};
可以只顯式的指定某些枚舉量的值(枚舉量的值可以重復)。
enum colors {red=10,yellow,blue}; //10,11,12
可以將整數強制轉換成枚舉量,語法:枚舉類型(整數)
#include <iostream> // 包含頭文件。
using namespace std; // 指定缺省的命名空間。int main()
{enum colors { red=0, yellow=1, blue=2, other=3 }; // 創建枚舉類型colors。colors cc = yellow; // 創建枚舉變量,并賦初始值。//colors cc = colors(1); // 創建枚舉變量,并賦初始值。cout << "red=" << red << ",yellow=" << yellow << ",blue=" << blue << ",other=" << other << endl;switch (cc){case red: cout << "紅色。\n"; break;case yellow: cout << "黃色。\n"; break;case blue: cout << "藍色。\n"; break;default: cout << "未知。\n"; }
}
十、引用
1、引用的基本概念
引用變量是C++新增的復合類型。
引用是已定義的變量的別名。
引用的主要用途是用作函數的形參和返回值。
聲明/創建引用的語法:數據類型 &引用名=原變量名;
注意:
-
引用的數據類型要與原變量名的數據類型相同。
-
引用名和原變量名可以互換,它們值和內存單元是相同的。
-
必須在聲明引用的時候初始化,初始化后不可改變。
-
C和C++用&符號來指示/取變量的地址,C++給&符號賦予了另一種含義。
示例:
#include <iostream> // 包含頭文件。
using namespace std; // 指定缺省的命名空間。int main()
{// 聲明 / 創建引用的語法:數據類型 & 引用名 = 原變量名;int a = 3; // 聲明普通的整型變量。int& ra = a; // 創建引用ra,ra是a的別名。cout << " a的地址是:" << &a << ", a的值是:" << a << endl;cout << "ra的地址是:" << &ra << ",ra的值是:" << ra << endl;ra = 5; cout << " a的地址是:" << &a << ", a的值是:" << a << endl;cout << "ra的地址是:" << &ra << ",ra的值是:" << ra << endl;
}
a的地址是:000000A3C975F794, a的值是:3
ra的地址是:000000A3C975F794,ra的值是:3
a的地址是:000000A3C975F794, a的值是:5
ra的地址是:000000A3C975F794,ra的值是:5
2、引用的本質
引用是指針常量的偽裝。
引用是編譯器提供的一個有用且安全的工具,去除了指針的一些缺點,禁止了部分不安全的操作。
變量是什么?變量就是一個在程序執行過程中可以改變的量。
換一個角度,變量是一塊內存區域的名字,它代表了這塊內存區域,當我們對變量進行修改的時候,會引起內存區域中內容的改變。
在計算機看來,內存區域根本就不存在什么名字,它僅有的標志就是它的地址,因此我們若想修改一塊內存區域的內容,只有知道他的地址才能實現。
所謂的變量只不過是編譯器給我們進行的一種抽象,讓我們不必去了解更多的細節,降低我們的思維跨度而已。
程序員擁有引用,但編譯器僅擁有指針(地址)。
引用的底層機制實際上是和指針一樣的。不要相信有別名,不要認為引用可以節省一個指針的空間,因為這一切不會發生,編譯器還是會把引用解釋為指針。
引用和指針本質上沒有區別。
#include <iostream> // 包含頭文件。
using namespace std; // 指定缺省的命名空間。int main()
{// 聲明 / 創建引用的語法:數據類型 & 引用名 = 原變量名;// 語法:數據類型 * const 變量名;int a = 3; // 聲明普通的整型變量。int& ra = a; // 創建引用ra,ra是a的別名。 把int&替換成int* const 把a替換成&aint* const rb = &a; // 聲明指針常量rb,讓它指向變量a。cout << " a的地址是:" << &a << ", a的值是:" << a << endl;cout << "ra的地址是:" << &ra << ", ra的值是:" << ra << endl; // 把&ra替換成ra,把ra替換成*racout << "rb的值是 :" << rb << ",*rb的值是:" << *rb << endl;ra = 5; cout << " a的地址是:" << &a << ", a的值是:" << a << endl;cout << "ra的地址是:" << &ra << ", ra的值是:" << ra << endl;cout << "rb的值是 :" << rb << ",*rb的值是:" << *rb << endl;
}
a的地址是:000000E3807AFB84, ?a的值是:3
ra的地址是:000000E3807AFB84, ra的值是:3
rb的值是 ?:000000E3807AFB84,*rb的值是:3
a的地址是:000000E3807AFB84, ?a的值是:5
ra的地址是:000000E3807AFB84, ra的值是:5
rb的值是 ?:000000E3807AFB84,*rb的值是:5
3、引用用于函數的參數
把函數的形參聲明為引用,調用函數的時候,形參將成為實參的別名。
這種方法也叫按引用傳遞或傳引用。(傳值、傳地址、傳引用只是說法不同,其實都是傳值。)
引用的本質是指針,傳遞的是變量的地址,在函數中,修改形參會影響實參。
1)傳引用的代碼更簡潔。
#include <iostream> // 包含頭文件。
using namespace std; // 指定缺省的命名空間。void func1(int no, string str) // 傳值。
{no = 8; str = "我有一只小小鳥。";cout << "親愛的" << no << "號:" << str << endl;
}void func2(int* no, string* str) // 傳地址。
{*no = 8;*str = "我有一只小小鳥。";cout << "親愛的" << *no << "號:" << *str << endl;
}void func3(int &no, string &str) // 傳引用。
{no = 8;str = "我有一只小小鳥。";cout << "親愛的" << no << "號:" << str << endl;
}int main()
{int bh = 3; // 超女的編號。string message = "我是一只傻傻鳥。"; // 向超女表白的內容。//func1(bh, message); // 傳值。//func2(&bh, &message); // 傳地址。func3(bh, message); // 傳引用。cout << "親愛的" << bh << "號:" << message << endl;
}
2)傳引用不必使用二級指針。
#include <iostream> // 包含頭文件。
using namespace std; // 指定缺省的命名空間。void func1(int** p) // 傳地址,實參是指針的地址,形參是二級指針。
{*p = new int(3); // p是二級指針,存放指針的地址。cout << "func1內存的地址是:" << *p << ",內存中的值是:" << **p << endl;
}void func2(int*& p) // 傳引用,實參是指針,形參是指針的別名。
{p = new int(3); // p是指針的別名。cout << "func2內存的地址是:" << p << ",內存中的值是:" << *p << endl;
}int main()
{int* p = nullptr; // 存放在子函數中動態分配內存的地址。func1(&p); // 傳地址,實參填指針p的地址。//func2(p); // 傳引用,實參填指針p。cout << "main 內存的地址是:" << p << ",內存中的值是:" << *p << endl;delete p;
}
3)引用的屬性和特別之處。
4、引用的形參和const
如果引用的數據對象類型不匹配,當引用為const時,C++將創建臨時變量,讓引用指向臨時變量。
什么時候將創建臨時變量呢?
-
引用是const。
-
數據對象的類型是正確的,但不是左值。
-
數據對象的類型不正確,但可以轉換為正確的類型。
結論:如果函數的實參不是左值或與const引用形參的類型不匹配,那么C++將創建正確類型的匿名變量,將實參的值傳遞給匿名變量,并讓形參來引用該變量。
將引用形參聲明為const的理由有三個:
-
使用const可以避免無意中修改數據的編程錯誤。
-
使用const使函數能夠處理const和非const實參,否則將只能接受非const實參。
-
使用const,函數能正確生成并使用臨時變量。
左值是可以被引用的數據對象,可以通過地址訪問它們,例如:變量、數組元素、結構體成員、引用和解引用的指針。
非左值包括字面常量(用雙引號包含的字符串除外)和包含多項的表達式。
5、引用用于函數的返回值
傳統的函數返回機制與值傳遞類似。
函數的返回值被拷貝到一個臨時位置(寄存器或棧),然后調用者程序再使用這個值。
double m=sqrt(36); // sqrt()是求平方根函數。
sqrt(36)的返回值6被拷貝到臨時的位置,然后賦值給m。
cout << sqrt(25);
sqrt(25)的返回值5被拷貝到臨時的位置,然后傳遞給cout。
如果返回的是一個結構體,將把整個結構體拷貝到臨時的位置。
如果返回引用不會拷貝內存。
語法:
返回值的數據類型& 函數名(形參列表);
注意:
-
如果返回局部變量的引用,其本質是野指針,后果不可預知。
-
可以返回函數的引用形參、類的成員、全局變量、靜態變量。
-
返回引用的函數是被引用的變量的別名,將const用于引用的返回類型。
#include <iostream> // 包含頭文件。
using namespace std; // 指定缺省的命名空間。const int &func2(int &ra) // 返回的是引用。
{ra++;cout << "ra的地址是:" << &ra << ",ra=" << ra << endl;return ra;
}int main()
{int a = 3;const int& b = func2(a); // 返回的是引用。cout << " a的地址是:" << &a << ", a=" << a << endl;cout << " b的地址是:" << &b << ", b=" << b << endl;// func2(a) = 10; // 返回引有的函數是被引用的變量的別名。// cout << " a的地址是:" << &a << ", a=" << a << endl;// cout << " b的地址是:" << &b << ", b=" << b << endl;
}
6、各種形參的使用場景
傳值、傳地址和傳引用的指導原則《C++ Primer Plus》
1)如果不需要在函數中修改實參
-
如果實參很小,如C++內置的數據類型或小型結構體,則按值傳遞。
-
如果實參是數組,則使用const指針,因為這是唯一的選擇(沒有為數組建立引用的說法)。
-
如果實參是較大的結構,則使用const指針或const引用。
-
如果實參是類,則使用const引用,傳遞類的標準方式是按引用傳遞(類設計的語義經常要求使用引用)。
2)如果需要在函數中修改實參
-
如果實參是內置數據類型,則使用指針。只要看到func(&x)的調用,表示函數將修改x。
-
如果實參是數組,則只能使用指針。
-
如果實參是結構體,則使用指針或引用。
-
如果實參是類,則使用引用。
當然,這只是一些指導原則,很可能有充分的理由做出其他的選擇。
例如:對于基本類型,cin使用引用,因此可以使用cin>>a,而不是cin>>&a。
十一、函數重載
1、函數的默認參數
默認參數是指調用函數的時候,如果不書寫實參,那么將使用的一個缺省值。
語法:返回值 函數名(數據類型 參數=值, 數據類型 參數=值,……);
注意:
-
如果函數的聲明和定義是分開書寫的,在函數聲明中書寫默認參數,函數的定義中不能書寫默認參數。
-
函數必須從右到左設置默認參數。也就是說,如果要為某個參數設置默認值,則必須為它后面所有的參數設置默認值。
-
調用函數的時候,如果指定了某個參數的值,那么該參數前面所有的參數都必須指定。
示例:
#include <iostream> // 包含頭文件。
using namespace std; // 指定缺省的命名空間。void func(int bh,const string &name="西施", const string& message="我喜歡你。") // 向超女表白的函數。
{cout << "親愛的"<<name<<"("<<bh<<"):" << message << endl;
}int main()
{func(3,"冰冰","我是一只傻傻鳥。"); func(5);
}
2、函數重載
函數重載(函數多態)是指設計一系列同名函數,讓它們完成相同(似)的工作。
C++允許定義名稱相同的函數,條件是它們的特征(形參的個數、數據類型和排列順序)不同。
#1?? ?int func(short a ?,string b);
#2?? ?int func(int a ? ,string b);
#3?? ?int func(double a,string b);
#4?? ?int func(int a ? ,string b, int len);
#5?? ?int func(string b , int a);
調用重載函數的時候,在代碼中我們用相同的函數名,但是,后面的實參不一樣,編譯器根據實參與重載函數的形參進行匹配,然后決定調用具體的函數,如果匹配失敗,編譯器將視為錯誤。
在實際開發中,視需求重載各種數據類型,不要重載功能不同的函數。
注意:
-
使用重載函數時,如果數據類型不匹配,C++嘗試使用類型轉換與形參進行匹配,如果轉換后有多個函數能匹配上,編譯將報錯。
-
引用可以作為函數重載的條件,但是,調用重載函數的時候,如果實參是變量,編譯器將形參類型的本身和類型引用視為同一特征。
-
如果重載函數有默認參數,調用函數時,可能導致匹配失敗。
-
const不能作為函數重載的特征。
-
返回值的數據類型不同不能作為函數重載的特征。
C++的名稱修飾:編譯時,對每個函數名進行加密,替換成不同名的函數。
void MyFunctionFoo(int,float);
void MyFunctionFoo(long,float);
?MyFunctionFoo@@YAXH(int,float);
#void MyFunctionFoo^$@(long,float)
3、內聯函數
C++將內聯函數的代碼組合到程序中,可以提高程序運行的速度。
語法:在函數聲明和定義前加上關鍵字inline。
通常的做法是將函數聲明和定義寫在一起。
注意:
-
內聯函數節省時間,但消耗內存。
-
如果函數過大,編譯器可能不將其作為內聯函數。
-
內聯函數不能遞歸。
示例:
#include <iostream> // 包含頭文件。
using namespace std;inline void show(const short bh, const string message) // 表白函數。
{cout << "親愛的" << bh << "號:" << message << endl;
}int main()
{//show(3, "我是一只傻傻鳥。");{int bh = 3;string message = "我是一只傻傻鳥。";cout << "親愛的" << bh << "號:" << message << endl;}// show(8, "我有一只小小鳥。");{int bh = 8;string message = "我有一只小小鳥。";cout << "親愛的" << bh << "號:" << message << endl;}// show(5, "我是一只小小鳥。");{int bh = 5;string message = "我是一只小小鳥。";cout << "親愛的" << bh << "號:" << message << endl;}
}