프로그램의 소스 파일을 실행 가능한 프로그램 파일로 변환하는 것을 의미
컴파일이 필요한 소스 파일들을 컴파일하고 그 결과와 라이브러리 등을 링크하는 처리가 이루어진다
.cpp, .h 소스파일을 컴파일하면 목적파일(.obj)이 나옴
링커로 인해 목적파일과 라이브러리가 합쳐져 실행 가능한 파일이 만들어짐
C++ 프로그램을 컴파일하기 전에 소스 프로그램을 가공하여 컴파일러가 실제로 번역할 소스 프로그램을 만드는 선행처리를 지시하는 명령어
#으로 시작하며 한행에 한 문장만 작성!
🌀대표적인 선행처리
1. 헤더파일 삽입 : #include
2. 매크로 선언/해제 : #define, #undef
3. 조건부 컴파일 : #if(#endif),ifdef ,ifndef
시간에 따라 순서대로 입력되거나 출력되는 일련의 데이터
표준 출력 스트림 객체
std::cout << ~ << std::endl표준 입력 스트림 객체
std::cin >> ~
특정한 명칭들이 인식되는 프로그램의 부분으로
다른 영역에 선언된 명칭과 무관하게 명칭공간 내에서 자유롭게 명칭을 선언하여 사용할 수 있다
동일할 명칭이라도 다른 명칭공간안에 있으면 별개의 것으로 취급!
전역 명칭공간 : 특정 명칭공간에 속하지 않는 기본 명칭공간
std 명칭공간 : 표준 C++ 라이브러리의 명칭들이 정의되어있는 곳 ex) std:: ...
정수 자료형 : char, int, short, long, bool, long long
고정소수점 방식의 수 표현
오버플로가 발생하지 않도록 조심!🌀정수의 리터럴 표현
159 : 10진수 int형 상수
0b10011111 : 2진수 int형 상수
0237 : 8진수 int형 상수
0x9f : 16진수 int형 상수
실수 자료형 : float, double, long double
부동소수점 방식의 수 표현
배열, 구조체, 클래스, 열거형(enum), 공용체(union), 포인터, 참조
프로그램이 실행되는 동안 기억하고 있어야 하는 값들을 저장하는 메모리 영역
자료형 추론
- 변수를 초기화할 때 초기화하는 값의 자료형으로 변수의 자료형을 추론
auto d(10) 는 int d(10)과 동일
const
- 변수의 값을 수정할 수 없게 함
- 초기화를 통해서만 값을 정함
constexpr
- 값을 컴파일 시 평가
- 런타임에 값을 평가하는 것에 비해 효율적으로 동작!
int a;
const b = 10;
std::cin >> a;constexpr int c = a + 10; 🌀컴파일 시 a의 값을 알 수 없기때문에 오류!!
constexpr int d = b + 100; //계산 가능! 110
🐣 자동 변수
🐣 정적 변수
int x; // 전역변수 : 정적 유효기간
void f(){
int y{10}; // 지역변수 : 자동 유효기간
static int z{0} // 정적 지역변수 : 정적 유효기간
}
z는 지역변수이지만 정적 유효기간을 갖는다 f함수가 끝나더라도 사라지지 않고 남아있다가 다음번 f함수가 호출될 때 z의 값을 유지하고 살아있다
🐣묵시적 형변환 (Implicit Conversion)
컴파일러에 의해 자동으로 수행되는 형변환
작은 크기의 데이터 타입을 큰 크기의 데이터 타입으로 자동으로 변환
int 값을 float 값에 할당
정밀도가 낮은 타입을 높은 정밀도의 타입으로 변환
short 값을 int 값에 할당
🐣명시적 형변환 (Explicit Conversion)
컴파일러에게 명확한 변환 요청을 직접 전달
캐스트 연산자
static_cast
연관된 자료형 간의 형 변환을 처리 (컴파일 단계)dynamic_cast
기초클래스와 파생클래스 간의 포인터 또는 참조 형 변환이 프로그램 실행 중 일어나도록 지시!!reinterpret_cast
관련이 없는 자료형 간의 변환const_cast
const를 일시적을 해제
여러가지 자료형의 데이터집합을 저장할 수 있는 새로운 자료형!
클래스와 만드는법은 비슷하다! class sample{}; 와 비슷하게 struct sample{}; 로 만든다!
C언어 에서는 구조체 데이터를 처리하는 함수를 만드려면 구조체와 별개의 함수를 따로 정의해야함!
C++에서는 구조체의 데이터와 그 데이터를 처리하는 함수를 하나로 묶을 수 있다!!
C++에서 구조체와 클래스는 가시성 제어만 빼고는 같다
어떤 대상이 위치한 곳을 가르키는(주소를 저장하는) 변수!
가리키는 대상 : 포인터에 지정된 자료형에 해당되는 변수, 동적으로 할당된 메모리, 참조
Type* ptrVar;
Type* : 가르킬 값의 자료형
ptrVar : 포인터 변수의 이름
포인터가 유효한 대상을 가리키게 한 후 사용해야함!!!
ptrVar = &var // ptrVar이 var을 가리키게 함
*ptrVar = value; // *ptrVar을 이용하여 val에 접근해서 value값을 넣어라!
& : 주소계산 연산자
*ptrVar : 포인터 ptrVar이 가리키는 곳
프로그램 동작 중에 기억공간의 필요성 및 소요량을 결정하여 필요한 공간을 할당하는 것
정적 메모리 할당과 달리 프로그램 컴파일 시간이 아닌 실행 시간에 이루어진다!
🌀 new/delete로 생성과 소멸!
🌀 일반적인 변수는 이름을 가지고 메모리를 사용할 수 있지만
동적 메모리 할당은 이름이 없기 때문에 반드시 포인터가 가르키게 해 주어야함!
delete로 메모리를 반납해서 사용할 수 없는 메모리가 된다해도
포인터는 여전히 가리키고 있으므로 위험!
ptrVar = nullptr; 을 이용해서 연결을 끊어주자
프로그램에서 어떤 데이터를 간접적으로 액세스할 수 있도록 그 데이터를 가리키는 값(주소)
🌈참조와 포인터는 모두 변수를 다른 변수에 연결하는 방법을 제공하지만 차이점이 있다!!!!🌈
- 포인터는 역참조(*) 연산자를 사용하여 값에 접근하지만 참조는 별도의 연산자가 필요하지 않고 일반적으로 일반 변수처럼 사용된다~~
- 포인터는 널 포인터(NULL pointer)로 초기화될 수 있으며 어떤 주소든 저장할 수 있지만
참조는 반드시 선언 시 기존 변수로 초기화되어야 한다 (항상 유효한 객체를 참조해야 한다)참조의 초기화 int number = 10; // 정수형 변수 int& ref = number; // ref가 number의 별명- 참조 변수는 초기화를 통해 지정된 참조 대상을 바꿀 수 없다
참조 유효기간 동안 하나의 대상만 참조할 수 있음int& ref = number; ref = anotherNumber; // 불가능!!
- 🌈 넘겨주기 방식
참조: 함수의 매개변수로 전달될 때 복사가 아닌 원본 변수 자체가 전달
포인터: 함수의 매개변수로 전달될 때 원본 변수의 복사본 또는 원본 변수의 주소가 전달
🌠 함수 호출 시 매개변수로 참조를 사용하면 해당 인자의 복사본이 아닌 원변수 자체가 전달된다
즉!! 함수 내에서 매개변수 값을 수정하면 호출자의 변수도 수정된다!
매개변수로 값을 전달할 때 실제로는 해당 값의 복사본이 전달되므로
함수 내에서 매개변수의 값을 변경해도 원본 변수에는 영향을 주지 않는다
값을 복사하여 새로운 메모리 공간에 저장해야 하므로 메모리 사용량이 늘어남!
특히 큰 객체나 구조체를 처리할 때는 성능상의 이슈가 발생할 수있다!
함수호출 효율성을 위해 참조호출을 할 때
🌠실매개변수의 변경을 원하지 않는다면 형식매개변수에 const 한정어를 지정하여 실매개변수 보호
원본 매개변수의 참조를 형식 매개변수에 전달한다
원본 변수 자체가 함수로 전달되므로 함수 내에서 매개변수 값을 변경하면 원본 변수도 변경된다!
참조 호출 방식은 값의 복사가 아니라 메모리 주소를 전달하기 때문에 메모리 사용량이 일정하거나 증가하는 크기가 값호출 방식보다 작다
반환값 없이도 결과 수정 가능!! 참조 호출을 사용하면 값을 반환하는 대신 매개변수를 수정하여 결과를 수정할 수 있다
컴파일러는 inline 키워드를 가진 함수에 대해 수행할지 여부를 결정한다
컴파일러가 inline 선언을 무시하고 일반 함수로 번역하는 경우!
1. 함수 복잡도가 큰 경우
2. 재귀 호출이 있는 경우
3. 프로그램 내에서 그 함수에 대한 포인터를 사용하는 경우
🌈🌈🌈 JAVA는 기본적으로 값 호출 방식을 사용!!🌈🌈🌈
복사 생성자는 기존의 동일 클래스 객체를 복사하여 새로운 객체를 만들 수 있게 한다
class test{
....
public:
tset(const test& obj){
.... // 생성되는 객체에 obj를 복사하는 처리
}
}
const 로 지정하지 않으면 객체를 수정한다고 판단함
상수객체를 가지고 이 복사생성자를 동작시키게 된다면 에러 발생!
객체를 복사할 때 단순히 메모리의 주소만 복사하여 두 개의 객체가 같은 데이터를 가리키게 하는 방식
#include <iostream>
class Resource {
public:
Resource() {
std::cout << "Resource 생성" << std::endl;
data = new int[5];
}
~Resource() {
std::cout << "Resource 소멸" << std::endl;
delete[] data;
}
private:
int* data;
};
class Test {
public:
Test() {
resource = new Resource();
}
~Test() {
delete resource;
}
private:
Resource* resource;
};
int main() {
Test obj1; // 첫 번째 객체 생성
Test obj2 = obj1; // 두 번째 객체 생성, 얕은 복사
return 0;
}
Test obj2 = obj1; 이 부분에서 묵시적 복사 생성자에 의해 얕은 복사가 수행되고
obj2.resource도 obj1.resource와 같은 메모리 주소를 가리키게 된다
문제는 두 개의 객체(obj1, obj2)가 동일한 리소스를 공유한다는 것
그래서 하나의 객체가 소멸될 때 해당 리소스에 대한 메모리 해제 작업이 수행되고
다른 객체도 여전히 동일한 메모리 주소를 가지고 있으므로 중복으로 메모리 해제 작업이 발생!!
rvalue 참조로 전달된 같은 클래스의 객체의 내용을 이동하여 객체를 만드는 생성자
VecF v1(3), v2(3)
VecF& vLRef = v1; // lvalue 참조로 lvalue를 참조
int& a = 10; // 오류 : lvalue 참조로 rvalue 참조
const int& b = 10; // 상수 lvalue 참조로는 rvalue 참조 가능
int&& c = 30; // rvalue 참조로는 rvalue 참조할 수 있음
VecF&& vRRef1 = v1.add(v2); // 함수의 반환 객체는 rvalue 이다
VecF&& vRRef2 = v2; // 오류: rvalue 참조로 lvalue를 참조할 수 없음
class test{
....
public:
tset(test&& obj){
....
}
}
이동생성자 같은 경우는 const 키워드 사용하지 않음
기존에 정의된 연산자를 사용자가 선언한 클래스의 객체에 대하여 사용할 수 있도록 정의하는 것!
int 연산의 + 와 double 연산의 + 는 다르다!
동일한 연산자라도 구체적인 처리방법은 피연산자의 자료형에따라 다르다
class MyClass {
public:
int value;
// + 연산자 다중정의
MyClass operator+(const MyClass& other) const {
MyClass result;
result.value = this->value + other.value;
return result;
}
};
🌠 이렇게 하면 MyClass 객체끼리 + 연산이 가능
int main() {
MyClass obj1;
obj1.value = 5;
MyClass obj2;
obj2.value = 10;
// 두 개의 MyClass 객체를 더함 (사용자 정의 객체도 연산이 가능하다)
MyClass sum = obj1 + obj2;
std::cout << "Sum: " << sum.value << std::endl; // 출력: Sum: 15
return 0;
}
이동 의미론(Move Semantics)을 지원하기 위해 사용
이동 의미론은 객체의 리소스를 효율적으로 전달하고 복사 비용을 줄이는 개념!
이동 대입 연산자나 이동 생성자와 함께 사용된다
🌠 언제쓰는가?
임시 객체의 소유권 이전
임시 객체나 우측값 참조된 객체를 다른 객체에게 소유권을 전달할 때 std::move를 사용
소유권이 전달되면 원래의 객체는 무효화되며 리소스를 공유하지 않는다
복사 대신 이동
복사 대신 리소스가 많이 소모되거나 큰 크기의 데이터를 가지고 있는 경우
std::move를 사용하여 해당 데이터를 복사하는 대신 이동시킬 수 있다
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string source = "Hello";
// source 문자열을 dest로 이동
std::string dest = std::move(source);
std::cout << "source: " << source << std::endl; // 출력: source:
std::cout << "dest: " << dest << std::endl; // 출력: dest: Hello
return 0;
}
source 문자열은 dest 문자열로 이동!
std::move(source) 호출에 의해 source 문자열의 소유권이 dest에게 전달
source 문자열은 빈 문자열("")이 되고 출력하면 아무것도 나타나지 않는다
class Base {
public:
int publicMember; // 공개 멤버
protected:
int protectedMember; // 보호 멤버
private:
int privateMember; // 비공개 멤버
};
class Derived : protected Base {
public:
void accessBaseMembers() {
publicMember = 10; // 공개 멤버는 파생 클래스 내에서 접근 가능
protectedMember = 20; // 보호 멤버는 파생 클래스 내에서 접근 가능
// privateMember = 30; // 오류: 비공개 멤버는 파생 클래스에서 직접 접근할 수 없다
}
};
int main() {
Derived obj;
// obj.publicMember = 10; 오류: Derived 클래스가 protected로 Base를 상속받았기 때문에
publicMember는 protected 멤버로 상속된다
따라서 외부에서의 직접적인 접근은 허용되지 않는다
// obj.protectedMember = 20; 오류: 보호 멤버는 클래스 외부에서 직접적으로 접근할 수 없다
return 0;
}
class Base {
public:
virtual void someFunction() {
}
};
class Derived final : public Base {
public:
void someFunction() override {
// 파생 클래스에서 오버라이딩된 동작
}
};
// Derived를 상속받으려고 하지만 final로 인해 컴파일 오류가 발생!
class AnotherDerived : public Derived {
public:
void someFunction() override {
// 오류
}
};
class Base {
public:
void display() {
std::cout << "Base 클래스의 display 함수" << std::endl;
}
};
class Derived : public Base {
public:
void display() {
std::cout << "Derived 클래스의 display 함수" << std::endl;
}
};
int main() {
Derived derived;
derived.display(); // Derived 클래스의 display 함수 호출
derived.Base::display(); // Base 클래스의 display 함수 호출
return 0;
}
class Base {
public:
void display() {
std::cout << "Base 클래스의 display 함수" << std::endl;
}
};
class Derived : public Base {
public:
void display() {
std::cout << "Derived 클래스의 display 함수" << std::endl;
}
};
int main() {
Derived derived;
Base* basePtr = &derived; // 파생 객체를 기초 포인터로 가리킴 !!!!!!
basePtr->display(); // 기초 포인터로 호출 시, Base 클래스의 display 함수 호출
return 0;
}
정적 연결은 컴파일시 함수 호출이 어떤 함수에 연결될지 결정되는 것을 의미한다
컴파일러는 포인터의 자료형을 가지고 멤버 함수를 선택하며 실제 객체 유형에 따라
다른 버전의 멤버 함수를 호출하지 않는다!!!
basePtr은 Base 클래스의 포인터로 선언되었기 때문에 basePtr->display()가 호출될 때, 컴파일러는 정적으로 Base 클래스의 display() 멤버 함수와 연결한다
int main() {
Derived derived;
Base* basePtr = &derived; // 파생 객체를 기초 포인터로 가리킴
// 강제 형변환을 사용하여 Derived 클래스로 포인터 타입 변환
Derived* derivedPtr = static_cast<Derived*>(basePtr);
derivedPtr->display(); // Derived 클래스의 display 함수 호출
return 0;
}
강제 형변환을 하여 파생클래스의 함수를 호출 할 수 있지만 위험하다
강제 형변환은 정적 타입 검사가 이루어지지 않기 때문이다
derivedPtr 가 실제로 Derived 를 가리키고 있을지 Base 를 가리키고 있을지
개발자의 판단에만 맡겨야 하니까 위험!!
🌠 기초클래스에서 가상함수로 선언한 멤버하수를 재정의한 파생클래스의 함수는
똑같이 가상함수이다 동적연결이 적용됨!!
class Base {
public:
virtual void display() {
std::cout << "Base 클래스의 display 함수" << std::endl;
}
};
class Derived : public Base {
public:
void display() override {
std::cout << "Derived 클래스의 display 함수" << std::endl;
}
};
int main() {
Derived derived;
Base* basePtr = &derived; // 파생 객체를 기초 포인터로 가리킴
basePtr->display(); // 동적 바인딩에 의해 Derived 클래스의 display 함수 호출
return 0;
}
class Person {
string name;
public:
Person(const string& n) : name(n) {}
string getName() const { return name; }
};
class Student : public Person {
string school;
public:
Student(const string& n, const string& s) : Person(n), school(s) {}
string getSchool() const { return school; }
};
int main() {
// static_cast 예시
Person p("John");
Student s("Alice", "ABC School");
// Person 객체를 Student로 변환
Student* studentPtr = static_cast<Student*>(&p);
// 주소는 변하지만 실제로는 Person 객체이므로 getSchool() 호출 시 예상치 못한 동작 발생 가능
cout << studentPtr->getSchool() << endl;
// dynamic_cast 예시
// 부모 포인터에 자식 객체 주소 대입
Person* personPtr = &s;
// dynamic_cast를 사용하여 실제 타입으로 변환
Student* studentPtr2 = dynamic_cast<Student*>(personPtr);
if (studentPtr2 != nullptr) {
cout << studentPtr2->getSchool() << endl;
} else {
cout << "다운캐스팅 실패" << endl;
}
return 0;
}
static_cast를 사용하여 Person 객체를 Student 포인터로 변환
이 경우 실제로는 Person 객체이기 때문에 studentPtr->getSchool()과 같이
자식 클래스에만 존재하는 멤버 함수를 호출할 때 예상치 못한 동작이 발생할 수 있다
dynamic_cast는 런타임 시점에 안전한 형 변환이 가능한지 확인하는 역할
코드에서는 부모 포인터(personPtr)에 대해 dynamic_cast<Student*>()를 수행하고 반환된 포인터(studentPtr2)가 유효한지(NULL이 아닌지) 확인
만약 다운캐스팅이 성공적으로 이루어진다면 (studentPtr2 != nullptr) 해당 포인터를 통해 자식 클래스의 멤버 함수(getSchool())에 접근할 수 있다
override
파생 클래스에서 부모 클래스의 가상 함수를 재정의할 때 사용된다
컴파일러에게 해당 함수가 부모 클래스의 가상 함수를 오버라이딩하고 있음을 명시적으로 알려줌
만약 부모 클래스에 해당하는 가상 함수와 시그니처가 다른 형태로 파생 클래스에서 재정의한다면
컴파일 에러가 발생!!
class Base {
public:
virtual void foo() {}
};
class Derived : public Base {
public:
void foo() override {} // foo() 메서드 오버라이딩
};
final
가상 함수를 선언하면 해당 함수는 파생 클래스에서 재정의할 수 없다
class Base {
public:
virtual void foo() final {} // 파생클래스에서 foo() 메서드 재정의 금지
};
class Derived : public Base {
public:
// void foo() override {} 에러 발생
};
선언 : virtual void myFunction() = 0;
선언
클래스 정의 시 쉼표로 구분하여 여러 개의 부모 클래스를 명시
class DerivedClass : public BaseClass1, public BaseClass2 { ... }
멤버 접근
범위 지정 연산자(::)를 사용
derivedObj.BaseClass1::someFunction();
가상 기초 클래스(virtual base)를 상속해서 중복 상속을 방지할 수 있음
다중 상속의 결과 1개의 기초클래스가 여러번 상속 받는것을 방지한다는 뜻!
다중 상속 시 발생할 수 있는 다이아몬드 문제(Diamond Problem)를 해결하기 위해 사용되는 개념
class BaseClass {
public:
int value;
};
class DerivedClass1 : virtual public BaseClass { // virtual 키워드 추가
public:
DerivedClass1() {
value = 10;
}
};
class DerivedClass2 : virtual public BaseClass { // virtual 키워드 추가
public:
DerivedClass2() {
value = 20;
}
};
class DiamondDerived : public DerivedClass1, public DerivedClass2 {
public:
DiamondDerived() {
// ...
}
};
extern myObject mobj;라고 선언
myObject 타입의 mobj 변수가 다른 파일에 정의되어 있다는 것을 선언하는 것
이렇게 선언된 전역 변수는 해당 파일을 인클루드하는 모든 C++ 소스 파일에서 사용할 수 있다
하지만 extern 선언만으로는 실제로 메모리에 해당 변수가 할당되지 않는다
따라서 mobj를 사용하기 위해서는 실제로 해당 변수를 정의해야 함!!
다른 소스 파일에서 mobj를 정의하거나 초기화해야 한다
// File1.cpp
extern myObject mobj; // mobj가 다른 파일에 정의되어 있다고 선언
void someFunction() {
// mobj 사용 가능
}
// File2.cpp
myObject mobj; // mobj 실제 정의 및 초기화
void anotherFunction() {
// mobj 사용 가능
}
File1.cpp에서 extern myObject mobj;로 mobj를 다른 파일에 정의되어 있다고 선언
그리고 File2.cpp에서 실제로 myObject mobj;로 mobj를 정의하고 초기화했다
🌠 두 개의 소스 파일에서 현재의 상태를 공유한다
출처 : 방송통신대학교