前言

为了期末考试写的,纯自查。不会像百科全书一样事无巨细地罗列清楚,基本用法还是要写一些的。

Part 1 输入输出流C 标准库 - <stdio.h>

1.输入输出流

1.1 printf函数

传参的内容:

printf("<格式化字符串>", <参量表>);

其中格式化字符串和参量表是什么捏?

格式化字符串传递打印的内容与用“%”来标识后面参量表中变量的数据类型

举个例子:

int d = 114514;
printf("qwq%d",d);

format 标签属性是 %[flags][width][.precision][length]specifier

其中占位符已在下一章介绍

flags(标识)描述
-在给定的字段宽度内左对齐,默认是右对齐(参见 width 子说明符)。
+强制在结果之前显示加号或减号(+ 或 -),即正数前面会显示 + 号。默认情况下,只有负数前面会显示一个 - 号。
空格如果没有写入任何符号,则在该值前面插入一个空格。
#与 o、x 或 X 说明符一起使用时,非零值前面会分别显示 0、0x 或 0X。
与 e、E 和 f 一起使用时,会强制输出包含一个小数点,即使后边没有数字时也会显示小数点。默认情况下,如果后边没有数字时候,不会显示显示小数点。
与 g 或 G 一起使用时,结果与使用 e 或 E 时相同,但是尾部的零不会被移除。
0在指定填充 padding 的数字左边放置零(0),而不是空格(参见 width 子说明符)。

1.2 占位符

printf()可以在输出文本中指定占位符。所谓“占位符”,就是这个位置可以用其他值代入。

  • %a:十六进制浮点数,字母输出为小写。
  • %A:十六进制浮点数,字母输出为大写。
  • %c:字符。
  • %d:十进制整数。
  • %e:使用科学计数法的浮点数,指数部分的e为小写。
  • %E:使用科学计数法的浮点数,指数部分的E为大写。
  • %i:整数,基本等同于%d
  • %f:小数(包含float类型和double类型)。
  • %g:6个有效数字的浮点数。整数部分一旦超过6位,就会自动转为科学计数法,指数部分的e为小写。
  • %G:等同于%g,唯一的区别是指数部分的E为大写。
  • %hd:十进制 short int 类型。
  • %ho:八进制 short int 类型。
  • %hx:十六进制 short int 类型。
  • %hu:unsigned short int 类型。
  • %ld:十进制 long int 类型。
  • %lo:八进制 long int 类型。
  • %lx:十六进制 long int 类型。
  • %lu:unsigned long int 类型。
  • %lld:十进制 long long int 类型。
  • %llo:八进制 long long int 类型。
  • %llx:十六进制 long long int 类型。
  • %llu:unsigned long long int 类型。
  • %Le:科学计数法表示的 long double 类型浮点数。
  • %Lf:long double 类型浮点数。
  • %n:已输出的字符串数量。该占位符本身不输出,只将值存储在指定变量之中。
  • %o:八进制整数。
  • %p:指针。
  • %s:字符串。
  • %u:无符号整数(unsigned int)。
  • %x:十六进制整数。
  • %zdsize_t类型。
  • %%:输出一个百分号。
width(宽度)描述
(number)要输出的字符的最小数目。如果输出的值短于该数,结果会用空格填充。如果输出的值长于该数,结果不会被截断。
*宽度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。

.precision(精度)描述
.number对于整数说明符(d、i、o、u、x、X):precision 指定了要写入的数字的最小位数。如果写入的值短于该数,结果会用前导零来填充。如果写入的值长于该数,结果不会被截断。精度为 0 意味着不写入任何字符。
对于 e、E 和 f 说明符:要在小数点后输出的小数位数。
对于 g 和 G 说明符:要输出的最大有效位数。
对于 s: 要输出的最大字符数。默认情况下,所有字符都会被输出,直到遇到末尾的空字符。
对于 c 类型:没有任何影响。
当未指定任何精度时,默认为 1。如果指定时不带有一个显式值,则假定为 0。
.*精度在 format 字符串中未指定,但是会作为附加整数值参数放置于要被格式化的参数之前。
length(长度)描述
h参数被解释为短整型或无符号短整型(仅适用于整数说明符:i、d、o、u、x 和 X)。
l参数被解释为长整型或无符号长整型,适用于整数说明符(i、d、o、u、x 和 X)及说明符 c(表示一个宽字符)和 s(表示宽字符字符串)。
L参数被解释为长双精度型(仅适用于浮点数说明符:e、E、f、g 和 G)。

比如需要输出精度为小数点后两位的double类型数据:

1.3输出格式

printf()可以定制占位符的输出格式。

(1)限定宽度

printf()允许限定占位符的最小宽度。

printf("%5d\n", 123); // 输出为 "  123"

上面示例中,%5d表示这个占位符的宽度至少为5位。如果不满5位,对应的值的前面会添加空格。

输出的值默认是右对齐,即输出内容前面会有空格;如果希望改成左对齐,在输出内容后面添加空格,可以在占位符的%的后面插入一个-号。

printf("%-5d\n", 123); // 输出为 "123  "

上面示例中,输出内容123的后面添加了空格。

对于小数,这个限定符会限制所有数字的最小显示宽度。

// 输出 "  123.450000"printf("%12f\n", 123.45);

上面示例中,%12f表示输出的浮点数最少要占据12位。由于小数的默认显示精度是小数点后6位,所以123.45输出结果的头部会添加2个空格。

(2)总是显示正负号

默认情况下,printf()不对正数显示+号,只对负数显示-号。如果想让正数也输出+号,可以在占位符的%后面加一个+

printf("%+d\n", 12); // 输出 +12printf("%+d\n", -12); // 输出 -12

上面示例中,%+d可以确保输出的数值,总是带有正负号。

(3)限定小数位数

输出小数时,有时希望限定小数的位数。举例来说,希望小数点后面只保留两位,占位符可以写成%.2f

// 输出 Number is 0.50printf("Number is %.2f\n", 0.5);

上面示例中,如果希望小数点后面输出3位(0.500),占位符就要写成%.3f

这种写法可以与限定宽度占位符,结合使用。

// 输出为 "  0.50"printf("%6.2f\n", 0.5);

上面示例中,%6.2f表示输出字符串最小宽度为6,小数位数为2。所以,输出字符串的头部有两个空格。

最小宽度和小数位数这两个限定值,都可以用*代替,通过printf()的参数传入。

printf("%*.*f\n", 6, 2, 0.5);// 等同于printf("%6.2f\n", 0.5);

上面示例中,%*.*f的两个星号通过printf()的两个参数62传入。

(4)输出部分字符串

%s占位符用来输出字符串,默认是全部输出。如果只想输出开头的部分,可以用%.[m]s指定输出的长度,其中[m]代表一个数字,表示所要输出的长度。

// 输出 helloprintf("%.5s\n", "hello world");

上面示例中,占位符%.5s表示只输出字符串“hello world”的前5个字符,即“hello”。

1.4 scanf()函数

scanf()函数用于读取用户的键盘输入。程序运行到这个语句时,会停下来,等待用户从键盘输入。用户输入数据、按下回车键后,scanf()就会处理用户的输入,将其存入变量。

scanf()函数与printf()函数比较相似,都是由第一段的格式化字符串中包含的占位符来向后面的变量传递值。比如:

scanf("%d", &i);

变量前面必须加上&符号,传递变量的地址而不是变量,当然如果是字符串变量其本身就是指针变量就不用再加上&了。

1.5 占位符

scanf()常用的占位符如下,与printf()的占位符基本一致。

  • %c:字符。
  • %d:整数。
  • %ffloat类型浮点数。
  • %lfdouble类型浮点数。
  • %Lflong double类型浮点数。
  • %s:字符串。
  • %[]:在方括号中指定一组匹配的字符(比如%[0-9]),遇到不在集合之中的字符,匹配将会停止。

上面所有占位符之中,除了%c以外,都会自动忽略起首的空白字符。%c不忽略空白字符,总是返回当前第一个字符,无论该字符是否为空格。如果要强制跳过字符前的空白字符,可以写成scanf(" %c", &ch),即%c前加上一个空格,表示跳过零个或多个空白字符。

下面要特别说一下占位符%s,它其实不能简单地等同于字符串。它的规则是,从当前第一个非空白字符开始读起,直到遇到空白字符(即空格、换行符、制表符等)为止。因为%s不会包含空白字符,所以无法用来读取多个单词,除非多个%s一起使用。这也意味着,scanf()不适合读取可能包含空格的字符串,比如书名或歌曲名。另外,scanf()遇到%s占位符,会在字符串变量末尾存储一个空字符\0

scanf()将字符串读入字符数组时,不会检测字符串是否超过了数组长度。所以,储存字符串时,很可能会超过数组的边界,导致预想不到的结果。为了防止这种情况,使用%s占位符时,应该指定读入字符串的最长长度,即写成%[m]s,其中的[m]是一个整数,表示读取字符串的最大长度,后面的字符将被丢弃。

2 运算符

2.1 算术运算符

包含以下几种,用于实现基本运算

  • +:正值运算符(一元运算符)
  • -:负值运算符(一元运算符)
  • +:加法运算符(二元运算符)
  • -:减法运算符(二元运算符)
  • *:乘法运算符
  • /:除法运算符
  • %:余值运算符

2.1.1 +/-运算符

非常简单,可以用于把两个数字加起来或者相减

减法运算符也可以坐负数标识

int a = 12 + 12;
int b = 12 - 22;
int c = -21;

2.1.2 *运算符

用来把两个数乘起来

int a = 12 * 12;

2.1.3 / 运算符

用于两个数字的相乘,请注意,如果相除的两个数字都是整形,那么返回的值只会是整数,

而只要有一个浮点数参与运算,将返回浮点值:

float a = 6/4;
printf("%f\n",a) 
//输出1.000000
float b = 6/4.0;
printf("%f\n",b) 
//输出1.500000

2.1.4 %运算符

取模运算符,用来算两个数相除的余数,注意,只接受整数取模,取模的正负性油第一个运算数的符号决定:

11 % -5 // 1
-11 % -5 // -1
-11 % 5 // -1

2.1.5 简写

以下是一些可以使用的简写方式,请注意,请不要在考试之外的地方使用,这个让代码可读性不太行,如果是协作的话会给别人带来许多不必要的困惑。

  • +=
  • -=
  • *=
  • /=
  • %=
i += 3;  // 等同于 i = i + 3
i -= 8;  // 等同于 i = i - 8
i *= 9;  // 等同于 i = i * 9
i /= 2;  // 等同于 i = i / 2
i %= 5;  // 等同于 i = i % 5

2.2 自增自减运算符

C允许我们在变量自身进行操作:++/--

i++; //等于 i = i + 1
i--; //等于 i = i - 1

请注意,在变量前或后面放置自增自减运算符效果并不一样,++i/--i 代表先对变量i进行自增/自减操作,再进行运算,而i++/i--代表再进行其他运算之后再改变i的值:

int i = 42;
int j;

j = (i++ + 10);
// i: 43
// j: 52

j = (++i + 10)
// i: 44
// j: 54

同样,并不建议在实际使用中使用这样的令人迷惑运算符,代码可读性是很重要的。

2.3 关系运算符

我们可以对两个变量之间放置关系运算符,来返回一个布尔值(True or Flase)

包括:

  • > 大于运算符
  • < 小于运算符
  • >= 大于等于运算符
  • <= 小于等于运算符
  • == 相等运算符
  • != 不相等运算符

比如对a,b进行比较:

a == b;
a != b;
a < b;
a > b;
a <= b;
a >= b;

布尔值返回的真或假在C中0代表假,而所有非0的值都代表真,比如1,2,3.....

我们通常在if语句或者while语句中使用关系运算符

2.3.1 一些猪头写法

通常没有任何教程会让你连续使用多个关系运算符,但是()()考试好像挺喜欢考这些,还是得说一说

比如以下例子:

int a = 1, b = 2, c = 3;
a < b < c;

哎这个就很蠢,它不是在问是不是a小于b然后b也小于c,因为c语言会一个一个地读取,这个表达式其实是这个样子:

(a < b ) < c;

a < b 返回的值是true: 1,然后用这个1去和c进行比较,这样并没有什么意义。

如果你需要实现a< b < c的操作那么请阅读下一节

2.4 逻辑运算符

如果你用过python 你就会知道python对这个处理得有多好:

# this is a python code
if john not in pesons or john not in group_b:
    pass

扯多了,我们来看c语言

  • !:否运算符(改变单个表达式的真伪)。
  • &&:与运算符(两侧的表达式都为真,则为真,否则为伪)。
  • ||:或运算符(两侧至少有一个表达式为真,则为真,否则为伪)。

举个例子:

if (1 != 2-1):
  printf("这里的!=就代表了在这里需要两侧不相同才可以返回真值")
\\同样的,你也可以这样写
if (!(1 == 2 - 1)):
  printf("这样把!写在外面也是允许的")
\\ || 与&&运算符基本相同如下
if (1 ! = 2 - 1 && 2 == 3 + 1)
  printf("就是这样的")

在这里面还有个小问题,就是&&运算符当第一个表达式结果为真之后就不会再去执行第二个表达式,可能会对变量的值产生一些意想不到的影响(当然我及其不推荐在逻辑运算符里面加一些奇怪的运算

2.5其他运算符号(考试不会考,也基本用不上,看看就行)

C 语言提供一些位运算符,用来操作二进制位(bit)。

(1)取反运算符

取反运算符是一个一元运算符,用来将每一个二进制位变成相反值,即0变成11变成0

// 返回 01101100
~ 10010011

上面示例中,~对每个二进制位取反,就得到了一个新的值。

注意,~运算符不会改变变量的值,只是返回一个新的值。

(2)与运算符&

与运算符&将两个值的每一个二进制位进行比较,返回一个新的值。当两个二进制位都为1,就返回1,否则返回0

// 返回 00010001
10010011 & 00111101

上面示例中,两个八位二进制数进行逐位比较,返回一个新的值。

与运算符&可以与赋值运算符=结合,简写成&=

int val = 3;
val = val & 0377;

// 简写成
val &= 0377;

(3)或运算符|

或运算符|将两个值的每一个二进制位进行比较,返回一个新的值。两个二进制位只要有一个为1(包含两个都为1的情况),就返回1,否则返回0

// 返回 10111111
10010011 | 00111101

或运算符|可以与赋值运算符=结合,简写成|=

int val = 3;
val = val | 0377;

// 简写为
val |= 0377;

(4)异或运算符^

异或运算符^将两个值的每一个二进制位进行比较,返回一个新的值。两个二进制位有且仅有一个为1,就返回1,否则返回0

// 返回 10101110
10010011 ^ 00111101

异或运算符^可以与赋值运算符=结合,简写成^=

int val = 3;
val = val ^ 0377;

// 简写为
val ^= 0377;

(5)左移运算符<<

左移运算符<<将左侧运算数的每一位,向左移动指定的位数,尾部空出来的位置使用0填充。

// 1000101000
10001010 << 2

上面示例中,10001010的每一个二进制位,都向左侧移动了两位。

左移运算符相当于将运算数乘以2的指定次方,比如左移2位相当于乘以4(2的2次方)。

左移运算符<<可以与赋值运算符=结合,简写成<<=

int val = 1;
val = val << 2;

// 简写为
val <<= 2;

(6)右移运算符>>

右移运算符>>将左侧运算数的每一位,向右移动指定的位数,尾部无法容纳的值将丢弃,头部空出来的位置使用0填充。

// 返回 00100010
10001010 >> 2

上面示例中,10001010的每一个二进制位,都向右移动两位。最低的两位10被丢弃,头部多出来的两位补0,所以最后得到00100010

注意,右移运算符最好只用于无符号整数,不要用于负数。因为不同系统对于右移后如何处理负数的符号位,有不同的做法,可能会得到不一样的结果。

右移运算符相当于将运算数除以2的指定次方,比如右移2位就相当于除以4(2的2次方)。

右移运算符>>可以与赋值运算符=结合,简写成>>=

int val = 1;
val = val >> 2;

// 简写为
val >>= 2;

2.6 运算优先级

  • 圆括号(()
  • 自增运算符(++),自减运算符(--
  • 一元运算符(+-
  • 乘法(*),除法(/
  • 加法(+),减法(-
  • 关系运算符(<>等)
  • 赋值运算符(=

以上优先级递减

完全记住所有运算符的优先级没有必要,解决方法是多用圆括号,防止出现意料之外的情况,也有利于提高代码的可读性

3 流程控制

3.1 if-else if-else语句:

用来进行条件的判断

if (expression) statement

当expression中的值为真(不为0),执行后面的语句

当然你也可以写在下一行:

if (x == 10)
  printf("x is 10\n");
  \\顺便一提这个缩进不是必要的,但是为了代码美观和可读,请缩进

如果if的代码块很长,我们可以使用大括号给框起来

if (coffee_cup != 0) {
  coffee.add();
  coffee.drink();
}

让我们加上else语句,else是当if并没有满足后所执行的代码

if (coffee_cup != 0) {
  coffee.add();
  coffee.drink();
}
else {
coffee.drink();
}

最后还可以加上else if 语句来实现多种条件的判断:

if (coffee_cup == 0) {
  coffee.add();
  coffee.drink();
}
else if(coffee_cup.temp>=100){
coffee.wait();
}

else {
coffee.drink();
}

强烈建议使用大括号,可读性很好

3.2三元运算符?:

可以认为是if else的简写版本

<expression1> ? <expression2> : <expression3>

当expression1的值为真,执行expression2,否则执行expression3

coffee_cup ? coffee.drink() : coffee.add();

3.3switch语句

在试图对很多当个变量的多个状态进行判断时,我们可以选择switch来实现

switch (expression) {
  case value1: statement
  case value2: statement
  default: statement
}

expression为需要判断的表达式,case 后面是需要和expression比对的值,expression和value对应上了就执行后面相应的值。

switch (grade) {
  case 0:
    printf("False");
    break;
  case 1:
    printf("True");
    break;
  default:
    printf("Illegal");
}

请注意,break语句是需要的,不然在执行完case之后不会退出swtich语句,而是继续判断后面的case语句,通过这个方式可以让多个case的值同时执行同一段语句

switch (grade) {
  case 0:
  case 1:
    printf("True");
    break;
  default:
    printf("Illegal");
}

这样就可以实现。

3.4 while 语句

while 用于循环,简而言之是当条件满足的时候一直执行代码

while (expression)
  statement
while (expression) {
  statement;
  statement;
}
i = 0;

while (i < 10) {
  printf("i is now %d!\n", i);
  i++;
}

printf("All done!\n");

用法也和上面的语句很像,像啊,很像啊。

当然你也可以写点死循环:

while (1) {
  // ...
}

3.5 do while 语句

这个和while语句很像,是在第一次执行while语句之前先执行一次do 代买块里面的内容。

do statement
while (expression);
i = 10;

do {
  printf("i is %d\n", i);
  i++;
} while (i < 10);

printf("All done!\n");

3.6 for 语句

for 也是循环语句,可以精确控制循环次数

for (initialization; continuation; action)
  statement;

上面代码中,for语句的条件部分(即圆括号里面的部分)有三个表达式。

  • initialization:初始化表达式,用于初始化循环变量,只执行一次。
  • continuation:判断表达式,只要为true,就会不断执行循环体。
  • action:循环变量处理表达式,每轮循环结束后执行,使得循环变量发生变化。

循环体部分的statement可以是一条语句,也可以是放在大括号里面的复合语句。下面是一个例子。

for (int i = 10; i > 0; i--)
  printf("i is %d\n", i);

上面示例中,循环变量ifor的第一个表达式里面声明,该变量只用于本次循环。离开循环体之后,就会失效。

条件部分的三个表达式,每一个都可以有多个语句,语句与语句之间使用逗号分隔。

int i, j;
for (i = 0, j = 999; i < 10; i++, j--) {
  printf("%d, %d\n", i, j);
}

上面示例中,初始化部分有两个语句,分别对变量ij进行赋值。

for的三个表达式都不是必需的,甚至可以全部省略,这会形成无限循环。

for (;;) {
  printf("本行会无限循环地打印。\n" );
}

上面示例由于没有判断条件,就会形成无限循环。

3.7 break 语句

break语句有两种用法。一种是与switch语句配套使用,用来中断某个分支的执行,这种用法前面已经介绍过了。另一种用法是在循环体内部跳出循环,不再进行后面的循环了。

for (int i = 0; i < 3; i++) {
  for (int j = 0; j < 3; j++) {
    printf("%d, %d\n", i, j);
    break;
  }
}

上面示例中,break语句使得循环跳到下一个i

while ((ch = getchar()) != EOF) {
  if (ch == '\n') break;
  putchar(ch);
}

上面示例中,一旦读到换行符(\n),break命令就跳出整个while循环,不再继续读取了。

注意,break命令只能跳出循环体和switch结构,不能跳出if结构。

if (n > 1) {
  if (n > 2) break; // 无效
  printf("hello\n");
}

上面示例中,break语句是无效的,因为它不能跳出外层的if结构。

3.8 continue 语句

continue语句用于在循环体内部终止本轮循环,进入下一轮循环。只要遇到continue语句,循环体内部后面的语句就不执行了,回到循环体的头部,开始执行下一轮循环。

for (int i = 0; i < 3; i++) {
  for (int j = 0; j < 3; j++) {
    printf("%d, %d\n", i, j);
    continue;
  }
}

上面示例中,有没有continue语句,效果一样,都表示跳到下一个j

while ((ch = getchar()) != '\n') {
  if (ch == '\t') continue;
  putchar(ch);
}

上面示例中,只要读到的字符是制表符(\t),就用continue语句跳过该字符,读取下一个字符。

3.9 goto 语句

goto 语句用于跳到指定的标签名。这会破坏结构化编程,建议不要轻易使用,这里为了语法的完整,介绍一下它的用法。

char ch;

top: ch = getchar();

if (ch == 'q')
  goto top;

上面示例中,top是一个标签名,可以放在正常语句的前面,相当于为这行语句做了一个标记。程序执行到goto语句,就会跳转到它指定的标签名。

infinite_loop:
  print("Hello, world!\n");
  goto infinite_loop;

上面的代码会产生无限循环。

goto 的一个主要用法是跳出多层循环。

for(...) {
  for (...) {
    while (...) {
      do {
        if (some_error_condition)
          goto bail;    
      } while(...);
    }
  }
}
    
bail:
// ... ...

上面代码有很复杂的嵌套循环,不使用 goto 的话,想要完全跳出所有循环,写起来很麻烦。

goto 的另一个用途是提早结束多重判断。

if (do_something() == ERR)
  goto error;
if (do_something2() == ERR)
  goto error;
if (do_something3() == ERR)
  goto error;
if (do_something4() == ERR)
  goto error;

上面示例有四个判断,只要有一个发现错误,就使用 goto 跳过后面的判断。

注意,goto 只能在同一个函数之中跳转,并不能跳转到其他函数。

4 数据类型

C语言有充分的数据类型,在声明变量时必须指明数据类型。

4.1 字符类型char

我们要知道程序在运行的时候是没有地方会放一个“横,撇,点,横折........”的,而是用一个表格来把一些数字和字符相互对应,在C语言中我们使用ASCII码表来对应:

首先我们可以声明单个字符(注意,字符常量使用单引号

char c = 'B';

对于电脑而言,char和int一样,使用了一个8位存储来存整数的值(算一算2 ^ 8 = 256就是char的大小范围)

当然,也可以直接声明对应的ASCII表值:

char c = 66;
// 等同于
char c = 'B';

因为是数字,你甚至可以把两个字符常量加在一起:

char a = 'B'; // 等同于 char a = 66;
char b = 'C'; // 等同于 char b = 67;

printf("%d\n", a + b); // 输出 133

单引号本身也是一个字符,如果要表示这个字符常量,必须使用反斜杠转义。

char t = '\'';

转义还包括一下字符

  • \a:警报,这会使得终端发出警报声或出现闪烁,或者两者同时发生。
  • \b:退格键,光标回退一个字符,但不删除字符。
  • \f:换页符,光标移到下一页。在现代系统上,这已经反映不出来了,行为改成类似于\v
  • \n:换行符。
  • \r:回车符,光标移到同一行的开头。
  • \t:制表符,光标移到下一个水平制表位,通常是下一个8的倍数。
  • \v:垂直分隔符,光标移到下一个垂直制表位,通常是下一行的同一列。
  • \0:null 字符,代表没有内容。注意,这个值不等于数字0。

转义写法还能使用八进制和十六进制表示一个字符。

  • \nn:字符的八进制写法,nn为八进制值。
  • \xnn:字符的十六进制写法,nn为十六进制值。

4.2 整形 int

4.2.1 简介

我们用int来声明一个整数变量。

注意,不同计算机的int变量值会有些许(大嘘)不同,通常还是16位的。

int a = 1;

4.2.2 有没有符号? signed,unsigned 

对于一个int变量,默认是有符号(正负性)的,但是你也可以自己手动指定

signed 代表有符号,unsigned 没有符号(学了英语不应该看不懂罢

对于int类型,默认是带有正负号的,也就是说int等同于signed int。由于这是默认情况,关键字signed一般都省略不写,但是写了也不算错。

signed int a;
// 等同于
int a;
unsigned int a;

当然也可以指定其为非负整数,还附带可以表示的数翻倍了。比如原来16位可以表示(-32767,32767),如果非负就可以表示(0,65535)

同样的,int 也可以省略:

unsigned a;

4.2.3 整数的子类

比如我们要表示超过65535的数字时,亦或是觉得我就表示几个状态要不了这么大空间怎么办呢

  • short int(简写为short):占用空间不多于int,一般占用2个字节(整数范围为-32768~32767)。
  • long int(简写为long):占用空间不少于int,至少为4个字节。
  • long long int(简写为long long):占用空间多于long,至少为8个字节。
short int a;
long int b;
long long int c;

在 默认情况下它们都是带有符号的数,你也可以加上unsigned来让它们的范围翻倍。

4.2.4 整数的极值(不会考

有时候需要查看,当前系统不同整数类型的最大值和最小值,C 语言的头文件limits.h提供了相应的常量,比如SCHAR_MIN代表 signed char 类型的最小值-128SCHAR_MAX代表 signed char 类型的最大值127

为了代码的可移植性,需要知道某种整数类型的极限值时,应该尽量使用这些常量。

  • SCHAR_MINSCHAR_MAX:signed char 的最小值和最大值。
  • SHRT_MINSHRT_MAX:short 的最小值和最大值。
  • INT_MININT_MAX:int 的最小值和最大值。
  • LONG_MINLONG_MAX:long 的最小值和最大值。
  • LLONG_MINLLONG_MAX:long long 的最小值和最大值。
  • UCHAR_MAX:unsigned char 的最大值。
  • USHRT_MAX:unsigned short 的最大值。
  • UINT_MAX:unsigned int 的最大值。
  • ULONG_MAX:unsigned long 的最大值。
  • ULLONG_MAX:unsigned long long 的最大值。

4.2.5 整数的进制

我们使用

C 语言的整数默认都是十进制数,如果要表示八进制数和十六进制数,必须使用专门的表示法。

八进制使用0作为前缀,比如0170377

int a = 012; // 八进制,相当于十进制的10

十六进制使用0x0X作为前缀,比如0xf0X10

int a = 0x1A2B; // 十六进制,相当于十进制的6699

有些编译器使用0b前缀,表示二进制数,但不是标准。

int x = 0b101010;

4.3 浮点数类型 float

有的时候我们需要表示一些带有小数部分的数字,这个时候就可以用浮点数来表示了

float c = 10.5;

float类型占用4个字节(32位),其中8位存放指数的值和符号,剩下24位存放小数的值和符号。float类型至少能够提供(十进制的)6位有效数字,指数部分的范围为(十进制的)-3737,即数值范围为10-37到1037

有时候,32位浮点数提供的精度或者数值范围还不够,C 语言又提供了另外两种更大的浮点数类型。

  • double:占用8个字节(64位),至少提供13位有效数字。
  • long double:通常占用16个字节。

因为使用二进制,所以浮点数的存储并不是一个精确的值,对于任何语言都是这样(比如python),所以在处理浮点数相加判断的时候要分外小心:

if (0.2+0.1 == 0.3)

这个表达式其实是false

对于0.A,或者A.0 0是可以省略的(比如0.2可以直接写.2)

4.4 bool类型 true/false

我们可以用bool类型来存储一个一个真伪值:

#include <stdbool.h>

bool flag = false;

4.5 溢出

每一种数据类型都有数值范围,如果存放的数值超出了这个范围(小于最小值或大于最大值),需要更多的二进制位存储,就会发生溢出。大于最大值,叫做向上溢出(overflow);小于最小值,叫做向下溢出(underflow)。

一般来说,编译器不会对溢出报错,会正常执行代码,但是会忽略多出来的二进制位,只保留剩下的位,这样往往会得到意想不到的结果。所以,应该避免溢出。

unsigned char x = 255;
x = x + 1;

printf("%d\n", x); // 0

上面示例中,变量x1,得到的结果不是256,而是0。因为xunsign char类型,最大值是255(二进制11111111),加1后就发生了溢出,256(二进制100000000)的最高位1被丢弃,剩下的值就是0

再看下面的例子。

unsigned int ui = UINT_MAX;  // 4,294,967,295
ui++;
printf("ui = %u\n", ui); // 0
ui--;
printf("ui = %u\n", ui); // 4,294,967,295

上面示例中,常量UINT_MAX是 unsigned int 类型的最大值。如果加1,对于该类型就会溢出,从而得到0;而0是该类型的最小值,再减1,又会得到UINT_MAX

溢出很容易被忽视,编译器又不会报错,所以必须非常小心。

for (unsigned int i = n; i >= 0; --i) // 错误

上面代码表面看似乎没有问题,但是循环变量i的类型是 unsigned int,这个类型的最小值是0,不可能得到小于0的结果。当i等于0,再减去1的时候,并不会返回-1,而是返回 unsigned int 的类型最大值,这个值总是大于等于0,导致无限循环。

为了避免溢出,最好方法就是将运算结果与类型的极限值进行比较。

unsigned int ui;
unsigned int sum;

// 错误
if (sum + ui > UINT_MAX) too_big();
else sum = sum + ui;

// 正确
if (ui > UINT_MAX - sum) too_big();
else sum = sum + ui;

上面示例中,变量sumui都是 unsigned int 类型,它们相加的和还是 unsigned int 类型,这就有可能发生溢出。但是,不能通过相加的和是否超出了最大值UINT_MAX,来判断是否发生了溢出,因为sum + ui总是返回溢出后的结果,不可能大于UINT_MAX。正确的比较方法是,判断UINT_MAX - sumui之间的大小关系。

下面是另一种错误的写法。

unsigned int i = 5;
unsigned int j = 7;

if (i - j < 0) // 错误
  printf("negative\n");
else
  printf("positive\n");

上面示例的运算结果,会输出positive。原因是变量ij都是 unsigned int 类型,i - j的结果也是这个类型,最小值为0,不可能得到小于0的结果。正确的写法是写成下面这样。

if (j > i) // ....

4.6 sizeof 运算符

sizeof是 C 语言提供的一个运算符,返回某种数据类型或某个值占用的字节数量。它的参数可以是数据类型的关键字,也可以是变量名或某个具体的值。

// 参数为数据类型
int x = sizeof(int);

// 参数为变量
int i;
sizeof(i);

// 参数为数值
sizeof(3.14);

上面的第一个示例,返回得到int类型占用的字节数量(通常是48)。第二个示例返回整数变量占用字节数量,结果与前一个示例完全一样。第三个示例返回浮点数3.14占用的字节数量,由于浮点数的字面量一律存储为 double 类型,所以会返回8,因为 double 类型占用的8个字节。

sizeof运算符的返回值,C 语言只规定是无符号整数,并没有规定具体的类型,而是留给系统自己去决定,sizeof到底返回什么类型。不同的系统中,返回值的类型有可能是unsigned int,也有可能是unsigned long,甚至是unsigned long long,对应的printf()占位符分别是%u%lu%llu。这样不利于程序的可移植性。

C 语言提供了一个解决方法,创造了一个类型别名size_t,用来统一表示sizeof的返回值类型。该别名定义在stddef.h头文件(引入stdio.h时会自动引入)里面,对应当前系统的sizeof的返回值类型,可能是unsigned int,也可能是unsigned long

C 语言还提供了一个常量SIZE_MAX,表示size_t可以表示的最大整数。所以,size_t能够表示的整数范围为[0, SIZE_MAX]

printf()有专门的占位符%zd%zu,用来处理size_t类型的值。

printf("%zd\n", sizeof(int));

上面代码中,不管sizeof返回值的类型是什么,%zd占位符(或%zu)都可以正确输出。

如果当前系统不支持%zd%zu,可使用%u(unsigned int)或%lu(unsigned long int)代替。

4.7 类型的自动转换

某些情况下,C 语言会自动转换某个值的类型。

4.7.1 赋值运算

赋值运算符会自动将右边的值,转成左边变量的类型。

(1)浮点数赋值给整数变量

浮点数赋予整数变量时,C 语言直接丢弃小数部分,而不是四舍五入。

int x = 3.14;

上面示例中,变量x是整数类型,赋给它的值是一个浮点数。编译器会自动把3.14先转为int类型,丢弃小数部分,再赋值给x,因此x的值是3

这种自动转换会导致部分数据的丢失(3.14丢失了小数部分),所以最好不要跨类型赋值,尽量保证变量与所要赋予的值是同一个类型。

注意,舍弃小数部分时,不是四舍五入,而是整个舍弃。

int x = 12.99;

上面示例中,x等于12,而不是四舍五入的13

(2)整数赋值给浮点数变量

整数赋值给浮点数变量时,会自动转为浮点数。

float y = 12 * 2;

上面示例中,变量y的值不是24,而是24.0,因为等号右边的整数自动转为了浮点数。

(3)窄类型赋值给宽类型

字节宽度较小的整数类型,赋值给字节宽度较大的整数变量时,会发生类型提升,即窄类型自动转为宽类型。

比如,charshort类型赋值给int类型,会自动提升为int

char x = 10;
int i = x + y;

上面示例中,变量x的类型是char,由于赋值给int类型,所以会自动提升为int

(4)宽类型赋值给窄类型

字节宽度较大的类型,赋值给字节宽度较小的变量时,会发生类型降级,自动转为后者的类型。这时可能会发生截值(truncation),系统会自动截去多余的二进制位,导致难以预料的结果。

int i = 321;
char ch = i; // ch 的值是 65 (321 - 256)

上面例子中,变量chchar类型,宽度是8个二进制位。变量iint类型,将i赋值给ch,后者只能容纳i(二进制形式为101000001,共9位)的后八位,前面多出来的二进制位被丢弃,保留后八位就变成了01000001(十进制的65,相当于字符A)。

浮点数赋值给整数类型的值,也会发生截值,浮点数的小数部分会被截去。

double pi = 3.14159;
int i = pi; // i 的值为 3

上面示例中,i等于3pi的小数部分被截去了。

4.7.2 混合类型的运算

不同类型的值进行混合计算时,必须先转成同一个类型,才能进行计算。转换规则如下:

(1)整数与浮点数混合运算时,整数转为浮点数类型,与另一个运算数类型相同。

3 + 1.2 // 4.2

上面示例是int类型与float类型的混合计算,int类型的3会先转成float3.0,再进行计算,得到4.2

(2)不同的浮点数类型混合运算时,宽度较小的类型转为宽度较大的类型,比如float转为doubledouble转为long double

(3)不同的整数类型混合运算时,宽度较小的类型会提升为宽度较大的类型。比如short转为intint转为long等,有时还会将带符号的类型signed转为无符号unsigned

下面例子的执行结果,可能会出人意料。

int a = -5;
if (a < sizeof(int))
  do_something();

上面示例中,变量a是带符号整数,sizeof(int)size_t类型,这是一个无符号整数。按照规则,signed int 自动转为 unsigned int,所以a会自动转成无符号整数4294967291(转换规则是-5加上无符号整数的最大值,再加1),导致比较失败,do_something()不会执行。

所以,最好避免无符号整数与有符号整数的混合运算。因为这时 C 语言会自动将signed int转为unsigned int,可能不会得到预期的结果。

4.7.3 整数类型的运算

两个相同类型的整数运算时,或者单个整数的运算,一般来说,运算结果也属于同一类型。但是有一个例外,宽度小于int的类型,运算结果会自动提升为int

unsigned char a = 66;

if ((-a) < 0) printf("negative\n");
else printf("positive\n");

上面示例中,变量a是 unsigned char 类型,这个类型不可能小于0,但是-a不是 unsigned char 类型,会自动转为 int 类型,导致上面的代码输出 negative。

再看下面的例子。

unsigned char a = 1;
unsigned char b = 255;
unsigned char c = 255;

if ((a - 5) < 0) do_something();
if ((b + c) > 300) do_something();

上面示例中,表达式a - 5b + c都会自动转为 int 类型,所以函数do_something()会执行两次。

4.7.4 函数

函数的参数和返回值,会自动转成函数定义里指定的类型。

int dostuff(int, unsigned char);

char m = 42;
unsigned short n = 43;
long long int c = dostuff(m, n);

上面示例中,参数变量mn不管原来的类型是什么,都会转成函数dostuff()定义的参数类型。

下面是返回值自动转换类型的例子。

char func(void) {
  int a = 42;
  return a;
}

上面示例中,函数内部的变量aint类型,但是返回的值是char类型,因为函数定义中返回的是这个类型。

4.7 类型的显式转换

原则上,应该避免类型的自动转换,防止出现意料之外的结果。C 语言提供了类型的显式转换,允许手动转换类型。

只要在一个值或变量的前面,使用圆括号指定类型(type),就可以将这个值或变量转为指定的类型,这叫做“类型指定”(casting)。

(unsigned char) ch

上面示例将变量ch转成无符号的字符类型。

long int y = (long int) 10 + 12;

上面示例中,(long int)10显式转为long int类型。这里的显示转换其实是不必要的,因为赋值运算符会自动将右边的值,转为左边变量的类型。

4.8 可移植类型(不会考啦

C 语言的整数类型(short、int、long)在不同计算机上,占用的字节宽度可能是不一样的,无法提前知道它们到底占用多少个字节。

程序员有时控制准确的字节宽度,这样的话,代码可以有更好的可移植性,头文件stdint.h创造了一些新的类型别名。

(1)精确宽度类型(exact-width integer type),保证某个整数类型的宽度是确定的。

  • int8_t:8位有符号整数。
  • int16_t:16位有符号整数。
  • int32_t:32位有符号整数。
  • int64_t:64位有符号整数。
  • uint8_t:8位无符号整数。
  • uint16_t:16位无符号整数。
  • uint32_t:32位无符号整数。
  • uint64_t:64位无符号整数。

上面这些都是类型别名,编译器会指定它们指向的底层类型。比如,某个系统中,如果int类型为32位,int32_t就会指向int;如果long类型为32位,int32_t则会指向long

下面是一个使用示例。

#include <stdio.h>
#include <stdint.h>

int main(void) {
  int32_t x32 = 45933945;
  printf("x32 = %d\n", x32);
  return 0;
}

上面示例中,变量x32声明为int32_t类型,可以保证是32位的宽度。

(2)最小宽度类型(minimum width type),保证某个整数类型的最小长度。

  • int_least8_t
  • int_least16_t
  • int_least32_t
  • int_least64_t
  • uint_least8_t
  • uint_least16_t
  • uint_least32_t
  • uint_least64_t

上面这些类型,可以保证占据的字节不少于指定宽度。比如,int_least8_t表示可以容纳8位有符号整数的最小宽度的类型。

(3)最快的最小宽度类型(fast minimum width type),可以使整数计算达到最快的类型。

  • int_fast8_t
  • int_fast16_t
  • int_fast32_t
  • int_fast64_t
  • uint_fast8_t
  • uint_fast16_t
  • uint_fast32_t
  • uint_fast64_t

上面这些类型是保证字节宽度的同时,追求最快的运算速度,比如int_fast8_t表示对于8位有符号整数,运算速度最快的类型。这是因为某些机器对于特定宽度的数据,运算速度最快,举例来说,32位计算机对于32位数据的运算速度,会快于16位数据。

(4)可以保存指针的整数类型。

  • intptr_t:可以存储指针(内存地址)的有符号整数类型。
  • uintptr_t:可以存储指针的无符号整数类型。

(5)最大宽度整数类型,用于存放最大的整数。

  • intmax_t:可以存储任何有效的有符号整数的类型。
  • uintmax_t:可以存放任何有效的无符号整数的类型。

上面的这两个类型的宽度比long longunsigned long更大。

5 大的要来了 指针

5.1 地址/内存都是什么

当我们把一个东西存入计算机的内存时,计算机会在内存中划一块指定大小的地方(声明变量的大小)来存这个值,但是当我们要使用这个值的时候,就需要知道这个值存在了内存的哪里,所以就用地址来表示内存中值的位置,也方便我们找寻存在内存中的值是多少,在哪里。

字符*表示指针,通常跟在类型关键字的后面,表示指针指向的是什么类型的值。比如,char*表示一个指向字符的指针,float*表示一个指向float类型的值的指针。

指针中存放的就是后面常量的地址。

int* intPtr;
//其实*写在任何地方都是可以的,但是写在类型后面比较。。额广泛?
int   *intPtr;
int * intPtr;
int*  intPtr;

当你需要一行中声明多个指针变量时:

// 正确
int * foo, * bar;

// 错误
int* foo, bar;

当然也可以套娃,毕竟指针存在内存里面也是有地址的:

int** foo;

这样就有了一个指向指针的指针(让阿尼亚来告诉你吧

5.2 *运算符

*这个符号除了表示指针以外,还可以作为运算符,用来取出指针变量所指向的内存地址里面的值。

void increment(int* p) {
  *p = *p + 1;
}

上面示例中,函数increment()的参数是一个整数指针p。函数体里面,*p就表示指针p所指向的那个值。对*p赋值,就表示改变指针所指向的那个地址里面的值。

上面函数的作用是将参数值加1。该函数没有返回值,因为传入的是地址,函数体内部对该地址包含的值的操作,会影响到函数外部,所以不需要返回值。事实上,函数内部通过指针,将值传到外部,是 C 语言的常用方法。

变量地址而不是变量值传入函数,还有一个好处。对于需要大量存储空间的大型变量,复制变量值传入函数,非常浪费时间和空间,不如传入指针来得高效。

5.3 &运算符

用于读取某个变量的地址。

void increment(int* p) {
  *p = *p + 1;
}

int x = 1;
increment(&x);
printf("%d\n", x); // 2

& 与 * 互相构成逆运算

5.4 初始化指针变量

初始化如果不赋值会给一个随机的地址,如果这个时候再去操作这个地址可能会出大问题。

所以我们可以给它赋一些初始值:

int* p = NULL;

这样。

在尝试读写时就会报错

5.5 指针的运算操作

5.5.1 指针与整数值的加减运算

指针与整数值的运算,表示指针的移动。

short* j;
j = (short*)0x1234;
j = j + 1; // 0x1236

上面示例中,j是一个指针,指向内存地址0x1234。你可能以为j + 1等于0x1235,但正确答案是0x1236。原因是j + 1表示指针向内存地址的高位移动一个单位,而一个单位的short类型占据两个字节的宽度,所以相当于向高位移动两个字节。同样的,j - 1得到的结果是0x1232

指针移动的单位,与指针指向的数据类型有关。数据类型占据多少个字节,每单位就移动多少个字节。

5.5.2 指针与指针的加法运算

指针只能与整数值进行加减运算,两个指针进行加法是非法的。

unsigned short* j;
unsigned short* k;
x = j + k; // 非法

上面示例是两个指针相加,这是非法的。

5.5.3 指针与指针的减法

相同类型的指针允许进行减法运算,返回它们之间的距离,即相隔多少个数据单位。

高位地址减去低位地址,返回的是正值;低位地址减去高位地址,返回的是负值。

这时,减法返回的值属于ptrdiff_t类型,这是一个带符号的整数类型别名,具体类型根据系统不同而不同。这个类型的原型定义在头文件stddef.h里面。

short* j1;
short* j2;

j1 = (short*)0x1234;
j2 = (short*)0x1236;

ptrdiff_t dist = j2 - j1;
printf("%td\n", dist); // 1

上面示例中,j1j2是两个指向 short 类型的指针,变量dist是它们之间的距离,类型为ptrdiff_t,值为1,因为相差2个字节正好存放一个 short 类型的值。

5.5.4 指针与指针的比较运算

指针之间的比较运算,比较的是各自的内存地址哪一个更大,返回值是整数1(true)或0(false)。

6 函数

函数的作用很鲜明:一段需要反复执行的代码。有输入与输出(当然可以不输出)。

函数的定义方法很简单:先定义返回值的类型,然后是函数名字,输入的参数,函数执行的函数体,return语句(函数输出语句)。

int plus_one(int n) {
  return n + 1;
}

比如以上的代码定义了一个返回整形的函数 输入也是整数 返回的内容是输入加一。

然后当需要调用这个函数的时候,就可以直接用函数名字加输入,然后输出给左边的变量:

int a = plus_one(13);
// a 等于 14

通常我们把函数定义的参数叫做形参,传递进去的参数叫做实参

C语言要求所有的函数应该在程序的开头处定义。

如果你的函数不需要返回任何值,请使用void关键字:

void myFunc(void) {
  // ...
}

同样的,我们可以在函数内部调用函数自己来实现一些操作,这个过程叫做递归:

//一个计算斐波那契数列的例子
unsigned long Fibonacci(unsigned n) {
  if (n > 2)
    return Fibonacci(n - 1) + Fibonacci(n - 2);
  else
    return 1;
}

6.2 main函数

对于C语言,程序的入口就在main函数处

ps:如果你是从arduino编程开始学习可能会疑惑为什么是从setup函数开始的,且不用定义main函数,那是因为在Arduino.h头文件里面已经定义好了:

int main(void)
{
    init();        //硬件初始化

    initVariant();  //特有硬件初始化。因为不同的开发板有自己独特的初始化逻辑。

#if defined(USBCON)
    USBDevice.attach();
#endif

    setup();

    for (;;) {
        loop();
        if (serialEventRun) serialEventRun();
    }

    return 0;
}

好,我们继续:

main并不会要求有某一个固定的返回类型,但是值得注意的是void修饰main的时候main里面return值是可以为编译器所接受的(当然我及其不推荐这样写)

int main(void) {
  printf("Hello World\n");
  return 0;
}

return 0;通常代表程序运行成功,甚至main里面不写return 0;也会被编译器给加上,当然还是加上比较看起来统一。

6.4 函数中指针的使用

如果想要在函数中改变外部参数的值,我们可以传递参数的指针进来,这样就可以在里面对外部变量进行操作。如图:

# include "stdio.h"

void swap(int *a,int *b){

    *a=*a+*b;
    *b=*a-*b;
    *a=*a-*b;

}

int main()
{int x = 1, y = 2;
    printf("%d,%d",x,y);
    swap(&x,&y);

return 0;

}

6.5 提前定义函数

当然我们为了规避必须在main之前申明函数这件事情,我们可以提前申请一个函数的原型在main之前,然后再需要定义的地方再对函数进行详细地描述。

void func1(void) {
}

void func2(void) {
}

int main(void) {
  func1();
  func2();
  return 0;
}

6.6 exit()函数

考试不会考这个。

exit()函数用来终止整个程序的运行。一旦执行到该函数,程序就会立即结束。该函数的原型定义在头文件stdlib.h里面。

exit()可以向程序外部返回一个值,它的参数就是程序的返回值。一般来说,使用两个常量作为它的参数:EXIT_SUCCESS(相当于 0)表示程序运行成功,EXIT_FAILURE(相当于 1)表示程序异常中止。这两个常数也是定义在stdlib.h里面。

// 程序运行成功
// 等同于 exit(0);
exit(EXIT_SUCCESS);

// 程序异常中止
// 等同于 exit(1);
exit(EXIT_FAILURE);

main()函数里面,exit()等价于使用return语句。其他函数使用exit(),就是终止整个程序的运行,没有其他作用。

C 语言还提供了一个atexit()函数,用来登记exit()执行时额外执行的函数,用来做一些退出程序时的收尾工作。该函数的原型也是定义在头文件stdlib.h

int atexit(void (*func)(void));

atexit()的参数是一个函数指针。注意,它的参数函数(下例的print)不能接受参数,也不能有返回值。

void print(void) {
  printf("something wrong!\n");
}

atexit(print);
exit(EXIT_FAILURE);

上面示例中,exit()执行时会先自动调用atexit()注册的print()函数,然后再终止程序。

6.7 函数的一些标识

6.7.1 extern 说明符

在main函数前声明,表示当前函数来自其他文件

extern int foo(int arg1, char arg2);

6.7.2 static 说明符

默认情况下,每次调用函数时,函数的内部变量都会重新初始化,不会保留上一次运行的值。static说明符可以改变这种行为。

static用于函数内部声明变量时,表示该变量只需要初始化一次,不需要在每次调用时都进行初始化。也就是说,它的值在两次调用之间保持不变。

#include <stdio.h>

void counter(void) {
  static int count = 1;  // 只初始化一次
  printf("%d\n", count);
  count++;
}

int main(void) {
  counter();  // 1
  counter();  // 2
  counter();  // 3
  counter();  // 4
}

注意 ,使用static赋值时,请使用常量而非变量。

6.7.3 const 说明符

函数参数里面的const说明符,表示函数内部不得修改该参数变量。

void f(int* p) {
  // ...
}

上面示例中,函数f()的参数是一个指针p,函数内部可能会改掉它所指向的值*p,从而影响到函数外部。

为了避免这种情况,可以在声明函数时,在指针参数前面加上const说明符,告诉编译器,函数内部不能修改该参数所指向的值。

void f(const int* p) {
  *p = 0; // 该行报错
}

上面示例中,声明函数时,const指定不能修改指针p指向的值,所以*p = 0就会报错。

但是上面这种写法,只限制修改p所指向的值,而p本身的地址是可以修改的。

void f(const int* p) {
  int x = 13;
  p = &x; // 允许修改
}

上面示例中,p本身是可以修改,const只限定*p不能修改。

如果想限制修改p,可以把const放在p前面。

void f(int* const p) {
  int x = 13;
  p = &x; // 该行报错
}

如果想同时限制修改p*p,需要使用两个const

void f(const int* const p) {
  // ...
}

6.8 可变参数

有些函数的参数数量是不确定的,声明函数的时候,可以使用省略号...表示可变数量的参数。

int printf(const char* format, ...);

上面示例是printf()函数的原型,除了第一个参数,其他参数的数量是可变的,与格式字符串里面的占位符数量有关。这时,就可以用...表示可变数量的参数。

注意,...符号必须放在参数序列的结尾,否则会报错。

头文件stdarg.h定义了一些宏,可以操作可变参数。

(1)va_list:一个数据类型,用来定义一个可变参数对象。它必须在操作可变参数时,首先使用。

(2)va_start:一个函数,用来初始化可变参数对象。它接受两个参数,第一个参数是可变参数对象,第二个参数是原始函数里面,可变参数之前的那个参数,用来为可变参数定位。

(3)va_arg:一个函数,用来取出当前那个可变参数,每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,第一个是可变参数对象,第二个是当前可变参数的类型。

(4)va_end:一个函数,用来清理可变参数对象。

下面是一个例子。

double average(int i, ...) {
  double total = 0;
  va_list ap;
  va_start(ap, i);
  for (int j = 1; j <= i; ++j) {
    total += va_arg(ap, double);
  }
  va_end(ap);
  return total / i;
}

上面示例中,va_list ap定义ap为可变参数对象,va_start(ap, i)将参数i后面的参数统一放入apva_arg(ap, double)用来从ap依次取出一个参数,并且指定该参数为 double 类型,va_end(ap)用来清理可变参数对象。

7 数组

c语言的数组是个很迷的东西,它会在声明的时候在内存中申请一块空间,在之后不会发生改变,空间的大小有数组的数据类型决定,为连续的内存地址。

7.1 数组的声明与赋值

数组的声明使用变量名加方括号来声明,方括号内是数组的元素数量:

int scores[100];

注意:数组的索引是从0开始而非从1开始,比如你要调用还是那个面声明那个数组的第一个值,你应该使用:

scores[0];

在试图访问数组中不存在的元素时,会出现数组越界操作,此时将不会报错(悲,与此相反,数组最后一个元素最后那个值将被访问,这是很不好的。

数组的赋值可以使用大括号来实现:

int a[5] = {22, 37, 3490, 18, 95};

请注意,大括号赋值必须在数组声明的时候使用,而不是在数组声明好了之后再改变数组的值,实惠报错的。

当然,数组的赋值不用传递每一个值,未赋值的值将默认为0。

int a[15] = {[2] = 29, [9] = 7, [14] = 48};

C 语言允许省略方括号里面的数组成员数量,这时将根据大括号里面的值的数量,自动确定数组的长度。

int a[] = {22, 37, 3490};
// 等同于
int a[3] = {22, 37, 3490};

省略成员数量时,如果同时采用指定位置的赋值,那么数组长度将是最大的指定位置再加1。

int a[] = {[2] = 6, [9] = 12};

上面示例中,数组a的最大指定位置是9,所以数组的长度是10。

7.2 数组的长度

sizeof运算符会返回整个数组的字节长度。

int a[] = {22, 37, 3490};
int arrLen = sizeof(a); // 12

上面示例中,sizeof返回数组a的字节长度是12

由于数组成员都是同一个类型,每个成员的字节长度都是一样的,所以数组整体的字节长度除以某个数组成员的字节长度,就可以得到数组的成员数量。

sizeof(a) / sizeof(a[0])

上面示例中,sizeof(a)是整个数组的字节长度,sizeof(a[0])是数组成员的字节长度,相除就是数组的成员数量。

注意,sizeof返回值的数据类型是size_t,所以sizeof(a) / sizeof(a[0])的数据类型也是size_t。在printf()里面的占位符,要用%zd%zu

int x[12];

printf("%zu\n", sizeof(x));     // 48
printf("%zu\n", sizeof(int));  // 4
printf("%zu\n", sizeof(x) / sizeof(int)); // 12

上面示例中,sizeof(x) / sizeof(int)就可以得到数组成员数量12

7.3 多维数组

我们可以声明一个多个维度的数组数组的维度和方括号数量相等。比如我们需要声明一个二维数组的话:

int board[10][10];

比如上面的例子的数组中就会存在100个元素

跟一维数组一样,多维数组每个维度的第一个成员也是从0开始编号。

多维数组也可以使用大括号,一次性对所有成员赋值。

int a[2][5] = {
  {0, 1, 2, 3, 4},
  {5, 6, 7, 8, 9}
};
int a[2][2] = {[0][0] = 1, [1][1] = 2};

不管数组有多少维度,在内存里面都是线性存储,或者说多维数组会被拍扁为一位的数组,a[0][0]的后面是a[0][1]a[0][1]的后面是a[1][0],以此类推。因此,多维数组也可以使用单层大括号赋值,下面的语句与上面的赋值语句是完全等同的。

int a[2][2] = {1, 0, 0, 2};

7.4 可变长度数组

int n = x + y;
int arr[n];

其中n是程序运行时才会确定的数组长度,称之为变长数组。

7.5 数组的地址

因为数组再内存中的线性存储,所以我们知道数组的第一个元素地址就可以索引整个数组的地址。

int a[5] = {11, 22, 33, 44, 55};
int* p;

p = &a[0];

printf("%d\n", *p);  // Prints "11"

上面示例中,&a[0]就是数组a的首个成员11的内存地址,也是整个数组的起始地址。反过来,从这个地址(*p),可以获得首个成员的值11

由于数组的起始地址是常用操作,&array[0]的写法有点麻烦,C 语言提供了便利写法,数组名等同于起始地址,也就是说,数组名就是指向第一个成员(array[0])的指针。

这样的话,如果把数组名传入一个函数,就等同于传入一个指针变量。在函数内部,就可以通过这个指针变量获得整个数组。

函数接受数组作为参数,函数原型可以写成下面这样。

// 写法一
int sum(int arr[], int len);
// 写法二
int sum(int* arr, int len);

上面示例中,传入一个整数数组,与传入一个整数指针是同一回事,数组符号[]与指针符号*是可以互换的。下一个例子是通过数组指针对成员求和。

int sum(int* arr, int len) {
  int i;
  int total = 0;

  // 假定数组有 10 个成员
  for (i = 0; i < len; i++) {
    total += arr[i];
  }
  return total;
}

上面示例中,传入函数的是一个指针arr(也是数组名)和数组长度,通过指针获取数组的每个成员,从而求和。

*&运算符也可以用于多维数组。

上面示例中,由于a[0]本身是一个指针,指向第二维数组的第一个成员a[0][0]。所以,*(a[0])取出的是a[0][0]的值。至于**a,就是对a进行两次*运算,第一次取出的是a[0],第二次取出的是a[0][0]。同理,二维数组的&a[0][0]等同于*a

7.6 数组指针的加减

C 语言里面,数组名可以进行加法和减法运算,等同于在数组成员之间前后移动,即从一个成员的内存地址移动到另一个成员的内存地址。比如,a + 1返回下一个成员的地址,a - 1返回上一个成员的地址。

int a[5] = {11, 22, 33, 44, 55};

for (int i = 0; i < 5; i++) {
  printf("%d\n", *(a + i));
}

上面示例中,通过指针的移动遍历数组,a + i的每轮循环每次都会指向下一个成员的地址,*(a + i)取出该地址的值,等同于a[i]。对于数组的第一个成员,*(a + 0)(即*a)等同于a[0]

由于数组名与指针是等价的,所以下面的等式总是成立。

a[b] == *(a + b)

上面代码给出了数组成员的两种访问方式,一种是使用方括号a[b],另一种是使用指针*(a + b)

如果指针变量p指向数组的一个成员,那么p++就相当于指向下一个成员,这种方法常用来遍历数组。

int a[] = {11, 22, 33, 44, 55, 999};

int* p = a;

while (*p != 999) {
  printf("%d\n", *p);
  p++;
}

上面示例中,通过p++让变量p指向下一个成员。

注意,数组名指向的地址是不能变的,所以上例中,不能直接对a进行自增,即a++的写法是错的,必须将a的地址赋值给指针变量p,然后对p进行自增。

遍历数组一般都是通过数组长度的比较来实现,但也可以通过数组起始地址和结束地址的比较来实现。

int sum(int* start, int* end) {
  int total = 0;

  while (start < end) {
    total += *start;
    start++;
  }

  return total;
}

int arr[5] = {20, 10, 5, 39, 4};
printf("%i\n", sum(arr, arr + 5));

上面示例中,arr是数组的起始地址,arr + 5是结束地址。只要起始地址小于结束地址,就表示还没有到达数组尾部。

反过来,通过数组的减法,可以知道两个地址之间有多少个数组成员,请看下面的例子,自己实现一个计算数组长度的函数。

int arr[5] = {20, 10, 5, 39, 88};
int* p = arr;

while (*p != 88)
  p++;

printf("%i\n", p - arr); // 4

上面示例中,将某个数组成员的地址,减去数组起始地址,就可以知道,当前成员与起始地址之间有多少个成员。

对于多维数组,数组指针的加减法对于不同维度,含义是不一样的。

int arr[4][2];

// 指针指向 arr[1]
arr + 1;

// 指针指向 arr[0][1]
arr[0] + 1

上面示例中,arr是一个二维数组,arr + 1是将指针移动到第一维数组的下一个成员,即arr[1]。由于每个第一维的成员,本身都包含另一个数组,即arr[0]是一个指向第二维数组的指针,所以arr[0] + 1的含义是将指针移动到第二维数组的下一个成员,即arr[0][1]。(maybe

同一个数组的两个成员的指针相减时,返回它们之间的距离。

int* p = &a[5];
int* q = &a[1];

printf("%d\n", p - q); // 4
printf("%d\n", q - p); // -4

上面示例中,变量pq分别是数组5号位置和1号位置的指针,它们相减等于4或-4。

7.7 数组的复制  

由于数组名是指针,所以复制数组不能简单地复制数组名。

int* a;
int b[3] = {1, 2, 3};

a = b;

上面的写法,结果不是将数组b复制给数组a,而是让ab指向同一个数组。

复制数组最简单的方法,还是使用循环,将数组元素逐个进行复制。

for (i = 0; i < N; i++)
  a[i] = b[i];

上面示例中,通过将数组b的成员逐个复制给数组a,从而实现数组的赋值。

另一种方法是使用memcpy()函数(定义在头文件string.h),直接把数组所在的那一段内存,再复制一份。

memcpy(a, b, sizeof(b));

上面示例中,将数组b所在的那段内存,复制给数组a。这种方法要比循环复制数组成员要快。

7.8 传递数组作为函数的实参

7.8.1 声明函数形参

我们通常会传递数组的变量名和数组的长度:

int sum_array(int a[], int n) {
  // ...
}

因为传递的数组变量名只会告诉数组的启示地址,所以需要传递数组的长度。

多维数组需要自己在函数内定义一下

int sum_array(int a[][4], int n) {
  // ...
}

int a[2][4] = {
  {1, 2, 3, 4},
  {8, 9, 10, 11}
};
int sum = sum_array(a, 2);

这样可以确保传递的数组size正确。

上面示例中,函数sum_array()的参数是一个二维数组。第一个参数是数组本身(a[][4]),这时可以不写第一维的长度,因为它作为第二个参数,会传入函数,但是一定要写第二维的长度4

这是因为函数内部拿到的,只是数组的起始地址a,以及第一维的成员数量2。如果要正确计算数组的结束地址,还必须知道第一维每个成员的字节长度。写成int a[][4],编译器就知道了,第一维每个成员本身也是一个数组,里面包含了4个整数,所以每个成员的字节长度就是4 * sizeof(int)

7.8.2 变长数组做参数:

pass来不及写

8 字符串

不同于python非常方便的字符串用法,这里甚至要单独搞一章出来。(也许我应该抽空写一个python教程

在C语言中我们的字符在前文中已经提到是char类型的整形数字,那么字符串也很好理解了,那就是用上一章提到的数组来把字符串一个一个全部串起来。

但是不同于用一个数组来把字符全部一个一个装起来,C语言要求字符串的结尾字符必须是'\0',请注意,这里不是0,而是\0,专门代表字符串的结束。

{'H', 'e', 'l', 'l', 'o', '\0'}

// 等价于
"Hello"

如上,我们也可以用双引号把这个字符串套起来,然后C语言会帮你把这个字符串塞进数组,然后加上结束符\0

注意:双引号括起来的是字符串,单引号括起来的是单个字符,如果你是耍蛇高手(我是说熟练使用python(笑,可能会在这里犯错。

C 语言允许合并多个字符串字面量,只要这些字符串之间没有间隔,或者只有空格,C 语言会将它们自动合并。

char greeting[50] = "Hello, ""how are you ""today!";
// 等同于
char greeting[50] = "Hello, how are you today!";
//等同于
char greeting[50] = "Hello, "
  "how are you "
  "today!";

输出字符串方法详见第一章。

8.2 字符串变量的声明

和数组一样,是一个指针指向数组的第一个值

// 写法一
char s[14] = "Hello, world!";

// 写法二
char* s = "Hello, world!";

上面两种写法都声明了一个字符串变量s。如果采用第一种写法,由于字符数组的长度可以让编译器自动计算,所以声明时可以省略字符数组的长度。

声明的数组大小可以大于字符串大小,但是不能小于。

char s[] = "Hello, world!";

那么写法一和写法二有什么区别呢

首先就是第一个写法的字符串允许修改其内部值,而第二个不允许。

char* s = "Hello, world!";
s[0] = 'z'; // 错误
char s[] = "Hello, world!";
s[0] = 'z';

第二个区别就是指针声明的方法可以把指针拿去指向别的地方,而第二个不允许。

char* s = "hello";
s = "world";
char s[10];
s = "abc"; // 错误

所以如果使用第二个方法赋值,请在申明变量的时候就给值写好。

想要重新赋值,必须使用 C 语言原生提供的strcpy()函数,通过字符串拷贝完成赋值。这样做以后,数组变量的地址还是不变的,即strcpy()只是在原地址写入新的字符串,而不是让数组变量指向新的地址。

char s[10];
strcpy(s, "abc");

上面示例中,strcpy()函数把字符串abc拷贝给变量s,这个函数的详细用法会在后面介绍。

8.3 strlen()

strlen()函数返回字符串的字节长度,不包括末尾的空字符\0。该函数的原型如下。

// string.h
size_t strlen(const char* s);

它的参数是字符串变量,返回的是size_t类型的无符号整数,除非是极长的字符串,一般情况下当作int类型处理即可。下面是一个用法实例。

char* str = "hello";
int len = strlen(str); // 5

strlen()的原型在标准库的string.h文件中定义,使用时需要加载头文件string.h

#include <stdio.h>
#include <string.h>

int main(void) {
  char* s = "Hello, world!";
  printf("The string is %zd characters long.\n", strlen(s));
}

注意,字符串长度(strlen())与字符串变量长度(sizeof()),是两个不同的概念。

char s[50] = "hello";
printf("%d\n", strlen(s));  // 5
printf("%d\n", sizeof(s));  // 50

上面示例中,字符串长度是5,字符串变量长度是50。

如果不使用这个函数,可以通过判断字符串末尾的\0,自己计算字符串长度。

关于sizeof的字符长度计算在前一章已有介绍。

int my_strlen(char *s) {
  int count = 0;
  while (s[count] != '\0')
    count++;
  return count;
}

8.4 strcpy() 

字符串的复制,不能使用赋值运算符,直接将一个字符串赋值给字符数组变量。

char str1[10];
char str2[10];

str1 = "abc"; // 报错
str2 = str1;  // 报错

上面两种字符串的复制写法,都是错的。因为数组的变量名是一个固定的地址,不能修改,使其指向另一个地址。

如果是字符指针,赋值运算符(=)只是将一个指针的地址复制给另一个指针,而不是复制字符串。

char* s1;
char* s2;

s1 = "abc";
s2 = s1;

上面代码可以运行,结果是两个指针变量s1s2指向同一字符串,而不是将字符串s1的内容复制给s2

C 语言提供了strcpy()函数,用于将一个字符串的内容复制到另一个字符串,相当于字符串赋值。该函数的原型定义在string.h头文件里面。

strcpy(char dest[], const char source[])

strcpy()接受两个参数,第一个参数是目的字符串数组,第二个参数是源字符串数组。复制字符串之前,必须要保证第一个参数的长度不小于第二个参数,否则虽然不会报错,但会溢出第一个字符串变量的边界,发生难以预料的结果。第二个参数的const说明符,表示这个函数不会修改第二个字符串。

#include <stdio.h>
#include <string.h>

int main(void) {
  char s[] = "Hello, world!";
  char t[100];

  strcpy(t, s);

  t[0] = 'z';
  printf("%s\n", s);  // "Hello, world!"
  printf("%s\n", t);  // "zello, world!"
}

上面示例将变量s的值,拷贝一份放到变量t,变成两个不同的字符串,修改一个不会影响到另一个。另外,变量t的长度大于s,复制后多余的位置(结束标志\0后面的位置)都为随机值。

strcpy()也可以用于字符数组的赋值。

char str[10];
strcpy(str, "abcd");

上面示例将字符数组变量,赋值为字符串“abcd”。

strcpy()的返回值是一个字符串指针(即char*),指向第一个参数。

char* s1 = "beast";
char s2[40] = "Be the best that you can be.";
char* ps;

ps = strcpy(s2 + 7, s1);

puts(s2); // Be the beast
puts(ps); // beast

上面示例中,从s2的第7个位置开始拷贝字符串beast,前面的位置不变。这导致s2后面的内容都被截去了,因为会连beast结尾的空字符一起拷贝。strcpy()返回的是一个指针,指向拷贝开始的位置。

strcpy()返回值的另一个用途,是连续为多个字符数组赋值。

strcpy(str1, strcpy(str2, "abcd"));

上面示例调用两次strcpy(),完成两个字符串变量的赋值。

另外,strcpy()的第一个参数最好是一个已经声明的数组,而不是声明后没有进行初始化的字符指针。

char* str;
strcpy(str, "hello world"); // 错误

上面的代码是有问题的。strcpy()将字符串分配给指针变量str,但是str并没有进行初始化,指向的是一个随机的位置,因此字符串可能被复制到任意地方。

strcpy()函数有安全风险,因为它并不检查目标字符串的长度,是否足够容纳源字符串的副本,可能导致写入溢出。如果不能保证不会发生溢出,建议使用strncpy()函数代替。

8.5 strncpy()

strncpy()strcpy()的用法完全一样,只是多了第3个参数,用来指定复制的最大字符数,防止溢出目标字符串变量的边界。

char* strncpy(
  char* dest, 
  char* src, 
  size_t n
);

上面原型中,第三个参数n定义了复制的最大字符数。如果达到最大字符数以后,源字符串仍然没有复制完,就会停止复制,这时目的字符串结尾将没有终止符\0,这一点务必注意。如果源字符串的字符数小于n,则strncpy()的行为与strcpy()完全一致。

strncpy(str1, str2, sizeof(str1) - 1);
str1[sizeof(str1) - 1] = '\0';

上面示例中,字符串str2复制给str1,但是复制长度最多为str1的长度减去1,str1剩下的最后一位用于写入字符串的结尾标志\0。这是因为strncpy()不会自己添加\0,如果复制的字符串片段不包含结尾标志,就需要手动添加。

strncpy()也可以用来拷贝部分字符串。

char s1[40];
char s2[12] = "hello world";

strncpy(s1, s2, 5);
s1[5] = '\0';

printf("%s\n", s1); // hello

上面示例中,指定只拷贝前5个字符。

8.6 strcat()

strcat()函数用于连接字符串。它接受两个字符串作为参数,把第二个字符串的副本添加到第一个字符串的末尾。这个函数会改变第一个字符串,但是第二个字符串不变。

该函数的原型定义在string.h头文件里面。

char* strcat(char* s1, const char* s2);

strcat()的返回值是一个字符串指针,指向第一个参数。

char s1[12] = "hello";
char s2[6] = "world";

strcat(s1, s2);
puts(s1); // "helloworld"

上面示例中,调用strcat()以后,可以看到字符串s1的值变了。

注意,strcat()的第一个参数的长度,必须足以容纳添加第二个参数字符串。否则,拼接后的字符串会溢出第一个字符串的边界,写入相邻的内存单元,这是很危险的,建议使用下面的strncat()代替。

8.7 strncat()

strncat()用于连接两个字符串,用法与strcat()完全一致,只是增加了第三个参数,指定最大添加的字符数。在添加过程中,一旦达到指定的字符数,或者在源字符串中遇到空字符\0,就不再添加了。它的原型定义在string.h头文件里面。

char* strncat(
  const char* dest,
  const char* src,
  size_t n
);

strncat()返回第一个参数,即目标字符串指针。

为了保证连接后的字符串,不超过目标字符串的长度,strncat()通常会写成下面这样。

strncat(
  str1, 
  str2, 
  sizeof(str1) - strlen(str1) - 1
);

strncat()总是会在拼接结果的结尾,自动添加空字符\0,所以第三个参数的最大值,应该是str1的变量长度减去str1的字符串长度,再减去1。下面是一个用法实例。

char s1[10] = "Monday";
char s2[8] = "Tuesday";

strncat(s1, s2, 3);
puts(s1); // "MondayTue"

上面示例中,s1的变量长度是10,字符长度是6,两者相减后再减去1,得到3,表明s1最多可以再添加三个字符,所以得到的结果是MondayTue

8.8 strcmp()

如果要比较两个字符串,无法直接比较,只能一个个字符进行比较,C 语言提供了strcmp()函数。

strcmp()函数用于比较两个字符串的内容。该函数的原型如下,定义在string.h头文件里面。

int strcmp(const char* s1, const char* s2);

按照字典顺序,如果两个字符串相同,返回值为0;如果s1小于s2strcmp()返回值小于0;如果s1大于s2,返回值大于0。

下面是一个用法示例。

// s1 = Happy New Year
// s2 = Happy New Year
// s3 = Happy Holidays

strcmp(s1, s2) // 0
strcmp(s1, s3) // 大于 0
strcmp(s3, s1) // 小于 0

注意,strcmp()只用来比较字符串,不用来比较字符。因为字符就是小整数,直接用相等运算符(==)就能比较。所以,不要把字符类型(char)的值,放入strcmp()当作参数。

8.9 strncmp()

由于strcmp()比较的是整个字符串,C 语言又提供了strncmp()函数,只比较到指定的位置。

该函数增加了第三个参数,指定了比较的字符数。它的原型定义在string.h头文件里面。

int strncmp(
  const char* s1,
  const char* s2, 
  size_t n
);

它的返回值与strcmp()一样。如果两个字符串相同,返回值为0;如果s1小于s2strcmp()返回值小于0;如果s1大于s2,返回值大于0。

下面是一个例子。

char s1[12] = "hello world";
char s2[12] = "hello C";

if (strncmp(s1, s2, 5) == 0) {
  printf("They all have hello.\n");
}

上面示例只比较两个字符串的前5个字符。

8.10 sprintf(),snprintf() 

sprintf()函数跟printf()类似,但是用于将数据写入字符串,而不是输出到显示器。该函数的原型定义在stdio.h头文件里面。

int sprintf(char* s, const char* format, ...);

sprintf()的第一个参数是字符串指针变量,其余参数和printf()相同,即第二个参数是格式字符串,后面的参数是待写入的变量列表。

char first[6] = "hello";
char last[6] = "world";
char s[40];

sprintf(s, "%s %s", first, last);

printf("%s\n", s); // hello world

上面示例中,sprintf()将输出内容组合成“hello world”,然后放入了变量s

sprintf()的返回值是写入变量的字符数量(不计入尾部的空字符\0)。如果遇到错误,返回负值。

sprintf()有严重的安全风险,如果写入的字符串过长,超过了目标字符串的长度,sprintf()依然会将其写入,导致发生溢出。为了控制写入的字符串的长度,C 语言又提供了另一个函数snprintf()

snprintf()只比sprintf()多了一个参数n,用来控制写入变量的字符串不超过n - 1个字符,剩下一个位置写入空字符\0。下面是它的原型。

int snprintf(char*s, size_t n, const char* format, ...);

snprintf()总是会自动写入字符串结尾的空字符。如果你尝试写入的字符数超过指定的最大字符数,snprintf()会写入 n - 1 个字符,留出最后一个位置写入空字符。

下面是一个例子。

snprintf(s, 12, "%s %s", "hello", "world");

上面的例子中,snprintf()的第二个参数是12,表示写入字符串的最大长度不超过12(包括尾部的空字符)。

snprintf()的返回值是写入格式字符串的字符数量(不计入尾部的空字符\0)。如果n足够大,返回值应该小于n,但是有时候格式字符串的长度可能大于n,那么这时返回值会大于n,但实际上真正写入变量的还是n-1个字符。如果遇到错误,返回一个负值。因此,返回值只有在非负并且小于n时,才能确认完整的格式字符串写入了变量。

8.11 字符串数组(什么字典

我们使用二维数组来吧字符串也给写入数组中:

char weekdays[7][10] = {
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
  "Sunday"
};

我们也可以这样写:

char* weekdays[] = {
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
  "Sunday"
};

这是一个指针数组,里面全是指向每个字符串第一个地址的指针。

9 struct 结构 结构体

9.1 定义结构体

C语言允许我们自己定义一些数据的类型,把各种数据类型赛在一起,叫做结构体,用.的方式来获取其内部的值,这样可以让代码可读性大大上升。

同时可以在传递函数的参数时如果有大量参数需要传递可以包在一起传递结构体:

一下是一个定义结构体的方法:

struct fraction {
  int numerator;
  int denominator;
};

这个结构体叫做fraction,包含两个属性numeratordenominator

注意,作为一个自定义的数据类型,它的类型名要包括struct关键字,比如上例是struct fraction,单独的fraction没有任何意义,甚至脚本还可以另外定义名为fraction的变量,虽然这样很容易造成混淆。另外,struct语句结尾的分号不能省略,否则很容易产生错误。

定义了新的数据类型以后,就可以声明该类型的变量,这与声明其他类型变量的写法是一样的。

struct fraction f1;

f1.numerator = 22;
f1.denominator = 7;

上面示例中,先声明了一个struct fraction类型的变量f1,这时编译器就会为f1分配内存,接着就可以为f1的不同属性赋值。可以看到,struct 结构的属性通过点(.)来表示,比如numerator属性要写成f1.numerator

struct 的数据类型声明语句与变量的声明语句,可以合并为一个语句。

struct book {
  char title[500];
  char author[100];
  float value;
} b1;

上面的语句同时声明了数据类型book和该类型的变量b1。如果类型标识符book只用在这一个地方,后面不再用到,这里可以将类型名省略。

struct {
  char title[500];
  char author[100];
  float value;
} b1;

上面示例中,struct声明了一个匿名数据类型,然后又声明了这个类型的变量b1

与其他变量声明语句一样,可以在声明变量的同时,对变量赋值。

struct {
  char title[500];
  char author[100];
  float value;
} b1 = {"Harry Potter", "J. K. Rowling", 10.0},
  b2 = {"Cancer Ward", "Aleksandr Solzhenitsyn", 7.85};

也可以不按顺序来赋值,但是要加上值得名字

struct {
  char title[500];
  char author[100];
  float value;
} b3 = {.title = "Harry Potter", .author = "J. K. Rowling", .value= 10.0};

9.2 结构体所占空间大小

struct 结构占用的存储空间,不是各个属性存储空间的总和,而是最大内存占用属性的存储空间的倍数,其他属性会添加空位与之对齐。这样可以提高读写效率。

struct foo {
  int a;
  char* b;
  char c;
};
printf("%d\n", sizeof(struct foo)); // 24

上面示例中,struct foo有三个属性,在64位计算机上占用的存储空间分别是:int a占4个字节,指针char* b占8个字节,char c占1个字节。它们加起来,一共是13个字节(4 + 8 + 1)。但是实际上,struct foo会占用24个字节,原因是它最大的内存占用属性是char* b的8个字节,导致其他属性的存储空间也是8个字节,这样才可以对齐,导致整个struct foo就是24个字节(8 * 3)。

多出来的存储空间,都采用空位填充,所以上面的struct foo真实的结构其实是下面这样。

struct foo {
  int a;        // 4
  char pad1[4]; // 填充4字节
  char *b;      // 8
  char c;       // 1
  char pad2[7]; // 填充7字节
};
printf("%d\n", sizeof(struct foo)); // 24

9.3 更多 写不完了

pass

10 Union 共用体结构(有一种面向对象的美

对于很多有一堆属性的物体,我们可以把它写在共用体里面:

C 语言提供了 Union 结构,用来自定义可以灵活变更的数据结构。它内部包含各种属性,但是所有属性共用一块内存,导致这些属性都是对同一个二进制数据的解读,其中往往只有一个属性的解读是有意义的。并且,后面写入的属性会覆盖前面的属性,这意味着同一块内存,可以先供某一个属性使用,然后再供另一个属性使用。这样做的最大好处是节省内存空间。

union quantity {
  short count;
  float weight;
  float volume;
};

要想访问:

// 写法一
union quantity q;
q.count = 4;

// 写法二
union quantity q = {.count=4};

// 写法三
union quantity q = {4};

参考:

https://wangdoc.com/clang/