深入理解C指针之五:指针和字符串

时间:2024-12-21 22:33:37

原文:深入理解C指针之五:指针和字符串

  基础概念

  字符串可以分配到内存的不同区域,通常使用指针来支持字符串操作。字符串是以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通过argcargv参数支持命令行参数。第一个参数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
}

  这个示例通过使用函数指针来实现不同规则下的字符串比较工作。