需求
假如有這樣的一個需求,有個日期,想要擷取獲得其年份。我們用 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)
$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...
也歡迎大家關注我的公眾號,不發騷擾,只發乾貨原創文章