音视频学习之路--C语言(1)
背景
这个系列是自学Android音视频系列。
前言
C和C++作为学习音视频技术首要具备的语言基础,所以十分必要学习和复习一下之前学习的C语言基础。
正文
C的入门大概会分成几章学习,由于之前在大学期间学习过C,而且后面做过简单的JNI开发,所以这里就简单回顾和复习一遍。
安装IDE
记得很久之前开发C都是用的Visual Studio,不过我看有人推荐使用Clion这个IDE,风格和Android studio一样,简直无缝切换,这里直接从官网下载,然后会发现需要购买,当然这里推荐有能力的可以购买,我这里找到一个生成激活码的地方:
33tool.com/idea/
有需要就直接激活即可。
CLion的风格就这样,不得不说JetBrains出品的产品还是很nice的。
配置环境
我这里使用windows电脑进行开发,所以需要配置一下环境,当然不用配置也是可以的,使用CLion直接run也是能编译的,但是我们还是要简单了解一下。
其中编译c语言的编译器叫做gcc,这里下载gcc非常方便,可以通过Cygwin64下载,选中gcc-core、make等几个插件即可,然后再配置系统变量,最后在命令行界面就可以使用gcc了。
这里IDE默认的hello world程序,在控制台ls发现只有一个main.c文件,这个.c也就是源程序,
调用gcc命令,会生成exe文件,
再运行exe文件,我们第一个hello world就完成了。
C语言
Hello World
看一下C语言的Hello World如何打印:
#include <stdio.h> int main() { printf("Hello, World!\n"); return 0; }复制代码
C直接使用main()作为程序的入口,而且方法和变量类型写前面,和Java语言类似;其中使用#include来导入头文件,也就是导入包;
关键字
不论啥语言都有自己的关键字,这里我们来看看C语言中的一些常用关键字:
其实还是蛮容易的,循环、判断都是所有语言通用的,基本数据类型C中更加区分了,和Java有所不同,其他关键字都可以凭字面意思理解。
数据类型
对于数据类型在Java中我们很熟悉就俩种,一个是基本数据类型,一个是引用数据类型,具体看下图:
在Java中数组、接口、类和null都是引用数据类型,其他是基本数据类型,在C中基本也差不多,但是有所区别,如下图:
这里特殊之处我觉得是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万多,所以这个打印第二行应该不对,打印是:
会发现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语言都是一样的,不过这里有个调用类型区别,也就是传值调用和引用调用。
这里有了指针的概念,所以可以进行引用调用,直接修改该地址指向的内容。
在这里我们可以对比一下Java,在Java中所有函数都是传值调用,但是还要注意一下的是Java的类型分为基本类型和引用类型,其中基本类型不用说传递肯定是值传递,但是引用类型时需要注意即使是复制也是复制的是引用,假如参数是class类型,其中字段还有引用类型,进行拷贝的话只是浅拷贝,里面的引用类型的字段还是一个,所以修改形参会影响传递的实参。
数组
数组就是一段内存连续的内容,其实没啥好说的,定义还有赋值啥的和Java中基本一样,但是这里有几点还是不同的,这里来梳理一下。首先是指针的思想,在Java中数组属于引用数据类型,所以定义一个数组变量其实就是一个指向数组的引用,在C中数组的数组名其实就是指向数组的第一个元素的指针。根据这个思想,我们可以看一下如何传递数组给函数:
形参是一个指针
void testArray(int *param){ }复制代码
形参是一个已经定义大小的数组
void testArray(int param[10]){ }复制代码
形参是一个未定义大小的数组
void testArray(int param[]){ }复制代码
指针
对于指针来说,这个就是C的灵魂所在,其实也非常的简单就是地址,下面是简单概述:
这里其实也比较简单,只需要明白特定类型的指针就是指向特定数据类型的一个地址即可。
函数返回指针
既然了解了指针,这里说一个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; }复制代码
这里返回一个数组,前面说了数组就是指向第一个元素的指针,由于数组是连续地址,所以在利用数组的指针获取其中的值时可以直接对指针++,这也是很巧妙的做法,然后看一下打印结果:
是符合的。但是仔细一想有点不对,我这个返回的地址是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; }复制代码
打印结果是:
这就不对了,但是为什么第一个值是对的,按理说都被释放了,这里都不对才是,具体原因不知。
关于为什么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; }复制代码
打印结果如下:
完全符合预期,这里和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:
注意这里获取数组长度是通过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了,但是依旧可以保存,这里的打印是:
还有其他的字符串操作API,主要也就是判断2个字符串是否相同、返回某个字符在字符串中第一次出现的位置等API。
总结
其实所有语言都是很类似的,设计思路很多都是通用的,不过C的指针还是Java语言无法对比的,确实好用,这篇文章先学习到这里,下篇文章继续。
作者:元浩875
链接:https://juejin.cn/post/7020676541172416519