C語言學習筆記:結構體與指標

量化就是探索生活發表於2020-11-12

結構體

結構體定義

與C++不同的是,C語言中定義結構體必須加上關鍵字struct,而C++中結構體是一個類的關鍵字,故可以不加。

struct persin{
	char name[20];
	int age;
	char gender;
	float height;
}

訪問方式

直接引用

通過.實現

struct person yu;
yu.age = 20;

間接引用

通過->實現

struct person *ptr = &yu;
ptr->age = 20;

結構體的記憶體分佈

不一定是結構體內元素的簡單相加,
上圖記憶體分佈
我們認為,結構體中的記憶體分佈應該如上圖左圖所示,但是實際上是右圖,由於結構體中存在記憶體對齊原則

記憶體對齊原則

1.按照結構體中所佔位元組最大的資料元素進行對齊,每次申請記憶體只能是這個最大記憶體的整數倍

2.而且結構體每個成員相對結構體首地址的記憶體偏移量都是該成員大小的整數倍。
3.結構體首地址,也是最大記憶體的整數倍。

上圖例子中,int和float都佔4個位元組,故每次至少申請4個位元組記憶體大小。

舉例說明:

struct node1{
	char a;
	char b;
	int c;
}

struct node2{
	char a;
	int c;
	char b;
}

上圖中,node1佔用八個位元組,第一次申請四個位元組的區域,兩個char型別變數都可以存入這四個位元組,餘下兩個位元組不夠存入int型別變數故再開闢四個位元組記憶體,總共開闢 2 ∗ 4 = 8 2*4=8 24=8個位元組。

node2佔用十二個位元組,第一次申請四個位元組的區域儲存一個char型別變數,餘下三個位元組不夠儲存int故再開闢一個4位元組記憶體,且int需要儲存在偏移量為4整數倍的位置,故佔用新開闢的這四個位元組,而最後一個char變數需要新開一片四位元組記憶體儲存,故總共開闢 3 ∗ 4 = 12 3*4=12 34=12個位元組。

對齊值

編譯器是按照什麼方式進行對齊?

1.資料型別自身的對齊值,即自身所佔記憶體大小
2.結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。
3.指定對齊值:#pragma pack (value)時的指定對齊值value。
4.資料成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中小的那個值。

當一個成員的對齊值確定為 N N N後,該成員所存放地址必須滿足 起 始 地 址 % N = 0 起始地址 \% N=0 %N=0

#pragma pack (1) /*指定按1位元組對齊*/
struct D
{
    char b;
    int a;
    short c;
};
#pragma pack () /*取消指定對齊,恢復預設對齊*/
//大小為 7個位元組, 空地址

共用體(聯合體)

各個欄位共用一片空間,空間大小為欄位中最大的那一個。

舉例 :P轉整數

#include<stdio.h>
            
union IP{ 
    struct{
        unsigned char a1; 
        unsigned char a2; 
        unsigned char a3; 
        unsigned char a4; 
    } ip;                                                                                                                                                                                  
    unsigned int num;
};          
            
int main(){
    union IP p;
    char str[100] = {0};
    int arr[4];
    while(~scanf("%s", str)){
        sscanf(str, "%d.%d.%d.%d", arr, arr + 1, arr + 2, arr + 3); 
        p.ip.a1 = arr[0];
        p.ip.a2 = arr[1];
        p.ip.a3 = arr[2];
        p.ip.a4 = arr[3];
        printf("%u\n", p.num);
    }               
    return 0;
}           

輸出結果與預期不符,改動最後一位整形數字變太大。

//輸入192.168.0.1
16820416
//輸入192.168.0.2
33597632

小端模式和大端模式

大端模式和小端模式是cpu兩種不同的儲存模式

小端:數字低位存在地址低位
大端:數字低位存在地址高位

上圖結果是由於在小端機中,IP第一位存在與地址低位。而轉int時,IP第一位192被作為數字低位,而最後一位2被作為數字高位。

在主函式中進行修改

int main(){
    union IP p;
    char str[100] = {0};
    int arr[4];
    while(~scanf("%s", str)){
        sscanf(str, "%d.%d.%d.%d", arr, arr + 1, arr + 2, arr + 3); 
        p.ip.a1 = arr[3];
        p.ip.a2 = arr[2];
        p.ip.a3 = arr[1];
        p.ip.a4 = arr[0];
        printf("%u\n", p.num);
    }               
    return 0;
}           

輸出結果正常

//輸入192.168.0.1
3232235521
//輸入192.168.0.2
3232235512

本機位元組序和網路位元組序

判斷大端機和小端機

int is_little(){
	static int num = 1;
	return ((char*)(&num))[0];
}
//返回1 則為小端機, 否則大端機

地址與指標

地址可以運算元據的本源:

地址

int 型別在記憶體中的儲存方式:
在這裡插入圖片描述
不同位數的作業系統的區別,就是記憶體定址能力不同。32位作業系統只能識別4GB記憶體,因為只有32個bit位。64位系統使用64bit,是一個不可能達到的上限。

機器給每一個位元組一個地址標號,變數的地址都是首地址,比如上圖變數a,首地址為0x187c20

指標變數

指標變數是也是變數,其儲存的值是地址,其自身也有地址。指標型別決定指向變數的型別。

同一個系統中,指標變數的大小都相同,指標型別決定定址偏移量。

*運算子的結合

*指標運算子的結合方式,跟變數結合

int a;
int (*p) = &a;
//通過編譯

int a;
(int*) p = &a;
//不通過編譯

多級指標,編譯時變數結合

int a;
int (*p) = &a;
int (**pp) = &p;
//通過編譯

但邏輯上理解為,先定義一個指標變數p,指向int型別的變數a的地址,在定義一個指標變數pp,指向一個int*型別的指標變數p

&取地址運算子

在C語言中,&a表示取出變數a的首地址,只能作用於存在於記憶體中的變數或者結構體。
只有位於記憶體中的變數可以取地址,位於暫存器中的變數不可以取地址。

int a = 1;
int* pa = &a;//編譯通過
int* pa2 = &(a+1); //編譯不通過, a+1在暫存器中

*解引用運算子

*還可以作為對指標變數解引用,用於通過指標間接訪問一個變數。

舉例

struct Data{
	int x,y;
}
struct Data a[2];
struct Data * p = a;
//多種方式表示 a[1].x
a[1].x 
(*(a + 1)).x 
(a + 1)->x 
(p + 1)->x 
(*(p + 1)).x 
p[1].x 
(&p[1])->x 
(&p[0] + 1)->x 
(*(&p[0] + 1)).x 

地址偏移量

struct Data{
	int a;
	double b;
	char c;
}

上圖例子中結構體struct Data的對齊值為8個位元組,int型別變數a與偏移量0位置,double型別變數b與偏移量8位置,char型別變數c與偏移量16位置。

//計算偏移量巨集定義offset
#define offset(T, a) ({\
	T* temp;\
	(char*)&(temp->a)-(chat*)temp;\
})
//更便捷的寫法
#define offset(T, a) (long)(&(((T*)NULL)->a))

指標定義陷阱

不要用巨集定義重新命名指標型別

#define pchar char*
typedef char* ppchar;
int main(){
pchar c1,c2;
//c2為一個char而不是char*
//展開程式碼為 char* c1,c2;
ppchar c2,c4;
//c3,c4均為char*
}

函式指標

定義函式指標,*必須跟指標變數走,否則為函式宣告。

int (*add)(int, int);//定義函式指標
int *add(int, int);//宣告返回指標變數的函式

使用typedef命名函式指標

typedef用法:

//內建型別重新命名
typedef long long lint;
typedef char * pchar;

//結構體型別重新命名
typedef struct __node{
	int x, y;
}Node, *PNode;

//函式指標命名:
typedef int (*func)(int);
//func可以接收任何傳入int返回int的函式

main函式引數

//作業系統給main函式傳參
int main();
int main(int argc, char *argv[]);
int main(int argc, char *argv[], char **env);
//返回值給作業系統

argc儲存引數個數,argv儲存字串陣列(個數為argv),env儲存環境變數。

測試引數

void output(int argc, char *argv[], char **env){
	printf("argc = %d\n");
	for (int i = 0; i < argc; i++){
		printf("argc[%d] = %s\n", i, argv[i]);
	}
	printf("\n\n);
	//通過空地址作為結束條件遍歷環境變數
	for (int i = 0; env[i]; i++){
		printf("env[%d] = %s\n", i, env[i]);
	}
}

int main((int argc, char *argv[], char **env){
	output(argc, argv, env);
	return 0;
}

命令列輸入

./a.out hello world

輸出

argc = 3
argv[0] = ./a.out
argv[1] = hello
argv[2] = world


env[0] = USER=jasonhuang
env[1] = LOGNAME=jasonhuang
env[2] = HOME=/home/jasonhuang
env[3] = PATH=/home/jasonhuang/bin:/usr/local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
env[4] = MAIL=/var/mail/jasonhuang
env[5] = SHELL=/usr/bin/zsh
env[6] = SSH_CLIENT=202.204.169.80 47785 22
env[7] = SSH_CONNECTION=202.204.169.80 47785 172.17.0.15 22
env[8] = SSH_TTY=/dev/pts/0
env[9] = TERM=xterm
env[10] = XDG_SESSION_ID=34430
env[11] = XDG_RUNTIME_DIR=/run/user/1001
env[12] = LANG=en_US.utf8
env[13] = SHLVL=1
env[14] = PWD=/home/jasonhuang/C_train/struct_p_union
env[15] = OLDPWD=/home/jasonhuang
env[16] = ZSH=/home/jasonhuang/.oh-my-zsh
env[17] = PAGER=less
env[18] = LESS=-R
env[19] = LSCOLORS=Gxfxcxdxbxegedabagacad
env[20] = LS_COLORS=rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=00:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arc=01;31:*.arj=01;31:*.taz=01;31:*.lha=01;31:*.lz4=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.tzo=01;31:*.t7z=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.dz=01;31:*.gz=01;31:*.lrz=01;31:*.lz=01;31:*.lzo=01;31:*.xz=01;31:*.zst=01;31:*.tzst=01;31:*.bz2=01;31:*.bz=01;31:*.tbz=01;31:*.tbz2=01;31:*.tz=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.war=01;31:*.ear=01;31:*.sar=01;31:*.rar=01;31:*.alz=01;31:*.ace=01;31:*.zoo=01;31:*.cpio=01;31:*.7z=01;31:*.rz=01;31:*.cab=01;31:*.wim=01;31:*.swm=01;31:*.dwm=01;31:*.esd=01;31:*.jpg=01;35:*.jpeg=01;35:*.mjpg=01;35:*.mjpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.svg=01;35:*.svgz=01;35:*.mng=01;35:*.pcx=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.m2v=01;35:*.mkv=01;35:*.webm=01;35:*.ogm=01;35:*.mp4=01;35:*.m4v=01;35:*.mp4v=01;35:*.vob=01;35:*.qt=01;35:*.nuv=01;35:*.wmv=01;35:*.asf=01;35:*.rm=01;35:*.rmvb=01;35:*.flc=01;35:*.avi=01;35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.emf=01;35:*.ogv=01;35:*.ogx=01;35:*.aac=00;36:*.au=00;36:*.flac=00;36:*.m4a=00;36:*.mid=00;36:*.midi=00;36:*.mka=00;36:*.mp3=00;36:*.mpc=00;36:*.ogg=00;36:*.ra=00;36:*.wav=00;36:*.oga=00;36:*.opus=00;36:*.spx=00;36:*.xspf=00;36:
env[21] = AUTOJUMP_SOURCED=1
env[22] = AUTOJUMP_ERROR_PATH=/home/jasonhuang/.local/share/autojump/errors.log
env[23] = _=/home/jasonhuang/C_train/struct_p_union/./a.out

舉例:實現執行使用者限制

可以通過環境變數USER,限制執行該程式的使用者

#include<string.h>
#include<stdlib.h>
void (char **env){
	for (int i = 0; env[i]; i++){
		if(!strcmp(env[i], "USER=", 5)){
			if(!strcmp(env[i]+5, "your_USER_NAME")){
				printf("Welcome, USER_NAME");
			}
			else{
				printf("You have no access, Please go out!");
				exit(0);
			}
		}
	}
}

相關文章