赞
踩
大家好,我是light。最近闲来无事,想写一些关于c语言的代码,希望对想要学习c语言的读者有帮助。
程序语言发展是由二进制代码语言(机器语言)开始发展,该语言不用翻译,可以用机器直接运行。再到后来的汇编语言,也就是简单的将一些常用的二进制代码片段用一些简单的英文字母所代替,再按照一定规则书写,
因此,汇编语言要比机器语言更便于阅读和理解。由于汇编语言依赖于硬件体系,并且助记符号数量比较多,所以其运用起来仍然不够方便。高级语言便应运而生。高级语言,其语法形式类似于英文,并且因为不需要对硬件进行直接操作,因此易于被普通人所理解与使用。
近年比较流行的高级语言包括C、C++、C#、VC、VB、Python、Java等。而在这些语言中,c语言更偏向于底层语言,对于初学者而言,学习c语言更有助于理解其他语言。很多芯片开发,软件编写都离不开c语言,对于电、通、气、自等专业的学生而言,c语言是必不可少的工具之一。
C语言是一种面向过程的语言,同时具有高级语言和汇编语言的优点。C语言可以广泛应用于不同的操作系统,如UNIX、MS-DOS、Microsoft Windows及Linux等。
c语言具有较高的运行效率。它继承了低级语言的优点,代码运行效率高,并具有良好的可读性和编写性。通常的,使用c语言开发出来的程序运行效率可达到汇编程序的80%~90%。
C语言的丰富的语句与关键字使得其代码不拘一格,灵活多变。
同时c语言支持自定义结构,这极大方便了程序表达一些复杂多变的数据类型。更有甚者可利用该特点,在自定义数据结构类型中加入函数指针,就可以实现c语言面向对象编程。
不仅如此,c语言还在拥有高运行效率的基础上,对语言语法进行优化,更贴近于人类思维。可以这么说,c语言是一 将高效与可读性进行有机结合、介于高级语言与底层语言之间、适用于底层软件开发与芯片开发的 语言。
C语言也有一些缺点。C语言使用指针,而涉及指针的编程错误往往难以察觉。导致代码bug难以察觉,必须有长时间的练习才能写出高质量代码。
C语言部分运算符组合之后晦涩难懂。C语言缺点有很多,但不是我们讨论的重点,在此就不多费口舌了。
对于已经学过一段时间的读者来说,选择一款自己最熟悉的IDE开发环境是最好的,对此我推荐visual studio与visual studio code等类似的具有强大功能的开发环境,这些开发环境内自带的代码补全、自动纠错、代码优化等功能能令我们软件开发变得事半功倍。
而对于初学者而言,我更推荐一些基础的开发IDE环境,例如DEV、VC6.0、或者一些简单的具有代码自动换行的c语言在线编译器即可。由或者可以用比较高级的代码编译器,将其代码自动补全功能关闭即可。因为对于初学者而言,自己手动敲全代码是很重要的。这样可以适应绝大多数开发环境,防止开发环境一换,对于c语言的关键字例如include都无法敲全。
小编之前学c语言时使用的是vc++6.0,这个软件网络上有很多资源,这里给大家提供一个。
链接:https://pan.baidu.com/s/1PApsWKEDMzvqM9NmSuw6hA
提取码:zukk
或者也可以用菜鸟工具中的在线编译器
https://c.runoob.com/compile/11/
VS下载地址官网
https://visualstudio.microsoft.com/zh-hans/
本文章前半部分采用线上编译器,后半部分涉及到程序调试采用visual studio 2019。
这部分内容出于全面性考虑,各个部分都有一些介绍,但由于不够系统全面,可能有些难以理解。但是这对于我们学习是没有影响的,这部分内容更多偏向于建立一个系统概念与整体框架,内容了解即可。
当我们有一个舒服的开发环境就可以尝试编写自己的第一个程序了。
#include <stdio.h>
int main()
{
printf("Hello, World! \n");
return 0;
}
将其复制或者手动键入菜鸟工具在线编译器后,点击运行即可
下面简单介绍一下c语言的书写格式。
在文章开头有#include <stdio.h>,这叫做头文件包含,stdio.h是标准(std)输入(in)输出(out)头文件,里面包含了一些常用函数封装,只有包含对应的头文件,我们的程序才能正常编译。
int main()是程序运行的入口,不管我们程序写的多么复杂,c语言的程序都是从main函数开始运行。
{}内是函数体,也就是函数的具体内容。在这个简单程序中主函数main内一共有两条语句,一个是printf(“Hello world!\n”); 这句话的意思是让屏幕输出一行字Hello world!,而\n表示换行。另一句话return 0;表示这个函数运行完成后给系统一个0的反馈。一般情况下,我们可以通过返回值判断该函数运行成功与否。
通过这个简单程序我们可以大概了解一个程序如何编写,以及程序基本框架是什么样子。初学者可能编译失败,其中几个比较容易出问题的点是:
我们可以随意修改双引号之间的内容,则屏幕上输出的内容也会随之而变。例如
#include <stdio.h>
int main()
{
//这是之前语句
printf("Hello, World! \n");
/*下面两条是新添加的语句*/
printf("我想换个东西输出,");
printf("看一下效果\n");
return 0;
}
一般我们会在难懂的语句后面添加代码注释,方便理解,注释格式有两种
1、以//开始,后面整行都会被注释。但是该方法只能注释一行或半行
2、以/* 开始,以 */结尾,中间文字会被注释。该方法灵活多变,但是不能嵌套。例如下面代码是错误的,嵌套注释1的开始会自动与嵌套注释2的结尾匹配。
#include <stdio.h>
int main()
{
//
return 0;
/*嵌套注释1
/*嵌套注释2*/
*/
编写规范的代码格式和添加详细的注释,是一个优秀程序员应该具备的好习惯。
到此为止,我们就学会了c语言的基本编写格式与书写规范了,有兴趣的同学可以尝试用刚学的知识在屏幕上输出一句格言:
不作什么决定的意志不是现实的意志;无性格的人从来不做出决定。
——黑格尔
参考例程
//第一个c语言程序参考例程1
#include <stdio.h>
int main()
{
printf("不作什么决定的意志不是现实的意志;无性格的人从来不做出决定。\n");
printf("——黑格尔\n");
return 0;
}
本小结仅需了解即可。
我们数学所学习的都是十进制数,在c语言无需添加任何符号的数字即表示十进制数,例如0、5、10;除了这个我们所熟悉的数以外,c语言常用进制还有2进制(寄存器)、8进制(较少用)、16进制(地址)。
他们之间可以互相转换,转换表如下
二进制 | 八进制 | 十进制 | 十六进制 | 二进制 | 八进制 | 十进制 | 十六进制 |
---|---|---|---|---|---|---|---|
0B0000 | 00 | 0 | 0x00 | 0B1000 | 010 | 8 | 0x08 |
0B0001 | 01 | 1 | 0x01 | 0B1001 | 011 | 9 | 0x09 |
0B0010 | 02 | 2 | 0x02 | 0B1010 | 012 | 10 | 0x0a |
0B0011 | 03 | 3 | 0x03 | 0B1011 | 013 | 11 | 0x0b |
0B0100 | 04 | 4 | 0x04 | 0B1100 | 014 | 12 | 0x0c |
0B0101 | 05 | 5 | 0x05 | 0B1101 | 015 | 13 | 0x0d |
0B0110 | 06 | 6 | 0x06 | 0B1110 | 016 | 14 | 0x0e |
0B0111 | 07 | 7 | 0x07 | 0B1111 | 017 | 15 | 0x0f |
看上面转换表可能有小伙伴有疑问,为什么有0B和0x前缀,0b或0B是二进制数字前缀,如果加上0b则表示该数据是一个二进制数,当我们看到0B10时要明白这是二而不是十。同理0x或0X是十六进制数前缀,可能有人不理解为什么要写0x08而不是0x8,其实他们是一样的,只是我习惯于写一个字节数据。而单独以0为前缀的默认是八进制。
在c语言中,我们可以直接写
int a=0b10;(a的值为二)
int a=010;(a的值为八)
int a=10;(a的值为十)
int a=0x10;(a的值为十六)
#include <stdio.h>
int main()
{
int a=0b10;
int b=010;
int c=10;
int d=0x10;
printf("%d %d %d %d\n",a,b,c,d); //以十进制输出4个变量,后续介绍
return 0;
}
我们首先要建立一个概念就是,计算机内部存储只有0或1,也就是所谓的二进制,更底层一点,在计算机中,只有高低电压变化,我们定义高电压为1,低电压为0,这样我们只要设计出可以存储高低电平的电路就可以存储数据了。
基于这个概念,我们可以简单的将计算机内存看做一堆0或1的数据,一个可以存储01数据的单位我们称为bit(比特),8个bit作为一个Byte(字节),1024(210)个Byte作为1KB,1024KB作为1MB,1024MB作为1GB,1024GB作为1TB……32位电脑就是指电脑最小存储单元是32bit,64位电脑最小存储单元则是64bit。以32位电脑为例,int类型变量占据存储空间有32bit,在计算机中表示数字0和数字6
0000 0000 0000 0000 0000 0000 0000 0000(0)
0000 0000 0000 0000 0000 0000 0000 0110(6)
所以理论上来说,不考虑负数,32bit的无符号unsigned int可以表示的数字范围为0到232-1。
而实际存储时,一般将最高位作为符号位,0表示正,1表示负。32bit带符号数int类型可表示数字范围是-231到2^31-1
0000 0000 0000 0000 0000 0000 0000 0001(1)
1000 0000 0000 0000 0000 0000 0000 0001(-1)
这就是最原始的原码,但是有一个问题,计算机不能直接对数字加减,(-1+1=0)
+1000 0000 0000 0000 0000 0000 0000 0001(-1)
+0000 0000 0000 0000 0000 0000 0000 0001(1)
=1000 0000 0000 0000 0000 0000 0000 0010(-2)
很显然不是我们希望的结果,所以不能用原码直接计算,对此我们计算机采用补码方式存储。
正数的原码反码补码都是同样的。对于负数,我们会将除了最高位符号位外的其他所有数字取反,这就是负数的反码。负数反码加1就是负数的补码。
1000 0000 0000 0000 0000 0000 0000 0001(-1原码)
1111 1111 1111 1111 1111 1111 1111 1110(-1反码)
1111 1111 1111 1111 1111 1111 1111 1111(-1补码)
0000 0000 0000 0000 0000 0000 0000 0001(1原码)
0000 0000 0000 0000 0000 0000 0000 0001(1反码)
0000 0000 0000 0000 0000 0000 0000 0001(1补码)
在计算机中,数据都是以补码形式保存,我们重新计算(-1+1)
+1111 1111 1111 1111 1111 1111 1111 1111(-1补码)
+0000 0000 0000 0000 0000 0000 0000 0001(1补码)
=0000 0000 0000 0000 0000 0000 0000 0000(0补码)
这样算出来的数据就是我们所期望的。
本章节介绍数据类型包括int、short、long、unsigned、char、float、double、_Bool
常用运算符=,sizeof()
常用输入输出函数printf(),scanf()
常用修饰符const,extern,static,volatile
在程序中,所有确切的数字例如5、1都是常量。除了数字常量外,还有字符常量和字符串常量。
字符常量是用单引号’选中的字符,单引号内只能有一个字符。例如’a’、‘b’、‘A’、‘1’、‘.‘等都是字符常量
字符串常量是用双引号选中的字符,双引号内可以有0到多个字符,例如"asdf\n",“hello world!”,双引号内字符数量总是比我们所看到的字符数量多一个,因为它会在字符串常量结尾自动添加一个’\0’休止符。例如:""内有一个字符’\0’,"asdf"有5个字符’a’,‘s’,‘d’,‘f’,‘\0’。
NULL是空指针常量类型,后续介绍。
在C语言中,变量与通常数学意义的自变量不同,C语言变量指的是具有一定内存的,可以修改值的一个存储单元。每当我们要使用变量保存数据时,我们都需要定义该变量,也就是在电脑中为该数据分配一个空间用于存储。
#include <stdio.h>
int main()
{
int a; //定义一个变量a,a的值处于非法状态
int b=1; //定义一个变量b,b的值为1
return 0; //退出主函数
}
在该程序中,我们定义了两个整形变量。int表示变量类型–整形,a和b为变量名称。在变量定义时候我们可以选择是否对变量进行初始化。当时需要注意的是,如果我们没有对变量初始化时,变量就处于非法状态,如果这时候我们使用了该变量,可能会导致不可预知的错误,具体错误由编译器决定。并且这种错误部分编译器不会报错,可能导致程序出现莫名其妙的bug,所用我们使用变量时最好先定义并初始化。
变量可以一次定义一个,也可以一次定义多个同类型变量,并且可以分别对变量进行初始化。例如:
#include <stdio.h>
int main()
{
int a,b=1,c; //变量定义
printf("a=%d,b=%d,c=%d\n",a,b,c); //输出变量
return 0;
}
我们之前使用过printf("Hello World!\n");
其中’\n’是转义字符,他的意思是换行,而这里出现的printf("a=%d,b=%d,c=%d\n",a,b,c);
中’%d’是以十进制输出一个变量,具体变量内容在双引号后按顺序排列,用’,'隔开。
变量命名规则
我们之前使用的一直都是int类型变量,实际上C语言主要使用的变量类型有整型变量、浮点型变量、字符型变量、布尔类型及自定义变量(结构体变量)这几种类型,衍生类型右指针类型、数组类型、字符串类型。
整型变量又称实型变量,不同电脑或机器对变量内存大小定义不同。对于32位机器,int整型变量所占内存为4字节(32bit);对于16位机器,int类型所占内存空间为2字节(16bit);对于64位机器,int类型所占空间为8字节(64bit);本文章默认以32位机器为准。
带符号整型有int、short、long和c11新添加的long long类型,不带符号整型有unsigned int、unsigned short、unsigned long、unsigned long long类型。具体见表格
类型名称 | 内存大小 | 理论范围 |
---|---|---|
short | 2字节 | -215到215-1 |
unsigned short | 2字节 | 0到216-1 |
int | 4字节 | -231到231-1 |
unsigned int或unsigned | 4字节 | 0到232-1 |
long | 8字节 | -263到263-1 |
unsigned long | 8字节 | 0到264-1 |
long long | 16字节 | -263到263-1 |
unsigned long long | 8字节 | 0到264-1 |
unsigned long long类型是目前C语言中精度最高的数据类型,可以用来表示20以内的阶乘数据,20以外的自测。并且unsigned long long的占用8字节64位,double或者long double 虽然也占有8个字节,但是他们的实际精度只有53位。
通过上述表格不难发现,对于整型变量,只要在类型前添加unsigned前缀就可以将该变量类型定义为无符号整型变量。我们可以用sizeof()运算符获取他们所占用地址的大小。例如:
#include <stdio.h> int main() { short a; unsigned short b; int c; unsigned d; long e; unsigned long f; long long g; unsigned long long h; printf("short内存大小%d\n\ unsigned short内存大小%d\n\ int内存大小%d\n\ unsigned int内存大小%d\n\ long内存大小%d\n\ unsigned long内存大小%d\n\ long long内存大小%d\n\ unsigned long long内存大小%d\n",\ sizeof(a),sizeof(b),sizeof(c),\ sizeof(d),sizeof(e),sizeof(f),\ sizeof(g),sizeof(h)); return 0; }
这里有两个新知识点,一个是sizeof运算符,它用于计算变量所占据的内存空间;另一个则是当一行语句过长时,在当前行后面添加’‘续行符即可将下一行也当做本行。对比程序可以看到不同类型所占内存大小不同。
后续我们会了解更多关于printf函数的用法,现在我们只需记住’\n’是换行,‘%d’是以十进制输出变量,’%u’是以无符号十进制输出变量即可。
#include <stdio.h>
int main() //原码 :1000 0000 0000 0000 0000 0000 0000 0001
{ //反码 :1111 1111 1111 1111 1111 1111 1111 1110
int a=-1; //计算机中存储的补码 :1111 1111 1111 1111 1111 1111 1111 1111
//分别用带符号数和无符号数输出
printf("带符号数:%d\n",a);
printf("无符号数:%u\n",a);
return 0;
}
之所以会将-1输出成4294967295是因为系统将-1的补码翻译成无符号正数,刚好是232-1。
整型变量无法做带小数点的除法运算,他的结果总是除法运算的向下取整,除非我们就是需要这个效果,负责涉及除法的运算应该使用下面即将介绍的浮点型。例如:
#include <stdio.h>
int main(void)
{
int a=5,b=2;
int c=a/b;
printf("%d/%d=%d\n",a,b,c);
return 0;
}
浮点型变量可以简单理解成我们通常所说的小数。整型变量只能保存整型值,如果强行赋值一个小数,系统会自动向下取整。这是因为系统存储小数时候,会自动将实数拆成整数与小数两部分,当遇到类似于需要将小数转为整数的情况,系统会只看整数部分。
#include <stdio.h>
int main()
{
int a;
a=0.618;
printf("%d\n",a);
}
如果我们想要保存一个小数,那么我们就需要使用浮点型变量。浮点型变量常用的有float、double两种,两者区别在于内存大小与精度不一样
类型名称 | 占用内存 | 精度 |
---|---|---|
float | 4字节 | 6-7位小数点 |
double | 8字节 | 15-16位小数点 |
他们使用方法与整型变量类似。printf支持输出小数,其符号略有区别,float类型用%f,double类型用%lf(可以理解为long float)。
#include <stdio.h>
int main()
{
float a=3.1415926535897932384626;
double b=3.1415926535897932384626;
printf("单精度a=%f,双精度b=%lf\n",a,b);
return 0;
}
默认%f和%lf输出6位小数,我们可以在%后面添加’.'和一个数字用以控制输出小数位数。
#include <stdio.h>
int main()
{
float a=3.1415926535897932384626;
double b=3.1415926535897932384626;
printf("单精度a=%.16f,双精度b=%.16lf\n",a,b); //保留16位小数
return 0;
}
可以看到float类型数据从第8位小数开始,数据已经无效了。
字符型变量其实与整型变量及其相似。字符型变量只有一种类型char,衍生类型有unsigned char
类型名称 | 内存大小 | 数据范围 |
---|---|---|
char | 1字节(8bit) | -128到127 |
unsigned char | 1字节(8bit) | 0到255 |
虽然字符变量本质是一个整型数,但是大多数时候我们都把这些数赋予一个特殊含义,在国际上通用标准是ASCII码表。
ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 |
---|---|---|---|---|---|---|---|
0 | NUT | 1 | SOH | 2 | STX | 3 | ETX |
4 | EOT | 5 | ENQ | 6 | ACK | 7 | BEL |
8 | BS | 9 | HT | 10 | LF | 11 | VT |
12 | FF | 13 | CR | 14 | SO | 15 | SI |
16 | DLE | 17 | DC1 | 18 | DC2 | 19 | DC3 |
20 | DC4 | 21 | NAK | 22 | SYN | 23 | TB |
24 | CAN | 25 | EM | 26 | SUB | 27 | ESC |
28 | FS | 29 | GS | 30 | RS | 31 | US |
32 | (space) | 33 | ! | 34 | " | 35 | # |
36 | $ | 37 | % | 38 | & | 39 | ’ |
40 | ( | 41 | ) | 42 | * | 43 | + |
44 | , | 45 | - | 46 | . | 47 | / |
48 | 0 | 49 | 1 | 50 | 2 | 51 | 3 |
52 | 4 | 53 | 5 | 54 | 6 | 55 | 7 |
56 | 8 | 57 | 9 | 58 | : | 59 | ; |
60 | < | 61 | = | 62 | > | 63 | ? |
64 | @ | 65 | A | 66 | B | 67 | C |
68 | D | 69 | E | 70 | F | 71 | G |
72 | H | 73 | I | 74 | J | 75 | K |
76 | L | 77 | M | 78 | N | 79 | O |
80 | P | 81 | Q | 82 | R | 83 | S |
84 | T | 85 | U | 86 | V | 87 | W |
88 | X | 89 | Y | 90 | Z | 91 | [ |
92 | \ | 93 | ] | 94 | ^ | 95 | _ |
96 | ` | 97 | a | 98 | b | 99 | c |
100 | d | 101 | e | 102 | f | 103 | g |
104 | h | 105 | i | 106 | j | 107 | k |
108 | l | 109 | m | 110 | n | 111 | o |
112 | p | 113 | q | 114 | r | 115 | s |
116 | t | 117 | u | 118 | v | 119 | w |
120 | x | 121 | y | 122 | z | 123 | { |
124 | 丨 | 125 | } | 126 | ~ | 127 | DEL |
而printf也可以输出字符,符号为%c
#include <stdio.h>
int main()
{
char a=97;
char b='b';
printf("%d,%d\n",a,b);//十进制输出
printf("%f,%f\n",a,b);//浮点数输出
printf("%c,%c\n",a,b);//字符型输出
return 0;
}
可以看到,当我们用十进制’%d’输出时候,可以将char类型当做普通数字变量使用,当我们使用’%c’输出时候,可以正常输出ascii码所对应的字符;但是当我们试图用浮点型格式符’ %lf’ 输出字符或整型变量时候,却发生错误。这是因为浮点数与整型数存储规则不同导致的。我们写代码时候应该尽量避免变量类型与输出格式符号不匹配。
在c99和c11的c语言标准中都添加了_Bool类型,布尔类型实际就是一个只有0或1的变量,也就是1bit。0表示假,1表示真。当我们将_Bool类型与其他类型混用时,采用的规则是0表示假,非0数表示真。
#include <stdio.h>
int main()
{
_Bool a=97;
printf("%d\n",a);
return 0;
}
可以看到当我们试图将97赋值给_Bool类型时候,系统判定97是真,于是赋值真(1)给_Bool类型变量a,当以十进制数输出a的时候,a的值为1。
自定义变量类型有两大类,一种是结构体struct,另一种是共用体union。这里我们简单介绍一下结构体,后续会有更详细的介绍。
结构体其中一种使用格式为
typedef struct
{
数据类型 数据1;
数据类型 数据2;
...
}结构体名称;
并且结构体类型定义不同于结构体变量定义,一般会将结构体类型定义放在程序最开始,而将结构体变量定义放在main函数内部。定义了结构体类型之后,我们可以定义结构体变量,使用方法类似于int类型。
我们无法直接对结构体变量进行赋值操作,想要访问结构体内的数据时,我们采用’.'运算符。结构体类型定义有几种不同方式,后续我们会详细讨论。
使用示例
#include <stdio.h> //结构体类型定义 typedef struct { int a; double b; }INT_DOUBLE; int main() { INT_DOUBLE x; //结构体变量定义 x.a=10; //结构体变量的使用 x.b=3.1415; printf("结构体使用示例\n"); printf("x内a=%d,x内b=%lf\n",x.a,x.b); return 0; }
因为结构体与共用体对于现阶段而言较为复杂,考虑文章结构问题,将其放在后续章节统一介绍。
指针概念简单,但是由于指针赋予编程者过大的权限,导致指针程序如果编写不当会有严重后果,所以本小节只介绍指针类型,在后续有专门指针章节讨论该问题。
在C语言中我们可以简单且抽象的将数据理解成线性存储空间,每个地址对于1字节内存,如下表:
名称 | 地址 | 内容 |
---|---|---|
0x 0000 0000 | 未知 | |
0x 0000 0001 | 未知 | |
… | ||
0x 0000 000f | 未知 | |
0x 0000 0010 | 未知 | |
0x 0000 0011 | 未知 | |
… | ||
0x f f f f f f f f | 未知 |
当我们使用int a;定义一个变量后,系统会自动分配一个内存地址给他,在之后任何需要a的地方,cpu都会从该地址读取或写入数据。例如:
#include <stdio.h>
int main()
{
int a=0x12345678;
printf("a的地址为%p",&a);
return 0;
}
这里有两个新知识点,%p是以十六进制显示一个地址。'&'取址符是获取变量地址。上述例程作用是定义一个变量,输出该变量的地址。
0x7ffdb0729dbc则是该变量地址,由于这是菜鸟工具在线编译器,其地址位数依赖于在线服务器的系统。
同样的程序在visual studio 2019(vs2019)运行结果为:
这就是64位机电脑运行结果,0x 0000 0035 525F FCA4转为二进制为:
0B 0000 0000 0000 0000 0000 0000 0011 0101 0101 0010 0101 1111 1111 1100 1010 0100
可以看到变量地址是64位的。
名称 | 地址 | 内容 |
---|---|---|
0x 0000 0000 0000 0000 | 未知 | |
… | ||
0x 0000 0035 525F FCA3 | 未知 | |
a | 0x 0000 0035 525F FCA4 | 0x78 |
0x 0000 0035 525F FCA5 | 0x56 | |
0x 0000 0035 525F FCA6 | 0x34 | |
0x 0000 0035 525F FCA7 | 0x12 | |
0x 0000 0035 525F FCA8 | 未知 | |
… | ||
0x FFFF FFFF FFFF FFFF | 未知 |
可以看到,系统为int a;
变量分配从0x0019ff2c开始的4个字节。并且低八位在前,高八位在后。
而指针就是专门针对变量地址进行操作。指针变量定义:
int *pint=NULL; //定义int类型指针,指针内容为空
char *pchar=NULL; //定义char类型指针,指针内容为空
float a=0,*pfloat1; //定义float类型变量a=0,float类型变量指针内容无意义
float *pfloat2=&a; //定义float类型指针,内容为a的地址
pfloat1=&a; //指针赋值操作,同类型取址符
pfloat2=pfloat1; //指针赋值操作,同类型直接赋值
不同类型指针只需在类型后添加*即可,可以一条语句定义多个相同类型指针变量与相同类型普通变量,并且可以分别初始化。指针可以选择不初始化(野指针)、初始化为NULL(空指针)、或者初始为某个相同类型变量的地址,用取址符&对变量取地址。例如
int a=0;
int p1;
int p2=NULL;
int p3=&a;
指针使用有一下几种操作,假设有int a=5;int *p=&a;
:
名称 | 运算符 | 描述 | 示例 |
---|---|---|---|
赋值 | = | 将p指向某一地址 | p=NULL将地址清空 |
地址自增 | ++ | p所指向的地址向后移h字节,h为该地址变量的类型的大小 | p++ 地址向后移4字节 |
地址自减 | – | p所指向的地址向前移h字节,h为该地址变量的类型的大小 | p-- 地址向前移4字节 |
地址加 | + | p所指向的地址向后移x*h字节,x为操作数,h为该地址变量类型的大小 | p=p+5 地址向后移5*4个字节 |
地址减 | - | p所指向的地址向前移x*h字节,x为操作数,h为该地址变量类型的大小 | p=p-5 地址向前移5*4个字节 |
解引用 | * | 获取p所指向的地址内容。 | *p=6; a的值被修改为6 |
指针本质就是一个int数,数据内容则是某个变量得地址,下面例程与表格将描述指针的原理。
#include <stdio.h> int main() { int a=5; int *p=NULL; printf("a的地址0x%p\n",&a); printf("p的地址0x%p\n",&p); printf("a的内容%d\n",a); printf("p的内容0x%p\n",p); //printf("p指向的地址的内容%d\n",*p); //此时p=NULL,解引用后会导致程序崩溃 printf("\n"); p=&a; printf("a的地址0x%p\n",&a); printf("p的地址0x%p\n",&p); printf("a的内容%d\n",a); printf("p的内容0x%p\n",p); printf("p指向的地址的内容%d\n",*p); printf("\n"); *p=10; printf("a的地址0x%p\n",&a); printf("p的地址0x%p\n",&p); printf("a的内容%d\n",a); printf("p的内容0x%p\n",p); printf("p指向的地址的内容%d\n",*p); return 0; }
VS2019运行结果:
执行
int a=5;
int *p=NULL;
后,数据如下表
名称 | 地址 | 内容 |
---|---|---|
… | ||
a的第1个字节 | 0x 0000 007C D583 FAC4 | 0x05 |
a的第2个字节 | 0x 0000 007C D583 FAC5 | 0x00 |
a的第3个字节 | 0x 0000 007C D583 FAC6 | 0x00 |
a的第4个字节 | 0x 0000 007C D583 FAC7 | 0x00 |
上述字节连起来为0x0000 0005 | ||
p的第1个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第2个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第3个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第4个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第5个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第6个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第7个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第8个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
上述字节连起来为0x0000 0000 0000 0000 |
执行p=&a;
后,数据如下表
名称 | 地址 | 内容 |
---|---|---|
… | ||
a的第1个字节 | 0x 0000 007C D583 FAC4 | 0x05 |
a的第2个字节 | 0x 0000 007C D583 FAC5 | 0x00 |
a的第3个字节 | 0x 0000 007C D583 FAC6 | 0x00 |
a的第4个字节 | 0x 0000 007C D583 FAC7 | 0x00 |
上述字节连起来为0x0000 0005 | ||
p的第1个字节 | 0x 0000 007C D583 FAE8 | 0xC4 |
p的第2个字节 | 0x 0000 007C D583 FAE8 | 0xFA |
p的第3个字节 | 0x 0000 007C D583 FAE8 | 0x83 |
p的第4个字节 | 0x 0000 007C D583 FAE8 | 0xD5 |
p的第5个字节 | 0x 0000 007C D583 FAE8 | 0x7C |
p的第6个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第7个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第8个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
上述字节连起来为0x0000 007C D583 FAC4 |
执行*p=10;
后,数据如下表
名称 | 地址 | 内容 |
---|---|---|
… | ||
a的第1个字节 | 0x 0000 007C D583 FAC4 | 0x0A |
a的第2个字节 | 0x 0000 007C D583 FAC5 | 0x00 |
a的第3个字节 | 0x 0000 007C D583 FAC6 | 0x00 |
a的第4个字节 | 0x 0000 007C D583 FAC7 | 0x00 |
上述字节连起来为0x0000 0005 | ||
p的第1个字节 | 0x 0000 007C D583 FAE8 | 0xC4 |
p的第2个字节 | 0x 0000 007C D583 FAE8 | 0xFA |
p的第3个字节 | 0x 0000 007C D583 FAE8 | 0x83 |
p的第4个字节 | 0x 0000 007C D583 FAE8 | 0xD5 |
p的第5个字节 | 0x 0000 007C D583 FAE8 | 0x7C |
p的第6个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第7个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
p的第8个字节 | 0x 0000 007C D583 FAE8 | 0x00 |
上述字节连起来为0x0000 007C D583 FAC4 |
所以说,地址变量p就是一个int数保存了另一个变量的地址。&p是地址变量自己的地址,例程中是0x0000 007C D583 FAE8;p是保存的同类型变量的地址,本例程中是0x 0000 007C D583 FAC4;*p是解引用,即访问该地址的所保存的数据,本例程中是0x 0000 000A;
同理,其他类型变量也是类似,仅仅是变量类型改变。例如:
#include <stdio.h> int main() { char a = 'X'; char* p = NULL; printf("a的地址0x%p\n", &a); printf("p的地址0x%p\n", &p); printf("a的内容%c\n", a); printf("p的内容0x%p\n", p); //printf("p指向的地址的内容%c\n",*p); //此时p=NULL,解引用后会导致程序崩溃 printf("\n"); p = &a; printf("a的地址0x%p\n", &a); printf("p的地址0x%p\n", &p); printf("a的内容%c\n", a); printf("p的内容0x%p\n", p); printf("p指向的地址的内容%c\n", *p); printf("\n"); *p = 'B'; printf("a的地址0x%p\n", &a); printf("p的地址0x%p\n", &p); printf("a的内容%c\n", a); printf("p的内容0x%p\n", p); printf("p指向的地址的内容%c\n", *p); return 0; }
既然指针也是变量,那么便可以有另一个指针变量指向该变量,称为二维指针。
int a=5; //a的内容是5
int *p=&a; //p的内容是a的地址
int **pp=&p; //pp的内容是p的地址
a=6; //修改a
p=&a; //修改p
*p=7; //修改a
pp=pp+1; //修改pp
*pp=&a; //修改p
**pp=8; //修改a
当读者看到这里是,基本已经了解C语言类型与变量了,那我们就不得不考虑一下,如果我们想要定义10个变量怎么办,100个呢,1000个呢。于是便有了数组的概念。
可以通过定义数组来解决这个问题。严格来说数组并不是一种数据类型,而是带有一定存储空间的地址。当我们想要一次定义100个int类型的数时,可以
int a[100];
int a[100]={0}
int a[100]={1,2,3,4,5,6,7,8,9,10};
如果不对数组进行初始化,则数组内的数据处于无效状态。数组定义时可以选择不初始化或列表初始化。={0}会将该数组中所有元素全部赋值为0;
={1}会将第一个元素赋值为1,其他元素全部赋值为0;={1,2,3,4,5,6,7,8,9,10};则是将数组中前10个元素对应赋值,后续数据默认赋值为0;
单独一个名称a表示数组首地址,不需要再对a使用取址符&。
当我们想要单独使用数组中某一元素时,可以采用下标索引a[0];
例如
#include <stdio.h>
int main()
{
int a[5]={0,1,2,3,4};
printf("%d",a[3]);
return 0;
}
下标索引需要注意几点
int a[5];
一共有5个元素,分别是a[0],a[1],a[2],a[3],a[4],要注意没有a[5]。a={1,2,3}
是错误的,应该写:a[0]=1;a[1]=2;a[2]=3;
#include <stdio.h>
int main()
{
int a[5]={0,1,2,3,4};
printf("a[0]=%d,不存在的a[100]=%d\n",a[0],a[100]);
return 0;
}
有一种特殊数组叫字符串。当我们想要char类型数组时,可以
char a[10];
char a[10]={'a','b'};
char a[10]={101,102,103,104,105,106,107,108,109,110};
char a[10]={0};
char a[10]="hello wor";
char a[]="hello world";
这种char类型数组我们又称字符串。
char a[10]表示定义一个大小为10的char类型字符串,字符串(或称数组)内变量都未初始化。
char a[10]={‘a’,‘b’};则是将ab对应的ascii码值赋值个字符串前两个变量,后面8个变量初始化为0;
char a[10]={101…110};是将字符串a中每个元素对应赋值。
char a[10]={0};是将字符串a中所有元素初始化为0
char a[10]=“hello wor”;定义了一个字符串并对其赋值,等价于
char a[10]={‘h’,‘e’,‘l’,‘l’,‘o’,’ ‘,‘w’,‘o’,‘r’,’\0’};注意字符串常量自定补’\0’结束符
最后一种数组定义较为特殊,系统会根据后续初始化变量来自动判断该数组内存大小,本程序中char a[]="hello world";
等价于char a[12]="hello world";
。
那如果我们给数组初始化一个大于数组容量的列表会怎样,一起看结果
#include <stdio.h>
int main()
{
char a[11]="hello world"; //第一种初始化
//char a[11]={'h','e','l','l','o',' ','w','o','r','l','d','\0'}; //第二种初始化
printf("%s\n",a);
return 0;
}
上述程序有一个新知识点,%s输出字符串。
当我们给一个仅有11个字符的字符串赋值"hello world"时,由于字符串常量中含有12个字符(结尾自动补充’\0’字符),赋值后程序可以运行(部分编译器报错),但是当屏幕上显示字符串时,却由于最后一个休止符丢失而导致该字符串不能正常休止,在显示完前11个字符后,后续因为没有休止符而显示乱码。
当使用列表赋值时,该编译器直接报错,错误原因是因为列表元素大于字符串容量(数组容量)
单独使用一个字符同数组。例如a[4]=‘o’。
了解到这里相信大家就能理解,字符串其实就是比较特殊的数组。当然,正因为他的特殊性,字符串拥有更多的操作方法。例如针对字符串处理的标准库函数、存储库函数、独特的字符串初始化等。
很多时候我们需要存储矩阵形式的数据,例如灰度图、RGB彩色图片数据、数学矩阵等,我们就可以采用二维数组。
二维数组定义与一维数组类似,可以理解成数组的数组。例如
int a[2][3];
// 未知 未知 未知
// 未知 未知 未知
int a[2][3]={{1}};
// 1 0 0
// 0 0 0
int a[2][3]={{1,2},{3}};
// 1 2 0
// 3 0 0
int a{2][3]={{1,2},{3,4,5}};
// 1 2 0
// 3 4 5
也可以对其全部初始化
int a[2][3]={{1,2,3},\
{4,5,6}};
就定义了一个2行3列的二维数组,矩阵排列为
1 | 2 | 3 |
---|---|---|
4 | 5 | 6 |
同样的,可以采用下标索引的方式获取数组中的元素,下标也是从0开始计数,a[0][1],表示第0行第1个元素,即2。
类比字符串,字符串数组就是char类型的二维数组。例如
char s[3][10];
//未初始化
char s[3][10]={{'a'},{'a','s','\0'}};
//初始化s为
// "a"
// "as"
// ""
char s[3][10]={"qwer","asdf","zxcv"};
//初始化s为
// "qwer"
// "asdf"
// "zxcv"
想要单独使用其中一个字符,方法同数组类似,数组名[行][列]。例如s[2][0]=‘z’;
而是s[1]则是字符串"asdf"的首地址,涉及到地址问题统一放在后续指针与数组章节。
C语言中对于程序中出现的常量都有默认类型。1、2、100、-125等整数类型默认为int型;1.2、-2.5等小数类型默认为double型;‘a’、’b‘、’H’等字符类型默认为char型;“asd”、"keng"等字符串(字符型数组)默认为char类型指针,即char*类型。
#include <stdio.h>这句话的含义,就是导入了一个名称为stdio.h的文件,这个文件定义了标准(std)输入输出(io),这里的输入输出是相对于电脑来说,例如鼠标、键盘都是输入设备,而屏幕、文件、音响则是输出设备。常用的标准输入输出有scanf、printf、getc、putc、getchar、putchar、gets、puts、fgets、fputs、fget。
printf()函数是标准输出函数, 一般用于向标准输出设备按规定格式输出信息。在编写程序时经常会用到此函数。printf()函数的调用格式为:
其中格式化字符串包括三部分内容:一部分是显示字符,,这些字符将按原样输出,例如printf(“你好”);一部分是转义字符或叫做控制字符,例如’\n’是换行、‘\0’是字符串休止符、’\t’是制表符(对齐符号)等,例如printf(“\n”);另一部分是格式化规定字符,以”%”开始,后跟一个或几个规定字符,用来确定输出内容格式,同时后面需加参量表,例如printf(“%d”,a);。参量表是需要输出的一系列参数,其个数必须与格式化字符串所说明的输出参数个数一样多,各参数之间用“,”分开,且顺序需要与格式化字符所对应。
\
ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 | ASCII值 | 控制字符 |
---|---|---|---|---|---|---|---|
0 | NUT | 1 | SOH | 2 | STX | 3 | ETX |
4 | EOT | 5 | ENQ | 6 | ACK | 7 | BEL |
8 | BS | 9 | HT | 10 | LF | 11 | VT |
12 | FF | 13 | CR | 14 | SO | 15 | SI |
16 | DLE | 17 | DC1 | 18 | DC2 | 19 | DC3 |
20 | DC4 | 21 | NAK | 22 | SYN | 23 | TB |
24 | CAN | 25 | EM | 26 | SUB | 27 | ESC |
28 | FS | 29 | GS | 30 | RS | 31 | US |
32 | (space) | 33 | ! | 34 | " | 35 | # |
36 | $ | 37 | % | 38 | & | 39 | ’ |
40 | ( | 41 | ) | 42 | * | 43 | + |
44 | , | 45 | - | 46 | . | 47 | / |
48 | 0 | 49 | 1 | 50 | 2 | 51 | 3 |
52 | 4 | 53 | 5 | 54 | 6 | 55 | 7 |
56 | 8 | 57 | 9 | 58 | : | 59 | ; |
60 | < | 61 | = | 62 | > | 63 | ? |
64 | @ | 65 | A | 66 | B | 67 | C |
68 | D | 69 | E | 70 | F | 71 | G |
72 | H | 73 | I | 74 | J | 75 | K |
76 | L | 77 | M | 78 | N | 79 | O |
80 | P | 81 | Q | 82 | R | 83 | S |
84 | T | 85 | U | 86 | V | 87 | W |
88 | X | 89 | Y | 90 | Z | 91 | [ |
92 | \ | 93 | ] | 94 | ^ | 95 | _ |
96 | ` | 97 | a | 98 | b | 99 | c |
100 | d | 101 | e | 102 | f | 103 | g |
104 | h | 105 | i | 106 | j | 107 | k |
108 | l | 109 | m | 110 | n | 111 | o |
112 | p | 113 | q | 114 | r | 115 | s |
116 | t | 117 | u | 118 | v | 119 | w |
120 | x | 121 | y | 122 | z | 123 | { |
124 | 丨 | 125 | } | 126 | ~ | 127 | DEL |
使用例程
#include <stdio.h>
int main()
{
printf("%c,%c,%c,%c\n",'a','A','c','F');
return 0;
}
转义字符 | 含义 | ASCII码 |
---|---|---|
\o | 空字符(NULL) | 0 |
\n | 换行符(LF) | 10 |
\r | 回车符(CR) | 13 |
\t | 水平制表符(HT) | 9 |
\v | 垂直制表符(VT) | 11 |
\a | 响铃(BEL) | 7 |
\b | 退格符(BS) | 8 |
\f | 换页符(FF) | 12 |
\’ | 单引号 | 39 |
\" | 双引号 | 34 |
\\ | 反斜杠 | 92 |
\? | 问号 | 63 |
%% | 百分号 | 37 |
\ddd | 任意ASCII码 | 三位八进制数 |
\xhh | 任意ASCII码 | 二位十六进制数 |
使用例程
#include <stdio.h>
int main()
{
printf("a\0b\0c\n"); //空字符与换行符
printf("e\rf\tg\v\n"); //回车符、水平制表符与垂直制表符
printf("h\ai\bj\f\n"); //响铃符、退格符、换页符
printf("\'\"\\\n"); //单引号、双引号、反斜杠
printf("\?\101\x41\n"); //问号、八进制ascii字符'A'、十六进制ascii字符'A'
}
由于转义字符大多数是特殊的控制字符,部分无法直接在屏幕上显示出来,而是具有特殊效果,所以部分转义字符可能无法显示或显示乱码。
符号 | 作用 |
---|---|
%d | 十进制有符号整数 |
%u | 十进制无符号整数 |
%f | 浮点数 |
%s | 字符串 |
%c | 单个字符 |
%p | 地址格式 |
%e | 指数形式的浮点数 |
%x,%X | 无符号十六进制表示整数 |
%0 | 无符号八进制表示整数 |
%g | 自动选择合适格式显示 |
注意点:
printf("%5d",a);
表示输出场宽为5的十进制数,若a的数据位数不够5位则右对齐,多余5位以实际场宽为准。printf("%.2f",a);
表示保留2位小数。printf("%9.2f",a);
表示输出场宽为9的浮点数,其中小数位为2,整数位为6,小数点占一位,若a的数据位数不够9位则右对齐,多余9位以实际占用场宽为准。printf("%ld,a);
表示输出long整数。printf("%f",a);
表示输出float类型单精度浮点数(4字节)。printf("%lf",a);
表示输出double类型双精度浮点数(8字节)。printf("%5d",a);
表示输出场宽为5的十进制,若a的数据位数不够5位则右对齐,多余5位以实际场宽为准。使用示例
#include <stdio.h> int main() { char c; int a=1234; float f=3.141592653589; double x=0.12345678987654321; c='\x41'; //十进制65 十六进制0x41 ascii 'A' printf("a=%d\n", a); //结果输出十进制整数a=1234 printf("a=%6d\n", a); //结果输出6位十进制数a= 1234 printf("a=%06d\n", a); //结果输出6位十进制数a=001234 printf("a=%2d\n", a); //a超过2位, 按实际值输出a=1234 printf("x=%lf\n", x); //输出长浮点数x=0.123457 printf("x=%18.16lf\n", x);//输出18位其中小数点后16位的长浮点数x=0.1234567898765432 printf("c=%c\n", c); //输出字符c=A printf("c=%x\n", c); //输出字符的ASCII码值c=41 return 0; }
scanf()函数是标准输入函数。它从标准输入设备(简谱)读取输入的信息。其调用格式为:scanf(格式化字符串, 地址表);
格式化字符串包括以下三类不同的字符:
scanf("%d",&a);
scanf("%d %d",&a,&b);
scanf("%d,%d",&a,&b);
scanf("%d,%d",&a,&b);
上例中的scanf()函数先读一个整型数, 然后把接着输入的逗号剔除掉, 最后读入另一个整型数。如果“,”这一特定字符没有找到, scanf()函数就终止。若参数之间的分隔符为空格, 则参数之间必须输入一个或多个空格。对于初学者来说,经常会将代码中%d,%d中的逗号写成英文逗号,然后键盘输入时候输入中文逗号,导致程序无法找到特定的英文逗号而出错。
注意:
char c;
scanf("%c",&c) //正确
//scanf("%c",c) //错误,对于变量输入需要取地址
char s[10];
scanf("%s",s); //正确
//scanf("%s",&s); //错误,数组名称本身就是地址,不需要再取地址
#include "stdio.h"
int main()
{
int a,b;
scanf("%3d %d",&a,&b); //变量a只接受前3位
printf("a=%d b=%d",a,b);
return 0;
}
输入
123456789
请编程:简单加法计算器
参考例程
#include "stdio.h"
int main()
{
double a,b;
printf("请输入加法算式,例如 \"1+2=\"\n");
scanf("%lf+%lf=",&a,&b);
printf("%lf+%lf=%lf\n",a,b,a+b);
return 0;
}
输入
3.4+2.5=
注意输入两个数之间的分隔符必须与scanf中的格式保持一直,本程序中采用’+'分隔。
putchar()使用格式为
char c='A';
putchar(c);
他的作用就是向标准输出设备输出一个字符,以回车键结束,其等价于printf("%c",c);
getchar()则是与putchar()相对应,putchar()为输出一个字符,getchar()为获得一个字符。getchar
char c='A';
getchar(c);
使用例程
#include <stdio.h>
int main()
{
char c='A'; //定义char类型变量c初始化为字符A
putchar(c); //屏幕显示(输出)字符
putchar('\n'); //输出换行
c=getchar(); //从键盘输入一个字符保存到c
putchar(c); //输出字符
putchar('\n'); //输出换行
return 0;
}
键盘输入
B
运行结果可以看到,屏幕显示内容为 ‘A’、换行、‘B’、换行。
puts(s)与gets(s)可以类比标准输入输出:
char s[10]="hello";
puts(s); //等价于printf("%s\n",s);
gets(s); //等价于scanf("%s",s); //字符数组不用加&
注意`:
puts(s);
输出完字符串后会自动换行。gets(s);
从缓冲区读取字符串直到遇到回车停止。但是scanf("%s",s);
是从缓冲区读取字符串直到遇到回车或空格停止。getch()与getchar()用法一样。区别在于getchar()读取字符时候,我们输入字符时,屏幕上会后相应的显示;而getch()读取字符则没有回显。当我们不需要屏幕回显时,比如贪吃蛇用awsd控制上下左右,每当我们按下一个键屏幕就显示相应字符显然不是我们想要的效果,这种时候就可以使用不带回显的输入。
同getchar(),只是该函数不论按下什么按键都直接获取相应字符,不以回车键结束。
C语言程序从我们敲代码到运行有四个步骤:
用于将所有的#include头文件以及宏定义替换成其真正的内容,预处理之后得到的仍然是文本文件,但文件体积会大很多。
这里的编译不是指程序从源文件到二进制程序的全部过程,而是指将经过预处理之后的程序转换成特定汇编代码(assembly code)的过程。
汇编过程将上一步的汇编代码转换成机器码(machine code),这一步产生的文件叫做目标文件,是二进制格式。
链接过程将多个目标文以及所需的库文件(.so等)链接成最终的可执行文件(executable file)。
作用域(scope):限定一个变量的可用范围,以{}为界限,变量必须定义在作用域开始。
生命周期:变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段。
定义在{}内部的变量称局部变量。局部变量的生命周期:进入作用域生命周期的开始,出作用域生命周期结束。例如以下程序,a的作用域在整个main内有效。
#include <stdio.h>
int main()
{
int a=0; //局部变量a,作用域为main函数
printf("a=%d\n",a);
return 0;
}
而下例程中,a的作用域仅仅在{}内有效。
#include <stdio.h>
int main()
{
{
int a=0; //局部变量a,作用域为{}内
}
printf("a=%d\n",a); //由于此时a不在作用域内,a的生命周期已经结束
//所以报错表示a不存在。
return 0;
}
定义在所有函数之外称为全局变量。全局变量的生命周期:整个程序的生命周期。例如
#include <stdio.h>
int a=1; //全局变量a=1
int b; //全局变量b=0
int main()
{
printf("a=%d,b=%d\n",a,b);
}
全局变量与局域变量区别:
#include <stdio.h>
int a; //全局变量a=0
int main()
{
int b; //局部变量b=未定义
printf("a=%d,b=%d\n",a,b);
}
#include <stdio.h> int a; //全局变量a=0 void fun() { printf("非主函数作用域a=%d\n",a); } int main() { int a=1; //局部变量a=1; { int a=2;//子作用域a=2; printf("子作用域a=%d\n",a); } printf("主函数内a=%d\n",a); fun(); return 0; }
这里出现一个新的概念:自定义函数fun。函数具体内容后续章节介绍,在本程序中作用是输出全局变量a=0。
修饰符指的是我们定义变量或函数时,在定义的语句添加的用于修饰变量或函数的关键字。常用修饰符有const、static、register、volatile、extern、auto。
表示该数据为常量。const修饰的是在它前面的类型,如果它前面没有类型,那它修饰的是紧跟着它的那个类型。使用const定义变量时,由于变量被const限制成不可改变的常量,所以该数据必须初始化,并且定义之后,任何试图修改该数据的操作都是非法的。例如
const int a=0; //修饰int,a是一个不可改变的数,值为0;
int const b=1; //修饰int,b时应该不可改变的数,值为1;
const int c=1,d=2; //修饰int,c和d都是常量
const int f; //错误,常量必须初始化
a=10; //错误,试图修改常量
b=1; //错误,试图修改常量
有点编译器会自动对未初始化变量默认初始化,可能const int f;
这种未定义的常量也可以通过编译,但是并没有什么意义,因为常量后期不能修改,而该常量目前的值是毫无意义的,那我们用它干什么呢。
并且通常对于不可改变的常量,我们用大写字母去命名数据,并不是说不可以使用其它名称,但如果拥有一个好的命名习惯,我们就可以通过变量名称看出他的类型及作用。
例程
#include <stdio.h>
int main()
{
const int W_SCREEN=128; //屏幕宽度为128像素
const int H_SCREEN=64; //屏幕高度为64像素
printf("屏幕宽度为%d\n",W_SCREEN);
printf("屏幕高度为%d\n",H_SCREEN);
printf("总像素点数%d\n",W_SCREEN*H_SCREEN);
return 0;
}
对于指针而言,const略有不同,区别在于顶层const与底层const
int *const p1=&a;
const与变量名称p1紧贴,修饰变量本身,这里表示p1指针从定义之后就不能修改了,从此p1就只能指向a。但是可以通过p1修改a的值,例如*p=5;
int const *p1; //写法1 const int *p2; //写法2
,这两种写法完全等价,其效果是无法通过该指针*p1去改变目标地址的内容(即a的内容)。但是可以修改p1的地址,例如p1=&b;
。int const *const p1; //写法1 const int *const p2; //写法2
const变量与指针的赋值问题——权限只能缩小,不能放大
假如有
int i=5;
const int ci=10;
int *p1=&i; //正确,权限相同,可以修改i的值
int *p2=&ci; //错误,权限扩大,通过p2修改ci的值,但是ci本身不可修改
p1=&i; //正确,可以修改指针变量本身,并且权限相同
p2=&ci; //错误,可以修改指针变量本身,但是权限扩大
*p1=20; //正确,可以通过普通指针变量修改目标地址的内容
*p2=20; //正确,可以通过普通指针变量修改目标地址的内容
int *const pc1=&i; //正确,权限相同,不去修改i的值
int *const pc2=&ci; //正确,权限相同,不能通过pc2修改ci的值,并且ci本身也不可修改
pc1=&i; //正确,顶层const可以修改指针变量本身,并且权限缩小
pc2=&ci; //正确,顶层const可以修改指针变量本身,并且权限相同
*pc1=20; //错误,顶层const不可以通过指针修改目标地址的内容
*pc2=20; //错误,顶层const不可以通过指针修改目标地址的内容
const int cp1=&i; //正确,权限相同,都可以修改i的值,但是cp1指针本身不可以修改
const int cp2=&ci; //错误,权限扩大,ci本身不可修改,但是cp2指针可以通过地址修改ci内容,所有扩大权限的操作都是非法的
cp1=&i; //错误,顶层const不能修改指针变量本身
cp2=&ci; //错误,顶层const不能修改指针变量本身
*pc1=20; //正确,顶层const可以修改目标地址的内容
*pc2=20; //正确,顶层const可以修改目标地址的内容
const int *const cpc1=&i;//正确,权限缩小,并且修改i的值
const int *const cpc2=&ci;//正确,权限相同,不能通过指针修改ci值,并且ci本身不可修改
cpc1=&i; //错误,指针变量本身不可修改
cpc2=&i; //错误,指针变量本身不可修改
*cpc1=20; //错误,不能通过指针变量修改目标地址的内容
*cpc2=20; //错误,不能通过指针变量修改目标地址的内容
static修饰符有3点作用:
#include <stdio.h>
//简单测试函数,目前看不懂没有关系,只需了解这个函数与主函数一样可以运行即可
void fun()
{
int a=0;
a=a+1;
printf("a=%d\n",a);
}
int main()
{
fun(); //第一次运行
fun(); //第二次运行
fun(); //第三次运行
}
未使用静态变量时候,每次执行fun()都会重新创建一个变量a=0;然后a=a+1;然后输出a=1;离开fun()时候a会被销毁。
而使用static修饰符后。
#include <stdio.h>
//简单测试函数,目前看不懂没有关系,只需了解这个函数与主函数一样可以运行即可
void fun()
{
static int a=0;
a=a+1;
printf("a=%d\n",a);
}
int main()
{
fun(); //第一次运行
fun(); //第二次运行
fun(); //第三次运行
}
添加static修饰符后,当初次执行fun()会创建一个变量a=0;并执行a=a+1;然后输出a=1;离开fun()后变量a不会被销毁,并且下次执行fun()时不会重新创建变量a,而是继续使用之前的变量a;直接执行a=a+1;然后输出a=2;变量a一直持续到程序结束才会被销毁。
register表示寄存器变量,极少使用。简单讲解一下,可以略过不看。
volatile表示易变变量,较少使用。简单讲解一下,可以略过不看。
register介绍中描述了数据需要从存储器读取到寄存器。我们的电脑十分智能,当我们第一次使用a变量时,cpu会将它读取到寄存器中,假设是寄存器A。当我们第二次使用变量a是,cpu判断之前程序并没有对a的操作,所以cpu认为寄存器A中的值就是a,于是不会再次从存储器中读取a,而是直接使用寄存器中的A。但是电脑中可能有其他程序篡改了存储器中的a,如果我们想要cpu每次使用变量a时都重新从存储器中读取,那就可以使用volatile修饰变量。
全局变量声明符号。在分文件编译中有详细介绍,通过该修饰符可以在不同文件中使用同一变量。
1.c语言中,关键字auto用于声明一个变量为自动变量
自动变量也称局部变量。将不在任何类、结构、枚举、联合和函数中定义的变量视为全局变量,而在函数中定义的变量视为局部变量。所有局部变量默认都是auto,一般省略不写。
auto声明全局变量时,编译出错:
#include <stdio.h>
auto int i;
int main(void)
{
}
auto声明局部变量时,编译正常:
int main(void)
{
auto int i = 1;
return 0;
}
2.c语言中,只使用auto修饰变量,变量的类型默认为整型。
int main(void)
{
double a = 1.2, b = 2.7;
auto c = a + b;//等价于int c=a+b;先计算a+b值为3.9
return 0; //然后将3.9赋值给int c,类型介绍中说过,c会向下取整=3
}
数据类型转换就是将数据(变量、数值、表达式的结果等)从一种类型转换为另一种类型。
C语言是强类型语言,如果一个运算符两边的运算数据类型不同,先要将其转换为相同的类型,强制类型转换可以消除程序中的警告,即确保写代码的程序员自己清楚类型转换,允许丢失一定精度做类型匹配。
强制类型转换是显示的、按照我们意愿把变量从一种类型转换为另一种数据类型,通常用于 。表达式为:(新变量类型)原变量名称
例如:
#include <stdio.h>
int main(void)
{
double a=9.2,b=9.2;
printf("a=%lf,b=%d\n",a,(int)b); //a是double型,用%lf输出
//b是double型,用%lf输出
return 0; //(int)b是int类型,用%d输出
}
再看一例:
#include <stdio.h>
int main(void)
{
int a=5,b=2; //int类型无法保存小数
double c,d; //双精度浮点型变量
c=a/b;
d=(double)a/b;
printf("5/2结果为%lf\n",c);
printf("5/2结果为%lf\n",d);
return 0;
}
本例中,c=a/b;由于a和b都是int类型,a/b结果也是int类型,即2,然后c=2;
d=(double)a/b;由于(double)a是double类型,b是int类型,两个不同类型数进行计算,系统会先将b也转为double,再进行运算,所以最后结果为2.5;
较为特殊的是,我们可以将空指针(void*)转为其他任意指针类型,例如:
void *pv=NULL;
int *pi=(int*)pv;
char *pc=(char*)pc;
另外有一种特殊且危险的使用方法是将底层const指针强制修改为可变指针并赋值给另一个指针。例如:
int a=5; //变量a
int const *p1=&a; //底层const指针,无法通过p1修改a的值
int *p2=(int*)p1 //强制将int const*类型转为int *类型,可以通过p2修改a的值
还有一种更为危险且特殊的用法:我们可以通过强制类型转换将一个int数转为指针类型,但是极有可能导致错误,所以不推荐使用。例如:
int a=0xffffffff;
int *p=(int*)a;
隐式类型转换又称自动类型转换,这种操作不需要程序员处理,系统会自动将运算符两边不同的数据类型进行匹配。
\
1、 将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换,例如:float f = 320;
这里320 是 int 类型的数据,需要先转换为 float 类型才能赋值给变量 f。再如:int n = f;
这里f 是 float 类型的数据,需要先转换为 int 类型才能赋值给变量 n。
在赋值运算中,赋值号两边的数据类型不同时,需要把右边表达式的类型转换为左边变量的类型,这可能会导致数据失真,或者精度降低,例如int a=3.4;
这里的3.4会从double类型自动转换为int类型,丢失小数。所以说,自动类型转换并不一定是安全的。对于不安全的类型转换,编译器一般会给出警告。
2、在不同类型的混合运算中,编译器也会自动地转换数据类型,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换的规则如下:
(1)转换按数据长度增加的方向进行,以保证数值不失真,或者精度不降低。例如,int 和 long 参与运算时,先把 int 类型的数据转成 long 类型后再进行运算。
(2)所有的浮点运算都是以双精度进行的,即使运算中只有 float 类型,也要先转换为 double 类型,才能进行运算。
(3)char 和 short 参与运算时,必须先转换成 int 类型。
通过下面例子更生动的表述一下:
#include<stdio.h>
int main(){
float PI = 3.14159; //float类型变量
int s1, r = 1; //int类型变量
double s2; //double类型变量
s1 = r * r * PI; //int*int*float先计算int*int结果为int
//之后计算int*float,先将int转为float,再计算float*float,结果为float
//最后int=float,先将float转为int再赋值
s2 = r * r * PI; //int*int*float计算过程如上,结果为float。
//double=float,先将float转为double再赋值
printf("s1=%d, s2=%f\n", s1, s2);
return 0;
}
上例中s1 = r * r * PI;
int=intintfloat计算步骤:
上例中s2 = r * r * PI;
double=intintfloat计算步骤:
在计算表达式r×r×PI时,r 和 PI 都被转换成 double 类型,表达式的结果也是 double 类型。但由于 s1 为整型,所以赋值运算的结果仍为整型,舍去了小数部分,导致数据失真。
无论是隐式类型转换还是强制类型转换,都只是为了本次运算而进行的临时性转换,转换的结果也会保存到临时的内存空间,不会改变数据本来的类型或者值。
在C语言中,有些类型既可以隐式转换,也可以强制转换,例如 int 到 double,float 到 int 等;而有些类型只能强制转换,不能隐式转换,例如以后将要学到的 void * 到 int *,int 到 char * 等。
可以隐式转换的类型一定能够强制转换,但是,需要强制转换的类型不一定能够隐式转换。现在我们学到的数据类型,既可以隐式转换,又可以强制转换,以后我们还会学到一些只能强制转换而不能隐式转换的类型。
可以隐式进行的类型转换一般风险较低,不会对程序带来严重的后果,例如,int 到 double 没有什么缺点,float 到 int 顶多是数值精度丢失。只能强制进行的类型转换一般风险较高,或者行为匪夷所思,例如,char * 到 int * 就是很奇怪的一种转换,这会导致取得的值也很奇怪,再如,int 到 char * 就是风险极高的一种转换,一般会导致程序崩溃。
使用强制类型转换时,程序员自己要意识到潜在的风险。
本章节为C语言核心部分,可以说掌握本章节对C语言学习至关重要。从本节开始,程序会渐渐变得复杂,这时在线编译器已经不能满足我们的需求了,所以我们后续程序都将使用Visual studio编译运行,Visual studio版本不同区别不大。
随着我们学习的深入,在线编译器已经无法满足需求了,我们需要自己再电脑上配置一个C语言程序开发环境。在C语言编译环境中曾给过大家VS、VC、在线编译器的下载地址,这里在展示一下。
vc++6.0,这个软件网络上有很多资源,这里给大家提供一个。
链接:https://pan.baidu.com/s/1PApsWKEDMzvqM9NmSuw6hA
提取码:zukk
或者也可以用菜鸟工具中的在线编译器
https://c.runoob.com/compile/11/
VS下载地址官网
https://visualstudio.
第一步:
**
首先先找自己电脑上的浏览器,在搜索框中输入“Visual studio”,找到应用程序官方下载地址。
第二步:
根据自己的电脑选择版本,我自己电脑是Windows 所以以下内容将是Windows的安装教程。
第三步:
安装与配置
勾选C++桌面开发。选择合适的安装位置。
第四步:
确认安装,并等安装进度条走完。
此时桌面应该有一个VS图标,打开即可使用。
1、创建工程项目:
输入该工程项目名称,支持中文;选择安装地址,创建工程。
2、添加工程文件
点击源文件-添加-新建项或按快捷键shift+ctrl+N
选择C++文件,输入文件任意文件名称(可以是中文、不能过长),文件后缀为 “.c” 表示c语言程序,如果为 “.cpp” 表示c++语言程序。点击添加
此时工程目录中也已经有程序文件main.c了
3、编写
其中快捷键有
#include <stdio.h>
int main()
{
int a = 5;
printf("原a=%d\n", a);
printf("请输入新a的值:");
scanf("%d", &a);
printf("新a的值为%d\n", a);
return 0;
}
4、编译、链接与运行
开发环境可以直接集成编译、链接、运行。点击调试-开始运行(不调试)或按ctrl+F5
注意下面提示错误。
这句话翻译过来的大概意思是该函数在新标准里面已经不再使用。我们不用管它,有两种方法解决
#pragma warning(disable:4996)
,4996警告全是因为函数版本低,可以直接使用这句话屏蔽。添加忽略警告之后,重新编译,程序成功运行。
在我们编写的程序中输入10后效果图
运行成功,并且程序与预期相同。
当程序语句过多,且程序出现非语法错误时候,我们会采用调试功能。
调试指的就是编译全部代码,但是并不运行全部代码,而是按照我们的意愿执行到指定行,常用调试方式有:
断点调试
断点调试指的是程序运行,直到下一个断点语句出现时,程序暂停运行,这时候我们可以将光标移动到变量上即可看到变量的值,并可以据此判断程序有没有问题。
点击红框所指的区域,可以设置或取消断点
点击调试-切换断点可以设置或取消断点,同时快捷键F9也是同样的作用
当我们设置好断点时,可以点击调试-开始调试或按快捷键F5进行调试,调试时,程序会自动运行到第一个断点处,按F5可以继续运行直到下一个断点或暂停运行。
提示框1是程序即将运行的语句,提示框2是终止或重启调试。
单步调试
当程序需要一句话一句话的单步执行时,可以采用单步调试,常用于循环错误检测或错误内容锁定。
点击调试-逐过程或按快捷键F10即可单步调试。
此时不论是否有断点,程序都将暂停在main函数开头。每按一次F10或点击图标或点击调试-逐过程都将执行一句语句。
当我们运行到自定义函数时,可以使用F11或点击快捷菜单中图标,该快捷键作用是进入函数内部。
当我们跳转到自定义函数内部是,可以使用shift+F11或点击快捷菜单栏中图标,快速运行完该自定义函数并跳出该自定义函数。
单步调试和断点调试可以配合使用,能够极大提升开发者开发效率。
运算符是一种告诉编译器执行特定的数学或逻辑操作的符号。C 语言内置了丰富的运算符,其种类有:算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、其他运算符。
本章将逐一介绍算术运算符、关系运算符、逻辑运算符、位运算符、赋值运算符和其他运算符。
\
下表显示了 C 语言支持的所有算术运算符。假设int变量 A 的值为 10,变量 B 的值为 20,则:
名称 | 运算符 | 描述 | 示例 |
---|---|---|---|
加 | + | 把两个操作数相加 | A + B 将得到 30 |
减 | - | 从第一个操作数中减去第二个操作数 | A - B 将得到 -10 |
乘 | * | 把两个操作数相乘 | A * B 将得到 200 |
除 | / | 分子除以分母 | B / A 将得到 2 |
取模 | % | 取模运算符,整除后的余数 | B % A 将得到 0 |
自加 | ++ | 自增运算符,整数值增加 1 | A++ 将得到 11 |
自减 | – | 自减运算符,整数值减少 1 | A-- 将得到 9 |
请看下面的实例,了解 C 语言中所有可用的算术运算符:
#include <stdio.h> int main() { int a = 5; int b = 2; int c ; c = a + b; printf("a+b的值是 %d\n", c ); c = a - b; printf("a-b的值是 %d\n", c ); c = a * b; printf("a*b的值是 %d\n", c ); c = a / b; printf("a/b的值是 %d\n", c ); c = a % b; printf("a%b的值是 %d\n", c ); c = a++; // 赋值后再加 1 ,c 为 21,a 为 22 printf("a++的值是 %d\n", c ); c = a--; // 赋值后再减 1 ,c 为 22 ,a 为 21 printf("a--的值是 %d\n", c ); return 0; }
运行完成后结果为:
a+b的值是 7
a-b的值是 3
a*b的值是 10
a/b的值是 2
ab的值是 1
a++的值是 5
a--的值是 6
值得注意的是,++与–运算有左结合和右结合两种类型。区别如下:
#include <stdio.h>
int main()
{
int a=10;
int b=10;
printf("a++运算时值为:%d\n",a++);
printf("a++运算后值为:%d\n\n",a);
printf("++b运算时值为:%d\n",++b);
printf("++b运算后值为:%d\n",b);
return 0;
}
运行结果为:
a++运算时值为:10
a++运算后值为:11
++b运算时值为:11
++b运算后值为:11
左结合a++和右结合++a运算后结果一样,唯一区别时在a++和++a所处的语句中,表达式的值a++是原来的值,而++a是加1后的值。a–与–a同理。
其原理也很简单。假如a=10:
下面我们看一个有意思的案例:
#include <stdio.h> int main() { int a, b, c, d; a = 5; b = a++ * a++ * a++; printf("a=%d,b=%d\n", a, b); a = 5; c = ++a * ++a * ++a; printf("a=%d,c=%d\n", a, c); a = 5; d = ++a * a++ * ++a; printf("a=%d,d=%d\n", a, d); return 0; }
运行结果为:
a=8,b=125
a=8,c=512
a=8,d=343
对于第一个结果,a++*a++*a++时,所有更新的a的数据都在后台保存,但是本条语句中,a的值一直为5。
对于第二个结果,++a*++a*++a时,所有数据都在本条语句生效,即先统计完成所有++a后,a的值为8,然后再8*8*8=512。
对于第三个结果,++a*a++*++a时,两条语句即时生效,a=7,执行完成7*7*7后,程序会再处理a++的值,a的值为8,最后结果7*7*7=343。
下表显示了 C 语言支持的所有关系运算符。假设变量 A 的值为 10,变量 B 的值为 20,则:
名称 | 运算符 | 描述 | 实例 |
---|---|---|---|
双等于 | == | 检查两个操作数的值是否相等,如果相等则条件为真。 | (A == B) 为假。 |
不相等 | != | 检查两个操作数的值是否相等,如果不相等则条件为真。 | (A != B) 为真。 |
大于 | > | 检查左操作数的值是否大于右操作数的值,如果是则条件为真。 | (A > B) 为假。 |
小于 | < | 检查左操作数的值是否小于右操作数的值,如果是则条件为真。 | (A < B) 为真。 |
大于等于 | >= | 检查左操作数的值是否大于或等于右操作数的值,如果是则条件为真。 | (A >= B) 为假。 |
小于等于 | <= | 检查左操作数的值是否小于或等于右操作数的值,如果是则条件为真。 | (A <= B) 为真。 |
请看下面的实例,了解 C 语言中所有可用的关系运算符,其中输出0表示假、1表示真:
#include <stdio.h>
int main()
{
int a=5,b=2;
printf("a==b的判断结果为%d\n",a==b);
printf("a!=b的判断结果为%d\n",a!=b);
printf("a>b的判断结果为%d\n",a>b);
printf("a<b的判断结果为%d\n",a<b);
printf("a>=b的判断结果为%d\n",a>=b);
printf("a<=b的判断结果为%d\n",a<=b);
return 0;
}
运行结果为:
a==b的判断结果为0
a!=b的判断结果为1
a>b的判断结果为1
a<b的判断结果为0
a>=b的判断结果为1
a<=b的判断结果为0
下表显示了 C 语言支持的所有关系逻辑运算符。假设变量 A 的值为 1,变量 B 的值为 0,则:
名称 | 运算符 | 描述 | 实例 |
---|---|---|---|
逻辑与 | && | 称为逻辑与运算符。如果两个操作数都非零,则条件为真。 | (A && B) 为假。 |
逻辑或 | ▏▏ | 称为逻辑或运算符。如果两个操作数中有任意一个非零,则条件为真。 | (A ▏▏ B) 为真。 |
逻辑非 | ! | 称为逻辑非运算符。用来逆转操作数的逻辑状态。如果条件为真则逻辑非运算符将使其为假。 | !(A && B) 为真。 |
请看下面的实例,了解 C 语言中所有可用的逻辑运算符,其中输出0表示假,输出1表示真:
#include <stdio.h>
int main()
{
int a=5,b=2;
printf("a&&b的判断结果为%d\n",a&&b);
printf("a||b的判断结果为%d\n",a||b);
printf("!(a&&b)的判断结果为%d\n",!(a&&b));
return 0;
}
运行结果为:
a&&b的判断结果为1
a||b的判断结果为1
!(a&&b)的判断结果为0
并且带有逻辑运算符的语句中,程序并不一定被执行,请看下面例子:
#include <stdio.h>
int main()
{
int a = 0;
int b = 0;
printf("a++&&b++结果为%d\n", a++ && b++);
printf("此时a为%d,b为%d\n", a, b);
return 0;
}
运行结果为:
a++&&b++结果为0
此时a为1,b为0
可能有同学发现,程序只执行了a++,并没有执行b++。这是因为系统会先判断&&运算符左边是否有效(是否为非0数),如果左侧有效,系统才会再去判断右边是否有效。在本程序中,系统先判断左侧a++的值在本条语句中为0,所以就不用执行并判断后续语句,直接就可以认定(a++&&b++)结果为假。
同理,对于||运算符而言,如果 (左表达式)||(右表达式) 语句中左表达式值为1,则程序不会执行右表达式,而是直接认定整个或逻辑运算结果为真。例如:
#include <stdio.h>
int main()
{
int a = 1;
int b = 1;
printf("a++||b++结果为%d\n", a++ || b++);
printf("此时a为%d,b为%d\n", a, b);
return 0;
}
运行结果为:
a++||b++结果为1
此时a为2,b为1
逻辑运算符与其他运算另一个不同点是,逻辑运算符(&&、||)左右两侧算两条语句。什么意思呢,就比如说++运算符,若a=5;则a++在本条语句中,a的值仍为5,但是在a++&&a++中,a在运算符左边时,其值为5,在运算符右边时,a的值为6,运行完本条语句后,a的值为7。例如:
#include <stdio.h>
int main()
{
int a = 1;
printf("a--||a的值为%d\n", a-- && a);
printf("之后a的值为%d\n", a);
return 0;
}
若是其他运算符,则由于a–&&a是同一条语句,语句中两个a的值都是1,最终1&&1=1。
但是不然,在逻辑运算中,a–先被执行完成,在左侧时,a–语句值为1,并且判断完成左侧后,a的值就被修改为0了,再到右侧时,a的值已经为0,所以最终a–&&a结果为0。
运行结果为:
a--||a的值为0
之后a的值为0
下表显示了 C 语言支持的所有关系运算符。为运算符直接将两个二进制数对应位相计算,假设变量 A 的值为 0B0101(5),变量 B 的值为 0B0011(3),则:
名称 | 运算符 | 描述 | 实例 |
---|---|---|---|
按为与 | & | 对应位上的两个数都为1,则结果的对应为为1,否则为0。 | ( A & B)的结果为 0B0001 |
按位或 | ▏ | 对应位上的两个数都为0,则结果的对应位为0,否则为1。 | ( A ▏ B)的结果为0B0111 |
按位异或 | ^ | 对应位上的两个数不相同,则结果为1,否则为0. | (A ^ B)的结果为0B0110 |
按位取反 | ~ | 对原操作数所有位取反 | (~ A)的结果为0B1010) |
左移 | << | 对原操作数左移x位,x为<<右边的数,右边进0 | (A << 1)的结果为0B1010 |
右移 | >> | 对原操作数右移x为,x为>>右边的数,左边进0 | (A >> 1)的结果为0B0010 |
假设如果 A = 60,且 B = 13,现在以二进制格式表示,它们如下所示:
A = 0011 1100
B = 0000 1101
相关位运算计算:
按位与A&B =
0B 0011 1100
& 0B 0000 1101
= 0B 0000 1100
按位或A|B =
0B 0011 1100
| 0B 0000 1101
= 0B 0011 1101
按位异或A^B =
0B 0011 1100
^ 0B 0000 1101
= 0B 0011 0001
按位取反~A =
~0B 0011 1100
=0B 1100 0011
左移A<<1 =
0B 0011 1100
《1
= 0B 0111 1000
右移A>>1 =
0B 0011 1100
》1
= 0B 0001 1110
请看下面的实例,了解 C 语言中所有可用的位运算符:
#include <stdio.h> int main() { unsigned int a = 60; /* 60 = 0011 1100 */ unsigned int b = 13; /* 13 = 0000 1101 */ int c = 0; c = a & b; /* 12 = 0000 1100 */ printf("a&b 的值是 %d\n", c ); c = a | b; /* 61 = 0011 1101 */ printf("a|b 的值是 %d\n", c ); c = a ^ b; /* 49 = 0011 0001 */ printf("a^b 的值是 %d\n", c ); c = ~a; /*-61 = 1100 0011 */ printf("~a 的值是 %d\n", c ); c = a << 1; /* 120 = 0000 0111 1000 */ printf("a<<1 的值是 %d\n", c ); c = a >> 1; /* 30 = 0001 1110 */ printf("a>>1 的值是 %d\n", c ); return 0; }
运行结果为:
a&b 的值是 12
a|b 的值是 61
a^b 的值是 49
~a 的值是 -61
a<<1 的值是 120
a>>1 的值是 30
下表列出了 C 语言支持的赋值运算符:
名称 | 运算符 | 描述 | 实例 |
---|---|---|---|
等于 | = | 简单的赋值运算符,把右边操作数的值赋给左边操作数(左边为可修改的左值) | C = A + B 将把 A + B 的值赋给 C |
加等于 | += | 加且赋值运算符,把右边操作数加上左边操作数的结果赋值给左边操作数 | C += A 相当于 C = C + A |
减等于 | -= | 减且赋值运算符,把左边操作数减去右边操作数的结果赋值给左边操作数 | C -= A 相当于 C = C - A |
乘等于 | *= | 乘且赋值运算符,把右边操作数乘以左边操作数的结果赋值给左边操作数 | C *= A 相当于 C = C * A |
除等于 | /= | 除且赋值运算符,把左边操作数除以右边操作数的结果赋值给左边操作数 | C /= A 相当于 C = C / A |
取模等于 | %= | 求模且赋值运算符,求两个操作数的模赋值给左边操作数 | C %= A 相当于 C = C % A |
左移等于 | <<= | 左移且赋值运算符 | C <<= 2 等同于 C = C << 2 |
右移等于 | >>= | 右移且赋值运算符 | C >>= 2 等同于 C = C >> 2 |
按位与等于 | &= | 按位与且赋值运算符 | C &= 2 等同于 C = C & 2 |
按位异或等于 | ^= | 按位异或且赋值运算符 | C ^= 2 等同于 C = C ^ 2 |
按位或等于 | ▏= | 按位或且赋值运算符 | C ▏= 2 等同于 C = C ▏ 2 |
请看下面的实例,了解 C 语言中所有可用的赋值运算符:
#include <stdio.h> int main() { int a = 21; int c ; c = a; printf("= 运算符实例,c 的值 = %d\n", c ); c += a; printf("+= 运算符实例,c 的值 = %d\n", c ); c -= a; printf("-= 运算符实例,c 的值 = %d\n", c ); c *= a; printf("*= 运算符实例,c 的值 = %d\n", c ); c /= a; printf("/= 运算符实例,c 的值 = %d\n", c ); c = 200; c %= a; printf("%%= 运算符实例,c 的值 = %d\n", c ); c <<= 2; printf("<<= 运算符实例,c 的值 = %d\n", c ); c >>= 2; printf(">>= 运算符实例,c 的值 = %d\n", c ); c &= 2; printf("&= 运算符实例,c 的值 = %d\n", c ); c ^= 2; printf("^= 运算符实例,c 的值 = %d\n", c ); c |= 2; printf("|= 运算符实例,c 的值 = %d\n", c ); return 0; }
运行结果为:
= 运算符实例,c 的值 = 21
+= 运算符实例,c 的值 = 42
-= 运算符实例,c 的值 = 21
*= 运算符实例,c 的值 = 441
/= 运算符实例,c 的值 = 21
%= 运算符实例,c 的值 = 11
<<= 运算符实例,c 的值 = 44
>>= 运算符实例,c 的值 = 11
&= 运算符实例,c 的值 = 2
^= 运算符实例,c 的值 = 0
|= 运算符实例,c 的值 = 2
下表列出了 C 语言支持的其他一些重要的运算符,假设int a=4:
名称 | 运算符 | 描述 | 实例 |
---|---|---|---|
逗号 | , | 逗号运算符,用于将语句分割,总是返回最后变量的值 | a=(5,6,7);值为7 |
内存计算 | sizeof() | 返回变量的大小。 | sizeof(a) 将返回 4 |
取址符 | & | 返回变量的地址。 | &a; 将给出变量的实际地址。 |
解引用符 | * | 通过指针调用变量。 | *p; 将指向一个变量。 |
条件运算符 | ? : | (条件表达式)? (如果为真则执行) : (如果为假则执行) | (a?a=2:a=3) 结果为a=2 |
点运算符 | . | 访问结构体成员 | a.c 访问结构体变量a中的成员变量c |
指向运算符 | -> | 访问结构体指针变量内成员 | p->c 访问结构体指针所指向的变量的成员 |
请看下面的实例,了解 C 语言中所有可用的其他运算符:
#include <stdio.h> struct TYPE { int a; }mydata; int main() { char c='D'; char *pchar=&c; int i=10; int *pint=&i; int b; struct TYPE*p=&mydata; mydata.a=0; //访问结构体变量内成员 p->a=2; //访问结构体指针所指向的结构体变量内成员 /* , 运算符实例 */ printf("(0,1,2)输出值为%d\n",(0,1,2)); /* sizeof 运算符实例 */ printf("char变量c大小为%d\n",sizeof(c)); //char类型变量大小为1 printf("char指针pchar大小为%d\n",sizeof(pchar)); //所有指针类型变量都是用于保存地址,其大小=int类型大小4 printf("int变量i大小为%d\n",sizeof(i)); //int类型变量大小为4 printf("int指针pint大小为%d\n",sizeof(pint)); //指针变量同int类型大小 /* & 和 * 运算符实例 */ pchar = &c; /* 'pchar' 现在包含 'c' 的地址 */ printf("pchar地址为%p\n",&pchar); printf("pchar内容为%p\n",pchar); //pchar保存的就是c的地址 printf("c地址为%p\n",&c); printf("通过*pchar访问c的值为%c\n",*pchar); printf("c的值为%c\n",c); /* 三元运算符实例 */ i = 10; b = (i == 1) ? 20: 30; //i不等于1,所以执行:与;之间内容,等价于b=30; printf( "b 的值是 %d\n", b ); b = (i == 10) ? 20: 30; //i不等于10,所以执行?与:之间内容,等价于b=20; printf( "b 的值是 %d\n", b ); }
运行结果为:
char变量c大小为1
char指针pchar大小为4
int变量i大小为4
int指针pint大小为4
pchar地址为00EFF82C
pchar内容为00EFF83B
c地址为00EFF83B
通过*pchar访问c的值为D
c的值为D
b 的值是 30
b 的值是 20
运算符的优先级确定表达式中项的组合。这会影响到一个表达式如何计算。某些运算符比其他运算符有更高的优先级,例如,乘除运算符具有比加减运算符更高的优先级。
例如 x = 7 + 3 * 2,在这里,x 被赋值为 13,而不是 20,因为运算符 * 具有比 + 更高的优先级,所以首先计算乘法 3*2,然后再加上 7。
下表将按运算符优先级从高到低列出各个运算符,具有较高优先级的运算符出现在表格的上面,具有较低优先级的运算符出现在表格的下面。在表达式中,较高优先级的运算符会优先被计算。
结合方向表示运算符计算时,先从那边开始计算,例如左到右表示先运算符左边操作数与运算符计算,然后再用运算符右边操作数参与计算。
单目运算符、双目运算符、三目运算符指的是该运算符有几个操作数,例如++是单目运算符,+ - * / 是双目运算符。
优先级 | 运算符 | 名称或含义 | 使用形式 | 结合方向 | 说明 |
---|---|---|---|---|---|
1 | [] | 数组下标 | 数组名[常量表达式] | 左到右 | |
()圆括号 | (表达式) | 函数名(形参表) | |||
. | 成员选择(对象) | 对象.成员名 | |||
-> | 成员选择(指针) | 对象指针->成员名 | |||
2 | - | 负号运算符 | -表达式 | 右到左 | 单目运算符 |
(类型) | 强制类型转换 | (数据类型)表达式 | |||
++ | 自增运算符 | ++变量 或 变量名++ | 单目运算符 | ||
- - | 自减运算符 | - -变量名 或 变量名- - | 单目运算符 | ||
* | 取值(即解引用)运算符 | *指针变量 | 单目运算符 | ||
& | 取地址运算符 | &变量名 | 单目运算符 | ||
! | 逻辑非运算符 | !表达式 | 单目运算符 | ||
~ | 按位取反运算符 | ~表达式 | 单目运算符 | ||
sizeof | 长度运算符 | sizeof(表达式) | |||
3 | / | 除 | 表达式 / 表达式 | 左到右 | 双目运算符 |
* | 乘 | 表达式*表达式 | 双目运算符 | ||
% | 余数(取模) | 整型表达式%整型表达式 | 双目运算符 | ||
4 | + | 加 | 表达式+表达式 | 左到右 | 双目运算符 |
- | 减 | 表达式-表达式 | 双目运算符 | ||
5 | << | 左移 | 变量<<表达式 | 左到右 | 双目运算符 |
>> | 右移 | 变量>>表达式 | 双目运算符 | ||
6 | > | 大于 | 表达式>表达式 | 左到右 | 双目运算符 |
>= | 大于等于 | 表达式>=表达式 | 双目运算符 | ||
< | 小于 | 表达式<表达式 | 双目运算符 | ||
<= | 小于等于 | 表达式<=表达式 | 双目运算符 | ||
7 | == | 等于 | 表达式==表达式 | 左到右 | 双目运算符 |
!= | 不等于 | 表达式!= 表达式 | 双目运算符 | ||
8 | & | 按位与 | 表达式&表达式 | 左到右 | 双目运算符 |
9 | ^ | 按位异或 | 表达式^表达式 | 左到右 | 双目运算符 |
10 | ▏ | 按位或 | 表达式▏表达式 | 左到右 | 双目运算符 |
11 | && | 逻辑与 | 表达式&&表达式 | 左到右 | 双目运算符 |
12 | ▏▏ | 逻辑或 | 表达式▏▏表达式 | 左到右 | 双目运算符 |
13 | ?: | 条件运算符 | 表达式1? 表达式2: 表达式3 | 右到左 | 三目运算符 |
14 | = | 赋值运算符 | 变量=表达式 | 右到左 | |
/= | 除后赋值 | 变量/=表达式 | |||
*= | 乘后赋值 | 变量*=表达式 | |||
%= | 取模后赋值 | 变量%=表达式 | |||
+= | 加后赋值 | 变量+=表达式 | |||
-= | 减后赋值 | 变量-=表达式 | |||
<<= | 左移后赋值 | 变量<<=表达式 | |||
>>= | 右移后赋值 | 变量>>=表达式 | |||
&= | 按位与后赋值 | 变量&=表达式 | |||
^= | 按位异或后赋值 | 变量^=表达式 | |||
▏= | 按位或后赋值 | 变量▏=表达式 | |||
15 | , | 逗号运算符 | 表达式,表达式,… | 左到右 |
请看下面的实例,了解 C 语言中运算符的优先级:
实例
#include <stdio.h> main() { int a = 20; int b = 10; int c = 15; int d = 5; int e; e = (a + b) * c / d; // ( ( 20 + 10 ) * 15 ) / 5 printf("(a + b) * c / d 的值是 %d\n", e ); e = ((a + b) * c) / d; // ( ( 20 + 10 ) * 15 ) / 5 printf("((a + b) * c) / d 的值是 %d\n" , e ); e = (a + b) * (c / d); // ( 20 + 10 ) * ( 15 / 5 ) printf("(a + b) * (c / d) 的值是 %d\n", e ); e = a + (b * c) / d; // 20 + ( (10 *15 ) / 5 ) printf("a + (b * c) / d 的值是 %d\n" , e ); return 0; }
运行结果为:
(a + b) * c / d 的值是 90
((a + b) * c) / d 的值是 90
(a + b) * (c / d) 的值是 90
a + (b * c) / d 的值是 50
再例如:
#include <stdio.h>
int main()
{
int a[5]={0,1,2,3,4};
int *p=a;
*p++=10;
printf("a[0]=%d,a[1]=%d,a[2]=%d,a[3]=%d,a[4]=%d\n",a[0],a[1],a[2],a[3],a[4]);
return 0;
}
运行结果为:
a[0]=10,a[1]=1,a[2]=2,a[3]=3,a[4]=4
解释:
int a[5]={0,1,2,3,4};
创建了一个拥有5个数的数组,数组名称与数组首地址为a。a[0]=0;…a[4]=4;
int *p=a;
定义一个指针指向数组a的首地址
*p++=10;
++运算符优先级高于*高于=
1、先执行++运算符。由于++运算符是左结合,则指针p在本条语句中不变,执行完本条语句后,指针向后移动一次。
2、再执行*运算符。程序通过指针p访问到数组的第0个元素即a[0]。
3、后执行=运算符。程序将10赋值给a[0]。
printf输出a0、a1、a2、a3、a4的值。
我们知道计算机执行程序指令是按顺序的方式执行的,也就是说,按照指定的顺序,一条指令一条指令的执行,执行完一条指定之后,再执行下一条指令。
当然现在很多CPU都是多核心、多线程的,并发执行多条指令,但对于同一个程序而言,CPU还是通过顺序的方式来执行指令的。 在C语言中程序执行时是按语句来顺序执行的,其中每一条语句都以分号结尾。
\
#include <stdio.h>
int main()
{
int a;
a=10;
printf("hello world%d\n",a);
return 0;
}
例如: 上面的每一条语句都是以分号结尾,语句可以是定义变量、初始化变量、任何表达式、调用的函数等。
可以这样理解:一条语句,就是程序执行的一个动作。 CPU是按顺序的方式执行语句,执行完当前语句之后,再执行下一条语句。 多条语句可以写在一行代码里,也可以将每一条语句书写为单独一行代码。 但是为了编程者能够方便的读写程序代码,通常将一条语句书写为单独的一行代码。
这种顺序往下执行的,最容易理解的语句就是C语言的顺序语句。
所谓条件语句就是只有满足一定条件才执行的语句,C语言所支持的条件语句有if语句和switch语句。
完整形式:
if(条件1)
{//满足条件1将执行
}
else if(条件2)
{//不满足前面条件,但满足条件2则执行
}
else if(条件3)
{//不满足前面条件,但满足条件3则执行
}
else
{//所有条件都不满足则执行
}
例如当我们想根据学生成绩进行评ABCD时,假设>90为A,>80为B,>60为C,否则为D。请看例程:
#include <stdio.h> int main() { int score; printf("请输入学生成绩:"); scanf("%d", &score); if (score > 90) { printf("成绩为A\n"); } else if (score > 80) { printf("成绩为B\n"); } else if (score > 60) { printf("成绩为C\n"); } else { printf("成绩为D\n"); } return 0; }
运行并输入100
后结果为:
请输入学生成绩:100
成绩为A
相信以上程序并不难理解。但是我们实际使用时,有时候并不需要那么多分支语句,可能就简单判断一下。这时就需要用到衍生的if语句
if(条件)
{//满足则执行
}
if(条件)
{//满足则执行
}
else
{//不满足则执行
}
if(条件1)
{//满足条件1则执行
}
else if(条件2)
{//不满足前面条件,但满足条件2则执行
}
同时,当{}中只有一条语句时,我们可以省略{}。例如之前程序可以重新写为:
#include <stdio.h> int main() { int score; printf("请输入学生成绩:"); scanf("%d", &score); if (score > 90) printf("成绩为A\n"); else if (score > 80) printf("成绩为B\n"); else if (score > 60) printf("成绩为C\n"); else printf("成绩为D\n"); return 0; }
但是不建议使用,因为这样写会导致阅读不便,同时当我们嵌套使用if语句时,会有else匹配错误的情况。例如:
#include <stdio.h> int main() { int a = 3; if (a > 5) { if (a < 10) { printf("a在5到10之间\n"); } } else { printf("a小于5\n"); } printf("ok\n"); return 0; }
程序运行结果为:
a小于5
ok
因为在该程序中,{}内都是只有一句语句,所以可以省略{}。改写之后程序为:
#include <stdio.h>
int main()
{
int a = 3;
if (a > 5)
if (a < 10)
printf("a在5到10之间\n");
else
printf("a小于5\n");
printf("ok\n");
return 0;
}
运行结果为:
ok
之所以程序运行结果不对,是因为else自动与最近的if语句匹配上,实际程序结构变成了:
#include <stdio.h>
int main()
{
int a = 3;
if (a > 5)
{
if (a < 10)
printf("a在5到10之间\n");
else
printf("a小于5\n");
}
printf("ok\n");
return 0;
}
就已经偏离了我们想要表达的意思,所以建议大家尽量不用省略{}符号。
除了if语句以外,C语言还有一种条件语句就是switch语句。相对于if灵活多变的形式而言,switch语句显得更刻板与标准化,这限制了它的使用(并不是所有条件都可以使用switch语句),但同时也给他带来了更高的运行效率。其基本形式为:
switch(总表达式)
{
case 常量1:语句1;
case 常量2:语句2;
...
case 常量3:语句n;
default:总语句;
}
执行逻辑是:
需要注意的是:
下面我们使用switch语句重写之前的判断成绩的程序:
#include <stdio.h> int main() { int score; printf("请输入学生成绩:"); scanf("%d", &score); switch (score / 10) //int数除以int数,结果仍然为int数,并且向下取整 { case 0: case 1: case 2: case 3: case 4: case 5:printf("学生成绩为D\n"); break;; case 6: case 7:printf("学生成绩为C\n"); break; case 8:printf("学生成绩为B\n"); break; case 9: case 10:printf("学生成绩为A\n"); break; } return 0; }
运行并输入70,结果为:
请输入学生成绩:70
学生成绩为C
指的是在一个条件语句中包含另一个条件语句,常用于较为复杂的判断场景。
示例:编写一个程序判断是否是闰年。
注释:闰年条件是:
参考例程:
#include <stdio.h> int main() { int year; printf("请输入年份:"); scanf("%d", &year); if ((year % 4 == 0) && (year % 100 != 0)) { printf("%d年是闰年\n",year); } else { if (year % 400 == 0) { printf("%d年是闰年\n", year); } else { printf("%d年不是闰年\n",year); } } return 0; }
运行并输入2000,结果为:
请输入年份:2000
2000年是闰年
运行并输入1921,结果为:
请输入年份:1921
1921年不是闰年
循环语句是指程序满足一定条件从而重复执行的控制语句。C语言中的循环语句有for、while、do while、goto。
循环语句中for具有较强的语句格式限制,其形式是:
for(初始化语句;条件语句;条件修改语句)
{//如果条件语句为真,则执行以下循环体
}
初始化语句、条件语句、条件修改语句可以由0条或多条语句构成,如果有多条语句,需要以逗号隔开。for语句逻辑是
举个例子:
#include <stdio.h>
int main()
{
int a;
int sum;
for(a=0,sum=0;a<10;a++) \\程序从a=0开始,运行到a=9时仍然执行循环体,直到a=10时,不执行循环,直接跳出
{
sum+=a;
}
printf("从0加到9结果为:%d\n", sum);
return 0;
}
运行结果为:
从0加到9结果为:45
练习:
1、在屏幕上显示由*组成的5行等腰直角三角形
参考程序
#include <stdio.h>
int main()
{
int i, j;
for (i = 0; i < 5; i++)
{
for (j = 0; j <= i; j++)
{
printf("*");
}
printf("\n");
}
return 0;
}
运行结果为:
*
**
***
****
*****
2、在屏幕上输出0到100的和。
参考例程:
#include <stdio.h>
int main()
{
int i, sum;
for (i = 0, sum = 0; i <= 100; i++)
{
sum += i;
}
printf("0-100的和为:%d\n", sum);
return 0;
}
运行结果为:
0-100的和为:5050
相对于for语句,while语句较为灵活多变。格式为:
while(条件)
{//如果条件为真则执行循环体
}
例如:在屏幕上显示01234
#include <stdio.h>
int main()
{
int i=0;
while(i<5)
{
printf("%d\n",i);
i++;
}
return 0;
}
运行结果为:
0
1
2
3
4
练习:
1、使用while重做:屏幕上输出由*组成的5行等腰直角三角形。
参考例程:
#include <stdio.h> int main() { int i, j; i = 0; while (i < 5) { j = 0; while (j <= i) { printf("*"); j++; } printf("\n"); i++; } return 0; }
运行结果为:
*
**
***
****
*****
2、使用while重做:计算0-100内的数的和。
参考例程:
#include <stdio.h>
int main()
{
int sum = 0;
int i = 0;
while (i <= 100)
{
sum += i;
++i;
}
printf("0-100的和为%d\n", sum);
return 0;
}
运行结果为:
0-100的和为5050
do while语句与while语句及其相似,其区别在于:while语句先判断条件,在考虑是否执行循环体;而do while语句先执行循环体,在判断是否进行下一次信循环,简单理解,dowhile循环体至少执行一次。实际上,do while语句在工程中用的不多。
do while语句形式:
do
{
}while(条件);
例如:计算5-10的和。
#include <stdio.h>
int main()
{
int i;
int sum;
i = 5;
sum = 0;
do
{
sum += i;
i++;
} while (i < 10);
printf("5-10的和为:%d\n", sum);
return 0;
}
运行结果为:
5-10的和为:35
练习:
1、使用do while语句重做:在屏幕上显示一个由*组成的5行等腰直角三角形。
参考例程:
#include <stdio.h> int main() { int i = 0, j = 0; do { j = 0; do { printf("*"); j++; } while (j <= i); printf("\n"); i++; } while (i < 5); return 0; }
运行结果为:
*
**
***
****
*****
2、使用do while语句重做:输出0-100的和。
参考例程:
#include <stdio.h>
int main()
{
int i = 0, sum = 0;
do
{
sum += i;
i++;
} while (i <= 100);
printf("0-100的和为%d\n", sum);
return 0;
}
运行结果为:
0-100的和为5050
break语句相信大家应该还没忘记,在条件语句中我们曾介绍过,break可以用于跳出switch语句。实际上,break语句的作用也正是用于跳出某一循环体。可以使用break语句跳出的有switch、for、while、do while。
例如:使用break+while语句计算0-100的和
#include <stdio.h> int main() { int i = 0, sum = 0; while (1) //永远循环,又称死循环 { sum += i; i++; if (i > 100) //如果i>100时,将强制跳出循环 { break; } } printf("0-100的和为%d", sum); return 0; }
运行结果为:
0-100的和为5050
continue作用与break极为相似,break用于跳出循环,continue则用于跳过本次循环进入下次循环,并且continue不能用于switch语句。
例如:计算0-100所有奇数和。
#include <stdio.h> int main() { int i, sum; for (i = 0, sum = 0; i <= 100; i++) { if (i % 2 == 0) //如果i为偶数,则跳过本次循环 { continue; } sum += i; } printf("0-100的奇数和为%d", sum); return 0; }
运行结果为:
0-100的奇数和为2500
goto语句严格来说并不是循环语句,应该称为无条件跳转语句。关于goto语句仅处于内容完整性考虑才介绍,实际使用时该语句极其危险。可以这么说,所有使用goto语句的程序都可以使用其他循环语句代替,并且goto语句极其危险不受控制,所以同学们可以不用看本小节。注意:goto语句本质不是只是程序跳转,所以不支持break语句。
goto语句使用格式:
语句1
语句2
标签1:
语句3
语句4
goto 标签1;
当程序运行到goto 标签1;时,直接无条件跳转到标签1处。
例如:使用goto语句计算0-100数的和。
#include <stdio.h>
int main()
{
int i = 0, sum = 0;
loop:
if (i <= 100)
{
sum += i;
i++;
goto loop;
}
printf("0-100的奇数和为%d", sum);
return 0;
}
运行结果为:
0-100的奇数和为5050
一般而言,C语言程序中for语句、while语句、break语句用的较多一点,相对而言do while、continue使用相对较少,goto语句几乎不使用。
那么我们程序到底选取哪种形式的循环语句呢,实际上,循环语句都可以互相转换,例如for(;条件;){}
就等价于while(条件){}
。但是由于for循环语句中,对于语句格式有着较为死板的要求,可以极大减少我们写程序时由于粗心而出现的错误,所以优先选择使用for语句,选取标准如下:
练习:
1、判断0-100有多少可以被7整除的数,并输出到屏幕。
分析:该程序从0-100遍历所有数,判断是否是7的整数。应采用for循环。
参考例程:
#include <stdio.h>
int main()
{
int num, i;
for (i = 0, num = 0; i <= 100; i++)
{
if (i % 7 == 0)
{
printf("%d是7的整数倍\n", i);
num++;
}
}
printf("0-100共有%d个7的整数倍数\n", num);
return 0;
}
运行结果为:
0是7的整数倍 7是7的整数倍 14是7的整数倍 21是7的整数倍 28是7的整数倍 35是7的整数倍 42是7的整数倍 49是7的整数倍 56是7的整数倍 63是7的整数倍 70是7的整数倍 77是7的整数倍 84是7的整数倍 91是7的整数倍 98是7的整数倍 0-100共有15个7的整数倍数
2、找到一个三位数abc,满足a3+b3+c3=abc。输出到屏幕上。
分析:由于我们并不确定要循环多少次,且循环条件并不是简单地比大小,所有采用while语句。
参考例程:
#include <stdio.h> int main() { int i = 100; int g, s, b; while (i < 1000) //最多判断到999 { b = i / 100; //百位 s = (i % 100) / 10; //十位 g = i % 10; //个位 if (b * b * b + s * s * s + g * g * g == i) { printf("找到该数字%d\n", i); break; //找到后就退出循环 } else { i++; } } return 0; }
运行结果为:
找到该数字153
函数是一组一起执行一个任务的语句。每个 C 程序都至少有一个函数—主函数 main() 。
我们可以把重复使用的代码、完成某项功能的代码划分为一个函数,当我们需要使用时,只需调用这个函数就可以了。
在主函数前定义的函数可以没有函数声明直接使用;在主函数后面定义的函数必需要函数声明才能在函数中调用,函数声明告诉编译器函数的名称、返回类型和参数。函数定义提供了函数的具体执行的语句。
函数还有很多叫法,比如方法、子例程或程序,等等。
C 语言中的函数定义的一般形式如下:
函数返回类型 函数名称( 形参类型1 形参名称1,形参类型2 形参名称2 )
{
语句;
return 变量(同函数返回类型);
}
在 C 语言中,函数由一个函数头和一个函数主体组成。下面列出一个函数的所有组成部分:
函数返回类型:一个函数可以返回一个值。函数返回类型 是函数返回的值的数据类型。有些函数执行所需的操作而不返回值,在这种情况下,函数返回类型 是关键字 void。例如我们一直写的主函数返回值一直是int类型。
函数名称:这是函数的实际名称。函数名和后面的参数列表一起构成了函数签名。
形参参数:形参参数就像是占位符。当函数被调用时,程序会向形参传递一个值,这个值被称为实际参数,这时函数形参就有了具体值了。参数列表包括函数参数的类型、顺序、数量。参数是可选的,也就是说,函数可能不包含参数。
函数主体:函数主体包含一组定义函数执行任务的语句。
示例:
#include <stdio.h>
void fun();
int main()
{
fun();
fun();
return 0;
}
void fun()
{
printf("hello world\n");
}
运行结果为:
hello world
hello world
上述程序因为fun函数在主函数之后定义,所以在使用fun()函数之前必须有fun函数的声明,否则编译器会因为找不到fun函数而报错。
下面介绍形参与实参的区别,会有一点关于指针的知识。
形参指的是定义或声明函数时,仅用于占位、并没有实际内存的变量,可以理解为形式参数。例如int fun1(int a,double b);
中形式参数有int类型的a和double类型的b。形参只有函数被调用时,系统才会分配内存,并且在退出函数时,销毁该内存。
相对应的,实参指的是在调用函数时,按照形式参数的个数与顺序,传递给被调用函数的一些具体的值或变量。传递参数时,仅仅是将实参的数据复制一份放到形参内存中,所以子函数中形参的改变并不影响实参的值。
请看一下程序:
#include <stdio.h> void fun(int a, int b) { a = 10; b = 11; printf("子函数中a=%d,b=%d\n", a, b); } int main() { int a = 3, b = 5; fun(a, b); printf("主函数中a=%d,b=%d\n", a, b); return 0; }
运行结果为:
子函数中a=10,b=11
主函数中a=3,b=5
那么我们能否通过子函数从而修改主函数中变量的值呢,答案是肯定的,通过指针修改!指针在C语言中妙用无穷,是灵魂所在。虽然我们不能直接修改主函数中变量的值,但是我们可以将主函数中变量的地址传递给子函数,然后子函数通过该地址访问并修改主函数中的变量,也就达到我们想要的效果了。例如一下程序:
#include <stdio.h> void fun(int *pa, int *pb) { *pa = 10; *pb = 11; printf("子函数中a=%d,b=%d\n", *pa, *pb); } int main() { int a = 3, b = 5; fun(&a, &b); printf("主函数中a=%d,b=%d\n", a, b); return 0; }
运行结果为:
子函数中a=10,b=11
主函数中a=10,b=11
上述程序中,主函数中变量被子函数修改了。一个函数可以有0或1个返回值,但是通过传递指针修改主函数变量的方式扩展函数返回值的数量,例如上述程序相当于有两个返回值a和b。
之前我们写过关于计算0到100累加和的程序,我们可以使用函数将其封装起来。
#include <stdio.h> //输入: // 参数1:begin累加初值 // 参数2:end累加终值 //输出:累加值 //作用:返回从begin到end的数的累加值 int fun(int begin, int end) { int i, sum; for (i = begin, sum = 0; i <= end; i++) { sum += i; } return sum; } int main() { int a; a = fun(1, 5); printf("1-5的和为%d\n", a); printf("0-100的和为%d\n", fun(0, 100)); return 0; }
运行结果为:
1-5的和为15
0-100的和为5050
在封装完成之后,我们想要计算从x到y的累加和就可以直接使用fun(x,y)来计算了,这样大大提高了代码可重复利用率。一般的我们会将高度重复的代码、或者实现了某一功能的代码封装成一个函数,然后再其他函数中调用。
本小节可做课外知识了解一下。与我们常见的函数不同,可变参函数的意思就是该函数的参数类型可变,参数个数可变。实际工程中我们自己写可变参函数较少,但是我们却经常使用,常见的可变参函数例如printf(),scanf()。看完本节,我们也可以写出这样的可变参函数。
首先可变参函数相关内容并不是在标准输入输出库(stdio.h)中定义,而是保存在标准参数库(stdarg.h)中,所以只要想使用可变参就需要在文件开头包含#include <stdarg.h>
。
可变参函数定义形式如下:
函数返回值类型 函数名称(强制形参类型 强制形参名称,...) { } //范例 //输入: // 参数1:需要计算的数据个数 // 参数2:double类型形参 // 参数3:double类型形参 // ... //输出:double类型的所有形参累加和 //作用:计算除第一个数以外的其他参数累加和 double add(int n, ...) { double sum = 0.0; va_list argptr; va_start(argptr, n); // 初始化argptr for (; n > 0; --n) // 对每个可选参数,读取类型为double的参数, sum += va_arg(argptr, double); // 然后累加到sum中 va_end(argptr); return sum; }
如此看来,可变参函数与普通函数定义相似:
void va_start(va_list argptr, int n);
va_start用于对参数初始化,从系统中读取n个数据保存至参数列表argptr中。上述例程先定义参数列表argptr,之后使用va_start读取n数据保存在参数列表argptr中。type va_arg(va_list argptr, type);
va_arg用于从从参数列表读取一个type类型变量。void va_end(va_list argptr);
va_end用于释放argptr参数列表的内存空间,因为argptr本质是指针,需要手动内存释放,后续指针专辑会专门讲解,切记使用完参数列表后需要va_end(argptr);
释放内存。上述例程完善一下为:
#include <stdio.h> #include <stdarg.h> //输入: // 参数1:需要计算的数据个数 // 参数2:double类型形参 // 参数3:double类型形参 // ... //输出:double类型的所有形参累加和 //作用:计算除第一个数以外的其他参数累加和 double add(int n, ...) { double sum = 0.0; va_list argptr; va_start(argptr, n); // 初始化argptr for (; n > 0; --n) // 对每个可选参数,读取类型为double的参数, sum += va_arg(argptr, double); // 然后累加到sum中 va_end(argptr); return sum; } int main() { printf("1+3+3=%lf\n", add(3, 1.0, 3.2, 3.2)); printf("2+2=%lf\n", add(2, 2, 2)); return 0; }
运行结果为:
1+3+3=7.400000
2+2=0.000000
细心地读者看到了2+2=0.000000是不对的,为什么会导致这种问题呢,原因很简单,参数列表不会对形参进行类型转换,它把int类型的参数2错误的用va_arg(argptr,double);
按照double数据类型的格式读取出来了,导致出现问题,如何解决?
add((int)2,(double)2,(double)2);
add(2,2.0,2.0);
修改之后运行结果为:
1+3+3=7.400000
2+2=4.000000
类似于递归证明,函数也可以递归调用,当函数直接或间接调用自己本身时,就构成了递归调用。例如:
void fun()
{
fun();
}
以上函数即为最简单的递归调用,如果他被调用将会陷入死循环。下面用实例演示递归函数的用处与作用。
例如:计算从0-n的累加和
#include <stdio.h> //输入:n表示累加终值 //输出:累加值 //作用:返回从0到n的数的累加值 int fun(int n) { if (n > 1) return n + fun(n - 1); else return 1; } int main() { printf("0-100的累加和为%d\n", fun(100)); return 0; }
运行结果为:
0-100的累加和为5050
由于递归函数理解较为复杂,这里稍微多费口舌介绍一下具体运行过程:
printf("0-100的累加和为%d\n",fun(100));
先执行fun(100)
。return 100+fun(100-1);
先要计算该语句中fun(100-1)的值。return 99+fun(99-1);
先要计算该语句中fun(99-1)的值。return 2+fun(2-1);
先要计算该语句中fun(2-1)的值。return 1;
return 1;
。return 2+1;
。return 99+(98+...+1);
。return 100+(99+...+1);
。递归函数适用于函数变量较少,函数体简单,递归次数有限且不算太多的情况。递归函数可以帮助我们理清思路,使得复杂的问题得以简化。下面请看经典例题汉罗塔。
练习:汉罗塔
一次只能移动一个圆盘,并且大圆盘不能放在小圆盘上,要求将A柱汉罗塔移动到C柱,且顺序保持不变。
对于该问题我们可以抽象成表格
当只有一个罗盘时:
A | B | C | 方法 | |
---|---|---|---|---|
第1步 | 1 | – | ||
第2步 | 1 | A->C |
当有两个罗盘时:
A | B | C | 方法 | |
---|---|---|---|---|
第1步 | 12 | – | ||
第2步 | 2 | 1 | A->B | |
第3步 | 1 | 2 | A->C | |
第4步 | 12 | B->C |
当有三个罗盘时
A | B | C | 方法 | |
---|---|---|---|---|
第1步 | 123 | – | ||
第2步 | 23 | 1 | A->C | |
第3步 | 3 | 2 | 1 | A->B |
第4步 | 3 | 12 | C->B | |
第5步 | 12 | 3 | A->C | |
第6步 | 1 | 2 | 3 | B->A |
第7步 | 1 | 23 | B->C | |
第8步 | 123 | A->C |
当罗盘个数逐渐上涨时,游戏步骤呈指数递增,那么我们如何使用编写程序完成该任务呢。
递归分析:
#include <stdio.h> //输入: // 参数1:x表示此时罗盘所在柱子 // 参数2:y表示此时空的柱子 // 参数3:z表示目标柱子 // 参数4:搬运罗盘的个数 //输出:无 //作用:将n个罗盘从x柱子搬运到z柱子的步骤 void fun(char x, char y, char z, int n) { if (n == 1) //如果只有一个罗盘,那就直接移过去 { printf("%c->%c\n", x, z); } else //如果有多个罗盘 { fun(x, z, y, n - 1); //先将前面1到n-1个罗盘搬到y上 printf("%c->%c\n", x, z); //再将第n个罗盘搬到z上 fun(y, x, z, n - 1); //再将1到n-1个罗盘搬到z上 } } int main() { int n; printf("请输入罗盘个数:"); scanf("%d", &n); fun('A', 'B', 'C', n); return 0; }
运行并输入1,结果为:
请输入罗盘个数:1
A->C
运行并输入2,结果为:
请输入罗盘个数:2
A->B
A->C
B->C
运行并输入3,结果为:
请输入罗盘个数:3
A->C
A->B
C->B
A->C
B->A
B->C
A->C
当有n个罗盘时,想要经历2n-1个步骤。这就是经典的汉罗塔递归程序,如果有点难以理解可以多看几遍该例程。
除了汉罗塔以外,图像处理中种子生长与数据结构中二叉树对于递归调用的应用也较多,后续涉及到数据结构的章节再详细介绍。
当我们学会了函数并了解函数的作用之后,回头再开之前的一直在用的printf与scanf函数。
printf函数原型是int printf(const char *format, ...);
,const char *表示不可改变char类型指针,指的是无法通过指针来改变目标地址内容,后面三个点表示可变参数。其返回值是打印字符个数,例如printf("asdf");
语句中,该函数返回值为3。
scanf函数原型是Int scanf (const char *format,...);
,使用示例:scanf("%d %d",&a,&b);
函数返回值为int型。
这样看来我们可以通过读取scanf函数返回值从而进行入参检查。
例如:
#include <stdio.h>
int main()
{
int s1, s2, s3;
printf("欢迎使用成绩录入系统\n");
printf("请输入学生成绩:");
while (scanf("%d %d %d", &s1, &s2, &s3) != 3)
{
getchar();
printf("\n输入有误,请重新输入:");
}
return 0;
}
运行并输入10 20 30
,结果为:
欢迎使用成绩录入系统
请输入学生成绩:10 20 30
ok
运行并输入80 c 90;,结果为:
欢迎使用成绩录入系统
请输入学生成绩:80 c 90
输入有误,请重新输入:
通过scanf函数的返回值,我们就实现了不同数据之间的入参检测。
C 标准库提供了大量的程序可以调用的内置函数。我们并不需要了解所有库函数,更不需要学习所有库函数具体是怎么实现的,我们只需要有个简单的印象,方便在我们想要时直接调用即可。例如常见的
通常存在于stdio.h中,一般用于从键盘鼠标输入,输出到文件或屏幕。
常用函数有
printf()函数
scanf()函数
putchar()函数
getchar()函数
getch()函数
puts()函数
gets()函数
文章第二部分–标准输入输出介绍过,这里就不多赘述了。
还有几个常用的输入输出函数与字符串有关
sprintf()函数
sscanf()函数
通常存在于stdio.h文件中,用于读取或写入文件,可以看作特殊的输入输出,其对象是文件。
fopen()函数
fprintf()函数
fscanf()函数
clearerr()函数
fclose()函数
fget()函数
fgets()函数
fputc()函数
fputs()函数
fseek()函数
fwrite()函数
fread()函数
通常存在于stdlib.h文件中,用于对电脑内存控制,直接通过指针访问与控制内存。
malloc()函数
calloc()函数
realloc()函数
free()函数
文章特殊部分–指针专辑–指针与动态内存将详细介绍介绍
通常存在于stdlib.h文件中,用于生成一个随机数。
srand()函数
srand(n);
n为int数srand(time);
则该随机数种子也不停变化。rand()函数
int a=rand();
通常存在于string.h中,用于对字符串实现各种操作。
strcat()函数
strcmp()函数
strcpy()函数
strlen()函数
memchr()函数
memcmp()函数
memcpy()函数
memmove()函数
memset()函数
strncat()函数
strchr()函数
strncmp()函数
strcoll()函数
strncpy()函数
strcspn()函数
strerror()函数
strpbrk()函数
strrchr()函数
strspn()函数
strstr()函数
strtok()函数
strxfrm()函数
通常存在于math.h文件中,主要是对一些常用数学运算符号和数学公式的封装。
sin()函数
cos()函数
tan()函数
asin()函数
acos()函数
atan()函数
atan2()函数
sinh()函数
cosh()函数
tanh()函数
*原型:double tanh (double);
frexp()函数
ldexp()函数
modf()函数
log()函数
log10()函数
pow()函数
powf()函数
exp()函数
sqrt()函数
round()函数
ceil()函数
floor()函数
abs()函数
fabs()函数
cabs()函数
frexp()函数
ldexp()函数
modf()函数
fmod()函数
isalnum()函数
isalpha()函数
iscntrl()函数
isdigit()函数
isgraph()函数
islower()函数
isprint()函数
ispunct()函数
isspace()函数
isupper()函数
isxdigit()函数
tolower()函数
toupper()函数
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。