在程序占用的整个内存中,有一块内存区域叫做栈(Stack),它是专门用来给函数分配内存的,每次调用函数,都会将相关数据压入栈中,包括局部变量、局部数组、形参、寄存器、冗余数据等。
栈是针对线程来说的,每个线程都拥有一个栈,如果一个程序包含了多个线程,那么它就拥有多个栈。
对每个线程来说,栈能使用的内存是有限的,一般是 1M~8M,这在编译时就已经决定了,程序运行期间不能再改变。如果程序使用的栈内存超出最大值,就会发生栈溢出(Stack Overflow)错误。(当然,我们也可以通过参数来修改栈内存的大小。)
递归函数内部嵌套了对自身的调用,除非等到最内层的函数调用结束,否则外层的所有函数都不会调用结束。通俗地讲,外层函数被卡主了,它要等待所有的内层函数调用完成后,它自己才能调用完成。
每一层的递归调用都会在栈上分配一块内存,有多少层递归调用就分配多少块相似的内存,所有内存加起来的总和是相当恐怖的,很容易超过栈内存的大小限制,这个时候就会导致程序崩溃。
例如,一个递归函数需要递归 10000 次,每次需要 1KB 的内存,那么最终就需要 10MB 的内存。

形参(形式参数)

在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。

实参(实际参数)

函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。

形参和实参的区别和联系

  1. 形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。
  2. 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。
  3. 实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。
  4. 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参。

缓冲区(Buffer)又称为缓存(Cache),是内存空间的一部分。也就是说,计算机在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区(缓存)。

scanf函数如下图:

scanf流程.png

注:
当控制字符串不是以格式控制符 %d、%c、%f 等开头时,空白符就不能忽略了,它会参与匹配过程,如果匹配失败,就意味着 scanf() 读取失败了。

scanf("%*[^\n]"); scanf("%*c");  //清空缓冲区

scanf() 完整写法:%{*} {width} type

  • type表示读取什么类型的数据,例如 %d、%s、%[a-z]、%1 等;type 必须有。
  • width表示最大读取宽度,可有可无。
  • *表示丢弃读取到的数据,可有可无。
  • int、char、float 等类型的变量用于 scanf() 时都要在前面添加&,而数组或者字符串用于 scanf() 时不用添加&,它们本身就会转换为地址。

  1. n

字符类型由单引号' '包围,字符串由双引号" "包围。

输出格式控制符:

格式控制符说明
%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输出一个字符串
%p它表示以十六进制的形式(带小写的前缀)输出数据的地址。如果写作%P,那么十六进制的前缀也将变成大写形式。

关于前缀

C语言中wchar_t(宽字符类型)是用来解决在不同编译器下使用不同长度来存储,wchar_t 类型位于 <wchar.h> 头文件中,具体长度如下:

  • 在微软编译器下,它的长度是 2,等价于 unsigned short;
  • 在GCC、LLVM/Clang 下,它的长度是 4,等价于 unsigned int。
  • 给字符串加上L前缀就变成了宽字符串,输出宽字符串可以使用 <wchar.h> 头文件中的 wprintf 函数,对应的格式控制符是%ls。
  • 在输出宽字符之前还要使用 setlocale 函数进行本地化设置,告诉程序如何才能正确地处理各个国家的语言文化。
#include <stdio.h>
#include <wchar.h>
#include <locale.h>
int main(int argc, const char * argv[]) {
    wchar_t a = L'A';  //英文字符(基本拉丁字符)
    wchar_t b = L'9';  //英文数字(阿拉伯数字)
    wchar_t c = L'中';  //中文汉字
    wchar_t d = L'国';  //中文汉字
    wchar_t e = L'。';  //中文标点
    wchar_t f = L'ヅ';  //日文片假名
    wchar_t g = L'♥';  //特殊符号
    wchar_t h = L'༄';  //藏文
    //将本地环境设置为简体中文
    setlocale(LC_ALL, "zh_CN");
    //使用专门的 putwchar 输出宽字符
    putwchar(a);  putwchar(b);  putwchar(c);  putwchar(d);
    putwchar(e);  putwchar(f);  putwchar(g);  putwchar(h);
    putwchar(L'\n');  //只能使用宽字符
    //使用通用的 wprintf 输出宽字符
    wprintf(
            L"Wide chars: %lc %lc %lc %lc %lc %lc %lc %lc\n",  //必须使用宽字符串
            a, b, c, d, e, f, g, h
            );
    return 0;
}

关于后缀

一个数字,是有默认类型的:对于整数,默认是 int 类型;对于小数,默认是 double 类型。不想让数字使用默认的类型,那么可以给数字加上后缀,手动指明类型:

  • 在整数后面紧跟 l 或者 L(不区分大小写)表明该数字是 long 类型;
  • 在小数后面紧跟 f 或者 F(不区分大小写)表明该数字是 float 类型。
#include <stdio.h>

int main(int argc, const char * argv[]) {
    long a = 100;
    int b = 294;
    float x = 52.55;
    double y = 18.6;
    /*
     100 和 294 这两个数字默认都是 int 类型的,将 100 赋值给 a,必须先从 int 类型转换为 long 类型,而将 294 赋值给 b 就不用转换了。
     
     52.55 和 18.6 这两个数字默认都是 double 类型的,将 52.55 赋值给 x,必须先从 double 类型转换为 float 类型,而将 18.6 赋值给 y 就不用转换了。
     */
    long c = 100l;
    int d = 294;
    short e = 32L;
    
    float f = 52.55f;
    double g = 18.6F;
    float h = 0.02;
    /*
     加上后缀,虽然数字的类型变了,但这并不意味着该数字只能赋值给指定的类型,它仍然能够赋值给其他的类型,只要进行了一下类型转换就可以了。
     */
    return 0;
}

关于printf()

printf() 格式控制符的完整形式:%flag[.precision]type
注:[ ] 表示此处的内容可有可无,是可以省略的。

  1. type 表示输出类型,比如 %d、%f、%c、%lf,type 就分别对应 d、f、c、lf;
  2. width 表示最小输出宽度,也就是至少占用几个字符的位置;
  3. .precision 表示输出精度,也就是小数的位数。
  4. flag 是标志字符。例如,%#x中 flag 对应 #,%-9d中 flags 对应-。
标志字符含 义
--表示左对齐。如果没有,就按照默认的对齐方式,默认一般为右对齐。
+用于整数或者小数,表示输出符号(正负号)。如果没有,那么只有负数才会输出符号。
空格用于整数或者小数,输出值为正时冠以空格,为负时冠以负号。
#- 对于八进制(%o)和十六进制(%x / %X)整数,# 表示在输出时添加前缀;八进制的前缀是 0,十六进制的前缀是 0x / 0X。- 对于小数(%f / %e / %g),# 表示强迫输出小数点。如果没有小数部分,默认是不输出小数点的,加上 # 以后,即使没有小数部分也会带上小数点。

注:

  • 当小数部分的位数大于 precision 时,会按照四舍五入的原则丢掉多余的数字;
  • 当小数部分的位数小于 precision 时,会在后面补 0。
    .precision 也可以用于整数和字符串,但是功能却是相反的:
  • 用于整数时,.precision 表示最小输出宽度。与 width 不同的是,整数的宽度不足时会在左边补 0,而不是补空格。
  • 用于字符串时,.precision 表示最大输出宽度,或者说截取字符串。当字符串的长度大于 precision 时,会截掉多余的字符;当字符串的长度小于 precision 时,.precision 就不再起作用。

ps:printf() 执行结束以后数据并没有直接输出到显示器上,而是放入了缓冲区,直到遇见换行符n才将缓冲区中的数据输出到显示器上。

输入格式控制符

格式控制符说明
%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读取一个字符串(以空白符为结束)

使用编码值来间接地表示字符的方式称为转义字符(Escape Character)。

转义字符以或者x开头,以开头表示后跟八进制形式的编码值,以x开头表示后跟十六进制形式的编码值。对于转义字符来说,只能使用八进制或者十六进制。
转义字符的初衷是用于 ASCII 编码,所以它的取值范围有限:

  • 八进制形式的转义字符最多后跟三个数字,也即ddd,最大取值是177;
  • 十六进制形式的转义字符最多后跟两个数字,也即xdd,最大取值是7f。

ps:字符类型由单引号' '包围,字符串由双引号" "包围。

#include <stdio.h>
int main(int argc, const char * argv[]) {
    char a = '\61';//字符1
    char b = '\141';//字符a
    char c = '\x31';//字符1
    char d = '\x61';//字符a
    printf("a=%c\nb=%c\nc=%c\nd=%c\n",a,b,c,d);
    return 0;
}