12、数组与字符串
本问介绍数组或字符串在内存中如何表示以及如何分配内存存储字符串。
首先理出对字符串可以执行的基本操作:
(1)比较两个字符串。(2)字符串复制。(3)查找字符串长度。(4)在字符串中查找子串。
下面是几种不同初始化字符串的方法:
char *strptr="hello"; char starrray1[]="hello"; char starrray2[6]="hello"; char starrray3[4]={'a','b','c','d'};
1、内存中的字符串排列
通常情况下,字符数组或字符串存储在连续内存单元中,如图所示:
通过类似于数组的方式访问串元素。数组索引和指针运算都可用来访问数组元素:
示例:
#include <stdio.h> int main() { char*str = "hello pointer"; int i = 0; for (i = 0; i < strlen(str); i++) { printf("%c", str[i]); } return 0; }
运行结果:
上述示例中,利用数组索引访问字符串中的字符。函数strlen(char*)用来返回字符串的长度。返回的长度不包括所有字符串总必有的最后一个空字符。
下面示例使用临时指针变量char* ptr遍历整个字符串。
#include <stdio.h> int main() { char* str = "hello pointer"; char* ptr = str; while (*ptr != '\0') { printf("%c", *ptr); ptr++; } return 0; }
运行结果如下:
2、动态内存分配
从堆区分配动态内存给字符串的方法类似于数组。程序员应该特别注意,数组中分配内存给字符序列存储字符串时需要一个额外的空字符标记字符串结尾。
#include <stdio.h> #include <malloc.h> int main() { char* src = "hello pointer"; char* dst = NULL; dst = (char*)malloc(sizeof(char)*(strlen(src)+1)); memcpy(dst, src, strlen(src)); while (*src != '\0') { printf("%c", *src); src++; } printf("\n"); while (*dst != '\0') { printf("%c", *dst); dst++; } return 0; }
运行结果如下:
定义字符串编译器时编译器自动在字符串结尾增加转义字符'\0'。某些文件中字符串常量也叫做字符串文本。最有趣的是从RO扇区分配内存给字符串常量。RO扇区是存储字符串文本和常量的只读数据区。存储在该区域的数据的生命周期为程序运行的整个生命周期。
示例:
#include <stdio.h> int* foo(void); int main() { int *m = foo(); printf("打印foo函数中的i的地址%d\n", *m); return 0; } int *foo(void) { int i = 10; return &i; }
运行结果如下:
虽然输出结果为10,但是着个程序是错误的,因为指针所指向的内存位置无效,但是这不过是因为编辑器的栈的生命周期没有结束,一点函数调用后,相应的生命周期结束,那么变量i的内存区域将不复存在。
但是对于字符串,其内存是从RO扇区分配的,所以在程序运行的整个生命周期都有效。
字符串文本的另一个重要特性是,一旦字符串文本被初始化其值就不能再进行修改了,相当于const char* 变量名,其中指针可被修改,但是所指的值不能。
示例:
#include <stdio.h> int main() { char *arry = "add"; arry[0] = 'b';//修改第0个索引的值,不允许。 //程序会产生分段错误 arry++; return 0; }
3、字符串操作
1、处理字符串输入
如前所述,需要注意分撇足够内存区域来存储字符串中的字符。其中scanf()中的%s是将输入字符串存储到变量的格式。
示例:
#include <stdio.h> #include <malloc.h> int main() { char arr[6]; char* strptr; printf("输入hello\n"); scanf("%s", arr); strptr = (char*)malloc(sizeof(char) * 10); printf("输入hello\n"); scanf("%s", strptr); }
运行结果如下:
2、字符串遍历
遍历字符串变量的每个索引是进行读数据或操作数据的最基本过程。通过逐个读索引读取字符串,这个在动态内存存储中已经演示过了。
3、字符串长度
字符串长度为字符串变量所春初字符的数。结束符不计算在内。
4、字符串复制
字符串复制即将字符串变量所指向的某个内存位置的存储数据复制到另一个内存单元所指向的内存位置的操作。下面为复制过程
void str_copy(char *dest_str,const char * src_str) { char* stemp=src_str; char* dtemp=dest_str; while(*stemp!='\0') { *dtemp=*stemp; stemp++; dtemp++; } *dtemp='\0'; }
5、字符串连接
字符串连接是将给定的字符串与其他输入字符串连接起来的操作。默认情况下字符串连接都从连接端进行。示例代码如下
void str_cat(char* deststr,const char* srcstr) { char *dtemp=deststr; char * stemp=srcstr; while(*dtemp!='\0') { dtemp++; } while(*srcstr!='\0') { *dtemp=*srcstr; dtemp++; srcstr++; } *dtemp='\0'; }
4、字符串数组
字符串数组的元素是指向字符数组或者字符串数组的指针。数组内每个字符串可以是不同长度。图4-2为字符串数组存储示意图。每个索引包含一个指向字符串变量长度的指针。如下图所示
1、字符串数组的声明
字符串数组可以通过三种方式来说完成。声明包括两个重要因素:其一为数组大小,其二为各元素的数据容纳能力。
下面给出字符串数组声明实例以方便读者理解内存排列和声明。
(1)
char str_arr[6][7];
上述的字符串数组声明中,每行能存储最大长度为6个字符的串。最后一个元素用来存储结束符。下图表示字符串数组的内存排列。
示例:
#include <stdio.h> #include <malloc.h> int main() { char arr[6][10] = { "EGRET","IBIS","MYNA","IORA","MUNIA","BULBUL" }; int i; for (i = 0; i < 6; i++) { printf("%d-%s\n", i, arr[i]); } return 0; }
程序运行结果
(2)编译大小未知和每个数组元素的数据容量已知时,使用数组符号。
char * str_arr2[10]
使用示例:
#include <stdio.h> #include <string.h> #include <malloc.h> int main() { char* arr[6]; char tempstring[30]; int i; for (i = 0; i < 6; i++) { printf("插入数据\n"); scanf("%s", tempstring); arr[i] = (char*)malloc(sizeof(char)*(strlen(tempstring) + 1)); strcpy(arr[i], tempstring); } printf("数组数据"); for (i = 0; i < 6; i++) { printf("%d-%s\n", i, arr[i]); } freestring(arr, 5); return 0; } freestring(char arr[], int length) { int i; for (i = 0; i <=length; i++) { free(arr[i]); } }
程序运行结果:
freestring(char arr[],int length)函数包括字符数组的地址及其长度。上述代码表示当遍历时只分配(动态分配)内存空间给数组每个元素存储的字符指针所指向的字符串。所以在freestring()中,当每次遍历时都需要释放动态分配内存。
(3)编译时数组大小与每个数组元素的数据容量都未知时使用第三中方法对字符串数组声明。
char** dynamic_str;
下图表示使用二级指针声明时字符串的内存排列。我们可以看到,内存中对应于每个索引数据为变量长度。
示例:在下面的代码中我们使用一种新技术来存储字符串数组。借助二级字符指针,首先创建指针存储空间,接着从堆中分配空间来存储指针,接下来再从堆中分配内存存储指针来再从堆中分配空间来存储新字符串的指针。
#include <stdio.h> #include <string.h> #include <malloc.h> int main() { char** arr=NULL; char tempstring[30]; int i; for (i = 0; i < 6; i++) { printf("插入数据\n"); scanf("%s", tempstring); if (arr == NULL) { arr = (char**)malloc(sizeof(char*)); } else { arr = (char**)realloc(arr,sizeof(char*)*(i+1)); } arr[i] = (char*)malloc(sizeof(char)*(strlen(tempstring) + 1)); strcpy(arr[i], tempstring); } for (i = 0; i < 6; i++) { printf("%d-%s\n", i, arr[i]); } freestring(arr, 5); return 0; } int freestring(char **arr, int length) { int i; for (i = 0; i <=length; i++) { free(arr[i]); } free(arr); }
运行结果如下:
在上述情况下释放内存稍有些棘手,下面为释放内存所需遵循的基本步骤:
(1)遍历数组每一行。
a.访问每行时,释放存储字符串的相应内存。
(2)一旦完成数组遍历,释放存储所有指向字符串指针的分配内存。
下图表示内存空间的释放步骤。如上所述,当迭代每个索引时,首先要在索引迭代过程中释放相应的数组,图中该步骤用虚线标出。完成该步后需要释放所有元素的内存区域。图中这一步骤用椭圆标出。