본문 바로가기
Programming/C++

C언어 기반의 C++

by OKOK 2017. 7. 28.

C++은 C언어를 포함합니다. 떄문에 C언어로 작성된 대부분의 프로그램은 C++ 컴파일러로도 컴파일이 가능합니다. 그러나 C++은 C언어가 지니지 않는 문법적 특성도 지니고 있습니다. 


printf 와 scanf를 대신하는 입출력 방식

#include <iostream>


int main(void)

{

int num = 20;

std::cout << "World" << std::endl;

std::cout << "Hello" << "World" << std::endl;

std::cout << num << ' ' << 'A';

std::cout << ' ' << 3.14 << std::endl;

return 0;


위의 소스코드와 실행결과를 함께 관찰하면 출력에 관한 몇 가지 특성을 파악할 수 있습니다. 헤더파일 선언문 #include<iostream>, std::cout과 <<을 이용한 출력, std::endl을 이용한 개행. 관찰 결과1: 헤더파일 선언문 #include<iostream>. 즉, std, cout, endl과 같은 것을 사용하기 위해서, 위의 헤더파일 선언문이 추가되어야 합니다. 헤더파일의 확장자는 C언어와 마찬가지로 .h 입니다. 그러나 C++ 에서는 프로그래머가 정의하는 헤더파일의 선언이 아닌, 표준 헤더파일의 선언에서는 확장자를 생략하기로 약속되어 있습니다. 과거의 표준 라이브러리와 새로운 표준 라이브러리의 구분, 새로운 표준 라이브러리를 사용하는 형태로 소스코드를 쉽게 변경할 수 있도록 확장자를 생략하기로 하였습니다. 


출력을 위해서는 다음의 형태를 취합니다. std::cout<<'output'; 출력대상의 위치에는 무엇이든 올 수 있습니다. 정수와 실수를 비롯하여 문자열, 그리고 변수도 올 수 있습니다. 그리고 c언어의 printf 와 달리 %d, %s 와 같은 서식문자를 이용해서 별도의 출력포맷을 지정하지 않아도 데이터의 성격에 따라서 적절한 출력이 이루어집니다. 


<<도 하나의 연산자이다. 이 연산자를 이용하면 둘 이상의 출력 대상을 연이어 출력할 수 있습니다. 기본적인 데이터의 출력방법을 알게 되었스니다. std::out, <<, std::endl의 정체에 대해서는 다른 포스팅에 다루도록 하겠습니다.


scanf를 대신하는 데이터 입력

#include <iostream>


int main(void)

{

int val1;

std::cout << "First number input: ";

std::cin >> val1;


int val2;

std::cout << "Second number input: ";

std::cin >> val2;


int result = val1 + val2;

std::cout << "plus result: " << result << std::endl;

return 0;


위 예제를 통해서 알 수 있는 것은 다음과 같습니다. 키보드로부터의 데이터 입력에도 헤더파일 선언문 #include<iostream>이 필요합니다. 키보드로부터의 데이터 입력에는 std::cin 과 >> 연산자가 사용됩니다. 변수의 선언은 어디서든 가능합니다.  C++ 에서는 데이터의 입력도 데이터의 출력과 마찬가지로 별도의 포맷 지정이 필요 없습니다. 


C언어로 프로그램을 작성하는 경우에는 함수를 정의함에 있어서 지역 변수의 선언이 항상 제일 먼저 등장해야만 했습니다. 그런 C++의 지역변수 선언은 함수 내 어디든 삽입이 가능합니다. 그래서 예제에서 변수선언이 유효했습니다. 참고로 C언어도 새로운 표준에서는 C__과 마찬가지로 변수선언의 위치에 제한을 두지 않습니다. 그러나 아직도 대부분의 컴파일러는 이를 허용하지 않습니다. 반면 C++의 모든 컴파일러는 지역변수의 선언 위치에 제한을 두지 않습니다. 

 

#include <iostream>


int main(void)

{

int val1, val2;

int result = 0;

std::cout << "two number input: ";

std::cin >> val1 >> val2;


if (val1 < val2)

{

for (int i = val1 + 1; i < val2; i++)

result += i;

}


else

{

for (int i = val2 + 1; i < val2; i++)

result += i;

}

std::cout << "total : " << result << std::endl;

return 0;


연속적인 데이터의 입력을 요구할 수 있다는 사실도 알 수 있습니다. std::cin>>val1>>val2; 첫 번째 입력되는 정수는 변수1이 저장되고, 두 번째 입력되는 정수가 변수2에 저장됩니다. 첫 째 정수와 두 번째 정수의 경계는 탭, 스페이스 바, 엔터 키의 입력과 같은 공백에 의해서 나눠집니다. 



배열 기반의 문자열 입출력


#include <iostream>


int main(void)

{

char name[100];

char lang[200];


std::cout << "name? :";

std::cin >> name;


std::cout << "lanuage? :";

std::cin >> lang;


std::cout << "my name is " << name << ". \n";

std::cout << "favoriate language is " << lang << ". " << std::endl;

return 0;


배열에 문자열의 저장을 명령하고 있습니다. \n 과 같은 특수문자는 C언어에서와 같은 의미를 지닙니다. 다만 std::endl로 개행의 역할을 대신할 뿐입니다. 



함수 오버로딩

c언어에서는 동일한 이름의 함수가 정의되는 것이 허용되지 않습니다. 두 함수의 이름이 같기 때문에 컴파일 오류가 발생합니다. 함수호출 시 전달되는 인자를 통해서 호출하고자 하는 함수의 구분이 가능하기 때문에 매개변수의 선언형태가 다르다면, 동일한 이름의 함수 정의를 허용할 수 있습니다. 이러한 형태의 함수 정의를 가리켜 함수 오버로딩이라고 합니다. C++은 호출할 함수를 찾을 때, 두 가지 정보를 동시에 활용합니다. '함수의 이름', '매개 변수의 선언' 즉, 다음의 함수 호출문을 보면, Myfunc(10, 20); 두 개의 int형 정수를 인자로 전달받을 수 있는 MyFunc 이라는 이름의 함수를 찾아보겠습니다. 반면 C언어는 함수의 이름만 이용해서 호출대상을 찾습니다. 즉, 위의 함수 호출문을 보면서 다음과 같이 판단하고 함수를 찾습니다. MyFunc 이 어디있나요. 함수의 오버로딩이 가능 하려면 매개 변수의 선언이 달라야 합니다. 매개 변수의 자료형 또는 개수가 달라야 합니다. 하지만 반환형은 함수호출 시, 호출되는 함수를 구분하는 기준이 될 수 없습니다. 


#include <iostream>


void MyFunc(void)

{

std::cout << "MyFunch(void) called" << std::endl;

}


void MyFunc(char c)

{

std::cout << "MyFunch(char c) called" << std::endl;

}


void MyFunc(int a, int b)

{

std::cout << "MyFunc(int a, int b) called"<<std::endl;

}


int main(void)

{

MyFunc();

MyFunc('A');

MyFunc(12, 13);

return 0;


매우 자연스러운 개념입니다.


매개변수의 디폴트 값

앞서 함수 오버로딩에 대해서 언급하였습니다. C++의 함수에는 디폴트 값이라는 것을 설정할 수 있습니다. 여기서 말하는 디폴트 값이란 기본적으로 설정되어 있는 값을 의미합니다. 함수호출 시 인자를 전달하지 않으면 7이 전달된 것으로 간주하겠다.


#include <iostream>


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

{

return num1 + num2;

}


int main(void)

{

std::cout << Adder() << std::endl;

std::cout << Adder(5) << std::endl;

std::cout << Adder(3, 5) << std::endl;

return 0;


함수의 디폴트 값이 1과 2로 설정되었습니다. 함수를 호출하면서 인자를 전달하지 않으면, 1과 2가 전달된것으로 간주됩니다. 함수 호출하면서 인자를 하나만 전달하고 있는 경우에는 첫 번째 매개변수로 전달됩니다. 따라서 두 번째 매개변수에는 2가 전달된 것으로 간주됩니다. 함수를 호출하면서 두 개의 인자를 직접 전달할 경우 매개 변수의 디폴트 값은 의미를 갖지 않습니다. 매개 변수에 디폴트 값이 설정되어 있으면, 선언된 매개변수의 수보다 적은 수의 인자전달이 가능합니다. 전달되는 인자는 왼쪽에서부터 채워져 나가고, 부족분은 디폴트 값으로 채워집니다. 디폴트 값은 함수의 선언 부분에만 표현하면 됩니다. 함수의 원형을 별도로 선언하는 경우, 매개 변수의 디폴트 값을 함수의 원형 선언에만 위치시켜야 합니다. 


#include <iostream>


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


int main(void)

{

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 함수에서 호출 시 컴파일이 가능하지 않게 됩니다. 부푼적 디폴트 값을 설정시 반드시 오른쪽부터 채울 것을 요구합니다. 함수에 전달되는 인자가 왼쪽에서부터 오른쪽으로 채워지기 때문입니다. 


#include <iostream>


int BoxVolume(int length, int width = 1, int height = 1);


int main(void)

{

std::cout << "[3, 3, 3] : " << BoxVolume(3, 3, 3) << std::endl;

std::cout << "[5,5,D] : " << BoxVolume(5, 5) << std::endl;

std::cout << "[7, D, D]: " << BoxVolume(7) << std::endl;

// std::cout << "[D, D, D] : " << BoxVolume() << std::endl;

return 0;

}


int BoxVolume(int length, int width, int height)

{

return length*width*height;


첫 번째 매개변수에는 디폴트 값이 지정되지 않았으므로, BoxVolume 함수를 호출할 떄는 반드시 하나 이상의 인자를 전달해야 합니다. 모두 유효한 함수 호출문입니다. 전달되는 인자는 왼쪽에서부터 채워지고 부족한 부분은 디폴트 값이 대신 전달하게 됩니다. 모든 매개변수에 디폴트 값이 지정된 것이 아니기 때문에, 인자를 전달하지 않는 형태의 함수호출은 컴파일 에러로 이어집니다. 


인라인 함수

in은 내부를 의미하고, line은 프로그램 코드라인을 의미합니다. 프로그램 코드라인 안으로 들어가 버린 함수라는 뜻입니다. 매크로 함수의 장점과 단점을 잘 알 고 있을 것입니다. 일반적인 함수에 비해서 실행속도의 이점이 있습니다. 매크로의 단점은 정의하기 어렵습니다. 복잡한 함수를 매크로의 형태로 정의하는데 한계가 있습니다. 전처리 과정을 거치면서 변경이 됩니다. 중요한 점은 함수의 몸체부분이 함수의 호출문을 대체했다는 점입니다. 이와 같이 함수의 몸체부분이 함수 호출 문장을 완전히 대체했을 때 함수가 인라인화 되었다 라고 표현합니다. 매크로 함수는 정의하기 복잡하니, 일반 함수처럼 정의가 가능하면 좋겠다라고 생각을 합니다. 이는 매크로 함수의 장점은 유지하되, 단점은 제거하면 좋겠다는 뜻입니다. 


매크로 함수를 이용하지 않은, C++ 기반의 인라인 함수의 정의 방법을 보이고 있습니다. 


#include <iostream>


inline int SQUARE(int x)

{

return x*x;

}


int main(void)

{

std::cout << SQUARE(5) << std::endl;

std::cout << SQUARE(12) << std::endl;

return 0;


매크로를 이용한 함수의 인라인화는 전처리기에 의해서 처리되지만, 키워드 inline을 이용한 함수의 인라인화는 컴파일러에 의해서치라고 딥니다. 따라서 컴파일러는 함수의 인라인화가 오히려 성능에 해가 된다고 판단할 경우, 이 키워드를 무시해버리기도 합니다. 또한 컴파일러는 필요한 경우 일부 함수를 임의로 인라인 처리하기도 합니다. 


매크로 ㅎㅁ수에는 있지만, 인라인 함수에는 없는 단점. 사실 위의 인라인 함수는 매크로 함수의 장점을 완전히 대체하지 못했습니다. 자료형에서 독립되지 못한다는 점입니다. 템플릿이라는 것을 이용하면 매크로 함수와 마찬가지로 자료형에 의존적이지 않은 함수가 완성됩니다. 


#include <iostream>


template <typename T>

inline T SQUARE(T x)

{

return x*x;

}


int main(void)

{

std::cout << SQUARE(5.5) << std::endl;

std::cout << SQUARE(12) << std::endl;


return 0;


이름공간에 대한 소개

이름을 붙여놓은 공간. 말 그대로 특정 영역에 이름을 붙여주기 위한 문법적 요소입니다. 


#include <iostream>


namespace BestComImpl

{

void SimpleFunc(void)

{

std::cout << "BestCom func" << std::endl;

}

}


namespace ProgComImpl

{

void SimpleFunc(void)

{

std::cout << "ProgCom func" << std::endl;

}

}


int main(void)

{

BestComImpl::SimpleFunc();

ProgComImpl::SimpleFunc();

return 0;


BestComImpl 이라는 이름의 공간을 마련하였습니다. 그리고 안에 함수 SimpleFuc 을 정의하였습니다. 따라서 이함수는 BestComImpl::SimpleFunc 이라고 지칭하게 됩니다. 이름공간 BestComImpl 내에 정의된 함수 SimpleFunc의 호출문장입니다. 예제에서 사용된 연산자 ::를 가리켜 범위지정 연산자(scope resolution operator)라 하며, 그 이름이 의미하듯이 이름공간을 지정할 때 사용하는 연산자입니다. 


함수의 선언과 정의를 분리하는 것이 일반적입니다. 함수의 선언은 헤더파일에 저장하고, 함수의 정의는 소스파일에 저장하는 것이 보통입니다. 예제를 통해서 이름공간 기반에서 함수의 선언과 정의를 구분하는 방법을 설명하겠습니다.


#include <iostream>


namespace BestComImpl

{

void SimpleFunc(void);

}


namespace ProgComImpl

{

void SimpleFunc(void);

}


int main(void)

{

BestComImpl::SimpleFunc();

ProgComImpl::SimpleFunc();

return 0;

}


void BestComImpl::SimpleFunc(void)

{

std::cout << "BestCom Func" << std::endl;

}


void ProgComImpl::SimpleFunc(void)

{

std::cout << "ProgCom Func" << std::endl;


이름공간 안에 함수의 선언만 삽입하였습니다. 이름 공간에 선언된 함수의 정의부분입니다. 이렇듯 ::연산자는 함수의 호출 이외에도 다양하게 사용됩니다. 


#include <iostream>


namespace BestComImpl

{

void SimpleFunc(void);

void PrettyFunc(void);

}


namespace ProgComImpl

{

void SimpleFunc(void);

}


int main(void)

{

BestComImpl::SimpleFunc();

return 0;

}


void BestComImpl::SimpleFunc(void)

{

std::cout << "BestCom Func" << std::endl;

PrettyFunc();

ProgComImpl::SimpleFunc();

}


void BestComImpl::PrettyFunc(void)

{

std::cout << "So Pretty" << std::endl;

}


void ProgComImpl::SimpleFunc(void)

{

std::cout << "ProgCom func" << std::endl;


이름공간은 둘 이상의 영역으로 나누어서 선언할 수도 있습니다. 이 둘은 동일공간으로 간주됩니다. 정의된 함수와 동일한 이름공간에 정의된 함수입니다. 따라서 이렇게 직접호출이 가능합니다. 함수의 호출위치가 어떻게 되건, 이름공간에 정의된 함수를 호출하는 방법에는차이가 없습니다. 이름공간 std에 선언된 cout, 이름공간 std에 선언된 cin, 이름공간 std에 선언된 endl, 


#include <iostream>


namespace Hybrid

{

void HybFunc(void)

{

std::cout << "So simple function" << std::endl;

std::cout << "In namespae Hybrid" << std::endl;

}

}


int main(void)

{

using Hybrid::HybFunc;

HybFunc();

return 0;


키워드 using을 이용해서 이름공간 Hybrid에 정의된 HybFunc를 호출할 때에는 이름공간을 지정하지 않고 호출하겠다는 것을 선언하고 있습니다. using 선언을통해서 이름공간의 저정엇이 HybFunc 함수를 호출하고 있습니다. using Hybrid::HyFunc; 선언은 HybFunc를 이름공간 Hybrid에서 찾으세요 라는 일종의 선언입니다. 이 때, HybFunc는 함수의 이름이 될 수 있고, 변수의 이름도 될 수 있습니다. 그리고 위 예제에서는 위의 선언이 main 함수 내에 존재하는데, 이러한 경우 지역변수의 선언과 마찬가지로 선언된 이후부터 효력을 발휘하며, 선언된 지역을 벗어나면, 선언의 효력은 잃게 됩니다. 따라서 프로그램 전체영역에 효력을 미치게 하려면 전역변수와 마찬가지로 함수 밖에 선언을 해야 합니다. 


#include <iostream>


using std::cin;

using std::cout;

using std::endl;


int main(void)

{

int num = 20;

cout << "Hello World~" << endl;

cout << "Hello World~" << endl;

return 0;


using 선언을 함수 밖에 전역의 형태에 삽입하였습니다. 따라서 이제부터 cin, cout, endl의 사용에 있어서 이름공간의 지정이 불필요합니다. 이름공간 std에 선언된 모든 것에 대해 이름공간 지정의 생략을 명령할 수 있습니다. using namespace std; 이는 이름공간 std에 선언된 모든 것에 접근할 때에는 이름공간의 지정을 생략하겠다는 선언입니다. 


#include <iostream>


using namespace std;


int main(void)

{

int num = 20;

cout << "Hello World~" << endl;

cout << "Hello World~" << endl;

return 0;


using namesapce 선언이 매력적으로 보일 것입니다. 이름충돌이 발생할 확률은 상대적으로 높아집니다. 무조건 편한 것만을 선호하기 보다는 상황을 판단해서 적절히 혼용하는 것이 필요합니다. 


#include <iostream>


using namespace std;


namespace AAA

{

namespace BBB

{

namespace CCC

{

int num1;

int num2;

}

}

}


int main(void)

{

AAA::BBB::CCC::num1 = 20;

AAA::BBB::CCC::num2 = 30;


namespace ABC = AAA::BBB::CCC;

cout << ABC::num1 << endl;

cout << ABC::num2 << endl;

return 0;


별칭이 선언되기 이전의 변수 num1 과 num2 의 접근을 보이고 있습니다. AAA::BBB::CCC에 ABC라는 별칭을 붙여주고 있습니다. 별칭의 선언 이후의 접근을 보이고 있습니다. 


지역변수의 이름이 전역변수의 이름과 같을 경우, 전역변수는 지역변수에 의해 가려진다는 특징을 기억하나요?