C++ の基礎 Day 4¶
1. 範囲 for 文¶
- ある Range(レンジ)に対する読みやすいループ構文。
- ここでの Range は配列やコンテナ、ビューなどの連続したデータの集まり。
for (要素宣言: Range)
という構文で、Range の要素に順番にアクセスする。
#include <print>
int main()
{
int a[] = { 1, 2, 3, 4, 5 };
for (int i : a)
{
std::println("{}", i);
}
}
#include <print>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
for (int i : v)
{
std::println("{}", i);
}
}
#include <print>
#include <string>
int main()
{
std::string s = "Hello";
for (char c : s)
{
std::println("{}", c);
}
}
#include <print>
#include <ranges>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
for (int i : v | std::views::reverse)
{
std::println("{}", i);
}
}
#include <print>
#include <ranges>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
for (int i : v | std::views::take(3))
{
std::println("{}", i);
}
}
- Range の要素が基本型でない場合、コピーのコストを避けるために、要素へは
const
(コンスト)参照でアクセスする。 - 基本型の要素に対して
const
参照でアクセスしても問題ないので、const auto&
(コンストオートアンド)を基本の選択肢とすると良い。
#include <print>
#include <vector>
#include <string>
int main()
{
std::vector<std::string> v = { "Hello", "World" };
for (const auto& s : v) // s は const std::string&
{
std::println("{}", s);
}
}
- 要素を変更したい場合は、参照
auto&
(オートアンド)でアクセスする。
#include <print>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
for (auto& i : v) // i は int&
{
i *= 2;
}
for (const auto& i : v) // i は const int&
{
std::println("{}", i);
}
}
- 範囲 for 文は、コンパイル時に次のような通常の for 文に展開される。
- ループ内で要素数が増減すると
__begin
や__end
に齟齬が生じるため、範囲 for 文の中で要素数を変更することはできない。
// for (item-declaration : range-initializer) statement
{
auto&& __range = range-initializer;
auto __begin = std::begin(__range);
auto __end = std::end(__range);
for ( ; __begin != __end; ++__begin)
{
item-declaration = *__begin;
statement
}
}
2. 配列¶
- 同じ型の複数の要素を連続したメモリ領域に格納するデータ構造。
- 通常はスタック領域に確保される。
- 配列の要素数はコンパイル時に決定され、実行時に要素数を変更することはできない。
#include <print>
int main()
{
int a[5] = { 1, 2, 3, 4, 5 };
for (const auto& i : a)
{
std::println("{}", i);
}
}
3. std::array
(アレイ)¶
- 配列を C++ のコンテナインタフェースに適合するようラップしたクラス。
- 通常はスタック領域に確保される。
.size()
(サイズ)や.begin()
(ビギン)、.end()
(エンド)などの便利なメンバ関数を持つ。- 実行時性能は配列と同じなので、C++ では配列よりも
std::array
を使うことが推奨される。
#include <print>
#include <array>
int main()
{
std::array<int, 5> a = { 1, 2, 3, 4, 5 };
std::println("size: {}", a.size());
for (const auto& i : a)
{
std::println("{}", i);
}
a.fill(0);
for (const auto& i : a)
{
std::println("{}", i);
}
}
4. std::vector
(ベクター)¶
4.1 概要¶
- 要素数を動的に変更できる配列。
- 通常の配列と同様、同じ型の要素を連続したメモリ領域に格納する。
- メモリはヒープ領域に確保される。
- 初期状態でのメモリは、必要な要素数分だけ確保される。要素数が増えると、より大きなメモリ領域が確保され、既存の要素は新しい領域にコピーされる。この際、新たに確保するメモリ領域のサイズは通常、旧領域の 1.5 倍から 2 倍に設定されており、これによって頻繁なメモリの再確保を抑制している。
.push_back()
(プッシュバック)による要素追加は、償却計算量(ならし計算量) O(1)。 std::vector
の要素は、常に、確保したメモリ領域の先頭から連続して配置される。配列の末尾への要素の追加や削除は、他の要素の移動が不要であるため高速(末尾への追加は償却計算量O(1)
, 末尾からの削除はO(1)
)。
- 一方で、配列の先頭または途中での要素の追加・削除は、それ以降の要素が移動する必要があるため、非効率(計算量
O(N)
)。
- 基本的に、
std::vector
は末尾での要素の追加・削除を高速に行う設計となっている。
4.2 std::vector
の構築¶
- リスト
{ ... }
に用意した値からstd::vector
を構築する。 - リスト内の値は
std::vector<Type>
の要素の型Type
に合わせる。
#include <vector>
#include <string>
int main()
{
std::vector<int> v1 = { 1, 2, 3, 4, 5 };
std::vector<std::string> v2 = { "Hello", "World" };
}
std::vector<Type> v(n, value);
は、n
個のType(value)
からなる配列を構築する。
#include <vector>
#include <string>
int main()
{
std::vector<int> v1(5, -1); // { -1, -1, -1, -1, -1 }
std::vector<std::string> v2(3, "Hello"); // { "Hello", "Hello", "Hello" }
}
std::vector<Type> v(n);
は、n
個のType()
からなる配列を構築する。Type()
は型によって次のような値になる。
Type() |
値 |
---|---|
bool() |
false |
int() |
0 |
double() |
0.0 |
std::string() |
空(要素数が 0)の文字列 |
std::vector<int>() |
空(要素数が 0)の vector |
std::pair<int, int>() |
std::pair<int, int>(0, 0) |
struct Point { int x, y; }; のときの Point() |
Point{ 0, 0 } |
struct Point { int x = -1, y = -1; }; のときの Point() |
Point{ -1, -1 } |
#include <vector>
#include <string>
int main()
{
std::vector<int> v1(5); // { 0, 0, 0, 0, 0 }
std::vector<std::string> v2(3); // { "", "", "" }
}
- 別の
std::vector<Type>
オブジェクトをコピーして新しいstd::vector<Type>
を構築できる。 - コピー元とコピー先の要素の型
Type
は一致している必要がある。
#include <vector>
#include <string>
int main()
{
std::vector<int> v1 = { 1, 2, 3, 4, 5 };
std::vector<int> v2 = v1; // { 1, 2, 3, 4, 5 }
std::vector<std::string> v3 = { "Hello", "World" };
std::vector<std::string> v4 = v3; // { "Hello", "World" }
}
std::vector
型のデフォルトコンストラクタは、要素数が 0 である空のstd::vector
を構築する。
#include <print>
#include <vector>
int main()
{
std::vector<int> v;
std::println("size: {}", v.size()); // size: 0
for (const auto& i : v)
{
// 1 回も実行されない
std::println("{}", i);
}
}
4.3 std::vector<Type>
の主要なメンバ関数¶
メンバ関数 | 戻り値 | 説明 |
---|---|---|
.size() |
size_t |
要素数を返す。 |
.capacity() |
size_t |
内部で確保しているメモリ領域に格納可能な要素数を返す。 |
.reserve(size_t n) |
void |
最低でも n 個の要素を格納できるメモリ領域を確保する。 |
.empty() |
bool |
要素数が 0 なら true 、それ以外なら false を返す。 |
.clear() |
void |
要素を全て削除し、要素数を 0 にする。 |
.resize(size_t n) |
void |
要素数を n に変更する。 |
.push_back(const Type& value) |
void |
末尾に value を追加する。 |
.pop_back() |
void |
末尾の要素を削除する。 |
.insert(std::vector<Type>::iterator pos, const Type& value) |
std::vector<Type>::iterator |
pos の位置に value を挿入する。挿入後、挿入された要素を指すイテレータを返す。 |
.erase(std::vector<Type>::iterator pos) |
std::vector<Type>::iterator |
pos の位置の要素を削除する。削除後、削除された要素の次の要素を指すイテレータを返す。 |
.erase(std::vector<Type>::iterator first, std::vector<Type>::iterator last) |
std::vector<Type>::iterator |
[first, last) の範囲の要素を削除する。削除後、削除された要素の次の要素を指すイテレータを返す。 |
.begin() |
std::vector<Type>::iterator |
配列の先頭位置を指すイテレータを返す。 |
.end() |
std::vector<Type>::iterator |
配列の末尾の終端位置を指すイテレータを返す。 |
.rbegin() |
std::vector<Type>::reverse_iterator |
配列の末尾位置を指す逆イテレータを返す。 |
.rend() |
std::vector<Type>::reverse_iterator |
配列の先頭の前を指す逆イテレータを返す。 |
.operator[](size_t i) |
Type& |
i 番目の要素にアクセスする。 |
.front() |
Type& |
先頭の要素にアクセスする。v[0] と同じ。 |
.back() |
Type& |
末尾の要素にアクセスする。v[v.size() - 1] と同じ。 |
#include <print>
#include <vector>
int main()
{
std::vector<int> v = { 1, 2, 3, 4, 5 };
std::println("size: {}", v.size());
std::println("v[2]: {}", v[2]);
v.push_back(6);
std::println("size: {}", v.size());
v.erase(v.begin() + 2);
for (const auto& i : v)
{
std::println("{}", i);
}
}
5. std::badic_string
(ベーシック・ストリング)¶
5.1 概要¶
- 標準ライブラリには、任意の文字型
Char
に対して、便利な文字列処理を提供するためのクラステンプレートstd::basic_string<Char>
がある。 - 機能はおおよそ
std::vector<Char>
と同じで、文字列を格納するための動的配列として使える。
文字型 | クラステンプレート | 別名 |
---|---|---|
char |
std::basic_string<char> |
std::string |
wchar_t |
std::basic_string<wchar_t> |
std::wstring (ダブリューストリング) |
char8_t |
std::basic_string<char8_t> |
std::u8string (ユーエイトストリング) |
char16_t |
std::basic_string<char16_t> |
std::u16string (ユーじゅうろくストリング) |
char32_t |
std::basic_string<char32_t> |
std::u32string (ユーさんじゅうにストリング) |
- この中でも
std::string
が C++ で最もよく使われる文字列クラス。 std::string
の内部実装を簡易的に表現した例:
struct string
{
char* m_ptr; // 確保したメモリの先頭を指すポインタ
size_t m_size; // 格納している文字列の長さ
size_t m_allocated_capacity; // 動的確保した配列に格納できる文字列の最大の長さ
};
より発展的な実装
現実の文字列処理のプログラムでは、"hello"
や "dog"
, "atcoder"
のように、数文字程度の短い文字列をたくさん扱うことが多い。そのような場合、std::string
が毎回配列の動的確保(内部での new
による確保)を行っていると、プログラムの実行速度が低下してしまう。そこで、一般的な std::basic_string
の実装者は、文字列の長さが短い場合にメモリの動的確保を行わなくて済むよう、std::string
内に小さな配列を用意し、そこに文字列を格納する工夫を行っている。これを SSO (Small String Optimization) と呼ぶ。
struct string
{
char* m_ptr; // 確保したメモリの先頭を指すポインタ
size_t m_size; // 格納している文字列の長さ
size_t m_allocated_capacity; // 動的確保した配列に格納できる文字列の最大の長さ
static constexpr size_t LocalCapacity = 15; // 動的確保不要で格納できる文字列の最大の長さ
char m_local_buffer[LocalCapacity + 1]; // 文字列の長さが LocalCapacity 以下の場合、動的確保の代わりに使う配列
};
m_local_buffer
は、文字列の長さが LocalCapacity
以下の場合に使うことのできる配列。
LocalCapacity
の値は、実装者の方針によって異なるが、一般には 15 程度になっています。この値が大きいほど、より長い文字列をメモリの動的確保を行わずに格納できるが、std::basic_string
クラス自体のサイズが大きくなり、空の文字列や 1 文字の文字列を格納したいだけのときも大きなメモリを消費してしまう。
文字列の格納に m_local_buffer
を使っている場合、m_ptr
は m_local_buffer
の先頭を指す。そのときの文字列の .capacity()
は LocalCapacity
。
struct string
{
char* m_ptr; // 確保したメモリの先頭を指すポインタ
size_t m_size; // 格納している文字列の長さ
size_t m_allocated_capacity; // 動的確保した配列に格納できる文字列の最大の長さ
static constexpr size_t LocalCapacity = 15; // 動的確保不要で格納できる文字列の最大の長さ
char m_local_buffer[LocalCapacity + 1]; // 文字列の長さが LocalCapacity 以下の場合、動的確保の代わりに使う配列
size_t capacity() const noexcept
{
if (_is_local())
{
return LocalCapacity;
}
else
{
return m_allocated_capacity;
}
}
// 内部関数: 文字列が m_local_buffer に格納されているかどうかを返す
bool _is_local() const noexcept
{
return (m_ptr == m_local_buffer);
}
};
さらに消費メモリを節約するため、union
を用いて m_local_buffer
と m_allocated_capacity
を同じメモリ領域に配置することが多い。実際、m_local_buffer
を使用しているときは m_allocated_capacity
は使わないため、合理的である。前述の string
の実装と比べると、sizeof(string)
は 40 バイトから 32 バイトに減る。gcc の標準ライブラリではこの方式の実装が採用されている。
```cpp struct string { char* m_ptr; // 確保したメモリの先頭を指すポインタ size_t m_size; // 格納している文字列の長さ
static constexpr size_t LocalCapacity = 15; // 動的確保不要で格納できる文字列の最大の長さ
union
{
char m_local_buffer[LocalCapacity + 1]; // 文字列の長さが LocalCapacity 以下の場合、動的確保の代わりに使う配列
size_t m_allocated_capacity; // 動的確保した配列に格納できる文字列の最大の長さ
} m_storage;
};
"hello"
という文字列を格納するstd::string
の中身の例(16 バイトのメモリが確保されていて、5 文字の文字列を格納している)。
- 確保された一定のサイズの
char
型の配列の先頭から、文字列の長さ("hello"
の場合は 5)分を消費して、文字列の各要素を、連続して格納する。 - 文字列を格納し終わった直後(ここでは
o
の直後)の要素には、必ず NULL 終端文字'\0'
が格納される。 - 「文字列の長さ」というときには、NULL 終端文字
'\0'
の分は含まない。 - 配列の NULL 終端以降の要素の状態は不定。
- 何も文字列を格納していない、空の状態の
std::string
の中身の例。
- 格納する文字列の長さが 0 だとしても、最後の文字の直後(ここでは配列の先頭)に NULL 終端文字
'\0'
を必ず格納するため、一定のサイズの配列は確保されている。
m_allocated_capacity
と同じ長さの文字列を格納しているstd::string
の中身。
- この状態からさらに新しい要素を追加しようとすると、配列の再確保が行われる。より広い
m_allocated_capacity
の値で配列を再確保し、既存の文字列を新しい配列にコピーしたのち、新しい追加の文字列が格納される。古い配列が使っていたメモリは解放される。
5.2 C 言語の文字列との相互運用性¶
- C++ の
std::string
クラスは、配列の最後に NULL 終端文字'\0'
を持ち、C 言語の文字列と簡単に相互運用できるように設計されている。 - メンバ関数
.c_str()
は、C 言語の文字列(char
型の配列に NULL 終端文字'\0'
が付いたもの。5.1 のm_ptr
)を返す。このメンバ関数を使用することで、std::string
を、NULL 終端された文字列を要求する C 言語の関数でも使用できる。
#include <string>
#include <cstdio>
int main()
{
std::string s = "Hello";
printf("%s\n", s.c_str());
}
- C 言語の文字列から
std::string
に変換するのは簡単。
5.3 std::string
の構築¶
- 文字列リテラルを使って
std::string
を構築する。
#include <print>
#include <string>
int main()
{
std::string s = "abc";
std::println("{}", s);
std::println("size: {}", s.size());
}
std::string s(n, ch);
は、n
回繰り返す文字ch
からなる文字列を構築する。
#include <print>
#include <string>
int main()
{
std::string s(5, 'a'); // 5 個の 'a'
std::println("{}", s);
std::println("size: {}", s.size());
}
- 別の
std::string
オブジェクトをコピーして新しいstd::string
を構築する。
#include <print>
#include <string>
int main()
{
std::string s = "abc";
std::string t = s; // s の値をコピーする
std::println("{}", t);
std::println("size: {}", t.size());
std::println("{}", s); // コピー元は変化していない
std::println("size: {}", s.size()); // コピー元は変化していない
}
std::string s = { 'a', 'b', 'c' };
のようにして、複数の文字からstd::string
を構築する。
#include <print>
#include <string>
int main()
{
char c = 'c';
std::string s = { 'a', 'b', c };
std::println("{}", s);
std::println("size: {}", s.size());
}
- デフォルトコンストラクタは、空の文字列で初期化する。
#include <print>
#include <string>
int main()
{
std::string s; // 空の文字列を構築する
std::println("{}", s);
std::println("size: {}", s.size());
}
5.4 std::string
の主要なメンバ関数¶
メンバ関数 | 戻り値 | 説明 |
---|---|---|
.size() |
size_t |
文字列の長さを返す。 |
.length() |
size_t |
.size() と同じ。 |
.capacity() |
size_t |
現在確保されているメモリ領域ので格納可能な文字列の最大の長さを返す。 |
.reserve(size_t n) |
void |
最低でも n 文字を格納できるメモリ領域を確保する。 |
.empty() |
bool |
文字列が空なら true 、それ以外なら false を返す。 |
.clear() |
void |
文字列を空にする。 |
.resize(size_t n) |
void |
文字列の長さを n に変更する。新しい領域は '\0' で埋める。 |
.push_back(char ch) |
void |
末尾に ch を追加する。 |
.pop_back() |
void |
末尾の要素を削除する。 |
.operator[](size_t i) |
char& |
i 番目の要素にアクセスする。 |
.front() |
char& |
先頭の要素にアクセスする。s[0] と同じ。 |
.back() |
char& |
末尾の要素にアクセスする。s[s.size() - 1] と同じ。 |
.operator+=(const std::string& str) |
std::string& |
文字列の末尾に str を追加する。 |
.c_str() |
const char* |
C 言語の文字列(char 型の配列に NULL 終端文字 '\0' が付いたもの)を返す。 |
.substr(size_t pos, size_t len) |
std::string |
pos 番目の文字から len 文字を抜き出した文字列を返す。 |
.start_with(char ch) |
bool |
文字列が ch で始まるかどうかを返す。 |
.start_with(const std::string& str) |
bool |
文字列が str で始まるかどうかを返す。 |
.end_with(char ch) |
bool |
文字列が ch で終わるかどうかを返す。 |
.end_with(const std::string& str) |
bool |
文字列が str で終わるかどうかを返す。 |
.contains(char ch) |
bool |
文字列が ch を含むかどうかを返す。 |
.contains(const std::string& str) |
bool |
文字列が str を含むかどうかを返す。 |
.find(char ch) |
size_t |
文字列の中で最初に ch が現れる位置を返す。見つからない場合は std::string::npos を返す。 |
.find(const std::string& str) |
size_t |
文字列の中で最初に str が現れる位置を返す。見つからない場合は std::string::npos を返す。 |
.begin() |
std::string::iterator |
文字列の先頭位置を指すイテレータを返す。 |
.end() |
std::string::iterator |
文字列の末尾の終端位置を指すイテレータを返す。 |
.rbegin() |
std::string::reverse_iterator |
文字列の末尾位置を指す逆イテレータを返す。 |
.rend() |
std::string::reverse_iterator |
文字列の先頭の前を指す逆イテレータを返す。 |
#include <print>
#include <string>
int main()
{
std::string s = "abcdefghijklmnopqrstuvwxyz";
std::println("size: {}", s.size());
std::println("capacity: {}", s.capacity());
s.push_back('!');
std::println("size: {}", s.size());
std::println("capacity: {}", s.capacity());
s.pop_back();
std::println("size: {}", s.size());
std::println("capacity: {}", s.capacity());
s.clear();
std::println("size: {}", s.size());
std::println("capacity: {}", s.capacity());
}
6. std::basic_string_view
(ベーシック・ストリング・ビュー)¶
6.1 概要¶
- 文字列の所有権は持たず、文字配列の全部または一部の範囲を参照するクラス。
文字型 | クラステンプレート | 別名 |
---|---|---|
char |
std::basic_string_view<char> |
std::string_view |
wchar_t |
std::basic_string_view<wchar_t> |
std::wstring_view |
char8_t |
std::basic_string_view<char8_t> |
std::u8string_view |
char16_t |
std::basic_string_view<char16_t> |
std::u16string_view |
char32_t |
std::basic_string_view<char32_t> |
std::u32string_view |
- 文字列の先頭のポインタと長さだけを持つ。
std::string_view
の内部実装を簡易的に表現した例:
- 軽量なクラスなので、
const
参照渡しではなく値渡しでよい。 const
ポインタを持つため、std::basic_string_view
経由で文字列を変更しようとするとコンパイルエラーになる。
#include <print>
#include <string_view>
#include <string>
int main()
{
std::string s = "Hello, World!";
std::string_view sv = s;
std::println("{}", sv);
std::println("size: {}", sv.size());
}
- 文字列リテラルを、
std::string
に変換せずに使いたい場合に便利。
#include <print>
#include <string_view>
#include <string>
void F1(const std::string& s)
{
}
void F2(std::string_view sv)
{
}
int main()
{
std::string s = "Hello, World!";
F1(s); // 効率的(const 参照渡し)
F2(s); // 効率的(ポインタと長さだけを渡す)
F1("Hello, World!"); // 非効率: std::string を構築する
F2("Hello, World!"); // 効率的:(ポインタと長さだけを渡す)
}
6.2 std::string_view
の主要なメンバ関数¶
メンバ関数 | 戻り値 | 説明 |
---|---|---|
.size() |
size_t |
文字列の長さを返す。 |
.length() |
size_t |
.size() と同じ。 |
.data() |
const Char* |
文字列の先頭のポインタを返す。 |
.empty() |
bool |
文字列が空なら true 、それ以外なら false を返す。 |
.remove_prefix(size_t n) |
void |
文字列の先頭から n 文字を削除する。 |
.remove_suffix(size_t n) |
void |
文字列の末尾から n 文字を削除する。 |
.operator[](size_t i) |
const Char& |
i 番目の要素にアクセスする。 |
.start_with(Char ch) |
bool |
文字列が ch で始まるかどうかを返す。 |
.start_with(std::string_view sv) |
bool |
文字列が sv で始まるかどうかを返す。 |
.end_with(Char ch) |
bool |
文字列が ch で終わるかどうかを返す。 |
.end_with(std::string_view sv) |
bool |
文字列が sv で終わるかどうかを返す。 |
.contains(Char ch) |
bool |
文字列が ch を含むかどうかを返す。 |
.contains(std::string_view sv) |
bool |
文字列が sv を含むかどうかを返す。 |
.find(Char ch) |
size_t |
文字列の中で最初に ch が現れる位置を返す。見つからない場合は std::string_view::npos を返す。 |
.find(std::string_view sv) |
size_t |
文字列の中で最初に sv が現れる位置を返す。見つからない場合は std::string_view::npos を返す。 |
.substr(size_t pos, size_t len) |
std::string_view |
pos 番目の文字から len 文字を抜き出した string_view を返す。 |
.begin() |
std::string_view::iterator |
文字列の先頭位置を指すイテレータを返す。 |
.end() |
std::string_view::iterator |
文字列の末尾の終端位置を指すイテレータを返す。 |
.rbegin() |
std::string_view::reverse_iterator |
文字列の末尾位置を指す逆イテレータを返す。 |
.rend() |
std::string_view::reverse_iterator |
文字列の先頭の前を指す逆イテレータを返す。 |
#include <print>
#include <string_view>
#include <string>
int main()
{
std::string s = "Hello, World!";
std::string_view sv = s;
std::string_view sv2 = sv.substr(0, 5);
std::println("{}", sv2);
}
力試し¶
問題 1¶
- 与えられた文字列をコピーして、含まれるアルファベットをすべて小文字にした文字列を返す関数
ToLower
を実装せよ。 <cctype>
ヘッダのstd::tolower(ch)
関数を使用するとよい。
#include <print>
#include <string_view>
#include <string>
#include <cctype>
std::string ToLower(std::string_view s)
{
std::string result(s); // string_view から string を構築する
}
int main()
{
std::string s = "Hello, World!";
std::println("{}", ToLower(s));
}
解答例
#include <print>
#include <string_view>
#include <string>
#include <cctype>
std::string ToLower(std::string_view s)
{
std::string result(s); // string_view から string を構築する
for (char& c : result)
{
c = std::tolower(c); // 小文字に変換する
}
return result;
}
int main()
{
std::string s = "Hello, World!";
std::println("{}", ToLower(s));
}
問題 2¶
- 与えられた英数文字列が回文であるかを判定する関数
IsPalindrome
を実装せよ。