C语言学习笔记4

发布时间 2023-07-26 15:49:04作者: 张彧520

前言

c语言中对字符和字符串的处理很是频繁,但是c语言本身是没有字符串类型的,字符串通常放在常量字符串中或者字符数组中。字符串常量适用于那些对它不做修改的字符串函数。

VS中库函数路径

C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.36.32532\crt\src

字符串库函数

求字符串长度

strlen

size_t strlen(const char*str);
字符串以'\0'作为结束标志,strlen函数返回的是在字符串中'\0'前面出现的字符个数(不包含'\0')
参数指向的字符串必须要以'\0'结束
注意函数的返回值位size_t,是无符号的(易错)
学会strlen函数的模拟实现
size_t==unsigned int	返回无符号类型

eg:
int str1="abcdef";		//6
char arr[]={'a','b','c','d','e','f'}		//s随机值

模拟实现
    #include <stdio.h>
#include <string.h>
int my_strlen(char * str) {
		if(*str != '\0') {
			return 1 + my_strlen(str + 1);	//递归调用
		}
		else {
			return 0;
		}
}
int main() {
	char arr[] = "bit";
	//int len = strlen(arr);	求字符串长度
	//printf("%d\n", len);
	int len = my_strlen(arr);//arr是数组,数组传参,传过去的不是整个数组,而是第一个元素的地址
	printf("len= %d\n", len);
	return 0;
}
长度不受限制的字符串函数

strcpy

char* strcpy(char* destination,const char* source);
源字符串必须以'\0'结束
会将源字符串中的'\0'拷贝到目标空间
目标空间必须足够大,以确保能存放源字符串
目标空间必须可变
学会模拟实现

错误示范
    char* arr1="abcdef";	//常量字符串
	char arr2[]={'b','i','t'}	//没有'\0'


#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<assert.h>
char* my_strcpy(char* dest, const char* src) {
	char* ret = dest;
	assert(dest != NULL);//断言,传违规报错
	assert(src != NULL);//断言
	//把src指向的字符串拷贝到dest指向的空间,包含'\0'字符
	while (*dest++ = *src++ ) {
		;
	}
	return ret;
}
int main() {
	char arr1[] = "#######";
	char arr2[] = "bit";
	printf("%s\n", my_strcpy(arr1, arr2));
	return 0;
}

strcat

char* strcat (char* destination,const char* source);
源字符串必须以'\0'结束
目标空间必须足够大,以确保能存放源字符串的内容
目标空间必须可修改
无法追加自己
    
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <assert.h>
char* my_strcat(char* des, const char* sou) {
	assert(des && sou);
	char* ret = des;
	//找到字符'\0'
	while (*des != '\0') {
		des++;
	}
	//追加
	while (*des++ = *sou++) {
		;
	}
	return ret;
}
int main() {
	char arr1[15] = "abc";
	char arr2[] = "def";
	char *ret = my_strcat(arr1, arr2);
	printf("%s", ret);
	return 0;
}

strcmp

int strcmp(const char*str1,const char*str2);
标准规定(vs里是这样,其他可能不一样)
第一个字符串大于第二个字符串,则返回大于0的数字
第一个字符串等于第二个字符串,则返回0
第一个字符串小于第二个字符串,则返回小于0的数字

模拟实现
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <assert.h>
int my_strcmp(const char* str1, const char* str2) {
	assert(str1 && str2);
	while (*str1 == *str2) {
		if (*str1 == '\0')
			return 0;
		str1++;
		str2++;
	}
	if (*str1 > *str2)
		return 1;
	else
		return -1;
}
int main() {
	const char *p1 = "abcdef";
	const char *p2 = "abcc";
	int ret = my_strcmp(p1, p2);
	printf("%d\n", ret);
	return 0;
}
长度受限制的字符串函数介绍

strncpy

char* strncpy(char* destination,const char* source,size_t num);
拷贝num个字符,从源字符串到目标空间
如果源字符串的长度小于num,则拷贝完源字符串之后,在目标的后边追加0,直到num个

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <assert.h>
char* my_strncpy(char* des, const char* sou, int num) {
	assert(str1 && str2 && num);
	char* start = des;
	while (num && (*des++ = *sou++)) 
		num--;
	if (num)
		while (--num)
			*des++ = '\0';
	return start;
}
int main() {
	char p1[10] = "abcdef";
	char p2[] = "abcc";
	printf("%s\n", my_strncpy(p1,p2,5));
	return 0;
}

strncat

char * strncat ( char * destination, const char * source, size_t num );

strncmp

int strncmp ( const char * str1, const char * str2, size_t num );
字符串查找

strstr

const char * strstr ( const char * str1, const char * str2 );      

char * strstr (char * str1, const char * str2 );

strtok

char * strtok ( char * str, const char * delimiters );

delimiters参数是个字符串,定义了用作分隔符的字符集合
第一个参数指定一个字符串,它包含了0个或者多个由sep字符串中一个或者多个分隔符分割的标记。
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。(注:strtok函数会 改变被操作的字符串,所以在使用strtok函数切分的字符串一般都是临时拷贝的内容并且可修改。)
strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
如果字符串中不存在更多的标记,则返回 NULL 指针。

eg:
//zyu
//gmail
//com
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
int main() {
	char p1[] = "zyu@gmail.com";
	const char *p2 = "@.";
	char* ret = NULL;
	for (ret = strtok(p1, p2); ret != NULL; ret = strtok(NULL, p2)) {
		printf("%s\n",ret);
	}
	return 0;
}
错误信息报告

strerror

char * strerror ( int errnum );
errno是一个全局的错误码的变量
当c语言库函数在执行过程中,发生了错误,就会把对应的错误码赋值到errno中
错误码      错误信息
0           No error
1			Operation not permitted
2			No such file or directory

eg:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
int main() {
	FILE* pf = fopen("text.c", "r");//打开text.c文件,只读模式
	if (pf == NULL) {
		printf("%s\n", strerror(errno));
	}
	else {
		printf("成功。\n");
	}
	return 0;
}
字符分类函数
函数 如果他的残数符合下列条件就返回真
iscntrl 任何控制字符
isspace 空白字符:空格' '、换页'\f'、换行'\n'、回车'\r'、制表符'\t'或则垂直制表符'\v'
isdigit 十进制数字0-9
isxdigit 十六进制数字,包括所有十进制,大小写a-f(A-F)
islower 小写字母a-z
isupper 大写字母A-Z
isalpha 字母a-z或A-Z
isalnum 字母或者数字a-z,A-Z,0-9
ispunct 标点符号,任何不属于数字或者字母的图形字符(可打印)
isgraph 任何图形字符
isprint 任何可打印字符,包括图形字符和空白字符
字符转换
tolower()
toupper()

eg:
//i am a student
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <ctype.h>
int main() {
	char arr[] = "I AM A Student";
	int i = 0;
	while (arr[i]) {
		if (isupper(arr[i])) {
			arr[i] = tolower(arr[i]);
		}
		i++;
	}
	printf("%s\n", arr);
	return 0;
}
内存操作函数

memcpy

void * memcpy ( void * destination, const void * source, size_t num );
内存复制
将num 个字节 的值从source指向的位置直接复制到destination指向的内存块

destination
指向要复制内容的目标数组的指针,类型转换为void*类型的指针。
source
指向要复制的数据源的指针,类型转换为const void*类型的指针。
num
要复制的字节数。
size_t是无符号整数类型。

memmove

void * memmove ( void * destination, const void * source, size_t num );
num 个字节 的值从source指向的位置复制到destination指向的内存块。复制就像使用中间缓冲区一样进行,允许目标和源重叠

destination
指向要复制内容的目标数组的指针,类型转换为void*类型的指针。
source
指向要复制的数据源的指针,类型转换为const void*类型的指针。
num
要复制的字节数。
size_t是无符号整数类型。

memset

void * memset ( void * ptr, int value, size_t num );
将ptr指向的内存块的 前num个字节设置为指定值

ptr
指向要填充的内存块的指针。
value
待设定值。该值作为int传递,但函数使用该值的unsigned char转换来填充内存块。
num
要设置为value 的字节数。
size_t是无符号整数类型。

memcmp

int memcmp ( const void * ptr1, const void * ptr2, size_t num );
将ptr1指向的内存块的 前num个字节与ptr2指向的前num个字节进行比较,如果它们全部匹配,则返回零;如果不匹配,则返回一个不同于零的值,表示哪个更大。 请注意,与strcmp不同,该函数在找到空字符后不会停止比较。

ptr1
指向内存块的指针。
ptr2
指向内存块的指针。
num
要比较的字节数。

结构体

结构体类型的声明

结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量。

struct tag
{
member-list;
}variable-list;		//全局变量
假如描述一个学生
struct stu
{
char name[20];
int age;
char sex[10];
char id[20];
};	//分号不能丢

匿名结构体类型只能引用一次。

结构的自引用

在结构中包含一个类型为该结构本身的成员

struct Node
{
int data;
struct Node*next;	//利用指针
};

typedef struct Node
{
int data;
struct Node*next;
}Node;

结构体变量的定义和初始化

在C语言中,结构体变量的定义和初始化方式如下:

定义结构体类型:

struct Book {
    char title[50];
    char author[50]; 
    char subject[100];
    int book_id;
};


定义结构体变量:
//方式一:
struct Book book1;  

// 方式二:起别名  
typedef struct { ... } Book;
Book book2;


初始化结构体:
struct Book book1 = {"C Programming", "Nuha Ali", "C Programming Subject", 6495};

Book book2 = {
    "Python Programming", 
    "John Doe",
    "Python Programming",
    7215 };


注意:

- 结构体成员初始化的顺序要与结构体定义中成员顺序一致
- 可以省略成员,未初始化的成员将默认初始化
- 可以仅初始化部分成员

访问结构体成员:
book1.title;   // "C Programming"
book2.book_id; // 7215


结构指针:
struct Book *p;
p = &book1;
p->book_id;  // 6495

我的建议是使用 typedef 起别名,这样定义结构体变量时会更简洁:
typedef struct { ... } Book;

Book book1;
Book *p = &book1;

结构体内存对齐

结构体变量的大小是根据它包含的成员计算的。计算结构体变量大小主要的步骤是:
1. 计算每个成员的大小
每个成员根据其类型的预定大小来计算,如 int 的大小一般是 4 字节,double 的大小一般是 8 字节。
2. 检查是否需要补齐
如果下一个成员的地址不再上一个成员的整数倍时,会插入一些补齐字节(也称作“填充”字节)来保持对齐。
3. 将每个成员的大小和补齐字节相加
最后将每个成员的大小和补充字节相加,得到整个结构体的大小。
结构体对齐是重要的,因为它可以提高内存访问效率。

可以设置默认对齐数
#pragma pack(4)

举个例子:
struct Test {
    char c;
    int i;   
    double d;
};
这里:
- char 的大小是 1 字节
- int 的大小是 4 字节,其对齐单位也是 4 字节
- double 的大小是 8 字节,对齐单位也是 8 字节
这里 int 和 double 之间需要插入 3 个补齐字节,所以总大小是:
1(char) + 4(int) + 3(填充) + 8(double) = 16 字节
所以该结构体的大小是 16 字节。



结构体内存对齐主要遵循以下规则:
1. 对齐单位是数据类型的大小。即 int 对齐单位是 4 字节,double 对齐单位是 8 字节。
2. 成员按照声明顺序排列。
3. 每个成员的起始地址对应它的数据类型规定的内存地址。
4. 如果下一个成员的地址不是上一个成员地址的整数倍时:
   - 会插入额外的字节数(这导致内存浪费,但是加快访问速度)
   - 这些额外的字节数被称为 "padding" ,是空闲的
   - 编译器选择的 padding 大小最少满足下一个成员的对齐要求
5. 结构体的总大小是其成员占用字节数的总和,包括 padding。
例如:
struct Test {
    char c;   
    int i;    
    double d; 
}
这里:
- c 对齐单位是 1 字节,起始地址是 0
- i 对齐单位是 4 字节,但它紧跟在 c 后面,地址是 1 ,不是 4 的倍数 
- 所以插入 3 个字节的 padding 
- d 对齐单位是 8 字节,起始地址变为 8
总结:
- c: 1 字节
- i: 4 字节
- padding: 3 字节 
- d: 8 字节
结构体总大小:1 + 4 + 3 + 8 = 16 字节



结构体内存空间的计算方法如下:
1. 每个成员按照其类型计算大小。
2. 内置基本类型(char、int等)直接按照其大小。
3. 如果有结构体嵌套,里面的结构体先计算大小。
4. 如果有数组,计算数组中每个元素的大小再乘以元素个数。
5. 成员之间有可能存在空闲空间(known as padding),为了对齐地址,编译器会在成员之间加入额外的字节数。
6. 最后将每个成员的大小相加,加上成员之间的空闲空间大小,得到整个结构体的总大小。
示例: 
struct Test {
    char c;   // 1 byte 
    int i;    // 4 byte
    double d; // 8 byte
    struct Inner {
        char c;
        int i;
    }s;      // 1 + 4 = 5 byte
};

// Total size = 
// c: 1
// i: 4 
// d: 8
// s: 5 (struct Inner)
// + 3 bytes padding after i  
// = 1 + 4 + 8 + 5 + 3 = 21 bytes

所以上述结构体的大小为 21 字节。
结构体内存对齐也很重要,编译器会在成员之间加入空闲空间,这样内存访问会更高效。


在C语言中,存在内存对齐的主要原因有以下几点:
1. 提高内存访问效率处理器访问内存时,是以一个字(word)为单位进行的。若变量的内存地址恰好是字大小的整数倍,那么处理器可以一次内存访问就读取到变量的值,访问速度很快。若不对齐,处理器需要进行多次内存访问才能读取变量,速度较慢。
2. 符合处理器和外设的访问要求很多处理器和外设都要求它们访问的内存地址必须是某个数字的整数倍,否则无法正确访问。进行对齐就可以满足这个要求。
3. 避免一些问题的出现某些处理器在访问未对齐的内存地址时,会抛出异常或者产生错误。进行对齐可以避免这种情况。
4. 提高代码可移植性不同的机器有不同的对齐要求。进行对齐可以使代码能够在不同平台上运行。所以总体来说,内存对齐是为了提高访问效率,使代码在不同平台可移植,避免一些问题,从而编写出性能更优和更稳定的程序。

结构体传参

在C语言中,可以通过两种方式对结构体进行传参:
1. 传结构体变量
直接传递结构体变量,函数内可以直接访问结构体成员:

struct Point {
  int x;
  int y; 
};
// 传递结构体变量
void printPoint(struct Point p) {
  printf("%d, %d", p.x, p.y);
}
int main() {
  struct Point p1 = {1, 2};
  printPoint(p1); // 传结构体变量
  return 0;
}


2. 传结构体指针
传递结构体的指针,函数内需要通过指针访问结构体成员:
void printPoint(struct Point *p) {
  printf("%d, %d", p->x, p->y); 
}
int main() {
  struct Point p2 = {3, 4};
  printPoint(&p2); // 传结构体指针
  return 0;
}
结构体传参的办法:
- 传结构体变量,函数直接访问成员
- 传指针,需要通过指针访问成员
根据情况选择合适的传参方式。指针方式可以减少内存复制。

结构体实现位段

在C语言中,结构体位段(Bit Field)允许在结构体中的成员占用的位数小于存储类型的位数。
位段成员必须是int,unsigned int或signed int(char,short); 
结构体位段的定义语法:
struct 结构体名{
  类型名 成员名 : 位数;
};


示例:
struct Packed_Struct {   
  unsigned int age : 3;
  unsigned int sex : 1;
  unsigned int married : 1;
};
这个结构体包含3个成员:
- age占3个bit位,取值范围0~7 
- sex占1个bit位,0表示男性,1表示女性
- married占1个bit位,0表示未婚,1表示已婚
位段的特点:

- 节省空间,只占实际需要的bit位数
- 一个成员只能占用连续的bit位
- 位段成员不能超过其类型的范围
位段经常用于需要节省空间并且每个字段值较小的场景,例如开关状态、信号表示等。
需要注意不同编译器对位段的支持可能有所不同。


在C语言中,结构体位段(Bit Field)允许指定成员占用的bit数。每个位段成员有以下特点:
1. 位段成员必须是整数类型(char、short、int等)或枚举类型,不能是其他类型。
2. 位段的宽度不能超过其类型的范围。例如int类型位段的宽度不能超过32位。
3. 位段的宽度可以是0,这表示该成员不占用任何存储空间。
4. 位段在结构体中的顺序位置可以任意。
5. 不同位段成员之间可能存在空隙,编译器会自动优化空隙的位置。
6. 位段不能使用取地址运算符&。
7. 位段成员赋值时不能跨越字节边界。
例如:
struct Packed_Struct {
  unsigned int f1:1;
  unsigned int :2; // 2位空隙
  unsigned int f2:3;
  unsigned int :0; // 不占用空间
  unsigned int f3:5; 
};
这个位段结构体有3个整数型成员,宽度分别是1、3、5位。
使用位段可以减小结构体的大小,但操作起来不如普通成员方便,需要注意一些特殊情况。

枚举

枚举类型的定义,优点和使用

在C语言中,枚举(enum)的定义方式如下:
enum 枚举名 { 
  标识符1, 
  标识符2,
  ...
};

枚举将一组相关的标识符定义在一起,赋予一个新的类型名。
枚举有以下特点:
- 枚举列表中的标识符不能重复。
- 如果不手动赋值,从0开始自动赋值。
- 默认为int类型。
示例:
enum Color {
  RED, 
  GREEN,
  BLUE
};

enum Signal {
  GREENLIGHT = 10,
  YELLOWLIGHT,
  REDLIGHT  
};
第一种方式没有指定值,默认为0,1,2。
第二种方式指定了GREENLIGHT的值,后面的会自动赋值11,12。
枚举提供了一种更友好的方式来定义一组数值常量,比#define宏定义更清晰。


枚举(enum)在C语言中使用,相比定义数值常量的#define宏,具有以下优点:
1. 增加代码可读性
枚举使用标签名来表示意义,比数字常量更易读和理解。
2. 避免命名污染
macro定义的常量是直接替换到代码中,可能会引起命名冲突。枚举为每组值定义一个新的类型。
3. 提供类型安全检查
枚举有自己的类型,不能与其他类型直接比较和赋值。增加了类型安全性。
4. 方便调试
调试时能显示枚举名称,而不是数字值,更易于理解。
5. 方便扩展和维护
向枚举中增加新值不会影响代码的其他部分。
6. 避免魔术数字
枚举值有明确的定义,避免出现难以理解的数字。
7. 方便在switch语句中使用
枚举值很适合在switch语句做条件判断。
总之,使用枚举可以写出更简洁、可读性更好、安全性更高的代码。在需要定义一组数值常量时,枚举是很好的选择。


这里是一个使用枚举的简单示例:
// 定义一个颜色枚举
enum Color {
  RED,
  GREEN,
  BLUE
};
int main() {
  enum Color favorite = RED;  
  // 枚举值可以直接 comparisons
  if (favorite == RED) {
    printf("Your favorite color is red!"); 
  }  
  // 枚举类型安全,不能与其他类型直接比较
  int a = 1; 
  if (favorite == a) { // 错误,不能与int比较
    ...
  }  
  return 0;
}
这个例子中,我们定义了一个Color枚举,包含红、绿、蓝三种颜色。
在main函数中,我们将favorite变量设置为RED枚举值。
然后通过比较favorite和RED来判断最喜欢的颜色。
枚举值之间可以直接比较。但是枚举类型与整数类型不可以直接比较,这样增加了类型安全性。
通过这个示例可以看出,使用枚举可以使代码更直观清晰。

联合(共用体)

联合类型的定义

在C语言中,联合(union)的定义格式如下:
union 联合体名 {
  成员1;
  成员2; 
  ...
} 对象名;

联合(union)的特点是:

- 联合的所有成员共享同一块内存空间,每次只能存放一个成员的值。
- 联合占用的内存大小等于最大成员大小。
- 可以通过任一成员来访问储存的值。
- 不同类型的成员不能同时存值。

例如:
union Data {
  int i; 
  float f;
  char str[20];
} data; 

联合Data有int,float,数组三个成员,但它们共享一块内存。
在任意时刻,只能存放一个成员的值,当修改一个成员时,其他成员的值就会丢失。
访问时也只能获取最后赋值成员的值。
联合可以用于节省空间,当需要存储多个互斥的数据时使用。但需要注意访问时的正确性。


联合(union)的一些常见使用场景包括:
1. 节省空间
当一个对象中多个成员不会同时使用时,可以使用联合节省空间。比如VARIANT结构在Windows编程中广泛使用。
2. 存放不同类型的数据
联合可以通过不同成员存取不同类型的数据,例如网络数据包的头部信息。
3. 类型转换
可以将数据存入联合的一个成员,然后从另一个成员读取来实现类型转换。
4. 实现类似重载函数的效果
根据传入参数的不同来调用联合中不同的处理函数,来得到类似重载函数的效果。
示例:
// 联合实现类型转换
union Data {
  int i;
  float f;
};
union Data data;
data.i = 10; // 赋int值
data.f; // 读取float值,获得10.0

注意事项:
- 必须跟踪当前的成员,否则容易读取到错误的数据
- 联合不能包含某些复杂类型如结构体
- 尽可能少使用联合,过度使用会使代码难以理解
总之,联合可以用来节省内存空间,但需要细心跟踪当前成员。应该谨慎使用,避免代码可读性变差。

联合的特点

联合(union)在C语言中有以下几个主要特点:
1. 共用内存空间
联合的所有成员共享同一段内存,一次只能存储一个成员的值。
2. 内存大小由最大成员决定
联合所占内存大小取决于最大成员的大小。
3. 任一成员存取都会覆盖值
向任意一个成员赋值,会覆盖之前存放在联合中的值。
4. 需跟踪当前成员
C语言不跟踪当前union的活跃成员,需要程序员自己跟踪。
5. 默认公有访问权限
联合默认成员是公有访问权限,可以直接访问。
6. 不能嵌套包含复杂类型
联合不能包含某些复杂类型,如结构体等。
7. 使用简单类型为佳
联合最好只包含简单内置类型,不要过于复杂。
8. 不要过度使用
联合的过度使用会使程序逻辑变得复杂难懂。
总结一下,联合可以节省空间,但需要谨慎使用,跟踪当前成员,避免误用。要在可读性与节省空间中求平衡。

联合大小的计算

在C语言中,联合的大小是其成员中最大成员的大小。
具体规则是:
1. 联合的大小等于其中最大成员的数据类型所占用的存储空间。如下:
union {
   char c; 
   int i;
   float f;
} u;
这里联合u的大小为int i占用的存储空间,一般是4个字节。
2. 如果联合的成员类型相同,那么它们共享同一块内存空间,联合的大小还是最大的那个类型的大小。
3. 在计算联合大小时,会忽略位域。
4. 由于联合的成员共享同一块内存,所以访问其它成员会覆盖前面的成员数据。

所以,根据上述规则可以总结出:
联合的大小等于其最大成员的大小,而与包含的成员个数无关。

结构体补充

在C语言中,结构体(struct)的定义格式如下:
struct 结构体名 {
  成员列表
};
结构体允许我们将不同的数据类型组合在一个自定义的类型中。

结构体定义示例:

// 定义一个结构体Person,包含姓名和年龄
struct Person {
  char name[20]; 
  int age;
};
// 定义一个结构体Student,包含学号、成绩
struct Student {
  int id;
  float score;
};
结构体的使用示例:
// 声明结构体变量p,类型为Person
struct Person p;
// 访问结构体成员  
p.age = 20;
strcpy(p.name, "John"); 
// 声明结构体指针变量ps,并分配内存
struct Person *ps = malloc(sizeof(struct Person));
// 通过指针访问结构体成员
ps->age = 25;
strcpy(ps->name, "Mary");

需要注意的是:
- 结构体中可以包含不同类型的数据
- 使用"."访问结构体变量的成员
- 使用"->"通过指针访问结构体成员
- 结构体变量和指针都需要先声明后使用



在C语言中, -> 和 . 是两个访问结构体成员的运算符。
.运算符用于访问结构体变量的成员。
->运算符用于通过指向结构体的指针访问结构体的成员。

例如:
struct Person {
  int age;
  char name[20];
}
struct Person p;
p.age = 18; // 通过.访问结构体成员
struct Person *ptr = &p; 
ptr->age = 20; // 通过->访问指针所指向的结构体成员

其中,p是结构体变量,ptr是指向结构体的指针。
.操作符直接作用于结构体变量,访问其成员。
->操作符作用于指向结构体的指针,访问指针所指向的结构体的成员。

所以:
- .访问结构体变量的成员
- ->通过指针访问结构体成员
这可以理解为:箭头总是指向它要访问的内容。



这里给出一个使用指针和结构体的具体示例:
#include <stdio.h>
struct Student {
  int id;
  char name[20];
  float score;
}
int main() {
  struct Student stu1 = {101, "John", 89.5};
  // 指针指向结构体
  struct Student *ptr = &stu1; 
  // 通过指针访问结构体成员
  printf("ID: %d\n", ptr->id); 
  printf("Name: %s\n", ptr->name);
  printf("Score: %f\n", ptr->score);
  // 直接通过结构体变量访问成员
  printf("ID: %d\n", stu1.id);
  printf("Name: %s\n", stu1.name);  
  printf("Score: %f\n", stu1.score);
  return 0;
}
输出:
ID: 101  
Name: John
Score: 89.500000
ID: 101
Name: John
Score: 89.500000
这个例子中:
- 定义了Student结构体,包含id、name、score成员
- 创建stu1作为结构体变量 
- ptr为指向stu1的指针
- 通过ptr->访问成员,也可以直接通过stu1.访问成员