最近一段时间在攻克Android
NDK开发。虽然大学的时候主要的学习是放在C/C++上的,但是自从大学毕业之后,就把所有学到的知识都还给老师了,所以,趁着这个机会,将C语言和NDK开发好好的总结一下,学习一下。


自己在网上也看了很多博客,感觉大神们写的都是比较难以理解,特别是像现在这种工作了一天的状态,想要再看这些东西的时候,都感觉花眼了。所以,自己希望能够将基础知识理顺。


首先先来看一张图,这张图相信很多做Android开发的人肯定非常熟悉,但是熟悉并不代表理解。再次看到这张图的时候,我发现之前在一些外包公司做的时候,大部分都是活跃在应用层次,深入理解却是少之又少,就算偶尔有框架的内容,也是别人封装好的。


在这种图里我们会发现,现在市面上一些非常厉害的App都是要跟C/C++进行交互的,比如抖音,微博,微信等。因为这些应用软件都会跟一些音频,视频,图片处理等内容挂钩。所以,如果想要成为高级或者终极程序员,C/C++这个坎是迈不过去的。

为什么是C语言?


看你这么好看,那就告诉你。这是我工作了三年之后的自我体会。相信很多小伙伴们都有看源码的经历,那么源码里很多东西,都会牵扯到底层的内容,所以,对于我来说,再看源码的时候,很多是看不懂的。再加上很多地方C语言是作为支撑语言的,也就是我们常说的技术支持,如果C语言不好,可能会导致我们很多东西都没有办法从核心上去优化。所以,千言万语汇成一句话,C语言非学不可。

C语言基础

变量

对于任何一门语言来说,我们都是会先从基础开始学习的,那么这个基础学习又大部分是从变量开始。在C语言中,变量是用来表示所占的存储空间大小的。如下所示
#include<stdio.h> int main(){ int i = 90; printf("i所占的存储空间是:%d\n",sizeof(i));
printf("i的值是:%d\n",i); return 0; }
在代码里我们使用了#include "studio.h"这样的代码。这就是我们所说的头文件,在C语言中,我们需要引入各种各样的头文件,头文件都是以.h
结尾的,包含一些函数声明这样的内容。我们也可以说是头文件,而以.c结尾的,我们就说是源文件,函数的实现会在源文件中

在命令行中执行下面命令
gcc hellowordl.c ./a.out
运行结果是:

我们会发现into所占的就是4个字节,那么我们可以将剩下的补全


使用printf输出内容的时候,需要将数据的类型也要跟上,例如int类型就是/d ;char类型就是/c.
/* C 语言的基本数据类型 , 输出占位符 int - %d short - %d long - %ld float - %f double - %lf
char -%c 字符串 - %s 八进制 - %o 十六进制 - %x */
指针


指针就是为了内存操作而产生的。学过java语言,我们知道,java中有垃圾回收机制,是固定时间内帮我们清除内存,优化内存,但是在C语言中,计算机并不会帮我们去执行,所以所有的关于内存操作的部分都要我们自己去执行。
例如:
#include<stdio.h> int main(){ int num = 100; int *numPoint = # return 0; }
指针存储的是变量的内存地址,而且只能存储内存地址,就算我们给他赋值了一个值,比如一个整数,他还是会变成一个地址


运行结果:


指针也是一个变量,建议以后再写指针的时候使用int* p = &num的方式。p本身就是一个变量,用来存储num的内存地址,而当我们使用的时候,p
就代表的是内存地址,而如果是* p表示的是p对象所代表的内存地址的值,是地址指向的值。

就像上面所说,指针也是变量,同样可以进行变量的计算
#include<stdio.h> int main(){ int arr[] = {89,80,13,45,68}; printf(
"输出数组arr的地址是:%#x\n",&arr); printf("另一种方法获取arr的地址:%#x\n",arr); printf(
"输出第一个元素的地址:%#x\n",&arr[0]); int* p = &arr; for(int i=0;i<5;i++){ printf(
"数组的内容是:%d\n",arr[i]); } printf("\n"); printf("以指针运算的方式输出数组数据"); for(int i=0;i<5
;i++){printf("新的方式下数组内容是:%d\n",*p); p++; } }
运行结果是:

取地址的结果都是一样的,输出的方式也相同的。
其实我们可以这样理解,数组第一个对象的地址值就是数组的地址值。

通过上面p++实现循环获取数据,这里我们先认为数组是一块连续的内存空间

函数


关于函数就不具体的介绍了,这里我们说一个知识点,就是如果形参是一个数据,那么再传入之前和在函数中,我们得到的地址值是不一样的,因为在函数中,我们会为形参再次创建一个对象,如下
#include<stdio.h> void changeNum(int i){ printf("函数中i的地址值是:%#x\n",&i); i = 300
; }int main(){ int i = 100; printf("传入函数之前i的地址值是:%#x\n",&i); changeNum(i);
printf("修改之后的值是:%d\n",i); return 0; }
运行结果是

传入函数之前的值与在函数中的值是不一样的,而且虽然在函数中我们对数据进行了修改,但是并没有改变在main方法中的数据。下面我们传递的是一个地址的例子
#include<stdio.h> void changeNum(int i){ printf("函数中i的地址值是:%#x\n",&i); i = 300
; } void changeNum2(int* p){ printf("函数中变量的地址只是:%#x\n",p); *p = 200; } int
main(){int i = 100; printf("传入函数之前i的地址值是:%#x\n",&i); changeNum2(&i); printf(
"修改之后的值是:%d\n",i); return 0; }

我们会发现,地址值是一样的,数值也发生了改变

二级指针

所谓的二级指针,我们可以理解为是指针的指针,也就是说一个存储空间中存储的是不是数值,而是地址,而这块存储空间的地址,就是我们所说的二级地址。
#include<stdio.h> int main(){ int i = 10; int* p = &i; int** p1 = &p; int * p2
=100; printf("指针作为普通变量:%d\n",p2); printf("i的地址:%#x\n",&i); printf("p的地址:%#x\n"
,&p);printf("通过p1获取p的地址:%#x\n",p1); printf("通过p1获取i的地址:%#x\n",*p1); printf(
"通过p1获取i的值:%#x\n",**p1); //修改i的值 ** p1 = 100; printf("修改之后的i的值:%d\n",i); printf(
"通过p获取修改之后i的值:%d\n",*p); printf("通过p1获取修改之后的i的值:%d\n",**p1); return 0; }


其实一句话概括就是:多级指针指向的就是上级指针的地址

函数指针

当我们创建一个函数之后,就会像变量一样,为函数分配一个内存地址
#include <stdio.h> void message(){ printf("调用了message函数\n"); } int main(){
void(*func_p)() = &message; func_p(); printf("函数指针的地址是:%#x\n",func_p); printf(
"如果直接调用函数名称获取地址:%#x\n",message); return 0; }

那么函数指针能有什么样的作用呢?
#include<stdio.h> int add(int num1,int num2){ return num1+num2; } int min(int
num1,int num2){ return num1-num2; } void showMsg(int(*fun)(int num1,int num2),
int a,int b){ int r = fun(a,b); printf("计算之后的结果是:%d\n",r); } int main(){
showMsg(add,11,12); showMsg(min,1,14); return 0; }

这个例子的主要作用就是,我们可以将函数作为我们的形参传递过来,类似于java中的多态。
同样,我们这里使用的是函数的名称,直接传递过来的,我们也可以传递函数的地址,可以起到同样的效果
#include<stdio.h> void requestNet(char* url,void(*callback)(char*)){ printf(
"请求的地址是:%s,正在请求网络...\n",url); char* ss = "获取到网络请求数据,为人性僻耽佳句,语不惊人死不休";
callback(ss); }void netCallback(char* ss){ printf("网络请求回调\n"); printf(
"请求得到的数据是:%s\n",ss); } int main(){ char* url = "http://www.baidu.com";
requestNet(url,netCallback); }


动态内存分配


在java中我们通过JVM实现对内存的分配,这样做的好处是很少会造成内存泄漏,但是也会存在内存越来越大的问题。所以在一些Android手机应用就是这样子,刚开始很流畅,结果越到后面越卡,特别是在处理比较大的文件或gif图片的时候。那么这时候,我们通过JNI,让C语言在需要的特定时间,释放内存,可以极大限度的让手机运行更加流畅。
C语言的内存分为下面的几个部分:
四区分配:

内存 描述 特性
栈区 是一个确定的常数,不同的操作系统会有不同的大小,超出之后会stackoverflow 自动创建,自动释放
堆区 用于动态内存分配 手动申请和释放,可以占用80%的内存
全局区或静态区 在程序中明确被初始化的全局变量,静态变量(包括全局静态变量和局部静态变量)和常量数据(包括字符串常量) 只初始化一次
程序代码区 代码取指令根据程序设计流程依次执行,对于顺序指令,只会执行一次,如果需要反复,需要跳出指令,如果需要递归,需要借助栈来实现
代码区的指令包括操作码和要操作的对象(或对象地址引用)
动态分配内存

C语言中动态分配内存实在堆区中的,java通过new一个对象出来的时候,也是在堆区中申请一块内存。如果我们想要在堆区中申明一块内存,则需要使用关键字
malloc,函数定义如下
void* __cdecl malloc( _In_ _CRT_GUARDOVERFLOW size_t _Size );
使用方式如下:
// 动态内存分配,使用malloc函数在对内存中开辟连续的内存空间,单位是:字节 // 申请一块40M的堆内存 int * p = (int* )
malloc(1024*1024*10*sizeof(int));
这里我们可以试着写一个小程序(小病毒,之前写过一个类似于清楚磁盘所有内容的小病毒)
#include<stdio.h> void func(){ //在函数中要求申请内存空间,那么如果我们一直申请内存空间,就会造成内存空间不足 int* p
= (int*)malloc(1021 * 1024 * 3 * sizeof(int)); } int main(){ while(1){ func(); }
return 0; }
这个地方我就不运行了。

静态分配内存

在使用静态分配内存的时候,内存大小是固定的,很容易超出栈内存的最大值。使用malloc申请内存,最重要的内容就是可以规定申请内存的大小,也可以使用
realloc重新申请内存大小
关于realloc函数的定义:
void* __cdecl realloc( _Pre_maybenull_ _Post_invalid_ void* _Block, _In_
_CRT_GUARDOVERFLOW size_t _Size );
使用方式:
// 重新申请内存大小 , 传入申请的内存指针 , 申请内存总大小 int* p = realloc(p,(len + add) * sizeof(int
));
一个例子,一开始申请一个空间内容,然后再增加到一定的内容:
#include<stdio.h> int main(){ int len; printf("请输入首次分配内存的大小:"); scanf("%d"
,&len);//动态分配内存,这里注意内存空间是连续的 int* p = (int*)malloc(len*sizeof(int)); //
给申请的内从空间赋值int i = 0; for(;i<len;i++){ p[i] = rand() % 100; printf("array[%d] =
%d,%#x\n",i,p[i],&p[i]); } printf("请输入增加内存的大小"); int add ; scanf("%d",&add); //
更改内存分配大小之后,之前赋值的内容是不变的int* p2 = (int*)realloc(p,(len + add) * sizeof(int)); //
给申请的内存空间赋值int j = len; for(;j < len + add;j++){ p2[j] = rand()%200; } for(int k=
0;k<len+add;k++){ printf("array[%d] = %d,%#x\n",k,p2[k],&p2[k]); } //释放内存 if(p2
!= NULL){ free(p2); p2 = NULL; }return 0; }

在这里我们会发现,就算我们改变了内存大小,但是之前存储的内容依然没有改变,保留了下来。

动态分配内存空间注意点:
1. 不能多次释放
2. 释放完成之后,给指针设置为NULL,表示释放完成
3. 内存泄漏(p重新赋值之后,调用free,并没有真正的完全释放,要在赋值之前释放前一个内存空间,也就是先释放,在赋值)

参考资料

C语言基础 <https://www.jianshu.com/p/4701cd1e1914>
NDK开发 <https://blog.csdn.net/Hi_Red_Beetle/article/details/78994767>

友情链接
KaDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:[email protected]
QQ群:637538335
关注微信