Android JNI 编程 - C语言基础知识 (二)

1,841 阅读8分钟

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实际的存储

image.png

简单处理下这个问题

#pragma pack(2)

再试一下即可

联合体

union Operator{
  int a;
  double d;
};
union Operator op={.a=4,.d=1.0};
PRINTFLINE("%d %f",op.a,op.d);

看下执行结果

image.png

这个联合体别的语言应该是没有的,有个特点是 他所占用的内存就是他内部字段最大的内存大小,

比如这里占用的内存大小就是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

方法挺多的:

image.png

包括一些转换类的函数:

image.png

字符串的转换

c语言中转换 主要是两种方式 atoX 简单场景用这个足够了 strtoX 更安全,功能更强大

stdlib.h 下:

image.png

字符串长度

strlen

image.png

字符串拆分

这里有经验的老司机 肯定能猜到 这个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开头的都有 image.png

区别就是 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中工作区的概念

image.png

随便打开一个文件吧, 看看基本操作

#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;
}

到这里的时候 运行大概率是要报错的

image.png

因为你编译成功以后的可执行文件的路径是在这里

image.png

而你fopen传递的是一个相对路径,所以必然会报错了,

简单的修改方法就是 修改一下clion的 工作区配置即可

image.png

让他默认在这个路径下工作即可

文件流的缓冲

#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;
}

image.png

这里的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;
}

序列化与反序列化

先推荐装一个插件 ,比较容易看二进制

image.png

#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进制

image.png