NDK开发(一) - C语言基础

在Android OS上开发应用程序,Google提供了两种开发包:SDK(Software Development Kit )和NDK(Native Develop Kit),前者用于Java/Kotlin开发,后者用于C/C++开发,另外还有一套JNI规范,提供Java调用C/C++能力。

这个篇章总结下NDK开发,主要内容包括:C/C++语言回顾、JNI语法、NDK开发实践。

那么开篇先来梳理下C语言。

一、指针

1.1 指针基础

我们对一块内存空间进行操作总共含有两种方式:

  • 直接通过变量名的方式对这块内存空间进行操作。(直接访问) int i = 10;。
  • 通过获取内存空间的地址对这块内存空间进行操作。(间接访问)指针通过地址来操作。

指针核心操作:

int a = 1;
int *p1, *p2;//指针声明
p1 = &a; //&取地址符,p1指向a的地址。
p2 = 1;
printf("p1:%d",*p1);//* 取值
printf("p2:%d",p2);

至于二级指针**p则是指向指针的指针,N级以此类推。

1.2 指针数组与数组指针

指针数组: int *p[n]; 本质是一个数组,存放指针的数组。

int *p[3] = {1, 2, 3};
int i;
for (i = 0; i < 3; ++i) {
   printf("%d", p[i]);
}

数组指针:int (*p)[n]; 本质是一个指针,一个指向数组首地址的指针。

int MAX = 3;
int var[] = {10, 100, 200};
int i, *ptr[MAX];
for (i = 0; i < MAX; i++) {
    ptr[i] = &var[i]; /* 赋值为整数的地址 */
}
for (i = 0; i < MAX; i++) {
    printf("Value of var[%d] = %d\n", i, *ptr[i]);
}
1.3 指针与函数参数

函数传参:就是形参复制一份实参的值。
函数内部修改外部变量的值,需要一级指针交换值;
函数内部修改外部指针变量的值,需要二级指针交换地址;

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

int main() {
    //实参
   int i = 1;
   int j = 2;
   swap(i, j);
}
swap函数内存关系
void swap1(int *a, int *b) {
    int tmp;
   tmp = *a;
   *a = *b;
   *b = tmp;
   printf("a:%d \n", *a);
   printf("b:%d \n", *b);
}
swap1函数内存关系
1.4 指针函数与函数指针

指针函数:int* fun(int x,int y); 本质是一个函数,其返回值为指针。
函数指针:int (*fun)(int x,int y); 本质是一个指针,其指向一个函数。

函数名是一个函数的首地址,所以我们可以将函数赋值给对应类型的函数指针。

int plus(int a, int b) {
    return a + b;
}

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

int main() {
    int (*p_plus)(int a, int b);//声明函数指针
   p_plus = plus;//函数指针指向函数
   int result = p_plus(1, 2);
   printf("result:%d \n", result);
   int a = 3;
   int b = 5;
   void (*p_swap)(void *a, void *b);//这里void类型通用性更好
   p_swap = (void (*)(void *, void *)) swap;
   p_swap(&a, &b);
   printf("a:%d,b:%d", a, b);
}

二、内存分配

静态存储区:编译阶段确定的,静态、常量、全局变量。

  • 已初始化数据区(data):存放程序中已初始化的全局变量、静态变量和常量。
  • 未初始化数据区(bss):存放程序中未初始化的全局变量,用零初始化。
  • 代码区(code):存放程序执行代码的一块内存区域。

动态存储区:程序执行阶段确定的。

  • 栈(stack):编译器自动分配和释放内存,在函数结束后系统会自动释放,不需要人为的进行任何操作。2M左右
  • 堆(heap):程序动态分配内存,malloc建立,系统不会在函数体执行结束后自动释放,需要用户手动释放通过free函数。

1)开辟内存空间

函数 描述
void *malloc(size_t size); 在内存的动态存储区中分配一块长度为size字节的连续区域。参数size为需要的内存空间的长度,返回该区域的地址。
void *calloc(size_t nmemb, size_t size); calloc()与malloc()相似,参数size为申请地址的单位元素长度,nmemb为参数个数。calloc会将所分配的空间中的每一位都初始化为零。
void *realloc(void *ptr, size_t size); realloc()是给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址空间。

1)malloc使用:

#include <stdio.h>
#include <stdlib.h>

int main() {
  // 动态分配10个整数的空间
  int *data = (int *)malloc(10 * sizeof(int));
  
  // 检查是否分配成功
  if(data == NULL) {
    printf("Error: memory allocation failed.\n");
    exit(1);
  }

  // 使用数据
  for(int i = 0; i < 10; i++) {
    data[i] = i * i;
  }

  // 打印数据
  for(int i = 0; i < 10; i++) {
    printf("data[%d] = %d\n", i, data[i]); 
  }

  // 释放内存 
  free(data);

  return 0;
}

2)realloc使用

realloc 函数用于重新分配之前通过 malloc、calloc 或 realloc 分配的内存块的大小。它可以用于调整动态分配的内存块的大小,使其更大或更小。

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

int main() {
    char *buffer = (char *)malloc(10 * sizeof(char)); // 初始分配内存大小为 10 个字符
    if (buffer == NULL) {
        return 1;
    }

    strcpy(buffer, "Hello"); // 将字符串 "Hello" 复制到 buffer 中

    printf("初始字符串: %s\n", buffer);

    // 使用 realloc 调整 buffer 的大小为 20 个字符
    buffer = (char *)realloc(buffer, 20 * sizeof(char));
    if (buffer == NULL) {
        return 1;
    }

    strcat(buffer, " World!"); // 在字符串后追加 " World!"

    printf("调整大小后的字符串: %s\n", buffer);

    free(buffer); // 释放动态分配的内存

    return 0;
}

2)void *memset(void *s,int c,size_t n) 将已开辟内存空间 s 的首 n 个字节的值设为值 c。用于内存空间初始化。
memset常见用法:初始化数组、初始化字符串

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

int main() {
  int nums[10];

  // 用memset将数组全部初始化为0 
  memset(nums, 0, sizeof(nums));

  // 打印数组确认所有元素为0
  for(int i = 0; i < 10; i++) {
    printf("%d ", nums[i]);
  }

  char str[100];

  // 用memset将字符串初始化为'@'
  memset(str, '@', sizeof(str));

  // 打印字符串看到全部为@
  printf("\n%s", str);

  return 0;
}

三、字符串

C语言没有string,字符串使用主要有3种方式:

  • char *arr1 = "Hello”; // 值不可修改的常量形式
  • char arr2[] = "Hello”; //char arr2[6] = {'H', 'e', 'l', 'l', 'o', '\0'}; 值可修改的栈内存方式
  • char * arr3 = (char *)malloc(2 * sizeof(char)); // 值可修改的堆内存方式

字符串修改方式:strcpy(str, newValue)

动态内存分配:

char *arr3 = malloc(6 * sizeof(char));
strcpy(arr3, "Hello”); //注意不是arr3 = “hello”,这样arr3会重新指向”Hello”的内存地址,造成malloc申请的内存浪费。
arr3[1] = 'o';//替换第二个字符
printf("%s", arr3);

free(arr3);

这里重点理解下char arr1[] 和char *arr2 这两种写法的区别:

  • char arr1[] = "Hello”;// 常量池分配”Hello”,字符数组arr1初始化并为它单独开辟内存空间,将”Hello” strcpy到对应内存地址上。
  • char *arr2 = "Hello”;//常量池分配”Hello”, 指针arr2指向”Hello”对应的内存地址。

举一个例子:

void modify(char *str) {
    str[strlen(str) - 1] = 'z';
}

int main() {
   char *str = "hello";
   modify(str);
   printf("%s \n", str);
}

直接报错,原因是:modify函数直接在常量的内存地址上修改内容, 常量是不允许被修改的。

解决方案:
方案1:使用可写内存的字符数组

char str[] = "hello";
strcpy(str, ”world“);

方案2:将字符串复制到新的可写内存

char *arr3 = "Hello";
char buf[100];
strcpy(buf, arr3); 
strcpy(buf, "World");

方案2:使用malloc分配内存

char *str = (char *)malloc(6*sizeof (char));
strcpy(str,"hello");
printf("1 %s \n", str);
strcpy(str,"world");
printf("2 %s \n", str);

注:
每次malloc都是独立分配内存,每块内存空间都需要单独free释放。优化内存分配大小可以使用realloc。

常用字符串函数:

函数名 描述
strcpy(s1, s2); 复制字符串 s2 到字符串 s1。
strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。
strlen(s1); 返回字符串 s1 的长度。
strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1<s2 则返回小于 0;如果 s1>s2 则返回大于 0。
strchr(s1, ch); 返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
strstr(s1, s2); 返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。

四、结构体、共用体、枚举

4.1 结构体

C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。

结构体声明和初始化

struct Student {
    char name[20];
     int age;
    int(*Msg)(char *,int)
} stu = {"susan", 20};//stu 全局变量, 可以直接初始化

struct {
    char name[20];
   int age;
} stu1, stu2;//stu1,stu2 全局匿名结构体。  作用:锁定结构体变量的数量

int main() {
    //两种初始化方式:
   // 1 申明和初始化合并写
   struct Student student = {"stan", 30};//声明(分配内存) + 初始化
   printf("name:%s", student.name);

   // 2 申明和初始化分开写
   struct Student student1;
   strcpy(student1.name, "zhangsan”);//字符串赋值要使用strcpy
   printf("name:%s", student1.name);
}

结构体数组:

struct Student stus[2] = {{"jack", 30},{"rose", 27}};
printf("student1 name:%s",stus[0].name);

结构体指针:

struct Student *stu = &student; //数组则直接指向首地址:struct Student *stu1 =stus;

使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符:
stud->name;

结构体函数指针:
结构体中不能有函数,但是可以有函数指针属性。

struct Man {
    char* (*Msg)(char *)
};

char *message(char *speak) {
    char *msg = malloc(sizeof(char)*6);
   strcpy(msg,"hello");
   strcat(msg, speak);
   return msg;
}

int main() {
    struct Man man;
   man.Msg = message;
   printf("%s", man.Msg("world"));
}

结构体中添加结构体指针成员变量

以实现链表为例:

struct Node {
    int data;
   struct Node *next;
};

int enqueNode(struct Node *head, int data) {
    struct Node *node = malloc(sizeof(struct Node));
   if (node == NULL) {
        return 0;
   }

   node->data = data;
   node->next = NULL;
   struct Node *p = head;
   while (p->next != NULL) {
        p = p->next;
   }
    p->next = node;
   return 1;
}

int main() {
    int i = 0;
   int num = 10;
   struct Node *root = malloc(sizeof(struct Node));
   root->data = 0;
   root->next = NULL;
   for (i = 0; i < num; i++) {
        enqueNode(root, i + 1);
   }

    while (root->next != NULL) {
        printf("%d \n", root->data);
       root = root->next;
   }
}
4.2 共用体

共用体是一种特殊的数据类型,允许您在相同的内存位置存储不同的数据类型。您可以定义一个带有多成员的共用体,但是任何时候只能有一个成员带有值。

union MyUnion {
    int a;
    char b;
};
  • a、b 使用的是相同内存地址空间。
  • 整个union内存开销取决于内部那个最大内存开销的成员变量,a 4byte, b1byte,那么MyUnion整体内存开销是4byte。
  • 任何时候只能有一个属性带有值,该值为最后赋值的属性的值。
4.3 枚举
enum Week {
    MON, TUES, WED, THURS, FRI, SAT, SUN
};
`
默认第一个元素值为0,后面依次为1,2 ...,以此类推。也可以指定第一个元素的值N,后续也依次为N+1,N+2… 如下所示:

enum Week2 {
   MON=2, TUES, WED, THURS, FRI, SAT, SUN
};

int main() {
    enum Week week;
   scanf("%d", &week);
   switch(week){
        case MON: puts("Monday"); break;
       case TUES: puts("Tuesday"); break;
       case WED: puts("Wednesday"); break;
       case THURS: puts("Thursday"); break;
       case FRI: puts("Friday"); break;
       case SAT: puts("Saturday"); break;
       case SUN: puts("Sunday"); break;
       default: puts("Error!");
   }
}
4.4 typedef

为类型取别名

typedef char *string; //为char* 取别名
typedef struct Node { //为Node结构体取别名
    int data;
   struct Node *left;
   struct Node *right;
} BinaryTreeNode;

int main() {
   string str = malloc(sizeof(string));
   strcpy(str, "abc");
   BinaryTreeNode *node = (BinaryTreeNode *) malloc(sizeof(BinaryTreeNode));
}

五、文件操作

常用函数介绍:

函数 描述 备注
FILE fopen(const char *filename, const char * mode) 创建或打开一个文件。 mode参数:
字符:
r: 只读打开已存在文件。
w: 只写打开或建立一个文件。
a: 追加写文件,不存在则新建。
字节:
rb: 只读打开二进制文件。
wb: 只写打开或建立一个二进制文件。
ab: 追加写一个二进制文件,不存在则新建。
int fclose( FILE *stream ) 关闭文件。
int fputc( int c, FILE *fp ); 把参数 c 的字符值写入到 fp 所指向的输出流中。 如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF。
int fgetc( FILE * fp ); 从 fp 所指向的输入文件中读取一个字符。 返回值是读取的字符,如果发生错误则返回 EOF。
int fputs( const char *s, FILE *fp ); 把字符串 s 写入到 fp 所指向的输出流中。 如果写入成功,它会返回一个非负值,如果发生错误,则会返回 EOF。
char *fgets( char *buf, int n, FILE *fp ); 从 fp 所指向的输入流中读取 n - 1 个字符。
size_t fread( void *buffer, size_t size, size_t count,FILE *stream ) 二进制文件读。 读取有内容,长度不为0
size_t fwrite( const void *buffer, size_t size, size_t count,FILE *stream ) 二进制文件写。
long ftell( FILE *stream ) 返回给定流stream的当前文件位置。
int fseek( FILE *stream, long offset, int whence ) 设置文件查找位置。 whence参数:
SEEK_SET 文件的开头
SEEK_CUR 文件指针的当前位置
SEEK_END 文件的末尾

文本文件读写:
读文件

char *path = “xxx";
FILE *fp = fopen(path,"r");
int size = 50;
char buff[size];
while (fgets(buff,size,fp)){
    printf("%s",buff);
}
fclose(fp);

写文件:

char *path = "/Users/hongtao/Desktop/NewFile-1.txt";
FILE *fp = fopen(path, "a+");//追加写
if (fp != NULL) {
    char *txt = "hello";
   fputs(txt, fp);
   fclose(fp);
}

二进制文件读写:

char *read_path = "xxx";
char *write_path = "xxx";
FILE *read_fp = fopen(read_path, "rb");
FILE *write_fp = fopen(write_path, "wb");
if(read_fp != NULL && write_fp != NULL){
    int size = 50;
   char buff[size];
   int len = 0;
   while ((len = fread(buff, sizeof(char), size, read_fp)) != 0) {
        fwrite(buff, sizeof(char), len, write_fp);
   }
    fclose(read_fp);
     fclose(write_fp);
}

获取文件大小:

char *read_path = "xxx";
FILE *fp = fopen(read_path, "r");
if (fp == NULL) {
   return 0;
}

fseek(fp, 0, SEEK_END);//文件末尾偏移offset=0的位置
long filesize = ftell(fp);

六、错误处理

属性与函数 描述
errno 错误码,定义在 errno.h 头文件中。
void perror(const char *msg) 基于errno的当前值,在标准错上产生一条出错信息,然后返回。输出内容:msg : 报错信息。
char * strerror(int errnum) 返回errnum(errno)对应的错误信息。

使用举例:

int main() {
    FILE *pf;
   int errnum;
   pf = fopen("unexist.txt", "rb");
   if (pf == NULL) {
        errnum = errno;
       //使用 stderr 文件流来输出所有的错误
       fprintf(stderr, "错误号: %d\n", errno);
       perror("通过 perror 输出错误");
       fprintf(stderr, "打开文件错误: %s\n", strerror(errnum));
   } else {
        fclose(pf);
   }
    return 0;
}

七、预处理命令

常用的预处理命令:

指令 描述
#define 宏定义
#include 引入一个源代码文件
#undef 取消已定义的宏
#ifdef 如果宏已经定义,则返回真
#ifndef 如果宏没有定义,则返回真
#if 如果给定条件为真,则编译下面代码
#else else
#elif else if
#endif 结束一个 #if……#else 条件编译块

使用:
define

#define 宏定义。它与typedef区别是:它不仅可以为类型定义别名,也能为数值定义别名。
#define TRUE 1
#define FALSE 0

参数化宏:
#define MAX(x,y) ((x) > (y) ? (x) : (y))
int main(void){
  printf("Max between 20 and 10 is %d\n", MAX(10, 20)); 
  return 0;
}

include 引用库

#include <stdio.h> 系统的用< >
#include “selfdefine.h"自定义的库用” “

条件判断

#ifdef DEBUG
  /* Your debugging statements here */
#endif

八、头文件

C语言中的头文件是一种扩展名为.h的文件,它们扮演着非常重要的角色,主要用于声明函数、宏定义、类型定义(如结构体、联合体、枚举)和全局变量等,以便在多个源文件(.c文件)之间共享。使用头文件可以使程序的结构更加清晰、模块化,便于管理和维护。

一般在头文件中写什么内容:

  • 函数原型:声明在其他文件中定义的函数。
  • 宏定义:定义常量值或者宏函数,用于在多个文件之间共享。
  • 类型定义:如struct、union、enum的定义,以及typedef用于定义新的类型名称。
  • 全局变量声明:使用extern关键字声明全局变量,其定义在相应的.c文件中。
  • 条件编译指令:防止头文件内容被多次包含。
    举例:
    假设我们有一个简单的数学运算模块,我们可以将其分为头文件(math_utils.h)和源文件(math_utils.c)
    math_utils.h:
// 宏定义
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// 函数原型声明
int add(int a, int b);
int subtract(int a, int b);

#endif // MATH_UTILS_H

math_utils.c:

#include "math_utils.h"

// 函数定义
int add(int a, int b) {
    return a + b;
}

int subtract(int a, int b) {
    return a - b;
}

main.c:

#include <stdio.h>
#include "math_utils.h"

int main() {
    int sum = add(1, 2);
    int difference = subtract(5, 3);
    
    printf("Sum: %d\n", sum);
    printf("Difference: %d\n", difference);
    
    return 0;
}

先总结到这,后续有别的技术点再陆续补充进来。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容