목차
1. 입출력 방식
<iostream> 에 있는 std::cout 과 std::cin 을 이용한다. scanf 나 printsf와 다르게 타입에 의존하지 않고 입출력을 할 수 있기 때문에 편의성이 개선되었다. 하지만 개행 등을 << 연산자로 이어줘야하기 때문에 코드가 길어지긴 한다.
또한 iostream.h 에서 h 를 생략하는데, 이는 새로운 STL 을 구분하고 기존의 것을 쉽게 변경하도록 하기 위함이다.
2. 함수 오버로딩
-동일한 함수명과 다른 매개변수의 자료형 혹은 개수일 때 가능하다.(리턴 타입은 전혀 무관하다.)
이는 c와 다르게 함수의 이름뿐만 아니라 매개변수의 선언까지 이용해서 호출대상을 찾기 때문이다.
3. 매개변수의 기본값-
c와 다르게 함수의 매개변수의 기본값을 정하는 것이 가능하다. 예를 들어 int func(int num=7){} 식이 가능하며, 이는 인자를 전달하지 않았을 시에 전달한 것처럼 간주한다는 뜻이다.
즉 func() 와 func(7)이 같은 의미이다.
원칙은 오른쪽에서부터 시작해 빠짐이 없이 하나씩 채워져야한다는 것이다. 즉 int func(int num1, int num2 =0) 같은 형태는 가능하되 int func (int num1=0, num2) 는 불가능하다는 것이다. 이는 호출시에 인자가 왼쪽부터 전해지기 때문에 빈칸을 유용하게 처리하기 위함이다. 만약 비워두면서 채울 경우 인자가 어떤 것인지 구분하기 모호해지므로 쓰지 않기로 한다.
4. 인라인 함수
함수를 코드안으로 집어넣겠다(in line)라는 의미이다. 기존의 #define은 코드를 그대로 붙여넣어 실행속도를 향상시킨다는 장점이 있었으나 정의하기가 어렵고 의도치 않은 문제가 발생할 가능성이 크다.
하지만 매크로 함수는 자료형에 의존적이지 않기 때문에 여전히 완전히 대체하긴 어렵다. ‘템플릿’을 사용하면 해결되는 문제다. -> 후술 template<typename t="T">
5. namespace
작업의 규모가 커지고 그룹이 다양해질 경우 서로가 사용하는 변수와 함수들을 분리할 필요가 있다. 겹치면 의도치 않은 에러가 날 수 있기 때문. 그래서 구별할 수 있는 이름을 부여하고 그 이름을 가진 공간에서 사용되는 변수와 함수를 선언해주면 된다. 접근은 :: 연산자로 이루어지며 범위지정 연산자(scope resolution operator) 라고 한다. 이들은 namespace 내부의 선언문과 외부의 정의로 이루어진다. 정의는 namespace의이름::함수() 로 이루어지며, 같은 이름공간의 함수 변수는 ::연산자 없이 사용가능하다. 또한 이름공간 내부에 이름공간을 삽입할 수 있다.(중첩) 편리한 방법은 using를 사용하는 방법이다. 가령 namespace kim 의 함수 call() 을 ::연산자 없이 사용하고 싶다면 using kim::call; 을 선언해주면 call()만으로 이용가능하다. 이는 지역변수의 선언과 마찬가지로 선언된 지역을 벗어나면 효력을 잃게 되므로 전체영역으로 하려면 함수 밖에 선언해야한다. 만약 하나하나 하기 귀찮고 이름공간 모든 것에 대해 생략하고 싶다면 using namespace kim; 을 선언해주면 된다. 하지만 그만큼 충돌할 확률도 높아지므로 주의해서 써야한다. 또한 별칭을 주는 것도 가능하다. kim 안의 namespace lee 안에 namespace park 이 있을 경우 kim::lee::park::(...) 으로 접근해야하지만, 간단하게 쓰기 위해 별명을 부여할 수 있다. namespace KLP=kim::lee::park; KLP::age = 10; 또한 ::연산자는 전역 변수가 지역변수에게 가려졌을 때 ::연산자를 앞에 써주면 전역 변수로 접근이 가능하다. int func(int a){ ::a = 0; }
-새로운 자료형 bool
true =1 false =0 ;를 키워드로 제공하는 자료형을 제공한다.1Byte
6.참조자(Reference)
할당된 메모리 공간에 별명을 하나 더 붙여보자. int a = 10; 이라면 c에서는 그 곳을 가리키는 포인터 int *b = &a; 로 선언해서 사용했었다. 함수에 외부 변수를 포인터를 매개변수로 던져서 주소로 직접 접근하여 값을 제공하는 경우를 c에선는 참조에 의한 호출 (call by reference)라고 불렀었다. c++에서는 하지만 참조에 의한 호출을 지원하기 위해서 유사한 기능을 넣었는데, 이것이 c++에서의 참조자(&)이다. 쓰는 방식은 일반 변수를 선언하는 것과 같고 사용방법도 같다. int& c= a; 여기서 &은 주소를 변환하는 연산자가 아니고 참조자를 선언하는 역할을 한다. 여기서 c=5; 로 바꾼다면 a도 5로 바뀌게 된다. 이는 같은 공간을 가리키는 다른 별명을 선언하는 것과 유사하다. 즉 한 명을 개똥이라고 부르는 사람도 있고 똥개라고 부르는 사람이 있어도 그 한 명은 동일한 사람인 것과 유사하다. 참조자의 수는 제한이 없으며 참조자가 참조자를 대상으로 선언할 수 있다. 하지만 별명인만큼 이미 존재하는 변수에 대해서만 선언이 가능하고, 선언과 동시에 정의되어야한다. int &c; 로 할 수 없다. 또한 null 로 초기화할 수 없다. int &c=20; , int &c=NULL; 불가.
이 별명은 같은 지역에 있을 때는 재밌다는 사실빼곤 의미가 없지만, 매개변수로 쓰일때는 그 가치가 다르다. 가령 c에서는 함수 호출할 때 연산한 결과를 반영해주려면 포인터를 사용했어야했다. 하지만 참조자를 이용하면 변수처럼 사용하되 그 공간에 바로 결과를 반영할 수 있어서 (쓰기가) 간편하다.
하지만 동시에 단점도 존재한다. 주소값을 넘겨주기 때문에 reference와 value 가 바로 구별되던 c와는 다르게 직접 코드를 분석하기전에 둘을 c++에서 구별하긴 힘들기 때문에 둘을 구별시켜줘야 할 필요가 있다. 그래서 필요한 것이 const 선언이다. 만약 참조자를 이용해서 내부의 값을 바꾸지 않을 경우 매개변수를 const로 선언하는 것이 좋다. 만약 변경된다면 컴파일 타임에 에러가 발생하기 때문에 런타임의 에러(잡기힘든)를 방지할 수 있다.
참조형으로 값을 반환할 수도 있다. 하지만 함수 내의 지역변수의 참조형을 반환할 경우는 함수가 종료되면 지역변수는 사라지기 때문에 전혀 이상한 곳을 가리키게 될 수도 있으므로 주의하자.
const 를 사용하면 참조자는 상수도 참조 가능하다. 이 때 상수가 저장된 장소를 참조하게 된다.(임시변수) int add(const int &n1, const int &n2) return num1+num2; 인자 전달을 목적으로 변수를 선언하지 않게된다.
7.new&delete
c언어의 malloc 과 free 를 대체할 편리한 개념이다. 힙 영역에 동적으로 할당, 해제시켜주는 역할을 한다. 기존 c 를 이용하려면 int *p = (int *)malloc(sizeof(int) * len); 의 형태를 띄어야하는데 무지 복잡하다. 그렇기에 c++에서는 int *p = new int(초기화); 의 형태로 사용이 가능하게 만들었으며 이는 배열에도(초기화 안됨) 마찬가지로 int *p = new int[3]; 로 가능하다.
free 대신 delete가 들어왔다. free와 마찬가지로 일반적인 애들을 해제시켜줄 땐 delete p를 쓰면 되지만 만약 p가 배열이라면 delete[] p; 의 형태로 써야한다.
8.구조체
struct 를 생략하기 위해서 c에서는 typedef 선언을 해주어야했지만 c++에 넘어와서는 굳이 해주지 않아도 struct를 생략하고 이름만으로 선언이 가능해졌다. typedef struct {}A; == struct A{};
또한 구조체의 장점인 연관이 있는 아이들끼리 묶어주는 기능을 최대한 살리기 위해 구조체 내의 함수를 선언할 수 있다. 이때의 장점은, 구조체 내의 함수는 구조체 내의 변수에 직접 접근이 가능하므로 매개변수를 통해 받을 필요가 없다. 물론 함수는 구조체 내에서 정의할 수 있지만, 접근성과 가독성을 위해 외부에서 정의를 많이하고, 그때는 ::연산자를 통해 참조하도록 한다. void kim::kick(){} 또한 구조체 안의 함수는 기본적으로 inline으로 취급되지만 외부로 꺼낼시엔 아니므로 inline을 쓰고 싶다면 키워드를 추가해줘야한다. 물론 대체되어야하기 때문에 선언과 동일한 파일에 저장되어야 참조가 가능하므로 inline함수는 선언과 동일한 곳에 정의한다.
9.클래스
구조체는 클래스의 일부이다. 구조체는 기본적으로 모든 멤버 변수와 함수가 접근제어 지시자 public으로 선언되지만 클래스는 private로 설정되는 것이 유일한 차이점이다.
여기서 접근제어 지시자(레이블)은 다음과 같은 3가지이다. public protected private; 3개는 각각 모두에게 접근 허용, 상속 관계에서 자식 클래스에게 접근 허용, 자신만 허용을 뜻한다.
10.객체지향 프로그래밍이란?
객체란 object를 어려운 말로 번역한 것이다. 즉, 어떤 물체를 뜻하는데 물체는 존재하는 사물의 특성과 그것이 할 수 있는 행동, 두 가지를 프로그램으로 짜는 것을 뜻한다. 이것을 구조체나 클래스로 구성하면 된다.
클래스의 틀을 짜고 나면 바로 이용할 수 있는 것이 아니다. 이는 선언만 한 것이므로 실제로 메모리 상에 데이터 값이 쓰인 것이 아니기 때문에 할당을 해주고 값을 넣어줘야 비로소 객체로서 취급할 수 있게 된다. 할당방식은 다른 변수처럼 classname a; 로 변수선언하거나 classname * ptr = new classname; 처럼 동적 할당방식(힙)을 사용하면 된다. 이제 만들어진 객체 사이에는 서로의 관계를 통해 메세지를 전달하고 전달받으며 작동을 하게 된다. 이런 형태의 함수 호출을 메세지 전달(Message Passing)이라고 부르고 객체 지향에서 핵심적인 역할을 한다.
11.객체의 특징
객체의 특징은 크게 2부분으로 나누어진다. 하나는 정보은닉(information Hiding)이고 하나는 캡슐화(capsulization)이다.
정보은닉은 접근제어 지시자를 사용하는 이유이며, 함부로 접근하면 안되는 정보들에 대해 보호하기 위함이다. 정해진 방법으로만 접근하게 만들어 안정성을 높여줄 수 있다. 또한 실수에 대해 컴파일 타임에 오류가 발생하도록 하기 때문에 잡기힘든 런타임 에러를 줄일 수 있다. 물론 자신의 멤버 변수의 값을 일절 변경시키지 않는 함수들을 호출하게 되는 경우도 있는데, 이때는 반드시 함수 연산자 뒤에 const를 붙여줘 상수 함수로 만들어주어 명시적으로 표현해야한다. 안정성 뿐만 아니라 매개 변수가 const일 때는 상수 함수가 아니면 사용이 제한되기 때문이다.
캡슐화는 여러 기능을 하나로 합쳐서 만들어놓은 것을 뜻한다. 하나의 일만 할 수 있는 캡슐이라면 함수와 다를바 없다.
12.생성자(constructor)와 소멸자(destructor)
초기화와 할당 해제 등을 손으로 일일이 해주지 않아도 된다. 생성자는 객체가 생성될 때 호출되는 함수이고 소멸자는 그 반대로 소멸될 때 호출되는 함수이다. c++ 컴파일러는, 만약 생성자와 소멸자를 사용자가 직접 선언하지 않을 경우 기본 생성자와 소멸자를 호출하는데 이들은 아무 역할을 하지 않는다. 만약 내가 멤버 변수에 대해 초기화가 필요하고 외부에서 접근할 수 없는 변수들이라면 지금까지는 초기화 함수를 만들어 진행했지만, 초기화를 깜빡할 위험 등 안정성에 영향을 줄 수 있는 요소들이 있기 때문에 생성자와 소멸자를 이용해 편리하게 사용할 수 있다.
생성자와 소멸자는 둘 다 리턴타입을 명시하지 않는다. 생성자는 이름은 반드시 클래스나 구조체의 이름과 같아야하며 매개 변수를 받을 수도 있다. 소멸자는 접두어로 ~를 붙여서 선언한다. 또한 반드시 void형으로 매개변수가 선언되어야 한다. 함수의 일종이므로 오버로딩과 디폴트 값 설정이 가능하다. 물론 상황별로 단 하나만 호출되어야하므로 모호해선 안된다. (default가 모두 설정되어있는 경우와 인자가 void일 경우 구분 잘 해야 함) 또한 인자가 없을 경우 classname a1; 처럼 호출해야지 a1()처럼 호출하면 타함수와 모호해지므로 쓸 수 없다.
또한 다른 클래스를 포함하는 클래스를 호출할 때, 다른 클래스의 멤버 변수들을 초기화시킬 수 있어야하는데, 멤버 이니셜라이저(Member Initializer)를 이용하여 생성자를 호출할 수 있다. 사용법은 생성자 옆에 : 연산자와 함께 다른 클래스의 이름(매개변수), ... 로 호출하면 된다. 이니셜라이저는 자신의 멤버 변수도 초기화가 가능한데, 사용법은 같다. : 변수명(초기화값). 또한 장점은 const가 붙은 변수도 초기화가 가능하다는 것에 있다. 하지만 c의 malloc 을 사용하면 생성자가 호출되지 않고 할당만 되므로 반드시 new를 이용해야 호출할 수 있다.
13.this
객체 자신을 가리키는 용도로 사용되는 상수 포인터이다.
14.복사 생성자
객체에 값을 대입할 때 자신과 같은 타입의 객체를 =연산자로 대입한다면 생성자를 따로 만들어주지 않았음에도 불구하고 얕은 복사를 통해 정보가 복사가 된다. 이는 복사 생성자가 컴파일러에 의해 자동으로 생성되기 때문이다. 즉 class a = b(class타입); 은 class a(b); 가 되고 이는 참조에 의한 호출로서 생성자를 호출하게 된다. 이 같은 묵시적 호출을 원하지 않는다면 복사 생성자에 explicit 키워드를 앞에 붙여 막으면 =으로 더 이상 대입할 수 없다. 매개 변수는 반드시 참조&에 의한 호출로 받아야 한다.
기본적으로 생성되는 디폴트 복사 생성자의 경우 얕은 복사를 이용하기 때문에 값만 복사가 된다. 이는 포인터를 변수로 갖고 있을 때 같은 메모리를 가리키게 하기 때문에 소멸자에서 할당 해제가 2번 일어날 경우 비정상적인 종료를 유발한다. 이를 방지하기 위해 깊은 복사(deep copy)를 쓴다. 예를 들어 힙 메모리의 첫 부분을 가리키는 포인터가 있을 경우, 새롭게 메모리를 할당해주고 원래 쓰고자했던 값을 새로운 메모리에 써주고 포인터를 가리키도록 한다면 할당 해제에도 각각의 메모리를 해제하기 때문에 문제가 사라진다.
복사 생성자는 크게 3가지 경우에 불린다. 1. 기존 객체로 새 객체를 초기화하는 경우(a=b), 2. call by value 방식의 호출 과정에서 객체를 인자로 전달하는 경우int func(int n); 3. 객체를 반환하되 참조형으로 반환하지 않는 경우 return a.
모두 메모리를 할당함과 동시에 초기화하는 경우이다. 초기화는 멤버 대 멤버가 복사되는 형태로 이뤄져야하기 때문에 복사 생성자의 호출을 사용함으로써 초기화가 된다.
int func(int n) { return n;} 의 형식일 경우 지역 객체 int n 은 리턴시에 사라지지만 n이 임시객체로 인자로 전달된다. 임시객체는 다음 행으로 넘어가면 (;를 만나면) 바로 소멸되지만 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.
15.const friend static
객체를 const 로 선언할 경우 값의 변경을 허용하지 않기 때문에 선언과 동시에 초기화해줘야하며, const가 붙은 멤버 함수만을 호출할 수 있다. 이는 내부에서 const class * this 가 호출되기 때문에 오버로딩도 가능하다.
friend의 경우 다른 클래스에서 내 내부의 요소들 (private)에 접근할 수 있는 권한을 부여하는 것이다. 하지만 정보은닉을 무시할 수 있는 요소이기 때문에 아주 신중히 써야한다.
c언어에서 static은 전역변수에 붙을 경우 파일 내에서만 사용하겠다는 의미와 , 함수 내에서는 한 번만 초기화하고 함수가 종료되어도 소멸시키지 않는다는 뜻이었다. 이는 클래스 내부에서 쓰일 경우 같은 클래스의 객체들 사이의 전역변수처럼 취급이 된다. 즉 객체 1,2,3 이 있어도 static 변수는 공유하면서 쓴다는 뜻이므로 전역 변수보다 조금 더 안전하게 사용이 가능하다. 하지만 객체의 멤버로 존재하지 않기 때문에 주의해야한다. static 멤버 함수 내에서는 static 멤버 변수와 함수만 호출이 가능하다. (컴파일 타임에 할당되기 때문) const 멤버 변수는 이니셜라이저를 통해야만 할 수 있었지만 const static 의 경우 선언과 동시에 초기화가 가능하다.
mutable - const를 변경할 수 있게 해줌..?
16.상속(Inheritance)
상속을 해주는 쪽을 상위(base , super, parent) 클래스라고 부르고 받는 쪽을 하위(derived ,sub, child) 클래스라고 부른다. 하위 클래스의 생성 과정에서 상위 클래스의 생성자는 항상 호출된다. 상위 생성자 -> 하위 생성자 순이기 때문에 멤버 이니셜라이저를 통해 상위 클래스에서 상속받은 변수들을 초기화해준다. 하지만 소멸자는 반대로 호출된다. 하위->상위. 상속은 3가지 형태를 가지는데 접근범위 지시자와 동일하게 public protected private 을 지원한다. 이 말뜻은 각 허용범위보다 큰 멤버들을 각 지시자로 변경시켜서 상속하겠다는 의미이다. 즉 public 멤버들은 protected 상속을 할 경우 protected로 바뀌어 상속되고, 하위 클래스에서부턴 protected 취급이 될 것이다. 물론 범위가 더 작은 경우는 따로 변경하지 않는다. 실제로 public 상속말고는 거의 쓰이지 않는다.
상속은 어느 하나가 다른 하나에 일부분(포함)일 경우에 주로 사용되지만, 소유 관계의 표현에도 사용될 수 있다. 하지만 소유 관계의 경우 추가되거나 가지고 있지 않을 경우 제약이 생길 수 있다.
부모 클래스의 함수와 동일한 함수를 재정의할 경우를 오버라이딩(overriding)이라고 한다. 오버라이딩을 할 경우 부모 클래스의 함수는 가려지게 되고 자식 클래스 하위의 모든 곳에서 함수는 오버라이딩한 함수가 쓰이게 된다. 상속되어서 완전히 같은 구조를 가진다고 해도 멤버 변수에 접근하는 영역이 다를 수 있으므로 이 때는 오버라이딩을 해야한다.
17.가상함수(virtual function)
런타임에 바인딩을 바꾸기 위한 함수로 (동적 바인딩) virtual 키워드로 선언한다. 이는 정적 바인딩(static binding)의 반대 개념으로 컴파일 타임에 결정되지 않고 런타임 중에 실제 가리키는 객체에 대해 바인딩되는 개념이다. 가령 class parent 에 virtual void func() 와 void func2() 가 있고 class child : public parent 에 virtual void func(), void func2()가 있었다고 가정하자. 그럼 parent *p; child c; 가 선언되었을 때 p=&c 로 두고 p.func()와 p.func2()를 호출한다면 전자는 c의 함수 func()가 불릴 것이고 후자는 parent의 func2()가 호출될 것이다.
순수 가상 함수(pure virtual function) 의 경우 가상함수인데 몸체가 정의되지 않은 함수를 의미하며 virtual void func() const=0; 으로 사용한다. 순수 가상 함수가 포함될 경우 1. 객체를 생성할 수 없기 때문에 실수를 방지할 수 있고, 2. 하위 클래스에서 반드시 정의해야하는 함수로 남길 수 있기 때문에 또한 실수를 방지할 수 있다.
이런 순수 가상함수가 하나라도 포함된 클래스를 추상 클래스(abstract class)라고 한다.
가상함수를 이용하여 다형성(polymorphism)을 구현할 수 있다. 다형성은 같은 모습 다른 결과라고 이해하면 될 것 같다.
18.연산자 오버로딩(operator overloading)
객체 operator op(const 객체){ ...} 의 형태를 띈다. 이는 +를 예로 들자면 객체+객체의 경우 객체.operator+(객체) 의 형태로 컴파일러가 해석하겠다는 약속이다. 물론 약속이기 때문에 오버로딩이 불가능한 연산자도 존재한다 (. , .* , :: , ?: , sizeof, typeid, static_cast, dynamic_cast, const_cast, reinterpret_cast ) 또한 멤버함수 기반으로만 허용되는 연산자도 있다.(= , (), [], ->)
다음의 주의사항을 따르자.
1. 본래의 의도를 벗어나지 않도록 한다.(전혀 새로운 방식 금지)
2. 연산자 우선순위와 결합성은 바뀌지 않는다.
3. 매개변수의 디폴트 값 설정 불가능하다.(타입 디펜던시)
4. 연산자 본래 기능을 덮을 수 없다.
19. 타입 캐스팅
c++은 정적 타입 언어(static typed language)이기 때문에 컴파일 타임에 오류가 발생하지 않으면 런타임에도 오류가 발생하지 않음을 보장한다.
하지만 c에서 쓰던 방식으로 명시적 형변환을 사용한다면, 컴파일러는 사용자의 명령을 무조건적으로 믿고 사용해버리기 때문에 안전한지, 어떤 식으로 오류를 발생할지 예측하기 힘들다. 또한 다른 타입들을 가리키려면 void*를 사용해야하는데, 이는 굉장히 제한적이다. 산술연산이 불가능하고 역참조 또한 불가능하며, 데이터가 손실될 염려도 크기 때문이다.이에 컴파일러 스스로 안전한지를 판단하고 선택하게 하여 런타임에 오류를 예방할 수 있도록 한다.
casting 연산자는 4개를 지원한다. 사용법은 ~_cast<type>(value) 이다.
1. static_cast - 논리(이성)적 형변환. 비교해서 안전하면 변환해줌 int * ~ = static_cast<int>
2. reinterpret_cast - 비이성적 형변환 , 상수->비상수 변환 안됨 char * = reinterpret_cast<char>
3. dynamic_cast -
4. const_cast - 상수 -> 비상수 변환 지원
</char></int></type></typename></iostream>
'C언어' 카테고리의 다른 글
C++ 프로그래밍 - 예외처리(Exception Handling)와 형변환 연산자(type casting operator) (0) | 2021.05.10 |
---|---|
C++ 프로그래밍 - 템플릿(Template) (0) | 2021.05.09 |
C++ 프로그래밍 - 연산자 오버로딩(operator overloading) (0) | 2021.05.09 |
C++ 프로그래밍 - 클래스의 상속(Inheritance)과 다형성(polymorphism) (1) | 2021.05.06 |
C++ 프로그래밍 - 클래스의 기초 (0) | 2021.05.05 |