在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);
}
void swap1(int *a, int *b) {
int tmp;
tmp = *a;
*a = *b;
*b = tmp;
printf("a:%d \n", *a);
printf("b:%d \n", *b);
}
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;
}
先总结到这,后续有别的技术点再陆续补充进来。