前言
本文内容主要为C语言中的基础知识,主要是为了让各位读者能对C语言有一个大概的整体认识,以便后续的进一步学习。虽然说是C语言的基础知识,但内容还是比较丰富的。若你从未接触过编程语言,学完本文一定会有巨大收获。若你已经学过C语言,也可以像看小说一样,简单过一遍,查缺补漏。本文有2万五千字,篇幅较长,建议收藏食用。
目录
一、什么是C语言
1.感性理解
C语言是干什么的?是编程写代码的,编程写代码的目的是告诉计算机它要执行的任务是什么,也就是要和计算机沟通。那如何和计算机沟通的?直接用汉语跟它说吗?它肯定是读不懂你的意思的。那应该用什么跟它沟通呢?使用编程语言,C语言就是这样一个与计算机沟通的编程语言。
2.计算机语言的发展过程
但只有这样的感性理解是不够的。我们还需要简单了解一下计算机语言的发展过程。
计算机只认识机器语言。在早期,计算机编程使用的就是机器语言。机器语言就是0和1排列组合而成的一段序列,每一段序列表示着某种操作,如01010111......。但很明显,使用这样的机器语言编程实在是太反人类了,而且每一种机器其对应的机器语言是不一样的,这意味着换个机器,就得重新写一遍代码。因此,后来出现了汇编语言以及汇编语言编译器,使用如sub,mov,jmp,loop等助记符其来进行编程。然后汇编语言编译器将其转换成当前机器对应的机器语言(也可以由硬件直接转换),以便计算机执行。但使用汇编语言进行编程体验仍然不够好,随后又出现了B语言这样的中级语言及其编译器,然后又出现了C语言、C++、Java、Python等高级语言及其对应的编译器。使用C语言这样的高级语言就使得编写代码不再是非常非常复杂的事情了。不过计算机只认识机器语言,所以使用C语言编写的代码其实是计算机不认识的东西。因此需要对其翻译,也就是转换。这个转换工作就由各种编译器来做,对于C语言,它首先被转换成汇编语言,然后将汇编语言转换成当前机器对应的机器语言。至于为什么不直接转成机器语言也非常简单,因为已经有了汇编做这样的到机器语言转换,那就没必要重复造轮子了,而且将高级语言直接转换成机器语言实在是太繁杂了,是没必要的。
二、第一个简单的C语言程序
#include <stdio.h>
int main()
{
printf("Hello world!");
return 0;
}
上述代码是一个简单的C程序代码,C语言编写的源代码保存在后缀(扩展名)为.c的文件中,称之为源文件。下面来对其进行简单的分析:
1.printf("Hello world!\n");
它是C语言里的语句。语句是C语言中程序执行的最基本的执行单位,也就是执行某个操作的一段代码,C语言中的语句必须以英文的分号结尾。C语言中一个完整的程序就是由一个或多个语句组成的。这里printf("Hello world!");语句执行的操作就是向屏幕打印Hello world!。
2.int main(){ xxx }
这是一个C语言程序的入口,称之为主函数,每个C程序必须有且仅有一个。C程序运行起来后将会从主函数的第一条语句开始依次向后执行。主函数中末尾的return 0;语句是默认的写法,这里受限于知识面只需要记住形如这样的代码叫函数、main()是主函数以及其末尾要写return 0;就行。
3.#include <stdio.h>
这里使用了printf(),printf()并不是C语言本身提供的东西,因而不能直接使用它。要使用printf()就需要在整个代码的开头带上#include <stdio.h>。目前只需要知道如果要使用printf()就必须在代码开头带上#include <stdio.h>就行了。
4.printf()的简单使用
printf()的作用就是将一段内容按照特定格式显示到屏幕上。例如printf("Hello world!");就是将Hello world!打印到屏幕上。
printf("Hello %s%d", "gentleman",1);就是将Hello gentleman1打印到屏幕上。其中%s%d是转换说明符,可以简单理解成在第一个参数"Hello %s%d"中的第一个转换说明符%s被后续的第二个参数"gentleman"替换了,第二个转换说明符%d被后续的第三个参数1替换了。
printf()的转换说明符非常多,这里了解几个常见的,不需要死记硬背,混个眼熟就够了。
转换说明符 | 对应的参数 |
---|---|
%d | 形如1,3,5这样的整数 |
%c | 形如使用''括起来的内容,如'a' |
%s | 形如使用""括起来的内容,如"abcdefg" |
%f | 形如3.14f这样的小数 |
%lf | 形如3.14这样的小数 |
三、基本数据类型介绍
char | 字符数据类型 |
short (int) | 短整型 |
int | 整型 |
long (int) | 长整型 |
long long (int) | 更长的整型 |
float | 单精度浮点数 |
double | 双精度浮点数 |
long double | 更长的浮点数 |
char、short、int、long、long long为整数类型,其中short、long以及long long其实是省略了int的写法。float、double、long double为小数类型。
此外,对于整数类型可以使用unsigned和signed来标识该类型是有符号整数还是无符号整数(不存在负数)。整数类型中除了char以外默认是有符号整数,char是否有符号是不确定的。
四、计算机中的数据单位
计算机在处理数据时,处理的是二进制数据。
什么是二进制数据?我们平时使用的数据是十进制数据,其特点就是逢10进1。二进制数据与之类似,是逢2进1。例如十进制数3,对应的二进制数就是11,它有两个二进制位。因此计算机中最小的数据单位便是一个二进制位,称之为比特位(bit)。此外,还有许多其他的数据单位。下面列举了常见的数据单位及其相互之间的转换关系。
数据单位 | 转换关系 |
---|---|
Byte(字节) | 1Byte=8bit |
KB | 1KB=1024Byte |
MB | 1MB=1024KB |
GB | 1GB=1024MB |
TB | 1TB=1024GB |
PB | 1PB=1024TB |
三、基本数据类型的大小
C语言中可以使用sizeof操作符来获得某种类型的大小或者某个值对应的类型大小,其数据单位为Byte(字节),其结果是一个无符号整数。利用sizeof操作符便可以获取数据类型的大小。可以使用如下代码得到各个基本数据类型的大小。
#include <stdio.h>
int main()
{
printf("char:%zu\n", sizeof(char));
printf("short:%zu\n", sizeof(short));
printf("int:%zu\n", sizeof(int));
printf("long:%zu\n", sizeof(long));
printf("long long:%zu\n", sizeof(long long));
printf("float:%zu\n", sizeof(float));
printf("double:%zu\n", sizeof(double));
printf("long double:%zu\n", sizeof(long double));
return 0;
}
在VS2019中运行结果如下:
这里有两处不太符合常理的地方。long是长整型,为什么其大小与int相同而不是大于它呢?long double为什么又和double大小相同呢?
C语言中其实并没有规定long一定要比int大、long double一定要比double大。在C语言标准中只规定了short的大小不能超过int的大小,long的大小不能小于int的大小,long double的大小不能小于double的大小。
此外,int的大小其实也是不确定的,有时int的大小只有16个比特位,即2个字节。不过目前大部分机器上int的大小都是4个字节,可以认为int的大小就是4个字节。
五、数据类型的意义
数据类型决定了数据在内存中开辟的空间大小,也决定了如何看待内存空间中的数据。
决定了开辟的空间大小好理解,决定了如何看待内存空间中的数据又是什么意思呢?
在前面可以发现float为4字节,int为4字节,它们有同样大小的内存空间。如果其中的数据都使用同样的组织形式,那int和float岂不是一样的吗?但我们知道它们其中一个表示浮点数,一个表示整数,是不一样的。也就是说它们的数据在内存中的组织形式是不一样的,也就是它们看待内存空间这种的数据的方式是不一样的。
六、常量
1.什么是常量
常量就是整个C程序运行过程中值不会发生改变的数据。
2.C语言中有哪些常量
(1)字面常量
我们在编写C语言代码的时候,直接写出来的值就是字面常量。例如:
int main()
{
2; //int类型的字面常量
2l; //带上l/L为long类型的字面常量
2ll; //带上两个l为long long类型的字面常量
2lu; //带上u表示无符号,这里即为unsigned long类型的字面常量
3.14; //double类型的字面常量
3.14f; //float类型的字面常量
3.14l; //带上l为long double类型的字面常量
'c'; //字符常量
"oh"; //字符串常量
return 0;
}
(2)#define定义的标识符常量
定义方法:#define 标识符名称 字面常量
如#define PI 3.14,在这句话之后的PI就是字面常量3.14。这样做可以让一些字面常量具有更明确的意义。
#define PI 3.14
int main()
{
PI; //3.14
return 0;
}
(3)枚举常量
有一些数据是可以枚举出来的,如性别只有男和女,星期只有周一到周日。这样的数据都是描述同一类事物且能枚举出来的。C语言中提供了枚举类型,枚举类型的每一个可能取值就是一个枚举常量。如下,使用enum关键字自定义了一个枚举类型enum week,{}中的就是枚举常量。
enum week
{
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
};
七、变量
1.什么是变量
变量与常量相反,是在整个程序运行当中可以发生改变的值。
2.如何定义变量
定义变量的格式:数据类型 变量名称 [= 变量的初始值];
int main()
{
char ch = 'a';
int a = -10;
unsigned int b = 10u;
float f = 1.2f;
return 0;
}
定义变量给定变量的初始值并不是必须的,即下面也是定义了变量
int main()
{
char ch;
int a;
unsigned int b;
float f;
return 0;
}
不过定义变量时给定变量的初始值,即对变量初始化可以给我们一种确定性,能让我们明确知道这个变量定义出来时装的是什么数据,否则其中的数据可能会不符合我们的预期。故定义变量时最好对其初始化。
此外也可以一次定义多个变量,如:
int main()
{
int a, b;
char ch1 = 'a', ch2;
double d1 = 1.1, d2 = 3.14;
return 0;
}
3.变量的命名
C语言中变量的命名规则如下:
- 由字母、数字和下划线组成
- 变量的第一个字符不能是数字
- 变量名区分大小写
- 不能使用关键字作为变量名
合法的变量名如cnt,_score,num1。非法的变量名如1sum,num-1,int等。
这些是变量名的硬性要求,是必须遵守的。
此外还有一些非硬性的约定。
如变量名要能见名知意,以便交流和维护。如cnt一眼就知道是count的缩写,表示这个变量是用来计数的;sum一看就知道这个变量保存的是某些数据的求和结果;ch一看就知道是char的缩写,表示这个变量存的是一个字符。
又如变量命名采用驼峰命名法进行命名或者采用蛇形命名法等命名方法进行命名。
驼峰命名法要求除第一个单词的首字母之外,后续每个单词的首字母都要大写,如totalCount,personInfo。
蛇形命名法要求单词之间使用下划线_进行分隔,如total_count,person_info。
4.变量的分类
变量可以简单分成局部变量和全局变量。
局部变量就是在函数内定义的变量,全局变量就是不在函数内定义的变量。
int global;
int main()
{
int local;
return 0;
}
上述代码中global不在形如int main(){xxx}的东西内定义,是全局变量,而local则在其内部定义,是局部变量。其中全局变量定义时不初始化默认为0值,局部变量不初始化为随机值。
5.变量的使用
变量的使用只需要将变量名写在语句当中,这样该变量就将参与语句的执行计算。例如下面为一个简单的求和程序:
#include <stdio.h>
int main()
{
int num1 = 0;
int num2 = 0;
int sum = 0;
printf("Plese input two integer number:");
scanf("%d%d", &num1, &num2);
sum = num1 + num2;
printf("sum = %d\n", sum);
return 0;
}
printf()是输出函数,scanf()是与之对应的输入函数。使用scanf()可以让程序从键盘中读取数据,使用它也需要#include <stdio.h>。
这里scanf("%d%d",&num1,&num2);的含义是读取两个整数,将读到的两个值放入到变量num1和num2中。num1和num2前面带上了&操作符。这个操作符能获取某个对象的地址,也就是这个对象在内存中的哪里,传给scanf()的&num1和&num2也就是告诉scanf()读取到的值要放到哪里。在这里能不能不带&直接用num1呢?这是不行的,直接用num1使用的是num1中存储的值0,scanf()不需要知道num里存的是什么,而是num1在哪里,以便它将数据放入其中。
此外,也可以使用赋值操作符=修改变量中的内容。
6.变量的作用域与生命周期
(1)变量的作用域
变量的作用域描述的是一个变量,在我们编写代码时,可以在怎样的范围内直接使用它。
例如,在上述代码中定义了两个局部变量a和b。然后去使用a和b。但编译器报错提示变量b是未定义的,因为使用b的时候已经在其作用域范围之外了,故b不可使用。那变量的作用域大概是怎样的范围呢?
对于局部变量而言,其作用域就是其所在的局部范围内,也就是从它的定义位置处开始一直到其所处的那一级别的{}的收括号}为止。
对于全局变量而言,其作用域是整个项目工程,也就是哪都能用,也可以跨文件使用(一个C程序一般不止一个.c的源文件)。
作用域级别相同的变量中变量名不能重复,如
此外,若有多个变量名相同的不同变量的作用域重叠,内层作用域中的变量将隐藏外层作用域中的变量,例如
(2)变量的生命周期
变量的生命周期描述的是程序运行起来后该变量的存在的一段时间,即在程序运行过程中从从变量创建出来到起被销毁的一个时间段。
前面说过C程序会从主函数的第一条语句开始依次向后执行各个语句。当执行到局部变量的定义时该局部变量就被创建,其生命周期也由此开始,最后执行到该局部变量的作用域之外时,该局部变量被销毁,其声明周期也就随之结束。
对于全局变量而言,它定义在函数外部,似乎不会被程序执行,所以不会被定义创建出来?其实不然,对于全局变量,它们会在程序运行起来之前就被创建好,然后才会开始执行主函数的第一条语句,此后一直到程序结束,它们才会被销毁。也就是说全局变量的生命周期是整个程序的生命周期。
7.变量的声明
上述代码中定义了一个全局变量a,然后试着运行程序,结果编译器报错提示a未声明。
这是什么意思呢?不是说全局变量的作用域是整个工程吗?为什么在这里却不能用呢?
其实不是不能用,C语言中所有的变量都必须在使用之前声明。在这里,因为编译器是从上往下检查的,当其检查到printf("%d",a);时,发现你使用了一个叫a的变量,然而编译器在其前面的检查过程中没有发现你声明过叫a的全局变量,因而提示无法使用。
对于这种情况,可以在使用该变量之前进行变量的声明。那如何进行声明呢?
声明只需要在要使用之前写下该变量的类型及名称即可。但这样的写法是不规范的。
聪明的你肯定能发现问题,这的声明非常像是定义了一个全局变量。其实变量的定义本身也是一种声明,也就是说int a;这样的语句既是声明又是定义,例如:
根据编译器的报错信息,我们可以发现它是认为这里是定义声明了一个局部变量a且没有对其初始化,然后试图使用未初始化的局部变量a,所以它报错。
那规范的声明是什么样的呢?
要在此基础上带上extern关键字,该关键字将会告诉编译器,有是怎样类型的叫什么的变量已经在别处定义好了,不需要再定义了。
此外,建议将变量的声明放在整个代码中的头部位置。
八、数组
假设有这样的一个场景,需要程序存储一个班级所有学生的学号。那应该如何写程序呢?定义几十个int类型的变量吗?那也太麻烦了。
对于这样的场景,就可以使用数组,数组是C语言提供的一种构造类型,它是一组相同类型元素的集合。
1.数组的定义
格式如下:
int main()
{
//arr为数组名,[]表示其为一个数组,[]中的数字10表示其有10个元素
//剩余的部分int就是该数组的元素类型
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int arr2[] = { 0,1 };
int arr3[10] = { 1,2 };
return 0;
}
如上,定义了一个拥有10个元素的数组arr,每一个元素的类型为int,并将其所有的10个元素都初始化。arr2没有指定元素个数,但进行了初始化,编译器可以由此推断出数组的大小,即arr2的元素个数会被推断为2。
此外,也可以进行不完全初始化,如arr3,其元素个数为10,但只将其前两个元素初始化了,此时剩余的元素会默认被初始化为0。但注意,并不是说数组元素不初始化会自动变为0。局部变量不初始化,其存储的值是随机值。
另外,定义数组变量时,[]中的表达式的值必须是一个常量。不过在C99标准之后允许使用变量来指定数组的元素个数,但此时不能在定义的时候初始化。
什么是C99标准呢?C语言其实就是一套语法规定,因此它就需要有一个标准的规定。这种标准的规定就是标准C。而这样的规定并不是一成不变的,每隔一段时间,就会出现新的C标准。C89就是1989年出台的C标准,C99就是1999年出台的C标准。此外,除了标准C,各个平台也对标准C做了各自的扩展。但为了保证编写的C代码能在不同的平台下编译成可执行程序,最好只使用标准C的内容。
C99往后的标准并没有流行起来,许多编译器对其支持的都不是很好,许多编译器都不能使用变量来指定数组的元素个数。
2.数组的下标
C语言规定数组的每一个元素都有一个下标,且下标从0开始依次增大。例如,若数组元素个数为10,在下标范围为0~9。访问数组元素时使用元素对应的下标来访问。
3.数组的使用
C语言中数组不能整体进行使用,只能使用下标访问其具体的某个元素来使用。例如:
此外,在使用数组访问其具体的某个元素时,[]中的内容可以是变量。例如:
arr[2]就是访问数组的第三个元素,也就是3。
需要注意,使用数组下标进行访问时不要越界访问,即在数组元素个数只有10个的情况下,访问时使用的下标只能是0~9的范围内的整数,不能是-1或者10等范围外的整数。此外,对于数组的越界访问,程序并不一定能够检测出来并报错提醒。
4.二维数组
形如int arr[10];这样的数组称之为一维数组,它就如同一维坐标系只需要一个值(即一个下标)就能找到对应的元素,逻辑上可以将其理解成数据排成了一行。
形如int arr[8][10]这样的数组称之为二维数组,如同二维坐标系需要两个值(即两个下标)才能确定一个元素,逻辑上可以将其理解成数据排成了8行10列。同样的还有三维数组等。其定义和使用与一维数组类似。
九、字符串
1.字符
屏幕上显示出来的各种符号就是字符,例如编写C语言代码时你看到的int a = 1;中所有的符号如i、n、t、空格、=、1、;等全都是字符。
然而我们知道,在C语言看来,int a = 1;是一条语句,含义是定义一个变量a,并将其初始化为1。也就是说C编译器编译时将C源文件中的字符1理解为一个整型数据,而不是字符。那在C语言中如何表示一个字符呢?
在C语言中使用''这样的一对单引号括起来的就是字符,也就是说'1'就是字符1。
C语言中常用char类型来保存字符数据。
但此时问题又来了,前面不是说char是整型类型吗?怎么又说它保存字符数据呢?
首先,先谈一个问题,在屏幕上看到的这些字符图案是给计算机看的吗?显然不是,是给我们人看的,那计算机需要关心这样的图案长什么样吗?计算机不需要关心,是我们人需要关心。但这里就带来了问题,计算机终究是要为人服务的,是要给人看的,那它必须能够显示这样的图案。所以在计算机中存在有各种映射表,映射表中存有字符编号以及与之对应的字符。当需要显示字符时,计算机根据字符编号来查找这张表来决定在屏幕上显示怎样的字符图案。也就是说程序中保存的字符数据其实是一个字符编号,也就是是一个整型,所以说char是一个整型数据。
通常情况下,C程序使用的这张映射表是ASCII表,其映射关系如下:
这张表不需要全部记住。只需要知道某一些字符的字符编号(ASCII码)就行了,一般只需要记住'0','A','a'这三个的ASCII码48,65,97,这样所有的字母字符和数字字符的ASCII码值也就全部知道了,因为数字字符整体是连续的,大写字母字符整体是连续的,小写字母字符整体也是连续的(注意,大小写字母不是连着排在一起的)。
值得一提的是在C语言中''括起来的常量叫整型字符常量,其类型为int,而不是char,而在C++中''括起来的常量的类型就是char。
2.转义字符
不难看出在C语言解析我们写的代码(一堆字符)的时候,'有着特殊的含义,即用来表示一个字符常量。那问题来了,如何表示一个'字符呢?使用'''吗?很明显,简单测试就能发现是不行的,'''前两个'会被认为是括字符常量用的一对',而第三个'会被认为是缺失了'的一对''的前半部分。那如何写一个含义是'的字符常量呢?
可以使用转义字符来进行表示,格式为'\'',其中\'就是一个转义字符。
转义字符就是将\后面的字符的含义进行转换,若该字符是字面意思那就将其转成特殊含义,若是具有特殊含义就是将其转成字面意思。
C语言中有哪些转义字符呢?
转义字符 | 释义 |
---|---|
\? | 在书写连续多个问号时使用,防止其被解析为三字母词(三字符序列) |
\' | 用于表示字符常量' |
\" | 用于表示字符常量" |
\\ | 用于表示一个字符常量\ |
\a | 响铃,蜂鸣 |
\b | 退格符 |
\f | 进纸符 |
\n | 换行 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
\ddd | ddd是一个1~3个八进制数字,是ASCII码的特殊表示,如\130对应的是字符X |
\xdd | dd表示1~2个十六进制数字,如\x30 |
(1)三字母词
什么是三字母词?三字母词就是两个?再跟一个字符的一个序列,它们会被替换某些字符。目前,三字母词有9个。
三字母词 | 对应字符 |
---|---|
??= | # |
??( | [ |
??) | \ |
??< | { |
??> | } |
??/ | / |
??! | | |
??' | ^ |
??- | ~ |
三字母词使用的非常少,现在大部分编译器编译时默认不会去做三字母词的转换。
(2)\a
\a能否产生警报取决于计算机的硬件(有些系统中,它不起作用)。蜂鸣是其最常见的响铃声。此外\a不会改变屏幕的光标位置,也就是显示设备中下一个字符将出现的位置。
(3)\b
\b改变屏幕的光标位置,让其后退一格。
当程序输出了abc之后,碰到了两个\b,此时光标移动到b的位置,即下一个字符d将打印到这里并覆盖b,然后将光标前进一位,再打印e覆盖掉c。即最终打印了ade。
(4)\f,\v
\f将光标位置变为下一页的开始处,\v将光标位置移至下一个垂直制表点。
但这两个转义字符在PC屏幕上只会显示出奇怪的符号,且打印它们后光标不会前进,只有将它们输出到打印机上才有用。
(5)\n与\r
\n将光标位置移到下一行,即换行。
\r将光标位置移动到当前行的开始处,即回车。
(6)\t
\t将光标位置移动到下一个水平制表点(通常是第1,9,17,25等字符位置)。
(7)\ddd与\xdd
\ddd与\xdd是ASCII码的特殊表示方式。
\ddd:如\130,其中130是一个八进制数据,逢8进1,其对应的十进制就是0+3*8+1*64=88,其对应的字符就是X。
\xdd:如\xa,其中a是一个十六进制数据,逢16进1。a代表的就是十进制中的10,其对应的字符就是\n换行。
3.字符串
C语言中"'括起来的一串字符称之为一个字符串,或者称之为字符串字面值。例如:
C语言中存在有字符串,但不存在字符串对应的类型。根据上图中编译器的提示,可以发现该字符串的类型在C语言中其实是char [14]的数组类型。即字符串数据的类型是char数组。
但奇怪的是,字符串常量"I am a string"中只有13个字符,为什么存储它的数组有14个元素呢?其实字符串末尾会有一个结束标志,这个结束标志是一个转义字符'\0',其对应的ASCII码就是0值。结束标志不计入字符串的长度。
如何用一个变量存储字符串呢?前面的字符串常量的类型已经告诉我们如何做了,使用一个char类型的数组来进行存储。具体的方式如下:
字符串的结束标志'\0'到底有什么用呢?下面来看看这段代码
str2中没有带上'\0',其打印出来的结果并不是如我们所想的那样只是打出abcde。这是为什么呢?
其实是因为printf()在打印字符串时是从字符串的开头一直往后打印,直到其碰到'\0'才会停止。所以在打印str2时不仅打印了abcde还打印了一些莫名其妙的东西。
至于为什么打印出来这些东西之后才会停下来,是因为内存中其实每个内存单元中都是有值的,每个内存单元的值可能是之前某个程序运行时保存的结果,所以里面的值是随机值,这也是为什么局部变量不初始化,其值会是随机值的原因。
对于字符串可以使用strlen()来求出其长度,要使用它需要在代码首部加上#include <string.h>,其结果值为无符号整数。
可以发现str1这个数组的大小为6个字节,即6个字符。而其长度为5,即最后的'\0'不计入长度,碰到它就认为该字符串结束了。例如:
十、注释
在写代码时,若代码结构较为复杂,过一段时间再来看就需要重新解析一遍代码弄清其含义,而且也不便于其他人阅读代码。若能留下一段文件解释就能方便多了,注释就是这样的一段说明文字。被注释掉的内容会被编译器忽略。
C语言中有两种注释风格:
1.C语言风格的注释:/*xxxxx*/,但其不能嵌套注释。
、*
被/*和*/囊括的部分全部都会变成注释。
但不能嵌套注释,因为/*会和与之靠的最近的*/匹配,例如:
最后一个*/没有与之对应的/*进行匹配。
2.C++风格的注释: //xxxx,C99将其引入了进来。
该风格的注释既可以注释一行也可以注释多行。
推荐使用这种注释。
使用注释除了能留下代码功能的描述信息,也可以将暂时不需要但又不想删除的代码留下来。
十一、选择语句
实际问题中,经常碰到根据不同的情况来选择做出不同的行为。C语言中提供了if语句和switch语句两种选择结构。
1.if语句
if语句的语法结构
if语句的语法结构如下:
(1)
if(表达式)
语句;
(2)
if(表达式)
语句;
else
语句;
(3)
if(表达式)
语句;
else if(表达式)
语句;
......
else
语句;
当if后的()中的表达式的结果为真时,执行if后的语句,否则不执行if后的语句。若if有匹配的else,则当表达式的结果为假时执行else后的语句。
那什么叫表达式?表达式其实就是常量、变量、函数调用等按照C语言语法规则用运算符和运算数连接起来的合法的式子。例如
#include <stdio.h>
int a = 10;
int main()
{
//下列都是表达式
1;
a;
a + 1;
a = 20;
a > 10;
a == 20;
printf("hello\n");
return 0;
}
那什么叫真,什么叫假?C语言中0表示为假,非0表示为真。同时C语言中具有表示真假含义的表达式的值为真时,值规定为1。例如:
>,<,<=,>=,==,!=称之为关系操作符。>,<,<=,>=的含义就是比较两个运算数的大小。==就是判断两个运算数的值是否相等,!=就是判断两个运算数的值是否不相等。
&&和||称为逻辑操作符,&&就相当于并且的意思,||就相当于或者的意思。
可以看出1>2表达式结果为假,故其值为0;1<2表达式结果为真,其值为1。
了解了什么是表达式,什么是真假,接下来可以拿几段代码演示if语句
悬空else问题
else与上面的if对齐,并不意味着else与第一个if匹配。else只会和在它上面且离它最近的未匹配其他else的if进行匹配。也就是说这个else是和if(b==2)进行的匹配。所以什么都没有打印。
此时else只能与if(a==1)进行匹配。
适当的使用{}能使代码逻辑更加清楚,好的代码风格是非常重要的。
2.switch语句
switch语句也是一种选择分支语句,常常用于多分支的情况。
switch语句的基本语法结构
switch语句的语法结构如下:
switch (整形表达式)
{
case 整形常量表达式:
语句;
...
case 整形常量表达式:
语句;
...
...
}
switch语句先判断这个整形表达式的值,然后去找等于该值的case标签,并从此位置向后运行。例如:
switch(表达式)中的表达式a的值为2,因此从case 2:开始向后执行,从而执行了2和4的打印。
switch语句中的break
switch语句需要搭配break才能实现分支。例如
当执行到break语句后将会跳出当前的switch语句。所以也可以写出如下的代码:
上述代码中,当day为1,2,3,4,5中的一个时,会跳到相应的case标签向后执行,最终执行打印weekday后便会执行break语句,跳出switch语句。也就是说break能将多条case语句划分成不同的分支部分。
default子句
若switch(表达式)中的表达式没有匹配的case标签值,会跳过所有的语句。若不想忽略所有case标签值以外的值可以在switch语句中添加default子句,它可以写在任何一个case标签可以出现的位置。当所有的case标签值都不匹配时便会执行default子句后的语句。每个switch语句只能有一个default子句。例如:
3.注意事项与建议
①switch语句中的case标签值不能重复。
②switch语句会跳到对应的case或者default标签位置依次向后执行语句,执行过程中碰到case或者default是不会去判断的,例如:
③建议将default子句用于处理错误情况,并放在末尾位置,这是一种良好的编程习惯。
④在switch语句中,不能直接在case标签后定义变量,因为case标签之间的区域并不是一个块作用域。如果在case标签后定义了变量,该变量的作用域超出了case分支的范围,可能与其他case分支产生冲突,导致代码可读性降低。例如:
如果一定要在case标签中定义变量,可以使用{}创建出一个块作用域。例如:
不过,并不建议这样在switch语句中定义变量。
⑤选择语句是可以嵌套的,if语句既可以嵌套if语句又可以嵌套switch语句,switch语句也是一样的。
十二、循环语句
在实际问题中,不仅仅只有选择问题,还存在着一些重复性工作,例如我们每天都要上学。C语言提供了while,do-while,for三种循环语句来解决这类问题。
1.while循环
(1)while循环结构
while (表达式)
{
循环语句;
}
while循环会先判断表达式是否为真。若为真则会执行循环体即{}内的语句,执行完毕后会再回来判断表达式是否为真,若为真则会继续执行。若表达式值为假则不再执行循环体内的语句。例如:
#include <stdio.h>
int main()
{
int i = 1;
while (i < 10)
{
printf("上学第%d天\n", i);
i = i + 1;
}
printf("放假了!\n");
return 0;
}
执行结果:
(2)while循环中的break与continue
break的作用是结束整个循环。具体来说就是当执行break语句后,后续的语句不再执行,并直接跳出整个循环语句,结束这个循环。
continue的作用是结束本轮循环,进入下一次循环。具体来说就是执行continue语句后,本次循环中的后续的语句不再执行,直接回到while(表达式)进行表达式真假的判断。
例如:
2.do-while循环
(1)do-while循环结构
do
{
循环语句;
} while (表达式);
第一次进入do-while循环时,会先直接执行循环语句,执行完毕后判断表达式真假。若为真则再次执行循环语句,若为假则结束循环。注意do-while语句最后要带上英文分号。
下面使用一段代码进行演示:
表达式直接为0,表示恒为假。但由于do-while循环第一次一定会执行,所以仍然打印了一个0。
(2)do-while循环中的break与continue
break和continue作用在各个循环语句中作用类似,区别只在于continue结束本轮循环后会向哪里跳。在do-while循环中执行continue会跳到下面的while(表达式)进行表达式真假判断。例如:
这里的表达式是一个赋值表达式,一般不建议在循环的条件判断处写赋值表达式,这里仅是一个演示。
可以发现i==-5时,跳过了打印,直接跳到下面的while(表达式)处计算表达式的值,判断其真假。非0为真,故赋值表达式结果为各个负数时,表达式值为真,继续执行循环。最终赋值表达式结果为0,为假,循环结束。
3.for循环
(1)for循环结构
for (表达式1; 表达式2; 表达式3)
{
循环语句;
}
表达式1为初始化部分,用于初始化一个变量,一般是用于条件判断的变量。
表达式2为条件判断部分,表达式结果为真,继续执行循环体的语句,为假终止循环。
表达式3为调整部分,用于调整变量的值,一般都是调整用于条件判断的变量值。
具体执行流程为,第一次进入for循环,先执行表达式1,再执行表达式2,为真则执行循环语句。循环语句执行完毕,再执行表达式3。之后判断表达式2真假,为真则继续执行循环语句,执行完后再执行表达式3。
下面使用一段代码进行演示:
注意,在C89标准中,表达式1不可以定义变量。而在C99中,表达式1可以定义变量。如:
不同编译器对C标准的支持程度各不相同,使用时需要注意。
(2)for循环中的break与continue
break作用与其他循环中的类似。
continue会跳过后续的循环语句,直接跳去执行表达式3,然后再进行表达式2的真假判断。例如:
(3)注意事项与建议
- 不要在for循环的循环体内修改用作条件判断的循环变量,否则可能导致for循环失去控制。
- for循环的三个表达式部分都可以省略不写,若省略判断部分则,判断恒为真。
4.死循环
死循环并不是上述三种循环外的别的循环,而是上述循环的条件判断部分因为一些原因始终判断为真导致循环永不终止的情况。例如:
上述while循环的条件表达式值始终为真,所以始终执行循环。而循环体内只有空语句,故什么都没有做。所以整个程序卡在这里,没有执行后续的打印end的操作。
再如:
当循环变量i为3时,continue跳过本轮循环后续语句的执行,也就跳过了循环变量的更新语句。最终i恒为3,每次判断i < 5始为真,每次进来都会执行continue,依然不更新循环变量i。程序卡在这里,循环无法退出。
十三、函数
1.什么是函数
一个较大的程序一般会将其分成多个程序模块,每个模块完成一种特定的功能。C语言中函数就是这样一种程序模块。它由一个或多个语句组成,负责完成某项特定任务,具备相对独立性。
2.函数的分类
(1)库函数
库函数就是编译器厂商根据C语言标准的规定(如某个函数有怎样的功能,返回值是什么,其具体实现交给编译器厂商)为我们实现好的函数。不过编译器厂商也会提供一些C标准没有的函数,如scanf_s(),不建议使用,使用的话不具备跨平台性,因为这样的函数在其他的编译器看来是一个不存在的函数。
前面的printf()和scanf()便是库函数,那还有什么样的库函数呢?如何学习库函数?下面是一个常用的网站:
https://legacy.cplusplus.com/reference/
(2)自定义函数
自定义函数就是我们自己写的函数。函数的具体功能由我们自行设计,给了程序员非常大的发挥空间。
3.函数的定义
函数的定义格式如下:
返回值类型 函数名(参数列表)
{
语句项;
}
上述格式中的返回值类型是这个函数执行后的结果的类型。参数列表中定义了一些局部变量,在函数调用时需要给这些局部变量传递值,函数调用时给这些局部变量的值的不同可以使这个函数产生出不一样的效果。参数列表中的这些局部变量叫做形式参数。其{}内容叫做函数体,写有一条或多条语句。函数名的命名规则与前面变量名的命名规则类似。同样函数在其作用域内也不可以同名。函数只能定义在全局中,不能定义在其他函数内部,故函数的作用域默认为整个项目工程。
下面使用一段代码进行演示:
int add(int num1, int num2)
{
return num1 + num2;
}
上面的代码定义了一个叫add的函数,其返回值是一个int类型的数,具有两个int类型的形式参数。其中的return语句将会结束函数的执行。若return语句后面带有表达式,则将会将该表达式的值作为函数的返回值。
4.函数的使用
函数的使用方式为:
函数名(参数列表);
是使用函数时需要给该函数的形式参数传递值,在使用函数时参数列表中的参数叫做实际参数。
下面使用一段代码进行演示
#include <stdio.h>
int add(int num1, int num2)
{
return num1 + num2;
}
int main()
{
int x = 10;
int y = 20;
int ans = add(x, y);
printf("%d\n", ans);
return 0;
}
上述代码中将变量x和y作为了调用函数时需要传递的实参,然后使用了变量ans来接收函数add()的返回值,然后将相加结果打印出来。
执行结果如下:
可能你会奇怪搞个相加还这么麻烦,我直接写成x+y不就行了吗?定义函数好像搞得更复杂了。
这里只是个简单的演示,所以这样写。以后实际编写代码的过程中,会碰到很多地方的代码功能是重复的,只是一些值不同而已。这个时候就可以使用函数完成这一份功能,然后在需要使用这个功能的地方,调用这个函数,传给这个函数相应的实参就可以了。
5.函数的声明
当上述代码进行编译时,编译器给出了警告表示add未定义。但我们不是在下面定义了吗?
跟前面全局变量的声明一样,编译器是对每个源文件从上往下检查的。当其检查到调用add()函数的位置处时却发现这个文件前面部分没有找到这个函数的有关说明。同样的,这种情况也需要进行函数的声明。
函数的声明格式如下:
函数返回值类型 函数名称(参数列表);
具体代码如下:
#include <stdio.h>
int main()
{
//这两条语句都是声明。声明可以声明多次。
int add(int num1, int num2);
int add(int, int);
int x = 10;
int y = 20;
int ans = add(x, y);
printf("%d\n", ans);
return 0;
}
int add(int num1, int num2)
{
return num1 + num2;
}
函数的声明和定义类似,但不需要带上函数体,只需要跟上分号即可。此外,其参数列表中的形参名称可以不写。因为它只是个声明,它不是为了告诉这个编译器这个函数内部具体是怎么实现的,而是告诉编译器其他位置有形参是两个int类型的、返回值是int类型的叫做add的一个函数的定义,不用担心其是否未定义。此处对于编译器而言不需要关心该函数的形参叫什么名字,即使你声明时把形参名称写成别的名称,编译器都不会发出警告。
尽管如此,建议函数声明带上形参的名称。虽然形参的名称对编译器来说是没有用的,但这对于我们人来说,一定程度上也能通过形参的名称结合函数的名字更好的推测这个函数的功能。
当然,这样的声明也是不规范的,最好还是跟前面全局变量的声明一样带上extern。
#include <stdio.h>
int main()
{
extern int add(int num1, int num2);
int x = 10;
int y = 20;
int ans = add(x, y);
printf("%d\n", ans);
return 0;
}
int add(int num1, int num2)
{
return num1 + num2;
}
6.递归
如果你从头看到这里,多少会发现,函数的调用始终是在另一个函数内的。也就是说函数内是可以调用其他函数的。
那能不能函数自己调用自己呢?当然可以。并且,有一种函数自己调用自己的编程技巧,这种技巧就是递归。
(1)递归的必要条件
递归这种编程技巧不是说只需要自己调用自己就行了,它有两个必要条件:
- 存在限制条件,当满足这个限制条件时,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
(2)递归的基本思想
递归其实就是将问题不断划分成性质相同但规模更小的子问题。求解时只需要关注如何将其划分成规模更小的子问题即可,不需要过分关注这个小的子问题是如何解决的。
下面是一个代码演示:
#include <stdio.h>
//求解n的阶乘n!
int factorial(int n)
{
if (n <= 1)
return 1;
else
return n * factorial(n - 1);
}
int main()
{
printf("%d\n", factorial(5));
return 0;
}
n!=n*(n-1)*(n-2)*...*2*1。同样n!=n*(n-1)!。这样就将求阶乘的问题划分成了子问题。
当然子问题也不能无限的将其划分的更小,它一定有其限制条件。我们都知道0!=1,1!=1,所以这里的限制条件就写成了当n<=1时直接返回1。而且在划分子问题的同时,n也在逐渐靠近0和1,即不断靠近限制条件。
7.函数的作用
函数的作用是实现代码复用,简化代码,提供对过程的封装和细节的隐藏。
8.sizeof不是函数
sizeof获取类型大小时,使用方式与调用函数极为相似,但它并不是函数。调用函数必须要有(),而sizeof在使用时传的若是一个类型名称,才必须带上(),若是某一个常量或者变量值,则不需要带上,但站在代码统一性的角度下,最好还是都带上()。
此外,函数的调用是传递不了类型名称的,它只能传递一个值。而sizeof是可以给它一个类型名称来获取结果的。
sizeof可以传入类型名称,而printf函数则无法传入类型名称。故sizeof不是函数。
由上面的例子可以知道,试图定义名为sizeof的函数时,报错信息为应输入标识符(即合法的函数的名字,其不应是操作符),而不是sizeof已由主体。故sizeof是操作符,而非函数。
十四、多文件程序与头文件
前面提到过,一般一个C程序,会存在有多个源文件,各个源文件实现不同的功能。也就是各个源文件要实现不同的函数,供其他的源文件内部去使用它提供的函数。例如有如下一个C程序。它有两个源文件main.c与add.c:
main.c:
#include <stdio.h>
int main()
{
printf("%d\n", add(1, 2));
return 0;
}
add.c
int add(int num1, int num2)
{
return num1 + num2;
}
此时,编译器给出警告标识,add函数未定义。
你可能会产生疑问,add()函数放在其他源文件中,那它对于编译器来说是先被编译器检查的还是后被编译器检查的。其实答案是两个都不是,编译器是对每份源文件单独进行编译,生成叫目标文件的东西,最终由链接器将目标文件链接上相应的库来形成可执行程序的。也就是是如果add()函数写在其他源文件里,那在main.c中想要使用add()函数,编译器一定无法检测到add()函数被定义过。
那怎么办?进行函数声明:
此时编译器没有给出任何警告。
也就是说对于其他源文件中实现的函数,在别的源文件中想要使用它必须敲一遍它的声明。但实际的项目代码中,一个源文件中的函数可能会在十多个甚至更多的源文件中被调用,如果后续这个源文件的某些函数不需要或者需要新增一些功能,还需要去一个个翻找所有使用过这个函数或需要使用它的源文件,去给他们添加或者删除声明。这实在是太恶心了!
因此头文件孕育而生。有了头文件之后,添加新的函数、新的全局变量或者新的一些类型的定义(C语言有些类型可以供我们自行定义)时,只需要修改头文件,在头文件中添加全局变量或者函数的声明亦或类型的定义就行了,其他需要使用它们的地方不需要做修改。
具体使用方法如下:
头文件add.h:
//函数的声明,全局变量的声明or类型的定义
extern int add(int num1, int num2);
add.c中:
#include "add.h"
int add(int num1, int num2)
{
return num1 + num2;
}
main.c
#include <stdio.h>
#include "add.h"
int main()
{
printf("%d\n", add(1, 2));
return 0;
}
简单来说,就是一个源文件对应一个头文件,一般将它们的文件名写成一样的。然后这个头文件中包含这个源文件中的各种全局变量or函数的声明以及一些类型的定义。然后再在该源文件(add.c)以及需要使用该源文件中的函数的其他源文件(main.c)的首部加上#include "头文件名称"即可。
那头文件到底是怎么做到这些的呢?其实很简单,就是文本替换。当编译器编译之前会先对源代码进行预处理,预处理时检查到#include指令时,它就会根据这个指令后面的内容找到对应的头文件,然后将那个头文件中的内容全部拷贝进去。也就是预处理后,头文件的什么声明和类型的定义全都在对应源文件之前写有#include的位置处写好了。说白了,就是头文件就是将原本要由人来做的事情交给了机器来做。
想必你也就能知道为什么printf()和scanf()这样的库函数在使用之前,为什么要在源文件首部带上#include <stdio.h>了。因为stdio.h中包含了这两个库函数的声明。至于为什么要把#include写在源文件首部,这是为了保证在这个源文件中使用里面声明的全局变量or函数or定义的类型之前,确保它们已经声明or定义了。
此外,你肯定能发现#include <stdio.h>和#include "add.h"非常相似,差别只是<>和""的区别。那它们有什么区别呢?
区别其实很简单,""是先在当前的工程目录下查找头文件,若没找到去系统的标准头文件路径去查找。而<>是直接去系统的标准头文件路径查找。至于这个系统的标准头文件路径在哪,可以使用叫做everything文件查找软件去搜索(Windows自带的文件搜索太捞了)。
十五、操作符
1.算术操作符
+-*/就是四则运算加减乘除。%用于计算除法后的余数。
+-*没有什么好说的。
对于除法/,其两个操作数若都为整数,则执行的是整数除法,结果为一个整数,向0取整。若有一个操作数为浮点数,则结果为浮点数。
对于%,其两个操作数必须为整数,其结果为除法后的余数。例如:
2.移位操作符
移位操作符的两个操作数必须为整数。其作用是得到一个被移动了比特位的数据(原始数据没有发生改变,就如同a+3;得到一个数据,但a的值没有变)。
什么叫移位呢?例如十进制数据20,对其左移一位就变成了200,对其右移一位变成了2,似乎有乘以10和除以10的作用。计算机中的数据是二进制的,每一个位称为比特位,每一次移动一个比特位,所以对一个二进制整数数据0000 0110(即十进制6)右移一位得到0000 1100(即12),左移一位即得到0000 0011(即十进制3),似乎有乘以2和除以2的作用。这里的受限于知识面,只能这样简陋的介绍,移位操作符有乘以2和除以2的作用,但这是不一定的。
至于为什么移位操作符不一定有乘以2和除以2的作用,是因为对于有符号整数而言,它们有一个比特位用于表示它是正数还是负数。
此外,对于移位操作符,不要尝试移动负数位,这种行为是标准未定义的。
3.位操作符
位操作符的两个操作数必须为整数。
它们也是依据二进制的比特位运算的。
&按位与操作符,这个操作符跟scanf("%d",&n);的&不是同一个意思。这个操作符会按位比较各个比特位,若两个二进制数对应的比特位上有一个为0,则得到的新数据对应的比特位值为0,若同时为1,则得到的新数据对应的比特位值为1。例如:
上述的计算只有在是无符号整数或者有符号正整数的情况下是这样,受限于知识面只介绍到这里,其他的按位操作的操作符都是如此,如|,^以及单目操作符中的按位取反操作符~。
|按位或操作符。它也是逐比特位的比较,若两个二进制整数数据中对应的比特位有一个1,则得到的新数据对应的比特位为1,若同时为0,则得到的新数据对应的比特位为0。例如:
^按位异或操作符。它也是逐比特位比较,相同为0,相异为1。例如:
4.赋值操作符
=,用于给一个变量赋值。
其余的赋值操作符称为复合操作符。
其他的复合操作符也是类似的。
5.单目操作符
单目操作符就是只有一个操作数的操作符,像是+这样的操作符有两个操作数,是双目操作符。
!是逻辑反操作符,可以将真变成假,假变成真。前面已经说过C语言中非0为真,0为假。例如:
-,+表示是负值or正值,非常简单。
&取地址操作符,*间接访问操作符。这两个操作符与后面的指针概念有关,这里先按下不表。
~按位取反操作符,其操作数只能为整数。其作用就是按比特位取反得到一个新数据,如0000 1010对其~得到的新数据就是1111 0101,至于这个数据究竟是十进制整数中的几,这里也受限于知识面无法详细介绍。
++,--自增,自减操作符。
这两个操作符分为前置和后置,即放在变量的前面还是后面。当放在前面时表示先让在该变量自增或自减1,然后返回该变量的值(即这个操作符和操作数结合的一个表达式的值)。当放在后面时表示先返回该变量的值(即这个操作符和操作数结合的表达式),然后再让该变量自增或自减1。例如:
--也是类似的。
注意,对于++--这样的操作符,其操作数不要在一个语句中出现多次,对同一操作数的++--也不要出现多次,因为各个表达式的求值顺序不一定是你所想的那样。例如:
例如这里,先对++a这个表达式求值得到11,同时也导致a的值变成了11。然后再对a求值,得到了11。而不是预想的从左往右计算表达式,从而得到10和11。
尤其是下面这种语句一定要避免:
在VS2019下:
它并不是从左往右依次计算出各个表达式的值,即2+3+4=9。由于前置++只是规定在得到表达式的结果之前进行自增,具体什么时候自增是不确定的。在VS2019这里,就是先让a自增了3次,变成4,然后去计算各个++a的表达式的值,即得到了4+4+4=12。在不同的编译器下,这段代码会有不同的表现。这样的表达式计算路径不唯一,尽管没有语法报错,但我们认为它是一段错误代码,是要避免的。
(类型)是强制类型转换操作符,有时候需要临时地将某些常量or变量值的类型变化一下,可以使用该操作符。例如:
注意强制类型转换操作符并不会改变一个数据原本的类型,只是临时地将其类型“改变一下”。
6.关系操作符
这一组就非常简单了,比较大小与比较是否相等。该操作符与操作数构成的表达式的结果是真或假,即1or0。
7.逻辑操作符
&&逻辑与操作符,就是并且的意思,其组成的表达式值为真or假,即1or0。当其两个操作数都为真时,&&与它们构成的表达式值才为真,否则为假。
||逻辑或操作符,表示或者。组成的表达式值为真or假,即1or0。当其两个操作数都为假
时,||与它们构成的表达式才为假,否则为真。
8.条件操作符(三目操作符)
条件操作符:表达式1?表达式2:表达式3
由于这个操作符拥有三个操作数,所以也叫三目操作符。这个操作符会先判断表达式1的真假。若表达式1为真则条件操作符构成的整个表达式的值为表达式2的值,若表达式1为假,则整个表达式的值为表达式3的值。有时可以使用它替换简单的if-else。
例如:
9.逗号表达式
逗号表达式:表达式1,表达式2,表达式3,......表达式N。其中的,便是逗号操作符。
逗号表达式,会依次从左往右依次计算各个表达式的值,最终整个逗号表达式的值为最后一个表达式的值。例如:
10.下标引用、函数调用与结构成员访问
[]下标引用操作符,即之前用于访问数组具体某个元素用的操作符。
()函数调用操作符,即调用函数时函数名后面跟着的圆括号。
.与->,是结构成员访问操作符,对于它们,先按下不表。
十六、关键字
C语言的关键字非常多,而且新的标准又会有新的关键字的加入。这里介绍的是C89标准的关键字。
C89的关键字有32个,具体如下:
其中大部分的关键字我们已经见过了。这里再介绍剩余关键字的一部分,其余的关键字,由于篇幅与知识面的原因不在这里简绍了。
1.auto
auto关键字就是用来表示一个变量在程序运行期间自动创建,自动销毁。说白了,就是表示一个变量为局部变量。然而,现在写在函数体内部的or函数的形参列表的变量默认都是局部变量,所以这个关键字我们用不上。
int main()
{
auto int a = 10;
return 0;
}
2.register
计算机的存储数据的介质有很多。如硬盘、内存、高速缓存、寄存器。它们的空间大小为硬盘>内存>高速缓存>寄存器。将它们中的数据搬入CPU中以参与计算的速度则是寄存器>高速缓存>内存>硬盘。register关键字就是建议编译器编译时将该变量放到寄存器中,而不是内存中。编译器会根据情况来决定是否将其放到寄存器中,即不一定放入寄存器里。
int main()
{
register int a = 10;
return 0;
}
3.typedef
这个关键字可以对类型名称做重新命名。在重新命名之后的位置可以使用其给类型重新定义的新名字。例如:
typedef unsigned int uint;
typedef int iArray10[10];
int main()
{
unsigned int a;
uint b;//无符号整型
iArray10 arr;//元素类型为int,数组元素个数为10的数组
return 0;
}
其格式就是typedef+使用需要被重命名的类型来定义“变量”。其中“变量名”就是重命名后得到的新的类型名称。
重新命名后的作用域范围内可以使用这个新的类型名称。不过一般不会将typedef语句置于函数内,而是放在全局中。
4.static
这个关键字虽然名字叫静止的,但它不是个好啃的骨头。简单来说它是改变全局变量or函数的链接属性以及局部变量的生命周期的。
什么是链接属性?具体来讲分成外部链接属性,内部链接属性与无链接属性。
前面谈到过,C语言程序变成可执行程序需要经过编译器的编译与链接器的链接的。其中编译是对一个个源文件单独进行的,也就是说编译出来的各个目标文件中,如果某个文件里使用了某个其他源文件的函数or全局变量,它是不知道这个全局变量在哪or函数具体是什么样的,需要经过链接器链接来让这个它们关联起来。默认情况下,全局变量是和函数都是外部链接属性。此时这样的全局变量or函数是允许在链接器链接时将使用它们的代码与它们关联起来。若是内部链接属性,则不允许将它们关联起来。具体来说如下:
(1)static修饰局部变量
局部变量默认是没有链接属性的,即局部变量的链接属性为无链接属性。所以static改变的不是它的链接属性,改变的是它的生命周期(受限于知识面只能就其表现来谈)。默认情况下局部变量被auto修饰,进入作用域被创建,生命周期开始,出了作用域被销毁,生命周期结束。而被static修饰的局部变量,其生命周期同全局变量一致,生命周期随整个程序。当程序第一次执行到该static修饰的局部变量定义处时,将会根据定义时给定的初始化的值将该变量初始化,后续再次执行到其“定义”位置处后,不再对其值修改,即不做任何操作。
具体效果见如下程序:
#include <stdio.h>
void printLocal()
{
int num = 10;
num += 1;
printf("%d\n", num);
}
void printStatic()
{
static int num = 10;
num += 1;
printf("%d\n", num);
}
int main()
{
printf("local:\n");
printLocal();
printLocal();
printLocal();
printf("----------\nstatic:\n");
printStatic();
printStatic();
printStatic();
return 0;
}
执行结果:
普通的局部变量,出了作用域被销毁,值不再存在,而static修饰的局部变量出了作用域不销毁,值仍然保留。static修饰的变量叫做静态变量,静态变量的生命周期随整个程序。
(2)static修饰全局变量
static修饰的全局变量将会改变其链接属性为内部链接属性,简单来讲static修饰的全局变量在其他源文件中不可见。写一些程序时,有时希望在源文件内使用一个全局变量,但又不影响其他源文件或被其他源文件使用,可以使用static修饰这个全局变量。
例如:
这个静态变量g_val只在test.c这个文件中有效,在main.c中,它是不可见的,所以它可以在其他源文件中定义一个同名的全局变量。
(3)static修饰函数
static修饰的函数,其链接属性也会从外部链接属性变为内部链接属性,即只在本文件内有效,在其他源文件中不可见。例如:
可以发现即使带上了add函数的声明,在其他源文件中也找不到add函数的定义。
由于static修饰的函数在其他源文件中不可见,故可以在其他源文件中定义它的同名函数:
写C程序时,有可能是你要编写相应的功能逻辑对外使用。但如果你的功能逻辑比较复杂,那就会有多个函数,即会对外暴露多个接口。暴露的接口越多,你编写的程序的实现细节被别人知道的的越多,同时也意味着他人的使用成本也越大(即不知道用什么、该怎么用))。故可以将你的代码逻辑中涉及具体细节的函数设为static,让他人看见不见,然后编写具有外部链接属性的函数来供外部调用,在这个函数内再去调用涉及具体逻辑的函数。例如:
十七、指针
1.内存与地址
计算机中所有的程序运行时其当前需要的代码和数据是保存在内存中的,所以内存是非常重要的存储器,因此也叫它为主存。
为了有效的使用内存,将整个内存划分成了一个个小的内存单元,每一个内存单元的大小就是1个字节。为了访问这些内存单元,在硬件设计时,就给内存单元进行了编号,这些编号称为该内存的地址。例如:
上述地址值以0x开头。0x开头是十六进制值。实际上的地址值也是一连串的01,但是那样写太长了,而且不便于观察。习惯上使用十六进制来表示地址值。此外,由于地址能唯一标识一个内存单元,所以也将其称为指针。所以也把地址叫做指针。
2.指针变量
指针是一个值,是一种数据,是数据就可以用变量来保存。保存指针的变量就叫做指针变量。
变量是创建在内存当中的,所以一个变量就会有地址。变量的大小除了char都大于1个字节,也就是说一个变量会占多个内存单元。那如何确定这个变量在哪一块区域呢?C语言中使用其占用的内存单元中地址值最小的那个值来作为一个变量的地址,利用这个地址值再结合该变量的大小便可以确定变量是哪一块区域。
使用&取地址操作符便可以取出变量的地址。
现在能取出变量的地址了,那这样的地址数据便可以保存起来,保存地址数据的变量便叫做指针变量。下面定义了一个指针变量:
上图中p便是一个指针变量,其类型为int*,左侧靠近p的这一个*表示p为一个指针变量。其中除*和变量名以外的部分表示该指针变量所指向的数据是什么类型。例如int* p;中除*和p以外,剩下的便是int,即该指针变量p所指向的数据类型是int。
例如:
int main()
{
int* p1, p2;
return 0;
}
上述代码中,p1左侧有一个*,表示p1为指针变量。而p2左侧没有*,为一个int类型的整型变量。
若要让p2也是指针变量,需要在其左侧带上*。
3.指针变量的使用
光保存一个地址值是没啥用的。保存一个地址值就是为了需要的时候根据这个地址值找到这个变量。
*叫做间接访问操作符(解引用操作符),使用它能根据一个地址找对应的内存空间。
例如:
4.指针数据的类型大小
前面说过指针就是地址,地址实际上是二进制的01序列,每一个二进制数字都是一个比特位。计算机的硬件上通过地址线来传递地址信息,一根地址线能传递高低电平,即1or0。所以有几根地址线,地址就有多少个比特位。
在32位环境(x86)下,是有32根地址线用于传输地址信息,故地址是一个32位的二进制数据,即有32个比特位,即4个字节。
在64位的环境下,有64根地址线用于传输地址信息,故地址有64个比特位,即8个字节。
5.指针与指针变量
指针是一个地址数据,而指针变量是一个变量,两者是不一样。但平时我们也把指针变量叫做指针(平时口头上说的指针都是指针变量)。
对于这点博主个人认为主要有两点原因:
- C语言是国外发明的,早期翻译时翻译的不严谨。
- 指针变量作为右值时,它就表示指针。
这里提到了一个概念,右值。其实也有与之相对应的左值。对于一个变量,它在表达式中,虽然都是同样一个名字,但其含义是不一样的。例如:
#include <stdio.h>
int main()
{
int a = 10;
//a在=左边,而且此时是将20这个数据放入到a变量的空间中,这时称其为左值
a = 20;
//a在=右边,而且此时是想将a变量中的内容,即20放入到b的空间中,故此处a为右值。
int b = a;
//并不是说一个变量在=左边就叫左值,在=右边就叫右值。
//而是根据这里变量名的含义,根据其是想使用这个变量的空间还是变量的内容来判断
printf("%d", b);//这里b变量就是个右值,要使用的是其内容。
return 0;
}
因此,当指针变量时一个右值的时候,这个变量名就表示的是它的内容。而指针变量的内容就是一个指针。例如:
十八、结构体
有时需要描述一个对象的各个信息。例如想要描述一个学生,假设学生具有名字、年龄、性别、学号信息。但如果只依靠之前学到的知识,想要描述一个学生,就需要为学生定义4个变量来记录其名字。如果有十个学生,就要定义40个变量。这不仅麻烦,而且为每个学生定义的4个变量不具备相关性。因此,对于这样的复杂对象,可以使用结构体来对其描述。
1.定义一个结构体类型
//struct为定义结构体的关键字,Stu为结构体标签名。struct Stu为这个结构体的类型
struct Stu
{
char name[20];
int age;
char sex[5];
char id[15];
//这些为结构体成员列表
};
定义出来的结构体类型在其作用域中可以用来定义结构体变量。其结构体表标签名的命名规则同前面变量的命名规则。
根据VS2019的提示,可以发现此处的var1的类型是内层定义的结构体类型。即同样的,内层定义的结构体类型会隐藏外层的定义的结构体类型。但定义结构体类型时只会在全局中定义,不会出现上述的情况,此处仅是演示。
2.结构体变量的定义
结构体变量可以在定义结构体类型的时候定义,也可以在其作用域中使用结构体类型来定义一个变量。例如:
int main()
{
struct Stu
{
char name[20];
int age;
char sex[5];
char id[15];
}s1;
struct Stu s2;
return 0;
}
其中s1和s2便是定义的结构体变量,是两个局部变量。
不过定义结构体类型一般都建议将其放在全局中定义,即:
struct Stu
{
char name[20];
int age;
char sex[5];
char id[15];
}s1,s2;
int main()
{
struct Stu s3;
return 0;
}
其中s1,s2为全局变量,s3为局部变量。
3.结构体变量的初始化
struct point
{
int x;
int y;
};
int main()
{
struct point p1 = { 1,1 };
struct point p2 = { 2,2 };
return 0;
}
此外,也可以乱序初始化结构体的某一些成员,如:
struct point
{
int x;
int y;
};
int main()
{
struct point p = { .y=10 };
return 0;
}
4.结构成员访问操作符.与->
结构体跟数组一样,也不能整体使用,只能一个个成员的使用。
那如何访问其成员呢?数组使用[],那结构体呢?结构体使用.与->来访问其成员。
对于一个结构体变量,可以使用.操作符来访问其成员,例如:
此外,对于一个指向结构体的指针变量,使用->来访问其成员。如:
结语
感谢各位读者的耐心阅读。本文只是粗略介绍C语言中的基础知识,许多知识介绍的较为简陋。若有所收获,请持续关注博主,后续博主会陆续地详细介绍C语言的相关知识。