read

C++


computer

한동안 블로그 작업을 못하다가 오랜만에 다시 적기 시작했다. 이전 포스팅에서는 node js의 개론과 함께 간단한 성질들을 알아 보았다. 이 후 Node js를 계속 진행 해야 하지만, 정리해야 할 것들이 좀 있어서 C++ 먼저 적어 놓기로 했다.

C++을 안잡은지 오래됬다는 게 조금 부끄럽기도 하고, 앞으로 내가 살아가려면 C++ 없이는 힘들겟구나 라는 생각이 들어 이번기회에 먼저 기초부터 책 한권을 다지고, 여러가지 책을 읽어볼 생각이다.


시작 전에..


내가 이번에 C++을 다시 시작하기로 한 이후로 (사실, 개인적으로 맨토라고 생각하시는 분의 영향이 컸다.) 책을 추천 받은 것은 윤성우 저열혈강의 C++ 프로그래밍이다.

기본적으로 C의 내용을 알고 있다는 것을 전재로 하고 시작하는데, 나 역시 모르는 내용은 아니니 잘 정리해서 넘어가려 한다. (실제로 앞에 약 30페이지 정도 넘기다 보니 오버로딩이 나온다. 여기에 이책에 반했다.)


Start


첫 부분 부터 이 책에서는 입출력 방식에 대해 printf 와 scanf를 대신한다라고 적어두었다. 즉, 기본적으로 다 안다고 하고 시작하므로 만일 C를 모른다면 한번정도는 공부하고 보는게 훨씬 편할 것이다.

#include <iostream>


int main() {
	int val1;
	std::cout << "hello world!" << std::endl;
	std::cin>>val1;
    std::cout << "입력값은? " << val1 << std::endl;
    return 0;
}

위 코드예시가 가장 맨 처음 나오는 데 만족 스러웠던 것은 stdnamespace로 쓰지 않았다는 점.. 대부분의 책들이 namespace를 일단 사용하고 쓰는데 반해 그렇지 않다는 점을 가장 첫 예시로 말해주는 게 마음에 들었다.

헤더 iostreamstd, cout, cin, endl, << 기호 등의 모습을 보여주었고 자세한 내용은 후에 접근 하기로 하였지만 이전 C와 다른 점에 대해서 하나씩 알아 가고 있다.

그리고 출력에 있어서 따로 %s,%d와 같은 기호를 쓰지 않아도 된다는 장점을 함께 기술하고 있다.

이후 지역변수의 선언, 문자열의 입력등의 기초적인 내용을 다루고 있기에 넘어가도록 한다.


함수 오버로딩(Function Overloading)


01Chapter 의 2번째로 다루는 것이 함수 오버로딩이다. 대부분 기초 책과는 좀 다르다는 점을 알 수 있다.

먼저 함수 오버로딩에 대해 C와 비교하며 이해해보자. 먼저 C 언어에서는 동일한 이름의 함수가 정의되는 것을 허용하지 않는다.

int myFunc(int num){
	num++;
    retunr num;
}

int myFunc(int a, int b){
    retunr a+b;
}

위와 같이 한다면 C에서는 오류가 난다. 그러나 함수 파라미터에 따라 차이를 주어서 함수 구분이 가능하기 때문에 두가지를 다 허용해도 괜찮지 않을까 라는 의문이 든다. 이를 C++에서는 허용을 하였고, 이를 함수 오버로딩(Function Overloading)이라고 한다.

위와 같이 C와 C++의 차이는 함수 호출시에 차이 때문인데, C++은 함수의 이름, 파라미터 두가지를 보고 호출을 한다면, C는 함수의 이름만 가지고 호출하기 때문이다.

그럼 함수 오버로딩의 예를 보도록 하자.

int myFunc(char c){...}
int myFunc(int n){...}
int myFunc(int a, int b){...}

위와 같을 경우가 오버로딩이 가능한 조건이고 파라미터의 자료형 또는 갯수가 다르다는 조건이다. 그러나

void myFunc(char c){...}
int myFunc(char c){...}

위와 같이 함수의 반환형이 다르다면 함수 호출의 기준에 의해 구분이 되지 않기 때문에 오버로딩이 불가능 하다.


Parameter Default value


다음으로 알아 볼 것은 파라미터의 디폴트 값이다. 디폴트 값이란 기본적으로 설정되어 있는 값을 의미한다.

파라미터에 설정하는 디폴트 값은 무엇인지 보도록 하자.

int MyFuncOne(int num=7){
	return num+1;
}

int MyFuncTwo(int num1=5, int num2=7){
	return num1+num2;
}

여기서

MyFuncOne의 파라미터 선언은 int num=7; 이다. 이것은 다음과 같은 의미를 지닌다. 함수호출 시 인자를 전달하지 않으면 7이 전달된 것으로 간주하겠다. 즉, MyFuncOne(); 과 MyFuncOne(7); 은 완전히 동일하게 된다. 따라서 MyFuncTwo(); 와 MyFuncTwo(5,7); 역시 동일하다는 것을 알 수 있다.

다음 예시를 보면 또다른 상황에 대한 답이 될 수 있다.

#include <iostream>

int Adder(int num1=1, int num2=2){

	return num1+num2;
	
}

int main() {
	
	std::cout<<Adder()<<std::endl;
	std::cout<<Adder(5)<<std::endl;
	std::cout<<Adder(3,5)<<std::endl;
	
	return 0;
}

위 코드의 출력은 3,7,8이 나오며, 두번째 출력을 보면 파라미터가 2개지만 하나만 들어왔을 때, 앞의 값은 입력이 되고 2번째 파라미터는 디폴트로 됨을 볼 수 있다. 즉 왼쪽부터 채워져 나간다.

위 디폴트 값의 선언은 당연히 함수의 선언 부분에 표현하면 된다.

#include <iostream>

int Adder(int num1=1, int num2=2);

int main() {
	
	std::cout<<Adder()<<std::endl;
	std::cout<<Adder(5)<<std::endl;
	std::cout<<Adder(3,5)<<std::endl;
	
	return 0;
}

int Adder(int num1, int num2){
	return num1+num2;
}

위 처럼 처음 선언시에 디폴트값을 넣어주면 된다. 밑에 넣으면 왜 안되는 지는 어찌보면 당연한데, 만약 선언시에 디폴트가 없다면, main에서는 어떻게 확인할 수 있을지에 대해 생각해보면 답이 나온다.

또한 부분적으로 디폴트값을 지정해 줄 수도 있는데, 위에 말 한 함수에 전달되는 파라미터가 왼쪽부터 오른쪽으로 채워지기 때문에

int Adder(int num1=1, int num2=2, int num3=3);
//Adder() == Adder(1,2,3)
int Adder(int num1, int num2=2, int num3=3);
//Adder(1) == Adder(1,2,3);
int Adder(int num1, int num2, int num3=3);
//Adder(1,2) == Adder(1,2,3);
int Adder(int num1=1, int num2, int num3);
//오류

위와 같은 예시들을 보면 오른쪽부터 디폴트값이 선언되지 않은 4번째 Adder함수는 오류가 남을 알 수 있다.

마지막으로 이책에서 문제가 하나 주어진다.

int SimpleFunc(int a=10){
	return a+1;
}

int SimpleFunc(void){
	return 10;
}

위 예시의 함수 오버로딩에는 문제가 있는데 어떠한 문제일까?

이런 문제가 주어졌다.

당연히 Default 값때문이다. 실행문을 보자.

#include <iostream>
 
int SimpleFunc(int a=10){
	return a+1;
}
 
int SimpleFunc(void){
	return 10;
}
int main() {
 
	std::cout << SimpleFunc() << std::endl;
	std::cout << SimpleFunc(5) << std::endl;
 
	return 0;
}

오류가 나고 오류는 overloading 이 모호하다고 한다. 디폴트값 때문이라는 것을 알 수 있다.


인라인 함수


먼저 inline 함수의 이름을 보면 “in”은 내부, “line”은 프로그램 코드라인을 의미 한다 즉, 프로그램 코드라인 안으로 들어가 버린 함수라는 뜻이 된다.

사실, 그냥 이렇게 봤을 때는 모를 수 있다. 일반 함수와 무슨 차이지? 라는 생각도 들수 있다고 생각한다.

C언어의 매크로 함수를 보겠다. 이 책에서도 안다고 생각하고 말하는데, 매크로와 일반 함수의 장단점을 보면 함수는 실행파일의 크기를 줄여주고 코드의 재사용성을 높여주지만 실행속도가 늦어지는 단점이 있다. 하지만, 매크로 함수는 실행파일의 크기가 커지고 스택프레임을 사용할 수 없기 때문에 재귀호출구조를 만들수 없다는 단점은 있지만 실행속도가 증가하는 장점이 있다. 즉, 매크로는 전처리 과정에서 실행되므로 단순히 코드량을 늘려서 실행을 한다고 보면된다. 말그대로 복사붙이기다.

#include <iostream>
#define SQUARE(x) ((x)*(x)) 
 
int main() {
 
	std::cout << SQUARE(5) << std::endl;
	return 0;
}

위 예제가 매크로 함수인데 사실 좀 만들기 어려울 수 있다. 복잡할 때도 있다. 그럼 일반 함수처럼 정의하게 만들자 해서 나온것이 inline 함수다.

#include <iostream>

inline int SQUARE(int x){
	return x*x;
}
 
int main() {
 
	std::cout << SQUARE(5) << std::endl;
	return 0;
}

이렇게 한다면 일반 함수형식으로 inline 함수가 구현되었다.

이곳에 들어가 보면 어셈블리어로 어떻게 달라지는 지 설명이 나와 있다.

중요한 부분은 매크로의 경우 함수의 인라인화가 전처리기에 의해 처리되지만, 키워드 inline을 이용한 함수의 인라인화는 컴파일러에 의해 처리가 된다. 따라서 컴파일러가 inline 사용에 대해 성능에 해가 된다거나 필요하다고 느낄 때 이 키워드를 무시하거나 추가해주기도 한다.

그런데 매크로의 장점이 인라인함수에는 적용 안되는 것이 있다. 매크로는 변수 타입에 영향을 받지 않는다. 그러나 inline 함수는 파라미터의 타입이 정해지므로 영향을 받을 수 밖에 없다. 그렇다고 오버로딩을 한다면 한번만 정의해서 완성시킨다는 장점과 동떨어지게 된다. 그래서 C++에서는 템플릿을 이용해서 자료형에 의존적이지 않는 함수를 만든다.

#include <iostream>

template <typename T>
inline T SQUARE(T x){
	return x*x;
}
 
int main() {
 
	std::cout << SQUARE(5) << std::endl;
	return 0;
}

템플릿은 추후에 정리할 것이고 위와 같이 될 수 있다는 것을 알고 넘어가도록 하겠다.


namespace


computer

namespace는 이름을 붙여놓은 공간이라고 보면 쉽다. 특정 영역에 이름을 붙여주기 위한 문법적 요소 이다.

즉, 여러 사람이 만든 코드로 협업을 한다 할 때, 함수명과 같은 경우 겹칠 위험이 있으므로 앞에 이름을 하나 더 붙여준다고 보면 된다.

#include <iostream>

void SimpleFunc(void){
	std::cout << "BestCom이 정의한 함수" << std::endl;
}

void SimpleFunc(void){
	std::cout << "ProCom이 정의한 함수" << std::endl;
}
 
int main() {
 
	SimpleFunc();
    return 0;
}

위 예시는 당연하게 오류가 난다. BestCom과 ProCom이 같은 이름으로 정의했다는 것을 가정한 코드다. 그러나 namespace를 지정해주면 조금 달라진다.

#include <iostream>

namespace BestComImp1{
	void SimpleFunc(void){
		std::cout << "BestCom이 정의한 함수" << std::endl;
	}
}

namespace ProComImp1{
    void SimpleFunc(void){
        std::cout << "ProCom이 정의한 함수" << std::endl;
    }
}
 
int main() {
 
	BestComImp1::SimpleFunc();
    ProComImp1::SimpleFunc();
    return 0;
}

위는 namespace를 선언한 모습이다. 이처럼 ~의 SimpleFunc() 이라고 선언한 모양이 되므로 겹치지 않게 된다. 이 때, :: 연산자를 범위지정 연산자(scope resolution operator)라고 하고, 이름공간을 지정할 때 사용하는 연산자이다.

이제 namespace를 기반으로한 함수 선언과 정의를 구분해보자.

일반적으로 선언과 정의를 분리하는 데, 함수의 선언은 헤더파일에, 정의는 소스파일에 저장하는 것이 보통이다. 그럼 다음 예시를 보자.

#include <iostream>

namespace BestComImp1{
	void SimpleFunc(void);
}

namespace ProgComImp1{
    void SimpleFunc(void);
}
 
int main() {
 
	BestComImp1::SimpleFunc();
    ProgComImp1::SimpleFunc();
    return 0;
}

void BestComImp1::SimpleFunc(void){
	std::cout << "BestCom이 정의한 함수" << std::endl;
}

void ProgComImp1::SimpleFunc(void){
	std::cout << "ProgCom이 정의한 함수" << std::endl;
}

그리고 참고로 동일한 이름공간에 정의된 함수를 호출할 때에는 굳이 이름공간을 명시할 필요없다. 밑 예제를 보도록 하자.

#include <iostream>

namespace BestComImp1{
	void SimpleFunc(void);
}

namespace BestComImp1{
	void PrettyFunc(void);
}

namespace ProgComImp1{
    void SimpleFunc(void);
}
 
int main() {
 
	BestComImp1::SimpleFunc();
    return 0;
}

void BestComImp1::SimpleFunc(void){
	std::cout << "BestCom이 정의한 함수" << std::endl;
	PrettyFunc();
	ProgComImp1::SimpleFunc();
}

void BestComImp1::PrettyFunc(void){
	std::cout << "So Pretty!!" << std::endl;
}

void ProgComImp1::SimpleFunc(void){
	std::cout << "ProgCom이 정의한 함수" << std::endl;
}

물론 BestComImp1이라는 namespace를 위와 같이 따로 선언할 수도 있다. 보통은 같이 선언을 해둔다.

이 다음은 namespace의 중첩에 대해 알아보자.

namespace Parent{
	int num=2;
    
    namespace SubOne{
    	int num=3;
    }
    
    namespace SubTwo{
    	int num=4;
    }
}

namespace는 또 다른 namespace가 내부에 들어갈 수 있다. 아마 어떻게 될지는 바로 알 수 있을 것이다.

출력 역시 마찬가지로,

std::cout << Parent::num << std::endl;
std::cout << Parent::SubOne::num << std::endl;
std::cout << Parent::SubTwo::num << std::endl;

로 될 것이라는 것을 알 수 있다.

이정도면 충분히 문법적인 의미를 다 알 수 있을 것이다.

이제 std 에 대해서 조금 알아볼까 한다.

지금껏 콘솔 입출력에 있어서 std::cout, std::cin을 사용해 왔다. 사용법은 알지만 정확한 의미를 정리해 보도록 하자.

::연산자는 아까전에 정리했었고, 따라서 “namespace std에 선언된 cout”, “namespace std에 선언된 cin” 등으로 알 수 있다.

즉, 헤더파일 iostream에 선언되어 있는 것을 유추해보면,

namespace std{
	cout ...
    cin ...
    endl ...
}

과 같이 되어 있다는 결론을 내릴 수 있다. 이처럼 이름충돌을 피하기 위해 std가 선언되어 있음을 알 수 있다.

그리고 일반적으로 많은 사람들이 std의 선언은 using을 이용한다.

#include <iostream>

namespace Hybrid{
	void HybFunc(void){
		std::cout << "So simple function!" <<std::endl;
		std::cout << "In namespace Hybrid!" << std::endl;
	}
}

 
int main() {
 
 	using Hybrid::HybFunc;
 	HybFunc();
	return 0;
}

위와 같이 선언한 것을보면 HybFunc을 이름공간 Hybrid에서 찾으라는 의미로 using을 사용하였다. 또한 HybFunc은 함수의 이름도 될 수 있고, 변수의 이름도 될 수 있다. 또한 선언이 main함수 내에 존재하는데, 이는 지역변수와 같이 되며 전역으로 선언을 하게 된다면 모든 곳에 선언 효력이 미친다.

그럼 좀더 나아가 보자.

#include <iostream>

using std::cin;
using std::cout;
using std::endl;
 
int main() {
 
 	int num=20;
 	cout << "Hello World!" << endl;
 	cout << num << endl;
	return 0;
}

위와 같이 using을 일일이 선언하여 할 수 있고, 만약 꼭 이렇게 해야할 상황이 아니라면 다음 예제를 통해 namespace std에 선언된 모든 것에 대해 namespace 지정의 생략을 명령할 수 있다.

using namespace std;

이는 namespace에 선언된 모든 것에 접근할 때에는 namespace 지정을 생략한다는 뜻이다.

#include <iostream>

using namespace std;
 
int main() {
 
 	int num=20;
 	cout << "Hello World!" << endl;
 	cout << num << endl;
	return 0;
}

그럼 namespace의 별칭 지정에 대해 알아 보도록 하자. 보통 namespace를 정말 과도하게 여러 중첩이 되면서 까지 사용을 하진 않는다. 그러나 만약 사용하게 된다면 이름을 정리하기가 꽤 귀찮아진다.

#include <iostream>

namespace AAA{
	namespace BBB{
    	namespace CCC{
			int num1;
            int num2;
        }
    }
}

다음과 같을 경우 우리는 AAA::BBB::CCC::num1=20; 과 같이 접근을 하는 데 별로 이쁘지 않다. 따라서 namespace ABC = AAA::BBB::CCC; 와 같이 적어줌으로써 편리성을 줄 수 있다.

namespace AAA{
	namespace BBB{
    	namespace CCC{
			int num1;
            int num2;
        }
    }
}

int main(){
	namespace ABC = AAA::BBB::CCC;
    ...
}

마지막으로 범위지정 연산자(Scope Resolution Operator)의 또 다른 기능에 대해 알아보자.

먼저 지역변수의 이름이 전역변수의 이름과 같다면 전역변수는 지역변수에 의해 가려진다는 것을 기억하고 있어야 한다. 그럴 때, 범위지정 연산자를 사용하면 전역변수에도 접근이 가능하다.

int val=100;

int myFunc(){
	int val = 20;
    val+=3; //지역변수 val 접근
    ::val+=7; //전역변수 val 접근
}

파일의 분할


namespace를 정리 하면서 파일의 저장에 대해 잠시 나왔었는데, 아마 이 책에서도 그부분이 나오다보니 참고사항으로 적어둔 것 같다.

C++을 제대로 공부하려면 이 내용을 꼭 알아두고 있도록 하자.

헤더파일의 의미와 정의방법
헤더파일에 삽입할 내용과 소스파일에 삽입할 내용을 구분하는 방법
 이상의 헤더파일과 소스파일을 만들어서 하나의 실행파일로 컴파일하는 방법

모르면 C언어 기본서에서 이 부분을 복습하기 바란다고 한다. 여기로 가면 정리가 잘되어 있는데, 조금 간추리자면, 헤더파일은 메뉴판과 같다. 다른 부가적인 설명이 없어도 헤더파일을 보면 대강은 알 수 있는 것이다. 나누어 저장하면서 활용성을 높이고, 단순화, 간략화, 최적화를 시키는 것이라고 볼 수 있다.

이후의 진도를 나가겠지만, 한 포스팅에 너무 많은 내용을 담으면 읽기가 힘들어지므로 다음 포스팅에 다음 내용들을 정리해 보겠다.

Blog Logo

구찌


Published

Image

구찌의 나도한번 해블로그

구찌의 개발 블로그 입니다.

Back to Overview