NDK开发之C语言

本文简要回顾了C语言的一些注意事项和理解细节,不再赘述C语言的所有语法

头文件

头文件作为引入文件,在编译的时候,加载到源代码,参与编译
在VS2013中可以看到,当引入头文件时候,只能看到函数的声明,其实现是在编译时候查找的
C的动态库函数不可重名,而C++可以,这是因为C++有命名空间的存在,而C没有

1
2
3
4
5
6
//引入头文件
#include <stdio.h>
void main()
{
printf("%s", "test");
}

基本数据类型

C中的基本数据类型包括:int, char, short, long, float, double
值得注意的是,在C语言中,char代表一个字节,而在Java中,char代表两个字节,在Java中与C的char对应的基本数据类型为byte,这个数据类型在C中是不存在的
基本数据类型常用的打印标识(与上述基本类型一一对应):%d, %c, %d, %ld, %f, %lf
%x – 十六进制小写
%X – 十六进制大写
%o – 八进制
%s – 字符串

sizeof关键字

在C语言中,sizeof作为关键字存在,而不是一个函数
使用sizeof()可以获得一个数据类型所占字节数的大小,例如:sizeof(char)

循环的注意事项

for循环的初始化值一般要在循环外定义,这样的原因是因为C89的规范,在很多发行版的Linux中,仍然采用的是C89的C语言规范,以下语句在很多Linux发行版无法编译通过

1
for(int i = 0; i < 10; i++){}

VS中使用scanf,gets等函数的注意事项

在VS中,像scanf,gets这样的函数被定义为不安全函数
要使用这些函数,就需要将其编译错误去掉,常用的方法有三种

  1. 添加宏定义,生效于单个文件

    1
    #define _CRT_SECURE_NO_WARNINGS
  2. 在设置中去除,生效于整个项目
    项目->属性->配置属性->C/C++ -> 预处理器 -> 预处理器定义,增加:_CRT_SECURE_NO_DEPRECATE

  3. 去除编译时的错误

    1
    #param warning(disable:4996)
  4. 使用VS自定义的函数

    1
    scanf_s("%s", buf);

以上三种方法都可以使编译通过,但都有不足之处,其最大的问题在与跨平台编译,相对来说第二种方法稍好,在Linux平台下编译的话,不用处理源代码便可以

VS中查看内存

在调试的时候可以查看内存在变化情况,这是VS最好用的功能之一
要使用内存查看,在程序调试的时候就需要存在断点
其步骤为:加断点 -> Debug模式 -> 调试 -> 窗口 -> 内存
然后按照内存地址,便可以查看到内存变化情况

dll相关

在一个exe程序中是不可以修改另一个exe程序的,要实现这一操作,就需要dll实现,这也是外挂的基本原理
生成dll的步骤为:项目 -> 属性 -> 常规 -> 配置类型:dll -> 生成解决方案
这样,一个dll就生成成功了,在dll中的函数需要用相关标识导出,这样才能被运用

1
2
3
4
5
__declspec(dllexport) void go()
{
int *p = 0x0011dd;
*p = 100;
}

指针的一些相关知识

指针存储的是内存地址

1
2
int i = 0;
int *p = &i;

无论何种类型的指针,其大小都是一样的
指针之所以要有数据类型,是因为指针取值的时候要知道其读取规则,指针得到一个内存地址,知道了存储的位置,但却不知道需要读取的长度,此时数据类型就指明了指针需要读取的长度
空指针:*p = NULL;,其指向地址为零的位置
多级指针:指针存储的是地址,而指针变量也有地址,也就是说,指针可以存储指针,这就是多级指针
指针的运算:一般只有在数组遍历的时候才有意义,这是由于数组的顺序排列导致的

数组简要说明

arrayName <=> &arrayName <=> &arrayName[0]
数组长度:sizeof(arr) / sizeof(type)
arr[i][j] <=> *(*(a + i) + j)
&arr[i][j] <=> (*(a + i) + j)
数组的[]在编译器底层做了类似于重定义的操作

1
2
3
4
5
6
int arr[5];
int i = 0;
for(; i < 5; i ++)
{
arr[i] = i;
}

而在[]符号出现以前,数组的操作是基于指针的

1
2
3
4
5
6
7
8
int arr[5];
int *p = arr;
int i = 0;
for(; p < arr + 5; p++)
{
*p = i;
i++;
}

当在栈中定义数组,数组定义过大时,会造成栈内存溢出
在Windows下,栈内存的大小为2M

函数指针

函数指针在NDK开发中有着大量运用,是重点内容
函数指针实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int add(int a, int b)
{
return a + b;
}

int minus(int a, int b)
{
return a - b;
}

void test(int(*func_p)(int a, int b), int m, int n)
{
int x = func_p(m, n);
printf("%d\n", x);
}

void main()
{
test(add, 1, 2); //执行加法运算
test(minus, 2, 1); //执行减法运算
getchar();
}

生成随机数

在C语言中,生成随机数是很重要的一个运用

1
2
srand((unsigned)time(NULL)); //time为随机数种子,如果没有这一步,生成的随机数一直固定
rand(); //此步骤生成随机数

C语言的内存划分

C语言中,对内存进行了抽象的划分,而这些划分在正是内存中是不存在的
栈区(stack),堆区(heap),全局区或静态区,字符常量区,程序代码区

栈内存自动释放,对内存手动释放

有了这些只是以后,要创建大型数组时候,就可以在堆内存中创建了
静态内存分配创建数组,数组的大小在创建的时候便已经固定:

1
int a[10]; //(此处不考虑C99才有的变长数组)

动态分配内存:

1
2
3
int *p = malloc(len * sizeof(int)); //其含义是创建一块大小为len*sizeof(int)大小的堆内存
//要操作可以使用p[i]
free(p)

在堆内存中开辟的空间,在使用完毕,必须使用free释放,否则会造成内存溢出

1
2
3
4
5
if(p != NULL)
{
free(p);
p = NULL;
}

一般使用malloc开辟的内存,需要用memset进行初始化,而使用calloc分配的内存已经初始化过了

如果分配的内存不够用,此时就需要使用realloc重新分配内存

1
int *p2 = realloc(p, sizeof(int) * (len + addlen));

重新分配内存,可能出现如下问题

  • 缩小:缩小的那一部分消失
  • 扩大
    • 若后面有足够的空间,扩展并返回原指针
    • 若后面空间不足,指到新空间,并将原有的值复制过去,清除现有数据并返回新地址
    • 如果申请失败,返回NULL,原来指针仍然有效

字符的修改问题

  • 字符数组存储在字符串中,那么可以被修改

    1
    2
    3
    4
    char str[] = {'c','h','i','n','a','\0'};
    char str[6] = {'c','h','i','n','a'};
    char str[10] = "china";
    str[0] = 's'; //可以修改的本质在于字符数组存在于栈内存中
  • 字符指针,不可修改

    1
    2
    char *p = "china";
    p[0] = 's'; //此时会报错,因为此时的字符存储在字符常量区,不可被修改
  • 字符操作的一些常用函数:strcpy, strcat, strchr, strstr, strcmp等,此处不再一一赘述,详细用法可参考C标准库-string.h以及字符串函数

结构体相关内容

结构体也是一种数据类型

1
2
3
4
5
struct Man
{
char name[64];
int age;
};

初始化结构体变量

1
2
3
4
5
6
//方法一
struct Man m1 = {"jack", 20};
//方法二
struct Man m2;
strcpy(m2.name, "jack");
m2.age = 20;

当结构体中存在指针的时候,需要使用strcpy进行赋值,否则会造成指针指向内容被释放,产生野指针的情况
结构体的其他写法:

1
2
3
4
5
struct Man
{
char *name;
int age;
}m1; //结构体变量名

1
2
3
4
5
struct Man
{
char *name;
int age;
}m1,m2 = {"jack", 20};

匿名结构体:用于控制结构体变量的个数,相当于单例

1
2
3
4
5
struct
{
char *name;
int age;
}m1;

结构体中允许嵌套结构体
结构体与指针

1
2
3
4
5
struct Man m1 = {"jack", 20};
struct Man *p = &m1;
//m1.name m1.age
//(*p).name (*p).age
//p->name p->age

结构体大小(字节对齐)
结构体的动态内存分配

1
2
3
4
5
6
7
8
9
10
struct Man *p_m = (struct Man *)malloc(sizeof(struct Man) * 10);
struct Man *p = p_m;
p->name = "jack";
p->age = 20;
p++;
p->name = "alen";
p->age = 19;
···
free(p_m);
p_m = NULL;

typedef类型取别名

1
2
3
typedef int jint;
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;

结构体取别名

1
2
3
4
5
typedef struct _Man
{
char name[64];
int age;
}Man,*ManP; //结构体别名和结构体指针别名,二者不存在必然联系,除非建立关联ManP = &Man

结构体的函数指针成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Girl
{
char *name;
int age;
void (*sayHi)(char *);
}
void sayHi(char* text){}
void main()
{
struct Girl girl;
girl.name = "Lucy";
girl.age = 18;
girl.sayHi = sayHi;
girl.sayHi("Hi");
}

联合体

不同类型的变量共同占用一段内存,任何时候只有一个成员
联合体的大小等于最大成员所占的字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
union my_value{
int x;
int y;
double z;
};

void main(){
union my_value d1;
d1.x = 90;
d1.y = 100; //最后一次赋值有效
printf("%d,%d,%lf\n", d1.x, d1.y, d1.z);
system("pause");
}

枚举

列举所有情况
限定值,保证数据的安全性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
enum day
{
Monday,
Tuesday = 2, //此时,Tuesday为2,下一个在没有指定的情况下为3
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
};

void main(){
enum day d = Monday;
printf("%d\n", d);
getchar();
}

可以任意指定位置
枚举的值必须为所列举的值

文本操作

读取文本文件

1
2
3
4
5
6
7
8
9
10
11
12
13
char *path = "C:\\a.txt"; //路径
FILE *fp = fopen(path, "r");
if(fp == NULL)
{
printf("文件打开失败");
return;
}
char buf[64] = { 0 }; //缓冲
while(fgets(buf, 64, fp))
{
printf("%s", buf);
}
fclose(fp); //关闭

写入文本文件

1
2
3
4
5
char *path = "C:\\b.txt";
FILE *fp = fopen(path, "w");
char *text = "test text";
fputs(text, fp);
fclose(fp);

逻辑:路径 -> 打开 -> 读取/写入 -> 关闭

c读写文本文件和二进制文件进体现在回车换行符

  • 写文本时,遇到”\n“,转化为”\r\n
  • 度文本时,遇到”\r\n“,转化为”\n

文件复制

1
2
3
4
5
6
7
8
9
10
11
12
char *read_path = "C:\\test.png";
char *write_path = "C:\\test_copy.png";
FILE *read_fp = fopen(read_path, "rb");
FILE *write_fp = fopen(write_path, "wb");
int buf[64] = 0;
int len = 0;
while(len = fread(buf, sizeof(int), 64, read_fp) != 0)
{
fwrite(buf, sizeof(int), len, write_fp);
}
fclose(read_fp);
fclose(write_fp);

获取文件大小

1
2
3
4
char *read_path = "C:\\test.png";
FILE *fp = fopen(read_path, "r");
fseek(fp, 0, SEEK_END); //SEEK_END文件末尾,0偏移量
long filesize = ftell(fp); //返回当前文件指针,相对于文件开头的位置

文本文件加解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//加密
void crypt(char *normal_path, char *crypt_path)
{
FILE *normal_fp = fopen(normal_path, "r");
FILE *crypt_fp = fopen(crypt_path, "w");
int ch = 0; //一次读取一个字符
while((ch = fgetc(normal_fp)) != EOF)
{
fputc(ch ^ 8, crypt_fp);
}
fclose(mormal_fp);
fclose(crypt_fp);
}
//解密
void decrypt(char *crypt_path, char *decrypt_path)
{
FILE *crypt_fp = fopen(crypt_path, "r");
FILE *decrypt_fp = fopen(decrypt_path, "w");
int ch = 0;
while((ch = fgetc(crypt_fp)) != EOF)
{
fputc(ch ^ 8, decrypt_fp);
}
fclose(crypt_fp);
fclose(decrypt_fp);
}

二进制文件加解密
和文本文件类似,不过是在读取和写入时候采用”rb”与”wb”

1
2
3
4
5
6
7
8
9
10
11
12
void crypt(char *normal_path, char *crypt_path)
{
FILE *normal_fp = fopen(normal_path, "rb");
FILE *crypt_fp = fopen(crypt_path, "wb");
int ch = 0; //一次读取一个字符
while((ch = fgetc(normal_fp)) != EOF)
{
fputc(ch ^ 8, crypt_fp);
}
fclose(mormal_fp);
fclose(crypt_fp);
}

日志输出

__VA_ARGS__可变参数

1
#define LOG(FORMAT,...) printf(##FORMAT,__VA_ARGS__);

日志区分级别

1
2
#define LOG_I(FORMAT,...) printf("INFO:"); printf(##FORMAT,__VA_ARGS__);
#define LOG_I(FORMAT,...) printf("ERRO:"); printf(##FORMAT,__VA_ARGS__);

1
2
3
4
#define LOG(LEVEL,FORMAT,...) printf("##LEVEL"); printf(##FORMAT,__VA_ARGS__);
#define LOG_I(FORMAT,...) LOG("INFO:",##FORMAT,__VA_ARGS__);
#define LOG_W(FORMAT,...) LOG("WARN:",##FORMAT,__VA_ARGS__);
#define LOG_E(FORMAT,...) LOG("ERRO:",##FORMAT,__VA_ARGS__);

Android下的LOG定义

1
2
3
#define LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,"jack",FORMAT,##__VA_ARGS__);
LOGI("%s", "test");
__android_log_print(ANDROID_LOG_INFO,"jack","%S","test");

Donate comment here