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};
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;
동적 할당을 이용한 배열
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 참조
참조는 어떠한 대상을 가리킨다는 점에서 포인터와 유사하지만 몇 가지 차이가 있다.
참조를 이용하여 값을 읽거나 저장할 때는 변수를 사용하는 형식과 동일하다. 포인터처럼 '*'와 같은 연산을 사용하지 않는다.
참조는 초기화를 통해 반드시 어떤 대상을 참조해야만 하므로 아무것도 참조하지 않는 상황은 발생하지 않는다. 포인터는 초기화를 하지 않거나 가리키던 대상을 delete하거나 포인터에 nullptr이 대입됨에 따라 사용하면 안 되는 곳을 가리키거나 아무것도 가리키지 않는 상황이 발생할 수 있다.
참조는 초기화를 통해 지정된 참조 대상을 바꿀 수 없어 참조의 유효기간 동안 하나의 대상만 참조한다. 포인터는 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 참조가 유용하게 활용된다.