前言
作为一个Android开发,想要学习音视频方向知识和NDK技术。就得具备C/C++ 语言基础,下面我们就先来学习 C语言基础。
简介
C 语言是一种通用的高级语言,最初是由丹尼斯·里奇在贝尔实验室为开发 UNIX 操作系统而设计的。C 语言最开始是于 1972 年在 DEC PDP-11 计算机上被首次实现。
在 1978 年,布莱恩·柯林汉(Brian Kernighan)和丹尼斯·里奇(Dennis Ritchie)制作了 C 的第一个公开可用的描述,现在被称为 K&R 标准。
UNIX 操作系统,C编译器,和几乎所有的 UNIX 应用程序都是用 C 语言编写的。由于各种原因,C 语言现在已经成为一种广泛使用的专业语言。
- 易于学习。
- 结构化语言。
- 它产生高效率的程序。
- 它可以处理底层的活动。
- 它可以在多种计算机平台上编译。
环境搭建
开发工具我使用的是CLion工具,你可以发现 CLion 页面跟使用风格包括快捷键都跟 AndroidStudio 一样。上手极其容易。安装教程如下https://blog.csdn.net/tunmengsmile/article/details/108197388
1. 程序结构
**创建我们编程生涯的第一个C程序–HelloWorld:**首先是不是觉得很懵,不知道怎么创建,哈哈其实第一次我创建的时候也懵。
- 第一步: 新建一个project
- 第二步: 新建一个C/C++文件
nice 终于创建好了,代码如下:
#include <stdio.h>
/***
* c语言入口程序
* @return
*/
int main (){
//主函数,程序从这里开始执行
printf("hello,world!\n");
return 0;
}
可以看到 C 语言的入口函数跟 Java 的类似吧,都是以 main 来定义的入口,接下来我们讲解一下上面这段程序的意思:
1. 程序的第一行 #include <stdio.h> (头文件)是预处理器指令,告诉C编译器在实际编译之前要包含stdio.h文件。一个C程序就是由若干头文件和函数组成。
2. int main() 是主函数,程序从这里开始执行。这个主函数就是C语言中的唯一入口。
3.printf(...) 是C中另一个可用的函数,会在屏幕上显示消息 " Hello World!"。 \n 转义字符 换行;
4. return 0; 终止 main() 函数,并返回值 0。
5. C语言注释方法有两种:
多行注释: /* 注释内容 */
单行注释: //注释一行
2.关键字
c程序由各种令牌组成,令牌可以是关键字、标识符、常量、字符串值、或者是一个符号。
我们都知道Java中有关键字这个概念, C中也有关键字,这些关键字不能作为常量名,变量名或者其它标识符名称。跟Java基本相同
接下来我们讲讲关键字:
又叫保留字,是编译器能识别的特殊单词,每种计算机语言都会有其特定的关键字,C语言中有32位关键字。如下表:
关键字 | 说明 |
---|---|
auto | 声明自动变量 |
break | 跳出当前循环 |
case | 开关语句分支 |
char | 声明字符型变量或者函数返回值类型 |
const | 声明只具可读变量 |
continue | 结束当前循环,开始下一个循环 |
default | 开关语句中的其它分支 |
do | 循环语句的循环体 |
double | 声明双进度浮点型变量或者函数返回值类型 |
else | 条件语句否定分支 |
enum | 声明枚举类型 |
extern | 声明变量或函数是在其它文件或本文件的其他位置定义 |
goto | 无条件跳转语句 |
float | 声明浮点型变量或者函数返回值类型 |
for | 一种循环语句 |
if | 条件语句 |
int | 声明整型变量或函数 |
long | 声明长整型变量或函数返回值类型 |
register | 声明寄存器变量 |
return | 子程序返回语句 |
short | 声明短整型变量或者函数 |
signed | 声明有符号类型变量或者函数 |
sizeof | 计算数据类型或者变量长度(即所占字节数) |
static | 声明静态变量 |
struct | 声明结构体类型 |
switch | 用于开关语句 |
typedef | 用以给数据类型取别名 |
unsigned | 声明无符号类型变量或函数 |
union | 声明共用体类型 |
void | 声明函数无返回值或无参树,声明无类型指针 |
volatile | 说明变量在程序执行中可被隐含地改变 |
while | 循环语句的循环条件 |
我相信,大部分关键字我们都能认识,并且能够使用,有一部分可能很少见,甚至一点印象也没有,接下来我挑一些我没印象的补补脑。
隐形刺客:auto
描述: auto关键字在我们写的代码里几乎看不到,但是它又无处不在,它是如此的重要,又是如此的与世无争,默默的履行着自己的义务,却又隐姓埋名。
作用:C程序是面向过程的,在C代码中会出现大量的函数模块,每个函数都有其生命周期(也称作用域),在函数生命周期中声明的变量通常叫做局部变量,也叫自动变量。例如:
int fun(){
int a = 10; // auto int a = 10;
// do something
return 0;
}
整型变量a在fun函数内声明,其作用域为fun函数内,出了fun函数,不能被引用,a变量为自动变量。也就是说编译器会在int a = 10之前会加上auto的关键字。
auto的出现意味着,当前变量的作用域为当前函数或代码段的局部变量。意味着当前变量会在内存栈上进行分配。
闪电飞刀:register
描述:register就和它的名字一样,很少出现在代码世界中,因为敢称为闪电飞刀的变量,通常只会在一些特定场合才能出现。它是如此的快,以致于CPU都对其刮目相看,但是它有一个致命的缺点,它的速度“看心情”而定,不是每一次都能让人满意。
作用:如果一个变量被register来修辞,就意味着,该变量会作为一个寄存器变量,让该变量的访问速度达到最快。比如:一个程序逻辑中有一个很大的循环,循环中有几个变量要频繁进行操作,这些变量可以声明为register类型。
寄存器变量:寄存器变量是指一个变量直接引用寄存器,也就是对变量名的操作的结果是直接对寄存器进行访问。寄存器是CPU的亲信,CPU操作的每个操作数和操作结果,都由寄存器来暂时保存,最后才写入到内存或从内存中读出。也就是说,变量的值通常保存在内存中,CPU对变量进行读取先是将变量的值从内存中读取到寄存器中,然后进行运算,运算完将结果写回到内存中。为什么要这么设计,而不直接对变量的值从内存中进行运算,而要再借助于寄存器?这是由于考虑到性能的问题才这么设计的。在计算机系统中,包含有很多种不同类型的存储器,如表所示。
在计算机中CPU的运算速度最快,现在都达到3GHZ左右,而相对应的存储器速度却相对慢很多,访问速度最快的寄存器和缓存,由于其体积又大,不适合大容量的使用,所以只能二者相接合的方式来提高效率。程序代码保存在内存中,当使用数据时,将其送到寄存器,让CPU来访问,使用完毕,送回内存保存。而C语言又允许使用寄存器来保存变量的值,很明显这样能大大提高程序的执行速度,但是,寄存器的个数是有限的,X86也就是十几个,ARM最多才37个。我们不可能将全部的变量都声明为寄存器变量,因为其它代码也要使用寄存器,同样,我们声明的寄存器变量也不一定直接保存在寄存器中,因为寄存器可能全部都在被其它代码占用。编译器只能是尽量的为我们的变量安排在寄存器中。
在使用寄存器变量时,请注意:
- 待声明为寄存器变量类型应该是CPU寄存器所能接受的类型,意味着寄存器变量是单个变量,变量长度应该小于等于寄存器长度
- 不能对寄存器变量使用取地址符“&”,因为该变量没有内存地址
- 尽量在大量频繁的操作时使用寄存器变量,且声明的变量个数应该尽量的少
乱世枭雄:static与extern
描述:在C程序世界里,不同代码国度以.c文件为国界分隔开来,在单个国家(C源文件)里有不同的函数占山为王,军阀割据,每个C程序世界里只有一个君主main和其首都(main函数体),main通过下传圣旨(参数),调用各种军阀(函数),来掌控整个C程序世界的有序运行。在和谐世界的幌子下,却是别番风景,某军阀(函数)心怀叵测,不想单纯听从于main的指挥与调度,树立了自己的政权旗帜static。static不用听附与main的调度,自己做主,私藏金库(空间)。而main对此却很无奈,因为相对static来说,extern更是让它皇权难保。不同的国家(不同的.c文件)之间通过extern相互私通,传递信息。二者联合作乱,让编程者逻辑混淆。当然,如果编程者连一个国家都没有走出去过(指将所有代码写到一个.c文件里),不能够上升到一个宏观的角度,将不知所云。乱世出枭雄,切听我慢慢道来其中一二。
作用:简单来说static修辞变量,就是指该变量空间独立于函数中的auto变量或叫栈变量(请查看auto关键字章节),static变量空间在内存中的静态区内被分配。如图所示。
在使用static的时候一定要注意以下两点:
-
static变量在程序运行(main启动)之前就已经被分配,它不像是局部变量那样动态在栈上分配的,它在程序彻底退出之后才被释放。
-
static变量有访问权限,在子函数里声明的static变量,只能在该函数内访问,如果static变量在函数体外声明,它的访问权限就是本文件内。
-
static不仅可以修辞变量,还可以用来修辞函数,如果用来修辞函数,和第2条有着相似的意义,表示该函数访问权限限制在本源文件内。
记住以上三点,可以解释很多初学者不明白的问题:
为什么子函数里声明的static变量,每次访问其值都是上一次的结果?答案见要点1。
为什么子函数里的static变量不能在子函数外面访问?答案见要点2。
如何避免不同文件里命名冲突的问题?答案见要点3。
extern是指,当前变量或函数不是在本源文件内声明的,它是外部变量或外部函数,正所谓“外来的和尚会念经”,能很好的体现extern的价值。当我们在本文件里试图引用一个外部声明的全局变量或函数时,可以在其前面加上extern,表示它是外来和尚。
宏观理解:通过上面的分析看来,C程序里,通过函数将功能区分开来,每个函数完成一个功能(这也是为什么函数的英文叫function),而又将一片相关联的功能集合在一个源文件里,这些功能和相关联的功能之间通常要有联系,而这种联系(亦可叫通信)就是通过static和extern进行联系起来的,当然这里面还要有头文件的功劳,关于头文件的解释,后面会单独拿来分析。
铁布衫:const
描述:相传C世界中出现了一件极品装备const,它的出现,让天下所有的刺客,黑客都失业了,在它的保护下,所有的变量都可以完好无损。
作用:const是constant的简写,表示海枯石栏,恒定不变,一旦相伴,永不“心”变。只要一个变量前面用const来修辞,就意味着该变量里的数据可以被访问,不能被修改。
虽然理解起const来相对比较容易理解,但是const不仅仅可以用来修辞基本类型,它还经常用来修辞一些构造类型和指针及其参合体:如数组,指针,指针数组,结构体数组,结构体指针数组等。一旦和这些复杂类型接合起来,还有一定的迷惑性的。我们一一进行分析。
1 const int a = 10;
2 int const a = 10;
3 const int a[10] = {1,2,3,4,5,6,7,8,9,10};
4 const int *p;
5 int * const p;
6 const struct devices dev[5];
7 struct devices const * dev[5];
看到上面列出的例子,我相信很多朋友都会倒吸一口冷气:想说爱你,不是一件容易的事。不过,我这有两招辨别使用的技巧:
将类型去掉
看const修辞谁,谁就拥有了铁布衫,谁的值就是不能修改的,readonly的。
1.去掉类型int变成:const a = 10,a拥有了铁布衫,a的值不变。
2.去掉类型int变成:const a = 10,a拥有了铁布衫,a的值不变,这两个效果一样。
3.去掉类型int变成:const a[10],a[10] 拥有了铁布衫,a数组里的值不变。
4.const修辞*p,去掉类型int变成:const *p,*p 拥有了铁布衫(下图中空间2),p所指向的空间里的值不变。
5.const修辞p,去掉类型int*变成:const p,指针变量p拥有了铁布衫(下图中空间1),指针变量p里的值不变,也就是说p不能再指向其它地址,但是p所指向的空间里的值可变,如图xxx所示。
6.去掉类型struct devices变成:const dev[5],dev[5] 拥有了铁布衫,dev[5]数组里的值不变。
7.这是一个devices结构体类型的指针数组,它拥有5个devices结构体类型指针,每个指针指向一个devices结构体,const修辞*dev[5],去掉类型struct devices变成:const dev[5],指针数组dev[5]拥有了铁布衫,指针数组dev中每个元素指向的空间里的值不变。
浪里白条:goto
描述:在所有的编程语言里,恐怕没有哪个关键字可以和goto相比,它可以指哪打哪,完全不用去顾忌编码规则,在代码世界里游刃有余,混得代码海洋里的浪里白条美誉,也正是由于其放荡不羁的特性,被编码规则牢牢划死在编程准则不允许之首。
作用:正如其名,go to everywhere,它可以在代码逻辑中任意穿梭,只要给我定义一个靶心(标签),我就可以打破逻辑直接到达,如下面示例。
if(网卡未初始化){
// 初始化网卡
if(初始化出错)
goto error;
}
char * buf = (char*)malloc(20);
if(接受网卡数据){
if(数据较验错误)
goto checkNumError;
//写入到buf中
}else{
goto timeupError;
}
checkNumError:
// 处理较验出错
goto freeMem;
timeupError:
// 处理超时出错
freeMem:
free(buf);
error:
// 其它善后处理
通过上面的代码可以看出,使用goto关键字,程序逻辑非常的自由,网卡初始化出错时,直接跳到23行执行,第9行,数据较验出错,直接跳到16行,然后处理完后跳到21行,执行buf的内存释放。虽然可以看到代码逻辑很自由,但是还是会发现有点混乱,如果程序员没有足够的代码经验,很容易出现逻辑问题,因此很多派系的编码规范中规定,禁止或尽量不使用goto关键字,很容易让程序员范迷糊。但是在很多场合下,使用goto关键字可以更方便快捷,比如:错误处理时,其实上面的例子就是一个简单的驱动错误处理例子。
专一王子:volatile
描述:每个变量和他的名字一样很善变,有时候它善变是发自内心的,有时是外部因素决定的,只有volatile变量才会表里如一,因此获得了专一王子的美誉。
作用:volatile字面意思是易挥发,易变化的意思,它修辞的变量表示该变量的值很容易由于外部因素发生改变,强烈请求编译器要老老实实的在每次对变量进行访问时去内存里读取。可能上面说的还不是很清楚,我们换个例子来说,你明天一个朋友过生日,今天把要送的礼物打包好了,一般情况下,我们明天起来不再需要再打开验证一下里面礼物是否存在,因为我们知道,只要礼物的外包装没有动过,里面东西应该不会被动。其实编译器和人一样聪明,为了提高效率也会玩省事,如下面的例子:
1 int a = 10;
2 int b = a;
3 int c = a;
编译器扫描了代码发现上面,第一行代码在将10赋给了整形变量a,之后a变量的值没有再发生改变。在后面第二行中,将a变量里的值取出来赋给b变量。在第三行代码里将a变量的值赋给c的时候,因为CPU访问内存速度较慢(看register关键字介绍),编译器为了提高效率,玩了“省事”,直接将10赋给了c。
单从上述代码我们来看是没有问题的,就如同从外包装看生日礼物完好一样。但是,上述代码如果运行在多线程中,在一个线程上下文中没有改变它的值,但是我们不能保证变量的值没有被其它线程改变。就好比是,生日礼物放到其它人那里保存,我们不敢100%保证它里面的东西还完好。当然这种数据不一致的机制不仅仅出现在多线程中,同样在设备的状态寄存器里也会存在。比如:网卡里的某状态寄存器里的值是否为1表示是否有网络数据到达,在当前时刻其值为1,不能代表着下一时刻其值还为1,它的值是由外界条件决定的,编译器肯定不能在这种情况下玩“省事”
为了防止在类似的情况下,编译器玩省事,可以将这些变量声明为volatile,这样,不管它的值有没有变化,每次对其值进行访问时,都会从内存里,寄存器里读取,从而保证数据的一致,做到表里如一。
struct关键字
C 语言中的 struct 声明结构体类型,可以看作变量的集合,struct中的每个数据成员都有独立的存储空间。为了让CPU能够更舒服地访问到变量,struct中的各成员变量的存储地址有一套对齐的机制。这个机制概括起来有两点:
- 每个成员变量的首地址,必须是它的类型的所占字节数的整数倍,如果不满足,它与前一个成员变量之间要填充(padding)一些无意义的字节来满足;
- 整个struct的大小,必须是该struct中所有成员的类型中占字节最大者的整数倍,如果不满足,在最后一个成员后面填充。
举个例子
struct student{
char sex;
int score;
};
第一个char类型成员与第二个int类型成员之间会填充数据,以满足第一个要求;
最大长度为整型占用4个字节的空间,所以其占用的空间为4的倍数,这样student占用的空间就是8个字节。
另外,数据成员的书写顺序也会影响结构体占用的空间的大小:
struct student{
char sex;
int score;
char subject;
};
占用的是4x3为12个字节,内存结构为先为sex分配一个字节的空间,然后执行对齐操作,下一个int型变量在下一个四字节的空间,然后,后面的subject为了对齐还占用4个字节的空间。
而如果是下面这种书写顺序:
struct student{
char sex;
char subject;
int score;
};
占用的是2X4为8个字节的空间,因为前两个字符连续存放在了四字节的空间的前两个里面。为了执行对齐,空了两个字符的空间,下一个int型的变量存放在下一个四字节的空间中。
总结:因为struct结构体的存储顺序会影响空间的使用率,所以以后应该尽量将相同数据类型的变量连续写。
空结构体多大?
注意下面这个空结构体:
struct student
{
}stu;
sizeof(stu)的值是多少呢?很遗憾,不是0,而是1。为什么呢?你想想,如果我们把struct student 看成一个模子的话,你能造出一个没有任何容积的模子吗?
显然不行。编译器也是如此认为。编译器认为任何一种数据类型都有其大小,用它来定义一个变量能够分配确定大小的空间。既然如此,编译器就理所当然的认为任何一个结构体都是有大小的,哪怕这个结构体为空。那万一结构体真的为空,它的大小为什么值比较合适呢?
假设结构体内只有一个char 型的数据成员,那其大小为1byte(这里先不考虑内存对齐的情况).也就是说非空结构体类型数据最少需要占一个字节的空间,而空结构体类型数据总不能比最小的非空结构体类型数据所占的空间大吧。这就麻烦了,空结构体的大小既不能为0,也不能大于1,怎么办?定义为0.5个byte?但是内存地址的最小单位是1 个byte,0.5 个byte 怎么处理?解决这个问题的最好办法就是折中,编译器理所当然的认为你构造一个结构体数据类型是用来打包一些数据成员的,而最小的数据成员需要1 个byte,编译器为每个结构体类型数据至少预留1 个byte的空间。所以,空结构体的大小就定位1 个byte。
柔性数组
也许你从来没有听说过柔性数组(flexible array)这个概念,但是它确实是存在的。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof 返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
typedef struct st_type
{
int i;
int a[];
}type_a;
这样我们就可以定义一个可变长的结构体, 用sizeof(type_a) 得到的只有4 , 就是sizeof(i)=sizeof(int)。那个0 个元素的数组没有占用空间,而后我们可以进行变长操作了。通过如下表达式给结构体分配内存:
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
这样我们为结构体指针p 分配了一块内存。用p->item[n]就能简单地访问可变长元素。
但是这时候我们再用sizeof(*p)测试结构体的大小,发现仍然为4。是不是很诡异?我们不是给这个数组分配了空间么?
别急,先回忆一下我们前面讲过的“模子”。在定义这个结构体的时候,模子的大小就已经确定不包含柔性数组的内存大小。柔性数组只是编外人员,不占结构体的编制。只是说在使用柔性数组时需要把它当作结构体的一个成员,仅此而已。再说白点,柔性数组其实与结构体没什么关系,只是“挂羊头卖狗肉”而已,算不得结构体的正式成员。
union关键字
C 语言中的 union 在语法上与 struct 相似, 只分配最大成员的空间,所有成员共享这个空间 。
struct A
{
int a;
int b;
int c;
};
union B
{
int a;
int b;
int c;
};
int main()
{
printf("sizeof(A) = %d\n",sizeof(A));//12
printf("sizeof(B) = %d\n",sizeof(B));//4
}
union 的使用受系统大小端的影响
#include <stdio.h>
int system_mode()
{
union SM
{
int i;
char c;
};
union SM sm;
sm.i = 1;
return sm.c;
}
int main()
{
//返回 1 时为小端,0 为大端模式
printf("System Mode: %d\n", system_mode());
return 0;
}
**sizeof **
sizeof(x)关键字,在编译时确定其值, 计算出x在内存中所占字节数.
- sizeof ( 数值 )
char c=8;
int i=32;
printf("%ld", sizeof(c )); // 结果:1, 因为char就是1字节
printf("%ld", sizeof(c+i )); // 结果:4, i是4字节, 运算时c值被隐式转换成int, 运算值是4字节
printf("%ld", sizeof(c=c+i)); // 结果:1, 等同于(c), 编译时, 因为=不被执行, 所以=的右边只是个屁
- sizeof ( 字符 )
// 小心, char和'a'在被=动作前, 是两个独立类型, 没关联
char c = 'a';
printf("%ld", sizeof(c)); // 结果:1, char类型是1字节
printf("%ld", sizeof('a'); // C结果:4,C++结果:1
// C99的标准, 'a'是整型字符常量,常量!常量!常量!被看成是int型, 所以占4字节。
// ISO C++的标准,'a'是字符字面量 ,被看成是char型,所以占1字节。
- sizeof ( 字符串 )
printf("%ld", sizeof("abc"); // 结果:4,双引号会在尾部自动添加转义字符'\0',即数据0X00, 所以是4
// 双引号作用: (1)字符串尾部加0, (2)开辟内存空间, (3)提取地址
- sizeof ( 数组 )
char a[50]="老师,早上好!";
printf("%ld", sizeof(p)); // 结果:50, 返回数组总字节大小
int arr[] = { 22, 34, 3, 32, 82, 55, 89, 50, 37, 5, 64, 35, 9, 70 };
int adb =sizeof(arr);
printf("总字节长度:%d\n",adb);
int ss =sizeof(*arr);
printf("地址长度ss:%d\n",ss);
int len = (int) sizeof(arr) / sizeof(*arr);
printf("数组长度ss:%d\n",len);
输出结果:
总字节长度:56
地址长度ss:4
数组长度ss:14
- sizeof ( 指针 )
// 小心, 指针是地址, 指向某数据空间, 地址常是4字节.
char *p="老师,早上好!";
printf("%ld", sizeof(p); // 结果:4, 返回指针大小,指针本身就是一个无符号整型数. 不是所指向的内存空间的大小
// char *a[8], 在数组内存放8个地址, 8个*4字节=32字节, 而不是8个*1字节=8字节
- sizeof ( 结构体 )
// 字节对齐,为快速处理数据,内存是按32位读写的,而不是一字节一字节地读写
// 结构体的首地址自动对齐至能被对齐字节数大小所整除。
// 结构体每个成员在结构体内的偏移地址都是成员大小的整数倍,否则, 在前方填充byte。
// 结构体的总大小为结构体对齐字节数大小的整数倍
struct T1{
int a; // 成员随意位置
char b;
int c;
short d;
}t1;
printf("%ld", sizeof(t1); // 结果:16, 4+4+4+4
struct T2{
int a; // 合理安排成员位置
char b;
short d;
int c;
}t2;
printf("%ld", sizeof(t2); // 结果:12, 4+4+4, 设计结构时,调整成员的位置,可节省存储空间。
- sizeof ( 函数 )
// 小心, 函数不执行
char Sum(int i, short s);
printf("%ld", sizeof(Sum(32, 8)); // 结果:1, 只会判断返回类型的大小. 函数是不会执行的!