C语言学习笔记-入门整合篇(十万字长文)

(该篇学习内容全部来自于C语言中文网, 本篇内容仅仅是简易学习笔记 , 以自己的理解+网站部分描述结合+个人补充,并不适合编程初学者观看!!! 需要有一定的编程基础)

数据在内存中存储

内存条包含了上亿个电子元器件。这些元器件,实际上就是电路;电路的电压会变化,要么是 0V,要么是 5V,只有这两种电压。5V 是通电,用1来表示,0V 是断电,用0来表示。所以,一个元器件有2种状态,0 或者 1。

一般情况下将8个元器件看做一个单位,即使表示很小的数,例如 1,也需要8个,也就是 00000001。

1个元器件称为1比特(Bit)或1位,8个元器件称为1字节(Byte),那么16个元器件就是2Byte,32个就是4Byte,以此类推:

单位换算:
1Byte = 8 Bit
1KB = 1024Byte

函数

C语言规定,一个程序必须有且只有一个 main 函数。main 被称为主函数,是程序的入口函数,程序运行时从 main 函数开始,直到 main 函数结束(遇到 return 或者执行到函数末尾时,函数才结束)。

#include <stdio.h>
//( ) 表明这是函数定义,{ } 之间的代码是函数要实现的功能。
int main()  
{
    
    
    puts("C语言中文网");
    return 0;
}

头文件的概念

C语言开发者们编写了很多常用函数,并分门别类的放在了不同的文件,这些文件就称为头文件(header file)。每个头文件中都包含了若干个功能类似的函数,调用某个函数时,要引入对应的头文件,否则编译器找不到函数。

实际上,头文件往往只包含函数的说明,也就是告诉我们函数怎么用,而函数本身保存在其他文件中,在链接时才会找到。对于初学者,可以暂时理解为头文件中包含了若干函数。

引入头文件使用#include命令,并将文件名放在< >中,#include 和 < > 之间可以有空格,也可以没有。

头文件以.h为后缀,而C语言代码文件以.c为后缀,它们都是文本文件,没有本质上的区别,#include 命令的作用也仅仅是将头文件中的文本复制到当前文件,然后和当前文件一起编译。你可以尝试将头文件中的内容复制到当前文件,那样也可以不引入头文件。

.h中代码的语法规则和.c中是一样的,你也可以#include <xxx.c>,这是完全正确的。不过实际开发中没有人会这样做,这样看起来非常不专业,也不规范。

扫描二维码关注公众号,回复: 16639506 查看本文章

较早的C语言标准库包含了15个头文件,stdio.h 和 stdlib.h 是最常用的两个: stdio 是 standard input
output 的缩写,stdio.h 被称为“标准输入输出文件”,包含的函数大都和输入输出有关,puts() 就是其中之一。 stdlib
是 standard library 的缩写,stdlib.h
被称为“标准库文件”,包含的函数比较杂乱,多是一些通用工具型函数,system() 就是其中之一。

变量和数据类型

变量定义&赋值

int a; // 这个语句的意思是:在内存中找一块区域,命名为 a,用它来存放整数。
a=123; // 把 123 交给了变量 a,这个过程叫做给变量赋值;第一次赋值,也称变量的初始化,或者赋初值。

数据类型(Data Type)

数据是放在内存中的,变量是给这块内存起的名字,有了变量就可以找到并使用这份数据。

诸如数字、文字、符号、图形、音频、视频等数据都是以二进制形式存储在内存中的,它们并没有本质上的区别,那么,00010000 该理解为数字16呢,还是图像中某个像素的颜色呢,还是要发出某个声音呢?如果没有特别指明,我们并不知道。

也就是说,内存中的数据有多种解释方式,使用之前必须要确定;上面的int a;就表明,这份数据是整数,不能理解为像素、声音等。int 有一个专业的称呼,叫做数据类型(Data Type)。

顾名思义,数据类型用来说明数据的类型,确定了数据的解释方式,让计算机和程序员不会产生歧义。

C语言基本的数据类型:

说 明 字符型 短整型 整型 长整型 单精度浮点型 双精度浮点型 无类型
数据类型 char short int long float double void

连续定义的多个变量以逗号,分隔,并且要拥有相同的数据类型;变量可以初始化,也可以不初始化。

int a, b, c;
float m = 10.9, n = 20.56;
char p, q = '@';

数据的长度

所谓数据长度(Length),是指数据占用多少个字节。占用的字节越多,能存储的数据就越多,对于数字来说,值就会更大,反之能存储的数据就有限。

多个数据在内存中是连续存储的,彼此之间没有明显的界限,如果不明确指明数据的长度,计算机就不知道何时存取结束。例如我们保存了一个整数1000,它占用4个字节的内存,而读取时却认为它占用3个字节或5个字节,这显然是不正确的。

所以,在定义变量时还要指明数据的长度。而这恰恰是数据类型的另外一个作用。数据类型除了指明数据的解释方式,还指明了数据的长度。因为在C语言中,每一种数据类型所占用的字节数都是固定的,知道了数据类型,也就知道了数据的长度。

整数(short,int,long)

在现代操作系统中,int一般占用 4 个字节(Byte)的内存,共计 32 位(Bit)。
int 是基本的整数类型,short 和 long 是在 int 的基础上进行的扩展,short 可以节省内存,long 可以容纳更大的值。

short a = 10;
short b, c = 99;
long m = 102023;
long n, p = 562131;

这样 a、b、c 只占用 2 个字节的内存,而 m、n、p 可能会占用 8 个字节的内存。

整型的长度

在不同的环境下, 只有 short 的长度是确定的,是两个字节,而 int 和 long 的长度无法确定。

C语言并没有严格规定 short、int、long 的长度,只做了宽泛的限制:

  • short 至少占用 2 个字节。
  • int 建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节。
  • short 的长度不能大于 int,long 的长度不能小于 int。

总结起来,它们的长度(所占字节数)关系为:

2shortintlong

这就意味着,short 并不一定真的“短”,long 也并不一定真的“长”,它们有可能和 int 占用相同的字节数。

举例:

  • 在 16 位环境下,short 的长度为 2 个字节,int 也为 2 个字节,long 为 4 个字节。16 位环境多用于单片机和低级嵌入式系统,在PC和服务器上已经见不到了。
  • 对于 32 位的 Windows、Linux 和 Mac OS,short 的长度为 2 个字节,int 为 4 个字节,long 也为 4 个字节。PC和服务器上的 32 位系统占有率也在慢慢下降,嵌入式系统使用 32 位越来越多。

在 64 位环境下,不同的操作系统会有不同的结果,如下所示:

操作系统 short int long
Win64(64位 Windows) 2 4 4
类Unix系统(包括 Unix、Linux、Mac OS、BSD、Solaris 等) 2 4 8

目前我们使用较多的PC系统为 Win XP、Win 7、Win 8、Win 10、Mac OS、Linux,在这些系统中,short 和 int 的长度都是固定的,分别为 2 和 4,大家可以放心使用,只有 long 的长度在 Win64 和类 Unix 系统下会有所不同,使用时要注意移植性。

sizeof 操作符

sizeof 用来获取某个数据类型或变量所占用的字节数,需要注意的是,sizeof 是C语言中的操作符,不是函数,所以可以不带( );
如果后面跟的是变量名称,那么可以省略( ),如果跟的是数据类型,就必须带上( )。

short a = 10;
int b = 100;
int short_length = sizeof a;
int int_length = sizeof(b);
int long_length = sizeof(long);
int char_length = sizeof(char);     

不同整型的输出

使用不同的格式控制符可以输出不同类型的整数,它们分别是:

  • %hd用来输出 short int 类型,hd 是 short decimal 的简写;
  • %d用来输出 int 类型,d 是 decimal 的简写;
  • %ld用来输出 long int 类型,ld 是 long decimal 的简写。
#include <stdio.h>
int main()
{
    
    
    short a = 10;
    int b = 100;
    long c = 9437;
    printf("a=%hd, b=%d, c=%ld\n", a, b, c);
    return 0;
}
  • 当使用%d输出 short,或者使用%ld输出 short、int 时,不管值有多大,都不会发生错误,因为格式控制符足够容纳这些值。

  • 当使用%hd输出 int、long,或者使用%d输出 long 时,如果要输出的值比较小(就像上面的情况),一般也不会发生错误,如果要输出的值比较大,就很有可能发生错误。

二进制数、八进制数和十六进制数的表示

二进制

二进制由 0 和 1 两个数字组成,使用时必须以0b或0B(不区分大小写)开头

//合法的二进制
int a = 0b101;  //换算成十进制为 5
int b = -0b110010;  //换算成十进制为 -50
int c = 0B100001;  //换算成十进制为 33

//非法的二进制
int m = 101010;  //无前缀 0B,相当于十进制
int n = 0B410;  //4不是有效的二进制数字

读者请注意,标准的C语言并不支持上面的二进制写法,只是有些编译器自己进行了扩展,才支持二进制数字。换句话说,并不是所有的编译器都支持二进制数字,只有一部分编译器支持,并且跟编译器的版本有关系。
下面是实际测试的结果:

  • Visual C++ 6.0 不支持。
  • Visual Studio 2015 支持,但是 Visual Studio 2010 不支持;可以认为,高版本的 Visual Studio 支持二进制数字,低版本的 Visual Studio 不支持。
  • GCC 4.8.2 支持,但是 GCC 3.4.5 不支持;可以认为,高版本的 GCC 支持二进制数字,低版本的 GCC 不支持。
  • LLVM/Clang 支持(内嵌于 Mac OS 下的 Xcode 中)。

八进制

八进制由 0~7 八个数字组成,使用时必须以0开头(注意是数字 0,不是字母 o)

//合法的八进制数
int a = 015;  //换算成十进制为 13
int b = -0101;  //换算成十进制为 -65
int c = 0177777;  //换算成十进制为 65535

//非法的八进制
int m = 256;  //无前缀 0,相当于十进制
int n = 03A2;  //A不是有效的八进制数字

十六进制

十六进制由数字 0~9、字母 A~F 或 a~f(不区分大小写)组成,使用时必须以0x或0X(不区分大小写)开头

//合法的十六进制
int a = 0X2A;  //换算成十进制为 42
int b = -0XA0;  //换算成十进制为 -160
int c = 0xffff;  //换算成十进制为 65535

//非法的十六进制
int m = 5A;  //没有前缀 0X,是一个无效数字
int n = 0X3H;  //H不是有效的十六进制数字

二进制数、八进制数和十六进制数的输出

C语言中常用的整数有 short、int 和 long 三种类型,通过 printf 函数,可以将它们以八进制、十进制和十六进制的形式输出。

short int long
八进制 %ho %o %lo
十进制 %hd %d %ld
十六进制 %hx 或者 %hX %x 或者 %X %lx 或者 %lX

二进制数的输出

部分编译器支持二进制数字的表示,但是却不能使用 printf 函数输出二进制。
通过转换函数可以将其它进制数字转换成二进制数字,并以字符串的形式存储,然后在 printf 函数中使用%s输出即可。

八进制数的输出

八进制数字和十进制数字不区分大小写,所以格式控制符都用小写形式。

十六进制数的输出

十六进制数字的表示用到了英文字母,有大小写之分,要在格式控制符中体现出来:

  • %hx、%x 和 %lx 中的x小写,表明以小写字母的形式输出十六进制数;
  • %hX、%X 和 %lX 中的X大写,表明以大写字母的形式输出十六进制数。
    short a = 0b1010110;  //二进制数字
    int b = 02713;  //八进制数字
    long c = 0X1DAB83;  //十六进制数字
   
    printf("a=%ho, b=%o, c=%lo\n", a, b, c);  //以八进制形似输出
    printf("a=%hd, b=%d, c=%ld\n", a, b, c);  //以十进制形式输出
    printf("a=%hx, b=%x, c=%lx\n", a, b, c);  //以十六进制形式输出(字母小写)
    printf("a=%hX, b=%X, c=%lX\n", a, b, c);  //以十六进制形式输出(字母大写)
    
	运行结果:
	a=126, b=2713, c=7325603
	a=86, b=1483, c=1944451
	a=56, b=5cb, c=1dab83
	a=56, b=5CB, c=1DAB83

一个数字不管以何种进制来表示,都能够以任意进制的形式输出。数字在内存中始终以二进制的形式存储,其它进制的数字在存储前都必须转换为二进制形式;同理,一个数字在输出时要进行逆向的转换,也就是从二进制转换为其他进制。

输出时加上前缀

  • 对于八进制数字,它没法和十进制、十六进制区分,因为八进制、十进制和十六进制都包含 0~7 这几个数字。
  • 对于十进制数字,它没法和十六进制区分,因为十六进制也包含 0~9 这几个数字。如果十进制数字中还不包含 8 和 9,那么也不能和八进制区分了。
  • 对于十六进制数字,如果没有包含 a~f 或者 A~F,那么就无法和十进制区分,如果还不包含 8 和 9,那么也不能和八进制区分了。

区分不同进制数字的一个简单办法就是,在输出时带上特定的前缀。在格式控制符中加上#即可输出前缀,例如 %#x、%#o、%#lX、%#ho 等:

    short a = 0b1010110;  //二进制数字
    int b = 02713;  //八进制数字
    long c = 0X1DAB83;  //十六进制数字
   
    printf("a=%#ho, b=%#o, c=%#lo\n", a, b, c);  //以八进制形似输出
    printf("a=%hd, b=%d, c=%ld\n", a, b, c);  //以十进制形式输出
    printf("a=%#hx, b=%#x, c=%#lx\n", a, b, c);  //以十六进制形式输出(字母小写)
    printf("a=%#hX, b=%#X, c=%#lX\n", a, b, c);  //以十六进制形式输出(字母大写)

	运行结果:
	a=0126, b=02713, c=07325603
	a=86, b=1483, c=1944451
	a=0x56, b=0x5cb, c=0x1dab83
	a=0X56, b=0X5CB, c=0X1DAB83

C语言中的正负数及其输出

在C语言中short、int、long 都可以带上正负号,如果不带正负号,默认就是正数。

如果将一个数字分为符号和数值两部分,那么不加 unsigned 的数字称为有符号数,能表示正数和负数,加了 unsigned 的数字称为无符号数,只能表示正数。

请读者注意一个小细节,如果是unsigned int类型,那么可以省略 int ,只写 unsigned,例如:
unsigned n = 100; 它等价于: unsigned int n = 100;

对于有符号数,符号也是数字的一部分,也要在内存中体现出来。符号只有正负两种情况,用1位(Bit)就足以表示;C语言规定,把内存的最高位作为符号位;C语言规定,在符号位中,用 0 表示正数,用 1 表示负数。

对于有符号数,如果只考虑正数,那么各种类型能表示的数值范围(取值范围)就比原来小了一半。但是在很多情况下,我们非常确定某个数字只能是正数,比如班级学生的人数、字符串的长度、内存地址等,这个时候符号位就是多余的了,就不如删掉符号位,把所有的位都用来存储数值,这样能表示的数值范围更大(大一倍)。

如果不希望设置符号位,可以在数据类型前面加上 unsigned 关键字:

unsigned short a = 12;
unsigned int b = 1002;
unsigned long c = 9892320;

无符号数的输出

无符号数可以以八进制、十进制和十六进制的形式输出,它们对应的格式控制符分别为:

unsigned short unsigned int unsigned long
八进制 %ho %o %lo
十进制 %hu %u %lu
十六进制 %hx 或者 %hX %x 或者 %X %lx 或者 %lX

严格来说,格式控制符和整数的符号是紧密相关的,具体就是:

  • %d 以十进制形式输出有符号数;
  • %u 以十进制形式输出无符号数;
  • %o 以八进制形式输出无符号数;
  • %x 以十六进制形式输出无符号数。

C 语言中printf 并不支持八进制和十六进制形式输出有符号数呢

下表全面地总结了不同类型的整数,以不同进制的形式输出时对应的格式控制符(- -表示没有对应的格式控制符):

short int long unsigned short unsigned int unsigned long
八进制 - - - - - - %ho %o %lo
十进制 %hd %d %ld %hu %u %lu
十六进制 - - - - - - %hx 或者 %hX %x 或者 %X %lx 或者 %lX
  • 当以有符号数的形式输出时,printf 会读取数字所占用的内存,并把最高位作为符号位,把剩下的内存作为数值位;
  • 当以无符号数的形式输出时,printf 也会读取数字所占用的内存,并把所有的内存都作为数值位对待;
  • 对于一个有符号的正数,它的符号位是 0,当按照无符号数的形式读取时,符号位就变成了数值位,但是该位恰好是 0 而不是 1,所以对数值不会产生影响;
  • “有符号正数的最高位是 0”这个巧合才使得 %o 和 %x 输出有符号数时不会出错。
  • 不管是以 %o、%u、%x 输出有符号数,还是以 %d 输出无符号数,编译器都不会报错,只是对内存的解释不同了;

整数在内存中是如何存储的

存储方式的目的:为了提高加减法的运算效率,硬件电路要设计得尽量简单。

原因:加法和减法是计算机中最基本的运算,计算机时时刻刻都离不开它们,所以它们由硬件直接支持。

存储方案难点:

  • 加法和减法是计算机中最基本的运算,计算机时时刻刻都离不开它们,所以它们由硬件直接支持,加法和减法是两种运算,增加了电路设计的复杂度。
  • 对于有符号数,内存要区分符号位和数值位,对于人脑来说,很容易辨别,但是对于计算机来说,就要设计专门的电路,这无疑增加了硬件的复杂性,增加了计算的时间。

设计思路:

  • 加法和减法可以合并为一种运算,就是加法运算,因为减去一个数相当于加上这个数的相反数。
  • 把符号位和数值位等同起来,让它们一起参与运算,不再加以区分,这样硬件电路就变得简单了。

存储方法:
在计算机内存中,整数一律采用补码的形式来存储。这意味着,当读取整数时还要采用逆向的转换,也就是将补码转换为原码。

原码:将一个整数转换成二进制形式,就是其原码。例如short a = 6;,a 的原码就是0000 0000 0000 0110;更改 a 的值a = -18;,此时 a 的原码就是1000 0000 0001 0010。
反码:对于正数,它的反码就是其原码(原码和反码相同);负数的反码是将原码中除符号位以外的所有位(数值位)取反,也就是 0 变成 1,1 变成 0。例如short a = 6;,a 的原码和反码都是0000 0000 0000 0110;更改 a 的值a = -18;,此时 a 的反码是1111 1111 1110 1101。
补码:对于正数,它的补码就是其原码(原码、反码、补码都相同);负数的补码是其反码加 1。例如short a = 6;,a 的原码、反码、补码都是0000 0000 0000 0110;更改 a 的值a = -18;,此时 a 的补码是1111 1111 1110 1110。

C语言整数的取值范围以及数值溢出

有符号数: 以int为例,共32位,最高位为符号位,31位为数值位
无符号数: 以int为例,共32位,无符号位,32位均为数值位

无符号数的取值范围

计算无符号数(unsigned 类型)的取值范围(或者说最大值和最小值)很容易,将内存中的所有位(Bit)都置为 1 就是最大值,都置为 0 就是最小值。

以 unsigned char 类型为例,它的长度是 1,占用 8 位的内存,所有位都置为 1 时,它的值为 28 - 1 =
255,所有位都置为 0 时,它的值很显然为 0。由此可得,unsigned char 类型的取值范围是 0~255。

有符号数的取值范围

char short int(4个字节) long(8个字节)
最小值 -2^7 = -128 -2^15 = -32,768 ≈ -3.2万 -2^31 = -2,147,483,648 ≈ -21亿 -2^63 ≈ -9.22×10^18
最大值 2^7 - 1= 127 2^15 - 1 = 32,767 ≈ 3.2万 2^31 - 1 = 2,147,483,647 ≈ 21亿 2^63 - 1≈ 9.22×10^18

数值溢出

#include <stdio.h>
int main()
{
    
    
	unsigned int a = 0x100000000;
	int b = 0xffffffff;
	printf("a=%u, b=%d\n", a, b);
    return 0;
}

运行结果:
a=0, b=-1

a 定义为无符号int类型,占4个字节,32位,最大值是 0xFFFFFFFF,0x100000000 = 0xFFFFFFFF + 1,溢出1,最高位溢出,只能读到32位0,所以读出来是0

b 定义为有符号数,有效数值位是31位,而0xffffffff,转化为二进制是32位1:1111 1111 … 1111, 所以最高位的 1 会覆盖符号位,数值位只留下 31 个 1,所以 b 的原码为:1111 1111 …… 1111 1111, 这也是 b 在内存中的存储形式。当 printf 读取到 b 时,由于最高位是 1,所以会被判定为负数,要从补码转换为原码:

[1111 1111 …… 1111 1111]补
= [1111 1111 …… 1111 1110]反
= [1000 0000 …… 0000 0001]原
= -1
最终 b 的输出结果为 -1。(关于b在内存中存储为1111 1111 …… 1111 1111这一点,尚不能理解;也无法理解最高位覆盖符号位的描述)

C语言中的小数(float,double)

C语言中常用的小数有两种类型,分别是 float 或 double;float 称为单精度浮点型,double 称为双精度浮点型。

小数的长度是固定的,float 始终占用4个字节,double 始终占用8个字节。

C语言同时支持两种形式的小数(十进制形式&指数形式)。但是在书写时,C语言中的指数形式和数学中的指数形式有所差异:

数学中的指数:7.25×10^2
C语言中的指数:aEn 或 aen
2.1E5 = 2.1×10^5,其中 2.1 是尾数,5 是指数。
3.7E-2 = 3.7×10^-2,其中 3.7 是尾数,-2 是指数

小数的输出

  • %f 以十进制形式输出 float 类型;
  • %lf 以十进制形式输出 double 类型;
  • %e 以指数形式输出 float 类型,输出结果中的 e 小写;
  • %E 以指数形式输出 float 类型,输出结果中的 E 大写;
  • %le 以指数形式输出 double 类型,输出结果中的 e 小写;
  • %lE 以指数形式输出 double 类型,输出结果中的 E 大写。
#include <stdio.h>
#include <stdlib.h>
int main()
{
    
    
    float a = 0.302;
    float b = 128.101;
    double c = 123;
    float d = 112.64E3;
    double e = 0.7623e-2;
    float f = 1.23002398;
    printf("a=%e \nb=%f \nc=%lf \nd=%lE \ne=%lf \nf=%f\n", a, b, c, d, e, f);
   
    return 0;
}

运行结果:
a=3.020000e-01
b=128.100998
c=123.000000
d=1.126400E+05
e=0.007623
f=1.230024
  • %f 和 %lf 默认保留六位小数,不足六位以 0 补齐,超过六位按四舍五入截断。
  • 将整数赋值给 float 变量时会变成小数。
  • 以指数形式输出小数时,输出结果为科学计数法;也就是说,尾数部分的取值为:0 ≤ 尾数 < 10。
  • b 的输出结果让人费解,才三位小数,为什么不能精确输出,而是输出一个近似值呢?这和小数在内存中的存储形式有关,很多简单的小数压根不能精确存储,所以也就不能精确输出。

小数还有一种更加智能的输出方式,就是使用%g。%g 会对比小数的十进制形式和指数形式,以最短的方式来输出小数,让输出结果更加简练。所谓最短,就是输出结果占用最少的字符。

#include <stdio.h>
#include <stdlib.h>
int main()
{
    
    
    float a = 0.00001;
    float b = 30000000;
    float c = 12.84;
    float d = 1.229338455;
    printf("a=%g \nb=%g \nc=%g \nd=%g\n", a, b, c, d);
   
    return 0;
}
运行结果:
a=1e-05
b=3e+07
c=12.84
d=1.22934
  • %g 默认最多保留六位有效数字,包括整数部分和小数部分;%f 和 %e 默认保留六位小数,只包括小数部分。
  • %g 不会在最后强加 0 来凑够有效数字的位数,而 %f 和 %e 会在最后强加 0 来凑够小数部分的位数。
  • %g 和 %lg 分别用来输出 float 类型和 double 类型,并且当以指数形式输出时,e小写。
  • %G 和 %lG 也分别用来输出 float 类型和 double 类型,只是当以指数形式输出时,E大写。

数字的后缀

  • 在整数后面紧跟 l 或者 L(不区分大小写)表明该数字是 long 类型;
  • 在小数后面紧跟 f 或者 F(不区分大小写)表明该数字是 float 类型。

小数和整数相互赋值

  • 将一个整数赋值给小数类型,在小数点后面加 0 就可以,加几个都无所谓。
  • 将一个小数赋值给整数类型,就得把小数部分丢掉,只能取整数部分,这会改变数字本来的值。注意是直接丢掉小数部分,而不是按照四舍五入取近似值。

小数在内存中是如何存储的

小数在内存中是以浮点数的形式存储的。浮点数是数字(或者说数值)在内存中的一种存储格式,它和定点数是相对的。

C语言标准规定,小数在内存中以科学计数法的形式来存储,具体形式为:

flt = (-1)sign × mantissa × baseexponent

在C语言中使用英文字符

字符类型由单引号’ '包围,字符串由双引号" "包围。

//字符正确的写法
char a = '1';
char b = '$';
char c = 'X';
char d = ' ';  // 空格也是一个字符

//错误的写法
char x = '中';  //char 类型不能包含 ASCII 编码之外的字符
char y = 'A';  //A 是一个全角字符
char z = "t";  //字符类型应该由单引号包围
// 字符串
char str1[] = "http://c.biancheng.net";
char *str2 = "C语言中文网";

在C语言中使用中文字符

非ASCII 编码字符存储

使用宽字符的编码方式。常见的宽字符编码有 UTF-16 和 UTF-32,它们都是基于 Unicode 字符集的,能够支持全球的语言文化。
C语言推出了一种新的类型,叫做 wchar_t。wchar_t 的长度由编译器决定:

  • 在微软编译器下,它的长度是 2,等价于 unsigned short;
  • 在GCC、LLVM/Clang 下,它的长度是 4,等价于 unsigned int。

单独的字符由单引号’ ‘包围,例如’B’、‘@’、‘9’等;但是,这样的字符只能使用 ASCII 编码,要想使用宽字符的编码方式,就得加上L前缀,例如L’A’、L’9’、L’中’、L’国’、L’。'。

注意,加上L前缀后,所有的字符都将成为宽字符,占用 2 个字节或者 4 个字节的内存,包括 ASCII 中的英文字符。给字符串加上L前缀就变成了宽字符串,它包含的每个字符都是宽字符,一律采用 UTF-16 或者 UTF-32 编码。

wchar_t a = L'A';  //英文字符(基本拉丁字符)
wchar_t b = L'9';  //英文数字(阿拉伯数字)
wchar_t c = L'中';  //中文汉字
wchar_t d = L'国';  //中文汉字
wchar_t e = L'。';  //中文标点
wchar_t f = L'ヅ';  //日文片假名
wchar_t g = L'♥';  //特殊符号
wchar_t h = L'༄';  //藏文

wchar_t web_url[] = L"http://c.biancheng.net";
wchar_t *web_name = L"C语言中文网";

将不加L前缀的字符称为窄字符,将加上L前缀的字符称为宽字符。窄字符使用 ASCII 编码,宽字符使用 UTF-16 或者 UTF-32 编码。

宽字符(串)的输出

putchar、printf 只能输出不加L前缀的窄字符,对加了L前缀的宽字符无能为力,我们必须使用 <wchar.h> 头文件中的宽字符输出函数,它们分别是 putwchar 和 wprintf:

  • putwchar 函数专门用来输出一个宽字符,它和 putchar 的用法类似;
  • wprintf 是通用的、格式化的宽字符输出函数,它除了可以输出单个宽字符,还可以输出宽字符串。宽字符对应的格式控制符为%lc, 宽字符串对应的格式控制符是%ls。

在输出宽字符之前还要使用 setlocale 函数(setlocale 函数位于 <locale.h> 头文件中)进行本地化设置,告诉程序如何才能正确地处理各个国家的语言文化。(先记住)

希望设置为中文简体环境:
Windows 下请写作:
setlocale(LC_ALL, "zh-CN");
在 Linux 和 Mac OS 下请写作:
setlocale(LC_ALL, "zh_CN");

代码演示:

#include <wchar.h>
#include <locale.h>

int main(){
    
    
    wchar_t a = L'A';  //英文字符(基本拉丁字符)
    wchar_t b = L'9';  //英文数字(阿拉伯数字)
    wchar_t c = L'中';  //中文汉字
    wchar_t d = L'国';  //中文汉字
    wchar_t e = L'。';  //中文标点
    wchar_t f = L'ヅ';  //日文片假名
    wchar_t g = L'♥';  //特殊符号
    wchar_t h = L'༄';  //藏文
   
    //将本地环境设置为简体中文
    setlocale(LC_ALL, "zh_CN");

    //使用专门的 putwchar 输出宽字符
    putwchar(a);  putwchar(b);  putwchar(c);  putwchar(d);
    putwchar(e);  putwchar(f);  putwchar(g);  putwchar(h);
    putwchar(L'\n');  //只能使用宽字符
   
    //使用通用的 wprintf 输出宽字符
    wprintf(
        L"Wide chars: %lc %lc %lc %lc %lc %lc %lc %lc\n",  //必须使用宽字符串
        a, b, c, d, e, f, g, h
    );
   
    //将本地环境设置为简体中文
    setlocale(LC_ALL, "zh_CN");
    //使用通用的 wprintf 输出宽字符
    wprintf(L"web_url: %ls \nweb_name: %ls\n", web_url, web_name);
    return 0;
}

C语言到底使用的编码

  • 对于 char 类型的窄字符,始终使用 ASCII 编码。

  • 对于 wchar_t 类型的宽字符和宽字符串,使用 UTF-16 或者 UTF-32 编码,它们都是基于 Unicode 字符集的。

  • 对于 char 类型的窄字符串,微软编译器使用本地编码,GCC、LLVM/Clang 使用和源文件编码相同的编码。

C语言转义字符

转义字符以\或者\x开头:

  • 以\开头表示后跟八进制形式的编码值,
  • 以\x开头表示后跟十六进制形式的编码值。
  • 对于转义字符来说,只能使用八进制或者十六进制。
  • 转义字符既可以用于单个字符,也可以用于字符串,并且一个字符串中可以同时使用八进制形式和十六进制形式。

转义字符的初衷是用于 ASCII 编码,所以它的取值范围有限:

  • 八进制形式的转义字符最多后跟三个数字,也即\ddd,最大取值是\177;
  • 十六进制形式的转义字符最多后跟两个数字,也即\xdd,最大取值是\x7f。
  • 超出范围的转义字符的行为是未定义的,有的编译器会将编码值直接输出,有的编译器会报错。

对于 ASCII 编码,0~31(十进制)范围内的字符为控制字符,它们都是看不见的,不能在显示器上显示,甚至无法从键盘输入,只能用转义字符的形式来表示。不过,直接使用 ASCII 码记忆不方便,也不容易理解,所以,针对常用的控制字符,C语言又定义了简写方式,完整的列表如下:

ASCII码值(十进制) 意义 转义字符
007 响铃(BEL) \a
008 退格(BS) ,将当前位置移到前一列 \b
012 换页(FF),将当前位置移到下页开头 \f
010 换行(LF) ,将当前位置移到下一行开头 \n
013 回车(CR) ,将当前位置移到本行开头 \r
009 水平制表(HT) \t
011 垂直制表(VT) \v
039 单引号
034 双引号 "
092 反斜杠 \

C语言标识符、关键字、注释、表达式和语句

标识符

  • C语言规定,标识符只能由字母(A~Z, az)、数字(09)和下划线(_)组成,并且第一个字符必须是字母或下划线,不能是数字。
  • C语言虽然不限制标识符的长度,但是它受到不同编译器的限制,同时也受到操作系统的限制。例如在某个编译器中规定标识符前128位有效,当两个标识符前128位相同时,则被认为是同一个标识符。
  • 在标识符中,大小写是有区别的,例如 BOOK 和 book 是两个不同的标识符。
  • 标识符虽然可由程序员随意定义,但标识符是用于标识某个量的符号,因此,命名应尽量有相应的意义,以便于阅读和理解,作到“顾名思义”。

关键字

关键字 说明
enum 声明枚举类型
union 声明共用数据类型
struct 声明结构体变量或函数
char 声明字符型变量或函数
double 声明双精度变量或函数
float 声明浮点型变量或函数
long 声明长整型变量或函数
int 声明整型变量或函数
short 声明短整型变量或函数
case 开关语句分支
switch 用于开关语句
else 条件语句否定分支(与 if 连用)
if 条件语句
void 声明函数无返回值或无参数,声明无类型指针
volatile 说明变量在程序执行中可被隐含地改变
static 声明静态变量
register 声明寄存器变量
extern 声明变量是在其他文件正声明
signed 声明有符号类型变量或函数
unsigned 声明无符号类型变量或函数
const 声明只读变量
typedef 用以给数据类型取别名
auto 声明自动变量
return 子程序返回语句(可以带参数,也可不带参数)循环条件
sizeof 计算数据类型长度
default 开关语句中的“其他”分支
break 跳出当前循环
continue 结束当前循环,开始下一轮循环
goto 无条件跳转语句
while 循环语句的循环条件
do 循环语句的循环体
for 一种循环语句

注释

C语言支持单行注释和多行注释:

  • 单行注释以//开头,直到本行末尾(不能换行);
  • 多行注释以/开头,以/结尾,注释内容可以有一行或多行。

表达式(Expression)和语句(Statement)

  • 表达式必须有一个执行结果,这个结果必须是一个值,例如3*4+5的结果 17,a=c=d=10的结果是 10,printf(“hello”)的结果是 5(printf 的返回值是成功打印的字符的个数)。
  • 以分号;结束的往往称为语句,而不是表达式,例如3*4+5;、a=c=d;等。

C语言加减乘除运算

加法 减法 乘法 除法 求余数(取余)
数学 + - × ÷
C语言 + - × / %

对除法的说明

C语言中的除法运算有点奇怪,不同类型的除数和被除数会导致不同类型的运算结果:

  • 当除数和被除数都是整数时,运算结果也是整数;如果不能整除,那么就直接丢掉小数部分,只保留整数部分,这跟将小数赋值给整数类型是一个道理。
  • 一旦除数和被除数中有一个是小数,那么运算结果也是小数,并且是 double 类型的小数。
  • 另外需要注意的一点是除数不能为 0,因为任何一个数字除以 0 都没有意义。

对取余运算的说明

C语言中的取余运算只能针对整数,也就是说,% 的两边都必须是整数,不能出现小数,否则编译器会报错。
余数可以是正数也可以是负数,由 % 左边的整数决定:

  • 如果 % 左边是正数,那么余数也是正数;
  • 如果 % 左边是负数,那么余数也是负数。
    在 printf 中,% 是格式控制符的开头,是一个特殊的字符,不能直接输出;要想输出 %,必须在它的前面再加一个 %,这个时候 % 就变成了普通的字符,而不是用来表示格式控制符了。

运算的简写

a = a # b 可以简写为:a #= b (# 表示 +、-、*、/、% 中的任何一种运算符。)
注意:a #= b 仅是一种简写形式,不会影响程序的执行效率。

C语言自增(++)和自减(–)

++和–分别称为自增运算符和自减运算符

自增自减完成后,会用新值替换旧值,将新值保存在当前变量中
自增自减的结果必须得有变量来接收,所以自增自减只能针对变量,不能针对数字,例如10++就是错误的。
需要重点说明的是,++ 在变量前面和后面是有区别的:

  • ++ 在前面叫做前自增(例如 ++a)。前自增先进行自增运算,再进行其他操作。
  • ++ 在后面叫做后自增(例如 a++)。后自增先进行其他操作,再进行自增运算。
  • 自减(–)也一样,有前自减和后自减之分。
#include <stdio.h>
int main()
{
    
    
    int a = 12, b = 1;
    int c = a - (b--);  // ①
    int d = (++a) - (--b);  // ②

    printf("c=%d, d=%d\n", c, d);

    return 0;
}
// 输出结果:c=11, d=14

C语言变量的定义位置以及初始值

在函数外部定义的变量叫做全局变量(Global Variable),在函数内部定义的变量叫做局部变量(Local Variable)

局部变量的定义位置

为了让编译器方便给变量分配内存,C89 标准规定,所有的局部变量(函数内部的变量)都必须定义在函数的开头位置,在定义完所有变量之前不能有其它的表达式。这种规定太过死板,虽然变量定义在函数开头,但是使用变量可能在函数的尾部,如果函数比较长,那么定义变量和使用变量的距离就有点远了,编写代码或者阅读代码时就要频繁得向前翻看代码,非常不方便,所以后来的 C99 标准就取消了这个限制。在实际开发中确认编译器开发标准是否支持代码格式.

变量的默认初始值

一个变量,即使不给它赋值,它也会有一个默认的值,这个值就是默认初始值。

  • 对于全局变量,它的默认初始值始终是 0,因为全局变量存储在内存分区中的全局数据区,这个区域中的数据在程序载入内存后会被初始化为 0。
  • 而对于局部变量,C语言并没有规定它的默认初始值是什么,所以不同的编译器进行了不同的扩展,有的编译器会初始化为 0,有的编译器放任不管,爱是什么就是什么。(变量定义时会给变量分配一块内存空间,如果不对变量进行初始化,那就意味着不对这块内存进行写入操作,这块内存的数据会保持不变,依然是分配之前的数据。这样的数据可能是当前程序在之前的运行过程中产生的,也可能是之前运行过的其它程序产生的,我们根本无法预测这样的数据到底是什么,所以你会看到它是一个毫无意义的值,这样的值是随机的,是垃圾值,没有使用价值。)

C语言运算符的优先级和结合性

当一个表达式中出现多个运算符时,C语言会先比较各个运算符的优先级,按照优先级从高到低的顺序依次执行;当遇到优先级相同的运算符时,再根据结合性决定先执行哪个运算符:如果是左结合性就先执行左边的运算符,如果是右结合性就先执行右边的运算符。

运算符优先级和结合性一览表

优先级

运算符

名称或含义

使用形式

结合方向

说明

1

[]

数组下标

数组名[常量表达式]

左到右

 

()

圆括号

(表达式)
函数名(形参表)

 

.

成员选择(对象)

对象.成员名

 

->

成员选择(指针)

对象指针->成员名

 

2

-

负号运算符

-表达式

右到左

单目运算符

(类型)

强制类型转换

(数据类型)表达式

 

++

自增运算符

++变量名
变量名++

单目运算符

--

自减运算符

--变量名
变量名--

单目运算符

*

取值运算符

*指针变量

单目运算符

&

取地址运算符

&变量名

单目运算符

!

逻辑非运算符

!表达式

单目运算符

~

按位取反运算符

~表达式

单目运算符

sizeof

长度运算符

sizeof(表达式)

 

3

/

表达式 / 表达式

左到右

双目运算符

*

表达式*表达式

双目运算符

%

余数(取模)

整型表达式%整型表达式

双目运算符

4

+

表达式+表达式

左到右

双目运算符

-

表达式-表达式

双目运算符

5

<<

左移

变量<<表达式

左到右

双目运算符

>>

右移

变量>>表达式

双目运算符

6

>

大于

表达式>表达式

左到右

双目运算符

>=

大于等于

表达式>=表达式

双目运算符

<

小于

表达式<表达式

双目运算符

<=

小于等于

表达式<=表达式

双目运算符

7

==

等于

表达式==表达式

左到右

双目运算符

!=

不等于

表达式!= 表达式

双目运算符

8

&

按位与

表达式&表达式

左到右

双目运算符

9

^

按位异或

表达式^表达式

左到右

双目运算符

10

|

按位或

表达式|表达式

左到右

双目运算符

11

&&

逻辑与

表达式&&表达式

左到右

双目运算符

12

||

逻辑或

表达式||表达式

左到右

双目运算符

13

?:

条件运算符

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

右到左

三目运算符

14

=

赋值运算符

变量=表达式

右到左

 

/=

除后赋值

变量/=表达式

 

*=

乘后赋值

变量*=表达式

 

%=

取模后赋值

变量%=表达式

 

+=

加后赋值

变量+=表达式

 

-=

减后赋值

变量-=表达式

 

<<=

左移后赋值

变量<<=表达式

 

>>=

右移后赋值

变量>>=表达式

 

&=

按位与后赋值

变量&=表达式

 

^=

按位异或后赋值

变量^=表达式

 

|=

按位或后赋值

变量|=表达式

 

15

,

逗号运算符

表达式,表达式,…

左到右

 

C语言数据类型转换(自动类型转换+强制类型转换)

自动类型转换

  1. 将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换。在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型转换为左边变量的类型,这可能会导致数据失真,或者精度降低;所以说,自动类型转换并不一定是安全的。
  2. 在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换的规则如下:
    2-1: 转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。例如,int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算。
    2-2: 所有的浮点运算都是以双精度进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算。
    2-3: char 和 short 参与运算时,必须先转换成 int 类型。

强制类型转换

自动类型转换是编译器默默地、隐式地进行的一种类型转换,不需要在代码中体现出来;强制类型转换是程序员明确提出的、需要通过特定格式的代码来指明的一种类型转换。换句话说,自动类型转换不需要程序员干预,强制类型转换必须有程序员干预。

强制类型转换的格式为:(type_name) expression
例如:

(float) a;  //将变量 a 转换为 float 类型
(int)(x+y);  //把表达式 x+y 的结果转换为 int 整型
(float) 100;  //将数值 100(默认为int类型)转换为 float 类型

( )的优先级高于/,对于表达式(double) sum / count,会先执行(double) sum,将 sum 转换为 double 类型,然后再进行除法运算,这样运算结果也是 double 类型,能够保留小数部分。注意不要写作(double) (sum / count),这样写运算结果将仍然不能保留小数部分。

类型转换只是临时性的

无论是自动类型转换还是强制类型转换,都只是为了本次运算而进行的临时性转换,转换的结果也会保存到临时的内存空间,不会改变数据本来的类型或者值。

C语言输入输出

C语言数据输出大汇总以及轻量进阶

在C语言中,有三个函数可以用来在显示器上输出数据,它们分别是:

puts():只能输出字符串,并且输出结束后会自动换行
putchar():只能输出单个字符
printf():可以输出各种类型的数据,是最灵活、最复杂、最常用的输出函数,完全可以替代 puts() 和 putchar()

汇总一下前面学到的格式控制符:

格式控制符 说明
%c 输出一个单一的字符
%hd、%d、%ld 以十进制、有符号的形式输出 short、int、long 类型的整数
%hu、%u、%lu 以十进制、无符号的形式输出 short、int、long 类型的整数
%ho、%o、%lo 以八进制、不带前缀、无符号的形式输出 short、int、long 类型的整数
%#ho、%#o、%#lo 以八进制、带前缀、无符号的形式输出 short、int、long 类型的整数
%hx、%x、%lx
%hX、%X、%lX
以十六进制、不带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字也小写;如果 X 大写,那么输出的十六进制数字也大写。
%#hx、%#x、%#lx
%#hX、%#X、%#lX
以十六进制、带前缀、无符号的形式输出 short、int、long 类型的整数。如果 x 小写,那么输出的十六进制数字和前缀都小写;如果 X 大写,那么输出的十六进制数字和前缀都大写。
%f、%lf 以十进制的形式输出 float、double 类型的小数
%e、%le
%E、%lE
以指数的形式输出 float、double 类型的小数。如果 e 小写,那么输出结果中的 e 也小写;如果 E 大写,那么输出结果中的 E 也大写。
%g、%lg
%G、%lG
以十进制和指数中较短的形式输出 float、double 类型的小数,并且小数部分的最后不会添加多余的 0。如果 g 小写,那么当以指数形式输出时 e 也小写;如果 G 大写,那么当以指数形式输出时 E 也大写。
%s 输出一个字符串

printf() 的高级用法

printf() 格式控制符的完整形式如下:

%[flag][width][.precision]type ( [ ] 表示此处的内容可有可无,是可以省略的)

  1. type 表示输出类型,比如 %d、%f、%c、%lf,type 就分别对应 d、f、c、lf;再如,%-9d中 type 对应 d。type 这一项必须有,这意味着输出时必须要知道是什么类型。
  2. width 表示最小输出宽度,也就是至少占用几个字符的位置;例如,%-9d中 width 对应 9,表示输出结果最少占用 9 个字符的宽度。当输出结果的宽度不足 width 时,以空格补齐(如果没有指定对齐方式,默认会在左边补齐空格);当输出结果的宽度超过 width 时,width 不再起作用,按照数据本身的宽度来输出。
  3. .precision 表示输出精度
    当.precision用于小数时,作用于小数的位数: 当小数部分的位数大于 precision 时,会按照四舍五入的原则丢掉多余的数字;当小数部分的位数小于 precision 时,会在后面补 0。
    .precision 也可以用于整数和字符串:用于整数时,.precision 表示最小输出宽度。与 width 不同的是,整数的宽度不足时会在左边补 0,而不是补空格; 用于字符串时,.precision 表示最大输出宽度,或者说截取字符串。当字符串的长度大于 precision 时,会截掉多余的字符;当字符串的长度小于 precision 时,.precision 就不再起作用。
  4. flag 是标志字符。例如,%#x中 flag 对应 #,%-9d中 flags 对应-。下表列出了 printf() 可以用的 flag:
    (-)表示左对齐。如果没有,就按照默认的对齐方式,默认一般为右对齐。
    (+)用于整数或者小数,表示输出符号(正负号)。如果没有,那么只有负数才会输出符号。
    (空格)用于整数或者小数,输出值为正时冠以空格,为负时冠以负号。
    (#)对于八进制(%o)和十六进制(%x / %X)整数,# 表示在输出时添加前缀;八进制的前缀是 0,十六进制的前缀是 0x / 0X; 对于小数(%f / %e / %g),# 表示强迫输出小数点。如果没有小数部分,默认是不输出小数点的,加上 # 以后,即使没有小数部分也会带上小数点。

printf() 不能立即输出的问题

Windows 和 Linux、Mac OS 的缓存机制不同, 表现也不同;要想破解 printf() 输出的问题,必须要了解缓存,它能使你对输入输出的认识上升到一个更高的层次,以后不管遇到什么疑难杂症,都能迎刃而解。可以说,输入输出的“命门”就在于缓存。

C语言scanf:读取从键盘输入的数据(含输入格式汇总表)

在C语言中,有多个函数可以从键盘获得用户输入:

  • scanf():和 printf() 类似,scanf() 可以输入多种类型的数据。
  • getchar()、getche()、getch():这三个函数都用于输入单个字符。
  • gets():获取一行数据,并作为字符串处理。

scanf() 是最灵活、最复杂、最常用的输入函数,但它不能完全取代其他函数。

&称为取地址符,也就是获取变量在内存中的地址。scanf 会根据地址把读取到的数据写入内存。
%p是一个新的格式控制符,它表示以十六进制的形式(带小写的前缀)输出数据的地址。如果写作%P,那么十六进制的前缀也将变成大写形式。

scanf()函数

对于 scanf(),输入数据的格式要和控制字符串的格式保持一致。
举例:

scanf("%d %d", &c, &d);  //输入两个整数并分别赋值给c、d
// 输入:10 23↙ (↙表示按下回车键)
// "%d %d"之间是有空格的,所以输入数据时也要有空格

scanf("%d, %d, %d", &a, &b, &c);
// 56,45,78↙
// 控制字符串为"%d, %d, %d",中间以逗号分隔,所以输入的整数也要以逗号分隔。

连续输入

我们从键盘输入的数据并没有直接交给 scanf(),而是放入了缓冲区中,直到我们按下回车键,scanf() 才到缓冲区中读取数据。如果缓冲区中的数据符合 scanf() 的要求,那么就读取结束;如果不符合要求,那么就继续等待用户输入,或者干脆读取失败。

scanf()的问题

#include <stdio.h>
int main()
{
    
    
    int a = 1, b = 2;
    scanf("a=%d", &a);
    scanf("b=%d", &b);
    printf("a=%d, b=%d\n", a, b);

    return 0;
}

case1:
a=99↙
a=99, b=2
输入a=99,按下回车键,程序竟然运行结束了,只有第一个 scanf() 成功读取了数据,第二个 scanf() 仿佛没有执行一样,根本没有给用户任何机会去输入数据。

case2:
a=99b=200↙
a=99, b=200
a 和 b 都能够正确读取了。注意,a=99b=200中间是没有任何空格的。

case3:
a=99 b=200↙
a=99, b=2
a=99 b=200 中间有空格,第二个 scanf() 又读取失败了!

以上case都是出字C语言中文网,未实际验证,需要谨慎对待, 只能作为参照。

要想破解 scanf() 输入的问题,一定要学习缓冲区,它能使你对输入输出的认识上升到一个更高的层次,以后不管遇到什么疑难杂症,都能迎刃而解。可以说,输入输出的“命门”就在于缓冲区。

输入其它数据

除了输入整数,scanf() 还可以输入单个字符、字符串、小数等:scanf() 和 printf() 虽然功能相反,但是格式控制符是一样的,单个字符、整数、小数、字符串对应的格式控制符分别是 %c、%d、%f、%s。

对读取字符串的说明

字符串的两种定义形式,它们分别是:

  • char str1[] = “http://c.biancheng.net”;
  • char *str2 = “C语言中文网”;

这两种形式其实是有区别的,第一种形式的字符串所在的内存既有读取权限又有写入权限,第二种形式的字符串所在的内存只有读取权限,没有写入权限。printf()、puts() 等字符串输出函数只要求字符串有读取权限,而 scanf()、gets() 等字符串输入函数要求字符串有写入权限,所以,第一种形式的字符串既可以用于输出函数又可以用于输入函数,而第二种形式的字符串只能用于输出函数。

对于第一种形式的字符串,在[ ]里面要指明字符串的最大长度,如果不指明,也可以根据=后面的字符串来自动推算,此处,就是根据"http://c.biancheng.net"的长度来推算的。如果只是定义了一个字符串,并没有立即给它赋值,所以没法自动推算,只能手动指明最大长度,那么就需要在[]中添加类似于[30],定义一个长度。

scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串

scanf() 格式控制符汇总

格式控制符 说明
%c 读取一个单一的字符
%hd、%d、%ld 读取一个十进制整数,并分别赋值给 short、int、long 类型
%ho、%o、%lo 读取一个八进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型
%hx、%x、%lx 读取一个十六进制整数(可带前缀也可不带),并分别赋值给 short、int、long 类型
%hu、%u、%lu 读取一个无符号整数,并分别赋值给 unsigned short、unsigned int、unsigned long 类型
%f、%lf 读取一个十进制形式的小数,并分别赋值给 float、double 类型
%e、%le 读取一个指数形式的小数,并分别赋值给 float、double 类型
%g、%lg 既可以读取一个十进制形式的小数,也可以读取一个指数形式的小数,并分别赋值给 float、double 类型
%s 读取一个字符串(以空白符为结束)

C语言输入字符和字符串(所有函数大汇总)

getchar()

最容易理解的字符输入函数是 getchar(),它就是scanf(“%c”, c)的替代品,除了更加简洁,没有其它优势了;或者说,getchar() 就是 scanf() 的一个简化版本。

getche()

  • getche() 没有缓冲区,输入一个字符后会立即读取,不用等待用户按下回车键,这是它和 scanf()、getchar() 的最大区别。
  • getche() 并不是标准函数,默认只能在 Windows 下使用,不能在 Linux 和 Mac OS 下使用。

getch()

  • getch() 也没有缓冲区,输入一个字符后会立即读取,不用按下回车键,这一点和 getche() 相同。
  • getch() 的特别之处是它没有回显,看不到输入的字符。所谓回显,就是在控制台上显示出用户输入的字符;没有回显,就不会显示用户输入的字符,就好像根本没有输入一样。回显在大部分情况下是有必要的,它能够与用户及时交互,让用户清楚地看到自己输入的内容。但在某些特殊情况下,我们却不希望有回显,例如输入密码,有回显是非常危险的,容易被偷窥。
  • 和 getche() 一样,getch也不是标准函数,默认只能在 Windows 下使用,不能在 Linux 和 Mac OS 下使用。

gets()

gets() 是有缓冲区的,每次按下回车键,就代表当前输入结束了,gets() 开始从缓冲区中读取内容,这一点和 scanf() 是一样的。gets() 和 scanf() 的主要区别是:

  • scanf() 读取字符串时以空格为分隔,遇到空格就认为当前字符串结束了,所以无法读取含有空格的字符串。
  • gets() 认为空格也是字符串的一部分,只有遇到回车键时才认为字符串输入结束,所以,不管输入了多少个空格,只要不按下回车键,对 gets() 来说就是一个完整的字符串。

总结

scanf() 可以一次性读取多份类型相同或者不同的数据,getchar()、getche()、getch() 和 gets() 每次只能读取一份特定类型的数据,不能一次性读取多份数据。

进入缓冲区(缓存)的世界,破解一切与输入输出有关的疑难杂症

计算机在内存中预留了一定的存储空间,用来暂时保存输入或输出的数据,这部分预留的空间就叫做缓冲区(缓存)。

为什么要引入缓冲区(缓存)

  • 减少了等待写入硬盘的次数
  • 减少硬件设备的读写次数, 从而减少时间和空间的开销

缓冲区的类型

  • 根据缓冲区对应的是输入设备还是输出设备,可以分为输入缓冲区和输出缓冲区。

  • 根据数据刷新(也可以称为清空缓冲区,就是将缓冲区中的数据“倒出”)的时机,可以分为全缓冲、行缓冲、不带缓冲。

    1. 全缓冲
      当缓冲区被填满以后才进行真正的输入输出操作。缓冲区的大小都有限制的,比如 1KB、4MB 等,数据量达到最大值时就清空缓冲区。全缓冲的典型代表是对硬盘文件的读写,在实际开发中,将数据写入文件后,打开文件并不能立即看到内容,只有清空缓冲区,或者关闭文件,或者关闭程序后,才能在文件中看到内容。这种现象,就是缓冲区在作怪。
    1. 行缓冲
      在输入或者输出的过程中遇到换行符时,才执行真正的输入输出操作。行缓冲的典型代表就是标准输入设备(也即键盘)和标准输出设备(也即显示器)。
    1. 不带缓冲
      不带缓冲区,数据就没有地方缓存,必须立即进行输入输出。getche()、getch() 就不带缓冲区,输入一个字符后立即就执行了,根本不用按下回车键。

C语言标准的模棱两可

C语言标准规定,输入输出缓冲区要具有以下特征:

  • 当且仅当输入输出不涉及交互设备时,它们才可以是全缓冲的。
  • 错误显示设备不能带有缓冲区。

缓冲区的刷新(清空)

所谓刷新缓冲区,就是将缓冲区中的内容送达到目的地。缓冲区的刷新遵循以下的规则:

  • 不管是行缓冲还是全缓冲,缓冲区满时会自动刷新;
  • 行缓冲遇到换行符\n时会刷新;
  • 关闭文件时会刷新缓冲区;
  • 程序关闭时一般也会刷新缓冲区,这个是由标准库来保障的;
  • 使用特定的函数也可以手动刷新缓冲区

结合C语言缓冲区谈scanf函数,那些奇怪的行为其实都有章可循

scanf() 是从标准输入设备(键盘)读取数据,带有行缓冲区的。
当遇到 scanf() 函数时,程序会先检查输入缓冲区中是否有数据:

  • 如果没有,就等待用户输入。用户从键盘输入的每个字符都会暂时保存到缓冲区,直到按下回车键,产生换行符\n,输入结束,scanf() 再从缓冲区中读取数据,赋值给变量。
  • 如果有数据,那就看是否符合控制字符串的规则:
    1. 如果能够匹配整个控制字符串,那最好了,直接从缓冲区中读取就可以了,就不用等待用户输入了。
    1. 如果缓冲区中剩余的所有数据只能匹配前半部分控制字符串,那就等待用户输入剩下的数据。
    1. 如果不符合,scanf() 还会尝试忽略一些空白符,例如空格、制表符、换行符等:
      – 3-1) 如果这种尝试成功(可以忽略一些空白符),那么再重复以上的匹配过程。
      – 3-2) 如果这种尝试失败(不能忽略空白符),那么只有一种结果,就是读取失败。

事实上这一章的学习中,有几个点的理解是很重要的,一个是 scanf 这个方法的逻辑,还有就是一些特殊的规则:
1)匹配失败意味着不会移动内部的位置指针
2)空格、制表符、换行符在大部分情况下都可以忽略,前面的两个例子就是这样。但是当控制字符串不是以格式控制符 %d、%c、%f 等开头时,空格、制表符、换行符就不能忽略了(比如:scanf(“a=%d”, &a) 这种写法),它会参与匹配过程,如果匹配失败,就意味着 scanf() 读取失败了。

事实上以上的引用部分关于特殊规则的描述,是我自己的总结,我认为这一章中是有些错误的,也反馈给了客户,不过还没有得到回复,如果有不一样的理解请在评论区给我留言,谢谢了:
在这里插入图片描述

在这里插入图片描述

C语言清空(刷新)缓冲区,从根本上消除那些奇怪的行为

清空(刷新)缓冲区即可:

  • 对于输出操作,清空缓冲区会使得缓冲区中的所有数据立即显示到屏幕上;很明显,这些数据没有地方存放了,只能输出了。
  • 对于输入操作,清空缓冲区就是丢弃残留字符,让程序直接等待用户输入,避免引发奇怪的行为。

清空输出缓冲区

清空输出缓冲区很简单,使用下面的语句即可:

fflush(stdout);
fflush() 是一个专门用来清空缓冲区的函数,stdout 是 standard output 的缩写,表示标准输出设备,也即显示器。整个语句的意思是,清空标准输出缓冲区,或者说清空显示器的缓冲区。

在 Linux 和 Mac OS 平台下清空缓冲区:

#include<stdio.h>
#include<unistd.h>
int main()
{
    
    
    printf("C语言中文网");
    fflush(stdout);  //本次输出结束后立即清空缓冲区
    sleep(5);
    printf("http://c.biancheng.net\n");
   
    return 0;
}
// 程序运行后,第一个 pirntf() 立即输出,等待 5 秒以后,第二个 printf() 才输出,这就符合我们的惯性思维了。如果不加fflush(stdout)语句,程序运行后,第一个 printf() 并不会立即输出,而是等待 5 秒以后和第二个 printf() 一起输出

清空输入缓冲区

两种通用的方案

  1. 使用 getchar() 清空缓冲区; 原理:getchar() 是带有缓冲区(行缓冲区)的,每次从缓冲区中读取一个字符,包括空格、制表符、换行符等空白符,只要我们让 getchar() 不停地读取,直到读完缓冲区中的所有字符,就能达到清空缓冲区的效果。请看下面的代码:
#include <stdio.h>
int main()
{
    
    
    int a = 1, b = 2;
    char c;

    scanf("a=%d", &a);
    while((c = getchar()) != '\n' && c != EOF); //在下次读取前清空缓冲区;该代码不停地使用 getchar() 获取缓冲区中的字符,直到遇见换行符\n或者到达文件结尾才停止。
    scanf("b=%d", &b);
    printf("a=%d, b=%d\n", a, b);
   
    return 0;
}
  1. 使用 scanf() 清空缓冲区;原理:scanf() 还有一种高级用法,就是使用类似于正则表达式的通配符,这样它就可以读取所有的字符了,包括空格、换行符、制表符等空白符,不会再忽略它们了。并且,scanf() 还允许把读取到的数据直接丢弃,不用赋值给变量。

scanf(“%*[^\n]”); scanf(“%*c”);
第一个 scanf() 将逐个读取缓冲区中\n之前的其它字符,% 后面的 * 表示将读取的这些字符丢弃,遇到\n字符时便停止读取。此时,缓冲区中尚有一个\n遗留,第二个 scanf() 再将这个\n读取并丢弃,这里的星号和第一个 scanf() 的星号作用相同。由于所有从键盘的输入都是以回车结束的,而回车会产生一个\n字符,所以将\n连同它之前的字符全部读取并丢弃之后,也就相当于清除了输入缓冲区。

#include <stdio.h>
int main()
{
    
    
    int a = 1, b = 2;
   
    scanf("a=%d", &a);
    scanf("%*[^\n]"); scanf("%*c"); //在下次读取前清空缓冲区
    scanf("b=%d", &b);
    printf("a=%d, b=%d\n", a, b);
   
    return 0;
}

总结

最靠谱、最通用、最有效的清空输入缓冲区的方案就是使用 getchar() 或者 scanf() 将缓冲区中的数据逐个读取出来,其它方案都有或多或少的问题。

C语言scanf的高级用法,原来scanf还有这么多新技能

指定读取长度

可以在格式控制符的中间加一个数字,用来表示读取数据的最大长度,例如:

  • %2d表示最多读取两位整数;
  • %10s表示读取的字符串的最大长度为 10,或者说,最多读取 10 个字符。

匹配特定的字符

%s

%s 控制符会匹配除空白符以外的所有字符,它有两个缺点:

  • %s 不能读取特定的字符,比如只想读取小写字母,或者十进制数字等,%s 就无能为力;
  • %s 读取到的字符串中不能包含空白符,有些情况会比较尴尬,例如,无法将多个单词存放到一个字符串中,因为单词之间就是以空格为分隔的,%s 遇到空格就读取结束了。

%[xxx]

[ ]包围起来的是需要读取的字符集合。例如,%[abcd]表示只读取字符abcd,遇到其它的字符就读取结束;注意,这里并不强调字符的顺序,只要字符在 abcd 范围内都可以匹配成功,所以你可以输入 abcd、dcba、ccdc、bdcca 等。

需要注意的是虽然字符的顺序不是关键,但是字符的连续性是有要求的,依然以%[abcd]为例,如果输入字符为 a1b2c3d4,事实上最后匹配的字符是a,如果是 ab12cd34,那么匹配的字符是ab。

使用连接符

为了简化字符集合的写法,scanf() 支持使用连字符-来表示一个范围内的字符,例如 %[a-z]、%[0-9] 等(连字符左边的字符对应一个 ASCII 码,连字符右边的字符也对应一个 ASCII 码,位于这两个 ASCII 码范围以内的字符就是要读取的字符。注意,连字符左边的 ASCII 码要小于右边的,如果反过来,那么它的行为是未定义的)。

常用的连字符举例:

  • %[a-z]表示读取 abc…xyz 范围内的字符,也即小写字母;
  • %[A-Z]表示读取 ABC…XYZ 范围内的字符,也即大写字母;
  • %[0-9]表示读取 012…789 范围内的字符,也即十进制数字。
  • %[a-zA-Z]表示读取大写字母和小写字母,也即所有英文字母;
  • %[a-z-A-Z0-9]表示读取所有的英文字母和十进制数字;
  • %[0-9a-f]表示读取十六进制数字。
不匹配某些字符

scanf() 允许我们在%[ ]中直接指定某些不能匹配的字符,具体方法就是在不匹配的字符前面加上^,例如:

  • %[^\n]表示匹配除换行符以外的所有字符,遇到换行符就停止读取;
  • %[^0-9]表示匹配除十进制数字以外的所有字符,遇到十进制数字就停止读取。
scanf("%[^\n]", str2); //这个操作也能读取带空格的字符串,scanf()完全可以取代 gets()

input: c c++ java python go javascript↙
output: str2=c c++ java python go javascript

丢弃读取到的字符

scanf() 允许把读取到的数据直接丢弃,不往变量中存放,具体方法就是在 % 后面加一个*,例如:

  • %*d表示读取一个整数并丢弃;
  • %*[a-z]表示读取小写字母并丢弃;
  • %*[^\n]表示将换行符以外的字符全部丢弃。

这里我们就可以解释,上一章节中通过scanf来清空缓存区的原理了:

scanf("%*[^\n]"); scanf("%*c");
//scanf("%*[^\n]");将换行符前面的所有字符清空,scanf("%*c");将最后剩下的换行符清空。

总结

scanf() 控制字符串的完整写法为:%{*} {width} type

  • { } 表示可有可无。
  • type表示读取什么类型的数据,例如 %d、%s、%[a-z]、%[^\n] 等;type 必须有。
  • width表示最大读取宽度,可有可无。
  • *表示丢弃读取到的数据,可有可无。

C语言循环结构和选择结构

C语言if else语句详解

if(判断条件){
    
    
    语句块1
}else{
    
    
    语句块2
}

C语言关系运算符详解

C语言提供了以下关系运算符:

关系运算符 含  义 数学中的表示
< 小于 <
<= 小于或等于
> 大于 >
>= 大于或等于
== 等于 =
!= 不等于
  • 关系运算符都是双目运算符,其结合性均为左结合;(对于含多个关系运算符的表达式,如 k == j == i+5,根据运算符的左结合性,先计算k == j,该式不成立,其值为0,再计算0==i+5,也不成立,故表达式值为0。)
  • 关系运算符的优先级低于算术运算符,高于赋值运算符
  • 在六个关系运算符中,<、<=、>、>=的优先级相同,高于 == 和 != , == 和 != 的优先级相同。
  • 在C语言中,有的运算符有两个操作数,例如 10+20,10和20都是操作数,+ 是运算符。我们将这样的运算符称为双目运算符。同理,将有一个操作数的运算符称为单目运算符,将有三个操作数的运算符称为三目运算符。
  • 常见的双目运算符有 +、-、*、/ 等,单目运算符有 ++、-- 等,三目运算符只有一个,就是 ? :
  • 关系运算符的运算结果只有 0 或 1;运算结果 1 称为“真”,表示条件成立,将 0 称为“假”,表示条件不成立。

再谈 if 语句的判断条件

if 语句的判断条件中不是必须要包含关系运算符,它可以是赋值表达式,甚至也可以是一个变量,例如:

//情况①
if(b){
    
    
    //TODO:
}
//情况②
if(b=5){
    
      //情况①
    //TODO:
}

C语言逻辑运算符详解

运算符 说明 结合性 举例
&& 与运算,双目,对应数学中的“且” 左结合 1&&0、(9>3)&&(b>a)
|| 或运算,双目,对应数学中的“或” 左结合 1||0、(9>3)||(b>a)
! 非运算,单目,对应数学中的“非” 右结合 !a、!(2<5)

逻辑运算的结果

逻辑运算的结果也只有“真”和“假”,“真”对应的值为 1,“假”对应的值为 0。

优先级

逻辑运算符和其它运算符优先级从低到高依次为:

赋值运算符(=) < 或|| < 与&& < 关系运算符 < 算术运算符 < 非(!)

按照运算符的优先顺序可以得出:

  • a>b && c>d 等价于 (a>b)&&(c>d)
  • !b==c||d<a 等价于 ((!b)==c)||(d<a)
  • a+b>c&&x+y<b 等价于 ((a+b)>c)&&((x+y)<b)
  • a||b&&c-3 等价于 a||(b&&(c-3))

C语言switch case语句详解

switch(表达式){
    
    
    case 整型数值1: 语句 1;
    case 整型数值2: 语句 2;
    ......
    case 整型数值n: 语句 n;
    default: 语句 n+1;
}

以上语句中,没有 break, 当和某个整型数值匹配成功后,会执行该分支以及后面所有分支的语句。

break 是C语言中的一个关键字,专门用于跳出 switch 语句。所谓“跳出”,是指一旦遇到 break,就不再执行 switch 中的任何语句,包括当前分支中的语句和其他分支中的语句;也就是说,整个 switch 执行结束了,接着会执行整个 switch 后面的代码。

注意点:

  • case 后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量。请看下面的例子:
case 10: printf("..."); break;  //正确
case 8+9: printf("..."); break;  //正确
case 'A': printf("..."); break;  //正确,字符和整数可以相互转换
case 'A'+19: printf("..."); break;  //正确,字符和整数可以相互转换
case 9.5: printf("..."); break;  //错误,不能为小数
case a: printf("..."); break;    //错误,不能包含变量
case a+10: printf("..."); break;  //错误,不能包含变量
  • default 不是必须的。当没有 default 时,如果所有 case 都匹配失败,那么就什么都不执行。

C语言?和:详解,C语言条件运算符详解

语法格式为:

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

max = (a>b) ? a : b;
等价于:
if(a>b){
    
    
    max = a;
}else{
    
    
    max = b;
}


  • 条件运算符的优先级低于关系运算符和算术运算符,但高于赋值符。因此以上还可以直接买写作:max=a>b ? a : b;
  • 条件运算符?和:是一对运算符,不能分开单独使用。
  • 条件运算符的结合方向是自右至左。例如:a>b ? a : c>d ? c : d 应该理解为:a>b ? a : ( c>d ? c : d )

C语言while循环和do while循环详解

while循环

while(表达式){
    
    
    语句块
}

do-while

do{
    
    
    语句块
}while(表达式);

do-while循环与while循环的不同在于:它会先执行“语句块”,然后再判断表达式是否为真,如果为真则继续循环;如果为假,则终止循环。因此,do-while 循环至少要执行一次“语句块”。

C语言for循环(for语句)详解

for 循环,它的使用更加灵活,完全可以取代 while 循环; for 循环的一般形式为:


 for(表达式1; 表达式2; 表达式3){
    
    
    语句块
}

它的运行过程为:

  • 先执行“表达式1”。
  • 再执行“表达式2”,如果它的值为真(非0),则执行循环体,否则结束循环。
  • 执行完循环体后再执行“表达式3”。
  • 重复执行步骤 2) 和 3),直到“表达式2”的值为假,就结束循环。

上面的步骤中,2) 和 3) 是一次循环,会重复执行,for 语句的主要作用就是不断执行步骤 2) 和 3)。

“表达式1”仅在第一次循环时执行,以后都不会再执行,可以认为这是一个初始化语句。“表达式2”一般是一个关系表达式,决定了是否还要继续下次循环,称为“循环条件”。“表达式3”很多情况下是一个带有自增或自减操作的表达式,以使循环条件逐渐变得“不成立”。

C语言for循环中的三个表达式

省略“表达式1(初始化条件)

int i = 1, sum = 0;
for( ; i<=100; i++){
    
    
    sum+=i;
}

上面代码的作用与以下代码相同:
int i, sum=0;
for(i=1/*语句①*/; i<=100/*语句②*/; i++/*语句③*/){
    
    
    sum+=i;
}

省略了“表达式2(循环条件)”

如果不做其它处理就会成为死循环,如下:

for(i=1; ; i++) sum=sum+i;

以上代码相当于:
i=1;
while(1){
    
    
    sum=sum+i;
    i++;
}

省略了“表达式3(自增或自减)”

可在循环体中加入修改变量的语句, 需要注意的是,如果不在循环体中对变量进行修改,那么如果满足“表达式2”的条件的话,也会变成一个“死循环”:

for( i=1; i<=100; ){
    
    
    sum=sum+i;
    i++;
} 

省略了“表达式1(初始化语句)”和“表达式3(自增或自减)”

for( ; i<=100 ; ){
    
    
    sum=sum+i;
    i++;
}

以上代码相当于:
while(i<=100){
    
    
    sum=sum+i;
    i++;
}

3个表达式可以同时省略

for( ; ; )

以上代码相当于:
while(1)

“表达式1”和“表达式3”可以是一个简单表达式也可以是其他语句

for( sum=0; i<=100; i++ )  sum=sum+i;
for( i=0,j=100; i<=100; i++,j-- )  k=i+j;
for( i=0,j=100; i<=100; i++,j-- )  k=i+j;

“表达式2”一般是关系表达式或逻辑表达式,但也可是数值或字符,只要其值非零,就执行循环体

for( i=0; (c=getchar())!='\n'; i+=c );

C语言break和continue用法详解(跳出循环)

使用while或for循环时,如果想提前结束循环(在不满足结束条件的情况下结束循环),可以使用break或continue关键字。

break关键字

当 break 关键字用于 while、for 循环时,会终止循环而执行整个循环语句后面的代码。break 关键字通常和 if 语句一起使用,即满足条件时便跳出循环。

continue语句

continue 语句的作用是跳过循环体中剩余的语句而强制进入下一次循环。continue语句只用在 while、for 循环中,常与 if 条件语句一起使用,判断条件是否成立。

C语言循环嵌套详解

在C语言中,if-else、while、do-while、for 都可以相互嵌套。所谓嵌套(Nest),就是一条语句里面还有另一条语句,例如 for 里面还有 for,while 里面还有 while,或者 for 里面有 while,while 里面有 if-else,这都是允许的。

对C语言选择结构和循环结构的总结

C语言中常用的编程结构有三种(其它编程语言也是如此),它们分别是:

  • 顺序结构:代码从前往后依次执行,没有任何“拐弯抹角”,不跳过任何一条语句,所有的语句都会被执行到。
  • 选择结构:也叫分支结构。代码会被分成多个部分,程序会根据特定条件(某个表达式的运算结果)来判断到底执行哪一部分。
  • 循环结构:程序会重新执行同一段代码,直到条件不再满足,或者遇到强行跳出语句(break/continue 关键字)。

C语言数组详解

C语言数组的基本概念

数组(Array)就是一组数据的集合;它所包含的每一个数据叫做数组元素(Element),所包含的数据的个数称为数组长度(Length),例如int a[4];就定义了一个长度为4的整型数组,名字是a。

数组中的每个元素都有一个序号,这个序号从0开始,而不是从我们熟悉的1开始,称为下标(Index)。使用数组元素时,指明下标即可,形式为:

arrayName[index]; 例如,a[0] 表示第0个元素,a[3] 表示第3个元素。

需要注意的是:

  • 数组中每个元素的数据类型必须相同,对于int a[4];,每个元素都必须为 int
  • 数组长度 length 最好是整数或者常量表达式,例如 10、20 * 4 等,这样在所有编译器下都能运行通过;如果 length 中包含了变量,例如 n、4*m 等,在某些编译器下就会报错
  • 访问数组元素时,下标的取值范围为 0 ≤ index < length,过大或过小都会越界,导致数组溢出,发生不可预测的情况

数组内存是连续的

数组是一个整体,它的内存是连续的;也就是说,数组元素之间是相互挨着的,彼此之间没有一点点缝隙。下图演示了int a[4];在内存中的存储情形:
在这里插入图片描述

数组的初始化

数组的定义方式:

dataType arrayName[length];

先定义,后赋值

int a[4];
a[0]=20;
a[1]=345;
a[2]=700;
a[3]=22;

定义数组的同时赋值

数组元素的值由{ }包围,各个值之间以,分隔。

int a[4] = {
    
    20, 345, 700, 22}; 

数组的初始化注意点

可以只给部分元素赋值。当{ }中值的个数少于元素个数时,只给前面部分元素赋值。

// 表示只给 a[0]~a[4] 5个元素赋值,而后面 5 个元素自动初始化为 0。
int a[10]={
    
    12, 19, 22 , 993, 344};

当赋值的元素少于数组总体元素的时候,剩余的元素自动初始化为 0:

  • 对于short、int、long,就是整数 0;
  • 对于char,就是字符 ‘\0’;
  • 对于float、double,就是小数 0.0。

可以通过下面的形式将数组的所有元素初始化为 0:

// 由于剩余的元素会自动初始化为 0,所以只需要给第 0 个元素赋值为 0 即可。
int nums[10] = {
    
    0};
char str[10] = {
    
    0};
float scores[10] = {
    
    0.0};

只能给元素逐个赋值,不能给数组整体赋值。

例如给 10 个元素全部赋值为 1:

int a[10] = {
    
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1};

而不是:
int a[10] = 1;

如给全部元素赋值,那么在定义数组时可以不给出数组长度

int a[] = {
    
    1, 2, 3, 4, 5};
int a[5] = {
    
    1, 2, 3, 4, 5};

以上两种定义方式是等价的

C语言二维数组的定义、初始化、赋值

二维数组的定义

二维数组定义的一般形式是:

dataType arrayName[length1][length2];

dataType 为数据类型,arrayName 为数组名,length1 为第一维下标的长度,length2 为第二维下标的长度。

二维数组在C语言内存中的存储方式

二维数组在概念上是二维的,但在内存中是连续存放的;换句话说,二维数组的各个元素是相互挨着的,彼此之间没有缝隙:

在C语言中,二维数组是按行排列的,即放完一行之后再放入第二行。以 int a[3][4] 为例,先存放 a[0] 行,再存放 a[1] 行,最后存放 a[2] 行;每行中的 4 个元素也是依次存放。数组 a 为 int 类型,每个元素占用 4 个字节,整个数组共占用 4×(3×4)=48 个字节。
可以这样认为,二维数组是由多个长度相同的一维数组构成的。

二维数组的初始化(赋值)

按行分段赋值
int a[5][3]={
    
     {
    
    80,75,92}, {
    
    61,65,71}, {
    
    59,63,70}, {
    
    85,87,90}, {
    
    76,77,85} };

按行连续赋值
int a[5][3]={
    
    80, 75, 92, 61, 65, 71, 59, 63, 70, 85, 87, 90, 76, 77, 85};

两种赋初值的结果是完全相同的。

注意点

可以只对部分元素赋值,未赋值的元素自动取“零”值

int a[3][3] = {
    
    {
    
    1}, {
    
    2}, {
    
    3}};
对每一行的第一列元素赋值,未赋值的元素的值为 0。赋值后各元素的值为:
1  0  0
2  0  0
3  0  0
int a[3][3] = {
    
    {
    
    0,1}, {
    
    0,0,2}, {
    
    3}};
赋值后各元素的值为:
0  1  0
0  0  2
3  0  0

如果对全部元素赋值,那么第一维的长度可以不给出

int a[3][3] = {
    
    1, 2, 3, 4, 5, 6, 7, 8, 9};
int a[][3] = {
    
    1, 2, 3, 4, 5, 6, 7, 8, 9};

以上两种写法均可

C语言字符数组和字符串详解

在C语言中,没有专门的字符串变量,没有string类型,通常就用一个字符数组来存放一个字符串。
C语言规定,可以将字符串直接赋值给字符数组,例如:

char str[30] = {
    
    "c.biancheng.net"};
char str[30] = "c.biancheng.net";  //这种形式更加简洁,实际开发中常用

也可以不指定数组长度,从而写作:

char str[] = {
    
    "c.biancheng.net"};
char str[] = "c.biancheng.net";  //这种形式更加简洁,实际开发中常用

字符数组只有在定义时才能将整个字符串一次性地赋值给它,一旦定义完了,就只能一个字符一个字符地赋值了。请看下面的例子:

char str[7];
str = "abc123";  //错误
//正确
str[0] = 'a'; str[1] = 'b'; str[2] = 'c';
str[3] = '1'; str[4] = '2'; str[5] = '3';

字符串结束标志

  • 在C语言中,字符串总是以’\0’作为结尾,所以’\0’也被称为字符串结束标志,或者字符串结束符。
  • '\0’是 ASCII 码表中的第 0 个字符,英文称为 NUL,中文称为“空字符”。该字符既不能显示,也没有控制功能,输出该字符不会有任何效果,它在C语言中唯一的作用就是作为字符串结束标志。
  • C语言在处理字符串时,会从前往后逐个扫描字符,一旦遇到’\0’就认为到达了字符串的末尾,就结束处理。‘\0’至关重要,没有’\0’就意味着永远也到达不了字符串的结尾。
  • 由" "包围的字符串会自动在末尾添加’\0’。例如,"abc123"从表面看起来只包含了 6 个字符,其实不然,C语言会在最后隐式地添加一个’\0’,这个过程是在后台默默地进行的,所以我们感受不到。
  • 需要注意的是,逐个字符地给数组赋值并不会自动添加’\0’,例如:char str[] = {‘a’, ‘b’, ‘c’}; 数组 str 的长度为 3,而不是 4,因为最后没有’\0’。
  • 当用字符数组存储字符串时,要特别注意’\0’,要为’\0’留个位置;这意味着,字符数组的长度至少要比字符串的长度大 1。请看下面的例子:char str[7] = “abc123”; "abc123"看起来只包含了 6 个字符,我们却将 str 的长度定义为 7,就是为了能够容纳最后的’\0’。如果将 str 的长度定义为 6,它就无法容纳’\0’了。当字符串长度大于数组长度时,有些较老或者不严格的编译器并不会报错,甚至连警告都没有,这就为以后的错误埋下了伏笔。

演示"C program"在内存中的存储情形:
在这里插入图片描述

需要注意的是:逐个给字符数组赋值,这个时候就很容易遗忘字符串结束标志’\0’
如下:

	char str[30];
    char c;
    int i;
    for(c=65,i=0; c<=90; c++,i++){
    
    
        str[i] = c;
    }

在函数内部定义的变量、数组、结构体、共用体等都称为局部数据。在很多编译器下,局部数据的初始值都是随机的、无意义的,而不是我们通常认为的“零”值,如果不加以处理,会产生奇葩的错误。

在给字符数组的成员逐一赋值的代码中,如果想要添加结束符:

char str[30];
....
str[i] = 0;  //此处为添加的代码,也可以写作 str[i] = '\0';

根据 ASCII 码表,字符'\0'的编码值就是 0

当然还有更加简便的办法,在定义的时候就确定初始值:

char str[30] = {
    
    0};  //将所有元素都初始化为 0,或者说 '\0'

字符串长度

在C语言中,使用string.h头文件中的 strlen() 函数来求字符串的长度,它的用法为:

length strlen(strname); 

char str[] = "http://c.biancheng.net/c/";
long len = strlen(str);// 此处 len 值为25,他并不会把结束符也算进长度

C语言字符串的输入和输出

字符串的输出

输出字符串时只需要给出名字,不能带后边的[ ],例如,下面的两种写法都是错误的:

printf("%s\n", str[]);
puts(str[10]);

正确的写法:

char str[] = "http://c.biancheng.net";
printf("%s\n", str);  //通过字符串名字输出
printf("%s\n", "http://c.biancheng.net");  //直接输出
puts(str);  //通过字符串名字输出
puts("http://c.biancheng.net");  //直接输出

字符串的输入

在C语言中,有两个函数可以让用户从键盘上输入字符串,它们分别是:

  • scanf():通过格式控制符%s输入字符串。除了字符串,scanf() 还能输入其他类型的数据。
  • gets():直接输入字符串,并且只能输入字符串。

scanf() 在读取数据时需要的是数据的地址,这一点是恒定不变的,所以对于 int、char、float 等类型的变量都要在前边添加&以获取它们的地址。但是在输入字符串时,却没有在前边添加&,因为字符串名字或者数组名字在使用的过程中一般都会转换为地址,所以再添加&就是多此一举,甚至会导致错误了。

C语言字符串处理函数

字符串连接函数 strcat()

意思是把两个字符串拼接在一起,语法格式为:

strcat(arrayName1, arrayName2); // arrayName1、arrayName2 为需要拼接的字符串。

  • strcat() 将把 arrayName2 连接到 arrayName1 后面,并删除原来 arrayName1 最后的结束标志’\0’。这意味着,arrayName1 必须足够长,要能够同时容纳 arrayName1 和 arrayName2,否则会越界(超出范围)。

字符串复制函数 strcpy()

意思是字符串复制,也即将字符串从一个地方复制到另外一个地方,语法格式为:

strcpy(arrayName1, arrayName2); // strcpy() 会把 arrayName2 中的字符串拷贝到 arrayName1 中,字符串结束标志’\0’也一同拷贝。将 str2 复制到 str1 后,str1 中原来的内容就被覆盖了,strcpy() 要求 arrayName1 要有足够的长度,否则不能全部装入所拷贝的字符串。

  • 需要注意的是上述表达中所说的内容覆盖,是指完全覆盖;举例:
char str1[50] = "http://c.biancheng.net/cpp/u/jiaocheng/";
char str2[50] = "123";
strcpy(str1, str2);
printf("str1: %s\n", str1);

最终输出的 str1 的内容是: 123

字符串比较函数 strcmp()

意思是字符串比较,语法格式为:

strcmp(arrayName1, arrayName2); // arrayName1 和 arrayName2 是需要比较的两个字符串。

  • 字符本身没有大小之分,strcmp() 以各个字符对应的 ASCII 码值进行比较。strcmp() 从两个字符串的第 0 个字符开始比较,如果它们相等,就继续比较下一个字符,直到遇见不同的字符,或者到字符串的末尾。
  • 返回值:若 arrayName1 和 arrayName2 相同,则返回0;若 arrayName1 大于 arrayName2,则返回大于 0 的值;若 arrayName1 小于 arrayName2,则返回小于0 的值。

C语言数组是静态的,不能插入或删除元素

  • 在C语言中,数组一旦被定义后,占用的内存空间就是固定的,容量就是不可改变的,既不能在任何位置插入元素,也不能在任何位置删除元素,只能读取和修改元素,我们将这样的数组称为静态数组。
  • 如果数组在定义后可以改变容量,允许在任意位置插入或者删除元素,那么这样的数组称为动态数组。
  • PHP、JavaScript 等解释型的脚本语言一般都支持动态数组,而 C、C++ 等编译型的语言一般不支持动态数组。
  • 如果由于项目要求,必须要在数组中插入或者删除元素,该怎么办呢?没办法,只能再造一个新数组!

C语言数组为什么是静态的

插入和删除数组元素都要移动内存,甚至重新开辟一块内存,这是相当消耗资源的。如果一个程序中有大量的此类操作,那么程序的性能将堪忧,这有悖于「C语言非常高效」的初衷,所以C语言并不支持动态数组。
为了保证程序执行效率,为了防止操作错误,C语言只支持静态数组,不支持动态数组。

C语言数组的越界和溢出

数组越界

C语言数组是静态的,不能自动扩容,当下标小于零或大于等于数组长度时,就发生了越界(Out Of Bounds),访问到数组以外的内存。如果下标小于零,就会发生下限越界(Off Normal Lower);如果下标大于等于数组长度,就会发生上限越界(Off Normal Upper)。

  • C语言为了提高效率,保证操作的灵活性,并不会对越界行为进行检查,即使越界了,也能够正常编译,只有在运行期间才可能会发生问题。
  • 我们访问数组时必须非常小心,要确保不会发生越界。

数组溢出

当赋予数组的元素个数超过数组长度时,就会发生溢出(Overflow)。

  • 在用字符串给字符数组赋值时,要保证数组长度大于字符串长度,以容纳结束符’\0’。

C语言变长数组:使用变量指明数组的长度

因为 C89 和 C99 对数组做出了不同的规定:

  • 在 C89 中,必须使用常量表达式指明数组长度;也就是说,数组长度中不能包含变量,不管该变量有没有初始化。
  • 而在 C99 中,可以使用变量指明数组长度。

下面的代码使用常量表达式指明数组长度,在任何编译器下都能编译通过:

int a[10];  //长度为10
int b[3*5];  //长度为15
int c[4+8];  //长度为12

下面的代码使用变量指明数组长度,在 GCC 和 Xcode 下能够编译通过,而在 VC 和 VS(包括 VC 6.0、VS2010、VS2013、VS2015、VS2017 等)下都会报错:

int m = 10, n;
scanf("%d", &n);
int a[m], b[n];

变长数组

变量的值在编译期间并不能确定,只有等到程序运行后,根据计算结果才能知道它的值到底是什么,所以数组长度中一旦包含了变量,那么数组长度在编译期间就不能确定了,也就不能为数组分配内存了,只有等到程序运行后,得到了变量的值,确定了具体的长度,才能给数组分配内存,我们将这样的数组称为变长数组(VLA, Variable Length Array)。

  • 普通数组(固定长度的数组)是在编译期间分配内存的,而变长数组是在运行期间分配内存的。

变长数组仍然是静态数组

变长数组是说数组的长度在定义之前可以改变,一旦定义了,就不能再改变了,所以变长数组的容量也是不能扩大或缩小的,它仍然是静态数组。

C语言对数组元素进行排序(冒泡排序法

C语言函数

C语言函数定义

C语言无参函数的定义

dataType  functionName(){
    
    
    //body
    return dataType
}

无返回值函数

有的函数不需要返回值,或者返回值类型不确定(很少见),那么可以用 void 表示,例如:

void hello(){
    
    
    printf ("Hello,world \n");
    //没有返回值就不需要 return 语句
}

C语言有参函数的定义

dataType  functionName( dataType1 param1, dataType2 param2 ... ){
    
    
    //body
}

函数定义时给出的参数称为形式参数,简称形参;函数调用时给出的参数(也就是传递的数据)称为实际参数,简称实参。函数调用时,将实参的值传递给形参,相当于一次赋值操作。

  • 形参的改变并不会影响到实参,可以说,他们是两个互不影响的变量

函数不能嵌套定义

C语言不允许函数嵌套定义;也就是说,不能在一个函数中定义另外一个函数,必须在所有函数之外定义另外一个函数。main() 也是一个函数定义,也不能在 main() 函数内部定义新函数。只能是函数调用其他函数。

C语言形参和实参的区别

  • 形参变量只有在函数被调用时才会分配内存,调用结束后,立刻释放内存,所以形参变量只有在函数内部有效,不能在函数外部使用。
  • 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的数据,在进行函数调用时,它们都必须有确定的值,以便把这些值传送给形参,所以应该提前用赋值、输入等办法使实参获得确定值。
  • 实参和形参在数量上、类型上、顺序上必须严格一致,否则会发生“类型不匹配”的错误。当然,如果能够进行自动类型转换,或者进行了强制类型转换,那么实参类型也可以不同于形参类型。
  • 函数调用中发生的数据传递是单向的,只能把实参的值传递给形参,而不能把形参的值反向地传递给实参;换句话说,一旦完成数据的传递,实参和形参就再也没有瓜葛了,所以,在函数调用过程中,形参的值发生改变并不会影响实参。
  • 形参和实参虽然可以同名,但它们之间是相互独立的,互不影响,因为实参在函数外部有效,而形参在函数内部有效。

C语言return的用法详解,C语言函数返回值详解

return 语句的一般形式为:

return 表达式;
或者
return (表达式);

有没有( )都是正确的,为了简明,一般也不写( )。例如:
return max;
return a+b;
return (100+200);

C语言函数调用详解

如果一个函数 A() 在定义或调用过程中出现了对另外一个函数 B() 的调用,那么我们就称 A() 为主调函数或主函数,称 B() 为被调函数。

当主调函数遇到被调函数时,主调函数会暂停,CPU 转而执行被调函数的代码;被调函数执行完毕后再返回主调函数,主调函数根据刚才的状态继续往下执行。

一个C语言程序的执行过程可以认为是多个函数之间的相互调用过程,它们形成了一个或简单或复杂的调用链条。这个链条的起点是 main(),终点也是 main()。当 main() 调用完了所有的函数,它会返回一个值(例如return 0;)来结束自己的生命,从而结束整个程序。

函数是一个可以重复使用的代码块,CPU 会一条一条地挨着执行其中的代码,当遇到函数调用时,CPU 首先要记录下当前代码块中下一条代码的地址(假设地址为 0X1000),然后跳转到另外一个代码块,执行完毕后再回来继续执行 0X1000 处的代码。整个过程相当于 CPU 开了一个小差,暂时放下手中的工作去做点别的事情,做完了再继续刚才的工作。

C语言函数声明以及函数原型

所谓声明(Declaration),就是告诉编译器我要使用这个函数,你现在没有找到它的定义不要紧,请不要报错,稍后我会把定义补上。

函数声明的格式非常简单,相当于去掉函数定义中的函数体,并在最后加上分号;,如下所示:

dataType  functionName( dataType1 param1, dataType2 param2 ... );

也可以不写形参,只写数据类型:

dataType  functionName( dataType1, dataType2 ... );

函数声明给出了函数名、返回值类型、参数列表(重点是参数类型)等与该函数有关的信息,称为函数原型(Function Prototype)。函数原型的作用是告诉编译器与该函数有关的信息,让编译器知道函数的存在,以及存在的形式,即使函数暂时没有定义,编译器也知道如何使用它。

有了函数声明,函数定义就可以出现在任何地方了,甚至是其他文件、静态链接库、动态链接库等。

对于多个文件的程序,通常是将函数定义放到源文件(.c文件)中,将函数的声明放到头文件(.h文件)中,使用函数时引入对应的头文件就可以,编译器会在链接阶段找到函数体。

最后再补充一点,函数原型给出了使用该函数的所有细节,当我们不知道如何使用某个函数时,需要查找的是它的原型,而不是它的定义,我们往往不关心它的实现。

C语言全局变量和局部变量

局部变量

定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。

  • 在 main 函数中定义的变量也是局部变量,只能在 main 函数中使用;同时,main 函数中也不能使用其它函数中定义的变量。main 函数也是一个函数,与其它函数地位平等。
  • 形参变量、在函数体内定义的变量都是局部变量。实参给形参传值的过程也就是给局部变量赋值的过程。
  • 可以在不同的函数中使用相同的变量名,它们表示不同的数据,分配不同的内存,互不干扰,也不会发生混淆。
  • 在语句块中也可定义变量,它的作用域只限于当前语句块。

全局变量

在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。

C语言规定,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量。

作用域

  • 在函数内部定义的变量,它的作用域也仅限于函数内部,出了函数就不能使用了,我们将这样的变量称为局部变量(Local Variable)。函数的形参也是局部变量,也只能在函数内部使用。
  • C语言允许在所有函数的外部定义变量,这样的变量称为全局变量(Global Variable)。全局变量的默认作用域是整个程序,也就是所有的代码文件,包括源文件(.c文件)和头文件(.h文件)。
  • 如果给全局变量加上 static 关键字,它的作用域就变成了当前文件,在其它文件中就无效了。

关于变量的命名

  • 不同函数内部的同名变量是两个完全独立的变量,它们之间没有任何关联,也不会相互影响。
  • 函数内部的局部变量和函数外部的全局变量同名时,在当前函数这个局部作用域中,全局变量会被“屏蔽”,不再起作用。也就是说,在函数内部使用的是局部变量,而不是全局变量。
  • 变量的使用遵循就近原则,如果在当前的局部作用域中找到了同名变量,就不会再去更大的全局作用域中查找。另外,只能从小的作用域向大的作用域中去寻找变量,而不能反过来,使用更小的作用域中的变量。

C语言块级变量

所谓代码块,就是由{ }包围起来的代码。
C语言允许在代码块内部定义变量,这样的变量具有块级作用域;换句话说,在代码块内部定义的变量只能在代码块内部使用,出了代码块就无效了。

在 for 循环条件里面定义变量

遵循 C99 标准的编译器允许在 for 循环条件里面定义新变量,这样的变量也是块级变量,它的作用域仅限于 for 循环内部。

单独的代码块

C语言还允许出现单独的代码块,它也是一个作用域。请看下面的代码:

#include <stdio.h>
int main(){
    
    
    int n = 22;  //编号①
    //由{ }包围的代码块
    {
    
    
        int n = 40;  //编号②
        printf("block n: %d\n", n);
    }
    printf("main n: %d\n", n);
   
    return 0;
}

总结

main() 函数是主函数,它可以调用其它函数,而不允许被其它函数调用。因此,C程序的执行总是从 main() 函数开始,完成对其它函数的调用后再返回到 main() 函数,最后由 main() 函数结束整个程序。

C语言预处理命令

C语言预处理命令是什么?

在编译之前对源文件进行简单加工的过程,就称为预处理(即预先处理、提前处理)。

  • 预处理主要是处理以#开头的命令,例如#include <stdio.h>等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。
  • 预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。

编译器会将预处理的结果保存到和源文件同名的.i文件中,例如 main.c 的预处理结果在 main.i 中。和.c一样,.i也是文本文件,可以用编辑器打开直接查看内容。
C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

实例

#include <stdio.h>

//不同的平台下引入不同的头文件
#if _WIN32  //识别windows平台
#include <windows.h>
#elif __linux__  //识别linux平台
#include <unistd.h>
#endif

int main() {
    
    
    //不同的平台下调用不同的函数
    #if _WIN32  //识别windows平台
    Sleep(5000);
    #elif __linux__  //识别linux平台
    sleep(5);
    #endif

    puts("http://c.biancheng.net/");

    return 0;
}

#if、#elif、#endif 就是预处理命令,它们都是在编译之前由预处理程序来执行的。

C语言#include的用法详解

#include使用详解

#include叫做文件包含命令,用来引入对应的头文件(.h文件)。#include 也是C语言预处理命令的一种。

#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。

#include 的用法有两种,如下所示:

#include <stdHeader.h>
#include "myHeader.h"

使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同:
1)使用尖括号< >,编译器会到系统路径下查找头文件;
2)而使用双引号" ",编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。

注意事项

  • 一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。
  • 同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。
  • 文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
  • 「在头文件中定义定义函数和全局变量」这种认知是原则性的错误!不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误。

C语言#define的用法,C语言宏定义

#define 叫做宏定义命令,它也是C语言预处理命令的一种。宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。

宏定义的一般形式为:#define 宏名 字符串。#表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名是标识符的一种,命名规则和变量相同。字符串可以是数字、表达式、if 语句、函数等。

  • 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
  • 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
  • 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用#undef命令。例如:
#define PI 3.14159

int main(){
    
    
    // Code
    return 0;
}

#undef PI

void func(){
    
    
    // Code
}

表示 PI 只在 main() 函数中有效,在 func() 中无效。
  • 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替,例如:
#include <stdio.h>
#define OK 100
int main(){
    
    
    printf("OK\n");
    return 0;
}
  • 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。例如:
#define PI 3.1415926
#define S PI*y*y    /* PI是已定义的宏名*/
  • 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
  • 可用宏定义表示数据类型,使书写方便。例如:
#define UINT unsigned int

C语言带参数的宏定义

C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。

带参宏定义的一般形式为:

#define 宏名(形参列表) 字符串

带参宏调用的一般形式为:

宏名(实参列表);

例如:

#define M(y) y*y+3*y  //宏定义
// TODO:
k=M(5);  //宏调用

在宏展开时,用实参 5 去代替形参 y,经预处理程序展开后的语句为k=5*5+3*5

对带参宏定义的说明

  • 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。
  • 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。
  • 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。

C语言宏参数的字符串化和宏参数的连接

在宏定义中,有时还会用到#和##两个符号,它们能够对宏参数进行操作。

# 的用法

#用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。

例如有如下宏定义:

#define STR(s) #s

printf("%s", STR(c.biancheng.net));
printf("%s", STR("c.biancheng.net"));

展开后:
printf("%s", "c.biancheng.net");
printf("%s", "\"c.biancheng.net\"");

##的用法

##称为连接符,用来将宏参数或其他的串连接起来。

例如有如下的宏定义:

#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00

printf("%f\n", CON1(8.5, 2));
printf("%d\n", CON2(12, 34));

展开后:
printf("%f\n", 8.5e2);
printf("%d\n", 123400);

C语言中几个预定义宏

预定义宏就是已经预先定义好的宏,可以直接使用,无需再重新定义。

ANSI C 规定了以下几个预定义宏,它们在各个编译器下都可以使用:

__LINE__:表示当前源代码的行号;
__FILE__:表示当前源文件的名称;
__DATE__:表示当前的编译日期;
__TIME__:表示当前的编译时间;
__STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
__cplusplus:当编写C++程序时该标识符被定义。

预定义宏演示:

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

int main() {
    
    
    printf("Date : %s\n", __DATE__);
    printf("Time : %s\n", __TIME__);
    printf("File : %s\n", __FILE__);
    printf("Line : %d\n", __LINE__);

    system("pause");
    return 0;
}

C语言#if、##ifdef、#ifndef的用法详解,C语言条件编译详解

#include <stdio.h>
int main(){
    
    
    #if _WIN32
        system("color 0c");
        printf("http://c.biancheng.net\n");
    #elif __linux__
        printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
    #else
        printf("http://c.biancheng.net\n");
    #endif

    return 0;
}

#if、#elif、#else 和 #endif 都是预处理命令,整段代码的意思是:如果宏 _WIN32 的值为真,就保留第 45 行代码,删除第 79 行代码;如果宏 __linux__ 的值为真,就保留第 7 行代码;如果所有的宏都为假,就保留第 9 行代码。
  • 这些操作都是在预处理阶段完成的,多余的代码以及所有的宏都不会参与编译,不仅保证了代码的正确性,还减小了编译后文件的体积。
  • 这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能。

#if 的用法

#if 整型常量表达式1
    程序段1
#elif 整型常量表达式2
    程序段2
#elif 整型常量表达式3
    程序段3
#else
    程序段4
#endif

它的意思是:如常“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else
  • #if 命令要求判断条件为“整型常量表达式”,也就是说,表达式中不能包含变量,而且结果必须是整数;

#ifdef 的用法

#ifdef  宏名
    程序段1
#else
    程序段2
#endif

它的意思是,如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。

#ifndef 的用法

#ifndef 宏名
    程序段1 
#else 
    程序段2 
#endif

与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。

三者之间的区别

#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。

C语言#error命令,阻止程序编译

#error 指令用于在编译期间产生错误信息,并阻止程序的编译,其形式如下:

#error error_message

#ifdef WIN32
#error This programme cannot compile at Windows Platform
#endif

WIN32 是 Windows 下的预定义宏。当用户在 Windows 下编译该程序时,由于定义了 WIN32 这个宏,所以会执行 #error 命令,提示用户发生了编译错误
  • 需要注意的是:报错信息不需要加引号" ",如果加上,引号会被一起输出。
希望以 C++ 的方式来编译程序时

#ifndef __cplusplus
#error 当前程序必须以C++方式编译
#endif

指针

C语言指针是什么?

计算机中所有的数据都必须放在内存中,不同类型的数据占用的字节数不一样,例如 int 占用 4 个字节,char 占用 1 个字节。为了正确地访问这些数据,必须为每个字节都编上号码,就像门牌号、身份证号一样,每个字节的编号是唯一的,根据编号可以准确地找到某个字节。

我们将内存中字节的编号称为地址(Address)或指针(Pointer)。

一切都是地址

  • 数据和代码都以二进制的形式存储在内存中,计算机无法从格式上区分某块内存到底存储的是数据还是代码。当程序被加载到内存后,操作系统会给不同的内存块指定不同的权限,拥有读取和执行权限的内存块就是代码,而拥有读取和写入权限(也可能只有读取权限)的内存块就是数据。
  • CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
  • 需要注意的是,虽然变量名、函数名、字符串名和数组名在本质上是一样的,它们都是地址的助记符,但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、字符串名和数组名表示的是代码块或数据块的首地址。

指针变量的定义和使用

定义

数据在内存中的地址也称为指针,如果一个变量存储了一份数据的指针,我们就称它为指针变量。

定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:

datatype *name;
或者
datatype *name = value;
  • *表示这是一个指针变量,datatype表示该指针变量所指向的数据的类型 。

普通变量一样,指针变量也可以被多次写入,请看下面的代码:

//定义普通变量
float a = 99.5, b = 10.6;
char c = '@', d = '#';
//定义指针变量
float *p1 = &a;
char *p2 = &c;
//修改指针变量的值
p1 = &b;
p2 = &d;

*是一个特殊符号,表明一个变量是指针变量,定义 p1、p2 时必须带*。而给 p1、p2 赋值时,因为已经知道了它是一个指针变量,就没必要多此一举再带上*,后边可以像使用普通变量一样来使用指针变量。也就是说,定义指针变量时必须带*,给指针变量赋值时不能带*

指针变量也可以连续定义:

int *a, *b, *c;  //a、b、c 的类型都是 int*   

注意每个变量前面都要带*

通过指针变量取得数据

指针变量存储了数据的地址,通过指针变量能够获得该地址上的数据,格式为:

*pointer

这里的*称为指针运算符,用来取得某个地址上的数据,请看下面的例子:

#include <stdio.h>

int main(){
    
    
    int a = 15;
    int *p = &a;
    printf("%d, %d\n", a, *p);  //两种方式都可以输出a的值
    return 0;
}

运行结果:
15, 15
  • 使用指针是间接获取数据,使用变量名是直接获取数据,前者比后者的代价要高。

指针除了可以获取内存上的数据,也可以修改内存上的数据:

int *p = &a;
*p = 100;
  • *在不同的场景下有不同的作用:可以用在指针变量的定义中,表明这是一个指针变量,以和普通变量区分开;使用指针变量时在前面加表示获取指针指向的数据,或者说表示的是指针指向的数据本身。

需要注意的是,给指针变量本身赋值时不能加*:

int *p;
p = &a;
*p = 100;

指针变量也可以出现在普通变量能出现的任何表达式中,例如:

int x, y, *px = &x, *py = &y;
y = *px + 5;  //表示把x的内容加5并赋给y,*px+5相当于(*px)+5
y = ++*px;  //px的内容加上1之后赋给y,++*px相当于++(*px)
y = *px++;  //相当于y=*(px++) -> y=*px; px+1
y = (*px)++;  //这里的结果相当于 y = *px; (*px)+1;
py = px;  //把一个指针的值赋给另一个指针

关于 * 和 & 的谜题

*&a和&*pa分别是什么意思:

  • &a可以理解为(&a),&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 pa),绕来绕去,又回到了原点,&a仍然等价于 a。
  • &*pa可以理解为&(*pa),*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa。

C语言指针变量的运算(加法、减法和比较运算)

指针变量保存的是地址,而地址本质上是一个整数,所以指针变量可以进行部分运算
  • 指针变量加减运算的结果跟数据类型的长度有关,而不是简单地加 1 或减 1
  • 数组中的所有元素在内存中是连续排列的,如果一个指针指向了数组中的某个元素,那么加 1 就表示指向下一个元素,减 1 就表示指向上一个元素,这样指针的加减运算就具有了现实的意义
  • 指针变量除了可以参与加减运算,还可以参与比较运算。当对指针变量进行比较运算时,比较的是指针变量本身的值,也就是数据的地址。如果地址相等,那么两个指针就指向同一份数据,否则就指向不同的数据。
  • 另外需要说明的是,不能对指针变量进行乘法、除法、取余等其他运算,除了会发生语法错误,也没有实际的含义。

C语言数组指针(指向数组的指针)详解

定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。

int arr[] = {
    
     99, 15, 100, 888, 252 };

arr 是int*类型的指针,每次加 1 时它自身的值会增加 sizeof(int),加 i 时自身的值会增加 sizeof(int) * i

定义一个指向数组的指针,例如:

int arr[] = {
    
     99, 15, 100, 888, 252 };
int *p = arr;
或者
int *p = &arr[0];

如果一个指针指向了数组,我们就称它为数组指针(Array Pointer)。
arr 本身就是一个指针,可以直接赋值给指针变量 p。arr 是数组第 0 个元素的地址
  • 数组指针指向的是数组中的一个具体元素,而不是整个数组,所以数组指针的类型和数组元素的类型有关,上面的例子中,p 指向的数组元素是 int 类型,所以 p 的类型必须也是int *。
  • p 并不知道它指向的是一个数组,p 只知道它指向的是一个整数,
  • 在求数组的长度时不能使用sizeof§ / sizeof(int),因为 p 只是一个指向 int 类型的指针
  • 根据数组指针不能逆推出整个数组元素的个数,以及数组从哪里开始、到哪里结束等信息

引入数组指针后,我们就有两种方案来访问数组元素了,一种是使用下标,另外一种是使用指针。

  1. 使用下标; 也就是采用 arr[i] 的形式访问数组元素。如果 p 是指向数组 arr 的指针,那么也可以使用 p[i] 来访问数组元素,它等价于 arr[i]。
  2. 使用指针; 也就是使用 *(p+i) 的形式访问数组元素。另外数组名本身也是指针,也可以使用 *(arr+i) 来访问数组元素,它等价于 *(p+i)。

练习

假设 p 是指向数组 arr 中第 n 个元素的指针,那么 p++、++p、(*p)++:

  • *p++ 等价于 *(p++),表示先取得第 n 个元素的值,再将 p 指向下一个元素
  • *++p 等价于 *(++p),会先进行 ++p 运算,使得 p 的值增加,指向下一个元素,整体上相当于 *(p+1),所以会获得第 n+1 个数组元素的值。
  • (*p)++ 就非常简单了,会先取得第 n 个元素的值,再对该元素的值加 1。

C语言字符串指针(指向字符串的指针)

C语言中没有特定的字符串类型,我们通常是将字符串放在一个字符数组中:

char str[] = "http://c.biancheng.net";

除了字符数组,C语言还支持另外一种表示字符串的方法,就是直接使用一个指针指向字符串:

char *str = "http://c.biancheng.net";
或者:
char *str;
str = "http://c.biancheng.net";

字符串中的所有字符在内存中是连续排列的,str 指向的是字符串的第 0 个字符;我们通常将第 0  个字符的地址称为字符串的首地址。字符串中每个字符的类型都是char,所以 str 的类型也必须是char *

字符数组和指针的区别:它们最根本的区别是在内存中的存储区域不一样,字符数组存储在全局数据区或栈区,指针形式的字符串存储在常量区。全局数据区和栈区的字符串(也包括其他数据)有读取和写入的权限,而常量区的字符串(也包括其他数据)只有读取权限,没有写入权限。

C语言数组灵活多变的访问形式

  • printf() 输出字符串时,要求给出一个起始地址,并从这个地址开始输出,直到遇见字符串结束标志\0。
  • 指针可以参加运算;*(str+4) 表示第4个字符。
  • 数组元素的访问形式可以看做 address[offset],address 为起始地址,offset 为偏移量:(str+1)[5]表示以地址 str+1 为起点,向后偏移5个字符,等价于str[6]。
  • 字符与整数运算时,先转换为整数(字符对应的ASCII码)。num1 与 c4 右边的表达式相同,对于 num1,*str+2 == ‘c’+2 == 99+2 == 101; 101 对应的字符为 ‘e’。

C语言指针变量作为函数参数

函数的参数不仅可以是整数、小数、字符等具体的数据,还可以是指向它们的指针。用指针变量作函数参数可以将函数外部的地址传递到函数内部,使得在函数内部可以操作函数外部的数据,并且这些数据不会随着函数的结束而被销毁。

用数组作函数参数

int max(int *intArr, int len){
    
    
    ...
    return maxValue;
}
或者
int max(int intArr[6], int len){
    
    
    ...
    return maxValue;
}
或者
int max(int intArr[], int len){
    
    
    ...
    return maxValue;
}
  • 不管是int intArr[6]还是int intArr[]都不会创建一个数组出来,编译器也不会为它们分配内存,实际的数组是不存在的,它们最终还是会转换为int *intArr这样的指针。这就意味着,两种形式都不能将数组的所有元素“一股脑”传递进来,大家还得规规矩矩使用数组指针。
  • 需要强调的是,不管使用哪种方式传递数组,都不能在函数内部求得数组长度,因为 intArr 仅仅是一个指针,而不是真正的数组,所以必须要额外增加一个参数来传递数组长度。

为什么不允许直接传递数组的所有元素,而必须传递数组指针呢?

  • 参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。
  • 对于像 int、float、char 等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行内存拷贝有可能是一个漫长的过程,会严重拖慢程序的效率,为了防止技艺不佳的程序员写出低效的代码,C语言没有从语法上支持数据集合的直接赋值。
  • 除了C语言,C++、Java、Python 等其它语言也禁止对大块内存进行拷贝,在底层都使用类似指针的方式来实现。

C语言指针作为函数返回值

C语言允许函数的返回值是一个指针(地址),我们将这样的函数称为指针函数。

char *strlong(char *str1, char *str2){
    
    
    if(strlen(str1) >= strlen(str2)){
    
    
        return str1;
    }else{
    
    
        return str2;
    }
}
  • 函数运行结束后会销毁在它内部定义的所有局部数据,包括局部变量、局部数组和形式参数,函数返回的指针请尽量不要指向这些数据,C语言没有任何机制来保证这些数据会一直有效,它们在后续使用过程中可能会引发运行时错误。
  • 这里所谓的销毁并不是将局部数据所占用的内存全部抹掉,而是程序放弃对它的使用权限,弃之不理,后面的代码可以随意使用这块内存。如果使用及时也能够得到正确的数据,如果有其它函数被调用就会覆盖这块内存,得到的数据就失去了意义。

C语言二级指针(指向指针的指针)详解

  • 指针可以指向一份普通类型的数据,例如 int、double、char 等,也可以指向一份指针类型的数据,例如 int *、double *、char * 等。
  • 如果一个指针指向的是另外一个指针,我们就称它为二级指针,或者指向指针的指针。
#include <stdio.h>
int main(){
    
    
    int a =100;
    int *p1 = &a;
    int **p2 = &p1;
    int ***p3 = &p2;
    return 0;
}

以三级指针 p3 为例来分析上面的代码。***p3等价于*(*(*p3))*p3 得到的是 p2 的值,也即 p1 的地址;*(*p3) 得到的是 p1 的值,也即 a 的地址;经过三次“取值”操作后,*(*(*p3)) 得到的才是 a 的值。

C语言空指针NULL以及void指针

空指针 NULL

一个指针变量可以指向计算机中的任何一块内存,不管该内存有没有被分配,也不管该内存有没有使用权限,只要把地址给它,它就可以指向,C语言没有一种机制来保证指向的内存的正确性

  • 未初始化的局部变量的值是不确定的,C语言并没有对此作出规定,不同的编译器有不同的实现,建议不要直接使用未初始化的局部变量。一个未初始化的局部变量,它的值是不确定的,究竟指向哪块内存也是未知的,大多数情况下这块内存没有被分配或者没有读写权限,在运行阶段会出现未知的错误。

建议对没有初始化的指针赋值为 NULL,例如:

char *str = NULL;

gets() 和 printf() 都对空指针做了特殊处理:

  • gets() 不会让用户输入字符串,也不会向指针指向的内存中写入数据;
  • printf() 不会读取指针指向的内容,只是简单地给出提示,让程序员意识到使用了一个空指针。

NULL

NULL 是“零值、等于零”的意思,在C语言中表示空指针。从表面上理解,空指针是不指向任何数据的指针,是无效指针,程序使用它不会产生效果。

NULL 是在stdio.h中定义的一个宏,它的具体内容为:

#define NULL ((void *)0) 

(void *)0表示把数值 0 强制转换为void *类型
C语言没有规定 NULL 的指向,只是大部分标准库约定成俗地将 NULL 指向 0,所以不要将 NULL0 等同起来

注意 NULL 和 NUL 的区别:NULL 表示空指针,是一个宏定义,可以在代码中直接使用。而 NUL 表示字符串的结束标志 ‘\0’,它是ASCII码表中的第 0 个字符。NUL 没有在C语言中定义,仅仅是对 ‘\0’ 的称呼,不能在代码中直接使用。

void 指针

void 用在函数定义中可以表示函数没有返回值或者没有形式参数,用在这里表示指针指向的数据的类型是未知的。
void *表示一个有效指针,它确实指向实实在在的数据,只是数据的类型尚未确定,在后续使用过程中一般要进行强制类型转换

C语言动态内存分配函数 malloc() 的返回值就是void *类型,在使用时要进行强制类型转换,请看下面的例子:

#include <stdio.h>
int main(){
    
    
    //分配可以保存30个字符的内存,并把返回的指针转换为 char *
    char *str = (char *)malloc(sizeof(char) * 30);
    gets(str);
    printf("%s\n", str);
    return 0;
}

数组和指针绝不等价,数组是另外一种类型

  • 将数组理解为由基本类型派生得到的稍微复杂一些的类型。sizeof 就是根据符号的类型来计算长度的。
  • 站在编译器的角度讲,变量名、数组名都是一种符号

数组到底在什么时候会转换为指针

C语言标准规定,当数组名作为数组定义的标识符(也就是定义或声明数组时)、sizeof 或 & 的操作数时,它才表示整个数组本身,在其他的表达式中,数组名会被转换为指向第 0 个元素的指针(地址)。
C语言标准还规定,数组下标与指针的偏移量相同。通俗地理解,就是对数组下标的引用总是可以写成“一个指向数组的起始地址的指针加上偏移量”。
C语言标准规定,作为“类型的数组”的形参应该调整为“类型的指针”。在函数形参定义这个特殊情况下,编译器必须把数组形式改写成指向数组第 0 个元素的指针形式。编译器只向函数传递数组的地址,而不是整个数组的拷贝。

C语言指针数组(数组每个元素都是指针)详解

如果一个数组中的所有元素保存的都是指针,那么我们就称它为指针数组。指针数组的定义形式一般为:

dataType *arrayName[length];

[ ]的优先级高于*,该定义形式应该理解为

//括号里面说明arrayName是一个数组,包含了length个元素,括号外面说明每个元素的类型为dataType *。
dataType *(arrayName[length]);

int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *arr[]

指针数组还可以和字符串数组结合使用:
第一种写法:

#include <stdio.h>
int main(){
    
    
    char *str[3] = {
    
    
        "c.biancheng.net",
        "C语言中文网",
        "C Language"
    };
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

第二种写法更加明了:

#include <stdio.h>
int main(){
    
    
    char *str0 = "c.biancheng.net";
    char *str1 = "C语言中文网";
    char *str2 = "C Language";
    char *str[3] = {
    
    str0, str1, str2};
    printf("%s\n%s\n%s\n", str[0], str[1], str[2]);
    return 0;
}

一道题目玩转指针数组和二级指针

#include <stdio.h>

int main(){
    
    
    char *lines[5] = {
    
    
        "COSC1283/1284",
        "Programming",
        "Techniques",
        "is",
        "great fun"
    };

    char *str1 = lines[1];
    char *str2 = *(lines + 3);
    char c1 = *(*(lines + 4) + 6);
    char c2 = (*lines + 5)[5]; // 这里会比较有意思,这里其实是要理解数组在什么时候被视为指针,什么时候视为数组变量
    char c3 = *lines[0] + 2;

    printf("str1 = %s\n", str1);
    printf("str2 = %s\n", str2);
    printf("  c1 = %c\n", c1);
    printf("  c2 = %c\n", c2);
    printf("  c3 = %c\n", c3);

    return 0;
}

C语言二维数组指针(指向二维数组的指针)详解

定义一个指向二维数组 a 的指针变量 p:

int (*p)[4] = a;

括号中的*表明 p 是一个指针,它指向一个数组,数组的类型为int [4],这正是 a 所包含的每个一维数组的类型。

[ ]的优先级高于*( )是必须要加的,如果赤裸裸地写作int *p[4],那么应该理解为int *(p[4]),p 就成了一个指针数组,而不是二维数组指针

*(p+1)+1表示第 1 行第 1 个元素的地址。如何理解呢:
1) *(p+1)单独使用时表示的是第 1 行数据,放在表达式中会被转换为第 1 行数据的首地址,也就是第 1 行第 0 个元素的地址,因为使用整行数据没有实际的含义,编译器遇到这种情况都会转换为指向该行第 0 个元素的指针;就像一维数组的名字,在定义时或者和 sizeof& 一起使用时才表示整个数组,出现在表达式中就会被转换为指向数组第 0 个元素的指针。
2) *(*(p+1)+1)表示第 1 行第 1 个元素的值。很明显,增加一个 * 表示取地址上的数据。

C语言函数指针(指向函数的指针)详解

一个函数总是占用一段连续的内存区域,函数名在表达式中有时也会被转换为该函数所在内存区域的首地址,这和数组名非常类似。我们可以把函数的这个首地址(或称入口地址)赋予一个指针变量,使指针变量指向函数所在的内存区域,然后通过指针变量就可以找到并调用该函数。这种指针就是函数指针。

函数指针的定义形式为:

returnType (*pointerName)(param list);

returnType 为函数返回值类型,pointerName 为指针名称,param list 为函数参数列表。参数列表中可以同时给出参数的类型和名称,也可以只给出参数的类型,省略参数的名称,这一点和函数原型非常类似。
注意( )的优先级高于*,第一个括号不能省略,如果写作returnType *pointerName(param list);就成了函数原型,它表明函数的返回值类型为returnType *。

示例:

#include <stdio.h>

//返回两个数中较大的一个
int max(int a, int b){
    
    
    return a>b ? a : b;
}

int main(){
    
    
    int x, y, maxval;
    //定义函数指针
    int (*pmax)(int, int) = max;  //也可以写作int (*pmax)(int a, int b)
    printf("Input two numbers:");
    scanf("%d %d", &x, &y);
    // pmax 是一个函数指针,在前面加 * 就表示对它指向的函数进行调用。注意( )的优先级高于*,第一个括号不能省略。
    maxval = (*pmax)(x, y);
    printf("Max value: %d\n", maxval);

    return 0;
}

攻克C语言指针

规则:

1)C语言标准规定,对于一个符号的定义,编译器总是从它的名字开始读取,然后按照优先级顺序依次解析。对,从名字开始,不是从开头也不是从末尾,这是理解复杂指针的关键!

符号优先级,从高到低:
1)定义中被括号( )括起来的那部分。
2)后缀操作符:括号( )表示这是一个函数,方括号[ ]表示这是一个数组。
3)前缀操作符:星号*表示“指向xxx的指针”。

1) int *p1[6];
p1 是一个拥有 6int * 元素的数组,也即指针数组。

2) int (*p3)[6];
p3 是一个指向拥有 6int 元素数组的指针,也即二维数组指针

3) int (*p4)(int, int);
p4 是一个指向原型为int func(int, int);的函数的指针。

4) char *(* c[10])(int **p);
c 是一个拥有 10 个元素的指针数组,每个指针指向一个原型为char *func(int **p);的函数。

5) int (*(*(*pfunc)(int *))[5])(int *);
pfunc 是一个函数指针,该函数的返回值是一个指针,它指向一个指针数组,指针数组中的指针指向原型为int func(int *);的函数。

main()函数的高级用法:接收用户输入的数据

#include <stdio.h>
int main(int argc, char *argv[]){
    
    
    int i;
    printf("The program receives %d parameters:\n", argc);
    for(i=0; i<argc; i++){
    
    
        printf("%s\n", argv[i]);
    }
    return 0;
}

对C语言指针的总结

常见指针变量的定义

定 义 含 义
int *p; p 可以指向 int 类型的数据,也可以指向类似 int arr[n] 的数组
int **p; p 为二级指针,指向 int * 类型的数据。
int *p[n]; p 为指针数组。[ ] 的优先级高于 *,所以应该理解为 int *(p[n]);
int (*p)[n]; p 为二维数组指针。
int *p(); p 是一个函数,它的返回值类型为 int *。
int (*p)(); p 是一个函数指针,指向原型为 int func() 的函数。
  • 指针变量可以进行加减运算,例如p++、p+i、p-=i。指针变量的加减运算并不是简单的加上或减去一个整数,而是跟指针指向的数据类型有关。

  • 给指针变量赋值时,要将一份数据的地址赋给它,不能直接赋给一个整数,例如int *p = 1000;是没有意义的,使用过程中一般会导致程序崩溃。

  • 使用指针变量之前一定要初始化,否则就不能确定指针指向哪里,如果它指向的内存没有使用权限,程序就崩溃了。对于暂时没有指向的指针,建议赋值NULL。

  • 两个指针变量可以相减。如果两个指针变量指向同一个数组中的某个元素,那么相减的结果就是两个指针之间相差的元素个数。

  • 数组也是有类型的,数组名的本意是表示一组类型相同的数据。在定义数组时,或者和 sizeof、& 运算符一起使用时数组名才表示整个数组,表达式中的数组名会被转换为一个指向数组的指针。

结构体

C语言结构体详解,C语言struct用法详解

在C语言中,可以使用结构体(Struct)来存放一组不同类型的数据。结构体的定义形式为:

struct 结构体名{
    
    
    结构体所包含的变量或数组
};

代码示例:

struct stu{
    
    
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在学习小组
    float score;  //成绩
};

结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。
注意大括号后面的分号;不能少,这是一条完整的语句。

结构体变量

变量定义:

struct stu stu1, stu2; (定义了两个变量 stu1 和 stu2,它们都是 stu 类型,都由 5 个成员组成。注意关键字struct不能少。)

定义结构体的同时定义结构体变量:

struct stu{
    
    
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在学习小组
    float score;  //成绩
} stu1, stu2;  // 将变量放在结构体定义的最后即可。

如果只需要 stu1、stu2 两个变量,后面不需要再使用结构体名定义其他变量,那么在定义时也可以不给出结构体名,如下所示:

struct{
    
      //没有写 stu
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在学习小组
    float score;  //成绩
} stu1, stu2;

成员的获取和赋值

结构体使用点号.获取单个成员。获取结构体成员的一般格式为:

结构体变量名.成员名;

通过这种方式可以获取成员的值,也可以给成员赋值:

#include <stdio.h>
int main(){
    
    
    struct{
    
    
        char *name;  //姓名
        int num;  //学号
        int age;  //年龄
        char group;  //所在小组
        float score;  //成绩
    } stu1;

    //给结构体成员赋值
    stu1.name = "Tom";
    stu1.num = 12;
    stu1.age = 18;
    stu1.group = 'A';
    stu1.score = 136.5;

    //读取结构体成员的值
    printf("%s的学号是%d,年龄是%d,在%c组,今年的成绩是%.1f!\n", stu1.name, stu1.num, stu1.age, stu1.group, stu1.score);

    return 0;
}

除了可以对成员进行逐一赋值,也可以在定义时整体赋值(整体赋值仅限于定义结构体变量的时候,在使用过程中只能对成员逐一赋值),例如:

struct{
    
    
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1, stu2 = {
    
     "Tom", 12, 18, 'A', 136.5 };
// 此处需要注意,上面的代码中其实只给stu2整体赋值了,如果想要给stu1 stu2 都赋值

    struct{
    
    
        char *name;  //姓名
        int num;  //学号
        int age;  //年龄
        char group;  //所在小组
        float score;  //成绩
    } stu1 = {
    
     "To00m", 121, 181, 'b', 1360.5 }, stu2 = {
    
     "Tom", 12, 18, 'A', 136.5 };

结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。

C语言结构体数组

定义结构体数组:
第一种,在定义结构体同时定义数组:

struct stu{
    
    
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
}class[5];

struct stu{
    
    
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
}class[5] = {
    
    
    {
    
    "Li ping", 5, 18, 'C', 145.0},
    {
    
    "Zhang ping", 4, 19, 'A', 130.5},
    {
    
    "He fang", 1, 18, 'A', 148.5},
    {
    
    "Cheng ling", 2, 17, 'F', 139.0},
    {
    
    "Wang ming", 3, 17, 'B', 144.5}
};

第二种定义分开:

struct stu{
    
    
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组 
    float score;  //成绩
};

struct stu s[5];

C语言结构体指针

C语言结构体指针的定义形式一般为:

struct 结构体名 *变量名;

定义结构体指针的实例:

//结构体
struct stu{
    
    
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = {
    
     "Tom", 12, 18, 'A', 136.5 };
//结构体指针
struct stu *pstu = &stu1;

也可以在定义结构体的同时定义结构体指针:

struct stu{
    
    
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
} stu1 = {
    
     "Tom", 12, 18, 'A', 136.5 }, *pstu = &stu1;

注意,结构体变量名和数组名不同,数组名在表达式中会被转换为数组指针,而结构体变量名不会,无论在任何表达式中它表示的都是整个集合本身,要想取得结构体变量的地址,必须在前面加&,所以给 pstu 赋值只能写作:struct stu *pstu = &stu1;

结构体和结构体变量是两个不同的概念:结构体是一种数据类型,是一种创建变量的模板,编译器不会为它分配内存空间,就像 int、float、char 这些关键字本身不占用内存一样;结构体变量才包含实实在在的数据,才需要内存来存储。

获取结构体成员

通过结构体指针可以获取结构体成员,一般形式为:

(pointer).memberName // 第一种写法中,.的优先级高于,(*pointer)两边的括号不能少。
或者:
pointer->memberName // 第二种写法中,->是一个新的运算符,习惯称它为“箭头”,有了它,可以通过结构体指针直接取得结构体成员;这也是->在C语言中的唯一用途。

结构体指针作为函数参数

结构体变量名代表的是整个集合本身,作为函数参数时传递的整个集合,也就是所有成员,而不是像数组一样被编译器转换成一个指针。如果结构体成员较多,尤其是成员为数组时,传送的时间和空间开销会很大,影响程序的运行效率。所以最好的办法就是使用结构体指针,这时由实参传向形参的只是一个地址,非常快速。

struct stu{
    
    
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在小组
    float score;  //成绩
}stus[] = {
    
    
    {
    
    "Li ping", 5, 18, 'C', 145.0},
    {
    
    "Zhang ping", 4, 19, 'A', 130.5},
    {
    
    "He fang", 1, 18, 'A', 148.5},
    {
    
    "Cheng ling", 2, 17, 'F', 139.0},
    {
    
    "Wang ming", 3, 17, 'B', 144.5}
};

void average(struct stu *ps, int len);

int main(){
    
    
    int len = sizeof(stus) / sizeof(struct stu);
    average(stus, len);
    return 0;
}

C语言枚举类型

枚举类型的定义形式为:

enum typeName{
    
     valueName1, valueName2, valueName3, ...... };

列出一个星期有几天:

enum week{
    
     Mon, Tues, Wed, Thurs, Fri, Sat, Sun };

枚举值默认从 0 开始,往后逐个加 1(递增);也就是说,week 中的 Mon、Tues … Sun 对应的值分别为 0、1 … 6。

给每个名字都指定一个值:

enum week{
    
     Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 };

// 这种有规则递增的数据,还可以这么写:
enum week{
    
     Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };

// 无规则的枚举
enum week{
    
     Mon = 1, Tues = 2, Wed = 3, Thurs = 5, Fri = 5, Sat = 6, Sun = 3 };

枚举是一种类型,通过它可以定义枚举变量:

// 第一种,在定义枚举类型的同时定义变量:a/b/c 的类型就是 enum week
enum week{
    
     Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a, b, c;
a = Mon;
或者
enum week{
    
     Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat;
或者:
enum week{
    
     Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
enum week a = Mon, b = Wed, c = Sat;

// 第二种,单独定义枚举变量
enum week{
    
     Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
enum week a, b, c;
a = Mon;

需要注意的两点是:

枚举列表中的 Mon、Tues、Wed 这些标识符的作用范围是全局的,不能再定义与它们名字相同的变量。
Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。
枚举和宏其实非常类似:宏在预处理阶段将名字替换成对应的值,枚举在编译阶段将名字替换成对应的值。我们可以将枚举理解为编译阶段的宏。

知识点补充&回忆:

Mon、Tues、Wed 这些名字都被替换成了对应的数字。这意味着,Mon、Tues、Wed 等都不是变量,它们不占用数据区(常量区、全局数据区、栈区和堆区)的内存,而是直接被编译到命令里面,放到代码区,所以不能用&取得它们的地址。这就是枚举的本质。
case 关键字后面必须是一个整数,或者是结果为整数的表达式,但不能包含任何变量,正是由于 Mon、Tues、Wed 这些名字最终会被替换成一个整数,所以它们才能放在 case 后面。

C语言共用体

定义格式为:

共用体有时也被称为联合或者联合体,这也是 Union 这个单词的本意。

union 共用体名{
    
    
    成员列表
};
  • 结构体占用的内存大于等于所有成员占用的内存的总和(成员之间可能会存在缝隙),共用体占用的内存等于最长的成员占用的内存。共用体使用了内存覆盖技术,同一时刻只能保存一个成员的值,如果对新的成员赋值,就会把原来成员的值覆盖掉。
  • 结构体和共用体的区别在于:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。

变量定义:
1)

union data{
    
    
    int n;
    char ch;
    double f;
};
union data a, b, c;

2)

union data{
    
    
    int n;
    char ch;
    double f;
};
union data a, b, c;

3)如果不再定义新的变量,也可以将共用体的名字省略:

union{
    
    
    int n;
    char ch;
    double f;
} a, b, c;

共用体的应用

共用体在一般的编程中应用较少,在单片机中应用较多。

示例:
学生信息包括姓名、编号、性别、职业、分数,教师的信息包括姓名、编号、性别、职业、教学科目。
如果把每个人的信息都看作一个结构体变量的话,那么教师和学生的前 4 个成员变量是一样的,第 5 个成员变量可能是 score 或者 course。当第 4 个成员变量的值是 s 的时候,第 5 个成员变量就是 score;当第 4 个成员变量的值是 t 的时候,第 5 个成员变量就是 course。

struct{
    
    
    char name[20];
    int num;
    char sex;
    char profession;
    union{
    
    
        float score;
        char course[20];
    } sc;
} bodys[TOTAL];

大端小端以及判别方式

大端和小端是指数据在内存中的存储模式,它由 CPU 决定:

大端模式(Big-endian)是指将数据的低位(比如 1234 中的 34 就是低位)放在内存的高地址上,而数据的高位(比如 1234 中的 12 就是高位)放在内存的低地址上。这种存储模式有点儿类似于把数据当作字符串顺序处理,地址由小到大增加,而数据从高位往低位存放。

小端模式(Little-endian)是指将数据的低位放在内存的低地址上,而数据的高位放在内存的高地址上。这种存储模式将地址的高低和数据的大小结合起来,高地址存放数值较大的部分,低地址存放数值较小的部分,这和我们的思维习惯是一致,比较容易理解。

C语言位域

位域定义:

在结构体定义时,我们可以指定某个成员变量所占用的二进制位数(Bit),这就是位域。

例子:

struct bs{
    
    
    unsigned m;
    unsigned n: 4;
    unsigned char ch: 6;
};

// :后面的数字用来限定成员变量占用的位数。成员 m 没有限制,根据数据类型即可推算出它占用 4 个字节(Byte)的内存。
// 成员 n、ch 被:后面的数字限制,不能再根据数据类型计算长度,它们分别占用 4、6 位(Bit)的内存。

C语言标准规定,位域的宽度不能超过它所依附的数据类型的长度。通俗地讲,成员变量都是有类型的,这个类型限制了成员变量的最大长度,:后面的数字不能超过这个长度。

C语言标准还规定,只有有限的几种数据类型可以用于位域。在 ANSI C 中,这几种数据类型是 int、signed int 和 unsigned int(int 默认就是 signed int);到了 C99,_Bool 也被支持了(但编译器在具体实现时都进行了扩展,额外支持了 char、signed char、unsigned char 以及 enum 类型,所以上面的代码虽然不符合C语言标准,但它依然能够被编译器支持。)。

位域的存储(不同的编译器有不同的实现)

位域的具体存储规则如下:

  • 当相邻成员的类型相同时,如果它们的位宽之和小于类型的 sizeof 大小,那么后面的成员紧邻前一个成员存储,直到不能容纳为止;如果它们的位宽之和大于类型的 sizeof 大小,那么后面的成员将从新的存储单元开始,其偏移量为类型大小的整数倍。
  • 当相邻成员的类型不同时,不同的编译器有不同的实现方案,GCC 会压缩存储,而 VC/VS 不会。
  • 如果成员之间穿插着非位域成员,那么不会进行压缩。

通过上面的分析,我们发现位域成员往往不占用完整的字节,有时候也不处于字节的开头位置,因此使用&获取位域成员的地址是没有意义的,C语言也禁止这样做。地址是字节(Byte)的编号,而不是位(Bit)的编号

无名位域

位域成员可以没有名称,只给出数据类型和位宽,如下所示:

struct bs{
    
    
    int m: 12;
    int  : 20;  //该位域成员不能使用
    int n: 4;
};

无名位域一般用来作填充或者调整成员位置。因为没有名称,无名位域不能使用。

C语言位运算(按位与运算、或运算、异或运算、左移运算、右移运算)

需要注意的是,这里的运算都是是根据内存中的二进制位进行运算的,而不是数据的二进制形式.
要理解这句话可以参考 & 运算中的例子。

按位与运算(&)

一个比特(Bit)位只有 0 和 1 两个取值,只有参与&运算的两个位都为 1 时,结果才为 1,否则为 0。例如1&1为 1,0&0为 0,1&0也为 0。

C语言中不能直接使用二进制,&两边的操作数可以是十进制、八进制、十六进制,它们在内存中最终都是以二进制形式存储,&就是对这些内存中的二进制位进行运算。

例子:9 & 5可以转换成如下的运算

  0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 10019 在内存中的存储)
& 0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 01015 在内存中的存储)
-----------------------------------------------------------------------------------
  0000 0000 -- 0000 0000 -- 0000 0000 -- 0000 00011 在内存中的存储)
  
按位与运算会对参与运算的两个数的所有二进制位进行&运算,9 & 5的结果为 1

按位与运算通常用来对某些位清 0,或者保留某些位。例如要把 n 的高 16 位清 0 ,保留低 16 位,可以进行n & 0XFFFF运算(0XFFFF 在内存中的存储形式为 0000 0000 – 0000 0000 – 1111 1111 – 1111 1111)。

按位或运算(|)

参与|运算的两个二进制位有一个为 1 时,结果就为 1,两个都为 0 时结果才为 0。例如1|1为1,0|0为0,1|0为1,这和逻辑运算中的||非常类似。

按位或运算可以用来将某些位置 1,或者保留某些位。例如要把 n 的高 16 位置 1,保留低 16 位,可以进行n | 0XFFFF0000运算(0XFFFF0000 在内存中的存储形式为 1111 1111 – 1111 1111 – 0000 0000 – 0000 0000)。

按位异或运算(^)

参与^运算两个二进制位不同时,结果为 1,相同时结果为 0。例如0^110^001^10

按位异或运算可以用来将某些二进制位反转。例如要把 n 的高 16 位反转,保留低 16 位,可以进行n ^ 0XFFFF0000运算(0XFFFF0000 在内存中的存储形式为 1111 1111 – 1111 1111 – 0000 0000 – 0000 0000)。

取反运算(~)

取反运算符为单目运算符,右结合性,作用是对参与运算的二进制位取反。例如1为0,~0为1,这和逻辑运算中的!非常类似。。

左移运算(<<)

左移运算符<<用来把操作数的各个二进制位全部左移若干位,高位丢弃,低位补0。

如果数据较小,被丢弃的高位不包含 1,那么左移 n 位相当于乘以 2 的 n 次方。

右移运算(>>)

右移运算符>>用来把操作数的各个二进制位全部右移若干位,低位丢弃,高位补 0 或 1。如果数据的最高位是 0,那么就补 0;如果最高位是 1,那么就补 1

如果被丢弃的低位不包含 1,那么右移 n 位相当于除以 2 的 n 次方(但被移除的位中经常会包含 1)。

使用位运算对数据或文件内容进行加密

数据加密解密的可以使用异或运算。
示例代码:

#include <stdio.h>
#include <stdlib.h>
int main(){
    
    
    char plaintext = 'a';  // 明文
    char secretkey = '!';  // 密钥
    char ciphertext = plaintext ^ secretkey;  // 密文
    char decodetext = ciphertext ^ secretkey;  // 解密后的字符
    char buffer[9];
    printf("            char    ASCII\n");
    // itoa()用来将数字转换为字符串,可以设定转换时的进制(基数)
    // 这里将字符对应的ascii码转换为二进制
    printf(" plaintext   %c     %7s\n", plaintext, itoa(plaintext, buffer, 2));
    printf(" secretkey   %c     %7s\n", secretkey, itoa(secretkey, buffer, 2));
    printf("ciphertext   %c     %7s\n", ciphertext, itoa(ciphertext, buffer, 2));
    printf("decodetext   %c     %7s\n", decodetext, itoa(decodetext, buffer, 2));
    return 0;
}

plaintext 与 decodetext相同,也就是说,两次异或运算后还是原来的结果。
// 注意:程序中的 itoa() 位于 stdlib.h 头文件,它并不是一个标准的C函数,只有Windows下有

通过一次异或运算,生成密文,密文没有可读性,与原文风马牛不相及,这就是加密;
密文再经过一次异或运算,就会还原成原文,这就是解密的过程;
加密和解密需要相同的密钥,如果密钥不对,是无法成功解密的。

上面的加密算法称为对称加密算法,加密和解密使用同一个密钥。

如果加密和解密的密钥不同,则称为非对称加密算法。在非对称算法中,加密的密钥称为公钥,解密的密钥称为私钥,只知道公钥是无法解密的,还必须知道私钥。

C语言重要知识点补充

C语言typedef的用法详解

使用关键字 typedef 可以为类型起一个新的别名。typedef 的用法一般为:

typedef  oldName  newName;

oldName 是类型原来的名字,newName 是类型新的名字。

例如:

typedef int INTEGER;
INTEGER a, b;
a = 1;
b = 2;

INTEGER a, b;等效于int a, b;

给数组类型定义别名的例子:

typedef char ARRAY20[20]; 

ARRAY20 a1, a2, s1, s2;
等价于: 
char a1[20], a2[20], s1[20], s2[20];

为结构体类型定义别名:

typedef struct stu{
    
    
    char name[20];
    int age;
    char sex;
} STU;

用 STU 定义结构体变量:
STU body1,body2;
等价于: 
struct stu body1, body2;

为指针类型定义别名:

typedef int (*PTR_TO_ARR)[4];

//表示 PTR_TO_ARR 是类型int * [4]的别名,它是一个二维数组指针类型。
//接着可以使用 PTR_TO_ARR 定义二维数组指针:

PTR_TO_ARR p1, p2;

可以为函数指针类型定义别名:

typedef int (*PTR_TO_FUNC)(int, int);
PTR_TO_FUNC pfunc;

需要强调的是,typedef 是赋予现有类型一个新的名字,而不是创建新的类型。为了“见名知意”,请尽量使用含义明确的标识符,并且尽量大写。

typedef 和 #define 的区别

typedef 在表现上有时候类似于 #define,但它和宏替换之间存在一个关键性的区别。正确思考这个问题的方法就是把 typedef 看成一种彻底的“封装”类型,声明之后不能再往里面增加别的东西。

可以使用其他类型说明符对宏类型名进行扩展,但对 typedef 所定义的类型名却不能这样做:

#define INTERGE int
unsigned INTERGE n;  //没问题

typedef int INTERGE;
unsigned INTERGE n;  //错误,不能在 INTERGE 前面添加 unsigned

在连续定义几个变量的时候,typedef 能够保证定义的所有变量均为同一类型,而 #define 则无法保证:

#define PTR_INT int *
PTR_INT p1, p2;

经过宏替换以后,第二行变为:

int *p1, p2;
这使得 p1、p2 成为不同的类型:p1 是指向 int 类型的指针,p2 是 int 类型。

------------------ 相反,在下面的代码中: ------------------
typedef int * PTR_INT
PTR_INT p1, p2;

p1、p2 类型相同,它们都是指向 int 类型的指针。

C语言const的用法详解,C语言常量定义详解

我们经常将 const 变量称为常量(Constant)。创建常量的格式通常为:

const type name = value;

// const 和 type 都是用来修饰变量的,它们的位置可以互换,也就是将 type 放在 const 前面:
type const name = value;

但我们通常采用第一种方式,不采用第二种方式。另外建议将常量名的首字母大写,以提醒程序员这是个常量。

由于常量一旦被创建后其值就不能再改变,所以常量必须在定义的同时赋值(初始化),后面的任何赋值行为都将引发错误:

    int n = 90;
    const int MaxNum1 = getNum();  //运行时初始化
    const int MaxNum2 = n;  //运行时初始化
    const int MaxNum3 = 80;  //编译时初始化

const 和指针

const 和指针一起使用会有几种不同的顺序,如下所示:

const int *p1; // 指针指向的数据不可修改,但是指针本身是可以修改的(也就是可以指向其他地址)
int const *p2; // 指针指向的数据不可修改,但是指针本身是可以修改的(也就是可以指向其他地址)
int * const p3; // 指针本身不可修改(指针不能指向其他地址),指针指向的数据可以修改
const int * const p4; // 指针本身不可修改(指针不能指向其他地址),指针指向的数据也不可以修改
int const * const p5; // 指针本身不可修改(指针不能指向其他地址),指针指向的数据也不可以修改

规律: const 离变量名近就是用来修饰指针变量的 (指针不可修改),离变量名远就是用来修饰指针指向的数据 (指针指向的数据不可修改),如果近的和远的都有,那么就同时修饰指针变量以及它指向的数据。

const 和函数形参

在C语言中,单独定义 const 变量没有明显的优势,完全可以使用#define命令代替。const 通常用在函数形参中,如果形参是一个指针,为了防止在函数内部修改指针指向的数据,就可以用 const 来限制。

在C语言标准库中,有很多函数的形参都被 const 限制了,下面是部分函数的原型:

size_t strlen ( const char * str );
int strcmp ( const char * str1, const char * str2 );
char * strcat ( char * destination, const char * source );
char * strcpy ( char * destination, const char * source );
int system (const char* command);
int puts ( const char * str );
int printf ( const char * format, ... );

const 和非 const 类型转换

当一个指针变量 str1 被 const 限制时,并且类似const char *str1这种形式,说明指针指向的数据不能被修改;如果将 str1 赋值给另外一个未被 const 修饰的指针变量 str2,就有可能发生危险。因为通过 str1 不能修改数据,而赋值后通过 str2 能够修改数据了,意义发生了转变,所以编译器不提倡这种行为,会给出错误或警告。

const char *和char *是不同的类型,不能将const char *类型的数据赋值给char *类型的变量。但反过来是可以的,编译器允许将char *类型的数据赋值给const char *类型的变量。

这种限制很容易理解,char *指向的数据有读取和写入权限,而const char *指向的数据只有读取权限,降低数据的权限不会带来任何问题,但提升数据的权限就有可能发生危险。

错误示例(将 const 类型赋值给非 const 类型的例子):

const char *str1 = "c.biancheng.net";
char *str2 = str1;
通过赋值、传参(传参的本质也是赋值)将 const 类型的数据交给了非 const 类型的变量,编译器不会容忍这种行为,会给出警告,甚至直接报错。

C语言随机数生成教程,C语言rand和srand用法详解

在C语言中,我们一般使用 <stdlib.h> 头文件中的 rand() 函数来生成随机数,它的用法为:

int rand (void); // void 表示不需要传递参数。

RAND_MAX 是 <stdlib.h> 头文件中的一个宏,它用来指明 rand() 所能返回的随机数的最大值。C语言标准并没有规定 RAND_MAX 的具体数值,只是规定它的值至少为 32767。在实际编程中,我们也不需要知道 RAND_MAX 的具体值,把它当做一个很大的数来对待即可。

示例代码:

int a = rand();

随机数的本质

rand() 函数产生的随机数是伪随机数,是根据一个数值按照某个公式推算出来的,这个数值我们称之为“种子”。种子在每次启动计算机时是随机的,但是一旦计算机启动以后它就不再变化了;也就是说,每次启动计算机以后,种子就是定值了,所以根据公式推算出来的结果(也就是生成的随机数)就是固定的。

重新播种

可以通过 srand() 函数来重新“播种”,这样种子就会发生改变。srand() 的用法为:

void srand (unsigned int seed);

作为参考:
它需要一个 unsigned int 类型的参数。在实际开发中,我们可以用时间作为参数,只要每次播种的时间不同,那么生成的种子就不同,最终的随机数也就不同。需要注意的是,如果代码在同一秒内运行多次,那么产生的种子一样,那么随机数产生的结果依然是一样的。

生成一定范围内的随机数

可以利用取模的方法:

int a = rand() % 10;    //产生0~9的随机数,注意10会被整除
int a = rand() % 51 + 13;    //产生13~63的随机数

C语言文件操作

C语言中的文件是什么?

操作文件的正确流程为:打开文件 --> 读写文件 --> 关闭文件

所谓打开文件,就是获取文件的有关信息,例如文件名、文件状态、当前读写位置等,这些信息会被保存到一个 FILE 类型的结构体变量中。关闭文件就是断开与文件之间的联系,释放结构体变量,同时禁止再对该文件进行操作。

文件流

我们把数据在数据源和程序(内存)之间传递的过程叫做数据流(Data Stream)。相应的,数据从数据源到程序(内存)的过程叫做输入流(Input Stream),从程序(内存)到数据源的过程叫做输出流(Output Stream)。

硬件

在操作系统中,为了统一对各种硬件的操作,简化接口,不同的硬件设备也都被看成一个文件。对这些文件的操作,等同于对磁盘上普通文件的操作。例如:

  • 通常把显示器称为标准输出文件,printf 就是向这个文件输出数据;
  • 通常把键盘称为标准输入文件,scanf 就是从这个文件读取数据。

C语言fopen/fclose()函数的用法

fopen()

fopen() 函数即可打开文件, 该函数定义在<stdio.h> 头文件中,它的用法为:

// filename为文件名(包括文件路径),mode为打开方式,它们都是字符串。
FILE *fopen(char *filename, char *mode); 

fopen() 函数的返回值

fopen() 会获取文件信息,包括文件名、文件状态、当前读写位置等,并将这些信息保存到一个 FILE 类型的结构体变量中,然后将该变量的地址返回。

FILE 是 <stdio.h> 头文件中的一个结构体,它专门用来保存文件信息。

如果希望接收 fopen() 的返回值,就需要定义一个 FILE 类型的指针。例如:

FILE *fp = fopen("demo.txt", "r");

表示以“只读”方式打开当前目录下的 demo.txt 文件,并使 fp 指向该文件,这样就可以通过 fp 来操作 demo.txt 了。fp 通常被称为文件指针。

判断文件是否打开成功

打开文件出错时,fopen() 将返回一个空指针,也就是 NULL,我们可以利用这一点来判断文件是否打开成功,请看下面的代码:

FILE *fp;
if( (fp=fopen("D:\\demo.txt","rb")) == NULL ){
    
    
    printf("Fail to open file!\n");
    exit(0);  //退出程序(结束程序)
}

fopen() 函数的打开方式

控制读写权限的字符串(必须指明)
打开方式 说明
"r" 以“只读”方式打开文件。只允许读取,不允许写入。文件必须存在,否则打开失败。
"w" 以“写入”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。
"a" 以“追加”方式打开文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。
"r+" 以“读写”方式打开文件。既可以读取也可以写入,也就是随意更新文件。文件必须存在,否则打开失败。
"w+" 以“写入/更新”方式打开文件,相当于wr+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么清空文件内容(相当于删除原文件,再创建一个新文件)。
"a+" 以“追加/更新”方式打开文件,相当于a和r+叠加的效果。既可以读取也可以写入,也就是随意更新文件。如果文件不存在,那么创建一个新文件;如果文件存在,那么将写入的数据追加到文件的末尾(文件原有的内容保留)。
控制读写方式的字符串(可以不写)
打开方式 说明
"t" 文本文件。如果不写,默认为"t"
"b" 二进制文件。

读写权限和读写方式可以组合使用,但是必须将读写方式放在读写权限的中间或者尾部(换句话说,不能将读写方式放在读写权限的开头)。例如:

  • 将读写方式放在读写权限的末尾:“rb”、“wt”、“ab”、“r+b”、“w+t”、“a+t”
  • 将读写方式放在读写权限的中间:“rb+”、“wt+”、“ab+”

关闭文件

fclose() 的用法为:

int fclose(FILE *fp);

文件正常关闭时,fclose() 的返回值为0,如果返回非零值则表示有错误发生。

文本文件和二进制文件的区别

  • 从物理上讲,二进制文件和字符文件并没有什么区别,它们都是以二进制的形式保存在磁盘上的数据。
  • 总体来说,不同类型的文件有不同的编码格式,必须使用对应的程序(软件)才能正确解析,否则就是一堆乱码,或者无法使用。

fopen() 中的文本方式和二进制方式

在C语言中:

  • 二进制方式很简单,读取文件时,会原封不动的读出文件的全部內容,写入数据时,也是把缓冲区中的內容原封不动的写到文件中。
  • 文本方式和二进制方式并没有本质上的区别,只是对于换行符的处理不同。

C语言程序将\n作为换行符
1)类 UNIX/Linux 系统在处理文本文件时也将\n作为换行符,所以程序中的数据会原封不动地写入文本文件中,反之亦然。

2) Windows 系统却不同,它将\r\n作为文本文件的换行符;在 Windows 系统中,如果以文本方式打开文件,当读取文件时,程序会将文件中所有的\r\n转换成一个字符\n。也就是说,如果文本文件中有连续的两个字符是\r\n,则程序会丢弃前面的\r,只读入\n;当写入文件时,程序会将\n转换成\r\n写入。也就是说,如果要写入的内容中有字符\n,则在写入该字符前,程序会自动先写入一个\r。

在windows上,如果用文本方式打开二进制文件进行读写,读写的内容就可能和文件的内容有出入。(这句话的理解其实是这样:二进制文件写入的时候,并不区分分隔符,导致的结果,文件中可能存在\n或者\r\n的字符,读写的时候这些字符的变换是会存在偏差的)

总体来说,对于 Windows 平台,为了保险起见,最好用"t"来打开文本文件,用"b"来打开二进制文件。对于 Linux 平台,使用"r"还是"b"都无所谓,既然默认是"r",那就什么都不写就行了。

以字符形式读写文件

对 EOF 的说明

EOF 是 end of file 的缩写,表示文件末尾,是在 stdio.h 中定义的宏,它的值是一个负数,往往是 -1(EOF 不绝对是 -1,也可以是其他负数,这要看编译器的实现)。fgetc() 的返回值类型之所以为 int,就是为了容纳这个负数(char不能是负数)。

EOF 本来表示文件末尾,意味着读取结束,但是很多函数在读取出错时也返回 EOF,那么当返回 EOF 时,到底是文件读取完毕了还是读取出错了?我们可以借助 stdio.h 中的两个函数来判断,分别是 feof() 和 ferror()。

feof() 函数用来判断文件内部指针是否指向了文件末尾

int feof ( FILE * fp );
当指向文件末尾时返回非零值,否则返回零值。

ferror() 函数用来判断文件操作是否出错

int ferror ( FILE *fp );
出错时返回非零值,否则返回零值。

fgetc

用法为:

int fgetc (FILE *fp);
fp 为文件指针。fgetc() 读取成功时返回读取到的字符,读取到文件末尾或读取失败时返回EOF

举例:

char ch;
FILE *fp = fopen("D:\\demo.txt", "r+");
ch = fgetc(fp); // 表示从D:\\demo.txt文件中读取一个字符,并保存到变量 ch 中。

在文件内部有一个位置指针,用来指向当前读写到的位置,也就是读写到第几个字节。在文件打开时,该指针总是指向文件的第一个字节。使用 fgetc() 函数后,该指针会向后移动一个字节,所以可以连续多次使用 fgetc() 读取多个字符。注意:这个文件内部的位置指针与C语言中的指针不是一回事。位置指针仅仅是一个标志,表示文件读写到的位置,也就是读写到第几个字节,它不表示地址。文件每读写一次,位置指针就会移动一次,它不需要你在程序中定义和赋值,而是由系统自动设置,对用户是隐藏的。

fputc

用法:

int fputc ( int ch, FILE *fp );
ch 为要写入的字符,fp 为文件指针。fputc() 写入成功时返回写入的字符,失败时返回 EOF,返回值类型为 int 也是为了容纳这个负数。

举例:

fputc('a', fp);
或者:
char ch = 'a';
fputc(ch, fp);
表示把字符 'a' 写入fp所指向的文件中。
  • 被写入的文件可以用写、读写、追加方式打开,用写或读写方式打开一个已存在的文件时将清除原有的文件内容,并将写入的字符放在文件开头。如需保留原有文件内容,并把写入的字符放在文件末尾,就必须以追加方式打开文件。不管以何种方式打开,被写入的文件若不存在时则创建该文件。
  • 每写入一个字符,文件内部位置指针向后移动一个字节。

以字符串的形式读写文件

fgets

用法:

char *fgets ( char *str, int n, FILE *fp );
str 为字符数组,n 为要读取的字符数目,fp 为文件指针。

返回值:读取成功时返回字符数组首地址,也即 str;读取失败时返回 NULL;
如果开始读取时文件内部指针已经指向了文件末尾,那么将读取不到任何字符,也返回 NULL

注意,读取到的字符串会在末尾自动添加 ‘\0’,n 个字符也包括 ‘\0’。也就是说,实际只读取到了 n-1 个字符,如果希望读取 100 个字符,n 的值应该为 101。

需要重点说明的是,在读取到 n-1 个字符之前如果出现了换行,或者读到了文件末尾,则读取结束。这就意味着,不管 n 的值多大,fgets() 最多只能读取一行数据,不能跨行。在C语言中,没有按行读取文件的函数,我们可以借助 fgets(),将 n 的值设置地足够大,每次就可以读取到一行数据。

fgets() 遇到换行时,会将换行符一并读取到当前字符串。该示例的输出结果之所以和 demo.txt 保持一致,该换行的地方换行,就是因为 fgets() 能够读取到换行符。而 gets() 不一样,它会忽略换行符。(这句话的理解:gets并不是对换行符无反应,它的反应就是忽略换行符,具体来说就是;fgets在读取到一行后,也会将换行符存储在数组的单元中,而gets则只会保存除了换行符之外的内容。)

fputs

用法为:

int fputs( char *str, FILE *fp );
str 为要写入的字符串,fp 为文件指针。写入成功返回非负数,失败返回 EOF

以数据块的形式读写文件

fgets() 有局限性,每次最多只能从文件中读取一行内容,因为 fgets() 遇到换行符就结束读取。如果希望读取多行内容,需要使用 fread() 函数;相应地写入函数为 fwrite()。

对于 Windows 系统,使用 fread() 和 fwrite() 时应该以二进制的形式打开文件

块数据,也就是若干个字节的数据,可以是一个字符,可以是一个字符串,可以是多行数据,并没有什么限制。

fread() & fwrite()

fread() 函数用来从指定文件中读取块数据:

size_t fread ( void *ptr, size_t size, size_t count, FILE *fp );

fwrite() 函数用来向文件中写入块数据:

size_t fwrite ( void * ptr, size_t size, size_t count, FILE *fp );

参数的说明:

  • ptr 为内存区块的指针,它可以是数组、变量、结构体等。fread() 中的 ptr 用来存放读取到的数据,fwrite() 中的 ptr 用来存放要写入的数据。
  • size:表示每个数据块的字节数。
  • count:表示要读写的数据块的块数。
  • fp:表示文件指针。
  • 理论上,每次读写 size*count 个字节的数据。
  • size_t 是在 stdio.h 和 stdlib.h 头文件中使用 typedef 定义的数据类型,表示无符号整数,也即非负数,常用来表示数量。
  • 返回值:返回成功读写的块数,也即 count。如果返回值小于 count:
    1] 对于 fwrite() 来说,肯定发生了写入错误,可以用 ferror() 函数检测。
    2] 对于 fread() 来说,可能读到了文件末尾,可能发生了错误,可以用 ferror() 或 feof() 检测。

数据写入完毕后,位置指针在文件的末尾,要想读取数据,必须将文件指针移动到文件开头,这就是rewind(fp);的作用

格式化读写文件

fscanf() 和 fprintf() 函数与前面使用的 scanf() 和 printf() 功能相似,都是格式化读写函数,两者的区别在于 fscanf() 和 fprintf() 的读写对象不是键盘和显示器,而是磁盘文件。

函数原型

int fscanf ( FILE *fp, char * format, ... );
int fprintf ( FILE *fp, char * format, ... );

fp 为文件指针,format 为格式控制字符串,... 表示参数列表
fprintf() 返回成功写入的字符的个数,失败则返回负数。
fscanf() 返回参数列表中被成功赋值的参数个数。

代码示例

	struct stu{
    
    
    char name[10];
    int num;
    int age;
    float score;
	} boya[N], boyb[N], *pa, *pb;

	FILE *fp;

    //从键盘读入数据,保存到boya
    printf("Input data:\n");
    for(i=0; i<N; i++,pa++){
    
    
        scanf("%s %d %d %f", pa->name, &pa->num, &pa->age, &pa->score);   
    }
    pa = boya;
    //将boya中的数据写入到文件
    for(i=0; i<N; i++,pa++){
    
    
        fprintf(fp,"%s %d %d %f\n", pa->name, pa->num, pa->age, pa->score);   
    }
    //重置文件指针
    rewind(fp);
    //从文件中读取数据,保存到boyb
    for(i=0; i<N; i++,pb++){
    
    
        fscanf(fp, "%s %d %d %f\n", pb->name, &pb->num, &pb->age, &pb->score);
    }
    pb=boyb;
    //将boyb中的数据输出到显示器
    for(i=0; i<N; i++,pb++){
    
    
        printf("%s  %d  %d  %f\n", pb->name, pb->num, pb->age, pb->score);
    }

番外:如果将 fp 设置为 stdin,那么 fscanf() 函数将会从键盘读取数据,与 scanf 的作用相同;设置为 stdout,那么 fprintf() 函数将会向显示器输出内容,与 printf 的作用相同。例如:

#include<stdio.h>
int main(){
    
    
    int a, b, sum;
    fprintf(stdout, "Input two numbers: ");
    fscanf(stdin, "%d %d", &a, &b);
    sum = a + b;
    fprintf(stdout, "sum=%d\n", sum);
    return 0;
}

随机读写文件

  • 实现随机读写的关键是要按要求移动位置指针,这称为文件的定位。

文件定位函数rewind和fseek

rewind() 用来将位置指针移动到文件开头:

void rewind ( FILE *fp );

fseek() 用来将位置指针移动到任意位置:

int fseek ( FILE *fp, long offset, int origin );
fp 为文件指针,也就是被移动的文件。

offset 为偏移量,也就是要移动的字节数。
之所以为 long 类型,是希望移动的范围更大,能处理的文件更大。
offset 为正时,向后移动;offset 为负时,向前移动。

origin 为起始位置,也就是从何处开始计算偏移量。
C语言规定的起始位置有三种,分别为文件开头、当前位置和文件末尾,
每个位置都用对应的常量来表示:
文件开头	  SEEK_SET	 0
当前位置	  SEEK_CUR	 1
文件末尾   SEEK_END	 2

值得说明的是,fseek() 一般用于二进制文件,在文本文件中由于要进行转换,计算的位置有时会出错。

代码示例

在移动位置指针之后,就可以用前面介绍的任何一种读写函数进行读写了。由于是二进制文件,因此常用 fread() 和 fwrite() 读写。

fwrite(boys, sizeof(struct stu), N, fp);  //写入三条学生信息
fseek(fp, sizeof(struct stu), SEEK_SET);  //移动位置指针
fread(&boy, sizeof(struct stu), 1, fp);  //读取一条学生信息
printf("%s  %d  %d %f\n", boy.name, boy.num, boy.age, boy.score);

运行结果:
Input data:
Tom 2 15 90.5↙
Hua 1 14 99↙
Zhao 10 16 95.5↙

Hua 1 14 99.000000

C语言实现文件复制功能(包括文本文件和二进制文件)

实现文件复制的主要思路是:开辟一个缓冲区,不断从原文件中读取内容到缓冲区,每读取完一次就将缓冲区中的内容写入到新建的文件,直到把原文件的内容读取完。

两个关键的问题:

  1. 开辟多大的缓冲区合适?
    缓冲区过小会造成读写次数的增加,过大也不能明显提高效率。目前大部分磁盘的扇区都是4K对齐的,如果读写的数据不是4K的整数倍,就会跨扇区读取,降低效率,所以我们开辟4K的缓冲区。

  2. 缓冲区中的数据是没有结束标志的,如果缓冲区填充不满,如何确定写入的字节数?
    最好的办法就是每次读取都能返回读取到的字节数。对于函数fread, 它返回成功读写的块数,该值小于等于 count。如果我们让参数 size 等于1,那么返回的就是读取的字节数。

/**
* 文件复制函数
* @param    fileRead    要复制的文件
* @param    fileWrite   复制后文件的保存路径
* @return   int         1: 复制成功;2: 复制失败
**/
int copyFile(char *fileRead, char *fileWrite){
    
    
    FILE *fpRead;  // 指向要复制的文件
    FILE *fpWrite;  // 指向复制后的文件
    int bufferLen = 1024*4;  // 缓冲区长度
    char *buffer = (char*)malloc(bufferLen);  // 开辟缓存
    int readCount;  // 实际读取的字节数
    if( (fpRead=fopen(fileRead, "rb")) == NULL || (fpWrite=fopen(fileWrite, "wb")) == NULL ){
    
    
        printf("Cannot open file, press any key to exit!\n");
        getch();
        exit(1);
    }
    // 不断从fileRead读取内容,放在缓冲区,再将缓冲区的内容写入fileWrite
    while( (readCount=fread(buffer, 1, bufferLen, fpRead)) > 0 ){
    
    
    // 正常情况下,每次会读取bufferLen个字节,即readCount=bufferLen;
    //如果文件大小不足bufferLen个字节,或者读取到文件末尾,实际读取到的字节就会小于bufferLen,即readCount<bufferLen。
    //所以通过fwrite()写入文件时,应该以readCount为准。
        fwrite(buffer, readCount, 1, fpWrite);
    }
    free(buffer);
    fclose(fpRead);
    fclose(fpWrite);
    return 1;
}

FILE结构体以及缓冲区深入探讨

FILE

FILE是在stdio.h中定义的一个结构体,该结构体中含有文件名、文件状态和文件当前位置等信息,fopen 返回的就是FILE类型的指针。

FILE是文件缓冲区的结构,fp也是指向文件缓冲区的指针。

不同编译器 stdio.h 头文件中对 FILE 的定义略有差异,这里以标准C举例说明:

typedef struct _iobuf {
    
    
    int cnt;  // 剩余的字符,如果是输入缓冲区,那么就表示缓冲区中还有多少个字符未被读取
    char *ptr;  // 下一个要被读取的字符的地址
    char *base;  // 缓冲区基地址
    int flag;  // 读写状态标志位
    int fd;  // 文件描述符
    // 其他成员
} FILE;

缓冲区

在这里插入图片描述
图解:上面的箭头表示的区域就相当是一个输入流,红色的地方相当于一个开关,这个开关可以控制往深绿色区域(标注的是缓冲区)里放进去的数据。

  • 键盘输入时,我们是从缓冲区中得到我们想要的数据;
  • 我们通过setbuf()或setvbuf()函数将缓冲区设置10个字节的大小,而从键盘输入了20个字节大小的数据,这样我们输入的前10个数据会放在缓冲区中,因为我们设置的缓冲区的大小只能够装下10个字节大小的数据,装不下20个字节大小的数据。那么剩下的那10个字节大小的数据将暂时放在了输入流中。

FILE & 缓冲区

以上述20个字节的输入与10字节的缓冲区为例:

FILE 结构体中几个相关成员的含义:
    cnt  // 剩余的字符,如果是输入缓冲区,那么就表示缓冲区中还有多少个字符未被读取
    ptr  // 下一个要被读取的字符的地址
    base  // 缓冲区基地址

向缓冲区中放入了10个字节大小的数据,FILE结构体中的 cnt 变为了10 ,假设缓冲区的基地址也就是 base 是0x00428e60 ,并且它是不变的 ,此时 ptr 的值也为0x00428e60 ,当从缓冲区中读取5个数据的时候,cnt 变为了5 ,ptr 则变为了0x0042e865表示下次应该从这个位置开始读取缓冲区中的数据 ,如果接下来再读取5个数据的时候,cnt 则变为了0 ,ptr 变为了0x0042869表示下次应该从这个位置开始从缓冲区中读取数据,但是此时缓冲区中已经没有任何数据了,所以要将输入流中的剩下的那10个数据放进来,这样缓冲区中又有了10个数据,此时 cnt 变为了10 ,而当缓冲区中重新放进来数据的时候这个 ptr 的值变为了0x00428e60 ,这是因为当缓冲区中没有任何数据的时候要将 ptr 这个值进行一下刷新,使其指向缓冲区的基地址也就是0x0042e860这个值!因为下次要从这个位置开始读取数据!

在这里有点需要说明:当我们从键盘输入字符串的时候需要敲一下回车键才能够将这个字符串送入到缓冲区中,那么敲入的这个回车键(\r)会被转换为一个换行符\n,这个换行符\n也会被存储在缓冲区中并且被当成一个字符来计算!比如我们在键盘上敲下了123456这个字符串,然后敲一下回车键(\r)将这个字符串送入了缓冲区中,那么此时缓冲区中的字节个数是7 ,而不是6。

缓冲区的刷新就是将指针 ptr 变为缓冲区的基地址 ,同时 cnt 的值变为0 ,因为缓冲区刷新后里面是没有数据的!

C语言获取文件大小(长度)

ftell()

ftell() 函数用来获取文件内部指针(位置指针)距离文件开头的字节数,它的原型为:

long int ftell ( FILE * fp );

注意:fp 要以二进制方式打开,如果以文本方式打开,函数的返回值可能没有意义。

获取文件大小

思路

  • 先使用 fseek() 将文件内部指针定位到文件末尾
  • 再使用 ftell() 返回内部指针距离文件开头的字节数,这个返回值就等于文件的大小。

代码

这里是引用

long fsize(FILE *fp){
    
    
    long n;
    fpos_t fpos;  //当前位置
    fgetpos(fp, &fpos);  //获取当前位置
    fseek(fp, 0, SEEK_END);
    n = ftell(fp);
    fsetpos(fp,&fpos);  //恢复之前的位置
    return n;
}

C语言插入、删除、更改文件内容

顺序文件

  • 顺序文件: txt、doc、mp4 等,文件内容是按照从头到尾的顺序依次存储在磁盘上的,就像排起一条长长的队伍,称为顺序文件。
  • 除了顺序文件,还有索引文件、散列文件等,一般用于特殊领域,例如数据库、高效文件系统等。

顺序文件的存储结构决定了它能够高效读取内容,但不能够随意插入、删除和修改内容。例如在文件开头插入100个字节的数据,那么原来文件的所有内容都要向后移动100个字节,这不仅是非常低效的操作,而且还可能覆盖其他文件。因此C语言没有提供插入、删除、修改文件内容的函数,要想实现这些功能,只能自己编写函数。

思路

插入数据

假设原来文件的大小为 1000 字节,现在要求在500字节处插入用户输入的字符串,那么可以这样来实现:

  1. 创建一个临时文件,将后面500字节的内容复制到临时文件;
  2. 将原来文件的内部指针调整到500字节处,写入字符串;
  3. 再将临时文件中的内容写入到原来的文件(假设字符串的长度为100,那么此时文件内部指针在600字节处)。

删除数据

假设原来文件大小为1000字节,名称为 demo.mp4,现在要求在500字节处往后删除100字节的数据,那么可以这样来实现:

  1. 创建一个临时文件,先将前500字节的数据复制到临时文件,再将600字节之后的所有内容复制到临时文件;
  2. 删除原来的文件,并创建一个新文件,命名为 demo.mp4;
  3. 将临时文件中的所有数据复制到 demo.mp4。

修改数据时,如果新数据和旧数据长度相同,那么设置好内部指针,直接写入即可;如果新数据比旧数据长,相当于增加新内容,思路和插入数据类似;如果新数据比旧数据短,相当于减少内容,思路和删除数据类似。实际开发中,我们往往会保持新旧数据长度一致,以减少编程的工作量,所以我们不再讨论新旧数据长度不同的情况。

文件复制函数

/**
 * 文件复制函数
 * @param  fSource       要复制的原文件
 * @param  offsetSource  原文件的位置偏移(相对文件开头),也就是从哪里开始复制
 * @param  len           要复制的内容长度,小于0表示复制offsetSource后边的所有内容
 * @param  fTarget       目标文件,也就是将文件复制到哪里
 * @param  offsetTarget  目标文件的位置偏移,也就是复制到目标文件的什么位置
 * @return  成功复制的字节数
**/
long fcopy(FILE *fSource, long offsetSource, long len, FILE *fTarget, long offsetTarget){
    
    
    int bufferLen = 1024*4;  // 缓冲区长度
    char *buffer = (char*)malloc(bufferLen);  // 开辟缓存
    int readCount;  // 每次调用fread()读取的字节数
    long nBytes = 0;  //总共复制了多少个字节
    int n = 0;  //需要调用多少次fread()函数
    int i;  //循环控制变量

    fseek(fSource, offsetSource, SEEK_SET);
    fseek(fTarget, offsetTarget, SEEK_SET);

    if(len<0){
    
      //复制所有内容
        while( (readCount=fread(buffer, 1, bufferLen, fSource)) > 0 ){
    
    
            nBytes += readCount;
            fwrite(buffer, readCount, 1, fTarget);
        }
    }else{
    
      //复制len个字节的内容
        n = (int)ceil((double)((double)len/bufferLen));
        for(i=1; i<=n; i++){
    
    
            if(len-nBytes < bufferLen){
    
     bufferLen = len-nBytes; }
            readCount = fread(buffer, 1, bufferLen, fSource);
            fwrite(buffer, readCount, 1, fTarget);
            nBytes += readCount;
        }
    }
    fflush(fTarget);
    free(buffer);
    return nBytes;
}

文件内容插入函数

/**
 * 向文件中插入内容
 * @param  fp      要插入内容的文件
 * @param  buffer  缓冲区,也就是要插入的内容
 * @param  offset  偏移量(相对文件开头),也就是从哪里开始插入
 * @param  len     要插入的内容长度
 * @return  成功插入的字节数
**/
int finsert(FILE *fp, long offset, void *buffer, int len){
    
    
    long fileSize = fsize(fp);
    FILE *fpTemp;  //临时文件

    if(offset>fileSize || offset<0 || len<0){
    
      //插入错误
        return -1;
    }

    if(offset == fileSize){
    
      //在文件末尾插入
        fseek(fp, offset, SEEK_SET);
        if(!fwrite(buffer, len, 1, fp)){
    
    
            return -1;
        }
    }

    if(offset < fileSize){
    
      //从开头或者中间位置插入
    	//tmpfile() 函数用来创建一个临时的二进制文件,可以读取和写入数据,相当于 fopen() 函数以"wb+"方式打开文件。
    	//该临时文件不会和当前已存在的任何文件重名,并且会在调用 fclose() 后或程序结束后自动删除。
        fpTemp = tmpfile();
        fcopy(fp, 0, offset, fpTemp, 0);
        fwrite(buffer, len, 1, fpTemp);
        fcopy(fp, offset, -1, fpTemp, offset+len);
        freopen(FILENAME, "wb+", fp );
        fcopy(fpTemp, 0, -1, fp, 0);
        fclose(fpTemp);
    }
   
    return 0;
}

文件内容删除函数

int fdelete(FILE *fp, long offset, int len){
    
    
    long fileSize = getFileSize(fp);
    FILE *fpTemp;
    if(offset>fileSize || offset<0 || len<0){
    
      //错误
        return -1;
    }
    fpTemp = tmpfile();
    fcopy(fp, 0, offset, fpTemp, 0);  //将前offset字节的数据复制到临时文件
    fcopy(fp, offset+len, -1, fpTemp, offset);  //将offset+len之后的所有内容都复制到临时文件
    freopen(FILENAME, "wb+", fp );  //重新打开文件
    fcopy(fpTemp, 0, -1, fp, 0);
    fclose(fpTemp);
    return 0;
}

C语言调试教程

调试器(Debugger)

  1. Remote Debugger
    Remote Debugger 是 VC/VS 自带的调试器,与整个IDE无缝衔接,使用非常方便,初学者建议使用该调试器,本教程也以 VS2010 为例讲解调试技巧。
  2. WinDbg
    大名鼎鼎的 Windows 下的调试器,它的功能甚至超越了 Remote Debugger,它还有一个命令行版本(cdb.exe),但是这个命令行版本的调试器指令比较复杂,不建议初学者使用。
  3. LLDB
    XCode 自带的调试器,Mac OS X 下开发必备调试器。
  4. GDB
    Linux 下使用最多的一款调试器,也有 Windows 的移植版,如果你不使用 VC/VS,GDB 将是一个不错的选择。

还有我本人用的 Code Debugger:

assert

assert 函数的用法很简单,我们只要传入一个表达式即可,
它会计算我们传入的表达式的结果,如果为真,则不会有任何操作,
但是如果我们传入的表达式的计算结果为假,
它就会像  stderr (标准错误输出)打印一条错误信息,
然后调用 abort 函数直接终止程序的运行。

一个健壮的程序,都会有30%~50%的错误处理代码,几乎用不上 assert 断言函数,我们应该将 assert 用到那些极少发生的问题下,比如Object* pObject = new
Object,返回空指针,这一般都是指针内存分配出错导致的,不是我们可以控制的。这时即使你使用了容错语句,后面的代码也不一定能够正常运行,所以我们也就只能停止运行报错了。

THE END!!!

猜你喜欢

转载自blog.csdn.net/cjh_android/article/details/115605642