【C语言进阶】数组与字符串

2,406 阅读9分钟

C语言中的数组与字符是学习C语言时非常重要的基础部分。它们不仅用于存储和处理数据,还是理解更复杂数据结构(如字符串、结构体、指针等)的基石。

一、数组

1.1. 定义

数组是一种基础的数据结构,用于在计算机内存中连续存储相同类型的数据。每个元素在数组中的位置通过索引(或下标)来访问,索引通常是从0开始的。意味着第一个元素的索引是0,第二个元素的索引是1,依此类推。

1.2. 声明与初始化

  • 声明数组

    int arr[10]; // 声明了一个整型数组,大小为10,但未初始化,其值是不确定的。

  • 初始化数组

    int arr[5] = {1, 2, 3, 4, 5}; // 声明并初始化了一个整型数组,大小为5,元素依次为1, 2, 3, 4, 5。

如果初始化时提供的元素少于数组的大小,剩余的元素将自动初始化为0(对于整型数组)。

int arr[10] = {1, 2, 3}; // 数组的前三个元素被初始化为1, 2, 3,其余元素自动初始化为0。

1.3. 访问元素

通过索引访问数组元素:

int secondElement = arr[1]; // 访问数组的第二个元素(索引为1的元素)

1.4. 遍历数组

使用循环结构(如for循环)遍历数组:

#include <stdio.h>  
  
int main() {  
    int arr[5] = {1, 2, 3, 4, 5};  
    for(int i = 0; i < 5; i++) {  
        printf("%d ", arr[i]); // 遍历数组并打印每个元素  
    }  
    return 0;  
}

二、字符串

2.1. 定义与特性

在C语言中,字符串是一个非常重要的概念,它是以空字符('\0')结尾的一串字符的集合,实际上是以字符数组的形式实现的。这种以空字符结尾的特性使得C语言能够识别字符串的结束位置,而不需要额外的长度信息。字符串字面量(如 "Hello, World!")在程序中被编译器处理为这样的字符数组,并自动在末尾添加一个空字符。

2.2. 声明与初始化

字符串的声明与初始化通常有两种方式:

1. 使用****字符数组

char str[] = "Hello"; // 声明并初始化一个字符数组,编译器会自动在末尾添加'\0'

str 是一个字符数组,它包含了字符串 "Hello" 和其结尾的空字符。

2. 使用字符指针

char *ptr = "Hello"; // 声明一个字符指针,指向字符串字面量 "Hello"

ptr 是一个指向常量字符串的指针,字符串 "Hello" 存储在只读内存区域中。尝试通过 ptr 修改字符串的内容将导致未定义行为,因为字符串字面量通常位于程序的只读数据段。

2.3. 操作字符串

C标准库提供了一系列函数来操作字符串,这些函数都依赖于字符串以空字符结尾的特性来确定字符串的结束位置。常用的字符串操作函数包括:

三、使用场景

C语言中的数组和字符串是编程中非常重要的基础部分,它们具有广泛的应用场景。以下是它们各自的主要应用场景。

3.1. 数组的应用场景

1. 数据存储和处理

  • 数组用于存储大量同类型的数据,通过索引可以快速访问和操作数组中的元素,使得数据的存储和处理变得高效和方便。

  • 例如,可以使用数组来存储学生的成绩、商品的库存量等,并通过循环遍历数组进行求和、平均值计算、排序等操作。

2. 批量处理

  • 使用数组可以实现批量数据的处理,如批量读取、写入文件,批量计算等。

  • 通过对数组中的每个元素执行相同的操作,可以简化代码并提高程序的执行效率。

3. 简化代码

  • 通过使用数组,可以将多个相关变量组合成一个数组,从而简化代码并提高代码的可读性。

  • 例如,在处理多个学生的信息时,可以将每个学生的姓名、年龄、成绩等信息存储在一个结构体数组中,而不是使用多个独立的变量。

4. 函数参数传递

  • 在函数调用时,可以通过数组将多个数据一次性传递给函数,简化函数参数的传递过程。

  • 有助于提高函数的复用性和灵活性。

5. 统计数据

  • 使用数组可以方便地统计数据的频率、分布等信息,如统计一组数据中的最大值、最小值、平均值等。

  • 通过对数组中的数据进行统计分析,可以实现各种数据挖掘和机器学习算法。

6. 动态内存分配

  • 在需要动态改变数组大小时,可以使用动态内存分配函数(如malloccallocrealloc)来分配和释放内存。

  • 使得数组的使用更加灵活,可以适应不同规模的数据处理需求。

7. 数据结构实现

  • 数组可以用作实现其他数据结构的基础,如链表、栈、队列等。

  • 例如,可以使用数组来模拟一个链表,通过数组中的每个元素来存储链表节点的信息。

3.2. 字符串的应用场景

字符串在C语言中通常通过字符数组来实现,因此它继承了数组的一些特性,并具有自己独特的应用场景。

1. 文本处理

  • 字符串在文本处理中有广泛的应用,如读取文件内容、搜索替换、分割字符串等操作。

  • 这些操作是文本编辑、编译器、解释器等工具的基础。

2. 用户界面

  • 在图形用户界面(GUI)开发中,字符串用来显示消息、按钮标签、菜单项等。

  • 它们是用户与程序交互的重要媒介。

3. 网络编程

  • 在网络编程中,字符串被用来表示网络数据包的内容、URL地址等。

  • 它们是网络通信中数据交换的基础。

4. 数据库操作

  • 数据库操作中经常需要处理字符串,如拼接SQL语句、解析数据、处理查询结果等。

  • 字符串处理是数据库编程中不可或缺的一部分。

5. 游戏开发

  • 在游戏开发中,字符串用来存储游戏内文本、用户输入、游戏设置等信息。

  • 它们为游戏提供了丰富的交互性和可玩性。

6. 日志记录

  • 在日志记录中,字符串被用来记录系统运行信息、错误信息等。

  • 它们是系统监控和故障排查的重要工具。

7. 加密解密

  • 在加密解密算法中,字符串被用来存储密钥、明文、密文等数据。

  • 字符串处理是信息安全领域中的基础操作之一。

四、注意事项

在C语言中,数组和字符串的使用是编程过程中的基础且重要的部分。以下是它们在使用时需要注意的事项。

4.1. 数组的使用注意事项

1. 定义与初始化

  • 数组的元素个数不能为0。

  • 数组的类型是一种自定义数据类型,如int arr[10]的类型为int[10]

  • 数组在定义时可以初始化,若只初始化部分元素,则剩余元素自动初始化为0(对于静态存储期或全局数组)。

2. 下标访问

  • 数组下标从0开始,n个元素的数组最后一个元素的下标为n-1。

  • 访问数组时索引必须小于数组的大小,否则会导致未定义行为,可能覆盖内存中的其他数据或导致程序崩溃。

    int arr[5];
    arr[5] = 6; // 错误:数组越界

3. 内存****连续性

  • 数组在内存中是连续存放的,访问效率较高。

  • 但这种连续性也意味着如果数组过大,可能会占用过多的连续内存空间。

4. 大小计算

  • 可以使用sizeof运算符计算数组的大小(以字节为单位)和元素个数(通过sizeof(数组)/sizeof(数组[0])计算)。

5. 数组大小不可变

一旦数组被声明,其大小就不能改变。如果需要动态改变数组大小,可以使用指针和动态内存分配(如mallocreallocfree)。

#include <stdlib.h>  
int *dynamicArray = (int *)malloc(10 * sizeof(int)); // 动态分配一个整型数组  
if (dynamicArray != NULL) {  
    // 使用dynamicArray  
    free(dynamicArray); // 使用完毕后释放内存  
}

6. 变长数组(VLA

  • C99标准引入了变长数组的概念,允许使用变量指定数组大小,但变长数组的长度只能在运行时确定,且不能初始化。

  • 使用变长数组时要特别注意其生命周期和内存使用,因为它们通常在栈上分配空间,可能会导致栈溢出。

7. 动态内存分配

  • 对于需要动态改变大小的数组,可以使用malloccallocrealloc等函数在堆上动态分配内存。

  • 使用动态内存分配时要确保正确管理内存,避免内存泄漏或野指针等问题。

4.2. 字符串的使用注意事项

1. 字符串定义

  • 字符串在C语言中通常通过字符数组表示,以空字符(\0)作为结束标志。

  • 字符串常量(如"hello")在编译时会被自动存储为字符数组,并在末尾添加\0

2. 字符串操作

  • 字符串不能直接被赋值(除了初始化时),但可以通过字符数组的形式来修改字符串的内容。

  • 使用标准库函数(如strcpystrcat等)时,要确保目标缓冲区足够大以容纳源字符串及其结束符\0

3. 缓冲区溢出

  • 字符串操作中最常见的问题是缓冲区溢出,可能导致程序崩溃或安全漏洞。

  • 要特别注意使用strncpystrncat等函数来限制复制的字符数,或使用动态内存分配来确保缓冲区足够大。

4. 字符串函数

  • C语言标准库提供了一系列操作字符串的函数(如strlenstrcmpstrcpy等),使用时要注意其参数类型和返回值类型。

  • 确保字符串以\0结尾,否则可能会导致字符串函数的行为异常。

5. 内存****管理

  • 如果字符串是通过动态内存分配得到的,那么在使用完毕后要及时释放内存,避免内存泄漏。

6. 字符串数组

  • 当需要存储多个字符串时,可以使用字符串数组(即字符数组的数组)。

  • 注意每个字符串的末尾都要有\0作为结束标志,并且整个字符串数组也需要足够的空间来存储这些\0字符。

C语言中的数组和字符串在使用时需要注意定义与初始化、下标访问、内存连续性、大小计算、变长数组的使用、动态内存分配、字符串操作、缓冲区溢出、字符串函数的使用以及内存管理等方面的问题。

五、总结

在C语言的学习中,数组和字符串是构建程序逻辑和处理数据不可或缺的基础数据结构。它们各自拥有独特的特性和应用场景,对于深入理解C语言以及后续编程语言的学习都至关重要。

5.1. 数组

  • 数组是一种连续存储相同类型数据的结构。它允许程序员通过索引(或下标)快速访问和操作每个元素。

  • 数组的声明需要指定类型和大小,大小在声明后不可改变(除非使用动态内存分配如malloccalloc)。

  • 数组的初始化可以在声明时进行,未初始化的元素默认值为0(对于全局和静态数组)。

  • 访问数组元素时需要注意索引不要越界,否则可能导致未定义行为,如访问非法内存区域。

  • 遍历数组通常使用循环结构,如for循环或while循环。

5.2. 字符串

  • 字符串在C语言中是以空字符('\0')结尾的字符数组。这种以空字符结尾的特性使得C语言能够识别字符串的结束位置。

  • 字符串字面量(如"Hello, World!")在内存中也是以这种方式存储的,编译器会自动在末尾添加一个空字符。

  • 字符串可以通过字符数组或字符指针来声明和初始化。使用字符数组时,需要为整个字符串(包括空字符)分配足够的空间;使用字符指针时,应注意字符串字面量通常存储在只读内存区域,不应被修改。

  • C标准库提供了一系列函数来操作字符串,如strlenstrcpystrcatstrcmp等。这些函数依赖于字符串以空字符结尾的特性来确定字符串的结束位置。

  • 操作字符串时需要特别小心,确保目标数组有足够的空间来存储结果字符串,以防止缓冲区溢出等安全问题。

5.3. 注意事项

  • 对于数组,要注意索引不要越界,避免访问非法内存区域。

  • 对于字符串,要确保字符串的末尾有空字符,否则函数可能会读取到内存中的垃圾数据。

  • 使用strcpystrcat等函数时要确保目标数组有足够的空间,防止缓冲区溢出。

  • 字符串字面量通常存储在只读内存区域,不应尝试修改它们。

掌握数组和字符串的声明、初始化、访问、遍历以及相关的注意事项,是深入学习C语言和其他编程语言的重要基础。它们不仅用于处理基础的数据存储和文本处理任务,还是实现更复杂数据结构(如链表、栈、队列等)和算法(如排序、搜索等)的基础。因此,在学习C语言的过程中,务必重视数组和字符串的学习和应用。

六、测试

问题:以下代码输出结果是什么?为什么?(某互联网公司2023年C语言面试题)

#include <stdio.h>
#include <string.h>
int main() {
    char str1[] = "abc";
    char str2[] = "abc\0def";
    printf("%d, %d", strlen(str1), strlen(str2));
    return 0;
}

答案

输出3, 3

原因:C语言中strlen函数计算字符串长度时,以'\0'为结束标志(不包含'\0'),str2'\0'后的def不会被统计。

问题:数组越界会导致什么后果?如何避免数组越界?

答案

后果:程序崩溃、内存数据被篡改、出现未定义行为(如访问非法内存);

避免方法:

① 访问时确保索引在0~n-1(n为数组长度);

② 用sizeof(数组)/sizeof(数组[0])动态获取数组长度;

③ 避免使用变长数组时过度分配栈空间;

④ 动态数组需合理管理内存边界。

问题strcpystrncpy的核心区别是什么?使用strcpy时需注意什么?(某大厂2024年嵌入式开发面试题)

答案

核心区别:strcpy无长度限制,会将源字符串(含'\0')完整复制到目标地址,直到遇到'\0'strncpy需指定复制长度n,最多复制n-1个字符,剩余位置补'\0'

使用strcpy注意事项:确保目标缓冲区有足够空间容纳源字符串(含'\0'),否则会导致缓冲区溢出。

问题sizeofstrlen 在处理字符数组时有何根本区别?

答案:

sizeof是编译时运算符,返回数组或类型占用的总内存字节数(包括\0)。strlen是运行时函数,计算从给定地址到第一个\0字符前的字符个数(不包括\0)。例如,char str[100] = "hello";sizeof(str)是100,而strlen(str)是5。

**问题:**在C语言中,数组名作为函数参数传递时,会发生什么?请解释“数组退化为指针”的含义。

答案:

数组名作为函数参数时,会退化为指向其首元素的指针,同时丢失其大小信息。因此,在函数内部无法用sizeof(参数)获得原数组长度,通常需要额外传递一个长度参数。例如,void func(int arr[]) 实际上等价于 void func(int *arr)

问题:以下代码存在什么安全隐患?如何安全地修正?

char dest[10];
char src[] = "This is a very long string that will cause overflow";
strcpy(dest, src);

答案

存在缓冲区溢出风险。src远长于deststrcpy会越过dest边界写入内存,导致未定义行为(程序崩溃、数据损坏或被恶意利用)。

安全修正:使用 snprintf(dest, sizeof(dest), "%s", src); 或确保复制前检查长度。

题目:C语言中,数组和指针有什么区别?请举例说明。

答案

数组是连续内存块,大小固定,声明后不能改变;指针是变量,存储地址,可重新指向其他内存。例如,int arr[5]定义数组,int *ptr定义指针,ptr = arr使指针指向数组首元素。数组名在多数情况下退化为指针,但 sizeof(arr)返回数组总大小,而 sizeof(ptr)返回指针大小。

题目:如何安全地复制字符串?比较 strcpystrncpy的优缺点。

答案

strcpy直接复制整个字符串,可能导致缓冲区溢出;strncpy可指定最大复制字符数,更安全,但不会自动添加空字符,需手动处理。例如,使用 strncpy(dest, src, size-1)并设置 dest[size-1] = '\0'

题目:C语言中,字符串字面量为什么通常不可修改?尝试修改会导致什么?

答案

字符串字面量(如 "hello")存储在只读内存段(如.rodata),修改会触发未定义行为(如程序崩溃)。这是因为编译器为优化和安全性设计。应使用字符数组(如 char str[] = "hello")进行修改。