基础概念
字符串可以分配到内存的不同区域,通常使用指针来支持字符串操作。字符串是以ASCII字符NUL结尾的字符序列。ASCII字符NUL表示为\0。字符串通常存储在数组或者从堆上分配的内存中。不过,并非所有的字符数组都是字符串。例如,字符数组可能没有NUL字符。
C中有两种类型的字符串。
* 单字节字符串。由char数据类型组成的序列。
* 宽字符串。由wchar_t数据类型组成的序列。
wchar_t数据类型用来表示宽字符串,可能是16位或32位宽。这两种字符串都以NUL结尾。宽字符主要用来支持非拉丁字符集,对于支持外语的应用程序很有用。
字符串的长度是除了NUL字符之外的字符数。为字符串分配内存时,要记得为NUL字符预留空间。注意NUL是一个字符,定义为\0,与NULL(void*(0))不一样。NULL用来表示一种特殊的指针。
字符常量是单引号引起来的字符序列。字符常量通常由一个字符组成,也可以包含多个字符,如转义字符。在C中,它们的类型是int。char的长度是1字节,而一个字符字面量的长度是 sizeof(int) 个字节。
printf("%d\n", sizeof('a'));//4
声明字符串的方式有3种:字面量、字符数组、字符指针。字符串字面量是用双引号引起来的字符序列,常用来进行初始化,它们位于字符串字面量池中。如果声明一个拥有32个字符的数组,那么只能放31个字符串文本,因为字符串要以NUL结尾。字符串在内存中的位置取决于声明的位置。
定义字面量时通常会将其分配在字面量池中,多次用到同一个字面量时,池中通常只有一份副本。通常认为字面量是不可变的。你可以关闭字面量池来生成多个副本。在哪里使用字符串字面量不重要,它没有作用域的概念。
有些编译器允许修改字符串,因此把不希望被修改的字符串声明为常量是个不错的选择。
字符串初始化方式取决于变量被声明为数组还是指针,字符串所用的内存要么是数组要么是指针指向的一块内存。我们可以从字符串字面量或其它地方(比如标准输入)得到字符。
char head[] = "hello man"; printf("size of head is : %d\n",sizeof(head)); //size of head is : 10
可以看到 "hello man"有9个字符,但是长度是10,因为有NUL字符。还可以strcpy函数初始化数组。
char head1[10]; strcpy(head1,"hello man");
注意不要用数组的名字作为左值。
动态内存分配可以提供更多灵活性,也可能让内存存在更久。通常使用malloc和strcpy来初始化字符串。
char* head2 = (char*) malloc (strlen("hello man")+1); strcpy(head2,"hello man");
注意在使用malloc决定所需内存长度时,要为NUL预留空间,并且使用strlen而不是sizeof。sizeof会返回数组和指针的长度,而不是字符串的长度。如果用字符串字面量来初始化指针会导致指针指向字符串字面量池。注意不要把字符字面量赋给指针,因为它是int类型。可以把字符字面量赋给解引后的指针。
*(head2 + 7) = 'e'; printf("head2 is %s\n", head2); //head2 is hello men
总之字符串可能位于全局或静态内存(global or static array),也可能位于字符串字面量池({……}),可能位于堆上(malloc),可能位于函数的栈帧里(char array[])。字符串的位置决定它能存在多久以及哪些程序可以访问它。全局内存的字符串会一直存在,可以被多个函数访问;静态字符串也一直存在,但只有定义它的函数能访问;堆上的字符串可以被多个函数访问,直到被释放前都存在。
标准字符串操作
比较字符串的标准方法是strcmp函数。其原型如下。
int strcmp(const char* s1, const char* s2);
如果两个字符串相等,返回0。s1大于s2,返回正数。s1小于s2,返回负数。
char command[16]; printf("enter a command :"); scanf("%s",command); if(strcmp(command,"quit")==0){ printf("you typed quit!\n"); } else { printf("i don't know what you typed!\n"); }
注意如果此处使用if(command == "quit")的话,被比较的实际是command的地址和字符串字面量的地址。
复制字符串通常使用strcpy函数实现,其原型如下:
char* strcpy(char* s1, const char* s2);
有一类应用程序会读入一系列字符串,挨个存入占最少内存的数组。先创建一个足够长的字符串数组,其长度足以容纳用户允许输入的最长字符串,然后把字符串读入这个数组。有了读取的字符串,我们就能根据字符串的长度分配合适的内存。
char mynames[32]; char* myname[30]; size_t count = 0; printf("enter a name please:"); scanf("%s",mynames); myname[count] = (char*) malloc (strlen(mynames)+1); strcpy(myname[count], mynames); count++;
可以在一个循环里重复这个操作。
两个指针可以引用同一个字符串。两个指针引用同一个地址称为别名。把一个指针赋值给另一个指针只是复制了字符串的地址而已。
字符串拼接涉及两个字符串的合并。通常用strcat来执行这种操作。这个函数的原型为:
char* strcat(char* s1, const char* s2);
下面是如何使用缓冲区拼接字符串。
char* error = "ERROR: "; char* errormsg = "not enough memory!"; char* _buffer = (char*) malloc (strlen(error) + strlen(errormsg) +1); strcpy(_buffer, error); strcpy(_buffer, errormsg); printf("%s\n", _buffer); printf("%s\n", error); printf("%s\n", errormsg);
如果直接使用strcpy(error, errormsg)的话,可能会覆盖error字符串字面量地址后面某些未知的内容,因为我们没有为新的字符串分配独立内存。拼接字符串常见的错误就是没有为新字符串额外分配空间。此外注意不要使用字符字面量代替字符串字面量作为该函数的参数。
传递和返回字符串
先定义一个函数。
size_t strLength(char* string){ size_t length = 0; while(*(string++)){ length++; } return length; } char simpleArray[] = "simple string"; char* simplePtr = (char*) malloc (strlen("simple string")+1); strcpy(simplePtr, "simple string"); printf("%d\n", strLength(simplePtr));//13
对指针调用这个函数,只需要传入指针的名字。对数组使用该函数也可以这么做。在这里数组的名字被解释成了地址。
printf("%d\n", strLength(simpleArray));
你也可以对数组的0下标使用取地址操作符,但是那样太繁琐了:strLength(&simpleArray[0])。
把参数声明为指向字符常量的指针,可以防止字符串被修改。假如要让函数返回一个由该函数初始化的字符串,必须决定是否由函数调用者负责释放分配的内存。如果在函数内部动态分配内存并返回指向该内存的指针,那么调用者必须负责最终释放该内存,这要求调用者必须清楚函数的使用方法。
main函数通常是应用程序第一个执行的函数。对基于命令行的程序来说,通过为其传递某些信息来打开某些功能的开关很常见。比如linux下的ls命令会通过接受 -la 等参数来执行不同行为。C通过argc和argv参数支持命令行参数。第一个参数argc是一个整数,用来指定传递参数的数量。系统至少会传递一个参数,这个参数是可执行文件的名字。第二个参数argv,通常被看做字符串指针的一维数组,每个指针引用一个命令行参数。
int main(int argc, char** argv){ int i =0; while(i<argc){ printf("argv[%d]is %s\n",i,argv[i]);//argv[0]is ./mysender i++; } }
可以看到,不附加任何参数,默认自带的一个参数就是 ./mysender,是我的编译后文件的名字。试试用以下方式执行:./mysender -f jack -la limit = 100,此时输出为:
argv[0]is ./mysender argv[1]is -f argv[2]is jack argv[3]is -la argv[4]is limit argv[5]is = argv[6]is 100
由于我把 "=" 符号用空格分开了,结果 "=" 被当做一个参数了。实际上应该用空格把每个参数分开,参数本身不应包含空格了。
函数返回字符串时,返回的实际是字符串的地址。这可能是字符串字面量的地址,可能是动态内存的地址,可能是本地字符串变量的地址。
先看第一种情况。对于静态的指向字符串字面量的指针,应该注意在不同地方重复使用会覆盖上一次的结果。字符串并非总是被看做常量,可以通过命令关闭字符串常量池,把字符串声明为常量可以防止字符串被修改。如果返回的是动态分配的内存,那么一定要注意防止内存泄露。返回局部变量字符串的地址可能有问题,因为函数执行完毕后该处内存可能被别的栈帧覆写。比如你在函数里声明一个字符串数组并初始化了,然后返回数组的地址,这个数组所占用的内存是不安全的,面临被其它函数的栈帧覆写的风险。
函数指针和字符串
通过函数指针控制程序执行是一种非常灵活的方法。
#include <stdio.h> #include <stdlib.h> #include <string.h> char* stringToLower(const char* string){ char* tmp = (char*) malloc (strlen(string) + 1); char* start = tmp; while(*string != 0){ *(tmp++) = tolower(*(string++)); } *tmp = 0; return start; } main(){ typedef int (fptroperation)(const char*, const char*); int compare(const char* s1, const char* s2){ return strcmp(s1,s2); } int compareIgnoreCase(const char* s1, const char* s2){ char* t1 =stringToLower(s1); char* t2 =stringToLower(s2); int result = strcmp(t1,t2); free(t1); free(t2); return result; } void sort(char* array[], int size, fptroperation operation){ int swap = 1; while(swap){ swap = 0; int l = 0; while(l<size-1){ if(operation(array[l],array[l+1])>0) { swap = 1; char* tmp = array[l]; array[l] = array[l+1]; array[l+1] = tmp; } l++; } } } void display(char* names[], int size){ int i = 0; while(i<size){ printf("%s ",names[i]); i++; } printf("\n"); } char* names[] = {"jack","rose","Titanic","hello","World"}; char* newnames[] = {"jack","rose","Titanic","hello","World"}; sort(names, 5, compare); display(names,5);//Titanic World hello jack rose sort(newnames, 5, compareIgnoreCase); display(newnames, 5);//hello jack rose Titanic World }
这个示例通过使用函数指针来实现不同规则下的字符串比较工作。