C语言基础
简介:这篇文章是作者在曾经学过c的基础上,为了快速复习巩固c,并且上手嵌入式开发而写的笔记。如果你也是类似的情况,可以阅读这篇文章,如果您是想扎实学c语言,那我并不建议仅仅看这篇文章。当然你也可以将这篇文章作为查缺补漏来使用,文章中如果有借鉴其他文章的话会在对应位置附上文章链接。
主要参考文章/视频:
C语言入门教程,C语言学习教程(非常详细) (biancheng.net)
4.1-数字电路与C语言基础--电平特性_哔哩哔哩_bilibili
普中51单片机开发攻略(这个资源需要大家自行上网搜索,这里不展示链接)
电平特性
单片机是一种数字集成芯片,数字电路中只有两种电平:高电平和低电平。
为了让大家在刚起步的时候对电平特性有一个清晰的认识,我们暂时定义单片机 输出与输入为 TTL 电平,其中高电平为+5V,低电平为 0V。
计算机的串口为 RS232 电平,其中高电平为-12V,低电平为+12V。这里强调的是,RS232C 电平为负逻 辑电平。因此当计算机与单片机之间要通信时, 需要依靠电平转换芯片,比如 MAX232 电平转换芯片。
TTL 电平信号用的最多,这是因为数据表示通常采用二进制,+5V 等价于逻 辑 1,0V 等价于逻辑 0,这被称为 TTL(晶体管-晶体管逻辑电平)信号系统。
TTL 电路和 CMOS 电路的逻辑电平关系如下:
①VOH:逻辑电平 1 的输出电压。
②VOL:逻辑电平 0 的输出电压。
③VIH:逻辑电平 1 的输入电压。
④VIL:逻辑电平 0 的输入电压。
TTL 电平临界值:
①VOHmin=2.4V,VOLmax=0.4V。
②VIHmin=2.0V,VILmax=0.8V。
CMOS 电平临界值(假设电源电压为+5V):
①VOHmin=4.99V,VOLmax=0.01V。
②VIHmin=3.5V,VILmax=1.5V。
TTL 和 CMOS 的逻辑电平转换:CMOS 电平能驱动 TTL 电平,但 TTL 电平不能驱动 CMOS 电平,需加上拉电阻。
通常情况下,单片机、DSP、FPGA 之间引脚能否直接相连要参考以下方法进 行判断:一般来说,同电压的是可以相连的,不过最好还是要查看下芯片技术手 册上的 VIL、VIH、VOL 和 VOH 的值,看是否能够匹配。有些情况在一般应用中没 有问题,但是参数上就是有点不够匹配,在某些情况下运行可能就不够稳定,或者不同批次的器件就不能运行。
进制的转换
1) 整数部分
例如,将八进制数字 53627 转换成十进制:
53627 = 5×8^4 + 3×8^3 + 6×8^2 + 2×8^1 + 7×8^0 = 22423(十进制)
从右往左看,第1位的位权为 8^0=1,第2位的位权为 8^1=8,第3位的位权为 8^2=64,第4位的位权为 8^3=512,第5位的位权为 8^4=4096 …… 第n位的位权就为 8^(n-1)。将各个位的数字乘以位权,然后再相加,就得到了十进制形式。
2) 小数部分
例如,将八进制数字 423.5176 转换成十进制:
423.5176 = 4×8^2 + 2×8^1 + 3×8^0 + 5×8^-1 + 1×8^-2 + 7×8^-3 + 6×8^-4 = 275.65576171875(十进制)
小数部分和整数部分相反,要从左往右看,第1位的位权为 8^-1=1/8,第2位的位权为 8^-2=1/64,第3位的位权为 8^-3=1/512,第4位的位权为 8^-4=1/4096 …… 第m位的位权就为 8^-m。
将十进制转换为二进制、八进制、十六进制
将十进制转换为其它进制时比较复杂,整数部分和小数部分的算法不一样,下面我们分别讲解。
1) 整数部分
十进制整数转换为 N 进制整数采用“除 N 取余,逆序排列”法。具体做法是:
- 将 N 作为除数,用十进制整数除以 N,可以得到一个商和余数;
- 保留余数,用商继续除以 N,又得到一个新的商和余数;
- 仍然保留余数,用商继续除以 N,还会得到一个新的商和余数;
- ……
- 如此反复进行,每次都保留余数,用商接着除以 N,直到商为 0 时为止。
把先得到的余数作为 N 进制数的低位数字,后得到的余数作为 N 进制数的高位数字,依次排列起来,就得到了 N 进制数字。
下图演示了将十进制数字 36926 转换成八进制的过程:

从图中得知,十进制数字 36926 转换成八进制的结果为 110076。
2) 小数部分
十进制小数转换成 N 进制小数采用“乘 N 取整,顺序排列”法。具体做法是:
- 用 N 乘以十进制小数,可以得到一个积,这个积包含了整数部分和小数部分;
- 将积的整数部分取出,再用 N 乘以余下的小数部分,又得到一个新的积;
- 再将积的整数部分取出,继续用 N 乘以余下的小数部分;
- ……
- 如此反复进行,每次都取出整数部分,用 N 接着乘以小数部分,直到积中的小数部分为 0,或者达到所要求的精度为止。
把取出的整数部分按顺序排列起来,先取出的整数作为 N 进制小数的高位数字,后取出的整数作为低位数字,这样就得到了 N 进制小数。
下图演示了将十进制小数 0.930908203125 转换成八进制小数的过程:

从图中得知,十进制小数 0.930908203125 转换成八进制小数的结果为 0.7345。
单位换算:
- 1Byte = 8 Bit
- 1KB = 1024Byte = 210Byte
- 1MB = 1024KB = 220Byte
- 1GB = 1024MB = 230Byte
- 1TB = 1024GB = 240Byte
- 1PB = 1024TB = 250Byte
- 1EB = 1024PB = 260Byte
编译(Compile)
将C语言代码转换成CPU能够识别的二进制指令,也就是将代码加工成 .exe 程序的格式;这个工具是一个特殊的软件,叫做编译器(Compiler)。
编译器能够识别代码中的词汇、句子以及各种特定的格式,并将他们转换成计算机能够识别的二进制形式,这个过程称为编译(Compile)。
C语言的编译器有很多种,不同的平台下有不同的编译器,例如:
- Windows 下常用的是微软开发的 Visual C++,它被集成在 Visual Studio 中,一般不单独使用;
- Linux 下常用的是 GUN 组织开发的 GCC,很多 Linux 发行版都自带 GCC;
- Mac 下常用的是 LLVM/Clang,它被集成在 Xcode 中(Xcode 以前集成的是 GCC,后来由于 GCC 的不配合才改为 LLVM/Clang,LLVM/Clang 的性能比 GCC 更加强大)。
你的代码语法正确与否,编译器说了才算,我们学习C语言,从某种意义上说就是学习如何使用编译器。
编译器可以 100% 保证你的代码从语法上讲是正确的,因为哪怕有一点小小的错误,编译也不能通过,编译器会告诉你哪里错了,便于你的更改。
链接(Link)
C语言代码经过编译以后,并没有生成最终的可执行文件(.exe 文件),而是生成了一种叫做目标文件(Object File)的中间文件(或者说临时文件)。目标文件也是二进制形式的,它和可执行文件的格式是一样的。对于 Visual C++,目标文件的后缀是.obj;对于 GCC,目标文件的后缀是.o。
目标文件经过链接(Link)以后才能变成可执行文件。既然目标文件和可执行文件的格式是一样的,为什么还要再链接一次呢,直接作为可执行文件不行吗?
不行的!因为编译只是将我们自己写的代码变成了二进制形式,它还需要和系统组件(比如标准库、动态链接库等)结合起来,这些组件都是程序运行所必须的。
链接(Link)其实就是一个“打包”的过程,它将所有二进制形式的目标文件和系统组件组合成一个可执行文件。完成链接的过程也需要一个特殊的软件,叫做链接器(Linker)。
随着我们学习的深入,我们编写的代码越来越多,最终需要将它们分散到多个源文件中,编译器每次只能编译一个源文件,生成一个目标文件,这个时候,链接器除了将目标文件和系统组件组合起来,还需要将编译器生成的多个目标文件组合起来。
再次强调,编译是针对一个源文件的,有多少个源文件就需要编译多少次,就会生成多少个目标文件。
编译链接总结
不管我们编写的代码有多么简单,都必须经过「编译 --> 链接」的过程才能生成可执行文件:
- 编译就是将我们编写的源代码“翻译”成计算机可以识别的二进制格式,它们以目标文件的形式存在;
- 链接就是一个“打包”的过程,它将所有的目标文件以及系统组件组合成一个可执行文件。
常用数据类型
建议观看文章:
STC89C52RC单片机额外篇 | 03 - 认识C51编译器支持的数据类型_stc89c52rc的sfr-CSDN博客
这里有个关键点,不同位的编译器的类型占据的字节长度是不同的。
在标准C语言中基本的数据类型,例如char、int、short、long、float与double,它们存储数据的长度是有差异的,而在C51编译器中,int与short是一样的,float与double也是一样的,我们通过表格来看看它们具体的定义:
C51扩充数据类型(C中是没有的 这是单片机内容)
这些寄存器的声明已经完全被包含在 51 单片机的特殊功能寄存器声明头 文件“reg51.h”中了
例如:sbit TI=SCON^1; SCON 是一个 8 位寄存器,SCON^1 表示这个 8 位寄存器的次低位,最低位是 SCON^0;SCON^7 表示这个寄存器的最高位。
该语句的功能就是将 SCON 寄存器的 次低位声明为 TI,以后若要对 SCON 寄存器的次低位操作,则可直接操作 TI。
例如:sfr SCON=0x98; SCON 是单片机的串行口控制寄存器,这个寄存器在单片机内存中的地址是 0X98。这样声明后,我们再以后要操作这个控制寄存器时,就可以直接对 SCON 进行操作,这时编译器也会明白,我们实际要操作的是单片机内部 0X98 地址处 的这个寄存器,而 SCON 仅仅是这个地址的一个代号或是名称而已,当然,我们 也可以定义成其他的名称。
变量的存储种类
存储种类是指变量在程序执行过程中的作用范围。C51 变量的存储种类有四 种,分别是自动(auto)、外部(extern)、静态(static)和寄存器(register)。
a.auto:
使用 auto 定义的变量称为自动变量,其作用范围在定义它的函数体或复合 语句内部,当定义它的函数体或复合语句执行时,C51 才为该变量分配内存空间, 结束时占用的内存空间释放。自动变量一般分配在内存的堆栈空间中。定义变量时,如果省略存储种类,则该变量默认为自动(auto)变量。
b.extern:
使用 extern 定义的变量称为外部变量。在一个函数体内,要使用一个已在 该函数体外或别的程序中定义过的外部变量时,该变量在该函数体内要用 extern 说明。外部变量被定义后分配固定的内存空间,在程序整个执行时间内 都有效,直到程序结束才释放。
c.static:
使用 static 定义的变量称为静态变量。它又分为内部静态变量和外部静态 变量。在函数体内部定义的静态变量为内部静态变量,它在对应的函数体内有效, 一直存在,但在函数体外不可见,这样不仅使变量在定义它的函数体外被保护, 还可以实现当离开函数时值不被改变。外部静态变量上在函数外部定义的静态变量。它在程序中一直存在,但在定义的范围之外是不可见的。如在多文件或多模 块处理中,外部静态变量只在文件内部或模块内部有效。
d.register:
使用 register 定义的变量称为寄存器变量。它定义的变量存放在 CPU 内部 的寄存器中,处理速度快,但数目少。C51 编译器编译时能自动识别程序中使用 频率最高的变量,并自动将其作为寄存器变量,用户可以无需专门声明。
存储器类型(正常情况不用操作)
存储器类型 存储器类型是用于指明变量所处的单片机的存储器区域情况。存储器类型与 存储种类完全不同。C51 编译器能识别的存储器类型有以下几种,见表所示。
定义变量时也可以省“存储器类型”,省时 C51 编译器将按编译模式默认存 储器类型。
特殊功能寄存器变量
51 系列单片机片内有许多特殊功能寄存器,通过这些特殊功能寄存器可以控 制 51 系列单片机的定时器、计数器、串口、I/O 及其它功能部件,每一个特殊 功能寄存器在片内 RAM 中都对应于一个字节单元或两个字节单元。
在 C51 中,允许用户对这些特殊功能寄存器进行访问,访问时须通过 sfr 或 sfr16 类型说明符进行定义,定义时须指明它们所对应的片内 RAM 单元的地址。
格式如下:
sfr 或 sfr16 特殊功能寄存器名=地址;
sfr 用于对 51 单片机中单字节的特殊功能寄存器进行定义,sfr16 用于对双 字节特殊功能寄存器进行定义。特殊功能寄存器名一般用大写字母表示。地址一 般用直接地址形式。
【例】特殊功能寄存器的定义。
sfr PSW=0xd0;
sfr SCON=0x98;
sfr TMOD=0x89;
sfr P1=0x90;
sfr16 DPTR=0x82;
sfr16 T1=0X8A
位变量
在 C51 中,允许用户通过位类型符定义位变量。位类型符有两个:bit 和 sbit。 可以定义两种位变量。
bit 位类型符用于定义一般的可位处理位变量。它的格式如下:
bit 位变量名;
在格式中可以加上各种修饰,但注意存储器类型只能是 bdata、data、idata。 只能是片内 RAM 的可位寻址区,严格来说只能是 bdata。
sbit 位类型符用于定义在可位寻址字节或特殊功能寄存器中的位,定义时须 指明其位地址,可以是位直接地址,可以是可位寻址变量带位号,也可以是特殊 功能寄存器名带位号。
格式如下:
sbit 位变量名=位地址;
如位地址为位直接地址,其取值范围为 0x00~0xff;如位地址是可位寻址变 量带位号或特殊功能寄存器名带位号,则在它前面须对可位寻址变量或特殊功能 寄存器进行定义。字节地址与位号之间、特殊功能寄存器与位号之间一般用“^” 作间隔。
如定义 51 单片机管脚:
sbit LED=P1^0;
在 C51 中,为了用户操作方便,C51 编译器把 51 单片机的常用的特殊功能寄 存器和特殊位进行了定义,放在一个“reg51.h”或“reg52.h”的头文件中,当 用户要使用时,只须要在使用之前用一条预处理命令“#include ” 或“#include ”把这个头文件包含到程序最开始位置,然后就可使用 殊功能寄存器名和特殊位名称。
printf
#include <stdio.h>
int main()
{
int n = 100;
char c = '@'; //字符用单引号包围,字符串用双引号包围
float money = 93.96;
printf("n=%d, c=%c, money=%f\n", n, c, money);
return 0;
}
输出结果: n=100, c=@, money=93.959999
sizeof 操作符
获取某个数据类型的长度可以使用 sizeof 操作符,如下所示:
#include <stdio.h>
int main()
{
short a = 10;
int b = 100;
int short_length = sizeof a;
int int_length = sizeof(b);
int long_length = sizeof(long);
int char_length = sizeof(char);
printf("short=%d, int=%d, long=%d, char=%d\n", short_length, int_length, long_length, char_length);
return 0;
}
在 32 位环境以及 Win64 环境下的运行结果为:
short=2, int=4, long=4, char=1
sizeof 用来获取某个数据类型或变量所占用的字节数,如果后面跟的是变量名称,那么可以省略( ),如果跟的是数据类型,就必须带上( )。
不同整型的输出
使用不同的格式控制符可以输出不同类型的整数,它们分别是:
%hd用来输出 short int 类型,hd 是 short decimal 的简写;%d用来输出 int 类型,d 是 decimal 的简写;%ld用来输出 long int 类型,ld 是 long decimal 的简写。
下面的例子演示了不同整型的输出:
#include <stdio.h>
int main()
{
short a = 10;
int b = 100;
long c = 9437;
printf("a=%hd, b=%d, c=%ld\n", a, b, c);
return 0;
}
运行结果: a=10, b=100, c=9437
二进制数、八进制数和十六进制数的表示
一个数字默认就是十进制的,表示一个十进制数字不需要任何特殊的格式。但是,表示一个二进制、八进制或者十六进制数字就不一样了,为了和十进制数字区分开来,必须采用某种特殊的写法,具体来说,就是在数字前面加上特定的字符,也就是加前缀。
1) 二进制
二进制由 0 和 1 两个数字组成,使用时必须以0b或0B(不区分大小写)开头,例如:
//合法的二进制
int a = 0b101; //换算成十进制为 5
int b = -0b110010; //换算成十进制为 -50
int c = 0B100001; //换算成十进制为 33
2) 八进制
八进制由 0~7 八个数字组成,使用时必须以0开头(注意是数字 0,不是字母 o),例如:
//合法的八进制数
int a = 015; //换算成十进制为 13
int b = -0101; //换算成十进制为 -65
int c = 0177777; //换算成十进制为 65535
3) 十六进制
十六进制由数字 09、字母 AF 或 a~f(不区分大小写)组成,使用时必须以0x或0X(不区分大小写)开头,例如:
//合法的十六进制
int a = 0X2A; //换算成十进制为 42
int b = -0XA0; //换算成十进制为 -160
int c = 0xffff; //换算成十进制为 65535
下表全面地总结了不同类型的整数,以不同进制的形式输出时对应的格式控制符(--表示没有对应的格式控制符)。
| short | int | long | unsigned short | unsigned int | unsigned long | |
|---|---|---|---|---|---|---|
| 八进制 | -- | -- | -- | %ho | %o | %lo |
| 十进制 | %hd | %d | %ld | %hu | %u | %lu |
| 十六进制 | -- | -- | -- | %hx 或者 %hX | %x 或者 %X | %lx 或者 %lX |
转义字符
字符集(Character Set)为每个字符分配了唯一的编号,我们不妨将它称为编码值。在C语言中,一个字符除了可以用它的实体(也就是真正的字符)表示,还可以用编码值表示。这种使用编码值来间接地表示字符的方式称为转义字符(Escape Character)。
转义字符以\或者\x开头,以\开头表示后跟八进制形式的编码值,以\x开头表示后跟十六进制形式的编码值。对于转义字符来说,只能使用八进制或者十六进制。
字符 1、2、3、a、b、c 对应的 ASCII 码的八进制形式分别是 61、62、63、141、142、143,十六进制形式分别是 31、32、33、61、62、63。下面的例子演示了转义字符的用法:
char a = '\61'; //字符1
char b = '\141'; //字符a
char c = '\x31'; //字符1
char d = '\x61'; //字符a
char *str1 = "\x31\x32\x33\x61\x62\x63"; //字符串"123abc"
char *str2 = "\61\62\63\141\142\143"; //字符串"123abc"
char *str3 = "The string is: \61\62\63\x61\x62\x63" //混用八进制和十六进制形式
转义字符既可以用于单个字符,也可以用于字符串,并且一个字符串中可以同时使用八进制形式和十六进制形式。
转义字符的初衷是用于 ASCII 编码,所以它的取值范围有限:
- 八进制形式的转义字符最多后跟三个数字,也即
\ddd,最大取值是\177; - 十六进制形式的转义字符最多后跟两个数字,也即
\xdd,最大取值是\x7f。
| 转义字符 | 意义 | ASCII码值(十进制) |
|---|---|---|
| \a | 响铃(BEL) | 007 |
| \b | 退格(BS) ,将当前位置移到前一列 | 008 |
| \f | 换页(FF),将当前位置移到下页开头 | 012 |
| \n | 换行(LF) ,将当前位置移到下一行开头 | 010 |
| \r | 回车(CR) ,将当前位置移到本行开头 | 013 |
| \t | 水平制表(HT) | 009 |
| \v | 垂直制表(VT) | 011 |
| ' | 单引号 | 039 |
| " | 双引号 | 034 |
| \ | 反斜杠 | 092 |
\n和\t是最常用的两个转义字符:
\n用来换行,让文本从下一行的开头输出,前面的章节中已经多次使用;\t用来占位,一般相当于四个空格,或者 tab 键的功能。
单引号、双引号、反斜杠是特殊的字符,不能直接表示:
- 单引号是字符类型的开头和结尾,要使用
\'表示,也即'\''; - 双引号是字符串的开头和结尾,要使用
\"表示,也即"abc\"123"; - 反斜杠是转义字符的开头,要使用
\\表示,也即'\\',或者"abc\\123"。
表达式(Expression)和语句(Statement)
表达式(Expression)和语句(Statement)的概念在C语言中并没有明确的定义:
- 表达式可以看做一个计算的公式,往往由数据、变量、运算符等组成,例如
3*4+5、a=c=d等,表达式的结果必定是一个值; - 语句的范围更加广泛,不一定是计算,不一定有值,可以是某个操作、某个函数、选择结构、循环等。
赶紧划重点:
- 表达式必须有一个执行结果,这个结果必须是一个值,例如
3*4+5的结果 17,a=c=d=10的结果是 10,printf("hello")的结果是 5(printf 的返回值是成功打印的字符的个数)。 - 以分号
;结束的往往称为语句,而不是表达式,例如3*4+5;、a=c=d;等。
加减乘除
| 加法 | 减法 | 乘法 | 除法 | 求余数(取余) | |
|---|---|---|---|---|---|
| 数学 | + | - | × | ÷ | 无 |
| C语言 | + | - | * | / | % |
强制类型转换
强制类型转换的格式为:
(type_name) expression
type_name为新类型名称,expression为表达式。例如:
(float) a; //将变量 a 转换为 float 类型
(int)(x+y); //把表达式 x+y 的结果转换为 int 整型
(float) 100; //将数值 100(默认为int类型)转换为 float 类型
下面是一个需要强制类型转换的经典例子:
#include <stdio.h>
int main(){
int sum = 103; //总数
int count = 7; //数目
double average; //平均数
average = (double) sum / count;
printf("Average is %lf!\n", average);
return 0;
}
运行结果: Average is 14.714286!
数据输出大汇总
在C语言中,有三个函数可以用来在显示器上输出数据,它们分别是:
- puts():只能输出字符串,并且输出结束后会自动换行
- putchar():只能输出单个字符
- printf():可以输出各种类型的数据
汇总一下格式控制符:
| 格式控制符 | 说明 |
|---|---|
| %c | 输出一个单一的字符 |
| %hd、%d、%ld | 以十进制、有符号的形式输出 short、int、long 类型的整数 |
| %hu、%u、%lu | 以十进制、无符号的形式输出 short、int、long 类型的整数 |
| %ho、%o、%lo | 以八进制、不带前缀、无符号的形式输出 short、int、long 类型的整数 |
| %#ho、%#o、%#lo | 以八进制、带前缀、无符号的形式输出 short、int、long 类型的整数 |
| %hx、%x、%lx %hX、%X、%lX | 以十六进制、不带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字也小写;如果 X 大写,那么输出的十六进制数字也大写。 |
| %#hx、%#x、%#lx %#hX、%#X、%#lX | 以十六进制、带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字和前缀都小写;如果 X 大写,那么输出的十六进制数字和前缀都大写。 |
| %f、%lf | 以十进制的形式输出 float、double 类型的小数 |
| %e、%le %E、%lE | 以指数的形式输出 float、double 类型的小数。如果 e 小写,那么输出结果中的 e 也小写;如果 E 大写,那么输出结果中的 E 也大写。 |
| %g、%lg %G、%lG | 以十进制和指数中较短的形式输出 float、double 类型的小数,并且小数部分的最后不会添加多余的 0。如果 g 小写,那么当以指数形式输出时 e 也小写;如果 G 大写,那么当以指数形式输出时 E 也大写。 |
| %s | 输出一个字符串 |
printf() 格式控制符的完整形式如下:
%[flag][width][.precision]type
[ ] 表示此处的内容可有可无,是可以省略的。
\1) type 表示输出类型,比如 %d、%f、%c、%lf,type 就分别对应 d、f、c、lf;再如,%-9d中 type 对应 d。
type 这一项必须有,这意味着输出时必须要知道是什么类型。
\2) width 表示最小输出宽度,也就是至少占用几个字符的位置;例如,%-9d中 width 对应 9,表示输出结果最少占用 9 个字符的宽度。
当输出结果的宽度不足 width 时,以空格补齐(如果没有指定对齐方式,默认会在左边补齐空格);当输出结果的宽度超过 width 时,width 不再起作用,按照数据本身的宽度来输出。
下面的代码演示了 width 的用法:
#include <stdio.h>
int main(){
int n = 234;
float f = 9.8;
char c = '@';
char *str = "http://c.biancheng.net";
printf("%10d%12f%4c%8s", n, f, c, str);
return 0;
}
运行结果:
234 9.800000 @http://c.biancheng.net
对输出结果的说明:
- n 的指定输出宽度为 10,234 的宽度为 3,所以前边要补上 7 个空格。
- f 的指定输出宽度为 12,9.800000 的宽度为 8,所以前边要补上 4 个空格。
- str 的指定输出宽度为 8,"c.biancheng.net" 的宽度为 22,超过了 8,所以指定输出宽度不再起作用,而是按照 str 的实际宽度输出。
\3) .precision 表示输出精度,也就是小数的位数。
- 当小数部分的位数大于 precision 时,会按照四舍五入的原则丢掉多余的数字;
- 当小数部分的位数小于 precision 时,会在后面补 0。
另外,.precision 也可以用于整数和字符串,但是功能却是相反的:
- 用于整数时,.precision 表示最小输出宽度。与 width 不同的是,整数的宽度不足时会在左边补 0,而不是补空格。
- 用于字符串时,.precision 表示最大输出宽度,或者说截取字符串。当字符串的长度大于 precision 时,会截掉多余的字符;当字符串的长度小于 precision 时,.precision 就不再起作用。
请看下面的例子:
#include <stdio.h>
int main(){
int n = 123456;
double f = 882.923672;
char *str = "abcdefghi";
printf("n: %.9d %.4d\n", n, n);
printf("f: %.2lf %.4lf %.10lf\n", f, f, f);
printf("str: %.5s %.15s\n", str, str);
return 0;
}
运行结果:
n: 000123456 123456
f: 882.92 882.9237 882.9236720000
str: abcde abcdefghi
对输出结果的说明:
- 对于 n,.precision 表示最小输出宽度。n 本身的宽度为 6,当 precision 为 9 时,大于 6,要在 n 的前面补 3 个 0;当 precision 为 4 时,小于 6,不再起作用。
- 对于 f,.precision 表示输出精度。f 的小数部分有 6 位数字,当 precision 为 2 或者 4 时,都小于 6,要按照四舍五入的原则截断小数;当 precision 为 10 时,大于 6,要在小数的后面补四个 0。
- 对于 str,.precision 表示最大输出宽度。str 本身的宽度为 9,当 precision 为 5 时,小于 9,要截取 str 的前 5 个字符;当 precision 为 15 时,大于 9,不再起作用。
\4) flag 是标志字符。例如,%#x中 flag 对应 #,%-9d中 flags 对应-。下表列出了 printf() 可以用的 flag:
| 标志字符 | 含 义 |
|---|---|
| - | -表示左对齐。如果没有,就按照默认的对齐方式,默认一般为右对齐。 |
| + | 用于整数或者小数,表示输出符号(正负号)。如果没有,那么只有负数才会输出符号。 |
| 空格 | 用于整数或者小数,输出值为正时冠以空格,为负时冠以负号。 |
| # | 对于八进制(%o)和十六进制(%x / %X)整数,# 表示在输出时添加前缀;八进制的前缀是 0,十六进制的前缀是 0x / 0X。对于小数(%f / %e / %g),# 表示强迫输出小数点。如果没有小数部分,默认是不输出小数点的,加上 # 以后,即使没有小数部分也会带上小数点。 |
请看下面的例子:
#include <stdio.h>
int main(){
int m = 192, n = -943;
float f = 84.342;
printf("m=%10d, m=%-10d\n", m, m); //演示 - 的用法
printf("m=%+d, n=%+d\n", m, n); //演示 + 的用法
printf("m=% d, n=% d\n", m, n); //演示空格的用法
printf("f=%.0f, f=%#.0f\n", f, f); //演示#的用法
return 0;
}
运行结果:
m= 192, m=192
m=+192, n=-943
m= 192, n=-943
f=84, f=84.
对输出结果的说明:
- 当以
%10d输出 m 时,是右对齐,所以在 192 前面补七个空格;当以%-10d输出 m 时,是左对齐,所以在 192 后面补七个空格。 - m 是正数,以
%+d输出时要带上正号;n 是负数,以%+d输出时要带上负号。 - m 是正数,以
% d输出时要在前面加空格;n 是负数,以% d输出时要在前面加负号。 %.0f表示保留 0 位小数,也就是只输出整数部分,不输出小数部分。默认情况下,这种输出形式是不带小数点的,但是如果有了#标志,那么就要在整数的后面“硬加上”一个小数点,以和纯整数区分开。
scanf()
scanf 是 scan format 的缩写,意思是格式化扫描,也就是从键盘获得用户输入,和 printf 的功能正好相反。
#include <stdio.h>
int main()
{
int a = 0, b = 0, c = 0, d = 0;
scanf("%d", &a); //输入整数并赋值给
scanf("%d", &b); //输入整数并赋值给变量b
printf("a+b=%d\n", a+b); //计算a+b的值并输出
scanf("%d %d", &c, &d); //输入两个整数并分别赋值给c、d
printf("c*d=%d\n", c*d); //计算c*d的值并输出
return 0;
}
运行结果: 12↙ 60↙ a+b=72 10 23↙ c*d=230
注意"%d %d"之间是有空格的,所以输入数据时也要有空格。对于 scanf(),输入数据的格式要和控制字符串的格式保持一致。
scanf() 格式控制符汇总
| 格式控制符 | 说明 |
|---|---|
| %c | 读取一个单一的字符 |
| %hd、%d、%ld | 读取一个十进制整数,并分别赋值给 short、int、long 类型 |
| %ho、%o、%lo | 读取一个八进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型 |
| %hx、%x、%lx | 读取一个十六进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型 |
| %hu、%u、%lu | 读取一个无符号整数,并分别赋值给 unsigned short、unsigned int、unsigned long 类型 |
| %f、%lf | 读取一个十进制形式的小数,并分别赋值给 float、double 类型 |
| %e、%le | 读取一个指数形式的小数,并分别赋值给 float、double 类型 |
| %g、%lg | 既可以读取一个十进制形式的小数,也可以读取一个指数形式的小数,并分别赋值给 float、double 类型 |
| %s | 读取一个字符串(以空白符为结束) |
gets()
输入字符串当然可以使用 scanf() 这个通用的输入函数,对应的格式控制符为%s,上节已经讲到了;本节我们重点讲解的是 gets() 这个专用的字符串输入函数,它拥有一个 scanf() 不具备的特性。
gets() 的使用也很简单,请看下面的代码:
#include <stdio.h>
int main()
{
char author[30], lang[30], url[30];
gets(author);
printf("author: %s\n", author);
gets(lang);
printf("lang: %s\n", lang);
gets(url);
printf("url: %s\n", url);
return 0;
}
gets() 是有缓冲区的,每次按下回车键,就代表当前输入结束了,gets() 开始从缓冲区中读取内容,这一点和 scanf() 是一样的。gets() 和 scanf() 的主要区别是:
- scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。
- gets() 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束,所以,不管输入了多少个空格,只要不按下回车键,对 gets() 来说就是一个完整的字符串。
1) getchar()
最容易理解的字符输入函数是 getchar(),它就是scanf("%c", c)的替代品,除了更加简洁,没有其它优势了;或者说,getchar() 就是 scanf() 的一个简化版本。
下面的代码演示了 getchar() 的用法:
#include <stdio.h>
int main()
{
char c;
c = getchar();
printf("c: %c\n", c);
return 0;
}
C语言中常用的从控制台读取数据的函数有五个,它们分别是 scanf()、getchar()、getche()、getch() 和 gets()。其中 scanf()、getchar()、gets() 是标准函数,适用于所有平台;getche() 和 getch() 不是标准函数,只能用于 Windows。
scanf() 是通用的输入函数,它可以读取多种类型的数据。
getchar()、getche() 和 getch() 是专用的字符输入函数,它们在缓冲区和回显方面与 scanf() 有着不同的特性,是 scanf() 不能替代的。
gets() 是专用的字符串输入函数,与 scanf() 相比,gets() 的主要优势是可以读取含有空格的字符串。
scanf() 可以一次性读取多份类型相同或者不同的数据,getchar()、getche()、getch() 和 gets() 每次只能读取一份特定类型的数据,不能一次性读取多份数据。
多个if else语句
if else 语句也可以多个同时使用,构成多个分支,形式如下:
if(判断条件1){
语句块1
} else if(判断条件2){
语句块2
}else if(判断条件3){
语句块3
}else if(判断条件m){
语句块m
}else{
语句块n
}
意思是,从上到下依次检测判断条件,当某个判断条件成立时,则执行其对应的语句块,然后跳到整个 if else 语句之外继续执行其他代码。如果所有判断条件都不成立,则执行语句块n,然后继续执行后续代码。
也就是说,一旦遇到能够成立的判断条件,则不再执行其他的语句块,所以最终只能有一个语句块被执行。
switch
switch 是另外一种选择结构的语句,用来代替简单的、拥有多个分枝的 if else 语句,基本格式如下:
switch(表达式){
case 整型数值1: 语句 1;
case 整型数值2: 语句 2;
......
case 整型数值n: 语句 n;
default: 语句 n+1;
}
记得如果不用break打断的话,会执行成功分支后的所有分支(default除外)
逻辑运算符
| 运算符 | 说明 | 结合性 | 举例 |
|---|---|---|---|
| && | 与运算,双目,对应数学中的“且” | 左结合 | 1&&0、(9>3)&&(b>a) |
| || | 或运算,双目,对应数学中的“或” | 左结合 | 1||0、(9>3)||(b>a) |
| ! | 非运算,单目,对应数学中的“非” | 右结合 | !a、!(2<5) |
优先级
逻辑运算符和其它运算符优先级从低到高依次为:
赋值运算符(=) < &&和|| < 关系运算符 < 算术运算符 < 非(!)
&& 和 || 低于关系运算符,! 高于算术运算符。
while循环
while循环的一般形式为:
while(表达式){ 语句块 }
意思是,先计算“表达式”的值,当值为真(非0)时, 执行“语句块”;执行完“语句块”,再次计算表达式的值,如果为真,继续执行“语句块”……这个过程会一直重复,直到表达式的值为假(0),就退出循环,执行 while 后面的代码。
for
or循环的一般形式:
for(初始化语句; 循环条件; 自增或自减){ 语句块 }
break
用于跳出循环
continue
用于跳过剩余语句,直接进入下一次循环
#include <stdio.h>
int main(){
char c = 0;
while(c!='\n'){ //回车键结束循环
c=getchar();
if(c=='4' || c=='5'){ //按下的是数字键4或5
continue; //跳过当次循环,进入下次循环
}
putchar(c);
}
return 0;
}
数组
对有序数组的查询
#include <stdio.h>
int main(){
int nums[10] = {0, 1, 6, 10, 23, 34, 100, 177, 296, 999};
int i, num, thisindex = -1;
printf("Input an integer: ");
scanf("%d", &num);
for(i=0; i<10; i++){
if(nums[i] == num){
thisindex = i;
break;
}else if(nums[i] > num){
break;
}
}
if(thisindex < 0){
printf("%d isn't in the array.\n", num);
}else{
printf("%d is in the array, it's index is %d.\n", num, thisindex);
}
return 0;
}
字符数组
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' }; //对全体元素赋值时可以省去长度
char str[30] = {"c.biancheng.net"};
char str[30] = "c.biancheng.net"; //这种形式更加简洁,实际开发中常用
在C语言中,字符串总是以'\0'作为结尾,所以'\0'也被称为字符串结束标志,或者字符串结束符。
'\0'是 ASCII 码表中的第 0 个字符,英文称为 NUL,中文称为“空字符”。该字符既不能显示,也没有控制功能,输出该字符不会有任何效果,它在C语言中唯一的作用就是作为字符串结束标志。
C语言在处理字符串时,会从前往后逐个扫描字符,一旦遇到'\0'就认为到达了字符串的末尾,就结束处理。'\0'至关重要,没有'\0'就意味着永远也到达不了字符串的结尾。
由" "包围的字符串会自动在末尾添加'\0'。例如,"abc123"从表面看起来只包含了 6 个字符,其实不然,C语言会在最后隐式地添加一个'\0',这个过程是在后台默默地进行的,所以我们感受不到。
下图演示了"C program"在内存中的存储情形:

如果只初始化部分数组元素,那么剩余的数组元素也会自动初始化为“零”值,所以我们只需要将 str 的第 0 个元素赋值为 0,剩下的元素就都是 0 了。
#include <stdio.h>
int main(){
char str[30] = {0}; //将所有元素都初始化为 0,或者说 '\0'
char c;
int i;
for(c=65,i=0; c<=90; c++,i++){
str[i] = c;
}
printf("%s\n", str);
return 0;
}
字符串长度
所谓字符串长度,就是字符串包含了多少个字符(不包括最后的结束符'\0')。例如"abc"的长度是 3,而不是 4。
在C语言中,我们使用string.h头文件中的 strlen() 函数来求字符串的长度,它的用法为:
length strlen(strname);
strname 是字符串的名字,或者字符数组的名字;length 是使用 strlen() 后得到的字符串长度,是一个整数。
下面是一个完整的例子
#include <stdio.h>
#include <string.h> //记得引入该头文件
int main(){
char str[] = "http://c.biancheng.net/c/";
long len = strlen(str);
printf("The lenth of the string is %ld.\n", len);
return 0;
}
运行结果: The lenth of the string is 25.
scanf() gets()
#include <stdio.h>
int main(){
char str1[30] = {0};
char str2[30] = {0};
char str3[30] = {0};
//gets() 用法
printf("Input a string: ");
gets(str1);
//scanf() 用法
printf("Input a string: ");
scanf("%s", str2);
scanf("%s", str3);
printf("\nstr1: %s\n", str1);
printf("str2: %s\n", str2);
printf("str3: %s\n", str3);
return 0;
}
运行结果:
Input a string: C C++ Java Python Input a string: PHP JavaScript
str1: C C++ Java Python str2: PHP str3: JavaScript
字符串连接函数 strcat()
用于输入输出的字符串函数,例如printf、puts、scanf、gets等,使用时要包含头文件stdio.h,而使用其它字符串函数要包含头文件string.h。
string.h是一个专门用来处理字符串的头文件,它包含了很多字符串处理函数,由于篇幅限制,本节只能讲解几个常用的,
strcat 是 string catenate 的缩写,意思是把两个字符串拼接在一起,语法格式为:
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[100]="The URL is ";
char str2[60];
printf("Input a URL: ");
gets(str2);
strcat(str1, str2);
puts(str1);
return 0;
}
字符串复制函数 strcpy()
strcpy 是 string copy 的缩写,意思是字符串复制,也即将字符串从一个地方复制到另外一个地方,语法格式为:
strcpy(arrayName1, arrayName2);
strcpy() 会把 arrayName2 中的字符串拷贝到 arrayName1 中,字符串结束标志'\0'也一同拷贝。请看下面的例子:
#include <stdio.h>
#include <string.h>
int main(){
char str1[50] = "《C语言变怪兽》";
char str2[50] = "http://c.biancheng.net/";
strcpy(str1, str2);
printf("str1: %s\n", str1);
return 0;
}
运行结果: str1: c.biancheng.net/
你看,将 str2 复制到 str1 后,str1 中原来的内容就被覆盖了。
另外,strcpy() 要求 arrayName1 要有足够的长度,否则不能全部装入所拷贝的字符串。
字符串比较函数 strcmp()
strcmp 是 string compare 的缩写,意思是字符串比较,语法格式为:
strcmp(arrayName1, arrayName2);
arrayName1 和 arrayName2 是需要比较的两个字符串。
字符本身没有大小之分,strcmp() 以各个字符对应的 ASCII 码值进行比较。strcmp() 从两个字符串的第 0 个字符开始比较,如果它们相等,就继续比较下一个字符,直到遇见不同的字符,或者到字符串的末尾。
返回值:若 arrayName1 和 arrayName2 相同,则返回0;若 arrayName1 大于 arrayName2,则返回大于 0 的值;若 arrayName1 小于 arrayName2,则返回小于0 的值。
对4组字符串进行比较:
#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;
}
运行结果: a VS b: 32 a VS c: -31 a VS d: 0
冒泡排序
下面我们以“3 2 4 1”为例对冒泡排序进行说明。
第一轮 排序过程 3 2 4 1 (最初) 2 3 4 1 (比较3和2,交换) 2 3 4 1 (比较3和4,不交换) 2 3 1 4 (比较4和1,交换) 第一轮结束,最大的数字 4 已经在最后面,因此第二轮排序只需要对前面三个数进行比较。
第二轮 排序过程 2 3 1 4 (第一轮排序结果) 2 3 1 4 (比较2和3,不交换) 2 1 3 4 (比较3和1,交换) 第二轮结束,次大的数字 3 已经排在倒数第二个位置,所以第三轮只需要比较前两个元素。
第三轮 排序过程 2 1 3 4 (第二轮排序结果) 1 2 3 4 (比较2和1,交换)
至此,排序结束。
算法总结及实现
对拥有 n 个元素的数组 R[n] 进行 n-1 轮比较。
第一轮,逐个比较 (R[1], R[2]), (R[2], R[3]), (R[3], R[4]), ……. (R[N-1], R[N]),最大的元素被移动到 R[n] 上。
第二轮,逐个比较 (R[1], R[2]), (R[2], R[3]), (R[3], R[4]), ……. (R[N-2], R[N-1]),次大的元素被移动到 R[n-1] 上。 。。。。。。 以此类推,直到整个数组从小到大排序。
具体的代码实现如下所示:
#include <stdio.h>
int main(){
int nums[10] = {4, 5, 2, 10, 7, 1, 8, 3, 6, 9};
int i, j, temp;
//冒泡排序算法:进行 n-1 轮比较
for(i=0; i<10-1; i++){
//每一轮比较前 n-1-i 个,也就是说,已经排序好的最后 i 个不用比较
for(j=0; j<10-1-i; j++){
if(nums[j] > nums[j+1]){
temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
}
}
}
//输出排序后的数组
for(i=0; i<10; i++){
printf("%d ", nums[i]);
}
printf("\n");
return 0;
}
运行结果: 1 2 3 4 5 6 7 8 9 10
优化冒泡算法
上面的算法是大部分教材中提供的算法,其中有一点是可以优化的:当比较到第 i 轮的时候,如果剩下的元素已经排序好了,那么就不用再继续比较了,跳出循环即可,这样就减少了比较的次数,提高了执行效率。
未经优化的算法一定会进行 n-1 轮比较,经过优化的算法最多进行 n-1 轮比较,高下立判。
优化后的算法实现如下所示:
#include <stdio.h>
int main(){
int nums[10] = {4, 5, 2, 10, 7, 1, 8, 3, 6, 9};
int i, j, temp, isSorted;
//优化算法:最多进行 n-1 轮比较
for(i=0; i<10-1; i++){
isSorted = 1; //假设剩下的元素已经排序好了
for(j=0; j<10-1-i; j++){
if(nums[j] > nums[j+1]){
temp = nums[j];
nums[j] = nums[j+1];
nums[j+1] = temp;
isSorted = 0; //一旦需要交换数组元素,就说明剩下的元素没有排序好
}
}
if(isSorted) break; //如果没有发生交换,说明剩下的元素已经排序好了
}
for(i=0; i<10; i++){
printf("%d ", nums[i]);
}
printf("\n");
return 0;
}
我们额外设置了一个变量 isSorted,用它作为标志,值为“真”表示剩下的元素已经排序好了,值为“假”表示剩下的元素还未排序好。
每一轮比较之前,我们预先假设剩下的元素已经排序好了,并将 isSorted 设置为“真”,一旦在比较过程中需要交换元素,就说明假设是错的,剩下的元素没有排序好,于是将 isSorted 的值更改为“假”。
每一轮循环结束后,通过检测 isSorted 的值就知道剩下的元素是否排序好。