728x90

먼저 Ui에 tablewidget을 하나 추가합니다.

 

Ui에서 더블클릭하여 Column을 추가하거나 cpp 코드에서 아래와 같이 컬럼을 추가합니다.

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    AddColumns();
}

void MainWindow::AddColumns()
{
    for(int i = 0 ; i < 16 ; i++) {
        ui->tableWidget->insertColumn(i);
    }
    
    // ui->tableWidget->setColumnCount(16);

    QStringList lHeader;
    lHeader << "one" << "two";

    ui->tableWidget->setHorizontalHeaderLabels(lHeader);
}

setHorizontalHeaderLabels에 QStringList를 넘겨 컬럼의 이름을 변경할 수 있습니다.

다음으로 row아이템들을 추가합니다.

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    AddColumns();		// column 추가
    AddItem();			// row와 아이템 추가
}

void MainWindow::AddItem()
{
    QTableWidget* table = ui->tableWidget;
    QTableWidgetItem* item = new QTableWidgetItem;
    QString tmp;
    int     nRow = 0;

    table->insertRow(nRow);

    item = new QTableWidgetItem;
    tmp.sprintf("row %d", nRow+1);
    item->setText(tmp);
    table->setVerticalHeaderItem(nRow,item);        // row의 이름 설정

    item = new QTableWidgetItem;
    item->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
    item->setBackground(QBrush(QColor(0,0,255)));
    table->setItem(0,8,item);
    item->setText("item" + QString::number(nRow));
    nRow++;
}

column과 비슷하게 setVerticalHeaderItem으로 row의 이름을 설정할 수 있습니다.

setVerticalHeaderItem을 하지 않으면 index 번호대로 설정이 됩니다.

setItem으로 원하는 row, coulmn위치에 아이템을 배치할 수 있습니다.

Item에 background나 alignment도 부여할 수 있습니다.

 

셀을 병합해서 사용하고 싶을때는 setSpan 함수를 사용합니다.

row, column의 인덱스를 전달하고 그리고 몇개의 셀을 병합할지를 전달합니다.

void QTableView::setSpan(int row, int column, int rowSpanCount, int columnSpanCount)
void MainWindow::AddItem()
{
    QTableWidget* table = ui->tableWidget;
    QTableWidgetItem* item = new QTableWidgetItem;
    QString tmp;
    int     nRow = 0;

    table->insertRow(nRow);

    item = new QTableWidgetItem;
    tmp.sprintf("row %d", nRow+1);
    item->setText(tmp);
    table->setVerticalHeaderItem(nRow,item);        // row의 이름 설정

    table->setSpan(nRow,8,1,8);
    item = new QTableWidgetItem;
    item->setTextAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
    item->setBackground(QBrush(QColor(0,0,255)));
    table->setItem(0,8,item);
    item->setText("item" + QString::number(nRow));
    nRow++;
}

다음으로 column과 row를 표의 크기에 맞게 세팅하는 방법입니다.

 

row와 coulmn을 추가하다보면 아래와 같이 한번에 모든 셀을 보는것이 힘들어집니다.

그럴땐 아래와 같은 옵션을 설정하면 모든 row와 coulmn의 셀을 볼수있게 맞춰서 출력이 됩니다.

ui->tableWidget->horizontalHeader()->setResizeMode(QHeaderView::Stretch);
ui->tableWidget->verticalHeader()->setResizeMode(QHeaderView::Stretch);

 

// 23.11.21 추가

최신 버전 QHeaderView에는 setResizeMode 함수가 없으니 setSectionResizeMode(ResizeMode mode) 혹은 특정 Column/Row에만 처리하고 싶다면 setSectionResizeMode(int logicalIndex, ResizeMode mode)를 사용하면 됩니다.

 

아무 설정없이 기본적으론 tablewidget의 아이템을 더블클릭하면 아이템을 수정할 수 있는데 이걸 방지하기 위해 아래 코드를 추가합니다.

ui->tableWidget->setEditTriggers(QAbstractItemView::NoEditTriggers);

그리고 vertical header(row1 ~ 16)에 라인이 안보이는데 아래와 같이 StyleSheet을 적용하면 verital header에도 라인이 그려지게 됩니다.

ui->tableWidget->verticalHeader()->setStyleSheet("QHeaderView::section{"
                                   "border-top:0px solid #E5E5E5;"
                                   "border-left:0px solid #E5E5E5;"
                                   "border-right:1px solid #E5E5E5;"
                                   "border-bottom: 1px solid #D8D8D8;"
                                   "background-color:white;"
                                   "padding:4px}");

table안에 아이템의 입력 값을 제한하기 위해서는 Item에 Validator를 적용한 QLineEdit를 Set해주면 됩니다.

QLineEdit* pLineEdit = new QLineEdit;
pLineEdit->installEventFilter(this);
pLineEdit->setFrame(false);

QIntValidator* pValidator = new QIntValidator(0,100);
pLineEdit->setValidator(pValidator);

ui->tablewidget->setCellWidget(nRow,nColumn,pLineEdit);

QLineEdit에 적용할 수 있는 Validator에 대한 내용은 아래 링크를 참고 부탁드립니다.

 

Qt LineEdit 입력 값 제한하기

QLineEdit을 사용할 때 입력 값을 제한하고 싶을 때가 있습니다. 이럴 때 사용할 수 있는게 QValidator 입니다. QValidator Class | Qt GUI 5.15.3 QValidator Class The QValidator class provides validation of..

1d1cblog.tistory.com

 

728x90
728x90

하나의 Signal을 처리할 때 여러 Slot 함수를 만들지 않고 싶다면 sender() 함수를 사용하여 처리할 수 있습니다. 이전 포스팅에서 사용했던 QSplitter로 예를 들어보겠습니다.

 

Horizontal, Vertical QSplitter가 있습니다. QSplitter의 이동이 끝나면 splitterMoved라는 Signal이 발생하고 이 Signal이 발생됐을 때 하나의 Slot 함수로 처리하고 싶습니다.

connect(horizonSplitter, SIGNAL(splitterMoved(int, int)), this, SLOT(slot_setSpliterState(int, int)));
connect(verticalSplitter, SIGNAL(splitterMoved(int, int)), this, SLOT(slot_setSpliterState(int, int)));

이럴 때 slot에서는 어떤 QObject에서 Signal이 발생한 것인지 알아야 그에 맞는 처리를 할 수 있습니다. 이럴 때 사용하는 것이 sender 함수 입니다.

void MainWindow::slot_setSpliterState(int pos, int index)
{
    const QString sKeyHorizonSplitter = "Spliiter/horizon";
    const QString sKeyVerticalSplitter = "Spliiter/vertical";
    
    QObject* obj = sender();
    if ( obj == nullptr ) { return; }

    if ( obj == horizonSplitter ) {
        qsetting->setValue(sKeyHorizonSplitter, horizonSplitter->saveState());
    } else if ( obj == verticalSplitter ) {
        qsetting->setValue(sKeyVerticalSplitter, verticalSplitter->saveState());
    }
}

위 코드는 sender를 통해 splitter를 비교 후 해당 splitter에 맞는 key-value로 세팅하는 코드입니다.

728x90
728x90

QSplitter를 사용하면 화면에 배치된 UI Component의 크기를 마우스 드래그로 동적으로 조절할 수 있습니다.


위와 같은 ui가 있을 때 TabWidget 안에 Group Box 간, 그리고 TabWidget과 하단의 Text Browser 간에 사이즈를 조절하고 싶을 때 아래와 같이 코드를 작성합니다.

#include <QSplitter>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    QSplitter* sp = new QSplitter();
    sp->addWidget(ui->groupBox);
    sp->addWidget(ui->groupBox_2);
    ui->horizontalLayout->addWidget(sp);

    QSplitter* sp2 = new QSplitter();
    sp2->addWidget(ui->tabWidget);
    sp2->addWidget(ui->textBrowser);
    sp2->setOrientation(Qt::Vertical);
    ui->gridLayout->addWidget(sp2);
}

QSplitter를 생성하고 조절하고자 하는 위젯을 AddWidget을 통해 추가 후 Layout에 추가해주면 됩니다.
 
다음으로 추가할 수 있는 속성들에 대한 함수입니다. 

  • setOrientation : Splitter의 방향을 지정할 수 있습니다.
  • setChildrenCollapsible : Widget의 크기를 최소한 이상으로 줄였을 때 숨길 것인지 설정할 수 있습니다.
  • setOpaqueResize : false로 설정하면 드래그가 종료 될 때 사이즈를 조절하게 됩니다.
  • saveState / restoreState : splitter의 size를 QSettings를 통해 저장할 수 있습니다.

 

728x90
728x90

우리가 windows에서 특정 handle의 상태를 체크하기 위해 WaitForSingleObject 혹은 WaitForMultipleObjects 함수를 사용합니다. 이 함수를 통해 I/O 혹은 뮤텍스 등 특정 이벤트 처리에도 사용합니다. 

 

열혈 TCP/IP 19-3. 커널 오브젝트의 두 가지 상태

커널 오브젝트의 두 가지 상태 - non-signaled 상태 : 이벤트가 발생하지 않은 상태 - signaled 상태 : 이벤트가 발생한 상태, 해당 리소스가 특정 상황에 도달한 상태 - 두 상태를 통해 리소스의 상황을

1d1cblog.tistory.com

하지만 이 함수를 사용하게 되면 바라보는 Handle(커널 오브젝트)가 Signaled 상태가 되는 지를 전달 받은 시간만큼 기다리는데 이때 Blocking 상태가 되게 됩니다. 이를 방지하기 위해 비동기 I/O와 같은 방식을 통해 해당 Handle에 대한 상태 체크를 대기하지 않고 다른 일을 처리할 수 있게 해줍니다.

 

윈도우즈 시스템 프로그래밍 - 19. 비동기 I/O와 APC(3)

중첩 I/O의 구현 - I/O를 할 리소스(파일, 파이프 등)을 만들 때 FILE_FLAG_OVERLAPPED를 인자로 주고 I/O를 하는데 OVERRAPED 구조체의 내용으로 비동기 I/O로 진행을 하고 I/O 연산이 끝나면 Event가 Signaled 상

1d1cblog.tistory.com

위 링크에서 볼 수 있듯이 중첩 I/O, 완료 루틴을 사용하면 되지만 boost에서 제공하는 방법을 사용하는 것도 하나의 방법입니다.

 

boost는 C++에서 사용할 수 있는 필수 라이브러리를 제공해주는데 이 중 비동기 작업을 할 수 있는 Boost.Asio를 제공합니다. 

 

Boost.Asio

Copyright © 2003-2020 Christopher M. Kohlhoff Boost.Asio is a cross-platform C++ library for network and low-level I/O programming that provides developers with a consistent asynchronous model using a modern C++ approach. Overview An overview of the featu

think-async.com

Asio로 비동기 소켓 통신, 시리얼 통신 등을 사용할 수 있고 이 중에서 우리가 볼 것은 Boost.Asio.windows의 object_handle 입니다. 이를 통해 생성된 핸들 값을 받고 비동기 처리를 할 수 있습니다.

#include <iostream>
#include <string>
#include <boost/asio.hpp>
#include <boost/asio/windows/object_handle.hpp>

int main()
{
    boost::asio::io_service io_service;
    HANDLE handle = createEvent(NULL, FALSE, FALSE, NULL);
 
    boost::asio::windows::object_handle asyncReceiveHandle(io, hRecieveEvent);
    asyncReceiveHandle.async_wait([&] (const boost::system::error_code& error) {
        
    });

    io_service.run();
}

 

728x90

'Programming > C++' 카테고리의 다른 글

[C++] Template(2)  (0) 2023.09.22
[C++] Template(1)  (2) 2023.09.19
[C++] std::forward_list, std::list  (0) 2023.07.18
[C++] std::array, std::vector  (0) 2023.07.12
[C++] 람다 함수  (0) 2023.04.07
728x90

이전 포스팅에서 간단히 템플릿에 대해서 설명하였습니다.

 

[C++] Template(1)

기본적으로 템플릿은 편의성과 확장성을 모두 만족할 수 있는 기법입니다. 예를 들어 매개변수로 전달받은 두 수를 더하는 함수를 만든다고 가정했을 때 매개변수는 정수형이 올 수도 실수형이

1d1cblog.tistory.com

우리가 template과 함께 typename 이라는 키워드를 사용하였습니다. 이 typename이라는 키워드는 템플릿 매개변수를 선언할 때 사용합니다. template <typename T> 처럼 사용하는 사람이 있고 template <class T> 로 사용하는 사람이 있는데 똑같이 동작합니다. typename이라는 키워드 자체를 타입이라고 명시를 시켜줄 때 사용할 수도 있습니다.

 

예를 들어 아래와 같이 코드를 작성했을 때 일반적인 상황에서는 전달받은 컨테이너가 가지고 있는 const_iterator 타입을 포인터 변수로 사용하는 코드가 됩니다. 하지만 만약에 전달받은 타입에 const_iterator라는 정적 변수가 있을 때에는 또 다른 의미를 가지게 됩니다. 그리고 x라는 전역 변수가 어딘가에 있다면 이 구문은 포인터 변수를 선언하는 코드가 아닌 단순 곱하기처럼 동작할 가능성을 지니게 됩니다.

template <typename T>
void print(const T& container)
{
	T::const_iterator * x;
}

그렇기에 이러한 문제를 방지하기 위해 이것은 타입이다 라고 명시할 수 있게 typename 키워드를 앞에 붙여서 사용할 수 있습니다.

template <typename T>
void print(const T& container)
{
	typename T::const_iterator * x;
}

템플릿으로 만들어진 클래스를 상속 받아 클래스를 하나 만들어 보겠습니다. Printer라는 클래스는 Test 변수를 하나 생성해서 printTest 함수를 호출합니다. 근데 이 Printer<T>를 상속받아 LogPrinter라는 클래스를 하나 더 만들고 이 안에서 print 함수를 호출하도록 하였습니다. 하지만 이대로 컴파일 시 에러가 나게 됩니다.

#include <iostream>

class Test {
public:
  void printTest() { std::cout << "Test"; }
};

template <typename T>
class Printer {
public:
  void print() {
    T t;
    t.printTest();
  }
};

template <typename T>
class LogPrinter : public Printer<T> {
public:  
  void lprint() {
    print();
  }
};

int main()
{
  LogPrinter<Test> lp;
  lp.lprint();
}

이 print라는 함수를 컴파일 단계에서는 기본 클래스를 명확히 할 수 없기 때문에 컴파일 할 수가 없습니다. 기본 클래스가 전달받은 타입에 따라 print라는 함수가 없을 수도 있기 때문입니다. 이러한 에러를 해결하기 위해서는 아래 방법을 사용할 수 있습니다.

// this 사용
template <typename T>
class LogPrinter : public Printer<T> {
public:  
  void lprint() {
    this->print();
  }
};

// using을 통해 기본 클래스에 함수가 있음을 알려줌
template <typename T>
class LogPrinter : public Printer<T> {
public:
  using Printer<T>::print;
  void lprint() {
    print();
  }
};

// 명시적으로 기본 클래스의 함수 호출
template <typename T>
class LogPrinter : public Printer<T> {
public:  
  void lprint() {
    Printer<T>::print();
  }
};

이렇게 템플릿을 사용하면 코드의 수를 줄일 수 있을 뿐 아니라 유지보수 및 기타 부분에 대해서 장점을 지니게 됩니다. 하지만 이런 템플릿도 잘못된 방법으로 사용할 경우 쓸데없이 코드가 비대해질 수 있습니다. 아래 코드로 예를 들어보겠습니다.

 

아래와 같이 타입과 크기를 전달받는 클래스가 있을 때 아래와 같이 사용한다면 Array<int, 10>::invert와 Array<int, 20>::invert에 대한 인스턴스화 되게 됩니다. 

template <typename T, int size>
class Array {
public:
    void invert();
};

int main()
{
    Array<int, 10> AInt10;
    Array<int, 20> AInt20;
    
    AInt10.invert();
    AInt20.invert();
}

이러한 중복을 막기 위해서는 굳이 사이즈 값은 템플릿으로 받지 않고 멤버 함수의 매개변수로 받아 처리할 수도 있습니다.

template <typename T>
class Array {
public:
    void invert(int nSize);
};

int main()
{
    Array<int> AInt10;
    Array<int> AInt20;
    
    AInt10.invert(10);
    AInt20.invert(20);
}
728x90

'Programming > C++' 카테고리의 다른 글

[C++] boost windows 비동기 처리하기  (2) 2023.10.23
[C++] Template(1)  (2) 2023.09.19
[C++] std::forward_list, std::list  (0) 2023.07.18
[C++] std::array, std::vector  (0) 2023.07.12
[C++] 람다 함수  (0) 2023.04.07
728x90

기본적으로 템플릿은 편의성과 확장성을 모두 만족할 수 있는 기법입니다. 예를 들어 매개변수로 전달받은 두 수를 더하는 함수를 만든다고 가정했을 때 매개변수는 정수형이 올 수도 실수형이 올 수도 있습니다. 이럴 때 함수 오버로딩을 통해 각 매개변수 타입에 대한 함수를 다중 정의할 수 있습니다.

int Add(int a, int b);
double Add(double a, double b);

만약 간단하지 않은 하나의 함수를 여러가지로 다중 정의했을 때 문제가 발생하는 경우 모든 함수를 고치거나 손봐야 하는 경우가 생깁니다. 이럴 경우 사용할 수 있는 것이 템플릿입니다. 템플릿은 아래와 같이 template이라는 키워드를 사용합니다. 이렇게 정의한 코드를 사용자가 호출하면 컴파일러에 의해 다중 정의 코드가 자동으로 만들어지게 됩니다.

template <typename T>
T Add(T a, T b);

 사용자는 호출할 때 자료형을 명시해도 되고 안해도 됩니다.

#include <iostream>
using namespace std;

template <typename T>
T Add(T a, T b)
{
  return a+b;
}

int main()
{
  cout << Add(3,4) << endl;
  cout << Add(3.3, 4.4) << endl;
  cout << Add<int>(3,4) << endl;
  cout << Add<int>(3.3, 4.4) << endl;
}

만약 위와 같이 자료형을 명시한다면 매개변수에 다른 타입을 넣어도 그 자료형에 맞게 캐스팅됩니다.

이 템플릿을 함수뿐 아니라 템플릿에도 적용이 가능합니다. 함수에서 사용하던 것과 다른 점이 있다면 무조건 타입을 명시해 주어야 합니다.

#include <iostream>
using namespace std;

template <typename T>
class valueSave {
public:
  valueSave(T value) : m_value(value) {}
  T getValue() { return m_value; }
private:
  T m_value;
};

int main()
{
  valueSave<int> vInt(10);
  cout << vInt.getValue() << endl;

  valueSave<double> vDouble(11.11);
  cout << vDouble.getValue() << endl;

  valueSave<int> vCast(13.13);
  cout << vCast.getValue() << endl;
}

템플릿을 사용하는 클래스의 선언과 정의를 분리하기 위해서는 아래와 같이 사용합니다.

template <typename T>
class valueSave {
public:
  valueSave(T value) : m_value(value) {}
  T getValue();
private:
  T m_value;
};

template <typename T>
T valueSave<T>::getValue()
{
  return m_value;
}

템플릿을 다중으로 사용할 수 있으며 그리고 템플릿으로 받는 값을 하나의 매개변수로도 사용할 수 있습니다.

#include <iostream>
using namespace std;

template <typename T, int nSize>
class valueSave {
public:
  valueSave(T value) : m_value(value) {}
  T getValue();
  void printSize() { cout << nSize << endl; }
private:
  T m_value;
};

template <typename T, int nSize>
T valueSave<T, nSize>::getValue()
{
  return m_value;
}

int main()
{
  valueSave<int, 3> vInt(10);
  cout << vInt.getValue() << endl;
  vInt.printSize();
}

이 템플릿의 타입과 값을 디폴트로 설정할 수 있습니다.

#include <iostream>
using namespace std;

template <typename T = int, int nSize = 5>
class valueSave {
public:
  valueSave(T value) : m_value(value) {}
  T getValue();
  void printSize() { cout << nSize << endl; }
private:
  T m_value;
};

template <typename T, int nSize>
T valueSave<T, nSize>::getValue()
{
  return m_value;
}

int main()
{
  valueSave<> vInt(10);
  cout << vInt.getValue() << endl;
  vInt.printSize();
}

템플릿을 사용하면 자료형에 상관없이 함수를 재사용할 수 있다는 장점이 있지만 만약 특정 자료형은 다르게 처리해야 하는 경우도 있습니다. 함수에서 이를 이용하기 위해서는 반환형, 매개변수 타입이 모두 같아야 합니다.

#include <iostream>
using namespace std;

template <typename T>
T Add(T a, T b) { return a+b; }

template <>
char Add(char a, char b) 
{
  cout << "char template" << endl;
  return 'c';
}

int main()
{
  cout << Add<int>(3,4) << endl;
  cout << Add<double>(3.3, 4.4) << endl;
  cout << Add<char>('a','b') << endl;
}

 물론 클래스 템플릿에서도 사용할 수 있습니다. 하지만 클래스를 선언할 때 특수화할 클래스에 타입을 명시해 주어야 합니다.

#include <iostream>
using namespace std;

template <typename T>
class valueSave {
public:
  valueSave(T value) : m_value(value) {}
  T getValue() { return m_value; }
private:
  T m_value;
};

template <>
class valueSave<char> {
public:
  valueSave(char value) : m_value(value) {}
  char getValue() {
    cout << "Char Class" << endl;
    return m_value; 
  }
private:
  char m_value;
};

int main()
{
  valueSave<int> vInt(10);
  cout << vInt.getValue() << endl;

  valueSave<char> vChar('A');
  cout << vChar.getValue() << endl;
}

템플릿 특수화된 클래스에서 선언부와 정의부를 분리할 때는 따로 template <>을 기술하지 않아도 됩니다.

#include <iostream>
using namespace std;

template <typename T>
class valueSave {
public:
  valueSave(T value) : m_value(value) {}
  T getValue() { return m_value; }
private:
  T m_value;
};

template <>
class valueSave<char> {
public:
  valueSave(char value) : m_value(value) {}
  char getValue();
private:
  char m_value;
};

char valueSave<char>::getValue()
{
  cout << "Char Class" << endl;
  return m_value; 
}

int main()
{
  valueSave<int> vInt(10);
  cout << vInt.getValue() << endl;

  valueSave<char> vChar('A');
  cout << vChar.getValue() << endl;
}

 

728x90

'Programming > C++' 카테고리의 다른 글

[C++] boost windows 비동기 처리하기  (2) 2023.10.23
[C++] Template(2)  (0) 2023.09.22
[C++] std::forward_list, std::list  (0) 2023.07.18
[C++] std::array, std::vector  (0) 2023.07.12
[C++] 람다 함수  (0) 2023.04.07
728x90

이전 포스팅에서 살펴본 std::array, std::vector의 경우 담겨져 있는 임의 데이터를 확인하기엔 좋지만 데이터의 추가/삭제가 잦은 경우에는 비효율적이라고 소개한적 있습니다.

 

[C++] std::array, std::vector

우리가 보통 배열을 사용한다면 아래와 같은 코드로 사용합니다. int arr[10]; int* pArr = new int[10]; 위와 같이 사용할 경우 정적 배열의 경우 고정된 크기로 설정이 가능하고 인덱스를 넘어 참조하려

1d1cblog.tistory.com

데이터의 추가 삭제가 많은 경우에는 연결리스트를 사용하는게 더욱 효율적일 수 있지만 사용자가 임의로 연결리스트를 구현하기 위해서 노드를 구현하고 그를 위해 동적할당을 한다면 memory leak이 발생할 가능성이 큽니다. 이럴 때 사용할 수 있게 std::array와 마찬가지로 std::forward_list를 제공합니다.

 

std::forward_list는 기본적인 연결리스트 기능을 갖추고 있고 추가 기능을 제공합니다. std::forward_list에서 데이터를 삽입하기 위해서는 push_front()와 insert_after()를 사용합니다. push_front 함수는 연결리스트 가장 앞에 데이터를 삽입하고 insert_after 함수는 반복자를 이용해서 원하는 위치 뒤로 데이터를 삽입합니다.

#include <iostream>
#include <forward_list>
using namespace std;

void printFwdlist(std::forward_list<int>& list) {
	for ( int i : list ) {
		cout << i << ' ';
	} cout << endl;
}

int main(int argc, char* argv[]) 
{
	std::forward_list<int> fwd_list = {1,2,3};
	
	fwd_list.push_front(0);
	printFwdlist(fwd_list);
	
	auto it = fwd_list.begin();
	
	fwd_list.insert_after(it, 5);
	fwd_list.insert_after(++it, 7);
	
	printFwdlist(fwd_list);
}

삭제의 경우에는 pop_front()와 erase_after()를 사용합니다. 삽입과 마찬가지로 각각 첫번째, 그리고 해당 위치 다음 데이터를 조작합니다. erase_after의 경우에는 두가지 형태가 제공되는데 그냥 반복자만 넘겨 해당 위치 다음의 데이터를 제거하는 것과 위치값을 2개를 넘겨 해당 범위의 데이터를 모두 삭제시키는 형태가 있습니다.

#include <iostream>
#include <forward_list>
using namespace std;

void printFwdlist(std::forward_list<int>& list) {
	for ( int i : list ) {
		cout << i << ' ';
	} cout << endl;
}

int main(int argc, char* argv[]) 
{
	std::forward_list<int> fwd_list = {1,2,3,4,5,6,7,8};
	
	fwd_list.pop_front();
	printFwdlist(fwd_list);
	
	auto it = fwd_list.begin();
	
	fwd_list.erase_after(it);
	printFwdlist(fwd_list);
	
	fwd_list.erase_after(it, fwd_list.end());
	printFwdlist(fwd_list);
}

위 함수 외에도 remove, remove_if 함수가 제공되는데 remove 함수는 매개변수 값이 std::forward_list 내에 존재할 경우 지워주게 되고 remove_if 함수는 람다함수를 매개변수로 넘겨 사용하며 함수 몸체에 등호 연산을 통해 해당 데이터를 지우게 됩니다.

#include <iostream>
#include <forward_list>
using namespace std;

void printFwdlist(std::forward_list<int>& list) {
	for ( int i : list ) {
		cout << i << ' ';
	} cout << endl;
}

int main(int argc, char* argv[]) 
{
	std::forward_list<int> fwd_list = {1,2,3,4,5,6,7,8};
	
	fwd_list.remove(1);
	printFwdlist(fwd_list);
	
	fwd_list.remove_if([](int n) { return n > 4; });
	printFwdlist(fwd_list);
}

이 외에도 정렬을 위한 sort 함수, 역순으로 뒤집는 reverse 함수, 인접하는 중복 데이터를 제거하는 unique 함수 등이 있습니다.

#include <iostream>
#include <forward_list>
using namespace std;

void printFwdlist(std::forward_list<int>& list) {
	for ( int i : list ) {
		cout << i << ' ';
	} cout << endl;
}

int main(int argc, char* argv[]) 
{
	std::forward_list<int> fwd_list = {4,1,3,2,5,9,7,8};
	printFwdlist(fwd_list);
	
	fwd_list.sort();						// 오름차순 정렬
	printFwdlist(fwd_list);
	
	fwd_list.sort(std::greater<int>());		// 내림차순 정렬
	printFwdlist(fwd_list);
	
	fwd_list.reverse();
	printFwdlist(fwd_list);
	
	std::forward_list<int> fwd_list2 = {1,1,2,3,3,4,5,4};
	fwd_list2.unique();
	printFwdlist(fwd_list2);
}

 

std::forward_list - cppreference.com

template<     class T,     class Allocator = std::allocator > class forward_list; (1) (since C++11) (2) (since C++17) std::forward_list is a container that supports fast insertion and removal of elements from anywhere in the container. Fast random acce

en.cppreference.com

std::foward_list에 존재하지 않는 기능이 추가된 연결 리스트가 std::list 입니다. std::list의 경우 이중연결리스트로 구현되어 있습니다. std::list에서는 임의의 위치에 데이터를 삽입/삭제할 때 _after가 빠진 함수 insert(), erase()를 사용하며 std::forward_list에서는 제공하지 않는 push_back(), pop_back()도 제공합니다.

#include <iostream>
#include <list>
using namespace std;

void printList(std::list<int>& list) {
	for ( int i : list ) {
		cout << i << ' ';
	} cout << endl;
}

int main(int argc, char* argv[]) 
{
	std::list<int> list = {1,2,3};
	list.push_back(4);
	
	printList(list);
	
	list.insert(list.begin(), 0);
	printList(list);
	
	list.insert(list.end(), 5);
	printList(list);
	
	list.push_back(6);
	list.pop_front();
	list.pop_back();
	
	printList(list);
}

 

std::list - cppreference.com

template<     class T,     class Allocator = std::allocator > class list; (1) (2) (since C++17) std::list is a container that supports constant time insertion and removal of elements from anywhere in the container. Fast random access is not supported.

en.cppreference.com

 

728x90

'Programming > C++' 카테고리의 다른 글

[C++] Template(2)  (0) 2023.09.22
[C++] Template(1)  (2) 2023.09.19
[C++] std::array, std::vector  (0) 2023.07.12
[C++] 람다 함수  (0) 2023.04.07
[Effective C++] 4. 설계 및 선언  (0) 2022.11.14
728x90

우리가 보통 배열을 사용한다면 아래와 같은 코드로 사용합니다.

int arr[10];
int* pArr = new int[10];

위와 같이 사용할 경우 정적 배열의 경우 고정된 크기로 설정이 가능하고 인덱스를 넘어 참조하려 할 경우 Segmanatation fault가 발생합니다. 동적 배열의 경우 new에 대한 delete가 빠질 경우 그에 따른 메모리 누수(memory leak)가 발생할 수 있습니다.

 

이럴 때 C++ 스타일의 배열인 std::array를 사용하면 좋습니다. std::array를 사용할 경우 메모리도 자동으로 할당, 해제하여 사용하기에도 안전합니다. 

 

array의 경우 아래처럼 template을 통해 타입을 지정할 수 있고 두 번째 인자로 크기를 상수로 넘겨주게 됩니다.

std::array를 사용하려면 #include<array>를 추가해주어야 합니다. 기본적으로 접근은 특정 인덱스에 접근하는 방법과 정해진 위치(앞, 뒤)에 접근하는 방법이 있는데 특정 인덱스에 접근하기 위해서는 기존처럼 [] 연산자를 사용하거나 at 함수를 사용할 수 있습니다. 혹은 정해진 위치를 접근하기 위해 front() 함수나 back() 함수를 통해 접근도 가능합니다.

#include <iostream>
#include <array>
using namespace std;

int main(int argc, char* argv[]) 
{
	std::array<int, 10> arr;
	arr[0] = 1;
	cout << "arr[0] = " << arr[0] << endl;
	cout << "arr.at(0) = " << arr.at(0) << endl;
	cout << "arr.front() = " << arr.front() << endl;
}

만약에 인덱스 범위를 넘어서 참조하려 한다면 기존 배열의 경우에는 segmentation fault가 났겠지만 std::array는 좀 다릅니다. [] 연산자를 통해 접근할 경우 쓰레기 값이 출력되고 at을 통해 접근 시 std::out_of_range 에러가 납니다.

이렇게 되면 예외 처리를 통해 에러를 방지할 수 있습니다.

#include <iostream>
#include <array>
using namespace std;

int main(int argc, char* argv[]) 
{
	std::array<int, 3> arr{1,2,3};	// 초기화

	try {
		for ( int i =0; i <= 3; i++) {
			cout << "arr.at(" << i << ") = " << arr.at(i) << endl;
		}
	} catch ( const std::out_of_range& e) {
		cout << "out of range" << endl;
	}
}

이 std::array를 매개변수로 던지는 방법은 const 참조형으로 넘기는 방법과 data 함수를 통해서 저장되어 있는 배열을 넘기는 방법이 있습니다.

#include <iostream>
#include <array>
using namespace std;

void print2constarg(const std::array<int,3>& arr) {
	for ( int i : arr ) {
		cout << i;
	}
	cout << endl;
}

void print2data(const int* pArr, std::size_t size) {
    for (std::size_t i = 0; i < size; i++) {
		cout << pArr[i] << ' ';
	}
	cout << endl;
}

int main(int argc, char* argv[]) 
{
	std::array<int, 3> arr{1,2,3};

	print2constarg(arr);
	print2data(arr.data(), arr.size());
}

위에서 print2constarg의 매개변수 std::array의 크기를 고정시켰는데 만약에 크기가 다른 array를 받으면 에러가 발생합니다.

이것을 방지하기 위해서 변수 크기를 템플릿화 해서 사용한다면 다른 크기의 array도 사용이 가능해집니다. 이렇게 타입도 템플릿 매개변수로 지정할 수 있지만 타입이 기본 자료형이 아닌 경우에 기본 출력이 아닌 방법을 사용해야 할 수 있기 때문에 타입은 지정하는 게 안전하다고 생각합니다.

template <size_t T>
void print2constarg(const std::array<int,T>& arr) {
	for ( int i : arr ) {
		cout << i;
	}
	cout << endl;
}

void print2data(const int* pArr, std::size_t size) {
    for (std::size_t i = 0; i < size; i++) {
		cout << pArr[i] << ' ';
	}
	cout << endl;
}

int main(int argc, char* argv[]) 
{
	std::array<int, 11> arr{1,2,3};

	print2constarg(arr);
	print2data(arr.data(), arr.size());
}

마지막으로 반복자를 통해 요소를 접근하는 방법입니다. begin은 첫 번째 원소를 가리키는 반복자를 반환, end는 마지막 원소의 다음을 가리키는 반복자를 반환합니다.

#include <iostream>
#include <array>
using namespace std;

template <size_t T>
void print2constarg(const std::array<int,T>& arr) {
	for ( auto i : arr ) {
		cout << i;
	}
	cout << endl;
	
	for ( auto it = arr.begin(); it!= arr.end(); ++it ) {
		cout << *it;
	}
	cout << endl;
}


int main(int argc, char* argv[]) 
{
	std::array<int, 3> arr{1,2,3};

	print2constarg(arr);
}

여기에 const_iterator를 사용할 수 있는 cbegin, cend 그리고 reverse_iterator를 사용하는 rbegin, rend가 있지만 반복자에 const가 붙는 것, begin/end가 반대인 것이 생각하면 됩니다.

 

std::array - cppreference.com

template<     class T,     std::size_t N > struct array; (since C++11) std::array is a container that encapsulates fixed size arrays. This container is an aggregate type with the same semantics as a struct holding a C-style array T[N] as its only non-s

en.cppreference.com

C 스타일의 배열보다는 std::array가 더 낫지만 array도 몇가지 단점이 있는데 크기를 상수로 전달해야 하고 요소를 추가/삭제할 수 없다는 제약이 있습니다. 이러한 제약을 해결한 std::vector가 있습니다.

 

vector는 생성 시에 굳이 크기를 넘겨주지 않아도 됩니다. 혹은 넘겨주고 그 요소들을 특정 값으로 모두 초기화할 수도 있습니다.

#include <iostream>
#include <vector>
using namespace std;

int main(int argc, char* argv[]) 
{
	std::vector<int> vecInt;
	std::vector<int> vecInt2 = {1,2,3};
	std::vector<int> vecInt3(10);
	std::vector<int> vecInt4(10,0);	
}

vector는 크기가 가변이기 때문에 추가/삭제도 가능한데 추가를 위해 insert(), push_back()를 사용합니다. insert의 경우 추가를 원하는 위치를 같이 넘겨 원하는 위치에 값을 추가하고 push_back의 경우에는 맨 뒤에 요소를 추가합니다.

#include <iostream>
#include <vector>
using namespace std;

int main(int argc, char* argv[]) 
{
	std::vector<int> vecInt;
	
	vecInt.push_back(0);
	vecInt.push_back(30);
	
	for ( int i : vecInt ) {
		cout << i << ' ';
	} cout << endl;
	vecInt.insert(vecInt.begin()+1,10);
	
	for ( int i : vecInt ) {
		cout << i << ' ';
	} cout << endl;
}

추가 시에 벡터가 가질 수 있는 capacity를 체크 후 모자르다면 capacity를 늘리고 추가합니다. capacity는 vector가 수용할 수 있는 요소 개수 정도라고 생각하면 되고 크기(size)는 현재 vector가 가지고 있는 요소 개수라고 생각하면 됩니다. 

 

push_back의 경우에는 단순히 뒤에 추가하는 작업이기 때문에 O(1)의 시간 복잡도를 나타내지만, insert의 경우 특정 위치에 요소를 추가하면 요소들을 복사/이동 해야하기 때문에 O(n)의 시간 복잡도를 나타냅니다. 그렇기 때문에 추가/삭제가 빈번히 일어나는 경우 vector 자료구조를 사용하는게 오히려 독이될 수 있습니다.

 

추가에 있어 emplace 함수가 있는데 이 함수는 객체를 요소로 추가할 때 임시 객체를 생성할 필요 없이 사용 가능하게 해주는 함수입니다. 이렇게 봤을 땐 무조건 emplace가 좋을거 같지만 형 변환 등에 취약할 수 있습니다. 관련해서 정리가 잘 되어 있는 블로그 포스팅 링크 남겨둡니다.

 

emplace_back 과 push_back 의 차이

item 타입의 생성자가 타입을 인자로 받는다면? push_back 함수는 '객체' 를 집어 넣는 형식으로, 객체가 없이 삽입을 하려면 "임시객체 (rvalue) " 가 있어야 합니다. 또는 암시적 형변환이 가능하다면,

openmynotepad.tistory.com

 

std::vector - cppreference.com

template<     class T,     class Allocator = std::allocator > class vector; (1) (2) (since C++17) 1) std::vector is a sequence container that encapsulates dynamic size arrays. The elements are stored contiguously, which means that elements can be acces

en.cppreference.com

 

728x90

'Programming > C++' 카테고리의 다른 글

[C++] Template(1)  (2) 2023.09.19
[C++] std::forward_list, std::list  (0) 2023.07.18
[C++] 람다 함수  (0) 2023.04.07
[Effective C++] 4. 설계 및 선언  (0) 2022.11.14
[Effective C++] 3. 자원 관리  (0) 2022.11.07

+ Recent posts