JNI之C语言中篇

在上一篇文章中简单介绍了C语言的基础知识点,如sizeof操作符、变量、常量、预处理指令、宏以及指针。本文继续上一篇文章的内容,着重介绍如下三个方面知识点:函数、数组以及结构体。

函数

有一点这里简单说明一下,有时候我们说函数,有时候我们又说方法,一般定义在类中的称为方法,反之都称为函数,所以在C语言中一般称之为函数。

在C或者C++中,函数定义类似变量,在类型前面还有类别修饰符。函数前面的类别修饰符有static和extern,默认是extern。使用static修饰的函数被称为内部函数或者静态函数,内部函数的作用域仅限于函数所处文件。通常情况下都是把只能在本文件使用的函数和全局变量放在文件开头,前面加上static修饰符。

main()函数称为主函数,如果在主函数在前,被调用的函数在后面,这时候被调用函数需要在主函数前面提前声明一下函数原型。如果被调用函数在主函数之前的话,不函数声明也可以直接调用。函数声明可以放在调用函数外部,也可以放在调用函数内部,在进行函数声明时,形参的名称可以省略,只需要指明形参类型即可,如下两种方式都可以。

int max(int a,int b,int c);
//或者
int max(int,int,int);

如果有多个函数都在同一个文件,一般主函数放前,被调用函数定义在后面,将被调用函数的声明放在主函数前面。

接下来介绍一个经典函数的使用示例,相信初学C或者C++时都遇到过下面交换两个参数值的函数。

void swap(int x, int y){
	int temp;
	temp = x;
	x = y;
	y = temp;
}
void main(){
	int x = 10, y = 20;
	swap(x, y);
	printf("x=%d,y=%d\n", x, y);//x=10,y=20
	system("pause");
}

上面例子中使用的方式很显然无法实现x和y值的交换,因为函数swap()一旦执行完成后形参x和y这两个局部变量就销毁了。

然后我们将swap()函数的两个形参都更改为了两个指针类型变量,代码如下,执行后x和y两个值顺利交换了。

void swap(int *p1, int *p2){
	int temp;
	temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}
void main(){
	int x = 10, y = 20;
	swap(&x, &y);
	printf("x=%d,y=%d\n", x, y);//x=20,y=10
	system("pause");
}

其实在C++中还可以使用另外一种方式,按照传引用的方式,传引用方式只在C++才可以使用,C是不支持如下类型的操作的。

void swap(int &x, int &y){
	int temp;
	temp = x;
	x = y;
	y = temp;
}
void main(){
	int x = 10, y = 20;
	swap(x, y);
	printf("x=%d,y=%d\n", x, y);//x=20,y=10
	system("pause");
}

如下是错误的示例。

//示例1
void swap(int *x, int *y){
	int *temp;
	*temp = *x;
	*x = *y;
	*y = *temp;
}

//示例2
void swap(int *x, int *y){
	int *temp;
	temp = x;
	x = y;
	y = temp;
}

示例1编译时会直接抛出异常,提示使用了未初始化的局部变量temp。指针变量在使用时必须先赋值后才可以使用,这里由于没有给temp变量进行赋值,所以它就指向了一个无法确定的值,那么temp执行的内存地址也是无法确定,有可能*temp指向的值是有用的系统的变量值,这样就有可能破坏系统的正常运行。

一般指针赋值有两种形式,使用取地址符&将一个变量赋值给指针变量,如p=&a,另外一种形式是将一个可以表示地址的变量直接赋值给指针变量,如p=pointer_1或者p=ary,其中ary是一个数组类型的变量。

示例2虽然可以正运行,但是输出结果不会交换入参两个指针变量所指向的值。由于形参和实参是单向值传递的,指针变量也遵从这一原则,在执行函数调用时我们不能改变实参指针变量的本身的值的,但是可以改变实参指针变量所指向的变量的值。

可以通过如下方式验证示例2。

void main(){
	int x = 10, y = 20;
	int *p1 = &x, *p2 = &y;
	printf("x=%p,y=%p\n", p1, p2);//x=006FFCA8,y=006FFC9C
	swap(p1,p2);
	printf("x=%d,y=%d\n", x, y);//x=10,y=20
	printf("x=%p,y=%p\n", p1, p2);//x=006FFCA8,y=006FFC9C
	
	system("pause");
}

指针函数和函数指针

指针函数和函数指针是两种不同的定义类型,指针函数在操作字符串或者数组时比较常见,但是使用函数指针则类似Java中多态的应用方式。

我们知道在C语言中是没有像Java中String这种类型的,所以一般使用字符数组或者字符指针代替字符串使用。

char *s = "hello world";
printf("%s\n", s);

char ary[] = { 'h', 'e', 'l', 'l', 'o','\0'};
printf("%s\n", ary);

先看一个指针函数的使用示例。

char *getString(){
	return "hello world";
}
void main(){
	char *s = getString();
	printf("%s\n", s);//hello world
}

指针函数定义的一般类型如下:

char *fp(char);

这里声明了一个函数fp,该函数的返回值指向字符变量的指针。

但是如果将上述函数定义如下,此时就成为了一个函数指针。

char (*fp)(char);

这种定义方式就是声明了一个函数指针fp,它指向函数返回值是int类型,入参是char类型的变量。之所以将*fp用括号()括起来,是因为括号()运算符优先级比指针运算符*的高,不加括号就变成了上面的指针函数了。

函数指针的主要用途就是把函数的地址作为参数传递给了其它函数,如下是一个函数指针使用的典型示例。

int add(int x, int y){
	return x + y;
}
int minus(int x, int y){
	return x - y;
}

void calc(int x, int y, int(*fp)(int, int)){
	int r = fp(x,y);
	printf("%d\n", r);
}

void main(){
	
	calc(10, 20, add);//30
	calc(10, 20, minus);//-10

}

在上述示例中设计了加减运算,我们还可以继续扩展,添加乘除运算,而calc函数不需要变动即可输出两个数的计算结果,当然了数据的合法性校验可以在每个执行函数中处理。

数组

数组的定义和Java中类似,但是也有所不同。

一维数组

数组初始化可以在定义时就初始化。

int array[5] = { 1, 2, 3, 4, 5 };

对全部数组元素初始化时可以不指定数组长度。

int array[] = { 1, 2, 3, 4, 5 };

当然了也可以不初始化全部数组元素,没有初始化的元素会自动初始化为0,但是如果初始值元素的数目大于了数组长度,编译时会报错。

跟Java不同的地方在于如果在定义时不初始化数组元素,在数组声明时必须指明数组的长度,而在Java中可以不必指定数组长度,因为在Java中只声明数组,这时候并不会为数组分配内存,只有new的时候才会分配内存。

int ary[];//报错
int ary[10];//正确

在C或者C++中是没有提供数组长度的API的,但是我们可以通过如下方式计算数组长度。

int len = sizeof(array) / sizeof(int);
for (int i = 0; i < len; i++){
	printf("%d  \n", array[i]);
}

如果我们直接访问数组变量名称,其实就是访问数组的地址,又由于指针也是地址,所以数组变量名称和指针变量可以互换使用,另外数组元素第一个元素的地址其实也是数组的地址 ,因为数组中的所有元素在内存中是一片连续的内存区域。

int array[] = { 1, 2, 3, 4, 5 };
int *p = array;
printf("%#x \n", p);//0x4ffd18
printf("%#x \n", array);//0x4ffd18
printf("%#x \n", &array[0]);//0x4ffd18

正是由于数组元素的内存连续性特性,所以在平常开发中经常会看到指针结合数组进行数值运算。指针的数值运算与基本数据类型数值运算不同,如果我们把指针p进行++或者--,其实是将指针指向了下一个元素或者上一个元素的地址,而不是单纯的将指针变量的值加1或者减1,而是加减了sizeof(int)字节。当然了如果数组类型是long类型的,指针变量值增加或减少的就是sizeof(long)个字节了。

printf("%d \n", *p);//1
p++;
printf("%d \n", *p);//2
printf("%#x \n", p);//0x4ffd1c

由于数组名和指针是可以相互转换的,所以数组名也可以当做指针参加数值运算,当一个指针指向了数组之后,指针和数组一样也可以使用下标访问数组。

int array[] = { 1, 2, 3, 4, 5};
int *p = array;
	
printf("%d\n",array[0]);//1
printf("%d\n",*array);//1
printf("%d\n", *(array+3));//4

p[4] = 100;
printf("%d\n", p[0]);//1
printf("%d\n", p[2]);//3
printf("%d\n", array[4]);//100

一般字符数组用于定义字符串,我们知道指针也可以用于定义字符串,而且指针和数组是可以互换使用的,但是需要注意,用指针指向的字符串,只可以使用指针下标访问字符串中的字符,但是不可更改某个字符的值。如果是使用一维字符数组定义的字符串,我们可以使用下标的方式更改某个字符的值。

char array[] = "Hello";
char *p = "Hello";

array[1] = 's';
//p[1] = 's';//运行直接报错
printf("%s\n", array);//Hsllo
printf("%c\n", array[1]);//s

printf("%s\n", p);//Hello
printf("%c\n", p[1]);//e

但是如果定义的字符数组时一个字符类型的数组,而不是一个字符串,这时候使用指针加下标是可以更改字符值的。

由于字符串的存储是利用一维数组实现的,字符数组的长度为字符串长度加1,因为系统在存储字符串常量时都会默认加上一个'\0',所以使用sizeof操作符计算字符串长度时都是字符串本身长度加1个字节。在平常使用字符数组定义一个字符串时,一般也要加上一个'\0'作为结束符,否则有可能输出的字符串会后缀一部分乱码。

在输出字符数组中字符串时遇到'\0'会停止输出。

char array[] = {'H','e','l','l','0','\0'};
char  *p = array;
p[1] = 's';
printf("%s\n", array);//Hsllo
printf("%c\n", array[1]);//s

printf("%s\n", p);//Hsllo
printf("%c\n", p[1]);//s

char ary[] = { "Hello" };
printf("%d\n", sizeof(ary));//6

char strAry[] = { 'H', 'e', 'l','\0', 'l', '0', '\0' };
printf("%s\n", strAry);//Hel

二维数组

C或者C++中也有多维数组,这里我们就借助于二维数组的介绍,因为平常开发中更高维度的数组也不常用到。

二维数组可以在声明时直接初始化,也可以先声明后初始化。

如下两种方式的二维数组直接初始化都可以。

//方式1
int array[2][3] = { { 3, 6, 9 }, { 8, 5, 2 } };
//方式2
int ary[2][3] = { 3, 6, 9, 8, 5, 2 };

printf("%d\n", sizeof(array));//24
printf("%d\n", array[0][2]);//9
printf("%d\n", array[1][2]);//2

printf("%d\n", ary[0][2]);//9
printf("%d\n", ary[1][2]);//2

在Java中不支持方式2的定义,方式2是将所有的数组元素写在了一个花括号内,按照数组元素在内存中的顺序对各元素进行的赋值。但是一般情况下不建议使用方式2进行赋值,元素少还可以很直观看出来,但是一旦元素比较多,容易遗漏,还不容易检查。

在二维数声明时可以省略第1维长度,但是第2维长度不可以省略。

一维数组可以定义字符串,二维数组则可以定义一个字符串数组。

char week[7][11] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};

在使用二维数组定义字符串数组时,需要按照最长的元素的长度声明数组,这样很容易造成空间浪费。我们知道指针也可以定义字符串,这样就可以结合指针和数组定义一个指针数组,每一个字符串元素的长度都可以是任意长度,不会造成空间浪费了。

char *week[7] = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};

结构体

结构体定义

在C语言中没有类的说法,但是结构体为我们提供了类似于类的操作,结构体的定义形式如下:

struct 结构体名
{
	成员列表
} 变量名列表;

在上述定义中,变量名列表可以省略,这样相当于仅仅定义了一个结构体,后面在想使用的位置再进行结构体变量的声明。当然了结构体名也可以省略,这种类型的结构体类似Java中匿名内部类的定义方式,后续不能在使用结构体定义其它变量了,但是该方式用的不多。

在结构体中定义的变量名称可以与程序中变量名称相同,因为结构体中的变量必须结合结构体变量才可以使用。

struct student
{
	int num;
	char name[20];
	float score;
};	

上述方式定义结构体之后并不会在内存中分配内存空间,而仅仅是定义了一种新的数据类型,因为在编译时对类型时不分配空间的,只对变量分配空间。当使用student结构体声明结构体变量时才会分配内存,注意结构体类型和结构体变量的差异性。

如果要访问结构体中的变量,我们可以使用.操作符。在C语言中如果要定义结构体变量,前面的struct不可以省略,但是在C++中可以省略不写。如果想要省略struct关键字也不是不可以实现,当下文介绍typedef使用时我们再做说明。

//方式1
struct student stu;
stu.num = 10;
stu.name = "admin";
stu.score = 98;

printf("%s,%d\n",stu.name,stu.num);

对结构体中变量进行赋值有多种形式,上述就是方式之一,先定义一个结构体变量,然后使用.运算符对结构体中的变量进行初始化赋值。

可以在声明结构体时同时对变量进行初始化,这种初始化方式类似数组变量初始化方式。

//方式2
struct student
{
	int num;
	char *name;
	float score;
} stu01 = {10,"hello",98.5},stu02;

也可以在定义结构体变量时进行初始化。

//方式3
struct student stu03 = {101,"name01",97.5};

如果结构体变量已经声明但是并未初始化,后续再进行赋值时只能使用方式1,这时候不可以使用大括号的方式进行赋值了。

接下来我们看一下结构体所占内存大小。

struct student
{
	char x;
	int num;
	char sex;
};
printf("%d \n", sizeof(struct student));//12

在32位机器编译环境中,char在内存中占1个字节,int占4个字节,那么student总大小应该是6字节,但是上面输出确实12字节,所占内存大了一倍。

但是如果将student中数据成员顺序调整一下。

struct student
{
	int num;
	char sex;
	char x;
};
printf("%d \n", sizeof(struct student));//8

现在输出又变成了8个字节了,再次实验一下,将所有的数据类型更改为int类型的,这时候使用sizeof操作符计算值变成了12个字节了。

struct结构体所占内存大小等于结构体中所有变量所占内存大小的总和,因为计算机内存空间划分是以字节划分的,为了更有效率的操作内存,系统在对struct分配内存时采取的是字节对齐的方式。所以平常开发中如果涉及到struct的定义,一定要对各种类型变量内存分配有所2了解,尽可能减少中间的填补空间。

内存分配字节对齐是默认场景,如果想要取消或者使用自定义字节对齐,可以使用预编译指令#pragma pack()设置。

结构体指针

结构体也有指针类型变量,而且在平常开发中使用广泛,比如链表就是借助结构体指针构建的。

示例这里还是借助于上面已经定义的结构体。

struct student
{
	int num;
	char *name;
	float score;
}
struct student stu;
stu.num = 10;

struct student *pt;
pt = &stu;

这时候如果我们想使用pt访问student结构体中变量,就必须使用(*pt).num才可以了,因为.运算符优先级高于*,所以pt必须使用括号括起来。但是在C或者C++中为了直观和方便,我们可以使用pt->num代替(*pt).num。

printf("%d \n", stu.num);//10
printf("%d \n", (*pt).num);//10
printf("%d \n", pt->num);//10

结构体是可以嵌套使用的,链表就是使用的结构体嵌套方式定义的。

struct student
{
	int num;
	char sex;
	double score;
	struct student next;//没有使用指针,运行会抛出异常
};

上述方式定义的结构体在分配内存时编译器会不知道分配多少内存,因为结构体student引用了自己,相当于形成了一个递归调用的死循环,所以这种方式是非法的。

但是如果我们将next定义成结构体指针,这时候就可以成功了,因为指针在同一个编译环境下所占的内存时确定的,链表就是采用的该方式定义的。

struct student
{
	int num;
	char sex;
	double score;
	struct student *next;
};

小结

本文介绍了C语言中函数、数组以及结构体的相关知识,重点需要关注的是函数的声明与定义,结构体和结构体指针的使用,数组、指针与字符串的使用,有关联合体、动态内存分配、链表和文件流操作将会在下篇继续介绍。

评论(1)

  • ClaytonSwire

    Hy there, What we have here is , an enchantingsacrifice Right-minded click on the link under to prepare https://drive.google.com/file/d/1bKEbqyfR2Vqmbn5wC6trO11e2FH-XNKt/preview

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