Table of Contents:

编程基础

C语言究竟是一门怎样的语言?

C语言除了能让你了解编程的相关概念,带你走进编程的大门,还能让你明白程序的运行原理,比如,计算机的各个部件是如何交互的,程序在内存中是一种怎样的状态,操作系统和用户程序之间有着怎样的“爱恨情仇”,这些底层知识决定了你的发展高度,也决定了你的职业生涯。

C语言概念少,词汇少,包含了基本的编程元素,后来的很多语言(C++、Java等)都参考了C语言,说C语言是现代编程语言的开山鼻祖毫不夸张。
C语言是计算机产业的核心语言

C语言是菜鸟和大神的分水岭

程序是在内存中运行的,一名合格的程序员必须了解内存,学习C语言是了解内存布局的最简单、最直接、最有效的途径,C语言简直是为内存而生的,它比任何一门编程语言都贴近内存

所有的程序都在拼尽全力节省内存,都在不遗余力提高内存使用效率,计算机的整个发展过程都在围绕内存打转,不断地优化内存布局,以保证可以同时运行多个程序。

攻克内存后我竟然也能够理解进程和线程了,原来进程和线程也是围绕内存打转的,从一定程度上讲,它们的存在也是为了更加高效地利用内存(另一方面还有CPU)。
从C语言到内存,从内存到进程和线程,环环相扣:不学C语言就吃不透内存,不学内存就吃不透进程和线程。

学编程难吗?多久能入门?

很多领域都是「一年打基础,两年见成效,三年有突破」,但是很多人在不到一年的时间里就放弃了,总觉得这个行业太难,不太适合自己。

学了C语言到底能做什么,能从事什么工作?

既然C语言能做这么多事情,为什么很多初学者学完C语言以后仍然非常迷茫,只能编写没有界面的控制台程序呢?

这是因为,C语言仅仅是一个工具,它的标准库也很简单,只提供了最基本的功能,如果希望开发出实用的程序,往往还需要学习其他领域方面的知识。例如:
* 开发硬件驱动要学习数字电路,了解 Windows 或 Linux 内核,阅读硬件厂商的接口说明书;
* 从事嵌入式开发要学习数字电路、模拟电路、ARM、Linux、Qt等;
* 开发PC软件要学习Windows编程,了解 GTK(GNU/Linux下开发图形界面的应用程序的主流开发工具之一)。

可以这么说,如果只会C语言,基本上是找不到工作的,读者要选定一个方向,继续学习其他知识。后面你会发现,C语言不过是冰山一角,是一项基本技能而已,程序员要学习的东西还很多。

ASCII编码,将英文存储到计算机

字符集字符编码不是一个概念,字符集定义了文字和二进制的对应关系,为字符分配了唯一的编号,而字符编码规定了如何将文字的编号存储到计算机中
字符集为每个字符分配一个唯一的编号,类似于学生的学号,通过编号就能够找到对应的字符。
可以将字符集理解成一个很大的表格,它列出了所有字符和二进制的对应关系,计算机显示文字或者存储文字,就是一个查表的过程
在 ASCII 编码中,大写字母、小写字母和阿拉伯数字都是连续分布的,这给程序设计带来了很大的方便。例如要判断一个字符是否是大写字母,就可以判断该字符的 ASCII 编码值是否在 65~90 的范围内
由于 ASCII 先入为主,已经使用了十来年了,现有的很多软件和文档都是基于 ASCII 的,所以后来的这些字符编码都是在 ASCII 基础上进行的扩展,它们都兼容 ASCII,以支持既有的软件和文档。
兼容 ASCII 的含义是,原来 ASCII 中已经包含的字符编码值不变,只是在这些字符的后面增添了新的字符。

为了达到「既能存储本国字符,又能节省内存」的目的,Big5、GB2312 等都采用变长编码方式:
原则是:越常用的字符占用的内存越少,越罕见的字符占用的内存越多。
* 对于原来的 ASCII 编码部分,用一个字节存储足以;
* 对于本国的常用字符(例如汉字、标点符号等),一般用两个字节存储;
* 对于偏远地区,或者极少使用的字符(例如藏文、蒙古文等),才使用三个甚至四个字节存储。

GB2312 --> GBK --> GB18030 是中文编码的三套方案,出现的时间从早到晚,收录的字符数目依次增加,并且向下兼容。
GB2312 和 GBK 收录的字符数目较少,用 1~2个字节存储;GB18030 收录的字符最多,用1、2、4 个字节存储。

本节我们多次说 Unicode是一套字符集,而不是一套字符编码,它们之间究竟有什么区别呢?
严格来说,字符集和字符编码不是一个概念:
* 字符集定义了字符和二进制的对应关系,为每个字符分配了唯一的编号。可以将字符集理解成一个很大的表格,它列出了所有字符和二进制的对应关系,计算机显示文字或者存储文字,就是一个查表的过程。
* 而字符编码规定了如何将字符的编号存储到计算机中。如果使用了类似 GB2312 和 GBK 的变长存储方案(不同的字符占用的字节数不一样),那么为了区分一个字符到底使用了几个字节,就不能将字符的编号直接存储到计算机中,字符编号在存储之前必须要经过转换,在读取时还要再逆向转换一次,这套转换方案就叫做字符编码

有的字符集在制定时就考虑到了编码的问题,是和编码结合在一起的,例如 ASCII、GB2312、GBK、BIG5 等,所以无论称作字符集还是字符编码都无所谓,也不好区分两者的概念。

而有的字符集只管制定字符的编号,至于怎么存储,那是字符编码的事情,Unicode 就是一个典型的例子,它只是定义了全球文字的唯一编号,我们还需要 UTF-8、UTF-16、UTF-32 这几种编码方案将 Unicode 存储到计算机中。

Unicode 可以使用的编码方案有三种,分别是:
* UTF-8:一种变长的编码方案,使用 1~6 个字节来存储; 兼容ANSI
* UTF-32:一种固定长度的编码方案,不管字符编号大小,始终使用 4 个字节来存储;
* UTF-16:介于 UTF-8 和 UTF-32 之间,使用 2 个或者 4 个字节来存储,长度既固定又可变。

UTF 是 Unicode Transformation Format 的缩写,意思是“Unicode转换格式”,后面的数字表明至少使用多少个比特位(Bit)来存储字符。
只有 UTF-8兼容 ASCII,UTF-32 和 UTF-16 都不兼容 ASCII,因为它们没有单字节编码

Windows 内核、.NET Framework、Cocoa、Java String 内部采用的都是 UTF-16 编码。UTF-16 是幕后的功臣,我们在编辑源代码和文档时都是站在前台,所以一般感受不到,其实很多文本在后台处理时都已经转换成了 UTF-16 编码。

乱码问题

造成乱码的原因就是因为使用了错误的字符编码去解码字节流,因此当我们在思考任何跟文本显示有关的问题时,
请时刻保持清醒:当前使用的字符编码是什么
例如最常见的网页乱码问题。如果你是网站技术人员,遇到这样的问题,需要检查以下原因:
* 服务器返回的响应头Content-Type没有指明字符编码
* 网页内是否使用META HTTP-EQUIV标签指定了字符编码
* 网页文件本身存储时使用的字符编码和网页声明的字符编码是否一致

宽字符和窄字符(多字节字符)

多字节字符(或窄字符 multibyte character): 采用 1~n 个字节存储,是变长的,例如 UTF-8、GB2312、GBK 等;
宽字符(wide character):编码方式是固定长度的,不管字符编号大小,始终采用 n 个字节存储,例如 UTF-32、UTF-16 等
多字节字符和宽字符(也就是wchar_t)的主要差异在于宽字符占用的字节数目都一样,而多字节字符的字节数目不等,这样的表示方式使得多字节字符串比宽字符串更难处理
Unicode 字符集可以使用窄字符的方式存储,也可以使用宽字符的方式存储;GB2312、GBK、Shift-JIS 等国家编码一般都使用窄字符的方式存储;ASCII 只有一个字节,无所谓窄字符和宽字符。

在计算机屏幕上,一个汉字要占两个英文字符的位置,人们把一个英文字符所占的位置称为“半角”,相对地把一个汉字所占的位置称为“全角”。
【wchar_t】
char是8位字符类型,最多只能包含256种字符,许多外文字符集所含的字符数目超过256个,char型无法表示。
wchar_t数据类型一般为16位或32位,但不同的C或C++库有不同的规定,如GNU Libc规定wchar_t为32位,总之,wchar_t所能表示的字符数远超char型。
【L"string"】
在字符串前加一个L作用: unicode字符集是两个字节组成的。L告示编译器使用两个字节的 unicode 字符集
如 L"我的字符串" 表示将ANSI字符串转换成unicode的字符串,就是每个字符占用两个字节。
strlen("asd") = 3;
strlen(L"asd") = 6;
_T
_T宏可以把一个引号引起来的字符串,根据你的环境设置,使得编译器会根据编译目标环境选择合适的(Unicode还是ANSI)字符处理方式
如果你定义了UNICODE,那么_T宏会把字符串前面加一个L。这时 _T("ABCD") 相当于 L"ABCD" ,这是宽字符串,返回的类型是wchar_t.
如果没有定义,那么_T宏不会在字符串前面加那个L,_T("ABCD") 就等价于 "ABCD"
如果你接触过unicode你就明白在许多API函数中字符串都需要宽字符的,也就是用两个字节来表示一个字符,这与ANSI字符不同,后者使用一个字节表示一个字符,字符串前加L,就是将ANSI字符转换成UNICODE字符。

在C语言中使用中文字符

正确地存储中文字符需要解决两个问题。
1) 足够长的数据类型
char 只能处理 ASCII 编码中的英文字符,是因为 char 类型太短,只有一个字节,容纳不下几万汉字,要想处理中文字符,必须得使用更长的数据类型。
一个字符在存储之前会转换成它在字符集中的编号,而这样的编号是一个整数,所以我们可以用整数类型来存储一个字符,比如 unsigned short、unsigned int、unsigned long 等。
2) 选择包含中文的字符集
C语言规定,对于汉语、日语、韩语等 ASCII 编码之外的单个字符,也就是专门的字符类型,要使用宽字符的编码方式。常见的宽字符编码有 UTF-16 和 UTF-32,它们都是基于 Unicode 字符集的,能够支持全球的语言文化。

在真正实现时,微软编译器(内嵌于 Visual Studio 或者 Visual C++ 中)采用 UTF-16 编码,使用 2 个字节存储一个字符,用 unsigned short 类型就可以容纳。GCC、LLVM/Clang(内嵌于 Xcode 中)采用 UTF-32 编码,使用 4 个字节存储字符,用 unsigned int 类型就可以容纳。

编码字符集和运行字符集
站在专业的角度讲,源文件使用的字符集被称为编码字符集,也就是写代码的时候使用的字符集;
程序中的字符或者字符串使用的字符集被称为运行字符集,也就是程序运行后使用的字符集。

源文件需要保存到硬盘,或者在网络上传输,使用的编码要尽量节省存储空间,同时要方便跨国交流,所以一般使用 UTF-8,这就是选择编码字符集的标准。
程序中的字符或者字符串,在程序运行后必须被载入到内存,才能进行后续的处理,对于这些字符来说,要尽量选用能够提高处理速度的编码,例如 UTF-16 和 UTF-32 编码就能够快速定位(查找)字符。

std::string s = "你好";  // s.size() = 6
std::string s = "ab";    // s.size() = 2
char str1[] = "你好";  // sizeof(str1) = 7
char str2[] = "ab";    // sizeof(str1) = 3

C语言转义字符

转义字符以\或者\x开头,以\开头表示后跟八进制形式的编码值,以\x开头表示后跟十六进制形式的编码值。对于转义字符来说,只能使用八进制或者十六进制。

C语言初探

C语言的三套标准:C89、C99和C11

增加了安全函数,例如 gets_s()、fopen_s() 等;
增加了 <threads.h> 头文件以支持多线程;
增加了 <uchar.h> 头文件以支持 Unicode 字符集;
以及其它一些细节。

变量和数据类型

数据类型可理解为创建变量的模具(模子);是固定内存大小的别名
数据类型的作用:编译器 预算对象(变量)分配的内存空间大小
变量本质:(一段连续)内存空间的别名
1、程序通过变量来申请和命名内存空间 int a = 0
2、通过变量名访问内存空间
sizeof是操作符,不是函数;sizeof测量的实体大小为编译期间就已确定

修改变量的3种方法:
* 1、直接
* 2、间接(指针)。内存有地址编号,拿到地址编号也可以修改内存;
* 3、c++ 引用

大话C语言变量和数据类型

int a;创造了一个变量 a,我们把这个过程叫做变量定义
a=123;把 123 交给了变量 a,我们把这个过程叫做给变量赋值
又因为是第一次赋值,也称变量的初始化,或者赋初值。

数据类型用来说明数据的类型,确定了数据的解释方式,让计算机和程序员不会产生歧义
数据是放在内存中的,在内存中存取数据要明确三件事情:
1. 数据存储在哪里
2. 数据的长度
3. 数据的处理方式。

实际情况也确实如此,C语言并没有严格规定 short、int、long 的长度,只做了宽泛的限制,移植时会带来问题:
* short 至少占用 2 个字节。
* int 建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节。
* short 的长度不能大于 int,long 的长度不能小于 int。
总结起来,它们的长度(所占字节数)关系为:
2 ≤ short ≤ int ≤ long

整数在内存中是如何存储的,为什么它堪称天才般的设计

C语言中的小数(float,double)

不像整数,小数没有那么多幺蛾子,小数的长度是固定的,float 始终占用4个字节,double 始终占用8个字节。
一个数字,是有默认类型的:对于整数,默认是 int 类型;对于小数,默认是double类型

如果不想让数字使用默认的类型,那么可以给数字加上后缀,手动指明类型:
* 在整数后面紧跟 l 或者 L(不区分大小写)表明该数字是 long 类型;
* 在小数后面紧跟 f 或者 F(不区分大小写)表明该数字是 float 类型。

加上后缀,虽然数字的类型变了,但这并不意味着该数字只能赋值给指定的类型,它仍然能够赋值给其他的类型,只要进行了一下类型转换就可以了。

将一个小数赋值给整数类型,就得把小数部分丢掉,只能取整数部分,这会改变数字本来的值
由于将小数赋值给整数类型时会“失真”,所以编译器一般会给出警告,让大家引起注意
c++用列表初始化若精度有丢失的话,编译器会报错的。

小数在内存中是如何存储的

小数在内存中是以浮点数的形式存储的。浮点数是数字在内存中的一种存储格式,它和定点数是相对的。

C语言使用定点格式存储整数,使用浮点格式存储小数,这是在“数值范围”和“数值精度”两项重要指标之间追求平衡的结果
浮点数和定点数中的“点”指的就是小数点

定点格式来存储小数,优点是精度高,因为所有的位都用来存储有效数字了,缺点是取值范围太小,不能表示很大或者很小的数字。
反面例子
在科学计算中,小数的取值范围很大,最大值和最小值的差距有上百个数量级,使用定点数来存储将变得非常困难。
例如,电子的质量为:
0.0000000000000000000000000009 克 = 9 × 10-28 克
太阳的质量为:
2000000000000000000000000000000000 克 = 2 × 1033 克
如果使用定点数,那么只能按照=前面的格式来存储,这将需要很大的一块内存,大到需要几十个字节。
更加科学的方案是按照=后面的指数形式来存储,这样不但节省内存,也非常直观。这种以指数的形式来存储小数的解决方案就叫做浮点数。浮点数是对定点数的升级和优化,克服了定点数取值范围太小的缺点。

C语言标准规定,小数在内存中以科学计数法的形式来存储,具体形式为:

flt = (-1)^sign × mantissa × base^exponent
base因其固定,可以省略

比如:19.625 = (-1)^0 x 1.9625 × 10^1

C语言中的几个重要概念

表达式必须有一个执行结果,这个结果必须是一个值,例如3*4+5的结果 17,a=c=d=10的结果是 10,printf("hello")的结果是 5(printf 的返回值是成功打印的字符的个数)。
以分号;结束的往往称为语句,而不是表达式,例如3*4+5;a=c=d;等。

C语言加减乘除运算

当除数和被除数都是整数时,运算结果也是整数;如果不能整除,那么就直接丢掉小数部分,只保留整数部分,这跟将小数赋值给整数类型是一个道理。
一旦除数和被除数中有一个是小数,那么运算结果也是小数,并且是 double 类型的小数。

取余,也就是求余数,使用的运算符是 %。C语言中的取余运算只能针对整数,也就是说,% 的两边都必须是整数,不能出现小数,否则编译器会报错。
余数可以是负数,由 % 左边的整数决定:如果 % 左边是负数,那么余数也是负数。如:-5%2=-1

C语言变量的定义位置以及初始值

为了让编译器方便给变量分配内存,C89 标准规定,所有的局部变量(函数内部的变量)都必须定义在函数的开头位置,在定义完所有变量之前不能有其它的表达式。因为不方便,所以后来的 C99 标准就取消了这个限制。

一个变量,即使不给它赋值,它也会有一个默认的值,这个值就是默认初始值
对于全局变量,它的默认初始值始终是 0,因为全局变量存储在内存分区中的全局数据区,这个区域中的数据在程序载入内存后会被初始化为 0。
而对于局部变量,C语言并没有规定它的默认初始值是什么,所以不同的编译器进行了不同的扩展,有的编译器会初始化为 0,有的编译器放任不管,爱是什么就是什么。

C语言运算符的优先级和结合性

+、-、*、/、= 是双目运算符;
++、-- 是单目运算符;
? : 是三目运算符(这是C语言里唯一的一个三目元算符)。

[[c总结-运算符优先级]]

C语言输入输出

char str1[] = "http://c.biancheng.net";
char *str2 = "C语言中文网";

差别
* 第一种形式的字符串所在的内存既有读取权限又有写入权限,
* 第二种形式的字符串所在的内存只有读取权限,没有写入权限。
printf()、puts() 等字符串输出函数只要求字符串有读取权限,而 scanf()gets() 等字符串输入函数要求字符串有写入权限,所以,第一种形式的字符串既可以用于输出函数又可以用于输入函数,而第二种形式的字符串只能用于输出函数。

C语言字符串的输入和输出

scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。
gets() 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束。换句话说,gets() 用来读取一整行字符串。

进入缓冲区(缓存)的世界,破解一切与输入输出有关的疑难杂症

根据清空缓冲区的时机,可以分为全缓冲、行缓冲、不带缓冲。
1) 全缓冲
在这种情况下,当缓冲区被填满以后才进行真正的输入输出操作。缓冲区的大小都有限制的,比如 1KB、4MB 等,数据量达到最大值时就清空缓冲区。
全缓冲的典型代表是对硬盘文件的读写
在实际开发中,将数据写入文件后,打开文件并不能立即看到内容,只有清空缓冲区,或者关闭文件,或者关闭程序后,才能在文件中看到内容。这种现象,就是缓冲区在作怪。
2) 行缓冲
在这种情况下,当在输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作。典型代表就是标准输入设备(也即键盘)和标准输出设备(也即显示器)。
对于 scanf(),不管用户输入多少内容,只要不按下回车键,就不进行真正的读取。这是因为 scanf() 是带有行缓冲的,用户输入的内容会先放入缓冲区,直到用户按下回车键,产生换行符\n,才会刷新缓冲区,进行真正的读取。
3) 不带缓冲
不带缓冲区,数据就没有地方缓存,必须立即进行输入输出。
getche()getch() 就不带缓冲区,输入一个字符后立即就执行了,根本不用按下回车键。
错误信息输出函数 perror() 也没有缓冲区。错误信息必须刻不容缓、立即、马上显示出来

1) 输入设备
scanf()、getchar()、gets() 就是从输入设备(键盘)上读取内容。对于输入设备,没有缓冲区将导致非常奇怪的行为,比如,我们本来想输入一个整数 947,没有缓冲区的话,输入 9 就立即读取了,根本没有机会输入 47,所以,没有输入缓冲区是不能接受的。Windows、Linux、Mac OS 在实现时都给输入设备带上了行缓冲,所以 scanf()、getchar()、gets() 在每个平台下的表现都一致。
但是在某些特殊情况下,我们又希望程序能够立即响应用户按键,例如在游戏中,用户按下方向键人物要立即转向,而且越快越好,这肯定就不能带有缓冲区了。Windows 下特有的 getche() 和 getch() 就是为这种特殊需求而设计的,它们都不带缓冲区。
2) 输出设备
printf()、puts()、putchar() 就是向输出设备(显示器)上显示内容。对于输出设备,有没有缓冲区其实影响没有那么大,顶多是晚一会看到内容,不会有功能性的障碍,所以 Windows 和 Linux、Mac OS 采用了不同的方案:
* Windows 平台下,输出设备是不带缓冲区的;
* Linux 和 Mac OS 平台下,输出设备带有行缓冲区。

缓冲区的刷新(清空)
所谓刷新缓冲区,就是将缓冲区中的内容送达到目的地。缓冲区的刷新遵循以下的规则:
* 不管是行缓冲还是全缓冲,缓冲区满时会自动刷新;
* 行缓冲遇到换行符\n时会刷新;
* 关闭文件时会刷新缓冲区;
* 程序关闭时一般也会刷新缓冲区,这个是由标准库来保障的;
* 使用特定的函数也可以手动刷新缓冲区。

C语言清空(刷新)缓冲区,从根本上消除那些奇怪的行为

对于输出操作,清空缓冲区会使得缓冲区中的所有数据立即显示到屏幕上;很明显,这些数据没有地方存放了,只能输出了。
对于输入操作,清空缓冲区就是丢弃残留字符,让程序直接等待用户输入,避免引发奇怪的行为。

清空输出缓冲区

fflush(stdout);

清空输入缓冲区

很遗憾地说,没有一种既简洁明了又适用于所有平台的清空输入缓冲区的方案。只有一种很蹩脚的方案能适用于所有平台,那就是将输入缓冲区中的数据都读取出来,但是却不使用
1) 使用 getchar() 清空缓冲区

#include <stdio.h>
int main()
{
    int a = 1, b = 2;
    char c;
    scanf("%d", &a);
    while((c = getchar()) != '\n' && c != EOF); //在下次读取前清空缓冲区
    scanf("%d", &b);
    printf("a=%d, b=%d\n", a, b);

    return 0;
}

2) 使用 scanf() 清空缓冲区
scanf() 还有一种高级用法,就是使用类似于正则表达式的通配符,这样它就可以读取所有的字符了,包括空格、换行符、制表符等空白符,不会再忽略它们了。并且,scanf() 还允许把读取到的数据直接丢弃,不用赋值给变量。

scanf("%*[^\n]"); scanf("%*c");

3) fflush(stdin)
C语言标准规定,当 fflush() 用于 stdout 时,必须要有清空输出缓冲区的作用;
但是C语言标准并没有规定 fflush() 用于 stdin 时的作用,编译器的实现者可以自由决定,所以它的行为是未定义的。
总之,fflush(stdin) 这种不标准的写法只适用于一部分编译器,通用性非常差,所以不建议使用。

C语言数组详解

我们可以通过下面的形式将数组的所有元素初始化为 0:

int nums[10] = {0};
char str[10] = {0};
float scores[10] = {0.0};

二维数组在概念上是二维的,但在内存中是连续存放的;存放有两种方式:
- 一种是按行排列, 即放完一行之后再放入第二行;
- 另一种是按列排列, 即放完一列之后再放入第二列。

在C语言中,二维数组是按行排列的。也就是先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。

C语言字符数组和字符串详解

字符数组只有在定义时才能将整个字符串一次性地赋值给它,一旦定义完了,就只能一个字符一个字符地赋值了。请看下面的例子:

char str[7];
str = "abc123";  //错误
//正确
str[0] = 'a'; str[1] = 'b'; str[2] = 'c';

C语言函数

对于单个源文件的程序,通常是将函数定义放到 main() 的后面,将函数声明放到 main() 的前面,这样就使得代码结构清晰明了,主次分明。

C语言变量的作用域,加深对全局变量和局部变量的理解

变量的使用遵循就近原则,如果在当前的局部作用域中找到了同名变量,就不会再去更大的全局作用域中查找。

C语言允许在代码块内部定义变量,这样的变量具有块级作用域;在代码块内部定义的变量只能在代码块内部使用,出了代码块就无效了。

递归的条件
要想让递归函数逐层进入再逐层退出,需要解决两个方面的问题:
* 存在限制条件,当符合这个条件时递归便不再继续。对于 factorial(),当形参 n 等于 0 或 1 时,递归就结束了。
* 每次递归调用之后越来越接近这个限制条件。对于 factorial(),每次递归调用的实参为 n - 1,这会使得形参 n 的值逐渐减小,越来越趋近于 1 或 0。

long factorial(int n) {
    if (n == 1) {
        return 1;
    }
    else {
        return factorial(n - 1) * n;  // 递归调用
    }
}

factorial() 是最简单的一种递归形式——尾递归,也就是递归调用位于函数体的结尾处。除了尾递归,还有更加烧脑的两种递归形式,分别是中间递归和多层递归:
中间递归:发生递归调用的位置在函数体的中间;
多层递归:在一个函数里面多次调用自己。

C语言多层递归函数(最烧脑的一种递归)

菲波那契数就是一个数列,数列中每个数的值就是它前面两个数的和,这种递归的复杂度是指数级别增长的
0 1 1 2 3 5 8 13

long fib(int n) {
    if (n <= 2) {
        return 1;
    }
    else {
        return fib(n - 1) + fib(n - 2);
    }
}

递归函数的致命缺陷:巨大的时间开销和内存开销

对每个线程来说,栈能使用的内存是有限的,一般是 1M~8M,这在编译时就已经决定了,程序运行期间不能再改变。如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误。

栈内存的大小和编译器有关,编译器会为栈内存指定一个最大值,在 VC/VS 下,默认是 1M,在 C-Free 下,默认是 2M,在 Linux GCC 下,默认是 8M。当然,我们也可以通过参数来修改栈内存的大小。

忽略语法细节,从整体上理解函数

从整体上看,C语言代码是由一个一个的函数构成的,除了定义和说明类的语句(例如变量定义、宏定义、类型定义等)可以放在函数外面,所有具有运算或逻辑处理能力的语句(例如加减乘除、if else、for、函数调用等)都要放在函数内部。

#include <stdio.h>
int a = 10;  
int b = a + 20;      //错误  int b = a + 20;是具有运算功能的语句,要放在函数内部。我在gcc中试了一下,这样是没问题的。
int main(){
    return 0;
}

C语言预处理命令(宏定义和条件编译)

C语言预处理命令是什么?

较之其他编程语言,C/C++ 语言更依赖预处理器#号开头的命令称为预处理命令。
编译器会将预处理的结果保存到和源文件同名的.i文件中,例如 main.c 的预处理结果在 main.i 中

C语言#include的用法详解(文件包含命令)

使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同
* 使用尖括号< >,编译器会到系统路径下查找头文件;
* 而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。

个人的习惯是使用尖括号来引入标准头文件,使用双引号来引入自定义头文件(自己编写的头文件),这样一眼就能看出头文件的区别。

「在头文件中定义定义函数和全局变量」这种认知是原则性的错误!不管是标准头文件,还是自定义头文件,都只能包含变量、结构体、函数的声明不能包含定义,定义是需要分配内存的,否则在多次引入时会引起重复定义错误。

C语言#define的用法,C语言宏定义

#define 用法的几点说明
* 1) 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以含任何字符,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
* 2) 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
* 3) 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。
* 4) 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替
* 5) 宏定义允许嵌套

宏定义只是简单的字符串替换,由预处理器来处理;
而 typedef 是在编译阶段由编译器处理的,它并不是简单的字符串替换,而给原有的数据类型起一个新的名字,将它作为一种新的数据类型。

#define 什么都能替换 代码混淆 易语言也是这样的原理

#include<stdio.h>
#include<stdlib.h>
#define 给老夫跑起来 main
#define 四大皆空 void 
#define 给老夫打印 printf
#define 给老夫pao system
四大皆空 给老夫跑起来()
{
 给老夫打印("gogogogog");
 给老夫pao("notepad");
 getchar();
}

C语言带参数的宏定义

对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。
对带参宏定义的说明:
* 1) 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。
* 2) 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。
* 3) 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。例如上面的宏定义中 (y)*(y) 表达式的 y 都用括号括起来,因此结果是正确的。
* 4) 对于带参宏定义不仅要在参数两侧加括号,还应该在整个字符串外加括号

C语言带参宏定义和函数的区别

带参数的宏和函数很相似,但有本质上的区别:宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。
带参数的宏也可以用来定义多个语句,在宏调用时,把这些语句又替换到源程序中,请看下面的例子:

#define SSSV(s1, s2, s3, v) s1 = length * width; s2 = length * height; s3 = width * height; v = width * length * height;

C语言宏参数的字符串化和宏参数的连接

# 的用法
#用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。例如有如下宏定义:

#define STR(s) #s
printf("%s", STR(c.biancheng.net));     //展开为  printf("%s", "c.biancheng.net");

##的用法
##称为连接符,用来将宏参数或其他的串连接起来。例如有如下的宏定义:

#define CON2(a, b) a##b##00
printf("%d\n", CON2(12, 34));   //展开为: printf("%d\n", 123400);

C语言中几个预定义宏

ANSI C 规定了以下几个预定义宏,它们在各个编译器下都可以使用:
* __LINE__:表示当前源代码的行号;
* __FILE__:表示当前源文件的名称;
* __DATE__:表示当前的编译日期;
* __TIME__:表示当前的编译时间;
* __STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
* __cplusplus:当编写C++程序时该标识符被定义。
* _DEBUG:VS/VC 有两种编译模式,Debug 和 Release

C语言#if##ifdef#ifndef的用法详解,C语言条件编译详解

Windows 有专有的宏_WIN32,Linux 有专有的宏__linux__
#if#elif#else#endif 这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译
#if 后面跟的是“整型常量表达式”,而 #ifdef#ifndef 后面跟的只能是一个宏名,不能是其他的。
比如:

    #if NUM == 10 || NUM == 20
        printf("NUM: %d\n", NUM);
    #else
        printf("NUM Error\n");
    #endif

VS/VC 有两种编译模式,Debug 和 Release。当以 Debug 模式编译程序时,宏 _DEBUG 会被定义。

C语言#error命令,阻止程序编译

#error 指令用于在编译期间产生错误信息,并阻止程序的编译
例如,我们的程序针对 Linux 编写,不保证兼容 Windows,那么可以这样做:

#ifdef WIN32
#error This programme cannot compile at Windows Platform
#endif

再如,当我们希望以 C++ 的方式来编译程序时,可以这样做:

#ifndef __cplusplus
#error 当前程序必须以C++方式编译
#endif

结构体

需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;
结构体变量才包含了实实在在的数据,需要内存空间来存储。
知识点有:结构体数组、结构体指针、结构体指针作为函数参数

C语言枚举类型(C语言enum用法)详解

枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。

enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };  //默认从0开始

int main(){
    enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } day;
    scanf("%d", &day);
    switch(day){
        case Mon: puts("Monday"); break;
        case Tues: puts("Tuesday"); break;
        case Wed: puts("Wednesday"); break;
        case Thurs: puts("Thursday"); break;
        case Fri: puts("Friday"); break;
        case Sat: puts("Saturday"); break;
        case Sun: puts("Sunday"); break;
        default: puts("Error!");
    }
    return 0;
}

Mon、Tues、Wed 这些名字在编译的时候都被替换成了对应的数字。这意味着,Mon、Tues、Wed 等都不是变量,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区。这就是枚举的本质。

C语言union(联合体或共用体)

作用是节省内存
结构体和union的区别在于:
结构体的各个成员会占用不同的内存,互相之间没有影响;
而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙)
共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。

一个经典的实例是:

//这个结构可以同时记录老师和学生的信息,通过profession字段来判断类型,然后再根据类型去sc union中的字段分别取值
struct{
    char name[20];
    int num;
    char sex;
    char profession;   //区分字段       
    union{
        float score;
        char course[20];
    } sc;
} ;

用union判别大端和小端

大端模式(Big-endian):低地址存最高有效位
小端模式(Little-endian):低地址存最低有效位

一般网络协议都采用大端模式进行传输,windows操作系统采用 Utf-16小端模式。

#include <stdio.h>
int main(){
    union{
        int n;
        char ch;
    } data;
    data.n = 0x00000001; //也可以直接写作 data.n = 1;
    if(data.ch == 1){    //低地址存最低有效位
        printf("Little-endian\n");
    }else{
        printf("Big-endian\n");
    }
    return 0;
}

共用体的各个成员是共用一段内存的。1 是数据的低位,如果 1 被存储在 data 的低字节,就是小端模式,这个时候 data.ch 的值也是 1。如果 1 被存储在 data 的高字节,就是大端模式,这个时候 data.ch 的值就是 0。

C语言位域

有些数据在存储时并不需要占用一个完整的字节,只需要占用一个或几个二进制位即可。
在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域,作用是节省内存
C语言标准规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int(int 默认就是 signed int);到了 C99,_Bool 也被支持了。

    //使用的时候跟结构体使用没有差别
    struct bs{ 
        unsigned m: 6;   //占用 6 位(Bit)
        unsigned n: 12;  //占用 12 位(Bit)
        unsigned p: 4;   //占用 4 位(Bit)
    };
    printf("%d\n", sizeof(struct bs));  //4  m、n、p 的位宽之和为 6+12+4 = 22,小于 32,所以它们会挨着存储,中间没有缝隙,

C语言标准并没有规定位域的具体存储方式,不同的编译器有不同的实现,但它们都尽量压缩存储空间
位域的具体存储规则如下:
* 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
* 如果成员之间穿插着非位域成员,那么不会进行压缩。
* 使用&获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号

无名位域

struct bs{
    int m: 12;
    int : 20; //该位域成员不能使用
    int n: 4;
};

无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。

C语言调试教程

一个健壮的程序,都会有30%~50%的错误处理代码,几乎用不上 assert 断言函数,我们应该将 assert 用到那些极少发生的问题下,比如Object* pObject = new Object,返回空指针,这一般都是指针内存分配出错导致的,不是我们可以控制的。这时即使你使用了容错语句,后面的代码也不一定能够正常运行,所以我们也就只能停止运行报错了。
* 调试信息的输出
Windows 操作系统提供的函数 —— OutputDebugString,这个函数非常常用,他可以向调试输出窗口输出信息(无需设置断点,执行就会输出调试信息),并且一般只在绑定了调试器的情况下才会生效,否则会被 Windows 直接忽略。这个函数在 windows.h 中被定义。
我们除了在调试器中可以看到调试字符串的输出,我们还可以借助 Sysinternals 软件公司研发的一个相当高级的工具 —— DebugView 调试信息捕捉工具,这个工具可以在随时随地捕捉 OutputDebugString 调试字符串的输出(包括发布模式构建的程序)

参考链接