【原創】淺談指標(七)字串相關(詳細版本)與指標運算

計算機知識雜談發表於2021-11-13

本文原創,僅在部落格園釋出,若在其他平臺發現均為盜取!!!

-1、寫作目的

昨天我寫過一個版本的字串相關,淺談指標系列:https://www.cnblogs.com/jisuanjizhishizatan/p/15545229.html
這篇文章由於時間倉促,寫的比較水,因此今天有時間我來重置一篇,稍微寫詳細一點。

0、前言

字串一直是C++的一個難點。昨天某個同學問我,關於字串的比較問題:

char s[10];
...
if(s=="123456789")...

沒有語法錯誤,但是執行結果不太對。
大家可以在這個地方停一停,想想是為什麼。

1、字串的比較

1.1.歷史原因

最初,C語言剛剛面世的時候,還沒有我們現在經常寫C++時使用的string型別。當時的人們使用字串,必須使用char的陣列模擬字串。
例如,輸出hello world,是這樣寫的:

char s[]={'H','e','l','l','o',' ','w','o','r','l','d','\0'};
for(int i=0;i<11;i++)putchar(s[i]);

這樣的寫法是很麻煩的,對此,C語言使用了""運算子,以及printf的%s引數。

char s[]={"Hello world"};
printf("%s",s);

這裡注意,""運算子,返回的是一個char陣列,如果在C++這樣寫:

string s="hello";

其本質是string類的運算子過載,過載了賦值運算子=。而並非表示""返回的是string型別。

1.2.陣列

我之前應該寫過(不過我忘記在哪篇裡面提到過),陣列名在表示式中,某些情況是可以解釋做指標的。
也就是說,例如:

int a[10];
int *p;
p=a;

在這裡,a表示指標,也就是陣列a的首個元素的地址。
因此,執行這樣的比較:

s=="123456789"

本質是對s的地址和"123456789"的地址進行比較。
我們來看一個簡單的程式:

#include<bits/stdc++.h>
using namespace std;
char str[3];
int main(){
  printf("%p\n",str);
  printf("%p\n","abc");
  printf("%d\n",sizeof("abc"));
}

輸出在我的Dev c++環境內顯示是:

我們可以看到,"abc"字串常量也是儲存在記憶體中的,儲存在只讀儲存區中。並且,sizeof("abc")返回5,表示"abc"的本質是陣列(而不是char的指標,否則會返回4)

1.3.比較函式

既然直接比較s和字串常量會比較地址,那麼,我們只能逐字元進行比較了。
標準庫有個函式叫做strcmp,這個函式我們在下一章討論。

2、strcmp函式

2.1.函式規範

很多字串處理的函式都包含在<string.h>中。strcmp就是這樣的,使用前需要#include<string.h>。
strcmp用於比較字串的大小,只能比較char陣列,返回值如下:

strcmp(a,b);
/*
a=b 返回0
a<b 返回負數
a>b 返回正數
*/

對於部分處理環境,a<b返回-1,a>b返回1,但是如果僅僅對1和-1判斷,在部分環境還是過不去的,因為標準庫沒有這樣規定。

2.2.自制strcmp函式

int strcmp(char *s1,char *s2){
	while(*s1==*s2 && *s1 && *s2){
		s1++;s2++;
	}
	return *s1-*s2;
}

上面的strcmp函式是我自己寫的,其中使用了簡單的指標運算。
在這裡,s1和s2就是s1和s2指向的字元,如果字元相等,且兩個字元不為0,那麼就繼續進行比較,也就是*s1 && *s2的含義。
s1-s2就是對字元作差,也就是對字元進行比較,如果大於返回正數,小於則返回負數。

2.3.指標運算

實際上,對指標進行加減運算(例如s1++一類的語句)叫做指標運算。在有些情況下,指標運算是晦澀難懂的,例如:

while(*p++);

相信大家都沒看懂這句話是什麼意思,實際這是strlen的實現程式碼中的一句,應該都沒想到吧。
下面是我仿照的strlen實現,不太容易看懂的版本

int strlen(char *s){
  char *p=s;
  while(*p++);
  return p-s-1;
}

那麼,我們為什麼需要指標運算呢?首先,在很久以前的C語言中,遍歷陣列,需要使用到如下的語句:

for(int i=0;i<n;i++){
  //對a[i]進行操作
}

之前也一定提到過,a[i]是*(a+i)的縮略形式,那麼,在迴圈體內,編譯器需要計算多次a+i。
但是,如果使用指標運算:

for(int *p=a;*p!=a+n;p++){
  //對p進行操作
}

這樣,使用p來代替陣列中多次出現的a[i],a+i中的加法運算只會在結束時執行一次。
因此,在以前的C語言中,使用指標運算可以加快程式的執行速度。但是,現在已經不是這樣了。以下引用自《征服C指標》:

如今,編譯器在不斷地被優化,對於迴圈內部重複出現的表示式的集中處理,是編譯器優化的基本內容。對於現在一般的 C 編譯器,無論你使用陣列還是指標,效率上都不會出現明顯的差距。基本上都是輸出完全相同的機器碼。
總的來說,C 的指標運算功能的出現,源自於早期的 C 自身沒有優化手段。這一點並不奇怪,請大家回想一下在前面介紹過的內容,C 本來只是為了解決開發現場的人們眼前的問題而出現的一種語言。Unix 之前的 OS 幾乎都是使用匯編寫的,即使晦澀難懂,人們也不會大驚小怪。對於當時的環境,追求什麼編譯器優化實在有點勉為其難。因此,當初開發 C語言的時候,是完全有必要提供指標運算功能的。可是……

這應該就是大家認為“指標很難懂”或是“指標容易出錯”的根本原因,都是複雜的指標運算所致的。
當然,凡事都有特例,比如,在“一個巨大的 char 陣列中,參雜了各種型別的資料,並且我們試圖讀取第多少位元組的資料”這樣的情況下,還是使用指標運算寫的程式比較容易理解。
無論如何,作為一個C++的程式設計師,我們應該學會閱讀一些基本的指標運算。至於寫指標運算,在非特殊情況下,我們還是一般不要使用指標運算,這樣會降低程式的可讀性。

3、strlen函式

3.1.函式規範

strlen(s)返回s的長度,不計'\0',只能用於char陣列。

3.2.自制strlen

int strlen(char *s){
  int ret;
  for(ret=0;s[ret]!='\0';ret++);
  return ret;
}

之前那個指標運算的strlen可能比較難懂,我們使用陣列再寫一個出來。這次,我們只對ret進行加法運算,如果s[ret]等於結束符'\0'表示字串結束,那麼就把他作為迴圈的條件。這樣使用for迴圈來寫,可能更加容易懂一些。

4、strcpy函式

4.1.函式規範

如果想要對字串進行拷貝,例如把char陣列的字串s賦值為"abc",那麼,

s="abc";

一句,如果s是陣列將會報錯,如果s為指標,那麼會使得s指向字串常量"abc",從而導致s是不可變的。
那麼,我們需要使用strcpy函式:

strcpy(s,"abc");

4.2.自制strcpy

void strcpy(char *dest,const char *src){
	int i=0;
	while(src[i]!='\0'){
		dest[i]=src[i];
		++i;
	}
}

這次我們同樣沒有使用指標運算。事實上,不使用指標運算的程式,在長度上並沒有很長,使用指標運算也不太能夠使程式簡潔。

5、wchar_t

wchar_t是C95新增的一個功能,表示寬字元,可以儲存中文。對應的字串函式,需要使用wcscpy,wcslen等函式,使用方法與char類似。
這一部分只是簡略提到一下,例如如下的程式:

#include<bits/stdc++.h>
int main(){
  wchar_t s[]= L"中文";
  for(int i=0;i<wcslen(s);i++)printf("%d ",s[i]);
}

輸出ascii碼。注意,對於DEV C++編譯器,需要調整編譯選項,不然報錯:

今天我們講解了幾個常見的C語言字串函式,以及指標運算。
完。

相關文章