C语言基础学习---基础法语

1. 编译器

gcc支持多种编程语言:C语言、C++、Objective-C、Java等。在后来,苹果公司自己开发了一款叫做clang的编译器

2. 编译流程

源文件 xxx.c 对它进行预处理,生成 xxx.i ,对与处理文件进行编译,生成汇编文件 xxx.s,对汇编文件进行编译生成目标文件 xxx.o,对目标文件进行链接生成可执行文件。

1
2
3
4
5
6
#生成 HelloC.o 目标文件
cc -c HelloC.c
#链接目标文件并将生成的可执行文件命名为 HelloC
cc -o HelloC HelloC.o
#编译并链接 Hello.c 源文件,并将最终生成的可执行文件命名为 HelloC。中间会生成 HelloC.o 目标文件,但它在链接完成后会被删除
cc -o HelloC HelloC.c

链接:源文件编译成功后,会生成一个.o目标文件,这就是一个二进制文件,但是,还是不能运行。目标文件不能运行的主要原因有2个:

1,在开发过程中,不可能将所有的代码都写在一个.c文件中,为了模块化开发,一般会将不同的功能写到不同的源文件中。源文件编译之后,每个源文件都有对应的.o文件,比如two.c生成了two.o、three.c生成了three.o,这些.o文件都不能单独运行,它们之间都有密不可分的关系,需要将所有相关联的.o目标文件组合在一起。

2,除开组合所有的目标文件之后,还需要将C语言的函数库包含进来,才能生成可执行文件。

将所有相关联的.o目标文件、以及C语言函数库组合在一起生成可执行文件的过程,我们称为“链接”。

整体流程图:

3. 关键词

1
2
3
4
5
6
7
auto double int struct break else long switch

case enum register typedef char extern return union

const float short unsigned continue for signed void

default goto sizeof volatile do if while static

4. 标识符

定义

  1. 标识符,从字面上理解就是用来标识某些东西的符号,标识的目的就是为了将这些东西区分开来。其实,标识符的作用就跟人类的名字差不多,为了区分每个人,就在每个人出生的时候起了个名字。
  2. C语言是由函数构成的,一个C程序中可能会有多个函数,为了区分这些函数,就给每一个函数都起了个名称。函数的名称就是标识符的一种。除了函数,变量的名称也是标识符。

标识符的命名规则

  1. 只能由26个英文字母的大小写、10个阿拉伯数字0~9、下划线_组成。
  2. 严格区分大小写,同一段英文字母的大写和小写是两个不同的标识符。
  3. 不能以数字开头。
  4. 不可以使用关键字作为标识符。

5. printf

5.1 基本使用

与 Java 当中的 sout 和 Android 中的 Log 不同,printf 用于输出变量时,需要制定输出的格式,比如是十进制还是八进制?

这种写法是错误的:

1
2
3
4
5
6
7
8
#include <stdio.h>

int main()
{
int a = 0x1D;
printf(a);
return 0;
}

这种写法是正确的

1
2
3
4
5
6
7
8
#include <stdio.h>

int main()
{
int a = 0x1D;
printf("变量a的值为%d", a);
return 0;
}

常用类型:

  1. d 以十进制形式输出带符号整数(正数不输出符号)
  2. o 以八进制形式输出无符号整数(不输出前缀0)
  3. x,X 以十六进制形式输出无符号整数(不输出前缀Ox)
  4. u 以十进制形式输出无符号整数
  5. f 以小数形式输出单、双精度实数
  6. e,E 以指数形式输出单、双精度实数
  7. g,G 以%f或%e中较短的输出宽度输出单、双精度实数
  8. c 输出单个字符
  9. s 输出字符串
  10. p 输出地址

5.2 精细控制

运算符优先级

6. 变量

占用的存储空间

一个变量所占用的存储空间,不仅跟变量类型有关,而且还跟编译器环境有关系。同一种类型的变量,在不同编译器环境下所占用的存储空间又是不一样的。我们都知道操作系统是有不同位数的,比如 Win7 有分 32 位、64 位,编译器也是一样的,也有不同位数:16 位、32 位、64 位(Mac 系统下的 clang 编译器是 64bit 的)。由于我们是 Mac 系统下开发,就以 64 位编译器为标准。

当定义一个变量时,系统就会为这个变量分配一定的存储空间。

1
2
3
4
5
6
7
8
int main()
{
char a = 'A';

int b = 10;

return 0;
}

1> 在 64bit 编译器环境下,系统为变量 a、b 分别分配 1 个字节、4 个字节的存储单元。也就是说:

  • 变量 b 中的 10 是用 4 个字节来存储的,4 个字节共 32 位,因此变量 b 在内存中的存储形式应该是 0000 0000 0000 0000 0000 0000 0000 1010。
  • 变量 a 中的’A’是用 1 个字节来存储的,1 个字节共 8 位,变量 a 在内存中的存储形式是 0100 0001,至于为什么’A’的二进制是这样呢,后面再讨论。

2> 上述变量 a、b 在内存中的存储情况大致如下表所示:

img

(注:”存储的内容” 那一列的一个小格子就代表一个字节,”地址” 那一列是指每个字节的地址)

  • 从图中可以看出,变量 b 占用了内存地址从 ffc1~ffc4 的 4 个字节,变量 a 占用了内存地址为 ffc5 的 1 个字节。每个字节都有自己的地址,其实变量也有地址。变量存储单元的第一个字节的地址就是该变量的地址。变量 a 的地址是 ffc5,变量 b 的地址是 ffc1。
  • 内存寻址是从大到小的,也就是说做什么事都会先从内存地址较大的字节开始,因此系统会优先分配地址值较大的字节给变量。由于是先定义变量 a、后定义变量 b,因此你会看到变量 a 的地址 ffc5 比变量 b 的地址 ffc1 大。
  • 注意看表格中变量 b 存储的内容,变量 b 的二进制形式是:0000 0000 0000 0000 0000 0000 0000 1010。由于内存寻址是从大到小的,所以是从内存地址最大的字节开始存储数据,存放顺序是 ffc4 -> ffc3 -> ffc2 -> ffc1,所以把前面的 0000 0000 都放在 ffc2~ffc4 中,最后面的八位 0000 1010 放在 ffc1 中。

负数占用的存储空间

其实任何数值在内存中都是以补码的形式存储的。

  • 正数的补码与原码相同。比如 9 的原码和补码都是 1001
  • 负数的补码等于它正数的原码取反后再 + 1。(取反的意思就是 0 变 1、1 变 0)

那么 - 10 的补码计算过程如下:

1> 先算出 10 的二进制形式:0000 0000 0000 0000 0000 0000 0000 1010

2> 对 10 的二进制进行取反:1111 1111 1111 1111 1111 1111 1111 0101

3> 对取反后的结果 + 1:1111 1111 1111 1111 1111 1111 1111 0110

因此,整数 - 10 在内存中的二进制形式是:1111 1111 1111 1111 1111 1111 1111 0110

img

默认值

在不同位置下定义的int类型变量,它的初始值是不同的

1
2
3
4
5
6
7
8
9
10
11
12
#include<stdio.h>


int i1;
static int i2;
int main(){
int i3;
static i4;
printf("i1,i2,i3,i4默认的初始值是: %d,%d,%d,%d\n",i1,i2,i3,i4);
return 0;
}
#i1,i2,i3,i4默认的初始值是: 0,0,405938230,0

全局int变量,初始值是0,局部int变量,非static初始值为0,static初始值为随机值

自动类型提升

char -> int -> float -> double

低向高

强制类型转换

long -> int ; float -> int ; int -> char

高向低

1
2
3
4
5
6
7
8
9
10
1 #include <stdio.h>
2
3 int main()
4 {
#C语言的语法限制不严格,这么写是可以的,java中需要显示强制类型转换
5 int i = 10.7;
6
7 printf("%d \n", i);
8 return 0;
9 }

7. 说明符

1> 我们已经知道,在 64bit 编译器环境下,1 个 int 类型变量取值范围是 - 231 ~ 231 - 1,最大值是 231-1。有时候,我们要使用的整数可能比 231-1 还大,比如 234 这个整数,如果还坚持用 int 类型变量来存储这个值的话,就会损失精度,得到的是垃圾数据。为了解决这个问题,C 语言允许我们给 int 类型的变量加一些说明符,某些说明符可以增大 int 类型变量的长度,这样的话,int 类型变量能存储的数据范围就变大了。

2> C 语言提供了以下 4 种说明符,4 个都属于关键字:

  • short 短型
  • long 长型
  • signed 有符号型
  • unsigned 无符号型

按照用途进行分类,short 和 long 是一类,signed 和 unsigned 是一类。

用法

这些说明符一般是用于修饰 int 类型的, 所以在使用时可以省略 int

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 1 // 下面两种写法是等价的
2 short int s1 = 1;
3 short s2 = 1;
4
5 // 下面两种写法是等价的
6 long int l1 = 2;
7 long l2 = 2;
8
9 // 可以连续使用2个long
10 long long ll = 10;
11
12 // 下面两种写法是等价的
13 signed int si1 = 3;
14 signed si2 = 3;
15
16 // 下面两种写法是等价的
17 unsigned int us1 = 4;
18 unsigned us2 = 4;
19
20 // 也可以同时使用2种修饰符
21 signed short int ss = 5;
22 unsigned long int ul = 5;

short 和 long

1>在 64bit 编译器环境下

int 占用 4 个字节(32bit),取值范围是 - 231~231-1;

short 占用 2 个字节(16bit),取值范围是 - 215~215-1;

long 占用 8 个字节(64bit),取值范围是 - 263~263-1

2>可以连续使用 2 个 long,也就是 long long。一般来说,long long 的范围是不小于 long 的,比如在 32bit 编译器环境下,long long 占用 8 个字节,long 占用 4 个字节。不过在 64bit 编译器环境下,long long 跟 long 是一样的,都占用 8 个字节。

3>short int 等价于 short,long int 等价于 long,long long int 等价于 long long

long 使用注意

long 和 int 都能够存储整型常量,为了区分 long 和 int,一般会在整型常量后面加个小写字母 l,比如 100l,表示 long 类型的常量。如果是 long long 类型呢,就加 2 个 l,比如 100ll。如果什么都不加,就是 int 类型的常量。因此,100 是 int 类型的常量,100l 是 long 类型的常量,100ll 是 long long 类型的常量。

使用 printf 输出时,long 用 %ld , long long 用 %lld

signed 和 unsigned

1> 首先要明确的:signed int 等价于 signed,unsigned int 等价于 unsigned

2> signed 和 unsigned 的区别就是它们的最高位是否要当做符号位,并不会像 short 和 long 那样改变数据的长度,即所占的字节数。

  • signed:表示有符号,也就是说最高位要当做符号位,所以包括正数、负数和 0。其实 int 的最高位本来就是符号位,已经包括了正负数和 0 了,因此 signed 和 int 是一样的,signed 等价于 signed int,也等价于 int。signed 的取值范围是 - 231 ~ 231 - 1
  • unsigned:表示无符号,也就是说最高位并不当做符号位,所以不包括负数。在 64bit 编译器环境下面,int 占用 4 个字节(32bit),因此 unsigned 的取值范围是:0000 0000 0000 0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111 1111 1111 1111,也就是 0 ~ 232 - 1

8. C 语言中的函数

  1. c 中没有「方法重写」 的概念,所以下面的函数定义是错误的
1
2
3
4
5
6
7
8
9
1 void test(int a)
2 {
3
4 }
5
6 int test()
7 {
8 return 10;
9 }
  1. 函数都应该独立定义,不能嵌套定义,所以下面的写法是错误的:
1
2
3
4
5
6
7
8
9
1 int main()
2 {
3 void test()
4 {
5
6 }
7
8 return 0;
9 }
  1. 在 C 语言中,函数的定义顺序是有讲究的:默认情况下,只有后面定义的函数才可以调用前面定义过的函数

  2. 如果想把函数的定义写在 main 函数后面,而且 main 函数能正常调用这些函数,那就必须在 main 函数的前面进行函数的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
 1 // 只是做个函数声明,并不用实现
2 int sum(int a, int b);
3
4 int main()
5 {
6 int c = sum(1, 4);
7 return 0;
8 }
9
10 // 函数的定义(实现)
11 int sum(int a, int b) {
12 return a + b;
13 }

9. #include

9.1 include 的作用

1
2
3
4
5
6
7
1 #include <stdio.h>
2
3 int main()
4 {
5 printf("Hello, World!\n");
6 return 0;
7 }
  • include 是 C 语言的预处理指令之一,所谓预处理,就是在编译之前做的处理,预处理指令一般以 # 开头

  • include 指令后面会跟着一个文件名,预处理器发现 #include 指令后,就会根据文件名去查找文件,并把这个文件的内容包含到当前文件中。被包含文件中的文本将替换源文件中的 #include 指令,就像你把被包含文件中的全部内容拷贝到这个 #include 指令所在的位置一样。所以第一行指令的作用是将 stdio.h 文件里面的所有内容拷贝到第一行中。

  • 如果被包含的文件拓展名为. h,我们称之为 “头文件”(Header File),头文件可以用来声明函数,要想使用这些函数,就必须先用 #include 指令包含函数所在的头文件
    -#include 指令不仅仅限于. h 头文件,可以包含任何编译器能识别的 C/C++ 代码文件,包括. c、.hpp、.cpp 等,甚至. txt、.abc 等等都可以

    也就是说你完全可以将第 3 行~ 第 7 行的代码放到其他文件中,然后用 #include 指令包含进来, 比如:

    1> 将第 3 行~ 第 7 行的代码放到 my.txt 中

    2> 在 main.c 源文件中包含 my.txt 文件

    编译链接后,程序还是可以照常运行的,因为 #include 的功能就是将文件内容完全拷贝到 #include 指令所在的位置

9.2 #include<> #include” “ 区别

1> 对于使用双引号 “” 来 include 文件,搜索的时候按以下顺序:

  • 先在这条 include 指令的父文件所在文件夹内搜索,所谓的父文件,就是这条 include 指令所在的文件
  • 如果上一步找不到,则在父文件的父文件所在文件夹内搜索;
  • 如果上一步找不到,则在编译器设置的 include 路径内搜索;
  • 如果上一步找不到,则在系统的 INCLUDE 环境变量内搜索

2> 对于使用尖括号 <> 来 include 文件,搜索的时候按以下顺序:

  • 在编译器设置的 include 路径内搜索;
  • 如果上一步找不到,则在系统的 INCLUDE 环境变量内搜索

9.3 stdio.h

  • stdio.h 是 C 语言函数库中的一个头文件,里面声明了一些常用的输入输出函数,比如往屏幕上输出内容的 printf 函数
  • 这里之所以包含 stdio.h 文件,是因为在第 5 行中用到了在 stdio.h 内部声明的 printf 函数,这个函数可以向屏幕输出数据,第 7 行代码输出的内容是:Hello, World!
  • 注意:stdio.h 里面只有 printf 函数的声明。前面已经提到:只要知道函数的声明,就可以调用这个函数,就能编译成功。不过想要这个程序能够运行成功,必须保证在链接的时候能找到函数的定义。其实链接除了会将所有的目标文件组合在一起,还会关联 C 语言的函数库,函数库中就有 printf 函数的定义。因此前面的程序是可以链接成功的。

10. 数组

10.1 一维数组

定义

  • 定义的形式为:类型 数组名 [元素个数]

  • 1
    int a[5];
  • 只能放在数组名的后面,下面的都是错误写法:

    1
    2
    int[5] a; // 错误
    int[] b; // 错误
  • 里面的个数必须是一个固定值,可以是常量 (比如 6、8)、常量表达式 (比如 3+4、5*7)。绝对不能使用变量或者变量表达式来表示元素个数,大多数情况下不要省略元素个数(当数组作为函数的形参和数组初始化时除外)

    下面的都是正确写法:

    1
    2
    3
    int  a[5];   // 整型常量
    int b['A']; // 字符常量,其实就是65
    int c[3*4]; // 整型常量表达式

    下面的都是错误写法:

    1
    2
    3
    int a[]; // 没有指定元素个数,错误
    int i = 9;
    int a[i]; // 用变量做元素个数,错误

一维数组的存储

定义数组时,系统将按照数组类型和个数分配一段连续的存储空间来存储数组元素,如 int a[3] 占据了连续的 6 字节存储空间(在 16 位编译器环境下,一个 int 类型占用 2 个字节)。要注意的是,数组名代表着整个数组的地址,也就是数组的起始地址。

img

注意:其实 a 不算是变量,是个常量,它代表着数组的地址。上图把 a 放到变量一栏是为了方便大家理解数组结构。

数组 a 的地址是 ffc1,a[0] 的地址是 ffc1,a[1] 的地址是 ffc3,a[2] 的地址是 ffc5。因此 a == &a[0],即第一个元素的地址就是整个数组的地址

一维数组初始化

  • 初始化的一般形式是:类型 数组名 [元素个数] = {元素 1, 元素 2, …};
1
int a[2] = {8, 10};

其实相当于:

1
2
3
int a[2];
a[0] = 8;
a[1] = 10;

注意的是:C 语言中编译器是不会对数组下标越界进行检查的,所以自己访问数组元素时要小心

  • 元素值列表可以是数组所有元素的初值,也可以是前面部分元素的初值
1
int a[4] = {2, 5};

当数组为整型时,初始化未确定初值的元素,默认为 0,所以上面的 a[2]、a[3] 都为 0

  • 当对全部数组元素都赋初值时,可以省略元素个数
1
int a[] = {2, 5, 7};

说明数组 a 的元素个数是 3

  • 数组初始化时的赋值方式只能用于数组的定义,定义之后只能一个元素一个元素地赋值

下面的写法是错误的:

1
2
3
1 int a[3];
2 a[3] = {1, 2, 3}; // 错误
3 a = {1, 2, 3}; // 错误

其实为什么是错误的写法呢?我们可以简要分析一下。

1> 第 2 行的 a[3] 代表着访问数组的第 4 个元素,首先这里已经是数组下标越界了;就算没有越界,给 a[3] 赋值时也应该赋一个 int 类型的整数,不应该是 {}。

2> 第 3 行的 a 是数组名,代表着数组的地址,它是个常量!给常量赋值,那肯定错了!

注意:函数参数为数组类型时,这时候是「引用传递」,也就是说,形参数组被修改时,实参数组也被修改了。

一维数组求长度

TODO//…………

10.2 二维数组

定义

定义形式:类型 数组名 [行数][列数]

1
int a[2][3]; // 共2行3列,6个元素

存储

  • C 语言把二维数组当作是一维数组的集合,即二维数组是一个特殊的一维数组:它的元素是一维数组。例如 int a[2][3] 可以看作由一维数组 a[0] 和一维数组 a[1] 组成,这两个一维数组都包含了 3 个 int 类型的元素

img

  • 二维数组的存放顺序是按行存放的,先存放第一行的元素,再存放第 2 行的元素。例如 int a[2][3] 的存放顺序是:a[0][0] → a[0][1] → a[0][2] → a[1][0] → a[1][1] → a[1][2]
  • 再来看看在内存中的存储情况,例如 int a[2][2]

img

(注意:a[0]、a[1] 也是数组,是一维数组,而且 a[0]、a[1] 就是数组名,因此 a[0]、a[1] 就代表着这个一维数组的地址)

1> 数组 a 的地址是 ffc1,数组 a[0] 的地址也是 ffc1,即 a = a[0];

2> 元素 a[0][0] 的地址是 ffc1,所以数组 a[0] 的地址和元素 a[0][0] 的地址相同,即 a[0] = &a[0][0];

3> 最终可以得出结论:a = a[0] = &a[0][0],以此类推,可以得出 a[1] = &a[1][0]

初始化

  • 按行进行初始化
1
int a[2][3] = { {2, 2, 3}, {3, 4, 5} };
  • 按存储顺序进行初始化 (先存放第 1 行,再存放第 2 行)
1
int a[2][3] = {2, 2, 3, 3, 4, 5};
  • 对部分元素进行初始化
1
2
int a[2][3] = { {2}, {3, 4} };
int b[3][3] = { { }, { , , 2}, {1, 2, 3}};
  • 如果只初始化了部分元素,可以省略行数,但是不可以省略列数
1
2
int a[][3] = {1, 2, 3, 4, 5, 6};
int a[][3] = {{1, 2, 3}, {3, 5}, {}};

有些人可能想不明白,为什么可以省略行数,但不可以省略列数。也有人可能会问,可不可以只指定行数,但是省略列数?

其实这个问题很简单,如果我们这样写:

1
int a[2][] = {1, 2, 3, 4, 5, 6}; // 错误写法

大家都知道,二维数组会先存放第 1 行的元素,由于不确定列数,也就是不确定第 1 行要存放多少个元素,所以这里会产生很多种情况,可能 1、2 是属于第 1 行的,也可能 1、2、3、4 是第一行的,甚至 1、2、3、4、5、6 全部都是属于第 1 行的

11. 字符串

11.1 字符串内存结构

Java 中使用 String 来代表数组, 但是 C 中没有这个对象, C 用「字符数组」来表示一个字符串。

  • 字符串可以看做是一个特殊的字符数组,为了跟普通的字符数组区分开来,应该在字符串的尾部添加了一个结束标志’\0’。’\0’是一个 ASCII 码值为 0 的字符,是一个空操作符,表示什么也不干。所以采用字符数组存放字符串,赋值时应包含结束标志’\0’。
  • 字符串 “mj” 的存储情况如下 (假设用字符数组 char a[] 来存储):

img

注意了,尾部有个’\0’,如果没有这个结束标记,说明这个字符数组存储的并不是字符串

11.2 字符串的初始化

1
2
3
4
5
6
7
8
9
10
11
12
 1 char a[3] = {'m', 'j', '\0'};
2
3 char b[3];
4 b[0] = 'm';
5 b[1] = 'j';
6 b[2] = '\0';
7
8 char c[3] = "mj";
9
10 char d[] = "mj";
11
12 char e[20] = "mj";

11.3 字符串输出

printf 输出

  • 用格式符 %s 表示需要输出一个字符串

    1
    2
    char a[3] = {'m', 'j', '\0'};
    printf("%s", a);

    输出结果:

    img

    最后面那个 \ 0 是不可能输出的,它只是个空字符,只是字符串结束的标记。

  • 如果把 ‘\0’ 去掉

    1
    2
    char a[3] = {'m', 'j'};
    printf("%s", a);

    输出结果:

    img

    跟上面添加了 \ 0 的输出结果是一样的。

    这样单独输出是看上去是一样的,但是在地址空间连续的情况下,输出就会出错。

  • 会出错的情况

1
2
3
4
5
6
7
8
9
1 char a[3] = {'m', 'j', '\0'}; // 添加了结束符\0
2
3 char b[] = {'i', 's'}; // 假设忘记添加结束符\0
4
5 printf("字符串a:%s", a); // 输出字符串a
6
7 printf("\n"); // 换行
8
9 printf("字符串b:%s", b); // 输出字符串b

第 3 行的字符数组 b 后面没有添加结束符 \ 0,因此 b 不算是个正宗的字符串。

按照你的猜想,字符串 b 的输出应该就是 “is”,但是输出结果为:

img

可以看出,当我们尝试输出 b 的时候,把 a 也输出了。

要搞清楚为什么,首先要看看 a 和 b 的内存地址:

1
2
3
printf("a的地址:%x", a);
printf("\n");
printf("b的地址:%x", b);

输出结果:

img

由这个数据我们可以分析出 a 和 b 的内存存储情况如下:

img

可以看出来,数组 b 和 a 的内存地址是连续的。我们再回到输出 b 的代码:

1
printf("字符串b:%s", b); // 输出字符串b

%s 表示期望输出一个字符串,因此 printf 函数会从 b 的首地址开始按顺序输出字符,一直到 \ 0 字符为止,因为 \ 0 是字符串的结束标记。

所以,如果想要创建一个字符串,记得加上结束符 \ 0,不然后果很严重,会访问到一些垃圾数据。

puts 输出

1
2
3
4
1 char a[] = "mj";
2 puts(a);
3
4 puts("lmj");

看第 2 行代码,puts 函数会从 a 的首地址开始输出字符,一直到 \ 0 字符为止。

输出结果:

img

  • puts 函数输出一个字符串后会自动换行。

  • puts 函数一次只能输出一个字符串,printf 函数则可以同时输出多个字符串

11.4 字符串输入

scanf函数

1
2
char a[10];
scanf("%s", a);
  • scanf 函数会从 a 的首地址开始存放用户输入的字符,存放完毕后,系统会自动在尾部加上一个结束标记 \ 0

  • 注意,不要写成 scanf(“%s”, &a),因为 a 已经代表了数组的地址,没必要再加上 & 这个地址运算符。

  • scanf 不能用来读取空格、tab

gets()函数

1
2
char a[10];
gets(a);

gets 跟 scanf 一样,会从 a 的首地址开始存放用户输入的字符,存放完毕后,系统会自动在尾部加上一个结束标记 \ 0。

  • gets 一次只能读取一个字符串,scanf 则可以同时读取多个字符串
  • gets 可以读入包含空格、tab 的字符串,直到遇到回车为止

11.3 字符串数组

  • 一维字符数组中存放一个字符串,比如一个名字 char name[20] = “mj”
  • 如果要存储多个字符串,比如一个班所有学生的名字,则需要二维字符数组,char names[15][20] 可以存放 15 个学生的姓名 (假设姓名不超过 20 字符)
  • 如果要存储两个班的学生姓名,那么可以用三维字符数组 char names[2][15][20]

12. 字符和字符串的常用处理函数

12.1 字符处理

声明在 stdio.h 头文件中

字符输出函数 putchar

字符输入函数 getchar

12.2 字符串处理

声明在 string.h 头文件中

strlen 函数

  • 这个函数可以用来测量字符串的字符个数,不包括 \ 0
1
2
3
4
5
6
7
1 int size = strlen("mj"); // 长度为2
2
3 char s1[] = "lmj";
4 int size1 = strlen(s1); // 长度为3
5
6 char s2[] = {'m', 'j', '\0', 'l', 'm', 'j', '\0'};
7 int size2 = strlen(s2); // 长度为2

看一下第 7 行,strlen 函数会从 s2 的首地址开始计算字符个数,直到遇到空字符 \ 0 为止。因为 s2 的第 1 个 \ 0 之前只有 mj 这 2 个字符,所以长度为 2。

strcpy函数

1
2
1 char s[10];
2 strcpy(s, "lmj");

strcpy 函数会将右边的 “lmj” 字符串拷贝到字符数组 s 中。从 s 的首地址开始,逐个字符拷贝,直到拷贝到 \ 0 为止。当然,在 s 的尾部肯定会保留一个 \ 0。

  • 假设右边的字符串中有好几个 \ 0,strcpy 函数只会拷贝第 1 个 \ 0 之前的内容,后面的内容不拷贝
1
2
3
1 char s[10];
2 char c[] = {'m', 'j', '\0', 'l', 'm', 'j', '\0'};
3 strcpy(s, c);

最后字符串 s 中的内容为:mj

strcat函数

1
2
char s1[30] = "LOVE";
strcat(s1, "OC");

strcat 函数会将右边的 “OC” 字符串拼接到 s1 的尾部,最后 s1 的内容就变成了 “LOVEOC”

img

strcat 函数会从 s1 的第 1 个 \ 0 字符开始连接字符串,s1 的第 1 个 \ 0 字符会被右边的字符串覆盖,连接完毕后在 s1 的尾部保留一个 \ 0

  • 注意下面的情况
1
2
3
1 char s1[30] = {'L', 'm', 'j', '\0', 'L', 'o', 'v', 'e', '\0'};
2 strcat(s1, "OC");
3 printf("%s", s1);

第 1 行初始化的 s1 有 2 个 \ 0,经过第 2 行的 strcat 函数后,输出结果:

img

img

strcmp函数

  • 这个函数可以用来比较 2 个字符串的大小
  • 调用形式为:strcmp(字符串 1, 字符串 2)
  • 两个字符串从左至右逐个字符比较(按照字符的 ASCII 码值的大小),直到字符不相同或者遇见’\0’为止。如果全部字符都相同,则返回值为 0。如果不相同,则返回两个字符串中第一个不相同的字符 ASCII 码值的差。即字符串 1 大于字符串 2 时函数返回值为正,否则为负。
1
2
3
4
5
6
1 char s1[] = "abc";
2 char s2[] = "abc";
3 char s3[] = "aBc";
4 char s4[] = "ccb";
5
6 printf("%d, %d, %d", strcmp(s1, s2), strcmp(s1, s3), strcmp(s1, s4));

输出结果:

img

-

共字
0%
.gt-container a{border-bottom: none;}