C++ 일반 자료구조


https://cplusplus.com/doc/tutorial/variables/

언어에 직접적으로 구현되어 대부분의 시스템에서 기본적으로 지원된다.

  • 문자 타입들: 한 문자를 표현하는 데 쓰임 (‘A’, ‘$’ 등)
    가장 흔한 타입은 char, 1바이트만을 사용하지만 더 넓은 범위를 지원하는 문자도 있다.
    • char: 1바이트 사용 (8비트)
    • char16_t: char보다 크게 2바이트 사용 (16비트)
    • char32_t: char16_t보다 크게 4바이트 사용 (32비트)
    • wchar_t: 가장 큰 범위로 표현할 수 있음
  • 정수 타입: 숫자를 저장할 수 있음 (7, 1024 등)
    음수의 유무에 따라 부호의 유무를 정할 수 있음
    • 부호 있음 (signed 생략 가능)
      • signed char: char와 같은 1바이트 사용 (8비트)
      • signed short int: char보다 크게 2바이트 사용 (16비트)
      • signed int: 4바이트 사용 (32비트)
      • signed long int: int와 같이 4바이트 사용 (32비트)
      • signed long long int: long보다 크게 8바이트 사용 (64비트)
    • 부호 없음 : 부호 있는 것과 크기가 같음
      • unsigned char
      • unsigned short int
      • unsigned int
      • unsigned long int
      • unsigned long long int
  • 소수점 타입: 3.14, 0.01와 같은 실수, 각자 정확도가 다른 3가지 유형이 있다.
    • float
    • double: float보다 정확도가 높음
    • long double: double보다 정확도가 높음
  • 부울 타입: true나 false로 상태를 나누는 타입
    • bool
  • void: 공간 없음, 아무런 유형이 아님
  • Null pointer: decltype(nullptr)

char(1바이트)외의 크기는 어디까지나 일반적으로 지정하는 크기일 뿐 어느 타입도 표준 크기가 정해져있지 않다.
따라서 특정 컴파일러에서 프로그램이 구동될 아키텍쳐에 맞춰 특정 타입의 사이즈를 수정될 수 있다.

또한 소수점 타입의 경우, 사용되는 비트를 가수, 지수 표현하여 정확도에 영향을 미치게 된다.

특정 시스템 및 컴파일러에서 기본 유형의 속성은 (표쥰 헤더  <limits>의) numeric_limits 클래스에서 확인할 수 있다. 특정 타입의 크기들을 알고 싶으면 <cstdint>에서 고정 크기 유형을 확인할 수 있다.

문자 시퀸스 Character sequences

string 클래스는 사실 문자가 순차적으로 나열된 것에 불과하다.

C 스타일 문자열
char 문자로 배열로 이루면 메모리 상에서 아래와 같은 구조로 나온다.

char foo [20];

이 때, 20개의 문자를 담을 용량은 확보하게 되는데 이걸 굳이 다 사용해야 하진 않다.

그리고 관례 상, 문자열의 마지막 문자로 Null 문자 (Null terminator) '\0'를 넣어 이를 알릴 수 있다. (당연하게도 null 문자도 용량에 포함된다.)

문자열을 초기화하는 동안에 Null 문자를 넣을 수도 있다.

char myword[] = { 'H', 'e', 'l', 'l', 'o', '\0' }; // 원소를 하나하나 선언하는 경우, 마지막에 직접 넣어야 한다.
char myword[] = "Hello"; // 문자열 리터럴의 경우, 자동으로 마지막에 추가된다.

// 반면에 아래와 같이 이미 선언한 문자열에 대해서는 추가되지 않는다.
myword = "Bye";
myword[] = "Bye";

// 배열은 선언할 때 외에는 값들을 한번에 할당할 수 없으므로 이 경우에는 배열 인덱스에 직접 삽입해야 한다.
myword[0] = 'B';
myword[1] = 'y';
myword[2] = 'e';
myword[3] = '\0';

string 클래스의 Null 문자

헤더 <string>을 통한 string 클래스에서도 이런 Null 문자를 지원한다.
같은 string 객체를 통해서는 지원되지 않지만 문자열 리터럴을 통한 연산에는 Null 문자를 지원한다.

char myntcs[] = "some text";
string mystring = myntcs;  // convert c-string to string
cout << mystring;          // printed as a library string
cout << mystring.c_str();  // printed as a c-string (string 클래스 내부의 변환 함수)

// 위의 출력한 2개 모두 Null 문자를 가지게 된다.

구조체 structure

데이터 요소들을 하나의 이름으로 묶은 그룹

struct type_name {
member_type1 member_name1;
member_type2 member_name2;
member_type3 member_name3;
.
.
} object_names;
struct product {
  int weight;
  double price;
} ;

product apple;
product banana, melon;

///////////////////////////////

struct product {
  int weight;
} ; // 오브젝트 이름을 생략하는 대신 최소한 하나의 멤버와 타입 이름을 이름 명시하여 선언

구조체를 선언했다면 내부의 변수를 구조체.멤버이름을 통해 일반적인 변수로써 직접 다룰 수 있게 된다.

또한 구조체 안에 구조체를 넣을 수도 있다.
물론 이 경우에는 해당 코드 흐름 상에서 이미 정의된 구조체여야 한다.

struct movies_t {
  string title;
  int year;
};

struct friends_t {
  string name;
  string email;
  movies_t favorite_movie;
} charlie, maria;

friends_t * pfriends = &charlie;

Unions

구조체랑 비슷하지만 멤버 변수끼리 메모리의 한 부분을 같이 사용하여 다른 데이터 타입으로 접근하는 것을 허용하는 점에서 꽤 다르게 작동된다.

union type_name {
  member_type1 member_name1;
  member_type2 member_name2;
  member_type3 member_name3;
  .
  .
} object_names;

union mytypes_t {
  char c;
  int i;
  float f;
} mytypes;

위의 예제에서 Union의 멤버 변수들은 타입이 각기 다르지만 같은 메모리를 공유하여 한쪽에서 값이 변경된다면 모든 변수에서 그 값이 반영하게 된다.

union mix_t {
  int l; // 4바이트
  struct {
    short hi; // 2 바이트
    short lo; // 2 바이트
    } s; // 총합: 4바이트
  char c[4]; // 1 * 4 바이트
} mix;

또한 클래스나 구조체의 멤버로써 이름 없는 anonymous unions로 선언하는 것도 가능하고 이 union의 이름 없이 멤버 변수만으로 접근이 가능하다.

struct book1_t {
  char title[50];
  char author[50];
  union {
    float dollars;
    int yen;
  } price;
} book1;
book1.price.dollars
struct book2_t {
  char title[50];
  char author[50];
  union {
    float dollars;
    int yen;
  };
} book2
book2.dollars

열거형 enum

별도로 식별자를 만들어 정수 값을 열거하는 타입을 새로 만들 수 있다.

enum type_name {
  value1, // value = default value (optional), without setting this, will have value = 0;
  value2, // value1 + 1
  value3,
  .
  .
} object_names;

enum colors_t {black, blue, green, cyan, red, purple, yellow, white};

나아가서 C++에서는 타입 체크의 안정성을 위해 암시적으로 정수형으로 변환하는 열거형 타입을 아예 진짜 타입으로 만들 수 있다.

enum class Colors {black, blue, green, cyan, red, purple, yellow, white};
// "enum struct" is also allowed.

Colors mycolor;
 
mycolor = Colors::blue;
if (mycolor == Colors::green) mycolor = Colors::red; 

또한 기본 타입을 통한 사이즈도 정할 수 있다. (확인 필요)
아래의 경우, EyeColor는 이제 char와 같은 1바이트의 크기를 가지게 된다.

enum class EyeColor : char {blue, green, brown}; 
// classname : type_name {value1, value2, ...}

형 변환 Type conversions

암시적 변환 (표준 변환)

서로 호환되는 타입으로 값아 복사할 때 수행된다.
일반적으로 숫자형 타입(int ~ double), bool, 포인터 변환이 있다.

작은 정수 타입(int)에서 소수 타입(float, double)로 변환하는 것은 대상 타입에서 정확히 동일한 값을 생성하도록 보장된다. 그 외에는 정확히 동일한 값을 생성하는 것이 보장되어 있지 않다.

예를 들어, 음수인 정수 값을 부호 없는 정수로 변환한다면 2의 보수 값을 얻게 된다. (따라서 -1은 부호 없는 정수에서 값이 반전되어 한계값까지 얻게됨.)

또한 bool과의 변환은 0인 값과 null pointer는 false, 그 외 1 이상의 값은 모두 false로 변환된다.

소수점에서 정수로 변환하는 경우, 소수점 이후의 숫자들은 버려진다. 또한 타입의 범위를 벗어났던 경우라면 변환이 제대로 되지 않아 예상치 못한 결과가 나올 수 있다.

이 외에도 같은 종류끼리의 변환이어도 구현에 따라 달라지거나 아예 변환을 못할 수도 있다.

만약에 이런 변환으로 값의 정확도를 잃을 수 있다면 컴파일러에서 경고하게 되지만 명시적 형변환으로 경고하지 않게 할 수 있다.

일반적인 타입 말고도 배열과 함수도 포인터로 변환될 수 있다.
– null pointer는 타입을 불문하고 변환될 수 있다.
– 모든 타입의 포인터는 void 포인터로 변환될 수 있다.
– 포인터 업캐스트: 상속된 클래스의 포인터는 기반 클래스의 포인터로 변환될 수 있다. (const, volatile 속성 변화 없음)

클래스의 경우는 3가지 멤버 함수로 이 암시적 변환을 다룰 수 있다.

  • 매개변수가 하나만 있는 생성자: 특정 타입으로 오브젝트 생성할 수 있도록 함
  • 할당 연산자: 할당할 경우에 특정 타입으로 변환할 수 있도록 함
  • 타입 캐스트 연산자: 특정 타입으로 변환할 수 있도록 함
// implicit conversion of classes:
#include <iostream>
using namespace std;

class A {};

class B {
public:
 // A를 B로 변환하는 생성자
  B (const A& x) {}
  // 할당 연산자를 통한 변환:
  B& operator= (const A& x) {return *this;}
  // 타입 캐스트 연산자를 통한 변환:
  operator A() {return A();}
};

int main ()
{
  A foo;
  B bar = foo;    // calls constructor
  bar = foo;      // calls assignment
  foo = bar;      // calls type-cast operator
  return 0;
}

타입 캐스트 연산자의 경우는 operator 키워드 옆에 변환될 타입을 쓰고 빈 괄호를 넣는다. 일반적인 함수처럼 반환 타입을 이때 적으므로 앞에 따로 적지 않는다.

명시적 형변환

C++에서 함수 호출 시, 각 매개변수마다 한번의 암시적 변환을 허용하는데 여기서 클래스를 매개변수로 넣을 때도 이 변환이 발생하여 원치 않은 결과가 발생될 수 있다.

void fn (B arg) {}

// foo = class A
fn (foo); // 위의 예제대로 선언되었다면 B가 아닌 A도 매개변수에 넣어 선언될 수 있다.

이 현상을 막으려면 생성자의 암시적 호출을 금지하는 속성을 추가하기 위해 explicit 키워드를 생성자 앞에 붙여야 한다.

// explicit:
#include <iostream>
using namespace std;

class A {};

class B {
public:
  explicit B (const A& x) {} // 명시적 선언
  B& operator= (const A& x) {return *this;}
  operator A() {return A();}
};

void fn (B x) {}

int main ()
{
  A foo;

  // B bar = foo;    // 위의 예제에서 사용했던 이 생성자 호출은 더 이상 안된다.
  B bar (foo);
  bar = foo;
  foo = bar;
  
//  fn (foo);  // not allowed for explicit ctor.
  fn (bar);  

  return 0;
}

타입 캐스트 연산자 함수에도 동일하게 explicit를 통한 암시적 호출 금지가 가능하다.

타입 캐스트 (명시적 형변환)

간단하게 두 구문으로 수행할 수 있다.

y = int (x);    // 함수형 표기
y = (int) x;    // C-스타일 캐스트 표기

이 연산자들을 이용하여 클래스와 포인터의 상호 변환이 가능하지만 잘못된 클래스를 가르킨 포인터로 인해 런타임 에러를 일으킬 수 있다.

// class type-casting
#include <iostream>
using namespace std;

class Dummy {
    double i,j;
};

class Addition {
    int x,y;
  public:
    Addition (int a, int b) { x=a; y=b; }
    int result() { return x+y;}
};

int main () {
  Dummy d;
  Addition * padd;
  padd = (Addition*) &d; // d는 애당초 같은 클래스가 아니다.
  cout << padd->result(); // 따라서 잘못된 클래스를 가르키고 멤버를 꺼내려고 한다면 여기서 에러가 발생한다.
  return 0;
}

이런 타입들의 변환을 위해 별도의 캐스팅 연산자들이 있다.

  • dynamic_cast <new_type> (expression)
    가상 멤버함수가 있는 클래스 포인터와 레퍼런스에만 사용 가능

    런타임에 타입 변환의 결과가 변환하고자 했던 클래스를 제대로 가르키는지 보증한다.
    기반 클래스를 가르키는 포인터 업캐스트는 암시적 변환으로 허용하지만 상속된 클래스를 가르키는 다운캐스트도 가능하여 제대로 된 오브젝트인지 확인을 거칠 필요가 있다.

    또한 nullptr을 다른 (상관 없는 클래스까지) 포인터 타입으로 암시적 변환하는 것과 어떤 포인터든 void* 로 변환하는 것도 가능하다.
// dynamic_cast
#include <iostream>
#include <exception>
using namespace std;

class Base { virtual void dummy() {} };
class Derived: public Base { int a; };

int main () {
  try {
    Base * pba = new Derived;
    Base * pbb = new Base;
    Derived * pd;

    pd = dynamic_cast<Derived*>(pba); // 할당 자체가 Derived로 이뤄졌으므로 이 변환은 제대로 이뤄진다.
    if (pd==0) cout << "Null pointer on first type-cast.\n";

    pd = dynamic_cast<Derived*>(pbb); // 다운 캐스트: Base로 할당되었지만 상속된 Derived 포인터로 변환되었으므로 다이나믹 캐스트는 nullptr를 반환한다.
    if (pd==0) cout << "Null pointer on second type-cast.\n";

  } catch (exception& e) {cout << "Exception: " << e.what();}
  return 0;
}

컴파일러에서 RTTI (Run-Time Type Information)를 지원하지 않는 경우, dynamic_cast를 사용할 수 없다.

또한 레포런스 타입으로 변환하는 것이 실패했던 경우하고 변환 자체가 불가능했다면 bad_cast 예외가 발생된다.

  • static_cast <new_type> (expression) (확인 필요)
    dynamic_cast와 유사하지만 해당 포인터가 가르키는 클래스를 런타임에 확인하지 않는다. (컴파일러에서 확인하므로 부하가 없다.) 따라서 이걸 다루는 건 전적으로 프로그래머 책임이다.

    또한 클래스를 가르키는 포인터 외에도 다른 부류도 변환이 가능하게 되는데 예를 들어 void*로 변환하고 다른 포인터 타입으로 변환했을 때 정확한 포인터 값을 얻는다면 제대로 변환되었음을 보증한다.
    그리고 정수, 소수점, 열거형으로도 변환할 수 있다.

    그리고 부가적으로 아래와 같이 수행한다.
    • 명시적으로 매개변수가 하나인 생성자나 변환 연산자 함수를 호출한다.
    • rvalue reference로 변환한다.
    • 열거 클래스 값을 정수와 소수점 값으로 변환한다.
    • 어떤 타입이든 void로 변환하면 평가에 따라 값을 버릴 수 있다.
  • reinterpret_cast <new_type> (expression)
    포인터 타입을 다른 포인터 타입으로 변환한다.
    전혀 다른 클래스여도 비트 단위로 재해석하여 변환된다.

    단순 정수 값으로 변환되거나 할 수 있는데 이는 포인터를 플랫폼에서 표현하는 방식에 따라 다를 수 있다. 따라서 이 값을 충분히 담을 수 있는 크기의 정수형으로 바꿔야 정상적으로 다른 포인터로 다시 바꿀 수 있다. (물론 정수로 메모리를 직접 다루는 것은 매우 위험하다.)

    reinterpret_cast에서 수행되지만 static_cast에서 될 수 없는 변환의 경우는 static_cast에서 수행되는 이진 표현을 해석하는 저수준 작업 과정에서 실패한 부류이므로 해당 시스템에서만 작동되는 코드라서 호환성이 없어지게 된다.
  • const_cast <new_type> (expression)
    해당 포인터가 가르키는 대상의 const와 volatile 속성을 잠시 삭제할 수 있다.
  • typeid (expression)
    특정 표현식의 타입을 확인할 수 있다.

    헤더 <typeinfo>에 선언된 type_info 타입의 상수 오브젝트 레퍼런스를 반환하며 논리 연산자를 통한 변수끼리의 타입 일치 등을 확인 할 수 있고 name 함수를 통한 타입의 이름도 확인할 수 있게 된다.

    그리고 반환되는 타입의 이름 문자열은 컴파일러와 라이브러리의 구현에 따라 다를 수 있고 포인터의 타입도 별개의 타입으로 취급한다는 점을 명심하라.
// typeid
#include <iostream>
#include <typeinfo>
using namespace std;

int main () {
  int * a,b;
  a=0; b=0;
  if (typeid(a) != typeid(b))
  {
    cout << "a and b are of different types:\n";
    cout << "a is: " << typeid(a).name() << '\n'; // a is: int *
    cout << "b is: " << typeid(b).name() << '\n'; // b is: int  
  }
  return 0;
}

  • 또한 RTTI를 사용할 수 있다면 직접 생성한 클래스와 같은 동적 오브젝트의 확인도 가능하다
// typeid, polymorphic class
#include <iostream>
#include <typeinfo>
#include <exception>
using namespace std;

class Base { virtual void f(){} };
class Derived : public Base {};

int main () {
  try {
    Base* a = new Base;
    Base* b = new Derived;
    cout << "a is: " << typeid(a).name() << '\n'; // a is: class Base *
    cout << "b is: " << typeid(b).name() << '\n'; // b is: class Base *
    cout << "*a is: " << typeid(*a).name() << '\n'; // *a is: class Base
    cout << "*b is: " << typeid(*b).name() << '\n'; // *b is: class Derived
  } catch (exception& e) { cout << "Exception: " << e.what() << '\n'; }
  return 0;
}

마지막으로 역참조 연산자(*)를 가진 null 값을 가진 포인터를 확인한다면 bad_typeid 예외가 발생된다. 


답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다