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

需求

假若有這樣的一個需求,有個日期,想要截取得到其年份。咱們用 php 可使用explode,也可使用strtokphp

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

可能你們對strtok不太熟悉,它的做用是用-來分割$a獲取子串,循環調用能夠達到和explode差很少的效果。具體能夠看下官方手冊裏面的 demo https://www.php.net/manual/zh...linux

實驗

實驗1

我之因此用strtok呢,是由於C 語言裏也有這個函數,這個函數比較「怪」,每一次調用,是將字符串中找到的-替換爲\0,而後返回標記字符串的首地址。segmentfault

#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;
}

運行的結果倒是sass

Segmentation fault

原理

當咱們使用指針變量做爲左值,雙引號字符串做爲右值時,背後雙引號的邏輯是:bash

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

因此char * date就在棧上存放裏雙引號字符串返回的首地址。當使用strtok的時候,經過實驗1能夠看到strtok實際是找到的字符串替換爲\0,也就是說須要修改原字符串的。而該字符串是在只讀區,不不能修改,因此運行出現了段錯誤。服務器

反過來思考,咱們 char date[]數組經過雙引號初始化的時候又是什麼原理,是否是也是雙引號返回了常量字符串首地址,而後再經過循環一個個賦值到char數組裏呢?函數

實驗3

猜測歸猜測。咱們經過實驗來證實下。spa

#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 反彙編能夠看到.net

$ 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...

也歡迎你們關注個人公衆號,不發騷擾,只發乾貨原創文章
圖片描述

相關文章
相關標籤/搜索