深入理解C指针 指针和字符串
5.1 字符串基础
字符串是以ASCII字符NUL结尾的字符序列。ASCII字符NUL表示为 \0 。字符串通常存储在数组或从堆上分配的内存。
字符串的长度是字符串中除了NUL字符之外的字符数。为字符串分配内存时,要记得为所有的字符再加上NUL字符分配足够的空间。
注意:NULL
和NUL
不同。NULL
用来表示特殊的指针,通常定义为( (void *) 0)
,而NUL
是一个char
,定义为 \0
,两者不能混用。
字符常量是单引号引起来的字符序列。字符常量通常由一个字符组成,也可以包含很多字符,比如转义字符。在C中,它们的类型是int
,如下所示:
printf("%d\n", sizeof(char)); printf("%d\n", sizeof('a'));
char
的长度是1字节,而字符字面量的长度是4字节。这个看似异常的现象乃语言设计者有意为之。
5.1.1 字符串声明
声明字符串的方式有三种:字面量、字符数组和字符指针。字符串字面量是用双引号引起来的字符序列,常用来进行初始化,它们位于字符串字面量池中。不要把字符串字面量和单引号引起来的字符搞混——后者是字符字面量。
5.1.2 字符串字面量池
定义字面量时通常会将其分配在字面量池中,这个内存区域保存了组成字符串的字符序列。多次用到同一个字面量时,字面量池中通常只有一份副本。这样会减少应用程序占用的内存。大部分编译器有关闭字面量池的选项,一旦关闭,字面量可能生成多个副本,每个副本拥有自己的地址。
GCC用-fwritable-strings
选项来关闭字符串池。在Microsoft Visual Studio 中,/GF
选项会打开字符串池。
字符串字面量一般分配在只读内存中,所以是不可变的。字符串字面量不存在作用域的概念。
在大部分编译器中,将字符串字面量看作常量,无法修改字符串。不过在有些编译器中,比如 GCC 字符串字面量是可修改的,因此最好把变量声明为const
类型。
5.1.3 字符串初始化
字符串所用的内存要么是数组要么是指针指向的一块内存。
1. 初始化char数组
用字符串字面量初始化char数组:
char header[] = "Media Player";
用strcpy函数初始化char数组:
char header[13]; strcpy(header, "Media Player");
更笨的办法是把字符逐个赋给数组元素:
header[0] = 'M'; header[1] = 'e'; …… header[12] = '\0';
下面的赋值是不合法的,我们不能把字符串字面量的地址赋给数组名字。
char header[13]; header = "Media Player";
2. 初始化char指针
初始化char * 常见方法是使用malloc和strcpy函数分配内存并将字面量复制到字符串中,如下所示:
char *header = (char*) malloc(strlen("Media Player")+1); strcpy(header,"Media Player");
再决定malloc
函数要用到的字符串长度时,要注意以下事项。
- 一定要记得算上终结符
NUL
- 不要用
sizeof
操作符,而是用strlen
函数来确定已有字符串的长度。sizeof
操作符会返回数组和指针的长度,而不是字符串的长度。
我们可以将字符串字面量的地址直接赋给字符指针,不过,这样不会产生字符串的副本。
char *header = "Media Player";
试图用字符字面量来初始化char
指针不会起作用。因为字符字面量是int
类型,这其实是尝试把整数赋给字符指针。这样将常会造成应用程序在解引指针时终止。
char * prefix = '+'; //不合法
正确的做法是像下面这样用malloc
函数:
char *prefix = (char *)malloc(2); *prefix = '+'; *(prefix + 1) = '\0';
3. 从标准输入初始化字符串
这里会出问题是因为我们在使用command
变量之前没有为其分配内存:
char *command; printf("Enter a Command:"); scanf("%s",command);
要解决这个问题需要首先为指针分配内存或则用定长数组代替指针。
4. 字符串的位置小结
char* globalHeader = "Chapter"; char globalArrayHeader[] = "Chapter"; void displayHeader(){ static char* staticHeader = "Chapter"; static char staticArrayHeader[] = "Chapter"; char* localHeader = "Chapter"; char localArrayHeader[] = "Chapter"; char* heapHeader = (char*)malloc(strlen("Chapter")+1); strcpy(heapHeader,"Chapter"); }
全局内存的字符串会一直存在,也可以被多个函数访问。
静态字符串也会一直存在,不过只有定义它们的函数才能访问。
分配在堆上的内存在释放之前会一直存在也可以被多个函数访问。
5.2 标准字符串操作
5.2.1 比较字符串
比较字符串的标准方法是用strcmp函数,原型如下:
int strcmp(const char *s1, const char *s2);
函数返回以下三种值之一:
- 负数:如果按字典序(字母序)s1比s2小就返回负数。
- 0:如果两个字符串相等就返回0。
- 正数:如果按字典序s1比s2大就返回正数。
比较两个字符串有几种不正确的方法,第一种识图用赋值操作符比较,如下:
char command[16]; printf("Enter a Command:"); scanf("%s", command); if(command = "Quit"){ ...
首先,这不是作比较,其次,这样会对导致类型不兼容的语法错误,我们不能把字符串字面量地址赋给数组名字。
另一种方法是相等操作符:
printf("Enter a Command:"); scanf("%s", command); if (command == "Quit") {
这样会得到假,因为比较的是command
的地址和字符串字面量的地址。相等操作符比较的是地址,而不是地址中的内容,用数组名字或者字符串字面量就会返回地址。
5.2.2 复制字符串
复制字符串通常用strcpy函数实现,其原型如下:
char * strcpy(char *s1, const char *s2)
str2可以是字符串常量、字符数组名或指向字符的指针变量。 str1必须是足够大且'\0'结束的字符数组或malloc函数分配的内存。
将str2中的字符串复制到str1数组中,覆盖str1中原有字符串。
char name[32]; char *names[30]; size_t count = 0; printf("Enter a name: "); scanf("%s", name); names[count] = (char *)malloc(strlen(name) + 1);//malloc函数分配每个字符串所需的内存,并将其赋给names中下一个可用的元素。 strcpy(names[count], name); count++;
5.2.3 拼接字符串
字符串拼接涉及两个字符串的合并。字符串拼接通常用strcpy函数实现,其原型如下:
char *strcat(char *s1, const char *s2);
此函数把第二个字符串拼接到第一个的到结尾。函数不会分配内存,这意味着第一个字符串必须足够长,能容纳拼接后的结果,否则函数可能会越界写入,导致不可预期的行为。函数的返回值的地址跟第一个参数的地址一样。str2可以是字符串常量、字符数组名或指向字符的指针变量。
正确的字符串拼接操作:
char* error = "ERROR: "; char* errorMessage = "Not enough memory"; char* buffer = (char*)malloc(strlen(error) + strlen(errorMessage) + 1); //给malloc函数参数加1是为了容纳NUL字符。 strcpy(buffer, error); strcat(buffer, errorMessage); printf("%s\n", buffer); printf("%s\n", error); printf("%s\n", errorMessage);
如果没有为拼接后的字符串分配独立的内存,就可能会覆写第一个字符串。假设第一个字面量在内存中的位置就在第二个字面量前面:
char* error = "ERROR: "; char* errorMessage = "Not enough memory"; strcat(error, errorMessage); printf("%s\n",error); printf("%s\n",errorMessage);
输出:
ERROR: Not enough memory
ot enough memory
errorMessage
字符串会左移一个字符,原因是拼接后的结果覆写了errorMessage
。
如果我们像下面这样用char
数组而不是用指针来存储字符串,就不一定能工作了:
char error[] = "ERROR: "; char errorMessage[] = "Not enough memory";
如果用下面这个strcat
调用会得到一个语法错误,这是因为我们试图把函数返回的指针赋给数组名字,这类操作不合法:
error = strcat(error, errorMessage);
如果像下面这样去掉赋值,就可能会有内存访问的漏洞,因为赋值操作会覆写栈帧的一部分。
strcat(error, errorMessage);
拼接字符串时容易犯错的另一个地方是使用字符字面量而不是字符串字面量。
char *path = "C:"; char *currentPath = (char *)malloc(strlen(path) + 2); strcpy(currentPath, path); currentPath = strcat(currentPath, "\\");
因为额外的字符和NUL字符需要空间,所以+2。字符串字面量中用到了转义字符,所以这里拼接的是一个反斜杠符。
如果使用字符字面量,那么就会得到一个运行时错误,原因是第二个参数(本应该为char *,' '单引号字符被解释为整数)被错误地解释为char
类型变量的地址:
currentPath = strcat(path, '\\');
5.3 传递字符串
5.3.1 传递简单字符串
size_t stringLength(char *string /*string[ ]*/ ){ char simpleArray[] = "simple string"; printf("%d\n", stringLength(simpleArray)); printf("%d\n", stringLength(&simpleArray)); printf("%d\n", stringLength(&simpleArray[0]));
在第一个语句中,我们用了数组名字,这会返回其地址。
第二个语句中,显示使用了取地址操作符,不过这样写有冗余,没有必要,而且会产生警告。
第三个语句中,我们对数组第一个元素用了取地址操作符,这样可以工作,没有警告,不过有点繁琐。
5.3.2 传递字符常量的指针
以字符常量指针的形式传递字符串指针可以防止传递的字符串被修改。
size_t stringLength(const char *string) {
5.4.2 返回动态分配内存的地址
如果需要从函数返回字符串,可以在堆上分配字符串的内存然后返回其地址。
char *blanks(int number) { char *spaces = (char *)malloc(number + 1); int i; for (i = 0; i < number; i++) { spaces[i] = ' '; } spaces[number] = '\0'; return spaces; }//返回一系列代表空白的字符串
释放返回的内存是函数调用者的责任,如果不再需要内存但没有将其释放将会造成内存泄漏。下面是一个内存泄漏的例子,printf函数中使用了字符串,但是接着他的地址就丢失了,因为没有保存:
printf("[%s]\n", blanks(5));
更安全的方法如下所示:
char *tmp = blanks(5); printf("[%s]\n", tmp); free(tmp);
返回局部字符串的地址
返回局部字符串的地址可能会有问题,如果内存被别的栈帧覆写就会损坏,应该避免使用这种方法。在函数内部声明一个数组,而不是动态分配内存,这个数组位于栈帧杀手给你。函数返回数组的地址:
#define MAX_TAB_LENGTH 32 char *blanks(int number) { char spaces[MAX_TAB_LENGTH]; int i; for (i = 0; i < number&&i < MAX_TAB_LENGTH; i++) spaces[i] = ' '; spaces[number] = '\0'; return spaces; }
执行函数后会返回数组的地址,但是之后下一次函数调用会覆写这块内存区域。