Android NDK开发(一)C语言基础语法

最近一段时间在攻克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类型就是/dchar类型就是/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 = &num;
    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语言基础
NDK开发

猜你喜欢

转载自blog.csdn.net/weixin_42580207/article/details/80901614