系统相关命令

时间和日期

序号 命令 作用
01 date 查看系统时间
02 cal calendar 查看日历,-y 选项可以查看一年的日历

磁盘信息

序号 命令 作用
01 df -h disk free 显示磁盘剩余空间
02 du -h [目录名] disk usage 显示目录下的文件大小
  • 选项说明
参数 含义
-h 以人性化的方式显示文件大小

进程信息

  • 所谓 进程,通俗地说就是 当前正在执行的一个程序
序号 命令 作用
01 ps aux process status 查看进程的详细状况
02 top 动态显示运行中的进程并且排序
03 htop 更方便的动态显示运行中的进程并且排序
04 kill [-9] 进程代号 终止指定代号的进程,-9 表示强行终止

ps 默认只会显示当前用户通过终端启动的应用程序

  • ps 选项说明功能
选项 含义
a 显示终端上的所有进程,包括其他用户的进程
u 显示进程的详细状态
x 显示没有控制终端的进程

提示:使用 kill 命令时,最好只终止由当前用户开启的进程,而不要终止 root 身份开启的进程,否则可能导致系统崩溃

  • 要退出 top 可以直接输入 q

用户权限相关命令

基本概念

  • 用户 是 Linux 系统工作中重要的一环,用户管理包括 用户 管理
  • 在 Linux 系统中,不论是由本机或是远程登录系统,每个系统都必须拥有一个账号,并且对于不同的系统资源拥有不同的使用权限
  • 在 Linux 中,可以指定 每一个用户 针对 不同的文件或者目录不同权限
  • 文件/目录 的权限包括:
序号 权限 英文 缩写 数字代号
01 read r 4
02 write w 2
03 执行 excute x 1

  • 为了方便用户管理,提出了 的概念

  • 在实际应用中,可以预先针对 设置好权限,然后 将不同的用户添加到对应的组中,从而不用依次为每一个用户设置权限

ls -l 扩展

  • ls -l 可以查看文件夹下文件的详细信息,从左到右依次是:
    • 权限,第 1 个字符如果是 d 表示目录
    • 硬链接数,通俗地讲,就是有多少种方式,可以访问到当前目录/文件
    • 拥有者,家目录下 文件/目录 的拥有者通常都是当前用户
    • ,在 Linux 中,很多时候,会出现组名和用户名相同的情况,后续会讲
    • 大小
    • 时间
    • 名称

权限示意图

chmod 简单使用

  • chmod 可以修改 用户/组文件/目录 的权限
  • 命令格式如下:
1
chmod +/-rwx 文件名|目录名

提示:以上方式会一次性修改 拥有者 / 权限

超级用户

  • Linux 系统中的 root 账号通常 用于系统的维护和管理,对操作系统的所有资源 具有所有访问权限
  • 在大多数版本的 Linux 中,都不推荐 直接使用 root 账号登录系统
  • 在 Linux 安装的过程中,系统会自动创建一个用户账号,而这个默认的用户就称为“标准用户”

sudo

  • susubstitute user 的缩写,表示 使用另一个用户的身份
  • sudo 命令用来以其他身份来执行命令,预设的身份为 root
  • 用户使用 sudo 时,必须先输入密码,之后有 5 分钟的有效期限,超过期限则必须重新输入密码

若其未经授权的用户企图使用 sudo,则会发出警告邮件给管理员

组管理 终端命令

提示:创建组 / 删除组 的终端命令都需要通过 sudo 执行

序号 命令 作用
01 groupadd 组名 添加组
02 groupdel 组名 删除组
03 cat /etc/group 确认组信息
04 chgrp -R 组名 文件/目录名 递归修改文件/目录的所属组

提示:

  • 组信息保存在 /etc/group 文件中
  • /etc 目录是专门用来保存 系统配置信息 的目录
  • 在实际应用中,可以预先针对 设置好权限,然后 将不同的用户添加到对应的组中,从而不用依次为每一个用户设置权限

用户管理 终端命令

提示:创建用户 / 删除用户 / 修改其他用户密码 的终端命令都需要通过 sudo 执行

创建用户/设置密码/删除用户
序号 命令 作用 说明
01 useradd -m -g 组 新建用户名 添加新用户 -m 自动建立用户家目录-g 指定用户所在的组,否则会建立一个和同名的组
02 passwd 用户名 设置用户密码 如果是普通用户,直接用 passwd 可以修改自己的账户密码
03 userdel -r 用户名 删除用户 -r 选项会自动删除用户家目录
04 cat /etc/passwd ` ` grep 用户名 确认用户信息 新建用户后,用户信息会保存在 /etc/passwd 文件中

提示:

  • 创建用户时,如果忘记添加 -m 选项指定新用户的家目录 —— 最简单的方法就是删除用户,重新创建
  • 创建用户时,默认会创建一个和用户名同名的组名
  • 用户信息保存在 /etc/passwd 文件中
查看用户信息
序号 命令 作用
01 id [用户名] 查看用户 UID 和 GID 信息
02 who 查看当前所有登录的用户列表
03 whoami 查看当前登录用户的账户名
passwd 文件

/etc/passwd 文件存放的是用户的信息,由 6 个分号组成的 7 个信息,分别是

  1. 用户名
  2. 密码(x,表示加密的密码)
  3. UID(用户标识)
  4. GID(组标识)
  5. 用户全名或本地帐号
  6. 家目录
  7. 登录使用的 Shell,就是登录之后,使用的终端命令,ubuntu 默认用 dash
usermod
  • usermod 可以用来设置 用户主组附加组登录 Shell,命令格式如下:
    • 主组:通常在新建用户时指定,在 etc/passwd 的第 4 列 GID 对应的组
    • 附加组:在 etc/group 中最后一列表示该组的用户列表,用于指定 用户的附加权限

提示:设置了用户的附加组之后,需要重新登录才能生效!

1
2
3
4
5
6
7
8
# 修改用户的主组(passwd 中的 GID)
usermod -g 组 用户名

# 修改用户的附加组
usermod -G 组 用户名

# 修改用户登录 Shell
usermod -s /bin/bash 用户名

注意:默认使用 useradd 添加的用户是没有权限使用 sudoroot 身份执行命令的,可以使用以下命令,将用户添加到 sudo 附加组中

1
usermod -G sudo 用户名
which

提示

  • /etc/passwd 是用于保存用户信息的文件
  • /usr/bin/passwd 是用于修改用户密码的程序
  • which 命令可以查看执行命令所在位置,例如:
1
2
3
4
5
6
7
8
9
which ls

# 输出
# /bin/ls

which useradd

# 输出
# /usr/sbin/useradd
binsbin
  • Linux 中,绝大多数可执行文件都是保存在 /bin/sbin/usr/bin/usr/sbin
  • /binbinary)是二进制执行文件目录,主要用于具体应用
  • /sbinsystem binary)是系统管理员专用的二进制代码存放目录,主要用于系统管理
  • /usr/binuser commands for applications)后期安装的一些软件
  • /usr/sbinsuper user commands for applications)超级用户的一些管理程序

提示:

  • cd 终端命令是内置在系统内核中的,没有独立的文件,因此用 which 无法找到 cd 命令的位置
切换用户
序号 命令 作用 说明
01 su - 用户名 切换用户,并且切换目录 - 可以切换到用户家目录,否则保持位置不变
02 exit 退出当前登录账户
  • su 不接用户名,可以切换到 root,但是不推荐使用,因为不安全
  • exit 示意图如下:

su和exit示意图

修改文件权限
序号 命令 作用
01 chown 修改拥有者
02 chgrp 修改组
03 chmod 修改权限
  • 命令格式如下:
1
2
3
4
5
6
7
8
# 修改文件|目录的拥有者
chown 用户名 文件名|目录名

# 递归修改文件|目录的组
chgrp -R 组名 文件名|目录名

# 递归修改文件权限
chmod -R 755 文件名|目录名
  • chmod 在设置权限时,可以简单地使用三个数字分别对应 拥有者其他 用户的权限
1
2
# 直接修改文件|目录的 读|写|执行 权限,但是不能精确到 拥有者|组|其他
chmod +/-rwx 文件名|目录名

文件权限示意图

  • 常见数字组合有(u表示用户/g表示组/o表示其他):
    • 777 ===> u=rwx,g=rwx,o=rwx
    • 755 ===> u=rwx,g=rx,o=rx
    • 644 ===> u=rw,g=r,o=r

其他命令

查找命令

find
  • 目录紧跟在find之后

    • find ./ -type 'l'找当前目录下的软连接, 子目录会递龟进入;
    • find ./ -name '*.jpg'-找当前目录下的jpg文件, 子目录会递龟进入;
    • find ./ -maxdepth 3 -name '*.jpg'-指定目录层级深度为3层;

    • find ./ -size +20M -size -50M-指定大小范围;

    • 按时间查找:

      • -atime(access访问时间)
      • -ctime(change更改时间)
      • -mtime(modify改动时间)
      • find ./ -ctime 3查找三天内被改动的文件;
    • ls -h-以人类可读的方式显示结果;

    • man手册中反斜杠/可以用于查找关键字;
grep

grep:按文件内容搜索”return”关键字:

grep -r "return" ./ -n

ps:监控后台进程的工作情况;

ps aux

加个管道过滤内容

ps aux | grep "kernel"(搜索本身会占一个进程)

如果将管道的手法用在find上(用xargs):

find /usr/ -maxdepth 3 -type -f | xargs ls -l

-execxargs的区别:前者会将结果不论多少一股脑的交给-exec, 而xargs会做分片处理(效率更高);

创建名字中有空格的文件:

1
2
$ touch abc\ def
$ touch "abc def"

由于xargs会将文件名中的空格误认为是分隔符, 解决方式: 控制分隔符:

1
find /usr/ -maxdepth 3 -type f -print0 | xargs -0 ls -l

打包压缩

  • 打包压缩 是日常工作中备份文件的一种方式
  • 在不同操作系统中,常用的打包压缩方式是不同的
    • Windows 常用 rar
    • Mac 常用 zip
    • Linux 常用 tar.gz

打包 / 解包

  • tar 是 Linux 中最常用的 备份工具,此命令可以 把一系列文件 打包到 一个大文件中,也可以把一个 打包的大文件恢复成一系列文件
  • tar 的命令格式如下:
1
2
3
4
5
# 打包文件
tar -cvf 打包文件.tar 被打包的文件/路径...

# 解包文件
tar -xvf 打包文件.tar
  • tar 选项说明
选项 含义
c 生成档案文件,创建打包文件
x 解开档案文件
v 列出归档解档的详细过程,显示进度
f 指定档案文件名称,f 后面一定是 .tar 文件,所以必须放选项最后

注意:f 选项必须放在最后,其他选项顺序可以随意

压缩/解压缩

zip

zip压缩:

zip -r ziptest.zip hello.c hello.cpp

zip解压缩:

unzip ziptest.zip

gzip

targzip 命令结合可以使用实现文件 打包和压缩

  • 打包和压缩
    • tar 只负责打包文件,但不压缩
    • gzip 压缩 tar 打包后的文件,其扩展名一般用 xxx.tar.gz

Linux 中,最常见的压缩文件格式就是 xxx.tar.gz

  • tar 命令中有一个选项 -z 可以调用 gzip,从而可以方便的实现压缩和解压缩的功能
  • 命令格式如下:
1
2
3
4
5
6
7
8
# 压缩文件
tar -zcvf 打包文件.tar.gz 被压缩的文件/路径...

# 解压缩文件
tar -zxvf 打包文件.tar.gz

# 解压缩到指定路径
tar -zxvf 打包文件.tar.gz -C 目标路径
选项 含义
-C 解压缩到指定目录,注意:要解压缩的目录必须存在
bzip2
  • tarbzip2 命令结合可以使用实现文件 打包和压缩(用法和 gzip 一样)
    • tar 只负责打包文件,但不压缩,
    • bzip2 压缩 tar 打包后的文件,其扩展名一般用 xxx.tar.bz2
  • tar 命令中有一个选项 -j 可以调用 bzip2,从而可以方便的实现压缩和解压缩的功能
  • 命令格式如下:
1
2
3
4
5
# 压缩文件
tar -jcvf 打包文件.tar.bz2 被压缩的文件/路径...

# 解压缩文件
tar -jxvf 打包文件.tar.bz2

软件安装

通过 apt 安装/卸载软件
  • apt 是 Advanced Packaging Tool,是 Linux 下的一款安装包管理工具
  • 可以在终端中方便的 安装卸载更新软件包
1
2
3
4
5
6
7
8
9
10
# 1. 安装软件
$ sudo apt install 软件包

# 2. 卸载软件
$ sudo apt remove 软件名

# 3. 更新已安装的包
$ sudo apt upgrade
# 4. 更新源服务器列表:
sudo vim /etc/apt/sources.list
配置软件源
  • 如果希望在 ubuntu 中安装软件,更加快速,可以通过设置镜像源,选择一个访问网速更快的服务器,来提供软件下载/安装服务
  • 提示:更换服务器之后,需要一个相对比较长时间的更新过程,需要耐心等待。更新完成后,再安装软件都会从新设置的服务器下载软件了

所谓镜像源,就是所有服务器的内容是相同的(镜像),但是根据所在位置不同,国内服务器通常速度会更快一些!

通过deb包安装
1
2
3
4
5
6
7
安装deb软件包命令: sudo dpkg -i xxx.deb
删除软件包命令: sudo dpkg -r xxx.deb
连同配置文件一起删除:sudo dpkg -r --purge xxx.deb
查看软件包信息命令:sudo dpkg -info xxx.deb
查看文件拷贝详细命令:sudo dpkg -L xxx.deb
查看系统中已安装软件包信息:sudo dpkg -l
重新配置软件包命令:sudo dpkg -reconfigure xxx
源码安装
1
2
3
4
5
6
1.解压缩源代码包
2.cd dir
3. ./configure # 检测文件是否缺失,创建Makefile, 检查编译环境
4. make # 编译源码,生成库和可执行程序
5. sudo make install #将库和可执行程序,安装到系统路径下
6. sudo make distclean # 删除和卸载软件

单用户操作系统和多用户操作系统

  • 单用户操作系统:指一台计算机在同一时间 只能由一个用户 使用,一个用户独自享用系统的全部硬件和软件资源
    • Windows XP 之前的版本都是单用户操作系统
  • 多用户操作系统:指一台计算机在同一时间可以由 多个用户 使用,多个用户共同享用系统的全部硬件和软件资源
    • UnixLinux 的设计初衷就是多用户操作系统

linux下的文件系统

Linux文件类型

  • 普通-
  • 目录d
  • 字符设备c
  • 块设备b
  • 软连接l
  • 管道p
  • 套接字s
  • Unknown

文件结构

  • Windows 下,打开 “计算机”,看到的是一个个的驱动器盘符:eg: C盘,D盘…。

    • 每个驱动器都有自己的根目录结构,形成多个树并列的情形。
  • Linux 下,看不到驱动器盘符,看到的是文件夹(目录)

    • linux没有盘符概念,只有一个根目录 /,所有文件都在它下面

linux文件系统

  • 用户目录

    • 位于 /home/xxx,称之为用户工作目录或家目录,表示方式:

      1
      2
      /home/xx
      ~
  • /:根目录,一般根目录下只存放目录,在 linux 下有且只有一个根目录,所有的东西都是从这里开始

    • 当在终端里输入 cd /home,其实是在告诉电脑,先从 /(根目录)开始,再进入到 home 目录
  • /bin、/usr/bin:可执行二进制文件的目录,如常用的命令 ls、tar、mv、cat

  • /boot:放置 linux 系统启动时用到的一些文件,如 linux 的内核文件:/boot/vmlinuz系统引导管理器:/boot/grub

  • /dev:存放linux系统下的设备文件访问该目录下某个文件,相当于访问某个设备,常用的是挂载光驱mount /dev/cdrom /mnt

  • /etc:系统配置文件存放的目录,不建议在此目录下存放可执行文件,重要的配置文件有

    • /etc/inittab
    • /etc/fstab
    • /etc/init.d
    • /etc/X11
    • /etc/sysconfig
    • /etc/xinetd.d

    • /etc/profile

  • /home:系统默认的用户家目录,新增用户账号时,用户的家目录都存放在此目录下

    • ~ 表示当前用户的家目录
    • ~xxx 表示用户 xxx 的家目录
  • /lib、/usr/lib、/usr/local/lib:系统使用的函数库的目录,程序在执行过程中,需要调用一些额外的参数时需要函数库的协助

  • /lost+fount:系统异常产生错误时,会将一些遗失的片段放置于此目录下

  • /mnt: /media:光盘默认挂载点,通常光盘挂载于 /mnt/cdrom 下,也不一定,可以选择任意位置进行挂载

  • /opt:给主机额外安装软件所摆放的目录

  • /proc:此目录的数据都在内存中,如系统核心,外部设备,网络状态,由于数据都存放于内存中,所以不占用磁盘空间,比较重要的文件有:/proc/cpuinfo、/proc/interrupts、/proc/dma、/proc/ioports、/proc/net/* 等

  • /root:系统管理员root的家目录

  • /sbin、/usr/sbin、/usr/local/sbin:放置系统管理员使用的可执行命令,如 fdisk、shutdown、mount 等。与 /bin 不同的是,这几个目录是给系统管理员 root 使用的命令,一般用户只能”查看”而不能设置和使用

  • /tmp:一般用户或正在执行的程序临时存放文件的目录,任何人都可以访问,重要数据不可放置在此目录下

  • /srv:服务启动之后需要访问的数据目录,如 www 服务需要访问的网页数据存放在 /srv/www 内

  • /usr:应用程序存放目录

    • /usr/bin:存放应用程序
    • /usr/share:存放共享数据
    • /usr/lib:存放不能直接运行的,却是许多程序运行所必需的一些函数库文件
    • /usr/local:存放软件升级包
    • /usr/share/doc:系统说明文件存放目录
    • /usr/share/man:程序说明文件存放目录
  • /var:放置系统执行过程中经常变化的文件

    • /var/log:随时更改的日志文件
    • /var/spool/mail:邮件存放的目录
    • /var/run:程序或服务启动后,其 PID 存放在该目录下

文件和目录常用命令

  • 查看目录内容
    • ls
  • 切换目录
    • cd
  • 创建和删除操作
    • touch
    • rm
    • mkdir
  • 拷贝和移动文件
    • cp
    • mv
  • 查看文件内容
    • cat
    • more
    • grep
  • 其他
    • echo
    • 重定向 >>>
    • 管道 |
查看目录ls 命令说明
  • ls 是英文单词 list 的简写,其功能为列出目录的内容,是用户最常用的命令之一,类似于 DOS 下的 dir 命令
Linux 下文件和目录的特点
  • Linux 文件 或者 目录 名称最长可以有 256 个字符
  • . 开头的文件为隐藏文件,需要用 -a 参数才能显示
  • . 代表当前目录
  • .. 代表上一级目录
ls 常用选项
参数 含义
-a 显示指定目录下所有子目录与文件,包括隐藏文件
-l 以列表方式显示文件的详细信息
-h 配合 -l 以人性化的方式显示文件大小
计算机中文件大小的表示方式
单位 英文 含义
字节 B(Byte) 在计算机中作为一个数字单元,一般为 8 位二进制数
K(Kibibyte) 1 KB = 1024 B,千字节 (1024 = 2 ** 10)
M(Mebibyte) 1 MB = 1024 KB,百万字节
千兆 G(Gigabyte) 1 GB = 1024 MB,十亿字节,千兆字节
T(Terabyte) 1 TB = 1024 GB,万亿字节,太字节
P(Petabyte) 1 PB = 1024 TB,千万亿字节,拍字节
E(Exabyte) 1 EB = 1024 PB,百亿亿字节,艾字节
Z(Zettabyte) 1 ZB = 1024 EB,十万亿亿字节,泽字节
Y(Yottabyte) 1 YB = 1024 ZB,一亿亿亿字节,尧字节
ls 通配符的使用
通配符 含义
* 代表任意个数个字符
? 代表任意一个字符,至少 1 个
[] 表示可以匹配字符组中的任一一个
[abc] 匹配 a、b、c 中的任意一个
[a-f] 匹配从 a 到 f 范围内的的任意一个字符
文件操作基本命令

more-分屏显示文件内容, 空格翻页;

less同理;

head -n file-查看file的前n行;

tail -n file-查看file的后n行;

目录及操作基本命令

一个目录所占的磁盘大小为4K;

cd --在两个目录之间来回切换;

rmdir-删除空目录;

cp -a/-r srcdir dstdir-拷贝目录;

查找文件
  • find 命令功能非常强大,通常用来在 特定的目录下 搜索 符合条件的文件
序号 命令 作用
01 find [路径] -name “*.py” 查找指定路径下扩展名是 .py 的文件,包括子目录
  • 如果省略路径,表示在当前文件夹下查找
  • 之前学习的通配符,在使用 find 命令时同时可用
硬链接和软链接
  • 软连接是一个文件,其中存的就是文件的路径, 路径有几个字符就占几个字节, 所以建议用绝对路径创建软连接;

    • 注意文件的权限, 软连接的权限代表其本身的权限, 与指向的目的文件无关;
  • 创建硬链接会增加硬链接计数;

    • 这些硬链接只想同一个文件, 修改一个其余的会同步变化;
    • 所有的硬链接有相同的Inode(文件统一id);
    • 删除只是把硬链接计数-1;
序号 命令 作用
01 ln -s 被链接的源文件 链接文件 建立文件的软链接,用通俗的方式讲类似于 Windows 下的快捷方式
  • 没有 -s 选项建立的是一个 硬链接文件
    • 两个文件占用相同大小的硬盘空间,工作中几乎不会建立文件的硬链接
  • 源文件要使用绝对路径,不能使用相对路径,这样可以方便移动链接文件后,仍然能够正常使用
1
2
ln -s hello.c hello.c.s #创建软连接;
ln hello.c hello.c.h #创建硬链接;
文件软硬链接的示意图

文件软硬链接示意图

在 Linux 中,文件名文件的数据 是分开存储的

  • 提示:
    • 在 Linux 中,只有文件的 硬链接数 == 0 才会被删除
    • 使用 ls -l 可以查看一个文件的硬链接的数量

磁盘分区类型

  • 主分区:最多只能有四个
  • 扩展分区:最多一个,算作主分区的一种,主分区加扩展分区最多有四个。扩展分区不能存储数据和格式化,必须再划分为逻辑分区才可以使用。
  • 逻辑分区:在扩展分区中划分

逻辑分区的编号从5开始

支持的文件系统

  • ext2:ext文件系统的升级版。最大支持16TB的分区和最大2TB的文件。
  • ext3:ext2的升级,增加日志功能。
  • ext4:ext3升级版本,主流使用,功能强大

文件系统常用命令

df, du, fsck, dump2fs

文件系统查看命令df
1
2
3
4
5
6
7
df \[选项][挂载点]

-a 所有文件系统信息

-h 使用习惯单位显示容量,如kB,MB,GB

ls 只统计目录下的大小,而不会统计子目录下的数据大小。
统计目录或文件大小du
1
-a , -h , -s

df命令从文件系统考虑,不光要考虑文件占用的空间,还要统计被命令或程序占用的空间(eg. 文件已经被删除,但程序并没有释放空间)

du命令面向文件,只会计算文件或目录占用的空间。

文件系统修复命令fsck
1
fsck[选项]分区设备文件名
显示磁盘状态命令dumpe2fs
1
dumpe2fs 分区设备文件名

文件系统常用命令-挂载命令

查询与自动挂载
1
2
3
mount [-l]:查询系统中已经挂载的设备,-l会显示卷标名称

mount -a 依据配置文件/etc/fastb的内容,自动挂载
挂载命令格式
1
2
3
4
5
6
7
8
9
10
11
mount [-t 文件系统] [-L 卷标名] [-o 特殊选项] 设备文件名 挂载点

选项:

-t 文件系统:加入文件系统类型来指定挂载的类型,可以ext3,ext4,iso9660等文件系统

-L 卷标名:挂载指定卷标的分区,而不是安装设备文件名挂载

-o 特殊指令(remount ...)

mount -o remount ,noexec /home
挂载光盘与U盘
1
2
3
4
5
6
7
#挂载光盘

mkdir /mnt/cdrom/ #建立挂载点

mount -t iso9660 /dev/cdrom /mnt/cdrom #挂载光盘

mount /dev/sr0 /mnt/cdrom
卸载命令
1
2
3
umount 设备文件名或挂载点

umount /mnt/cdrom
挂载U盘
1
2
3
4
5
fdisk -l 查看U盘设备文件名

mount -t vfat /dev/sdb1 /mnt/usb/

linux默认不支持NTFS文件系统

fdisk分区

fdisk命令分区过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
1.添加硬盘,虚拟机必须在关机的情况下添加

2.查看新硬盘 fdisk -l

3.使用fdisk命令分区

fdisk /dev/sdb

4.重新读取分区表信息

partprobe

5.格式化分区

扩展分区不可以格式化

mkfs -t ext4 /dev/sdb1

6.建立挂载分区
分区自动挂载与fstab文件修复
1
2
3
4
5
6
7
将挂载写入/etc/fstab文件,一定要写对

mount -a 依据配置文件 /etc/fstab的内容,自动挂载

/etc/fstab文件修复

mount -o remount,rm /

分配swap分区

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
1.free命令
查看内存与swap分区使用情况
cached(缓存):加速数据的读取过程
buffer(缓冲):写入数据过程中,将分散的写入操作保存到内存中,当达到一定的程度再集中写入硬盘,加速数据的写入过程。

2.新建swap分区
fdisk /dev/sdb
还需将分区ID改为82

3.格式化
mkswap /dev/sdb6

4.加入swap分区
swapon /dev/sdb6 加入swap分区
swapoff /dev/sdb6 取消swap分区

5.swap分区开机自动挂载
vi /etc/fstab

静态库和动态库对比

静态库

一些目标代码的集合。按照习惯,linux中一般一.a作为文件名后缀。使用ar(archiver)命令可以创建静态库。

在可执行程序运行前就已经加入到执行码中,成为执行程序的一部分。

静态库在应用程序生成时,可以不必再编译,节省编译时间。

静态库会占用大量存储空间。

静态库

动态库

在执行程序启动时加载到执行程序中,可以被多个执行程序共享使用。

动态库不需要编译入程序, 运行时动态加载, 导致速度慢了一些

动态库

二者的适合场景:

  • 静态库: 对空间要求较低, 对时间要求较高
  • 动态库: 对时间要求较低, 对空间要求较高

静态库制作

先用gcc的-c参数将源文件编译成二进制文件, 再用ar命令封装静态库

1
2
3
4
5
6
# 有文件add.c div1.c sub.c
gcc -c add.c -o add.o
gcc -c sub.c -o sub.o
gcc -c div1.c -o div1.o

ar rcs libMyMath.a add.o sub.o div1.o

使用:

1
2
# 将库直接加入编译的源文件中即可使用
gcc test.c libMyMath.a -o test1

静态库使用及头文件对应

隐式声明: 编译过程中没有遇到函数定义和函数声明, 编译器会帮助做隐式声明;

但是这种隐式声明只能对于返回值为int型的;

解决方法:

1
2
3
4
5
6
7
8
9
/*添加头文件,防止头文件重复包含,一旦头文件被展开过一次,_MYMATH_H_就被定义过了,后面就不会再展开*/
#ifndef _MYMATH_H_
#define _MYMATH_H_

int add(int,int);
int sub(int,int);
int div1(int,int);

#endif

然后将源文件和库联编即可, 注意源文件在前

1
2
# 动态库存放在~/sys/staticLib/lib
~/sys/staticLib$ gcc test.c ./lib/libMyMath.a -o test -I ./inc

动态库制作

生成与位置无关的代码

将源文件.c编译为目标文件.o, 生成与位置无关的代码, 借助参数-fPIC

动态库加载

编译生成hello.o的时候, 各个函数的地址还是相对于main的地址, 链接阶段填入main的地址;

由于动态库的函数在库里, 不能像程序内部的函数一样直接填入main的地址, 动态函数在a.out中没有位置, 依赖于@plt, 进行延迟绑定;

  • 查看二进制文件的反汇编代码:objdump -dS test

  • 输出重定向:objdump -dS test > test.s

制作演示:

1
2
3
4
5
6
7
8
# 1. 将.c文件生成.o文件(生成与位置无关的代码-fPIC):
gcc -c add.c -o add.o -fPIC
# 2. 使用gcc -shared制作动态库:
gcc -shared add.o sub.o div1.o -o libMyMath.so
# 3. 编译可执行程序时, 指定所使用的动态库, -l 指定库名, -L 指定库路径:
gcc test.c -o test -l MyMath -L ./lib
# 4. 运行可执行程序
./test # 报错(编译通过,执行错误,找不到文件)

动态库加载错误原因及解决办法

上面的错误原因:

  • 链接器:工作于链接阶段, 工作时需要指定-l和-L参数, 上面已经指定
  • 动态链接器:工作于程序运行阶段, 工作时需要提供动态库所在目录

上面两者没有任何关系

方法1:

  • 动态链接器要根据环境变量寻找动态库:LD_LIBRARY_PATH

  • 执行export LD_LIBRARY_PATH=./lib

  • 指定后就可以执行了(但是上面指定的只是临时的, 环境变量是进程的概念)

  • 要想永久指定, 需要更改配置文件, 加入环境变量, 重启终端使之生效:

1
2
# ~/.bashrc下加入
export LD_LIBRARY_PATH=./lib

方法2:

  • 像标准C库这种本身就在系统的环境变量里, 所以能找到;

  • 滥竽充数法:将库文件放到系统根目录下的lib里就可以了;

  • ldd test可以查看程序运行所需要的动态库

最后一种方法:修改配置文件法;

1
2
3
sudo vim /etc/ld.so.conf
# 写入动态库绝对路径, 保存;
sudo ldconfig -v #使配置文件生效

动态库和静态库共存时, 编译器优先使用动态库;

简介

优点:执行速度快,功能强大,编程自由。代码量小:dll封装等

缺点:编程周期长,可移植性较差,过于自由,容易出错,对于平台库依赖较多。

可用部分:网站后台,程序库,游戏引擎,写语言,操作系统,微处理器

构成:32个关键字,9种控制语句,34种运算符(算术运算符,关系运算符,逻辑运算符,位运算符,复制运算傅符,条件运算符,逗号运算符,指针运算符,求字节数,强制类型转换,分量运算符,下标运算符)

1
2
3
#include <xxx>  //表示导入系统文件

#include "xxx" //表示导入自定义文件

c编译步骤

  • 预处理:宏文件展开、头文件展开、条件编译等,同时将代码中的注释删除,并不检查语法
    • gcc -E hello.c -o hello.i
  • 编译:检查语法,将预处理后文件编译生成汇编文件
    • gcc -S hello.i -o hello.s
  • 汇编:将汇编文件生成目标文件(二进制文件)
    • gcc -c hello.s -o hello.o
  • 链接:程序依赖各种库,编译之后需要将库链接到最终的可执行程序中
    • gcc hello.o -o hello
    • -o 表示生成一个文件

程序执行过程

硬盘(外部存储设备)->内存(MEM,代码区,数据区,栈区,堆区)->CPU

64位与32位操作系统区别

  • 寄存器是CPU内部最基本的存储单元
  • CPU对外通过总线(地址、控制、数据)来和外部设备交互,总线的带宽是8位,同时CPU的寄存器也是8位,那个CPU就叫做8位CPU
  • 如果总线是32位,寄存器也是32位的,这个CPU为32位CPU
  • 所有的64位CPU兼容32位的指令,32位要兼容16位的指令,所以在64位的CPU上可以识别32位的指令
  • 在64位的架构上运行64位的操作系统,那个这个系统为64位
  • 64位的CPU运行32位的操作系统,这个系统为32位
  • 64位的软件不能运行在32位的CPU上

总线越宽,速度越快

寄存器,缓存,内存的关系

所有的运算都要放到CPU中计算,CPU直接打交道的其实是寄存器

内存和寄存器进行数据读写

数据类型

数据类型关键字:char, short, int ,long, float, double

unsigned, signed, struct, union, enum, void

控制语句关键字:if ,else, switch, case,default, for ,while , break, continue,goto, return

存储类关键字auto , extern, register, static, const

其他关键字:sizeof, typedef( 定义函数指针,定义别名), volatile(防止编译器做优化)

数据类型的作用:编译器预算对象(变量)分配的内存空间大小

基址:在编译过程中决定

常量:在程序运行过程中,其值不能发生变化的量

1
2
3
const int price = 3; //(不安全写法,限定在c语言中)

#define PI 3.14159 //宏定义常量

变量:在程序运行过程中,其值可以发生变化的量

标识符命名规则

  • 不能使用系统关键字

  • 允许字母,下划线,数字,数字不能开头

  • 标识符区分大小写

  • 见名知意

整型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
%d

%o 8进制int类型

%x 16进制int类型,字母以小写输出

%X 16进制int ,字母以大写输出

%u 输出10进制无符号数

定义八进制数据:以0开头

定义十六进制数据:以0x开头

在计算机中不可以直接定义二进制

&取地址符

sizeof()不是函数,不需要包含任何头文件,计算一个数据类型的大小,单位为字节。

操作系统栈和堆

地址空间布局:
操作系统地址空间布局

执行期间编译器自动分配,编译器用它实现函数调用,调用函数时,栈增长,函数返回时,栈收缩。局部变量、函数参数、返回数据、返回地址等放在栈中

栈的特点

  1. 内存分配取决于编译器,用户栈在程序运行期间可以动态的扩展和收缩
  2. 和数据结构中的“栈”本质上是不一样的,但是操作方式类似于栈。
  3. 数据从栈中的进出满足“后进后出”的规律。
  4. 栈向低地址方向增长,esp(栈指针)指向栈顶元素。

动态储存器分配器维护着的一个进程的虚拟存储器区域。一般由程序员分配释放(堆在操作系统对进程初始化的时候分配),若程序员不释放,程序结束时可能由OS回收,每个进程,内核都维护着一个变量brk指向堆顶。

堆的特点

  1. 内存分配取决于程序员,C/C++可以手动释放该片内存。
  2. 和数据结构的”堆“完全两回事,没有半点关系,在这里堆的结构更像链表
  3. 所有的对象,包括数组的对象都存在堆上
  4. 堆内存被所有的线程共享
  5. 引用类型总是放在堆中。
  6. 堆向高地址方向增长,内核维护的变量brk指向堆顶

注意:值类型和指针总是放在他们被声明的地方
当值类型的数据在方法体内被声明时,它们都应该放在栈上。
如果一个值类型被声明在方法体外且存在于一个引用类型中,那么它将会被堆里的引用类型所取代。

全局区/静态区:

全局变量、静态变量、常量的存储区域,程序终止时系统释放。

文字常量区:

存放常量字符串,程序结束后由系统释放。

程序代码区:

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

实例

1
2
3
4
5
6
7
8
9
10
11
12
int a = 0;        //全局初始化区
char *p1; //全局未初始化区
void main()
{
int b; //栈
char s[] = "123"; //栈
char *p2; //栈
char *p3 = "sdfghhj"; //其中,“sdfghhj\0”常量区,p3在栈区
static int c = 0; //全局区
p1 = (char*)malloc(10); //10个字节区域在堆区
strcpy(p1,"sdfghhj"); //"sdfghhj\0"在常量区,编译器可能会优化p1和p3指向同一块区域
}

栈和堆的区别:

  1. 栈内存存储的的是局部变量,堆内存存储的是实体。
  2. 栈内存的更新的速度会更快些(局部变量),堆内存的更新速度相对更慢
  3. 栈内存的访问直接从地址读取数据到寄存器,然后放到目标地址,而堆内存的访问更麻烦,先将分配的地址放到寄存器,在读取地址的值,最后再放到目标文件中,开销更大。
  4. 栈内存是连续的空间,堆内存一般情况不是连续的,频繁地开辟空间,释放空间容易产生内存碎片(外碎片)。

栈和堆的联系:

堆中对象是直接由栈中的句柄(引用)管理者,所以堆负责产生真实对象,栈负责管理对象。

类型系统

字符变量实际上并不是将该字符本身放到变量的内存单元,而是将该字符对应的ASCII编码放到变量的存储单元。char的本质就是一个字节大小的整型。

不以f结尾的常量都是double类型,以f结尾的为float类型。

1
2
3
4
5
float a = 3.14 //实际为double类型转换为float类型

%p //打印地址,一个变量对应的内存地址编号(无符号十六禁止整型数)

a = 3.2e3f //科学计数 3.2*1000 = 3200

整型和字符型数据存储

数据在计算机中主要以补码的形式存储。

数据传输以bit表示。

原码:最高位为符号位,0表示正,1表示负。当两个整数相减或不同符号数相加时,必须比较两个数哪个绝对值大才能决定谁减谁,才能确定结果为正还是负,所以原码不便于加减运算

反码:正数与原码一样。负数:符号位不变,其它取反。反码运算也不方便,通常用来作为补码的中间过渡。

补码:计算机系统中,数值一律用补码来存储。对于正数:原码,反码,补码相同。负数:补码为它的反码加1补码符号位不动,其他位求反,加1得到原码

补码原因:

  • 统一零的编码(0在计算机中的存储方式:按照原码和反码都需要区分0和-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
27
28
29
char ch = 10; 
原码 0000 1010
反码 0000 1010
补码 0000 1010
char ch1 = -10;
原码 1000 1010
反码 1111 0101
补码 1111 0110

ch + ch1 = 0
0000 1010
1111 0110
1 0000 0000
0000 0000

76 - 32 转化为 76 + (-32)
76原码 0100 1100
反码 0100 1100
补码 0100 1100
-32原码 1010 0000
反码 1101 1111
补码 1110 0000
1 0010 1100
0010 1100 得到原码为: 0010 110044
76 - 82
-82原码 1101 0010
反码 1010 1101
补码 1010 1110
相加: 1111 1010 得到原码: 1000 0110-6

8bit数据最大存储区间为:[-128, 127]

数据存储时,将-0对应的区间值设为-2^7 也就是-128

无符号:数据在计算机中不存在符号位

usigned char : 0 - 2^8 -1 : 0 - 255

数值溢出

当超过一个数据类型能够存放最大的范围时,数值会溢出

有符号最高位溢出的区别:符号位溢出会导致数的正负发生变化,但最高位的溢出会导致最高位丢失。

类型限定

限定符 含义
extern 声明一个变量,extern声明的变量没有建立存储空间。在定义的时候再创建存储空间。
const 定义一个常量,常量的值不能修改
volatile 防止编译器优化代码
register 定义寄存器变量,提高效率,建议型的变量而不是命令型的指令。如果CPU有空闲寄存器,则register生效,没有空闲则无效

字符串常量

  • 字符串常量是内存中一段连续的char空间,以’\0’结尾
  • 字符串常量是由双引号括起来的字符序列,如”china”
  • 字符串常量与字符常量的不同:
    • ‘a’为字符常量, 实际存储’a’
    • “a”为字符串常量 , 实际存储’a’’\0’
    • 每个字符串的结尾,编译器会自动的添加一个结束标志位’\0’,即”a”包含两个字符’a’和’\0’.
    • 占位符%s,表示输出一个字符串,遇到\0停止。
1
2
3
putchar() //输出字符,可以是变量,字符,数字
getchar() //从标准输入设备读取一个char
scanf() //内部参数中不能包含\n,可以用空格,逗号等。。。

运算符号

算术运算符

  • 两个整数相除一定得到一个整数,默认向下取整,如果要向上取整,原数+1后再做除法

  • 取余只能对整数

  • 自增,自减

  • 后自增: a++ , 先进行表达式计算,再进行++

  • 前自增: ++a ,在表达式之前进行++,再进行表达式计算

1
2
3
int a = 10;
int b = ++a * 10;// a = 11, b = 110
int b = a++ * 10;// a = 11, b = 100 先完成计算,再增加1

比较运算符

c语言的比较运算中,”真“用数字”1”来表示,”假“用数字”0“表示

运算符优先级

单目高于双目运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1. [] () . 若同时出现,从左到右
2. \- ~(按位取反) ++ -- *(取值运算符) &(取地址运算符) ! (类型)(强制类型转换) sizeof, 从右到左
3. / * %
4. \+ -
5. << >>
6. \>= > < <=
7. == !=
8. & (按位与)
9. ^(按位异或)
10. |
11. &&
12. ||
13. ?:
14. = /= %= -= += >>= <<= &= ^= |=
15. , 逗号运算符号

数组和字符串

数组:把具有相同类型的若干变量按有序形式组织起来。

数组名是一个常量,是一个地址,指向数组首地址。

数组在内存中占用的大小:sizeof(数组名) = 数组类型*数组个数

数组的定义和初始化:{}

数组的个数必须是常量或常量表达式

数组必须预先知道大小,动态数组->开辟堆空间

二维数组

有行有列

数组的名为一个地址常量,二维数组的arr[0]也为地址常量

1
2
3
printf("%p\n",arr);
printf("%p\n",arr[0]);
printf("%p\n",&arr[0][0]); //所有输出都相等

多维数组: 嵌套定义

字符数组和字符串
1
2
3
4
5
char arr[5] = {'h','e','l','l','o'}; //字符数组
char arr[6] = {'h','e','l','l','o'}; //字符串,最后一个arr[5]自动赋值为0,相当于'\0'
char* arr = "hello";
char arr[] = "hello";
char arr[] = {"hello"};

字符串是字符数组的一个特例。

字符串结束标志为\0, 数字0等同于\0, 但不等同于’0’

字符数组与字符串的区别
  • C中没有字符串这种数据类型,可以通过char的数组来替代
  • 字符串一定是一个char的数组,但char的数组未必是字符串
  • 数字0(和字符’\0’等级)结尾的char数组就是一个字符串,但如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char的数组。
1
char arr[100] = {110,111,112,32, 32,43};//数字对应ASCII码,可以打印出字符串

gets() 允许输入的字符串含有空格,scanf不允许含有空格

scanf("%[\^\n]",ch);//接收非\n的所有内容(通过正则表达式来做约束)

由于scanf和gets无法知道字符串s大小,必须遇到换行符或读到文件结尾为止才接收输入,因此容易导致字符数组越界(缓冲区溢出)的情况。

char* fgets(char *s, int size, FILE * stream)从stram指定的文件内读入字符,保存到s所指定的内存空间,直到出现换行字符,读到文件结尾或是已读了size-1个字符为止,最后会自动加上字符\0作为结束标志。

如果是从键盘输入,stream为stdin

1
2
3
puts():标准输出字符串,在输出完成后自动输出一个\n
fputf()
strlen() 计算指定字符串的长度,不包含字符串结束符\0

类型转换

不同类型数据之间进行混合运算时必然涉及到类型的转换问题

转换的两种方法:

  • 自动转换(隐式转换):遵循一定的规则,由编译系统自动完成
  • 强制类型转换:把表达式的运算结果强制转换成所需的数据类型

类型转换的原则:占用内存字节数少(值域小)的类型向占用内存字节数多(值域大)的类型转换,以保证精度不降低。

强制类型转换运算符,不会四舍五入

程序流程结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (适合多区间,带嵌套)

if else 条件嵌套

if eles if else

switch case break default(不适合多区间,执行效率高)

三目运算符: ?:

表达式1?表达式2:表达式3 如果表达式1为真,结果为表达式2,为假则表达式结果为3

while

do while

跳转语句

break 在switch条件语句和循环语句中都可以使用break语句

  • 出现在switch条件语句中时,作用是终止某个case并跳出switch结构
  • 当出现在循环语句中,作用是跳出当前内循环语句,执行后面的程序
  • 当出现在嵌套循环语句中,跳出最近的内循环语句,执行后面的程序

continue:在循环语句中,如果希望立即终止本次循环,并执行下一次循环,此时需要使用continue语句。

goto语句(无条件跳转,尽量少用)

1
2
3
goto FLAG;
xxxx
FLAG;

函数

函数分类

系统函数和用户定义的函数

  • 系统函数,即库函数:由编译系统提供,用户中不必自己定义,可以直接使用
  • 用户定义函数,用于解决用户的专门需要

调用函数的要素:

  • 头文件
  • 函数名字,必须和声明的名字一样
  • 功能
  • 参数
  • 返回值

函数定义和使用

返回值类型 函数名 (参数列表)

1
2
3
4
{
代码体;// 函数功能实现的过程
return;
}
  • 在不同函数中函数中的变量名可以重名,因为作用域不同。

  • 在函数调用过程中传递的参数为实参(有具体的值)。

  • 函数定义中参数称为形式参数。

  • 在函数调用过程中,将实参传递给形参。

  • 在函数调用结束,函数会在内存中销毁。

在定义函数时指定的形参,在未出现函数调用时,它们并不占用内存中的存储单元,因此称为形式参数或者形参,表示它们并不是实际存在的数据,所以,形参中的变量不能赋值

如果函数返回的类型和return语句中表达式的值不一致,则以函数返回类型为准,即函数返回类型决定返回值的类型。对数值型数据,可以自动进行类型转换。

注意:如果函数返回的类型和return语句中表达式的值不一致,而它又无法自动进行类型转换,程序则会报错。

实参可以常量、变量或表达式,无论实参数是何类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。所以,这里的变量是在圆括号外面定义好的、赋好值的变量。

void类型不可以直接定义数据,void类型可以作为函数的返回值类型,表示没有返回值。

函数声明

  • 函数声明:如果用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数做声明。

    • 所谓函数声明,就是在函数尚在未定义的情况下,事先将该函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便编译器能够正常运行。
    • 注意:一个函数只能被定义一次,但可以声明多次。
  • 函数定义

声明和定义的区别

  • 声明变量不需要建立存储空间,如:extern int a;
  • 定义变量需要建立存储空间,如: int b;

从广义的角度来说声明中包含着定义,即定义是声明的一个特例,所以并非所有的声明都是定义。

  • int b; //即是声明,同时又是定义
  • 对于extern b 来说只是声明不是定义

一般情况下,把建立存储空间的声明称为“定义”, 而把不需要建立存储空间的声明称为“声明”

主函数和exit函数

在main函数中调用了exit和return结果是一样的,但在子函数中调用return只是代表子函数终止了,在子函数中使用exit,那么程序终止。

多文件编程

  • 函数功能实现放在其他.c文件中

  • 函数声明放到.h文件中

  • .h文件头部 # program once //防止头文件重复包含

头文件一般用于

  • 全局变量的定义
  • 函数的声明

  • 导入自己定义的头文件,用“xxx.h”

  • 一个相同名字的头文件对应一个相同名字的源文件

为了避免同一个文件被include多次,c/c++中有两种方式:

1
2
3
4
5
#ifndef //一般定义的方式为
#ifndef \__SOMEFILE_H__
#define \__SOMEFILE_H__
#endif
#pragma once //只能用于windows中

函数的三种参数传递方式

  • 传入参数:

    • 指针作为函数参数
    • 同时有const关键字修饰
    • 指针指向有效区域, 在函数内部做读操作
  • 传出参数:

    • 指针作为函数参数
    • 在函数调用前, 指针指向的空间可以无意义, 但必须有效
    • 在函数内部做写操作
    • 函数调用结束后充当函数返回值
  • 传入传出参数:

    • 指针作为函数参数
    • 在函数调用前, 指针指向的空间有实际意义
    • 在函数内部, 先做读操作, 再做写操作
    • 函数调用结束后, 充当函数返回值

指针

内存是沟通CPU和硬盘的桥梁

  • 暂存放CPU中的运算数据
  • 暂存与硬盘等外部存储器交换的数据

物理存储器和存储地址空间

物理存储器为实际存在的具体存储器芯片。

存储地址空间:对存储器编码的范围。软件中常说的内存含义。

  • 编码:对每个物理存储单元(一个字节)分配一个号码
  • 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写

内存地址

  • 将内存抽象为一个很大的一维字符数组
  • 编码就是对内存的每一个字节分配一个32位或64位的编号(与处理器的位数有关)
  • 内存编号称为内存地址。内存中的每一个数据都会分配相应的地址。
  • char:占一个字节分配一个地址
  • int:占四个字节分配四个地址
  • float, struct,函数,数组等

小端对齐,大端对齐

int* 为一个指针

地址也是一种特殊的数据类型,故存放地址的指针定义需要指明这一点,也就是二级指针的应用。

所有的指针类型存储的都是内存地址,内存地址都是一个无符号十六进制整型数**

&是取地址符号,是升维度的

*是取值符号,是降维度的

1
2
3
4
5
int main()
{
char ch = 97;
int* p = &ch; //指针类型不匹配,后面通过指针访问和修改数据都将报错
}

在定义指针类型的时候,一定要和变量的类型对应上。

野指针和空指针

野指针:指针变量指向一个未知的空间

1
int *p = 100;//野指针,程序中允许存在野指针

操作野指针对应的内存空间可能报错.

指针变量也是变量,是变量就可以任意赋值,不要越界即可(32位为4字节,64位为8字节),但是,任意数值赋值给指针变量没有意义,因为这样的指针就变成了野指针,此指针指向的区域是未知的(操作系统不允许操作此指针指向的内存区域).所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。

但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),c语言中可以把NULL赋值给此指针,这样就标志此指针为空指针,没有在任何指向。

操作系统将0-255的地址作为系统占用,不允许访问操作。

1
2
int *p = NULL;
#define NULL ((void*)0) //NULL为一个值为0的宏常量,内存地址为0的空间

操作空指针对应的空间一定会报错

空指针可以用作条件判断: if (p==NULL)

万能指针void *

void 指针可以*指向任意变量的内存空间

万能指针可以接收任意类型变量的内存地址

在通过万能指针修改变量的值时,需要找到变量对应的指针类型

const修饰的指针变量

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main()
{
const int a = 10;

int *p = &a;
*p = 100; //指针间接修改常量的值
printf("%d\n",a); //a = 100
}

const修饰指针:

  • const 修饰指针类型:const int* p (p为一个指向int的指针,)可以修改指针变量的值,不可以修改指针指向内存空间的值
  • const修饰指针变量:int * const p (p为const类型的指针,是一个常量),可以修改指针指向内存空间的值,不可以修改指针变量的值。

const int const p //const修饰指针类型,修饰指针变量,*只读指针

指针和数组

数组名是数组的首元素地址,但它是一个常量。

指针类型变量+1,等同于内存地址 + sizeof(type)

两个指针相减,得到的结果是两个指针的偏移量(步长)

所有的指针类型相减结果都是int类型。

数组作为函数参数会退化为指针,丢失了数组的精度

1
2
3
4
void my_strcpy(char *dest, char*ch)
{
while(*dest++ = *ch++);
}
1
2
int *p = &array[5];
p[-2]; //*(p-2),想当于 p[3]

指针操作数组时下标允许是负数

指针可以比较大小,逻辑运算

指针数组

  • 指针数组,是一个数组,数组的每个元素都是指针类型

  • 指针数组里面元素存储的是指针

  • 指针数组是一个特殊的二维数组模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main()
{
int a[] = {1,2,3};
int b[] = {4,5,6};
int c[] = {7,8,9};

//指针数组是一个特殊的二维数组模型
int* arr[] = {a,b,c};

//arr和&arr[0]是指针数组的首地址
//指针数组对应二级指针
printf("%p\n",arr);
printf("%p\n",&arr[0]);printf("%p\n",a);
printf("%p\n",a);

int **p = arr;
}

多级指针

二级指针就是指向一个一级指针变量地址的指针。

指针数组和二级指针建立关系

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
#include <stdio.h>

int main()
{
int a[] = {1,2,3};
int b[] = {4,5,6};
int c[] = {7,8,9};

//指针是一个特殊的二维数组模型
int *arr[] = {a,b,c};

//指针数组和二维指针建立关系
int **p = arr;

printf("%d\n", **p);
//二级指针加偏移量,相当于跳过了一个一维数组大小
//一级指针加偏移量,相当于跳过一个元素
printf("%d\n",**(p+1));
printf("%d\n", *(*(p+1)+1)); //arr[1][1]

for(int i=0; i<3; i++)
{
for(int j = 0; j<3; j++)
{
printf("%d ", p[i][j]);
printf("%d ",*(p[i]+j));
printf("%d ",*(*(p+i)+j));
printf("\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
#include <stdio.h>

void remove_space01(char* ch)
{
char str[100]={0};
char * p = str;

int i = 0;
int j = 0;
while(ch[i]!= '\0')
{
if(ch[i] != ' ')
{
str[j] = ch[i];
j++;
}
i++;
}
printf("%s\n",str);

while(*ch++ = *p++);
}

void remove_space(char* ch)
{
char* ftemp = ch;//遍历数组
char* rtemp = ch; //记录空格

while(*ftemp)
{
if(*ftemp != ' ')
{
*rtemp = *ftemp;
rtemp++;
}
ftemp++;
}
*rtemp = 0;

}

int main()
{
char test[] = " he l l o";
remove_space(test);
printf("%s\n",test);
}

内存管理

作用域

  • 代码块作用域({}之间的一段代码)
  • 函数作用域
  • 文件作用域

局部变量:在函数内部定义的变量,使用auto修饰,生命周期:从创建到函数结束

全局变量:在函数体外定义存放在数据区,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用extern声明。

  • 全局变量的声明周期和程序运行周期一样
  • 不同文件的全局变量不可重名
  • 全局变量可以和局部变量重名,使用最近的一个

内存布局

在没有运行程序前,也就是程序没有加载到内存之前,可执行程序内部已经分好了3段信息,分别为代码区(text),数据区(data)和未初始化数据区(bss)3个部分。

静态(static)局部变量

  • static局部变量的作用域也是在定义的函数内有效,在数据区存储
  • static局部变量的生命周期和程序运行周期一样,同时static局部变量的值只初始化一次,但可以赋值多次
  • static局部变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0,字符型变量赋空字符

静态全局变量

可以在本文件中使用,不可以在其他文件中使用

生命周期:数据区保存,从程序开始到程序结束

变量类型 作用域 生命周期 存储位置
局部变量 函数内部 从局部变量创建到函数结束 栈区
全局变量 项目中所有文件 从程序创建到程序销毁 数据区
静态局部变量 函数内部 从程序创建到程序销毁 数据区
静态全局变量 定义所在的文件 从程序创建到程序销毁 数据区

未初始化数据(根据编译器可能不同):

局部变量未初始化,值为乱码

未初始化的全局变量,值为0

  • 全局初始化数据区/静态数据区(data段)
    • 该区包含了在程序中明确被初始化的全局变量、已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如:字符串常量)
  • 未初始化数据区(bss区)
    • 存入的是全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为0或者空(NULL)
  • 程序在加载到内存前,代码区和全局区(data和bss)的大小就是固定的,程序运行期间不能改变。然后,运行可执行程序,系统把程序加载到内存,除了根据可执行程序的信息分出代码区(text)、数据区(data)和未初始化数据区(bss)之外,还额外增加了栈区、堆区。

全局函数和静态函数

在c中函数默认为全局的,使用关键字static可以将函数声明为静态,函数定义为static就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能使用,即使在其他文件中声明也没用。

对于不同文件中的static函数名可以相同

全局函数的名称是作用域中唯一的(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
#include <stdio.h>

//安全的常量,存储区域为数据区常量区
const int abc = 123;

//未初始化全局变量
int a1;

//初始化全局变量
int b1 = 10;

//未初始化静态全局变量
static int c1;

//初始化静态全局变量
static int d1 = 10;


int main()
{
int e1 = 10;

//未初始化局部静态变量
static int f1;

//初始化局部静态变量
static int h1 = 10;

//字符串常量
char* p = "hello world";
//数组
int arr[] = {1,2,3,4};
//指针
int* pp = arr;

printf("未初始化全局变量:%p\n",&a1);
printf("初始化全局变量:%p\n",&b1);
printf("未初始化静态全局变量:%p\n",&c1);
printf("初始化全局静态变量:%p\n",&d1);

printf("局部变量:%p\n",&e1);
printf("未初始化局部静态变量%p\n",&f1);
printf("初始化局部静态变量%p\n",&h1);
printf("字符串常量%p\n",&p);
printf("数组%p\n",arr);
printf("指针变量%p\n",pp);
printf("指针地址%p\n",&pp);
}

未初始化全局变量:0x601058
初始化全局变量:0x601040
未初始化静态全局变量:0x601050
初始化全局静态变量:0x601044
局部变量:0x7fffee32d54c
未初始化局部静态变量0x601054
初始化局部静态变量0x601048
字符串常量0x7fffee32d550
数组0x7fffee32d560
指针变量0x7fffee32d560
指针地址0x7fffee32d558

const修饰的局部常量是不安全的,const修饰的全局常量是安全的

内存模型

  • 代码区:程序执行二进制码(程序指令),特点:(共享:另外的执行程序可以调用它,共享的目的是对于频繁被执行的程序,只需要在内存中有一份代码即可。只读:防止程序意外修改了它的指令。代码区还规划了局部变量的相关信息)
  • 数据区:
    • 初始化数据区(data段):包含了在程序中明确被初始化的全局变量,已经初始化的静态变量(包括全局静态变量和局部静态变量)和常量数据(如:字符串常量)
    • 未初始化数据区(bss段):存入全局未初始化变量和未初始化静态变量。未初始化数据区的数据在程序开始执行之前被内核初始化为0或者空NULL
    • 常量区
  • 栈区:系统为每一个应用程序分配一个临时的空间(局部变量,局部信息,函数参数,数组),栈区大小为:1M,在windowns中可以扩展到10M,在linux中可以扩展到16M
  • 堆区:存储大数据,图片,音频文件。
    • 手动开辟 malloc
    • 手动释放 free

栈区内存占用从高地址到低地址,数组的[0]从低地址开始。

两个连续的变量保存会存在一定的地址空缺是为了放置直接根据一个变量的地址推到下一个变量的地址。

栈区:先进后出,后进先出

  • 栈区(stack):栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值,返回值,局部变量等。在程序运行过程中实时加载和释放。因此:局部变量的生存周期为申请到释放该段栈空间。
  • 堆区(heap):堆是一个大容器,其容量要远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。

堆空间开辟和释放

1
void* malloc(size_t size);

内存的动态存储区(堆区)分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型,分配的内存空间内容不确定,一般使用memset初始化。

1
2
int* p = (int*) malloc (sizeof(int) *1024);
void free(void* ptr);

释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址,对同一内存空间释放多次会出错。

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 <stdlib.h>

int main()
{
//栈区大小
// int arr[820000*3] ={0};//段错误,核心转移

//开辟空间存储
int* p = (int*)malloc(sizeof(int)*1024);
printf("%p\n",p);
//使用堆空间
*p = 123;
printf("%d\n",*p);
//释放空间
free(p);

//p为野指针
printf("%p\n",p);
*p = 456;
printf("%d\n",*p);
return 0;
}

0x195b010
123
0x195b010
456

为了避免野指针的出现,一般将指针赋值为NULL.

开辟的空间使用指针或者数组的方式来进行操作

内存处理函数

1
2
3
#include <string.h>

void* memset(void* s,int c,size_t n);

将内存区域的前n个字节以参数c填入,返回值,s的首地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
int* p = (int*)malloc(sizeof(int)*10);

//重置内存空间的值
memset(p,'c',40);
for(int i=0; i<10;i++)
{
printf("%c\n",p[i]);
}

free(p);
return 0;
}
1
2
#include <string.h>
void* memcpy(void* dest, void* src, size_t n);

拷贝src所指的内存内容的前n个字节到dest所指的内存地址上。

注意:dest和src所指的内存空间不可重叠,可能会导致程序报错。

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>

int main()
{
int arr[] = {1,2,3,4,5,6,7,8,9,10};
int *p = (int*)malloc(sizeof(int)*10);

memcpy(p, arr, sizeof(int)*10);
for(int i=0; i<10; i++)
{
printf("%d ",p[i]);
}
free(p);
return 0;
}

memcpy()与strcpy()的区别:

字符串拷贝遇到\0则自动结束,内存拷贝不会出现类似情况

内存拷贝:拷贝的内容和字节有关,和拷贝内容无关

如果拷贝的目标和源发生重叠,可能报错

1
2
3
4
memmove()//用法和memcpy一样,区别在于:dest和src所指的内存空间重叠时,memmove()仍然能处理,不过执行效率比memcpy低

int memcmp(const void* st, const void* s2, size_t n);//比较s1和s2所指向内存区域的前n个字节
//返回值:0,1,-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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void fun(int* p)
{
//传值
p = (int*)malloc(sizeof(int)*10);
}

void fun1(int** p)
{
//传地址
*p = (int*)malloc(sizeof(int)*10);
printf("形参%p\n",*p);
}

int* fun2()
{
//返回地址
int* p = malloc(sizeof(int) *10);
return p;
}

int main()
{
//数组下标越界
// char* p = (char*)malloc(sizeof(char)*10);
// strcpy(p,"hello worldS");
// printf("%s\n",p);
// free(p);

//野指针
// int* p = (int*)malloc(0);
// printf("%p\n",p);
// *p = 100;
// printf("%d\n",*p);
// free(p); //windows下程序挂,linux似乎做了优化?
// return 0;

//多次释放空间
// int* p = malloc(sizeof(int)*10);
// free(p);
// //解决办法
// p = NULL; //空指针允许多次释放
// free(p);//放弃,核心已转储

int* p = NULL;
// fun(p); //形参和实参一致,都是值传递

fun1(&p); //地址传递
for(int i=0; i<10;i++)
{
p[i] = i;
printf("%d ",p[i]);
}
printf("\n");
free(p);

int* p1 = fun2();
for(int i=0; i<10;i++)
{
p1[i] = i;
printf("%d ",p1[i]);
}
free(p1);
return 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
27
28
29
#include <stdio.h>
#include <stdlib.h>

int main()
{
//开辟二级指针对应的堆空间
int** p = (int**)malloc(sizeof(int*) *5);
for(int i =0; i<5; i++)
{
//开辟一级指针对应的堆空间
p[i] = (int*)malloc(sizeof(int) * 3);
}
for(int i=0; i<5; i++)
{
for(int j=0; j<3; j++)
{
p[i][j] = i+j;
printf("%d ",p[i][j]);
}
}
//free
for(int i=0;i<5;i++)
{
free(p[i]);
}
free(p);

return 0;
}

结构体

数组:描述一组具有相同类型数据的有序集合,用于处理大量相同类型的数据运算。

有时需要将不同类型的数据组合成一个有机的整体。显然单独定义变量会比较繁琐,数据不方便管理

定义结构体变量的方式:

  • 先声明结构体类型再定义变量名struct stu{成员列表}; struct stu Mike;
  • 在声明类型的同时定义变量。struct stu{成员列表}Mike,Bod;
  • 直接定义结构体类型变量(无类型名).struct {成员变量} Mike,Bob;

结构体类型和结构体变量关系:

  • 结构体类型:指定了一个结构体类型,相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元
  • 结构体变量:系统根据结构体类型(内部成员状况)为止分配空间。

结构体数组:

结构体成员需要偏移对齐

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
#include <stdio.h>

struct Student{
char name[21];
int age;
char sex;
int score[3];
};

int main()
{
struct Student stu[3]=
{
{"黄x航",22,'M',89,90,89},
{"码x东",18,'F',89,54,65},
{"司正x",32,'M',89,98,98}

};

for(int i=0; i<3-1;i++)
{
for(int j=0; j<3-1-i;j++)
{
if(stu[j].age >stu[j+1].age)
{
struct Student temp = stu[j];
stu[j] = stu[j+1];
stu[j+1] = temp;
}
}
}
}

结构体赋值

用=可以进行复制

深拷贝和浅拷贝

如果结构体内部有指针指向堆内存,那么就不能使用编译器默认的赋值行为,应该手动控制赋值过程。

结构体指针

若结构体中包含有指针类型的成员数据,则在给结构体变量赋值的时候需要考虑指针赋值(是开辟新空间或是赋常量的值)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

struct stu{
char name[21];
int age;
int scores[3];
char addr[51];
};

int main()
{
//结构体指针
struct stu ss = {"test",30, 100,100,100,"边境"};
struct stu* p = &ss;
printf("%s\n",(*p).name);
printf("%s\n",p->addr);
return 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>

typedef struct student ss;
struct student
{
char* name;
int age;
int* scores;
char* addr;
};


int main()
{
ss* p = (ss*)malloc(sizeof(ss)*3);
for(int i=0; i<3; i++)
{
p[i].name = (char*)malloc(sizeof(char)*21);
p[i].scores = (int*)malloc(sizeof(int)*3);
p[i].addr = (char*)malloc(sizeof(char)*21);
}

for(int i=0; i<3; i++)
{
scanf("%s%d%d%d%d%s",p[i].name,&p[i].age,
&p[i].scores[0],&p[i].scores[1],&p[i].scores[2],p[i].addr);
}

for(int i=0; i<3; i++)
{
printf("%s ",p[i].name);
printf("%d ",p[i].age);
printf("%d ",p[i].scores[0]);
printf("%d ",(p+i)->scores[1]);
printf("%d ",(p+i)->scores[2]);
printf("%s\n",(p+i)->addr);
}

//释放存储空间
for(int i=0;i<3;i++)
{
free(p[i].name);
free(p[i].scores);
free(p[i].addr);
}
free(p);
return 0;
}

结构体做函数参数

  • 结构体普通变量做函数参数

  • 结构体指针变量做函数参数

  • 结构体数组名做函数参数

  • const修饰结构体指针形参变量

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>
#include <string.h>

typedef struct Student ss;
struct Student{
char name[21];
int age;
int score;
char addr[51];
};

void fun01(ss stu)
{
strcpy(stu.name,"lujunyi");
printf("%s\n",stu.name);
}

int main()
{
ss stu = {"宋江",50,101,"水船"};
fun01(stu);
printf("%s\n",stu.name); //值传递,不改变
return 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
27
28
29
//结构体指针
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct student ss;

struct student{
char name[21];
int age;
int score;
char addr[21];
};

void fun02(ss* p)
{
strcpy(p->name, "公孙胜");
printf("%s\n",p->name);
}

int main()
{
//结构体指针作为函数参数
ss stu = {"吴用",50, 101,"水泊梁山"};

fun02(&stu);
printf("%s\n",stu.name);
return 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
//结构体数组做函数参数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct student ss;

struct student{
char name[21];
int age;
int score;
char addr[21];
};

//数组作为函数参数退化为指针,丢失元素精度,需要传递个数
void BubbleSort(ss stu[],int len)
{
for(int i = 0; i<len-1; i++)
{
for(int j = 0; j<len-i-1; j++)
{
if(stu[j].age >stu[j+1].age)
{
ss temp = stu[j];
stu[j] = stu[j+1];
stu[j+1] = temp;
}
}
}
}

int main()
{
ss stu[3] =
{
{"鲁智深",30,78,"五台山"},
{"呼吁",29,78,"三台山"},
{"呈共",31,87,"滇池"}
};

BubbleSort(stu, 3);

for(int i=0;i<3;i++)
{
printf("%s\t%d\t%d\t%s\n",stu[i].name,stu[i].age,stu[i].score,stu[i].addr);
}
return 0;
}

方法介绍

方法能给用户自定义的类型添加新的行为。和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者

在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。

也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。

例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
37
package main

import "fmt"

type Person struct {
age int
}

func (p Person) howOld() int {
return p.age
}

func (p *Person) growUp() {
p.age += 1
}

func main() {
// qcrao 是值类型
qcrao := Person{age: 18}

// 值类型 调用接收者也是值类型的方法
fmt.Println(qcrao.howOld())

// 值类型 调用接收者是指针类型的方法
qcrao.growUp()
fmt.Println(qcrao.howOld())

// stefno 是指针类型
stefno := &Person{age: 100}

// 指针类型 调用接收者是值类型的方法
fmt.Println(stefno.howOld())

// 指针类型 调用接收者也是指针类型的方法
stefno.growUp()
fmt.Println(stefno.howOld())
}

输出结果:

1
2
3
4
18
19
100
101

调用了 growUp 函数后,不管调用者是值类型还是指针类型,它的 Age 值都改变了。

编译器背后工作

实际上,当类型和方法的接收者类型不同时,其实是编译器在背后做了一些工作,用一个表格来呈现:

- 值接收者 指针接收者
值类型调用者 方法会使用调用者的一个副本,类似于“传值” 使用值的引用来调用方法,上例中,qcrao.growUp() 实际上是 (&qcrao).growUp()
指针类型调用者 指针被解引用为值,上例中,stefno.howOld() 实际上是 (*stefno).howOld() 实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针

值接收者和指针接收者

不管接收者类型是值类型还是指针类型,都可以通过值类型或指针类型调用,这里面实际上通过语法糖起作用的。

结论:实现了接收者是值类型的方法,相当于自动实现了接收者是指针类型的方法;而实现了接收者是指针类型的方法,不会自动生成对应接收者是值类型的方法。

例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
package main

import "fmt"

type coder interface {
code()
debug()
}

type Gopher struct {
language string
}

func (p Gopher) code() {
fmt.Printf("I am coding %s language\n", p.language)
}

func (p *Gopher) debug() {
fmt.Printf("I am debuging %s language\n", p.language)
}

func main() {
var c coder = &Gopher{"Go"}
c.code()
c.debug()
}

上述代码里定义了一个接口 coder,接口定义了两个函数:

1
2
code()
debug()

接着定义了一个结构体 Gopher,它实现了两个方法,一个值接收者,一个指针接收者。

最后,我们在 main 函数里通过接口类型的变量调用了定义的两个函数。

运行,结果:

1
2
I am coding Go language
I am debuging Go language

如果把 main 函数的第一条语句换一下:

1
2
3
4
5
func main() {
var c coder = Gopher{"Go"}
c.code()
c.debug()
}

运行一下,报错:

1
2
3
# command-line-arguments
src/learn/tongbu/tongbu.go:25:6: cannot use Gopher{...} (type Gopher) as type coder in assignment:
Gopher does not implement coder (debug method has pointer receiver)

两处代码的差别: 第一次是将 &Gopher 赋给了 coder;第二次则是将 Gopher 赋给了 coder

第二次报错是说,Gopher 没有实现 coder。很明显,因为 Gopher 类型并没有实现 debug 方法;表面上看, *Gopher 类型也没有实现 code 方法,但是因为 Gopher 类型实现了 code 方法,所以让 *Gopher 类型自动拥有了 code 方法。

当然,上面的说法有一个简单的解释:接收者是指针类型的方法,很可能在方法中会对接收者的属性进行更改操作,从而影响接收者;而对于接收者是值类型的方法,在方法中不会对接收者本身产生影响。

所以,当实现了一个接收者是值类型的方法,就可以自动生成一个接收者是对应指针类型的方法,因为两者都不会影响接收者。但是,当实现了一个接收者是指针类型的方法,如果此时自动生成一个接收者是值类型的方法,原本期望对接收者的改变(通过指针实现),现在无法实现,因为值类型会产生一个拷贝,不会真正影响调用者。

最后,需要记住:

如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。

适用场合

如果方法的接收者是值类型,无论调用者是对象还是对象指针,修改的都是对象的副本,不影响调用者;如果方法的接收者是指针类型,则调用者修改的是指针指向的对象本身。

使用指针作为方法的接收者的理由:

  • 方法能够修改接收者指向的值。
  • 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。

是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的本质

如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 header,而 header 本身就是为复制设计的。

如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份实体

参考资料

【飞雪无情 Go实战笔记】https://www.flysnow.org/2017/04/03/go-in-action-go-interface.html

【何时使用指针接收者】http://ironxu.com/711

【理解Go Interface】http://lanlingzi.cn/post/technical/2016/0803_go_interface/

要开发一个C语言程序,让它输出红色的文字,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main(){
#if _WIN32
system("color 0c");
printf("xxxxtest1\n");
#elif __linux__
printf("\033[22;31mxxxtest2m");
#else
printf("xxxxtest3n");
#endif

return 0;
}

#if、#elif、#else 和 #endif 都是预处理命令,整段代码的意思是:如果宏 WIN32 的值为真,就保留第 4、5 行代码,删除第 7、9 行代码;如果宏 _linux 的值为真,就保留第 7 行代码;如果所有的宏都为假,就保留第 9 行代码。

这些操作都是在预处理阶段完成的,多余的代码以及所有的宏都不会参与编译,不仅保证了代码的正确性,还减小了编译后文件的体积。

这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能。

#if用法

#if 用法的一般格式为:

1
2
3
4
5
6
7
8
9
\#if 整型常量表达式1
程序段1
\#elif 整型常量表达式2
程序段2
\#elif 整型常量表达式3
程序段3
\#else
程序段4
\#endif

它的意思是:如常“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else。这一点和 if else 非常类似。

#elif 和 #else 也可以省略,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
int main(){
#if _WIN32
printf("This is Windows!\n");
#else
printf("Unknown platform!\n");
#endif

#if __linux__
printf("This is Linux!\n");
#endif

return 0;
}

#ifdef用法

#ifdef 用法的一般格式为:

1
2
3
4
5
#ifdef  宏名
程序段1
#else
程序段2
#endif

它的意思是,如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。

也可以省略 #else:

1
2
3
#ifdef  宏名
程序段
#endif

VS/VC 有两种编译模式,Debug 和 Release。在学习过程中,我们通常使用 Debug 模式,这样便于程序的调试;而最终发布的程序,要使用 Release 模式,这样编译器会进行很多优化,提高程序运行效率,删除冗余信息。

为了能够清楚地看到当前程序的编译模式,我们不妨在程序中增加提示,请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
int main(){
#ifdef _DEBUG
printf("正在使用 Debug 模式编译程序...\n");
#else
printf("正在使用 Release 模式编译程序...\n");
#endif

system("pause");
return 0;
}

当以 Debug 模式编译程序时,宏 _DEBUG 会被定义,预处器会保留第 5 行代码,删除第 7 行代码。反之会删除第 5 行,保留第 7 行。

#ifndef 的用法

#ifndef 用法的一般格式为:

1
2
3
4
5
#ifndef 宏名
程序段1
#else
程序段2
#endif

与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。

区别与注意

#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#define NUM 10
int main(){
#if NUM == 10 || NUM == 20
printf("NUM: %d\n", NUM);
#else
printf("NUM Error\n");
#endif
return 0;
}

再如,两个宏都存在时编译代码A,否则编译代码B:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#define NUM1 10
// #define NUM2 20
int main(){
#if (defined NUM1 && defined NUM2)
//代码A
printf("NUM1: %d, NUM2: %d\n", NUM1, NUM2);
#else
//代码B
printf("Error\n");
#endif
return 0;
}
1
#ifdef 可以认为是 #if defined 的缩写

Ubuntu环境变量的添加和删除

添加环境变量的位置

  • /etc/profile 该文件为系统的每个用户设置环境信息,当用户第一次登录时,该文件被执行,并从/etc/profile.d目录的配置文件中搜集shell的设置。
  • /etc/environment 登录操作系统使用的第二个文件,系统在读取自己的profile之前,设置环境文件的环境变量
  • /etc/bashrc 为每一个运行bash shell的用户执行该文件。当bash shell被打开时,该文件被读取
  • ~/.profile 每个用户都可以使用该文件输入专用于自己使用的shell信息,当用户登录时,该文件仅仅执行一次。默认情况下设置一些环境变量,执行用户的.bashrc文件
  • ~/.bashrc 该文件包含专用的bash shell的bash信息,当登录以及每次打开新的shell时,该文件被读取。

添加方法

  • 方法一:直接修改/etc/enviroment文件,这种方法的作用域是全局的,永久性的。
1
2
3
#打开/etc/environment文件,其内容如下:
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games"
#在环境变量PATH中添加你要添加的路径即可。
  • 方法二:修改/etc/profile文件,这种方法的作用域是全局的,永久性的。
1
2
3
#这个文件不是保存环境变量信息的文件,在登录时,系统只是根据它的内容对环境变量进行设置。
export PATH=$PATH:[your path1]:[your path2]:[...]
export PATH=[your path1]:[your path2]:[...]:$PATH #其中,各个PATH之间用冒号分隔,$PATH指代添加your path前的环境变量。
  • 方法三:修改/etc/bashrc或者~/.bashrc文件,这两个文件不是为了保存环境变量,只是在使用bash shell时进行设置而已,所以设置方法和二中一样。对于/etc/bashrc文件,修改的作用于所有用户,但对于~/.bashrc文件,修改的仅仅作用于当前用户。这种修改的作用也是永久性的。

  • 方法四:修改~/.profile文件,作用仅限于当前用户,但同样也是永久性的。这种方法和修改/etc/profile本质上是一样的,这种之不过是仅仅修改了当前用户自己的配置文件。所以作用仅限于当前用户,但同样也是永久性的。

  • 方法五:在Terminal中使用shell命令,只在当前Terminal中起作用,关闭了当前Terminal就无效了。对其他Terminal也无效

显示环境变量

  • 显示所有环境变量
1
env #打印所有的环境变量
  • 显示指定环境变量
1
echo $PATH #打印PATH环境变量

让修改生效

使用source命令也可以让修改立即生效。使用方法为:

1
source [file name]  # file name 指的是上面修改过的文件的file name

删除对环境变量的修改

将以上方法中对配置文件的修改还原回去即可。另外,由于第五种方法由于是临时性质的且局部的,只需要关闭这个Terminal就好了。

主要目标

这个项目的主要目标是识别和测量摄像头拍摄到的照片中的物体。具体要求为:在隔摄像头(罗技170,500万像素)1米外放置一块白板,白板上贴上一张白纸,白纸上用黑色笔标注出一个70x50的矩形框,然后放置一些色块和实物的打印图片粘贴到黑色矩形框内(色块颜色包括:黑色、黄色、绿色、蓝色,形状包括:圆形、椭圆、矩形、正方形,实物包括:绿箭盒、曲曲饼盒、方便面盒、可乐盒)。具体的情况差不多和下图相似(像素不高,和实际略有区别):

image

项目中需要设计图形化界面,以黑色矩形框为标定,标定结束后准确识别出物体的种类并测量出物体的长宽、中心点、面积、偏转角。比赛中声明每个物体有编号,如下:

1
2
3
4
//圆形、正方形、长方形、椭圆形4种(ID依次为1, 2, 3, 4)
//可乐罐、口香糖、方便桶面、饼干盒4种(ID依次为81, 82, 83, 84)
//黑、红、黄、绿、蓝5种(ID依次为1, 2, 3, 4, 5)
//ID[2]:第一个为颜色,第二个为形状

解决方法

技术方案

Opencv+QT:Opencv做摄像头图像采集,QT(QT部分中规中举,后面就不怎么提啦)提供输入输出。

检测的基本流程

  • 1.对拍摄的图像进行高斯过滤,二值化,轮廓查找,然后找轮廓的contours进行判断

    • 图像的二值化就是将图像上的像素点的灰度值设置为0或255,这样将使整个图像呈现出明显的黑白效果。与边缘检测相比,轮廓检测有时能更好的反映图像的内容,而要对图像进行轮廓检测,则必须要先对图像进行二值化,在数字图像处理中,二值图像占有非常重要的地位,图像的二值化使图像中数据量大为减少,从而能凸显出目标的轮廓。
    • threshold 方法是通过遍历灰度图中点,将图像信息二值化,处理过后的图片只有两种色值。
  • 2.将面积小于400的轮廓滤掉(有点利用比赛规则的意思了),进行矩形的判断:

    • 使用多边形逼近的点的个数来判断是否是矩形(如果是则到步骤4)
    • 如果满足凸多边形而且有四个逼近点说明是矩形,这里限制了轮廓面积大于800
    • 计算最小外接矩形,滤掉照片的轮廓矩形框(最外面的标定框)
    • 判断杂色
      • 如果有杂色,按照长宽比可将可乐罐和其他过滤,否则是绿箭
      • 如果没有杂色,通过长宽比(长和宽差值在4个像素内)计算是正方形还是矩形,然后特别计算正方形角度(-45度到+45度)
  • 3.如果前面没有检测到,则检测圆

    • 先求出轮廓的最小外接圆和最小外接椭圆
    • 计算外接的圆的半径(通常大近3个像素)和用面积计算出的半径,差值小于4,外界椭圆的面积差值小于30,说明大致使用圆的形状
    • 检测杂色
      • 如果是杂色,说明是奥利奥,通过最小外接矩形计算
      • 如果不是杂色,计算圆的信息
    • 如果计算出来不符,就进行椭圆的判定,计算的出来的半径大于4最小外接椭圆的面积和轮廓面积差值小于30
    • 判断杂色
      • 如果是杂色,可能是可乐罐(较少),通过可乐罐的比值限定小于或大于某个阈值进行计算
      • 如果不是杂色,计算椭圆的信息
  • 4.如果还没检测到,只可能是可乐罐或者方便面

    • 判断杂色,非杂色不用管,通过可乐罐的小于0.6或者大于1.6计算
    • 否则是方便面直接计算.

杂色判断

在上面这个流程中,判断是色块和实物的方法其实一个简单办法就是判断这个图形中是否有杂色。判断杂色我们使用的是纯RGB进行颜色判断,RGB检测中本来检测颜色的方法使用的是给定一个阈值范围,因为RGB值{R,G,B}(0≤R,G,B≤255)比较明显。但是由于摄像头的色差,最后出来的结果比较不理想。于是我们进行了一些小的改进。

方法思路

传入一个轮廓的中心点。然后从改点往四个方向进行DFS,判断下一个点和这个点之间各个通道的RGB值的差值,如果差值大于20,那么就说明是杂色。如果不是杂色,那么根据中心点的RGB三个通道的差值来进行判断。
找轮廓和中心点的方法就不写了。现在已经有中心点了。具体如图:

中心点

将dfs到的点在二值图上打亮,得到的结果可以看到扫描的区域比较理想。

打亮二值图

经过调试参数之后准确地得到了最后结果:

结果图

具体实现

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
//---------检测是否纯色-----------
int dir[4][2]={
0,1,1,0,0,-1,-1,0
};//往四个方向进行搜索
int check(int x,int y)
{
if(x>0&&x<=cvGetSize(thrImg).width&&y>0&&y<=cvGetSize(thrImg).height)
return 1;
return 0;
}

void dfs(int x,int y,int watch[3],int depth)
{
//对该点的颜色进行读取,存到watchi[3]中
int watchi[3];
for(int i=0;i<3;i++)
watchi[i] = originMat.at<Vec3b>(y, x)[i];

//是否在二值图上显示dfs到的点
// cout<<x<<":"<<y<<endl<<"0:"<<watch[0]<<" 1:"<<watch[1]<<" 2:"<<watch[2]<<endl;
// cvSetReal2D(thrImg, y, x, 255.0);
// cvShowImage("colorJudge", thrImg);
// cvWaitKey();

//进行判断,对watch值进行比较
for(int i=0;i<3;i++)
{
if(abs(watch[i]-watchi[i])>20)
mix=1;
}
for(int i=0;i<4;i++)
{
int dx=x+dir[i][0];
int dy=y+dir[i][1];
if(check(dx,dy)&&depth<9&&!vis[dy][dx]){
vis[dy][dx]=1;
dfs(dx,dy,watchi,depth+1);
}
}
}

int isColorPure(int x,int y)
{//传值按照x,y
int ans=isColorPure(x,y,0);
return ans;
}
int isColorPure(int x,int y,int depth)
{//传值按照x,y
memset(vis,0,sizeof(vis));
//对该点的颜色进行读取,存到watchi[3]中
mix=0;
int watchi[3];
for(int i=0;i<3;i++)
watchi[i] = originMat.at<Vec3b>(y, x)[i];

vis[y][x]=1;
dfs(x,y,watchi,depth);
// cvWaitKey();
if(mix==1){
return -1;
}else{
return getColor(y,x);
}
}

得到某个点的颜色。首先根据差值来判断(用这个就能有返回值了,后面的基本没作用),如果判断不出来则根据范围判断。

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
int getColor(int x,int y)
{ //传值按照y,x
// imshow("mat",originMat);
// 源图像载入及判断
if( !originMat.data )
return -1;
Mat tempImage = originMat.clone();
int watch[3],flag[3];
flag[0]=flag[1]=flag[2]=0;
for(int i=0;i<3;i++)
watch[i] = originMat.at<Vec3b>(x, y)[i];

for(int i=0;i<3;i++){
if(watch[i]>colorRecgnize) //
flag[i]=1;
}//BGR蓝绿红

cvSetReal2D(thrImg, x, y, 255.0);
cvShowImage("colorJudge", thrImg);
// cvSet2D(originImg,x,y, cvScalar(0, 255, 0, 0)); //绘制来查看检测点的位置
// cvShowImage("yanse", originImg);

int colorRange=40;
//通过BGR之间的差值来进行判断
if(abs(watch[0]-watch[1]<colorRange)&&abs(watch[0]-watch[2])<colorRange&&abs(watch[1]-watch[2])<colorRange){
return 1;//黑
}else if(watch[2]-watch[0]>colorRange&&watch[2]-watch[1]>colorRange){
return 2;//红
}else if(watch[2]-watch[0]>colorRange&&watch[1]-watch[0]>colorRange&&abs(watch[1]-watch[2])<colorRange){
return 3;//黄
}else if(watch[1]-watch[0]>colorRange&&watch[1]-watch[2]>colorRange){
return 4;//绿
}else if(watch[0]-watch[1]>colorRange&&watch[0]-watch[2]>colorRange){
return 5;//蓝
}
//-------------以上通过差值进行判断-----------
// cvSetReal2D(thrImg, x, y, 255.0);
// cvShowImage("colorJudge", thrImg);
// cvSet2D(originImg,x,y, cvScalar(0, 255, 0, 0)); //绘制来查看检测点的位置
// cvShowImage("yanse", originImg);
if(flag[0]==0&&flag[1]==1&&flag[2]==1){
return 3;//黄
}else if(flag[0]==1&&flag[1]==0&&flag[2]==0){
return 5;//蓝
}else if(flag[0]==0&&flag[1]==1&&flag[2]==0){
return 4;//绿
}else if(flag[0]==0&&flag[1]==0&&flag[2]==1){
return 2;//红
}else if(flag[0]==0&&flag[1]==0&&flag[2]==0){
return 1;//黑
}
return 5; //都检测不出来返回蓝色
}

学习教程

  1. IplImage, CvMat, Mat 的关系和相互转换
  2. OpenCV函数cvFindContours
  3. 提取轮廓两种方法及绘制轮廓中最大等级分析
  4. OpenCV中寻找轮廓函数cvFindContours的使用说明以及序列cvSeq的用法说明
  5. RGB坐标像素的储存和提取
  6. opencv 连通区域边界坐标提取
  7. OpenCV霍夫变换:霍夫线变换,霍夫圆变换合辑
  8. 均值,中值,高斯滤波
  9. 检测矩形的参考&&检测圆,直线
  10. 霍夫圆变换函数HoughCircles
  11. OpenCV霍夫变换识别圆

  12. 使用Mat的opencv中的椭圆拟合

  13. OpenCV画轮廓的外界圆矩形椭圆等
  14. 利用cvMinAreaRect2求取轮廓最小外接矩形
  15. CvBox2D说明

后记

其实整个项目比较简单,但意外的参加这种比赛也算是长了自己的见识,在此也特别感谢为我们提供了帮助的老师。

shell简介

基本定义

  • 一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。
  • Shell 既是一种命令语言,又是一种程序设计语言

  • Shell 是指一种应用程序,这个应用程序提供了一个界面,用户通过这个界面访问操作系统内核的服务

真正能够控制计算机硬件(CPU、内存、显示器等)的只有操作系统内核(Kernel),图形界面和命令行只是架设在用户和内核之间的一座桥梁。由于安全、复杂、繁琐等原因,用户不能直接接触内核(也没有必要),需要另外再开发一个程序,让用户直接使用这个程序;该程序的作用就是接收用户的操作(点击图标、输入命令),并进行简单的处理,然后再传递给内核。如此一来,用户和内核之间就多了一层“代理”,这层“代理”既简化了用户的操作,也保护了内核。用户界面和命令行就是这个另外开发的程序,就是这层“代理”。在Linux下,这个命令行程序叫做 Shell

shell的作用

  • 调用其他程序,给其他程序传递数据或参数,并获取程序的处理结果;
  • 在多个程序之间传递数据,把一个程序的输出作为另一个程序的输入;
  • Shell 本身也可以被其他程序调用。

Shell 本身支持的命令并不多,但是它可以调用其他的程序,每个程序就是一个命令,这使得 Shell 命令的数量可以无限扩展,其结果就是 Shell 的功能非常强大,完全能够胜任 Linux 的日常管理工作,如文本或字符串检索、文件的查找或创建、大规模软件的自动部署、更改系统设置、监控服务器性能、发送报警邮件、抓取网页内容、压缩文件等。

Shell 主要用来开发一些实用的、自动化的小工具,而不是用来开发具有复杂业务逻辑的中大型软件,例如检测计算机的硬件参数、一键搭建Web开发环境、日志分析等。

shell脚本

任何代码最终都要被“翻译”成二进制的形式才能在计算机中执行。

  • 编译型语言:

    • 如 C/C++、Pascal、Go语言、汇编等,必须在程序运行之前将所有代码都翻译成二进制形式,也就是生成可执行文件,用户拿到的是最终生成的可执行文件,看不到源码。
    • 这个过程叫做编译(Compile),这样的编程语言叫做编译型语言,完成编译过程的软件叫做编译器(Compiler)。
    • 编译型语言的优点是执行速度快、对硬件要求低、保密性好,适合开发操作系统、大型应用程序、数据库等。
  • 解释型语言或者脚本语言(Script)

    • 如 Shell、JavaScript、Python、PHP等,需要一边执行一边翻译,不会生成任何可执行文件,用户必须拿到源码才能运行程序。程序运行后会即时翻译,翻译完一部分执行一部分,不用等到所有代码都翻译完。
    • 这个过程叫做解释,这样的编程语言叫做解释型语言或者脚本语言(Script),完成解释过程的软件叫做解释器
    • 脚本语言的优点是使用灵活、部署容易、跨平台性好,非常适合Web开发以及小工具的制作。
    • Shell 就是一种脚本语言,我们编写完源码后不用编译,直接运行源码即可。

Shell 脚本很适合处理纯文本类型的数据,而 Linux 中几乎所有的配置文件、日志文件(如 NFS、Rsync、Httpd、Nginx、MySQL 等),以及绝大多数的启动文件都是纯文本类型的文件。

Shell 脚本是实现 Linux 系统自动管理以及自动化运维所必备的工具,Linux 的底层以及基础应用软件的核心大都涉及 Shell 脚本的内容。

Shell 脚本的优势在于处理偏操作系统底层的业务,例如,Linux 内部的很多应用(有的是应用的一部分)都是使用 Shell 脚本开发的,因为有 1000 多个 Linux 系统命令为它作支撑。

常见的shell:sh, bash, csh, tcsh, ash

Linux由多个组织机构开发,不同的组织机构为了发展自己的 Linux 分支可能会开发出功能类似的软件,它们各有优缺点,用户可以自由选择。Shell 就是这样的一款软件,不同的组织机构开发了不同的 Shell,它们各有所长,有的占用资源少,有的支持高级编程功能,有的兼容性好,有的重视用户体验。

Shell 既是一种脚本编程语言,也是一个连接内核和用户的软件。

常见的 Shell 有 sh、bash、csh、tcsh、ash 等。

sh

sh 的全称是 Bourne shell,由 AT&T 公司的 Steve Bourne开发,为了纪念他,就用他的名字命名。

sh 是 UNIX 上的标准 shell,很多 UNIX 版本都配有 sh。sh 是第一个流行的 Shell。

csh

sh 之后另一个广为流传的 shell 是由柏克莱大学的 Bill Joy (Bill Joy 是一个风云人物,他创立了 BSD 操作系统,开发了 vi 编辑器,还是 Sun 公司的创始人之一)。设计的,这个 shell 的语法有点类似C语言,所以才得名为 C shell ,简称为 csh。

BSD 是 UNIX 的一个重要分支,后人在此基础上发展出了很多现代的操作系统,最著名的有 FreeBSD、OpenBSD 和 NetBSD,就连 Mac OS X 在很大程度上也基于BSD。

tcsh

tcsh 是 csh 的增强版,加入了命令补全功能,提供了更加强大的语法支持。

ash

一个简单的轻量级的 Shell,占用资源少,适合运行于低内存环境,但是与下面讲到的 bash shell 完全兼容。

bash

bash shell 是 Linux 的默认 shell. bash 兼容 sh :针对 sh 编写的 Shell 代码可以不加修改地在 bash 中运行

bash 和 sh 的一些不同之处:

  • bash 扩展了一些命令和参数;
  • bash 并不完全和 sh 兼容,它们有些行为并不一致,但在大多数企业运维的情况下区别不大,特殊场景可以使用 bash 代替 sh。

shell查看

Shell 是一个程序,一般都是放在/bin或者/user/bin目录下,当前 Linux 系统可用的 Shell 都记录在/etc/shells文件中。/etc/shells是一个纯文本文件,你可以在图形界面下打开它,也可以使用 cat 命令(cat(英文全拼:concatenate)命令用于连接文件并打印到标准输出设备上)查看。

通过 cat 命令来查看当前 Linux 系统的可用 Shell:

1
2
3
4
5
$cat /etc/shells
/bin/sh
/bin/dash
/bin/bash
/bin/rbash

在现代的 Linux 上,sh 已经被 bash 代替,/bin/sh往往是指向/bin/bash的符号链接。

如果希望查看当前 Linux 的默认 Shell,那么可以输出 SHELL 环境变量:

1
2
$ echo $SHELL
/bin/bash

输出结果表明默认的 Shell 是 bash。

echo是一个 Shell 命令,用来输出变量的值,SHELL是 Linux 系统中的环境变量,它指明了当前使用的 Shell 程序的位置,也就是使用的哪个 Shell。

终端使用shell

一种进入 Shell 的方法是让 Linux 系统退出图形界面模式,进入控制台模式,这样一来,显示器上只有一个简单的带着白色文字的“黑屏”,就像图形界面出现之前的样子。这种模式称为 Linux 控制台(Console)。

现代 Linux 系统在启动时会自动创建几个虚拟控制台(Virtual Console),其中一个供图形桌面程序使用,其他的保留原生控制台的样子。虚拟控制台其实就是 Linux 系统内存中运行的虚拟终端(Virtual Terminal)。

从图形界面模式进入控制台模式也很简单,往往按下Ctrl + Alt + Fn(n=1,2,3,4,5...)快捷键就能够来回切换。

例如,CentOS 在启动时会创建 6 个虚拟控制台,按下快捷键Ctrl + Alt + Fn(n=2,3,4,5,6)可以从图形界面模式切换到控制台模式,按下Ctrl + Alt + F1可以从控制台模式再切换回图形界面模式。可以发现,1号控制台被图形桌面程序占用了。

Ubuntu中Ctrl + Alt + F7对应图形界面。

输入用户名和密码,登录成功后就可以进入 Shell 了。$是命令提示符,我们可以在它后面输入 Shell 命令。

在图形界面模式下,输入密码时往往会显示为,密码有几个字符就显示几个;而在控制台模式下,输入密码什么都不会显示,好像按键无效一样,但只要输入的密码正确就能够登录。

图形界面也是一个程序,会占用CPU时间和内存空间,当 Linux 作为服务器系统时,安装调试完毕后,应该让 Linux 运行在控制台模式下,以节省服务器资源。正是由于这个原因,很多服务器甚至不安装图形界面程序,管理员只能使用命令来完成各项操作。

在Ubuntu中也可以用快捷键Ctrl + Alt + t快速启动一个终端。打开终端后即可输入Shell命令。

shell提示符

启动终端模拟包或者从 Linux 控制台登录后,便可以看到 Shell 提示符。

对于普通用户,Base shell 默认的提示符是美元符号$;对于超级用户(root 用户),Bash Shell 默认的提示符是井号#(可使用sudo su切换到超级用户)。该符号表示 Shell 等待输入命令。

同的 Linux 发行版使用的提示符格式不同。例如在 Ubuntu中,默认的提示符格式为:dongshifu@dong:~$

这种格式包含了以下三个方面的信息:

  • 启动 Shell 的用户名,也即 dongshifu;
  • 本地主机名称,也即dong;
  • 当前目录,波浪号~是主目录的简写表示法。

shell脚本编辑与运行

打开文本编辑器,新建文件,扩展名为sh(sh代表shell),扩展名并不影响脚本执行,见名知意即可。

输入shell代码:

1
2
#!/bin/bash
echo "Hello World !"

“#!” 是一个约定的标记,它告诉系统这个脚本需要什么解释器来执行,即使用哪一种Shell。echo命令用于向窗口输出文本。

运行shell脚本的方法:

  • 作为可执行程序

将上面的代码保存为test.sh,并 cd 到相应目录:

1
2
chmod +x ./test.sh  #使脚本具有执行权限
./test.sh #执行脚本

注意,一定要写成./test.sh,而不是test.sh。运行其它二进制的程序也一样,直接写test.sh,linux系统会去PATH里寻找有没有叫test.sh的,而只有/bin, /sbin, /usr/bin,/usr/sbin等在PATH里,你的当前目录通常不在PATH里,所以写成test.sh是会找不到命令的,要用./test.sh告诉系统说,就在当前目录找。

通过这种方式运行bash脚本,第一行一定要写对,好让shell查找到正确的解释器。

  • 作为解释器参数

这种运行方式是,直接运行解释器,其参数就是shell脚本的文件名,如:

1
/bin/bash test.sh

这种方式运行的脚本,不需要在第一行指定解释器信息,写了也没用。

例子:用read命令从stdin获取输入并赋值给PERSON变量,最后在stdout输出:

1
2
3
4
#!/bin/bash
echo "what is your name?"
read PERSON
echo "Hello, $PERSON"

shell变量:shell变量的定义、删除变量、只读变量、变量类型

脚本语言在定义变量时通常不需要指明类型,直接赋值就可以,Shell 变量也遵循这个规则。

在 Bash shell 中,每一个变量的值都是字符串,无论你给变量赋值时有没有使用引号,值都会以字符串的形式存储。这意味着,Bash shell 在默认情况下不会区分变量类型,即使你将整数和小数赋值给变量,它们也会被视为字符串,这一点和大部分的编程语言不同。

如果有必要,你也可以使用 declare 关键字显式定义变量的类型,但在一般情况下没有这个需求。

定义变量

三种定义变量方式:

1
2
3
variable=value
variable='value'
variable="value"

variable 是变量名,value 是赋给变量的值。如果 value 不包含任何空白符(例如空格、Tab缩进等),那么可以不使用引号;如果 value 包含了空白符,那么就必须使用引号包围起来。使用单引号和使用双引号有区别。

注意,赋值号的周围不能有空格

Shell 变量的命名规范和大部分编程语言都一样:

  • 变量名由数字、字母、下划线组成;
  • 必须以字母或者下划线开头;
  • 不能使用 Shell 里的关键字(通过 help 命令可以查看保留关键字)。

使用变量

使用一个定义过的变量,只要在变量名前面加美元符号$即可,如:

1
2
skill="Java"
echo "I am good at ${skill}Script"

变量名外面的花括号{ }是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,如果不给 skill 变量加花括号,写成echo "I am good at $skillScript",解释器就会把 $skillScript 当成一个变量(其值为空),代码执行结果就不是我们期望的样子了。

修改变量的值

已定义的变量,可以被重新赋值,如:

1
2
3
4
lang=shell
echo ${lang}
lang=python
echo ${lang}

第二次对变量赋值时不能在变量名前加

单引号与双引号的区别

1
2
3
4
5
6
7
8
#!/bin/bash

test="you are so cute"
chare='hi,${test}'
chare2="hi,${test}"

#hi,${test}
#hi,you are so cute

以单引号' '包围变量的值时,单引号里面是什么就输出什么,即使内容中有变量和命令(命令需要反引起来)也会把它们原样输出。这种方式比较适合定义显示纯字符串的情况,即不希望解析变量、命令等的场景。

以双引号” “包围变量的值时,输出时会先解析里面的变量和命令,而不是把双引号中的变量名和命令原样输出。这种方式比较适合字符串中附带有变量和命令并且想将其解析后再输出的变量定义。

如果变量的内容是数字,那么可以不加引号;如果真的需要原样输出就加单引号;其他没有特别要求的字符串等最好都加上双引号,定义变量时加双引号是最常见的使用场景。

将命令的结果赋值给变量

shell支持将命令的执行结果赋值给变量,常见的方式为:

1
2
variable=`command`
variable=$(command)

eg:

1
2
test=$(ls -al)
echo $test

只读变量

使用 readonly 命令可以将变量定义为只读变量,只读变量的值不能被改变。

1
2
3
4
5
test="you are so cute"
readonly test
test="you"

#bash: test: 只读变量

删除变量

使用 unset命令可以删除变量。语法:

1
unset variable_name

变量被删除后不能再次使用;unset 命令不能删除只读变量。

变量类型

运行shell时,会同时存在三种变量:

1) 局部变量

局部变量在脚本或命令中定义,仅在当前shell实例中有效,其他shell启动的程序不能访问局部变量。

2) 环境变量

所有的程序,包括shell启动的程序,都能访问环境变量,有些程序需要环境变量来保证其正常运行。必要的时候shell脚本也可以定义环境变量。

3) shell变量

shell变量是由shell程序设置的特殊变量。shell变量中有一部分是环境变量,有一部分是局部变量,这些变量保证了shell的正常运行

shell特殊变量:Shell $0, $#, $​*, $@, $?, $$和命令行参数

变量 含义
$0 当前脚本的文件名
$n 传递给脚本或函数的参数。n 是一个数字,表示第几个参数。例如,第一个参数是$1,第二个参数是$2。
$# 传递给脚本或函数的参数个数。
$* 传递给脚本或函数的所有参数。
$@ 传递给脚本或函数的所有参数。被双引号(“ “)包含时,与 $* 稍有不同,下面将会讲到。
$? 上个命令的退出状态,或函数的返回值。
$$ 当前Shell进程ID。对于 Shell 脚本,就是这些脚本所在的进程ID。

命令行参数

运行脚本时传递给脚本的参数称为命令行参数。命令行参数用 $n$ 表示,例如,$1 表示第一个参数,$2 表示第二个参数,依次类推。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/bin/bash

echo "file name: $0"
echo "First parameter: $1"
echo "Second parameter: $2"
echo "Quoted values: $@"
echo "Quoted values: $*"
echo "Total number of parameters: $#"

#运行结果
./test1.sh shifu dong
file name: ./test1.sh
First parameter: shifu
Second parameter: dong
Quoted values: shifu dong
Quoted values: shifu dong
Total number of parameters: 2

$* ,$@的区别

$* 和 $@ 都表示传递给函数或脚本的所有参数,不被双引号(“ “)包含时,都以”$1” “$2” … “$n” 的形式输出所有参数。

但是当它们被双引号(“ “)包含时,”$*” 会将所有的参数作为一个整体,以”$1 $2 … ​$n”的形式输出所有参数;”​$@” 会将各个参数分开,以”$1” “$2” … “$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
#!/bin/bash
echo "\$*=" $*
echo "\"\$*\"=" "$*"

echo "\$@=" $@
echo "\"\$@\"=" "$@"

echo "print each param from \$*"
for var in $*
do
echo "$var"
done

echo "print each parm from \$@"
for var in $@
do
echo $var
done

echo "print each parm form \"\$*\""
for var in "$*"
do
echo $var
done

echo "print each parm from \"\$@\""
for var in "$@"
do
echo $var
done

运行结果:
./test2.sh "A" "B"
$*= A B
"$*"= A B
$@= A B
"$@"= A B
print each param from $*
A
B
print each parm from $@
A
B
print each parm form "$*"
A B
print each parm from "$@"
A
B

hexo 文章管理

1.增加文章

1
hexo new xx

创建的文件自动保存在source/_post文件夹下,为MarkDown格式

可以在文件中开头通过:

1
2
3
4
5
6
7
8
9
10
11
---
title: 标题 # 自动创建,如 hello-world
date: 日期 # 自动创建,如 2019-09-22 01:47:21
tags:
- 标签1
- 标签2
- 标签3
categories:
- 分类1
- 分类2
---

来添加文章的必要信息。

2.标签页添加

在项目的根目录下执行命令

1
hexo new page tags

执行命令后自动生成一个source/tags/index.md文件,内容如下:

1
2
3
4
---
title: tags
date: 2019-09-26 16:44:17
---

可以为其增加type字段指定页面的类型:

1
2
type: tags
comments: false

在使用的主题_config.yml文件将页面的链接加到主菜单中,修改menu字段:

1
2
3
4
5
6
7
8
9
menu:
home: / || home
#about: /about/ || user
tags: /tags/ || tags
#categories: /categories/ || th
archives: /archives/ || archive
#schedule: /schedule/ || calendar
#sitemap: /sitemap.xml || sitemap
#commonweal: /404/ || heartbeat

本地服务重启,可以观察到页面状态变化(左侧导航出现标签,点击之后会显示标签的列表)。

3.分类页

对文章进行归类,一个文章可以对应某个或多个分类,可以通过以下命令创建分类页:

1
hexo new page categories

生成一个/source/categories/index.md文件。

在其中增加type字段来指定页面的类型:

1
2
type: categories 
comments: false

然后在使用的主题_config.yml文件中将页面链接加入到主菜单中,修改menu字段:

1
2
3
4
5
6
7
8
9
menu:
home: / || home
#about: /about/ || user
tags: /tags/ || tags
categories: /categories/ || th
archives: /archives/ || archive
#schedule: /schedule/ || calendar
#sitemap: /sitemap.xml || sitemap
#commonweal: /404/ || heartbeat

4.添加搜索页

需要搜索全站的内容,所以一个搜索功能的支持也是很有必要的,要添加搜索的支持,需要先安装一个插件,叫做 hexo-generator-searchdb,命令如下:

1
npm install hexo-generator-searchdb --save

然后在项目的_config.yml中添加搜索设置如下:

1
2
3
4
5
search:
path: search.xml
field: post
format: html
limit: 10000

然后在主题的 _config.yml 里面修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
# Local search
# Dependencies: https://github.com/wzpan/hexo-generator-search
local_search:
enable: true
# If auto, trigger search by changing input.
# If manual, trigger search by pressing enter key or search button.
trigger: auto
# Show top n results per article, show all results by setting to -1
top_n_per_article: 5
# Unescape html strings to the readable one.
unescape: false
# Preload the search data when the page loads.
preload: false

5.404页面添加

若需要添加一个 404 页面,直接在根目录 source 文件夹新建一个 404.md 文件,内容可以仿照如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
---
title: 404 Not Found
date: 2019-11-27 10:41:27
---

<center>
对不起,您所访问的页面不存在或者已删除。
您可以<a href="https://dongshifu.github.io>">点击此处</a>返回首页。
或访问<a href="https://blog.csdn.net/dongshifo">查看更多内容。
</center>

<blockquote class="blockquote-center">
Dongshifu
</blockquote>