Misaki`s blog

学习是一种态度


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

【Linux】第三章-进程概念

发表于 2019-03-31 | 分类于 Linux
字数统计: 4.4k

第三章 进程概念

  从第三章起我们将进入Linux系统编程的阶段,我们将从计算机操作系统的角度进一步学习Linux。

冯诺依曼体系结构

  在这里又要旧话重提讲一下冯诺依曼体系结构,在冯诺依曼提出计算机的理论组成结构后,现代计算机的组成结构基本都是按照这一套冯诺依曼几十年前提出的体系结构进程设计与制造的。可以说冯诺依曼奠定了现代计算机的硬件体系结构。

  冯诺依曼体系结构可以概括为将计算机分为以下几个部分:1、输入设备(键盘);2、输出设备(显示器)3、存储器(内存);4、运算器(cpu);5、控制器(cpu)。

  对于冯诺依曼体系结构的理解我们不应该只停留在字面上,我们要深入理解计算机的硬件之间数据流的关系,计算机内部在我们执行各种操作的时候数据流是如何移动的?这些理解会随着对计算机系统的理解强化而更加深入。

操作系统

  操作系统可以笼统解释为是一个安装在计算机中的软件,为了让计算机更加方便使用。但实际上,操作系统是统筹合理管理计算机上的软硬件资源的工具。

  那么操作系统如何管理计算机呢?其实计算机在进行管理时会将计算机上的一个个模块用类似于结构的东西进行描述以便于信息的管理,之后再利用数据结构进行组织,因此可以描述为六个字:先描述,后组织。

  计算机上的安装的硬件往往不能直接进行使用,我们需要通过驱动这个媒介才能进行使用,因此对下计算机则会组织和管理驱动来管理硬件。对上操作系统会将系统调用接口进行封装和管理变成库(lib)之后才会提供给用户进行使用。就像我们在写C语言时使用的系统函数,这里调用的接口不是直接的系统调用接口而是包含在库中的一层封装是为了让我们更加方便的进行系统调用从而控制硬件执行操作。

  因此在这里我们可以概括操作系统为:对下管理硬件资源,对上提供良好的执行环境。

进程概念

什么是进程

  进程:进行中的程序。那么计算机系统是怎么描述和组织一个个进程的呢?系统通过一个叫进程控制块的东西进行描述和组织一个个进程,简称PCB。而在Linux上PCB是一个结构体,这个结构体中保存着一个个进程的信息。因此对于操作系统来说一个PCB就是一个进程,系统在管理进程实际上就是在管理一个个PCB,这是系统管理进程的媒介。

PCB

  那么PCB都包含有哪些信息呢?

  1、PID:这是PCB中最为重要的信息,也是我们区分不同的进程的标志。每一个进程都有一个属于自己的ID编号这个编号就存储在PID中。

  2、进程状态:任务的状态信息。

  3、优先级:相比于其他进程的优先级。

  4、程序计数器:由于CPU一次只能同时执行一个进程,因此为了达到看上去可以同时执行多个进程因此CPU会不停的高速在各个进程中进行切换执行,一次执行一点,这个被称为CPU轮循机制。那么在不停切换的时候需要记录各个进程当前执行到的代码,因此就在PCB中进行保存。程序计数器就是记录下一次执行的命令的地址。

  5、内存指针:包括程序代码和进程相关数据的指针。

  6、上下文数据:进程执行时CPU寄存器中存储的数据。

  7、IO状态信息:包括IO请求和分配给进程的IO设备和打开的文件等。

  8、记账信息:处理的时间综合,使用时钟综合,时间限制等。

  9、其他信息。

进程查看

  在Linux中进程的信息都保存在/proc这个系统文件夹中,我们可以使用ls /proc/来查看进程信息。同样我们也可以用ps或top来查看进程信息。

  我们可以首先写一个可以一直执行的程序然后来查看这个进程的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("%d\n", getpid());//getpid()函数可以得到当前进程的PID
sleep(1);
}
}



[misaki@localhost 第三章]$ ./a.out
5517
5517

  我们重新打开一个终端查看以下这个进程的信息。

  先用查看系统文件的方式查看一下进程信息。

查看进程

  我们确实在系统文件中找到了我们的进程,但是这样的文件信息不太便于我们直接查看进程的信息。因此接下来我们使用ps命令。

查看进程

  在这个命令执行后我们可以查看到进程的PID以及占用虚拟内存及物理内存的多少和大小以及进程的状态和执行的命令等。

创建进程

  我们可以在一个进程中使用系统命令fork()创建其子进程,我们创建子进程可以更快的处理数量很多的数据。

  fork()是有返回值的,在父进程中fork()返回子进程的PID,在子进程中返回0,由此我们可以区分父子进程。由于子进程会完全复制父进程的PID因此子进程在创建后会和父进程一起执行创建进程之后的语句直到进程结束。

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
/**
*
* fork初使用
* 通过复制调用进程,创建一个新的进程(子进程)
*/
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("parent pid = %d\n", getpid());
pid_t pid = fork();//复制了父进程的PCB,因此程序计数器也会跟父进程一致,因此子进程下一句执行的代码和父进程一致
printf("child1:%d\n", pid);
if(pid < 0)
{
return -1;
}
else if(pid == 0)//fork有返回值,子进程返回0,父进程返回的是子进程的pid
{
printf("child:%d\n", getpid());//得到当前进程的PID
}
else
{
printf("parent:%d\n", getpid());//得到当前进程的PID
}
printf("Misaki\n");
}


[misaki@localhost 第三章]$ ./Creat
parent pid = 6200
child1:6201
parent:6200
Misaki
child1:0
child:6201
Misaki

  在这个例子中父进程先执行之后才执行了子进程,并且子进程只执行了fork()之后的语句。

进程状态

  在程序执行后,每个进程都有属于自己的状态,并且我们可以通过ps -aux或者ps -axj命令查看到进程的状态。基本状态分为以下几种。

  1、运行状态(R):这个状态表明进程要么正在运行要么就在进程队列中。

  2、睡眠状态(S):表示进程正在等待某个时间的完成。同时睡眠也分为可中断和不可中断的睡眠。这里的睡眠指可中断睡眠。

  3、磁盘睡眠状态(D):这里的睡眠指的即是不可中断睡眠。这个状态的进程往往是在等待IO的结束。

  4、停止状态(T):这里的停止状态指的是通过指令让一个进程处于暂停状态,同时我们是随时可以将其恢复继续运行的。

  5、死亡状态(X):这个状态并不会在查看进程的时候显示,因为这个状态的进程已经完全停止了,是不可恢复的。

僵尸进程

  这是要尤为重点讨论一个进程的状态——僵尸进程。这是一种十分危险的进程状态,是由于子进程在运行中退出但父进程并未读取子进程的退出状态码所导致的,并且此时的子进程为了等待父进程读取退出状态码一直处于僵死状态,由此既产生僵尸进程,直到父进程也退出。以下我将演示僵尸进程的形成。

  我首先书写了一个程序达到让创建新进程后让父进程睡眠60秒并且让子进程立刻退出,由此来达到制造一个长达60秒的僵尸进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return -1;
}
else if(id > 0)
{
printf("父进程:%d\n", getpid());
sleep(60);
}
else
{
printf("子进程:%d\n", getpid());
_exit(-1);
}
}

  之后我们运行程序后,用新的控制台查看查看进程状态即可得到如下图的信息。

查看进程

  可以看到我们的子进程已经变成了僵尸进程。

  造成僵尸进程的后果:

  1、浪费资源。由于僵尸进程一直不退出,父进程一直在关心自己子进程的退出,但是迟迟不会读取,由此系统需要一直维护僵尸进程,并且如果有大量的僵尸进程也会消耗大量资源进行维护。

  2、内存泄漏。由于创造进程需要消耗内存而内存一直无法得到释放,就会造成内存泄漏。

  那我们如何关闭僵尸进程呢?也很简单,只需要关闭他的父进程即可。至于如何避免造成僵尸进程,我们需要用到进程等待技术,这一点我们之后再讲。

孤儿进程

  孤儿进程也是一种特殊的进程。他是在父进程先退出子进程还在执行的情况下造成的。

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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid = fork();
if(pid < 0)
{
perror("fork error");
return -1;
}
else if(pid == 0)
{
printf("I am child:%d\n", getpid());
sleep(60);
}
else
{
printf("I am parent:%d\n", getpid());
sleep(5);
exit(0);
}
}


[misaki@localhost 第三章]$ ./Guer
I am parent:4646
I am child:4647

  以上这个程序我们创造了一个持续一分钟的孤儿进程。我们在另一个终端中查看下父进程退出前后的进程信息。

  前:

前

  后:

后

  通过父进程退出前后我们可以看出来子进程的父进程会从原来的父进程变成1号进程——init进程。这个进程就像是一个巨大的孤儿院,只要你有init进程回收机制,我们的孤儿进程就会被1号进程领养。

进程优先级

PRI + NI

  在操作系统下,我们往往是多个进程同时执行,但是往往在不同的进程中拥有着优先级别,系统会将资源优先分配给优先级高的进程,这样重要的进程才不会显得卡顿。我们的进程往往分为交互式进程和批处理进程,交互式进程要求一旦用户操作就优先运行,因此它们的优先级往往要高于批处理进程。我们先用ps -elf查看一下进程的优先级。

进程优先级

  我们主要关注这两行数据,PRI和NI,它们共同决定了一个进程的优先级。PRI(优先级) = PRI(初始) + NI(NICE值),我们往往无法直接改变一个程序的PRI优先级,但是我们可以通过修改进程的NI值来达到修改进程优先级的目的。PRI值越小则优先级越高,NI值的范围为-20 ~ 19,一共40个优先级级别。

  我们可以使用nice -n 新NICE值 -p PID来更改一个已有进程的NICE值,也可以使用nice -n 新NICE值 可执行程序来用一个NICE值运行一个程序。不过我们要注意修改NICE值需要root用户。

其他概念

  1、并行:cpu资源充足,多个程序同时运行。

  2、并发:cpu资源不足,多个程序轮换运行。

  3、独立性:多个进程之间相互独立。

  4、竞争性:进程多资源少,多个进程间竞争资源。

环境变量

基本概念

  环境变量是保存系统运行环境参数的变量。常见环境变量:PATH,HOME,SHELL。

环境变量相关基础指令

  1、echo $NAME:可以查看一个环境变量。

  2、env:查看所有环境变量。

  3、export:声明一个环境变量。

  4、unset:删除一个环境变量。

  我们在将一个程序的路径加入PATH环境变量后即可直接使用程序名执行程序。export PATH=$PATH:程序路径。

在程序中获取环境变量

通过命令行第三个参数

  每个程序在执行时都会接收到操作系统的一张表,这张表是由字符指针数组构成的,每个指针都会指向一个以’\0’结尾的环境变量,并且这张表存储在main函数的第三个参数中。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main(int argc, char* argv[], char* env[])
{
int i = 0;
for(; env[i]; i++){
printf("%s\n", env[i]);
}
return 0;
}

  由此即可将所有的环境变量打印出来。

通过第三方变量environ

  在C语言的libc官方库中,有一个事先定义好的全局变量environ,这个变量也指向环境变量表,我们可以通过它来找到所有的环境变量。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main()
{
extern char** environ;
int i = 0;
for(; environ[i]; i++){
printf("%s\n", environ[i]);
}
return 0;
}

通过系统调用获得环境变量

  getenv()是一个系统调用接口专门用来帮助我们在程序中获取环境变量的值。

1
2
3
4
5
6
7
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}

程序地址空间

  我们之前在学习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
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0){
perror("fork");
return 0;
}
else if(id == 0){ //child,子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取
g_val=100;
printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}else{ //parent
sleep(3);
printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
sleep(1);
return 0;
}


child[3046]: 100 : 0x80497e8
parent[3045]: 0 : 0x80497e8

  为什么会这样呢,命名地址相同但是值却不同?我们的系统在给每个进程分配内存时都会进行一套处理,所让我们看到的地址都不是真实的地址我们称之为虚拟地址,与之相对的,真实的在内存中的存储方法是不可见的,是由系统进行管理的,我们称这种真正的地址为物理地址。所以在我们上面这个程序中父进程与子进程的变量的地址相同其实是它们的虚拟地址相同,但必然有着不同的物理地址,因此才会有不同的值。

虚拟地址空间

  在每个程序执行时系统就会为它分配一块物理内存,纳闷物理内存是如何和虚拟内存关联的呢?那就必然有着一个中间起到连接作用的工具——页表。页表是一用来记录虚拟地址与物理地址它们之间的关系,并且对内存数据进行管理。如下所示。

虚拟地址空间

  不过在我们创建子进程时也并不是一旦创建就会直接分配物理内存,这样有点过于浪费内存空间了。得益于写时拷贝技术可以让我们创建完子进程后父子进程公用同一块物理内存,只有在子进程改变变量时才会分配新的物理内存,这样大大减少了对内存的消耗。

【Linux】Linux基础-3

发表于 2019-03-24 | 分类于 Linux
字数统计: 701

Linux基础

第3节

Linux文件相关命令

  在Linux中文件分为以下几种:

  -:普通文件

  d:目录文件

  l:符号链接文件

  p:管道文件

  c:字符设备文件

  b:块设备文件

  s:套接字文件

  以下文件相关命令以作补充。

  tac:逆序显示文件内容。

tac

  more:分页显示文件内容,键入命令后可以用空格向下翻页,b向上翻页,回车按行向下移动,q退出。

  less:分页显示内容(功能比more更全面),可用/向下匹配字符串也可用?向上匹配字符串。

  head:默认显示文件的前10行。-n选项显示文件前n行。

head

  tail:默认显示文件末尾10行。-n选项显示文件末尾n行.-f选项刷新文件末尾数据,用于实时查看更新日志。

  |:管道符,连接两个命令,将前一个命令的结果作为第二个命令的输入。

  例:打印文件第10行的内容。

例子

  >>:重定向符,改变数据流向,并流向文件末尾。

  >:重定向符,改变数据流向,将原本数据消除,再将 数据导入。

重定向

Linux压缩/打包相关命令

  1、zip//unzip:zip压缩格式。

压缩

  2、gzip/gunzip:gzip压缩格式。

压缩

  3、bzip2/bunzip2:bzip2压缩格式。

压缩

  4、tar:打包。-c打包指令,-v显示打包文件,-f指定包名,-x拆包命令,-z压缩/解压缩转gzip压缩格式,-j压缩/解压缩转bzip2压缩格式。

打包

时间相关命令

  1、cal:查看日历。-3查看相邻三个月的日历,-y查看全年的日历,-j以一年的第多少天来 显示日历。

日历

日历

  2、date:显示时间。+"格式符串"按照一定的格式打印时间,-s在root用户下修改时间。

打印时间

设置时间

匹配搜索命令

  1、grep:文件字符串匹配,-i忽略大小写匹配,-v反向匹配,-R递归目录下所有文件进行匹配。

grep

  
2、find:文件查找。-name按文件名查找,-type按类型查找,-size +/-按大小查找,-exec,连接其他命令对找到的文件进行处理。

find

其他命令

  1、du:显示每个文件或目录的磁盘使用空间。-s仅显示总计大小,-h分别列出各个目录的空间。

du

  2、df:列出文件系统整体磁盘的使用情况。-a显示所有文件系统

df

  3、top:用来实时监控进程信息和状态

  4、free:用来查看系统内存的使用情况。

df

【DS】第一章-初识数据结构

发表于 2019-03-19 | 分类于 数据结构
字数统计: 1.2k

第一章-初识数据结构

前言

什么是数据结构

  数据结构(Data Structure)是计算机存储、组织数据的方式,指之间相互存在一种或特定关系的数据元素的集合。

什么是算法

  算法是解决一类问题的一系列计算步骤。

数据结构和算法的重要性

  笔试,面试必备。实际项目开发提供思想和思路,程序员的内功。

如何学好数据结构

  1、疯狂写代码。

  2、注意画图和思考。不懂得地方多画几遍图,思考出思路过后再用代码实现

  3、多去在线OJ,推荐牛客网,和领扣网。

时间复杂度和空间复杂度

  我们在实现一个算法之后应该如何来计算或者测量这个算法的优劣呢?这就要考虑到依靠算法的效率。效率分为时间效率和空间效率,但是我们该如何衡量这两大效率呢?因为每个计算机的性能都不一样,因此我们不能依靠在计算机上运行测试时间长短来计算效率,我们总不能把全世界的算法都放到同一台计算机上来进行测试。

  因此我们为了更好的更方便的测试一个算法的效率就依靠数学计算来进行测量,由此诞生了时间复杂度和空间复杂度。

时间复杂度

  时间复杂度在计算机科学中是一个数学函数,他基本代表了一个算法执行基本操作的次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void Func1(int N)
{
int count = 0;
for (int i = 0; i < N ; ++ i)
{
for (int j = 0; j < N ; ++ j)
{
++count;
}
}
for (int k = 0; k < 2 * N ; ++ k)
{
++count;
}
int M = 10;
while (M--)
{
++count;
}
printf("%d\n", count);
}

  像以上这这段代码我们可以用一个函数F(n)来计算其一次的执行次数,可得到F(n) = n ^ 2 + 2 * n + 10,但是这样表示过于复杂,也不够直观,由此我们引入了大O渐进表示法(Big O notation),以下是大O渐进表示法的推导法则:

  1、用常数1取代运行时间中的所有加法常数。

  2、在修改后的运行次数函数中,只保留最高阶项。

  3、如果最高阶项存在且不是1,则去除与这个项目相乘的常数。得到的结果就是大O阶。

  有了这个方法我们可以将以上的F(n)表示 为O(F(n)) = n ^ 2。对于这个阶级我们称之为平方阶,其时间效率是十分底下的,也是最为常见的。我们往往在使用了两个循环的时候就会出现平方阶。以下还有几个比较常见的阶级。

  1、O(1):常数阶。这个阶级是效率最为高效的,但也是可遇而不可求的。

  2、O(logn):对数阶。对数阶是效率次于常数阶的也是十分高效的阶级,也是我们平时所追求的算法的极致效率。

  3、O(n):线性阶。线性阶也是效率 十分高效的了,并且也是十分常见的,同样的如果我们的算法不能达到对数阶那么也尽量应该达到线性阶

  4、O(n ^ 2):平方阶的效率比起以上的阶级效率上就要低下一些,但是在实战中并非所有的算法都能够达到前三大阶级,因此如果能达到平方阶我们则也可以称这个算法是好的,至少是有效的。

  5、O(2 ^ n):指数阶,这个阶级效率 十分低下的效率,占用的时间增长十分的快,如果n的数值较小我们还是可以计算出结果的,但是n一旦稍微大一些,我们计算的时间往往过于漫长,因此一旦出现指数阶的算法我们是并不能将其投入使用的,这个算法也不能算是有效的。

空间复杂度

  空间复杂度与时间复杂度类似,也采用大O渐进表示法,往往先计算出占单位空间的大小表达式,再进行化简表示。相比空间复杂度我们往往更加关注于时间复杂度,因为时间效率往往是优先的。有的情况下我们不得不做出舍弃空间减少时间的决策,同样的也会做出舍弃时间减少空间的决策,这都要根据具体场景来进行决定。

【C语言】第十四章-程序的编译

发表于 2019-03-10 | 分类于 C语言进阶
字数统计: 2.2k

第十四章 程序的编译

程序的编译过程

  在我们使用ide或者gcc的时候,编译器总是自动帮我们直接生成了可执行文件,但是在编一个过程那种还可细分为几个步骤,这几个步骤的说明则是这一章的重点。

预处理

  预处理是编译器对我们的代码进行的第一道处理 ,在这个过程中编译器会做以下几件事情。

  1、拷贝头文件。

  2、去掉注释。

  3、对宏展开。

  4、处理条件编译。

  我们在Linux中可以通过指令让gcc生成预处理后的文件。这里假设我有一个hello.c文件。

        gcc -E hello.c -o hello.i

  之后就会生成一个预处理后的文件,我们可以用vim打开看看其中的内容,会发现代码一下多了好几百行,这就是因为编译器将头文件中的内容全部拷贝了进来所导致的,同时会发现编译器在预处理期间确实就做了上述的4件事。

编译

  在预处理结束后编译器才真正开始进行编译,编译器会对整合的代码进行语义和语法分析处理,使我们的源码变成汇编代码。当然我们也有语句可以让gcc生成编译过后的文件。

        gcc -S hello.i -o hello.s

  编译后打开.s文件会发现我们的代码会转换为汇编语言,但是计算机也并不直接看得懂汇编语言,于是就有了下一步编译。

汇编

  在这一步里编译器会进行汇编将生成的汇编文件转换为机器码也就是二进制指令,这样计算机就看得懂了,我们可以用以下这段代码生成汇编结束的文件。

        gcc -c hello.s -o hello.o

  汇编结束后会发现代码我们就真的一个字符都看不懂了,这就是因为文件已经变成了二进制的指令,但是到此编译过程还没有结束,我们还差最后一步生成可执行文件。

链接

  在最后一步中会进行各个文件的链接,因为我们的各个文件都是分开进行编译的,就需要最后一步来将他们合并,因此最后一步往往有两个步骤,合并段表,合并符号表和符号表的重定位,具体可以理解为将项目中各个源文件进行合并统一管理。之后就可以生成可执行文件了。

        gcc hello.o -o Hello

  至此编译过程就结束了。

预处理详解

预处理符号

  在程序编写中有一些语言自带的在预处理中会进行处理的特殊的宏供我们使用,这里列举出一些。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main()
{
printf("当前的文件:%s\n", __FILE__);//__FILE__表示当前的文件
printf("当前的行号:%d\n", __LINE__);//__LINE__表示当前的行号
printf("文件被编译的日期:%s\n", __DATE__);//打印文件被编译的日期
printf("文件被编译的时间:%s\n", __TIME__);//打印文件被编译的时间
printf("是否遵循ANSI C标准:%s\n", __STDC__ != 0 ? "是" : "否");//如果编译器遵循ANSI C标准,它就是个非零值
}



[misaki@localhost 程序的编译]$ ./Main
当前的文件:main.c
当前的行号:5
文件被编译的日期:Mar 18 2019
文件被编译的时间:00:28:03
是否遵循ANSI C标准:是

  这些符号在预处理期间就会进行替换从而可以使用在各个场景下。

define详解

  宏定义是在预处理期间就被处理的宏,用提十分广泛,不光是定义常量,定义函数,定义别名都有着不可小觑的作用。

  但是在使用宏定义的过程中我们始终要记住宏就是文本替换,不过是将上面的代码替换了下来罢了,由这一特性宏既有优势也有劣势。

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
#include <stdio.h>
/* 续行符 \ */
//用宏定义函数想要换行的话再行尾+'\'
#define CHECK(fp) if (fp == NULL) \
{ \
printf("fopen failed! %s:%d\n", __FILE__, __LINE__); \
}
void Check(FILE* fp)
{
if(fp == NULL)
{
printf("fopen failed! %s:%d\n", __FILE__, __LINE__);
}
}
int main()
{
FILE* fp1 = fopen("./test.txt", "r");
CHECK(fp1);
Check(fp1);
FILE* fp2 = fopen("./test.txt", "r");
CHECK(fp2);
Check(fp2);
}


[misaki@localhost 程序的编译]$ ./a.out
fopen failed! hello.c:42
fopen failed! hello.c:17
fopen failed! hello.c:45
fopen failed! hello.c:17

  这个例子可以看出用宏定义写的函数会明确的将代码出错的行号准确的返回,让我们知道是哪个位置出现了错误,但是用普通的函数却做不到这一点,只会返回函数定义的地方,这也是得益于宏定义的特性。同时我们还要注意宏定义是没有参数检查的,因此我们再宏定义中的fp参数其实可以是任意类型的。

  同样宏定义也有着不少的缺点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#define ADD(x, y) x + y
#define MUL(x, y) x * y
int main()
{
int a = ADD(10, 20) * 10 + 20;
printf("a = %d\n", a);//纯文本替换这里就会出错
int a2 = MUL(10, 10 + 10);//也会出错
printf("a = %d\n", a2);
}



[misaki@localhost 程序的编译]$ ./a.out
a = 230
a = 110

  以上这两个例子我们都没有的到预期想要的结果,都是因为宏定义直接将文本拷贝至此因此没有优先顺序,导致计算没有按照预期进行,为此我们不得不多加几个括号。
  综上所述宏定义有利有弊,我们总结一下哪些情况一定要使用宏定义。

  1、打印日志的行号和文件。

  2、没有参数类型检查。

  3、要求开销更小。

‘#’和’##’

‘#’

  当我们使用printf()打印时不同的字符串是可以拼接的,我们使用以下语句printf("Hello""Misaki")会出现HelloMisaki的结果,我们利用这一点可以使用宏定义模拟实现printf()。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#define PRINT(FORMAT, VALUE) \
printf("the value = " FORMAT " is format\n", VALUE);
int main()
{
int i = 10;
PRINT("%d", i + 1);
}



[misaki@localhost 程序的编译]$ ./a.out
the value = 11 is format

  这样自然可以,但是这样只有当参数是字符串的时候才可以完成拼接,呢有没有什么方式可以将不是字符串的参数在宏中转换为字符串呢?

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#define PRINT(FORMAT, VALUE) \
printf("the value = " #VALUE " is format\n", VALUE);
int main()
{
int i = 10;
PRINT("%d", i + 1);
//宏的参数 # 能把参数变成一个字符串,然后这个字符串就可以再代码中进行文本拼接
}

  这样即可将不是字符串的参数转换为字符串。

‘##’

  ##的作用是符号拼接,这个操作符十分强大,甚至允许拼接变量。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#define ADD_TO_NUM(num , value) \
sum##num += value;//## 拼接可以拼接变量,这里变成了sum(num) => sum1
int main()
{
int sum1 = 10;
ADD_TO_NUM(1, 10);//等于给num1 + 10
printf("%d\n", sum1);
}

其他预处理指令

undef

  undef用于移除一个已经有了的宏定义。

1
2
3
#define SIZE 10
#undef SIZE//清除原来的宏定义
#define SIZE 5

条件编译

  当满足某个条件的时候进行编译,否则不编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main()
{
#if 0 //条件为真就编译,为假就不编译
printf("hehe\n");
#else
printf("haha\n");
#endif
}



[misaki@localhost 程序的编译]$ ./a.out
haha

  另一种条件编译。

1
2
3
#ifdef SIZE//如果SIZE被宏定义就编译以下代码
printf("haha\n");
#endif

pragma once

  头文件在预处理阶段会进行问呗拷贝合并到一个文件中,因此如果一个头文件多次引用就会导致重复定义从而报错,因此我们往往想要一个头文件只编译一次,因此我们就会使用pragma once告诉编译器这个文件只编译一次。我们还可以使用以下的代码替代它,不过效果并不如它,在一些特定的情况下也会出错。

1
2
3
4
#ifndef time
#define time
... ...
#endif

【C语言】第十三章-文件操作

发表于 2019-02-27 | 分类于 C语言进阶
字数统计: 2.6k

第十三章-文件操作

  之前我们所讨论的动态内存管理所操作的是计算机中的内存,而本章节的内容所讨论的核心是计算机的外村,也就是磁盘。我们往往想要将一个程序开辟的内存中数据存储到外存中,以供下次程序启动时使用,而本章节就会讨论相关语法。

打开文件

  我们所有的文件相关操作都是使用一个文件的指针进行操作的,而这个文件的指针我们还有一个名字就是句柄,句柄就像是遥控器,我们往往无法直接对文件进行操作,但是使用遥控器就可以进行远程遥控,控制文件完成相关操作。而在文件最开始打开时我们就会获取到这么一个句柄。

  在此我们会借助一个fopen()函数,函数原型为FILE * fopen ( const char * filename, const char * mode );接下来我们具体使用一下。

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
int main()
{
FILE* fp = fopen("./test.txt", "r");
if(fp == NULL)//如果文件 打开失败
{
perror("fopen");//输出错误提示
}
}

  以上这行代码我们我们调用了fopen函数,函数第一个参数时文件路径,第二个参数为打开模式。我们用r模式想要打开test.txt文件,但是如果我们当前目录下没有这个文件就会打开失败,于是会返回错误信息,产生以下结果。

1
2
[misaki@localhost test]$ ./Main 
fopen: No such file or directory

  而如果存在这个文件也有可能会打开失败,呢就是当我们没有文件的读权限的时候我们也会打开失败。

  关于打开模式有很多种,有的打开模式只支持读取不支持写入,有的打开模式再没有目标文件的时候会自行创建或是删除原有文件中的内容。具体可以 参考以下这张表。

打开模式

关闭文件

  在我们打开文件使用完毕后不可缺少的就是要关闭文件,那么我们如果不关闭文件会产生怎样的后果呢?

  在操作系统中我们是通过句柄来操作文件,但其实我们打开文件所产生的句柄所指向的是一个文件数据结构,这个数据结构包含一块缓冲区和一个文件描述符,这些都存储在内存上,而我们打开文件就会消耗内存进行管理,因此如果我们打开文件却忘了关闭文件就会造成和我们给指针划分内存却忘了释放一样的结果——内存泄漏或文件描述符泄漏。

1
fclose(fp);//文件不使用的时候,就需要及时关闭掉

读取文件

  我们在用打开文件后可以进行的首要操作就是文件的读取。这里要用到最多的一个函数就是fread(),函数原型为size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );,参数中ptr是读取到的缓冲区指针,stream是文件指针。这里要注意的是里面的size和count参数。size参数是以多少个字节为一块,count参数表示一共读取多少块,也就是说size * count就是目标读取文件的字节数。然而文件所返回的数值表示实际读取了的块的数目。一般情况下我们会将size参数给定为1,以一个字节为一块,这样方便我们计算预计读取和实际读取的字节数。

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
#include <stdio.h>                                                              
#include <errno.h>
int main()
{
//1、打开文件
//fp => 句柄 句柄就是遥控器
FILE* fp = fopen("./test.txt", "r");//第一个参数文件路径,第二个参数打开法方式
//w打开方式打开文件会将原来文件中的内容全部清空
//每个程序一打开就会默认打开三个文件:标准输入,标准输出,标准错误
if(fp == NULL)
{
perror("fopen");
//printf("%s\n", strerror(errno));//打印错误码
//进程的退出码,echo $?的 方式能获取上个进程的退出码
return 1;
//结果出错,返回非0
//进程退出码
}
//2、读文件
//buffer => 缓冲区:提高读取效率,一般情况下是一块内存
char buf[1024] = { 0 };
size_t n = fread(buf, 1, 100, fp);//1 * 100 = 预计读取的字节数
//1为一个元素几个字节,读取100个元素,返回成功读取的元素个数
printf("%s\n", buf);
printf("%ld\n", n);
fclose(fp);//文件不使用的时候,就需要及时关闭掉
//文件不及时关闭的话,句柄泄露/资源泄露/文件描述符泄漏
//可能导致后续的文件就无法打开了(一个进程可以打开的文件是有上限的)
return 0;
}


[misaki@localhost File]$ ./a.out
Misaki

7

  以上这段代码通过fread()函数将文件中的数据全部读入到了事先准备好的缓冲区中,然后我们又将缓冲区中的数据全部打印了出来。注意我们这里打印fread()函数读取的块数时返回了7,这是因为在文件中编辑默认末尾会加上换行符。

写入文件

  写文件是我们另外一个经常使用的功能,有读必有写。而写文件最为常用的函数是fwrite(),原型为size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );,这里的参数与fopen十分类似就不一一介绍了。

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
#include <stdio.h>                                                              
#include <errno.h>
int main()
{
//1、打开文件
//fp => 句柄 句柄就是遥控器
FILE* fp = fopen("./test.txt", "w");//第一个参数文件路径,第二个参数打开法方式
//w打开方式打开文件会将原来文件中的内容全部清空
//每个程序一打开就会默认打开三个文件:标准输入,标准输出,标准错误
if(fp == NULL)
{
perror("fopen");
//printf("%s\n", strerror(errno));//打印错误码
//进程的退出码,echo $?的 方式能获取上个进程的退出码
return 1;
//结果出错,返回非0
//进程退出码
}
//3、写文件
char buf[1024] = "Misaki1";
size_t n2 = fwrite(buf, 1, 7, fp);
//参数与fread相同,同样返回成功写入的元素个数
printf("n2 = %lu\n", n2);
if(n2 == 0) //如果写入失败打印错误信息
{
perror("fwrite");
}
fclose(fp);//文件不使用的时候,就需要及时关闭掉
//文件不及时关闭的话,句柄泄露/资源泄露/文件描述符泄漏
//可能导致后续的文件就无法打开了(一个进程可以打开的文件是有上限的)
return 0;
}
[misaki@localhost File]$ ./a.out
n2 = 7
[misaki@localhost File]$ cat test.txt
Misaki1[misaki@localhost File]$

  可以看到缓冲区中的内容已经成功写入文件了。不过要注意的是如果我们想要向文件中写入,就必须要使用具有可写功能的打开模式,而在这里所选择的w模式会自动将文件清空。

文件随机读写

  当我们打开文件时文件指针便会指向文件,并且在文件指针指向的地方进行读写,然而我们可以通过以下几个函数控制文件指针。

1
2
3
int fseek ( FILE * stream, long int offset, int origin );//使用不同的模式使文件指针相对于原来位置进行移动,成功返回0,否则返回其他
void rewind ( FILE * stream );//让文件指针位置回到文件起始位置
long int ftell ( FILE * stream );//返回 文件相对于起始位置的偏移量

  例子:

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
#include <stdio.h>                                                              
#include <errno.h>
int main()
{
//1、打开文件
//fp => 句柄 句柄就是遥控器
FILE* fp = fopen("./test.txt", "w");//第一个参数文件路径,第二个参数打开法方式
//w打开方式打开文件会将原来文件中的内容全部清空
//每个程序一打开就会默认打开三个文件:标准输入,标准输出,标准错误
if(fp == NULL)
{
perror("fopen");
//printf("%s\n", strerror(errno));//打印错误码
//进程的退出码,echo $?的 方式能获取上个进程的退出码
return 1;
//结果出错,返回非0
//进程退出码
}
//3、写文件
char buf[1024] = "Misaki1";
size_t n2 = fwrite(buf, 1, 7, fp);
//参数与fread相同,同样返回成功写入的元素个数
printf("n2 = %lu\n", n2);
if(n2 == 0) //如果写入失败打印错误信息
{
perror("fwrite");
}
//7、文件的随机读写
size_t p = ftell(fp);
printf("%lu\n", p);
fseek(fp, 3, SEEK_SET);
p = ftell(fp);
printf("%lu\n", p);
//4、关闭文件
fclose(fp);//文件不使用的时候,就需要及时关闭掉
//文件不及时关闭的话,句柄泄露/资源泄露/文件描述符泄漏
//可能导致后续的文件就无法打开了(一个进程可以打开的文件是有上限的)
return 0;
}


[misaki@localhost File]$ ./a.out
n2 = 7
7
3

  以上这个例子就是用了文件指针的相关控制函数改变了文件指针的位置达到可以随机读写的目的。

其他函数

  在文件的读写上除了fread()和fwrite()函数外还有一些函数有着强大的读写功能。

  1、char * fgets ( char * str, int num, FILE * stream );

  int fputs ( const char * str, FILE * stream );

  第一组函数与puts和gets的使用方法类似,不过这里是向文件中读取或写入字符串。

  2、int fgetc ( FILE * stream );

  int fputc ( int character, FILE * stream );

  第二组函数与getc和putc类似,不过这里是向文件中读取或写入字符。

  3、int fprintf ( FILE * stream, const char * format, ... );

  int fscanf ( FILE * stream, const char * format, ... );

  这一组函数更为强大,是格式化文件输入输出函数,使用与printf和scanf类似。

【C语言】第十二章-动态内存管理

发表于 2019-02-19 | 分类于 C语言进阶
字数统计: 2.6k

第十二章 动态内存管理

为什么会出现动态内存分配

  这个问题要结合数组来进行讨论。在C99之前的标准中,C语言中数组的定义要求必须给定常量的大小才能定义数组,也就是说数组变量的内存空间在编译时就已成定局,无法更改。那我们如果想要申请一块内存空间,空间大小在运行时才进行确定该怎么做呢?这就要用到动态的内存分配了,这里的空间大小的确定是在运行时才进行确定的。

动态内存分配函数

malloc和free

  在内存的动态分配中,首当其冲最为重要的两个函数就是malloc和free函数,它们挑起了动态内存分配的大梁。
  malloc函数负责向计算机申请一块连续的内存空间,并且返回指向该块内存的指针。

        void* malloc(size_t size);

  但是值得注意的是malloc所返回的指针的类型是void*型,所以在实际使用中我们往往要进行牵制类型转换将其转换为我们想要的指针类型。关于这个函数还有以下几点说明。

  1、如果开辟空间成功则返回一个指向该空间的指针。

  2、如果开辟空间失败则返回NULL。

  3、如果参数为0则是未定义行为。

  4、返回类型为void*所以编译器并不知道所开辟的空间的类型,由使用者决定。

  既然开辟了空间,并且这个空间编译器不会帮我们自动将其释放那么就必然要求我们在使用完毕后将其释放,不浪费内存空间不造成泄露,因此就出现了free函数。

        void free (void* ptr);

  1、如果参数ptr的不是动态开辟的则行为是未定义的。

  2、如果参数ptr == NULL则函数什么都不做。

  malloc和free函数都在stdlib.h头文件中。

  以下举一个动态分配内存并且释放的例子。

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
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 0;
printf("请输入要分配的数组的内存空间的大小:");
scanf("%d", &n);
int* array = (int*)malloc(sizeof(int) * n);
printf("分配成功!\n");
printf("请给数组赋值:");
for(int i = 0; i < n; i++)
{
scanf("%d", &array[i]);
}
for(int i = 0; i < n; i++)
{
printf("%d\t", array[i]);
}
printf("\n");
free(array);
array = NULL;
printf("数组已经释放!\n");
}


[misaki@localhost test]$ ./Main
请输入要分配的数组的内存空间的大小:8
分配成功!
请给数组赋值:1
2
3
4
5
6
7
8
1 2 3 4 5 6 7 8
数组已经释放!

  在以上这个例子中我们动态地在程序运行时对一个数组进行了空间的分配,并且让用户对数组进行了赋值,之后打印,数组使用完毕后我们又用free函数将其空间释放。这是一个很简单但是很实用的动态内存分配的例子。在这个例子中要尤为记住的一点是我们在进行动态的内存分配后一定不能忘记在使用完毕后将内存空间释放,并且将指针赋值为NULL,这一点是十分关键的,否则将造成内存泄漏和野指针,对程序造成很大的影响。

  往往内存泄漏不是我们忘记free而是不经意间造成的,以下就是一个典型的内存泄漏的例子。

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* p = (int*)malloc(sizeof(int));
p = (int*)malloc(sizeof(int));
free(p);
p = NULL;
}

  这个例子中我们明明进行了释放却也造成了内存泄漏,这是因为我们申请了两次内存空间,但是用同一个指针来接收,只释放了一次,因此造成了内存的泄漏。因此我们在实际使用中牵扯动态内存分配的时候都一定要小心又小心。

calloc

  C语言还提供了一个函数进行动态内存分配。这个函数与malloc函数很相似,但是有一点不同的是这个函数会自动进行初始化。但是初始化有时候也不尽然全是好事,当我们要申请一个特别大的空间时,初始化会浪费很多很多的时间。

      
void* calloc(size_t num, size_t size);

  calloc的功能是分配num个大小为size的内存空间,并将内存空间初始化为0。举个例子。

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
#include <stdio.h>  
#include <stdlib.h>
int main()
{
int n = 0;
printf("请输入数组长度:");
scanf("%d", &n);
int* array = (int*)calloc(n, sizeof(int));
for(int i = 0; i < n; i++)
{
printf("%d\n", array[i]);
}
free(array);
}


[misaki@localhost test]$ ./Main
请输入数组长度:8
0
0
0
0
0
0
0
0

  在以上这个例子中我们看到calloc确实可以进行动态内存分配并且进行了初始化,但是这也是又两面性的。因此也要谨慎使用。

realloc

  当我们将内存空间进行动态分配后我们如果想要扩大我们分配的内存该怎么做呢?我们可以分配一块新的内存然后将原来内存中的数据再放到新的内存中。但是在C语言标准库中为了方便已经给我们准备了这么一个扩容函数realloc。

      
void* realloc(void* ptr, size_t size);

  ptr是要调整大小的内存空间,size是调整后的大小。至于计算机是怎么进行扩容的呢?这里要分两种情况。

  1、如果目标内存空间后面有足够大小的空间则直接将后面的空间归入目标空间中即可。

  2、如果目标空间后面没有足够大小的空间则在内存中重新寻找一片足够大小的空间进行开辟并且将原来的数据放入到新的空间中。

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
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 0;
printf("请输入数组的长度:");
scanf("%d", &n);
int* array = (int*)malloc(sizeof(int) * n);
printf("请输入目标扩容的大小:");
scanf("%d", &n);
array = (int*)realloc(array, n * sizeof(int));
printf("扩容成功!\n");
printf("请给数组赋值:");
for(int i = 0; i < n; i++)
{
scanf("%d", &array[i]);
}
printf("打印数组:");
for(int i = 0; i < n; i++)
{
printf("%d\n", array[i]);
}
}


[misaki@localhost test]$ ./Main
请输入数组的长度:5
请输入目标扩容的大小:8
扩容成功!
请给数组赋值:1
2
3
4
5
6
7
8
打印数组:1
2
3
4
5
6
7
8

  以上这个例子我们成功对已经分配的内存空间进行了扩容。

常见动态内存错误

  1、对NULL指针的解引用操作。

  2、对动态开辟空间越界访问。

  3、对非动态内存使用free释放。

  4、释放一块动态开辟内存的一部分。

  5、对同一块内存多次释放。

  6、动态开辟内存忘记释放。

  以上的错误都是十分常见的,因此我们在对内存进行操作的时候一定要万分小心。

C/C++程序内存开辟

  在这里将详细介绍下计算机内存中的几个区域。

  1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些
存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有
限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。

  2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分配方式类似
于链表。

  3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

  4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

  通常我们定义的变量都存在栈区,动态分配内存时变量的空间都是在堆区上进行分配的,堆区有着更大更充足的空间。由此一来我们对动态内存分配就有了更深的了解。

柔性数组

  柔性数组是在C99中最新的语法,其允许在结构体中最后一个成员是一个位置大小的数组,这就是柔性数组。

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
#include <stdio.h>
#include <stdlib.h>
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
int main()
{
type_a* p = (type_a*)malloc(sizeof(type_a) + sizeof(int) * 10);//为结构体分配内存空间
p->i = 0;
for(int i = 0 ;i < 10; i++)
{
p->a[i] = 0;
}
for(int i = 0; i < 10; i++)
{
printf("%d\n", p->a[i]);
}
free(p);
}


[misaki@localhost test]$ ./Main
0
0
0
0
0
0
0
0
0
0

  在以上这个例子中我们使用了柔性数组,在使用柔性数组的时候我们要对结构体进行动态内存分配,并且分配的空间要大于结构体除柔性数组外的空间。

  柔性数组的特点:

  1、结构体中柔性数组前面必须至少一个其他成员。

  2、sizeof测量结构体大小不包括柔性数组的大小。

  3包含柔性数组的结构体用malloc进行内存动态分配,并且分配的内存大于结构体大小以适应 预期大小。

【C语言】项目-电话簿

发表于 2019-02-16 | 分类于 项目
字数统计: 1.4k

C语言项目

电话簿

实现思路

  这个程序的整体思路和实现还是很简单的,我们利用了动态分配内存的方式首先创建了电话簿联系人类型,然后创建电话簿类,其实事联系人类型构成的全局数组,然后分别实现,界面函数,增删改查打印等功能,在主函数离实现总体思路,利用while循环让用户选择具体功能调用函数的方式进行总体实现。

  ————————————————————————

  2019.3.12更新:

  利用文件操作使得系统可以进行数据的保存。

实现代码

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
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#define ADDRESS_INFOS_MAX 200
typedef struct AddressInfo
{
char name[1024];
char phone[1024];
}AddressInfo;
typedef struct AddressBook
{
AddressInfo* infos;
int size;
int capacity;//容量
}AddressBook;
AddressBook g_addr_book;
//初始化
void Init(AddressBook* addr_book)
{
assert(addr_book != NULL);
addr_book->size = 0;
addr_book->capacity = 10;
addr_book->infos = (AddressInfo*)malloc(sizeof(AddressInfo) * addr_book->capacity);
return;
}
//菜单
int Menu()
{
printf("===========================\n");
printf("1、新增\n");
printf("2、删除\n");
printf("3、修改\n");
printf("4、查找\n");
printf("5、排序\n");
printf("6、显示全部\n");
printf("7、删除全部\n");
printf("0、退出\n");
printf("===========================\n");
int choice;
scanf("%d", &choice);
return choice;
}
//扩容
void Realloc(AddressBook* addr_book)
{
assert(addr_book != NULL);
//1、扩大capacity的取值
addr_book->capacity *= 2;
//2、申请一个更大的内存
AddressInfo* old_infos = addr_book->infos;
addr_book->infos = (AddressInfo*)malloc(sizeof(AddressInfo) * addr_book->capacity);
//3、将原有内存中的数据复制过来
for(int i = 0; i < addr_book->size; i++)
{
addr_book->infos[i] = old_infos[i];
}
//4、释放原有的内存
free(old_infos);
//此处每次扩容阔多少由我们自己定制
printf("扩容成功!\n");
}
//新增
void AddAddressBook(AddressBook* addr_book)
{
assert(addr_book != NULL);
if(addr_book->size >= addr_book->capacity)
{
printf("空间已满进行扩容!\n");
Realloc(addr_book);//扩容
}
printf("请输入联系人信息!\n");
AddressInfo* p = &addr_book->infos[addr_book->size];
printf("请输入联系人姓名:");
scanf("%s", p->name);
printf("请输入联系人电话:");
scanf("%s", p->phone);
addr_book->size++;
printf("添加成功!\n");
return;
}
//删除
void DelAddressBook(AddressBook* addr_book)
{
assert(addr_book != NULL);
int id;
printf("请输入要删除的联系人序号:");
scanf("%d", &id);
if(id < 0 || id >= addr_book->size)
{
printf("输入需要有误!\n");
return;
}
printf("删除的序号为[%d],确认删除请输入Y:", id);
char sure[1024] = {0};
scanf("%s", sure);
if(strcmp(sure, "Y") != 0)
{
printf("删除中止!\n");
return;
}
AddressInfo* from = &addr_book->infos[addr_book->size - 1];
AddressInfo* to = &addr_book->infos[0];
*to = *from;
addr_book->size--;
return;
}
//修改
void ModifyAddressBook(AddressBook* addr_book)
{
assert(addr_book != NULL);
printf("修改联系人!\n");
printf("请输入需要修改的联系人序号:");
int id = 0;
scanf("%d", &id);
if(id < 0 || id >= addr_book->size)
{
printf("输入序号错误!\n");
return;
}
AddressInfo* p = &addr_book->infos[id];
char input[1024] = {0};
printf("请输入要修改的姓名:");
scanf("%s", input);
if(strcmp(input, "#") != 0)
{
strcpy(p->name, input);
}
printf("请输入要修改的电话:");
scanf("%s", input);
if(strcmp(input, "#") != 0)
{
strcpy(p->phone, input);
}
printf("修改成功!\n");
return;
}
//查找
void FindAddressBook(AddressBook* addr_book)
{
assert(addr_book != NULL);
printf("开始进行查找!\n");
printf("请输入要查找的姓名:");
char name[1024] = {0};
scanf("%s", name);
int count = 0;
for(int i = 0; i < addr_book->size; i++)
{
AddressInfo* p = &addr_book->infos[i];
if(strcmp(name, p->name) == 0)
{
printf("[%d] %s\t %s", i, p->name, p->phone);
++count;
}
}
return;
}
//排序
void SortAddressBook(AddressBook* addr_book)
{
assert(addr_book != NULL);
for(int i = 0; i < addr_book->size - 1; i++)//冒泡排序
{
for(int j = 0; j < addr_book->size - i - 1; j++)
{
if(strcmp(addr_book->infos[j].name, addr_book->infos[j + 1].name) > 0)
{
AddressInfo temp = addr_book->infos[j];
addr_book->infos[j] = addr_book->infos[j + 1];
addr_book->infos[j + 1] = temp;
}
}
}
printf("排序完成!\n");
}
//打印全部
void PrintAllAddressBook(AddressBook* addr_book)
{
assert(addr_book != NULL);
printf("显示所有联系人!\n");
for(int i = 0; i < addr_book->size; i++)
{
AddressInfo* p = &addr_book->infos[i];
printf("[%d] %s\t%s\n", i, p->name, p->phone);
}
printf("共显示了%d条数据!\n", addr_book->size);
return;
}
//清除全部
void ClearAllAddressBook(AddressBook* addr_book)
{
assert(addr_book != NULL);
printf("确定要清除全部信息么,确定请输入Y:");
char sure[1024] = {0};
scanf("%s", sure);
if(strcmp(sure, "Y") != 0)
{
printf("清除已经取消!\n");
return;
}
addr_book->size = 0;
return;
}
//文件读取
size_t Read(AddressBook* addr_book)
{
FILE* fp = fopen("./AddrBookData.txt", "r");
if(fp == NULL)
{
fp = fopen("./AddrBookData.txt", "w+");
}
size_t n = 0;
char* buf[1024] = { 0 };
while(fgets(buf, sizeof(buf), fp) != NULL)
{
if(addr_book->size >= addr_book->capacity)
{
Realloc(addr_book);//扩容
}
AddressInfo* p = &addr_book->infos[addr_book->size];
sscanf(buf, "%s %s", p->name, p->phone);
addr_book->size++;
}
fclose(fp);
n = addr_book->size;
printf("读取了%lu条数据!\n", n);
return n;
}
//文件存储
size_t Save(AddressBook* addr_book)
{
FILE* fp = fopen("./AddrBookData.txt", "w");
size_t n = 0;
for(int i = 0; i < addr_book->size; i++)
{
fprintf(fp, "%s %s\n", addr_book->infos[i].name, addr_book->infos[i].phone);
n++;
}
fclose(fp);
printf("存储了%lu条数据!\n", n);
return n;
}
int main()
{
Init(&g_addr_book);
Read(&g_addr_book);
typedef void (*ptr_func)(AddressBook*);
ptr_func table[] = {
AddAddressBook,
DelAddressBook,
ModifyAddressBook,
FindAddressBook,
SortAddressBook,
PrintAllAddressBook,
ClearAllAddressBook,
};
while(1)
{
int choice = Menu();
if(choice == 0)
{
printf("使用完毕,退出!\n");
Save(&g_addr_book);
return 0;
}
table[choice - 1](&g_addr_book);
}
}

【MySQL】第七章-复合查询

发表于 2019-02-13 | 分类于 MySQL
字数统计: 2.8k

第七章 复合查询

  之前我们所讲的查询都是来自于一张表,然后有很多种方式进行查询,但在实际开发中这可能远远不够,因此本章的复合查询就是一些关于多表张表一块进行查询的操作。

多表查询

  本次示例一共用三张表来进行,建表过程省略,以下进行表内容的展示。

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
MariaDB [scott]> show tables;
+-----------------+
| Tables_in_scott |
+-----------------+
| dept |
| emp |
| salgrade |
+-----------------+
3 rows in set (0.00 sec)
MariaDB [scott]> select * from dept;
+--------+------------+----------+
| deptno | dname | loc |
+--------+------------+----------+
| 10 | ACCOUNTING | NEW YORK |
| 20 | RESEARCH | DALLAS |
| 30 | SALES | CHICAGO |
| 40 | OPERATIONS | BOSTON |
+--------+------------+----------+
4 rows in set (0.00 sec)
MariaDB [scott]> select * from emp;
+--------+--------+-----------+------+---------------------+---------+---------+--------+
| empno | ename | job | mgr | hiredate | sal | comm | deptno |
+--------+--------+-----------+------+---------------------+---------+---------+--------+
| 007369 | SMITH | CLERK | 7902 | 1980-12-17 00:00:00 | 800.00 | NULL | 20 |
| 007499 | ALLEN | SALESMAN | 7698 | 1981-02-20 00:00:00 | 1600.00 | 300.00 | 30 |
| 007521 | WARD | SALESMAN | 7698 | 1981-02-22 00:00:00 | 1250.00 | 500.00 | 30 |
| 007566 | JONES | MANAGER | 7839 | 1981-04-02 00:00:00 | 2975.00 | NULL | 20 |
| 007654 | MARTIN | SALESMAN | 7698 | 1981-09-28 00:00:00 | 1250.00 | 1400.00 | 30 |
| 007698 | BLAKE | MANAGER | 7839 | 1981-05-01 00:00:00 | 2850.00 | NULL | 30 |
| 007782 | CLARK | MANAGER | 7839 | 1981-06-09 00:00:00 | 2450.00 | NULL | 10 |
| 007788 | SCOTT | ANALYST | 7566 | 1987-04-19 00:00:00 | 3000.00 | NULL | 20 |
| 007839 | KING | PRESIDENT | NULL | 1981-11-17 00:00:00 | 5000.00 | NULL | 10 |
| 007844 | TURNER | SALESMAN | 7698 | 1981-09-08 00:00:00 | 1500.00 | 0.00 | 30 |
| 007876 | ADAMS | CLERK | 7788 | 1987-05-23 00:00:00 | 1100.00 | NULL | 20 |
| 007900 | JAMES | CLERK | 7698 | 1981-12-03 00:00:00 | 950.00 | NULL | 30 |
| 007902 | FORD | ANALYST | 7566 | 1981-12-03 00:00:00 | 3000.00 | NULL | 20 |
| 007934 | MILLER | CLERK | 7782 | 1982-01-23 00:00:00 | 1300.00 | NULL | 10 |
+--------+--------+-----------+------+---------------------+---------+---------+--------+
14 rows in set (0.00 sec)
MariaDB [scott]> select * from salgrade;
+-------+-------+-------+
| grade | losal | hisal |
+-------+-------+-------+
| 1 | 700 | 1200 |
| 2 | 1201 | 1400 |
| 3 | 1401 | 2000 |
| 4 | 2001 | 3000 |
| 5 | 3001 | 9999 |
+-------+-------+-------+
5 rows in set (0.00 sec)

  接下来我们要显示雇员名、雇员工资以及所在部门的名字,由于这几条信息分布在emp表及dept表中,所以我们需要用到多表查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MariaDB [scott]> select emp.ename, emp.sal, dept.dname from emp, dept where emp.deptno = dept.deptno;
+--------+---------+------------+
| ename | sal | dname |
+--------+---------+------------+
| SMITH | 800.00 | RESEARCH |
| ALLEN | 1600.00 | SALES |
| WARD | 1250.00 | SALES |
| JONES | 2975.00 | RESEARCH |
| MARTIN | 1250.00 | SALES |
| BLAKE | 2850.00 | SALES |
| CLARK | 2450.00 | ACCOUNTING |
| SCOTT | 3000.00 | RESEARCH |
| KING | 5000.00 | ACCOUNTING |
| TURNER | 1500.00 | SALES |
| ADAMS | 1100.00 | RESEARCH |
| JAMES | 950.00 | SALES |
| FORD | 3000.00 | RESEARCH |
| MILLER | 1300.00 | ACCOUNTING |
+--------+---------+------------+
14 rows in set (0.00 sec)

  以上这段指令就是显示了emp表中的ename,sal和dept表中的dname字段,并且在最后加入了限定条件要求emp表中的deptno要和dept表中的deptno值相同,由此即可打印出相匹配的信息。

  那么我们不加限定条件的话会怎样呢?

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
MariaDB [scott]> select emp.ename, emp.sal, dept.dname from emp, dept;
+--------+---------+------------+
| ename | sal | dname |
+--------+---------+------------+
| SMITH | 800.00 | ACCOUNTING |
| SMITH | 800.00 | RESEARCH |
| SMITH | 800.00 | SALES |
| SMITH | 800.00 | OPERATIONS |
| ALLEN | 1600.00 | ACCOUNTING |
| ALLEN | 1600.00 | RESEARCH |
| ALLEN | 1600.00 | SALES |
| ALLEN | 1600.00 | OPERATIONS |
| WARD | 1250.00 | ACCOUNTING |
| WARD | 1250.00 | RESEARCH |
| WARD | 1250.00 | SALES |
| WARD | 1250.00 | OPERATIONS |
| JONES | 2975.00 | ACCOUNTING |
| JONES | 2975.00 | RESEARCH |
| JONES | 2975.00 | SALES |
| JONES | 2975.00 | OPERATIONS |
| MARTIN | 1250.00 | ACCOUNTING |
| MARTIN | 1250.00 | RESEARCH |
| MARTIN | 1250.00 | SALES |
| MARTIN | 1250.00 | OPERATIONS |
| BLAKE | 2850.00 | ACCOUNTING |
| BLAKE | 2850.00 | RESEARCH |
| BLAKE | 2850.00 | SALES |
| BLAKE | 2850.00 | OPERATIONS |
| CLARK | 2450.00 | ACCOUNTING |
| CLARK | 2450.00 | RESEARCH |
| CLARK | 2450.00 | SALES |
| CLARK | 2450.00 | OPERATIONS |
| SCOTT | 3000.00 | ACCOUNTING |
| SCOTT | 3000.00 | RESEARCH |
| SCOTT | 3000.00 | SALES |
| SCOTT | 3000.00 | OPERATIONS |
| KING | 5000.00 | ACCOUNTING |
| KING | 5000.00 | RESEARCH |
| KING | 5000.00 | SALES |
| KING | 5000.00 | OPERATIONS |
| TURNER | 1500.00 | ACCOUNTING |
| TURNER | 1500.00 | RESEARCH |
| TURNER | 1500.00 | SALES |
| TURNER | 1500.00 | OPERATIONS |
| ADAMS | 1100.00 | ACCOUNTING |
| ADAMS | 1100.00 | RESEARCH |
| ADAMS | 1100.00 | SALES |
| ADAMS | 1100.00 | OPERATIONS |
| JAMES | 950.00 | ACCOUNTING |
| JAMES | 950.00 | RESEARCH |
| JAMES | 950.00 | SALES |
| JAMES | 950.00 | OPERATIONS |
| FORD | 3000.00 | ACCOUNTING |
| FORD | 3000.00 | RESEARCH |
| FORD | 3000.00 | SALES |
| FORD | 3000.00 | OPERATIONS |
| MILLER | 1300.00 | ACCOUNTING |
| MILLER | 1300.00 | RESEARCH |
| MILLER | 1300.00 | SALES |
| MILLER | 1300.00 | OPERATIONS |
+--------+---------+------------+
56 rows in set (0.00 sec)

  结果会如上所示,这有点像集合中的笛卡尔积,会将所有记录以此都匹配一遍。以下还有几个例子。

1
2
3
4
5
6
7
8
9
MariaDB [scott]> select ename, sal, dname from emp, dept where emp.deptno = dept.deptno and dept.deptno = 10;
+--------+---------+------------+
| ename | sal | dname |
+--------+---------+------------+
| CLARK | 2450.00 | ACCOUNTING |
| KING | 5000.00 | ACCOUNTING |
| MILLER | 1300.00 | ACCOUNTING |
+--------+---------+------------+
3 rows in set (0.00 sec)

  这个例子是打印部门号为10的部门名,员工和工资。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
MariaDB [scott]> select ename, sal, grade from emp, salgrade where emp.sal between losal and hisal;
+--------+---------+-------+
| ename | sal | grade |
+--------+---------+-------+
| SMITH | 800.00 | 1 |
| ALLEN | 1600.00 | 3 |
| WARD | 1250.00 | 2 |
| JONES | 2975.00 | 4 |
| MARTIN | 1250.00 | 2 |
| BLAKE | 2850.00 | 4 |
| CLARK | 2450.00 | 4 |
| SCOTT | 3000.00 | 4 |
| KING | 5000.00 | 5 |
| TURNER | 1500.00 | 3 |
| ADAMS | 1100.00 | 1 |
| JAMES | 950.00 | 1 |
| FORD | 3000.00 | 4 |
| MILLER | 1300.00 | 2 |
+--------+---------+-------+
14 rows in set (0.00 sec)

  这个例子是打印各个员工的姓名,工资,及工资级别。

自连接

  自连接是在一张表中进行的连接查询

1
2
3
4
5
6
7
MariaDB [scott]> select empno, ename from emp where emp.empno=(select mgr from emp where ename = 'ford');
+--------+-------+
| empno | ename |
+--------+-------+
| 007566 | JONES |
+--------+-------+
1 row in set (0.02 sec)

  这个例子中,我们首先进行了后面括号中的查询,查询到了ename为ford的员工信息的mgr字段信息,然后再进行前半部分查询,查找empno为后半部分查找结果的所有员工的empno和ename信息。所以连起来就是查找了empno与ename为ford的mgr相等的员工的empno及ename值。这里用到了子查询的知识,在后面有详细讲解。

  除了单一表的自连接我们还有多表的自连接,这里需要用到别名的配合。

1
2
3
4
5
6
7
MariaDB [scott]> select leader.empno, leader.ename from emp leader, emp worker where leader.empno = worker.mgr and worker.ename = 'ford';
+--------+-------+
| empno | ename |
+--------+-------+
| 007566 | JONES |
+--------+-------+
1 row in set (0.01 sec)

  这里的功能和第一个例子的功能是一样的,但是用到了两张表,虽然两张表是相同的但是我们通过赋予不同的别名来达到识别不同的表的功能。

子查询

  子查询是指嵌入在其他查询语句中的查询语句,往往能完成多次的更为复杂的查询工作。

单行子查询

  单行子查询只满足查询结果的一个条件,往往最先执行的查询结果也只有一行。

1
2
3
4
5
6
7
8
9
10
11
MariaDB [scott]> select * from emp where deptno = (select deptno from emp where ename = 'smith');
+--------+-------+---------+------+---------------------+---------+------+--------+
| empno | ename | job | mgr | hiredate | sal | comm | deptno |
+--------+-------+---------+------+---------------------+---------+------+--------+
| 007369 | SMITH | CLERK | 7902 | 1980-12-17 00:00:00 | 800.00 | NULL | 20 |
| 007566 | JONES | MANAGER | 7839 | 1981-04-02 00:00:00 | 2975.00 | NULL | 20 |
| 007788 | SCOTT | ANALYST | 7566 | 1987-04-19 00:00:00 | 3000.00 | NULL | 20 |
| 007876 | ADAMS | CLERK | 7788 | 1987-05-23 00:00:00 | 1100.00 | NULL | 20 |
| 007902 | FORD | ANALYST | 7566 | 1981-12-03 00:00:00 | 3000.00 | NULL | 20 |
+--------+-------+---------+------+---------------------+---------+------+--------+
5 rows in set (0.00 sec)

  这个例子是查找了和smith相同部门的员工的信息。这里我这先查找了smith的部门编号,然后找和他编号相同的员工的信息。

多行子查询

  多行子查询往往首先查询的查询结果有很多条,这里需要用到几个关键字来进行条件控制。

  in:表示某某条件其中一条相同即可。

1
2
3
4
5
6
7
8
9
10
11
12
MariaDB [scott]> select ename,job,sal,empno from emp where job in (select distinct job from emp
-> where deptno=10) and deptno<>10;
+-------+---------+---------+--------+
| ename | job | sal | empno |
+-------+---------+---------+--------+
| SMITH | CLERK | 800.00 | 007369 |
| JONES | MANAGER | 2975.00 | 007566 |
| BLAKE | MANAGER | 2850.00 | 007698 |
| ADAMS | CLERK | 1100.00 | 007876 |
| JAMES | CLERK | 950.00 | 007900 |
+-------+---------+---------+--------+
5 rows in set (0.00 sec)

  查询和10号部门的工作相同的雇员的名字,岗位,工资,部门号。(不包括10号部门本身的雇员)
  all:表示全部。

1
2
3
4
5
6
7
8
9
10
MariaDB [scott]> select ename, sal, deptno from emp where sal > all(select sal from emp where deptno=30);
+-------+---------+--------+
| ename | sal | deptno |
+-------+---------+--------+
| JONES | 2975.00 | 20 |
| SCOTT | 3000.00 | 20 |
| KING | 5000.00 | 10 |
| FORD | 3000.00 | 20 |
+-------+---------+--------+
4 rows in set (0.00 sec)

  显示工资比30号部门的所有员工的工资还要搞得员工的姓名、工资和部门号。
  any:表示多个里面选取任意个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
MariaDB [scott]> select ename, sal, deptno from emp where sal > any(select sal from emp where deptno=30);
+--------+---------+--------+
| ename | sal | deptno |
+--------+---------+--------+
| ALLEN | 1600.00 | 30 |
| WARD | 1250.00 | 30 |
| JONES | 2975.00 | 20 |
| MARTIN | 1250.00 | 30 |
| BLAKE | 2850.00 | 30 |
| CLARK | 2450.00 | 10 |
| SCOTT | 3000.00 | 20 |
| KING | 5000.00 | 10 |
| TURNER | 1500.00 | 30 |
| ADAMS | 1100.00 | 20 |
| FORD | 3000.00 | 20 |
| MILLER | 1300.00 | 10 |
+--------+---------+--------+
12 rows in set (0.00 sec)

  显示工资比部门30的任意员工的工资搞得员工的姓名、工资和部门号。

多列子查询

  我们之前的查找无论单行还是多行子查询返回的都只是一列数据,子查询所查找的都是单一字段。那么多列子查询中的子查询所返回就是多行多列的结果。

1
2
3
4
5
6
7
MariaDB [scott]> select ename from emp where (deptno, job)=(select deptno, job from emp where ename='smith') and ename != 'smith';
+-------+
| ename |
+-------+
| ADAMS |
+-------+
1 row in set (0.00 sec)

  这里就是利用了多列子查询做到了多个字段之间匹配的情况,查询出了和smith的部门和岗位完全相同的所有雇员,并且不含smith本人。

在from子句中使用子查询

  我们之前的子查询都是在where子句中进行嵌套查询的。而我们也可以在from子句中使用子查询,这样我们所查找的目标表就是子查询所返回的表。

1
2
3
4
5
6
7
8
9
10
11
12
MariaDB [scott]> select ename, deptno, sal, format(asal,2) from emp,(select avg(sal) asal, deptno dt from emp group by deptno) tmp where emp.sal > tmp.asal and emp.deptno = tmp.dt;
+-------+--------+---------+----------------+
| ename | deptno | sal | format(asal,2) |
+-------+--------+---------+----------------+
| ALLEN | 30 | 1600.00 | 1,566.67 |
| JONES | 20 | 2975.00 | 2,175.00 |
| BLAKE | 30 | 2850.00 | 1,566.67 |
| SCOTT | 20 | 3000.00 | 2,175.00 |
| KING | 10 | 5000.00 | 2,916.67 |
| FORD | 20 | 3000.00 | 2,175.00 |
+-------+--------+---------+----------------+
6 rows in set (0.01 sec)

  显示高于自己部门平均工资的员工的姓名、部门、工资、平均工资。

1
2
3
4
5
6
7
8
9
10
11
MariaDB [scott]> select emp.ename, emp.sal, emp.deptno, ms from emp, (select max(sal) ms, deptno from emp group by deptno) tmp
-> where emp.deptno = tmp.deptno and emp.sal = tmp.ms;
+-------+---------+--------+---------+
| ename | sal | deptno | ms |
+-------+---------+--------+---------+
| BLAKE | 2850.00 | 30 | 2850.00 |
| SCOTT | 3000.00 | 20 | 3000.00 |
| KING | 5000.00 | 10 | 5000.00 |
| FORD | 3000.00 | 20 | 3000.00 |
+-------+---------+--------+---------+
4 rows in set (0.00 sec)

  查找每个部门工资最高的人的姓名、工资、部门、最高工资。

合并查询

  在实际操作中我们往往会遇到需要将多个查询结果合并起来的查询,这样的操作我们成为合并查询。在这里我们要用到两个关键字。
  union:该操作符用于取地两个结果集的并集。当使用该操作符时,会自动去掉结果集中的重复行。

1
2
3
4
5
6
7
8
9
10
11
12
13
MariaDB [scott]> select ename, sal, job from emp where sal > 2500 union
-> select ename, sal, job from emp where job = 'manager';
+-------+---------+-----------+
| ename | sal | job |
+-------+---------+-----------+
| JONES | 2975.00 | MANAGER |
| BLAKE | 2850.00 | MANAGER |
| SCOTT | 3000.00 | ANALYST |
| KING | 5000.00 | PRESIDENT |
| FORD | 3000.00 | ANALYST |
| CLARK | 2450.00 | MANAGER |
+-------+---------+-----------+
6 rows in set (0.00 sec)

  这样就将两个查询结果进行了合并。相当于取了查询结果的并集。
  union all这个操作符是将两个结果集完全合并到一起,不会去掉其中的重复行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MariaDB [scott]> select ename, sal, job from emp where sal > 2500 union all select ename, sal, job from emp where job = 'manager';
+-------+---------+-----------+
| ename | sal | job |
+-------+---------+-----------+
| JONES | 2975.00 | MANAGER |
| BLAKE | 2850.00 | MANAGER |
| SCOTT | 3000.00 | ANALYST |
| KING | 5000.00 | PRESIDENT |
| FORD | 3000.00 | ANALYST |
| JONES | 2975.00 | MANAGER |
| BLAKE | 2850.00 | MANAGER |
| CLARK | 2450.00 | MANAGER |
+-------+---------+-----------+
8 rows in set (0.00 sec)

  和union对比union all并没有去掉两个结果中的重复结果,比如jones的数据就在最终结果中出现了两次。

【C语言】第十一章-自定义类型详解

发表于 2019-02-08 | 分类于 C语言进阶
字数统计: 2.6k

第十一章-自定义类型详解

结构体

  结构体的基本知识在我之前C语言初阶的部分已经有详细说明过了,这里知识稍微体积之后便进入更为深层的方面。

结构体的声明

  结构体声明的声明及使用是十分简单且方便的,使用以下的语法我们即可声明一个自定义的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
int a;
char b[1024];
}Test;
int main()
{
Test test;
test.a = 10;
strcpy(test.b, "Main");
printf("%d, %s\n", test.a, test.b);
}


[misaki@localhost test]$ ./Main
10, Main

匿名结构体

  但是有一种较为特殊的结构体,这种结构体没有名称,因此它在声明结束后无法再次定义变量,只能一次性使用,我们称这种结构体为匿名结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct
{
int a;
}test;
int main()
{
test.a = 10;
printf("%d\n",test.a);
}


[misaki@localhost test]$ ./Main
10

  这种匿名结构体只能供我们一次性使用,声明结束后不能再定义其他的结构体变量。

结构体的自引用

  结构体的自引用一般大多数用在链表,图,树等数据结构中,之后在数据结构有关章节中我们会经常用到链式存储结构,这时候就要用到我们的结构体的自引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
int a;
char b[10];
struct test* next;//一般用于指向下一个相同类型的结构体
}Test;
int main()
{
Test test;
}

  在这个结构体的定义中我们就将结构体本身的指针包含进了结构体中,这个在链式存储中一般用于指向下一个结构体起到寻址的作用,但是我们要注意在自引用中只能包含其本身的指针,不能直接自引用,否则将是不合法的。

结构体内存对齐

  这个部分是结构体中十分重要的一个知识点,在面试和笔试中也是常考点。
  我们之前计算结构体大小都是将结构体各成员变量大小之和相加就得出了结构体的总大小,但是我们看接下来这个例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
char c1;
short a;
char c2;
}Test;
int main()
{
Test test;
printf("%lu\n", sizeof(test));
}


[misaki@localhost test]$ ./Main
6

  从以上这个例子可以看出这个结构体占了6个字节,但是个成员加起来应该就是4个字节啊,这是为什么呢?

  其实结构体大小的计算与内存对齐有着很大的关系,以下是内存对齐的基本规则:

  1. 第一个成员在与结构体变量偏移量为0的地址处。

  2. 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。 VS中默认的值为8 Linux中的默认值为4。

  3. 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。

   4. 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是
所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

  接下来我们用以上的规则来计算一下刚才这个结构体的总大小。首先char类型放在地址为0的地方,占一个字节。short的大小为2,我的环境是Linux所以默认对齐数为4,short小于它所以short对齐为2,于是要放在地址为2的整数倍的地方,于是舍弃1这个地址放在2处,占两个字节,此时的总大小为1 + 1(补齐)+ 2 = 4。之后还要再放一个char同理得对齐数为1,放在任意地址处即可,于是放在5处,占一个字节。由此所有变量都放完了,但是根据规则中第三条我们还要让总大小为最大对齐数得整数倍,在这个结构体中最大对齐数为short的对齐数,为2,于是此时末尾还要再补齐一个字节,于是总大小为1 + 1(补齐)+ 2 + 1(补齐) = 6。由此这个结构体的大小才算是真正得出。

  但是为什么会出现内存对齐这个规定呢?
这是为了某些平台的原因及性能的原因,其中牵扯到更为底层的知识,为了性能和平台牺牲这点内存换来更高的效率是完全值得的。

  接下来还有几个例子以供练习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
char c1;
int a;
char c2;
}Test;
int main()
{
Test test;
printf("%lu\n", sizeof(test));
}


[misaki@localhost test]$ ./Main
12

  1 + 3(补齐)+ 4 + 1 + 3(补齐) = 12。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
char c1;
char c2;
int a;
}Test;
int main()
{
Test test;
printf("%lu\n", sizeof(test));
}


[misaki@localhost test]$ ./Main
8

  1 + 1 + 2(补齐) + 4 = 8。

  从以上这个例子可以看出相同的成员变量就连不同的声明顺序也会导致结构体大小的不同。

  当然我们也可以修改系统默认的对齐数,通过一个宏即可。

          #pragma pack(对齐数)

  这样即可修改默认的对齐数。

位段

  位段是规定结构体中各部分成员所占空间的一种语法,由此来达到节省占用空间的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct test
{
int a : 2;//只占两位
int b : 5;//只占五位
}Test;
int main()
{
Test test;
test.a = 3;
test.b = 31;
printf("%d,%d\n", test.a, test.b);
}


[misaki@localhost test]$ ./Main
-1,-1

  我们只要给他们赋予超过存储空间的值就会出现问题。

枚举

  枚举类型是一种类似于用宏定义常量的自定义类型。

枚举的定义

  枚举定义与结构体的声明十分类似,不过我们要谨记中间的每个常量要用,隔开。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum Sex
{
MALE,
FEMALE,
UNKNOWN,
};
int main()
{
printf("MALE = %d,FEMALE = %d, UNKNOWN = %d\n", MALE, FEMALE, UNKNOWN);
}

  这样我们就定义了一个枚举类型,其中的每个变量都是常量,我们可以直接使用。如果我们并没有给他们进行赋值的话他们会从上到下从0开始一次增长1自动赋值,但是如果我们给钱一个数赋值了的话它们会从前一个数的值开始依次增长1自动赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum Sex
{
MALE = 2,
FEMALE,
UNKNOWN,
};
int main()
{
printf("MALE = %d,FEMALE = %d, UNKNOWN = %d\n", MALE, FEMALE, UNKNOWN);
}


[misaki@localhost test]$ ./Main
MALE = 2,FEMALE = 3, UNKNOWN = 4

  同时我们还可以用枚举类型定义变量,这样定义的变量的值就必须是枚举常量中出现的常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
enum Sex
{
MALE,
FEMALE,
UNKNOWN,
};
int main()
{
enum Sex sex = MALE;
printf("sex = %d\n", sex);
}


[misaki@localhost test]$ ./Main
sex = 0

  这样定义的变量就不得不使用枚举中的常量,否则会进行报错,多了一步验证。

为什么使用枚举

  1、枚举类型有验证检查,更加严谨。

  2、枚举类型一次可以定义多个常量,十分方便。

  3、枚举类型可以防止命名污染。

联合体

  联合体也叫共用体,是一种较为特殊的自定义类型。

声明与使用

  联合体与结构体体定义起来的语法完全一致,但他们有一点不同的地方就在于,结构体声明了很多个成员变量,在使用的时候可以同时使用里面所有的成员变量,但联合体则更倾向于多选一,定义了很多的成员变量,但是只能使用其中一个,而且其大小也是根据其中最大的成员变量决定的,因此联合体是一个很抠门的类型,但是它也可以有效节省空间,在一些情况下有很大作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
union Un
{
char c;
int i;
};
int main()
{
union Un un;
printf("%lu\n",sizeof(un));
un.i = 4;
un.c = 'a';
printf("%d\n", un.i);
}


[misaki@localhost test]$ ./Main
4
97

  从以上这个例子可以看出共用体所占空间的确与结构体是不一样的,并且当我们同时给两个成员变量赋值时另一个成员变量会扰乱其他成员变量的赋值,由此可见它们确实是存储在同一块内存空间上的。

  其实共用体除了节省空间这一项优点上其实它有很多其它作用,可以帮助我们很方便的完成一些不用共用体就很难做到的东西,这都和它独特的特性有关。

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
union Ip
{
uint32_t a;
struct
{
char d1;
char d2;
char d3;
char d4;
};
};
int main()
{
union Ip ip;
ip.a = 0x1;
printf("%d.%d.%d.%d\n", ip.d1,ip.d2,ip.d3,ip.d4);
}


[misaki@localhost test]$ ./Main
1.0.0.0

  用这种方式则可以轻松做到ip地址间两种表示形式的转换,同时用联合体还可以做到我们之前写过的大小端的验证。

【MySQL】第六章-表的增删改查

发表于 2019-02-06 | 分类于 MySQL
字数统计: 3.6k

第六章 表的增删改查

  本章开始介绍表的增删改查,是数据库操作中最为重要的部分,尤其是数据查找,基本数据库的所有操作的核心都在于查找。

插入

  在建表过后我们就要进行数据的插入,在此我们可以进行数据的全列插入也可以进行多行数据的指定列插入,这里有最常用的几种语法,接下来开始分别介绍。首先我们先建一个表,以这个表为例进行举例。

1
2
3
4
5
6
7
MariaDB [student]> create table student 
-> (
-> id int primary key auto_increment,
-> name varchar(20) not null,
-> class_id int
-> );
Query OK, 0 rows affected (0.00 sec)

单行整列插入

  接下来我们对整列数据都进行插入,一次只插入一行。

1
2
3
4
5
MariaDB [student]> insert into student values(1, '钢铁侠', 1000);
Query OK, 1 row affected (0.08 sec)

MariaDB [student]> insert into student values(2, '擎天柱', 1001);
Query OK, 1 row affected (0.00 sec)

  然后我们打印一下整表的数据看下结果。

1
2
3
4
5
6
7
8
MariaDB [student]> select * from student;
+----+-----------+----------+
| id | name | class_id |
+----+-----------+----------+
| 1 | 钢铁侠 | 1000 |
| 2 | 擎天柱 | 1001 |
+----+-----------+----------+
2 rows in set (0.00 sec)

  我们已经将数据插入进去了。

多行指定列插入

  当我们想要一次插入多行并且指定特殊列给定数据时就可以使用以下的语法,并且未赋值的列会自动赋予默认值。

1
2
3
MariaDB [student]> insert into student(name,class_id) values('大黄蜂',1003),('蜘蛛侠',1004);
Query OK, 2 rows affected (0.00 sec)
Records: 2 Duplicates: 0 Warnings: 0

  查看一下数据。

1
2
3
4
5
6
7
8
9
10
MariaDB [student]> select * from student;
+----+-----------+----------+
| id | name | class_id |
+----+-----------+----------+
| 1 | 钢铁侠 | 1000 |
| 2 | 擎天柱 | 1001 |
| 3 | 大黄蜂 | 1003 |
| 4 | 蜘蛛侠 | 1004 |
+----+-----------+----------+
4 rows in set (0.00 sec)

  可以看到数据已经插入,并且插入了两行,由于我对id字段设置了自增长,于是即使我不给它值也会自动帮我们填充。

插入否则更新

  这种插入方式针对的是当我们设置的主键或者唯一键产生冲突时形成替代方案的做法。当我们插入一个数据如果数据中的主键或者唯一键与已经存在的数据冲突我们就会对其进行数据更新,至于更新哪些数据都是由我们来指定的。

1
2
3
MariaDB [student]> insert into student values(1, '奇异博士', 1000)
-> on duplicate key update name = '奇异博士', class_id = 1000;
Query OK, 2 rows affected (0.00 sec)

  打印当前数据。

1
2
3
4
5
6
7
8
9
10
MariaDB [student]> select * from student;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
| 2 | 擎天柱 | 1001 |
| 3 | 大黄蜂 | 1003 |
| 4 | 蜘蛛侠 | 1004 |
+----+--------------+----------+
4 rows in set (0.00 sec)

  由于我们擦汗如的数据主键冲突我们将目标主键那一行其他字段的值进行了修改,当然如果没有冲突的话就会整行插入新的数据了。不过此时要注意我们更新不可更新主键或唯一键的值。

1
2
MariaDB [student]> insert into student values(1, '奇异博士', 1000) on duplicate key update id = 2, name = '奇异博士', class_id = 1000;
ERROR 1062 (23000): Duplicate entry '2' for key 'PRIMARY'

替换

  这种语法也是针对处理主键或唯一键的冲突数据的,如果插入的数据不产生冲突则直接插入,如果产生冲突则完全替换原数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
MariaDB [student]> replace into student values(2, '绿巨人', 1001);
Query OK, 2 rows affected (0.01 sec)

MariaDB [student]> select * from student;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
| 2 | 绿巨人 | 1001 |
| 3 | 大黄蜂 | 1003 |
| 4 | 蜘蛛侠 | 1004 |
+----+--------------+----------+
4 rows in set (0.00 sec)

  以上这几种插入语法都十分简单,最为常用的是前两种插入,我们之前也有大量使用,掌握起来也很简单。

查找

  查找是本章的重点,也是数据库最为常用的操作。

全列查询

  全列查询我们之前经常使用,语法及案例如下。

1
2
3
4
5
6
7
8
9
10
MariaDB [student]> select * from student;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
| 2 | 绿巨人 | 1001 |
| 3 | 大黄蜂 | 1003 |
| 4 | 蜘蛛侠 | 1004 |
+----+--------------+----------+
4 rows in set (0.00 sec)

指定列查询

  如果我们只想查询表中指定字段的数据,则可以使用以下的语法。

1
2
3
4
5
6
7
8
9
10
MariaDB [student]> select id, name from student;
+----+--------------+
| id | name |
+----+--------------+
| 1 | 奇异博士 |
| 2 | 绿巨人 |
| 3 | 大黄蜂 |
| 4 | 蜘蛛侠 |
+----+--------------+
4 rows in set (0.00 sec)

查询字段为表达式

  我们可以将数据进行一些处理之后再进行查询,比如我们将id字段整体加1之后再打印。

1
2
3
4
5
6
7
8
9
10
MariaDB [student]> select id + 1, name, class_id from student;
+--------+--------------+----------+
| id + 1 | name | class_id |
+--------+--------------+----------+
| 2 | 奇异博士 | 1000 |
| 3 | 绿巨人 | 1001 |
| 4 | 大黄蜂 | 1003 |
| 5 | 蜘蛛侠 | 1004 |
+--------+--------------+----------+
4 rows in set (0.00 sec)

  我们甚至可以直接查询表达式。

1
2
3
4
5
6
7
8
9
10
MariaDB [student]> select id, name, class_id, 10 from student;
+----+--------------+----------+----+
| id | name | class_id | 10 |
+----+--------------+----------+----+
| 1 | 奇异博士 | 1000 | 10 |
| 2 | 绿巨人 | 1001 | 10 |
| 3 | 大黄蜂 | 1003 | 10 |
| 4 | 蜘蛛侠 | 1004 | 10 |
+----+--------------+----------+----+
4 rows in set (0.00 sec)

  我们还可以将多个字段进行操作之后打印。

1
2
3
4
5
6
7
8
9
10
MariaDB [student]> select id, name, class_id + id from student;
+----+--------------+---------------+
| id | name | class_id + id |
+----+--------------+---------------+
| 1 | 奇异博士 | 1001 |
| 2 | 绿巨人 | 1003 |
| 3 | 大黄蜂 | 1006 |
| 4 | 蜘蛛侠 | 1008 |
+----+--------------+---------------+
4 rows in set (0.01 sec)

  如果我们觉得列名太长的话我们可以使用as语法指定别名。

1
2
3
4
5
6
7
8
9
10
MariaDB [student]> select id, name, class_id + id as 表达式 from student;
+----+--------------+-----------+
| id | name | 表达式 |
+----+--------------+-----------+
| 1 | 奇异博士 | 1001 |
| 2 | 绿巨人 | 1003 |
| 3 | 大黄蜂 | 1006 |
| 4 | 蜘蛛侠 | 1008 |
+----+--------------+-----------+
4 rows in set (0.00 sec)

结果去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
MariaDB [student]> insert into student(name, class_id) values('葫芦娃', 1000);
Query OK, 1 row affected (0.00 sec)

MariaDB [student]> select * from student;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
| 2 | 绿巨人 | 1001 |
| 3 | 大黄蜂 | 1003 |
| 4 | 蜘蛛侠 | 1004 |
| 5 | 葫芦娃 | 1000 |
+----+--------------+----------+
5 rows in set (0.00 sec)

  我们插入了一条新的数据,之后我们发现葫芦娃的班级和奇异博士的班级重复了,我想要去重,该怎么办呢。

1
2
3
4
5
6
7
8
9
10
MariaDB [student]> select distinct class_id from student;
+----------+
| class_id |
+----------+
| 1000 |
| 1001 |
| 1003 |
| 1004 |
+----------+
4 rows in set (0.00 sec)

  加入distinct关键字即可去重,不过我们要注意,只有打印出来的字段完全一致才会进行去重,当我们如果打印了多个字段,但是两行数据间各字段的值并不都是相同的则不会去重。

1
2
3
4
5
6
7
8
9
10
11
MariaDB [student]> select distinct name, class_id from student;
+--------------+----------+
| name | class_id |
+--------------+----------+
| 奇异博士 | 1000 |
| 绿巨人 | 1001 |
| 大黄蜂 | 1003 |
| 蜘蛛侠 | 1004 |
| 葫芦娃 | 1000 |
+--------------+----------+
5 rows in set (0.00 sec)

where条件

  如果说查询是数据库中最重要操作,那么where条件则是查询中最重要的语法。所谓where条件不过是再查询中加入条件,查询目标要求的数据。

操作符

  在where条件中我们也许会用到以上的操作符,接下来举几个例子。

1
2
3
4
5
6
7
8
MariaDB [student]> select * from student where class_id = 1000;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
| 5 | 葫芦娃 | 1000 |
+----+--------------+----------+
2 rows in set (0.00 sec)

  这个案例是查找class_id为1000的学生。

1
2
3
4
5
6
7
8
9
MariaDB [student]> select * from student where class_id = 1000 or class_id = 1001;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
| 2 | 绿巨人 | 1001 |
| 5 | 葫芦娃 | 1000 |
+----+--------------+----------+
3 rows in set (0.00 sec)

  这个案例是查找class_id为1000或者为1001的学生。

1
2
3
4
5
6
7
8
9
MariaDB [student]> select * from student where id between 1 and 3;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
| 2 | 绿巨人 | 1001 |
| 3 | 大黄蜂 | 1003 |
+----+--------------+----------+
3 rows in set (0.00 sec)

  这个案例是查找id在1和3之间的学生。

1
2
3
4
5
6
7
8
9
MariaDB [student]> select * from student where id in (2,3,5,6);
+----+-----------+----------+
| id | name | class_id |
+----+-----------+----------+
| 2 | 绿巨人 | 1001 |
| 3 | 大黄蜂 | 1003 |
| 5 | 葫芦娃 | 1000 |
+----+-----------+----------+
3 rows in set (0.00 sec)

  这个案例是查找id为2或者3或者5或者6的学生。

1
2
3
4
5
6
7
MariaDB [student]> select * from student where name like '奇%';
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
+----+--------------+----------+
1 row in set (0.00 sec)

  这个案例是查找姓名开头为奇的学生,这里用到了模糊查找,%则表示匹配任意多个包括0个字符。

1
2
3
4
5
6
7
MariaDB [student]> select * from student where name like '奇___';
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
+----+--------------+----------+
1 row in set (0.00 sec)

  而这个案例中的模糊查找用到了_则严格匹配指定数量的字符,如果我们在后面只打一个呢么是查找不到的。

1
2
MariaDB [student]> select * from student where name like '奇_';
Empty set (0.00 sec)

  关于where的操作就先说到这里,这个操作十分灵活,还需要在实践中去拓宽。

结果排序

  我们查询的结果打印出来往往是无序的,是根据插入的先后进行打印的,因此我们需要对数据进行排序,这就要用到接下来的语法。

1
2
3
4
5
6
7
8
9
10
11
MariaDB [student]> select * from student order by class_id;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
| 5 | 葫芦娃 | 1000 |
| 2 | 绿巨人 | 1001 |
| 3 | 大黄蜂 | 1003 |
| 4 | 蜘蛛侠 | 1004 |
+----+--------------+----------+
5 rows in set (0.00 sec)

  默认的排序顺序是升序的我们可以通过加入DESC改变为降序。

1
2
3
4
5
6
7
8
9
10
11
MariaDB [student]> select * from student order by class_id desc;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 4 | 蜘蛛侠 | 1004 |
| 3 | 大黄蜂 | 1003 |
| 2 | 绿巨人 | 1001 |
| 1 | 奇异博士 | 1000 |
| 5 | 葫芦娃 | 1000 |
+----+--------------+----------+
5 rows in set (0.01 sec)

  这里要注意的是NULL被视为最小的值,同时如果查询没有加入order by那么这个数据就是无序的,这点不要混淆。

  同时排序中可以按照表达式排序,也可以使用别名。

1
2
3
4
5
6
7
8
9
10
11
MariaDB [student]> select id, name, class_id, id + class_id as 表达式 from student order by 表达式;
+----+--------------+----------+-----------+
| id | name | class_id | 表达式 |
+----+--------------+----------+-----------+
| 1 | 奇异博士 | 1000 | 1001 |
| 2 | 绿巨人 | 1001 | 1003 |
| 5 | 葫芦娃 | 1000 | 1005 |
| 3 | 大黄蜂 | 1003 | 1006 |
| 4 | 蜘蛛侠 | 1004 | 1008 |
+----+--------------+----------+-----------+
5 rows in set (0.00 sec)

筛选分页结果

  我们在对一个表进行查询时有时表中数据会很多,一次查询会导致卡死,于是我们可以进行筛选分页,分波查询。

1
2
3
4
5
6
7
8
9
MariaDB [student]> select * from student order by id limit 3 offset 0;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 奇异博士 | 1000 |
| 2 | 绿巨人 | 1001 |
| 3 | 大黄蜂 | 1003 |
+----+--------------+----------+
3 rows in set (0.01 sec)

  以上的操作我们将数据按id字段进行排序,并且分页显示,一页三个,以上我们打印了第一页。

1
2
3
4
5
6
7
8
MariaDB [student]> select * from student order by id limit 3 offset 3;
+----+-----------+----------+
| id | name | class_id |
+----+-----------+----------+
| 4 | 蜘蛛侠 | 1004 |
| 5 | 葫芦娃 | 1000 |
+----+-----------+----------+
2 rows in set (0.00 sec)

  由于第二页不足三个,但是打印不会有影响。

修改

  说完查找,修改和删除就显得简单多了,首先是修改。

1
2
3
4
5
6
7
MariaDB [student]> select id, name from student where id = 1;
+----+--------------+
| id | name |
+----+--------------+
| 1 | 奇异博士 |
+----+--------------+
1 row in set (0.01 sec)

  我现在想要把id为1的学生名字改成钢铁侠,可以使用以下的语法。

1
2
3
4
5
6
7
8
9
10
11
MariaDB [student]> update student set name = '钢铁侠' where id = 1;
Query OK, 1 row affected (0.09 sec)
Rows matched: 1 Changed: 1 Warnings: 0

MariaDB [student]> select id, name from student where id = 1;
+----+-----------+
| id | name |
+----+-----------+
| 1 | 钢铁侠 |
+----+-----------+
1 row in set (0.00 sec)

  这样就已经修改完成了,这个语法是最为常用的修改语法,可以修改单个数据,也可以修改整表数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MariaDB [student]> update student set class_id = class_id + 1;
Query OK, 5 rows affected (0.02 sec)
Rows matched: 5 Changed: 5 Warnings: 0

MariaDB [student]> select * from student;
+----+-----------+----------+
| id | name | class_id |
+----+-----------+----------+
| 1 | 钢铁侠 | 1001 |
| 2 | 绿巨人 | 1002 |
| 3 | 大黄蜂 | 1004 |
| 4 | 蜘蛛侠 | 1005 |
| 5 | 葫芦娃 | 1001 |
+----+-----------+----------+
5 rows in set (0.01 sec)

  不过要注意修改全表的语句要慎用。

删除

  删除的语法也很简单,删除单行。

1
2
3
4
5
6
7
8
9
10
11
12
13
MariaDB [student]> delete from student where id = 5;
Query OK, 1 row affected (0.00 sec)

MariaDB [student]> select * from student;
+----+-----------+----------+
| id | name | class_id |
+----+-----------+----------+
| 1 | 钢铁侠 | 1001 |
| 2 | 绿巨人 | 1002 |
| 3 | 大黄蜂 | 1004 |
| 4 | 蜘蛛侠 | 1005 |
+----+-----------+----------+
4 rows in set (0.00 sec)

  这样就删除了单行的数据,同时也可以删除整表的数据,但同时要慎用。

1
2
3
4
5
MariaDB [student]> delete from student;
Query OK, 4 rows affected (0.00 sec)

MariaDB [student]> select * from student;
Empty set (0.00 sec)

  但是删除整表数据不会重置auto_increment数值。
  相比于删除整表还有一种截断表的操作,起作用也可以删除整表数据,但是其只能对整表操作,并且由于不会对数据进行操作所以速度更快,并且会重置auto_increment值。

1
2
3
4
5
6
7
8
9
10
11
12
MariaDB [student]> insert into student(name, class_id) values('关羽',1003),('刘备',1004);
Query OK, 2 rows affected (0.01 sec)
Records: 2 Duplicates: 0 Warnings: 0

MariaDB [student]> select * from student;
+----+--------+----------+
| id | name | class_id |
+----+--------+----------+
| 6 | 关羽 | 1003 |
| 7 | 刘备 | 1004 |
+----+--------+----------+
2 rows in set (0.00 sec)

  我们接着上次删除的表继续插入数据,自增长从6开始,之后我们截断表。

1
2
3
4
5
MariaDB [student]> truncate student;
Query OK, 0 rows affected (0.00 sec)

MariaDB [student]> select * from student;
Empty set (0.00 sec)

  数据也同样被清楚了,但是auto_increment也同时被重置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MariaDB [student]> insert into student(name, class_id) values('钢铁侠',1000);
Query OK, 1 row affected (0.00 sec)

MariaDB [student]> select * from student;
+----+-----------+----------+
| id | name | class_id |
+----+-----------+----------+
| 1 | 钢铁侠 | 1000 |
+----+-----------+----------+
1 row in set (0.00 sec)

MariaDB [student]> show create table student;
+---------+----------------------------------------------------------------------------------------------------------------------------------------------------+
| Table | Create Table |
+---------+----------------------------------------------------------------------------------------------------------------------------------------------------+
| student | CREATE TABLE `student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL,
`class_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 |
+---------+----------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

聚合函数

聚合函数

  这些就是常用的聚合函数,接下来我们举几个例子看下他们的实际用法。

1
2
3
4
5
6
7
8
9
10
11
MariaDB [student]> select * from student;
+----+--------------+----------+
| id | name | class_id |
+----+--------------+----------+
| 1 | 钢铁侠 | 1000 |
| 2 | 蜘蛛侠 | 1001 |
| 3 | 绿巨人 | 1002 |
| 4 | 奇异博士 | 1003 |
| 5 | 美国队长 | 1000 |
+----+--------------+----------+
5 rows in set (0.00 sec)

  我们先向表中插入测试数据。

1
2
3
4
5
6
7
MariaDB [student]> select count(id) from student;
+-----------+
| count(id) |
+-----------+
| 5 |
+-----------+
1 row in set (0.00 sec)

  返回id数据的数量,不会将NULL包含进去。

1
2
3
4
5
6
7
MariaDB [student]> select sum(id) from student;
+---------+
| sum(id) |
+---------+
| 15 |
+---------+
1 row in set (0.00 sec)

  显示id的和。

1
2
3
4
5
6
7
MariaDB [student]> select avg(id) from student;
+---------+
| avg(id) |
+---------+
| 3.0000 |
+---------+
1 row in set (0.00 sec)

  显示id的平均值。

group by语句

  group by是分组语句,用于按照某一数据进行分组计算打印。

1
2
3
4
5
6
7
8
9
10
MariaDB [student]> select avg(id), name, class_id from student group by class_id;
+---------+--------------+----------+
| avg(id) | name | class_id |
+---------+--------------+----------+
| 3.0000 | 钢铁侠 | 1000 |
| 2.0000 | 蜘蛛侠 | 1001 |
| 3.0000 | 绿巨人 | 1002 |
| 4.0000 | 奇异博士 | 1003 |
+---------+--------------+----------+
4 rows in set (0.00 sec)

  以上的就是按照class_id进行分组,然后打印出各班级的平均id的值。

1…567…9
MisakiFx

MisakiFx

Hard working or giving up!!!

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

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