C와 C++에 대해서
07 Nov 2019 | C CPP C++C와 C++에 대해서
1. C와 C++의 차이점
- C++은 C를 확장시킨 언어이며, C는 절차지향 패턴을 따르는 반면 C++은 객체지향 패턴을 따름
- C에서는 같은 이름을 가진 함수가 존재할 수 없지만, C++에서는 오버로딩/오버라이딩을 통한 존재 가능
- 오버로딩: 같은 이름의 함수를 여러 개 정의하고, 매개변수의 유형과 개수를 다르게 해 유형의 호출에 응답하게 함
- 오버라이딩: 상위 클래스가 갖는 멤버변수가 하위 클래스로 상속되듯이, 상위 클래스가 갖고 있는 메소드도 하위 클래스로 상속 되어 하위 클래스에서 사용 가능. 즉, 상속 관계에 있는 클래스 간에 같은 이름의 메소드를 정의하는 기술을 오버라이딩 (하위 클래스에서 해당 메소드 재정의해 사용 가능)
- C에서는 변수, 함수를 코드 초반에 선언해야 하지만, C++에선 중간 어디서든 선언 가능
2. 클래스와 구조체의 차이점
- C의 구조체
- 구조체는 하나의 구조로 묶일 수 있는 데이터, 즉 변수들의 집합
- C에서도 클래스처럼 함수 포인터를 이용해 클래스처럼 함수를 포함시킬 수 있음
- 구조체(struct)는 default가 public
- 구조체는 하나의 구조로 묶일 수 있는 데이터, 즉 변수들의 집합
- C++의 클래스
- 클래스는 구조체처럼 변수 뿐만 아니라, 합수(메서드)까지 포함 가능
- 클래스(class)는 default가 private
3. 객체지향 프로그래밍과 절차지향 프로그래밍과의 차이점은?
- 각각 객체지향 프로그래밍은 Object Oriented Programming(OOP), 절차지향 프로그래밍은 Procedural Programming
- 절차지향, 구조적 프로그래밍(C)
- 초창기에 많이 사용한 방법으로 순차적 프로그래밍이라고도 함
- 해야할 작업을 순서대로 코딩
- 구조적 프로그래밍에서는 함수 단위로 구성되며 기능별로 묶어놓은 특징이 있음
- 장점
- 컴퓨터의 처리구조와 유사해 실행속도가 빠름
- 단점
- 유지보수가 어려움
- 실행 순서가 정해져있으므로 코드의 순서가 바뀌면 동일한 결과를 보장하기 어려움
- 디버깅이 어려움
- 객체지향 프로그래밍(JAVA, C++, C#, Python)
- 주 구성요소는 클래스와 객체
- 상속과 다형성을 특징으로 들 수 있음
- 다형성은 오버로딩/오버라이딩을 의미
- 클래스를 활용해 각각의 기능별로 구성이 가능하며, 이를 나중에 하나로 합쳐서 프로그램의 완성이 가능
- 객체 별로 개발이 가능하기에 팀 프로젝트를 하기에도 유리한 장점을 갖고 있음
- 코드의 재사용이 가능하며 오류 발생 가능성이 적고 안전성이 높음
- 장점
- 코드의 재활용성이 높음
- 코딩이 절차지향보다 간편함
- 디버깅이 쉬움
- 단점
- 처리속도가 절차지향보다 느림
- 설계에 많은 시간소요가 들어감
4. C++ 클래스의 소멸자에 virtual 키워드 붙이는 이유
- 상속 받았을 때 virtual 키워드가 없다면 최상위 부모 객체에 대한 소멸만 이루어져 메모리 누수의 원인이 됨
- C++에서 가상함수(virtual)는 파생클래스에서 재정의할 것으로 기대하는 멤버 함수를 의미
- 자신이 호출하는 객체의 동적 타입에 따라 실제 호출할 함수가 결정됨
- 기초클래스에서 virtual 키워드를 사용해 가상함수를 선언하면 파생 클래스에서 재정의된 멤버 함수도 자동으로 가상함수가 됨
5. 동적 바인딩이란?*
- C++ 컴파일러는 함수 호출 시 어느 블록에 있는 함수를 호출해야 하고, 해당 함수가 저장된 정확한 메모리 위치까지 알아야 함
- 이처럼 함수를 호출하는 코드에서 어느 블록에 있는 함수를 실행하라는 의미로 해석하는 것을 바인딩(binding) 이라고 함
- C++의 경우 함수가 오버로딩 될 수 있으므로 이 작업이 복잡해짐
- 대부분 함수를 호출하는 코드는 컴파일 타임에 고정된 메모리 주소로 변환되는데, 이것을 정적 바인딩 또는 초기 바인딩 이라고 함
- C++에서는 가상함수가 아닌 멤버함수는 모두 정적 바인딩
- 가상함수의 호출은 컴파일러가 어떤 함수를 호출해야 하는지 미리 알 수 없음
- 따라서 가상함수의 경우에는 런타임에 올바른 함수가 실행되도록 해야 하는데, 이를 동적 바인딩이라고 함
6. 가상 함수 테이블이란?*
- 클래스 안에 가상함수가 포함되어 있을 시 객체를 생성할 때 가상함수를 가리키는 포인터가 생성됨
- 이 포인터는 가상테이블의 시작주소를 가리키는 포인터이며, 각 클래스마다 하나의 고유 가상테이블이 생성됨
- 가상함수를 호출하면 C++ 프로그램은 가상함수 테이블에 접근해 자신이 필요한 함수의 주소를 찾아 호출함
- 가상 함수를 사용하면 이처럼 함수 호출의 과정이 복잡해지므로 메모리와 실행속도 측면에서 부담이 가중됨
7. malloc()과 new에 대해
- malloc()은 함수이고 new는 연산자
malloc()
은 시스템함수로서 함수 안에서 메모리를 할당함new
는 연산자로 바로 메모리를 할당하는것이 아니라 생성자를 호출해 메모리를 할당- 생성자를 통해 호출하기때문에 new로 메모리를 할당하면 생성 시 초기화가 가능한 장점이 있음
- realloc()은 메모리 할당이 실패할 경우 null이 반환되기 때문에 기존의 메모리가 할당되어있는 포인터를 잃어버리게되는 문제가 있음
8. 오버로딩과 오버라이딩
- 오버로딩: 하나의 클래스에 같은 이름을 가진 메서드를 재정의
- 변환형, 매개변수의 타입이 다르거나 매개변수의 개수가 달라야 함
- 오버라이딩: 클래스 간 상속 관계에서 메서드를 재정의
- 단순 재사용이 아니라 자식 클래스에서 상속 받은 메서드를 재정의하며 다른 연산을 수행토록 함
- 조건: 메서드 이름, 매개변수, 리턴 타입이 같아야 함
9. Volatile 키워드
- Volatile로 선언된 변수는 외부적인 요인으로 그 값이 언제든지 바뀔 수 있음을 뜻함
- 따라서 컴파일러는 volatile 선언된 변수에 대해서는 최적화를 수행하지 않음
- 사용 이유
- Memory-mapped I/O처럼 메모리 주소에 연결된 하드웨어 레지스터에 값을 쓰는 프로그램이라면 이런 컴파일러의 최적화 방식(반복작업 하지 않도록 하는)은 오류를 야기할 수 있음
- 각각의 register writing 과정이 하드웨어에 특정 명령을 전달하는 것이므로, 주소가 같다는 이유만으로 중복되는 쓰기 명령을 없애버리면 하드웨어가 오작동할 수 있음
- 사용되는 분야: 임베디드 프로그래밍, 인터럽트 서비스 루틴의 사용, 멀티 쓰레드 환경
- 공통점은 현재 프로그램의 수행 흐름과 상관없이 외부 요인이 변수 값을 변경 가능하다는 점
10. Call by value, Call by reference
- 함수 호출 시, 메모리 공간 안에서는 함수를 위한 별도의 임시 공간이 생성됨(C++은 스택에 저장)
- 함수 종료시 해당 메모리 공간은 사라짐
- Call-by-value, 즉 값에 의한 호출 방식은 함수 호출 시 전달되는 변수의 값을 복사해 함수의 인자로 전달하는 방식 이며, 이 인자는 함수 안에서 지역적으로 사용되는 local 변수의 성격을 가짐
- 따라서 함수 외부에서 값이 변경돼도 외부 값은 그대로 유지됨
- Call-by-reference, 즉 참조에 의한 호출 방식은 함수 호출 시 전달되는 변수의 레퍼런스(주솟값)를 인자로 전달하는 방식 이며, 함수 안에서 값이 변하면 외부의 전달해준 값도 변경됨
- 그냥 전달받은 변수가 저장된 메모리 주소에 접근해 해당 값을 갖고 연산한다고 생각하면 됨
11. C에서 구조체(struct)의 sizeof 적용시 주의점
typedef struct temp
{
char a;
int b;
}T;
- 위 경우, 1byte의 cher와 4byte의 int의 변수를 가지므로 sizeof(T)를 하면 5가 나올 것으로 예상 할 수 있음
- 하지만, 실제로는 sizeof(T)=8
- 이는 구조체 안에서 가장 큰 자료형을 따라가기 때문이며, 순서에 따라 또 달라짐
12. const 키워드
- 포인터 변수에 const 키워드를 붙이는 방법은 2가지가 있음
const int* MAX = &value
: const가 앞에 붙는 경우로,*MAX = 10
으로 다른값으로 초기화 불가int* const MAX = &value
: const가 뒤에 붙는 경우로,MAX = &other
로 다른 포인터 변수로 초기화 불가const int* const MAX = &value
: 둘 다 불가능 (값 및 다른 포인터 변수로 초기화 불가)- 구조체 멤버 변수에 const 키워드가 붙는 경우 구조체 변수를 생성할 때마다 초기화를 해줘야 함
- 클래스 멤버 변수에 const를 뒤에 붙이면 그 함수 내에선 클래스 멤버 변수 값을 변경 할 수 없음
- 즉, const 함수 내에서는 const 함수가 아니면 호출 불가
- 함수 내에서 매개변수를 const로 상수화시키는 경우는 값이 변하면 안되는 매개변수에 const를 사용해줘 실수로 발생하는 버그를 줄임
13. 함수 포인터
- 함수를 배열 또는 구조체에 넣거나, 함수 자체를 함수의 매개변수로 넘겨주고, 반환값으로 가져오기 위해 만든 포인터
void hello()
{
printf(“Hello, world!\n”);
}
int main()
{
void (*fptr)(); //반환 없는 void, 매개변수가 없는 함수포인터 fptr 선언
// int (*fptr)(int, int); << 반환형이 int이고, 매개변수가 int 2개를 받는 함수포인터 fptr
fp = hello;
fp(); // Hello, world! 출력
}
14. 메모리 단편화를 해결할 수 있는 기법
- 메모리 단편화: 사용가능한 메모리가 충분히 존재해도 할당(사용)이 불가능한 상태
- 내부 단편화: 할당될 크기에 비해 실제 나뉘어 있는 메모리의 크기가 커서 해당 단위 자원 내부에 사용하지 않는 메모리 공간이 발생 할 때를 의미
- 즉, 프로세스가 필요한 양보다 더 큰 메모리가 할당되어서 프로세스에서 사용하는 메모리 공간이 낭비 되는 상황
- ex. 메모장을 사용할 때 OS가 4kb를 할당해줬지만, 실제론 1kb만큼만 사용하고 있다면 3kb만큼의 내부 단편화가 발생함
- 외부 단편화: 나뉘어 있는 단위의 크기보다 할당될 크기가 더 커서 남는 메모리의 전체 합은 충분하나 할당이 불가능할 때를 의미
- 즉, 메모리 할당/해제의 반복 작업에서 사용되지 않는 작은 메모리가 중간중간 존재하게 되고, 이 작은 메모리들이 많아져서 총 메모리 공간은 충분하지만 실제로 할당 할 수 없는 상황
- ex. 32MB 램에 처음에 8mb, 다음에 16mb, 마지막에 8mb 메모리가 할당되는 프로세스가 돌아가고 첫 번째와 마지막 프로세스가 종료된 경우, 총 빈 공간은 16mb가 되지만 9mb가 필요한 새 프로세스에 대한 메모리를 할당할 공간이 부족해지는 현상
- 해결방법
- 압축: 여러 곳에 분산되어 있는 단편화된 메모리 공간들을 결합해서 하나의 큰 공간으로 만듦
- 페이징: 가상메모리 사용, 외부 단편화 해결, 내부 단편화 존재
- 보조기억장치를 이용한 가상메모리를 같은 크기의 블록으로 나눈 것을 페이지라 하고, RAM을 페이지와 같은 크기로 나눈 것을 프레임이라고 할 때
- 사용하지 않는 프레임을 페이지에 옮기고 필요한 메모리를 페이지 단위로 프레임에 옮기는 기법
- 페이지와 프레임을 대응시키기 위해 page mapping 과정이 필요해서 paging table을 만듦
- 페이징 기법을 사용하면 연속적이지 않은 공간도 활용 가능하기에 외부 단편화 문제를 해결 가능
- 하지만 페이지 단위에 맞게 꽉채워 쓰는게 아니므로 내부 단편화의 문제는 여전히 존재
- 페이지 단위를 작게하면 내부 단편화 문제도 해결 가능하지만 page mapping 과정이 복잡해져 효율이 떨어지게 됨
- 세그멘테이션: 논리 메모리와 물리 메모리를 서로 다른 크기의 논리적 단위인 세그먼트로 분할
- 메모리에 적재될 때 빈공간을 찾아 할당하는 사용자 관점의 가상 메모리 관리기법
- 공간들을 계속 쓰다보면 결국 외부 단편화가 발생 할 수 잇음
15. 인터페이스와 추상클래스의 차이
- 추상 클래스의 목적
- 기존의 클래스에서 공통된 부분을 추상화하여 상속하는 클래스에게 구현을 강제화
- 메서드의 동작을 구현하는 자식클래스로 책임을 위임/공유의 목적
- 인터페이스의 목적
- 구현하는 모든 클래스에 대해 특정한 메서드가 반드시 존재하도록 강제
- 인터페이스는 다중 구현이 가능하지만 추상클래스는 다중 상속이 불가능
16. 인라인 함수
- 일반적인 함수를 호출할 때, 새로운 스택을 생성해 프로그램 카운트 루틴을 새롭게 시작하게 됨
- 이 경우 성능저하를 초래할 수 있음
- 인라인 함수는 실제 함수를 호출하는것이 아닌, 정의된 함수가 호출의 시기의 루틴에 그대로 코드를 옮겨놓는 것과 같은 결과를 만들어냄
- 빨라진다!
17. 수많은 생성자에 관해
- 디폴트 생성자: 인수도 없고 내용도 없는 생성자
- 클래스 내에서 아무런 생성자도 정의하지 않은 경우 호출됨
- 내용이 비었기에 멤버 변수는 쓰레기 값을 갖고 있음
- 복사 생성자: 같은 클래스의 객체끼리의 대입을 할 때 사용되는 생성자
- 디폴트 복사 생성자: 같은 클래스 타입의 객체를 인수로 받아 멤버 대 멤버 복사를 하는 생성자
- 얕은 복사: 복사 생성자를 통해 다른 객체가 힙에 할당된 동일한 메모리에 있는 변수를 참조
- 결국 새로운 주소값을 생성한 것이 아닌, 받아온 객체가 갖고 있는 멤버 변수의 메모리에 다리만 얹어놓는 꼴
- 깊은 복사: 이를 위해선 컴파일 상에서 제공된 디폴트 복사 생성자를 오버로딩 해야함
myClass(const myClass &T)
{
a = T.a;
name = new char[strlen(_name) + 1];
strcpy(name, _name);
} // 매개변수로 자신 객체가 들어올 때 상황에 대해 똑같이 한번 더 오버로딩 해줌
18. void 포인터
- 포인터 주소를 저장하는 변수
- 포인터의 크기는 4byte (32bit OS 기준)
- void 포인터는 자료형이 결정되지 않은, 어떤 자료형의 주소든 그 주솟값을 저장하는 포인터
- 어떤 자료형의 주소든 저장 할 수 있음
- 어떤 자료형의 주소든 4byte를 넘지 않음