C语言自定义数据类型详解(一)——结构体类型(上)

什么是自定义数据类型呢?顾名思义,就是我们用户自己定义和设置的类型。

在C语言中,我们的自定义数据类型一共有三种,它们分别是:结构体(struct),枚举(enum),联合(union)。接下来,我们将对这三者中的结构体进行系统和深入的学习。

目录

一、结构体的声明与创建:

(1)如何声明和创建结构体类型:

(2)函数内声明和函数外声明:

(3)匿名结构体的声明与创建:

二、结构体的自引用:

问题一:自引用的成员变量不是以指针的形式出现的:

问题二:使用不完整的数据类型去定义自引用成员变量:

三、结构体变量定义,初始化和赋值:

(1)关于结构体变量的定义:

(2)关于结构体变量的初始化:

 i. 按照顺序进行初始化(可以进行不完全的初始化):

ii. 指定成员进行初始化:

(3)区分赋值和初始化:

四、和结构体有关的计算:


一、结构体的声明与创建:

(1)如何声明和创建结构体类型:

在使用C语言来解决一些实际问题的过程当中,我们难免会发现,对于生活中的有一些数据实体,我们难以直接使用某一种单一的基本数据类型来阐述它。

比如说我现在想要表示一个学生的实体,那对于一个学生这样的实体而言,我们需要关注的信息可能有他的名字name,他的年龄gae,他考试得了多少分score等等诸如此类的要素。显然,站在我们程序设计者的角度,在这些要素里面既有字符类型name,又有int类型age,还有double类型的score。

没法用一个单一的基本数据类型来阐述它,而我们又不希望把这些诸多要素分散开来,更希望把它们整合起来表达一个具体的实体。这个时候结构体struct应运而生,结构体类型允许用户使用struct关键字将各种不同的数据类型组合在一起,形成新的数据类型用以表示复杂的数据对象。

比如下面就是一个典型的,用以表示学生的结构体类型,它的声明代码:

struct Student
{
	char name[20];//名字
	int age;      //年龄
	char sex[5];  //性别
	double score; //成绩
};

其中struct是关键字Student是结构体的名字(显然这个可以由用户自己来设计),{}里面的内容诸如name,age……这些我们称之为结构体的成员变量(这个也是用户根据自己的业务需求来设计)。 

另外,这里唯一值得大家注意和说明的一点就是:Student它不是一个完整的数据类型,struct Student它才是一个完整的数据类型。即:

struct Student
{
	char name[20];//名字
	int age;      //年龄
	char sex[5];  //性别
	double score; //成绩
};

int main()
{
    Student data1        //error,这种定义结构体的方式是不正确的!
    struct Student data2 //right,这种定义结构体的方式才正确!
    return 0;
}

(2)函数内声明和函数外声明:

结构体声明啥的有了,那带来的第一个问题便是声明位置的问题。

我们很多时候声明结构体,都习惯于将其放在函数外声明。但是并不排除有小伙伴会把结构体的声明放在函数内,如图所示:

#include<stdio.h>

int main()
{
	struct Book
	{
		char name[20];
		char author[12];
		float price;
	};
	struct Book data = { "Childhood","Gorky",5 };

	printf("%s's %s costs $%.2f", data.name, data.author, data.price);
	return 0;
}

这样写有没有问题呢,OK,没有问题。这里面可能还涉及有一些东西,诸如结构体变量的初始化这些你可能还不太会。但是这些都不是重点,我希望你注意到的是:

结构体类型和本地变量一样,在函数内声明的结构体类型往往只能在函数内部被使用。

所以,大部分的开发者,都习惯于将一个结构体的声明放在函数的外面,这样这个结构体类型就能在多个函数里面被使用啦。

(3)匿名结构体的声明与创建:

关于C语言结构体的声明和创建,在有些地方,你可能会发现有人会把代码写成下面这样的形式:

struct
{
	int x;
	int y;
}p1, p2;

这段声明,和我们前面所提到的结构体声明,它最大的特点就是在struct后面,没有那个用户所指定的名字了。然后后面紧接着的p1,p2,它们是这个类型的两个变量(注意它不是这个类型的名字)。这种定义结构体变量的方式在C语言里面是被允许的。

我们把这种没有名字的结构体类型,统一地叫做匿名结构体。这种结构体类型的最大特点是,它没有名字,无法被用户长期地使用

使用这种结构体的开发者,他们的需求,仅仅只是暂时性地需要一个或多个这种类型的变量罢了。然后这几个变量里面呢,有一些明确的成员比如x和y。至于这个类型,它是什么名字的,我不关心,因为这只是我暂时性的需求,我并不打算在很远的将来继续使用这种类型。这个时候我就可以去使用这种匿名结构体。

关于匿名结构体另外一个好玩的事情是,我们不妨先来一起来看一下下面这段代码:

#include<stdio.h>
//匿名结构体类型
struct
{
	int a;
	char b;
	float c;
}x;
struct
{
	int a;
	char b;
	float c;
}*p;

然后提出的问题是:在上面代码的基础上,下面这段代码是否合法:

int main()
{
	p = &x;
	return 0;
}

有很多人会简单认为,说,这两个匿名结构体成员变量都是一模一样的,因此他们是两个相同的结构体类型。实际上,在编译器看来,虽然这两个匿名结构体内部的成员都是一模一样的,但仍然是两个不同的结构体类型。 因此用户如果在前面代码的基础上执行p = &x操作,这样的行为将会认为是不合法的。

但是现在绝大多数的编译器,它大概率只会给你一个警告,但是我仍然希望广大读者朋友们,不要这样草率地去使用匿名结构体。

二、结构体的自引用:

结构体的自引用是指:在结构体中包含一个类型为该结构体本身的成员,这个成员变量总是以指针的形式出现

这种结构体的应用场景常常出现在数据结构中。如图所示:示范如何用结构体声明一个链表结构。

//数据结构————链表的声明
struct ListNode
{
	int val;
	struct ListNode* next;
};

这里有两个初学者容易出问题的地方。

问题一:自引用的成员变量不是以指针的形式出现的:

//数据结构————链表的声明
struct ListNode
{
	int val;
	struct ListNode next;
};

这样做,在C语言里面是不被允许的。你想,如果这样做的行为被允许了,那带来的其中一个问题就是,结构体大小将变得无法计算。

即最开始的那种定义的链表,它结构体的大小,如果要计算的话,我只要知道当前这台机器它的系统架构是x86(C语言的指针在x86的系统架构下大小是4Byte),还是x64(C语言的指针在x64的系统架构下大小是8Byte).我就能大概判断出

sizeof(struct ListNode) = sizeof(int) + 4/8(实际的大小计算会更复杂,我们将在后续的篇章给大家介绍)

但是如果说自引用的成员变量不是以指针的形式出现的。那

sizeof(struct ListNode) = sizeof(int) + sizeof(struct ListNode)

你会发现这将是一个无法被计算的表达式。

问题二:使用不完整的数据类型去定义自引用成员变量:

在正式阐述这个问题之前,我们先来认识一个C语言里面的关键字typrdef我们说typedef是C语言里面的一个关键字,它的作用就是用来给一个数据类型起别名的

注意这里起别名的含义,起了别名并不代表你前面那个类型的名字你用不了了。eg:

typedef int DataType;

int main()
{
    //以下都是在定义int类型的变量:
    int x = 0;
    DataType y = 0;
    return 0;
}

有了这个语法做铺垫,小伙伴们以后在定义链表的时候就可以这样声明和创建它了:

//数据结构————链表的声明
typedef struct ListNode
{
	int val;
	struct ListNode* next;
}ListNode;

int mian()
{
    //ListNode就是struct ListNode的别名,因此下面两种写法都是正确的:
    struct ListNode node1;
    ListNode node2;
    return 0;
}

而带来便捷的同时也可能带来一些潜在的隐患:

//数据结构————链表的声明
typedef struct ListNode
{
	int val;
	ListNode* next;//error!!!不要使用不完整的数据类型
}ListNode;

注意,这样去写就又不对了,这是因为我的typedef这个动作做完之后ListNode才是struct ListNode,换句话说,在这段代码语句中,ListNode仍然还只是一个不完整的,不可被使用的数据类型。

对于结构体的自引用大家平时注意一下这两个问题,那基本的使用就问题不大了。因此这一块的内容我们给大家介绍到这里。

三、结构体变量定义,初始化和赋值:

有了类型,接下来我们还需要学会如何去使用这个我们自定义出来的数据类型。首先第一点当然就是,如何使用这个数据类型去定义一个变量,并对这个变量进行初始化的操作。

(1)关于结构体变量的定义:

我们这里主要和大家介绍两种不同的定义结构体变量的方式方法。

第一种就是一个结构体类型被声明出来时,这个时候我们是可以去定义一些结构体变量的。如图所示:

//s1,s2都是struct Student类型的结构体变量:
struct Student
{
	char name[20];
	int age;
}s1,s2;

第二种就是声明时我不定义变量,在函数内部,当我要用到这种类型的变量时,我再去定义它。如图所示:

#include<stdio.h>
struct Student
{
	char name[20];
	int age;
};

int main()
{
	//当我需要这种类型变量的时候我再去定义它:
	struct Student s1;
	return 0;
}

(2)关于结构体变量的初始化:

关于结构体变量的初始化,一般而言有两种方式:

  1. 按照顺序对它的各个成员进行初始化;
  2. 指定其成员变量进行变量。

接下来,我们将就一个表示学生的struct Student来进行逐个说明:

//一个表示学生的结构体:
struct Student
{
	char name[20];
	int age;
    double score;
};

 i. 按照顺序进行初始化(可以进行不完全的初始化):

如图所示,即为按照顺序对结构体的各个成员进行初始化示例:

#include<stdio.h>

struct Student
{
	char name[20];
	int age;
	double score;
};

int main()
{
	//一、在结构体变量定义时,按顺序进行初始化(可以只初始化一部分,只要按顺序即可):
	struct Student s1 = { "Lisi",20,100 };
	struct Student s2 = { "Wangwu",19};
	printf("name:%s\tage:%d \tscore:%.2f\n", s1.name, s1.age, s1.score);
	printf("name:%s\tage:%d \tscore:%.2f\n", s2.name, s2.age, s2.score);
	return 0;
}

ii. 指定成员进行初始化:

如图所示,即为在定义结构体变量时指定成员进行初始化示例:

#include<stdio.h>

struct Student
{
	char name[20];
	int age;
	double score;
};

int main()
{
	//一、在结构体变量定义时,按顺序进行初始化(可以只初始化一部分,只要按顺序即可):
	struct Student s1 = { "Lisi",20,100 };
	struct Student s2 = { "Wangwu",19};
	printf("name:%s\tage:%d \tscore:%.2f\n", s1.name, s1.age, s1.score);
	printf("name:%s\tage:%d \tscore:%.2f\n", s2.name, s2.age, s2.score);

	//二、在结构体变量定义时,指定成员进行初始化:
	struct Student s3 = { .name = "Zhangsan", .score = 80 };
	printf("name:%s\tage:%d  \tscore:%.2f\n", s3.name, s3.age, s3.score);
	return 0;
}

(3)区分赋值和初始化:

大家在对变量进行操作时,一定要区分好赋值和初始化。在C语言里面,它们是两个截然不同的概念。如图所示:

#include<stdio.h>

int main()
{
    //变量的初始化:
    int x = 10;

    //变量的赋值:
    x = 20;
    return 0;
}

简单来说:

  • 初始化是指在变量声明时就为其赋予的初值,变量一开始是没有值的或者说只有未知值,初始化的目的在于使变量一开始就处于一个已知的状态,这一点对于避免未定义的行为非常重要。
  • 赋值是指在变量已经有一个确定的值的前提(即初始化)下,改变其当前值的行为。 

如果有了上面的基础,你大概就能很好地掌握,我接下来要阐述的关于结构体赋值的几个要点。关于结构体的赋值,在C语言里面我们要注意,不能使用列表(列表也就是形如{......}的这种形式)。即下面这些关于结构体赋值的操作在C语言里面都是错误的行为(如果你的这些行为被允许了,请考虑将你文件改为.c,因为这种行为在C++里面是被允许的,但是纯C当然就不行):

#include<stdio.h>

struct Book {
	char name[20];
	double price;
}book1;

int main()
{
	//错误的结构体赋值操作:
	book1 = { "平凡的世界", 8 };
	book1 = { .name = "查理九世", .price = 5 };
	return 0;
}

那正确的赋值操作,应该怎么做?正确的赋值操作应该是下面这种的:

#include<stdio.h>
#include<stdlib.h>

struct Book {
	char name[20];
	double price;
}book1;

int main()
{
	//正确的结构体赋值操作:
	strcpy(book1.name, "查理九世");
	book1.price = 5;
	return 0;
}

既然用不了列表,我们就不要去用列表嘛,OK,就是这么简单。另外,小伙伴可能会有下面这种行为:

book.name = "查理九世";

注意,name是个数组名,数组名是数组首元素的地址,那是一个常量的地址。即name的地址是改不了了的,但是你上面的代码语句的操作:是将一个常量字符串的地址交给name,这种行为已经对name的值进行改变了,因此是不被允许的。

你只能改的是name数组里面存储的内容。这里用我们前面学过的字符串内容拷贝函数——strcpy,是个不错的选择。

四、和结构体有关的计算:

以下面这个结构体为例子,我们来快速过一下和结构体有关的一些运算:

//p1,p2是两个struct Point的结构体变量:
struct Point
{
    int x;
    int y;
}point1 = {1, 2}, point2 = {3, 4};

首先和一般的变量一样,你可以用" & "操作符,拿到结构体变量的地址,并把它交给一个同类型的结构体指针变量进行保存:

struct Point* p1 = &point1;

再者,两个不同的结构体变量之间可以进行诸如point1 = point2,也就是赋值的操作:

point1 = point2;                    //相当于point1.x = point2.x,point1.y = point2.y。
point1 = (struct Point){ 5,6 };     //相当于point1.x = 5,point1.y = 6。

当然啦,结构体也可以作为我们函数的参数:比方说我现在想要设计一个函数PrintStruct,这个函数可以打印出给定结构体的各个成员,我大致可以这样来设计它的参数类型(一般情况下,我们更加推介大家使用第二种写法(即:传址优于传值),具体原因我们会在之后的篇章给大家进行说明):

//写法一:形参写的是结构体变量:
void PrintStruct(struct Point point);
//写法二:形参写的是结构体指针:
void PrintStruct(struct Point* p);

其次对于结构体变量来说,我们可以使用" . "结构体变量访问成员操作符。来访问它的内部成员。

printf("point1.x = %d, point1.y = %d", point1.x, point1.y);

 最后对于结构体指针变量,可以使用" -> "结构体指针访问成员操作符。来访问它的内部成员。

printf("point1.x = %d, point1.y = %d", p1 -> x, p1 -> y);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/959037.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Windows上通过Git Bash激活Anaconda

在Windows上配置完Anaconda后&#xff0c;普遍通过Anaconda Prompt激活虚拟环境并执行Python&#xff0c;如下图所示&#xff1a; 有时需要连续执行多个python脚本时&#xff0c;直接在Anaconda Prompt下可以通过在以下方式&#xff0c;即命令间通过&&连接&#xff0c;…

MinIO的安装与使用

目录 1、安装MinIO 1.1 下载 MinIO 可执行文件 1.2 检查 MinIO 是否安装成功 1.3 设置数据存储目录 1.4 配置环境变量&#xff08;可选&#xff09; 1.5 编写启动的脚本 1.6 开放端口 1.7 访问 2、项目实战 2.1 引入依赖 2.2 配置yml文件 2.3 编写Minio配置类 2.4…

零基础Vue学习1——Vue学习前环境准备

目录 环境准备 创建Vue项目 项目目录说明 后续开发过程中常用命令 环境准备 安装开发工具&#xff1a;vscode、webstorm、idea都可以安装node:V22以上版本即可安装pnpm 不知道怎么安装的可以私信我教你方法 创建Vue项目 本地新建一个文件夹&#xff0c;之后在文件夹下打开…

Linux查看服务器的内外网地址

目录&#xff1a; 1、内网地址2、外网地址3、ping时显示地址与真实不一致 1、内网地址 ifconfig2、外网地址 curl ifconfig.me3、ping时显示地址与真实不一致 原因是dns缓存导致的&#xff0c;ping这种方法也是不准确的&#xff0c;有弊端不建议使用&#xff0c;只适用于测试…

二叉树的最大深度(C语言详解版)

一、摘要 嗨喽呀大家&#xff0c;leetcode每日一题又和大家见面啦&#xff0c;今天要讲的是104.二叉树的最大深度&#xff0c;思路互相学习&#xff0c;有什么不足的地方欢迎指正&#xff01;好啦让我们开始吧&#xff01;&#xff01;&#xff01; 二、题目简介 给定一个二…

OpenCV imread函数读取图像__实例详解

OpenCV imread函数读取图像__实例详解 本文目录&#xff1a; 零、时光宝盒 一、imread函数定义 二、imread函数支持的文件格式 三、imread函数flags参数详解 &#xff08;3.1&#xff09;、Flags-1时&#xff0c;样返回加载的图像&#xff08;使用alpha通道&#xff0c;否…

VMware虚拟机安装macOS11

1.安装虚拟机 如果尚未安装虚拟机&#xff0c;请先进行安装。地址&#xff1a;VMware17下载地址​​​​​​ 2、下载苹果镜像文件 macOS Big Sur 11.0.1 (20B29) 3、下载unlock文件&#xff08;目的是开启VMware的macOS选项功能&#xff09; https://download.csdn.net/d…

探究 Facebook 隐私安全发展方向,未来走向何方?

随着社交媒体的普及&#xff0c;隐私和数据安全问题成为了全球关注的焦点。Facebook&#xff0c;作为全球最大的社交平台之一&#xff0c;其隐私安全问题尤其引人注目。近年来&#xff0c;随着用户数据泄露事件的不断发生&#xff0c;Facebook 不断调整其隐私政策&#xff0c;探…

jQuery阶段总结(二维表+思维导图)

引言 经过23天的学习&#xff0c;期间有期末考试&#xff0c;有放假等插曲。本来应该在学校里学习&#xff0c;但是特殊原因&#xff0c;让回家了。但是在家学习的过程&#xff0c;虽然在学&#xff0c;很让我感觉到不一样。但是效果始终还是差点的&#xff0c;本来17、18号左右…

LabVIEW太阳能照明监控系统

在公共照明领域&#xff0c;传统的电力照明系统存在高能耗和维护不便等问题。利用LabVIEW开发太阳能照明监控系统&#xff0c;通过智能控制和实时监测&#xff0c;提高能源利用效率&#xff0c;降低维护成本&#xff0c;实现照明系统的可持续发展。 ​ 项目背景 随着能源危机…

Golang Gin系列-8:单元测试与调试技术

在本章中&#xff0c;我们将探讨如何为Gin应用程序编写单元测试&#xff0c;使用有效的调试技术&#xff0c;以及优化性能。这包括设置测试环境、为处理程序和中间件编写测试、使用日志记录、使用调试工具以及分析应用程序以提高性能。 为Gin应用程序编写单元测试 设置测试环境…

Spring Boot 邂逅Netty:构建高性能网络应用的奇妙之旅

一、引言 在当今数字化时代&#xff0c;构建高效、可靠的网络应用是开发者面临的重要挑战。Spring Boot 作为一款强大的 Java 开发框架&#xff0c;以其快速开发、简洁配置和丰富的生态支持&#xff0c;深受广大开发者喜爱。而 Netty 作为高性能、异步的网络通信框架&#xff…

科普篇 | “机架、塔式、刀片”三类服务器对比

一、引言 在互联网的世界里&#xff0c;服务器就像是默默运转的超级大脑&#xff0c;支撑着我们日常使用的各种网络服务。今天&#xff0c;咱们来聊聊服务器家族中的三位 “明星成员”&#xff1a;机架式服务器、塔式服务器和刀片式服务器。如果把互联网比作一座庞大的城市&…

中民集团张敏海为国际和平发展和国际经贸合作增添更多活力

中民集团生物科技有限公司总经理、中国战略与管理研究会志愿军研究会会长助理张敏海&#xff0c;受韩国GSL集团和潘基文基金会的邀请&#xff0c;在韩国济州岛&#xff0c;与前联合国秘书长、海南博鳌论坛理事长潘基文先生&#xff0c;澳洲大使、潘基文助理、GSL集团总顾问金奉…

2025年国产化推进.NET跨平台应用框架推荐

2025年国产化推进.NET跨平台应用框架推荐 1. .NET MAUI NET MAUI是一个开源、免费&#xff08;MIT License&#xff09;的跨平台框架&#xff08;支持Android、iOS、macOS 和 Windows多平台运行&#xff09;&#xff0c;是 Xamarin.Forms 的进化版&#xff0c;从移动场景扩展到…

数据库SQLite和SCADA DIAView应用教程

课程简介 此系列课程大纲主要包含七个课时。主要使用到的开发工具有&#xff1a;SQLite studio 和 SCADA DIAView。详细的可成内容大概如下&#xff1a; 1、SQLite 可视化管理工具SQLite Studio &#xff1a;打开数据库和查询数据&#xff1b;查看视频 2、创建6个变量&#x…

Redis vs. 其他数据库:深度解析,如何选择最适合的数据库?

一、如何为项目选择合适的数据库&#xff1f; 选择合适的数据库是一个复杂的过程&#xff0c;需要综合考虑多个因素。下面几个维度来详细阐述&#xff1a; 1.数据模型 关系型数据库&#xff08;RDBMS&#xff09;&#xff1a;适用于高度结构化、关联性强的数据&#xff0c;如电…

c#使用log4Net配置日志文件

1.# 写一个通用类 LogHelper using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using log4net;namespace WindowsFormsApplication22 {public class LogHelper{static ILog mylog LogManager.GetLogge…

2_高并发内存池_各层级的框架设计及ThreadCache(线程缓存)申请内存设计

一、高并发内存池框架设计 高并发池框架设计&#xff0c;特别是针对内存池的设计&#xff0c;需要充分考虑多线程环境下&#xff1a; 性能问题锁竞争问题内存碎片问题 高并发内存池的整体框架设计旨在提高内存的申请和释放效率&#xff0c;减少锁竞争和内存碎片。 高并发内存…

【深入理解FFMPEG】命令行阅读笔记

这里写自定义目录标题 第三章 FFmpeg工具使用基础3.1 ffmpeg常用命令3.1.13.1.3 转码流程 3.2 ffprobe 常用命令3.2.1 ffprobe常用参数3.2.2 ffprobe 使用示例 3.3 ffplay常用命令3.3.1 ffplay常用参数3.3.2 ffplay高级参数3.3.4 ffplay快捷键 第4章 封装与解封装4.1 视频文件转…