C++ 함수 선언과 정의 (Forward Declaration)

1. Forward Declaration(전방 선언)이란?

• 함수의 정의(implementation) 없이, 함수의 이름 매개변수 타입만 선언하는 것. 

• 예: int add(int a, int b);

 

함수를 정의하는곳이 main함수보다 아래의 있을경우 컴파일러는 해당함수가 main함수내에서 사용될때 무슨함수인지를 모른다.

프로그래밍은 기본적으로 위에서 아래로 실행되기때문에  main함수보다 아래에 정의되어있는 함수들은 인식하지못한다

 

하지만 위에다가 모든정의를 적게되면 사실상 코드가 난잡해질수있으니 사용되는 함수들의 정의 즉, 함수의 기능을전부다 적은 코드는 아래의 적고 #include와같이 전처리기들은 보통 코드파일 맨위에적는데 그와같이 정의된 함수들의 리턴타입,파리미터정도를 적은 함수의 모양을 "선언" 해준다 (declaration) 근데 이것을 코드의 앞에다가 하기때문에 전방선언 (forward declaration)이라고 한다.

 

2. 코드 구조 예시

#include <iostream>
using namespace std;

// 전방 선언
int add(int a, int b);

int main() {
    cout << add(1, 2) << endl;  // 함수 호출
    return 0;
}

// 함수 정의
int add(int a, int b) {
    return a + b;
}

3. Forward Declaration의 필요성

• 함수 정의를 main() 함수 이후에 작성할 수 있게 해줌.

• 코드 가독성과 유지보수성 향상.

추가 정리: 헤더 파일 활용

• #include <iostream>: 입출력 기능 사용.

• using namespace std: 네임스페이스 생략으로 간결한 코드 작성.

lvalue : 식이 끝난 후에도 계속 존재하는 객체를 가리킵니다. 메모리 상에 실제로 존재하며, 주소를 가질 수 있는 값입니다. 예를 들어, 변수는 lvalue입니다.

rvalue : 식이 끝나면 없어지는 임시 객체를 가리킵니다. 이 값은 메모리 상에 실제 위치를 가지지 않을 수 있습니다.

다음과 같은 코드를 보면 이해가 쉽습니다.

cpp

int x = 10;// x는 lvalue, 10은 rvalue
int y = x;// y와 x 모두 lvalue

그럼, 왜 함수의 리턴값은 rvalue인가요?

메소드나 함수가 값을 반환하면, 그 값은 일반적으로 함수 호출이 끝나면 사라지는 임시 값입니다. 그래서 이런 반환값을 rvalue라고 부릅니다. 예를 들어,

cpp

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

int main() {
    int sum = add(3, 4);// add(3, 4)의 반환값은 rvalue
}

하지만, lvalue 참조를 반환하는 함수는 lvalue를 반환할 수 있습니다. 이런 함수는 주로 객체의 상태를 변경하는 멤버 함수에서 볼 수 있습니다.

cpp

class MyClass {
    int value;
public:
    int& get() { return value; }
};

MyClass obj;
obj.get() = 10;// get()의 반환값은 lvalue

이렇게 lvalue 참조를 반환하는 경우는 예외적인 상황이며, 일반적으로 함수나 메소드의 반환값은 rvalue입니다.

동적메모리할당이란?

int a = 10;
...
/* 프로그램 작동중 */
...
{
	int b = 20; 
}
...
/* 프로그램 작동중 */
...

만약 이렇게 코드를 작성한다고한다면 변수 a와 b는 컴퓨터 메모리의 어느한곳을 할당 받게 된다.

블록안에 있거나 함수안에 있는 지역변수들은 해당 블록이 끝나면 사라지게되며, 전역변수는 프로그램이 시작되고 생성이 되며 프로그램이 종료될때 소멸된다.

하지만 필요에 따라서는 메모리에 할당한후 그 기억공간이 필요없다면 미리없어져도 될수있어야한다.

필요에따라서 메모리에 할당도 할수있어야하며 필요에따라서 반환도 해야한다

이것을 동적 메모리할당 ( Dynamic memory allocation)이라고 한다.

그런데 동적 메모리할당으로 생선된 저장공간은 변수처럼 이름이없다. 그래서 이름을통해 액세스 할수없다.

동적으로 할당한곳을 포인터변수가 가리키게 하면 그 포인터를 이용해서 액세스 할수있다.

new : 동적으로 메모리를 할당하는 연산자

1. ptrVar = new TypeName;
2. ptrVar = new TypeName[n];

1번 형식은 지정된 자료형의 데이터 1개를 저장할수있는 공간을 할당하며,

2번 형식은 지정된 자료형의 데이터 n개를 저장할수있는 배열을 할당한다.

❗이때 지정된 자료형은 포인터의 자료형과 일치해야한다.

delete : 동적으로 메모리를 반납하는 연산자

1. delete ptrVar;
2. delete [] ptrVar;

1번형식은 데이터1개를 반환하며

2번형식은 데이터n개가 할당된 배열을 반환한다

❗new로 만든 형식 그대로 delete해주면된다.

//동적으로 할당하는방법
int *intPtr;
intPtr = new int;
...
//동적으로 반환하는방법
delete intPtr;
intPtr = nullptr;

delete연산자를 이용 해당 공간을 시스템에 반환하여도 intPtr포인터는 여전히 그공간을 가리키고있다.

반드시 intPtr = nullptr; 이라는 명령어로 가리키고있는 곳이 없음을 명시해주어야 합니다.

추가설명

delete 연산자는 동적으로 할당된 메모리를 메모리 풀로 반환합니다. 그러나 delete는 포인터 변수 자체를 수정하거나 변경하지 않습니다. 즉, delete는 메모리를 해제하지만 포인터 변수가 그 메모리를 가리키는 것을 그대로 둡니다. 이를 '댕글링 포인터(dangling pointer)'라고 합니다.

댕글링 포인터는 실제적으로 메모리를 가리키고 있지만, 그 메모리는 이미 해제되어 프로그램에서 사용할 수 없는 상태입니다. 댕글링 포인터를 통해 메모리에 접근하려고 하면 예측할 수 없는 결과를 초래할 수 있습니다. 이는 메모리 누수와 함께 동적 메모리 관리에서 가장 주의해야 할 문제 중 하나입니다.

따라서 메모리를 해제한 후에는 포인터를 nullptr로 설정하여 댕글링 포인터를 방지하는 것이 좋습니다. nullptr는 포인터가 어떤 메모리도 가리키고 있지 않음을 나타내는 특별한 값입니다. 이렇게 하면 포인터가 더 이상 유효하지 않은 메모리를 가리키지 않도록 할 수 있습니다.

 

const는 상수를 설정하는것으로 아주 얕게 알고 있었고 단순히 값이 변하지않는다는것으로 알고있었습니다.

javascript에서는 const 를 이용해서 단순히 값을 안바뀌는 변수 정도로만 생각했는데 C++ const 한정어는 여러가지의 사용방법이있는거 같습니다.


C와 C++언어에서는 포인터에대한 const 한정어가 허용됩니다. 이때 const한정어를 넣는 위치에 따라서도 의미가 달라지기도합니다.

const int a = 10;

위와 같은 문장이 있을경우 해당 문장은 저는 이렇게 해석하면 쉽게 이해가 됐습니다.

바뀌지않는 정수 a = 10;

역시 영어보단 한글이 더 직관적이죠?

C++언어에서는 변수를 선언하고 초기화하는 한문장을 만들때 데이터타입 변수명 ; 이런형식으로 선언합니다.

const 한정어는 초기화를 해주어야 합니다.물론 대입연산자 “=” 를 활용하여 해당하는 데이터타입을 선언과 동시에 할당하여 초기화를 하죠

이제 a 는 바뀌지않는 값 10을 가진 변수가 되었습니다.


여기서 조금더 어려운 개념인 const 한정어가 사용되는 포인터와 주소값의 예를 들어보겠습니다.

int a =10 , b =20;  //a,b에 각각 int형 10으로 초기화 해주었습니다.
const int *ptr = &a; // 포인터변수 ptr은 a의 주소를 가리킵니다.
*ptr = 30; // error!!
ptr = &b; // ok!!

위의 문장에서 ptr은 const int * 로 정의 되어있습니다 이는 ptr이 const int에 대한 포인터라는 의미 입니다.

비록 a가 const로 지정되지는 않았지만 ptr이 상수에 대한 포인터라고 지정했으므로 *ptr의 값을 수정하는것은 허용하지 않습니다. 하지만 주소를 변경하는 다음문장은되는게 이상하죠?

*즉, const int *ptr = &a;에서 ptr는 'const int'를 가리키는 포인터이므로, ptr을 통해 가리키는 값(ptr)은 변경할 수 없습니다. 그래서 *ptr = 30;은 오류를 발생시킵니다.

그러나 ptr 자체, 즉 ptr가 가리키는 주소는 const로 지정되지 않았습니다. 따라서 ptr는 다른 주소를 가리킬 수 있습니다. 그래서 ptr = &b;는 허용됩니다.

주소를 변경하는것을 막으려면 const 의 위치를 바꾸어 줘야 합니다.

int a =10 , b =20;  //a,b에 각각 int형 10으로 초기화 해주었습니다.
 int * const ptr = &a; // 포인터변수 ptr은 a의 주소를 가리킵니다.
*ptr = 30; // ok!!
ptr = &b; // error!!

const 한정어 const int *ptr 이 경우, ptr은 const int를 가리키는 포인터입니다. 따라서 ptr을 통해 가리키는 값(*ptr)은 변경할 수 없습니다. 그러나 ptr 자체, 즉 ptr이 가리키는 주소는 변경 가능합니다.int * const ptr: 이 경우, ptr은 const 포인터입니다. 즉, ptr이 가리키는 주소 자체가 변경할 수 없습니다. 그러나 ptr을 통해 가리키는 값(*ptr)은 변경 가능합니다.

const int *ptr = &a;  // 값을 변경할수없음
int * const ptr = &b; // 주소를 변경할수없음

const한정어의 위치에따라 포인터는 다르게 작동하는것을 반드시 알고 넘어 가야 할것 같습니다.

저또한 공부를 하는입장이지만 이렇게 글을 남김으로써 더욱 이해가 잘되고 다음에 다시 볼때도 편하겠네요

 

연산자 다중정의란 C++에 정의된 연산자가 사용자가 선언한 클래스의 객체에 대하여 사용할수있도록 정의하는것

⚠️연산자 다중정의주의사항

연산자의 의미를 바꾸지않는다

연산자의 고유한특성이 유지되도록한다.

  • 우선순위,피연산자의수는 불변 해야 한다
  • 전위표기와 후위표기 연산자의 의미는 유지 해야 한다.

연산자 다중정의 대상

클래스의 객체 간 대입 및 이동 대입 연산자

*특히 동적할당을 받는 포인터를 포함하는경우 고려해야함

수치형 객체의 산술 연산자 다중정의

*교환법칙도 함께 고려함

두 객체를 비교하기 위한 관계 연산자의 다중정의

스트림 입력을 및 출력을 위한 <<,>>연산자

⚠️다중정의를 할수없는 연산자

멤버 선택 연산자 ( . )

맴버 포인터 연산자 ( .* )

유효범위 연산자 ( :: )

조건 연산자 ( ? : )

연산자 다중정의위치

  • 클래스의 멤버로 정의하는방법
    • 연산자의 구현 과정에서 객체의멤버를 액세스 할수있음
  • 클래스외부에서 정의하는방법
    • 클래스의 멤버가 아니므로, 객체의 private맴버는 직접 사용할수없음
    →필요하다면 private멤버를 엑세스할수있는 방법을 마련해야함

단항 연산자의 다중정의

피연산자가 1개인 연산자

전위표기 다중정의 형식

//전위 표기 다중정의형식
Returntype ClassName::opertor opSymbol()
{
	.....
}

예:전위표기 ++ 연산자의 다중정의

class IntClass1{
	int a;
public:
	IntClass1(int n=0;):a(n) {};
	IntClass & operator ++ (){
		++a;
		return *this;
	}
	int getValue () const {
		return a;
	}
}

전위 표기 연산자는 반환값이 “참조형 자신” 이다

매개변수는 없다.


후위표기 다중정의 형식

//후위 표기 다중정의형식
Returntype ClassName::opertor opSymbol(int)
{
	.....
}

❗후위표기는 파라미터로 ‘int’를 받는다 인수전달의 의미가 아니라 후위표기법을 사용하는 단항 연산자이다

예:후위표기 ++ 연산자의 다중정의

class IntClass2{
	int a;
	public:
 IntClass2(int n=0;) : a(0){};
 IntClass2 operator ++(int){
	Intclass2 tmp(*this);
		++a;
		return tmp
	};
 };

후위표기법에서는 객체를 반환하며

매개변수는 “int” 이다


이항 연산자의 다중정의

이항 연산자 다중정의 형식

ReturnClasss ClassName::operator opSymbol(ArgClass arg)
{
	....
}
  • opSymbol : + - * / && || 등의 이항 연산자 기호
  • 객체 자신이 좌측 피연산자, arg가 우측 피연산자에 해당됨

→ a + b 라면 a가 객체자신 arg가 b임

예: 복소수객체 complex20bj 로 풀이

복소수 객체 + 복소수 객체

수식: complex20bj1 + complex20bj2

Complex2 Complex2::operator + (const Complex2 &c)const {
	Complex2 tmp(*this);
tmp.rPart += c.rPart;
tmp.iPart += c.iPart;
return tmp;
};

자신(*this) 객체를 복사한 tmp에 더할 c 객체에 r.Part , i.Part 를 더한 tmp를 반환해준다.

복소수 객체+ 실수

수식 : complex20bj + 10.0

Complex2 Complex2::operator + (double r)const {
return Complex2(rPart + r, iPart);
};

double값이 묵시적형변환으로 복소수 + 복소수 로 처리됨

실수+ 복소수 객체

좌측피연산자가 실수이므로 C++에서 미리 정해둔 데이터이기에 연산자를 다중정의할수없음

→클래스에 속하지 않는 외부의 별도 연산자로 정의함

수식 : 10.0 + conplex20bj

Complex2 operator + (double r, const Complex2 &c){
	retrun Complex2(r + c.rPart,c.iPart);
};

매개변수를 두개를 받음 좌측(첫번쨰)매개변수는 실수 , 우측 매개변수는 복소수 객체이다

⚠️하지만 위의 방식은 c.rPrat , c.iPrat 는 private멤버를 사용했기떄문에 오류가 발생한다 !!!

첫번째 해결책 접근가능 메소드만들기

Complex2에 preivate 멤버를 액세스할수있는 맴버할수를 만든다

// 클래스를 정의할때 private 멤버를 접근할수있는 메소드를 만든다
class Complex2{
	double rPart,iPart;
public:
	double real() const {return rPart;}
	double imag() const {return iPart;}
};

// 사용시에는 메소드를 불러사용할수 있다.
Complex2 operator + (double r, const Complex2 &c){
	retrun Complex2(r + c.real(),c.imag() );
}

하지만 이렇게 사용하게되면 메소드를 일일이 설저해주어야 하고

클래스의 캡슐화즉 정보은닉의 의미가 조금 사라지게된다

두번째 해결책 friend

// 연산자 다중정의시 friend를 선언해준다
class Complex2{
	double rPart,iPart;
public:
		.....
friend Comp lex2 operator + (double r, Complex2 &c);

// friend로 선언된 다중정의 연산자는 private멤버에 접근할수있다.
Complex2 operator + (double r, const Complex2 &c){
	retrun Complex2(r + c.rPart(),c.iPart() );
}

friend로 선언된 다중정의 연산자는 private멤버에 접근할수 있다

함수를 호출하는 과정에는 인수를 전달하고 함수의 위치로 분기하며 결과를 반환하고 호출한 위치로 되돌아오는 동작이 수반된다. 이에 따른 처리시간 및 코드의 증가는 비록 미미한 것이지만, 떄로는 매우 빠른 처리가 필요하여 불필요한 시간 지연을 피하고 싶을 때가 있다.

inline 함수

함수가 가지는 모듈화의 장점을 살리면서 이러한 불필요한 실행 효율 저하를 막기위해 사용할수있inline함수는 작성하는것은 inline키워드를 사용하는것 외에는 일반함수와 동일하다. 그러나 컴파일러가 번역할때에는 일반 함수와는 달리 함수의 처리문장이 호출되는 위치에 직접 삽입된다


#include <iostream>
using namespace std;

inline vo id SwapValues(int &x,int &y){
	int temp = x;
	x=y;
	y=temp;
}

<aside> 💡 inline으로 선언하더라도 반드시 Inline으로 번역되는것은아님

</aside>

  1. 함수가 너무 큰경우
  2. 순환 호출
  3. 프로그램내에서 그함수에 대한 포인터를 사용하는 경우

C++언어는 기본적으로 C언어에 클래스 가 추가되어진 형태로 개발되었습니다.

실제로 C++의 초기이름은 C with Classes로 명명 되었으며, 1983년 이르러 C++로 이름이 바뀌었습니다

구조화된 프로그래밍의 등장

컴퓨터프로그래밍 역사의 초창기에 구조화된 프로그래밍언어들이 등장하였습니다. 구조화된 프로그래밍에서는 프로그램의 제어구조를세가지방법으로 제어하였습니다.

  1. 순차제어
  2. 조건제어
  3. 반복제어

구조화 프로그래밍 에 따라 알고리즘을  구현하게되면 프로그램의 흐름이 앞에서뒤로 "순차적" 으로 진행되는 단순한 형태로 만들어지므로 

오류가 발생하거나 유지,보수등을 할필요가있을때 프로그램을 보다 쉽게 이해하고 수정 할수가있습니다.

 

그러나 프로램의규모가 커지고 점점더 복잡해짐에 따라 구조화 프로그래밍 기법은 큰 두가지 문제점이 발견되었습니다.

첫째. 함수와 데이터가 분리되어있다.

프로그램의 요소들은 실세계에 존재하는 대상을 표현합니다.

이 대상물은 자기의 상태를 표현하는 속성(데이터)을 가지고 있고 정해진 기능(함수)을 수행할수있습니다.

따라서 이를 모델링 하려면 데이터나 함수 어느한 가지가 아니라 두가지가 함께 표현되는것이 바람직 합니다.

그러나 구조화 프로그래밍에서는 업무의 프로세스에 중점을 두고있어 대상의 업무절차에 따라 작업을 수행하는 일련의  함수들을 만들며

데이터는 이러한 함수들을 사용하여 처리할 대상이 됩니다. 

그결과 함수와 데이터가 분리되어 표현되므로 실세계의 대상을 모델링하는 능력은 부족하게됩니다.

 

둘째. 전역변수를 모든함수에서 제한없이 사용할수있는점점

이상적인 함수는 함수에 전달된 데이터를 적절하게 처리하여 결과를 반환하는 것이 가장 이상적이며
지역변수를사용하여 서로 독립된 기능을 수행해야합니다.

 

하지만 전역변수를 함수에서 사용하게되면 데이터의 불변성을 해칠수가있으며 함수끼리의 연관성이 생기게됩니다.

함수가 독립적으로 기능을 수행하여야하는데 전역변수를 통하여 함수끼리 연관성이 생기며

이는 프로그램을 유지,보수,수정등을 더욱 어렵게 합니다.

 

이외에도 여러가지 문제점이있습니다 재사용성이 낮으며 확장성이 낮고 소프트웨어 개발이 일반 공산품에 비해 생산성이 낮은원인은 한번설계한 부품을 재사용하거나 기존설계를 수정및 보완 하기가 어려워 지기 때문입니다.

객체지향 언어

객체지향 언어의 시작은 1967년 발표된 Simula 67이다

이름 조차 시뮬라67인 이 언어는 시뮬레이션(simulation)을 위하여 개발된 언어입니다.

시뮬레이션이란?

현실세계의 존재하는 여러가지 개체와 이들이 상호작용하는 과정을 컴퓨터를 통해 가상으로 수행하는것 입니다.

 

이처럼 객체지향언어는 태생부터가 시뮬레이션과 아주 밀접한 관계가있는 개념입니다.

분석의 출발이 어떤 프로세스를 통해 작업을 수행하는가? 가 아닌

현실세계를 시뮬레이션하는 가상세계 안에 어떠한 개체들이 존재하는가 이다.

 

추상화(abstraction)

현실세계에 존재하는 개체들을 컴퓨터로 표현하는 방법

현실세계에 존재하는 개체를 컴퓨터로 표현해야합니다. 하지만 현실세계에있는 모든 세세한 사항까지 있는 그대로 컴퓨터로 표현할수는없습니다. 그래서 핵심적이고 고유한것들을 뽑아내어 모델링을 합니다.

현실세계의 개체와 완전히 같지는 않지만 프로그램에서 필요로하는 개체를 표현할수가있습니다. 이렇게 모델링하는것을 추상화 라고합니다.

 

객체지향프로그래밍에서는 이렇게 시뮬레이션된  대상체에 데이터와 행위들을 결합하여둔것을 가장기본개념인 객체(Object)라고하고

객체가 가지고있는 데이터를 속성(attribute), 행위를 구현한것이 메소드(method) 라고 합니다. 

 

객체의 속성은 그 객체의 상태를 나타내는 데이터가 되며,

객체의 메소드는 내부의 데이터를 사용하여 정해진 동작을 수행하는 함수입니다.

그래서 메소드는 함수라고하는것입니다.

 

이렇게 객체지향방식에서는 관련된 데이터와 함수들이 하나로 묶여 객체로 표현되고

이 객체들이 상호작용하도록 연결이 형성되므로 구조화방식의 설계에 비해 구성요소들 사이의 연결관계가 단순해집니다.

 

객체지향방식에서의 연결은 구조화 방식에서의 연결에 비해 느슨한 연결관계가 형성됩니다. 
느슨한 연결이란것은 한 구성요소의 수정이 다른구성요소에 큰영향을 미치지 않게 된다는것을 의미합니다. 왜냐하면

객체를 통한 캡슐화(encapsulation)가 이루어지고 있기 때문이죠

 

캡슐화는 객체의 사용자측면과 설계자측면을 분리하는것을 뜻합니다.

 

사용자 측면 에서는

객체를 사용하는 사용자는 어떻게 사용하는지에대한 정보가 필요하며 세부적인 객체의 상태 기능의 구현 등 세부적으로 알필요가 없습니다.

사용자의 입장에서는 객체를 사용하는방법만 알면되며

 

설계자 측면 에서는

객체가  동작하기위해 세부적인 구현부분을 정의해야하며 객체의 세부적인 내용을 변경 조작 할수가있어야합니다.

 

즉 캡슐화는 이와같이 객체의 내부의 구현에대한 사항은 감추고 외부로는 공개된 메소드를 통해 인터페이스를 제공하여 객체를 사용할수있게 만드는것을 의미합니다.

이와같이 캡슐화된 객체 내부의 세부사항을 사용자가 직접 사용할 수 없도록 감추는데 이를 정보은닉(information hiding)이라고 합니다.

 

 

 캡슐화된 텔레비전 :   텔레비전사용자는텔레비전의 상세한 기판 재료 기술등을 알필요가없다.

사용자는 전원버튼 채널버튼만 필요하다.

+ Recent posts