指针与字符串
导读: 字符串可以分配在内存的不同区域,通常使用指针来支持字符串操作。指针支持动态分配字符串和将字符串作为参数传递给函数。
了解声明和初始化字符串的不同方法,研究C程序中字面量池的使用及其影响。此外,我们还会了解比较、复制和拼接字符串等常见字符串操作。
字符串通常以字符指针的形式传递给函数和从函数返回。我们可以用字符指针传递字符串,也可以用字符常量的指针传递字符串,后者可以避免字符串被函数修改。
我们也可以从函数返回字符串,从而满足某个请求。可以将这个字符串从外面传给函数并由函数修改,也可以在函数内部分配,还可以返回静态分配的字符串。
指针是保存字符串地址的。我们可以使用地址索引和接引数据的。
一、字符串基础
字符串是以ASCII字符NUL结尾的字符序列。ASCII字符NUL表示为 \0。字符串通常存储在数组或者从堆上分配的内存中。不过,并非所有的字符数组都是字符串,字符数组可能没有NUL字符。字符数组也用来表示布尔值等小的整数单元,以节省内存空间。C语言中有两种字符串
- 单字节字符串
由char数据类型组成的序列。 - 宽字符串
由wchar_t数据类型组成的序列。
1、字符串声明
声明字符串的方式有三种:字面量、字符数组 和 字符指针。字符串字面量是用双引号引起来的字符序列,常用来进行初始化,它们位于字符串字面量池中。
声明例子如下:
//栈区字符串
char header[32];
//保存字符串的指针
char *header;
2、字符串字面量池
字符串字面量一般分配在只读内存中 (但最好显示声明字符串不可修改),所以是不可变的。字符串字面量在哪里使用,或者它是全局、静态或局部的都无关紧要,从这个角度讲,字符串字面量不存在作用域的概念。
const char*tabHeader="Media Player";
*tabHeader='L'; //会报错,不能休该
内存示意图:
3、字符串初始化
1、初始化字符数组
//第一种:直接字符字面量赋值
char header[]="Media Player";
//第二种:使用字符操作函数
char header[13];
strcpy(header,"Media Player");
//第三种:逐个赋值
header[0]='M';
header[1]='e';
header[12]='\0';
2、初始化字符指针
在使用malloc函数分配空间后,这个内存空间不会主动设置字符串结尾的标志 都是要手动设置的。
- 一定要存在正确的字符串分配地址【不可未分配地址就赋值】
- 一定要记得算上终结符NUL。
- 不要用sizeof操作符,而是用strlen函数来确定已有字符串的长度。
sizeof操作符会返回数组和指针的长度,而不是字符串的长度。
//使用malloc函数分配空间
char *header=(char*)malloc(strlen("Media Player")+1);
//第一种:直接字符字面量赋值
char*header="Media Player";
//第二种:使用字符拷贝函数和字面量赋值
strcpy(header,"Media Player");
//第三种:逐个赋值
*(header+0)='M';
*(header+1)='e';
.....
*(header+12)='\0';
3、从其他输入初始化字符串
其实就是从文件、用户输入等方面获取的字符串。初始化和前面两种差不多,只是来源不一样,上面是程序写死的,这个是开放的。
//从控制台输入 命令
#define StringLength 100
char*command = (char*)malloc(sizeof(char) * StringLength +1);;
printf("Enter a Command:");
scanf("%s",command);
4、字符串位置小结
下面是一个字符字面量、数组和指针的调用例子。
char*globalHeader="Chapter";
char globalArrayHeader[]="Chapter";
void displayHeader()
{
static char* staticHeader="Chapter";
char* localHeader="Chapter";
static char staticArrayHeader[]="Chapter";
char localArrayHeader [ ] = "Chapter" ;
char*heapHeader=(char*)malloc(strlen("Chapter")+1);
strcpy(heapHeader,"Chapter");
}
内存示意图:
二、标准字符串操作
下面几个函数都定义于头文件 <string.h>
下面情况要注意:
- 不能使用char类型变量的地址 : currentPath=strcat(path, ‘\’);
1、比较字符串
//定义
int strcmp( const char *s1, const char *s2);
原理:
相互比较的方法是被比较的字符串中首对不同字符(都转译成 unsigned char )的值间的差的符号。
返回值:
- 负数
如果按字典序(字母序)s1比s2小就返回负数。 - 0
如果两个字符串相等就返回0。 - 正数
如果按字典序s1比s2大就返回正数。
实例代码:
#include <string.h>
#include <stdio.h>
void demo(const char* lhs, const char* rhs)
{
int rc = strcmp(lhs, rhs);
const char *rel = rc < 0 ? "precedes" : rc > 0 ? "follows" : "equals";
printf("[%s] %s [%s]\n", lhs, rel, rhs);
}
int main(void)
{
const char* string = "Hello World!";
demo(string, "Hello!");
demo(string, "Hello");
demo(string, "Hello there");
demo("Hello, everybody!" + 12, "Hello, somebody!" + 11);
}
输出:
[Hello World!] precedes [Hello!]
[Hello World!] follows [Hello]
[Hello World!] precedes [Hello there]
[body!] equals [body!]
2、复制字符串
char* strcpy(char*s1,const char*s2);
复制 s2 所指向的空终止字节字符串,包含空终止符,到首元素为 s1 所指的字符数组。
注意要点:
- s1 的大小要大于 s2 的大小,以防止截断;
- 可以使用安全的 strcpy_s()函数;
3、拼接字符串
char *strcat(char*s1,const char*s2);
此函数把第二个字符串拼接到第一个的结尾,第二个字符串是以常量char指针的形式传递的。
注意要点:
- 函数不会分配内存,这意味着第一个字符串必须足够长,能容纳拼接指针和字符串后的结果,否则函数可能会越界写入,导致不可预期的行为。
- 函数的返回值的地址跟第一个参数的地址一样。这在某些情况下比较方便,比如这个函数作为printf函数的参数时。
三、传递字符串
1、传递简单字符串
size_t stringLength(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",stringLength(simplePtr));
printf("%d\n",stringLength(simpleArray));
printf("%d\n",stringLength(&simpleArray));
printf("%d\n",stringLength(&simpleArray[0]));
程序内存示意图:
2、传递字符常量的指针
传递字符常量就是为了安全;防止程序非法修改字符串
就像字符串函数 strcat;
char *strcat(char*s1,const char*s2);
char* strcpy(char*s1,const char*s2);
这个s2就是源字符串;s1就是目的字符串
标准库里面为了安全就是 使用 const 来防止源字符串被修改
3、传递需要初始化的字符串
有些情况下我们想让函数返回一个由该函数初始化的字符串。假设我们想传递一个
部件的信息,比如名字和数量,然后让函数返回表示这个信息的格式化字符串。通
过把格式化处理放在函数内部,我们可以在程序的不同部分重用这个函数。
不过,我们得决定是给函数传递一个空缓冲区让它填充并返回,还是让函数动态分
配缓冲区并返回。
要传递缓冲区,需要一些条件:
- 必须传递缓冲区的地址和长度【定位字符串地址,防止访问越界】
- 调用者负责释放缓冲区【防止内存泄漏】
- 函数通常返回缓冲区的指针
4、给应用程序传递参数
main函数通常是应用程序第一个执行的函数。对基于命令行的程序来说,通过为其
传递信息来打开某种行为的开关或控制某种行为很常见。可以用这些参数来指定要
处理的文件或是配置应用程序的输出。比如说,Linux的 ls 命令会基于接收到的参
数列出当前目录下的文件。
C用传统的argc和argv参数支持命令行参数。第一个参数argc,是一个指定传递
的参数数量的整数。系统至少会传递一个参数,这个参数是可执行文件的名字。第二
个参数argv,通常被看做字符串指针的一维数组,每个指针引用一个命令行参数。
下面的main函数只是简单地列出了它的参数,每行一个。在这个版本中,argv被
声明为一个char指针的指针。
int main(int argc, char** argv)
{
for(int i=0;i<argc;i++)
{
printf("argv[%d]%s\n",i,argv[i]);
}
}
程序可以用下面的命令执行:
process.exe-f names.txt limit=12 -verbose
输出如下:
argv[0] c:/process.exe
argv[1] -f
argv[2] names.txt
argv[3] limit=12
argv[4] -verbose
四、返回字符串
函数返回字符串时,它返回的实际是字符串的地址。这里应该关注的主要问题是如
何返回合法的地址,要做到这一点,可以返回以下三种对象之一的引用:
- 字面量
- 动态分配的内存
- 本地字符串变量
1、返回字面量的地址
这个很好理解,举例下面的写法,就是返回的字面量
char* returnALiteral(int code)
{
switch(code)
{
case 100:
return"Boston Processing Center";
case 200:
return"Denver Processing Center";
case 300:
return "Atlanta Processing Center";
case 400:
return"San Jose Processing Center";
}
}
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;
}
//调用
char*tmp=blanks(5);
程序内存示意图:
释放返回的内存是函数调用者的责任,如果不再需要内存但没有将其释放会造成内存泄漏。下面是一个内存泄漏的例子,printf函数中使用了字符串,但是接着它的地址就丢失了,因为我们没有保存:
printf("[%s]\n",blanks(5));
一个更安全的方法如下所示:
//这里的tmp就保存这个地址,可能后面还需要操作
char*tmp=blanks(5);
printf("[%s]\n",tmp);
//其他操作.........
free(tmp);
注意要点:
- 返回局部字符串的地址可能会有问题,如果内存被别的栈帧覆写就会损坏。会读取错误
- 返回静态成员没问题,但是没有堆对象灵活。