C语言的概述
TIOBE 编程社区指数(The TIOBE Programming Community index)是编程语言流行度的指标,该榜单每月更新一次,指数基于全球技术工程师、课程和第三方供应商的数量。包括流行的搜索引擎,如谷歌、必应、维基百科、亚马逊 和百度都用于指数计算。
TIOBE 指数并不代表语言的好坏,开发者可以使用该榜单检查自身的编程技能是否需要更新,或者在开始构建新软件时对某一语言做出选择的一个重要参考。
C语言被称呼为:“C生万物,编程之本”
与C语言相关的语言很多。
其中最早的一门语言叫 Algol 60,是 1960 年产生的,它是真正的第一门面向问题的语言。
1963 年剑桥大学在 Algol 60 的基础上研发出了 CPL。
1967 年剑桥大学的Martin Richards对 CPL 进行了简化,产生了 BCPL。
1970 年,美国 AT&T公司【美国电话电报公司(American Telephone and Telegraph Company)】所属贝尔实验室(AT&T Bell Laboratory)的研究员Ken Thompson以 BCPL 为基础,设计出了很简单而且很接近硬件的B语言(取 BCPL 的首字母)。
1971 年,贝尔实验室的Dennis Ritchie加入了Ken Thompson的开发项目,合作开发 UNIX。
UNIX 系统是世界上第一个真正的操作系统。
1972 年,Dennis Ritchie在B语言的基础上最终设计出了一种新的语言,C语言。
1973 年年初,C语言的主体完成。Ken Thompson和Dennis Ritchie开始用C语言完全重写 UNIX,这就是 UNIX 第 5 版。
由于 UNIX 操作系统是用C语言编写的,而这个系统很流行,于是C语言也跟着流行起来。
随后又出现了 C++。
C++ 是Bjarne Stroustrup编写的, 他也来自贝尔实验室,是C语言创始人Dennis Ritchie的下属。
后来 Sun 公司又对 C++ 进行改写,产生了 Java。
而微软公司发现 Java 很流行,就造出了一个类似的语言——C#
要学习 C++、Java 或者 C# 的话,那么C语言就最好要学一下!
Win上搭建C语言的学习环境
MinGW-W64下载网页:https://www.mingw-w64.org/
下载的两个选择,压缩版和在线exe安装版
下面对几个选项给出说明
-
x86_64:表示MinGW-W64要在64为的操作系统上运行
-
i686:表示MinGW-W64要在32位的操作系统上运行
-
posix:表示MinGW-W64在非windows系统上运行
-
win32:表示MinGW-W64在windows系统上运行
-
sjlj:表示MinGW-W64编译生成的程序可以在32位和64位系统上运行
-
dwarf:表示MinGW-W64编译生成的程序只能在32位系统上运行
-
seh:表示MinGW-W64编译生成的程序只能在64位系统上运行
下载压缩包的话,选择合适位置解压,将mingw64/bin加入环境变量即可
MinGW系列只提供了名字为 mingw32-make.exe 的执行文件,
事实上,该.exe 和make.exe 功能一样,为了make执行时能找到该文件,
建议复制 mingw32-make.exe 一份,并将复制文件命名为’make.exe’,先做在这里,以后用的时候用。-Makefile编译用如(make 回车)
验证是否成功配置
记事本上写第一个C语言的程序:hello.c
/*
多行
注解:给人看的,计算机执行程序的时候,看不见
*/
//单行注解,这里写的程序是第一个C程序,主要目的,感受一下C语言程序怎么写
//通过这个程序,了解GCC编译器内部通过哪些细节步骤,把C源程序编程操作系统可以执行的二进制程序的
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
printf("hello world!");
exit(0);
}
用编译器gcc,从一个C语言的源文件到执行,要经历的过程:
1、C源文件
2、预处理 gcc -E hello.c > hello.i , 凡是’#'开头的内容都是在预处理阶段进行处理,hello.i就是预处理后的结果文件。
3、编译 gcc -S hello.i , 默认会生成编译的结果文件hello.s , 做的事情其实就是生成汇编语言。
4、汇编 gcc -c hello.s , 默认会生成汇编语言的汇编成目标文件hello.o , 这个时候hello.o就已经是一个二进制文件。
5、链接 gcc hello.o -o hello ,就直接生成可执行文件了。
6、可执行文件 hello 就可以执行当前目标下的hello了。
上面这样做很麻烦,只是为了让大家了解gcc编程c语言内部要经历的过程!
直接gcc hello.c 或者 gcc hello.c -o hello 或者 gcc -g hello.c -o hello 就可以了,
gcc会自动完成上面我们讲的所有步骤!
加上-g 选项,会保留代码的文字信息,便于后面的调试。
编辑工具
下载安装vscode:
https://visualstudio.microsoft.com/zh-hans/
设置中文:
命令面板【Ctrl+Shift+P】–config 然后搜索出来的【Configure Display Language】
输入Chinese,然后选择【Chinese (Simplified)Language Pack for Visual Studio Code】,然后点击右侧的【Install】
安装好中文语言包之后软件会提示重启VSCode,点击【Yes】重启VSCode软件
安装插件
- Code Runner:右键即可编译运行单文件,很方便;但无法Dubug
- C/C++:又名 cpptools,提供Debug和Format功能
载入写好的hello.c文件
打开vscode载入刚刚的c文件,网上网友说编译c文件要打开文件所在的文件夹,我尝试了下,确实是这样的。如下图:
然后点运行–启动调试,他会跳出一个launch.json的一个配置文件:是要修改的
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "C/C++ Runner: Debug Session",
"type": "cppdbg",
"request": "launch",
"args": [],
"stopAtEntry": false,
"externalConsole": true,
"cwd": "c:/Users/Administrator/Desktop/笔记/AnLi",
"program": "c:/Users/Administrator/Desktop/笔记/AnLi/build/Debug/outDebug",
"MIMode": "gdb",
"miDebuggerPath": "gdb",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
//修改成,gdb调试
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) 启动",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceRoot}/${fileBasenameNoExtension}.exe",
"args": [],
"stopAtEntry":false,
"cwd": "${workspaceRoot}",
"environment":[],
"externalConsole":false,
"MIMode":"gdb",
"miDebuggerPath":"c:\\mingw64\\bin\\gdb.exe",
"preLaunchTask":"gcc",
"setupCommands":[
{
"description":"为gdb启用整齐打印",
"text":"-enable-pretty-printing",
"ignoreFailures":true
}
]
}
]
}
program,指明了需要运行的文件,${}的格式是表明这是变量。
miDebuggerPath,是我们安装gdb的路径,gdb工具是用来调试的二进制文件(可执行二进制文件也叫命令)。
preLauchTask,是一个重点,我们这个配置的意思是用gdb去调试 program参数指定的二进制文件,
但如果没有这个文件怎么办,我们可以运行gcc编译工具生成二进制*.exe文件,preLaunchTask就是干这个的!
tasks.json,下面是一个任务时的配置,多个任务用[]括起来
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"command":"gcc",
"args": ["-g","${file}","-o","${fileBasenameNoExtension}.exe"]
//gcc -g hello.c -o hello.exe
}
用CMD
命令面板(Ctrl+Shift+P)中,输入select选择第一条:
之后选择目标Shell
Linux+Win10上搭建C语言的学习环境**
1)在VMware上安装Ubuntu20.04操作系统
下载
可以到百度搜索ubuntu 再进入官网或https://mirrors.tuna.tsinghua.edu.cn/ubuntu-releases/
,找到一个合适的下载,最新版本是22.x版,安装时选桌面安装,这样好调试c语言开发。
安装ubuntu20.04时,最好让虚拟机断网安装,这样就可以节省下Language pack download的时间,本地网速不太理想的情况下,download会是一个相当漫长的过程。
设置root密码,安装vm-tools设置CDROM为自动检测,换源
sudo passwd root //设置root用户密码,先输入当前用户密码,再输两次新密码---我一般设密码为:xiong
su - root //进入root用户
下图是安装vm-tools,安装它的好处是,可以复制贴贴到虚拟机以外的系统中
第三方软件连接linux
putty连接Ubuntu20
Putty连接Ubuntu出现 connection refused 以及 access denied 解决方案
1.无法连接Ubuntu
即PuTTY进不去,报错:connection refused
这个时候是因为ssh服务没有开,那么你需要先检查ssh服务是否启用,如果没有的话,请输入以下指令:
sudo apt install openssh-server
安装完成之后再次用PuTTY连接,这时候已经可以进入了
2.putty 连接ubuntu20 root Access denied
报错解释:
这个错误表明你正在使用PuTTY尝试以root用户身份连接到一个Ubuntu 20.04系统,但是访问被拒绝。通常,Ubuntu默认情况下不允许root用户通过SSH远程登录,出于安全考虑。
解决方法:
如果你有物理访问权限,可以直接在Ubuntu系统上以root用户登录。
如果你想通过SSH以root身份登录,可以修改SSH配置来允许root登录。这样做需要编辑/etc/ssh/sshd_config文件,将PermitRootLogin的值改为yes。
sudo apt-get install vim
sudo vim /etc/ssh/sshd_config
修改或添加以下行:
PermitRootLogin yes
ESC+:wq 保存文件并退出编辑器。然后重启SSH服务以应用更改:
sudo systemctl restart ssh
之后,你应该能够使用PuTTY以root用户连接到Ubuntu系统。
注意:出于安全考虑,强烈建议不要在生产环境中允许root用户通过SSH登录。
考虑使用其他安全措施,如SSH密钥认证或使用su/sudo来执行管理任务。
SecureCRT连接Ubuntu
首先:查询ip地址ifconfig,如果没有此命令,则要安装net-tools:sudo apt install net-tools
SecureCRT出现Key exchange failed.No compatible key exchange method,解决办法
//修改服务器:vim /etc/ssh/sshd_config
文件尾部增加:
HostKeyAlgorithms +ssh-rsa
PubkeyAcceptedKeyTypes +ssh-rsa
//下面内容为CRT弹出内容加进尾部即可
KexAlgorithms curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1
//ESC+:wq 保存退出。
systemctl restart sshd.service
下面是换源,先备份本地的源配置
//下面是换源,先备份本地的源配置
cp -v /etc/apt/sources.list /etc/apt/sources.list.backup
chmod 777 /etc/apt/sources.list
vi /etc/apt/sources.list
//阿里云的源:按DD,删除所有sources.list中的内容,复制以下内容放到sources.list中
deb http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ focal-backports main restricted universe multiverse
//选按esc,再按:wq,修改后:
apt update
apt upgrade //升级
安装C语言编译器,首先进和root用户中
sudo - root
apt-get install gcc
直接建立共享文件夹
2)建立Windows和Ubuntu20.04的共享文件夹或者搭建samba服务
一,设置两系统的共享文件夹(我是桌面的workspacec).
进入共享文件夹,编译执行一个c文件测试一下效果
# vmware-hgfsclient //查看当前共享目录
workspaceC
#cd /mnt
#mkdir hfgs
#vmhgfs-fuse .host:/ /mnt/hgs -o nonempty -o allow_other
//设置自动挂载
#vim /etc/fstab
.host:/ /mnt/hgfs fuse.vmhgfs-fuse allow_other,defaults 0 0
#mount -a //让配置生效
//进入共享文件夹,测试一下
#cd/hgfs
#cd workspaceC
#gcc -g hello.c -o myhello //编译hello.c,这里的可执行文件不是.exe,而没有扩展名的
#ls
hello.c hello.exe myhello
#./myhello //执和可执行文件
Hello world! //显示效果,这里因为printf()中没有加\n换行符,所以与文件目录显示在一行。
Centos7.x中进入共享文件夹,比unbuntu要麻烦一些,所以很多人用ubuntu来做共享测试
# vmware-hgfsclient //查看共享文件夹情况
workspaceC
//在 /mnt 下创建一个目录,用于挂载共享文件夹,创建hgfs目录
#mkdir /mnt/hgfs
//查看目录的创建情况
#ls -l /mnt
# mount -t vmhgfs .host:/workspaceC /mnt/workspaceC //持载共享目录
//如果内核版本是4.0及以后可以执行:挂载,我用的是以下这个命令
# vmhgfs-fuse .host:/ /mnt/hgfs -o subtype=vmhgfs-fuse,allow_other
//自动挂载共享目录 对于 kernel 4.0 之前的版本:
vim /etc/fstab
.host:/mywinshare /mnt/mylinuxshare vmhgfs defaults 0 0
//貌似旧版内核下共享文件夹会自动mount到 /mnt/hgfs 目录下,所以也不需要增加 fstab 项。对于 kernel 4.0 及之后的版本:
.host:/ /mnt/hgfs fuse.vmhgfs-fuse allow_other,defaults 0 0
#mount -a //使自动挂载生效
//如果出现if you are sure this is safe, use the 'nonempty' mount option,则
#vmhgfs-fuse .host:/ /mnt/hgfs -o nonempty -o allow_other
//如果后续不想使用共享文件,可以通过如下命令卸载共享目录
# umount /mnt/workspaceC
安装samba服务器进行共享
ubuntn上安装
sudo apt install -y samba samba-common //有的时候发现E安装不上,更新一下源sudo apt-get update
安装成功后,需要修改一下samba的配置
sudo cp /etc/samba/smb.conf /etc/samba/smb_bk.conf //备份配置文件
sudo vim /etc/samba/smb.conf //真正修改之
#后面加:
[share]
comment = share
path = /home/zhonghao
browseable = yes
writable = yes
read = yes
directory mask = 0775
create mask = 0775
#修改配置后重启服务
sudo service smbd restart
#最后给samba填加一个用户
sudo smbpasswd -a zhonghao //该用户是我系统时设置的,也可以把它做samba的用户登陆账号
centos 7上安装极samba服务器
//sudo yum -y install samba
yum install samba samba-client samba-common -y
sudo systemctl start smb nmb //启动Samba应用
sudo systemctl enable smb nmb //查看状态
ps -ef | grep -E 'smb|nmb' //查看进程
[root@Linuxidc-Server ~]# ps -ef | grep -E 'smb|nmb'
root 9885 1 0 14:48 ? 00:00:00 /usr/sbin/nmbd
root 9887 1 0 14:48 ? 00:00:00 /usr/sbin/smbd
root 9888 9887 0 14:48 ? 00:00:00 /usr/sbin/smbd
root 9889 9887 0 14:48 ? 00:00:00 /usr/sbin/smbd
root 9890 9887 0 14:48 ? 00:00:00 /usr/sbin/smbd
root 9959 9627 0 15:02 pts/0 00:00:00 grep --color=auto -E smb|nmb
//查看Samba应用服务端口,smbd应用进程主要监听139和445端口, nmbd应用进程主要监听137与138端口。
netstat -tunlp | grep -E 'smbd|nmbd'
cat /etc/samba/smb.conf //查看Samba配置文件
systemctl restart smb nmb //重启服务
systemctl status smb.service //查看服务状态
//创建服务器用户,本人直接用root用户了
useradd g
smbpasswd -a g //设置用户密码,本人:123
//他建共享目录(win与linux)
su samba
mkdir share //默认在 /home/samba/目录中创建了share文件夹
chmod 777 -R share //为共享文件夹设置全权限
//修改配置文件
su - root
cp /etc/samba/smb.conf /etc/samba/smb.conf.back //备份samba的配置文件
vi /etc/samba/smb.conf //使用vi编辑器配置文件,在配置文件修改如下:
[global]
workgroup = WORKGROUP
security = user
passdb backend = tdbsam
[homes]
comment = Home Directories
valid users = %S, %D%w%S
browseable = No
read only = No
inherit acls = Yes
[printers]
comment = All Printers
path = /var/tmp
printable = Yes
create mask = 0600
browseable = No
[print$]
comment = Printer Drivers
path = /var/lib/samba/drivers
write list = @printadmin root
force group = @printadmin
create mask = 0664
directory mask = 0775
[share]
path = /home/samba/share
available = yes
browseable = yes
public = yes
writable = yes
create mask = 0775
directory mask = 0775
valid users = g
ESC:wq 回车保存
//关闭防火墙,禁用SELinux
systemctl disable firewalld
vim /etc/selinux/config
修改为:SELINUX=disabled
//重启电脑,再重启smb服务
systemctl start smb
win连接远程samba服务器,到目前为止,云服务器没有成功过
windows 7中
windows10中
或按win键+R键,再输入如下图所示
**若点击share文件夹,则第一次会弹出对话框,输入账号:zhonghao,密码: xiong,若进不了,则有可能是防炎啬打了,centos7上关闭防火墙 systemctl stop firewalld,Ubuntu上关闭防火墙 **
//关闭 【ubuntu】防火墙 : sudo ufw status 可以查看当前 的防火墙的状态
sudo systemctl disable ufw.service 禁用防火墙
进制、数据类型、常量、变量、运算符、表达式、语句和程序
1、进制
计算机底层保存和处理的数据都是二进制数
数据在计算机中的表示,最终以二进制的形式存在 , 就是各种 <黑客帝国>电影中那些 0101010… 的数字 ;
如果我们直接操作二进制的话 , 面对这么长的数进行思考或操作,没有人会喜欢。
用16进制或8进制可以解决这个问题。因为,进制越大,数的表达长度也就越短。
之所以使用16或8进制,而不其它的,诸如9或20进制,
是因为8、16,分别是2的3次方、4次方,便于和二进制转换 。
10进制
我们最熟悉的10进制 , 用 0~9 的数表示 , 逢10进1
16进制
如果是 16 进制 , 它就是 由 0-9,A-F组成, 与10进制的对应关系是:0-9 对应 0-9;A-F对应10-15,字母不区分大小写,逢16进1。
2进制 和 8进制
2进制 由 0-1组成,逢2进1。
8进制 由 0-7组成 ,逢8进1。
二进制与十进制之间的转换
十进制转二进制
方法为:十进制数除2取余法,即十进制数除2,余数为权位上的数,得到的商值继续除2,依此步骤继续向下运算直到商为0为止。
(具体用法如下图)
二进制转十进制
方法为:把二进制数按权展开、相加即得十进制数。
(具体用法如下图)
二进制与十六进制之间的转换
二进制转十六进制
方法为:十六进制是取四合一。注意事项,4位二进制转成十六进制是从右到左开始转换,不足时补0。
(具体用法如下图)
注意:java中的十六进制的数表示方法为:0x或0X的开头,比如0x12C67
十六进制转二进制
方法为:十六进制数通过除2取余法,得到二进制数,对每个十六进制为4个二进制,不足时在最左边补零。
(具体用法如下图)
**补码:**在计算机底层数据是使用补码的形式来存放和运算的。
一个正数的补码和它的原码的形式是相同的。
负数的补码形式:除第一位为1外,其他各位取反加1
所以补码的设计目的是:
⑴使符号位能与有效值部分一起参加运算,从而简化运算规则.
⑵使减法运算转换为加法运算,进一步简化计算机中运算器的线路设计,所有这些转换都是在计算机的最底层进行运算的时候使用,而在我们使用的汇编、C、java等其他高级语言中使用的都是原码。
补码在计算机底层中的运算例子:
负数的补码就是除符号位外,取反加1,而正数不变,正数的原码反码补码是一样的.
下面是补码的运算:
( 1 )- ( 1 )= ( 1 )+ ( -1 )
=(00000001)补+ (11111111)补
= (00000000)补= ( 0 )正确
( 1 )- ( 2)= ( 1 )+ ( -2 )
= (00000001)补+ (11111110)补
= (11111111)补= ( -1 ) 正确
(-1) = (10000001)原码=(11111110 )反码 =((11111110 )+ 1)补码
C语言中数据类型
在这里,我们主要任务是了解基本类型,后面的构造类型、指针有专门的章节具体讲解:
short、int、long、char、float、double …关键字代表C 语言里的基本数据类型,数据类型可以看作是“模子”
什么是“模子”? 举个例子:
大家应该见过蜂窝煤:
做蜂窝煤的这个东西叫藕煤器,拿着它在和好的煤堆里这么一咔,一个蜂窝煤出来了。
一般的半径12cm,12 个孔。不同型号的藕煤器咔出来的蜂窝煤大小不一样,孔数也不一样。
这个藕煤器其实就是个模子。
现在我们联想一下,short、int、long、char、float、double 这六个东东其实就很像不同类型的藕煤器!
拿着它们在内存上咔咔咔,咔出来的不是蜂窝煤,而是不同大小的内存空间,
这些内存空间如果需要反复的在程序中获取来用,可以给它关联一个名字。
这个名字就是我们后面要介绍的常量或变量的名字。
内存空间或者磁盘空间里,都是存放数据用的,描述在这些设备里的数据的存储单位:
位bit:是计算机中最小的储存单位
字节byte:一个byte是由8个bit组成,它是最小的可寻址单元,B也表示字节,它是byte的简写
1KB (Kilobyte 千字节)=1024B;
1MB (Megabyte 兆字节 简称“兆”)=1024KB;
1GB (Gigabyte 吉字节 又称“千兆”)=1024MB;
1TB (Trillionbyte 万亿字节 太字节)=1024GB;
1PB(Petabyte 千万亿字节 拍字节)=1024TB;
1EB(Exabyte,百亿亿字节,艾字节)=1024PB;
1ZB(Zettabyte,十万亿亿字节,泽字节)= 1024EB;
1YB(Yottabyte,一亿亿亿字节,尧字节)= 1024ZB;
1BB(Brontobyte,一千亿亿亿字节)= 1024YB;
注意:不同的系统平台数据类型所占空间大小可能会有所不同,具体平台可以用sizeof关键字测试一下:
一般情况下,Windows系统是:
short 咔出来的内存大小是2 个byte;
int 咔出来的内存大小是4 个byte;
long 咔出来的内存大小是4 个byte;
float 咔出来的内存大小是4 个byte;
double 咔出来的内存大小是8 个byte;
char 咔出来的内存大小是1 个byte。
printf("%d",sizeof(int)); //4
变量和常量
字符常量要用单引号括起来,例如上面的’A’,注意单引号只能括一个字符而不能像双引号那样括一串字符,
字符常量也可以是一个转义序列,例如’\n’,这时虽然单引号括了两个字符,但实际上只表示一个字符。
其他的一些转义符号:
符号常量
在C语言中,可以用一个标识符来表示一个常量,称之为符号常量。
符号常量在使用之前必须先定义,其一般形式为:
#define 标识符 常量
其中#define是一条预处理命令(预处理命令都以"#"开头),称为宏定义命令,宏这块后面慢慢理解。
一经定义,以后在程序中所有出现该标识符的地方均代之以该常量值。
习惯上符号常量的标识符用大写字母,变量标识符用小写字母,以示区别。
#include <stdio.h>
#include <stdlib.h>
#define PRICE 30
int main(void){
printf("Hello world!\n");
printf("%o \n",100);
printf("%d\n",sizeof(int));
printf("%d\n",PRICE);
printf("价格常量值= %d\n",PRICE);
printf("八进制= %o\n",PRICE);
printf("十六进制= %x\n",PRICE);
exit(0);
}
//结果
Hello world!
144
4
30
价格常量值= 30
八进制= 36
十六进制= 1e
++和移位运算符
#include <stdio.h>
#include <stdlib.h>
int main(void){
int a = 10;
//a++;
printf("%d\n",a++); //10,先用,再运算
printf("%d\n",++a); //12,先运算,再用
printf("%d\n",a); //12
//逻辑运算符,返回布尔值,返1表示true,返0表示false
printf("%d\n",12>2); //1
//移位运算
int c = 8,b=1;
c=c<<2;
b=b>>1;
printf("%d %d\n",c,b); //4 0
exit(0);
}
其它位运算符
运算符意义示例对于每个位位置的结果(1=设定,0=清除)
<<向左移位x<<yx 的每个位向左移动 y 个位,相当于乘以2的n次方 >>向右移位x>>yx 的每个位向右移动 y 个位,相当于除以2的n次方
& 位 AND x&y 如果 x 和 y 都为 1,则得到 1;如果 x 或 y 任何一个为 0,或都为0,则得到 0
| 位 OR x|y 如果 x 或 y 为 1,或都为 1,则得到 1;如果 x 和 y 都为 0,则得到 0
^ 位 XOR x^y 如果 x 或 y 的值不同,则得到 1;如果两个值相同,则得到 0
~ 位 NOT(I的补码) ~x 如果 x 为 0,则得到 1,如果 x 是 1,则得到 0
注意:位运算符的操作数必须是整数类型。
在了解位运算符的基本规则下,要求进步掌握:
1)将某个整数num 的p个位置的二进制位变成1:num | (1<<p);
2)将某个整数num 的p个位置的二进制位变成0:num & ~(1<<p);
全局变量和局部变量
前面,我们一直将变量定义在 main 函数里面,其实,变量也可以定义在 main 函数外面:
#include <stdio.h>
//在main函数外部定义变量
int n = 100;
char c = '@';
int main(){
//在main函数内部定义变量
float f = 89.5;
char str[32] = "helloworld"; //前面没用过,涉及倒后面的数组,暂时先用一下,表示定义字符串变量。
//输出变量
printf("n: %d\nc: %c\nf: %f\nstr: %s\n", n, c, f, str);
return 0;
}
在函数外部定义的变量叫做全局变量(Global Variable)
在函数内部定义的变量叫做局部变量(Local Variable)
全局变量和局部变量的区别:
局部变量的作用域也仅限于函数内部,出了函数就不能使用了;
全局变量的默认作用域是整个程序,也就是所有的代码文件,包括源文件(.c文件)和头文件(.h文件)。
大体上了解C语言的内存布局
由C语言代码(文本文件)形成可执行程序(二进制文件),需要经过预处理-编译-汇编-连接几个阶段阶段。编译过程把C语言文本文件生成汇编程序,汇编过程把汇编程序形成二进制机器代码,连接过程则将各个源文件生成的二进制机器代码文件组合成一个文件。
合成的统一文件,它由几个部分组成。
各个部分会存放在不同的内存区域中:
(1)代码段(Code或Text)
在C语言中,程序语句进行编译后,形成机器代码。在执行程序的过程中,CPU的程序计数器指向代码段的每一条机器代码,并由处理器依次运行。
基本数学运算(+,-),逻辑运算(&&,||),位运算(&,|,^)等都属于代码段的内容。
(2)只读数据段(RO data)
常量和下面要讲的常变量,不能被改变的数据放这里。
(3)已初始化读写数据段(RW data)
在程序中一般为已经初始化的全局变量,已经初始化的静态局部变量(static修饰的已经初始化的变量)
(4)未初始化数据段(BSS)
只是在程序中声明,但是没有初始化值的变量,这些变量在程序运行之前不需要占用内存空间。因此它只会在目标文件中被标识,将会在程序运行时产生。
(5)堆(heap)
堆内存只在程序运行时出现,一般由程序员分配和释放。在具有操作系统的情况下,如果程序没有释放,操作系统可能在程序(例如一个进程)结束后回收内存。
(6)栈(stack)
栈内存只在程序运行时出现,在函数内部使用的变量
变量前的修饰符
1.const关键字
const 意思是“恒定不变的”!
用 const 定义的变量的值是不允许改变的。这也就意味着必须在定义的时候就给它赋初值。
说 const 定义的是变量,但又相当于常量;说它定义的是常量,但又有变量的属性,所以叫常变量。
const int a = 10;
//上面的写法等价于:
int const a = 10;
const 和 define比较:
从功能上说const 和 define确实很像,但它们又有明显的不同,define是预处理命令定义的常量,而const是普通变量的定义。
2.static关键字
static关键字表示修饰全局变量的作用域只能是本身的编译单元(一个文件的编译)。
static修饰的局部变量表示变量不放在栈内存里,放在读写数据段里,所以相当于本编译单元里的全局变量。
还是通过例子来理解:
/*test.c*/
#include <stdio.h>
static int i = 100;
void fun(){
printf("extern.c file i:%d\n",i);
}
/*main.c*/
#include <stdio.h>
#include <stdlib.h>
int i;
int main(void){
printf("main.c file i:%d\n",i);
exit(0);
}
//联合编译,自动生成一个a.exe文件
C:\Users\Administrator\Desktop\workspaceC>gcc main.c test.c
C:\Users\Administrator\Desktop\workspaceC>a
main.c file i:0
const作用在局部变量中,相当于当前编译单元中的全局变量
#include <stdio.h>
#include <stdlib.h>
void fun(){
static int m =10;
m++;
printf("m:%d\n",m);
}
int main(void){
fun();
fun();
fun();
exit(0);
}
//gcc -g const.c -o const.ext 或开发IDE中运行
[Running] cd "c:\Users\Administrator\Desktop\workspaceC\" && gcc const.c -o const && "c:\Users\Administrator\Desktop\workspaceC\"const
m:11
m:12
m:13
3.volatile关键字
volatile和编译器优化关系:
由于内存访问速度远不及CPU处理速度,为提高机器整体性能,需要优化措施:
1)在硬件上:CPU在运算时会直接用的设备由近到远,主要有寄存器、高速缓存和内存。
CPU与寄存器交互速度是最快的,其次是高速缓存,再次是内存。
2)执行程序的编译器常用的一个优化方法:将内存变量缓存到寄存器。
由于访问寄存器要比访问内存单元快的多,编译器在存取变量时,为提高存取速度,编译器优化有时会先把变量读取到一个寄存器中;以后再取变量值运算时就直接从寄存器中取值。但如果这个变量的值变化很频繁,就有可能造成CPU运行的数据和变量里的实时数据不一致,造成运算结果错误,volatile的作用就是提醒编译器它后面所定义的变量随时都有可能改变,告诉编译器对该变量不做优化,要运算时都会直接从变量内存地址中读取数据,从而避免不一致的错误。
***注意:***频繁地使用volatile很可能会增加代码尺寸和降低性能,因此要合理的使用volatile,一般不用它。
4.extern关键字
关键字extern,仅仅是声明这个变量/函数可能在别的源文件里定义。
局部变量的声明不能有extern的修饰。在全局变量上,我们可以理解写和没写基本是一样的。
通过例子来理解:
/*test.c*/
#include <stdio.h>
int i = 100;
void fun(){
printf("extern.c file i:%d\n",i);
}
/*main.c*/
#include <stdio.h>
#include <stdlib.h>
extern int i; //此处不写extern也没关系,因为当我们联合编译时,也可找到基它文件i赋的值
int main(void){
printf("main.c file i:%d\n",i);
exit(0);
}
//联合编译,自动生成一个a.exe文件
C:\Users\Administrator\Desktop\workspaceC>gcc main.c test.c
C:\Users\Administrator\Desktop\workspaceC>a
main.c file i:100
5.register关键字
register修饰符会给编译程序暗示我修饰的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储读取速度。
使用register修饰符有几点限制
(1)register变量必须是能被CPU所接受的类型。
这通常意味着register变量必须是一个单个的值,并且长度应该小于或者等于整型的长度。
不过,有些机器的寄存器也能存放浮点数。
(2)因为register变量可能不存放在内存中,所以不能用“&”来获取register变量的地址。
(3)只有局部变量和形式参数可以作为寄存器变量,其它(如全局变量)不行。
(4)局部静态变量不能定义为寄存器变量。不能写成:register static int a, b, c;
(5)由于寄存器的数量有限(不同的cpu寄存器数目不一),不能定义任意多个寄存器变量,而且某些寄存器只能接受特定类型的数据(如指针和浮点数),因此真正起作用的register修饰符的数目和类型都依赖于运行程序的机器,而任何多余的register修饰符都将被编译程序所忽略。
注意:
早期的C编译程序不会把变量保存在寄存器中,除非你命令它这样做,这时register修饰符是C语言的一种很有价值的补充。然而,随着编译程序设计技术的进步,在决定哪些变量应该被存到寄存器中时,现在的C编译环境能比程序员做出更好的决定。实际上,许多编译程序都会忽略register修饰符,因为尽管它完全合法,但它仅仅是暗示而不是命令。
6.auto关键字
auto被解释为一个自动存储变量的关键字,也就是申明一块临时的变量内存。
变量,没加修饰符的时候,默认就是atuo这个关键字。
注意:静态变量、全局变量申明后没初始化会有一个默认的初始值0,局部变量没有初始化值,系统随机赋值一个垃圾值,所以局部变量一定要赋值。
基本输入输出函数
输入输出(Input and Output, IO)是用户和程序“交流”的过程。
在控制台程序中,输出一般是指将数据(包括数字、字符等)显示在屏幕上,输入一般是指获取用户在键盘上输入的数据。
主要讲解在stdio.h头文件申明的几个标准输入输出函数,
1–printf ,scanf:最基本输入输出函数, 是最灵活、最复杂、最常用的输入输出函数
2–getchar,putchar:针对字符的输入和输出函数,有针对性用起来简单点
3–gets,puts:针对字符串的输入输出函数,有针对性用起来简单点
1、printf函数,scanf函数
如果在程序中要使用 printf,那么就必须要包含头文件 stdio.h,此函数包含在该头文件中的。
printf是基本的输出函数,其功能是将程序运行的结果输出到屏幕上。
printf 函数的原型为:
# include <stdio.h>
int printf(const char *format, ...);
//const char *format :常变量 变量类型字符类型 指针变量,char *format暂时你就理解为字符串
//... :可变参数,参数的个数不确定
printf 的格式有三种:
1) printf("字符串\n");
# include <stdio.h>
int main(void)
{
printf("Hello World!\n"); // \n表示换行
return 0;
}
2) printf("字符串可以包含输出控制符",输出参数);
# include <stdio.h>
int main(void)
{
int i = 10;
printf("%d\n", i); /*%d是输出控制符,d 表示十进制,后面的 i 是输出参数*/
return 0;
}
这句话的意思是将变量 i 以十进制输出。
那么现在有一个问题:i 本身就是十进制,为什么还要将 i 以十进制输出呢?
因为程序中虽然写的是 i=10,但是在内存中并不是将 10 这个十进制数存放进去,而是将 10 的二进制代码存放进去了。计算机只能执行二进制 0、1 代码,而 0、1 代码本身并没有什么实际的含义,它可以表示任何类型的数据。所以输出的时候要强调是以哪种进制形式输出。所以就必须要有“输出控制符”,以告诉操作系统应该怎样解读二进制数据。
3) printf("输出控制符1 输出控制符2…", 输出参数1, 输出参数2, …);
# include <stdio.h>
int main(void)
{
int i = 10;
int j = 3;
printf("%d %d\n", i, j);
return 0;
}
**注意:**printf 中双引号内除了输出控制符和转义字符\n外,所有其余的普通字符全部都原样输出。“输出控制符”和“输出参数”无论在“顺序上”还是在“个数上”一定要一一对应。
常用的输出控制符主要有以下几个:
%d 按十进制整型数据的实际长度输出。
%ld 输出长整型数据。
%md m 为指定的输出字段的宽度。如果数据的位数小于 m,则左端补以空格,若大于 m,则按实际位数输出。
%-md 有-表示左对齐输出,如省略表示右对齐输出。
%0md 有0表示指定空位填0,如省略表示指定空位填空格。要写在m的前面,不能和-一起用。
%u 输出无符号整型(unsigned)。
%c 用来输出一个字符。
%f 用来输出小数。不指定字段宽度,由系统自动指定,整数部分全部输出,小数部分输出 6 位,超过 6 位的四舍五入。
%.nf 输出实数时小数点后保留 n 位,注意 n 前面有个点,不写n默认值是6。
%o 八进制输出
%s 用来输出字符串。用 %s 输出字符串同前面直接输出字符串是一样的。但是此时要先定义字符数组或字符指针存储或指向字符串。
%x(或 %X 或 %#x 或 %#X) 以十六进制形式输出整数,这个很重要。加#,则输出前缀有 'ox'
%x、%X、%#x、%#X 的区别
一定要掌握 %x(或 %X 或 %#x 或 %#X),
因为调试的时候经常要将内存中的二进制代码全部输出,然后用十六进制显示出来。
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int i = 47;
printf("%x\n", i); //2f
printf("%X\n", i); //2F
printf("%#x\n", i); //0x2f
printf("%#X\n", i); //0X2F
return 0;
}
如何输出 %d、\ 和双引号
printf 中有输出控制符%d,转义字符前面有反斜杠\,还有双引号。那么大家有没有想过这样一个问题:
怎样将这三个符号通过 printf 输出到屏幕上呢?
要输出%d只需在前面再加上一个%,要输出\只需在前面再加上一个\,要输出双引号也只需在加上一个\即可。
程序如下:
printf("%%d\n"); //%d
printf("\\\n"); // \
printf("\"\"\n"); // ""
int n = 234;
float f = 9.8;
char c = '@';
char *str = "helloworld";
printf("%10d%14.9f%5c%8s", n, f, c, str); // 234 9.800000191 @helloworld
return 0;
scanf函数
scanf是从键盘获得用户输入,和 printf 的功能正好相反。
#include <stdio.h>
#include <stdlib.h>
int main(void){
int a =1,b=2,c=3,d=4;
scanf("%d",&a); // &:取地址符,&a表示获取到a变量对应在内存中的地址
scanf("%d",&b);
printf("变量a: %d和变量b: %d的和: %d\n",a,b,a+b);
exit(0);
}
***注意:***对于 scanf(),输入数据的格式要和控制字符串的格式保持一致。
printf输出中文乱码是编码是问题,解决办法:终端输入:chcp 65001 回车。本人这里是系统默认的编译cmd,不是windows的cmd,所以没有出现中文乱码.
内存地址的理解:
前面反复讲计算机里的数据是以二进制的形式保存在内存/磁盘中的,字节(Byte)是最小的可操作单位。
为了便于管理,我们给内存中每个字节分配了一个编号,使用该字节时,只要知道编号就可以,就像每个学生都有学号,老师会让学生回答问题,喊学号就可以了。字节的编号是有顺序的,从 0 开始,接下来是 1、2、3……
这个编号,就叫做内存的地址(Address)。
int a;会在内存中分配四个字节的空间,我们将第一个字节的地址称为变量 a 的地址,也就是&a的值。
对于前面讲到的整数、浮点数、字符,都可以使用 & 获取它们对应的变量的地址,
scanf 会根据地址把读取到用户在键盘上敲击的数据写入内存。
我们不妨将变量的地址输出看一下:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int a='F';
int b=12;
int c=452;
printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c); //&a=0X61FE4C, &b=0X61FE48, &c=0X61FE44
exit(0);
}
输入其它数据
除了输入整数,scanf() 还可以输入单个字符、字符串、小数等,请看下面的演示:
#include <stdio.h>
#include <stdlib.h>
int main(void){
char letter;
int age;
char str[30];
float price;
scanf("letter=%c", &letter);
scanf("%d", &age);
scanf("%s", &str);
scanf("%f", &price);
printf("%c\n", letter);
printf("%d, %s,%g\n", age, str, price);
return 0;
}
//编译scanf.c,自动生成a.exe文件,运行a可执行文件,回车后,程序会阻塞程序运行,让我们输入数据
PS C:\Users\Administrator\Desktop\workspaceC\inputout>gcc scan.c
PS C:\Users\Administrator\Desktop\workspaceC\inputout> ./a
//这里输入时,一定要按规定的格式输入,不然会出现怪现像。
letter=a
12
xiongshaowen
12.23
a
12, xiongshaowen,12.23
PS C:\Users\Administrator\Desktop\workspaceC\inputout>
最后需要注意的一点是,scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以一般情况下无法读取含有空格的字符串,想要读取空格要作其它处理
2-getchar()和putchar()函数
getchar()字符输入函数,它就是scanf(“%c”, c)的替代品,除了更加简洁,没有其它优势了;
或者说,getchar() 就是 scanf() 的一个简化版本。
putchar©就是printf(“%c\n”, c);简化版本了。
#include <stdio.h>
int main()
{
char c = getchar();
putchar(c);
return 0;
}
3-gets()和puts()函数
gets() 这个专用的字符串输入函数,puts(cc)专用打印输出字符串函数。
#include <stdio.h>
int main()
{
char aa[30], bb[30], cc[30];
gets(aa);
puts(aa);//使用 puts() 函数连换行符 '\n' 都省了,使用 puts() 显示字符串时,系统会自动在其后添加一个换行符
gets(bb);
puts(bb)
gets(cc);
puts(cc);
return 0;
}
gets() 是有缓冲区的,每次按下回车键,就代表当前输入结束了,
gets() 开始从缓冲区中读取内容,这一点和 scanf() 是一样的。
gets() 和 scanf() 的主要区别是:
scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。
gets() 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束。
**总结:**gets() 能读取含有空格的字符串,而 scanf() 不能。
scanf() 可以一次性读取多份类型相同或者不同的数据,
getchar() 和 gets() 每次只能读取一份特定类型的数据,不能一次性读取多份数据。
基本输入输出函数进阶知识点:
认识缓冲区(缓存)
缓冲区(Buffer)又称为缓存(Cache),是内存空间的一部分。
也就是说,计算机在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区(缓存)。
有时候,从键盘输入的内容,或者将要输出到显示器上的内容,会暂时进入缓冲区,待时机成熟,再一股脑将缓冲区中的所有内容“倒出”,我们才能看到变量的值被刷新,或者屏幕产生变化。
有时候,用户希望得到最及时的反馈,输入输出的内容就不能进入缓冲区。
缓冲区的必要性:
缓冲区是为了让低速的输入输出设备和高速的用户程序能够协调工作,能提升程序效率,并降低输入输出设备的读写次数。
缓冲区的类型
根据缓冲区对应的是输入设备还是输出设备,可以分为输入缓冲区和输出缓冲区。
根据数据刷新(也可以称为清空缓冲区,就是将缓冲区中的数据“倒出”)的时机,
可以分为全缓冲、行缓冲、不带缓冲。
1) 全缓冲
在这种情况下,当缓冲区被填满以后才进行真正的输入输出操作。缓冲区的大小都有限制的,比如 1KB、4MB 等,数据量达到最大值时就清空缓冲区。
2) 行缓冲
在这种情况下,当在输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作。
//对于 printf() 函数:
#include <stdio.h>
#include <unistd.h>
int main()
{
printf("AAAAAAA"); //\n不仅仅有换行这个格式的作用,还有一个隐藏的功能:刷新缓存
sleep(5); //程序暂停5秒钟
printf("BBBBBBBBBBB\n");
return 0;
}
windows操纵系统和Linux操纵系统对printf的缓冲是否默认开启有差别,win默认关闭printf的缓冲区,而其它像scanf()默认是打开的,linux默认开启,
手动开启:setvbuf(stdout, buf, _IOLBF, 512);
存在缓冲和不存在缓冲允许结果是不一样的!
下图是两种操作系统效果图
下面的代码开启了缓冲区buf,后,win和linux执行效果是一样的
#include <stdio.h>
#include <unistd.h>
char buf[512];
int main()
{
setvbuf(stdout,buf,_IOLBF,512); //开启行缓冲区
printf("AAAAAAA\n"); //\n不仅仅有换行这个格式的作用,还有一个隐藏的功能:刷新缓存
sleep(5); //程序暂停5秒钟
printf("BBBBBBBBBBB\n");
return 0;
}
我们可以手动刷新缓冲区的,下面代码就是用了 fflush(stdout);
#include <stdio.h>
#include <unistd.h>
char buf[512];
int main()
{
setvbuf(stdout,buf,_IOLBF,512); //开启行缓冲区
printf("AAAAAAA"); //\n不仅仅有换行这个格式的作用,还有一个隐藏的功能:刷新缓存
fflush(stdout); //手动刷新缓冲区,即不用在printf()中加'\n'
sleep(5); //程序暂停5秒钟
printf("BBBBBBBBBBB\n");
return 0;
}
3) 不带缓冲
不带缓冲区,数据就没有地方缓存,必须立即进行输入输出。
getche()、getch() 就不带缓冲区,输入一个字符后立即就执行了,根本不用按下回车键。
这两个方法是Windows下特有的方法,Linux和Mac上没有,就不举例了。
scanf()缓冲区问题
首先声明一下,这个的缓冲区不管在那都是默认打开的,并且缓冲区中有\n符,所以如下代码运行时是有异常的
#include <stdio.h>
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("b=%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
处理方法:
#include <stdio.h>
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("%d", &b); //%d开头,前面有空格也好,换行已好默认忽略。
printf("a=%d, b=%d\n", a, b);
return 0;
}
//或
#include <stdio.h>
int main()
{
int a = 1, b = 2;
scanf("a=%d", &a);
scanf("%*[\n]"); scanf("%*[ ]"); //在下次读取前清空缓冲区
scanf("b=%d", &b);
printf("a=%d, b=%d\n", a, b);
return 0;
}
scanf() 高级用法
1) 指定读取长度
在前面讲过printf() 中可以指定最小输出宽度!
就是在格式控制符的中间加上一个数字,例如,%10d表示输出的整数至少占用 10 个字符的位置:
如果整数的宽度不足 10,那么在左边以空格补齐;
如果整数的宽度超过了 10,那么以整数本身的宽度来输出,10 不再起作用。
其实,scanf() 也有类似的用法,也可以在格式控制符的中间加一个数字,用来表示读取数据的最大长度,
例如:
%2d表示最多读取两位整数;
%10s表示读取的字符串的最大长度为 10,或者说,最多读取 10 个字符。
#include <stdio.h>
int main(){
int n;
float f;
char str[23];
scanf("%2d", &n);
scanf("%*[^\n]"); scanf("%*c"); //清空缓冲区
scanf("%5f", &f);
scanf("%*[^\n]"); scanf("%*c"); //清空缓冲区
scanf("%22s", str);
printf("n=%d, f=%g, str=%s\n", n, f, str);
return 0;
}
限制读取数据的长度在实际开发中非常有用,最典型的一个例子就是读取字符串:我们为字符串分配的内存是有限的,用户输入的字符串过长就存放不了了,就会冲刷掉其它的数据,从而导致程序出错甚至崩溃;
在用 gets() 函数读取字符串的时候, gets() 不能控制读取到的字符串的长度,风险极高。
就目前学到的知识而言,虽然 scanf() 可以控制字符串的长度,但是字符串中却不能包含空白符,这是硬伤,所以 scanf() 暂时还无法替代 gets()。不过后面还有scanf() 的更高级用法,届时 scanf() 就可以完全替代 gets(),并且比 gets() 更加智能。
2) 匹配特定的字符
%s 控制符有两个缺点:
%s 不能读取特定的字符,比如只想读取小写字母,或者十进制数字等,%s 就无能为力;
%s 读取到的字符串中不能包含空白符,有些情况会比较尴尬,例如,无法将多个单词存放到一个字符串中,因为单词之间就是以空格为分隔的,%s 遇到空格就读取结束了。
要想解决以上问题,可以使用 scanf() 的另外一种字符匹配方式,就是%[xxx],[ ]包围起来的是需要读取的字符规则。例如,%[abcd]表示只读取字符abcd,遇到其它的字符就读取结束;
注意,这里并不强调字符的顺序,只要字符在 abcd 范围内都可以匹配成功,所以你可以输入 abcd、dcba、ccdc、bdcca 等。
#include <stdio.h>
int main(){
char str[30];
scanf("%[abcd]", str);
printf("%s\n", str);
return 0;
}
3)使用连接符
连字符左边的字符对应一个 ASCII 码,连字符右边的字符也对应一个 ASCII 码,位于这两个 ASCII 码范围以内的字符就是要读取的字符。注意,连字符左边的 ASCII 码要小于右边的,如果反过来,那么它的行为是未定义的。
常用的连字符举例:
%[A-Z]表示读取 ABC…XYZ 范围内的字符,也即大写字母;
%[0-9]表示读取 012…789 范围内的字符,也即十进制数字。
%[a-z]表示读取 abc…xyz 范围内的字符,也即小写字母;
你也可以将它们合并起来,例如:
%[a-zA-Z]表示读取大写字母和小写字母,也即所有英文字母;
%[a-zA-Z0-9]表示读取所有的英文字母和十进制数字;
%[0-9a-f]表示读取十六进制数字。
#include <stdio.h>
int main(){
char str[30];
scanf("%[a-zA-Z]", str); //只读取字母
printf("%s\n", str);
return 0;
}
4)不匹配某些字符
假如现在有一种需求,就是读取换行符以外的所有字符,或者读取 0~9 以外的所有字符,该怎么实现呢?
总不能把剩下的字符都罗列出来吧,一是麻烦,二是不现实。
scanf() 允许我们在%[ ]中直接指定某些不能匹配的字符,具体方法就是在不匹配的字符前面加上^,例如:
%[^\n]表示匹配除换行符以外的所有字符,遇到换行符就停止读取;
%[^0-9]表示匹配除十进制数字以外的所有字符,遇到十进制数字就停止读取。
#include <stdio.h>
int main(){
char str1[30], str2[30];
scanf("%[^0-9]", str1);
scanf("%*[^\n]"); scanf("%*c"); //清空缓冲区
scanf("%[^\n]", str2);
printf("str1=%s \nstr2=%s\n", str1, str2);
return 0;
}
5) 丢弃读取到的字符
在前面的代码中,每个格式控制符都要对应一个变量,把读取到的数据放入对应的变量中。其实你也可以不这样做,scanf() 允许把读取到的数据直接丢弃,不往变量中存放,具体方法就是在 % 后面加一个*,例如:
%d表示读取一个整数并丢弃;
%[a-z]表示读取小写字母并丢弃;
%*[^\n]表示将换行符以外的字符全部丢弃。
#include <stdio.h>
int main(){
int n;
char str[30];
scanf("%[^\n]", str);
scanf("%*[^\n]"); scanf("%*c"); //清空缓冲区
scanf("%d",&n);
printf("n=%d, str=%s\n", n, str);
return 0;
}
解决问题,scanf函数能获取带空格的字符串数据,完全替换gets函数的写法:
#include <stdio.h>
int main()
{
char str[32];
scanf("%32[^\n]",&str); //sancf替换gets函数的写法!
printf("str=%s\n",str);
return 0; // exit(0);
}
流程控制
C语言中的流程控制可以总结为三大结构:顺序结构、选择结构和循环结构
1-顺序结构就是让程序按照从头到尾的顺序依次执行每一条C语言代码,不重复执行,也不跳过任何代码。
2-选择结构也称分支结构,就是让程序有选择性的执行代码,可以跳过没用的代码,只执行有用的代码。
3-循环结构就是让程序不断地重复执行同一段代码,在操场转圈圈一样。
顺序结构普通的程序就是依次执行每一条C语言代码,我们前面写的所有Demo都是这种结构。
选择结构
1、if else语句
#include <stdio.h>
int main()
{
int age = 0;
printf("请输入你的年龄:");
scanf("%d", &age);
if(age>=18){
printf("你已经成年!\n"); //只有一条语句的时候,大括号可以省略
}else{
printf("你还未成年!\n");
}
return 0;
}
多个if else语句
#include <stdio.h>
int main(){
char c;
printf("Input a character:");
c=getchar(); //对键盘的按键进行监听
if(c<32)
printf("This is a control character\n");
else if(c>='0'&&c<='9')
printf("This is a digit\n");
else if(c>='A'&&c<='Z')
printf("This is a capital letter\n");
else if(c>='a'&&c<='z')
printf("This is a small letter\n");
else
printf("This is an other character\n");
return 0;
}
if语句的嵌套
#include <stdio.h>
int main(){
int a,b;
printf("Input two numbers:");
scanf("%d %d",&a,&b);
if(a!=b){ //!=表示不等于
if(a>b) printf("a>b\n");
else printf("a<b\n");
}else{
printf("a=b\n");
}
return 0;
}
2、switch case语句
C语言虽然没有限制 if else 能够处理的分支数量,
但当分支过多时,用 if else 处理会不太方便,代码量也稍微有点冗余。
#include <stdio.h>
int main(){
int a;
printf("Input integer number:");
scanf("%d",&a);
switch(a){
case 1: printf("Monday\n"); break;
case 2: printf("Tuesday\n"); break;
case 3: printf("Wednesday\n"); break;
case 4: printf("Thursday\n"); break;
case 5: printf("Friday\n"); break;
case 6: printf("Saturday\n"); break;
case 7: printf("Sunday\n"); break;
default:printf("error\n"); break;
}
return 0;
}
case 后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量。
default 不是必须的。当没有 default 时,如果所有 case 都匹配失败,那么就什么都不执行。
?和:组成的条件运算符
if(a>b){
max = a;
}else{
max = b;
}
//上面的写法等价于:
max = (a>b) ? a : b;
循环结构
1、while循环
通过例子来讲while循环:
用 while 循环计算1加到100的值:
#include <stdio.h>
int main(){
int i=1, sum=0;
while(i<=100){
sum+=i;
i++;
}
printf("%d\n",sum);
return 0;
}
while循环使用注意两点:
1循环条件恒定成立时的话,while 循环会一直执行下去,永不结束,成为“死循环”。
2 循环条件不成立的话,while 循环就一次也不会执行。
2、do while循环
用do-while计算1加到100的值:
#include <stdio.h>
int main(){
int i=1, sum=0;
do{
sum+=i;
i++;
}while(i<=100);
printf("%d\n", sum);
return 0;
}
大体上和while循环差不多,唯一的差别:
1)while循环:先判断控制循环的逻辑条件,在执行循环体,逻辑条件为false,就不会执行循环体。
2)do while循环:不管逻辑条件是true还是false,都先执行一次循环体,然后在判断条件决定是否继续执行。
3、for循环
对比while循环,下面语句①②③都是必不可少的,被放到了不同的地方,代码结构较为松散,容易漏掉!
#include <stdio.h>
int main(){
int i, sum=0;
int i = 1; //语句① 控制循环的变量
while(i<=100 /*语句②*/ ){ //判断循环要不要继续的条件
sum+=i;
i++; //语句③ 更新控制循环的变量的值
}
printf("%d\n",sum);
return 0;
}
在实际开发中常常用for循环的写法代替上面讲的while循环,尽管效果没什么差别:
#include <stdio.h>
int main(){
int sum=0;
for(int i=1/*语句①*/; i<=100/*语句②*/; i++/*语句③*/){
sum+=i;
}
printf("%d\n",sum);
return 0;
}
上面是for循环规规矩矩的写法,下面我们在看看不规矩的写法:
- 修改“从1加到100的和”的代码,省略“表达式1(初始化条件)”
int i = 1, sum = 0;
for( ; i<=100; i++){
sum+=i;
}
- 省略了“表达式2(循环条件)”,如果不做其它处理就会成为死循环。例如:
for(int i=1; ; i++) sum=sum+i;
相当于:
int i=1;
while(1){
sum=sum+i;
i++;
}
- 省略了“表达式3”,这时可在循环体中加入修改变量的语句。例如:
for(int i=1; i<=100; ){
sum=sum+i;
i++;
}
- 省略了“表达式1”和“表达式3”。例如:
int i=0;
for( ; i<=100 ; ){
sum=sum+i;
i++;
}
相当于:
int i=0;
while(i<=100){
sum=sum+i;
i++;
}
- 3个表达式可以同时省略。例如:
for( ; ; ) 语句
相当于:
while(1) 语句
***注意:***尽量不要在循环体中定义条变量,这会大量浪费内存,可以先定义一个变量,再在循环中重新赋值。
4、break和continue关键字
当 break 关键字用于 while、for 循环时,会终止循环。
break 关键字通常和 if 语句一起使用,即满足条件时便跳出循环。
continue 语句的作用是跳过循环体中剩余的语句而强制进入下一次循环。
continue语句只用在 while、for 循环中,常与 if 条件语句一起使用,判断条件是否成立。
#include <stdio.h>
int main(){
int i=1, sum=0;
while(1){ //循环条件为死循环
sum+=i;
i++;
if(i>100) break; //如果i>100整个循环结束了,不同于return语句,会执行下面的语句(循环外的)
}
printf("%d\n", sum);
return 0;
}
#include <stdio.h>
int main(){
char c = 0;
while(c!='\n'){ //回车键结束循环
c=getchar();
if(c=='4' || c=='5'){ //按下的是数字键4或5
continue; //跳过当次循环,进入下次循环,即输入一个字符串后,过滤掉4,5字符
}
putchar(c);
}
return 0;
}
5、循环嵌套举例
例子1:简单的for循环嵌套
#include <stdio.h>
int main()
{
int i, j;
for(i=1; i<=4; i++){ //外层for循环
for(j=1; j<=4; j++){ //内层for循环
printf("i=%d, j=%d\n", i, j);
}
printf("\n");
}
return 0;
}
//执行结果
[Running] cd "c:\Users\Administrator\Desktop\workspaceC\if_for\" && gcc xunhuan.c -o xunhuan && "c:\Users\Administrator\Desktop\workspaceC\if_for\"xunhuan
i=1, j=1
i=1, j=2
i=1, j=3
i=1, j=4
i=2, j=1
i=2, j=2
i=2, j=3
i=2, j=4
i=3, j=1
i=3, j=2
i=3, j=3
i=3, j=4
…………。
#include <stdio.h>
int main()
{
int i=1, j=0;
while(1){ // 外层循环
j=1;
while(1){ // 内层循环
printf("%-4d", i*j);
j++;
if(j>4) break; //跳出内层循环
}
printf("\n");
i++;
if(i>4) break; // 跳出外层循环
}
/*int i, j;
for(i=1; i<=4; i++){ //外层for循环
for(j=1; j<=4; j++){ //内层for循环
printf("%-4d", i*j);
}
printf("\n");
}*/
return 0;
}
//执行结果
[Running] cd "c:\Users\Administrator\Desktop\workspaceC\if_for\" && gcc while.c -o while && "c:\Users\Administrator\Desktop\workspaceC\if_for\"while
1 2 3 4
2 4 6 8
3 6 9 12
4 8 12 16
例子3:输出九九乘法表。
#include <stdio.h>
int main(){
int i, j;
for(i=1; i<=9; i++){ //外层for循环
for(j=1; j<=i; j++){ //内层for循环
printf("%d*%d=%-3d ", i, j, i*j);
}
printf("\n");
}
return 0;
}
循环问题
1.***错误:***只允许在 C99 模式下使用‘for’循环初始化声明
for (int i = 0; i < (sizeof(a) / sizeof(a[0])); i++) 。。。。
处理:即把int i=0;定义在循环体外。
#数组
数组(Array)就是一些列具有相同类型的数据的集合,这些数据在内存中依次挨着存放,彼此之间没有缝隙。
C语言数组属于构造数据类型。
为什么要在编程里设计数组这种东西:为了减少变量数目,让开发更有效率,能让多个数据绑定一个变量名。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int a1=20, a2=345, a3=700, a4=22;
int b1=55, b2=66, b3=77, b4=88;
int c1=99, c2=11, c3=12, c4=13;
int d1=14, d2=15, d3=16, d4=17;
printf("%-9d %-9d %-9d %-9d\n", a1, a2, a3, a4);
printf("%-9d %-9d %-9d %-9d\n", b1, b2, b3, b4);
printf("%-9d %-9d %-9d %-9d\n", c1, c2, c3, c4);
printf("%-9d %-9d %-9d %-9d\n", d1, d2, d3, d4);
return 0;
}
有了数组后,上面的一堆代码可以这样写:
int a[16] = {20, 345, 700, 22, 55, 66, 77, 88, 99, 11, 12, 13, 14, 15, 16, 17};
for (int i = 0; i < (sizeof(a) / sizeof(a[0])); i++) {
printf("%-9d", a[i]);
if((i+1)%4==0) printf("\n");
}
数组的定义方式:
dataType arrayName[length];
dataType 为数据类型,arrayName 为数组变量名称,length 为数组长度。
例如:
int a[16]; //定义一个长度为16的整数类型数组
float m[12]; //定义一个长度为 12 的浮点型数组
char ch[9]; //定义一个长度为 9 的字符型数组
注意:C语言中的数组是静态的,数组一旦被定义后,占用的内存空间就是固定的,容量就是不可改变的,既不能在任何位置插入元素,也不能在任何位置删除元素,只能读取和修改元素,我们将这样的数组称为静态数组。
和其他变量的定义一样,数组变量定义的时候,也可以同时做初始化:
int a[4] = {20, 345, 700, 22};
对于数组的初始化需要注意以下几点:
-
可以只给部分元素赋值。当{ }中值的个数少于元素个数时,只给前面部分元素赋值。例如:
int a[10]={12, 19, 22 , 993, 344};
表示只给 a[0]~a[4] 5个元素赋值,而后面 5 个元素自动初始化为 0。 -
只能给元素逐个赋值,不能给数组整体赋值。例如给 10 个元素全部赋值为 1,只能写作:
int a[10] = {1, 1, 1, 1, 1, 1, 1, 1, 1, 1};
而不能写作:
int a[10] = 1;
- 如给全部元素赋值,那么在定义数组时可以不给出数组长度。例如:
int a[] = {1, 2, 3, 4, 5};
等价于
int a[5] = {1, 2, 3, 4, 5};
数组的定义,本质是根据数据长度分配对应个数的内存空间格子,一个格子放置一个数组元素,数组中的每个元素对应的内存格子都有一个序号,这个序号从0开始,而不是从我们熟悉的1开始,称为下标(Index)。
要获取数组元素时,我们通过数组变量名称+下标的形式:
arrayName[index],例如,a[0] 表示第0个元素,a[3] 表示第4个元素。
修改数组里边的小标对应的内存格子里的数据:
arrayName[index] = 修改后的值;
在使用数组过程中,我们经常会使用循环结构将数据放入数组中,或者使用循环结构输出数组中元素值,
下面我们就来演示一下如何将 1~10 这十个数字放入数组中:
#include <stdio.h>
int main(){
int nums[10];
//将1~10放入数组中
for(int i=0; i<10; i++){
nums[i] = (i+1);
}
//依次输出数组元素,这种做法叫数组遍历
for(int i=0; i<10; i++){
printf("%d ", nums[i]);
}
return 0;
}
需要注意的是:
- 数组中每个元素的数据类型必须相同,对于int a[4];,每个元素都必须为 int。
- 数组长度 length 最好是整数或者常量表达式,例如 10、204 等,这样在所有编译器下都能运行通过;
如果 length 中包含了变量,例如 n、4m 等,在某些编译器下就会报错,某些不会。 - 访问数组元素时,下标的取值范围为 0 ≤ index < length,过大或过小都会越界,导致数组溢出,
发生不可预测的情况,我们要避免数组下标溢出。
##二维数组
二维数组的定义、初始化、赋值、取值、遍历
数组可以看作是一行连续的数据,只有一个下标,称为一维数组。
在实际问题中有很多数据是二维的或多维的,因此C语言允许构造多维数组。多维数组元素有多个下标,以确定它在数组中的位置。
二维数组定义的一般形式是:
dataType arrayName[length1][length2];
我们可以将二维数组看做一个 Excel 表格,有行有列,length1 表示行数,length2 表示列数,要在二维数组中定位某个元素,必须同时指明行和列。
例如:int a[3][4];
a[0][0], a[0][1], a[0][2], a[0][3]
a[1][0], a[1][1], a[1][2], a[1][3]
a[2][0], a[2][1], a[2][2], a[2][3]
二维数组的初始化(赋值)
二维数组的初始化可以按行分段赋值,也可按行连续赋值。
例如,对于数组 a[5][3],按行分段赋值应该写作:
int a[5][3]={ {80,75,92}, {61,65,71}, {59,63,70}, {85,87,90}, {76,77,85} };
按行连续赋值应该写作:
int a[5][3]={80, 75, 92, 61, 65, 71, 59, 63, 70, 85, 87, 90, 76, 77, 85};
这两种赋初值的结果是完全相同的。
单个运算赋值和取值:
a[0][0] = 12;
printf("a[0][0]=%d\n",a[0][0])
二维数组遍历:
例: 用二维数据保存 5 个人,每个人有 3 门课程的考试成绩,求该小组各科的平均分和总平均分。
#include <stdio.h>
int main(void) {
int i=0,j=0; //循环控制变量
int sum = 0; //保存每门课的总成绩;
double average =0; //总平均分
double v[3]; //放3门课,每门课的平均分
int a[5][3]; //存放每人没门课成绩的二维数组
printf("请输入5人3门课的成绩:\n");
for(i=0;i<3;i++){ //外循环遍历的课程,3门课
for(j=0;j<5;j++){ //内循环遍历的5个人
scanf("%d",&a[j][i]);
sum += a[j][i];
}
//每循环一次,就能得到一门课的所有人的成绩综合
v[i] = sum/5.0;
sum = 0;
}
average = (v[0] + v[1] + v[2])/3.0;
for(int k=0;k<3;k++){
printf("第%d门课的平均成绩:%f\n",k+1,v[k]);
}
printf("总平均成绩是:%f\n",average);
int jj =0;
for(int ii=0;ii<5;ii++){
for(jj=0;jj<3;jj++){
printf("%-5d",a[ii][jj]);
}
printf("\n");
}
return 0;
}
//执行结果
请输入5个3门课的成绩:
78
89
56
65
25
69
78
52
36
45
57
89
69
70
28
第1门课的平均成绩:62.600000
第2门课的平均成绩:56.000000
第3门课的平均成绩:62.600000
总平均分成绩是: 60.400000
78 69 57
89 78 89
56 52 69
65 36 70
25 45 28
C语言字符数组和字符串
用来存放字符的数组称为字符数组,例如:
char a[10]; //一维字符数组
char b[5][10]; //二维字符数组
char c[20]={'c', ' ', 'p', 'r', 'o', 'g', 'r', 'a','m'}; // 给部分数组元素赋值
char d[]={'c', ' ', 'p', 'r', 'o', 'g', 'r', 'a', 'm' }; //对全体元素赋值时可以省去长度
字符串定义和初始化:
C语言规定,可以将字符串直接赋值给字符数组,例如:
char str[30] = {"helloworld"};
char str[30] = "helloworld"; //这种形式更加简洁,实际开发中常用
为了方便,你也可以不指定数组长度,从而写作:
char str[] = {"helloworld"};
char str[] = "helloworld"; //这种形式更加简洁,实际开发中常用
给字符数组赋值时,我们通常使用这种写法,将字符串一次性地赋值(可以指明数组长度,也可以不指明),而不是一个字符一个字符地赋值,那样做太麻烦了。
这里需要留意一个坑,字符数组只有在定义时才能将整个字符串一次性地赋值给它,一旦定义完了,就只能一个字符一个字符地赋值了。
请看下面的例子:
char str[7];
str = "abc123"; //错误
//正确
str[0] = 'a'; str[1] = 'b'; str[2] = 'c';
str[3] = '1'; str[4] = '2'; str[5] = '3';
字符串结束标志要我门注意
C语言的字符串结尾标志的解决方案有点奇妙,或者说有点奇葩。
在C语言中,字符串总是以’\0’作为结尾,所以’\0’也被称为字符串结束标志,或者字符串结束符。
'\0’是 ASCII 码表中的第 0 个字符,英文称为 NUL,中文称为“空字符”。该字符既不能显示,也没有控制功能,输出该字符不会有任何效果,它在C语言中唯一的作用就是作为字符串结束标志。
由" “包围的字符串会自动在末尾添加’\0’。例如,“abc123"从表面看起来只包含了 6 个字符,其实不然,C语言会在最后隐式地添加一个’\0’,这个过程是在后台默默地进行的,所以我们感受不到。
``c`
char str[] = “abc123”;
printf(”%d”,sizeof(str)); //7
要特别注意'\0',要为'\0'留个位置,进行指定长度的字符数组定义,字符数组的长度至少要比字符串的长度大 1:
```c
char str[7] = "abc123";
C语言为了提高效率,保证操作的灵活性,并不会对数组越界行为进行检查,即使越界了,也能够正常编译,只有在运行期间才可能会发生问题。在项目开发中,一定一定要注意,这点特别容易导致程序错误。
另外需要注意的是,逐个字符地给数组赋值并不会自动添加’\0’,例如:
char str[] = {'a', 'b', 'c'};
数组 str 的长度为 3,而不是 4,因为最后没有’\0’。这样赋值我们建议:
char str[] = {'a', 'b', 'c','\0'};
另外,在定义字符串变量的时候,如不是定义的同时不需要赋有意义初始值,我们建议这样写:
char str[30] = {0}; //将所有元素都初始化为 0,或者说 '\0'
string.h中声明的其他部分字符串处理函数
1)字符串长度函数 strlen()
所谓字符串长度,就是字符串包含了多少个字符(不包括最后的结束符’\0’)。例如"abc"的长度是 3,而不是 4。
在C语言中,我们使用string.h头文件中的 strlen() 函数来求字符串的长度,比如:
#include <stdio.h>
#include <string.h> //记得引入该头文件
int main(){
char str[] = "helloworld";
long len = strlen(str);
printf("The lenth of the string is %ld.\n", len);
return 0;
}
2)字符串连接函数 strcat()
strcat 意思是把两个字符串拼接在一起,语法格式为:
strcat(arrayName1, arrayName2);
arrayName1、arrayName2 为需要拼接的字符串。
strcat() 将把 arrayName2 连接到 arrayName1 后面,并删除原来 arrayName1 最后的结束标志’\0’。
这意味着,arrayName1 必须足够长,要能够同时容纳 arrayName1 和 arrayName2,否则会越界。
strcat() 的返回值为 arrayName1 的地址。
#include <stdio.h>
#include <string.h>
int main(){
char str1[11]="hello";
char str2[6] = "world";
strcat(str1, str2);
puts(str1);
return 0;
}
3)字符串复制函数 strcpy()
strcpy 意思是字符串复制,也即将字符串从一个地方复制到另外一个地方,语法格式为:
strcpy(arrayName1, arrayName2);
strcpy() 会把 arrayName2 中的字符串拷贝到 arrayName1 中,字符串结束标志’\0’也一同拷贝。
请看下面的例子:
#include <stdio.h>
#include <string.h>
int main(){
char str1[6] = "hello";
char str2[6] = "world";
strcpy(str1, str2);
printf("str1: %s\n", str1);
return 0;
}
将 str2 复制到 str1 后,str1 中原来的内容就被覆盖了。
另外,strcpy() 要求 arrayName1 要有足够的长度,否则不能全部装入所拷贝的字符串。
4)字符串比较函数 strcmp()
strcmp 意思是字符串比较,语法格式为:
strcmp(arrayName1, arrayName2);
arrayName1 和 arrayName2 是需要比较的两个字符串。
字符本身没有大小之分,strcmp() 以各个字符对应的 ASCII 码值值进行比较。
strcmp() 从两个字符串的第 0 个字符开始比较,如果它们相等,就继续比较下一个字符,直到遇见不同的字符,或者到字符串的末尾。
返回值:
若 arrayName1 和 arrayName2 相同,则返回0;
若 arrayName1 大于 arrayName2,则返回大于 0 的值;
若 arrayName1 小于 arrayName2,则返回小于0 的值。
#include <stdio.h>
#include <string.h>
int main(){
char a[] = "aBcDeF";
char b[] = "AbCdEf";
char c[] = "aacdef";
char d[] = "aBcDeF";
printf("a VS b: %d\n", strcmp(a, b));
printf("a VS c: %d\n", strcmp(a, c));
printf("a VS d: %d\n", strcmp(a, d));
return 0;
}
函数
函数的本质就是一个可以重复使用的代码块,它使得我们的程序更加模块化,提高代码的复用性。
将常用的代码以固定的格式封装(包装)成一个独立的模块,只要知道这个模块的名字就可以重复使用它,这个模块就叫做函数(Function)。
###1、函数的定义和调用
1. C语言无参函数的定义
语法:
dataType functionName(void){
//body
return xxx;
}
//有很多时候,我们也写成:
dataType functionName(){
//body
return xxx;
}
dataType 是返回值类型,它可以是C语言中的任意数据类型,例如 int、float、char 等。
functionName 是函数名,它是标识符的一种,命名规则前面讲过。
():放参数的地方,无参数就是括号里没东西,函数名后面的括号( )不能少。
body 是函数体,它是函数需要执行的代码,是函数的主体部分。即使只有一个语句,函数体也要由{ }包围。
如果有返回值,在函数体中使用 return 语句返回。
return 出来的数据的类型要和 dataType 一致。
比如:
int onePlusTwo(){
return 1+2;
}
无参函数的调用,注意先申明后使用的基本规则:
函数名();
onePlusTwo();
括号里什么都没有和有void的区别
这样我们就能知道,onePlusTwo()这种形式不是说onePlusTwo这个函数不允许带任何参数,而是指它带什么类型的参数,有多少个,是不确定的。
而如果我们使用了onePlusTwo(void)这种形式,则就完全限定了onePlusTwo的参数是不允许带任何参数的。
2)无返回值函数的定义
语法格式:
void functionName(){
//body
}
void是C语言中的一个关键字,表示“空类型”或“无类型”,比如:
void hello(){
printf ("Hello,world \n");
//没有返回值就不需要 return 语句
}
3)有参函数的定义
如果函数需要接收用户传递的数据,那么定义时就要带上参数。语法:
dataType functionName(dataType1 param1, dataType2 param2 ... ){
//body
return xxx;
}
dataType1 param1, dataType2 param2 …是参数列表。函数可以只有一个参数,也可以有多个,多个参数之间由,分隔。参数本质上也是变量,定义时要指明类型和名称。
数据通过参数传递到函数内部进行处理,处理完成以后再通过返回值讲函数处理结果返回到调用函数的地方。
int onePlusTwo(int a,int b){
return a+b;
}
有参函数的调用:
onePlusTwo(2,3);
int rs = onePlusTwo(2,3);
函数定义时给出的参数称为形式参数,简称形参;函数调用时给出的参数(也就是传递的数据)称为实际参数,简称实参。函数调用时,将实参的值传递给形参,相当于一次赋值操作。
原则上讲,实参的类型和数目要与形参保持一致。如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型,例如将 int 类型的实参传递给 float 类型的形参就会发生自动类型转换。
注意:函数不能嵌套定义,但可以相互调用
。
【示例】计算sum = 1! + 2! + 3! + … + (n-1)! + n!
分析:可以编写两个函数,一个用来计算阶乘,一个用来计算累加的和。
#include <stdio.h>
//求阶乘
long factorial(int n){
int i;
long result=1;
for(i=1; i<=n; i++){
result *= i;
}
return result;
}
// 求累加的和
long sum(long n){
int i;
long result = 0;
for(i=1; i<=n; i++){
//在定义过程中出现嵌套调用
result += factorial(i);
}
return result;
}
int main(){
printf("1!+2!+...+9!+10! = %ld\n", sum(10)); //main() --> printf() --> sum() --> factorial()
return 0;
}
//执行结果
//1!+2!+...+9!+10! = 4037913
2、库函数和自定义函数
C语言在发布时已经为我们封装好了很多函数,它们被分门别类地放到了不同的头文件中声明(头文件可能只是申明没有定义),使用函数时引入对应的头文件即可。
这些C语言自带的函数称为库函数(Library Function)。库(Library)是编程中的一个基本概念,可以简单地认为它是一很多函数的仓库,在磁盘上往往是一个文件夹。
C语言自带的库称为标准库(Standard Library),其他公司或个人开发的库称为第三方库。
除了库函数,我们还可以编写自己的函数,拓展程序的功能。自己编写的函数称为自定义函数。
3、C语言函数声明以及函数原型
C语言代码由上到下依次执行,原则上函数定义要出现在函数调用之前,否则就会报错。
但在实际开发中,经常会在函数定义之前使用它们,这个时候就需要提前声明。
所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。
函数声明的格式非常简单,相当于去掉函数定义中的函数体,并在最后加上分号;
dataType functionName( dataType1 param1, dataType2 param2 ... );
//也可以不写形参,只写数据类型:
dataType functionName( dataType1, dataType2 ... );
函数声明给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息,称为函数原型(Function Prototype)
。
有了函数声明,函数定义就可以出现在任何地方了,甚至是其他文件、静态链接库、动态链接库等。
【实例1】定义一个函数 sum(),计算从 m 加到 n 的和,并将 sum() 的定义放到 main() 后面。
#include <stdio.h>
//函数声明
int sum(int m, int n); //也可以写作int sum(int, int);
int main(){
int begin = 5, end = 86;
int result = sum(begin, end);
printf("The sum from %d to %d is %d\n", begin, end, result);
return 0;
}
//函数定义
int sum(int m, int n){
int i, sum=0;
for(i=m; i<=n; i++){
sum+=i;
}
return sum;
}
然而在实际开发中,往往都是上千行、上万行、上百万行的代码,将这些代码都放在一个源文件中简直是灾难,不但检索麻烦,而且打开文件也很慢,所以必须将这些代码分散到多个文件中。
对于多个文件的程序,通常是将函数定义放到源文件(.c文件)中,将函数的声明放到头文件(.h文件)中,使用函数时引入对应的头文件就可以,编译器会在链接阶段找到函数体。
4、C语言递归函数
一个函数在它的函数体内调用它自身称为递归调用,这种函数称为递归函数。
执行递归函数将反复调用其自身,每调用一次就进入新的一层,当最内层的函数执行完毕后,再一层一层地由里到外退出。
下面我们通过一个求阶乘的例子,看看递归函数到底是如何运作的。阶乘 n! 的计算公式如下:
#include <stdio.h>
//求n的阶乘
long factorial(int n) {
if (n == 0 || n == 1) {
return 1;
}else {
return factorial(n - 1) * n; // 递归调用
}
}
int main() {
int a;
printf("Input a number: ");
scanf("%d", &a);
printf("Factorial(%d) = %ld\n", a, factorial(a));
return 0;
}
递归函数有巨大的时间开销和内存开销,有这种玩儿法,但不推荐大家在项目中去使用,一般情况下我们都是用循环去解决问题,你仔细想想其实据递归不就是另类的循环吗:
//求阶乘
long factorial(int n){
int i;
long result=1;
for(i=1; i<=n; i++){
result *= i;
}
return result;
}
return 0和exit(0)的区别
简单来说:
exit(0):退出程序;
return 0:退出函数,返回值。
详细说:
1)return返回函数值,是关键字; exit 是一个函数。
2)return是语言级别的,它表示了调用堆栈的返回;而exit是系统调用级别的,它表示了一个进程的结束。
3)return是函数的退出(返回);exit是进程的退出。
4)return是C语言提供的,exit是操作系统提供的(或者函数库中给出的)。
5)return用于结束一个函数的执行,将函数的执行信息传出给其他调用函数使用;exit函数是退出应用程序。
6)非主函数中调用return和exit效果很明显,但是在main函数中调用return和exit的现象就很模糊,多数情况下现象都是一致的。
指针
1、什么是指针
C语言用变量来存储数据,用函数定义一段可以重复使用的代码,它们最终都要放到内存中才能供 CPU 使用。
不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。
为了正确地访问这些数据和指令,必须为每个字节空间上都编上号码,CPU运算时根据编号可以准确地找到对应的数据和指令,这个编号前面讲过称为内存地址(Address)。
CPU 读取指令和数据时需要的是地址,不是变量名和函数名!
变量名和函数名只是地址的一种助记符,当源程序文件被编译和链接成可执行程序后,它们都会被替换成地址。
假设变量 a、b、c 在内存中的地址分别是 0X1000、0X2000、0X3000,
那么加法运算c = a + b;将会被转换成类似的形式:0X3000 = (0X1000) + (0X2000);
( )表示取值操作,整个表达式的意思是,取出地址 0X1000 和 0X2000 上的值,将它们相加,把相加的结果保存到地址为 0X3000 的内存空间。
普通变量名、函数名、字符串变量名和数组变量名在本质上是一样的,它们都是地址的助记符,在编写代码的过程中,我们认为普通变量名表示的是数据本身,而函数名、字符串变量名和数组变量名表示的是代码块或数据块的首地址。
指针就是内存地址,是变量的地址,或函数的入口地址。
指针变量的定义和使用
如果一个变量里储的值是另外一份存储数据的内存块的首地址,我们就称它为指针变量。
指针变量的值就是保存了某份数据的内存空间的首地址,这样的一份数据可以是数组、字符串、函数,也可以是另外的一个普通变量或指针变量。
定义指针变量
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:
datatype *name;
或者
datatype *name = value;
*表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型 。例如:
int *p1;
p1 是一个指向 int 类型数据的指针变量,至于 p1 究竟指向哪一份数据,应该由赋予它的值决定。再如:
int a = 100;
int *p_a = &a;
在定义指针变量 p_a 的同时对它进行初始化,并将变量 a 的地址赋予它,此时 p_a 就指向了 a。
和普通变量一样,指针变量也可以被多次写入,只要你想,随时都能够改变指针变量的值,请看下面的代码:
//定义普通变量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
//定义指针变量
float *p1 = &a;
char *p2 = &c;
//修改指针变量的值
p1 = &b;
p2 = &d;
*
是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。
而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,
后边可以像使用普通变量一样来使用指针变量。
也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*。
假设变量 a、b、c、d 的地址分别为 0X1000、0X1004、0X2000、0X2004,下面的示意图很好地反映了 p1、p2 指向的变化:
需要强调的是,p1、p2 的类型分别是float *和char *,而不是float和char,它们是完全不同的数据类型!
指针变量也可以连续定义,例如:
int *a, *b, *c; //a、b、c 的类型都是 int*
通过指针变量取得和修改指针变量指向的内存地址里保存的数据
指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:pointer;
这里的称为指针运算符,用来取得某个地址上的数据,请看下面的例子:
#include <stdio.h>
int main(){
int a = 15;
int *p_a = &a;
printf("%d, %d\n", a, *p_a); //两种方式都可以输出a的值
return 0;
}
假设变量 a、p_a 的地址分别为 0X1000、0XF0A0,它们的指向关系如下图所示:
程序被编译和链接后,a、p_a 被替换成相应的地址。使用 *p_a 的话,要先通过地址 0XF0A0 取得变量 p_a 保存的值,这个值是变量 a 的地址,然后再通过这个值取得变量 a 的数据,前后共有两次运算;而使用 a 的话,可以通过地址 0X1000 直接取得它的数据,只需要一步运算。
也就是说,使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。
指针除了可以获取内存上的数据,也可以修改内存上的数据,例如:
#include <stdio.h>
int main(){
int a = 15, b = 99, c = 222;
int *p = &a; //定义指针变量
*p = b; //通过指针变量修改内存上的数据
c = *p; //通过指针变量获取内存上的数据
printf("%d, %d, %d, %d,%#X\n", a, b, c, *p, p); //99,99,99,99,0X61FE0C
return 0;
}
2,‘*’总结:
1)定义指针变量时的和使用指针变量时的意义完全不同。
int *p = &a;
*p = 100;
2)给指针变量本身赋值时不能加*。
int *p;
p = &a;
*p = 100;
3)指针变量也可以出现在普通变量能出现的任何表达式中。
int x, y, *px = &x, *py = &y;
y = *px + 5; //表示把x的内容加5并赋给y,*px+5相当于(*px)+5
y = ++*px; //px的内容加上1之后赋给y,++*px相当于++(*px)
y = *px++; //相当于y=*(px++)
py = px; //把一个指针的值赋给另一个指针
关于 * 和 & 的谜题
假设有一个 int 类型的变量 a,pa 是指向它的指针,那么*
&a和&*
pa分别是什么意思呢?
*
&a可以理解为*
(&a),&a表示取变量 a 的地址(等价于 pa),*
(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*
&a仍然等价于 a。
&*
pa可以理解为&(*
pa),*
pa表示取得 pa 指向的数据(等价于 a),&(*
pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa。
对星号的总结*
在我们目前所学到的语法中,星号*主要有三种用途:
1)表示乘法,例如int a = 3, b = 5, c; c = a * b;,这是最容易理解的。
2)表示定义一个指针变量,以和普通变量区分开,例如int a = 100; int *p = &a;。
3)表示获取指针指向的数据,是一种间接操作,例如int a, b, *p = &a; *p = 100; b = *p;。
3、指针变量的运算
指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行运算,例如加法、减法、比较等,请看下面的代码:
#include <stdio.h>
int main(){
int a = 10, *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char c = '@', *pc = &c;
//最初的值
printf("&a=%d, &b=%d, &c=%d\n", &a, &b, &c); //&a=-101024084, &b=-101024096, &c=-101024097
printf("pa=%d, pb=%d, pc=%d\n", pa, pb, pc); //&a=-101024084, &b=-101024096, &c=-101024097
//加法运算
pa++; pb++; pc++;
printf("pa=%d, pb=%d, pc=%d\n", pa, pb, pc); //pa=-101024080, pb=-101024088, pc=-101024096
//减法运算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%d, pb=%d, pc=%d\n", pa, pb, pc); //pa=-101024088, pb=-101024104, pc=-101024098
//比较运算
if(pa == paa){
printf("pa和paa都指向同一个地址的数据:%d\n", *paa);
}else{
printf("pa和paa都指向的不是同一个地址的数据,*pa:%d,*paa:%d\n", *pa,*paa); //*pa:4199705,*paa:10
}
return 0;
}
从运算结果可以看出:pa、pb、pc 每次加 1,它们的地址分别增加 4、8、1,正好是 int、double、char 类型的长度;减 2 时,地址分别减少 8、16、2,正好是 int、double、char 类型长度的 2 倍。
----这很奇怪,指针变量加减运算的结果跟数据类型的长度有关,而不是简单地加 1 或减 1,这是为什么呢?
-----以 a 和 pa 为例,a 的类型为 int,占用 4 个字节,pa 是指向 a 的指针,如下图所示:
刚开始的时候,pa 指向 a 的开头,通过 *pa 读取数据时,从 pa 指向的位置向后移动 4 个字节,把这 4 个字节的内容作为要获取的数据,这 4 个字节也正好是变量 a 占用的内存。
如果pa++;使得地址加 1 的话,就会变成如下图所示的指向关系:
这个时候 pa 指向整数 a 的中间,*pa 使用的是红色虚线画出的 4 个字节,其中前 3 个是变量 a 的,后面 1 个是其它数据的,把它们“搅和”在一起显然没有实际的意义,取得的数据也会非常怪异。
如果pa++;使得地址加 4 的话,正好能够完全跳过整数 a,指向它后面的内存,
-----我们知道,数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义,这个有意义的应用我们后面在具体写demo来研究。
------不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,做加减运算没有意义,因为不知道它后面指向的是什么数据。
------指针变量除了可以参与加减运算,还可以参与比较运算。当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。
-----另外需要说明的是,不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。
###4、数组指针
数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以int arr[] = { 99, 15, 100, 888, 252 };为例,该数组在内存中的分布如下图所示:
定义数组时,要给出数组名和数组长度,数组变量名可以被认为就是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向:
结合我们前面分析的指针变量的加减运算,循环遍历数组元素,我们还可以这样干:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int len = sizeof(arr) / sizeof(int); //求数组长度
int i;
for(i=0; i<len; i++){
printf("%d ", *(arr+i) ); //*(arr+i)等价于arr[i]
}
printf("\n");
return 0;
}
我们也可以定义一个指向数组的指针变量,例如:
int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr; //数组类型变量,就不建议在画蛇添足,前面在加一个&
arr 本身可以被认为是一个指针,可以直接赋值给指针变量 p:int *p = arr;
arr 是数组第 0 个元素的地址,所以int *p = arr;也可以写作int *p = &arr[0];。
也就是说,arr、p、&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头的内存地址。
如果一个指针变量指向了数组,我们就称它为数组指针变量(Array Pointer)。
数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *。
反过来想,p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于程序员的编码。
更改上面的代码,使用数组指针来遍历数组元素:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr
//int *p = (int *)&arr; //画蛇添足,没必有要这样写代码
//int *p = &arr[2]; //arr[2]是普通变量了,这时要加&去取它的地址
int len = sizeof(arr) / sizeof(int);
for(int i=0; i<len; i++){
printf("%d ", *(p+i) ); //99 15 100 888 252
printf("%d ",p[i]);
}
printf("\n");
return 0;
}
//执行结果:
99 99 15 15 100 100 888 888 252 252
//更改上面的代码,让 p 指向数组中的第二个元素:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int *p = &arr[2]; //也可以写作 int *p = arr + 2;
printf("%d, %d, %d, %d, %d\n", *(p-2), *(p-1), *p, *(p+1), *(p+2) );
return 0;
}
注意:不能使用指针【如:sizeof§ / sizeof(int)】计算数组长度.
总结访问数组元素的方法:
-
使用下标
arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,也可以使用 p[i] 来访问数组元素。 -
使用指针
使用 *(p+i) 的形式访问数组元素。另外数组名本身也可以认为是指针,使用 *(arr+i) 来访问数组元素。
注意:arr当成指针来用的时候,是个常量,不能修改,始终指向数组首地址。
关于数组指针的谜题
假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思呢?
*p++ 等价于两步 p; p++;,表示先取得第 n 个元素的值,再将 p 指向下一个元素。
++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
(*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。
5、字符串指针
C语言中没有专门的字符串类型,前面我们讲了可以用字符数组表示字符串,除此之外,
C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:
char *str = "helloworld!";
//等价于:
char *str; //定义指针变量
str = "helloworld!"; //把字符串的首地址赋给str变量。内部把这个字符串理解为一个数组也可以。
直接输出字符串
printf(“%s\n”, str);
//%s表示输出一个字符串,给出字符指针变量名str,则系统先输出它所指向的一个字符数据,
//然后自动使string加1,使之指向下一个字符,然后再输出一个字符,……,如此直到遇到字符串结束标志‘\0’为止。
和整数指针变量,不同哈,要区别,把字符串指针看成一个单独的特殊的类型!
int *i = 10; 不能等价于: int *i; i=10;
输出内容:sprintf("%d",i);不对的哈!
通过字符数组名或字符指针变量可以输出一个字符串。
而对一个数值型数组,是不能企图用数组名输出它的全部元素的。如:
int i[3] = {1,2,3}
printf("%d\n",i);
是不行的,只能逐个元素输出。
显然,字符串是特殊的!可以把字符串看作为一个整体来处理,可以对一个字符串进行整体的输入输出。
还可以用指针和数组的方式输出字符串:
-----我们知道,数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义,这个有意义的应用我们后面在具体写demo来研究。
------不过C语言并没有规定变量的存储方式,如果连续定义多个变量,它们有可能是挨着的,也有可能是分散的,这取决于变量的类型、编译器的实现以及具体的编译模式,所以对于指向普通变量的指针,做加减运算没有意义,因为不知道它后面指向的是什么数据。
------指针变量除了可以参与加减运算,还可以参与比较运算。当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。
-----另外需要说明的是,不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。
4、数组指针
数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以int arr[] = { 99, 15, 100, 888, 252 };为例,该数组在内存中的分布如下图所示:
定义数组时,要给出数组名和数组长度,数组变量名可以被认为就是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向:
结合我们前面分析的指针变量的加减运算,循环遍历数组元素,我们还可以这样干:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int len = sizeof(arr) / sizeof(int); //求数组长度
int i;
for(i=0; i<len; i++){
printf("%d ", *(arr+i) ); //*(arr+i)等价于arr[i]
}
printf("\n");
return 0;
}
我们也可以定义一个指向数组的指针变量,例如:
int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr; //数组类型变量,就不建议在画蛇添足,前面在加一个&
arr 本身可以被认为是一个指针,可以直接赋值给指针变量 p:int *p = arr;
arr 是数组第 0 个元素的地址,所以int *p = arr;也可以写作int *p = &arr[0];。
也就是说,arr、p、&arr[0] 这三种写法都是等价的,它们都指向数组第 0 个元素,或者说指向数组的开头的内存地址。
如果一个指针变量指向了数组,我们就称它为数组指针变量(Array Pointer)。
数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *。
反过来想,p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,究竟如何使用 p 取决于程序员的编码。
更改上面的代码,使用数组指针来遍历数组元素:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int *p = arr
//int *p = (int *)&arr; //画蛇添足,没必有要这样写代码
//int *p = &arr[2]; //arr[2]是普通变量了,这时要加&去取它的地址
int len = sizeof(arr) / sizeof(int);
for(int i=0; i<len; i++){
printf("%d ", *(p+i) ); //99 15 100 888 252
printf("%d ",p[i]);
}
printf("\n");
return 0;
}
//执行结果:
99 99 15 15 100 100 888 888 252 252
//更改上面的代码,让 p 指向数组中的第二个元素:
#include <stdio.h>
int main(){
int arr[] = { 99, 15, 100, 888, 252 };
int *p = &arr[2]; //也可以写作 int *p = arr + 2;
printf("%d, %d, %d, %d, %d\n", *(p-2), *(p-1), *p, *(p+1), *(p+2) );
return 0;
}
注意:不能使用指针【如:sizeof§ / sizeof(int)】计算数组长度.
总结访问数组元素的方法:
-
使用下标
arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,也可以使用 p[i] 来访问数组元素。 -
使用指针
使用 *(p+i) 的形式访问数组元素。另外数组名本身也可以认为是指针,使用 *(arr+i) 来访问数组元素。
注意:arr当成指针来用的时候,是个常量,不能修改,始终指向数组首地址。
关于数组指针的谜题
假设 p 是指向数组 arr 中第 n 个元素的指针,那么 *p++、*++p、(*p)++ 分别是什么意思呢?
*p++ 等价于两步 p; p++;,表示先取得第 n 个元素的值,再将 p 指向下一个元素。
++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
(*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。假设 p 指向第 0 个元素,并且第 0 个元素的值为 99,执行完该语句后,第 0 个元素的值就会变为 100。
5、字符串指针
C语言中没有专门的字符串类型,前面我们讲了可以用字符数组表示字符串,除此之外,
C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串,例如:
char *str = "helloworld!";
等价于:
char *str; //定义指针变量
str = "helloworld!"; //把字符串的首地址赋给str变量。内部把这个字符串理解为一个数组也可以。
直接输出字符串
printf("%s\n", str);
//%s表示输出一个字符串,给出字符指针变量名str,则系统先输出它所指向的一个字符数据,
//然后自动使string加1,使之指向下一个字符,然后再输出一个字符,……,如此直到遇到字符串结束标志‘\0’为止。
和整数指针变量,不同哈,要区别,把字符串指针看成一个单独的特殊的类型!
int *i = 10; 不能等价于: int *i; i=10;
输出内容:sprintf(“%d”,i);不对的哈!
通过字符数组名或字符指针变量可以输出一个字符串。
而对一个数值型数组,是不能企图用数组名输出它的全部元素的。
字符串是特殊的!可以把字符串看作为一个整体来处理,可以对一个字符串进行整体的输入输出。
还可以用指针和数组的方式输出字符串:
#include <stdio.h>
#include <string.h>
int main(){
char *str = "helloworld!";
int len = strlen(str), i;
//使用*(str+i)
for(i=0; i<len; i++){
printf("%c", *(str+i));
}
printf("\n");
//使用str[i]
for(i=0; i<len; i++){
printf("%c", str[i]);
}
printf("\n");
return 0;
}
看起来和字符数组是差不多,它们都可以使用%s输出整个字符串,都可以使用*或[ ]获取单个字符,这两种表示字符串的方式是不是就没有区别了呢?
有! 它们最根本的区别是在内存中的存储区域不一样,字符数组存储在可读写数据段(全局数据区)或栈区,指针形式的字符串存储在只读数据段(常量区)。全局数据区和栈区的字符串有读取和写入的权限,而常量区的字符串只有读取权限,没有写入权限。
内存权限的不同导致的一个明显结果就是,字符数组在定义后可以读取和修改每个字符,而对于指针形式的字符串,一旦被定义后就只能读取不能修改,任何对它的赋值都是错误的。
我们将指针形式的字符串称为字符串常量,意思很明显,常量只能读取不能写入。
#include <stdio.h>
int main(){
char *str = "Hello World!";
str[3] = 'P'; //错误
return 0;
}
#include <stdio.h>
int main(){
char *str = "Hello World!";
str = "abc123";
// *str = "abc123"; //有星号和没有星号的区别!
int len = sizeof(str)/sizeof(char); //数组长度,指针所指的字符串当数组了
int i;
printf("%s\n",str);
for(i=0;i<len;i++){
printf("%c",str[i]);
}
printf("\n");
return 0;
}
linux上
到底使用字符数组还是字符串常量
在编程过程中如果只涉及到对字符串的读取,那么字符数组和字符串常量都能够满足要求;
如果有写入(修改)操作,那么只能使用字符数组,不能使用字符串常量。
典型的选择字符数组的场景:获取用户输入的字符串,只能使用字符数组:
#include <stdio.h>
int main(){
char str[30];
gets(str);
printf("%s\n", str);
return 0;
}
***总结:***C语言有两种表示字符串的方法,一种是字符数组,另一种是字符串常量,它们在内存中的存储位置不同,使得字符数组可以读取和修改,而字符串常量只能读取不能修改。
指针变量作为函数参数
上一章,我们讲解了函数,但是参数传递中,我们演示的Demo都是用的值传递,现在学习了指针后,我们可以对函数参数传递做一个总结,函数参数传递分值传递和地址传递:
首先:我们再列一个值传递的例子
#include <stdio.h>
void swap(int x, int y){
int temp; //临时变量
temp = x;
x = y;
y = temp;
}
int main(){
int a = 66, b = 99;
swap(a, b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
//执行结果: a,b没有交换值
a = 66, b = 99
值传递是相当于复制一份数据,并没有改变数据值。
我们再来地址传递
#include <stdio.h>
void swap(int *p1, int *p2){
int temp; //临时变量
temp = *p1;
*p1 = *p2;
*p2 = temp;
}
int main(){
int a = 66, b = 99;
swap(&a, &b);
printf("a = %d, b = %d\n", a, b);
return 0;
}
//执行结果
a = 99, b = 66
对数组这样的大内存块,进行参数传递的时候必须用指针,计算机底层是不允许大内存块直接进行拷贝数据的,那样如果数组达到亿级时,程序卡死的怀颖人生:
#include <stdio.h>
int max(int *intArr, int len){ //数组长度要由调用者提供,值传递是拿不到的
int i, maxValue = intArr[0]; //假设第0个元素是最大值
for(i=1; i<len; i++){
if(maxValue < intArr[i]){
maxValue = intArr[i];
}
}
return maxValue;
}
int main(){
int nums[6]={22,11,55,88,99,66,33};
int len = sizeof(nums)/sizeof(int);
printf("Max value is %d!\n",max(nums,len));
return 0;
}
指针作为函数返回值
C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。
下面的例子定义了一个函数 longstr(),用来返回两个字符串中较长的一个:
#include <stdio.h>
//两个字符串,返回一个指针,指向字符串最长的那个
char * longstr(char * str1,char * str2){ //c语言中没有字符串String这种数据,只能用char数组或指针表示
if(strlen(str1)>= strlen(str2)){
return str1;
}else{
return str2;
}
}
int main(){
char a[30]={0},b[30]={0};
gets(a); //linux中,会有警告,提示gets是危险的,不建议使用,用了也不会错
gets(b);
char * rsstr = longstr(a,b);
printf("较长的字符为: %s\n",rsstr);
return 0;
}
用指针作为函数返回值时需要注意的一点是,函数运行结束后我们认为会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误,所以C编译器是不会通过编译的。
如下代码是不好的代码:
#include <stdio.h>
int *func(){
int n = 100;
return &n; //程序编译之前就会发出警告信息,提示将会发生不可知的错误。
}
int main(){
int *p = func(), n;
n = *p;
printf("value = %d\n", n);
return 0;
}
指向指针的指针,空指针NULL以及void指针
指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int *、double *、char * 等。
如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
假设有一个 int 类型的变量 a,p1是指向 a 的指针变量,p2 又是指向 p1 的指针变量,它们的关系如下图所示:
将这种关系转换为C语言代码:
int a =100;
int *p1 = &a;
int **p2 = &p1;
指针变量也是一种变量,也会占用存储空间,也可以使用&获取它的地址。
C语言不限制指针的级数,每增加一级指针,在定义指针变量时就得增加一个星号*。
p1 是一级指针,指向普通类型的数据,定义时有一个*;
p2 是二级指针,指向一级指针 p1,定义时有两个*。
如果我们希望再定义一个三级指针 p3,让它指向 p2,那么可以这样写:
int ***p3 = &p2;
四级指针也是类似的道理:
int ****p4 = &p3;
实际开发中会经常使用一级指针和二级指针,几乎用不到高级指针。
想要获取指针指向的数据时,一级指针加一个*,二级指针加两个*,三级指针加三个*,以此类推,请看代码:
#include <stdio.h>
int main(){
int a =100;
int *p1 = &a;
int **p2 = &p1;
int ***p3 = &p2;
printf("%d, %d, %d, %d\n", a, *p1, **p2, ***p3);
printf("&p2 = %#X, p3 = %#X\n", &p2, p3);
printf("&p1 = %#X, p2 = %#X, *p3 = %#X\n", &p1, p2, *p3);
printf(" &a = %#X, p1 = %#X, *p2 = %#X, **p3 = %#X\n", &a, p1, *p2, **p3);
return 0;
}
//执行结果
100, 100, 100, 100
&p2 = 0X61FE00, p3 = 0X61FE00
&p1 = 0X61FE08, p2 = 0X61FE08, *p3 = 0X61FE08
&a = 0X61FE14, p1 = 0X61FE14, *p2 = 0X61FE14, **p3 = 0X61FE14
1)空指针 NULL
一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只要把地址给它,它就可以指向,C语言没有一种机制来保证指向的内存的正确性,程序员必须自己提高警惕。
很多初学者会在无意间对没有初始化的指针进行操作,这是非常危险的,请看下面的例子:
#include <stdio.h>
int main(){
char *str; //声明了,没有初始化,局部变量,编译器会随机给一个垃圾值!
gets(str); //程序直接在这里就崩溃了!
printf("%s\n", str);
return 0;
}
不要直接使用未初始化的局部变量。
上面的代码中,str 就是一个未初始化的局部变量,它的值是不确定的,究竟指向哪块内存也是未知的,大多数情况下这块内存没有被分配或者没有读写权限,使用 gets() 函数向它里面写入数据当然会出错的。
#include <stdio.h>
int main(){
char as[30];
char *str = as; //声明了,没有初始化,局部变量,编译器会随机给一个垃圾值!
gets(str); //程序直接在这里就崩溃了!
printf("您输入的字符:%s\n", str);
return 0;
}
程序很复杂了后,遇到不确定初始值的指针变量的时候一般情况下我们会赋值为NULL指针:
char *str=NULL;
NULL 是“零值、等于零”的意思,在C语言中表示空指针。
注意区分大小写,null 没有任何特殊含义。
#include <stdio.h>
int main(){
char *str = NULL; //这里不定义空指针的话,会卡在这里
gets(str); //系统不作处理,即不会让我们输入数据
printf("%s\n", str);
return 0;
}
程序不会阻塞,我们有理由怀疑gets()函数,对传入的变量,进行了判断如果是NULL,就什么也不做,当然就不会阻塞程序等用户输入了!
我们在自己定义的函数中也可以进行类似的判断,例如:
void func(char *p){
if(p == NULL){
printf("(null)\n");
}else{
printf("%s\n", p);
}
}
这样能够从很大程度上增加程序的健壮性,防止程序意外崩溃或出错。
2)NULL到底是什么?void指针
NULL 是在stdio.h中定义的一个宏,它的具体内容为:
#define NULL ((void *)0)
(void *)0表示把数值 0 强制转换为void *类型,最外层的( )把宏定义的内容括起来,防止发生歧义。
从整体上来看,NULL 指向了地址为 0 的内存。
最低地址处有一段内存区域被称为保留区,这个区域不存储有效数据,也不能被用户程序访问,将 NULL 指向这块区域很容易检测到违规指针。
注意,C语言没有规定 NULL 的指向,只是大部分标准库约定成俗地将 NULL 指向 0,所以不要将 NULL 和 0 等同起来,例如下面的写法是不专业的:
int *p = 0;
而应该坚持写为:
int *p = NULL;
注意 NULL 和 NUL 的区别:NULL 表示空指针,是一个宏定义,可以在代码中直接使用。而 NUL 表示字符串的结束标志 ‘\0’,它是ASCII码表中的第 0 个字符。NUL 没有在C语言中定义,仅仅是对 ‘\0’ 的称呼,不能在代码中直接使用。
void
用在函数定义中可以表示函数没有返回值或者没有形式参数,
用在指针里,表示指向的数据的类型是未知的。
也就是说,void *表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换。
#include <stdio.h>
#include <string.h>
void *strlong(void *str1, char *str2){
if(strlen(str1) >= strlen(str2)){
return str1;
}else{
return str2;
}
}
int main(){
char str1[30], str2[30], *str;
gets(str1);
gets(str2);
str = (char *)strlong(str1, str2);
printf("Longer string: %s\n", str);
return 0;
}
总结:void *是一种指针类型,我们可以将别的类型的指针无需强制类型转换的赋值给void *类型。也可以将void *强制类型转换成任何别的指针类型,至于强转的类型是否合理,就需要我们程序员自己控制了。
###指针数组
-----如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。
指针数组的定义形式一般为:
dataType * arrayName[length];
----除了每个元素的数据类型是指针以外,指针数组和普通数组在其他方面都是一样的,下面是一个简单的例子:
#include <stdio.h>
int main(){
int a = 16, b = 932, c = 100;
//定义一个指针数组
int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *arr[]
for(int i=0;i<3;i++){
printf("%d\n",*arr[i]);
}
return 0;
}
指针数组还可以和字符串数组结合使用,请看下面的例子:
#include <stdio.h>
int main(){
char *str[3] = {
"hello",
"world",
"c language"
};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}
字符数组 str 中存放的是字符串的首地址,不是字符串本身,字符串本身位于其他的内存区域,和字符数组是分开的,等价写法:
#include <stdio.h>
int main(){
char *str0 = "hello";
char *str1 = "world";
char *str2 = "c language";
char *str[3] = {str0, str1, str2};
printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
return 0;
}
练习
#include <stdio.h>
int main(){
char * lines[6] = {
"xiongshaowen",
"xuhuifeng",
"熊少文",
"徐会凤",
"isxuhuifengxiongshaowen",
"fuqi"
};
char * str1 = lines[1];
char * str2 = *(lines +3); //先让lines移动3个位置,再用*取出字符串值,再赋给str2地址指向它
char c1 = *(*(lines+4)+6); //‘isxuhuifengxiongshaowen"的第七个字符
char c2 =(*lines + 5)[5]; //'xiongshaowen'的首地址 移5位到,再取它所指地址后的当一个数组的第六个元素即e
char c3 = *lines[0] +2; // 'x'+2,即x的ASSIC值加2,变为z
char c4 = *(lines+1)[0]+2; //第二个指针指向的字符串的第一个元素x+2,即也为z
printf("str1= %s\n",str1); //xuhuifeng
printf("str2= %s\n",str2); //徐会凤
printf(" c1= %c\n",c1); //i
printf(" c2= %c\n",c2); //e
printf(" c3= %c\n",c3); //z
printf(" c4= %c\n",c4); //z
}
二维数组指针(指向二维数组的指针)
二维数组在概念上是二维的,有行和列,但在内存中所有的数组元素都是连续排列的,它们之间没有“缝隙”。
以下面的二维数组 a 为例:
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
从概念上理解,a 的分布像一个矩阵:
0 1 2 3 4 5 6 7 8 9 10 11
但在内存中,a 的分布是一维线性的,整个数组占用一块连续的内存:
另外,逻辑上二维数组是二维的,但是本质上C语言允许把一个二维数组分解成多个一维数组来处理。
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
总结:数组指针,指针数组,二维数组指针!
数组指针:指针变量指向一维数组中某个元素,这个指针变量就是数组指针。
int a[]={1,2,3};
int *pa = a;
指针数组:数组里的元素都是指针叫指针数组。
dataType * arrayName[length];
二维数组指针:指针变量指向二维数组中的某个元素。
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a;
括号中的表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。
[ ]的优先级高于,( )是必须要加的,如果赤裸裸地写作int * p[4],p 就成了一个指针数组,而不是二维数组指针。
二维数组指针进行加法(减法)运算时,它前进(后退)的步长与它指向的数据类型有关,p 指向的数据类型是int [4],那么p+/-1就移动 4×4 = 16 个字节,在二维数组来看,就是指针加减1,指针就移动一行。
二维数组用下标获取元素前面讲过了,这里我们讨论如何使用指针 p 来访问二维数组中的每个元素
- p指向数组 a 的开头,也即第 1 行,下标为0;p+1前进一行,指向第 2 行,下标为1。
- *(p+1)表示取地址上的数据,也就是整个第 2 行数据。注意是一行数据,是多个数据,不是第 2 行中的第 1 个元素,下面的运行结果有力地证明了这一点:
#include <stdio.h>
int main(){
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a; //定义二维指针数组
printf("%d\n", sizeof(*(p+1))); //16,表示这个数据有16个字节,4个int数据
//用数组指针遍历二维数组
int i=0,j=0;
for(i=0;i<3;i++){
for(j=0;j<4;j++){
printf("%-4d",*(*(p+i)+j));
}
printf("\n");
}
return 0;
}
//执行结果
16
0 1 2 3
4 5 6 7
8 9 10 11
这里有问题需要讨论一下:
数组变量名,二维数组指针加减法运算后的结果,有时候表示的是一个指针(就是一个内存地址),
有的时候表示的是数组本身,一个整体的数组,你们自己要灵活的理解,正确的理解,
当我们数组变量名和上面的二维数组指针在定义的时候,在做sizeof函数的参数的时候,代表的是整体的数组本身,
当我们把数组变量名和上面的二维数组指针放在表达式里的时候,代表是就是一个具体的指针(就是一个内存地址)
- *(p+1)+1表示第2 行第2 个元素的地址, ((p+1)+1)表示第 2 行第 2 个元素的值。如何理解呢?
#include <stdio.h>
int main(){
int a[3][4] = { {0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11} };
int (*p)[4] = a;
printf("%d",*(*(p+1)+1));
return 0;
}
总结:根据上面的结论,使用指针遍历二维数组。
#include <stdio.h>
int main(){
int a[3][4]={0,1,2,3,4,5,6,7,8,9,10,11};
int(*p)[4];
int i,j;
p=a;
for(i=0; i<3; i++){
for(j=0; j<4; j++){
printf("%-4d ",*(*(p+i)+j));
}
printf("\n");
}
return 0;
}
函数指针(指向函数的指针)
一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。
我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。
函数指针的定义形式为:
returnType (*pointerName)(参数列表);
第一个括号不能省略,如果写作returnType *pointerName(参数列表);就成了函数原型,它表明函数的返回值类型为returnType *。
#include <stdio.h>
//返回两个数中较大的一个
int max(int a, int b){
return a>b ? a : b;
}
int main(){
//定义函数指针
int (*pmax)(int, int) = max; //也可以写作int (*pmax)(int a, int b)
int maxval = (*pmax)(11, 12); //调用语法指针变量前面加星号。
printf("Max value: %d\n", maxval);
return 0;
}
函数指针一般的时候用不到,它的好处有提供调用的灵活性,提供封装性,避免命名冲突!
###main()函数高级用法
main() 是C语言程序的入口函数,有且只能有一个,它实际上有两种标准的原型:
int main();
int main(int argc, char *argv[]);
前面的课程中我们一直使用第一种原型,它简单易懂,能让初学者很快入手。
第二种原型在实际开发中也经常使用,它能够让我们在程序启动时给程序传递数据。
#include <stdio.h>
int main(int argc, char *argv[]){
int i;
printf("The program receives %d parameters:\n", argc);
for(i=0; i<argc; i++){
printf("%s\n", argv[i]);
}
return 0;
}
指针的总结
怎么样对付这些复杂指针:
C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。注意,从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键!
对于初学者,有几种运算符的优先级非常容易混淆,它们的优先级从高到低依次是:
1)定义中被括号( )括起来的那部分。
2)后缀操作符:括号( )表示这是一个函数,方括号[ ]表示这是一个数组。
3)前缀操作符:星号*表示“指向xxx的指针”。
练习理解下面的含有指针的表达式:
-
int * p1[6];
从 p1 开始理解,它的左边是 *,右边是 [ ],[ ] 的优先级高于 *,所以编译器先解析p1[6],p1 首先是一个拥有 6 个元素的数组,然后再解析int *,它用来说明数组元素的类型。它指针数组。 -
int (*p3)[6];
从 p3 开始理解,( ) 的优先级最高,编译器先解析(*p3),p3 首先是一个指针,剩下的int [6]是 p3 指向的数据的类型,它是二维数组指针。 -
int (*p4)(int a, int b);
从 p4 开始理解,( ) 的优先级最高,编译器先解析(*p4),p4 首先是一个指针,它后边的 ( ) 说明 p4 指向的是一个函数,括号中的int, int是参数列表,开头的int用来说明函数的返回值类型。整体来看,p4 是一个指向原型为int func(int, int);的函数的指针,它是函数指针, -
char ( c[10])(int **p);
这个定义有两个名字,分别是 c 和 p,首先确认那个是主变量名,一般两个括号,后面的括号是函数后缀,里边是函数的参数。
应该是以 c 作为主变量的名字,先来看括号内部(绿色粗体):
char * (* c[10]) (int **p);
[ ] 的优先级高于 ,编译器先解析c[10],c 首先是一个数组,它前面的表明每个数组元素类型都是一个指针,说明 c 是一个指针数组,只是指针指向的数据类型尚未确定。
把指针数组抠出来,看剩下的部分:
char * (* c[10]) (int **p);
绿色粗体表明 c 是一个指针数组,红色粗体表明指针指向的数据类型,合起来就是:c 是一个拥有 10 个元素的指针数组,每个指针指向一个原型为char *func(int **p);的函数。
- int (((*pfunc)(int ))[5])(int );
从 pfunc 开始理解,先看括号内部(绿色粗体):
int (((*pfunc)(int *))[5])(int *);
pfunc 是一个指针。
跳出括号,看它的两边(红色粗体):
int (((*pfunc)(int *))[5])(int *);
根据优先级规则应该先看右边的(int *),它表明这是一个函数,int 是参数列表。再看左边的,它表明函数的返回值是一个指针,只是指针指向的数据类型尚未确定。
把上面已经理解的整个部分扣掉,看剩下的部分就应该是说明函数返回什么类型的指针。
int (* (*(*pfunc)(int *)) [5])(int *);
我们接着分析,再向外跳一层括号(红色粗体):
int (* (*(*pfunc)(int *)) [5])(int );
[ ] 的优先级高于 ,先看右边,[5] 表示这是一个数组,再看左边, 表示数组的每个元素都是指针。也就是说, [5] 是一个指针数组,函数返回的指针就指向这样一个数组。
那么,指针数组中的指针又指向什么类型的数据呢?再向外跳一层括号(橘黄色粗体):
int (* (*(*pfunc)(int *)) [5]) (int *);
先看橘黄色部分的右边,它是一个函数,再看左边,它是函数的返回值类型。也就是说,指针数组中的指针指向原型为int func(int *);的函数。
将上面的三部分合起来就是:pfunc 是一个函数指针(蓝色部分),该函数的返回值是一个指针,它指向一个指针数组(红色部分),指针数组中的指针指向原型为int func(int *);的函数(橘黄色部分)。
预处理命令和头文件编写
预处理命令
1、预处理命令和预处理的意义
前面各章中,已经多次使用过#include命令。
使用库函数之前,应该用#include引入对应的头文件。
这种以#号开头的命令称为预处理命令。
回顾用编译器gcc,从一个C语言的源文件到执行,要经历的过程:
1、编写C源文件
2、预处理 gcc -E hello.c > hello.i
3、编译 gcc -S hello.i
4、汇编 gcc -c hello.s
5、链接 gcc hello.o -o hello
6、可执行文件 hello 就可以执行当前目标下的hello了。
预处理就是在编译之前还需要对源文件进行简单的处理,为编译做准备,这种准备有时候是非常有必要,有意义的:
***例如,***我们希望自己的程序在 Windows 和 Linux 下都能够运行, Windows 下使用 Visual Studio 编译,然后在 Linux 下使用 GCC 编译。不同编译器支持的函数可能不同的,现在有个问题,程序中的某个功能,假设 win平台下使用 a(),linux平台下使用 b(),win下的函数在 linux下不能编译通过,反之亦然,怎么办呢?
这就需要在编译之前先对源文件进行处理:
如果检测到是 VS,就保留 a() 删除 b();如果检测到是 GCC,就保留 b() 删除 a()。
#include <stdio.h>
int main() {
//不同的平台下调用不同的函数
#if _WIN32 //识别windows平台
a();
#elif __linux__ //识别linux平台
b();
#endif
return 0;
}
2、#include命令的使用
#include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种。
#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。
#include 的用法有两种,如下所示:
#include <stdHeader.h>
#include "myHeader.h"
使用尖括号< >和双引号" “的区别在于头文件的搜索路径不同:
1)使用尖括号< >,编译器会到系统路径下查找头文件;
2)而使用双引号” ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。一般的情况下,我们引入系统头文件就用<>,引入我们自己编程的头文件就用“”。
关于 #include 用法的注意事项:
1)一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。
2)同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。一般情况下系统头文件,都可以重复引入,不得出错。同时也要注意,我们自己编写头文件的时候,也要做防止重入引入的机制。
3)文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
#include 用法举例
引入系统头文件没什么好讲的,这里我们玩玩引入自己编写的头文件,编写三个文件main.c;my.h;my.c:
my.c 所包含的代码:
int a = 100; //申明和定义和赋值一体,对应OS会分配内存空间
//计算从m加到n的和
int sum(int m, int n) {
int i, sum = 0;
for (i = m; i <= n; i++) {
sum += i;
}
return sum;
}
my.h 所包含的代码:
//声明变量
extern int a; //只是申明:OS不会分配内存空间!
//声明函数
extern int sum(int m, int n); //没有函数体,同样OS不会分配内存空间
main.c 所包含的代码:
#include <stdio.h>
#include "my.h"
int main() {
printf("%d %d\n", a,sum(1, 100));
return 0;
}
//执行结果:
//编译
gcc *.c
./a
//结果
100 5050
注意:该三个文件要放在一个目录下,不能gcc -g main.c -o main.exe,不然会出现undefined reference to sum' C:\Users\ADMINI~1\AppData\Local\Temp\ccU25TQp.o:main.c:(.rdata$.refptr.a[.refptr.a]+0x0): undefined reference to
a’ 错误,要gcc *
.c
运行—启动调试,注意的问题与处理方法:
在当前这个目录有多文件并需要其它文件包进来时,我们打开一个文件如main.c,再启动调是有问题的,打不开调试终端,这样我们也没办法gcc了,
处理方法:
打开另一个文件,没有包含(include)文件的c文件,然后,启动调试,保证打开了终端,我们在终端中再切换到main.c所在的目录中,gcc *.c
我们在 my.c 中定义了 sum() 函数,在 my.h 中声明了 sum() 函数,大多数情况下头文件里只能包含变量和函数的声明,不能包含定义,定义都是放在别的源文件里做。
C语言中的宏定义
#define
叫做宏定义命令,它也是C语言预处理命令的一种。
所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就在预处理阶段全部替换成指定的字符串。
我们先通过一个例子来看一下 #define 的用法:
#include <stdio.h>
#define N 100 //前面讲的符号常量,本质就是宏,宏这个字符串不需要用双引号,这个是特殊的
int main(){
int sum = 20 + N;
printf("%d\n", sum);
return 0;
}
宏定义的一般形式为:
#define 宏名 字符串
#表示这是一条预处理命令,所有的预处理命令都以 # 开头。
宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等。
这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。
程序中反复使用的表达式就可以使用宏定义,例如:
#include <stdio.h>
#define M (n*n+3*n)
int main(){
int sum, n;
printf("Input a number: ");
scanf("%d", &n);
sum = 3*M+4*M+5*M;
printf("sum=%d\n", sum);
return 0;
}
程序的开头首先定义了一个宏 M,它表示 (nn+3n) 这个表达式。
在7 行代码中使用了宏 M,预处理程序将它展开为下面的语句:
sum=3*(n*n+3*n)+4*(n*n+3*n)+5*(n*n+3*n);
需要注意的是,在宏定义中表达式(nn+3n)两边的括号不能少,否则在宏展开以后可能会产生歧义。
下面是一个反面的例子:
#difine M n*n+3*n
在宏展开后将得到下述语句:
s=3*n*n+3*n+4*n*n+3*n+5*n*n+3*n;
和原来预想的表达式,就相差很远了,所以进行宏定义时要注意,应该保证在宏替换之后不发生歧义。
对 #define 用法的几点说明
-
宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。
字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,
如有错误,只能在编译已被宏展开后的源程序时发现。 -
宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
-
宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。
如要终止其作用域可使用#undef命令。例如:
#define PI 3.14159
int main(){
// Code
return 0;
}
#undef PI
void func(){
// Code
}
- 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替,例如:
#include <stdio.h>
#define OK 100
int main(){
printf("OK\n");
return 0;
}
该例中定义宏名 OK 表示 100,但在 printf 语句中 OK 被引号括起来,因此不作宏替换,而作为字符串处理。
表示 PI 只在 main() 函数中有效,在 func() 中无效。
5) 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。
例如:
#define PI 3.1415926
#define S PI*y*y /* PI是已定义的宏名*/
对语句:
printf("%f", S);
在宏代换后变为:
printf("%f", 3.1415926*y*y);
-
习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
-
可用宏定义表示数据类型,使书写方便。例如:
#define UINT unsigned int
在程序中可用 UINT 作变量说明:
UINT a, b;
讲到#define简化数据类型的标记,不得不联想到另一个关键字:typedef,它的作用是为一种数据类型定义一个新名字,但是它和#define是本质的不同的:
宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。
另外:typedef本身是一种存储类的关键字,与auto、extern、static、register等关键字不能一起使用。
例子:
#define PIN1 int *
typedef int *PIN2;
从形式上看这两者相似, 但在实际使用中却不相同。
下面用 PIN1,PIN2 说明变量时就可以看出它们的区别:
PIN1 a, b;
在宏代换后变成:
int * a, b;
表示 a 是指向整型的指针变量,而 b 是整型变量。
然而:
PIN2 a,b;
表示 a、b 都是指向整型的指针变量。
因为 PIN2 是一个新的、完整的数据类型。
由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟只是简单的字符串替换。
在使用时要格外小心,以避出错。
8)宏定义是可以包含多条语句的,要换行用\
#include <stdio.h>
#define MYCODE int n=0;int sum=0;\
printf("Input a number: ");\
scanf("%d",&n);\
sum = n*n+3*n;\
printf("sum=%d\n", sum);
int main(){
MYCODE
return 0;
}
C语言带参数宏定义
C语言允许宏带有参数。
在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。
带参宏定义的一般形式为:
#define 宏名(形参列表) 字符串
在字符串中可以含有各个形参。
带参宏调用的一般形式为:
宏名(实参列表)
例如:
#include <stdio.h>
#define M(y) (y*y+3)
int main(){
int k = M(5);
return 0;
}
//执行编译
gcc -E define2.c > m001.i
//我们打开m001.i,到该文件尾部可看到宏展开的代码
// # 3 "define2.c"
// int main(){
// int k = (5*5 +3);
// return 0;
// }
在宏展开时,用实参 5 去代替形参 y,经预处理程序展开后的语句为k=(55+35)。
【示例】输出两个数中较大的数。
#include <stdio.h>
#define MAX(a,b) (a>b) ? a : b
int main(){
int x , y, max;
printf("input two numbers: ");
scanf("%d %d", &x, &y);
max = MAX(x, y);
printf("max=%d\n", max);
return 0;
}
对带参宏定义的说明
-
带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。
例如把:
#define MAX(a, b) (a>b)?a:b
不可以写为:
#define MAX (a,b) (a>b)?a:b
-
在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。
而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,
调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。
【示例】输入 n,输出 (n+1)^2 的值。
#include <stdio.h>
#define SQ(y) (y)*(y)
int main(){
int a, sq;
printf("input a number: ");
scanf("%d", &a);
sq = SQ(a+1);
printf("sq=%d\n", sq);//如果输入8,结果:81,说明可以调用参数值改变了,不存函数中的值传递问题
return 0;
}
- 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
例如上面的宏定义中 (y)*(y) 表达式的 y 都用括号括起来,因此结果是正确的。
如果去掉括号,把程序改为以下形式:
#include <stdio.h>
#define SQ(y) y*y
int main(){
int a, sq;
printf("input a number: ");
scanf("%d", &a);
sq = SQ(a+1);
printf("sq=%d\n", sq);//sq=a+1*a+1;如果输入8,结果:无
return 0;
}
***注意:***带参数的宏和函数很相似,但有本质上的区别,宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。
####宏参数的字符串化和宏参数的连接
在宏定义中,有时还会用到#和##两个符号,它们能够对宏参数进行操作。
#
用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。
例如有如下宏定义:
#define STR(s) #s
那么:
printf("%s", STR(helloworld)); //这里没有双引号
printf("%s", STR("helloworld")); //这里有双引号
在预处理时分别被替换为:
printf("%s", "helloworld");
printf("%s", "\"helloworld\""); //多了转义字符
使用#会在两头添加新的引号,而原来的引号会被转义。
##
称为连接符,用来将宏参数或其他的串连接起来。
例如有如下的宏定义:
#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00
那么:
printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));
将被展开为:
printf("%f\n", 8.5e2); //850.000000
printf("%d\n", 123400);
####C语言中几个预定义宏
ANSI发布的C语言标准里规定了以下几个预定义宏,它们在各个编译器下都可以使用:
__LINE__:表示当前源代码的行号;
__FILE__:表示当前源文件的名称;
__DATE__:表示当前的编译日期;
__TIME__:表示当前的编译时间;
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
__cplusplus__:当编写C++程序时该标识符被定义。
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Date : %s\n", __DATE__);
printf("Time : %s\n", __TIME__);
printf("File : %s\n", __FILE__);
printf("Line : %d\n", __LINE__);
system("pause");
return 0;
}
//执行结果
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling> gcc -g define6.c -o define6.exe
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling> ./define6
Date : Apr 17 2024
Time : 11:48:14
File : define6.c
Line : 7
C语言条件编译
假如现在要开发一个C语言程序,让它输出红色的文字,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?
这个程序的难点在于,不同平台下控制文字颜色的代码不一样,我们必须要能够识别出不同的平台。
windows平台上:
#include <stdio.h>
#include <windows.h>
int main(int argc, char *argv[]) {
HANDLE hconsole; // HANDLE:是一个类型,顾名思义是一个持有资源的句柄
hconsole = GetStdHandle(STD_OUTPUT_HANDLE); // 这句话的意思,获取一个持有标准输出的资源句柄
// SetConsoleTextAttribute函数作用设置前进色和背景色
SetConsoleTextAttribute(hconsole,0x4a); //0x4a:4表示背景色,a表示前景色
printf("Hello,World!\n");
return 0;
}
//颜色值:
0 = 黑色 8 = 灰色
1 = 蓝色 9 = 淡蓝色
2 = 绿色 A = 淡绿色
3 = 湖蓝色 B = 淡浅绿色
4 = 红色 C = 淡红色
5 = 紫色 D = 淡紫色
6 = 黄色 E = 淡黄色
7 = 白色 F = 亮白色
linux平台上:
printf("\033[字背景颜色;字体颜色m字符串\033[0m" );
printf("\033[47;31mHello,World!\n\033[0m");
//47是字背景颜色, 31m是字体的颜色, Hello,World!\n是字符串\033[0m是控制码.
//部分颜色代码:
//字背景颜色: 40--49 // 字颜色: 30--39
40: 黑 30: 黑
41: 红 31: 红
42: 绿 32: 绿
43: 黄 33: 黄
44: 蓝 34: 蓝
45: 紫 35: 紫
46: 深绿 36: 深绿
47:白色 37:白色
Windows 有专有的宏_WIN32,Linux 有专有的宏__linux__,以现有的知识,我们很容易就想到了 if else,
预处理命令里也有分支语句:
#include <stdio.h>
int main(){
#if _WIN32
HANDLE hconsole;
hconsole = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(hconsole,0x7a); //0x4a:4表示背景色,a表示前景色
printf("Hello,World!\n");
#elif __linux__
printf("\033[47;42mHello,World!\n\033[22;30m");
#else
printf("Hello,World!\n");
#endif
return 0;
}
这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。
条件编译是预处理程序的功能,不是编译器的功能。
#ifdef 的用法
#ifdef 用法的一般格式为:
#ifdef 宏名 //当前的宏已被定义过
程序段1
#else
程序段2
#endif
例:如果定义了宏NUM1,则处理,没有则另处理。
#include <stdio.h>
#define NUM1 10
int main(){
#ifdef NUM1 //改:#ifdef NUM3,改: #ifndef 看看结果,这对判断宏有没有定义很管用。
//代码A
printf("NUM1: %d\n", NUM1);
#else
//代码B
printf("Error\n");
#endif
return 0;
}
#if、#ifdef、#ifndef三者之间的区别
#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。
例如,下面的形式只能用于 #if:
#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:
#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;
}
C语言#error命令
#error 指令用于在编译期间产生错误信息
,并阻止程序的编译,其形式如下:
#error error_message
例如:
#include <stdio.h>
int main() {
#ifdef _WIN32
#error this program cannot compile at win plateform!
#endif
return 0;
}
需要注意的是:报错信息不需要加引号" ",如果加上,引号会被一起输出。
#####pragma once
#pragma指令的作用是:用于指定计算机或操作系统特定的编译器功能。
#pragma有很多种用法,但确实很偏,这里我们先了解一下#pragma once,以后的开发中遇到其他用法,现查就是了!
***#pragma once的作用:保证头文件只被include一次,从而防止变量、函数的重复声明,***看例子:
//t1.h
#pragma once
int a;
//t2.h
#include "t1.h"
//main.c
#include "t1.h"
#include "t2.h"
int main(){
return 0;
}
//预处理
gcc -E main.c >main.i //产生main.i文件,打开,定位到文件最后,可以看到,处理两次int a
注意:#pragma once指令防止重复编译是针对某一个源文件而言的,
即某个源文件包含了两个头文件,而那两个头文件之一又包含了另一个,
此时#pragma once会发生作用,起到只编译一次的作用。
定义宏的时候防止重复定义,一般加#ifndef判断:
#ifndef _X_H
#define _X_H
#endif
头文件的编写
多文件的C语言程序的源文件到执行,大致要经历的过程:
1)强符号和弱符号
我们在编写代码的过程中经常会遇到一种叫做符号重复定义(Multiple Definition)的错误,这是因为在多个源文件中定义了名字相同的全局变量,并且都将它们初始化了。
例如,在 a.c 中定义了全局变量 global:
int global = 10;
在 b.c 中又对 global 进行了定义:
int global = 20;
那么在链接时就会报错。
在C语言中,链接器默认
函数和初始化了的全局变量为强符号(Strong Symbol),
未初始化的全局变量为弱符号(Weak Symbol)。
链接器会按照如下的规则处理被多次定义的强符号和弱符号:
- 不允许强符号被多次定义;如果有多个强符号,那么链接器会报符号重复定义错误。
- 如果一个符号在某个目标文件中是强符号,在其他文件中是弱符号,强符号覆盖弱符号。
- 如果一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个。
比如目标文件 a.o 定义全局变量 global 为 int 类型,占用4个字节,
目标文件 b.o 定义 global 为 double 类型,占用8个字节,那么被链接后,符号 global 占用8个字节。
请尽量不要使用多个不同类型的弱符号,否则有时候很难发现程序错误。
在 GCC 中,可以通过__attribute__((weak))来强制定义任何一个符号为弱符号。
#include <stdio.h>
extern int ext; //表示这个变量,只是声明,不是定义,extern关键字表示此变量定义在了其它文件中了,只是引用
int weak; //弱弱符号
int strong = 100 ; //strong是强符号,因为它即定底子,又有初值
__attribute__ ((weak)) int weak2=2; //此处weak2虽然定义也有初值,但被__attribute__转成了弱符号了
int main(){
return 0;
}
weak1 和 weak2 是弱符号,strong 和 main 是强符号,而 extern修饰的ext既非强符号也非弱符号,它是一个对外部变量的引用(使用)。
为了加深理解,我们不妨再来看一个多文件编程的例子。
main.c 源码:
#include <stdio.h>
//弱符号
__attribute__((weak)) int a = 20;
__attribute__((weak)) void func(){
printf("main c file\n");
}
int main(){
printf("a = %d\n", a);
func();
return 0;
}
a.c 源码:
#include <stdio.h>
int a = 20; //a 即有初始值,为强符号了,在整个项目中都优先用,会覆盖其它文件中定义的,没有初值的a
void foo(){ //foo也是强符号
printf("a.c file foo function\n");
}
执行与结果
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling\strong_weaksign> gcc *.c
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling\strong_weaksign> ./a
20
a.c file foo function
在 main.c 中,a 和 func 都是弱符号,在 module.c 中,a 和 foo 都是强符号,强符号会覆盖弱符号,所以链接器最终会使用 a.c 中的符号。
需要注意的是,attribute((weak))只对链接器有效,对编译器不起作用,编译器不区分强符号和弱符号,只要在一个源文件中定义两个相同的符号,不管它们是强是弱,都会报“重复定义”错误。
#include <stdio.h>
__attribute__((weak)) int a = 20;
int a = 9999;
int main(){
printf("a = %d\n", a);
return 0;
}
这段代码在编译阶段就会报错,编译器会认为变量 a 被定义了两次,属于重复定义。
2)强引用和弱引用
所谓引用(Reference),是指对符号的使用。
int a = 100, b = 200, c;
c = a + b;
第一行是符号定义,第二行是符号引用。
目前我们所看到的符号引用,在所有目标文件被链接成可执行文件时,它们的地址都要被找到,如果没有符号声明,链接器就会报符号未声明错误,这种被称为强引用(Strong Reference)。
前面有例子,强引用时, 我们用 #include “xxx.h文件” 该文件中声明了强引用的符号了,这里我们在当前文件中也声明了(即使是弱声明也是声明了呀)。
弱引用(Weak Reference)同一个文件,如果符号有声明,就正常使用,如果没有声明,也不报错。
先在变量或函数声明的前面加上__attribute__((weak)),下面在用,符号的引用就变为弱引用:
#include <stdio.h>
__attribute__((weak)) int b=2; //a强制成弱符号了
__attribute__((weak)) void foo2(){
printf("main.c file foo function!\n");
}
int main(){
printf("%d \n",b);
foo2();
return 0;
}
//执行与结果
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling\strong_weaksign> gcc *.c
PS C:\Users\Administrator\Desktop\workspaceC\yichulimingling\strong_weaksign> ./a
2
main.c file foo function!
弱符号和弱引用对于库来说十分有用,我们在开发库时,可以将某些符号定义为弱符号,这样就能够被用户定义的强符号覆盖,从而使得程序可以使用自定义版本的函数,用可以使用软件本身的函数,增加了很大的灵活性。
其次,外面在调用某些函数时候用弱引用,被调用的模块,可以很方便地进行裁剪和组合。裁剪掉部分模块顶多让功能弱一些,程序不会报错。
C语言模块化编程中头文件编写的原则
前面我们在演示多文件编程时创建了 main.c 和 a.c 两个源文件,并在a.c 中定义了一个函数和一个全局变量,然后在 main.c 中进行了弱声明。
不过实际开发中很少这样做,一般是将函数和变量的声明放到头文件,再在当前源文件中 #include 进来。如果变量的值是固定的,最好使用宏来代替。下面的例子是改进后的代码。
my.h源码
#define OS "Windows 10" //定义宏
extern int a;
extern void foo();
main.c源文件
#include <stdio.h>
//__attribute__((weak)) int b=2; //a强制成弱符号了
//__attribute__((weak)) void foo2(){
// printf("main.c file foo function!\n");
//}
#include "my.h"
int main(){
printf("%d \n",a);
foo();
printf("OS: %s\n", OS);
return 0;
}
.h 和 .c 在项目中承担的角色不一样:
.c 文件主要负责实现,也就是定义函数和变量;
.h 文件主要负责声明(包括变量声明和函数声明)、宏定义、类型定义等。
这些不是C语法规定的内容,而是约定成俗的规范,或者说是长期形成的事实标准。
根据这份规范,头文件可以包含如下的内容:
(1)可以声明函数,但不可以定义函数。
(2)可以声明变量,但不可以定义变量。
(3)可以定义宏,包括带参的宏和不带参的宏。
(4)结构体的定义、自定义数据类型一般也放在头文件中。
在项目开发中,我们可以将一组相关的变量和函数定义在一个 .c 文件中,并用一个同名的 .h 文件(头文件)进行声明,其他模块如果需要使用某个变量或函数,那么引入这个头文件就可以。
比较规范的C语言多文件编程的结构
#include <stdio.h>
#include "./include/login.h"
#include "./include/reg.h"
#include "./include/tools.h"
int main(){
return 0;
}
执行:gcc main.c ./module/*.c -o main.exe