C语言复习

嵌入式层次结构

用户空间:
命令 脚本(命令的集合) app
-----------------------------------系统调用-------------------------
内核空间:
内核也是一个程序,只不过是在计算机启动时就运行的程序。
linux内核五大功能:
1.进程管理:----时间片轮转 上下文切换 分时多任务
2.内存管理:----空间的分配和回收
3.文件管理:----将一堆 0 1 转换成方便人类识别的内容
4.网络管理:----网络协议栈
5.设备管理:----通过设备驱动去操作硬件,linux中一切皆文件
----------------------------------------------------------------------------
硬件空间:
led 摄像头 扬声器 键盘

注意: 用户空间想要操作硬件,需要先进到内核空间,进入内核空间的方式 是通过系统调用,(系统调用就是内核给用户提供的访问内核的接口函数),
然后通过内核操作硬件。

为什么linux要分为用户空间和内核空间? 主要是出于安全的角度,段错误之后,程序崩了,但是系统不会崩。

Ubuntu的基本操作 及说明

终端打开方式

1-图标方式
2-ctrl+alt+t 打开新终端
3-ctrl+shift+n 打开同路径新终端
4-ctrl+shift+t 左右分屏方式打开同路径新终端

终端字体调整

放大:ctrl shift +
缩小:ctrl -

终端复制和粘贴

复制:ctrl shift c
粘贴:ctrl shift v

ctrl 空格 切换输入法
shift 切换中英文

linux文件系统结构

在这里插入图片描述
linux下一切皆文件

绝对路径和相对路径:

路径之间用 / 分隔
绝对路径:相对于根目录的路径
相对路径:相对于当前所在的路径

linux中所有和文件相关的命令都是即支持绝对路径,也支持相对路径的。

linux系统常用命令

ls 列出当前路径下所有的文件

ls  文件名		列出指定的单个文件
ls  目录名		列出指定目录下的所有文件
ls  -a		列出当前路径下所有的文件,包括隐藏文件 隐藏文件都是以 . 开头的
ls  -l			(小写的L)列出当前路径下所有的文件 包括文件的详细信息
ls -lh 		列出当前路径下所有的文件 包括文件的详细信息,且文件大小用 K M G 单位显示

cd 目录文件名 进入指定的目录

cd  /					进入根目录
cd  ~				进入当前用户的家目录  也可以直接 cd 回车
cd  .					进入当前路径
cd  ..				进入上一级路径
cd  - 				进入上一次所在的路径

路径之间使用 / 分隔的
cd /home/linux/目录1 绝对路径的用法
cd ./目录1 相对路径的用法
cd …/… 进入上一级路径的上一级路径

pwd 显示当前所在的绝对路径
whoami 查看当前使用的用户
mkdir 目录d1 在当前路径下创建一个目录文件,名为 d1

mkdir  ~/d2	在当前用户的家目录下创建一个目录文件,名为 d2
mkdir  d1  d2  d3  在当前路径下并列的创建三个目录文件 分别名为  d1 d2 d3
mkdir  -p  d1/d2/d3	在当前路径以嵌套的形式创建 d1/d2/d3 

touch f1 新建普通文件 名为 f1(若已经存在文件f1 表示更新文件的时间戳)
rmdir 目录文件名 删除目录文件 (这个命令只能删除空目录)
rm 文件名 删除指定的文件

rm  -r 目录文件名	如果删除的是目录文件 需要加 -r 选项
rm -f  文件名		忽略提示信息 直接删除

cp 复制

cp  file1  file2 		
如果文件file2不存在,表示将文件file1复制一份儿取名为 file2
如果文件file2存在,表示将文件file1复制一份儿取名为 file2,会覆盖原来的 file2

cp  file1  dir1		
将文件file1复制一份,放到目录dir1里面

cp  -r  dir1  dir2	
如果目录dir2不存在,表示将目录dir1复制一份儿取名为 dir2
如果目录dir2存在,表示将目录dir1复制一份放到 dir2 里面

mv

mv  file1  file2 		
如果文件file2不存在,表示将文件file1重命名为 file2
如果文件file2存在,表示将文件file1重命名为 file2,会覆盖原来的 file2

mv  file1  dir1		
将文件file1移动到目录dir1里面

mv  dir1  dir2	
如果目录dir2不存在,表示将目录dir1重命名为 dir2
如果目录dir2存在,表示将目录dir1移动到 dir2 里面

cat 命令

cat  普通文件名:查看文件内容
cat  -n  普通文件名:带行号显示文件内容

clear 清屏 (快捷键ctrl+l(小写的L))
exit 命令

su  用户名   切换用户 需要密码
如果涉及到用户切换,exit表示退回到上一次的用户
如果已经是最开始的用户了  再执行exit表示退出终端

tab 补全键 :能补全命令或者文件名

文件的详细信息

-rw-rw-r-- 1 linux linux 612418 5月 31 2022 aaa.tar.gz

  • 文件的类型 bsp-lcd
    b 块设备文件
    s 套接字文件
    p 管道文件
    – 普通文件
    l 小写的(L)链接文件
    c 字符设备文件
    d 目录文件
    rwxrw-r-- 文件的权限 分为三组 分别表示 所属用户的权限 所属组的权限 其他人的权限
    r 读权限
    w 写权限
    x 执行权限
    – 没有对应的权限
    1 文件的硬链接个数
    linux 文件的所属用户
    linux 文件的所属组
    612418 文件的大小 单位是 字节 Byte
    1Byte = 8bit
    1 KB = 1024 B
    1 MB = 1024 KB
    1 GB = 1024 MB
    1 TB = 1024 GB
    5月 31 2022 文件的时间戳 文件最后一次被修改的时间
    aaa.tar.gz 文件名

命令练习

练习:
1.在用户的家目录下创建 目录文件 dir1 和 普通文件 file1
2.在家目录下给dir1目录嵌套创建 dir1/dir2/dir3/dir4/dir5
3.在家目录下直接一步进入到 dir4 里面
4.在dir4目录中将家目录下的file1 移动到上一级的dir3中
5.在dir4目录下创建一个目录文件 test
6.将test 复制到上一级的dir3中
7.在dir4中直接查看dir3中有哪些文件
8.在dir4中直接回到家目录 删除刚才创建的目录 dir1

cd ~
mkdir dir1 
touch file1
mkdir -p  dir1/dir2/dir3/dir4/dir5
cd dir1/dir2/dir3/dir4
mv ~/file1 ..
mkdir test 
cp -r test ..
ls ..
cd ~
rm -r dir1 

linux软件包管理

常用软件包后缀名

.deb (Ubuntu使用)
.rpm (redhat使用)

软件包命名规则

sl_3.03-17build2_amd64.deb

sl _ 3.03 - 17build2 _ amd64 .deb
软件名 版本号 修订版本号 架构 Ubuntu使用

架构:64位 amd63 32位 i386

软件的安装

离线安装–dpkg
注意:不会检查软件的依赖,如有有依赖需要自己手动安装。

安装软件
sudo  dpkg  -i  软件包名  
sudo  dpkg  -i  sl_3.03-17build2_amd64.deb
查看软件的man手册
man  软件名
查看已经安装的软件的信息
sudo  dpkg  -l  (小写的L)  软件名 
查看软件的安装路径
sudo  dpkg  -L  软件名 
卸载软件
sudo  dpkg  -r  软件名  会保留配置文件
完全卸载
sudo  dpkg  -P  软件名 

注意:如果使用 dpkg 命令提示 已经加锁xxxxx
就先执行下面的命令 sudo rm /var/lib/dpkg/lock*

在线安装-- apt-get
会检查软件的依赖,如有有依赖会一并安装到系统中。
测试主机有没有联网:
ping www.baidu.com
如果出现下面的内容,说明是有网的
在这里插入图片描述
在这里插入图片描述
注意:每个服务器中都会保存所有软件仓库的地址
由于用户所在的区域不同,连接不同的服务器下载软件的速度也不同
用户下载软件的时候,要先确定自己连接哪个服务器。

选择服务器的流程: 点击Ubuntu左下角的显示应用程序–》软件更新器—》提示检查更新先点击停止
—》点击设置–》选择Ubuntu软件—》所有的复选框都勾选—》下载自后面的下拉箭头
—》选择其他站点–》 选择合适的服务器(我们选清华大学的即可 tsinghua)密码是1–》点关闭

打开下面的文件:
sudo vi /etc/apt/sources.list
将里面所有的 https 改成 http
保存退出

更新本地源
(将服务器中保存的软件的地址同步到我们的主机上
成功后会将软件的地址同步到主机的 /var/lib/apt/lists 路径下)
sudo apt-get update

安装软件
sudo apt-get install 软件名
一些linux中的小软件
sudo apt-get install sl //安装小火车应用程序
sudo apt-get install oneko //安装一个小猫
sudo apt-get install bastet //俄罗斯方块 终端字体缩小点就能玩了
sudo apt-get install frozen-bubble //泡泡龙
sudo apt-get install kolourpaint4 //画图板
sudo apt-get install xawtv //打开摄像头的软件
sudo apt-get install cmatrix //代码雨

卸载软件
sudo apt-get remove 软件名
下载源代码
sudo apt-get source 软件名
只下载不安装
sudo apt-get download 软件名
清理安装包
sudo apt-get clean
安装软件的安装包默认会残留在(/var/cache/apt/archives 路径下)

压缩和打包的命令–重要

压缩和解压

操作对象是单个文件。

压缩方式

gzip —> .gz
bzip2 --> .bz2
xz --> .xz

上述三种压缩方式:
压缩率:从上到下是依次递增的
压缩速率:从上到下是依次递减的

gzip 文件名
将文件按照gzip格式压缩
压缩后,源文件就不存在了,会生成 文件名.gz 的压缩后的文件
解压命令 gunzip 文件名.gz

bzip2 文件名 将文件按照bzip2格式压缩
压缩后,源文件就不存在了,会生成 文件名.bz2的压缩后的文件
解压命令 bunzip2 文件名.bz2

xz 文件名 将文件按照xz格式压缩
压缩后,源文件就不存在了,会生成 文件名.xz 的压缩后的文件
解压命令 unxz 文件名.xz

归档和解归档–tar

也叫作(打包和解包)
操作对象是多个文件,一般多用于操作目录文件,
会将目录文件自身及其子目录下所有的文件都归档成一个文件。
归档默认是不压缩的。

归档的命令 tar

tar 命令的参数
-c	归档
-x	解归档
-v	显示详细信息
-f	必须写在选项最后 后面就接文件名了
附加选项:
-z	归档的同时将归档后的文件按gzip格式压缩
-j	归档的同时将归档后的文件按bzip2格式压缩
-J	归档的同时将归档后的文件按xz格式压缩

归档的操作:
tar -cvf dir.tar dir 作用是将dir目录及其子目录下所有的文件归档成一个名叫dir.tar 的文件,归档后的文件名随便起,一般操作习惯上
原目录文件名.tar
c和v的位置可以互换,但是 f 必须放在最后

解归档:
tar -xvf dir.tar

归档的同时进行压缩:
tar -zcvf dir1.tar.gz dir1
tar -jcvf dir2.tar.bz2 dir2
tar -Jcvf dir3.tar.xz dir3

对应的解归档并解压的命令
tar -zxvf dir1.tar.gz
tar -jxvf dir2.tar.bz2
tar -Jxvf dir3.tar.xz

通用的解归档并解压的命令
tar -xvf dir1.tar.gz
tar -xvf dir2.tar.bz2
tar -xvf dir3.tar.xz

查看文件的命令

cat 命令

cat 文件名 显示文件全部内容
cat -n 文件名 显示文件全部内容

head 命令

head 文件名 //显示文件开头的内容 默认显示前10行
head -n 20 文件名 //显示文件开头的内容 显示前20行
head -30 文件名 //显示文件开头的内容 显示前30行

tail 命令

tail 文件名 //显示文件结尾的内容 默认显示后10行
tail -n 20 文件名 //显示文件结尾的内容 显示后20行
tail -30 文件名 //显示文件结尾的内容 显示后30行

tail -f 文件名 //动态的显示文件新增的内容,常用查看日志文件

| 管道符:作用是将前面命令的结果作为后面命令的参数

head -105 file1 |tail -n 6 显示文件第100到105行的内容

more 命令 --不常用

more 文件名 按百分比显示文件内容 按回车键 向下滚动 按 q 退出

less 命令 --不常用

less 文件名 滚动显示文件内容 按方向键的上下控制滚动 按q退出

查看二进制文件 --不常用

od -c 二进制文件名

统计文件内容的命令wc

wc 文件名 统计文件内容
执行上面的命令 会得到下面的信息

5  		   8 		  45 	file2
行数	单词数	  字符数	    文件名

wc -l(小写的L) 文件名 统计行数 line
wc -w 文件名 统计单词数 word
wc -c 文件名 统计字符数 char
wc 可以配合通配符来统计多个文件内容

在这里插入图片描述

检索文件内容的命令grep

grep “string” file1 //在file中检索所有包含 string 的行

grep命令常用的选项:
-n	带行号显示
-i	忽略大小写
-w	精确查找,string有前缀和后缀都不行
-v	反选,不包含  string的行
-R	递归查找,可以检索子目录下文件的内容

常用的用法:
grep -nR “string” * 在当前路径及子目录下的所有文件中检索包含string的行

grep语句中关于"string"也有说法:
grep “^string” file1 在file1中查找以string开头的行
grep “string " f i l e 1 在 f i l e 1 中查找以 s t r i n g 结尾的行 g r e p " s t r i n g " file1 在file1中查找以string结尾的行 grep "^string "file1file1中查找以string结尾的行grep"string” file1 在file1中查找string独自成行的行

查找文件的命令find

find
find 路径 -name 文件名 //在指定的路径及子目录下查找是否存在指定的文件

find ./ -name 01test.c 在当前目录及其子目录下查找所有名字叫 01test.c 的文件
find /dev -name mouse0 在/dev目录及其子目录下查找所有名字叫 mouse0 的文件

注意:find命令是不能配合通配符使用的,会出现下面的问题
find ./ -name .c 这个命令的作用不是在当前目录及子目录下找所有.c 结尾的文件
这个命令会把当前路径下所有的.c文件替换到
.c的位置再执行
如果当前路径有多个.c文件执行是或报错的
在这里插入图片描述
查找当前目录及子目录下找所有.c 结尾的文件,可以使用下面的用法
find ./ -type f 查找当前目录及子目录下找所有的普通文件 f 普通
find ./ -type f |grep “.c” 查找当前目录及子目录下找所有的.c 文件
find ./ -type f |grep “.c” |wc -l .c文件的个数
拓展:
xargs 将前面命令的结果逐个的作为后面的命令的参数
find ./ -type f |grep “.c” |xargs wc -l 所有.c文件的行数

文件内容截取的命令

cut
-d 分隔符
-f 指定要选择的域

注意:cut命令是以行为单位进行操作的
在这里插入图片描述
在linux系统中存在一个 /etc/passwd 文件,该文件中记录的就是系统中已有用户的信息。

如:
linux❌1001:1001:,:/home/yangfs:/bin/bash
用户名:有密码:用户id:组id:描述字段:用户的家目录:默认的命令行解析器
练习:
输出 /etc/passwd 文件中linux用户的行号?
cat /etc/passwd |cut -d : -f 1 |grep -n “^linux$” |cut -d : -f 1

通配符

7.1 *
通配任意长度的任意字符
原有文件:
ABCD.c A.c B.c C.c D.c hqyj2.txt jhkajsdhfkjasdhf.txt
a.c b.c c.c d.c hqyj1.txt hqyj3.txt kajsdhfjkashdf.c
ls *:
ABCD.c A.c B.c C.c D.c hqyj2.txt jhkajsdhfkjasdhf.txt
a.c b.c c.c d.c hqyj1.txt hqyj3.txt kajsdhfjkashdf.c
ls .txt:
hqyj1.txt hqyj2.txt hqyj3.txt jhkajsdhfkjasdhf.txt
ls A
:
ABCD.c A.c
7.2 ?
通配一个长度的任意字符
原有文件:
ABCD.c A.c B.c C.c D.c hqyj2.txt jhkajsdhfkjasdhf.txt
a.c b.c c.c d.c hqyj1.txt hqyj3.txt kajsdhfjkashdf.c
ls ?.c:
a.c A.c b.c B.c c.c C.c d.c D.c
ls ???.c:
ABCD.c
ls hqyj?.txt:
hqyj1.txt hqyj2.txt hqyj3.txt

7.3 [字符1字符2字符n].c
[abc] 通配abc中的任意一个字符
原有文件:
ABCD.c A.c B.c C.c D.c hqyj2.txt jhkajsdhfkjasdhf.txt
a.c b.c c.c d.c hqyj1.txt hqyj3.txt kajsdhfjkashdf.c
ls [abcqwer].c:
a.c b.c c.c

7.4 [起始字符-结束字符]
[0-9] 通配0~9中的任意一个字符
原有文件:
ABCD.c A.c B.c C.c D.c hqyj2.txt jhkajsdhfkjasdhf.txt
a.c b.c c.c d.c hqyj1.txt hqyj3.txt hqyj4.txt kajsdhfjkashdf.c
ls hqyj[1-3].txt:
hqyj1.txt hqyj2.txt hqyj3.txt

如果是字母,涉及到本地语序
本地语序默认 aAbBcC…zZ
清空本地语序: export LC_ALL=C
清空本地语序之后 abc…zA…Z:
恢复本地语序:unset LC_ALL
ls [a-c].c:
a.c b.c c.c
ls [A-C].c:
A.c B.c C.c
7.5 [^字符1字符2字符n]
[^abc] 反选,不包括abc中的任意一个字符
原有文件:
ABCD.c A.c B.c C.c D.c hqyj2.txt jhkajsdhfkjasdhf.txt
a.c b.c c.c d.c hqyj1.txt hqyj3.txt hqyj4.txt kajsdhfjkashdf.c
ls [^abc].c:
A.c B.c C.c d.c D.c

文件权限管理–重要

文件的权限

ls -l 可以查看文件的权限
-rwxrw-r-- 1 yangfs yangfs 0 3月 17 14:11 ABCD.c
rwx 所属用户的权限
rw- 所属组的权限
r-- 其他人的权限
r 读权限
w 写权限
x 执行权限

  • 没有对应的权限

修改文件的权限 chmod

注意:只有文件的所属用户和root用户才能修改文件的权限

使用±的方式修改

u 所属用户 user
g 所属组 group
o 其他人 other
a 所有的 all
例如:
chmod u-r 文件名 将文件所属用户的读权限减掉
chmod g+w 文件名 将文件所属组的写权限加上
chmod o-x 文件名 将文件对其他人的执行权限减掉
chmod a-r 文件名 将文件的所有读权限都减掉

使用八进制数来修改

linux系统使用3个八进制数来表示权限
0777—>rwxrwxrwx
0664 —>rw-rw-r–
0541 —>r-xr----x

chmod 664 file1 将file1的权限修改为 rw-rw-r–
chmod 440 file2 将file1的权限修改为 r–r-----

拓展:
touch创建的文件默认权限是:0666 & ~umask
umask默认为0002
所以默认权限是 0664

修改文件所属用户

注意:只有root用户才能修改文件所属用户
sudo chown 新的用户名 文件名
例如:
sudo chown user1 file1
将文件file1的所属用户改为 user1

修改文件所属组

注意:只有root用户才能修改文件所属组
sudo chgrp 新的组名 文件名
例如:
sudo chgrp user1 file1
将文件file1的所属组改为 user1

用户管理相关的命令----了解

linux系统是多用户的操作系统。

添加用户

sudo adduser 用户名
在这里插入图片描述
用户创建成功之后会在
/etc/passwd 文件中添加新的用户的信息
/etc/group 文件中添加新的组的信息

新添加的用户默认是没有 sudo 权限的
在这里插入图片描述
需要先将 新用户的信息添加到 /etc/sudoers 文件中:
1.su root
2.chmod u+w /etc/sudoers 该文件默认是没有写权限的需要 先添加写权限
3.vi /etc/sudoers
4.将root那一行 复制一份儿 将roo改成新的用户名 保存退出即可
在这里插入图片描述
5.chmod u-w /etc/sudoers 修改之后即可减掉写权限 防止误修改
6.exit

切换用户

su 用户名 需要用户的密码

退出当前用户

exit

查看当前使用的用户

whoami

修改用户的密码

注意:只有用户自己和管理员才能修改用户的密码
sudo passwd 用户名

修改用户信息–usermod

sudo usermod -c “string” 用户名 将用户的描述字段信息改成 string
sudo usermod -d /home/xxx 用户名 修改用户的家目录为 /home/xxx
注意:不能草率的修改用户的家目录,
因为家目录下有一些和用户信息相关联的配置文件
需要提前先拷贝到新的家目录,否则可能会导致用户登录不了
sudo usermod -l(小写的L) hello user1 将用户user1的用户名改成 hello
sudo usermod -g hello user1 将用户user1的所属组改成 hello

查看用户的简要信息

id 用户名

删除用户

sudo deluser 用户名

删除组

sudo delgroup 组名

链接文件

软链接

也称为符号链接
ls -l —> 类型为 l (小写的L) 的链接文件就是软链接文件
软链接相当于windows中的快捷方式
创建软链接:
ln -s 被链接的文件 要生成的链接文件

创建软链接时 最好使用 绝对路径
如果使用相对路径 那么当链接文件和被链接的文件 的相对位置发生变化时
链接就失效了
当源文件被删除了或者移动了 链接也会失效

硬链接

相当于给文件起了一个别名。
创建软链接:
ln 被链接的文件 要生成的链接文件
硬链接只是给文件起了一个别名,因为多个硬链接文件的 inode号都是一样的
linux系统是根据inode号来识别文件的
使用 stat 文件名 可以查看文件的inode号
对于硬链接文件,删除的只是一个名字,只有把所有名字都删除了,文件才真正的被删除了
修改一个文件,其他的硬链接文件也会随之发生变化

关机和重启命令

关机命令

sudo shutdown -h now //立即关机
sudo shutdown -h +30 //30分钟后关机
sudo shutdown -h 10:20 //10:20关机

重启的命令

sudo shutdown -r now //立即重启
sudo shutdown -r +30 //30分钟后重启
sudo shutdown -r 10:20 //10:20重启
sudo reboot //立即重启

磁盘相关的命令

查看系统磁盘信息

sudo fdisk -l (小写的L)
在这里插入图片描述
其中,sda1表示的含义,a表示第一个盘符,如果有其他盘符或者外接盘符
依次次增 sdb sdc sdd
1 表示该硬盘的分区编号是1

查看磁盘的使用情况

df -h
在这里插入图片描述

挂载磁盘

硬盘接入系统后,默认是不提供挂载的,需要我们自己手动挂载
sudo mount 设备名(/dev/sdb1) 挂载的目录
在这里插入图片描述
解除挂载
sudo umount 设备名(/dev/sdb1)

对磁盘的操作

sudo fdisk 设备名(/dev/sdb1)
m 获取帮助
d 删除分区
F 列出未分区的空闲区
l 列出已知分区类型
n 添加新分区
p 打印分区表
t 更改分区类型
v 检查分区表
i 打印某个分区的相关信息
w 将分区表写入磁盘并退出
q 退出而不保存更改

指定磁盘的格式

FAT32格式不允许存储单个的超过4G的文件
NTFS格式的允许
sudo mkfs.ntfs /dev/sdb1
sudo mkfs.fat /dev/sdb1

环境变量

概念

环境变量就是用来保存系统的启动相关的信息及系统运行相关信息的变量。
env命令可以显示系统中已有的环境变量信息。
使用 echo $环境变量名 可以显示环境变量的值

常见环境变量

OLDPWD //保存上一次所在路径
USER //保存当前使用的用户名
PWD //保存当前所在路径
HOME //保存用户的家目录
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
PATH //是用来保存可执行文件的路径的
//当执行命令的时候,系统会在PATH保存的路径中逐个的
//查找有没有该可执行文件,有就执行,没有就报错

//如果想让我们自己的a.out可以任意在任意路径下直接执行
//方式1:将我们的a.out 放到PATH中记录的某个路径中,需要sudo权限
//方式2:可以将我们的a.out的路径加入到PATH里面

修改环境变量的方式

方式1:只对当前终端生效
直接在终端执行指令即可
export PATH=$PATH:新的路径 //注意:并不是所有的环境变量都需要追加赋值
生效方式:立即生效

方式2:对当前用户生效
修改用户家目录下的 .bashrc 文件
将声明环境变量的语句追加到文件最后,
由于该文件每次打开新的终端时都会被执行
所以相当于对当前用户生效(每个用户都有自己独立的 .bashrc 文件)
export PATH=$PATH:新的路径
生效方式:重新打开终端生效 或者 source ~/.bashrc 立即生效

方式3:对所有用户生效 ----不常用
修改 /etc/profile 文件
将声明环境变量的语句追加到文件最后,
由于该文件每次启动系统时都会被执行,所以相当于对所有用户生效
export PATH=$PATH:新的路径
生效方式:重启系统生效 或者 sudo source /etc/profile 立即生效

vi编辑器

打开vi编辑器 : vi 文件名
命令行模式:打开vi编辑器,默认的就是在命令行模式。一般是用来复制粘贴代码的。
其他模式回到命令行模式 按 esc 键

yy		复制光标所在行
nyy		n是一个数字,从光标所在行开始,复制n行
p		在光标所在行下面开始粘贴
P		在光标所在行上面开始粘贴
dd		剪切光标所在行
ndd		n是一个数字,从光标所在行开始,剪切n行
gg		将光标定位到第一行
G		将光标定位到最后一行
ngg		n是一个数字,将光标定位到第n行
u		撤销
ctrl r		重做(反撤销)
/word	在全文中查找单词 word 按回车后  n 查找下一处  N  查找上一处

插入行模式:
在命令行模式下,输入下面的按键,进入插入模式:

i	在光标所在位置前面开始插入
a	在光标所在位置后面开始插入
I	(大写的i)在光标所在行的行首开始插入
A	在光标所在行的行尾开始插入
o	在光标所在行的下面,新起一行开始插入
O	在光标所在行的上面,新起一行开始插入
底行模式:在命令行模式下按 冒号 进入底行模式,底行模式一般是用来保存退出的。
:w	保存
:q	退出
:wq	保存并退出  :x 也可以
:q!	不保存强制退出
:noh		取消查找后的单词高亮
:vsp  文件名  左右分屏的方式打开多个文件同时编辑  ctrl ww 切换编辑窗口
:wqa	保存并退出所有编辑的文件
:set number	显示行号   :set nu  也可以
:set nonumber	取消显示行号   :set nonu  也可以
:%s/aaa/bbb/g	将全文的aaa都替换成bbb
:m,ns/aaa/bbb/g	将第m行到第n行的所有aaa都替换成bbb
:%s/aaa/bbb/gc	将全文的aaa都替换成bbb 每次会询问 y 替换  n 不替换

gcc编译器

编程语言分为 编译型语言 和 解释型语言。

编译型语言:
在执行之前必须要专门有一个编译的过程,编译就是将我们人类能是别的高级语言
翻译成计算机能识别的低级语言的过程。编译器就是专门做这个工作的软件。
优点:由于已经提前专门翻译过了,执行的过程中无需重新翻译,执行效率相对较高
缺点:依赖于编译器,跨平台性相对较差
例如:C C++

解释型语言:
执行之前无需单独编译,而是在执行的过程中,由解释器逐行的翻译给计算机看的。
也叫脚本语言。
优点:跨平台性相对较好
缺点:每次执行都需要重新翻译,执行效率相对较低
例如:shell python

linux系统中 C语言的编译器是 gcc 编译器

程序说明及编译方式

程序说明

#include <stdio.h>
//#开头的行 称为预处理行 
//include  是包含头文件的关键字
//<>    里面是头文件的名字
// stdio.h  是标准输入输出的头文件  我们使用 printf就在这个头文件里
//   ()圆括号  []方括号  {}花括号  <>尖括号
// int 是函数的返回值类型 ----先不用管
//  main 主函数  是程序的入口 每个程序必须有 且只能有1个
//  () 里面是main函数的参数----先不用管 
//    main函数的()里面可以空着不写,但是()必须写
//   {}里面是函数体 也就是我们要执行的代码
int main(int argc, const char *argv[]){
    //printf是系统给我们提供的输出的函数
    //功能是将后面 "" 里面的内容打印到终端
    //  \n  是换行符  也就是回车的意思
    printf("hello world\n");  //C语言的每条指令结尾要有 分号 ;
    
    //函数的返回值  ---先不用管
    return 0;
}

// 单行注释

/*
    多行
    注释
*/

#if 0
    多行
    注释
#endif

编译方式

默认可执行文件名
gcc xxx.c xxx.c是你自己的 .c 文件名
这种编译方式默认会在当前路径下生成一个名叫 a.out 的可执行文件
使用 ./a.out 就可以执行了

自定义可执行文件名
gcc xxx.c -o diy_name xxx.c是你自己的 .c 文件名 diy_name是你定义的可执行文件名
这种编译方式可以生成自定义名字的可执行文件
./自定义的名字 就可以执行了

按照编译流程分步编译
预处理-->编译-->汇编-->链接 
预处理:展开头文件  替换宏定义  删除注释
gcc  -E  xxx.c  -o  xxx.i
编译:词法分析、语法分析 说白了就是查错的
如果无误  会生成对应的汇编文件
gcc  -S  xxx.i   -o  xxx.s
汇编:将汇编文件生成对应的二进制文件(目标文件)
gcc  -c  xxx.s  -o  xxx.o
链接:多个目标文件链接 链接库文件 生成对应的可执行文件
gcc  xxx.o  -o   a.out 

计算机中数据的处理

数值型数据的表示方式

十进制

方便人类识别和处理的
特点:逢10进1 每一位上的数字范围 [0-9]
前导符:没有前导符
例如:100 1234

二进制

方便计算机识别和处理的
特点:逢2进1 每一位上的数字只能是 0 或者 1
前导符:0b
例如:0b1010001 0b1011

二进制转十进制:
0b11101 --> 从右向左 12^0 + 02^1 + 12^2 + 12^3 + 1*2^4
== 1 + 0 + 4 + 8 + 16
== 29
注意:其他任何进制转10进制都可以使用这种方式,
只不过将底数的2 换成对应进制的数字即可

如果熟练 可以使用 8421转换 ,8421指的是每一位上1的权重
0001 -->1
0010 -->2
0100 -->4
1000 -->8

十进制转二进制:
使用辗转相除法(除2取余法)
使用十进制的数据除以2,保留商和余数,然后用商继续除以2,
再保留商和余数,依次类推,直到商为0时结束。
将得到的余数反向取出,就是对应的二进制数了。

以十进制的29转二进制位例:
在这里插入图片描述

八进制

特点:逢8进1 每一位上的数字范围 [0, 7]
前导符:0
例如:0345 0556

八进制转二进制:
方式1:八转十 然后 十转二
方式2:1位八进制对应3位二进制
0357 —> 0b011101111
二进制转八进制:
从右向左 每3位二进制对应1位八进制,高位不够 补0
0b011010101 —> 0325

十六进制

特点:逢16进1 每一位上的数字范围 [0, 9] a:10 b:11 c:12 d:13 e:14 f:15
前导符:0x
例如:0xAB12 0x78EF

十六进制转二进制:
方式1:十六转十 然后 十转二
方式2:1位十六进制对应4位二进制
0x78EF —> 0b0111100011101111
二进制转十六进制:
从右向左 每4位二进制对应1位十六进制,高位不够 补0
0b0001110101010011 —> 0x1D53

注意:不管几进制的数据,在计算机中都会转换成二进制处理。

非数值型数据的表示形式

ASCII码

计算机中只能处理二进制的数值型数据,但是实际编程的过程中,
也经常会遇到跟多非数值型数据,如人名、企业名、网址 等
“www.baidu.com” “zhangsan” “4399” ‘M’
(非数值型数据都是用 单引号或者双引号引起来的)
计算机也需要处理这些非数值型数据,科学家们就发明了一种叫做 ascii 码的东西
使用 man ascii 就可以查看ascii码表 按 q 退出

ascii其实就是规定了 字符 和 整数对应的关系
每个字符都有一个对应的整数,叫做该字符的ascii码

实际使用字符的时候,本质上使用的都是该字符对应的ascii码

常见字符对应的ascii码
'A'  ~  'Z'   :  65~90
'a'  ~  'z'    :  97~122
'0'  ~  '9'    : 48~57
'\0'    :    0
'\n'    :    10 

转义字符:
所有字符都可以使用 ‘+数字(八进制)’ 来表示
除此之外,C语言中还定义一些 ‘+字母’ 来表示那些无法显示的字符
如 ‘\n’ ‘\0’ ‘\a’ …
这些就叫做转义字符,因为这些符号已经不是字母本身的含义了。

词法符号

关键字

所谓的关键字就是编译器中已经规定好的一些有特殊含义的单词,直接使用即可。
C语言是严格区分大小写的,关键字都是小写的。

char short int long float double signed unsigned struct union enum void
const static extern register volatile auto
typedef
sizeof
if else switch case break default do while goto for continue return 

标识符

所谓的标识符就是我们自己起的名字,变量名、函数名、结构体名、共用体名。。
命名时要符合标识符的命名规范:
1.只能由数字、字母、下划线组成
2.不能以数字开头
3.不能和关键字冲突
杨老师给大家加一条:命名是尽量做到“望文知意” 。

数据类型

数据类型的作用相当于模子,决定了由他定义的变量需要操作系统分配多大的内存空间。

C语言的本质是操作内存
内存和硬盘的区别:
内存:读写速度快 数据掉电丢失 价格贵
硬盘:读写速度慢 数据掉电不丢失 价格便宜
内存分配的最小单位:字节 Byte

数据类型的分类

基本类型:整型、浮点型、枚举类型、
构造类型:数组、结构体、共用体、
指针类型、
空类型 void、

整数类型

整数类型又可以细分为 char short int long 和 long long 类型
其中每种类型又分为 有符号的(signed) 和 无符号的(unsigned)
不写有无符号时,默认都是有符号的
有符号数 最高位为符号位 符号位为1(负数) 0(正数)

char 字符类型

占用内存空间的大小:1字节 8 bit
能存储的数据范围:
无符号:[0, 2^8-1]
有符号:[-2^7 , 2^7-1]

为了解决正负0的问题,计算机中存储的是数据的补码形式
规定了 10000000 为 -128 的补码
负数比正数多一个 下面的类型与之同理

short 短整型

占用内存空间的大小:2字节 16 bit
能存储的数据范围:
无符号:[0, 2^16-1]
有符号:[-2^15 , 2^15-1]

int 整型

占用内存空间的大小:4字节 32 bit
能存储的数据范围:
无符号:[0, 2^32-1]
有符号:[-2^31 , 2^31-1]

long 长整型

在32位系统中 和 int 一样
在64位系统中 和 long long 一样

long long 长长整型

占用内存空间的大小:8字节 64 bit
能存储的数据范围:
无符号:[0, 2^64-1]
有符号:[-2^63 , 2^63-1]

浮点型(实型)

就是小数的意思。
float 4字节 单精度浮点型
double 8字节 双进度浮点型
浮点型数据的存储涉及到小数的二进制,比较复杂。
要注意:浮点型存储 存储的是拼凑的近似值

空类型

void 空类型 一般不单独使用 都是配合着指针使用的

原码反码补码转换问题

数据在存储到计算的过程中,涉及到原码、反码、补码转换的问题
原码给人类看的
反码
用来转换原码和补码的
补码==给计算机看的

无符号数:原码、反码、补码是一样的
有符号的正数:原码、反码、补码是一样的
有符号的负数:
反码 == 原码 符号位不变 其他位按位取反 (0变1 1变0)
补码 == 反码+1

规则:存储时看数据(正负),取出时看类型(有无符号)

常量

整个程序运行的过程中,值不允许发生变化的量。

常量的分类

           前导符		输出时占位符		例如
整型常量
十进制		无			%d  %u			1314
二进制		0b				无			0b10101
八进制		0				%o			0765
十六进制		0x				%x			0x67EF
浮点型常量
float		单精度浮点型			%f
double	双精度浮点型			%lf (小写的L)		3.14
指数常量			就是科学计数法	%e			4.567e2  -->  4.567*10^2
字符常量							%c			'M'
字符串常量						%s			"hello"
标识常量			----宏定义    #define

整型常量

long 类型输出要用 %ld
long long 类型输出要用 %lld
unsigned 类型输出要用 %u %lu %llu

浮点型常量

浮点型常量一般都是有小数部分的。
有两种表示形式:
一般形式: 3.14 5.28
指数形式: [+/-]M.Ne[+/-]T —> 就是科学计数法
-3.4567e3 —> -3.4567*10^3 —> -3456.7
2.345e-2 —> 2.345 * 10 ^ -2 ----> 0.02345

使用浮点型常量给变量赋值
默认显示6位小数 超过的部分 四舍五入
float 也可以使用 %.nf 来表示显示 n位小数  n是一个具体的数据
double 也可以使用 %.nlf 来表示显示 n位小数  n是一个具体的数据

小数也可以按指数行数输出
	float c = 1234.56;
	printf("c = %e\n", c);// 1.23456e3

	float d = 2.3456e2;
	printf("d = %f\n", d);// 234.559998

字符常量

所谓的字符常量就是前面说的 一个 非数值型数据
字符常量必须要用 单引号 ‘’ 引起来,且单引号中只能引一个字符
‘M’ ‘h’ ‘8’ ‘\n’
字符就是整型,整型就是字符,具体看我们怎么用。

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//存储字符常量 用 char 类型的变量即可
	char v1 = 'A';
	printf("v1 = [%d]  [%c]\n", v1, v1); // 65 A

	char v2 = 66;
	printf("v2 = [%d]  [%c]\n", v2, v2); // 66 B

	char v3 = '8';
	printf("v3 = [%d]  [%c]\n", v3, v3); // 56 8

	char v4 = 10;
	//char v4 = '\n';
	printf("v4 = [%d]  [%c]\n", v4, v4); // 10 回车

	//思考
	//1. 如何将字符 '8' 转换成 整数 8
	char v5 = '8';
	//v5 = v5 - 48;
	v5 = v5 - '0'; //字符参与运算时  本质就是其对应的ascii参与运算
	printf("v5 = %d\n", v5);//8

	//2.如何将 大写字母 转换成 小写
	char v6 = 'M';
	//v6 = v6 + 32;
	v6 = v6 + ('a'-'A');//和上面的写法本质是一样的
	printf("v6 = %c\n", v6);// m

	return 0;
}

字符串常量

字符串是由连续的一个或多个字符组成的。
字符串常量必须用 双引号 “” 引起来
“www.hqyj.com” “zhangsan” “m”

注意:每个字符串结尾都有一个隐藏的字符 ‘\0’ 用来标识字符串结束的
所以, “hello” 占用的内存空间是 6个字节

在代码中:
a 变量a
‘a’ 字符a
“a” 字符串a

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//可以使用数组来保存字符串常量 ----后面详细讲
	char str[32] = "hello world";
	printf("str = %s\n", str);//hello world

	//也可以直接使用指针指向字符串常量 ----后面详细讲
	char *p = "beijing";
	printf("p = %s\n", p);//beijing

	//注意:C语言中对字符串的处理  遇到 '\0' 就结束了
	char str2[32] = "hello\0world";
	printf("str2 = %s\n", str2);//hello

	char str3[32] = "hello0world";
	printf("str3 = %s\n", str3);//hello0world    '0' 和 '\0' 不一样

	return 0;
}

标识常量–宏定义

宏定义就是给表达式起一个别名,以后想使用这个表达式的时候,使用别名即可,
当表达式需要改变的时候,只需要修改定义处即可,就无须修改整个代码了。

格式:
#define 宏名 宏值
注意:宏定义的名字是一个标识符,要符合标识符命名规范,且一般情况下,宏名都大写。

注意事项:
1.宏定义是在预处理阶段完成替换的;
2.宏定义只是一个简单的替换,无脑替换;

变量

在整个程序运行的过程中,值允许发生变化的量。

定义变量的格式

存储类型 数据类型 变量名;

存储类型:const statict extern register volatile auto ----C高级课程讲
局部变量不写存储类型 默认的就是 auto
数据类型:决定了由他定义的变量需要操作系统分配多大的内存空间
变量名:是一个标识符 要符合标识符的命名规范

定义变量的作用

相当于告诉操作系统,给我的变量分配内存空间,准备存储数据了。

变量的初始化和赋值

 初始化就是在定义一个变量的同时 赋一个初值
 变量是可以被重新赋值的
 常量不允许放在等号左边
 如果定义变量的时候内有初始化 里面存的就是随机值
 同一个作用域(同一个{ })中 不允许定义重名的变量
 变量也可以参与运算
 定义变量时 如果不知道用谁初始化 可以先用 0 来初始化(为了防止随机值对我们程序的影响)

强制类型转化

强制类型转换本身是不安全的,使用的时候要谨慎。
小的类型转大的类型 一般没问题
大的类型转小的类型 就可能会出现数据的截断和丢失。

所谓强制类型转换,就是指,在某次运算中,通过某种方式,
将变量的值的类型,转换成其他类型来参与本次运算。简称:强转。
不会影响变量自身的类型,变量自身的类型只和定义变量时的类型有关。

显式强转

格式:
(新的类型)变量;

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int m = 5;
	int n = 2;
	float ret = m / n; //两个整型 做运算 得到的结果 也是整型
					 //  m / n  结果 就已经是 2 了 会舍弃小数位
					 //  即使使用 float 来接 也是把 2 赋值给 float变量
	printf("ret = %f\n", ret);//2.000000

	//需要将m和n的类型强转成 float 或者 double 来参与本次运算
	ret = (float)m / (float)n;
	printf("ret = %f\n", ret);//2.500000

	//m和n自身的类型不会受影响
	printf("%d  %d\n", m, n);

	return 0;
}

隐式强转

是有编译器根据上下文自动推导而来的,
如果编译器认为本次转换是安全的,则直接通过
如果认为是不安全的,则可能会报一个警告,具体报不报也取决与编译器的严谨程度。

#include <stdio.h>

int main(int argc, const char *argv[])
{
	float m = 3.567;
	int n = m; //这种操作相当于对浮点型的取整操作 小数位舍弃
	printf("n = %d\n", n); //3

	return 0;
}

运算符

概念

运算符就是一个符号,使用来连接多个表达式从而进行运算的。
所谓的表达式,就是有运算符、运算量、标点符号等组成的一个有效序列
是用来表示运算过程的。

运算符的分类

算数运算符:+ - * / % (模除 取余的意思 10%3 -->1)
自增自减运算符: ++ –
关系运算符:> < >= <= != ==
逻辑运算符:&& || !
位运算符: & | ^ ~ << >>
赋值运算符: = += -= *= 等
条件运算符: ?: (C语言中唯一一个三目运算符)
sizeof运算符:计算变量或者类型的大小的
逗号运算符:----了解即可 不常用

自增自减

++ – 单目运算符

int a = 10;
a++; <> a = a+1;
++a; <
> a = a+1;
a–; <> a = a-1;
–a; <
> a = a-1;

注意:以++运算符为例,不管是 a++ 还是 ++a, a的值都会自增1,
但是表达式的结果是不一样的
–操作和++操作同理
a = 10;
b = ++a;
//上述两步之后 a = 11 b = 11
a = 10;
b = a++;
//上述两步之后 a = 11 b = 10
在这里插入图片描述

关系运算符

< >= <= != ==
关系运算符就是用来比较大小关系的
关系运算符表达式的结果是一个 bool 类型 0表示假 非0表示真
多用于控制语句中
注意:
一定要区分 = 和 ==
== 关系运算符
= 赋值运算符

逻辑运算符

&& || !
逻辑运算符是用来连接由多个关系运算符组成的表达式的,
逻辑运算符表达式的结果也是一个bool类型。

&& 逻辑与 表示并且的意思
逻辑与连接的多个表达式,如果都为真,则整个表达式的结果才为真
有一个表达式为假,整个表达式的结果就为假
|| 逻辑或 表示或者的意思
逻辑或连接的多个表达式,有一个为真,整个表达式的结果就为真
如果所有表示都为假,整个表达式的结果才为假
! 逻辑非 表示逻辑取反的意思 真变假 假变真

注意:

逻辑与&& 的用法:
C语言中不允许出现下面的写法:
(10<x<20)
需要该写成  (10<x && x<20)
逻辑运算符的短路原则:
逻辑与连接的多个表达式,如果遇到某个表达式为假了,后面的就都不执行了
逻辑或连接的多个表达式,如果遇到某个表达式为真了,后面的就都不执行了
(与 一假即假   ,  或  一真即真)

位运算符

位指的是bit位,位运算都是针对二进制而言的,
不管几进制的数据,只要是做位运算,都会转换成二进制来参与运算
一般情况下,做位运算时使用的都是无符号的数据,
如果是有符号的,可能会涉及原码、反码、补码转换的问题。

一般多用于硬件设备的控制和标志位的控制。

&	按位与,按位运算,全1为1,有0为0
|	按位或,按位运算,有1为1,全0为0
^	按位异或,按位运算,不同为1,相同为0
~	按位取反,按位运算,0变1,1变0
<<	按位左移,整个数据向左移动,舍弃高位,低位补0
>>	按位右移,整个数据向右移动,舍弃低位,高位补0 
小技巧:
1或任何数 结果都是1
1与任何数 结果还是任何数
0与任何数 结果都是0
0或任何数 结果都是任何数 
1异或任何数 结果相当于任何数的取反
0异或任何数 结果还是任何数 

位运算控制硬件设备的例子:
已知条件:
8个led灯 采用共阴极的接法
给引脚高电平灯亮 给引脚低电平灯灭
灯的编号:
7 6 5 4 3 2 1 0
在这里插入图片描述

#include <stdio.h>
#include <unistd.h>

int main(int argc, const char *argv[])
{
	unsigned char led = 0;
	//点亮 1 3 5 7 号灯
	//led = 0xaa;//这种方法虽然能将 1357点亮 但是顺便就把0246熄灭了
	led = led | 0xaa;//这种写法 就不会影响其他位了
	printf("led = %#x\n", led);//0xaa

	//再点亮4号灯
	//led = led | 0x10;
	led = led | (1<<4);//上面的写法也可以 这种写法就不用算数了
	printf("led = %#x\n", led);//0xba

	//再将5号灯熄灭
	//led = led & 0xdf;
	led = led & ~(1<<5);//上面的写法也可以 这种写法就不用算数了
	printf("led = %#x\n", led);//0x9a
	
	led = 0;

	//思考:如何能让3号灯亮1秒 灭1秒
#if 0
	while(1){
		led = led | (1<<3);
		printf("led = %#x\n", led);//0x08
		sleep(1);//让进程休眠1秒
		led = led & ~(1<<3);
		printf("led = %#x\n", led);//0x00
		sleep(1);
	}
#endif
#if 1
	//这种更简单一些
	while(1){
		led = led ^ (1<<3);
		printf("led = %#x\n", led);//0x8  0 
		sleep(1);
	}
#endif
    //死循环后 按 ctrl c 停止
	return 0;
}

条件运算符

?:
是C语言中唯一一个三目运算符

格式:
表达式1?表达式2:表达式3;
执行逻辑:
先执行表达式1,如果表达式1为真,则执行表达式2,否则执行表达式3.
和简单的 if..else 语句基本一样

sizeof运算符

sizeof是用来计算变量或者类型占用的内存空间的大小的,单位是 字节

格式:
sizeof(变量名或者类型名);
注意:
sizeof的用法和函数调用特别像,但是要注意,sizeof是C语言的关键字,不是函数调用。

32位系统中 sizeof计算的结果是int类型
64位系统中 sizeof计算的结果是long类型
(64位系统将程序按32位编译 需要加编译选项  -m32 )

运算符优先级问题

如果自己写代码时,分不清楚运算符的优先级了,
可以通过加()的方式解决,因为()的优先级是最高的。
如: ((a+b)-(c*6))

如果看别人的代码,复杂的表达式分不清优先级时,可以参考下面的顺口溜:
单算移关与,异或逻条赋。

常用的输入输出函数

putchar/getchar

#include <stdio.h>
int putchar(int c);
功能:向终端输出一个字符
参数:就是我们要输出的字符
#include <stdio.h>
int getchar(void);
功能:在终端获取一个字符
返回值:就是获取到的字符

puts/gets

gets 可以获取带有空格的字符串
gets编译时会报一个警告 这个警告是友好的 可以忽略

#include <stdio.h>
int puts(const char *s);
功能:向终端输出一个字符串  自带换行符
参数:就是我们要输出的字符串的首地址
#include <stdio.h>
char *gets(char *s);
功能:在终端获取一个字符串
参数:就是用来保存字符串的缓冲区的首地址
注意:要保证缓冲区足够大,否则会出现越界访问--很严重的问题

printf/scanf

#include <stdio.h>
int printf(const char *format, ...);
功能:向终端输出一个自定义的格式化的字符串
参数:
format:格式
格式说明:
%c		字符
%d		有符号十进制
%u		无符号十进制
%o		八进制
%x		十六进制
%f		浮点型
%s		字符串
%e		指数形式
%%		%
附加格式说明:
l		小写的L ,输出long 或者 long long 或者double时使用
#		八进制和十六进制前导符
.nf		n是一个数字,表示输出n位小数
m		m是一个数字,表示指定输出数据的位宽
如果实际数据实际的宽度超过指定的宽度 以实际为准
0		指定位宽时,不足的位用0补位
-		左对齐
+		输出正数时显示正号  如果是负数 会忽略正号
... :可变参

#include <stdio.h>
int scanf(const char *format, ...);
功能:在终端按照指定的格式获取数据
参数:
format:格式
格式用法和printf基本一致
scanf的格式怎样写,输入时就得怎样输入。
... :可变参

scanf注意:

#include <stdio.h>

int main(int argc, const char *argv[])
{
#if 0
	char v1;
	scanf("%c", &v1); //注意 变量前要加 &
	printf("v1 = %c\n", v1);
#endif

#if 0
	int v2 = 0;
	scanf("%d", &v2); //只能输入整数 否则会有问题
	printf("v2 = %d\n", v2);
#endif

#if 0
	float v3;
	scanf("%f", &v3);
	printf("v3 = %f\n", v3);
#endif

	char str[32] = {0};
	//scanf("%s", str); //注意 数组名就是首地址 不用加 &
		//scanf不能直接获取带有空格的字符串
	//printf("str = [%s]\n", str);
	
	//如果想获取带有空格的字符串
	//方法1:使用 gets 获取  --推荐使用
	//方法2:使用 [^\n] 获取
	scanf("%[^\n]", str);
	printf("str = [%s]\n", str);

	return 0;
}

c语言程序的结构有三种:顺序、分支(选择)、循环

分支控制语句

if…else 语句

一、简化格式
    if(表达式){
        代码块;
    }
    if(表达式){
        代码块1;
    }else{
        代码块2;
    }
二、阶梯格式
    if(表达式1){
        代码块1;
    }else if(表达式2){
        代码块2;
    }else if(表达式n){
        代码块n;
    }else{
        其他分支;
    }
三、嵌套格式
    if(表达式1){
        if(表达式11){
            代码块11;
        }else if(表达式12){
            代码块12;
        }else{
            其他分支1;
        }
    }else if(表达式2){
        if(表达式21){
            代码块21;
        }else if(表达式22){
            代码块22;
        }else{
            其他分支2;
        }
    }else if(表达式n){
        代码块n;
    }else{
        其他分支;
    } 

注意:
1.if…else语句中,如果代码块只有一行,那么{}可以不写;
2.else之前必须要有if与之对应,否则报错;
3.else与同一层次的前面与之的最近的if结合;
4.关于if…else语句的表达式,
一般情况下都是有关系运算符和逻辑运算符组成的,表达式的结果是bool类型
如果是特殊情况,要注意下面的写法:
if(a=b) 这种写法表达式的结果取决于b的值 b为0则为假 b为非0则为真
if(a=10) 这种写法表达式的结果取决于常量的值 0则为假 非0则为真
所以常量和变量比较相等时,建议将常量写在等号左边,
这时如果少写等号了,编译阶段就会报错了 if(10a)
if(a) 这种写法表达式的结果取决于a的值 a为0则为假 a为非0则为真 等价于 if(a!=0)
if(!a) 这种写法表达式的结果取决于a的值 a为0则为真 a为非0则为假 等价于 if(a
0)
5.同一层次中如果出现多个if语句
if(){}
if(){}
这两个if之间没有任何关系,是独立的。
6.if…else语句是分支控制语句!!!

switch…case语句

格式:

switch(表达式){
    case 常量表达式1:
        代码块1;
        break;
    case 常量表达式2:
        代码块2;
        break;
    case 常量表达式n:
        代码块n;
        break;
    default:
        其他分支;
        break;
}

注意:
1.switch后面的()中可以是变量,也可以是表达式
一般情况下,都是整型或者字符类型,不能是浮点型 。
2.每个case后面的常量表达式就是switch后面表达式所有可能的结果。
3.break的作用的是执行完某个分支的代码后,就立即结束整个switch…case语句
如果没有break,程序会继续执行下面case的代码块(不再判断,直接执行)
直到遇到break或者整个swtich…case语句结束----case击穿
4.default分支的作用,相当于if…else语句中的else分支,
如果前面的case都不满足,则执行default分支
如果不关心其他分支,整个default分支都可以不写

循环控制语句

注意:我们一般情况下使用的循环 都是有结束条件的,如果没有结束条件,就是死循环了。

使用go to实现循环

goto本身是用来实现代码跳转的,注意只能在同一个函数中跳转。
注意:goto对程序的逻辑性和易读性有一定的影响,要谨慎使用。

基本使用格式

代码块1;
goto NEXT;
代码块2;
NEXT:   //标签  是一个标识符 要符合命名规范
代码块3;

while循环

格式

while(表达式){
    代码块;
}

do…while循环

格式

do{
    代码块;
}while(表达式);     //注意 不要忘记此处的分号

do…while 和 while 的区别
while:先判断 后执行
do…while:先执行 后判断
不管do…while的表达式为真还是为假,代码块都至少执行一次。

for循环

格式

for(表达式1; 表达式2; 表达式3){
    代码块;
}

执行逻辑

先执行表达式1,然后执行表达式2,如果表达式2为真,
则执行代码块,然后执行表达式3,
然后再执行表达式2,如果还为真,则继续执行代码块和表达式3
直到表达式2为假,循环立即结束

关于for循环的三个表达式

表达式1:只执行一次,一般是用来给循环变量赋初值的
C99的编译器,允许在for循环的表达式1中定义变量,
但是这种方式定义的变量,只允许在循环内部使用
循环结束时,就被操作系统回收了
表达式2:和前面的while的表达式一样,一般是用来判断真假的
表达式3:一般是用来改变循环变量的值,从而控制循环结束的

这三个表达式,如果哪个不需要,可以不写,但是两个 ;; 必须写
三个表达式中每个表达式都可以由多句组成,之间用逗号分隔,如下
for(i = 0, j = 0; i<10 && j<20; i++, j+=3)

死循环

所谓的死循环就是一直执行循环体,不会结束的循环。

while(1){			//常用的写法
//循环体
}

for(;;){	//注意 三个表达式都可以不写 但是 两个; 必须要写
//循环体
}

辅助控制关键字

break

break 可以用在switch…case语句中 表示执行完某个分支就立即结束整个switch…case语句
break 也可以用在循环中 表示立即结束本层的循环

continue

continue 只能用在循环中 表示立即结束本层的本次循环

return

return 用在函数中 表示返回函数执行的结果
如果用在main函数中 表示立即结束整个程序

其他:

共享文件夹的创建

linux 中共享文件夹的路径: /mnt/hgfs/共享文件夹名称

共享文件夹创建:
1.win桌面右键,新建文件夹 取名 share
2.Ubuntu关机(不是挂起)
3.vmware 菜单栏 虚拟机 设置 选项 共享文件夹
4.总是启用,选择原有的共享文件夹 移除
5.添加,跟着向导依次往下走 主机路径 选中刚才创建的文件夹
6.点击确认 之后重新打开Ubuntu
7.windows中的文件夹 和 linux中 /mnt/hgfs/share 就是共享关系了

vi编辑器配置补全main函数:

1.vi ~/.vim/snippets/c.snippets
2.按G将光标定位到文件最后
3.上下左右的右键可以将折叠展开
4.在 snippet main 下面添加 #include <stdio.h>
注意所有内容都以tab键开头 空行也要有一个tab键
5. :wq 保存退出
以后打开新的 .c 文件时 插入下模式下 输入 main 然后按 tab 键 就可以补全了

VScode连接不上Ubuntu的排查方式

1.确认Ubuntu的网络状态
	Ubuntu中   ifconfig   ip 是不是   192.168.250.100
2.如果不是,重启网络服务:
	sudo service network-manager stop
	sudo rm /var/lib/NetworkManager/NetworkManager.state
	sudo vi /etc/NetworkManager/NetworkManager.conf 
		将里面的 managed=false 改成 managed=true
		保存退出
	sudo service network-manager start
	ifconfig 查看ip地址 如果不是 192.168.250.100
		则 执行 sudo netplan apply
		然后再确定 是不是 192.168.250.100
		ping www.baidu.com  看一下有没有网
3.上面操作做完后 如果vscode还是连不上Ubuntu
		先在 window 按 win+r  输入cmd 
			ssh linux@192.168.250.100
		如果没问题  ls 能看到 自己 Ubuntu家目录下的内容
4.重启一下vscode  如果还不行 再单独找老师处理

冒泡排序

升序:从小到大
降序:从大到小

基本思想

相邻的两个元素之间进行比较,按照要求进行交换。

实现思路–升序为例

第一趟排序:
第一个元素和第二个元素进行比较,将较大的元素放在第二个位置上
然后第二个元素和第三个元素进行比较,将较大的元素放在第三个位置上
依次类推,直到第一趟排序结束,最大的元素就在最后一个位置上了。
第二趟排序:
第一个元素和第二个元素进行比较,将较大的元素放在第二个位置上
然后第二个元素和第三个元素进行比较,将较大的元素放在第三个位置上
依次类推,直到第二趟排序结束,第二大的元素就在倒数第二个位置上了。
依次类推,直到整个数据有序。

代码实现

#include <stdio.h>
int main(){
    int s[10] = {12, 34, 43, 56, 87, 55, 120, 98, 6, 4};
    int len = sizeof(s)/sizeof(s[0]);
    int i = 0;
    int j = 0;
    int temp = 0;
    //排序前
    for(i = 0; i < len; i++){
        printf("%d  ", s[i]);
    }
    printf("\n");
#if 0
    //先完成一趟排序
    for(i = 0; i < len-1; i++){ //此处的-1是为了防止越界的
        if(s[i] > s[i+1]){
            temp = s[i];
            s[i] = s[i+1];
            s[i+1] = temp;
        }
    }
#endif
    //再完成整个排序
    //外层循环控制趟数
    for(j = 0; j < len-1; j++){ //此处的-1是最后一趟只有一个元素了 不用排序了
        //内层循环控制每趟排序
        for(i = 0; i < len-1-j; i++){ //此处的-1是为了防止越界的
                                    //此处的 -j 是因为每趟都能确定好一个元素的位置
                                    //可以减少比较的次数 提高效率
                                    
            if(s[i] > s[i+1]){//如果是降序 只需将此处的 > 改成 < 即可
                temp = s[i];
                s[i] = s[i+1];
                s[i+1] = temp;
            }
        }
    }
    
    //排序后
    for(i = 0; i < len; i++){
        printf("%d  ", s[i]);
    }
    printf("\n");
    return 0;
}

冒泡排序动图:
在这里插入图片描述

数组

概念

数组是用来保存一组相同数据类型的数据的,
数组是一个构造类型,
数组中的每个数据称为数组的元素或者数组的成员,
数组中的每个元素在内存上都是连续的。不管几维数组,都是连续的。

一维数组

一维数组的概念

所谓的一维数组,就是下标只有一个的数组。
一维数组的元素,在内存上是连续的。

一维数组的格式

存储类型 数据类型 数组名[下标];

存储类型:先不用管,不写默认就是 auto
数据类型:表示数组中每个元素的数据类型
数组名:是一个标识符,要符合标识符的命名规范
下标:在定义数组时,下标一般都是常量,表示数组中元素的个数
在其他场景下,可以是常量、也可以是变量、也可以是表达式
表示访问数组中的哪个元素
例:

int s[5];  //定义了一个一维数组,数组名叫s,数组中共有5个元素
             //每个元素都是一个int类型的变量

一维数组的性质

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//定义了一个一维数组,数组名叫s,数组中共有5个元素
	//每个元素都是一个int类型的变量
	int s[5];

	//一维数组的大小 = 单个元素的大小 * 元素的个数
	printf("sizeof(s) = %ld\n", sizeof(s)); //20 == sizeof(int) * 5

	//一维数组访问元素的方式
	// 数组名[下标]  注意下标从0开始
	// 当取出数组中某个元素之后 对他的操作和对普通的单个变量操作是一模一样的
	s[0] = 10;
	s[1] = 20;
	s[2] = 30;
	s[3] = 40;
	s[4] = 50;
	printf("s[0] = %d\n", s[0]);//10
	printf("s[1] = %d\n", s[1]);//20
	printf("s[2] = %d\n", s[2]);//30
	printf("s[3] = %d\n", s[3]);//40
	printf("s[4] = %d\n", s[4]);//50

	//注意:数组名是常量 不能被赋值 也不能++
	int s2[5];
	//s2 = s;//错误的
	//s2++;  //错误的
	
	//数组一旦定义好了 就不能整体赋值了
	//只能一位一位的赋值
	s2[0] = s[0];
	s2[1] = s[1];

	//数组的元素在内存上是连续的
	//  printf  使用 %p 可以将数据按地址的方式输出
	//  & 可以获取变量的地址
	printf("&s[0] = %p\n", &s[0]);//依次相差4 一个sizeof(int)
	printf("&s[1] = %p\n", &s[1]);
	printf("&s[2] = %p\n", &s[2]);
	printf("&s[3] = %p\n", &s[3]);
	printf("&s[4] = %p\n", &s[4]);

	//遍历一维数组
	int i = 0; //用循环变量做为数组的下标
	//for(i = 0; i < 5; i++){
	for(i = 0; i < sizeof(s)/sizeof(s[0]); i++){
		printf("%d  ", s[i]);
	}
	printf("\n");

	return 0;
}

一维数组的初始化

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//一维数组如果不初始化 里面每个元素都是随机值
	//int s[5];
	
	//完全初始化
	//int s[5] = {1, 2, 3, 4, 5};
	
	//不完全初始化
	//这种方式是从左到右依次初始化 没有初始化的元素默认都是0
	//int s[5] = {1, 2};
	
	//全部初始化成0  ----最常用的用法
	//int s[5] = {0};
	
	//省略下标的初始化
	//这种方式 编译器会根据给定数据的个数 自动计算长度
	int s[] = {1, 2, 3, 4, 5, 6};
	printf("sizeof(s) = %ld\n", sizeof(s));//24

	int i = 0;
	for(i = 0; i < 5; i++){
		printf("%d  ", s[i]);
	}
	printf("\n");

	return 0;
}

关于数组越界访问的问题

数组越界访问的问题 编译器不会检查
需要程序员自己检查
数组越界导致的错误是不可预知的
可能不报错
可能段错误
可能修改了不该修改的数据

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int s[5];
	s[5234] = 123;
	printf("%d\n", s[5]);

	return 0;
}

二维数组

二维数组的概念

所谓的二维数组,就是下标有两个的数组。

二维数组的格式

存储类型 数据类型 数组名[行数][列数];
二维数组的元素在内存上也是连续的。
二维数组的本质也是一维数组,
多了一个行号,只是多了一种按行操作的方式而已
二维数组的列数很重要,因为他决定了按行操作时的跨度。
在这里插入图片描述
例:

int s[3][4]; //定义了一个二维数组,数组名叫s
            //数组中共有3行4列 共计 12个元素
            //每个元素都是一个 int 类型的变量

二维数组的性质

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int s[3][4]; //定义了一个二维数组,数组名叫s
            //数组中共有3行4列 共计 12个元素
            //每个元素都是一个 int 类型的变量
	
	//二维数组的大小 = 单个元素的大小 * 行数 * 列数
	printf("sizeof(s) = %ld\n", sizeof(s)); //48 == sizeof(int) * 3 * 4

	//二维数组访问元素的方式
	// 数组名[行号][列号]  注意:行号和列号都是从0开始的
	// 当取出二维数组的一个元素后 对该元素的操作 和 对单个变量的操作是一模一样的
	s[0][0] = 100;
	s[1][2] = 200;
	s[2][3] = 300;
	printf("s[0][0] = %d\n", s[0][0]);
	printf("s[1][2] = %d\n", s[1][2]);
	printf("s[2][3] = %d\n", s[2][3]);

	//二维数组的数组名也是一个常量 不能被赋值 也不能++
	//s = 1234; //错误的
	//s++;		//错误的
	
	//遍历二维数组
	int i = 0;
	int j = 0;
	//外层控制行数
	for(i = 0; i < 3; i++){
		//内层控制列数
		for(j = 0; j < 4; j++){
			printf("%d  ", s[i][j]);
		}
		printf("\n");
	}

	printf("------------------------------------\n");

	//二维数组的元素在内存上也是连续的
	for(i = 0; i < 3; i++){
		for(j = 0; j < 4; j++){
			printf("%p  ", &s[i][j]);//依次相差4字节
		}
		printf("\n");
	}

	return 0;
}

二维数组的初始化

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//二维数组如果没有初始化 里面也是随机值
	//int s[3][4];
	
	//以行为单位
		//完全初始化
		//int s[3][4] = {{1,2,3,4},\
						{5,5,6,7},\
						{9,10,11,12}};
		//不完全初始化
		//没有初始化的元素 默认用0初始化
		//int s[3][4] = {{1,2},\
						{3,4},\
						{5}};\
	//不以行为单位
		//完全初始化
		//int s[3][4] = {1,2,3,4,\
						5,6,7,8,\
						9,10,11,12};
		//不完全初始化
		//没有初始化的元素 默认用0初始化
		//int s[3][4] = {1,2,3,4,5};
	
	//全部初始化成0  ----最常用的用法
	//int s[3][4] = {0};

	//省略下标的初始化
	//行可以省略 列不可以省略	
	//这种写法 即使给的元素不够一整行了 也会按整行分配空间
	//没有初始化的元素 默认用0初始化
	int s[][4] = {1,2,3,4,5,6,7,8,9};
	printf("sizeof(s) = %ld\n", sizeof(s));//48

	//遍历二维数组
	int i = 0;
	int j = 0;
	for(i = 0; i < 3; i++){
		for(j = 0; j < 4; j++){
			printf("%d  ", s[i][j]);
		}
		printf("\n");
	}

	return 0;
}

字符数组和字符串

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//字符数组:数组每个元素都是一个 char 类型的变量
	char s1[5] = {'h','e','l','l','o'};
	int i = 0;
	for(i = 0; i < 5; i++){
		printf("%c", s1[i]);
	}
	putchar(10);

	//将字符串存在字符数组中,可以有下面的写法
	//写法1
	char s2[6] = {"abcde"}; //注意字符串结尾都有隐藏的 '\0'
							//要多分配一个字节给 '\0'用 否则越界
	printf("s2 = [%s]\n", s2);//abcde
	//写法2
	char s3[6] = "world";//{}可以省略
	printf("s3 = [%s]\n", s3);//world
	//写法3
	char s4[] = "beijing"; //会根据字符串的长度 自动计算计算空间大小
						//会 将 '\0' 算进去
	printf("s4 = [%s]\n", s4);//beijing
	printf("sizeof(s4) = %ld\n", sizeof(s4));//8

	//注意:如果不是字符串 就不能按字符串的方式操作
	//如:不能使用 printf的 %s 输出
	//  %s 将后面的数据按字符串的方式输出 
	//  直到遇到 '\0' 才结束 如果没有遇到 就一直往后找
	//printf("s1 = [%s]\n", s1);//结果是不可预知的

	//这种方式可以按字符串的方式操作  因为不完全初始化 后面都是 0
	//而 0 就是  '\0'
	char s5[6] = {'h','e','l','l','o'};
	printf("s5 = [%s]\n", s5);//hello

	//注意  0  '\0'  '0'  的区别
	//  0  和 '\0' 是一样的  ascii码都是 0
	//  '0'的ascii码是 48
	char s6[32] = "hello beijing0hqyj";
	printf("s6 = [%s]\n", s6);//hello beijing0hqyj
	s6[5] = '0';
	printf("s6 = [%s]\n", s6);//hello0beijing0hqyj
	s6[5] = '\0';
	printf("s6 = [%s]\n", s6);//hello
	s6[5] = 0;
	printf("s6 = [%s]\n", s6);//hello

	return 0;
}

字符串处理函数

strlen strcpy strcat strcmp

strlen 计算长度

函数的功能:计算字符串的长度,不包括结尾的'\0' 

头文件:#include <string.h>

函数原型:size_t strlen(const char *s);

参数:就是要计算长度的字符串的首地址

返回值:计算的结果

练习:
自己实现一下 strlen 函数的功能。

#include <stdio.h>
int main(){
    char s[32] = "hello\0aaskjdhfkasjdf";
    int count = 0;
    int i = 0;
    //while(s[i] != '\0'){
    //while(s[i] != 0){
    while(s[i]){    //三种写法都可以
        count++;
        i++;
    }
    printf("len = %d\n", count);//5
    
    return 0;
}

strcpy 拷贝

函数的功能:将src拷贝给dest  包括src的'\0'

注意:目标字符串dest要足够大 

头文件:#include <string.h>

函数原型:char *strcpy(char *dest, const char *src);

参数:dest:目标字符串    src:源字符串

返回值:目标字符串----一般不使用

练习:自己实现一次 strcpy 函数的功能。

#include <stdio.h>

int main(int argc, const char *argv[])
{
	char s1[32] = "hello world";
	char s2[32] = "beijing";

	//把s2 拷贝给 s1
	int i = 0;
    //while(s2[i] != '\0'){
    //while(s2[i] != 0){
	while(s2[i]){
		s1[i] = s2[i];
		i++;
	}
	s1[i] = s2[i];//将s2的'\0'也复制给s1

	printf("s1 = [%s]\n", s1);//beijing

	return 0;
}

strcat 追加

函数的功能:将src追加到dest后面 会覆盖dest结尾的'\0'

**注意:要保证dest足够大** 

头文件:#include <string.h>

函数原型:char *strcat(char *dest, const char *src);

参数:dest:目标字符串     src:源字符串

返回值:目标字符串----一般不使用

练习:自己实现一次 strcat 函数的功能。

#include <stdio.h>
#include <string.h>

int main(int argc, const char *argv[])
{
	char s1[32] = "hello";
	char s2[32] = "beijing";
	printf("前 s1 = [%s]\n", s1);//hello
	printf("前 s2 = [%s]\n", s2);//beijing

	//将s2追加到s1后面
	//先找s1的'\0'的位置
	int i = 0;
	while(s1[i]!='\0'){i++;}
	//循环追加
	int j = 0;
	while(s2[j] != '\0'){
		s1[i] = s2[j];
		i++;
		j++;
	}
	//将s2的'\0'也追加给s1
	s1[i] = s2[j];

	printf("后 s1 = [%s]\n", s1);//hellobeijing
	printf("后 s2 = [%s]\n", s2);//beijing

	return 0;
}

strcmp 比较

函数的功能:比较两个字符串

比较的过程是逐个比较两个字符串中字符的ascii码,
直到出现大小关系时立即返回,
只有两个字符串中第一个'\0'之前所有字符都相等,
才认为两个字符串相等

头文件:#include <string.h>

函数原型:int strcmp(const char *s1, const char *s2);

参数:s1和s2就是要参与比较的两个字符串

返回值:
0	s1==s2
>0	s1>s2
<0	s1<s2

指针

概念

内存中每个字节都有一个编号,这个编号就叫做指针,也叫做地址。
专门用于存储这个变量叫做指针变量。

只不过我们平时交流的时候:
指针:指针变量
地址:地址编号

指针相关的操作

& :取地址符,获取变量的地址
对于多字节的变量,取地址取到的是编号最小的那个,叫做首地址
* :在定义指针变量的时候,只起到一个标识作用,标识定义的是一个指针变量
在其他场景下,对指针取 * 操作,都表示操作指针保存的地址里面的内容

指针和变量的关系

在这里插入图片描述

指针的基本使用

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//程序在执行到定义变量的语句时,操作系统会根据变量的类型给变量分配内存
	int a;
	//通过变量名就可以操作对应的内存空间
	a = 10;

	// & 可以获取变量的地址
	// 使用 %p 输出
	printf("&a = %p\n", &a);

	//使用指针可以保存变量的地址
	//定义指针的格式    数据类型 *指针变量名
	int *p;
	p = &a; //指针p保存了变量a的地址 我们称之为 指针p指向变量a
	printf("p = %p  &a = %p\n", p, &a);//一样的
	//也可以用初始化的写法  int *p = &a;

	//当指针保存了变量的地址后 就可以通过指针操作变量对应的内存空间了
	*p = 520;
	printf("*p = %d  a = %d\n", *p, a); //  520 520

	//不能使用普通变量来保存地址
	//long long value = &a;
	//printf("value = %#llx\n", value);//只保存可以
	//*value = 1314;//但是普通变量不允许取 * 操作
	
	//指针只能保存已经分配了的地址
	//int *p2 = 0x11223344;
	//printf("p2 = %p\n", p2);
	//*p2 = 100;//对没有分配的地址进行取 * 操作 错误是不可预知的
	
	//指针类型的作用
	//决定了从他保存的地址开始 一共能操作多少个字节
	//int *p 能操作4个字节
	//char *p 只能操作一个字节
	//所以一般情况下 我们都让指针的类型 和指向的变量的类型保持一致
	//目的是为了 让 操做空间的大小一致
	
     //常量是没有地址可言的
     //int *p = &10;
 
	//一行中可以定义多个指针
	//要注意下面的写法
	//int *p3, p4;//这种写法 p3 是指针  p4就是一个int类型的变量
	//p3 = &a;
	//p4 = &a;
	int *p3, *p4; //这样写 p3 和 p4 才都是指针
	p3 = &a;
	p4 = &a;

	//定义指针如果不初始化 里面都是随机值 也就是指针指向随机地址
	//这种指针叫做  野指针 , 野指针对程序是有害的 错误不可预知
	//int *p5;
	//*p5 = 1234;//错误是不可预知的
	
	//定义指针时如果不知道指向谁 可以先指向 NULL 用来防止野指针
	// 指向 NULL 的指针叫做 空指针
	// NULL 本质   (void *)0
	// 应用程序对编号为0的地址取 *操作 一定是段错误
	// 段错误也比不可预知的错误好!!!
	int *p6 = NULL;
	//*p6 = 1234; //段错误

	return 0;
}

指针变量的大小

32位系统中 指针的大小都是4字节的
64位系统中 指针的大小都是8字节的

#include <stdio.h>

int main(int argc, const char *argv[])
{
	char *a;
	short *b;
	int *c;
	long *d;
	long long *e;
	float *f;
	double *g;
	printf("sizeof(char *) = %ld  sizeof(a) = %ld\n", sizeof(char *), sizeof(a));//8 8
	printf("sizeof(short *) = %ld  sizeof(b) = %ld\n", sizeof(short *), sizeof(b));//8 8
	printf("sizeof(int *) = %ld  sizeof(c) = %ld\n", sizeof(int *), sizeof(c));//8 8
	printf("sizeof(long *) = %ld  sizeof(d) = %ld\n", sizeof(long *), sizeof(d));//8 8
	printf("sizeof(long long *) = %ld  sizeof(e) = %ld\n", sizeof(long long *), sizeof(e));//8 8
	printf("sizeof(float *) = %ld  sizeof(f) = %ld\n", sizeof(float *), sizeof(f));//8 8
	printf("sizeof(double *) = %ld  sizeof(g) = %ld\n", sizeof(double *), sizeof(g));//8 8

	return 0;
}

指针的运算

指针的运算本质就是指针保存的地址量作为运算量来参与运算。
既然是地址的运算,能进行的运算就是有限的了。
相同类型的指针变量之间做运算才有意义。

指针能作用的运算:
算数运算: + - ++ –
关系运算:> < >= <= == !=
赋值运算: =

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//        0x20 0x24 0x28 0x2c 0x30
	int s[5] = {10, 20, 30, 40, 50};
	//一个指针加上一个整数n 表示加上n个指针的数据类型的大小
	int *p1 = &s[0];
	int *p2 = p1+4; //p2 = p1+4*sizeof(int)
	printf("*p1 = %d\n", *p1);//10
	printf("*p2 = %d\n", *p2);//50
	printf("p1 = %p  p2 = %p\n", p1, p2);//相差16 == 4*sizeof(int)

	//指针的强转是安全的
	char *p3 = (char *)&s[0];
	char *p4 = p3+4;
	printf("p3 = %p  p4 = %p\n", p3, p4);//相差4 == 4*sizeof(char)


	//两个指针做差 得到的结果是相差的指针的数据类型的个数
	int *p5 = &s[0];
	int *p6 = &s[3];
	int ret = p6-p5;
	printf("ret = %d\n", ret);//3  表示相差3个sizeof(int)

	short *p7 = (short *)&s[0];
	short *p8 = (short *)&s[3];
	ret = p8-p7;
	printf("ret = %d\n", ret);//6  表示相差6个sizeof(short)


	//自增自减运算 要注意下面的用法  以自增为例 自减同理
	//          0x20 0x24 0x28 0x2c 0x30
	//int s[5] = {10, 20, 30, 40, 50};
	int *p9 = &s[0];
	int v1 = *++p9;
	printf("v1 = %d  p9 = %p  &s[1] = %p\n", v1, p9, &s[1]); // 20  后两个地址一样

	int *q1 = &s[0];
	int v2 = *q1++;
	printf("v2 = %d  q1 = %p  &s[1] = %p\n", v2, q1, &s[1]); // 10 后两个地址一样

	int *q2 = &s[0];
	int v3 = (*q2)++; // 等价于  int v3 = s[0]++;
	printf("v3 = %d  s[0] = %d\n", v3, s[0]); // 10 11

	//指针的关系运算
	int *q3 = &s[0];
	int *q4 = &s[1];
	if(q4 > q3){
		printf("yes\n");
	}else{
		printf("no\n");
	}

	//指针的赋值运算 指针变量本质也是变量 可以被重新赋值
	int *q5 = &s[0];
	q5 = &s[2];
	int *q6 = &s[3];
	q5 = q6;

	return 0;
}

大小端存储问题

不同类型的CPU对多字节数据的存储方式也是不同的。
分为小端存储和大端存储。
在这里插入图片描述
我们现在用的是小端的主机。

请你用C语言写一个简单的程序,判断你使用的主机是大端存储还是小端存储。

#include <stdio.h>
int main(){
    int a = 0x12345678;
    char *p = (char *)&a;
    if(0x78 == *p){
        printf("小端\n");
    }else if(0x12 == *p){
        printf("大端\n");
    }
    return 0;
}

思考:在32位小端存储的主机上,下面的代码会输出什么。

int s[5] = {1,2,3,4,5};
int *p = (int *)((int)s+1);
printf("%x\n", *p);  //2000000

指针和一维数组

#include <stdio.h>

int main(int argc, const char *argv[])
{
	
	int s[5] = {10, 20, 30, 40, 50};

	//数组名就是数组的首地址
	printf("s = %p\n", s);
	printf("s+1 = %p\n", s+1);//相差1个int
	//数组名的操作空间 和 数组的类型是一致的
	
	//研究 数组名[下标]  访问元素的本质
	printf("*(s+0) = %d  s[0] = %d\n", *(s+0), s[0]);// 10 10
	printf("*(s+1) = %d  s[1] = %d\n", *(s+1), s[1]);// 20 20
	//也就是说 数组名[下标] 方式访问元素的本质就是对指针取*操作
	// s[i] <==> *(s+i)
	
	//可以定义一个指针来保存数组的首地址, 有下面的写法
	//写法1:
	//int *p = &s[0];
	//写法2:
	int *p = s;  //常用的写法
	//写法3:不要使用
	//int *p = &s;// &s 这种写法相当于指针的升维操作 改变了指针的操作空间
				// 记住:不要对数组名进行 取地址 & 操作 !!!
	printf("p = %p\n", p);
	
	//当指针保存了数组的首地址之后  就可以操作数组元素了
	//有如下的等价关系
	//  s[i]  <==>  *(s+i)  <==>  *(p+i)  <==>  p[i]

	//遍历一维数组
	int i = 0;
	for(i = 0; i < 5; i++){
		//printf("%d  ", s[i]);
		//printf("%d  ", *(s+i));
		//printf("%d  ", p[i]);
		printf("%d  ", *(p+i));
	}
	printf("\n");

	// p 和 s 的区别
	// p 是指针 是变量 可以被赋值 也可以执行++操作
	// s 是数组名 是常量 不可以被赋值 也不可以执行++操作
	for(i = 0; i < 5; i++){
		printf("%d  ", *p++);
		//printf("%d  ", *s++);//错误的 s不能++
	}
	printf("\n");

	return 0;
}

指针和二维数据

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int s[3][4] = {1,2,3,4,
					5,6,7,8,
					9,10,11,12};
	//二维数组数组名也是首地址
	printf("s = %p\n", s);
	//研究二维数组数组名的操作空间
	printf("s+1 = %p\n", s+1);//相差16 --> 4*sizeof(int)
	//也就是说二维数组的数组名操作空间是一整行元素 --我们称之为 行指针
	
	//对二维数组的数组名取 一次*操作 相当于给行指针进行降维操作
	//将操作空间是一行元素的指针 降维 成操作空间是一个元素的指针 列指针
	printf("*s = %p\n", *s);
	printf("*s+1 = %p\n", *s+1);//相差4 -->1*sizeof(int)

	//对列指针再取*操作,才是操作内容
	printf("**s = %d\n", **s);//1
	printf("*(*(s+0)+0) = %d\n", *(*(s+0)+0));//1
	printf("*(*s+2) = %d\n", *(*s+2));//3
	printf("*(*s+2) = %d\n", *(*s+2));//3
	printf("*(*(s+2)+1) = %d\n", *(*(s+2)+1));//10

	//也就是说 有如下的等价关系
	// s[i][j]  <==> *(s[i]+j) <==> *(*(s+i)+j)
	
	//注意:二维数组的数组名 操作空间是一行元素
	//已经超过了基本类型的操作空间了
	//所以 不能使用普通的指针来指向二维数组
	//因为普通的指针没法按行操作
	//int *p = s;//一般不这样使用
	//保存二维数组的首地址 需要用 数组指针

	//二维数组的遍历
	int i = 0;
	int j = 0;
	for(i = 0; i < 3; i++){
		for(j = 0; j < 4; j++){
			//printf("%d  ", s[i][j]);
			//printf("%d  ", *(s[i]+j));//这种写法不常用
			printf("%d  ", *(*(s+i)+j));
		}
		printf("\n");
	}
	
	return 0;
}

数组指针

本质是一个指针,指向一个二维数组,也叫行指针。
数组指针多用于将二维数组作为函数的参数传递时。
格式:

数据类型  (*指针变量名)[列宽];

例:

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int s[3][4] = {1,2,3,4,
					5,6,7,8,
					9,10,11,12};
	//定义了一个数组指针p指向二维数组s
	//int (*p)[4] = s;//初始化的写法
	int (*p)[4] = NULL;
	p = s;

	//数组指针指向二维数组后 操作就和二维数组数组名的操作是一样的了
	//也就是说 有如下的等价关系
	//s[i][j] <==> *(s[i]+j) <==> *(*(s+i)+j) <==
	//==>  p[i][j] <==> *(p[i]+j) <==> *(*(p+i)+j)

	//二维数组的遍历
	int i = 0;
	int j = 0;
	for(i = 0; i < 3; i++){
		for(j = 0; j < 4; j++){
			//printf("%d  ", s[i][j]);
			//printf("%d  ", *(s[i]+j));//这种写法不常用
			//printf("%d  ", *(*(s+i)+j));
			//printf("%d  ", p[i][j]);
			//printf("%d  ", *(p[i]+j));//这种写法不常用
			printf("%d  ", *(*(p+i)+j));
		}
		printf("\n");
	}

	//p和s的区别
	//还是  p是变量  s是常量

	return 0;
}

不能对一维数组名取地址

指针数组

本质是一个数组,数组中每个元素都是一个指针。
格式:

数据类型 *指针数组名[下标];

例:

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//处理多个字符串 可以将其存储在二维数组里
	char name1[4][64] = {
			"zhangsan",
			"lisi",
			"fulajimier.fulajimiluoweiqi.pujing",
			"zhaoliu"};
	printf("%s\n", name1[0]);//zhangsan
	printf("%s\n", name1[1]);//lisi
	printf("%s\n", name1[2]);//fulajimier.fulajimiluoweiqi.pujing
	printf("%s\n", name1[3]);//zhaoliu
	//但是这种用法 不好 会造成空间上的严重浪费 因为需要以最长的字符串为准
	
	printf("----------------------------------------\n");

	//也可以使用指针数组来处理
	//定义了一个指针数组 数组名叫 name2 数组中共有4个元素
	//每个元素都是一个 char * 类型的指针
	char *name2[4] = {NULL};
	name2[0] = "zhangsan";
	name2[1] = "lisi";
	name2[2] = "fulajimier.fulajimiluoweiqi.pujing";
	name2[3] = "zhaoliu";

	printf("%s\n", name2[0]);
	printf("%s\n", name2[1]);
	printf("%s\n", name2[2]);
	printf("%s\n", name2[3]);

	return 0;
}

指针和字符串

虚拟内存的划分:
在这里插入图片描述

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//可以将字符串保存在字符数组中
	//s1是数组 在栈区 "hello world" 是字符串常量 在字符串常量区
	//这个操作相当于用字符串常量区的 "hello world"
	//给栈区的数组初始化
	char s1[32] = "hello world";
	//后面对s1的操作 操作的都是栈区的数组
	//栈区的内容是允许修改的
	*s1 = 'H';
	printf("s1 = [%s]\n", s1);//Hello world
	char s2[32] = "hello world";
	//栈区定义多个数组,即使保存一样的数据,数组的首地址也不一样
	printf("s1 = %p,  s2 = %p\n", s1, s2);//不一样


	//也可以使用指针直接指向字符串常量
	//这种写法 指针变量p在 栈区 保存的地址是字符串常量区 "hello world"的地址
	char *p1 = "hello world";
     printf("p1 = %s\n", p1);//读操作允许
	//字符串常量区的内容是不允许修改的
	//*p1 = 'H';//段错误 不允许修改
	//不管定义多少个指针,只要指向同一个字符串常量 那么保存的地址就是一样的	
	char *p2 = "hello world";
	printf("p1 = %p,  p2 = %p\n", p1, p2);//一样的

	return 0;
}

二级指针

二级指针是用来保存一级指针的地址的。
二级指针多用于将一级指针的地址作为函数的参数传递。

int a = 10; //变量
int *p = &a; //一级指针
int **q = &p; //二级指针

变量、一级指针、二级指针 关系图。
在这里插入图片描述

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int a = 10;
	int *p = &a;
	int **q = &p;
	//有了上述操作之后 有下面的等价关系
	//  a  <==>  *p  <==>  **q
	//  &a <==>  p   <==>  *q
	//  &p <==> q
	printf("a = %d  *p = %d  **q = %d\n", a, *p, **q);//一样的
	printf("&a = %p  p = %p  *q = %p\n", &a, p, *q);//一样的
	printf("&p = %p  q = %p\n", &p, q);//一样的

	//通过二级指针也可以操作变量 但是需要取 ** 操作
	**q = 1314;
	printf("a = %d\n", a);//1314

	//注意:用一级指针保存一级指针的地址
	//int *q2 = &p; //可以保存
	//但是一级指针不能取 ** 操作  所以保存了也没有意义
	//**q2 = 1314;//错误的

	return 0;
}

const 关键字

const修饰变量时,表示不能通过变量名,修改变量的值。
const int a = 10;
a = 20; //报错

const 修饰指针的时候,有如下几种写法:----笔试题常考

const int *p;
int const *p;
int * const p;
const int * const p;
//区分的时候,看const和*的相对位置关系
    // 如果const在*的左边,表示修饰的是 *p
    // 表示不能通过指针修改指向的空间的内容 但是指针的指向可以修改
    // 如果const在*的右边,表示修饰的是 p
    // 表示可以通过指针修改指向的空间的内容 但是指针的指向不可以修改
    //如果*的左右都有const,表示都不能修改

例:

#include <stdio.h>

int main(int argc, const char *argv[])
{
	int a = 10;
	int b = 20;

	const int *p1 = &a;
	//*p1 = 1314;//错误的
	//p1 = &b;//正确的

	//这种写法和上面的写法完全一样 只不过上面的写法更常用
	int const *p2 = &a;

	int * const p3 = &a;
	//*p3 = 1314;//正确的
	//p3 = &b;//错误的
	
	const int * const p4 = &a;
	//*p4 = 1314;//错误的
	//p4 = &b;//错误的

	return 0;
}

函数

概念

将实现某些功能的代码封装成代码块,想使用这个功能的时候,
通过代码块的名字就可以调用,就无须写重复的代码了。
这个代码块就叫做函数,代码块的名字就叫做函数名。
如:strlen strcpy atoi pow printf gets 。。。

函数的定义和调用

定义函数的格式:

返回值类型  函数名(函数的参数列表){
    函数体;//也就是要实现功能的代码块
}

函数名:也是一个标识符,要符合标识符的命名规范。
例:

#include <stdio.h>

//int 是函数的返回值类型 如果没有返回值 可以写成 void 但是不能不写
//print_menu 是函数名 是一个标识符 要符合命名规范
//()  里面是函数的参数列表 如果没有 可以空着 或者写void 括号必须写
//{}  里面是函数体 也就是用来实现功能的代码块
int print_menu(void){
    printf("-----------------------------\n");
    printf("| 1.zhuce 2.denglu 3.tuichu |\n");
	printf("-----------------------------\n");
	printf("请输入你的选择:\n");
	//return 表示返回函数运行的结果 如果没有返回值 可以不写 或者写 return;
	//如果有返回值 return后面既可以是常量 也可以是变量 也可以是表达式
	//但是要注意 类型必须和函数名前面的类型保持一致
	return 0;
}
//函数一旦定义好之后 就可以在其他函数中调用了
//函数如果不被调用 里面的代码是不会执行的

void test(void){
	printf("----test start----\n");
	print_menu();
	printf("----test end----\n");
}

int main(int argc, const char *argv[])
{
	printf("----main start----\n");
	//函数的调用
	//通过函数名即可调用函数 ()里面是要给函数串的参数
	//如果没有参数,可以不写,但是()必须写
	//当程序执行到调用函数的语句时  就会跳转到函数内部执行
	print_menu();
	//执行完之后回到调用位置继续向下执行
	
	//下次再使用函数的功能时 直接通过函数名再次调用即可
	print_menu();

	test();

	printf("----main end----\n");
	return 0;
}

函数的声明

如果把所有函数都定义在main函数的前面,在main函数中调用是没有问题的。
但是函数之间相互调用,就可能出现不认识的情况。
这时候,就需要用到函数的声明了。

#include <stdio.h>

//函数的声明
void func1();
void func2();//一般都放在文件开头的位置 方便其他函数调用

int main(int argc, const char *argv[])
{
	func1();
	return 0;
}

//函数的定义
void func1(){
	printf("i am func1\n");
	func2();
}

//函数的定义
void func2(){
	printf("i am func2\n");
}

函数的参数

函数为什么要有参数?
在函数实现功能的过程中,有些值,函数里面没有,
这时,就需要调用者,在调用函数的时候,以参数的形式将这些值传递给函数。

#include <stdio.h>

void my_add(int x, int y);
//void my_add(int, int); //有参数的函数声明 可以只写类型 不写形参名

int main(int argc, const char *argv[])
{
	int a = 10;
	int b = 20;
	//有参数的函数调用
	//调用函数的时候 ()里面的叫做函数的实际参数 简称 实参
	//要注意 实参的个数和类型 要和 形参 保持一致
	my_add(a, b);

	my_add(100, 200);

	return 0;
}

//功能:输出两个整数的和
//定义函数时 ()里面的叫做函数的形式参数,简称 形参
//形参只是告诉调用者 调用我这个函数的时候 需要几个 什么类型的函数
//在调用函数的时候 操作系统会给形参分配空间 然后用实参来初始化形参
//相当于隐藏了两句代码: int x = a; int y = b;
//形参只能在函数里面使用
//在函数调用结束的时候 形参占用的内存空间就被操作系统回收了
void my_add(int x, int y){
	int sum = x+y; //sum占用的空间 在函数调用结束时 也会被回收
	printf("sum = %d\n", sum);
	return;
}

函数的返回值

函数为什么要有返回值?
函数执行的结果,有时需要供后面使用,而不是直接输出到终端,
这时就需要函数返回执行的结果了。
如果需要返回值,就写,如果不需要,可以不写。

#include <stdio.h>

//计算两个整数的和并返回计算的结果
int my_add(int x, int y){
    int sum = x+y;
    return sum;//注意类型要和函数名前面的类型一致
    printf("hello world\n");//函数中遇到return之后后面的代码就都不执行了
}

int main(int argc, const char *argv[])
{
	int a = 10;
	int b = 20;
	int ret = 0;//可以定义一个变量来接收函数执行的结果
	ret = my_add(a, b);
	printf("ret = %d\n", ret);//30

	//有返回值的函数也不是接收返回值
	//函数也回被调用 只不过没有现象了
	my_add(100, 200);
	
	return 0;
}

实际开发的时候,函数一般都是有返回值的,
因为可以通过返回值来判断函数的执行情况,
如下面的伪代码:

#define ERR_NET 1
#define ERR_DATABASE 2
#define ERR_LOG 3
#define ERR_USER 4
#define OK 0

int pro_init(){
    if(建立网络连接失败){
        return -ERR_NET;
    }
    if(连接数据库失败){
        return -ERR_DATABASE;
    }
    if(打开日志文件失败){
        return -ERR_LOG;
    }
    if(加载用户信息失败){
        return -ERR_USER;
    }
    return OK;
}
int main(){
    int ret = pro_init();
    if(OK != ret){
        根据ret的不同来执行不同的补救措施了
    }
    return 0;
}

全局和局部

#include <stdio.h>

//作用域:在哪个范围内可以访问
//生命周期:何时被创建 何时被回收

//没有被任何 {} 扩住的变量 就叫做全局变量
//作用域: 整个文件
//生命周期: 在main函数执行之前就已经被分配好了 直到整个程序结束 才被回收
int v1 = 100;

void test(){
	//被任何{} 扩住的变量 都叫做局部变量
	//作用域:最近的{}
	//生命周期:定义时被创建 最近的{}结束时 被回收
	int v2 = 200;
	printf("test: v1 = %d\n", v1);
	printf("test: v2 = %d\n", v2);
}

int main(int argc, const char *argv[])
{
	printf("main: v1 = %d\n", v1);
	//printf("main: v2 = %d\n", v2); //不可以访问局部变量 v2
							//因为已经出了v2的作用域了
	test();

	//全局变量和局部变量重名时 访问 采用局部优先原则
	int v1 = 1314;
	printf("main xxx v1 = %d\n", v1);//1314

	return 0;
}

函数的传参方式

全局传参–了解

复制传参(值传递)

是将实参的值复制了一份,传给了形参。
在函数内部,不管如何修改形参,实参都不会发生变化,
因为实参和形参不在同一块内存空间上。

#include <stdio.h>

//功能:将参数扩大10倍后求和
int my_add_10(int x, int y){
	//即使形参和实参重名了 也没问题 因为不在同一个作用域
	//不管对形参如何修改 实参都不会变化
	x *= 10;
	y *= 10;
	printf("func:x = %d  y = %d\n", x, y);//100 200
	printf("func:&x = %p  &y = %p\n", &x, &y);//和下面的不一样
	return x+y;
}

int main(int argc, const char *argv[])
{
	int x = 10;
	int y = 20;
	printf("main:&x = %p  &y = %p\n", &x, &y);//和上面的不一样
	printf("%d\n", my_add_10(x, y));//300
	printf("main:x = %d  y = %d\n", x, y);//10 20

	return 0;
}

地址传参(地址传递)

#include <stdio.h>

//实现不了将 a+b 的值放在c中的功能
int my_add_1(int x, int y, int z){
    z = x+y;
    printf("z = %d\n", z);//30
}

//x和y的传参方式是值传递  z的传参方式是地址传递
//当函数中需要修改实参的值时 就应该把实参的地址传过来
int my_add_2(int x, int y, int *z){
	*z = x+y;
}

int main(int argc, const char *argv[])
{
	int a = 10;
	int b = 20;
	int c = 0;
	my_add_1(a, b, c);
	printf("1111:c = %d\n", c); //0

	my_add_2(a, b, &c);
	printf("2222:c = %d\n", c); //30

	return 0;
}

一维数组的传参方式

字符串的传参方式

字符串传参:只传首地址即可,因为每个字符串结尾都有’\0’作为标识。
例:

#include <stdio.h>

char *my_strcpy(char *dest, const char *src){
	char *ptemp = dest; //备份一下 dest 用作返回值
	while(*src != '\0'){
		*ptemp = *src;
		ptemp++;
		src++;
	}
	*ptemp = *src;
	return dest;
}

int main(int argc, const char *argv[])
{
	char s1[32] = "hello";
	char s2[32] = "beijing";
	char *p = my_strcpy(s1, s2);
	printf("%s\n", p);//beijing

	printf("s1 = [%s]\n", s1);//beijing
	printf("s2 = [%s]\n", s2);//beijing

	return 0;
}

整型数组传参方式

整型数组传参时:既要传数组的首地址,还要传数组的长度
因为整型数组是没有 ‘\0’ 作为结束标志的
例:

#include <stdio.h>
//遍历一维数组的函数
void print_arr(int *p, int len){ //最常用的写法
	int i = 0;
	for(i = 0; i < len; i++){
		printf("%d  ", p[i]);
	}
	printf("\n");
}
#if 0
//也可以用下面的两种写法
//下面的两种写法叫做 代码的自注释
//这两种写法 p 的本质也是指针
//void print_arr(int p[100], int len){
//void print_arr(int p[], int len){
	printf("sizeof(p) = %ld\n", sizeof(p));//8
	int i = 0;
	for(i = 0; i < len; i++){
		printf("%d  ", p[i]);
	}
	printf("\n");
}
#endif

int main(int argc, const char *argv[])
{
	int s[5] = {1,2,3,4,5};
	print_arr(s, 5);

	int s2[10] = {1,2,3,4,5,6,7,8,9,10};
	print_arr(s2, 10);

	return 0;
}

二维数组的传参方式

二维数组传参时:要用数组指针作为形参

#include <stdio.h>
//遍历二维数组的函数
void print_arr(int (*p)[4], int row, int column){
	int i = 0;
	int j = 0;
	for(i = 0; i < row; i++){
		for(j = 0; j < column; j++){
			printf("%d  ", p[i][j]);
		}
		printf("\n");
	}
}

int main(int argc, const char *argv[])
{
	int s[3][4] = {1,2,3,4,
					5,6,7,8,
					9,10,11,12};
	print_arr(s, 3, 4);

	return 0;
}

main函数的参数

在这里插入图片描述

#include <stdio.h>

int main(int argc, const char *argv[])
{
	//argc 是在命令行执行命令是 参数的个数 (包括 可执行文件名 在内的)
	printf("argc = %d\n", argc);

	printf("argv[0] = [%s]\n", argv[0]); //就是可执行文件  ./a.out
	printf("----------------------\n");
	int i = 0;
	for(i = 0; i < argc; i++){
		printf("%s\n", argv[i]);
	} 

	return 0;
}

复习

C语言的本质

C语言本质操作的是内存
内存分配的最小单位 字节Byte

分配内存的方式

1.定义变量时,操作系统会根据变量的类型,给变量分配对应大小的内存空间
存储类型 数据类型 变量;
2.malloc由程序员手动在堆区分配

C语言中变量的数据类型

数据类型决定了它定义的变量分配空间的大小。
指针中的数据类型,不论什么样的指针只能保存一个字节的编号,它具体能操作几个字节,取决于指针的类型。char类型只能操作1个字节,int可以操作4个字节。
一个字节能存几个十六进制数?两个(因为一个十六进制占四个二进制,两个十六进制占八个二进制)八位二进制就是一个字节

基本类型:
		字符类型		char		%c		1字节
		短整型		short	%d		2字节
		整型			int		%d		4字节
		长整型		long		%ld		32位系统(4字节)  64位系统(8字节)
		长长整型		long long	%lld		8字节
		单精度浮点型	float		%f		4字节
		双精度浮点型	double	%lf		8字节
		多精度浮点型	long double	%Lf	32位系统(12字节)  64位系统(16字节)
		枚举类型		enum
构造类型:
		数组	char s[5]     int  s[10]
		结构体			struct
		共用体(联合体)	 union
指针类型:
		char *p1----
		int *p2---
		int (*p3)[3]----数组指针
		int **p4----二级指针
		大小:32位系统4字节    64位系统8字节---不管是什么类型的指针大小都是一样的
		指针类型的作用是从他保存的地址开始能操作多少个字节。
空类型:
		void---函数的返回值见到过
		void *----操作空间不确定

存储类型

const

const修饰变量时,表示不能通过变量名,修改变量的值。

const int a =10;
a=20;  //报错

const 修饰指针的时候,有如下几种写法:----笔试题常考

const int *p;
int const *p;
int * const p;
const int * const p;
//区分的时候,看const和*的相对位置关系
    // 如果const在*的左边,表示修饰的是 *p
    // 表示不能通过指针修改指向的空间的内容 但是指针的指向可以修改
    // 如果const在*的右边,表示修饰的是 p
    // 表示可以通过指针修改指向的空间的内容 但是指针的指向不可以修改
    //如果*的左右都有const,表示都不能修改

static

static关键字有两个作用:

1.延长局部变量的生命周期,延长到整个程序结束
而且static修饰的局部变量,不是存在栈区的而是存在静态区base段和data段,已经初始化的是在data段,没初始化的是在base段

2.限定作用域,static修饰的变量或函数只能在当前文件中访问
例子,见下面extern的例子

#include <stdio.h>
int a = 10;
void my_test1(){
    int num = 10;  //num是局部变量,声明周期是最近的{}结束
    num++;
    printf("num = %d\n", num);
}
void my_test2(){
	static int num = 10; //num占用的空间在main函数执行前就分配好了 在data段
						//这条指令每次调用函数不会重新执行
	num++;
	printf("num = %d\n", num);
}
int main(int argc, const char *argv[])
{
	my_test1();//11
	my_test1();//11
	my_test1();//11
//因为num是局部变量,声明周期是最近的{}结束,所以每次用完都会回收
//故结果都为11
	my_test2();//11
	my_test2();//12
	my_test2();//13
 
 //问:被static修饰的局部变量与全局变量有什么区别??
	//static修饰的局部变量和 全局变量 生命周期一样
	//但是作用域不一样  static修饰的变量作用也还是最近的{}
	printf("a = %d\n",a);
	//printf("num = %d\n", num);//错误的 出了作用域不能访问
	return 0;
}

extern

extern关键字的作用是声明 全局变量或者函数是在其他.c文件中定义的
如 b.c中需要使用a.c中定义的全局变量或函数,需要在b.c中使用extern声明
在这里插入图片描述

register

register修饰的是一个寄存器类型的变量,被执行的效率高。
CPU取数据的优先级(寄存器–>cache(高速缓存)—>内存)
但是寄存器的个数是有限的,有 37个的 有40个的 …
所以将所有的变量都定义成寄存器变量是不现实的
----实际应用层开发的过程中,基本不使用。
C语言的本质是操作内存
在这里插入图片描述
注意:register修饰的变量是不能取地址的。
内存中每个字节都有一个编号,这个编号是地址。寄存器没有在内存中,所以寄存器没有地址。

volatile

防止编译器优化的。
要求CPU每次取数据,都在内存上取。

volatile使用场景:
访问中断状态的寄存器
多线程访问同一个变量的时候

auto

声明变量是一个自动类型的变量—栈区的一般都是自动的
如果定义局部变量的时候,不写存储类型,默认的都是auto
非自动类型的变量:----静态区
全局变量
static 修饰的局部变量

指针复习

一级指针

int a = 10;
int *p = &a; —指针保存变量的地址是要操作它, 怎么通过指针操作变量?
p = 520; ----给p赋值相当于给a赋值
int b = 20;
p = &b; ----给p赋值是改变了p的指向
char *q = “hello”; p 和 q的区别?操作空间不同。p从保存的空间开始能操作4个字节,q只能操作一个字节
验证指针操作空间就让他们+1看加多少。
p++; //正确的,指针的指向向后偏移一个 int —指向的是a后边的那块空间,越界访问了但是没有问题
q++; //正确的,指针的指向向后偏移一个 char

二级指针

int a = 10;
int *p = &a;
int q = &p; —二级指针是用来保存一级指针的地址的。一级指针的地址为什么要用二级指针来保存?
—因为二级指针可以取
的操作
a <> *p <> **q
&a <> p <> *q
&p <==> q
在这里插入图片描述
//以64位系统为例
p++; //正确的 指针p的指向向后偏移 4字节 —指向的是a后边的那块空间,越界访问了但是没有问题
(*p)++; // a++ p的指向没变
*p++; //正确的 相当于 先取出a的值 然后p++ ----指针的指向变了,里边的值没变
q++; //正确的 指针q的指向向后偏移 8字节 一个 int * 的大小 —q是二级指针 操作空间是一个一级指针的大小
(*q)++; // p++
*q++; // 正确的 相当于 先取出p的值 然后q++
(**q)++; // a++

指针和一维数组

char arr[20];
char *p = arr; —定义一个指针指向首地址
a[i] <> *(a+i) <> p[i] <==> *(p+i)

已知条件:
	char *p = "hello world";   --p在栈区 它保存的地址是字符串常量区hello world的地址
	char a[] = "hello world";
	char *str;
	char b[10] = "beijing";
判断:
	p++;		//对	p的指向向后移动1个char         从保存h到保存e的地址
	*p++;		//对	相当于先取出 *p 然后 p++       相当于先取出h,再改变p的指向
	(*p)++;		//错	字符串常量区的内容不允许修改    在尝试把h变成i,不可以,因为字符串常量区内容不许修改
	*p='M';		//错	字符串常量区的内容不允许修改    
	p="hqyj";	//对	修改指针变量p的指向 让其指向字符串常量 "hqyj"
	a++;		//错	a是数组名 是常量
	*a++;		//错	a是数组名 是常量
	(*a)++;		//对	a[0]++     就是尝试把h变成i
	*a='M';		//对	a[0]='M'   
	a="hqyj";	//错	a是数组名 是常量 ---a是数组 数组一旦定义好了不能整体赋值,可以使用strcpy(a,"hqyj");   
	str++;		//对	指针的指向可以偏移  --str是野指针 野指针也可以++  指针的指向可以偏移
	*str='M';	//错	野指针 错误不可预知   
	str=p;		//对	指针变量的相互赋值 让str也指向 "hello world"
	*str='M';	//错	字符串常量区的内容不允许修改
	b = a;		//错	b是数组名 是常量  如果想赋值  strcpy(b, a);

数组指针

本质是一个指针,指向一个二维数组,也叫作行指针。
int s[2][3] = {1,2,3,4,5,6};
int (*p)[3] = s; //定义数组指针指向二维数组
s[i][j] <> *(s[i]+j) <> ((s+i)+j) <> p[i][j] <> (p[i]+j) <==> ((p+i)+j)
数组指针的操作空间是一行元素:
p++; //向后偏移12字节 == 3
sizeof(int)

指针数组

本质是一个数组,因为数组中每个元素都是一个指针
char *arr[5];
//定义了一个指针数组,数组名叫arr 数组中共有5个元素 每个元素都是一个char *类型的指针
arr[0] = “hello”;
char num = ‘M’;
arr[1] = #

指针函数

本质是一个函数,返回值是一个指针类型。
注意:不能返回局部变量的地址,因为局部变量在函数调用结束之后,就被操作系统回收了
可以返回:
1.全局变量的地址
2.static修饰的局部变量的地址
3.参数传递过来的地址(如 strcpy实现的过程)

#include <stdio.h>

//错误的 不能返回局部变量的地址
int *my_func1(int x, int y){
	int temp = x+y;
	return &temp;   //---temp占用的空间会被操作系统回收
}

int value = 1314;

int *my_func2(){
	return &value;//可以返回全局变量的地址
}

int *my_func3(int x, int y){
	static int temp = 0;   //static修饰的变量不能用变量来初始化
	temp = x+y;            //因为他在main函数执行之前就已经分配空间了
	return &temp;//可以返回static修饰的局部变量的地址
}

int main(int argc, const char *argv[])
{
	int a = 10;
	int b = 20;
	int *p = NULL;
	//p = my_func1(a, b);
	//printf("*p = %d\n", *p);
	
	p = my_func2();
	printf("*p = %d\n", *p); //1314

	p = my_func3(a, b);
	printf("*p = %d\n", *p); //30

	return 0;
}

函数指针

本质是一个指针,指向一个函数。
格式:
返回值类型 (*函数指针名)(函数的形参表);
例:

#include <stdio.h>

void my_add(int x, int y){
    printf("%d\n", x+y);
}

int main(int argc, const char *argv[])
{
    int a = 10;
	int b = 20;
	//通过函数名可以调用函数
	my_add(a, b);

	//也可以定义函数指针指向该函数
	void (*p)(int, int) = NULL;
	p = my_add; //函数名就是函数的首地址
	//函数指针指向函数后 通过函数指针也可以调用函数
	p(a, b);

	return 0;
}

函数指针的典型使用场景----用作回调函数

#include <stdio.h>

int my_add(int x, int y){
    int temp = x+y;
    return temp;
}

int my_sub(int x, int y){
    int temp = x-y;
	return temp;
}

//在jisuan函数内部通过 函数指针p调用函数时,
//具体调用的是哪一个函数 取决于 用户调用 jisuan函数时,传递的第三个参数
//第三个参数是哪个函数 通过p调用的就是哪个函数
//相当于通过p去调用用户指定的函数  称之为  回调函数
int jisuan(int x, int y, int (*p)(int, int)){
    int temp = p(x, y);	
    return temp;
}

int main(int argc, const char *argv[])
{
	int a = 10;
	int b = 20;
	printf("%d\n", jisuan(a, b, my_add));//30
	printf("%d\n", jisuan(a, b, my_sub));//-10

	return 0;
}

linux系统信号处理函数就是通过回调函数来实现的。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void sig_hqyj(int x){
    printf("hello world\n");
}

int main(int argc, const char *argv[])
{
    //因为写linux操作的人当时并不知道信号产生后
    //用户自定义的行为是什么
    //所以预留了一个函数指针
    //用户将自己的逻辑封装在函数里 把函数名传给signal (函数名就是函数的首地址)
    //信号产生的时候 signal函数内部就通过函数指针去调用用户自己的函数了
	signal(SIGINT, sig_hqyj);  //---注册信号与信号处理函数的关系
	
	while(1){
		printf("hqyj\n");
		sleep(1);
	}

	return 0;
}

函数指针数组

本质是一个数组,数组中每个元素都是一个函数指针。
格式:
返回值类型 (*函数指针数组名[下标])(函数的形参表);
例如:

#include <stdio.h>

int my_add(int x, int y){
    return x+y;
}

int my_sub(int x, int y){
    return x-y;
}

int main(int argc, const char *argv[])
{
	//定义了一个函数指针数组 数组名叫 arr 数组中共有2个元素
	//每个元素都是一个能指向 返回值为 int  形参列表为(int, int)的函数的函数指针
	int (*arr[2])(int, int) = {NULL};
	arr[0] = my_add;
	arr[1] = my_sub;

	//初始化的写法
	//int (*arr[2])(int, int) = {my_add, my_sub};
	
	//通过函数指针数组 也可以调用函数
	printf("%d\n", arr[0](10, 20));//30
	printf("%d\n", arr[1](100, 200));//-100

	return 0;
}

函数指针数组指针

本质是一个指针,指向一个函数指针数组。
格式:
返回值类型 (*(*函数指针数组指针名))(函数的形参表);
例:

#include <stdio.h>

int my_add(int x, int y){
	return x+y;
}

int my_sub(int x, int y){
	return x-y;
}

int main(int argc, const char *argv[])
{
	//函数指针数组
	int (*arr[2])(int, int) = {my_add, my_sub};

	//函数指针数组指针
	int (*(*p))(int, int) = arr;

	//调用函数
	printf("%d\n", p[0](10, 20));//30
	printf("%d\n", (*(p+1))(10, 20));//-10

	return 0;
}

在这里插入图片描述

动态内存的分配和回收

由于栈区的内存空间都是由操作系统负责分配和回收的,
使用的过程中,不够灵活,空间的大小,和生命周期都不够灵活。
所以有些场景下,需要我们灵活操作内存的时候,就需要自己手动分配和回收了。
自己手动分配的内存是在堆区的。

分配 malloc

函数说明

功能:
    在堆区手动分配内存空间
头文件:
    #include <stdlib.h>
函数原型:
    void *malloc(size_t size);
参数:
    size  要分配的空间的大小 单位是 字节
返回值:
    成功:分配的空间的首地址
    失败:NULL

malloc 一般不会失败 只有虚拟内存不够的时候才有可能失败

回收 free

堆区自己分配的空间,不会主动被操作系统回收,而是需要我们自己在不再需要使用的时候
手动调用 free 函数来进行回收,所谓的回收,是回收使用权,
相当于告诉操作系统,这块空间我不再使用了,你可以把他分给别人了
如果自己的程序中只分配空间,而不释放,会造成内存泄漏的问题
内存泄漏就是指:只分配不回收,导致的系统内存资源越来越少的问题
内存泄漏只发生在长时间运行的服务器程序上,
因为进程结束时,操作系统会回收进程的所有资源

内存泄漏一旦出现就很难解决,所以要养成良好的代码习惯,分配时就想好何时回收。

函数说明

功能:
    回收malloc分配的空间
头文件:
    #include <stdlib.h>
函数原型:
    void free(void *ptr);
参数:
    ptr  分配的空间的首地址   //操作之前要保留首地址  不能把首地址弄丢了
返回值:
    无

例:

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

int main(int argc, const char *argv[])
{
	//malloc的返回值是 void *
	//我们实际使用时都把他显式强转成自己想要的类型
	//即使不写显式强转也没问题 因为会做隐式强转 且指针的强转是安全的
	//一般我们都写成显式强转 方便代码的阅读
	//int *p = (int *)malloc(4);
	int *p = (int *)malloc(sizeof(int));
	if(NULL == p){ //对返回值做检查 防止malloc失败导致的错误
		printf("内存分配失败..\n");    
		return -1;
	}
	//堆区的空间如果没有赋值之前 也是随机值
	printf("*p = %d\n", *p);  //随机值  

	//通过指针给分配的空间复制
	*p = 1314;
	//通过指针读取分配的空间里面的值
	printf("*p = %d\n", *p);//1314

	//回收空间 防止内存泄漏
	free(p);
	p = NULL; //防止野指针---指针指向的那块空间回收了 但是指针还保存着它的首地址 
    			//所以此处需要置NULL
	return 0;
}

typedef

tyepdef的功能:给类型起别名
如:
typedef int hqyj; //给int起个别名叫 hqyj
//后面使用int定义变量和使用 hqyj定义变量是一样的
int a = 10;
hqyj b = 10;

为什么要给类型起别名?
1.有些类型名字比较长,写起来不方便,如 unsigned long long int
这种情况就可以使用typedef起个别名
typedef unsigned long long int hqyj;
后面使用hqyj定义变量和使用 unsigned long long int定义变量是一样的
2.我们后面会学到枚举、结构体、共用体,这些类型名字都有两个单词组成
配合着typedef使用起来会更方便。

关于C语言中的类型名
定义变量的语句

int a;
int *a;
int **a;
int a[5];
int a[3][4];
int *a[5];   指针数组
int (*a)[5];   数组指针
int (*a)(int);  函数指针
int (*a[5])(int);  函数指针数组

把定义变量的语句中的变量名去掉,剩下的就是类型名:

int ;
int *;
int **;
int [5];
int [3][4];
int *[5];
int (*)[5];
int (*)(int);
int (*[5])(int);

在定义变量的语句前加上typedef 原来的变量名 就变成了新的类型名:

typedef int a;
typedef int *a;
typedef int **a;
typedef int a[5];
typedef int a[3][4];
typedef int *a[5];
typedef int (*a)[5];
typedef int (*a)(int);
typedef int (*a[5])(int);

笔试面试题:
typedef和宏定义有什么区别?
1.typedef必须加分号 define不强制要求
2.宏定义只是一个简单的无脑替换,而typedef是给类型起别名

#include <stdio.h>

typedef int * hqyj1;
#define hqyj2 int *

int main(){
    int a = 100;
    int b = 200;
    //这种写法 p1 和 p2 都是指针
    hqyj1 p1, p2;
    p1 = &a;
    p2 = &b;
    
    //下面的写法 在预处理阶段会被替换成
    // int *q1, q2;
    // 这种写法 q1 是指针 q2就是一个普通的int变量
    hqyj2 q1, q2;
    q1 = &a;
    q2 = b;

    return 0;
}

枚举-enum

概念

枚举是一个基本类型,枚举就是数据的有限罗列。
枚举是用来防止“魔鬼数字”的

定义枚举的格式

enum 枚举类型名{
    成员1,
    成员2,
    ...
    成员n
};

注意事项:

  1. enum和枚举类型名 共同构成一个类型,定义变量时: enum 枚举类型名 变量名;
  2. 枚举的成员之间用 逗号 分隔
  3. 如果枚举的成员1没有被赋值,默认值是0
  4. 枚举的成员的值是依次递增的,每次递增1
  5. 如果某个成员被赋了初始值,那么后面的成员在该值的基础上依次递增1
  6. 枚举类型的大小,取决于枚举中最大元素的值,一般情况下都是4字节的
  7. 枚举的成员一旦定义好之后 就都是常量
  8. 当局部变量的名字和枚举成员的名字冲突时,采用局部优先原则

定义枚举变量的格式

1.先定义枚举类型,再定义枚举变量
enum Gender{
    boy, girl
};
enum Gender c1,c2;

2.定义枚举类型的同时,定义枚举变量
enum Gender{
    boy, girl
}g1, g2;

3.省略枚举类型名的定义变量的方式
注意:这种写法 就不能再定义其他的变量了
enum{
    boy, girl
}g1, g2;

枚举和typedef结合

方式1typedef enum Gender{
    goy, girl
}hqyj; //hqyj 就是 enum Gender 的别名
enum Gender g1;
hqyj g2;

方式2typedef enum{
    goy, girl
}hqyj;  //这种写法就只能使用 hqyj 来定义变量了
hqyj g1, g2;

结构体

概念

结构体是一个构造类型,
结构体里面既可以是相同的数据类型的集合
也可以是不同的数据类型的集合
一般情况下,结构体多用于管理一组不同数据类型的数据

定义结构体的格式

struct 结构体类型名{
    数据类型1 成员1;
    数据类型2 成员2;
    ...
    数据类型n 成员n;
};

注意:
1.结构体的使用方式和枚举类似,又略有不同
2.结构体成员之间用 分号 分隔
3.结构体的成员都是变量----可以被重新赋值
4.结构体的成员在内存上都是连续的(涉及到内存对齐的问题)
5.结构体变量之间可以直接相互赋值

定义结构体变量

变量:
struct 结构体类型名 结构体变量名;
指针: 结构体指针占8个字节 64位系统也是8个
struct 结构体类型名 *结构体指针名;

结构体访问成员

struct Test{
    int a;
    char b;  
};
变量版:
    变量名.成员名
    struct Test t1;
    t1.a = 100;
    t1.b = 120;
指针版:
    指针名->成员名
    struct Test *p1;
    p1 = &t1;//既可以指向结构体变量
    //也可以直接malloc在堆区分配
    struct Test *p2 = (struct Test *)malloc(sizeof(struct Test));
    p2->a = 100;
    p2->b = 120;
    //对p1的操作同理

例:

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

//定义结构体类型
struct Test{
    int a;
    char b;
};

int main(){
    //每个结构体变量都有自己独立的内存空间
    //都有自己独立的成员  t1.a 和 t2.a 是不同的 
    struct Test t1;
    struct Test t2;
    //结构体变量访问成员  用.的方式
    t1.a = 10;
    t1.b = 20;
    t2.a = 30;
    t2.b = 40;
    printf("t1.a = %d  t1.b = %d\n", t1.a, t1.b);//10 20
    printf("t2.a = %d  t2.b = %d\n", t2.a, t2.b);//30 40

    //结构体指针访问成员  ->
    //使用指针的三个步骤:定义指针、明确指针的指向(指向已经分配的空间)、解引用???
    // struct Test *p1;  //野指针
    // p1->a=100;  //错误的 对野指针不能做取成员的操作
    struct Test *p1 = &t1;
    struct Test *p2 = (struct Test *)malloc(sizeof(struct Test));
    p2->a = 50;
    p2->b = 60;
    printf("p1->a = %d  p1->b = %d\n", p1->a, p1->b);//10 20
    printf("p2->a = %d  p2->b = %d\n", p2->a, p2->b);//50 60
    free(p2);
    p2 = NULL;//防止野指针

    return 0;
}

结构体数组的定义方式:
struct 结构体类型名 数组名[下标];
给内存空间清0
memset(首地址,0, 长度);

练习 :使用结构体实现学生管理系统

结构体

结构体变量的赋值和初始化的方式–了解

方式1struct Test{
    int a;
    char b[32];
    char c;
};
struct Test t1;
t1.a = 100;
strcpy(t1.b, "hello");
t1.c = 50;

方式2:
struct Test{
    int a;
    char b[32];
    char c;
};
struct Test t1 = {10, "xiaoming", 20};

方式3:
struct Test{
    int a;
    char b[32];
    char c;
}t1; //定义类型的同时定义变量
t1.a = 100;
strcpy(t1.b, "hello");
t1.c = 50;

方式4:
struct Test{
    int a;
    char b[32];
    char c;
}t1 = {10, "xiaoming", 20}; //定义类型的同时定义变量 并初始化

方式5:
struct Test{
    int a;
    char b[32];
    char c;
};
struct Test t1 = {
    .a = 100, 
    .b = "hello"
}; //部分初始化
struct Test t1 = {
    .a = 100, 
    .c = 30
}; //部分初始化

方式6:
struct Test{
    int a;
    char b[32];
    char c;
};
struct Test t1;

结构体数组的初始化和赋值–了解

方式1struct Test{
    int a;
    char b[32];
    char c;
};
struct Test s[2];
s[0].a = 10;
strcpy(s[0].b, "hello");
s[0].c = 20;
s[1].a = 30;
strcpy(s[1].b, "beijing");
s[1].c = 40;

方式2struct Test{
    int a;
    char b[32];
    char c;
};
struct Test s[2] = {
    {10, "zhangsan", 20},
    {30, "lisi", 40}
};

方式3struct Test{
    int a;
    char b[32];
    char c;
};
struct Test s[3] = {
    [0] = {10, "zhangsan", 20},
    [2] = {30, "lisi", 40}
};

方式4struct Test{
    int a;
    char b[32];
    char c;
};
struct Test s[3] = {
    [0] = {.a = 10, .b = "zhangsan"},
    [2] = {.a = 30, .c = 40}
};

结构体清零

使用 memset 即可
#include <string.h>
void *memset(void *s, int c, size_t n);
功能:从指针s指向的地址开始 向后填充 n 个字节的 c表示字符

struct Test{
    int a;
    char b[32];
    char c;
};
//变量清零
struct Test t1;
memset(&t1, 0, sizeof(t1));
//指针清零
struct Test *p1 = (struct Test *)malloc(sizeof(struct Test));
memset(p1, 0, sizeof(struct Test));

//注意前面不要写成下面的写法
//下面的写法有两处错误
// 1. &p1 表示的指针自身占用的内存空间的首地址
// 2. sizeof(p1) 就是一个指针大小  是固定值
memset(&p1, 0, sizeof(p1));

C语言中结构体中不能定义函数

C++中,结构体里面允许定义函数
C语言中,结构体里面是不允许定义函数的,但是可以有函数指针

#include <stdio.h>

int my_add(int x, int y){
    return x+y;
}

int my_sub(int x, int y){
    return x-y;
}

struct Test{
    int a;
    int (*p)(int, int);//函数指针
};

int main(int argc, const char *argv[])
{
    struct Test t1 = {.p = my_add};
    struct Test t2 = {.p = my_sub};
    printf("%d\n", t1.p(10, 20));//30
    printf("%d\n", t2.p(10, 20));//-10

    return 0;
}

结构体对齐

-----------------------64位系统-----------------------
在64位系统中,就是按照最大的成员进行对齐
特殊:long double —> 32位系统 12字节 64位系统 16字节

struct Test{
    char a;
    long double b;
};//32

-----------------------32位系统-----------------------
64位系统将程序按32位编译,要加编译选项 -m32
对齐规则:
1.如果成员中都是小于4字节的,则按照最大的成员对齐
2.如果有成员大于等于4字节,则都按照4字节对齐
3.要特别注意char和short连续存储的问题!!!
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
结构体中包含结构体时的对齐规则:

struct A{
    int a;
    char b;
}; //8

struct B{
    struct A m;
    char n;
    int k;
};//16  //因为结构体A中是按照4字节对齐的,所以结构体A定义的变量都是8字节的
        //当将结构体A嵌套在结构体B中的时候,由于为了满足A自身的对齐 多给A分了三个字节
        //这三个字节 B中的成员是不能占用的

struct C{
    short a;
    short b;
    short c;  
};  //6

struct D{
    struct C m;
    char n;
    int k;
};//12   //因为结构体C中是按照2字节对齐的, 所以结构体C定义的变量都是6字节的
        //当将结构体C嵌套在结构体D中的时候,并没有为了满足C自身的对齐而额外给C分配空间
        //而是因为结构体D中是4字节对齐的, 相当于将C放在D中的时候,为了满足D的对齐
        //D给C额外分了两个字节,这两个字节D中的成员是可以占用的

结构体位域

结构体位域是压缩结构体的一种手段。

#include <stdio.h>

//结构体位域的用法
struct Led{
    unsigned char led0:1;   //本来unsigned char 应该占用8bit
    unsigned char led1:1;   //使用 :1 的方式指定 每个成员只占1个bit位
    unsigned char led2:1;
    unsigned char led3:1;
    unsigned char led4:1;
    unsigned char led5:1;
    unsigned char led6:1;
    unsigned char led7:1;
};

int main(int argc, const char *argv[])
{
    printf("%ld\n", sizeof(struct Led)); //1
    struct Led my_led;  //1字节
    my_led.led0 = 1;
    my_led.led1 = 0;
    printf("led0 = %d\n", my_led.led0);
    printf("led1 = %d\n", my_led.led1);

    //注意 结构体压缩后 每个成员占用的bit少了 能存储的数据范围也随之变小了
    //my_led.led2 = 2; //溢出了
    //printf("led2 = %d\n", my_led.led2); //  10  1溢出  结果是0

    return 0;
}

共用体(联合体)–union

1.定义共用体的格式以及共用体访问成员的方式,
和结构体的用法一模一样 只不过是将 struct 改成 union 即可

2.共用体中所有的成员共用同一块内存空间

3.共用体的所有成员首地址都是一样的

4.共用体的大小 取决成员中最大的那个

定义共用体的格式

union Test{
    数据类型1 成员1;
    数据类型2 成员2;
    ...
    数据类型n 成员n; 
};

例:

#include <stdio.h>
#include <string.h>

union Test{
        char a;
        short b;
        int c;
};

int main(int argc, const char *argv[])
{
        printf("sizeof(union Test) = %ld\n", sizeof(union Test)); //4

        union Test t1;
        memset(&t1, 0, sizeof(t1));
        t1.a = 123;
        //由于共用体的所有成员是共用同一块内存空间的 所以
        //修改其中一个成员  其他成员的值 也会随之发生变化
        //所以使用共用体时要谨慎,他是不安全的
        printf("t1.c = %d\n", t1.c);//123

        //一样的
        printf("&t1.a = %p\n", &t1.a);
        printf("&t1.b = %p\n", &t1.b);
        printf("&t1.c = %p\n", &t1.c);

        return 0;
}

请使用共用体,判断你使用的主机是大端存储还是小端存储?

#include <stdio.h>

union Test{
    char a;
    int b;
};

int main(int argc, const char *argv[])
{
    union Test t;
    t.b = 0x12345678;
    if(0x78 == t.a){
            printf("小端\n");
    }else if(0x12 == t.a){
            printf("大端\n");
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值