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是重新申请的地址空间。

malloc使用:

int *p = (int *) malloc(sizeof(int) * 1024 * 10);
if (p == NULL) {
    printf("malloc fail");
}

free(p);

2)void *memset(void *s,int c,size_t n) 将已开辟内存空间 s 的首 n 个字节的值设为值 c。用于内存空间初始化。

int size = sizeof(char) * 4;
char *p = malloc(size);
memset(p, 'a', size);

三、字符串

C语言没有string,字符串两种表示方式:
char arr1[] = "Hello”; //char arr1[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
char *arr2 = "Hello”;//

动态内存分配:

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";

方案2:char *str = (char *) malloc(6 * sizeof(char));
strcpy(str, "hello");

常用字符串函数:

函数名 描述
strcpy(s1, s2); 复制字符串 s2 到字符串 s1。
strcat(s1, s2); 连接字符串 s2 到字符串 s1 的末尾。
strlen(s1); 返回字符串 s1 的长度。
strcmp(s1, s2); 如果 s1 和 s2 是相同的,则返回 0;如果 s1s2 则返回大于 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() {
    //两种初始化方式:
   //局部变量
   struct Student student = {"stan", 30};//声明(分配内存) + 初始化
   printf("name:%s", student.name);
   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  系统的用< >
#include “selfdefine.h"自定义的库用” “

条件判断

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

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

你可能感兴趣的:(NDK开发(一) - C语言基础)