C Primer Plus笔记

C语言概述

C程序的基本结构

​ 程序由一个或多个函数组成,必须有 main()函数。

​ 函数由函数头和函数体组成。函数头包括函数名、传入该函数的信息类型和函数的返回类型。通过函数名后的圆括号可识别出函数,圆括号里可能为空,可能有参数。函数体被花括号括起来,由一系列语句、声明组成。

1
2
3
4
5
6
7
8
int main(void)  //函数头
/* 以下为函数体 */
{
int q; //声明
q = 1; //语句
printf("%d is neat.\n",q); //语句
return 0;
}

多个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//* two_func.c -- 一个文件中包含两个函数 */
#include <stdio.h>
void butler(void); /* ANSI/ISO C函数原型 */
int main(void)
{
printf("I will summon the butler function.\n");
butler();
printf("Yes. Bring me some tea and writeable
DVDs.\n"); return 0;
}
void butler(void) /* 函数定义开始 */
{
printf("You rang, sir?\n");
}

该程序的输出如下:

1
2
3
I will summon the butler function.
You rang, sir?
Yes.Bring me some tea and writeable DVDs.

分析:

butler() 函数在程序中出现了3 次。第1 次是函数原型(prototype),告知编译器在程序中要使用该函数;第 2 次以函数调用(function call)的形式出现在 main()中;最后一次出现在函数定义(function definition)中,

butler()单独不运行的原因:

​ 在典型的C程序中,只有main函数会在程序启动时自动执行。其他函数不会在程序启动时自动执行,除非它们被显式地从main函数或其他函数中调用。在您提供的代码示例中,只有main函数会在程序启动时执行,因为它是程序的入口点。其他函数,如butler函数,只有在被调用时才会执行。

关键字与保留标识符

关键字是C语言的词汇。不能用它们作为标识符(如,变量名)。

保留标识符(reserved identifier),C语言已经指定了它们的用途或保留它们的使用权,如果你使用这些标识符来表示其他意思会导致一些问题。保留标识符包括那些以下划线字符开头的标识符和标准库函数名,如printf()。

本章注意点:


标识符名

可以用小写字母、大写字母、数字和下划线(_)来命名。而且,名称的第1个字符必须是字符或下划线,不能是数字

操作系统和C库经常使用以一个或两个下划线字符开始的标识符(如,_kcab),因此最好避免在自己的程序中使用这种名称。

转义序列

换行符是一个转义序列(escape sequence)。转义序列用于代表难以表示或无法输入的字符。如,\t代表Tab键,\b代表Backspace键(退格键)。每个转义序列都以反斜杠字符(\)开始。

​ 反转义符的使用通常发生在字符串文字(用双引号或单引号括起来的文本)中,以便在其中插入特殊字符或控制字符。所以,一般来说,反转义符需要在字符串文字中使用,不管是在双引号(“”)中还是单引号(‘’)中。

参数中的%d在打印时有什么作用?

%d相当于是一个占位符,其作用是指明输出值的位置。

%提醒程序,要在该处打印一个变量,d表明把变量作为十进制整数打印。

printf()函数名中的这个 f表示 “format”(格式),提醒用户这是一种格式化打印函数。


数据与C

数据类型关键字

K&R给出的关键字 C90标准添加的关键字 C99标准添加的关键字
int signed _Bool
long void _Complex
short _Imaginary
unsigned
char
float
double

注:在C语言中

​ 用int关键字来表示基本的整数类型。后3个关键字(longshortunsigned)和C90新增的signed用于提供基本整数类型的变式,例如unsigned short intlong long int

char关键字用于指定字母和其他字符(如,#、$、%和*)。另外,char类型也可以表示较小的整数。

floatdoublelong double表示带小数点的数。

_Bool 类型表示布尔值( truefalse ) , _complex_Imaginary分别表示复数和虚数。

unsigned intunsigned只用于非负值的场合。这种类型与有符号类型表示的范围不同。用于表示正负号的位现在用于表示另一个二进制位,所以无符号整型可以表示更大的数。例如,16位unsigned int允许的取值范围是0~65535,而不是-32768~32767。

不同数据类型的储存

整数的储存

整数类型通常以二进制补码形式存储在内存中。补码表示法用于表示正数、负数和零。

以转化-56为例:

  1. 确定绝对值的二进制表示:将负数的绝对值转换为正数的二进制表示。

    ​ 首先,找出-56的绝对值,即56,并将其转换为二进制。56的二进制表示为111000。

  2. 取反:将这个正数的二进制表示中的每个位都取反,即0变为1,1变为0。

    ​ 现在,将上一步得到的二进制表示中的每一位取反,0变为1,1变为0,得到000111。

  3. 加1:接下来,在取反后的二进制数上加1。

    ​ 接下来,在取反后的结果上加1。000111 + 1 = 001000。

  4. 添加符号位:符号位是二进制表示中的一个特殊位,用于表示一个数的正负。在有符号整数表示法中,符号位通常是最高位(最左边的位),用来表示整数的正负性。

    在得到的结果前面添加符号位,因为原数是负数,所以最高位为1。最终的补码表示为1001000`。

整数的最大最小值

  • C 标准对基本数据类型只规定了允许的最小大小。
  • 对于 16 位机,short 和 int 的最小取值范围是[−32767,32767];对于32位机,long的最小取值范围是[−2147483647,2147483647]。对于unsigned short和unsigned int,最小取值范围是[0,65535];对于unsigned long,最小取值范围是[0,4294967295]。long long类型是为了支持64 位的需求, 最小取值范围是[−9223372036854775807,9223372036854775807];unsigned long long 的最小取值范围是[0,18446744073709551615]

浮点数的储存

以IEEE 754标准,32位可以用于表示大约7个有效数字的浮点数,64位可以用于表示大约15到17位的有效数字。

单精度浮点数(32位)为例:

​ 遵循IEEE 754标准,这个标准定义了浮点数的二进制表示方式以及进行浮点数运算的规则。

  • 1位用于表示符号位(正负号),0表示正数,1表示负数。
  • 8位用于表示指数部分(exponent)。
  • 剩下的23位用于表示尾数部分(mantissa)。

以转化42.625为例

  1. 将整数部分转换为二进制

    将整数部分42转换为二进制,得到101010

  2. 将小数部分转换为二进制

    将小数部分0.625转换为二进制。通常,可以将小数部分乘以2并取整数部分,然后将余数作为下一位的小数部分,一直重复直到小数部分为0或者达到所需的精度。对于0.625,这个过程如下:

    • 0.625 * 2 = 1.25,整数部分是1,小数部分更新为0.25。
    • 0.25 * 2 = 0.5,整数部分是0,小数部分更新为0.5。
    • 0.5 * 2 = 1.0,整数部分是1,小数部分更新为0.0。

    因此,0.625的二进制表示为0.101

  3. 将整数部分和小数部分组合

    将整数部分101010和小数部分0.101组合在一起,得到二进制表示101010.101

  4. 规范化

    在IEEE 754中,浮点数采用科学计数法表示,其中二进制小数点位于左边的第一个非零位前面。因此,需要将二进制数规范化,将小数点移到合适的位置。在这种情况下,将小数点移到最左边,得到规范化的二进制表示为1.01010101 x 2^5

  5. 确定指数和尾数

    • 指数:因为小数点向左移了5位,所以指数为5。在IEEE 754中,还需要加上一个偏移值(127),因此指数为5 + 127 = 132。以8位表示指数,二进制为10000100
    • 尾数:小数点左边的部分是尾数,即01010101
  6. 组合符号、指数和尾数

    • 符号位:因为42.625是正数,所以符号位为0。
    • 指数:上面计算得到的8位指数为10000100
    • 尾数:上面计算得到的尾数为01010101

最终,将这些组合在一起得到32位的浮点数表示为:0 10000100 01010101000000000000000

这就是42.625的单精度浮点数表示,其中第一个位是符号位,接下来的8位是指数,剩下的23位是尾数。这个二进制表示可以转换为十进制浮点数为42.625。

注:

偏移值的作用:使次方数换算为二进制时不出现小数点

偏移值的确定即确定最小次方数:(以32位为例)

​ 32位精度有8位二进制用来表示次方数,故次方的范围数位(2**8 -1)即255。0占去1位,剩下正负进行平分,故最小的次方数位-127,故偏移值为127,加上偏移值后次方数不存在负数的情况

因使用过二进制的科学计数法,小数点前一定为1,故尾数部分只取小数点后的一部分

进制

进制书面表示

  • 1011B表示二进制1011,也记作(1011)2
  • 1357O(字母O)表示八进制1357,也记为(1357)8
  • 2049D表示为十进制2049,也记作(2049)10
  • 3FB9H表示十六进制数3FB9,也记作(3FB9)16

初始化变量时的进制表示

  • 0b或者0B表示二进制
  • 0表示八进制
  • 0x0X表示16进制
  • 不加默认表示10进制

可在整数输入的数值后添加L/l表示此整数以32位进制运算,如

1
int a = 7L

可在浮点数后输入f表示此浮点数以32位float进行运算(默认为64位double进行运算),以便提高运算效率,如

1
float a = 7.0f

输出中的进制表示

  • %d/%i表示有符号十进制输出
  • %x%X表示以十六进制输出
  • %o表示以八进制输出
  • %f表示输出单精度浮点数
  • %ld表示输出长整型十进制整数
  • %lo表示输出长整型八进制整数
  • %lx表示输出长整型十六进制整数
  • %lf表示输出双精度浮点数
  • %c格式对应的是单个字符
  • %s格式对应的是字符串
  • %hd表示输出short类型的十进制整数
  • %ho表示输出short类型的八进制整数
  • %hx表示输出short类型的十六进制整数
  • %lld表示输出long long int整数
  • %llx表示输出long long int十六进制整数
  • %llo表示输出long long int八进制整数
  • %llu表示输出long long unsigned整数
  • %u表示输出无符号整数
  • %e/%E表示输出以指数计数法的浮点数
  • %le/%E表示输出以指数计数法的长浮点数
  • %a/%A表示输出以十六进制指数计数法的浮点数
  • %%表示输出百分号
  • %p(在不支持%p的编译器中%u%lu 代替%p)表示指针
  • %g/%G自动选择以%f%e输出
  • %zd作为函数sizeof()的输出

注:

  • unsigned 修饰符不能用于浮点数类型。它只能用于整数数据类型

  • 默认八进制和十六进制都是整数类型数值

  • 输出时,如果要在八进制和十六进制值前显示00x前缀,要分别在转换说明中加入#(即在%后加入#)

  • %1f表示输出浮点数并保留小数点后一位

  • %1.2f表示输出浮点数且小数点前至少一字节(不足空格补足)并保留两位小数

  • %d可用于输出char类型的变量,但不能用于scanf()赋值

进制的转化(1)–负数

十进制转二进制(十进制为负数)

当将负数从十进制转换为二进制时,通常会使用二进制补码表示法。这是因为在计算机内部,负数通常以二进制补码的形式存储。下面是将负数转换为二进制的一般过程:

  1. 确定负数的绝对值:首先,确定负数的绝对值。例如,如果要将-5转换为二进制,绝对值是5。

  2. 将绝对值转换为二进制:将绝对值转换为二进制的标准方法是使用除2取余法(或称为短除法):

    • 不断将绝对值除以2,同时记录每一步的余数。
    • 将余数以逆序的方式排列,就得到了二进制表示。例如,对于绝对值5,过程如下:
      • 5 ÷ 2 = 2 余 1
      • 2 ÷ 2 = 1 余 0
      • 1 ÷ 2 = 0 余 1
    • 然后,将这些余数逆序排列,得到二进制数101。
  3. 将二进制数取反:在二进制补码中,正数的补码和原码相同,但负数的补码需要将其绝对值的二进制数取反,即0变为1,1变为0。在上面的例子中,二进制数101取反后变为010。

  4. 将取反后的结果加1:最后一步是将取反后的结果加1。在上面的例子中,010 + 1 = 011。

  5. 加上符号位:最终,加上符号位。在二进制补码中,符号位是最左边的位,0表示正数,1表示负数。在上面的例子中,要表示-5,将最左边的位设置为1,得到最终结果1101。

所以,将负数-5转换为二进制补码的结果是1101。这个过程可以用于将任何负数转换为二进制补码。请注意,不同的编程语言可能有不同的方式来表示二进制补码,但这个过程是通用的。

二进制转十进制(符号位为1)

  1. 找到符号位:首先,从二进制表示中找到符号位。在二进制补码中,符号位是最左边的位,1表示负数,0表示正数。

  2. 取反:将除符号位以外的所有位取反,即0变为1,1变为0。这是为了得到负数的绝对值的二进制表示。

  3. 加1:对取反后的结果加1,以得到负数的绝对值的二进制补码表示。

  4. 计算绝对值的十进制:将上一步得到的二进制数转换为十进制。这是标准的二进制到十进制转换。从最右边的位(最低位)开始,将每个位的值乘以2的幂,然后相加,直到处理完所有位。

  5. 加上负号:根据最初的符号位,将正数的绝对值前面添加负号。

让我们以一个例子来说明这个过程。假设我们有一个8位的二进制补码:11011010。

  1. 找到符号位:符号位是最左边的位,1表示负数。
  2. 取反:去除符号位并将其余位取反,得到00100110。
  3. 加1:对取反后的结果加1,得到00100111。
  4. 计算绝对值的十进制:将00100111转换为十进制。从右到左,第1位是1,第2位是2,第3位是4,第4位是0,第5位是0,第6位是0,第7位是0,第8位是-128(因为符号位是1)。计算总和:1 + 2 + 4 + 0 + 0 + 0 + 0 + (-128) = -121。
  5. 加上负号:最初的符号位是1,所以最终结果是-(-121),即121。

因此,二进制补码11011010对应的十进制值是121。这个过程可以用于将任何负数的二进制补码表示转换为十进制。

进制的转换(2)-正数

​ 正数的转化无需像负数-取反、加一,直接将除符号位的二进制转化为十进制即可

数值溢出

整数

程序实例

1
2
3
4
5
6
7
8
9
10
11
12
13
/*(printf()函数使用%u说明显示unsigned int类型的值)。*/
/* toobig.c-- 超出系统允许的最大int值*/
#include <stdio.h>
int main(void)
{
int i = -2147483648;
int k = 2147483647;
unsigned int j = 4294967295;
printf("%d %d %d\n", i, i - 1, i - 2);
printf("%d %d %d\n", k, k + 1, k + 2);
printf("%u %u %u\n", j, j + 1, j + 2);
return 0;
}

运行结果

1
2
3
-2147483648 2147483647 2147483646
2147483647 -2147483648 -2147483647
4294967295 0 1

分析:

可以把无符号整数j看作是汽车的里程表。当达到它能表示的最大值时,会重新从起始点开始。整数 i 也是类似的情况。它们主要的区别是,在超过最大值时,unsigned int 类型的变量 j 从 0开始;而int类型的变量i则从−2147483648(从绝对值最大)开始。

浮点值的上溢和下溢

上溢:

​ 当计算导致数字过大,超过当前类型能表达的范围时,就会发生上溢。在这种情况下会给toobig赋一个表示无穷大的特定值,而且printf()显示该值为inf或infinity(或者具有无穷含义的其他内容)。

下溢:

​ 当出现下溢的时候,计算机只好把尾数部分的位向右移,空出第 1 个二进制位,并丢弃最后一个二进制数。以十进制为例,把一个有4位有效数字的数(如,0.1234E-10)除以10,得到的结果是0.0123E-10。虽然得到了结果,但是在计算过程中却损失了原末尾有效位上的数字。

C语言把损失了类型全精度的浮点值称为低于正常的(subnormal)浮点值。。现在,C库已提供了用于检查计算是否会产生低于正常值的函数。

字符

char类型用于储存字符(如,字母或标点符号),但是从技术层面看,char是整数类型。

在C 语言中, 用单引号(' ')括起来的单个字符被称为字符常量。编译器一发现'A',就会将其转换成相应的代码值。

非打印字符

C Primer Plus 140-144

有些ASCII字符打印不出来。例如,一些代表行为的字符(如,退格、换行、终端响铃或蜂鸣)

以下有两种方法

方法一:使用ASCII码

例如,蜂鸣字符的ASCII值是7,因此可以这样写:

1
char beep = 7

方法二:使用转义符

转义序列赋给字符变量时,必须用单引号把转义序列括起来。
例如,假设有下面一行代码:

1
char nerf = '\n';
转义序列 含义 转移符号 含义
\a 警报 \v 垂直制表符
\b 退格 \\ 反斜杠
\f 换页 \' 单引号
\n 换行 \" 双引号
\r 回车 \? 问号
\t 水平制表符 \xhhh 十六进制值 (hhh 是1~3位十六进制数字,即每个h可表示 0~f中的一个数,)
\000 八进制值 (oo 必须是有效的八进制数,即每个o可表示 0~7中的一个数,可省略数字前多余的0默认也为8进制,但为了防止出问题建议补全为3位) \+任意符号 输出对应符号

对于\0oo\xhh,等同于使用ASCII的十进制对应数据效果

何时使用ASCII码?何时使用转义序列?如果要在转义序列(假设使用'\f')和ASCII码(’\014‘)之间选择,请选择前者(即’\f‘)。这样的写法不仅更好记,而且可移植性更高。’\f’在不使用ASCII码的系统中,仍然有效。

本章注意点:


变量初始化

需要每个值进行初始化,否则会造成错误

getchar()函数的使用

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* platinum.c -- your weight in platinum */
#include <stdio.h>
int main(void)
{
float weight; /* 你的体重 */
float value; /* 相等重量的白金价值 */
printf("Are you worth your weight in platinum?\n");
printf("Let's check it out.\n");
printf("Please enter your weight in pounds: ");
/* 获取用户的输入 */
scanf("%f", &weight);
/* 假设白金的价格是每盎司$1700 */
/* 14.5833用于把英镑常衡盎司转换为金衡盎司[1]*/
value = 1700.0 * weight * 14.5833;
printf("Your weight in platinum is worth $%.2f.\n", value);
printf("You are easily worth that! If platinum prices drop,\n");
printf("eat more to maintain your value.\n");
return 0;
}

程序的输出在屏幕上一闪而过,此时可使用,在程序中添加下面一行代码:

1
getchar();

但程序的输出依旧在屏幕上一闪而过,本例,需要调用两次getchar()函数:

1
2
getchar();
getchar();

分析

​ 在这种情况下,键入 156 并按下Enter键(发送一个换行符),然后scanf()读取键入的数字,第1个getchar()读取换行符,第2个getchar()让程序暂停,等待输入。

原因:

​ 在你输入自己的数值后按下回车,scanf函数读取用户输入的体重值时,回车键会被当作输入的一部分,并被存储在输入缓冲区中。scanf函数会读取用户输入的数值,但回车键仍然留在缓冲区中,并且getchar()函数会获取回车键字符。

声明变量的作用

​ 声明为变量创建和标记存储空间

数据类型应与打印类型一致

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* print2.c--更多printf()的特性 */
#include <stdio.h>
int main(void)
{
unsigned int un = 3000000000; /* int为32位和short为16
位的系统 */
short end = 200;
long big = 65537;
long long verybig = 12345678908642;
printf("un = %u and not %d\n", un, un);
printf("end = %hd and %d\n", end, end);
printf("big = %ld and not %hd\n", big, big);
printf("verybig= %lld and not %ld\n", verybig, verybig);
return 0;
}

输出结果:

1
2
3
4
un = 3000000000 and not -1294967296
end = 200 and 200
big = 65537 and not 1
verybig= 12345678908642 and not 1942899938

该例表明,使用错误的转换说明会得到意想不到的结果。

原因:

情况一:例如第一行输出

内存中的数位相同:不同数据类型中不同的位数所表示含义不一样,造成输出错误,也如:long long int 和double这两个数据类型

情况二:例如第三、四行输出

不同的修饰符可以截断成不同类型值,printf()从二进制的后往前读取位数

把 65537 以二进制格式写成一个 32 位数是00000000000000010000000000000001。使用%hd, printf()只会查看后 16 位,所以显示的值是 1。与此类似,输出的最后一行先显示了verybig的完整值,然后由于使用了%ld,printf()只显示了储存在后32位的值。

注意:

在使用 printf()函数时,切记检查每个待打印值都有对应的转换说明,还要检查转换说明的类型是否与待打印值的类型相匹配。

断行输入

​ 只要不在引号内部或一个单词中间断行,就可以被分为两行

sizeof()函数

sizeof是C语言的内置运算符,以字节为单位给出指定类型的大小。C99和C11提供%zd转换说明匹配sizeof的返回类型。一些不
支持C99和C11的编译器可用%u或%lu代替%zd。

1
2
3
printf("Type int has a size of %zd bytes.\n", sizeof(int));
printf("type int has a size of %u bytes.\n", sizeof(int));
printf("type int has a size of %lu bytes.\n", sizeof(int));

输入的数据类型与设定数据类型不一致的情况

把一个类型的数值初始化给不同类型的变量时,编译器会把值转换成与变量匹配的类型,这将导致部分数据丢失。

例如

1
2
int cost = 12.99; /* 用double类型的值初始化int类型的变量 */
float pi = 3.1415926536; /* 用double类型的值初始化float类型的变量 */

第1个声明,cost的值是12。C编译器把浮点数转换成整数时,会直接丢弃(截断)小数部分,而不进行四舍五入。第2个声明会损失一些精度,因为C只保证了float类型前6位的精度。


字符串和格式化

字符串与char数组

字符串(character string)是一个或多个字符的序列,双引号不是字符串的一部分。双引号仅告知编译器它括起来的是字符串,正如单引号用于标识单个字符一样。

C语言没有专门用于储存字符串的变量类型,字符串都被储存在char类型的数组(数组是同类型数据元素的有序序列)中。数组由连续的存储单元组成,字符串中的字符被储存在相邻的存储单元中

数组末尾位置的字符\0 。这是空字符( null character),C语言用它标记字符串的结束。空字符不是数字0,它是非打印字符。C中的字符串一定以空字符结束,这意味着数组的容量必须至少比待存储字符串中的字符数多1

常量和C预处理器

假设程序中的多处使用一个常量,有时需要改变它的值。毕竟,税率通常是浮动的。如果程序使用符号常量,则只需更改符号。常量的定义,不用在程序中查找使用常量的地方,然后逐一修改。C语言还提供了一个更好的方案——C预处理器。只需在程序顶部添加下面一行:

1
#define TAXRATE 0.015

用大写表示符号常量是 C 语言一贯的传统。这样,在程序中看到全大写的名称就立刻明白这是一个符号常量,而非变量。

C头文件limits.hfloat.h分别提供了与整数类型和浮点类型大小限制相关的详细信息。

如果在程序中包含limits.h头文件,就可编写下面的代码:

1
printf("Maximum int value on this system = %d\n",INT_MAX);

limits.h中的一些明示常量

float.h中的一些明示常量

strlen()函数

strlen()函数给出字符串中的字符长度

输出时可以使用%zd,在早期版本中换成%u%lu

printf()的转化说明修饰符

修饰符 含义
标记 表4.5描述了5种标记(-、+、空格、#和0),可以不使用标记或使用多个标记(不会对输出结果造成影响)
-表示输出时采用左对齐。
+这表示要输出的整数值为正数时,要在其前面显示加号(+),而负数仍然会显示减号(-)
空格:有符号值若为正,则在值前面显示前导空格(不显示任何符号); 若为负,则在值前面显示减号+标记覆盖一个空格
#:把结果转换为另一种形式。如果是%o 格式,则以0开始:如果是%X或格式,则以0X0X开始:对于所有的浮点格式,#保证了即使后面没有任何数字,也打印一个小数点字符。对于%g%G格式,#防止结果后面的0被删除
通常情况下,整数会用空格来填充以满足指定的宽度,但使用0可以指示用零字符来填充。(用于右对齐的情况)
示例:”%-10d
数字 最小字段宽度
如果该字段不能容纳待打印的数字或字符串,系统会使用更宽的字段
示例:”%4d
.数字 对于%e、%E 和%f 转换,表示小数点右边数字的位数
对于%q和%G转换,表示有效数字最大位数
对于%s转换,表示待打印字符的最大数量
对于整型转换,表示待打印数字的最小位数
如有必要,使用前%0来达到这个位数
只使用.表示其后跟随一个0,所以%.f%.0f相同
示例:"%5.2f"打印一个浮点数,字段宽度为5字符,其中小数点后有两位数字
h 和整型转换说明一起使用,表示short intunsigned short int类型的值示例:"%hu"、"%hx"、"%6.4hd"
hh 和整型转换说明一起使用,表示signed charunsigned char类型的值
示例:“%hhu“、”%hhx“、”%6.4hhd
j 和整型转换说明一起使用,表示intmax_t或uintmax_t类型的值。这些类型定义在stdint.h中
示例:”%jd”、”%8jx
l 和整型转换说明一起使用,表示long int或unsignedlongint类型的值
示例:”&ld”、”%8lu”
ll 和整型转换说明一起使用,表示long long int或unsigned long long int类型的值(C99)
示例:”%lld“、”%81lu
L 和浮点转换说明一起使用,表示long double类型的值
示例:”%Ld“、”%10.4Le
t 和整型转换说明一起使用,表示ptrdiff_t类型的值。ptrdiff_t是两个指针差值的类型(C99)
示例:”%td“、”%12ti
z 和整型转换说明一起使用,表示size_t类型的值。size_t是sizeof返回的类型(C99)
示例:”%zd“、”%12zd

注:printf()中*的使用

1
printf("Weight = %*.*f\n", width, precision, weight);

可以代表此处的数值由后面的变量进行决定,如此处的字符宽度由width进行决定,保留小数位数由precision进行决定

scanf()

读取字符串的规则

scanf()在读取输入时就已完成把空字符放入字符串末尾这项工作.它在遇到第1个空白(空格、制表符或换行符)时就不再读取输入。

&的使用

​ 如果用scanf()读取基本变量类型的值,在变量名前加上一个&;
​ 如果用scanf()把字符串读入字符数组中,不要使用&。

scanf()的读取规则

每次读取一个字符,跳过所有的空白字符,直至遇到第1个非空白字符才开始读取

scanf()不断地读取和保存字符,直至遇到非数字字符。

然后scanf()把非数字字符放回输入。这意味着程序在下一次读取输入时,首先读到的是上一次读取丢弃的非数字字符。

如果第1个非空白字符是A而不是数字,scanf()将停在那里,并把A放回输入中,不会把值赋给指定变量。程序在下一次读取输入时,首先读到的字符是A。如果程序只使用%d转换说明,scanf()就一直无法越过A读下一个字符。

scanf()中的使用规则

1
scanf("%*d %*d %d", &n);

使用*可以使scanf()跳过前两个函数的赋值,在上述代码中跳过两个整数,把第3个整数拷贝给n

返回值

scanf()函数返回读取数据的数量,所以如果读取对应类型则返回1,如果读取不成功则不返回0,在转换值之前出现问题,会返回一个特殊值EOF(通常被定义为-1)。

读取小数值

由于输入的小数在c语言中默认作为double进行处理,所以对应的数据类型需要使用&lf,否则会出错。

本章注意点:


"X"'X'的区别

区别之一在于’x’是基本类型(char),而”x”是派生类型(char数组)

区别之二是”x”实际上由两个字符组成:’x’和空字符\0

sizeof 何时使用了圆括号

圆括号的使用时机否取决于运算对象是类型还是特定量。运算对象是类型时,圆括号必不可少,但是对于特定量,可有可无。也就是说,对于类型,应写成sizeof(char)或sizeof(float);对于特定量,可写成sizeof name或sizeof 6.28。尽管如此,还是建议所有情况下都使用圆括号,如sizeof(6.28)

const限定符

C90标准新增了const关键字,用于限定一个变量为只读.这使得成为一个只读值,可以在计算中使用, 可以打印, 但是不能更改值。


运算符、表达式和语句

运算符

基本运算符

=、+、-、*、/

注:C 没有指数运算符。不过,C 的标准数学库提供了一个pow()函数用于指数运算。例如,pow(3.5, 2.2)返回3.5的2.2次幂。

赋值运算符

=并不意味着“相等”,而是一个赋值运算符。赋值行为从右往左进行。

1
bmw = 2002;

读作“把值2002赋给变量bmw”

求模运算符-%

用于整数运算,求模运算符给出其左侧整数除以右侧整数的余数

一元运算符与二元运算符

一元运算符:只有一个运算对象,如:-16

二元运算符:有两个运算对象,如:23-14

递增递减运算符

递增运算符:++

递增运算符执行简单的任务,将其运算对象递增1。

递减运算符:--

++的前缀形式和后缀形式的区别

1
2
3
4
int a = 1
int b, c;
b = a++
c = ++a

在最后的输出结果中会发现,b=1而c=3;这因为后缀在运算过程中在a使用完成后再进行递增,而前缀先进行递增而后进行计算。

结合优先级

只有圆括号的优先级比递增递减运算符高。因此, x*y++ 表示的是(x)*(y++) , 而不是(x+y)++。不过后者无效,因为递增和递减运算符只能影响一个变量(或者,更普遍地说,只能影响一个可修改的左值),而组合x*y本身不是可修改的左值。

关系运算符

运算符 含义
< 小于
> 大于
<= 小于或等于
>= 大于或等于
== 等于
!= 不等于

注:

可以通过关系运算符判断字符,但不能判断字符串

1
2
3
int a;
char ch;
a = (ch != 's')//根据ASCII区分大小写

通过关系运算符可以比较浮点数,但最好使用<>进行判断,因为浮点数在储存时会造成舍入误差造成原本相等的两数不相等。可使用fabs()函数-返回绝对值来进行比较浮点数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <math.h>//fabs()函数所需的头文件
#include <stdio.h>
int main(void)
{
const double ANSWER = 3.14159;
double response;
printf("What is the value of pi?\n");
scanf("%lf", &response);
while (fabs(response - ANSWER) > 0.0001)//浮点数两数相等所自定义的允许误差
{
printf("Try again!\n");
scanf("%lf", &response);
}
printf("Close enough!\n");
return 0;
}

逗号运算符

逗号运算符扩展了for循环的灵活性,以便在循环头中包含更多的表达式

逗号运算符并不局限于在for循环中使用,但是这是它最常用的地方。逗号运算符有两个其他性质。首先,它保证了被它分隔的表达式从左往右求值。(但逗号运算符无法在计算中插入使用以改变计算顺序,如:b = 3 * 5 - 10, * 2 + 15是错误的写法)

假设在写数字时不小心输入了逗号:

1
houseprice = 249,500;

这不是语法错误,C 编译器会将其解释为一个逗号表达式,即houseprice = 249 是逗号左侧的子表达式,500; 是右侧的子表达式。

逗号也可用作分隔符。

逗号运算符会依次计算每个表达式,并返回最后一个表达式的值作为整个逗号运算符表达式的值。

逻辑运算符

逻辑运算符 含义
&&
||
!

备选拼写:iso646.h头文件

C99标准新增了可代替逻辑运算符的拼写,它们被定义在ios646.h头文件中。如果在程序中包含该头文件,便可用and代替&&、or代替||、not代替!

判断范围问题

&&运算符可用于测试范围。例如,要测试score是否在90~100的范围内,可以这样写:

1
2
if (range >= 90 && range <= 100)
printf("Good show!\n");

千万不要模仿数学上的写法:

1
2
3
4
5
6
if (90 <= range <= 100) // 千万不要这样写!
printf("Good show!\n");
/*
由于<=运算符的求值顺序是从左往右,所以编译器把测试表达式解释为:
(90 <= range) <= 100
*/

子表达式90 <= range的值要么是1(为真),要么是0(为假)。这两个值都小于100,所以不管range的值是多少,整个表达式都恒为真。因此,**在范围测试中要使用&&**。

条件运算符——?:

以下述代码为例

1
max = (a > b) ? a : b;

上述语句翻译为:如果a大于b,那么将max设置为a;否则,设置为b。

&运算符-查找地址

一元&运算符给出变量的存储地址。PC地址通常用十六进制形式表示

%p是输出地址的转换说明

间接运算符-*

间接运算符* ,该运算符有时也称为解引用运算符。*用于给出储存在指针指向地址上的值。

1
2
ptr = &bah;
val = *ptr; // 找出ptr指向的值

以上代码相当于如下代码

1
val = bah;

不要把间接运算符和二元乘法运算符(*)混淆,虽然它们使用的符号相同,但语法功能不同。

运算符的优先级

优先级 运算符 结合律
1 后缀运算符:[] () · -> ++ –(类型名称){列表} 从左到右
2 一元运算符:++ – ! ~ + - * & sizeof_Alignof 从右到左
3 类型转换运算符:(类型名称) 从右到左
4 乘除法运算符:* / % 从左到右
5 加减法运算符:+ - 从左到右
6 移位运算符:<< >> 从左到右
7 关系运算符:<<= >>= 从左到右
8 相等运算符:== != 从左到右
9 位运算符 AND:& 从左到右
10 位运算符 XOR:^ 从左到右
11 位运算符 OR:| 从左到右
12 逻辑运算符 AND:&& 从左到右
13 逻辑运算符 OR:|| 从左到右
14 条件运算符:?: 从右到左
15 赋值运算符: = += -= *= /= %= &= ^= |= <<= >>= 从右到左
16 逗号运算符:, 从左到右
1
total += *start++;

一元运算*++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是*start

类型转化

在语句和表达式中,如果使用混合类型,C会采用一套规则进行自动类型转换。

基本的类型转换规则

  1. 当类型转换出现在表达式时,无论是unsigned还是signedcharshort都会被自动转换成int,如有必要会被转换成unsigned int
  2. 涉及两种类型的运算,两个值会被分别转换成两种类型的更高级别。
  3. 在赋值表达式语句中,计算的最终结果会被转换成被赋值变量的类型。这个过程可能导致类型升级或降级
  4. 当作为函数参数传递时,char和short被转换成int,float被转换成double。

类型升级通常都不会有什么问题,但是类型降级会导致较低类型可能放不下整个数字。

强制类型转换运算符

有时需要进行精确的类型转换,或者在程序中表明类型转换的意图。这种情况下要用到强制类型转换(cast),即在某个量的前面放置用圆括号括起来的类型名

1
2
mice = 1.6 + 1.7;
mice = (int)1.6 + (int)1.7;

第1行使用自动类型转换。首先,1.6和1.7相加得3.3。然后,为了匹配int 类型的变量,3.3被类型转换截断为整数3。第2行,1.6和1.7在相加之前都被转换成整数(1),所以把1+1的和赋给变量mice。

本章注意点:


许多其他语言都会回避该程序中的三重赋值,但是C完全没问题。赋值的顺序是从右往左

在C语言中,除法操作默认会执行整数除法,即如果操作数都是整数,结果将会是整数,而不是浮点数.整数除法会截断计算结果的小数部分(丢弃整个小数部分),不会四舍五入结果.

在定义函数时,需要在最上面进行函数原型声明

未进行函数的声明或未指定参数类型的情况下,会导致参数升级为不正确的数据类型,在函数调用中显式使用强制类型转换,可以修复这个问题:

1
pound ((int)f); // 把f强制类型转换为正确的类型

所以最好在开头进行函数的原型声明


循环

while循环

使用格式

1
2
while(判断条件)//此处不加;
条件成立的执行语句;

while通过括号的数值最后是否为0来进行判断一个真假,若为0则为假,若不为0则为真(但c语言中0为假,而1为真)。

while()后加;表示执行空语句

for循环

使用格式

1
for(语句a;语句b;语句c)

第1个表达式是初始化,只会在for循环开始时执行一次。第 2 个表达式是测试条件,在执行循环之前对表达式求值。如果表达式为假,循环结束。第3个表达式执行更新,在每次循环结束时执行。

注:在for循环中,可以省略一个或多个语句,但分号不能省略。

for语句的()定义的变量是局部变量,只在此for循环内的语句中有效

do….while循环

使用格式

1
2
3
do
执行语句//可为单一语句或者复合语句
while ( 判断条件 );//此处一定要加分号

do while循环在执行完循环体后才执行测试条件,所以至少执行循环体一次

本章注意点:


逻辑判断符号与赋值符号不要混淆

1
2
while(a == 1);//语句一
while(a = 1);//语句二

语句一中为判断a是否等于1

语句二为将1赋值给a,最后将1的参数给while最后造成语句二的循环不断进行


分支与跳转

if、else、else if语句

使用格式

1
2
3
4
5
6
if(判断语句)//此处不加分号
执行语句;//可为单语句或多语句
else if()
执行语句;//可为单语句或多语句
else()
执行语句;//可为单语句或多语句

else与if配对原则:else与离它最近的if匹配,除非最近的if被花括号括起来

数值的范围判断问题

continue语句

​ continue 语句让程序跳过continue 语句后在循环内的代码,让程序重新进入循环。

​ continue还可用作占位符。例如,下面的循环读取并丢弃输入的数据,直至读到行末尾:

1
2
while (getchar() != '\n')
continue;

break语句

程序执行到循环中的break语句时,会终止包含它的循环,并继续执行下一阶段。

多重选择:switchbreak

使用格式

1
2
3
4
5
6
7
8
9
10
11
switch (整数值(包括char类型)。)
{
case 值://表达式中只包含整型常量
执行语句
break;
case 值://表达式中只包含整型常量
执行语句
break;
default:
执行语句
}

注:switch只处理了第1个字符

break语句让程序离开switch语句,跳至switch语句后面的下一条语句。如果没有break语句,就会从匹配标签开始执行到switch末尾

break 语句可用于循环和switch 语句中, 但是continue只能用于循环中。

case多重标签

可以在switch语句中使用多重case标签

1
2
3
4
5
6
7
8
9
10
switch (ch)
{
case 'a':
case 'A': a_ct++;
break;
case 'e':
case 'E': e_ct++;
break;
default: break;
}

假设如果ch是字母a,switch语句会定位到标签为case 'a' :的位置。由于该标签没有关联break语句,所以程序流直接执行下一条语
句,即a_ct++;。如果 ch是字母A,程序流会直接定位到case 'A' :。本质上,两个标签都指的是相同的语句。

getchar()putchar()函数

getchar()函数不带任何参数,它从输入队列中返回下一个字符。
例如,下面的语句读取下一个字符输入,并把该字符的值赋给变量ch:

1
2
ch = getchar();
scanf("%c", &ch);//与上一句等效

putchar()函数打印它的参数。例如,下面的语句把之前赋给ch的
值作为字符打印出来:

1
2
putchar(ch);
printf("%c", ch);//于上一句等效

由于这些函数只处理字符, 所以它们比更通用的scanf()printf()函数更快、更简洁。而且,注意 getchar()putchar()不需要转换说明,因为它们只处理字符。这两个函数通常定义在 stdio.h头文件

ctype.h

​ C 有一系列专门处理字符的函数,ctype.h头文件包含了这些函数的原型。这些函数接受一个字符作为参数, 如果该字符属于某特殊的类别, 就返回一个非零值(真);否则,返回0(假)。

数组

数组(array)是按顺序储存的一系列类型相同的值。

1
float debts[20];

声明debts是一个内含20个元素的数组,每个元素都可以储存float 类型的值。数组的第1 个元素是debts[0] , 第2 个元素是debts[1],以此类推,直到debts[19]

注意,数组元素的编号从0开始,不是从1开始。

字符组与字符串的差别

字符串最末尾有由空字符\0,而字符组没有。如果char类型的数组末尾包含一个表示字符串末尾的空字符\0,则该
数组中的内容就构成了一个字符串

指定数组大小

在C99标准之前,声明数组时只能在方括号中使用整型常量表达式。所谓整型常量表达式,是由整型常量构成的表达式。

1
2
3
4
5
6
7
8
9
10
11
int n = 5;
int m = 8;
float a1[5]; // 可以
float a2[5*2 + 1]; //可以
float a3[sizeof(int) + 1]; //可以
float a4[-4]; // 不可以,数组大小必须大于0
float a5[0]; // 不可以,数组大小必须大于0
float a6[2.5]; // 不可以,数组大小必须是整数
float a7[(int)2.5]; // 可以,已被强制转换为整型常量
float a8[n]; // C99之前不允许
float a9[m]; // C99之前不允许

未指定数组大小

编译器会把数组的大小设置为足够装得下初始化的值。

初始化数组

1
int powers[8] = {1,2,4,6,8,16,32,64}; /* 从ANSI C开始支持这种初始化 */

如上所示,用以逗号分隔的值列表(用花括号括起来)来初始化数组,各值之间用逗号分隔。在逗号和值之间可以使用空格。根据上面的初始化,把 1 赋给数组的首元素(powers[0]),以此类推

除在声明时可使用{}对数组进行赋值,其他时候只能用变量[序号] = 对数组进行逐一赋值

注:使用const声明数组,可把数组设置为只读

声明数组形参

​ 因为数组名是该数组首元素的地址,作为实际参数的数组名要求形式参数是一个与之匹配的指针。只有在这种情况下,C才会把int ar[]int * ar解释成一样。也就是说,ar是指向int的指针。由于函数
原型可以省略参数名,所以下面4种原型都是等价的:

1
2
3
4
int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);

但是,在函数定义中不能省略参数名。

指定初始化器(适用于C99)

1
int arr[6] = {[5] = 212}; // 把arr[5]初始化为212

而C99规定,可以在初始化列表中使用带方括号的下标指明待初始化的元素

指定初始化的一些特性

1
int days[MONTHS] = { 31, 28, [4] = 31, 30, 31, [1] = 29 };
  • 如果指定初始化器后面有更多的值,那么后面这些值将被用于初始化指定元素后面的元素。如该例中的初始化列表中的片段:[4] =
    31,30,31,那么后面这些值将被用于初始化指定元素后面的元素。
  • 第二,如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化。。例如,程序清单10.5中,初始化列表开始
    时把days[1]初始化为28,但是days[1]又被后面的指定初始化[1] =29初始化为29。

二维数组

声明

1
数据类型 变量[主数组个数][每个主数组所需的个数]

初始化

例:

1
2
3
4
5
6
7
8
float rain[5][12] =
{
{4.3,4.3,4.3,3.0,2.0,1.2,0.2,0.2,0.4,2.4,3.5,6.6},
{8.5,8.2,1.2,1.6,2.4,0.0,5.2,0.9,0.3,0.9,1.4,7.3},
{9.1,8.5,6.7,4.3,2.1,0.8,0.2,0.2,1.1,2.3,6.1,8.4},
{7.2,9.9,8.4,3.3,1.2,0.8,0.4,0.0,0.6,1.7,4.3,6.2},
{7.6,5.6,3.8,2.8,3.8,0.2,0.0,0.0,0.0,1.3,2.6,5.2}
};

初始化时也可省略内部的花括号,只保留最外面的一对花括号。只要保证初始化的数值个数正确,初始化的效果与上面相同。但是如果初始化的数值不够,则按照先后顺序逐行初始化,直到用完所有的值。

其他多维数组

二维数组的相关内容都适用于三维数组或更多维的数组。

const修饰数组

1
const int days[MONTHS] = {31,28,31,30,31,30,31,31,30,31,30,31};

如果程序稍后尝试改变数组元素的值,编译器将生成一个编译期错误消息。

如果此时声明指向这个const数组,此时指针也必须用const修饰,且不能修改解引用指针的值,否则都将报错。

1
2
*pd = 29.89; // 不允许
pd[2] = 222.22; //不允许

但可以进行指针加法

1
pd++; /* 让pd指向rates[1] -- 没问题 */

注意:

const数据或非const数据的地址初始化为指向const的指针或为其赋值是合法的,此时不能修改解引用指针的值

变长数组(适用C99)

在创建数组时,可以使用变量指定数组的维度。

注:变长数组中的“变”不是指可以修改已创建数组的大小。一旦创建了变长数组,它的大小则保持不变。这里的“变”指的是:

声明一个带二维变长数组参数的函数

1
int sum2d(int rows, int cols, int ar[rows][cols]);

注:前两个形参(rows和cols)用作第3个形参二维数组ar的两个维度。因为ar的声明要使用rows和cols,所以在形参列表中必须在声明ar之前先声明这两个形参。

以下为错误写法

1
int sum2d(int ar[rows][cols], int rows, int cols);

变形

C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度:

1
int sum2d(int, int, int ar[*][*]);

需要注意的是

  • 在函数定义的形参列表中声明的变长数组并未实际创建数组。
  • 和传统的语法类似,变长数组名实际上是一个指针。这说明带变长数组形参的函数实际上是在原始数组中处理数组,因此可以修改传入的数组。

复合字面量(使用C99)

字面量是除符号常量外的常量。例如,5是int类型字面量, 81.3是double类型的字面量

创建

下面的复合字面量创建了一个和diva数组相同的匿名数组,也有两个int类型的值:

1
(int [2]){10, 20} // 复合字面量

其中int [2]即是复合字面量的类型名。

初始化有数组名的数组时可以省略数组大小,复合字面量也可以省略大小,编译器会自动计算数组当前的元素个数:

1
(int []){50, 20, 90} // 内含3个元素的复合字面量

使用

因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它

用法一:使用指针记录地址

1
2
int * pt1;
pt1 = (int [2]) {10, 20};

用法二:把复合字面量作为实际参数传递给带有匹配形式参数的函数

1
2
3
4
int sum(const int ar[], int n);
...
int total3;
total3 = sum((int []){4,4,4,5,5,5}, 6);

本章注意点:


数组名是数组首元素的地址。

如果flizny是一个数组,下面的语句成立:

1
flizny == &flizny[0]; // 数组名是该数组首元素的地址

多维数组的双重间接性质

假设有下面的声明:

1
int zippo[4][2]; /* 内含int数组的数组 */
  • 然后数组名zippo是该数组首元素的地址。
  • 1.因为zippo是数组首元素的地址,所以zippo的值和&zippo[0]的值相同。而zippo[0]本身是一个内含两个整数的数组,所以zippo[0]
    的值和它首元素(一个整数)的地址(即&zippo[0][0]的值)相同。
  • 简而言之,zippo[0]是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。由于这个整数和内含两个整数的数组都开始于同一个地址,所以zippozippo[0]zippo[0][0]的值相同。
  • 2.给指针或地址加1,其值会增加对应类型大小的数值。在这方面,zippozippo[0]不同,因为zippo指向的对象占用了两个int大小,而zippo[0] 指向的对象只占用一个int 大小。因此, zippo + 1zippo[0] + 1的值不同。
  • 3.**zippo*&zippo[0][0]等价,这相当于zippo[0][0]
  • 4.解引用一个指针(在指针前使用*运算符)或在数组名后使用带下标的[]运算符,得到引用对象代表的值。其中解引用主数组的结果就是次数组,即 *zippo == zippo[0]成立。
  • 总结:将数组理解成数组的数组。因占用地址为首元素的数据类型大小及其个数,zippo[0]是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。由于主数组为zippo[]的合集,故zippo + 1zippo[0] + 1的值不同。即zippo + 1 == zippo[1]成立,zippo[0]+1 == zippo[0][1]。解引用主数组形参就是得到主数组的第一个值,也就是次数组zippo[0]

数组中sizeof()的使用

在数组中sizeof()被视为整型常量,可以用于声明数组内的数目,如

1
int a[sizeof(int)];

指针

指针(pointer)是一个值为内存地址的变量(或数据对象)。

声明指针

1.声明变量的指针

因声明指针变量时必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。另外,程序必须知道储存在指定地址上的数据类型。

以下为声明指针的例子

1
2
3
int * pi; // pi是指向int类型变量的指针
char * pc; // pc是指向char类型变量的指针
float * pf, * pg; // pf、pg都是指向float类型变量的指针

类型说明符表明了指针所指向对象的类型,星号(*)表明声明的变量是一个指针。

2.声明多维数组的指针

多维数组的指针会有不同大小的地址,因此多维数组的指针在声明是必须指向一个内含对应个数和对应文件类型的数组

例如:

1
int (* pz)[2]; // pz指向一个内含两个int类型值的数组

前面有圆括号的版本,*先与pz结合,因此声明的是一个指向数组(内含两个int类型的值)的指针。

区别于:

1
int * pax[2]; // pax是一个内含两个指针元素的数组,每个元素都指向int的指针

由于[]优先级高,先与pax结合,所以pax成为一个内含两个元素的数组。然后*表示pax数组内含两个指针。最后,int表示pax数组中的指针都指向int类型的值。因此,这行代码声明了两个指向int的指针

3.声明函数指针

方式一

1
2
3
函数返回类型 (*指针名)(函数列表); // 声明函数指针

指针名 = 函数名; // 将add函数的地址赋值给函数指针(函数名实际上是函数地址)

方式二

1
函数返回类型 (*指针名)(函数列表) = &函数名; // 声明并初始化函数指针

4.声明指向指针的指针

1
指向的指针数据类型** 指针名 = &被指向的指针

例如:

1
2
int* ptr = &num;        // 指向int类型的指针
int** ptrToPtr = &ptr; // 指向int*类型的指针

8种基本用法

  1. 赋值
  2. 解引用
  3. 取址
  4. 指针与整数相加
  5. 递增指针
  6. 指针减去一个整数
  7. 递减指针
  8. 指针求差

注:

  • 关系运算符可以比较两个指针的值

  • 可以用一个指针减去另一个指针得到一个整数,或者用一个指针减去一个整数得到另一个指针。

  • 指针和指针不能直接相加

指针运算与递增递减操作

特别注意,只有指针变量可以进行递增递减操作,如数组名,字符串常量等指针常量等不能进行递增递减操作。

1
2
3
4
char a[] = "NO"
char * pi = "YES"
++pi; //正确
++a; //错误

解引用未初始化的指针

切记:创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。

指针未被初始化,其值是一个随机值,所以不知道5将储存在何处。这可能不会出什么错,也可能会擦写数据或代码,或者导致程序崩溃

函数指针

在C语言中,函数指针允许我们将函数的地址存储在指针变量中,并通过该指针变量来调用函数。其调用格式与函数相同。

1
指针名(参数列表);

使用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

int main() {
int (*operation)(int, int); // 声明函数指针

operation = add; // 将add函数的地址赋值给函数指针
printf("Addition: %d\n", operation(2, 3)); // 调用add函数

operation = subtract; // 将subtract函数的地址赋值给函数指针
printf("Subtraction: %d\n", operation(5, 3)); // 调用subtract函数

return 0;
}

指针的兼容性

指针之间的赋值比数值类型之间的赋值要严格。两个类型的指针不能相互赋值。

本章注意点:


指针加法

在C中,指针加1指的是增加一个存储单元,指针加1的操作并不是简单地将指针的值增加1,而是将指针向后移动一个存储单元的大小。。对数组而言,这意味着把加1后的地址是下一个元素的地址,而不是下一个字节的地址。

函数

函数的声明、定义与引用

函数的声明

1
函数定义的数据返回类型 函数名(参数列表);

函数的定义

1
2
3
4
5
6
数据返回类型 函数名(参数列表)
{
函数体

return 返回变量;
}

函数引用

1
函数名(参数列表)

函数参数的传递

1.变量

编写一个处理基本类型(如,int)的函数时,要选择是传递int类型的值还是传递指向int的指针。通常都是直接传递数值,只有程序需要在函数中改变该数值时,才会传递指针

这意味着在C语言中,函数参数传递是按值传递的,这意味着在函数调用时,实参的值会被复制到形参中。实参不会因为函数中的语句而被修改,但可利用指针,修改实参的内存地址所对应的值来进行修改实参。

例:交换两个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>

void interchange(int* u, int* v);

int main(void) {
int x = 5, y = 10;
printf("Originally x = %d and y = %d.\n", x, y);
interchange(&x, &y);
printf("Now x = %d and y = %d.\n", x, y);
return 0;
}

void interchange(int* u, int* v) {
int temp;
temp = *u;
/*
u的值是&x,所以u指向x。这意味着用*u即可表示x的值,这正是我们需要的。不要写成这样:
temp = u;
*/
*u = *v;
/*
这条语句相当于:
x = y;
*/
*v = temp;
}

2.数组

​ 对于数组别无选择,必须传递指针,因为这样做效率高。如果一个函数按值传递数组,则必须分配足够的空间来储存原数组的副本,然后把原数组所有的数据拷贝至新的数组中。如果把数组的地址传递给函数,让函数直接处理原数组则效率要高。

字符串

定义字符串

​ 用双引号括起来的内容称为字符串字面量(string literal),也叫作字符串常量(string constant)。双引号中的字符和编译器自动加入末尾的\0字符, 都作为字符串储存在内存中

1
2
3
const char m1[40] = "Limit yourself to one line's worth."
//等价于
const char m1[40] = { 'L','i', 'm', 'i', 't', ' ', 'y', 'o', 'u', 'r','s', 'e', 'l', 'f', ' ', 't', 'o', ' ', 'o', 'n', 'e', ' ','l', 'i', 'n', 'e','\", 's', ' ', 'w', 'o', 'r','t', 'h', '.', '\0' };

字符串常量

例如:

1
#define NAME "HeyJWEI"

char类型数组

例如:

1
char name[] = "HEYJW";

指向char的指针

例如:

1
const char * pi = "HeyJWEI";

:如果要在字符串内部使用双引号,必须在双引号前面加上一个反斜杠(\)

字符串串联规则:从ANSI C标准起,如果字符串字面量之间没有间隔,或者用空白字符分隔(包括换行),C会将其视为串联起来的字符串字面量。

数组形式和指针形式的区别

数组

​ 数组形式在计算机的内存中分配为一个内含 字母数+1 个元素的数组(每个元素对应一个字符,还加上一个末尾的空字符’\0’),每个元素被初始化为字符串字面量对应的字符。通常,字符串都作为可执行文件的一部分储存在数据段中。当把程序载入内存时,也载入了程序中的字符串。字符串储存在静态存储区(static memory)中。但是,程序在开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中。注意,此时字符串有两个副本。一个是在静态内存中的字符串字面量,另一个是储存在数组中的字符串。

即以下流程

  1. 字符串的内存分配

  2. 字符串的初始化

  3. 字符串的存储位置

  4. 字符串的拷贝

    注:此时编译器便把数组名识别为该数组首元素地址&数组名[0])的别名。这里关键要理解,在数组形式中,数组名是地址常
    量。不能更改ar1,如果改变了ar1,则意味着改变了数组的存储位置(即地址)。可以进行类似 数组名+1这样的操作,标识数组的下一个元素。但是不允许进行++数组名这样的操作。

指针

指针形式(*pt1)也使得编译器为字符串在静态存储区预留元素的空间。另外,一旦开始执行程序,它会为指针变量pt1留出一个储存位置,并把字符串的地址储存在指针变量中。

注:

​ 该变量最初指向该字符串的首字符,但是它的值可以改变。因此,可以使用递增运算符。例如,++pt1将指向第 2 个字符

​ 用双引号括起来的内容被视为指向该字符串储存位置的指针(类似于字符数组的指针,只指向第一个字母)。这类似于把数组名作为指向该数组位置的指针。

注:

字符串字面量被视为const数据。由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针。这意味着不能用pt1改变它所指向的数据,但是仍然可以改变pt1的值(即,pt1指向的位置)。如果把一个字符串字面量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const。

puts()函数

printf()函数一样,puts()函数也属于stdio.h系列的输入/输出函数。但是,与printf()不同的是,puts()函数只显示字符串,而且自动在显示的字符串末尾加上换行符

一些神奇的语句

学习过程中碰到了一些我觉得很神奇的语句

1.逻辑运算符在赋值中的直接使用

1
a = (10==2);//此时a = 0

2.在while()中嵌入scanf()函数

1
while(scanf("%d",&a));//此时程序先运行scanf()函数后将函数的返回值输入给while,若为整数则返回1,若为非整数则返回0

3.在while()中的括号中填入数字

1
while(1);//若括号中的数值不为0,即可执行while循环

4.可利用scanf()的返回值对变量进行赋值

1
a = scanf("%d",&c)

5.获取字符同时进行判断

1
while ((ch = getchar()) != '\n')

等同于

1
2
3
while (
(ch = getchar()) // 给ch赋一个值
!= '\n') // 把ch和\n作比较

但区别于

1
while (ch = getchar() != '\n')

​ !=运算符的优先级比=高,所以先对表达式getchar() != ‘\n’求值。由于这是关系表达式,所以其值不是1就是0(真或假)。然后,把该值赋给ch。

6.由于C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。可写出一些简洁的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/* sum_arr2.c -- 数组元素之和 */
#include <stdio.h>
#define SIZE 10
int sump(int * start, int * end);
int main(void)
{
int marbles[SIZE] = { 20, 10, 5, 39, 4,
16, 19, 26, 31, 20 }; long answer;
answer = sump(marbles, marbles + SIZE);
printf("The total number of marbles is
%ld.\n", answer);
return 0;
}
/* 使用指针算法 */
int sump(int * start, int * end)
{
int total = 0;
while (start < end)
{
total += *start; // 把数组元素的值加起来
start++; // 让指针指向下一个元素
} return total;
}

注意,使用这种“越界”指针(超出数组有含义的内存地址)的函数调用更为简洁

7.多维数组的解引用与加法操作

1
2
(*(*(zippo+2) + 1))
//代替数组表示法(zippo[2][1])

Bug日记

1.for循环局部变量错误

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#define MONTHS 12 // number of months in a year
#define YEARS 5 // number of years of data

int main(void) {
// initializing rainfall data for 2010 - 2014
float rain[YEARS][MONTHS] = {
{4.3, 4.3, 4.3, 3.0, 2.0, 1.2, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6},
{8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.3},
{9.1, 8.5, 6.7, 4.3, 2.1, 0.8, 0.2, 0.2, 1.1, 2.3, 6.1, 8.4},
{7.2, 9.9, 8.4, 3.3, 1.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 6.2},
{7.6, 5.6, 3.8, 2.8, 3.8, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 5.2}
};
int year, month;
float subtot = 1, total;

printf(" YEAR RAINFALL (inches)\n");
for (year = 0, total = 0; year < YEARS; year++) {
// for each year, sum rainfall for each month
subtot = 0;
for (float *zhizheng = rain[year], subtot = 0; zhizheng < &rain[year][MONTHS] ; zhizheng++)//错误地方
subtot += *zhizheng;
printf("%5d %15.1f\n", 2010 + year, subtot);
total += subtot; // total for all years
}

return 0;
}

在错误的for循环处,subtot被重新定义,变成for循环的局部变量