C语言学习笔记:结构体与指针

结构体

结构体定义

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

结构体的内存分布

不一定是结构体内元素的简单相加,
C语言学习笔记:结构体与指针_第1张图片
我们认为,结构体中的内存分布应该如上图左图所示,但是实际上是右图,由于结构体中存在内存对齐原则

内存对齐原则

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
            
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 类型在内存中的存储方式:
C语言学习笔记:结构体与指针_第2张图片
不同位数的操作系统的区别,就是内存寻址能力不同。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
#include
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);
			}
		}
	}
}

你可能感兴趣的:(c语言,指针)