这是NDK系列的第三章,将会学习C语言的基础知识。主要分为以下几个内容:
- 函数指针与指针运算
- 静态开辟内存与动态开辟内存
- 指针对字符串的操作运用
- 结构体与结构体指针数组
- 数组与数组指针
- 加载文件 & 对文件的加密 / 解密
一、函数指针
1.1 函数指针 demo
#include <stdio.h>
void add(int num1, int num2); // 先声明
void mins(int num1, int num2) {
printf("num1 - num2 = %d\n", (num1 - num2));
}
// 操作 回调到 add mins
// void(*method)(int,int) 声明好 函数指针
// void 返回值
// (*method) 函数名
// (int,int) 两个参数
void opreate(void(*method)(int,int), int num1, int num2) {
method(num1, num2);
printf("opreate函数的 method指针是多少:%p\n", method);
}
// 7.函数指针。(回调) Java接口的回调
int main() { // 【第一种写法】
opreate(add, 10, 10);
opreate(mins, 100, 10);
// 原理是什么?
printf("main函数的 add指针是多少:%p\n", add);
printf("main函数的 mins指针是多少:%p\n", mins);
// &add和add是一样的值吗
printf("%p, %p\n", add, &add); // 004018CE, 004018CE 一样的
return 0;
}
// 再实现 使用
void add(int num1, int num2) {
printf("num1 + num2 = %d\n", (num1 + num2));
}
1.2 什么是函数指针
所谓函数指针,可以 "野蛮" 的认为将函数作为方法参数的一部分。现在对上面的demo进行分解:
- 首先定义出两个方法:add(int num1, int num2) 和 mins(int num1, int num2)。
- 定义出一个方法:
opreate(void(*method)(int,int), int num1, int num2)。
其间第一个入参就是 函数指针,在上一篇 NDK 的文章说过在 C 语言中一切都可以看作是指针,都有其自己的地址,因此这里我们传进来一个 函数 实际上传入一个 指针,因此这个参数又叫 函数指针,后面括号里的参数,就是这个函数所需要的参数。
1.3 函数指针的地址
先来看一下上面 demo 的运行结果:
num1 + num2 = 20
opreate函数的 method指针是多少:0000000000401635
num1 - num2 = 90
opreate函数的 method指针是多少:0000000000401550
main函数的 add指针是多少:0000000000401635
main函数的 mins指针是多少:0000000000401550
0000000000401635, 0000000000401635
从上面的运行结果更进一步说明传进去的就是一个 指针,只不过名字叫做 函数指针 罢了。
- 在上面的 opreate 函数中接受 add 和 mins 函数的地址,然后在 opreate 函数中进行调用。
- 从最后一句运行结果可以知道 传入的函数无论是 add 还是 &add 其地址都是一样的,其作用都是将此函数的地址传进去。
函数指针
二、静态开辟内存与动态开辟内存
2.1 静态开辟内存 demo
#include <stdio.h>
#include <unistd.h>
// 函数进栈 定义一个int arr[5]; 定义一个 int i; (静态的范畴)
// 进栈
void staticAction() {
int arr[5]; // 静态开辟 栈区 (栈成员)
for (int i = 0; i <5; ++i) {
arr[i] = i;
printf("%d, %p\n", *(arr + i), arr + i);
}
} // 函数的末尾会弹栈(隐式):执行完毕会弹栈 会释放所有的栈成员
// 2.静态开辟。
int main() {
// int 4 * 10 = 40M
// int arr[10 * 1024 * 1024]; // 10M * 4 = 40M 会栈溢出
// int arr[1 * 1024 * 1024]; 会栈溢出
int arr[(int)(0.2 * 1024 * 1024)]; // 不会栈溢出
// 栈区:占用内存大小 最大值: 大概 2M 大于2M会栈溢出 平台有关系的
// 堆区:占用内存大小 最大值: 大概80% 40M没有任何问题,基本上不用担心 堆区很大的
// 大概80%: Windows系统 给我们的编译器给予的空间 的 百分之百八十
while (9) {
sleep(100);
staticAction(); // 调用开辟20
}
return (0);
}
对于以上的 demo 我们需要了解以下几个知识点:
- 静态 指的是一般的定义一个变量或指针 / 数组等,如:int arr[5]; / int i;
- 当执行一个函数开始时,从函数的起始就是 进栈,当函数执行到最后一个 大括号时也就是函数的末尾时,函数会进行 弹栈,会将函数中所有的栈成员进行释放操作,注意这是一个隐式操作。
- 栈区的大小:占用内存大小 最大值: 大概 2M 大于2M会栈溢出 平台有关系的
- 堆区的大小:占用内存大小 最大值: 大概80% 40M没有任何问题,基本上不用担心 堆区很大的
大概80%: Windows系统 给我们的编译器给予的空间 的 百分之百八十
以下是对上面demo的画图解释。
2.2 动态开辟内存
所需场景:
静态开辟的内存空间大小,是不能修改的,如果不需要动态修改空间大小,当然使用栈区,尽量使用 静态开辟的,如果实在是需要动态改变,才使用动态开辟。
demo:
#include <stdio.h>
#include <stdlib.h>
int main() {
int num;
printf("请输入数的个数:");
// 获取用户输入的值
scanf("%d", &num);
// 动态开辟 用户输入的值 空间的大小 【堆区】
int * arr = malloc(sizeof(int) * num);
// int arr2 [] == int * arr 一样的了
int print_num;
// 循环接收
for (int i = 0; i < num; ++i) {
printf("请输入第%d个的值:", i);
// 获取用户输入的值
scanf("%d", &print_num);
arr[i] = print_num;
printf("每个元素的值:%d, 每个元素的地址:%p\n", *(arr + i), arr + i);
}
// for 循环打印
for (int i = 0; i < num; ++i) {
printf("输出元素结果是:%d\n", arr[i]); // arr[i] 隐士 等价与 * (arr + i)
}
return 0;
}
上面看到通过 malloc(size_t _Size) API 就可以在内存上动态开辟出一块空间。其中所需参数是开辟内存的大小。
2.3 动态开辟内存-重新开辟(扩容)
刚才上面的 demo 已经通过 malloc(size_t _Size) API 在内存动态开辟一块内存了,那么此时如果还想再继续再动态开辟内存的话该如何操作呢?且看一下这个demo:
#include <stdio.h>
#include <stdlib.h>
// 动态开辟之realloc
int mainT0() {
int num;
printf("请输入个数");
// 获取用户输入的值
scanf("%d", &num);
// 5个值
int * arr = (int *) malloc(sizeof(int) * num);
for (int i = 0; i < num; ++i) {
arr[i] = (i + 10001); // arr[i]的内部隐士 == *(arr+i)
}
printf("开辟的内存指针: %p\n", arr);
// ================================= 在堆区开辟新的空间 加长空间大小
// C的岗位
// C工程师的面试题: realloc 为什么一定要传入 arr指针,为什么要传总大小
// 新增
int new_num;
printf("请输入新增加的个数");
scanf("%d", &new_num);
// 原来的大小4 + 新增加的大小4 = 总大小 8
// void *realloc (void *前面开辟的指针, size_t总大小);
int * new_arr = (int *) realloc(arr, sizeof(int) * (num + new_num));
if (new_arr) { // new_arr != NULL 我才进if 【非0即true】
int j = num; // 4开始
for (; j < (num + new_num); j++) { // 5 6 7 8
arr[j] = (j + 10001);
}
printf("新 开辟的内存指针: %p\n", new_arr);
// 后 打印 内容
for (int i = 0; i < (num + new_num); ++i) {
printf("新 元素的值:%d, 元素的地址:%p\n",
*(arr + i),
(arr + i)
);
}
}
// 我已经释放
/*free(new_arr);
new_arr = NULL;*/
// 1000行代码
// 。。。
// 重复释放/重复free VS会奔溃, CLion会优化(发现不奔溃) [错误的写法]
/*free(new_arr);
new_arr = NULL;*/
// 必须释放【规则】
/*if (arr) {
free(arr); // 如果不赋值给NULL,就是悬空指针了
arr = NULL;
}*/
/*if (new_arr) {
free(new_arr); // 如果不赋值给NULL,就是悬空指针了
new_arr = NULL;
}*/
if (new_arr) { // new_arr != NULL 进去if, 重新开辟的堆空间是成功的
free(new_arr);
new_arr = NULL;
arr = NULL; // 他还在指向那块空间,为了不出现悬空指针,指向NULL的空间
} else { // 重新开辟的堆空间是失败的
free(arr);
arr = NULL;
}
return 0;
}
注意点:
- 在进行释放操作时应先判断对象是否存在,避免发生重复释放的问题
- 释放完对象之后还应该同时释放指针对应的空间,避免出现悬空指针
- 当使用 realloc API 时,可能会出现创建新指针失败的情况,当出现创建失败的情况时(资源不够时,程序较多时),则应释放原来的对象以及指针
- realloc(void *_Memory,size_t _NewSize); 这个API中两个入参,第一个传进的是第一次动态创建的指针,第二个传入的是两个指针的总大小。传入第一个指针是为了方便拷贝(两个内存进行拼接时,需要先拷贝的第一个指针)传入总大小是为了判断内存是否足够创建新的内存。
三、字符串的操作
3.1 字符串两种形式
在 C 语言可以通过两种方式来声明字符串:数组 和 指针。
#include <stdio.h>
// 字符串
int mainT2() {
char str[] = {'D', 'e', 'r', 'r', 'y', '\0'};
str[2] = 'z'; // 这里能修改?
printf("第一种方式:%s\n", str); // printf 必须遇到 \0 才结束
char * str2 = "Derry"; // 隐式 Derry+\0
str2[2] = 'z'; // 会崩溃,为什么,不允许访问,为什么
printf("第二种方式:%s\n", str);
return 0;
}
上面 demo 中通过两种方式创建了两个字符串,其中第一种方式可以修改不同位置的元素值,而第二种方式通过相同的方式则无法修改元素的值,会发生崩溃,这是为什么呢?
同时可以看到上面通过第一个方式创建时会看到在数组的末尾会拼接一个 '\0',这是因为当通过 printf 方法打印通过数组创建字符串时需要添加一个结束标志位,否则此时 printf 方法不知道字符串已经结束。
我们首先要有一个概念:在程序运行,执行main 函数时就发生了进栈的操作,同时程序中还存在着全局区/静态区域的地方,里面存在一些字符串的常量等数据。
第一种方式修改:通过数组的方式创建字符串,其实是将静态区中 Derry 这个字符串拷贝到 main 函数栈中,其操作的是 Derry 这个字符的副本,因此不会发生错误。
第二种方式修改:通过指针的方式创建字符串,则是直接将 str2 新建出来的指针的地址指向静态区中的 Derry 字符串,此时如果通过 str2 进行修改,则修改的是静态区中的数据,而 静态区中的数据是不允许修改的。
以下是上面两种方式的图示:
3.2 字符串转换,字符串的比较
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
// 字符串转换 =======================
char * num = "1"; // 字符串
num = "12.68";
// 【int】
int result = atoi(num);
if (result) { // 非0即ture 不是0进入if, 0就是转换失败了
printf("恭喜你转换成功:%d\n", result);
} else {
printf("转换失败!\n");
}
// 【double】
double resultD = atof(num);
printf("恭喜你转换成功:%lf\n", resultD);
// 字符串的比较 ======================
char * str1 = "Derry";
char * str2 = "derry";
// int resultC = strcmp(str1, str2); // strcmp = 区分大小写
int resultC = strcmpi(str1, str2); // strcmpi = 不区分大小写
if (!resultC) { // 0代表是相等的, 非0代表是不相等的
printf("相等");
} else {
printf("不相等");
}
return 0;
}
执行结果:
这个demo主要演示字符串转换,字符串的比较在C语言中相关API 的使用:
- atoi 将字符串转换成 int
- double - 将字符串转换成 double
- strcmp 区分大小写的比较字符串
- strcmpi 不区分大小写的比较字符串
3.3 字符串两种形式字符串查找,包含,拼接
// 字符串查找,包含,拼接。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char * text = "name is Derry";
char * subtext = "D";
char * pop = strstr(text, subtext);
// 怎么去 字符串查找
if (pop) { // 非NULL,就进入if,就查找到了
printf("查找到了,pop的值是:%s\n", pop);
} else {
printf("没有查找到,subtext的值是:%s\n", subtext);
}
// 包含了D吗
if (pop) {
printf("包含了\n");
} else {
printf("没有包含\n");
}
// printf("pop地址%p, text地址:%p,\n", pop, text);
// 求取位置? 数组是一块连续的内存空间,没有断层,所以可以-
int index = pop - text; // pop="Derry" - text"name is Derry"
printf("%s第一次出现的位置是:%d\n", subtext, index); // 我的D在第8个位置
// 指针是可以:++ -- += -=
// 拼接 ========================
char destination[25]; // 容器 25的大小 已经写死了
char * blank = "--到--", *CPP="C++", *Java= "Java";
strcpy(destination, CPP); // 先Copy到数组里面去
strcat(destination, blank); // 然后再拼接
strcat(destination, Java); // 然后再拼接
printf("拼接后的结果:%s\n", destination); // C++--到--Java
return 0;
}
执行结果为:
其中 strstr 这个字符串的作用是查找字符串中第一个出现的指定字符串。两个参数意义是:(资源字符串,查找字符串)
之前就说过,在C语言中数组与指针其实是相等的,由于数组是一块连续的内存空间,没有断层,所以可以进行 ++ -- += -=这些操作。
因此可以通过以上这些操作结合 strstr 这个 API 获取到第一个出现指定字符串的下标。
通过 strcat 这个字符串进行字符串的拼接操作。
3.4 大小写转换(手写API)
在这个demo中主要学习
- C语言中指针用法的思想
- 大小写转换的API
// 大小写转换(手写API)
#include <stdio.h>
#include <ctype.h>
// 指针的理解
void lower(char * dest, char * name) {
char * temp = name; // 临时指针,你只能操作,临时指针,不能破坏name指针
while (*temp) {
*dest = tolower(*temp);
temp ++; // 挪动指针位置 ++
dest ++; // 挪动指针位置 ++ 目的是为了 挪动一个存储一个 挪动一个存储一个 ...
}
// printf '\0'
*dest = '\0'; // 避免printf打印系统值
printf("不能破坏 name:%s\n", name); // temp的好处就是,不会破坏name
}
// 全部变成小写 derry
int mainT6() {
char * name = "DerrY";
// 先定义结果
char dest[20];
lower(dest, name);
printf("小写转换后的结构是:%s\n", dest);
return 0;
}
执行结果为:
注意点:
- 将需要操作的数组传进去后,为了避免操作原始数据导致原始数据发生变化,通常需要定义一个临时变量来存储需要改变的数据。
- 通过指针 ++、-- 等操作移动指针位置,获取每一个字符串。
- 通过 tolower API 将对应的字符串修改为小写。
四、结构体与结构体指针数组
C语言中的结构体就类似于 Java 中类(class)的概念一样
4.1 结构体定义与使用
- 第一种写法
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <malloc.h>
struct Dog {
// 成员
char name[10]; // copy进去
int age;
char sex;
};
int main() {
struct Dog dog; // 这样写完,成员是没有任何初始化的,成员默认值 是系统值(name:?@, age:3133440, sex:€)
printf("name:%s, age:%d, sex:%c \n", dog.name, dog.age, dog.sex);
// 赋值操作
// dog.name = "旺财";
strcpy(dog.name, "旺财");
dog.age = 3;
dog.sex = 'G';
printf("name:%s, age:%d, sex:%c \n", dog.name, dog.age, dog.sex);
return 0;
}
运行结果:
在C语言中定义结构体用 struct 关键字。
- 当没对结构体里的数据进行初始化时,可以看到打印出来的是一些脏数据。
- 当用数组定义字符串时只能用 copy (拷贝)的方式对该对象进行赋值。
- 结构体第二种写法
struct Person {
// 成员
char * name; // 字符指针 = "赋值"
int age;
char sex;
} ppp = {"Derry", 33, 'M'},
ppp2,
ppp3,
pppp4,
pppp5
// ...
;
int main() {
// Person == ppp == struct Person ppp;
printf("name:%s, age:%d, sex:%c \n", ppp.name, ppp.age, ppp.sex);
// 赋值
// strcpy(pppp5.name, "Derry5"); // Copy不进去
pppp5.name = "DerryO";
pppp5.age = 4;
pppp5.sex = 'M';
printf("name:%s, age:%d, sex:%c \n", pppp5.name, pppp5.age, pppp5.sex);
return 0;
}
运行结果:
- 当用指针定义字符串时可以用 = 的方式对该对象进行赋值。
- 可以直接在结构体的末尾创捷对象,直接对其初始化。
- 结构体第三种写法
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <malloc.h>
struct Study {
char * studyContent; // 学习的内容
};
struct Student {
char name[10];
int age;
char sex;
// Study study; // VS的写法
struct Study study; // Clion工具的写法
struct Wan {
char * wanContent; // 玩的内容
} wan;
};
int main() {
struct Student student =
{"李元霸", 88, 'm' ,
{"学习C"},
{"王者农药"}
};
printf("name:%s, age:%d, sex:%c,study:%s, wan:%s \n",
student.name, student.age, student.sex, student.study.studyContent, student.wan.wanContent);
return 0;
}
运行结果:
- 在这个demo中展示了结构体嵌套的情况,被嵌套的结构体需要先于嵌套结构体先被定义出来。
- 也可以直接在结构体里直接创建一个新的结构体,同时对其命名。
4.2 结构体指针 与 动态内存开辟
- 结构体指针
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <malloc.h>
struct Cat {
char name[10];
int age;
};
int main() { // 栈
// 结构体
struct Cat cat = {"小花猫", 2};
// 结构体 指针 -> 调用一级指针成员
// VS的写法:Cat * catp = &cat;
struct Cat *catp = &cat;
catp->age = 3;
strcpy(catp->name, "小花猫2");
printf("name:%s, age:%d \n", catp->name, catp->age);
return 0;
}
运行结果
- 声明结构体指针跟声明其他指针一样也是用 *,然后用 & 取出该对象的地址。
- 结构体指针取出对应的对象用 ->
- 结构体指针的动态内存开辟
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
struct Cat2 {
char name[10];
int age;
};
int main() { // 堆
// VS的写法:Cat2 * cat = (Cat2 *) malloc(sizeof(Cat2));
struct Cat2 *cat = malloc(sizeof(struct Cat2));
strcpy(cat->name, "金色猫");
cat->age = 5;
printf("name:%s, age:%d \n", cat->name, cat->age);
// 堆区的必须释放
free(cat);
cat = NULL;
return 0;
}
- 对结构体动态开辟内存也是用 malloc 关键字
- 当使用malloc 关键字在堆区动态开辟内存时千万记得在结束时需要释放指针,及其内存。
- 结构体的数组
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct Cat3 {
char name[10];
int age;
};
int main() {
// 栈区 静态范畴
struct Cat3 cat [10] = {
{"小黄", 1},
{"小白", 2},
{"小黑", 3},
{},
{},
{},
{},
{},
{},
{},
};
// VS的写法
// cat[9] = {"小黑9", 9},
// ClION的写法
struct Cat3 cat9 = {"小黑9", 9};
// cat[9] = cat9;
*(cat + 9) = cat9;
printf("name:%s, age:%d \n", cat9.name, cat9.age);
// 堆区 动态范畴 ==============================
struct Cat3 * cat2 = malloc(sizeof(struct Cat3) * 10);
// 【1元素地址的操作】给他赋值,请问是赋值,那个元素 (默认指向首元素地址)
strcpy(cat2->name, "小花猫000");
cat2->age = 9;
printf("name:%s, age:%d \n", cat2->name, cat2->age);
// 【8元素地址的操作】 给第八个元素赋值
cat2 += 7;
strcpy(cat2->name, "小花猫888");
cat2->age = 88;
printf("name:%s, age:%d \n", cat2->name, cat2->age);
free(cat2);
cat2 = NULL;
return 0;
}
运行结果:
- 之前说过在C语言中数组与指针可以认为是一个概念,因此默认的结构体指针指向的就是结构体数组的第一条数据。
- 移动结构体指针,就相当于移动结构体数组,需要记住的是下标都是从 0 开始。
- 同样地,在堆区开辟内存的话最后要记得释放内存。
4.3 结构体与结构体指针-别名
大家看到上面demo中有很多注释说在 VS 编译器中代码是怎么写的,在 Clion 中代码是怎么写的。由于编译器的差异导致了不同平台有不同的代码要求,如果要根据不同平台去维护多套代码这显然成本太大了,因此有了别名来解决这个问题:
#include <stdio.h>
#include <stdlib.h>
struct Workder_ {
char name[10];
int age;
char sex;
};
// VS的写法:typedef Workder_
typedef struct Workder_ Workder_; // 给结构体取别名
typedef Workder_ * Workder; // 给结构体指针取别名
// 源码是这样写的
// 给结构体AV 取了一个别名等于AV
typedef struct {
char name[10];
int age;
char sex;
} AV;
int main() {
// 以前 Clion工具 必须加上 struct VS又不用加 代码差异化大
// struct Workder_ workder1 = malloc(sizeof(struct Workder_));
// 现在 (兼容代码的写法,保持一致)
Workder_* workder2 = malloc(sizeof(Workder_));
// VS CLion 他们都是一样的写法
Workder workder = malloc(sizeof(Workder_));
AV av = {"VideoInfo", 54, 'M'}; // 结构体 VS Clion xxx工具 兼容写法
AV * avp = malloc(sizeof(AV)); // 结构体指针
free(workder);
workder = NULL;
free(workder2);
workder2 = NULL;
return 0;
}
- 通过 typedef 关键字对结构体和结构体指针设置别名,之后操作这个结构体就可以兼容所有平台的语法,既可以使用 VS 的语法,也可以使用 Clion 的语法。
- 可以直接在声明结构体时加上 typedef 以及别名,这样新建的结构体就是一个兼容所有平台的结构体。
4.4 枚举
C语言中的枚举类似于Java中的枚举,同样的为了解决上面说到的不同平台不同语法的问题,也采用 typedef 关键字 对枚举取别名,从而兼容所有平台的语法:
#include <stdio.h>
// 枚举 int 类型的
enum CommentType {
TEXT = 10,
TEXT_IMAGE,
IMAGE
};
typedef enum CommentType CommentType;
// 作业:处理好 差异化代码
int main() {
// Clion工具的写法如下:
// enum CommentType commentType = TEXT;
// enum CommentType commentType1 = TEXT_IMAGE;
// enum CommentType commentType2 = IMAGE;
// VS工具的写法如下:
CommentType commentType = TEXT;
CommentType commentType1 = TEXT_IMAGE;
CommentType commentType2 = IMAGE;
printf("%d, %d, %d \n", commentType, commentType1, commentType2);
return 0;
}
运行结果: