2017年4月8日 星期六

C語言 字元char與字串 差異與詳解

C語言 字元char與字串 差異與詳解

初學C語言的時候遇到字串總是感到莫名其妙與不知道該怎麼用,比如說為什麼字串不能直接接上,或直接相等置換等等問題,大致列舉了一些常見的錯誤與解法。

常見的錯誤

先說一個幾乎都會誤會的點,把字串當作型態,實際上字串是字元的陣列,這麼說可能沒感覺,就是大家明明都知道。
int arr[3], arr2[3];
arr1 = arr2;
這樣是不可行的,卻還是認為
char* s="123";
char s2[4];

s2=s;
這樣是可行的,然後就採了一堆坑…
待會還是會提到,下面就開始提提有哪些該注意的地方

指標與陣列的關係與差異

隱式轉換

陣列是陣列、指標是指標
只不過在大多數的時候陣列會自動轉換成指標,操作起來就像個指標一樣。具體差異在哪裡,並不太容易敘述清楚,下面會簡單的舉個例子說明不可互相替代。

指標宣告與陣列宣告

一般來說常數是存在唯讀記憶體內,比如說一個副程式的呼叫
fun("ABC");
那個”ABC”不能夠被更動,是帶const屬性的,宣告的時候也一樣
char* str="ABC";
這樣子的方式會建立暫存,並返回指標,讓str指向那個暫存的唯獨空間
所以當你嘗試修改他時,是非法的。
char* str="ABC";
str[0]='0';
如果我們使用字串的話則會不同的情況發生
char str[]="ABC";
創建一個陣列,並將其內容初始化,這種情況下我們才可以正常的存取,從這裡可以看出來,除非你很確定我不會動到他,否則還是盡量使用
char str[]="ABC";
避免自己採坑了,之後不小心修改到發生錯誤


為什麼不能直接相等傳遞

char str1[]="ABC";
char str2[]="DEF";

str1 = str2; //Error
就像整數陣列一樣沒辦法直接等號過去,很容易誤把字串當成是一種變數叫字串,實際上他是字元的陣列。
換個方式寫你可能就有感覺了
int arr1[]={1, 2, 3};
int arr2[]={3, 2, 1};

arr1 = arr2; // Error

另外這裡也可以看出陣列與指標的不同之處
int arr1[]={1, 2, 3}, *p1=arr1;
int arr2[]={3, 2, 1}, *p2=arr2;

p1=p2; // is ok

只能利用for迴圈一個一個搬移過去
char str1[]="ABC";
char str2[]="DEF";

for (int i=0; i<3 ; ++i){
    str1[i]=str2[i];
}
你也可以使用內建的函式搬移
#include <stdio.h>
#include <string.h>

int main (){
  char str1[]="Sample string";
  char str2[40];
  char str3[40];
  strcpy (str2,str1);
  strcpy (str3,"copy successful");
  printf ("str1: %s\nstr2: %s\nstr3: %s\n",str1,str2,str3);
  return 0;
}


為什麼不能用+的

同上原因,陣列也沒辦法直接用加的,除此之外還要注意一個問題,長度是否足夠容下相加後
char str1[]="ABC";
char str2[]="ABC";
上述的作法長度會是3,可是相加後他們會變成6,你必須有一個足夠長的陣列,比如說將第二個長度拉長兩倍
char str1[]="ABC";
char str2[]="ABC___";

str2[3] = str1[0]
str2[5] = str1[1]
str2[4] = str1[2]
字串的相接也有函式可以使用
strcat (str2, str1);


字串殘留上一次的字元、清空與初始化字串

比如說這樣的範例,試圖讓副函式操作字串
/*****************************************************************
Name : 
Date : 2017/04/08
By   : CharlotteHonG
Final: 2017/04/08
*****************************************************************/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

void fun(char* str, char* str2){
    for(int i = 0; i < strlen(str2); ++i) {
        str[i]=str2[i];
    }
    printf("%s\n", str);
}
/*==============================================================*/
int main(int argc, char const *argv[]){
    char s[]="ABCDEF", s2[]="abc";
    fun(s, s2);
    return 0;
}
/*==============================================================*/
印出的結果是
abcDEF
主因是不正確的操作字串

必須要知道的是自串一個很特別的地方在於,判斷他的長度或結束點實際上是以 '/0' 做判別的,每個字串的結尾都會有這個符號,可以通過簡單的方法檢測
char* str="ABC";

if(str[3] == '\0')
    printf("End\n");
補上正確的結束符號
void fun(char* str, char* str2){
    int len=strlen(str2);
    for(int i = 0; i < len; ++i) {
        str[i]=str2[i];
    } str[len]='\0';
    printf("%s\n", str);
}
由此可以推斷有一點必須要小心,字串的長度必須是實際長度+1
char str[4]="ABC";


不容易發現的坑

宣告的缺失的結束符

此外有些編譯器這樣寫不會幫你補上 '\0' 以下寫法可能會導致各種問題,找不到結束字元
char str[]="ABC";
手動補上結束字元可以處理這個問題
char str[4]="ABC\0";
這種情況下建議補上長度不要讓編譯器自動幫你計算,如果移植到會幫你補的編譯器,沒有打長度可能會多要一個空間(結尾兩個結束符),不過就算多要了空間也不會造成問題,只是長度多一個而已。

複製時缺失的結束符

此外 string.h 內的複製函式 strncpy() 存在一樣問題,複製的長度如果不足到底,沒有複製完會導致結束符號沒有被複製到
char s[]="ABCDEF";
strncpy(s, "123", 3);
printf("%s\n", s); // s is "123DEF"
輸出的結果s會是 "123DEF" 這可能不是你要的結果,必須小心這個不容易發現的坑;然後一個看似沒問的長度計算也是一個小坑,就算使用長度查找也會因為結尾的字元不會被算進去
char str[4]="ABC\0";
printf("len is %d", (int)strlen(str)); // len is 3
仍然會缺少複製結束字元需自行補上+1的長度
char s[]="ABCDEF", s2[]="abc";
strncpy(s, s2, strlen(s2)+1);
printf("%s\n", s); // s is "abc"

越界存取

上面的+1又延伸出一個問題,小心加錯了會導致非法存取,看一下面的例子
char s[]="ABCDEF", s2[3]="abc";
                   // ^ 這裡
strncpy(s, s2, strlen(s2)+1);
printf("%s\n", s); // s is "abc"
看起來好像都一樣,實際上那個
s2[3]="abc"
不存在結束符號之外,s2他的合法操作空間只有3
strlen(s2)+1
返還的長度為3,再加上1為4,strncpy不會幫你檢查越界存取
把s1與s2反過來也是,s2可以被塞超過自己的長度,然後編譯不一定會出錯,但是已經是非法存取了,只是運氣好沒炸。

沒有留言:

張貼留言