변환 생성자(conversion constructor)란, 클래스의 객체를 다른 타입으로 자동으로 변환시켜주는 생성자를 의미합니다.
즉, 특정 클래스의 객체를 다른 클래스의 객체로 변환할 수 있도록 해주는 역할을 합니다.
변환 생성자는 다음과 같은 형태로 정의됩니다.

 

class TargetClass {
public:
    TargetClass(SourceClass source_object);
};
위의 코드에서 TargetClass는 변환하려는 타입이고, SourceClass는 변환할 객체의 원래 타입입니다.
이러한 변환 생성자는 해당 클래스를 대상으로 암시적으로 호출되며, 사용자가 직접 호출할 수는 없습니다.

 

 

예를 들어, 다음과 같은 Rational 클래스가 있다고 가정해보겠습니다

 

class Rational {
public:
    Rational(int numerator, int denominator);
};

 

이 클래스는 분자와 분모를 받아서 유리수(Rational number)를 생성하는 생성자입니다.

이제 이 클래스의 변환 생성자를 추가하여, Rational 객체를 double 타입으로 변환할 수 있도록 만들어 보겠습니다.

 

class Rational {
public:
    Rational(int numerator, int denominator);
    operator double() const {
        return static_cast<double>(numerator) / denominator;
    }
private:
    int numerator;
    int denominator;
};

 

위의 코드에서 operator double() 함수가 변환 생성자입니다. 이 함수는 Rational 객체를 double 타입으로 변환합니다.

즉, 다음과 같이 Rational 객체를 double로 변환하여 사용할 수 있습니다.

 

Rational r(3, 2);
double d = r; // Rational 객체 r을 double 타입으로 자동 변환

 

위와 같이 변환 생성자를 사용하면, 클래스 객체의 타입 변환을 간단하게 처리할 수 있으며, 코드의 가독성과 유지보수성을 높일 수 있습니다. 하지만, 자주 사용되는 타입 변환이 아닌 경우, 불필요한 오버헤드를 초래할 수 있으므로, 적절한 사용을 고려해야 합니다.

 

 

아래 예제에서는 Matrix 클래스를 정의하고, 두 개의 Matrix 객체를 더하는 연산을 수행합니다. 이 때, 더하기 연산자를 통해 두 개의 Matrix 객체를 더할 수 있도록 Matrix 클래스에 변환 생성자와 변환 연산자를 구현합니다.

 

#include <iostream>
#include <vector>

class Matrix {
public:
  Matrix(int rows, int cols) : rows_(rows), cols_(cols), data_(rows * cols) {}

  // 변환 생성자: std::vector<double>를 Matrix로 변환
  Matrix(const std::vector<double>& data, int rows, int cols) : rows_(rows), cols_(cols), data_(data) {}

  // 변환 연산자: Matrix를 std::vector<double>로 변환
  operator std::vector<double>() const {
    return data_;
  }

  // 더하기 연산자
  Matrix operator+(const Matrix& other) const {
    Matrix result(rows_, cols_);

    for (int i = 0; i < rows_ * cols_; ++i) {
      result.data_[i] = data_[i] + other.data_[i];
    }

    return result;
  }

  // 인덱싱 연산자
  double& operator()(int row, int col) {
    return data_[row * cols_ + col];
  }

  // 상수 인덱싱 연산자
  const double& operator()(int row, int col) const {
    return data_[row * cols_ + col];
  }

private:
  int rows_;
  int cols_;
  std::vector<double> data_;
};

int main() {
  // 2x2 Matrix 생성
  Matrix m1(2, 2);
  m1(0, 0) = 1.0;
  m1(0, 1) = 2.0;
  m1(1, 0) = 3.0;
  m1(1, 1) = 4.0;

  // 2x2 Matrix 생성
  Matrix m2(2, 2);
  m2(0, 0) = 5.0;
  m2(0, 1) = 6.0;
  m2(1, 0) = 7.0;
  m2(1, 1) = 8.0;

  // Matrix 더하기
  Matrix result = m1 + m2;

  // 결과 출력
  std::vector<double> data = result; // Matrix를 std::vector<double>로 변환
  std::cout << "Result:" << std::endl;
  for (int i = 0; i < data.size(); ++i) {
    std::cout << data[i] << " ";
    if ((i + 1) % 2 == 0) {
      std::cout << std::endl;
    }
  }
}
이 예제에서는 Matrix 클래스에 std::vector<double> 을 인수로 받는 변환 생성자를 추가합니다. 이 생성자는 std::vector<double>
을 Matrix 객체로 변환합니다. 또한, Matrix 객체를 std::vector<double> 로 변환하는 변환 연산자도 추가합니다. 이 변환 연산자는
Matrix 객체를 std::vector<double>으로 변환합니다. 이렇게 하면 Matrix 객체를 std::vector<double>로 변환할 수 있으며, 이는 더하기 연산의 결과를 출력하기 위해 사용됩니다.
더하기 연산자에서는 두 개의 Matrix 객체를 더한 결과를 새로운 Matrix 객체로 반환합니다. 이때 반환된 Matrix 객체는 std::vector<double>로 변환할 수 있으며, 이를 통해 결과를 출력합니다.
이 예제에서는 또한 Matrix 클래스에 인덱싱 연산자와 상수 인덱싱 연산자를 정의하여 Matrix 객체의 요소에 접근할 수 있도록 했습니다. 이처럼 변환 생성자와 변환 연산자를 사용하면, 클래스와 클래스 간의 변환을 편리하게 처리할 수 있습니다.
\하지만 이러한 기능을 남용하면 코드의 가독성을 떨어뜨릴 수 있으므로, 필요한 경우에만 사용하는 것이 좋습니다.

 

끝으로, 제 C++ 역량 향상에 많은 도움을 줬던 책(한권을 뽑으라고 하면 뽑는 책) 소개하며 포스팅을 마치겠습니다 :)

 

http://www.yes24.com/Product/Goods/117986059

 

전문가를 위한 C++ - YES24

더 쉽게, 더 빠르게, 더 깔끔하게!프로그래머가 알아야 할 C++와 C++20의 모든 것C++는 마스터하기 어렵기로 악명 높지만, 풍부한 기능 덕분에 게임이나 상용 소프트웨어 애플리케이션을 개발하는

www.yes24.com

 

C++에서 C 파일을 컴파일 하려면, 해당 C 파일이 포함된 헤더 파일에 extern "C" 를 선언해주어야 합니다.
이렇게 하면 C++ 컴파일러는 해당 헤더 파일에 있는 함수 및 변수들을 C 스타일로 링크하게 됩니다.
만약 C 파일 자체를 컴파일 하기 위해서는, 해당 C 파일 내에도 extern "C" 를 선언해주어야 합니다.
이렇게 하면 C++ 컴파일러는 해당 C 파일을 C 스타일로 컴파일하게 됩니다.

 

예를 들어, 아래와 같은 C 파일과 헤더 파일이 있다고 가정해보겠습니다.

 

< sample.c 파일 >

 

#include "sample.h"

int add(int a, int b) {
    return a + b;
}

 

< sample.h 파일 >

 

#ifndef SAMPLE_H
#define SAMPLE_H

#ifdef __cplusplus
extern "C" {
#endif

int add(int a, int b);

#ifdef __cplusplus
}
#endif

#endif // SAMPLE_H

 

위의 예제에서 sample.h 파일에서 extern "C" 선언을 사용하여 add 함수가 C 스타일로 링크되도록 설정하였습니다.
이제 이 헤더 파일을 C++ 파일에서 include 하여 사용하면 됩니다.

 

#include "sample.h"

int main() {
    int result = add(1, 2);
    return 0;
}

 

sample.c 파일에서 extern "C"를 선언하지 않아도 되는 이유는, C 파일 자체가 C 스타일로 작성되어 있기 때문입니다.
C 파일은 C++과는 달리 *name mangling 을 하지 않으므로, extern "C" 선언이 필요하지 않습니다.
반면에, C++ 파일에서는 함수나 변수 이름 맹글링이 발생할 수 있으므로, C++ 파일에서 C 함수를 호출하거나 C 변수를 사용할 때는 extern "C" 선언을 해주어야 합니다.
따라서, C++에서 C 함수 또는 변수를 사용할 때는 해당 C 함수 또는 변수가 선언된 헤더 파일에 extern "C" 선언을 추가해주어야 합니다. 그러나 C 파일에서는 해당 선언을 할 필요가 없습니다.

 

 

name mangling 이란?

함수 및 클래스 이름과 같은 식별자(identifier)를 컴파일러가 처리하기 쉬운 내부 표현 형식으로 변환하는 과정을 말합니다.
이는 C++의 오버로딩(overloading) 및 네임스페이스(namespaces)와 같은 기능을 지원하기 위해 필요합니다.
C++ 컴파일러는 linker 가 사용하는 symbol 테이블을 생성하는데, 이 기호 테이블에는 함수와 클래스 이름 및 다른 식별자에 대한 정보가 포함됩니다. 이 정보를 사용하여 링커는 프로그램의 다른 부분에서 해당 함수 또는 클래스를 사용할 수 있습니다.
그러나 C++에서 함수 및 클래스 이름은 여러 매개변수를 가질 수 있습니다.

이를테면, add(int a, int b)와 add(double a, double b)는 동일한 이름의 함수일지라도 서로 다른 매개변수 유형을 가지므로 서로 다른 기호(symbol)로 링크되어야 합니다. 이때, 컴파일러는 이러한 함수 및 클래스 이름을 링커에서 사용할 수 있는 고유한 기호로 변환하기 위해 name mangling을 수행합니다.

즉, Name mangling은 컴파일러에 따라 다를 수 있지만, 일반적으로 함수 및 클래스 이름에 유일한 식별자를 추가하여 고유한 이름을 만듭니다. 이 식별자는 함수의 매개변수 유형 및 반환 유형과 같은 정보를 포함합니다.

예를 들어, add(int, int) 함수의 이름은 _Z3addii와 같은 형식으로 변환될 수 있습니다.

Name mangling은 일반적으로 개발자가 직접적으로 관여하지 않지만, 링커 오류를 해결하거나 디버깅하는 과정에서 체크해야 할 수도 있습니다.

std::fstream은 "파일"에 대한 입력 및 출력을 지원하는 클래스로 파일 스트림 클래스라고도 불립니다.

(std::ifstream와 std::ofstream의 기능을 모두 포함하고 있어 파일을 읽고 쓸 수 있습니다)

 

std::fstream은 <fstream> 헤더 파일에서 정의되며, std::fstream 클래스 내부에 구현된 함수를 사용하여 파일을 열고 닫습니다.

 

아래는 std::fstream 클래스의 주요 멤버 함수입니다.

open() : 파일을 엽니다.
close() : 파일을 닫습니다.
read() : 파일에서 데이터를 읽습니다.
write() : 파일에 데이터를 씁니다.
seekg() : 파일 내에서 읽기 위치를 이동합니다.
seekp() : 파일 내에서 쓰기 위치를 이동합니다.
tellg() : 읽기 위치를 반환합니다.
tellp() : 쓰기 위치를 반환합니다.
eof() : 파일 끝에 도달했는지 확인합니다.

 

먼저, 파일을 읽고 쓰는 간단한 코드를 한번 보도록 하겠습니다.

예제: 파일 읽고 쓰기

#include <iostream>
#include <fstream>

int main() {
    std::fstream fs;
    fs.open("example.txt", std::ios::in | std::ios::out | std::ios::binary);
    if (!fs) {
        std::cout << "파일 열기 실패" << std::endl;
        return 1;
    }
    // 파일 읽기
    char buffer[256];
    fs.read(buffer, 256);
    std::cout << buffer << std::endl;

    // 파일 쓰기
    fs.write("Hello World!", 12);
    fs.close();

    return 0;
}
먼저 fs라는 이름의 fstream 인스턴스를 활용하여 fstream 내부함수인 open 함수를 호출합니다,
파일이름은 example.txt 이고, 파일 처리 방식을 입출력이 모두 가능한 바이너리 형식으로 지정했습니다.
(여러 개의 열기 모드를 지정할 때는 비트 OR 연산자(|)를 사용)
아래는 open 함수의 주요 열기 모드입니다.
  ios_base::in : 파일을 읽기 위해 엽니다.
  ios_base::out : 파일을 쓰기 위해 엽니다.
  ios_base::app : 파일 끝에 추가하기 위해 엽니다.
  ios_base::binary : 이진 파일로 엽니다.
파일 읽기에 실패한 경우 읽기 실패에 대한 로깅을 하고 프로그램을 종료하고,
그렇지 않은 경우라면 read 함수로 파일 내용을 읽은 다음 buffer 라는 이름의 배열에 저정한 후 콘솔 출력합니다.
그리고 Hello World! 를 파일 끝에 추가로 쓰고 파일을 닫습니다.

 

예제 : 로그 파일 생성

로그 파일을 생성할 때는 이전 로그 내용을 보존하면서 새로운 로그 내용을 추가해야 하므로 std::fstream::out | std::fstream::app 모드로 파일을 열어, 새로운 로그 내용이 파일 끝에 추가되게끔 함으로써 이전 로그 내용을 계속 유지할 수 있습니다.

#include <iostream>
#include <fstream>

int main() {
    std::fstream log_file;
    log_file.open("log.txt", std::fstream::out | std::fstream::app);
    if (!log_file.is_open()) {
        std::cerr << "Failed to open log file." << std::endl;
        return -1;
    }
    log_file << "New log message." << std::endl;
    log_file.close();
    return 0;
}

 

위 예제에서는 log.txt 파일을 쓰기 모드로 열면서 파일 끝에 추가 쓰기 모드를 지정하여 로그 파일에 새로운 로그 메시지를 추가합니다. 만약 파일을 열지 못했을 경우, is_open() 함수를 사용하여 파일이 열렸는지 확인합니다.

 

fstream과 버퍼

 

std::fstream은 기능적 특성상 내부적으로 파일 입출력 작업을 위한 버퍼를 가지고 있습니다.

버퍼는 일시적으로 데이터를 저장하고, 저장된 데이터를 파일에 쓰거나 파일에서 읽어들일 때 사용됩니다.

 

std::fstream 클래스는 내부적으로 두 개의 버퍼를 가지고 있습니다.

하나는 입력 버퍼(input buffer)이고, 다른 하나는 출력 버퍼(output buffer)입니다.

입력 버퍼는 파일에서 읽어들인 데이터를 저장하는데 사용되며, 출력 버퍼는 파일에 쓰기 위한 데이터를 저장하는데 사용됩니다

 

std::fstream 클래스에서는 버퍼링을 사용하여 입출력 작업의 성능을 향상시킬 수 있습니다.

버퍼링을 사용하면 데이터를 파일에 쓰거나 파일에서 읽어들이는 작업을 수행할 때, 매번 파일에 직접 접근하지 않고 버퍼에 데이터를 저장하거나 버퍼에서 데이터를 읽어들입니다. 이렇게 함으로써 파일 입출력 작업이 더욱 효율적으로 수행될 수 있습니다.

 

std::fstream 클래스에서는 기본적으로 출력 버퍼링이 사용되며, 입력 버퍼링은 사용되지 않습니다.

즉, 출력 작업은 버퍼에 쌓인 데이터가 일정 크기 이상이 되거나, 파일을 닫을 때까지 버퍼에 저장되어 있게 됩니다.

반면에 입력 작업은 즉시 처리되며, 파일에서 데이터를 읽어들이면 입력 버퍼에 저장하지 않고 즉시 읽어들입니다.

 

버퍼링을 제어하기 위해서는 std::fstream 클래스의 멤버 함수인 flush()와 rdbuf()를 사용할 수 있습니다.

flush() 함수는 출력 버퍼에 저장된 데이터를 파일에 쓰는 작업을 수행하며, rdbuf() 함수는 입력 및 출력 버퍼에 대한 스트림 버퍼 포인터를 반환합니다. 이를 통해 버퍼를 직접 제어할 수 있습니다.

 

fstream 내부 버퍼와 운영체제의 버퍼

std::fstream 내부 버퍼와 운영체제(OS)의 버퍼는 서로 독립적으로 작동합니다.

std::fstream 내부 버퍼는 C++ 라이브러리에서 제공되는 버퍼링 기능으로, 입출력 연산의 성능을 향상시키기 위한 것인 반면,

운영체제의 버퍼는 실제 파일 시스템에 접근하여 입출력 연산을 수행하는 데 사용됩니다.

 

std::fstream 클래스의 객체를 생성하면, 내부적으로 파일과 연결된 운영체제의 파일 디스크립터를 생성하고, 파일 시스템에서 파일을 열

어야 합니다. 이 때, 운영체제는 해당 파일에 대한 버퍼를 생성하여 파일 시스템에서 데이터를 읽어들이거나 쓰기 작업을 수행합니다

 

std::fstream 클래스에서는 입출력 작업을 수행할 때, 먼저 C++ 내부 버퍼에 데이터를 저장하고, 일정량 이상의 데이터가 쌓이면 운영체제의 버퍼에 데이터를 쓰거나, 운영체제의 버퍼에서 데이터를 읽어들입니다. 이 때, C++ 내부 버퍼와 운영체제의 버퍼는 서로 독립적으로 작동하기 때문에, C++ 내부 버퍼에 데이터를 쓴 후에도, 바로 운영체제의 버퍼에 데이터가 쓰이는 것이 아니라, 내부 버퍼에 데이터가 쌓이게 됩니다. 이후에, 일정량 이상의 데이터가 쌓이면, C++ 라이브러리에서 버퍼의 내용을 운영체제의 버퍼로 옮겨주고, 운영체제의 입출력 함수를 호출하여 실제 파일 시스템에서 데이터를 읽거나 쓰게 됩니다

 

즉, std::fstream 내부 버퍼와 운영체제의 버퍼는 서로 다른 기능을 수행하면서도, 데이터를 주고받는 과정에서 서로 협력하여 작동합니다. 이러한 구조는 입출력 연산의 성능을 최적화하기 위한 것입니다.

 

fstream을 통해 파일로부터 데이터를 읽어들일 때 운영체제의 역할

 

std::fstream 클래스를 사용하여 파일로부터 데이터를 읽어들일 때, 운영체제(OS)는 다음과 같은 역할을 수행합니다

1. 파일 시스템에서 파일을 열기: std::fstream 객체가 생성되면, 운영체제는 파일 시스템에서 해당 파일을 열어야 합니다.
    파일이 존재하지 않거나, 권한이 없는 경우 등에는 파일 열기에 실패할 수 있습니다
2. 파일 읽기: 파일이 열리면, std::fstream 클래스의 멤버 함수를 이용하여 데이터를 읽어들일 수 있습니다.
    데이터를 읽어들일 때, 운영체제는 해당 파일의 내용을 버퍼에 읽어들입니다.
    버퍼는 운영체제의 내부 버퍼와 std::fstream 클래스의 내부 버퍼 두 가지가 있습니다.
3. 버퍼의 데이터를 프로그램으로 복사: 운영체제가 파일의 데이터를 버퍼에 읽어들이면, C++ 라이브러리는 이 데이터를 std::fstream      클래스의 내부 버퍼로 복사합니다. std::fstream 클래스는 내부적으로 버퍼를 사용하여 파일의 데이터를 저장합니다.
   이렇게 복사된 데이터는 프로그램에서 처리될 수 있는 형태가 되며, 읽어들인 데이터는 프로그램에서 이용됩니다
4. 파일 닫기: 데이터를 모두 읽어들인 후에는, 파일을 닫아야 합니다. 파일을 닫으면, 해당 파일에 대한 운영체제의 자원이 반환됩니다

위와 같은 과정을 통해, 운영체제는 파일 시스템에서 파일을 읽어들이고, std::fstream 클래스는 내부적으로 데이터를 버퍼에 저장합니다. 이러한 구조는 파일 입출력 작업을 효율적으로 수행하기 위한 것입니다.

 

 

seekg

 

std::fstream 클래스는 파일 입출력 작업을 수행하기 위한 다양한 멤버 함수를 제공합니다. seekg 함수는 그 중 하나로, 파일 내에서 위치를 이동하는데 사용됩니다.

seekg 함수는 파일 포인터의 위치를 이동시킵니다. 파일 포인터는 파일에서 데이터를 읽어들이거나 쓰기 위해 사용되는 위치 정보를 담고 있는 변수입니다. seekg 함수를 이용하면, 파일 포인터를 파일 내의 원하는 위치로 이동시킬 수 있습니다.

seekg 함수는 다음과 같은 구조를 가집니다

 

 

 

std::fstream& seekg (streampos pos);
std::fstream& seekg (streamoff off, ios_base::seekdir way);
  • 첫 번째 형식의 seekg 함수는, 파일 포인터를 지정된 위치로 이동시킵니다. pos 매개변수는 파일 내에서 이동시킬 위치를 나타내며, streampos 타입으로 지정됩니다.
  • 두 번째 형식의 seekg 함수는, 상대적인 위치와 이동 방향을 이용하여 파일 포인터를 이동시킵니다. off 매개변수는 현재 위치에서 이동할 바이트 수를 나타내며, way 매개변수는 이동 방향을 나타내는 ios_base::seekdir 열거형 값 중 하나를 지정합니다.

예를 들어, seekg(10)은 파일 포인터를 파일의 10번째 위치로 이동시킵니다. seekg(-5, std::ios_base::cur)은 현재 파일 포인터 위치에서 5바이트 앞으로 이동시킵니다.

seekg 함수는 std::ios::cur (현재 위치), std::ios::beg (파일 시작 위치), std::ios::end (파일 끝 위치)와 같은 ios::seekdir 값으로 이동 방향을 지정할 수 있습니다. 이러한 방법을 이용하면 파일 포인터를 특정 위치가 아닌, 파일의 시작이나 끝 부분으로 이동시킬 수 있습니다.

 

아래는 seekg 함수의 사용 예제입니다. 예제에서는 파일에서 10바이트를 읽어들이기 위해, 파일 포인터를 파일의 10번째 위치로 이동시킨 후 데이터를 읽어들이는 방법을 보여줍니다

 

#include <fstream>
#include <iostream>

int main() {
    std::ifstream infile("data.txt", std::ios::in | std::ios::binary);

    if (!infile) {
        std::cerr << "Failed to open file." << std::endl;
        return 1;
    }

    // 파일 포인터를 10번째 위치로 이동시킴
    infile.seekg(10, std::ios::beg);

    char data[11] = {0};
    infile.read(data, 10);

    std::cout << "Data read from file: " << data << std::endl;

    infile.close();

    return 0;
}
위 예제에서, seekg 함수를 이용하여 파일 포인터를 10번째 위치로 이동시킨 후 read 함수를 이용하여 10바이트를 읽어들입니다. 이렇게 파일 포인터를 원하는 위치로 이동시켜 파일 내의 원하는 위치에서 데이터를 읽어들일 수 있습니다.

 

 

seekg(0, std::ios::end)는 파일 포인터를 파일의 끝 부분으로 이동시키는 역할을 합니다.

두 번째 인자인 std::ios::end는 이동 방향을 나타내며, 파일의 끝에서부터 이동하라는 의미입니다. 첫 번째 인자인 0은 파일 끝으로부터 0바이트 떨어진 위치로 이동하라는 의미이므로, 실제로는 파일의 끝 부분으로 파일 포인터가 이동됩니다.

이렇게 파일 포인터를 파일 끝 부분으로 이동시키면, 파일의 크기를 계산하거나 파일에 새로운 데이터를 추가할 때 유용합니다. 예를 들어, 파일의 크기를 계산하기 위해서는 파일 포인터를 끝 부분으로 이동시켜 파일의 위치를 알아내고, 그 값을 파일의 크기로 사용할 수 있습니다. 또한, 파일 끝에 새로운 데이터를 추가하기 위해서도 파일 포인터를 끝 부분으로 이동시켜야 합니다.

 

src.seekg(beg, std::ios::beg)는 파일 포인터를 파일의 시작 부분으로부터 beg 바이트 떨어진 위치로 이동시키는 역할을 합니다.

첫 번째 인자인 beg는 파일 포인터를 이동할 바이트 수를 나타내며, 두 번째 인자인 std::ios::beg는 이동 방향을 나타냅니다. std::ios::beg는 파일의 시작부터 이동하라는 의미입니다.

예를 들어, src.seekg(10, std::ios::beg)은 파일의 시작 부분으로부터 10바이트 떨어진 위치로 파일 포인터를 이동시킵니다. 이렇게 파일 포인터를 원하는 위치로 이동시켜 파일 내의 원하는 위치에서 데이터를 읽어들일 수 있습니다.

 

std::fstream src(frag, std::fstream::in); 
src.seekg(0, std::ios::end); 
src.seekg(beg, std::ios::beg);
위 코드는 파일을 열어서 파일 포인터를 파일의 끝으로 이동시킨 후, 다시 파일의 시작 부분으로부터 beg바이트 떨어진 위치로 파일 포인터를 이동시키는 역할을 합니다.
첫 번째 줄에서 std::fstream::in 모드로 파일을 열어서 src 객체를 생성합니다. 이때, 파일을 읽기 모드로 열기 때문에 파일 내용을 수정할 수 없고, 읽기만 가능합니다.
두 번째 줄에서 src.seekg(0, std::ios::end)를 호출하여 파일 포인터를 파일의 끝으로 이동시킵니다. 이렇게 파일 포인터를 파일의 끝으로 이동시키면, 파일의 크기를 계산하거나 파일에 새로운 데이터를 추가할 때 유용합니다.
세 번째 줄에서 src.seekg(beg, std::ios::beg)를 호출하여 파일 포인터를 파일의 시작 부분으로부터 beg바이트 떨어진 위치로 이동시킵니다. 이렇게 파일 포인터를 원하는 위치로 이동시켜서 파일 내의 원하는 위치에서 데이터를 읽어들일 수 있습니다.
즉, 이 코드는 파일에서 데이터를 읽어들이기 위해서 파일 포인터를 파일의 시작 부분으로부터 beg바이트 떨어진 위치로 이동시킨 후, 파일의 끝 부분에서부터 파일 포인터를 이동시켜 파일의 크기를 계산하는 방법을 사용합니다

버퍼(Buffer)

입력 버퍼는 사용자가 키보드나 마우스를 통해 입력한 데이터를 임시적으로 저장하는 메모리 공간입니다.

일반적으로 입력 버퍼는 FIFO(First-In-First-Out) 방식으로 동작합니다.

사용자가 키를 누르면 해당 키의 입력 데이터가 입력 버퍼의 맨 뒤에 추가됩니다.

그리고 프로그램에서 입력 함수를 호출할 때마다, 입력 버퍼의 가장 앞에 있는 데이터가 제거되며, 이 데이터를 함수가 반환합니다.

 

입력 버퍼는 보통 운영체제에서 관리되며, 이를 통해 여러 개의 프로그램이 동시에 입력을 처리할 수 있습니다. 입력 버퍼는 입력 함수를 호출하기 전까지는 입력 데이터를 저장하기 위한 임시적인 저장소일 뿐입니다. 따라서 입력 함수를 호출하면 입력 버퍼에서 데이터가 제거되므로, 같은 데이터를 두 번 이상 읽을 수 없습니다.

 

예를 들어, 사용자가 "Hello"라는 문자열을 입력하고, 프로그램에서 입력 함수를 두 번 호출하는 경우를 생각해보겠습니다

 

#include <iostream>
using namespace std;

int main() {
    char ch1, ch2, ch3, ch4, ch5;
    ch1 = getchar();
    ch2 = getchar();
    ch3 = getchar();
    ch4 = getchar();
    ch5 = getchar();

    cout << "입력한 문자: " << ch1 << ch2 << ch3 << ch4 << ch5 << endl;
    return 0;
}
위 예제 코드에서 사용자가 "Hello"를 입력하면, 입력 버퍼에는 "H", "e", "l", "l", "o", '\n'이 저장됩니다.
이후 getchar() 함수를 다섯 번 호출하면, 입력 버퍼에서 맨 앞에 있는 데이터가 제거되며, 이를 함수가 반환합니다.
따라서, 위 예제 코드에서 출력되는 문자열은 "Hello\n"이 됩니다.
즉, 입력 버퍼는 프로그램이 입력 데이터를 효율적으로 처리할 수 있도록 하는 운영체제의 기능으로,
입력 함수를 호출할 때마다 데이터를 반환하고, 버퍼에서 제거합니다.

 

 

입력 버퍼는 운영체제에서 제공하는 메모리 공간 중 하나로, 일반적으로 커널 내부의 메모리 영역에 위치합니다.

(따라서 프로그램에서는 입력 버퍼에 직접적으로 접근할 수 없습니다)

운영체제는 표준 입력 스트림에서 데이터를 읽어들이고, 이를 입력 버퍼에 저장합니다.

이후 입력 함수가 호출되면, 입력 버퍼에서 데이터를 읽어들이고, 함수가 반환됩니다.

 

입력 버퍼는 보통 운영체제에서 관리되며, 프로그램에서는 이를 직접적으로 조작할 수 없습니다.

대신, 프로그램은 입력 함수를 호출하여 입력 버퍼에서 데이터를 읽어들입니다.

입력 버퍼의 크기는 운영체제나 터미널의 설정에 따라 다르며, 이를 조절할 수 있는 경우도 있습니다.

일반적으로 입력 버퍼의 크기는 몇 바이트에서 몇 킬로바이트까지 가능합니다

입력 버퍼의 크기는 일반적으로 정적으로 결정됩니다.
즉, 프로그램이 실행될 때 버퍼의 크기가 결정되고, 이후에는 변경되지 않습니다.
이는 대부분의 운영체제에서 버퍼의 크기가 고정되어 있기 때문입니다.
버퍼의 크기는 운영체제나 터미널의 설정에 따라 다르며, 보통 수십바이트에서 수천바이트까지 가능합니다.
이 값은 보통 프로그램에서 변경할 수 없으며, 운영체제나 터미널 설정을 변경하여 버퍼의 크기를 조절할 수 있습니다.
또한, 동적으로 버퍼의 크기를 조절하는 방법도 있습니다.
이 경우에는 프로그램에서 직접적으로 버퍼를 관리하여 크기를 조절할 수 있으며, 이를 위해서는 동적 메모리 할당 함수인 malloc, realloc, free 등을 이용하여 메모리를 할당하고 해제해야 합니다.
그러나 이 방법은 일반적으로 복잡하고 오버헤드가 크므로, 버퍼 크기를 미리 결정하는 것이 일반적입니다

 

버퍼와 사용자 메모리

 

입력 함수가 호출되면, 입력 버퍼에 저장된 데이터를 가져와서 스택이나 힙 영역에 저장할 수 있습니다.

이 때, 입력 함수가 반환하는 값은 입력 버퍼에서 가져온 데이터가 저장된 위치를 나타내는 포인터입니다.

프로그래머는 이 포인터를 이용하여 가져온 데이터를 변수나 배열에 저장할 수 있습니다

 

예를 들어, C++에서 std::cin을 이용하여 입력을 받는 경우, 입력 버퍼에 저장된 데이터를 가져와서 std::string이나 char 배열 등의 변수에 저장할 수 있습니다. 이 때, std::cin 함수는 입력 버퍼에서 가져온 데이터가 저장된 위치를 나타내는 포인터를 반환하며, 이를 이용하여 변수에 데이터를 저장합니다

 

표준 입력 스트림(standard input stream)
표준 입력 스트림은 키보드나 파일 등의 입력 소스에서 데이터를 읽어오는 C++ 표준 라이브러리의 입력 스트림 객체입니다.
표준 입력 스트림은 std::cin 객체를 통해 사용할 수 있으며, >> 연산자를 이용하여 데이터를 읽어올 수 있습니다.
예를 들어, int형 변수에 값을 입력받는 코드는 다음과 같습니다.

int num;
std::cin >> num;

표준 입력 스트림은 프로그램에서 입력을 받는 가장 일반적인 방법입니다.
키보드로부터 데이터를 입력받는 경우에는 표준 입력 스트림이 사용되며,
파일로부터 데이터를 입력받는 경우에도 표준 입력 스트림을 이용하여 파일의 내용을 읽어올 수 있습니다.

 

std::cin

 

std::cin은 C++ 표준 라이브러리에서 제공하는 입력 스트림 객체 중 하나로, 표준 입력(standard input)으로부터 데이터를 읽어옵니다. std::cin은 iostream 헤더 파일에 정의되어 있으며,아래와 같이 동작합니다.

 

1. 키보드나 파일 등의 입력 소스에서 데이터를 읽어옵니다. 이때, 키보드로부터 데이터를 입력받는 경우에는 사용자가 Enter 키를 입력할 때까지 입력을 받습니다

2. 입력된 데이터를 입력 버퍼(input buffer)에 저장합니다

3. 프로그램에서 std::cin 객체를 이용하여 데이터를 읽어오는 경우, 입력 버퍼에서 데이터를 읽어옵니다

4. 읽어온 데이터를 변수에 저장합니다. 이때, >> 연산자를 이용하여 데이터를 읽어옵니다.

     >> 연산자는 입력 버퍼에서 공백(space), 탭(tab), 개행(newline) 등을 구분자로 사용하여 데이터를 분리합니다

 

 

std::cin은 입력 버퍼에 데이터가 있으면 즉시 데이터를 읽어옵니다.

따라서, std::cin을 이용하여 입력을 받을 때에는 입력 버퍼가 비어있는지 확인하는 것이 중요합니다.

입력 버퍼가 비어있지 않은 상태에서 std::cin을 이용하여 데이터를 읽어오면, 입력 버퍼에 남아있는 데이터를 읽어올 수 있습니다.

이를 방지하기 위해서는 std::cin을 이용하여 데이터를 읽어오기 전에 입력 버퍼를 비워주는 작업이 필요합니다

 

예를 들어, 다음과 같은 코드는 사용자로부터 정수를 입력받고, 입력된 값을 출력합니다.

 

#include <iostream>

int main() {
    int num;
    std::cin >> num;
    std::cout << "입력된 값: " << num << std::endl;
    return 0;
}

 

이 코드는 std::cin을 이용하여 사용자로부터 정수를 입력받은 후, 입력된 값을 출력합니다.

>> 연산자를 이용하여 데이터를 읽어올 때, 입력 버퍼에 데이터가 없는 경우에는 사용자의 입력을 기다리며,

입력 버퍼에 데이터가 있는 경우에는 바로 데이터를 읽어옵니다

 

getch

 

C++에서 콘솔 입력을 받을 때 버퍼를 이용하지 않고 바로 입력을 받는 방법으로는 "getch()" 함수를 사용하는 방법이 있습니다.

 

getch()" 함수는 conio.h 헤더 파일에 정의되어 있으며, 키보드 입력을 즉시 받아서 처리합니다. 이 함수는 키 입력을 받으면 바로 반환하므로 버퍼를 사용하지 않습니다

 

다음은 "getch()" 함수를 사용하여 콘솔에서 한 글자를 입력받는 예제 코드입니다.

 

#include <iostream>
#include <conio.h>

using namespace std;

int main() {
    char ch = getch();
    cout << "입력한 문자: " << ch << endl;
    return 0;
}

 

위 예제 코드에서 getch() 함수는 사용자로부터 한 글자를 입력받고, 그 결과를 변수 ch에 저장합니다.
그리고 저장된 값을 출력합니다. 이렇게 하면 입력 버퍼를 사용하지 않고 즉시 입력을 받을 수 있습니다.

 

getchar

 

"getchar()" 함수는 stdio.h 헤더 파일에 정의되어 있으며, 표준 입력 스트림에서 한 글자를 읽어서 처리합니다. 이 함수는 버퍼를 사용하지 않으므로 입력을 받을 때마다 바로 처리할 수 있습니다.

 

다음은 "getchar()" 함수를 사용하여 콘솔에서 한 글자를 입력받는 예제 코드입니다.

 

#include <iostream>
#include <cstdio>

using namespace std;

int main() {
    char ch = getchar();
    cout << "입력한 문자: " << ch << endl;
    return 0;
}

 

위 예제 코드에서 getchar() 함수는 표준 입력 스트림에서 한 글자를 읽어서 변수 ch에 저장합니다.
그리고 저장된 값을 출력합니다. 이렇게 하면 입력 버퍼를 사용하지 않고 즉시 입력을 받을 수 있습니다

 

C++에서 콘솔 입력을 받을 때 버퍼를 사용하지 않는 또 다른 방법으로는 "getchar_unlocked()" 함수를 사용하는 방법이 있습니다

"getchar_unlocked()" 함수는 getchar() 함수와 비슷하지만, 내부적으로 동기화를 수행하지 않아서 더 빠르게 입력을 받을 수 있습니다. 이 함수는 일반적으로 멀티스레드 환경에서 사용되지 않으며, 동기화가 필요하지 않은 단일 스레드 환경에서 사용됩니다

다음은 "getchar_unlocked()" 함수를 사용하여 콘솔에서 한 글자를 입력받는 예제 코드입니다.

 

#include <iostream>
#include <cstdio>

using namespace std;

inline char read_char() {
    char c = getchar_unlocked();
    while (c == ' ' || c == '\n' || c == '\r') {
        c = getchar_unlocked();
    }
    return c;
}

int main() {
    char ch = read_char();
    cout << "입력한 문자: " << ch << endl;
    return 0;
}

 

위 예제 코드에서 read_char() 함수는 getchar_unlocked() 함수를 사용하여 한 글자를 읽어서 처리합니다. 이 함수는 입력 버퍼를 사용하지 않으므로 더 빠르게 입력을 처리할 수 있습니다. 함수 내부에서는 입력 받은 값이 공백 문자(' '), 개행 문자('\n'), 혹은 캐리지 리턴('\r')인 경우에는 다시 입력을 받도록 처리합니다.

 

getch()와 getchar()  차이

 

getch()와 getchar() 함수는 모두 콘솔에서 입력을 받는 함수입니다. 그러나 두 함수에는 몇 가지 차이가 있습니다.

 

1. 헤더파일

 

getch() 함수는 conio.h 헤더 파일에 정의되어 있고, getchar() 함수는 stdio.h 헤더 파일에 정의되어 있습니다

 

2. 입력 대기 방식

 

getch() 함수는 사용자가 키를 누르는 즉시 바로 입력을 받아들입니다. 이에 반해 getchar() 함수는 사용자가 입력을 마치고 Enter 키를 누르면 입력을 받아들입니다

 

3. 입력 처리 방식

 

getch() 함수는 입력 받은 키 값을 즉시 반환합니다. 이에 반해 getchar() 함수는 표준 입력 스트림에서 한 글자를 읽어서 반환합니다

 

4. 입력 처리 대상

 

getch() 함수는 콘솔에서 키 입력만을 받아들입니다.

이에 반해 getchar() 함수는 콘솔에서 뿐만 아니라 파일 등에서도 입력을 받아들일 수 있습니다

따라서, getch() 함수는 사용자가 키 입력을 바로 반영하여 빠른 입력 처리가 필요한 경우에 사용되며, getchar() 함수는 문자열 입력을 받아야 하는 경우나, 파일에서 입력을 받아들여야 하는 경우에 사용됩니다

getchar() 함수는 표준 입력 스트림에서 한 글자씩 읽어들입니다. 때문에 두 글자 이상을 입력하고 Enter 키를 누르면 첫 번째 글자만 읽어들이고, 두 번째 글자는 입력 버퍼에 남게 됩니다. 그리고 Enter 키는 새로운 줄 문자('\n')으로 인식되어 입력 버퍼에 추가됩니다.

이후 getchar() 함수를 다시 호출하면 입력 버퍼에 있는 다음 글자를 읽어들이게 됩니다. 만약 입력 버퍼에 다음 글자가 없다면, getchar() 함수는 입력 대기 상태가 됩니다

따라서, getchar() 함수를 두 번 호출하면 두 글자 이상의 입력을 받을 수 있습니다. 다음은 이에 대한 예제 코드입니다

 

#include <iostream>
#include <cstdio>

using namespace std;

int main() {
    char ch1 = getchar();
    char ch2 = getchar();
    cout << "입력한 문자: " << ch1 << ch2 << endl;
    return 0;
}
위 예제 코드에서 첫 번째 getchar() 함수는 입력 버퍼에서 한 글자를 읽어들이고, 두 번째 getchar() 함수는 다음 글자를 읽어들입니다. 그리고 두 글자를 모두 출력합니다.

 

const char* :  C-style 문자열을 나타내는 포인터

여기서 C-styel 문자열이란, 말그대로 C 언어에서 사용하는 문자열 표현 방식으로, Null 종료 문자열(Null-terminated string)을 사용하여 문자열을 표현합니다.

Null 종료 문자열은 문자열의 끝에 항상 널 문자('\0')가 포함된 문자열을 의미합니다.

예를 들어, "Hello"라는 문자열은 다음과 같이 C-style 문자열로 표현할 수 있습니다.

 

char str[] = {'H', 'e', 'l', 'l', 'o', '\0'};

 

 

이렇게 문자열을 표현할 경우 문자열의 끝을 알기 위해 널 문자('\0')을 사용합니다.
이를 이용해 문자열의 길이를 계산하거나, 문자열을 다루는 다양한 함수를 사용할 수 있습니다.

다음은 C-style 문자열에서 자주 사용되는 함수들입니다 (c++에서 사용할 경우, <cstring> 헤더 include 필요)

 

1.  strlen() : 문자열 길이 계산

 

const char* str = "Hello, world!";
int length = strlen(str); // 13

 *공백, 특수문자(, !) 등도 포함. null 은 미포함.

2. strcpy() : 문자열 복사

 

char src[] = "Hello";
char dest[10];
strcpy(dest, src);
// dest는 "Hello"로 설정됩니다.

 

3.  strcat() : 문자열 연결

 

char str1[20] = "Hello";
char str2[20] = "World";
strcat(str1, str2);
// str1은 "HelloWorld"로 설정됩니다.

 

4. strcmp() : 두 문자열 비교

 

char str1[] = "Hello";
char str2[] = "World";
int result = strcmp(str1, str2); // 음수 값이 반환됩니다.

 

5. strchr() : 문자열에서 특정 문자 검색

 

const char* str = "Hello, world!";
char ch = 'o';
const char* ptr = strchr(str, ch); // "o, world!"

 

6. strstr() : 특정 문자열 검색

 

const char* str = "Hello, world!";
const char* sub_str = "world";
const char* ptr = strstr(str, sub_str); // "world!"

 

C-style 문자열은 char 배열 형태로 표현됩니다.

 

#include <stdio.h>
#include <string.h>

int main() {
    char str1[20] = "Hello";
    char str2[] = ", world!";
    strcat(str1, str2);
    printf("%s\n", str1);

    char str3[20];
    strncpy(str3, str1 + 7, 5);
    printf("%s\n", str3);

    int result = strcmp(str1, str2);
    printf("%d\n", result);

    char* ptr = strstr(str1, "world");
    if (ptr != NULL) {
        strncpy(ptr, "Earth", 5);
    }
    printf("%s\n", str1);

    return 0;
}

 

위 예제에서는 strcat(), strncpy(), strcmp(), strstr() 등의 함수를 사용하여 C-style 문자열을 배열로 표현하고 조작합니다.
strcat(str1, str2) : str1 끝에 str2를 이어 붙입니다.
strncpy(str3, str1 + 7, 5) : str1의 8번째 문자부터 5개의 문자를 str3에 복사합니다.
strcmp(str1, str2) : str1과 str2를 비교합니다.
strstr(str1, "world") : str1에서 "world" 문자열을 찾습니다.
strncpy(ptr, "Earth", 5) : ptr에서 시작하는 위치에 "Earth" 문자열의 5개 문자를 복사합니다.
그러면 str1에는 "Hello, world!"가 저장되어 있을 것입니다.
strcat() 함수를 사용하여 ", world!" 문자열을 이어 붙여 "Hello, world!"를 완성합니다.
그 다음 strncpy() 함수를 사용하여 str1에서 8번째 문자부터 5개의 문자를 str3에 복사하고,
strcmp() 함수를 사용하여 str1과 str2를 비교합니다.
마지막으로 strstr() 함수를 사용하여 str1에서 "world" 문자열을 찾고, strncpy() 함수를 사용하여 "Earth" 문자열로 대체합니다.

 

출력결과

 

Hello, world!
world
72
Hello, Earth!

 

이 때, 문자열의 크기를 지정하지 않아도 되며, 필요에 따라 문자열의 길이를 동적으로 할당할 수 있습니다.

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char* str = NULL;
    size_t len = 0;
    ssize_t read;

    printf("Enter a string: ");
    read = getline(&str, &len, stdin);

    if (read == -1) {
        printf("Error reading input\n");
        return 1;
    }

    printf("Input string: %s\n", str);

    char* str2 = (char*)malloc(strlen(str) + 1);
    strcpy(str2, str);
    printf("Copied string: %s\n", str2);

    free(str);
    free(str2);

    return 0;
}
위 예제에서는 getline() 함수를 사용하여 키보드로부터 입력을 받습니다.
입력된 문자열의 길이에 따라 필요한 만큼 메모리를 동적으로 할당합니다.
그 다음, strcpy() 함수를 사용하여 할당된 메모리에 문자열을 복사합니다.
getline() 함수는 입력된 문자열의 길이와 할당된 메모리의 크기를 반환합니다.
malloc() 함수를 사용하여 문자열의 길이에 따라 필요한 만큼 메모리를 동적으로 할당합니다.
할당된 메모리에 strcpy() 함수를 사용하여 문자열을 복사합니다.
마지막으로 free() 함수를 사용하여 할당된 메모리를 해제합니다.

출력결과

 

Enter a string: Hello, world!
Input string: Hello, world!
Copied string: Hello, world!

 

C-style 문자열은 C++에서도 사용할 수 있습니다

(다만, C++에서는 문자열을 다루기 위해 string 클래스를 사용하는 것이 더 안전하고 편리합니다)

 

*(참고) getline

getline() 함수는 C++의 표준 라이브러리인 <iostream> 헤더 파일에 선언된 함수로,

파일 스트림 또는 입력 스트림에서 한 줄씩 문자열을 읽어올 때 사용됩니다.

getline() 함수는 기본적으로 세 개의 매개변수를 받습니다.

 

getline(std::istream& input_stream, std::string& str, char delimiter='\n');

 

  • input_stream: 입력 스트림 또는 파일 스트림 객체입니다. std::cin, std::ifstream 등이 될 수 있습니다.
  • str: 읽어온 문자열이 저장될 std::string 객체입니다.
  • delimiter: 읽어올 문자열의 끝을 표시하는 구분자입니다. 기본값은 개행 문자('\n')입니다.

getline() 함수는 입력 스트림에서 delimiter 문자를 만나거나 스트림의 끝에 도달할 때까지 문자열을 읽어 str 객체에 저장합니다.
이 때, delimiter 문자는 입력 스트림에서 제거되지 않고 버퍼에 남게 됩니다.

getline() 함수를 사용하면 키보드나 파일 등에서 한 줄씩 문자열을 읽어와 처리할 수 있습니다. getline() 함수를 이용하면, 사용자가 입력한 문자열을 std::string 객체에 저장할 수 있으며, 입력 스트림을 더 이상 읽지 않을 때까지 문자열을 계속해서 읽어올 수 있습니다.

 



 string :  문자열을 다루기 위해 사용하는 c++  표준 라이브러리 클래스

 

string은 문자열을 동적으로 할당하여 저장하고, 문자열의 길이 정보도 함께 저장합니다.

문자열을 수정하거나 추가하는 작업이 자유롭기 때문에 보다 편리하게 문자열을 다룰 수 있습니다.

또한, string 클래스는 C++ 표준 라이브러리의 일부로 제공되기 때문에 라이브러리에서 제공하는 다양한 기능을 사용할 수 있습니다.

 

std::string 클래스의 내부 구현은 크게 두 가지로 나눌 수 있습니다.

첫 번째는 문자열 데이터를 저장하기 위한 메모리 관리를 위한 부분이며,

두 번째는 문자열을 조작하기 위한 멤버 함수들을 구현하는 부분입니다.

 

첫 번째 부분에서는 문자열 데이터를 저장하기 위한 메모리를 할당하고, 필요에 따라 재할당합니다.

문자열 데이터는 동적으로 할당된 메모리 공간에 저장됩니다.

std::string 클래스는 문자열 데이터의 길이와 할당된 메모리 공간의 크기를 구별하며, 이를 통해 문자열 데이터를 조작할 때 메모리를 효율적으로 사용합니다.

두 번째 부분에서는 문자열을 조작하기 위한 멤버 함수들이 구현됩니다.

이러한 멤버 함수들은 문자열 데이터에 직접 접근하지 않고, 문자열 데이터에 대한 포인터와 길이 정보를 사용합니다.

이를 통해 문자열을 조작하는 과정에서 문자열 데이터의 내부 구조가 변경되더라도 멤버 함수들이 제대로 동작할 수 있습니다.

 

#include <iostream>
#include <string>

int main() {
    std::string str = "Hello, world!";

    // 문자열 데이터에 접근하는 방법
    const char* data = str.data(); // C-style 문자열로 변환된 데이터 포인터 반환
    std::cout << "Data: " << data << std::endl;

    // 문자열 데이터의 길이를 구하는 방법
    size_t length = str.length();
    std::cout << "Length: " << length << std::endl;

    // 문자열 데이터를 조작하는 방법
    str += " Welcome to the world!";
    std::cout << "Modified string: " << str << std::endl;

    return 0;
}
이 예제 코드에서는 std::string 클래스의 객체를 생성하고, 이를 통해 문자열 데이터를 다루는 방법을 보여줍니다.
먼저, 문자열 데이터에 접근하는 방법은 data라는 std::strring 클래스 멤버함수를 통해 할 수 있습니다.
data() 멤버 함수를 통해 내부 문자열 데이터의 C-style 문자열 형태로 변환된 데이터 포인터를 반환합니다.
이 포인터를 이용하면 std::string 클래스가 제공하지 않는 C-style 문자열 함수를 사용할 수 있습니다.

문자열 데이터의 길이를 구하는 방법 또한 std::string 클래스 멤버함수인 length() 통해 구할 수 있습니다.

 

단, 이 함수는 문자열 데이터가 저장된 메모리의 크기와는 다릅니다.

std::string 클래스는 문자열 데이터의 길이와 할당된 메모리 공간의 크기를 구별하여 문자열을 효율적으로 처리합니다.

std::string 클래스의 멤버 함수인 length()는 문자열의 길이를 반환합니다.
이 길이는 문자열 데이터에 포함된 실제 문자 개수를 의미합니다.
하지만 문자열 데이터가 메모리에 저장되는 방식 때문에, 문자열 길이와 실제 메모리 크기는 일치하지 않을 수 있습니다.
std::string 클래스는 동적으로 문자열 데이터를 할당하고 관리합니다.
이 때, 문자열 데이터는 null 종료 문자('\0')로 끝나야 합니다. 따라서 문자열 데이터를 저장하는 메모리 공간의 크기는 문자열 길이보다 1 큽니다. 예를 들어, "hello" 문자열의 경우 길이는 5이지만, 이 문자열을 저장하는 메모리 공간의 크기는 6입니다.
또한, std::string 클래스는 문자열 데이터를 저장하기 위해 동적으로 메모리를 할당합니다.
이 때, 할당되는 메모리의 크기는 문자열 데이터의 길이와는 관련이 없을 수 있습니다.
예를 들어, "hello" 문자열을 저장하기 위해 할당된 메모리 공간이 8바이트인 경우가 있습니다.
이는 메모리 할당의 내부 알고리즘에 따라 달라질 수 있습니다.
따라서, std::string 클래스의 length() 멤버 함수로 얻는 문자열 길이와 실제 문자열 데이터가 저장된 메모리 크기는 일치하지 않을 수 있습니다.
하지만, std::string 클래스는 문자열 데이터의 길이와 메모리 크기를 투명하게 관리하기 때문에, 사용자는 이러한 내부 구현 상세를 신경쓰지 않고 문자열을 처리할 수 있습니다

 

다양한 멤버 함수를 제공하여 문자열 데이터를 조작할 수 있는데, 이 예제에서는 += 연산자를 사용하여 문자열 데이터를 뒤에 추가하는 것을 구현했습니다. 물론 이 외에도 append(), replace(), insert() 등 다양한 멤버 함수를 사용할 수 있습니다.

 

이러한 방식으로 std::string 클래스는 문자열 데이터를 동적으로 할당하고, 문자열을 조작하는 다양한 멤버 함수들을 제공합니다.

내부적으로는 문자열 데이터를 저장하기 위한 메모리 관리와 문자열 조작 함수들의 구현 등이 이루어지며, 이를 통해 문자열 데이터를 효율적으로 처리할 수 있습니다.

 

아래는 string 멤버함수를 활용하여 문자열 데이터를 처리하는 예제입니다.

(문자열을 분리하여 벡터에 저장하고, 저장된 문자열을 역순으로 출력하는 기능 구현)

#include <iostream>
#include <string>
#include <vector>

int main() {
    std::string str = "apple,banana,cherry,date,elderberry";

    // 문자열을 구분자로 분리하여 벡터에 저장하는 방법
    std::vector<std::string> vec;
    size_t start = 0, end = 0;
    while ((end = str.find(',', start)) != std::string::npos) {
        vec.push_back(str.substr(start, end - start));
        start = end + 1;
    }
    vec.push_back(str.substr(start));

    // 저장된 문자열을 역순으로 출력하는 방법
    for (auto iter = vec.rbegin(); iter != vec.rend(); ++iter) {
        std::cout << *iter << std::endl;
    }

    return 0;
}
std::string 클래스의 멤버 함수인 find()과 substr()을 활용하여 문자열을 분리하고, 이를 std::vector 컨테이너에 저장합니다.
이후 저장된 문자열을 역순으로 출력합니다.
1) 문자열을 구분자(' , ')로 분리하여 벡터에 저장
문자열에서 , 문자가 있는 위치를 찾아내는 방법은 find() 멤버 함수를 활용합니다.
이 함수는 문자열에서 주어진 문자 또는 문자열을 찾아, 해당 위치를 반환합니다.
find() 함수의 두 번째 인자는 검색을 시작할 위치를 지정하는 인자입니다.
따라서 start 변수에 이전에 찾은 , 문자의 위치를 저장하고, 이를 시작 위치로 지정하여 , 문자를 찾아냅니다.
이렇게 찾은 문자열은 substr() 멤버 함수를 활용하여 추출합니다.
이 함수는 문자열에서 주어진 위치부터 지정한 길이만큼의 문자열을 반환합니다.
2) 저장된 문자열을 역순으로 출력하는 방법
이 예제에서는 std::vector 컨테이너의 멤버 함수인 rbegin()과 rend()를 활용하여 반복자(iterator)를 역순으로 순회합니다.
이렇게 역순으로 순회하면서 저장된 문자열을 출력합니다.
이러한 방식으로 std::string 클래스의 멤버 함수와 std::vector 컨테이너를 활용하여 문자열을 분리하고, 저장된 문자열을 처리하는 다양한 기능을 구현할 수 있습니다.

 

std::string 클래스의 내부 구현에서는 또한 문자열 데이터의 내부 인코딩에 대한 처리도 이루어집니다.

std::string 클래스는 기본적으로 ASCII나 UTF-8 인코딩을 사용하지만, 다른 인코딩을 사용하는 경우에도 멤버 함수를 통해 문자열 데이터를 적절하게 변환할 수 있습니다.

즉, std::string 클래스는 문자열 데이터를 동적으로 할당하고, 문자열을 조작하는 다양한 멤버 함수들을 제공하는 라이브러리 클래스입니다. 내부적으로는 문자열 데이터를 저장하기 위한 메모리 관리와 문자열 조작 함수들의 구현 등이 이루어지며, 이를 통해 문자열 데이터를 효율적으로 처리할 수 있습니다.

1. 순환 참조 문제 

 

(예제1)

#include <memory>
#include <iostream>

struct Node {
    std::shared_ptr<Node> next;
    int value;
    Node(int val): value(val) {
        std::cout << "Node " << value << " created." << std::endl;
    }
    ~Node() {
        std::cout << "Node " << value << " destroyed." << std::endl;
    }
};

int main() {
    std::shared_ptr<Node> head = std::make_shared<Node>(1);
    std::shared_ptr<Node> second = std::make_shared<Node>(2);
    std::shared_ptr<Node> third = std::make_shared<Node>(3);
    
    head->next = second;
    second->next = third;
    third->next = head; // 순환 참조
    
    return 0;
}

위 예제에서 Node 구조체는 단순히 value와 다음 노드를 가리키는 next 멤버 변수를 가지고 있습니다.

main 함수에서는 head, second, third 세 개의 노드를 생성하고 next 포인터로 연결합니다.

그런데 third 노드에서 head 노드를 가리키는 것을 볼 수 있습니다.

이로 인해 head가 참조하는 객체의 reference count가 2가 되어 메모리에서 해제되지 않고 유지됩니다.

프로그램을 실행해보면 다음과 같은 출력을 확인할 수 있습니다.

Node 1 created.
Node 2 created.
Node 3 created.
Node 1 destroyed.

 

(예제2)

#include <memory>
#include <iostream>

struct Node {
    std::shared_ptr<Node> next;
    std::weak_ptr<Node> prev;
    int value;
    Node(int val): value(val) {
        std::cout << "Node " << value << " created." << std::endl;
    }
    ~Node() {
        std::cout << "Node " << value << " destroyed." << std::endl;
    }
};

int main() {
    std::shared_ptr<Node> head = std::make_shared<Node>(1);
    std::shared_ptr<Node> second = std::make_shared<Node>(2);
    std::shared_ptr<Node> third = std::make_shared<Node>(3);
    
    head->next = second;
    second->prev = head;
    second->next = third;
    third->prev = second;
    third->next = head; // 순환 참조
    
    return 0;
}

이 예제에서는 Node 구조체에 prev라는 멤버 변수를 추가했습니다.

prev는 현재 노드의 이전 노드를 가리키는 weak_ptr입니다.

main 함수에서는 head, second, third 세 개의 노드를 생성하고 next와 prev 포인터로 연결합니다.

이번에도 third 노드에서 head 노드를 가리키는 것을 볼 수 있습니다.

프로그램을 실행해보면 다음과 같은 출력을 확인할 수 있습니다.

Node 1 created.
Node 2 created.
Node 3 created.
Node 1 destroyed.
Node 2 destroyed.
Node 3 destroyed.

세 개의 노드가 모두 생성된 후 main 함수가 끝나면서 head, second, third가 소멸됩니다.

그리고 head가 참조하는 노드는 third에서도 참조되고 있으므로 reference count가 2인 상태로 남아있게 됩니다.

또한 second는 head를 가리키는 prev 포인터를 가지고 있지만 weak_ptr을 사용했기 때문에 head가 소멸됨에 따라 prev 포인터는 무효화됩니다.

이로 인해 Node 1 destroyed., Node 2 destroyed. 메시지가 출력되지 않습니다.

이와 같이 shared_ptr이 더 복잡한 순환 참조 문제를 일으킬 수 있으므로, 이러한 문제가 발생하지 않도록 설계를 잘 해야 합니다.

순환 참조를 방지하기 위해서는 weak_ptr을 사용하거나, shared_ptr 대신 unique_ptr을 사용하거나, 참조를 다른 객체에게 위임하는 방식을 사용하는 등의 방법이 있습니다.

 

2. shared_ptr 에 raw pointer 를 대입할 때 발생하는 문제

 

#include <memory>
#include <iostream>

struct Node {
    std::shared_ptr<Node> next;
    int value;
    Node(int val): value(val) {
        std::cout << "Node " << value << " created." << std::endl;
    }
    ~Node() {
        std::cout << "Node " << value << " destroyed." << std::endl;
    }
};

int main() {
    std::shared_ptr<Node> head = std::make_shared<Node>(1);
    std::shared_ptr<Node> second = std::make_shared<Node>(2);
    
    head->next = second;
    second->next = head;

    std::shared_ptr<Node> third(head->next.get()); // raw pointer를 대입
    
    return 0;
}

이 예제에서는 third라는 새로운 shared_ptr을 생성할 때, head의 next 포인터를 가리키는 raw pointer를 사용합니다.

이렇게 되면 third와 head는 같은 객체를 참조하게 됩니다. 따라서 head와 second도 같은 객체를 참조하게 됩니다.

프로그램을 실행해보면 다음과 같은 출력을 확인할 수 있습니다.

Node 1 created.
Node 2 created.

두 개의 노드가 생성된 후 main 함수가 끝나면서 head, second, third가 소멸됩니다.

그러나 head와 second는 같은 객체를 참조하고 있으므로 reference count가 2인 상태로 남아있게 됩니다.

이로 인해 Node 1 destroyed., Node 2 destroyed. 메시지가 출력되지 않습니다.

이와 같이 raw pointer를 사용하여 shared_ptr이 같은 객체를 참조하게 되면, 객체가 제대로 해제되지 않는 문제가 발생할 수 있습니다. 이러한 문제를 방지하기 위해서는 raw pointer를 사용하는 것을 지양하고,

가능하면 shared_ptr 또는 weak_ptr을 사용하여 참조 관리를 해주는 것이 좋습니다.

 

3.  std::enable_shared_from_this 를 상속받아야 shared_ptr 의 안전한 사용이 가능한 이유

 

C++11에서 std::enable_shared_from_this를 상속받는 클래스는 해당 객체가 std::shared_ptr에 의해 관리되는 경우, std::shared_ptr가 객체를 안전하게 참조할 수 있도록 해줍니다.

이를 통해 shared_ptr이 안전하게 사용될 수 있는 이유는 다음과 같습니다.

std::shared_ptr은 객체의 reference count를 관리하며, 객체가 참조하는 모든 std::shared_ptr이 소멸될 때 해당 객체도 소멸됩니다. 그러나 이 때 참조하는 모든 std::shared_ptr이 소멸되어 reference count가 0이 되어야만 객체가 소멸되기 때문에, shared_ptr에 대한 순환 참조 문제가 발생할 수 있습니다.

즉, 객체가 참조하는 shared_ptr과 객체 자체를 참조하는 shared_ptr이 서로를 참조하는 순환 참조 문제가 발생할 수 있습니다.

 

std::enable_shared_from_this는 이러한 순환 참조 문제를 해결하기 위해, 해당 클래스에서 shared_from_this() 함수를 제공합니다. 이 함수는 std::shared_ptr의 인스턴스를 생성하면서 해당 객체를 가리키는 포인터를 반환합니다.

이 함수는 해당 객체가 std::shared_ptr에 의해 참조될 때 호출되어 std::shared_ptr의 내부 참조 카운트를 증가시킵니다.

또한 이 함수는 해당 객체가 이미 std::shared_ptr에 의해 관리되고 있지 않은 경우, std::bad_weak_ptr 예외를 발생시킵니다.

따라서, std::enable_shared_from_this를 상속받는 클래스에서 shared_from_this() 함수를 사용하여 객체를 안전하게 참조하면,

순환 참조 문제를 방지하면서 std::shared_ptr에 의해 안전하게 관리될 수 있습니다.

 

< std::enable_shared_from_this >

std::enable_shared_from_this 클래스는 std::shared_ptr에서 객체의 안전한 참조 계수(reference counting)를 관리하기 위한 기능을 제공합니다. 이 클래스는 std::shared_ptr를 사용하여 동적으로 할당된 객체의 참조 계수를 추적하는 데 필요한 일부 메커니즘을 구현합니다.

보통 std::shared_ptr를 사용하여 객체를 참조하는 경우, 해당 객체를 참조하는 모든 std::shared_ptr의 참조 계수가 1씩 증가하고, std::shared_ptr가 객체의 스코프를 벗어나면 참조 계수가 1씩 감소합니다. 이러한 메커니즘은 객체의 안전한 수명 관리를 보장합니다.

그러나 클래스 내부에서 std::shared_ptr의 사용을 제어하기 위해서는 std::enable_shared_from_this 클래스를 상속받아야 합니다. 이를 상속받은 클래스에서는 shared_from_this() 멤버 함수를 사용하여 this 포인터로부터 std::shared_ptr 인스턴스를 생성할 수 있습니다. 이렇게 생성된 std::shared_ptr은 해당 객체를 참조하는 다른 std::shared_ptr 인스턴스와 함께 안전하게 참조 계수를 관리할 수 있습니다.

따라서 std::enable_shared_from_this 클래스는 객체가 안전하게 참조되는 것을 보장하기 위한 용도로 사용됩니다.

#include <memory>
#include <iostream>

class MyClass : public std::enable_shared_from_this<MyClass>
{
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }

    std::shared_ptr<MyClass> getSharedPtr() { return shared_from_this(); }
};

int main()
{
    std::shared_ptr<MyClass> p1(new MyClass());
    std::shared_ptr<MyClass> p2 = p1->getSharedPtr();

    std::cout << "p1 use_count: " << p1.use_count() << std::endl; // 2
    std::cout << "p2 use_count: " << p2.use_count() << std::endl; // 2

    p1.reset();
    std::cout << "p2 use_count after reset p1: " << p2.use_count() << std::endl; // 1

    p2.reset();
    std::cout << "End of program" << std::endl;

    return 0;
}

위 코드에서 MyClass는 std::enable_shared_from_this<MyClass>를 상속받습니다.

그리고 getSharedPtr() 함수는 shared_from_this() 함수를 호출하여 현재 객체를 가리키는 std::shared_ptr을 반환합니다.

main() 함수에서는 p1과 p2라는 두 개의 std::shared_ptr 인스턴스를 생성합니다.

p1은 new MyClass()로 생성된 객체를 가리키고, p2는 p1의 getSharedPtr() 함수를 호출하여 생성된 std::shared_ptr을 가리킵니다.

그리고 p1의 reference count를 출력하면 2가 출력되는 것을 볼 수 있습니다.

이는 p1과 p2가 동일한 객체를 가리키기 때문입니다.

이후 p1을 reset() 메소드를 통해 소멸시키면, p2의 reference count가 1이 되는 것을 확인할 수 있습니다.

마지막으로 p2를 reset() 메소드를 통해 소멸시키면, MyClass 객체도 함께 소멸됩니다.

이때 MyClass 객체의 소멸자가 호출되며, "MyClass destructor" 메시지가 출력됩니다.

 

 

std::enable_shared_from_this를 상속받지 않은 클래스에서 shared_from_this() 함수를 사용하는 예제

#include <memory>
#include <iostream>

class MyClass
{
public:
    MyClass() { std::cout << "MyClass constructor" << std::endl; }
    ~MyClass() { std::cout << "MyClass destructor" << std::endl; }

    std::shared_ptr<MyClass> getSharedPtr() { return shared_from_this(); }
};

int main()
{
    std::shared_ptr<MyClass> p1(new MyClass());
    std::shared_ptr<MyClass> p2 = p1->getSharedPtr();

    std::cout << "p1 use_count: " << p1.use_count() << std::endl; // 1
    std::cout << "p2 use_count: " << p2.use_count() << std::endl; // 1

    p1.reset();
    std::cout << "p2 use_count after reset p1: " << p2.use_count() << std::endl; // 1

    p2.reset();
    std::cout << "End of program" << std::endl;

    return 0;
}

위 코드에서 MyClass는 std::enable_shared_from_this<MyClass>를 상속받지 않습니다. 그리고 getSharedPtr() 함수는 shared_from_this() 함수를 호출하여 현재 객체를 가리키는 std::shared_ptr을 반환합니다.

main() 함수에서는 p1과 p2라는 두 개의 std::shared_ptr 인스턴스를 생성합니다. p1은 new MyClass()로 생성된 객체를 가리키고,

p2는 p1의 getSharedPtr() 함수를 호출하여 생성된 std::shared_ptr을 가리킵니다.

그러나 MyClass 클래스는 std::enable_shared_from_this<MyClass>를 상속받지 않았기 때문에 shared_from_this() 함수가 사용될 때 문제가 발생합니다. 이 예제에서 shared_from_this() 함수를 호출하면, std::bad_weak_ptr 예외가 발생합니다.

따라서 main() 함수에서 p1과 p2의 reference count는 모두 1인 상태이며, 둘 다 reset() 메소드를 호출하면 객체가 제대로 소멸됩니다.

 

4. 사용자 지정 deleter 를 사용

std::shared_ptr에서 사용자 정의 deleter를 지정하여 메모리 할당과 해제를 처리하는 예제입니다.

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass(int n = 0) : num(n) {}
    void print() { std::cout << "MyClass::num = " << num << std::endl; }
private:
    int num;
};

void custom_deleter(MyClass* p) {
    std::cout << "Custom deleter called" << std::endl;
    delete p;
}

int main() {
    std::shared_ptr<MyClass> sp1(new MyClass(1), custom_deleter);
    std::shared_ptr<MyClass> sp2(new MyClass(2), [](MyClass* p) { std::cout << "Lambda deleter called" << std::endl; delete p; });
    
    sp1->print();
    sp2->print();
    
    return 0;
}

위 예제에서는 std::shared_ptr를 사용하여 MyClass 객체를 생성합니다.

std::shared_ptr의 생성자에서 두 번째 인자로 사용자 정의 deleter를 지정하고 있습니다.

첫 번째 std::shared_ptr는 함수 custom_deleter를, 두 번째 std::shared_ptr는 람다 함수를 사용하여 deleter를 지정하고 있습니다.

custom_deleter 함수는 객체를 삭제하기 전에 "Custom deleter called"라는 메시지를 출력합니다.

람다 함수는 객체를 삭제하기 전에 "Lambda deleter called"라는 메시지를 출력합니다.

따라서, std::shared_ptr를 통해 객체를 참조하다가 std::shared_ptr의 수명이 끝나면 사용자 정의 deleter가 호출되어 객체를 삭제합니다.

 

 

상수 객체 (const object)

C++에서 상수(const) 객체는 그 값이 변경될 수 없는 객체입니다.

상수 객체는 객체를 생성할 때 값이 할당되며, 이후에는 변경되지 않습니다.

 

const int num = 10; // 'num'은 상수 객체이며, 값은 10입니다.

위 드에서 const 키워드는 num 변수가 상수임을 나타냅니다. 이후에는 num의 값을 변경할 수 없습니다.

상수 객체는 객체 지향 프로그래밍에서 매우 중요한 역할을 합니다.

상수 객체를 사용하면, 값이 변경되면 안되는 상황에서 값이 변경되는 것을 방지할 수 있습니다.

또한, 상수 객체를 이용하면 컴파일러가 값을 임베딩(embedding)할 수 있어서, 코드의 실행 속도를 높일 수 있습니다.

 

코드의 실행 속도를 높일 수 있는 이유

상수 객체는 일반적으로 값을 변경할 수 없기 때문에, 컴파일러는 이러한 객체를 메모리에 상수로 저장하도록 최적화할 수 있습니다.

이를 통해 실행 속도를 향상시키고, 프로그램의 안정성을 높일 수 있습니다

 

   1. 메모리 접근 시간 단축

상수 객체의 값을 직접 코드에 삽입함으로써, 상수 객체의 값을 참조하기 위한 메모리 접근을 생략할 수 있습니다.

이는 프로그램 실행 시간을 단축시키는데 큰 도움이 됩니다.

 

const int num = 10;
int x = num + 5;

컴파일러는 num + 5를 15로 대체할 수 있습니다.

이렇게 상수 값을 바로 사용하면, 변수나 메모리를 참조하지 않고 값을 얻을 수 있으므로 실행 속도를 높일 수 있습니다.

 

    2.   코드 크기 감소

상수 객체를 사용하면, 상수 값이 중복해서 사용될 때 메모리에 상수 값의 복사본이 저장되는 것을 방지할 수 있습니다.

대신, 컴파일러는 상수 값을 임베딩해서 사용하므로 코드 크기가 감소하게 됩니다.

상수 값을 메모리에 저장할 경우, 상수 값이 여러 번 사용될 때마다 해당 메모리 주소를 참조하므로 코드가 길어질 수 있습니다.

하지만 상수 값을 임베딩하면 코드가 더 간결해지고, 프로그램의 크기가 작아질 수 있습니다.

결론적으로, 상수 객체를 이용하면 컴파일러가 값을 임베딩할 수 있어서, 코드의 실행 속도를 높일 수 있습니다.

이를 통해 프로그램의 성능을 향상시킬 수 있습니다.

 

 

C++에서 상수 객체는 전역 상수(constant)지역 상수(constant)로 나뉘어서 메모리의 서로 다른 영역에 저장됩니다.

전역 상수는 프로그램이 실행될 때 프로그램의 데이터 섹션(data section)에 저장됩니다.

데이터 섹션은 전역 변수와 정적(static) 변수도 저장하는 영역으로, 프로그램이 종료될 때까지 메모리에 남아있습니다.

지역 상수는 해당 변수가 정의된 블록이 실행될 때 스택(stack) 메모리 영역에 저장됩니다.

블록이 실행될 때마다 해당 상수의 값을 읽어와서 사용하며, 블록이 끝나면 해당 상수는 스택에서 제거됩니다.

 

상수 멤버함수 (const member function)

상수 멤버함수란 클래스 멤버함수 선언 뒤에 'const' 키워드로 선언 된 함수로,

상수 멤버 함수 안에서는 모든 멤버를 상수 처리한다는 특징이 있습니다.

여기서 상수 처리한다는 것은 함수 내부에서 사용되는 변수 값을 바꿀 수 없다는 의미입니다. 

 

#include <iostream>

class Point
{
private:
	int x, y;

public:	
	Point(int a = 0, int b= 0) : x(a), y(b) {}

	void set(int a, int b)
	{
		x= a;
		y= b;
	}
	void print() const // 상수 멤버 함수 : 안에서 모든 멤버를 상수처리
	{
		std::cout<< x << " , " << y <<std::endl;
	}
};

int main()
{
	const Point p(1,1);
	p.x = 10;     //error 값 바꿀 수 없음
	p.set(10,20); //error 값 바꿀 수 없음
	p.print(); // error

	return 0;
}

 

 위 코드에서 객체 p 는 const object 로 선언되었습니다. 따라서 내부 멤버 변수인 x 값을 직접 변경하거나, 변수 값을 변경하는 기능이 구현된 set함수를 호출할 경우 error 가 발생합니다.
그런데 print 함수는 값을 변경하지 않음에도 호출하지 못합니다.
그 이유는 p이 상수 객체이므로 호출하는 함수 또한 상수함수임이 보장되어야 하기 때문입니다.
이것을 보장하기 위해, 멤버함수를 const 선언 해줌으로써 컴파일러가 해당 함수가 변수 값을 변경하지 않는 const 함수라는 것을 알수 있도록 해주면 됩니다.

 

사실, 객체의 상태를 변경하지 않는 '모든' 멤버함수는 (getXXX 류), 반드시 const 멤버함수가 되어야 합니다.

#include <iostream>


struct Rect
{
	int ox, oy, width, height;

	Rect(int x= 0, int y= 0, int w= 0, int h = 0): ox(x), oy(y), width(w), height(h){}

	int getArea() const { return width*height;}
};


void foo(const Rect& r) // call by value overhaed --> const & 가 좋음
{
	int n = r.getArea();  
}
int main()
{

	Rect r(0,0, 10, 10);

	int n= r.getArea();

	foo(r);
	return 0;
}
위 예제는 getArea 함수가 const 선언 되어야 하는 이유에 대해 설명하는 코드 입니다.
foo()  라는 함수에서 Rect 의 인스턴스를 통해 getArea 함수를 호출하는 경우를 생각해봅시다.
이때 오버헤드 측면에서 call by value 형태로 인자를 받아오는 것보다는, const 객체 참조로 받는 것이 타당합니다.
과정에서 r이 '상수 객체'로서 복사(?) 되므로 getArea() 가 반드시 const 선언 되어있어야 r.getArea()를 호출 할 수 있게 됩니다.

 

논리적 상수성

Case1. mutable 이용

#include <iostream>
#include <stdio.h>

class Point
{
private:
	int x, y;
	// mutable 멤버 변수 : 상수 멤버함수 안에서도 값 변경 가능
	mutable char cache[16];
	mutable bool cache_valid = false;

public:	
	Point(int a = 0, int b= 0) : x(a), y(b) {}

	const char* toString() const// 상수 멤버함수여야 하는 이유
	{		
		if(cache_valid == false){
			sprintf(cache,"%d, %d", x,y );  // 멤버함수(cache, cache_valid를 변경할 수 없음)
			cache_valid = true;
		}
		return cache; 
	}
};
위 코드 예제는 sprintf 의 오버헤드가 크다고 가정하고,  cache 값이 변경됐을 경우에만 버퍼를 새롭게 써서 리턴하는 구조를 가져가는 형태로 구현을 한 것입니다.
그런데 toString 함수는 const 함수로 내부에서 멤버 변수들인 cache 및 cache_valid 값을 변경할 수 없습니다.
이 때, 이 두 변수들을 mutable 로 선언해주면, const 함수 내에서도 값을 변경할 수 있게 됩니다.
이렇게 함으로써 const 함수를 유지하면서도 내부적으로 변경이 필요한 변수들의 값을 바꿀 수 있습니다.

 

Case2. 포인터 이용

struct Cache
{
	char cache[16];
	bool cache_valid = false;
};
class Point
{
private:
	int x, y;
	Cache* pCache;
public:	
	Point(int a = 0, int b= 0) : x(a), y(b) {
		pCache = new Cache;
	}
	~Point(){
		delete pCache;
	}
	const char* toString() const// 상수 멤버함수여야 하는 이유
	{
 		if(pCache->cache_valid == false){
			sprintf(pCache->cache,"%d, %d", x,y );  // 멤버함수(cache, cache_valid를 변경할 수 없음)
			pCache->cache_valid = true;
		}
		return pCache->cache; 
	}
};
const 함수 내에 일부 변수를 변경하고자 할때 사용할 수 있는 두번째 방법은, 변경이 필요한 변수들을 별도 자료구조(Cache)에 담아두고, 이를 가리키는 포인터를 클래스 멤버 변수로 선언하는 것입니다.
이 객체가 생성자에서 Cache 인스턴스를 할당하고, const 함수 내에서는 이 인스턴스를 통해서 변수에 접근 및 변경을 할 수 있습니다.

 

const return

 

const를 반환하면, 반환된 값이 수정되지 않도록 보장합니다. 이는 다른 함수나 코드에서 반환된 값을 수정하는 것을 방지하므로 코드 안정성을 높이는 데 도움이 됩니다. 또한, 반환된 값을 const로 선언함으로써, 해당 값을 수정하는 코드가 컴파일러에서 에러로 처리되므로 유지 보수성을 높이는 데 도움이 됩니다.

또한, const를 반환하면 임시 객체에 대한 복사를 피할 수 있습니다. 예를 들어, 다음과 같은 함수가 있다고 가정해 봅시다.

 

const string getGreeting() {
  return "Hello";
}
이 함수가 호출되면, 문자열 "Hello"를 반환하는 임시 객체가 생성됩니다. 이 객체는 const로 반환되기 때문에, 함수 외부에서 수정될 수 없습니다. 만약 반환 값이 const가 아니라면, 임시 객체가 생성되는 것 외에도, 복사 생성자가 호출되어 복사본이 생성되어야 하므로 성능 저하를 초래할 수 있습니다.따라서, C++에서 const를 반환하는 것은 코드의 안정성, 유지 보수성, 성능 향상 등 여러 가지 이점을 제공합니다.

 

다른 예제를 살펴보겠습니다.

 

class Car {
public:
  const Engine& getEngine() const {
    return engine;
  }

private:
  Engine engine;
};
Car 클래스는  Engine 클래스의 객체를 포함하고 있습니다. 
getEngine() 함수는 const 로 선언되어 있으며, Engine 객체를 반환합니다.
이 함수는 const 로 선언되었기 때문에 Car 객체가 const 로 선언되었을 때 호출될 수 있습니다.

 

const Car myCar;
const Engine& myEngine = myCar.getEngine();
위 코드에서 myCar는 const로 선언되었으므로, getEngine() 함수는 const 버전이 호출됩니다. getEngine() 함수가 반환하는 Engine 객체는 const로 선언되어 있으므로, myEngine 변수도 const로 선언되어야 합니다.
이렇게 함으로써, myEngine 객체가 수정되지 않도록 보장됩니다.
만약 getEngine() 함수가 const를 반환하지 않는다면, myCar 객체가 const로 선언되었을 때 getEngine() 함수를 호출할 수 없으며, myEngine 변수를 const로 선언하지 않으면 myEngine 객체가 수정될 수 있습니다. 

 

따라서, const를 반환하는 함수를 사용함으로써 코드의 안정성과 유지 보수성을 높일 수 있습니다.

const pointer

 

C++에서 const 포인터는 포인터 자체가 변경될 수 없다는 것을 나타냅니다. 즉, 포인터가 가리키는 메모리의 값을 변경할 수는 있지만, 포인터가 가리키는 주소 자체는 변경할 수 없습니다.

const 포인터를 사용하면, 코드 안정성과 유지 보수성을 높일 수 있습니다. 예를 들어, 다음과 같은 코드를 생각해봅시다

 

void printValues(const int* values, int size) {
  for (int i = 0; i < size; ++i) {
    cout << values[i] << " ";
  }
}

 

위 코드에서 values 매개변수는 const int*로 선언되어 있습니다. 이는 values 포인터가 가리키는 메모리의 값을 변경할 수 있지만, values 포인터가 가리키는 주소 자체는 변경할 수 없다는 것을 나타냅니다.
이렇게 함으로써, printValues() 함수는 values 포인터를 통해 전달된 값들을 변경하지 않으므로, 호출한 쪽에서 전달한 값을 보존할 수 있습니다. 또한, const int* 타입을 사용하면, 잘못된 포인터 연산이나 잘못된 메모리 참조 등과 같은 문제를 방지할 수 있습니다.

 

또한, const 포인터는 포인터를 상수로 취급할 때도 유용합니다. 다음과 같은 예를 생각해보세요.

 

int value = 10;
const int* const pValue = &value;

 

위 코드에서 pValue는 const int* const로 선언되어 있습니다.

이는 pValue 포인터가 가리키는 주소 자체와, pValue 포인터가 가리키는 메모리의 값을 변경할 수 없다는 것을 나타냅니다.

이렇게 함으로써, pValue는 상수 포인터가 되어, 가리키는 값을 변경할 수 없으므로, 값이 변경되지 않도록 보장할 수 있습니다

 

 

virtual function (가상함수) 오버라이딩 두가지 규칙

1. 상속된 메소드를 재정의할 경우에는 오리지널 원형과 정확히 일치시킬 필요가 있음

(에외: 리턴형의 공변(covariance)- 리턴형이 기초 클래스에 대한 참조나 포인터인 경우 파생 클래스에 대한 참조나 포인터로 대체)

2. 기초 클래스 선언이 오버로딩 되어있다면, 파생 클래스에서 모든 기초 클래스 버전들을 재정의 해야 

 

Abstract class (추상 클래스) 의 특징

적어도 하나의 순수(pure) 가상함수를 가진 클래스

순수가상함수(pure virtual function) : 함수 선언 뒤에 “=0” 붙여서 표현하며 선언만으로 존재

순수가상함수를 포함한 클래스는 객체생성 불가능(기초역할을 하기위해서만 존재)

 

상속과 오버라이딩(Inheritance and Overriding)

기초 클래스 메소드를 파생 클래스에서 재정의 할때는 일반적으로 해당 기초 클래스 메소드를 virtual 로 선언함

virtual 키워드 사용하지 않을 경우, 프로그램은 참조형이나 포인터형에 기초하여 메소드를 선택

virtual 키워드 사용할 경우, 프로그램은 참조나 포인터에 의해 지시되는 객체형에 기초하여 메소드 선택

 

※ 주의: 파생 클래스에 하나라도 오버라이딩 된 함수가 존재할 경우, 기초 클래스에 가상 소멸자 선언해줘야 함

virtual 소멸자가 아니라면, 포인터형에 해당하는 소멸자만 호출될 것

(즉, 파생 클래스를 지시하는 경우에도 기초 클래스 소멸자가 호출된다는 것을 의미)

따라서 virtual 소멸자로 객체형에 해당하는 소멸자가 호출된 이후 기초 클래스 소멸자가가 자동으로 호출되게함

 

정적 결합과 동적 결합(Static binding & Dynamic binding)

 

정적 결합: 컴파일 동안 일어나는 결합 (초기 결합)

동적 결합: 런타임 동안 올바른 가상 메소드가 선택되도록 하는 결합(말기 결합)

 

객체에 대한 참조를 사용하여 또는 객체를 지시하는 포인터를 사용하여 가상 메소드가 호출되면,

프로그램은 그 참조나 포인터형을 위해 정의된 메소드를 사용하지 않고, 

객체형을 위해 정의된 메소드를 사용하며 이것을 “동적 결합”이라고 함

 

기초 클래스 포인터나 참조가 파생 클래스 객체를 지시하는 것이 항상 가능하므로 이러한 로직이 중요함

(암시적) 업캐스팅: 기초 클래스 포인터나 참조가 기초 클래스 객체나 파생 클래스 객체를 참조하는 것을 가능하게 함

 

동적결합이 필요한 이유

이에 대한 해답으로 가상 멤버 함수 개념이 등장

 

friend (프렌드) 와 virtual function 가상함수)

프렌드는 가상함수가 될 수 없음

멤버 함수만 가상함수가 될 수 있는데, 프렌드는 클래스 멤버가 아니기 때문

이 경우 프렌드 함수 내부적으로 가상 멤버 함수를 사용하게 하면됨

 

가상함수를 다시 정의하지 않으면, 파생 클래스는 그 함수의 기초 클래스 버전을 사용함

(길게 이어진 파생 사슬 구조라면 가장 최근에 정의된 버전을 사용함)

 

어떤 함수를 파생 클래스에서 재정의하면 동일한 함수 시그니처를 갖고 있는 기초 클래스 선언만 가리는 것이 아니라,

매개변수 시그니처와 상관 없이 같은 이름을 가진 모든 기초 클래스 메소드를 가림

 

 

가상함수 테이블의 생성 및 참조

virtual table 는 컴파일러에 의해 생성됨

생성하는 조건은 클래스 내부에 최소 하나의 가상함수가 정의 되어 있는 경우임

가상함수 테이블은 해당 클래스의 가상함수 리스트를 포함하고 있는 자료 구조를 의미함

 

가상함수를 포함하고 있는 클래스의 각 객체들은 가상함수 테이블을 가리키는 포인터를 가지는데,

이 포인터는 객체의 생성자에 의해 초기화 되며, 해당 클래스의 가상함수 테이블을 가리킴

 

객체에 의해 가상함수가 호출되는 것은, 가상함수 테이블 포인터를 이용하여 가상함수 테이블 내에 호출된 함수를 찾아 호출하게 되는 것

 

가상함수 테이블은 객체 포인터나 참조에서 가상함수가 호출되는 런타임 때에 참조 됨

컴파일러는 가상함수 테이블 포인터를 사용하는 코드를 생성함으로써, (객체의 실제 타입을 근거로 )정확한 함수를 찾아내게 됨

이러한 과정을 dynamic dispatch 라고 하며, 이 과정을 통해 런타임에 객체의 실제 타입으로부터 정확한 함수가 호출될 수 있게 되는 것임

 

가상함수 및 테이블의 사용은 C++의 핵심 기술중 하나인 다형성(Polymorphism)을 지원함으로써 유연하고 확장성 있는 코드를 구성할 수 있음

 

결론: virtual table 자체는 컴파일 타임(compile time)동안에 생성되나 조회는 런타임(runtime)에 이루어짐

+ Recent posts