C语言中的类型系统
结构体
// 声明一个结构体
struct Person{
char *name;
int age;
char *id;
};
// 声明一个结构体变量
struct Person person={
.name="hello",.age=10,.id="123"
};
// 声明一个结构体的指针
struct Person *p_person=&person;
PRINTFLINE("%d:",person.age);
// 结构体指针访问的 -> 操作符
PRINTFLINE("%s:",p_person->name);
// 每次都要struct Person 很麻烦 那直接定义一个 别名 就可以简化操作了
typedef struct Person Person;
PRINTFLINE("%lu:", sizeof(Person));
内存对齐
还是前面的程序
我们看下 这个person的结构体 到底占用多少个byte, 打印出来是24个byte
有人就觉得奇怪了,为啥? 2个char 一共16个byte 一个int 4个byte 应该是 20个byte 才对啊,
多出来的4个byte 哪来的?
我们看下 内存快照,看一下这个person实际的存储
简单处理下这个问题
#pragma pack(2)
再试一下即可
联合体
union Operator{
int a;
double d;
};
union Operator op={.a=4,.d=1.0};
PRINTFLINE("%d %f",op.a,op.d);
看下执行结果
这个联合体别的语言应该是没有的,有个特点是 他所占用的内存就是他内部字段最大的内存大小,
比如这里占用的内存大小就是8,
此外 因为是共享8个byte 内存大小,如果d有值,那么会覆盖掉a的区域,从而a的值就不是你设置的值了
这个特点 以我浅薄的知识,java kotlin 是没有类似机制的
枚举
typedef enum FILE_IMAGE{
PNG,JPEG,WEBP
}
枚举倒是和java 区别不大,本质上都是int值, 上述的例子中就是 0 1 2 3个值
下面这个例子就是 0 5 6
typedef enum FILE_IMAGE{
PNG,JPEG=5,WEBP
}
判断字节序
假设内存中 有个4个字节 分别存了4个值 分别是 01 02 03 04, 在内存中 从左到右分布着 这4个字节
对于cpu来说 怎么读这4个字节就有意思了
0x04030201 这样就是小端序 (一般cpu都是小端读) 0x01020304 这样就是大端序 (一般是网络传输用大端序)
既然环境是不一样的,那么有时候 我们需要来判断一下当前系统是大端还是小端序
这个其实用union 来判断就很容易 。 我们可以写个程序来验证一下
假设我们要存一个值 是 0x100 内存中的分布 无非就是00 01 (小端) 或者是 01 00(大端)
基于union 来表示一下
bool isSmallEndian() {
union {
char c[2];
short s;
} value = {.s=0x100};
return value.c[0] == 0;
}
C语言中的字符串
这个小节 个人觉得有个印象就可以了,需要用的时候 直接谷歌搜一下api即可,没必要花时间去记(字符串比较,字符串查找,字符串拆分,字符串的连接,字符串的复制)
如何判断一个字符是数字还是字母
标准库都有现成的,有兴趣的可以看下实现,这里不多说了
#include <stdio.h>
#include <ioprint.h>
#include <ctype.h>
int main(){
PRINTFLINE("%d:",isdigit('1'));
return 0;
}
唯一要注意的是这里面如果返回值是0 代表 不是,>0 就代表是, 但是不一定是1
方法挺多的:
包括一些转换类的函数:
字符串的转换
c语言中转换 主要是两种方式 atoX 简单场景用这个足够了 strtoX 更安全,功能更强大
stdlib.h 下:
字符串长度
strlen
字符串拆分
这里有经验的老司机 肯定能猜到 这个strtok 这个函数的写法 ,在多线程环境下 肯定是不安全的
#include <stdio.h>
#include <stdlib.h>
#include <ioprint.h>
#include <ctype.h>
#include <string.h>
int main() {
char string[] = "c,1972;c++,1983;java,1995;Rust,2010;Kotlin,2011";
typedef struct {
char *name;
int year;
} Language;
const char *language_break = ";";
const char *fieled_break = ",";
int language_cap = 3;
int language_size = 0;
// 动态申请一块内存,因为不知道输入的字符串到底有多长 自然也不知道这个结构体会有几个,索性先动态申请一块
// 长度为3 size的 内存
Language *languages = malloc(sizeof(Language) * language_cap);
// 如果检索不到 则返回null 否则返回检索到的第一个字符串
char *next = strtok(string, fieled_break);
while (next) {
Language language;
language.name = next;
// strtok函数的第一个参数是要分割的字符串,第二个参数是用于指定分隔符的字符串。在第一次调用strtok时,
// 我们需要将要分割的字符串作为第一个参数传递给它。之后,我们可以在后续调用中将第一个参数设置为NULL,
// 以表示继续使用上一次调用返回的状态来分割同一字符串
next = strtok(NULL, language_break);
if (next) {
language.year = atoi(next);
// 判断是否要扩大内存
if (language_size + 1 >= language_cap) {
language_cap *= 2;
languages = realloc(languages, language_cap);
}
languages[language_size++] = language;
next = strtok(NULL, fieled_break);
}
}
PRINTFLINE("langeuages :%d",language_size);
int i;
for (i = 0; i < language_size; ++i) {
PRINTFLINE("name=%s,year=%d",languages[i].name,languages[i].year);
}
free(languages);
return 0;
}
常见的内存操作函数
mem开头的部分函数 str开头的都有
区别就是 mem开头的这几个函数 多了一个size函数, 原因很简单 mem不知道你要操作啥类型自然不知道 到底要多少size了,而str明确知道你是字符串 知道你最后是null结尾, mem开头的 并不知道这些
还记得前面一个章节说的 申请一个数组的时候 要做初始化嘛,现在memset更加方便做初始化了
int main() {
char *mem = malloc(10);
memset(mem,0,10);
PRINT_INT_ARRAY(mem ,10);
free(mem);
return 0;
}
在 C 语言中,memmove() 和 memcpy() 都用于在内存中移动一段数据。它们的功能类似,但有一些区别。
memmove() 函数可以在重叠的内存区域中移动数据,而 memcpy() 函数则不能。如果源和目的内存区域重叠,并且需要在重叠的内存区域中移动数据,就必须使用 memmove() 函数,否则可能会产生不可预测的结果。
#include <stdio.h>
#include <stdlib.h>
#include <ioprint.h>
#include <ctype.h>
#include <string.h>
int main() {
char src[] = "helloworld";
char *dest = malloc(11);
memset(dest, 0, 11);
memcpy(dest, src, 11);
puts(dest);
memmove(dest + 3, dest + 1, 4);
puts(dest);
return 0;
}
宽字符串和窄字符串
总结一下: 如果你要处理的字符串是用数字+英文,那么窄字符串就可以,如果包含中文那就宽字符串
窄字符字符串使用的是 ASCII 或 ANSI 字符集,并且每个字符只占用一个字节(8 位)。它们通常使用 char 类型表示。
宽字符字符串使用的是 Unicode 字符集,并且每个字符占用两个字节(16 位)。它们通常使用 wchar_t 类型表示。
当需要处理一些特殊字符集(如中文、日文、韩文等)时,宽字符字符串通常更加方便和实用。
// 宽字符字符串的定义
wchar_t* wstr = L"Hello, world!";
// 窄字符字符串的定义
char* str = "Hello, world!";
文件的输入输出
clion中工作区的概念
随便打开一个文件吧, 看看基本操作
#include <stdio.h>
int main() {
FILE *file = fopen("CMakeLists.txt", "r");
if (file) {
puts("open file success");
fclose(file);
} else {
perror("fopen txt");
}
return 0;
}
到这里的时候 运行大概率是要报错的
因为你编译成功以后的可执行文件的路径是在这里
而你fopen传递的是一个相对路径,所以必然会报错了,
简单的修改方法就是 修改一下clion的 工作区配置即可
让他默认在这个路径下工作即可
文件流的缓冲
#include <stdio.h>
int main() {
FILE *file = fopen("CMakeLists.txt", "r");
// 缓冲区的内存 生命周期要和文件流保持一致
char buf[8192];
if (file) {
setvbuf(file, buf, _IOLBF, 8192);
puts("open file success");
fclose(file);
} else {
perror("fopen txt");
}
return 0;
}
这里的3个宏稍微解释一下, FBF一般是读二进制文件用的,LBF 读文本文件用的, NBF 就不解释了
读取文件内容
#include <stdio.h>
int main() {
FILE *file = fopen("CMakeLists.txt", "r");
// 缓冲区的内存 生命周期要和文件流保持一致
char buf[8192];
if (file) {
puts("open file success");
setvbuf(file, buf, _IOLBF, 8192);
// 读文件 注意getc返回的是int类型 绝对不是char
int next_char = getc(file);
while (next_char != EOF) {
putchar(next_char);
next_char = getc(file);
}
fclose(file);
} else {
perror("fopen txt");
}
return 0;
}
复制文件的多种实现方式
第一种写法(效率低,但是可以复制二进制文件):
//
// Created by 吴越 on 2023/2/28.
//
#include <stdio.h>
#include "ioprint.h"
#define COPY_ILLEGAL_ARGUMENTS (-1) // 参数错误
#define COPY_SRC_OPEN_ERROR (-2) // 源文件打开失败
#define COPY_DEST_OPEN_ERRPR (-3) // 目标文件打开失败
#define COPY_SRC_READ_ERROR (-4) // 源文件读取失败
#define COPY_DEST_WRITE_ERROR (-6) // 目标文件写入错误
#define COPY_UNKNOWN_ERROR (-5) // 未知错误
#define COPY_SUCCESS (1) // 拷贝成功
int CopyFile(char const *src, char const *dest) {
if (!src || !dest) {
return COPY_ILLEGAL_ARGUMENTS;
}
FILE *src_file = fopen(src, "r");
if (!src_file) {
return COPY_SRC_OPEN_ERROR;
}
FILE *dest_file = fopen(dest, "w");
if (!dest_file) {
// 不要忘记把源文件给关闭掉
fclose(src_file);
return COPY_DEST_OPEN_ERRPR;
}
int result;
while (1) {
// 一次读一个文字 其实效率很低
int next = fgetc(src_file);
if (next == EOF) {
// 注意读取文件中 各种错误的判断
if (ferror(src_file)) {
result = COPY_SRC_READ_ERROR;
} else if (feof(src_file)) {
result = COPY_SUCCESS;
} else {
result = COPY_UNKNOWN_ERROR;
}
break;
}
if (fputc(next, dest_file) == EOF) {
// 写入文件的错误 判断就很简单
result = COPY_DEST_WRITE_ERROR;
break;
}
}
fclose(src_file);
fclose(dest_file);
return result;
}
int main() {
int result = CopyFile("file/test.jpeg","file/test2.jpeg");
int result2= CopyFile("file/1.txt","file/2.txt");
PRINTFLINE("result: %d,result2 :%d",result,result2);
}
第二种写法,虽然加了缓存,读写效率更高,但是无法复制二进制文件,只能复制文本文件 因为fgets fputs 都是按行读写, 二进制文件 里面是没有行这个概念的,这里一定要谨记哟
#define BUFFER_SIZE 512
int CopyFile2(char const *src, char const *dest) {
if (!src || !dest) {
return COPY_ILLEGAL_ARGUMENTS;
}
FILE *src_file = fopen(src, "r");
if (!src_file) {
return COPY_SRC_OPEN_ERROR;
}
FILE *dest_file = fopen(dest, "w");
if (!dest_file) {
// 不要忘记把源文件给关闭掉
fclose(src_file);
return COPY_DEST_OPEN_ERRPR;
}
int result = COPY_SUCCESS;
char buffer[BUFFER_SIZE];
char *next;
while (1) {
next = fgets(buffer, BUFFER_SIZE, src_file);
if (!next) {
if (ferror(src_file)) {
result = COPY_SRC_READ_ERROR;
} else if (feof(src_file)) {
result = COPY_SUCCESS;
} else {
result = COPY_UNKNOWN_ERROR;
}
break;
}
if (fputs(next, dest_file) == EOF) {
result = COPY_DEST_WRITE_ERROR;
break;
}
}
fclose(src_file);
fclose(dest_file);
return result;
}
终极版本(直接二进制读写)
int CopyFile3(char const *src, char const *dest) {
if (!src || !dest) {
return COPY_ILLEGAL_ARGUMENTS;
}
// windows系统一定要 加b后缀 linux和mac 无所谓
FILE *src_file = fopen(src, "rb");
if (!src_file) {
return COPY_SRC_OPEN_ERROR;
}
FILE *dest_file = fopen(dest, "wb");
if (!dest_file) {
// 不要忘记把源文件给关闭掉
fclose(src_file);
return COPY_DEST_OPEN_ERRPR;
}
int result = COPY_SUCCESS;
char buffer[BUFFER_SIZE];
while (1) {
size_t bytes_read = fread(buffer, sizeof(buffer[0]), BUFFER_SIZE, src_file);
// 这里读多少字节 就要写多少字节,写的少了那就肯定错误了
if (fwrite(buffer, sizeof(buffer[0]), bytes_read, dest_file) < bytes_read) {
result = COPY_DEST_WRITE_ERROR;
break;
}
if (bytes_read < BUFFER_SIZE) {
if (ferror(src_file)) {
result = COPY_SRC_READ_ERROR;
} else if (feof(src_file)) {
result = COPY_SUCCESS;
} else {
result = COPY_UNKNOWN_ERROR;
}
break;
}
}
fclose(src_file);
fclose(dest_file);
return result;
}
序列化与反序列化
先推荐装一个插件 ,比较容易看二进制
#include <stdio.h>
#include <stdlib.h>
#include <ioprint.h>
#include <string.h>
#include <wchar.h>
#define ERROR 0
#define OK 1
typedef struct {
int visibility;
int allow;
int rate;
int font_size;
} Settings;
int SaveSettings(Settings *settings, char *settings_file) {
FILE *file = fopen(settings_file, "wb");
if (file) {
// 二进制写文件了,
fwrite(&settings->visibility, sizeof(settings->visibility), 1, file);
fwrite(&settings->allow, sizeof(settings->allow), 1, file);
fwrite(&settings->rate, sizeof(settings->rate), 1, file);
fwrite(&settings->font_size, sizeof(settings->font_size), 1, file);
fclose(file);
PRINTFLINE("save success");
return OK;
} else {
perror("fuck Failed to save settings");
return ERROR;
}
}
void LoadSettings(Settings *settings, char *settings_file) {
FILE *file = fopen(settings_file, "r");
if (file) {
fread(&settings->visibility, sizeof(settings->visibility), 1, file);
fread(&settings->allow, sizeof(settings->allow), 1, file);
fread(&settings->rate, sizeof(settings->rate), 1, file);
fread(&settings->font_size, sizeof(settings->font_size), 1, file);
fclose(file);
} else {
perror("Failed to read settings");
settings->visibility = -1;
settings->allow = -1;
settings->rate = -1;
settings->font_size = -1;
}
}
void PrintSettings(Settings *settings) {
PRINTFLINE("visibility:%d , allow:%d, rate:%d,font_size:%d",
settings->visibility, settings->allow, settings->rate, settings->font_size);
}
#define FILE_NAME "settings.bin"
int main() {
Settings settings;
LoadSettings(&settings, FILE_NAME);
PrintSettings(&settings);
settings.visibility = 5;
settings.allow = 6;
settings.rate = 7;
settings.font_size = 8;
SaveSettings(&settings, FILE_NAME);
LoadSettings(&settings, FILE_NAME);
PrintSettings(&settings);
return 0;
}
然后看一下这个文件的16进制