今回はPaizaオンラインハッカソンVol.6に参加してみた(六村リオミッション, C++)をC言語で書いた場合の回答例とその解説を行いたい。六村リオミッションではその性質上、オブジェクト指向プログラミングの考え方ができるかどうかで楽になるか地獄を見るかが変わってくる。
回答例
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
/// コーヒーを定義する
typedef struct _Coffee {
float powder; ///< コーヒー粉
float water; ///< 湯
}Coffee;
/// コーヒーの初期値
static const Coffee CoffeeEmpty = {0.0, 0.0};
/// コーヒーの量を取得する
/// @param coffee 量を取得する対象のコーヒー
/// @return 粉と湯の合計値
static float CoffeeGetTotal(Coffee *coffee) {
return coffee->powder + coffee->water;
}
/// コーヒーの濃度を取得する
/// @param coffee 濃度を取得する対象のコーヒー
/// @param asPercent パーセントとして出力するかどうか
/// @return コーヒーの濃度
static float CoffeeGetConsentration(Coffee *coffee, _Bool asPercent) {
float consentration = coffee->powder / CoffeeGetTotal(coffee);
return asPercent ? consentration * 100 : consentration;
}
/// コーヒーにお湯を入れる
/// @param coffee お湯を入れる対象のコーヒー
/// @param quantity お湯の量
static void CoffeeAddWater(Coffee *coffee, float quantity) {
coffee->water += quantity;
}
/// コーヒーに粉を入れる
/// @param coffee 粉を入れる対象のコーヒー
/// @param quantity 粉の量
static void CoffeeAddPowder(Coffee *coffee, float quantity) {
coffee->powder += quantity;
}
/// コーヒーを味見する
/// @param coffee 味見をする対象のコーヒー
/// @param quantity 味見する量
static void CoffeeTaste(Coffee *coffee, float quantity) {
float total = CoffeeGetTotal(coffee);
coffee->powder -= quantity * coffee->powder / total;
coffee->water -= quantity * coffee->water / total;
}
/// 改行コードを除去する
/// @param str 改行コードを除去する対象の文字列
static void removeReturnCode(char *str) {
if (strchr(str, '\n')) {
str[strlen(str) - 1] = '\0';
}
}
int main(void){
// バッファの長さ
const size_t bufLen = 100;
// 分割に使用する文字
const char *splitChar = " ";
// 処理数を取得する
char linesStr[bufLen];
fgets(linesStr, sizeof(bufLen), stdin);
removeReturnCode(linesStr);
int lines = atoi(linesStr);
// コーヒーを生成する
Coffee coffee = CoffeeEmpty;
for (int idx = 0; idx < lines; idx++) {
// コーヒー処理用の文字列を取得する
char actStr[bufLen];
fgets(actStr, bufLen, stdin);
removeReturnCode(actStr);
// 行動のタイプ
enum {
ADD_WATER = 1,
ADD_POWDER = 2,
TASTE = 3
};
// 処理タイプと量を取得する(処理のタイプと量は空白で分かれている)
int actType = atoi(strtok(actStr, splitChar));
float quantity = atof(strtok(NULL, splitChar));
// 処理タイプに応じ、指定された量分の処理を行う
switch (actType) {
case ADD_WATER:
CoffeeAddWater(&coffee, quantity);
break;
case ADD_POWDER:
CoffeeAddPowder(&coffee, quantity);
break;
case TASTE:
CoffeeTaste(&coffee, quantity);
break;
default:
return EXIT_FAILURE;
break;
}
}
// 濃度を出力する
printf("%d", (int)CoffeeGetConsentration(&coffee, true));
return EXIT_SUCCESS;
}
解説
今回はC言語で擬似的なオブジェクト指向プログラミングを行った。
C言語ではオブジェクト指向プログラミングの仕組みは提供されていないため、代替として構造体と関数を使って擬似的に再現した。基本的には構造体になんらかの計算を関数で行う場合、その構造体へのポインターに計算する値を入れることでオブジェクト指向プログラミングのような扱い方をすることができるようになる。
ここではCoffeeという構造体 ((正確には_Coffeeという構造体のtypedefとしてCoffeeを指定した)) と、Coffee構造体へのポインターに対して各種計算を行う関数(CoffeeAddWater、CoffeeAddPowderなど)という組み合わせで行っている。
C言語においては、文字列を分割する際はstrtok関数を使う。これは初回は引数に(分割したい文字列, 分割に使う文字列)を指定、2回目以降は(NULL, 分割に使う文字列)を指定する。なお、今回の場合は文字列が置き換えられても問題ないためスルーしたが、分割の際に分割に使った文字列がNULL文字に置き換えられるため、元の文字列が置き換えられるのが望ましくない場合は、コピーを作成してから行うべきである。なお、これ以上分割対象のテキストがない場合は返り値にNULLが帰るようになっている。strtok関数に関する詳細についてはC言語Tips集 – 文字列を指定文字で分割する(C言語関数辞典)を参照のこと。
それ以外については、基本的にはC++での考え方とほとんど変わらない。使う関数が違うことと、計算方法に違うところがあるのでその点に気をつけるのみである。
結果
cf: https://paiza.jp/poh/joshibato/rio/result/7ead545b
当然ながら全テストケース通過、処理時間も0.01秒で統一された。
最後に
今回はなるべくC++などのようにオブジェクト指向プログラミングの考え方を意識してコーディングを行った。この場合は直接計算する場合と比較して(インライン展開を行わない限り)関数呼び出しなどで若干のレイテンシーが発生して処理速度という点で不利になるものの、分かりやすさあるいはメンテナンス性ではこちらの方が上であること、近年のPCの処理速度であればレイテンシーも完全に無視できるレベルであるため、C言語でもなるべくオブジェクト指向の考え方に沿ってプログラムを組みたいところである。
ウェブマスター。本ブログでITを中心にいろいろな情報や意見などを提供しています。主にスマートフォン向けアプリやウェブアプリの開発を携わっています。ご用の方はコメントかコンタクトフォームにて。