C++ クラス構成と動的メモリ管理の実装解説

クラス構成による GUI 模擬

オブジェクト指向設計において、クラス間の関係性を理解するために、コンポジション(構成)を用いた簡易 GUI システムを実装します。ここでは、ボタンクラスをウィンドウクラス内部で管理し、インタフェースの公開範囲(public/private)が設計に与える影響を検討します。

Widget クラスの実装

UI 部品を表す基底となるクラスです。ラベル情報を持ち、操作された際の動作を定義します。

#pragma once
#include <iostream>
#include <string>

class Widget {
public:
    explicit Widget(const std::string &caption_);
    const std::string& get_caption() const;
    void activate();

private:
    std::string caption;
};

Widget::Widget(const std::string &caption_): caption{caption_} {
}

inline const std::string& Widget::get_caption() const {
    return caption;
}

inline void Widget::activate() {
    std::cout << "Widget '" << caption << "' activated\n";
}

Panel クラスの実装

複数の Widget を保持するコンテナクラスです。内部状態を隠蔽し、必要な操作のみを公開します。

#pragma once
#include <iostream>
#include <vector>
#include <algorithm>
#include "widget.hpp"

class Panel {
public:
    explicit Panel(const std::string &header_);
    void render() const;
    void shutdown();
    void register_widget(const std::string &caption);
    void trigger_widget(const std::string &caption);

private:
    bool exists(const std::string &caption) const;

private:
    std::string header;
    std::vector<Widget> widgets;
};

Panel::Panel(const std::string &header_): header{header_} {
    widgets.emplace_back("close");
}

inline void Panel::render() const {
    std::string border(40, '=');
    std::cout << border << std::endl;
    std::cout << "Panel : " << header << std::endl;
    int index = 0;
    for(const auto &w: widgets)
        std::cout << ++index << ". " << w.get_caption() << std::endl;
    std::cout << border << std::endl;
}

inline void Panel::shutdown() {
    std::cout << "shutdown panel '" << header << "'" << std::endl;
    trigger_widget("close");
}

inline bool Panel::exists(const std::string &caption) const {
    for(const auto &w: widgets)
        if(w.get_caption() == caption)
            return true;
    return false;
}

inline void Panel::register_widget(const std::string &caption) {
    if(exists(caption))
        std::cout << "widget " << caption << " already exists!\n";
    else
        widgets.emplace_back(caption);
}

inline void Panel::trigger_widget(const std::string &caption) {
    for(auto &w: widgets)
        if(w.get_caption() == caption) {
            w.activate();
            return;
        }
    std::cout << "no widget: " << caption << std::endl;
}

動作検証

メインプログラムでは、Panel インスタンスを生成し、Widget の追加・表示・終了操作を行います。

#include "panel.hpp"
#include <iostream>

void run_simulation(){
    Panel p("MainScreen");
    p.register_widget("create");
    p.register_widget("delete");
    p.register_widget("update");
    p.register_widget("create"); // 重複追加テスト
    p.render();
    p.shutdown();
}

int main() {
    std::cout << "Composition based GUI simulation:\n";
    run_simulation();
}

インタフェースを public にすることで外部からのアクセスは容易になりますが、実装変更時の影響範囲が広がります。逆に private に encapsulate することで内部詳細を隠蔽し、保守性を高めることができます。また、参照戻し値は性能面で有利ですが、const 修飾がない場合は安全性に注意が必要です。

標準コンテナの複製動作

std::vector におけるコピーコンストラクトの動作(深コピー)を確認します。ネストされた構造においても、要素が独立して複製されることを検証します。

#include <iostream>
#include <vector>

void verify_flat_vector();
void verify_nested_vector();
void print_vector(const std::vector<int> &v);
void print_matrix(const std::vector<std::vector<int>>& v);

int main() {
    std::cout << "Deep Copy Check 1: std::vector<int>\n";
    verify_flat_vector();

    std::cout << "\nDeep Copy Check 2: Nested std::vector\n";
    verify_nested_vector();
}

void verify_flat_vector() {
    std::vector<int> source(5, 100);
    const std::vector<int> dest(source);

    std::cout << "********** After Copy Construction **********\n";
    std::cout << "source: "; print_vector(source);
    std::cout << "dest: "; print_vector(dest);
    
    source.at(0) = -99;

    std::cout << "********** After Modifying source[0] **********\n";
    std::cout << "source: "; print_vector(source);
    std::cout << "dest: "; print_vector(dest); 
}

void verify_nested_vector() {
    std::vector<std::vector<int>> source{{10, 20, 30}, {40, 50, 60, 70}};
    const std::vector<std::vector<int>> dest(source);

    std::cout << "********** After Copy Construction **********\n";
    std::cout << "source: "; print_matrix(source);
    std::cout << "dest: "; print_matrix(dest);

    source.at(0).push_back(-99);

    std::cout << "********** After Modifying source[0] **********\n";
    std::cout << "source: \n";  print_matrix(source);
    std::cout << "dest: \n";  print_matrix(dest);
}

void print_vector(const std::vector<int> &v) {
    if(v.empty()) {
        std::cout << '\n';
        return;
    }
    std::cout << v.at(0);
    for(size_t i = 1; i < v.size(); ++i)
        std::cout << ", " << v.at(i);
    std::cout << '\n';  
}

void print_matrix(const std::vector<std::vector<int>>& v) {
    if(v.empty()) {
        std::cout << '\n';
        return;
    }
    for(auto &row: v)
        print_vector(row);
}

at() メソッドは境界チェックを行い安全ですが、オペレータ [] はチェックを行わないため高速です。参照変数を用いることでメモリコピーを回避できますが、参照元の寿命管理に注意が必要です。

独自動的配列クラスの実装

std::vector の動作を模倣し、動的メモリ管理の仕組みを理解するための簡易コンテナクラスを作成します。コピーコンストラクトと代入処理における深コピーの重要性を確認します。

SimpleVector クラス定義

#pragma once
#include <iostream>
#include <cstdlib>

class SimpleVector {
public:
    SimpleVector();
    explicit SimpleVector(int size_);
    SimpleVector(int size_, int fill_value);
    SimpleVector(const SimpleVector &other);
    ~SimpleVector();
    
    int size() const;
    int& access(int index);
    const int& access(int index) const;
    SimpleVector& copy_from(const SimpleVector &other);

    int* begin();
    int* end();
    const int* begin() const;
    const int* end() const;

private:
    int count;
    int *buffer;
};

SimpleVector::SimpleVector(): count{0}, buffer{nullptr} {}

SimpleVector::SimpleVector(int size_): count{size_}, buffer{new int[size_]} {}

SimpleVector::SimpleVector(int size_, int fill_value): count{size_}, buffer{new int[size_]} {
    for(auto i = 0; i < count; ++i)
        buffer[i] = fill_value;
}

SimpleVector::SimpleVector(const SimpleVector &other): count{other.count}, buffer{new int[count]} {
    for(auto i = 0; i < count; ++i)
        buffer[i] = other.buffer[i];
}

SimpleVector::~SimpleVector() {
    delete [] buffer;
}

int SimpleVector::size() const {
    return count;
}

const int& SimpleVector::access(int index) const {
    if(index < 0 || index >= count) {
        std::cerr << "Error: index out of range\n";
        std::exit(1);
    }
    return buffer[index];
}

int& SimpleVector::access(int index) {
    if(index < 0 || index >= count) {
        std::cerr << "Error: index out of range\n";
        std::exit(1);
    }
    return buffer[index];
}

SimpleVector& SimpleVector::copy_from(const SimpleVector &other) { 
    if(this == &other) 
        return *this;

    int *temp_buffer = new int[other.count];
    for(int i = 0; i < other.count; ++i)
        temp_buffer[i] = other.buffer[i];
    
    delete[] buffer;
    count = other.count;
    buffer = temp_buffer;
    return *this;
}

int* SimpleVector::begin() { return buffer; }
int* SimpleVector::end() { return buffer + count; }
const int* SimpleVector::begin() const { return buffer; }
const int* SimpleVector::end() const { return buffer + count; }

動作テスト

#include "simple_vector.hpp"
#include <iostream>

void test_basic();
void test_assignment();
void print_data(const SimpleVector &v);

int main() {
    std::cout << "Test 1: Basic Operations\n";
    test_basic();

    std::cout << "\nTest 2: Assignment Logic\n";
    test_assignment();
}

void test_basic() {
    int n;
    std::cout << "Enter size: ";
    std::cin >> n;

    SimpleVector v1(n);
    for(auto i = 0; i < n; ++i)
        v1.access(i) = (i + 1) * 5;
    std::cout << "v1: ";  print_data(v1);

    SimpleVector v2(n, 42);
    SimpleVector v3(v2);
    v2.access(0) = -1;
    std::cout << "v2: ";  print_data(v2);
    std::cout << "v3: ";  print_data(v3);
}

void test_assignment() {
    const SimpleVector  src(5, 42);
    SimpleVector dst;
    dst.copy_from(src);

    std::cout << "src: ";  print_data(src);
    std::cout << "dst: ";  print_data(dst);
}

void print_data(const SimpleVector &v) {
    if(v.size() == 0) {
        std::cout << '\n';
        return;
    }
    std::cout << v.access(0);
    for(auto i = 1; i < v.size(); ++i)
        std::cout << ", " << v.access(i);
    std::cout << '\n';
}

メモリ確保と解放の順序は重要です。新しいメモリを確保してから古いものを解放するパターン(copy_and_swap や一時変数利用)を採用することで、例外発生時のデータ損失を防ぎます。const メソッドと非 const メソッドのオーバーロードにより、読み取り専用アクセスと書き込みアクセスを区別できます。

行列クラスの実装

2 次元データを管理する Matrix クラスを実装します。1 次元配列を論理的に 2 次元として扱うインデックス計算と、const 正確性について扱います。

MatrixGrid クラス定義

#pragma once
#include <iostream>
#include <algorithm>
#include <cstdlib>

class MatrixGrid {
public:
    MatrixGrid(int h_, int w_, double init_val = 0);
    explicit MatrixGrid(int size_, double init_val = 0);
    MatrixGrid(const MatrixGrid &src);
    ~MatrixGrid();

    void load_data(const double *src_data, int total_size);
    void reset();
    
    const double& cell(int r, int c) const;
    double& cell(int r, int c);
    
    int height() const;
    int width() const;
    void show() const;

private:
    int h_rows;
    int w_cols;
    double *data_pool;
};

MatrixGrid::MatrixGrid(int h_, int w_, double init_val): h_rows{h_}, w_cols{w_}, data_pool{new double[h_ * w_]} {
    std::fill_n(data_pool, h_ * w_, init_val);
}

MatrixGrid::MatrixGrid(int size_, double init_val): h_rows{size_}, w_cols{size_}, data_pool{new double[size_ * size_]} {
    std::fill_n(data_pool, size_ * size_, init_val);
}

MatrixGrid::MatrixGrid(const MatrixGrid &src): h_rows{src.h_rows}, w_cols{src.w_cols}, data_pool{new double[src.h_rows * src.w_cols]} {
    std::copy(src.data_pool, src.data_pool + src.h_rows * src.w_cols, data_pool);
}

MatrixGrid::~MatrixGrid() {
    delete[] data_pool;
}

void MatrixGrid::load_data(const double *src_data, int total_size) {
    if(total_size != h_rows * w_cols) {
        std::cerr << "Size Mismatch Error\n";
        std::exit(1);
    }
    for(int i = 0; i < h_rows; i++) {
        std::copy(src_data + i * w_cols, src_data + (i + 1) * w_cols, data_pool + i * w_cols);    
    }
}

void MatrixGrid::reset() {
    std::fill_n(data_pool, h_rows * w_cols, 0);    
}

const double& MatrixGrid::cell(int r, int c) const {
    if(r < 0 || r >= h_rows || c < 0 || c >= w_cols) {
        std::cerr << "IndexError: out of range\n";
        std::exit(1);
    }
    return data_pool[r * w_cols + c];
}

double& MatrixGrid::cell(int r, int c) {
    if(r < 0 || r >= h_rows || c < 0 || c >= w_cols) {
        std::cerr << "IndexError: out of range\n";
        std::exit(1);
    }
    return data_pool[r * w_cols + c];
}

int MatrixGrid::height() const { return h_rows; }
int MatrixGrid::width() const { return w_cols; }

void MatrixGrid::show() const {
    for(int i = 0; i < h_rows; i++) {
        std::cout << data_pool[i * w_cols];
        for(int j = 1; j < w_cols; j++) {
            std::cout << ", " << data_pool[i * w_cols + j];
        }
        std::cout << '\n';
    }
}

利用例

#include <iostream>
#include <cstdlib>
#include "matrix_grid.hpp"

void demo_creation();
void demo_modification();
void print_row(const MatrixGrid &m, int r_idx);

int main() {
    std::cout << "Demo 1: Creation\n";
    demo_creation();

    std::cout << "\nDemo 2: Modification\n";
    demo_modification();
}

void demo_creation() {
    double raw_data[1000] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int h, w;
    std::cout << "Enter height and width: ";
    std::cin >> h >> w;

    MatrixGrid m1(h, w);
    m1.load_data(raw_data, h * w);

    MatrixGrid m2(w, h);
    m2.load_data(raw_data, w * h);

    MatrixGrid m3(h);
    m3.load_data(raw_data, h * h);

    std::cout << "Matrix m1: \n";   m1.show();
    std::cout << "Matrix m2: \n";   m2.show();
    std::cout << "Matrix m3: \n";   m3.show();
}

void demo_modification() {
    MatrixGrid m1(2, 3, -1);
    const MatrixGrid m2(m1);
    
    std::cout << "Matrix m1: \n";   m1.show();
    std::cout << "Matrix m2: \n";   m2.show();

    m1.reset();
    m1.cell(0, 0) = 1;

    std::cout << "After update m1: \n";
    std::cout << "Row 0 of m1: "; print_row(m1, 0);
    std::cout << "Row 0 of m2: "; print_row(m2, 0);
}

void print_row(const MatrixGrid &m, int r_idx) {
    if(r_idx < 0 || r_idx >= m.height()) {
        std::cerr << "IndexError: row index out of range\n";
        exit(1);
    }
    std::cout << m.cell(r_idx, 0);
    for(int j = 1; j < m.width(); ++j)
        std::cout << ", " << m.cell(r_idx, j);
    std::cout << '\n';
}

アプリケーション:住所録管理

これまでのクラス設計技術を統合し、 Lambda 式を用いたソート機能を持つ住所録システムを構築します。

Entry クラス

#pragma once
#include <iostream>
#include <string>

class Entry {
public:
    Entry(const std::string &person_name, const std::string &number_);
    const std::string &name() const;
    const std::string &number() const;
    void show() const;

private:
   std::string person_name;
   std::string number;
};

Entry::Entry(const std::string &person_name, const std::string &number_): person_name{person_name}, number{number_} {}

const std::string& Entry::name() const { return person_name; }
const std::string& Entry::number() const { return number; }

void Entry::show() const {
    std::cout << person_name << ", " << number;
}

AddressBook クラス

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include "entry.hpp"

class AddressBook {
public:
    void register_entry(const std::string &name, const std::string &number);
    void delete_entry(const std::string &name);
    void search(const std::string &name) const;
    void list_all() const;
    size_t count() const;
    
private:
    int find_index(const std::string &name) const;
    void organize();

private:
    std::vector<Entry> database;
};

void AddressBook::register_entry(const std::string &name, const std::string &number) {
    if(find_index(name) == -1) {
        database.emplace_back(name, number);
        std::cout << name << " registered.\n";
        organize();
        return;
    }
    std::cout << name << " exists. failed.\n"; 
}

void AddressBook::delete_entry(const std::string &name) {
    int idx = find_index(name);
    if(idx == -1) {
        std::cout << name << " not found.\n";
        return;
    }
    database.erase(database.begin() + idx);
    std::cout << name << " removed.\n";
}

void AddressBook::search(const std::string &name) const {
    int idx = find_index(name);
    if(idx == -1) {
        std::cout << name << " not found!\n";
        return;
    }
    database[idx].show(); 
    std::cout << '\n';
}

void AddressBook::list_all() const {
    for(auto &item: database) {
        item.show(); 
        std::cout << '\n';
    }
}

size_t AddressBook::count() const {
    return database.size();
}

int AddressBook::find_index(const std::string &name) const {
    for(size_t i = 0; i < database.size(); ++i) {
        if(database[i].name() == name) {
            return static_cast<int>(i);
        }
    }
    return -1;
}

void AddressBook::organize() {
     std::sort(database.begin(), database.end(), 
              [](const Entry& a, const Entry& b) {
                  return a.name() < b.name();
              });
} 

実行フロー

#include "address_book.hpp"

void run_app() {
    AddressBook book;

    std::cout << "1. Register entries\n";
    book.register_entry("Bob", "18199357253");
    book.register_entry("Alice", "17300886371");
    book.register_entry("Linda", "18184538072");
    book.register_entry("Alice", "17300886371");

    std::cout << "\n2. List all\n";
    std::cout << "Total: " << book.count() << " entries.\n";
    book.list_all();

    std::cout << "\n3. Search\n";
    book.search("Bob");
    book.search("David");

    std::cout << "\n4. Delete\n";
    book.delete_entry("Bob");
    book.delete_entry("David");
}

int main() {
    run_app();
}

深コピーはオブジェクトごとに独立したメモリ領域を確保し、データの実体を複製します。一方、浅コピーは参照のみを共有するため、一方の変更が他方に影響します。STL コンテナを利用することでメモリ管理の負担を減らし、Lambda 式を用いることでコレクション操作を簡潔に記述できます。これらの技術を組み合わせることで、安全かつ効率的な C++ プログラムを構築可能です。

タグ: C++ class Memory-Management OOP STL

8月1日 03:38 投稿