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 位元組

char1 位元組

float4 位元組

通過對比,可以推翻前面的結論:運行時系統為結構體所分配的內存空間大小並不一定是構建這個結構體的所有子數據類型的大小之和。

原因何在?

這是因為內存對齊的緣故,內存對齊並不是本文的主題。這裡只粗略說一下,運行時為結構體分配內存時,並不是我們所想像地簡單地按順序依次分配,實際上是為了提高內存的訪問速度,以首地址為起始點,後續的內存空間地址儘可能是首地址的倍數。

1.png

如上圖所示,在為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;![3.png](//s2.51cto.com/images/20220822/1661143541316303.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)

輸出結果:

答案是不會,因為 2 個結構體有各自獨立的內存區域,一旦完成最初的賦值之後,2 者之間就沒有多大聯繫了。如下圖,修改 stu的數據,不可能影響到 stu1的數據。

2.png

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;

輸出結果:

5.png

為什麼?

其實 stu是才是真正的結構體實體,存儲了結構體的所有分量數據。而stu_是指針實體,存儲的是真正結構體所在的地址。也就是使用 stustu_訪問的是同一個結構體內存空間

結構體實體只有一個,結構體變量名和結構體指針只是 2 種不同的訪問入口。

6.png

  • 使用另一個動態聲明的結構體中的數據。因為動態聲明結構體的變量都是指針類型,直接賦值即可。
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;

輸出結果:

4.png

此種方案和上面的引用靜態結構體的方案本質是一樣的,真正的結構體實體只有一個,但有 2 個結構體指針變量引用此結構體。無論使用那一個結構體指針變量修改結構體,都是可以的。

7.png

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;
}

輸出結果:

8.png

如上代碼,試圖通過調用函數修改原結構體中的數據信息,結論是修改不了的。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;
}

9.png

結構體作為函數的返回值。

  • 返回靜態結構體,如下代碼,本質是把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是結構體函數的隱式變量,用來存儲已經分配了內存空間的結構體實體。因為無論創建多少個結構體,結構體中的函數代碼都只有一份,保存在代碼區。當某個結構體需要調用函數時,則需要把自己的地址傳遞給這個函數,以便讓此函數知道處理的數據源頭。

10.png

如上圖示,如果 this中保存的是 stu的地址,,函數會處理 stu的數據。如果this中保存的是 stu_的地址,函數會處理 stu_的數據。

5. 總結

結構體是C++中最基礎的知識,只有熟練掌握後,才能構建出寵大的程序體系。