프로그래밍 언어/C & C++ 정리

포인터 & 함수 포인터 & 깊은 복사 & 얕은 복사 & 생성자 & 오버로딩 & explicit & 형변환 연산자 & R-value , L-value

뽀또치즈맛 2023. 6. 12. 20:40

포인터

 

지금까지 변수 선언으로 메모리에 공간을 확보하고, 그곳을 데이터를 넣고 꺼내 쓰는 공간으로 사용했다.

변수명은 그러한 메모리 공간을 식별할 수 있는 이름이었다.

 

그러나 변수는 선언된 블록({}), 함수 내부로 사용이 제한되어 있다.

같은 변수명을 사용했다 하더라도 블록이나 함수가 다르면 별도의 저장 공간을 확보하므로

전혀 다른 변수로 사용되는 것이다.

그래서 사용 범위를 벗어난 경우도 데이터를 공유할 수 있는 새로운 방법이 포인터 개념이다.

 

 

 

메모리의 주소

 

메모리라는 것은 우리가 데이터를 넣고 꺼내 쓰는 공간으로,

그 위치를 식별할 수 있어야 한다.

프로그램은 사용하는 메모리의 위치를 주소 값으로 식별할 수 있다.

 

메모리의 위치를 식별하는 주소 값은 바이트 단우로 구분된다.

이 값은 0부터 시작하고 바이트 단위로 1씩 증가하므로

2바이트 이상의 크기를 갖는 변수는 여러 개의 주소 값에 걸쳐 할당된다.

 

 

 

주소 연산자 : &

 

주소 연산자를 사용하면,

이제 저장된 공간을 이름이 아닌 주소로 사용할 수 있다.

 

여기서 주소라 하면 변수가 할당된 메모리 공간의 시작 주소를 의미한다.

시작 주소를 알면 그 위치부터 변수의 크기만큼 메모리를 사용할 수 있다.

주소는 주소 연산자 &를 사용해서 구한다.

 

 

포인터와 간접 참조 연산자 : *

 

간접 참조 연산자를 사용하면 변수에 할당된 메모리 주소를 활용할 수 있다.

메모리의 주소는 필요할 때마다 계속 주소 연산을 수행하는 것보다

한 번 구한 주소를 저장해서 사용하면 편리하다.

포인터가 바로 변수의 메모리 주소를 저장하는 변수이다.

따라서 주소를 저장할 포인터도 변수처럼 선언하고 사용한다.

다만, 선언할 때 변수 앞에 *만 붙여주면 된다.

 

 

 

const를 사용한 포인터

 

const 예약어를 포인터에 사용하면

이는 가리키는 변수의 값을 바꿀 수 없다는 의미로,

변수에 사용하는 것과는 다른 의미를 가진다.

 

 

함수 포인터

 

포인터가 다른 변수의 주소를 저장하는 변수라는 것을 배웠다.

이와 유사하게 함수 포인터는 함수를 가리키는 변수다.

즉, 함수의 주소를 저장하는 변수다.

 

만약,

int func()
{
	return 1;
}

이라는 함수가 있다면 여기서 int는 l-value 이다.

int func()
{
	return 1;
}

int main()
{
	func();
}

여기서 func()을 std::cout 으로 출력하면 func()의 주소값이 나온다.

 

 

즉 함수 또한 고유의 주소값을 가진다.

 

그럼 함수 또한 포인터를 사용하여 이용할 수 있다.

그럼 결국에는 주소값을 함수 포인터로 다룰 수 있다.

 

 

함수 포인터의 사용 방법은 다음과 같다.

bool compare(int a, int b)
{
	return a == b;
}

int main()
{

	bool (*fp)(int, int); 
    fp = &compare;
    
    // bool (*fp)(int, int) = compare; 도 가능하다.
    
    bool res = (*fp)(2, 3);
}

 

불러올 함수의 자료형 (*fp)(매개 변수의 자료형을 나열) 의 형식으로 되어 있다.

 

여기서 *fp 는 다양한 이름으로 다루어도 되지만 임의로 지정하겠다

 

이후 fp에 해당 함수의 주소값을 넣어주고

 

다른 bool값에 함수의 결과를 넣어주거나 하는 방법으로

함수를 응용할 수 있다.

 

배열을 응용한 함수는 다음과 같이 만든다.

int arrMin(const int arr[], int n)
{
	int min = arr[0];
	for (int i = 1; i < n; i++)
    {
    	if(arr[i] < min)
        {
        	min = arr[i];
        }
    }
    return min;
}


int main()
{
	int arr[7] = { 3, 1, 4, 1, 5, 9, 5};
}

만약 여기서 cout << arrMin(arr, 7) << endl;

을 한다면 return된 min 값인 1이 나와야한다.

 

int square(int x) { return x * x; }
int myFunc(int x) { return x * (x - 15) / 2; }

int arrFnMin(const int arr[], int n, int (*f)(int))
{
	int min = f(arr[0]);
    for (int i = 1; i < n; i++)
    {
    	if (f(arr[i]) < min)
        {
        	min = f(arr[i]);
        }
    }
    
    return min;
}

int main()
{
	int arr[7] = { 3, 1, -4, 1, 5, 9, -2 };
    cout << arrFnMin(arr, 7, square) << endl;
    cout << arrFnMin(arr, 7, myFunc) << endl;
}

 

때문에 함수 포인터를 응용한 함수를 만들어 낼 수 있는데,

위와 같은 식으로 응용할 수 있다.

 

 

 

깊은 복사 & 얕은 복사

 

얕은 복사

 

얕은 복사는 실제 데이터가 아닌 단지 메모리 주소만을 복사하여

변수의 데이터로 만드는 방법을 얕은 복사라 한다.

 

int *a = new int(5);
int *b = new int(3);

a = b // 얕은 복사 (참조만 복사 - 주소만 가져간다)

delete a;
deleta b;

 

깊은 복사

 

변수가 관리하는 리소스 자체를 복사(새롭게 메모리를 할당)하여

새롭게 멤버 변수에 입력시키는 것이다.

얕은 복사에 비해 작업 시간과 리소스의 소모가 따른다.

 

int *a = new int(5);
int *b = new int(3);

*a = *b // 깊은 복사

delete a;
deleta b;

 

생성자

클래스가 멤버를 초기화하는 방법을 사용자 지정하거나 클래스의 개체를 만들 때 함수를 호출하려면 생성자를 정의한다.

생성자는 클래스와 같은 이름을 사용하며 반환 값이 없다. 

다양한 방법으로 초기화를 사용자 지정하는 데 필요한 만큼 오버로드된 생성자를 정의할 수 있다.

일반적으로 생성자는 클래스 정의 또는 상속 계층 구조 외부의 코드가

클래스의 개체를 만들 수 있도록 공용 접근성을 갖는다.

그러나 생성자를 또는 private로 protected 선언할 수도 있다.

 

멤버 이니셜라이저

 

생성자는 필요에 따라 생성자 본문이 실행되기 전에 클래스 멤버를 초기화하는 멤버 이니셜라이저 목록을 가질 수 있다.

멤버 이니셜라이저 목록은 std::initializer_list<T>의 이니셜라이저 목록과 동일하지 않다.

 

    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}

 

식별자는 클래스 멤버를 참조해야하며, 인수 값으로 초기화된다.

인수는 생성자 매개 변수, 함수 호출 또는 std::initializer_list<T> 중 하나일 수 있다.

 

기본 생성자는 일반적으로 매개 변수가 없지만

기본값이 있는 매개 변수를 가질 수있다.

class Box {
public:
    Box() { /*perform any required default initialization steps*/}

    // All params have default values
    Box (int w = 1, int l = 1, int h = 1): m_width(w), m_height(h), m_length(l){}
...
}

 

기본 생성자는 특수 멤버 함수 중 하나이다.

클래스에 선언된 생성자가 없는 경우 컴파일러는 암시적으로 inline 기본 생성자를 제공한다.

 

복사 생성자

 

복사 생성자는 동일한 형식의 개체에서 멤버 값을 복사하여 개체를 초기화한다.

클래스 멤버가 스칼라 값과 같은 모든 간단한 형식인 경우

컴파일러에서 생성된 복사 생성자로 충분하며 사용자 고유를 정의할 필요가 없다.

클래스에 더 복잡한 초기화가 필요한 경우 사용자 지정 복사 생성자를 구현해야 한다.

컴파일러에서 생성된 복사 생성자는 포인터를 복사하기만 하면

새 포인터가 다른 포인터의 메모리 위치를 가리키도록 한다.

 

오버로딩

오버로딩은 같은 이름의 함수에 매개변수를 다르게 사용하여 매개 변수에 따라 다른 함수가 실행되는 것이다.

오버로딩은 함수 중복 정의이다.

 

이와 이름이 비슷한 오버라이딩은

오버라이딩은 상속받았을 때 부모 클래스의 함수를 사용하지 않고

다른 기능을 실행할 때 함수를 자식 클래스에 같은 이름, 매개변수로 재정의해서 사용하는 것이다.

때문에 오버라이딩은 함수 재정의이다.

 

오버로딩은,

메소드 이름이 같아야 한다.

리턴형이 같아도 되고 달라도 된다.

파라미터 개수가 달라야한다.

파라미터 개수가 같을 경우, 자료형이 달라야 한다. 

 

explicit

 

explicit 키워드는 자신이 원하지 않은 형변환이 일어나지 않도록 제한하는 키워드이다.

즉, 묵시적 형변환을 할 수 없게 만들고 명시적인 형변환만 가능하도록 만드는 것이다.

 

  • 명시적 형변환
    Tmp tmp1 = Tmp(1);
  • 묵시적 형변환
    Tmp tmp2 = 10;

 

 

형변환 연산자

C++ 에서는 총 4개의 형변환 연산자를 제공한다.

  • static_cast
  • const_cast
  • dynamic_cast
  • reinterpret_cast

dynamic_cast: 상속관계에서의 안전한 형 반환

다이내믹 캐스트의 경우 상속관계에 놓여 있는 두 클래스 사이에서 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환하는 경우 사용한다.

 

const_cast: const의 성향을 삭제하라!

const로 선언된 char(별) 변수를 매개변수가 char(별) 로 받는 함수에 넣기위해 const_cast를 사용하였다. const int또한 마찬가지 const_cast를 사용하여 인자전달.

 

static_cast : A 타입에서 B 타입으로

static_cast는 dynamic_cast의 특성 뿐만아니라 기초클래스의 포인터 및 참조형 데이터도 유도 클래스의 포인터 및 참조형 데이터로 아무런 조건없이 형 변환시켜준다. 그러나 이에 대한 책임은 프로그래머가 져야 한다.

 

reinterpret_cast: 상관없는 자료형으로의 형 변환

포인터를 대상으로 하는, 그리고 포인터와 관련이 있는 모든 유형의 형 변환을 허용한다.

 

 

R-value, L-value

l-value에는 프로그램이 액세스할 수 있는 주소가 있다.

l-value식의 예로는 변수, 배열 요소, l-value 참조를 반환하는 함수 호출,

비트 필드, 공용체 및 클래스 멤버를 포함한 변수 이름이 있다(const).

 

prvalue 식에는 프로그램에서 액세스할 수 있는 주소가 없다. 

prvalue 식의 예로는 리터럴, 비참조 유형을 반환하는 함수 호출,

식 평가 중에 생성되지만 컴파일러에서만 액세스할 수 있는 임시 개체가 있다.

 

xvalue 식에는 프로그램에서

더 이상 액세스할 수 없는 주소가 있지만

식에 대한 액세스를 제공하는 rvalue 참조를 초기화하는 데 사용할 수 있다. 

예제에는 rvalue 참조를 반환하는 함수 호출, 배열 첨자,

멤버 및 배열 또는 개체가 rvalue 참조인 멤버 식에 대한 포인터가 포함된다.

 

// lvalues_and_rvalues2.cpp
int main()
{
    int i, j, *p;

    // Correct usage: the variable i is an lvalue and the literal 7 is a prvalue.
    i = 7;

    // Incorrect usage: The left operand must be an lvalue (C2106).`j * 4` is a prvalue.
    7 = i; // C2106
    j * 4 = 7; // C2106

    // Correct usage: the dereferenced pointer is an lvalue.
    *p = i;

    // Correct usage: the conditional operator returns an lvalue.
    ((i < 3) ? i : j) = 7;

    // Incorrect usage: the constant ci is a non-modifiable lvalue (C3892).
    const int ci = 7;
    ci = 9; // C3892
}

 

lvalue

 

표현식 이후에도 사라지지 않는 값. 이름을 지니는 변수.

 

rvalue

 

표현식 이후에는 사라지는 값. 임시 변수.

 

 

 

 

 

 

'프로그래밍 언어 > C & C++ 정리' 카테고리의 다른 글

포인터  (0) 2023.10.28
부동소수점  (0) 2023.06.20
템플릿의 특수화  (0) 2023.05.18
복사 생성자 & 복사 대입 연산자 & explicit & extern  (0) 2023.04.27
C++ STL Pair & Map  (0) 2023.04.10