Phper 學 C 興趣入門 -為什麼有的字串處理這麼難

發表於2019-09-11

需求

假如有這樣的一個需求,有個日期,想要擷取獲得其年份。我們用 php 可以使用explode,也可以使用strtok

$a = "2019-09-10 00:00:00";
echo strtok($a,"-"); // 2019

可能大家對strtok不太熟悉,它的作用是用-來分割$a獲取子串,迴圈呼叫可以達到和explode差不多的效果。具體可以看下官方手冊裡面的 demo https://www.php.net/manual/zh...

實驗

實驗1

我之所以用strtok呢,是因為C 語言裡也有這個函式,這個函式比較“怪”,每一次呼叫,是將字串中找到的-替換為\0,然後返回標記字串的首地址。

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char date[] = "2019-09-10";
    char *tmp   = strtok(date, "-");

    printf("%s,%p\n", tmp, (void *) tmp);   // 2019,0x7ffe8741bdd0
    printf("%s,%p\n", date, (void *) date); // 2019,0x7ffe8741bdd0
    printf("%d,%c\n", date[4], date[4]);    // 0,

    return 0;
}

實驗2

當我們使用char指標來作為字串的初始化時,又會是怎樣呢?

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char *date = "2019-09-10";
    char *tmp  = strtok(date, "-");

    printf("%s,%p\n", tmp, (void *) tmp);   // 2019,0x7ffe8741bdd0
    printf("%s,%p\n", date, (void *) date); // 2019,0x7ffe8741bdd0
    printf("%d,%c\n", date[4], date[4]);    // 0,

    return 0;
}

執行的結果卻是

Segmentation fault

原理

當我們使用指標變數作為左值,雙引號字串作為右值時,背後雙引號的邏輯是:

  • 在只讀區申請記憶體,存放字串
  • 在字串尾加上了'/0'
  • 返回字串的首地址

所以char * date就在棧上存放裡雙引號字串返回的首地址。當使用strtok的時候,通過實驗1可以看到strtok實際是找到的字串替換為\0,也就是說需要修改原字串的。而該字串是在只讀區,不不能修改,所以執行出現了段錯誤。

反過來思考,我們 char date[]陣列通過雙引號初始化的時候又是什麼原理,是不是也是雙引號返回了常量字串首地址,然後再通過迴圈一個個賦值到char陣列裡呢?

實驗3

猜想歸猜想。我們通過實驗來證明下。

#include <stdio.h>

int main(int argc, char const *argv[])
{
    char *str1  = "123";
    char str2[] = {'1','2','3'};
    char str3[] = {"123"};
    char str4[] = "123";
 
    return 0;
}

通過objdump 反彙編可以看到

$ gcc a.c
$ objdump -D a.out
00000000004004ed <main>:
  4004ed:    55                       push   %rbp
  4004ee:    48 89 e5                 mov    %rsp,%rbp
  4004f1:    89 7d cc                 mov    %edi,-0x34(%rbp)
  4004f4:    48 89 75 c0              mov    %rsi,-0x40(%rbp)
  4004f8:    48 c7 45 f8 c0 05 40     movq   $0x4005c0,-0x8(%rbp)
  4004ff:    00
  400500:    c6 45 f0 31              movb   $0x31,-0x10(%rbp)
  400504:    c6 45 f1 32              movb   $0x32,-0xf(%rbp)
  400508:    c6 45 f2 33              movb   $0x33,-0xe(%rbp)
  40050c:    c7 45 e0 31 32 33 00     movl   $0x333231,-0x20(%rbp)
  400513:    c7 45 d0 31 32 33 00     movl   $0x333231,-0x30(%rbp)
  40051a:    b8 00 00 00 00           mov    $0x0,%eax
  40051f:    5d                       pop    %rbp
  400520:    c3                       retq
  400521:    66 2e 0f 1f 84 00 00     nopw   %cs:0x0(%rax,%rax,1)
  400528:    00 00 00
  40052b:    0f 1f 44 00 00           nopl   0x0(%rax,%rax,1)

image.png

$objdump -j .rodata -d 3.out

a.out:     file format elf64-x86-64


Disassembly of section .rodata:

00000000004005b0 <_IO_stdin_used>:
  4005b0:    01 00 02 00 00 00 00 00                             ........

00000000004005b8 <__dso_handle>:
    ...
  4005c0:    31 32 33 00                                         123.

實驗結論

可以看到
第一個變數(黃色框)初始化是傳入了一個地址,而這個地址4005c0正是下面只讀資料段裡面的,我們可以看到下面4005c0 儲存資料31323300十六進位制對應的ascii碼裡面的就是123\0
第二個變數(紅色框)是通過三次mov操作放到了棧上(movb表示按位元組移動)。
第三個變數和第四個變數的方式一樣,都是直接把字串傳遞到了棧上,而不是像第一個變數那樣,傳遞的是一個地址。

所以,用指標初始化的字串在只讀取,不能被改寫;用 char 陣列形式初始化的字串,即使使用了雙引號來初始化,也是在棧上,後面程式是可以改寫的。

擴充套件

C 語言也太坑爹了,這樣每個函式怎麼用,我們怎麼知道傳入的字串在函式內部會不會做變更呢?
其實在函式手冊可以看到一些細節,比如下面的函式

char *strchr(const char *s, int c);
char *strtok(char *str, const char *delim);
char *strcat(char *dest, const char *src);

當形參為const char *的時候,說明函式不會對該段記憶體裡的資料做變更,傳入棧上、堆上、只讀區的地址都行;反之,如果形參為char *就要小心了,可以認為它的意思是陣列,會改變傳入的“字串”。

思考

根據我們上面分析的

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char *date = "2019";
    strcat(date, "-09-10");
    printf("%s,%p\n", date, (void *) date);

    return 0;
}

執行時肯定是Segmentation fault了,因為“2019”是存在了只讀取。

如果換成下面的程式碼,又會怎樣呢?

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char date[] = "2019";
    strcat(date, "-09-10");
    printf("%s,%p\n", date, (void *) date);

    return 0;
}

linux gcc 編譯可執行,但是實際是有問題的,比如我改成

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char date[] = "2019";
    strcat(date, "-09-1000000000000000000");
    printf("%s,%p\n", date, (void *) date);

    return 0;
}

就會出現段錯誤,也許在你的伺服器編譯執行又不報錯,如果不報錯請增加追加字串的長度然後嘗試。(C 程式就是這麼神奇,能執行不一定表示沒問題)因為date初始化分配的記憶體不足以存放連線之後的字串。我們改寫為

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {

    char date[11] = "2019";
    strcat(date, "-09-10");
    printf("%s,%p\n", date, (void *) date);

    return 0;
}

這樣就可以正常執行了。坑爹啊,C 語言也麻煩了,一不小心就寫錯,怪不得 PHP 是世界上最好的語言。

安利

世上無難事只怕有心人,如果覺得想學C語言,又比較困難,不如我們一起來學,趕快上車 https://segmentfault.com/ls/1...

相關文章