JNI之C语言上篇

JNI这部分的知识一直以来都想整理一下,最近抽时间梳理了一下,大体上会分为三个部分进行介绍。首先是C语言相关知识点,然后是C++相关知识点,最后会结合前面的C和C++相对系统的学习一下JNI语法,并结合一般示例进行JNI使用介绍。

JNI即Java Native Interface的缩写,它提供了Java和C或者C++语言进行通信的API。JNI主要是Java为C或者C++通信设计的,但是并不排除结合其它语言的使用方式。一般涉及到JNI调用,Java层方法都会有一个native关键字的修饰符,这种使用native关键字修饰的方法被称为本地方法,然后借助于JNI语法生成一个.h的头文件,使用C或者C++实现本地方法,最后打包成动态链接库文件被Java调用。动态链接库在Window系统上面一般以dll作为文件扩展名,Linux系统上面是以so为文件扩展名,所以Android上面我们使用的都是so库文件。

本文主要介绍C语言的语法以及编码风格,我们知道C是一门面向过程的编程语言,而Java是面向对象的,所以编程风格上面差异性还是相当大的。

学习的内容主要包括一下几个方面:

  • 数据类型;
  • 变量与常量;
  • 预处理指令与宏;
  • 指针、函数以及数组;
  • 结构体和联合体;
  • 内存分配。

数据类型

如下是节选自谭浩强版《C程序设计第四版》的截图:

在C或者C++语言中变量所占的内存是不确定的,由具体的编译系统自行决定。如 Turbo C 2.0为每一个int分配的是2字节,而在32位编译环境的Visual Studio上面分配的是4个字节长度。另外一点需要注意,整形类型还分为无符号和有符号两类,无符号类型只能表示正整数,而有符号可以表示正负整数。浮点类型的数据没有有符号还是无符号之说,如果需要选择一种方式表示的话,浮点型数都是有符号浮点数。

在Java中也是没有有符号和无符号之分,所有的整形数据都是既可以表示正整数也可以表示负整数,而且每一种数据类型所占内存也是确定的,比如int型整数就是32位长度的,占4个字节,这跟Java语言本身的特点有关,Java具有平台无关性,因为它是运行在Java虚拟机上面的,请记住虚拟机本身是有平台之分的。

在C包含的数据类型中有一个bool类型,这个类型是C99才引入的。如果我们在C99之前要表示bool类型的,一般都是以0代替false,非零代替true。当我们在C语言中想使用bool类型时,需要引入一个<stdbool.h>的头文件,bool类型的表示形式跟C++中是一样的,0表示false,非零表示true。这里跟Java有很大的不同,在Java中boolean类型的变量跟整型变量是无法相互转换的,如果转换一定会报语法错误,无法通过编译。无论是Java还是C或者C++,char类型变量都可以转换为整形变量,这点是一致的。

请注意在C或者C++中都是没有Java中byte字节类型变量的。

sizeof操作符

在C或者C++中sizeof是一个操作符,简单来讲就是返回一个对象或者类型的所占的内存字节数。其实用于动态内存分配的malloc或者calloc函数,入参也是以字节为单位的。

sizeof有如下两种用法:

  • sizeof(type_name);//sizeof(类型);
  • sizeof object;//sizeof对象;

开发中更倾向于使用第一种方式。

int a=100;
sizeof(int)//4
sizeof(a)//4

sizeof计算对象的大小其实也是转换成计算对象类型的大小,所以同种类型的不同对象其sizeof值都是一致的。

sizeof的计算发生在编译阶段,如下是C或者C++程序的编译和连接过程。

最后要注意的是sizeof是一个操作符而不是函数,有关对结构体和指针的计算下文介绍到结构体和指针知识点时再做介绍。

变量与常量

变量

变量分为局部变量和全局变量,在函数内定义的变量是局部变量,在函数外部定义的变量是全局变量。这里局部变量和全局变量跟Java类似,Java中全局变量也叫做成员变量、字段或者属性。

一般定义变量除了变量类型、名称和值以外,还有其它属性,另外一个最常见的属性就是存储类别。上面所说的变量分为局部变量和全局变量,是从作用域(空间角度)角度划分的。但是还可以从变量的生存周期即存在时间划分,有的变量在程序的整个运行期间都是存在的,而有的变量在函数执行完成后就释放了,不存在了。

存在于程序的整个运行期间的变量称为静态存储类别,另外一种需要根据运行期间需要动态分配的变量称为自动存储类别。在C或者C++中有4中类型的存储类别,分别是自动的(auto)、静态的(static)、寄存器的(register)和外部的(extern),其中auto和register用来声明自动存储类别,static和extern则用来声明静态存储类别和函数标识符。auto是默认的存储类别,一般见到的变量前面都没有显示的使用存储类别关键字,是因为这些变量都是使用的默认存储类别。

使用auto声明的变量被称为,strong>自动变量,这里就不做介绍了,前面不带存储类别声明的变量都是自动变量。

一般情况下无论是静态存储类别还是动态存储类别的变量都是存放在内存中的。但是如果有某些变量使用比较频繁,如在某个函数中执行100000次for循环,这时候可以将变量存在寄存器中,因为寄存器的存取速度远高于内存的存取速度,这样可以提高执行效率。定义寄存器变量只需要在变量类型前面加上一个register关键字即可。目前的计算机优化的编译系统可以以将频繁使用的变量自动放入寄存器,所以register关键字在平常开发中不常使用,只需要碰到时知道这是寄存器变量即可。

使用extern修饰的变量称为外部变量,外部变量总是和全局变量联系在一起。我们知道在C语言中,无论是变量还是函数必须先声明再使用,但是如果使用了extern,就可以在方法中先使用变量,在方法的后面再声明,有关extern在函数定义中的使用方式,在下文介绍函数部分时再详细介绍。

示例代码如下:

#include <stdio.h>

void main(){
	extern int m, n;// 这里的数据类型int可以省略,直接写成extern m,n;
	printf("%d,%d\n",m,n);//10,100
	system("pause");
}

int m = 10;
int n = 100;

当然了extern的作用不止于此,如果一个源程序被拆分成了若干个源文件组成,在一个源文件中声明的外部变量需要在某个源文件中定义为全局变量。

//F01.c
int a,b;
char c;
void main(){
	...
}

//F02.c
extern int a,b;
exterb char c;
void func(int x,int y){
	...
}

static修饰的变量称之为静态变量,在C或者C++中static既可以修饰全局变量也可以修饰局部变量。不同于Java语言,在Java中static变量只能修饰全局变量,不可以用于定义局部变量,否则编译时直接报错。

由于全局变量是对整个工程可见,其他文件可以使用extern外部声明后直接使用,如果有其它文件再定义一个相同的名称的变量,那么编译器会直接报错。但是有时候想定义一个全局变量只在当前文件中使用,其它文件不可以访问,这时候全局变量前面加上一个static修饰符即可,这种变量被称为静态全局变量。静态全局变量仅在当前文件可见,可以有效降低程序模块之间的耦合,避免不同文件同名变量的冲突。

使用static修饰的局部变量称为静态局部变量,这种定义变量的方式跟Java有很大的不同。静态变量是在变量定义时就分配好了内存,当程序运行结束时才会被撤销,但是又由于变量是局部变量,所以只能在定义它的函数内使用。静态局部变量作用域仅限于定义它的函数体内,函数外面是无法使用的,生存周期却跟程序本身一样长。

如下是使用局部变量和静态布局变量程序示例。

void func(){
	int j = 0;
	++j;
	printf("%d ", j);//1 1 1 1 1
}
void localStaticFunc(){
	static int j = 0;
	++j;
	printf("%d ", j);//1 2 3 4 5
}

void main(){
	for (int i = 0; i < 5; i++){
		func();
	}
	system("pause");
}

存储类别这里可以结合Java语言的语法,在定义Java变量时,我们在变量类型前面还会使用权限修饰符private、public以及final或者static等关键字,其实这些关键字就是标记变量的作用域和生存周期的。

常量

这里常量定义跟Java类似,就是一旦定义后就不可以更改的量。在C或者C++中通俗来讲定义常量有两种形式,一种时使用const修饰符,一种是使用预处理指令#define,有时候也被称作预编译指令。

使用const修饰的常量在定义时必须进行初始化赋值,否则编译报错。

C语言:当修饰一个标识符的时候我们来说,这个标识符依然是一个变量,但是它具有常属性,不能被修改。即它定义的变量叫做常变量
C++: const修饰的标识符就是一个常量。

如果更改const修饰的值,编译时就会直接报错。

//C
void main(){
	const int i = 100;
	i = 101;//error C2166: 左值指定 const 对象

	system("pause");
}
//C++
void main(){
	const int i = 100;
	i = 101;//error C3892: “i”: 不能给常量赋值
	system("pause");
}

使用#define定义的常量一般称为符号常量,有关预处理指令下文会做详细介绍。

#define <标识符> <常量> #define PI 3.14

预处理指令与宏

预处理指令有时候也叫做预编译指令,宏也是通过预处理指令定义的。通过在上面的C或者C++程序的编译和连接过程图,我们可以知道,预处理指令通过预处理器处理是发生在程序编译之前的。所有的预处理指令都是以#开头的,由于预处理指令不是C或者C++语句,每一行预处理指令结尾处都没有以“;”结尾。

常用的预处理指令有#include、#define、#if、#else、#elif、#endif、#ifdef、#ifndef、#undef、#pragma。

#include

#include是将另一个源文件嵌入该指令所在的源文件中,文件使用尖括号<>或者双引号括起来。最常见的是使用#include导入头文件,在导入头文件时,一般以尖括号<>引入的头文件,这些头文件是系统库。如果以双引号括起来,这些头文件通常是在工程目录下。当然了如果是系统库,也可以使用双引号引入,在执行嵌入的过程中,它会首先在工程目录下寻找,如果找不到再到系统库寻找。

#include指令可以放在程序的任何位置使用,也可以引入其它扩展名的文件,如下我们引入了一个source_txt.txt的源文件,该源文件中只有一个输出语句,执行程序后仍然可以输出“hello world”。

//source_txt.txt
printf("%s\n", "hello world");

//主程序
#include <stdio.h>
void main(){
	#include "source_txt.txt"
	system("pause");
}

#define

#define用于定义符号常量或者宏,符号常量和宏的定义方式一样,不过符号常量定义的是一个常量标识符+常量值,而宏定义的是一个操作,在宏中可以引用符号常量,不过平常开发中很少明确区分什么是符号常量或者什么是宏,一般统一将#define定义的类型称为宏。

在使用#define定义后,程序会在编译之前将代码中所有的符号常量或者宏用所定义的值替换它,类似我们使用了replaceAll文本替换操作。

下面我们结合符号常量和宏使用一个简单的示例介绍一下。

#define PI 3.14
#define CIRCLE_AREA( r )( PI * (r) * (r) )

假设在程序中出现了如下语句:

area = CIRCLE_AREA(5);

预处理程序会展开为:

area = ( 3.14 * (5) * (5) );

这里请注意使用了括号,使用括号就是为了使编译器以正确的顺序行表达式的值,因为定义宏的参数r有可能是一个表达式,如(x + 2),加上括号,当展开后还是计算面积的公式。

area = ( 3.14 * (x+2) * (x+2) );

#if #ifdef 条件编译指令

条件编译指令的一般形式如下:

#if 常量表达式
	程序段1;
#else
	程序段2;
#endif

如果#if之后的表达式常数为true,则编译程序段1,否则编译程序段2。

而#ifdef和#ifndef指令分别相当于#if define()和#if ! define()。

如下是一个简单示例的JNI头文件。

#include "jni.h"

#ifndef _Included_com_sunny_demo_MainTest
#define _Included_com_sunny_demo_MainTest
#ifdef __cplusplus
extern "C" {
#endif
	/*
	 * Class:     com_sunny_demo_MainTest
	 * Method:    getStringFromC
	 * Signature: ()Ljava/lang/String;
	 */
	JNIEXPORT jstring JNICALL Java_com_sunny_demo_MainTest_getStringFromC
		(JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

由于C语言不支持extern "C"语法,为了使生成的头文件同时支持C和C++语言,这里使用的C++的宏"__cplusplus"来判断是不是C++编译器。if和endif的使用方式类似jsp中使用表达式的处理方式。

使用条件编译指令,不但可以避免重复引入的问题,还可以使程序具有良好的扩展性。在开发时通过条件编译指令定义符号选择编译哪一部分代码,这样就不必删除或者注释掉某些代码了。

#ifdef DEBUG
	//选择需要执行的程序逻辑
#endif

#pragma once也是一个常用的预处理指令,不过在一些旧版本的编译器上面可能并不支持该语法。如果在头文件最开始处加入一条该指令,就可以保证头文件只被引入一次,但是为了使我们的应用程序具有良好的平台可移植性,一般还是更倾向于使用宏定义的方式。

指针

指针就是地址,指针变量是存放地址的变量,在平常开发中或者某些书籍上面,一般直接使用指针表示,将指针用作指针变量的简称。

通常定义指针方式如下:

类型名 * 指针变量名

如:

int *pointer_1, *pointer_2;

定义指针时前面*表示变量类型是指针类型变量,指针变量名是pointer_1和pointer_2,而不是 *pointer_1和 *pointer_2。

指针的赋值类似变量的赋值,可以先定义然后初始化,也可以定义时直接初始化。

int a= 12;

int *p;
p = &a;//注意这里一定不可以使用*p
//或者
int *p= &a;

在第一种方式中一定要注意,赋值时一定是p而不可以写成*p

  • *是指针运算符或者间接访问运算符,*p代表了指针变量指向的对象。
  • &是取地址符,&a是变量a的地址。

上述例子两种使用指针的方式其实是固定的写法,也可以这么认为,只有上面两种方式,如果写成如下方式都是错误的。

//如下都是错误写法
int *p;
*p = a;

或者

int *p = a;

在定义指针变量时,指针变量中只能存放地址,不可以将一个整数赋值给指针变量。

如何访问一个指针变量指向的值呢?访问时*p和p都可以访问,但是代表的意义却不同。

printf("%d\n", *p);//输出指针变量p指向的整形变量a
printf("%#x\n",p);//输出指针变量p指向的地址,这里以16进制输出

由于*和&优先级相等而且互为逆运算,所以&*p和*&p表示的是相同的值都是输出p的地址。

printf("%d\n", *p);  // 12
printf("%d\n", p);   // 11533256
printf("%d\n", *&p); // 11533256
printf("%#x\n",p);   // 0xaffbc8

如果要输出指针的16进制表示,其实C或者C++中有专门打印指针的输出格式,%p就是指定的指针输出格式,它在32位编译系统中,会输出一个8位的数据,如果不够8位前面补0显示。

int a = 10;
int *p = &a;
printf("x=%p\n", p);//005EFBC4

当变量声明的时候,如果没有确切的地址可以赋值,可以为指针变量赋值为NULL。NULL值被称为空指针,NULL指针是在标准库中定义的一个值为0字符常量,所以如果输出NULL内存的地址,输出值是0。

在多数操作系统上面,程序是不允许访问地址为0内存,因为该内存是操作系统保留的。然而,内存地址0具有特殊的意义,它表明该指针不指定任何一个内存地址。

可以通过下面的代码检测一个指针是否是空指针。

if(p)     /* 如果 p 非空,则完成 */
if(!p)    /* 如果 p 为空,则完成 */

指针在进行赋值时跟类型时强相关的,为了程序在C和C++之间具有良好的可移植性,如果是一个变量定义的是int类型,那么指针前面的类型也应该是int类型,虽然在C中编译时不会报错,但是如果在C++会直接提示编译不通过。

int a = 10;
long *p = &a;//C++编译不过

在同一个编译系统下的指针,如果使用siteof操作符计算指针占用内存的大小,它的值都是确定的,无论是int类型还是double或者其它结构体或者数组等,比如在32位系统下,指针所占的内存大小都是4字节。

有时候我们会遇到多级指针,比如**p,p本身就是一个指针变量,**p就是一个指向指针的指针变量,如下通过一个简单的示例看一下多级指针。

void main(){
	int x = 12;
	
	int *p = &x;
	printf("x=%d\n", *p);//x=12

	int **q = &p;
	printf("x=%d\n", **q);//x=12

	int ***r = &q;
	printf("x=%d\n", ***r);//x=12
	
	system("pause");
}

由于指针是使用一个整型数值表示的地址,所以指针也可以进行数值运算,这部分在下文结合数组再做介绍。

使用const修饰的指针使用方式大致分为以下两种:

  • const int *p;// 指向整形常量的指针,它指向的值不能修改
  • int *const p;// 指向整形的常量指针,p指针只能在定义时赋值,不可以先声明再赋值,而且一旦赋值后不可再指向别的指针。
//方式一:
int i = 12;
const int *p;
p = &i;
*p = 14;//此处编译报错,提示左侧必须是可修改的值。

//方式二
int i = 12;
int *const p=&i;
int *p2 = &i;
p = p2;//此处编译报错。

在有些代码中会有结合了上述两种方式的定义类型,如const int *const p=&i,这种类型可以说是结合指针的绝对意义上的常量定义方式了,首先它指向的值不可以更改,再者指针变量p也不可以更改。

小结

本文简单介绍了一下C语言的基础知识点,包括typeof关键字、变量、常量、预处理指令、宏和指针,如C或者C++中结构体、链表、动态内存分配、数组、字符串以及文件流等知识点下文继续介绍。因为博主不是做C或者C++开发的,所以现在整理的有关内容有可能有错误疏忽之处,如有发现还请及时指出以求共同进步。

评论

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