Misaki`s blog

学习是一种态度


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

【Linux】Linux编程-基础

发表于 2018-12-05 | 分类于 Linux
字数统计: 1.6k

Linux编程 基础

  我们学习Linux的主要目的就是为了在让我们所写的代码可以在Linux环境下稳定运行,因为作为一个服务端程序员,我们将来所接触的服务器的系统内核全部都为Linux,也就是说我们所写的所有代码都要求在Linux服务器上依然可以稳定运行,这是作为服务端程序员的基本素养。因此Linux环境下的编程也是我们必须要学习的。

第1节

Linux编程准备材料

  我们将来并不是直接在服务器上对Linux服务器进行编程,更多的是用远程链接的方式来操作服务器,因此作为远程链接的桥梁有一款软件是我们今后会经常使用的,也是之前我所提过的Xshell。其实在Linux系统内核外壳有一门独到的脚本编程语言,我们利用这款脚本语言来控制Linux,这款语言就叫shell。我们之前所说的Linux基本命令其实就是在利用shell脚本语言来控制Linux系统。而在这里所说的Xshell这么一款模拟Linux终端的模拟软件,在Xshell里链接到我们的Linux系统后的操作就与直接在Linux终端里的操作几乎完全一样了(实际上还是有一些区别)。因此我们之后的所有操作都会利用Xshell来远程控制我们的虚拟机。因此在Linux编程中Xshell是必不可少的。

  Xshell下载地址:https://gitee.com/HGtz2222/xshell_install_package/raw/master/Xshell-6.0.0082p.exe

将Xshell连接到Linux

  1、打开Linux虚拟机和Xshell。我们要将Linux连接到Xshell首先当然肯定是要先打开我们的虚拟机啦,当然Xshell也要打开

  2、在Linux终端输入ifconfig指令,然后在下图红框处找到自己的ip地址。

Xshell连接

  然后在Xshell中输入如下图的命令。

Xshell连接

  之后按要求输入Linux中的用户名及密码即可连接上虚拟机。

Xshell连接

  连接上之后的操作都和Linux终端中的一致了。同时Xshell比Linux的终端好看了不止一点,可以改变界面透明度和背景,使用起来心情舒畅多了。

如何在Linux中编程

  正如我们之前在Windows中使用的vs2017这个ide一样,在Windows中我们大多使用类如这样方便的集成式ide来进行编程,ide将我们编写程序所需要用到的工具进行整合,合为一体供我们使用,十分方便快捷。

  而在Linux中不同于Windows的是,Linux这类操作系统更崇尚的是利用简而精的软件而并非Windows使用所崇尚的这种一站集成式的软件,因此在Linux中编程我们并不会接触到类似于vs2017这样的集成式的开发环境,而是将其打散成多个小软件分别使用。这样的方式是类Windows操作系统与类Unix操作系统关键性的区别。

  那么在vs2017这个继承开发环境中都包含了哪些基本的功能供我们使用呢?可以大致分为以下四种,并且我们在Linux中都找到了对应的软件来代替vs2017中这一功能,我也一并将其列出来。

  1、代码编辑器 —— vim。

  2、编译器 —— gcc。

  3、调试器 —— gdb。

  4、项目组织 —— makefile。

  以上这四款基本软件我们今后在Linux编程中会经常使用,可以说只要是在Linux下编程就必定会用到这四款软件。那么我们怎么在Linux下找到这四款软件呢?

yum使用

  在Linux中内置了一个类似于应用商店的东西,我们称之为包管理器。在这个应用商店中我们可以找到一些我们常用的包,来提供给我们的Linux一些新的扩展功能。接下来我会以在yum下载rzsz这个软件包为例讲解如何使用yum。以下流程均需虚拟机联网。

  1、我们先利用yum来搜索rzsz的软件包。输入yum list | grep rzsz命令,稍等即可出现搜索结果。如果过长时间未出现结果可取消重试或检查网络连接后重试。(ctrl+c取消命令)

Xshell连接

  2、切换到root管理员下载安装rzsz既可。确保自己在root管理员权限下输入yum install 包名即可自动安装,其中包名为我们刚刚搜索出来的软件包的名称。

  注意:在Linux下安装删除软件均需切换到root管理员才可执行。

  (输入su命令后输入root密码后即可切换为root管理员。)

Xshell连接

  之后我们的rzsz就安装成功了,如果需要删除这个包,则也需要在root管理员权限下,输入yum erase 包名即可。当操作结束后最好退出root权限(输入exit即可退出)。毕竟还是用普通用户比较安全。

  虽说我们安装好了rzsz但是这个包到底有什么用处呢?大致作用可以解释为让Windows和Linux两个系统上的文件可以自由快速传输,如果没有这个软件包纯粹利用FTP进行文件传输有着极高的延迟,而且不够方便,而有了rzsz这可以大大改善这一点,是我们将来工作的必备软件包之一。

之后的准备

  在介绍完yum的使用后,我们平时使用的很多软件包都可以直接从yum上进行下载,但也并非所有东西yum里都有。

  但是在Linux编程上我们要经常使用vim、gcc、gdb、makefile这四款软件,但是好消息是这四款软件有的在Linux上已经提前安装好了,我们直接使用即可。关于这四款软件的具体使用会在Linux编程的其他章节继续介绍。

【Linux】Linux基础-2

发表于 2018-12-04 | 分类于 Linux
字数统计: 2.2k

Linux基础

第2节

  在此章节着重介绍Linux的基本指令,并且穿插一些Linux的基本知识点进行讲解,同时学习Linux最重要的一点是不要去背,在使用中学习,用的多了自然就会了,并且一开始学习Linux大家可能会觉得各种不顺手,但是学习久了大家就会被Linux的魅力所吸引,欲罢不能!。

终端简介

基础介绍

  在Linux的使用中,我们最为经常使用的就是终端,我们之后可以说一切的指令都会在终端中进行输入,那么如何打开终端呢?鼠标右键点击Linux桌面,就会出现打开终端选项,点击后会出现一个如下图一般的类似于Windows控制台的窗口,这就是Linux的终端了。所谓终端类似于Windows下的控制台,大家可以理解为控制Linux的神经中枢,我们的操作之后都会在这个终端中进行,不过之后会讲解如何使用Xshell,大家就可以通过远程的方式链接到终端了。

终端

Linux的目录

  大家可以注意到在终端中命令前都会加上这么一行字。

目录

  正如图中所示,这三个标签都有各自的含义,其中当前目录名尤为重要,因为我们在一个地方输入命令我们首先就得清楚我们当前在哪个目录下,不然可能会出现不必要的错误。而目录中最为特殊的目录有两个~表示的是家目录,/表示的是根目录。

  我们可以把Linux想象成一栋大厦,这栋大厦中有很多很多的住户,只有一个神一般的存在我们称其为楼管。我们是其中一个住户我们在这栋大厦中有自己的家,我们可以在自己家中为所欲为,不收任何限制,那么其实家目录就好比这栋大厦中我们自己的家一样,完全受我们自己控制,我们之后的大部分操作也都是在家目录中进行的。而根目录就好比这栋大楼的根基一样,十分危险也有重重保障,我们在根目录中的权限受到限制,毕竟我们不能说把一栋楼拆了就把一栋楼拆了,这栋楼没了Linux也就凉凉了,上面所有的用户都得来找你算账。然而在Linux这种大厦中有一个神一般的楼管,他可以在整栋大厦中为所欲为,这个楼管就是root管理员。

  但在实际中Linux的目录结构更像是一棵树,是数据结构中的树,就是一棵跟实际中的树正好相反的树。(大致如下图)

目录

  可以发现这样的目录结构与Windows十分相似,没错这样的目录结构方便管理,更符合大家对目录的思想。可以看出根目录在树的最顶端(在数据结构中应该叫做树根),是一切目录的根目录,因此可以看出根目录在Linux中举足轻重的地位,是整个系统的根本。

文件信息

  接下来我们可以尝试在终端中输入ll这个指令,这个指令的具体功能可以大概描述为查看当前目录下文件以及目录的信息,输入后大概会出现以下的信息。

文件信息

  在列表中列出的标蓝字体的为目录,也就是我们俗称的文件夹,未标蓝的则为文件。可以看出不论是目录还是文件夹的前都有一行信息,关于这行信息我们是接下来要详细说明的。
文件信息

  我用红线将这行信息分为7段,其中每一段都有自己不同的含义。

  段1:文件属性,这个之后详细解释;段2:文件个数,如果是目录可能包含有多个文件,如果是文件,则当然是1了;段3:文件拥有者(Owner),创建文件的用户,如果实在家目录下一般都是用户本身;段4:表示拥有者所属的组(Group),如果没有添加新的组,则默认会将用户名作为组名;段5:文件大小,单位是byte;段6:创建时间;段7:文件名。

  在段1中,我们要重要介绍。段1的第一个字母表示的是文件所属类型,如果是目录则显示的是d,如果是文件则显示的是-,还有一些其他文件属性。之后的九个栏位,表示文件的权限。Linux的文件基本上分为三个权限:可读(r),可写(w),可执行(x)。剩下的格子就以每3格为一个单位表示不同用户的权限。因为Linux是多用户多任务系统,所以一个文件可能同时被许多人使用,所以我们一定要设好每个文件的权限,其文件的权限位置排列顺序是(以drwxr-xr-x为例):

  d(所属类型)-rwx(拥有者)r-x(拥有者所在的组)r-x(其他用户)

  这个例子表示的信息是:这是一个目录,并且拥有者自己可读,可写,可执行;同一组的用户可读,不可写,可执行;其它用户可读,不可写,可执行。另外,有一些程序属性的执行部分不是X,而是S,这表示执行这个程序的使用者,临时可以有和拥有者一样权力的身份来执行该程序。一般出现在系统管理之类的指令或程序,让使用者执行时,拥有root身份。

  文件的使用权限身为root管理员是可以随时更改的,具体更改方法和关于文件权限的更多内容我们会在之后讲解。

基础指令

  1、ls:显示当前目录下文件,效果类似于ll,但没有其强大。

  2、ll:显示当前目录下文件,已经做过演示,十分常用。

  3、ctrl+l:清屏,十分常用。

  4、cd 目录:目录跳转。cd的使用十分灵活我们可以使用它进入Linux的各种目录之中。举几个例子。

  进入根目录:cd /;再回到家目录cd ~;cd也可以回到家目录;cd /home/misaki/也可以回到家目录,其中misaki是我的用户名,大家改为自己的用户名即可,不难发现我们的家目录的真正目录就在/目录中的home目录中的用户名目录中。

基础指令

  5、Ctrl+Insert:复制,Shift+Insert:粘贴。在Linux中复制粘贴的快捷键与Windows的略有不同,大家要注意。

  6、touch 文件名:在当前目录下创建一个新的文件,创建后可以用ll查看。

基础指令

  7、echo 内容 > 文件名:向文件中写入内容。

基础指令

  8、cat 文件名:读取文件。

基础指令

  9、rm 文件名:删除文件。

基础指令

  10、mkdir 目录名:创建目录。

基础指令

  11、mkdir -p 目录1/目录2/目录3:创建复杂目录。

基础指令

  12、pwd:显示当前目录的绝对路径。

基础指令

  从这里也可以看出家目录的确是在跟目录下的home目录里的。

  13、rm -r 目录名:删除目录。

基础指令

  14、rm -rf 目录名:强制删除目录。

  其中-后的指令被称为指令的选项,可以为这条命令添加很多额外效果,例如-i表示执行前提问,-f表示强制等等。
  15、mv 原文件 目标文件:改名。mv 原文件 目录:移动文件。

基础指令

基础指令

  16、man:帮助手册。操作指令可在这里查看官方文档。要会使用。

基础指令

基础指令

  17、cp 原文件 目标文件:拷贝文件到此目录下并命名为目标文件。cp 原文件 目录:拷贝文件到目录下。cp -r a b:拷贝a目录到b目录下。

基础指令

  到此为止Linux的基本常用指令就全部介绍完毕了,正如我一开始说的呢样,不要去背Linux指令,在使用中理解,用的多了自然就熟悉了,在下个章节我们会介绍如何使用Xshell远程控制终端,并且如何在终端上编写程序,还请大家准备下Xshell这款软件。

【Linux】Linux基础-1

发表于 2018-12-02 | 分类于 Linux
字数统计: 1.7k

Linux基础

  在本篇教程中我会陆续开始简单介绍Linux操作系统,我们学习C语言,作为一个后端开发工程师,将来与Linux打交道是必不可少的,我们所写的代码也都会要求在Linux系统下可以运行,因此在简单学习过C语言过后,我们不得不尽快开始熟悉Linux操作系统,最终目的是为了适应Linux下编程,尤其是Linux下的网络编程,这是不得不尽快开始的一步。

第一节

Linux安装

准备材料

  1、vmare12版本。

  2、centOS-7的64位Linux镜像文件。

  本博客不提供材料的获取途径,请大家准备。

安装步骤

  1、我们首先安装vmare12,安装后打开vm会呈现以下的界面。

初始界面

  此处我已经实现装好了一个Linux虚拟机,因此在界面上有显示出来,不过这些都无关紧要,我们接下来新建一个新的Linux虚拟机。
  我们按下ctrl+n快捷键,或者在左上角文件选项中选择新建虚拟机即可弹出以下新建窗口,我们果断选择典型选项然后选择下一步快速创建即可。

安装流程1

  2、之后我们选择安装程序光盘映像文件并且在路径中选中我们之前准备好的centOS-7的64位Linux镜像文件即可。之后便可以在我们的虚拟机中安装Linux系统。这里要注意的一点是尽量保证路径全是英文字符或者数字不要出现中文,否则有可能出错。

安装流程2

  3、之后输入我们的用户名以及密码。大家务必记住这里的用户名以及密码,之后我们还会经常使用。

安装流程3

  4、在之后的页面中输入虚拟机名称及路径即可,这个大家都可以根据自身情况自定义,但是尽量给虚拟机的路径下留出足够大的空间,至少应有40G来保证Linux系统的流畅运行。

安装流程4

  5、之后我们需要将页面中的两个参数,最大磁盘大小及将虚拟磁盘存储为单个文件选项全部改为下图所示,这样将会大大的提升我们日后使用Linux时的体验。

安装流程5

  6、在此页面中我们需要根据自身电脑性能对给虚拟机分配的硬件进行优化,初始的设置并不足以使虚拟机流畅运行,因此我们选择自定义硬件。

安装流程6

  之后将虚拟机内存更改为2048。

安装流程6

  然后将虚拟机的处理器数量及每个处理器的核心数全部改为2,以上这些设置都会使日后我们使用虚拟机时更加顺畅。

安装流程6

  之后我们关闭自定义硬件页面,点击完成即可创建好一个虚拟机。

  7、点击完成后虚拟机会自动帮我们初始化Linux的安装程序,稍等片刻后,我们之后会进入以下页面。(如果有哪项还红着就点进去再点左上角DONE返回即可)

安装流程7

  之后我们所要做的则是按照下图所示顺序将圈红的这一项更改一下,以便我们之后使用Linux即可开始正式安装。

安装流程7

安装流程7

  更改完成后点击左上角DONE返回主页面再点击右下角Begin Installation即可开始安装

安装流程7

  8、在安装页面我们无需做任何更改,我们之前所输入的密码会被直接认作为root密码,我们所创建的用户会自动生成Linux用户,如果之前没有设置,可以在这里设置。之后我们只需要等进度条加载慢后重启虚拟机即可进入Linux。

安装流程8

  9、在第一次进入Linux我们要对Linux进行初始用户配置。在进入接下来页面时我们只需要点击这个出现感叹号的新选项进入选择同意协议再返回即可进行下一步。

安装流程9

安装流程9

  10、之后我们选择自己的用户,输入之前我们设置的密码并且选择语言,然后一路下一步即可正式进入Linux系统。

安装流程10

安装流程10

Linux基本介绍

  我们虽然安装好了Linux并且可以开始使用了,不过请注意我们以后使用Linux则都是使用Linux的终端而放弃了Linux的图形界面,因为在服务器传输时命令行的传输远比界面传输要高效很多,并且在一些硬件上并达不到支持使用图形界面的配置,因此我们之后则大多都是在Linux上利用终端和命令行的方式使用Linux。

什么是Linux

  Linux是一个操作系统的内核,及一个操作系统最为核心的部分,如常见的Android操作系统用的则就是Linux内核,而Linux的发新版则是以Linux为操作系统内核的一系列操作系统的合集。Linux之所以出名有很多原因,可以说现在只要牵扯服务端开发则Linux就是我们必不可少需要接触的东西。他继承了Unix的很多思想,可以说是Unix的子孙后代之一,在Unix这个强大的内核下派生出了很多后辈,他们被统称为Unix-like操作系统,与Windows操作系统不同的是这类系统崇尚去创造小而精的工具然后互相配合去完成一项复杂的任务,而Linux则是其派系中最为成功的系统之一。

Linux怎么学习

  如果说学习C语言需要一倍的时间,学习数据结构及算法需要两倍的时间,学习Linux则需要付出三倍甚至更多的时间,Linux不光需要我们平时不断的熟悉使用不断的练习,还需要我们多多了解Linux相关理论,两者结合才能掌握Linux,因此我也推荐大家多多参阅Linux相关书籍,多多了解Linux底层原理,对我们的学习都有很大帮助。我同时将Linux学习分为大致三个部分。

  1、基本操作(指令操作)

  2、编程接口

  3、网络编程

  以上三个部分无论哪个部分都十分重要,因此我们都不可掉以轻心,要理论实践相互及结合才行。从下一节开始我会开始介绍一些最为基础的Linux指令,同时讲解如何将Linux与Xshell结合起来使用。

【C语言】第七章-结构体

发表于 2018-11-27 | 分类于 C语言初阶
字数统计: 2.5k

第七章 结构体

第1节

  在程序编写中,我们难免会遇到一些复杂的数据类型无法用单一的普通数据类型表示,于是我们便可以通过使用结构体来定义属于我们自己的数据类型。所谓结构体是一些值的集合,这些值成为成员变量。结构体的每个成员可以是不同类型的变量。我们将一个个对象进行抽象,提取其中最关键的最核心的信息,加以表示,并通过结构体的方式表现出来,这样的模式,有点类似于CPP的面向对象的编程,但在CPP中对象和类的概念还要更为强大,更为广泛。

结构体的声明

  例如我们要描述一个学生,我们可以将学生的基本信息声明为一个结构体,有了学生类型的结构体我们想要定义学生类型的变量则易如反掌了。

1
2
3
4
5
struct Student
{
char name[1024];//姓名
int id;//学号
};

  通过以上的代码,我们则定义好了一个名为Student,有两个成员变量name和id的结构体,但是由于在C语言中单纯使用结构体定义变量要加上struct关键字例如struct Student stu1;,十分麻烦,因此,我们往往用typedef关键字将其进行重命名来简化使用,例如以下的代码段。

1
2
3
4
5
typedef struct _student
{
char name[1024];//姓名
int id;//学号
}Student;

  有了这样的声明,我们则可以更简单的定义结构体变量,例如Student stu1;。

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

定义

  结构体变量的定义主要分为两种方法,一种是在声明时进行定义,另一种是在使用时进行定义。我们先来学习如何在声明时进行定义。

1
2
3
4
5
struct student
{
char name[1024];//姓名
int id;//学号
}stu1, stu2;

  使用以上的方法我们则在声明结构体的同时定义了两个个名为stu1和stu2类型为Student的变量。但是要奇迹这种方法如果在使用typedef关键字的情况下无法使用。

  接下来我们在使用时再进行变量的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

typedef struct student
{
char name[1024];//姓名
int id;//学号
}Student;
int main()//主函数,函数入口
{
Student stu1, stu2;
}

  可以看出使用结构体定义变量的方法和使用普通数据类型定义变量的方法别无二致。在此我依然提倡大家使用typedef关键字对结构体进行重命名,这样使得我们使用起来更为方便,不过这种习惯在CPP中就可以摒弃了,因为在CPP中哪怕不使用 typedef关键字,结构体类型定义变量时也不需要加struct关键字。

初始化

  在定义完结构体变量后我们就可以对结构体变量进行初始化,同样的,分为在声明时进行初始化和在使用时进行初始化。我们先学习第一种情况。

1
2
3
4
5
struct student
{
char name[1024];//姓名
int id;//学号
}stu1 = { "Misaki", 111 }, stu2 = {"张三", 112};

  这种初始的方式是和在声明时定义变量的方式一起连用的,达到可以快速定义变量并初始化的效果。这种初始化的语法与数组初始化的语法十分相似,我们都要按照结构体内成员变量的顺序依次初始化,

  接下来是在使用时定义变量的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

typedef struct student
{
char name[1024];//姓名
int id;//学号
}Student;
int main()//主函数,函数入口
{
Student stu1 = { "Misaki", 111 };
Student stu2 = { "张三", 112 };
system("pause");
}

  结构体初始化的方式大同小异,都与数组的初始化十分相似,大家在实际使用中,视情况进行使用。

结构体成员的访问

结构体变量访问成员

  在结构体的使用过程中,最常使用的就是访问一个结构体变量的成员变量,不过好在C语言中结构体成员的访问十分的方便和快捷。我们只需要使用一个我们之前提到过的访问结构体成员变量操作符.即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

typedef struct student
{
char name[1024];//姓名
int id;//学号
}Student;
int main()//主函数,函数入口
{
Student stu1 = { "Misaki", 111 };
Student stu2 = { "张三", 112 };
printf("stu1的姓名为:%s,学号为:%d\n", stu1.name, stu1.id);
printf("stu2的姓名为:%s,学号为:%d\n", stu2.name, stu2.id);
system("pause");
}

  结构体的成员变量也不过是一个普通数据类型的变量,使用起来与普通数据类型别无二致,前提是我们要将其从结构体变量中访问到。从代码中大家可以看出我使用结构体变量.成员变量名的方式即可轻易访问到一个结构体变量的成员变量。

结构体指针访问成员变量

  结构体指针访问结构体成员变量的方法与结构体变量访问成员变量的方法类似,只不过我们要将其中的.换为->。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

typedef struct student
{
char name[1024];//姓名
int id;//学号
}Student;
int main()//主函数,函数入口
{
Student stu1 = { "Misaki", 111 };
Student* stu2 = &stu1;
printf("stu1的姓名为:%s,学号为:%d\n", stu2->name, stu2->id);
system("pause");
}

结构体嵌套

  在结构体使用的过程中,我们声明一个结构体的成员变量为另外一个结构体类型,这样来达到多重嵌套表达复杂数据类型的效果。以下我将展示一个结构体多重嵌套的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
#include <string.h>
typedef struct student
{
char name[50];//姓名
int id;//学号
}Student;
typedef struct area
{
Student students[10];
}Area;
typedef struct city
{
Area areas[10];
}City;
typedef struct province
{
City cities[10];
}Province;
typedef struct country
{
Province provinces[10];
}Country;
int main()//主函数,函数入口
{
Country country;
Country* pCountry = &country;
country.provinces[0].cities[0].areas[0].students[0].id = 111;
strcpy(pCountry->provinces[0].cities[0].areas[0].students[0].name, "Misaki");
printf("学生的学号为:%d,姓名为:%s\n", country.provinces[0].cities[0].areas[0].students[0].id,country.provinces[0].cities[0].areas[0].students[0].name);
system("pause");
}

  大家在使用结构体嵌套来表示数据类型时务必要注意开辟合适的内寸空间,在上面的例子中我们每一个嵌套中的结构体都有着10倍的Student类型的变量的大小,综合起来已经开辟了十分多的内寸空间,如果我们此时将Student中的大小改为1024大家就会发现程序已经闪退了,这就是我们过度占用内存系统为了保护自身所作出的反应机制。

结构体传参

  结构体变量的参数传递与普通变量几乎完全一样,因此我们依旧分为两种方式进行讲解。

形参传递

  与普通参数传递一样,这种只传入形参的方法不会对原本结构体变量的内容进行改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
#include <string.h>
typedef struct student
{
char name[50];//姓名
int id;//学号
}Student;
void Func(Student stu)
{
stu.id = 110;
printf("在函数内部学生的学号为:%d\n", stu.id);
}
int main()//主函数,函数入口
{
Student stu = { "Misaki", 111 };
Func(stu);
printf("在函数外部学生的学号为:%d\n", stu.id);
system("pause");
}

指针传递

  传入结构体指针来进行结构体变量的传递使得结构体在过大的情况下更为方便,省去了编译器将变量重新复制一份的时间,更节省空间和时间,是我最为推荐的传递方式。即使我们并不想函数随意更改我们的结构体变量,我们也只需要传递指向常量的结构体指针即可,这样的指针如何定义我在指针章节已经详细讲过。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
#include <string.h>
typedef struct student
{
char name[50];//姓名
int id;//学号
}Student;
void Func(Student* stu)//如果需要给一个指向常量的指针则需要改为 const Student* stu;
{
stu->id = 110;
printf("在函数内部学生的学号为:%d\n", stu->id);
}
int main()//主函数,函数入口
{
Student stu = { "Misaki", 111 };
Func(&stu);
printf("在函数外部学生的学号为:%d\n", stu.id);
system("pause");
}

  以上则为结构体章节的全部内容,结构体在大型程序的编写中十分重要,由于C语言中没有类和对象的概念,因此我们只能依靠力量单薄的结构体来进行大型程序中数据类型的管理来达到类似于面向对象编程的效果,不过这样远远不够,因此想要更好的理解“抽象”“泛型”“多态”则需要进行对CPP的学习。
  到此C语言初阶部分章节全部结束,本教程目前仅仅教授了C语言最为基本的语法部分,C语言更为深层的使用我们在C语言进阶进行详细讲解。

【C语言】第六章-指针

发表于 2018-11-26 | 分类于 C语言初阶
字数统计: 2.3k

第六章 指针

第1节

什么是指针

  在计算机科学中,指针时编程语言中的一个对象,利用地址,它的值直接指向存在在电脑存储器当中的另一个地方的值。简而言之,指针是一个存储变量地址的变量。在32位系统上,地址是32个0或1组成的序列,所以要求用4个字节来存储,所以指针在32位机器上是4个字节的大小。 因此推而广之,在64位机器上,指针是8个字节大小的。

指针和指针类型

指针的基本使用

  我们可以使用int* num;的语句来定义一个指针类型的变量,并直接在其后进行赋值,但是要注意我们要赋给指针变量的一定是一个变量的地址,此时我们就需要用到&取地址符。

  而如果我们我们想要取到指针中的值时则需要用到*解引用操作符。以下的例子展示了指针的基本使用。

1
2
3
4
5
6
7
8
9
10
11
12
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

int main()//主函数,函数入口
{
int num = 10;
int*p = &num;//创建指针变量并让其指向num变量
printf("%d\n", p);//打印指针p所指向的内存地址
printf("%d\n", *p);//打印p所指向变量的值
system("pause");
}

  但是我们不能将非法的内存地址赋给指针变量。那么什么是非法的内存地址呢?所谓非法的内存地址即我们并未向系统申请过的内存地址。我们每定义一个变量系统都会分给我们一块内存空间以便我们存放数据,而如果我们对指针直接进行赋值,那么我们就无法保证赋给指针的变量是合法的,此时如果再访问指针则是未定义行为(在C标准中并未明确说明的操作行为),这是十分危险的。比如说我们使用这样的语句int* p = 0x100;则是十分不安全的写法。因此对于指针我们不要对其进行乱赋值,防止其指向非法的内存

  为了防止防止指针指向不合法的内存,对于我们定义了但暂时不用的指针变量,出于安全起见,我们可以将其暂时赋值为NULL。如int* p = NULL。

指针常见注意事项

  1、不可将字面值常量的地址赋给指针。如int* p = &10这样的写法就是错的。

  2、不可给指针直接复制内存地址,以免造成访问非法内存的错误。

  3、对于暂时不使用的指针将其值赋为NULL防止造成野指针,发生意想不到的麻烦。

  4、在32位机器下指针的大小为32位,4个字节,指针具体大小视系统而变。

指针和数组名

  之前数组讲解时有提到过数组名就是元素的首地址,那么我们是否可以理解为数组名就是一个指向首元素的指针呢?其实这么理解实在欠妥,虽然数组和指针十分相似,有着千丝万缕的联系,但数组可千万不能和指针一概而论。
  在C语言中,数组和指针可以进行转换,这为我们很多操作行了方便,但数组和指针永远属于两个不同的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

int main()//主函数,函数入口
{
int arr[] = { 1,2,3,4 };
int* p = arr;
printf("%d\n",sizeof(arr));//数组的大小
printf("%d\n", sizeof(arr + 0));//数组隐式转换为指针,参与运算
printf("%d\n", sizeof(p));//一个和数组指向相同元素的指针
system("pause");
}

  在这个例子中我们很明显就能看出指针和数组的区别,当我们取数组的大小时,会进行计算得出数组所有元素大小之和,而我们取指针的大小时,永远都只会是4,尤其是我们在打印arr + 0的大小的时候由于数组名不能参与运算,于是数组名隐式转换为指针后才参与运算,从结果是4也能看得出来此时的arr + 0已经是一个指针了,哪怕它所指向的内存地址并没有发生改变。

  初次之外我们可以利用数组名和指针可以相互转换的特点和指针可以参与运算的特点利用指针来访问数组中的元素。

1
2
3
4
5
6
7
8
9
10
11
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

int main()//主函数,函数入口
{
int arr[] = { 1,2,3,4 };
printf("%d\n", *(arr + 1));
printf("%d\n", arr[1]);
system("pause");
}

  在这个例子中我们可以看到两种取数组中元素的方式都可得到数组中的第二个元素,因此我们不难得出结论*(arr + 1) <=> arr[1],即这两种方式是等价的。因此我们很多情况下可以利用指针灵活使用数组。

1
2
3
4
5
6
7
8
9
10
11
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

int main()//主函数,函数入口
{
int arr[] = { 1,2,3,4 };
int* p = arr + 1;
printf("%d\n", p[-1]);//等价于*(p - 1);
system("pause");
}

  尤其是对于一个指针,下标的方式依然使用,并且不同于数组,在指针的下表中甚至可以使用负数作为下标。

指针运算

指针 +- 整数

  指针加减一个整数等同于跳过了几个元素,而绝对不是内存地址加减整数,这点我们之前的例子中已经深有体会了,这里不再详细说明了。

指针 - 指针

  指针虽然不能相加,但是可以相减,指针相减计算的是两个指针间所偏移的元素个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

int main()//主函数,函数入口
{
int arr[] = { 1,2,3,4 };
int* p1 = arr + 1;
int* p2 = arr + 2;
printf("%d\n", p2 - p1);//得出p2与p1间相偏移了一个元素
printf("%d\n", p1 - p2);//向左偏移则为负数
system("pause");
}

指针的关系运算

  指针间也可也进行普通的关系比较,比如指针相等,指针大小等等,其中指针大小比较时,指针相比于另一个指针向右便宜则为大,向左则为小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

int main()//主函数,函数入口
{
int arr[] = { 1,2,3,4 };
int* p1 = arr + 1;
int* p2 = arr + 1;
int* p3 = arr + 2;
printf("%d\n", p1 == p2);//得出p2与p1间相偏移了一个元素
printf("%d\n", p3 > p2 ? 1 : -1);//向左偏移则为负数
system("pause");
}

  不过在此值得说明的是:只有在连续的一段内存上指针的相减以及指针的关系运算才有意义,否则都是无意义的。

指针微进阶

  C语言指针博大精深,有着很多的门路,在此我们挑其中最好理解的两点进行讲解。

数组指针

1
2
3
4
5
6
7
8
9
10
11
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

int main()//主函数,函数入口
{
int arr[] = { 1,2,3,4 };
printf("%p\n", arr);
printf("%p\n", &arr + 1);
system("pause");
}

  从运行结果大家不难发现,&arr + 1别arr的结果要多了16个字节,也就正好是整个数组的长度。也就是说&arr我们取到了整个数组的指针,在我们对这个指针进行+1时,指针跳过了整个数组,于是我们称指向整个数组的指针为数组指针。

常量指针和指向常量的指针

  常量我相信大家都不陌生,也就是在初始化后无法再进行改变的量,但是在指针中常量也分为常量指针和指向常量的指针两种。

  指向常量的指针:对指针所指向的变量无法进行更改的指针。

         const int* p = &num

         int const* p = &num(等价)

  这样的指针在我们不想让指针修改一个值却又方便传入的时候经常使用。

  常量指针:指针一旦指向某个内存地址无法再更改其指向。
         int* const p = &num
  这样的指针与数组名十分类似,自身的值都无法进行改变,但是在强调一遍数组和指针是两种数据类型,不可搞混。

  剩下指针更为高阶的知识和用法将在C语言进阶篇进行讲解。

【C语言】项目-扫雷

发表于 2018-11-19 | 分类于 项目
字数统计: 1.4k

C语言项目

扫雷

实现思想

  扫雷的实现是使用两个数组,一个数组是用来显示的地图数组,而另一个数组是雷阵,每当玩家输入坐标后会根据雷阵判断该位置是否有雷,如果没有会计算出该位置周围地雷的数量并返回打印出来。不过值得一提的是,地图的初始化要多设计出一个边框,这个边框不需要打印,但是在计算周围地雷数量的时候却会格外方便。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
#define _CRT_SECURE_NO_WARNINGS
#define MAX_ROW 10
#define MAX_COL 10
#define MINE_NUM 10
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
/*!
* \项目名 扫雷
* \日期 2018/11/19 23:19
*
* \作者 Misaki
* 联系方式 1761607418@qq.com
*
* 文件描述: 10 * 10扫雷游戏初步实现
*
*/
/*
菜单
返回:用户输入
*/
int Menu()
{
system("cls");
printf("*********************************\n");
printf("1、开始游戏!\n");
printf("2、游戏说明!\n");
printf("0、退出游戏!\n");
printf("*********************************\n");
printf("请选择:");
int choose = 0;
rewind(stdin);
if(scanf("%d", &choose) != 1) return 3;
return choose;
}
/*
初始化地图
参数:两张地图
*/
void InitMap (char show_map[MAX_ROW + 2][MAX_COL + 2], char mine_map[MAX_ROW + 2][MAX_COL + 2])
{
//现将两个地图全部初始化
for (int i = 0; i < MAX_ROW + 2; i++)
{
for (int j = 0; j < MAX_COL + 2; j++)
{
show_map[i][j] = '*';
mine_map[i][j] = '0';
}
}
//设置随机雷阵
for (int i = 0; i < MINE_NUM; )
{
int it = rand() % 10 + 1;
int jt = rand() % 10 + 1;
if (mine_map[it][jt] != '1')
{
mine_map[it][jt] = '1';
i++;
}
}
}
/*
打印地图
参数:地图
*/
void PrintMap(char map[MAX_ROW + 2][MAX_COL + 2])
{
system("cls");
printf(" ");
for (int i = 1; i < MAX_ROW + 1; i++)
{
printf("%d ", i);
}
printf("\n");
printf(" ");
for (int i = 1; i < MAX_ROW + 1; i++)
{
printf("___", i);
}
printf("\n");
for (int i = 1; i < MAX_ROW + 1; i++)
{
printf("%02d|", i);
for (int j = 1; j < MAX_COL + 1; j++)
{
printf("%c ", map[i][j]);
}
printf("\n");
}
printf("\n");
}
/*
判断周围有几个雷
参数:地雷地图,用户选择的坐标
返回:雷的个数
*/
int MineNum(char mine_map[MAX_ROW + 2][MAX_COL + 2], int row, int col)
{
int count = (mine_map[row][col - 1] - '0') + (mine_map[row - 1][col - 1] - '0') + (mine_map[row - 1][col] - '0')
+ (mine_map[row - 1][col + 1] - '0') + (mine_map[row][col + 1] - '0') + (mine_map[row + 1][col + 1] - '0')
+ (mine_map[row + 1][col] - '0') + (mine_map[row + 1][col - 1] - '0');
return count;
}
/*
更新游戏显示地图
参数:两张地图和用户选择的坐标
*/
void UpdateMap(char show_map[MAX_ROW + 2][MAX_COL + 2], char mine_map[MAX_ROW + 2][MAX_COL + 2], int row, int col)
{
int count = MineNum(mine_map, row, col);
show_map[row][col] = '0' + count;
if (count == 0)
{
if (row >= 1 && col - 1 >= 1)
{
int count1 = MineNum(mine_map, row, col - 1);
show_map[row][col - 1] = '0' + count1;
}
if (row - 1 >= 1 && col - 1 >= 1)
{
int count2 = MineNum(mine_map, row - 1, col - 1);
show_map[row - 1][col - 1] = '0' + count2;
}
if (row - 1 >= 1 && col >= 1)
{
int count3 = MineNum(mine_map, row - 1, col);
show_map[row - 1][col] = '0' + count3;
}
if (row - 1 >= 1 && col + 1 <= 10)
{
int count4 = MineNum(mine_map, row - 1, col + 1);
show_map[row - 1][col + 1] = '0' + count4;
}
if (row >= 1 && col + 1 <= 10)
{
int count5 = MineNum(mine_map, row, col + 1);
show_map[row][col + 1] = '0' + count5;
}
if (row + 1 <= 10 && col + 1 <= 10)
{
int count6 = MineNum(mine_map, row + 1, col + 1);
show_map[row + 1][col + 1] = '0' + count6;
}
if (row + 1 <= 10 && col >= 1)
{
int count7 = MineNum(mine_map, row + 1, col);
show_map[row + 1][col] = '0' + count7;
}
if (row + 1 <= 10 && col - 1 >= 1)
{
int count8 = MineNum(mine_map, row + 1, col - 1);
show_map[row + 1][col - 1] = '0' + count8;
}
}
}
int LostMineNum(char show_map[MAX_ROW + 2][MAX_COL + 2])
{
int count = 0;
for (int i = 1; i < MAX_ROW + 1; i++)
{
for (int j = 1; j < MAX_COL + 1; j++)
{
if (show_map[i][j] == '*')
{
count++;
}
}
}
return count;
}
/*游戏主体*/
void Game()
{
//将地图加个边框,目的为了后续方便设计计算雷数算法
char show_map[MAX_ROW + 2][MAX_COL + 2];//显示地图
char mine_map[MAX_ROW + 2][MAX_COL + 2];//地雷地图
//初始化地图
InitMap(show_map, mine_map);
PrintMap(mine_map);
while (1)
{
//打印地图
PrintMap(show_map);
printf("当前未扫区域 / 总雷数:%d / %d\n", LostMineNum(show_map), MINE_NUM);
//用户输入(判断是否合法)
int row = 0, col = 0;
while (1)
{
printf("请输入坐标(x y):");
rewind(stdin);
int result = scanf("%d%d", &row, &col);
if (row < 1 || row > 10 || col < 1 || col > 10 || result != 2)
{
printf("看好再输!\n");
}
else
{
break;
}
}
//判断该处是否有雷,有雷直接GG跳出循环
if (mine_map[row][col] == '1')
{
PrintMap(mine_map);
printf("pa!你死了\n");
break;
}
//没扫完继续,计算该处附近有及个雷,填入显示地图中如果没有雷,继续计算周边一圈全部显示到地图中
UpdateMap(show_map, mine_map, row, col);
//没雷判断是否是最后一个,全部扫完游戏结束跳出循环
if (LostMineNum(show_map) <= 10)
{
PrintMap(mine_map);
printf("你赢啦,牛逼!\n");
break;
}
}
//游戏结束打印地雷地图
system("pause");
}
void GameExplain()
{
system("cls");
printf("扫雷都不玩你有没有童年!\n");
system("pause");
}
int main()
{
srand(time(NULL));
int choose = 0;
while (choose = Menu())
{
if (choose == 1)
{
//开始游戏
Game();
continue;
}
if (choose == 2)
{
//游戏说明
GameExplain();
continue;
}
else
{
printf("别瞎b输!\n");
system("pause");
continue;
}
}
system("pause");
return 0;
}

  扫雷的实现十分简单,但是在这个版本中我并没有做出当周围地雷数为0时会一直清理直到周围全部有地雷为止的算法,这个会在后续进行实现。

【C语言】第五章-操作符

发表于 2018-11-19 | 分类于 C语言初阶
字数统计: 2.3k

第五章 操作符

  操作符在C语言中的使用是十分经典且典型的,因为在C语言的引导下,后世很多编程语言也都和C语言有着极其相似的操作符使用方法,因此学好这一章也会为日后学习更多的编程语言打下基础。

第1节

操作符

算术操作符

            + - * / %

  这是C语言中最为常用的几个操作符,使用也十分简单,除了%操作符外,其它操作符可以用于整数和浮点数。%操作符,的两个操作数必须是整数,返回的是整除后的余数。

移位操作数

  左移操作数:<<

  左移规则:将数字的在内存中的二进制表示左移一位,右边补0, 等同于将变量中的数本身乘以2并且值重新赋给变量。

1
2
3
4
5
6
7
8
9
10
11
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int main()//主函数,函数入口
{
int num = 10;
//未移位前:00000000000000000000000000001010
printf("%d\n", num << 1);//打印20
//左移一位后:00000000000000000000000000010100
system("pause");
}

  右移操作数:>>

  右移规则:右移规则分为两种。

  1、逻辑移位:左边用0填充,右边丢弃

  2、算数移位:左边用符号位填充,右边丢弃。

1
2
3
4
5
6
7
8
9
10
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int main()//主函数,函数入口
{
int num = -1;
//右移位前:11111111111111111111111111111111
printf("%d\n", num >> 1);//打印-1 //右移移位后:11111111111111111111111111111111
system("pause");
}

  从结果可见,我们右移后在左边用了符号位1来填充,可见编译器默认使用了算数移位。
  对移位运算符来说,有一点是十分致命的,就是永远不要移动负数位,这个标准是未定义的,会发生无法意想的错误。

位操作符

  位操作符主要针对的是两个数的二进制表示中各位上的数字进行运算。

  按位与:&,都为1则为1,否则为0。

  按位或:|,一个为1就为1,否则为0。

  按位异或:^,不同则为1,相同为0。

  按位取反:~,1变0,0变1。

  位操作符两边的操作数都必须是整数。 通过位操作符,我们可以实现一些针对于整数的二进制表示的算法,例如我们可以实现计算一个整数二进制表示中有多少个1。
  方法1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int BitOneCount(int num)
{
int count = 0;
while (num != 0)
{
if (num % 2 == 1)
{
count++;
}
num /= 2;
}
return count;
}
int main()//主函数,函数入口
{
printf("%d\n", BitOneCount(3));//打印2
system("pause");
}

  这一种方法是较为简单的算法,写起来也十分直观,但是有一个致命的缺点就是它对负数的计算就并不是那么准确了,因此我们通过改进得出了一下这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int BitOneCount(int num)
{
int count = 0;
for (int i = 0; i < 32; i++)//一个整形最多有32位,因此我们需要从第一位开始进行32个循环逐步计数
{
if (num >> i & 1 != 0)//右移验证最低位是否是1,是1则计数器+1
{
count++;
}
}
return count;
}
int main()//主函数,函数入口
{
printf("%d\n", BitOneCount(-1));//打印32
system("pause");
}

  以上的这种算法利用位运算符直接对二进制数进行计算,即适用于正数,也适用于负数,是十分推荐使用的算法。

赋值操作符

  赋值预算符:=,我们之前已经多次使用了,这里不在介绍了,不过基于普通的赋值运算符,C语言中给我们提供了更多的方便我们使用的复合赋值符。这里举其中一两个例子。

        a += 1 => a = a + 1;

        a -= 1 => a = a - 1;

  以此类推,有很多的运算符都可与=号结合方便我们的使用。

单目操作符

  单目操作符是指只有一个操作数的操作符。

  逻辑反操作:!,这个操作符可以将语句的逻辑取反,在C语言中,0是表示假,非0表示真,因此在使用这个操作符时,可以将逻辑取到与之相反的一方。

  负值/正值:-/+,与加减法不同,负值个操作符可以将数值编程他的相反数。

  取地址:&,取地址操作符可以取到一个变量在内存中存储的地址。

  sizeof:sizeof(),可以取到变量在内存中所占的空间大小,单位是字节。

  自增/自减操作符:++/--,自增自减操作符写在操作数的前或后有着不同的效果,在日常使用中机器容易出错,因此在很多情况下不推荐使用。

  解引用操作符:*,这个操作符可以通过指针找到指针所指变量中的数据,并将其返回,在指针使用中非常常见。

  强制类型转换操作符:(类型),这个操作符可以将C语言中的类型进行强制转换,比如说double a = (double)1;,有非常大的使用空间。

关系操作符

  关系操作符主要常用于判断,在C语言中有以下关系操作符。

        > >= < <= == !=

逻辑操作符

  逻辑操作符多用于同时需要多个条件或多个条件只要一个满足即可的情况。

  逻辑与:&&,逻辑或:||。

 1 && 1 == 1,1 && 0 == 0,1 || 0 == 1,0 || 0 == 0。

条件操作符(三目操作符)

  这是C语言中唯一的一个三目操作符,具体使用如下。

  exp 1 ? exp2 : exp3:如果exp1为真则执行exp2,否则执行exp3。有时使用三目运算符比使用if语句要更加方便。

逗号表达式

  exp1, exp2, exp3...expn:如果一个表达式中出现了逗号,则表达式从左向右一次执行,但整个表达式的结果是最后一个表达式的结果。

下标引用,函数调用和结构成员

  下标引用:数组名[],下标引用在数组中经常使用。

  函数调用:函数名(),在函数调用时会经常使用。

  结构体成员:./->,前者在结构体变量中可以取到结构的成员,后者在结构体指针中可以取到结构体成员,这两个操作符在结构体操作中都十分常用。

表达式求值

  

隐式转换

  在C语言中整形算数运算总是至少以缺省整形的精度进行运算的,为了获得这个精度,表达式中的字符和短整型的操作数在使用前都会先隐式转换为整形才会进行运算,这个也叫做整形提升。

  那么是如何进行整形提升的呢?

  当char中的值为有符号的负数时,char a = -1原本的char只有8个符号位为11111111,在提升后会在高位补符号位变为11111111111111111111111111111111。而如果对于没有符号位的char a = 1,原本的char为00000001,提升后会在高位补符号位变为00000000000000000000000000000001。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>

int main()//主函数,函数入口
{
unsigned char a = 255;
printf("%d\n", (a << 1) >> 1);
//计算前先整形提升为00 00 00 ff
//左移后变为00 00 01 fe
//右移后又变为00 00 00 ff
//正因为整形提升的存在才不会造成溢出
system("pause");
}

算术转换

  在表达式的计算中,如果某个操作符量的操作数为不同类型,那么其中一个操作数就会转换为另外一个操作数的类型来方便运算,并且转换总是从低级向高级进行转换。

int -> unsigned int -> long int -> unsigned long int -> float -> double -> long double

  int以下的类型都会自动先转换为int,之前的隐式类型转换已经说过了。

  不过算数转换也要合理不然会造成数据丢失。

      float f = 3.14; int num = f

一些常见问题

  1、操作符的求值有三个影响的因素:(1)操作符的优先级;(2)操作符的结合性;(3)是否控制求值顺序。
  2、有一些表达式的运算在标准中未进行定义,因此在不同环境下的运行结果不尽相同,需要多多注意。

【C语言】第四章-数组

发表于 2018-11-18 | 分类于 C语言初阶
字数统计: 1.5k

第四章 数组

  数组能帮助我们批量定义一些变量,省去我们在需要大量使用变量需要一个个定义变量的麻烦。而且利用数组所定义的变量又一个最大的特点就是这些变量在内存空间上都是连续的。这一章我们将详细介绍数组及其应用。

第1节

一维数组

数组的创建及初始化

  数组是一组同类型元素的集合。数组的定义方式:

        type_t arr_name[const_n]

  数组的创建及初始化示例。

1
2
3
4
5
6
7
8
9
10
11
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int main()//主函数,函数入口
{
int arr1[10];//创建一个10个元素大小的整形数组
int arr2[10] = { 1,2,3 };//创建一个10个元素大小的整形数组,并将前3个元素赋值,未赋值元素默认为0
int arr3[] = { 1,2,3 };//创建一个3个元素大小的整形数组,并且赋值
char arr4[] = "abcdef";//创建一个字符数组用来存储字符串"abcdef"
system("pause");
}

  在数组创建后我依旧建议立刻进行赋值,就像变量创建后也应该立刻进行赋值一样。如果数组不立刻使用我们应该将他们全部初始化为0。我们可以这么写。

          int arr[10] = { 0 };

一维数组的使用

  我们可以通过数组名[下标]的方式来取到数组中的某一个元素,不过值得说明的是,数组中的下标是从0开始的,也就是说,下标的最大值 == 数组元素总个数 - 1。下面是数组使用的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int main()//主函数,函数入口
{
int arr[10] = { 0 };//数组的创建及初始化
//计算数组元素的个数
int sz = sizeof(arr) / sizeof(arr[0]);
//让数组中每个元素的值等于在数组中的下标
for (int i = 0; i < sz; i++)
{
arr[i] = i;
}
//打印数组的内容
for (int i = 0; i < sz; i++)
{
printf("%d\t", arr[i]);
}
printf("\n");
system("pause");
}

一维数组在内存中的存储

  数组在内存中的存储是连续的, 不间断的,也正因此我们才可以通过下标来逐个访问到数组的每一个元素。同时数组名就是数组元素的首地址。

  数组在内存中的存储时连续的我们可以通过以下这个程序来进行验证。

1
2
3
4
5
6
7
8
9
10
11
12
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int main()//主函数,函数入口
{
int arr[10] = { 0 };
for (int i = 0; i < sizeof(arr) / sizeof(arr[0]); i++)
{
printf("&arr[%d] = %p\n", i, &arr[i]);
}
system("pause");
}

  从程序执行结果大家就可以看出,整形数组中每个元素的地址之间刚好间隔4个字节,也就是一个整形的空间大小,因此可以得出结论,数组中元素在内存中都是连续排列的。

二维数组

二维数组的定义及初始化

  我们可以使用以下的语法进行对一个二维数组的定义:

          int arr[3][4];

  在这个例子中我定义了一个三行四列的二维数组。在定义之后我们即可对它进行初始化,而二维数组的初始化和一维数组极为相似。

  二维数组定义及初始化示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int main()//主函数,函数入口
{
int arr1[3][4] = {
{1, 2, 3, 4},
{2, 2, 3, 4},
{3, 2, 3, 4},
};
//定义的数组是这样的
// 1 2 3 4
// 2 2 3 4
// 3 2 3 4
int arr[3][4] = { 1, 2, 3, 4 };
//定义的数组是这样的
// 1 2 3 4
// 0 0 0 0
// 0 0 0 0
int arr[3][4] = {
{1, 2},
{3, 4},
};
//定义的数组是这样的
// 1 2 0 0
// 3 4 0 0
// 0 0 0 0
int arr[][4] = {
{2, 3},
{4, 5},
};
//定义的数组是这样的
// 2 3 0 0
// 4 5 0 0
system("pause");
}

  这这段示例中可以看出二维数组与一维数组定义十分相似,但是不同的是二维数组的列下标在定义时不能省略,而行下标是可以省略的。

二维数组的使用

  二维数组的使用也和一维数组类似,不过我们这时候要是想要取得元素中的某个下标我们需要用两个下标来进行表示,两个下标也都是从0开始计数的。因此我们要想遍历一个二维数组至少需要使用两重循环。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int main()//主函数,函数入口
{
int arr[3][4] = { 0 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
arr[i][j] = 4 * i + j;
}
}
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{
printf("%d\t", arr[i][j]);
}
printf("\n");
}
system("pause");
}

二维数组的存储

  二维数组与一维数组一样在内存中存储元素都是相连着的,在这里我们不再细致讨论。

数组作为函数参数

  数组传入函数时,会将数组隐式转换为指针。因此在数组传入函数时并不会记录原数组的长度大小等信息,需要我们传值进去。

1
2
3
4
5
//一定要设置一个参数用来传入函数的大小
void bubble_sort(int arr[], int sz)
{
......
}

【C语言】项目-三子棋

发表于 2018-11-17 | 分类于 项目
字数统计: 1.7k

C语言项目

三子棋

实现思想

  大家都玩过五子棋,也对五子棋的游戏规则不陌生,今天我们就仿照五子棋做一个三子棋,以函数模块为进行封装逐步实现,并且实现一个万能算法,可以达到n*n的棋盘上玩n子棋(前提是屏幕够大)。

  实现具体思想很简单,先做出菜单,然后根据玩家选择执行不同的内容函数,如果选择游戏则开始三子棋的游戏主体函数,执行完成之后重新执行菜单,知道玩家选择退出游戏!

  三子棋的主体游戏思想也很简单,用一个数组表示棋盘,每一个回合开始前先打印棋盘,随后让玩家进行坐标选择要落子的地方,此时注意 要判断玩家输入是否合法,玩家输入后将棋盘上的数据进行更改,并判断是否已经分出胜负,未结束的话是由电脑随机选择坐标进行落子(在这里我们暂时先进行简单的设计,让电脑随机落子),电脑落子完成后也要判断是否游戏已经结束,未结束的话重新打印棋盘即可完成一个循环。在游戏中我们应该考虑到游戏有三中胜负情况,分别是:玩家胜、电脑胜和平局。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
/*!
* \文件名 源.cpp
* \日期 2018/11/10 13:35
*
* \作者 Misaki
* 联系方式 1761607418@qq.com
*
* 文件描述: 三子棋
*
*/
#define _CRT_SECURE_NO_WARNINGS
#define ROW 5//棋盘行
#define COL 5//棋盘列
#define WIN 5//获胜条件
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <time.h>
char g_board[ROW][COL];
/*
将棋盘进行初始化
*/
void initBoard()
{
for (int row = 0; row < ROW; row++)
{
for (int col = 0; col < COL; col++)
{
g_board[row][col] = ' ';//空位置用' '表示
}
}
}
/*
打印菜单
*/
int menu()
{
system("cls");
int choose = 0;
printf("1、开始游戏\n");
printf("2、游戏说明\n");
printf("3、退出\n");
printf("请选择:");
scanf("%d", &choose);
return choose;
}
/*
打印棋盘
*/
void printBoard()
{
system("cls");
for (int col = 0; col < COL; col++)
{
printf(" _____");
}
printf(" \n");
for (int row = 0; row < ROW; row++)
{
printf("|");
for (int col = 0; col < COL; col++)
{
printf("%6c", '|');
}
printf("\n|");
for (int col = 0; col < COL; col++)
{
printf(" %c |", g_board[row][col]);
}
printf("\n|");
for (int col = 0; col < COL; col++)
{
printf("_____|");
}
printf("\n");
}
printf("\n");
}
/*
玩家回合
*/
void playerMove(int* row, int* col)
{
printf("玩家回合:\n");
int rowt = 0, colt = 0, result;
while (1)
{
printf("请输入坐标:(格式:y x)");
if (result = scanf("%d%d", &rowt, &colt) != 2)
{
printf("你输的是坐标么?重来\n");
rewind(stdin);
continue;
}
if (rowt < 0 || rowt >= ROW || colt < 0 || colt >= COL)
{
printf("输入坐标不合法,请重新输入!\n");
rewind(stdin);
continue;
}
if (g_board[rowt][colt] != ' ')
{
printf("这个位置已经有子了!\n");
rewind(stdin);
continue;
}
g_board[rowt][colt] = 'x';
//将值返回出去
*row = rowt;
*col = colt;
break;
}
}
void computerMove(int* row, int* col)
{
int rowt = 0;
int colt = 0;
while (1)
{
rowt = rand() % (ROW);
colt = rand() % (COL);
if (g_board[rowt][colt] == ' ')
{
break;
}
}
g_board[rowt][colt] = 'o';
//将值返回出去
*row = rowt;
*col = colt;
}
/*
判断是否平局
*/
int isdraw()
{
int num = 0;
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
if (g_board[i][j] == ' ')
{
num++;
}
}
}
if (num == 0)//平局了,棋盘满了
{
return 1;
}
else//还能打
{
return 0;
}
}
/*
判断是否三子相连
参数:当前下子位置
返回值:谁获得胜利
*/
char judge(int row, int col)
{
int num =1;//计数相连棋子个数
int i = 1;//判断周围第i个棋子是否相同
//从左到右的判断
for (i = 1; i < WIN; i++)
{
if ((col + i) > (COL - 1) || g_board[row][col] != g_board[row][col + i])
{
break;
}
num++;
}
//从右到左的判断
for (i = 1; i < WIN; i++)
{
if ((col - i) < 0 || g_board[row][col] != g_board[row][col - i])
{
break;
}
num++;
}
if (num >= WIN)
{
return g_board[row][col];
}
num = 1;
//从上到下的判断
for (i = 1; i < WIN; i++)
{
if ((row + i) > (ROW - 1) || g_board[row][col] != g_board[row + i][col])
{
break;
}
num++;
}
//从下到上的判断
for (i = 1; i < WIN; i++)
{
if ((row - i) < 0 || g_board[row][col] != g_board[row - i][col])
{
break;
}
num++;
}
if (num >= WIN)
{
return g_board[row][col];
}
num = 1;
//从左上到右下的判断
for (i = 1; i < WIN; i++)
{
if ((row + i) > (ROW - 1) || (col + i) > (COL - 1) || g_board[row][col] != g_board[row + i][col + i])
{
break;
}
num++;
}
//从右下到左上的判断
for (i = 1; i < WIN; i++)
{
if ((row - i) < 0 || (col - i) < 0 || g_board[row][col] != g_board[row - i][col - i])
{
break;
}
num++;
}
if (num >= WIN)
{
return g_board[row][col];
}
num = 1;
//从右上到左下的判断
for (i = 1; i < WIN; i++)
{
if ((row + i) > (ROW - 1) || (col - i) < 0 || g_board[row][col] != g_board[row + i][col - i])
{
break;
}
num++;
}
//从左下到右上的判断
for (i = 1; i < WIN; i++)
{
if ((row - i) < 0 || (col + i) > (COL - 1) || g_board[row][col] != g_board[row - i][col + i])
{
break;
}
num++;
}
if (num >= WIN)
{
return g_board[row][col];
}
if (isdraw())//平局了
{
return 'd';
}
else
{
return ' ';
}
}
/*
游戏是否结束
*/
int isOver(int row, int col)
{
char c = judge(row, col);
if (c == ' ')
{
return 0;
}
if (c == 'x')
{
printf("恭喜你赢了!\n");
system("pause");
return 1;
}
if (c == 'o')
{
printf("不走运,你输了!\n");
system("pause");
return 1;
}
else
{
printf("平局!\n");
system("pause");
return 1;
}
}
/*
游戏主体
*/
void mainGame()
{
int row = 0;
int col = 0;
//初始化棋盘
initBoard();
//打印棋盘
printBoard();
//循环控制游戏进程
while (1)
{
//玩家开始下
playerMove(&row, &col);
//打印棋盘
printBoard();
//判定是否游戏结束
if (isOver(row, col))
{
break;
}
//电脑下
computerMove(&row, &col);
//打印棋盘
printBoard();
printf("计算机回合↑\n");
//判定是否游戏结束
if (isOver(row, col))
{
break;
}
}
}
void gameExplain()
{
system("cls");
printf("五子棋你会玩,三子棋你不会玩?\n");
printf("什么你五子棋都不会玩,你不会百度,搜狗,goole啊!\n");
system("pause");
}
int main()
{
srand(time(NULL));
while (1)
{
int choose = menu();
if (choose == 1)
{
mainGame();
continue;
}
if (choose == 2)
{
gameExplain();
continue;
}
if (choose == 3)
{
break;
}
else
{
printf("别瞎比选!\n");
rewind(stdin);
system("pause");
continue;
}
}
printBoard();
return 0;
}

  在这里值得一提的是我用来判断是否获胜的算法,算法可总结为以落子为中心,向横纵正斜和反斜四个方向分别计数,以达到统计是否已经连子获胜的目的。

【C语言】第三章-函数-2

发表于 2018-11-11 | 分类于 C语言初阶
字数统计: 2k

第三章 函数

第2节

函数调用

  函数调用一般有两种方式,一种是形参不会影响实参的传值调用,另一种是形参会影响实参的传址调用。

传值调用

  传值调用是将实参的值传入函数体中,传入的不过是实参的副本,不会改变实参。这个在上一节已经讲过其中的原因正式因为C语言副本传参的这个特性,这也为我们带来了很多麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
void Swap(int x, int y)//交换x, y的值
{
int tmp = x;
x = y;
y = tmp;
}
int main()//主函数,函数入口
{
int a = 4;
int b = 5;
Swap(a, b);
printf("a = %d, b = %d", a, b);
system("pause");
}

  运行后大家会发现,a, b的值并没有交换,这正是因为我们在这里只用了传值调用,传入的副本不会使实参改变。

传址调用

  传址调用是将参数的地址进行传入,其实就是把指针作为参数,之前我们说过地址就像是变量的门牌号,所以当我们将变量的地址传入的时候,实参就被锁定了,形参的改变,也会使实参改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
void Swap(int* x, int* y)
{
int tmp = *x;
*x = *y;
*y = tmp;
}
int main()//主函数,函数入口
{
int a = 4;
int b = 5;
Swap(&a, &b);
printf("a = %d, b = %d", a, b);
system("pause");
}

  上面这行代码运行后a, b的值就会被交换,然而我们所做的改变不过是将函数的参数类型改为了指针,因此我们如果想要函数能够改变实参的值,我们就必须将参数的指针传入,前提是写一个参数为指针的函数。在CPP中我们会学到比传址调用更加方便的改变实参的方式。

函数的嵌套调用和链式访问

嵌套调用

  嵌套调用是构成C语言最基础的语法,简单来说就是允许在函数内调用其它函数,比如我们在main函数中调用printf函数,这种方式相信大家都不陌生了。

链式访问

  链式访问是在函数参数里调用函数,这种调用方式也很简单,不过是将一个有返回值的函数在另一个函数的参数列表中进行调用,运行时会优先调用参数列表中的函数然后根据返回值进行判断外函数如何运行。这两种调用都十分简单,今后大家会经常用到,在这里不做过多赘述。

函数的声明和定义

函数声明

  因为我们在写函数的时候必须将函数的定义写在函数调用之前(之前我们都是写在main函数前的),因此当我们想要将函数定义写在调用之后以达到美观的易读的效果时我们要怎么做呢?

  这就要用到函数的声明了,所谓函数的声明,不过是在函数的定义写在函数调用之后的情况时为了让编译器依旧可以找到函数的定义的位置我们需要在调用之前所做的一个声明罢了。

  1、声明要告诉编译器又一个函数叫什么,参数是什么,返回类型是什么,但是具体函数存在不存在,无关紧要。

  2、函数声明一般出现在函数调用之前,满足先调用后使用。

  3、函数的声明一般是放在头文件中的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int func(int);//函数的声明可以省略写参数名
int main()//主函数,函数入口
{
int num = 0;
func(num);
system("pause");
}
int func(int num)
{
/*
something;
*/
return num;
}

  在这个例子中,我们将函数的定义写在了函数调用之后,之所以可以这么做全部都多亏与我们在函数调用之前加上了函数的声明,函数的声明不是必须的,但是我们一般将函数的声明写在头文件中,这样会使我们的代码更加易读和管理。

头文件

  既然谈到了函数的定义我们就不得不提一个和函数定义紧密相连的东西,头文件,上文也说过函数一般是定义在头文件中的。

1
2
3
//头文件
#pragma once
int func(int);//函数的声明可以省略写参数名

1
2
3
4
5
6
7
8
//另一个.c文件
int func(int num)
{
/*
something;
*/
return num;
}
1
2
3
4
5
6
7
8
9
10
11
//主函数所在的文件
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
#include "test.h"
int main()//主函数,函数入口
{
int num = 0;
func(num);
system("pause");
}

  可以发现我将自己定义的函数写在了另外两个文件中,而在主函数所在文件中引入了函数声明所在的头文件即可使用函数了。这种书写函数的方式使得我们的代码看起来更加整洁。

  值得一提的是,在主函数加入我们自己写的头文件的时候,最好用""来包含文件名比较好,这样可以更方便我们更快的找到函数。在头文件中大家还可以看到#program once这句话,它的作用是保证我们在多次调用同一个我文件的时候不会造成多次调用,发生错误,可以说是为了弥补C语言头文件使用的弊端,也是头文件必须的。
  可能也会有人在其他教材中发现头文件中还有这么一种书写格式。

1
2
3
4
5
6
7
8
9
10
//头文件
#ifndef TEST
#define TEST


int func(int);//函数的声明可以省略写参数名



#endif // !TEST

  其实这几个语句的效果和#program once效果类似,不过这是以前的写法并且带着很多的弊端,所以今后大家写头文件还是放弃使用这种格式。

函数的递归

  所谓函数的递归简单来说就是在函数内部调用他自身,达到一种循环调用的效果,是很重要的一种程序设计方法,有时使用递归设计程序会给我们带来很多方便,也会使程序运行更加流畅,不过有时依然是迭代来的更加自然。

  递归的程序设计是使用一种减而治之的思想,从局部处理考虑到整体处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>//添加头文件
#include <stdlib.h>
int factor(int num)//递归求阶乘
{
if (num == 1)//如果数字等于一则返回它本身
{
return 1;
}
return num * factor(num - 1);//num! = num * (num - 1)!
}
int main()//主函数,函数入口
{
printf("3的阶乘是:%d!\n", factor(3));
system("pause");
}

  上面这个例子就是一个很好的利用递归进行的程序设计,在某些方面递归就是可以使你的代码更好书写,但是递归也在某些方面有着很麻烦的十分不可理喻的效率,比如在利用递归求斐波那契数的时候,就会产生大量的重复运算,大大降低了效率。
  递归是很重要的程序设计思想,要完全掌握这种设计理念还需要今后多多练习。

1…789
MisakiFx

MisakiFx

Hard working or giving up!!!

86 日志
10 分类
64 标签
GitHub E-Mail 网易云音乐 CSDN

© 2020 MisakiFx
我的网站的访客数:
|
主题 — NexT.Pisces v5.1.4
博客全站共273.4k字