介绍
发展历史
为了简化机器语言的编写,1950年代中期,汇编语言应运而生。它使用助记符(mnemonic)来表示机器指令,极大地提高了程序员的工作效率。即便如此,书写和调试一个程序也需要用很长时间,并且需要程序员详细了解编写代码需要的指令、寄存器和CPU知识。
为了解决这些问题,IBM公司的一个研究小组开始着手研究一门能高效运行且简单易用的编程语言。1956年,IBM公司的John Backus带领他的团队开发出了世界上第一个高级语言FORTRAN(FORmula TRANslation,意为公式翻译),在科学和工程计算领域取得了很大成功,但该语言是专门为IBM 704计算机设计的,只能在特定型号的机器上工作。
为了解决这一问题,GAMM(德国应用数学和力学学会)、IBM、Datatron等公司要求ACM评估一种能脱离机器型号限定的“通用编程语言”。1958年5月,在美国计算机科学家Alan Jay Perlis的领导下,ACM、GAMM等代表在苏黎世会议上成立了IAL算法语言委员会,制定了国际算法语言(International Algorithmic Language,IAL)的定义并发布了IAL58语言。随后,ACM推崇IAL作为编程语言的标准,该算法语言由各种不同的“关键字”和“表达式”按照一定的语法规则组成,脱离了指令系统成为更接近人类的语言系统,编写者不必懂得完整的计算机的内部结构和工作原理也可以很好编写程序。
1959年,IAL被重新命名为ALGOL(算法语言)。
1960年,ALGOL算法语言委员会发布了ALGOL 60,里程碑式地引入了递归、布尔类型、块结构等概念,ALGOL 60也成为了后来所有计算机语言语法的鼻祖,Alan Jay Perlis也因此成为了世界首届图灵奖的获得者,以表彰其在 Algol 58 和 Algol 60 的形成和改进过程中的核心和关键作用。但ALGOL 60最大的缺点就是缺乏标准的输入输出能力,ALGOL 60编写的程序无法兼容不同的计算机外部设备,因此ALGOL 60迎来了进一步的迭代。
1963年,剑桥大学基于ALGOL 60发布了CPL(Combined Programming Langurage)语言,该语言旨在用于工业控制、数据处理等更为广泛的领域,因此规模宏大而复杂,编译器的编写进度十分缓慢,该语言并未大规模流行。
1967年,英国剑桥大学的Matin Richards对CPL语言做了简化,推出了BCPL(the Basic Combined Programming Language)语言,该语言是最早使用库函数封装基本输入输出的语言之一,主要用于编写系统和编译器,但BCPL对字符串的支持很差,并且内存管理很糟糕。
1969年,美国贝尔实验室的Kenneth Lane Thompson对BCPL语言做了改进和优化,由此衍生出了B语言,并用于书写UNIX系统,该语言的名字取自BCPL中的第一个字母,但此时的B语言过于简单,功能有限。
1972年,贝尔实验室的Dennis MacAlistair Ritchie在B语言的基础上设计出了C语言(取BCPL的第二个字母作为该语言的名字),C语言保留了BCPL和B语言的优点,简练而接近硬件,又克服了它们无数据类型等缺点。1973年初, C语言的主体完成,汤普森和里奇使用C语言重写了UNIX操作系统,极大地提升了UNIX操作系统的可移植性,而C语言随着UNIX的广泛使用得到了迅速推广,成为了应用最广泛的计算机语言之一。
1983年,美国国家标准化协会(ANSI)制定了C语言标准——ANSI C,1987年,ANSI又公布了新的标准87 ANSI C,该标准于1990年被国际标准化组织(Internation Standard Organization)接受成为C语言国际标准——ISO/IEC 9899,简称C89/C90标准,是C语言的主流标准,被广泛应用于各个领域。1999年,ISO对C语言进行了大量扩展和改进,加入了如long long等新的数据类型,发布了C99标准,该标准在部分领域使用广泛,但在工业等领域普及度较低。目前最新标准为2018年发布的C17标准,但尚未被广泛使用。
编译流程
C语言源文件以.c为文件名后缀,从高级语言转换为机器码程需要经过预处理、编译、汇编、链接四个过程
预处理
C语言源文件首先会被预处理,以一个名为hello.c的源文件为例,hello.c源文件经过预处理器处理,会生成一个hello.i临时文件,预处理阶段会进行以下工作:
- 删除代码中的所有注释
- 对宏进行代码扩展,通过#define等指令定义的常量或表达式,会被替换到文件中,对#if等条件编译指令限定的代码进行选择性编译
- 包含文件,通过#include指令包含的文件,其内容会被添加到源文件中
编译
编译器会将临时文件hello.i编译为汇编文件hello.s,该阶段编译器会对代码进行语法检查,并返回源代码中存在的语法错误和警告
汇编
汇编程序将hello.s汇编文件编译成机器码,在Windows环境下生成hello.obj对象文件,在Linux环境下生成hello.o目标文件
链接
链接器会将多个目标文件合并为一个文件,并与库函数文件(.lib)等依赖链接,生成可执行文件,在Windows环境下生成一个可执行文件 hello.exe,在 Linux环境下中生成 hello.out文件
库函数文件由各软件公司编写并已经编译成目标文件(.obj文件),它们将在链接阶段与源程序编译而成的目标文件(.obj文件)相链接,生成可执行文件。
C语言内存模型
准确来讲,这是Linux的虚拟空间布局的一部分,Linux系统内核进程和用户进程所占的虚拟内存比是1:3,Windows是2:2,以32位Linux系统为例,模拟一个4GB的内存地址块沙盘,其中高地址位的1GB内存为系统内核进程占用的内核空间,低地址位的3GB内存为用于用户内进程的用户空间,内核空间由操作系统负责维护和处理,因此本文只涉及日常开发中C程序可以操控的3GB用户空间部分。
该虚拟沙盘地址通过页表(Page Table)映射到物理内存,其中,蓝色区域会被映射到物理内存的不同内存段,白色区域只用于虚拟内存地址的分析,不会映射到物理内存中,不占用内存空间

保留区(Reserved)
保留区是系统预留且禁止用户进程访问的地址区域,位于虚拟地址空间的最低部分,不赋予物理地址,一般用于捕捉空指针异常等情况。在大多数操作系统中,极小的地址空间通常都是不允许访问的,如NULL、0等。因此C语言也将无效指针赋值为0,因为0地址在正常情况下是不会存放有效的可访问数据的。
在32位X86架构的Linux系统中,系统将预留128MB的保留区,因此用户进程可执行程序一般从虚拟地址空间0x08048000开始加载内存区域。
代码区(Code/Text)
用于存放二进制代码,该区域是只读的,以防止恶意程序修改代码区的数据。代码段指令中包括操作码和操作数(操作对象),如果操作对象是立即数(具体数值),则该数据将直接包含在代码中;如果是字符串常量、变量中的数据,则将引用该数据地址。
静态区(Static)
静态区又称全局区,这部分的空间大小在编译时就已经确定,因此是静态的,静态区分为只读数据段(RO Data,又称为常量区)、已初始化数据段(又称为读写数据段,RW Data)和未初始化数据段(又称为BSS段,Block Started by Symbol)三部分
- 只读数据段(.rodata):只读,用于存储字符串常量,以及const修饰的只读全局变量(只读局部变量存储于栈区)、只读字符串变量,只读静态变量
- 已初始化数据段(.data):可读可写,存储已初始化且初值不为0的全局变量和静态局部变量
- 未初始化数据段(.bss):可读可写,存储未初始化或初始化为零的全局变量和静态变量。这些变量在编译阶段会被收集起来放到.bss段中,并在程序初始化时自动赋值为0(对数值型变量)或空字符(对字符变量)。由于.bss段的值全为0,因此这个段只记录段长,在编译-链接生成的可执行文件中不占用物理文件空间(全是0,没必要存储),能在一定程度上节省磁盘空间。在程序执行(初始化)时,加载器(loader)根据其段长分配相应大小的内存,并将这些内存空间全部初始化为0。因此.bss段不占用物理文件尺寸,但占用内存空间;.data段占用物理文件,也占用内存空间
堆(Heap)
由程序员分配释放,如果程序员不释放,则操作系统在程序结束时回收,通过malloc、realloc、calloc语句开辟空间,通过free释放。
堆内存的增长方向为由低地址向高地址增长,其工作方式类似于数据结构中的链表。在操作系统中有一个记录空闲内存地址的链表,当使用malloc()等语句开辟内存空间时,操作系统会遍历该链表,寻找第一个空间大于等于所申请空间的堆节点,然后将该节点从空闲链表中删除,并将该节点的空间分配给程序,此外,系统会在该空间的首地址处记录所分配的空间大小,以便free语句能正确释放空间。
由于每次分配的堆节点大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中,频繁的开辟、释放空间势必会造成内存空间的不连续,从而造成大量碎片。
堆空间的大小受限于系统的有效虚拟内存,32位Linux系统中堆内存可达2.9G空间。堆的末端由break指针标识,当堆管理器需要更多内存时,可通过系统调用brk()和sbrk()来移动break指针以扩张堆,一般由系统自动调用。
内存映射段(mmap)
该区域用于映射可执行文件用到的动态链接库到内存中,方便更快速地读取。
此外,在Linux中,如果通过malloc()请求一个大的内存空间,C运行库将在该区域创建一个匿名内存映射,而不使用堆内存。
栈(Stack)
栈内存由编译器负责分配和释放,主要用于存放:
- 函数内部定义的局部自动变量(auto类型),包括const修饰的只读局部变量
- 函数参数和返回值
- 调用函数时的上下文环境,如函数返回地址等
- 暂存一些算术表达式的计算结果
- alloca()函数分配的栈内内存
栈内存的增长方向为由高地址向低地址增长,其工作方式与数据结构中的栈相似,即先入后出,且栈分配的内存空间是连续的,不会有碎片化现象。栈是由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多
栈空间的大小由系统预定义(通常默认2M或10M),当栈剩余空间大于所申请空间时,系统将分配内存,否则将抛出栈溢出(stack overflow)错误
各段生成时间
在C语言程序编译-链接后生成的映像文件中,将只包含代码段、只读数据段(.rodata)和读写数据段(.data),双击运行该文件后,程序在初始化时将动态生成未初始化数据段(.bss),程序的运行过程,将动态生成堆(Heap)和栈(Stack)
预处理命令
预处理命令是C语言源程序中用于改进程序设计环境、提高编程效率的特殊命令,不属于C语言的组成部分,无法进行编译,只会对其在预处理阶段作出相应处理。预处理命令以#开头,且不以分号结尾,以便与一般的C语言语句区分
#include文件包含
文件包含处理指将一个源文件中的内容包含到另一个源文件中,被包含的文件称为头文件(或标题文件),文件名常以.h作为后缀,也可以以.c作为后缀甚至没有后缀。在编译的预处理阶段,头文件中的内容会被复制到主文件中,替换掉 #include <xxx.h>语句,得到一个新的源文件,再对该文件进行编译。
- #include <文件名>一般用于包含系统库函数头文件
- #include “文件名”一般用于包含用户自己编写的头文件
注意事项:
- 一个#include语句只能包含一个文件,如果要包含多个文件,则只能使用多个语句
- 使用<文件名>包含文件时,系统会到存放C语言库函数头文件的路径下寻找该文件,,使用“文件名”包含文件时,系统会首先在用户当前目录中寻找文件,如果没有,再去C语言库函数路径下
- 如果A文件包含了B文件,而B文件需要使用C文件中的内容,则可以在A文件中使用两个#include命令同时包含文件B和C,且包含C文件的指令需要出现在包含B文件的指令前。也可以嵌套包含,即B文件包含C文件,A文件包含B文件
- A文件包含B文件后,B文件中的内容会在预处理阶段被复制到A文件中,因此B文件中的全局静态变量在A文件中有效,不需要extern声明语句
- #include通常用于包含.h头文件,也可以包含其他文本文件,如:源代码文件(.c)、c++的头文件(.hpp或.hxx)、库文件(.lib、.dll、.a等),甚至可以为.txt文本文件
#define 宏定义
用一个指定的标识符代表一个字符串,在编译预处理阶段,预处理器会对源代码中的宏名进行宏展开,即将宏名替换成对应的字符串
- #define 标识符 字符串 定义无参宏定义
- #undef 标识符 终止宏定义
- #define 标识符(参数表列) 字符串 定义带参数的宏定义
注意事项:
- 宏名一般使用大写字母表示,以便与普通变量名区分
- 宏定义只用作字符串替换,与定义普通变量不同,不分配内存空间
- 宏定义可以嵌套定义
- 宏定义用于定义符号常量,定义完毕后不能再赋值,也不能被修改
- 宏定义语句写于函数外,作用范围为宏定义语句之后到本源文件结束,或者到#undef 宏名行终止
- 预处理阶段只对宏名进行字符串替换,不作语法准确性检查。如果宏定义中有错误,只有在宏名被宏展开后的编译阶段,编译器对源程序进行语法检查时才会发现宏定义中存在的错误
条件编译
条件编译指预处理器根据条件编译指令,选择性将源代码中的部分代码送到编译器进行编译
#ifdef宏定义判断
#ifdef 标识符
程序1
#else
程序2
#endif
如果指定的标识符已经被#define定义过,则编译程序1,否则编译程序2,程序1和程序2既可以是C语句,也可以是预处理命令,类似于if…else…语句,#else可以不使用
#ifndef无宏定义判断
逻辑与#ifdef相反,如果指定的标识符未被#define定义,则编译程序1,否则编译程序2
#ifndef 标识符
程序1
#else
程序2
#endif
#if表达式判断
如果表达式值为真,编译程序1,否则编译程序2
#if 表达式
程序1
#else
程序2
#endif
变量
C语言中变量的声明方式如下:
存储类型数据类型 变量名;
- 存储类型代表编译器为变量分配内存的方式,如:自动变量内存分配于动态存储器,静态变量和外部变量内存分配于静态存储区,寄存器变量存储位置分配于CPU中的寄存器
- 存储类型和作用域(变量定义的位置)决定了变量的生存期
- 数据类型指该变量存储的数据类型
- 变量名需要满足标识符命名规则
标识符命名规则
标识符即C语言中的变量名、符号常量名、函数名、数组名等用来给数据对象命名的字符序列,标识符需要满足以下规则:
- 只能由英文字母、数字、下划线组成
- 不能以数字开头
- 不能使用关键字
- 严格区分大小写
变量的作用域和生存期
变量从其作用域区分,分为局部变量和全局变量:
局部变量:定义在函数内部或{ }包裹的代码块内部,只在本函数或代码块内生效,分为静态局部变量和动态局部变量。默认情况下,局部变量的存储类型为auto(动态局部变量),变量内存位于动态存储区,代码块内的语句执行完毕变量就会被销毁,释放其内存。可以将局部变量的存储类型定义为static(静态局部变量),此时变量内存位于静态存储区,整个程序运行期间其内存不会被释放
全局变量:定义在函数的外部,默认存储类型为extern,有效作用域为变量定义位置开始到本源文件结束,其作用域可以通过extern声明进行拓展。全局变量在程序的整个执行过程都占用内存单元。习惯上,全局变量的首字母用大写表示,当全局变量名和局部变量名相同时,局部变量有效
变量的生存期是指变量从生成(分配内存)到被销毁(释放内存)的时间段。对于存储在静态存储区中的变量,其内存在编译时就已经确定,是静态的,内存空间在整个程序运行期间不会被释放,变量与程序共存亡。而对于存储在动态存储区中的变量,其内存由操作系统根据程序运行动态分配。变量的存储类型直接决定了变量的生存期。
变量的存储类型
C语言提供的存储类型主要有以下几种:
- 自动变量 auto
- 静态变量 static
- 外部变量 extern
- 寄存器变量 register
auto自动变量
又称动态局部变量或局部作用域变量,定义于函数内部,是C语言中极为常用的变量类型,因此C语言把它设计为缺省类型(默认类型),即auto可以省略不写,反之,如果没有指定局部变量的存储类型,则默认为auto,因此以下两个语句等价
{
int num=10;
auto int num=10;
}
自动变量数据存储在动态存储区中,操作系统会在代码执行到变量定义语句时为其分配内存,退出所属语句块时释放内存。函数形参和函数内定义的变量(未明确声明为static)都属于该类变量。自动变量有以下特点:
- 自动变量在定义时不会自动初始化。所以除非程序员显式指定初值,否则自动变量的值是随机的垃圾值
- 自动变量在退出函数(语句块)后,其分配的内存立即被释放,再次进入该语句块,该变量被重新分配内存,所以不会保存上一次退出时的值
static静态变量
静态变量分为静态局部变量和静态外部变量,二者用于不同的场景。静态变量在静态存储区分配内存,在整个程序运行期间不会被释放,与程序共存亡。对于已经初始化的静态变量,其内存空间位于静态存储区的.data段,在编译时赋予内存。对于未初始化的静态变量,其内存空间位于静态存储区的.bss段,并在程序初始化时,给变量赋值0(对数值型变量)或空字符(对字符变量)。
静态局部变量
自动变量的内存会在代码退出函数块后被释放,其值也不会被保留,下次进入该函数块,该变量会被分配新的内存,如果希望变量的值在退出函数语句块后依旧被保留,其占用的存储单元不释放,方便函数基于上一次的运算结果进一步进行计算,此时可以使用static关键字将局部变量定义为静态局部变量。
int main(void){
int i,n;
scanf("%d",&n);
for(i=1;i<=n;i++){
Func(i);
}
return 0;
}
long Func(int n){
static long p=1;
return p*n;
}
静态局部变量,有以下特点:
- 如果定义静态局部变量时不赋初值,编译器会自动赋为0(对数值型变量)或空字符(对字符变量)
- 自动变量(动态局部变量)占据的内存在函数调用结束后会被释放,每次调用都需要重新初始化。而静态局部变量仅在第一次调用函数时被初始化一次,其占据的内存在退出函数后不会被释放,再次调用该函数其值上次退出函数时的值
- 虽然静态局部变量在函数调用结束时仍然存在,但其他函数是无法引用它的
静态外部变量
外部变量默认存储类型为extern,可以被其他文件使用extern声明后引用,如果希望外部变量只限于被本文件使用,而不能被其他文件引用,则可以在声明外部变量时添加static声明,将其声明为静态外部变量,该类变量可以应用于以下场景:
- 多人开发时,可以在不同文件中声明同名外部变量而互不干扰
- 权限控制,避免其他文件引用或修改外部变量的值
static int Number;
extern外部变量
如果定义在函数之外的变量没有指定其他存储类别,那么它就是一个外部变量,外部变量是全局变量,作用域是从它的定义点到本文件末尾。但如果要在定义点之前或在其他文件中使用它,那就需要使用关键字extern对其进行声明(注意不是定义,编译器不为其分配内存)
声明语句为:
extern 类型名 变量名;
其中类型名可以省略,因此以下语句等价
extern int Num1;
extern Num1;
注意!
在定义外部变量时,直接在函数外定义 int Num1; 即可
int Num1; 是“定义性声明”语句,会给变量分配内存空间,
extern int Num1; 为“引用性声明”语句,用于拓展外部变量作用域,不会分配内存,不能用来定义一个未初始化的外部变量
但如果在定义时进行初始化,根据右结合原则,会当作定义了变量,并进行了extern作用域拓展处理
extern int Num1=200;//既是定义语句,也是extern声明语句
在一个文件内声明外部变量
如果外部变量不在文件开头定义,那么它的作用域就是从它的定义点到本文件末尾。如果在定义点之前想要引用该变量,则需要在引用之前对使用extern对其进行外部变量声明,声明后就可以从声明之处起,合法使用该外部变量
int main(void){
int getMax(int ,int);
extern Num1,Num2;//外部变量声明(仅作声明,不分配内存)
printf("max is:%d", getMax(Num1,Num2);)//在定义前引用变量
}
int Num1=10,Num2=20;//定义外部变量
int getMax(int x,int y){
return x>y?x:y;
}
在多文件中声明外部变量
C语言程序也可以由多个源文件组成,如果多个文件都需要用到同一个外部变量,不能在多个文件中都同时定义该变量,否则程序在链接阶段会出现“重复定义”错误。正确做法是:在某一个文件中定义外部变量,其他文件中用extern对该变量作“外部变量声明”,将变量的作用域拓展到这些文件中,然后在这些文件中合法引用外部变量。
#include<stdio.h>
int Num;//定义外部变量
int main(void){
int getValue(int);
printf("Input the value of Num:");
scanf("%d",&Num);
printf("%d",getValue(10));
return 0;
}
file2.c文件:
extern Num;//声明A为其他文件已经定义的外部变量
int getValue(int n)
{
return Num*n;
}
register寄存器变量
对于使用频率较高的变量,可以将其声明为寄存器变量,减少CPU对内存的频繁数据访问,使得程序更小,执行速度更快
int main(void)
{
int n;
long countNum(int);
scanf("%d",&n);
countNum(n);
return 0;
}
long countNum(int n)
{
register long i,f=1;
for(i=1;i<n;i++){
f=f*i;
}
return f;
}
寄存器变量有以下特点:
- 只有局部自动变量和形参可以作为寄存器变量
- CPU中的寄存器数量有限,不能定义太多的寄存器变量
- 有的系统把寄存器变量当作自动变量处理,在内存中分配存储空间,并非放到寄存器中
现代编译器能自动优化程序,自动把普通变量优化为寄存器变量,并且可以忽略用户定义的寄存器变量,因此一般无需特别声明变量为register,仅作了解。
数据类型
数据类型总览
基本类型
分类 | 关键字 | 长度(Bytes) | 示例 | |
---|---|---|---|---|
整型 | 短整型 | short | 2 | short a;或 short int a; |
整型 | int | 4 | int a; | |
长整型 | long | 4 | long a;或 long int a; | |
长长整型 | long long | 8 | long long a;或 long long int a; | |
无符号整型 | unsigned | 8 | unsigned int a;
unsigned long a;
|
|
浮点型(实型) | 单精度 | float | 4 | float a; |
双精度 | double | 8 | double a; | |
长双精度 | long double | 12或8 | long double a; | |
字符型 | char | 1 | char a; | |
枚举型 | enum | - | enum res{yes,no,none}; |
构造类型
构造类型 | 关键字 | 示例 |
---|---|---|
数组 | - | int num[10]; |
结构体 | struct | |
共用体 | union | union{int num;char name[5];}man |
其他类型
数据类型 | 关键字 | 示例 |
---|---|---|
指针 | - | int * p; |
无类型 | void | void f1(){…} |
整型
整型从符号位的归属可分为有符号整型(signed)和无符号整型(unsigned)。有符号整型最高位为符号位,0表示该值为正数,1表示该值为负数,而无符号整型的最高位不表示正负,依旧用于储存数值,因此无符号整型只能表示0和正整数,但无符号整型可存储的数值范围要比同位数的有符号整型大2倍,如:signed int数值范围为-32768-32767,而unsigned int 数值范围为0-65535。如果一个整型不声明为unsigned或signed,则默认隐含为signed,因而signed可以省略不写。
对于短整型和长整型,int也可以省略不写(以下[]内的均可省略不写)
类型 | 比特位数 | 取值范围 |
---|---|---|
[signed] short [int] | 16 | -215~(215-1) |
unsigned short [int] | 16 | 0~(216-1) |
[signed] long [int] | 32 | -231~(231-1) |
unsigned long [int] | 32 | 0~(232-1) |
对于整型常量,通常在其数值后加上相应符号表示其具体类型,如:加上U或u表示无符号整型,加上L或者l表示长整型
浮点型
浮点数有两种表示方式:
- 十进制小数形式,如:0.5,1.3,.95(省略0)
- 指数形式,以e或E为底数(其中e代表10),e的左边为数值部分,e的右边为指数部分(必须为整数),如:12.3可以表示为1.23e1、0.123e2、123e-1等形式,如果e左边的数值部分,小数点左边有且只有一位非零的数字,则该表示方法称为“规范化的指数形式”,如:1.23e1为规范化的指数形式,而0.123e2、123e-1不是
C语言默认将浮点型常量作为双精度(double)类型进行处理,如果在常量后加上f或F将作为float型处理,加上l或L则作为long double型处理,eg:1.25e-2f,1.25L
字符
字符常量使用单引号’’包裹,一个字符占用一个字节(中文字符需要两个字节)。字符在内存中以ASCII码形式存储,因此字符型数据和整型数据是通用且可以用于计算的(仅限于0-255之间的整数),常见的应用为大写字符+32转换为小写字符
此外,C语言中还有一些特殊的转义字符,作用如下
字符 | 含义 | 字符 | 含义 |
---|---|---|---|
\n | 换行 | \a | 响铃报警提示音 |
\r | 回车(不换行),将光标位置移到当前行开头 | \“ | 双引号 |
\0 | 空操作字符,常用作字符串结束标志 | \‘ | 单引号 |
\t | 水平制表,跳到下一个Tab位置 | \\ | 反斜线 |
\v | 垂直制表 | \? | 问号 |
\b | 光标位置退一格 | \ddd | 1到3位八进制ASCII码代表的字符 |
\f | 换页 | \xhh | 1到2位十六进制ASCII码代表的字符 |
字符串
字符串常量使用双引号””包裹,字符串末尾会被加上‘\0’作为字符串结束的标志。C语言中没有专门的关键字用来声明字符串变量,因此需要使用字符数组或者指针来存储和处理字符串
字符数组
字符串可以存储于字符数组中,但字符数组中存储的并不一定是字符串,这取决于字符数组最后一个元素是否是字符串结束标志’\0’,’\0’也占用一个字节内存,但它不计入字符串的实际长度,只计入数组长度。注意,如果字符数组最后没有’\0’,系统将无法将该数组当作字符串来处理(如:无法正确被printf(“%s”,xx)输出)
char str[6]={'H','e','l','l','o','\0'};
如果省略对数组长度的声明,则必须人为在初始化列表中添加'\0',否则系统将无法将str当作字符串来处理
char str[]={'H','e','l','l','o'};//长度为5的普通字符数组
char str[]={'H','e','l','l','o','\0'};//长度为6的字符串
用printf("%s",str)语句输出上述变量时,第一个变量由于缺少'\0',输出语句会在输出完Hello后继续输出乱码,直到遇见'\0',而第二个变量会被正常输出
也可以用字符串常量初始化字符数组,这样可以不指定数组大小,且由于字符串常量”Hello”末尾自带’\0’,可以不必人为添加’\0’char str[6]={"Hello"};
char str[]={"Hello"};//数组长度可省略
char str[]="Hello";//大括号可省略
但只允许在定义时整体赋值,不允许在赋值语句中整体赋值char str[6];
str[]={"Hello"};//不允许先定义,定义完在赋值语句中整体赋值,Visual Studio报错:不允许使用不完整的类型
无论用哪种方式初始化字符数组,如果指定数组长度,都要保证预留足够空间以便存储字符串结束标志,即:字符数组大小一定要比字符串的实际字符数大1
可以使用数组下标访问字符串的单个字符,如:
str[1]代表字符’e’
char weekday[][10]={"Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"};
其中最长的字符串"Wendesday"长度为9,因此第二维长度应当为10,对于长度小于10的字符串,其剩余的内存单元会被'\0'填充
如果字符串太长,可以拆分为几个小片段写在不同行,其中空格也将占用数组长度和空间
char longString[]="One day you will leave this world behind"
"so live a life you will remember"
字符指针
字符指针是指向字符型数据的指针变量。每个字符串在内存中都占用一段连续的存储空间,并有唯一确定的地址,因此只要将字符串首地址赋值给指针变量,就可以让字符指针指向一个字符串。对于字符串常量,C语言按字符数组处理,在内存中开辟一个字符数组存储该字符串,字符串常量本身就代表存放它的常量存储区的首地址。
char * ptr="Hello";
等价于
char * ptr;
ptr="Hello";
2. 由于字符串常量存储于只读的常量存储区中,因此只可以修改指针变量的指向,不能修改ptr所指向存储单元的内容
*ptr='a';//错误!写入权限异常,不能修改所指向存储空间的内容
ptr="another";//允许修改指针的指向
3. 通过指针也可以访问单个字符,如:
*(ptr+1)代表字符'e'
也可以修改指针指向位置,使其从所指位置开始输出,直至遇到'\0',以此输出字符串的部分内容,如:
char * sentence="Meet you like the wind";
sentence=sentence+14;
printf("%s",sentence);//输出the wind
字符串的输出
字符串的输出支持以下三种方法:
for(int i=0;i<6;i++){
printf("%c",str[i]);
}
由于字符串长度并不等于字符数组大小,且上述方法无法灵活输出不同长度字符串,因此一般会借助字符串结束标志'\0'来结束字符串输出,如:
for(int i=0;str[i]!='\0';i++){
printf("%c",str[i]);
}
2. 用格式符s,将字符串作为一个整体输出,同样输出到字符串结束标志为止
printf("%s",str)
3. 使用puts()函数(<stdio.h>库函数),puts()函数用于从括号内的参数给出的地址开始,依次输出存储单元中的字符,直到遇到第一个'\0'时输出结束,并自动输出一个换行符,缺点是该函数无法如printf()函数一样添加其他字符信息并控制输出格式
puts(str)
字符串指针变量可以指向一个格式字符串,可以替代printf()函数中的格式,作为一种可变格式的字符串输出
char * format;
format="a=%d,b=%f\n";
printf(format,a,b);
也可以用字符数组实现
char format[]="a=%d,b=%f\n";
printf(format,a,b);
字符串的输入
与输出相似,字符串的输入支持以下三种方法:
for(int i=0;i<6;i++){
scanf("%c",&str[i]);
}
2. 用格式符s,将字符串作为一个整体输入,直至遇到空白字符、空格、回车符、制表符为止,注意!由于字符数组名str本身已经代表字符串的首地址,因此str前不能再加上取地址符&,此外,用scanf()不能输入带空格、制表符的字符串,空格、制表符、回车符及其之后的字符会被留在输入缓冲区
scanf("%s",str);
3. 使用gets()函数(<stdio.h>库函数),gets()函数以回车符作为输入终止符,同时将回车符从输入缓冲区读走,但不作为字符串的一部分,因此该函数可用于输入带空格的字符串
gets(str);
以上方法都有一定的安全隐患,如果输入字符数超过了定义的字符数组大小,多出的字符可能引起缓冲区溢出,带来安全隐患,因此可以使用能限制输入字符串长度的函数
fgets(str,sizeof(str),stdin)
该语句表示从标准输入stdin中读取一行长度为sizeof(str)的字符串存储到str为首地址的存储区中,多余的字符舍弃
char * ptr;//ptr未被初始化
scanf("%s",ptr);
上述代码,编译时给指针变量ptr分配了内存单元,但ptr中的值是一个不可预料的垃圾值,该指针指向一个未确定的存储单元,可能指向空白存储区,也可能指向已经存放指令或数据的内存段,在现代编译器中会报错使用了未初始化的局部变量“ptr”,然而在低版本编译器中可能顺利通过编译,带来潜在安全隐患
因此,推荐使用字符数组输入字符串
char str[10];
scanf("%s",str);
如果使用字符指针输入字符串,则保证该指针指向了确定的地址
char str[10];
char * ptr=str;
gets(ptr);
构造类型
数组
一维数组
定义方式:
数组名的命名规则遵守标识符命名规范,数组长度需要使用常量表达式,不能包含变量,一维数组的初始化可以使用以下方法:
- 在定义时赋予全部初值,如:int nums[5]={0,1,2,3,4};
- 也可以只给一部分元素赋值,其余元素会被赋0(char类型赋’\0’),如:int nums[5]={0,1};
- 如果全部元素相同,可以只写一个值,如:int nums[5]={0};
- 如果对全部元素赋予初值,由数据个数已经确定,可以不指定数组长度,如:int nums[]={0,1,2,3,4};
int nums[5]={11,22,33,44,55};
但不允许在赋值语句中被整体赋值
int nums[5];
nums[]={11,22,33,44,55};//错误
这是由于数组在定义时,编译系统就会为其分配连续的内存单元,其内存空间大小就已确定,后续的整体赋值语句如果执行,可能会导致内存覆盖或溢出等不可预期的行为,所以需要注意用数组和指针操作字符串时的不同情况
char str[6];
str[]={"Hello"};//错误
char * ptr;
ptr="Hello";//正确
2. 数组名代表数组首元素的内存地址,即数组的起始地址,是一个地址常量,不能像修改指针变量一样对数组名进行++或--等操作
int a[10];int *p;
p=a;
p++;//允许,指针指向a[1]
a++;//不允许,a是数组首地址,无法修改
3. C语言数组无length属性,获取数组长度可借助sizeof()函数
int nums[] = {1,2,3,4,5};
printf("%d", sizeof(nums)/sizeof(nums[0]));
二维数组
定义方式:
对数组名和数组长度的要求同一维数组,二维数组的初始化可以使用以下方法:
- 分行赋值,如:
int nums[3][2]={{1,2},{3,4},{5,6}}
- 也可以一次性统一赋值,但不如第一种赋值方法清晰,如:
int nums[3][2]={1,2,3,4,5,6}
- 可以只给部分元素赋值,同样其余元素会被赋0(char类型赋’\0’),如:
int nums[3][2]={{1},{},{0,6}}
- 如果对所有元素赋初值,可以不指定第一维的长度,但第二维的长度声明永远不能省略
结构体
结构体定义
对于复杂数据对象,仅仅使用几种基本数据类型无法准确反映它们之间的内在联系,也无法做统一处理,于是衍生出了允许用户自定义的数据类型,C语言中称之为构造数据类型(复合数据类型),结构体就是构造数据类型的典型代表
结构体模板的声明格式如下:
结构体模板只是声明了一种数据类型,并未声明结构体类型的变量,编译器不为其分配内存,需要再使用以下语句声明结构体变量,编译器才会为结构体变量分配内存:
struct 结构体名 结构体变量名;
- 结构体的名字又称为结构体标签,用于标记用户自定义的结构体类型,struct+结构体名 共同构成一个数据类型,如:下述例子中的struct student是一个类型名,作用同系统提供的标准类型(如int、char等),因此不为其分配内存
- 结构体成员的命名遵从变量的命名规则
- 结构体成员也可以为一个结构体,即结构体可以嵌套
- 结构体成员名可以与程序其他位置的变量名相同,二者互不干扰
struct student
{
long studentID;
char studentName[10];
int score[4];
};//声明结构体模板,末尾;不可省略
struct student stu1,stu2;//声明结构体变量
也可以在声明结构体模板时,定义结构体变量:
struct student
{
long studentID;
char studentName[10];
int score[4];
}stu1,stu2;
eg:当结构体模板与结构体变量一起声明时,结构体名是可以省略的,但该方法由于未指定结构体名,无法在程序其他地方再定义结构体变量,因此并不常用
struct//无结构体名
{
long studentID;
char studentName[10];
int score[4];
}stu1,stu2;
关键字typedef用于为已有数据类型定义一个别名,数据类型别名通常大写,便于区分已有数据类型。定义结构体时可用typedef定义一个结构体别名,便于使用更简洁的形式定义结构体变量
struct student
{
long studentID;
char studentName[10];
int score[4];
};
typedef struct student STUDENT;
与
typedef struct student
{
long studentID;
char studentName[10];
int score[4];
}STUDENT;
二者都为struct student类型定义了一个新名字STUDENT,因此以下两条定义结构变量的语句等价:
STUDENT stu1,stu2;//简洁形式
struct student stu1,stu2;
结构体变量初始化与成员引用
结构体变量的成员可以通过将成员初值置于花括号内进行初始化
STUDENT stu1={114604,"SHIWIVI",{111,222,333,444}}
也可以在定义结构体模板和变量同时初始化
struct student
{
long studentID;
char studentName[10];
int score[4];
}stu1={114604,"SHIWIVI",{111,222,333,444}}
访问结构体变量的成员必须使用成员选择运算符(圆点运算符),访问格式为:
结构体变量名.成员名
- C语言规定,不能将结构体变量作为一个整体输入、输出,如:printf(“%d%s..”,stu1)为非法语句
- 只能通过用圆点运算符访问结构体成员的方式来输入输出,如:printf(“%s”,stu1.studentName)
- 成员运算符.在所有运算符中优先级最高,因此通常可以将stu1.studentName当成一个整体看待
- 结构体嵌套时,必须以级联方式访问结构体成员,如:stu1.birthday.year
- 结构体成员可以像普通变量一样进行各种运算,如:stu1.birthday.year++
- C语言允许对具有相同结构体类型的变量进行整体赋值,如:stu1初始化后,可以通过stu2=stu1对stu2进行初始化,结构体成员会进行逐一顺序赋值
- 也可以使用=号对结构体成员进行分开赋值,但注意,当结构体成员为字符数组时,由于字符数组名是该数组的首地址,是一个地址常量,不能作为赋值表达式左值,因此对字符数组类型的结构体成员进行赋值时,必须使用字符串处理函数strcpy()
- 结构体变量的地址是其所占存储空间的首地址,而结构体成员的地址与该成员在结构体中的所处位置和所占内存字节数有关,可以单独访问成员地址,如:&su1.studentID
#include<stdio.h>
typedef struct date
{
int year;
int month;
int day;
}DATE;
typedef struct student
{
long studentID;
char studentName[10];
DATE birthday;
int score[4];
}
int main(void){
STUDENT stu1={114604,"SHIWIVI",{1998,10,1},{111,222,333,444}};
STUDENT stu2;
stu2=stu1;//整体赋值
//也可以对结构体成员分开赋值,整体赋值等价于下述赋值语句
stu2.studentID=stu1.studentID;
strcpy(stu2.studentName,stu1.studentName);
stu2.birthday.year=stu1.birthday.year;
...
stu2.score[0]=stu1.score[0];
...
//通过键盘输入成员值时,单独访问成员变量地址
scanf("ld%",&su1.studentID);
scanf("%s",stu1.studentName);
scanf("%d",&stu1.birthday.year);
for(int i=0;i<4;i++){
scanf("%d",&stu1.score[i]);
}
return 0;
}
结构体所占内存字节数
系统为结构体变量分配内存大小时,结构体类型所占字节数,并非是所有成员所占内存字节数的总和,这是因为许多计算机系统为了提高寻址效率,处理器体系为特定的数据类型引入了内存对齐需求,编译器为了满足处理器的对齐要求,会在较小的成员后加入补位,从而导致结构体实际所占内存字节数会比预计的多出一些字节。
如:32位计算机体系结构,short型数据从偶数地址开始存放,int型数据则被对齐在4字节地址边界,这样就保证了一个int型数据通过一次内存操作就能被访问到,而读取存储在没有对齐地址处的32位整数,则需要两次读取操作,再从两次读取到的64位数据中提取该32位整数相关的数据,这样会导致系统性能下降
因此计算结构体类型大小时一定要使用sizeof()运算符,不能想当然直接对各成员所占内存进行简单求和。
#include<stdio.h>
typedef struct sample {
char c1;
int num;
char c2;
}SAMPLE;
int main(void) {
SAMPLE s = { 'a',10,'b' };
printf("%d", sizeof(s));//12而非6
return 0;
}
根据内存对齐需求,如下图所示,c1和c2后会被增补3个字节补位,以达到与成员变量num内存地址对齐的要求,因此结构体变量s将占用12个字节的存储单元而非6个字节。如果将int类型改为short,则c1和c2将以short(2字节)为基准,后补1个字节空闲存储单元,s将占用6个字节内存
结构体数组
与普通数组类似,结构体数组也可以在定义时统一初始化
STUDENT stu[3]={{1101,"LiLin",11,22,33,44},{1102,"ZhangKe",10,20,30,40},{1103,"MaLong",15,25,35,45}};//数组长度可以省略不写
或
struct student
{
long studentID;
char studentName[10];
int score[4];
} stu[3]={{1101,"LiLin",11,22,33,44},{1102,"ZhangKe",10,20,30,40},{1103,"MaLong",15,25,35,45}};
访问数组元素的方法与普通数组一样
printf("%ld",stu[1].studentID);
指向结构体的指针
可以使用一个指针变量p指向一个结构体变量,此时该指针变量的值就是结构体变量的起始地址,随后,就可以使用指向运算符(箭头运算符)访问结构体的成员,以下三种访问结构体成员变量的方式等价:
- 结构体变量.成员名
- (*p).成员名
- p->成员名
struct student stu1={1101,"LiLin",11,22,33,44}; struct student * p=&stu1; printf("%ld",stu1.studentID); printf("%ld",(*p).studentID); printf("%ld",p->studentID);
指针也可以指向结构体数组,操作同指向普通数组类似,对指针进行的++操作会使指针指向下一个结构体数组元素
STUDENT stu[3]={{1101,"LiLin",11,22,33,44},{1102,"ZhangKe",10,20,30,40},{1103,"MaLong",15,25,35,45}};
struct student * p=stu;
for(;p<stu+3;p++){
printf("%s\n",p->studentName);
}
//p被定义为是指向struct student类型的指针,如果需要将其指向其他类型,可以使用强制类型转换
p=(struct student *)stu[0].studentName;
printf("%s",p);//输出stu[0]的studentName
//但此时p仍然保持原来的类型,p+1将指向stu[1]的studentName
printf("%s",p+1);
共用体
共用体(也称为联合,Union)是将不同类型的数据存放在一起,占用同一段内存的一种构造数据类型,共用体的声明形式与结构体类似:
- 共用体类型所占内存大小取决于其成员中所占内存空间最大的成员变量
- 共用体同一内存段可以用来存放不同类型的数据,但在同一时间只能存放其中一种类型的成员,也就是说,同一时刻只能有一个成员起作用
- 共用体变量中起作用的成员是最后一个被赋值的成员,在存入一个新的成员值后,原有的成员值会被覆盖
- 共用体的成员一般单独赋值、调用
- 共用体变量的地址和其各成员的地址都是同一个地址
- 共用体不能进行比较操作
- 共用体不能作为函数参数,也不能作为函数返回值,但可以使用指向共用体变量的指针
- 不能只引用共用体变量,需要引用共用体的具体成员
union sample
{
short i;
char ch;
float f;
};
union sample a;//共用体a中i,ch,f共占一段内存空间,因此a的大小由成员最大数据类型float决定,占用4个字节
a.i=10;//可以给共用体成员单独赋值
a.ch='R';
a.f=11.11;//完成上述3个赋值运算后,只有a.f有效,a.i和a.ch将被覆盖失效
//如果在定义时初始化共用体的成员初值,C89规定只能对共用体的第一个成员进行初始化,但c99无该限制,允许按以下形式按名设置成员初值
union sample b={.i=10,.ch='b',.f=22.22};//但也只有f会生效
共用体可以用来存储程序中逻辑相关但情形互斥的变量,共享内存空间可以节省内存,也避免了操作失误引起的逻辑冲突。如:职工管理系统中,职工的个人婚姻状况只能有三种情况:未婚、已婚、离婚。且这三种情况应当是互斥的,此时可以用共用体来存储该数据
struct date
{
int year;
int month;
int day;
};
struct marriedState //已婚信息
{
struct date marryDate;//记录结婚日期
char spouseName[20];//记录配偶姓名
};
struct divorceState //离婚信息
{
struct date divorceDay;//记录离婚日期
};
union maritalState //共用体,存储婚姻状况
{
int single;//未婚
struct marriedState married;//已婚
struct divorceState divorce;//离婚
};
struct person
{
char name[20];
char sex;
int age;
union maritalState marital;
int marryFlag; //共用体无法直接看出是哪个成员生效,因此使用一个变量标记婚姻状态
};
共用体也可以用来构造混合数据结构,高效使用存储空间,如:需要存储的数据是int和float型数据的混合,可以定义如下共用体:
typedef union
{
int i;
float f;
}NUMBER;
NUMBER array[20];//既可以存储int数据,也可以存储float数据
枚举
当一个变量只有几种固定的值时,就可以使用枚举表示,枚举类型的定义方法与结构体类似,需要使用enum关键字:
- 枚举常量都是整型常量,除非特别指定,否则其值按顺序为0,1,2….
- 允许在定义时明确指定枚举常量的值,如:enum response {no=1,yes=1,none=0},也可以只指定前几个值,其后的值会自动递增,如:enum month{Jan=1,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep,Oct,Nov,Dec};
- 程序员在花括号内定义的枚举常量只作为标识符使用,并不代表什么含义,其值本质仍是整型常量,不是字符串
- 枚举常量可以作为整型值进行比较、输出操作
- 可以定义枚举型数组,如:enum response answers[10];
enum response{no,yes,none};
enum response res1,res2;
或
enum response{no,yes,none} res1,res2;
或
enum {no,yes,none} res1,res2;//枚举类型和枚举变量一起定义时,枚举标签可省略
1. 枚举变量的赋值只能从枚举常量中选取
enum response{no,yes,none} res1,res2;
res1=yes;//只能赋值no、yes、none中的其中一个
2. 枚举变量可以用于条件语句,比较规则是按其定义时的整型值进行比较,默认情况下为:no=0,yes=1,none=2
if(res1==yes)
if(res1>none)
3. 枚举常量的值是整型常量,因此只能作为整型值处理,而不能作为字符串来使用
printf("%d",res1);//正确,输出res1为1
printf("%s",res1);//错误!不能作为字符串使用,无法输出yes
4. 虽然枚举常量都表示为整型常量,但不能直接将整型常量赋值给枚举变量,因为它们属于不同类型
res1=2; //错误
可以进行强制类型转换,相当于将值为2的none赋给res1,2也可以替换为表达式
res1=(enum response)2;
指针
指针定义
定义 | 含义 |
---|---|
int * p | 指向整型数据的指针变量 |
int *p[n] | 指针数组,数组中含n个指针 |
int (*p)[n] | 二维数组的行指针,p指向含n个元素的一维数组 |
int * p() | p为返回指针的函数,该指针指向整型值 |
int (*p)() | p为指向函数的指针,该函数返回整型值 |
int **p | p为指向指针的指针,p指向的指针指向整型数据 |
- 指针:一个变量的地址称为该变量的”指针”,反之亦然,变量的指针即变量的地址
- 指针变量:专门用于存放地址的变量
- &:取地址运算符
- *:指针运算符,取出指针变量指向的内容,又称为间接寻址运算符
指针的定义:数据类型 * 变量名
int a=10;
int * p;//定义int *类型的变量p,p只能用于存储地址,*只是表明该变量是指针变量
p=&a;//取出变量a的地址并存储到p中
printf("%d",* p);//*p表示取出变量p中地址指向的内容,等价于printf("%d",a)
printf("%p",&a);//输出变量a在内存中所占存储空间的首地址,等价于printf("%p",p)
使用指针的注意事项:
- 不要使用未初始化的指针变量,未决定指针指向时,可以使指针变量初始化为NULL(stdio.h中定义为零值的宏)
- 要永远清楚指针指向哪里,指针必须指向一块有意义的内存
- 要永远清楚每个指针指向的对象内容是什么
指针与数组
使用指针操作数组可以有效提高运行速度,减小内存占用。数组在定义时,编译系统就会为其分配连续的内存单元,相应地,内存首地址也确定了。C语言中,数组名代表数组首元素的内存地址(数组名作形参时除外,此时数组名不占据内存单元),因此可以直接将数组名赋值给指针变量
- 数组变量名代表数组起始内存地址
- 数组首元素地址也是数组起始内存地址
而后,就可以使用指针引用数组元素。对于指针变量p,p+1将指向数组的下一个元素,即地址跳到1*sizeof(基类型)个字节以后,而非对p中的地址进行简单加1,如:如果p指向int类型的数组,p+1会将p中的地址值增加4个字节对应的地址值,使其指向4个字节以后的空间。使用指针引用数组元素有以下特点:
实际上,读取数组元素a[i]就是通过*(a+i)处理,即在数组首元素地址的基础上加上地址偏移量得到要找元素的地址,然后取出该地址中的数据,由此可得,[ ]实际上是变址运算符,指向数组的指针变量也可以使用该变址运算符,如:p[i]等价于*(p+i)等价于i[p]
常用的指针变量运算
- *p++ 右结合,解析为*(p++),先获得*p的值,再使p+1=>p
- *(++p) 先对p加1,使其指向下一个元素,再获得此时*p的值
- (*p)++ 获得*p的值,并对该值加1
- *(p--),*(--p),(*p)-- 与上类似
数组元素的遍历
在遍历数组元素时,通过数组下标或通过数组元素地址遍历,二者执行效率是相同的,C编译系统对nums[i]的处理也是将其转换为*(nums+i)来作处理的,即根据首元素地址重新计算元素偏移地址,再访问该地址
而通过指针变量遍历时,指针变量通过有规律地改变地址值(p++),直接指向元素,不必每次都重新计算地址,极大提高了执行效率
数组名nums代表数组元素首地址,是一个指针常量,它的值是固定不变的,因此只能像*(nums+i)一样基于此地址进行计算,不能使用nums++改变该地址值
for(int *p=nums;nums<(p+5);nums++){//错误代码
printf(“%d”,*a)
}
指针与函数传参
使用指针作为函数参数时,不再是简单的值传递,而是地址传递,可以直接修改原参数的值
#include<stdio.h>
int main(void) {
void inv(int*, int);
int nums[10] = { 0,11,22,33,44,55,66,77,88,99 };
printf("original array:");
for (int* p = nums; p < nums + 10; p++) {
printf("%d ", *p);
}
inv(nums, 10);
printf("\ninverted array:");
for (int* p = nums; p < nums + 10; p++) {
printf("%d ", *p);
}
return 0;
}
void inv(int * p,int n) {//n为数组长度
int temp;
for (int i = 0; i < n / 2; i++) {
temp = *(p+i);
*(p + i) = *(p + n - 1 - i);
*(p + n - 1 - i) = temp;
}
}
在函数中修改数组值,实参与形参的对应关系一般有以下4中
1. 形参和实参都用数组名,形参接收实参的数组首地址,因此形参和实参数组共用一段内存空间。定义形参a[]时,可以不指定数组长度,因为编译器实际上是将形参数组名作为指针变量来处理,并不会真的开辟一个数组空间
void inv(int a[],int n) inv(nums,10)
2. 实参用数组名,形参用指针变量。形参的指针初始指向数组元素首地址
void inv(int *p,int n) inv(nums,10)
3. 实参和形参都用指针变量。实参n先指向数组首地址,再将该地址值传递给形参p,初始时两个指针指向同一个地址
void inv(int *p,int n) int *n=nums; inv(n,10)
4. 实参为指针变量,形参为数组名。编译时,编译系统会将形参数组名作为a作为指针变量来处理,同样,初始时两个指针指向同一个地址
void inv(int a[],int n) int *n=nums; inv(n,10)
二维数组与指针
一维数组可以拓展到二维数组、三维数组等等,以二维数组为例,一个二维数组可以视为是由多个一维数组构成的,即二维数组的数组元素也是一个数组。
int a[3][4]={{0,0,0,0},{11,11,11,11},{22,22,22,22}};
对于该数组,可以视为数组a中包含3个元素:a[0],a[1],a[2],而这3个元素每个都是一维数组:
a[0]==> a[0][0]、a[0][1]、a[0][2]、a[0][3]
a[1]==> a[1][0]、a[0][1]、a[0][2]、a[0][3]
a[2]==> a[2][0]、a[0][1]、a[0][2]、a[0][3]
暂且忽略a[0],a[1],a[2]内部包含的内容,将它们当作普通的数组元素,由此:
数组a是包含a[0]、a[1]、a[2]三个数组元素的一维数组,其中:
a是数组首元素a[0]的地址(即&a[0]),a+1是a[1]的地址(&a[1]),a+2是a[2]的地址(&a[2]),所以:
a=&a[0],a+1=&a[1],a+2=&a[2];
*(a+0)=a[0],*(a+1)=a[1],*(a+2)=a[2];
此时将a[0]、a[1]、a[2]展开,这三个元素每个都是一维数组,在这三个数组内部,a[0]、a[1]、a[2]分别是它们内部一维数组的数组名,又因为C语言中数组名代表数组首元素地址,因此:
a[0]即为a[0][0]的地址,即&a[0][0],a[0]+1则为a[0][1]的地址 &a[0][1]……
即:
a[0]+0=&a[0][0],a[0]+1=&a[0][1],a[0]+2=&a[0][2],a[0]+3=&a[0][3]
*(a[0]+0)=a[0][0],*(a[0]+1)=a[0][1],*(a[0]+2)=a[0][2],*(a[0]+3)=a[0][3];
由于a[0]和*a等价,因此
a[0]+0与*(a+0)均是a[0][0]的地址,a[0]+1与*(a+0)+1均是a[0][1]的地址,其他同理,由此可得:
*(a[0])=*(*(a+0)+0)=**a=a[0][0],*(a[0]+1)=*(*(a+0)+1)=*(*a+1)=a[0][1]
*(a[1]+2)=*(*(a+1)+2)=a[1][2],*(a[2]+3)=*(*(a+2)+3)=a[2][3]
其他以此类推
注意!
对于一维数组,a[i]代表一个数组元素,占据内存单元,拥有物理地址,但在二维数组中,a[i]代表一维数组名,只是一个地址(如同一维数组中的数组名只是一个指针常量一样),务必记住,a[i]和*(a+i)是等价的
由上,总结:
表示a[i][j]地址的方法:
- a[i]+j
- *(a+i)+j
- &a[i][j]
表示a[i][j]值的方法:
- *(a[i]+j)
- *(*(a+i)+j)
- (*(a+i))[j]
- a[i][j]
二维数组的行指针与列指针
二维数组存储 | ||||
---|---|---|---|---|
行/列地址 | a[i]+1 | a[i]+2 | a[i]+3 | a[i]+4 |
a+0 | a[0][0] | a[0][1] | a[0][2] | a[0][3] |
a+1 | a[1][0] | a[1][1] | a[1][2] | a[1][3] |
a+2 | a[2][0] | a[2][1] | a[2][2] | a[2][3] |
其中,二维数组名a是指向行地址的,因此a+1中的"1"代表一个含有4个整型元素的一维数组所占存储单元的字节数,即4*sizeof(int),a+1将指向下一行
一维数组名a[0]、a[1]、a[2]是指向列元素的,a[0]+1中的1代表一个整型元素所占存储单元的字节数,a[0]+1将指向下一个列元素
在指向行的指针前加一个*,就转换为指向列的指针,如:指针a和a+1是指向行的,而*a和*(a+1)则是指向列的,它们拓展为*(a+0)+0和*(a+1)+0,分别指向第0行0列元素和第1行第0列。反之,在指向列的指针前加上&,该指针就变成了指向行的指针,如:列指针a[0]指向第0行第0列的元素,而&a[0]等价于&*(a+0),而&*a等价于a,因此&a[0]是指向第0行的行指针。注意,&a[i]不能理解为a[i]的物理地址,因为二维数组中并不存在a[i]元素,它只是一个地址计算方法,能得到第i行的起始地址。
练习: a、a+0、&a[0]代表第0行首地址 *a、*(a+0),a[0],a[0]+0代表第0行第0列的地址 &a[0][0]代表第0行第0列元素地址 a[0][0]、**a代表第0行第0列元素值 a+i、&a[i]代表第i行地址 a[i]+j、\*(a+i)+j,&a[i][j]代表第i行第j列地址 a[i][j]、\*(\*(a+i)+j)代表第i行第j列元素值二维数组元素的引用(指针变量)
通过二维数组的列指针变量引用
由于列指针指向数组的具体元素,因此定义列指针与定义同类型普通指针相同:
int * p
可以用以下三种等价方法对其进行初始化:
- p=a[0]
- p=*a
- p=&a[0][0]
此时,由于需要使用列指针对二维数组元素进行引用,因此可以将二维数组看成一个由m行*n列个元素组成的一维数组,获取第i行j列的元素,则其在一维数组中的索引为i*n+j,因此a[i][j]元素的地址为p+i*n+j,其值表示方法为*(p+i*n+j)或p[i*n+j]
注意,此时不能用p[i][j]来表示数组元素,因为给p赋初值时将其赋为了列指针,即是将二维数组作为一维数组来进行处理的,p++将使指针依次指向下一个数组元素。正因如此,在定义二维数组的列指针时,无须指定它所指向的二维数组的列数。因此二维数组的列指针也常常用作函数参数,以实现二维数组的行列数需要动态指定的场合。
#include<stdio.h>
int main(void) {
void outputArray(int *,int);
int a[3][4] = { {11,11,11,11},{22,22,22,22},{33,33,33,33} };
outputArray(a[0], 12);//列指针初始化int *p=a[0]
outputArray(*a, 12);//列指针初始化int *p=*a
outputArray(&a[0][0], 12);//列指针初始化int *p=&a[0][0]
return 0;
}
void outputArray(int* p, int length) {
for (int* index = p; index < p + length; index++) {
printf("%d,", *index);
}
printf("\n");
}
通过二维数组的行指针变量引用
由于行指针不再指向数组元素,而是指向一维数组,因此行指针变量比较特殊:
int (*p)[4]
可以用以下方法对其进行初始化:
- p=a
- p=&a[0]
说明:定义了一个可指向含有4个元素一维整型数组的指针变量,[4]表示所指一维数组的长度,在声明变量时必须显式指定,不可省略!该指针可以作为一个指向二维数组的行指针,且它所指向的二维数组的每一行有4个元素。注意:定义该行指针变量时( )也不可省略,因为[ ]优先级高于*,p会优先与[ ]结合,int *p[4]是指针数组的定义方式。
可以用以下四种等价形式引用a[i][j]的值:
- p[i][j]
- *(p[i]+j)
- *(*(p+i)+j)
- (*(p+i))[j]
对于行指针变量,p++将指向二维数组的下一行,即移动4*sizeof(int)字节,因此需要显式指定所指一维数组的长度int (*p)[4],否则无法计算指针移动的字节数。
#include<stdio.h>
#define N 3
int main(void) {
//用普通的二维数组方式操作数组
void inputArray(int p[][N], int,int);
void outputArray(int p[][N], int,int);
//只使用行指针操作数组元素
void inputArray(int(*p)[N], int, int);
void outputArray(int(*p)[N], int, int);
int a[2][3];
printf("Input 2*3 numbers:\n");
inputArray(a, 2,3);//把数组行数和列数也传参
outputArray(a, 2,3);
return 0;
}
void inputArray(int p[][N], int rows,int columns) {
void inputArray(int (*p)[N], int rows, int columns) {
printf("Input numbers:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++)
{
scanf_s("%d", &p[i][j]);
scanf_s("%d", *(p+i)+j);
}
}
}
void outputArray(int p[][N], int rows, int columns) {
void outputArray(int (*p)[N], int rows, int columns) {
printf("Output numbers:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++)
{
printf("%d,", p[i][j]);
printf("%d,", *(*(p+i)+j));
}
}
}
指向函数的指针
一个函数在编译时被分配一个入口地址,与数组类似,函数名代表该函数的入口地址,函数的入口地址就称为该函数的指针,可以用一个指针变量指向函数,然后通过该指针变量调用该函数,指向函数的指针变量的定义形式为:
函数返回值类型 (* 指针变量名)(函数参数)
- 定义举例:int (*p)(int,int),表示定义一个指向函数的指针变量p,它可以任意指向同类型的不同函数(该函数应是int类型且有两个int参数),并非固定指向一个函数
- *p两侧的括号不能省略,p先与*结合表明是指针变量,再与后面的( )结合表明该指针变量指向函数
- 对指向函数的指针变量进行p++、p+n等操作是无意义的
int main(void){
int getMax(int,int);
int (*p)(int,int);//定义指向函数的指针
int a=10,b=20;
p=getMax;//将函数地址赋给p,不涉及实参和形参问题,因此不需要写任何参数
int result=(*p)(a,b);//使用指针调用函数,只需用(*p)替代函数名即可
return 0;
}
int getMax(int a,int b){
return a>b?a:b;
}
函数指针变量的用途之一是将函数作为参数传递到其他函数,即实现函数入口地址的传递,常见用法是:调用同一个函数时,在不同情况下实现不同的功能
#include<stdio.h>
int main(void) {
int getMax(int, int);
int add(int, int);
int multiply(int, int);
void process(int, int, int (* fun)(int, int));
int a = 10, b = 20;
process(a, b, getMax);
process(a, b, add);
process(a, b, multiply);
return 0;
}
void process(int a,int b,int (* fun)(int,int)) {
int result =(*fun)(a, b);//fun指向不同函数地址,实现不同功能
printf("%d\n", result);
}
int getMax(int a, int b) {
return a > b ? a : b;
}
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
返回指针的函数
函数除了可以返回整型值、字符值等,也可以返回一个指针(返回一个地址),这种返回指针的函数,定义形式为:
类型 * 函数名(参数)
- 函数定义举例:int * fun(int,int),定义一个函数fun,该函数返回类型为int *,即返回一个指针,该指针指向int类型的数据,函数名fun和*两侧没有( ),fun先与(int,int)结合表明fun()是个函数
- 通过返回指针,C语言就可以实现返回数组、字符串等数据
#include<stdio.h>
int main(void) {
char* str1 = "aaa", * str2 = "111";
char result[20];//存储拼接后的字符串,空间要足够大
char* strCat(char*, char*,char *);
printf("%s", strCat(result, str1, str2));
return 0;
}
char* strCat(char * result,char* str1, char* str2) {
char* p = result;
while (*str1 != '\0') {
*p = *str1;
str1++;
p++;
}
while (*str2 != '\0') {
*p = *str2;
str2++;
p++;
}
*p = '\0';//拼接后的字符串末尾加上字符串结束标志符
return result;//返回result数组的初始起始地址
}
指针数组
一个数组,若其元素均为指针,则该数组为指针数组,一维指针数组的定义形式为:
类型名 * 数组名[数组长度]
- 如:int *p[4],p先与[4]结合,形成p[4],表明这是一个数组,再与*结合,表明该数组是指针类型
- 注意区分于int (*p)[4],这是指向一维数组的指针变量,即指向二维数组指针变量中的行指针
- 指针数组可用于指向不等长字符串、不等长数组等场景
#include<stdio.h>
#include<string.h>
int main(void) {
void sort(char* ptr[], int length);
char* ptr[]= { "Document","ASCII","Java","CPP" };
sort(ptr, 4);
for (int i = 0; i < 4; i++) {
printf("%s\n", ptr[i]);
}
return 0;
}
void sort(char* ptr[], int length) {
char* temp;
int i, j, k;
for (i = 0; i < length-1; i++) {
k = i;
for (j = i + 1; j < length; j++) {//内循环找出全部字符串中字符顺序最小的字符串
if (strcmp(ptr[k], ptr[j]) > 0) {//strcmp()函数,ptr[k]中的字母在ASCII中位置大于ptr[j],则返回值大于0
k = j;
}
}
if (k != i) {//如果找到的字符串不是i,调换顺序
temp = ptr[i]; ptr[i] = ptr[k]; ptr[k] = temp;
}
}
}
指向指针的指针
指针变量也可以指向一个另一个指针,指向指针的指针变量定义形式为:
类型 *** 变量名
- 如:char **p,指针变量p指向另一个指针变量,被p指向的指针变量指向一个字符型数据
- 结合上一节的指针数组,就可以用指向指针的指针操作指针数组中的指针所指向的数据
- 理论上指向指针的指针可以无限套娃,如:int *****p,但多层指针嵌套容易混乱,难以理解
#include<stdio.h>
int main(void){
char* ptr[]= { "Document","ASCII","Java","CPP" };
char **p;
for(int i=0;i<4;i++){
p=ptr+i;
printf("%s\n",*p);
}
}
2. eg:注意输出字符串、数组数据与整型等其他普通类型数据时,指针运算符(间接寻址运算符)需要取到哪一层值(取出的值是地址值还是变量本身的值)
#include<stdio.h>
int main(void){
int a = 11, b = 22;
int c = a + b;
int d = a + b + c;
int* nums[] = { &a,&b,&c,&d };
int** p=nums;
for (int i = 0; i < 4; i++,p++) {
printf("%d\n",**p);
}
return 0;
}
void * 无类型指针
void *称为通用指针或无类型指针,用于声明基类型未知的指针变量,即声明了一个指针变量,但未指定它指向哪一种基类型的数据。它可以指向任意类型的数据,只需要在将它的值赋给其他类型的指针变量时,进行强制类型转换即可。ANSI标准要求动态分配内存的函数(malloc、calloc等)返回类型为void *无类型指针,方便将其转换为其他任意类型的指针变量。
注意区别于空指针(NULL),空指针未指向任何有效内存(指向低地址保留区),而void *当值不为NULL时,它指向了有效内存空间,只是尚未指定它指向的基类型。
char *str;
void *p;
str=(char *)p;
或
p=(void *)str;
动态分配内存函数返回值根据指向数据类型需要转换为其他类型
int * p=(int *)malloc(10*sizeof(int));
指针数组与main函数的形参
一般情况下,main函数一般会写为空参数形式:int main(void),实际上main函数可以带有参数,如:
int main(int argc,char * argv[])
Java语言中的main默认就为上述形式,argc和argv就是main函数的形参。main函数由操作系统调用,在main所在的.c文件被编译后,会生成可执行文件(windows下后缀为.exe),在系统命令终端下(如:Linux的shell、windows的cmd),输入文件路径/可执行文件名 参数1 参数2…即可调用该可执行文件并将参数传递给main函数
int main(int argc,char * argv[]){
while(argc>1){
++argv;
printf("%s\n",*argv);
--argc;
}
}
运算符
运算符优先级与结合性
优先级 | 运算符 | 名称 | 使用 | 说明 | 结合方式 |
---|---|---|---|---|---|
1 | [ ] | 数组下标 | 数组名[] | 从左到右 | |
( ) | 圆括号 | (表达式) | . | 成员选择(对象) | 对象.成员名 | -> | 成员选择(指针) | 对象指针->成员名 |
2 | - | 负号 | -表达式 | 单目运算符 | 从右到左 |
(类型) | 类型转换 | (数据类型)表达式 | |||
sizeof( ) | 长度运算符 | sizeof(表达式) | |||
! | 逻辑非 | !表达式 | |||
~ | 按位取反 | ~表达式 | |||
* | 取值 | *指针变量 | |||
& | 取地址运算符 | &变量名 | |||
++ | 自增 | ++变量名或 变量名++ | |||
-- | 自减 | --变量名或变量名-- | |||
3 | * | 乘 | 表达式*表达式 | 双目运算符 | 从左到右 |
/ | 除 | 表达式/表达式 | |||
% | 求余 | 表达式%表达式 | |||
4 | + | 加 | 表达式+表达式 | 双目运算符 | 从左到右 |
- | 减 | a-b | |||
5 | << | 左移 | 变量<<表达式 | 双目运算符 | 从左到右 |
>> | 右移 | 变量>>表达式 | |||
6 | <、<=、>、>= | 关系运算(大于,大于等于,小于,小于等于) | a<b | 双目运算符 | 从左到右 |
7 | ==、!= | 是否等于 | if(a==b) | 双目运算符 | 从左到右 |
8 | & | 按位与 | a&b | 双目运算符 | 从左到右 |
9 | ^ | 按位异或 | a^b | 双目运算符 | 从左到右 |
10 | | | 按位或 | a|b | 双目运算符 | 从左到右 |
11 | && | 逻辑与 | a&&b | 双目运算符 | 从左到右 |
12 | || | 逻辑或 | a||b | 双目运算符 | 从左到右 |
13 | ? : | 条件运算 | a>b?true:false | 三目运算符 | 从右到左 |
14 | = | 赋值运算 | a=10 | 双目运算符 | 从右到左 |
+=、-=、*=、/=、%=、>>=、<<=、&=、^=、|= | 运算后赋值 | a+=10 | |||
15 | , | 逗号运算符(按顺序求值) | a,b,c | 从左到右 |
赋值与复合赋值
静态变量和全局变量的赋值在编译阶段进行,而局部自动变量的赋值在程序运行到该语句时进行。赋值语句遵循右结合原则(从右向左运算)
赋值语句需要注意:将字节数长的数据赋值给字节数短的变量,会截取部分数据,也可能造成整个数据错误
- 浮点型(float、double)数据赋值给整型变量,将舍弃小数部分
- 将double数据赋值给float变量,将截取前7位有效数字
- 将int、short、long型数据赋值给char变量,只截取该数据的低8位到char变量中
- 将unsigned 数据赋值给有符号整型时,进行高位补0即可,但如果该unsigned 数据的数值超出了有符号整型最大值范围时,会产生数据错误!
将字节数短的数据赋值给字节数长的变量,一般需要进行位拓展
- 将整型数据赋值给浮点型变量,数值不变,拓展小数部分为0以浮点数形式进行存储
- 将float数据赋值给double,数值不变,拓展有效位数
- 将char数据赋值给整型变量,将char的1个字节数据存储到低8位,如果该整型变量为无符号整型(unsigned),则高8位补0。如果该整型变量为有符号整型,且该char字符最高位为0,则高8位补0,若char字符最高位为1,则高8位补1,以保证char对应的数值不变
- 将带符号的int赋值给long型变量,将int的值存入long的低16位,如果int为正整数(最高位为0),则long的高16位补0,反之补1
自增与自减
用于对变量进行加1或减1操作,自增和自减运算符都属于一元运算符,只需要一个操作数,且操作数必须是变量,不能是常量或表达式。自增与自减运算符分为前缀(如++i)和后缀(i++)形式。区别在于前缀是先对变量进行加1操作,再使用变量。后缀形式是先使用变量的当前值,再进行加1操作,自减运算符同理。
逗号表达式
表达式1,表达式2,…表达式n
用于串联多个表达式,表达式从左至右顺序运算,整个逗号表达式的值是最后一个表达式的值。多数情况下,逗号表达式仅用来顺序求值,并不一定用到整个逗号表达式的最终值。常见的情况是用于for语句进行多个变量的顺序运算,逗号表达式的运算优先级是所有运算种最低的。
位运算
位运算是直接对二进制位进行运算,只能用于整型或字符型数据,不能用于浮点型,位运算符常用于编写系统软件。对于需要两个操作数的位运算,如果给的两个操作数长度不同,则系统将给较短的操作数高位补0或1,以保证两个操作数长度相同。如果该数是无符号数或者正数,则高位补0,如果为负数则高位补1。
取反~
取反运算为单目运算符,用于将二进制数按位取反,eg:~12=-13
按位与&
对两个数的二进制码进行按位与,该运算有一些特殊用途:
- 数据清零,使之与0相与即可将所有二进制位置为0
- 保留或截取一个数中的某些指定位
按位或|
可用于将指定的二进制位置为1
按位异或^
- 使指定二进制位翻转
- 交换两个值,不使用临时变量
- 一个数与0相^,保留原值
- 一个数同本身相^,结果为0
左移运算符<<
将一个数的二进制位左移若干位,高位溢出部分舍弃,低位补0。一个数左移n位,如果左边被舍弃的部分全为0,则该数相当于乘以2n
右移运算符>>
将一个数的二进制位右移若干位,低位溢出部分舍弃,高位补的值取决于该数值本身。如果是无符号数,或者该数符号位为0(该数为正),则高位补0,如果该数为负数,高位补0还是补1取决于编译器,补0称为”逻辑右移”,补1称为”算术右移”。
同理,如果右边移除部分均为0,则右移n位相当于该数除以2n
位运算赋值运算符
位运算和赋值运算可以组成复合赋值运算符,如:&=、|=、<<=、>>=、^=等
关系运算符
注意,关系运算符的优先级低于算术运算符,高于赋值运算符,且关系运算符中<、<=、>、>=的优先级是高于==、!=的
逻辑运算
逻辑运算符中优先级顺序为!高于&&高于||,且&&和||有短路特性
C语言中没有逻辑常量true和false,编译器在表示逻辑时使用1代表真,0代表假,在表示数值时,以非0代表真,0代表假
条件运算(三元表达式)
条件判断语句?表达式1:表达式2
先求解条件判断语句,若为真则执行表达式1,整个条件表达式的值
为表达式1的值,否则执行表达式2并作为条件表达式的值
选择与循环控制
选择语句
if()..else、while()、do…while()、for
switch多分支选择
- case语句只起语句标号的作用,并不是在该处进行条件判断
- switch语句表达式的值找到匹配的case入口标号后,会一直执行下去,不再进行判断,直到执行完或遇到break语句
break与continue
break用于终止循环和跳出switch,只能用于循环语句和switch语句。continue用于结束本次循环,代码将继续下一次循环判定。使用goto语句的形成的循环体不能使用这两个语句跳出循环。
goto语句
goto语句为无条件转向语句,语法为:
语句标号用标识符表示,命名规则同变量,常用于组成循环,或者从循环内部跳出循环,但会导致程序可读性差,结构混乱,应当谨慎使用。
函数
main函数
谭浩强书的主函数一般写为void man(),但C语言标准(C99)定义的标准main函数写法为
标准输入输出函数
C语言本身不提供输入输出语句,输入输出操作由C函数库中的函数提供支持。这是因为输入输出涉及到硬件操作,没有输入输出语句可以让C语言编译器避免在编译阶段处理硬件有关的问题,可以极大地简化编译系统,并提高其通用性和可移植性。
C函数库由各软件公司编写,并已经编译成目标文件(.obj文件),源程序中的printf()等语句在编译阶段并不会被编译为目标指令,而是等待链接阶段源程序和库函数链接后,在执行阶段直接调用库函数目标文件(.obj文件)中的printf()函数。不同函数库提供的函数名、功能完全不同,但有一些通用的”标准输入输出函数”,调用这些函数需要引入头文件#inculde<stdio.h>
printf输出函数
标准输出语句格式为:
输出表列即需要输出的数据,多个输出的数据使用,分隔,可以为表达式;格式控制需要使用双引号括起来,可以包括两种信息
- 普通字符:printf会原样输出普通字符,包括空格、换行符等
- 格式字符:由%和格式字符组成,用于指定输出数据的格式
eg: a=12345,b=123
printf("%4d,%4d",a,b);//输出12345, 123(123前补个空格)
4.%o,%x,%X,以八进制、十六进制(字母小写)、十六进制(字母大写)形式输出整型(不输出前导符0和0x),该输出方式会将符号位也作为八进制和十六进制的数值位输出,因此无法输出负数,输出时支持指定输出位数。同样,八进制和十六进制支持用l(long)、h(short)、m(输出位宽)修饰
5.%u,输出无符号(unsigned)整型,%lu输出unsigned long类型
6.%c,输出一个字符,支持将0~255内的整数输出为ASCII码对应字符,也支持将这些字符输出为整数
7.%s,输出字符串
- %ms,指定输出的字符个数,若字符串串长大于m则原样输出字符串,若小于m则左补空格
- %-ms,同上,但是右补空格
- %m.ns,输出m个字符,但只取字符串左端n个字符,右对齐,左补空格
- %-m.ns,同上,但左对齐,右补空格
eg:
printf("%5.2s","china");//输出 ch(左补3个空格)
printf("%4.2s","china");//输出 ch(右补2个空格)
printf("%-5.2s","china");//输出ch (右补3个空格)
8.%f,输出浮点数(包括单、双精度),输出全部整数部分,并输出6位小数。单精度浮点数有效位数一般为7位,双精度浮点数有效位数一般为16位,给出小数6位。
- %m.nf,输出m列(小数点算一列),其中保留有n位小数(四舍五入),左补空格
- %-m.nf,同上,但左对齐,右补空格
- %.nf,省略m,即输出所有整数部分,保留n位小数
eg:float a=123.456
printf("%f",a);//输出123.456001,输出6位小数,有一定存储误差
printf("%8.2f",a);//输出 123.46,左补2个空格,保留2位小数,小数四舍五入
printf("%-8.2f",a);//输出123.46 ,右补2个空格
printf("%.2f",a);//输出123.46,整数部分全部输出,小数部分保留2位
float x,y;
x=111111.111,y=222222.222;
printf("%f",x+y);//输出333333.328125,只有前7位有效
9. %e或%E,以规格化指数形式输出小数,e和E分别表示指数e的大小写形式,可以使用%m.ne或%m.nE形式指定输出位数和数字部分的小数位数,一般默认数字部分的小数数位为6位,指数部分为5位(如:e+002)
printf("%e",123.456);
printf("%e",123.456);//输出1.234560e+002,默认数字部分6位小数
printf("%10.2E",123.456);//输出 1.23E+002,左补1个空格,数字部分的小数保留2位,注意:小数点、e、+等字符均占用一个位置
10.%g或%G,根据数值大小自动选择使用%f或%e形式,保证输出宽度最小,不输出无意义的0
11.%%,输出%
12.%p,以十六进制无符号整数形式输出变量或指针变量的地址
int num = 10;
int* p;
p = &num ;
printf("%p\n", &num);//输出结果参考:001DFB0C
printf("%p\n", p);
scanf输入函数
标准输入语句格式为:
格式控制与printf()函数的格式控制类似,如果格式控制中有格式说明以外的字符,则输入数据时需要在对应位置输入相同的字符;地址表列可以为变量地址、字符串首地址,通常需要用取地址符&加以引导,多个地址参数使用,分隔。
格式输入符 | 说明 |
---|---|
%d或%i | 输入有符号十进制整数 |
%I64d | 输入long long类型整数 |
%u | 输入无符号十进制整数 |
%o | 输入无符号八进制整数 |
%x或%X | 输入无符号十六进制整数 |
%c | 输入一个字符,空白字符、回车、制表符也视为有效字符 |
%s | 输入字符串,输入空白字符、回车、制表符会被认为输入结束,但开头输入空白字符会被系统跳过 |
%f | 输入小数,可以用小数形式或指数形式 |
%e,%E,%g,%G | 与%f相同,大小写作用相同 |
%% | 输入一个% |
格式修饰符 | 说明 |
---|---|
l | 用于输入长整型数据(%ld,%lo,%lx,%lu)和double型数据(%lf,%le) |
h | 用于输入短整型数据(%hd,%ho,%hx) |
域宽m(正整数) | 指定输入数据的宽度(列数),系统根据此宽度自动截取数据 |
* | 忽略输入修饰符,表示对应的输入项在读入后不赋给相应变量 |
用scanf()输入数据时,遇到以下情况会被认为数据输入结束:
- 遇到空格符、回车符、制表符(Tab)
- 达到输入域宽
- 遇到非法字符输入
putchar()与getchar()
用于输入或输出一个字符,只能用于处理字符
- getchar() 输入一个字符,无参数
- putchar(参数) 输出一个字符,参数可以为字符变量、整型变量,也可以为一个字符常量或转义字符
自定义函数
函数定义
自定义函数的定义语法为:
- 函数返回值只能有一个,返回值类型可以是除数组外的任何类型
- 函数无返回值时,返回值类型需要声明为void,可以不需要return语句,但通常会以 return;作为程序结束语句,表示程序正常执行结束且返回值为空,这是一种良好的编程习惯
- 如果函数返回值和函数定义时声明的返回值类型不同,则将以函数类型为准,系统自动进行数据转换
- 在定义函数时如果不指定返回值类型,系统会隐含指定为int型
- 函数名的命名规则与变量相同
- 形参(形式参数)必须指定类型,但变量名任意
- 形参在函数调用前,不占用存储单元,函数调用时才被分配内存,并在执行完后会被回收
函数调用
函数调用语法:
- 实参(实际参数)可以为常量,或者有确定值的变量或表达式
- 实参和形参的数据类型应该相同,或者赋值兼容(参考赋值一节的赋值规则,如字符型和整型相互通用),但应该尽量避免使用赋值兼容
#include<stdio.h>
//函数定义时不指定返回值类型,隐含为int类型
//用Visual Studio编译这段代码时,会有提示warning C4013: “getMax”未定义;假设外部返回 int
getMax(int a, int b) {
return a > b ? a : b;
}
int main(void) {
printf("%d", getMax(10, 20));
return 0;
}
函数声明
又称函数原型(Function Prototype)声明,其作用是将函数名、函数类型、形参类型、个数和顺序通知编译系统,以便调用该函数时系统按此进行对照检查。函数声明语法:
- 如果被调用的函数定义在主调函数之前,可以不加声明,编译器会根据函数定义时首部提供的信息对函数调用作准确性检查
- 如果被调用函数类型为整型,也可以不加声明
- 函数声明可以在文件开头(所有函数前),也可以在主调函数中
- 对于形参,编译器只检查形参类型和个数,不检查参数名,因此参数名任意,也可以省略
#include<stdio.h>
int main(void) {
int getMax(int, int);//原型声明
printf("%d", getMax(10, 20));
return 0;
}
int getMax(int a, int b) {
return a > b ? a : b;
}
内部函数与外部函数
外部函数:定义函数时,可以在函数返回值类型前加上extern将其定义为外部函数,extern可以省略,因此如果函数未特意声明为内部函数,则默认隐含为外部函数,即C语言中,函数默认为外部函数,外部函数可以被其他文件调用,只需要在要调用此函数的文件中,用extern作函数原型声明即可,其中函数原型声明前的extern也可以省略
内部函数:又称静态函数,定义函数时在函数首部添加static修饰即可定义内部函数,内部函数作用域仅限于本文件,不能被其他文件调用,不同的文件可以有同名的内部函数,互不干扰
#include<stdio.h>
int main(void)
{
extern void getStrings(char str[]);//extern可以省略,写为void getString(char str[])
static void test();//调用内部函数需要static声明
char str[50];
getStrings(str);
printf("%s\n", str);
test();//调用本文件中的test()函数
return 0;
}
static void test()//内部函数,仅供本文件调用
{
printf("file1.c");
return;
}
file2.c
#include<stdio.h>
extern void getStrings(char str[])//extern 可以省略
{
gets(str);
}
static void test()//内部函数,可以与file1.c中的test()函数同名
{
printf("file2.c");
return;
}
类型限定符
C语言中,类型限定符用于指定数据的访问属性。常见的类型限定符包括const(常量)、volatile(易变的)、restrict(限定)和_Atomic(原子)
const
const修饰变量
const用于限定变量的值不能被修改,表明变量中存储的值是一个常量,这类变量本质是变量,但又有常量属性,因此又称为”常变量”,const修饰的常变量必须在定义的同时赋值,而后其值就不能再改变,任何赋值行为都将引发错误。定义常变量时,const和变量类型位置可以互换,因此以下两个语句等价:
const int num=100;
int const num=100;
全局常变量存储于静态存储区的.rodata段,而局部常变量存储于栈内存中。对于局部常变量,由于栈内存本身是可读写的,所以即便编译器会对const修饰的局部变量进行内存保护,防止该变量的值被修改,但我们依然可以用其他方法修改该变量的值:
const int num=100;
int *p=&num ;
*p=0;
printf("%d",num);//num值被修改为0
const修饰数组
对于数组,由于数组名本身就代表数组起始地址,是一个地址常量,不能被任何限定符修饰,因此const修饰数组时,将用于修饰数组元素,即const修饰数组时,数组元素为常量无法修改
const int nums[] = { 10,20,30,40,50 };
nums[2] *= 10;//错误!无法修改
const修饰复合类型
const修饰结构体、枚举等复合类型时,表示该类型中的所有成员值不能被修改
struct test {
int a;
int b;
};
const struct test num = { 10,20 };
或
const struct test {
int a;
int b;
} num = {10,20};
或
struct test {
int a;
int b;
} const num = {10,20};
初始化完毕成员变量值不能再被修改
num.a=100;//错误!不能修改成员值
指针与const类型限定符
指针常用来在函数间传递数据,方便调用者直接对数据进行操作,但有时我们只希望将数据传到函数内部,而不希望它们在函数内被修改,此时,我们可以使用const对参数进行限定
int a,b;
1. const放在类型关键字前面
const int * p=&a;
按从右到左解析为:p是一个指针变量,可指向一个整型常量,*p是一个常量,而p不是。即*p的值是不可修改的,无法用*p=10;等方式重新赋值,但指针变量p的指向是可以被修改的,即p=&b是允许的
2. const位于类型关键字后*变量名前
int const *p=&a;
按从右到左解析为:p是一个指针常量,可以指向一个常量整数,同样*p是一个常量,而p不是,不能用指针变量p去修改这个"为常量的整数",与第一种情况作用相同
3. const位于*后,变量名前
int * const p=&a;
按从右到左解析为:p是一个指针常量,可以指向一个整型数据,它表明p是一个常量,而*p不是。由于p是常量指针,因此不能修改p的指向,p=&b操作是非法的,但*p=20是合法的
4. 两个const修饰,一个位于类型关键字前,一个位于*后
const int * const p=&a;
按从右到左解析为:p是一个指针常量,可指向一个整型常量,它表明p和*p都是常量,是只读的,*p=20和p=&b操作都是非法的
上述四种用法中,第一种用法较为常用,C语言很多库函数都使用该方法,只允许函数访问该指针指向的内容,不允许修改其内容,如:
int puts(const char * str);
int printf(const char * format , ...);
对于被const限制的指针变量,不能将普通指针变量赋值给被限制的指针变量,但反过来可以,如:const char *和char *是不同的类型,const char *指向的数据只有读取权限,而char *指向的数据有读写权限,因此不能将const char *赋值给char *,但可以将char *类型的数据赋值给const char *类型的变量
volatile
volatile用于声明变量的值是易变的,每次用到该变量的值时都需要去内存中重新读取这个变量的值,而不是读取其在寄存器中的备份。在多线程环境下,volatile 表示变量可能会被多个同时执行的线程修改,存取时无需额外的内存保护,并且防止优化编译器把变量从内存装入寄存器中,因为如果变量被装入寄存器,那么两个线程有可能一个使用内存中的变量,一个使用寄存器中的变量,造成程序的错误执行,volatile的声明方式为:
volatile int counter;
restrict
restrict是C99标准新增的限定符,该限定符只能用于指针变量,其作用是程序员通过该限定符告知编译器,restrict修饰的指针独占其所指向的内存,所有对该内存的操作都将基于该指针,而不再会有其他任何变量或指针操作该内存,以便让编译器进行一些针对性的优化。这种优化是针对特殊使用场景的,因此可能出现没有任何优化的情况,并且由于restrict指针独占是由程序员来判断并加以限定的,可能会出现误判的情况,因此,restrict一般用于非常需要性能优化,并且已经确认两个指针不会指向同一内存的场景,其定义语法为:
int * restrict a;
eg:业务上明确得知,某个malloc开辟的空间,只会通过mySpace指针变量进行操作,则可以使用restrict限定符告知编译器,由其进行优化
int * restrict mySpace=(int *)malloc(10*sizeof(int));
_Atomic
_Atomic是C11标准引入的限定符,用于指定原子类型,提供原子性操作。原子操作是指对原子对象的操作是不可被打断的,该操作一旦执行,不会被其他线程或者事件中断,直到操作执行完毕。默认的声明方式为:
_Atomic int x=0;
但在实际使用时,应该使用<stdatomic.h>头文件中已经定义好的原子类型,该头文件还定义了相关的原子操作函数
typedef _Atomic int atomic_int;
typedef _Atomic char atomic_char;
...
atomic_init(volatile A* obj, C desired )初始化原子类型变量
....
eg:定义一个原子类型变量并初始化
#include <stdio.h>
#include <stdatomic.h>
int main(void)
{
atomic_int a;
atomic_init(&a,10);
return 0;
}
动态内存分配
动态内存分配函数在堆上分配内存,使用前需要包含<stdlib.h>头文件
内存空间申请与释放
- void *malloc(unsigned int size);
分配长为size字节的内存空间,返回该内存首地址,若无足够内存单元,则返回空指针NULL
int *p=(int *)malloc(4*sizeof(int))
- void *calloc(unsigned int num,unsigned int size);
用于给同一类型的数据分配连续的内存空间并赋值为0(数值型)或空(字符型)。它相对于声明了一个一维数组,并且会初始化该数组值为0或空,参数num为所需申请的内存空间数量,相对于数组长度,参数size为每个内存空间的字节数,相对于数组基类型的字节长度。若申请成功,返回该内存空间的首地址,否则返回空指针NULL
int * p=(int *)calloc(10,sizeof(int));
相对于
int * p=(int *)malloc(10*sizeof(int));
但calloc()会将分配的内存自动初始化为0或空,更为安全
- void *realloc(void *p,unsigned int size);
用于改变原来分配的内存,将p所指向的存储空间大小改为size个字节,返回值是新分配的内存空间首地址,与原来分配的首地址不一定相同
- void free(void *p);
用于释放所申请的内存空间,即p所指向的空间,其中p只能是由malloc()和calloc()申请的内存地址
文件操作
二进制文件与文本文件
C语言文件有两种类型:文本文件(也称ASCII码文件)和二进制文件。二者的区别在于存储数值型数据的方式不同。二进制文件中,数值型数据是将整个数值转码为二进制形式存储;而在文本文件中,数值型数据的每一位数字作为一个字符以其ASCII码形式存储,每个数字都单独占用一个字节的存储空间。如:对于short int n=123,二进制文件中,变量n为short类型仅占用2个字节存储空间,而在文本文件中占用3个字节存储空间。
二进制文件和文本文件各有优缺点。文本文件可以很方便被其他程序读取,包括文本编辑器、Office办公软件等,且输出内容与字符一一对应,一个字节表示一个字符,便于对字符逐个处理,便于输出字符,但一般占用的存储空间较大,且ASCII码和字符转换需要花费一定时间。二进制文件可以节省空间和转换时间,但不方便其他程序读取,不能直接输出其字符形式。
无论文件内容是什么形式,C语言一律将数据看成由字节构成的序列,即字节流,对文件的存取也是以字节为单位进行的,输入、输出的数据仅受程序控制而不受物理符号(如回车换行符)控制,因此,C语言文件又称为流式文件。
C语言文件系统分为缓冲型和非缓冲型,缓冲型文件系统是指系统自动为每一个正在使用的文件在内存中开辟一个输入/输出文件缓冲区,作为程序和文件之间数据交换的缓冲区域。在读文件时,数据先从磁盘送到缓冲区,再传给C语言程序;在写文件时,数据从C语言程序送到缓冲区,装满缓冲区后再输出到磁盘。缓冲文件系统利用文件指针标识文件,不同版本的C语言缓冲区大小不一样,一般为512字节。而非缓冲文件系统不会自动数值文件缓冲区,缓冲区必须由程序员自己设定,它使用称为文件号的整数来标识文件。
在UNIX系统中,缓冲文件系统用来处理文本文件,用非缓冲文件系统来处理二进制文件。用缓冲文件系统进行输入输出的操作又称为高级文件操作,用非缓冲文件系统来处理输入输出的操作又称为低级输入输出操作。ANSI C标准规定只采用缓冲文件系统,即既用缓冲文件系统处理文本文件,也用它处理二进制文件,因此下文主要为高级文件操作函数。
文件指针
缓冲文件系统中,系统为每个正在使用的文件在内存中开辟了一个缓冲区,用来存放文件的有关信息(如:文件名、文件句柄、文件状态、文件路径等),这些信息保存在一个结构体变量中,该结构体类型由系统定义,名为FILE,stdio.h文件中有该文件类型声明:
typedef struct
{ short level; //缓冲区"满"或"空"的程度
unsigned flags;//文件状态标志
char fd; //文件描述符
unsigned char hold;//如果无缓冲区不读取字符
short bsize;//缓冲区大小
unsigned char *buffer;//数据缓冲区位置
unsigned ar *curp;//指针当前指向
unsigned istemp;//临时文件指示器
short token;//用于有效性检查
}FILE;
由此,我们可以定义一个文件类型的指针变量:FILE *fp,然后将该指针指向一个文件的结构体变量,从而通过该指针访问该结构体变量中的文件信息,并通过这些信息访问该文件。
文件的打开与关闭
fopen()函数
函数fopen()用于打开文件,其函数原型如下:
该函数返回一个文件类型的指针,如果文件打开失败,则返回一个空指针NULL(NULL在stdio.h文件中被定义为0)。fopen()有两个形参,第一个形参filename表示文件名,可以包含文件路径和文件名两部分,第二个形参mode表示文件的打开方式,取值如下所示:
字符 | 含义 |
---|---|
r | 以只读方式打开文本文件,只能读出而不能写入数据,若文件不存在,则报错 |
w | 以只写方式(新建形式),创建并打开文本文件,无论文件是否存在,都会创建一个新的文本文件,只能写入数据,已经存在的文件将被覆盖 |
a | 以只写方式(追加形式),打开文本文件,位置指针移到文件末尾,向文件尾部添加新数据,若文件不存在,则会新建一个文件 |
b | 与上面的字符串组合,表示打开二进制文件 |
rb | 以只读方式打开二进制文件 |
wb | 以只写方式打开二进制文件 |
ab | 以只写方式向二进制文件末尾添加数据 |
+ | 与上面的字符串组合,表示以读写方式打开文本文件,既可以向文件中写入数据,也可以从文件中读取数据 |
r+ | 以可读可写的方式打开文本文件 |
w+ | 以可读可写方式创建一个新的文本文件 |
a+ | 以可读可写方式向文本文件末尾追加数据 |
rb+ | 以可读可写的方式打开二进制文件 |
wb+ | 以可读可写方式创建一个新的二进制文件 |
ab+ | 以可读可写方式向二进制文件末尾追加数据 |
注意:
- 有的C语言编译系统可能不完全提供上述功能,如:有的C语言编译器不支持r+、w+、a+
- 输入文本文件时,内容中的回车换行符会被转换为一个换行符,在输出时,则会将换行符转换为回车和换行两个字符。而在输入输出二进制文件时,不会进行这种转换,文件中的数据与内存中的数据完全一致,一一对应
fclose()函数
函数fclose()用来关闭一个由函数fopen()打开的文件,其函数原型如下:
函数返回一个整型值,当文件成功关闭时,返回0值,否则返回一个非0值(一般为EOF -1),可用于判断文件是否关闭成功。当使用fclose()关闭文件后,指针fp将不再指向该文件。
由于操作系统对于同时打开的文件数目是有限制的,所以文件使用结束后必须关闭文件。此外,不建议使用读写方式打开文件,因为读写共用一个缓冲区,每次读写都会改变文件位置指针,很容易写乱,破坏文件内容,并且需要调用文件定位函数才能在读写之间切换。
FILE * fp;
fp=fopen("D:\\demo.text","a+");
fclose(fp);
文件的读写
按字符读写
- int fgetc(FILE *fp) 从fp所指向文件读取一个字符,并使位置指针指向下一个字符。如果读取成功,则返回该字符,若读取到文件末尾,则返回EOF(stdio定义为-1)
- int fputc(int c,FILE *fp) 将字符c(尽管c定义为int型,但只写入低字节)写到文件指针fp所指向的文件中。若写入错误,则返回EOF,否则返回字符c
#include<stdio.h>
#include<stdlib.h>
int main(void){
FILE *fp;
char ch,filename[10];
scanf("%s",filename);//输入文件名
if((fp=fopen(filename,"w"))==NULL){
printf("文件打开失败\n");
exit(0);
}
ch=getchar();//接收执行scanf语句时输入的回车符
ch=getchar();
while(ch!='#')
{
fputc(ch,fp);
ch=getchar();
}
fclose(fp);
return 0;
}
eg:从当前路径文件夹下读取名为"myTest.txt"的文件,并将内容输出到控制台
int main(void) {
FILE* fp;
char ch;
if ((fp = fopen("myTest.txt", "r")) == NULL) {
printf("文件打开失败!\n");
exit(0);
}
while (!feof(fp)) {
ch = fgetc(fp);
putchar(ch);
}
return 0;
}
按字符串读写
- char * fgets(char *s,int n,FILE *fp) 从fp所指向文件中读取最多含n-1个字符的字符串,并在字符串末尾添加’\0’,然后存入s。当读取到回车换行符、到达文件末尾或者读满n-1个字符时,函数返回该字符串的首地址,即指针s的值;读取失败时返回空指针NULL
- int fputs(const char *s,FILE * fp) 将字符串s写入文件fp,若写入错误,则返回EOF(-1),否则返回一个非负数
#include<stdio.h>
#include<stdlib.h>
int main(void){
FILE * fp;
char str[20];
if((fp=fopen("demo.txt","a"))==NULL){
printf("Failure to open file!\n");
exit(0);
}
gets(str);
fputs(str,fp);
fclose(fp);
if((fp=fopen("demo.txt","r"))==NULL){
printf("Failure to open file!\n");
exit(0);
}
fgets(str,20,fp);
puts(str);
fclose(fp);
return 0;
}
按格式读写文件
- int fscanf(FILE *fp,格式字符串,输入表列地址)从指定文件按格式读取数据,第1个参数为文件指针,第2个参数为格式控制字符串,第3个参数为输入变量的地址表列
- int fprintf(FILE *fp,格式字符串,输出表列)按指定格式向文件写入数据,第1个参数为文件指针,第2个参数为格式控制字符串,第3个参数为要写入的变量
用函数fscanf()和fprintf()进行文件的格式化读写,读写方便,容易理解,但输入时要将ASCII字符转换为二进制数,输出时要将二进制数转换为ASCII字符,耗时较多。因此,内存和磁盘进行频繁数据交换时,应当尽量避免使用这两个函数,而用fread()和fwrite()函数
int i=10;
float j=4.5;
fprintf(fp,"%d,%6.2f",i,j);//保存为10, 4.50
fscanf(fp,"%d,%f",&i,&j);//假设文件内容为10,4.5,则i赋值为10,j赋值为4.5
按数据块读写文件
- unsigned int fread(void *buffer,unsigned int size,unsigned int count,FILE *fp); 从fp所指的文件读取数据块并存储到buffer所指向的内存。buffer是待存入数据块的起始地址,可以是数组起始地址、结构体变量起始地址等,size是每个数据块的大小,count是最多允许读取的数据块个数,函数返回的是实际读取到的数据块个数
- unsigned int fwrite(const void *buffer,unsigned int size,unsigned int count,FILE *fp); 将buffer所指内存中的数据块写入fp所指的文件。同样,buffer是待读出数据块的起始地址,size是每个数据块的大小,count是最多允许写入的数据块个数,函数返回的是实际写入的数据块个数
块数据读写允许用户以数组、结构体等数据类型整块读写,不再局限于一次只读写一个字符或字符串,可以指定想要读写的内存块大小,最小为1字节,最大可以为整个文件
#include<stdio.h>
#include<stdlib.h>
int main(void) {
int nums[] = { 11,22,33,44,55,66,77,88,99 };
int numsRead[9];
FILE* fp;
if ((fp = fopen("data.txt", "w")) == NULL) {
printf("文件打开失败!");
exit(0);
}
fwrite(nums, 4, sizeof(nums)/4, fp);//将数组nums到文件fp中,数组为int类型,数据块为4Bytes,需要写入9个数据块
fclose(fp);
if ((fp = fopen("data.txt", "r")) == NULL) {
printf("文件打开失败!");
exit(0);
}
fread(numsRead, 4, 9, fp);//从fp文件中读取9个4Bytes的数据块,并存储到numsRead数组中
for (int i = 0; i < 9; i++) {
printf("%4d", numsRead[i]);
}
fclose(fp);
return 0;
}
eg:按块文件读写结构体变量,输入每个学生的信息并保存到studentData.txt文件中,并添加从该文件中读出学生信息并打印到控制台的功能
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 10
typedef struct student {
char name[10];
long studentID;
int age;
int classID;
}STUDENT;
int main(void) {
void writeToFile(STUDENT[], int);
int readFromFile(STUDENT[]);
void printToScreen(STUDENT[], int);
STUDENT stu[MAXSIZE];
int studentNum;
printf("请输入学生数量:\n");
scanf("%d", &studentNum);
printf("请输入学生信息:\n");
for (int i = 0; i <studentNum; i++) {
scanf("%s %ld %d %d", stu[i].name, &stu[i].studentID, &stu[i].age, &stu[i].classID);
}
writeToFile(stu, studentNum);
studentNum = readFromFile(stu);
printToScreen(stu, studentNum);
return 0;
}
void writeToFile(STUDENT stu[], int n) {
FILE* fp;
if ((fp = fopen("studentData.txt", "w")) == NULL) {
printf("文件打开失败!");
exit(0);
}
fwrite(stu, sizeof(STUDENT), n, fp);
fclose(fp);
}
int readFromFile(STUDENT stu[]) {
FILE* fp;
int i;
if ((fp = fopen("studentData.txt", "r")) == NULL) {
printf("文件打开失败!\n");
exit(0);
}
for (i = 0; !feof(fp); i++) {
fread(&stu[i], sizeof(STUDENT), 1, fp);
}
fclose(fp);
return i - 1;
}
void printToScreen(STUDENT stu[], int num) {
printf("----------------------\n");
for (int i = 0; i < num; i++) {
printf("%6s%10ld%4d%4d\n", stu[i].name, stu[i].studentID, stu[i].age, stu[i].classID);
}
}
数字读写putw()和getw()
putw()和getw()不是ANSI C标准定义的函数,但很多编译器提供这两个函数,有的编译器将其命名为_putw()和_getw()函数(Visual Studio 2019(编译器版本主要支持ANSI C89标准,但其中包含几个Microsoft扩展支持ISO C99的一部分函数)只能用这两个函数名),因此这两个函数的在不同编译器中函数名可能不同,函数名中的w是指word(字)
- int getw(FILE *fp) 从fp读取一个整数,如果读取成功,返回该整数,读取失败或文件结束,返回-1
- int putw(int w, FILE *fp) 将一个字符或字输出到文件fp,返回值为输出的整数
#include<stdio.h>
#include<stdlib.h>
int main(void) {
FILE* fp;
int getNum;
if ((fp = fopen("test.txt", "w")) == NULL) {
printf("文件打开失败!\n");
exit(0);
}
for (int i = 0; i < 10; i++) {
_putw(i, fp);
}
fclose(fp);
if ((fp = fopen("test.txt", "r")) == NULL) {
printf("文件打开失败!\n");
exit(0);
}
for (int i = 0; i < 10; i++) {
getNum = _getw(fp);
printf("%4d", getNum);
}
fclose(fp);
return 0;
}
文件定位
之前的示例执行的都是顺序文件处理,数据项是依次进行读写的,如果需要读取第5个数据项,按顺序文件处理方法必须先读取前4个数据项。如果要读写指定位置的数据项,则需要使用文件的随机访问,允许在文件中随机定位,并在文件任意位置读写数据。
为了实现文件的定位,每一个打开的文件中,都有一个文件位置指针,用来指向当前文件读写位置。当对文件进行顺序读写时,每读完一个字节,位置指针就会自动指向下一个字节。当需要随机读写时,需要我们人为指定位置指针的指向,C语言提供了几个关于位置指针的函数
重置位置指针rewind()
void rewind(FILE *fp) 使fp文件的位置指针指向文件首字节,即重置位置指针到文件开头
FILE *fp1=fopen("file1.txt","r");
FILE *fp2=fopen("file2.txt","r");//应当有文件打开失败的错误处理,这里省略
while(!feof(fp1)){
putchar(getc(fp1));
}
rewind(fp1);
while(!feof(fp1)){
putc(getc(fp1),fp2);
}
fclose(fp1);
fclose(fp2);
指定指针位置fseek()
int fseek(FILE *fp,long offset,int fromwhere);将fp的位置指针从fromwhere移动offset个字节
offset是一个偏移量,告诉文件指针跳过多少个字节,ANSI C要求offset是long类型的(其常量值后面要加L),这样当文件长度大于64kb时不至于出问题。当offset为正时,位置指针向后移动,为负时向前移动。由于需要指定具体字节数,因此该参数往往需要sizeof(数据类型)函数来获取相应类型的字节数。
fromwhere用于确定偏移量计算的起始位置,可以取以下三个值:
起始点 | 别名 | 值 |
---|---|---|
文件开始 | SEEK_SET | 0 |
文件当前位置 | SEEK_CUR | 1 |
文件末尾 | SEEK_END | 2 |
#include<stdio.h>
#include<stdlib.h>
#define MAXSIZE 10
typedef struct student {
char name[10];
long studentID;
int age;
int classID;
}STUDENT;
int main(void) {
void searchData(char fileName[],long k);
long k;
printf("Input the searching record number:");
scanf("%ld",&k);
searchData("studentData.txt",k);
return 0;
}
void searchData(char fileName[],long k)
{
FILE* fp;
STUDENT stu;
if ((fp = fopen(filename, "r")) == NULL) {
printf("文件打开失败!\n");
exit(0);
}
fseek(fp, (k - 1) * sizeof(STUDENT), SEEK_SET);
fread(&stu, sizeof(STUDENT), 1, fp);
printf("%8s%8ld%4d%4d", stu.name, stu.studentID, stu.age, stu.classID);
fclose(fp);
}
返回指针位置ftell()
long ftell(FILE *fp)返回当前位置指针相对于文件开头的位移量(字节数),若函数调用失败则返回-1L
fseek(fp, (k - 1) * sizeof(STUDENT), SEEK_SET);//执行完指针跳转,不确定指针指向哪里,可以用ftell()返回指针位置
long num=ftell(fp);//指针相对于文件头的偏移字节数
int k=num/sizeof(STUDENT);//指针指向的数据序号
文件状态
- int feof(FILE *fp)检查fp所指向文件是否已经读到文件末尾。当文件位置指针指向文件结束符时,返回非0值,否则返回0值
- int ferror(FILE *fp)检查对文件的输入输出操作是否出错,如果出错返回非零值,如果未出错,返回0。对同一个文件,每调用一次输入输出函数,ferror()函数的值都会更新
- void clearerr(FILE *fp)将fp所指向文件的文件错误标志和文件结束标志置为0
- void perror(const char *str)向标准错误输出字符串str,并随后附上冒号以及全局变量errno代表的错误消息的文字说明,无返回值
- int rename(const char *old,const char *new)将文件名old所指的文件改为new,成功返回0,出错返回1
输入输出重定向
在从终端设备输入输出时,系统会自动打开三个标准文件:标准输入、标准输出、标准出错输出。系统为其定义了三个文件指针:stdin、stdout和stderr,分别指向终端输入、终端输出和标准出错输出(也从终端输出)。在默认情况下,标准输入设备是键盘,标准输出设备是屏幕,我们从终端输入、输出时不需要手动打开终端文件,系统会自动打开,同样,如果程序指定从stdin所指的文件输入数据,其实就是指从终端键盘输入数据。
文件读写操作中的很多函数是标准输入输出函数的文件操作版,如:fprintf()是printf()的文件操作版,二者差别在于fprintf()多了一个文件指针类型的参数(FILE * fp),如果给该参数传递的值是stdout,那么fprintf()就和printf()完全一样了,同理,以下语句是两两等价的:
putchar(c)等价于fputc(c,stdout)
puts(str)等价于fputs(str,stdout)
getchar()等价于fgetc(stdin)
以下函数,fgets()比gets()还多了一个参数size
char *fgets(char *str,int size,FILE *fp);
char *gets(char *str);
参数size用于限制输入字符串的长度,说明fgets()函数输入缓冲区大小,使读入的字符数不能超过限定的缓冲区大小,从而达到防止缓冲区溢出攻击的目的,因此,以下语句,后者安全性更高
gets(str);
fgets(str,sizeof(str),stdin);
虽然系统隐含的标准I/O文件是指终端文件,但标准输入和标准输出是可以重定向的,操作系统可以把它们重定向到其他文件或具有文件属性的设备,只有标准错误输出不能进行一般的输出重定向,如:把标准输出重定向到打印机,把标准输入重定向到U盘文件等。使用“<”表示输入重定向,用“>”表示输出重定向。如:file.exe是可执行文件,执行该程序时需要从键盘输入数据,如果现在要求从文件file.in读取所需要输入的数据,那么只需在DOS命令提示符下,输入以下命令即可:
C语言中的随机数
C语言标准库在<stdlib.h>头文件中提供了用于生成随机数的函数rand(),其函数原型为:int rand(void),该函数可以生成0~32767范围内的随机数,但该函数生成的数是伪随机数,每次调用该函数都会重复生成以下值:
printf("%d\n", rand());//41
printf("%d\n", rand());//18467
printf("%d\n", rand());//6334
printf("%d\n", rand());//26500
printf("%d\n", rand());//19169
......
这是由于,各编程语言获取随机数实际上都是基于递推公式计算出一组数值,当序列足够长,这组数值可以近似满足均匀分布。在计算随机数时,这些随机函数都是基于一种名为”种子”的基准值进行运算,当种子值不变,生成的随机数也将固定。C语言中,rand()函数的种子值默认为1,可以通过srand()函数来修改该种子值,其函数原型为:
void srand(unsigned int seed);
srand(6);
printf("%d\n", rand());//58
printf("%d\n", rand());//6673
printf("%d\n", rand());//30119
printf("%d\n", rand());//15745
printf("%d\n", rand());//5206
如果将种子值设为当前时间,由于时间是每时每刻都不一样的,此时就可以真正生成一个随机数,C语言提供了获取当前时间戳的函数:
time_t time(time_t *timer);
该函数会当前时间到1970年1月1日0时0分0秒的时间差,单位为秒,其返回值类型为time_t(但本质上为64位整型),在用作生成随机数时,传入的参数一般为空指针NULL(或0)
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
int main()
{ srand((unsigned int)time(NULL));
printf("%d\n", rand());
printf("%d\n", rand());
return 0;
}
如果需要指定生成随机数的范围,则还需要一些额外的运算
srand((unsigned int)time(NULL));
1. 通过取模运算来限制随机数范围
int randomNum = rand() % 100;//生成0-99之间的随机整数
int randomNum = rand()%100+1;//生成1-100之间的随机整数
该语句解析为:
int randomNum = rand()%(101-1)+1;//生成1-101(不包括101)之间的整数
生成min-max(不包括max)之间的随机整数
rand()%(max-min)+min
2. 将随机整数除以RAND_MAX宏常量来获取随机小数:stdlib.h定义了rand()函数能返回伪随机数的最大值RAND_MAX,其值一般为32767,我们将获得的随机整数除以该常量值,就获得了一个0.0-1.0之间的随机小数
double random_double = (double)rand()/(double)RAND_MAX;
类似地可以限定该随机小数的范围为a-b:
(double)rand() / RAND_MAX * (b - a) + a;
常用函数和头文件
列出了一些常用的函数及其头文件,这些头文件中还有很多其他函数,可以参考文档:https://cplusplus.com/reference/,里面罗列了很多C语言和C++的头文件和函数,二者文件名和函数形参可能有所不同但能类似套用
字符与字符串
以下是对部分字符的定义
字符类 | 说明 |
---|---|
标点符号字符 | !@”#$%^&’(),*+-./:<=>;[]\_{ |
图形字符 | 包括字母、数字、标点符号 |
空白字符 | 空格符、水平制表符\t、换行符\n、垂直制表符\v、换页符\f、回车符\r |
控制字符 | 在 ASCII 编码中,这些字符的八进制代码是从 000 到 037,以及 177(DEL) |
可打印字符 | 包括字母、数字、标点符号和空格字符 |
以下函数用于测试和映射字符,需要包含字符串<ctype.h>,除了最后的大小写转换函数,其余函数如果参数 c 满足描述的条件,则这些函数返回非零值(true),一般返回值为1,否则返回零(false)
函数名 | 函数原型 | 说明 |
---|---|---|
isalnum | int isalnum(int c) | 检查字符c是否是字母和数字 |
isalpha | int isalpha(int c) | 检查字符c是否是字母 |
iscntrl | int iscntrl(int c) | 检查字符c是否是控制字符 |
isprint | int isprint(int c) | 检查字符c是否是可打印的 |
isdigit | int isdigit(int c) | 检查字符c是否是十进制数字 |
isxdigit | int isxdigit(int c) | 检查字符c是否是十六进制数字 |
isgraph | int isgraph(int c) | 检查字符c是否有图形表示法,有图形表示法的字符是指除了空白字符以外的所有可打印的字符 |
islower | int islower(int c) | 检查字符c是否是小写字母 |
isupper | int isupper(int c) | 检查字符c是否是大写字母 |
ispunct | int ispunct(int c) | 检查字符c是否是标点符号字符 |
isspace | int isspace(int c) | 检查字符c是否是空白字符 |
tolower | int tolower(int c) | 把给定的字符c转换为小写字母,如果已经是小写字母则c不变 |
toupper | int toupper(int c) | 把给定的字符c转换为大写字母,如果已经是大写字母则c不变 |
以下函数用于处理字符串,需要包含头文件<string.h>
函数名 | 函数原型 | 说明 |
---|---|---|
strlen | unsigned int strlen( const char *str); | 返回字符串str中实际字符的个数(不包括终止符’\0’) |
strcat | char *strcat(char *str1,const char *str2) | 把字符串str2拼接到str1后面,并在拼接好的str1串末尾添加一个’\0’,原str1末尾的’\0’将被覆盖。因无边界检查,调用时应保证str1的空间足够大,能存放原始str1和str2两个串的内容。函数返回指向str1的指针 |
strncat | char * strncat( char * strl, constchar * str2, unsigned int count); | 把字符串str2中不多于count个字符拼接到str1后面,并添加\0’,原str1末尾的’\0’将覆盖,函数指向返回str1的指针 |
strcpy | char * strcpy( char * strl, constchar *str2); | 把str2指向的字符串复制到str1中,str2必须是终止符为’\0’的字符串指针,函数返回指向str1的指针 |
strncpy | char *strncpy(char * strl, constchar *str2,unsigned int count); | 把str2指向的字符串中的count个字符复制到st1中,str2必须是终止符为’\0’的字符串指针。如果str2指向的字符串少于 count个字符,则将’\0’加到str1的尾部,直到满足coun个字符为止。如果str2指向的字符串长度大于count个字符,则结果串str1不用’\0’结尾,函数返回指向strl的指针 |
strcmp | int strcmp(const char * strl,const char * str2); | 按字典顺序比较两个字符串str1和str2。若str1<str2,则返回负数。若str1=str2,则返回0。若str1>str2,则返回正数 |
strcnmp | int strcnmp( const char * strl,const char*str2,unsigned intcount); | 按字典顺序比较两个字符串str1和str2的不多于count个字符。若str1<str2,则返回负数。若str1=str2,则返回0。若str1>str2,则返回正数 |
strtr | char * strstr(char * strl, char *str2); | 找出str2字符串在str1字符串中第一次出现的位置(不包括str2的串结束符)。函数返回该位置的指针。若找不到则返回空指针 |
数学函数
使用以下函数,源文件需要包含<math.h>头文件,以下函数这里只写出了返回值和形参为double类型的函数原型,它们中的绝大多数还有一个返回值和形参为float或long double类型的函数原型,因此可以类似地套用
函数名 | 函数原型 | 说明 |
---|---|---|
fabs | double fabs( double x); | 计算x的绝对值,返回浮点数 |
abs | double abs (double x); | 在c++中该函数声明于cmath头文件,可以用于计算double、float、long double的绝对值,在c语言中声明于stdlib.h文件,用于计算int的绝对值 |
ceil | double ceil (double x); | 向上取整 |
floor | double floor( double x); | 向下取整,计算不大于x的最大整数 |
round | double round (double x); | 四舍五入取整 |
pow | double pow( double base, doubleexp); | 返回base为底的exp次幂,即baseexp,返回计算结果。当base等于0而exp小于0时或者base小于0而exp不为整数时,出现结果错误。该函数要求参数base和exp以及函数的返回值为double类型,否则有可能出现数值溢出问题 |
sqrt | double sqrt(double x) | 计算√x(根号下x)的值,注意x>=0 |
exp | double exp( double x); | 计算ex的值 |
fmod | double fmod ( double x, double y); | 计算整除x/y的余数 |
log | double log( double x ); | 计算logex,即lnx,返回计算结果。注意,x>0 |
log10 | double log10( double x); | 计算 log10x,返回计算结果。注意,x>0 |
sin | double sin(double x) | 计算sinx的值,x为弧度值 |
cos | double cos(double x) | 计算cos(x)的值,x为弧度值 |
tan | double tan(double x); | 计算tanx的值 |
asin | double asin(double x) | 计算cos-1(x)的值,注意,x应为-1到1范围 |
acos | double acos(double x) | 计算sin-1(x)的值,注意,x应为-1到1范围 |
atan | double atan(double x) | 计算tan-1(x)的值 |
atan2 | double atan2(double x,double y) | 计算tan-1(x/y)的值 |
sinh | double sinh(double x) | 计算x的双曲正弦函数sinh(x)的值 |
cosh | double cosh(double x) | 计算x的双曲余弦 cosh(x)的值 |
tanh | double tanh(double x) | 计算x的双曲正切函数tanh(x)的值 |
frexp | double frexp(double val,int*eptr); | 把双精度数val分解为小数部分(尾数)x和以2为底的指数n(阶码),即val=x*2 n,n存放在eptr指向的变量中,函数返回小数部分x,0.5≤x<1 |
modf | double modf( double val, double *iptr); | 把双精度数val分解为整数部分和小数部分,把整数部分存到iptr指向的单元。返回val的小数部分 |
其他常用函数
函数名 | 头文件 | 函数原型 | 说明 |
---|---|---|---|
atof | stdlib.h | double atof(const char * str); | 把str指向的字符串转换成双精度浮点值,串中必须含合法的浮点数,否则返回值无定义。函数返回转换后的双精度浮点值 |
atoi | stdlib.h | int atoi(const char *str); | 把str指向的字符串转换成整型值,串中必须含合法的整型数,否则返回值无定义。函数返回转换后的整型值 |
alol | stdlib.h | long int atol(const char * str); | 把str指向的字符串转换成长整型值,串中必须含合法的整型数,否则返回值无定义。函数返回转换后的长整型值 |
exit | stdlib.h | void exit(int code); | 使程序立即终止,清空和关闭任何打开的文件。程序正常退出状态由code等于0或EXITSUCCESS表示,如exit(0),非0值或EXIT_FAILURE表明定义实现错误。函数无返回值 |
rand | stdlib.h | int rand(void); | 产生伪随机数序列。函数返回0到RAND_MAX之间的随机整数,RAND_MAX至少是32767 |
srand | stdlib.h | void srand(unsigned int seed ); | 为函数rand()生成的伪随机数序列设置起点种子值,函数无返回值 |
time | time.h | time_t time(time_t *timer) | 如果传入的参数为空指针NULL,则该函数返回当前时间到1970年1月1日0时0分0秒的时间差,单位为秒,返回值类型为time_t(但本质上为64位整型)。如果参数为time_t类型指针变量timer,则该时间差值也将放于timer所指向的内存中 |
clock | time.h | clock_t clock(void); | clock_t是long类型,该函数返回硬件的时钟节拍数,需要换成秒或者毫秒,通常需要除以CLK_TCK或者CLOCKS_PER_SEC。例如,在VC6.0下,这两个量的值都是1000,表示硬件1秒钟的时钟节拍数为1000,该函数常用于测量从程序开始运行到clock()被调用时所消耗的时间,或用于实现计时器功能,如:计算一个进程的时间需要用clock()除以1000。注意:本函数仅能返回ms级的计时精度 |
Sleep | stdlih.h | Sleep(unsigned long second); | 在标准C中和Linux下是函数的首字母不大写。但在VC和Code::blocks环境下首字母要大写。Sleep()函数的功能是将进程挂起一段时间,即起到延时的作用。参数的单是毫秒 |
system | stdlib.h | int system(char * command); | 发出一个DOS命令。例如,system(“CLS”)可以实现清屏操作 |
kbhit | conio.h | int kbhit(void); | 检查当前是否有键盘输人,若有则返回一个非0值,否则回 0 |
getch | conio.h | int getch(void); | 无需用户按回车键即可得到入户输入,只要用户按下一个键,立刻返回用户输入字符的ASCII码值,但输入的字符不会显示在屏幕上,出错时返回-1,该函数在游戏中比较常用 |