背景
这个系列是自学Android音视频系列。
前言
C和C++作为学习音视频技术首要具备的语言基础,所以十分必要学习和复习一下之前学习的C语言基础。
这里给大家分享一个B站的音视频开发教程:音视频开发全系列教程_哔哩哔哩_bilibili
正文
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
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
#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
#include "support.h"
int a = 10;
int b = 20;
int main() {
int sum = add(a,b);
printf("sum = %d",sum);
return 0;
}
这样才可以,但是这个总感觉很别扭,所以可以使用extern来解决:
#include
#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
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
//声明一个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
#include
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
#include
#include
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语言无法对比的,确实好用,这篇文章先学习到这里,下篇文章继续。
音视频开发全系列教程_哔哩哔哩_bilibili
本文转自 https://juejin.cn/post/7020676541172416519,如有侵权,请联系删除。