공부/C++

2. 포인터와 레퍼런스

bokob 2024. 3. 30. 18:53

2.1 포인터

변수의 주소

포인터(pointer)

  • 변수의 주소를 나타냄

 

변수의 주소

  • 변수가 포함하고 있는 제일 첫 번째 바이트의 주소
  • 변수의 주소는 변수 이름 앞에 &를 붙여줌. ex) a의 주소는 &a

 

포인터 변수

  • 주소값을 저장하는 변수
  • 포인터라고 약칭

 

포인터 변수의 선언 및 사용

간접 참조 연산자

  • 포인터 변수가 가리키는 실제 변수의 값을 참조
#include <iostream>
using namespace std;

int main()
{
	int n = 10, m;
	char c = 'A';
	double d;

	int* p = &n;    // p는 n의 주소값을 가짐
	char* q = &c;   // q는 c의 주소값을 가짐
	double* r = &d; // r은 d의 주소값을 가짐

	*p = 25; // n에 25가 저장됨
	*q = 'A'; // c에 문자 'A'가 저장됨
	*r = 3.14; // d에 3.14가 저장됨
	m = *p + 10; // p가 가리키는 값(n 변수값)+10을 m에 저장

	cout << n << ' ' << *p << "\n"; // 둘 다 25가 출력됨
	cout << c << ' ' << *q << "\n"; // 둘 다 'A'가 출력됨
	cout << d << ' ' << *r << "\n"; // 둘 다 3.14가 출력됨
	cout << m << "\n"; // m 값 35 출력
}

 

#include <iostream>
using namespace std;

int main()
{
	int arr[6] = { 1,2,3,4,5,6 };
	int* chr_ptr;

	// chr_ptr이 arr 배열이 저장되어 있는 메모리 주소값을 갖도록 초기화
	chr_ptr = arr;

	chr_ptr++;

	cout << chr_ptr << "\n";  // 4 출력
	cout << *chr_ptr << "\n"; // 2 출력
	cout << arr << "\n";      // 0 출력
	cout << arr + 4 << "\n";  // 16 출력
	cout << &arr[3] << "\n";  // 12 출력
	cout << arr[4] << "\n";   // 5 출력
	
	// arr[3] 값을 chr_ptr을 이용해 출력
	cout << *(chr_ptr + 2) << "\n";
	cout << chr_ptr[2] << "\n";
}

 

배열의 원소를 가리키는 포인터

int arrays[10];
int* p = &arrays[5]; // int* p = arrays+5

 

배열과 포인터

배열 이름은 배열 메모리의 시작 주소로 다룸

 

1차원 배열의 이름

1차원 배열의 이름

  • 1차원 배열의 첫 번째 원소의 주소값
  • 포인터 상수(constant): 바뀔 수 없는 값 -> 배열의 메모리 주소가 바뀌지 않음
int nArray[10];
if(nArray == &nArray[0])
{
    // 항상 이곳 실행
}
nArray = &nArray[1]; // 에러
nArray++;            // 에러

 

원소의 주소

  • 주소 연산자 사용
&nArray[2]
  • 배열이름 사용
nArray+2 == &nArray[2]

 

1차원 배열의 이름을 사용해서 배열 탐색

주소를 이용한 원소 참조

int nArray[10];

for(int i=0; i<10; i++)
    *(nArray + i) = i; // nArray[i] = i와 동일

 

2차원 배열과 포인터

2차원 배열의 이름

int M[3][3];
  • 배열의 시작 주소
M = &M[0]
*M == M[0]

M+1 == &M[1]
*(M+1) == &M[1][0]

**(M)   == M[0][0]
**(M+1) == M[1][0]

 

부분 배열의 이름

  • 부분 배열의 시작 주소
M[0] == *M       // M[0]은 0행의 시작주소
m[1] == *(M + 1) // M[1]은 2행의 시작주소

 

  • 포인터 자료형
*(M[1])   == M[1][0] // 값
*(M[1]+1) == M[1][1] // 값
*(M[1]+2) == M[1][2] // 값

 

포인터 배열

포인터 배열은 포인터 변수들을 배열 요소로 갖는 배열

2차원 배열과 포인터 배열

1차원 배열의 배열명을 포인터 배열에 저장하면 포인터 배열을 2차원 배열처럼 사용할 수 있다.

int ary1[4] = {1,2,3,4};
int ary2[4] = {11,12,13,14};
int ary3[4] = {21,22,23,24};
int* ptr_ary[3] = {ary1, ary2, ary3}; // 각 배열명을 포인터 배열에 초기화

 

문자열 포인터 배열

문자열  포인터 배열

  • 많이 사용되는 포인터 배열
  • 문자열들을 효율적으로 저장
  • 문자열 상수는 문자열의 시작주소

 

문자열 포인터 배열과 2차원 문자 배열

초기화

  • 2차원 문자 배열: 모든 문자열이 배열에 복사
  • 문자열 포인터 배열: 주소값만 복사

char s[10] = "dog";
char *p = "dog";

cout<<(s=="dog") // 0 출력
cout<<(p=="dog") // 1 출력

// "dog" 자체가 상수를 관리하는 메모리 영역 (Text 영역)에 있어서 
// 한 번 선언되면 중복해서 선언되지 않는다.
// 따라서 s에 담긴 "dog"을 제외한 나머지 "dog"은 동일한 "dog"이다.

 

원소의 접근 및 변경

  • 2차원 문자 배열
  • 문자열 포인터 배열
// 원소 접근
cout<<(animal[0][1] == 'o');  // 1 
cout<<(animal[0] == "dog");   // 1

cout<<(ptr_ary[0][1] == 'o'); // 1
cout<<(ptr_ary[0] == "dog");  // 1

// 원소 변경
animal[0][1] = 'i';  // "dog"이 "dig"으로 바뀜
ptr_ary[0] = "cat";  // 교체는 가능하다. 주소값을 바꾸는 것이기 때문이다.
ptr_ary[0][1] = 'i'; // 에러, 상수이기 때문에 수정불가

 

이중 포인터

이중 포인터

  • 포인터 변수를 가리키는 포인터 변수

참고) 포인터는 메모리 주소 단위로 결정된다. ex) 32 bit OS에서는 4 byte, 64 bit OS에서는 8 byte

 

배열 포인터

  • 배열을 가리키는 포인터
int arr[2][3] = { {11,12,13}, {21,22,23} };

int(*parr)[3]; // 배열 포인터
int* p; int** pt;

parr = arr;
cout << parr << arr;
cout << parr + 1 << arr + 1;	//  주소, 3 x 4byte 만큼 늘어남, parr+1 == arr+1
cout << *(parr + 1) << parr[1] << *(arr + 1) << arr[1];		 // 주소
cout << **(parr + 1) << **(arr + 1) << *arr[1] << arr[1][0]; // 21

p = arr[0];
cout << p << arr[0] << *arr;					  // p == arr[0] == *arr 각 원소의 시작주소
cout << *(p + 1) << *(arr[0] + 1) << *(*arr + 1); // 12

pt = &p;			// pt = arr; (X)
cout << *pt << p;	// *pt == p
cout << **pt << *p; // **pt == *p
  • 2차원 배열을 포인터로 사용하기 위해서는 배열 포인터로 사용하는 것이 좋다.

 

const와  포인터

포인터 변수에 const 속성을 부여할 때의 고려사항

  • 포인터 변수 자체에 const 속성 부여
  • 포인터 변수가 가리키는 변수에 const 속성 부여

 

포인터 변수에 const 속성을 적용한 3가지 경우를 비교

  • 포인터 변수가 가리키는 변수가 const인 경우 (포인터로 변수 변경할 수는 있지만 값을 변경할 수는 없음)
int i1 = 10;
int i2 = 20;
const int* p = &i1;

p = &i2; // 가능
*p = 30; // 에러

 

  • 포인터 변수 자신이 const인 경우 (포인터 변수를 변경하지 못지만, 값을 변경할 수는 있음)
int i1 = 10;
int i2 = 20;
int* const p = &i1;

p = &i2; // 에러
*p = 30; // 가능

 

두 변수 모두 const인 경우 (포인터 변수, 값 모두 변경 못함)

int i1 = 10;
int i2 = 20;
const int* const p = &i1;

p = &i2; // 에러
*p = 30; // 에러

 

2.2 레퍼런스

레퍼런스의 선언 및 사용

레퍼런스(Reference) 선언

참조자 & 도입

이미 존재하는 변수에 대한 다른 이름 선언

  • 참조 변수는 이름만 생김
  • 참조 변수에 새로운 공간을 할당하지 않음
  • 초기화로 지정된 기존 변수 공유

 

const 레퍼런스

const 레퍼런스

  • 레퍼런스를 선언할 때 const 키워드를 함께 사용하면 레퍼런스가 참조하는 변수의 값을 변경할 수 없다.
  • 레퍼런스는 자신이 참조하는 변수에 읽기 전용의 접근(read-only access)만 가능하다.
int data = 100;
const int &ref = data;

cout << ref; // cout << data;와 같다
ref = 200;   // const 레퍼런스는 변경할 수 없으므로 컴파일 에러
data = 200;  // data를 직접 변경하는 것은 가능

 

참조에 의한 호출

레퍼런스(참조) 변수를 가장 많이 활용하는 사례 : call by reference

함수의 매개 변수를 레퍼런스 타입으로 선언

  • 참조 매개 변수(reference parameter)라고 부름
  • 참조 매개 변수의 이름만 생기고 공간이 생기지 않음
  • 참조 매개 변수는 실인자 변수 공간 공유
  • 참조 매개 변수에 대한 조작은 실인자 변수 조작 효과
int main(){
    int x = 10;
    
    foo(x);
    cout<<"x = "<<x<<"\n;
}

void foo(int &ref){
    ref++; // ref는 x의 별명이므로, main 함수의 x를 증가시킨다.
}

 

2.3 함수에서의 인자 전달

함수의 인자 전달

함수의 인자 전달 방법을 결정하는 기준

  • 함수를 호출할 때 넘겨준 인자를 함수 안에서 사용만 하고 변경하지 않을 때 -> 값에 의한 전달 (call by value)
  • 함수를 호출할 때 넘겨준 인자를 함수 안에서 변경해야 할 때-> 포인터에 의한 전달이나 레퍼런스에 의한 전달 (call by reference)

 

const 레퍼런스의 전달

함수의 인자로 사용할 때의 const 레퍼런스

  • 구조체나 클래스 형을 넘길 때 유용하게 사용된다.
  • 구조체를 인자로 전달할 때 레퍼런스로 전달하면 구조체를 복사하지 않고 별명으로만 전달한다.
#include <string>
using namespace std;

typedef struct STUDENNT {
    string name;
    int grade[4];
}STUDENT;

void Print(const STUDENT& s)
{
    s.grade[0] = 0; // 함수 안에서 s를 변경할 수 없으므로 컴파일 에러
}

int main()
{
    STUDENT s1 = { "엄준식", 100, 90, 80, 99 };
    Print(s1);
}

 

함수의 인자전달 방법 - 포인터

포인터의 타입을 인자로 전달하는 방법의 정리

  • 함수의 매개 변수는 포인터 타입으로 정의한다.
  • 인자를 넘겨줄 때는 결과 값을 담고 싶은 변수의 주소를 넘겨준다.
  • 함수 안에서 결과를 넘겨줄 때는 매개변수가 가리키는 곳에 값을 넣어준다.
void GCD_LCM(int a, int b, int* pgcd, int* plcm)
{
    // ...
    *pgcd = 5; // pgcd는 gcd를 가리키고 있으므로 gcd의 실제 값이 바뀜
}

int main()
{
    // ...
    GCD_LCM(28, 35, &gcd, &lcm);
}

 

함수의 인자전달 방법 - 레퍼런스

레퍼런스 타입을 인자로 전달하는 방법의 정리

  • 함수의 매개 변수는 레퍼런스 타입으로 정의한다.
  • 인자를 넘겨줄 때는 결과 값을 담고 싶은 변수를 그대로 넘겨준다.
  • 함수 안에서 결과를 넘겨줄 때는 매개 변수에 값을 넣어준다.
void GCD_LCM(int a, int b, int& pgcd, int& plcm)
{
    // ...
    pgcd = 5; // pgcd는 gcd의 별명이므로 gcd의 실제 값이 바뀜
}

int main()
{
    // ...
    GCD_LCM(28, 35, gcd, lcm);
}

 

함수의 인자전달 방법 - 1차원 배열

배열을 인자로 전달하는 방법의 원리

  • 배열 전체를 한 번에 보낼 수 없으므로, 배열의 시작 주소를 보냄
  • 참조에 의한 호출(call-by-reference)를 사용하여 전달

 

1차원 배열의 전달

  • 배열의 주소를 넘겨준다
  • 인자로 넘어온 배열을 사용할 때는 그냥 평범한 배열을 사용하듯이 하면 된다.
void UsingArray(int arr[]) 
{            // int arr[3], int *arr 와 같다. 사람에 따라 다르게 의미를 받아들임
    // ...
    cout<<arr[0]<<*(arr+1)<<"\n";
}

int main()
{
    int arr1[3] = {1,2,3};
    
    UsingArray(arr1);
    UsingArray(arr1 + 1);
    UsingArray(&arr1[2]);
}

 

함수의 인자전달 방법 - 2차원 배열

2차원 배열의 전달

  • 1차원 배열의 경우와 같이 배열의 주소 전달
  • 호출된 함수에서는 몇 개의 원소가 있는지 알 수 있도록 함
    • 1차원 원소의 개수는 절대로 생략하지 않는다. -> offset을 뛸 때 사용하기 때문이다.

참고) 2차원 배열의 경우는 이중 포인터로 받아도 됨

void Using2DArray(int arr[][3]){ 
               // int arr[5][3];
    // ...
}

int main()
{
    int array2[5][3] = { {1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}, {13,14,15} };

    Using2DArray(array2);
}

 

참조 리턴

C 언어의 함수 리턴

함수는 반드시 값만 리턴

  • 기본 타입 값: int, char, double 등
  • 포인터 값

 

C++의 함수 리턴

함수는 값 외에 참조 리턴 가능

  • 참조 리턴
    • 변수 등과 같이 현존하는 공간에 대한 참조 리턴
    • 변수의 값을 리턴하는 것이 아님

 

값을 리턴하는 함수 vs. 참조를 리턴하는 함수

 

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

4. 클래스와 객체의 기본  (0) 2024.04.09
3. 개선된 함수 기능  (0) 2024.03.31
1. C++ 프로그래밍의 기본  (2) 2024.03.26
0. C++ 시작  (0) 2024.03.24
C++ 공부  (0) 2024.03.14