devseop08 님의 블로그

[Basic] 배열, 포인터, 참조 본문

Language/C++

[Basic] 배열, 포인터, 참조

devseop08 2025. 6. 5. 16:24

1. 배열

  • 동일한 타입의 데이터를 저장할 수 있는 공간을 연속적으로 여러 개 묶어 하나의 이름을 갖는 변수로 만들고,
    각각의 원소를 첨자로 지정하는 것
  • 복합 데이터 타입(compound data type)
  • 개별 요소들에 직접적인 접근
  • 특징
    • 고정된 길이
    • 연속된 메모리 주소에 저장
    • 인덱스를 통해 접근 가능
    • 인덱스는 0부터 시작, 마지막 인덱스는 size - 1
    • out of bound 체크를 하지 않는다.
    • 초기화가 필요하다.
    • 효율적인 데이터 구조
    • 요소를 빠르게 읽는데 유리
  • 배열 선언과 초기화
int scores[5];

  • 배열 길이를 const 상수로 정의할 수 있다.
const int CONSTANT_VALUE = 5;

int scores[CONSTANT_VALUE];
  • 배열 길이를 const 상수가 아닌 변수로 정의하는 것은 불가하다.
int variable = 5;

int scores[variable]; // ERROR
  • 배열 요소 초기화는 선언과 동시에 이뤄져야 한다.
int scores[5] = {100, 200, 300, 400, 500};
  • 초기화 이후엔 배열 요소에 하나씩 접근하여 대입하는 것만 가능하다.
int scores[5];

scores[5] = {100, 200, 300, 400, 500}; // ERROR

scores[0] = 100; // OK
scores[1] = 200; // OK
scores[2] = 300; // OK
scores[3] = 400; // OK
scores[4] = 500; // OK
  • 대입 연산을 생략하여 배열 초기화
int scores[5]{100, 200, 300, 400, 500};
  • 2차원 배열
int arr2D[3][2]{{10,11},{20,21},{30,21}};

2. 포인터

포인터 사용

  • 포인터는 변수의 타입 중 하나
  • 포인터 변수의 값은 메모리의 주소값
  • 포인터 변수는 자신에게 저장된 주소값에 해당하는 메모리의 데이터 타입을 알아야 한다.
  • 포인터 사용의 이유
    • 동적 할당을 위해 힙 영역의 메모리를 사용
    • 변수의 범위 문제로 접근할 수 없는 곳의 데이터를 사용
    • 배열의 효율적인 사용
    • 다형성은 포인터를 기반으로 구현됨
    • 시스템 응용 프로그램/ 임베디드 프로그래밍에서는 메모리에 직접 접근이 필요함
  • 포인터 변수의 정의
    • 기존 변수 뒤에 "*"를 붙여 포인터 변수 정의
    • 초기화 방법
    • int* intPtr = nullptr; double* doublePtr = nullptr;
    • 초기화를 하지 않으면 쓰레기 값이 들어있는 상태이므로 쓰레기 방지를 위한 초기화가 필요
    • nullptr은 "nowhere" 개념 => 임의의 메모리 주소를 가리키는 상태가 아니라, 아무것도 가리키지 않는 상태를 의미
  • 변수의 주소값 얻어오기
    • 포인터 변수는 주소값을 저장하므로, 주소값을 얻어올 수 있어야 함.
    • 이를 위해 주소 연산자 ("&")를 사용
      • 연산자가 적용되는 피연산자의 주소값이 반환됨, 즉 변수의 주소값이 반환됨
      • 피연산자는 주소값을 얻을 수 있는 종류여야 한다(l-value)
    int num = 10;
    
    cout << "Value : " << num << endl;
    cout << "Address : " << &num << endl;
    cout << "Address : " << &10 << endl;  // ERROR : 10은 주소 연산 불가
  • 주소값의 이해
    • 포인터의 변수의 크기포인터가 가리키고 있는 대상의 크기 는 별개이다.
    • 포인터 변수들은 모두 같은 크기
      • X86에서는 4byte
      • 포인터는 "주소값"을 저장하기 때문이다.
    • 타입은 왜 필요한가?
      • 해당 주소의 값에 접근할 때 몇 바이트 크기인지 알아야 한다.
      • 컴파일러는 포인터가 가리키는 타입이 맞는지 확인한다.
    int score1 = 10;
    double score2 = 10.7;
    
    int* scorePtr = nullptr;
    scorePtr = &score1;
    scorePtr = &score2;  // ERROR: COMPILER ERROR    
  • 포인터의 역참조
    • 포인터의 주소에 저장된 데이터에 접근
    • '*' 연산자를 사용
int score = 10;
int* scorePtr = &score;

cout << *scorePtr << endl; // 10

*scorePtr = 20; // 포인터 역참조 연산

cout << *scorePtr << endl; // 20
cout << score << endl; // 20
double highTemp = 100.7;
double lowTemp = 37.4;
double* tempPtr = &highTemp;

cout << *tempPtr << endl; // 100.7
tempPtr = &lowTemp;
cout << *tempPrt << endl; // 37.4
  • 구조체 밎 객체의 포인터
    • 기본 자료형 뿐만 아니라 구조체나 객체에 대한 포인터도 사용할 수 있다.
struct Strct { double x, y;};

Strct temp{10.0,20.0};
Strct* ptr = &temp;

(*ptr).x = 30.0; // '.' 연산이 역참조 연산(*)보다 우선시 되기 때문에 () 필요
(*ptr).y = 40.0; // '.' 연산이 역참조 연산(*)보다 우선시 되기 때문에 () 필요

ptr -> x = 50.0; // (*ptr).x와 동일
ptr -> y = 60.0; // (*ptr).y와 동일
  • const 한정어와 포인터
    • 포인터에도 const 한정어를 사용할 수 있다.
    • 이 때 const 한정어의 사용 위치에 따라 의미가 달라진다
    • const 한정어가 포인터 타입 앞에 위치하는 경우 => 해당 포인터 변수에 대한 역참조를 통해 포인터 변수가 가리키는 데이터의 값을 변경시키는 것이 불가하지만 , 해당 포인터 변수에 저장된 주소값을 변경하는 것은 가능하다.
    int a = 10, b = 20; 
    const int* ptr = &a; 
    *ptr = 30 // ERROR 
    ptr = &b // OK
    • const 한정어가 포인터 타입 뒤에 위치하는 경우 => 해당 포인터 변수에 대한 역참조를 통해 포인터 변수가 가리키는 데이터의 값을 변경시키는 것이 가능하지만, 해당 포인터 변수에 저장된 주소값을 변경하는 것은 불가하다.
    int a = 10, b = 20; 
    int* const ptr = &a; 
    *ptr = 30; // OK 
    ptr = &b; // ERROR
    • const 한정어를 사용한 변수에 대한 포인터 변수는 포인터 타입 앞에 위치한 const로 선언돼야 한다.
    int a = 10; 
    const int b = 20;
    int* ptr1 = &a; // OK 
    int* ptr2 = &b; // ERROR 
    const int* ptr3 = &b // OK

메모리 할당 및 반환

  • 동적 메모리 할당
    • 때에 따라 필요할 때 기억 공간을 할당하고 더 이상 그 공간이 필요하지 않으면 반환할 수 있어야 한다.
    • 런타임힙 메모리를 할당
      • 프로그램 실행 도중 얼마나 많은 메모리가 필요한지 미리 알 수 없는 경우
      • 즉, 데이터의 크기가 동적으로 결정되는 경우(다이나믹)
        • 사용자의 입력에 따라 크기가 변하는 경우
        • 파일을 선택하여 내용을 읽어오는 경우
      • 큰 데이터를 저장해야 할 경우(stack은 크기가 작음, 몇 MB정도 )
      • 객체의 생애 주기(언제 메모리가 할당되고 해제되어야 할지)를 직접 제어하는 경우
    • 힙 메모리는 스택과는 달리 스스로 해제되지 않는다 => 사용이 끝나고 해제해주지 않으면 메모리 누수 발생
  • 동적 메모리 할당 방법
    • 동적으로 할당된 저장공간을 포인터 변수가 가리키게 하면 그 포인터를 이용하여 동적 할당 공간에 액세스 할 수 있다.
    • new 연산자와 delete 연산자 사용
      • new 연산자의 역할 = C 언어의 malloc 함수와 동일(Heap 메모리 공간에 원하는 타입의 메모리 공간을 할당받은 뒤 그것의 주소값을 반환)
      • delete 연산자의 역할 = C 언어의 free 함수와 동일(해당 메모리 공간의 데이터를 삭제하고 반납)
      int* intPtr = nullptr; 
      intPtr = new int; // int 타입의 메모리 공간 힙 메모리에 할당 후 주소 반환 
      cout << intPtr << endl; 
      cout << *intPtr << endl; // garbage value 
      *intPtr = 100; 
      cout << *intPtr << endl; // 100 
      delete intPtr; // free 
      intPtr = nullPtr;
    • 동적 할당을 이용한 배열
      • new[], delete[] 연산자 이용
      int* arrayPtr = nullptr; 
      int size = 0; 
      cout << "size of array? : "; cin >> size; 
      arrayPtr = new int[size]; 
      arrayPtr[0] = 10; 
      arrayPtr[1] = 20; 
      arrayPtr[2] = 30; 
      delete[] arrayPtr;

배열과 포인터

  • 배열 이름은 배열의 첫 요소의 주소를 가리킨다.
  • 포인터 변수의 값은 주소값
  • 포인터 변수와 배열이 같은 주소값을 가진다면, 포인터 변수와 배열은 동일하게 사용 가능하다.
  • 단, 배열은 주소값을 정의한 후 다른 주소로 변경이 불가하고 sizeof 연산 시 배열 첫 요소로 저장된 값의 메모리 크기 혹은 배열 첫 요소가 저장된 메모리의 주소값의 크기가 아닌 배열 전체 크기를 계산한다는 점이 포인터 변수와의 차이이다.
int scores[] = { 100, 200, 300 };

cout << scores << endl;   // 주소값
cout << *scores << endl;  // 100

cout << scores[1] << endl;  // 200
cout << scores[2] << endl;  // 300

cout << *(scores + 1) << endl;  // 200
cout << *(scores + 2) << endl;  // 300

int* scorePtr = scores;   // 중요!!!

cout << scorePtr << endl;   // 주소값
cout << *scorePtr << endl;  // 100

cout << scorePtr[1] << endl;  // 200
cout << scorePtr[2] << endl;  // 300

cout << *(scorePtr + 1) << endl;  // 200
cout << *(scorePtr + 2) << endl;  // 300

3. 참조

  • 참조는 포인터와 유사한 개념
  • 어떠한 변수에 대한 참조는 그 변수의 별명
  • 참조는 '&' 기호를 이용하여 다음과 같은 형식으로 선언한다.
Type &refVar = variable;
  • 참조는 초기화를 통해 참조 대상을 지정해야 한다.
  • 위의 형식에서 refVar를 variable로 초기화한 것은 refVar에 variable의 저장값을 넣은 게 아니라 refVar가 variable를 참조하도록 지정한 것이다. => 앞으로 refVar를 사용하는 것은 variable를 사용하는 것과 동일한 결과를 낸다.
int a=10, b=20;
int &aRef = a; // aRef에 a에 저장된 값을 넣는 게 아니라 aRef는 변수 a에 대한 참조라는 뜻
cout << aRef << endl; // 변수 a를 출력하는 것과 동일
aRef = 100; // 변수 a에 100을 저장하는 것과 동일
aRef = b; // 변수 a에 변수 b에 저장된 값 20을 저장하는 것과 동일
  • 참조자가 어떤 변수를 참조하도록 초기화하는 것은 참조를 선언할 때에만 가능하며 이후에는 참조 위치를 바꿀 수 없음을 기억하라

포인터 vs 참조

  • 참조는 어떠한 대상을 가리킨다는 점에서 포인터와 유사하지만 몇 가지 차이가 있다.
      1. 참조를 이용하여 값을 읽거나 저장할 때는 변수를 사용하는 형식과 동일하다. 포인터처럼 '*'와 같은 연산을 사용하지 않는다.
      1. 참조는 초기화를 통해 반드시 어떤 대상을 참조해야만 하므로 아무것도 참조하지 않는 상황은 발생하지 않는다. 포인터는 초기화를 하지 않거나 가리키던 대상을 delete하거나 포인터에 nullptr이 대입됨에 따라 사용하면 안 되는 곳을 가리키거나 아무것도 가리키지 않는 상황이 발생할 수 있다.
      1. 참조는 초기화를 통해 지정된 참조 대상을 바꿀 수 없어 참조의 유효기간 동안 하나의 대상만 참조한다. 포인터는 const 한정어를 지정하지 않는 이상 프로그램 동작 중에 다른 대상을 가리키도록 변경할 수 있다.

l-value 참조와 r-value 참조

  • C++11에서 참조는 l-value 참조와 r-value 참조로 구분한다.
  • l-value 참조는 값을 저장할 수 있는 대상을 참조하기 위한 참조이다.
  • 만약 l-value 참조를 사용하되, 그 참조를 통해 참조 대상의 값을 바꾸지 못하게 하려면 const 한정어를 지정해야 한다.
int x{ 10 };
const int &xRef = x;
cout << xRef << endl;
xRef += 10; // error: const 참조로 값을 수정할 수 없음
  • r-value 참조는 값을 사용한 후에는 그 값을 더 이상 가지고 있을 필요가 없는 대상을 참조한다.
  • 객체의 값을 다른 객체로 이동하는 용도에 r-value 참조가 유용하게 활용된다.

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

[Basic] 상수와 리터럴  (0) 2025.07.07
[OOP] 생성자와 소멸자  (1) 2025.06.08
[Basic] 타입과 변수  (0) 2025.06.07
[OOP] 클래스 선언과 객체 정의  (0) 2025.06.06
[Basic] C++ 프로그램 작성 및 빌드  (0) 2025.06.05