c언어의 main()은 실행 파일 옵션을 매개 변수로 받을 수 있다.

아래 예제를 보자.

#include <stdio.h>

int main(int argc, char* argv[] )
{
	printf("argument count: %d\n", argc);
	int i = 0;
	while (i < argc)
	{
		printf("argument value[%d]: %s\n", i, argv[i]);
		i++;
	}
	return 0;
}

 

해당 파일의 경로로 가서 파일을 실행시킴과 동시에 옵션을 전달해줄 수 있다.

window에서 실행파일이 있는 경로로 가서 주소창에 cmd를 입력하면 커맨드창을 실행할 수 있다.

 

argv의 0 번째에는 파일이름 나머지에는 전달한 옵션이 저장되어 있는 것을 확인할 수 있다.

'C language' 카테고리의 다른 글

함수포인터  (0) 2024.12.14
변수 키워드 const, volatile  (1) 2024.12.14
변수 키워드 static과 extern  (1) 2024.12.14
전처리지시문, Header Guard  (1) 2024.12.14
c 구조체, 유니온, typedef, 패딩, 비트필드  (0) 2024.12.13

함수 포인터

함수의 이름도 주소이다.

#include<stdio.h>

void Func1(int a) {
	printf("Func1 %d\n", a);
}
int Func2() {
	printf("Func2\n");
	return 10;
}
int main() {

	void (*p)(int);
	p = Func1;
	(*p)(10);

	int (*q)();
	q = Func2;

	printf("%d", (*q)() + 100);

	return 0;
}

구조체 내에서 함수 포인터를 사용하여 아래와 같이 사용한다.

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iopoll)(struct kiocb *kiocb, struct io_comp_batch *,
			unsigned int flags);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
	__poll_t (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	unsigned long mmap_supported_flags;
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	void (*splice_eof)(struct file *file);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
				   struct file *file_out, loff_t pos_out,
				   loff_t len, unsigned int remap_flags);
	int (*fadvise)(struct file *, loff_t, loff_t, int);
	int (*uring_cmd)(struct io_uring_cmd *ioucmd, unsigned int issue_flags);
	int (*uring_cmd_iopoll)(struct io_uring_cmd *, struct io_comp_batch *,
				unsigned int poll_flags);
} __randomize_layout;

 

간단한 예제로 작성해보자

#include <stdio.h>

// 구조체 정의: 함수 포인터를 멤버로 가지는 구조체
struct Hyndrome_operation {
    void (*act1)(int); // 함수 포인터 멤버
    void (*act2)(int); // 함수 포인터 멤버
};

// 함수 선언
void Func1(int x) {
    printf("Func1 : % d\n", x);
}

void Func2(int x) {
    printf("Func2 : %d\n", x);
}

int main() {
    // 구조체 변수 선언 및 선택적 초기화
    struct Hyndrome_operation hyops = {
        .act1 = Func1,
        .act2 = Func2,
    };

    // 함수 포인터를 멤버로 갖는 구조체의 함수 호출
    hyops.act1(10); // Func1 호출
    hyops.act2(20); // Func2 호출

    return 0;
}

'C language' 카테고리의 다른 글

main argument  (1) 2024.12.14
변수 키워드 const, volatile  (1) 2024.12.14
변수 키워드 static과 extern  (1) 2024.12.14
전처리지시문, Header Guard  (1) 2024.12.14
c 구조체, 유니온, typedef, 패딩, 비트필드  (0) 2024.12.13

const

변수에 적용

const는 영어로 constant 상수를 뜻한다

변수에 해당 키워드가 붙으면 값의 변경이 불가능하다.

int main()
{
	const int var1 = 1;
	var1 = 2; // 불가능: const가 붙으면 값의 변경이 불가능하다
	return 0;
}

포인터에 적용

포인터에 사용되면 위치에 따라 의미가 달라짐

포인터가 가리키는 값이 변경 불가능한 경우

const int *ptr = &x;  // ptr은 x를 가리킬 수 있지만, 값을 변경할 수 없음
*ptr = 20;            // 오류! 가리키는 값 변경 불가
ptr = &y;             // OK! ptr이 다른 변수를 가리키는 것은 가능

포인터 자체가 변경 불가능한 경우

int *const ptr = &x;  // ptr은 x를 가리키며, 포인터의 주소를 변경할 수 없음
*ptr = 20;            // OK! 가리키는 값 변경 가능
ptr = &y;             // 오류! ptr의 주소 변경 불가

포인터와 가리키는 값 모두 변경 불가능한 경우

const int *const ptr = &x;  // ptr이 가리키는 값과 주소 모두 변경 불가
*ptr = 20;                  // 오류! 값 변경 불가
ptr = &y;                   // 오류! 주소 변경 불가

 

volatile

최적화를 방해하는 키워드이다. 

컴파일러는 사용하지 않는 쓸모 없는 코드들을 정리한다.

volatile 용도

  • 메모리 주소를 가진 IO 레지스터
    • H/W가 연결된 메모리 주소값은 사용자가 값을 수정하지 않아도 H/W에 의해 변경될 수 있는데, 컴파일러가 최적화 한다고 주소값을 없애버릴 수 있음
  • 인터럽트 핸들러가 값을 변경하는 전역 변수
    • 인터럽트 핸들러가 값을 변경할 수 있다는 것을 컴파일러가 인지 못할 수 있음
  • 최적화에 의해 오류가 발생할 가능성이 있는 변수

'C language' 카테고리의 다른 글

main argument  (1) 2024.12.14
함수포인터  (0) 2024.12.14
변수 키워드 static과 extern  (1) 2024.12.14
전처리지시문, Header Guard  (1) 2024.12.14
c 구조체, 유니온, typedef, 패딩, 비트필드  (0) 2024.12.13

기본적으로 소스파일이 같은 프로젝트에 있어서 컴파일할 때 링킹이 되는 경우 함수는  별다른 추가 코드 작성 없이 공유된다.

아래 코드를 예시로 보자

main.c

int main()
{
	Hyndrome();
	return 0;
}

hyndrome.c

#include <stdio.h>

void Hyndrome()
{
	printf("I am HYndrome\n");
}

 

하지만 변수는 공유되지 않는다.

extern

extern 키워드를 사용하면 변수도 공유할 수 있다.

아래 예시를 보자

main.c

#include <stdio.h>

extern int a = 1;

int main() {
	printf("main1 %d\n", a);
	Hyndrome();
	printf("main2 %d\n", a);
	return 0;
}

hyndrome.c

#include <stdio.h>
int a;
void Hyndrome() {
	//main.c 에서 extern 된 a 값 출력
	printf("Hyndrome1 %d\n", a);
	a = 2;
	printf("Hyndrome2 %d\n", a);
}

활용

header에 extern 변수를 사용하는 경우 

#include만 하면, 누구나 쓸 수 있는 전역 변수가 된다

common.h

#ifndef COMMON_H
#define COMMON_H

extern int globalVariable;

#endif

소스 파일에 extern 변수를 사용하는 경우

내부적으로 전역 변수를 사용할 때 사용

#include "common.h"

void function1()
{
	extern int globalVariable;
}

Static

static은 다양한 곳에 쓰인다.

함수 내 지역 변수 앞 : 전역 변수화

프로그램이 종료될 때까지 변수의 값이 유지되지만 해당 함수 내부에서만 접근이 가능함

#include <stdio.h>

void function1()
{
	static int var1 = 0;
	for (int i = 0; i < 5; i++)
	{
		printf("function1: %d\n", var1++);
	}
}

int main()
{
	function1();
	function1();
	// printf("result : %d\n", var1); 불가능: var는 function1 내부에서만 접근 가능

	return 0;
}

전역 변수 앞 : 다른 소스 파일에서 공유 불가 ( static > extern)

파일 내부에서만 접근할 수 있는 파일 범위(File scope)를 가지게 된다.

다른 파일에서 전역 변수 이름이 충돌하지 않도록 보호한다.

static은 extern 보다 우선 순위를 가진다.

'C language' 카테고리의 다른 글

함수포인터  (0) 2024.12.14
변수 키워드 const, volatile  (1) 2024.12.14
전처리지시문, Header Guard  (1) 2024.12.14
c 구조체, 유니온, typedef, 패딩, 비트필드  (0) 2024.12.13
고정길이 정수 - stdint.h  (0) 2024.12.13

전처리 지시문

MS learn challenge 전처리 지시문

 

전처리기 지시문

자세한 정보: 전처리기 지시문

learn.microsoft.com

https://learn.microsoft.com/ko-kr/cpp/preprocessor/preprocessor-directives?view=msvc-170

#define

매크로 함수라 한다. 단순 치환이 된다.

  • 장점: 함수 호출이 아니므로 실행속도가 빠름
  • 단점: 실제 코드로 변경되므로 코드 사이즈가 늘어남 
#define SUM(a,b) a+b

int main()
{
	int result = SUM(3, 5);
	
	return 0;
}

 

#undef

정의된 매크로를 해제한다.

정의되지 않은 매크로에 해제에 대한 에러가 발생하지 않는다.

다수의 소스파일에서 정의된 매크로를 재정의할 때 사용한다.

#ifdef ~ #endif , #ifndef  ~ #endf

단일 매크로 확인에 적합하다. 간결하고 읽기 쉽다.

#include <stdio.h>

#define RELEASE
// #define DEBUG

#ifdef DEBUG
int main()
{
	printf("debug mode\n");
	return 0;
}
#endif

#ifdef RELEASE
int main()
{
	printf("release version\n");
	return 0;
}
#endif

#if ~ #elif ~ #else ~ #endif

&&, ||, ==, !=, >, < 등 연산 기호를 사용할 수 있다.

#if defined 

#ifdef와 동일하게 사용 가능하다. 

&&, ||, ==, !=, >, < 등 연산 기호를 사용할 수 있어서 다중 조건에 사용 가능하다.

#error

error 메세지를 출력한다.

어떤 장치를 장착할 지 알수 없는 초기 개발 단계에서 초기화 코드를 작성할 때 쓰인다고 한다.

#include <stdio.h> 

#define DEV1
#define MODE 3

#if defined DEV1 && MODE == 3
int main()
{
	printf("success!\n");
}
#elif !defined(DEV1)
#error no device
#else
#error incorrect mode
#endif

 

Header Guard

헤더 파일에서 중복으로 포함되는 것을 방지하기 위한 기법이다.

조건부 컴파일 전처리기 지시자로 헤더 파일이 중복으로 포함되는 것을 방지할 수 있다.

방법 1 - #pragma once

간단하고 사용하기 쉬운 장점이 있다.

사실 현재 대부분의 컴파일러가 지원한다.

하지만 방법 2에 비해서 잘 사용하지 않는다. 그 이유는

  • 방법 2인 #ifndef가 gcc 표준이다 (gcc ver3.4 이전 버전에서 지원 x)
  • 같은 파일이 여러 경로에 있을 경우에 컴파일러가 다른 파일로 인식해서 중복 포함

자바스크립트에서 ; 를 계속 포함하듯 gcc가 워낙 메이저하다보니 잘 사용이 안되는 것 같다.

#pragma once
// 헤더 파일 내용
#include <stdio.h>

void func1();

 

방법2 - #ifndef ~ #define ~ #endif

해당 이름의 매크로가 없을 경우 해당 이름의 매크로를 정의하고 헤더파일을 작성하는 방법이다.

해당 헤더 파일을 다시 부를 경우 해당 이름의 매크로가 있어서 생략되게 된다.

#ifndef HEADER_FILE_H
#define HEADER_FILE_H
// 헤더 파일 내용
#include <stdio.h>

void func1();
#endif // !

 

데이터 신호를 주고 받을 때, 일반적으로 1 byte 단위가 자주 사용된다.

주소는 byte 단위이다. 리틀 엔디안을 잊지 말자!

#include <stdint.h>
#include <string.h>

int main()
{
	uint8_t target[5] = { 0xAB, 0x12, 0x34, 0x56, 0xCD };

	union _Data_
	{
		uint8_t receiveData[5];

		struct {
			uint8_t head;
			uint8_t body[3];
			uint8_t tail;
		}msg;
	}data;

	memcpy(&data, target, 5);

	return 0;
}

 

C 구조체

struct

타입을 모아 새로운 타입을 만드는 문법

c와 c++에서 차이가 있다

  • c에서는 만들고 난 뒤, 한꺼번에 초기화 불가능 (선언할 때는 가능) 
  • c에서는 선언할 때 앞에 struct 붙여줘야 함

기본 문법

struct AB
{
	int a;
	int b;
};

int main()
{
	struct AB ab;

	ab.a = 1;
	ab.b = 2;

	return 0;
}

구조체 초기화는 선언할 때만 가능

구조체 특정 멤버를 선언 시, 선택해서 초기화할 수 있음

구조체를 만들면서 변수를 만들 수 있음

구조체 내부에 구조체를 만들 수 있음 (구조체의 이름은 필요하지 않아서 생략 가능)

 

typedef

기존 타입을 원하는 이름으로 정의해서 사용할 수 있다

아래 예시에서는 struct AB 를 SAB로 typedef 하였다.

위 방식보다는 아래 방식이 더 많이 사용된다

아예 구조체 이름을 생략해버릴 수도 있다

union

생긴 건 구조체와 비슷하지만 멤버끼리 값을 공유한다!

아래 예시를 보자

 

union 안에 있는 두 변수의 시작 메모리 주소가 같은 것을 확인할 수 있다.

주소는 byte 단위로 리틀 엔디안이 적용되는 것을 확인할 수 있다.

 

위 내용을 적용해보자

#include <stdint.h>

#pragma pack(1) // 여기서 부터 패딩 사용 안함
typedef union {
	uint8_t ori[6];

	struct {
		uint8_t opcode;

		uint8_t lba_part1 : 5;
		uint8_t reserved : 3;

		uint8_t lba_part2;
		uint8_t lba_part3;

		uint8_t length;
		uint8_t control;
	} field;
}Node;
#pragma pack(4) // 여기서 부터 패딩 사용

 

#pragma는 뭐고 uint8_t lba_part1 : 5; 뒤에 붙은 ' : ' 는 뭘까 ?

padding에 대한 개념을 알아보자!

padding 

아래 상황을 보자

AB 구조체에서 a 는 char라서 1 byte인데 뒤에 3byte가 비고 b가 오는 것을 확인할 수 있다.

이는 CPU가 값을 편하게 읽을 수 있도록 컴파일러가 padding을 줘서 그렇다.

CPU가 4byte 씩 읽을 수 있다면 아래와 같은 차이가 생긴다.

하지만 여기서 문제가 생기는데 컴파일러마다 padding을 주는 조건이 다르다

따라서 padding을 주지 않도록 설정해주는 매크로가 바로 #pragma이다.

 

uint8_t lba_part1 : 5 에서 :비트 필드(bit-field)의 크기를 지정하는 부분이다.

비트 필드는 구조체의 각 멤버가 1비트 또는 그 이상의 비트 단위로 할당될 수 있도록 합니다.

이렇게 설정할 경우 uint8_t  타입에서 5비트만 사용 하고,

1byte 내에서 5비트만 차지하게 된다

고정길이 정수를 사용하는 이유

int는 표준으로 4byte라고 명시되어있지 않다

=> int는 4byte가 아닐 수도 있다!

시스템마다 다른 사이즈를 사용할 수 있다.

어떤 시스템에서도 똑같은 크기를 갖게할 수 있도록 고정길이 정수를 사용한다.

 

여담으로 int는 원래 cpu가 가장 효율적으로 처리할 수 있는 크기를 의미하도록 설계되었다. 32bit 운영체제에서 가장 효율적으로 처리할 수 있는 크기는 4 byte 여서, 32 bit 운영체제에서는 int는 4 byte로 설계되었다.

지금 컴퓨터는 대부분 64 bit 운영체제를 사용하지만 대부분 int 크기가 8 byte가 아닌  4 byte로 사용한다.

그 이유는 

  •  호환성: 이전의 C/C++ 코드의 대부분이 32 bit 환경에서 작성되었고, int 는 4 byte로 대부분 익숙하기 때문
  • 성능: 과거에는 CPU 아키텍처가 데이터 모델과 더 밀접하게 연결되어 있어서 int가 CPU 레지스터 크기와 거의 동일했지만, 현대에는 컴퓨터 아키텍처와 데이터 모델이 분리되어 반드시 동일할 필요가 없어짐
  • 메모리 사용량: 메모리 사용량이 증가하면 캐시성능에 영향을 미칠 수 있으므로 불필요한 크기 증가를 피할 수 있음

stdin.h

#include <stdint.h>

IBM 문서

 

<stdint.h>

<stdint.h> 포함 파일은 너비를 지정하고 해당 매크로 세트를 정의하는 정수 유형 세트를 선언합니다. 또한 다른 표준 포함 파일에 정의된 유형에 대응하는 정수 유형의 한계를 지정하는 매크로도

www.ibm.com

 

고정길이 정수타입

  • int8_t : char
  • int16_t : short
  • int32_t : int
  • int64_t : long long

 

  • uint8_t : unsigned char
  • uint16_t : unsigned short
  • uint32_t : unsigned int
  • uint65_t : unsigned long long

unsigned는 부호없는 정수타입을 나타내는 키워드이다. - 부호를 포함하는 데이터타입에 비해서 약 2배의 양의 범위를 표현할 수 있게 해준다. 

그 외

  • 최소 너비 정수 유형
    • int_least8_t ...
  • 가장 빠른 최소너비 정수 유형 = 해당 크기 이상의 값을 저장할 수 있으면서 해당 플랫폼에서 가장 빠르게 동작
    • int_fast8_t ...
  • 가장 큰 너비 정수 유형
    • intmax_t, uintmax_t

 

문자열은 포인터로 처리된다.

c언어에서는 문자열 마지막에는 항상 '\0'  널 문자가 있기 때문에 배열 선언시 + 1칸 더해줘야 한다.

c언어에서 널문자로 문자열 끝임을 인지한다.

몰론 널 문자 없이 꽉꽉 채워 저장할 수는 있지만, 그 경우 문자열로서 가치는 사라진다.

char v[4] = "ABC" ; vs const char* v = "ABC";

배열을 하나 만들고 네글자를 삽입하는 방식  vs 포인터를 이용해 문자열 상수를 가리키는 방식이다

무슨 차이가 있을까?

const char* v = "ABC";

포인터로 문자열을 가리키는 것부터 알아보자!

"ABC"는 코드상에서 리터럴로 메모리에서 읽기 전용의 데이터 세그먼트 영역에 저장된다.

포인터는 리터럴의 주소를 가리키게 되고 따라서 수정이 불가능하다.

const는 해당 데이터가 변경되지 않음을 보장한다.

char v[4] = "ABC";

배열을 선언하고 초기화를 할 경우, 해당 리터럴이 배열이 저장되어 있는 메모리 공간에 복사가 되어 저장된다.

  • Static : 전역 변수일 경우
  • Stack : 지역 변수일 경우

어느 곳이든 둘다 수정이 가능하다.

 

따라서 char v[4] = "ABC" ; vs const char* v = "ABC" 는 포인터가 가리키고 있는 메모리 영역이 다르며,  그 차이에 따라서 수정 여부가 달라진다.

 

+ Recent posts