The Essentials of C++, David hunter

Chapter 4. Pointer and Reference Types 포인터와 참조 유형

4.1 Pointer Variables 포인터 변수

  • A “pointer” variable is one that contains the address of a storage location. The “” operator is used to declare that a variable is a pointer. For example, the declaration int *p means that p will be a pointer to (i.e., contain the address of) a location at which an integer is stored. Alternately, one can decalre several pointers to integers with the statement int p, q, r, s. This is equivalent to int *p, *q, *r, *s. Once a pointer variable, p has been assigned a value (i.e., made to point to something) the identifier *p is used to reference the location to which p points. Accessing *p is called “dereferencing” the pointer.

  • “포인터(pointer)” 변수는 저장 위치의 주소를 포함하는 변수입니다. “*” 연산자는 변수가 포인터임을 선언하는 데 사용됩니다. 예를 들어, 선언 int p는 p가 정수가 저장된 위치의 주소를 포함하는 포인터가 될 것임을 의미합니다. 또한 int p, q, r, s 문을 사용하여 정수에 대한 여러 포인터를 선언할 수 있습니다. 이는 int *p, *q, *r, *s와 동등합니다. 한번 포인터 변수인 p에 값이 할당되면 (즉, 어떤 것을 가리키도록 만들면) 식별자 *p는 p가 가리키는 위치를 참조하는 데 사용됩니다. *p에 접근하는 것은 포인터를 “역참조(dereferencing)”한다고 합니다.

  • One way to assign a pointer variable a value (which should be an address) is to use the “&” operator, which returns the address of its operand. This is illustrated below.

  • 포인터 변수에 값을 할당하는 한 가지 방법은 “&” 연산자를 사용하는 것입니다. “&” 연산자는 피연산자의 주소를 반환합니다. 이것은 아래에서 설명됩니다.

int *p, i = 15;
p = &i; // p는 i의 주소를 포함합니다.
*p = 20; // p가 가리키는 주소(즉, i)에 20을 저장합니다.
cout << i; // i를 출력합니다.
  • In this example, both *p and i refer to the same storage cell. This situation, in which a storage location can be referred to by more than one identifier, is called “aliasing.”

  • 이 예제에서 *p와 i 모두 동일한 저장 셀을 참조합니다. 이런 상황에서는 저장 위치가 두 개 이상의 식별자로 참조될 수 있는데, 이를 “별칭(aliasing)”이라고 합니다.

  • Pointer variables can also be given values by assigning to them the results of a new operation. The new operator allocates a storage unit for a specified type and returns the address of the allocated unit. Thus, the statement p = new int allocates storage for an integer and puts its address in p. If there is insufficient storage to allocate a variable of the specified type, new returns a value of zero.

  • 포인터 변수는 또한 “new” 연산의 결과를 할당함으로써 값을 받을 수 있습니다. “new” 연산자는 지정된 유형을 위한 저장 단위를 할당하고 할당된 단위의 주소를 반환합니다. 따라서 “p = new int” 문은 정수를 위한 저장 공간을 할당하고 그 주소를 p에 넣습니다. 지정된 유형의 변수를 할당하기에 충분한 저장 공간이 없는 경우, “new”는 값 0을 반환합니다.

    해설

      int *p; // 정수형 포인터 변수 선언
      p = new int; // new 연산자를 사용하여 정수형 변수를 위한 메모리 할당
    


    포인터 변수는 메모리 할당 연산인 “new”를 통해 값을 받을 수 있습니다.
    “new” 연산자는 지정된 데이터 유형을 위한 메모리 저장 공간을 할당하고, 할당된 메모리 블록의 주소를 반환합니다.
    따라서 “p = new int” 문장은 정수를 저장할 메모리 공간을 할당하고, 그 메모리 블록의 주소를 포인터 변수 p에 저장합니다.
    그러나 지정된 데이터 유형을 위한 충분한 메모리 공간이 없는 경우, “new” 연산자는 값 0을 반환합니다. 이는 메모리 할당에 실패했음을 나타냅니다.

  • The reverse of new is delete. The delete operator is used to deallocate storage allocated by new. The statement delete p means “return the storage unit whose address is in p to the pool of available storage units.”

  • “new”의 반대는 “delete”입니다. “delete” 연산자는 “new”로 할당된 저장 공간을 해제하는 데 사용됩니다. “delete p” 문은 “p에 저장된 주소를 사용하여 해당 주소의 저장 단위를 사용 가능한 저장 단위 풀에 반환한다”는 의미를 가집니다.

    해설

      delete p; // 메모리 해제. 메모리 매니저에게 해제하라고 알려주면, 매니저가 다시 사용 가능하다고 표시함.
    
  • Pointer variables which have not yet been made to point to something should be assigned the value 0. It is traditional to use a symbolic constant called NULL to represent this value. Thus, sequences such as the following are frequently seen:

  • 아직 어떤 대상을 가리키지 않는 포인터 변수는 값 0을 할당해야 합니다. 이 값을 나타내기 위해 종종 NULL이라는 상수 심볼을 사용하는 것이 전통적입니다. 따라서 다음과 같은 순서가 자주 나타납니다:
      // c++11 이전 문법
      #define NULL 0
      ...
      void main()
      {
          int *p = NULL, *q = NULL;
          ...
          if (p == NULL) p = new integer;
          ...
      }
    

    해설
    최신 문법

      int *p = nullptr, *q = nullptr;
      if (p == nullptr) {
          p = new int;
      }
      delete p;
    
  • The #define statement is used to define symbolic constants. This statement will be fully explained in a subsequent chapter.

  • #define 문은 상징적인 상수를 정의하는 데 사용됩니다. 이 문장은 이후 장에서 자세히 설명될 것입니다.

  • Pointer variables can be made the subject of arithmetic operations. When used with pointer variables, the increment and decrement operators are indexed according to the size of the data type they point to. That is, the expression ++p is equivalent to p = p + s, where s is the size of the data type pointed to by p. Pointer arithmetic is considered archaic in C++ and is not recommended.

  • 포인터 변수는 산술 연산의 대상이 될 수 있습니다. 포인터 변수와 함께 사용될 때, 증가 및 감소 연산자는 포인터가 가리키는 데이터 유형의 크기에 따라 인덱싱됩니다. 즉, 표현식 ++p는 p = p + s와 동등하며, 여기서 s는 p가 가리키는 데이터 유형의 크기입니다. 포인터 산술은 C++에서 구식으로 간주되며 권장되지 않습니다.

    예시 코드와 해설

      #include <iostream>
    
      int main() {
          int arr[] = {10, 20, 30, 40, 50};
          int *p = arr; // 포인터 변수 p를 배열 arr의 첫 번째 요소에 가리키도록 초기화합니다.
    
          // 배열 요소에 접근하며 증가 연산자를 사용합니다.
          std::cout << "배열 요소 출력:" << std::endl;
          for (int i = 0; i < 5; ++i) {
              std::cout << "arr[" << i << "] = " << *p << std::endl;
              ++p; // 포인터를 다음 배열 요소로 증가시킵니다.
          }
    
          return 0;
      }
    


    포인터 변수는 다른 변수와 마찬가지로 산술 연산의 대상이 될 수 있습니다. 포인터 변수를 사용하여 메모리 내의 위치를 계산하거나 변경할 수 있습니다.
    포인터 변수와 함께 사용될 때, 증가(++) 및 감소(–) 연산자는 포인터가 가리키는 데이터 유형의 크기에 따라 동작합니다. 즉, 포인터 증가 및 감소 연산은 현재 포인터 위치에서 데이터 유형의 크기만큼 이동하게 됩니다.
    예를 들어, 정수를 가리키는 포인터 변수 p가 있고, 이 때 ++p를 실행하면 p가 가리키는 정수 데이터 크기만큼 증가하게 됩니다.
    이러한 포인터 산술은 C++에서는 구식으로 간주되며, 포인터 산술은 주의해서 사용해야 합니다. 배열과 같은 데이터 구조를 다룰 때는 일반적으로 인덱스를 사용하는 것이 더 안전하고 가독성이 좋은 방법입니다.

      #include <iostream>
    
      int main() {
          int arr[] = {10, 20, 30, 40, 50};
    
          // 배열 요소에 접근하며 인덱스를 사용합니다.
          std::cout << "배열 요소 출력:" << std::endl;
          for (int i = 0; i < 5; ++i) {
              std::cout << "arr[" << i << "] = " << arr[i] << std::endl;
          }
    
          return 0;
      }
    

    또한 C++에서는 스마트 포인터와 컨테이너 클래스를 사용하여 포인터 관리를 더 안전하게 할 수 있습니다.

      #include <iostream>
      #include <memory>
      #include <vector>
    
      int main() {
          // std::vector를 사용하여 데이터를 저장합니다.
          std::vector<int> arr = {10, 20, 30, 40, 50};
    
          // std::vector를 사용하여 스마트 포인터를 저장하는 컨테이너를 생성합니다.
          std::vector<std::shared_ptr<int>> pointerContainer;
    
          // 데이터를 스마트 포인터로 래핑하여 컨테이너에 추가합니다.
          for (const auto &elem : arr) {
              pointerContainer.push_back(std::make_shared<int>(elem));
          }
    
          // 컨테이너를 통해 스마트 포인터에 안전하게 접근합니다.
          for (const auto &ptr : pointerContainer) {
              std::cout << "값: " << *ptr << std::endl;
          }
    
          // 스마트 포인터를 명시적으로 삭제하거나 해제할 필요가 없습니다.
          // 스마트 포인터는 자동으로 메모리를 관리합니다.
    
          return 0;
      }
    
  • The operator -> is used to reference the individual fields of a structure that is referenced by a pointer. The expression p->f is equivalent to (*p).f. The use of pointer with structures is illustrated below:

  • 연산자 ->는 포인터로 참조된 구조체의 개별 필드를 참조하는 데 사용됩니다. 표현식 p->f는 (*p).f와 동일합니다. 포인터를 사용한 구조체의 사용 예시는 아래에 나와 있습니다:

      student_record *p;
      p = new student_record;
      p->id = 1013;
      p->classification = 'F';
      // ...
      delete p;
    

    예시 코드와 해설

      #include <iostream>
    
      struct Person {
          std::string name;
          int age;
      };
    
      int main() {
          Person person1;
          person1.name = "Alice";
          person1.age = 30;
    
          // 직접 구조체의 멤버변수에 접근. 구조체 객체가 스택에 할당되어 있을 때 사용
          std::cout << person1.age << std::endl; 
        
          // 포인터를 사용하여 구조체 필드에 접근
          // Person* ptr = &person1;
          Person* ptr; // 구조체의 포인터 변수 선언
          ptr = &person1; // 포인터 변수 ptr을 인스턴스 person1로 초기화
    
          // 포인터 ptr을 사용하여 구조체 person1의 멤버변수 name 접근. '->' 연산자를 사용하여 접근할 수 있다.
          // 구조체에 동적으로 접근하거나, 구조체가 힙에 할당되어 있을 때 사용
          std::cout << "이름: " << ptr->name << ", 나이: " << ptr->age << std::endl;
    
          // 동일한 동작을 하는 코드 (p->f를 (*p).f로 대체)
          std::cout << "이름: " << (*ptr).name << ", 나이: " << (*ptr).age << std::endl;
    
          return 0;
      }
    


    -> 연산자는 포인터로 참조된 구조체클래스의 개별 필드를 참조하기 위해 사용됩니다. 이 연산자를 사용하면 포인터가 가리키는 구조체나 클래스의 멤버에 접근할 수 있습니다.
    p->f 표현식은 (*p).f와 동일한 의미를 가집니다. 즉, 포인터 p가 가리키는 구조체나 클래스의 f 필드에 접근하는 것을 의미합니다.

참고, Stack and Heap
  • 스택(Stack):
    • LIFO (Last-In, First-Out): 스택은 LIFO 방식을 따릅니다. 즉, 가장 최근에 추가된 데이터가 가장 먼저 제거됩니다.
    • 정적 할당: 스택은 컴파일 시간에 크기가 결정되며, 고정된 크기를 가집니다. 함수 호출 및 로컬 변수와 같은 상대적으로 작은 데이터 구조를 저장하는 데 사용됩니다.
    • 빠른 접근: 스택은 상대적으로 빠른 접근 시간을 가집니다. 데이터의 추가와 제거가 상수 시간(O(1))에 이루어집니다.
    • 스레드 별 스택: 각 스레드는 별도의 스택을 가지며, 스레드 간에 스택은 독립적입니다.
  • 힙(Heap):
    • 동적 할당: 힙은 런타임에 크기가 결정되며, 동적으로 할당된 데이터를 저장하는 데 사용됩니다. 메모리 할당 및 해제는 프로그래머에게 달려 있어야 합니다.
    • 무제한 크기: 힙은 거의 무제한한 크기의 메모리를 가질 수 있습니다. 시스템의 가용 메모리에 따라 동적으로 확장됩니다.
    • 느린 접근: 힙은 스택보다 접근 시간이 더 느립니다. 메모리 할당과 해제는 일반적으로 선형 시간(O(n)) 또는 상수 시간(O(1))이 아닐 수 있습니다.
    • 공유 가능: 여러 스레드 또는 프로세스가 동일한 힙 공간을 공유할 수 있으므로 데이터를 공유하는 데 사용됩니다.
  • 주의할 점은, 스택과 힙은 각각 다른 용도와 제한 사항을 가지고 있으며, 올바른 상황에서 적절하게 사용해야 합니다. 예를 들어, 작은 데이터 구조 또는 로컬 변수는 스택에 할당하고, 대규모 데이터나 동적 크기가 필요한 데이터 구조는 힙에 할당하는 것이 일반적인 사용 사례입니다.

4.2 Dynamic Arrays 동적 배열

  • Pointers and be used to implement arrays whose size is not known until runtime. These are known as dynamically allocated arrays. The methods is illustrated below.
  • 포인터는 런타임 시점까지 크기를 알 수 없는 배열을 구현하는 데 사용될 수 있습니다. 이러한 배열은 동적으로 할당된 배열로 알려져 있습니다. 이 방법은 아래에 설명되어 있습니다.

추가 코드 및 해설
array a의 사이즈를 2로 할당해 두었는데, 사용자가 4를 입력하면, 런타임 오류 stack smashing 발생한다.

#include <iostream>

int main()
{
    int a[2], size;
    std::cout << "How many data points are there? ";
    std::cin >> size;
    for (int i = 0; i < size; ++i) {
        std::cin >> a[i];
        std::cout << "a[i]: " << a[i] << std::endl;
    }
    // stack smashing detected: terminated.
    // Aborted (core dumped)
    return 0;
}
#include <iostream>

int main() {
    int *a, size;
    std::cout << "How many data points are there? ";
    std::cin >> size;
    a = new int[size]; // 포인터 변수 a에 메모리를 동적으로 할당한다. 초기값으로 사용자 지정값을 받는다.
    for (int i = 0; i < size; ++i) 
        std::cin >> a[i];
    delete[] a;
    return 0;
}
  • The declaration a[] is equivalent to writing *a, thus the declaration could have been written int a[].

  • 선언 a[]는 *a를 작성하는 것과 동일하며, 선언은 int a[]로 작성할 수 있었습니다.

    추가해설

      int *a // 포인터로 선언. 여기선 동적 메모리에 할당
      int a[5] // 배열로 선언. 스택 메모리에 할당
    
      a[i] // 접근은 어디서든 가능하다.
    
  • The [] operator can be used to index any pointer variable. Given that p is a pointer, the expression p[n] returns the address p+n*s, where s is the size of the data type pointed to by p.

  • [] 연산자는 모든 포인터 변수에 적용할 수 있습니다. 포인터 p가 주어졌을 때, 표현식 p[n]은 주소 p+n*s를 반환합니다. 여기서 s는 p가 가리키는 데이터 유형의 크기입니다.

    추가 코드 및 해설

      #include <iostream>
    
      int main() {
          int *a, size;
          std::cout << "How many data points are there? ";
          std::cin >> size;
          a = new int[size];
          for (int i = 0; i < size; ++i) {
              std::cin >> a[i];
              std::cout << "a[i]: " << a[i] << std::endl;
              std::cout << "&a[i]: " << &a[i] <<std::endl;
          }
    
          delete[] a; 
          return 0;
      }
    

    출력: 주소는 4 bytes씩 증가했다

      How many data points are there? 3
      11
      a[i]: 11
      &a[i]: 0x562fafd406d0
      23
      a[i]: 23
      &a[i]: 0x562fafd406d4
      56
      a[i]: 56
      &a[i]: 0x562fafd406d8
    

4.3 Reference Variables

  • Like pointers, reference variables contain the address of a memory cell. Reference variables are declared as type& var1 = var2. They must be given an initial value when declared, and the value must be something which has an address, not a constant or an expression. The effect of the declaration is to make var1 and alias for var2. The association between a reference variable and the variable it aliases cannt be changed. The example below illustrates the workings of reference variables.

  • 포인터와 마찬가지로 참조 변수(reference variables)는 메모리 셀의 주소를 포함합니다. 참조 변수는 type& var1 = var2와 같은 형태로 선언됩니다.

    type& var1 = var2
    

    참조 변수를 선언할 때 반드시 초기값을 할당해야 하며, 이 값은 주소를 가지는 것이어야 하며 상수나 표현식일 수 없습니다. 이 선언의 효과는 var1을 var2의 별칭(alias)으로 만드는 것입니다. 참조 변수와 그가 가리키는 변수 간의 연결은 변경할 수 없습니다. 아래의 예제는 참조 변수의 작동 방식을 설명합니다.

#include <iostream>
using namespace std;

int main() {
    int i = 15;
    int& p = i; // declare p as an alias for i
    p = 20; // put 20 in the storage cell whose address is in p (that is, i)
    cout << i; // 20 is printed
    int j = 50;
    p = j; // put the value of j (50) in the cell whose address is in p
    cout << i; // 50 is printed
    return 0;
}
  • One cannot create arrays of references, nor pointers to references.

  • 참조 변수의 배열을 생성하거나 참조에 대한 포인터를 만들 수 없습니다.

해설
참조 변수는 이미 다른 변수에 별칭(alias)으로 연결되어 있으며, 참조 자체가 다른 변수를 가리키는 것이므로 배열 요소로 사용하거나 참조에 대한 포인터를 만드는 것은 논리적으로 어려워서 허용되지 않습니다. 참조 변수는 이미 하나의 변수와 연결되어 있으므로 별도의 배열이나 포인터로 사용할 필요가 없습니다.

추가 설명
참조 변수(reference variable)는 C++에서 유용하게 사용되는 개념 중 하나이며, 특히 함수 매개변수로 전달할 때나 함수의 반환값으로 활용될 때 주로 사용됩니다. 참조 변수는 다음과 같은 장점을 갖습니다:

메모리 효율성: 참조 변수는 포인터와 달리 별도의 메모리 공간을 차지하지 않습니다. 따라서 메모리 사용량을 줄일 수 있습니다.
객체의 변경: 함수에서 객체를 참조로 전달하면 원본 객체를 수정할 수 있습니다. 이는 함수 호출을 통해 객체를 복사하는 것보다 빠르고 메모리 효율적입니다.
간결한 문법: 포인터를 사용하는 것보다 간결한 문법을 제공하며 코드를 더 읽기 쉽게 만듭니다.
오류 방지: 참조 변수는 포인터와 달리 널 포인터(NULL pointer)로 초기화되지 않으므로 널 포인터로 인한 오류가 발생하지 않습니다.
참조 반환: 함수에서 참조를 반환하는 경우 함수의 결과를 다른 변수에 할당하거나 수정할 수 있으므로 연속적인 함수 호출이 가능하며, 연산자 체이닝과 같은 디자인 패턴을 구현할 수 있습니다.

참조 변수는 특히 객체 지향 프로그래밍에서 클래스와 함께 사용되며, 코드의 가독성과 유지보수성을 향상시키는 데 도움을 줍니다. 그러나 참조 변수를 적절하게 사용해야 하며, 오용하면 코드를 이해하기 어려워질 수 있으므로 주의가 필요합니다.

추가 코드
참조 변수를 활용하여 객체를 변경하는 예시

#include <iostream>
#include <string>

class Person {
public:
    std::string name;
    int age;

    Person(const std::string& n, int a) : name(n), age(a) {}

    void introduce() {
        std::cout << "My name is " << name << " and I am " << age << " years old." << std::endl;
    }
};

int main() {
    Person person1("Alice", 30);
    Person person2("Bob", 25);

    std::cout << "Before modification:" << std::endl;
    person1.introduce();
    person2.introduce();

    // 참조 변수를 사용하여 객체 수정
    Person& ref = person1;
    ref.name = "Alicia";
    ref.age = 31;

    std::cout << "\nAfter modification:" << std::endl;
    person1.introduce();
    person2.introduce();

    return 0;
}

4.4 Recursive Structures 재귀 구조

  • A recursive data type is one which makes a reference to itself. For example, a linked list is a recursive data structure since each element of the list contains a pointer to the next element in the list. The example below illustrates how to define a linked list of character values in C++.

  • 재귀 데이터 타입은 자기 자신을 참조하는 데이터 타입입니다. 예를 들어, 연결 리스트는 각 리스트 요소가 리스트의 다음 요소를 가리키는 포인터를 포함하기 때문에 재귀적인 데이터 구조입니다. 아래 예제는 C++에서 문자 값들의 연결 리스트를 정의하는 방법을 보여줍니다.

struct ListCell; // forward declaration
struct ListCell {
    char contents;
    ListCell *next;
};
  • Because the definition of a ListCell contains a pointer to a ListCell, it is necessary to precede the definition with a “forward decalration.” This prevents the compiler from generating an unknown identifier error when the declaration ListCell *next() is encountered (since ListCell would otherwise be an undeclared identifier at that point).

  • ListCell의 정의가 ListCell을 가리키는 포인터를 포함하고 있기 때문에 “forward declaration(전방 선언)”을 사용하여 정의를 먼저 해야 합니다. 이로써 컴파일러가 ListCell *next() 선언을 만났을 때 알 수 없는 식별자 오류를 발생시키지 않도록 합니다. 그렇지 않으면 해당 지점에서 ListCell이 선언되지 않은 식별자가 됩니다.