字元、字串、字元陣列、字串指標變數
字元和字串的區別
字元
如下,定義char
,每一個字元一般情況下佔用8個位元組。
char c, ch;
字串
例如"helloworld";
注意
' '
和" "
的區別,後者為常量字串
在C++中,有兩種型別的字串表示形式:
- C-風格字串
- C++引入的string類
C-風格字串
C 風格的字串起源於 C 語言,並在 C++ 中繼續得到支援。字串實際上是使用null
字元'\0'
終止的一維字元陣列。因此,一個以null
結尾的字串,包含了組成字串的字元。
null
字元對應的ASCII
為0。
下面的宣告和初始化建立了一個 “Hello” 字串。由於在陣列的末尾儲存了空字元,所以字元陣列的大小比單詞 “Hello” 的字元數多一個。
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
其實,您不需要把null
字元放在字串常量的末尾。C++ 編譯器會在初始化陣列時,自動把'\0'
放在字串的末尾。所以也可以利用下面的形式進行初始化
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*
與許多相同點,代表字元陣列,可以對應一個字串,如
char * a="string1";
char b[]="string2";
這裡a是一個指向char變數的指標,b則是一個char陣列(字元陣列)
字元陣列(char [])和字串指標變數(char *)的區別
以下幾個表達是等價的
#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;
}
又例如如下程式碼:
#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";
定義時可以寫為:
char *ps;
ps="C Language";
而對陣列方式: static char st[]={"C Language"};
只能對字元陣列的各元素逐個賦值,不能寫為:
char st[20];
st={"C Language"};
這是因為,字串指標變數只是一個指向字串首地址的指標變數,我們可以對指標變數進行賦值,確定其指向的地址空間;
而字串陣列在定義時便在記憶體中為其分配了空間,也就是說,我們不能隨意的改變這個陣列的地址。而"C Language"
是有新的地址的。
改變值的方法
“abcd”是文字常量區分配了記憶體儲存的,棧上分配一地址給p1並指向“abcd”,那麼如果在後面的程式碼中改變了“abcd”,自然崩潰。所以,需要加上const
限定。
但是說到底,為什麼改變p1中的內容就是危險的,字元陣列的內容就能隨意改變呢?這是因為“abcd”是在編譯時刻就確定的,而“1234”是在執行時刻賦值的。所以,編譯器在編譯時就已經知道p1指向的是常量,他並不希望你改變,但是陣列不同,可以說他只是個儲存的工具,編譯器編譯時並不知道它裡面是什麼。
總結如下:
C++中使用char*
定義字串,不能改變字串內的字元的內容,但卻可以把另外一個字串(新地址)賦值給它,即p1是一個char型指標變數,其值(指向)可以改變;此時,若指向的新地址為字串陣列的地址,則可更改字串中的內容
#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";
;但是case2
和case3
中的pstr
更改了指向,即操作的是char a[]
。
但是對於字串陣列而言,改變字串內的字元的內容,但是char[]
是常量,值不能改變!:
char greeting2[] = "Hello";
greeting2[2] = '0';
greeting2 = "Hell"; // 報錯
printf("Greeting2 message: %s\n", greeting2 );
理解參考上述的,字串陣列在定義時便在記憶體中為其分配了空間,也就是說,我們不能隨意的改變這個陣列的地址。
但在往後的存取中,在棧上的陣列比指標所指向的字串是要快的。
還網上找到如下程式碼,很詳細
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"優化成一個地方。
}
佔用儲存空間
#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*指向的區域有時可寫,有時只讀
比如:
char * a="string1";
char b[]="string2";
gets(a); //試圖將讀入的字串儲存到a指向的區域,執行崩潰!
gets(b) //OK
解釋: a指向的是一個字串常量,即指向的記憶體區域只讀;
b始終指向他所代表的陣列在記憶體中的位置,始終可寫!
注意,若改成這樣gets(a)就合法了:
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 *
本身是一個字元指標變數,但是它既可以指向字串常量,又可以指向字串變數,指向的型別決定了對應的字串能不能改變!
初始化操作
測試程式碼:
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);
結果:
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個操作:
- 宣告一個char*變數(也就是聲明瞭一個指向char的指標變數);
- 在記憶體中的文字常量區中開闢了一個空間儲存字串常量”string1”
- 返回這個區域的地址,作為值,賦給這個字元指標變數a
最終的結果:指標變數a指向了這一個字串常量“string1”
(注意,如果這時候我們再執行:char * c=”string1”;則,c==a,實際上,只會執行上述步驟的1和3,因為這個常量已經在記憶體中建立)
char b[]=”string2”;則是實現了2個操作:
- 宣告一個char 的陣列,
- 為該陣列“賦值”,即將”string2”的每一個字元分別賦值給陣列的每一個元素
最終的結果:“陣列的值”(注意不是b的值)等於”string2”,而不是b指向一個字串常量
實際上, char * a=”string1”; 的寫法是不規範的!
因為a指向了即字元常量,一旦strcpy(a,"string2")
就糟糕了,試圖向只讀的記憶體區域寫入,程式會崩潰的!儘管VS下的編譯器不會警告,但如果你使用了語法嚴謹的Linux下的C編譯器GCC,或者在windows下使用MinGW編譯器就會得到警告。
所以,我們還是應當按照”型別相同賦值”的原則來寫程式碼:
const char * a="string1";
保證意外賦值語句不會通過編譯
另外,關於char*
和char[]
在函式引數中還有一個特殊之處,執行下面的程式碼
#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[]的區別:
const char * a=”string1”
char b[]=”string2”;
二者的區別在於:
a
是const char *
型別,b
是char* const
型別 (或者理解為(const char)*xx
和char* (const xx)
)a
是一個指標變數,a
的值(指向)是可以改變的,但a
只能指向(字串)常量,指向的區域的內容不可改變;b
是一個指標常量,b
的值(指向)不能變;但b
指向的目標(陣列b
在記憶體中的區域)的內容是可變的- 作為函式的宣告的引數的時候,
char []
是被當做char *
來處理的!兩種形參宣告寫法完全等效!
指標常量,常量指標
指標常量
什麼是指標常量?指標常量即指標型別的常量。
例:
#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;
}
常量指標
什麼是常量指標?常量指標即是指向常量的指標,指標的值可以改變,指標所指的地址中的內容為常量不能改變,
例:
const char *name1="John";
char s[]="abc";
name1=s; //正確,name1存放的地址可以改變
char * name2= name1; //不可以,因為name2 和 name1存放的是同一塊地址,如果name2地址中的內容改了,則name1的內容也改了,那麼name1就不再是指向常量的指標了。
字串型別之間的轉換: string、const char*、 char* 、char[]
相互轉換
轉換表格
源格式->目標格式 | string | char* | const char* | char[] |
---|---|---|---|---|
string | NULL | 直接賦值 | 直接賦值 | 直接賦值 |
char* | strcpy | NULL | const_cast | char*=char |
const char* | c_str() | 直接賦值 | NULL | const char*=char; |
char[] | copy() | strncpy_s() | strncpy_s() | NULL |
總結方法
- 變成string,直接賦值。
- char[]變成別的,直接賦值。
- char變constchar容易,const char變char麻煩。
<const_cast><char*>(constchar*);
- string變char要通過const char中轉。
- 變成char[]。string逐個賦值,char* const char* strncpy_s()。
程式碼示例
const char*
和 char*
之間的轉換
const char*
是指向常量的指標,而不是指標本身為常量,可以不被初始化.該指標可以指向常量也可以指向變數,只是從該指標的角度而言,它所指向的是常量,通過該指標不能修改它所指向的資料.
const char*
轉為char*
const char*
是不能直接賦值到char*
的,這樣編譯都不能通過,理由:假如可以的話,那麼通過char*
就可以修改const char
指向的內容了,這是不允許的.所以char*
要另外開闢新的空間。
#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*
直接賦值就可以了
const char* cpc;
char* pc="abcde";
cpc=pc;
string轉為其他型別
①、string轉const char*
#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*
#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[]
#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
#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
#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[]
#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
#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
#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[]
#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[]轉為其他型別
#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的型別。
#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:
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:
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。
如下面:
char* parr = new char[10];
parr = "hello";
printf("%d", strlen(parr));
執行結果為5
。
參考
Sizeof與Strlen的區別與聯絡
C++ 字串與字元陣列 詳解
字元陣列和字串的區別
c/c++中string與char的區別
const char* 和char* 之間的轉換
One more thing
更多關於人工智慧、Python、C++、計算機等知識,歡迎訪問我的個人部落格進行交流, 點這裡~~