2022年3月25日 星期五

怪異的 c 語法

在 telegram 上看到有關 c 語言的一些怪異語法。
覺得有趣, 來分析看看

twitter 上的程式碼是圖片, 不太習慣, 自己打成文字版本。

a1.c
1 #include <stdio.h>
2
3 int main(int argc, char *argv[])
4 {
5   puts("-0.5" + 1);
6   return 0;
7 }


把 "-0.5" 想成 const char *str="-0.5", str + 1 是什麼呢? 當然就是 0.5 了。

a2.c
1 #include <stdio.h>
2
3 int main(int argc, char *argv[])
4 {
5   printf("%d\n", 50 ** "2");
6   return 0;
7 }


一樣的道理, 把 "2" 想成 const char *str2 = "2", *str2 就是 '2', 等於是作 50 * '2'。

a3.c
 1 #include <stdio.h>
 2
 3 int main(int argc, char *argv[])
 4 {
 5   int x = 5;
 6
 7   // while x goes to 0
 8   while(x --> 0)
 9   {
10     printf("%d ", x);
11   }
12
13   return 0;
14 }
15 result: 4 3 2 1 0 


這個只是
 8   while(x --         >           0)

盡量不要寫 a3.c L8 的語法, 你搞不清楚 5 和 0 比, 還是 4 和 0 比, 這寫法是 5 和 0 比, L10 印出 4, 依序下去。

2022年3月18日 星期五

要用幾個變數來接受 c scanf 的輸入

初學者在學 c 的鍵盤輸入時, 最困擾的通常是要宣告幾個變數來接受鍵盤的資料, 以 scanf 為例, 如果宣告一個 int input, 不就只能接受一個輸入嗎? 如果輸入兩筆資料時要怎麼辦? 30 筆的時候該怎麼辦? 好像沒辦法事先知道要宣告幾個變數來接收鍵盤輸入。

這時候先學習 c++ 的好處就來了, 不需要先寫可怕的 malloc 來處理這件事情, 用 std::vector 就可以搞定, 如果你不是用 c++ 而是學 c, 那就得先刻一個類似 vector 的資料結構, 通常這時候也應該會寫 list, 剛好派上用場。

初學的時候一般會用一個折衷的辦法, 先輸入一個數字, 例如 5, 代表之後要輸入 5 筆資料, 再用 malloc 5 個 int 大小的記憶體區間, 厄, 不太好是吧, 還不如一次就把這些觀念帶上, 先用 c++ vector, 再用 c 自己實做一個 vector/list, 這本來就是本科系無法迴避的課題。scanf 可沒想像的簡單。如果是我來教學, 我會這麼做, 讓初學者知道有這樣的資料結構, 等學習一段時間之後, 再去實做那種資料結構。

list 1 s.cpp
 1 #include <cstdio>
 2 #include <vector>
 3 
 4 using namespace std;
 5 
 6 int main(int argc, char *argv[])
 7 {
 8   vector<int> vals;
 9   int input=0;
10   int ret;
11 
12   while(1)
13   {
14     ret = scanf("%d", &input);
15     printf("ret: %d\n", ret);
16     printf("input: %d\n", input);
17     if (ret)
18     {
19       vals.push_back(input);
20     }
21     if (ret == EOF || ret == 0)
22     {
23       break;
24     }
25   }
26 
27   for (auto &i : vals)
28   {
29     printf("i: %d\n", i);
30   }
31   return 0;
32 }

我用 c++ 就偷懶一下了。scanf 對 c 初學者來說, 門檻真的太高了, 它比想像中的還要難。

另外一個類似的問題, 如果你要輸入一個字串, 要宣告 char str[10]; 確定這個夠嗎? 如果輸入的字串有 20 char 怎麼辦? POSIX.1-2008 親切的提供了 m 來處理這個問題, 參考 list 2 的用法, 然而在更早之前 GNU 提供了 a, 所以有點混亂, 更麻煩的是 a 現在提供給浮點數用, 而且成為標準, 但如果你用的是夠新的 c 編譯器, 應該有 m 可以用。windows/vs, mac/clang 請自行確認有沒有 m 可以用。請記得不用時要 free 這個 str, 另外傳給 scanf 時, 是傳指標的指標 &str(char **), 相信整死 c 初學者了。

list 2 s2.cpp
25   char *str;
26
27   ret = scanf("%ms", &str);
28
29   printf("str: %s\n", str);
30
31   free(str);

還沒完, 如果格式轉換出錯時怎麼辦, %d 結果輸入了 abc 呢? 這個真的整死我, 花了不少時間才搞定, 因為轉換失敗, 這時候 abc 還存在 stdin buffer 裡頭, 如果使用 list 1. L12 用 while 去抓, 在我的平台會一直無窮迴圈, 因為 abc 一直存在, scanf 會一直格式錯誤, return 0, errno 會得到 EILSEQ。

解法很直覺, 清掉 stdin buffer 就好了吧, 出動 fflush(stdin), 結果沒用, 再補上 clearerr(stdin), 也沒用, 靠, stdin buffer 清不掉阿, fflush 名過其實。後來找到 __fpurge(stdin) 終於清掉 stdin buffer。這時候 scanf 就又會等在那邊等著使用者按下鍵盤輸入了。

linux man page 提到: For input streams associated with seekable files (e.g., disk files, but not pipes or terminals), 也許 stdin 不算是 seekable files 吧! 所以 fflush(stdin) 無效。另外「C程式語言」B-4 提到 fflush 對於 input streams, 結果未定義 (undefined behavior)。

__fpurge(stdin) 不確定其他平台有沒有, 請自行查詢。另外一個有可攜性的作法就是把 stdin buffer 讀出來丟掉。

int c;
while ((c = getchar()) != '\n' && c != EOF);

我本來只想寫第一段那個而已, 沒想到後來補了這麼多, scanf 難阿!

ref:
  1. C語言——使用scanf函式時需要注意的問題
  2. I am not able to flush stdin

2022年3月13日 星期日

printf 的 integer promotion

巧者勞力, 智者勞心
p.c
 1 #include <stdio.h>
 2
 3 int main(int argc, char *argv[])
 4 {
 5   unsigned char a=0x80;
 6   signed char b=0x80;
 7
 8   printf("unsigned char a: %#x, signed char b: %#x\n", a, b);
 9   printf("unsigned char a: %#x (%d), signed char b: %hhx (%hhd)\n", a, a, b, b);
10   return 0;
11 }

list 1 執行結果
 1 unsigned char a: 0x80, signed char b: 0xffffff80
 2 unsigned char a: 0x80 (128), signed char b: 80 (-128)


list 1 是執行結果, L1 顯示了 signed char b: 0xffffff80, 不是預期的 0x80, 有點奇怪是吧? 引用之前的文章 - compiler [7] - code generator - funcall call, pass argument
function 參數的傳遞比想像中複雜, 當 function 沒有 prototype 時或是使用 K&R style 的宣告或是 ... 這種參數 - ex: printf(const char *format, ...), 會發動 integer promtion, 這很好理解, 可以參考《“对于那些没有原型的函数,传递给函数的实参将进行缺省参数提升”是什么意思?
printf 的參數是 ... 所以會發動 integer promtion, integer promtion 之後, signed char b 0x80 的 -128 變成了 4bytes 的 -128, 也就是 0xffffff80, 這便是 %#x 印出 0xffffff80 的原因。

cpu 用什麼指令做這件事情, 反組譯一下觀察, list 2 L716, 717 的指令 movsbl, movzbl 就是在做這件事情。由於比較熟悉 x86-32, 所以編譯成 x86-32 來觀察, 其他平台應該也有類似的指令, 就不一一觀察了。

list 2. p.c.dis 編譯指令 gcc -static -no-pie -fno-pic -m32 -g p.c -o p
     1 
     2 p:     file format elf32-i386
   705 08049815 <main>:
   706  8049815:	8d 4c 24 04          	lea    0x4(%esp),%ecx
   707  8049819:	83 e4 f0             	and    $0xfffffff0,%esp
   708  804981c:	ff 71 fc             	push   -0x4(%ecx)
   709  804981f:	55                   	push   %ebp
   710  8049820:	89 e5                	mov    %esp,%ebp
   711  8049822:	53                   	push   %ebx
   712  8049823:	51                   	push   %ecx
   713  8049824:	83 ec 10             	sub    $0x10,%esp
   714  8049827:	c6 45 f7 80          	movb   $0x80,-0x9(%ebp) # unsigned char a=0x80
715 804982b: c6 45 f6 80 movb $0x80,-0xa(%ebp) # signed char b=0x80
716 804982f: 0f be 55 f6 movsbl -0xa(%ebp),%edx # signed char b=0x80 717 8049833: 0f b6 45 f7 movzbl -0x9(%ebp),%eax # unsigned char a=0x80 718 8049837: 83 ec 04 sub $0x4,%esp 719 804983a: 52 push %edx 720 804983b: 50 push %eax 721 804983c: 68 08 30 0b 08 push $0x80b3008 722 8049841: e8 da 85 00 00 call 8051e20 <_IO_printf> 723 8049846: 83 c4 10 add $0x10,%esp 724 8049849: 0f be 5d f6 movsbl -0xa(%ebp),%ebx 725 804984d: 0f be 4d f6 movsbl -0xa(%ebp),%ecx 726 8049851: 0f b6 55 f7 movzbl -0x9(%ebp),%edx 727 8049855: 0f b6 45 f7 movzbl -0x9(%ebp),%eax 728 8049859: 83 ec 0c sub $0xc,%esp 729 804985c: 53 push %ebx 730 804985d: 51 push %ecx 731 804985e: 52 push %edx 732 804985f: 50 push %eax 733 8049860: 68 34 30 0b 08 push $0x80b3034 734 8049865: e8 b6 85 00 00 call 8051e20 <_IO_printf> 735 804986a: 83 c4 20 add $0x20,%esp 736 804986d: b8 00 00 00 00 mov $0x0,%eax 737 8049872: 8d 65 f8 lea -0x8(%ebp),%esp 738 8049875: 59 pop %ecx 739 8049876: 5b pop %ebx 740 8049877: 5d pop %ebp 741 8049878: 8d 61 fc lea -0x4(%ecx),%esp 742 804987b: c3 ret

另外 printf 提供了 %hh 來印出 signed char 或是 unsigned char 變數。

ref:

2022年3月5日 星期六

正版漫畫的受害者

一直都有在收 jojo 紙本漫畫, 通常都是等全部出完在去買二手全套, jojo lion 第八部我也打算這麼做, 我的 jojo 漫畫幾乎全部都是二手書, 終於等到連載完結了, 找了一下二手, 卻反而比新的貴, 通常是前面幾集出版社絕版不出, 所以二手常會看到比新的還貴, 之前買的「瘋狂怪醫芙蘭」就是這樣, 但我還是買新的, 缺了 6 ,7, 因為「瘋狂怪醫芙蘭」是單元劇, 不是連載的劇情, 就沒差, 不想給二手書抬價, 利用真的想收書的讀者。
fig 1. 20210908 訂購於博客來

回到 jojo lion, 查了一下 jojo lion, 竟然 1 ~ 25 都還有, 就趕緊出手了, 剩下 26, 27 就等出版社出版再買, 這就不會有問題。第一集打折後 83, 其餘是 75 元。

會找二手一次買齊也是因為我不喜歡等劇情, 想一次看個過癮, 但其實等我看到 25 時, 出版社才出到 26, 而 27 我等到 2022/2/22 才購得, 日文版本 jojo lion 27 在 2021/9/17 出版, 繁體中文版時間落差有點大。看網路盜版的早就都看完劇情了, 正版支持者卻要等這麼久才能看完, 心理不太舒服。還要有收不到整套的風險。

ジョジョリオン 27 (ジャンプコミックス) 
出版社 ‏ : ‎ 集英社 (2021年9月17日)
出版日期 ‏ : ‎ 2021年9月17日
语言 ‏ : ‎ 日语
漫画 ‏ : ‎ 208页

JOJO的奇妙冒險PART 8 JOJO Lion 27 (完)
ジョジョリオン
作者:荒木飛呂彦
出版社:東立出版社有限公司
出版日期:2022/02/11

另外 jojo 1-63 重新再版了, 有同好遇到以下情況。

A君
對 我1/7在博客來下訂整套
結果寄前62本來
63一直調貨 昨天回信說
Andrew Chen, [17.02.22 10:44]
您於01月07日訂購的 

[ JOJO的******** ] [取消數量: 1 ] 

目前廠商已缺貨,博客來已無庫存,經我們四處尋找亦調不到商品,非常抱歉,需為您取消,因為此商品已扣款成功,因此為你辦理退款手續。

還好之後 63 有收到, 要不然太幹了。

另外「戀上換裝娃娃」也有類似情形, 不知道為什麼 1 - 6 突然間買不到了, 第一集 2019/12/6 的書這麼快就買不到了, 真奇怪, 網路上的二手戀上換裝娃娃 1-6 一樣是高於新書得的售價, 當然沒買, 最痛恨這種吃定人想要抬高售價的行為。貴一點還可以接受, 高到一倍就無法接受了。

fig 1. 二手賣 1300nt


好在最近又再刷的樣子, 2022/2/19 購得 1 - 7, 我本來放棄想買日文版本了。查了一下版權頁, 果然是再刷, 我的是第三刷 2022/3/25, 真奇怪, 竟然在 2022/2/19 就可以買到。

看看 fig 1 的售價, 一般來說二手應該是在半價左右, 也就是 6 本應該在 400nt 左右, 但因為熱門絕版反而給了 1300nt 的販售價格, 如果之前沒人買, 現在應該賣不出去。在 20220303 左右查訊「戀上換裝娃娃」二手/新書販售價格, 也都是正常的販售價格了。當然二手如果不是半價, 就算保存再好, 一樣比較難賣。



影片的說明我自己覺得買斷第一集和最後一集的部份有點怪, 畢竟就算買斷第二集也有類似的效果, 一樣都是不完整, 為什麼會需要集中在第一集和最後一集? 我自己是沒有觀察到這樣的二手現象。不過還是會注意能不能完整收集, 太貴或是有缺, 就不會買了。

ref:
戀上換裝娃娃 1~6 台版東立再版2/21上市