字符、字符串、字符数组、字符串指针变量

字符和字符串的区别

字符

如下,定义char,每一个字符一般情况下占用8个字节。

1
char c, ch;

字符串

例如"helloworld";

注意' '" "的区别,后者为常量字符串

在C++中,有两种类型的字符串表示形式:

  • C-风格字符串
  • C++引入的string类

C-风格字符串

C 风格的字符串起源于 C 语言,并在 C++ 中继续得到支持。字符串实际上是使用null字符'\0'终止的一维字符数组。因此,一个以null结尾的字符串,包含了组成字符串的字符。

null字符对应的ASCII为0。

下面的声明和初始化创建了一个 “Hello” 字符串。由于在数组的末尾存储了空字符,所以字符数组的大小比单词 “Hello” 的字符数多一个。

1
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};

其实,您不需要把null字符放在字符串常量的末尾。C++ 编译器会在初始化数组时,自动把'\0'放在字符串的末尾。所以也可以利用下面的形式进行初始化

1
char greeting[] = "Hello";

以下是 C/C++ 中定义的字符串的内存表示:

C++ 中有大量的函数用来操作以null结尾的字符串:

| 序号 | 函数 | 功能 |
| | —: |
| 1 | strcpy(s1,s2) | 复制字符串 s2 到字符串 s1 |
| 2 | strcat(s1,s2) | 连接字符串 s2 到字符串 s1 的末尾 |
| 3 | strlen(s1) | 返回字符串 s1 的长度 |
| 4 | strcmp(s1,s2) | 返回s1与s2的比较结果 |
| 5 | strchr(s1,ch) | 返回一个指针,指向字符串s1中字符ch的第一次出现的位置 |
| 6 | strstr(s1,s2) | 返回一个指针,指向字符串s1中s2的第一次出现的位置 |

C++ 中的 String 类

C++ 标准库提供了 string 类类型,支持上述所有的操作,另外还增加了其他更多的功能。比如:

  • append() – 在字符串的末尾添加字符
  • find() – 在字符串中查找字符串
  • insert() – 插入字符
  • length() – 返回字符串的长度
  • replace() – 替换字符串
  • substr() – 返回某个子字符串

4种字符串类型

C++中的字符串一般有以下四种类型,

  • string
  • char*
  • const char*
  • char[]

下面分别做简单介绍,并说明其中的一些区别

string

string是一个C++类库中的一个类,它位于名称空间std中,因此必须使用using编译指令或者std::string来引用它。它包含了对字符串的各种常用操作,它较char*的优势是内容可以动态拓展,以及对字符串操作的方便快捷,用+号进行字符串的连接是最常用的操作。

关于string的更多性质可以参考下面关于char *的讨论。

char*

char*指向字符串的指针(其实严格来说,它是指向字符串的首个字母),你可以让它指向一串常量字符串。

const char*const char *

该声明指出,指针指向的是一个const char类型,即不能通过当前的指针对字符串的内容作出修改

注意这里有两个概念:

  • char * const [指向字符的静态指针]
  • const char * [指向静态字符的指针]

前者const修饰的是指针,代表不能改变指针
后者const修饰的是char,代表字符不能改变,但是指针可以变,也就是说该指针可以指针其他的const char。

char[]

char*与许多相同点,代表字符数组,可以对应一个字符串,如

1
2
char * a="string1";
char b[]="string2";

这里a是一个指向char变量的指针,b则是一个char数组(字符数组)

字符数组(char [])和字符串指针变量(char *)的区别

以下几个表达是等价的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

int main ()
{
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
printf("Greeting message: %s\n", greeting );

char greeting1[6] = "Hello"; //must over 5
//greeting2[1] = '1';
printf("Greeting1 message: %s\n", greeting1 );

char greeting2[] = "Hello";
printf("Greeting2 message: %s\n", greeting2 );

char *greeting3 = "Hello"; // can't change
printf("Greeting3 message: %s\n", greeting3 );

return 0;
}

又例如如下代码:

1
2
3
4
5
6
7
8
#include<iostream>
using namespace std;
int main()
{
char *p1 = "abcd";
char p2[] = "1234";
return 0;
}

这二者的区别还在于:

存储方式

p1是一个指针变量,有一块内存存储它,它的内容是字符串的地址,而字符串本身是存放在以该首地址为首的一块连续的内存空间中并以'\0'作为串的结束。那么我们要访问字符串就先要取出p1中存储的地址,然后计算偏移量,进行访问;而p2是字符数组,是由于若干个数组元素组成的,它可用来存放整个字符串。

访问方式

不同于p1,p2直接是字符串的地址,直接访问就行了

定义方法

对字符串指针方式 char *ps="C Language"; 定义时可以写为:

1
2
char *ps;
ps="C Language";

而对数组方式: static char st[]={"C Language"}; 只能对字符数组的各元素逐个赋值,不能写为:

1
2
char st[20];
st={"C Language"};

这是因为,字符串指针变量只是一个指向字符串首地址的指针变量,我们可以对指针变量进行赋值,确定其指向的地址空间;

而字符串数组在定义时便在内存中为其分配了空间,也就是说,我们不能随意的改变这个数组的地址。而"C Language"是有新的地址的。

改变值的方法

“abcd”是文字常量区分配了内存存储的,栈上分配一地址给p1并指向“abcd”,那么如果在后面的代码中改变了“abcd”,自然崩溃。所以,需要加上const限定。

但是说到底,为什么改变p1中的内容就是危险的,字符数组的内容就能随意改变呢?这是因为“abcd”是在编译时刻就确定的,而“1234”是在运行时刻赋值的。所以,编译器在编译时就已经知道p1指向的是常量,他并不希望你改变,但是数组不同,可以说他只是个存储的工具,编译器编译时并不知道它里面是什么。

总结如下:

C++中使用char*定义字符串,不能改变字符串内的字符的内容,但却可以把另外一个字符串(新地址)赋值给它,即p1是一个char型指针变量,其值(指向)可以改变;此时,若指向的新地址为字符串数组的地址,则可更改字符串中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<iostream>

using namespace std;

int main()
{
//case1
char* pstr = "hello world";
pstr = "aa"; //改变了pstr的指向,但是所指空间仍然不可改变
//pstr[1] = "a"; //报错
//pstr[1] = 'a'; //报错

//case2
char a[] = "hello";
pstr = a;
//pstr[1] = "a"; // error: invalid conversion from 'const char*' to 'char' [-fpermissive]|
pstr[1] = 'a';
cout<<pstr<<endl;
pstr = "case2";
cout<<pstr<<endl;

//case3
char* pstr1;
pstr1 = a;
pstr1[1] = 'a';
cout<<pstr1<<endl;
pstr1 = "case3";
cout<<pstr1<<endl;
}

运行结果

解释:上述case1中赋值操作char* pstr = "hello world";相当于const char* pstr = "hello world";;但是case2case3中的pstr更改了指向,即操作的是char a[]

但是对于字符串数组而言,改变字符串内的字符的内容,但是char[]是常量,值不能改变!:

1
2
3
4
char greeting2[] = "Hello";
greeting2[2] = '0';
greeting2 = "Hell"; // 报错
printf("Greeting2 message: %s\n", greeting2 );

理解参考上述的,字符串数组在定义时便在内存中为其分配了空间,也就是说,我们不能随意的改变这个数组的地址。

但在往后的存取中,在栈上的数组比指针所指向的字符串是要快的。

还网上找到如下代码,很详细

1
2
3
4
5
6
7
8
9
10
11
12
int a=0; //全局初始化区
char *p1; //全局未初始化区
main()
{
int b; //栈
char s[]="abc"; //栈
char *p2; //栈
char *p3="123456"; //123456\0在常量区,p3在栈上。
static int c=0; //全局(静态)初始化区
p2 = (char*)malloc(20); //分配得来得10和20字节的区域就在堆区。
strcpy(p1,"123456"); //123456\0放在常量区,编译器可能会将它与p3所向"123456"优化成一个地方。
}

占用存储空间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <string.h>

int main()
{
char c1[] = "helloworld";
char *c2 = "helloworld";

printf("c1:%s\n", c1);
printf("c2:%s\n", c2);
printf("sizeof: %d %d\n", sizeof(c1), sizeof(c2));
printf("strlen: %d %d\n", strlen(c1), strlen(c2));
return 0;
}

“helloword”一共10个字符,所以strlen的值都为10;

差别体现在sizeof的值。用字符串数组定义的”helloword”占11个字节,是因为”helloword”加上结尾的”\0”一共十一个char型字符,每个char型字符占1个字节;

而用字符串指针变量定义时,sizeof的值仅为4个字节,这是因为s2是一个指针,在32位系统中,地址占4个字节。

对应内存的可读写性

char[]对应的内存区域总是可写,char*指向的区域有时可写,有时只读
比如:

1
2
3
4
char * a="string1";
char b[]="string2";
gets(a); //试图将读入的字符串保存到a指向的区域,运行崩溃!
gets(b) //OK

解释: a指向的是一个字符串常量,即指向的内存区域只读;
b始终指向他所代表的数组在内存中的位置,始终可写!

注意,若改成这样gets(a)就合法了:

1
2
3
4
5
char * a="string1";
char b[]="string2";
a=b; //a,b指向同一个区域
gets(a) //OK
printf("%s",b) //会出现gets(a)时输入的结果

解释: a的值变成了是字符数组首地址,即&b[0],该地址指向的区域是char *或者说 char[8],习惯上称该类型为字符数组,其实也可以称之为“字符串变量”,区域可读可写。

总结char *本身是一个字符指针变量,但是它既可以指向字符串常量,又可以指向字符串变量,指向的类型决定了对应的字符串能不能改变!

初始化操作

测试代码:

1
2
3
4
5
char *a="Hello World"; 
char b[]="Hello World";
printf("%s, %d\n","Hello World", "Hello World");
printf("%s, %d %d\n", a, a, &a);
printf("%s, %d %d\n", b, b, &b);

结果:

1
2
3
Hello World,13457308
Hello World,13457308 2030316
Hello World,2030316 2030316

结果可见:尽管都对应了相同的字符串,但”Hellow World”的地址 和a对应的地址相同,与b指向的地址有较大差异;&a 、&b都是在同一内存区域,且&b==b

根据c内存区域划分知识,我们知道,局部变量都创建在栈区,而常量都创建在文字常量区,显然,a、b都是栈区的变量,但是a指向了常量(字符串常量),b则指向了变量(字符数组),指向了自己(&b==b==&b[0])。

说明以下问题:
char * a=”string1”;是实现了3个操作:

  1. 声明一个char*变量(也就是声明了一个指向char的指针变量);
  2. 在内存中的文字常量区中开辟了一个空间存储字符串常量”string1”
  3. 返回这个区域的地址,作为值,赋给这个字符指针变量a

最终的结果:指针变量a指向了这一个字符串常量“string1”
(注意,如果这时候我们再执行:char * c=”string1”;则,c==a,实际上,只会执行上述步骤的1和3,因为这个常量已经在内存中创建)

char b[]=”string2”;则是实现了2个操作:

  1. 声明一个char 的数组,
  2. 为该数组“赋值”,即将”string2”的每一个字符分别赋值给数组的每一个元素

最终的结果:“数组的值”(注意不是b的值)等于”string2”,而不是b指向一个字符串常量

实际上, char * a=”string1”; 的写法是不规范的!

因为a指向了即字符常量,一旦strcpy(a,"string2")就糟糕了,试图向只读的内存区域写入,程序会崩溃的!尽管VS下的编译器不会警告,但如果你使用了语法严谨的Linux下的C编译器GCC,或者在windows下使用MinGW编译器就会得到警告。

所以,我们还是应当按照”类型相同赋值”的原则来写代码:

1
const char * a="string1";

保证意外赋值语句不会通过编译

另外,关于char*char[]在函数参数中还有一个特殊之处,运行下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>

void fun1(char *p1, char p2[]){
printf("%s %d %d\n",p1,p1,&p1);
printf("%s %d %d\n",p2,p2,&p2);
p2 = "asdf"; //通过! 说明p2不是常量!
printf("%s %d %d\n",p2,p2,&p2);
}
int main(){
char a[] = "Hello";
fun1(a,a);

printf("\n");
char *b = "hello";
fun1(b, b);
return 0;
}

运行结果:

结果出乎意料!上面结果表明p2这时候根本就是一个指针变量!
结论是:作为函数的形式参数,两种写法完全等效的!都是指针变量!

const char*与char[]的区别:

1
2
const char * a=”string1” 
char b[]=”string2”;

二者的区别在于:

  1. aconst char *类型, bchar* const类型 (或者理解为(const char)*xxchar* (const xx)
  2. a是一个指针变量,a的值(指向)是可以改变的,但a只能指向(字符串)常量,指向的区域的内容不可改变;
  3. b是一个指针常量,b的值(指向)不能变;但b指向的目标(数组b在内存中的区域)的内容是可变的
  4. 作为函数的声明的参数的时候,char []是被当做char *来处理的!两种形参声明写法完全等效!

指针常量,常量指针

指针常量

什么是指针常量?指针常量即指针类型的常量。
例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
int main(){
char *const name1="John";
//name1="abc"; //错误,name1指针,不能变,一个指针类型的变量,存放的是地址,所以不能把'"abc"的地址赋给name1
char * name2= name1; //可以

char a[] = "hello";
char *const name3 = a;
printf("%s\n", name3);
a[1] = 'a';
printf("%s\n", name3);
return 0;
}

常量指针

什么是常量指针?常量指针即是指向常量的指针,指针的值可以改变,指针所指的地址中的内容为常量不能改变,
例:

1
2
3
4
const char *name1="John";
char s[]="abc";
name1=s; //正确,name1存放的地址可以改变
char * name2= name1; //不可以,因为name2 和 name1存放的是同一块地址,如果name2地址中的内容改了,则name1的内容也改了,那么name1就不再是指向常量的指针了。

字符串类型之间的转换: string、const char*、 char* 、char[]相互转换

转换表格

源格式->目标格式stringchar*const char*char[]
stringNULL直接赋值直接赋值直接赋值
char*strcpyNULLconst_castchar*=char
const char*c_str()直接赋值NULLconst char*=char;
char[]copy()strncpy_s()strncpy_s()NULL

总结方法

  1. 变成string,直接赋值。
  2. char[]变成别的,直接赋值。
  3. char变constchar容易,const char变char麻烦。<const_cast><char*>(constchar*);
  4. string变char要通过const char中转。
  5. 变成char[]。string逐个赋值,char const char strncpy_s()。

代码示例

const char*char* 之间的转换

const char*是指向常量的指针,而不是指针本身为常量,可以不被初始化.该指针可以指向常量也可以指向变量,只是从该指针的角度而言,它所指向的是常量,通过该指针不能修改它所指向的数据.

const char*转为char*
const char*是不能直接赋值到char*的,这样编译都不能通过,理由:假如可以的话,那么通过char*就可以修改const char指向的内容了,这是不允许的.所以char*要另外开辟新的空间。

1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;
void main(){
const char* cpc="abcde";
char* pc=new char[100];
strcpy(pc,cpc);
cout<<pc<<endl;
}

char*转为const char*
直接赋值就可以了

1
2
3
const char* cpc;
char* pc="abcde";
cpc=pc;

string转为其他类型

①、string转const char*

1
2
3
4
5
6
7
8
9
10
11
#include "stdafx.h"
#include <iostream>
int _tmain(intargc, _TCHAR* argv[])
{
std::string str = "HelloWorld!"; //初始化string类型,并具体赋值
const char* constc = nullptr; //初始化const char*类型,并赋值为空
constc= str.c_str(); //string类型转const char*类型
printf_s("%s\n", str.c_str()); //打印string类型数据 .c_str()
printf_s("%s\n", constc); //打印const char*类型数据
return 0;
}

②、string转char*

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stdafx.h"
#include <iostream>
int _tmain(intargc, _TCHAR* argv[])
{
std::string str = "HelloWorld!"; //初始化string类型,并具体赋值
char* c = nullptr; //初始化char*类型,并赋值为空
const char* constc = nullptr; //初始化const char*类型,并赋值为空
constc= str.c_str(); //string类型转const char*类型
c= const_cast<char*>(constc); //const char*类型转char*类型
printf_s("%s\n", str.c_str()); //打印string类型数据 .c_str()
printf_s("%s\n",c); //打印char*类型数据
return 0;

}

③、string转char[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "stdafx.h"
#include <iostream>
int _tmain(intargc, _TCHAR* argv[])
{
std::string str = "HelloWorld!"; //初始化string类型,并具体赋值
char arrc[20] = {0}; //初始化char[]类型,并赋值为空
for (int i = 0; i < str.length(); i++) //string类型转char[]类型
{
arrc[i]=str[i];
}
printf_s("%s\n", str.c_str()); //打印string类型数据 .c_str()
printf_s("%s\n", arrc); //打印char[]类型数据
return 0;
}

const char*转为其他类型

①const char*转string

1
2
3
4
5
6
7
8
9
10
11
#include "stdafx.h"
#include <iostream>
int _tmain(intargc, _TCHAR* argv[])
{
const char* constc = "Hello World!"; //初始化const char* 类型,并具体赋值
std::string str; //初始化string类型
str= constc; //const char*类型转string类型
printf_s("%s\n", constc); //打印const char* 类型数据
printf_s("%s\n", str.c_str()); //打印string类型数据
return 0;
}

②const char转char

1
2
3
4
5
6
7
8
9
10
11
#include "stdafx.h"
#include <iostream>
int _tmain(intargc, _TCHAR* argv[])
{
const char* constc = "Hello World!"; //初始化const char* 类型,并具体赋值
char* c = nullptr; //初始化char*类型
c= const_cast<char*>(constc); //const char*类型转char*类型
printf_s("%s\n", constc); //打印const char* 类型数据
printf_s("%s\n", c); //打印char*类型数据
return 0;
}

③const char*转char[]

1
2
3
4
5
6
7
8
9
10
11
#include "stdafx.h"
#include <iostream>
int _tmain(intargc, _TCHAR* argv[])
{
const char* constc = "Hello World!"; //初始化const char* 类型,并具体赋值
char arrc[20] = { 0 }; //初始化char[]类型,并赋值为空
strncpy_s(arrc,constc,20); //const char*类型转char[]类型
printf_s("%s\n", constc); //打印const char* 类型数据
printf_s("%s\n", arrc); //打印char[]类型数据
return 0;
}

char*转为其他类型

①char*转string

1
2
3
4
5
6
7
8
9
10
11
#include "stdafx.h"
#include <iostream>
int _tmain(intargc, _TCHAR* argv[])
{
char* c = "HelloWorld!"; //初始化char* 类型,并具体赋值
std::string str; //初始化string类型
str= c; //char*类型转string类型
printf_s("%s\n", c); //打印char* 类型数据
printf_s("%s\n", str.c_str()); //打印string类型数据
return 0;
}

②char转const char

1
2
3
4
5
6
7
8
9
10
11
#include "stdafx.h"
#include <iostream>
int _tmain(intargc, _TCHAR* argv[])
{
char* c = "HelloWorld!"; //初始化char* 类型,并具体赋值
const char* constc = nullptr; //初始化const char* 类型,并具体赋值
constc= c; //char*类型转const char* 类型
printf_s("%s\n", c); //打印char* 类型数据
printf_s("%s\n", constc); //打印const char* 类型数据
return 0;
}

③char*转char[]

1
2
3
4
5
6
7
8
9
10
11
#include "stdafx.h"
#include <iostream>
int _tmain(intargc, _TCHAR* argv[])
{
char* c = "HelloWorld!"; //初始化char* 类型,并具体赋值
char arrc[20] = { 0 }; //初始化char[] 类型,并具体赋值
strncpy_s(arrc,c,20); //char*类型转char[] 类型
printf_s("%s\n", c); //打印char* 类型数据
printf_s("%s\n", arrc); //打印char[]类型数据
return 0;
}

char[]转为其他类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "stdafx.h"
#include <iostream>
int _tmain(intargc, _TCHAR* argv[])
{
char arrc[20] = "HelloWorld!";//初始化char[] 类型并具体赋值
std::string str; //初始化string
const char* constc = nullptr; //初始化const char*
char*c = nullptr; //初始化char*
str= arrc; //char[]类型转string类型
constc= arrc; //char[]类型转const char* 类型
c= arrc; //char[]类型转char*类型
printf_s("%s\n", arrc); //打印char[]类型数据
printf_s("%s\n", str.c_str()); //打印string类型数据
printf_s("%s\n", constc); //打印const char* 类型数据
printf_s("%s\n", c); //打印char*类型数据
return 0;
}

初始化{0}解释

其中,char ch[80]={0};char ch[80]={"\0"};的效果是一样的,表示,所有的元素都赋值为0,是int的类型。

1
2
3
4
5
6
7
8
#include<stdio.h>
int main()
{
char ch[80]={0};
//char ch[80]={"\0"};
printf("%d %d\n", ch[0],ch[1]);
printf("%s ", ch);
}

运行结果为:

sizeof与strlen的区别与联系

sizeof

sizeof(…)是运算符,在头文件中typedef为unsigned int,其值在编译时即计算好了,参数可以是数组、指针、类型、对象、函数等。

它的功能是:获得保证能容纳实现所建立的最大对象的字节大小。

由于在编译时计算,因此sizeof不能用来返回动态分配的内存空间的大小。实际上,用sizeof来返回类型以及静态分配的对象、结构或数组所占的空间,返回值跟对象、结构、数组所存储的内容没有关系。

具体而言,当参数分别如下时,sizeof返回的值表示的含义如下:

  • 数组——编译时分配的数组空间大小;
  • 指针——存储该指针所用的空间大小(存储该指针的地址的长度,是长整型,应该为4);
  • 类型——该类型所占的空间大小;
  • 对象——对象的实际占用空间大小;
  • 函数——函数的返回类型所占的空间大小。函数的返回类型不能是void。

strlen

strlen(…)是函数,要在运行时才能计算。参数必须是字符型指针(char*)。当数组名作为参数传入时,实际上数组就退化成指针了。

它的功能是:返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的,该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,直到遇到结束符NULL。返回的长度大小不包括NULL

举例

例1:

1
2
3
4
char arr[10] = "What?";
int len_one = strlen(arr);
int len_two = sizeof(arr);
cout << len_one << " and " << len_two << endl;

输出结果为:5 and 10

点评:sizeof返回定义arr数组时,编译器为其分配的数组空间大小,不关心里面存了多少数据。strlen只关心存储的数据内容,不关心空间的大小和类型。

例2:

1
2
3
4
5
char * parr = new char[10];
int len_one = strlen(parr);
int len_two = sizeof(parr);
int len_three = sizeof(*parr);
cout << len_one << " and " << len_two << " and " << len_three << endl;

输出结果:23 and 4 and 1

点评:第一个输出结果23实际上每次运行可能不一样,这取决于parr里面存了什么(从parr[0]开始知道遇到第一个NULL结束);第二个结果实际上本意是想计算parr所指向的动态内存空间的大小,但是事与愿违,sizeof认为parr是个字符指针,因此返回的是该指针所占的空间(指针的存储用的是长整型,所以为4);第三个结果,由于*parr所代表的是parr所指的地址空间存放的字符,所以长度为1。

如下面:

1
2
3
char* parr = new char[10];
parr = "hello";
printf("%d", strlen(parr));

运行结果为5

参考

Sizeof与Strlen的区别与联系
C++ 字符串与字符数组 详解
字符数组和字符串的区别
c/c++中string与char的区别
const char 和char 之间的转换

------ 本文结束------
坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道