C++ stack 영역

C++/C++ 문법 2019. 12. 17. 09:01
728x90

C 및 C++ 에서 변수가 어떻게 저장되는지를 예제를 통해서 알아보자.

[그림 출처] : http://myblog.opendocs.co.kr/archives/1301


전역변수 및 상수는 data 영역을 사용한다.

지역변수 앞에 static을 붙이면 전역변수가 된다.

Heap 메모리 : 힙 영역에 저장되고 지속성이 보장되나 프로그래머에의해 꼼꼼한 관리(메모리 해제)가 필요함.

stack은 LIFO(Last In First Out) 방식의 시퀀스이다.
지역변수, 매개변수는 stack 영역을 사용한다.


#include <iostream>
using namespace std;

void sub(int x) {
    int k = 2;
    cout << "x = " << x << ", x 주소 : " << (uintptr_t)&x << endl;
    cout << "k = " << k << ", k 주소 : " << (uintptr_t)&k << endl;
}

int main() {
    int a = 3;
    cout << "a = " << a << ", a 주소 : " << (uintptr_t)&a << endl;
    sub(a);
}

실행결과

맨 처음에 a 주소가 할당되면서 주소값이 가장 크다는 걸 알 수 있고,

k주소값은 맨 나중에 할당되면서 주소값이 적다는 걸 알 수 있다.

여기서 int a = 3 을 static int a = 3 으로 변경해서 주소값이 어떻게 바뀌는지 확인해보라.

그리고 static int k = 2; 으로 변경해서 주소값을 확인해 보라.



sub 함수를 호출할 때 사용하는 주소가 어떻게 다른지 확인해보자.

#include <iostream>
using namespace std;

void sub(int x) { // int x 는 매개변수
    cout << "x = " << x << ", x 주소 : " << (uintptr_t)&x << endl;
}

int main() {
    sub(1);
    sub(3);
    sub(5);
}

실행결과 → 매개변수는 모두 동일한 메모리 주소를 사용

함수가 매개변수 주소를 할당하고, 반납하고, 다시 같은 주소에 값을 할당하고, 반납한다는 걸 알 수 있다.


재귀함수 호출을 해 봄으로써 변수가 어떻게 저장되는지 확인해보자.

#include <iostream>
using namespace std;

void sub(int x) {
    cout << "x = " << x << ", x 주소 : " << (uintptr_t)&x << endl;
    if(x > 1) sub(x - 1);
}

int main() {
    sub(1);
    sub(3);
    sub(5);
}

실행결과

주소가 어떻게 할당되는지 확인해 보면,

sub(1)을 실행시 매개변수 1에 대한 주소값을 사용하고, 주소를 반납한다.

sub(3)을 실행시 매개변수 3에 대한 주소값이 sub(1) 실행시의 주소값과 같음을 확인할 수 있다.

재귀함수 sub(2)의 주소는 처음할당된 주소값보다 작음을 알 수 있다.

재귀함수 sub(1)의 주소는 더 적은 값임을 알 수 있다.

sub(5)을 실행시 매개변수 5에 대한 주소값, 재귀함수 sub(4), sub(3), sub(2), sub(1)에 대한 주소값을 확인해보면,

맨 처음에 할당된 주소값이 가장 크고, 위로 갈 수록 주소값이 점점 적어지는 걸 확인할 수 있다.



예제1. 출처 : https://www.softwaretestinghelp.com/stack-in-cpp/


#include<iostream>
using namespace std;

#define MAX 1000 //max size for stack

class Stack {
    int top;
public:
    int myStack[MAX]; //stack array

    Stack() { top = -1; }
    bool push(int x);
    int pop();
    bool isEmpty();
};

//pushes element on to the stack
bool Stack::push(int item) {
    if (top >= (MAX - 1)) {
        cout << "Stack Overflow!!!";
        return false;
    }
    else {
        myStack[++top] = item;
        cout << item << endl;
        return true;
    }
}

//removes or pops elements out of the stack
int Stack::pop() {
    if (top < 0) {
        cout << "Stack Underflow!!";
        return 0;
    }
    else {
        int item = myStack[top--];
        return item;
    }
}

//check if stack is empty
bool Stack::isEmpty() {
    return (top < 0);
}

// main program to demonstrate stack functions
int main() {
    class Stack stack;
    cout << "The Stack Push " << endl;
    stack.push(10); // 스택에 등록
    stack.push(20);
    stack.push(30);

    cout << endl << "The Stack Pop : " << endl;
    while (!stack.isEmpty()) { // 스택이 비어 있지 않는 동안
        cout << stack.pop() << endl;
    }
    return 0;
}


예제2.

#include <iostream>
#include <vector>
#include <stack>
using namespace std;

int main() {

    stack<int> st;  // default 는 deque 컨테이너 사용
    //stack<int, vector<int>> st;  vector 컨테이너를 이용하여 stack 컨테이너 생성

    st.push(10);    // stack에 등록
    st.push(20);
    st.push(30);

    cout << st.top() << endl;   // stack 제일 위의 요소 가져오기
    st.pop();  // 스택 제일 위의 요소 제거
    cout << st.top() << endl;
    st.pop();
    cout << st.top() << endl;
    st.pop();

    if (st.empty())  // stack이 비었는지 확인
        cout << "stack에 데이터 없음" << endl;

    return 0;
}
 

실행해보면, stack 에 저장된 순서가 10, 20, 30 으로 맨 아래부터 저장되어 맨 위에 30이 저장되어 있음을 확인할 수 있다.

728x90
블로그 이미지

Link2Me

,
728x90

C++ 에서 변수 앞에 const를 붙여 상수화가 되어, 그 값을 변경하지 못하게 한다.

변경하지 않아야 하는 변수에 엉뚱한 값을 할당하여, 에러가 발생할 경우 찾아내기가 쉽지 않다.

const 선언은 코드의 안정성을 높이기 위한 것이다.
https://www.youtube.com/watch?v=pFo84VEZCAM 에 const 동영상 강좌 설명이 참 잘 되어 있다.

C언어의 문법과 C++ 문법이 동일한 것은 C언어 강좌를 들어서 이해하는 것이 좋을 거 같다. 유튜브의 다양한 강좌를 들어보고 가장 마음에 들고 좋은 건 링크를 걸어서 적어둔다.


const 변수타입 변수명 = 초기화 값; // 선언시 초기화 값을 넣어야 된다. 값을 변경할 수 없다.


ex) const double PI = 3.14139;


const 변수타입 *변수명; // 변수명(포인터)이 가리키는 주소의 값을 변경할 수 없다.

#include <iostream>
using namespace std;
int main() {
    int* pta;
    int a = 10;
    pta = &a;
    *pta = 5;

    cout << "*pta : " << *pta << endl;
    cout << "a : " << a << endl;

    const int* ptb; // 변수명이 가리키는 주소의 값을 상수화 → 포인터가 가리키는 값을 상수화
    int b = 20;
    ptb = &b;
    //*ptb = 12; // 컴파일 에러. ptb가 가리키는 주소의 값이 상수화되어 변경할 수 없다.
    b = 30; // 값은 변경할 수 있다.


   cout << "*ptb :" << *ptb << endl;


    int c = 15;
    ptb = &c; // 포인터 변수에 다른 주소를 할당할 수는 있다.
    cout << "*ptb :" << *ptb << endl;
}
 


변수타입 * const 변수명 = 초기화값; // 포인터 변수를 상수화한다.

#include <iostream>
using namespace std;
int main() {
    int a;
    int* const pta = &a;
    int b;
    *pta = 10; // pta 가 가리키는 주소의 값을 변경할 수 있다.
    pta = &b; // 컴파일 에러. pta (포인터)가 가리키는 주소가 상수화되어 변경할 수 없다.
} 


const 변수타입 * const 변수명 = 초기화 값; // 변수명이 가리키는 주소와 그 주소의 값도 변경할 수 없다.

#include <iostream>
using namespace std;
int main() {

    int a;
    const int* const pta = &a;
    int b;
    a = 15; // 직접 값을 변경할 수 있다.
    *pta = 10; // 컴파일 에러. pta 가 가리키는 주소의 값이 상수화되어 변경할 수 없다.
    pta = &b; // 컴파일 에러. pta 가 가리키는 주소가 상수화되어 변경할 수 없다.

}



메소드의 상수화

#include <iostream>
using namespace std;

class Account {
private:
    int money; // 멤버 변수
public:
    Account() : money(0) {}
    Account(int money) : money(money) {}

    void Deposit(const int d) { // 매개변수 상수화
        money += d;
        cout << d << "원을 예금" << endl;
    }

    void Withdraw(const int d) { // 매개변수 상수화
        if (money >= d) {
            money -= d;
            cout << d << "원을 인출" << endl;
        }
    }

    int viewMoney() const { // 메소드의 상수화
        // money++; // 컴파일 에러. 멤버 변수 값을 변경할 수 없다.
        return money;
    }
};

int main() {

    Account account(1000);

    cout << "잔고 : " << account.viewMoney() << "원" <<endl << endl;

    account.Deposit(1000);

    cout << "잔고 : " << account.viewMoney() << "원" << endl << endl;

    account.Withdraw(500);

    cout << "잔고 : " << account.viewMoney() << "원" << endl << endl;
   
}
 


728x90

'C++ > C++ 문법' 카테고리의 다른 글

C++ stack 영역  (0) 2019.12.17
C++ STL Vector(벡터)  (0) 2019.12.01
C++ 포인터 이해  (0) 2019.11.30
C++ 클래스 상속  (0) 2019.11.29
C++ 이동 생성자(move constructor), 이동 대입 연산자(Move assignment operator)  (2) 2019.11.28
블로그 이미지

Link2Me

,
728x90

표준 템플릿 라이브러리라고 불리는 STL은 자료와 알고리즘을 효율적으로 관리할 수 있도록 C++에서 제공하는 표준 라이브러리다.

컨테이너(container)는 어떤 종류의 객체 컬렉션(collection)을 관리하는데 사용된다.


순차 컨테이너, sequence containers
순차 컨테이너는 모든 요소들을 선형적인 순차열로 저장하는 형태다.
원소들이 규칙에 따라 저장한 순서대로 배열이 된다.
벡터(vector), 데큐(deque), 리스트(list)가 있다.

연관 컨테이너, associative containers
연관 컨테이너는 키(key)와 값(value) 쌍처럼 관련된(연관된) 데이터를 하나의 쌍 형태로 저장한다.
키와 값을 이용하여 요소들에 대한 빠른 접근을 제공한다.
세트(set), 멀티세트(multiset), 맵(map), 멀티맵(multimap) 가 있다.

Vector

ㅇ C++에서 벡터는 배열의 기능을 확장한 것이다.
ㅇ 배열은 동적할당을 하면 그 크기를 바꿀 수 없지만, 벡터는 크기를 자유롭게 변경할 수 있다.
ㅇ Vector의 특성

    - 연속적인 공간에서 원소를 저장/관리

    - 단방향 원소 저장을 위한 container

    - push_back(element) : 벡터 요소를 마지막에 추가

    - pop_back(element) : 벡터의 마지막 요소를 삭제

    - size() : 벡터 내의 요소수를 반환

    - capacity() : 할당된 공간 크기를 리턴
    - at(index) : 지정 인덱스에 있는 요소 반환

    - empty() : 벡터가 비어 있으면 true 반환

    - clear() : 벡터의 모든 요소 삭제

    - container 에 보관된 원소에 접근할 때 반복자(iterator)를 사용한다.

      ▶ begin() : 첫번째 원소를 가리킨다.

      ▶ end() : 마지막 원소를 가리킨다.

      ▶ erase(iterator) : iterator가 가리키는 원소 제거


ㅇ vector의 선언

    - #include <vector> 로 헤더를 포함해야 한다.

    - std::vector<자료형> 이름; // using namespace std;를 사용하시면 std:: 생략이 가능

      vector<int> iv(10) : 0으로 초기화 된 10개의 원소를 가지는 vector iv를 생성

      vector<int> iv(10, 1) : 1로 초기화된 10개의 원소를 가지는 vector iv를 생성


예제1

#include <iostream>
#include <string>
#include <vector>

using namespace std;

int main() {
    vector<int> iv; // int 타입 벡터 선언 및 비어있는 벡터 생성

    // 벡터 데이터 생성
    for (int i = 0; i < 10; i++) {
        iv.push_back(i+1);
    }

    // for문을 이용한 데이터 출력
    for (int i = 0; i < iv.size(); i++) {
        cout << iv[i] << " ";
    }
    cout << endl;

    cout << "size : " << iv.size() << endl;

    iv.push_back(15); // 벡터 요소 추가
    iv.push_back(16); // 벡터 요소 추가

    // 반복자를 활용한 데이터 출력
    vector<int>::iterator iter;
    for (iter = iv.begin(); iter != iv.end(); iter++) {
        cout << *iter << " ";
    }
    cout << endl;

    cout << "size : " << iv.size() << endl;

    iv.pop_back(); // 벡터 마지막 요소 삭제

    // 반복자를 활용한 데이터 출력
    for (iter = iv.begin(); iter != iv.end(); iter++) {
        cout << *iter << " ";
    }
    cout << endl;

    return 0;
}

실행결과


728x90
블로그 이미지

Link2Me

,

C++ 포인터 이해

C++/C++ 문법 2019. 11. 30. 11:50
728x90

C++ 포인터에 대한 이해를 얼마나 하고 있는지 복습하는 예제다.

포인터형 변수는 16진수로 표기된다.

cout << " x = " << x << ", x 주소 : " << (uintptr_t)&x << endl;

포인터 주소를 10진수로 출력되도록 하려면 uintptr_t 로 형변환을 하면 된다.


 - 포인터는 변수다.

 - C언어 및 C++ 언어에서는 주소도 데이터다.

 - 포인터 변수는 메모리 번지를 저장한다.

 - 자신의 자료형과 같은 주소를 값으로 갖는다.

 - 포인터의 크기는 자료형에 상관없이 4bytes이다.


int a = 100; // 변수의 선언, 자료형 변수 = 값;
int* ptr; // 포인터의 선언
ptr = &a; // 주소 연산자는 변수의 이름 앞에 사용하여, 해당 변수의 주소값을 반환


주소 연산자(&)
주소 연산자는 변수의 이름 앞에 사용하여, 해당 변수의 주소값을 반환한다.
'&'기호는 앰퍼샌드(ampersand)라고 읽으며, 주소 연산자 또는 번지 연산자 라고 불린다.


역참조 연산자(*)
역참조 연산자는 포인터의 이름이나 주소 앞에 사용하여, 포인터가 가리키는 주소에 저장된 값을 반환한다.


포인터 선언

int* ptr; // 올바른 예

int* ptr1, ptr2; // 잘못된 예

int *ptr1, *ptr2; // 올바른 예

두개 이상의 int형 포인터를 선언하고 싶을 때에는 각각의 포인터 변수 이름 앞에 역참조 연산자(*)를 따로 사용하여 선언해야 한다.


int a = 7;         // 변수의 선언
int* ptr = &a;     // 포인터의 선언 및 주소 할당

// 자료형 변수 = 값;

// 자료형 : int*, 변수 : ptr, 값:&a

// ptr도 변수이므로 &ptr 즉 주소를 갖는다.
int **pptr = &ptr; // 더블 포인터 : 포인터의 포인터


예제1

#include <iostream>
using namespace std;

int main() {
    // 주소 연산자(&) : 변수의 이름 앞에 사용하여, 해당 변수의 주소값을 반환한다.
    // 역참조 연산자(*) : 포인터의 이름이나 주소 앞에 사용하여, 포인터가 가리키는 주소에 저장된 값을 반환한다.

    int a = 10, b = 20, c = 30;
    cout << endl << " a = " << a << ", b = " << b << ", c = " << c << endl;


    int* p = &a; // 포인터 선언 및 a 변수의 주소 할당
    cout << endl << " *p = " << *p << " // 포인터 p → a " << endl;
    cout << " a = " << a << endl;

    int &r = b; // 참조변수 선언 및 b 변수 할당
    cout << endl << " r = " << r << " // reference r → b" << endl;
    cout << " b = " << b << endl;

    int** pp = &p; // 더블 포인터 : p 포인터의 포인터
    cout << endl << " **pp = " << **pp << " // 포인터 p → a " << endl;
    cout << " a = " << a << endl;

    int* (&rp) = p; // p 포인터에 reference 변수 rb 선언
    cout << endl << " *rp = " << *rp << " // reference rp → p포인터 p → a " << endl;
    cout << " a = " << a << endl;

    r = c / *p;
    cout << endl << " r = " << r << " // reference r → b " << endl;
    cout << " b = " << b << endl;

    rp = &c; // rp 즉 p 포인터 변수에 c변수 주소 할당
    cout << endl << " *rp = " << *rp << " // reference rp → p → c " << endl;
    cout << " c = " << c << endl;

    **pp = 40;
    cout << endl << " **pp = " << **pp << " // 포인터 p → c " << endl;
    cout << " c = " << c << endl;

    *p = 50;
    cout << endl << " *p = " << *p << " // 포인터 p → c " << endl;
    cout << " c = " << c << endl;

    *pp = &a;
    cout << endl << " **pp = " << **pp << " // 포인터 p → a " << endl;
    cout << " a = " << a << endl;

    *rp = 60;
    cout << endl << " *rp = " << *rp << " // reference rp → p, 포인터 p → a  " << endl;
    cout << " a = " << a << endl;

    cout << endl << "***** 결과 출력 *****" << endl;
    cout << " a = " << a << endl;
    cout << " b = " << b << endl;
    cout << " c = " << c << endl;
    cout << " *p = " << *p << endl;
    cout << " r = " << r << endl;
    cout << " **pp = " << **pp << endl;
    cout << " *rp = " << *rp << endl;
}

실행결과



참고하면 좋은 자료

C언어 포인터 인강 ② 포인터 때문에 미쳐 버릴 것 같은 당신께 https://www.youtube.com/watch?v=LtmJm068muw&t=31s

728x90
블로그 이미지

Link2Me

,

C++ 클래스 상속

C++/C++ 문법 2019. 11. 29. 21:24
728x90

C++에서 클래스 상속(inheritance)이란 기존에 정의되어 있는 클래스의 모든 멤버 변수와 멤버 함수를 물려받아, 새로운 클래스를 작성하는 것을 의미한다.
ㅇ 기존에 정의되어 있던 클래스를 base class 또는 parent class 또는 super class 라고 한다.
ㅇ 이를 상속받아(→모든 특성을 물려받아) 새로 만들어지는 클래스를 파생(derived) 클래스 또는 자식 클래스, 하위(sub) 클래스라고 한다.
ㅇ 기존에 작성된 클래스를 재활용할 수 있다.
ㅇ class 파생클래스명 : public 부모클래스명 {
       // 접근 제어 지시자 : public, protected, private

       // 상속시 부모 클래스에서 접근 가능한 멤버는 protected와 public 뿐이다.
   }


접근제어 지시자

 public

 클래스 외부 접근 허용

 자식 클래스 접근 허용

 클래스 내부 접근 허용

 proteced 로 상속을 받으면 부모 클래스를 proteced 로 상속함

 protected

 클래스 외부 접근 불가

 자식 클래스 접근 허용

 클래스 내부 접근 허용

 

 private

 클래스 외부 접근 불가

 자식 클래스 접근 불가

 클래스 내부 접근 허용

 


예제는 헤더파일(선언부)와 cpp 파일(정의부)를 분리하는 걸 고려하여 코드를 작성했다.

예제1

#include <iostream>
#include <string>

using namespace std;

class Person {
private:
    string name;
    int age;
public:
    Person(); // 기본 생성자
    Person(const string& name, int age);
    ~Person(); // 소멸자

    void setName(string name);
    void setAge(int age);

    string getName();
    int getAge();
};

Person::Person() {
    cout << " Person 기본 생성자 호출- 메모리 주소 : " << this << endl;
    this->name = "";
    this->age = 0;
}

Person::Person(const string& name, int age) {
    cout << " Person 매개변수 2개 생성자 호출- 메모리 주소 : " << this << endl;
    this->name = name;
    this->age = age;
}

Person::~Person() {
    cout << " ~Person() 호출- 메모리 주소 : " << this << endl;
}

void Person::setName(string name) {
    this->name = name;
}

void Person::setAge(int age) {
    this->age = age;
}

string Person::getName() {
    return this->name;
}

int Person::getAge() {
    return this->age;
}

class UStudent : public Person { // 접근 제어 지시자를 변경해보면서 에러 발생을 확인해 보자
private:
    string id; // 학번
    string major; // 전공과목
public:
    UStudent();
    UStudent(const string& id, const string& major, const string& name, int age);
    ~UStudent();

    void setId(string id);
    void setMajor(string major);

    string getId();
    string getMajor();

    void Display(); //메시지 출력
};

UStudent::UStudent() {
    cout << " UStudent 기본 생성자 호출- 메모리 주소 : " << this << endl;
}

UStudent::UStudent(const string& id, const string& major, const string& name, int age) : Person(name, age) {
    cout << " UStudent 매개변수 4개 생성자 호출- 메모리 주소 : " << this << endl;
    this->id = id;
    this->major = major;
}

UStudent::~UStudent() {
    cout << " ~UStudent() 호출- 메모리 주소 : " << this << endl;
}

void UStudent::setId(string id) {
    this->id = id;
}

void UStudent::setMajor(string major) {
    this->major = major;
}

string UStudent::getId() {
    return this->id;
}

string UStudent::getMajor() {
    return this->major;
}

void UStudent::Display() {
    cout << " 성명 : " << this->getName() << ", 나이 : " << this->getAge() << ", 학번 : " << this->getId() << ", 전공 : " << this->getMajor() << endl;
}

int main() {

    UStudent std1; // 파생 클래스의 객체를 생성하면, 제일 먼저 기초 클래스의 생성자를 호출
    std1.setName("이순신");
    std1.setAge(20);
    std1.setId("20190001");
    std1.setMajor("전기전자공학부");
    std1.Display();

    cout << endl;

    UStudent std2("20160002", "사회복지학부", "홍길동", 23);
    std2.Display();

    cout << endl;
    // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.

// 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.

출처: https://link2me.tistory.com/1756?category=1075719 [소소한 일상 및 업무TIP 다루기]
// 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.

출처: https://link2me.tistory.com/1756?category=1075719 [소소한 일상 및 업무TIP 다루기]

}

실행결과



참고하면 도움될 게시글

상속과 접근 제어 유투브 강좌 : https://www.youtube.com/watch?v=yWI8GTLsBR8

상속 접근 지정자 : https://thrillfighter.tistory.com/531


728x90
블로그 이미지

Link2Me

,
728x90
C++에서 모든 표현식은 Lvalue 또는 Rvalue 이다.

C++11 표준에서는 Lvalue 참조자 외에 Rvalue를 참조할 수 있는 Rvalue 참조자를 추가했다.

Rvalue 참조자는 Visual Studio 2010 이상에서 사용 가능하다.


https://docs.microsoft.com/ko-kr/cpp/cpp/move-constructors-and-move-assignment-operators-cpp?view=vs-2019 에 이동 생성자(Move constructor) 및 이동 대입 연산자(Move assignment operator)에 대한 사용법이 나와있다.


int a = 3;
a 는 메모리 상에서 존재하는 변수다. a 의 주소값을 & 연산자를 통해 알아 낼 수 있다.
보통 이렇게 주소값을 취할 수 있는 값을 Lvalue 라고 부른다.
그리고 Lvalue는 어떠한 표현식의 왼쪽 오른쪽 모두에 올 수 있다(왼쪽에만 와야 하는게 아니다).

반면에 오른쪽에 있는 '3' 을 살펴보자. '3' 은 주소값을 취할 수 있는게 아니다.
'3' 은 위 표현식을 연산할 때만 잠깐 존재하고 연산후에는 사라지는 값이다. 즉, '3' 은 실체가 없는 값입니다.
이렇게, 주소값을 취할 수 없는 값을 Rvalue 라고 부른다.
Lvalue가 식의 왼쪽 오른쪽 모두 올 수 있는 반면, Rvalue는 식의 오른쪽에만 존재해야 한다.

C++ 에서 int& b = a; 형태로 사용하는 reference(참조자, 참조변수)는 Lvalue 참조자이다.


C++ 클래스에는 기본 생성자, 복사 생성자, 이동 생성자가 있다.

포인터를 멤버로 가지는 클래스는 복사 생성자에서 깊은 복사(deep copy)를 수행한다.

C++ 11에는 Rvalue reference를 파라미터로 갖는 새로운 타입의 생성자가 추가되었으며, 이를 이동 생성자(move constructor)라고 한다. 이동 생성자는 호출되면 얕은 복사(shallow copy)를 하고 원본의 소유권을 대상으로 이전(move)하는 방식으로 객체를 생성한다. 원본 객체를 NULL로 초기화하여 접근할 수 없게 만든다.


Person::Person(Person&& other) noexcept {
    name = other.name; // 얕은 복사
    phone = other.phone; // 얕은 복사
    age = other.age;

    // 임시 객체 소멸 시에 메모리를 해제하지 못하게 한다.
    other.name = nullptr; // nullptr 는 C++ 11 에 추가된 키워드로, 기존의 NULL 대체
    other.phone = nullptr; // 이전 객체에서 pointer는 삭제
    other.age = 0;
}


이동생성자의 호출 조건
1. 임시객체를 전달할 때.
2. std::move()를 사용하여 인자를 rValue참조자라 불리는걸로 변환하여 전달할때.


예제 1과 같이 이동 생성자와 복사 생성자를 모두 구현해 놓으면, 코드 상황에 따라 복사가 되거나, 이동이 된다.

예제1.

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class Person {
    char* name;
    char* phone;
    int age;
public:
    Person();
    Person(const char _name[], const char _phone[], int _age);
    ~Person();
    Person(const Person& p); // 복사 생성자 선언
    Person& operator=(const Person& p); // 복사 이동 연산자 선언
    Person(Person&& p) noexcept; // 이동 생성자 선언
    Person& operator=(Person&& p) noexcept; // 이동 복사 연산자 선언
    void ShowData(); // 선언

    //Setter method
    void setName(const char _name[]) {
        name = new char[strlen(_name) + 1]; // NULL문자를 고려하여 +1만큼 할당
        strcpy_s(name, strlen(_name) + 1, _name);
    }
    void setPhone(const char _phone[]) {
        phone = new char[strlen(_phone) + 1];
        strcpy_s(phone, strlen(_phone) + 1, _phone);
    }
    void setAge(int age) { this->age = age; }

    // Getter method
    string getName() { return name; }
    string getPhone() { return phone; }
    int getAge() { return age; }
};

Person::Person() {
    cout << " 매개변수 없는 생성자 호출- 메모리 주소 : " << this << endl;
    name = NULL;
    phone = NULL;
    age = 0;
}

Person::Person(const char _name[], const char _phone[], int _age) {
    cout << " 매개변수 3개 생성자 호출- 메모리 주소 : " << this << endl;
    name = new char[strlen(_name) + 1]; // NULL문자를 고려하여 +1만큼 할당
    strcpy_s(name, strlen(_name) + 1, _name);

    phone = new char[strlen(_phone) + 1];
    strcpy_s(phone, strlen(_phone) + 1, _phone);

    age = _age;
}

Person::~Person() { // DeAllocate the heap
    cout << " 소멸자 호출- 메모리 주소 : " << this << endl;
    delete[]name;
    delete[]phone;
}

Person::Person(const Person& p) : age(p.age) {
    cout << " Copy constructor 호출- 메모리 주소 : " << this << endl;
    name = new char[strlen(p.name) + 1]; // NULL문자를 고려하여 +1만큼 할당
    strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사
    // The statement name = new char; will create the new heap location
    // and then copies the value of obj content to new heap location.

    phone = new char[strlen(p.phone) + 1];
    strcpy_s(phone, strlen(p.phone) + 1, p.phone);
}

Person& Person::operator=(const Person& p) {
    cout << " Copy assignment operator 호출- 메모리 주소 : " << this << endl;
    if (this != &p) {
        delete[]name;
        delete[]phone;

        name = new char[strlen(p.name) + 1]; // NULL문자를 고려하여 +1만큼 할당
        strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사

        phone = new char[strlen(p.phone) + 1];
        strcpy_s(phone, strlen(p.phone) + 1, p.phone);

        age = p.age;
    }
    return *this;
}

Person::Person(Person&& other) noexcept {
    cout << " Move constructor 호출 - 메모리 주소 : " << this << endl;
    name = other.name; // 얕은 복사
    phone = other.phone; // 얕은 복사
    age = other.age;

    // 임시 객체 소멸 시에 메모리를 해제하지 못하게 한다.
    other.name = nullptr; // nullptr 는 C++ 11 에 추가된 키워드로, 기존의 NULL 대체
    other.phone = nullptr; // 이전 객체에서 pointer는 삭제
    other.age = 0;
}

Person& Person::operator=(Person&& other) noexcept {
    cout << " Move assignment operator 호출 - 메모리 주소 : " << this << endl;
    if (this != &other) {
        delete[]name;
        delete[]phone;

        name = other.name; // 얕은 복사
        phone = other.phone; // 얕은 복사
        age = other.age;

        other.name = nullptr; // 이전 객체에서 pointer는 삭제
        other.phone = nullptr;
        other.age = 0;
    }
    return *this;
}

void Person::ShowData() { // 클래스 외부에서 클래스 멤버 함수 정의
    cout << " name : " << name << ", phone : " << phone << ", age : " << age << endl;
}


Person setData() {
    Person tmp;
    tmp.setName("이순신");
    tmp.setPhone("010-9999-8888");
    tmp.setAge(54);
    return tmp;
}


int main() {
    cout << endl; // 출력 구분 목적
    //Person p1("홍길동", "010-1234-5555", 34);
    Person p1;
    p1 = setData(); // 얕은 복사
    p1.ShowData();

    p1.setAge(40);

    cout << endl;
    Person p2(p1); // 복사 생성자 호출
    //Person p2 = std::move(p1); //move 함수를 이용해서 p1를 생성할 때 이동생성자를 호출하게 함
    p2.ShowData();

    cout << endl;
    Person p3;
    p3 = p2; // p3.operator=(p2); // 복사 대입 연산자 호출
    //p3 = std::move(p2); // 이동 대입 연산자 호출
    p3.setAge(35);
    p3.ShowData();

    // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.
    cout << endl;
}
 


완벽한 예제가 못되는 거 같아 좀 아쉽다. 좀 더 가다듬게 되면 수정해 놓을 것이다.

728x90

'C++ > C++ 문법' 카테고리의 다른 글

C++ 포인터 이해  (0) 2019.11.30
C++ 클래스 상속  (0) 2019.11.29
C++ 복사 대입 연산자(copy assignment operator)  (0) 2019.11.28
C++ 얕은 복사, 깊은 복사(Shallow Copy vs. Deep Copy)  (0) 2019.11.27
C++ 복사 생성자  (0) 2019.11.27
블로그 이미지

Link2Me

,
728x90

C++ 복사 대입 연산자는 뭐고 언제 호출이 될까?


대입 연산자는 자신과 같은 타입의 다른 객체를 대입받을 때 사용하는 연산자이다.
객체 자체와 직접적인 연관이 있기 때문에 클래스의 멤버 함수로만 정의할 수 있으며 전역 함수로는 정의할 수 없다.
정적 함수로도 만들 수 없고 반드시 일반 멤버 함수로 만들어야 한다.

대입 연산자는 연산자 오버로딩의 결과로써 실행된다.
따로 선언하지 않을 경우 default로 얕은 복사를 하는 대입 연산자를 컴파일러가 자동으로 만든다.

포인터를 가지는 경우 복사생성자(Copy Constructor)와 대입연산자(Copy Assignment Operator)를 반드시 정의해야 한다.


예제1. (테스트 : Visual Studio 2019 Community)

#include <iostream>
#include <cstring>
using namespace std;

class Person {
    char* name;
    char* phone;
    int age;
public:
    Person();
    // VC++에서 char* _name은 문자열 상수를 직접 입력으로 받지 못함
    Person(const char _name[], const char _phone[], int _age);
    ~Person();
    Person(const Person& p);
    Person& operator=(const Person& p);
    void ShowData(); // 선언
};

Person::Person() {
    cout << " 매개변수 없는 생성자 호출" << endl;
    name = NULL;
    phone = NULL;
    age = 0;
}

Person::Person(const char _name[], const char _phone[], int _age) {
    cout << " 매개변수 3개 생성자 호출" << endl;
    name = new char[strlen(_name) + 1]; // NULL문자를 고려하여 +1만큼 할당
    strcpy_s(name, strlen(_name) + 1, _name);

    phone = new char[strlen(_phone) + 1];
    strcpy_s(phone, strlen(_phone) + 1, _phone);

    age = _age;

    cout << " name 주소 : " << (void*)name << ", phone 주소 : " << (void*)phone << endl;
}

Person::~Person() { // DeAllocate the heap
    cout << " 소멸자 호출" << endl;
    delete[]name;
    delete[]phone;

    cout << " name 주소 해제 : " << (void*)name << ", phone 주소 해제 : " << (void*)phone << endl;
}

Person::Person(const Person& p) : age(p.age) {
    cout << " 복사 생성자 호출" << endl;
    name = new char[strlen(p.name) + 1]; // NULL문자를 고려하여 +1만큼 할당
    strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사
    // The statement name = new char; will create the new heap location
    // and then copies the value of obj content to new heap location.

    phone = new char[strlen(p.phone) + 1];
    strcpy_s(phone, strlen(p.phone) + 1, p.phone);

    cout << " name 주소 : " << (void*)name << ", phone 주소 : " << (void*)phone << endl;
}

Person& Person::operator=(const Person& p) {
    cout << " 복사 대입 연산자 호출" << endl;
    if (this != &p) {
        delete[]name;
        delete[]phone;

        name = new char[strlen(p.name) + 1]; // NULL문자를 고려하여 +1만큼 할당
        strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사

        phone = new char[strlen(p.phone) + 1];
        strcpy_s(phone, strlen(p.phone) + 1, p.phone);

        age = p.age;

        cout << " name 주소 : " << (void*)name << ", phone 주소 : " << (void*)phone << endl;
    }
    return *this;
}

void Person::ShowData() { // 클래스 외부에서 클래스 멤버 함수 정의
    cout << " name : " << name << ", phone : " << phone << ", age : " << age << endl;
}

int main() {
    cout << endl; // 출력 구분 목적
    Person p1("홍길동", "010-1234-5555", 34);
    p1.ShowData();

    cout << endl;
    Person p2(p1); // 복사 연산자 호출
    p2.ShowData();

    cout << endl;
    Person p3;
    p3 = p1; // 대입연산자 호출된다. p3.operator=(p1);
    p3.ShowData();

    // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.
    cout << endl;
}


실행결과


깊은 복사가 2번(복사생성자, 복사 대입 생성자) 일어나는 것이 좋은 것일까?

728x90
블로그 이미지

Link2Me

,
728x90

기존 객체와 같은 값을 가진 객체를 복사할 때 값 형식이냐 참조 형식이냐에 따라 얕은 복사와 깊은 복사의 개념이 나눠진다.


stack 영역
- 프로그램이 자동으로 사용하는 임시 메모리 영역이다.
- 지역 변수, 매개변수, 리턴 값 등 잠시 사용되었다가 사라지는 데이터를 저장하는 영역이다.
- 함수 호출 시 생성되고, 함수가 끝나면 시스템에 반환 된다.
- 스택 사이즈는 각 프로세스마다 할당 되지만 프로세스가 메모리에 로드 될 때 스택 사이즈가 고정되어 있어, 런타임 시에 스택 사이즈를 바꿀 수는 없다.
- 명령 실행시 자동 증가/감소 하기 때문에 보통 메모리의 마지막 번지를 지정 한다.


heap 영역
- 동적으로 메모리를 할당 하고자 할 때 위치하는 메모리 영역으로 동적 데이터 영역이라고 부르며, 메모리 주소 값에 의해서만 참조되고 사용되는 영역이다.
- 이 영역에 데이터를 저장 하기 위해서 C는 malloc(), C++은 new() 함수를 사용한다.

new int(3)로 객체를 생성하면 heap 공간에 메모리를 할당하고 값 3을 저장한다.  포인터 변수 a로 100번지 주소를 가리키게 한다.

new int(5)로 객체를 생성하면 heap 공간에 메모리를 할당하고 값 5를 저장한다.  포인터 변수 b로 200번지 주소를 가리키게 한다.


a = b 를 함으로써 a 는 b의 주소를 가리키게 된다. 즉, b주소가 가진 값 5를 출력한다.

동적으로 할당한 메모리는 포인터가 진입점인데 100번지 메모리 공간은 진입점을 잃어버려 Garbage가 된다.


복사가 발생하여

대상체는 1개인데, 대상체에 접근할 수 있는 포인터가 2개인 경우를 shallow copy(얕은 복사)라고 한다.

대상체가 2개인데, 대상체에 접근할 수 있는 포인터가 각각 1개인 경우를 deep copy(깊은 복사)라고 한다.


Java, C#에서는 Garbage Collector 가 삭제를 해주는데 C++에서는 없다.


#include <iostream>
using namespace std;

int main() {

    int* a = new int(3); // 메모리 공간 할당
    int* b = new int(5);

    cout << "a의 주소(복사전) : " << a << endl;
    cout << "b의 주소(복사전) : " << b << endl;   

 


    a = b; // 얕은 복사(참조 복사) ← b의 주소를 a에 복사 즉 a 는 b의 주소 200을 가리킨다.
    // *a = *b; // 깊은 복사(값 복사)

    *b = 10;


    cout << "a의 주소(복사후) : " << a << endl;
    cout << "b의 주소(복사후) : " << b << endl;

    cout << "a의 값 : " << *a << endl;
    cout << "b의 값 : " << *b << endl;

    delete a; // a가 가리키고 있는 메모리 공간 해제
    delete b;
}


얕은 복사를 했을 경우 아래와 같은 에러가 발생한다.

주소값을 참조하기 때문에 복사를 하였더라도 같은 주소를 참조하게 된다.

소멸자가 호출될 때 같은 메모리를 참조하기 때문에 한번 delete 된 메모리를 다시 delete 하기 때문에 오류 창이 뜬다.


깊은 복사를 할 경우에는 에러가 발생하지 않는다.

깊은 복사는 주소가 가리키는 값 즉, 그 메모리의 값을 복사하는 형태다.

새로운 메모리를 할당하기 때문에 원본과 복사본의 참조 공간이 다르므로 소멸자도 오류가 발생하지 않는다.


얕은 복사와 깊은 복사를 좀 더 이해하기 위해서

구글에서 영문으로 shallow copy vs deep copy c++ example 검색하여

https://owlcation.com/stem/Copy-Constructor-shallow-copy-vs-deep-copy 사이트를 찾아 읽어보고

Visual Studio 2019 Community 에서 테스트하고 적어둔다.


#include <iostream>
using namespace std;

class ShalloC {
private:
    int* x; //Sample 01: Private Data Member
    // This class contains only one integer pointer as private data member.
public:
    //Sample 02: Constructor with single parameter
    ShalloC(int m) {
        cout << "생성자 호출" << endl;
        x = new int;
        *x = m;
        // The constructor will create a memory location in a heap
        // and copy the passed in value m to the heap content.
    }

    //Sample 08: Introduce Copy Constructor and perform Deep Copy
    ShalloC(const ShalloC& obj)    {
        cout << "복사 생성자 호출" << endl;
        x = new int;
        *x = obj.GetX();
        // The statement x = new int; will create the new heap location
        // and then copies the value of obj content to new heap location.
    }

    //Sample 03: Get and Set Functions
    int GetX() const {
        return *x;
    }
    void SetX(int m) {
        cout << "SetX " << endl;
        *x = m;
    }

    //Sample 04: Print Function
    void PrintX() {
        cout << "Int X = " << *x << endl;
    }

    //Sample 05: DeAllocate the heap
    ~ShalloC() {
        cout << "소멸자 호출" << endl;
        delete x; // new 로 할당한 메모리 해제
    }
};

int main()
{
    //Sample 06: Create Object 1 and copy that to Object 2.
    //           Print the data member for both Object 1 & 2.
    ShalloC ob1(10);
    ob1.PrintX();
    ShalloC ob2 = ob1;
    ob2.PrintX();

    //Sample 07: Change the Data member value of Object 1
    //           And print both Object 1 and Object 2
    cout << endl;
    ob1.SetX(12); // ob1 객체 값 변경
    ob1.PrintX(); // ob1 객체의 값 출력
    ob2.PrintX(); // ob2 객체의 값 출력
}

In the Program main we created two Objects ob1 and ob2.

The object ob2 is created using the copy constructor. How? And where is the "copy constructor".?

If you look at the statement ShalloC ob2 = ob1 ; you clearly know that the ob2 is not yet created and in the mean time ob1 is already created.

Hence, a copy constructor is invoked. Even though the copy constructor not implemented, the compiler will provide default copy constructor. Once both the objects are created we print the values in ob1 and ob2.


실행결과

ob1 객체의 값을 변경하고 나서 ob1 의 값과 ob2의 값이 서로 달라진 걸 확인할 수 있다.

이렇게 값을 변경해 봄으로써 깊은 복사(deep copy)가 된 것인지 얕은 복사(shallow copy)가 된 것인지 명확히 알 수 있다.


복사 생성자 코드 부분을 주석처리하고 테스트 해보면 이해가 명확해진다.

default 복사 생성자가 자동으로 추가되어 얕은 복사를 수행했고, 소멸자는 같은 메모리 주소를 2번 삭제하려고 하다보니 에러 메시지를 출력한다.

728x90
블로그 이미지

Link2Me

,

C++ 복사 생성자

C++/C++ 문법 2019. 11. 27. 08:58
728x90

C++ 클래스에는 멤버 변수와 멤버 함수가 있다.

클래스 객체를 생성하면 복사 생성자를 별도로 선언하지 않아도 default 로 복사 생성자(Copy constructor)가 자동으로 생성된다.

만약 생성자 내에서 동적 할당을 한다면 반드시 구현해 주어야 할 것은 소멸자, 복사 생성자이다.

'얕은 복사'는 주소값만 복사하는 것이며, '깊은 복사'는 따로 메모리 할당까지 해주는 것이다.

default copy constructor 는 얕은 복사가 발생하여 소멸자에서 에러가 발생한다.

따라서 개발자가 직접 깊은 복사가 되도록 copy constructor를 구현해 주어야 한다.



C++ 복사 생성자 동영상 강좌를 따라하는데 에러가 발생하고 안된다.

동영상 강좌가 Visual Studio 2017 이전 버전인가 보다. 해결방법은 아래 설명되어 있다.

Person 클래스의 ① 복사 생성자를 구현한 상태로 실행해보고 ② 다시 주석처리하고 실행해보면, 결과가 다르게 나온다는 걸 확인할 수 있다.

예제1.

#include <iostream>
using namespace std;

class Point {
private:
    int x, y;
public:
    Point(int _x=0, int _y=0) : x(_x), y(_y){}
    Point(const Point& p) { // 복사 생성자
        x = p.x;
        y = p.y;
    }
    // 복사 생성자는 명시적으로 생성해주지 않으면 default 복사생성자가 자동으로 생성된다.
    void ShowPosition();
};

void Point::ShowPosition() {
    cout << "(" << x << ", " << y << ")" << endl;
}

class Person {
    char* name;
    char* phone;
    int age;
public:
    Person(char* _name, char* _phone, int _age);
    Person(const Person &p) {
        name = new char[strlen(p.name) + 1]; // 문자열 할당. + 1 은 NULL 문자 고려
        strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사

        phone = new char[strlen(p.phone) + 1];
        strcpy_s(phone, strlen(p.phone) + 1, p.phone);

        age = p.age;
    }
    ~Person();
    void ShowData();
};

Person::Person(char* _name, char* _phone, int _age) {
    name = new char[strlen(_name) + 1];
    strcpy_s(name, strlen(_name) + 1,_name);

    phone = new char[strlen(_phone) + 1];
    strcpy_s(phone, strlen(_phone) + 1,_phone);

    age = _age;
}

Person::~Person() { // DeAllocate the heap
    delete []name;
    delete []phone;
}

void Person::ShowData() {
    cout << "name : " << name << endl;
    cout << "phone : " << phone << endl;
    cout << "age : " << age << endl;
}

int main() {
    Point p1(10, 20); // 객체가 생성될 때 메모리가 할당된다.
    Point p2(p1); // 복사 생성자 호출 발생

    p1.ShowPosition();
    cout << "p1의 주소 값 : " << &p1 << endl;
    p2.ShowPosition();
    cout << "p2의 주소 값 : " << &p2 << endl;

    Person p3("홍길동", "010-1234-5555", 30);
    Person p4(p3);

    p4.ShowData();
    // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.
}

에러메시지 내용은

'Person::Person(const Person &)': 인수 1을(를) 'const char [4]'에서 'char *'(으)로 변환할 수 없습니다.


Visual Studio 2017 이상부터는 표준 문법을 엄격히 사용하도록 되어 있다.

Visual Studio 는 문자열 저장방식을 char * 가 아닌 string 사용을 권장하고 있다.

에러 메시지를 해결하는 방법으로 해당 파일에서 마우스 우클릭 누르면 속성이 나온다.

C/C++ → 언어 → 준수모드 : 아니오 로 변경하면 에러 메시지 없이 결과가 출력된다.

이것이 올바른 해결책은 아니다.

에러 메시지에 해당되는 https://docs.microsoft.com/ko-kr/cpp/error-messages/compiler-errors-2/compiler-error-c2664?f1url=https%3A%2F%2Fmsdn.microsoft.com%2Fquery%2Fdev16.query%3FappId%3DDev16IDEF1%26l%3DKO-KR%26k%3Dk(C2664)%26rd%3Dtrue&view=vs-2019 를 차분히 읽어보니 해결책은 따로 있다.


const char* _name, const char* _phone 로 const 를 붙이면 해결된다.


copy_constructor.cpp



다른 방법 : char* _name 을 const char _name[] 으로 변경해주고, 선언과 정의를 분리했다.

예제2에서 결과 출력이 잘 되는 걸 확인할 수 있다.

예제2.

#include <iostream>
#include <cstring>
using namespace std;

class Person {
    char* name;
    char* phone;
    int age;
public:
    Person(const char _name[], const char _phone[], int _age);
    ~Person();
    Person(const Person& p);
    void ShowData(); // 선언
};

Person::Person(const char _name[], const char _phone[], int _age) {
    cout << "생성자 호출" << endl;
    name = new char[strlen(_name) + 1];
    strcpy_s(name, strlen(_name) + 1, _name);

    phone = new char[strlen(_phone) + 1];
    strcpy_s(phone, strlen(_phone) + 1, _phone);

    age = _age;


    cout << "name 주소 : " << (void*)name << endl;
    cout << "phone 주소 : " << (void*)phone << endl;
}

Person::~Person() {
    cout << "소멸자 호출" << endl;
    delete[]name; // 동적할당한 메모리 해제
    delete[]phone;
// 동적할당한 메모리 해제


    cout << "name 주소 해제 : " << (void*)name << endl;
    cout << "phone 주소 해제 : " << (void*)phone << endl;
}

Person::Person(const Person& p) : age(p.age) {
    cout << "복사 생성자 호출" << endl;
    name = new char[strlen(p.name) + 1]; // 문자열 할당. +1은 NULL문자(\0) 고려
    strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사

    phone = new char[strlen(p.phone) + 1];
    strcpy_s(phone, strlen(p.phone) + 1, p.phone);


    cout << "name 주소 : " << (void*)name << endl;
    cout << "phone 주소 : " << (void*)phone << endl;
}

void Person::ShowData() { // 클래스 외부에서 클래스 멤버 함수 정의
    cout << "name : " << name << endl;
    cout << "phone : " << phone << endl;
    cout << "age : " << age << endl;
}

int main() {
    Person p1("홍길동", "010-1234-5555", 30);
    p1.ShowData();

    Person p2(p1); // 복사 생성자 호출
    p2.ShowData();
    // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.
}

생성자 호출이 되고, 복사 생성자가 호출된다.

소멸자가 두번 호출되는 걸 확인할 수 있고 소멸된 순서를 확인할 수 있다.



이번에는 char *name 대신에 string 으로 변경하여 #include <string> 를 추가하고 코드를 수정해서 테스트했다.

결과는 잘 출력된다.


예제3.

#include <iostream>
#include <string>
using namespace std;

class Point {
private:
    int x, y;
public:
    Point(int _x=0, int _y=0) : x(_x), y(_y){}
    Point(const Point& p) { // 복사 생성자
        x = p.x;
        y = p.y;
    }
    // 복사 생성자는 명시적으로 생성해주지 않으면 default 복사생성자가 자동으로 생성된다.
    void ShowPosition();
};

void Point::ShowPosition() {
    cout << "(" << x << ", " << y << ")" << endl;
}

class Person {
    string name;
    string phone;
    int age;
public:
    Person(string _name, string _phone, int _age);
    ~Person();
    Person(const Person& p); // Copy Constructor

    //Setter method
    void setName(string name) {    this->name = name; }
    void setPhone(string phone) { this->phone = phone; }
    void setAge(int age) { this->age = age; }

    // Getter method
    string getName() { return name; }
    string getPhone() { return phone; }
    int getAge() { return age; }

    void ShowData();
};

Person::Person(string _name, string _phone, int _age) {
    cout << "Person 생성자 호출- 메모리 주소 : " << this << endl;
    name = _name;
    phone = _phone;
    age = _age;
}

Person::~Person() {
    cout << "Person 소멸자 호출- 메모리 주소 : " << this << endl;
}

Person::Person(const Person& p) {
    cout << "Person 복사 생성자 호출 - 메모리 주소 : " << this << endl;
    name = p.name;
    phone = p.phone;
    age = p.age;
}

void Person::ShowData() {
    cout << "name : " << name << endl;
    cout << "phone : " << phone << endl;
    cout << "age : " << age << endl;
}

int main() {
    Point p1(10, 20); // 객체가 생성될 때 메모리가 할당된다.
    Point p2(p1);

    p1.ShowPosition();
    cout << "p1의 주소 값 : " << &p1 << endl;
    p2.ShowPosition();
    cout << "p2의 주소 값 : " << &p2 << endl << endl;

    Person p3("홍길동", "010-1234-5555", 30);
    Person p4(p3);

    cout << endl;
    p4.ShowData();
    // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.
}


Person 클래스의 생성자 호출, 복사 생성자 호출, 소멸자 호출이 2번 되는 걸 확인할 수 있다.

728x90

'C++ > C++ 문법' 카테고리의 다른 글

C++ 복사 대입 연산자(copy assignment operator)  (0) 2019.11.28
C++ 얕은 복사, 깊은 복사(Shallow Copy vs. Deep Copy)  (0) 2019.11.27
C++ 연산자 오버로딩  (0) 2019.11.26
C++ static  (0) 2019.11.25
C++ 클래스 이해  (0) 2019.11.23
블로그 이미지

Link2Me

,
728x90

C++ 연산자 오버로딩 이라는 개념은 생소하다. 왜냐하면 Java, C# 에서는 들어본 적이 없다.


C++ 자료형은 int, float, double, char 등이 존재하고 사칙 연산이 가능하다.

사용자 정의 자료형에는 구조체(struct) 와 클래스가 있다.

C언어에서는 구조체는 사칙 연산이 불가능하다. C언어에서는 클래스는 없다.

C++ 에서 클래스에서 생성한 객체끼리 사칙 연산이 가능하도록 만들어 놓은 것이 연산자 오버로딩이다.

연산자 오버로딩은 operator 라는 함수명을 사용하며 뒤에 사칙연산자( +, -, *, / )가 붙는다.


#include <iostream>
using namespace std;

class Point {
private:
    int x, y;
public:
    Point(int _x=0, int _y=0) : x(_x), y(_y){}
    void ShowPosition();
    Point operator+(const Point& p);
    bool operator==(const Point& p);
    Point operator-(const Point& p);
    friend bool operator!=(const Point&, const Point&); // friend 선언
    // Point 클래스 입장에서 다른 클래스 또는 전역 함수를 자신의 친구로 선언하는 것
    // 전역 함수 operator 에게 private 및 protected 멤버에 접근할 수 있는 권한을 부여
    // friend 는 정보 은닉에 위배되는 개념
    // 연산자 오버로딩에서 유용하게 사용
    friend Point& operator--(Point& p);
};


void Point::ShowPosition() {
    cout << "(" << x << ", " << y << ")" << endl;
}

// 멤버 함수 기반의 연산자 오버로딩
Point Point::operator+(const Point& p) { // 매개변수 상수화
    // const 란 정의된 변수를 상수화하여, 그 값을 변경하지 못하게 하는 것이다.
    // 값 변경의 위험이 느껴질 때, 매개변수 앞에 const 를 붙여 상수화 시키는 것이다.
    return Point(x + p.x, y + p.y);
}

bool Point::operator==(const Point& p) {
    if (x == p.x && y == p.y) return true;
    else return false;
}

Point Point::operator-(const Point& p) {
    return Point(x - p.x, y - p.y);
}


bool operator!=(const Point& p1, const Point& p2){ // 전역 함수
    // Point 클래스의 private 영역 접근 가능
    // friend 로 선언된 대상에게는 완벽하게 public 으로 작용한다.
    return (p1 != p2);
}


Point& operator--(Point& p) { //전역함수 
    p.x--;
    p.y--;
    return p;
}


int main() {
    Point p1(1, 6); // 객체가 생성될 때 메모리가 할당된다.
    Point p2(2, 4);
    Point p3 = p1.operator+(p2);
    //Point p3 = p1 + p2;
    Point p4 = p3 - p2; // Point p4 = p3 - p2 + p1; 로 해봐도 결과가 나오는 걸 확인 할 수 있다.

    p1.ShowPosition();
    p2.ShowPosition();
    p3.ShowPosition();
    p4.ShowPosition();


    --p2;
    p2.ShowPosition();
} 


Point p3 = p1 + p2; 에서 p1, p2는 객체이므로 기본적으로 + 연산이 불가능하다.

앞에 operator 라는 키워드를 붙여서 p1과 p2를 이용해서 operator +라는 함수를 호출해주게 되는 것이다.

p1.operator+(p2); 로 operator 함수가 호출되어 수행하도록 약속되어 있다.



테스트에 사용한 파일

c++operator.cpp


728x90

'C++ > C++ 문법' 카테고리의 다른 글

C++ 얕은 복사, 깊은 복사(Shallow Copy vs. Deep Copy)  (0) 2019.11.27
C++ 복사 생성자  (0) 2019.11.27
C++ static  (0) 2019.11.25
C++ 클래스 이해  (0) 2019.11.23
C++ namespace(네임스페이스)  (0) 2019.11.22
블로그 이미지

Link2Me

,

C++ static

C++/C++ 문법 2019. 11. 25. 05:53
728x90

static 은 변수에 선언하거나, 함수에 선언할 수 있다.

- static으로 지정된 멤버는 생명주기가 전역 변수로 바뀐다.

- 모든 객체에서 공유하여 사용한다.

- 객체를 선언하지 않고 사용한다.

- 객체 소유가 아니므로 this 포인터를 사용할 수 없다.


예제1.

#include <iostream>
using namespace std;

void Counter() {
    int count = 0;
    cout << " " << count++;
}

void Counter2() {
    static int count = 0;
    cout << " " << count++;
}

class ABC {
public:
    int a;
    static int counter; // 여기에서 0 할당하면 에러가 발생한다.
    void getABC() {
        a = counter;
        cout << "a = " << a << endl;
    }
};

int ABC::counter = 0; // 클래스 외부에서 전역 변수처럼 초기화시켜야 한다.

int main() {
    for (int i = 0; i < 5; i++) {
        Counter(); // 함수가 호출될 때마다 값이 초기화 됨
    }
    cout << endl;

    for (int i = 0; i < 5; i++) {
        Counter2(); // main()함수가 종료되기 전까지 값을 유지
    }
    cout << endl;

    ABC abc = ABC(); // 객체 생성
    for (int i = 0; i < 5; i++) {
        abc.getABC(); // 함수 호출
        ABC::counter++; // couter 값 증가
    }
}
 

static 변수를 적용한 것과 적용하지 않은 경우 값의 결과를 출력해서 확인할 수 있도록 해봤다.


728x90

'C++ > C++ 문법' 카테고리의 다른 글

C++ 복사 생성자  (0) 2019.11.27
C++ 연산자 오버로딩  (0) 2019.11.26
C++ 클래스 이해  (0) 2019.11.23
C++ namespace(네임스페이스)  (0) 2019.11.22
C++ 오버로딩  (0) 2019.11.22
블로그 이미지

Link2Me

,

C++ 클래스 이해

C++/C++ 문법 2019. 11. 23. 12:00
728x90

이제부터 본격적인 C++ 학습을 하게 되는 것인가 싶다.


구조체는 하나의 구조로 묶을 수 있는 데이터(변수들)의 집합이었다면, Class는 변수 뿐만 아니라 함수까지 포함시킬 수 있다고 해서 구조체의 확장된 버전이라고 설명한다.

struct 에서는 기본이 public 접근 권한인데,

Class에서는 접근을 제한할 수 있는 접근 제어 지시자 public, private, protected 가 추가되었다.

ㅇ 접근 제어 지시자는 객체 지향 프로그래밍의 특징 중 하나인 정보 은닉을 위한 키워드이다.

ㅇ public 영역은 모든 객체에서 접근할 수 있지만,

ㅇ private 영역은 해당 객체 내의 멤버 변수나 멤버 함수만이 접근할 수 있다.

ㅇ 클래스의 기본 접근 제어 권한은 private로 설정되어 있어, private 접근 제어 지시자는 생략할 수 있다.

ㅇ protected 접근 제어 지시자인 경우

    - protected 멤버는 파생 클래스에 대해서는 public 멤버처럼 취급되며,

    - 외부에서는 private 멤버처럼 취급된다.

생성자, 소멸자, 상속의 개념이 추가되었다.

ㅇ 생성자 : 객체가 생성될 때, 자동으로 호출되는 함수로써 객체를 초기화하는 함수이다.

ㅇ 소멸자 : 생성된 객체가 메모리 해제가 될 때 자동으로 호출되는 함수다.

ㅇ 생성자/소멸자는 public 속성을 가진다.

ㅇ 생성자는 클래스명과 같은 이름을 가지며, 매개변수를 가질 수 있다.

ㅇ 소멸자는 생성자와 동일하게 클래스명과 같은 이름의 함수이며, 생성자와 구분하기 위해 ~를 소멸자 앞에 붙인다.


클래스와 멤버
클래스 안에는 변수와 함수를 선언 할수 있다.
클래스 내의 변수를 멤버변수라고 한다.
클래스 내의 함수는 메서드 또는 멤버함수라고 한다.


클래스 선언
클래스 선언에는 class라는 키워드가 쓰이고, 중괄호를 사용하여 시작과 끝을 표시한다.
그리고 닫는 중괄호에는 꼭 세미콜론으로 끝내야 한다.
class Person {
    string name;   //멤버변수
    int age;       //멤버변수
    double height; //멤버변수

public: // 접근제어 지시자
    void SetPerson(string _name, int _age, double _height); // 멤버함수
    void showPerson(); // 멤버함수
};



예제 1

#include <iostream>
#include <string>
using namespace std;

// Person 클래스 선언
class Person {
//private: // 접근 제어 지시자 (private는 클래스 외부에서 접근 불가, private 접근 제어 지시자는 생략 가능)
    string name;
    int age;
    double height;

public:
    void SetPerson(string _name, int _age, double _height);
    void showPerson();

    Person() { // default 생성자
        cout << "=== 생성자 호출 : 인자값 없음 ===" << endl;
        age = 30;
        name = "홍길동";
        height = 170;
    }


   // 3개의 매개변수가 있는 생성자
    Person(string _name, int _age, double _height) { // 생성자 오버로딩(overloading)
        cout << "=== 생성자 호출 : 인자값 3개 ===" << endl;
        this->name = _name;
        this->age = _age;
        this->height = _height;
    }

    ~Person(){ // 소멸자
        cout << "=== 소멸자 호출 : 인자값 없음 ===" << endl;
    }

}; //세미콜론 필수

void Person::SetPerson(string _name, int _age, double _height) {
    this->name = _name;
    this->age = _age;
    this->height = _height;
}

void Person::showPerson() {
    cout << "성명 : " << name << endl;
    cout << "나이 : " << age << endl;
    cout << "키 : " << height << endl;
    cout << "나의 주소 : " << this << endl;
}

int main() {
    cout << endl << "=== main 함수 시작 ===" << endl;

    Person p = Person(); // 기본 생성자 사용 (1. 메모리 할당, 2. 메모리할당이 성공한 경우 객체의 생성자의 호출)
    p.showPerson();
    cout << "p의 주소 : " << &p << endl << endl;
   
    //Person p2("이순신", 41, 188); // 오버로딩 된 생성자 사용 (생성자 생성시 객체 전달)
    Person p2 = Person("이순신", 41, 188); // Person p2("이순신", 41, 188); 와 동일
    p2.showPerson();
    cout << "p2의 주소 : " << &p2 << endl << endl;
   
    p.SetPerson("김구", 48, 178); // 객체의 값을 변경
    p.showPerson();
    cout << "p의 주소 : " << &p << endl << endl;

    cout << "=== main 함수 종료 ===" << endl;
} 


this 는 객체 자신을 가리키는 포인터로 현 객체의 메모리 주소를 나타낸다.

따로 선언할 필요없이 객체가 생성되면 생성된 객체는 this 포인터를 가진다.


변수 p 의 주소값과 변수 p2의 주소값과 this 주소값을 서로 비교해볼 수 있도록 했으며, 실행순서를 알 수 있도록 출력문(cout)을 추가했다.



생성자 표기법

    Person() { // default 생성자
        cout << "=== 생성자 호출 : 인자값 없음 ===" << endl;
        age = 30;
        name = "홍길동";
        height = 170;
    }


   // 3개의 매개변수가 있는 생성자
    Person(string _name, int _age, double _height) { // 생성자 오버로딩(overloading)
        cout << "=== 생성자 호출 : 인자값 3개 ===" << endl;
        this->name = _name;
        this->age = _age;
        this->height = _height;
    }

    // default 생성자
    Person() : name("홍길동"), age(30), height(170) {
        cout << "=== 생성자 호출 : 인자값 없음 ===" << endl;
    }

    // 생성자 오버로딩(overloading)
    Person(string _name, int _age, double _height) : name(_name),age(_age), height(_height) {
        cout << "=== 생성자 호출 : 인자값 3개 ===" << endl;
    }

cout 출력줄을 없애면 생성자를 한줄로 표시할 수 있다.



변수 p 는 실제 존재하는 객체를 가리키는 참조자일 뿐이다.


객체지향에 대한 개념을 제대로 이해하고 싶다면, C#/유니티 기준으로 설명하는 클래스와 오브젝트 한방에 이해하기 동영상  https://www.youtube.com/watch?v=GwL5uUP7F1Y 를 강추한다.


728x90

'C++ > C++ 문법' 카테고리의 다른 글

C++ 연산자 오버로딩  (0) 2019.11.26
C++ static  (0) 2019.11.25
C++ namespace(네임스페이스)  (0) 2019.11.22
C++ 오버로딩  (0) 2019.11.22
C++ 참조 변수(reference)  (0) 2019.11.22
블로그 이미지

Link2Me

,
728x90

네임스페이스(namespace)

네임스페이스(namespace)는 모든 식별자(변수, 함수, 형식 등의 이름)가 고유하도록 보장하는 코드 영역을 정의한다.
스코프 분석 연산자(::)는 검색하려는 네임 스페이스를 구체적으로 선택할 수 있도록 해 주므로 매우 유용하다.
컴파일러가 특정 네임스페이스에서 식별자를 찾게 하는 첫 번째 방법은 스코프 분석 연산자(::)를 사용하는 것이다.


namespace 이름
{
    선언내용;
}


#include <iostream>
using namespace std;

namespace AAA {
    int score;
    void set() {
        score = 30;
    }
}


int main(void) {
    AAA::set();

    cout << "Hello World! C++" << endl;
    cout << "AAA score : " << AAA::score << endl;

    return 0;
}


728x90

'C++ > C++ 문법' 카테고리의 다른 글

C++ static  (0) 2019.11.25
C++ 클래스 이해  (0) 2019.11.23
C++ 오버로딩  (0) 2019.11.22
C++ 참조 변수(reference)  (0) 2019.11.22
C++ 입력(cin) 과 출력(cout)  (0) 2019.11.21
블로그 이미지

Link2Me

,

C++ 오버로딩

C++/C++ 문법 2019. 11. 22. 17:44
728x90

구글링해보니 C언어로 순수 오버로딩하는 건 어렵다고 나온다. 그러므로 C++ 에서 사용된다고 이해하자.


C++ Overloading 이란 함수 이름이 같은 함수를 여러개 만들 때, 매개 변수의 정보를 다르게 하여 공통 기능의 함수를 하나로 묶어주는게 오버로딩이다.

오버로딩의 조건은

ㅇ 매개변수의 개수가 다르게 한 경우

    선언부에서 매개변수의 초기값을 미리 설정할 수 있다.

    단, 초기화는 매개변수의 오른쪽부터 순서대로 해야 유효하다.


ㅇ 매게변수의 타입을 다르게 한 경우


※ 반환형의 차이는 함수 오버로딩의 조건에 포함되지 않는다. 모호성이 발생하기 때문이다.

   void ABC(int a, int b){ ... }

   int ABC(int a, int b){ ... }


#include <iostream>

void swap(int& a, int& b) {
    int tmp = a;
    a = b;
    b = tmp;
}

void swap(double& a, double& b) {
    double tmp = a;
    a = b;
    b = tmp;
}

void swap(int* (&a), int* (&b)) {
    int *tmp = a; // 포인터에 저장된 주소값을 저장
    a = b;
    b = tmp;
}

int main() {

    int a = 10, b = 20;
    double da = 11.11, db = 22.33;
    int* pa = &a, * pb = &b;

    // 함수의 이름은 같아도 매개변수의 타입이 달라서 오버로딩 조건 성립
    swap(a, b);
    swap(da, db);
    swap(pa, pb);

    std::cout << "a : " << a << " , b : " << b << std::endl;
    std::cout << "da : " << da << " , db : " << db << std::endl;
    std::cout << "pa : " << *pa << " , pb : " << *pb << std::endl;
}



구글 이미지를 검색하면 이런 이미지를 찾을 수 있다.

함수 오버로딩과 함수 오버라이딩의 개념 차이를 그림으로 이해할 수 있다.

함수 오버라이딩은 객체간 상속 관계에서 사용되는 문법으로 함수의 이름, 매개변수, 반환 타입이 모두 일치해야 한다.

부모 클래스의 함수를 자식 클래스에서 재정의하는 것이다.


728x90

'C++ > C++ 문법' 카테고리의 다른 글

C++ 클래스 이해  (0) 2019.11.23
C++ namespace(네임스페이스)  (0) 2019.11.22
C++ 참조 변수(reference)  (0) 2019.11.22
C++ 입력(cin) 과 출력(cout)  (0) 2019.11.21
C언어 중첩 구조체(Nested structure)  (0) 2019.11.20
블로그 이미지

Link2Me

,
728x90

참조 변수 (Reference variable)
C언어에서 일반 변수와 포인터 변수에 대해서 학습했는데 변수란 메모리 공간에 할당된 이름이다. 그 이름을 통해 해당 메모리 공간에 접근한다. C언어에서는 하나의 메모리 공간에 하나의 이름만 부여할 수 있다.

하지만 C++ 에서는 reference 를 선언하게 되면, "이름이 존재하는 공간에 하나의 이름을 더 부여" 할 수 있다.


reference 를 참조 변수 또는 참조자 라고 표현한다. 책에 따라 우리말 명칭이 약간씩 다름을 알자.


reference는 C++에서 지원하는 변수 타입으로 다른 객체 또는 값의 별칭으로 사용된다.

int a(10); // C언어의 int a = 10; 과 같다.

int &ref = a; // 선언 방법은 자료형 &참조변수이름 = 원래변수;

non-const 값 참조형은 자료형 뒤에 앰퍼샌드(&)를 사용하여 선언한다.
Lvalue는 메모리 주소를 가진 객체이고 Rvalue는 메모리 주소가 없고, 표현식 범위에만 있는 임시 값이다.

참조변수에 대입하는 값은 Lvalue 만 가능하다.

ㅇ 주소 연산자 & 는 Lvalue를 요구하기 때문에 표현식이 Rvalue라면 컴파일 오류가 발생한다.

C++에서 모든 표현식은 Lvalue 또는 Rvalue 이다.
Lvalue는 객체를 참조하는 표현식이다. 메모리 위치를 가지고 있다. const 타입을 포함한 모든 변수는 Lvalue 이다.
Rvalue는 표현식이 종료된 이후에는 더이상 존재하지 않는 임시적인 값이다. 상수 또는 임시 객체는 Rvalue 이다.

ㅇ 단항 & (address-of) 연산자는 그것의 피연산자로 Lvalue 를 필요로 한다.

    int n, *p;
    p = &n;     // Fine. &n는 n이  Lvalue 인 경우에만 유효한 표현식이다. 
    &n = p;     // Error: &n is an Rvalue

ㅇ 포인터 p 는 항상 객체를 가리킨다. 그러므로 *p 는 Lvalue 이다.

    int a[N];
    int *p = a;
    *p = 3;      // Fine.

    *(p + 1) = 4; // Fine. (p+1) 는 Rvalue

ㅇ 리턴타입이 오직 참조인 경우에만 함수 호출은 Lvalue 이다.
    int& GetBig(int& a, int& b) {    // 함수 호출을 Lvalue 로 만들기 위해 참조를 반환
        return ( a > b ? a : b );
    }


출처 : http://jeremyko.blogspot.com/2012/08/lvalue-rvalue.html 에서 필요 부분만 발췌

원출처 : https://www.codeproject.com/Articles/313469/The-Notion-of-Lvalues-and-Rvalues


참조자는 별도의 메모리를 할당하지 않고 변수 선언문에서 초기화에 사용한 변수의 메모리를 참조하여 사용한다. 이러한 이유로 참조자는 선언과 동시에 무조건 변수를 참조하도록 해야 한다.

null 값을 저장할 수 있는 포인터와 다르게, null 참조 같은 것은 없다.


배열의 요소는 변수로 간주되어 참조자 선언이 가능하다.

int arr[3] = { 1, 2, 3};

int &ref1 = arr[0];

int &ref2 = arr[1];

int &ref3 = arr[2];


포인터 변수도 변수이기 때문에 참조자의 선언이 가능하다.

int num = 10;

int *ptr = &num;

int &ref = num;

int *(&pref) = ptr;


예제1.

#include <iostream>

using namespace std;

void swap(int& a, int& b) { // 함수의 매개변수를 참조변수(reference)로 선언하는 방식
    // 참조변수(reference)로 매개변수를 받았다

    // 포인터 연산을 할 필요가 없으므로 보다 안정적이다.

    // 함수의 호출 형태를 구분하기 어렵다.
    int tmp = a;
    a = b;
    b = tmp;
}

int main() {
    int a(10);
    int b(5);

    swap(a, b);

    std:cout << "두 수의 값을 바꾸는 함수" << std::endl;
    std::cout << "a = " << a << std::endl;
    std::cout << "b = " << b << std::endl;

    int arr[10] = {1,2,3,4,5,6,7,8,9,10};

    for (int n : arr) {
        cout << n << ' ';
        n++;
    }
    cout << endl;

    // reference 변수

    // 자료형 &참조변수이름 = 원래변수;
    int &ref = a; // 변수를 입력받는다. 상수는 안된다.
    ref = 20;

    cout << "ref : " << ref << endl;
    cout << "a : " << a << endl;

    ref++;
    // reference 연산은 reference가 참조하는 변수의 이름으로 하는 연산과 효과가 똑같다.
    cout << "ref : " << ref << endl;
    cout << "a : " << a << endl;

    string food = "Pizza"; // food 변수
    string &meal = food; // food 의 reference

    cout << endl;
    cout << food << endl;  // Pizza 출력
    cout << meal << endl;  // Pizza 출력

    string* ptr = &food; // 포인터 선언 (주소값을 입력 받는다)

    cout << endl;

    // food의 메모리 주소와 reference 메모리 주소가 동일하고, 할당한 포인터 주소가 동일함을 확인할 수 있다.

    // 하지만 포인터 자체 주소는 다르다는 것도 알 수 있다.

    cout << &food << endl;  // food 값의 메모리 주소 출력
    cout << &meal << endl;  // refernece 값의 메모리 주소 출력
    cout << ptr << endl;    // 포인터의 메모리 주소 출력
    cout << &ptr << endl;    // 포인터 자체 메모리 주소 출력
    cout << *ptr << endl; // 역참조 연산자(*)로 포인터가 가리키는 값 출력

}


예제2

#include <iostream>
using namespace std;

int& increment(int& val) { // 참조 변수로 원본을 받음
    val++;
    return val;
}

int main() {

    int n = 10;
    int& ref = increment(n); // 파라미터로 reference 원본 넘겨줌

    cout << "n : " << n << endl;
    cout << "ref: " << ref << endl;

    cout << "n 주소값 : " << &n << endl;
    cout << "ref 주소값 : " << &ref << endl;
}

원본 변수 n 은 참조 변수 ref가 사용되는 동안 유지되기 때문에 결과는 11을 반환한다.

반환이 int& 인데 int 로 하면 어떻게 되는지 변경해보라.

같은 주소를 사용하는지 다른 주소를 사용하는지 확인이 될 것이다.


그런데 return 받는 변수가 지역변수라면 어떤 결과가 발생할까?

https://www.youtube.com/watch?v=Bko1OoExWhg&t=319s 동영상 강좌를 참조해서 들어보자.


#include <iostream>
using namespace std;

int &CalSum(int s, int e) {
    int sum = 0;
    for (; s <= e; ++s) {
        sum += s;
        cout << " sum : " << sum << endl;
    }

    cout << " sum address : " << &sum << endl;
    return sum;
}

int main() {

    int &ref1 = CalSum(1, 20);
    int &ref2 = CalSum(1, 10);

    // cout << endl; // 이 한줄을 넣으면 결과가 엉뚱하게 나온다.
    cout << " ref1: " << ref1 << ", ref2: " << ref2 << endl;

    return 0;
}

실행 결과

C++ 에서 함수 처리 return 메커니즘은 return 값을 임시 저장 공간에 복사한 후 그것을 return하는 방식이다.
하지만 return by reference 형식을 사용할 경우 임시 저장공간을 사용하지 않고 호출 함수가 return값에 직접 접근을 하게 된다.


main()이 실행되면서 int ref1 과 int ref2 에 메모리를 각각 4 바이트씩 할당한다.

그 다음에 CalSum() 함수가 실행되면 함수 내 지역변수 int sum 에 메모리를 할당하기 위해 추가적으로 4바이트를 할당한다.


CalSum(1, 20) 의 결과 리턴으로 210을 반환한다. 그런 다음에 CalSum()함수가 종료되고, 지역변수 sum 이 차지하는 메모리 공간은 사라진다.

CalSum(1, 10)을 실행하면서 다시 지역변수 sum 이 차지하는 메모리 공간을 할당한다. 할당된 공간의 메모리 주소는 동일한 주소 공간을 차지한다는 걸 위 실행 결과에서 확인할 수 있다.

여기서 ref1 도 동일한 메모리 주소를 참조하고 있기 때문에 결과는 210이 아닌 55를 반환한다.


그 이유는 함수 내의 로컬 변수의 reference를 참조할 경우, local 변수는 함수 종료시 파괴되지만 ref1의 경우 reference 자체를 계속해서 참조하고 있기 때문이다.


따라서 지역변수를 reference 로 return 하는 경우 원하지 않는 결과를 초래할 수 있으므로 코딩시 주의해야 한다.

728x90

'C++ > C++ 문법' 카테고리의 다른 글

C++ namespace(네임스페이스)  (0) 2019.11.22
C++ 오버로딩  (0) 2019.11.22
C++ 입력(cin) 과 출력(cout)  (0) 2019.11.21
C언어 중첩 구조체(Nested structure)  (0) 2019.11.20
C언어 구조체(Struct) 이해 및 C++ 구조체 예제  (0) 2019.11.19
블로그 이미지

Link2Me

,
728x90

C언어에서는 입출력을 위한 printf 함수와 scanf 함수를 사용하기 위해 표준 입출력 헤더파일 #include <stdio.h> 을 선언하는데, C++ 에서 표준 입출력 헤더파일 #include <iostream> 을 선언한다.


C++ 헤더 파일에는 .h 가 붙어있지 않다.

std::cout 은 출력을 담당하는 코드이고

std::cin 은 입력을 담당하는 코드이다.

using namespace std; 를 해주면 std::cout 대신에 cout 을 사용할 수 있다.

C언어에서는 %d, %s, %p, %c 처럼 자료형에 따라 서식 지정이 필요한데 C++ 에서는 필요가 없다.

std::endl 은 개행을 하는 코드로 C언어의 \n 과 같다.


#include <iostream> // C++ 표준 입출력 헤더파일
#include <string>

using namespace std;

int main() {
    int a, b;
   
    // shift(시프트) 연산자로 출력하는 방법
    // cout << "출력할 내용";
    std::cout << "Hello, World!" << std::endl;
    std::cin >> a >> b;
    std::cout << a << " + " << b << " = " << a + b << std::endl;

    string str;
    str = "Hello";
    cout << str << endl;

    string name;
    cout << "이름 입력 : ";
    cin >> name;

    string message = "안녕하세요, " + name + " 님";
    cout << message << endl;

}


728x90
블로그 이미지

Link2Me

,
728x90

구조체(struct)는 하나 이상의 변수를 그룹 지어서 새로운 자료형을 정의하는 것이다.

중첩 구조체란 다른 구조체를 멤버로 포함하는 구조체이다.


예제 1

#define _CRT_SECURE_NO_WARNINGS // strcpy 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>  // strcpy 함수가 선언된 헤더 파일

struct Phone {
    int areacode;
    unsigned long long number;
};

struct Person {
    char name[20];
    int age;
    char address[100];
    struct Phone phone;
};

int main() {
    Person p1;

    strcpy(p1.name, "홍길동"); //p1.name = "홍길동"; 로 입력하면 에러 발생함.
    p1.age = 30;
    strcpy(p1.address, "서울시 관악구 조원중앙로");
    p1.phone.areacode = 82;
    p1.phone.number = 1012345678;

    printf("성명 : %s\n", p1.name);
    printf("나이 : %d\n", p1.age);
    printf("주소 : %s\n", p1.address);
    printf("%d %llu\n", p1.phone.areacode, p1.phone.number);
    // %llu : unsigned long long 변수를 출력할 때 사용
    // %lld : signed long long 변수를 출력할 때 사용

} 


예제2

#include <stdio.h>
#include <string.h>

struct college_detail {
    int college_id;
    char college_name[50];
};

typedef struct {
    int id;
    char name[20];
    float percentage;
    // structure within structure
    struct college_detail clg_data;
}Student_Data; // 구조체 별칭

int main() {

    Student_Data stu_data = { 1, "홍길동", 89.5, 71145, "OO대학교" };
    Student_Data *stu_data_ptr = &stu_data; // 구조체 포인터 선언

    printf(" Id : %d \n", stu_data_ptr->id);
    printf(" 성명 : %s \n", stu_data_ptr->name);
    printf(" 퍼센트 : %f \n\n", stu_data_ptr->percentage);

    printf(" 대학교 Id : %d \n",    stu_data_ptr->clg_data.college_id);
    printf(" 대학교 : %s \n", stu_data_ptr->clg_data.college_name);

    return 0;
}


구조체 선언을 아래와 같이 해도 된다.

struct college_detail {
    int college_id;
    char college_name[50];
};


struct student_detail {
    int id;
    char name[20];
    float percentage;
    // structure within structure
    struct student_college_detail clg_data;
}Student_Data, *stu_data_ptr;

struct student_detail stu_data = { 1, "홍길동", 89.5, 71145, "OO대학교" };
stu_data_ptr = &stu_data;


그리고 구조체 내부에 구조체를 넣어도 된다.

typedef struct {
    int id;
    char name[20];
    float percentage;

    struct college_detail {
        int college_id;
        char college_name[50];
    };
    struct college_detail clg_data;
}Student_Data; // 구조체 별칭


728x90
블로그 이미지

Link2Me

,
728x90

typedef는 C언어에서 타입의 새로운 별칭을 정의하는 키워드이다.

C언어는 자료를 체계적으로 관리하기 위해 구조체라는 문법을 제공한다.

구조체는 관련 정보를 하나의 의미로 묶을 때 사용하며, struct 키워드로 정의한다.
다음은 인적 정보를 표현한 구조체인데 struct Person은 Person 구조체를 정의한다는 뜻이다.
struct Person {
    char name[20];        // 이름
    int age;              // 나이
    char address[100];    // 주소
};


아래 예제를 통해서 구조체 개념을 이해한다.

#define _CRT_SECURE_NO_WARNINGS // strcpy 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>  // strcpy 함수가 선언된 헤더 파일

// 구조체 선언
//typedef struct { int x, y; } Point;
struct Point { int x, y; };

typedef struct _Person { // _Person 은 생략 가능(익명 구조체)
    char name[20];
    int age;
    char address[100];
} Person; // 구조체 별칭을 Person으로 정의

int main() {

    Point p;

    p.x = 3;
    p.y = 4;

    printf("\n (%d, %d)\n", p.x, p.y);
    printf(" sizeof(p) = %d\n", sizeof(p));

    Person p1; // 구조체 별칭 Person으로 변수 선언
   
    strcpy(p1.name, "홍길동");
    p1.age = 30;
    strcpy(p1.address, "서울시 관악구 조원중앙로");

    printf("\n 이름 : %s\n", p1.name);
    printf(" 나이 : %d\n", p1.age);
    printf(" 주소 : %s\n", p1.address);

    // 메모리에 어떻게 저장이 되는지 확인
    printf("\n sizeof(p1) = %d\n", sizeof(p1));
    printf(" 이름 : %d\n", p1.name);
    printf(" 나이 : %d\n", &p1.age);
    printf(" 주소 : %d\n\n", p1.address);


  

    return 0;
}


위 그림에서 보면 메모리 크기는 20 bytes + 4 bytes + 100 bytes = 124 bytes

주소 표시를 %d 로 하여 메모리 공간이 연속적으로 할당되어 있음을 확인할 수 있다.


구조체 포인터

포인터가 어떤 변수의 주소를 담아서 가리키는 변수라는 것은 배웠다.
구조체를 가리키는 포인터를 구조체 포인터라고 한다.
포인터를 선언할 때 int *ptr 형식으로 선언했다. 구조체는 struct 구조체 이름 이 자료형이나 마찬가지이다.
따라서 struct Person *p1 과 같이 구조체 포인터를 선언해야 한다.

구조체 별칭을 사용하면 Person *ptr 과 같이 선언할 수 있다.

#define _CRT_SECURE_NO_WARNINGS // strcpy 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>  // strcpy 함수가 선언된 헤더 파일

typedef struct {
    char name[20];
    int age;
    char address[100];
} Person; // 구조체 별칭

void PersonSwap(Person* a, Person* b) {
    Person tmp = *a;
    *a = *b;
    *b = tmp;
}

int main() {

    Person p1; // 구조체 별칭 Person으로 변수 선언
   
    strcpy(p1.name, "홍길동");
    p1.age = 34;
    strcpy(p1.address, "서울시 관악구 조원중앙로");

    printf("\n 이름 : %s\n", p1.name);
    printf(" 나이 : %d\n", p1.age);
    printf(" 주소 : %s\n", p1.address);

    Person *ptr1 = &p1; // 구조체 포인터 선언

    printf("\n 이름 : %s\n", (*ptr1).name); // 구조체 포인터를 이용해서 값을 대입
    printf(" 나이 : %d\n", (*ptr1).age); // 구조체 포인터를 사용하기 위해서는 항상 괄호를 사용해야 한다.
    printf(" 주소 : %s\n", (*ptr1).address);

    printf("\n 이름 : %s\n", ptr1->name); // ptr1->name은 (*ptr1).name 과 동일
    printf(" 나이 : %d\n", ptr1->age);
    printf(" 주소 : %s\n", ptr1->address);

    Person p2 = {"이순신",40,"서울시 서초구 양재동"};

    PersonSwap(&p1, &p2);

    // swap 결과의 p1 출력
    printf("\n 이름 : %s\n", ptr1->name);
    printf(" 나이 : %d\n", ptr1->age);
    printf(" 주소 : %s\n", ptr1->address);

    return 0;
}


C++ 에서는 헤더 파일이 다르고, printf 출력문을 std::cout 로 하는 것이 다르지만 기본 개념은 동일하다.

C++에서는 변수의 실제 이름 대신 사용할 수 있는 참조자(reference)라는 새로운 기능이 추가되었다.
참조자는 크기가 큰 구조체와 같은 데이터를 함수의 인수로 전달해야 할 경우에 사용할 수 있다.
또한, C++의 클래스(class)를 설계할 때에도 자주 사용된다.
int 변수이름;            // 변수의 선언
int& 참조자이름 = 변수이름; // 참조자 선언, int&는 int형 변수에 대한 참조를 의미


C++ 참조변수 : https://link2me.tistory.com/1746


C++ 구조체 예제

#include <iostream>
using namespace std;

struct Person {
    int age;
    double weight;
    char name[20];
};

int main() {
    cout << endl;
   
    Person p; // 구조체 변수 선언

    p.age = 25;
    p.weight = 60;
    strcpy_s(p.name, "홍길동");

    cout << " age : " << p.age << ", 몸무게 : " << p.weight << ", 이름 : " << p.name << endl;

    Person& ref = p; // 참조변수 선언, 참조변수는 선언과 동시에 초기화되어야 한다.
    ref.age = 35;
    strcpy_s(ref.name, "이순신");
    // ref.name = "이순신"; // 에러 발생
    ref.weight = 75;

    cout << " age : " << ref.age << ", 몸무게 : " << ref.weight << ", 이름 : " << ref.name << endl;

    Person* ptr = &p; // 구조체 포인터 변수 선언
    ptr->age = 20;

    cout << " age : " << ptr->age << ", 몸무게 : " << ptr->weight << ", 이름 : " << ptr->name << endl;

    cout << " address : " << (uintptr_t)&p << endl;
    cout << " address : " << (uintptr_t)&ref << endl;
    cout << " address : " << (uintptr_t)&ptr << endl;

    return 0;
}
 



구조체 포인터 선언 및 동적 메모리 할당

구조체 포인터에 메모리를 할당하는 방법을 알아보자.

#define _CRT_SECURE_NO_WARNINGS // strcpy 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>  // strcpy 함수가 선언된 헤더 파일
#include <stdlib.h>  // malloc, free, exit 함수가 선언된 헤더 파일

typedef struct {
    char name[20];
    int age;
    char address[100];
} Person;

int main() {

    Person* ptr = (Person*)malloc(sizeof(Person)); // 구조체 포인터 선언, 메모리 할당

    // 화살표 연산자로 구조체 멤버에 접근하여 값 할당
    if (ptr != NULL) {
        strcpy(ptr->name, "홍길동");
        ptr->age = 34;
        strcpy(ptr->address, "서울시 관악구 조원중앙로");
    }
    else {
        exit(1); // 에러시 강제 종료, exit(0) : 정상 종료
        // NULL Pointer 역참조 : 일반적으로 객체가 NULL이 될 수 없다는 가정을 위반했을 때
        // 발생하는 보안 취약점이다.
    }

    // 화살표 연산자로 구조체 멤버에 접근하여 값 출력
    printf("이름: %s\n", ptr->name);     
    printf("나이: %d\n", ptr->age);     
    printf("주소: %s\n", ptr->address);  

    free(ptr);  // 동적 메모리 해제

    return 0;
}

마지막으로 구조체를 다 사용했다면 반드시 free 함수로 메모리를 해제해 줘야 한다.



728x90
블로그 이미지

Link2Me

,
728x90

C언어 포인터를 학습하고 나서 swap 함수로 값을 넘기는 것과 주소를 넘기는 것을 비교해보면서 원하는 결과가 어떻게 도출되는지를 확인해볼 수 있다.


swap 함수와 swap_p함수를 같이 넣어서 call by value, call by reference 를 각각 처리하는 걸 비교해볼 수 있게 했다.


#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

void swap(int x, int y) { // int &x, int &y 로 변경해 보면 결과가 어떻게 나오는지 확인해보라.
    int tmp = x;
    x = y;
    y = tmp;

    printf("swap 함수 내 결과 : ");
    printf("x = %d, y = %d ", x, y);
    printf("&x = %p, &y = %p\n", &x, &y);


  

    // swap() 함수 내에서는 x, y 값이 변경 되었지만,
    // 정작 main() 함수에서는 a, b 값이 그대로 인 것을 확인할 수 있다
    // 오직 swap() 함수 내의 지역 변수에서만 유효함을 확인할 수 있다
}

void swap_p(int *x, int *y) { // 함수의 매개변수는 포인터 타입으로 선언
    int tmp = *x; // 역참조 연산자(*)를 사용하여 포인터가 가리키는 주소에 저장된 값을 tmp 에 저장
    *x = *y;
    *y = tmp; // tmp 를 포인터가 가리키는 주소에 저장된 값에 저장

    printf("swap 함수 내 결과 : ");
    printf("x = %d, y = %d ", *x, *y); // 값 출력
    printf("&x = %p, &y = %p\n", x, y); // 주소 출력


  

}

int main() {
    int a = 2;
    int b = 3;

    printf("swap 이전 값 출력 : ");
    printf("a = %d, b = %d ", a, b);
    printf("&a = %p, &b = %p\n", &a, &b);

    //scanf("%d%d", &a, &b);

    //swap(a, b); // 값을 넘겨준다(call-by-value)

    swap_p(&a, &b); // 주소를 넘겨준다.(call-by-reference)

    printf("swap 이후 값 출력 : ");
    printf("a = %d, b = %d ", a, b);
    printf("&a = %p, &b = %p\n", &a, &b);

    return 0;
}


값에 의한 전달(call by value)
값에 의한 전달 방법은 인수로 전달되는 변수가 가지고 있는 값을 함수 내의 매개변수에 복사하는 방식이다.
이렇게 복사된 값으로 초기화된 매개변수는 인수로 전달된 변수와는 완전히 별개의 변수가 된다.
따라서 함수 내에서의 매개변수 조작은 인수로 전달되는 변수에 아무런 영향을 미치지 않다.


참조에 의한 전달(call by reference)
참조에 의한 전달 방법은 인수로 전달된 변수의 값을 복사하는 것이 아닌, 원본 데이터를 직접 전달하는 것이다.
C언어에서는 이러한 작업을 포인터를 사용하여 인수로 전달된 변수의 주소값을 전달한다.
하지만 C++에서는 이러한 작업을 참조자(reference)를 사용하여 전달할 수 있다.



Java

Java는 기본형(primitive) 데이터 외에는 모두 참조형(reference) 데이터다.

기본형은 call by value 이고 객체는 call by reference 이다.


구글링을 해보면 Java 는 call by value 다, call by reference 다 의견이 분분하다.

https://www.tutorialspoint.com/Call-by-value-and-Call-by-reference-in-Java 를 참조하면 도움이 될 것 같다.


public class Tester {

    public static void main(String[] args) {
        int a = 30;
        int b = 45;
        System.out.println("Before swapping, a = " + a + ", b = " + b);

        swap(a, b);
       
        System.out.println("\nAfter swapping, a = " + a + ", b = " + b);
    }

    public static void swap(int x, int y) {
        System.out.println("\nBefore swapping(Inside), x = " + x + ", y = " + y);

        int tmp = x;
        x = y;
        y = tmp;
        System.out.println("After swapping(Inside), x = " + x + ", y = " + y);
    }

}

실행 결과 ==> 원하던 결과가 아님을 알 수 있다. swap 메소드에 넘기는 것이 value 이기 때문이다.

Before swapping, a = 30, b = 45
Before swapping(Inside), x = 30, y = 45
After swapping(Inside), x = 45, y = 30
After swapping, a = 30, b = 45


public class Tester {

    public static void main(String[] args) {
        IntWrapper a = new IntWrapper(30);
        IntWrapper b = new IntWrapper(45);


        System.out.println("Before swapping, a = " + a.value + ", b = " + b.value);

        swap(a, b);

        System.out.println("After swapping, a = " + a.value + ", b = " + b.value);
    }

    public static void swap(IntWrapper x, IntWrapper y) {
        System.out.println("Before swapping(Inside), x = " + x.value + ", y = " + y.value);

        int tmp = x.value;
        x.value = y.value;
        y.value = tmp;
        System.out.println("After swapping(Inside), x = " + x.value + ", y = " + y.value);
    }
}

class IntWrapper {
    public int value;

    public IntWrapper(int value) {
        this.value = value;
    }
}

실행 결과 ==> reference 로 받은 다음, 그 내부 변수를 서로 변경하는 방법으로 처리

Before swapping, a = 30, b = 45
Before swapping(Inside), x = 30, y = 45
After swapping(Inside), x = 45, y = 30
After swapping, a = 45, b = 30

728x90
블로그 이미지

Link2Me

,
728x90

배열 포인터 : 배열을 가리키는 포인터. 연속적인 메모리만 가리킬 수 있다.

포인터 배열 : 배열에 메모리 주소 값을 저장할 수 있는 배열. 즉 포인터들이 배열이다.


 - 배열명은 변수가 아니라 상수로 시작주소를 갖고 있다. 따라서 값을 변경할 수 없다.

 - 포인터는 값을 변경할 수 있다.

 - 배열은 내부적으로 포인터다.

 - 배열과 포인터는 서로 호환된다.

 - 배열명은 a = a + 1; 불가능하다.

 - 포인터는 p = p + 1; 가능하다.



예제1.

#include <stdio.h>
#include <iostream> //  C++에선 <iostream>에 정의되어 있는 std::를 이용해서 입출력을 사용한다.

int main() {
    // 배열 포인터 : 배열을 가리키는 포인터. 연속적인 메모리만 가리킬 수 있다.
    // 포인터 배열 : 배열에 메모리 주소 값을 저장할 수 있는 배열. 즉 포인터들이 배열이다.

    /*
        1. ptr == &ptr[0]
        2. *ptr = ptr[0]
        3. ptr + 1 = ptr 에 sizeof(*ptr)을 더한 값
    */

    int arr[5] = {1,2,3,4,5};

    int(*ptr_arr)[5]; // int형 타입의 인덱스를 5개 가지고 있는 배열을 가리키는 배열 포인터를 선언
    ptr_arr = &arr;

    printf("\n***** 배열 포인터 *****\n");

    // 배열의 주소를 가진 포인터는 +1 / -1 연산으로 어느 원소든 쉽게 접근이 가능하다.
    // 포인터에 1을 더했지만 주소는 배열의 단위인 int(4byte) 만큼 더해진 것을 확인할 수 있다.
    std::cout << "arr[0] 주소 값 :: " << *ptr_arr << "\n";
    std::cout << "arr[1] 주소 값 :: " << *(ptr_arr + 1) << "\n";
    std::cout << "arr[2] 주소 값 :: " << *(ptr_arr + 2) << "\n";
    std::cout << "arr[3] 주소 값 :: " << *(ptr_arr + 3) << "\n";
    std::cout << "arr[4] 주소 값 :: " << *(ptr_arr + 4) << "\n\n";

    // 입력은 std::cin >> "변수" ,  출력을 std::cout << "변수 or 문자열", 문자열 변경은 std::endl;로 사용

    for (int i = 0; i < 5; i++) {
        printf("arr[%d] 주소 값 :: %p\n",i, ptr_arr[i]);
    }
    printf("\n");

    for (int i = 0; i < 5; i++) {
        printf("%d\n", (*ptr_arr)[i]);
    }

    // 포인터 배열
    int* ptr[5]; // 포인터 배열 선언

    for (int i = 0; i < 5; i++) {
        ptr[i] = &arr[i];
    }

    printf("\n***** 포인터 배열 *****\n");
    for (int i = 0; i < 5; i++) {
        printf("arr[%d] 주소 값 :: %p\n", i, ptr[i]);
    }

    for (int i = 0; i < 5; i++) {
        printf("arr[%d] 값 :: %d\n", i, *ptr[i]);
    }
    printf("\n");


    printf("\n***** 2차원 배열을 포인터에 넣기 *********\n");
     
    // 2차원 배열을 포인터에 넣기

    int Arr[2][3] = { {1,2,3},{4,5,6} };

    // 포인터 선언 : 자료형 (*포인터이름)[가로크기]
    int (*Ptr)[3] = Arr; // Ptr 은 행 전체를 가리키는 포인터 (가로크기가 4인 배열을 가리키는 포인터)

    printf("%p\n", *Ptr);
    printf("%p\n", *Arr);
    printf("%d\n", Ptr[1][1]);
    printf("%d\n", sizeof(Arr)); // 결과 : 4 X 5 = 20
    printf("%d\n", sizeof(Ptr));  // 결과 : 4
    printf("\n");

    for (int i = 0; i < 2; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", Ptr[i][j]);
        }
        printf("\n");
    }

    printf("**************\n");
    for (int(*row)[3] = Arr; row < Arr + 2; row++) {
        for (int *col = *row; col < *row + 3; col++) {
            printf("%d ", *col);
        }
        printf("\n");
    }
 
}



아래 코드는 https://dojang.io/mod/page/view.php?id=317 에 있는 걸 Visual Studio 2019 Community 로 테스트해서 에러나는 부분은 수정했다.


배열의 크기를 동적으로 할당받는 걸 시도해보면...

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
    int size;
    scanf("%d", &size);  // 배열의 크기를 입력받음

    int Arr[size];    // Visual Studio 2019에서는 컴파일 에러
}

에러가 발생한다.

C에서 배열의 크기를 동적으로 할당하는 방법은

포인터에 메모리를 동적으로 할당하여 배열처럼 사용하는 방법으로는 가능하다.

포인터에 malloc 함수로 메모리를 할당해주면 된다.


예제2.

#include <stdio.h>
#include <stdlib.h>  // malloc, free 함수가 선언된 헤더 파일

int main()
{
    // 포인터에 할당된 메모리를 배열처럼 사용하기

    // 자료형 *포인터이름 = malloc(sizeof(자료형) * 크기);

    int* numPtr = (int*)malloc(sizeof(int) * 10);    // int 10개 크기만큼 동적 메모리 할당
    // malloc 함수 : 동적으로 메모리를 할당하는 함수 (힙(Heap) 영역에 메모리를 할당)
    // malloc은 메모리만 할당하는 함수이기 때문에 개발자가 어떠한 데이터 형을 저장하는지 예측할 수 없다.
    // int형 데이터를 저장하기 위해서는 리턴되는 void*을 int*로 변환해야 한다.


    numPtr[0] = 10;    // 배열처럼 인덱스로 접근하여 값 할당
    numPtr[9] = 20;    // 배열처럼 인덱스로 접근하여 값 할당

    printf("\n**** 메모리 동적 할당 ****\n");
    printf("%d\n", numPtr[0]);    // 배열처럼 인덱스로 접근하여 값 출력
    printf("%d\n", numPtr[9]);    // 배열처럼 인덱스로 접근하여 값 출력

    free(numPtr); // 동적으로 할당한 메모리 해제
    // 동적할당 후 더 사용할 이상 필요가 없다면 꼭 free함수로 메모리를 해제 시켜주어야 한다.

    return 0; // 굳이 적어주지 않아도 된다.

}


힙(heap)은 C 언어나 자바와 같은 프로그래밍 환경에서 원시 자료형이 아닌 보다 큰 크기의 데이터를 담고자 동적으로 할당하는 메모리 공간을 지칭한다.

- 장점: 상황에 따라 원하는 크기만큼의 메모리가 할당되므로 경제적이며,

         이미 할당된 메모리라도 언제든지 크기를 조절할 수 있다.
- 단점: 더 이상 사용하지 않을 때 명시적으로 메모리를 해제해 주어야 한다.


C++ 에서는 new 연산자를 통해 동적 할당하고, delete 연산자를 통해 메모리를 해제한다.


C++ 배열 및 포인터에 대한 예제다.

char myString[255];

cin >> myString  으로 입력 받으면 공백 이후 입력값은 무시된다.

cin.getline(myString, 255); 로 입력을 받으면 공백도 입력이 된다.


예제3.

#include <iostream>

using namespace std;

void ShowArray(int arr[]) {
    cout << "function size : " << sizeof(arr) << endl; // 4
    cout << "function value : " << *arr << endl; // 배열의 첫번째 값 반환
    cout << endl;
}

int main() {

    int arr[5] = { 1, 2, 3, 4, 5 };

    for (int i = 0; i < sizeof(arr)/sizeof(int); i++) {
        cout << arr[i] << endl;
    }
    cout << endl;

    int* ptr = arr; // 포인터 선언 및 주소 할당

    cout << "arr 값 : " << *arr << endl; // 배열의 첫번째 값 반환
    cout << "ptr 값 : " << *ptr << endl; // 배열의 첫번째 값 반환
    cout << endl;

    cout << "arr 주소 : " << arr << endl; // 배열명이 배열의 첫번째 요소의 주소 반환
    cout << "arr[0] 주소 : " << &arr[0] << endl; // 배열의 첫번째 요소의 주소 반환
    cout << endl;

    cout << "arr[0] 주소 : " << (uintptr_t)&arr[0] << endl; // 10진수 주소 반환
    cout << "ptr 주소 : " << uintptr_t(ptr) << endl; // 10진수 주소 반환
    cout << endl;

    cout << "ptr1 주소 : " << uintptr_t(ptr + 1) << endl; // 10진수 주소 반환
    cout << "ptr2 주소 : " << uintptr_t(ptr + 2) << endl; // 10진수 주소 반환
    cout << "ptr3 주소 : " << uintptr_t(ptr + 3) << endl; // 10진수 주소 반환
    cout << endl;

    // 포인터 연산
    for (int i = 0; i < sizeof(arr) / sizeof(int); i++) {
        cout << "ptr" << i << " 값 : " << *(ptr + i) << " , 주소 : " << (uintptr_t)(ptr + i) << endl;
    }
    cout << endl;

    cout << "arr size : " << sizeof(arr) << endl; // 4 X 5 = 20
    cout << "ptr size : " << sizeof(ptr) << endl; // 4
    cout << endl;

    ShowArray(arr);

    char name[] = "GilDong, Hong"; // 문자 배열로 마지막에 NULL 값이 들어가 있다.
    char* ptr_n = name; // 배열명이 주소를 반환하므로, 포인터 변수 선언 및 주소 할당

    cout << name << endl; // 값을 반환. 주소를 출력할 것으로 생각되는데 값을 반환한다.
    cout << (uintptr_t)&name << endl; // 주소 반환

    const int n_name = sizeof(name)/sizeof(char);

    for (int i = 0; i < n_name; i++) {
        cout << *(ptr_n + i); // 마지막 NULL 값까지 반환됨
    }

    return 0;
}
 



참조하면 좋은 강좌

https://www.youtube.com/watch?v=H-FfGUwSU-U&t=296s


728x90
블로그 이미지

Link2Me

,