JNI之C语言下篇

本文继续介绍C语言剩下的知识点,通过对前面两篇文章JNI之C语言上篇JNI之C语言中篇的介绍,我们已经了解了结构体、指针、数组、函数、预处理指令宏等,接下来介绍其它几种数据类型,如联合体和枚举,还有动态内存分配以及文件流操作。

联合体和枚举

联合体

联合体也叫共用体,在定义方式上面类似结构体的定义,只是联合体在定义时使用的关键字是union。如果仅仅是声明了一个联合体,而没有定义变量,这时候编译器不会为联合体分配内存。联合体在定义变量时,可以先定义后声明,也可以在定义的时候同时声明变量,所以联合体在定义和变量声明的方式上面和结构体一样。

如下是一个联合体的定义示例。

union number
{
	int i;
	char ch;
	float f;
};

和结构体不同的是,结构体所占内存大小是各成员所占内存大小的和,而联合体所占内存大小等于成员中占用最大内存大小,而不是所有成员占用内存之和。

printf("%d \n", sizeof(union number));//4

访问联合体中的元素,跟也和结构体一样,可以使用.或者->运算符,其中联合体指针使用->运算符。由于联合体中所有成员共用一个存储单元,所以共用体中各成员的地址都是同一个地址。所以当我们对其中一个成员赋值后,其它成员也会被赋值,而且在联合体中起作用的成员是最后一次被赋值的成员。

union number num;
num.i = 97;

union number *pnum = #

printf("%d \n", num.i);//97
printf("%c \n", num.ch);//'a'
printf("%f \n", num.f);//0.000000

printf("%d \n", pnum->i);//97

printf("%#x \n", &num.i);//0x5afac8
printf("%#x \n", &num.ch);//0x5afac8
printf("%#x \n", &num.f);//0x5afac8

在上述示例中,如果输出num.ch也是以%d格式化形式输出,那么num.ch的值也是97。在使用%f格式化输出f值时,可以看到输出结果是0.000000,因为我们赋值的成员是i,i是int类型的,当以浮点型数输出时,系统会将存储单元中数按浮点形式处理,浮点型数存储跟整型数存储格式不同,浮点数存储是以指数形式存储的。一般整数使用%f格式化输出结果是0,浮点数以%d格式化输出结果也是0。

当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union)。

枚举

枚举在使用上面跟Java中类似,如下就是一个简单枚举类型的应用举例。

enum weekday{ Mon, Tue, Wed, Thu, Fri, Sat, Sun};
enum weekday a, b, c;
a = Wed;
b = Thu;
c = Fri;
printf("%d,%d,%d \n", a,b,c);//2,3,4

在枚举weekday中,Mon、Tue、Wed等标识符对应的是整数值,默认情况下是从0开始计数,这一点跟Java类似,我们在Java中枚举有一个ordinal()方法可以输出枚举标识的索引值,默认也是从0开始的。

在定义枚举类型时也可以自定义标识符对应的常数,如下使用=操作符对枚举标识符进行赋值在Java中是不允许的。

enum weekday{ Mon=5, Tue, Wed, Thu, Fri, Sat, Sun};

第一个枚举常量定义为了5,后续的常量会一次递增1。当然了枚举还允许定义离散的值。

enum weekday{ Mon=5, Tue, Wed=9, Thu, Fri=16, Sat, Sun};

在定义枚举类型时,枚举名是可以省略的,没有枚举名的枚举类型叫做匿名枚举,匿名枚举表示的是一组常量。

enum{ Mon=5, Tue, Wed=9, Thu, Fri=16, Sat, Sun};

typedef

除了可以使用C或者C++中提供的基础类型、结构体、联合体和枚举之外,还可以使用typedef声明新的类型名代替已有类型名。

jni.h中就有许多定义的新的类型名,如下是定义的部分新类型名。

typedef unsigned char   jboolean;
typedef unsigned short  jchar;
typedef short           jshort;
typedef float           jfloat;
typedef double          jdouble;

当我们在程序需要一个整数进行计数时,我们可以命名一个新类型Count,代表int类型:

typedef int Count;
Count i,j;//相当于定义了int i,j

在前面介绍struct结构体时,我们说C语言中每次使用struct结构体声明变量,变量前面必须加上struct关键字才可以,C++中可以省略struct关键字,但是如果使用typedef后,示例如下,再次使用struct结构体定义变量时,struct关键字就可以省略了。

typedef struct student
{
	int num;
	char *name;
	float score;
} student;
void main(){

	student stu;
	stu.num = 12;
	
	printf("%d \n",stu.num);//12
	system("pause");
}

在上述示例中,我们使用typedef重新定义了一个类型student,该类型就是一个结构体类型,新类型student与结构体名称student一样,于是再使用时就可以跟真实结构体定义变量一样了,这样看代码也简洁了许多。

使用typedef定义类型有利于程序良好的通用性和移植性。例如在有的编译系统上面int类型的是4个字节,而有的就变成了8个字节,当我们需要将8字节系统移植到4字节系统上面时,需要将所有的int类型变量更改为long类型。但是如果我们使用typedef定义类型的话,这时候只需要更改一个定义类型代码即可,将所有的typedef类型变量放在一个.h文件中。

typedef  int  Integer;
typedef  long  Integer;

typedef和#define有些像是之处,但是typedef定义新类型是在已有类型基础上定义的,是类型别名,而#define支持的种类比较多了,不仅可以为类型定义别名,还可以为数值定义别名。typedef是编译器处理,#define是预处理指令,由预处理器处理。

动态内存分配

在C或者C++中,全局变量和静态变量是分配中静态存储区的,非静态局部变量是分配在栈的动态存储区域的,但是除此之外还有一个区域叫做堆区域。在堆区域存储的数据有这样一些特点,这些数据不必在声明时定义,也不必在函数结束时释放,它可以在需要时随时开辟,不需要时随时释放。当然了既然这么灵活,就需要程序员自己向系统申请所需内存大小了,其实这也是动态内存分配的一个弊端,如果申请的内存没有及时释放,后续很容易造成内存泄漏。

动态内存分配相当对于静态内存有很大的优势,动态内存分配可以更有效地利用内存。假设声明了一个int类型数组,数组容量大小是100,如果业务需要,仅仅在数组中存储了10个数据,这时候我们浪费了90%的内存,但是如果需要101个容量,这时候数组有不够用了。

void *指针

我们知道void是不能定义变量的,但是void指针则不同了,void指针可以转换为任意类型的指针,void指针被称为无类型指针。void指针有点跟Java中泛型类型,可以代表任意类型。

在C语言中void指针可以自动转化为任意类型指针,不需要开发人员自己手动强制转换,但是在C++中则主要开发者强制转换为开发人员申请类型的指针,无论是在C或者C++中,任何类型的指针都可以转换为void指针,而不必强制转换。

int i = 100;
int *p = &i;

void *vp = p;
printf("%d \n",vp);

int *p01=malloc(sizeof(int));
*p01 = 101;
printf("%d \n", *p01);//101

C语言为内存分配和管理提供了如下几个函数,它们位于<stdlib.h>头文件中。

序号函数和描述
1void *calloc(unsigned int num, unsigned int num_bytes);
在内存中动态地分配num个长度为num_bytes的连续空间,并将每一个字节都初始化为 0。所以它的结果是分配了num*num_bytes个字节长度的内存空间,并且每个字节的值都是0。
2void free(void *address);
该函数释放address所指向的内存块,释放的是动态分配的内存空间。
3void *malloc(unsigned int num_bytes);
在堆区分配一块指定大小的内存空间,用来存放数据。这块内存空间在函数执行完成后不会被初始化,它们的值是未知的。
4void *realloc(void *address,unsigned int newsize);
该函数重新分配内存,把内存扩展到 newsize

calloc和malloc虽然都可以申请分配内存,但是它们使用方式上是由很大差异性的。首先从入参上面可以很明显看出差异性,malloc是分配num_bytes字节大小空间,而calloc是分配n*num_bytes字节空间。再者,malloc分配的空间不会被初始化为0,通俗讲,就是这块被分配的num_bytes内存区域不会被格式化,相当于可以使用这块区域,但该内存区域如果有数据还会保留着,除非被新数据覆盖,但是calloc分配的内存区域就不同了,该区域会被初始化为0,可以理解为n*num_bytes内存区域会被格式化了。

如下两种使用方式分配的内存大小是一样的。

int *pm = malloc(20 * sizeof(int));
	
int *pc = calloc(20,sizeof(int));

接下来我们介绍一个简单示例,建立一个动态数组,输入5个学生的成绩,并检查有无低于60分的成绩,并输出不及格的成绩。

void check(int *);

void main(){

	int *p=malloc(5 * sizeof(int));
	if (p == NULL){
		return;
	}
	for (int i = 0; i < 5; i++){
		scanf_s("%d",p+i);//65 85 50 59 95
	}
	check(p);
	free(p);//释放内存
	p=NULL;
	system("pause");
}
void check(int *p){
	for (int i = 0; i < 5; i++){
		if (p[i] < 60){
			printf("%d ", p[i]);//50 59
		}
	}
	printf("\n");
}

当申请内存后,一定要判断函数返回结果是否为NULL,如果为NULL,表示申请分配内存失败,一定要走其它流程,否则会导致访问内存错误。

假设上述示例中分配的是5个int类型的内存,如果输入的数值超过5个,极有可能造成内存访问冲突的异常,类似“读取位置0xFFFFFFF8时发生访问冲突”。

当程序退出时,操作系统会自动释放所有程序分配的内存,但是,一般情况下动态申请内存后,程序不会立即退出,所以建议在不需要的情况下,注意调用free()函数释放内存,否则会导致系统内存泄漏,极有可能导致内存溢出。另外free()函数不可以多次对同一个指针使用,一旦已经释放再次调用free()函数会引发异常。

当内存被释放后,建议将指针主动赋值为NULL,这样表示指针没有指向任何内存空间。

在C++语言中还可以使用new和delete动态分配和释放内存,当介绍C++时再做介绍。

链表

链表是一种十分常见的数据结构,在学习数据结构时,我们知道线性表有两种存储方式,一种是顺序存储,使用数组形式存放数据,Java中最常见的集合列表ArrayList就是该种存储方式;另外一种存储方式就是链式存储结构,Java中最常见的集合列表LinkedList采取的是链式存储结构。

当使用数组存储数据时,查询效率比较高,因为可以通过索引直接拿到所处位置的元素,但是当执行插入或者删除操作时,需要移动较多的元素才能实现操作,而且数组采取的是静态空间的分配方式,很容易造成内存的浪费。

链表与数组存储方式有很大不同,可以说跟数组是优劣互补,查询效率比较低,必须从头指针开始遍历。但是插入和删除效率高,只需要找到元素的前驱节点更改指针指向即可,而且链表采取的是动态内存分配方式,不会造成内存浪费,就是在存储是需要一些额外的空间存储链表上下关系。

链表中的元素在内存中的地址可以是不连续的。

静态链表

所谓静态链表就是链表在构建过程中内存不是动态分配的,所有的节点数据时直接定义的,当然了也无法用完后释放。

接下来介绍一个静态链表的示例,定义三个学生的学号成绩信息并输出。

示例代码如下:

typedef struct student
{
	int no;
	float score;
	struct student *next;
} student;

void main(){
	student a, b, c, *head, *p;
	a.no = 1001; a.score = 98;
	b.no = 1002; b.score = 90.5;
	c.no = 1003; c.score = 97.5;
	head = &a;
	a.next = &b;
	b.next = &c;
	c.next = NULL;
	p = head;
	while (p != NULL){
		printf("%d  %f\n", p->no,p->score);
		p = p->next;
	}
}

动态链表

动态链表就是在程序执行过程创建一个链表,即一个接一个地创建节点和输入节点数据,并建立前后链接关系。

其实链表有的有头节点,有的没有头结点,如下是定义的一个有头结点的链表。

typedef int DataType;
typedef struct node
{
	DataType data;
	struct student *next;
} LNode,*LinkList;

/*创建空单链表,入口参数:无;返回值:单链表的头指针,0代表创建失败,非0表成功*/
LinkList Create_LinkList(){
	LinkList H;
	H = malloc(sizeof(LNode));
	if (H){
		H->next = NULL;
	}
	return H;
}
//或者 没有返回值必须使用二级指针或者一级指针引用
void Create_LinkList(LinkList *H){
	*H = malloc(sizeof(LNode));
	if (*H){
		(*H)->next = NULL;
	}
}
/*销毁单链表, 入口参数:单链表头指针的地址, 出口参数:无*/
void Destroy_LinkList(LinkList *H){
	LinkList p, q;
	p = *H;
	while (p){
		q = p;
		p = p->next;
		free(q);
	}
	*H = NULL;
}
int  Length_LinkList(LinkList H)
{ /* 求单链表表长,入口参数:单链表头指针,出口参数:表长,-1表示单链表不存在。*/

	LinkList  p = H;   /* p指向头结点*/
	int  count = -1;  /*H带头结点所以从-1开始*/
	while (p)  /* p所指的是第 count + 1 个结点*/
	{
		p = p->next;
		count++;
	}  /*while */
	return count;
}
LinkList Locate_LinkListPos(LinkList  H, int i)
{
	LinkList p;
	int j;
	p = H;  j = 0;
	while (p && j<i)     /*查找第i个元素*/
	{
		p = p->next;
		j++;
	} /*while*/
	if (j != i || !p)
	{
		printf("参数i错或表不存在");
		return (NULL);
	}     /*第i个元素不存在*/
	return p;
}
int  Insert_LinkList(LinkList H, int i, DataType x)
{
	LinkList   p, q;
	p = Locate_LinkListPos(H, i - 1);/*找第i-1个结点地址*/
	if (!p)
	{
		printf("i有误");
		return (0);
	}
	q = (LinkList)malloc(sizeof(LNode));
	if (!q)
	{
		printf("申请空间失败");
		return (0);
	}     /*申请空间失败,不能插入*/
	q->data = x;
	q->next = p->next;     /*新结点插入在第i-1个结点的后面*/
	p->next = q;
	return 1;     /*插入成功,则返回*/
}
int  Del_LinkList(LinkList  H, int i)
{ /*删除单链表H上的第i个结点;返回参数:0不成功,1成功*/
	LinkList   p, q;
	p = Locate_LinkListPos(H, i - 1);
	/*找第i-1个结点地址,见算法2.10*/
	if (!p || !p->next)
	{
		printf("参数 i 错");
		return (0);    /*第i个结点不存在不能删除*/
	}
	q = p->next;        /*q指向第i个结点*/
	p->next = q->next;   /*从链表中删除*/
	free(q);            /*释放*s */
	return 1;
}

void main(){
	LinkList H;
	H = Create_LinkList();
	//...
	Destroy_LinkList(&H);
}

文件与字符串函数

字符串函数

在C中操作字符串时需要引入头文件<string.h>,而且字符串是利用一维字符数组实现的,系统在默认存储字符串时会在末尾加上一个'\0'作为结束符,所以当我们使用sizeof操作符计算字符串所占内存大小时,真实内存大小都是字符串本身内存字节数+1。

前面我们介绍过字符数组和字符指针是可以相互转换的,但是如果使用sizeof操作符计算内存大小,它们之间有很大的不同的,如果是字符指针赋值的字符串,计算的大小在32系统下应该都是一个确定值4,其实就是计算指针占用内存大小,因为无论什么类型指针,在同一个编译系统下所占内存大小都是相同而且确定的值。计算字符数组大小则完全不同了,跟数组大小是强相关的,是真实字节数大小+1。

如下是几个常用字符串函数。

函数 介绍
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);返回一个char指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
strstr(s1, s2);返回一个char指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。
char str[] = "hello";
char *pstr = "hello";
char des[20];

strcpy(des,str);

printf("%s \n", des);//hello
printf("%d \n", sizeof(pstr));//4
printf("%d \n", sizeof(str));//6
printf("%d \n", sizeof(des));//20
printf("%d \n", strlen(des));//5

char *ch = strchr(des, 'e');
printf("%s \n", ch);//ello

文件

在C或者C++中提供了用于操作文件的结构体FILE,在FILE结构体中存放了文件的相关信息。在使用FILE结构操作文件时,我们一般不是直接定义FILE结构体变量,而是使用一个指向FILE类型的指针变量。

先来看一个文件操作的简单示例。

#include <stdio.h>
void main(){

	FILE *fp;
	if ((fp = fopen("test.txt", "w")) != NULL){
		fprintf(fp,"this is test file!");
		fclose(fp);
	}else{
		printf("Open file or create file error !");
	}
}

函数fopen()用于新建一个文件或者打开一个已存在的文件,该函数接收两个字符指针类型的入参,第一个入参是文件名filename,第二个参数是文件的打开方式mode,函数返回一个FILE类型指针。

FILE *fopen( const char * filename, const char * mode );
模式描述
r打开一个已有的文本文件,允许读取文件。
w打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。写入内容时会从文件开头开始写入。如果文件存在,则会覆盖原文件,重新写入。
a打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。写入文件的内容中追加到文件末尾。
r+打开一个文本文件,允许读写文件。
w+打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。
a+打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。
b打开一个二进制文件。

函数fprintf()与printf()相似,都是格式化输出数据,只是一个输出到屏幕上,一个输出到文件中。

文件操作完成后要使用函数fclose()关闭文件,关闭后可以将文件缓冲中数据写入到磁盘中,如果文件没有及时关闭,可能会导致文件数据丢失。

int fclose( FILE *fp );

运行上述代码后会在当前目录下生成一个text.txt的文件,其实C语言中对文件的操作跟Java中操作文件很相似。

读写文件

int fputc( int c, FILE *fp );
int fgetc( FILE * fp );
int fputs( const char *s, FILE *fp );//读取一个字符
char *fgets( char *buf, int n, FILE *fp );//读取一个字符串

上述4个函数前2个是写入和读取字符,另外2个是写入和读取字符串。

fputc(97,fp);//写入a
fputc('\n', fp);
fputs("string",fp);
/*******文件内容*********
hello world
a
string
****************/
void main(){

	FILE *fp;
	char buff[255];
	if ((fp = fopen("test.txt", "r")) != NULL){
		fscanf(fp, "%s", buff);
		printf("1:%s\n", buff);//1:hello

		fgets(buff, 255, (FILE*)fp);
		printf("2:%s\n", buff);//2: world

		fgets(buff, 255, (FILE*)fp);//3:a
		printf("3:%s\n", buff);

		fgets(buff, 255, (FILE*)fp);//4:string
		printf("4:%s\n", buff);
		fclose(fp);
	}else{
		printf("Open file or create file error !");
	}
}

函数fgets()从fp所指向的输入流中读取n - 1个字符。它会把读取的字符串复制到缓冲区buf,并在最后追加一个‘\0’字符来终止字符串。

如果这个函数在读取最后一个字符之前就遇到一个换行符'\n'或文件的末尾EOF,则只会返回读取到的字符,包括换行符。但是如果使用fscanf()函数来从文件中读取字符串,在遇到第一个空格字符时,它会停止读取。

还有一个函数feof(),该函数用于判断文件读取时是否到达末尾了,如果到达末尾了则可以跳出循环了。

除了上述介绍的几种文件操作函数之外,还有二进制读取文件以及随机读取文件的函数,这里就不使用示例展示了,有兴趣的可以查看相关资料学习。

小结

在介绍JNI之前,C语言部分可以说是告一段了。

JNI之C语言上篇

JNI之C语言中篇

JNI之C语言下篇

通过三篇文章的介绍,涵盖了C语言在JNI中能用到的多数基础点,当然了如果把所有相关知识点都介绍到,仅凭这些文章还是远远不够的,但是如果把所介绍的所有点都已经非常熟悉了,相信在使用JNI中遇到的C语言库时,读懂大致逻辑,简单说理一下流程还是不成问题的。接下来就是介绍C++中特有的一些特性了,比如引用、类、对象、运算符重载以及模板函数等等,再后面就可以进入JNI部分了。

评论

您确定要删除吗?删除之后不可恢复