짧은 설명
- C 컴파일러에는 bool 데이터형이 없다.
- #include <stdbool.h> 헤더파일을 사용한다.
이름짓기 규칙
- 파스칼 : 단어 첫글자를 대문자로 짓는다.
- 카멜 : 첫글자를 소문자로 쓰고, 다음 단어부터 대문자를 쓴다.
- 스네이크 : 전부 소문자로 작성하고 단어는 _(언더바)로 구분한다.
C++ 기본 문법
입출력
- C언어: stdio.h의 printf, scanf 사용
- C++: iostream의 cout, cin 사용
- std::cout - 표준 출력 스트림
- std::cin - 표준 입력 스트림
- std::endl - 줄바꿈 + 버퍼 비우기
- printf와 달리 형식 지정자(%d, %f 등)가 필요 없음
- 타입을 자동으로 인식하여 출력
#include <iostream>
int main() {
std::cout << "Hello, world!" << std::endl;
}
- #include <iostream>: C++의 입출력을 위한 헤더 파일
- input output 스트림의 헤더파일이다.
- std : 표준 라이브러리의 이름이다.
- sfml의 표준 라이브러리 이름은 sf::로 사용한다.
- std::cout: 표준 출력 객체 (console output)
- 전역변수다.
- cout : 아웃 스트림이라는 의미다.
- 출력 스트림 연산자다.
- <<: 출력 연산자 (삽입 연산자)
- 좌항 : 변수
- 우항 : 변수에 맞춰 해야할 일
- 좌항의 변수가 해야할 일의 결과를 리턴한다.
- std::endl: 줄바꿈 + 출력 버퍼 비우기
- endline이라는 뜻이다.
- "Hello, world!" : const character pointer 형이다.
- 순서 : <<가 있는 좌항 우항, 끝나면 마지막 우항이다.
- std::cout << “hello ~~”
- std::cout << std::endl;
여러 타입 출력
using namespace std;
int main() {
int a = 5;
double b = 3.14159265358979;
char c = 'd';
cout << a << ' ' << b << ' ' << c << endl;
}
- using namespace std;: std:: 생략 가능하게 한다.
- 여러 값을 연속으로 출력: << 연산자 체이닝
- 정수, 실수, 문자 모두 형식 지정자 없이 출력
- 소수점 기본으로 5자리까지만 출력된다.
- 바꾸고 싶다면 따로 지정해야한다.
- 소수점 기본으로 5자리까지만 출력된다.
cin 입력받기
int main() {
int a, b;
cin >> a >> b;
std::cout << a << " + " << b << " = " << a + b << std::endl;
}
- cin >> a >> b;: 두 개의 정수 연속 입력
- >>: 입력 연산자 (추출 연산자)
- scanf처럼 & 주소 연산자 불필요
- 공백으로 구분하여 입력
- 12 233 입력 ⇒ 12와 233이 변수에 저장된다.
연산자 재정의
- 연산자를 새로 정의해서 사용하는 것이다.
- 이항, 단항, 왼항 데이터형, 우항 데이터형을 직접 정의할 수 있다.
string
- C언어: char 배열로 문자열 처리한다.
- C++: string 클래스로 편리하게 문자열을 처리한다.
- 크기 자동 조절, 문자열 연산 지원
#include <string>
string name;
string greeting;
cout << "이름 입력: ";
cin >> name;
greeting = "안녕하세요, " + name + " 님!";
cout << greeting << endl;
cout << name << "은 총 " << name.length() << "글자입니다." << endl;
cout << name << "의 첫 글자는 " << name[0] << "입니다." << endl;
- #include <string>: string 클래스 사용을 위한 헤더
- string : 스트링 ‘클래스’다.
- + 연산자로 문자열 연결
- length() 메서드로 문자열 길이 확인
- string 클래스의 멤버함수다.
- [] 연산자로 개별 문자 접근 (0부터 시작)
초기화 방법
int a = 10; // 복사 초기화
int b(20); // 직접 초기화
int c{30}; // 유니폼 초기화 (C++11)
int d = {40}; // 복사 리스트 초기화
- 직접 초기화 : b를 20으로 초기화 한다는 의미
- 이후에 생성자를 통해서 객체를 초기화할때 생성자 인자에 초기화하는 의미로 사용된다.
- 유니폼 초기화 : 최근 생긴 문법이다.
- 중괄호 안에 값을 넣지 않으면 ‘기본값 = 0 or 널포인터’가 들어간다.
- 제약사항이 가장 많다.
- 중괄호 {}를 사용
- 축소 변환(narrowing conversion) 방지
- 모든 타입에 일관된 문법 적용
- double → float 로 값을 넣으면 보통은 암묵적 형변환이 일어나지만 유니폼 초기화로는 컴파일 에러가 일어난다.
- 중괄호 안에 값을 넣지 않으면 ‘기본값 = 0 or 널포인터’가 들어간다.
// 기본적인 초기화
int hp{100}; // 캐릭터 체력
double attackPower{25.5}; // 공격력
// 배열 초기화
int scores[]{100, 85, 90, 95}; // 크기 자동 추론
// 축소 변환 방지 예제
int level = 5.7; // OK: 5로 변환됨 (소수점 버림)
// int level2{5.7}; // 에러: 축소 변환 불가
// 0으로 초기화
int gold{}; // 0으로 초기화
double exp{}; // 0.0으로 초기화
cout << "HP: " << hp << endl;
cout << "공격력: " << attackPower << endl;
cout << "골드: " << gold << endl;
// 구조체 초기화 (미리보기)
struct Player {
int level;
int hp;
int mp;
};
Player player1{10, 100, 50}; // 유니폼 초기화로 구조체 초기화
- 중괄호 초기화는 타입 안전성 제공
- 실수를 정수형에 넣는 실수 방지
- 빈 중괄호 {}는 0으로 초기화
- 배열, 구조체 등 모든 타입에 일관되게 사용 가능
레퍼런스(l-value)와 r-value 레퍼런스
- 간접 참조 / 직접 참조
- 변수에 바로 읽고 쓰기 : 직접 참조
- 포인터를 통해 주소에 접근 후 읽고 쓰기 : 간접 참조
- 레퍼런스(&): 다른 변수의 별명
- 데이터형 마지막에 붙을때는 의미가 달라진다.
- 데이터형& : 데이터형 레퍼런스가 된다.
- 포인터와 유사하지만 더 안전하고 편리
- 포인터가 더 제약사항이 적어 자유롭다는 뜻이다.
- 선언 시 반드시 초기화 필요
- NULL이 될 수 없음
- 포인터는 유효한지 아닌지는 쓰기 전까지 확인이 불가능하다.
int main() {
// 기본적인 레퍼런스 사용
int playerHP = 100;
int& hp = playerHP; // hp는 playerHP의 별명
cout << "원래 HP: " << playerHP << endl;
cout << "레퍼런스 HP: " << hp << endl;
hp -= 30; // 레퍼런스로 값 변경
cout << "공격 후 HP: " << playerHP << endl; // 70
// 레퍼런스와 포인터 비교
int enemyHP = 80;
int* pHP = &enemyHP; // 포인터는 주소를 저장
int& refHP = enemyHP; // 레퍼런스는 별명
*pHP -= 10; // 포인터로 접근
refHP -= 10; // 레퍼런스로 접근 (더 간단)
cout << "적 HP: " << enemyHP << endl; // 60
// 함수 매개변수로 사용
int gold = 1000;
// addGold(gold); // 함수에서 레퍼런스로 받으면 원본 수정 가능
}
// 레퍼런스를 매개변수로 받는 함수
void damagePlayer(int& hp, int damage) {
hp -= damage;
if (hp < 0) hp = 0;
}
- int& : 메모리에 값을 쓸 수 있지만 주소 자체에서로 사용할 수 있다.
- 포인터의 간접 참조처럼 값 변경이 가능해지는 것이다.
- 들어온 주소를 참조할 수 있는 메모리 공간이 생기는 것이다.
- 참조할 대상을 ‘반드시’ 넣어야한다.
- 포인터처럼 넣지만 &를 쓰지않는다.
- 일반 변수 사용하듯이 사용할 수 있다.
- 값 변경도 일어난다.
- 매개변수로 레퍼런스를 받는다면?
- 값이 변경된다.
- 사용처 : 매개변수, 맴버 변수 를 받을 때 사용한다.
레퍼런스 vs 포인터
- 레퍼런스: 선언시 초기화 필수, NULL 불가, 재할당 불가
- 포인터: NULL 가능, 재할당 가능, * 연산자 필요
- 레퍼런스가 더 안전하고 사용하기 편리
r-value 레퍼런스
- int&& max = 200; 형태다.
- 상수, 즉 읽기만 가능한 레퍼런스다.
- 일반 레퍼런스 (l-value)는 읽고 쓰기가 가능하다.
범위 기반 for문
int main() {
int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (int i : arr) {
cout << i << ' ';
}
cout << endl;
}
- 순회가 가능한 배열에서 사용 가능한 문법이다.
- for (int i : arr): 배열의 각 요소를 i에 복사
- 반복 할때마다 i 값은 반복할 때 마다 생기고 사라지기를 반복한다.
- 값만 같지 다른 메모리이다.
- 인덱스 관리 불필요
- 배열 크기를 몰라도 된다.
for (int i : arr) {
i++; // arr의 각 칸에 들어있는 값을 1씩 증가시킨다.
}
for (int i : arr) {
cout << i << ' ';
}
1 2 3 4 5 6 7 8 9 10
- i는 배열 요소의 복사본
- i를 수정해도 원본 배열은 변경되지 않음
for (int& i : arr) { // i를 레퍼런스로 선언하면...
i++; // arr의 각 칸을 바꿀 수 있다.
}
for (int i : arr) { // 여기서는 배열의 내용만 출력하므로 일반 변수로 사용
cout << i << ' ';
}
2 3 4 5 6 7 8 9 10 11
- int& i: 레퍼런스로 받으면 원본 수정 가능
- 읽기만 할 때는 일반 변수 사용
- 수정이 필요할 때는 레퍼런스 사용
함수 오버로딩
- 오버 로딩 : 이른은 같지만 매개변수가 다른 여러 함수를 정의하는 기능이다.
- 매개변수가 달라 다른 함수로 여겨지는 것이다.
- 인자의 데이터형을 확인하고 해당 함수를 실행시키는 것이다.
void swap(int& a, int& b) { // int형 교환하는 함수
int tmp = a;
a = b;
b = tmp;
}
void swap(double& a, double& b) { // double형 교환하는 함수
double tmp = a;
a = b;
b = tmp;
}
void swap(char& a, char& b) { // char형 교환하는 함수
char tmp = a;
a = b;
b = tmp;
}
int main() {
int a = 10, b = 20;
double da = 12.34, db = 56.78;
char ca = 'A', cb = 'B';
swap(a, b); // swap(int &a, int &b) 호출
swap(da, db); // swap(double &a, double &b) 호출
swap(ca, cb); // swap(char &a, char &b) 호출
cout << a << " " << b << endl;
cout << da << " " << db << endl;
cout << ca << " " << cb << endl;
}
- 주의점 : int 매개변수 오버로딩과 int& 매개변수 오버로딩을 하려 했을때 같은 데이터형은 컴파일 에러가 생긴다.
- 매개변수 개수에 따라 다른 동작
- 함수 이름 하나로 여러 상황 처리
디폴트 매개변수
void f(int a = 1) {
cout << a << endl;
}
int main() {
f(123); // a에 123을 대입.
f(); // a에 기본값인 1을 대입.
}
- 위와같은 상황은 초기화만 했기 때문에 오버로딩은 아니다.
void f() ~~
void f(int a = 1) {
cout << a << endl;
}
int main() {
f(123); // a에 123을 대입.
f(); // a에 기본값인 1을 대입.
}
- 이런식이면 컴파일 에러가 발생한다.
int inventory[8] = { 0 };
int score = 0;
void getItem(int itemId, int cnt = 1, int sc = 0) {
inventory[itemId] += cnt; // cnt를 주지 않은 경우, 아이템 개수는 1로 간주
score += sc; // sc를 주지 않은 경우, score는 변화 없음
}
int main() {
getItem(6, 5); // 6번 아이템을 5개 획득
getItem(3); // 3번 아이템을 하나 획득
getItem(5, 4, 7000); // 5번 아이템을 4개 얻고 7000점을 획득
// 보유하고 있는 아이템 개수 출력
for (int i : inventory) {
cout << i << " ";
}
cout << endl;
cout << "점수 : " << score << endl;
}
연산자 오버로딩
- 피연산자의 데이터형에 따라 연산의 결과가 다르게 호출된다.
- 위의 &의 위치와 같은 경우다.
- &변수 : 어드레스 연산자
- 데이터형& : 레퍼런트 변수
객체와 클래스
- 객체 == 변수 == 인스턴스
- 클래스 : 데이터 형
네임스페이스
- 네임스페이스는 이름 충돌을 방지하기 위한 C++의 기능
- 같은 이름의 변수나 함수를 서로 다른 영역에서 사용 가능하다.
- 대규모 프로젝트에서 특히 유용하다.
- 구조체와는 다르다.
- 네임스페이스에 선언된 변수와 함수는 ‘전역 변수 및 함수’로 사용된다.
- namespace의 코드블럭은 전역으로 취급한다.
- 이름은 같지만 다른 영역으로 취급된다.
- 네임스페이스에 선언된 변수와 함수는 ‘전역 변수 및 함수’로 사용된다.
- 네임스페이스 안에 오는것들 : 변수, 함수, 구조체, 네임스페이스 등…
namespace doodle {
int n;
void set() {
n = 20;
}
}
namespace google {
int n;
void set() {
n = 30;
}
}
int main() {
doodle::set(); // doodle 네임스페이스의 set() 호출
google::set(); // google 네임스페이스의 set() 호출
cout << doodle::n << endl; // 20 출력
cout << google::n << endl; // 30 출력
}
- namespace 이름 { } 형태로 정의
- 네임스페이스::멤버 형태로 접근
- 같은 이름의 변수 n과 함수 set()이 각각 다른 네임스페이스에 존재
- 가장 가까운 동일 이름의 메모리에 접근하게 된다.
네임스페이스 외부에서 함수 정의
namespace doodle {
int n;
void set(); // 함수 선언만
}
// 네임스페이스 외부에서 함수 정의
void doodle::set() {
n = 20;
}
int main() {
doodle::set();
cout << doodle::n << endl; // 20 출력
}
- 함수 선언과 정의를 분리 가능
- 정의할 때는 네임스페이스::함수명 형태 사용
중첩 네임스페이스
namespace A {
namespace B {
int a = 5;
}
}
int main() {
cout << A::B::a << endl; // 5 출력
}
- 더 중첩도 가능하다.
using문 - 네임스페이스 생략
namespace doodle {
int a = 10;
int b = 20;
int c = 30;
}
int main() {
// using 선언 - 특정 멤버만 가져옴
using doodle::a;
cout << a << endl; // doodle:: 없이 사용 가능
// using 지시문 - 네임스페이스 전체를 가져옴
using namespace doodle;
cout << b << endl; // b도 바로 사용 가능
cout << c << endl; // c도 바로 사용 가능
}
- using namespace std;를 헤더 파일에서는 사용하지 않는 것이 좋음
- 왠만해서는 using을 사용하지 않는게 좋다.
- 이름 충돌 가능성이 있으므로 주의해서 사용
클래스
- 클래스: 객체를 만들기 위한 설계도 (붕어빵 틀)
- 객체: 클래스로부터 만들어진 실체 (붕어빵)
- C의 구조체를 확장한 개념
- 접근지정자
- public : 외부, 내부 모두 접근 가능한 지정자다.
- private : 내부에서만 접근 가능한 지정자다.
- 구조체, 클래스 모두에 해당되는 규칙이다.
- 내부 및 외부는 코드 블럭을 기준으로 한다.
- 위쪽의 접근권한을 밑의 변수 및 함수가 얻게되는 것이다.
- 접근권한은 번갈아 가면서 사용할 수 있다.
struct TV { // struct도 클래스처럼 사용 가능
private:
bool powerOn;
int channel;
int volume;
public:
void on() {
powerOn = true;
}
void off() {
powerOn = false;
}
private:
bool a;
int b;
int c;
};
int main() {
TV mytv; // TV 클래스의 객체 생성
mytv.on();
mytv.setChannel(10);
mytv.setVolume(50);
cout << "현재 채널은 " << mytv.getChannel() << endl;
cout << "현재 볼륨은 " << mytv.getVolume() << endl;
}
- private: 클래스 외부에서 접근 불가
- public: 클래스 외부에서 접근 가능
- getter/setter 메서드로 private 멤버에 안전하게 접근
- 데이터 유효성 검사 가능 (채널 범위, 볼륨 범위)
접근 지정자 - 클래스와 구조체 차이
class TV { // class 키워드 사용
bool powerOn; // 기본적으로 private
int channel; // 기본적으로 private
int volume; // 기본적으로 private
public: // 여기부터 public
void on() {
powerOn = true;
}
// ... 나머지 메서드들
};
struct TV2 { // struct 키워드 사용
bool powerOn; // 기본적으로 public
int channel; // 기본적으로 public
int volume; // 기본적으로 public
// 메서드들도 기본적으로 public
void on() {
powerOn = true;
}
};
- class: 기본 접근 지정자가 private
- struct: 기본 접근 지정자가 public
- C++에서는 struct도 클래스처럼 메서드를 가질 수 있음
- 그 외에는 차이점이 많이 없다.
- 사용처에 따라서 사용하면 된다.
- 수치화된 데이터, 멤버를 모두 공개해도 되겠다 : 구조체
- 몬스터, 플레이어, 게임보드 등 멤버 변수 및 함수가 필요한 경우 : 클래스
- 클래스는 멤버에 직접 접근을 대체로 막기때문에 직접 접근을 해야한다면 구조체로 만드는게 좋다.
- 모르겠으면 클래스로 만드는게 좋다.
this 포인터
class MyClass {
private:
int value;
public:
void getValue()
{
return value;
}
void setValue(int value) {
this->value = value; // this->value는 멤버 변수
// value는 매개변수
}
void printThis() {
cout << "this 포인터의 값: " << this << endl;
}
};
int main() {
MyClass obj1, obj2;
cout << "obj1의 주소: " << &obj1 << endl;
obj1.printThis();
obj1.setValue(20);
cout << "obj2의 주소: " << &obj2 << endl;
obj2.printThis();
}
- 클래스는 변수에 대한 get, set 함수가 기본이다.
- 매개변수와 멤버 변수 이름이 같을 때 구분
- 객체마다 고유한 this 포인터 값을 가짐
- 해당 함수를 포함하는 객체의 주소를 가져오는 것이다.
- this에 외부에서 선언한 객체의 주소가 할당되는 것이다.
- 객체가 선언될때 this의 메모리가 생성된다.
- 메서드 체이닝에 활용 가능
- this는 클래스의 멤버 함수 내부에서만 사용 가능하다.
메서드 체이닝
- *this를 사용하는 문법이다.
class Position
{
private:
int x, y;
public:
Position& refReturn()
{
return *this;
}
void init()
{
this->x = 0;
this->y = 0;
std::cout << "초기 위치: (" << this->x << ", " << this->y << ")" << std::endl;
}
Position& moveX(int x)
{
this->x += x;
return *this;
}
Position& moveY(int y)
{
this->y += y;
return *this;
}
void getPosition()
{
std::cout << "(" << this->x << ", " << this->y << ")" << std::endl;
}
};
int main()
{
//연습문제 3 - 7 - Position 클래스 (메서드 체이닝)
Position pos;
Position& refPos = pos.refReturn();
pos.init();
refPos.moveX(10).moveY(20).moveX(5);
std::cout << "이동 후 위치: ";
pos.getPosition();
return 0;
}
- *this : this의 결과 즉, 객체를 가리킨다.
- 반환형이 레퍼런스이니 즉, 객체를 바꿀수있는 형태로 반환하는 것이다.
- 반환형을 *this.를 반환하면 해당 객체를 레퍼런스 형태로 바꾸게 된다.
- 여러번 연결시켜서 반환이 가능해진다.