diff --git a/README.md b/README.md index c7c14f2..8fa7273 100644 --- a/README.md +++ b/README.md @@ -64,8 +64,9 @@ npm run lint - nを8, 10, 12, 15 など変えて何回か出力させ、それを統合していい感じの章立てを決める - 実際にドキュメントを書かせる > 以下の内容で`言語名`チュートリアルの第`n`章を書いてください。他の言語でのプログラミングは経験がある人を対象にします。 - > タイトルにはレベル1の見出し(#), それ以降の見出しにはレベル2以下(##)を使用してください。 - REPLで動作可能なコード例はスクリプトではなくREPLの実行例として書いてください。 + > タイトルにはレベル1の見出し(#), それ以降の見出しにはレベル2以下(##)を使用してください。 + > canvasは使わずに出力してください。 + > REPLで動作可能なコード例はスクリプトではなくREPLの実行例として書いてください。 > コード例はREPLの実行例では \`\`\``言語名`-repl 、ソースファイルの場合は \`\`\``言語名`:ファイル名`.拡張子` ではじまるコードブロックで示してください。ファイル名は被らないようにしてください。 > また、ファイルの場合は \`\`\``言語名`-exec:ファイル名`.拡張子` のコードブロック内に実行結果例を記載してください。 > また、最後には この章のまとめ セクションと、練習問題を2つほど書いてください。練習問題はこの章で学んだ内容を活用してコードを書かせるものにしてください。 @@ -81,7 +82,6 @@ npm run lint > - Gemini出力の調整 - Canvasを使われた場合はやり直す。(Canvasはファイル名付きコードブロックで壊れる) - - 箇条書きの最後に `` と出力される場合がある。消す - 太字がなぜか `**キーワード**` の代わりに `\*\*キーワード\*\*` となっている場合がある。 `\*\*` → `**` の置き換えで対応 - 見出しの前に `-----` (水平線)が入る場合がある。my.code();は水平線の表示に対応しているが、消す方向で統一 - `言語名-repl` にはページ内で一意なIDを追加する (例: `言語名-repl:1`) diff --git a/app/[docs_id]/markdown.tsx b/app/[docs_id]/markdown.tsx index e6a70c0..4443e69 100644 --- a/app/[docs_id]/markdown.tsx +++ b/app/[docs_id]/markdown.tsx @@ -1,5 +1,7 @@ import Markdown, { Components, ExtraProps } from "react-markdown"; import remarkGfm from "remark-gfm"; +import removeComments from "remark-remove-comments"; +import remarkCjkFriendly from "remark-cjk-friendly"; import { EditorComponent, getAceLang } from "../terminal/editor"; import { ExecFile } from "../terminal/exec"; import { JSX, ReactNode } from "react"; @@ -13,7 +15,10 @@ import { export function StyledMarkdown({ content }: { content: string }) { return ( - + {content} ); diff --git a/app/[docs_id]/page.tsx b/app/[docs_id]/page.tsx index c402020..a4067d4 100644 --- a/app/[docs_id]/page.tsx +++ b/app/[docs_id]/page.tsx @@ -7,7 +7,7 @@ import { splitMarkdown } from "./splitMarkdown"; import { PageContent } from "./pageContent"; import { ChatHistoryProvider } from "./chatHistory"; import { getChatFromCache } from "@/lib/chatHistory"; -import { getLanguageName } from "@/pagesList"; +import { getLanguageName, pagesList } from "@/pagesList"; async function getMarkdownContent(docs_id: string): Promise { try { @@ -61,6 +61,14 @@ export default async function Page({ }) { const { docs_id } = await params; + if ( + !pagesList + .find((lang) => docs_id.startsWith(`${lang.id}-`)) + ?.pages.find((page) => docs_id.endsWith(`-${page.id}`)) + ) { + notFound(); + } + const mdContent = getMarkdownContent(docs_id); const splitMdContent = mdContent.then((text) => splitMarkdown(text)); const initialChatHistories = getChatFromCache(docs_id); diff --git a/app/[docs_id]/pageContent.tsx b/app/[docs_id]/pageContent.tsx index 0d48c68..f5968db 100644 --- a/app/[docs_id]/pageContent.tsx +++ b/app/[docs_id]/pageContent.tsx @@ -84,7 +84,7 @@ export function PageContent(props: PageContentProps) { return (
(
{ sectionRefs.current[index] = el; diff --git a/app/[docs_id]/styledSyntaxHighlighter.tsx b/app/[docs_id]/styledSyntaxHighlighter.tsx index 29b959e..f2b1be4 100644 --- a/app/[docs_id]/styledSyntaxHighlighter.tsx +++ b/app/[docs_id]/styledSyntaxHighlighter.tsx @@ -24,15 +24,21 @@ export type MarkdownLang = | "rb" | "cpp" | "c++" + | "rust" + | "rs" | "javascript" | "js" | "typescript" | "ts" | "bash" | "sh" + | "powershell" | "json" + | "toml" | "csv" | "html" + | "makefile" + | "cmake" | "text" | "txt"; @@ -43,11 +49,16 @@ export type SyntaxHighlighterLang = | "ruby" | "c" | "cpp" + | "rust" | "javascript" | "typescript" | "bash" + | "powershell" | "html" - | "json"; + | "json" + | "ini" + | "makefile" + | "cmake"; export function getSyntaxHighlighterLang( lang: MarkdownLang | undefined ): SyntaxHighlighterLang | undefined { @@ -61,6 +72,9 @@ export function getSyntaxHighlighterLang( case "cpp": case "c++": return "cpp"; + case "rust": + case "rs": + return "rust"; case "javascript": case "js": return "javascript"; @@ -70,10 +84,18 @@ export function getSyntaxHighlighterLang( case "bash": case "sh": return "bash"; + case "powershell": + return "powershell"; case "json": return "json"; + case "toml": + return "ini"; case "html": return "html"; + case "makefile": + return "makefile"; + case "cmake": + return "cmake"; case "csv": // not supported case "text": case "txt": @@ -81,7 +103,9 @@ export function getSyntaxHighlighterLang( return undefined; default: lang satisfies never; - console.error(`getSyntaxHighlighterLang() does not handle language ${lang}`); + console.error( + `getSyntaxHighlighterLang() does not handle language ${lang}` + ); return undefined; } } diff --git a/app/accountMenu.tsx b/app/accountMenu.tsx index 9daae66..b0c4dce 100644 --- a/app/accountMenu.tsx +++ b/app/accountMenu.tsx @@ -42,7 +42,7 @@ export function AccountMenu() { } }; - if (isPending) { + if (isPending || !session) { return
; } diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..db5b853 --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,47 @@ +"use client"; // Error boundaries must be Client Components + +import clsx from "clsx"; +import Link from "next/link"; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + return ( +
+

エラー

+

ページの読み込み中にエラーが発生しました。

+
+        {error.message}
+      
+ {error.digest && ( +

+ Digest: {error.digest} +

+ )} +
+
+ + + トップに戻る + +
+
+ ); +} diff --git a/app/layout.tsx b/app/layout.tsx index 151cde0..036996f 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -27,7 +27,7 @@ export default function RootLayout({ {/* mocha.css がbodyに背景色などを設定してしまうので、それを上書きしている */} -
+
+

404

+

指定されたページが見つかりません。

+
+ + トップに戻る + +
+ ); +} diff --git a/app/pagesList.ts b/app/pagesList.ts index 42db5ab..b4cb327 100644 --- a/app/pagesList.ts +++ b/app/pagesList.ts @@ -77,17 +77,37 @@ export const pagesList = [ 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: "プロジェクトの分割とビルド" }, + { id: 2, title: "型システムと制御構造" }, + { id: 3, title: "データ集合とモダンな操作" }, + { id: 4, title: "ポインタとメモリ管理" }, + { id: 5, title: "関数と参照渡し" }, + { id: 6, title: "プロジェクトの分割とビルド" }, + { id: 7, title: "クラスの基礎" }, + { id: 8, title: "クラスを使いこなす" }, + { id: 9, title: "継承とポリモーフィズム" }, + { id: 10, title: "テンプレート" }, + { id: 11, title: "STL ①:コンテナ" }, + { id: 12, title: "STL ②:アルゴリズムとラムダ式" }, + { id: 13, title: "RAIIとスマートポインタ" }, + ], + }, + { + id: "rust", + lang: "Rust", + description: "a", + pages: [ + { id: 1, title: "Rustの世界へようこそ" }, + { id: 2, title: "基本構文と「不変性」" }, + { id: 3, title: "関数と制御フロー" }, + { id: 4, title: "所有権" }, + { id: 5, title: "借用とスライス" }, + { id: 6, title: "構造体とメソッド構文" }, + { id: 7, title: "列挙型とパターンマッチ" }, + { id: 8, title: "モジュールシステムとパッケージ管理" }, + { id: 9, title: "コレクションと文字列" }, + { id: 10, title: "エラーハンドリング" }, + { id: 11, title: "ジェネリクスとトレイト" }, + { id: 12, title: "ライフタイム" }, ], }, ] as const; diff --git a/app/sidebar.tsx b/app/sidebar.tsx index 6347af1..a8d00b3 100644 --- a/app/sidebar.tsx +++ b/app/sidebar.tsx @@ -14,6 +14,8 @@ import { } from "react"; import { DynamicMarkdownSection } from "./[docs_id]/pageContent"; import clsx from "clsx"; +import { LanguageIcon } from "./terminal/icons"; +import { RuntimeLang } from "./terminal/runtime"; export interface ISidebarMdContext { loadedDocsId: string; @@ -159,18 +161,29 @@ export function Sidebar() { setDetailsOpen(newDetailsOpen); }} > - {group.lang} + + + {group.lang} +
    {group.pages.map((page) => (
  • - {page.id}. + + + {page.id}. + + {page.title} {`${group.id}-${page.id}` === currentDocsId && diff --git a/app/terminal/README.md b/app/terminal/README.md index 7f8f40c..1d9edef 100644 --- a/app/terminal/README.md +++ b/app/terminal/README.md @@ -132,23 +132,28 @@ EditorComponent コンポーネントを提供します。 ### Worker -web worker でコードを実行する実装です。worker側のスクリプトは /public にあります。 +web worker でコードを実行する実装です。 workerとの通信部分は言語によらず共通なので、それをworker/runtime.tsxで定義しています。 -Contextは言語ごとに分けて(worker/pyodide.ts などで)定義しています。 Pythonの実行環境にはPyodideを使用しています。 PyodideにはKeyboardInterruptを送信する機能があるのでinterrupt()でそれを利用しています。 Rubyの実行環境にはruby.wasmを使用しています。 -JavaScriptはeval()を使用しています。runFiles()のAPIだけ実装していません。 +JavaScriptはeval()を使用しています。 ### Wandbox -wandbox.org のAPIを利用してC++コードを実行しています。C++以外にもいろいろな言語に対応しています。 +wandbox.org のAPIを利用してコードを実行します。 APIから利用可能なコンパイラとオプションのリストが得られるので、言語ごとにそこからオプションを選択するロジックを実装しています。 C++ではg++の中でheadでない最新のものを選択し、warningスイッチオン、boost有効、std=最新を指定しています。 また、コード実行時にシグナルハンドラーをユーザーのコードに挿入し、エラー時にスタックトレースを表示する処理とそれをjs側でパースする処理を実装しています。 +Rustは最新のものを選択し、-Cdebuginfo=1を追加しています。 +ユーザーのコードをモジュールとしてprog.rsのmain()から呼び出す形に変更しており、ユーザーのコードに `mod foo;` → `use super::foo;`, `fn main()` → `pub fn main()` の改変を加えています。 + +### TypeScript + +[@typescript/vfs](https://www.npmjs.com/package/@typescript/vfs) を使用してブラウザ上でTypeScriptコードをコンパイルし、jsEvalランタイムに渡します。 diff --git a/app/terminal/editor.tsx b/app/terminal/editor.tsx index 4e329a9..e680420 100644 --- a/app/terminal/editor.tsx +++ b/app/terminal/editor.tsx @@ -20,6 +20,7 @@ const AceEditor = lazy(async () => { await import("ace-builds/src-min-noconflict/mode-python"); await import("ace-builds/src-min-noconflict/mode-ruby"); await import("ace-builds/src-min-noconflict/mode-c_cpp"); + await import("ace-builds/src-min-noconflict/mode-rust"); await import("ace-builds/src-min-noconflict/mode-javascript"); await import("ace-builds/src-min-noconflict/mode-typescript"); await import("ace-builds/src-min-noconflict/mode-json"); @@ -36,6 +37,7 @@ export type AceLang = | "python" | "ruby" | "c_cpp" + | "rust" | "javascript" | "typescript" | "json" @@ -53,6 +55,9 @@ export function getAceLang(lang: MarkdownLang | undefined): AceLang { case "cpp": case "c++": return "c_cpp"; + case "rust": + case "rs": + return "rust"; case "javascript": case "js": return "javascript"; @@ -65,9 +70,13 @@ export function getAceLang(lang: MarkdownLang | undefined): AceLang { return "csv"; case "sh": case "bash": + case "powershell": case "text": case "txt": case "html": + case "toml": + case "makefile": + case "cmake": case undefined: console.warn(`Ace editor mode not implemented for language: ${lang}`); return "text"; diff --git a/app/terminal/exec.tsx b/app/terminal/exec.tsx index 1307080..e016cba 100644 --- a/app/terminal/exec.tsx +++ b/app/terminal/exec.tsx @@ -74,9 +74,12 @@ export function ExecFile(props: ExecProps) { return (
    -
    +
    - + {getCommandlineStr?.(props.filenames)}
    diff --git a/app/terminal/highlight.ts b/app/terminal/highlight.ts index a170f71..39b9731 100644 --- a/app/terminal/highlight.ts +++ b/app/terminal/highlight.ts @@ -27,6 +27,7 @@ function getPrismLanguage(language: RuntimeLang): PrismLang { return "javascript"; case "cpp": case "typescript": + case "rust": throw new Error( `highlight for ${language} is disabled because it should not support REPL` ); diff --git a/app/terminal/icons.tsx b/app/terminal/icons.tsx new file mode 100644 index 0000000..2250958 --- /dev/null +++ b/app/terminal/icons.tsx @@ -0,0 +1,121 @@ +import { RuntimeLang } from "./runtime"; + +interface Props { + lang: RuntimeLang; + className?: string; +} +export function LanguageIcon(props: Props) { + switch (props.lang) { + case "cpp": + return ( + + + + ); + case "python": + return ( + + + + + + ); + case "ruby": + return ( + + {/*Ruby Icon + This is shape (source) for Clarity vector icon theme for gtk + + + + Ruby Icon + This is shape (source) for Clarity vector icon theme for gtk + + + Jakub Jankiewicz + + + + + Jakub Jankiewicz + + + 2010 + image/svg+xml + + + + + */} + + + ); + case "javascript": + return ( + + + + ); + case "typescript": + return ( + + + + ); + case "rust": + return ( + + + + ); + default: + props.lang satisfies never; + console.warn("unknown lang for LanguageIcon:", props.lang); + return null; + } +} diff --git a/app/terminal/page.tsx b/app/terminal/page.tsx index d209e91..20f4ffb 100644 --- a/app/terminal/page.tsx +++ b/app/terminal/page.tsx @@ -16,6 +16,16 @@ import { ExecFile } from "./exec"; import { useTypeScript } from "./typescript/runtime"; import { useTerminal } from "./terminal"; +import main_py from "./samples/main.py?raw"; +import main_rb from "./samples/main.rb?raw"; +import main_js from "./samples/main.js?raw"; +import main2_ts from "./samples/main2.ts?raw"; +import main_cpp from "./samples/main.cpp?raw"; +import sub_h from "./samples/sub.h?raw"; +import sub_cpp from "./samples/sub.cpp?raw"; +import main2_rs from "./samples/main2.rs?raw"; +import sub_rs from "./samples/sub.rs?raw"; + export default function RuntimeTestPage() { return (
    @@ -60,7 +70,7 @@ const sampleConfig: Record = { repl: true, replInitContent: '>>> print("Hello, World!")\nHello, World!', editor: { - "main.py": 'print("Hello, World!")', + "main.py": main_py, }, exec: ["main.py"], }, @@ -68,7 +78,7 @@ const sampleConfig: Record = { repl: true, replInitContent: 'irb(main):001:0> puts "Hello, World!"\nHello, World!', editor: { - "main.rb": 'puts "Hello, World!"', + "main.rb": main_rb, }, exec: ["main.rb"], }, @@ -76,7 +86,7 @@ const sampleConfig: Record = { repl: true, replInitContent: '> console.log("Hello, World!");\nHello, World!', editor: { - "main.js": 'console.log("Hello, World!");', + "main.js": main_js, }, exec: ["main.js"], }, @@ -84,8 +94,7 @@ const sampleConfig: Record = { repl: false, editor: { // main.tsにすると出力ファイルがjavascriptのサンプルと被る - "main2.ts": - 'function greet(name: string): void {\n console.log("Hello, " + name + "!");\n}\n\ngreet("World");', + "main2.ts": main2_ts, }, exec: ["main2.ts"], readonlyFiles: ["main2.js"], @@ -93,17 +102,20 @@ const sampleConfig: Record = { cpp: { repl: false, editor: { - "main.cpp": `#include -#include "sub.h" - -int main() { - std::cout << "Hello, World!" << std::endl; -}`, - "sub.h": ``, - "sub.cpp": ``, + "main.cpp": main_cpp, + "sub.h": sub_h, + "sub.cpp": sub_cpp, }, exec: ["main.cpp", "sub.cpp"], }, + rust: { + repl: false, + editor: { + "main2.rs": main2_rs, + "sub.rs": sub_rs, + }, + exec: ["main2.rs"], + }, }; function RuntimeSample({ lang, @@ -190,6 +202,7 @@ function MochaTest() { const jsEval = useJSEval(); const typescript = useTypeScript(jsEval); const wandboxCpp = useWandbox("cpp"); + const wandboxRust = useWandbox("rust"); const runtimeRef = useRef>(null!); runtimeRef.current = { python: pyodide, @@ -197,6 +210,7 @@ function MochaTest() { javascript: jsEval, typescript: typescript, cpp: wandboxCpp, + rust: wandboxRust, }; const [searchParams, setSearchParams] = useState(""); diff --git a/app/terminal/runtime.tsx b/app/terminal/runtime.tsx index 62e49db..0423a92 100644 --- a/app/terminal/runtime.tsx +++ b/app/terminal/runtime.tsx @@ -44,6 +44,7 @@ export type RuntimeLang = | "python" | "ruby" | "cpp" + | "rust" | "javascript" | "typescript"; @@ -61,6 +62,9 @@ export function getRuntimeLang( case "cpp": case "c++": return "cpp"; + case "rust": + case "rs": + return "rust"; case "javascript": case "js": return "javascript"; @@ -69,11 +73,15 @@ export function getRuntimeLang( return "typescript"; case "bash": case "sh": + case "powershell": case "json": + case "toml": case "csv": case "text": case "txt": case "html": + case "makefile": + case "cmake": case undefined: // unsupported languages return undefined; @@ -90,6 +98,7 @@ export function useRuntime(language: RuntimeLang): RuntimeContext { const jsEval = useJSEval(); const typescript = useTypeScript(jsEval); const wandboxCpp = useWandbox("cpp"); + const wandboxRust = useWandbox("rust"); let runtime: RuntimeContext; switch (language) { @@ -108,6 +117,9 @@ export function useRuntime(language: RuntimeLang): RuntimeContext { case "cpp": runtime = wandboxCpp; break; + case "rust": + runtime = wandboxRust; + break; default: language satisfies never; throw new Error(`Runtime not implemented for language: ${language}`); @@ -158,11 +170,17 @@ export function langConstants(lang: RuntimeLang | AceLang): LangConstants { case "c_cpp": case "cpp": return { - tabSize: 2, + // 2文字派と4文字派があるが、geminiが4文字で出力するので4でいいや + tabSize: 4, + }; + case "rust": + return { + tabSize: 4, }; case "json": return { - tabSize: 2, + // python-7章で使っている + tabSize: 4, }; case "csv": case "text": diff --git a/app/terminal/samples/main.cpp b/app/terminal/samples/main.cpp new file mode 100644 index 0000000..b14b8ae --- /dev/null +++ b/app/terminal/samples/main.cpp @@ -0,0 +1,7 @@ +#include "sub.h" +#include + +int main() { + std::cout << "Hello, World!" << std::endl; + std::cout << "foo() = " << foo() << std::endl; +} diff --git a/app/terminal/samples/main.js b/app/terminal/samples/main.js new file mode 100644 index 0000000..184dfcc --- /dev/null +++ b/app/terminal/samples/main.js @@ -0,0 +1 @@ +console.log("Hello, World!"); diff --git a/app/terminal/samples/main.py b/app/terminal/samples/main.py new file mode 100644 index 0000000..7df869a --- /dev/null +++ b/app/terminal/samples/main.py @@ -0,0 +1 @@ +print("Hello, World!") diff --git a/app/terminal/samples/main.rb b/app/terminal/samples/main.rb new file mode 100644 index 0000000..5de78a4 --- /dev/null +++ b/app/terminal/samples/main.rb @@ -0,0 +1 @@ +puts "Hello, World!" diff --git a/app/terminal/samples/main2.rs b/app/terminal/samples/main2.rs new file mode 100644 index 0000000..d95bf87 --- /dev/null +++ b/app/terminal/samples/main2.rs @@ -0,0 +1,6 @@ +mod sub; + +fn main() { + println!("Hello, World!"); + println!("sub::foo() = {}", sub::foo()); +} diff --git a/app/terminal/samples/main2.ts b/app/terminal/samples/main2.ts new file mode 100644 index 0000000..06796c8 --- /dev/null +++ b/app/terminal/samples/main2.ts @@ -0,0 +1,5 @@ +function greet(name: string): void { + console.log("Hello, " + name + "!"); +} + +greet("World"); diff --git a/app/terminal/samples/sub.cpp b/app/terminal/samples/sub.cpp new file mode 100644 index 0000000..dda02b5 --- /dev/null +++ b/app/terminal/samples/sub.cpp @@ -0,0 +1,5 @@ +#include "sub.h" + +int foo() { + return 42; +} diff --git a/app/terminal/samples/sub.h b/app/terminal/samples/sub.h new file mode 100644 index 0000000..eacbb8d --- /dev/null +++ b/app/terminal/samples/sub.h @@ -0,0 +1,3 @@ +#pragma once + +int foo(); diff --git a/app/terminal/samples/sub.rs b/app/terminal/samples/sub.rs new file mode 100644 index 0000000..185ce22 --- /dev/null +++ b/app/terminal/samples/sub.rs @@ -0,0 +1,3 @@ +pub fn foo() -> i32 { + 42 +} diff --git a/app/terminal/tests.ts b/app/terminal/tests.ts index 7ad70e4..6ccfbe0 100644 --- a/app/terminal/tests.ts +++ b/app/terminal/tests.ts @@ -34,6 +34,7 @@ export function defineTests( python: `print("${msg}")`, ruby: `puts "${msg}"`, cpp: null, + rust: null, javascript: `console.log("${msg}")`, typescript: null, } satisfies Record @@ -56,6 +57,7 @@ export function defineTests( python: [`${varName} = ${value}`, `print(${varName})`], ruby: [`${varName} = ${value}`, `puts ${varName}`], cpp: [null, null], + rust: [null, null], javascript: [ `const ${varName} = ${value}`, `console.log(${varName})`, @@ -86,6 +88,7 @@ export function defineTests( python: `raise Exception("${errorMsg}")`, ruby: `raise "${errorMsg}"`, cpp: null, + rust: null, javascript: `throw new Error("${errorMsg}")`, typescript: null, } satisfies Record @@ -108,6 +111,7 @@ export function defineTests( python: [`testVar = 42`, `while True:\n pass`, `print(testVar)`], ruby: [`testVar = 42`, `loop do\nend`, `puts testVar`], cpp: [null, null, null], + rust: [null, null, null], javascript: [ `const testVar = 42`, `while(true) {}`, @@ -150,6 +154,7 @@ export function defineTests( python: `with open("${targetFile}", "w") as f:\n f.write("${msg}")`, ruby: `File.open("${targetFile}", "w") {|f| f.write("${msg}") }`, cpp: null, + rust: null, javascript: null, typescript: null, } satisfies Record @@ -178,6 +183,7 @@ export function defineTests( "test.cpp", `#include \nint main() {\n std::cout << "${msg}" << std::endl;\n return 0;\n}\n`, ], + rust: ["test.rs", `fn main() {\n println!("${msg}");\n}\n`], javascript: ["test.js", `console.log("${msg}")`], typescript: ["test.ts", `console.log("${msg}")`], } satisfies Record @@ -202,6 +208,10 @@ export function defineTests( "test_error.cpp", `#include \nint main() {\n throw std::runtime_error("${errorMsg}");\n return 0;\n}\n`, ], + rust: [ + "test_error.rs", + `fn main() {\n panic!("${errorMsg}");\n}\n`, + ], javascript: ["test_error.js", `throw new Error("${errorMsg}");\n`], // TODO: tscが出す型エラーのテストはできていない typescript: ["test_error.ts", `throw new Error("${errorMsg}");\n`], @@ -248,6 +258,14 @@ export function defineTests( }, ["test_multi_main.cpp", "test_multi_sub.cpp"], ], + rust: [ + { + "test_multi_main.rs": + "mod test_multi_sub;\nfn main() {\n test_multi_sub::print_message();\n}\n", + "test_multi_sub.rs": `pub fn print_message() {\n println!("${msg}");\n}\n`, + }, + ["test_multi_main.rs"], + ], javascript: [null, null], typescript: [null, null], } satisfies Record< @@ -280,6 +298,7 @@ export function defineTests( `File.open("${targetFile}", "w") {|f| f.write("${msg}") }`, ], cpp: [null, null], + rust: [null, null], javascript: [null, null], typescript: [null, null], } satisfies Record diff --git a/app/terminal/wandbox/api.ts b/app/terminal/wandbox/api.ts index 4de1f12..bc4a421 100644 --- a/app/terminal/wandbox/api.ts +++ b/app/terminal/wandbox/api.ts @@ -93,12 +93,14 @@ export interface SelectedCompiler { compilerOptions: string[]; compilerOptionsRaw: string[]; getCommandlineStr: (filenames: string[]) => string; // 表示用 -}; +} interface CompileProps { compilerName: string; compilerOptions: string[]; compilerOptionsRaw: string[]; - codes: Code[]; + code?: string; + codes: Record; + // codes: Code[]; } export interface CompileResultWithOutput extends CompileResult { output: ReplOutput[]; @@ -108,26 +110,26 @@ export async function compileAndRun( options: CompileProps ): Promise { // Call the ndjson API instead of json API - const response = await fetch( - new URL("/api/compile.ndjson", 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), - } - ); + const response = await fetch(new URL("/api/compile.ndjson", WANDBOX), { + method: "post", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + compiler: options.compilerName, + code: options.code ?? "", + codes: Object.entries(options.codes).map(([name, code]) => ({ + file: name, + code: code ?? "", + })) satisfies Code[], + options: options.compilerOptions.join(","), + stdin: "", + "compiler-option-raw": options.compilerOptionsRaw.join("\n"), + "runtime-option-raw": "", + save: false, + is_private: true, + } satisfies CompileParameter), + }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); @@ -150,7 +152,7 @@ export async function compileAndRun( buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); - + // Keep the last incomplete line in the buffer buffer = lines.pop() || ""; diff --git a/app/terminal/wandbox/cpp.ts b/app/terminal/wandbox/cpp.ts index ed0d5e2..4c508e1 100644 --- a/app/terminal/wandbox/cpp.ts +++ b/app/terminal/wandbox/cpp.ts @@ -1,6 +1,8 @@ import { ReplOutput } from "../repl"; import { compileAndRun, CompilerInfo, SelectedCompiler } from "./api"; +import _stacktrace_cpp from "./cpp/_stacktrace.cpp?raw"; + export function selectCppCompiler( compilerList: CompilerInfo[] ): SelectedCompiler { @@ -70,7 +72,7 @@ export function selectCppCompiler( // その他オプション options.compilerOptionsRaw.push("-g"); - // commandline.push("-g"); + commandline.push("-g"); options.getCommandlineStr = (filenames: string[]) => { return [...commandline, ...filenames, "&&", "./a.out"].join(" "); @@ -85,53 +87,60 @@ export async function cppRunFiles( filenames: string[] ): Promise { const result = await compileAndRun({ - compilerName: options.compilerName, - compilerOptions: options.compilerOptions, + ...options, compilerOptionsRaw: [ ...options.compilerOptionsRaw, ...filenames, "_stacktrace.cpp", ], - codes: [ - ...Object.entries(files).map(([name, code]) => ({ - file: name, - code: code || "", - })), - { file: "_stacktrace.cpp", code: CPP_STACKTRACE_HANDLER }, - ], + codes: { ...files, "_stacktrace.cpp": _stacktrace_cpp }, }); + let outputs = result.output; + // Find stack trace in the output - const traceIndex = result.output.findIndex( - (line) => line.type === "stderr" && line.message === "Stack trace:" + const signalIndex = outputs.findIndex( + (line) => + line.type === "stderr" && line.message.startsWith("#!my_code_signal:") + ); + const traceIndex = outputs.findIndex( + (line) => line.type === "stderr" && line.message === "#!my_code_stacktrace:" ); - - let outputs: ReplOutput[]; - + + if (signalIndex >= 0) { + outputs[signalIndex] = { + type: "error", + message: outputs[signalIndex].message.slice(17), + } as const; + } if (traceIndex >= 0) { - // CPP_STACKTRACE_HANDLER のコードで出力されるスタックトレースを、js側でパースしていい感じに表示する - outputs = result.output.slice(0, traceIndex); - outputs.push({ - type: "trace" as const, + // _stacktrace.cpp のコードで出力されるスタックトレースを、js側でパースしていい感じに表示する + const trace = outputs.slice(traceIndex + 1); + const otherOutputs = outputs.slice(0, traceIndex); + const traceOutputs: ReplOutput[] = [{ + type: "trace", message: "Stack trace (filtered):", - }); - - for (const line of result.output.slice(traceIndex + 1)) { - // ユーザーのソースコードだけを対象にする - if (line.type === "stderr" && line.message.includes("/home/wandbox")) { - outputs.push({ - type: "trace" as const, - message: line.message.replace("/home/wandbox/", ""), - }); + }]; + + for (const line of trace) { + if(line.type === "stderr"){ + // ユーザーのソースコードだけを対象にする + if (line.message.includes("/home/wandbox")) { + traceOutputs.push({ + type: "trace", + message: line.message.replace("/home/wandbox/", ""), + }); + } + }else{ + otherOutputs.push(line); } } - } else { - outputs = [...result.output]; + outputs = [...otherOutputs, ...traceOutputs]; } - + if (result.status !== "0") { outputs.push({ - type: "system" as const, + type: "system", message: `ステータス ${result.status} で異常終了しました`, }); } @@ -139,45 +148,3 @@ export async function cppRunFiles( return outputs; } - -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; -`; diff --git a/app/terminal/wandbox/cpp/_stacktrace.cpp b/app/terminal/wandbox/cpp/_stacktrace.cpp new file mode 100644 index 0000000..d6e29fe --- /dev/null +++ b/app/terminal/wandbox/cpp/_stacktrace.cpp @@ -0,0 +1,39 @@ +#define BOOST_STACKTRACE_USE_ADDR2LINE +#include +#include +#include +void signal_handler(int signum) { + signal(signum, SIG_DFL); + switch(signum) { + case SIGILL: + std::cerr << "#!my_code_signal:Illegal instruction" << std::endl; + break; + case SIGABRT: + std::cerr << "#!my_code_signal:Aborted" << std::endl; + break; + case SIGBUS: + std::cerr << "#!my_code_signal:Bus error" << std::endl; + break; + case SIGFPE: + std::cerr << "#!my_code_signal:Floating point exception" << std::endl; + break; + case SIGSEGV: + std::cerr << "#!my_code_signal:Segmentation fault" << std::endl; + break; + default: + std::cerr << "#!my_code_signal:Signal " << signum << " received" << std::endl; + break; + } + std::cerr << "#!my_code_stacktrace:" << 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; diff --git a/app/terminal/wandbox/runtime.tsx b/app/terminal/wandbox/runtime.tsx index aadbd6b..3199762 100644 --- a/app/terminal/wandbox/runtime.tsx +++ b/app/terminal/wandbox/runtime.tsx @@ -12,8 +12,9 @@ import { compilerInfoFetcher, SelectedCompiler } from "./api"; import { cppRunFiles, selectCppCompiler } from "./cpp"; import { RuntimeContext, RuntimeLang } from "../runtime"; import { ReplOutput } from "../repl"; +import { rustRunFiles, selectRustCompiler } from "./rust"; -type WandboxLang = "cpp"; +type WandboxLang = "cpp" | "rust"; interface IWandboxContext { ready: boolean; @@ -46,20 +47,14 @@ export function WandboxProvider({ children }: { children: ReactNode }) { } return { cpp: selectCppCompiler(compilerList), + rust: selectRustCompiler(compilerList), }; }, [compilerList]); const getCommandlineStrWithLang = useCallback( (lang: WandboxLang) => { if (selectedCompiler) { - lang satisfies RuntimeLang; - switch (lang) { - case "cpp": - return selectedCompiler.cpp.getCommandlineStr; - default: - lang satisfies never; - throw new Error(`unsupported language: ${lang}`); - } + return selectedCompiler[lang].getCommandlineStr; } else { return () => ""; } @@ -79,6 +74,8 @@ export function WandboxProvider({ children }: { children: ReactNode }) { switch (lang) { case "cpp": return cppRunFiles(selectedCompiler.cpp, files, filenames); + case "rust": + return rustRunFiles(selectedCompiler.rust, files, filenames); default: lang satisfies never; throw new Error(`unsupported language: ${lang}`); @@ -101,6 +98,8 @@ export function WandboxProvider({ children }: { children: ReactNode }) { } export function useWandbox(lang: WandboxLang): RuntimeContext { + lang satisfies RuntimeLang; + const context = useContext(WandboxContext); if (!context) { throw new Error("useWandbox must be used within a WandboxProvider"); diff --git a/app/terminal/wandbox/rust.ts b/app/terminal/wandbox/rust.ts new file mode 100644 index 0000000..01f24ee --- /dev/null +++ b/app/terminal/wandbox/rust.ts @@ -0,0 +1,125 @@ +import { ReplOutput } from "../repl"; +import { compileAndRun, CompilerInfo, SelectedCompiler } from "./api"; + +import prog_rs from "./rust/prog.rs?raw"; + +export function selectRustCompiler( + compilerList: CompilerInfo[] +): SelectedCompiler { + // 最初のRustコンパイラを使う + const selectedCompiler = compilerList.find((c) => c.language === "Rust"); + if (!selectedCompiler) { + throw new Error("compiler not found"); + } + + return { + compilerName: selectedCompiler.name, + compilerOptions: [], + compilerOptionsRaw: ["-Cdebuginfo=1"], + getCommandlineStr: (filenames: string[]) => + [ + "rustc", + "-Cdebuginfo=1", + filenames[0], + "&&", + "./" + filenames[0].replace(/\.rs$/, ""), + ].join(" "), + }; +} + +export async function rustRunFiles( + options: SelectedCompiler, + files: Record, + filenames: string[] +): Promise { + const mainModule = filenames[0].replace(/\.rs$/, ""); + const result = await compileAndRun({ + ...options, + // メインファイルでmod宣言したものをこちらに移す + code: + [...(files[filenames[0]]?.matchAll(/mod\s+\w+\s*;/g) ?? [])].reduce( + (prev, m) => prev + `${m}\n`, + "" + ) + prog_rs.replaceAll("__user_main_module__", mainModule), + codes: { + ...files, + // メインファイルのみ: + // main()を強制的にpubに書き換え、 + // mod foo; を use super::foo; に書き換える + [filenames[0]]: files[filenames[0]] + ?.replace(/(?:pub\s+)?(fn\s+main\s*\()/g, "pub $1") + .replaceAll(/mod\s+(\w+)\s*;/g, "use super::$1;"), + }, + }); + + let outputs = result.output; + + // Find stack trace in the output + const panicIndex = outputs.findIndex( + (line) => line.type === "stderr" && line.message === "#!my_code_panic_hook:" + ); + + if (panicIndex >= 0) { + const traceIndex = + panicIndex + + outputs + .slice(panicIndex) + .findLastIndex( + (line) => + line.type === "stderr" && line.message === "stack backtrace:" + ); + const otherOutputs = outputs.slice(0, panicIndex); + const traceOutputs: ReplOutput[] = [ + { + type: "trace", + message: "Stack trace (filtered):", + }, + ]; + for (const line of outputs.slice(panicIndex + 1, traceIndex)) { + if (line.type === "stderr") { + otherOutputs.push({ + type: "error", + message: line.message, + }); + } else { + otherOutputs.push(line); + } + } + for (let i = traceIndex + 1; i < outputs.length; i++) { + const line = outputs.at(i)!; + const nextLine = outputs.at(i + 1); + if (line.type === "stderr") { + // ユーザーのソースコードだけを対象にする + if ( + /^\s*\d+:/.test(line.message) && + nextLine && + /^\s*at .\//.test(nextLine.message) && + !/^\s*at .\/prog.rs/.test(nextLine.message) + ) { + traceOutputs.push({ + type: "trace", + message: line.message.replace("prog::", ""), + }); + traceOutputs.push({ + type: "trace", + message: nextLine.message, + }); + i++; // skip next line + } + } else { + otherOutputs.push(line); + } + } + + outputs = [...otherOutputs, ...traceOutputs]; + } + + if (result.status !== "0") { + outputs.push({ + type: "system", + message: `ステータス ${result.status} で異常終了しました`, + }); + } + + return outputs; +} diff --git a/app/terminal/wandbox/rust/prog.rs b/app/terminal/wandbox/rust/prog.rs new file mode 100644 index 0000000..1e2ba78 --- /dev/null +++ b/app/terminal/wandbox/rust/prog.rs @@ -0,0 +1,13 @@ +mod __user_main_module__; + +fn main() { + std::env::set_var("RUST_BACKTRACE", "1"); + let default_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + eprintln!("#!my_code_panic_hook:"); + // デフォルトのフックを呼び出しpanicメッセージとスタックトレース表示 + default_hook(info); + })); + + __user_main_module__::main(); +} diff --git a/app/terminal/worker/pyodide.worker.ts b/app/terminal/worker/pyodide.worker.ts index 55c4749..1594de3 100644 --- a/app/terminal/worker/pyodide.worker.ts +++ b/app/terminal/worker/pyodide.worker.ts @@ -8,7 +8,11 @@ import type { PyCallable } from "pyodide/ffi"; import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime"; import type { ReplOutput } from "../repl"; +import execfile_py from "./pyodide/execfile.py?raw"; +import check_syntax_py from "./pyodide/check_syntax.py?raw"; + const PYODIDE_CDN = `https://cdn.jsdelivr.net/pyodide/v${pyodideVersion}/full/`; +const HOME = `/home/pyodide/`; let pyodide: PyodideInterface; let pyodideOutput: ReplOutput[] = []; @@ -134,7 +138,7 @@ async function runFile({ id, payload }: WorkerRequest["runFile"]) { } } - const pyExecFile = pyodide.runPython(EXECFILE_CODE) as PyCallable; + const pyExecFile = pyodide.runPython(execfile_py) as PyCallable; pyExecFile(`${HOME}/${name}`); } catch (e: unknown) { console.log(e); @@ -198,7 +202,7 @@ async function checkSyntax({ id, payload }: WorkerRequest["checkSyntax"]) { try { // Pythonのコードを実行して結果を受け取る - const status = (pyodide.runPython(CHECK_SYNTAX_CODE) as PyCallable)(code); + const status = (pyodide.runPython(check_syntax_py) as PyCallable)(code); self.postMessage({ id, payload: { status }, @@ -238,39 +242,3 @@ self.onmessage = async (event: MessageEvent) => { return; } }; - -// Python側で実行する構文チェックのコード -// codeop.compile_commandは、コードが不完全な場合はNoneを返します。 -const CHECK_SYNTAX_CODE = ` -def __check_syntax(code): - import codeop - - compiler = codeop.compile_command - try: - # compile_commandは、コードが完結していればコンパイルオブジェクトを、 - # 不完全(まだ続きがある)であればNoneを返す - if compiler(code) is not None: - return "complete" - else: - return "incomplete" - except (SyntaxError, ValueError, OverflowError): - # 明らかな構文エラーの場合 - return "invalid" - -__check_syntax -`; - -const HOME = `/home/pyodide/`; - -// https://stackoverflow.com/questions/436198/what-alternative-is-there-to-execfile-in-python-3-how-to-include-a-python-fil -const EXECFILE_CODE = ` -def __execfile(filepath): - with open(filepath, 'rb') as file: - exec_globals = { - "__file__": filepath, - "__name__": "__main__", - } - exec(compile(file.read(), filepath, 'exec'), exec_globals) - -__execfile -`; diff --git a/app/terminal/worker/pyodide/check_syntax.py b/app/terminal/worker/pyodide/check_syntax.py new file mode 100644 index 0000000..c7dc846 --- /dev/null +++ b/app/terminal/worker/pyodide/check_syntax.py @@ -0,0 +1,20 @@ +def __check_syntax(code): + # Python側で実行する構文チェックのコード + # codeop.compile_commandは、コードが不完全な場合はNoneを返します。 + + import codeop + + compiler = codeop.compile_command + try: + # compile_commandは、コードが完結していればコンパイルオブジェクトを、 + # 不完全(まだ続きがある)であればNoneを返す + if compiler(code) is not None: + return "complete" + else: + return "incomplete" + except (SyntaxError, ValueError, OverflowError): + # 明らかな構文エラーの場合 + return "invalid" + + +__check_syntax \ No newline at end of file diff --git a/app/terminal/worker/pyodide/execfile.py b/app/terminal/worker/pyodide/execfile.py new file mode 100644 index 0000000..972e23d --- /dev/null +++ b/app/terminal/worker/pyodide/execfile.py @@ -0,0 +1,11 @@ +def __execfile(filepath): + # https://stackoverflow.com/questions/436198/what-alternative-is-there-to-execfile-in-python-3-how-to-include-a-python-fil + with open(filepath, "rb") as file: + exec_globals = { + "__file__": filepath, + "__name__": "__main__", + } + exec(compile(file.read(), filepath, "exec"), exec_globals) + + +__execfile \ No newline at end of file diff --git a/app/terminal/worker/ruby.worker.ts b/app/terminal/worker/ruby.worker.ts index 9fc396c..0bdb918 100644 --- a/app/terminal/worker/ruby.worker.ts +++ b/app/terminal/worker/ruby.worker.ts @@ -6,6 +6,8 @@ import type { RubyVM } from "@ruby/wasm-wasi/dist/vm"; import type { MessageType, WorkerRequest, WorkerResponse } from "./runtime"; import type { ReplOutput } from "../repl"; +import init_rb from "./ruby/init.rb?raw"; + let rubyVM: RubyVM | null = null; let rubyOutput: ReplOutput[] = []; let stdoutBuffer = ""; @@ -37,20 +39,8 @@ async function init({ id }: WorkerRequest["init"]) { const { vm } = await DefaultRubyVM(rubyModule); rubyVM = vm; - rubyVM.eval(` -$stdout = Object.new.tap do |obj| - def obj.write(str) - require "js" - JS.global[:stdout].write(str) - end -end -$stderr = Object.new.tap do |obj| - def obj.write(str) - require "js" - JS.global[:stderr].write(str) - end -end -`); + rubyVM.eval(init_rb); + self.postMessage({ id, payload: { capabilities: { interrupt: "restart" } }, diff --git a/app/terminal/worker/ruby/init.rb b/app/terminal/worker/ruby/init.rb new file mode 100644 index 0000000..2a55820 --- /dev/null +++ b/app/terminal/worker/ruby/init.rb @@ -0,0 +1,12 @@ +$stdout = Object.new.tap do |obj| + def obj.write(str) + require "js" + JS.global[:stdout].write(str) + end +end +$stderr = Object.new.tap do |obj| + def obj.write(str) + require "js" + JS.global[:stderr].write(str) + end +end \ No newline at end of file diff --git a/declatations.d.ts b/declatations.d.ts index 2f49eec..d3b1330 100644 --- a/declatations.d.ts +++ b/declatations.d.ts @@ -1,3 +1,8 @@ declare module "ace-builds/src-min-noconflict/*"; declare module "prismjs/components/*"; declare module "mocha/mocha.js"; + +declare module "*?raw" { + const contents: string; + export = contents; +} diff --git a/next.config.ts b/next.config.ts index d694b51..af379b9 100644 --- a/next.config.ts +++ b/next.config.ts @@ -33,11 +33,28 @@ const nextConfig: NextConfig = { webpack: (config, { isServer }) => { if (!isServer) { config.resolve.fallback = { - "child_process": false, + child_process: false, "node:child_process": false, ...config.resolve.fallback, }; } + // import hoge from "./file?raw" でfileの中身を文字列としてインポート + for (const rule of config.module.rules) { + if (rule.resourceQuery instanceof RegExp) { + // skip + } else if (Array.isArray(rule.resourceQuery?.not)) { + rule.resourceQuery.not.push(/raw/); + } else { + if (rule.resourceQuery) { + console.warn("resourceQuery already exists:", rule.resourceQuery); + } + rule.resourceQuery = { not: /raw/ }; + } + } + config.module.rules.push({ + resourceQuery: /raw/, + type: "asset/source", + }); return config; }, }; diff --git a/package-lock.json b/package-lock.json index c758127..9f84d36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,9 @@ "react-dom": "^19", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", + "remark-cjk-friendly": "^1.2.3", "remark-gfm": "^4.0.1", + "remark-remove-comments": "^1.1.1", "swr": "^2.3.6", "typescript": "^5.9.3", "zod": "^4.0.17" @@ -16146,6 +16148,12 @@ "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==", "license": "CC0-1.0" }, + "node_modules/html-comment-regex": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", + "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "license": "MIT" + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -17805,6 +17813,50 @@ "micromark-util-types": "^2.0.0" } }, + "node_modules/micromark-extension-cjk-friendly": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/micromark-extension-cjk-friendly/-/micromark-extension-cjk-friendly-1.2.3.tgz", + "integrity": "sha512-gRzVLUdjXBLX6zNPSnHGDoo+ZTp5zy+MZm0g3sv+3chPXY7l9gW+DnrcHcZh/jiPR6MjPKO4AEJNp4Aw6V9z5Q==", + "license": "MIT", + "dependencies": { + "devlop": "^1.1.0", + "micromark-extension-cjk-friendly-util": "2.1.1", + "micromark-util-chunked": "^2.0.1", + "micromark-util-resolve-all": "^2.0.1", + "micromark-util-symbol": "^2.0.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "micromark": "^4.0.0", + "micromark-util-types": "^2.0.0" + }, + "peerDependenciesMeta": { + "micromark-util-types": { + "optional": true + } + } + }, + "node_modules/micromark-extension-cjk-friendly-util": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-cjk-friendly-util/-/micromark-extension-cjk-friendly-util-2.1.1.tgz", + "integrity": "sha512-egs6+12JU2yutskHY55FyR48ZiEcFOJFyk9rsiyIhcJ6IvWB6ABBqVrBw8IobqJTDZ/wdSr9eoXDPb5S2nW1bg==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.0", + "micromark-util-character": "^2.1.1", + "micromark-util-symbol": "^2.0.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependenciesMeta": { + "micromark-util-types": { + "optional": true + } + } + }, "node_modules/micromark-extension-gfm": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", @@ -20246,6 +20298,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark-cjk-friendly": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/remark-cjk-friendly/-/remark-cjk-friendly-1.2.3.tgz", + "integrity": "sha512-UvAgxwlNk+l9Oqgl/9MWK2eWRS7zgBW/nXX9AthV7nd/3lNejF138E7Xbmk9Zs4WjTJGs721r7fAEc7tNFoH7g==", + "license": "MIT", + "dependencies": { + "micromark-extension-cjk-friendly": "1.2.3" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@types/mdast": "^4.0.0", + "unified": "^11.0.0" + }, + "peerDependenciesMeta": { + "@types/mdast": { + "optional": true + } + } + }, "node_modules/remark-gfm": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", @@ -20297,6 +20370,17 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/remark-remove-comments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/remark-remove-comments/-/remark-remove-comments-1.1.1.tgz", + "integrity": "sha512-Z0OONcdhf3I7lKJcR3TRCKqpgGnhugtn/xBWdPZuEpLm67y5hf7Z0CI4p8j6zq1uX2koyUxo2O6y2MNZyPA0JA==", + "license": "MIT", + "dependencies": { + "html-comment-regex": "^1.1.2", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.3" + } + }, "node_modules/remark-stringify": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", diff --git a/package.json b/package.json index 06dfe3b..71a9bc3 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,9 @@ "react-dom": "^19", "react-markdown": "^10.1.0", "react-syntax-highlighter": "^16.1.0", + "remark-cjk-friendly": "^1.2.3", "remark-gfm": "^4.0.1", + "remark-remove-comments": "^1.1.1", "swr": "^2.3.6", "typescript": "^5.9.3", "zod": "^4.0.17" diff --git a/public/docs/cpp-10.md b/public/docs/cpp-10.md index 50887e9..8e4fa58 100644 --- a/public/docs/cpp-10.md +++ b/public/docs/cpp-10.md @@ -1,301 +1,224 @@ -# 第10章: 標準テンプレートライブラリ (STL) ②:アルゴリズムとラムダ式 +# 第10章: テンプレートによる汎用プログラミング -ようこそ、C++チュートリアルの第10章へ!前の章ではSTLのコンテナについて学び、様々なデータを効率的に格納する方法を見てきました。しかし、データを格納するだけではプログラムは完成しません。そのデータを並べ替えたり、検索したり、特定の処理を施したりといった「操作」が必要です。 +これまでの章では、`int`や`double`、あるいは自作の`Car`クラスのように、特定の型に対して処理を行う関数やクラスを作成してきました。しかし、プログラムが複雑になるにつれて、「型は違うけれど、行いたい処理は全く同じ」という状況が頻繁に発生します。例えば、2つの値の大きい方を返す`max`という関数を考えてみましょう。 -この章では、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 +int max_int(int a, int b) { + return (a > b) ? a : b; } -``` -```cpp-exec:iterator_example.cpp -Numbers: 0 1 2 3 4 -Numbers (range-based for): 0 1 2 3 4 +double max_double(double a, double b) { + return (a > b) ? a : b; +} ``` -このように、イテレータはコンテナの種類を問わず、統一的な方法で要素にアクセスする仕組みを提供します。これが、アルゴリズムが様々なコンテナに対して汎用的に機能する理由です。 - -## 便利なアルゴリズム +このように、型ごとに同じロジックの関数をいくつも用意するのは非効率的ですし、バグの温床にもなります。 -C++の標準ライブラリには、`` ヘッダと `` ヘッダに数多くの便利なアルゴリズムが用意されています。ここでは、特によく使われるものをいくつか紹介します。 +この問題を解決するのが**テンプレート**です。テンプレートを使うと、具体的な型を "仮引数" のように扱い、様々な型に対応できる関数やクラスの「設計図」を作ることができます。このような、型に依存しないプログラミングスタイルを**ジェネリックプログラミング(汎用プログラミング)**と呼びます。 -### `std::sort`: 要素を並べ替える +## 関数テンプレート: intでもdoubleでもstringでも動く関数を作る -名前の通り、指定された範囲の要素をソートします。デフォルトでは昇順に並べ替えます。 +関数テンプレートを使うと、先ほどの`max`関数の問題をエレガントに解決できます。 -```cpp:sort_example.cpp +```cpp:function_template_intro.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; +// Tという名前で型を仮引数として受け取るテンプレートを宣言 +template +T max_value(T a, T b) { + return (a > b) ? a : b; } -``` - -```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; - } + // int型でmax_valueを呼び出す + std::cout << "max(10, 20) = " << max_value(10, 20) << std::endl; - value_to_find = 99; - it = std::find(numbers.begin(), numbers.end(), value_to_find); + // double型でmax_valueを呼び出す + std::cout << "max(3.14, 1.41) = " << max_value(3.14, 1.41) << std::endl; - if (it != numbers.end()) { - std::cout << "Found " << *it << std::endl; - } else { - std::cout << value_to_find << " not found." << 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:find_example.cpp -Found 30 at index 2 -99 not found. +```cpp-exec:function_template_intro.cpp +max(10, 20) = 20 +max(3.14, 1.41) = 3.14 +max("world", "hello") = world ``` -### `std::for_each`: 各要素に処理を適用する - -指定された範囲の全ての要素に対して、特定の関数(処理)を適用します。ループを書くよりも意図が明確になる場合があります。 - -```cpp -// 3番目の引数に関数を渡す -std::for_each(numbers.begin(), numbers.end(), print_function); -``` +### テンプレートの仕組み -ここで「特定の処理」をその場で手軽に記述する方法が**ラムダ式**です。 +`template `という部分が、この関数がテンプレートであることを示しています。 -## ラムダ式:その場で書ける無名関数 + * **`template <...>`**: テンプレートの宣言を開始します。 + * **`typename T`**: `T`という名前の「型引数」を定義しています。`typename`の代わりに`class`と書くこともできますが、意味は同じです。`T`は、このテンプレートが実際に使われるときに具体的な型(`int`や`double`など)に置き換えられます。 -ラムダ式(Lambda Expression)は、C++11から導入された非常に強力な機能です。一言で言えば、「**その場で定義して使える名前のない小さな関数**」です。これにより、アルゴリズムに渡すためだけの短い関数をわざわざ定義する必要がなくなり、コードが非常に簡潔になります。 - -ラムダ式の基本的な構文は以下の通りです。 +`main`関数で`max_value(10, 20)`のように呼び出すと、コンパイラは引数の型が`int`であることから、`T`を`int`だと自動的に判断します(これを**テンプレート引数推論**と呼びます)。そして、内部的に以下のような`int`版の関数を生成してくれるのです。 ```cpp -[キャプチャ](引数リスト) -> 戻り値の型 { 処理本体 } +// コンパイラが内部的に生成するコードのイメージ +int max_value(int a, int b) { + return (a > b) ? a : b; +} ``` - * `[]` **キャプチャ句**: ラムダ式の外にある変数を取り込んで、式の中で使えるようにします。 - * `[]`: 何もキャプチャしない。 - * `[=]`: 外の変数を全て値渡し(コピー)でキャプチャする。 - * `[&]`: 外の変数を全て参照渡しでキャプチャする。 - * `[x, &y]`: 変数 `x` は値渡し、変数 `y` は参照渡しでキャプチャする。 - * `()` **引数リスト**:通常の関数と同じ引数を取ることができます。 - * `-> 戻り値の型`: 戻り値の型を指定します。多くの場合、コンパイラが推論できるため省略可能です。 - * `{}` **処理本体**: 関数の処理内容を記述します。 +同様に、`double`や`std::string`で呼び出されれば、それぞれの型に対応したバージョンの関数が自動的に生成されます。これにより、私たちは一つの「設計図」を書くだけで、様々な型に対応できるのです。 + +## クラステンプレート: 様々な型のデータを格納できるクラスを作る -`std::for_each` とラムダ式を組み合わせた例を見てみましょう。 +テンプレートの力は、クラスにも適用できます。これにより、様々な型のデータを格納できる汎用的なクラス(コンテナなど)を作成できます。例えば、「2つの値をペアで保持する」クラスを考えてみましょう。 -```cpp:for_each_lambda_example.cpp +```cpp:class_template_intro.cpp #include -#include -#include +#include -int main() { - std::vector numbers = {1, 2, 3, 4, 5}; +// 2つの型 T1, T2 を引数に取るクラステンプレート +template +class Pair { +public: + T1 first; + T2 second; + + // コンストラクタ + Pair(T1 f, T2 s) : first(f), second(s) {} - // 各要素を2倍して表示する - std::cout << "Doubled numbers: "; - std::for_each(numbers.begin(), numbers.end(), [](int n) { - std::cout << n * 2 << " "; - }); - std::cout << std::endl; + void print() { + std::cout << "(" << first << ", " << second << ")" << std::endl; + } +}; - // 外部の変数をキャプチャする例 - int sum = 0; - // `&sum` で sum を参照キャプチャし、ラムダ式内で変更できるようにする - std::for_each(numbers.begin(), numbers.end(), [&sum](int n) { - sum += n; - }); +int main() { + // T1=int, T2=std::string としてPairクラスのオブジェクトを生成 + Pair p1(1, "apple"); + p1.print(); - std::cout << "Sum: " << sum << std::endl; + // T1=std::string, T2=double としてPairクラスのオブジェクトを生成 + Pair p2("pi", 3.14159); + p2.print(); + + // 違う型のPair同士は当然、別の型として扱われる + // p1 = p2; // これはコンパイルエラーになる return 0; } ``` -```cpp-exec:for_each_lambda_example.cpp -Doubled numbers: 2 4 6 8 10 -Sum: 15 +```cpp-exec:class_template_intro.cpp +(1, apple) +(pi, 3.14159) ``` -このコードは、`for_each` の3番目の引数に直接処理を書き込んでいます。非常に直感的で読みやすいと思いませんか? +### クラステンプレートの仕組み -ラムダ式は、特に `std::sort` のソート順をカスタマイズする際に真価を発揮します。例えば、数値を降順にソートしたい場合、比較ルールをラムダ式で与えることができます。 +関数テンプレートと基本的な考え方は同じですが、いくつか重要な違いがあります。 -```cpp:lambda_sort_example.cpp -#include -#include -#include +1. **明示的な型指定**: + 関数テンプレートではコンパイラが型を推論してくれましたが、クラステンプレートの場合は、オブジェクトを生成する際に`Pair`のように、開発者が明示的に型を指定する必要があります。 -int main() { - std::vector numbers = {5, 2, 8, 1, 9}; +2. **インスタンス化**: + `Pair`のように具体的な型を指定してオブジェクトを作ることを、テンプレートの**インスタンス化**と呼びます。コンパイラは、この指定に基づいて`T1`を`int`に、`T2`を`std::string`に置き換えた、以下のような新しいクラスを内部的に生成します。 - // 比較関数としてラムダ式を渡す - // a > b であれば true を返すことで降順ソートになる - std::sort(numbers.begin(), numbers.end(), [](int a, int b) { - return a > b; - }); + ```cpp + // コンパイラが内部的に生成するクラスのイメージ + class Pair_int_string { // クラス名は実際には異なります + public: + int first; + std::string second; - std::cout << "Sorted in descending order: "; - for (int num : numbers) { - std::cout << num << " "; - } - std::cout << std::endl; + Pair_int_string(int f, std::string s) : first(f), second(s) {} - return 0; -} -``` + void print() { + std::cout << "(" << first << ", " << second << ")" << std::endl; + } + }; + ``` -```cpp-exec:lambda_sort_example.cpp -Sorted in descending order: 9 8 5 2 1 -``` + `Pair`と`Pair`は、コンパイルされると全く別のクラスとして扱われることに注意してください。 -## この章のまとめ +クラステンプレートは、C++の強力なライブラリである**STL (Standard Template Library)**の根幹をなす技術です。次章で学ぶ`vector`や`map`といった便利なコンテナは、すべてクラステンプレートで実装されています。 -この章では、STLのアルゴリズムとラムダ式について学びました。 - - * **イテレータ**は、コンテナの要素を指し示すオブジェクトであり、アルゴリズムとコンテナの間のインターフェースとして機能します。 - * `` ヘッダには、**`std::sort`** (ソート)、**`std::find`** (検索)、**`std::for_each`** (繰り返し処理) といった、汎用的で強力なアルゴリズムが多数用意されています。 - * **ラムダ式**は、その場で定義できる無名関数であり、アルゴリズムに渡す処理を簡潔かつ直感的に記述することができます。 - * **キャプチャ**機能を使うことで、ラムダ式の外にある変数を取り込んで処理に利用できます。 - -コンテナ、イテレータ、アルゴリズム、そしてラムダ式。これらを組み合わせることで、C++におけるデータ処理は、他の多くの言語に引けを取らない、あるいはそれ以上に表現力豊かで効率的なものになります。 +## この章のまとめ -### 練習問題1: 文字列の長さでソート + * **ジェネリックプログラミング**は、特定の型に縛られない、汎用的なコードを書くための手法です。 + * **テンプレート**は、C++でジェネリックプログラミングを実現するための機能です。 + * **関数テンプレート**を使うと、様々な型の引数に対して同じ処理を行う関数を定義できます。呼び出し時には、コンパイラが**テンプレート引数推論**によって型を自動的に決定します。 + * **クラステンプレート**を使うと、様々な型を扱える汎用的なクラスを定義できます。オブジェクトを生成する際には、`< >`内に具体的な型を**明示的に指定**してインスタンス化する必要があります。 -`std::vector` を用意し、格納されている文字列を、文字数が短い順にソートして、結果を出力するプログラムを作成してください。`std::sort` とラムダ式を使用してください。 +テンプレートを使いこなすことで、コードの再利用性が劇的に向上し、より柔軟で堅牢なプログラムを記述できるようになります。 -**ヒント**: ラムダ式は2つの文字列を引数に取り、1つ目の文字列の長さが2つ目の文字列の長さより短い場合に `true` を返すように実装します。 +### 練習問題1: 汎用的なprint関数 +任意の型の配列(ここでは`std::vector`を使いましょう)を受け取り、その要素をすべて画面に出力する関数テンプレート`print_elements`を作成してください。 ```cpp:practice10_1.cpp #include #include -#include +#include + +// ここに関数テンプレート print_elements を実装してください + int main() { - std::vector words = {"apple", "banana", "kiwi", "cherry", "fig", "grape"}; + 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:practice10_1.cpp -fig kiwi grape apple banana cherry +Integers: 1 2 3 4 5 +Strings: C++ is powerful ``` -### 練習問題2: 条件に合う要素のカウント +### 練習問題2: 汎用的なスタッククラス -`std::vector` に整数をいくつか格納します。その後、ラムダ式と `std::for_each`(または他のアルゴリズム)を使って、以下の2つの条件を満たす要素がそれぞれいくつあるかを数えて出力してください。 +後入れ先出し(LIFO)のデータ構造であるスタックを、クラステンプレート`SimpleStack`として実装してください。以下のメンバ関数を持つようにしてください。 -1. 正の偶数である要素の数 -2. 負の奇数である要素の数 + * `void push(T item)`: スタックに要素を追加する + * `T pop()`: スタックの先頭から要素を取り出す + * `bool is_empty()`: スタックが空かどうかを返す -**ヒント**: カウント用の変数を2つ用意し、ラムダ式のキャプチャ句で参照キャプチャ (`[&]`) して、式の中でインクリメントします。 +`std::vector`を内部のデータ格納場所として利用して構いません。`int`型と`char`型で動作を確認してください。 ```cpp:practice10_2.cpp #include #include -#include +#include -int main() { - std::vector numbers = {3, -1, 4, -5, 6, -7, 8, 0, -2}; +// ここにクラステンプレート 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:practice10_2.cpp -Positive even count: 3 -Negative odd count: 3 +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-11.md b/public/docs/cpp-11.md index 909494c..2f97046 100644 --- a/public/docs/cpp-11.md +++ b/public/docs/cpp-11.md @@ -1,396 +1,252 @@ -# 第11章: モダンC++の流儀:RAIIとスマートポインタ +# 第11章: 標準テンプレートライブラリ (STL) ①:コンテナ -これまでの章で、`new` と `delete` を使った動的なメモリ管理を学びました。しかし、これらの手動管理は `delete` の呼び忘れによるメモリリークや、複雑なコードでのリソース管理の煩雑さを引き起こす原因となりがちです。 +C++の大きな魅力の一つに、**標準テンプレートライブラリ (Standard Template Library, STL)** の存在があります。STLは、よく使われるデータ構造やアルゴリズムを、汎用的かつ効率的に実装したライブラリ群です。この章では、STLの心臓部である**コンテナ**に焦点を当て、データの格納と管理を劇的に楽にする方法を学びます。 -C++11以降の「モダンC++」では、こうした問題を解決するための洗練された仕組みが導入されました。この章では、エラーハンドリングのための**例外処理**、リソース管理の基本思想である **RAIIイディオム**、そしてそれを具現化する**スマートポインタ** (`std::unique_ptr`, `std::shared_ptr`) について学び、より安全で堅牢なコードを書くための流儀を身につけます。 +## STLの全体像: コンテナ、アルゴリズム、イテレータ -## 例外処理: try, catch を使ったエラーハンドリング +STLは、主に3つの要素から構成されています。 -プログラムでは、ファイルの読み込み失敗やメモリ確保の失敗など、予期せぬエラーが発生することがあります。C++では、このような状況を処理するために**例外 (Exception)** という仕組みが用意されています。 +1. **コンテナ (Containers)**: データを格納するためのデータ構造です。`vector`(可変長配列)や`map`(連想配列)など、様々な種類があります。 +2. **アルゴリズム (Algorithms)**: ソート、検索、変換など、コンテナ上のデータに対して操作を行う関数群です。 +3. **イテレータ (Iterators)**: コンテナの要素を指し示し、アルゴリズムがコンテナの種類に依存せずに各要素にアクセスするための統一的なインターフェースを提供します。ポインタを一般化したようなものです。 -例外処理は、以下の3つのキーワードで構成されます。 +これら3つが連携することで、C++プログラマは効率的で再利用性の高いコードを素早く書くことができます。この章では「コンテナ」を、次の章では「アルゴリズム」と、それらをつなぐ「イテレータ」の応用を詳しく見ていきます。 - * `throw`: 例外的な状況が発生したことを知らせるために、例外オブジェクトを「投げる」。 - * `try`: 例外が発生する可能性のあるコードブロックを囲む。 - * `catch`: `try` ブロック内で投げられた例外を「捕まえて」処理する。 +## std::vector: 最もよく使う可変長配列 -基本的な構文を見てみましょう。 +`std::vector`は、最も基本的で最もよく使われるコンテナです。他の言語でいうところの「リスト」や「動的配列」に相当し、要素を連続したメモリ領域に格納します。 -```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; -} + * **動的なサイズ**: 必要に応じて自動的にサイズが拡張されます。 + * **高速なランダムアクセス**: インデックス(添字)を使って `[i]` の形式で要素に高速にアクセスできます (`O(1)`)。 + * **末尾への高速な追加・削除**: `push_back()` や `pop_back()` を使った末尾への操作は非常に高速です。 -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::vector`を使うには、``ヘッダをインクルードする必要があります。 - std::cout << "Program finished." << std::endl; - return 0; -} -``` +```cpp:vector_example.cpp +#include +#include +#include -```cpp-exec:exception_basic.cpp -Trying to divide... -Caught an exception: Division by zero! -Program finished. -``` +int main() { + // string型の要素を格納するvectorを作成 + std::vector names; -`divide` 関数内で `b` が0だった場合に `throw` が実行され、関数の実行は即座に中断されます。制御は呼び出し元の `catch` ブロックに移り、そこでエラー処理が行われます。これにより、エラーが発生してもプログラム全体がクラッシュすることなく、安全に処理を続行できます。 + // push_backで末尾に要素を追加 + names.push_back("Alice"); + names.push_back("Bob"); + names.push_back("Charlie"); -### 例外とリソースリーク + // インデックスを使った要素へのアクセス + std::cout << "Index 1: " << names[1] << std::endl; -ここで、`new` と `delete` を使った手動のメモリ管理と例外処理が組み合わさると、問題が発生します。 + // 範囲for文 (range-based for loop) を使った全要素の走査 + std::cout << "\nAll names:" << std::endl; + for (const std::string& name : names) { + std::cout << "- " << name << std::endl; + } -```cpp:raw_pointer_problem.cpp -#include -#include + // size()で現在の要素数を取得 + std::cout << "\nCurrent size: " << names.size() << std::endl; -void process_data() { - int* data = new int[100]; // リソース確保 - std::cout << "Data allocated." << std::endl; + // pop_backで末尾の要素を削除 + names.pop_back(); // "Charlie"を削除 - // 何らかの処理... - bool something_wrong = true; - if (something_wrong) { - throw std::runtime_error("Something went wrong during processing!"); + 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; - // 例外が投げられると、この行には到達しない - 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! -``` +```cpp-exec:vector_example.cpp +Index 1: Bob + +All names: +- Alice +- Bob +- Charlie -この例では、`process_data` 関数内で `throw` が実行されると、関数の実行が中断され `catch` ブロックにジャンプします。その結果、`delete[] data;` の行が実行されず、確保されたメモリが解放されない**メモリリーク**が発生します。 +Current size: 3 -この問題を解決するのが、C++の最も重要な設計思想の一つである **RAII** です。 +After pop_back: +- Alice +- Bob +Current size: 2 +``` -## RAIIイディオム +`std::vector`は、どのコンテナを使うか迷ったら、まず最初に検討すべきデフォルトの選択肢と言えるほど万能です。 -**RAII (Resource Acquisition Is Initialization)** は、「リソースの確保は、オブジェクトの初期化時に行い、リソースの解放は、オブジェクトの破棄時に行う」という設計パターンです。日本語では「リソース取得は初期化である」と訳されます。 +## std::map: キーと値のペアを管理する連想配列 -C++では、オブジェクトがそのスコープ(変数が宣言された `{}` の範囲)を抜けるときに、そのオブジェクトの**デストラクタ**が自動的に呼び出されます。この仕組みは、関数が正常に終了した場合だけでなく、**例外が投げられてスコープを抜ける場合でも保証されています**。 +`std::map`は、キー (key) と値 (value) のペアを管理するためのコンテナです。他の言語の「辞書 (dictionary)」や「ハッシュマップ (hash map)」に似ています。キーを使って値を高速に検索、追加、削除できます。 -RAIIはこの性質を利用して、リソースの解放処理をデストラクタに記述することで、リソースの解放を自動化し、`delete` の呼び忘れや例外発生時のリソースリークを防ぎます。 +**主な特徴**: -簡単なRAIIクラスの例を見てみましょう。 + * **キーによる高速な検索**: キーに基づいて要素が自動的にソートされて格納されるため、検索、挿入、削除が高速です (`O(log n)`)。 + * **一意なキー**: `std::map`内のキーは重複しません。同じキーで値を挿入しようとすると、既存の値が上書きされます。 -```cpp:raii_concept.cpp +`std::map`を使うには、``ヘッダをインクルードする必要があります。 + +```cpp:map_example.cpp #include +#include +#include -class ResourceWrapper { -private: - int* m_data; +int main() { + // キーがstring型、値がint型のmapを作成 + std::map scores; + + // []演算子で要素を追加・更新 + scores["Alice"] = 95; + scores["Bob"] = 88; + scores["Charlie"] = 76; -public: - // コンストラクタでリソースを確保 - ResourceWrapper() { - m_data = new int[10]; - std::cout << "Resource acquired." << std::endl; + // []演算子で値にアクセス + 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; } - // デストラクタでリソースを解放 - ~ResourceWrapper() { - delete[] m_data; - std::cout << "Resource released." << std::endl; + // count()でキーの存在を確認 + std::string search_key = "Charlie"; + if (scores.count(search_key)) { + std::cout << "\n" << search_key << " is in the map." << std::endl; } -}; -void use_resource() { - ResourceWrapper rw; // オブジェクトが生成され、コンストラクタでリソースが確保される - std::cout << "Using resource..." << std::endl; + // erase()で要素を削除 + scores.erase("Bob"); - // この関数が終了するとき (正常終了でも例外でも)、 - // rwのデストラクタが自動的に呼ばれ、リソースが解放される -} + std::cout << "\nAfter erasing Bob:" << std::endl; + for (const auto& pair : scores) { + std::cout << "- " << pair.first << ": " << pair.second << std::endl; + } -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` の中で例外が発生したとしても、デストラクタは保証付きで呼び出されます。 +```cpp-exec:map_example.cpp +Bob's score: 88 -この強力なRAIIイディオムを、動的メモリ管理のために標準ライブラリが提供してくれているのが**スマートポインタ**です。 +All scores: +- Alice: 98 +- Bob: 88 +- Charlie: 76 +- David: 100 -## スマートポインタ: new/deleteを自動化する +Charlie is in the map. -スマートポインタは、RAIIを実装したクラステンプレートで、生ポインタ (`int*` など) のように振る舞いながら、リソース (確保したメモリ) の所有権を管理し、適切なタイミングで自動的に解放してくれます。 - -モダンC++では、メモリ管理に生ポインタを直接使うことはほとんどなく、スマートポインタを使うのが基本です。主に2種類のスマートポインタを使い分けます。 - -### `std::unique_ptr` - -`std::unique_ptr` は、管理するオブジェクトの**所有権を唯一に保つ**スマートポインタです。つまり、あるオブジェクトを指す `unique_ptr` は、常に一つしか存在できません。 +After erasing Bob: +- Alice: 98 +- Charlie: 76 +- David: 100 +``` - * **唯一の所有権**: コピーが禁止されています。オブジェクトの所有権を別の `unique_ptr` に移したい場合は、**ムーブ (`std::move`)** を使います。 - * **軽量**: ポインタ一つ分のサイズしかなく、オーバーヘッドが非常に小さいです。 +`std::map`は、キーと値のペアを効率的に管理したい場合に非常に強力なツールです。 -`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がこの関数のスコープを抜けるときにデストラクタが呼ばれる -} +STLには、他にも特定の目的に特化したコンテナが多数用意されています。ここでは代表的なものをいくつか紹介します。 -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; -} -``` + * `std::list`: 双方向リスト。要素の途中への挿入・削除が非常に高速 (`O(1)`) ですが、ランダムアクセスはできません(先頭から順番にたどる必要があります)。``ヘッダが必要です。 + * `std::set`: 重複しない要素の集合を管理します。要素は自動的にソートされます。特定の要素が集合内に存在するかどうかを高速に判定したい場合 (`O(log n)`) に便利です。``ヘッダが必要です。 + * `std::unordered_map`: `std::map`と同様にキーと値のペアを管理しますが、内部的にハッシュテーブルを使うため、平均的な検索・挿入・削除がさらに高速 (`O(1)`) です。ただし、要素はソートされません。``ヘッダが必要です。 + * `std::queue`, `std::stack`: それぞれ先入れ先出し (FIFO)、後入れ先出し (LIFO) のデータ構造を実装するためのコンテナアダプタです。 -```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. -``` +どのコンテナを選択するかは、プログラムの要件(データのアクセスパターン、挿入・削除の頻度など)によって決まります。まずは`std::vector`を基本とし、必要に応じて他のコンテナを検討するのが良いアプローチです。 -`unique_ptr` は、オブジェクトの所有者が誰であるかが明確な場合に最適です。基本的にはまず `unique_ptr` を使うことを検討しましょう。 +## この章のまとめ -### `std::shared_ptr` + * **STL**は、**コンテナ**、**アルゴリズム**、**イテレータ**の3つの主要コンポーネントから構成される、C++の強力な標準ライブラリです。 + * **コンテナ**は、データを格納するためのクラスです。 + * `std::vector`は、最も一般的に使われる動的配列で、高速なランダムアクセスと末尾への簡単な要素追加が特徴です。 + * `std::map`は、キーと値のペアを管理する連想配列で、キーによる高速な検索が可能です。 + * 他にも`std::list`, `std::set`など、特定の用途に合わせた様々なコンテナが用意されています。 -`std::shared_ptr` は、管理するオブジェクトの**所有権を複数のポインタで共有できる**スマートポインタです。 +### 練習問題1: 数値ベクタの操作 - * **共有された所有権**: `shared_ptr` は自由にコピーできます。コピーされるたびに、内部の**参照カウンタ**が増加します。 - * **自動解放**: `shared_ptr` が破棄される(デストラクタが呼ばれる)と参照カウンタが減少し、**参照カウンタが0になったとき**に、管理しているオブジェクトが解放(`delete`)されます。 - * **オーバーヘッド**: 参照カウンタを管理するための追加のメモリと処理が必要なため、`unique_ptr` よりもわずかにオーバーヘッドが大きいです。 +`std::vector`型の整数のリストに対して、以下の処理を行うプログラムを作成してください。 -`shared_ptr` を作成するには、`std::make_shared` を使うのが効率的で安全です。 +1. ベクタに格納されている全ての数値の合計値を計算して表示する。 +2. ベクタの中の最大値を検索して表示する。 +3. ベクタの要素を逆順にして、その内容を表示する。(ヒント:新しいベクタを作っても良いですし、`std::swap`を使っても構いません) -```cpp:shared_ptr_example.cpp +```cpp:practice11_1.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. -``` + std::vector numbers = {3, 5, 2, 8, 6}; -`shared_ptr` は、オブジェクトの寿命が単一のスコープや所有者に縛られず、複数のオブジェクトから共有される必要がある場合に便利です。ただし、所有権の関係が複雑になりがちなので、本当に共有が必要な場面に限定して使いましょう。 + // 1. 合計値の計算 -## この章のまとめ - * **例外処理**は `try`, `catch`, `throw` を使い、エラーが発生してもプログラムを安全に継続させるための仕組みです。 - * 手動のメモリ管理下で例外が発生すると、**リソースリーク**を引き起こす危険があります。 - * **RAIIイディオム**は、リソースの確保をコンストラクタ、解放をデストラクタで行うことで、リソース管理を自動化するC++の重要な設計思想です。 - * **スマートポインタ**はRAIIを動的メモリ管理に適用したもので、`new` と `delete` の手動管理を不要にします。 - * **`std::unique_ptr`** はオブジェクトの**唯一の所有権**を管理します。軽量であり、所有権が明確な場合に第一の選択肢となります。 - * **`std::shared_ptr`** はオブジェクトの**所有権を共有**します。参照カウントによって管理され、最後の所有者がいなくなったときにオブジェクトを解放します。 + // 2. 最大値の検索 -モダンC++プログラミングでは、`new` と `delete` を直接書くことは極力避け、RAIIとスマートポインタを全面的に活用することが、安全でメンテナンス性の高いコードへの第一歩です。 -### 練習問題1: `unique_ptr` と所有権の移動 + // 3. 要素の逆順表示 -`Employee` という名前のクラスを作成してください。このクラスは、コンストラクタで社員名を受け取って表示し、デストラクタで「(社員名) is leaving.」というメッセージを表示します。 -`main` 関数で、`"Alice"` という名前の `Employee` オブジェクトを `std::make_unique` で作成し、その `unique_ptr` を `promote_employee` という関数に渡してください。`promote_employee` 関数は `unique_ptr` を引数として受け取り(所有権が移動します)、「(社員名) has been promoted\!」というメッセージを表示します。 - -プログラムを実行し、コンストラクタとデストラクタのメッセージが期待通りに表示されることを確認してください。 + return 0; +} +``` -```cpp:practice11_1.cpp -#include -#include -#include +```cpp-exec:practice11_1.cpp +Sum: 24 +Max: 8 +Reversed: 6 8 2 5 3 +``` -// ここにEmployeeクラスを定義 +### 練習問題2: 簡単な単語カウンター -int main() { +英文(スペースで区切られた単語の列)を読み込み、各単語が何回出現したかをカウントするプログラムを`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-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 #include - -// ここにProjectクラスを定義 - +#include int main() { + std::string text = "cpp is fun and cpp is powerful"; } ``` - ```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. +and: 1 +cpp: 2 +fun: 1 +is: 2 +powerful: 1 ``` diff --git a/public/docs/cpp-12.md b/public/docs/cpp-12.md index ff36b7e..62dd606 100644 --- a/public/docs/cpp-12.md +++ b/public/docs/cpp-12.md @@ -1,293 +1,301 @@ -# 第12章: プロジェクトの分割とビルド +# 第12章: 標準テンプレートライブラリ (STL) ②:アルゴリズムとラムダ式 -これまでの章では、すべてのコードを1つの `.cpp` ファイルに記述してきました。しかし、プログラムが大規模で複雑になるにつれて、このアプローチは現実的ではなくなります。コードの可読性が低下し、少しの変更でもプログラム全体の再コンパイルが必要になり、開発効率が大きく損なわれるからです。 +前の章ではSTLのコンテナについて学び、様々なデータを効率的に格納する方法を見てきました。しかし、データを格納するだけではプログラムは完成しません。そのデータを並べ替えたり、検索したり、特定の処理を施したりといった「操作」が必要です。 -この章では、プログラムを複数のファイルに分割し、それらを効率的に管理・ビルドする方法を学びます。これは、小さなプログラムから一歩進み、本格的なソフトウェア開発を行うための重要なステップです。 +この章では、STLのもう一つの強力な柱である**アルゴリズム**ライブラリを学びます。これらのアルゴリズムは、コンテナ内のデータを操作するための汎用的な関数群です。そして、アルゴリズムをさらに柔軟かつ強力にするための現代的なC++の機能、**ラムダ式**についても解説します。これらをマスターすれば、驚くほど少ないコードで複雑なデータ操作が実現できるようになります。 -## ヘッダファイルとソースファイル +## イテレータ:コンテナとアルゴリズムを繋ぐ架け橋 -C++では、プログラムを**ヘッダファイル**と**ソースファイル**という2種類のファイルに分割するのが一般的です。 +アルゴリズムは、特定のコンテナ(`std::vector` や `std::list` など)に直接依存しないように設計されています。では、どうやってコンテナ内の要素にアクセスするのでしょうか?そこで登場するのが**イテレータ (Iterator)** です。 - * **ヘッダファイル (`.h` または `.hpp`)**: 「宣言」を置く場所です。クラスの定義、関数のプロトタイプ宣言、定数、テンプレートなどを記述します。他のファイルに対して「何ができるか(インターフェース)」を公開する役割を持ちます。 - * **ソースファイル (`.cpp`)**: 「実装」を置く場所です。ヘッダファイルで宣言された関数の具体的な処理内容などを記述します。ヘッダファイルが公開したインターフェースを「どのように実現するか」を記述する役割を持ちます。 +イテレータは、コンテナ内の要素を指し示す「ポインタのような」オブジェクトです。ポインタのように `*` で要素の値を参照したり、`++` で次の要素に進んだりできます。 -### なぜ分割するのか? 🤔 +ほとんどのコンテナは、以下の2つの重要なイテレータを取得するメンバ関数を持っています。 -1. **関心の分離**: インターフェース(何ができるか)と実装(どうやるか)を分離することで、コードの見通しが良くなります。他の開発者はヘッダファイルを見るだけで、その機能の使い方がわかります。 -2. **コンパイル時間の短縮**: ソースファイルを変更した場合、再コンパイルはそのファイルだけで済みます。プロジェクト全体を再コンパイルする必要がないため、大規模なプロジェクトでは開発サイクルが劇的に速くなります。 -3. **再利用性の向上**: よく使う関数やクラスをまとめておけば、別のプロジェクトでそのファイルをインクルードするだけで簡単に再利用できます。 + * `begin()`: コンテナの先頭要素を指すイテレータを返す。 + * `end()`: コンテナの**最後の要素の次**を指すイテレータを返す。これは有効な要素を指していない「番兵」のような役割を果たします。 -### 分割の例 +アルゴリズムは、この `begin()` と `end()` から得られるイテレータのペアを使い、操作対象の「範囲」を指定します。範囲は半開区間 `[begin, end)` で表され、`begin` が指す要素は範囲に含まれ、`end` が指す要素は含まれません。 -簡単な足し算を行う関数を別のファイルに分割してみましょう。 +簡単な例を見てみましょう。イテレータを使って `vector` の全要素を表示するコードです。 -まず、関数の「宣言」をヘッダファイルに記述します。 +```cpp:iterator_example.cpp +#include +#include -```cpp:math_utils.h -// 関数の宣言を記述するヘッダファイル +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; -// この関数が他のファイルから参照されることを示す -int add(int a, int b); + return 0; +} ``` -次に、この関数の「実装」をソースファイルに記述します。 +```cpp-exec:iterator_example.cpp +Numbers: 0 1 2 3 4 +Numbers (range-based for): 0 1 2 3 4 +``` -```cpp:math_utils.cpp -// 関数の実装を記述するソースファイル +このように、イテレータはコンテナの種類を問わず、統一的な方法で要素にアクセスする仕組みを提供します。これが、アルゴリズムが様々なコンテナに対して汎用的に機能する理由です。 -#include "math_utils.h" // 対応するヘッダファイルをインクルード +## 便利なアルゴリズム -int add(int a, int b) { - return a + b; -} -``` +C++の標準ライブラリには、`` ヘッダと `` ヘッダに数多くの便利なアルゴリズムが用意されています。ここでは、特によく使われるものをいくつか紹介します。 -最後に、`main`関数を含むメインのソースファイルから、この`add`関数を呼び出します。 +### `std::sort`: 要素を並べ替える -```cpp:math_app.cpp +名前の通り、指定された範囲の要素をソートします。デフォルトでは昇順に並べ替えます。 + +```cpp:sort_example.cpp #include -#include "math_utils.h" // 自作したヘッダファイルをインクルード +#include +#include // std::sort のために必要 +#include int main() { - int result = add(5, 3); - std::cout << "The result is: " << result << std::endl; + 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:math_app.cpp,math_utils.cpp -The result is: 8 +```cpp-exec:sort_example.cpp +Sorted numbers: 1 2 5 8 9 +Sorted words: apple banana cherry ``` -ここで注目すべき点は、`math_app.cpp`が`add`関数の具体的な実装を知らないことです。`math_utils.h`を通じて「`int`を2つ受け取って`int`を返す`add`という関数が存在する」ことだけを知り、それを利用しています。 +### `std::find`: 要素を検索する -## インクルードガード +指定された範囲から特定の値を持つ要素を検索します。 -複数のファイルから同じヘッダファイルがインクルードされる状況はよくあります。例えば、`A.h`が`B.h`をインクルードし、ソースファイルが`A.h`と`B.h`の両方をインクルードするような場合です。 + * **見つかった場合**: その要素を指すイテレータを返します。 + * **見つからなかった場合**: 範囲の終端を示すイテレータ (`end()`) を返します。 -もしヘッダファイルに何の対策もしていないと、同じ内容(クラス定義や関数宣言)が複数回読み込まれ、「再定義」としてコンパイルエラーが発生してしまいます。 +この性質を利用して、要素が存在するかどうかをチェックできます。 -```cpp:A.h -#include "B.h" // B.hをインクルード +```cpp:find_example.cpp +#include +#include +#include // std::find のために必要 -// A.hの内容 -``` +int main() { + std::vector numbers = {10, 20, 30, 40, 50}; + int value_to_find = 30; -```cpp:B.h -class B { - // Bクラスの内容 -}; -``` + // numbers の中から 30 を探す + auto it = std::find(numbers.begin(), numbers.end(), value_to_find); -```cpp:bad_include_app.cpp -#include "A.h" -#include "B.h" // B.hが二重にインクルードされる + 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; + } -int main() { - [[maybe_unused]] B b; // Bクラスを使う + 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: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 { - | ^ +```cpp-exec:find_example.cpp +Found 30 at index 2 +99 not found. ``` -この問題を解決するのが**インクルードガード**です。インクルードガードは、ヘッダファイルの内容が1つの翻訳単位(ソースファイル)内で一度しか読み込まれないようにするための仕組みです。 +### `std::for_each`: 各要素に処理を適用する -### 伝統的なインクルードガード - -プリプロセッサディレクティブである `#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 +// 3番目の引数に関数を渡す +std::for_each(numbers.begin(), numbers.end(), print_function); ``` - * **最初のインクルード**: `MATH_UTILS_H` は未定義なので、`#define` が実行され、中身が読み込まれます。 - * **2回目以降のインクルード**: `MATH_UTILS_H` は既に定義されているため、`#ifndef` から `#endif` までのすべてが無視されます。 +ここで「特定の処理」をその場で手軽に記述する方法が**ラムダ式**です。 -マクロ名 (`MATH_UTILS_H`) は、ファイル名に基づいて一意になるように命名するのが慣習です。 +## ラムダ式:その場で書ける無名関数 -### \#pragma once +ラムダ式(Lambda Expression)は、C++11から導入された非常に強力な機能です。一言で言えば、「**その場で定義して使える名前のない小さな関数**」です。これにより、アルゴリズムに渡すためだけの短い関数をわざわざ定義する必要がなくなり、コードが非常に簡潔になります。 -より現代的で簡潔な方法として `#pragma once` があります。多くのモダンなコンパイラがサポートしています。 +ラムダ式の基本的な構文は以下の通りです。 ```cpp -#pragma once - -#include - -std::string to_upper(const std::string& str); +[キャプチャ](引数リスト) -> 戻り値の型 { 処理本体 } ``` -この一行をヘッダファイルの先頭に書くだけで、コンパイラがそのファイルが一度しかインクルードされないように処理してくれます。特別な理由がない限り、現在では `#pragma once` を使うのが主流です。 - -## プロジェクトのビルド + * `[]` **キャプチャ句**: ラムダ式の外にある変数を取り込んで、式の中で使えるようにします。 + * `[]`: 何もキャプチャしない。 + * `[=]`: 外の変数を全て値渡し(コピー)でキャプチャする。 + * `[&]`: 外の変数を全て参照渡しでキャプチャする。 + * `[x, &y]`: 変数 `x` は値渡し、変数 `y` は参照渡しでキャプチャする。 + * `()` **引数リスト**:通常の関数と同じ引数を取ることができます。 + * `-> 戻り値の型`: 戻り値の型を指定します。多くの場合、コンパイラが推論できるため省略可能です。 + * `{}` **処理本体**: 関数の処理内容を記述します。 -複数のソースファイル(`.cpp`)は、それぞれがコンパイルされて**オブジェクトファイル**(`.o` や `.obj`)になります。その後、**リンカ**がこれらのオブジェクトファイルと必要なライブラリを結合して、最終的な実行可能ファイルを生成します。 +`std::for_each` とラムダ式を組み合わせた例を見てみましょう。 -この一連の作業を**ビルド**と呼びます。ファイルが増えてくると、これを手動で行うのは非常に面倒です。そこで、ビルド作業を自動化する**ビルドシステム**が使われます。 +```cpp:for_each_lambda_example.cpp +#include +#include +#include -### 手動でのビルド (g++) +int main() { + std::vector numbers = {1, 2, 3, 4, 5}; -先ほどの`math_app.cpp`と`math_utils.cpp`を例に、g++コンパイラで手動ビルドする手順を見てみましょう。 + // 各要素を2倍して表示する + std::cout << "Doubled numbers: "; + std::for_each(numbers.begin(), numbers.end(), [](int n) { + std::cout << n * 2 << " "; + }); + std::cout << std::endl; -```bash -# 1. 各ソースファイルをコンパイルしてオブジェクトファイルを生成する (-c オプション) -g++ -c math_app.cpp -o main.o -g++ -c math_utils.cpp -o math_utils.o + // 外部の変数をキャプチャする例 + int sum = 0; + // `&sum` で sum を参照キャプチャし、ラムダ式内で変更できるようにする + std::for_each(numbers.begin(), numbers.end(), [&sum](int n) { + sum += n; + }); -# 2. オブジェクトファイルをリンクして実行可能ファイルを生成する -g++ main.o math_utils.o -o my_app + std::cout << "Sum: " << sum << std::endl; -# 3. 実行する -./my_app + return 0; +} ``` -または、以下のように1回のg++コマンドで複数ソースファイルのコンパイルとリンクを同時に行うこともできます。 - -```bash -g++ math_app.cpp math_utils.cpp -o my_app -./my_app +```cpp-exec:for_each_lambda_example.cpp +Doubled numbers: 2 4 6 8 10 +Sum: 15 ``` -### Makefileによる自動化 +このコードは、`for_each` の3番目の引数に直接処理を書き込んでいます。非常に直感的で読みやすいと思いませんか? -`make`は、ファイルの依存関係と更新ルールを記述した`Makefile`というファイルに従って、ビルドプロセスを自動化するツールです。 +ラムダ式は、特に `std::sort` のソート順をカスタマイズする際に真価を発揮します。例えば、数値を降順にソートしたい場合、比較ルールをラムダ式で与えることができます。 -以下は、非常にシンプルな`Makefile`の例です。 - -```makefile -# コンパイラを指定 -CXX = g++ -# コンパイルオプションを指定 -CXXFLAGS = -std=c++17 -Wall - -# 最終的なターゲット(実行可能ファイル名) -TARGET = my_app - -# ソースファイルとオブジェクトファイル -SRCS = math_app.cpp math_utils.cpp -OBJS = $(SRCS:.cpp=.o) +```cpp:lambda_sort_example.cpp +#include +#include +#include -# デフォルトのターゲット (makeコマンド実行時に最初に実行される) -all: $(TARGET) +int main() { + std::vector numbers = {5, 2, 8, 1, 9}; -# 実行可能ファイルの生成ルール -$(TARGET): $(OBJS) - $(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS) + // 比較関数としてラムダ式を渡す + // a > b であれば true を返すことで降順ソートになる + std::sort(numbers.begin(), numbers.end(), [](int a, int b) { + return a > b; + }); -# オブジェクトファイルの生成ルール (%.o: %.cpp) -# .cppファイルから.oファイルを作るための汎用ルール -%.o: %.cpp - $(CXX) $(CXXFLAGS) -c $< -o $@ + std::cout << "Sorted in descending order: "; + for (int num : numbers) { + std::cout << num << " "; + } + std::cout << std::endl; -# 中間ファイルなどを削除するルール -clean: - rm -f $(OBJS) $(TARGET) + return 0; +} ``` -この`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) +```cpp-exec:lambda_sort_example.cpp +Sorted in descending order: 9 8 5 2 1 ``` -この`CMakeLists.txt`を使ってビルドする一般的な手順は以下の通りです。 +## この章のまとめ -```bash -# 1. ビルド用の中間ファイルを置くディレクトリを作成し、移動する -mkdir build -cd build +この章では、STLのアルゴリズムとラムダ式について学びました。 -# 2. CMakeを実行して、ビルドシステム(この場合はMakefile)を生成する -cmake .. + * **イテレータ**は、コンテナの要素を指し示すオブジェクトであり、アルゴリズムとコンテナの間のインターフェースとして機能します。 + * `` ヘッダには、**`std::sort`** (ソート)、**`std::find`** (検索)、**`std::for_each`** (繰り返し処理) といった、汎用的で強力なアルゴリズムが多数用意されています。 + * **ラムダ式**は、その場で定義できる無名関数であり、アルゴリズムに渡す処理を簡潔かつ直感的に記述することができます。 + * **キャプチャ**機能を使うことで、ラムダ式の外にある変数を取り込んで処理に利用できます。 -# 3. make (または cmake --build .) を実行してビルドする -make +コンテナ、イテレータ、アルゴリズム、そしてラムダ式。これらを組み合わせることで、C++におけるデータ処理は、他の多くの言語に引けを取らない、あるいはそれ以上に表現力豊かで効率的なものになります。 -# 4. 実行する -./my_app -``` +### 練習問題1: 文字列の長さでソート -CMakeは、ライブラリの検索、依存関係の管理、テストの実行など、大規模プロジェクトに必要な多くの機能を備えており、現在のC++開発における標準的なツールとなっています。 +`std::vector` を用意し、格納されている文字列を、文字数が短い順にソートして、結果を出力するプログラムを作成してください。`std::sort` とラムダ式を使用してください。 -## この章のまとめ +**ヒント**: ラムダ式は2つの文字列を引数に取り、1つ目の文字列の長さが2つ目の文字列の長さより短い場合に `true` を返すように実装します。 - * **プロジェクトの分割**: プログラムは「宣言」を記述する**ヘッダファイル** (`.h`) と、「実装」を記述する**ソースファイル** (`.cpp`) に分割することで、保守性や再利用性が向上します。 - * **インクルードガード**: ヘッダファイルの多重インクルードによる再定義エラーを防ぐために、`#pragma once` や `#ifndef`/`#define`/`#endif` を使用します。 - * **ビルドシステム**: 複数のファイルをコンパイル・リンクするプロセスを自動化するために、`make` や `CMake` といったツールが使われます。特に **CMake** はクロスプラットフォーム開発におけるデファクトスタンダードです。 -### 練習問題1: 電卓クラスの分割 +```cpp:practice12_1.cpp +#include +#include +#include -`Calculator` というクラスを作成してください。このクラスは、加算、減算、乗算、除算のメンバ関数を持ちます。 +int main() { + std::vector words = {"apple", "banana", "kiwi", "cherry", "fig", "grape"}; -* `Calculator.h`: `Calculator`クラスの定義を記述します。 -* `Calculator.cpp`: 各メンバ関数の実装を記述します。 -* `practice12_1.cpp`: `Calculator`クラスのインスタンスを作成し、いくつかの計算を行って結果を表示します。 + return 0; +} +``` -これらのファイルをg++で手動ビルドして、プログラムを実行してください。 +```cpp-exec:practice12_1.cpp +fig kiwi grape apple banana cherry +``` -```cpp:Calculator.h +### 練習問題2: 条件に合う要素のカウント -``` +`std::vector` に整数をいくつか格納します。その後、ラムダ式と `std::for_each`(または他のアルゴリズム)を使って、以下の2つの条件を満たす要素がそれぞれいくつあるかを数えて出力してください。 -```cpp:Calculator.cpp +1. 正の偶数である要素の数 +2. 負の奇数である要素の数 -``` +**ヒント**: カウント用の変数を2つ用意し、ラムダ式のキャプチャ句で参照キャプチャ (`[&]`) して、式の中でインクリメントします。 -```cpp:practice12_1.cpp +```cpp:practice12_2.cpp #include -#include "Calculator.h" +#include +#include int main() { - Calculator calc; + std::vector numbers = {3, -1, 4, -5, 6, -7, 8, 0, -2}; + - 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 +```cpp-exec:practice12_2.cpp +Positive even count: 3 +Negative odd count: 3 ``` diff --git a/public/docs/cpp-13.md b/public/docs/cpp-13.md new file mode 100644 index 0000000..a5f5f04 --- /dev/null +++ b/public/docs/cpp-13.md @@ -0,0 +1,396 @@ +# 第13章: モダン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:practice13_1.cpp +#include +#include +#include + +// ここにEmployeeクラスを定義 + + +int main() { + + +} +``` + +```cpp-exec:practice13_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:practice13_2.cpp +#include +#include +#include +#include + +// ここにProjectクラスを定義 + + +int main() { + + +} +``` + +```cpp-exec:practice13_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-2.md b/public/docs/cpp-2.md index 9bb94a1..ebdf94c 100644 --- a/public/docs/cpp-2.md +++ b/public/docs/cpp-2.md @@ -1,107 +1,51 @@ -# 第2章: C++の型システムとメモリ +# 第2章: C++の型システムと制御構造:静的型付けとスコープを再確認する -C++は**静的型付け言語**です。これは、コンパイル時にすべての変数の型が決定され、一度決まった型は変更できないことを意味します。この厳密な型システムは、一見すると面倒に感じるかもしれませんが、大規模なプログラムでもバグを未然に防ぎ、高いパフォーマンスを引き出すための重要な仕組みです。 +C++は**静的型付け言語**です。PythonやJavaScriptのような動的型付け言語とは異なり、コンパイル時に変数の型が確定している必要があります。これにより、実行時のエラーを未然に防ぎ、高いパフォーマンスを実現します。 -この章では、C++の基本的な型と、他の高級言語ではあまり意識することのない「メモリ」との関係について学んでいきましょう。 +## 基本的なデータ型 -## 基本データ型 +C++には多くの型がありますが、まずは以下の基本型を押さえましょう。 -他の多くの言語と同様に、C++にも数値を扱うための基本的なデータ型が用意されています。すでにご存知のものが多いと思いますが、C++における特徴と合わせて再確認しましょう。 + * **整数型**: `int` (通常4バイト), `long long` (8バイト, 大きな整数) + * **浮動小数点型**: `double` (倍精度, 基本的にこれを使う), `float` (単精度) + * **文字型**: `char` (1バイト文字), `std::string` (文字列クラス。厳密には基本型ではありませんが、実用上必須) + * **ブール型**: `bool` (`true` または `false`) -| 型 (Type) | 説明 (Description) | サイズの例 (Typical Size) | 値の範囲の例 (Example Range) | -| :---------------- | :----------------------------------------------- | :---------------------- | :------------------------------------------------------- | -| `int` | 整数を格納します (Integer) | 4 bytes | `-2,147,483,648` ~ `2,147,483,647` | -| `double` | 倍精度浮動小数点数を格納します (Double-precision float) | 8 bytes | 約 `±1.7E308` (有効数字15桁程度) | -| `char` | 1文字を格納します (Character) | 1 byte | `-128` ~ `127` または `0` ~ `255` | -| `bool` | 真偽値を格納します (Boolean) | 1 byte | `true` または `false` | +C++では変数のサイズ(ビット幅)が環境によって異なる場合がありますが、現代的な環境では `int` は32bit以上であることが保証されています。 -**ポイント**: C++の規格では、`int`が何バイトであるかといったサイズを厳密には定めていません。環境(OSやCPUアーキテクチャ)によって変わる可能性があります。しかし、多くのモダンな環境では上記のサイズが一般的です。 +## 変数の初期化:ユニフォーム初期化 `{}` -### 変数の宣言・代入・初期化 +C++には変数を初期化する方法がいくつもありますが、C++11以降では **波括弧 `{}` を使った初期化(ユニフォーム初期化)** が推奨されています。 -変数は、値を入れておくための「名前付きの箱」のようなものです。C++で変数を使うには、まず「どのような種類の箱(**型**)を、どんな名前で用意するか」をコンピュータに伝える必要があります。これを**宣言 (Declaration)** と呼びます。 +なぜ `{}` が良いのでしょうか? それは、**縮小変換(Narrowing Conversion)** を防げるからです。例えば、少数のデータを整数型変数に無理やり入れようとした時、`=` なら黙って切り捨てられますが、`{}` ならコンパイルエラーにしてくれます。 -```cpp -// 整数を入れるための'age'という名前の箱を宣言 -int age; -``` - -宣言した変数に値を入れることを**代入 (Assignment)** と言います。代入には `=` 記号を使います。 - -```cpp -// 宣言済みの変数 'age' に 30 を代入 -age = 30; -``` - -多くの場合、宣言と代入は同時に行います。これを**初期化 (Initialization)** と呼び、こちらの書き方が一般的で安全です。 - -```cpp:data_types.cpp +```cpp:initialization.cpp #include int main() { - // 宣言と同時に初期化 - int age = 30; - double pi = 3.14159; - char initial = 'A'; - bool is_student = true; - - std::cout << "Age: " << age << std::endl; - std::cout << "Pi: " << pi << std::endl; - std::cout << "Initial: " << initial << std::endl; - std::cout << "Is student? " << is_student << std::endl; // boolは通常 1 (true) または 0 (false) として出力される - - return 0; -} -``` - -```cpp-exec:data_types.cpp -Age: 30 -Pi: 3.14159 -Initial: A -Is student? 1 -``` + // 推奨:波括弧による初期化 + int age{25}; // int age = 25; と同じだがより安全 + double weight{65.5}; + bool is_student{false}; -## 基本的な演算 + // 縮小変換の防止(コメントアウトを外すとコンパイルエラーになります) + // int height{170.5}; // エラー! doubleからintへの情報の欠落を防ぐ -C++では、数値型の変数を使って基本的な算術計算ができます。 + // 従来の方法(=を使う)も間違いではありませんが、警告が出ないことがあります + int rough_height = 170.9; // 170に切り捨てられる(エラーにならない) -| 演算子 | 意味 | 例 | 結果 | -|:---:|:---|:---|:---:| -| `+` | 加算 | `5 + 2` | `7` | -| `-` | 減算 | `5 - 2` | `3` | -| `*` | 乗算 | `5 * 2` | `10` | -| `/` | 除算 | `5 / 2` | `2` | -| `%` | 剰余 | `5 % 2` | `1` | - -### ⚠️ 整数除算の注意点 - -ここで特に注意が必要なのが `/` (除算) です。**整数 (`int`) 同士の割り算の結果は、小数点以下が切り捨てられ、整数 (`int`) になります。** - -```cpp:integer_division.cpp -#include - -int main() { - int a = 7; - int b = 2; - - std::cout << "7 / 2 = " << a / b << std::endl; - - // 正しい計算結果(浮動小数点数)を得るには? - // 演算する値の少なくとも一方が浮動小数点数型である必要があります。 - double c = 7.0; - std::cout << "7.0 / 2 = " << c / b << std::endl; + std::cout << "Alice is " << age << " years old." << std::endl; + std::cout << "Height (rough): " << rough_height << std::endl; return 0; } ``` -```cpp-exec:integer_division.cpp -7 / 2 = 3 -7.0 / 2 = 3.5 +```cpp-exec:initialization.cpp +Alice is 25 years old. +Height (rough): 170 ``` -`7 / 2` が `3` になってしまうのは、`int` 型の `a` と `int` 型の `b` で演算した結果もまた `int` 型になる、というC++のルールのためです。小数点以下の値を得たい場合は、`7.0` のように、どちらかの値を `double` などの浮動小数点数型にする必要があります。 - ## 型を厳密に扱う 静的型付けの恩恵を最大限に受けるために、C++には型をより安全かつ便利に扱うための仕組みがあります。 @@ -156,165 +100,149 @@ auto y = 3.14; // y は double型になる auto z = "hello"; // z は const char* (C言語スタイルの文字列) になるので注意 ``` -## C++の文字列: `std::string` +## コンソール入出力 (`std::cin`, `std::cout`) + +C言語の `printf`/`scanf` と異なり、C++ではストリーム(データの流れ)として入出力を扱います。型指定子(`%d`など)を覚える必要がなく、型安全です。 -C言語では文字列を`char`の配列(`char*`)として扱いましたが、これは扱いにくく、バグの温床でした。モダンC++では、**`std::string`** クラスを使うのが標準的です。`std::string`は、文字列の連結、長さの取得、部分文字列の取り出しといった操作を安全かつ簡単に行うための豊富な機能を提供します。 + * `std::cout << 値`: 出力(Console OUT) + * `std::cin >> 変数`: 入力(Console IN) + * `std::endl`: 改行を行い、バッファをフラッシュする。 -`std::string`を使うには、``ヘッダをインクルードする必要があります。 +> my.code(); の実行環境には入力機能がないので、コード例だけ示します: -```cpp:string_example.cpp +```cpp #include -#include // std::string を使うために必要 +#include int main() { - // 文字列の宣言と初期化 - std::string greeting = "Hello"; + int id; + std::string name; - // 文字列の連結 - std::string name = "C++"; - std::string message = greeting + ", " + name + "!"; + // 複数の値を出力する場合、<< で連結します + std::cout << "Enter ID and Name: "; + + // キーボードから "101 Bob" のように入力されるのを待つ + std::cin >> id >> name; - std::cout << message << std::endl; - - // 文字列の長さを取得 - std::cout << "Length: " << message.length() << std::endl; + std::cout << "User: " << name << " (ID: " << id << ")" << std::endl; + // User: Bob (ID: 101) return 0; } ``` -```cpp-exec:string_example.cpp -Hello, C++! -Length: 11 -``` - -## 複数の値をまとめて扱う:配列 +## 制御構文:if, switch, while, for -同じ型のデータを複数個まとめて扱いたい場合、配列を使います。C++にはいくつかの配列の形がありますが、ここでは代表的な3つを軽く紹介します。 +他のC系言語(Java, C\#, JSなど)とほぼ同じですが、いくつか注意点があります。 -### 1\. Cスタイルの配列 +### if文 -C言語から引き継がれた、最も基本的な配列です。 +`if (条件式)` の条件式は `bool` に変換可能なものである必要があります。C++では `0` は `false`、それ以外は `true` とみなされます。 -```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 -``` +### switch文とフォールスルー -**特徴**: +`switch` 文は `break` を書かない限り、次の `case` へ処理が流れます(フォールスルー)。意図的なフォールスルーでない限り、`break` を忘れないように注意が必要です。C++17以降では `[[fallthrough]];` 属性をつけることで、「意図的なものである」とコンパイラに伝え、警告を抑制できます。 - * 構文がシンプル。 - * 配列のサイズ自体を保持していないため、プログラマがサイズを管理する必要がある。 +### ループ構文 -### 2\. `std::array` (固定長配列) +`while`, `for` も標準的です。 -Cスタイル配列を安全に使いやすくしたものです。サイズがコンパイル時に決まっている場合に使います。``ヘッダが必要です。 - -```cpp:std_array.cpp +```cpp:control.cpp #include -#include int main() { - std::array scores = {88, 92, 75, 100, 69}; - - // 安全なアクセス方法 .at() - scores.at(2) = 80; - std::cout << "3番目のスコア: " << scores.at(2) << std::endl; + // --- if文 --- + const int score = 85; + if (score >= 90) { + std::cout << "Grade: A" << std::endl; + } else if (score >= 80) { + std::cout << "Grade: B" << std::endl; + } else { + std::cout << "Grade: C or below" << std::endl; + } + + // --- switch文 --- + const int rank = 2; + std::cout << "Rank " << rank << ": "; + + switch (rank) { + case 1: + std::cout << "Gold" << std::endl; + break; + case 2: + std::cout << "Silver" << std::endl; + // breakを忘れるとcase 3も実行される + [[fallthrough]]; // C++17: 意図的に下に流すことを明示 + case 3: + std::cout << "(Medalist)" << std::endl; + break; + default: + std::cout << "Participant" << std::endl; + } + + // --- 基本的なforループ --- + std::cout << "Countdown: "; + for (int i = 3; i > 0; --i) { + std::cout << i << " "; + } + std::cout << "Start!" << std::endl; - // サイズを取得できる .size() - std::cout << "配列のサイズ: " << scores.size() << std::endl; + return 0; } ``` -```cpp-exec:std_array.cpp -3番目のスコア: 80 -配列のサイズ: 5 -``` - -**特徴**: - - * Cスタイル配列同様、サイズは固定。 - * `.size()` で要素数を取得できる。 - * `.at(i)` を使うと、範囲外のインデックスにアクセスしようとした際にエラーを検知してくれるため安全性が高い。 - -### 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:std_vector.cpp -現在のサイズ: 4 -最後のスコア: 100 +```cpp-exec:control.cpp +Grade: B +Rank 2: Silver +(Medalist) +Countdown: 3 2 1 Start! ``` +## この章のまとめ -**特徴**: - - * サイズを**実行中に変更**できる(要素の追加や削除が可能)。 - * `std::array` と同様の便利な機能 (`.size()`, `.at()` など) を持つ。 - * C++で最もよく使われるコンテナ(データ構造)の一つ。 - + * **型システム**: `int`, `double`, `bool` などの基本型があり、静的に管理される。 + * **初期化**: `int x{10};` のような波括弧 `{}` を使う初期化が推奨される(縮小変換の防止)。 + * **型推論と定数**: `auto` で型推論を行い、変更しない変数には `const` を付ける。 + * **入出力**: `std::cout`, `std::cin` を使い、`<<`, `>>` 演算子でデータを流す。 + * **制御構文**: 基本は他言語と同じだが、`switch` のフォールスルー挙動などに注意する。 -## この章のまとめ - * C++は**静的型付け言語**であり、コンパイル時にすべての変数の型が決まる。 - * `int`, `double`, `char`, `bool` といった基本的なデータ型が存在する。 - * 変数は**宣言**してから使い、宣言と同時に値を代入する**初期化**が一般的。 - * 基本的な**四則演算**ができるが、**整数同士の除算**は結果が整数に切り捨てられる点に注意。 - * **`const`** を使うことで、変数を不変にし、プログラムの安全性を高めることができる。 - * **`auto`** を使うことで、コンパイラに型を推論させ、コードを簡潔に書くことができる。 - * モダンC++では、文字列は**`std::string`**、配列は**`std::vector`** や **`std::array`** クラスを使って安全かつ便利に扱う。 - * 宣言された変数は、メモリ上の特定の**アドレス**に、その型に応じた**サイズ**の領域を確保して格納される。 +## 練習問題1:うるう年判定機 -### 練習問題1 +西暦(整数)を変数 `year` に代入し、その年がうるう年かどうかを判定して結果を出力するプログラムを書いてください。 -あなたの名前(`std::string`)、年齢(`int`)、視力(`double`)をそれぞれ変数として宣言し、コンソールに出力するプログラムを書いてください。ただし、名前は一度決めたら変わらないものとして、`const`を使って宣言してください。 +> **うるう年の条件** +> +> 1. 4で割り切れる年はうるう年である。 +> 2. ただし、100で割り切れる年はうるう年ではない。 +> 3. ただし、400で割り切れる年はうるう年である。 ```cpp:practice2_1.cpp #include -#include int main() { + const int year = 2025; + // ここにコードを書く } ``` ```cpp-exec:practice2_1.cpp ``` +### 練習問題2:FizzBuzz(C++スタイル) + +1から20までの整数を順に出力するループを作成してください。ただし、以下のルールに従ってください。 -### 練習問題2 + * 数値が3で割り切れるときは数値の代わりに "Fizz" と出力。 + * 数値が5で割り切れるときは数値の代わりに "Buzz" と出力。 + * 両方で割り切れるときは "FizzBuzz" と出力。 + * それ以外は数値をそのまま出力。 -2つの`std::string`変数 `firstName` と `lastName` を宣言し、あなたの姓名で初期化してください。その後、これら2つの変数を連結してフルネームを`fullName`という新しい変数に格納し、そのフルネームと文字数(長さ)をコンソールに出力するプログラムを書いてください。 +出力はスペース区切りまたは改行区切りどちらでも構いません。変数の初期化には `{}` を、ループカウンタの型には `auto` を使用してみてください。 ```cpp:practice2_2.cpp #include -#include int main() { diff --git a/public/docs/cpp-3.md b/public/docs/cpp-3.md index 637f33c..6cb1ae6 100644 --- a/public/docs/cpp-3.md +++ b/public/docs/cpp-3.md @@ -1,334 +1,267 @@ -# 第3章: 関数と参照 +# 第3章: データ集合とモダンな操作:「配列」ではなく「コンテナ」としてデータを扱う -プログラムを構成する基本的な部品である「関数」について、C++ならではの引数の渡し方や便利な機能を学びます。他の言語で関数に慣れている方も、C++特有の概念である「参照」は特に重要なので、しっかり理解していきましょう。 +他の言語(Python, JavaScript, C\#など)経験者がC++を学び始めるとき、最も躓きやすいのが「文字や配列の扱い」です。古いC言語の教科書では、ポインタ操作やメモリ管理が必須となる「Cスタイル」のやり方から入ることが多いのですが、**現代の実務的なC++(モダンC++)では、もっと安全で便利な「クラス(コンテナ)」を使います。** -## 関数の基本: 宣言と定義の分離 +この章では、ポインタの複雑な話を抜きにして、他の高級言語と同じくらい直感的にデータを扱えるツールを紹介します。 -プログラム内の特定のタスクを実行するコードのまとまりを「**関数**」と呼びます。C++では、この関数を利用する前に、コンパイラがその関数の存在と使い方を知っている必要があります。そのため、「**宣言 (declaration)**」と「**定義 (definition)**」という2つの概念が重要になります。 +## 文字列の扱い:std::string - * **宣言 (Declaration)**: 関数の使い方(名前、引数、戻り値)をコンパイラに教える。本体の処理はない。 - * **定義 (Definition)**: 関数の具体的な処理内容を記述する。 +C言語では文字列を扱うために `char*` や `char[]` を使い、ヌル終端文字 `\0` を意識する必要がありました。これはバグの温床です。 +C++では、標準ライブラリの `std::string` クラスを使用します。これはPythonの `str` や Javaの `String` のように直感的に扱えます。 -### 関数の宣言 +### 主な機能 -宣言の基本的な文法は以下の通りです。 + * **代入・初期化**: 文字列リテラルをそのまま代入可能。 + * **結合**: `+` 演算子で結合可能。 + * **比較**: `==`, `!=` などで中身の文字列比較が可能(C言語の `strcmp` は不要)。 + * **サイズ取得**: `.size()` または `.length()` メソッドを使用。 -``` -戻り値の型 関数名(引数の型1 引数名1, 引数の型2 引数名2, ...); -``` - - * **戻り値の型 (Return Type)**: 関数が処理を終えた後に返す値の型です。例えば、`int`型なら整数値を返します。 - * **関数名 (Function Name)**: 関数を呼び出すときに使う名前です。 - * **引数リスト (Parameter List)**: 関数が処理のために受け取る値です。`()`の中に、`型名 変数名`のペアをコンマで区切って記述します。引数が必要ない場合は `()` の中を空にします。 - * **セミコロン (`;`)**: 宣言の最後には必ずセミコロンを付けます。 + -### 戻り値がない場合: `void`型 - -関数が何も値を返す必要がない場合もあります。例えば、「画面にメッセージを表示するだけ」といった関数です。その場合、戻り値の型として `void` という特別なキーワードを使います。 - -```cpp -void printMessage(std::string message); -``` - -第2章で学んだように、`int`や`double`などの型は変数を定義するために使えましたが、`void`は「型がない」ことを示す特殊な型なので、`void my_variable;` のように変数を定義することはできません。あくまで関数の戻り値の型としてのみ使います。 - -### コンパイラは上から順に読む - -C++のコンパイラはソースコードを上から下へと順番に読み込んでいきます。そのため、`main`関数のような場所で別の関数を呼び出すコードに出会ったとき、コンパイラはその時点ですでに関数の「宣言」または「定義」を読み込んでいる必要があります。 +```cpp:string_demo.cpp +#include +#include // std::stringを使うために必要 -つまり、**`main`関数よりも上(前)に、呼び出す関数の定義か宣言のどちらかが書かれていなければコンパイルエラー**になります。 +int main() { + // 初期化 + std::string greeting = "Hello"; + std::string target = "World"; -コードを整理するため、一般的には`main`関数の前に関数の「宣言」だけを記述し、`main`関数の後(または別のファイル)に具体的な処理内容である「定義」を記述するスタイルがよく使われます。 + // 文字列の結合 + std::string message = greeting + ", " + target + "!"; -以下の例で確認してみましょう。 + // 出力 + std::cout << message << std::endl; -```cpp:declaration_definition.cpp -#include -#include + // 長さの取得 + std::cout << "Length: " << message.size() << std::endl; // .length()でも同じ -// 1. 関数の「宣言」(プロトタイプ宣言) -// これにより、main関数の中で greet や add を使ってもコンパイラはエラーを出さない。 -void greet(std::string name); // 戻り値がない関数の宣言 -int add(int a, int b); // int型の値を返す関数の宣言 + // 文字列の比較 + if (greeting == "Hello") { + std::cout << "Greeting matches 'Hello'." << std::endl; + } -// main関数: プログラムの開始点 -int main() { - // 宣言があるので、これらの関数を呼び出すことができる - greet("Taro"); + // 特定の文字へのアクセス(配列のようにアクセス可能) + message[0] = 'h'; // 先頭を小文字に変更 + std::cout << "Modified: " << message << std::endl; - int result = add(5, 3); - std::cout << "5 + 3 = " << result << std::endl; - return 0; } - -// 2. 関数の「定義」(本体の実装) -// 実際の処理はここに書く。 -void greet(std::string name) { - std::cout << "Hello, " << name << "!" << std::endl; -} - -int add(int a, int b) { - return a + b; -} ``` -```cpp-exec:declaration_definition.cpp -Hello, Taro! -5 + 3 = 8 +```cpp-exec:string_demo.cpp +Hello, World! +Length: 13 +Greeting matches 'Hello'. +Modified: hello, World! ``` -この例では、`main`関数が始まる前に`greet`関数と`add`関数の宣言をしています。これにより、`main`関数内でこれらの関数を自由な順序で呼び出すことができ、コードの可読性が向上します。関数の具体的な実装は`main`関数の後にまとめて記述することで、「プログラムの全体的な流れ(`main`)」と「各部分の具体的な処理(関数の定義)」を分離して考えることができます。 +> **Note:** `std::string` は必要に応じて自動的にメモリを拡張します。プログラマがメモリ確保(malloc/free)を気にする必要はありません。 -## 引数の渡し方 +## 可変長配列:std::vector -C++の関数の引数の渡し方には、主に **「値渡し」「ポインタ渡し」「参照渡し」** の3つがあります。ここでは特にC++特有の「参照渡し」に注目します。 +「データの個数が事前にわからない」「途中でデータを追加したい」という場合、C++で最も頻繁に使われるのが `std::vector` です。これは「動的配列」や「可変長配列」と呼ばれ、Pythonの `list` や Javaの `ArrayList` に相当します。 -### 値渡し (Pass by Value) +### 基本操作 -引数に渡された値が**コピー**されて、関数内のローカル変数として扱われます。関数内でその値を変更しても、呼び出し元の変数は影響を受けません。これは多くの言語で標準的な引数の渡し方です。 + * **宣言**: `std::vector<型> 変数名;` + * **追加**: `.push_back(値)` で末尾に追加。 + * **アクセス**: `変数名[インデックス]` または `.at(インデックス)`。 + * **サイズ**: `.size()`。 -```cpp:pass_by_value.cpp -#include + -void tryToChange(int x) { - x = 100; // 関数内のコピーが変更されるだけ - std::cout << "Inside function: x = " << x << std::endl; -} +```cpp:vector_demo.cpp +#include +#include // std::vectorを使うために必要 int main() { - int my_number = 10; - std::cout << "Before function call: my_number = " << my_number << std::endl; - tryToChange(my_number); - std::cout << "After function call: my_number = " << my_number << std::endl; // 10のまま変わらない - return 0; -} -``` + // 整数を格納するvector(初期サイズは0) + std::vector numbers; -```cpp-exec:pass_by_value.cpp -Before function call: my_number = 10 -Inside function: x = 100 -After function call: my_number = 10 -``` + // データの追加 + numbers.push_back(10); + numbers.push_back(20); + numbers.push_back(30); - * **長所**: 呼び出し元の変数が不用意に書き換えられることがなく、安全です。 - * **短所**: 大きなオブジェクト(例えば、たくさんの要素を持つ `std::vector` など)を渡すと、コピーのコストが無視できなくなり、パフォーマンスが低下する可能性があります。 + // サイズの確認 + std::cout << "Size: " << numbers.size() << std::endl; -### ポインタ渡し (Pass by Pointer) - -これはC言語から引き継がれた伝統的な方法で、変数のメモリアドレスを渡します。ポインタ(アドレスを指し示す変数)を介して、呼び出し元の変数を直接変更できます。詳細は第4章で詳しく学びますが、ここでは簡単に紹介します。 - -```cpp:pass_by_pointer.cpp -#include + // 要素へのアクセス + std::cout << "First element: " << numbers[0] << std::endl; + + // .at() を使うと範囲外アクセスの時に例外を投げてくれる(安全) + try { + std::cout << numbers.at(100) << std::endl; // 範囲外 + } catch (const std::out_of_range& e) { + std::cout << "Error: " << e.what() << std::endl; + } -// ポインタを受け取るには、型名の後にアスタリスク * を付ける -void changeWithPointer(int* ptr) { - *ptr = 100; // アスタリスク * でポインタが指す先の値にアクセス -} + // 初期化リストを使った宣言(C++11以降) + std::vector prices = {10.5, 20.0, 33.3}; + std::cout << "Price list size: " << prices.size() << std::endl; -int main() { - int my_number = 10; - // 変数のアドレスを渡すには、アンパサンド & を付ける - changeWithPointer(&my_number); - std::cout << "After function call: my_number = " << my_number << std::endl; // 100に変わる return 0; } ``` -```cpp-exec:pass_by_pointer.cpp -After function call: my_number = 100 +```cpp-exec:vector_demo.cpp +Size: 3 +First element: 10 +Error: vector::_M_range_check: __n (which is 100) >= this->size() (which is 3) +Price list size: 3 ``` -ポインタは強力ですが、`nullptr`(どこも指していないポインタ)の可能性を考慮する必要があるなど、扱いが少し複雑です。 - -### 参照渡し (Pass by Reference) +## 固定長配列:std::array -C++の大きな特徴の一つが**参照 (Reference)** です。参照は、既存の変数に**別名**を付ける機能と考えることができます。。 +データの個数が決まっている場合(例えば、3次元座標、RGB値、固定バッファなど)は、`std::vector` よりも `std::array` が適しています。 -関数に参照を渡すと、値のコピーは発生せず、関数内の引数は呼び出し元の変数の「別名」として振る舞います。そのため、関数内での操作が呼び出し元の変数に直接反映されます。構文もポインタよりずっとシンプルです。 +「なぜ昔ながらの `int arr[5];` を使わないの?」と思われるかもしれません。 +Cスタイルの配列は、他のコンテナ(vectorなど)と操作感が異なり、サイズ情報を自分で管理しなければならないなどの欠点があります。`std::array` はC配列のパフォーマンス(スタック確保)と、コンテナの利便性(`.size()`などが使える)を両立させたものです。 -```cpp:pass_by_reference.cpp +```cpp:array_demo.cpp #include - -// 参照を受け取るには、型名の後にアンパサンド & を付ける -void changeWithReference(int& ref) { - ref = 100; // 通常の変数と同じように扱うだけ -} +#include // std::arrayを使うために必要 int main() { - int my_number = 10; - changeWithReference(my_number); // 呼び出し側は特別な記号は不要 - std::cout << "After function call: my_number = " << my_number << std::endl; // 100に変わる - return 0; -} -``` - -```cpp-exec:pass_by_reference.cpp -After function call: my_number = 100 -``` - + // int型でサイズ3の配列を宣言・初期化 + // std::array<型, サイズ> + std::array coords = {10, 20, 30}; - * **長所**: コピーが発生しないため効率的です。また、構文がシンプルで、呼び出し元の変数を変更する意図が明確になります。 - * **注意点**: 関数内で値を変更できるため、意図しない書き換えに注意が必要です。 + std::cout << "X: " << coords[0] << std::endl; + std::cout << "Y: " << coords[1] << std::endl; + std::cout << "Z: " << coords[2] << std::endl; -#### `const`参照: 効率と安全性の両立 + // vectorと同じようにsize()が使える + std::cout << "Dimension: " << coords.size() << std::endl; -「大きなオブジェクトを渡したいけど、コピーは避けたい。でも関数内で値を変更されたくはない」という場合に最適なのが **`const`参照** です。 - -```cpp:const_reference.cpp -#include -#include - -// const参照で受け取ることで、コピーを防ぎつつ、 -// messageが関数内で変更されないことを保証する -void printMessage(const std::string& message) { - // message = "changed!"; // この行はコンパイルエラーになる! - std::cout << message << std::endl; -} - -int main() { - std::string greeting = "Hello, C++ world! This is a long string."; - printMessage(greeting); return 0; } ``` -```cpp-exec:const_reference.cpp -Hello, C++ world! This is a long string. +```cpp-exec:array_demo.cpp +X: 10 +Y: 20 +Z: 30 +Dimension: 3 ``` -**C++のベストプラクティス**: - - * `int`や`double`などの小さな基本型は**値渡し**。 - * 関数内で引数を**変更する必要がある**場合は**参照渡し (`&`)**。 - * 大きなオブジェクトを渡すが**変更はしない**場合は **`const`参照 (`const &`)** を使う。 - -## 関数のオーバーロード - -C++では、**同じ名前で引数の型や個数が異なる関数を複数定義**できます。これを**オーバーロード (Overload)** と呼びます。コンパイラは、関数呼び出し時の引数の型や個数を見て、どの関数を呼び出すべきかを自動的に判断してくれます。 +## 範囲ベース for ループ (Range-based for) -```cpp:overloading.cpp -#include -#include - -// int型の引数を1つ取るprint関数 -void print(int value) { - std::cout << "Integer: " << value << std::endl; -} +`std::vector` や `std::array` の中身を順番に処理する場合、インデックス `i` を使った `for (int i = 0; i < n; ++i)` は書くのが面倒ですし、境界外アクセスのリスクがあります。 -// string型の引数を1つ取るprint関数 -void print(const std::string& value) { - std::cout << "String: " << value << std::endl; -} +モダンC++では、PythonやC\#の `foreach` に相当する **範囲ベース for ループ** が使えます。 -// double型とint型の引数を取るprint関数 -void print(double d_val, int i_val) { - std::cout << "Double: " << d_val << ", Integer: " << i_val << std::endl; -} +### 基本構文 -int main() { - print(123); - print("hello"); - print(3.14, 42); - return 0; +```cpp +for (要素の型 変数名 : コンテナ) { + // 処理 } ``` -```cpp-exec:overloading.cpp -Integer: 123 -String: hello -Double: 3.14, Integer: 42 -``` - -これにより、`printInt`, `printDouble` のように別々の名前を付ける必要がなくなり、コードが直感的で読みやすくなります。 +ここで便利なのが、**`auto` キーワード**です。`auto` を使うと、コンパイラが型を自動推論してくれるため、型名を詳しく書く必要がなくなります。 -注意点として、戻り値の型が違うだけではオーバーロードはできません。あくまで引数のリストが異なる必要があります。 - -## デフォルト引数 - -関数の引数に、あらかじめ**デフォルト値**を設定しておくことができます。これにより、関数を呼び出す際に該当する引数を省略できるようになります。 - -デフォルト引数は、引数リストの**右側**から設定する必要があります。一度デフォルト引数を設定したら、それより右側にある引数もすべてデフォルト引数を持たなければなりません。 - -```cpp:default_arguments.cpp +```cpp:range_for_demo.cpp #include +#include #include -// 第2引数 greeting にデフォルト値を設定 -void greet(const std::string& name, const std::string& greeting = "Hello") { - std::cout << greeting << ", " << name << "!" << std::endl; -} - int main() { - // 第2引数を省略。デフォルト値 "Hello" が使われる - greet("Alice"); + std::vector inventory = {"Sword", "Shield", "Potion"}; - // 第2引数を指定。指定した値 "Hi" が使われる - greet("Bob", "Hi"); + std::cout << "--- Inventory List ---" << std::endl; + + // string item : inventory と書いても良いが、autoが楽 + for (auto item : inventory) { + std::cout << "- " << item << std::endl; + } + + // 数値の計算例 + std::vector scores = {80, 65, 90, 72}; + int total = 0; + + for (auto score : scores) { + total += score; + } + + std::cout << "Total Score: " << total << std::endl; return 0; } ``` -```cpp-exec:default_arguments.cpp -Hello, Alice! -Hi, Bob! +```cpp-exec:range_for_demo.cpp +--- Inventory List --- +- Sword +- Shield +- Potion +Total Score: 307 ``` -## この章のまとめ +> **Advanced Hint:** +> 上記の `auto item` は、要素を「コピー」して取り出します。`std::string` のような大きなデータを扱う場合、コピーコストを避けるために `const auto& item` (参照)を使うのが一般的ですが、これについては**第5章**で詳しく解説します。今の段階では「`auto` でループが回せる」と覚えておけば十分です。 -この章では、C++の関数に関する基本的ながらも重要な機能を学びました。 +# この章のまとめ - * **関数の宣言と定義の分離**: プログラムの構造を整理し、分割コンパイルを可能にするための基本です。 - * **引数の渡し方**: - * **値渡し**: 引数のコピーを作成し、元の変数を保護します。 - * **参照渡し (`&`)**: 変数の「別名」を渡し、コピーのコストをなくします。呼び出し元の変数を変更するため、または効率化のために使います。 - * **`const`参照渡し (`const&`)**: 効率的でありながら、意図しない変更を防ぐためのC++の定石です。 - * **関数のオーバーロード**: 同じ名前で引数リストの異なる関数を定義でき、文脈に応じた適切な関数が自動で選ばれます。 - * **デフォルト引数**: 関数の引数を省略可能にし、柔軟な関数呼び出しを実現します。 +1. **文字列**: `char*` ではなく `std::string` を使う。結合や比較が簡単で安全。 +2. **動的配列**: データの増減がある場合は `std::vector` を使う。`push_back()` で追加できる。 +3. **固定配列**: サイズ固定の場合は `std::array` を使う。Cスタイル配列のモダンな代替。 +4. **ループ**: コンテナの全要素走査には「範囲ベース for ループ」と `auto` を使うとシンプルに書ける。 -特に「参照」は、この先のC++プログラミングで頻繁に登場する極めて重要な概念です。値渡しとの違い、そして`const`参照との使い分けをしっかりマスターしましょう。 +これらの「標準ライブラリ(STL: Standard Template Library)」のコンテナを活用することで、メモリ管理の苦労を飛ばして、アプリケーションのロジックに集中できるようになります。 -### 練習問題1: 値の交換 +### 練習問題1: 数値リストの統計 -2つの`int`型変数の値を交換する関数 `swap` を作成してください。この関数は、呼び出し元の変数の値を直接変更できるように、**参照渡し**を使って実装してください。 +`std::vector` を使用して、好きな整数を5つほど格納してください(コード内で直接初期化して構いません)。 +その後、範囲ベース for ループを使用して、その数値の「合計」と「最大値」を求めて出力するプログラムを作成してください。 ```cpp:practice3_1.cpp #include +#include -// ここにswap関数を実装してください +int main() { + // ここに整数リストを初期化してください + std::vector numbers = {12, 45, 7, 23, 89}; -// main関数 -int main() { - int a = 10; - int b = 20; - std::cout << "Before: a = " << a << ", b = " << b << std::endl; - swap(a, b); - std::cout << "After: a = " << a << ", b = " << b << std::endl; + // 結果を出力 + std::cout << "Sum: " << sum << std::endl; + std::cout << "Max Value: " << max_value << std::endl; + return 0; } ``` ```cpp-exec:practice3_1.cpp -(期待される実行結果) -Before: a = 10, b = 20 -After: a = 20, b = 10 +Sum: 176 +Max Value: 89 ``` -### 問題2: 図形の面積 - -**関数のオーバーロード**を使い、正方形と長方形の面積を計算する `calculate_area` という名前の関数を実装してください。 +### 練習問題2: 単語のフィルタリング -1. 引数が1つ (`int side`) の場合は、正方形の面積 (side✕side) を計算して返す。 -2. 引数が2つ (`int width`, `int height`) の場合は、長方形の面積 (width✕height) を計算して返す。 - -作成した関数を`main`関数から呼び出し、結果が正しく表示されることを確認してください。 +以下の単語リスト `words` の中から、**文字数(長さ)が5文字より大きい単語だけ**を選んで表示するプログラムを作成してください。 +(ヒント:`std::string` の `.size()` または `.length()` メソッドと `if` 文を使用します) ```cpp:practice3_2.cpp #include +#include +#include +int main() { + std::vector words = {"Apple", "Banana", "Cherry", "Date", "Elderberry"}; + + std::cout << "Words longer than 5 characters:" << std::endl; + + // ここにコードを書く + + return 0; +} ``` ```cpp-exec:practice3_2.cpp +Words longer than 5 characters: +Banana +Cherry +Elderberry ``` diff --git a/public/docs/cpp-4.md b/public/docs/cpp-4.md index 5d14126..1d0a05c 100644 --- a/public/docs/cpp-4.md +++ b/public/docs/cpp-4.md @@ -1,280 +1,305 @@ -# 第4章: ポインタと動的メモリ +# 第4章: ポインタとメモリ管理の深淵 -C++の最も強力かつ、多くの初学者がつまずくトピックの一つである「ポインタ」を学びます。ポインタを理解することで、C++がどのようにメモリを扱っているのか、その裏側を垣間見ることができます。他の言語では隠蔽されているメモリへの直接的なアクセスは、パフォーマンスが重要な場面で絶大な効果を発揮します。さあ、メモリを直接操作する感覚を掴んでいきましょう。 +ようこそ、C++学習の最大の山場へ。 +第3章までは、`std::vector`や`std::string`といった便利な機能を使ってきましたが、今回はその「裏側」で何が起きているかを覗き込みます。 -## 変数とメモリ +他の言語(Java, Python, C\#など)では言語機能やガベージコレクション(GC)が隠蔽してくれている「メモリ」という物理的なリソースを、C++では直接操作することができます。これがC++の強力な武器であり、同時にバグの温床でもあります。 -C++を深く理解する上で避けて通れないのが**メモリ**の概念です。変数を宣言すると、コンピュータのメモリ上にその型に応じたサイズの領域が確保されます。 +ここを理解すれば、第3章の機能がいかに偉大だったか、そしてコンピュータが実際にどう動いているかが手に取るようにわかるようになります。 -例えば、`int x = 10;` と書くと、 +## ポインタの基礎 -1. コンパイラは`int`型に必要なメモリサイズ(例: 4バイト)を判断します。 -2. プログラム実行時、メモリ上のどこかに4バイトの領域が確保されます。 -3. その領域に`x`という名前が割り当てられ、値として`10`が格納(バイナリ形式で書き込み)されます。 +ポインタとは、変数の「値」ではなく、その変数がメモリ上の**どこにあるか(アドレス)**を格納する変数です。 -この「メモリ上のどこか」を示すのが**メモリアドレス**(単にアドレスとも)です。ちょうど、現実世界の家の住所のようなものだと考えてください。アドレスは、メモリ上の各バイトに割り振られた通し番号のようなもので、通常は16進数で表現されます。 +変数の型に応じて、対応するポインタの型が存在します。例えば、`int`型の変数のアドレスを格納するなら `int*` 型、`double`型の変数のアドレスを格納するなら `double*` 型のポインタを使います。アスタリスク `*` がポインタ型であることを示します。 + +> ポインタ変数の宣言時に `*` を型の横に付けるか、変数名の横に付けるかは好みが分かれますが、意味は同じです (`int* p;` と `int *p;` は等価)。このチュートリアルでは `int* p;` のように型の側に付けます。 + +### アドレスと間接参照 + + * **アドレス演算子 `&`**: 変数のメモリ上の住所(アドレス)を取得します。 + * **間接参照演算子 `*`**: ポインタが指し示している住所に行き、その中身(値)にアクセスします。 -変数名の前に`&`(アドレス演算子)を付けることで、その変数が格納されているメモリアドレスを知ることができます。 + -```cpp:memory_address.cpp +```cpp:basic_pointer.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; + int number = 42; + // numberのアドレスを取得して ptr に格納 + // int* は「int型へのポインタ」という意味 + int* ptr = &number; - std::cout << "変数 'pi' の値: " << pi << std::endl; - std::cout << "変数 'pi' のメモリアドレス: " << &pi << std::endl; - std::cout << std::endl; + std::cout << "numberの値: " << number << std::endl; + std::cout << "numberのアドレス (&number): " << &number << std::endl; + std::cout << "ptrの値 (アドレス): " << ptr << std::endl; + + // アドレスの中身を見る(間接参照) + std::cout << "ptrが指す中身 (*ptr): " << *ptr << std::endl; - std::cout << "変数 'name' の値: " << name << std::endl; - std::cout << "変数 'name' のメモリアドレス: " << &name << std::endl; + // ポインタ経由で値を書き換える + *ptr = 100; + std::cout << "書き換え後のnumber: " << number << std::endl; return 0; } ``` -```cpp-exec:memory_address.cpp -変数 'age' の値: 30 -変数 'age' のメモリアドレス: 0x7ffee3b8c9ac - -変数 'pi' の値: 3.14 -変数 'pi' のメモリアドレス: 0x7ffee3b8c9a0 -変数 'name' の値: Alice -変数 'name' のメモリアドレス: 0x7ffee3b8c990 +```cpp-exec:basic_pointer.cpp +numberの値: 42 +numberのアドレス (&number): 0x7ffedffe3adc +ptrの値 (アドレス): 0x7ffedffe3adc +ptrが指す中身 (*ptr): 42 +書き換え後のnumber: 100 ``` -## ポインタの正体: アドレスを格納する変数 +※ アドレス(0x...)は実行環境ごとに異なります。 -**ポインタ**とは、この**メモリアドレスを格納するための専用の変数**です。 +### `nullptr` の使用 -変数の型に応じて、対応するポインタの型が存在します。例えば、`int`型の変数のアドレスを格納するなら `int*` 型、`double`型の変数のアドレスを格納するなら `double*` 型のポインタを使います。アスタリスク `*` がポインタ型であることを示します。 +ポインタが「どこも指していない」ことを示したい場合、C++では `nullptr` を使用します。 +古いC++やC言語では `NULL` や `0` が使われていましたが、モダンC++では型安全な `nullptr` を使うのが鉄則です。初期化されていないポインタは不定な場所を指すため、必ず初期化しましょう。 ```cpp:pointer_declaration.cpp #include int main() { - // int型の変数のアドレスを格納するためのポインタ変数 - int* p_number; + // ポインタの宣言 + // 初期化していないポインタは不定なアドレスを指す可能性がある。 + int* p; + std::cout << "p の初期値(アドレス): " << p << std::endl; - // double型の変数のアドレスを格納するためのポインタ変数 - double* p_value; + // *p = 10; // 【危険】未初期化ポインタの間接参照は未定義動作 // どの変数も指していないことを示す特別な値 nullptr // ポインタを初期化する際は nullptr を使うのが安全です - int* p_safe = nullptr; + p = nullptr; + std::cout << "p の値(アドレス): " << p << std::endl; - if (p_safe == nullptr) { - std::cout << "p_safe は何も指していません。" << std::endl; + if (p == nullptr) { + std::cout << "p は何も指していません。" << std::endl; } + // *p = 10; // 【危険】nullptrはどこも指していないので、やっぱり未定義動作 + return 0; } ``` ```cpp-exec:pointer_declaration.cpp -p_safe は何も指していません。 +p の初期値(アドレス): 0x7ffedffe3ab8 +p の値(アドレス): 0 +p は何も指していません。 ``` -ポインタ変数の宣言時に `*` を型の横に付けるか、変数名の横に付けるかは好みが分かれますが、意味は同じです (`int* p;` と `int *p;` は等価)。このチュートリアルでは `int* p;` のように型の側に付けます。 +## 配列とポインタの関係 -## ポインタの操作: 間接参照 (`*`) とアドレス取得 (`&`) +第3章では `std::vector` を使いましたが、C++にはC言語互換の「生の配列(Cスタイル配列)」も存在します。これはサイズが固定で、機能が制限されています。 -ポインタを操作するための2つの重要な演算子を学びましょう。 +### 配列名の減衰(Decay) - * **アドレス取得演算子 (`&`)**: 変数の前に付けると、その変数のメモリアドレスを取得できます。 - * **間接参照演算子 (`*`)**: ポインタ変数の前に付けると、そのポインタが指し示しているアドレスに格納されている値を取得(または変更)できます。「参照先の値」を取り出すイメージです。 +実は、配列の名前は式の中で使うと、**「先頭要素へのポインタ」**として扱われます。これを「減衰(Decay)」と呼びます。 -言葉だけだと少し分かりにくいので、コードで見てみましょう。 +### ポインタ演算 -```cpp:pointer_operations.cpp +ポインタに対して数値を足し引きすると、**「その型のサイズ分」**だけアドレスが移動します。 +`int`(通常4バイト)のポインタに `+1` すると、メモリアドレスは4増えます。 + +```cpp:array_decay.cpp #include int main() { - int number = 42; - // int型のポインタ変数 p_number を宣言 - int* p_number; - - // アドレス取得演算子(&)を使って、変数numberのアドレスをp_numberに代入 - p_number = &number; + // Cスタイル配列の宣言(サイズ固定) + int primes[] = {2, 3, 5, 7}; - std::cout << "変数 number の値: " << number << std::endl; - std::cout << "変数 number のアドレス: " << &number << std::endl; - std::cout << "ポインタ p_number の値 (格納しているアドレス): " << p_number << std::endl; + // 配列名 primes は &primes[0] とほぼ同じ意味になる + int* ptr = primes; - // 間接参照演算子(*)を使って、p_numberが指す先の値を取得 - std::cout << "ポインタ p_number が指す先の値: " << *p_number << std::endl; + std::cout << "先頭要素 (*ptr): " << *ptr << 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; + // ポインタ演算 + // ptr + 1 は次のint要素(メモリ上で4バイト隣)を指す + std::cout << "2番目の要素 (*(ptr + 1)): " << *(ptr + 1) << std::endl; + + // 配列添字アクセス primes[2] は、実は *(primes + 2) のシンタックスシュガー + std::cout << "3番目の要素 (primes[2]): " << primes[2] << std::endl; + std::cout << "3番目の要素 (*(primes + 2)): " << *(primes + 2) << std::endl; return 0; } ``` -```cpp-exec:pointer_operations.cpp -変数 number の値: 42 -変数 number のアドレス: 0x7ffc... -ポインタ p_number の値 (格納しているアドレス): 0x7ffc... -ポインタ p_number が指す先の値: 42 - ---- ポインタ経由で値を変更 --- -変更後の変数 number の値: 100 -ポインタ p_number が指す先の値: 100 +```cpp-exec:array_decay.cpp +先頭要素 (*ptr): 2 +2番目の要素 (*(ptr + 1)): 3 +3番目の要素 (primes[2]): 5 +3番目の要素 (*(primes + 2)): 5 ``` -実行結果のアドレス (`0x7ffc...`の部分) は、実行するたびに変わる可能性があります。 - -この例から、`p_number = &number;` によって `p_number` が `number` を指すようになり、`*p_number` を使うことで `number` の中身を読み書きできることが分かります。`*p_number` は `number` とほぼ同じように扱えるわけです。 - -## 動的なメモリ確保: `new` と `delete` - -これまでの変数は、プログラムの実行前にコンパイラが必要なメモリ領域(**スタック領域**)を確保していました。しかし、プログラム実行中に、必要な分だけメモリを確保したい場合があります。例えば、ユーザーの入力に応じて、可変長のデータを保存するようなケースです。 +## 文字列の正体(Legacy) -このように、プログラムの実行中に動的にメモリを確保する仕組みが用意されており、確保される領域は**ヒープ領域**(またはフリーストア)と呼ばれます。 +`std::string` が登場する前、文字列は単なる `char` 型の配列でした。これを「Cスタイル文字列」と呼びます。 +現在でも、ライブラリとの連携などで頻繁に目にします。 - * **`new`**: ヒープ領域から指定した型のメモリを確保し、その領域へのポインタを返します。 - * **`delete`**: `new` で確保したメモリを解放します。 +### 文字列リテラルと `char*` -`new` で確保したメモリは、自動的には解放されません。自分で責任を持って `delete` を使って解放する必要があります。これを怠ると、**メモリリーク**(確保したメモリが解放されずに残り続け、利用可能なメモリがどんどん減っていくバグ)の原因となります。 +Cスタイル文字列は、文字の並びの最後に「終端文字 `\0`(ヌル文字)」を置くことで終わりを表します。 -```cpp:dynamic_memory.cpp +```cpp:legacy_string.cpp #include +#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; + // 文字列リテラルは const char 配列 + const char* c_str = "Hello"; - // 解放後のポインタはどこを指しているか分からない危険な状態(ダングリングポインタ) - // なので、nullptr を代入して安全な状態にしておくのが良い習慣 - p_int = nullptr; + // std::string から Cスタイル文字列への変換 + std::string cpp_str = "World"; + const char* converted = cpp_str.c_str(); // .c_str() を使う + std::cout << "C-Style: " << c_str << std::endl; + std::cout << "C++ String: " << cpp_str << std::endl; + std::cout << "Converted to C-Style: " << converted << std::endl; + + // 注意: c_str は配列なのでサイズ情報を持っていない + // 終端文字 '\0' まで読み進める必要がある + return 0; } ``` -```cpp-exec:dynamic_memory.cpp -確保したメモリが指す先の値: 2025 -格納されているアドレス: 0x55a1... +```cpp-exec:legacy_string.cpp +C-Style: Hello +C++ String: World +Converted to C-Style: World ``` -`new` と `delete` は必ずペアで使います。図書館で本を借りたら(`new`)、必ず返却カウンターに返す(`delete`)のと同じです。返さなければ、他の人がその本を借りられなくなってしまいますよね。 +**重要:** モダンC++では基本的に `std::string` を使いましょう。`char*` は参照用やAPI互換のために使います。 -## ポインタと配列: 切っても切れない関係性 +## 動的なメモリ確保 -C++では、Cスタイルの配列とポインタは非常に密接な関係にあります。実は、**配列名はその配列の先頭要素を指すポインタ**として扱うことができます。 +ここがメモリ管理の核心です。プログラムが使うメモリ領域には大きく分けて「スタック」と「ヒープ」があります。 -```cpp:pointer_and_array.cpp -#include +### スタック (Stack) -int main() { - int numbers[] = {10, 20, 30, 40, 50}; + * これまでの変数は主にここに置かれます。 + * 関数のスコープ `{ ... }` を抜けると**自動的に消滅**します。 + * 管理が楽で高速ですが、サイズに制限があります。 - // 配列名は先頭要素へのポインタとして扱える - int* p_numbers = numbers; +### ヒープ (Heap) - std::cout << "numbers[0] の値: " << numbers[0] << std::endl; - std::cout << "p_numbers が指す先の値: " << *p_numbers << std::endl; + * プログラマが**手動で確保・解放**する領域です。 + * 広大なサイズを使えますが、管理を怠ると危険です。 - // ポインタの指すアドレスを1つ進める(次の要素を指す) - p_numbers++; +### `new` と `delete` - std::cout << "ポインタをインクリメントした後、p_numbers が指す先の値: " << *p_numbers << std::endl; - std::cout << "これは numbers[1] と同じ: " << numbers[1] << std::endl; +ヒープ領域を使うには `new` 演算子を使用し、使い終わったら必ず `delete` 演算子でメモリをOSに返却(解放)する必要があります。 + +```cpp:heap_memory.cpp +#include + +int main() { + // ヒープ上に整数を1つ確保 + int* pInt = new int(10); - // ポインタとオフセットで配列要素にアクセス - // *(numbers + 2) は numbers[2] と等価 - std::cout << "*(numbers + 2) の値: " << *(numbers + 2) << std::endl; + // ヒープ上に配列を確保 (サイズ100) + // std::vectorを使わない場合、サイズは動的に決められるが管理は手動 + int size = 5; + int* pArray = new int[size]; + + // 配列への書き込み + for(int i = 0; i < size; ++i) { + pArray[i] = i * 10; + } + + std::cout << "ヒープ上の値: " << *pInt << std::endl; + std::cout << "ヒープ上の配列[2]: " << pArray[2] << std::endl; + + // 【重要】使い終わったら必ず解放する! + delete pInt; // 単体の解放 + delete[] pArray; // 配列の解放 (delete[] を使うこと) + + // 解放後のアドレスには触ってはいけない(ダングリングポインタ) + // 安全のため nullptr にしておく + pInt = nullptr; + pArray = nullptr; return 0; } ``` -```cpp-exec:pointer_and_array.cpp -numbers[0] の値: 10 -p_numbers が指す先の値: 10 -ポインタをインクリメントした後、p_numbers が指す先の値: 20 -これは numbers[1] と同じ: 20 -*(numbers + 2) の値: 30 +```cpp-exec:heap_memory.cpp +ヒープ上の値: 10 +ヒープ上の配列[2]: 20 ``` -`p_numbers++` のようにポインタをインクリメントすると、ポインタは「次の要素」を指すようになります。`int`型ポインタなら4バイト(環境による)、`double`型ポインタなら8バイトといったように、指している型に応じて適切なバイト数だけアドレスが進みます。これを**ポインタ演算**と呼びます。 +### 恐怖の「メモリリーク」 -この仕組みにより、ポインタを使って配列の要素を順に辿っていくことができます。ただし、配列の範囲外を指すポインタ(例えば、5つの要素を持つ配列の6番目を指すなど)を間接参照してしまうと、未定義の動作を引き起こし、プログラムがクラッシュする原因になるため、細心の注意が必要です。 +もし `delete` を忘れるとどうなるでしょう? +確保されたメモリは、プログラムが終了するまで「使用中」のまま残ります。これを**メモリリーク**と呼びます。長時間動くサーバーなどでこれが起きると、メモリを食いつぶしてシステムがクラッシュします。 -## この章のまとめ +**第3章の振り返り:** +`std::vector` や `std::string` は、内部で `new` と `delete` を自動的に行ってくれています。 - * **ポインタ**は、変数の**メモリアドレス**を格納する特殊な変数です。 - * `&` 演算子で変数のアドレスを取得し、ポインタに代入します。 - * `*` 演算子でポインタが指す先の値にアクセス(読み書き)します。 - * `new` を使うと、プログラム実行中に**ヒープ領域**から動的にメモリを確保できます。 - * `new` で確保したメモリは、使い終わったら必ず `delete` で解放しなければならず、怠ると**メモリリーク**になります。 - * 配列名は、その配列の先頭要素を指すポインタとして扱うことができます。 + * 作成時に `new` で確保。 + * スコープを抜けるときに自動で `delete`(デストラクタ)。 + これがC++のクラスの強力な機能(RAII)です。生の `new/delete` を直接使うことは、モダンC++では「最後の手段」あるいは「ライブラリを作る側の仕事」と考えられています。 + + +## この章のまとめ -ポインタはC++の強力な機能ですが、使い方を誤ると厄介なバグの原因にもなります。しかし、この仕組みを理解することは、C++をより深く知る上で不可欠です。後の章で学ぶスマートポインタなど、現代のC++ではポインタをより安全に扱うための機能も提供されています。 +1. **ポインタ**はメモリアドレスを保持する変数。`&`で取得、`*`でアクセス。 +2. ポインタの初期化には `nullptr` を使う。 +3. **配列名**は先頭要素へのポインタとして振る舞う(減衰)。 +4. `ptr + i` は、`ptr` の指す型 `i` 個分先のアドレスを指す。 +5. **ヒープメモリ**は `new` で確保し、必ず `delete` で解放する。 +6. `delete` を忘れると**メモリリーク**になる。これを防ぐために `std::vector` などのコンテナクラスが存在する。 -### 練習問題1 +## 練習問題1: ポインタによる配列操作 -`double` 型の変数 `pi` を `3.14159` で初期化してください。次に、`double*` 型のポインタ `p_pi` を宣言し、`pi` のアドレスを代入してください。最後に、ポインタ `p_pi` を使って `pi` の値を `2.71828` に変更し、変数 `pi` の値が変更されたことを確認するプログラムを書いてください。 +`int` 型のCスタイル配列 `arr` について、 `int*` 型のポインタを使って走査し、**すべての値を2倍に書き換えてください**(`[]` 演算子は使わず、ポインタ演算 `*` と `++` または `+` を使用すること)。 ```cpp:practice4_1.cpp #include int main() { - + int arr[] = {10, 20, 30, 40, 50}; + + // ここにコードを書く + + + std::cout << "配列の値を2倍にしました: "; + for (int i = 0; i < 5; ++i) { + std::cout << arr[i] << " "; + } + return 0; } ``` ```cpp-exec:practice4_1.cpp -3.14159 -2.71828 +配列の値を2倍にしました: 60 80 100 120 140 ``` -### 練習問題2 - -2つの整数を足し算する関数 `add` を作成し、その結果をポインタを使って返すプログラムを作成してください。 +### 問題2:手動メモリ管理の体験 -1. `int* add(int a, int b)` というシグネチャの関数を定義します。 -2. 関数内で `new` を使って `int` 型のメモリを動的に確保します。 -3. その確保したメモリに `a + b` の計算結果を格納します。 -4. 確保したメモリへのポインタを返します。 -5. `main` 関数でこの `add` 関数を呼び出し、結果を受け取るポインタ変数を宣言します。 -6. ポインタを間接参照して計算結果を出力します。 -7. 最後に、`main` 関数内で `delete` を使って、`add` 関数内で確保されたメモリを解放するのを忘れないでください。 +`n` 個の整数を格納できる配列を**ヒープ領域(`new`)**に確保してください。 +その配列に 0 から `n-1` までの数値を代入し、合計値を計算して表示してください。 +最後に、確保したメモリを適切に解放してください。 ```cpp:practice4_2.cpp #include -int* add(int a, int b) { - -} - int main() { + const int n = 5; + + // ここにコードを書く + + return 0; } ``` ```cpp-exec:practice4_2.cpp -7 +配列の合計値は: 10 ``` diff --git a/public/docs/cpp-5.md b/public/docs/cpp-5.md index d707c12..1428ee7 100644 --- a/public/docs/cpp-5.md +++ b/public/docs/cpp-5.md @@ -1,340 +1,349 @@ -# 第5章: オブジェクト指向の入口:クラスの基礎 +# 第5章: 関数の設計とデータの受け渡し:コピー、参照、ポインタ -これまでの章では、C++の基本的な文法やメモリの扱い方について学んできました。この章からは、C++の最も強力な機能の一つである**オブジェクト指向プログラミング (Object-Oriented Programming, OOP)** の世界に足を踏み入れます。OOPの考え方を身につけることで、より大規模で複雑なプログラムを、現実世界の「モノ」の概念に近い形で、直感的に設計・実装できるようになります。その第一歩として、OOPの中核をなす**クラス**の基礎を学びましょう。 +前章(第4章)では、C++のメモリモデルの核心である「ポインタ」について学びました。ポインタは強力ですが、構文が複雑になりがちで、バグの温床にもなりえます。 -## クラスとは?: データ(メンバ変数)と処理(メンバ関数)のカプセル化 +C++では、C言語から受け継いだポインタに加え、より安全で直感的な**「参照(Reference)」**という概念が導入されています。本章では、関数の設計を通して、この「参照」がいかに強力な武器になるかを学びます。「データをどう渡すか」は、C++のパフォーマンスと設計の良し悪しを決める最も重要な要素の一つです。 -他のプログラミング言語でオブジェクト指向に触れたことがあるなら、「クラスはオブジェクトの設計図」という説明を聞いたことがあるかもしれません。C++でもその考え方は同じです。クラスは、ある「モノ」が持つべき**データ(属性)**と、そのデータに対する**処理(操作)**を一つにまとめたものです。 +## 関数の宣言と定義 - - **データ(属性)**: クラス内に定義された変数のことで、**メンバ変数 (member variables)** または**データメンバ**と呼びます。 - - **処理(操作)**: クラス内に定義された関数のことで、**メンバ関数 (member functions)** または**メソッド**と呼びます。 +PythonやJavaScriptのような言語では、関数をどこに書いても(あるいは実行時に解決されて)呼び出せることが多いですが、C++のコンパイラはコードを上から下へと一直線に読みます。そのため、**「使用する前に、その関数が存在すること」**をコンパイラに知らせる必要があります。 -このように、関連するデータと処理を一つのクラスにまとめることを、OOPの重要な概念の一つである**カプセル化 (encapsulation)** と呼びます。💊 +### プロトタイプ宣言 -例として、「人」を表す`Person`クラスを考えてみましょう。「人」は「名前」や「年齢」といったデータ(メンバ変数)を持ち、「自己紹介する」といった処理(メンバ関数)を行うことができます。 +関数を `main` 関数の後に定義したい場合、事前に「こういう名前と引数の関数がありますよ」と宣言だけしておく必要があります。これを**プロトタイプ宣言**と呼びます。 -```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 +```cpp:declaration_intro.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; - } -}; +// プロトタイプ宣言 +// 戻り値の型 関数名(引数の型1 引数名1, 引数の型2 引数名2, ...); +// 本体({}の中身)は書かず、セミコロンで終わる +void greet(int times); 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." と出力される - + std::cout << "main関数開始" << std::endl; + + // 定義は下にあるが、宣言があるので呼び出せる + greet(3); + return 0; } + +// 関数の定義 +void greet(int times) { + for (int i = 0; i < times; ++i) { + std::cout << "Hello C++!" << std::endl; + } +} ``` -```cpp-exec:instantiation.cpp -My name is Taro, and I am 30 years old. -My name is Hanako, and I am 25 years old. +```cpp-exec:declaration_intro.cpp +main関数開始 +Hello C++! +Hello C++! +Hello C++! ``` -このように、`クラス名 インスタンス名;` という形でインスタンスを生成できます。インスタンスのメンバ変数やメンバ関数にアクセスするには、`インスタンス名.メンバ名` のように**ドット演算子 (`.`)** を使います。`taro`と`hanako`は同じ`Person`クラスから作られたインスタンスですが、それぞれが独立したデータを持っていることがわかります。 +実際の開発では、プロトタイプ宣言をヘッダーファイル(`.h`)に書き、定義をソースファイル(`.cpp`)に書くことで、大規模なプログラムを管理します(これについては次章で詳しく解説します)。 -## アクセス制御: public と private による情報の隠蔽 +### 戻り値がない場合: `void`型 -先ほどの`Person`クラスの例では、`main`関数から`taro.age = 30;`のようにメンバ変数に直接アクセスできました。これは手軽ですが、問題を引き起こす可能性があります。例えば、年齢にマイナスの値や非現実的な値を設定できてしまうかもしれません。 +関数が何も値を返す必要がない場合もあります。例えば、「画面にメッセージを表示するだけ」といった関数です。その場合、戻り値の型として `void` という特別なキーワードを使います。 ```cpp -Person jiro; -jiro.name = "Jiro"; -jiro.age = -5; // 本来ありえない値が設定できてしまう! -jiro.introduce(); +void printMessage(std::string message); ``` -このような意図しない操作を防ぐために、C++には**アクセス制御**の仕組みがあります。クラスのメンバは、外部からのアクセスの可否を指定できます。 +第2章で学んだように、`int`や`double`などの型は変数を定義するために使えましたが、`void`は「型がない」ことを示す特殊な型なので、`void my_variable;` のように変数を定義することはできません。あくまで関数の戻り値の型としてのみ使います。 - - **`public`**: クラスの外部(`main`関数など)から自由にアクセスできます。 - - **`private`**: そのクラスのメンバ関数からしかアクセスできません。外部からはアクセス不可です。 +## 引数の渡し方(パフォーマンスと安全性) -アクセス制御の基本は、**メンバ変数は`private`にし、メンバ関数は`public`にする**ことです。これにより、データの不正な書き換えを防ぎ、クラスの内部実装を外部から隠蔽します。これを**情報の隠蔽 (information hiding)** と呼び、カプセル化の重要な目的の一つです。 +ここが本章のハイライトです。他の言語では言語仕様として決まっていることが多い引数の渡し方を、C++ではプログラマが意図的に選択できます。 -`private`なメンバ変数に安全にアクセスするために、`public`なメンバ関数(**ゲッター**や**セッター**と呼ばれる)を用意するのが一般的です。 +### 1\. 値渡し (Pass by Value) -```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; - } - } +```cpp:pass_by_value.cpp +#include - // ゲッター: メンバ変数の値を取得する - std::string getName() const { - return name; - } +// 値渡し:xは呼び出し元のコピー +void attemptUpdate(int x) { + x = 100; // コピーを変更しているだけ + std::cout << "関数内: " << x << " (アドレス: " << &x << ")" << std::endl; +} - int getAge() const { - return age; - } +int main() { + int num = 10; + std::cout << "呼び出し前: " << num << " (アドレス: " << &num << ")" << std::endl; + + attemptUpdate(num); + + // numは変わっていない + std::cout << "呼び出し後: " << num << std::endl; + return 0; +} +``` - // このメンバ関数はクラス内部にあるので、privateメンバにアクセスできる - void introduce() { - std::cout << "My name is " << name << ", and I am " << age << " years old." << std::endl; - } -}; +```cpp-exec:pass_by_value.cpp +呼び出し前: 10 (アドレス: 0x7ff...) +関数内: 100 (アドレス: 0x7ff...) <-- アドレスが違う=別の領域(コピー) +呼び出し後: 10 +``` -int main() { - Person saburo; +### 2\. ポインタ渡し (Pass by Pointer) - // saburo.name = "Saburo"; // エラー! privateメンバには直接アクセスできない - // saburo.age = -10; // エラー! +C言語からある手法です。第4章で学んだポインタ(アドレス)を渡します。 - // publicなメンバ関数を経由して安全に値を設定 - saburo.setName("Saburo"); - saburo.setAge(28); + * **メリット:** コピーが発生しない(アドレス値のコピーのみ)。呼び出し元のデータを変更できる。 + * **デメリット:** 呼び出す際に `&` を付ける必要がある。関数内で `*` や `->` を使う必要があり、構文が汚れる。`nullptr` チェックが必要になることがある。 - saburo.introduce(); + - saburo.setAge(-10); // エラーメッセージが出力される +```cpp:pass_by_pointer.cpp +#include - // publicなメンバ関数経由で値を取得 - std::cout << "Name: " << saburo.getName() << std::endl; +// ポインタ渡し:アドレスを受け取る +void updateByPointer(int* ptr) { + if (ptr != nullptr) { + *ptr = 200; // アドレスの指す先を書き換える + } +} +int main() { + int num = 10; + + // アドレスを渡す + updateByPointer(&num); + + std::cout << "ポインタ渡し後: " << num << 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 +```cpp-exec:pass_by_pointer.cpp +ポインタ渡し後: 200 ``` -`setAge`関数内で値の妥当性チェックを行っている点に注目してください。このように、クラスの利用者は内部の実装を気にすることなく、提供された`public`なインターフェース(メンバ関数)を通じて安全にオブジェクトを操作できます。 +### 3\. 参照渡し (Pass by Reference) + +C++の真骨頂です。**「参照(Reference)」**とは、既存の変数に別の名前(エイリアス)をつける機能です。引数の型に `&` を付けるだけで宣言できます。 + + * **メリット:** コピーが発生しない。**構文は「値渡し」と同じように書ける**(`*`や`&`を呼び出し側で意識しなくていい)。`nullptr` になることがないため安全性が高い。 + * **デメリット:** 関数内で値を変更すると、呼び出し元も変わる(意図しない変更に注意)。 + + + +```cpp:pass_by_ref.cpp +#include -> `const`キーワード: `getName() const` のようにメンバ関数の後ろに`const`を付けると、その関数がメンバ変数を変更しないことをコンパイラに約束します。このような関数を**constメンバ関数**と呼びます。 +// 参照渡し:引数に & をつける +// ref は呼び出し元の変数の「別名」となる +void updateByRef(int& ref) { + ref = 300; // 普通の変数のように扱えるが、実体は呼び出し元 +} -## コンストラクタとデストラクタ: オブジェクトが生まれてから消えるまで +int main() { + int num = 10; + + // 値渡しと同じように呼び出せる(&num と書かなくていい!) + updateByRef(num); + + std::cout << "参照渡し後: " << num << std::endl; + return 0; +} +``` -オブジェクトは生成され、利用され、やがて破棄されます。このライフサイクルに合わせて特別な処理を自動的に実行するための仕組みが**コンストラクタ**と**デストラクタ**です。 +```cpp-exec:pass_by_ref.cpp +参照渡し後: 300 +``` -### コンストラクタ (Constructor) +### 4\. const 参照渡し (Pass by const Reference) -**コンストラクタ**は、インスタンスが生成されるときに**自動的に呼び出される**特別なメンバ関数です。主な役割は、メンバ変数の初期化です。 +これが**C++で最も頻繁に使われるパターン**です。「コピーはしたくない(重いから)。でも、関数内で書き換えられたくもない」という要求を満たします。 -コンストラクタには以下の特徴があります。 + * **構文:** `const 型& 引数名` + * **用途:** `std::string`、`std::vector`、クラスのオブジェクトなど、サイズが大きくなる可能性があるデータ。 - - 関数名がクラス名と全く同じ。 - - 戻り値の型を指定しない(`void`も付けない)。 - - 引数を取ることができ、複数定義できる(オーバーロード)。 + -```cpp:constructor.cpp -class Person { -private: - std::string name; - int age; +```cpp:const_ref.cpp +#include +#include +#include -public: - // 引数付きコンストラクタ - Person(const std::string& initName, int initAge) { - std::cout << "Constructor called for " << initName << std::endl; - name = initName; - age = initAge; - } - // ... -}; +// const参照渡し +// textの実体はコピーされないが、書き換えも禁止される +void printMessage(const std::string& text) { + // text = "Modified"; // コンパイルエラーになる + std::cout << "Message: " << text << std::endl; +} int main() { - // インスタンス生成時にコンストラクタが呼ばれ、引数が渡される - Person yuko("Yuko", 22); // この時点でコンストラクタが実行される - yuko.introduce(); + std::string bigData = "This is a potentially very large string..."; + + // コピーコストゼロで渡す + printMessage(bigData); + + return 0; } ``` -```cpp-exec:constructor.cpp -Constructor called for Yuko -My name is Yuko, and I am 22 years old. +```cpp-exec:const_ref.cpp +Message: This is a potentially very large string... ``` -このように、インスタンス生成時に`()`で初期値を渡すことで、オブジェクトを生成と同時に有効な状態にできます。`set`関数を別途呼び出す手間が省け、初期化忘れを防ぐことができます。 +> **ガイドライン:** +> +> * `int` や `double` などの基本型 → **値渡し** でOK。 +> * 変更させたいデータ → **参照渡し** (`T&`)。 +> * 変更しないがサイズが大きいデータ(string, vectorなど) → **const参照渡し** (`const T&`)。 -### デストラクタ (Destructor) +## 関数の機能拡張 -**デストラクタ**は、インスタンスが破棄されるとき(例えば、変数のスコープを抜けるとき)に**自動的に呼び出される**特別なメンバ関数です。主な役割は、オブジェクトが使用していたリソース(メモリやファイルなど)の後片付けです。 +C++には関数をより柔軟に使うための機能が備わっています。 -デストラクタには以下の特徴があります。 +### オーバーロード (Function Overloading) - - 関数名が `~` + クラス名。 - - 戻り値も引数も取らない。 - - 1つのクラスに1つしか定義できない。 +引数の**型**や**数**が異なれば、同じ名前の関数を複数定義できます。C言語では関数名はユニークである必要がありましたが、C++では「名前+引数リスト」で区別されます。 -```cpp:constructor_destructor.cpp +```cpp:overloading.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; - } +// int型を受け取る関数 +void print(int i) { + std::cout << "Integer: " << i << std::endl; +} - void introduce() { - std::cout << "My name is " << name << ", and I am " << age << " years old." << std::endl; - } -}; +// double型を受け取る関数(同名) +void print(double d) { + std::cout << "Double: " << d << 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のスコープが終わり、デストラクタが呼ばれる +// 文字列を受け取る関数(同名) +void print(const std::string& s) { + std::cout << "String: " << s << std::endl; +} int main() { - create_person_scope(); - - std::cout << "--- Back in main ---" << std::endl; - + print(42); + print(3.14); + print("Overloading"); 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 --- +```cpp-exec:overloading.cpp +Integer: 42 +Double: 3.14 +String: Overloading ``` -実行結果を見ると、`kenji`オブジェクトが生成されたときにコンストラクタが、`create_person_scope`関数のスコープを抜けるときにデストラクタが自動的に呼び出されていることがわかります。動的に確保したメモリの解放など、クリーンアップ処理はデストラクタに書くのが定石です。この考え方は、今後の章で学ぶRAII(Resource Acquisition Is Initialization)という重要な概念に繋がります。 +### デフォルト引数 -## この章のまとめ +引数が省略された場合に使われるデフォルト値を設定できます。これはプロトタイプ宣言(または最初にコンパイラが見る定義)に記述します。 +※デフォルト引数は**後ろの引数から順に**設定する必要があります。 -この章では、C++におけるオブジェクト指向プログラミングの第一歩として、クラスの基本的な概念を学びました。 +```cpp:default_args.cpp +#include - - **クラス**は、データ(**メンバ変数**)と処理(**メンバ関数**)を一つにまとめた「設計図」です。 - - クラスから実体である**オブジェクト(インスタンス)**を生成して使用します。 - - **カプセル化**は、関連するデータと処理をまとめることです。 - - **アクセス制御**(`public`, `private`)により、外部からアクセスされたくないメンバを保護します(**情報の隠蔽**)。 - - **コンストラクタ**は、オブジェクト生成時に自動で呼ばれ、初期化を行います。 - - **デストラクタ**は、オブジェクト破棄時に自動で呼ばれ、後片付けを行います。 +// power: 指数を省略すると2乗になる +// verbose: 詳細出力を省略するとfalseになる +int power(int base, int exponent = 2, bool verbose = false) { + int result = 1; + for (int i = 0; i < exponent; ++i) { + result *= base; + } + + if (verbose) { + std::cout << base << " の " << exponent << " 乗を計算しました。" << std::endl; + } + return result; +} -クラスを使いこなすことで、プログラムの部品化が進み、再利用性やメンテナンス性が格段に向上します。次の章では、クラスのさらに進んだ機能について学んでいきましょう。 +int main() { + std::cout << power(3) << std::endl; // 3^2, verbose=false + std::cout << power(3, 3) << std::endl; // 3^3, verbose=false + std::cout << power(2, 4, true) << std::endl; // 2^4, verbose=true + return 0; +} +``` -### 練習問題1: 長方形クラス +```cpp-exec:default_args.cpp +9 +27 +2 の 4 乗を計算しました。 +16 +``` + +## この章のまとめ -幅(`width`)と高さ(`height`)をメンバ変数として持つ`Rectangle`クラスを作成してください。 + * **プロトタイプ宣言**を使うことで、関数の定義順序に依存せずに記述できる。 + * **値渡し**は安全だが、大きなオブジェクトではコピーコストがかかる。 + * **参照渡し (`&`)** は、ポインタのような効率性を持ちながら、変数のエイリアスとして直感的に扱える。 + * **`const` 参照渡し (`const T&`)** は、大きなデータを「読み取り専用」で効率的に渡すC++の定石である。 + * **オーバーロード**により、同じ名前で異なる引数を受け取る関数を作れる。 + * **デフォルト引数**で、呼び出し時の記述を省略できる。 - - メンバ変数は`private`で定義してください。 - - コンストラクタで幅と高さを初期化できるようにしてください。 - - 面積を計算して返す`getArea()`メソッドと、周の長さを計算して返す`getPerimeter()`メソッドを`public`で実装してください。 - - `main`関数で`Rectangle`クラスのインスタンスをいくつか生成し、面積と周の長さを表示するプログラムを作成してください。 +## 練習問題1: 値の入れ替え(Swap) + +2つの `int` 変数を受け取り、その値を入れ替える関数 `mySwap` を作成してください。 +ポインタではなく、**参照渡し**を使用してください。 ```cpp:practice5_1.cpp #include -#include -// ここにRectangleクラスを定義してください -int main() { - // ここでRectangleクラスのインスタンスを生成し、面積と周の長さを表示してください +// ここにmySwap関数を実装してください + +// main関数 +int main() { + int a = 10; + int b = 20; + std::cout << "Before: a = " << a << ", b = " << b << std::endl; + mySwap(a, b); + std::cout << "After: a = " << a << ", b = " << b << std::endl; return 0; } ``` ```cpp-exec:practice5_1.cpp +(期待される実行結果) +Before: a = 10, b = 20 +After: a = 20, b = 10 ``` +### 問題2:ベクター統計 -### 練習問題2: 書籍クラス +`std::vector` を受け取り、その中の「最大値」を見つけて返す関数 `findMax` を作成してください。 +ただし、以下の条件を守ってください。 -タイトル(`title`)、著者(`author`)、ページ数(`pages`)をメンバ変数として持つ`Book`クラスを作成してください。 +1. ベクターはコピーされないようにしてください(**参照渡し**)。 +2. 関数内でベクターの内容が変更されないことを保証してください(**const**)。 +3. ベクターが空の場合は `0` を返すなどの処理を入れてください。 - - メンバ変数は`private`で定義してください。 - - コンストラクタで、タイトル、著者、ページ数を初期化できるようにしてください。 - - 本の情報を整形してコンソールに出力する`printInfo()`メソッドを`public`で実装してください。(例: `Title: [タイトル], Author: [著者], Pages: [ページ数] pages`) - - `main`関数で`Book`クラスのインスタンスを生成し、その情報を表示してください。 + ```cpp:practice5_2.cpp #include -#include -// ここにBookクラスを定義してください +#include +#include // maxを使うなら便利ですが、for文でも可 + +// ここに findMax を作成 -int main() { - // ここでBookクラスのインスタンスを生成し、情報を表示してください +int main() { + std::vector data = {10, 5, 8, 42, 3}; + std::cout << "Max: " << findMax(data) << std::endl; return 0; } ``` - ```cpp-exec:practice5_2.cpp -Title: The Great Gatsby, Author: F. Scott Fitzgerald, Pages: 180 pages +Max: 42 ``` diff --git a/public/docs/cpp-6.md b/public/docs/cpp-6.md index afb6046..93ba385 100644 --- a/public/docs/cpp-6.md +++ b/public/docs/cpp-6.md @@ -1,503 +1,293 @@ -# 第6章: クラスを使いこなす +# 第6章: プロジェクトの分割とビルド -第5章では、C++のオブジェクト指向プログラミングの核となる`class`の基本的な使い方を学びました。しかし、クラスを真に強力なツールとして使いこなすには、もう少し知識が必要です。この章では、オブジェクトのコピー、演算子のオーバーロード、クラスで共有されるメンバなど、より実践的でパワフルな機能について掘り下げていきます。これらの概念をマスターすることで、あなたの書くクラスはより安全で、直感的で、再利用性の高いものになるでしょう。 +これまでの章では、すべてのコードを1つの `.cpp` ファイルに記述してきました。しかし、プログラムが大規模で複雑になるにつれて、このアプローチは現実的ではなくなります。コードの可読性が低下し、少しの変更でもプログラム全体の再コンパイルが必要になり、開発効率が大きく損なわれるからです。 -## オブジェクトのコピー: コピーコンストラクタと代入演算子 +この章では、プログラムを複数のファイルに分割し、それらを効率的に管理・ビルドする方法を学びます。これは、小さなプログラムから一歩進み、本格的なソフトウェア開発を行うための重要なステップです。 -オブジェクトをコピーしたい場面は頻繁にあります。例えば、関数の引数にオブジェクトを渡すとき(値渡し)や、既存のオブジェクトで新しいオブジェクトを初期化するときなどです。 +## ヘッダファイルとソースファイル -```cpp -Vector2D v1(1.0, 2.0); -Vector2D v2 = v1; // ここでコピーが発生! -``` +C++では、プログラムを**ヘッダファイル**と**ソースファイル**という2種類のファイルに分割するのが一般的です。 -多くの場合、コンパイラが自動的に生成するコピー機能で十分です。しかし、クラスがポインタなどでリソース(メモリなど)を管理している場合、単純なコピーでは問題が発生します。 + * **ヘッダファイル (`.h` または `.hpp`)**: 「宣言」を置く場所です。クラスの定義、関数のプロトタイプ宣言、定数、テンプレートなどを記述します。他のファイルに対して「何ができるか(インターフェース)」を公開する役割を持ちます。 + * **ソースファイル (`.cpp`)**: 「実装」を置く場所です。ヘッダファイルで宣言された関数の具体的な処理内容などを記述します。ヘッダファイルが公開したインターフェースを「どのように実現するか」を記述する役割を持ちます。 -### 何もしないとどうなる? (浅いコピー) +### なぜ分割するのか? 🤔 -まず、コピーの機能を自分で作らなかった場合に何が起きるか見てみましょう。 -コンパイラは、メンバ変数を単純にコピーするだけの「浅いコピー」を行います。 +1. **関心の分離**: インターフェース(何ができるか)と実装(どうやるか)を分離することで、コードの見通しが良くなります。他の開発者はヘッダファイルを見るだけで、その機能の使い方がわかります。 +2. **コンパイル時間の短縮**: ソースファイルを変更した場合、再コンパイルはそのファイルだけで済みます。プロジェクト全体を再コンパイルする必要がないため、大規模なプロジェクトでは開発サイクルが劇的に速くなります。 +3. **再利用性の向上**: よく使う関数やクラスをまとめておけば、別のプロジェクトでそのファイルをインクルードするだけで簡単に再利用できます。 -ここでは、`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:math_utils.h +// 関数の宣言を記述するヘッダファイル -### 解決策:コピー機能を自作する (深いコピー) - -この問題を解決するために、**コピーコンストラクタ**と**コピー代入演算子**を自分で定義して、「深いコピー」を実装します。深いコピーとは、ポインタの指す先の実体(データそのもの)を新しく作ってコピーすることです。 - -```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; // コピーコンストラクタが呼ばれる +// この関数が他のファイルから参照されることを示す +int add(int a, int b); +``` - std::cout << "\n--- rh3の作成 ---" << std::endl; - ResourceHolder rh3(20); +次に、この関数の「実装」をソースファイルに記述します。 - std::cout << "\n--- rh3にrh1を代入 ---" << std::endl; - rh3 = rh1; // コピー代入演算子が呼ばれる +```cpp:math_utils.cpp +// 関数の実装を記述するソースファイル - std::cout << "\n--- 各オブジェクトの状態 ---" << std::endl; - std::cout << "rh1: "; rh1.print(); - std::cout << "rh2: "; rh2.print(); // rh1とは別のメモリを持っている - std::cout << "rh3: "; rh3.print(); // rh1とは別のメモリを持っている +#include "math_utils.h" // 対応するヘッダファイルをインクルード - std::cout << "\n--- main関数終了 ---" << std::endl; - return 0; // ここでrh1, rh2, rh3のデストラクタが呼ばれ、それぞれが確保したメモリを安全に解放する +int add(int a, int b) { + return a + b; } ``` -```cpp-exec:resource_holder.cpp ---- rh1の作成 --- -Resource 10 created. (at 0x139f065e0) +最後に、`main`関数を含むメインのソースファイルから、この`add`関数を呼び出します。 ---- 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 +```cpp:math_app.cpp +#include +#include "math_utils.h" // 自作したヘッダファイルをインクルード ---- main関数終了 --- -Resource 10 destroyed. (at 0x139f06640) -Resource 10 destroyed. (at 0x139f06600) -Resource 10 destroyed. (at 0x139f065e0) +int main() { + int result = add(5, 3); + std::cout << "The result is: " << result << std::endl; + return 0; +} ``` -*(メモリアドレスは実行するたびに変わります)* - -実行結果を見ると、`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);` | +```cpp-exec:math_app.cpp,math_utils.cpp +The result is: 8 +``` -### 実装例 +ここで注目すべき点は、`math_app.cpp`が`add`関数の具体的な実装を知らないことです。`math_utils.h`を通じて「`int`を2つ受け取って`int`を返す`add`という関数が存在する」ことだけを知り、それを利用しています。 -`Vector2D` クラスで `+`(加算)、`==`(等価比較)、`<<`(ストリーム出力)をオーバーロードしてみましょう。 +## インクルードガード -```cpp:operator_overloading.cpp -#include +複数のファイルから同じヘッダファイルがインクルードされる状況はよくあります。例えば、`A.h`が`B.h`をインクルードし、ソースファイルが`A.h`と`B.h`の両方をインクルードするような場合です。 -class Vector2D { -public: - double x, y; +もしヘッダファイルに何の対策もしていないと、同じ内容(クラス定義や関数宣言)が複数回読み込まれ、「再定義」としてコンパイルエラーが発生してしまいます。 - Vector2D(double x = 0.0, double y = 0.0) : x(x), y(y) {} +```cpp:A.h +#include "B.h" // B.hをインクルード - // メンバ関数として + 演算子をオーバーロード - Vector2D operator+(const Vector2D& rhs) const { - return Vector2D(this->x + rhs.x, this->y + rhs.y); - } +// A.hの内容 +``` - // メンバ関数として == 演算子をオーバーロード - bool operator==(const Vector2D& rhs) const { - return (this->x == rhs.x) && (this->y == rhs.y); - } +```cpp:B.h +class B { + // Bクラスの内容 }; +``` -// 非メンバ関数として << 演算子をオーバーロード -// 第1引数が std::ostream& なので、メンバ関数にはできない -std::ostream& operator<<(std::ostream& os, const Vector2D& v) { - os << "(" << v.x << ", " << v.y << ")"; - return os; -} +```cpp:bad_include_app.cpp +#include "A.h" +#include "B.h" // B.hが二重にインクルードされる 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; - } + [[maybe_unused]] B b; // Bクラスを使う 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) +```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 { + | ^ ``` -`operator<<` は、左辺のオペランドが `std::ostream` 型(`std::cout` など)であるため、`Vector2D` のメンバ関数としては定義できません。そのため、非メンバ関数として定義するのが一般的です。 +この問題を解決するのが**インクルードガード**です。インクルードガードは、ヘッダファイルの内容が1つの翻訳単位(ソースファイル)内で一度しか読み込まれないようにするための仕組みです。 -## staticメンバ +### 伝統的なインクルードガード -通常、クラスのメンバ変数はオブジェクトごとに個別のメモリ領域を持ちます。しかし、あるクラスの**全てのオブジェクトで共有したい**情報もあります。例えば、「これまでに生成されたオブジェクトの総数」などです。このような場合、**staticメンバ**を使用します。 +プリプロセッサディレクティブである `#ifndef`, `#define`, `#endif` を使います。 -### staticメンバ変数 +```cpp +#ifndef MATH_UTILS_H // もし MATH_UTILS_H が未定義なら +#define MATH_UTILS_H // MATH_UTILS_H を定義する -`static` キーワードを付けて宣言されたメンバ変数は、特定のオブジェクトに属さず、クラスそのものに属します。そのため、全オブジェクトでただ1つの実体を共有します。これを**クラス変数**と呼ぶこともあります。 +// --- ヘッダファイルの中身 --- +int add(int a, int b); +// ------------------------- - * **宣言**: クラス定義の中で `static` を付けて行います。 - * **定義**: クラス定義の外(ソースファイル)で、メモリ上の実体を確保し、初期化します。 +#endif // MATH_UTILS_H +``` -### staticメンバ関数 + * **最初のインクルード**: `MATH_UTILS_H` は未定義なので、`#define` が実行され、中身が読み込まれます。 + * **2回目以降のインクルード**: `MATH_UTILS_H` は既に定義されているため、`#ifndef` から `#endif` までのすべてが無視されます。 -`static` キーワードを付けて宣言されたメンバ関数は、特定のオブジェクトに依存せずに呼び出せます。そのため、`this` ポインタ(後述)を持ちません。 +マクロ名 (`MATH_UTILS_H`) は、ファイル名に基づいて一意になるように命名するのが慣習です。 - * **アクセス**: staticメンバ変数や他のstaticメンバ関数にはアクセスできますが、非staticなメンバ(インスタンスごとのメンバ変数やメンバ関数)にはアクセスできません。 - * **呼び出し**: `クラス名::関数名()` のように、オブジェクトを生成しなくても呼び出せます。 +### \#pragma once -### 実装例 +より現代的で簡潔な方法として `#pragma once` があります。多くのモダンなコンパイラがサポートしています。 -ゲームに登場する `Player` クラスがあり、現在何人のプレイヤーが存在するかを管理する例を見てみましょう。 +```cpp +#pragma once -```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; - } -}; +std::string to_upper(const std::string& str); +``` -// (3) staticメンバ変数の定義と初期化 -int Player::playerCount = 0; +この一行をヘッダファイルの先頭に書くだけで、コンパイラがそのファイルが一度しかインクルードされないように処理してくれます。特別な理由がない限り、現在では `#pragma once` を使うのが主流です。 -int main() { - // オブジェクトがなくてもstaticメンバ関数を呼び出せる - std::cout << "ゲーム開始時のプレイヤー数: " << Player::getPlayerCount() << std::endl; - std::cout << "---" << std::endl; +## プロジェクトのビルド - Player p1("Alice"); - Player p2("Bob"); +複数のソースファイル(`.cpp`)は、それぞれがコンパイルされて**オブジェクトファイル**(`.o` や `.obj`)になります。その後、**リンカ**がこれらのオブジェクトファイルと必要なライブラリを結合して、最終的な実行可能ファイルを生成します。 - { - Player p3("Charlie"); - std::cout << "現在のプレイヤー数 (p1経由): " << p1.getPlayerCount() << std::endl; - } // p3のスコープが終わり、デストラクタが呼ばれる +この一連の作業を**ビルド**と呼びます。ファイルが増えてくると、これを手動で行うのは非常に面倒です。そこで、ビルド作業を自動化する**ビルドシステム**が使われます。 - std::cout << "---" << std::endl; - std::cout << "ゲーム終了時のプレイヤー数: " << Player::getPlayerCount() << std::endl; +### 手動でのビルド (g++) - return 0; -} -``` +先ほどの`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 -```cpp-exec:static_members.cpp -ゲーム開始時のプレイヤー数: 0 ---- -Alice がゲームに参加しました。現在のプレイヤー数: 1 -Bob がゲームに参加しました。現在のプレイヤー数: 2 -Charlie がゲームに参加しました。現在のプレイヤー数: 3 -現在のプレイヤー数 (p1経由): 3 -Charlie がゲームから退出しました。現在のプレイヤー数: 2 ---- -ゲーム終了時のプレイヤー数: 2 -Alice がゲームから退出しました。現在のプレイヤー数: 1 -Bob がゲームから退出しました。現在のプレイヤー数: 0 +# 3. 実行する +./my_app ``` -`playerCount` は `p1`, `p2`, `p3` の全てで共有されており、一つの値が更新されていることがわかります。 +または、以下のように1回のg++コマンドで複数ソースファイルのコンパイルとリンクを同時に行うこともできます。 -## thisポインタ +```bash +g++ math_app.cpp math_utils.cpp -o my_app +./my_app +``` -非staticなメンバ関数が呼び出されるとき、その関数は「どのオブジェクトに対して呼び出されたか」を知る必要があります。コンパイラは、そのメンバ関数に対して、呼び出し元のオブジェクトのアドレスを暗黙的に渡します。このアドレスを保持するのが `this` ポインタです。 +### Makefileによる自動化 -`this` は、メンバ関数内で使用できるキーワードで、自分自身のオブジェクトを指すポインタです。 +`make`は、ファイルの依存関係と更新ルールを記述した`Makefile`というファイルに従って、ビルドプロセスを自動化するツールです。 -`this` ポインタが主に使われるのは、以下のような場面です。 +以下は、非常にシンプルな`Makefile`の例です。 -1. **メンバ変数と引数の名前が同じ場合** - コンストラクタの初期化子リストを使わない場合など、引数名とメンバ変数名が同じになることがあります。その際、`this->` を付けることでメンバ変数であることを明示できます。 +```makefile +# コンパイラを指定 +CXX = g++ +# コンパイルオプションを指定 +CXXFLAGS = -std=c++17 -Wall - ```cpp - void setX(double x) { - this->x = x; // this->x はメンバ変数, x は引数 - } - ``` +# 最終的なターゲット(実行可能ファイル名) +TARGET = my_app -2. **自分自身の参照やポインタを返す場合** - コピー代入演算子で `return *this;` としたように、オブジェクト自身を返したい場合に使います。これにより、**メソッドチェーン**(`obj.setX(10).setY(20);` のような連続したメソッド呼び出し)が可能になります。 +# ソースファイルとオブジェクトファイル +SRCS = math_app.cpp math_utils.cpp +OBJS = $(SRCS:.cpp=.o) -### 実装例 +# デフォルトのターゲット (makeコマンド実行時に最初に実行される) +all: $(TARGET) -メソッドチェーンを実現する簡単な例を見てみましょう。 +# 実行可能ファイルの生成ルール +$(TARGET): $(OBJS) + $(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS) -```cpp:this_pointer.cpp -#include +# オブジェクトファイルの生成ルール (%.o: %.cpp) +# .cppファイルから.oファイルを作るための汎用ルール +%.o: %.cpp + $(CXX) $(CXXFLAGS) -c $< -o $@ -class Point { -private: - int x, y; +# 中間ファイルなどを削除するルール +clean: + rm -f $(OBJS) $(TARGET) +``` -public: - Point(int x = 0, int y = 0) : x(x), y(y) {} +この`Makefile`があるディレクトリで、ターミナルから`make`と入力するだけで、必要なコンパイルとリンクが自動的に実行されます。`math_app.cpp`だけを変更した場合、`make`は`main.o`だけを再生成し、再リンクするため、ビルド時間が短縮されます。 - // 自身の参照を返すことで、メソッドチェーンを可能にする - Point& setX(int newX) { - this->x = newX; - return *this; // 自分自身の参照を返す - } +### CMakeによるモダンなビルド管理 - Point& setY(int newY) { - this->y = newY; - return *this; // 自分自身の参照を返す - } +`Makefile`は強力ですが、OSやコンパイラに依存する部分があり、複雑なプロジェクトでは管理が難しくなります。 - void print() const { - std::cout << "(" << this->x << ", " << this->y << ")" << std::endl; - } -}; +**CMake**は、`Makefile`やVisual Studioのプロジェクトファイルなどを自動的に生成してくれる、クロスプラットフォーム対応のビルドシステムジェネレータです。`CMakeLists.txt`という設定ファイルに、より抽象的なビルドのルールを記述します。 -int main() { - Point p; +```cmake +# CMakeの最低要求バージョン +cmake_minimum_required(VERSION 3.10) - // メソッドチェーン - p.setX(10).setY(20); +# プロジェクト名を設定 +project(MyAwesomeApp) - p.print(); +# C++の標準バージョンを設定 +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) - return 0; -} +# 実行可能ファイルを追加 +# add_executable(実行ファイル名 ソースファイル1 ソースファイル2 ...) +add_executable(my_app math_app.cpp math_utils.cpp) ``` -```cpp-exec:this_pointer.cpp -(10, 20) -``` +この`CMakeLists.txt`を使ってビルドする一般的な手順は以下の通りです。 -`setX` が `p` 自身の参照を返すため、その返り値に対して続けて `.setY(20)` を呼び出すことができます。 +```bash +# 1. ビルド用の中間ファイルを置くディレクトリを作成し、移動する +mkdir build +cd build -## この章のまとめ +# 2. CMakeを実行して、ビルドシステム(この場合はMakefile)を生成する +cmake .. -この章では、クラスをより効果的に利用するための応用的な機能を学びました。 +# 3. make (または cmake --build .) を実行してビルドする +make - * **オブジェクトのコピー**: ポインタなどリソースを管理するクラスでは、**コピーコンストラクタ**と**コピー代入演算子**を定義し、**深いコピー**を実装することが重要です。これにより、リソースの二重解放などの問題を未然に防ぎます。 - * **演算子のオーバーロード**: `+` や `==` などの演算子を自作クラスに対して定義することで、コードの可読性を高め、直感的な操作を可能にします。 - * **staticメンバ**: `static`メンバ変数やメンバ関数は、クラスの全オブジェクトで共有されるデータや機能を提供します。オブジェクトを生成しなくてもアクセスできるのが特徴です。 - * **thisポインタ**: 非staticメンバ関数内で、呼び出し元のオブジェクト自身を指すポインタです。メンバ変数と引数の区別や、メソッドチェーンの実装に役立ちます。 +# 4. 実行する +./my_app +``` -これらの機能を組み合わせることで、C++のクラスは単なるデータの入れ物から、振る舞いを伴った洗練された部品へと進化します。 +CMakeは、ライブラリの検索、依存関係の管理、テストの実行など、大規模プロジェクトに必要な多くの機能を備えており、現在のC++開発における標準的なツールとなっています。 -### 練習問題1: 複素数クラス +## この章のまとめ -実部 (real) と虚部 (imaginary) を`double`型で持つ複素数クラス `Complex` を作成してください。以下の要件を満たすものとします。 + * **プロジェクトの分割**: プログラムは「宣言」を記述する**ヘッダファイル** (`.h`) と、「実装」を記述する**ソースファイル** (`.cpp`) に分割することで、保守性や再利用性が向上します。 + * **インクルードガード**: ヘッダファイルの多重インクルードによる再定義エラーを防ぐために、`#pragma once` や `#ifndef`/`#define`/`#endif` を使用します。 + * **ビルドシステム**: 複数のファイルをコンパイル・リンクするプロセスを自動化するために、`make` や `CMake` といったツールが使われます。特に **CMake** はクロスプラットフォーム開発におけるデファクトスタンダードです。 -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)` のように表示されるとより良い) +### 練習問題1: 電卓クラスの分割 -```cpp:practice6_1.cpp -#include +`Calculator` というクラスを作成してください。このクラスは、加算、減算、乗算、除算のメンバ関数を持ちます。 -// ここに Complex クラスを実装してください +* `Calculator.h`: `Calculator`クラスの定義を記述します。 +* `Calculator.cpp`: 各メンバ関数の実装を記述します。 +* `practice6_1.cpp`: `Calculator`クラスのインスタンスを作成し、いくつかの計算を行って結果を表示します。 -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; -} -``` +これらのファイルをg++で手動ビルドして、プログラムを実行してください。 -```cpp-exec:practice6_1.cpp -c1: (1 + 2i) -c2: (3 + 4i) -c1 + c2 = (4 + 6i) -c1 * c2 = (-5 + 10i) -``` +```cpp:Calculator.h -### 練習問題2: 動的配列クラスのコピー制御 +``` -整数 (`int`) の動的配列を管理するクラス `IntArray` を作成してください。このクラスは、コンストラクタで指定されたサイズの配列を `new` で確保し、デストラクタで `delete[]` を使って解放します。 +```cpp:Calculator.cpp -この `IntArray` クラスに対して、**深いコピー**を正しく行うための**コピーコンストラクタ**と**コピー代入演算子**を実装してください。 +``` -```cpp:practice6_2.cpp +```cpp:practice6_1.cpp #include - -// ここに IntArray クラスを実装してください +#include "Calculator.h" 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; + 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:practice6_2.cpp -arr1: 0 10 20 30 40 -arr2 (コピー): 0 10 20 30 40 -arr3 (代入): 0 10 20 30 40 +```cpp-exec:practice6_1.cpp,Calculator.cpp +3 + 5 = 8 +10 - 2 = 8 +4 * 7 = 28 +20 / 4 = 5 ``` diff --git a/public/docs/cpp-7.md b/public/docs/cpp-7.md index 221f018..32eb09b 100644 --- a/public/docs/cpp-7.md +++ b/public/docs/cpp-7.md @@ -1,246 +1,310 @@ -# 第7章: 継承とポリモーフィズム +# 第7章: オブジェクト指向の入口:クラスの基礎 -オブジェクト指向プログラミング(OOP)の真の力を解放する時が来ました!💪 この章では、OOPの強力な柱である「**継承 (Inheritance)**」と「**ポリモーフィズム (Polymorphism) / 多態性**」を学びます。これらの概念をマスターすることで、コードの再利用性を高め、柔軟で拡張性の高いプログラムを設計できるようになります。 +これまでの章では、C++の基本的な文法やメモリの扱い方について学んできました。この章からは、C++の最も強力な機能の一つである**オブジェクト指向プログラミング (Object-Oriented Programming, OOP)** の世界に足を踏み入れます。OOPの考え方を身につけることで、より大規模で複雑なプログラムを、現実世界の「モノ」の概念に近い形で、直感的に設計・実装できるようになります。その第一歩として、OOPの中核をなす**クラス**の基礎を学びましょう。 -## クラスの継承 +## クラスとは?: データ(メンバ変数)と処理(メンバ関数)のカプセル化 -**継承**とは、既存のクラス(**親クラス**または**基底クラス**と呼びます)の機能を引き継いで、新しいクラス(**子クラス**または**派生クラス**と呼びます)を作成する仕組みです。これにより、共通の機能を何度も書く必要がなくなり、コードの重複を避けられます。 +他のプログラミング言語でオブジェクト指向に触れたことがあるなら、「クラスはオブジェクトの設計図」という説明を聞いたことがあるかもしれません。C++でもその考え方は同じです。クラスは、ある「モノ」が持つべき**データ(属性)**と、そのデータに対する**処理(操作)**を一つにまとめたものです。 -例えば、「動物」という大まかなクラスがあり、その特徴を引き継いで「犬」や「猫」といった具体的なクラスを作ることができます。「犬」も「猫」も「動物」が持つ「食べる」という共通の機能を持っていますよね。 + - **データ(属性)**: クラス内に定義された変数のことで、**メンバ変数 (member variables)** または**データメンバ**と呼びます。 + - **処理(操作)**: クラス内に定義された関数のことで、**メンバ関数 (member functions)** または**メソッド**と呼びます。 -C++では、クラス名の後に `: public 親クラス名` と書くことで継承を表現します。 +このように、関連するデータと処理を一つのクラスにまとめることを、OOPの重要な概念の一つである**カプセル化 (encapsulation)** と呼びます。💊 -```cpp:inheritance_basic.cpp -#include -#include +例として、「人」を表す`Person`クラスを考えてみましょう。「人」は「名前」や「年齢」といったデータ(メンバ変数)を持ち、「自己紹介する」といった処理(メンバ関数)を行うことができます。 -// 親クラス (基底クラス) -class Animal { +```cpp +class Person { public: + // メンバ変数 std::string name; + int age; - void eat() { - std::cout << name << " is eating." << std::endl; + // メンバ関数 + 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 -// 子クラス (派生クラス) -// Animalクラスのpublicメンバを引き継ぐ -class Dog : public Animal { +// Personクラスの定義 +class Person { public: - void bark() { - std::cout << name << " says Woof!" << std::endl; + std::string name; + int age; + + void introduce() { + std::cout << "My name is " << name << ", and I am " << age << " years old." << std::endl; } }; int main() { - Dog my_dog; - my_dog.name = "Pochi"; + // Personクラスのインスタンスを生成 + Person taro; + + // メンバ変数に値を代入 (ドット演算子 . を使用) + taro.name = "Taro"; + taro.age = 30; - // 親クラスから継承したメンバ変数・メンバ関数 - my_dog.eat(); + // メンバ関数を呼び出す + taro.introduce(); // "My name is Taro, and I am 30 years old." と出力される - // Dogクラス独自のメンバ関数 - my_dog.bark(); + // 別のインスタンスを生成 + Person hanako; + hanako.name = "Hanako"; + hanako.age = 25; + hanako.introduce(); // "My name is Hanako, and I am 25 years old." と出力される return 0; } ``` -```cpp-exec:inheritance_basic.cpp -Pochi is eating. -Pochi says Woof! +```cpp-exec:instantiation.cpp +My name is Taro, and I am 30 years old. +My name is Hanako, and I am 25 years old. ``` -この例では、`Dog`クラスは`Animal`クラスを継承しています。そのため、`Dog`クラスのオブジェクト `my_dog` は、`Animal`クラスで定義されたメンバ変数 `name` やメンバ関数 `eat()` を、まるで自分のものであるかのように利用できます。 +このように、`クラス名 インスタンス名;` という形でインスタンスを生成できます。インスタンスのメンバ変数やメンバ関数にアクセスするには、`インスタンス名.メンバ名` のように**ドット演算子 (`.`)** を使います。`taro`と`hanako`は同じ`Person`クラスから作られたインスタンスですが、それぞれが独立したデータを持っていることがわかります。 -## 仮想関数 (virtual) とポリモーフィズム +## アクセス制御: public と private による情報の隠蔽 + +先ほどの`Person`クラスの例では、`main`関数から`taro.age = 30;`のようにメンバ変数に直接アクセスできました。これは手軽ですが、問題を引き起こす可能性があります。例えば、年齢にマイナスの値や非現実的な値を設定できてしまうかもしれません。 + +```cpp +Person jiro; +jiro.name = "Jiro"; +jiro.age = -5; // 本来ありえない値が設定できてしまう! +jiro.introduce(); +``` -継承の最も強力な側面は、**ポリモーフィズム(多態性)**を実現できることです。ポリモーフィズムとは、ギリシャ語で「多くの形を持つ」という意味で、プログラミングにおいては「**同じインターフェース(指示)で、オブジェクトの種類に応じて異なる振る舞いをさせる**」ことを指します。 +このような意図しない操作を防ぐために、C++には**アクセス制御**の仕組みがあります。クラスのメンバは、外部からのアクセスの可否を指定できます。 -これを実現するのが **仮想関数 (virtual function)** です。親クラスの関数宣言の前に `virtual` キーワードを付けると、その関数は仮想関数になります。 + - **`public`**: クラスの外部(`main`関数など)から自由にアクセスできます。 + - **`private`**: そのクラスのメンバ関数からしかアクセスできません。外部からはアクセス不可です。 -親クラスのポインタや参照は、子クラスのオブジェクトを指すことができます。このとき、呼び出された仮想関数は、ポインタが指している**オブジェクトの実際の型**に基づいて決定されます。 +アクセス制御の基本は、**メンバ変数は`private`にし、メンバ関数は`public`にする**ことです。これにより、データの不正な書き換えを防ぎ、クラスの内部実装を外部から隠蔽します。これを**情報の隠蔽 (information hiding)** と呼び、カプセル化の重要な目的の一つです。 -言葉だけでは難しいので、コードで見てみましょう。 +`private`なメンバ変数に安全にアクセスするために、`public`なメンバ関数(**ゲッター**や**セッター**と呼ばれる)を用意するのが一般的です。 -```cpp:polymorphism_example.cpp +```cpp:access_control.cpp #include #include -class Animal { +class Person { +private: + // メンバ変数は外部から隠蔽する + std::string name; + int age; + public: - // speak() を仮想関数として宣言 - virtual void speak() { - std::cout << "Some generic animal sound..." << std::endl; + // セッター: メンバ変数に値を設定する + void setName(const std::string& newName) { + name = newName; } -}; -class Dog : public Animal { -public: - // 親クラスの仮想関数を上書き (オーバーライド) - void speak() override { // overrideキーワードについては後述 - std::cout << "Woof!" << std::endl; + void setAge(int newAge) { + if (newAge >= 0 && newAge < 150) { // 不正な値をチェック + age = newAge; + } else { + std::cout << "Error: Invalid age value." << std::endl; + } } -}; -class Cat : public Animal { -public: - // 親クラスの仮想関数を上書き (オーバーライド) - void speak() override { - std::cout << "Meow!" << std::endl; + // ゲッター: メンバ変数の値を取得する + std::string getName() const { + return name; } -}; -// Animalへのポインタを受け取る関数 -void make_animal_speak(Animal* animal) { - animal->speak(); // ポインタが指す先の実際のオブジェクトに応じて、適切な speak() が呼ばれる -} + int getAge() const { + return age; + } + + // このメンバ関数はクラス内部にあるので、privateメンバにアクセスできる + void introduce() { + std::cout << "My name is " << name << ", and I am " << age << " years old." << std::endl; + } +}; int main() { - Animal generic_animal; - Dog dog; - Cat cat; + Person saburo; + + // saburo.name = "Saburo"; // エラー! privateメンバには直接アクセスできない + // saburo.age = -10; // エラー! + + // publicなメンバ関数を経由して安全に値を設定 + saburo.setName("Saburo"); + saburo.setAge(28); - std::cout << "Calling through function:" << std::endl; - make_animal_speak(&generic_animal); - make_animal_speak(&dog); // Dogオブジェクトを渡す - make_animal_speak(&cat); // Catオブジェクトを渡す + saburo.introduce(); + + saburo.setAge(-10); // エラーメッセージが出力される + + // publicなメンバ関数経由で値を取得 + std::cout << "Name: " << saburo.getName() << std::endl; return 0; } ``` -```cpp-exec:polymorphism_example.cpp -Calling through function: -Some generic animal sound... -Woof! -Meow! +```cpp-exec:access_control.cpp +My name is Saburo, and I am 28 years old. +Error: Invalid age value. +Name: Saburo ``` -`make_animal_speak` 関数は `Animal*` 型の引数を取りますが、`Dog`オブジェクトや`Cat`オブジェクトのアドレスを渡すことができています。そして、`animal->speak()` を呼び出すと、`animal` ポインタが実際に指しているオブジェクトの `speak()` が実行されます。これがポリモーフィズムです。もし `Animal`クラスの `speak()` に `virtual` が付いていなければ、どのオブジェクトを渡しても `Animal` の `speak()` が呼ばれてしまいます。 +`setAge`関数内で値の妥当性チェックを行っている点に注目してください。このように、クラスの利用者は内部の実装を気にすることなく、提供された`public`なインターフェース(メンバ関数)を通じて安全にオブジェクトを操作できます。 -## オーバーライド (override) +> `const`キーワード: `getName() const` のようにメンバ関数の後ろに`const`を付けると、その関数がメンバ変数を変更しないことをコンパイラに約束します。このような関数を**constメンバ関数**と呼びます。 -先ほどの例で `override` というキーワードが登場しましたね。これはC++11から導入されたもので、子クラスの関数が**親クラスの仮想関数を上書き(オーバーライド)する意図があることを明示する**ためのものです。 +## コンストラクタとデストラクタ: オブジェクトが生まれてから消えるまで -`override` を付けておくと、もし親クラスに対応する仮想関数が存在しない場合(例えば、関数名をタイプミスした場合など)に、コンパイラがエラーを検出してくれます。 +オブジェクトは生成され、利用され、やがて破棄されます。このライフサイクルに合わせて特別な処理を自動的に実行するための仕組みが**コンストラクタ**と**デストラクタ**です。 + +### コンストラクタ (Constructor) + +**コンストラクタ**は、インスタンスが生成されるときに**自動的に呼び出される**特別なメンバ関数です。主な役割は、メンバ変数の初期化です。 + +コンストラクタには以下の特徴があります。 + + - 関数名がクラス名と全く同じ。 + - 戻り値の型を指定しない(`void`も付けない)。 + - 引数を取ることができ、複数定義できる(オーバーロード)。 + +```cpp:constructor.cpp +class Person { +private: + std::string name; + int age; -```cpp -class Dog : public Animal { public: - // もし親クラスのspeakがvirtualでなかったり、 - // speek() のようにタイプミスしたりすると、コンパイルエラーになる。 - void speak() override { - std::cout << "Woof!" << std::endl; + // 引数付きコンストラクタ + Person(const std::string& initName, int initAge) { + std::cout << "Constructor called for " << initName << std::endl; + name = initName; + age = initAge; } + // ... }; -``` -意図しないバグを防ぐために、仮想関数をオーバーライドする際は必ず `override` を付ける習慣をつけましょう。 +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. +``` -時には、「具体的な実装を持たず、子クラスに実装を強制するための設計図」としてのみ機能するクラスを定義したい場合があります。これが**抽象クラス (Abstract Class)** です。 +このように、インスタンス生成時に`()`で初期値を渡すことで、オブジェクトを生成と同時に有効な状態にできます。`set`関数を別途呼び出す手間が省け、初期化忘れを防ぐことができます。 -抽象クラスは、**純粋仮想関数 (pure virtual function)** を1つ以上持つクラスです。純粋仮想関数は、末尾に `= 0` を付けて宣言します。 +### デストラクタ (Destructor) -```cpp -virtual void function_name() = 0; // これが純粋仮想関数 -``` +**デストラクタ**は、インスタンスが破棄されるとき(例えば、変数のスコープを抜けるとき)に**自動的に呼び出される**特別なメンバ関数です。主な役割は、オブジェクトが使用していたリソース(メモリやファイルなど)の後片付けです。 -抽象クラスは以下の特徴を持ちます。 +デストラクタには以下の特徴があります。 - * インスタンス化(オブジェクトの作成)ができない。 - * 抽象クラスを継承した子クラスは、全ての純粋仮想関数をオーバーライド(実装)しなければならない。さもなければ、その子クラスもまた抽象クラスとなる。 + - 関数名が `~` + クラス名。 + - 戻り値も引数も取らない。 + - 1つのクラスに1つしか定義できない。 -```cpp:abstract_class_example.cpp +```cpp:constructor_destructor.cpp #include +#include -// Shapeは純粋仮想関数 draw() を持つため、抽象クラスとなる -class Shape { -public: - // 純粋仮想関数 - // このクラスを継承するクラスは、必ず draw() を実装しなければならない - virtual void draw() = 0; - - // 仮想デストラクタ (継承を扱う際は重要。詳しくは今後の章で) - virtual ~Shape() {} -}; +class Person { +private: + std::string name; + int age; -class Circle : public Shape { public: - void draw() override { - std::cout << "Drawing a circle: ○" << std::endl; + // コンストラクタ + Person(const std::string& initName, int initAge) { + std::cout << "Constructor called for " << initName << "." << std::endl; + name = initName; + age = initAge; } -}; -class Square : public Shape { -public: - void draw() override { - std::cout << "Drawing a square: □" << std::endl; + // デストラクタ + ~Person() { + std::cout << "Destructor called for " << name << "." << std::endl; } -}; -int main() { - // Shape my_shape; // エラー!抽象クラスはインスタンス化できない + void introduce() { + std::cout << "My name is " << name << ", and I am " << age << " years old." << std::endl; + } +}; - Circle circle; - Square square; +void create_person_scope() { + std::cout << "--- Entering scope ---" << std::endl; + Person kenji("Kenji", 45); // kenjiはこのスコープ内でのみ生存 + kenji.introduce(); + std::cout << "--- Exiting scope ---" << std::endl; +} // ここでkenjiのスコープが終わり、デストラクタが呼ばれる - Shape* shape1 = &circle; - Shape* shape2 = □ +int main() { + create_person_scope(); - shape1->draw(); - shape2->draw(); + std::cout << "--- Back in main ---" << std::endl; return 0; } ``` -```cpp-exec:abstract_class_example.cpp -Drawing a circle: ○ -Drawing a square: □ +```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 --- ``` -`Shape` クラスは「図形なら描画できるはずだ」というインターフェース(契約)を定義し、具体的な描画方法は子クラスである `Circle` や `Square` に任せています。このように、抽象クラスはプログラムの骨格となる設計を強制するのに非常に役立ちます。 +実行結果を見ると、`kenji`オブジェクトが生成されたときにコンストラクタが、`create_person_scope`関数のスコープを抜けるときにデストラクタが自動的に呼び出されていることがわかります。動的に確保したメモリの解放など、クリーンアップ処理はデストラクタに書くのが定石です。この考え方は、今後の章で学ぶRAII(Resource Acquisition Is Initialization)という重要な概念に繋がります。 ## この章のまとめ - * **継承**: 既存のクラスの機能を引き継ぎ、コードの再利用性を高める仕組みです。`(子クラス) : public (親クラス)` のように書きます。 - * **ポリモーフィズム**: 「同じ指示でも、オブジェクトの種類によって異なる振る舞いをさせる」性質です。 - * **仮想関数 (`virtual`)**: ポリモーフィズムを実現するための鍵です。親クラスの関数に `virtual` を付けると、ポインタや参照経由で呼び出した際に、オブジェクトの実際の型に応じた関数が実行されます。 - * **オーバーライド (`override`)**: 子クラスで親クラスの仮想関数を上書きする意図を明示します。コンパイラがチェックしてくれるため、安全性が向上します。 - * **抽象クラス**: 1つ以上の**純粋仮想関数 (`virtual ... = 0;`)** を持つクラスです。インスタンス化できず、継承されるための設計図として機能します。 +この章では、C++におけるオブジェクト指向プログラミングの第一歩として、クラスの基本的な概念を学びました。 + + - **クラス**は、データ(**メンバ変数**)と処理(**メンバ関数**)を一つにまとめた「設計図」です。 + - クラスから実体である**オブジェクト(インスタンス)**を生成して使用します。 + - **カプセル化**は、関連するデータと処理をまとめることです。 + - **アクセス制御**(`public`, `private`)により、外部からアクセスされたくないメンバを保護します(**情報の隠蔽**)。 + - **コンストラクタ**は、オブジェクト生成時に自動で呼ばれ、初期化を行います。 + - **デストラクタ**は、オブジェクト破棄時に自動で呼ばれ、後片付けを行います。 -### 練習問題1:乗り物の階層構造 +クラスを使いこなすことで、プログラムの部品化が進み、再利用性やメンテナンス性が格段に向上します。次の章では、クラスのさらに進んだ機能について学んでいきましょう。 -`Vehicle` という親クラスを作成し、`move()` というメンバ関数を持たせましょう。次に、`Vehicle` を継承して `Car` クラスと `Motorcycle` クラスを作成し、それぞれが独自の `move()` の振る舞いをするようにオーバーライドしてください。 +### 練習問題1: 長方形クラス -`main` 関数では、`Vehicle` のポインタの配列を作成し、`Car` と `Motorcycle` のオブジェクトを格納して、ループでそれぞれの `move()` を呼び出してください。 +幅(`width`)と高さ(`height`)をメンバ変数として持つ`Rectangle`クラスを作成してください。 + + - メンバ変数は`private`で定義してください。 + - コンストラクタで幅と高さを初期化できるようにしてください。 + - 面積を計算して返す`getArea()`メソッドと、周の長さを計算して返す`getPerimeter()`メソッドを`public`で実装してください。 + - `main`関数で`Rectangle`クラスのインスタンスをいくつか生成し、面積と周の長さを表示するプログラムを作成してください。 ```cpp:practice7_1.cpp #include #include - - -// ここに Vehicle, Car, Motorcycle クラスを定義してください - +// ここにRectangleクラスを定義してください 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(); - } + // ここでRectangleクラスのインスタンスを生成し、面積と周の長さを表示してください return 0; } @@ -249,33 +313,28 @@ int main() { ```cpp-exec:practice7_1.cpp ``` -### 問題2: 従業員の給与計算 -`Employee` という抽象クラスを定義してください。このクラスは、従業員の名前を保持し、給与を計算するための純粋仮想関数 `calculate_salary()` を持ちます。 +### 練習問題2: 書籍クラス -次に、`Employee` を継承して、`FullTimeEmployee`(月給制)と `PartTimeEmployee`(時給制)の2つのクラスを作成します。それぞれのクラスで `calculate_salary()` を具体的に実装してください。 +タイトル(`title`)、著者(`author`)、ページ数(`pages`)をメンバ変数として持つ`Book`クラスを作成してください。 -`main` 関数で、それぞれのクラスのインスタンスを作成し、給与が正しく計算されることを確認してください。 + - メンバ変数は`private`で定義してください。 + - コンストラクタで、タイトル、著者、ページ数を初期化できるようにしてください。 + - 本の情報を整形してコンソールに出力する`printInfo()`メソッドを`public`で実装してください。(例: `Title: [タイトル], Author: [著者], Pages: [ページ数] pages`) + - `main`関数で`Book`クラスのインスタンスを生成し、その情報を表示してください。 ```cpp:practice7_2.cpp #include #include - -// ここに Employee, FullTimeEmployee, PartTimeEmployee クラスを定義してください - +// ここにBookクラスを定義してください 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; + // ここでBookクラスのインスタンスを生成し、情報を表示してください return 0; } ``` ```cpp-exec:practice7_2.cpp -Alice's Salary: $3000 -Bob's Salary: $1600 +Title: The Great Gatsby, Author: F. Scott Fitzgerald, Pages: 180 pages ``` diff --git a/public/docs/cpp-8.md b/public/docs/cpp-8.md index 074c84e..3db56d3 100644 --- a/public/docs/cpp-8.md +++ b/public/docs/cpp-8.md @@ -1,224 +1,503 @@ -# 第8章: テンプレートによる汎用プログラミング +# 第8章: クラスを使いこなす -これまでの章では、`int`や`double`、あるいは自作の`Car`クラスのように、特定の型に対して処理を行う関数やクラスを作成してきました。しかし、プログラムが複雑になるにつれて、「型は違うけれど、行いたい処理は全く同じ」という状況が頻繁に発生します。例えば、2つの値の大きい方を返す`max`という関数を考えてみましょう。 +第7章では、C++のオブジェクト指向プログラミングの核となる`class`の基本的な使い方を学びました。しかし、クラスを真に強力なツールとして使いこなすには、もう少し知識が必要です。この章では、オブジェクトのコピー、演算子のオーバーロード、クラスで共有されるメンバなど、より実践的でパワフルな機能について掘り下げていきます。これらの概念をマスターすることで、あなたの書くクラスはより安全で、直感的で、再利用性の高いものになるでしょう。 -```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; -} +オブジェクトをコピーしたい場面は頻繁にあります。例えば、関数の引数にオブジェクトを渡すとき(値渡し)や、既存のオブジェクトで新しいオブジェクトを初期化するときなどです。 + +```cpp +Vector2D v1(1.0, 2.0); +Vector2D v2 = v1; // ここでコピーが発生! ``` -このように、型ごとに同じロジックの関数をいくつも用意するのは非効率的ですし、バグの温床にもなります。 +多くの場合、コンパイラが自動的に生成するコピー機能で十分です。しかし、クラスがポインタなどでリソース(メモリなど)を管理している場合、単純なコピーでは問題が発生します。 -この問題を解決するのが**テンプレート**です。テンプレートを使うと、具体的な型を "仮引数" のように扱い、様々な型に対応できる関数やクラスの「設計図」を作ることができます。このような、型に依存しないプログラミングスタイルを**ジェネリックプログラミング(汎用プログラミング)**と呼びます。 +### 何もしないとどうなる? (浅いコピー) -## 関数テンプレート: intでもdoubleでもstringでも動く関数を作る +まず、コピーの機能を自分で作らなかった場合に何が起きるか見てみましょう。 +コンパイラは、メンバ変数を単純にコピーするだけの「浅いコピー」を行います。 -関数テンプレートを使うと、先ほどの`max`関数の問題をエレガントに解決できます。 +ここでは、`int`へのポインタを一つだけ持つ`ResourceHolder`(リソース保持者)というクラスを考えます。 -```cpp:function_template_intro.cpp +```cpp:shallow_copy.cpp +// 悪い例:浅いコピーの問題点 #include -#include -// Tという名前で型を仮引数として受け取るテンプレートを宣言 -template -T max_value(T a, T b) { - return (a > b) ? a : b; +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() { - // int型でmax_valueを呼び出す - std::cout << "max(10, 20) = " << max_value(10, 20) << std::endl; + std::cout << "--- rh1の作成 ---" << std::endl; + ResourceHolder rh1(10); - // double型でmax_valueを呼び出す - std::cout << "max(3.14, 1.41) = " << max_value(3.14, 1.41) << std::endl; + std::cout << "\n--- rh2をrh1で初期化 ---" << std::endl; + ResourceHolder rh2 = rh1; // コピーコンストラクタが呼ばれる - // std::string型でも動作する! - std::string s1 = "world"; - std::string s2 = "hello"; - std::cout << "max(\"world\", \"hello\") = " << max_value(s1, s2) << std::endl; + std::cout << "\n--- rh3の作成 ---" << std::endl; + ResourceHolder rh3(20); - return 0; + 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:function_template_intro.cpp -max(10, 20) = 20 -max(3.14, 1.41) = 3.14 -max("world", "hello") = world +```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) ``` -### テンプレートの仕組み +*(メモリアドレスは実行するたびに変わります)* -`template `という部分が、この関数がテンプレートであることを示しています。 +実行結果を見ると、`rh1`, `rh2`, `rh3` はそれぞれ異なるメモリアドレス (`Address`) を持っていることがわかります。これにより、各オブジェクトは独立したリソースを管理でき、プログラム終了時にそれぞれのデストラクタが安全にメモリを解放できます。 - * **`template <...>`**: テンプレートの宣言を開始します。 - * **`typename T`**: `T`という名前の「型引数」を定義しています。`typename`の代わりに`class`と書くこともできますが、意味は同じです。`T`は、このテンプレートが実際に使われるときに具体的な型(`int`や`double`など)に置き換えられます。 +| 機能 | いつ呼ばれるか | 何をするか | +| :--- | :--- | :--- | +| **コピーコンストラクタ** | オブジェクトが**作られる時**に、他のオブジェクトで初期化される場合
    `ResourceHolder r2 = r1;` | 新しいリソースを確保し、元のオブジェクトの**値**をコピーする。 | +| **コピー代入演算子** | **既にあるオブジェクト**に、他のオブジェクトを代入する場合
    `r3 = r1;` | 1. 自分が持っている古いリソースを解放する。
    2. 新しいリソースを確保し、元のオブジェクトの**値**をコピーする。 | -`main`関数で`max_value(10, 20)`のように呼び出すと、コンパイラは引数の型が`int`であることから、`T`を`int`だと自動的に判断します(これを**テンプレート引数推論**と呼びます)。そして、内部的に以下のような`int`版の関数を生成してくれるのです。 +このように、ポインタでリソースを管理するクラスでは、安全なコピーを実現するためにこの2つの関数を自分で定義することが不可欠です。 -```cpp -// コンパイラが内部的に生成するコードのイメージ -int max_value(int a, int b) { - return (a > b) ? a : b; +## 演算子のオーバーロード + +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) ``` -同様に、`double`や`std::string`で呼び出されれば、それぞれの型に対応したバージョンの関数が自動的に生成されます。これにより、私たちは一つの「設計図」を書くだけで、様々な型に対応できるのです。 +`operator<<` は、左辺のオペランドが `std::ostream` 型(`std::cout` など)であるため、`Vector2D` のメンバ関数としては定義できません。そのため、非メンバ関数として定義するのが一般的です。 + +## staticメンバ -## クラステンプレート: 様々な型のデータを格納できるクラスを作る +通常、クラスのメンバ変数はオブジェクトごとに個別のメモリ領域を持ちます。しかし、あるクラスの**全てのオブジェクトで共有したい**情報もあります。例えば、「これまでに生成されたオブジェクトの総数」などです。このような場合、**staticメンバ**を使用します。 -テンプレートの力は、クラスにも適用できます。これにより、様々な型のデータを格納できる汎用的なクラス(コンテナなど)を作成できます。例えば、「2つの値をペアで保持する」クラスを考えてみましょう。 +### staticメンバ変数 -```cpp:class_template_intro.cpp +`static` キーワードを付けて宣言されたメンバ変数は、特定のオブジェクトに属さず、クラスそのものに属します。そのため、全オブジェクトでただ1つの実体を共有します。これを**クラス変数**と呼ぶこともあります。 + + * **宣言**: クラス定義の中で `static` を付けて行います。 + * **定義**: クラス定義の外(ソースファイル)で、メモリ上の実体を確保し、初期化します。 + +### staticメンバ関数 + +`static` キーワードを付けて宣言されたメンバ関数は、特定のオブジェクトに依存せずに呼び出せます。そのため、`this` ポインタ(後述)を持ちません。 + + * **アクセス**: staticメンバ変数や他のstaticメンバ関数にはアクセスできますが、非staticなメンバ(インスタンスごとのメンバ変数やメンバ関数)にはアクセスできません。 + * **呼び出し**: `クラス名::関数名()` のように、オブジェクトを生成しなくても呼び出せます。 + +### 実装例 + +ゲームに登場する `Player` クラスがあり、現在何人のプレイヤーが存在するかを管理する例を見てみましょう。 + +```cpp:static_members.cpp #include #include -// 2つの型 T1, T2 を引数に取るクラステンプレート -template -class Pair { +class Player { +private: + std::string name; + // (1) staticメンバ変数の宣言 + static int playerCount; + public: - T1 first; - T2 second; + Player(const std::string& name) : name(name) { + playerCount++; // オブジェクトが生成されるたびにインクリメント + std::cout << name << " がゲームに参加しました。現在のプレイヤー数: " << playerCount << std::endl; + } - // コンストラクタ - Pair(T1 f, T2 s) : first(f), second(s) {} + ~Player() { + playerCount--; // オブジェクトが破棄されるたびにデクリメント + std::cout << name << " がゲームから退出しました。現在のプレイヤー数: " << playerCount << std::endl; + } - void print() { - std::cout << "(" << first << ", " << second << ")" << std::endl; + // (2) staticメンバ関数の宣言 + static int getPlayerCount() { + // name などの非staticメンバにはアクセスできない + return playerCount; } }; +// (3) staticメンバ変数の定義と初期化 +int Player::playerCount = 0; + int main() { - // T1=int, T2=std::string としてPairクラスのオブジェクトを生成 - Pair p1(1, "apple"); - p1.print(); + // オブジェクトがなくてもstaticメンバ関数を呼び出せる + std::cout << "ゲーム開始時のプレイヤー数: " << Player::getPlayerCount() << std::endl; + std::cout << "---" << std::endl; - // T1=std::string, T2=double としてPairクラスのオブジェクトを生成 - Pair p2("pi", 3.14159); - p2.print(); - - // 違う型のPair同士は当然、別の型として扱われる - // p1 = p2; // これはコンパイルエラーになる + 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:class_template_intro.cpp -(1, apple) -(pi, 3.14159) +```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ポインタ -1. **明示的な型指定**: - 関数テンプレートではコンパイラが型を推論してくれましたが、クラステンプレートの場合は、オブジェクトを生成する際に`Pair`のように、開発者が明示的に型を指定する必要があります。 +非staticなメンバ関数が呼び出されるとき、その関数は「どのオブジェクトに対して呼び出されたか」を知る必要があります。コンパイラは、そのメンバ関数に対して、呼び出し元のオブジェクトのアドレスを暗黙的に渡します。このアドレスを保持するのが `this` ポインタです。 -2. **インスタンス化**: - `Pair`のように具体的な型を指定してオブジェクトを作ることを、テンプレートの**インスタンス化**と呼びます。コンパイラは、この指定に基づいて`T1`を`int`に、`T2`を`std::string`に置き換えた、以下のような新しいクラスを内部的に生成します。 +`this` は、メンバ関数内で使用できるキーワードで、自分自身のオブジェクトを指すポインタです。 - ```cpp - // コンパイラが内部的に生成するクラスのイメージ - class Pair_int_string { // クラス名は実際には異なります - public: - int first; - std::string second; +`this` ポインタが主に使われるのは、以下のような場面です。 - Pair_int_string(int f, std::string s) : first(f), second(s) {} +1. **メンバ変数と引数の名前が同じ場合** + コンストラクタの初期化子リストを使わない場合など、引数名とメンバ変数名が同じになることがあります。その際、`this->` を付けることでメンバ変数であることを明示できます。 - void print() { - std::cout << "(" << first << ", " << second << ")" << std::endl; - } - }; + ```cpp + void setX(double x) { + this->x = x; // this->x はメンバ変数, x は引数 + } ``` - `Pair`と`Pair`は、コンパイルされると全く別のクラスとして扱われることに注意してください。 +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; // 自分自身の参照を返す + } -クラステンプレートは、C++の強力なライブラリである**STL (Standard Template Library)**の根幹をなす技術です。次章で学ぶ`vector`や`map`といった便利なコンテナは、すべてクラステンプレートで実装されています。 + 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)` を呼び出すことができます。 ## この章のまとめ - * **ジェネリックプログラミング**は、特定の型に縛られない、汎用的なコードを書くための手法です。 - * **テンプレート**は、C++でジェネリックプログラミングを実現するための機能です。 - * **関数テンプレート**を使うと、様々な型の引数に対して同じ処理を行う関数を定義できます。呼び出し時には、コンパイラが**テンプレート引数推論**によって型を自動的に決定します。 - * **クラステンプレート**を使うと、様々な型を扱える汎用的なクラスを定義できます。オブジェクトを生成する際には、`< >`内に具体的な型を**明示的に指定**してインスタンス化する必要があります。 +この章では、クラスをより効果的に利用するための応用的な機能を学びました。 + + * **オブジェクトのコピー**: ポインタなどリソースを管理するクラスでは、**コピーコンストラクタ**と**コピー代入演算子**を定義し、**深いコピー**を実装することが重要です。これにより、リソースの二重解放などの問題を未然に防ぎます。 + * **演算子のオーバーロード**: `+` や `==` などの演算子を自作クラスに対して定義することで、コードの可読性を高め、直感的な操作を可能にします。 + * **staticメンバ**: `static`メンバ変数やメンバ関数は、クラスの全オブジェクトで共有されるデータや機能を提供します。オブジェクトを生成しなくてもアクセスできるのが特徴です。 + * **thisポインタ**: 非staticメンバ関数内で、呼び出し元のオブジェクト自身を指すポインタです。メンバ変数と引数の区別や、メソッドチェーンの実装に役立ちます。 -テンプレートを使いこなすことで、コードの再利用性が劇的に向上し、より柔軟で堅牢なプログラムを記述できるようになります。 +これらの機能を組み合わせることで、C++のクラスは単なるデータの入れ物から、振る舞いを伴った洗練された部品へと進化します。 -### 練習問題1: 汎用的なprint関数 +### 練習問題1: 複素数クラス -任意の型の配列(ここでは`std::vector`を使いましょう)を受け取り、その要素をすべて画面に出力する関数テンプレート`print_elements`を作成してください。 +実部 (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:practice8_1.cpp #include -#include -#include - -// ここに関数テンプレート print_elements を実装してください +// ここに Complex クラスを実装してください 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); - + 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:practice8_1.cpp -Integers: 1 2 3 4 5 -Strings: C++ is powerful +c1: (1 + 2i) +c2: (3 + 4i) +c1 + c2 = (4 + 6i) +c1 * c2 = (-5 + 10i) ``` -### 練習問題2: 汎用的なスタッククラス - -後入れ先出し(LIFO)のデータ構造であるスタックを、クラステンプレート`SimpleStack`として実装してください。以下のメンバ関数を持つようにしてください。 +### 練習問題2: 動的配列クラスのコピー制御 - * `void push(T item)`: スタックに要素を追加する - * `T pop()`: スタックの先頭から要素を取り出す - * `bool is_empty()`: スタックが空かどうかを返す +整数 (`int`) の動的配列を管理するクラス `IntArray` を作成してください。このクラスは、コンストラクタで指定されたサイズの配列を `new` で確保し、デストラクタで `delete[]` を使って解放します。 -`std::vector`を内部のデータ格納場所として利用して構いません。`int`型と`char`型で動作を確認してください。 +この `IntArray` クラスに対して、**深いコピー**を正しく行うための**コピーコンストラクタ**と**コピー代入演算子**を実装してください。 ```cpp:practice8_2.cpp #include -#include -#include -// ここにクラステンプレート SimpleStack を実装してください +// ここに IntArray クラスを実装してください 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 + 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:practice8_2.cpp -Popped from int_stack: 20 -Popped from int_stack: 10 -Popped from char_stack: B -Popped from char_stack: A +arr1: 0 10 20 30 40 +arr2 (コピー): 0 10 20 30 40 +arr3 (代入): 0 10 20 30 40 ``` diff --git a/public/docs/cpp-9.md b/public/docs/cpp-9.md index e3b8791..25ce4d6 100644 --- a/public/docs/cpp-9.md +++ b/public/docs/cpp-9.md @@ -1,252 +1,281 @@ -# 第9章: 標準テンプレートライブラリ (STL) ①:コンテナ +# 第9章: 継承とポリモーフィズム -C++の大きな魅力の一つに、**標準テンプレートライブラリ (Standard Template Library, STL)** の存在があります。STLは、よく使われるデータ構造やアルゴリズムを、汎用的かつ効率的に実装したライブラリ群です。この章では、STLの心臓部である**コンテナ**に焦点を当て、データの格納と管理を劇的に楽にする方法を学びます。 +オブジェクト指向プログラミング(OOP)の真の力を解放する時が来ました!💪 この章では、OOPの強力な柱である「**継承 (Inheritance)**」と「**ポリモーフィズム (Polymorphism) / 多態性**」を学びます。これらの概念をマスターすることで、コードの再利用性を高め、柔軟で拡張性の高いプログラムを設計できるようになります。 -## STLの全体像: コンテナ、アルゴリズム、イテレータ +## クラスの継承 -STLは、主に3つの要素から構成されています。 +**継承**とは、既存のクラス(**親クラス**または**基底クラス**と呼びます)の機能を引き継いで、新しいクラス(**子クラス**または**派生クラス**と呼びます)を作成する仕組みです。これにより、共通の機能を何度も書く必要がなくなり、コードの重複を避けられます。 -1. **コンテナ (Containers)**: データを格納するためのデータ構造です。`vector`(可変長配列)や`map`(連想配列)など、様々な種類があります。 -2. **アルゴリズム (Algorithms)**: ソート、検索、変換など、コンテナ上のデータに対して操作を行う関数群です。 -3. **イテレータ (Iterators)**: コンテナの要素を指し示し、アルゴリズムがコンテナの種類に依存せずに各要素にアクセスするための統一的なインターフェースを提供します。ポインタを一般化したようなものです。 +例えば、「動物」という大まかなクラスがあり、その特徴を引き継いで「犬」や「猫」といった具体的なクラスを作ることができます。「犬」も「猫」も「動物」が持つ「食べる」という共通の機能を持っていますよね。 -これら3つが連携することで、C++プログラマは効率的で再利用性の高いコードを素早く書くことができます。この章では「コンテナ」を、次の章では「アルゴリズム」と、それらをつなぐ「イテレータ」の応用を詳しく見ていきます。 +C++では、クラス名の後に `: public 親クラス名` と書くことで継承を表現します。 -## std::vector: 最もよく使う可変長配列 +```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()` を、まるで自分のものであるかのように利用できます。 -`std::vector`は、最も基本的で最もよく使われるコンテナです。他の言語でいうところの「リスト」や「動的配列」に相当し、要素を連続したメモリ領域に格納します。 +## 仮想関数 (virtual) とポリモーフィズム -**主な特徴**: +継承の最も強力な側面は、**ポリモーフィズム(多態性)**を実現できることです。ポリモーフィズムとは、ギリシャ語で「多くの形を持つ」という意味で、プログラミングにおいては「**同じインターフェース(指示)で、オブジェクトの種類に応じて異なる振る舞いをさせる**」ことを指します。 - * **動的なサイズ**: 必要に応じて自動的にサイズが拡張されます。 - * **高速なランダムアクセス**: インデックス(添字)を使って `[i]` の形式で要素に高速にアクセスできます (`O(1)`)。 - * **末尾への高速な追加・削除**: `push_back()` や `pop_back()` を使った末尾への操作は非常に高速です。 +これを実現するのが **仮想関数 (virtual function)** です。親クラスの関数宣言の前に `virtual` キーワードを付けると、その関数は仮想関数になります。 -`std::vector`を使うには、``ヘッダをインクルードする必要があります。 +親クラスのポインタや参照は、子クラスのオブジェクトを指すことができます。このとき、呼び出された仮想関数は、ポインタが指している**オブジェクトの実際の型**に基づいて決定されます。 -```cpp:vector_example.cpp +言葉だけでは難しいので、コードで見てみましょう。 + +```cpp:polymorphism_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"); +class Animal { +public: + // speak() を仮想関数として宣言 + virtual void speak() { + std::cout << "Some generic animal sound..." << std::endl; + } +}; - // インデックスを使った要素へのアクセス - std::cout << "Index 1: " << names[1] << std::endl; +class Dog : public Animal { +public: + // 親クラスの仮想関数を上書き (オーバーライド) + void speak() override { // overrideキーワードについては後述 + std::cout << "Woof!" << std::endl; + } +}; - // 範囲for文 (range-based for loop) を使った全要素の走査 - std::cout << "\nAll names:" << std::endl; - for (const std::string& name : names) { - std::cout << "- " << name << std::endl; +class Cat : public Animal { +public: + // 親クラスの仮想関数を上書き (オーバーライド) + void speak() override { + std::cout << "Meow!" << std::endl; } +}; - // size()で現在の要素数を取得 - std::cout << "\nCurrent size: " << names.size() << std::endl; +// Animalへのポインタを受け取る関数 +void make_animal_speak(Animal* animal) { + animal->speak(); // ポインタが指す先の実際のオブジェクトに応じて、適切な speak() が呼ばれる +} - // pop_backで末尾の要素を削除 - names.pop_back(); // "Charlie"を削除 +int main() { + Animal generic_animal; + Dog dog; + Cat cat; - 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; + 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:vector_example.cpp -Index 1: Bob +```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()` が呼ばれてしまいます。 -All names: -- Alice -- Bob -- Charlie +## オーバーライド (override) -Current size: 3 +先ほどの例で `override` というキーワードが登場しましたね。これはC++11から導入されたもので、子クラスの関数が**親クラスの仮想関数を上書き(オーバーライド)する意図があることを明示する**ためのものです。 -After pop_back: -- Alice -- Bob -Current size: 2 +`override` を付けておくと、もし親クラスに対応する仮想関数が存在しない場合(例えば、関数名をタイプミスした場合など)に、コンパイラがエラーを検出してくれます。 + +```cpp +class Dog : public Animal { +public: + // もし親クラスのspeakがvirtualでなかったり、 + // speek() のようにタイプミスしたりすると、コンパイルエラーになる。 + void speak() override { + std::cout << "Woof!" << std::endl; + } +}; ``` -`std::vector`は、どのコンテナを使うか迷ったら、まず最初に検討すべきデフォルトの選択肢と言えるほど万能です。 +意図しないバグを防ぐために、仮想関数をオーバーライドする際は必ず `override` を付ける習慣をつけましょう。 -## std::map: キーと値のペアを管理する連想配列 +## 抽象クラス -`std::map`は、キー (key) と値 (value) のペアを管理するためのコンテナです。他の言語の「辞書 (dictionary)」や「ハッシュマップ (hash map)」に似ています。キーを使って値を高速に検索、追加、削除できます。 +時には、「具体的な実装を持たず、子クラスに実装を強制するための設計図」としてのみ機能するクラスを定義したい場合があります。これが**抽象クラス (Abstract Class)** です。 -**主な特徴**: +抽象クラスは、**純粋仮想関数 (pure virtual function)** を1つ以上持つクラスです。純粋仮想関数は、末尾に `= 0` を付けて宣言します。 - * **キーによる高速な検索**: キーに基づいて要素が自動的にソートされて格納されるため、検索、挿入、削除が高速です (`O(log n)`)。 - * **一意なキー**: `std::map`内のキーは重複しません。同じキーで値を挿入しようとすると、既存の値が上書きされます。 +```cpp +virtual void function_name() = 0; // これが純粋仮想関数 +``` -`std::map`を使うには、``ヘッダをインクルードする必要があります。 +抽象クラスは以下の特徴を持ちます。 -```cpp:map_example.cpp + * インスタンス化(オブジェクトの作成)ができない。 + * 抽象クラスを継承した子クラスは、全ての純粋仮想関数をオーバーライド(実装)しなければならない。さもなければ、その子クラスもまた抽象クラスとなる。 + +```cpp:abstract_class_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; +// 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; } +}; - // count()でキーの存在を確認 - std::string search_key = "Charlie"; - if (scores.count(search_key)) { - std::cout << "\n" << search_key << " is in the map." << std::endl; +class Square : public Shape { +public: + void draw() override { + std::cout << "Drawing a square: □" << std::endl; } +}; + +int main() { + // Shape my_shape; // エラー!抽象クラスはインスタンス化できない - // erase()で要素を削除 - scores.erase("Bob"); + Circle circle; + Square square; - std::cout << "\nAfter erasing Bob:" << std::endl; - for (const auto& pair : scores) { - std::cout << "- " << pair.first << ": " << pair.second << std::endl; - } + Shape* shape1 = &circle; + Shape* shape2 = □ + + shape1->draw(); + shape2->draw(); 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 +```cpp-exec:abstract_class_example.cpp +Drawing a circle: ○ +Drawing a square: □ ``` -`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`を基本とし、必要に応じて他のコンテナを検討するのが良いアプローチです。 +`Shape` クラスは「図形なら描画できるはずだ」というインターフェース(契約)を定義し、具体的な描画方法は子クラスである `Circle` や `Square` に任せています。このように、抽象クラスはプログラムの骨格となる設計を強制するのに非常に役立ちます。 ## この章のまとめ - * **STL**は、**コンテナ**、**アルゴリズム**、**イテレータ**の3つの主要コンポーネントから構成される、C++の強力な標準ライブラリです。 - * **コンテナ**は、データを格納するためのクラスです。 - * `std::vector`は、最も一般的に使われる動的配列で、高速なランダムアクセスと末尾への簡単な要素追加が特徴です。 - * `std::map`は、キーと値のペアを管理する連想配列で、キーによる高速な検索が可能です。 - * 他にも`std::list`, `std::set`など、特定の用途に合わせた様々なコンテナが用意されています。 + * **継承**: 既存のクラスの機能を引き継ぎ、コードの再利用性を高める仕組みです。`(子クラス) : public (親クラス)` のように書きます。 + * **ポリモーフィズム**: 「同じ指示でも、オブジェクトの種類によって異なる振る舞いをさせる」性質です。 + * **仮想関数 (`virtual`)**: ポリモーフィズムを実現するための鍵です。親クラスの関数に `virtual` を付けると、ポインタや参照経由で呼び出した際に、オブジェクトの実際の型に応じた関数が実行されます。 + * **オーバーライド (`override`)**: 子クラスで親クラスの仮想関数を上書きする意図を明示します。コンパイラがチェックしてくれるため、安全性が向上します。 + * **抽象クラス**: 1つ以上の**純粋仮想関数 (`virtual ... = 0;`)** を持つクラスです。インスタンス化できず、継承されるための設計図として機能します。 -### 練習問題1: 数値ベクタの操作 +### 練習問題1:乗り物の階層構造 -`std::vector`型の整数のリストに対して、以下の処理を行うプログラムを作成してください。 +`Vehicle` という親クラスを作成し、`move()` というメンバ関数を持たせましょう。次に、`Vehicle` を継承して `Car` クラスと `Motorcycle` クラスを作成し、それぞれが独自の `move()` の振る舞いをするようにオーバーライドしてください。 -1. ベクタに格納されている全ての数値の合計値を計算して表示する。 -2. ベクタの中の最大値を検索して表示する。 -3. ベクタの要素を逆順にして、その内容を表示する。(ヒント:新しいベクタを作っても良いですし、`std::swap`を使っても構いません) +`main` 関数では、`Vehicle` のポインタの配列を作成し、`Car` と `Motorcycle` のオブジェクトを格納して、ループでそれぞれの `move()` を呼び出してください。 ```cpp:practice9_1.cpp #include -#include +#include -int main() { - std::vector numbers = {3, 5, 2, 8, 6}; - // 1. 合計値の計算 +// ここに Vehicle, Car, Motorcycle クラスを定義してください - // 2. 最大値の検索 +int main() { + // Vehicleのポインタの配列を作成 + Vehicle* vehicles[2]; + Car my_car; + Motorcycle my_motorcycle; - // 3. 要素の逆順表示 + vehicles[0] = &my_car; + vehicles[1] = &my_motorcycle; + // それぞれのmove()を呼び出す + for (int i = 0; i < 2; ++i) { + vehicles[i]->move(); + } return 0; } ``` ```cpp-exec:practice9_1.cpp -Sum: 24 -Max: 8 -Reversed: 6 8 2 5 3 ``` +### 問題2: 従業員の給与計算 -### 練習問題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単語ずつ格納される -} -``` +`Employee` という抽象クラスを定義してください。このクラスは、従業員の名前を保持し、給与を計算するための純粋仮想関数 `calculate_salary()` を持ちます。 +次に、`Employee` を継承して、`FullTimeEmployee`(月給制)と `PartTimeEmployee`(時給制)の2つのクラスを作成します。それぞれのクラスで `calculate_salary()` を具体的に実装してください。 +`main` 関数で、それぞれのクラスのインスタンスを作成し、給与が正しく計算されることを確認してください。 ```cpp:practice9_2.cpp #include -#include #include -#include + +// ここに Employee, FullTimeEmployee, PartTimeEmployee クラスを定義してください + int main() { - std::string text = "cpp is fun and cpp is powerful"; + 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:practice9_2.cpp -and: 1 -cpp: 2 -fun: 1 -is: 2 -powerful: 1 +Alice's Salary: $3000 +Bob's Salary: $1600 ``` diff --git a/public/docs/rust-1.md b/public/docs/rust-1.md new file mode 100644 index 0000000..72ace66 --- /dev/null +++ b/public/docs/rust-1.md @@ -0,0 +1,148 @@ +# 第1章: Rustの世界へようこそ + +Rustは、Mozillaによって開始され、現在はRust Foundationによって管理されているオープンソースのシステムプログラミング言語です。Stack OverflowのDeveloper Surveyで長年「最も愛されている言語」に選ばれ続けているのには理由があります。 + +## Rustの特徴:なぜ学ぶのか? + +経験豊富なプログラマにとって、Rustは「トレードオフを解消する」言語として映るはずです。 + +### 1\. メモリ安全性(Memory Safety) + +C/C++ではプログラマの責任であったメモリ管理を、Rustは**所有権(Ownership)**というコンパイル時のシステムで保証します。 + + * ガベージコレクタ(GC)は**存在しません**。 + * ランタイムコストなしに、ダングリングポインタや二重解放、バッファオーバーフローをコンパイル段階で防ぎます。 + +### 2\. ゼロコスト抽象化(Zero-cost Abstractions) + +「使わないものにはコストを払わない。使うものについては、手書きのコード以上のコストをかけない」というC++の哲学を継承しています。イテレータや高階関数を使っても、最適化された低レベルコードと同等のパフォーマンスが得られます。 + +### 3\. 安全な並行性(Fearless Concurrency) + +多くの言語で並行処理はバグの温床(データ競合など)ですが、Rustではコンパイラがデータ競合を検知し、コンパイルエラーとして報告します。「コンパイルが通れば、並行性のバグを含んでいる可能性は低い」という安心感を持ってコーディングできます。 + +## 開発環境の構築 + +Rustの開発環境は非常にモダンで、バージョン管理やパッケージ管理が統合されています。 + +### `rustup` のインストール + +Rustのバージョンマネージャである `rustup` を使用してインストールするのが標準的です。 + +macOS / Linux / WSL (Windows Subsystem for Linux) の場合: + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +Windowsの場合: +公式サイト(rust-lang.org)から `rustup-init.exe` をダウンロードして実行します(C++ビルドツールが必要になる場合があります)。 + +インストール後、以下のコマンドでバージョンが表示されれば成功です。 + +```bash +rustc --version +cargo --version +``` + +## Hello World (`rustc` を直接使う) + +まずは、ビルドシステムを使わずにコンパイラ `rustc` を直接叩いて、Rustプログラムの最小単位を見てみましょう。 + +以下のコードを記述します。 + +```rust:hello.rs +fn main() { + // !がついているのは関数ではなく「マクロ」の呼び出しです + println!("Hello, world from rustc!"); +} +``` + +コンパイルと実行は以下の手順で行います。 + +1. コンパイル: `rustc hello.rs` + * これにより、バイナリファイル(Windowsなら`.exe`)が生成されます。 +2. 実行: `./hello` (Windowsなら `.\hello.exe`) + + + +```rust-exec:hello.rs +Hello, world from rustc! +``` + +### ポイント + + * **`fn main() { ... }`**: エントリポイントです。引数や戻り値がない場合、このように記述します。 + * **`println!`**: 末尾に `!` がついているのは**マクロ**であることを示しています。Rustでは可変長引数を取る機能などをマクロとして実装しています。 + * **セミコロン `;`**: 文の終わりには必須です。 + +## Cargo:Rustのビルドシステムとパッケージマネージャ + +実際の開発では `rustc` を直接使うことは稀です。公式のビルドシステム兼パッケージマネージャである **Cargo** を使用します。Node.jsにおける `npm`、Pythonにおける `pip` + `venv` のような存在ですが、それ以上にプロジェクトのライフサイクル全体を管理します。 + +### プロジェクトの作成 (`cargo new`) + +新しいプロジェクトを作成します。 + +```bash +cargo new hello_cargo +cd hello_cargo +``` + +このコマンドにより、以下のディレクトリ構造が生成されます。 + + * **`Cargo.toml`**: パッケージのマニフェストファイル(依存関係やメタデータを記述)。 + * **`src/main.rs`**: ソースコード。 + * **`.gitignore`**: Gitの設定ファイルも自動生成されます。 + +### Cargoの主要コマンド + +生成されたプロジェクトで、以下のコマンドを試してみましょう。 + +1. **`cargo check`** + + * **重要**: コンパイルが可能かどうかのチェックだけを行い、実行ファイルは生成しません。高速に動作するため、コーディング中の構文チェックとして頻繁に使用します。 + +2. **`cargo build`** + + * デバッグビルドを行います。成果物は `target/debug/` に生成されます。コンパイル速度重視で、最適化は行われません。 + +3. **`cargo run`** + + * ビルドと実行を一度に行います。開発中はこれが最も使われます。 + +4. **`cargo build --release`** + + * リリースビルドを行います。`target/release/` に生成されます。コンパイル時間は長くなりますが、強力な最適化がかかり、実行速度が劇的に向上します。 + +`src/main.rs` の中身は以下のようになっています(生成時デフォルト)。 + +```rust +fn main() { + println!("Hello, world!"); +} +``` + +## フォーマッタとリンタ + +Rustは「公式のスタイル」を重視する言語です。議論の余地をなくすために強力なツールが標準で用意されています。 + +### `rustfmt` (コードフォーマッタ) + +Go言語の `gofmt` のように、コードを自動整形します。 + +```bash +cargo fmt +``` + +多くのエディタ(VS Codeなど)では保存時に自動実行されるよう設定するのが一般的です。 + +### `clippy` (リンタ) + +単なるスタイルチェックだけでなく、パフォーマンスの改善案や、Rustらしい書き方(Idiomatic Rust)を提案してくれます。 + +```bash +cargo clippy +``` + +例えば、無駄な計算や非推奨なAPIの使用などを指摘してくれるため、学習中はこのコマンドの警告に従うだけでRustの理解が深まります。 diff --git a/public/docs/rust-10.md b/public/docs/rust-10.md new file mode 100644 index 0000000..15bfacd --- /dev/null +++ b/public/docs/rust-10.md @@ -0,0 +1,337 @@ +# 第10章: エラーハンドリング + +ようこそ、第10章へ。ここまでRustの所有権や型システムについて学んできましたが、この章では実用的なアプリケーション開発において避けては通れない「エラーハンドリング」について解説します。 + +他の多くの言語(Java, Python, C++など)とRustが最も大きく異なる点の一つが、**「例外(Exception)」が存在しない**ことです。 + +Rustでは、エラーは「誰かがキャッチしてくれることを祈って投げるもの」ではなく、**「戻り値として明示的に処理すべき値」**として扱われます。この設計思想により、予期せぬクラッシュを防ぎ、堅牢なソフトウェアを構築することができます。 + +## エラーの分類 + +Rustでは、エラーを大きく2つのカテゴリーに分類します。 + +1. **回復不可能なエラー (Unrecoverable Errors):** + * バグ、配列の範囲外アクセス、メモリ不足など。 + * プログラムは即座に停止すべき状況。 + * 手段: `panic!` +2. **回復可能なエラー (Recoverable Errors):** + * ファイルが見つからない、パースの失敗、ネットワーク切断など。 + * 呼び出し元で対処(リトライやエラーメッセージ表示)が可能な状況。 + * 手段: `Result` + +## 回復不可能なエラー (`panic!`) + +プログラムが続行不可能な状態に陥った場合、Rustはパニック(panic)を起こします。これはデフォルトでスタックを巻き戻し(unwind)、データを掃除してからプログラムを終了させます。 + +もっとも単純な方法は `panic!` マクロを呼ぶことです。 + +```rust:panic_demo.rs +fn main() { + println!("処理を開始します..."); + + // 何か致命的なことが起きたと仮定 + panic!("ここで致命的なエラーが発生しました!"); + + // 以下の行は実行されません + println!("この行は表示されません"); +} +``` + +```rust-exec:panic_demo.rs +処理を開始します... +thread 'main' panicked at panic_demo.rs:5:5: +ここで致命的なエラーが発生しました! +``` + +**ポイント:** + + * `panic!` は、基本的に「プログラムのバグ」や「どうしようもない状況」でのみ使用します。 + * 通常の制御フロー(入力値のバリデーション失敗など)には使用しません。 + +## 回復可能なエラー (`Result`) + +Rustのエラーハンドリングの主役は `Result` 列挙型です。以前の章で学んだ `Option` に似ていますが、失敗した場合に「なぜ失敗したか(エラー内容)」を持つ点が異なります。 + +定義は以下のようになっています(標準ライブラリに含まれています)。 + +```rust +enum Result { + Ok(T), // 成功時:値 T を含む + Err(E), // 失敗時:エラー E を含む +} +``` + +### 基本的な `Result` の処理 + +他の言語での `try-catch` の代わりに、Rustでは `match` 式を使って成功と失敗を分岐させるのが基本です。 + +```rust:result_basic.rs +fn divide(numerator: f64, denominator: f64) -> Result { + if denominator == 0.0 { + // 失敗時は Err でラップして返す + return Err(String::from("0で割ることはできません")); + } + // 成功時は Ok でラップして返す + Ok(numerator / denominator) +} + +fn main() { + let inputs = vec![(10.0, 2.0), (5.0, 0.0)]; + + for (num, den) in inputs { + let result = divide(num, den); + + match result { + Ok(val) => println!("{} / {} = {}", num, den, val), + Err(e) => println!("エラー: {}", e), + } + } +} +``` + +```rust-exec:result_basic.rs +10 / 2 = 5 +エラー: 0で割ることはできません +``` + +この明示的な分岐により、プログラマはエラー処理を「忘れる」ことができなくなります(コンパイラが `Result` を無視すると警告を出したり、使おうとすると型エラーになるため)。 + +## `unwrap` と `expect` の使い所 + +毎回 `match` で分岐を書くのは冗長な場合があります。「失敗したらプログラムをクラッシュさせていい」という場合や、「ここでは絶対に失敗しない」と確信がある場合のために、ヘルパーメソッドが用意されています。 + +### `unwrap` + +`Result` が `Ok` なら中身を返し、`Err` なら即座に `panic!` します。手っ取り早いですが、エラーメッセージは一般的で詳細が含まれません。 + +### `expect` + +`unwrap` と同じ挙動ですが、パニック時に表示するメッセージを指定できます。**デバッグのしやすさから、通常は `unwrap` よりも `expect` が推奨されます。** + +```rust:unwrap_expect.rs +fn main() { + let valid_str = "100"; + let invalid_str = "hello"; + + // 1. unwrap: 成功時は値を返す + let n: i32 = valid_str.parse().unwrap(); + println!("パース成功: {}", n); + + // 2. expect: 失敗時は指定したメッセージと共に panic! する + // 以下の行を実行するとクラッシュします + let _m: i32 = invalid_str.parse().expect("数値のパースに失敗しました"); +} +``` + +```rust-exec:unwrap_expect.rs +thread 'main' panicked at unwrap_expect.rs:11:35: +数値のパースに失敗しました: ParseIntError { kind: InvalidDigit } +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace +``` + +**使い所のヒント:** + + * **プロトタイピング・実験:** `unwrap` を多用してロジックを素早く組む。 + * **テストコード:** テスト中に失敗したらテスト自体を落としたいので `unwrap` が有用。 + * **「絶対に失敗しない」ロジック:** 理論上エラーにならない場合でも、型合わせのために `unwrap` が必要な場合があります。 + +## エラーの伝播(`?` 演算子) + +関数内でエラーが発生した際、その場で処理せずに呼び出し元へエラーを返したいことがよくあります。これを「エラーの伝播(propagation)」と呼びます。 + +Rustにはこれを劇的に短く書くための **`?` 演算子** があります。 + + * `Result` 値の後ろに `?` を置く。 + * 値が `Ok` なら、中身を取り出して処理を続行。 + * 値が `Err` なら、**その関数から即座に `return Err(...)` する。** + + + +```rust:error_propagation.rs +use std::num::ParseIntError; + +// 文字列を受け取り、最初の文字を切り出して数値に変換し、10倍して返す +fn get_first_digit_scaled(text: &str) -> Result { + // 1. 文字列が空の場合のエラー処理 + let first_char = text.chars().next().ok_or("空の文字列です".to_string())?; + + // 2. 文字を数値にパース(失敗したらエラー伝播) + // ParseIntError を String に変換するために map_err を使用しています + // (?演算子はFromトレイトを使って自動変換を行いますが、ここでは単純化のため手動変換します) + let num: i32 = first_char.to_string() + .parse() + .map_err(|_| format!("'{}' は数値ではありません", first_char))?; + + Ok(num * 10) +} + +fn main() { + match get_first_digit_scaled("5 apples") { + Ok(v) => println!("計算結果: {}", v), + Err(e) => println!("エラー発生: {}", e), + } + + match get_first_digit_scaled("banana") { + Ok(v) => println!("計算結果: {}", v), + Err(e) => println!("エラー発生: {}", e), + } +} +``` + +```rust-exec:error_propagation.rs +計算結果: 50 +エラー発生: 'b' は数値ではありません +``` + +`?` 演算子を使うことで、`match` のネスト地獄(右方向へのドリフト)を防ぎ、コードの流れを「成功ルート」を中心に記述できます。 + +## カスタムエラー型の定義 + +ライブラリや大規模なアプリケーションを作る場合、`String` 型のエラーでは情報が不足します。Rustでは `std::error::Error` トレイトを実装した独自の型(通常は Enum)を定義するのが一般的です。 + +Rustのエコシステムでは、ボイラープレート(定型コード)を減らすために **`thiserror`** というクレートが非常に人気ですが、ここでは仕組みを理解するために標準機能だけで実装してみます。 + +```rust:custom_error.rs +use std::fmt; + +// 1. 独自のエラー型を定義 +#[derive(Debug)] +enum MyToolError { + IoError(String), + ParseError(String), + LogicError, +} + +// 2. Display トレイトの実装(ユーザー向けのエラーメッセージ) +impl fmt::Display for MyToolError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + MyToolError::IoError(msg) => write!(f, "IOエラー: {}", msg), + MyToolError::ParseError(msg) => write!(f, "解析エラー: {}", msg), + MyToolError::LogicError => write!(f, "ロジックエラーが発生しました"), + } + } +} + +// 3. Error トレイトの実装(これを実装することで、Rustのエラーエコシステムと統合される) +impl std::error::Error for MyToolError {} + +// 使用例 +fn dangerous_operation(input: i32) -> Result { + match input { + 0 => Err(MyToolError::IoError("ディスク書き込み失敗".into())), + 1 => Err(MyToolError::ParseError("不正なヘッダ".into())), + 2 => Err(MyToolError::LogicError), + _ => Ok("成功!".into()), + } +} + +fn main() { + let results = [dangerous_operation(0), dangerous_operation(3)]; + + for res in results { + match res { + Ok(s) => println!("結果: {}", s), + Err(e) => println!("失敗: {}", e), // Displayの実装が使われる + } + } +} +``` + +```rust-exec:custom_error.rs +失敗: IOエラー: ディスク書き込み失敗 +結果: 成功! +``` + +**補足:** `thiserror` クレートを使うと、上記の `impl fmt::Display` などをマクロで自動生成でき、以下のように簡潔に書けます(参考情報)。 + +```rust +// thiserrorを使った場合のイメージ +#[derive(thiserror::Error, Debug)] +enum MyToolError { + #[error("IOエラー: {0}")] + IoError(String), + // ... +} +``` + +## この章のまとめ + + * **例外はない**: Rustは戻り値 `Result` でエラーを表現する。 + * **Panic**: 回復不可能なエラーには `panic!` を使うが、乱用しない。 + * **Result処理**: 基本は `match` で処理する。 + * **便利メソッド**: `unwrap` は強制取り出し(失敗時パニック)、`expect` はメッセージ付きパニック。 + * **?演算子**: エラーが発生したら即座に呼び出し元へ `Err` を返すショートカット。 + * **型安全性**: コンパイラがエラー処理の漏れを指摘してくれるため、堅牢なコードになる。 + +### 練習問題 1: 安全な割り算 + +2つの `f64` を受け取り、割り算の結果を返す関数 `safe_div` を作成してください。 + + * 戻り値は `Result` としてください。 + * 0で割ろうとした場合は、「Division by zero」というエラーメッセージを含む `Err` を返してください。 + * `main` 関数で、正常なケースとエラーになるケースの両方を呼び出し、結果を表示してください。 + + + +```rust:practice10_1.rs +fn safe_div(a: f64, b: f64) -> Result { + // ここにコードを書いてください + +} + +fn main() { + let test_cases = vec![(10.0, 2.0), (5.0, 0.0)]; + + for (a, b) in test_cases { + match safe_div(a, b) { + Ok(result) => println!("{} / {} = {}", a, b, result), + Err(e) => println!("エラー: {}", e), + } + } +} +``` + +```rust-exec:practice10_1.rs +10 / 2 = 5 +エラー: Division by zero +``` + +### 練習問題 2: データ処理チェーン(エラー伝播) + +文字列形式の数値(例: "10,20,30")を受け取り、カンマ区切りの最初の数値を2倍にして返す関数 `process_csv_data` を作成してください。 + + * 以下の手順を行い、途中でエラーがあれば `?` 演算子などを使って伝播させてください。 + 1. 文字列をカンマ `,` で分割し( `split` メソッド)、最初の要素を取得する(要素がない場合はエラー)。 + 2. 取得した文字列を `i32` にパースする( `parse` メソッド)(パース失敗はエラー)。 + 3. 数値を2倍して返す。 + * 関数の戻り値は `Result` とします(エラー型の変換が必要な場合は `map_err` を活用してください)。 + + + +```rust:practice10_2.rs +fn process_csv_data(csv: &str) -> Result { + + +} + +fn main() { + let inputs = ["10,20,30", "abc,20", "", " ,50"]; + + for input in inputs { + print!("Input: {:<10} => ", format!("\"{}\"", input)); + match process_csv_data(input) { + Ok(n) => println!("結果: {}", n), + Err(e) => println!("エラー: {}", e), + } + } +} +``` + +```rust-exec:practice10_2.rs +Input: "10,20,30" => 結果: 20 +Input: "abc,20" => エラー: 'abc' は数値ではありません +Input: "" => エラー: 要素が空です +Input: " ,50" => エラー: 要素が空です +``` + diff --git a/public/docs/rust-11.md b/public/docs/rust-11.md new file mode 100644 index 0000000..a9ee48e --- /dev/null +++ b/public/docs/rust-11.md @@ -0,0 +1,327 @@ +# 第11章: ジェネリクスとトレイト + +Rustチュートリアルの第11章へようこそ。 +この章では、Rustにおける抽象化とコード再利用の核心である「ジェネリクス」と「トレイト」について解説します。 + +他のプログラミング言語での経験がある方にとって、ジェネリクスは馴染み深い概念かもしれませんが、トレイトはクラス継承とは異なるアプローチをとります。これらを理解することで、柔軟かつ高速なRustコードが書けるようになります。 + +## 他言語との違い:Rustのアプローチ + +Rustのジェネリクスとトレイトは、C++のテンプレートやHaskellの型クラスに近い性質を持っています。JavaやPythonなどのオブジェクト指向言語(OOP)出身の方が特に意識すべき違いは以下の通りです。 + +1. **継承の欠如:** Rustにはクラス継承(`extends`)がありません。代わりに**トレイト(Trait)**を使用して共通の振る舞いを定義し、構造体(Struct)や列挙型(Enum)に実装します。これは「継承よりコンポジション(構成)」を好む現代的な設計思想を言語レベルで強制するものです。 +2. **ダックタイピングとの違い:** Pythonなどの動的型付け言語では「アヒルのように歩き、アヒルのように鳴くなら、それはアヒルだ」というダックタイピングが一般的ですが、Rustではコンパイル時に厳密に型チェックを行います。「アヒルのように鳴く」能力があることを**トレイト境界**で明示する必要があります。 +3. **静的ディスパッチと単相化(Monomorphization):** Rustのジェネリクスは、コンパイル時に具体的な型ごとにコードを生成(展開)します。これを単相化と呼びます。 + * **メリット:** 実行時のオーバーヘッドがゼロ(C++のテンプレートと同様)。仮想関数テーブル(vtable)を参照する動的ディスパッチのようなコストがかかりません。 + * **デメリット:** バイナリサイズが若干大きくなる可能性があります。 + +## ジェネリックなデータ型と関数 + +ジェネリクスを使用すると、具体的なデータ型に依存しないコードを書くことができます。Rustでは慣習として `T` (Typeの略)などの短い大文字識別子を使用します。 + +### ジェネリックな関数 + +もっとも単純な例として、型 `T` の引数をそのまま返す関数を考えてみましょう。 + +```rust:generic_function.rs +fn inspect(value: T) { + // 実際にはここで何か処理を行うが、 + // Tが何であるか(DisplayやDebug等)を知らないと + // プリントすらできないため、ここでは単純にスコープを抜ける +} + +fn main() { + inspect(10); // i32 + inspect(3.14); // f64 + inspect("Hello"); // &str + println!("Compilation successful."); +} +``` + +```rust-exec:generic_function.rs +Compilation successful. +``` + +これだけではあまり役に立ちませんが、構文としては `fn 関数名<型パラメータ>(引数)` という形になります。 + +### ジェネリックな構造体 + +構造体のフィールドの型をジェネリックにすることも可能です。 + +```rust:generic_struct.rs +// T型のxとyを持つPoint構造体 +struct Point { + x: T, + y: T, +} + +// 異なる型を持たせたい場合は複数のパラメータを使う +struct MixedPoint { + x: T, + y: U, +} + +fn main() { + let integer = Point { x: 5, y: 10 }; + let float = Point { x: 1.0, y: 4.0 }; + + // 以下の行はコンパイルエラーになる(xとyが同じTである必要があるため) + // let error = Point { x: 5, y: 4.0 }; + + let mixed = MixedPoint { x: 5, y: 4.0 }; + + println!("Int Point: x = {}, y = {}", integer.x, integer.y); + println!("Mixed Point: x = {}, y = {}", mixed.x, mixed.y); +} +``` + +```rust-exec:generic_struct.rs +Int Point: x = 5, y = 10 +Mixed Point: x = 5, y = 4 +``` + +## トレイトの定義と実装 + +トレイトは、**「特定の型がどのような機能を持っているか」**を定義するものです。JavaやC\#の「インターフェース」に非常に近い概念です。 + +### トレイトの定義 + +ここでは、「情報を要約できる」という振る舞いを表す `Summary` トレイトを定義してみましょう。 + +```rust +pub trait Summary { + fn summarize(&self) -> String; // メソッドのシグネチャのみ定義 +} +``` + +### トレイトの実装 + +定義したトレイトを具体的な型に実装するには、`impl トレイト名 for 型名` ブロックを使用します。 + +```rust:trait_impl.rs +// トレイトの定義 +trait Summary { + fn summarize(&self) -> String; + + // デフォルト実装を持たせることも可能 + fn greeting(&self) -> String { + String::from("(Read more below...)") + } +} + +struct NewsArticle { + headline: String, + location: String, + author: String, +} + +// NewsArticleにSummaryトレイトを実装 +impl Summary for NewsArticle { + fn summarize(&self) -> String { + format!("{}, by {} ({})", self.headline, self.author, self.location) + } +} + +struct Tweet { + username: String, + content: String, +} + +// TweetにSummaryトレイトを実装 +impl Summary for Tweet { + fn summarize(&self) -> String { + format!("{}: {}", self.username, self.content) + } + // greetingはデフォルト実装を使用するため記述しない +} + +fn main() { + let article = NewsArticle { + headline: String::from("Rust 1.0 Released"), + location: String::from("Internet"), + author: String::from("Core Team"), + }; + + let tweet = Tweet { + username: String::from("horse_ebooks"), + content: String::from("of course, as you probably already know, people"), + }; + + println!("Article: {}", article.summarize()); + println!("Tweet: {} {}", tweet.summarize(), tweet.greeting()); +} +``` + +```rust-exec:trait_impl.rs +Article: Rust 1.0 Released, by Core Team (Internet) +Tweet: horse_ebooks: of course, as you probably already know, people (Read more below...) +``` + +## トレイト境界(Trait Bounds) + +ジェネリック関数を作る際、型 `T` に対して「どんな型でもいい」のではなく、「特定の機能(トレイト)を持っている型だけ受け付けたい」という場合がほとんどです。これを制約するのが**トレイト境界**です。 + +### 基本的な構文 + +以下の関数は、引数 `item` が `Summary` トレイトを実装していることを要求します。 + +```rust +// 糖衣構文(Syntactic Sugar) +fn notify(item: &impl Summary) { + println!("Breaking news! {}", item.summarize()); +} + +// 正式なトレイト境界の構文 +fn notify_formal(item: &T) { + println!("Breaking news! {}", item.summarize()); +} +``` + +### 複数のトレイト境界と `where` 句 + +複数のトレイトが必要な場合(例えば「表示可能」かつ「要約可能」であってほしい場合)、`+` でつなぎます。制約が多くなりシグネチャが長くなる場合は、`where` 句を使って整理できます。 + +```rust:trait_bounds.rs +use std::fmt::Display; + +trait Summary { + fn summarize(&self) -> String; +} + +struct Book { + title: String, + author: String, +} + +impl Summary for Book { + fn summarize(&self) -> String { + format!("{} by {}", self.title, self.author) + } +} + +// Displayトレイトは標準ライブラリで定義されている(println!等で使用) +impl Display for Book { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Book({})", self.title) + } +} + +// itemはSummaryとDisplayの両方を実装している必要がある +fn notify(item: &T) +where + T: Summary + Display, +{ + println!("Notify: {}", item.summarize()); + println!("Display format: {}", item); +} + +fn main() { + let b = Book { + title: String::from("The Rust Book"), + author: String::from("Steve Klabnik"), + }; + + notify(&b); +} +``` + +```rust-exec:trait_bounds.rs +Notify: The Rust Book by Steve Klabnik +Display format: Book(The Rust Book) +``` + +## 代表的な標準トレイト + +Rustには、すべてのRustプログラマが知っておくべき標準トレイトがいくつかあります。これらはしばしば `#[derive(...)]` 属性を使って自動的に実装されます。 + +1. **`Debug`**: `{:?}` でフォーマット出力するためのトレイト。開発中のデバッグ出力に必須です。 +2. **`Display`**: `{}` でフォーマット出力するためのトレイト。ユーザー向けの表示に使います。自動導出(derive)はできず、手動実装が必要です。 +3. **`Clone`**: `.clone()` メソッドで明示的にディープコピー(またはそれに準ずる複製)を作成するためのトレイト。 +4. **`Copy`**: 値がビット単位のコピーで複製できることを示すマーカートレイト。これが実装されている型(`i32`など)は、代入しても所有権が移動(Move)せず、コピーされます。 + + + +```rust:std_traits.rs +// Debug, Clone, Copyを自動導出 +#[derive(Debug, Clone, Copy)] +struct Point { + x: i32, + y: i32, +} + +fn main() { + let p1 = Point { x: 10, y: 20 }; + + // Copyトレイトがあるので、p1はmoveされない。コピーされる。 + let p2 = p1; + + // Debugトレイトがあるので {:?} が使える + println!("p1: {:?}", p1); + println!("p2: {:?}", p2); + + // Cloneトレイトがあるので明示的に呼ぶこともできる(Copyがある場合、動作はCopyと同じになることが多い) + let p3 = p1.clone(); + println!("p3: {:?}", p3); +} +``` + +```rust-exec:std_traits.rs +p1: Point { x: 10, y: 20 } +p2: Point { x: 10, y: 20 } +p3: Point { x: 10, y: 20 } +``` + +> **注意:** `String` や `Vec` などのヒープ領域へのポインタを持つ型は、所有権のルール上、安易に `Copy` を実装できません(二重解放エラーになるため)。それらは `Clone` のみを実装します。 + +# この章のまとめ + + * **ジェネリクス**: 型をパラメータ化し、コードの重複を減らします。コンパイル時に単相化されるため、実行時コストがかかりません。 + * **トレイト**: 共通の振る舞いを定義します。インターフェースに似ていますが、継承ではなくコンポジションを促進します。 + * **トレイト境界**: ジェネリクス型 `T` に対して、「特定のトレイトを実装している型のみ」を受け入れるよう制約を課します。 + * **標準トレイト**: `Debug`, `Display`, `Clone`, `Copy` など、Rustの基本動作を支える重要なトレイトが存在します。 + +これらを使いこなすことで、Rustコンパイラに安全性を保証させつつ、再利用性の高いライブラリのようなコードを書くことができるようになります。 + +### 練習問題 1: ジェネリックなペア + +2つの異なる型 `T` と `U` を保持できる構造体 `Pair` を作成してください。 +そして、その構造体にメソッド `new` (インスタンス作成)と、デバッグ出力をするメソッド `print_pair` を実装してください。 +(ヒント:`print_pair` 内で `T` と `U` を表示するには、それぞれの型に `Debug` トレイトの制約が必要です) + +```rust:practice11_1.rs + + + +fn main() { + let pair = Pair::new(10, "Hello"); + pair.print_pair(); +} +``` +```rust-exec:practice11_1.rs +10 and "Hello" +``` + +### 問題 2: 最大値を探す + +ジェネリックなスライス `&[T]` を受け取り、その中の最大値を返す関数 `largest` を作成してください。 +比較を行うためには `T` にどのようなトレイト境界が必要か考えてください(ヒント:比較には `std::cmp::PartialOrd` が必要です。また、スライスから値を取り出して返すには `Copy` があると簡単です)。 + +```rust:practice11_2.rs + + + +fn main() { + let number_list = vec![34, 50, 25, 100, 65]; + let result = largest(&number_list); + println!("The largest number is {}", result); + + let char_list = vec!['y', 'm', 'a', 'q']; + let result = largest(&char_list); + println!("The largest char is {}", result); +} +``` + +```rust-exec:practice_solutions.rs +The largest number is 100 +The largest char is y +``` diff --git a/public/docs/rust-12.md b/public/docs/rust-12.md new file mode 100644 index 0000000..37de42b --- /dev/null +++ b/public/docs/rust-12.md @@ -0,0 +1,252 @@ +# 第12章: ライフタイム(Lifetimes) + +ようこそ、Rustの学習における「最難関」とも呼ばれる章へ。 +これまで所有権や借用(第4章、第5章)を学んできましたが、**ライフタイム(Lifetimes)** はそれらを支えるコンパイラのロジックそのものです。 + +他の言語(C/C++)では、メモリが解放された後にその場所を指し続ける「ダングリングポインタ」がバグの温床でした。JavaやPythonのようなガベージコレクション(GC)を持つ言語では、これを自動で管理しますが、パフォーマンスのコストがかかります。 + +Rustは**「コンパイル時に参照の有効性を厳密にチェックする」**ことで、GCなしでメモリ安全性を保証します。そのための仕組みがライフタイムです。 + +## ライフタイムとは何か + +ライフタイムとは、簡単に言えば**「その参照が有効である期間(スコープ)」**のことです。 + +実は、これまでの章でもあなたは無意識にライフタイムを使用してきました。通常、コンパイラが自動的に推論してくれるため、明示する必要がなかっただけです。しかし、コンパイラが「参照の有効期間が不明瞭だ」と判断した場合、プログラマが明示的に注釈(アノテーション)を加える必要があります。 + +### ダングリング参照を防ぐ + +ライフタイムの主な目的は、無効なデータを指す参照を作らせないことです。 + +以下のコードを見てください(これはコンパイルエラーになります)。 + +```rust +{ + let r; // ---------+-- rのライフタイム + // | + { // | + let x = 5; // -+-- xのライフタイム + r = &x; // | | + } // -+ | + // | + println!("r: {}", r); // | +} // ---------+ +``` + +ここで、`r` は `x` を参照しようとしています。しかし、内側のブロック `{}` が終わった時点で `x` は破棄されます。その後に `r` を使おうとすると、`r` は「解放されたメモリ」を指していることになります。 + +Rustのコンパイラ(**借用チェッカー**)は、このスコープのズレを検知し、「`x` の寿命が短すぎる」としてエラーを出します。 + +## 関数のライフタイム注釈 + +最も頻繁にライフタイム注釈が必要になるのは、**「引数として参照を受け取り、戻り値として参照を返す関数」**です。 + +2つの文字列スライスを受け取り、長い方を返す関数 `longest` を考えてみましょう。 + +```rust:longest_fail.rs +fn longest(x: &str, y: &str) -> &str { + if x.len() > y.len() { + x + } else { + y + } +} + +fn main() { + let string1 = String::from("abcd"); + let string2 = "xyz"; + + let result = longest(string1.as_str(), string2); + println!("The longest string is {}", result); +} +``` + +このコードをコンパイルしようとすると、以下のようなエラーが出ます。 + +```rust-exec:longest_fail.rs +error[E0106]: missing lifetime specifier + | +1 | fn longest(x: &str, y: &str) -> &str { + | ---- ---- ^ expected named lifetime parameter + | + = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `x` or `y` +``` + +**なぜエラーになるのか?** +コンパイラには、`longest` 関数が `x` を返すのか `y` を返すのか実行時まで分かりません。そのため、**戻り値の参照がいつまで有効であれば安全なのか(xの寿命に合わせるべきか、yの寿命に合わせるべきか)** を判断できないのです。 + +### ライフタイム注釈の構文 + +ここで**ジェネリックなライフタイム注釈**が登場します。 +構文は `'a` のようにアポストロフィから始まる名前を使います。通常は `'a`(a, b, c...)が使われます。 + +注釈のルールは以下の通りです: + + * 関数名の後に `<'a>` でライフタイムパラメータを宣言する。 + * 引数と戻り値の参照に `&'a str` のように付与する。 + +修正したコードがこちらです。 + +```rust:longest_success.rs +// 'a というライフタイムを宣言し、 +// 「引数x、引数y、そして戻り値は、すべて少なくとも 'a と同じ期間だけ生きている」 +// という制約をコンパイラに伝える。 +fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { + if x.len() > y.len() { + x + } else { + y + } +} + +fn main() { + let string1 = String::from("long string is long"); + let result; + { + let string2 = String::from("xyz"); + // resultのライフタイムは、string1とstring2のうち「短い方」の寿命に制約される + result = longest(string1.as_str(), string2.as_str()); + println!("The longest string is '{}'", result); + } + // ここで string2 がドロップされるため、result も無効になる。 + // もしここで result を使おうとするとコンパイルエラーになる(安全!)。 + // println!("The longest string is '{}'", result); +} +``` + +```rust-exec:longest_success.rs +The longest string is 'long string is long' +``` + +**重要なポイント:** +ライフタイム注釈 `'a` は、変数の寿命を**延ばすものではありません**。 +「複数の参照の寿命の関係性」をコンパイラに説明し、**「渡された参照の中で最も寿命が短いもの」** に戻り値の寿命を合わせるように制約するものです。 + +## ライフタイム省略ルール + +「待ってください。第4章で書いた `fn first_word(s: &str) -> &str` は注釈なしで動きましたよ?」 + +鋭い質問です。初期のRustではすべての参照に明示的なライフタイムが必要でした。しかし、あまりにも頻出するパターン(例えば「引数が1つなら、戻り値のライフタイムもそれと同じ」など)があったため、Rustチームはそれらを自動推論する**「ライフタイム省略ルール(Lifetime Elision Rules)」**をコンパイラに組み込みました。 + +コンパイラは以下の3つのルールを順番に適用します。それでもライフタイムが決まらない場合のみ、エラーを出して人間に注釈を求めます。 + +1. **各引数に独自のライフタイムを割り当てる** + * `fn foo(x: &str, y: &str)` → `fn foo<'a, 'b>(x: &'a str, y: &'b str)` +2. **入力ライフタイムが1つだけなら、そのライフタイムをすべての出力(戻り値)に割り当てる** + * `fn foo<'a>(x: &'a str) -> &'a str` + * これが `first_word` 関数で注釈が不要だった理由です。 +3. **メソッド(`&self` または `&mut self` を含む)の場合、`self` のライフタイムをすべての出力に割り当てる** + * これにより、構造体のメソッドを書く際にいちいち注釈を書かずに済みます。 + +## 構造体定義におけるライフタイム注釈 + +これまでの章では、構造体には `String` や `i32` などの「所有される型」を持たせてきました。 +しかし、構造体に**参照**を持たせたい場合もあります。その場合、**「構造体そのものよりも、中の参照先が長生き(あるいは同等の寿命)である」**ことを保証しなければなりません。 + +```rust:struct_lifetime.rs +// ImportantExcerpt構造体は、'a という期間だけ生きる文字列スライスを保持する +struct ImportantExcerpt<'a> { + part: &'a str, +} + +fn main() { + let novel = String::from("Call me Ishmael. Some years ago..."); + + // 最初の文(ピリオドまで)を取得 + let first_sentence = novel.split('.').next().expect("Could not find a '.'"); + + // 構造体のインスタンスを作成 + // part は novel の一部を参照している。 + // novel が有効である限り、i も有効であることが保証される。 + let i = ImportantExcerpt { + part: first_sentence, + }; + + println!("Novel start: {}", i.part); +} +``` + +```rust-exec:struct_lifetime.rs +Novel start: Call me Ishmael +``` + +もし `User` 構造体のような定義で `<'a>` を忘れると、「参照を持たせるならライフタイムを指定せよ」というエラーになります。これは、構造体が生きている間に参照先のデータが消えてしまうのを防ぐためです。 + +## 静的ライフタイム ('static) + +特別なライフタイムとして `'static` があります。 +これは、**「参照がプログラムの実行期間全体にわたって有効である」**ことを意味します。 + +すべての文字列リテラルは `'static` ライフタイムを持っています。なぜなら、それらはプログラムのバイナリ自体に埋め込まれており、メモリ上の位置が固定されているからです。 + +```rust +let s: &'static str = "I have a static lifetime."; +``` + +**注意点:** +エラーメッセージで「`'static` が必要です」と提案されることがありますが、安易に `'static` を使って解決しようとしないでください。多くの場合、それは「参照ではなく所有権を持つべき」か「ライフタイムの関係を正しく記述すべき」場面であり、本当にプログラム終了までデータを保持し続けたいケースは稀です。 + +## この章のまとめ + + * **ライフタイム**は、参照が有効なスコープの長さを表す概念です。 + * Rustの**借用チェッカー**は、ライフタイムを比較してダングリング参照(無効なメモリへのアクセス)を防ぎます。 + * 関数で複数の参照を扱い、戻り値がそれらに依存する場合、`<'a>` のような**ジェネリックライフタイム注釈**が必要です。 + * **構造体**に参照を持たせる場合も、ライフタイム注釈が必要です。 + * **省略ルール**のおかげで、一般的なケースでは注釈を省略できます。 + +ライフタイムの記法は最初は「ノイズ」に見えるかもしれませんが、これは「メモリ安全性をコンパイラとプログラマが対話するための言語」です。これを理解すれば、C++のような複雑なメモリ管理の落とし穴を完全に回避できます。 + +### 練習問題 1: 参照を持つ構造体とメソッド + +以下の要件を満たすコードを作成してください。 + +1. `Book` という構造体を定義してください。 +2. この構造体は `title` というフィールドを持ち、それは `String` ではなく文字列スライス `&str` です(ライフタイム注釈が必要です)。 +3. `main` 関数で `String` 型の変数(例: `"The Rust Programming Language"`)を作成し、`Book` のインスタンスにその参照を渡してください。 +4. `Book` のインスタンスを表示してください(`Debug` トレイを導出(`#[derive(Debug)]`)して構いません)。 + +```rust:practice12_1.rs +// ここにBookの定義を書いてください + + +fn main() { + let book_title = String::from("The Rust Programming Language"); + let my_book = Book { + title: &book_title, + }; + + println!("Book details: {:?}", my_book); +} +``` + +```rust-exec:practice12_1.rs +Book details: Book { title: "The Rust Programming Language" } +``` + +### 練習問題 2: 最初の単語を返す関数(ライフタイム付き) + +以下の要件を満たすコードを作成してください。 + +1. 2つの文字列スライス `str1`, `str2` を受け取る関数 `first_word_of_longer` を作成してください。 +2. この関数は、2つの文字列のうち**長い方**を選び、その文字列の**最初の単語**(スペースまで)をスライスとして返します。 + * 例: "Hello World" と "Hi" なら、"Hello" を返す。 +3. 引数と戻り値に適切なライフタイム注釈 `'a` を付けてください。 +4. `main` 関数で動作確認をしてください。 + +*(ヒント: 単語の切り出しは `s.split_whitespace().next()` などが使えますが、戻り値のライフタイムが引数と紐付いていることが重要です)* + +```rust:practice12_2.rs +// ここにfirst_word_of_longer関数を書いてください + + +fn main() { + let string1 = String::from("Hello World from Rust"); + let string2 = String::from("Hi"); + + let first_word = first_word_of_longer(string1.as_str(), string2.as_str()); + println!("The first word of the longer string is: '{}'", first_word); +} +``` + +```rust-exec:practice12_2.rs +The first word of the longer string is: 'Hello' +``` diff --git a/public/docs/rust-2.md b/public/docs/rust-2.md new file mode 100644 index 0000000..baab9e7 --- /dev/null +++ b/public/docs/rust-2.md @@ -0,0 +1,209 @@ +# 第2章: 基本構文と「不変性」の哲学 + +他の言語での開発経験がある方にとって、Rustの学習で最初に直面するカルチャーショックが「変数の扱い」です。 + +C++やJava、Pythonなどでは「変数はデフォルトで書き換え可能(Mutable)」であることが一般的ですが、Rustでは**「デフォルトで不変(Immutable)」**です。この設計こそが、Rustがコンパイル時に多くのバグを排除できる理由の根幹にあります。 + +第2章では、Rustの構文の基礎と、その背後にある哲学を学びます。 + +## 変数と可変性(let vs let mut) + +Rustでは変数を宣言するために `let` キーワードを使用します。しかし、単に `let` で宣言された変数は、値を一度代入すると二度と変更できません。 + +### 不変変数(Immutable) + +まず、以下のコードを見てください。これは意図的にコンパイルエラーになるように書かれています。 + +```rust:immutability_error.rs +fn main() { + let x = 5; + println!("xの値は: {}", x); + + // 値を再代入しようとする(コンパイルエラーになる) + x = 6; + println!("xの値は: {}", x); +} +``` + +```rust-exec:immutability_error.rs +error[E0384]: cannot assign twice to immutable variable `x` + --> immutability_error.rs:6:5 + | +2 | let x = 5; + | - first assignment to `x` +... +6 | x = 6; + | ^^^^^ cannot assign twice to immutable variable + | + +``` + +これをコンパイルしようとすると、Rustコンパイラは「不変変数 `x` に二度代入することはできない」と強く叱ってくれます。 + +### 可変変数(Mutable) + +値を変更したい場合は、`mut`(mutableの略)キーワードを明示的に付ける必要があります。これにより、「この変数は値が変わる可能性がある」とコードの読み手やコンパイラに宣言します。 + +```rust:mutability_demo.rs +fn main() { + // mut をつけることで可変になる + let mut x = 5; + println!("xの値は: {}", x); + + x = 6; + println!("xの値は: {}", x); +} +``` + +```rust-exec:mutability_demo.rs +xの値は: 5 +xの値は: 6 +``` + +> **なぜデフォルトが不変なのか?** +> 大規模なシステムや並行処理において、「いつの間にか値が変わっている」ことはバグの主要な原因です。Rustは「変更が必要な箇所だけを明示的にする」ことで、コードの予測可能性と安全性を高めています。 + +## シャドーイング(Shadowing) + +Rustには、他の言語ではあまり見られない**シャドーイング(Shadowing)という強力な機能があります。これは、同じ名前の変数を `let` を使って再宣言**することで、前の変数を「覆い隠す」機能です。 + +`mut` との違いは2点あります: + +1. **型を変更できる**: `mut` は値の変更しかできませんが、シャドーイングは新しい変数を宣言しているのと同じなので、型を変えることができます。 +2. **不変に戻る**: 処理が終わった後、その変数は再び不変(immutable)になります。 + + + +```rust:shadowing.rs +fn main() { + let x = 5; + + // 最初のシャドーイング: 値を加工する + let x = x + 1; + + { + // 内側のスコープでのシャドーイング + let x = x * 2; + println!("内側のスコープでのx: {}", x); + } + + // スコープを抜けると、外側のxの値が参照される + println!("外側のスコープでのx: {}", x); + + // 型の変更を伴うシャドーイングの例 + let spaces = " "; // 文字列スライス型 + let spaces = spaces.len(); // usize型(整数) + + println!("スペースの数: {}", spaces); +} +``` + +```rust-exec:shadowing.rs +内側のスコープでのx: 12 +外側のスコープでのx: 6 +スペースの数: 3 +``` + +他言語では `spaces_str`、`spaces_num` のように変数名を変える場面でも、Rustでは変数の意味が変わらなければ同じ名前を再利用してコードをすっきり保つことができます。 + +## 基本的なデータ型 + +Rustは静的型付け言語ですが、型推論が強力なため、多くの場合に型注釈は不要です。しかし、ここでは明示的な理解のために型を見ていきます。 + +### スカラー型(単一の値を表す) + +1. **整数型**: + * `i32` (デフォルト), `i64`, `u32`, `u8` など。 + * アーキテクチャ依存の `isize`, `usize` (インデックス用によく使われる)。 +2. **浮動小数点型**: + * `f64` (デフォルト, 倍精度), `f32` (単精度)。 +3. **論理値型**: + * `bool` (`true` / `false`)。 +4. **文字型**: + * `char`。Rustのcharは4バイトで、Unicodeスカラー値を扱います(ASCIIだけでなく、漢字や絵文字も1文字として扱えます)。シングルクォート `'` で囲みます。 + +### 複合型(複数の値をまとめる) + +1. **タプル (Tuple)**: + * 異なる型をまとめる固定長のリストです。 +2. **配列 (Array)**: + * **同じ型**の要素をまとめる**固定長**のリストです。 + * ※ 長さが可変のリストが必要な場合は、後の章で学ぶ「ベクタ (`Vec`)」を使います。配列はスタック上に確保されるため、高速ですがサイズ変更はできません。 + + + +```rust:data_types.rs +fn main() { + // --- スカラー型 --- + let int_val: i32 = -10; + let float_val: f64 = 2.5; + let char_val: char = 'あ'; // Unicode文字 + let emoji: char = '🦀'; + + println!("Scalar: {}, {}, {}, {}", int_val, float_val, char_val, emoji); + + // --- 複合型: タプル --- + // 型が異なっていてもOK + let tup: (i32, f64, u8) = (500, 6.4, 1); + + // 分解(Destructuring)して値を取り出す + let (x, y, z) = tup; + println!("Tuple分解: yの値は {}", y); + + // ドット記法でアクセス + println!("Tupleアクセス: 最初の値は {}", tup.0); + + // --- 複合型: 配列 --- + // 型と長さを指定: [型; 長さ] + let arr: [i32; 5] = [1, 2, 3, 4, 5]; + + // 同じ値で初期化する記法 let a = [3; 5] は [3, 3, 3, 3, 3] + let months = ["Jan", "Feb", "Mar"]; + + println!("Array: 2番目の月は {}", months[1]); +} +``` + +```rust-exec:data_types.rs +Scalar: -10, 2.5, あ, 🦀 +Tuple分解: yの値は 6.4 +Tupleアクセス: 最初の値は 500 +Array: 2番目の月は Feb +``` + +## この章のまとめ + + * **不変性がデフォルト**: 変数は `mut` をつけない限り変更できない。これにより安全性が担保される。 + * **シャドーイング**: `let` を重ねることで変数の再定義が可能。値の加工や型の変更に便利。 + * **データ型**: 整数、浮動小数点、文字(Unicode対応)、タプル、配列(固定長)などの基本型がある。 + +他の言語では「定数」として扱われるような使い方が、Rustでは「デフォルトの変数」になります。最初は窮屈に感じるかもしれませんが、「変化するものは目立つようにする」というRustの哲学に慣れると、コードの流れが追いやすくなることに気づくはずです。 + +### 練習問題 1: 計算とシャドーイング + +以下の手順に従ってコードを書いてください。 + +1. 変数 `x` を定義し、文字列 `"100"` を代入する。 +2. シャドーイングを使って、`x` を数値の `100` (文字列からパースする)に変換する。 + * ヒント: `"100".parse().expect("Not a number")` で数値に変換できます。 +3. さらにシャドーイングを使って、`x` に `50` を足した値にする。 +4. 最終的な `x` の値を表示する。 + +```rust:practice2_1.rs +``` + +```rust-exec:practice2_1.rs +``` + +### 練習問題 2: 配列とタプルの操作 + +1. 浮動小数点型の配列 `data` を定義し、`[1.1, 2.2, 3.3]` で初期化する。 +2. タプル `result` を定義し、配列 `data` の最初の要素と最後の要素を格納する。 +3. タプルを分解(デストラクチャリング)して、それぞれの値を表示する。 +4. この一連の操作で `mut` が必要ない理由を考える。 + +```rust:practice2_2.rs +``` + +```rust-exec:practice2_2.rs +``` diff --git a/public/docs/rust-3.md b/public/docs/rust-3.md new file mode 100644 index 0000000..f58ca74 --- /dev/null +++ b/public/docs/rust-3.md @@ -0,0 +1,219 @@ +# 第3章: 関数と制御フロー + +他の言語での経験がある皆さんなら、関数やループの基本的な概念はすでにご存知でしょう。しかし、Rustには「すべてが式である」という強力な設計思想があり、これが制御フローの書き方にも大きく影響しています。 + +この章では、Rust特有の「文(Statement)と式(Expression)」の違いを理解し、それを踏まえた関数の定義方法と、柔軟な制御フローについて学びます。 + +## 関数と「式」指向 + +Rustの関数定義は `fn` キーワードを使用し、変数や関数の命名規則にはスネークケース(`snake_case`)を採用するのが慣例です。 + +### 引数と戻り値 + +Rustは静的型付け言語であるため、関数の定義では**各引数の型を必ず宣言**する必要があります。戻り値がある場合は、矢印 `->` の後に型を記述します。 + +### 文(Statement)と式(Expression) + +Rustを理解する上で最も重要な概念の一つがこれです。 + + * **文 (Statement):** 何らかの操作を行い、値を返さないもの(例: `let y = 6;`)。 + * **式 (Expression):** 評価されて結果の値を返すもの(例: `5 + 6`、`{ code_block }`、関数呼び出し)。 + +他の多くの言語と異なり、Rustでは**ブロックの最後の式がそのブロックの戻り値**になります。`return` キーワードを使うこともできますが、一般的には「最後の行のセミコロンを省略する」スタイルが好まれます。 + +以下のコードで確認してみましょう。 + +```rust:functions_demo.rs +fn main() { + let x = 5; + let y = 10; + + // 値を返す関数呼び出し + let result = add(x, y); + println!("{} + {} = {}", x, y, result); + + // 文と式の違いを示すブロック + let z = { + let a = 3; + a + 1 // セミコロンがないので、この式が評価されzに代入される + }; + + println!("zの値: {}", z); +} + +// 引数を取り、i32型を返す関数 +fn add(a: i32, b: i32) -> i32 { + // 最後の行にセミコロンがないため、この式の計算結果が戻り値となる + // "return a + b;" と書くのと同じ意味だが、こちらがRustらしい書き方 + a + b +} +``` + +```rust-exec:functions_demo.rs +5 + 10 = 15 +zの値: 4 +``` + +> **注意:** もし `a + 1` の後ろにセミコロンをつけて `a + 1;` とすると、それは「文」となり、値を返さなくなります(正確には空のタプル `()` を返します)。コンパイルエラーの原因になりやすいため注意してください。 + +## 制御フロー + +Rustの制御フロー(`if`, `loop`, `while`, `for`)もまた「式」として扱うことができます。 + +### if 式 + +Rustの `if` は式です。つまり、条件分岐の結果を変数に代入することができます。 +三項演算子(`condition ? a : b`)のような専用の構文はRustにはありませんが、`if` がその役割を果たします。 + +重要なルールとして、**条件式は必ず `bool` 型でなければなりません**。JavaScriptやC++のように、数値を自動的に `true/false` に変換することはありません。 + +```rust:if_expression.rs +fn main() { + let condition = true; + + // ifの結果を変数に束縛できる + // ifブロックとelseブロックが返す型は同じである必要がある + let number = if condition { + 5 + } else { + 6 + }; + + println!("numberの値: {}", number); +} +``` + +```rust-exec:if_expression.rs +numberの値: 5 +``` + +### ループ構造 + +Rustには3種類のループがあります。 + +1. `loop`: 無限ループ +2. `while`: 条件付きループ +3. `for`: コレクションや範囲に対するループ + +#### loop と値の戻り + +`loop` キーワードは、明示的に `break` するまで永遠に繰り返します。面白い機能として、`break` の後に値を置くことで、ループ全体の結果としてその値を返すことができます。 + +```rust:loop_return.rs +fn main() { + let mut counter = 0; + + // ループの結果を変数resultに代入 + let result = loop { + counter += 1; + + if counter == 10 { + // カウンタが10になったら、counter * 2 の値を返して終了 + break counter * 2; + } + }; + + println!("ループの結果: {}", result); +} +``` + +```rust-exec:loop_return.rs +ループの結果: 20 +``` + +#### while + +`while` は他の言語とほぼ同じです。条件が真である限り実行されます。 + +```rust +// 例(実行不要) +let mut number = 3; +while number != 0 { + println!("{}!", number); + number -= 1; +} +``` + +#### for ループ + +Rustで最も安全かつ頻繁に使用されるのが `for` ループです。配列の要素を走査したり、特定の回数だけ処理を行ったりする場合、インデックス管理が不要な `for` が推奨されます。 + +数値の範囲を指定する場合は `Range` 型(`start..end`)を使用します。 + +```rust:for_loop.rs +fn main() { + let a = [10, 20, 30, 40, 50]; + + // 配列のイテレータを使ったループ + println!("--- 配列の走査 ---"); + for element in a.iter() { + println!("値: {}", element); + } + + // Rangeを使ったループ (1から3まで。4は含まない) + println!("--- 範囲指定 ---"); + for number in 1..4 { + println!("カウント: {}", number); + } +} +``` + +```rust-exec:for_loop.rs +--- 配列の走査 --- +値: 10 +値: 20 +値: 30 +値: 40 +値: 50 +--- 範囲指定 --- +カウント: 1 +カウント: 2 +カウント: 3 +``` + +## この章のまとめ + + * **関数:** パラメータの型と戻り値の型を明示する。 + * **文と式:** Rustは式指向であり、セミコロンのない最後の式がブロックの戻り値となる。 + * **if式:** `if` は値を返すことができる。条件は必ず `bool` でなければならない。 + * **ループ:** `loop`(無限・値返し可能)、`while`(条件付き)、`for`(イテレータ・範囲)があり、特に `for` が推奨される。 + +## 練習問題 1: 摂氏・華氏変換 + +摂氏(Celsius)を華氏(Fahrenheit)に変換する関数 `c_to_f` を作成してください。 +公式: F = C × 1.8 + 32 + +```rust:practice3_1.rs +fn main() { + let celsius_temp = 25.0; + let fahrenheit_temp = c_to_f(celsius_temp); + println!("{}°C は {}°F です", celsius_temp, fahrenheit_temp); +} + +// ここに関数を実装してください + +``` + +```rust-exec:practice3_1.rs +25°C は 77°F です +``` + +### 練習問題 2: フィボナッチ数列 + +第 `n` 番目のフィボナッチ数を求める関数 `fib` を実装してください。 +制御フロー(`if` または ループ)を使用してください。 +(定義: 第0項=0, 第1項=1, 第n項 = 第n-1項 + 第n-2項) + +```rust:practice3_2.rs +fn main() { + let n = 10; + println!("フィボナッチ数列の第 {} 項は {} です", n, fib(n)); +} + +// ここに関数を実装してください + +``` + +```rust-exec:practice3_2.rs +フィボナッチ数列の第 10 項は 55 です +``` diff --git a/public/docs/rust-4.md b/public/docs/rust-4.md new file mode 100644 index 0000000..8332572 --- /dev/null +++ b/public/docs/rust-4.md @@ -0,0 +1,242 @@ +# 第4章: 所有権(Ownership)システム + +Rustへようこそ。ここからがRustの学習における**最大の山場**であり、同時に**最大の特徴**である「所有権(Ownership)」システムについて解説します。 + +他のプログラミング言語(Python, Java, C++など)の経験がある方にとって、この概念は最も馴染みがなく、直感に反する場合があるかもしれません。しかし、所有権こそがガベージコレクション(GC)なしでメモリ安全性を保証するRustの魔法の源です。 + +本章では、参照(借用)の概念に入る前に、基礎となる「所有権の移動(ムーブ)」とメモリ管理の仕組みを理解します。 + +## スタックとヒープのメモリ管理 + +所有権を理解するには、計算機科学の基礎である「スタック(Stack)」と「ヒープ(Heap)」の違いを意識する必要があります。多くの高水準言語ではこれを意識しなくてもコードが書けますが、システムプログラミング言語であるRustでは、値がどこに配置されるかが言語の挙動に直結します。 + +### スタック(Stack) + + * **特徴:** Last In, First Out (LIFO)。高速。 + * **データ:** コンパイル時にサイズが既知のデータ(`i32`, `bool`, 固定長配列など)が置かれます。 + * **動作:** 関数呼び出し時にローカル変数がプッシュされ、関数終了時にポップされます。 + +### ヒープ(Heap) + + * **特徴:** 任意の順序で確保・解放が可能。スタックより低速(ポインタ経由のアクセスが必要)。 + * **データ:** コンパイル時にサイズが不明、または可変長のデータ(`String`, `Vec`など)が置かれます。 + * **動作:** メモリ管理が必要。OSにメモリを要求し、そのアドレス(ポインタ)を受け取ります。ポインタ自体はスタックに置かれます。 + +**Rustの所有権システムは、主にこの「ヒープデータの管理」を自動化・安全化するためのルールセットです。** + +## 所有権の3つのルール + +Rustのコンパイラは、以下の厳格なルールに基づいてメモリ管理を行います。これを破るとコンパイルエラーになります。 + +> **所有権のルール** +> +> 1. Rustの各値は、**所有者(Owner)**と呼ばれる変数を持つ。 +> 2. いかなる時も、所有者は**一人だけ**である。 +> 3. 所有者がスコープから外れると、値は**破棄(ドロップ)**される。 + +### 変数のスコープ + +まずは単純なスコープの例を見てみましょう。これは他の言語とほぼ同じです。 + +```rust:scope_example.rs +fn main() { + { // s はここで宣言されていないので無効 + let s = "hello"; // s はここから有効になる + println!("{}", s); // s を使用できる + } // ここでスコープ終了。s は無効になる +} +``` + +```rust-exec:scope_example.rs +hello +``` + +ここで重要なのは、スコープを抜けた瞬間にRustが自動的にメモリを解放する処理(`drop`関数)を呼び出すという点です。これはC++のRAII (Resource Acquisition Is Initialization) パターンと同様です。 + +## ムーブセマンティクス(Move vs Copy) + +ここからがRust独自の挙動です。データ型によって、「代入」の意味が変わります。 + +### Copyトレイト:スタックのみのデータ + +整数型のような単純な値は、サイズが固定でスタック上にあります。この場合、変数を代入すると**値がコピー**されます。 + +```rust +let x = 5; +let y = x; // xの値(5)がコピーされてyに入る +// ここでは x も y も両方有効 +``` + +### Move(移動):ヒープデータの場合 + +`String`型のようにヒープにメモリを確保する型を見てみましょう。 + +```rust +let s1 = String::from("hello"); +let s2 = s1; +``` + +C++などの経験があると、これは「ポインタのコピー(浅いコピー)」あるいは「ディープコピー」のどちらかだと思うかもしれません。 +しかしRustでは、これは**所有権の移動(Move)**とみなされます。 + +1. `s1` はヒープ上の "hello" を指すポインタ、長さ、容量をスタックに持っています。 +2. `s2 = s1` を実行すると、スタック上のデータ(ポインタ等)のみが `s2` にコピーされます。 +3. **重要:** この瞬間、Rustは `s1` を**無効**とみなします。 + +なぜなら、もし `s1` も有効なままだと、スコープを抜けた時に `s1` と `s2` が同じヒープメモリを2回解放しようとしてしまう(二重解放エラー)からです。 + +以下のコードを実行して確認してみましょう。 + +```rust:move_error_demo.rs +fn main() { + let s1 = String::from("hello"); + let s2 = s1; // 所有権が s1 から s2 へ移動(ムーブ) + + // s1 はもう無効なので、以下の行はコンパイルエラーになる + // println!("{}, world!", s1); + + println!("s1 is moved."); + println!("s2 is: {}", s2); +} +``` + +```rust-exec:move_error_demo.rs +s1 is moved. +s2 is: hello +``` + +もし `println!("{}", s1)` のコメントアウトを外すと、`value borrowed here after move` という有名なコンパイルエラーが発生します。 + +### Clone:ディープコピー + +もしヒープ上のデータも含めて完全にコピーしたい場合は、明示的に `.clone()` メソッドを使用します。 + +```rust:clone_example.rs +fn main() { + let s1 = String::from("hello"); + let s2 = s1.clone(); // ヒープデータごとコピーする(コストは高い) + + println!("s1 = {}, s2 = {}", s1, s2); +} +``` + +```rust-exec:clone_example.rs +s1 = hello, s2 = hello +``` + +## 所有権と関数 + +関数に変数を渡す動作も、代入と同様に機能します。つまり、**関数へ値を渡すと所有権が移動します**(Copy型を除く)。 + +```rust:function_ownership.rs +fn main() { + let s = String::from("hello"); // s がスコープに入る + + takes_ownership(s); // s の値が関数にムーブされる + // ここで s はもう有効ではない! + + let x = 5; // x がスコープに入る + makes_copy(x); // x も関数に移動するが、 + // i32はCopyトレイトを持つので、 + // この後も x を使って問題ない + +} // ここで x がスコープアウト。s もスコープアウトだが、 + // 所有権は既に移動しているので何も起きない。 + +fn takes_ownership(some_string: String) { // some_string に所有権が移る + println!("{}", some_string); +} // ここで some_string がスコープアウトし、`drop` が呼ばれる。メモリ解放。 + +fn makes_copy(some_integer: i32) { // some_integer に値がコピーされる + println!("{}", some_integer); +} // ここで some_integer がスコープアウト。何も起きない。 +``` + +```rust-exec:function_ownership.rs +hello +5 +``` + +### 戻り値と所有権 + +関数から値を返すことで、所有権を呼び出し元に戻すことができます。 + +```rust:return_ownership.rs +fn main() { + let s1 = gives_ownership(); // 戻り値の所有権が s1 に移動 + let s2 = String::from("hello"); // s2 スコープイン + let s3 = takes_and_gives_back(s2); // s2 は関数にムーブされ、 + // 戻り値が s3 にムーブされる + println!("s1: {}, s3: {}", s1, s3); +} + +fn gives_ownership() -> String { + let some_string = String::from("yours"); + some_string // 所有権を呼び出し元に返す +} + +// 文字列を受け取り、それをそのまま返す関数 +fn takes_and_gives_back(a_string: String) -> String { + a_string // 所有権を返す +} +``` + +```rust-exec:return_ownership.rs +s1: yours, s3: hello +``` + +## この章のまとめ + + * **所有権のルール:** 値の所有者は常に一人。所有者がスコープを抜けると値は破棄される。 + * **スタック vs ヒープ:** サイズ固定のデータはスタック(高速)、可変長データはヒープ(管理が必要)に置かれる。 + * **Move(ムーブ):** ヒープデータを変数に代入(または関数渡し)すると、所有権が移動し、元の変数は使用不能になる。 + * **Copy:** 整数などの単純な型は、ムーブではなく自動的にコピーされる。 + * **Clone:** ヒープデータをディープコピーしたい場合は `clone()` を使う。 + +このシステムのおかげで、Rustは実行時のガベージコレクションによる停止を避けつつ、ダングリングポインタ(無効なメモリを指すポインタ)や二重解放のバグをコンパイル時に完全に防ぐことができます。 + +しかし、「関数に値を渡すたびに所有権がなくなってしまい、使えなくなる」のは不便だと感じたでしょう。値を一時的に関数に使わせたいだけなのに、いちいち所有権を返してもらうのは面倒です。 + +次章の**「借用(Borrowing)」**では、所有権を渡さずに値を参照する方法を学びます。 + +### 練習問題 1: ムーブの回避 + +以下のコードは、`s1` の所有権が `s2` に移動してしまったためコンパイルエラーになります。`s1` と `s2` の両方を表示できるように修正してください(`clone`を使用する方法と、新しい文字列を作る方法のどちらでも構いません)。 + +```rust:practice4_1.rs +fn main() { + let s1 = String::from("Rust"); + let s2 = s1; + + println!("Original: {}", s1); // ここでエラー + println!("New: {}", s2); +} +``` + +```rust-exec:practice4_1.rs +``` + +### 練習問題 2: 所有権のリレー + +以下の `process_string` 関数は文字列を受け取って長さを出力しますが、戻り値がないため、呼び出し元で元の文字列が使えなくなってしまいます。 +`process_string` を修正して、受け取った文字列の所有権を呼び出し元に返すようにし、`main` 関数でその後の処理ができるようにしてください。 + +```rust:practice4_2.rs +fn main() { + let s = String::from("ownership"); + + // ここで s を渡して、処理後に戻ってきた所有権を new_s で受け取るように修正する + process_string(s); + + // 修正後は以下のコメントを解除しても動作するようにする + // println!("String is still valid: {}", new_s); +} + +// 戻り値の型と実装を修正してください +fn process_string(input: String) { + println!("Length is: {}", input.len()); +} +``` + +```rust-exec:practice4_2.rs +``` diff --git a/public/docs/rust-5.md b/public/docs/rust-5.md new file mode 100644 index 0000000..14b3f16 --- /dev/null +++ b/public/docs/rust-5.md @@ -0,0 +1,288 @@ +# 第5章: 借用(Borrowing)とスライス + +前章では「所有権(Ownership)」というRust独自のメモリ管理システムについて学びました。「所有権は一度に1つの変数しか持てない」というルールは強力ですが、関数に値を渡すたびに所有権が移動(ムーブ)してしまうと、毎回値を返り値として受け取らなければならず不便です。 + +そこで登場するのが**「参照(Reference)」**と**「借用(Borrowing)」**です。これを使うことで、所有権を渡さずに値にアクセスすることが可能になります。 + +## 5.1 参照と借用(& と \&mut) + +**参照(Reference)**とは、所有権を持たずにデータへアクセスするためのポインタのようなものです。他の言語のポインタと似ていますが、Rustの参照は「常に有効なデータを指していること」が保証されています。 + +関数の引数として参照を渡すことを、Rustでは**「借用(Borrowing)」**と呼びます。誰かから本を借りても、それを捨てたり(メモリ解放)、転売したり(所有権の譲渡)できないのと同じです。 + +### 不変参照(Immutable Reference) + +デフォルトでは、参照は**不変**です。借りた値を読むことはできますが、変更することはできません。参照を作成するには `&` を使います。 + +```rust:calculate_length.rs +fn main() { + let s1 = String::from("hello"); + + // &s1 で s1 への参照を渡す(所有権は移動しない) + let len = calculate_length(&s1); + + // 所有権は移動していないので、ここで s1 をまだ使える! + println!("The length of '{}' is {}.", s1, len); +} + +// 引数の型が &String になっていることに注目 +fn calculate_length(s: &String) -> usize { + s.len() +} // ここで s がスコープを抜けるが、所有権を持っていないのでメモリは解放されない +``` + +```rust-exec:calculate_length.rs +The length of 'hello' is 5. +``` + +### 可変参照(Mutable Reference) + +借りた値を変更したい場合は、**可変参照**を使用します。これには `&mut` を使い、元の変数も `mut` である必要があります。 + +```rust:mutable_borrow.rs +fn main() { + let mut s = String::from("hello"); + + change(&mut s); // 可変参照を渡す + + println!("Result: {}", s); +} + +fn change(some_string: &mut String) { + some_string.push_str(", world"); +} +``` + +```rust-exec:mutable_borrow.rs +Result: hello, world +``` + +## 5.2 借用のルール + +Rustには、メモリ安全性とデータ競合(Data Race)を防ぐための非常に重要なルールがあります。これを**「借用のルール」**と呼びます。 + +> **【借用の鉄則】** +> ある特定のスコープにおいて、以下の**どちらか一方**しか満たすことはできません: +> +> 1. **1つの**可変参照(`&mut T`)を持つ。 +> 2. **任意の数の**不変参照(`&T`)を持つ。 + +### なぜ可変参照は1つだけなのか? + +もし「同じデータに対する可変参照」が同時に2つ存在したらどうなるでしょうか? +Aさんがデータを書き換えている最中に、Bさんも書き換えようとすると、データが破損する可能性があります。Rustはこの可能性をコンパイル時に排除します。 + +### 不変参照と可変参照の共存禁止 + +同様に、「誰かがデータを読んでいる最中(不変参照)に、誰かがデータを書き換える(可変参照)」ことも禁止されています。読んでいる最中にデータが変わると困るからです。 + +以下のコードはコンパイルエラーになります(概念を示すための例です)。 + +```rust +let mut s = String::from("hello"); + +let r1 = &s; // 問題なし +let r2 = &s; // 問題なし +let r3 = &mut s; // 大問題!不変参照が存在している間に可変参照は作れない + +println!("{}, {}, and {}", r1, r2, r3); +``` + +しかし、**スコープ**を利用すれば、この制限をクリアできます。直前の参照が使用されなくなった(スコープを抜けた)後であれば、新しい参照を作ることができます。 + +```rust:borrow_scope.rs +fn main() { + let mut s = String::from("hello"); + + { + let r1 = &mut s; + r1.push_str(" world"); + // r1 はここでスコープを抜ける + } + + // r1 はもういないので、新しい不変参照を作れる + let r2 = &s; + println!("Final string: {}", r2); +} +``` + +```rust-exec:borrow_scope.rs +Final string: hello world +``` + +## 5.3 ダングリングポインタの防止 + +C++などの言語では、メモリが解放された後の場所を指し続けるポインタ(ダングリングポインタ)を作ってしまうことがあり、これが重大なバグの原因になります。 +Rustでは、**コンパイラがこれを絶対に許可しません**。 + +以下のコードは、「関数内で作成した変数の参照」を返そうとしていますが、これはエラーになります。 + +```rust +// コンパイルエラーになる例 +fn dangle() -> &String { + let s = String::from("hello"); + &s // sの参照を返す +} // ここでsはスコープを抜け、メモリが解放される。参照先が消滅する! +``` + +Rustコンパイラは「データのライフタイム(生存期間)が、その参照よりも短い」と判断し、エラーを出してくれます。この場合は、参照ではなく値を返して所有権を移動させるのが正解です。 + +## 5.4 スライス型(Slice) + +参照の特殊な形として**「スライス(Slice)」**があります。スライスは、コレクション(配列や文字列など)の**一部分**への参照です。所有権を持たない点は通常の参照と同じです。 + +### 文字列スライス(`&str`) + +特に重要なのが文字列スライスです。`String` の一部を切り出して参照します。 +書き方は `[開始インデックス..終了インデックス]` です。 + +```rust:string_slices.rs +fn main() { + let s = String::from("Hello Rust World"); + + let hello = &s[0..5]; // 0番目から4番目まで(5は含まない) + let rust = &s[6..10]; // 6番目から9番目まで + + println!("Slice 1: {}", hello); + println!("Slice 2: {}", rust); + + // 省略記法 + let len = s.len(); + let start = &s[0..5]; + let start_short = &s[..5]; // 0は省略可能 + + let end = &s[6..len]; + let end_short = &s[6..]; // 末尾も省略可能 + + let all = &s[..]; // 全体 + + println!("Full: {}", all); +} +``` + +```rust-exec:string_slices.rs +Slice 1: Hello +Slice 2: Rust +Full: Hello Rust World +``` + +### 文字列リテラルはスライスである + +これまで何気なく使っていた文字列リテラルですが、実はこれこそがスライスです。 + +```rust +let s = "Hello, world!"; +``` + +ここで `s` の型は `&str` です。これはバイナリの静的領域に格納されている文字列データへのスライス(参照)なのです。 + +### 引数としての `&str` + +関数で文字列を受け取る際、`&String` よりも `&str` を使う方が柔軟性が高まります。なぜなら、`&str` を引数にすれば、`String` も `&str`(リテラルなど)も両方受け取れるからです。 + +```rust +// この定義の方が汎用的 +fn first_word(s: &str) -> &str { + // 実装... + &s[..] // 仮の実装 +} +``` + +## 5.5 その他のスライス + +スライスは文字列だけでなく、配列に対しても使えます。 + +```rust:array_slice.rs +fn main() { + let a = [10, 20, 30, 40, 50]; + + // 配列の一部を借用する + let slice = &a[1..3]; // [20, 30] + + // assert_eq! は値が等しいか確認するマクロ + assert_eq!(slice, &[20, 30]); + + println!("Slice elements: {:?}", slice); +} +``` + +```rust-exec:array_slice.rs +Slice elements: [20, 30] +``` + +## この章のまとめ + + * **参照(`&`)**: 所有権を移動させずに値にアクセスする仕組み。 + * **借用**: 関数の引数として参照を渡すこと。 + * **借用のルール**: + 1. 不変参照(`&T`)は同時にいくつでも作れる。 + 2. 可変参照(`&mut T`)は同時に1つしか作れない。 + 3. 不変参照と可変参照は同時に存在できない。 + * **スライス**: コレクションの一部分を参照するビュー。`&str` は文字列の一部への不変参照。 + +Rustのコンパイラ(ボローチェッカー)は厳格ですが、それはバグのない安全なコードを書くための強力なパートナーです。慣れてくれば、コンパイルが通った時点でロジックの正しさに自信が持てるようになります。 + +## 練習問題 1: 参照渡しへの書き換え + +以下のコードは動作しますが、`calculate_area` 関数が所有権を奪ってしまうため、再度 `rect1` を使おうとするとエラーになります。 +`calculate_area` が `Rectangle` の**不変参照**を受け取るように修正し、`main` 関数で `rect1` を再利用できるようにしてください。 + +```rust:practice5_1.rs +struct Rectangle { + width: u32, + height: u32, +} + +fn main() { + let rect1 = Rectangle { width: 30, height: 50 }; + + let area = calculate_area(rect1); // ここを修正 + + // 現在のコードだと、ここで rect1 を使うとエラーになる + // println!("rect1 is {:?}", rect1); + + println!("The area is {}", area); +} + +fn calculate_area(rect: Rectangle) -> u32 { // ここを修正 + rect.width * rect.height +} +``` + +```rust-exec:practice5_1.rs +``` + +### 問題 2: スライスの活用 + +Eメールアドレスを表す文字列を受け取り、**「@」より前のユーザー名部分だけを文字列スライス(`&str`)として返す**関数 `extract_username` を作成してください。もし「@」が含まれていない場合は、文字列全体を返してください。 + +ヒント: 文字列をバイト配列として扱い、ループで `@` を探します。 + +```rust:practice5_2.rs +fn main() { + let email = String::from("user@example.com"); + let username = extract_username(&email); + println!("Username: {}", username); // "user" と出力されるべき + + let simple = String::from("admin"); + let simple_name = extract_username(&simple); + println!("Username: {}", simple_name); // "admin" と出力されるべき +} + +fn extract_username(s: &str) -> &str { + let bytes = s.as_bytes(); + + for (i, &item) in bytes.iter().enumerate() { + // ここにコードを書く + + } + + // 「@」が含まれていない場合は、文字列全体を返す + +} +``` + +```rust-exec:practice5_2.rs +Username: user +Username: admin +``` diff --git a/public/docs/rust-6.md b/public/docs/rust-6.md new file mode 100644 index 0000000..1d63002 --- /dev/null +++ b/public/docs/rust-6.md @@ -0,0 +1,288 @@ +# 第6章: 構造体とメソッド構文 + +他のオブジェクト指向言語(C++、Java、Pythonなど)の経験がある方にとって、Rustの**構造体(Struct)**は「クラス」に似ているように見えますが、決定的な違いがいくつかあります。 + +最も大きな違いは以下の2点です: + +1. **クラス継承が存在しない**: Rustには`extends`や親クラスという概念がありません(コードの再利用には、後述する「トレイト」や「コンポジション」を使用します)。 +2. **データと振る舞いの分離**: データ定義(`struct`)とメソッド定義(`impl`)は明確に分かれています。 + +この章では、関連するデータをグループ化し、それに対する操作を定義する方法を学びます。 + +## 構造体の定義とインスタンス化 + +構造体は、異なる型の値を一つにまとめて名前を付けたカスタムデータ型です。 + +### 基本的な定義 + +C言語の `struct` や、メソッドを持たないクラスのようなものです。フィールド名と型を定義します。 + +```rust:user_struct.rs +struct User { + username: String, + email: String, + sign_in_count: u64, + active: bool, +} + +fn main() { + // インスタンス化 + // フィールドの順番は定義と異なっても構いません + let mut user1 = User { + email: String::from("someone@example.com"), + username: String::from("someusername123"), + active: true, + sign_in_count: 1, + }; + + // ドット記法でフィールドにアクセス + user1.email = String::from("another@example.com"); + + println!("User: {}, Email: {}", user1.username, user1.email); +} +``` + +```rust-exec:user_struct.rs +User: someusername123, Email: another@example.com +``` + +> **注意**: Rustでは、インスタンス全体が可変(`mut`)か不変かのどちらかになります。特定のフィールドだけを可変(`mut`)にすることはできません。 + +### フィールド初期化省略記法 + +関数引数や変数の名前がフィールド名と同じ場合、記述を省略できます。これはJavaScriptのオブジェクト定義に似ています。 + +```rust +fn build_user(email: String, username: String) -> User { + User { + email, // email: email と同じ + username, // username: username と同じ + active: true, + sign_in_count: 1, + } +} +``` + +### 構造体更新記法 + +既存のインスタンスの値を元に、一部だけ変更した新しいインスタンスを作成する場合、`..` 構文を使用できます。 + +```rust +// user1のデータを元に、emailだけ変更したuser2を作成 +let user2 = User { + email: String::from("another@example.com"), + ..user1 // 残りのフィールドはuser1と同じ値が入る +}; +``` + +## タプル構造体とユニット様構造体 + +名前付きフィールドを持たない構造体も定義できます。 + +### タプル構造体 (Tuple Structs) + +フィールドに名前がなく、型だけが並んでいる構造体です。「型」として区別したい場合に便利です。例えば、同じ `(i32, i32, i32)` でも、「色」と「座標」は計算上混ぜるべきではありません。 + +```rust:tuple_structs.rs +struct Color(i32, i32, i32); +struct Point(i32, i32, i32); + +fn main() { + let black = Color(0, 0, 0); + let origin = Point(0, 0, 0); + + // black と origin は構造が同じでも、型としては別物なので + // 関数に渡す際などにコンパイラが区別してくれます。 + + // 中身へのアクセスはタプル同様にインデックスを使用 + println!("Origin X: {}", origin.0); +} +``` + +```rust-exec:tuple_structs.rs +Origin X: 0 +``` + +### ユニット様構造体 (Unit-like Structs) + +フィールドを全く持たない構造体です。`struct AlwaysEqual;` のように定義します。これらは、データを持たずに振る舞い(トレイト)だけを実装したい場合に役立ちますが、詳細は後の章で扱います。 + +## 所有権と構造体 + +構造体における所有権の扱いは非常に重要です。 + +1. **所有するデータ**: `String`や`Vec`のような所有権を持つ型をフィールドにすると、その構造体のインスタンスがデータの所有者になります。構造体がドロップされると、フィールドのデータもドロップされます。 +2. **参照**: 構造体に「参照(\&strなど)」を持たせることも可能ですが、これには**ライフタイム(第12章)の指定が必要になります。そのため、現段階では構造体には(参照ではなく)`String`などの所有権のある型**を使うことを推奨します。 + + + +```rust +// 推奨(現段階) +struct User { + username: String, // Userがデータを所有する +} + +// 非推奨(ライフタイムの知識が必要になるためコンパイルエラーになる) +// struct User { +// username: &str, +// } +``` + +## impl ブロック:メソッドと関連関数 + +ここが、他言語のクラスにおける「メソッド定義」に相当する部分です。データ(`struct`)とは別に、`impl`(implementation)ブロックを使って振る舞いを定義します。 + +### メソッドの定義 + +メソッドは、最初の引数が必ず `self` (自分自身のインスタンス)になる関数です。 + + * `&self`: データを読み取るだけ(借用・不変)。最も一般的。 + * `&mut self`: データを書き換える(借用・可変)。 + * `self`: 所有権を奪う(ムーブ)。メソッド呼び出し後に元の変数は使えなくなる。変換処理などで使う。 + + + +```rust:rectangle.rs +#[derive(Debug)] // print!で{:?}を使ってデバッグ表示するために必要 +struct Rectangle { + width: u32, + height: u32, +} + +// Rectangle型に関連する関数やメソッドを定義 +impl Rectangle { + // メソッド:面積を計算する(読み取りのみなので &self) + fn area(&self) -> u32 { + self.width * self.height + } + + // メソッド:他の四角形を含めるか判定する + fn can_hold(&self, other: &Rectangle) -> bool { + self.width > other.width && self.height > other.height + } +} + +fn main() { + let rect1 = Rectangle { width: 30, height: 50 }; + let rect2 = Rectangle { width: 10, height: 40 }; + + // メソッド呼び出し(自動参照外し機能により、rect1.area() と書ける) + println!("The area of the rectangle is {} pixels.", rect1.area()); + + if rect1.can_hold(&rect2) { + println!("rect1 can hold rect2"); + } +} +``` + +```rust-exec:rectangle.rs +The area of the rectangle is 1500 pixels. +rect1 can hold rect2 +``` + +### 関連関数 (Associated Functions) + +`impl`ブロックの中で、第1引数に `self` を取らない関数も定義できます。これらはインスタンスではなく、型そのものに関連付けられた関数です。 +他言語での「静的メソッド(Static Method)」に相当します。 + +最も一般的な用途は、コンストラクタのような役割を果たす初期化関数の作成です。Rustには `new` というキーワードはありませんが、慣習として `new` という名前の関連関数をよく作ります。 + +```rust:associated_fn.rs +#[derive(Debug)] +struct Circle { + radius: f64, +} + +impl Circle { + // 関連関数(selfがない) + // コンストラクタのように振る舞う + fn new(radius: f64) -> Circle { + Circle { radius } + } + + // メソッド(selfがある) + fn area(&self) -> f64 { + 3.14159 * self.radius * self.radius + } +} + +fn main() { + // 関連関数の呼び出しは :: を使う + let c = Circle::new(2.0); + + println!("Circle radius: {}, area: {}", c.radius, c.area()); +} +``` + +```rust-exec:associated_fn.rs +Circle radius: 2, area: 12.56636 +``` + +## この章のまとめ + + * **構造体 (`struct`)** は関連するデータをまとめるカスタム型です。 + * **タプル構造体** は名前付きフィールドを持たない構造体で、特定の型を区別するのに便利です。 + * **メソッド** は `impl` ブロック内に定義し、第1引数に `self` を取ります。 + * **関連関数** は `self` を取らず、`型名::関数名` で呼び出します(コンストラクタ `new` など)。 + * Rustには継承がないため、データ構造とメソッドの組み合わせのみでオブジェクト指向的な設計を行います。 + +次章では、Rustの強力な機能の一つである「列挙型(Enum)」と、フロー制御の要である「パターンマッチ」について学びます。 + +### 練習問題1: RPGのキャラクター + +以下の要件を満たす `Character` 構造体と `impl` ブロックを作成してください。 + +1. フィールド: + * `name`: キャラクター名 (`String`) + * `hp`: 現在のヒットポイント (`i32`) + * `attack_power`: 攻撃力 (`i32`) +2. 関連関数 `new`: + * 名前を受け取り、hpを100、attack\_powerを10で初期化したインスタンスを返す。 +3. メソッド `take_damage`: + * ダメージ量 (`i32`) を受け取り、`hp` から引く。ただし、`hp` は0未満にならないようにする(0で止める)。 + * このメソッドは `hp` を変更するため、`&mut self` が必要です。 + +```rust:practice6_1.rs +// ここにCharacter構造体とimplブロックを実装してください + + +fn main() { + let mut hero = Character::new(String::from("Hero")); + println!("{} has {} HP.", hero.name, hero.hp); + + hero.take_damage(30); + println!("After taking damage, {} has {} HP.", hero.name, hero.hp); + + hero.take_damage(80); + println!("After taking more damage, {} has {} HP.", hero.name, hero.hp); +} +``` +```rust-exec:practice6_1.rs +Hero has 100 HP. +After taking damage, Hero has 70 HP. +After taking more damage, Hero has 0 HP. +``` + +### 練習問題2: 座標計算 + +2次元座標を表すタプル構造体 `Point(f64, f64)` を作成し、以下を実装してください。 + +1. 関連関数 `origin`: 原点 `(0.0, 0.0)` を持つ `Point` を返す。 +2. メソッド `distance_to`: 別の `Point` への参照を受け取り、2点間の距離を計算して返す `f64`。 + * ヒント: 距離の公式は sqrt((x₂ - x₁)² + (y₂ - y₁)²) です。平方根は `f64`型の値に対して `.sqrt()` メソッドで計算できます。 + +```rust:practice6_2.rs +// ここにPointタプル構造体とimplブロックを実装してください + + +fn main() { + let p1 = Point::origin(); + let p2 = Point(3.0, 4.0); + + let distance = p1.distance_to(&p2); + println!("Distance from origin to p2 is {}", distance); +} +``` +```rust-exec:practice6_2.rs +Distance from origin to p2 is 5.0 +``` diff --git a/public/docs/rust-7.md b/public/docs/rust-7.md new file mode 100644 index 0000000..eb1fc66 --- /dev/null +++ b/public/docs/rust-7.md @@ -0,0 +1,326 @@ +# 第7章: 列挙型(Enum)とパターンマッチ + +Rustチュートリアル第7章へようこそ。 +これまでの章では、構造体を使って関連するデータをまとめる方法を学びました。この章では、**列挙型(Enum)と、それに関連する強力な制御フロー構造であるパターンマッチ**について学びます。 + +他の言語(C、C++、Javaなど)でのEnumは、単に「名前付き定数のリスト」であることが多いですが、RustのEnumは「代数的データ型(Algebraic Data Types)」に近い性質を持っており、はるかに強力です。 + +## Enumの定義と値の保持 + +最も基本的なEnumの使い方は、C言語などと同様に「ありうる値の列挙」です。しかし、RustのEnumの真価は、**各バリアント(選択肢)にデータを持たせることができる**点にあります。 + +例えば、IPアドレスを表現する場合を考えてみましょう。IPアドレスにはV4とV6があり、それぞれ異なる形式のデータを持ちます。 + +```rust:ip_address.rs +#[derive(Debug)] +enum IpAddr { + V4(u8, u8, u8, u8), // 4つのu8を持つ + V6(String), // Stringを持つ +} + +fn main() { + let home = IpAddr::V4(127, 0, 0, 1); + let loopback = IpAddr::V6(String::from("::1")); + + println!("Home: {:?}", home); + println!("Loopback: {:?}", loopback); +} +``` + +```rust-exec:ip_address.rs +Home: V4(127, 0, 0, 1) +Loopback: V6("::1") +``` + +### 構造体との違い + +これを構造体で実装しようとすると、「種類(kind)」フィールドと「データ」フィールドを持つ必要がありますが、V4の場合にV6用のフィールドが無駄になったり、型安全性が下がったりします。 +RustのEnumを使うと、**「V4なら必ず4つの数値がある」「V6なら文字列がある」**ということが型レベルで保証されます。 + +### あらゆる種類のデータを埋め込める + +Enumの各バリアントには、名前付きフィールドを持つ構造体のような形や、タプルのような形など、自由に定義できます。 + +```rust:message_enum.rs +#[derive(Debug)] +enum Message { + Quit, // データなし + Move { x: i32, y: i32 }, // 名前付きフィールド(構造体風) + Write(String), // 単一のString(タプル風) + ChangeColor(i32, i32, i32), // 3つのi32(タプル風) +} + +impl Message { + // Enumにもメソッドを定義できる + fn call(&self) { + println!("メッセージを処理します: {:?}", self); + } +} + +fn main() { + let m1 = Message::Write(String::from("hello")); + let m2 = Message::Move { x: 10, y: 20 }; + let m3 = Message::Quit; + + m1.call(); + m2.call(); + m3.call(); +} +``` + +```rust-exec:message_enum.rs +メッセージを処理します: Write("hello") +メッセージを処理します: Move { x: 10, y: 20 } +メッセージを処理します: Quit +``` + +## Option\ 型(Null安全性の核心) + +Rustには、他の多くの言語にある **Null(ヌル)が存在しません**。 +その代わり、標準ライブラリで定義された `Option` というEnumを使用します。これは「値が存在するかもしれないし、しないかもしれない」ことを表現します。 + +`Option` は以下のように定義されています(概念図): + +```rust +enum Option { + Some(T), // 値がある場合。Tは任意の型。 + None, // 値がない場合。 +} +``` + +### なぜこれが安全なのか? + +`Option` 型と `T` 型(例えば `i32`)は異なる型です。そのため、**「値がないかもしれないもの」を、チェックせずにそのまま計算に使うことがコンパイラレベルで禁止されます。** + +```rust:option_intro.rs +fn main() { + let some_number = Some(5); + let some_string = Some("a string"); + + // Noneの場合は型推論できないため、明示的に型を指定する必要がある + let absent_number: Option = None; + + let x: i8 = 5; + let y: Option = Some(5); + + // 以下の行はコンパイルエラーになります。 + // i8 と Option は足し算できません。 + // let sum = x + y; + + println!("x: {}", x); + // 値を取り出すには明示的な処理が必要(後述のmatchなどを使う) + println!("y is: {:?}", y); + println!("absent is: {:?}", absent_number); +} +``` + +```rust-exec:option_intro.rs +x: 5 +y is: Some(5) +absent is: None +``` + +値を使うためには、`Option` から `T` を取り出す処理(Nullチェックに相当)を必ず書かなければなりません。これにより、「うっかりNullを参照してクラッシュ」という事故を防げます。 + +## match フロー制御演算子 + +`match` は、Enumの値を処理するための最も強力なツールです。C言語やJavaの `switch` に似ていますが、より表現力が高く、コンパイラによる**網羅性チェック(Exhaustiveness Check)**があります。 + +### 網羅性チェック + +`match` は、あり得るすべてのパターンをカバーしなければなりません。一つでも漏れているとコンパイルエラーになります。 + +```rust:match_shapes.rs +enum Shape { + Circle(f64), // 半径 + Rectangle(f64, f64), // 幅, 高さ + Triangle(f64, f64, f64), // 3辺の長さ(簡略化のためヘロンの公式用) +} + +fn calculate_area(shape: Shape) -> f64 { + match shape { + // パターンマッチで中のデータを取り出す(バインディング) + Shape::Circle(radius) => { + std::f64::consts::PI * radius * radius + }, + Shape::Rectangle(w, h) => { + w * h + }, + Shape::Triangle(a, b, c) => { + let s = (a + b + c) / 2.0; + (s * (s - a) * (s - b) * (s - c)).sqrt() + } + } +} + +fn main() { + let c = Shape::Circle(10.0); + let r = Shape::Rectangle(3.0, 4.0); + + println!("円の面積: {:.2}", calculate_area(c)); + println!("長方形の面積: {:.2}", calculate_area(r)); +} +``` + +```rust-exec:match_shapes.rs +円の面積: 314.16 +長方形の面積: 12.00 +``` + +### Option\ と match + +`Option` の中身を取り出す際も `match` がよく使われます。 + +```rust:match_option.rs +fn plus_one(x: Option) -> Option { + match x { + None => None, + Some(i) => Some(i + 1), + } +} + +fn main() { + let five = Some(5); + let six = plus_one(five); + let none = plus_one(None); + + println!("Five: {:?}", five); + println!("Six: {:?}", six); + println!("None: {:?}", none); +} +``` + +```rust-exec:match_option.rs +Five: Some(5) +Six: Some(6) +None: None +``` + +### `_` プレースホルダー + +全ての値を個別に書きたくない場合、`_`(アンダースコア)を使って「その他すべて」にマッチさせることができます。これは `switch` 文の `default` に相当します。 + +```rust +let dice_roll = 9; +match dice_roll { + 3 => println!("3が出ました"), + 7 => println!("7が出ました"), + _ => println!("それ以外が出ました"), // 3, 7 以外はここに来る +} +``` + +## if let 記法 + +`match` は強力ですが、**「ある1つのパターンだけ処理して、他は全部無視したい」**という場合には記述が長くなりがちです。 +そのような場合に `if let` が便利です。 + +これは以下の `match` のシンタックスシュガー(糖衣構文)です。 + +```rust +// matchを使う場合(冗長) +let config = Some("config_value"); +match config { + Some(val) => println!("設定値: {}", val), + _ => (), // 何もしない +} +``` + +これと同じことを `if let` で書くと以下のようになります。 + +```rust:if_let_demo.rs +fn main() { + let config = Some("config_value"); + let missing: Option<&str> = None; + + // 「もし config が Some(val) というパターンにマッチするならブロックを実行」 + if let Some(val) = config { + println!("設定値があります: {}", val); + } + + // else も使えます + if let Some(val) = missing { + println!("設定値: {}", val); + } else { + println!("設定値がありません"); + } +} +``` + +```rust-exec:if_let_demo.rs +設定値があります: config_value +設定値がありません +``` + +`if let` を使うとコードが短くなりますが、`match` が強制する「網羅性チェック」の恩恵は失われます。状況に応じて使い分けましょう。 + +## この章のまとめ + + * **Enum(列挙型)**: RustのEnumは、異なる型や量のデータを各バリアントに持たせることができる(代数的データ型)。 + * **Option\**: `Some(T)` と `None` によって、Null安全を実現する。値を使うには `Option` の皮を剥く処理が必須となる。 + * **match**: パターンマッチングを行う制御フロー。コンパイラが全てのケースを網羅しているかチェックしてくれる。 + * **if let**: 単一のパターンだけを扱いたい場合の簡潔な記法。 + +### 練習問題 1: コインの分類機 + +アメリカの硬貨を表すEnum `Coin` を定義してください。 + + * バリアント: `Penny` (1セント), `Nickel` (5セント), `Dime` (10セント), `Quarter` (25セント) + * `Quarter` バリアントには、`UsState` というEnum(各州の名前を持つ)をデータとして持たせてください(例: `Quarter(UsState::Alaska)`)。 + * `Coin` を受け取り、その価値(セント単位)を文字列で返す関数 `value_in_cents` を `match` を使って実装してください。Quarterの場合は、その州の名前も同時に返してください。 + +```rust:practice7_1.rs +// UsState, Coin, value_in_cents を作成してください + +fn main() { + let coin1 = Coin::Penny; + let coin2 = Coin::Quarter(UsState::California); + + println!("Coin1 value: {}", value_in_cents(coin1)); + println!("Coin2 value: {}", value_in_cents(coin2)); +} +``` + +```rust-exec:practice7_1.rs +Coin1 value: 1 cent +Coin2 value: 25 cents from California +``` + +### 練習問題 2: 簡易計算機 + +2つの数値に対する操作を表すEnum `Operation` を定義し、計算を行ってください。 + +1. Enum `Operation` を定義します。 + * `Add`: 2つの `i32` を持つ + * `Subtract`: 2つの `i32` を持つ + * `Multiply`: 2つの `i32` を持つ + * `Divide`: 2つの `i32` を持つ +2. 関数 `calculate(op: Operation) -> Option` を実装してください。 + * `match` を使用して計算結果を返します。 + * 割り算の場合、0での除算(ゼロ除算)を防ぐため、分母が0なら `None` を返し、計算できるなら `Some(結果)` を返してください。他の演算は常に `Some` で返します。 +3. `main` 関数でいくつかのパターンを試し、結果を表示してください。 + +```rust:practice7_2.rs +// Operation enum と calculate 関数を実装してください + +fn main() { + let add = Operation::Add(5, 3); + let subtract = Operation::Subtract(10, 4); + let multiply = Operation::Multiply(6, 7); + let divide = Operation::Divide(20, 4); + let divide_by_zero = Operation::Divide(10, 0); + + println!("5 + 3 = {:?}", calculate(add)); + println!("10 - 4 = {:?}", calculate(subtract)); + println!("6 * 7 = {:?}", calculate(multiply)); + println!("20 / 4 = {:?}", calculate(divide)); + println!("10 / 0 = {:?}", calculate(divide_by_zero)); +} +``` + +```rust-exec:practice7_2.rs +5 + 3 = Some(8) +10 - 4 = Some(6) +6 * 7 = Some(42) +20 / 4 = Some(5) +10 / 0 = None +``` diff --git a/public/docs/rust-8.md b/public/docs/rust-8.md new file mode 100644 index 0000000..37ddb68 --- /dev/null +++ b/public/docs/rust-8.md @@ -0,0 +1,312 @@ +# 第8章: モジュールシステムとパッケージ管理 + +これまでの章では、関数や構造体を使ってコードを整理する方法を学びました。プログラムの規模が大きくなると、コードをさらに大きな単位で整理し、詳細を隠蔽(カプセル化)し、再利用性を高める必要が出てきます。 + +他の言語(C++のnamespace、Javaのpackage、Pythonのmoduleなど)での経験があれば、Rustのモジュールシステムも直感的に理解できる部分が多いですが、**「デフォルトで非公開(private)」**というRust特有の哲学や、ファイルシステムとの対応関係には注意が必要です。 + +この章では、Rustのコード整理の仕組みである「モジュールシステム」と、外部ライブラリを活用するための「パッケージ管理」について学びます。 + +## パッケージ、クレート、モジュール + +Rustのモジュールシステムは、以下の3つの概念で構成されています。 + +1. **パッケージ (Package):** `Cargo.toml` ファイルを持ち、1つ以上のクレートをビルドするための機能です。通常 `cargo new` で作成されるプロジェクト全体を指します。 +2. **クレート (Crate):** 木構造状のモジュール群からなるコンパイル単位です。 + * **バイナリクレート:** 実行可能な形式(`src/main.rs` がルート)。 + * **ライブラリクレート:** 他のプロジェクトから利用される形式(`src/lib.rs` がルート)。 +3. **モジュール (Module):** クレート内のコードをグループ化し、可視性(公開/非公開)を制御する仕組みです。 + +### モジュールの基本定義 (`mod`) + +モジュールは `mod` キーワードを使って定義します。モジュールの中に、さらにモジュール(サブモジュール)を入れることも可能です。 + +まずは、1つのファイル内でモジュールを定義して、その構造を見てみましょう。 + +```rust:simple_module.rs +// "restaurant" という名前のモジュールを定義 +mod restaurant { + // モジュール内に関数を定義 + // 注意: デフォルトでは親モジュールからアクセスできません(後述) + fn make_coffee() { + println!("コーヒーを淹れます"); + } + + // サブモジュール + mod front_of_house { + fn add_to_waitlist() { + println!("順番待ちリストに追加しました"); + } + } +} + +fn main() { + // restaurant::make_coffee(); + // restaurant::front_of_house::add_to_waitlist(); +} +``` + +```rust-exec:simple_module.rs +``` + +このコードはコンパイルに通りますが、`main` 関数から `make_coffee` などを呼び出そうとするとエラーになります。それは**可視性**の問題があるからです。 + +## 可視性と `pub` キーワード + +Rustのモジュールシステムの最大の特徴は、**「すべてのアイテム(関数、構造体、モジュールなど)は、デフォルトで非公開(private)」**であるという点です。 + + * **非公開:** 定義されたモジュール自身と、その子モジュールからのみアクセス可能。親モジュールからは見えません。 + * **公開 (`pub`):** 親モジュールや外部からアクセス可能になります。 + +親モジュール(この場合は `main` 関数がいるルート)から子モジュールの中身を使うには、明示的に `pub` をつける必要があります。 + +```rust:simple_module_with_pub.rs +mod restaurant { + // pubがないので、restaurantモジュール内からしか呼べない + fn make_coffee() { + println!("コーヒーを淹れます"); + } + + // pubをつけてサブモジュールも公開 + pub mod front_of_house { + // ここも公開関数にする + pub fn add_to_waitlist() { + println!("順番待ちリストに追加しました"); + } + } +} +fn main() { + // これで呼び出せるようになる + restaurant::front_of_house::add_to_waitlist(); + + // restaurant::make_coffee(); +} +``` + +```rust-exec:simple_module_with_pub.rs +順番待ちリストに追加しました +``` + +### 構造体の可視性 + +構造体に `pub` をつけた場合、**構造体そのものは公開されますが、フィールドはデフォルトで非公開のまま**です。フィールドごとに `pub` を決める必要があります。 + +```rust:struct_visibility.rs +mod kitchen { + pub struct Breakfast { + pub toast: String, // 公開フィールド + seasonal_fruit: String, // 非公開フィールド + } + + impl Breakfast { + // コンストラクタ(これがないと外部からインスタンスを作れない) + pub fn summer(toast: &str) -> Breakfast { + Breakfast { + toast: String::from(toast), + seasonal_fruit: String::from("peaches"), + } + } + } +} + +fn main() { + // コンストラクタ経由でインスタンス作成 + let mut meal = kitchen::Breakfast::summer("ライ麦パン"); + + // 公開フィールドは変更・参照可能 + meal.toast = String::from("食パン"); + println!("トースト: {}", meal.toast); + + // 非公開フィールドにはアクセスできない + // meal.seasonal_fruit = String::from("blueberries"); // エラー! +} +``` + +```rust-exec:struct_visibility.rs +トースト: 食パン +``` + +## use キーワードとパス + +モジュールの階層が深くなると、毎回 `restaurant::front_of_house::add_to_waitlist()` のようにフルパスを書くのは面倒です。 +`use` キーワードを使うと、パスをスコープに持ち込み、短い名前で呼び出せるようになります。これは他言語の `import` に相当します。 + +### 絶対パスと相対パス + +パスの指定方法には2種類あります。 + +1. **絶対パス:** クレートのルート(`crate`)から始まるパス。 +2. **相対パス:** 現在のモジュール(`self`)や親モジュール(`super`)から始まるパス。 + +```rust:use_paths.rs +mod sound { + pub mod instrument { + pub fn clarinet() { + println!("クラリネットの音色♪"); + } + } +} + +// 絶対パスで持ち込む +use crate::sound::instrument; + +// 相対パスの場合(このファイル内であれば以下も同じ意味) +// use self::sound::instrument; + +fn main() { + // useのおかげで、直接 instrument を使える + instrument::clarinet(); + instrument::clarinet(); +} +``` + +```rust-exec:use_paths.rs +クラリネットの音色♪ +``` + +> **慣習:** 関数を持ち込むときは、親モジュールまでを `use` して `親::関数()` と呼び出すのが一般的です(関数の出処が明確になるため)。一方、構造体やEnumは完全なパスを指定して直接名前だけで使えるようにすることが多いです。 + +## モジュールのファイル分割 + +これまでは説明のために1つのファイルにすべてのモジュールを書いてきましたが、実際の開発ではファイルを分割します。 + +Rustでは**「ファイルシステム上の構造」**と**「モジュール階層」**が対応します。 + +例えば、`main.rs` と `front_of_house.rs` がある場合: + +```rust:main.rs +// ファイルの中身をモジュールとして宣言 +// これにより、コンパイラは front_of_house.rs を探しに行きます +mod front_of_house; + +pub use crate::front_of_house::hosting; + +fn main() { + hosting::add_to_waitlist(); +} +``` + +```rust:front_of_house.rs +// ここには "mod front_of_house { ... }" の枠は書かない +pub mod hosting { + pub fn add_to_waitlist() { + println!("リストに追加しました"); + } +} +``` + +```rust-exec:main.rs +リストに追加しました +``` + +さらに `front_of_house` の中にサブモジュールを作りたい場合は、ディレクトリを作成します。 + + * `src/main.rs` + * `src/front_of_house/` (ディレクトリ) + * `mod.rs` (または `front_of_house.rs` と同義。ディレクトリのエントリーポイント) + * `hosting.rs` + +このように、Rustはファイルやディレクトリの存在だけで自動的にモジュールを認識するのではなく、**親となるファイルで `mod xxx;` と宣言されたものだけ**をコンパイル対象として認識します。これがC\#やJavaなどの「フォルダにあるものは全部パッケージに含まれる」言語との大きな違いです。 + +## 外部クレートの利用 + +Rustのパッケージ管理システムであるCargoを使うと、外部ライブラリ(クレート)を簡単に利用できます。 + +### Cargo.toml への追加 + +`Cargo.toml` の `[dependencies]` セクションに、使いたいクレートの名前とバージョンを記述します。例えば、乱数を生成する `rand` クレートを使う場合: + +```toml +[dependencies] +rand = "0.8.5" +``` + +### コードでの利用 + +外部クレートも、プロジェクト内のモジュールと同じように `use` でスコープに持ち込んで使用します。 + +```rust +use std::collections::HashMap; // 標準ライブラリも 'std' という外部クレートのような扱い + +// 外部クレート rand を使用する想定のコード +use rand::Rng; + +fn main() { + let mut scores = HashMap::new(); + scores.insert("Blue", 10); + scores.insert("Yellow", 50); + + println!("スコア: {:?}", scores); + + let secret_number = rand::thread_rng().gen_range(1..101); + println!("乱数: {}", secret_number); +} +``` + +標準ライブラリ(`std`)はデフォルトで利用可能ですが、それ以外のクレートは crates.io (Rustの公式パッケージレジストリ)から自動的にダウンロード・ビルドされます。 + +> 注: my.code(); のオンライン実行環境では外部クレートは使用できません。 + +## 第8章のまとめ + + * **パッケージとクレート:** `cargo new` で作るのがパッケージ、生成されるバイナリやライブラリがクレートです。 + * **モジュール:** コードを整理する箱です。`mod` で定義します。 + * **可視性:** すべてのアイテムはデフォルトで**非公開 (private)** です。公開するには `pub` をつけます。 + * **パスとuse:** `use` キーワードでモジュールへのパスを省略(インポート)できます。絶対パス(`crate::`)と相対パス(`self::`, `super::`)があります。 + * **ファイル分割:** `mod filename;` と宣言することで、別ファイルのコードをサブモジュールとして読み込みます。 + +この章の内容を理解することで、大規模なアプリケーション開発への準備が整いました。 + +### 練習問題1:ライブラリの設計 + +以下の仕様に従って、架空の図書館システムモジュールを作成してください。 + +1. `library` という親モジュールを作成する。 +2. その中に `books` というサブモジュールを作成する。 +3. `books` モジュールの中に `Book` 構造体を作成する。フィールドは `title` (String, 公開) と `isbn` (String, 非公開) とする。 +4. `Book` 構造体に、新しい本を作成するコンストラクタ `new(title: &str)` を実装する(ISBNは内部で適当な文字列を設定する)。 +5. `main` 関数から `library::books::Book` を使って本を作成し、タイトルを表示するコードを書く。 + + + +```rust:practice8_1.rs + +fn main() { + let my_book = library::books::Book::new("Rust入門"); + println!("本のタイトル: {}", my_book.title); +} +``` +```rust:library/mod.rs +``` +```rust:library/books.rs +``` + +```rust-exec:practice8_1.rs +本のタイトル: Rust入門 +``` + +### 練習問題2:パスと可視性の修正 + +以下のコードは可視性の設定とパスの指定が誤っているためコンパイルできません。修正して正常に「ネットワーク接続完了」と表示されるようにしてください。 + +```rust:practice8_2.rs +mod network { + fn connect() { + println!("ネットワーク接続完了"); + } + + mod server { + fn start() { + // 親モジュールのconnectを呼びたい + connect(); // ここが間違っている + } + } +} + +fn main() { + // ネットワークモジュールのconnectを呼びたい + connect(); // ここも間違っている +} +``` + +```rust-exec:practice8_2.rs +``` diff --git a/public/docs/rust-9.md b/public/docs/rust-9.md new file mode 100644 index 0000000..0854d86 --- /dev/null +++ b/public/docs/rust-9.md @@ -0,0 +1,323 @@ +# 第9章: 一般的なコレクションと文字列 + +これまでの章では、配列やタプルといった固定長のデータ構造を扱ってきました。これらはスタックに格納されるため高速ですが、コンパイル時にサイズが決まっている必要があります。 + +本章では、Rustの標準ライブラリが提供する、**ヒープ領域**にデータを格納する動的なコレクションについて学びます。これらは実行時にサイズを変更可能です。特に、他の言語経験者が躓きやすい「Rustにおける文字列(UTF-8)の扱い」には重点を置いて解説します。 + +主に以下の3つを扱います。 + +1. **ベクタ (`Vec`)**: 可変長のリスト。 +2. **文字列 (`String`)**: UTF-8エンコードされたテキスト。 +3. **ハッシュマップ (`HashMap`)**: キーと値のペア。 + +## ベクタ (`Vec`):可変長配列 + +ベクタは、同じ型の値をメモリ上に連続して配置するデータ構造です。C++の `std::vector` や Javaの `ArrayList`、Pythonのリストに近いものです。 + +### ベクタの作成と更新 + +`Vec::new()` 関数または `vec!` マクロを使用して作成します。要素を追加するには `push` メソッドを使いますが、ベクタを変更するためには `mut` で可変にする必要があります。 + +```rust:vector_basics.rs +fn main() { + // 空のベクタを作成(型注釈が必要な場合がある) + let mut v: Vec = Vec::new(); + v.push(5); + v.push(6); + v.push(7); + + // vec!マクロを使うと型推論が効くため記述が楽 + let mut v2 = vec![1, 2, 3]; + v2.push(4); + + println!("v: {:?}", v); + println!("v2: {:?}", v2); + + // popで末尾の要素を削除して取得(Optionを返す) + let last = v2.pop(); + println!("Popped: {:?}", last); + println!("v2 after pop: {:?}", v2); +} +``` + +```rust-exec:vector_basics.rs +v: [5, 6, 7] +v2: [1, 2, 3, 4] +Popped: Some(4) +v2 after pop: [1, 2, 3] +``` + +### 要素へのアクセス + +要素へのアクセスには「インデックス記法 `[]`」と「`get` メソッド」の2通りの方法があります。安全性において大きな違いがあります。 + + * `&v[i]`: 存在しないインデックスにアクセスすると**パニック**を起こします。 + * `v.get(i)`: `Option<&T>` を返します。範囲外の場合は `None` になるため、安全に処理できます。 + + + +```rust:vector_access.rs +fn main() { + let v = vec![10, 20, 30, 40, 50]; + + // 方法1: インデックス(確実に存在するとわかっている場合に使用) + let third: &i32 = &v[2]; + println!("3番目の要素は {}", third); + + // 方法2: getメソッド(範囲外の可能性がある場合に使用) + match v.get(100) { + Some(third) => println!("101番目の要素は {}", third), + None => println!("101番目の要素はありません"), + } + + // イテレーション + // &v とすることで所有権を移動させずに参照でループする + print!("要素: "); + for i in &v { + print!("{} ", i); + } + println!(); + + // 値を変更しながらイテレーション + let mut v_mut = vec![1, 2, 3]; + for i in &mut v_mut { + *i += 50; // 参照外し演算子(*)を使って値を書き換える + } + println!("変更後: {:?}", v_mut); +} +``` + +```rust-exec:vector_access.rs +3番目の要素は 30 +101番目の要素はありません +要素: 10 20 30 40 50 +変更後: [51, 52, 53] +``` + +## 文字列 (`String`) と UTF-8 + +Rustにおける文字列は、他の言語経験者にとって最も混乱しやすい部分の一つです。 +Rustの文字列は、**UTF-8エンコードされたバイトのコレクション**として実装されています。 + +### `String` と `&str` の違い(復習) + + * **`String`**: 所有権を持つ、伸長可能な、ヒープ上の文字列(`Vec` のラッパー)。 + * **`&str` (文字列スライス)**: どこか(バイナリ領域やヒープ領域)にある文字列データへの参照。 + +### 文字列の操作 + +`String` は `Vec` と同様に `push_str` や `+` 演算子で結合できます。 + +```rust:string_ops.rs +fn main() { + let mut s = String::from("foo"); + s.push_str("bar"); // 文字列スライスを追加 + s.push('!'); // 1文字追加 + println!("{}", s); + + let s1 = String::from("Hello, "); + let s2 = String::from("World!"); + + // + 演算子を使用。 + // s1はムーブされ、以降使用できなくなることに注意 + // シグネチャは fn add(self, s: &str) -> String に近いため + let s3 = s1 + &s2; + + println!("{}", s3); + // println!("{}", s1); // コンパイルエラー:s1はムーブ済み + + // format!マクロを使うと所有権を奪わず、読みやすく結合できる + let s4 = String::from("tic"); + let s5 = String::from("tac"); + let s6 = String::from("toe"); + + let s_all = format!("{}-{}-{}", s4, s5, s6); + println!("{}", s_all); +} +``` + +```rust-exec:string_ops.rs +foobar! +Hello, World! +tic-tac-toe +``` + +### なぜインデックスアクセスができないのか? + +多くの言語では `s[0]` で1文字目を取得できますが、Rustでは**コンパイルエラー**になります。 + +Rustの文字列はUTF-8です。ASCII文字は1バイトですが、日本語のような文字は3バイト(またはそれ以上)を使用します。 + + * `"A"` -\> `[0x41]` (1バイト) + * `"あ"` -\> `[0xE3, 0x81, 0x82]` (3バイト) + +もし `"あ"` という文字列に対して `s[0]` で1バイト目を取得できたとしても、それは `0xE3` という意味のないバイト値であり、プログラマが期待する「あ」ではありません。Rustはこの誤解を防ぐために、インデックスアクセスを禁止しています。 + +文字列の中身を見るには、「バイトとして見る」か「文字(スカラ値)として見る」かを明示する必要があります。 + +```rust:string_utf8.rs +fn main() { + let s = "こんにちは"; // UTF-8で各文字3バイト + + // NG: s[0] はコンパイルエラー + + // 文字(char)として反復処理 + // RustのcharはUnicodeスカラ値(4バイト) + print!("Chars: "); + for c in s.chars() { + print!("{} ", c); + } + println!(); + + // バイトとして反復処理 + print!("Bytes: "); + for b in s.bytes() { + print!("{:x} ", b); // 16進数で表示 + } + println!(); + + // 部分文字列(スライス)の取得には範囲指定が必要 + // ただし、文字の境界に合わないバイトを指定すると実行時にパニックする + let s_slice = &s[0..3]; // 最初の3バイト=「こ」 + println!("Slice: {}", s_slice); +} +``` + +```rust-exec:string_utf8.rs +Chars: こ ん に ち は +Bytes: e3 81 93 e3 82 93 e3 81 ab e3 81 a1 e3 81 8a +Slice: こ +``` + +## ハッシュマップ (`HashMap`) + +ハッシュマップは、キーと値をマッピングしてデータを格納します。Pythonの `dict`、JavaScriptの `Map` やオブジェクト、Rubyの `Hash` に相当します。標準ライブラリの `std::collections` モジュールからインポートする必要があります。 + +### 基本的な操作 + +```rust:hashmap_demo.rs +use std::collections::HashMap; + +fn main() { + let mut scores = HashMap::new(); + + // 挿入 + scores.insert(String::from("Blue"), 10); + scores.insert(String::from("Yellow"), 50); + + // 値の取得(getはOption<&V>を返す) + let team_name = String::from("Blue"); + if let Some(score) = scores.get(&team_name) { + println!("{}: {}", team_name, score); + } + + // 反復処理(順序は保証されない) + for (key, value) in &scores { + println!("{}: {}", key, value); + } +} +``` + +```rust-exec:hashmap_demo.rs +Blue: 10 +Yellow: 50 +Blue: 10 +``` + +### 所有権の移動 + +`HashMap` にキーや値を挿入すると、`String` のような所有権を持つ型はマップ内に**ムーブ**されます(`i32` のような `Copy` トレイトを持つ型はコピーされます)。挿入後に元の変数を使おうとするとエラーになります。 + +### エントリ API による更新 + +「キーが存在しなければ値を挿入し、存在すれば何もしない(あるいは値を更新する)」というパターンは非常に一般的です。Rustでは `entry` APIを使うとこれを簡潔に書けます。 + +```rust:hashmap_update.rs +use std::collections::HashMap; + +fn main() { + let mut scores = HashMap::new(); + scores.insert(String::from("Blue"), 10); + + // 上書き(同じキーでinsertすると値は上書きされる) + scores.insert(String::from("Blue"), 25); + println!("Blue updated: {:?}", scores); + + // キーがない場合のみ挿入 (or_insert) + scores.entry(String::from("Yellow")).or_insert(50); + scores.entry(String::from("Blue")).or_insert(50); // 既に25があるので無視される + println!("Entry check: {:?}", scores); + + // 既存の値に基づいて更新(単語の出現回数カウントなど) + let text = "hello world wonderful world"; + let mut map = HashMap::new(); + + for word in text.split_whitespace() { + // or_insertは挿入された値への可変参照(&mut V)を返す + let count = map.entry(word).or_insert(0); + *count += 1; // 参照外ししてインクリメント + } + + println!("Word count: {:?}", map); +} +``` + +```rust-exec:hashmap_update.rs +Blue updated: {"Blue": 25} +Entry check: {"Blue": 25, "Yellow": 50} +Word count: {"world": 2, "hello": 1, "wonderful": 1} +``` + +## この章のまとめ + + * **`Vec`**: 同じ型の要素を可変長で保持します。範囲外アクセスには注意し、必要なら `get` メソッドを使用します。 + * **`String`**: UTF-8エンコードされたバイト列のラッパーです。インデックス `[i]` によるアクセスは禁止されており、文字として扱うには `.chars()` を、バイトとして扱うには `.bytes()` を使用します。 + * **`HashMap`**: キーバリューストアです。`entry` APIを使用すると、「存在確認してから挿入・更新」という処理を効率的かつ安全に記述できます。 + +### 練習問題1:整数のリスト分析 + +整数のベクタ `vec![1, 10, 5, 2, 10, 5, 20, 5]` が与えられたとき、以下の3つを計算して表示するプログラムを作成してください。 + +1. **平均値 (Mean)** +2. **中央値 (Median)**: リストをソートしたときに真ん中に来る値。 +3. **最頻値 (Mode)**: 最も頻繁に出現する値(ヒント:ハッシュマップを使って出現回数を数えます)。 + +```rust:practice9_1.rs +fn main() { + let numbers = vec![1, 10, 5, 2, 10, 5, 20, 5]; + + +} +``` +```rust-exec:practice9_1.rs +平均値: 7.25 +中央値: 5 +最頻値: 5 +``` + +### 問題2:ピッグ・ラテン (Pig Latin) 変換 + +文字列を「ピッグ・ラテン」と呼ばれる言葉遊びに変換する関数を作成してください。ルールは以下の通りです。 + +1. 単語が**母音** (a, i, u, e, o) で始まる場合、単語のお尻に `-hay` を追加します。 + * 例: `apple` -\> `apple-hay` +2. 単語が**子音**で始まる場合、最初の文字を単語のお尻に移動し、`-ay` を追加します。 + * 例: `first` -\> `irst-fay` + +アルファベットのみ、小文字のみの想定で構いません。 + +```rust:practice9_2.rs +fn pig_latin(word: &str) -> String { + // ここに変換ロジックを実装 + + +} +fn main() { + println!("{}", pig_latin("apple")); + println!("{}", pig_latin("first")); +} +``` +```rust-exec:practice9_2.rs +apple-hay +irst-fay +```