1. 程式人生 > >字元、字串、字元陣列、字串指標變數

字元、字串、字元陣列、字串指標變數

字元和字串的區別

字元

如下,定義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";;但是case2case3中的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個操作:

  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編譯器就會得到警告。

所以,我們還是應當按照”型別相同賦值”的原則來寫程式碼:

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”; 

二者的區別在於:

  1. aconst char *型別, bchar* const型別 (或者理解為(const char)*xxchar* (const xx)
  2. a是一個指標變數,a的值(指向)是可以改變的,但a只能指向(字串)常量,指向的區域的內容不可改變;
  3. b是一個指標常量,b的值(指向)不能變;但b指向的目標(陣列b在記憶體中的區域)的內容是可變的
  4. 作為函式的宣告的引數的時候,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

總結方法

  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*要另外開闢新的空間。

#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++、計算機等知識,歡迎訪問我的個人部落格進行交流, 點這裡~~