puts(“C语言中文网”); 这里有一个生疏的词汇puts,用来让计算机在屏幕上显示文字。 更加专业的称呼: "在屏幕上显示文字"叫做输出(Output); 每个文字都是一个字符(Character); 多个字符组合起来,就是一个字符序列,叫做字符串(String)。 puts 是 output string 的缩写,意思是”输出字符串“。 在C语言中,字符串需要用双引号" "包围起来,C语言中文网什么也不是,计算机不认识它,“C语言中文网"才是字符串。 puts 在输出字符串的时候,需要将字符串放在( )内。 在汉语和英语中,分别使用。和.表示一句话的结束,而在C语言中,使用;表示一个语句的结束。puts(“C语言中文网”)表达了完整的意思,是一个完整的语句,需要在最后加上;,表示当前语句结束了。 总结起来,上面的语句可以分为三个部分: puts( )命令计算机输出字符串; "C语言中文网"是要输出的内容; ;表示语句结束。 字符串输出 用puts代替printf 效率更高 一些相似的中英文标点符号: 中文分号;和英文分号;; 中文逗号,和英文逗号,; 中文冒号:和英文冒号:; 中文括号()和英文括号(); 中文问号?和英文问号?; 中文单引号’‘和英文单引号’ '; 中文双引号“ ”和英文双引号” "。 严格区分语法,避免低级错误 另外最重要的一点是:“相同”字符在全角和半角状态下对应的编码值(例如 Unicode 编码、GBK 编码等)不一样,所以它们是不同的字符。 每种编程语言的源文件都有特定的后缀,以方便被编译器识别,被程序员理解。源文件后缀大都根据编程语言本身的名字来命名,例如: C语言源文件的后缀是.c; C++语言(C Plus Plus)源文件的后缀是.cpp; Java 源文件的后缀是.java; Python 源文件的后缀是.py; JavaScript 源文件后置是.js。 我们平时所说的程序,是指双击后就可以直接运行的程序,这样的程序被称为可执行程序(Executable Program)。在 Windows 下,可执行程序的后缀有.exe和.com(其中.exe比较常见);在类 UNIX 系统(Linux、Mac OS 等)下,可执行程序没有特定的后缀,系统根据文件的头部信息来判断是否是可执行程序。
可执行程序的内部是一系列计算机指令和数据的集合,它们都是二进制形式的,CPU 可以直接识别,毫无障碍;但是对于程序员,它们非常晦涩,难以记忆和使用。 嵌入式系统 而在嵌入式系统方面,可用的C语言编译器就非常丰富了,比如: 用于 Keil 公司 51 系列单片机的 Keil C51 编译器; 当前大红大紫的 Arduino 板搭载的开发套件,可用针对 AVR 微控制器的 AVR GCC 编译器; ARM 自己出的 ADS(ARM Development Suite)、RVDS(RealView Development Suite)和当前最新的 DS-5 Studio; DSP 设计商 TI(Texas Instruments)的 CCS(Code Composer Studio); DSP 设计商 ADI(Analog Devices,Inc.)的 Visual DSP++ 编译器,等等。
通常,用于嵌入式系统开发的编译工具链都没有免费版本,而且一般需要通过国内代理进行购买。所以,这对于个人开发者或者嵌入式系统爱好者而言是一道不低的门槛。
不过 Arduino 的开发套件是可免费下载使用的,并且用它做开发板连接调试也十分简单。Arduino 所采用的C编译器是基于 GCC 的。
还有像树莓派(Raspberry Pi)这种迷你电脑可以直接使用 GCC 和 Clang 编译器。此外,还有像 nVidia 公司推出的 Jetson TK 系列开发板也可直接使用 GCC 和 Clang 编译器。树莓派与 Jetson TK 都默认安装了 Linux 操作系统。
在嵌入式领域,一般比较低端的单片机,比如 8 位的 MCU 所对应的C编译器可能只支持 C90 标准,有些甚至连 C90 标准的很多特性都不支持。因为它们一方面内存小,ROM 的容量也小;另一方面,本身处理器机能就十分有限,有些甚至无法支持函数指针,因为处理器本身不包含通过寄存器做间接过程调用的指令。
而像 32 位处理器或 DSP,一般都至少能支持 C99 标准,它们本身的性能也十分强大。而像 ARM 出的 RVDS 编译器甚至可用 GNU 语法扩展。
下图展示了上述C语言编译器的分类。
int 一般占用 4 个字节(Byte)的内存,共计 32 位(Bit)。如果不考虑正负数,当所有的位都为 1 时它的值最大,为 232-1 = 4,294,967,295 ≈ 43亿,这是一个很大的数,实际开发中很少用到,而诸如 1、99、12098 等较小的数使用频率反而较高。 只有 short 的长度是确定的,是两个字节,而 int 和 long 的长度无法确定,在不同的环境下有不同的表现。short 至少占用 2 个字节。 int 建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节。 short 的长度不能大于 int,long 的长度不能小于 int。 在 32 位环境以及 Win64 环境下的运行结果为: short=2, int=4, long=4, char=1 在 64 位 Linux 和 Mac OS 下的运行结果为: short=2, int=4, long=8, char=1 %hd用来输出 short int 类型,hd 是 short decimal 的简写; %d用来输出 int 类型,d 是 decimal 的简写; %ld用来输出 long int 类型,ld 是 long decimal 的简写。 为了解决这个问题,C语言推出了一种新的类型,叫做 wchar_t。w 是 wide 的首字母,t 是 type 的首字符,wchar_t 的意思就是宽字符类型。wchar_t 的长度由编译器决定:wchar_t 类型位于 <wchar.h> 头文件中,它使得代码在具有良好移植性的同时,也节省了不少内存,以后我们就用它来存储宽字符。
上节我们讲到,单独的字符由单引号’ ‘包围,例如’B’、’@’、‘9’等;但是,这样的字符只能使用 ASCII 编码,要想使用宽字符的编码方式,就得加上L前缀,例如L’A’、L’9’、L’中’、L’国’、L’。’。
注意,加上L前缀后,所有的字符都将成为宽字符,占用 2 个字节或者 4 个字节的内存,包括 ASCII 中的英文字符。 putchar、printf 只能输出不加L前缀的窄字符,对加了L前缀的宽字符无能为力,我们必须使用 <wchar.h> 头文件中的宽字符输出函数,它们分别是 putwchar 和 wprintf: putwchar 函数专门用来输出一个宽字符,它和 putchar 的用法类似; wprintf 是通用的、格式化的宽字符输出函数,它除了可以输出单个宽字符,还可以输出宽字符串(稍后讲解)。宽字符对应的格式控制符为%lc。 <stdio.h> 头文件中的 putchar、puts、printf 函数只能用来处理窄字符; <wchar.h> 头文件中的 putwchar、wprintf 函数只能用来处理宽字符。
getchar() 最容易理解的字符输入函数是 getchar(),它就是scanf("%c", c)的替代品,除了更加简洁,没有其它优势了;或者说,getchar() 就是 scanf() 的一个简化版本。下面的代码演示了 getchar() 的用法: 1.#include <stdio.h> 2.int main() 3.{ 4. char c; 5. c = getchar(); 6. printf(“c: %c\n”, c); 7. 8. return 0; 9.} 输入示例: @↙ c: @ 你也可以将第 4、5 行的语句合并为一个,从而写作: char c = getchar(); 2) getche() getche() 就比较有意思了,它没有缓冲区,输入一个字符后会立即读取,不用等待用户按下回车键,这是它和 scanf()、getchar() 的最大区别。请看下面的代码: 1.#include <stdio.h> 2.#include <conio.h> 3.int main() 4.{ 5. char c = getche(); 6. printf(“c: %c\n”, c); 7. 8. return 0; 9.} 输入示例: @c: @ 输入@后,getche() 立即读取完毕,接着继续执行 printf() 将字符输出,所以没有按下回车键程序就运行结束了。
注意,getche() 位于 conio.h 头文件中,而这个头文件是 Windows 特有的,Linux 和 Mac OS 下没有包含该头文件。换句话说,getche() 并不是标准函数,默认只能在 Windows 下使用,不能在 Linux 和 Mac OS 下使用。 3) getch() getch() 也没有缓冲区,输入一个字符后会立即读取,不用按下回车键,这一点和 getche() 相同。getch() 的特别之处是它没有回显,看不到输入的字符。所谓回显,就是在控制台上显示出用户输入的字符;没有回显,就不会显示用户输入的字符,就好像根本没有输入一样。
回显在大部分情况下是有必要的,它能够与用户及时交互,让用户清楚地看到自己输入的内容。但在某些特殊情况下,我们却不希望有回显,例如输入密码,有回显是非常危险的,容易被偷窥。
getch() 使用举例: 1.#include <stdio.h> 2.#include <conio.h> 3.int main() 4.{ 5. char c = getch(); 6. printf(“c: %c\n”, c); 7. 8. return 0; 9.} 输入@后,getch() 会立即读取完毕,接着继续执行 printf() 将字符输出。但是由于 getch() 没有回显,看不到输入的@字符,所以控制台上最终显示的内容为c: @。
注意,和 getche() 一样,getch() 也位于 conio.h 头文件中,也不是标准函数,默认只能在 Windows 下使用,不能在 Linux 和 Mac OS 下使用。 对三个函数的总结 函数 缓冲区 头文件 回显 适用平台 getchar() 有 stdio.h 有 Windows、Linux、Mac OS 等所有平台 getche() 无 conio.h 有 Windows getch() 无 conio.h 无 Windows
关于缓冲区,我们将在下节《进入缓冲区(缓存)的世界,破解一切与输入输出有关的疑难杂症》中展开讲解。 输入字符串 输入字符串当然可以使用 scanf() 这个通用的输入函数,对应的格式控制符为%s,上节已经讲到了;本节我们重点讲解的是 gets() 这个专用的字符串输入函数,它拥有一个 scanf() 不具备的特性。
gets() 的使用也很简单,请看下面的代码: 1.#include <stdio.h> 2.int main() 3.{ 4. char author[30], lang[30], url[30]; 5. gets(author); 6. printf(“author: %s\n”, author); 7. gets(lang); 8. printf(“lang: %s\n”, lang); 9. gets(url); 10. printf(“url: %s\n”, url); 11. 12. return 0; 13.} 运行结果: YanChangSheng↙ author: YanChangSheng C-Language↙ lang: C-Language http://c.biancheng.net http://biancheng.net↙ url: http://c.biancheng.net http://biancheng.net gets() 是有缓冲区的,每次按下回车键,就代表当前输入结束了,gets() 开始从缓冲区中读取内容,这一点和 scanf() 是一样的。gets() 和 scanf() 的主要区别是: scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。 gets() 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束,所以,不管输入了多少个空格,只要不按下回车键,对 gets() 来说就是一个完整的字符串。
也就是说,gets() 能读取含有空格的字符串,而 scanf() 不能。 总结 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() 每次只能读取一份特定类型的数据,不能一次性读取多份数据。 清空输出缓冲区 清空输出缓冲区很简单,使用下面的语句即可: fflush(stdout); fflush() 是一个专门用来清空缓冲区的函数,stdout 是 standard output 的缩写,表示标准输出设备,也即显示器。整个语句的意思是,清空标准输入缓冲区,或者说清空显示器的缓冲区。
Windows 平台下的 printf()、puts()、putchar() 等输出函数都是不带缓冲区的,所以不用清空 使用 scanf() 清空缓冲区 scanf() 还有一种高级用法,就是使用类似于正则表达式的通配符,这样它就可以读取所有的字符了,包括空格、换行符、制表符等空白符,不会再忽略它们了。并且,scanf() 还允许把读取到的数据直接丢弃,不用赋值给变量。
请看下面的语句: scanf("%*[^\n]"); scanf("%*c"); 第一个 scanf() 将逐个读取缓冲区中\n之前的其它字符,% 后面的 * 表示将读取的这些字符丢弃,遇到\n字符时便停止读取。此时,缓冲区中尚有一个\n遗留,第二个 scanf() 再将这个\n读取并丢弃,这里的星号和第一个 scanf() 的星号作用相同。由于所有从键盘的输入都是以回车结束的,而回车会产生一个\n字符,所以将\n连同它之前的字符全部读取并丢弃之后,也就相当于清除了输入缓冲区。 然而在实际开发中,往往都是几千行、上万行、百万行的代码,将这些代码都放在一个源文件中简直是灾难,不但检索麻烦,而且打开文件也很慢,所以必须将这些代码分散到多个文件中。对于多个文件的程序,通常是将函数定义放到源文件(.c文件)中,将函数的声明放到头文件(.h文件)中,使用函数时引入对应的头文件就可以,编译器会在链接阶段找到函数体。 在一个函数内部修改全局变量的值会影响其它函数,全局变量的值在函数内部被修改后并不会自动恢复,它会一直保留该值,直到下次被修改。C语言允许在代码块内部定义变量,这样的变量具有块级作用域;换句话说,在代码块内部定义的变量只能在代码块内部使用,出了代码块就无效了。 双层递归的调用关系和数据结构中二叉树的结构完全吻合,所以双层递归常用于二叉树的遍历。
单层递归每次只等待一个函数的结果,双层递归每次要等待两个函数的结果,这就是它们之间最本质的区别。 标准C语言(ANSI C)共定义了15 个头文件,称为“C标准库”,所有的编译器都必须支持,如何正确并熟练的使用这些标准库,可以反映出一个程序员的水平。 合格程序员:<stdio.h>、<ctype.h>、<stdlib.h>、<string.h> 熟练程序员:<assert.h>、<limits.h>、<stddef.h>、<time.h> 优秀程序员:<float.h>、<math.h>、<error.h>、<locale.h>、<setjmp.h>、<signal.h>、<stdarg.h>
以上各类函数不仅数量众多,而且有的还需要硬件知识才能使用,初学者要想全部掌握得需要一个较长的学习过程。我的建议是先掌握一些最基本、最常用的函数,在实践过程中再逐步深入。由于课时关系,本教程只介绍了很少一部分库函数,其余部分读者可根据需要查阅C语言函数手册,网址是 http://www.cplusplus.com。 include 的用法有两种,如下所示: #include <stdHeader.h> #include “myHeader.h” 使用尖括号< >和双引号" “的区别在于头文件的搜索路径不同: 使用尖括号< >,编译器会到系统路径下查找头文件; 而使用双引号” ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。 在头文件中定义定义函数和全局变量」这种认知是原则性的错误!不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误。对 #define 用法的几点说明 1.1) 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。#define PI 3.14159
3.int main(){ 4. // Code 5. return 0; 6.} 7. 8.#undef PI 9. 10.void func(){ 11. // Code 12.} 表示 PI 只在 main() 函数中有效,在 func() 中无效。
代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替,例如: 1.#include <stdio.h> 2.#define OK 100 3.int main(){ printf(“OK\n”);return 0; 6.} 运行结果: OK该例中定义宏名 OK 表示 100,但在 printf 语句中 OK 被引号括起来,因此不作宏替换,而作为字符串处理。
宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。例如: #define PI 3.1415926 #define S PIyy /* PI是已定义的宏名*/ 对语句: printf("%f", S); 在宏代换后变为: printf("%f", 3.1415926yy);
习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
可用宏定义表示数据类型,使书写方便。例如: #define UINT unsigned int 在程序中可用 UINT 作变量说明: UINT a, b; 应注意用宏定义表示数据类型和用 typedef 定义数据说明符的区别。宏定义只是简单的字符串替换,由预处理器来处理;而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。
请看下面的例子: #define PIN1 int * typedef int *PIN2; //也可以写作typedef int (*PIN2); 从形式上看这两者相似, 但在实际使用中却不相同。
下面用 PIN1,PIN2 说明变量时就可以看出它们的区别: PIN1 a, b; 在宏代换后变成: int * a, b; 表示 a 是指向整型的指针变量,而 b 是整型变量。然而: PIN2 a,b; 表示 a、b 都是指向整型的指针变量。因为 PIN2 是一个新的、完整的数据类型。由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟只是简单的字符串替换。在使用时要格外小心,以避出错。 ANSI C 规定了以下几个预定义宏,它们在各个编译器下都可以使用: __LINE__:表示当前源代码的行号; __FILE__:表示当前源文件的名称; __DATE__:表示当前的编译日期; __TIME__:表示当前的编译时间; __STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1; __cplusplus:当编写C++程序时该标识符被定义。
预定义宏演示: 1.#include <stdio.h> 2.#include <stdlib.h> 3. 4.int main() { 5. printf(“Date : %s\n”, DATE); 6. printf(“Time : %s\n”, TIME); 7. printf(“File : %s\n”, FILE); 8. printf(“Line : %d\n”, LINE); 9. 10. system(“pause”); 11. return 0; 12.} VS下的输出结果: Date : Mar 6 2016 Time : 11:47:15 File : main.c Line : 8
C-Free 5.0 下的输出结果: Date : Mar 6 2016 Time : 12:12:59 File : C:\Users\mozhiyan\Desktop\demo.c Line : 8 < 宏参数的字符串化和宏参数的连接C语言条件编译 > 所有教程 计算机中的数据是以字节(Byte)为单位存储的,每个字节都有不同的地址。现代 CPU 的位数(可以理解为一次能处理的数据的位数)都超过了 8 位(一个字节),PC机、服务器的 CPU 基本都是 64 位的,嵌入式系统或单片机系统仍然在使用 32 位和 16 位的 CPU。
对于一次能处理多个字节的CPU,必然存在着如何安排多个字节的问题,也就是大端和小端模式。以 int 类型的 0x12345678 为例,它占用 4 个字节,如果是小端模式(Little-endian),那么在内存中的分布情况为(假设从地址 0x 4000 开始存放): 内存地址 0x4000 0x4001 0x4002 0x4003 存放内容 0x78 0x56 0x34 0x12
如果是大端模式(Big-endian),那么分布情况正好相反: 内存地址 0x4000 0x4001 0x4002 0x4003 存放内容 0x12 0x34 0x56 0x78
我们的 PC 机上使用的是 X86 结构的 CPU,它是小端模式;51 单片机是大端模式;很多 ARM、DSP 也是小端模式(部分 ARM 处理器还可以由硬件来选择是大端模式还是小端模式)。 这就是加密的关键技术: 通过一次异或运算,生成密文,密文没有可读性,与原文风马牛不相及,这就是加密; 密文再经过一次异或运算,就会还原成原文,这就是解密的过程; 加密和解密需要相同的密钥,如果密钥不对,是无法成功解密的。
上面的加密算法称为对称加密算法,加密和解密使用同一个密钥。
如果加密和解密的密钥不同,则称为非对称加密算法。在非对称算法中,加密的密钥称为公钥,解密的密钥称为私钥,只知道公钥是无法解密的,还必须知道私钥。