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

32

需求

假如有這樣的一個需求,有個日期,想要截取獲得其年份。我們用 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...

也歡迎大家關注我的公衆号,不發騷擾,隻發幹貨原創文章
圖片描述


如果覺得我的文章對你有用,請随意贊賞

你可能感興趣的

時光 · 9月12日

學c還是很強的哈哈

+1 回複

kumfo · 9月12日

搞啥C語言啊,PHP一條道走到黑。

回複

0

說的是~

周夢康 作者 · 9月12日
年年 · 9月17日

不是 C 語言坑,是你沒用對
char date[] = "2019";
strcat(date, "-09-1000000000000000000");
printf("%s,%pn", date, (void *) date);
這段代碼不能運行的根本原因,簡單的說就是,你企圖用一個 500ml 的容器去裝 1000ml 的水,這怎麼可能裝的下呢
動态腳本語言,在處理數組時都會做動态擴容,而 C 的數組是不會這樣做的,所以還是多學多練吧

回複

0

(⊙o⊙)…我隻是舉反例,下面不是又舉例子給說明了麼

周夢康 作者 · 9月17日
0

謝謝這麼認真的回複哈~我說c坑是故意說得玩笑話。

周夢康 作者 · 9月17日
載入中...