5 C 语言数组与字符串的全面解析

目录

[1 数组的概念与特性](#1 数组的概念与特性)

[1.1 什么是数组](#1.1 什么是数组)

[1.2 数组的特点](#1.2 数组的特点)

[1.3 数组的用途](#1.3 数组的用途)

[2 一维数组的定义与初始化](#2 一维数组的定义与初始化)

[2.1 一维数组的定义](#2.1 一维数组的定义)

[2.2 声明与定义的区别](#2.2 声明与定义的区别)

[2.3 一维数组的多种初始化](#2.3 一维数组的多种初始化)

[3 数组名的命名规则与作用](#3 数组名的命名规则与作用)

[3.1 数组名的命名规则](#3.1 数组名的命名规则)

[3.2 数组名的作用](#3.2 数组名的作用)

[4 一维数组在内存中的存储](#4 一维数组在内存中的存储)

[4.1 内存存储示例](#4.1 内存存储示例)

[4.2 CLion 调试数组所在内存](#4.2 CLion 调试数组所在内存)

[5 数组的访问越界](#5 数组的访问越界)

[5.1 数组越界的原因及影响](#5.1 数组越界的原因及影响)

[5.2 数据损坏示例及调试步骤](#5.2 数据损坏示例及调试步骤)

[5.3 循环访问越界示例](#5.3 循环访问越界示例)

[5.4 预防数组越界的方法](#5.4 预防数组越界的方法)

[6 数组的传递](#6 数组的传递)

[6.1 值传递](#6.1 值传递)

[6.2 进入子函数中调试](#6.2 进入子函数中调试)

[6.3 CLion 调试按钮说明](#6.3 CLion 调试按钮说明)

[6.4 数组长度的处理](#6.4 数组长度的处理)

[7 字符数组](#7 字符数组)

[7.1 字符数组的定义](#7.1 字符数组的定义)

[7.2 字符数组的初始化](#7.2 字符数组的初始化)

[7.3 无结束符输出乱码](#7.3 无结束符输出乱码)

[7.4 字符数组的修改与打印](#7.4 字符数组的修改与打印)

[8 字符串的输入输出](#8 字符串的输入输出)

[8.1 使用 printf 输出字符串](#8.1 使用 printf 输出字符串)

[8.2 使用 scanf 输入单个字符串](#8.2 使用 scanf 输入单个字符串)

[8.3 使用 scanf 输入多个字符串](#8.3 使用 scanf 输入多个字符串)

[8.4 使用 gets 函数读取一行文本](#8.4 使用 gets 函数读取一行文本)

[8.5 使用 fgets 函数代替 gets](#8.5 使用 fgets 函数代替 gets)

[8.6 使用 puts 输出字符串](#8.6 使用 puts 输出字符串)

[9 字符串操作函数](#9 字符串操作函数)

[9.1 strlen 函数](#9.1 strlen 函数)

[9.2 strcpy 函数](#9.2 strcpy 函数)

[9.3 strcmp 函数](#9.3 strcmp 函数)

[9.4 strcat 函数](#9.4 strcat 函数)

[10 本章判断题](#10 本章判断题)

[11 OJ 练习](#11 OJ 练习)

[11.1 课时5作业1](#11.1 课时5作业1)

[11.2 课时5作业2](#11.2 课时5作业2)


1 数组的概念与特性

1.1 什么是数组

在 C 语言中,数组是一种基本的数据结构,用于在计算机内存中连续存储相同类型的数据

数组通过一个单独的变量名(数组名)和一个索引(也称为下标)来访问其元素。数组的索引从 0 开始,即第一个元素的索引是 0,第二个元素的索引是 1,依此类推。

1.2 数组的特点

类型固定:数组中的所有元素必须具有相同的类型。

连续存储:数组中的元素在内存中连续存储,这意味着可以通过指针和简单的算术运算来访问或遍历数组中的元素。

快速访问:通过索引(下标)可以快速地访问数组中的任意元素,这种访问方式的时间复杂度为O(1)。

大小固定:数组的大小在声明时必须指定一旦数组被声明,其大小就不能改变。如果需要存储更多或更少的元素,可能需要使用其他数据结构(如链表)或重新声明一个大小合适的数组。

1.3 数组的用途

批量数据存储:数组非常适合用于存储一组相关的数据,如学生的成绩、员工的薪资等。

算法实现:许多算法的实现都依赖于数组,如排序算法(冒泡排序、快速排序等)、搜索算法(二分搜索等)。

作为其他数据结构的基础 :数组是许多更复杂数据结构(如栈、队列、链表、哈希表、图等)的基础。例如,栈和队列可以用数组来实现,尽管在实际应用中,链表可能更受欢迎因为它们允许动态地增加或减少元素的数量。

性能优化 :由于数组中的元素在内存中是连续存储的,因此遍历数组通常比遍历链表等其他数据结构要快。此外,某些算法(如快速排序中的分区操作)在数组上比在链表上更高效地实现。


2 一维数组的定义与初始化

2.1 一维数组的定义

在 C 语言中,一维数组的定义涉及指定数组的类型、数组名以及数组的大小(即包含的元素数量)。定义数组时,你需要告诉编译器数组将存储什么类型的数据(如整型 int、浮点型 float、字符型 char 等),以及数组中将有多少个这样的元素。

定义格式:

cpp 复制代码
类型说明符 数组名[常量表达式];
  • 类型说明符 :指定数组中元素的数据类型,如 int、float、char 等。
  • 数组名 :是用户定义的标识符,用于在程序中引用整个数组。
  • 常量表达式 :指定数组的大小,它必须是一个正整数常量常量表达式,表示数组中元素的数量。这个值在编译时确定,并且在数组的生命周期内保持不变。

示例:

cpp 复制代码
int scores[10]; // 定义一个整型数组scores,包含10个整型元素  
char letters[26]; // 定义一个字符型数组letters,包含26个字符型元素

2.2 声明与定义的区别

在 C 语言中,数组的"声明 "和"定义 "有时可以互换使用,但严格来说,声明只是告诉编译器某个标识符的存在和类型,而不分配内存空间;而定义则同时声明了标识符并为其分配了内存空间。

声明(Declaration):

声明只是告诉编译器某个标识符的存在和它的类型,但不分配内存。例如,声明一个数组的外部引用:

cpp 复制代码
extern int numbers[10];  // 不分配内存

这条语句告诉编译器 numbers 是一个整数数组,且数组大小为 10,但并不为其分配内存。

定义(Definition):

定义不仅声明了标识符,还为其分配了内存。数组的定义同时包含了它的声明。例如:

cpp 复制代码
int numbers[10];  // 分配内存

这条语句声明了 numbers 是一个整数数组,并分配了能够存储10个整数的内存。

使用场景:

仅声明:适用于在多文件程序中,当数组在一个文件中定义,而在其他文件中使用时。例如,在头文件中声明,在源文件中定义。

定义:适用于在当前文件中需要使用数组时,定义并分配内存。

2.3 一维数组的多种初始化

1、在定义数组时可以直接对数组元素赋初值

cpp 复制代码
int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

注意,不能写成下面这种形式:

cpp 复制代码
int a[10]; 
a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // a[10] 这种写法是错误的

解释: 首先,在定义数组 int a[10]; 时,只是声明了一个具有 10 个元素的整型数组 a ,但此时并没有对其进行初始化。其次,a[10] 这种写法是错误的。因为数组的下标是从 0 开始的,所以对于一个具有 10 个元素的数组,合法的下标范围是 0 到 9 ,使用 a[10] 会导致越界访问,这是不符合 C 语言的数组访问规则的。最后,C 语言要求在定义数组的同时进行初始化,如果要对数组进行初始化,应该在定义数组的时候完成,就像 int a[10]={0,1,2,3,4,5,6,7,8,9}; 这种形式。

如果数组已经在之前被定义,可以通过循环或逐个赋值的方式来初始化数组的元素,如下所示:

cpp 复制代码
int a[10];  
for (int i = 0; i < 10; i++) {  
    a[i] = i; // 将数组的每个元素初始化为其索引值  
}

或者使用单独的赋值语句(但通常不推荐这种方式,因为它冗长且容易出错):

cpp 复制代码
int a[10];  
a[0] = 0;  
a[1] = 1;  
a[2] = 2;  
// ... 以此类推  
a[9] = 9;

2、可以只给数组的一部分元素赋值

objectivec 复制代码
int a[10] = {0, 1, 2, 3, 4};

这里定义了含有 10 个元素的数组 a,但只提供了前 5 个元素的初值。未明确赋值的后 5 个元素将自动初始化为 0 。

3、若要使数组中全部元素的值为0,可以显式地为每个元素赋0

objectivec 复制代码
int a[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

或者更简洁地,只需为第一个元素赋 0,其他元素将自动初始化为 0,如下所示:

objectivec 复制代码
int a[10] = {0};

4、在初始化时,如果给出了数组的全部元素值,则可以省略数组的长度定义,编译器会根据初值列表中元素的数量自动确定数组的大小,考研初始时不建议使用!

objectivec 复制代码
int a[] = {1, 2, 3, 4, 5};

5、使用循环初始化数组

当数组的初始化需要基于某种模式或规则时,可以使用循环来初始化。

cpp 复制代码
int numbers[5];
for (int i = 0; i < 5; i++) {
    numbers[i] = i * 2; // 将数组初始化为 0, 2, 4, 6, 8
}

6、静态初始化

在定义全局或静态数组时,如果不显式初始化,数组元素将被自动初始化为 0。后续在讲解 static的用法,先了解。

cpp 复制代码
static int numbers[5]; // 所有元素自动初始化为 0

3 数组名的命名规则与作用

3.1 数组名的命名规则

命名规则 :数组名的命名应遵循与变量名相同的标识符命名规则 ,即由字母、数字和下划线组成,且不能以数字开头,同时避免使用C语言中的保留字。

指定元素个数 :在定义数组时,必须明确指定数组中元素的个数 ,这个个数通过方括号中的常量表达式来表示,即数组的长度。这个长度在编译时就必须确定,不能依赖于程序运行时的变量值。

常量表达式的限制 :常量表达式中可以包含常量符号常量,但不能包含变量。这意味着C语言不直接支持在定义数组时使用变量来动态指定数组大小。

以下是错误的声明示例(最新的 C 标准支持,但是考研初始复试最好不要这么写,学校的编译器通常都很老):

objectivec 复制代码
int n;
scanf("%d", &n); /* 在程序中临时输入数组的大小 */
int a[n];

以下这些错误在编写C语言程序时应避免:

objectivec 复制代码
float a[0];      /* 数组大小为 0 没有意义 */
int b(2)(3);     /* 不能使用圆括号 */
int k=3, a[k];   /* 不能用变量说明数组大小*/

3.2 数组名的作用

作为数组首元素的地址

在大多数上下文中,数组名被用作指向其第一个元素的指针。这意味着,当你尝试获取数组名的值时,你实际上得到的是数组中第一个元素的地址(即内存地址)。这种特性使得数组名在表达式中通常可以转换为指向其第一个元素的指针。

例如,在表达式 mark 和 &mark[0] 中,两者都表示数组 mark 的第一个元素的地址。

cpp 复制代码
#include <stdio.h>

int main() {
    int mark[5] = {90, 85, 92, 78, 88};

    // 使用数组名访问第一个元素
    printf("第一个元素的值是: %d\n", mark[0]);  // 90

    // 数组名作为数组首元素的地址
    printf("%p\n",mark); //000000000061FE00
    // 下面取得的是数组 mark 第一个元素的地址。
    printf("%p\n",&mark[0]);  //000000000061FE00

    // 演示数组名在表达式中会被转换为指向第一个元素的指针
    if (mark == &mark[0]) {
        printf("mark 和 &mark[0] 指向相同的地址。\n");
    }

    return 0;
}

用于数组元素的访问

数组名与下标一起使用来访问数组中的特定元素。这是通过计算从数组第一个元素开始的偏移量来实现的。例如,mark[5] 访问的是 mark 数组中索引为 5 的元素。

  • 数组名(如 mark)是指向数组第一个元素的指针。
  • 数组下标(如 [5])表示相对于数组起始地址的偏移量。
  • 实际访问时,会将数组名(数组起始地址)加上下标乘以元素的大小(通常是类型的大小,以字节为单位),从而确定具体元素的地址。
cpp 复制代码
#include <stdio.h>  
  
int main() {  
    int mark[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};  
  
    // 访问数组中的特定元素  
    printf("mark[5] 的值是: %d\n", mark[5]); // 输出 60  
  
    // 访问数组的第一个元素  
    printf("mark[0] 的值是: %d\n", mark[0]); // 输出 10  
  
    // 访问数组的最后一个元素(注意:索引是从0开始的,所以最后一个元素的索引是数组大小减1)  
    printf("mark[9] 的值是: %d\n", mark[9]); // 输出 100  
  
    // 注意:访问超出数组边界的元素(如 mark[10])是未定义行为,应该避免  
  
    return 0;  
}

在 sizeof 运算符中的特殊作用

尽管数组名在其他情况下被当作指向第一个元素的指针,但在 sizeof 运算符中,数组名表示整个数组的大小(以字节为单位)。这是 sizeof 运算符的一个特殊规则,允许开发者查询数组的总大小,而不仅仅是第一个元素的地址或大小。

cpp 复制代码
#include <stdio.h>

int main() {
    int mark[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 使用 sizeof 运算符获取整个数组的大小(以字节为单位)
    printf("数组 mark 的总大小是: %zu 字节\n", sizeof(mark)); // 40 byte

    // 假设 int 类型为 4 字节(这取决于编译器和平台),我们可以验证上面的结果
    printf("基于 int 类型的大小(假设为 4 字节),数组 mark 应该占用: %zu 字节\n", 10 * sizeof(int)); // 40 byte

    // 验证 sizeof(mark) 和 10 * sizeof(int) 是否相等
    if (sizeof(mark) == 10 * sizeof(int)) {
        printf("验证成功:数组 mark 的总大小与预期相符。\n");
    } else {
        printf("验证失败:数组 mark 的总大小与预期不符。\n");
    }

    // 注意:在 sizeof 运算符中,数组名不会被转换为指向第一个元素的指针
    // 因此,sizeof(mark) 返回的是整个数组的大小,而不是指针的大小

    return 0;
}

数组名的不可变性

在C语言中,数组名是不可修改的左值。这意味着你不能像修改指针那样修改数组名来指向另一个地址。尝试这样做(例如,mark = &anotherArray[0];)会导致编译错误。

cpp 复制代码
#include <stdio.h>

int main() {
    int mark[5] = {1, 2, 3, 4, 5};
    int anotherArray[5] = {10, 20, 30, 40, 50};

    // 尝试将 mark 修改为指向 anotherArray 的第一个元素
    // 下面这行代码会导致编译错误
    // mark = &anotherArray[0]; // 错误:赋值给只读的变量 'mark'  

    // 正确的做法是声明一个指针,并将它指向另一个数组的第一个元素
    int *ptr = &anotherArray[0]; // 后续知识点讲解

    // 现在可以通过 ptr 访问 anotherArray 的元素
    printf("通过 ptr 访问 anotherArray 的第一个元素: %d\n", *ptr); // 10
    

    // 提醒:虽然可以打印数组名作为地址,但不能修改它

    return 0;
}

作为函数参数

当数组名作为函数参数传递时,它不会传递整个数组(不是整个数组的拷贝),而是传递数组的首元素的地址。这样做可以提高效率,因为不需要在函数调用时复制整个数组。同时,这也意味着函数内部可以访问和修改数组的元素,但这些修改会影响到原始数组,因为它们是通过同一个内存地址进行的。

在函数声明中,通常不需要(也不允许)在数组名后指定数组的大小,因为函数接收的是指向第一个元素的指针,而不是整个数组。然而,在某些情况下,如果函数需要知道数组的大小来正确地处理数组(例如,遍历整个数组),则通常会通过额外的参数来传递数组的大小。

cpp 复制代码
#include <stdio.h>  
  
// 函数声明,不需要指定数组大小  
void printArray(int *arr, int size);  
  
int main() {  
    int myArray[5] = {1, 2, 3, 4, 5};  
  
    // 调用函数,传递数组名和数组大小  
    printArray(myArray, 5);  
  
    return 0;  
}  
  
// 函数定义,接收指向整型的指针和数组的大小  
void printArray(int *arr, int size) {  
    for (int i = 0; i < size; i++) {  
        printf("%d ", arr[i]); // 通过指针访问数组元素  
    }  
    printf("\n");  
}

在这个示例中,printArray 函数接收一个指向整型的指针 arr 和一个整数 size 作为参数。在 main 函数中,我们传递了 myArray 数组名和数组的大小给 printArray 函数。尽管在函数声明和定义中没有指定数组的大小,但我们通过额外的参数 size 来告诉函数需要处理多少个元素。函数内部通过指针 arr 来访问和打印数组的元素。

这里先了解函数即可,函数具体知识点在后续进行讲解。


4 一维数组在内存中的存储

4.1 内存存储示例

当声明一个如 int mark[100]; 的一维数组时,mark 数组在内存中连续占用空间。每个元素都是整型(int),占用 4 字节。数组的索引(或称下标)从 0 开始,因此访问数组 mark 中的元素方式是从 mark[0] 到 mark[99]。注意,没有 mark[100],因为数组索引是从 0 开始计数的。

假设 mark[100] 在内存中的起始地址为 0x1000,每个 int 占用 4 字节,则各个元素在内存中的地址如下:

cpp 复制代码
mark[0] 的地址为 0x1000
mark[1] 的地址为 0x1004
mark[2] 的地址为 0x1008
...
mark[99] 的地址为 0x10FC

以下是一个简单的示例代码,演示如何声明、初始化和访问一维数组中的元素,并打印每个元素的地址:

cpp 复制代码
#include <stdio.h>

int main() {
    int mark[100]; // 声明一个一维数组

    // 初始化数组元素
    for (int i = 0; i < 100; i++) {
        mark[i] = i * 2; // 每个元素赋值为索引的两倍
    }

    // 打印数组元素的值及其地址
    for (int i = 0; i < 100; i++) {
        // 通过将 &mark[i] 转换为 (void*)&mark[i],明确告诉编译器我们传递的是一个 void* 类型的指针,这样更安全和标准。
        printf("mark[%d] = %d, Address = %p\n", i, mark[i], (void*)&mark[i]);
    }

    return 0;
}

输出结果如下所示:

从输出结果可以看到,数组元素在内存中是连续存储的,每个元素的地址与它的索引成正比关系。这种连续存储的方式使得数组访问非常高效,因为可以通过简单的地址计算直接访问任何一个元素。

4.2 CLion 调试数组所在内存

上面的代码中,我们在数组初始化完成后打上断点,进行调试:

新建变量监视,输入想查看的变量等内容,回车即可:

查看数组所占用的内存:


5 数组的访问越界

数组越界(Array Out of Bounds)是在访问数组元素时使用了超出数组定义范围的索引。在C语言中,数组越界不会触发编译错误 ,而是在运行时产生未定义行为(undefined behavior)。这可能导致程序崩溃、数据损坏,甚至引发安全漏洞。C 和 C++ 语言在设计之初,并没有内置对数组访问越界的自动检测机制,这主要是出于性能、灵活性和兼容性的考虑。

5.1 数组越界的原因及影响

数组越界通常是由以下原因引起的:

  • 错误的循环条件:例如,循环条件错误导致访问数组时索引超出范围。
  • 计算错误:在索引计算过程中出现错误,导致访问越界。
  • 忘记数组边界:开发者没有正确地处理数组的边界条件。

数组越界访问会带来多种影响,包括但不限于:

  • 程序崩溃:访问非法内存地址可能导致程序崩溃(如段错误)。
  • 数据损坏:修改了不属于数组的内存,可能会损坏其他数据。
  • 安全漏洞:恶意用户可以利用数组越界漏洞进行攻击,如缓冲区溢出攻击。

5.2 数据损坏示例及调试步骤

cpp 复制代码
#include <stdio.h>

//数组越界
int main() {
    int a[5] = {1, 2, 3, 4, 5}; //定义数组时,数组长度必须固定
    int j = 20;
    int i = 10;

    a[5] = 6; //访问越界,未定义行为
    a[6] = 7; //访问越界,未定义行为,可能覆盖内存中的其他数据

    printf("i=%d\n", i); //i 我们并没有赋值,但是却发生了改变,为 7

    return 0;
}

下图显示了代码运行情况。如下图所示,在第 10 行左键打上断点,然后单击"小虫子"按钮,在内存视图依次输入&j、&a、&i 来查看整型变量 j、整型数组 a、整型变量 i 的地址,即可看到三个变量的地址,这里就像我们给衣柜的每个格子的编号,第一格、第二格......一直到柜子的最后一格。

操作系统对内存中的每个位置也给予一个编号,对于 Windows 32 位控制台应用程序来说,这个编号的范围是从 0x00 00 00 00 到 0xFF FF FF FF,总计为 2 的 32 次方,大小为4G。(考研中遇到的一般是32位的地址)这些编号称为地址(我们是 64 位程序,所以在 CLion 中地址显示的是 64 位,如 0x00 00 00 00 00 61 fe 00)。

通过内存视图我们可以看到,数组 a 再内存中的占用情况,同时也可以看到变量 i 和 j 再内存中的存储情况:

当我们继续执行 a[5] = 6; a[6] = 7; 后就修改了不属于数组的内存,损坏(覆盖)了原来 i 和 j 的所代表的内存地址中存放的数据,就导致最终输出 i 时,打印出了 7 而不是 10。

5.3 循环访问越界示例

以下是一个示例,演示数组越界的情况及其可能的影响:

cpp 复制代码
#include <stdio.h>

int main() {
    int numbers[5] = {1, 2, 3, 4, 5};

    // 正确访问数组元素
    for (int i = 0; i < 5; i++) {
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }

    // 错误:数组越界访问
    for (int i = 0; i <= 5; i++) { // 注意:这里循环条件错误,导致访问越界
        printf("numbers[%d] = %d\n", i, numbers[i]);  // numbers[5] 的值是未定义的
    }

    return 0;
}

5.4 预防数组越界的方法

检查数组边界:确保访问数组时的索引在合法范围内。

使用常量定义数组大小 :使用 #define 或 const 关键字定义数组大小,避免硬编码的数字。

编译器和工具:使用现代编译器和静态分析工具,可以检测潜在的数组越界问题。

标准库函数 :尽量使用安全的标准库函数,如 strncpy 而不是 strcpy,以避免缓冲区溢出。

cpp 复制代码
#include <stdio.h>

#define SIZE 5

int main() {
    int numbers[SIZE] = {1, 2, 3, 4, 5};

    // 正确访问数组元素
    for (int i = 0; i < SIZE; i++) {
        printf("numbers[%d] = %d\n", i, numbers[i]);
    }

    // 防止数组越界
    for (int i = 0; i < SIZE; i++) {
        if (i >= 0 && i < SIZE) {
            printf("Safe access: numbers[%d] = %d\n", i, numbers[i]);
        } else {
            printf("Index %d is out of bounds\n", i);
        }
    }

    return 0;
}

数组另一个值得关注的地方是, 编译器并不检查程序对数组下标的引用是否在数组的合法范围内 。 这种不加检查的行为有好处也有坏处,好处是不需要浪费时间对有些已知正确的数组下标进行检查,坏处是这样做将无法检测出无效的下标引用。
一个良好的经验法则是:如果下标值是通过那些已知正确的值计算得来的,那么就无须检查;如果下标值是由用户输入的数据产生的,那么在使用它们之前就必须进行检查,以确保它们位于有效范围内。


6 数组的传递

在 C 语言中,关于数组的传递到函数中的行为,首先要明确的是 C 语言本身并不直接支持"引用传递"(在C++等语言中,引用传递是一种显式支持的机制,允许函数直接修改传入参数的值)。然而,在C语言中,当数组作为参数传递给函数时,其行为在某些方面表现得像是"引用传递" ,但实际上是通过"值传递 "实现的,但这里的"值"并不是数组本身的值,而是数组首元素的地址

函数具体的知识点在后续章节讲解,这里只是简单使用下函数。

6.1 值传递

在 C 语言中,所有类型的参数(包括数组)都是通过值传递的。这意味着函数接收的是参数的一个副本。但是,对于数组来说,这个"副本"有点特殊:它实际上是数组首元素的地址(或者说是指向数组首元素的指针,8 byte)

当数组名作为参数传递给函数时,它会被隐式地转换为指向数组首元素的指针。因此,函数内部无法知道数组的确切大小(除非额外通过参数传递数组的大小)。函数内部通过这个指针来访问数组的元素,并且可以修改这些元素的值(因为是通过指针访问的)。

cpp 复制代码
#include <stdio.h>
#define N 5 // 定义宏N为5

// 子函数:将具体的功能封装起来,模块化,可以反复调用,简化多次书写相同的代码
// 函数声明,用于打印数组并修改其最后一个元素
// 注意:C语言的函数调用是值传递,但这里传递的是数组的首地址(即指向数组第一个元素的指针)
// 通常需要将大小作为另一个参数传递给函数
// 形参 b[] 里面不要写长度,长度是不能自动传递过来的
void print(int b[], int len) {
    for (int i = 0; i < len; i++) {
        printf("%3d", b[i]); // 使用%3d格式化输出,确保数字之间有足够的空格
    }
    b[4] = 20; // 在子函数中,通过传递的数组首地址,修改数组的第5个元素(索引为4)
    printf("\n"); // 打印换行符
}

// 主函数:main 函数只能有一个
int main() {
    int a[5] = {1, 2, 3, 4, 5};
    // 调用print函数,传入数组a和它的长度5
    // 尽管C语言通过值传递参数,但数组名在表达式中会被转换为指向数组首元素的指针
    // 因此,自定义的子函数 print 内部可以访问和修改数组a的内容
    print(a, 5);
    // 打印a[4]的值,以验证 print 函数是否成功修改了它
    printf("a[4]=%d\n", a[4]); // a[4]=20

    return 0;
}

6.2 进入子函数中调试

对上面这个程序进行调试,如下图 1 所示,在第 22 行 打断点,点击步入按钮:形状为向下箭头 可以进入子函数,即进入 print 函数,这时会发现数组 b 的大小变为 8 字节,如下图 2 所示,这是因为一维数组在传递时,其长度是传递不过去的,所以我们通过 len 来传递数组中的元素个数。实际数组名中存储的是数组的首地址,在调用函数传递时,是将数组的首地址给了变量 b(其实变量 b 是指针类型 ,所以为 8 byte,具体原理会在指针节讲解),在 b[] 的方括号中填写任何数字都是没有意义。这时我们在 print 函数内修改元素 b[4]=20,可以看到数组 b 的起始地址和 main 函数中数组 a 的起始地址相同,即二者在内存中位于同一位置,当函数执行结束时,数组 a 中的元素 a[4] 就得到了修改。

总结:

  • C语言通过值传递所有参数,包括数组。
  • 数组名作为参数传递时,实际上传递的是数组首元素的地址(或者说是指向数组首元素的指针)。
  • 这种机制允许函数访问和修改数组的元素,尽管它是通过值传递实现的。
  • 要注意数组的大小不是自动传递的,如果需要知道数组的大小,通常需要将大小作为另一个参数传递给函数。

6.3 CLion 调试按钮说明

步过 (Step Over) 按钮

按钮形状:折弯箭头

快捷键:F8

功能:执行当前行的代码,但不进入任何被调用的函数。它用于逐步执行代码,跳过函数调用的内部实现,直接到达下一行代码。适合在调试时希望继续执行但不关注某个函数细节时使用。

步入 (Step Into)按钮

按钮形状:向下箭头

快捷键:F7

功能:进入当前行调用的函数内部。如果当前行有函数调用,步入将带你进入那个函数的第一行代码。这对于需要调试函数内部实现时非常有用。

步出 (Step Out)按钮

按钮形状:向上箭头

快捷键:Shift + F8

功能:在当前函数内部执行完剩余的代码,并返回到调用该函数的代码行。适合当你已经进入一个函数但想快速返回到函数调用处时使用

6.4 数组长度的处理

计算整个数组的大小:sizeof(array)

计算单个元素的大小:sizeof(array[0])

计算数组的元素个数:用整个数组的大小除以单个元素的大小:sizeof(array) / sizeof(array[0])

这种方法只适用于在编译时已知大小的数组(即静态数组)。对于动态分配的数组(使用 malloc 或其他动态内存分配函数),不能使用 sizeof 来计算数组长度,因为 sizeof 在这种情况下只会返回指针的大小。

cpp 复制代码
#include <stdio.h>  
  
// 函数声明  
void printArray(int arr[], int size);  
  
int main() {  
    // 定义一个整型数组并初始化  
    int myArray[] = {1, 2, 3, 4, 5};  
    // 计算数组长度(注意:这种方法仅适用于数组作为局部变量时)  
    int size = sizeof(myArray) / sizeof(myArray[0]);  // 常用写法
    // 或者下面这句
    // int size = sizeof(myArray) / sizeof(int); 
  
    // 调用函数,传递数组和它的长度  
    printArray(myArray, size);  
  
    return 0;  
}  
  
// 函数定义  
void printArray(int arr[], int size) {  
    // 使用for循环遍历数组  
    for (int i = 0; i < size; i++) {  
        // 打印当前元素  
        printf("%d ", arr[i]);  
    }  
    // 打印换行符以美化输出  
    printf("\n");  
}

7 字符数组

7.1 字符数组的定义

C 语言中的字符数组是一种用于存储字符序列(即字符串)的数据结构。字符数组中的每个元素都是一个字符,通常使用 char 类型来表示。由于字符串在 C 语言中是以字符数组的形式存储的,并且以空字符('\0')作为结束标志,因此在使用字符数组存储字符串时需要特别注意这一点。

字符数组的定义与一维整型数组类似,但元素类型为 char。

例如:定义一个可以存储 10 个字符的数组,但实际存储的字符串长度最多为 9,因为最后一个位置用于存储字符串结束符 '\0' 。

cpp 复制代码
char str[10]; 

7.2 字符数组的初始化

1. 对每个字符单独赋值

可以逐个为字符数组的每个元素赋值,包括结束符 '\0'(如果需要的话)。

cpp 复制代码
char str[10];  
str[0] = 'H';  
str[1] = 'e';  
str[2] = 'l';  
str[3] = 'l';  
str[4] = 'o';  
str[5] = '\0'; // 明确添加字符串结束符  
// 注意:如果忘记添加'\0',则这个数组不是一个合法的C字符串

2. 使用花括号进行整体初始化

可以在定义数组的同时,使用花括号 {} 来初始化数组中的元素。

如果初始化时提供的字符少于数组的大小,剩余的元素会自动初始化为 '\0'(但这一点并不是C语言标准强制要求的,仅在某些情况下或某些编译器中有效)。如果明确提供了结束符'\0',则按照提供的值进行初始化。

cpp 复制代码
char str[10] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 明确包含结束符  
char str2[10] = "Hello"; // 字符串常量自动添加'\0',等价于 {'H', 'e', 'l', 'l', 'o', '\0'}

3. 使用字符串常量进行初始化

这是最常用的初始化方法,直接使用双引号括起来的字符串常量对字符数组进行初始化。C语言会自动在字符串的末尾添加一个 '\0' 作为结束符。

cpp 复制代码
char str[10] = "Hello"; // 自动添加'\0',因此可以存储的最大字符数为9

在 C 语言中,**当使用双引号括起来的字符串常量来初始化字符数组时,可以不需要显式地指定数组的长度。**编译器会自动计算字符串的长度(不包括结尾的空字符 '\0'),并在这个长度的基础上加1来确保数组有足够的空间来存储字符串及其结尾的空字符。

cpp 复制代码
char c[] = "hello";

这里,编译器会自动将数组 c 的大小设置为6('h', 'e', 'l', 'l', 'o' 这5个字符加上结尾的 '\0' 空字符)。你不需要(也不应该)在方括号中指定这个长度,因为编译器已经为你做了这件事。

注意事项:

  • 字符数组的大小应该足够大,以存储预期的字符串及其结束符'\0'。
  • 如果没有显式地初始化字符数组的最后一个元素为'\0',并且后续通过数组名访问字符串时,可能会导致未定义行为(如访问到数组边界之外的内存)。
  • 字符串常量是不可修改的,试图修改字符串常量中的字符会导致未定义行为。但是,可以通过字符数组来存储和修改字符串内容。

7.3 无结束符输出乱码

在 C 语言中,字符串的结尾需要一个特殊的字符 '\0'(空字符)来表示字符串的结束。这个空字符不占用字符串内容的可见部分,但在字符串数组中是必须的,以便函数如 printf 能正确识别字符串的结束位置。

错误的示例:

cpp 复制代码
#include <stdio.h>

// 输出字符串乱码时,要去查看字符数组中是否存储了结束符'\0'
int main() {
    // 数组大小为5,没有为'\0'预留空间
    char c[5] = {'h','e','l','l','o'}; // 或 char c[5] = "hello";


    // 尝试打印字符串,但因为没有'\0',printf 会继续读取直到遇到'\0'或访问违规
    printf("%s\n", c); // 输出可能不是"hello",而是乱码或程序崩溃

    return 0;
}

在这个例子中,printf 试图打印一个字符串,但由于数组 c 没有以 '\0' 结尾,printf 可能会继续读取内存直到找到一个 '\0' 或者触发访问违规(segmentation fault)。实际输出可能因系统而异,但通常是不可预测的。输出结果如下所示:

调试内存结果如下所示:

正确的示例:

cpp 复制代码
#include <stdio.h>

// 输出字符串乱码时,要去查看字符数组中是否存储了结束符'\0'
int main() {
    // 数组大小为 6,为'\0'预留了空间
    // char c[6] = {'h','e','l','l','o','\0'};  // 这种写法太慢
     char c[6] = "hello";   // 常用这种
    
    // 简化写法
    // char c[] = "hello";   // 初始化一个字符数组并希望它是一个字符串时,可以省略显式地写入 '\0


    // 现在可以安全地打印字符串
    printf("%s\n", c); // 输出 "hello"

    return 0;

}

在这个例子中,c 数组被正确地初始化为包含 'h', 'e', 'l', 'l', 'o' 和 '\0'。这样,当 printf 尝试打印这个字符串时,它会在读取到 '\0' 时停止,确保输出是正确的 "hello"。

调试内存结果如下所示:

简化写法:

在C语言中,当你初始化一个字符数组并希望它是一个字符串时,你可以省略显式地写入 '\0',编译器会自动为你做这件事,这种方式既简洁又安全,避免了忘记添加 '\0' 导致的潜在问题。

cpp 复制代码
char c[] = "hello"; // 自动包含'\0'

7.4 字符数组的修改与打印

cpp 复制代码
#include <stdio.h>

void print(char d[])
{
    d[0] = 'H'; // 修改数组的第一个内容

    int i = 0;
    // 在ASCII码表中,空字符('\0')的数值是0
    while (d[i]) {
        printf("%c", d[i]); // 一个字符一个字符的输出 How
        i++;
    }
    printf("\n");  // 换行

}

int main()
{
    char d[5] = "how"; // 编译器自动添加'\0'

    // 原始输出
    printf("Original d: %s\n", d);  // Original d: how

    // 修改后的输出
    print(d); // d的第一个字符将被修改为'H'

    return 0;
}

在 ASCII 码表中,空字符('\0')的数值是0,而在 C 语言中,任何数值为 0 的表达式都被视为逻辑假(false)。因此,当 d[i] 指向字符串的末尾,即 d[i] 是 '\0' 时,d[i] 的值就是 0,这时 while 循环的条件就不满足了,循环就会终止。


8 字符串的输入输出

8.1 使用 printf 输出字符串

printf 函数使用 %s 格式说明符来输出一个字符串。当你有一个以空字符('\0')结尾的字符数组(即C语言中的字符串)时,可以这样做:

cpp 复制代码
#include <stdio.h>  
  
int main() {  
    char str[] = "Hello, World!";  
    printf("%s\n", str); // 输出: Hello, World!  
    return 0;  
}

在这个例子中,printf 会从 str 指向的地址开始,一直打印字符,直到遇到空字符 '\0' 为止。

8.2 使用 scanf 输入单个字符串

scanf 函数使用 %s 格式说明符来从标准输入读取一个字符串,并存储到指定的字符数组中。但是,这里有几个重要的注意事项:

缓冲区溢出:scanf 使用 %s 读取字符串时,不会自动检查目标数组的大小,这可能导致缓冲区溢出。如果输入的字符串比目标数组大,多余的字符将被写入数组边界之外的内存区域,这可能导致未定义行为,如数据损坏或程序崩溃。应该指定一个最大宽度来限制读取的字符数,以防止缓冲区溢出。

空白字符 :scanf 使用 %s 读取字符串时,会在遇到第一个空白字符(空格、制表符、换行符等)时停止读取。这意味着它不能用来读取包含空格的字符串。

cpp 复制代码
#include <stdio.h>  
  
int main() {  
    char str[50]; // 假设我们知道输入不会超过49个字符(留一个位置给'\0')  
    printf("Enter a string: ");  
    // 使用宽度限制来防止缓冲区溢出  
    scanf("%49s", str); // 注意:这不会读取空格之后的字符  
  
    printf("You entered: %s\n", str);  
    return 0;  
}

在 scanf("%49s", str); 这行代码中,49是一个宽度指定符(width specifier),它告诉scanf函数在读取字符串时最多可以读取多少个字符(不包括结尾的空字符'\0')。

具体来说,%49s 意味着 scanf 会从标准输入中读取最多49个字符来填充 str 数组。一旦读取了49个字符,或者遇到了空白字符(如空格、制表符或换行符),或者到达了输入的末尾,scanf 就会停止读取,并在读取的字符串末尾自动添加一个空字符 '\0' 来标记字符串的结束。

这个宽度指定符非常重要,因为它可以防止缓冲区溢出。如果你知道你的字符串不会超过某个长度,你应该总是使用这个宽度指定符来限制 scanf 读取的字符数。

8.3 使用 scanf 输入多个字符串

在 C 语言中,scanf 函数可以用来读取多个字符串,但你需要为每个字符串提供一个对应的 %s 格式说明符,并且每个字符串都需要一个对应的变量来存储。此外,由于 scanf 在读取字符串时会以空格(包括空格、制表符、换行符等)作为字符串之间的分隔符,因此你不能直接使用 scanf 来读取包含空格的单个字符串。但是,对于多个由空格分隔的字符串,scanf 是可以胜任的。

cpp 复制代码
#include <stdio.h>  
  
int main() {  
    char str1[50];  
    char str2[50];  
    char str3[50];  
  
    printf("Enter three strings separated by spaces: ");  
    // 注意:这里假设用户输入的字符串之间只有一个空格,并且不超过数组大小  
    scanf("%49s %49s %49s", str1, str2, str3);  
  
    // 打印读取的字符串  
    printf("You entered:\n");  
    printf("%s\n", str1);  
    printf("%s\n", str2);  
    printf("%s\n", str3);  
  
    return 0;  
}

8.4 使用 gets 函数读取一行文本

gets 函数类似于 scanf 函数,用于从标准输入 (stdin,通常是键盘)读取一行文本。前面我们已经知道 scanf 函数在读取字符串时遇到空格就认为读取结束,所以当输入的字符串存在空格时,我们需要使用 gets 函数进行读取。

cpp 复制代码
char *gets(char *str);

gets 函数从标准输入(stdin)读取一行文本(直到遇到换行符 '\n'),它会在读取到换行符之后立即停止读取,并将换行符从输入缓冲区中丢弃(即不会将其存储到数组中),然后在数组的末尾添加一个空字符('\0')作为字符串的终止符。

cpp 复制代码
#include <stdio.h>

int main() {
    char str[100];  // 定义一个足够大的字符数组来存储输入的字符串

    printf("请输入一行字符串: ");
    fflush(stdout);

    gets(str);  // 从标准输入读取字符串并存储到 str 中

    printf("您输入的字符串是: %s\n", str);

    return 0;
}

效果展示:

8.5 使用 fgets 函数代替 gets

gets 函数在使用时存在一些安全风险。因为它不会检查输入字符串的长度是否超过了目标数组的大小,可能导致缓冲区溢出的问题。在 C11 标准中,gets 函数已被弃用,建议使用 fgets 函数来替代,它可以指定读取的最大字符数,从而避免缓冲区溢出的风险。

C 语言中的 fgets 函数是一个用于从指定的流中读取一行数据的标准库函数。

cpp 复制代码
char *fgets(char *str, int n, FILE *stream);
  • str :一个指向字符数组的指针,fgets 会将读取的字符串保存在这个数组中。
  • n :表示要读取的最大字符数(包括最后的空字符 '\0')。通常可以直接将数组的大小作为这个参数的值,fgets 函数会自动处理字符的读取和存储,以确保不会越界并在字符串末尾添加空字符 '\0'。
  • stream :一个指向FILE对象的指针,表示要从中读取的流。例如,stdin表示标准输入流(通常是键盘),或者可以是任何打开的文件指针。

功能描述

fgets 函数从指定的 stream 中读取字符,直到遇到下列三种情况之一:

  • 读取到了换行符 \n。
  • 读取了n-1个字符。
  • 遇到了文件结束符 EOF。

如果读取成功,则 fgets 会将读取到的字符(包括换行符,如果有的话)存储到 str 指向的字符数组中,并在末尾自动加上一个终止符\0。然后,它返回一个指向str的指针。如果读取失败或到达文件末尾,则返回NULL。

注意事项

换行符的处理 :如果输入的字符串长度没有超过 sizeof(str) - 1 ,那么系统会将最后输入的换行符 '\n' 保存进来,保存的位置是紧跟输入的字符,然后剩余的空间用 '\0' 填充。此时输出该字符串时,printf 中不需要加换行符 '\n',因为字符串中已经有了换行符。如果输入的字符串长度超过了 sizeof(str) - 1,那么 fgets 只会读取 sizeof(str) - 1 个字符,并在末尾添加 '\0',多余的字符会留在输入缓冲区中。

字符串长度 :由于 fgets 会在末尾自动添加终止符 \0,所以实际能够存储的字符数(不包括终止符)是 n-1。

安全性:相比于 gets 函数,fgets 函数更加安全,因为它允许指定读取的最大字符数,从而避免了缓冲区溢出的风险。

读取失败的情况:如果读取失败(例如,由于文件不存在或无法读取),fgets 将返回NULL。因此,在使用 fgets 时,应该检查其返回值以确保读取成功。

输入缓存:当从标准输入(如键盘)读取时,fgets 会等待用户输入并按下回车键。用户输入的内容(包括换行符)会被存储在输入缓存区中,fgets会从中读取数据。如果输入的数据超过了指定的最大字符数(n-1),则多余的数据会留在输入缓存区中,可能会影响后续的输入操作。

cpp 复制代码
#include <stdio.h>

int main() {
    char str[100]; // 定义一个足够大的字符数组来存储输入的字符串

    printf("数组的长度:sizeof(str) = %zu\n", sizeof(str));
    printf("请输入一行文本(最多99个字符):");
    fflush(stdout);

    // 使用fgets从标准输入读取一行文本,最多读取99个字符(为'\0'留出空间)
    if (fgets(str, sizeof(str), stdin) != NULL) {
        // 直接输出读取的字符串,包括可能的换行符
        printf("你输入的文本是:%s", str);
    } else {
        // 如果fgets返回NULL,通常意味着发生了错误(如EOF)
        printf("读取文本时发生错误。\n");
    }

    return 0;
}

效果展示:

可以通过循环遍历的方法,去掉输入数据中可能包含的换行符:

cpp 复制代码
#include <stdio.h>

int main() {
    char str[100]; // 定义一个足够大的字符数组来存储输入的字符串
    int i = 0;

    // 提示用户输入一行文本
    printf("请输入一行文本(最多99个字符):");

    // 使用fgets从标准输入读取一行文本,最多读取99个字符(为'\0'留出空间)
    if (fgets(str, sizeof(str), stdin) != NULL) {
        // 遍历字符串,查找换行符'\n'
        // 注意:fgets会将换行符(如果有的话)也读取到str中,并在其后添加'\0'
        for (i = 0; str[i] != '\0'; i++) {
            // 如果找到了换行符,并且它不是字符串的最后一个字符(即'\0')
            // 则将其替换为字符串的终止符'\0',从而移除换行符
            if (str[i] == '\n' && str[i+1] == '\0') {
                str[i] = '\0';
                break; // 退出循环,因为我们已经找到了并处理了换行符
            }
        }

        printf("%s", str);
        // puts(str);  这个会自动换行

    } else {
        // 如果fgets返回NULL,通常意味着发生了错误(如EOF)
        printf("读取文本时发生错误。\n");
    }

    return 0;
}

8.6 使用 puts 输出字符串

C 语言中的 puts 函数是一个用于向标准输出设备(通常是屏幕)输出字符串的标准库函数。

cpp 复制代码
int puts(const char *str);

功能描述

puts 函数接受一个指向以 null(\0)结尾的字符串的指针作为参数,并将该字符串输出到标准输出设备(stdout) 。在输出字符串的末尾,puts 会自动添加一个换行符 \n( 等价于printf("%s\n",c),因此输出的字符串之后会立即换行。

返回值

如果成功执行,puts 函数返回非负值(通常是0,但 C 标准只保证非负值,并不强制为 0)。如果发生错误(如输出失败),则返回 EOF(通常是-1)。然而,需要注意的是,在大多数实现中,由于 puts 只是将字符串输出到标准输出,并不检查输出是否成功(除非底层 I/O 操作失败),因此它几乎总是返回 0。

使用注意事项

只能输出字符串 :puts 函数只能输出字符串,不能输出数值或进行格式转换。如果需要输出数值或进行格式化的字符串输出,应使用 printf 函数。

自动换行 :puts 在输出字符串后会自动添加一个换行符(等价于 printf("%s\n",c)),因此在连续使用 puts 输出多个字符串时,每个字符串之间都会有一个空行。如果需要控制换行,应使用 printf 或其他输出函数。

不包含结尾的空字符:尽管 puts 的参数是指向以 \0 结尾的字符串的指针,但 puts 在输出时不会包含这个结尾的空字符。

头文件:puts 函数定义在 <stdio.h> 头文件中,因此在使用 puts 之前需要包含这个头文件。

cpp 复制代码
#include <stdio.h>

int main() {
    char str1[100];
    char str2[100];

    printf("请使用 gets 输入一行字符串: ");
    fflush(stdout);
    gets(str1);  // 不推荐使用,存在缓冲区溢出风险

    printf("请使用 fgets 输入一行字符串: ");
    fflush(stdout);
    fgets(str2, sizeof(str2), stdin);

    puts("使用 gets 输入的字符串:");
    puts(str1);

    puts("使用 fgets 输入的字符串:");
    puts(str2);

    return 0;
}

输出结果如下所示:


9 字符串操作函数

在C语言中,str 系列字符串操作函数是处理字符串时常用的工具,它们定义在 <string.h> 头文件中。

9.1 strlen 函数

功能 :计算字符串的长度(不包括终止符 '\0')。

原型:size_t strlen(const char *str);

头文件:<string.h>

返回值:返回字符串 str 的长度,类型为 size_t(通常是unsigned int)。

注意 :strlen 通过遍历字符串直到遇到第一个 '\0' 来计算长度,因此字符串必须是以 '\0' 结尾的

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "Hello, World!";
    // 计算字符串的长度(不包括终止符 '\0')
    printf("The length of '%s' is %zu.\n", str, strlen(str)); // 13
    
    return 0;
}

9.2 strcpy 函数

功能 :将源字符串(src,右边的)复制 到目标字符串(dest,左边的)中,会覆盖目标字符串(dest)数组中原有的内容包括终止符'\0'

原型:char *strcpy(char *dest, const char *src);

头文件:<string.h>

返回值:返回目标字符串 dest 的指针。

注意

目标字符串 dest 必须有足够的空间来容纳源字符串 src,包括终止符 '\0',否则会发生缓冲区溢出。

源字符串 src 可以是一个常量字符串,但目标字符串 dest 必须是一个字符数组,因为需要修改其内容。

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char dest[50];
    const char src[] = "Hello, World!";

    strcpy(dest, src);
    
    printf("Copied string: %s\n", dest);
    return 0;
}

9.3 strcmp 函数

功能比较两个字符串(比较 ASCII 值)。

原型:int strcmp(const char *str1, const char *str2);

头文件:<string.h>

返回值

如果 str1 和 str2 字符串相等,则返回0。

如果 str1 在字典序上小于 str2,则返回负数。

如果 str1 在字典序上大于 str2,则返回正数。

注意 :strcmp 按字典序逐个字符比较,直到遇到不同的字符或遇到字符串的终止符 '\0'。

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char str1[] = "apple";
    char str2[] = "banana";
    char str3[] = "apple";

    printf("Comparing '%s' and '%s': %d\n", str1, str2, strcmp(str1, str2)); // 返回负数,-1
    printf("Comparing '%s' and '%s': %d\n", str2, str1, strcmp(str2, str1)); // 返回整数,1
    printf("Comparing '%s' and '%s': %d\n", str1, str3, strcmp(str1, str3)); // 返回0
    return 0;
}

9.4 strcat 函数

功能 :将源字符串(src,右边的)拼接到目标字符串(dest,左边的)的末尾,并包括终止符 '\0'。

原理:首先找到目标字符串的终止符 '\0' ,然后从这个位置开始将源字符串的字符逐个复制到目标字符串中,直到源字符串的终止符 '\0' 。此时,拼接后的字符串以新的终止符 '\0' 结束,原来目标字符串的终止符被覆盖了。

原型:char *strcat(char *dest, const char *src);

头文件:<string.h>

返回值:返回目标字符串 dest 的指针。

注意

目标字符串 dest 必须有足够的空间来容纳两个字符串拼接后的结果,包括终止符 '\0',否则会发生缓冲区溢出。

dest 和 src 都必须是以 '\0' 结尾的字符串。

源字符串 src 可以是一个常量字符串,但目标字符串 dest 必须是一个字符数组,因为需要修改其内容。

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char dest[50] = "Hello, ";
    const char src[] = "World!";

    strcat(dest, src);
    
    printf("Concatenated string: %s\n", dest);
    return 0;
}

这些字符串操作函数初试用的较少,一般是在复试机考时用得到~

还有更多的字符串操作函数 strncpy、strncmp、strchr、strrchr等可以查看函数库。


10 本章判断题

1、数组内两个元素可以存储不同的数据类型 ?

A 正确 B 错误

答案:B

解释:数组内的元素必须存储相同的数据类型。


2、int mark[100];我们可以做 mark[100]=3;?

A 正确 B 错误

答案:B

解释:数组下标从零开始,定义 int mark[100],只能访问 mark[0] 到 mark[99]


3、int a[10]={0,1,2,3,4}; 定义 a 数组有 10 个元素,但花括号内只提供 5 个初值,这表示只给

前 5 个元素赋初值,后 5 个元素的值为 0?

A 正确 B 错误

答案:A

解释:初始化元素时,剩余的未赋值元素,会被初始化为零。


4、数组 int arr[5]; 我们做 arr[5]=20 这个操作造成了访问越界?

A 正确 B 错误

答案:A

解释: int arr[5] 数组访问 arr[0] 到 arr[4],5及以后的值都是访问越界。


5、访问越界是非常危险的,因为 C 和 C++ 语言没有针对访问越界进行检测?

A 正确 B 错误

答案:A

解释:我们的实例演示了访问越界会改变其他变量的值,因此非常危险。


6、数组传递时,可以把自身长度传递给子函数?

A 正确 B 错误

答案:B

解释:数组传递时,只是把数组的起始地址传递给了子函数,长度不能传递给子函数,我们需要使用额外的变量,来把长度传递给子函数。


7、在子函数中改变数组中某个元素的值,子函数结束后,数组内元素值发生变化?

A 正确 B 错误

答案:A

解释:当我们把数组名传递给子函数后,在子函数内就可以访问数组的某个元素,同时可以进行修改。


8、char c[10]={'T','a','m' ,'h','a','p's'p'.'y'} 这种初始化方式是常用的方式?

A 正确 B 错误

答案:B

解释:字符数组的初始化最常用的方式是 char c[10]="lamhappy" 这种方式。


9、char c[5] 我们可以放5个字符来使用?

A 正确 B 错误

答案:B

解释:char c[5] 我们只能放4个字符,第五个字符需要放置结束符 '\0',否则 printf("%s\n",c); 输出会造成乱码。


10、 char c[10]; scanf("%s",c); 读取字符串时会读取到空格和 \n ?

A 正确 B 错误

答案:B

解释:scanf("%s",c); 会忽略空格和 \n,因此无法读取到空格和 \n。


11、gets 一次可以读取一行,能够读取空格?

A 正确 B 错误

答案:A

解释:如果要一次读一行,同时需要把空格读到字符数组中,那么就需要用gets。


12、puts(c) 等价于 printf("%s\n",c)?

A 正确 B 错误

答案:A

解释:如果输出字符串,使用 puts 更加方便,注意 puts 只能用于输出字符串,不能输出其他类型。


13、strlen 函数用于统计字符串长度,strcpy 函数用于将某个字符串复制到字符数组中,

strcmp 函数用于比较两个字符串的大小, strcat 函数用于将两个字符串连接到一起?

A 正确 B 错误

答案:A

解释:这个需要记住。


14、如果字符串没有结束符 '\0',也可以使用 strlen 正确统计长度?

A 正确 B 错误

答案:B

解释: strlen是通过结束符 '\0'来判断字符串长度的,如果没有结束符,无法统计字符串长度。


11 OJ 练习

11.1 课时5作业1

这道题主要是练习数组的操作,通过对数组进行遍历,即可依次把元素读取到数组中,再次进行遍历就可以统计数字2的出现次数(也可以遍历一次)。

cpp 复制代码
#include <stdio.h>  
  
int main() {  
    // 声明变量n用于存储用户将要输入的数组大小  
    // 声明变量counter用于统计数组中值为2的元素数量,初始化为0  
    int n, counter = 0;  
    // 声明一个大小为100的整数数组array,用于存储用户输入的数值  
    int array[100];  
  
    // 从标准输入读取数组的大小n  
    scanf("%d", &n);  
  
    // 使用for循环遍历数组的每一个位置  
    for (int i = 0; i < n; i++) {  
        // 从标准输入读取一个整数并存储在array[i]中  
        scanf("%d", &array[i]);  
        // 检查当前元素是否等于2,如果是,则counter加1  
        if (array[i] == 2) {  
            counter++;  
        }  
    }  
    // 输出计数器counter的值,即数组中值为2的元素数量  
    printf("%d", counter);  
  
    // 程序正常结束  
    return 0;  
}

11.2 课时5作业2

测试样例:

这个题目主要考察的如何对字符数组中的字符进行翻转,同时要掌握 strcmp 函数的使用。

cpp 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char str[100],reverseStr[100];
    // 使用gets函数读取一行输入到str中(注意:gets函数是不安全的,因为它可能导致缓冲区溢出,建议使用fgets代替)
    gets(str);

    // 遍历原字符串,将其字符从后向前复制到reverseStr中,实现反转
    for (int i = strlen(str) - 1, j = 0; i >= 0; i--, j++) {
        reverseStr[j] = str[i];
    }
    // 注意:反转后的字符串没有以null字符('\0')结尾,这会导致strcmp的行为未定义。
    // 应该在循环结束后添加reverseStr[j] = '\0';来确保字符串正确终止。
    // 或者在在一开始定义数组时就给出默认值:char reverseStr[100] = {0}
    reverseStr[strlen(str)] = '\0'; // 正确添加null终止符

    // 比较原字符串和反转后的字符串  
    int result = strcmp(str, reverseStr);

    if (result < 0) {
        printf("%d\n", -1);
    } else if (result > 0) {
        printf("%d\n", 1);
    } else {
        printf("%d\n", 0);
    }

    return 0;
}
相关推荐
码到成龚4 个月前
c++习题27-大整数减法
算法·字符数组
4U2471 年前
C语言之strstr函数的使用和模拟实现
c语言·指针·字符数组·模拟实现·strstr
小林up1 年前
《C和指针》笔记30:函数声明数组参数、数组初始化方式和字符数组的初始化
c语言·参数·数组·初始化·字符数组·初始化方式