개발개발/c++

c++의 지역변수, 전역변수, 메모리 영역, static 변수, 동적할당, 함수 오버로딩, 함수의 디폴트 인자

유잉유잉유잉 2024. 12. 8. 06:35
728x90

▶️  지역변수

  • 코드블록 안에서 선언하여 사용하는 변수
  • 함수의 인자도 지역변수 입니다.
  • 함수 종료시 메모리가 제거됩니다.
int main()
{
	// 지역변수
    int num = 0;
    
	return 0;
}

 

▶️  전역변수 

  • 코드블록 바깥에서 선언하여 사용하는 변수
  • 변수명은 앞에 g를 붙여서 전역변수임을 표시합니다.
    ex) int gNum = 1000;
  • 프로그램 시작시 메모리에 생성 됩니다.
    • 컴파일 타임에 메모리 크기와 위치가 정해집니다.
  • 프로그램 종료시 메모리에서 제거 됩니다.
  • 선언한 후 아래 모든 코드에서 전역변수 사용이 가능합니다.
#include <iostream>

// 전역변수
int num = 0;

int main()
{
	return 0;
}
  • 전역변수 단점
    - 메모리 컨트롤이 되지 않아서 의도대로 사용하기 어렵습니다.
    - 편한만큼 구조적으로 문제가 생길가능성이 많습니다.
    • 여기저기에서 사용하기 때문에 문제가 생겼을 때 찾기 어렵습니다.

 

 

 

▶️  c++에서 메모리의 영역

  • 스택 : 지역변수매개변수가 만들어지는 영역.
    • 코드블럭 안에서만 사용되며 코드블럭이 종료될 때, 메모리에서 제거됩니다.
  • 데이터 : 전역변수, 정적변수가 만들어지는 영역. 
    • 정적변수와 전역변수는 초기화값을 대입하지 않은 경우 자동으로 0으로 초기화합니다.
    • 전역변수컴파일 시점에 메모리에 할당되며, 프로그램 종료시 메모리에서 제거됩니다.
    • 정적변수는 호출시(함수 내 정적변수라면 함수 호출시, 객체 내 멤버 정적변수라면 객체 생성시) 메모리에 할당되며, 프로그램 종료시에 메모리에서 제거됩니다.
  • 코드 : 프로그램 실행시 코드가 기계어로 저장되는 영역.
  • 힙 : 동적 할당한 변수의 메모리가 만들어지는 영역.
    • 동적 할당한 변수를 제거하지 않을 경우 메모리릭이 발생합니다.
    • 동적 할당한 변수는 사용하지 않는 시점에 직접 제거해주어야 합니다.

 

 

 

 

▶️ static  변수 

  • 정적 변수 선언
  • 프로그램이 끝날때 까지 메모리가 유지됩니다.
  • 너무 많이 사용할 경우 잉여 메모리가 발생할 수 있습니다.
void Test ()
{
	// static 변수 : 정적 변수 선언
	// 이 변수는 처음 한번만 초기화가 되고
	// 다음부터는 초기화가 무시된다.
	
	// 아래 변수는 정적변수지만 지역변수기도 함.
	// call이 되지 않으면 따로 메모리에 생성되지 않는다.
    
	// 아래에서 해당 함수가 호출되기때문에 컴파일러가 아래 정적변수는 메모리에 할당한다.
	// 만약 함수 호출하는 부분을 주석처리할 경우 해당 변수는 메모리에 할당되지 않는다.
	static int numStatic = 50;
	++numStatic;
	std::cout << "numStatic : " << numStatic << std::endl;

}

int main()
{
	Test (); // numStatic: 51
    Test (); // numStatic: 52 // 초기화는 무시되고 계속 증가
    Test (); // numStatic: 53
    Test (); // numStatic: 54
    
	return 0;
}

 

 

 

 

▶️  동적할당

  • 런타임에 메모리의 생성과 해제를 컨트롤할 수 있는 기능입니다

 

  • c언어 방식 : malloc, free 함수를 이용하여 메모리를 생성하고 해제 합니다.
    클래스의 생성자 호출 X
  • c++ 방식 : new, delete 연산자를 이용하여 메모리를 생성하고 해제합니다
    클래스의 생성자 호출 o

 

  • 구조
    new 할당할타입;
    ex) new int ;
    =>  할당할 타입의 메모리 크기만큼 힙 영역에 메모리를 생성하고 그 주소를 반환합니다.

  • new를 이용하여 동적할당 된 메모리는 
    delete를 이용하여 메모리 제거를 하지 않을 경우
    메모리가 그대로 남아있게 됩니다. 
    이를 메모리 릭 이라고 합니다.

  • delete 메모리주소(포인터) ; 를 이용하여 메모리를 제거해야 합니다.
#include <iostream>

// 이런식으로 사용하는 이유 
// 초기화시 추가적 작업을 일괄적으로 처리하기 위해서
int* CreateDynamicInt ()
{
	return new int;
}

// bool 타입으로 공간할당이 되었는지 아닌지를 판단
bool CreateDynamicInt1 ( int** alloc )
{
	*alloc = new int;
	return true;
}

// 이중포인터를 사용하지 않는다면 동적할당한 주소를 넘겨줄 수 없음
// => 잘못된 포인터의 사용예시
bool CreateDynamicInt2 ( int* alloc )
{
	alloc = new int;
	return true;
}

// 다른 방법 : 포인터 레퍼런스를 사용. 근데 대부분 이중포인터를 사용 
bool CreateDynamicInt3 ( int *& alloc )
{
	alloc = new int;
	return true;
}

int main()
{
	int* numArr = new int;

	*numArr = 500;

	delete numArr;

	// 동적 배열 할당
	int * pArr = new int[10];

	pArr[1] = 300;
	delete[] pArr;

	// 동적할당된 
	numArr = CreateDynamicInt ();
	delete numArr;

	CreateDynamicInt1 ( &numArr );
	delete numArr;

	CreateDynamicInt2 ( numArr );
	delete numArr;

	CreateDynamicInt3 ( numArr );
	delete numArr;

	return 0;
}

 

 


 

 

▶️  함수 오버로딩

  • 같은 이름의 함수를 다른 형태로 여러개 만들어주는 기능입니다.

  • 반환타입은 오버로딩에 영향이 없습니다.
  • 함수의 인자를 이용하여 같은 형태인지 다른 형태인지를 판단할 수 있습니다.
#include <iostream>

void Output ()
{
	std::cout << "" << std::endl;
}

void Output ( int num )
{
	std::cout << num << std::endl;
}

void Output ( float num )
{
	std::cout << num << std::endl;
}

void Output ( int a, int b )
{
	std::cout << a << ", "<< b << std::endl;
}

int main ()
{
	Output ();
	Output ( 33 );
	Output ( 3.3f );
	Output ( 3, 3 );

	return 0;
}

 

▶️  함수의 디폴트 인자

  • 함수의 인자에 기본 값을 설정하는 기능입니다.
  • 기본값을 설정하면
    • 해당 인자에 값을 넘겨줄 경우 디폴트 인자값을 사용하지 않습니다.
    • 인자가 없을 경우 기본값으로 사용합니다.

  • 디폴트인자는 여러개의 기본값을 지정할 수도 있습니다.
  • 가장 오른쪽 인자부터 왼쪽으로 차례대로 디폴트 인자를 지정해야 합니다.
  • 디폴트인자가 있는 함수의 오버로딩시 조심히 사용하여야 합니다.
#include <iostream>

int add ( int a, int b = 100 )
{
	return a + b;
}

// 오버로딩상황에서 디폴트인자를 사용할 경우 어떤게 사용될지 모름
// 잘못된 예시 
/*int add (int a)
{
	return a;
}
*/

//// 잘못된 예시2
//int add ( int a, float num2 = 3.3f )
//{
//
//}

 

 

 


 

▶️  재귀함수

  • 자기 자신을 호출하는 방식입니다.
  • 함수가 호출되며 스택이 계속 쌓입니다

  • 장점 : 코드를 줄일 수 있습니다.
  • 단점 : 너무많은 연산을 할 경우 스택오버플로우가 생깁니다. 
#include <iostream>

int Factorial ( int num )
{
	if (num == 1)
		return 1;

	return num * Factorial(num - 1) ;
}

int main ()
{
	std::cout << Factorial ( 5 ) << std::endl;	// 120 출력 

	return 0;
}

/*
계산 순서
F(5) - N : 5
return 5 * f(4-1);
	F(4) - N:4
	return 4 * F(4-1);
		F(3) - N : 3
		return 3 * f(3-1);
			F(2) - N:2
			return 2 * F(2-1);
				F(1) - N:1
				return 1;
                
*/

 

 

▶️  꼬리재귀

  • 재귀호출이 끝날 때 아무런 작업 없이 바로 결과를 반환하는 방식입니다.
  • 꼬리재귀도 최적화를 해줘야함. 컴파일 입장에선 반복문이기때문에 스택에 쌓이지 않습니다.
    -> 재귀를 할 경우 꼬리재귀로 사용해주길 권장드립니다.

 

#include <iostream>

int FactorialTail( int num, int result = 1 )
{
	if ( num == 1 )
		return result;

	return FactorialTail( num - 1, num * result );
}

int main()
{
	std::cout << FactorialTail ( 5 ) << std::endl; // 120 출력

	return 0;    
}

/*
꼬리재귀 계산 순서
F(5) - N : 5
return FT(1, 5);
    FT(1, 5) - R : 1 N : 5
    return FT( 5*1, 5-1)
        FT(5, 4) - R : 5 N : 4
        return FT(4 * 5, 4-1)
            FT(20, 3) - R : 20 N : 3
            return FT( 20 * 3, 3-1);
                FT(60, 2) - R:60, N : 2
                return FT(60 * 2, 2-1);
                    FT(120, 1) - R:120, N : 1
                    return 120;
*/

 

 

▶️  함수포인터

  • 함수 이름은 해당 함수가 올라가있는 메모리상 주소를 나타냅니다.
  • 함수포인터는 함수의 주소를 저장하기 위한 변수입니다.
  • 함수의 형태는 반환타입과 인자로 결정이 됩니다.

  • 선언방법
    반환타입 (*이름)(인자타입) ;

#include <iostream>

void Output ()
{
	std::cout << "Output Function" << std::endl;
}

// output()과 같은 타입의 함수다
void OutputTest ()
{
	std::cout << "OutputTest Function" << std::endl;
}

int Add ( int num, int num2 )
{
	return num + num2;
}

// add()와 Minus()는 같은 타입의 함수이다
int Minus ( int  num, int num2 )
{
	return num - num2;
}

int main ()
{
	// 함수를 호출한다는것은 함수주소();를 호출해준다는 의미이다.
	Output (); // Output Function 출력

	// 함수의 이름은 해당 함수가 올라가있는 메모리의 주소.
	std::cout << Output << std::endl; // 00007FF65EE110B4

	// 반환형이 없고, 인자도 없는 함수 포인터 선언
	void( *FuncPointer )();
	FuncPointer = Output;

	// FuncPointer 변수는 output()의 주소를 가지고 있다.
	// 그러므로 함수 호출 방법인
	// 함수주소(); 방식으로 호출이 가능하다
	FuncPointer (); // Output Function 출력

	FuncPointer = OutputTest;
	FuncPointer (); // OutputTest Function 출력

	// Add()는 반환타입 int에 int타입 인자 2개가 있다.
	// 그러므로 형태가 달라서 FuncPointer에 주소를 저장할 수 없다.
	//FuncPointer = Add; // [에러메세지] "int (*)(int num, int num2)" 형식의 값을 "void (*)()" 형식의 엔터티에 할당할 수 없습니다.

	int( *FuncInt )( int, int ) = Add;

	std::cout << FuncInt ( 10, 20 ) << std::endl;	// 30 출력

	FuncInt = Minus;

	std::cout << FuncInt ( 30, 10 ) << std::endl; // 20 출력

	// 함수포인터도 변수이기에 배열선언이 가능
	int( *FuncArr[2] )( int, int );
	FuncArr[0] = Add;
	FuncArr[1] = Minus;

	for (int i = 0 ; i < 2 ; i++)
	{
		std::cout << FuncArr[i] ( 5, 3 ) << std::endl;
        /*
        8
        2
        출력
        */
	}

	return 0;
}
728x90