基础概念
字符串可以分配到内存的不同区域,通常使用指针来支持字符串操作。字符串是以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
}
这个示例通过使用函数指针来实现不同规则下的字符串比较工作。