51c~C语言~合集1

我自己的原文哦~  https://blog.51cto.com/whaosoft/12428240

一、C语言和C++的区别

​ C语言虽说经常和C++在一起被大家提起,但可千万不要以为它们是一个东西。现在我们常用的C语言是C89标准,C++是C++99标准的。C89就是在1989年制定的标准,如今最新的是C11和C++11标准。根据不同的标准,它们的功能也会有所不同,但是越新的版本支持的编译器越少,所以本文在讨论的时候使用的C语言标准是C89,C++标准是C++99。

    本文来介绍C语言和C++中那些不同的地方

函数默认值

    在C++中我们在定义或声明一个函数的时候,有时会在形参中给它赋一个初始值作为不传参数时候的缺省值,例如:

int FUN(int a = 10);

    代表没有传参调用的时候,自动给a赋一个10的初始值。然而这种操作在c89下是行不通的,在c语言下这么写就会报错。我们都知道,系统在调用任何一个函数的时候都有函数栈帧的开辟,如果函数有参数则需要压入实参。平常在我们人为给定实参的时候,是按照参数列表从右向左依次将参数通过指令传入寄存器,再通过push指令压入。现在我们已经给定了函数参数的默认值,那么在压实参的时候只需要一步push初始值即可。效率更高。

    另外需要注意的是,赋初始值必须从参数列表的右边开始赋值,从左边开始赋值将会出错:

int sum1(int a = 10,int b);        //错误
int sum2(int a,int b = 20);            //正确
sum1( ,20) //?很可惜这样属于语法错误
sum2(20); //合情合理,没有任何问题
int  fun(int a ,int b = 10);
int  fun(int a = 20,int b);

    其实这样声明完全没有问题,两句声明是同一个函数(函数多次声明没有问题),第一句已经给b了一个初始值,运行到第二句时已经等价于

int fun(int a = 20,int b = 10);

    但是注意,这两句的顺序不能反转,否则就是错误的。

总结:C89标准的C语言不支持函数默认值,C++支持函数默认值,且需要遵循从右向左赋初始值。

 inline内联函数

    说到内联函数大家应当不陌生,它又是一个C89标准下C语言没有的函数。它的具体做法和宏非常相似,也是在调用处直接将代码展开,只不过宏它是在预编译阶段展开,而内联函数是在 编译阶段进行处理的。同时,宏作为预处理并不进行类型检查,而inline函数是要进行类型检查的,也就可以称作“更安全的宏”。

    内联函数和普通函数的区别:内联函数没有栈帧的开辟回退,一般我们直接把内联函数写在头文件中,include之后就可以使用,由于调用时直接代码展开所以我们根本不需要担心什么重定义的问题——它连符号都没有生成当然不会所谓重定义了。普通函数生成符号,内联函数不会生成符号。

    关于inline还需要注意的一点是,我们在使用它的时候往往是用来替换函数体非常小(1~5行代码)的函数的。这种情况下函数的堆栈开销相对函数体大小来说就非常大了,这种情况使用内联函数可以大大提高效率。相反如果是一个需要很多代码才能实现的函数,则不适合使用。一是此时函数堆栈调用开销与函数体相比已经是微不足道了,二是大量的代码直接展开的话会给调试带来很大的不便。三是如果代码体达到一个阈值,编译器会将它变成普通函数。

同时,递归函数不能声明为inline函数。说到底inline只是对编译器的建议,最终能否成功也不一定。同时,我们平常生成的都是debug版本,在这个版本下inline是不起作用的。只有生成release版时才会起作用。

    总结:C89没有,在调用点直接展开,不生成符号,没有栈帧的开辟回退,仅在Release版本下生效。一般写在头文件中。

函数重载

    C语言中产生函数符号的规则是根据名称产生,这也就注定了c语言不存在函数重载的概念。而C++生成函数符号则考虑了函数名、参数个数、参数类型。需要注意的是函数的返回值并不能作为函数重载的依据,也就是说int sum和double sum这两个函数是不能构成重载的!

    我们的函数重载也属于多态的一种,这就是所谓的静多态。

  • 静多态:函数重载,函数模板
  • 动多态(运行时的多态):继承中的多态(虚函数)

    使用重载的时候需要注意作用域问题,请看如下代码:

#include <iostream>
using namespace std;
bool compare(int a,int b)
{
return a > b;
}
bool  compare(double a,double b)
{
return a > b;
}
int main()
{
//bool compare(int a,int b);
compare(10,20);
compare(10.5,20.5);
return 0;
}
我在全局作用域定义了两个函数,它们由于参数类型不同可以构成重载。    此时main函数中调用则可以正确的调用到各自的函数。

    但是请看main函数中被注释掉的一句代码。如果我将它放出来,则会提出警告:将double类型转换成int类型可能会丢失数据。这就意味着我们编译器针对下面两句调用都调用了参数类型int的compare。由此可见,编译器调用函数时优先在局部作用域搜索,若搜索成功则全部按照该函数的标准调用。若未搜索到才在全局作用域进行搜索。     

    总结:C语言不存在函数重载,C++根据函数名参数个数参数类型判断重载,属于静多态,必须同一作用域下才叫重载。

 const

    这一部分非常重要。在我的另一篇博客“C语言的32个关键字”中对C语言中的const也有所讲解。当中提到了这么一个问题:C语言中被const修饰的变量不是常量,叫做常变量或者只读变量,这个常变量是无法当作数组下标的。然而在C++中const修饰的变量可以当作数组下标使用,成为了真正的常量。这就是C++对const的扩展。

    C语言中的const:被修饰后不能做左值,可以不初始化,但是之后没有机会再初始化。不可以当数组的下标,可以通过指针修改。简单来说,它和普通变量的区别只是不能做左值而已。其他地方都是一样的。

    C++中的const:真正的常量。定义的时候必须初始化,可以用作数组的下标。const在C++中的编译规则是替换(和宏很像),所以它被看作是真正的常量。也可以通过指针修改。需要注意的是,C++的指针有可能退化成C语言的指针。比如以下情况:

int b = 20;
const int a = b;
这时候的a就只是一个普通的C语言的const常变量了,已经无法当数组的下标了。(引用了一个编译阶段不确定的值)

const在生成符号时,是local符号。即在本文件中才可见。如果非要在别的文件中使用它的话,在文件头部声明:extern cosnt int data = 10;这样生成的符号就是global符号。

    总结:C中的const叫只读变量,只是无法做左值的变量;C++中的const是真正的常量,但也有可能退化成c语言的常量,默认生成local符号。

引用

    说到引用,我们第一反应就是想到了他的兄弟:指针。引用从底层来说和指针就是同一个东西,但是在编译器中它的特性和指针完全不同。

int a = 10;
int &b = a;
int *p = &a;
//b = 20;
//*p = 20;
首先定义一个变量a = 10,然后我们分别定义一个引用b以及一个指针p指向a。我们来转到反汇编看看底层的实现:

图片

    可以看到底层实现完全一致,取a的地址放入eax寄存器,再将eax中的值存入引用b/指针p的内存中。至此我们可以说(在底层)引用本质就是一个指针。了解了底层实现,我们回到编译器。我们看到对a的值的修改,指针p的做法是*p = 20;即进行解引用后替换值。底层实现:

图片

    再来看看引用修改:

图片

    我们看到修改a的值的方法也是一样的,也是解引用。只是我们在调用的时候有所不同:调用p时需要*p解引用,b则直接使用就可以。由此我们推断出:引用在直接使用时是指针解引用。p直接使用则是它自己的地址。这样我们也了解了,我们给引用开辟的这块内存是根本访问不到的。如果直接用就直接解引用了。即使打印&b,输出的也是a的地址。

    注:"*"的作用是引用指针指向的变量值,引用其实就是引用该变量的地址,“解”就是把该地址对应的东西解开,解出来,就像打开一个包裹一样,那就是该变量的值了,所以称为“解引用”。也就是说,解引用是返回内存地址中对应的对象。

    在此附上将指针转为引用的小技巧:

int *p = &a
/*我们将 引用符号移到左边 将 *替换即可:*/
int &p = a

    接下来看看如何创建数组的引用:

int array[10] = {0};//定义一个数组

    我们知道,array拿出来使用的话就是数组array的首元素地址。即是int *类型。那么&array是什么意思呢?int **类型,用来指向array[0]地址的一个地址吗?不要想当然了,&array是整个数组类型。

    那么要定义一个数组引用,按照上面的小诀窍,先来写写数组指针吧:

int (*q) [10] = &array;

    将右侧的&对左边的*进行覆盖:

int (&q)[10] = array;

    测试sizeof(q) = 10。我们成功创建了数组引用。经过上面的详解 ,我们知道了引用其实就是取地址。那么我们都知道一个立即数是没有地址的,即

int &b = 10;

    这样的代码是无法通过编译的。那如果你就是非要引用一个立即数,其实也不是没有办法:

const int &b  = 10;

    即将这个立即数用const修饰一下,就可以了。为什么呢?

    这时因为被const修饰的都会产生一个临时量来保存这个数据,自然就有地址可取了。

    总结:引用底层就是指针,使用时会直接解引用,可以配合const对一个立即数进行引用。

malloc,free && new,delete

    这个问题很有意思,也是重点需要关注的问题。malloc()和free()是C语言中动态申请内存和释放内存的标准库中的函数。而new和delete是C++运算符、关键字。new和delete底层其实还是调用了malloc和free。它们之间的区别有以下几个方面:

①:malloc和free是函数,new和delete是运算符。

②:malloc在分配内存前需要大小,new不需要。

    例如:

int *p1 = (int *)malloc(sizeof(int));
int *p2 = new int;     //int *p3 = new int(10);

    malloc时需要指定大小,还需要类型转换。new时不需要指定大小因为它可以从给出的类型判断,并且还可以同时赋初始值。

③:malloc不安全,需要手动类型转换,new不需要类型转换。

详见上一条。

④:free只释放空间,delete先调用析构函数再释放空间(如果需要)。

与第⑤条对应,如果使用了复杂类型,先析构再call operator delete回收内存。

⑤:new是先调用构造函数再申请空间(如果需要)。

与第④条对应,我们在调用new的时候(例如int *p2 = new int;这句代码 ),底层代码的实现是:首先push 4字节(int类型的大小),随后call   operator new函数分配了内存。由于我们这句代码并未涉及到复杂类型,如类类型,所以也就没有构造函数的调用。如下是operator new的源代码,也是new实现的重要函数:

void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{       // try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{       // report no memory
_THROW_NCEE(_XSTD bad_alloc, );
}
return (p);
}
我们可以看到,首先malloc(size)申请参数字节大小的内存。    如果失败(malloc失败返回0)则进入判断:    如果_callnewh(size)也失败的话,抛出bad_alloc异常。    _callnewh()这个函数是在查看new handler是否可用。    如果可用会释放一部分内存再返回到malloc处继续申请。    如果new handler不可用就会抛出异常。

⑥:内存不足(开辟失败)时处理方式不同。

malloc失败返回0,new失败抛出bad_alloc异常。 

⑦:new和malloc开辟内存的位置不同。

malloc开辟在堆区,new开辟在自由存储区域。 

⑧:new可以调用malloc(),但malloc不能调用new。

new就是用malloc()实现的,new是C++独有malloc当然无法调用。

作用域

    C语言中作用域只有两个:局部,全局。C++中则是有:局部作用域,类作用域,名字空间作用域三种。

    所谓名字空间就是namespace,我们定义一个名字空间就是定义一个新作用域。访问时需要以如下方式访问,以std为例:

std::cin<< "123" <<std::endl;

    例如我们有一个名字空间叫Myname,其中有一个变量叫做data。如果我们希望在其他地方使用data的话,需要在文件头声明:using Myname::data;这样一来data就使用的是Myname中的值了。可是这样每个符号我们都得声明岂不是累死?我们只要using namespace Myname;就可以将其中所有符号导入了。所以我们经常看到如下代码:

using namespace std;

二、C语言面试-指针和引用的使用场景?

先解决两个疑问

◆ 指针和引用的不同之处是什么?

◆ 何时用用指针?何时用引用?

指针和引用的不同之处

    看如下代码:

图片

    指针是用来表示内存地址的,而指针这个整数正是被指向的变量地址。

    而引用就是给变量重新起了一个名字,引用也就是“别名”。

不同之处

◆ 指针在声明时可以暂时不初始化,当然每次使用指针时都要做检查,以防出现空指针异常的问题0。而引用永远都不会为空,它一定得代表某个对象。

◆ 指针存放的是地址,可以被重新赋值。而引用总是指向它最初代表的那个对象。

指针和引用使用场合

    引用主要是作为函数的参数和返回值来使用的,看如下代码:

图片

    通过vec[3] =3可改变vector容器的值,这是因为[]操作符返回的就是引用。也就是为内部的变量取了一个别名,还能让[]操作符返回一个指针,即*vec[3]=3。实际上引用可以做的事情指针都可以做,那为什么还要使用引用?

总结

    用恰当的工具做恰如其分的工作。

    指针可以毫无约束的操作内存中的任何东西,功能十分强大,但也很危险,所以可以在恰当的时机使用引用。当你需要指向某个东西,绝不会让其指向其它东西,例如有些函数参数为了避免拷贝可以使用引用,或者实现一个操作符而其语法需求无法由指针达成,例如vec[3]=3,可以使用引用,而其它任何时候,都要使用指针。

三、C语言中return的各种用法

  按初学的理解,return的任务就是返回对应的参数,在外层函数中对这个参数做进一步处理。

    实际上return的用法不只这些。

为调用的函数返回参数值

    此类应用最为普遍,通常是在一个具有返回值的函数中,返回一个参数值,这个返回的参数可以是一个数、表达式。通常情况下,返回值是一个值,如果想返回多个值可以参考以下三点:

  • 设置全局变量,全局变量是定义在函数外的一个变量,其不属于任何一个函数,所以,虽然在调用函数中return返回一个值,但可以将其余的返回值赋值给全局变量,这样就做到了返回多个值的效果。
  • 使用数组名或指针作为函数形参,返回的是一个地址,主函数可以根据这个地址找到多个,要返回的数据。
  • 使用结构体作为函数形参,结构体中可以包含多种形式的变量,以结构体作为形参相当于将所有返回值打包在一起,一次返回。

提前结束函数

    由于return具有把程序流程从被调函数转向主调函数的作用,所以时候也会用来终止函数的调用,也就是说return是提前结束函数的唯一方法。因为函数一遇到return就立即返回,return后面的程序都不再执行,所以一个函数中可以有多个return,但只会执行一个。return后面可以跟一个参数作为返回值,也可以不跟参数,仅仅代表结束此函数。

返回一个函数

    如果return后面跟的是一个函数的话,就意味着,跳出此调用函数,并且跳出后执行return后的函数,然后继续在主函数中执行程序。

    有时利用return返回一个操作,比如在程序调试中出错后,需要return跳出错误函数并向串口打印出错误的原因,这个时候return后面的函数就用来向串口打印错误原因的。

四、C语言基础知识点汇总

C语言入门程序hello world

#include<stdio.h> 
int main()
{
/*在双引号中间输入Hello World*/
    printf("Hello World"); //在屏幕打印输出Hello World
return 0; 
}

    注:在最新的C标准中,main函数前的类型为int而不是void

C语言的具体结构

    简单来说,一个C程序就是由若干头文件和函数组成。

图片

    #include <stdio.h>就是一条预处理命令, 它的作用是通知C语言编译系统在对C程序进行正式编译之前需做一些预处理工作。

    函数就是实现代码逻辑的一个小的单元。

必不可少之主函数

    一个C程序有且只有一个主函数,即main函数。

图片

  • C程序就是执行主函数里的代码,也可以说这个主函数就是C语言中的唯一入口
  • 而main前面的int就是主函数的类型
  • printf()是格式输出函数,这里就记住它的功能就是在屏幕上输出指定的信息
  • return是函数的返回值,根据函数类型的不同,返回的值也是不同的
  • \n是转义字符中的换行符。(注意:C程序一定是从主函数开始执行的)

写代码的良好习惯

  • 一个说明或一个语句占一行,例如:包含头文件、一个可执行语句结束都需要换行
  • 函数体内的语句要有明显缩进,通常以按一下Tab键为一个缩进
  • 括号要成对写,如果需要删除的话也要成对删除
  • 当一句可执行语句结束的时候末尾需要有分号
  • 代码中所有符号均为英文半角符号

图片

程序解释——注释

    注释是写给程序员看的,不是写给电脑看的。

    C语言注释方法有两种:

多行注释:/* 注释内容 */


单行注释://注释一行

C标识符

    C语言规定,标识符可以是字母(A~Z,a~z)、数字(0~9)、下划线_组成的字符串,并且第一个字符必须是字母或下划线。在使用标识符时还有注意以下几点:

  • 标识符的长度最好不要超过8位,因为在某些版本的C中规定标识符前8位有效,当两个标识符前8位相同时,则被认为是同一个标识符
  • 标识符是严格区分大小写的。例如Imooc和imooc 是两个不同的标识符
  • 标识符最好选择有意义的英文单词组成做到"见名知意",不要使用中文
  • 标识符不能是C语言的关键字。想了解更多C语言关键字的知识

变量定义与赋值

    变量就是可以变化的量,而每个变量都会有一个名字(标识符)。变量占据内存中一定的存储单元。使用变量之前必须先定义变量,要区分变量名和变量值是两个不同的概念。

图片

    变量定义与赋值的一般形式如下。

图片

    注意:在定义中不允许连续赋值,如下操作是不合法的。

int a=b=c=5; //不合法的赋值操作

    变量的赋值分为两种方式:

  • 先声明再赋值
  • 声明的同时赋值

基本数据类型

    C语言中的数据类型如下图所示。

图片

    最常用的整型、实型与字符型(char,int,float,double):

图片

    整型数据是指不带小数的数字(int,short int,long int, unsigned int, unsigned short int,unsigned long int):

图片

    注意:

  • int short int long int是根据编译环境的不同,所取范围不同
  • 而其中short int和long int至少是表中所写范围, 但是int在表中是以16位编译环境写的取值范围
  • 另外 c语言int的取值范围在于他占用的字节数 ,不同的编译器,规定是不一样
  • ANSI标准定义int是占2个字节,TC是按ANSI标准的,它的int是占2个字节的。但是在VC里,一个int是占4个字节的

    浮点数据是指带小数的数字。

    生活中有很多信息适合使用浮点型数据来表示,比如:人的体重(单位:公斤)、商品价格、圆周率等等。

    因为精度的不同又分为3种(float,double,long double):

图片

格式化输出语句

    格式化输出语句,也可以说是占位输出,是将各种类型的数据按照格式化后的类型及指定的位置从计算机上显示。其格式为:

printf("输出格式符",输出项);

图片

    当输出语句中包含普通字符时,可以采用一下格式:

printf("普通字符输出格式符", 输出项);

图片

    注意:格式符的个数要与变量、常量或者表达式的个数相对应。

不可改变的常量

    在程序执行过程中,值不发生改变的量称为常量,比如:

图片

    在C语言中,可以用一个标识符来表示一个常量,常量在使用之前必须先定义,其一般形式为:

#include <stdio.h>
#define POCKETMONEY 10    //定义常量及常量值
int main()
{
// POCKETMONEY = 12;  //小明私自增加零花钱对吗?
printf("小明今天又得到%d元零花钱\n", POCKETMONEY);
return 0;  
}

    符号常量不可以被改变。

自动类型转换

    数据类型存在自动转换的情况。

    自动转换发生在不同数据类型运算时,在编译的时候自动完成。

图片

    char类型数据转换为int类型数据遵循ASCII码中的对应值。

    注意:

  • 字节小的可以向字节大的自动转换,但字节大的不能向字节小的自动转换
  • char可以转换为int,int可以转换为double,char可以转换为double

强制类型转换

    强制类型转换是通过定义类型转换运算来实现的。其一般形式为:

(数据类型) (表达式)

    其作用是把表达式的运算结果强制转换成类型说明符所表示的类型。在使用强制转换时应注意以下问题:

  • 数据类型和表达式都必须加括号, 如把(int)(x/2+y)写成(int)x/2+y则成了把x转换成int型之后再除2再与y相加了
  • 转换后不会改变原数据的类型及变量值,只在本次运算中临时性转换
  • 强制转换后的运算结果不遵循四舍五入原则

C语言基本运算符

    除法运算中注意:

  • 如果相除的两个数都是整数的话,则结果也为整数,小数部分省略,如8/3 = 2
  • 而两数中有一个为小数,结果则为小数,如:9.0/2 = 4.500000

    取余运算中注意:

  • 该运算只适合用两个整数进行取余运算,如:10%3 = 1
  • 运算后的符号取决于被模数的符号,如(-10)%3 = -1,而10%(-3) = 1

自增与自减运算符

    自增运算符为++,其功能是使变量的值自增1。

    自减运算符为--,其功能是使变量值自减1。

    它们经常使用在循环中,自增自减运算符有以下几种形式:

图片

赋值运算符

    C语言中赋值运算符分为简单赋值运算符和复合赋值运算符。

    例如:

a = 3;    //变量a并赋值为3
a += 5;   //这个算式就等价于a = a+5,将变量a和5相加之后再赋值给a

    注意:复合运算符中运算符和等号之间是不存在空格的。

关系运算符

    C语言中的关系运算符:

图片

    关系表达式的值是真和假,在C程序中用整数1和0表示。

    注意:>=, <=, ==, !=这种符号之间不能存在空格。

逻辑运算符

    C语言中的逻辑运算符:

图片

    逻辑运算的值也是有两种分别为真和假,C语言中用整型的1和0来表示。其求值规则如下:

  • 与运算 &&

    参与运算的两个变量都为真时,结果才为真,否则为假。例如:5>=5 && 7>5 ,运算结果为真。

  • 或运算 ||

    参与运算的两个变量只要有一个为真,结果就为真。两个量都为假时,结果为假。例如:5>=5||5>8,运算结果为真

  • 非运算!

    参与运算的变量为真时,结果为假;参与运算量为假时,结果为真。例如:!(5>8),运算结果为真

三目运算符

    C语言中的三目运算符,其格式为:

表达式1 ? 表达式2 : 表达式3;

    程序执行过程是:

    判断表达式1的值是否为真,如果是真的话执行表达式2,如果是假的话执行表达式3。

运算符大比拼之优先级比较

    各种运算符号的顺序:

图片

    优先级别为1的优先级最高,优先级别为10的优先级别最低。

分支结构之简单if语句

    C语言中的分支结构语句中的if条件语句。

    简单if语句的基本结构如下:

if(表达式)
{
  执行代码块;
}

    其语义是:如果表达式的值为真,则执行其后的语句,否则不执行该语句。

    注意:if()后面没有分号,直接写{}。

分支结构之简单if-else语句

    简单的if-else语句的基本结构:

图片

    语义是: 如果表达式的值为真,则执行代码块1,否则执行代码块2。

    注意:if()后面没有分号,直接写{},else后面也没有分号,直接写{}。

分支结构之多重if-else语句

    C语言中多重if-else语句,其结构如下:

图片

    语义是:依次判断表达式的值,当出现某个值为真时,则执行对应代码块,否则执行代码块n。

    注意:当某一条件为真的时候,则不会向下执行该分支结构的其他语句。

分支结构之嵌套if-else语句

    C语言中嵌套if-else语句。嵌套if-else语句的意思,就是在if-else语句中,再写if-else语句。其一般形式为:

图片

循环结构之while循环

    反复不停的执行某个动作就是江湖人称的循环 。

    C语言中有三种循环结构,先看一下C语言while循环的结构:

图片

    其中表达式表示循环条件,执行代码块为循环体。

    while语句的语义是:计算表达式的值,当值为真(非0)时, 执行循环体代码块。

  • while语句中的表达式一般是关系表达或逻辑表达式,当表达式的值为假时不执行循环体,反之则循环体一直执行
  • 一定要记着在循环体中改变循环变量的值,否则会出现死循环(无休止的执行)
  • 循环体如果包括有一个以上的语句,则必须用{}括起来,组成复合语句

循环结构之do-while循环

    C语言中的do-while循环,一般形式如下:

图片

    do-while循环语句的语义是:

    它先执行循环中的执行代码块,然后再判断while中表达式是否为真,如果为真则继续循环;如果为假,则终止循环。因此,do-while循环至少要执行一次循环语句。

    注意:使用do-while结构语句时,while括号后必须有分号。

循环结构之for循环

    C语言中for循环一般形式:

图片

    它的执行过程如下:

  • 执行表达式1,对循环变量做初始化
  • 判断表达式2,若其值为真(非0),则执行for循环体中执行代码块,然后向下执行;若其值为假(0),则结束循环
  • 执行表达式3,(i++)等对于循环变量进行操作的语句
  • 执行for循环中执行代码块后执行第二步;第一步初始化只会执行一次
  • 循环结束,程序继续向下执行

    注意:for循环中的两个分号一定要写

    在for循环中:

  • 表达式1是一个或多个赋值语句,它用来控制变量的初始值
  • 表达式2是一个关系表达式,它决定什么时候退出循环
  • 表达式3是循环变量的步进值,定义控制循环变量每循环一次后按什么方式变化
  • 这三部分之间用分号分开

结束语句之break语句

    需要中断循环,在C语言中可以使用break语句进行该操作,使用break语句时注意以下几点:

  • 在没有循环结构的情况下,break不能用在单独的if-else语句中。
  • 在多层循环中,一个break语句只跳出当前循环。

结束语句之continue语句

    continue语句的作用是结束本次循环开始执行下一次循环。

    break语句与continue语句的区别是:

    break是跳出当前整个循环,continue是结束本次循环开始下一次循环。

分支结构之switch语句

    switch语句结构如下:

图片

    switch语句时还应注意以下几点:

  • 在case后的各常量表达式的值不能相同,否则会出现错误
  • 在case子句后如果没有break;会一直往后执行**一直到遇到break;**才会跳出switch语句
  • switch后面的表达式语句只能是整型或者字符类型
  • 在case后,允许有多个语句,**可以不用{}**括起来
  • 各case和default子句的先后顺序可以变动,而不会影响程序执行结果
  • default子句可以省略不用

自定义函数

    自定义函数的一般形式:

图片

    注意:

  • []中包含的内容可以省略,数据类型说明省略时默认是int类型函数
  • 参数省略时表示该函数是无参函数,参数不省略表示该函数是有参函数

函数调用

    需要用到自定义函数的时候,就得调用它,在C语言中,函数调用的一般形式为:

函数名([参数]);

    注意:对无参函数调用的时候可以将[]包含的省略。

有参与无参

    在函数中不需要函数参数的称之为无参函数,在函数中需要函数参数的称之为有参函数。

    有参和无参函数的一般形式如下:

图片

    有参函数和无参函数的唯一区别在于:函数 () 中多了一个参数列表。

形参与实参

    函数的参数分为形参和实参两种。

    形参是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。

    实参是在调用时传递该函数的参数。

    函数的形参和实参具有以下特点:

  • 形参只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只有在函数内部有效
  • 实参可以是常量、变量、表达式、函数等
  • 在参数传递时,实参和形参在数量上,类型上,顺序上应严格一致,否则会发生类型不匹配的错误

图片

函数的返回值

    函数的返回值是指函数被调用之后,执行函数体中的程序段所取得的并返回给主调函数的值。

    函数的返回值要注意以下几点:

  • 函数的值只能通过return语句返回主调函数

    return语句的一般形式为:

return 表达式

    或者为:  

return (表达式);
  • 函数值的类型和函数定义中函数的类型应保持一致

    如果两者不一致,则以函数返回类型为准,自动进行类型转换。

  • 没有返回值的函数,返回类型为void

递归函数

    递归就是一个函数在它的函数体内调用它自身,递归函数必须有结束条件。

    执行递归函数将反复调用其自身,每调用一次就进入新的一层。

    一句话总结递归:自我调用且有完成状态。

    例题:

    有5个人坐在一起,问第5个人多少岁?他说比第4个人大2岁。问第4个人岁数,他说比第3个人大2岁。问第3个人,又说比第2人大两岁。问第2个人,说比第1个人大两岁。最后 问第1个人,他说是10岁。请问第5个人多大?

    程序分析:

    利用递归的方法,递归分为回推和递推两个阶段。要想知道第5个人岁数,需知道第4人的岁数,依次类推,推到第1人(10岁),再往回推。

#include <stdio.h> 
int dfs(int n) {
return n == 1 ? 10 : dfs(n - 1) + 2;
}
int main()
{ 
printf("第5个人的年龄是%d岁", dfs(5)); 
return 0;
}

局部与全局

    C语言中的变量,按作用域范围可分为两种,即局部变量和全局变量。

  • 局部变量也称为内部变量。局部变量是在函数内作定义说明的。其作用域仅限于函数内, 离开该函数后再使用这种变量是非法的。在复合语句中也可定义变量,其作用域只在复合语句范围内
  • 全局变量也称为外部变量,它是在函数外部定义的变量。它不属于哪一个函数,它属于一个源程序文件。其作用域是整个源程序

变量存储类别

    C语言根据变量的生存周期来划分,可以分为静态存储方式和动态存储方式。

    静态存储方式:是指在程序运行期间分配固定的存储空间的方式。静态存储区中存放了在整个程序执行过程中都存在的变量,如全局变量。

    动态存储方式:是指在程序运行期间根据需要进行动态的分配存储空间的方式。动态存储区中存放的变量是根据程序运行的需要而建立和释放的,通常包括:函数形式参数;自动变量;函数调用时的现场保护和返回地址等。

    C语言中存储类别又分为四类:

  • 自动(auto)
  • 静态(static)
  • 寄存器的(register)
  • 外部的(extern)

    用关键字auto定义的变量为自动变量,auto可以省略,auto不写则隐含定为“自动存储类别”,属于动态存储方式。如:

图片

    用static修饰的为静态变量,如果定义在函数内部的,称之为静态局部变量;如果定义在函数外部,称之为静态外部变量。如下为静态局部变量:

图片

    注意:静态局部变量属于静态存储类别,在静态存储区内分配存储单元,在程序整个运行期间都不释放;静态局部变量在编译时赋初值,即只赋初值一次;如果在定义局部变量时不赋初值的话,则对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符变量)。

    为了提高效率,C语言允许将局部变量得值放在CPU中的寄存器中,这种变量叫“寄存器变量”,用关键字register作声明。例如:

图片

    注意:只有局部自动变量和形式参数可以作为寄存器变量;一个计算机系统中的寄存器数目有限,不能定义任意多个寄存器变量;局部静态变量不能定义为寄存器变量。

    用extern声明的的变量是外部变量,外部变量的意义是某函数可以调用在该函数之后定义的变量。如:

图片

内部函数与外部函数

    在C语言中不能被其他源文件调用的函数称谓内部函数 ,内部函数由static关键字来定义,因此又被称谓静态函数,形式为:

static [数据类型] 函数名([参数])

    这里的static是对函数的作用范围的一个限定,限定该函数只能在其所处的源文件中使用,因此在不同文件中出现相同的函数名称的内部函数是没有问题的。

    在C语言中能被其他源文件调用的函数称谓外部函数 ,外部函数由extern关键字来定义,形式为:

extern [数据类型] 函数名([参数])

    C语言规定,在没有指定函数的作用范围时,系统会默认认为是外部函数,因此当需要定义外部函数时extern也可以省略。

数组

    程序中也需要容器,只不过该容器有点特殊,它在程序中是一块连续的,大小固定并且里面的数据类型一致的内存空间,它还有个好听的名字叫数组。可以将数组理解为大小固定,所放物品为同类的一个购物袋,在该购物袋中的物品是按一定顺序放置的。

    我们来看一下如何声明一个数组:

数据类型 数组名称[长度];

    数组只声明也不行啊,看一下数组是如何初始化的。说到初始化,C语言中的数组初始化是有三种形式的,分别是:

数据类型 数组名称[长度n] = {元素1,元素2…元素n};
数据类型 数组名称[] = {元素1,元素2…元素n};
数据类型 数组名称[长度n]; 数组名称[0] = 元素1; 数组名称[1] = 元素2; 数组名称[n-1] = 元素n;

    我们将数据放到数组中之后又如何获取数组中的元素呢?

    获取数组元素时:

数组名称[元素所对应下标];

    如:初始化一个数组 int arr[3] = {1,2,3}; 那么arr[0]就是元素1。

    注意:

  • 数组的下标均以0开始
  • 数组在初始化的时候,数组内元素的个数不能大于声明的数组长度
  • mtianyan: 如果采用第一种初始化方式,元素个数小于数组的长度时,多余的数组元素初始化为0
  • 在声明数组后没有进行初始化的时候,静态(static)和外部(extern)类型的数组元素初始化元素为0,自动(auto)类型的数组的元素初始化值不确定

数组的遍历

    数组就可以采用循环的方式将每个元素遍历出来,而不用人为的每次获取指定某个位置上的元素,例如我们用for循环遍历一个数组:

图片

    注意以下几点:

  • 最好避免出现数组越界访问,循环变量最好不要超出数组的长度
  • C语言的数组长度一经声明,长度就是固定,无法改变,并且C语言并不提供计算数组长度的方法

    由于C语言是没有检查数组长度改变或者数组越界的这个机制,可能会在编辑器中编译并通过,但是结果就不能肯定了,因此还是不要越界或者改变数组的长度。

    C语言获取数组长度:

int length = sizeof(arr)/sizeof(arr[0]);

数组作为函数参数

    数组可以由整个数组当作函数的参数,也可以由数组中的某个元素当作函数的参数。

    整个数组当作函数参数,即把数组名称传入函数中,例如:

图片

    数组中的元素当作函数参数,即把数组中的参数传入函数中,例如:

图片

    数组作为函数参数时注意以下事项:

  • 数组名作为函数实参传递时,函数定义处作为接收参数的数组类型形参既可以指定长度也可以不指定长度
  • 数组元素作为函数实参传递时,数组元素类型必须与形参数据类型一致

数组的应用-冒泡排序

    以升序排序为例冒泡排序的思想:相邻元素两两比较,将较大的数字放在后面,直到将所有数字全部排序。就像小学排队时按大小个排一样,将一个同学拉出来和后面的比比,如果高就放后面,一直把队伍排好。

#include <stdio.h>
int main()
{
double arr[]={1.78, 1.77, 1.82, 1.79, 1.85, 1.75, 1.86, 1.77, 1.81, 1.80};
int i,j;
printf("\n************排队前*************\n");
for(i=0;i<10;i++)
    {
if(i != 9)   
printf("%1.2f, ", arr[i]);  //%1.2f表示小数点前一位,小数点后精确到两位
else
printf("%1.2f", arr[i]);    //%1.2f表示小数点前一位,小数点后精确到两位
    }
for(i=8; i>=0; i--)
    {
for(j=0;j<=i;j++)
        {
if( arr[j]>arr[j+1])      //当前面的数比后面的数大时
            {
double temp;    //定义临时变量temp
                temp=arr[j];//将前面的数赋值给temp
                arr[j]=arr[j+1];             //前后之数颠倒位置
                arr[j+1]=temp;//将较大的数放在后面    
            }                 
        }                
    }
printf("\n************排队后*************\n");
for(i=0;i<10;i++)
    {
if(i != 9)   
printf("%1.2f, ", arr[i]);  //%1.2f表示小数点前一位,小数点后精确到两位     
else
printf("%1.2f", arr[i]);    //%1.2f表示小数点前一位,小数点后精确到两位
    }
return 0;    
}

数组的应用-数组查找功能

    当我们购物之后,拎着购物袋回到家,会一一检查购物袋中的物品看是否缺少或者都是想购之物。那么应用到程序中,可以使用数组查找功能,看看是否存在该数据,如果存在并返回该元素的下标。

#include <stdio.h>
int getIndex(int arr[5],int value)
{
int i;
int index;
for(i=0;i<5;i++)
    {
/* 请完善数组查询功能 */
if(arr[i]==value)
        {
index=i;
break;
        }  
index=-1;
    }
return index;
}
int main()
{
int arr[5]={3,12,9,8,6};
int value = 8;
int index = getIndex(arr,value);      //这里应该传什么参数呢?
if(index!=-1)
    {
printf("%d在数组中存在,下标为:%d\n",value,index);             
    }
else
    {
printf("%d在数组中不存在。\n",value);    
    }
return 0;    
}

字符串与数组

    C语言中,是没有办法直接定义字符串数据类型的,但是我们可以使用数组来定义我们所要的字符串。一般有以下两种格式:

char 字符串名称[长度] = “字符串值”;     //[]中的长度是可以省略不写的
char 字符串名称[长度] = {‘字符1’,‘字符2’,…,‘字符n’,’\0’}; //最后一个元素必须是’\0’,’\0’表示字符串的结束标志

    注意:在输出字符串的时候可使用如下语句。

printf(“%s”,字符数组名字);
puts(字符数组名字);

字符串函数

    常用的字符串函数如下(strlen,strcmp,strcpy,strcat,atoi):

    使用字符串函数注意以下事项:

    strlen()获取字符串的长度,在字符串长度中是不包括‘\0’而且汉字和字母的长度是不一样的。比如:

图片

    strcmp()在比较的时候会把字符串先转换成ASCII码再进行比较,返回的结果为0表示s1和s2的ASCII码相等,返回结果为1表示s1比s2的ASCII码大,返回结果为-1表示s1比s2的ASCII码小,例如:

图片

    strcpy()拷贝之后会覆盖原来字符串且不能对字符串常量进行拷贝,比如:

图片

    strcat在使用时s1与s2指的内存空间不能重叠,且s1要有足够的空间来容纳要复制的字符串,如:

图片

多维数组

    多维数组的定义格式是:

数据类型 数组名称[常量表达式1][常量表达式2]…[常量表达式n];

图片

    定义了一个名称为num,数据类型为int的二维数组。其中第一个[3]表示第一维下标的长度,就像购物时分类存放的购物;第二个[3]表示第二维下标的长度,就像每个购物袋中的元素。

图片

    多维数组的初始化与一维数组的初始化类似也是分两种:

数据类型 数组名称[常量表达式1][常量表达式2]…[常量表达式n] = {{值1,…,值n},{值1,…,值n},…,{值1,…,值n}};
数据类型 数组名称[常量表达式1][常量表达式2]…[常量表达式n]; 数组名称[下标1][下标2]…[下标n] = 值;

    多维数组初始化要注意以下事项:

  • 采用第一种始化时数组声明必须指定列的维数。mtianyan: 因为系统会根据数组中元素的总个数来分配空间,当知道元素总个数以及列的维数后,会直接计算出行的维数
  • 采用第二种初始化时数组声明必须同时指定行和列的维数

    二维数组定义的时候,可以不指定行的数量,但是必须指定列的数量。

多维数组的遍历

    多维数组也是存在遍历的,和一维数组遍历一样,也是需要用到循环。不一样的就是多维数组需要采用嵌套循环。

    注意:多维数组的每一维下标均不能越界

    实例代码:

#include <stdio.h>
#define N 10
//打印分数 
void printScore(int score[])
{
int i;
printf("\n");
for(i=0;i<N;i++)
  {
printf("%d ",score[i]);               
  }
printf("\n");     
}
//计算考试总分 
int getTotalScore(int score[])
{
int sum = 0;
int i;
for(i=0;i<N;i++)
  {
    sum+=score[i];                
  } 
return sum;
}
//计算平均分 
int getAvgScore(int score[])
{
return getTotalScore(score)/N;   
}
//计算最高分 
int getMax(int score[])
{
int max = -1;
int i;
for(i=0;i<N;i++)
  {
if(score[i]>max)
    {
      max = score[i];              
    }                
  } 
return max;
}
//计算最低分 
int getMin(int score[])
{
int min =100;
int i;
for(i=0;i<N;i++)
  {
if(score[i]< min)
    {
      min = score[i];              
    }                
  } 
return min;
}
//分数降序排序 
void sort(int score[])
{
int i,j;
for(i=N-2;i>=0;i--)
  {
for(j=0;j<=i;j++)
    {
if(score[j]<score[j+1])
      {
int temp;
        temp = score[j];
        score[j] = score[j+1]; 
        score[j+1]=temp;                  
      }                 
    }                   
  }
  printScore(score);     
}
int main()
{
int score[N]={67,98,75,63,82,79,81,91,66,84};
int sum,avg,max,min;
  sum = getTotalScore(score);
  avg = getAvgScore(score);
  max = getMax(score);
  min = getMin(score);
printf("总分是:%d\n",sum);
printf("平均分是:%d\n",avg);
printf("最高分是:%d\n",max);
printf("最低分是:%d\n",min);
printf("----------成绩排名---------\n");
  sort(score);
return 0;    
}

五、C语言的编译过程

  C语言的编译链接过程要把我们编写的一个C程序源代码,转换成可以在硬件上运行的程序(可执行代码),需要进行编译和链接。过程图解如下:

图片

    本文讲解C语言编译过程中所做的工作,对我们理解头文件、库等的工作过程是有帮助的。而且清楚的了解编译链接过程还对我们在编程时定位错误,以及编程时尽量调动编译器的检测错误会有很大的帮助的。

编译

    编译是读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,源文件的编译过程包含预处理与编译优化两个主要阶段。

预处理

    第一个阶段是预处理阶段,在正式的编译阶段之前进行。预处理阶段将根据已放置在文件中的预处理指令来修改源文件的内容。如#include指令就是一个预处理指令,它把头文件的内容添加到.cpp文件中。这个在编译之前修改源文件的方式提供了很大的灵活性,以适应不同的计算机和操作系统环境的限制。一个环境需要的代码跟另一个环境所需的代码可能有所不同,因为可用的硬件或操作系统是不同的。在许多情况下,可以把用于不同环境的代码放在同一个文件中,再在预处理阶段修改代码,使之适应当前的环境。

    主要是以下几方面的处理:

  • 宏定义指令,如 #define a  b

    对于这种伪指令,预编译所要做的是将程序中的所有a用b替换,但作为字符串常量的 a则不被替换。还有 #undef,则将取消对某个宏的定义,使以后该串的出现不再被替换。

  • 条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif等。

    这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。

  • 头文件包含指令,如#include "FileName"或者#include <FileName>等。

    在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在 /usr/include目录下。在程序中#include它们要使用尖括号(< >)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。

  • 特殊符号,预编译程序可以识别一些特殊的符号。

    例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

    预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。

编译、优化

    第二个阶段编译、优化阶段,经过预编译得到的输出文件中,只有常量;如数字、字符串、变量的定义,以及c语言的关键字,如main,if,else,for,while,{,}, +,-,*,\等等。

    编译程序所要做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。

    优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。

    对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。

    后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。

汇编

    汇编实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。目标文件由段组成。通常一个目标文件中至少有两个段:

    代码段:该段中所包含的主要是程序的指令。

    该段一般是可读和可执行的,但一般却不可写。

    数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

    UNIX环境下主要有三种类型的目标文件:

  • 可重定位文件

    其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。

  • 共享的目标文件

    这种文件存放了适合于在两种上下文里链接的代码和数据。第一种是链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个 目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。

  • 可执行文件

    它包含了一个可以被操作系统创建一个进程来执行之的文件。汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。

链接过程

    由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。

    例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。

    链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够诶操作系统装入执行的统一整体。

    根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种:

  • 静态链接

    在这种链接方式下,函数的代码将从其所在的静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。

  • 动态链接

    在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所做的,只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。

    对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

六、C语言:总结的文件操作

C 语言把文件看作是一个字符(字节)的序列,即由一个一个字符(字节)的数据顺序组成。

根据数据的组织形式,可分为 ASCⅡ 文件和二进制文件。文件的操作包括:文件的打开、文件的关闭、文件的读写操作、文件状态检查以及文件的定位等。

1 文件的打开

1.1 函数原型

FILE *fopen(char *pname,char *mode)

1.2 功能说明

按照 mode 规定的方式,打开由 pname指定的文件。若找不到由 pname 指定的相应文件,就按以下方式之一处理:

  • 此时如 mode 规定按写方式打开文件,就按由pname 指定的名字建立一个新文件;
  • 此时如 mode 规定按读方式打开文件,就会产生一个错误;

打开文件的作用:

  • 分配给打开文件一个FILE 类型的文件结构体变量,并将有关信息填入文件结构体变量;
  • 开辟一个缓冲区;
  • 调用操作系统提供的打开文件或建立新文件功能,打开或建立指定文件;

FILE *:指出 fopen 是一个返回文件类型的指针函数;

1.3 参数说明

pname:是一个字符指针,它将指向要打开或建立的文件的文件名字符串。mode:是一个指向文件处理方式字符串的字符指针。

1.4 返回值

正常返回:被打开文件的文件指针。
异常返回:NULL,表示打开操作不成功。

//定义一个名叫fp文件指针
FILE *fp;
//判断按读方式打开一个名叫test的文件是否失败
if((fp=fopen("test","r")) == NULL)//打开操作不成功
{
    printf("The file can not be opened.\n");     
    exit(1);//结束程序的执行
}

要说明的是:C 语言将计算机的输入输出设备都看作是文件。例如,键盘文件、屏幕文件等。

ANSI C 标准规定,在执行程序时系统先自动打开键盘、屏幕、错误三个文件。这三个文件的文件指针分别是:标准输入 stdin、标准输出 stdout 和标准出错 stderr。

2 文件的关闭

2.1 函数原型

int fclose(FILE *fp);

2.2 功能说明

关闭由 fp 指出的文件。此时调用操作系统提供的文件关闭功能,关闭由 fp->fd 指出的文件;释放由 fp 指出的文件类型结构体变量;返回操作结果,即 0 或 EOF。

2.3 参数说明

fp:一个已打开文件的文件指针。

2.4 返回值

正常返回:0。
异常返回:EOF,表示文件在关闭时发生错误。

int n=fclose(fp);

3 文件的读写操作

3.1 从文件中读取一个字符

3.1.1 函数原型

int fgetc(FILE *fp);

3.1.2 功能说明

从fp所指文件中读取一个字符。

3.1.3 参数说明

fp:这是个文件指针,它指出要从中读取字符的文件。

3.1.4返回值

正常返回:返回读取字符的代码。
非正常返回:返回 EOF。例如,要从"写打开"文件中读取一个字符时,会发生错误而返回一个 EOF。

显示指定文件的内容:

//程序名为:display.c
//执行时可用:display filename1 形式的命令行运行。显示文件filename1中的内容。例如,执行命令行display display.c将在屏幕上显示display的原代码。


//File display program.
#include <stdio.h>
void main(int argc,char *argv[]) //命令行参数
{
    int ch;//定义文件类型指针
    FILE *fp;//判断命令行是否正确
    if(argc!=2)
    {
        printf("Error format,Usage: display filename1\n");
        return; //键入了错误的命令行,结束程序的执行
    }
    //按读方式打开由argv[1]指出的文件
    if((fp=fopen(argv[1],"r"))==NULL)
    {
        printf("The file <%s> can not be opened.\n",argv[1]);//打开操作不成功
        return;//结束程序的执行
    }
    //成功打开了argv[1]所指文件
    ch=fgetc(fp); //从fp所指文件的当前指针位置读取一个字符
    while(ch!=EOF) //判断刚读取的字符是否是文件结束符
    {
        putchar(ch); //若不是结束符,将它输出到屏幕上显示
        ch=fgetc(fp); //继续从fp所指文件中读取下一个字符
    } //完成将fp所指文件的内容输出到屏幕上显示
    fclose(fp); //关闭fp所指文件
}

3.2 写一个字符到文件中去3.2.1 函数原型

int fputc(int ch,FILE *fp)

3.2.2 功能说明

把 ch中的字符写入由 fp 指出的文件中去。

3.2.3 参数说明

ch:是一个整型变量,内存要写到文件中的字符(C 语言中整型量和字符量可以通用)。fp:这是个文件指针,指出要在其中写入字符的文件。

3.2.4 返回值

正常返回:要写入字符的代码。
非正常返回:返回 EOF。例如,要往"读打开"文件中写一个字符时,会发生错误而返回一个EOF。

将一个文件的内容复制到另一个文件中去:

//程序名为:copyfile.c
//执行时可用:copyfile filename1 filename2形式的命令行运行,将文件filename1中的内容复制到文件filename2中去。
//file copy program.
#include <stdio.h>
void main(int argc,char *argv[]) //命令行参数
{
    int ch;
    FILE *in,*out; //定义in和out两个文件类型指针
    if(argc!=3) //判断命令行是否正确
    {
        printf("Error in format,Usage: copyfile filename1 filename2\n");
        return; //命令行错,结束程序的执行
    }
    //按读方式打开由argv[1]指出的文件
    if((in=fopen(argv[1],"r"))==NULL)
    {
        printf("The file <%s> can not be opened.\n",argv[1]);
        return; //打开失败,结束程序的执行
    }
    //成功打开了argv[1]所指文件,再
    //按写方式打开由argv[2]指出的文件
    if((out=fopen(argv[2],"w"))==NULL)
    {
        printf("The file %s can not be opened.\n",argv[2]);
        return; //打开失败,结束程序的执行
    }
    //成功打开了argv[2]所指文件
    ch=fgetc(in); //从in所指文件的当前指针位置读取一个字符
    while(ch!=EOF) //判断刚读取的字符是否是文件结束符
    {
        fputc(ch,out); //若不是结束符,将它写入out所指文件
        ch=fgetc(in); //继续从in所指文件中读取下一个字符
    } //完成将in所指文件的内容写入(复制)到out所指文件中
    fclose(in); //关闭in所指文件
    fclose(out); //关闭out所指文件
}

按十进制和字符显示文件代码,若遇不可示字符就用井号"#"字符代替之。

//程序名为:dumpf.c
//执行时可用:dumpf filename1 形式的命令行运行。
// File dump program.
#include <stdio.h>
void main(int argc,char *argv[])
{
    char str[9];
    int ch,count,i;
    FILE *fp;
    if(argc!=2)
    {
        printf("Error format,Usage: dumpf filename\n");
        return;
    }
    if((fp=fopen(argv[1],"r"))==NULL)
    {
        printf("The file %s can not be opened.\n",argv[1]);
        return;
    }
    count=0;
    do{
        i=0;
        //按八进制输出第一列,作为一行八个字节的首地址
        printf("%06o: ",count*8);
        do{
            // 从打开的文件中读取一个字符
            ch=fgetc(fp);
            // 按十进制方式输出这个字符的ASCII码
            printf("%4d",ch);
            // 如果是不可示字符就用"#"字符代替
            if(ch<' '||ch>'~') str[i]='#';
            // 如果是可示字符,就将它存入数组str以便形成字符串
            else str[i]=ch;
            // 保证每一行输出八个字符
            if(++i==8) break;
        }while(ch!=EOF); // 遇到文件尾标志,结束读文件操作
        str[i]='\0'; // 在数组str加字符串结束标志
        for(;i<8;i++) printf(" "); // 一行不足八个字符用空格填充
        printf(" %s\n",str); // 输出字符串
        count++; // 准备输出下一行
    }while(ch!=EOF); // 直到文件结束
    fclose(fp); // 关闭fp所指文件
}

3.3 从文件中读取一个字符串3.3.1 函数原型

char *fgets(char *str,int n,FILE *fp)

3.3.2 功能说明

从由 fp 指出的文件中读取 n-1 个字符,并把它们存放到由 str 指出的字符数组中去,最后加上一个字符串结束符 '\0'。

3.3.3 参数说明

str:接收字符串的内存地址,可以是数组名,也可以是指针。
n:指出要读取字符的个数。
fp:这是个文件指针,指出要从中读取字符的文件。

3.3.4 返回值

正常返回:返回字符串的内存首地址,即 str 的值。
非正常返回:返回一个 NULL 值,此时应当用 feof() 或 ferror() 函数来判别是读取到了文件尾,还是发生了错误。例如,要从"写打开"文件中读取字符串,将发生错误而返回一个 NULL 值。

3.4 写一个字符串到文件中去

3.4.1函数原型

int fputs(char *str,FILE *fp)

3.4.2 功能说明

把由 str 指出的字符串写入到 fp 所指的文件中去。

3.4.3 参数说明

str:指出要写到文件中去的字符串。fp:这是个文件指针,指出字符串要写入其中的文件。

3.4.4 返回值

正常返回:写入文件的字符个数,即字符串的长度。
非正常返回:返回一个 NULL 值,此时应当用 feof() 或 ferror( )函数来判别是读取到了文件尾,还是发生了错误。例如,要往一个"读打开" 文件中写字符串时,会发生错误而返回一个NULL值。

将一个文件的内容附加到另一个文件中去。

//程序名:linkfile.c
//执行时可用:linkfile filename1 filename2形式的命令行运行,将文件filename2的内容附加在文件filename1之后。
// file linked program.
#include <stdio.h>
#define SIZE 512
void main(int argc,char *argv[])
{
    char buffer[SIZE];
    FILE *fp1,*fp2;
    if(argc!=3)
    {
        printf("Usage: linkfile filename1 filename2\n");
        return;
    }
    // 按追加方式打开argv[1] 所指文件
    if((fp1=fopen(argv[1],"a"))==NULL)
    {
        printf("The file %s can not be opened.\n",argv[1]);
        return;
    }
    if((fp2=fopen(argv[2],"r"))==NULL)
    {
        printf("The file %s can not be opened.\n",argv[2]);
        return;
    }
    // 读入一行立即写出,直到文件结束
    while(fgets(buffer,SIZE,fp1)!=NULL)
        printf("%s\n",buffer);
    while(fgets(buffer,SIZE,fp2)!=NULL)
        fputs(buffer,fp1);
    fclose(fp1);
    fclose(fp2);
    if((fp1=fopen(argv[1],"r"))==NULL)
    {
        printf("The file %s can not be opened.\n",argv[1]);
        return;
    }
    while(fgets(buffer,SIZE,fp1)!=NULL)
        printf("%s\n",buffer);
    fclose(fp1);
}

3.5 往文件中写格式化数据3.5.1 函数原型

int fprintf(FILE *fp,char *format,arg_list)

3.5.2 功能说明

将变量表列(arg_list)中的数据,按照 format 指出的格式,写入由 fp 指定的文件。fprintf() 函数与 printf() 函数的功能相同,只是 printf() 函数是将数据写入屏幕文件(stdout)。

3.5.3 参数说明

fp:这是个文件指针,指出要将数据写入的文件。
format:这是个指向字符串的字符指针,字符串中含有要写出数据的格式,所以该字符串成为格式串。格式串描述的规则与 printf() 函数中的格式串相同。
arg_list:是要写入文件的变量表列,各变量之间用逗号分隔。

//存储文件名:save.txt
//程序代码如下:
// file display program.
#include <stdio.h>
void main()
{
    char name[10];
    int nAge,nClass;
    long number;
    FILE *fp;
    if((fp=fopen("student.txt","w"))==NULL)
    {
        printf("The file %s can not be opened.\n","student.txt");
        return;
    }
    fscanf(stdin,"%s %d %d %ld",name,&nClass,&nAge,&number);
    fprintf(fp,"%s %5d %4d %8ld",name,nClass,nAge,number);
    fclose(fp);
    if((fp=fopen("student.txt","r"))==NULL)
    {
        printf("The file %s can not be opened.\n","student.txt");
        return;
    }
    fscanf(fp,"%s %d %d %ld",name,&nClass,&nAge,&number);
    printf("name nClass nAge number\n");
    fprintf(stdout,"%-10s%-8d%-6d%-8ld\n",name,nClass,nAge,number);
    fclose(fp);
}

3.6 以二进制形式读取文件中的数据3.6.1 函数原型

int fread(void *buffer,unsigned sife,unsigned count,FILE *fp)

3.6.2 功能说明

从由 fp 指定的文件中,按二进制形式将 sife*count 个数据读到由 buffer 指出的数据区中。

3.6.3 参数说明

buffer:这是一个 void 型指针,指出要将读入数据存放在其中的存储区首地址。sife:指出一个数据块的字节数,即一个数据块的大小尺寸。
count:指出一次读入多少个数据块(sife)。
fp:这是个文件指针,指出要从其中读出数据的文件。

3.6.4 返回值

正常返回:实际读取数据块的个数,即 count。
异常返回:如果文件中剩下的数据块个数少于参数中 count 指出的个数,或者发生了错误,返回 0 值。此时可以用feof() 和 ferror() 来判定到底出现了什么情况。

3.7 以二进制形式写数据到文件中去

3.7.1 函数原型

int fwrite(void *buffer,unsigned sife,unsigned count,FILE *fp)

3.7.2 功能说明

按二进制形式,将由 buffer 指定的数据缓冲区内的 sife*count 个数据写入由 fp 指定的文件中去。

3.7.3 参数说明

buffer:这是一个 void型指针,指出要将其中数据输出到文件的缓冲区首地址。
sife:指出一个数据块的字节数,即一个数据块的大小尺寸。
count:一次输出多少个数据块(sife)。
fp:这是个文件指针,指出要从其中读出数据的文件。

3.7.4 返回值

正常返回:实际输出数据块的个数,即 count。
异常返回:返回0值,表示输出结束或发生了错误。

#include <stdio.h>
#define SIZE 4
struct worker
{ int number;
    char name[20];
    int age;
};
void main()
{
    struct worker wk;
    int n;
    FILE *in,*out;
    if((in=fopen("file1.txt","rb"))==NULL)
    {
        printf("The file %s can not be opened.\n","file1.txt");
        return;
    }
    if((out=fopen("file2.txt","wb"))==NULL)
    {
        printf("The file %s can not be opened.\n","file2.txt");
        return;
    }
    while(fread(&wk,sizeof(struct worker),1,in)==1)
        fwrite(&wk,sizeof(struct worker),1,out);
    fclose(in);
    fclose(out);
}

3.8 以二进制形式读取一个整数

3.8.1 函数原型

int getw(FILE *fp)

3.8.2 功能说明

从由 fp 指定的文件中,以二进制形式读取一个整数。

3.8.3 参数说明

fp:是文件指针。

3.8.4 返回值

正常返回:所读取整数的值。
异常返回:返回 EOF,即 -1。由于读取的整数值有可能是 -1,所以必须用 feof() 或 ferror() 来判断是到了文件结束,还是出现了一个出错。

#include <stdio.h>
void main(int argc,char *argv[])
{
    int i,sum=0;
    FILE *fp;
    if(argc!=2)
    {
        printf("Command error,Usage: readfile filename\n");
        exit(1);
    }
    if(!(fp=fopen(argv[1],"rb")))
    {
        printf("The file %s can not be opened.\n",argv[1]);
        exit(1);
    }
    for(i=1;i<=10;i++) sum+=getw(fp);
    printf("The sum is %d\n",sum);
    fclose(fp);
}

3.9 以二进制形式存贮一个整数3.9.1 函数原型

int putw(int n,FILE *fp)

3.9.2 功能说明

以二进制形式把由变量 n 指出的整数值存放到由 fp 指定的文件中。

3.9.3 参数说明

n:要存入文件的整数。
fp:是文件指针。

3.9.4 返回值

正常返回:所输出的整数值。
异常返回:返回 EOF,即 -1。由于输出的整数值有可能是 -1,所以必须用 feof() 或 ferror() 来判断是到了文件结束,还是出现了一个出错。

#include <stdio.h>
void main(int argc,char *argv[])
{
    int i;
    FILE *fp;
    if(argc!=2)
    {
        printf("Command error,Usage: writefile filename\n");
        return;
    }


    if(!(fp=fopen(argv[1],"wb")))
    {
        printf("The file %s can not be opened.\n",argv[1]);
        return;
    }
    for(i=1;i<=10;i++) printf("%d\n", putw(i,fp));
    fclose(fp);
}

4 文件状态检查

4.1 文件结束

4.1.1 函数原型

int feof(FILE *fp)

4.1.2 功能说明

该函数用来判断文件是否结束。

4.1.3 参数说明

fp:文件指针。

4.1.4 返回值

0:假值,表示文件未结束。
1:真值,表示文件结束。

#include <stdio.h>
void main(int argc,char *argv[])
{
    FILE *in,*out;
    char ch;
    if(argc!=3)
    {
        printf("Usage: copyfile filename1 filename2\n");
        return;
    }
    if((in=fopen(argv[1],"rb"))==NULL)
    {
        printf("The file %s can not be opened.\n",argv[1]);
        return;
    }
    if((out=fopen(argv[2],"wb"))==NULL)
    {
        printf("The file %s can not be opened.\n",argv[2]);
        return;
    }
    while(!feof(in))
    {
        ch=fgetc(in);
        if(ferror(in))
        {
            printf("read error!\n");
            clearerr(in);
        }
        else
        {
            fputc(ch,out);
            if(ferror(out))
            {
                printf("write error!\n");
                clearerr(out);
            }
        }
    }
    fclose(in);
    fclose(out);
}

4.2 文件读/写出错4.2.1 函数原型

int ferror(FILE *fp)

4.2.2 功能说明

检查由 fp 指定的文件在读写时是否出错。

4.2.3 参数说明

fp:文件指针。

4.2.4 返回值

0:假值,表示无错误。
1:真值,表示出错。

4.3 清除文件错误标志

4.3.1 函数原型

void clearerr(FILE *fp)

4.3.2 功能说明

清除由 fp 指定文件的错误标志。

4.3.3 参数说明

fp:文件指针。

4.3.4 返回值

无。

#include <stdio.h>
void main(int argc,char *argv[])
{
    FILE *in,*out;
    char ch;
    if(argc!=3)
    {
        printf("Usage: copyfile filename1 filename2\n");
        return;
    }
    if((in=fopen(argv[1],"rb"))==NULL)
    {
        printf("The file %s can not be opened.\n",argv[1]);
        return;
    }
    if((out=fopen(argv[2],"wb"))==NULL)
    {
        printf("The file %s can not be opened.\n",argv[2]);
        return;
    }
    while(!feof(in))
    {
        ch=fgetc(in);
        if(ferror(in))
        {
            printf("read error!\n");
            clearerr(in);
        }
        else
        {
            fputc(ch,out);
            if(ferror(out))
            {
                printf("write error!\n");
                clearerr(out);
            }
        }
    }
    fclose(in);
    fclose(out);
}

4.4 了解文件指针的当前位置4.4.1 函数原型

long ftell(FILE *fp)

4.4.2 功能说明

取得由 fp 指定文件的当前读/写位置,该位置值用相对于文件开头的位移量来表示。

4.4.3参数说明

fp:文件指针。

4.4.4 返回值

正常返回:位移量(这是个长整数)。
异常返回:-1,表示出错。

5 文件定位

5.1 反绕

5.1.1 函数原型

void rewind(FILE *fp)

5.1.2 功能说明

使由文件指针 fp 指定的文件的位置指针重新指向文件的开头位置。

5.1.3 参数说明

fp:文件指针。

5.1.4返回值

无。

#include <stdio.h>
void main()
{
    FILE *in,*out;
    in=fopen("filename1","r");
    out=fopen("filename2","w");
    while(!feof(in)) fputc(fgetc(in),out);
    rewind(out);
    while(!feof(in)) putchar(fgetc(in));
    fclose(in);
    fclose(out);
}

5.2 随机定位5.2.1 函数原型

int fseek(FILE *fp,long offset,int base)

5.2.2 功能说明

使文件指针 fp 移到基于 base 的相对位置 offset 处。

5.2.3 参数说明

fp:文件指针。
offset:相对 base 的字节位移量。这是个长整数,用以支持大于 64KB 的文件。
base:文件位置指针移动的基准位置,是计算文件位置指针位移的基点。ANSI C 定义了 base 的可能取值,以及这些取值的符号常量。

5.2.4 返回值

正常返回:当前指针位置。
异常返回:-1,表示定位操作出错。

#include <stdio.h>
#include <string.h>
struct std_type
{
    int num;
    char name[20];
    int age;
    char class;
}stud;
int cstufile()
{
    int i;
    FILE *fp;
    if((fp=fopen("stufile","wb"))==NULL)
    {
        printf("The file can't be opened for write.\n");
        return 0;
    }
    for(i=1;i<=100;i++)
    {
        stud.num=i;
        strcpy(stud.name,"aaaa");
        stud.age=17;
        stud.class='8';
        fwrite(&stud,sizeof(struct std_type),1,fp);
    }
    fclose(fp);
    return 1;
}
void main()
{
    int n;
    FILE *fp;
    if(cstufile()==0) return;
    if((fp=fopen("stufile","rb"))==NULL)
    {
        printf("The file can not be opened.\n");
        return;
    }
    for(n=0;n<100;n+=2)
    {
        fseek(fp,n*sizeof(struct std_type),SEEK_SET);
        fread(&stud,sizeof(struct std_type),1,fp);
        printf("%10d%20s%10d%4c\n",stud.num,stud.name,stud.age,stud.class);
    }
    fclose(fp);
}

5.3 关于exit()函数

5.3.1 函数原型

void exit(int status)

5.3.2 功能说明

exit() 函数使程序立即终止执行,同时将缓冲区中剩余的数据输出并关闭所有已经打开的文件。

5.3.3 参数说明

status:为 0 值表示程序正常终止,为非 0 值表示一个定义错误。

5.3.4 返回值

无。

5.4 关于feof()函数

5.4.1 函数原型

int feof(FILE *fp)

5.4.2 功能说明

在文本文件(ASCII 文件)中可以用值为 -1 的符号常量 EOF 来作为文件的结束符。但是在二进制文件中 -1 往往可能是一个有意义的数据,因此不能用它 来作为文件的结束标志。为了能有效判别文件是否结束,ANSI C 提供了标准函数 feof(),用来识别文件是否结束。

5.4.3 参数说明

fp:文件指针。

5.4.4 返回值

返回为非 0 值:已到文件尾。返回为 0 值:表示还未到文件尾。

七、避坑:C语言的短路现象

C语言短路现象算是C语言的基础吧,不过有时候代码写得不规范也容易引入一些bug,所以这些操作在工程师实践中尽量少用。

虽然下面找的例子比较简单,但如果后面是其他表达式,或许你并不会那么容易分辨。并且,你可能会斩钉截铁的说,我绝对不会这样编写我的代码,但你确定的同事不会这样?你的前同事不会?

好了,下面的案例来源于网络,仅供参考:

避坑1

短路现象:

a && b && c
 
//只有a为真(非0)才需要判断b的值;
//只有a和b都为真,才需要判断c的值。

举例:

//求最终a、b、c、d的值:


main()
{
 int a,b,c,d;
 
 a = 0; 
 b = 1; 
 c = 2;
 d = a++ && b++ && --c;
 
 printf("a=%d b=%d c=%d d=%d\n",a,b,c,d);
}


//因为a++是先判断a的值再自加,而a初始值为0, 所以(a++)为假,
//由短路现象可知&&后面式子b++和--c就都不会执行;
//对于赋值语句,是先将a的值赋值给d,然后再自加,
//所以d的值为0,a最终为1。

执行结果:

图片

避坑2

短路现象:

a || b || c


//只要a为真(非0)就不必判断b和c;
//只有a为假,才需要判断b的值;
//只有a和b都为假,才有必要判断c的值。

举例:

//求最终a、b、c、d的值:


main()
{
 int a,b,c,d;
 
 a = 0; 
 b = 1; 
 c = 2;
 d = a++ || b++ || --c;
 
 printf("a=%d b=%d c=%d d=%d\n",a,b,c,d);
}


//因为a++是先判断a的值再自加,而a初始值为0, 所以(a++)为假,
//由短路现象可知,还需要继续判断 || 后面的表达式b++,
//b++要先判断b的值,b为1,所以b++为真,
//由短路现象可知,后面的式子--c就不再执行;
//对于赋值语句,不再是将a的值赋值给d,而是将b先赋值给d然后a和b再自加,
//所以d的值为1,a最终为1,b最终为2。

执行结果:

图片

八、C语言中的extern "C"

在你的C语言代码中,不知能否看到类似下面的代码:

图片

    这好像没有什么问题,你应该还会想:“嗯⋯是啊,我们的代码都是这样写的,从来没有因此碰到过什么麻烦啊~”。​​[付费]STM32嵌入式资料包​

    你说的没错,如果你的头文件从来没有被任何C++程序引用过的话。

    这与C++有什么关系呢? 看看__cplusplus(注意前面是两个下划线) 的名字你就应该知道它与C++有很大关系。__cplusplus是一个C++规范规定的预定义宏。你可以信任的是:所有的现代C++编译器都预先定义了它;而所有C语言编译器则不会。另外,按照规范__cplusplus的值应该等于1 9 9 7 1 1 L ,然而不是所有的编译器都照此实现,比如g++编译器就将它的值定义为1。

    所以,如果上述代码被C语言程序引用的话,它的内容就等价于下列代码。

图片

    在这种情况下,既然extern "C" { }经过预处理之后根本就不存在,那么它和#include指令之间的关系问题自然也就是无中生有。相关推荐:整理的比较全面的C语言入门笔记!

extern "C"的前世今生

    在C++编译器里,有一位暗黑破坏神,专门从事一份称作“名字粉碎”(name mangling)的工作。当把一个C++的源文件投入编译的时候,它就开始工作,把每一个它在源文件里看到的外部可见的名字粉碎的面目全非,然后存储到二进制目标文件的符号表里。

    之所以在C++的世界里存在这样一个怪物,是因为C++允许对一个名字给予不同的定义,只要在语义上没有二义性就好。比如,你可以让两个函数是同名的,只要它们的参数列表不同即可,这就是函数重载(function overloading);甚至,你可以让两个函数的原型声明是完全相同的,只要它们所处的名字空间(namespace)不一样即可。事实上,当处于不同的名字空间时,所有的名字都是可以重复的,无论是函数名,变量名,还是类型名。

    另外,C++程序的构造方式仍然继承了C语言的传统:编译器把每一个通过命令行指定的源代码文件看做一个独立的编译单元,生成目标文件;然后,链接器通过查找这些目标文件的符号表将它们链接在一起生成可执行程序。相关文章:C语言编译过程。编译和链接是两个阶段的事情。

    事实上,编译器和链接器是两个完全独立的工具。编译器可以通过语义分析知道那些同名的符号之间的差别;而链接器却只能通过目标文件符号表中保存的名字来识别对象。

    所以,编译器进行名字粉碎的目的是为了让链接器在工作的时候不陷入困惑,将所有名字重新编码,生成全局唯一,不重复的新名字,让链接器能够准确识别每个名字所对应的对象。

    但 C语言却是一门单一名字空间的语言,也不允许函数重载,也就是说,在一个编译和链接的范围之内,C语言不允许存在同名对象。比如,在一个编译单元内部,不允许存在同名的函数,无论这个函数是否用static修饰;在一个可执行程序对应的所有目标文件里,不允许存在同名对象,无论它代表一个全局变量,还是一个函数。所以,C语言编译器不需要对任何名字进行复杂的处理(或者仅仅对名字进行简单一致的修饰(decoration),比如在名字前面统一的加上单下划线_)。

    C++的缔造者Bjarne Stroustrup在最初就把——能够兼容C,能够复用大量已经存在的C库——列为C++语言的重要目标。但两种语言的编译器对待名字的处理方式是不一致的,这就给链接过程带来了麻烦。

    例如,现有一个名为my_handle.h的头文件,内容如下:

图片

    然后使用C语言编译器编译my_handle.c,生成目标文件my_handle.o。由于C语言编译器不对名字进行粉碎,所以在my_handle.o的符号表里,这三个函数的名字和源代码文件中的声明是一致的。

图片

    随后,我们想让一个C++程序调用这些函数,所以,它也包含了头文件my_handle.h。假设这个C++源代码文件的名字叫my_handle_client.cpp,其内容如下:

图片

    其中,粗体的部分就是那三个函数的名字被粉碎后的样子。

    然后,为了让程序可以工作,你必须将my_handle.o和my_handle_client.o放在一起链接。由于在两个目标文件对于同一对象的命名不一样,链接器将报告相关的“符号未定义”错误。

图片

    为了解决这一问题,C++引入了链接规范(linkage specification)的概念,表示法为extern"language string",C++编译器普遍支持的"language string"有"C"和"C++",分别对应C语言和C++语言。

    链接规范的作用是告诉C++编译:对于所有使用了链接规范进行修饰的声明或定义,应该按照指定语言的方式来处理,比如名字,调用习惯(calling convention)等等。

链接规范的用法有两种

    1.单个声明的链接规范,比如:

    2. 一组声明的链接规范,比如:

    对我们之前的例子而言,如果我们把头文件my_handle.h的内容改成:

图片

    然后使用C++编译器重新编译my_handle_client.cpp,所生成目标文件my_handle_client.o中的符号表就变为:

图片

    从中我们可以看出,此时,用extern "C" 修饰了的声明,其生成的符号和C语言编译器生成的符号保持了一致。这样,当你再次把my_handle.o和my_handle_client.o放在一起链接的时候,就不会再有之前的“符号未定义”错误了。

    但此时,如果你重新编译my_handle.c,C语言编译器将会报告“语法错误”,因为extern"C"是C++的语法,C语言编译器不认识它。此时,可以按照我们之前已经讨论的,使用宏__cplusplus来识别C和C++编译器。修改后的my_handle.h的代码如下:

图片

小心门后的未知世界

    在我们清楚了 extern "C" 的来历和用途之后,回到我们本来的话题上,为什么不能把#include 指令放置在 extern "C" { ... } 里面?

    我们先来看一个例子,现有a.h,b.h,c.h以及foo.cpp,其中foo.cpp包含c.h,c.h包含b.h,b.h包含a.h,如下:

图片

    现使用C++编译器的预处理选项来编译foo.cpp,得到下面的结果:

    正如你看到的,当你把#include指令放置在extern "C" { }里的时候,则会造成extern "C" { } 的嵌套。这种嵌套是被C++规范允许的。当嵌套发生时,以最内层的嵌套为准。比如在下面代码中,函数foo会使用C++的链接规范,而函数bar则会使用C的链接规范。

图片

    如果能够保证一个C语言头文件直接或间接依赖的所有头文件也都是C语言的,那么按照C++语言规范,这种嵌套应该不会有什么问题。但具体到某些编译器的实现,比如MSVC2005,却可能由于 extern "C" { } 的嵌套过深而报告错误。不要因此而责备微软,因为就这个问题而言,这种嵌套是毫无意义的。你完全可以通过把#include指令放置在extern "C" { }的外面来避免嵌套。拿之前的例子来说,如果我们把各个头文件的 #include 指令都移到extern "C" { } 之外,然后使用C++编译器的预处理选项来编译foo.cpp,就会得到下面的结果:

图片

    这样的结果肯定不会引起编译问题的结果——即便是使用MSVC。

    把 #include 指令放置在extern "C" { }里面的另外一个重大风险是,你可能会无意中改变一个函数声明的链接规范。比如:有两个头文件a.h,b.h,其中b.h包含a.h,如下:

图片

    按照a.h作者的本意,函数foo是一个C++自由函数,其链接规范为"C++"。但在b.h中,由于#include "a.h"被放到了extern "C" { }的内部,函数foo的链接规范被不正确地更改了。

    由于每一条 #include 指令后面都隐藏这一个未知的世界,除非你刻意去探索,否则你永远都不知道,当你把一条条#include指令放置于extern "C" { }里面的时候,到底会产生怎样的结果,会带来何种的风险。或许你会说,“我可以去查看这些被包含的头文件,我可以保证它们不会带来麻烦”。但,何必呢?毕竟,我们完全可以不必为不必要的事情买单,不是吗?

Q&A

Q: 难道任何#include指令都不能放在extern "C"里面吗?

A: 正像这个世界的大多数规则一样,总会存在特殊情况。

    有时候,你可能利用头文件机制“巧妙”的解决一些问题。比如,#pragma pack的问题。这些头文件和常规的头文件作用是不一样的,它们里面不会放置C的函数声明或者变量定义,链接规范不会对它们的内容产生影响。这种情况下,你可以不必遵守这些规则。

    更加一般的原则是,在你明白了这所有的原理之后,只要你明白自己在干什么,那就去做吧。

Q: 你只说了不应该放入extern "C"的,但什么可以放入呢?    

A: 链接规范仅仅用于修饰函数和变量,以及函数类型。所以,严格的讲,你只应该把这三种对象放置于extern "C"的内部。

    但,你把C语言的其它元素,比如非函数类型定义(结构体,枚举等)放入extern "C"内部,也不会带来任何影响。更不用说宏定义预处理指令了。

    所以,如果你更加看重良好组织和管理的习惯,你应该只在必须使用extern "C"声明的地方使用它。即使你比较懒惰,绝大多数情况下,把一个头件自身的所有定义和声明都放置在extern"C"里面也不会有太大的问题。

Q: 如果一个带有函数/变量声明的C头文件里没有extern "C"声明怎么办?

A: 如果你可以判断,这个头文件永远不可能让C++代码来使用,那么就不要管它。

    但现实是,大多数情况下,你无法准确的推测未来。你在现在就加上这个extern "C",这花不了你多少成本,但如果你现在没有加,等到将来这个头文件无意中被别人的C++程序包含的时候,别人很可能需要更高的成本来定位错误和修复问题。

Q: 如果我的C+ +程序想包含一个C头文件a . h,它的内容包含了C的函数/变量声明,但它们却没有使用extern "C"链接规范,该怎么办?

A: 在a.h里面加上它。

    某些人可能会建议你,如果a.h没有extern "C",而b.cpp包含了a.h,可以在b.cpp里加上 :

    这是一个邪恶的方案,原因在之前我们已经阐述。但值得探讨的是,这种方案这背后却可能隐含着一个假设,即我们不能修改a.h。不能修改的原因可能来自两个方面:

  • 头文件代码属于其它团队或者第三方公司,你没有修改代码的权限;
  • 虽然你拥有修改代码的权限,但由于这个头文件属于遗留系统,冒然修改可能会带来不可预知的问题。

    对于第一种情况,不要试图自己进行workaround,因为这会给你带来不必要的麻烦。正确的解决方案是,把它当作一个bug,发送缺陷报告给相应的团队 或第三方公司。如果是自己公司的团队或你已经付费的第三方公司,他们有义务为你进行这样的修改。如果他们不明白这件事情的重要性,告诉他们。如果这些头文 件属于一个免费开源软件,自己进行正确的修改,并发布patch给其开发团队。

    在第二种情况下,你需要抛弃掉这种不必要的安全意识。因为,首先,对于大多数头文件而言,这种修改都不是一种复杂的,高风险的修改,一切都在可控的范围之 内;其次,如果某个头文件混乱而复杂,虽然对于遗留系统的哲学应该是:“在它还没有带来麻烦之前不要动它”,但现在麻烦已经来了,逃避不如正视,所以上策 是,将其视作一个可以整理到干净合理状态的良好机会。

Q: 我们代码中关于extern "C"的写法如下,这正确吗?

图片

A: 不确定。

    按照C++的规范定义,__cplusplus 的值应该被定义为199711L,这是一个非零的值;尽管某些编译器并没有按照规范来实现,但仍然能够保证__cplusplus的值为非零——至少我到目前为止还没有看到哪款编译器将其实现为0。这种情况下,#if __cplusplus ... #endif完全是冗余的。

    但,C++编译器的厂商是如此之多,没有人可以保证某款编译器,或某款编译器的早期版本没有将__cplusplus的值定义为0。但即便如此,只要能够保证宏__cplusplus只在C++编译器中被预先定义 ,那么,仅仅使用#ifdef __cplusplus ⋯ #endif就足以确保意图的正确性;额外的使用#if __cplusplus ... #endif反而是错误的。

    只有在这种情况下:即某个厂商的C语言和C++语言编译器都预先定义了__cplusplus ,但通过其值为0和非零来进行区分,使用#if __cplusplus ... #endif才是正确且必要的。

    既然现实世界是如此复杂,你就需要明确自己的目标,然后根据目标定义相应的策略。比如:如果你的目标是让你的代码能够使用几款主流的、正确遵守了规范的编译器进行编译,那么你只需要简单的使用#ifdef __cplusplus ... #endif就足够了。

    但如果你的产品是一个雄心勃勃的,试图兼容各种编译器的(包括未知的)跨平台产品, 我们可能不得不使用下述方法来应对各种情况 ,其中__ALIEN_C_LINKAGE__是为了标识那些在C和C++编译中都定义了__cplusplus宏的编译器。

    这应该可以工作,但在每个头文件中都写这么一大串,不仅有碍观瞻,还会造成一旦策略进行修改,就会到处修改的状况。违反了DRY(Don't Repeat Yourself)原则,你总要为之付出额外的代价。解决它的一个简单方案是,定义一个特定的头文件——比如clinkage.h,在其中增加这样的定义:

图片

    以下举例中c的函数声明和定义分别在cfun.h 和 cfun.c 中,函数打印字符串 “this is c fun call”,c++函数声明和定义分别在cppfun.h 和 cppfun.cpp中,函数打印字符串 "this is cpp fun call", 编译环境vc2010.

C++ 调用 C 的方法

    c++ 调用 c 的方法,关键是要让c的函数按照c的方式编译,而不是c++的方式。

(1) cfun.h如下:

 cppfun.cpp 如下:

(2)cfun.h同上

  cppfun.cpp 如下:

extern "C"
{
#include "cfun.h"//注意include语句一定要单独占一行;
}
#include "cppfun.h"
#include <iostream>
using namespace std;

void cppfun()
{
cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
return 0;
}
(3)cfun.h如下:
#ifndef _C_FUN_H_
#define _C_FUN_H_

#ifdef __cplusplus
extern "C"
{
#endif

void cfun();

#ifdef __cplusplus
}
#endif

#endif

cppfun.cpp如下:

#include "cfun.h"
#include "cppfun.h"
#include <iostream>
using namespace std;

void cppfun()
{
cout<<"this is cpp fun call"<<endl;
}

int main()
{
    cfun();
return 0;
}

C调用 C++ 的方法

    c调用c++,关键是C++ 提供一个符合 C 调用惯例的函数。

    在vs2010上测试时,没有声明什么extern等,只在在cfun.c中包含cppfun.h,然后调用cppfun()也可以编译运行,在gcc下就编译出错,按照c++/c的标准这种做法应该是错误的。以下方法两种编译器都可以运行。

cppfun.h如下:

#ifndef _CPP_FUN_H_
#define _CPP_FUN_H_

extern "C" void cppfun();


#endif
cfun.c如下:
//#include "cppfun.h" //不要包含头文件,否则编译出错
#include "cfun.h"
#include <stdio.h>

void cfun()
{
printf("this is c fun call\n");
}

extern void cppfun();

int main()
{
#ifdef __cplusplus
    cfun();
#endif
    cppfun();
return 0;
}

九、C语言中枚举enum的用法

本文举例说明C语言中enum枚举关键字的用法。

用来同时定义多个常量

    利用enum定义月份的例子如下。

#include<stdio.h>
enum week {Mon=1,Tue,Wed,Thu,Fri,Sat,Sun};
int main()
{
    printf("%d",Tue);
    return 0;
}

    这样定义Mon的值为1之后,Tue的值就被默认定义为2,Wed的值为3,依此类推。如果没写Mon=1的话,Mon的默认值就为0。例如:

enum color {red,blue,green,yellow}; //red的值默认为0

    从中间开始赋值的情况,见如下例子:

enum color {red,blue,green=5,yellow}; 
//red、bule、green、yellow的值依次为0、1、5、6

用来限定变量的取值范围

    有时为了保证程序的健壮性而使用enum。

#include<stdio.h>
enum Month {Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,dec};
int main()
{
    enum Month a =  Feb;
    printf("%d",a);
    return 0;
}

    比如上面例子,枚举类型a的取值被限定在那12个变量中。

enum类型的定义方法

    在定义enum的同时声明变量:

enum Month {Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,dec} a,b;
//这样就声明了两个枚举类型a和b

    定义完enum之后再声明变量:

enum Month {Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,dec};
enum Month a =  Feb;

    定义匿名的枚举变量:

enum  {Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,dec} a;
//这样就只能使用a这一个枚举类型的变量,不能再定义其他枚举类型了

十、C语言中结构体struct的用法

定义结构体变量

    下面举一个例子来说明怎样定义结构体变量。

struct string 
 { 
    char name[8]; 
    int age; 
    char sex[2]; 
    char depart[20]; 
    float wage1, wage2, wage3, wage4, wage5; 
 }person;

    这个例子定义了一个结构名为string的结构体变量person。还可以省略变量名person,做如下定义:

struct string 
 { 
    char name[8]; 
    int age; 
    char sex[2]; 
    char depart[20]; 
    float wage1, wage2, wage3, wage4, wage5; 
 };  
 struct string person;   //定义结构名为string的结构体变量person

    定义多个具有相同形式的结构变量:

struct string Liming, Liuqi, ...;

    有一种结构体常常出现在函数内部,省略结构名,则称之为无名结构,如下:

图片

结构体成员的获取与赋值

    结构是一个新的数据类型,结构成员的表示方式为:

结构变量.成员名

    如果将"结构变量.成员名"看成一个整体,这样就可以像其他变量那样使用。

    下面这个例子定义了一个结构变量stu1,分别给name、age、group赋值,并打印输出。

#include <stdio.h>
int main()
{
struct
  {
    char *name;  //姓名
int age;  //年龄
    char group;  //所在小组
  } stu1;
//给结构体成员赋值
  stu1.name = "Tom";
  stu1.age = 18;
  stu1.group = 'A';
//读取结构体成员的值
printf("%s的年龄是%d,在%c组\n", stu1.name, stu1.age, stu1.group);
return 0;
}

结构体数组

    结构体数组就是具有相同结构类型的变量集合,假如要定义一个班级40个同学 的姓名、性别、年龄和住址, 可以定义成一个结构数组。如下所示:

struct
 { 
    char name[8]; 
    char sex[2]; 
    int age; 
    char addr[40]; 
 }student[40];

    结构体数组成员的访问是以数组元素为结构变量的, 其形式为:

结构数组元素.成员名

    例如:

student[0].name
student[30].age

结构体指针

    结构体指针由一个加在结构变量名前的*操作符来定义,定义一个结构体指针如下:

struct string
{ 
   char name[8]; 
   char sex[2]; 
   int age; 
   char addr[40]; 
}*student;

    使用结构体指针对结构体成员的访问与结构体变量对结构体成员的访问在表达方式不同。结构体指针对结构体成员的访问方式为:

结构体指针名->结构体成员

    给上面定义的结构体中name和age赋值的语句:

strcpy(student->name, "acket"); //student->name就是(*student).name
student->age=18;

    需要指出的是结构体指针是指向结构体的一个指针,即结构体中第一个成员的首地址,因此在使用之前应该对结构体指针初始化,即分配整个结构体长度的字节空间:

student=(struct string*)malloc(size of (struct string));
//size of (struct string)是自动求取string结构体的字节长度

    malloc()函数定义了一个大小为结构体长度的内存区域,然后将其地址作为结构体指针返回,相关文章:C语言中的内存管理。

位结构

    位结构是一种特殊的结构体,位结构定义的一般形式为:

struct 位结构名
 { 
    数据类型 变量名: 整型常数; 
    数据类型 变量名: 整型常数; 
 }位结构变量;

    其中数据类型必须是int(unsigned或signed,但当成员长度为1时, 会被认为是unsigned类型),整型常数必须是0~15的非负整数,表示二进制位个数。变量名是选择项,可以不命名,下面定义了一个位结构:

struct
 { 
    unsigned incon: 8;    /*incon占用低字节 的0~7共8位*/ 
    unsigned txcolor: 4;/*txcolor占用高字节的0~3位共4位*/ 
    unsigned bgcolor: 3;/*bgcolor占用高字节的4~6位共3位*/ 
    unsigned blink: 1;  /*blink占用高字节的第7位*/ 
 }ch;

    位结构成员的访问与结构体成员的访问相同,访问位结构中的bgcolor成员可写成:

ch.bgcolor

举个例子

struct info
{ 
    char name[8]; 
    int age; 
    struct addr address; 
    float pay; 
    unsigned state: 1; 
    unsigned pay: 1; 
}workers;

    上面结构体定义了工资的信息,其中有两个只有1位的位结构成员,表示工人的状态以及工资是否已发放。

typedef定义结构体

typedef struct  person
{
int age ;
char *name;
char *sex;
}student;
student stu1; //此处可以用student来定义一个结构体变量

    typedef的作用就相当于给struct person取了一个别名student。

十一、经典的结构体和联合体共用实例
1 结构体

结构体占用的内存大小,首先和编译器的系统位数有关系,类似于CPU是 64 bits 还是 32 bits 的情形;其次,结构体需要考虑字节对齐的问题。

实际上占用的内存大小, 在 Linux 中可以 使用 sizeof 进行获取,默认为字节对齐的大小。

2 联合体

联合体的参数共享同一个内存地址,所占的内存大小完全是由联合体中参数类型决定字长,然后数据共享,内存共享等。

结构体和联合体连用例子:

2.1 首先定义一个结构体

内部包含联合体,如下:

#define MAX_SOFTKEY_LEN    4


typedef enum
{
 ENUM_TRANSFER,
 ENUM_CONFERENCE,
 ENUM_ANSWER,
 ENUM_HOLD,
}KeyType;


typedef struct tag_CallRecordInfo
{ 
 char line;                  // current recording line.
 unsigned char state;         // current machine state.
 unsigned short total;      // current total used lines.
 KeyType type;
 union
 {
  char Transferkey[MAX_SOFTKEY_LEN];     // transfer key buffer
  char Conferencekey[MAX_SOFTKEY_LEN];   // conference key buffer
  char AnswerKey[MAX_SOFTKEY_LEN];       // talking key buffer
  char HoldKey[MAX_SOFTKEY_LEN];         // hold key buffer
 }SoftKey;


}CallRecordInfo;

解释如下结构体:

联合体 union 表示公用一个 4 字节的内存,并且定义了一个联合体变量 SoftKey,这个
联合体最大的好处就是在赋值或者清空时能够直观地清楚,不需要再引用联
合体的参数。如:
CallRecordInfo info;
对 info.SoftKey 的操作会影响联合体内部参数的值,并且数值一致。也
就是说,我们只要对 info.SoftKey 进行赋值即可, 然后联合体内的参
数内容是一样的。即:
info.SoftKey = info.SoftKey.TransferKey.

2.2 结构体占用的内存大小

作者的机器为 Ubuntu 18.04, sizeof(int) = 4 的机器。

图片

2.3 整体代码

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


#define MAX_SOFTKEY_LEN    4


typedef enum
{
 ENUM_TRANSFER,
 ENUM_CONFERENCE,
 ENUM_ANSWER,
 ENUM_HOLD,
}KeyType;


typedef struct tag_CallRecordInfo
{ 
 char line;                  // current recording line.
 unsigned char state;         // current machine state.
 unsigned short total;      // current total used lines.
 KeyType type;




 union
 {
  char Transferkey[MAX_SOFTKEY_LEN];     // transfer key buffer
  char Conferencekey[MAX_SOFTKEY_LEN];   // conference key buffer
  char AnswerKey[MAX_SOFTKEY_LEN];       // talking key buffer
  char HoldKey[MAX_SOFTKEY_LEN];         // hold key buffer
 }SoftKey;


}CallRecordInfo;


CallRecordInfo RecordInfo ;


void SetSoftKeyValue(int state, KeyType type, char *keybuf)
{


 RecordInfo.state = state;
 RecordInfo.type = type;
 memset(&RecordInfo.SoftKey, 0, MAX_SOFTKEY_LEN);


 if(NULL != keybuf)
 {
  memcpy(&RecordInfo.SoftKey, keybuf, MAX_SOFTKEY_LEN);
 }
}


int main(int argc, char const *argv[])
{
 
 char buf[4] = "123";


 SetSoftKeyValue(0, ENUM_TRANSFER, buf);


 printf("\n%s --- %lu\n", RecordInfo.SoftKey.Conferencekey, sizeof(CallRecordInfo));
 return 0;
}

2.4 结果

图片

十二、C语言结构体2

有的时候,我们所遇到的数据结构,不仅仅是一群数字或者是字符串那么简单。比如我们每一个人的学籍信息,学号是一个长整数,名字却是字符;甚至有更复杂的情况,这种问题在现实生活中并不少见。我们之前学过一种叫数组的数据结构,它可以允许我们把很多同类型的数据集中在一起处理。相对于之前,这已经是一次极大的进步。但是,新的问题,往往又会出现,这个时候,我们就得上更高端的装备——结构体。

相比于数组,结构体有以下的更强大的优势:

  • 批量存储数据
  • 存储不同类型的数据
  • 支持嵌套

结构体的声明与定义声明

结构体的声明使用​​struct​​关键字,如果我们想要把我们的学籍信息组织一下的话,可以这样表示:

struct Info
{
    unsigned long identifier;//学号,用无符号长整数表示
    char name[20];//名字,用字符数组表示
    unsigned int year;//入学年份,用无符号整数表示
    unsigned int years;//学制,用无符号整数表示
}

这样,我们就相当于描绘好了一个框架,以后要用的话直接定义一个这种类型的变量就好了。

定义

我们刚刚申请了一个名叫​​Info​​的结构体类型,那么理论上我们可以像声明其他变量的操作一样,去声明我们的结构体操作,但是C语言中规定,声明结构体变量的时候,​​struct​​关键字是不可少的。

​struct 结构体类型名 结构体变量名​

不过,你可以在某个函数里面定义:

#include <stdio.h>


struct Info
{
    unsigned long identifier;//学号,用无符号长整数表示
    char name[20];//名字,用字符数组表示
    unsigned int year;//入学年份,用无符号整数表示
    unsigned int years;//学制,用无符号整数表示
};


int main(void)
{
    /**
     *在main函数中声明结构体变量
     *结构体变量名叫info
     *struct关键字不能丢
     */
    struct Info info;
    ...
}

也可以在声明的时候就把变量名定义下来(此时这个变量是全局变量):

#include <stdio.h>


struct Info
{
    unsigned long identifier;//学号,用无符号长整数表示
    char name[20];//名字,用字符数组表示
    unsigned int year;//入学年份,用无符号整数表示
    unsigned int years;//学制,用无符号整数表示
} info;
/**
 *此时直接定义了变量
 *该变量是全局变量
 *变量名叫info
 */


int main(void)
{
    ...
}

访问结构体成员

结构体成员的访问有点不同于以往的任何变量,它是采用点号运算符​​.​​来访问成员的。比如,​​info.name​​就是引用​​info​​结构体的​​name​​成员,是一个字符数组,而​​info.year​​则可以查到入学年份,是个无符号整型。

比如,下面开始录入学生的信息:

//Example 01
#include <stdio.h>


struct Info
{
    unsigned long identifier;//学号,用无符号长整数表示
    char name[20];//名字,用字符数组表示
    unsigned int year;//入学年份,用无符号整数表示
    unsigned int years;//学制,用无符号整数表示
};


int main(void)
{
    struct Info info;


    printf("请输入学生的学号:");
    scanf("%d", &info.identifier);
    printf("请输入学生的姓名:");
    scanf("%s", info.name);
    printf("请输入学生的入学年份:");
    scanf("%d", &info.year);
    printf("请输入学生的学制:");
    scanf("%d", &info.years);


    printf("\n数据录入完毕\n\n");


    printf("学号:%d\n姓名:%s\n入学年份:%d\n学制:%d\n毕业时间:%d\n", \
        info.identifier, info.name, info.year, info.years, info.year + info.years);
    return 0;
}

运行结果如下:

//Consequence 01
请输入学生的学号:20191101
请输入学生的姓名:Harris
请输入学生的入学年份:2019
请输入学生的学制:4


数据录入完毕


学号:20191101
姓名:Harris
入学年份:2019
学制:4
毕业时间:2023

初始化结构体

像数组一样,结构体也可以在定义的时候初始化,方法也几乎一样:

struct Info info = {
    20191101,
    "Harris",
    2019,
    4
};

在C99标准中,还支持给指定元素赋值(就像数组一样):

struct Info info = {
    .name = "Harris",
    .year = 2019
};

对于没有被初始化的成员,则「数值型」成员初始化为0,「字符型」成员初始化为‘\0’。

对齐

下面这个代码,大家来看看会发生什么:

//EXample 02 V1
#include <stdio.h>


int main(void)
{
    struct A
    {
        char a;
        int b;
        char c;
    } a = {'a', 10, 'o'};
    
    printf("size of a = %d\n", sizeof(a));
    
    return 0;
}

我们之前学过,​​char​​类型的变量占1字节,​​int​​类型的变量占4字节,那么这么一算,一个结构体A型的变量应该就是6字节了。别急,我们看运行结果:

//COnsequence 02 V1
size of a = 12

怎么变成12了呢?标准更新了?老师教错了?都不是。我们把代码改一下:

//EXample 02 V2
#include <stdio.h>


int main(void)
{
    struct A
    {
        char a;
        char c;
        int b;
    } a = {'a', 'o', 10};
    
    printf("size of a = %d\n", sizeof(a));
    
    return 0;
}

结果:

//Consequence 02 V2
size of a = 8

实际上,这是编译器对我们程序的一种优化——内存对齐。在第一个例子中,第一个和第三个成员是​​char​​类型是1个字节,而中间的​​int​​却有4个字节,为了对齐,两个​​char​​也占用了4个字节,于是就是12个字节。

而在第二个例子里面,前两个都是​​char​​,最后一个是​​int​​,那么前两个可以一起占用4个字节(实际只用2个,第一个例子也同理,只是为了访问速度更快,而不是为了扩展),最后的​​int​​占用4字节,合起来就是8个字节。

关于如何声明结构体来节省内存容量,可以阅读下面的这篇文章,作者是艾瑞克·雷蒙,时尚最具争议性的黑客之一,被公认为开源运动的主要领导者之一:

英文原版,中文版​

结构体嵌套

在学籍里面,如果我们的日期想要更加详细一些,精确到day,这时候就可以使用结构体嵌套来完成:

#include <stdio.h>


struct Date
{
    unsigned int year;
    unsigned int month;
    unsigned int day;
};


struct Info
{
    unsigned long identifier;//学号,用无符号长整数表示
    char name[20];//名字,用字符数组表示
    struct Date date;/*---入学日期,用结构体Date表示---*/
    unsigned int years;//学制,用无符号整数表示
};


int main(void)
{
    ...
}

如此一来,比我们单独声明普通变量快多了。

不过,这样访问变量,就必须用点号一层层往下访问。比如要访问​​day​​这个成员,那就只能​​info.date.day​​而不能直接​​info.date​​或者​​info,day​​。

//Example 03
#include <stdio.h>


struct Date
{
    unsigned int year;
    unsigned int month;
    unsigned int day;
};


struct Info
{
    unsigned long identifier;//学号,用无符号长整数表示
    char name[20];//名字,用字符数组表示
    struct Date date;/*---入学日期,用结构体Date表示---*/
    unsigned int years;//学制,用无符号整数表示
};


int main(void)
{
    struct Info info;
    printf("请输入学生的学号:");
    scanf("%d", &info.identifier);
    printf("请输入学生的公众号名:");
    scanf("%s", info.name);
    printf("请输入学生的入学年份:");
    scanf("%d", &info.date.year);
    printf("请输入学生的入学月份:");
    scanf("%d", &info.date.month);
    printf("请输入学生的入学日期:");
    scanf("%d", &info.date.day);
    printf("请输入学生的学制:");
    scanf("%d", &info.years);


    printf("\n数据录入完毕\n\n");


    printf("学号:%d\n公众号名:%s\n入学时间:%d/%d/%d\n学制:%d\n毕业时间:%d\n",\
           info.identifier, info.name,\
           info.date.year, info.date.month, info.date.day,\
           info.years, info.date.year + info.years);
    return 0;
}

运行结果如下:

//Consequence 03
请输入学生的学号:20191101
请输入学生的公众号名:STM32嵌入式开发
请输入学生的入学年份:2019
请输入学生的入学月份:9
请输入学生的入学日期:7
请输入学生的学制:4


数据录入完毕


学号:20191101
公众号名:STM32嵌入式开发
入学时间:2019/9/7
学制:4
毕业时间:2023

结构体数组

刚刚我们演示了存储一个学生的学籍信息的时候,使用结构体的例子。那么,如果要录入一批学生,这时候我们就可以沿用之前的思路,使用结构体数组。

我们知道,数组的定义,就是存放一堆相同类型的数据的容器。而结构体一旦被我们声明,那么你就可以把它看作一个类型,只不过是你自己定义的罢了。

定义结构体数组也很简单:

struct 结构体类型
{
    成员;
} 数组名[长度];


/****或者这样****/


struct 结构体类型
{
    成员;
};
struct 结构体类型 数组名[长度];

结构体指针

既然我们可以把结构体看作一个类型,那么也就必然有对应的指针变量。

struct Info* pinfo;

但是在指针这里,结构体和数组就不一样了。我们知道,数组名实际上就是指向这个数组第一个元素的地址,所以可以将数组名直接赋值给指针。而结构体的变量名并不是指向该结构体的地址,所以要使用取地址运算符​​&​​才能获取地址:

pinfo = &info;

通过结构体指针来访问结构体有以下两种方法:

  1. ​(*结构体指针).成员名​
  2. ​结构体指针->成员名​

第一个方法由于点号运算符比指针的取值运算符优先级更高,因此需要加一个小括号来确定优先级,让指针先解引用变成结构体变量,在使用点号的方法去访问。

相比之下,第二种方法就直观许多。

这两种方法在实现上是完全等价的,但是点号只能用于结构体变量,而箭头只能够用于指针。

第一种方法:

#include <stdio.h>
...
int main(void)
{
    struct Info *p;
    p = &info;
    
    printf("学号:\n", (*p).identifier);
    printf("姓名:\n", (*p).name);
    printf("入学时间:%d/%d/%d\n", (*p).date.year, (*p).date.month, (*p).date.day);
    printf("学制:\n", (*p).years);
    return 0;
}

第二种方法:

#include <stdio.h>
...
int main(void)
{
    struct Info *p;
    p = &info;
    
    printf("学号:\n", p -> identifier);
    printf("姓名:\n", p -> name);
    printf("入学时间:%d/%d/%d\n", p -> date.year, p -> date.month, p -> date.day);
    printf("学制:\n", p -> years);
    return 0;
}

传递结构体信息传递结构体变量

我们先来看看下面的代码:

//Example 04
#include <stdio.h>


int main(void)
{
    struct Test
    {
        int x;
        int y;
    }t1, t2;


    t1.x = 3;
    t1.y = 4;
    t2 = t1;


    printf("t2.x = %d, t2.y = %d\n", t2.x, t2.y);
    return 0;
}

运行结果如下:

//Consequence 04
t2.x = 3, t2.y = 4

这么看来,结构体是可以直接赋值的。那么既然这样,作为函数的参数和返回值也自然是没问题的了。

先来试试作为参数:

//Example 05
#include <stdio.h>
struct Date
{
    unsigned int year;
    unsigned int month;
    unsigned int day;
};


struct Info
{
    unsigned long identifier;
    char name[20];
    struct Date date;
    unsigned int years;
};


struct Info getInput(struct Info info);
void printInfo(struct Info info);


struct Info getInput(struct Info info)
{
    printf("请输入学号:");
    scanf("%d", &info.identifier);
    printf("请输入姓名:");
    scanf("%s", info.name);
    printf("请输入入学年份:");
    scanf("%d", &info.date.year);
    printf("请输入月份:");
    scanf("%d", &info.date.month);
    printf("请输入日期:");
    scanf("%d", &info.date.day);
    printf("请输入学制:");
    scanf("%d", &info.years);


    return info;
}


void printInfo(struct Info info)
{
    printf("学号:%d\n姓名:%s\n入学时间:%d/%d/%d\n学制:%d\n毕业时间:%d\n", \
        info.identifier, info.name, \
        info.date.year, info.date.month, info.date.day, \
        info.years, info.date.year + info.years);
}


int main(void)
{
    struct Info i1 = {};
    struct Info i2 = {};
    printf("请录入第一个同学的信息...\n");
    i1 = getInput(i1);
    putchar('\n');
    printf("请录入第二个学生的信息...\n");
    i2 = getInput(i2);


    printf("\n录入完毕,现在开始打印...\n\n");
    printf("打印第一个学生的信息...\n");
    printInfo(i1);
    putchar('\n');
    printf("打印第二个学生的信息...\n");
    printInfo(i2);


    return 0;
}

运行结果如下:

//Consequence 05
请录入第一个同学的信息...
请输入学号:20191101
请输入姓名:Harris
请输入入学年份:2019
请输入月份:9
请输入日期:7
请输入学制:4


请录入第二个学生的信息...
请输入学号:20191102
请输入姓名:Joy
请输入入学年份:2019
请输入月份:9
请输入日期:8
请输入学制:5


录入完毕,现在开始打印...


打印第一个学生的信息...
学号:20191101
姓名:Harris
入学时间:2019/9/7
学制:4
毕业时间:2023


打印第二个学生的信息...
学号:20191102
姓名:Joy
入学时间:2019/9/8
学制:5
毕业时间:2024

传递指向结构体变量的指针

早期的C语言是不允许直接将结构体作为参数直接传递进去的。主要是考虑到如果结构体的内存占用太大,那么整个程序的内存开销就会爆炸。不过现在的C语言已经放开了这方面的限制。

不过,作为一名合格的开发者,我们应该要去珍惜硬件资源。那么,传递指针就是一个很好的办法。

将刚才的代码修改一下:

//Example 06
#include <stdio.h>
struct Date
{
    unsigned int year;
    unsigned int month;
    unsigned int day;
};


struct Info
{
    unsigned long identifier;
    char name[20];
    struct Date date;
    unsigned int years;
};


void getInput(struct Info *info);
void printInfo(struct Info *info);


void getInput(struct Info *info)
{
    printf("请输入学号:");
    scanf("%d", &info->identifier);
    printf("请输入姓名:");
    scanf("%s", info->name);
    printf("请输入入学年份:");
    scanf("%d", &info->date.year);
    printf("请输入月份:");
    scanf("%d", &info->date.month);
    printf("请输入日期:");
    scanf("%d", &info->date.day);
    printf("请输入学制:");
    scanf("%d", &info->years);
}


void printInfo(struct Info *info)
{
    printf("学号:%d\n姓名:%s\n入学时间:%d/%d/%d\n学制:%d\n毕业时间:%d\n", \
        info->identifier, info->name, \
        info->date.year, info->date.month, info->date.day, \
        info->years, info->date.year + info->years);
}


int main(void)
{
    struct Info i1 = {};
    struct Info i2 = {};
    printf("请录入第一个同学的信息...\n");
    getInput(&i1);
    putchar('\n');
    printf("请录入第二个学生的信息...\n");
    getInput(&i2);


    printf("\n录入完毕,现在开始打印...\n\n");
    printf("打印第一个学生的信息...\n");
    printInfo(&i1);
    putchar('\n');
    printf("打印第二个学生的信息...\n");
    printInfo(&i2);


    return 0;
}

此时传递的就是一个指针,而不是一个庞大的结构体。​

动态申请结构体

结构体也可以在堆里面动态申请:

//Example 01
#include <stdio.h>
...
int main(void)
{
    struct Info *i1;
    struct Info *i2;
    
    i1 = (struct Info *)malloc(sizeof(struct Info));
    i2 = (struct Info *)malloc(sizeof(struct Info));
    if (i1 == NULL || i2 == NULL)
    {
        printf("内存分配失败!\n");
        exit(1);
    }
    
    printf("请录入第一个同学的信息...\n");
    getInput(i1);
    putchar('\n');
    printf("请录入第二个学生的信息...\n");
    getInput(i2);


    printf("\n录入完毕,现在开始打印...\n\n");
    printf("打印第一个学生的信息...\n");
    printInfo(i1);
    putchar('\n');
    printf("打印第二个学生的信息...\n");
    printInfo(i2);
    
    free(i1);
    free(i2);
    
    return 0;
}

实战:建立一个图书馆数据库

实际上,我们建立的数组可以是指向结构体指针的数组。

代码实现如下:

//Example 02
#include <stdio.h>
#include <stdlib.h>


#define MAX_SIZE 100


struct Date
{
    int year;
    int month;
    int day;
};


struct Book
{
    char title[128];
    char author[48];
    float price;
    struct Date date;
    char publisher[48];
};


void getInput(struct Book* book);//录入数据
void printBook(struct Book* book);//打印数据
void initLibrary(struct Book* lib[]);//初始化结构体
void printLibrary(struct Book* lib[]);//打印单本书数据
void releaseLibrary(struct Book* lib[]);//释放内存


void getInput(struct Book* book)
{
    printf("请输入书名:");
    scanf("%s", book->title);
    printf("请输入作者:");
    scanf("%s", book->author);
    printf("请输入售价:");
    scanf("%f", &book->price);
    printf("请输入出版日期:");
    scanf("%d-%d-%d", &book->date.year, &book->date.month, &book->date.day);
    printf("请输入出版社:");
    scanf("%s", book->publisher);
}


void printBook(struct Book* book)
{
    printf("书名:%s\n", book->title);
    printf("作者:%s\n", book->author);
    printf("售价:%.2f\n", book->price);
    printf("出版日期:%d-%d-%d\n", book->date.year, book->date.month, book->date.day);
    printf("出版社:%s\n", book->publisher);
}


void initLibrary(struct Book* lib[])
{
    for (int i = 0; i < MAX_SIZE; i++)
    {
        lib[i] = NULL;
    }
}


void printLibrary(struct Book* lib[])
{
    for (int i = 0; i < MAX_SIZE; i++)
    {
        if (lib[i] != NULL)
        {
            printBook(lib[i]);
            putchar('\n');
        }
    }
}


void releaseLibrary(struct Book* lib[])
{
    for (int i = 0; i < MAX_SIZE; i++)
    {
        if (lib[i] != NULL)
        {
            free(lib[i]);
        }
    }
}


int main(void)
{
    struct Book* lib[MAX_SIZE];
    struct Book* p = NULL;
    int ch, index = 0;


    initLibrary(lib);


    while (1)
    {
        printf("请问是否要录入图书信息(Y/N):");
        do
        {
            ch = getchar();
        } while (ch != 'Y' && ch != 'N');


        if (ch == 'Y')
        {
            if (index < MAX_SIZE)
            {
                p = (struct Book*)malloc(sizeof(struct Book));
                getInput(p);
                lib[index] = p;
                index++;
                putchar('\n');
            }
            else
            {
                printf("数据库已满!\n");
                break;
            }
        }
        else
        {
            break;
        }
    }


    printf("\n数据录入完毕,开始打印验证...\n\n");
    printLibrary(lib);
    releaseLibrary(lib);


    return 0;
}

运行结果如下:

//Consequence 02
请问是否要录入图书信息(Y/N):Y
请输入书名:人类简史
请输入作者:尤瓦尔·赫拉利
请输入售价:32.25
请输入出版日期:2016-3-4
请输入出版社:中信出版集团


请问是否要录入图书信息(Y/N):N


数据录入完毕,开始打印验证...


书名:人类简史
作者:尤瓦尔·赫拉利
售价:32.25
出版日期:2016-3-4
出版社:中信出版集团

单链表

我们知道,数组变量在内存中,是连续的,而且不可拓展。显然在一些情况下,这种数据结构拥有很大的局限性。比如移动数据的时候,会牵一发而动全身,尤其是反转这种操作更加令人窒息。那么,需要需要一种数据结构来弄出一种更加灵活的“数组”,那么这,就是「链表」

本节我们只讲讲单链表。

所谓链表,就是由一个个「结点」组成的一个数据结构。每个结点都有「数据域」「指针域」组成。其中数据域用来存储你想要存储的信息,而指针域用来存储下一个结点的地址。如图:

单链表

当然,链表最前面还有一个头指针,用来存储头结点的地址。

这样一来,链表中的每一个结点都可以不用挨个存放,因为有了指针把他们串起来。因此结点放在哪都无所谓,反正指针总是能够指向下一个元素。我们只需要知道头指针,就能够顺藤摸瓜地找到整个链表。

因此对于学籍数据库来说,我们只需要在Info结构体中加上一个指向自身类型的成员即可:

struct Info
{
    unsigned long identifier;
    char name[20];
    struct Date date;
    unsigned int years;
    struct Info* next;
};

在单链表中插入元素头插法

这种每次都将数据插入单链表的头部(头指针后面)的插入法就叫头插法。

如果要把学生信息加入到单链表,可以这么写:

void addInfo(struct Info** students)//students是头指针
{
    struct Info* info, *temp;
    info = (struct Info*)malloc(sizeof(struct Info));
    if (info == NULL)
    {
        printf("内存分配失败!\n");
        exit(1);
    }
    
    getInput(info);
    
    if (*students != NULL)
    {
        temp = *students;
        *students = info;
        info->next = temp;
    }
    else
    {
        *students = info;
        info->next = NULL;
    }
}

由于students存放的是头指针,因此我们需要传入它的地址传递给函数,才能够改变它本身的值。而students本身又是一个指向Info结构体的指针,所以参数的类型应该就是​​struct Info**​​。

往单链表里面添加一个结点,也就是先申请一个结点,然后判断链表是否为空。如果为空,那么直接将头指针指向它,然后​​next​​成员指向​​NULL​​。若不为空,那么先将​​next​​指向头指针原本指向的结点,然后将头指针指向新结点即可。

那么,打印链表也变得很简单:

想要读取单链表里面的数据,只需要迭代单链表中的每一个结点,直到​​next​​成员为​​NULL​​,即表示单链表的结束。

最后,当然还是别忘了释放空间:

void releaseStu(struct Info** students)
{
    struct Info* temp;
    
    while (*students != NULL)
    {
        temp = *students;
        *students = (*students)->next;
        free(temp);
    }
}

尾插法

与头插法类似,尾插法就是把每一个数据都插入到链表的末尾。

void addInfo(struct Info** students)
{
    struct Info* info, *temp;
    info = (struct Info*)malloc(sizeof(struct Info));
    if (info == NULL)
    {
        printf("内存分配失败!\n");
        exit(1);
    }
    
    getInput(info);
    
    if (*students != NULL)
    {
        temp = *students;
        *students = info;
        //定位到链表的末尾的位置
        while (temp->next != NULL)
        {
            temp = temp->next;
        }
        //插入数据
        temp->next = info;
        info->next = temp;
    }
    else
    {
        *students = info;
        info->next = NULL;
    }
}

这么一来,程序执行的效率难免要降低很多,因为每次插入数据,都要先遍历一次链表。如果链表很长,那么对于插入数据来说就是一次灾难。不过,我们可以给程序添加一个指针,让它永远都指向链表的尾部,这样一来,就可以用很少的空间换取很高的程序执行效率。

代码更改如下:

void addInfo(struct Info** students)
{
    struct Info* info, *temp;
    static struct Info* tail;//设置静态指针
    info = (struct Info*)malloc(sizeof(struct Info));
    if (info == NULL)
    {
        printf("内存分配失败!\n");
        exit(1);
    }
    
    getInput(info);
    
    if (*students != NULL)
    {
        tail->next = info;
        info->next = NULL;
    }
    else
    {
        *students = info;
        info->next = NULL;
    }
}

搜索单链表

单链表是我们用来存储数据的一个容器,那么有时候需要快速查找信息就需要开发相关搜索的功能。比如说输入学号,查找同学的所有信息。

struct Info *searchInfo(struct Info* students, long* target)
{
    struct Info* info;
    info = students;
    while (info != NULL)
    {
        if (info->identifier == target)
        {
            break;
        }
        info = info->next;
    }
    
    return book;
};


void printInfo(struct Info* info)
{
    ...
}
...


int main(void)
{
    ...
    printf("\n请输入学生学号:");
    scanf("%d", input);
    info = searchInfo(students, input);
    if (info == NULL)
    {
        printf("抱歉,未找到相关结果!\n");
    }
    else
    {
        do
        {
            printf("相关结果如下:\n");
            printInfo(book);
        } while ((info = searchInfo(info->next, input)) != NULL);
    }
    
    releaseInfo(...);
    return 0;
}

插入结点到指定位置

到了这里,才体现出链表真正的优势。

设想一下,如果有一个有序数组,现在要求你去插入一个数字,插入完成之后,数组依然保持有序。你会怎么做?

没错,你应该会挨个去比较,然后找到合适的位置(当然这里也可以使用二分法,比较节省算力),把这个位置后面的所有数都往后移动一个位置,然后将我们要插入的数字放入刚刚我们腾出来的空间里面。

你会发现,这样的处理方法,经常需要移动大量的数据,对于程序的执行效率来说,是一个不利因素。那么链表,就无所谓。反正在内存中,链表的存储毫无逻辑,我们只需要改变指针的值就可以实现链表的中间插入。

//Example 03
#include <stdio.h>
#include <stdlib.h>


struct Node
{
    int value;
    struct Node* next;
};


void insNode(struct Node** head, int value)
{
    struct Node* pre;
    struct Node* cur;
    struct Node* New;


    cur = *head;
    pre = NULL;


    while (cur != NULL && cur->value < value)
    {
        pre = cur;
        cur = cur->next;
    }


    New = (struct Node*)malloc(sizeof(struct Node));
    if (New == NULL)
    {
        printf("内存分配失败!\n");
        exit(1);
    }
    New->value = value;
    New->next = cur;


    if (pre == NULL)
    {
        *head = New;
    }
    else
    {
        pre->next = New;
    }
}


void printNode(struct Node* head)
{
    struct Node* cur;


    cur = head;
    while (cur != NULL)
    {
        printf("%d ", cur->value);
        cur = cur->next;
    }
    putchar('\n');
}


int main(void)
{
    struct Node* head = NULL;
    int input;


    printf("开始插入整数...\n");
    while (1)
    {
        printf("请输入一个整数,输入-1表示结束:");
        scanf("%d", &input);
        if (input == -1)
        {
            break;
        }
        insNode(&head, input);
        printNode(head);
    }


    return 0;
}

运行结果如下:

//Consequence 03
开始插入整数...
请输入一个整数,输入-1表示结束:4
4
请输入一个整数,输入-1表示结束:5
4 5
请输入一个整数,输入-1表示结束:3
3 4 5
请输入一个整数,输入-1表示结束:6
3 4 5 6
请输入一个整数,输入-1表示结束:2
2 3 4 5 6
请输入一个整数,输入-1表示结束:5
2 3 4 5 5 6
请输入一个整数,输入-1表示结束:1
1 2 3 4 5 5 6
请输入一个整数,输入-1表示结束:7
1 2 3 4 5 5 6 7
请输入一个整数,输入-1表示结束:-1

删除结点

删除结点的思路也差不多,首先修改待删除的结点的上一个结点的指针,将其指向待删除结点的下一个结点。然后释放待删除结点的空间。

...
void delNode(struct Node** head, int value)
{
    struct Node* pre;
    struct Node* cur;
    
    cur = *head;
    pre = NULL;
    while (cur != NULL && cur->value != value)
    {
        pre = cur;
        cur = cur->next;
    }
    if (cur == NULL)
    {
        printf("未找到匹配项!\n");
        return ;
    }
    else
    {
        if (pre == NULL)
        {
            *head = cur->next;
        }
        else
        {
            pre->next = cur->next;
        }
        free(cur);
    }
}

内存池

C语言的内存管理,从来都是一个让人头秃的问题。要想更自由地管理内存,就必须去堆中申请,然后还需要考虑何时释放,万一释放不当,或者没有及时释放,造成的后果都是难以估量的。

当然如果就这些,那倒也还不算什么。问题就在于,如果大量地使用​​malloc​​和​​free​​函数来申请内存,首先使要经历一个从应用层切入系统内核层,调用完成之后,再返回应用层的一系列步骤,实际上使非常浪费时间的。更重要的是,还会产生大量的内存碎片。比如,先申请了一个1KB的空间,紧接着又申请了一个8KB的空间。而后,这个1KB使用完了,被释放,但是这个空间却只有等到下一次有刚好1KB的空间申请,才能够被重新调用。这么一来,极限情况下,整个堆有可能被弄得支离破碎,最终导致大量内存浪费。

那么这种情况下,我们解决这类问题的思路,就是创建一个内存池。

内存池,实际上就是我们让程序创建出来的一块额外的缓存区域,如果有需要释放内存,先不必使用​​free​​函数,如果内存池有空,那么直接放入内存池。同样的道理,下一次程序申请空间的时候,先检查下内存池里面有没有合适的内存,如果有,则直接拿出来调用,如果没有,那么再使用​​malloc​​。

其实内存池我们就可以使用单链表来进行维护,下面通过一个通讯录的程序来说明内存池的运用。

普通的版本:

//Example 04 V1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


struct Person
{
 char name[40];
 char phone[20];
 struct Person* next;
};


void getInput(struct Person* person);
void printPerson(struct Person* person);
void addPerson(struct Person** contects);
void changePerson(struct Person* contacts);
void delPerson(struct Person** contacts);
struct Person* findPerson(struct Person* contacts);
void displayContacts(struct Person* contacts);
void releaseContacts(struct Person** contacts);


void getInput(struct Person* person)
{
 printf("请输入姓名:");
 scanf("%s", person->name);
 printf("请输入电话:");
 scanf("%s", person->phone);
}


void addPerson(struct Person** contacts)
{
 struct Person* person;
 struct Person* temp;


 person = (struct Person*)malloc(sizeof(struct Person));
 if (person == NULL)
 {
  printf("内存分配失败!\n");
  exit(1);
 }


 getInput(person);


 //将person添加到通讯录中
 if (*contacts != NULL)
 {
  temp = *contacts;
  *contacts = person;
  person->next = temp;
 }
 else
 {
  *contacts = person;
  person->next = NULL;
 }
}


void printPerson(struct Person* person)
{
 printf("联系人:%s\n", person->name);
 printf("电话:%s\n", person->phone);
}


struct Person* findPerson(struct Person* contacts)
{
 struct Person* current;
 char input[40];


 printf("请输入联系人:");
 scanf("%s", input);


 current = contacts;
 while (current != NULL && strcmp(current->name, input))
 {
  current = current->next;
 }


 return current;
}


void changePerson(struct Person* contacts)
{
 struct Person* person;


 person = findPerson(contacts);
 if (person == NULL)
 {
  printf("找不到联系人!\n");
 }
 else
 {
  printf("请输入联系电话:");
  scanf("%s", person->phone);
 }
}


void delPerson(struct Person** contacts)
{
 struct Person* person;
 struct Person* current;
 struct Person* previous;


 //先找到待删除的节点的指针
 person = findPerson(*contacts);
 if (person == NULL)
 {
  printf("找不到该联系人!\n");
 }
 else
 {
  current = *contacts;
  previous = NULL;


  //将current定位到待删除的节点
  while (current != NULL && current != person)
  {
   previous = current;
   current = current->next;
  }


  if (previous == NULL)
  {
   //若待删除的是第一个节点
   *contacts = current->next;
  }
  else
  {
   //若待删除的不是第一个节点
   previous->next = current->next;
  }


  free(person);//将内存空间释放
 }
}


void displayContacts(struct Person* contacts)
{
 struct Person* current;


 current = contacts;
 while (current != NULL)
 {
  printPerson(current);
  current = current->next;
 }
}


void releaseContacts(struct Person** contacts)
{
 struct Person* temp;


 while (*contacts != NULL)
 {
  temp = *contacts;
  *contacts = (*contacts)->next;
  free(temp);
 }
}


int main(void)
{
 int code;
 struct Person* contacts = NULL;
 struct Person* person;


 printf("| 欢迎使用通讯录管理程序 |\n");
 printf("|--- 1:插入新的联系人 ---|\n");
 printf("|--- 2:查找现有联系人 ---|\n");
 printf("|--- 3:更改现有联系人 ---|\n");
 printf("|--- 4:删除现有联系人 ---|\n");
 printf("|--- 5:显示当前通讯录 ---|\n");
 printf("|--- 6:退出通讯录程序 ---|\n");


 while (1)
 {
  printf("\n请输入指令代码:");
  scanf("%d", &code);
  switch (code)
  {
  case 1:addPerson(&contacts); break;
  case 2:person = findPerson(contacts);
   if (person == NULL)
   {
    printf("找不到该联系人!\n");
   }
   else
   {
    printPerson(person);
   }
   break;
  case 3:changePerson(contacts); break;
  case 4:delPerson(&contacts); break;
  case 5:displayContacts(contacts); break;
  case 6:goto END;
  }
 }


END://此处直接跳出恒循环
 releaseContacts(&contacts);


 return 0;


}

运行结果如下:

//Consequence 04 V1
| 欢迎使用通讯录管理程序 |
|--- 1:插入新的联系人 ---|
|--- 2:查找现有联系人 ---|
|--- 3:更改现有联系人 ---|
|--- 4:删除现有联系人 ---|
|--- 5:显示当前通讯录 ---|
|--- 6:退出通讯录程序 ---|


请输入指令代码:1
请输入姓名:HarrisWilde
请输入电话:0101111


请输入指令代码:1
请输入姓名:Jack
请输入电话:0101112


请输入指令代码:1
请输入姓名:Rose
请输入电话:0101113


请输入指令代码:2
请输入联系人:HarrisWilde
联系人:HarrisWilde
电话:0101111


请输入指令代码:2
请输入联系人:Mike
找不到该联系人!


请输入指令代码:5
联系人:Rose
电话:0101113
联系人:Jack
电话:0101112
联系人:HarrisWilde
电话:0101111


请输入指令代码:3
请输入联系人:HarrisWilde
请输入联系电话:0101234


请输入指令代码:5
联系人:Rose
电话:0101113
联系人:Jack
电话:0101112
联系人:HarrisWilde
电话:0101234


请输入指令代码:6

下面加入内存池:

//Example 04 V2
#include <stdio.h>
#include <stdlib.h>
#include <string.h>


#define MAX 1024


struct Person
{
 char name[40];
 char phone[20];
 struct Person* next;
};


struct Person* pool = NULL;
int count;


void getInput(struct Person* person);
void printPerson(struct Person* person);
void addPerson(struct Person** contects);
void changePerson(struct Person* contacts);
void delPerson(struct Person** contacts);
struct Person* findPerson(struct Person* contacts);
void displayContacts(struct Person* contacts);
void releaseContacts(struct Person** contacts);
void releasePool(void);


void getInput(struct Person* person)
{
 printf("请输入姓名:");
 scanf("%s", person->name);
 printf("请输入电话:");
 scanf("%s", person->phone);
}


void addPerson(struct Person** contacts)
{
 struct Person* person;
 struct Person* temp;


 //如果内存池不是空的,那么首先从里面获取空间
 if (pool != NULL)
 {
  person = pool;
  pool = pool->next;
  count--;
 }
 //内存池为空,则直接申请
 else
 {
  person = (struct Person*)malloc(sizeof(struct Person));
  if (person == NULL)
  {
   printf("内存分配失败!\n");
   exit(1);
  }
 }




 getInput(person);


 //将person添加到通讯录中
 if (*contacts != NULL)
 {
  temp = *contacts;
  *contacts = person;
  person->next = temp;
 }
 else
 {
  *contacts = person;
  person->next = NULL;
 }
}


void printPerson(struct Person* person)
{
 printf("联系人:%s\n", person->name);
 printf("电话:%s\n", person->phone);
}


struct Person* findPerson(struct Person* contacts)
{
 struct Person* current;
 char input[40];


 printf("请输入联系人:");
 scanf("%s", input);


 current = contacts;
 while (current != NULL && strcmp(current->name, input))
 {
  current = current->next;
 }


 return current;
}


void changePerson(struct Person* contacts)
{
 struct Person* person;


 person = findPerson(contacts);
 if (person == NULL)
 {
  printf("找不到联系人!\n");
 }
 else
 {
  printf("请输入联系电话:");
  scanf("%s", person->phone);
 }
}


void delPerson(struct Person** contacts)
{
 struct Person* person;
 struct Person* current;
 struct Person* previous;
 struct Person* temp;
 {


 };


 //先找到待删除的节点的指针
 person = findPerson(*contacts);
 if (person == NULL)
 {
  printf("找不到该联系人!\n");
 }
 else
 {
  current = *contacts;
  previous = NULL;


  //将current定位到待删除的节点
  while (current != NULL && current != person)
  {
   previous = current;
   current = current->next;
  }


  if (previous == NULL)
  {
   //若待删除的是第一个节点
   *contacts = current->next;
  }
  else
  {
   //若待删除的不是第一个节点
   previous->next = current->next;
  }


  //判断内存池中有没有空位
  if (count < MAX)
  {
   //使用头插法将person指向的空间插入内存池中
   if (pool != NULL)
   {
    temp = pool;
    pool = person;
    person->next = temp;
   }
   else
   {
    pool = person;
    person->next = NULL;
   }
   count++;
  }
  //没有空位,直接释放
  else
  {
   free(person);//将内存空间释放
  }
 }
}


void displayContacts(struct Person* contacts)
{
 struct Person* current;


 current = contacts;
 while (current != NULL)
 {
  printPerson(current);
  current = current->next;
 }
}


void releaseContacts(struct Person** contacts)
{
 struct Person* temp;


 while (*contacts != NULL)
 {
  temp = *contacts;
  *contacts = (*contacts)->next;
  free(temp);
 }
}


void releasePool(void)
{
 struct Person* temp;
 while (pool != NULL)
 {
  temp = pool;
  pool = pool->next;
  free(temp);
 }
}


int main(void)
{
 int code;
 struct Person* contacts = NULL;
 struct Person* person;


 printf("| 欢迎使用通讯录管理程序 |\n");
 printf("|--- 1:插入新的联系人 ---|\n");
 printf("|--- 2:查找现有联系人 ---|\n");
 printf("|--- 3:更改现有联系人 ---|\n");
 printf("|--- 4:删除现有联系人 ---|\n");
 printf("|--- 5:显示当前通讯录 ---|\n");
 printf("|--- 6:退出通讯录程序 ---|\n");


 while (1)
 {
  printf("\n请输入指令代码:");
  scanf("%d", &code);
  switch (code)
  {
  case 1:addPerson(&contacts); break;
  case 2:person = findPerson(contacts);
   if (person == NULL)
   {
    printf("找不到该联系人!\n");
   }
   else
   {
    printPerson(person);
   }
   break;
  case 3:changePerson(contacts); break;
  case 4:delPerson(&contacts); break;
  case 5:displayContacts(contacts); break;
  case 6:goto END;
  }
 }


END://此处直接跳出恒循环
 releaseContacts(&contacts);
 releasePool();


 return 0;


}

typedef给数据类型起别名

C语言是一门古老的语言,它是在1969至1973年间,由两位天才丹尼斯·里奇和肯·汤普逊在贝尔实验室以B语言为基础开发出来的,用于他们的重写UNIX计划(这也为后来UNIX系统的可移植性打下了基础,之前的UNIX是使用汇编语言编写的,当然也是这两位为了玩一个自己设计的游戏而编写的)。天才就是和咱常人不一样,不过他俩的故事,在这篇里面不多啰嗦,我们回到话题。

虽然C语言诞生的很早,但是却依旧不是最早的高级编程语言。目前公认的最早的高级编程语言,是IBM公司于1957年开发的FORTRAN语言。C语言诞生之时,FORTRAN已经统领行业数十年之久。因此,C语言要想快速吸纳FORTRAN中的潜在用户,就必须做出一些妥协。

我们知道,不同的语言的语法,一般来说是不同的,甚至还有较大的差距。比如:

C:

int a, b, c;
float i, j, k;

而FORTRAN语言是这样的:

integer :: a, b, c;
real :: i, j, k;

如果让FORTRAN用户使用原来的变量名称进行使用,那么就能够快速迁移到C语言上面来,这就是​​typedef​​的用处之一。

我们使用FORTRAN语言的类型名,那就这么办:

typedef int integer;
typedef float real;


integer a, b, c;
real i, j, k;

结构体的搭档

虽然结构体的出现能够让我们有一个更科学的数据结构来管理数据,但是每次使用结构体都需要​​struct...​​,未免显得有些冗长和麻烦。有了​​typedef​​的助攻,我们就可以很轻松地给结构体类型起一个容易理解的名字:

typedef struct date
{
    int year;
    int month;
    int day;
} DATE;//为了区分,一般用全大写


int main(void)
{
    DATE* date;
    ...
}

甚至还可以顺便给它的指针也定义一个别名:

typedef struct date
{
    int year;
    int month;
    int day;
} DATE, *PDATE;

进阶

我们还可以利用​​typedef​​来简化一些比较复杂的命令。

比如:

int (*ptr) [5];

我们知道这是一个数组指针,指向一个5元素的数组:​​动画讲解C语言中的指针,从未如此简单​​。那么我们可以改写成这样:

typedef int(*PTR_TO_ARRAY)[3];

这样就可以把很复杂的声明变得很简单:

PTR_TO_ARRAY a = &array;

取名的时候要尽量使用容易理解的名字,这样才能达到使用​​typedef​​的最终目的。​

共用体

共用体也称联合体。​

声明

和结构体还是有点像:

union 共用体名称
{
    成员1;
    成员2;
    成员3;
};

但是两者有本质的不同。共用体的每一个成员共用一段内存,那么这也就意味着它们不可能同时被正确地访问。如:

//Example 05
#include <stdio.h>
#include <string.h>


union Test
{
 int i;
 double pi;
 char str[9];
};


int main(void)
{
 union Test test;


 test.i = 10;
 test.pi = 3.14;
 strcpy(test.str, "TechZone");


 printf("test.i: %d\n", test.i);
 printf("test.pi: %.2f\n", test.pi);
 printf("test.str: %s\n", test.str);


 return 0;
}

执行结果如下:

//Consequence 05
test.i: 1751344468
test.pi: 3946574856045802736197446431383475413237648487838717723111623714247921409395495328582015991082102150186282825269379326297769425957893182570875995348588904500564659454087397032067072.00
test.str: TechZone

可以看到,共用体只能正确地展示出最后一次被赋值的成员。共用体的内存应该要能够满足最大的成员能够正常存储。但是并不一定等于最大的成员的尺寸,因为还要考虑内存对齐的问题。

共用体可以类似结构体一样来定义和声明,但是共用体还可以允许不带名字:

union
{
 int i;
 char ch;
 float f;
} a, b;

初始化

共用体不能在同一时间存放多个成员,所以不能批量初始化

union data
{
    int i;
    char ch;
    float f;
};


union data a = {520};        //初始化第一个成员
union data b = a;            //直接使用一个共用体初始化另一个共用体
union data c = {.ch = 'C'};  //C99的特性,指定初始化成员

枚举

枚举是一个基本的数据类型,它可以让数据更简洁。

如果写一个判断星期的文章,我们当然可以使用宏定义来使代码更加易懂,不过:

#define MON 1
#define TUE 2
#define WED 3
#define THU 4
#define FRI 5
#define SAT 6
#define SUN 7

这样的写法有点费键盘。那么枚举就简单多了:

enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
};

**注意:**第一个枚举成员的默认值为整型的 0,后续枚举成员的值在前一个成员上加 1。我们在这个实例中把第一个枚举成员的值定义为 1,第二个就为 2,以此类推。

枚举变量的定义和声明方法和共用体一样,也可以省略枚举名,直接声明变量名。

//Example 06
#include <stdio.h>
#include <stdlib.h>


int main()
{


    enum color { red = 1, green, blue };


    enum  color favorite_color;


    printf("请输入你喜欢的颜色: (1. red, 2. green, 3. blue): ");
    scanf("%d", &favorite_color);


    //输出结果
    switch (favorite_color)
    {
    case red:
        printf("你喜欢的颜色是红色");
        break;
    case green:
        printf("你喜欢的颜色是绿色");
        break;
    case blue:
        printf("你喜欢的颜色是蓝色");
        break;
    default:
        printf("你没有选择你喜欢的颜色");
    }


    return 0;
}

执行结果如下:

//Consequence 06
请输入你喜欢的颜色: (1. red, 2. green, 3. blue): 3
你喜欢的颜色是蓝色

也可以把整数转换为枚举类型:

//Example 07


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


int main()
{
    enum day
    {
        saturday,
        sunday,
        monday,
        tuesday,
        wednesday,
        thursday,
        friday
    } workday;


    int a = 1;
    enum day weekend;
    weekend = (enum day) a;  //使用强制类型转换
    //weekend = a; //错误
    printf("weekend:%d", weekend);
    return 0;
}

运行结果如下:

//Consequence 07
weekend:1

位域

C语言除了开发桌面应用等,还有一个很重要的领域,那就是「单片机」开发。单片机上的硬件资源十分有限,容不得我们去肆意挥洒。单片机使一种集成电路芯片,使采用超大规模集成电路技术把具有数据处理能力的CPU、RAM、ROM、I/O、中断系统、定时器/计数器等功能(有的还包括显示驱动电路、脉宽调制电路、模拟多路转换器、A/D转换器等电路)集成到一块硅片上构成的一个小而完善的微型计算机系统,在工控领域使用广泛。

对于这样的设备,通常内存只有256B,那么能够给我们利用的资源就十分珍贵了。在这种情况下,如果我们只需要定义一个变量来存放布尔值,一般就申请一个整型变量,通过1和0来间接存储。但是,显然1和0只用1个bit就能够放完,而一个整型却是4个字节,也就是32bit。这就造成了内存的浪费。

好在,C语言为我们提供了一种数据结构,称为「位域」(也叫位端、位字段)。也就是把一个字节中的二进制位划分,并且你能够指定每个区域的位数。每个域有一个域名,并允许程序中按域名进行单独操作。

使用位域的做法是在结构体定义的时候,在结构体成员后面使用冒号(:)和数字来表示该成员所占的位数。

//Example 08
#include <stdio.h>


int main(void)
{
 struct Test
 {
  unsigned int a : 1;
  unsigned int b : 1;
  unsigned int c : 2;
 } test;
 
 test.a = 0;
 test.b = 1;
 test.c = 2;


 printf("a = %d, b = %d, c = %d\n", test.a, test.b, test.c);
 printf("size of test = %d\n", sizeof(test));


 return 0;
}

运行结果如下:

//Consequence 08
a = 0, b = 1, c = 2
size of test = 4

如此一来,结构体​​test​​只用了4bit,却存放下了0、1、2三个整数。但是由于2在二进制中是10,因此占了2个bit。如果把​​test.b​​赋值为2,那么:

//Consequence 08 V2
a = 0, b = 0, c = 2
size of test = 4

可以看到,b中的10溢出了,只剩下0。

当然,位域的宽度不能够超过本身类型的长度,比如:

unsigned int a : 100;

那么就会报错:

错误  C2034  “main::test::a”: 位域类型对位数太小

位域成员也可以没有名称,只要给出类型和宽度即可:

struct Test
{
    unsigned int x : 1;
    unsigned int y : 2;
    unsigned int z : 3;
    unsigned int : 26;
};

无名位域一般用来作为填充或者调整成员的位置,因为没有名称,所以无名位域并不能够拿来使用。

C语言的标准只说明unsigned int和signed int支持位域,然后C99增加了_Bool类型也支持位域,其他数据类型理论上是不支持的。不过大多数编译器在具体实现时都进行了扩展,额外支持了signed char、unsigned char以及枚举类型,所以如果对char类型的结构体成员使用位域,基本上也没什么问题。但如果考虑到程序的可移植性,就需要谨慎对待了。另外,由于内存的基本单位是字节,而位域只是字节的一部分,所以并不能对位域进行取地址运算。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值