バーチャルプログラマ・技術者 Advent Calendar 2023 3日目.
組み込み中心にプログラムを書いてきたVTuberです.
C言語に色々と面白い特徴/仕様/記法があるので,それを紹介していきたいと思います.
自己紹介
現代社会と倫理の教員をしているVTuberの創技 光と申します.
メインではC言語を使ってます.C++は難しすぎて何もわかりません.
元々組み込み畑の人間で,よくわからない仕様とかを見るのが大好きです.
注意書き
- 紹介するものは大抵処理系に依存します.
- 間違い/記載漏れ等あるかと思います.
- 寛容な心をもってエンタメとしてご覧ください.
対象読者
- C言語を触っている人
- C言語を触っていない人
main()関数がなくてもコンパイルは通る (1)
C言語では基本main()
関数を書かなければコンパイルエラー(正確にはリンクエラー)が発生し,実行させることができないのですが,ある方法を使ってmain()
を書かずにコンパイルを通すことができます.
// main関数を書かない場合
int a=0;
// undefined reference to `WinMain' collect2.exe: error: ld returned 1 exit status
// mainという変数を宣言する
int main=0;
// エラー無し
このように,C言語では処理上main
というシンボルへジャンプしているだけ(mainという名前の変数や関数を見つけ,それを実行している)なので,ソースコードでmain
という名前の何かしらを宣言するとコンパイルを通すことができます.
main()関数がなくてもコンパイルは通る (2)
しかし先ほどのような書き方だと何の処理も行えないわけです.
なので今度はmain()
を使用せずに何かしらの処理を実行できるようにしてみます.
#include <stdio.h>
int start(){
printf("Hello World\n");
return 0;
}
// gcc 01.c -nostartfiles
// Hello World
このように,C言語ではstart()
を記載し,コンパイル時に-nostartfiles
を付けることで,main()
を用いずともプログラムを実行することができます.
start()
以外にも,
//sample1
#include <stdio.h>
void beforeMain (void) __attribute__((constructor));
void beforeMain(void){
printf("Hello World\n");
return 0;
}
//sample2
#include <stdio.h>
int execute(){
printf("Hello World\n");
return 0;
}
など,様々な方法があります.
main()は再帰的に呼び出せる
とりあえずこちらのプログラムをご覧ください.
#include <stdio.h>
int main(void){
printf("HelloWorld\n");
main();
return 0;
}
こちら実行するとこうなります.
~> ./a.exe
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
HelloWorld
...
そうmain()
を再帰的に呼び出したことにより,終了することなく永遠と実行されてしまうんです.
(追記:2023/12/3)補足:バージョンによる違い
コメントでご指摘いただきました. バージョンによって,```main()```の再起呼び出しが```call```であることより,(処理上スタックを使う場合があるため)永遠ではないそうです.こちらISO/IEC 9899:1999によると
If the return type of the main function is a type compatible with int, a return from the initial call to the main function is equivalent to calling the exit function with the value
returned by the main function as its argument;10) reaching the } that terminates the
main function returns a value of 0. If the return type is not compatible with int, the
termination status returned to the host environment is unspecified.
https://www.dii.uchile.cl/~daespino/files/Iso_C_1999_definition.pdf
と書かれています.
一応複数回の呼び出しは想定されているようですが,処理系に依存しそうな感じです.
なので,規格的にはOKだが,保証はされていないっぽいですね.
あとC++では禁止されています.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/n4861.pdf
C言語で10進数の0は存在しない.
#include <stdio.h>
int main(void){
printf("%d",0);
return 0;
}
// 0
この時printf()
の引数にある0は10進数ではなく,8進数の0です.
何言ってんだこいつと思ってらっしゃることでしょう.
何を根拠に8進数と主張しているのかと.
実はISO/IEC 9899にはこう書かれています.
decimal-constant:
nonzero-digit
decimal-constant digit
octal-constant:
0
octal-constant octal-digit
https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2310.pdf
10進数は0以外で始まる値である.と書かれていますし,何より0は8進数であると明記されています.
このように,C言語の0は10進数ではなく8進数であると定義されているわけです.
が,まぁ10進数だろうが8進数だろうが実質的には何も変わらないので何の役にも立たない知識です.
(追記:2023/12/3)補足:オーバーフローによる0の表現
コメントでご指摘いただきました. 2^128=340282366920938463463374607431768211456 を```printf("%d")```した場合0と表示されるそうです.配列名はアドレスを表すものだから...
C言語には配列という,メモリの連続性が保証された,固定長の配列変数があります.
C言語では要素の追加/削除は基本的にはできません.
さて,C言語で配列を扱う時はこのようなプログラムとなります.
#include <stdio.h>
#define idx 10
int main(void){
int i=0;
int array[idx]={0};
for(i=0;i<idx;i++){
printf("%d\n",array[i]);
}
return 0;
}
array[{index}]
と書くことで,array
配列の{index}
番目の要素を参照することができます.
が,実はもう一つarray
配列の{index}
番目の要素を参照する方法があります.
それがこちら
#include <stdio.h>
#define idx 10
int main(void){
int i=0;
int array[idx]={0};
for(i=0;i<idx;i++){
- printf("%d\n",array[i]);
+ printf("%d\n",i[array]);
}
return 0;
}
お分かりいただけましたか?
{index}[array]
と,配列名とn番目の指定の変数が逆になっています.
(修正:2023/12/3)修正前文章
これはC言語の仕様上,配列は頭の変数(index=0)のアドレスを参照しているだけなので,処理上は```array```は```array[0]```のアドレスが入っておりarray
を評価するとarray[0]
と同じアドレスを示し,そこからint
一つ分先のアドレスを参照することでarray[1]
を参照できるようにしているわけです.
#include <stdio.h>
#define idx 10
int main(void){
int array[idx]={0};
printf("array:%p\n",array);
//array:000000000061FDF0
printf("array[0]:%p\n",&array[0]);
//array[0]:000000000061FDF0
printf("array[1]:%p\n",&array[1]);
//array[1]:000000000061FDF4
return 0;
}
(修正:2023/12/3)修正前文章
コメントでご指摘いただきました. (訂正により削除した文章) つまるところ,C言語では **配列のアドレス+要素数(×配列の型のバイト)** という形で値を参照しているので,逆 つまり**要素数(×配列の型のバイト)+ 配列のアドレス**問題がないというわけです.C言語で,array[index]
は*(array+index)
の糖衣構文(わかりやすくするための書き方)であるため,交換法則(足す数と足される数は入れ替えても結果は変わらない)より,*(index+array)
と書いてもいいそうです.
まぁこの仕様は通常C言語を使っていたら思いつかないこともないわけで,C言語(それ以外も?)では,関数の引数に配列を使うと関数内で配列の値を変えることができるんですが,これは配列はただアドレスを持っているだけだからというわけです.(訂正:2023/12/3)array
はarray[0]
と同じアドレスを持っているからです.
#include <stdio.h>
#define idx 10
void function(int array[],int *num){
array[0]=5;
*num=5;
}
int main(void){
int array[idx]={0};
int num=0;
function(array,&num);
printf("array:%d",array[0]);
// 5
printf("num:%d",num);
// 5
return 0;
}
この場合のfunction
内で行われていること(array[0]
の値変更,num
の値変更)は,処理上同じようになっています.
配列と文字列...
まずはこちらのプログラムをご覧ください.
#include <stdio.h>
int main(void){
int c = 0["abc"];
printf("%d",c);
// 96
return 0;
}
このコードは,"abc"
という文字配列の0番目("a")を指定し,c
に代入しています.そのため,出力は"a"のASCIIコードである96になっているわけです.
これは先ほどの{index}[array]
と同様の挙動です.
関数もアドレスだから...
実は関数もアドレスを持っています.
#include <stdio.h>
int main(void){
printf("%p",main);
// 0000000000401550
return 0;
}
なので,この関数のアドレスを別の名前の変数に入れて関数を実行させることもできます.
#include <stdio.h>
void funtcion(){
printf("FUNCTION\n");
}
int main(void){
void (*funcptr)(void)=funtcion;
funcptr();
// FUNCTION
return 0;
}
そして,変数に関数アドレスを入れることができるということは,配列で管理することができるということです.
#include <stdio.h>
typedef void (*FunctionPointer)();
void function1() {
printf("Function 1\n");
}
void function2() {
printf("Function 2\n");
}
void function3() {
printf("Function 3\n");
}
int main() {
FunctionPointer functions[] = {function1, function2, function3};
for (int i = 0; i < 3; i++) {
functions[i]();
}
/*
* Function 1
* Function 2
* Function 3
*/
return 0;
}
このようにして関数を配列に入れ,それぞれ要素数を指定し実行させることができます.
これを駆使すれば,main()
内に1行しか書かずに大きなプログラムを動かすことができます.
あと私はよくゲームのシーン管理に使っています.
printfの返り値
#include <stdio.h>
int main(void)
{
int pre=printf("123456\n");
printf("%d",pre);
// 7
return 0;
}
printf()
には返り値があり,printf()
の返り値は表示された文字数が返されます.
これ何のためにあるのかわかりませんが,昔聞いた話だとC言語にはvoid
という型が無かったため,int
にし,慣習的に何かしらの値を返すようにした.とかなんとか...
締め
C言語はメモリやアドレス系の面白い仕様(というか記法)があるので,そういうのを見ると「あ~低レイヤー触ってんな~」ってなりますよね.
それでは良きC言語ライフを!
訂正,修正に関して
コメントでご指摘いただきました文章の訂正,補足,修正箇所には前の文章を載せております.
また,一部項目タイトルも変更させていただいております.