音视频学习之路--C语言(1)

461 阅读12分钟

背景

这个系列是自学Android音视频系列。

前言

C和C++作为学习音视频技术首要具备的语言基础,所以十分必要学习和复习一下之前学习的C语言基础。

正文

C的入门大概会分成几章学习,由于之前在大学期间学习过C,而且后面做过简单的JNI开发,所以这里就简单回顾和复习一遍。

安装IDE

记得很久之前开发C都是用的Visual Studio,不过我看有人推荐使用Clion这个IDE,风格和Android studio一样,简直无缝切换,这里直接从官网下载,然后会发现需要购买,当然这里推荐有能力的可以购买,我这里找到一个生成激活码的地方:

33tool.com/idea/

有需要就直接激活即可。

CLion的风格就这样,不得不说JetBrains出品的产品还是很nice的。

image.png

配置环境

我这里使用windows电脑进行开发,所以需要配置一下环境,当然不用配置也是可以的,使用CLion直接run也是能编译的,但是我们还是要简单了解一下。

其中编译c语言的编译器叫做gcc,这里下载gcc非常方便,可以通过Cygwin64下载,选中gcc-core、make等几个插件即可,然后再配置系统变量,最后在命令行界面就可以使用gcc了。

这里IDE默认的hello world程序,在控制台ls发现只有一个main.c文件,这个.c也就是源程序,

image.png

调用gcc命令,会生成exe文件,

image.png

再运行exe文件,我们第一个hello world就完成了。

image.png

C语言

Hello World

看一下C语言的Hello World如何打印:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

C直接使用main()作为程序的入口,而且方法和变量类型写前面,和Java语言类似;其中使用#include来导入头文件,也就是导入包;

关键字

不论啥语言都有自己的关键字,这里我们来看看C语言中的一些常用关键字:

C关键字.png

其实还是蛮容易的,循环、判断都是所有语言通用的,基本数据类型C中更加区分了,和Java有所不同,其他关键字都可以凭字面意思理解。

数据类型

对于数据类型在Java中我们很熟悉就俩种,一个是基本数据类型,一个是引用数据类型,具体看下图:

image.png

在Java中数组、接口、类和null都是引用数据类型,其他是基本数据类型,在C中基本也差不多,但是有所区别,如下图:

C数据类型.png

这里特殊之处我觉得是C有个函数类型,这个其实就是函数指针,在C中有大作用。

printf格式控制

为什么要说这个呢,因为Java的基本数据类型就那几个,但是C不一样,C里面有算术类型,而这个算术类型还巨多,范围也不一样,不区分一下还是很容易搞错,刚好C有个sizeof方法可以查看类型所存储的大小。

对于printf函数打印算术类型数据也是很有讲究,它可以理解为按xx格式读取xx类型的整数/小说,赋值给xx类型,比如下面代码:

//读取一个十进制的整数,赋值给int
printf("l1 : %d \n",1225422554);
//读取一个十进制的整数,赋值给short
printf("l2 : %hd \n",1225422554);
//读取一个十进制的整数,赋值给long
printf("l3 : %ld \n",1225422554);

这都是读取一个十进制的整数,是通过%d这个d来表示,但是赋值给的类型确不一样,其中short能保存的最大值是3万多,所以这个打印第二行应该不对,打印是:

image.png

会发现l2是不对,这也是符合情理的。

总结如下,以后对于打印算术类型也要小心处理。

格式控制符说明
%c读取一个单一的字符
%hd、%d、%ld读取一个十进制整数,并分别赋值给 short、int、long 类型
%ho、%o、%lo读取一个八进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型
%hx、%x、%lx读取一个十六进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型
%hu、%u、%lu读取一个无符号整数,并分别赋值给 unsigned short、unsigned int、unsigned long 类型
%f、%lf读取一个十进制形式的小数,并分别赋值给 float、double 类型
%e、%le读取一个指数形式的小数,并分别赋值给 float、double 类型
%g、%lg既可以读取一个十进制形式的小数,也可以读取一个指数形式的小数,并分别赋值给 float、double 类型
%s读取一个字符串(以空白符为结束)

C中的变量定义和声明

在C语言中的变量定义、声明有一点不一样,变量声明向编译器保证变量以指定的类型和名称存在。

但是分2种情况:

  • 默认是建立存储空间的,比如int a在声明的时候就建立了存储空间。
  • 另一种是不需要的,通过extern关键字声明变量但是不定义他,比如extern int a其中a可以在别的文件中定义。

C中定义常量

啥是常量我们就不说了,相当于Java的final变量。主要有2种方式:

  • 使用#define预处理器。
  • 使用const关键字。

这里需要注意一个预处理器,一个是关键字,关于啥是预处理器我也不是很明白,后面再说。

#define age 18

void sizeofFun();

int main() {
    const int i = 19;
    printf("age = %d  i = %d",age,i);
    return 0;
}

其中#define是预处理方式。

存储类

啥子是存储类呢,这个概念在Java中是没有的,其实很简单就是变量的几种修饰符,作用就是定义这个变量的范围和生命周期。

在前面的关键字小节中我们说了auto和register关键字,分别是本地变量和可以把变量保存在寄存器中,其实还有2种,分别是static和extern:

  • static:也就是静态的,这个和Java的静态变量和静态方法是一样的,也就是生命周期是程序的生命周期,属于全局变量。 -extern:这个其实就是可以理解为导包,提供一个全局变量的引用,这个变量可以在其他文件中定义。

说道这里不得不说C的执行,是按文件执行的,所以变量的顺序定义是有先后顺序的,比如下面代码:

#include <stdio.h>
#include "support.h"

int main() {
    //这里编译不过
    int sum = add(a,b);
    printf("sum = %d",sum);
    return 0;
}

int a = 10;
int b = 20;

这里a、b变量在main()方法之后定义,就无法使用,必须在main()方法之前:

#include <stdio.h>
#include "support.h"

int a = 10;
int b = 20;

int main() {
    int sum = add(a,b);
    printf("sum = %d",sum);
    return 0;
}

这样才可以,但是这个总感觉很别扭,所以可以使用extern来解决:

#include <stdio.h>
#include "support.h"

int main() {
    extern int a;
    extern int b;
    int sum = add(a,b);
    printf("sum = %d",sum);
    return 0;
}

int a = 10;
int b = 20;

这里的extern也就相当于扩展了作用域。

当然除了在一个文件中使用extern,在C中extern关键字最多的使用是多文件处理时,比如下面是main.c文件:

#include <stdio.h>

int a = 10;
int b = 20;
int add();

int main() {
    int sum = add();
    printf("sum = %d",sum);
    return 0;
}

定义了a、b2个变量和add函数,然后在addFun.c文件中:

extern int a;
extern int b;

int add(){
    return  a + b;
}

按理说这个的add方法肯定无法执行得到a和b,因为不在一个文件中,但是这里使用extern关键字可以实现。

函数

函数其实和Java中的定义是一样的,返回值在前,函数名和参数形成函数签名,但是这里说一个不一样的,就是函数声明,在Java中你定义一个函数,必须要有方法体,除非是接口,不然是无法定义成功的,但是在C中就不一样了,比如下面代码:

#include <stdio.h>
//声明一个max函数
int max(int,int );

int main() {
    printf("max = %d",max(10,20));
    return 0;
}
//函数实现地方
int max(int num1,int num2){
    return (num1 > num2)? num1 : num2;
}

这种函数声明和函数主体的定义在Java中绝对是不可能的,在C中可以这样实现,声明和实现可以分开。

函数参数

如果函数要使用参数,必须声明接受参数值的变量,这些变量被称为函数的形式参数,形参就像局部变量,在进入函数时被创建,退出函数时销毁,这个和其他Java语言都是一样的,不过这里有个调用类型区别,也就是传值调用和引用调用。

函数参数调用.png

这里有了指针的概念,所以可以进行引用调用,直接修改该地址指向的内容。

在这里我们可以对比一下Java,在Java中所有函数都是传值调用,但是还要注意一下的是Java的类型分为基本类型和引用类型,其中基本类型不用说传递肯定是值传递,但是引用类型时需要注意即使是复制也是复制的是引用,假如参数是class类型,其中字段还有引用类型,进行拷贝的话只是浅拷贝,里面的引用类型的字段还是一个,所以修改形参会影响传递的实参。

数组

数组就是一段内存连续的内容,其实没啥好说的,定义还有赋值啥的和Java中基本一样,但是这里有几点还是不同的,这里来梳理一下。首先是指针的思想,在Java中数组属于引用数据类型,所以定义一个数组变量其实就是一个指向数组的引用,在C中数组的数组名其实就是指向数组的第一个元素的指针。根据这个思想,我们可以看一下如何传递数组给函数:

  • 形参是一个指针
void testArray(int *param){
    
}
  • 形参是一个已经定义大小的数组
void testArray(int param[10]){

}
  • 形参是一个未定义大小的数组
void testArray(int param[]){

}

指针

对于指针来说,这个就是C的灵魂所在,其实也非常的简单就是地址,下面是简单概述:

指针概述.png

这里其实也比较简单,只需要明白特定类型的指针就是指向特定数据类型的一个地址即可。

函数返回指针

既然了解了指针,这里说一个C语言的强大之处,就是它的函数返回值可以是指针类型,但是注意不能返回局部变量的地址,除非局部变量定义为static。

看一波下面代码:

#include <time.h>
#include <stdlib.h>

int * getRandom(){
    //这里的static修饰
    static int r[10];
    srand((unsigned) time(NULL));
    for (int j = 0; j < 10; ++j) {
        r[j] = rand();
        printf("[%d] : %d \n",j,r[j]);
    }
    return r;
}

int main() {
    int *p;
    p = getRandom();
    for (int j = 0; j < 10; j ++) {
        printf("*(p + [%d]) : %d \n",j,*(p + j));
    }
    return 0;
}

这里返回一个数组,前面说了数组就是指向第一个元素的指针,由于数组是连续地址,所以在利用数组的指针获取其中的值时可以直接对指针++,这也是很巧妙的做法,然后看一下打印结果:

image.png

是符合的。但是仔细一想有点不对,我这个返回的地址是r这个数组的,但是r是局部变量,按理说局部变量会在函数结束后就释放的,所以这个指向的值是空的才对,其实不然,这里使用了static修饰,假如把static修饰给去掉:

int * getRandom(){
    //这里的static修饰去掉
    int r[10];
    srand((unsigned) time(NULL));
    for (int j = 0; j < 10; ++j) {
        r[j] = rand();
        printf("[%d] : %d \n",j,r[j]);
    }
    return r;
}

int main() {
    int *p;
    p = getRandom();
    for (int j = 0; j < 10; j ++) {
        printf("*(p + [%d]) : %d \n",j,*(p + j));
    }
    return 0;
}

打印结果是:

image.png

这就不对了,但是为什么第一个值是对的,按理说都被释放了,这里都不对才是,具体原因不知。

关于为什么C不支持返回局部非static的变量,因为局部变量和Java一样结构是保存在栈中的,方法执行完就释放了,但是static变量是存放在静态数据区,不会随着函数的执行结束而清除。

其实这就涉及到了C的存储位置,这里先不说了,由于只熟悉Java的,就不过多扩展了,后面有机会再探究。

函数指针

说起这个其实很有意思,我们必须要知道一个一个函数的类型是啥,也就是函数参数以及返回值,关于函数名你想叫啥就是啥,所以这里函数指针就是指向函数的指针,熟悉kotlin中的高级函数的话,这个就非常容易理解。

直接看下面代码:

double max1(double num1,double num2){
    return (num1 > num2) ? num1 : num2;
}

int main() {
    //定义一个函数指针,返回值是double,参数是(double,double)
    double (*p)(double ,double ) = *max1;
    double a,b,c,d;
    printf("input 3 numbers: \n");
    scanf("%lf %lf %lf",&a,&b,&c);
    d = p(p(a,b),c);
    printf("max: %lf \n",d);
    return 0;
}

这里的函数指针p其实就相当于kotlin中的p:(double,double) -> double 这种类型,很好理解。

回调函数

在Java中我们使用回调会立马想起使用接口,但是比较麻烦,其实在kotlin中我们就是使用了高级函数来进行回调的,也就是定义一个变量,它的类型是高阶函数类型,在需要实现的地方对这个变量进行处理,就会回调到被调用地方。

而上面所说的函数指针,其实和这玩意差不多,所以使用函数指针来实现回调函数很简单。

下面来看一个非常简单的例子:

#include <stdio.h>
#include <time.h>
#include <stdlib.h>

void test(int *array,size_t arraySize,int (*p)(void )){
    for (int i = 0; i < arraySize; ++i) {
        //p就是函数指针
        array[i] = p();
    }
}

int getNextValue(){
    return rand();
}

int main() {
    int array[10];
    //直接传递函数名,也就是函数指针
    test(array,10,getNextValue);
    for (int i = 0; i < 10; ++i) {
        printf("value : %d \n",array[i]);
    }
    return 0;
}

打印结果如下:

image.png

完全符合预期,这里和kotlin的区别就是C这里传递函数指针也就是函数名即可,只要方法签名相同和返回值相同就可以。

字符串

说起字符串这个东西,Java就方便多了,因为在C中没有String类型,字符串是一个char类型的一维数组,不仅如此,数组最后一个位置还是null字符‘\0’,就比如下面:

char ch[] = {'h','e','l','l','o','\0'};
//简写
char ch1[] = "hello";

int main() {
    printf("ch size = %d \n", sizeof(ch));
    printf("ch1 size = %d", sizeof(ch1));
    return 0;
}

这里的长度都是6:

image.png

注意这里获取数组长度是通过sizeof,sizeof函数返回的是数组的长度,但是字符串的长度计算是不带\0的,所以字符串长度是5,字符串长度的api是strlen函数,下面看一下:

char ch[6] = {'h','e','l','l','o','\0'};
char ch1[6] = "world";
char ch2[12];

int main() {
    //复制
    strcpy(ch2,ch1);
    printf("ch2 : %s \n",ch2);

    //拼接
    strcat(ch,ch1);
    printf("ch : %s \n",ch);

    //返回长度
    int len = strlen(ch);
    printf("ch str size : %d \n",len);
    int len1 = sizeof(ch);
    printf("ch size : %d",len1);
    
    return 0;
}

这里有3个字符串,其中ch通过拼接,这时的长度肯定不止6了,但是依旧可以保存,这里的打印是:

image.png

还有其他的字符串操作API,主要也就是判断2个字符串是否相同、返回某个字符在字符串中第一次出现的位置等API。

总结

其实所有语言都是很类似的,设计思路很多都是通用的,不过C的指针还是Java语言无法对比的,确实好用,这篇文章先学习到这里,下篇文章继续。