반응형

보통 C언어에서 가장 어려워하는 부분은 포인터이다.

이 포스팅은 어떻게 하면 포인터를 더 쉽게 이해할 수 있을까 에 대한 경험과 고민이 섞여 있다.

회사 후배가 프로젝트 소스 코드를 보고 덕지덕지 추가되어 있는 포인터 인자들을 보고 지레 겁을 먹고 포인터에 대한 질문을 종종 한다.

요즘엔 대학교에서는 학생들이 C언어를 그리 선호하지 않는 것 같다. 

아무래도 Low Level 언어인 C는 개발자들에게 불친절하고 포인터라는 큰 벽이 있고, 특히 Computer Science(CS) 분야가 각광받으면서 파이썬 위주로 프로젝트를 진행하기 때문 아닐까 추측해본다.

(Low Level 언어 : 컴퓨터 중심 언어, 속도가 생명.

High Level 언어 : 사람 중심 언어, 개발은 Low Level 보다 간단하나 속도가 느림. ) 

나도 그리 큰 경력은 없지만 지금까지 본 개발자 후배들이 전부 JAVA나 파이썬으로 프로젝트는 많이 해봤으나, C 경험은 적었다.

Static, 지역변수, 전역변수, 매개변수, Function 등의 개념은 있지만, 포인터가 어렵다는 것만 기억하는 사람들이 많다.

하지만 생각보다 간단하다는 것을 알려주고 싶었다.

포인터에 대한 두려움이 존재하지만, 약간의 개발 지식이 있는 사람을 위한다.


이 글을 읽고 꼭 두 가지만 기억하면 된다고 생각한다. 이건 후배들에게도 항상 강조하는 말이다.

1. 변수 선언과 할당

2. *, & 연산자 종류

 

1. 변수 선언과 할당
#include <stdio.h>

int main()
{
    int a;                  // a라는 변수명을 가지는 Int형 변수 선언, 메모리 공간에 값이 비어 있는 a가 생김.
    a = 10;                 // 값이 비어 있는 a의 메모리에 10이라는 값이 할당됨.
    
    printf("a = %d\n", a);

    return 0;
}

간단한 예제를 든다.

포인터를 설명하기 이 전, 변수 선언과 할당에 대해 설명한다.

코드에 주석을 달아 두었다. 우선, 값을 할당하기 이 전에 "a" 라는 이름을 가지는 메모리 공간을 만들었다. int형을 선언했기 때문에 4byte 만큼을 사용하겠다는 컴퓨터와의 약속인 셈이다.

하지만 "a"라는 메모리 공간에는 비어 있다. 

(모든 컴퓨터의 int형이 4byte는 아니지만, 일반적으론 4byte이다.)  나중에 이어 쓸 예정

728x90
반응형
반응형

MCU는 전원이 꺼지고 켜진 경우 (POR : Power on Reset)
처리된 데이터(static 변수, 지역 변수, 매개 변수) 들은  메모리 상에서 남아있지 않고 사라진다.
그래서 MCU가 꺼지고 다시 켜졌을 경우, 데이터를 저장해야 할 때가 있다.
아두이노는 이 역할을 하는 내장 메모리가 있으며, 이를 EEPROM이라고 한다. (하드 디스크처럼 기간과 횟수가 그리 많지 않음.)


(초기 ROM(Read Only Memory)로 오로지 공장에서 출하 과정에서 메모리에  write가 되면, 그 이 후부턴 read 밖에 할 수 없었음. read 만 할 수 있는 문제를 해결하기 위해 자외선을 통해 몇 번의 write를 할 수 있는 EPROM이 개발됨. 조금 더 시간이 흘러 자외선이 아닌 전기로 write가 가능한 EEPROM(Electrically Erasable Programmable ROM)이 개발 됨.

2byte 정수를 1byte 배열로 나눠서 저장하는 글을 작성하면서 EEPROM 이야기를 하는 이유는
EEPROM에 데이터를 저장할 때 한 개의 주소에 1Byte(8bit) 씩 저장하므로 1byte를 초과하는 데이터는 나눠서 저장해야 하기 때문이다.
그리고 CRC-16 결과 값인 2byte 정수를 1byte 배열로 나눠서 저장한다.

CRC(cyclic redundancy check )  = 데이터의 무결성을 체크하기 위해 사용.

예제는 간단하다.
비트 마스킹을 통해 원하는 변수를 추출하고 다운 캐스팅을 통해 형 변환을 해주면 된다.

#include <stdio.h>

#define EEP_DATA_SIZE 2

static unsigned char eepImage[EEP_DATA_SIZE];

static void EepHandler_2byte_to_1byte(void){
    int l_result = 0x0102;            				// 2byte = 0x01_02
    
    eepImage[0] = (unsigned char)(l_result >> 8);	// 0x01_02 >> 8 = 0x00_01
    eepImage[1] = (unsigned char)(l_result & 0x00FF);  // 0x01_02 & FF = 0x00_02

}

int main()
{
    EepHandler_2byte_to_1byte();
    
    printf("EEPImage[0] : %d\n", eepImage[0]);
    printf("EEPImage[1] : %d\n", eepImage[1]);
    
    return 0;
}
728x90
반응형
반응형

Build의 순서는 아래와 같다.

소스코드 -> 전처리 -> 컴파일 -> 링크 -> 실행

C언어를 처음 공부할 때, #define을 사용하지 않으면 전처리 과정을 거치지 않는다고 생각한 적이 있다.

#으로 시작하는 문장은 전부 전처리기 지시자임에도 #include는 전처리기라고 생각하지 않았다. 항상 C언어를 실습할 때, 의무적으로 #inlcude <stdio.h>를 작성한 폐해이다.

전처리란 무엇인가?

컴파일 전에 처리해야 하는 일이고 전처리를 수행하는 장치를 전처리기라고 한다.

전처리기는 헤더 파일을 불러오거나, 소스 파일 내부의 특정 문자열을 상수 또는 문자로 치환하거나, 조건에 따라서 코드의 일부를 컴파일하거나 컴파일하지 못하게 하는 선택 기능을 제공한다.

궁금증이 하나 발생한다.

#define은 왜 사용하는가? 단순히 변수나 함수를 선언 및 호출하면 되지 않는가? 일반 변수 및 함수와 매크로(#define)의 차이는 무엇인가?

이를 이해하기 위해선 컴파일 단계를 이해할 필요가 있다.

컴파일이란 무엇인가? 기계어로 번역하는 번역기이다. 왜 기계어로 번역하는가? 소스 코드는 사람이 이해하기 쉬운 문장이며, 기계는 이해할 수 없다. 그러므로 기계가 이해할 수 있는 문장으로 번역해줘야 한다.

C언어가 생기기 이 전, 개발자들은 기계(컴퓨터, CPU)를 직접 제어하기 위해 기계어(어셈블리어)를 직접 작성하였다. 하지만 CPU는 다양했고, CPU의 종류에 따라 어셈블리어 작성하는 방법도 각기 달랐다고 하며, 간단한 프로그램에도 코드가 굉장히 길어졌다. 

이 문제점을 해결하기 위해, 기계어를 직접 사람이 작성하지 않아도 되는 번역기를 개발했는데 그것이 컴파일러이다. 개발자들은 더 이상 기계의 종류를 상관하지 않아도 된다.


C언어는 소스코드 중복을 줄이고 코드의 재사용성을 높이기 위해서 함수를 제공한다. 하지만, 함수는 호출될 때 *스택 프레임이 일어나기 때문에 속도가 느려지는 단점이 있다. 따라서 코딩할때는 매크로를 함수와 같은 기능으로 작성하지만 실제 기계어로 번역될 때는 직접 코딩한 것처럼 처리(치환, 전처리 단계에서 매크로가 소스코드로 변환)된다.

함수 호출 시, 스택이라는 메모리 공간에 매개 변수, 반환 포인터 값, 지역 변수 순서로 저장되는 값을 스택 프레임이라고 한다.

즉, 저장 공간인 스택을 메모리 낭비를 줄이고 실행 속도를 높이기 위해 매크로를 사용한다. 하지만 매크로는 소스코드로 바로 변환되므로 실행 파일 크기가 커지며, 재귀호출을 할 수 없다는 단점을 가지므로 적절하게 사용하는 것이 중요하다. 

요즘엔 컴퓨터의 메모리가 굉장히 넉넉하기 때문에 메모리 걱정을 하지 않을 때가 많지만, 메모리 공간이 넉넉치 않은 임베디드 시스템과 같은 환경에서는 매크로를 많이 사용한다.


아래 매크로 함수와 일반 함수 예시를 보겠다.

#include <stdio.h>

#define MUL(x, y) x*y

int mul(x, y)
{
	return x*y;
}
int main()
{
	int a, b;
    
    scanf("%d %d", &a, &b);
    printf("%d", MUL(a+1, b+1));
    printf("%d", mul(a+1, b+1));
}

// result
// a=3, b=4 입력
// 8
// 20

왜 결과값이 8, 20으로 다른 값이 나올까?  매크로는 전처리 단계에서 소스 코드로 변경된다고 했다. 

MUL(a+1, b+1)을 소스 코드로 치환되면 a+1*b+1 이다.

그러므로 3+1*4+1로 계산되어 8이라는 결과가 출력된다. mul과 같은 결과를 내기 위해선, 

#define MUL(x, y) ((x) * (y)) 로 변경하면 된다. 소스코드로 치환하게 되면, ((a+1)*(b+1))이다.

728x90
반응형
반응형

strcpy : string copy

char* strcpy(char *strDestination, const char *strSource)

// strDestination : 복사 당하는 대상
// strSource : 복사할 대상
// return strDestination 포인터
// 
// _CRT_SECURE_NO_WARNINGS 발생하므로 사용 지양, strcpy_s 사용 지향

errno_t strcpy_s(char *dest, rsize_t dest_size, const char *src)

// dest : 복사 당하는 대상
// dest_size : src 메모리 크기
// src : 복사할 대상
// return strDestination 포인터

 

#include <iostream>

using namespace::std;


void string_buf(char* dest, int& size ,char* src)
{
	strcpy_s(dest, size, src);
	cout << "\n\r[INFO] " << dest << endl;
}


int main()
{
	int size = strlen("Hello World");
	size++;

	char* buf = new char[size];

	string_buf(buf, size, (char*)"Hello World");

	delete [] buf;

	return 0;
}
728x90
반응형
반응형

Call by Value 와 Call by Reference 의 차이에 대해 이야기합니다. 

 

아래 하나의 예제를 보겠습니다.

#include <stdio.h>


void call_by_value(int test)
{
	test = test + 10;
	printf("call by value address %p\n", &test);
}

int call_by_value_return(int test)
{
	test = test + 10;
	printf("call by value address %p\n", &test);
	return test;
}

void call_by_reference(int *test)
{
	printf("call by reference %p\n", test);
	*test = *test+1;
}


int main()
{
	int value = 10;

	printf("val %d\n", value);
	printf("val address %p\n", &value);

	printf("-------------------------\n");
	call_by_value(value);
	printf("val %d\n", value);
	printf("val address %p\n", &value);

	printf("-------------------------\n");
	call_by_reference(&value);
	printf("val %d\n", value);
	printf("val address %p\n", &value);

	printf("-------------------------\n");
	int value_return = call_by_value_return(value);
	printf("val %d\n", value);
	printf("val address %p\n", &value);
	printf("val_return %d\n", value_return);
	printf("val_return address %p\n", &value_return);

	return 0; 
}

아래 간단한 C코드의 결과이다.

위 결과가 위 처럼 나온 것을 이해하기 위해선 메모리에 대한 고민이 필요합니다.

 

우선, 메모리에는 컴파일 전 메모리를 할당하는 데이터 영역과 스택(Stack)영역과 힙(Heap)영역이 있습니다. 

  • 데이터(Data) 영역 (정적 메모리)
    • 전역 변수와 static 변수가 할당되는 영역
    • 프로그램의 시작과 동시에 할당되고 프로그램이 종료되어야 메모리에서 소멸됨
  • 스택(Stack) 영역
    • 함수 호출 시 생성되는 지역 변수와 매개 변수가 저장되는 영역
    • 함수 호출이 완료되면 사라짐
  • 힙(Heap) 영역
    • 동적으로 할당된 변수

데이터 영역과 힙 영역은 사용되고 있지 않습니다.

스택 영역의 현재 코드를 그림으로 간단히 나타내면,

 

위 코드 메모리를 간단한 블록 그림으로 나타냄

스택영역은 컴파일하면서 Stack영역을 위 사진 같이 메모리를 할당합니다.

 

Call by Value는 Main 함수에서 다른 함수로 인자의 을 전달하기 때문에, Main 함수의 인자와 다른 함수의 인자는 서로 다른 주소를 가집니다. 그렇기 때문에 다른 함수에서 인자 값이 변하더라도 Main 함수의 인자는 다른 변수이기 때문에 아무 일도 일어나지 않습니다. 

 

Call by Reference는 Main 함수에서 다른 함수로 인자의 주소 값을 전달하기 때문에, Main 함수의 인자와 다른 함수의 인자는 서로 같은 주소를 가집니다. 그렇기 때문에 다른 함수에서 값이 인자 값이 변하더라도 Main 함수의 인자는 같은 변수 이기 때문에 변하게 됩니다.

728x90
반응형

+ Recent posts