diff --git a/app/pagesList.ts b/app/pagesList.ts index 207e6b9..442b2fa 100644 --- a/app/pagesList.ts +++ b/app/pagesList.ts @@ -25,8 +25,18 @@ export const pagesList = [ lang: "C++", description: "C++の基本から高度な機能までを学べるチュートリアル", pages: [ + { id: 1, title: "C++の世界へようこそ" }, { id: 2, title: "型システムとメモリ" }, { id: 3, title: "関数と参照" }, + { id: 4, title: "ポインタと動的メモリ" }, + { id: 5, title: "クラスの基礎" }, + { id: 6, title: "クラスを使いこなす" }, + { id: 7, title: "継承とポリモーフィズム" }, + { id: 8, title: "テンプレート" }, + { id: 9, title: "STL ①:コンテナ" }, + { id: 10, title: "STL ②:アルゴリズムとラムダ式"}, + { id: 11, title: "RAIIとスマートポインタ" }, + { id: 12, title: "プロジェクトの分割とビルド" }, ], }, ] as const; diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index c6b3ee7..7bb2a29 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -18,7 +18,7 @@ export type ExecLang = "python" | "cpp"; interface ExecProps { /* * Pythonの場合はメインファイル1つのみを指定する。 - * C++の場合はソースコード(.cpp)とヘッダー(.h)を全部指定し、ExecFile内で拡張子を元にソースコードと追加コードを分ける。 + * C++の場合はソースコード(.cpp)を全部指定する。 */ filenames: string[]; language: ExecLang; @@ -63,17 +63,9 @@ export function ExecFile(props: ExecProps) { if (!props.filenames || props.filenames.length === 0) { throw new Error("C++の実行には filenames プロパティが必要です"); } - commandline = wandbox.cppOptions - ? `${wandbox.cppOptions.commandline} ${props.filenames.join(" ")} && ./a.out` - : ""; + commandline = wandbox.getCommandlineStr("C++", props.filenames); runtimeInitializing = false; - const namesSource = props.filenames!.filter((name) => - name.endsWith(".cpp") - ); - const namesAdditional = props.filenames!.filter( - (name) => !name.endsWith(".cpp") - ); - exec = () => wandbox.runFiles("C++", namesSource, namesAdditional); + exec = () => wandbox.runFiles("C++", props.filenames); break; default: props.language satisfies never; diff --git a/app/terminal/repl.tsx b/app/terminal/repl.tsx index 7ac11f8..5cbda66 100644 --- a/app/terminal/repl.tsx +++ b/app/terminal/repl.tsx @@ -16,7 +16,7 @@ import { useSectionCode } from "../[docs_id]/section"; import { Terminal } from "@xterm/xterm"; export interface ReplOutput { - type: "stdout" | "stderr" | "error" | "return" | "system"; // 出力の種類 + type: "stdout" | "stderr" | "error" | "return" | "trace" | "system"; // 出力の種類 message: string; // 出力メッセージ } export interface ReplCommand { @@ -41,6 +41,9 @@ export function writeOutput( case "error": term.write(chalk.red(message)); break; + case "trace": + term.write(chalk.blue.italic(message)); + break; case "system": term.write(systemMessageColor(message)); break; diff --git a/app/terminal/wandbox/api.ts b/app/terminal/wandbox/api.ts new file mode 100644 index 0000000..1293725 --- /dev/null +++ b/app/terminal/wandbox/api.ts @@ -0,0 +1,146 @@ +import { type Fetcher } from "swr"; +import { type ReplOutput } from "../repl"; + +const WANDBOX = "https://wandbox.org"; +// https://github.com/melpon/wandbox/blob/ajax/kennel2/API.rst <- 古いけど、説明と例がある +// https://github.com/melpon/wandbox/blob/master/feline/src/types.rs +/* RustのVecはバイト配列ですが、serialize_with = "serialize_utf8"という指定があるため、 +JSONにシリアライズされる際にはUTF-8文字列に変換されると解釈し、TypeScriptの型はstringとします。 +by gemini +*/ +export interface SwitchOption { + name: string; + "display-flags": string; + "display-name": string; +} +export interface SwitchSingle { + type: "single"; + name: string; + "display-name": string; + "display-flags": string; + default: boolean; +} +export interface SwitchSelect { + type: "select"; + name: string; + options: SwitchOption[]; + default: string; +} +/** + * Rustの 'Switch' enum に対応するディスクリミネேテッドユニオン型です。 + * 'type' プロパティの値によって `SwitchSingle` か `SwitchSelect` かを判別できます。 + */ +export type Switch = SwitchSingle | SwitchSelect; +export interface CompilerInfo { + name: string; + version: string; + language: string; + "display-name": string; + templates: string[]; + "compiler-option-raw": boolean; + "runtime-option-raw": boolean; + "display-compile-command": string; + switches: Switch[]; +} +interface Code { + file: string; + code: string; +} +export interface CompileParameter { + compiler: string; + code: string; + codes?: Code[]; + options?: string; + stdin?: string; + "compiler-option-raw"?: string; + "runtime-option-raw"?: string; + github_user?: string; + title?: string; + description?: string; + save?: boolean; + created_at?: number; + is_private?: boolean; + "compiler-info"?: CompilerInfo; +} +export interface CompileResult { + status: string; + signal: string; + compiler_output: string; + compiler_error: string; + compiler_message: string; + program_output: string; + program_error: string; + program_message: string; + permlink: string; + url: string; +} + +export const compilerInfoFetcher: Fetcher = () => + fetch(new URL("/api/list.json", WANDBOX)).then( + (res) => res.json() as Promise + ); + +interface CompileProps { + compilerName: string; + compilerOptions: string[]; + compilerOptionsRaw: string[]; + codes: Code[]; +} +export interface CompileResultWithOutput extends CompileResult { + compilerOutput: ReplOutput[]; + compilerError: ReplOutput[]; + programOutput: ReplOutput[]; + programError: ReplOutput[]; +} + +export async function compileAndRun( + options: CompileProps +): Promise { + const result: CompileResult = await fetch( + new URL("/api/compile.json", WANDBOX), + { + method: "post", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + compiler: options.compilerName, + code: "", + codes: options.codes, + options: options.compilerOptions.join(","), + stdin: "", + "compiler-option-raw": options.compilerOptionsRaw.join("\n"), + "runtime-option-raw": "", + save: false, + is_private: true, + } satisfies CompileParameter), + } + ).then((res) => res.json()); + return { + ...result, + compilerOutput: result.compiler_output.trim() + ? result.compiler_output + .trim() + .split("\n") + .map((line) => ({ type: "stdout" as const, message: line })) + : [], + compilerError: result.compiler_error.trim() + ? result.compiler_error + .trim() + .split("\n") + .map((line) => ({ type: "error" as const, message: line })) + : [], + programOutput: result.program_output.trim() + ? result.program_output + .trim() + .split("\n") + .map((line) => ({ type: "stdout" as const, message: line })) + : [], + programError: result.program_error.trim() + ? result.program_error + .trim() + .split("\n") + .map((line) => ({ type: "error" as const, message: line })) + : [], + }; +} diff --git a/app/terminal/wandbox/cpp.ts b/app/terminal/wandbox/cpp.ts new file mode 100644 index 0000000..da004d2 --- /dev/null +++ b/app/terminal/wandbox/cpp.ts @@ -0,0 +1,197 @@ +import { ReplOutput } from "../repl"; +import { compileAndRun, CompilerInfo } from "./api"; + +const CPP_STACKTRACE_HANDLER = ` +#define BOOST_STACKTRACE_USE_ADDR2LINE +#include +#include +#include +void signal_handler(int signum) { + signal(signum, SIG_DFL); + switch(signum) { + case SIGILL: + std::cerr << "Illegal instruction" << std::endl; + break; + case SIGABRT: + std::cerr << "Aborted" << std::endl; + break; + case SIGBUS: + std::cerr << "Bus error" << std::endl; + break; + case SIGFPE: + std::cerr << "Floating point exception" << std::endl; + break; + case SIGSEGV: + std::cerr << "Segmentation fault" << std::endl; + break; + default: + std::cerr << "Signal " << signum << " received" << std::endl; + break; + } + std::cerr << "Stack trace:" << std::endl; + std::cerr << boost::stacktrace::stacktrace(); + raise(signum); +} +struct _init_signal_handler { + _init_signal_handler() { + signal(SIGILL, signal_handler); + signal(SIGABRT, signal_handler); + signal(SIGBUS, signal_handler); + signal(SIGFPE, signal_handler); + signal(SIGSEGV, signal_handler); + } +} _init_signal_handler_instance; +`; + +interface CppOptions { + compilerName: string; + compilerOptions: string[]; + compilerOptionsRaw: string[]; + commandline: string[]; // 表示用 +} + +// 使用可能なコンパイラーの情報からコンパイルオプションを決定する +// コマンドライン文字列表示用と実行時用で処理を共通化するため +function cppOptions(compilerList: CompilerInfo[]): CppOptions { + const compilerListForLang = compilerList.filter((c) => c.language === "C++"); + // headでない最初のgccを使う + const selectedCompiler = compilerListForLang.find( + (c) => c.name.includes("gcc") && !c.name.includes("head") + ); + if (!selectedCompiler) { + throw new Error("compiler not found"); + } + + const options: CppOptions = { + compilerName: selectedCompiler.name, + compilerOptions: [], + commandline: ["g++"], // selectedCompiler["display-compile-command"] + compilerOptionsRaw: [], + }; + + // singleオプション: + const warningSwitch = selectedCompiler.switches.find( + (s) => s.name === "warning" + ); + if (warningSwitch && warningSwitch.type === "single") { + options.compilerOptions.push("warning"); + options.commandline.push(warningSwitch["display-flags"]); + } else { + console.warn("warning switch not found"); + } + + // selectオプション: + for (const switchSelect of selectedCompiler.switches.filter( + (s) => s.type === "select" + )) { + // boost最新、stdは最新を選ぶ ほかはデフォルト + if (switchSelect.name.includes("boost")) { + const boostLatestOption = switchSelect.options + .filter((o) => !o.name.includes("nothing")) + .sort() + .reverse()[0]; + if (boostLatestOption) { + options.compilerOptions.push(boostLatestOption.name); + // options.commandline.push(boostLatestOption["display-flags"]); + } else { + console.warn("boost option not found"); + } + } else if (switchSelect.name.includes("std")) { + const stdLatestOption = switchSelect.options + .filter((o) => o.name.startsWith("c++")) + .sort() + .reverse()[0]; + if (stdLatestOption) { + options.compilerOptions.push(stdLatestOption.name); + options.commandline.push(stdLatestOption["display-flags"]); + } else { + console.warn("std option not found"); + } + } else { + const defaultOption = switchSelect.options.find( + (o) => o.name === switchSelect.default + ); + options.compilerOptions.push(switchSelect.default); + options.commandline.push(defaultOption!["display-flags"]); + } + } + + // その他オプション + options.compilerOptionsRaw.push("-g"); + // options.commandline.push("-g"); + + return options; +} + +export function getCppCommandlineStr( + compilerList: CompilerInfo[], + filenames: string[] +) { + return [ + ...cppOptions(compilerList).commandline, + ...filenames, + "&&", + "./a.out", + ].join(" "); +} + +export async function cppRunFiles( + compilerList: CompilerInfo[], + files: Record, + filenames: string[] +) { + const options = cppOptions(compilerList); + const result = await compileAndRun({ + compilerName: options.compilerName, + compilerOptions: options.compilerOptions, + compilerOptionsRaw: [ + ...options.compilerOptionsRaw, + ...filenames, + "_stacktrace.cpp", + ], + codes: [ + ...Object.entries(files).map(([name, code]) => ({ + file: name, + code: code || "", + })), + { file: "_stacktrace.cpp", code: CPP_STACKTRACE_HANDLER }, + ], + }); + + const traceIndex = result.programError.findIndex( + (line) => line.message === "Stack trace:" + ); + const outputs: ReplOutput[] = [ + ...result.compilerOutput, + ...result.compilerError, + ...result.programOutput, + ...(traceIndex >= 0 + ? result.programError.slice(0, traceIndex) + : result.programError), + ]; + if (traceIndex >= 0) { + // CPP_STACKTRACE_HANDLER のコードで出力されるスタックトレースを、js側でパースしていい感じに表示する + outputs.push({ + type: "trace" as const, + message: "Stack trace (filtered):", + }); + for (const line of result.programError.slice(traceIndex + 1)) { + // ユーザーのソースコードだけを対象にする + if (line.message.includes("/home/wandbox")) { + outputs.push({ + type: "trace" as const, + message: line.message.replace("/home/wandbox/", ""), + }); + } + } + } + if (result.status !== "0") { + outputs.push({ + type: "system" as const, + message: `ステータス ${result.status} で異常終了しました`, + }); + } + // TODO: result.signal はいつ使われるのか? + + return outputs; +} diff --git a/app/terminal/wandbox/wandbox.tsx b/app/terminal/wandbox/wandbox.tsx index 70fbe76..2abcdb3 100644 --- a/app/terminal/wandbox/wandbox.tsx +++ b/app/terminal/wandbox/wandbox.tsx @@ -1,31 +1,20 @@ "use client"; -import { - createContext, - ReactNode, - useCallback, - useContext, - useMemo, -} from "react"; +import { createContext, ReactNode, useCallback, useContext } from "react"; import { ReplOutput } from "../repl"; import { useFile } from "../file"; -import useSWR, { Fetcher } from "swr"; +import useSWR from "swr"; +import { compilerInfoFetcher } from "./api"; +import { cppRunFiles, getCppCommandlineStr } from "./cpp"; type WandboxLang = "C++"; interface IWandboxContext { - // filesの中から、namesSourceで指定されたファイルをソースコードとして、namesAdditionalで指定されたファイルを追加コードとして渡して実行する - runFiles: ( - lang: WandboxLang, - namesSource: string[], - namesAdditional: string[] - ) => Promise; - cppOptions: WandboxOptions | null; -} -interface WandboxOptions { - compilerName: string; - compilerOptions: string[]; - commandline: string; + // filesの中から、filenamesで指定されたものをコードとして渡して実行する + // ヘッダーとソースはまとめて渡す + runFiles: (lang: WandboxLang, filenames: string[]) => Promise; + // 表示用のコマンドライン文字列を取得 + getCommandlineStr: (lang: WandboxLang, filenames: string[]) => string; } const WandboxContext = createContext(null!); @@ -37,177 +26,42 @@ export function useWandbox() { return context; } -const WANDBOX = "https://wandbox.org"; -// https://github.com/melpon/wandbox/blob/ajax/kennel2/API.rst <- 古いけど、説明と例がある -// https://github.com/melpon/wandbox/blob/master/feline/src/types.rs -/* RustのVecはバイト配列ですが、serialize_with = "serialize_utf8"という指定があるため、 -JSONにシリアライズされる際にはUTF-8文字列に変換されると解釈し、TypeScriptの型はstringとします。 -by gemini -*/ -export interface SwitchOption { - name: string; - "display-flags": string; - "display-name": string; -} -export interface SwitchSingle { - type: "single"; - name: string; - "display-name": string; - "display-flags": string; - default: boolean; -} -export interface SwitchSelect { - type: "select"; - name: string; - options: SwitchOption[]; - default: string; -} -/** - * Rustの 'Switch' enum に対応するディスクリミネேテッドユニオン型です。 - * 'type' プロパティの値によって `SwitchSingle` か `SwitchSelect` かを判別できます。 - */ -export type Switch = SwitchSingle | SwitchSelect; -export interface CompilerInfo { - name: string; - version: string; - language: string; - "display-name": string; - templates: string[]; - "compiler-option-raw": boolean; - "runtime-option-raw": boolean; - "display-compile-command": string; - switches: Switch[]; -} -interface Code { - file: string; - code: string; -} -interface CompileParameter { - compiler: string; - code: string; - codes?: Code[]; - options?: string; - stdin?: string; - "compiler-option-raw"?: string; - "runtime-option-raw"?: string; - github_user?: string; - title?: string; - description?: string; - save?: boolean; - created_at?: number; - is_private?: boolean; - "compiler-info"?: CompilerInfo; -} -interface CompileResult { - status: string; - signal: string; - compiler_output: string; - compiler_error: string; - compiler_message: string; - program_output: string; - program_error: string; - program_message: string; - permlink: string; - url: string; -} - -const compilerInfoFetcher: Fetcher = () => - fetch(new URL("/api/list.json", WANDBOX)).then( - (res) => res.json() as Promise - ); - export function WandboxProvider({ children }: { children: ReactNode }) { const { files } = useFile(); const { data: compilerList, error } = useSWR("list", compilerInfoFetcher); if (error) { console.error("Failed to fetch compiler list from Wandbox:", error); } - const cppOptions = useMemo(() => { - if (!compilerList) { - return null; - } - const compilerListForLang = compilerList?.filter( - (c) => c.language === "C++" - ); - const compilerOptions: string[] = []; - const commandlineArgs: string[] = []; - - // headでない最初のgccを使う - const selectedCompiler = compilerListForLang.find( - (c) => c.name.includes("gcc") && !c.name.includes("head") - ); - if (!selectedCompiler) { - throw new Error("compiler not found"); - } - const compilerName = selectedCompiler.name; - // commandlineArgs.push(selectedCompiler["display-compile-command"]); - commandlineArgs.push("g++"); - - // singleオプション: - const warningSwitch = selectedCompiler.switches.find( - (s) => s.name === "warning" - ); - if (warningSwitch && warningSwitch.type === "single") { - compilerOptions.push("warning"); - commandlineArgs.push(warningSwitch["display-flags"]); - } else { - console.warn("warning switch not found"); - } - // selectオプション: - for (const switchSelect of selectedCompiler.switches.filter( - (s) => s.type === "select" - )) { - // boostはnothing、stdは最新を選ぶ ほかはデフォルト - if (switchSelect.name.includes("boost")) { - const boostNothingOption = switchSelect.options.find((o) => - o.name.includes("nothing") - ); - if (boostNothingOption) { - compilerOptions.push(boostNothingOption.name); - commandlineArgs.push(boostNothingOption["display-flags"]); - } else { - console.warn("boost nothing option not found"); - } - } else if (switchSelect.name.includes("std")) { - const stdLatestOption = switchSelect.options - .filter((o) => o.name.startsWith("c++")) - .sort() - .reverse()[0]; - if (stdLatestOption) { - compilerOptions.push(stdLatestOption.name); - commandlineArgs.push(stdLatestOption["display-flags"]); - } else { - console.warn("std option not found"); + const getCommandlineStr = useCallback( + (lang: WandboxLang, filenames: string[]) => { + if (compilerList) { + switch (lang) { + case "C++": + return getCppCommandlineStr(compilerList, filenames); + default: + lang satisfies never; + throw new Error(`unsupported language: ${lang}`); } } else { - const defaultOption = switchSelect.options.find( - (o) => o.name === switchSelect.default - ); - compilerOptions.push(switchSelect.default); - commandlineArgs.push(defaultOption!["display-flags"]); + return ""; } - } - - return { - compilerName, - compilerOptions, - commandline: commandlineArgs.join(" "), - } satisfies WandboxOptions; - }, [compilerList]); - + }, + [compilerList] + ); const runFiles = useCallback( - async ( - lang: WandboxLang, - namesSource: string[], - namesAdditional: string[] - ) => { - let options: WandboxOptions | null; + async (lang: WandboxLang, filenames: string[]) => { + if (!compilerList) { + return [ + { type: "error" as const, message: "Wandbox is not ready yet." }, + ]; + } + console.log(files) switch (lang) { case "C++": - options = cppOptions; - break; + return cppRunFiles(compilerList, files, filenames); default: + lang satisfies never; return [ { type: "error" as const, @@ -215,91 +69,12 @@ export function WandboxProvider({ children }: { children: ReactNode }) { }, ]; } - if (!options) { - return [ - { type: "error" as const, message: "Wandbox is not ready yet." }, - ]; - } - const result: CompileResult = await fetch( - new URL("/api/compile.json", WANDBOX), - { - method: "post", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - compiler: options.compilerName, - code: "", - codes: namesSource - .map((name) => ({ - file: name, - code: files[name] || "", - })) - .concat( - namesAdditional.map((name) => ({ - file: name, - code: files[name] || "", - })) - ), - options: options.compilerOptions.join(","), - stdin: "", - "compiler-option-raw": namesSource.join("\n"), - "runtime-option-raw": "", - save: false, - is_private: true, - } satisfies CompileParameter), - } - ).then((res) => res.json()); - - let outputs: ReplOutput[] = []; - if (result.compiler_output) { - outputs = outputs.concat( - result.compiler_output - .trim() - .split("\n") - .map((line) => ({ type: "stdout" as const, message: line })) - ); - } - if (result.compiler_error) { - outputs = outputs.concat( - result.compiler_error - .trim() - .split("\n") - .map((line) => ({ type: "error" as const, message: line })) - ); - } - if (result.program_output) { - outputs = outputs.concat( - result.program_output - .trim() - .split("\n") - .map((line) => ({ type: "stdout" as const, message: line })) - ); - } - if (result.program_error) { - outputs = outputs.concat( - result.program_error - .trim() - .split("\n") - .map((line) => ({ type: "error" as const, message: line })) - ); - } - if (result.status !== "0") { - outputs.push({ - type: "system" as const, - message: `ステータス ${result.status} で異常終了しました`, - }); - } - // TODO: result.signal はいつ使われるのか? - - console.log(outputs); - return outputs; }, - [files, cppOptions] + [compilerList, files] ); return ( - + {children} ); diff --git a/public/docs/cpp-1.md b/public/docs/cpp-1.md new file mode 100644 index 0000000..4bc6e9a --- /dev/null +++ b/public/docs/cpp-1.md @@ -0,0 +1,129 @@ +# 第1章: C++の世界へようこそ + +C++プログラミングの全体像を掴む章へようこそ! 🎉 この章では、あなたがこれまでに培ってきたプログラミングの知識を土台に、C++ならではの特徴や文化に触れていきます。他の言語との違いを意識しながら、まずは「**ソースコードを書き、コンパイルして実行する**」というC++の基本的な開発フローを体験しましょう。 + +## C++とは? + +C++は、C言語を拡張して作られた、非常にパワフルで汎用性の高いプログラミング言語です。その歴史は古く、1983年にBjarne Stroustrupによって開発が始まりました。 + +### 特徴 + +他の言語と比較したC++の特徴は以下の通りです。 + + * **パフォーマンス 🚀:** C++は、OSやハードウェアに近い低レベルな操作が可能で、実行速度が非常に高速です。このため、パフォーマンスが最重要視される場面で絶大な信頼を得ています。 + * **静的型付け:** 変数の型はコンパイル時に決定されます。これにより、実行前に多くのエラーを発見でき、大規模な開発でもコードの安全性を保ちやすくなります。 + * **マルチパラダイム:** 手続き型プログラミング、オブジェクト指向プログラミング、ジェネリックプログラミングなど、様々なプログラミングスタイルをサポートしています。これにより、問題の性質に合わせて最適なアプローチを選択できます。 + * **C言語との互換性:** C言語のコードの多くは、ほとんどそのままC++のコードとしてコンパイルできます。C言語で書かれた膨大なソフトウェア資産を活用できるのは大きな利点です。 + +### C++が使われる分野 + +その高いパフォーマンスと柔軟性から、C++は以下のような幅広い分野の第一線で活躍しています。 + + * **ゲーム開発:** Unreal EngineやUnity(一部)など、多くの有名ゲームエンジンがC++で開発されています。キャラクターの物理演算やリアルタイムグラフィックス描画など、速度が求められる処理に不可欠です。 + * **OS開発:** Windows, macOS, Linuxといった主要なオペレーティングシステムのカーネルやシステムコンポーネントの多くがC++で記述されています。 + * **金融システム:** 1ミリ秒を争う高頻度取引(HFT)システムなど、超低遅延が求められる金融アプリケーションで使用されています。 + * **組み込みシステム・IoT:** 自動車のエンジン制御ユニット(ECU)や家電、産業用ロボットなど、リソースが限られた環境でも高速に動作する必要があります。 + +## 開発環境のセットアップ + +C++プログラムを実行するには、**コンパイラ**が必要です。コンパイラは、人間が書いたC++のソースコードを、コンピュータが理解できる機械語に翻訳するツールです。 + +このウェブサイト上ではブラウザ上でコードを編集し実行できる環境を提供しており、特別なセットアップは不要です。しかし、ローカル環境でC++を学びたい場合は、以下のようなコンパイラとIDE(統合開発環境)をインストールすることをお勧めします。 + +### コンパイラとIDE + +* **コンパイラ:** + * **GCC (GNU Compiler Collection):** Linuxで標準的に使われるコンパイラ。Windows (MinGW/MSYS2) やmacOSでも利用可能です。 + * **Clang:** AppleのXcodeで標準的に使われているコンパイラ。エラーメッセージが分かりやすいと評判です。 + * **MSVC (Microsoft Visual C++):** MicrosoftのVisual Studioに付属するコンパイラ。Windows開発の標準です。 +* **IDE (統合開発環境) / エディタ:** + * **Visual Studio (Windows):** コンパイラ、エディタ、デバッガなど、C++開発に必要な全てが詰まった強力なIDEです。初心者には特におすすめです。 + * **Visual Studio Code (Windows/macOS/Linux):** 軽量なエディタですが、拡張機能を入れることで強力なC++開発環境を構築できます。 + +### おすすめのセットアップ + + * **Windows:** **Visual Studio Community** をインストールするのが最も簡単です。インストーラーで「C++によるデスクトップ開発」ワークロードを選択すれば、必要なものがすべて揃います。 + * **macOS:** ターミナルで `xcode-select --install` を実行し、**Xcode Command Line Tools** をインストールします。これにはClangコンパイラが含まれます。エディタは **Visual Studio Code** がおすすめです。 + * **Linux (Ubuntu/Debian系):** ターミナルで `sudo apt update && sudo apt install build-essential g++` を実行してGCCコンパイラをインストールします。エディタは **Visual Studio Code** がおすすめです。 + + +## 最初のプログラム + +環境が整ったら、さっそく定番の "Hello, World\!" プログラムを作成し、C++開発の流れを掴みましょう。 + +`main.cpp` という名前でファイルを作成し、以下のコードを記述してください。 + +```cpp:main.cpp +// 画面に "Hello, World!" と表示するプログラム + +#include + +int main() { + std::cout << "Hello, World!" << std::endl; + return 0; +} +``` + +### コンパイルと実行 + +このコードを実行するには、まず**コンパイル**して実行可能ファイルを生成する必要があります。ターミナル(Windowsの場合はコマンドプロンプトやPowerShell)で以下のコマンドを実行します。 + +```bash +# g++ (GCC) または clang++ (Clang) を使ってコンパイル +# -o main は出力ファイル名を main にするという意味 +g++ main.cpp -o main + +# 生成された実行可能ファイルを実行 +./main +``` + +```powershell +# Visual C++ (MSVC) を使う場合 +cl main.cpp /Fe:main.exe + +# 生成された実行可能ファイルを実行 +main.exe +``` + +このウェブサイト上の実行環境で動かす場合は、以下の実行ボタンをクリックしてください。 +実行すると、ターミナルに以下のように表示されるはずです。 + +```cpp-exec:main.cpp +Hello, World! +``` + +PythonやJavaScriptのようなインタプリタ言語とは異なり、C++では「コンパイル」という一手間が必要です。このステップにより、実行前にコード全体がチェックされ、高速なネイティブコードが生成されます。 + +## C++の基本構造 + +先ほどの "Hello, World\!" プログラムには、C++の基本的な要素が詰まっています。一つずつ見ていきましょう。 + +### `#include ` - ヘッダファイルのインクルード + +`#include` は、他のファイルに書かれた機能を利用するための**プリプロセッサ命令**です。ここでは、コンソールへの入出力機能を提供する `iostream` という標準ライブラリのヘッダファイルを読み込んでいます。これにより、`std::cout` などが使えるようになります。 + + * `< >` で囲む: 標準ライブラリのヘッダファイルをインクルードする場合 + * `" "` で囲む: 自分で作成したヘッダファイルをインクルードする場合 (後の章で学びます) + +### `int main()` - main関数 + +`main`関数は、C++プログラムのエントリーポイント(**実行開始点**)です。OSはプログラムを実行するとき、まずこの`main`関数を呼び出します。 + + * `int`: `main`関数が整数 (integer) の値を返すことを示します。この戻り値はプログラムの終了ステータスとしてOSに渡され、`0` は**正常終了**を意味するのが慣例です。 + * `()`: 関数が引数を取らないことを示しています。(引数を取ることも可能です) + +### `std::cout` と `std::endl` - 名前空間 (namespace) + + * `std::cout`: "character output stream" の略で、コンソールへの標準出力を担当します。 + * `<<`: **ストリーム挿入演算子**と呼ばれ、右側のデータを左側のストリーム(ここでは `std::cout`)に流し込むイメージです。 + * `std::endl`: "end line" の略で、改行を出力し、出力バッファをフラッシュ(強制的に出力)します。 + * `std::`: `cout` や `endl` が `std` という**名前空間 (namespace)** に属していることを示します。名前空間は、大規模なプログラムで関数や変数の名前が衝突するのを防ぐための仕組みです。`std` はC++の標準ライブラリが定義されている名前空間です。 + +## この章のまとめ + + * C++は、**パフォーマンス**と**多機能性**を両立した強力なプログラミング言語です。 + * C++の開発フローは、「**ソースコード作成 → コンパイル → 実行**」が基本です。 + * すべてのC++プログラムは `main` 関数から実行が始まります。 + * `#include` で外部ライブラリの機能を読み込み、`namespace` で名前の衝突を避けます。 + +これであなたもC++プログラマの仲間入りです!次の章では、C++の根幹をなす型システムとメモリの仕組みについて学んでいきましょう。 diff --git a/public/docs/cpp-10.md b/public/docs/cpp-10.md new file mode 100644 index 0000000..50887e9 --- /dev/null +++ b/public/docs/cpp-10.md @@ -0,0 +1,301 @@ +# 第10章: 標準テンプレートライブラリ (STL) ②:アルゴリズムとラムダ式 + +ようこそ、C++チュートリアルの第10章へ!前の章ではSTLのコンテナについて学び、様々なデータを効率的に格納する方法を見てきました。しかし、データを格納するだけではプログラムは完成しません。そのデータを並べ替えたり、検索したり、特定の処理を施したりといった「操作」が必要です。 + +この章では、STLのもう一つの強力な柱である**アルゴリズム**ライブラリを学びます。これらのアルゴリズムは、コンテナ内のデータを操作するための汎用的な関数群です。そして、アルゴリズムをさらに柔軟かつ強力にするための現代的なC++の機能、**ラムダ式**についても解説します。これらをマスターすれば、驚くほど少ないコードで複雑なデータ操作が実現できるようになります。 + +## イテレータ:コンテナとアルゴリズムを繋ぐ架け橋 + +アルゴリズムは、特定のコンテナ(`std::vector` や `std::list` など)に直接依存しないように設計されています。では、どうやってコンテナ内の要素にアクセスするのでしょうか?そこで登場するのが**イテレータ (Iterator)** です。 + +イテレータは、コンテナ内の要素を指し示す「ポインタのような」オブジェクトです。ポインタのように `*` で要素の値を参照したり、`++` で次の要素に進んだりできます。 + +ほとんどのコンテナは、以下の2つの重要なイテレータを取得するメンバ関数を持っています。 + + * `begin()`: コンテナの先頭要素を指すイテレータを返す。 + * `end()`: コンテナの**最後の要素の次**を指すイテレータを返す。これは有効な要素を指していない「番兵」のような役割を果たします。 + +アルゴリズムは、この `begin()` と `end()` から得られるイテレータのペアを使い、操作対象の「範囲」を指定します。範囲は半開区間 `[begin, end)` で表され、`begin` が指す要素は範囲に含まれ、`end` が指す要素は含まれません。 + +簡単な例を見てみましょう。イテレータを使って `vector` の全要素を表示するコードです。 + +```cpp:iterator_example.cpp +#include +#include + +int main() { + std::vector numbers = {0, 1, 2, 3, 4}; + + // イテレータを使ってコンテナを走査 + std::cout << "Numbers: "; + for (auto it = numbers.begin(); it != numbers.end(); ++it) { + std::cout << *it << " "; // *it で要素の値にアクセス + } + std::cout << std::endl; + + // C++11以降の範囲ベースforループ (内部ではイテレータが使われている) + std::cout << "Numbers (range-based for): "; + for (int num : numbers) { + std::cout << num << " "; + } + std::cout << std::endl; + + return 0; +} +``` + +```cpp-exec:iterator_example.cpp +Numbers: 0 1 2 3 4 +Numbers (range-based for): 0 1 2 3 4 +``` + +このように、イテレータはコンテナの種類を問わず、統一的な方法で要素にアクセスする仕組みを提供します。これが、アルゴリズムが様々なコンテナに対して汎用的に機能する理由です。 + +## 便利なアルゴリズム + +C++の標準ライブラリには、`` ヘッダと `` ヘッダに数多くの便利なアルゴリズムが用意されています。ここでは、特によく使われるものをいくつか紹介します。 + +### `std::sort`: 要素を並べ替える + +名前の通り、指定された範囲の要素をソートします。デフォルトでは昇順に並べ替えます。 + +```cpp:sort_example.cpp +#include +#include +#include // std::sort のために必要 +#include + +int main() { + std::vector numbers = {5, 2, 8, 1, 9}; + + // numbers.begin() から numbers.end() の範囲をソート + std::sort(numbers.begin(), numbers.end()); + + std::cout << "Sorted numbers: "; + for (int num : numbers) { + std::cout << num << " "; + } + std::cout << std::endl; + + std::vector words = {"banana", "apple", "cherry"}; + std::sort(words.begin(), words.end()); + + std::cout << "Sorted words: "; + for (const auto& word : words) { + std::cout << word << " "; + } + std::cout << std::endl; + + return 0; +} +``` + +```cpp-exec:sort_example.cpp +Sorted numbers: 1 2 5 8 9 +Sorted words: apple banana cherry +``` + +### `std::find`: 要素を検索する + +指定された範囲から特定の値を持つ要素を検索します。 + + * **見つかった場合**: その要素を指すイテレータを返します。 + * **見つからなかった場合**: 範囲の終端を示すイテレータ (`end()`) を返します。 + +この性質を利用して、要素が存在するかどうかをチェックできます。 + +```cpp:find_example.cpp +#include +#include +#include // std::find のために必要 + +int main() { + std::vector numbers = {10, 20, 30, 40, 50}; + int value_to_find = 30; + + // numbers の中から 30 を探す + auto it = std::find(numbers.begin(), numbers.end(), value_to_find); + + if (it != numbers.end()) { + // 見つかった場合 + std::cout << "Found " << *it << " at index " << std::distance(numbers.begin(), it) << std::endl; + } else { + // 見つからなかった場合 + std::cout << value_to_find << " not found." << std::endl; + } + + value_to_find = 99; + it = std::find(numbers.begin(), numbers.end(), value_to_find); + + if (it != numbers.end()) { + std::cout << "Found " << *it << std::endl; + } else { + std::cout << value_to_find << " not found." << std::endl; + } + + return 0; +} +``` + +```cpp-exec:find_example.cpp +Found 30 at index 2 +99 not found. +``` + +### `std::for_each`: 各要素に処理を適用する + +指定された範囲の全ての要素に対して、特定の関数(処理)を適用します。ループを書くよりも意図が明確になる場合があります。 + +```cpp +// 3番目の引数に関数を渡す +std::for_each(numbers.begin(), numbers.end(), print_function); +``` + +ここで「特定の処理」をその場で手軽に記述する方法が**ラムダ式**です。 + +## ラムダ式:その場で書ける無名関数 + +ラムダ式(Lambda Expression)は、C++11から導入された非常に強力な機能です。一言で言えば、「**その場で定義して使える名前のない小さな関数**」です。これにより、アルゴリズムに渡すためだけの短い関数をわざわざ定義する必要がなくなり、コードが非常に簡潔になります。 + +ラムダ式の基本的な構文は以下の通りです。 + +```cpp +[キャプチャ](引数リスト) -> 戻り値の型 { 処理本体 } +``` + + * `[]` **キャプチャ句**: ラムダ式の外にある変数を取り込んで、式の中で使えるようにします。 + * `[]`: 何もキャプチャしない。 + * `[=]`: 外の変数を全て値渡し(コピー)でキャプチャする。 + * `[&]`: 外の変数を全て参照渡しでキャプチャする。 + * `[x, &y]`: 変数 `x` は値渡し、変数 `y` は参照渡しでキャプチャする。 + * `()` **引数リスト**:通常の関数と同じ引数を取ることができます。 + * `-> 戻り値の型`: 戻り値の型を指定します。多くの場合、コンパイラが推論できるため省略可能です。 + * `{}` **処理本体**: 関数の処理内容を記述します。 + +`std::for_each` とラムダ式を組み合わせた例を見てみましょう。 + +```cpp:for_each_lambda_example.cpp +#include +#include +#include + +int main() { + std::vector numbers = {1, 2, 3, 4, 5}; + + // 各要素を2倍して表示する + std::cout << "Doubled numbers: "; + std::for_each(numbers.begin(), numbers.end(), [](int n) { + std::cout << n * 2 << " "; + }); + std::cout << std::endl; + + // 外部の変数をキャプチャする例 + int sum = 0; + // `&sum` で sum を参照キャプチャし、ラムダ式内で変更できるようにする + std::for_each(numbers.begin(), numbers.end(), [&sum](int n) { + sum += n; + }); + + std::cout << "Sum: " << sum << std::endl; + + return 0; +} +``` + +```cpp-exec:for_each_lambda_example.cpp +Doubled numbers: 2 4 6 8 10 +Sum: 15 +``` + +このコードは、`for_each` の3番目の引数に直接処理を書き込んでいます。非常に直感的で読みやすいと思いませんか? + +ラムダ式は、特に `std::sort` のソート順をカスタマイズする際に真価を発揮します。例えば、数値を降順にソートしたい場合、比較ルールをラムダ式で与えることができます。 + +```cpp:lambda_sort_example.cpp +#include +#include +#include + +int main() { + std::vector numbers = {5, 2, 8, 1, 9}; + + // 比較関数としてラムダ式を渡す + // a > b であれば true を返すことで降順ソートになる + std::sort(numbers.begin(), numbers.end(), [](int a, int b) { + return a > b; + }); + + std::cout << "Sorted in descending order: "; + for (int num : numbers) { + std::cout << num << " "; + } + std::cout << std::endl; + + return 0; +} +``` + +```cpp-exec:lambda_sort_example.cpp +Sorted in descending order: 9 8 5 2 1 +``` + +## この章のまとめ + +この章では、STLのアルゴリズムとラムダ式について学びました。 + + * **イテレータ**は、コンテナの要素を指し示すオブジェクトであり、アルゴリズムとコンテナの間のインターフェースとして機能します。 + * `` ヘッダには、**`std::sort`** (ソート)、**`std::find`** (検索)、**`std::for_each`** (繰り返し処理) といった、汎用的で強力なアルゴリズムが多数用意されています。 + * **ラムダ式**は、その場で定義できる無名関数であり、アルゴリズムに渡す処理を簡潔かつ直感的に記述することができます。 + * **キャプチャ**機能を使うことで、ラムダ式の外にある変数を取り込んで処理に利用できます。 + +コンテナ、イテレータ、アルゴリズム、そしてラムダ式。これらを組み合わせることで、C++におけるデータ処理は、他の多くの言語に引けを取らない、あるいはそれ以上に表現力豊かで効率的なものになります。 + +### 練習問題1: 文字列の長さでソート + +`std::vector` を用意し、格納されている文字列を、文字数が短い順にソートして、結果を出力するプログラムを作成してください。`std::sort` とラムダ式を使用してください。 + +**ヒント**: ラムダ式は2つの文字列を引数に取り、1つ目の文字列の長さが2つ目の文字列の長さより短い場合に `true` を返すように実装します。 + + +```cpp:practice10_1.cpp +#include +#include +#include + +int main() { + std::vector words = {"apple", "banana", "kiwi", "cherry", "fig", "grape"}; + + return 0; +} +``` + +```cpp-exec:practice10_1.cpp +fig kiwi grape apple banana cherry +``` + +### 練習問題2: 条件に合う要素のカウント + +`std::vector` に整数をいくつか格納します。その後、ラムダ式と `std::for_each`(または他のアルゴリズム)を使って、以下の2つの条件を満たす要素がそれぞれいくつあるかを数えて出力してください。 + +1. 正の偶数である要素の数 +2. 負の奇数である要素の数 + +**ヒント**: カウント用の変数を2つ用意し、ラムダ式のキャプチャ句で参照キャプチャ (`[&]`) して、式の中でインクリメントします。 + +```cpp:practice10_2.cpp +#include +#include +#include + +int main() { + std::vector numbers = {3, -1, 4, -5, 6, -7, 8, 0, -2}; + + + return 0; +} +``` + +```cpp-exec:practice10_2.cpp +Positive even count: 3 +Negative odd count: 3 +``` diff --git a/public/docs/cpp-11.md b/public/docs/cpp-11.md new file mode 100644 index 0000000..909494c --- /dev/null +++ b/public/docs/cpp-11.md @@ -0,0 +1,396 @@ +# 第11章: モダンC++の流儀:RAIIとスマートポインタ + +これまでの章で、`new` と `delete` を使った動的なメモリ管理を学びました。しかし、これらの手動管理は `delete` の呼び忘れによるメモリリークや、複雑なコードでのリソース管理の煩雑さを引き起こす原因となりがちです。 + +C++11以降の「モダンC++」では、こうした問題を解決するための洗練された仕組みが導入されました。この章では、エラーハンドリングのための**例外処理**、リソース管理の基本思想である **RAIIイディオム**、そしてそれを具現化する**スマートポインタ** (`std::unique_ptr`, `std::shared_ptr`) について学び、より安全で堅牢なコードを書くための流儀を身につけます。 + +## 例外処理: try, catch を使ったエラーハンドリング + +プログラムでは、ファイルの読み込み失敗やメモリ確保の失敗など、予期せぬエラーが発生することがあります。C++では、このような状況を処理するために**例外 (Exception)** という仕組みが用意されています。 + +例外処理は、以下の3つのキーワードで構成されます。 + + * `throw`: 例外的な状況が発生したことを知らせるために、例外オブジェクトを「投げる」。 + * `try`: 例外が発生する可能性のあるコードブロックを囲む。 + * `catch`: `try` ブロック内で投げられた例外を「捕まえて」処理する。 + +基本的な構文を見てみましょう。 + +```cpp:exception_basic.cpp +#include +#include // std::runtime_error のために必要 + +// 0で割ろうとしたら例外を投げる関数 +double divide(int a, int b) { + if (b == 0) { + // エラー内容を示す文字列を渡して例外オブジェクトを作成し、投げる + throw std::runtime_error("Division by zero!"); + } + return static_cast(a) / b; +} + +int main() { + int a = 10; + int b = 0; + + try { + // 例外が発生する可能性のあるコード + std::cout << "Trying to divide..." << std::endl; + double result = divide(a, b); + std::cout << "Result: " << result << std::endl; // この行は実行されない + } catch (const std::runtime_error& e) { + // std::runtime_error 型の例外をここで捕まえる + std::cerr << "Caught an exception: " << e.what() << std::endl; + } + + std::cout << "Program finished." << std::endl; + return 0; +} +``` + +```cpp-exec:exception_basic.cpp +Trying to divide... +Caught an exception: Division by zero! +Program finished. +``` + +`divide` 関数内で `b` が0だった場合に `throw` が実行され、関数の実行は即座に中断されます。制御は呼び出し元の `catch` ブロックに移り、そこでエラー処理が行われます。これにより、エラーが発生してもプログラム全体がクラッシュすることなく、安全に処理を続行できます。 + +### 例外とリソースリーク + +ここで、`new` と `delete` を使った手動のメモリ管理と例外処理が組み合わさると、問題が発生します。 + +```cpp:raw_pointer_problem.cpp +#include +#include + +void process_data() { + int* data = new int[100]; // リソース確保 + std::cout << "Data allocated." << std::endl; + + // 何らかの処理... + bool something_wrong = true; + if (something_wrong) { + throw std::runtime_error("Something went wrong during processing!"); + } + + // 例外が投げられると、この行には到達しない + std::cout << "Deleting data..." << std::endl; + delete[] data; // リソース解放 +} + +int main() { + try { + process_data(); + } catch (const std::runtime_error& e) { + std::cerr << "Error: " << e.what() << std::endl; + } + // process_data内で確保されたメモリは解放されないままである! + return 0; +} +``` + +```cpp-exec:raw_pointer_problem.cpp +Data allocated. +Error: Something went wrong during processing! +``` + +この例では、`process_data` 関数内で `throw` が実行されると、関数の実行が中断され `catch` ブロックにジャンプします。その結果、`delete[] data;` の行が実行されず、確保されたメモリが解放されない**メモリリーク**が発生します。 + +この問題を解決するのが、C++の最も重要な設計思想の一つである **RAII** です。 + +## RAIIイディオム + +**RAII (Resource Acquisition Is Initialization)** は、「リソースの確保は、オブジェクトの初期化時に行い、リソースの解放は、オブジェクトの破棄時に行う」という設計パターンです。日本語では「リソース取得は初期化である」と訳されます。 + +C++では、オブジェクトがそのスコープ(変数が宣言された `{}` の範囲)を抜けるときに、そのオブジェクトの**デストラクタ**が自動的に呼び出されます。この仕組みは、関数が正常に終了した場合だけでなく、**例外が投げられてスコープを抜ける場合でも保証されています**。 + +RAIIはこの性質を利用して、リソースの解放処理をデストラクタに記述することで、リソースの解放を自動化し、`delete` の呼び忘れや例外発生時のリソースリークを防ぎます。 + +簡単なRAIIクラスの例を見てみましょう。 + +```cpp:raii_concept.cpp +#include + +class ResourceWrapper { +private: + int* m_data; + +public: + // コンストラクタでリソースを確保 + ResourceWrapper() { + m_data = new int[10]; + std::cout << "Resource acquired." << std::endl; + } + + // デストラクタでリソースを解放 + ~ResourceWrapper() { + delete[] m_data; + std::cout << "Resource released." << std::endl; + } +}; + +void use_resource() { + ResourceWrapper rw; // オブジェクトが生成され、コンストラクタでリソースが確保される + std::cout << "Using resource..." << std::endl; + + // この関数が終了するとき (正常終了でも例外でも)、 + // rwのデストラクタが自動的に呼ばれ、リソースが解放される +} + +int main() { + std::cout << "Entering main." << std::endl; + use_resource(); + std::cout << "Exiting main." << std::endl; + return 0; +} +``` + +```cpp-exec:raii_concept.cpp +Entering main. +Resource acquired. +Using resource... +Resource released. +Exiting main. +``` + +`use_resource` 関数が終了すると、`rw` オブジェクトがスコープを抜けるため、`ResourceWrapper` のデストラクタが自動的に呼び出され、`delete[]` が実行されます。もし `use_resource` の中で例外が発生したとしても、デストラクタは保証付きで呼び出されます。 + +この強力なRAIIイディオムを、動的メモリ管理のために標準ライブラリが提供してくれているのが**スマートポインタ**です。 + +## スマートポインタ: new/deleteを自動化する + +スマートポインタは、RAIIを実装したクラステンプレートで、生ポインタ (`int*` など) のように振る舞いながら、リソース (確保したメモリ) の所有権を管理し、適切なタイミングで自動的に解放してくれます。 + +モダンC++では、メモリ管理に生ポインタを直接使うことはほとんどなく、スマートポインタを使うのが基本です。主に2種類のスマートポインタを使い分けます。 + +### `std::unique_ptr` + +`std::unique_ptr` は、管理するオブジェクトの**所有権を唯一に保つ**スマートポインタです。つまり、あるオブジェクトを指す `unique_ptr` は、常に一つしか存在できません。 + + * **唯一の所有権**: コピーが禁止されています。オブジェクトの所有権を別の `unique_ptr` に移したい場合は、**ムーブ (`std::move`)** を使います。 + * **軽量**: ポインタ一つ分のサイズしかなく、オーバーヘッドが非常に小さいです。 + +`unique_ptr` を作成するには、`std::make_unique` を使うのが安全で推奨されています。 + +```cpp:unique_ptr_example.cpp +#include +#include // スマートポインタのために必要 +#include // std::moveのために必要 + +struct MyData { + MyData() { std::cout << "MyData constructor" << std::endl; } + ~MyData() { std::cout << "MyData destructor" << std::endl; } + void greet() { std::cout << "Hello from MyData!" << std::endl; } +}; + +void process_ptr(std::unique_ptr ptr) { + std::cout << "Inside process_ptr" << std::endl; + ptr->greet(); + // ptrがこの関数のスコープを抜けるときにデストラクタが呼ばれる +} + +int main() { + std::cout << "--- Block 1 ---" << std::endl; + { + // std::make_unique を使ってオブジェクトを生成し、unique_ptrで管理 + std::unique_ptr u_ptr1 = std::make_unique(); + + // 生ポインタと同じように -> や * でメンバにアクセスできる + u_ptr1->greet(); + + // コピーはコンパイルエラーになる + // std::unique_ptr u_ptr2 = u_ptr1; // ERROR! + + // 所有権を u_ptr3 に移動 (ムーブ) + std::unique_ptr u_ptr3 = std::move(u_ptr1); + + // ムーブ後、u_ptr1 は空(nullptr)になる + if (u_ptr1 == nullptr) { + std::cout << "u_ptr1 is now empty." << std::endl; + } + + u_ptr3->greet(); + } // ブロックを抜けると u_ptr3 が破棄され、MyDataのデストラクタが呼ばれる + + std::cout << "\n--- Block 2 ---" << std::endl; + { + auto u_ptr4 = std::make_unique(); + // 関数の引数に渡すことで所有権を譲渡する + process_ptr(std::move(u_ptr4)); + std::cout << "Returned from process_ptr" << std::endl; + } + + std::cout << "\nProgram finished." << std::endl; + return 0; +} +``` + +```cpp-exec:unique_ptr_example.cpp +--- Block 1 --- +MyData constructor +Hello from MyData! +u_ptr1 is now empty. +Hello from MyData! +MyData destructor + +--- Block 2 --- +MyData constructor +Inside process_ptr +Hello from MyData! +MyData destructor +Returned from process_ptr + +Program finished. +``` + +`unique_ptr` は、オブジェクトの所有者が誰であるかが明確な場合に最適です。基本的にはまず `unique_ptr` を使うことを検討しましょう。 + +### `std::shared_ptr` + +`std::shared_ptr` は、管理するオブジェクトの**所有権を複数のポインタで共有できる**スマートポインタです。 + + * **共有された所有権**: `shared_ptr` は自由にコピーできます。コピーされるたびに、内部の**参照カウンタ**が増加します。 + * **自動解放**: `shared_ptr` が破棄される(デストラクタが呼ばれる)と参照カウンタが減少し、**参照カウンタが0になったとき**に、管理しているオブジェクトが解放(`delete`)されます。 + * **オーバーヘッド**: 参照カウンタを管理するための追加のメモリと処理が必要なため、`unique_ptr` よりもわずかにオーバーヘッドが大きいです。 + +`shared_ptr` を作成するには、`std::make_shared` を使うのが効率的で安全です。 + +```cpp:shared_ptr_example.cpp +#include +#include +#include + +struct MyResource { + MyResource() { std::cout << "MyResource constructor" << std::endl; } + ~MyResource() { std::cout << "MyResource destructor" << std::endl; } +}; + +int main() { + std::shared_ptr s_ptr1; // 空のshared_ptr + + std::cout << "--- Block 1 ---" << std::endl; + { + // std::make_shared を使ってオブジェクトを生成し、shared_ptrで管理 + s_ptr1 = std::make_shared(); + std::cout << "Use count: " << s_ptr1.use_count() << std::endl; // 1 + + { + // s_ptr2 は s_ptr1 と同じオブジェクトを指す + std::shared_ptr s_ptr2 = s_ptr1; + std::cout << "Use count: " << s_ptr1.use_count() << std::endl; // 2 + std::cout << "Use count: " << s_ptr2.use_count() << std::endl; // 2 + } // s_ptr2がスコープを抜ける。参照カウンタが1に減る + + std::cout << "Use count after s_ptr2 is gone: " << s_ptr1.use_count() << std::endl; // 1 + } // s_ptr1がスコープを抜ける。参照カウンタが0になり、オブジェクトが破棄される + + std::cout << "\n--- Block 2 ---" << std::endl; + { + auto shared_res = std::make_shared(); + std::cout << "Initial use count: " << shared_res.use_count() << std::endl; // 1 + + std::vector> ptr_vec; + ptr_vec.push_back(shared_res); // コピー。参照カウンタは2 + ptr_vec.push_back(shared_res); // コピー。参照カウンタは3 + + std::cout << "Use count after pushing to vector: " << shared_res.use_count() << std::endl; // 3 + } // shared_resとptr_vecがスコープを抜ける。 + // 全てのshared_ptrが破棄され、最後に参照カウンタが0になり、オブジェクトが破棄される + + std::cout << "\nProgram finished." << std::endl; + return 0; +} +``` + +```cpp-exec:shared_ptr_example.cpp +--- Block 1 --- +MyResource constructor +Use count: 1 +Use count: 2 +Use count: 2 +Use count after s_ptr2 is gone: 1 +MyResource destructor + +--- Block 2 --- +MyResource constructor +Initial use count: 1 +Use count after pushing to vector: 3 +MyResource destructor + +Program finished. +``` + +`shared_ptr` は、オブジェクトの寿命が単一のスコープや所有者に縛られず、複数のオブジェクトから共有される必要がある場合に便利です。ただし、所有権の関係が複雑になりがちなので、本当に共有が必要な場面に限定して使いましょう。 + +## この章のまとめ + + * **例外処理**は `try`, `catch`, `throw` を使い、エラーが発生してもプログラムを安全に継続させるための仕組みです。 + * 手動のメモリ管理下で例外が発生すると、**リソースリーク**を引き起こす危険があります。 + * **RAIIイディオム**は、リソースの確保をコンストラクタ、解放をデストラクタで行うことで、リソース管理を自動化するC++の重要な設計思想です。 + * **スマートポインタ**はRAIIを動的メモリ管理に適用したもので、`new` と `delete` の手動管理を不要にします。 + * **`std::unique_ptr`** はオブジェクトの**唯一の所有権**を管理します。軽量であり、所有権が明確な場合に第一の選択肢となります。 + * **`std::shared_ptr`** はオブジェクトの**所有権を共有**します。参照カウントによって管理され、最後の所有者がいなくなったときにオブジェクトを解放します。 + +モダンC++プログラミングでは、`new` と `delete` を直接書くことは極力避け、RAIIとスマートポインタを全面的に活用することが、安全でメンテナンス性の高いコードへの第一歩です。 + +### 練習問題1: `unique_ptr` と所有権の移動 + +`Employee` という名前のクラスを作成してください。このクラスは、コンストラクタで社員名を受け取って表示し、デストラクタで「(社員名) is leaving.」というメッセージを表示します。 + +`main` 関数で、`"Alice"` という名前の `Employee` オブジェクトを `std::make_unique` で作成し、その `unique_ptr` を `promote_employee` という関数に渡してください。`promote_employee` 関数は `unique_ptr` を引数として受け取り(所有権が移動します)、「(社員名) has been promoted\!」というメッセージを表示します。 + +プログラムを実行し、コンストラクタとデストラクタのメッセージが期待通りに表示されることを確認してください。 + +```cpp:practice11_1.cpp +#include +#include +#include + +// ここにEmployeeクラスを定義 + + +int main() { + + +} +``` + +```cpp-exec:practice11_1.cpp +Employee Alice has joined the company. +Alice has been promoted! +Employee Alice is leaving. +``` + +### 問題2: `shared_ptr` と所有権の共有 + +`Project` という名前のクラスを作成してください。コンストラクタでプロジェクト名を受け取り、デストラクタで「Project (プロジェクト名) is finished.」と表示します。 + +`main` 関数で、`"Project Phoenix"` という名前の `Project` オブジェクトを `std::make_shared` で作成してください。 +次に、`std::vector>` を作成し、作成した `shared_ptr` を2回 `push_back` してください。 +その後、`shared_ptr` の参照カウント (`use_count()`) を表示してください。 +最後に、`vector` を `clear()` して、再度参照カウントを表示してください。 +プログラムの実行が終了するときに `Project` のデストラクタが呼ばれることを確認してください。 + +```cpp:practice11_2.cpp +#include +#include +#include +#include + +// ここにProjectクラスを定義 + + +int main() { + + +} +``` + +```cpp-exec:practice11_2.cpp +Project Project Phoenix is started. +Initial use count: 1 +Use count after pushing to vector: 3 +Use count after clearing vector: 1 +Project Project Phoenix is finished. +``` diff --git a/public/docs/cpp-12.md b/public/docs/cpp-12.md new file mode 100644 index 0000000..ff36b7e --- /dev/null +++ b/public/docs/cpp-12.md @@ -0,0 +1,293 @@ +# 第12章: プロジェクトの分割とビルド + +これまでの章では、すべてのコードを1つの `.cpp` ファイルに記述してきました。しかし、プログラムが大規模で複雑になるにつれて、このアプローチは現実的ではなくなります。コードの可読性が低下し、少しの変更でもプログラム全体の再コンパイルが必要になり、開発効率が大きく損なわれるからです。 + +この章では、プログラムを複数のファイルに分割し、それらを効率的に管理・ビルドする方法を学びます。これは、小さなプログラムから一歩進み、本格的なソフトウェア開発を行うための重要なステップです。 + +## ヘッダファイルとソースファイル + +C++では、プログラムを**ヘッダファイル**と**ソースファイル**という2種類のファイルに分割するのが一般的です。 + + * **ヘッダファイル (`.h` または `.hpp`)**: 「宣言」を置く場所です。クラスの定義、関数のプロトタイプ宣言、定数、テンプレートなどを記述します。他のファイルに対して「何ができるか(インターフェース)」を公開する役割を持ちます。 + * **ソースファイル (`.cpp`)**: 「実装」を置く場所です。ヘッダファイルで宣言された関数の具体的な処理内容などを記述します。ヘッダファイルが公開したインターフェースを「どのように実現するか」を記述する役割を持ちます。 + +### なぜ分割するのか? 🤔 + +1. **関心の分離**: インターフェース(何ができるか)と実装(どうやるか)を分離することで、コードの見通しが良くなります。他の開発者はヘッダファイルを見るだけで、その機能の使い方がわかります。 +2. **コンパイル時間の短縮**: ソースファイルを変更した場合、再コンパイルはそのファイルだけで済みます。プロジェクト全体を再コンパイルする必要がないため、大規模なプロジェクトでは開発サイクルが劇的に速くなります。 +3. **再利用性の向上**: よく使う関数やクラスをまとめておけば、別のプロジェクトでそのファイルをインクルードするだけで簡単に再利用できます。 + +### 分割の例 + +簡単な足し算を行う関数を別のファイルに分割してみましょう。 + +まず、関数の「宣言」をヘッダファイルに記述します。 + +```cpp:math_utils.h +// 関数の宣言を記述するヘッダファイル + +// この関数が他のファイルから参照されることを示す +int add(int a, int b); +``` + +次に、この関数の「実装」をソースファイルに記述します。 + +```cpp:math_utils.cpp +// 関数の実装を記述するソースファイル + +#include "math_utils.h" // 対応するヘッダファイルをインクルード + +int add(int a, int b) { + return a + b; +} +``` + +最後に、`main`関数を含むメインのソースファイルから、この`add`関数を呼び出します。 + +```cpp:math_app.cpp +#include +#include "math_utils.h" // 自作したヘッダファイルをインクルード + +int main() { + int result = add(5, 3); + std::cout << "The result is: " << result << std::endl; + return 0; +} +``` + +```cpp-exec:math_app.cpp,math_utils.cpp +The result is: 8 +``` + +ここで注目すべき点は、`math_app.cpp`が`add`関数の具体的な実装を知らないことです。`math_utils.h`を通じて「`int`を2つ受け取って`int`を返す`add`という関数が存在する」ことだけを知り、それを利用しています。 + +## インクルードガード + +複数のファイルから同じヘッダファイルがインクルードされる状況はよくあります。例えば、`A.h`が`B.h`をインクルードし、ソースファイルが`A.h`と`B.h`の両方をインクルードするような場合です。 + +もしヘッダファイルに何の対策もしていないと、同じ内容(クラス定義や関数宣言)が複数回読み込まれ、「再定義」としてコンパイルエラーが発生してしまいます。 + +```cpp:A.h +#include "B.h" // B.hをインクルード + +// A.hの内容 +``` + +```cpp:B.h +class B { + // Bクラスの内容 +}; +``` + +```cpp:bad_include_app.cpp +#include "A.h" +#include "B.h" // B.hが二重にインクルードされる + +int main() { + [[maybe_unused]] B b; // Bクラスを使う + + return 0; +} +``` + +```cpp-exec:bad_include_app.cpp +In file included from bad_include_app.cpp:2: +B.h:1:7: error: redefinition of 'class B' + 1 | class B { + | ^ +In file included from A.h:1, + from bad_include_app.cpp:1: +B.h:1:7: note: previous definition of 'class B' + 1 | class B { + | ^ +``` + +この問題を解決するのが**インクルードガード**です。インクルードガードは、ヘッダファイルの内容が1つの翻訳単位(ソースファイル)内で一度しか読み込まれないようにするための仕組みです。 + +### 伝統的なインクルードガード + +プリプロセッサディレクティブである `#ifndef`, `#define`, `#endif` を使います。 + +```cpp +#ifndef MATH_UTILS_H // もし MATH_UTILS_H が未定義なら +#define MATH_UTILS_H // MATH_UTILS_H を定義する + +// --- ヘッダファイルの中身 --- +int add(int a, int b); +// ------------------------- + +#endif // MATH_UTILS_H +``` + + * **最初のインクルード**: `MATH_UTILS_H` は未定義なので、`#define` が実行され、中身が読み込まれます。 + * **2回目以降のインクルード**: `MATH_UTILS_H` は既に定義されているため、`#ifndef` から `#endif` までのすべてが無視されます。 + +マクロ名 (`MATH_UTILS_H`) は、ファイル名に基づいて一意になるように命名するのが慣習です。 + +### \#pragma once + +より現代的で簡潔な方法として `#pragma once` があります。多くのモダンなコンパイラがサポートしています。 + +```cpp +#pragma once + +#include + +std::string to_upper(const std::string& str); +``` + +この一行をヘッダファイルの先頭に書くだけで、コンパイラがそのファイルが一度しかインクルードされないように処理してくれます。特別な理由がない限り、現在では `#pragma once` を使うのが主流です。 + +## プロジェクトのビルド + +複数のソースファイル(`.cpp`)は、それぞれがコンパイルされて**オブジェクトファイル**(`.o` や `.obj`)になります。その後、**リンカ**がこれらのオブジェクトファイルと必要なライブラリを結合して、最終的な実行可能ファイルを生成します。 + +この一連の作業を**ビルド**と呼びます。ファイルが増えてくると、これを手動で行うのは非常に面倒です。そこで、ビルド作業を自動化する**ビルドシステム**が使われます。 + +### 手動でのビルド (g++) + +先ほどの`math_app.cpp`と`math_utils.cpp`を例に、g++コンパイラで手動ビルドする手順を見てみましょう。 + +```bash +# 1. 各ソースファイルをコンパイルしてオブジェクトファイルを生成する (-c オプション) +g++ -c math_app.cpp -o main.o +g++ -c math_utils.cpp -o math_utils.o + +# 2. オブジェクトファイルをリンクして実行可能ファイルを生成する +g++ main.o math_utils.o -o my_app + +# 3. 実行する +./my_app +``` + +または、以下のように1回のg++コマンドで複数ソースファイルのコンパイルとリンクを同時に行うこともできます。 + +```bash +g++ math_app.cpp math_utils.cpp -o my_app +./my_app +``` + +### Makefileによる自動化 + +`make`は、ファイルの依存関係と更新ルールを記述した`Makefile`というファイルに従って、ビルドプロセスを自動化するツールです。 + +以下は、非常にシンプルな`Makefile`の例です。 + +```makefile +# コンパイラを指定 +CXX = g++ +# コンパイルオプションを指定 +CXXFLAGS = -std=c++17 -Wall + +# 最終的なターゲット(実行可能ファイル名) +TARGET = my_app + +# ソースファイルとオブジェクトファイル +SRCS = math_app.cpp math_utils.cpp +OBJS = $(SRCS:.cpp=.o) + +# デフォルトのターゲット (makeコマンド実行時に最初に実行される) +all: $(TARGET) + +# 実行可能ファイルの生成ルール +$(TARGET): $(OBJS) + $(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS) + +# オブジェクトファイルの生成ルール (%.o: %.cpp) +# .cppファイルから.oファイルを作るための汎用ルール +%.o: %.cpp + $(CXX) $(CXXFLAGS) -c $< -o $@ + +# 中間ファイルなどを削除するルール +clean: + rm -f $(OBJS) $(TARGET) +``` + +この`Makefile`があるディレクトリで、ターミナルから`make`と入力するだけで、必要なコンパイルとリンクが自動的に実行されます。`math_app.cpp`だけを変更した場合、`make`は`main.o`だけを再生成し、再リンクするため、ビルド時間が短縮されます。 + +### CMakeによるモダンなビルド管理 + +`Makefile`は強力ですが、OSやコンパイラに依存する部分があり、複雑なプロジェクトでは管理が難しくなります。 + +**CMake**は、`Makefile`やVisual Studioのプロジェクトファイルなどを自動的に生成してくれる、クロスプラットフォーム対応のビルドシステムジェネレータです。`CMakeLists.txt`という設定ファイルに、より抽象的なビルドのルールを記述します。 + +```cmake +# CMakeの最低要求バージョン +cmake_minimum_required(VERSION 3.10) + +# プロジェクト名を設定 +project(MyAwesomeApp) + +# C++の標準バージョンを設定 +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# 実行可能ファイルを追加 +# add_executable(実行ファイル名 ソースファイル1 ソースファイル2 ...) +add_executable(my_app math_app.cpp math_utils.cpp) +``` + +この`CMakeLists.txt`を使ってビルドする一般的な手順は以下の通りです。 + +```bash +# 1. ビルド用の中間ファイルを置くディレクトリを作成し、移動する +mkdir build +cd build + +# 2. CMakeを実行して、ビルドシステム(この場合はMakefile)を生成する +cmake .. + +# 3. make (または cmake --build .) を実行してビルドする +make + +# 4. 実行する +./my_app +``` + +CMakeは、ライブラリの検索、依存関係の管理、テストの実行など、大規模プロジェクトに必要な多くの機能を備えており、現在のC++開発における標準的なツールとなっています。 + +## この章のまとめ + + * **プロジェクトの分割**: プログラムは「宣言」を記述する**ヘッダファイル** (`.h`) と、「実装」を記述する**ソースファイル** (`.cpp`) に分割することで、保守性や再利用性が向上します。 + * **インクルードガード**: ヘッダファイルの多重インクルードによる再定義エラーを防ぐために、`#pragma once` や `#ifndef`/`#define`/`#endif` を使用します。 + * **ビルドシステム**: 複数のファイルをコンパイル・リンクするプロセスを自動化するために、`make` や `CMake` といったツールが使われます。特に **CMake** はクロスプラットフォーム開発におけるデファクトスタンダードです。 + +### 練習問題1: 電卓クラスの分割 + +`Calculator` というクラスを作成してください。このクラスは、加算、減算、乗算、除算のメンバ関数を持ちます。 + +* `Calculator.h`: `Calculator`クラスの定義を記述します。 +* `Calculator.cpp`: 各メンバ関数の実装を記述します。 +* `practice12_1.cpp`: `Calculator`クラスのインスタンスを作成し、いくつかの計算を行って結果を表示します。 + +これらのファイルをg++で手動ビルドして、プログラムを実行してください。 + +```cpp:Calculator.h + +``` + +```cpp:Calculator.cpp + +``` + +```cpp:practice12_1.cpp +#include +#include "Calculator.h" + +int main() { + Calculator calc; + + std::cout << "3 + 5 = " << calc.add(3, 5) << std::endl; + std::cout << "10 - 2 = " << calc.subtract(10, 2) << std::endl; + std::cout << "4 * 7 = " << calc.multiply(4, 7) << std::endl; + std::cout << "20 / 4 = " << calc.divide(20, 4) << std::endl; + return 0; +} +``` + +```cpp-exec:practice12_1.cpp,Calculator.cpp +3 + 5 = 8 +10 - 2 = 8 +4 * 7 = 28 +20 / 4 = 5 +``` diff --git a/public/docs/cpp-2.md b/public/docs/cpp-2.md index 74001ed..9bb94a1 100644 --- a/public/docs/cpp-2.md +++ b/public/docs/cpp-2.md @@ -188,55 +188,97 @@ Hello, C++! Length: 11 ``` -## 変数とメモリ +## 複数の値をまとめて扱う:配列 -さて、C++を深く理解する上で避けて通れないのが**メモリ**の概念です。変数を宣言すると、コンピュータのメモリ上にその型に応じたサイズの領域が確保されます。 +同じ型のデータを複数個まとめて扱いたい場合、配列を使います。C++にはいくつかの配列の形がありますが、ここでは代表的な3つを軽く紹介します。 -例えば、`int x = 10;` と書くと、 +### 1\. Cスタイルの配列 -1. コンパイラは`int`型に必要なメモリサイズ(例: 4バイト)を判断します。 -2. プログラム実行時、メモリ上のどこかに4バイトの領域が確保されます。 -3. その領域に`x`という名前が割り当てられ、値として`10`が格納(バイナリ形式で書き込み)されます。 +C言語から引き継がれた、最も基本的な配列です。 -この「メモリ上のどこか」を示すのが**メモリアドレス**(単にアドレスとも)です。アドレスは、メモリ上の各バイトに割り振られた通し番号のようなもので、通常は16進数で表現されます。 +```cpp:c_style_array.cpp +#include + +int main() { + // int型の要素を5つ持つ配列を宣言し、初期化 + int scores[5] = {88, 92, 75, 100, 69}; + + // 要素へのアクセス (インデックスは0から始まる) + scores[2] = 80; // 3番目の要素を80に変更 + std::cout << "3番目のスコア: " << scores[2] << std::endl; +} +``` + +```cpp-exec:c_style_array.cpp +3番目のスコア: 80 +``` + +**特徴**: + + * 構文がシンプル。 + * 配列のサイズ自体を保持していないため、プログラマがサイズを管理する必要がある。 + +### 2\. `std::array` (固定長配列) -変数名の前に`&`(アドレス演算子)を付けることで、その変数が格納されているメモリアドレスを知ることができます。 +Cスタイル配列を安全に使いやすくしたものです。サイズがコンパイル時に決まっている場合に使います。``ヘッダが必要です。 -```cpp:memory_address.cpp +```cpp:std_array.cpp #include -#include +#include int main() { - int age = 30; - double pi = 3.14; - std::string name = "Alice"; + std::array scores = {88, 92, 75, 100, 69}; + + // 安全なアクセス方法 .at() + scores.at(2) = 80; + std::cout << "3番目のスコア: " << scores.at(2) << std::endl; - std::cout << "変数 'age' の値: " << age << std::endl; - std::cout << "変数 'age' のメモリアドレス: " << &age << std::endl; - std::cout << std::endl; + // サイズを取得できる .size() + std::cout << "配列のサイズ: " << scores.size() << std::endl; +} +``` +```cpp-exec:std_array.cpp +3番目のスコア: 80 +配列のサイズ: 5 +``` - std::cout << "変数 'pi' の値: " << pi << std::endl; - std::cout << "変数 'pi' のメモリアドレス: " << &pi << std::endl; - std::cout << std::endl; +**特徴**: - std::cout << "変数 'name' の値: " << name << std::endl; - std::cout << "変数 'name' のメモリアドレス: " << &name << std::endl; + * Cスタイル配列同様、サイズは固定。 + * `.size()` で要素数を取得できる。 + * `.at(i)` を使うと、範囲外のインデックスにアクセスしようとした際にエラーを検知してくれるため安全性が高い。 - return 0; +### 3\. `std::vector` (可変長配列) + +プログラムの実行中にサイズを自由に変更できる、非常に柔軟で強力な配列です。迷ったらまず `std::vector` を検討するのが良いでしょう。``ヘッダが必要です。 + +```cpp:std_vector.cpp +#include +#include + +int main() { + // 最初は3つの要素を持つ + std::vector scores = {88, 92, 75}; + + // 末尾に新しい要素を追加 .push_back() + scores.push_back(100); + + std::cout << "現在のサイズ: " << scores.size() << std::endl; + std::cout << "最後のスコア: " << scores.at(3) << std::endl; } ``` -```cpp-exec:memory_address.cpp -変数 'age' の値: 30 -変数 'age' のメモリアドレス: 0x7ffee3b8c9ac +```cpp-exec:std_vector.cpp +現在のサイズ: 4 +最後のスコア: 100 +``` -変数 'pi' の値: 3.14 -変数 'pi' のメモリアドレス: 0x7ffee3b8c9a0 -変数 'name' の値: Alice -変数 'name' のメモリアドレス: 0x7ffee3b8c990 -``` +**特徴**: + + * サイズを**実行中に変更**できる(要素の追加や削除が可能)。 + * `std::array` と同様の便利な機能 (`.size()`, `.at()` など) を持つ。 + * C++で最もよく使われるコンテナ(データ構造)の一つ。 -このコードを実行すると、`0x7ffee...` のようなアドレスが表示されるはずです(値は実行のたびに変わります)。今は「変数はメモリ上の特定の場所に、特定のサイズで存在している」というイメージを持つことが重要です。この概念は、第4章で学ぶポインタを理解するための基礎となります。 ## この章のまとめ @@ -246,7 +288,7 @@ int main() { * 基本的な**四則演算**ができるが、**整数同士の除算**は結果が整数に切り捨てられる点に注意。 * **`const`** を使うことで、変数を不変にし、プログラムの安全性を高めることができる。 * **`auto`** を使うことで、コンパイラに型を推論させ、コードを簡潔に書くことができる。 - * モダンC++では、文字列は\*\*`std::string`\*\* クラスを使って安全かつ便利に扱う。 + * モダンC++では、文字列は**`std::string`**、配列は**`std::vector`** や **`std::array`** クラスを使って安全かつ便利に扱う。 * 宣言された変数は、メモリ上の特定の**アドレス**に、その型に応じた**サイズ**の領域を確保して格納される。 ### 練習問題1 diff --git a/public/docs/cpp-4.md b/public/docs/cpp-4.md new file mode 100644 index 0000000..5d14126 --- /dev/null +++ b/public/docs/cpp-4.md @@ -0,0 +1,280 @@ +# 第4章: ポインタと動的メモリ + +C++の最も強力かつ、多くの初学者がつまずくトピックの一つである「ポインタ」を学びます。ポインタを理解することで、C++がどのようにメモリを扱っているのか、その裏側を垣間見ることができます。他の言語では隠蔽されているメモリへの直接的なアクセスは、パフォーマンスが重要な場面で絶大な効果を発揮します。さあ、メモリを直接操作する感覚を掴んでいきましょう。 + +## 変数とメモリ + +C++を深く理解する上で避けて通れないのが**メモリ**の概念です。変数を宣言すると、コンピュータのメモリ上にその型に応じたサイズの領域が確保されます。 + +例えば、`int x = 10;` と書くと、 + +1. コンパイラは`int`型に必要なメモリサイズ(例: 4バイト)を判断します。 +2. プログラム実行時、メモリ上のどこかに4バイトの領域が確保されます。 +3. その領域に`x`という名前が割り当てられ、値として`10`が格納(バイナリ形式で書き込み)されます。 + +この「メモリ上のどこか」を示すのが**メモリアドレス**(単にアドレスとも)です。ちょうど、現実世界の家の住所のようなものだと考えてください。アドレスは、メモリ上の各バイトに割り振られた通し番号のようなもので、通常は16進数で表現されます。 + +変数名の前に`&`(アドレス演算子)を付けることで、その変数が格納されているメモリアドレスを知ることができます。 + +```cpp:memory_address.cpp +#include +#include + +int main() { + int age = 30; + double pi = 3.14; + std::string name = "Alice"; + + std::cout << "変数 'age' の値: " << age << std::endl; + std::cout << "変数 'age' のメモリアドレス: " << &age << std::endl; + std::cout << std::endl; + + std::cout << "変数 'pi' の値: " << pi << std::endl; + std::cout << "変数 'pi' のメモリアドレス: " << &pi << std::endl; + std::cout << std::endl; + + std::cout << "変数 'name' の値: " << name << std::endl; + std::cout << "変数 'name' のメモリアドレス: " << &name << std::endl; + + return 0; +} +``` +```cpp-exec:memory_address.cpp +変数 'age' の値: 30 +変数 'age' のメモリアドレス: 0x7ffee3b8c9ac + +変数 'pi' の値: 3.14 +変数 'pi' のメモリアドレス: 0x7ffee3b8c9a0 + +変数 'name' の値: Alice +変数 'name' のメモリアドレス: 0x7ffee3b8c990 +``` + +## ポインタの正体: アドレスを格納する変数 + +**ポインタ**とは、この**メモリアドレスを格納するための専用の変数**です。 + +変数の型に応じて、対応するポインタの型が存在します。例えば、`int`型の変数のアドレスを格納するなら `int*` 型、`double`型の変数のアドレスを格納するなら `double*` 型のポインタを使います。アスタリスク `*` がポインタ型であることを示します。 + +```cpp:pointer_declaration.cpp +#include + +int main() { + // int型の変数のアドレスを格納するためのポインタ変数 + int* p_number; + + // double型の変数のアドレスを格納するためのポインタ変数 + double* p_value; + + // どの変数も指していないことを示す特別な値 nullptr + // ポインタを初期化する際は nullptr を使うのが安全です + int* p_safe = nullptr; + + if (p_safe == nullptr) { + std::cout << "p_safe は何も指していません。" << std::endl; + } + + return 0; +} +``` + +```cpp-exec:pointer_declaration.cpp +p_safe は何も指していません。 +``` + +ポインタ変数の宣言時に `*` を型の横に付けるか、変数名の横に付けるかは好みが分かれますが、意味は同じです (`int* p;` と `int *p;` は等価)。このチュートリアルでは `int* p;` のように型の側に付けます。 + +## ポインタの操作: 間接参照 (`*`) とアドレス取得 (`&`) + +ポインタを操作するための2つの重要な演算子を学びましょう。 + + * **アドレス取得演算子 (`&`)**: 変数の前に付けると、その変数のメモリアドレスを取得できます。 + * **間接参照演算子 (`*`)**: ポインタ変数の前に付けると、そのポインタが指し示しているアドレスに格納されている値を取得(または変更)できます。「参照先の値」を取り出すイメージです。 + +言葉だけだと少し分かりにくいので、コードで見てみましょう。 + +```cpp:pointer_operations.cpp +#include + +int main() { + int number = 42; + // int型のポインタ変数 p_number を宣言 + int* p_number; + + // アドレス取得演算子(&)を使って、変数numberのアドレスをp_numberに代入 + p_number = &number; + + std::cout << "変数 number の値: " << number << std::endl; + std::cout << "変数 number のアドレス: " << &number << std::endl; + std::cout << "ポインタ p_number の値 (格納しているアドレス): " << p_number << std::endl; + + // 間接参照演算子(*)を使って、p_numberが指す先の値を取得 + std::cout << "ポインタ p_number が指す先の値: " << *p_number << std::endl; + + std::cout << std::endl << "--- ポインタ経由で値を変更 ---" << std::endl; + + // ポインタ経由で、変数numberの値を100に変更 + *p_number = 100; + + std::cout << "変更後の変数 number の値: " << number << std::endl; + std::cout << "ポインタ p_number が指す先の値: " << *p_number << std::endl; + + return 0; +} +``` + +```cpp-exec:pointer_operations.cpp +変数 number の値: 42 +変数 number のアドレス: 0x7ffc... +ポインタ p_number の値 (格納しているアドレス): 0x7ffc... +ポインタ p_number が指す先の値: 42 + +--- ポインタ経由で値を変更 --- +変更後の変数 number の値: 100 +ポインタ p_number が指す先の値: 100 +``` + +実行結果のアドレス (`0x7ffc...`の部分) は、実行するたびに変わる可能性があります。 + +この例から、`p_number = &number;` によって `p_number` が `number` を指すようになり、`*p_number` を使うことで `number` の中身を読み書きできることが分かります。`*p_number` は `number` とほぼ同じように扱えるわけです。 + +## 動的なメモリ確保: `new` と `delete` + +これまでの変数は、プログラムの実行前にコンパイラが必要なメモリ領域(**スタック領域**)を確保していました。しかし、プログラム実行中に、必要な分だけメモリを確保したい場合があります。例えば、ユーザーの入力に応じて、可変長のデータを保存するようなケースです。 + +このように、プログラムの実行中に動的にメモリを確保する仕組みが用意されており、確保される領域は**ヒープ領域**(またはフリーストア)と呼ばれます。 + + * **`new`**: ヒープ領域から指定した型のメモリを確保し、その領域へのポインタを返します。 + * **`delete`**: `new` で確保したメモリを解放します。 + +`new` で確保したメモリは、自動的には解放されません。自分で責任を持って `delete` を使って解放する必要があります。これを怠ると、**メモリリーク**(確保したメモリが解放されずに残り続け、利用可能なメモリがどんどん減っていくバグ)の原因となります。 + +```cpp:dynamic_memory.cpp +#include + +int main() { + // new を使ってヒープ領域にint一つ分のメモリを確保 + // 確保された領域へのアドレスが p_int に格納される + int* p_int = new int; + + // ポインタ経由で確保した領域に値を書き込む + *p_int = 2025; + + std::cout << "確保したメモリが指す先の値: " << *p_int << std::endl; + std::cout << "格納されているアドレス: " << p_int << std::endl; + + // 使い終わったメモリは必ず delete で解放する + delete p_int; + + // 解放後のポインタはどこを指しているか分からない危険な状態(ダングリングポインタ) + // なので、nullptr を代入して安全な状態にしておくのが良い習慣 + p_int = nullptr; + + return 0; +} +``` + +```cpp-exec:dynamic_memory.cpp +確保したメモリが指す先の値: 2025 +格納されているアドレス: 0x55a1... +``` + +`new` と `delete` は必ずペアで使います。図書館で本を借りたら(`new`)、必ず返却カウンターに返す(`delete`)のと同じです。返さなければ、他の人がその本を借りられなくなってしまいますよね。 + +## ポインタと配列: 切っても切れない関係性 + +C++では、Cスタイルの配列とポインタは非常に密接な関係にあります。実は、**配列名はその配列の先頭要素を指すポインタ**として扱うことができます。 + +```cpp:pointer_and_array.cpp +#include + +int main() { + int numbers[] = {10, 20, 30, 40, 50}; + + // 配列名は先頭要素へのポインタとして扱える + int* p_numbers = numbers; + + std::cout << "numbers[0] の値: " << numbers[0] << std::endl; + std::cout << "p_numbers が指す先の値: " << *p_numbers << std::endl; + + // ポインタの指すアドレスを1つ進める(次の要素を指す) + p_numbers++; + + std::cout << "ポインタをインクリメントした後、p_numbers が指す先の値: " << *p_numbers << std::endl; + std::cout << "これは numbers[1] と同じ: " << numbers[1] << std::endl; + + // ポインタとオフセットで配列要素にアクセス + // *(numbers + 2) は numbers[2] と等価 + std::cout << "*(numbers + 2) の値: " << *(numbers + 2) << std::endl; + + return 0; +} +``` + +```cpp-exec:pointer_and_array.cpp +numbers[0] の値: 10 +p_numbers が指す先の値: 10 +ポインタをインクリメントした後、p_numbers が指す先の値: 20 +これは numbers[1] と同じ: 20 +*(numbers + 2) の値: 30 +``` + +`p_numbers++` のようにポインタをインクリメントすると、ポインタは「次の要素」を指すようになります。`int`型ポインタなら4バイト(環境による)、`double`型ポインタなら8バイトといったように、指している型に応じて適切なバイト数だけアドレスが進みます。これを**ポインタ演算**と呼びます。 + +この仕組みにより、ポインタを使って配列の要素を順に辿っていくことができます。ただし、配列の範囲外を指すポインタ(例えば、5つの要素を持つ配列の6番目を指すなど)を間接参照してしまうと、未定義の動作を引き起こし、プログラムがクラッシュする原因になるため、細心の注意が必要です。 + +## この章のまとめ + + * **ポインタ**は、変数の**メモリアドレス**を格納する特殊な変数です。 + * `&` 演算子で変数のアドレスを取得し、ポインタに代入します。 + * `*` 演算子でポインタが指す先の値にアクセス(読み書き)します。 + * `new` を使うと、プログラム実行中に**ヒープ領域**から動的にメモリを確保できます。 + * `new` で確保したメモリは、使い終わったら必ず `delete` で解放しなければならず、怠ると**メモリリーク**になります。 + * 配列名は、その配列の先頭要素を指すポインタとして扱うことができます。 + +ポインタはC++の強力な機能ですが、使い方を誤ると厄介なバグの原因にもなります。しかし、この仕組みを理解することは、C++をより深く知る上で不可欠です。後の章で学ぶスマートポインタなど、現代のC++ではポインタをより安全に扱うための機能も提供されています。 + +### 練習問題1 + +`double` 型の変数 `pi` を `3.14159` で初期化してください。次に、`double*` 型のポインタ `p_pi` を宣言し、`pi` のアドレスを代入してください。最後に、ポインタ `p_pi` を使って `pi` の値を `2.71828` に変更し、変数 `pi` の値が変更されたことを確認するプログラムを書いてください。 + +```cpp:practice4_1.cpp +#include + +int main() { + +} +``` + +```cpp-exec:practice4_1.cpp +3.14159 +2.71828 +``` + +### 練習問題2 + +2つの整数を足し算する関数 `add` を作成し、その結果をポインタを使って返すプログラムを作成してください。 + +1. `int* add(int a, int b)` というシグネチャの関数を定義します。 +2. 関数内で `new` を使って `int` 型のメモリを動的に確保します。 +3. その確保したメモリに `a + b` の計算結果を格納します。 +4. 確保したメモリへのポインタを返します。 +5. `main` 関数でこの `add` 関数を呼び出し、結果を受け取るポインタ変数を宣言します。 +6. ポインタを間接参照して計算結果を出力します。 +7. 最後に、`main` 関数内で `delete` を使って、`add` 関数内で確保されたメモリを解放するのを忘れないでください。 + +```cpp:practice4_2.cpp +#include + +int* add(int a, int b) { + +} + +int main() { + +} +``` + +```cpp-exec:practice4_2.cpp +7 +``` diff --git a/public/docs/cpp-5.md b/public/docs/cpp-5.md new file mode 100644 index 0000000..d707c12 --- /dev/null +++ b/public/docs/cpp-5.md @@ -0,0 +1,340 @@ +# 第5章: オブジェクト指向の入口:クラスの基礎 + +これまでの章では、C++の基本的な文法やメモリの扱い方について学んできました。この章からは、C++の最も強力な機能の一つである**オブジェクト指向プログラミング (Object-Oriented Programming, OOP)** の世界に足を踏み入れます。OOPの考え方を身につけることで、より大規模で複雑なプログラムを、現実世界の「モノ」の概念に近い形で、直感的に設計・実装できるようになります。その第一歩として、OOPの中核をなす**クラス**の基礎を学びましょう。 + +## クラスとは?: データ(メンバ変数)と処理(メンバ関数)のカプセル化 + +他のプログラミング言語でオブジェクト指向に触れたことがあるなら、「クラスはオブジェクトの設計図」という説明を聞いたことがあるかもしれません。C++でもその考え方は同じです。クラスは、ある「モノ」が持つべき**データ(属性)**と、そのデータに対する**処理(操作)**を一つにまとめたものです。 + + - **データ(属性)**: クラス内に定義された変数のことで、**メンバ変数 (member variables)** または**データメンバ**と呼びます。 + - **処理(操作)**: クラス内に定義された関数のことで、**メンバ関数 (member functions)** または**メソッド**と呼びます。 + +このように、関連するデータと処理を一つのクラスにまとめることを、OOPの重要な概念の一つである**カプセル化 (encapsulation)** と呼びます。💊 + +例として、「人」を表す`Person`クラスを考えてみましょう。「人」は「名前」や「年齢」といったデータ(メンバ変数)を持ち、「自己紹介する」といった処理(メンバ関数)を行うことができます。 + +```cpp +class Person { +public: + // メンバ変数 + std::string name; + int age; + + // メンバ関数 + void introduce() { + std::cout << "My name is " << name << ", and I am " << age << " years old." << std::endl; + } +}; +``` + +`class Person { ... };` という構文でクラスを定義します。クラス定義の最後にはセミコロン`;`が必要なので忘れないようにしましょう。現時点では、`public:`というキーワードは「これらのメンバは外部からアクセスできます」という意味だと考えておいてください。詳細は後ほど説明します。 + +## インスタンスの生成: クラスからオブジェクトを作ってみる + +クラスはあくまで「設計図」です。実際にプログラムで利用するためには、この設計図をもとに実体を作る必要があります。クラスから作られた実体のことを**オブジェクト (object)** または**インスタンス (instance)** と呼び、オブジェクトを作ることを**インスタンス化 (instantiation)** と言います。 + +インスタンス化の構文は、変数の宣言とよく似ています。 + +```cpp:instantiation.cpp +#include +#include + +// Personクラスの定義 +class Person { +public: + std::string name; + int age; + + void introduce() { + std::cout << "My name is " << name << ", and I am " << age << " years old." << std::endl; + } +}; + +int main() { + // Personクラスのインスタンスを生成 + Person taro; + + // メンバ変数に値を代入 (ドット演算子 . を使用) + taro.name = "Taro"; + taro.age = 30; + + // メンバ関数を呼び出す + taro.introduce(); // "My name is Taro, and I am 30 years old." と出力される + + // 別のインスタンスを生成 + Person hanako; + hanako.name = "Hanako"; + hanako.age = 25; + hanako.introduce(); // "My name is Hanako, and I am 25 years old." と出力される + + return 0; +} +``` + +```cpp-exec:instantiation.cpp +My name is Taro, and I am 30 years old. +My name is Hanako, and I am 25 years old. +``` + +このように、`クラス名 インスタンス名;` という形でインスタンスを生成できます。インスタンスのメンバ変数やメンバ関数にアクセスするには、`インスタンス名.メンバ名` のように**ドット演算子 (`.`)** を使います。`taro`と`hanako`は同じ`Person`クラスから作られたインスタンスですが、それぞれが独立したデータを持っていることがわかります。 + +## アクセス制御: public と private による情報の隠蔽 + +先ほどの`Person`クラスの例では、`main`関数から`taro.age = 30;`のようにメンバ変数に直接アクセスできました。これは手軽ですが、問題を引き起こす可能性があります。例えば、年齢にマイナスの値や非現実的な値を設定できてしまうかもしれません。 + +```cpp +Person jiro; +jiro.name = "Jiro"; +jiro.age = -5; // 本来ありえない値が設定できてしまう! +jiro.introduce(); +``` + +このような意図しない操作を防ぐために、C++には**アクセス制御**の仕組みがあります。クラスのメンバは、外部からのアクセスの可否を指定できます。 + + - **`public`**: クラスの外部(`main`関数など)から自由にアクセスできます。 + - **`private`**: そのクラスのメンバ関数からしかアクセスできません。外部からはアクセス不可です。 + +アクセス制御の基本は、**メンバ変数は`private`にし、メンバ関数は`public`にする**ことです。これにより、データの不正な書き換えを防ぎ、クラスの内部実装を外部から隠蔽します。これを**情報の隠蔽 (information hiding)** と呼び、カプセル化の重要な目的の一つです。 + +`private`なメンバ変数に安全にアクセスするために、`public`なメンバ関数(**ゲッター**や**セッター**と呼ばれる)を用意するのが一般的です。 + +```cpp:access_control.cpp +#include +#include + +class Person { +private: + // メンバ変数は外部から隠蔽する + std::string name; + int age; + +public: + // セッター: メンバ変数に値を設定する + void setName(const std::string& newName) { + name = newName; + } + + void setAge(int newAge) { + if (newAge >= 0 && newAge < 150) { // 不正な値をチェック + age = newAge; + } else { + std::cout << "Error: Invalid age value." << std::endl; + } + } + + // ゲッター: メンバ変数の値を取得する + std::string getName() const { + return name; + } + + int getAge() const { + return age; + } + + // このメンバ関数はクラス内部にあるので、privateメンバにアクセスできる + void introduce() { + std::cout << "My name is " << name << ", and I am " << age << " years old." << std::endl; + } +}; + +int main() { + Person saburo; + + // saburo.name = "Saburo"; // エラー! privateメンバには直接アクセスできない + // saburo.age = -10; // エラー! + + // publicなメンバ関数を経由して安全に値を設定 + saburo.setName("Saburo"); + saburo.setAge(28); + + saburo.introduce(); + + saburo.setAge(-10); // エラーメッセージが出力される + + // publicなメンバ関数経由で値を取得 + std::cout << "Name: " << saburo.getName() << std::endl; + + return 0; +} +``` + +```cpp-exec:access_control.cpp +My name is Saburo, and I am 28 years old. +Error: Invalid age value. +Name: Saburo +``` + +`setAge`関数内で値の妥当性チェックを行っている点に注目してください。このように、クラスの利用者は内部の実装を気にすることなく、提供された`public`なインターフェース(メンバ関数)を通じて安全にオブジェクトを操作できます。 + +> `const`キーワード: `getName() const` のようにメンバ関数の後ろに`const`を付けると、その関数がメンバ変数を変更しないことをコンパイラに約束します。このような関数を**constメンバ関数**と呼びます。 + +## コンストラクタとデストラクタ: オブジェクトが生まれてから消えるまで + +オブジェクトは生成され、利用され、やがて破棄されます。このライフサイクルに合わせて特別な処理を自動的に実行するための仕組みが**コンストラクタ**と**デストラクタ**です。 + +### コンストラクタ (Constructor) + +**コンストラクタ**は、インスタンスが生成されるときに**自動的に呼び出される**特別なメンバ関数です。主な役割は、メンバ変数の初期化です。 + +コンストラクタには以下の特徴があります。 + + - 関数名がクラス名と全く同じ。 + - 戻り値の型を指定しない(`void`も付けない)。 + - 引数を取ることができ、複数定義できる(オーバーロード)。 + +```cpp:constructor.cpp +class Person { +private: + std::string name; + int age; + +public: + // 引数付きコンストラクタ + Person(const std::string& initName, int initAge) { + std::cout << "Constructor called for " << initName << std::endl; + name = initName; + age = initAge; + } + // ... +}; + +int main() { + // インスタンス生成時にコンストラクタが呼ばれ、引数が渡される + Person yuko("Yuko", 22); // この時点でコンストラクタが実行される + yuko.introduce(); +} +``` + +```cpp-exec:constructor.cpp +Constructor called for Yuko +My name is Yuko, and I am 22 years old. +``` + +このように、インスタンス生成時に`()`で初期値を渡すことで、オブジェクトを生成と同時に有効な状態にできます。`set`関数を別途呼び出す手間が省け、初期化忘れを防ぐことができます。 + +### デストラクタ (Destructor) + +**デストラクタ**は、インスタンスが破棄されるとき(例えば、変数のスコープを抜けるとき)に**自動的に呼び出される**特別なメンバ関数です。主な役割は、オブジェクトが使用していたリソース(メモリやファイルなど)の後片付けです。 + +デストラクタには以下の特徴があります。 + + - 関数名が `~` + クラス名。 + - 戻り値も引数も取らない。 + - 1つのクラスに1つしか定義できない。 + +```cpp:constructor_destructor.cpp +#include +#include + +class Person { +private: + std::string name; + int age; + +public: + // コンストラクタ + Person(const std::string& initName, int initAge) { + std::cout << "Constructor called for " << initName << "." << std::endl; + name = initName; + age = initAge; + } + + // デストラクタ + ~Person() { + std::cout << "Destructor called for " << name << "." << std::endl; + } + + void introduce() { + std::cout << "My name is " << name << ", and I am " << age << " years old." << std::endl; + } +}; + +void create_person_scope() { + std::cout << "--- Entering scope ---" << std::endl; + Person kenji("Kenji", 45); // kenjiはこのスコープ内でのみ生存 + kenji.introduce(); + std::cout << "--- Exiting scope ---" << std::endl; +} // ここでkenjiのスコープが終わり、デストラクタが呼ばれる + +int main() { + create_person_scope(); + + std::cout << "--- Back in main ---" << std::endl; + + return 0; +} +``` + +```cpp-exec:constructor_destructor.cpp +--- Entering scope --- +Constructor called for Kenji. +My name is Kenji, and I am 45 years old. +--- Exiting scope --- +Destructor called for Kenji. +--- Back in main --- +``` + +実行結果を見ると、`kenji`オブジェクトが生成されたときにコンストラクタが、`create_person_scope`関数のスコープを抜けるときにデストラクタが自動的に呼び出されていることがわかります。動的に確保したメモリの解放など、クリーンアップ処理はデストラクタに書くのが定石です。この考え方は、今後の章で学ぶRAII(Resource Acquisition Is Initialization)という重要な概念に繋がります。 + +## この章のまとめ + +この章では、C++におけるオブジェクト指向プログラミングの第一歩として、クラスの基本的な概念を学びました。 + + - **クラス**は、データ(**メンバ変数**)と処理(**メンバ関数**)を一つにまとめた「設計図」です。 + - クラスから実体である**オブジェクト(インスタンス)**を生成して使用します。 + - **カプセル化**は、関連するデータと処理をまとめることです。 + - **アクセス制御**(`public`, `private`)により、外部からアクセスされたくないメンバを保護します(**情報の隠蔽**)。 + - **コンストラクタ**は、オブジェクト生成時に自動で呼ばれ、初期化を行います。 + - **デストラクタ**は、オブジェクト破棄時に自動で呼ばれ、後片付けを行います。 + +クラスを使いこなすことで、プログラムの部品化が進み、再利用性やメンテナンス性が格段に向上します。次の章では、クラスのさらに進んだ機能について学んでいきましょう。 + +### 練習問題1: 長方形クラス + +幅(`width`)と高さ(`height`)をメンバ変数として持つ`Rectangle`クラスを作成してください。 + + - メンバ変数は`private`で定義してください。 + - コンストラクタで幅と高さを初期化できるようにしてください。 + - 面積を計算して返す`getArea()`メソッドと、周の長さを計算して返す`getPerimeter()`メソッドを`public`で実装してください。 + - `main`関数で`Rectangle`クラスのインスタンスをいくつか生成し、面積と周の長さを表示するプログラムを作成してください。 + +```cpp:practice5_1.cpp +#include +#include +// ここにRectangleクラスを定義してください + +int main() { + // ここでRectangleクラスのインスタンスを生成し、面積と周の長さを表示してください + + return 0; +} +``` + +```cpp-exec:practice5_1.cpp +``` + + +### 練習問題2: 書籍クラス + +タイトル(`title`)、著者(`author`)、ページ数(`pages`)をメンバ変数として持つ`Book`クラスを作成してください。 + + - メンバ変数は`private`で定義してください。 + - コンストラクタで、タイトル、著者、ページ数を初期化できるようにしてください。 + - 本の情報を整形してコンソールに出力する`printInfo()`メソッドを`public`で実装してください。(例: `Title: [タイトル], Author: [著者], Pages: [ページ数] pages`) + - `main`関数で`Book`クラスのインスタンスを生成し、その情報を表示してください。 + +```cpp:practice5_2.cpp +#include +#include +// ここにBookクラスを定義してください + +int main() { + // ここでBookクラスのインスタンスを生成し、情報を表示してください + + return 0; +} +``` + +```cpp-exec:practice5_2.cpp +Title: The Great Gatsby, Author: F. Scott Fitzgerald, Pages: 180 pages +``` diff --git a/public/docs/cpp-6.md b/public/docs/cpp-6.md new file mode 100644 index 0000000..afb6046 --- /dev/null +++ b/public/docs/cpp-6.md @@ -0,0 +1,503 @@ +# 第6章: クラスを使いこなす + +第5章では、C++のオブジェクト指向プログラミングの核となる`class`の基本的な使い方を学びました。しかし、クラスを真に強力なツールとして使いこなすには、もう少し知識が必要です。この章では、オブジェクトのコピー、演算子のオーバーロード、クラスで共有されるメンバなど、より実践的でパワフルな機能について掘り下げていきます。これらの概念をマスターすることで、あなたの書くクラスはより安全で、直感的で、再利用性の高いものになるでしょう。 + +## オブジェクトのコピー: コピーコンストラクタと代入演算子 + +オブジェクトをコピーしたい場面は頻繁にあります。例えば、関数の引数にオブジェクトを渡すとき(値渡し)や、既存のオブジェクトで新しいオブジェクトを初期化するときなどです。 + +```cpp +Vector2D v1(1.0, 2.0); +Vector2D v2 = v1; // ここでコピーが発生! +``` + +多くの場合、コンパイラが自動的に生成するコピー機能で十分です。しかし、クラスがポインタなどでリソース(メモリなど)を管理している場合、単純なコピーでは問題が発生します。 + +### 何もしないとどうなる? (浅いコピー) + +まず、コピーの機能を自分で作らなかった場合に何が起きるか見てみましょう。 +コンパイラは、メンバ変数を単純にコピーするだけの「浅いコピー」を行います。 + +ここでは、`int`へのポインタを一つだけ持つ`ResourceHolder`(リソース保持者)というクラスを考えます。 + +```cpp:shallow_copy.cpp +// 悪い例:浅いコピーの問題点 +#include + +class ResourceHolder { +private: + int* m_data; // 動的に確保したデータへのポインタ +public: + ResourceHolder(int value) { + m_data = new int(value); // メモリを確保 + std::cout << "Resource " << *m_data << " created. (at " << m_data << ")" << std::endl; + } + ~ResourceHolder() { + std::cout << "Resource " << *m_data << " destroyed. (at " << m_data << ")" << std::endl; + delete m_data; // メモリを解放 + } + // コピーコンストラクタや代入演算子を定義していない! +}; + +int main() { + ResourceHolder r1(10); + ResourceHolder r2 = r1; // 浅いコピーが発生! + // r1.m_data と r2.m_data は同じアドレスを指してしまう + + // main()終了時、r1とr2のデストラクタが呼ばれる + // 同じメモリを2回deleteしようとしてクラッシュ!💥 + return 0; +} +``` +```cpp-exec:shallow_copy.cpp +Resource 10 created. (at 0x139f065e0) +Resource 10 destroyed. (at 0x139f065e0) +Resource 107521 destroyed. (at 0x1a4012b0) +free(): double free detected in tcache 2 +``` + +この例では、`r2`が作られるときに`r1`のポインタ`m_data`の値(メモリアドレス)だけがコピーされます。その結果、2つのオブジェクトが1つのメモリ領域を指してしまいます。プログラム終了時にそれぞれのデストラクタが呼ばれ、同じメモリを2回解放しようとしてエラーになります。 + +### 解決策:コピー機能を自作する (深いコピー) + +この問題を解決するために、**コピーコンストラクタ**と**コピー代入演算子**を自分で定義して、「深いコピー」を実装します。深いコピーとは、ポインタの指す先の実体(データそのもの)を新しく作ってコピーすることです。 + +```cpp:resource_holder.cpp +#include + +class ResourceHolder { +private: + int* m_data; // リソースとして動的に確保したintへのポインタ + +public: + // コンストラクタ: intを1つ動的に確保し、値を設定 + ResourceHolder(int value) { + m_data = new int(value); + std::cout << "Resource " << *m_data << " created. (at " << m_data << ")" << std::endl; + } + + // デストラクタ: 確保したメモリを解放 + ~ResourceHolder() { + if (m_data != nullptr) { + std::cout << "Resource " << *m_data << " destroyed. (at " << m_data << ")" << std::endl; + delete m_data; + } + } + + // --- ここからが本題です --- + + // 1. コピーコンストラクタ (深いコピー) + // ResourceHolder r2 = r1; のように、オブジェクトの作成と同時にコピーするときに呼ばれる + ResourceHolder(const ResourceHolder& other) { + // ① 新しいメモリを確保する + // ② otherの「値」(*other.m_data)を、新しいメモリにコピーする + m_data = new int(*other.m_data); + std::cout << "COPY CONSTRUCTOR: New resource " << *m_data << " created. (at " << m_data << ")" << std::endl; + } + + // 2. コピー代入演算子 (深いコピー) + // r3 = r1; のように、既存のオブジェクトに代入するときに呼ばれる + ResourceHolder& operator=(const ResourceHolder& other) { + std::cout << "COPY ASSIGNMENT OPERATOR called." << std::endl; + + // ① 自己代入のチェック (a = a; のような無駄な処理を防ぐ) + if (this == &other) { + return *this; // 何もせず自分自身を返す + } + + // ② 自分が元々持っていた古いリソースを解放する + delete m_data; + + // ③ 新しいリソースを確保し、相手の値をコピーする + m_data = new int(*other.m_data); + + return *this; // 自分自身を返すことで、a = b = c; のような連続代入が可能になる + } + + void print() const { + std::cout << "Value: " << *m_data << ", Address: " << m_data << std::endl; + } +}; + +int main() { + std::cout << "--- rh1の作成 ---" << std::endl; + ResourceHolder rh1(10); + + std::cout << "\n--- rh2をrh1で初期化 ---" << std::endl; + ResourceHolder rh2 = rh1; // コピーコンストラクタが呼ばれる + + std::cout << "\n--- rh3の作成 ---" << std::endl; + ResourceHolder rh3(20); + + std::cout << "\n--- rh3にrh1を代入 ---" << std::endl; + rh3 = rh1; // コピー代入演算子が呼ばれる + + std::cout << "\n--- 各オブジェクトの状態 ---" << std::endl; + std::cout << "rh1: "; rh1.print(); + std::cout << "rh2: "; rh2.print(); // rh1とは別のメモリを持っている + std::cout << "rh3: "; rh3.print(); // rh1とは別のメモリを持っている + + std::cout << "\n--- main関数終了 ---" << std::endl; + return 0; // ここでrh1, rh2, rh3のデストラクタが呼ばれ、それぞれが確保したメモリを安全に解放する +} +``` + +```cpp-exec:resource_holder.cpp +--- rh1の作成 --- +Resource 10 created. (at 0x139f065e0) + +--- rh2をrh1で初期化 --- +COPY CONSTRUCTOR: New resource 10 created. (at 0x139f06600) + +--- rh3の作成 --- +Resource 20 created. (at 0x139f06620) + +--- rh3にrh1を代入 --- +COPY ASSIGNMENT OPERATOR called. + +--- 各オブジェクトの状態 --- +rh1: Value: 10, Address: 0x139f065e0 +rh2: Value: 10, Address: 0x139f06600 +rh3: Value: 10, Address: 0x139f06640 + +--- main関数終了 --- +Resource 10 destroyed. (at 0x139f06640) +Resource 10 destroyed. (at 0x139f06600) +Resource 10 destroyed. (at 0x139f065e0) +``` + +*(メモリアドレスは実行するたびに変わります)* + +実行結果を見ると、`rh1`, `rh2`, `rh3` はそれぞれ異なるメモリアドレス (`Address`) を持っていることがわかります。これにより、各オブジェクトは独立したリソースを管理でき、プログラム終了時にそれぞれのデストラクタが安全にメモリを解放できます。 + +| 機能 | いつ呼ばれるか | 何をするか | +| :--- | :--- | :--- | +| **コピーコンストラクタ** | オブジェクトが**作られる時**に、他のオブジェクトで初期化される場合
`ResourceHolder r2 = r1;` | 新しいリソースを確保し、元のオブジェクトの**値**をコピーする。 | +| **コピー代入演算子** | **既にあるオブジェクト**に、他のオブジェクトを代入する場合
`r3 = r1;` | 1. 自分が持っている古いリソースを解放する。
2. 新しいリソースを確保し、元のオブジェクトの**値**をコピーする。 | + +このように、ポインタでリソースを管理するクラスでは、安全なコピーを実現するためにこの2つの関数を自分で定義することが不可欠です。 + +## 演算子のオーバーロード + +C++では、`+`, `-`, `==`, `<<` などの組み込み演算子を、自作のクラスで使えるように**再定義(オーバーロード)**できます。これにより、クラスのインスタンスをあたかも組み込み型(`int`や`double`など)のように直感的に扱えるようになります。 + +例えば、2次元ベクトルを表す `Vector2D` クラスがあるとします。`v3 = v1 + v2;` のように、ベクトル同士の足し算を自然に記述できると便利ですよね。 + +演算子のオーバーロードは、メンバ関数または非メンバ関数(グローバル関数)として定義します。 + +| 演算子 | メンバ関数での定義 | 非メンバ関数での定義 | +| :--- | :--- | :--- | +| 二項演算子 (`+`, `==` etc.) | `T operator+(const U& rhs);` | `T operator+(const T& lhs, const U& rhs);` | +| 単項演算子 (`-`, `!` etc.) | `T operator-();` | `T operator-(const T& obj);` | + +### 実装例 + +`Vector2D` クラスで `+`(加算)、`==`(等価比較)、`<<`(ストリーム出力)をオーバーロードしてみましょう。 + +```cpp:operator_overloading.cpp +#include + +class Vector2D { +public: + double x, y; + + Vector2D(double x = 0.0, double y = 0.0) : x(x), y(y) {} + + // メンバ関数として + 演算子をオーバーロード + Vector2D operator+(const Vector2D& rhs) const { + return Vector2D(this->x + rhs.x, this->y + rhs.y); + } + + // メンバ関数として == 演算子をオーバーロード + bool operator==(const Vector2D& rhs) const { + return (this->x == rhs.x) && (this->y == rhs.y); + } +}; + +// 非メンバ関数として << 演算子をオーバーロード +// 第1引数が std::ostream& なので、メンバ関数にはできない +std::ostream& operator<<(std::ostream& os, const Vector2D& v) { + os << "(" << v.x << ", " << v.y << ")"; + return os; +} + +int main() { + Vector2D v1(1.0, 2.0); + Vector2D v2(3.0, 4.0); + + // operator+ が呼ばれる + Vector2D v3 = v1 + v2; + std::cout << "v1: " << v1 << std::endl; // operator<< + std::cout << "v2: " << v2 << std::endl; // operator<< + std::cout << "v3 = v1 + v2: " << v3 << std::endl; // operator<< + + // operator== が呼ばれる + if (v1 == Vector2D(1.0, 2.0)) { + std::cout << "v1 is equal to (1.0, 2.0)" << std::endl; + } + + return 0; +} +``` + +```cpp-exec:operator_overloading.cpp +v1: (1, 2) +v2: (3, 4) +v3 = v1 + v2: (4, 6) +v1 is equal to (1.0, 2.0) +``` + +`operator<<` は、左辺のオペランドが `std::ostream` 型(`std::cout` など)であるため、`Vector2D` のメンバ関数としては定義できません。そのため、非メンバ関数として定義するのが一般的です。 + +## staticメンバ + +通常、クラスのメンバ変数はオブジェクトごとに個別のメモリ領域を持ちます。しかし、あるクラスの**全てのオブジェクトで共有したい**情報もあります。例えば、「これまでに生成されたオブジェクトの総数」などです。このような場合、**staticメンバ**を使用します。 + +### staticメンバ変数 + +`static` キーワードを付けて宣言されたメンバ変数は、特定のオブジェクトに属さず、クラスそのものに属します。そのため、全オブジェクトでただ1つの実体を共有します。これを**クラス変数**と呼ぶこともあります。 + + * **宣言**: クラス定義の中で `static` を付けて行います。 + * **定義**: クラス定義の外(ソースファイル)で、メモリ上の実体を確保し、初期化します。 + +### staticメンバ関数 + +`static` キーワードを付けて宣言されたメンバ関数は、特定のオブジェクトに依存せずに呼び出せます。そのため、`this` ポインタ(後述)を持ちません。 + + * **アクセス**: staticメンバ変数や他のstaticメンバ関数にはアクセスできますが、非staticなメンバ(インスタンスごとのメンバ変数やメンバ関数)にはアクセスできません。 + * **呼び出し**: `クラス名::関数名()` のように、オブジェクトを生成しなくても呼び出せます。 + +### 実装例 + +ゲームに登場する `Player` クラスがあり、現在何人のプレイヤーが存在するかを管理する例を見てみましょう。 + +```cpp:static_members.cpp +#include +#include + +class Player { +private: + std::string name; + // (1) staticメンバ変数の宣言 + static int playerCount; + +public: + Player(const std::string& name) : name(name) { + playerCount++; // オブジェクトが生成されるたびにインクリメント + std::cout << name << " がゲームに参加しました。現在のプレイヤー数: " << playerCount << std::endl; + } + + ~Player() { + playerCount--; // オブジェクトが破棄されるたびにデクリメント + std::cout << name << " がゲームから退出しました。現在のプレイヤー数: " << playerCount << std::endl; + } + + // (2) staticメンバ関数の宣言 + static int getPlayerCount() { + // name などの非staticメンバにはアクセスできない + return playerCount; + } +}; + +// (3) staticメンバ変数の定義と初期化 +int Player::playerCount = 0; + +int main() { + // オブジェクトがなくてもstaticメンバ関数を呼び出せる + std::cout << "ゲーム開始時のプレイヤー数: " << Player::getPlayerCount() << std::endl; + std::cout << "---" << std::endl; + + Player p1("Alice"); + Player p2("Bob"); + + { + Player p3("Charlie"); + std::cout << "現在のプレイヤー数 (p1経由): " << p1.getPlayerCount() << std::endl; + } // p3のスコープが終わり、デストラクタが呼ばれる + + std::cout << "---" << std::endl; + std::cout << "ゲーム終了時のプレイヤー数: " << Player::getPlayerCount() << std::endl; + + return 0; +} +``` + +```cpp-exec:static_members.cpp +ゲーム開始時のプレイヤー数: 0 +--- +Alice がゲームに参加しました。現在のプレイヤー数: 1 +Bob がゲームに参加しました。現在のプレイヤー数: 2 +Charlie がゲームに参加しました。現在のプレイヤー数: 3 +現在のプレイヤー数 (p1経由): 3 +Charlie がゲームから退出しました。現在のプレイヤー数: 2 +--- +ゲーム終了時のプレイヤー数: 2 +Alice がゲームから退出しました。現在のプレイヤー数: 1 +Bob がゲームから退出しました。現在のプレイヤー数: 0 +``` + +`playerCount` は `p1`, `p2`, `p3` の全てで共有されており、一つの値が更新されていることがわかります。 + +## thisポインタ + +非staticなメンバ関数が呼び出されるとき、その関数は「どのオブジェクトに対して呼び出されたか」を知る必要があります。コンパイラは、そのメンバ関数に対して、呼び出し元のオブジェクトのアドレスを暗黙的に渡します。このアドレスを保持するのが `this` ポインタです。 + +`this` は、メンバ関数内で使用できるキーワードで、自分自身のオブジェクトを指すポインタです。 + +`this` ポインタが主に使われるのは、以下のような場面です。 + +1. **メンバ変数と引数の名前が同じ場合** + コンストラクタの初期化子リストを使わない場合など、引数名とメンバ変数名が同じになることがあります。その際、`this->` を付けることでメンバ変数であることを明示できます。 + + ```cpp + void setX(double x) { + this->x = x; // this->x はメンバ変数, x は引数 + } + ``` + +2. **自分自身の参照やポインタを返す場合** + コピー代入演算子で `return *this;` としたように、オブジェクト自身を返したい場合に使います。これにより、**メソッドチェーン**(`obj.setX(10).setY(20);` のような連続したメソッド呼び出し)が可能になります。 + +### 実装例 + +メソッドチェーンを実現する簡単な例を見てみましょう。 + +```cpp:this_pointer.cpp +#include + +class Point { +private: + int x, y; + +public: + Point(int x = 0, int y = 0) : x(x), y(y) {} + + // 自身の参照を返すことで、メソッドチェーンを可能にする + Point& setX(int newX) { + this->x = newX; + return *this; // 自分自身の参照を返す + } + + Point& setY(int newY) { + this->y = newY; + return *this; // 自分自身の参照を返す + } + + void print() const { + std::cout << "(" << this->x << ", " << this->y << ")" << std::endl; + } +}; + +int main() { + Point p; + + // メソッドチェーン + p.setX(10).setY(20); + + p.print(); + + return 0; +} +``` + +```cpp-exec:this_pointer.cpp +(10, 20) +``` + +`setX` が `p` 自身の参照を返すため、その返り値に対して続けて `.setY(20)` を呼び出すことができます。 + +## この章のまとめ + +この章では、クラスをより効果的に利用するための応用的な機能を学びました。 + + * **オブジェクトのコピー**: ポインタなどリソースを管理するクラスでは、**コピーコンストラクタ**と**コピー代入演算子**を定義し、**深いコピー**を実装することが重要です。これにより、リソースの二重解放などの問題を未然に防ぎます。 + * **演算子のオーバーロード**: `+` や `==` などの演算子を自作クラスに対して定義することで、コードの可読性を高め、直感的な操作を可能にします。 + * **staticメンバ**: `static`メンバ変数やメンバ関数は、クラスの全オブジェクトで共有されるデータや機能を提供します。オブジェクトを生成しなくてもアクセスできるのが特徴です。 + * **thisポインタ**: 非staticメンバ関数内で、呼び出し元のオブジェクト自身を指すポインタです。メンバ変数と引数の区別や、メソッドチェーンの実装に役立ちます。 + +これらの機能を組み合わせることで、C++のクラスは単なるデータの入れ物から、振る舞いを伴った洗練された部品へと進化します。 + +### 練習問題1: 複素数クラス + +実部 (real) と虚部 (imaginary) を`double`型で持つ複素数クラス `Complex` を作成してください。以下の要件を満たすものとします。 + +1. コンストラクタで実部と虚部を初期化できるようにする。 +2. 複素数同士の足し算 (`+`) と掛け算 (`*`) を演算子オーバーロードで実装する。 + * 加算: $(a+bi) + (c+di) = (a+c) + (b+d)i$ + * 乗算: $(a+bi) \* (c+di) = (ac-bd) + (ad+bc)i$ +3. `std::cout` で `(a + bi)` という形式で出力できるように、`<<` 演算子をオーバーロードする。(虚部が負の場合は `(a - bi)` のように表示されるとより良い) + +```cpp:practice6_1.cpp +#include + +// ここに Complex クラスを実装してください + +int main() { + Complex c1(1.0, 2.0); // 1 + 2i + Complex c2(3.0, 4.0); // 3 + 4i + Complex sum = c1 + c2; + Complex product = c1 * c2; + + std::cout << "c1: " << c1 << std::endl; + std::cout << "c2: " << c2 << std::endl; + std::cout << "c1 + c2 = " << sum << std::endl; + std::cout << "c1 * c2 = " << product << std::endl; + return 0; +} +``` + +```cpp-exec:practice6_1.cpp +c1: (1 + 2i) +c2: (3 + 4i) +c1 + c2 = (4 + 6i) +c1 * c2 = (-5 + 10i) +``` + +### 練習問題2: 動的配列クラスのコピー制御 + +整数 (`int`) の動的配列を管理するクラス `IntArray` を作成してください。このクラスは、コンストラクタで指定されたサイズの配列を `new` で確保し、デストラクタで `delete[]` を使って解放します。 + +この `IntArray` クラスに対して、**深いコピー**を正しく行うための**コピーコンストラクタ**と**コピー代入演算子**を実装してください。 + +```cpp:practice6_2.cpp +#include + +// ここに IntArray クラスを実装してください + +int main() { + IntArray arr1(5); // サイズ5の配列を作成 + for (int i = 0; i < 5; ++i) { + arr1.set(i, i * 10); // 0, 10, 20, 30, 40 + } + + IntArray arr2 = arr1; // コピーコンストラクタ + IntArray arr3(3); + arr3 = arr1; // コピー代入演算子 + + std::cout << "arr1: "; + for (int i = 0; i < 5; ++i) { + std::cout << arr1.get(i) << " "; + } + std::cout << std::endl; + + std::cout << "arr2 (コピー): "; + for (int i = 0; i < 5; ++i) { + std::cout << arr2.get(i) << " "; + } + std::cout << std::endl; + + std::cout << "arr3 (代入): "; + for (int i = 0; i < 5; ++i) { + std::cout << arr3.get(i) << " "; + } + std::cout << std::endl; + + return 0; +} +``` + +```cpp-exec:practice6_2.cpp +arr1: 0 10 20 30 40 +arr2 (コピー): 0 10 20 30 40 +arr3 (代入): 0 10 20 30 40 +``` diff --git a/public/docs/cpp-7.md b/public/docs/cpp-7.md new file mode 100644 index 0000000..221f018 --- /dev/null +++ b/public/docs/cpp-7.md @@ -0,0 +1,281 @@ +# 第7章: 継承とポリモーフィズム + +オブジェクト指向プログラミング(OOP)の真の力を解放する時が来ました!💪 この章では、OOPの強力な柱である「**継承 (Inheritance)**」と「**ポリモーフィズム (Polymorphism) / 多態性**」を学びます。これらの概念をマスターすることで、コードの再利用性を高め、柔軟で拡張性の高いプログラムを設計できるようになります。 + +## クラスの継承 + +**継承**とは、既存のクラス(**親クラス**または**基底クラス**と呼びます)の機能を引き継いで、新しいクラス(**子クラス**または**派生クラス**と呼びます)を作成する仕組みです。これにより、共通の機能を何度も書く必要がなくなり、コードの重複を避けられます。 + +例えば、「動物」という大まかなクラスがあり、その特徴を引き継いで「犬」や「猫」といった具体的なクラスを作ることができます。「犬」も「猫」も「動物」が持つ「食べる」という共通の機能を持っていますよね。 + +C++では、クラス名の後に `: public 親クラス名` と書くことで継承を表現します。 + +```cpp:inheritance_basic.cpp +#include +#include + +// 親クラス (基底クラス) +class Animal { +public: + std::string name; + + void eat() { + std::cout << name << " is eating." << std::endl; + } +}; + +// 子クラス (派生クラス) +// Animalクラスのpublicメンバを引き継ぐ +class Dog : public Animal { +public: + void bark() { + std::cout << name << " says Woof!" << std::endl; + } +}; + +int main() { + Dog my_dog; + my_dog.name = "Pochi"; + + // 親クラスから継承したメンバ変数・メンバ関数 + my_dog.eat(); + + // Dogクラス独自のメンバ関数 + my_dog.bark(); + + return 0; +} +``` + +```cpp-exec:inheritance_basic.cpp +Pochi is eating. +Pochi says Woof! +``` + +この例では、`Dog`クラスは`Animal`クラスを継承しています。そのため、`Dog`クラスのオブジェクト `my_dog` は、`Animal`クラスで定義されたメンバ変数 `name` やメンバ関数 `eat()` を、まるで自分のものであるかのように利用できます。 + +## 仮想関数 (virtual) とポリモーフィズム + +継承の最も強力な側面は、**ポリモーフィズム(多態性)**を実現できることです。ポリモーフィズムとは、ギリシャ語で「多くの形を持つ」という意味で、プログラミングにおいては「**同じインターフェース(指示)で、オブジェクトの種類に応じて異なる振る舞いをさせる**」ことを指します。 + +これを実現するのが **仮想関数 (virtual function)** です。親クラスの関数宣言の前に `virtual` キーワードを付けると、その関数は仮想関数になります。 + +親クラスのポインタや参照は、子クラスのオブジェクトを指すことができます。このとき、呼び出された仮想関数は、ポインタが指している**オブジェクトの実際の型**に基づいて決定されます。 + +言葉だけでは難しいので、コードで見てみましょう。 + +```cpp:polymorphism_example.cpp +#include +#include + +class Animal { +public: + // speak() を仮想関数として宣言 + virtual void speak() { + std::cout << "Some generic animal sound..." << std::endl; + } +}; + +class Dog : public Animal { +public: + // 親クラスの仮想関数を上書き (オーバーライド) + void speak() override { // overrideキーワードについては後述 + std::cout << "Woof!" << std::endl; + } +}; + +class Cat : public Animal { +public: + // 親クラスの仮想関数を上書き (オーバーライド) + void speak() override { + std::cout << "Meow!" << std::endl; + } +}; + +// Animalへのポインタを受け取る関数 +void make_animal_speak(Animal* animal) { + animal->speak(); // ポインタが指す先の実際のオブジェクトに応じて、適切な speak() が呼ばれる +} + +int main() { + Animal generic_animal; + Dog dog; + Cat cat; + + std::cout << "Calling through function:" << std::endl; + make_animal_speak(&generic_animal); + make_animal_speak(&dog); // Dogオブジェクトを渡す + make_animal_speak(&cat); // Catオブジェクトを渡す + + return 0; +} +``` + +```cpp-exec:polymorphism_example.cpp +Calling through function: +Some generic animal sound... +Woof! +Meow! +``` + +`make_animal_speak` 関数は `Animal*` 型の引数を取りますが、`Dog`オブジェクトや`Cat`オブジェクトのアドレスを渡すことができています。そして、`animal->speak()` を呼び出すと、`animal` ポインタが実際に指しているオブジェクトの `speak()` が実行されます。これがポリモーフィズムです。もし `Animal`クラスの `speak()` に `virtual` が付いていなければ、どのオブジェクトを渡しても `Animal` の `speak()` が呼ばれてしまいます。 + +## オーバーライド (override) + +先ほどの例で `override` というキーワードが登場しましたね。これはC++11から導入されたもので、子クラスの関数が**親クラスの仮想関数を上書き(オーバーライド)する意図があることを明示する**ためのものです。 + +`override` を付けておくと、もし親クラスに対応する仮想関数が存在しない場合(例えば、関数名をタイプミスした場合など)に、コンパイラがエラーを検出してくれます。 + +```cpp +class Dog : public Animal { +public: + // もし親クラスのspeakがvirtualでなかったり、 + // speek() のようにタイプミスしたりすると、コンパイルエラーになる。 + void speak() override { + std::cout << "Woof!" << std::endl; + } +}; +``` + +意図しないバグを防ぐために、仮想関数をオーバーライドする際は必ず `override` を付ける習慣をつけましょう。 + +## 抽象クラス + +時には、「具体的な実装を持たず、子クラスに実装を強制するための設計図」としてのみ機能するクラスを定義したい場合があります。これが**抽象クラス (Abstract Class)** です。 + +抽象クラスは、**純粋仮想関数 (pure virtual function)** を1つ以上持つクラスです。純粋仮想関数は、末尾に `= 0` を付けて宣言します。 + +```cpp +virtual void function_name() = 0; // これが純粋仮想関数 +``` + +抽象クラスは以下の特徴を持ちます。 + + * インスタンス化(オブジェクトの作成)ができない。 + * 抽象クラスを継承した子クラスは、全ての純粋仮想関数をオーバーライド(実装)しなければならない。さもなければ、その子クラスもまた抽象クラスとなる。 + +```cpp:abstract_class_example.cpp +#include + +// Shapeは純粋仮想関数 draw() を持つため、抽象クラスとなる +class Shape { +public: + // 純粋仮想関数 + // このクラスを継承するクラスは、必ず draw() を実装しなければならない + virtual void draw() = 0; + + // 仮想デストラクタ (継承を扱う際は重要。詳しくは今後の章で) + virtual ~Shape() {} +}; + +class Circle : public Shape { +public: + void draw() override { + std::cout << "Drawing a circle: ○" << std::endl; + } +}; + +class Square : public Shape { +public: + void draw() override { + std::cout << "Drawing a square: □" << std::endl; + } +}; + +int main() { + // Shape my_shape; // エラー!抽象クラスはインスタンス化できない + + Circle circle; + Square square; + + Shape* shape1 = &circle; + Shape* shape2 = □ + + shape1->draw(); + shape2->draw(); + + return 0; +} +``` + +```cpp-exec:abstract_class_example.cpp +Drawing a circle: ○ +Drawing a square: □ +``` + +`Shape` クラスは「図形なら描画できるはずだ」というインターフェース(契約)を定義し、具体的な描画方法は子クラスである `Circle` や `Square` に任せています。このように、抽象クラスはプログラムの骨格となる設計を強制するのに非常に役立ちます。 + +## この章のまとめ + + * **継承**: 既存のクラスの機能を引き継ぎ、コードの再利用性を高める仕組みです。`(子クラス) : public (親クラス)` のように書きます。 + * **ポリモーフィズム**: 「同じ指示でも、オブジェクトの種類によって異なる振る舞いをさせる」性質です。 + * **仮想関数 (`virtual`)**: ポリモーフィズムを実現するための鍵です。親クラスの関数に `virtual` を付けると、ポインタや参照経由で呼び出した際に、オブジェクトの実際の型に応じた関数が実行されます。 + * **オーバーライド (`override`)**: 子クラスで親クラスの仮想関数を上書きする意図を明示します。コンパイラがチェックしてくれるため、安全性が向上します。 + * **抽象クラス**: 1つ以上の**純粋仮想関数 (`virtual ... = 0;`)** を持つクラスです。インスタンス化できず、継承されるための設計図として機能します。 + +### 練習問題1:乗り物の階層構造 + +`Vehicle` という親クラスを作成し、`move()` というメンバ関数を持たせましょう。次に、`Vehicle` を継承して `Car` クラスと `Motorcycle` クラスを作成し、それぞれが独自の `move()` の振る舞いをするようにオーバーライドしてください。 + +`main` 関数では、`Vehicle` のポインタの配列を作成し、`Car` と `Motorcycle` のオブジェクトを格納して、ループでそれぞれの `move()` を呼び出してください。 + +```cpp:practice7_1.cpp +#include +#include + + +// ここに Vehicle, Car, Motorcycle クラスを定義してください + + +int main() { + // Vehicleのポインタの配列を作成 + Vehicle* vehicles[2]; + + Car my_car; + Motorcycle my_motorcycle; + + vehicles[0] = &my_car; + vehicles[1] = &my_motorcycle; + + // それぞれのmove()を呼び出す + for (int i = 0; i < 2; ++i) { + vehicles[i]->move(); + } + + return 0; +} +``` + +```cpp-exec:practice7_1.cpp +``` + +### 問題2: 従業員の給与計算 + +`Employee` という抽象クラスを定義してください。このクラスは、従業員の名前を保持し、給与を計算するための純粋仮想関数 `calculate_salary()` を持ちます。 + +次に、`Employee` を継承して、`FullTimeEmployee`(月給制)と `PartTimeEmployee`(時給制)の2つのクラスを作成します。それぞれのクラスで `calculate_salary()` を具体的に実装してください。 + +`main` 関数で、それぞれのクラスのインスタンスを作成し、給与が正しく計算されることを確認してください。 + +```cpp:practice7_2.cpp +#include +#include + +// ここに Employee, FullTimeEmployee, PartTimeEmployee クラスを定義してください + + +int main() { + FullTimeEmployee full_time_emp("Alice", 3000); // 月給3000ドル + PartTimeEmployee part_time_emp("Bob", 20, 80); // 時給20ドル、80時間勤務 + + std::cout << full_time_emp.get_name() << "'s Salary: $" << full_time_emp.calculate_salary() << std::endl; + std::cout << part_time_emp.get_name() << "'s Salary: $" << part_time_emp.calculate_salary() << std::endl; + + return 0; +} +``` + +```cpp-exec:practice7_2.cpp +Alice's Salary: $3000 +Bob's Salary: $1600 +``` diff --git a/public/docs/cpp-8.md b/public/docs/cpp-8.md new file mode 100644 index 0000000..074c84e --- /dev/null +++ b/public/docs/cpp-8.md @@ -0,0 +1,224 @@ +# 第8章: テンプレートによる汎用プログラミング + +これまでの章では、`int`や`double`、あるいは自作の`Car`クラスのように、特定の型に対して処理を行う関数やクラスを作成してきました。しかし、プログラムが複雑になるにつれて、「型は違うけれど、行いたい処理は全く同じ」という状況が頻繁に発生します。例えば、2つの値の大きい方を返す`max`という関数を考えてみましょう。 + +```cpp +int max_int(int a, int b) { + return (a > b) ? a : b; +} + +double max_double(double a, double b) { + return (a > b) ? a : b; +} +``` + +このように、型ごとに同じロジックの関数をいくつも用意するのは非効率的ですし、バグの温床にもなります。 + +この問題を解決するのが**テンプレート**です。テンプレートを使うと、具体的な型を "仮引数" のように扱い、様々な型に対応できる関数やクラスの「設計図」を作ることができます。このような、型に依存しないプログラミングスタイルを**ジェネリックプログラミング(汎用プログラミング)**と呼びます。 + +## 関数テンプレート: intでもdoubleでもstringでも動く関数を作る + +関数テンプレートを使うと、先ほどの`max`関数の問題をエレガントに解決できます。 + +```cpp:function_template_intro.cpp +#include +#include + +// Tという名前で型を仮引数として受け取るテンプレートを宣言 +template +T max_value(T a, T b) { + return (a > b) ? a : b; +} + +int main() { + // int型でmax_valueを呼び出す + std::cout << "max(10, 20) = " << max_value(10, 20) << std::endl; + + // double型でmax_valueを呼び出す + std::cout << "max(3.14, 1.41) = " << max_value(3.14, 1.41) << std::endl; + + // std::string型でも動作する! + std::string s1 = "world"; + std::string s2 = "hello"; + std::cout << "max(\"world\", \"hello\") = " << max_value(s1, s2) << std::endl; + + return 0; +} +``` + +```cpp-exec:function_template_intro.cpp +max(10, 20) = 20 +max(3.14, 1.41) = 3.14 +max("world", "hello") = world +``` + +### テンプレートの仕組み + +`template `という部分が、この関数がテンプレートであることを示しています。 + + * **`template <...>`**: テンプレートの宣言を開始します。 + * **`typename T`**: `T`という名前の「型引数」を定義しています。`typename`の代わりに`class`と書くこともできますが、意味は同じです。`T`は、このテンプレートが実際に使われるときに具体的な型(`int`や`double`など)に置き換えられます。 + +`main`関数で`max_value(10, 20)`のように呼び出すと、コンパイラは引数の型が`int`であることから、`T`を`int`だと自動的に判断します(これを**テンプレート引数推論**と呼びます)。そして、内部的に以下のような`int`版の関数を生成してくれるのです。 + +```cpp +// コンパイラが内部的に生成するコードのイメージ +int max_value(int a, int b) { + return (a > b) ? a : b; +} +``` + +同様に、`double`や`std::string`で呼び出されれば、それぞれの型に対応したバージョンの関数が自動的に生成されます。これにより、私たちは一つの「設計図」を書くだけで、様々な型に対応できるのです。 + +## クラステンプレート: 様々な型のデータを格納できるクラスを作る + +テンプレートの力は、クラスにも適用できます。これにより、様々な型のデータを格納できる汎用的なクラス(コンテナなど)を作成できます。例えば、「2つの値をペアで保持する」クラスを考えてみましょう。 + +```cpp:class_template_intro.cpp +#include +#include + +// 2つの型 T1, T2 を引数に取るクラステンプレート +template +class Pair { +public: + T1 first; + T2 second; + + // コンストラクタ + Pair(T1 f, T2 s) : first(f), second(s) {} + + void print() { + std::cout << "(" << first << ", " << second << ")" << std::endl; + } +}; + +int main() { + // T1=int, T2=std::string としてPairクラスのオブジェクトを生成 + Pair p1(1, "apple"); + p1.print(); + + // T1=std::string, T2=double としてPairクラスのオブジェクトを生成 + Pair p2("pi", 3.14159); + p2.print(); + + // 違う型のPair同士は当然、別の型として扱われる + // p1 = p2; // これはコンパイルエラーになる + + return 0; +} +``` + +```cpp-exec:class_template_intro.cpp +(1, apple) +(pi, 3.14159) +``` + +### クラステンプレートの仕組み + +関数テンプレートと基本的な考え方は同じですが、いくつか重要な違いがあります。 + +1. **明示的な型指定**: + 関数テンプレートではコンパイラが型を推論してくれましたが、クラステンプレートの場合は、オブジェクトを生成する際に`Pair`のように、開発者が明示的に型を指定する必要があります。 + +2. **インスタンス化**: + `Pair`のように具体的な型を指定してオブジェクトを作ることを、テンプレートの**インスタンス化**と呼びます。コンパイラは、この指定に基づいて`T1`を`int`に、`T2`を`std::string`に置き換えた、以下のような新しいクラスを内部的に生成します。 + + ```cpp + // コンパイラが内部的に生成するクラスのイメージ + class Pair_int_string { // クラス名は実際には異なります + public: + int first; + std::string second; + + Pair_int_string(int f, std::string s) : first(f), second(s) {} + + void print() { + std::cout << "(" << first << ", " << second << ")" << std::endl; + } + }; + ``` + + `Pair`と`Pair`は、コンパイルされると全く別のクラスとして扱われることに注意してください。 + +クラステンプレートは、C++の強力なライブラリである**STL (Standard Template Library)**の根幹をなす技術です。次章で学ぶ`vector`や`map`といった便利なコンテナは、すべてクラステンプレートで実装されています。 + +## この章のまとめ + + * **ジェネリックプログラミング**は、特定の型に縛られない、汎用的なコードを書くための手法です。 + * **テンプレート**は、C++でジェネリックプログラミングを実現するための機能です。 + * **関数テンプレート**を使うと、様々な型の引数に対して同じ処理を行う関数を定義できます。呼び出し時には、コンパイラが**テンプレート引数推論**によって型を自動的に決定します。 + * **クラステンプレート**を使うと、様々な型を扱える汎用的なクラスを定義できます。オブジェクトを生成する際には、`< >`内に具体的な型を**明示的に指定**してインスタンス化する必要があります。 + +テンプレートを使いこなすことで、コードの再利用性が劇的に向上し、より柔軟で堅牢なプログラムを記述できるようになります。 + +### 練習問題1: 汎用的なprint関数 + +任意の型の配列(ここでは`std::vector`を使いましょう)を受け取り、その要素をすべて画面に出力する関数テンプレート`print_elements`を作成してください。 + +```cpp:practice8_1.cpp +#include +#include +#include + +// ここに関数テンプレート print_elements を実装してください + + +int main() { + std::vector v_int = {1, 2, 3, 4, 5}; + std::cout << "Integers: "; + print_elements(v_int); + + std::vector v_str = {"C++", "is", "powerful"}; + std::cout << "Strings: "; + print_elements(v_str); + + return 0; +} +``` + +```cpp-exec:practice8_1.cpp +Integers: 1 2 3 4 5 +Strings: C++ is powerful +``` + +### 練習問題2: 汎用的なスタッククラス + +後入れ先出し(LIFO)のデータ構造であるスタックを、クラステンプレート`SimpleStack`として実装してください。以下のメンバ関数を持つようにしてください。 + + * `void push(T item)`: スタックに要素を追加する + * `T pop()`: スタックの先頭から要素を取り出す + * `bool is_empty()`: スタックが空かどうかを返す + +`std::vector`を内部のデータ格納場所として利用して構いません。`int`型と`char`型で動作を確認してください。 + +```cpp:practice8_2.cpp +#include +#include +#include + +// ここにクラステンプレート SimpleStack を実装してください + +int main() { + SimpleStack int_stack; + int_stack.push(10); + int_stack.push(20); + std::cout << "Popped from int_stack: " << int_stack.pop() << std::endl; // 20 + std::cout << "Popped from int_stack: " << int_stack.pop() << std::endl; // 10 + + SimpleStack char_stack; + char_stack.push('A'); + char_stack.push('B'); + std::cout << "Popped from char_stack: " << char_stack.pop() << std::endl; // B + std::cout << "Popped from char_stack: " << char_stack.pop() << std::endl; // A + + return 0; +} +``` + +```cpp-exec:practice8_2.cpp +Popped from int_stack: 20 +Popped from int_stack: 10 +Popped from char_stack: B +Popped from char_stack: A +``` diff --git a/public/docs/cpp-9.md b/public/docs/cpp-9.md new file mode 100644 index 0000000..e3b8791 --- /dev/null +++ b/public/docs/cpp-9.md @@ -0,0 +1,252 @@ +# 第9章: 標準テンプレートライブラリ (STL) ①:コンテナ + +C++の大きな魅力の一つに、**標準テンプレートライブラリ (Standard Template Library, STL)** の存在があります。STLは、よく使われるデータ構造やアルゴリズムを、汎用的かつ効率的に実装したライブラリ群です。この章では、STLの心臓部である**コンテナ**に焦点を当て、データの格納と管理を劇的に楽にする方法を学びます。 + +## STLの全体像: コンテナ、アルゴリズム、イテレータ + +STLは、主に3つの要素から構成されています。 + +1. **コンテナ (Containers)**: データを格納するためのデータ構造です。`vector`(可変長配列)や`map`(連想配列)など、様々な種類があります。 +2. **アルゴリズム (Algorithms)**: ソート、検索、変換など、コンテナ上のデータに対して操作を行う関数群です。 +3. **イテレータ (Iterators)**: コンテナの要素を指し示し、アルゴリズムがコンテナの種類に依存せずに各要素にアクセスするための統一的なインターフェースを提供します。ポインタを一般化したようなものです。 + +これら3つが連携することで、C++プログラマは効率的で再利用性の高いコードを素早く書くことができます。この章では「コンテナ」を、次の章では「アルゴリズム」と、それらをつなぐ「イテレータ」の応用を詳しく見ていきます。 + +## std::vector: 最もよく使う可変長配列 + +`std::vector`は、最も基本的で最もよく使われるコンテナです。他の言語でいうところの「リスト」や「動的配列」に相当し、要素を連続したメモリ領域に格納します。 + +**主な特徴**: + + * **動的なサイズ**: 必要に応じて自動的にサイズが拡張されます。 + * **高速なランダムアクセス**: インデックス(添字)を使って `[i]` の形式で要素に高速にアクセスできます (`O(1)`)。 + * **末尾への高速な追加・削除**: `push_back()` や `pop_back()` を使った末尾への操作は非常に高速です。 + +`std::vector`を使うには、``ヘッダをインクルードする必要があります。 + +```cpp:vector_example.cpp +#include +#include +#include + +int main() { + // string型の要素を格納するvectorを作成 + std::vector names; + + // push_backで末尾に要素を追加 + names.push_back("Alice"); + names.push_back("Bob"); + names.push_back("Charlie"); + + // インデックスを使った要素へのアクセス + std::cout << "Index 1: " << names[1] << std::endl; + + // 範囲for文 (range-based for loop) を使った全要素の走査 + std::cout << "\nAll names:" << std::endl; + for (const std::string& name : names) { + std::cout << "- " << name << std::endl; + } + + // size()で現在の要素数を取得 + std::cout << "\nCurrent size: " << names.size() << std::endl; + + // pop_backで末尾の要素を削除 + names.pop_back(); // "Charlie"を削除 + + std::cout << "\nAfter pop_back:" << std::endl; + for (const std::string& name : names) { + std::cout << "- " << name << std::endl; + } + std::cout << "Current size: " << names.size() << std::endl; + + return 0; +} +``` + +```cpp-exec:vector_example.cpp +Index 1: Bob + +All names: +- Alice +- Bob +- Charlie + +Current size: 3 + +After pop_back: +- Alice +- Bob +Current size: 2 +``` + +`std::vector`は、どのコンテナを使うか迷ったら、まず最初に検討すべきデフォルトの選択肢と言えるほど万能です。 + +## std::map: キーと値のペアを管理する連想配列 + +`std::map`は、キー (key) と値 (value) のペアを管理するためのコンテナです。他の言語の「辞書 (dictionary)」や「ハッシュマップ (hash map)」に似ています。キーを使って値を高速に検索、追加、削除できます。 + +**主な特徴**: + + * **キーによる高速な検索**: キーに基づいて要素が自動的にソートされて格納されるため、検索、挿入、削除が高速です (`O(log n)`)。 + * **一意なキー**: `std::map`内のキーは重複しません。同じキーで値を挿入しようとすると、既存の値が上書きされます。 + +`std::map`を使うには、``ヘッダをインクルードする必要があります。 + +```cpp:map_example.cpp +#include +#include +#include + +int main() { + // キーがstring型、値がint型のmapを作成 + std::map scores; + + // []演算子で要素を追加・更新 + scores["Alice"] = 95; + scores["Bob"] = 88; + scores["Charlie"] = 76; + + // []演算子で値にアクセス + std::cout << "Bob's score: " << scores["Bob"] << std::endl; + + // 新しいキーで追加 + scores["David"] = 100; + + // 既存のキーの値を更新 + scores["Alice"] = 98; + + // 範囲for文を使った全要素の走査 + // autoキーワードを使うと型推論が効いて便利 + std::cout << "\nAll scores:" << std::endl; + for (const auto& pair : scores) { + std::cout << "- " << pair.first << ": " << pair.second << std::endl; + } + + // count()でキーの存在を確認 + std::string search_key = "Charlie"; + if (scores.count(search_key)) { + std::cout << "\n" << search_key << " is in the map." << std::endl; + } + + // erase()で要素を削除 + scores.erase("Bob"); + + std::cout << "\nAfter erasing Bob:" << std::endl; + for (const auto& pair : scores) { + std::cout << "- " << pair.first << ": " << pair.second << std::endl; + } + + return 0; +} +``` + +```cpp-exec:map_example.cpp +Bob's score: 88 + +All scores: +- Alice: 98 +- Bob: 88 +- Charlie: 76 +- David: 100 + +Charlie is in the map. + +After erasing Bob: +- Alice: 98 +- Charlie: 76 +- David: 100 +``` + +`std::map`は、キーと値のペアを効率的に管理したい場合に非常に強力なツールです。 + +## その他: 目的に応じたコンテナ + +STLには、他にも特定の目的に特化したコンテナが多数用意されています。ここでは代表的なものをいくつか紹介します。 + + * `std::list`: 双方向リスト。要素の途中への挿入・削除が非常に高速 (`O(1)`) ですが、ランダムアクセスはできません(先頭から順番にたどる必要があります)。``ヘッダが必要です。 + * `std::set`: 重複しない要素の集合を管理します。要素は自動的にソートされます。特定の要素が集合内に存在するかどうかを高速に判定したい場合 (`O(log n)`) に便利です。``ヘッダが必要です。 + * `std::unordered_map`: `std::map`と同様にキーと値のペアを管理しますが、内部的にハッシュテーブルを使うため、平均的な検索・挿入・削除がさらに高速 (`O(1)`) です。ただし、要素はソートされません。``ヘッダが必要です。 + * `std::queue`, `std::stack`: それぞれ先入れ先出し (FIFO)、後入れ先出し (LIFO) のデータ構造を実装するためのコンテナアダプタです。 + +どのコンテナを選択するかは、プログラムの要件(データのアクセスパターン、挿入・削除の頻度など)によって決まります。まずは`std::vector`を基本とし、必要に応じて他のコンテナを検討するのが良いアプローチです。 + +## この章のまとめ + + * **STL**は、**コンテナ**、**アルゴリズム**、**イテレータ**の3つの主要コンポーネントから構成される、C++の強力な標準ライブラリです。 + * **コンテナ**は、データを格納するためのクラスです。 + * `std::vector`は、最も一般的に使われる動的配列で、高速なランダムアクセスと末尾への簡単な要素追加が特徴です。 + * `std::map`は、キーと値のペアを管理する連想配列で、キーによる高速な検索が可能です。 + * 他にも`std::list`, `std::set`など、特定の用途に合わせた様々なコンテナが用意されています。 + +### 練習問題1: 数値ベクタの操作 + +`std::vector`型の整数のリストに対して、以下の処理を行うプログラムを作成してください。 + +1. ベクタに格納されている全ての数値の合計値を計算して表示する。 +2. ベクタの中の最大値を検索して表示する。 +3. ベクタの要素を逆順にして、その内容を表示する。(ヒント:新しいベクタを作っても良いですし、`std::swap`を使っても構いません) + +```cpp:practice9_1.cpp +#include +#include + +int main() { + std::vector numbers = {3, 5, 2, 8, 6}; + + // 1. 合計値の計算 + + + // 2. 最大値の検索 + + + // 3. 要素の逆順表示 + + + return 0; +} +``` + +```cpp-exec:practice9_1.cpp +Sum: 24 +Max: 8 +Reversed: 6 8 2 5 3 +``` + + +### 練習問題2: 簡単な単語カウンター + +英文(スペースで区切られた単語の列)を読み込み、各単語が何回出現したかをカウントするプログラムを`std::map`を使って作成してください。最後に、出現した全単語とその出現回数をアルファベット順に表示してください。 + +> 文字列を単語ごとに分割するには、以下のように`std::istringstream`を使うと便利です。 +```cpp +#include + +std::string text = "this is a sample text"; +std::istringstream iss(text); +std::string word; +while (iss >> word) { + // wordには1単語ずつ格納される +} +``` + + + +```cpp:practice9_2.cpp +#include +#include +#include +#include + +int main() { + std::string text = "cpp is fun and cpp is powerful"; + + +} +``` +```cpp-exec:practice9_2.cpp +and: 1 +cpp: 2 +fun: 1 +is: 2 +powerful: 1 +```