C++ 鍊氣期之結構體
1. 前言
隨着計算機
向著不同領域的延伸,數據
的概念已經不僅局限於數值型數據,計算機需要處理大量的非數值、且複雜的類型數據。
為了能抽象地描述這些非數值、複雜類型的數據,C++
引入了複合數據類型
的概念。
C++
數據類型分基本(原生)數據類型
和複合數據類型
,結構體
就是一種複合數據類型。可認為複合數據類型
是通過組合基本數據類型得到的一種新類型
,新類型
用來描述問題域中的特定數據
。
本文所用到的成員
一詞指的是組成複合數據類型中的某一個子類型。
2. 結構體
現有一個開發學生管理系統
的需求,系統需要一個學生信息
管理模塊,包含添加
、刪除
、更新……
學生信息功能。解決這個問題之前,則需要考慮如何存儲學生的個人信息以及一個學校的所有學生信息。
學生的個人信息包含學生的年齡
、性別
、成績……
如果僅存儲一個學生信息,這個問題很好解決,定義 3
個變量即可。
如果需要存儲全校學生信息,可以考慮使用數組
,因受限於數組只能存儲同類型數據的特點。為了完成這個需求,則需要 3
個數組,一個用來存儲年齡
、一個用來存儲性別
一個用來存儲成績
。顯然,在編碼時,需要隨時隨地同步 3
個數組,稍有不慎,便會出現錯誤。
此時,可能會有一個想法,能不能創建一個學生類型
,然後存儲在數組中,數組中不再存儲基本數據類型,而是一種新的學生類型
,如同二維數組一樣,一維數組中存儲一維數組,且不是一件很開心的事情 。
於是誕生出了一種設計理念:複合基本類型,設計出一種新的數據類型。
複合的方式有很多種,結構體
僅是其中之一。
2.1 結構體語法
//學生結構體:複合了 3 種基本數據類型
struct Student{
//學生年齡
int age;
//學生性別
char sex;
//學生成績
float score;
};
結構體
是一種數據類型,使用語法和基本類型一樣。
數據類型名 變量名;
一旦在語法上承認了這種數據類型
,和其它類型的區別就在於編譯器為之所分配的內存大小。
結構體
和數組
類似。創建數組
和結構體
時,都是開闢了一個連續區域, 這個連續區域是多個變量的集合。數組
這個連續區域只能保存類型相同的數據,結構體
這個連續區域則可以存儲不同類型的數據。
也就是說,在定義結構體之後,C++
運行時系統為之分配的是一個連續區域。那麼這個區域有多大?是不是由組成此結構體的子數據類型的大小之和?
下面來求證一下。
首先使用c++
的sizeof
函數計算一下結構體的大小:
int main(int argc, char** argv) {
//創建結構體類型變量
Student stu;
//計算結構體的大小
int size= sizeof(stu);
cout<<size<<endl;
return 0;
}
輸出結果:12
。也就是說在使用此結構體時,運行時系統分配的空間是12
。
Student
結構體由一個int
、一個char
、一個float
複合而成。理論上所需要的存儲空間大小應該是4+1+4=9
。
int
是 4 位元組
char
是1
位元組
float
是4
位元組
通過對比,可以推翻前面的結論:運行時系統為結構體
所分配的內存空間大小
並不一定是構建這個結構體的所有子數據類型的大小之和。
原因何在?
這是因為內存對齊
的緣故,內存對齊並不是本文的主題。這裡只粗略說一下,運行時為結構體分配內存時,並不是我們所想像地簡單地按順序依次分配,實際上是為了提高內存的訪問速度,以首地址為起始點,後續的內存空間地址儘可能是首地址的倍數。
如上圖所示,在為char
類型的變量分配空間時,為了保證訪問float
時的地址能被 4
整除,會為 char
類型變量填充 3
個空位元組,導致結構體最後被分配到的空間是 12
。
如下結構體類型:
struct Student {
double age;
char sex;
double score;
};
在內存中佔用 24
個位元組,原由和上述是一樣的。
對結構體有了一個大致了解後,再看一下使用結構體的 2
種語法結構:
- 靜態聲明。
- 動態聲明。
2
種語法結構的區別在於數據存儲的位置有差異性。當然,從字面意思而言,動態創建更有靈活性,事實也是如此。
2.2 靜態聲明
靜態聲明的特點:數據存儲在棧中,變量中保存的是結構體本身。
如下代碼:
#include <iostream>
using namespace std;
//學生結構體
struct Student {
//年齡
double age;
//性別
char sex;
//成績
double score;
};
int main(int argc, char** argv) {
//靜態聲明
Student stu;
return 0;
}
和使用其它的變量一樣,聲明後需要給結構體初始化數據,常用初始化方式有 3
種:
- 使用
{}
進行初始化。優點是,一次到位,簡潔明了。
Student stu={12,'M',99.5};
//可以省略 =
Student stu1 {12,'M',89};
//可以使用空 {} 為每一個分量設置一個默認值
Student stu2 {}
-
使用
.
運算符訪問結構體的各個分量,對結構體進行初始化和使用。數組是同類型變量的集合,數組會為每一個存儲單元指定一個唯一編號 。結構中的類型差異性很大,編號策略並不合適。但
.
運算符本質和編號是一樣,都是通過移動指針來尋找變量。
Student stu;
//初始化
stu.age=12;
stu.score=98.8;
stu.sex='M';
給結構體賦值後,方能使用結構體中保存的數據,可以使用.運算符
使用結構體中的每一個分量。
cout<<"年齡:"<<stu.age<<endl;
cout<<"成績:"<<stu.score<<endl;
cout<<"性別:"<<stu.sex<<endl;
- 使用另一個結構體中的數據。靜態聲明的結構體之間,採用的是
值複製
策略,即把一個結構體中的值賦值給另一個結構體。
//原結構體
Student stu;
stu.age=12;
stu.score=98.8;
stu.sex='M';
//通過靜態創建的結構體之間可以直接賦值
Student stu1=stu;
cout<<"年齡:"<<stu1.age<<endl;
cout<<"成績:"<<stu1.score<<endl;
cout<<"性別:"<<stu1.sex<<endl;
//輸出結果
//年齡:12
//成績:98.8
//性別:M
這裡做一個測試,如果更改第一個結構體中某個分量的值,是否會影響第二個結構體中同名分量的值。
Student stu1=stu;
//修改 stu 結構體中的年齡信息
stu.age=15;
//輸出 stu1 中的數據
cout<<"年齡:"<<stu1.age<<endl;
cout<<"成績:"<<stu1.score<<endl;
cout<<"性別:"<<stu1.sex<<endl;
輸出結果:
答案是不會,因為 2
個結構體有各自獨立的內存區域,一旦完成最初的賦值之後,2
者之間就沒有多大聯繫了。如下圖,修改 stu
的數據,不可能影響到 stu1
的數據。
2.3 動態聲明
動態創建的結構體的特點:數據存儲在堆中,結構體變量存儲的是結構體在內存中的地址。如下語句:
Student * stu_=new Student();
new
運算符會在堆中為結構體開闢一個內存區域,並且返回此內存區域的首地址,然後保存在 stu_
指針變量中。所以 stu_
變量存儲的是指針類型數據,可以隨時更改所指向的結構體實體。
- 初始化結構體:動態聲明的結構體可以使用
->
運算符(指針引用運算符)為結構體中的每一個分量賦值,也可以使用.
運算符訪問結構體中的分量。
//初始化結構體
stu_->age=13;
stu_->sex='W';
stu_->score=99.7;
//使用結構體中的數據
cout<<"年齡:"<<stu_->age<<endl;
cout<<"成績:"<<stu_->score<<endl;
cout<<"性別:"<<stu_->sex<<endl;
//也可以使用 . 運算符訪問動態結構體中的數據。
cout<<"年齡:"<<(* stu_).age<<endl;
cout<<"成績:"<<(* stu_).score<<endl;
cout<<"性別:"<<(* stu_).sex<<endl;
- 使用另一個靜態結構體中的數據。
因為動態聲明的結構體變量保存的是地址,需要使用 &
取地址運算符,才能把靜態結構體的地址賦值給動態聲明的結構體類型變量。
//靜態聲明結構體
Student stu;
stu.age=12;
stu.score=98.8;
stu.sex='M';
//把靜態結構體的地址賦值給結構體指針變量
Student * stu_=&stu;
cout<<"年齡:"<<stu_->age<<endl;
cout<<"性別:"<<stu_->sex<<endl;
cout<<"成績:"<<stu_->score<<endl;
如果修改靜態結構體中分量的值,動態引用會不會受影響?如下測試一下,便可知答案是會
。
Student stu;
stu.age=12;
stu.score=98.8;
stu.sex='M';
Student * stu_=&stu;
cout<<"年齡:"<<stu_->age<<endl;
cout<<"性別:"<<stu_->sex<<endl;
cout<<"成績:"<<stu_->score<<endl;
//修改靜態結構體中的年齡
stu.age=15;
cout<<"修改之後……"<<endl;
cout<<"年齡:"<<stu_->age<<endl;
cout<<"性別:"<<stu_->sex<<endl;
cout<<"成績:"<<stu_->score<<endl;
輸出結果:
為什麼?
其實 stu
是才是真正的結構體實體,存儲了結構體的所有分量數據。而stu_
是指針實體,存儲的是真正結構體所在的地址。也就是使用 stu
和stu_
訪問的是同一個結構體內存空間
。
結構體實體只有一個,結構體變量名和結構體指針只是
2
種不同的訪問入口。
- 使用另一個動態聲明的結構體中的數據。因為動態聲明結構體的變量都是指針類型,直接賦值即可。
Student * stu_=new Student();
stu_->age=12;
stu_->sex='M';
stu_->score=78.9;
cout<<"年齡:"<<stu_->age<<endl;
cout<<"性別:"<<stu_->sex<<endl;
cout<<"成績:"<<stu_->score<<endl;
//直接賦值
Student * stu_1=stu_;
cout<<"年齡:"<<stu_1->age<<endl;
cout<<"性別:"<<stu_1->sex<<endl;
cout<<"成績:"<<stu_1->score<<endl;
輸出結果:
此種方案和上面的引用靜態結構體的方案本質是一樣的,真正的結構體實體只有一個,但有 2
個結構體指針變量引用此結構體。無論使用那一個結構體指針變量修改結構體,都是可以的。
3. 結構體和函數
結構體
可以作為函數的參數類型,也可以作為函數的返回類型。
作為函數的參數:
#include <iostream>
using namespace std;
//結構體
struct Student {
double age;
char sex;
double score;
};
void updateStudent(Student stu){
stu.age=15;
stu.sex='W';
stu.score=100;
}
int main(int argc, char** argv) {
//結構體
Student stu;
//初始化
stu.age=12;
stu.sex='M';
stu.score=98.8;
//調用函數修改
updateStudent(stu);
//輸出
cout<<stu.age<<endl;
cout<<stu.sex<<endl;
cout<<stu.score<<endl;
return 0;
}
輸出結果:
如上代碼,試圖通過調用函數修改原結構體中的數據信息,結論是修改不了的。main
函數中調用updateStudent
函數時,是把主函數中結構體中的值複製給updateStudent
函數的結構體參數。默認情況下,以結構體作參數,採用的是值傳遞。
只有當形式參數的類型是指針或引用時,才可以影響主函數中的結構體中的數據。
//結構體指針作為參數
void updateStudent(Student *stu){
stu->age=15;
stu->sex='W';
stu->score=100;
}
//結構體引用
void updateStudent_(Student & stu){
stu.age=15;
stu.sex='W';
stu.score=100;
}
int main(int argc, char** argv) {
//結構體
Student stu;
//初始化
stu.age=12;
stu.sex='M';
stu.score=98.8;
//調用 updateStudent_(stu) 能達到相同效果
updateStudent(&stu);
//輸出
cout<<stu.age<<endl;
cout<<stu.sex<<endl;
cout<<stu.score<<endl;
return 0;
}
結構體作為函數的返回值。
- 返回靜態結構體,如下代碼,本質是把
createStudent
函數中創建的結構中的數據複製給主函數中名為stu
的結構體。函數調用完畢後,createStudent
函數中的結構體所使用的內存空間會被自動回收。
Student createStudent() {
//結構體
Student stu;
//初始化
stu.age=12;
stu.sex='M';
stu.score=98.8;
return stu;
}
int main(int argc, char** argv) {
Student stu=createStudent();
cout<<stu.age<<endl;
cout<<stu.score<<endl;
cout<<stu.sex<<endl;
return 0;
}
-
返回結構體指針。
注意,返回結構體指針時,不能是指向局部變量的指針。
Student stu;
Student * createStudent() {
//初始化
stu.age=12;
stu.sex='M';
stu.score=98.8;
return &stu;
}
int main(int argc, char** argv) {
Student *stu=createStudent();
cout<<stu->age<<endl;
cout<<stu->score<<endl;
cout<<stu->sex<<endl;
return 0;
}
- 返回結構體引用,不能返回局部變量的引用。因為局部變量在函數調用結束後就會被回收,返回的引用就沒有任何意義可言。
Student stu;
Student & createStudent() {
//初始化
stu.age=12;
stu.sex='M';
stu.score=98.8;
return stu;
}
int main(int argc, char** argv) {
Student stu=createStudent();
cout<<stu.age<<endl;
cout<<stu.score<<endl;
cout<<stu.sex<<endl;
return 0;
}
4. 再論結構體
一旦確定一種數據類型後,同時也確定了在此數據類型上所能做的操作。結構體類型是由開發者遵循語法規則自定義的一種新數據類型,對於這種數據類型的內置操作也只能由開發者自己決定。
結構體中除了可以指定複合了那幾種子數據類型,還可以提供相應的函數。
#include <iostream>
using namespace std;
//結構體
struct Student {
double age;
char sex;
double score;
//初始化函數,此函數沒有返回類型說明
Student(double age, char sex,double score) {
this->age=age;
this->sex=sex;
this->score=score;
}
//自我顯示函數
void showSelf() {
cout<<"年齡:"<<this->age<<endl;
cout<<"性別:"<<this->sex<<endl;
cout<<"成績:"<<this->score<<endl;
}
//其它函數……
};
int main(int argc, char** argv) {
//調用初始化函數
Student stu(12,'M',87.9);
stu.showSelf();
Student stu_(14,'W',80.1);
stu_.showSelf();
return 0;
}
上述代碼中出現了一個this
關鍵字,此關鍵字的含義是什麼?
this
是結構體函數的隱式變量,用來存儲已經分配了內存空間的結構體實體。因為無論創建多少個結構體,結構體中的函數代碼都只有一份,保存在代碼區。當某個結構體需要調用函數時,則需要把自己的地址傳遞給這個函數,以便讓此函數知道處理的數據源頭。
如上圖示,如果 this
中保存的是 stu
的地址,,函數會處理 stu
的數據。如果this
中保存的是 stu_
的地址,函數會處理 stu_
的數據。
5. 總結
結構體是C++
中最基礎的知識,只有熟練掌握後,才能構建出寵大的程序體系。