C生万物 | 第一篇 —— 初识C语言【适合入门,建议收藏】
Hello,大家好。接下去的两个月,会为大家更新【万物之源——C】这个栏目,带你从小白到大神熟练掌握C语言
初识C语言
- 一、什么是C语言
- 二、第一个C语言程序
- 1、逐步教学
- 2、扩展与疑难分析
- 三、数据类型
- 四、变量与常量
- 1、变量的命名规则
- 2、变量的分类
- 3、变量的使用
- 4、变量的作用域和生命周期
- 5、常量的种类
- 六 、字符串
- 1、概念
- 2、求解字符串的长度【strlen】
- 3、转义字符【含面试题】
- 七、注释
- 八、选择语句
- 九、循环语句
- 十、函数
- 原理剖析
- 十一、数组
- 1、数组的定义
- 2、数组的下标
- 3、数组的使用
- 十二、操作符
- 1、算数运算符
- 2、移位操作符
- 3、位操作符
- 4、赋值操作符
- 5、单目操作符
- 6、关系操作符
- 7、逻辑操作符
- 8、条件操作符
- 9、逗号表达式
- 10、其他
- 十三、常见关键字
- 1、前言
- 2、关键字分类
- 3、有关数据存储的底层原理
- 4、typedef关键字
- 5、static关键字
- 5.1、static关键字修饰局部变量
- ❗内存原理图❗
- 5.2、static关键字修饰全局变量
- 5.3、static关键字修饰函数
- 十四、 #define 定义常量和宏
- 十五、 指针
- 1、引言
- 2、内存的概念和介绍
- 3、指针变量的大小
- 十六、结构体
- 拓展
- 十七、总结与提炼
一、什么是C语言
C语言是一门通用的【计算机编程语言】,广泛地应用于底层开发。C语言的设计目标是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。
- 上面提到了这个底层开发,我们经常会听到这个上层应用开发和下层应用开发,这个到底怎么区分呢?
- 我们通过下面这张图来看一下
- 可以看到,对于像C/C++这样的语言,适合于底层应用的开发,例如说是服务器、嵌入式之类的;而像一些上层应用的开发,就要用到现在很流行的Java语言,就很适合上层应用的开发
- 那C/C++就不能开发上层应用了吗?当然不是,只是对于下层应用的涉及会广泛一些而已,像C++也是一门面向对象的编程语言,也可以用来开发一些上层应用
接下来我们再来讲一下这个C语言的标准都有哪些
- 首先,C语言最初的标准,就是美国国家标准局为C语言制定了一套完整的美国国家标准语法,称为ANSI C
- 然后在2011年12月8日,国际标准化组织(ISO)和国际电工委员会(IEC)发布的C11标准是C语言的第三个官方标准,也是C语言的最新标准,该标准更好的支持了汉字函数名和汉字标识符,一定程度上实现了汉字编程
二、第一个C语言程序
1、逐步教学
了解了什么是C语言之后,我们就要使用C语言来进行编程,那C语言的主要编译器有哪些呢?
- 其编译器主要有Clang、GCC、WIN-TC、SUBLIME、MSVC、Turbo C等。
- 我们在之后所写的代码都是在MSVC,也就是微软推出的VS2019上运行,当然VS2022也是可以的,只是刚刚发布可能有些地方会有Bug没修复,但是2019的话已经是经过了一段时间的沉淀,已经属于是比较稳定的了🐌
好,首先我们来看看如何去创建工程,来编写我们的第一个C语言程序
- 首先,打开VS2019,看到右下角有一个创建新项目,点进去即可
- 然后的话你需要选择左侧的空项目,右侧也是,默认第一个,接着在上面选择C++,当然,如果你编写其他程序的时候也可以选择其他程序语言
- 接着点击下一步
- 然后我们进入这个配置新项目中,修改其保存的位置,不要去使用默认的,然后这个项目名称也最好不要使用中文,当然你为了可以方便阅读项目也可以写
- 好,接下来我们点击创建,就会自动给我们在D盘的C文件夹中自动创建一个工程
- 然后旁边会有这么一栏【解决方案资源管理器】,这里可以去管理我们创建出来的.h【头文件】和.c【代码文件】
- 但是有同学说,啊呀,我这个解决方案资源管理器找不要了,怎么办呢,这个时候不要急,也是后办法的
- 这个时候我们只要找到【菜单栏】里的【编辑】,然后就可以看到我们的解决方案资源管理器课,或者你记住个快捷键【Ctrl + Alt + L】也是可以的
- 知道如何打开解决方案资源管理器后,接下来我们就要去创建存放代码的文件了
- 选择右击源文件,然后选择添加——新建项
- 然后在这里要选择上面的.cpp文件,因为这是C/C++的编译器,然后文件名称的地方选择.c的后缀,这是C语言的源文件后缀,.cpp是C++的
- 然后我们写上main函数测试一下,打印一句话,点击上方的【本地Windows调试器】或者是按Ctrl + F5,就可以先调试控制台【终端】,也就是我们常说的小黑框框打印出这句话
- 这就是我们所写出的第一个C语言程序
2、扩展与疑难分析
- 这是打印的一句代码,现在我将这句代码多复制几份,然后打印输出,大家看一下
- 可以看到,这个代码被输出了好几行,那这个时候就有同学疑问,这是为什么呢?这个输出是什么原理呢?
- 这个时候我们就可以进行一个【调试】,点击菜单栏中的【调试】,然后进入看到有一个开始调试和开始执行【不调试】,后者也是我们上面运行代码的第三种方式
- 然后前后就是我们调试的进入命令,而且还可以直接按快捷键【F5】,若是笔记本的话,如果安不了就按【Fn + F5】,进入调试
- 但是在调试之前不要忘了在你的代码左边,也就是这个数字左边点一下,就会出现一个小红点,这个小红点表示你运行代码就是默认调试
- 然后我们进入调试
- 然后就可以看到这个小红点里面多了一个小箭头,这个就是调试所要用到的【逐步语句箭头符号】
- 这个时候我们按下【F10】,就可以让代码一句句地往下走,叫做【逐语句】若是你按下【F11】,就可以进入一个你封装过的函数,叫做【逐过程】,这个我们后面在调试的时候再做详细的讲解
- 那这个时候又有同学说了,这个main是什么东西呀,我不认识这个?
- 我们现在来讲一下这个main是什么
- 对于这个miain,我们管它叫做main函数,是我们运行程序的主函数入口,所有要在终端显示的内容都要放在此处运行才可以被看到。当然再后面我们还会说到【函数】,你可以将一个单独的功能封装成为一个函数,然后在这里调用函数传参即可
int main(void)
{
printf("Hello Bit\n");
printf("Hello Bit\n");
printf("Hello Bit\n");
printf("Hello Bit\n");
printf("Hello Bit\n");
return 0;
}
- 然后我们缩写的代码就叫做【函数体】,
- 这个int叫做函数返回类型,return 0是函数的返回值,这两个要相互对应,不可以函数返回类型是整型,但是返回的是一个字符类型
- 而且尤其要注意的一点是,这个main函数只能有一个,而不能有多个,我们来试试写多个mian函数会出现什么结果
- 很明显可以看到,说到【此main函数已有主体】
- 所以我们在撰写main函数的时候要记住不可以写多个,只能写一个
- 那这里有同学又说了,大体的框架我是知道了,但是这个【printf】是什么意思呀,我还是不懂?那我们再来说一说
- 【printf】是我们在C语言里面的输出语句,那有输出一定是有输入,它叫做【scanf】,这个我们在后面也会详细说到,他们叫做【输入输出流】,都存在于一个头文件中,这个头文件叫做【stdio.h】,所以我们应该要在程序的开头写上这个包含头文件的代码,这样你才可以使用这个头文件里的内容
#include <stdio.h>
三、数据类型
写出了我们的第一个C语言程序,接下来我们来说一说C语言中的数据类型
-
首先我们为什么要写代码呢?那就是为了解决生活中的问题
-
比如说我们在购物软件购物的时候,买到商品的价格可能是整数,也可能是小数,而且这个商品的平常在后台也是一种“字符串”的类型,这么多的种类,要怎么区分呢?这个时候我们引入了一个东西——【数据类型】
-
我们先来整体地看一下C语言中有哪些数据类型,下面就是
- 那对于这些数据类型,它们的大小是多少呢?
- 我们使用【sizeof()】这个运算符来看一下
printf("%d\n", sizeof(char));
printf("%d\n", sizeof(short));
printf("%d\n", sizeof(int));
printf("%d\n", sizeof(long));
printf("%d\n", sizeof(long long));
printf("%d\n", sizeof(float));
printf("%d\n", sizeof(double));
- 从上述运行结果我们可以清楚明了地看出来,那有同学又说这个1,4,8的单位是什么呢?
- 我来告诉你,它们的单位是B【字节】,在计算机里1B = 8b,这是一种十进制写法
- 那为什么会出现这么多类型呢?它们是用来干嘛的?
刚才我们有说到过,因为生活中每种事物、每种东西都是不同的,它们的数据类型都不同,那么这个时候,为了满足多种需求,就要给出多种数据类型。这也就更加丰富的表达生活中的各种值
- 就像我们在下面定义的这些变量,有char类型的,有int类型的,也要double类型的
char ch = 'w';
int weight = 120;
double salary = 200.00;
四、变量与常量
1、变量的命名规则
对于变量,并不是可以随便命名的,而是具有一定的规则,接下来我们来看一下具体有什么规则
- 只能由字母(包括大写和小写)、数字和下划线( _ )组成
- 不能以数字开头
- 长度不能超过63个字符
- 变量名中区分大小写的
- 变量名不能使用关键字
然后我们再来具体讲一下
- 首先第一条,字面意思,很明确,就是需要用着三种东西来组成变量,可以不包含它们所有,但是其他的字符就不可以
- 我们可以从下面这张图很清晰地看出来
2、变量的分类
对于变量,可以分为全局变量和局部变量
- 这样不好说,我们一起到代码里来看看
- 从下可以看出,global是一个全局变量,而num则是一个局部变量
- 然后我们看下面的这段代码,我在main函数内部又定义了一个global,然后此时我们在终端打印的时候这个global便成了50,
- 这是为什么呢?因为局部变量的global覆盖了全局的global,可以理解为是一个优先使用的原理
3、变量的使用
了解了什么是变量,现在我们就要具体地去使用这个变量了,让我们一起来看看
- 我们来看下这段代码📰
int main(void)
{
int num1 = 0;
int num2 = 0;
scanf("%d %d", &num1, &num2);
printf("sum = %d\n", num1 + num2);
return 0;
}
- 这是一个求和的功能,scanf的话就是我们上面所说到的【输入流】
- 接着我们再来看一下下面这段代码
- 可以看到,程序报出了错误,大家去试一下的话也是会这样,这是程序报出的一个运行错误,建议我们使用【scanf_s】这个来进行输入,但是可以对于这个【scanf_s】,它是VS里自带的输入流函数,若是放到其他平台上去就运行不了了,而【scanf】则是一个C语言的一个输入函数
- 但是大家仔细看我用箭头指向的这段,这也是编译器提供给我们的第二种解决方案,然后我们需要在整个程序的开头使用【#define】去给出这么一个文件定义,保证后面不会进行报错
- 最后还要加上一个【1】,不要忘记了
#define _CRT_SECURE_NO_WARNINGS 1
Everything
- 上面这个链接是干嘛的呢,对于这个定义,若是每次新建一个文件就重新敲一次,那是不是显得很麻烦,这个时候我们就有了一个快捷的办法,就是直接在VS2019的一个叫【newc++file.cpp】里面添加上这句定义,这样你每次新建一个源文件时就不需要去再打这个代码了
- 具体怎么操作呢,很简单,你只要点进我的这个链接,下载一个这个工具,然后在这个搜索框内输入newc++就可以看到这个文件,接着右击【打开路径】即可
- 在你安装VS的地方会有这个文件,然后用【记事本】的方式打开,将我上面这句代码写进去就行,然后你可能会失败,因为要管理员的权限才能修改内容
- 这时候你只需要将这个文件复制到桌面,然后一样修改便可,接着再拷贝回去,然后在你的编译器里新建一个源文件测试一下即可
- 这里可以看到,我们新建一个add.c,就可以看到默认出现了这个定义
4、变量的作用域和生命周期
清楚了如果使用变量去编写程序,接下来我们来了解一下变量的作用域和生命周期
作用域
作用域(scope)是程序设计概念,通常来说,一段程序代码中所用到的名字并不总是有效/可用的而限定这个名字的可用性的代码范围就是这个名字的作用域
- 了解了其基本概念,我们就到代码中来演示一下
- 可以看到,我们下面是写了一个代码块,然后里面定义了一个变量a,然后将其打印
- 接着在代码块的外层也打印了这个a,但是可以看到,程序报出了错误,说【未定义标识符】,所以可以看出这个作用域是具有限定范围的
- 以上的话就是局部变量的作用域,既然局部变量有作用域,那么全局变量也拥有作用域
- 从下面代码可以看出,完全没有报出错误,因为对于全局变量的作用域,是从定义变量开始到整个程序运行结束,整个变量才会被销毁,而这个变量a,则是在这个代码块结束之后便会销毁,所以我们在后面才访问不到这个变量
- 对于全局变量的作用域,我们还可以这么来看,把这个b变量定义在add.c这个文件下,然后我们在test.c这个文件定义一下这个变量b,用这个extern关键字声明一下即可,表明这个是一个外部变量
生命周期
变量的生命周期指的是变量的创建到变量的销毁之间的一个时间段
- 对于生命周期这个概念还是比较抽象的,它不是一个范围,而是一个时间段,就我们上面的代码看来,这个变量a,它的生命周期也就是从它在这个代码块定义开始到这个代码块结束,这就是它的生命周期
局部变量的生命周期是:进入作用域生命周期开始,出作用域生命周期结束。
全局变量的生命周期是:整个程序的生命周期
5、常量的种类
讲完了变量,接下去我们来讲讲常量
常量的定义形式和变量有所区别,在C语言中,常量一般分为以下几种
- 字面常量
- const 修饰的常变量
- #define定义的标识符常量
- 枚举常量
我们到代码中来看一下
//字面常量 - 直接写出来的这种数字
3.14;
100;
//const修饰的常量
const float PI = 3.14f;
PI = 6.18; //error - 常量不可直接修改
//#define的标识符常量
#define MAX 100
printf("max = %d\n",MAX); //这里的MAX也属于常量
-
看完了上面三种常量的表示形式,我们再来看最后一种枚举的形式【枚举在后面会细讲】
-
假设我们现在要定义一个性别的枚举体,然后里面有三种
enum Sex {
MALE,
FEMALE,
SECRET
};
- 然后我们来输出一下这些枚举体中的变量
int main(void)
{
printf("%d %d %d\n", MALE, FEMALE, SECRET);
//MALE = 3; 常量,不可修改
return 0;
}
- 可以看到,输出的是0 1 2,因为在枚举体中,默认是从0开始,然后依次递增
- 但是这可以修改吗?当然可以,比如说,你可以把MALE定义成为 = 4,这样后面输出的便是4 5 6,也是呈现一个依次递增的结果
- 然后我们对枚举体中的内容进行修改的时候,程序报出了错误,这就是因为枚举体中的内容全都是常量的原因,因此是不可以修改的
六 、字符串
1、概念
在讲完常量之后,我们来讲一讲字符串
- 在讲字符串之前,我们先来看看下面这个,因为在计算机中存储、表示的都是二进制,所以我们打印出来的也会是一个二进制
- 首先定义一个char类型的字符变量ch,然后分别以%c、%d、%s去打印出来,可以看到,显示的结果各不相同
- %c打印的就是一个字符,%d打印的则是这个字符在ASCLL码表中的值,最后的%s,我们打印的就是ch这个字符串
- 然后我们便开始讲一讲字符串
概念:双引号引起的一串字符叫做字符串
- 所以无论是多个字符还是单个字符,又或者是无字符,但只要是在双引号内部的,都可以叫做是字符串
int main(void)
{
//"zhangsan "
//"a"
//""
}
2、求解字符串的长度【strlen】
说完了字符串了基本概念,了解了什么是字符串,接下去我们来看看如何入求解字符串的长度
- 对于字符串,大家要知道,结束标志是一个 \0 的转义字符,转义字符我们下个模块就讲到,这个\0表示这个字符串已经结束了。但是在计算字符串长度的时候,这个\0是不计算在内的
- 也及时说,求字符串的长度统计的是字符串中\0之前出现多少个字
那我们如何去求解一个字符串的长度呢?
- 这里我们需要使用到一个string.h头文件里的一个函数,叫做strlen(),可以计算一个字符串的长度,也就是\0之前的长度,这个函数我们后面会细讲
printf("len = %d\n", strlen("abc")); //a b c \0
- 可以看到,长度为三,求的是\0之前的长度,但是这样看不太明显,我们再来看一个👀
- 可以看到,我们手动加上了一个\0,输出的len长度还是3,所以\0后面的def是没有被算进去的
前面我们说到过char类型可以定义一个字符变量,不仅如此,在这里,我们还可以定义一个字符数组,什么是字符数组呢?就是这个数组中存放的都是字符,我们来看一个具体的定义
char arr[] = "abcdef";
- 以上就是字符数组的基本定义
- 接着我们再来看一种,下面这一种也是字符数组的定义方式
char arr2[] = { 'a','b','c','d','e','f' };
- 可以看到,这两个字符数组中的内容是一样,那他们打印出来,以及计算出来的长度是否是一样的呢,我们一起来看一下
- 很明显,从以上结果来看,是不一样的,这是为什么呢?我们通过一张图示来了解一下
- 从以上这张图我们应该可以很明显地看出来,为什么第二个arr2数组在打印的时候会出现【烫烫烫】这种东西呢,如果你自己去编译器里试试的话应该也是这样
- 因为第二个数组的内容值给到了’f’,并不是一个完整的字符串,但是第一个数组给到的却是一个完整的字符串,所以自动拥有一个’\0’,第二个数组就没了,所以后面打印出来的只会是一个随机值,然后直至碰到一个\0为止,遍历才会结束
- 所以我们可以看到这个长度也是受到了影响,arr1数组的长度就是6,但是arr2数组的长度却是加上了一些随机字符后的长度,这就显得不准确了
3、转义字符【含面试题】
好,接下来我们再来说一个东西叫做什么呢?叫【转义字符】
- 那什么叫做转义字符呢?我们通过代码来看看
int main(void)
{
printf("abcndef");
printf("abc\ndef");
}
- 通过运行结果我们可以看到,这个n,若是在其前面加上了一个\,则它的意思就发生了变化,在转义字符里面就叫做【\n换行】,所以可以看到打印到【abc】就发生了换行,在后一行打印了def
- 然后我们再来看到一个打印一个test.c的路径,但是呢,从运行结果来看,并没有真正地打印出来一个路径,而是出现了很多空格,这是为什么呢?
- 细心的小伙伴应该可以发现这个test的第一个字符t和路径的\是同一个颜色,都淡化了,在转义字符里,这个叫做【\t水平制表符】,也及时们键盘上的【Tab键】,敲一下就能空出4个空格
- 所以我们在printf输出一个东西的时候,不要去写一些转义字符,那C语言中有哪些转义字符呢,我们一起来看一下
- 用一个表格给大家呈现📊
转义字符 | 释义 |
---|---|
? | 在书写连续多个问号时使用,防止他们被解析成三字母词 |
\ ’ | 用于表示字符常量 |
\“ | 用于表示一个字符串内部的双引号 |
\ \ | 用于表示一个反斜杠,防止它被解释为一个转义序列符 |
\a | 警告字符,蜂鸣 |
\b | 退格符 |
\f | 进纸符 |
\n | 换行 |
\r | 回车 |
\t | 水平制表符 |
\v | 垂直制表符 |
\ddd | ddd表示1~3个八进制的数字。如:\130X |
\xdd | dd表示2个十六进制数字。 如:\x30 0 |
- 我们挑重点的说一下
- 首先是二三两个,看第一行代码,若你printf的是一个【‘’‘】,那么编译器就会报错,你本是想打印一个【’】,这个时候该怎么办呢,你只需要在前面加上一个\,这就变成了一个转义字符,然后就可以像运行结果一样输出了
- 然后第三个也是同理,这里便不做过多详解
- 然后就是刚才我们所说的打印路径,要怎样才能打印出这个路径呢?没错,你只需要将【\】换成【\】就可以了,这就表示一个反斜杠,我们到代码里看一下
- 可以看到,这个路径已经被完全打印出来了
- 最后,我们再来讲一下最后两个有关八进制和十六进制的
- 一样,先给出运行结果
- 有关【\ddd】,表示的就是八进制,也就是通过八进制去计算,如果用%d打印出来的就是计算的结果,如果使用%c打印出来的就是这个数字所对应的ASCLL码符号
- 对于十六进制也是同理,/xdd值就是十进制,然后看上面的打印也是同理
- 对于【\x3a】的话就是用a*160
- 然后我们来说一道有关转义字符的面试题
printf("%d\n", strlen("c:\test\628\test.c"));
- 你认为这个结果打印出来是多少,我问了很多人,答案有18、15、14、13、12、11五花八门
- 而真正的答案是【14】,为什么呢?我们来看一下
分析
- 首先第一步,我们刚学了转义字符,【\t】表示的一定是一个字符,所以18排除
- 然后第二步,相信大家最疑惑的就是这个\628,这其实就是我们上面所说的\ddd,但是这都能算吗?这个时候你就要去想,八进制能包含8吗,八进制就是0~7的数字,所以是不会有8,因此这个【\62】算一个转义字符
- 最后我们就可以得出答案为【14】
七、注释
了解了字符串,接下来我们来看看C语言中的注释
首先我们要先了解一下注释是什么,它可以用来干嘛
1、注释可以将你不需要或者还不想删除地代码暂时屏蔽起来,在程序执行的时候会直接跳过注释的代码,会不运行
2、如果在一些程序中有一些晦涩难懂的代码,可能你写出来的代码需要需要一些批注后面在阅读你代码的人才能知道这段代码是什么意思,这个时候你就可以在这段代码的上方或是下方写一些注释,这样后人就可以看懂你写的代码
对于这一点,如果大家以后在成为程序员后进入企业工作也需要养着这样良好的习惯,在一段比较难懂的代码前添加一些注释,不仅是为了后人,你自己后面也可能会去阅读你自己写过的代码,这个时候若是你有一些注释写着,阅读起代码来就不会那么累了
int main(void)
{
//下面是创建一个整型变量并赋值10
int a = 10;
int b = 100;
//C++ 注释风格 - C99
/* C语言的注释风格
int c = 10;
int d = 20;
printf("ff");
*/ // - 嵌套的话此处结束注释
//不支持嵌套注释
return 0;
}
- 我们来分析一下,对于双斜杠//,这个是C99中对于C++的注释风格,那C语言的注释风格是怎样的呢,就是/**/
- 但是后面我还补充了一句话,就是对于C语言的这种注释方法,不可以产生嵌套的,例如说这里若是在外层加上一个/**/,这样return 0;不会被注释了,为什么呢?因为上面的/*和return 0;上面的那个 先匹配了,所以就不会到下面的
八、选择语句
说完了注释,接下去我们来讲讲选择语句
- 那有同学问,什么是选择语句呀?
- 在C语言或是其他编程语言中,都有着三种结构方式:【顺序】、【选择】和【循环】,之后我们也会细讲到。对于生活中的各种事情,都可以用这三种结构方式组成或者是嵌套形成
- 那这个选择语句是什么呢?就是你有时候会面临两种或多种选择,不同的选择对应的就是不同的结果
具体我们通过代码来看一下
int main(void)
{
int input;
printf("你要好好学习吗(1/0)\n");
scanf("%d", &input);
if (input == 1) {
printf("拿一个好offer\n");
}
else {
printf("回家种田\n");
}
return 0;
}
- 上面这一段代码,就是一个选择语句,你可以在终端输入你的选择,问你要不要好好学习。若你选择的是1好好学习,那么你就可以拿一个好offer;但若是你选择0摆烂,那么就只能回家种田
- 任何事情都取决于你的选择👈
九、循环语句
然后我们再来说说另一种形式,也就是循环语句
- 什么是循环呢?循环就是你一直不断重复地做一件事,直到某个条件满足时才会退出,比如我们作为一个学生都要日复一日地学习,大家以后走上工作岗位后要日复一日地工作🖊
- 然后我们通过一段代码再来看一下
int main(void)
{
int line = 0;
printf("好好学习\n");
while (line < 20000)
{
printf("写代码:%d\n", line);
line++;
}
if (line == 20000)
printf("好offer\n");
return 0;
}
- 作为一个程序员,你一定要多写代码,这里我们给到一个循环,你一直在写代码,写一行代码这个line就++,然后直到你写到20000行代码的时候,这个循环就退出了,然后你就找到了一个好offer
- 当然这只是举例,若是你真的像找到一个好工作,就要不断地写代码,不断地去提升自己
然后对于循环的话,不仅仅是有while,还有do…while(),for这些,我们在后续都会讲到
十、函数
接下去我们来说说函数,函数其实就是将一个功能单独封装成为一个模块,然后这个模块可以被多次调用,以便来实现代码的简化
- 我们先来看这么一段代码
- 这是一段两数求和的代码,输入两个数据,然后输出它们的和
- 但是设想,若是我们要再求另外两个数的和,那么就要再次输入,然后求和的代码就要再写一遍,这就徒增了代码量,显得整个程序很冗余,那要怎么简化呢?对就是使用函数
int main(void)
{
int num1 = 0, num2 = 0;
int sum = 0;
printf("请输入两个操作数字\n");
scanf("%d %d", &num1, &num2);
sum = num1 + num2;
printf("sum = %d\n", sum);
return 0;
}
- 我们来看一下使用函数简化完之后是什么样子
- 你不要看这段代码很长的感觉,但是我使用了三次求和,得到了三个求和结果
int Add(int m, int n)
{
return (m + n);
}
int main(void)
{
int sum1 = 0;
int sum2 = 0;
int sum3 = 0;
sum1 = Add(1, 2);
sum2 = Add(5, 7);
sum3 = Add(4, 9);
printf("sum1 = %d\n", sum1);
printf("sum2 = %d\n", sum2);
printf("sum3 = %d\n", sum3);
return 0;
}
原理剖析
- 可能还有同学对这个函数是如何传参调用的不太了解,我们通过图示来看一下
- 我们假设是看到的是一个工厂,工厂都要进原料,然后在工厂内部加工然后才能生产出一个个产品
- 然后将这个工厂映射为函数,这个原料其实就是我们所传入的实参,工厂内部会有人来接应这个原料然后送到对应的地方,这个就是形参,然后呢,将其送入到工厂内部加工,就是在函数内部对传入的形参进行相应的操作,然后这个返回值其实就是加工完之后的产品,它会进行一个返回,在主函数体里接收一下即可
- 上面就是我们对这个函数体的剖析👆
十一、数组
1、数组的定义
好,说完了函数,我们的内容也就讲了60%了,接下去我们来谈谈数组
- 首先我们知道数字这个概念,但是当我们需要一堆的数字,那么这个数字存到哪里去呢?没错,也就是用一个叫【数组】的东西存放起来
- 那要怎么存放呢,我们来看一下
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
- 首先你需要先声明这个数组是什么类型的,是整型、字符型还是浮点型等,数组的话这些数据都是可以存储的,然后在一个变量后面加上一个[],括号里可以写上你准备为这个数组开辟多大的空间,比如说上面写的是10,那么这个数组中最多能存下的数据也就只有10个,但是若你不写的话,就可以存多个,后面会教大家一个方法去计算没有给出数据具体大小如何去求这个数组的大小
- 这里先给出,大家可以先看看,sizeof()是一种单目操作符,是用来计算你所使用的操作数所占的空间字节大小
int sz = sizeof(a)/sizeof(a[0]);
- 刚才说过,数组除了可以存放整数数据外,还可以存放字符型、浮点型的数据
char b[] = { 'a','b','c'};
double c[] = {2.5,6.9,7.7,1.5 }
2、数组的下标
知道了数组怎么声明,那我们声明的这些数组怎么获取到呢?
- 没错,我们要通过下标去
- C语言规定:数组的每个元素都有一个下标,下标是从0开始的访问。下面就是我们通过下标去访问的a数组中下标为8的元素,打印出来的就会是9
//下标访问数组元素
printf("%d\n", a[8]);
- 具体大家看图示就能一目了然了📃
- 可以看到,这里arr数组的大小是10,那我们去访问18这下标会怎么样呢,去编译器里看看👀
- 从图中可以看出,总共下标也就只有9,你访问到了,那就会产生越界访问,那么你访问到的就会是一个随机值
3、数组的使用
了解了数组该如何定义,清楚了可以通过下标去访问数组,接下来我们来看看数组究竟如何使用
- 看一下代码。我们首先定义了一个大小而10的整型数组,然后将其内容初始化为0,然后我们通过一个在while循环中通过scanf去输入一些数据,将其一一地通过下标放入数组中。然后呢,还是一样,通过循环去遍历这个数组,调用printf去打印出里面的数据
- 这就是数组的一种使用方法,其余的我们放到后面数组章节细讲
int main(void)
{
int arr[10] = { 0 };
//0 ~ 9 - 输入10个值给数组
int i = 0;
while (i < 10) {
scanf("%d", &arr[i]);
i++;
}
i = 0;
while (i < 10) {
printf("%d ", arr[i]);
i++;
}
return 0;
}
十二、操作符
C语言中操作符不少,这里我们做简要介绍,后续会详细讲解
1、算数运算符
- 这里我们重点来讲讲【除】和【取余】
- 首先来看这段代码,你认为结果会是多少,3.5吗?
int a = 7 / 2;
printf("%d\n", a);
- 不,运行结果是3.因为运算符的左右两边都是整数,所以执行的是整数除法,包括下面这段也是一样,运行结果都是3,只是因为float浮点数的原因,小数点后多出6个0
float f = 7 / 2;
printf("%f\n", f);
- 那怎么将其变为浮点数除法,也就是得到3.5这个答案,你只需要将7或2任意一个数字改为浮点数即可,例如说7.0、2.0,至少有一个操作数是浮点数执行的才是浮点数除法
我们来看看结果👇
- 然后我们再来看看【取余】操作符
int main(void)
{
// % 取余操作符,关注的是除法后的余数
int a = 7 % 2; //商3余1
printf("%d\n", a);
return 0;
}
- 取到就是一个整数对另一个整数做除法后的余数
2、移位操作符
- 这一块大家先了解一下,先不做细讲
3、位操作符
- 上面叫移位,这里叫位,区别大吗?区别可大了,完全是两个概念,大家也先了解一下
4、赋值操作符
- 有关赋值运算符,第一个大家应该不陌生,就是我们常见的赋值运算,后面呢则是一些【加减乘除取余移位】这些复合而成的,你可以到编译器里自己试试看
5、单目操作符
重点来说一下单目运算法
- 可以看到,内容也是有很多,你可先浏览一遍
- 首先是这个取反操作,在C语言中呢,表示真假只有两种,用0表示假,用非0表示真
- 所以看到代码,这个a就是【真】,所以会执行“haha”语句,但若是你把a换成0,那么!a就是【真】,这个就是就会打印“hehe”语句
//C语言是如何表示真假的呢?
//0表示假,非0表示真
//-1 表示的就是真
int main(void)
{
//把假变成真,把真变成假
int a = 10;
if (a)
printf("haha\n");
if(!a) //这个不执行
printf("hehe\n");
return 0;
}
- 然后【取正】【取负】很简单,看到下面代码,会打印a的相反数
int a = -10;
printf("%d\n", +a);
printf("%d\n", -a);
- 这个取正取负不要和【双目运算符】中的加减搞混🔢
- 然后对于取地址和星号运算符,我们在下面指针的部分介绍📖
- 然后来看看sizeof()这个运算符,那有同学说,sizeof()不是一个函数吗??那你可不要混淆了,谁说带有小括号的就一定是函数,虽然这个小括号【()】也是一种运算符,叫做【函数调用】运算符
- 但是在这里,sizeof()只是一种运算符罢了,我们一起来看看
int a = 10;
char c = 'w';
int arr[10] = { 0 };
printf("%d\n", sizeof(a)); //4
printf("%d\n", sizeof(c)); //1
printf("%d\n", sizeof(arr)); //40
- 对于sizeof()这个运算符呢,它是用来计算所占内存空间的大小,单位是字节,所以上面代码的输出分别为整型、字符型和一个数组所占内存空间的大小
- 因为这些变量都是用int、char这些变量类型定义出来的,所以可以直接用sizeof()传入这些变量的类型,出来的结果也是一样的
printf("%d\n", sizeof(a)); //4
printf("%d\n", sizeof(int)); //4
printf("%d\n", sizeof(c)); //1
printf("%d\n", sizeof(char)); //1
可以看到,与我所写的完全吻合
- 那这时候又有同学问了,既然这是一个操作符,那干嘛要这个括号呢?直接去掉不就好了,不然引起歧义,就像下面这样👇
printf("%d\n", sizeof a);
- 那这样子可不可以呢?你去试一下就知道了,是不可以的。
- 这其实就更好地可以说明sizeof是一个操作符,不是函数,括号可以省略
- 然后我们就可以得出结论:变量可去括号,类型不可取去括号
printf("%d\n", sizeof int);
上面我们有提到过,当我们没有对一个数组设定初始化元素个数时,可以使用sizeof()去计算这个数组中有多少元素
- 就是下面这样,sizeof(arr)就是整个数组的大小,sizeof(arr[0])便是一个元素的大小,当然你也可以写成【sizeof(int)】,因为整型数组中一个元素的大小一定是4个字节,也就是int所占的字节大小
printf("%d\n", sizeof(arr));;
int sz = sizeof(arr) / sizeof(arr[0]);
printf("%d\n", sz);
那有同学经常会把sizeof()和strlen()这两个搞混,我们来辨析一下
-
strlen 是库函数,只能针对字符串。求字符串的长度。计算的是字符串\0之前的字符个数
-
sizeof 是操作符,是计算所占内存空间的大小
后面在讲面试题的时候会给大家再详细介绍的
二进制按位取反我们也放到后面详细介绍
然后我们来说一下 【前置、后置–】与【 前置、后置++】
- 这两个其实是一样的,我们先来看一下【 前置、后置++】
- 对于前置++的话就是先++在赋值,所以下面的a会把++完之后的值给到b,然后自己也会++,因此最后输出的便是11 11
int a = 10;
int b = ++a; //先++后赋值
// a = a + 1
// b = a
printf("%d %d\n", a, b); //11 11
- 对于后置++就不一样了,刚好相反,就是c会先把自己的值给到d,然后自己再++,所以d得到的就是10,c后面++完后之后就是11
int c = 10;
int d = c++; //先赋值后++
// d = c;
// c = c + 1
printf("%d %d\n", c, d); //11 10
- 然后我们来说【前置、后置–】,同理,大家自己分析一下即可🔍
int a = 10;
int b = --a; ///先--在赋值
printf("%d %d\n", a, b); //9 9
int c = 10;
int d = c--; ///先赋值后--
printf("%d %d\n", c, d); //9 10
- 然后这一段也是一样,首先打印a–的一定是–之前的值,然后打印的就是–之后的值,也就是9
int a = 10;
printf("%d\n", a--); //先使用,后--
printf("%d\n", a);
对于前置、后置的±,老是喜欢出一些面试题,把你搞晕,其实根本没什么意义,我们来看看
int a = 1;
int b = (++a) + (++a) + (++a);
printf("%d\n", b);
- 从VS来看,这段代码的运行结果是12,但是从下面的图示看出,在Linux的gcc编译器中,运行结果竟然是10
- 一段代码在不同编译器运行结果不同,说明这段代码其实有问题的,准确的说是存在歧义的
- 所以对于这些题我们应对面试就行了,不要去太过钻牛角尖,只会让你的知识点变得混乱
- 好,接下来呢,我们来讲单目操作符的最后一个,也就是强制类型转换
- 首先来看这两句代码,你认为这会输出什么
int a = 3.14;
printf("%d\n", a);
- 因为3.14给到了一个整型变量a,所以只会保留整数,这个时候大家看下面我用红笔画起来的一段Warning,说这个【从“double”转换到“int”】,可能会丢失数据
- 这个时候应该怎么办呢?你可以在3.14前面加上一个(int),将这个数强制转换成整型,也就是我上面注释掉的一行代码,这个时候你再去运行试试,就不会报出Warning了
6、关系操作符
- 对于前面的四个,直接用就可以了,和直观地进行一个比较。我们来讲一下后面的两个
- 也就是比较两个数是否相等与不等,这个要与【赋值运算符】中的【=】做区别,一个是比较两个数或是变量的,另一个则是进行赋值运算的,不混淆了
int a = 10;
int b = 20;![在这里插入图片描述](https://img-blog.csdnimg.cn/b0fd12d950cb47fa8b727c5cf40c058a.jpeg#pic_center)
if (a == b)
printf("haha\n");
if(a != b)
printf("hehe\n");
- 那这个时候就有同学问了,除了这个数字的比较,可以比较字符串吗,我们来看看
char arr1[] = "abcdef";
char arr2[] = "abcdef";
if (arr1 == arr2)
printf("==\n");
else
printf("!=\n");
- 可以看到,两个字符数组明明是一样的,但是却走了第二个分支,打印了【!=】,这里其实就出问题了。为什么呢?这个我们后面会说到,数组名是整个数组的首元素地址,其实【==】比较的是它们的地址
- 对于字符串的比较,在C语言中有专门的函数,叫做strcmp(),它和strlen()一样都是属于【string.h】头文件里的,若比较的两者相等的话,则会返回0,前者大于后者,返回 > 0的数,后者大于前置,返回 < 0的数,所以只需要去判断一下去和0的大小即可
//两个字符串不可以用“==”来判断是否相等,使用strcmp(库函数)
char arr1[] = "abcdef";
char arr2[] = "abcdef";
if (strcmp(arr1,arr2) == 0)
//if (arr1 == arr2)
printf("==\n");
else
printf("!=\n");
这样的话结果就正确了
7、逻辑操作符
- 对于逻辑与和逻辑或关注的是什么?就是真或者是假,这个我们在前面的单目运算符号【!】也提到过
- 逻辑与【&&】是并且的意思,逻辑或【| |】是或者的意思。为真则为1,为假则为0
- 对于逻辑与,只有两个数均为1是才为1,只要有一个为0,结果即为0
- 从上述代码和运行结果可以看出,因为a,b都不是0,因此它们的结果为1,才可以进入下面那个if判断,若是把a,b其中任意一个改为0,则结果便为0,然后不会进入下面的这个判断
- 再来看下面这个逻辑或,对于逻辑或的话,只要其中有一个为1,则为1,只有两个数均为0是,才为0
- 所以下面的显示结果是0,并且没有进入这个if条件的判断
8、条件操作符
然后的话是对于条件操作符这一块,其实就是三目运算符
- 下面是一段简单的if分支判断,然后给b赋值
int a = 10;
int b = 0;
if (a > 5)
b = 3;
else
b = -3;
printf("b = %d\n", b);
- 但是对于三目运算符来说,不用这么麻烦,只需要这么一句就好了
- 具体意思就是,判断a是否大于5,若是,则将b赋值为3,若不是,则将b赋值为-3
a > 5 ? b = 3 : b = -3;
- 但是其还有更简便的写法,也是一样去判断,最后把得出来的值给到左边的b即可
b = (a > 5 ? 3 : -3);
9、逗号表达式
我们先来说一下其运算规则:从左向右依次计算,整个表达式的结果是最后一个表示式的结果
- 列举了一个逗号表达式,大家可以去自己试着计算一下,每过一个表达式参与运算的变量都会改变,最后看打印出的a,b,c的值,也是发生了变化
int a = 3;
int b = 5;
int c = 0;
int d = (a += 2, b = b - c + a, c = a + b);
//a = 5 b = 10 c = 5 + 10 = 15
printf("d = %d\n", d);
printf("%d %d %d\n", a, b, c);
10、其他
还剩下三个四个,归不了类,就放到其他中讲一讲,首先就是这个【下标引用操作符】
- 什么是下标引用操作符呢?也就是这个[ ],我们在定义数组的时候指定的数组大小
int arr[10] = { 0 };
arr[4] = 5;
printf("%d\n", arr[4]);
- 对于上面这段代码,我们称arr 4 是 [ ]的两个操作符
- 我们回忆一下【+】运算符,这是一个双目运算符,比如说2 + 3,那么就可以称2 3 是 +的两个操作符,对于我们来说2 + 3可以写成3 + 2,既然双目运算符可以这么交换着来做,那【下标引用操作符】可以吗,答案是可以的!!!
- 上面这段代码,我们还可以写成这样
int arr[10] = { 0 };
4[arr] = 5;
printf("%d\n", 4[arr]);
- 怎么样,你是不是大为震撼,这居然也可以😯,如果不相信的话可以自己去编译器里试试哦
接下来的话是这个叫【函数调用】的操作符
int Add(int x, int y)
{
return (x + y);
}
int main(void)
{
int c = Add(2, 3); //()是函数调用操作符,操作数是:Add 2 3
printf("c = %d\n", c);
return 0;
}
- 很明确,就是函数外面的两个小括号,这个我们在上面说到sizeof()操作符时也提到过,对于sizeof(),虽然其有(),但是不可以把它认为是一个函数,它也会是一个操作符
- 然后对于函数调用操作符的话,看到上面的Add函数,()是操作符,那么操作数就是Add 2 3
十三、常见关键字
1、前言
了解了C语言中的常见运算符,接下来我们来看看C语言中的关键字
- 首先对于关键字,我们要注意的两点是
1、关键字是直接使用的,我们得了解
2、变量名不能是关键字
- 从下面这些可以看出,在C语言中,关键字还是蛮多的,我们尝试着给他们分分类🌟
- 首先单独说一下【auto】,它比较特殊,因为在编译器中,当你定义一个变量的时候默认就是存在的
- auto,翻译过来就是自动的,在C语言里指得是自动变量,所有你定义的局部变量都是自动创建、自动销毁的,所以局部变量都是auto修饰的
- 就像下面这个整型变量a,你在int的前面加上或是不加auto 都是不会报错的, 原因就是所有局部变量都是auto
//auto int a = 10;
int a = 10; //auto可以省略
2、关键字分类
然后我们再来看一下各种关键字的分类
- 大体地分了几个类,详细的我们到后面【关键字详解】这一章节再细讲
3、有关数据存储的底层原理
- 上面有说到一个寄存器关键字,有关寄存器这个东西,涉及到了数据存储,我们来详细说一下,让大家先了解一下这个底层原理
首先你要知道在计算机中的数据是存放在哪里的
①内存 ②硬盘 ③高速缓存 ④寄存器
然后我们再通过这张图来了解一下
- 对于计算机中的寄存器,其实它所空间是很小的,单位只有字节,但是它的读写速度非常快,可以直接与CPU【中央处理器】进行交互
- 然后越往下这个空间越大,内存的话现在普遍都是8G,16G这样,大一点有32G,不会像很早之前的只有2MB这样;对于硬盘的话,我们去市场上买也是500G,1个T这样的大小
- 继续说回我们的寄存器,因为它的读取速度很快,因此CPU直接到寄存器里面拿数据,但这个时候寄存器内存不够大了怎么办呢?装不过这么多,这个时候我们所知道的高速缓存,也就是Cache,会给寄存器提供内存,那高速缓存里又不够了,这个时候就继续让内存给它提供。这样的话整体地提高了计算机的运行效率
- 好,这里是说到了寄存器相关的底层知识,给大家拓展一下
- 下面就是【register】这个关键字的用法,在定义这个变量b的时候加上了这个关键字,就是【建议】编译器把这个变量放到寄存器中,这里要注意,只是建议,而不是直接放入
- 具体再如何使用大家可以去查阅一些资料,这里不做过多详解
//建议把b放到寄存器中
register int b = 10;
接下去我们单独再来重点讲一下两个关键字📕
4、typedef关键字
首先就是这个【typedef】,这个关键字的话是用来重命名的,用在结构体上会比较多
例如下面这个是【数据结构】中链表的存储结构体,也就是将【struct Linknode】这个结构体名字重命名成LNode,这样之后要定义这个结构体变量的时候就不需要再写【struct Linknode】了,结构体的话我们后面也会详细讲到📚
typedef struct Linknode{
int data[MaxSize];
struct Linknode* next;
}LNode
- 当然它不止应用在结构体上,对于一些数据类型也是可以重命名的,例如下面这个【unsigned int】,后面你在使用【uint】定义变量的时候就和【unsigned int】一样
typedef unsigned int uint;
int main(void)
{
unsigned int num1 = 0;
uint num2 = 0; //与num1一样
return 0;
}
5、static关键字
最后再来说一下这个static关键字,这个关键字的话我在Java专栏中也是有过介绍,感兴趣也可以去看看Java|static关键字
- 这里是来说说C语言中的static关键字,首先你要了解static关键字可以用来修饰什么,它主要是可以用来修饰下面三个
5.1、static关键字修饰局部变量
首先我们来看看static对于局部变量的修饰
void test()
{
int a = 3;
a++;
printf("%d ", a);
}
int main(void)
{
int i = 0;
while (i < 10)
{
test();
i++;
}
return 0;
}
- 你认为上面这段程序会输出什么。看到主程序,使用while循环来控制i变量,当i = 10的时候边不会进入循环,所以是会循环10次,然后看内部的test()函数,每次循环调用这个函数的时候都会定义一个变量a,然后++之后变为4,然后输出
- 所以这段程序的运行结果是会输出10个4
- 然后我们修改一下这个定义的变量a,将其设置为静态变量,也就是在int前面加上一个static修饰
- 那这个时候你认为上面那段程序会打印出什么呢?
static int a = 3;
- 首先你要了解静态变量的特性以及其余普通变量之前的区别
①普通的局部变量是放在内存的栈区上的,进入局部范围,变量创建,出了局部范围变量销毁
②当static修饰局部变量时,局部变量是在静态区开辟空间的,这时的局部变量,出了作用域变量不销毁,下次进去作用域,使用的是上一次遗留的数据
(改变了存储位置,由栈区–>静态区,使得变量的生命周期发生了变化)
- 知道了这些,你应该清楚这个打印结果是多少了,没错,就是4~13,每一次进入这个test()函数时,这个变量a将不再被执行,也就是只会在第一次进入这个函数的时候定义,之后就会一直存在,知道这个程序结束时它才会被销毁,所以这个变量a每次保留的便是上一次++后的结果
- 看到了上面的结果,对于静态变量修饰成员相信你也有了一个初步的了解,对于普通的局部变量,是存储在栈区上的。但是对于静态变量,你知道它是存储在什么地方的吗?
- 没错,就静态区,我们通过下面这张图再来了解一下在计算机内存中变量到底是如何存储的
❗内存原理图❗
- 可以看到,在静态区里,不仅仅是有静态变量,还有这个全局变量,接下去就让我们来看看【static关键字修饰全局变量】
5.2、static关键字修饰全局变量
//add.c
int g_val = 2022;
//test.c
extern int g_val; //声明外部符号
int main(void)
{
//2.修饰全局变量
printf("%d\n", g_val);
return 0;
}
- 以上的这种全局变量声明,以及【extern】关键字调用,我们在上面有讲到过,这是可以运行的,但是若这个g_val变量被定义成了静态变量,会怎么样呢?我们来看看
-
可以看到,这个外部命令无法被解析,注解中我也有写到
全局变量是具有外部链接属性的,如果全局变量被static修饰,外部链接属性就变成了内部链接属性,其他源文件就没法再通过链接找到这个符号
-
所以可以得出结论,static修饰后的局部变量只能在自己所在的.c文件内部使用~
5.3、static关键字修饰函数
接下去我们再来看看用static关键字去修饰函数
- 其实这个理念也是一样的,若你不使用static修饰,那你可以用extern关键字做一个引入,那它就是一个外部链接,但若是你使用static修饰,那么这个函数就只能本源文件使用,不可以给到外部的文件使用,这就是一个内部链接了
- 我们可以来试一下
- 可以看到,也是同理,这是一个内部链接,外部是访问不到的,即使是有这个extern关键字
十四、 #define 定义常量和宏
说完了操作符、关键字,内容就只剩20%了,接下去我们来讲讲【#define 定义常量和宏】
- 首先来说说这个#define去定义常量
#define MAX 100
int main(void)
{
printf("%d\n", MAX);
int a = MAX;
int arr[MAX] = { 0 };
printf("%d\n", a);
return 0;
}
- 看到如上代码,我使用#define定义了一个MAX常量,并且其值为100,在main函数中,你就可以直接使用这个常量,对它进行打印、赋值
- 然后还可以对数组进行初始化,之前在数组模块中有说到过,不可以使用变量对数组进行初始化,也就是像下面这样,这是C89中规定的
int n = 10;
int a[n];
-
我上面这种初始化方式是使用常量进行初始化,这是可以的。虽然在C99中又说了支持了这种变长数组,但是我们还是不要养成这种习惯,对于数组的初始化要么直接指定长度、或者是干脆不指定,但是后面要添加一个{0},否则也是不对的;再或者就是用我们这种常量的形式进行一个初始化,也是可以的
-
当然除了定义整型数据常量,其他类型也是可以的,例如字符串也可以
-
就像这样👇,去打印出来的话和指定定义一个字符串常量是等同的
#define STR "abcdef"
printf("%s\n", STR);
讲完使用#define去定义常量,我们再来说说宏定义,它也是利用#define去声明的
- 下面有一个求和的函数以及宏定义求和的写法,你可以对照一下
//函数
int Add(int x, int y)
{
return x + y;
}
//宏
#define ADD(x,y) ((x) + (y)) //为了保持整体性
- 然后你一定会发现一点的是使用宏来完成求和的功能更加简便,确实是这样,但是宏其实也是有缺陷的,看到代码,我对于参数x和y都加了小括号这可以保证参数的整体性,但如果你不使用这个小括号的话,那程序在传参的时候就会出现问题了。而且宏它是没有作用域这么一个概念的,作用域我们上面有讲到过,就是一个代码、一个变量的作用返回,可在宏定义中你无法去看到这个代码的作用域。最后一点,也是最重要的一点,对于宏的话我们是无法进入其内部进行调试的,调试对于我们每个程序员来说都是一个基本技能,若是一段内嵌的代码,我们无法进去调试的话,这样就看不到计算机的运行思维,对于一些大型程序来说就增大了程序的阅读性。看了上述我所说,你应该明白了宏有哪些缺陷了
- 当然这里只是简介一下,具体的在后期学习下去还是会进行一个详细说明的。
除了求和的功能外,其实你还可以定义其他功能,例如说比较两个数的较大值,就可以像下面这么去写,使用一个三目运算符即可
#define MAX(x,y) ((x) > (y) ? (x) : (y))
十五、 指针
说了那么多,终于是讲到指针了,关于指针这一块,是C语言中最重要的知识点,也是较难理解的,这里给大家做一个入门,后期也会做一个详细的介绍
1、引言
- 对于指针这一块,很多同学在刚开始学习C语言的时候就听别人说起过,说指针很难很难,访问内存、取地址这些操作,既危险又难搞,所以都被吓坏了。
- 但其实呢,指针并没有你们想象中得那么复杂,用宇哥的话来说,那这个东西其实就是【纸老虎(paper tiger)】,一捅就破,只是你不敢去尝试罢了。这些东西其实都是人脑设计出来的,只要你动脑去思考,那一定是没问题的
- 浙大有一位教师,它也是教授C语言的,我在慕课上听过他的课,他说“在学习计算的时候,一定要建立一个强大的内心”,那我觉得这句话说得很好,如果你没有一个强大的内心,你怎么去应对如此多繁杂的事物呢,是吧。这位老师叫做【翁恺】,大家也可以去听听他的课,感受一下如果去学习计算机
说了这么多,接下去就让我们进入指针的学习吧📚
2、内存的概念和介绍
- 对于指针这一块的话,是直接和计算机中的内存发生关系的,所以我们先来讲讲内存相关的知识,带大家先行了解一下底层的知识
- 对于内存,大家在日常生活中应该也有听到过,例如我们为电脑、笔记本买的内存条,以及我们手机的内存,对于这个内存来说,一般都是8G或是16个G,与内存相对应的,那就是硬盘,一般的话都是500G或是1个T这样
- 那了解了我们生活中的内存,那有同学就会想,这个内存在生活中我是知道了,但是在这个计算机中、在编译器中又是什么样的呢?我们继续来探讨一下
- 在计算机中呢,内存是一块连续的存储空间,但是这样就无法分配到每一个部分进行使用,这个时候呢就将内存分成了一块块小的内存单元,那为了能够有效的访问到内存的每个单元,就给内存单元进行了编号,这些编号就被称为该内存单元的地址
- 从上面这张图示,就可以很直观地看出内存在计算机中到底是如何存储的,每一个内存单元的大小都是一个字节,然后为它们都一一进行了编号,这样在外界需要访问时,就可以根据这个内存编号去指定进行一个访问,那上面说到了,这个一个个编号其实就被称为是地址
知晓了地址的基本概念后,接下去让我们到编译器中去看看这个地址究竟是怎样分部的🏡
- 首先我们来看最简单的一句代码,就是定义一个变量a,我们都知道int整型的变量在内存中是占4个字节的,我们在这句代码上打个断点进入内存窗口一看究竟
int main(void)
{
int a = 4;
}
- 然后呢输入这个【&a】,就可以看到内存中为变量a开辟的4个内存单元,首地址就是从【0x0052FBC4】开始,整型变量占4个字节,看我框起来的这4个就是,对于每一个字节它都有自己的一个编号,&a呢就是取到第一个字节的地址
- 那我们要怎么使用代码去获取这个地址呢,看,就像这样
&a //拿到的值第一个字节的地址
- 我们通过调试窗口再来看一下。很明显,得到了我们想要的结果
- 或者你不想到调试窗口中去看的话,也是可以的,我们直接printf打印出来即可,可以看到,这里使用的是%p,这是专门对于地址访问的,如果你是%d,出来的就是格式化后的数字了
- 讲完了内存,讲完了地址,接下去才是真正的指针,但其实对于地址来说,它就是指针,指针是地址的一个别名,下面就来看一下指针是如何去定义的
int a = 4;
int* pa = &a;
- 可以看到,在int类型后我加上了一个【*】星号,这就说明这是一个指针类型的变量,这个pa就是【指针变量】,因为上面说过指针是地址的别名,所以这个等式是成立的,pa这个指针变量可以去接收a的地址,也存放了a的地址
- 那如果你还是不太懂的话我画了一张图,希望你通过这张图能够对指针变量存放地址有一个理解🎓
- 那对于这个指针变量,难道只能存储整数的变量。当然不是,若是你要存储一个char类型的数据,这时候只要把指针变量的类型从【int*】改成【char*】就可以了,就像下面这样。那如果你对这个都理解了,那上面的也不会有问题
char ch = "w";
char* pc = &ch;
博主拍了拍你,问了你个问题说:既然这个指针变量存放了变量的地址,那么可不可以通过这个指针变量去访问到这个地址并且把它打印出来呢?答案是可以的,这就是涉及到我们的下一个知识点,就是指针的解引用
- 那这个解引用怎么操作呢?就是利用【*】这个操作符,这个我们前面说要放到指针这里来讲,现在我们就来说一说,这个星号,其实不仅是作为定义指针变量的符号,也可以将它叫做【解引用】操作符,例如下面这样
*pa
- 上面定义了,pa就是一个指针变量,*pa其实就是通过pa中存放的地址,找到这个地址所存放的空间,这个时候取到的其实就是变量a,因为取到了这个地址,这块空间上所存放的起始就是a变量的内容,我们通过运行来看一下
- 从上述运行图可以看出,&a和指针变量pa的地址是一样的,这就印证了我们最初的说法,指针变量里存放的是变量的地址,然后*pa解引出来就是变量a中存放的内容,通过a的地址成功地找到了a这块内存空间
小结
- 通过上面一系列的叙述和讲解,你对内存、地址、指针这三块之间的关系有没有一个初步了解了呢,我们来总结一下
①指针其实就是地址,地址就是内存单元的编号
②把地址进行存储的时候,就可以放到一个变量中,这个变量就是【指针变量】
③我们说的指针,实际上在专业术语中叫做【指针变量】
④【*】星号解引用可以通过存放变量的地址快速访问到那个变量在内存中所存放的位置,继而获取内容
3、指针变量的大小
我们都知道,每一个变量都是有大小的,它们在内存中的大小取决于它们的变量类型,如果你忘记了,可以再翻上去看看,但是对于指针变量,它有大小吗?我们来探讨一下
- 求一个变量的大小,你还记得用什么吗?没错,就是sizeof()这个关键字
int a = 10;
printf("%d\n",sizeof(a));
printf("%d\n",sizeof(int));
- 上面这段代码的打印就是4 4,那请问下面这段呢?
int a = 10;
int* pa = &a;
printf("%d\n",sizeof(pa));
printf("%d\n",sizeof(int*));
- 很明显,也是4 4,你算对了吗。
- 在编译器的偏左上角,有一个x64和x86,这个东西叫做【运行环境】,x64代表你在64位OS的环境下运行代码,x86代表的就是32位,刚才我选择的是32位,现在我把它改成64位,你觉得会发生什么变化?我们一起来看一看
- 很明显,一样的代码,但是在不同的运行环境下所产生的值却不通过,这是为什么呢?那这个时候就有同学迷茫了,这个指针变量的大小到底取决于什么呢?
- 这个时候其实又要追溯到地址这一块的知识了,为什么又要牵扯到地址呀!!!
- 因为指针变量存放的就是地址,所以指针变量的大小取决于存储一个地址需要多大的空间
要看到不是地址,其实是存储的地址线,没错,就是硬件上的地址线
- 我们所用的电脑,其实就是硬件,是硬件的话就需要通电,那在我们的电脑上其实就存在着这么一种【地址线】,我们上面所说的32位与64位,也可以对应到这个地址线中,因为在32位的环境下,是32根地址线;64位环境下就是64根地址线
- 当我们是32根地址线时,在通电之后就会有一个电信号,这个电信号就是0/1,那这些电信号具体是怎样的呢,我们来看一下
- 就是0101这样的存储方式,然后根据二进制的逢二进一去罗列出这32根地址线可以存储下多少地址,这里告诉你,一共是有232个地址可以存储
- 那这其实就可以得出结论了,32个0或者1组成得的地址序列,需要32个bit,也就是4个byte去存放,而这4个字节也就对应着我们指针变量的大小,因此就可以得出为什么指针变量的大小是4个字节了
- 然后来解释一下为什么在64位环境下这个指针变量就变成了8个字节,这其实你自己也可以去推导,32个0或1组成的地址序列需要32个比特位,那么这里便需要64个比特位,根据1B = 8bit,所以就需要8个byte去存放,这也可以得出在64位环境下指针变量的大小是8个字节
了解了上面这些,知道了指针变量的大小取决于地址的大小,下面我们来看看这些指针变量的大小是多少,我是在32位环境下运行的
printf("%d\n", sizeof(short*));
printf("%d\n", sizeof(long*));
printf("%d\n", sizeof(long long*));
printf("%d\n", sizeof(float*));
printf("%d\n", sizeof(double*));
- 是1 4 8 4 8 吗,如果是这个答案的话请你再回去仔细看一下上面的推导过程
- 我们来看一下运行结果
- 可以看到,均为4,为什么呢?因为它们都是指针变量,只要是指针变量那么求它的大小看的是什么,没错,看到就是地址的大小,上面说了,我是在32位环境下运行的,因此就是32根地址线,需要32个bit,也就是4个byte去存放,继而退出这个指针变量的大小是4B
接下来给你看个很细的东西
- 首先肯定不是这个字,而是这个【%d】,为什么要说这个呢,给你看一下我运行后的结果过,可以看到,报了很多Warning,这是为什么呢,明明这个代码就是可以运行的,而且还可以出结果
- 但是能出结果的代码就一定没问题吗?这个见解可不对,你要自己分析或者看编译器给你报出的问题。可以看到,这里报了一个【符号不匹配】的问题,为什么呢?这里明确说一下,sizeof()计算数据字节大小的时候默认返回的类型是unsigned int,也就是无符号整型,但%d是用来打印整型变量的,所以这里才会出现【符号不匹配】的问题
- 那这该怎么改呢,应该将其修改问%zu去打印才对,你只要记住它是专门用来打印sizeof()的返回值的就行了,不行深入了解也没关系
- 修改如下,可以看到,已经一个Warning都没有了
十六、结构体
OK,终于是到了最后一个模块,对于结构体,也是C语言中比较重要的一个部分,因为C语言是一门面向过程的语言,它不像C++、Java那样面向对象,具有类,可以在类内定义成员变量和成员方法,所以C中就有了结构体这一个东西,可以用来描述复杂对象
- 我们都去书店买过书,知道书它有书名、出版社、作者,这些都可以定义为字符类型,但是还有书的价格,怎么定义呢,难道也定义成字符类型吗,当然不是,依旧是定义成浮点类型,除了这些,其实还有很多种类需要去定义
- 但是对于这么多的类型,都要分开吗,这肯定不行,这样这本书就不是一个整体了,如果你有面向对象的思维就知道,它的属性和行为都是定义在这一个类中,都是封装好的,这就是类的封装
- 在C语言中,我们也可以实现封装,那就是用结构体
我们以学生类型来做一个封装
- 可以看到,我们使用到了struct这个关键字,这个就是在关键字那一模块所属要留在这里讲解的关键字,stu就是student的简写。可以看到,里面有着三种类型,分别是姓名、年龄和成绩,因为一个学生都具备这三种属性,这是他们共同的属性,所以可以将他们封装在一起
struct stu {
char name[20]; //姓名
int age; //年龄
float score; //成绩
};
- 那对于结构体这种复合类型怎么去定义变量进行初始化呢,我们来看看
- 其实和普通变量也是一样的,你要把【struct stu】看做是一个类型,用这个类型定义出来的变量就叫做【结构体变量】,对于每个结构体变量的初始化,都需要使用一对大括号{},里面去一一按照顺序去初始化你在结构体中定义的变量
- 温馨提示:这里的float成绩类型后面加上f与double类型作为区分
struct stu s1 = { "zhangsan",20,80.5f };
struct stu s2 = { "lisi",25,90.5f };
那初始化了这些变量后如何将他们打印出来呢,也就访问到每一个同学的信息
- 这里就要使用到【.】点操作符,这个也是我们前面留下的,这个操作符可以去通过结构体声明出来的变量访问当前这个变量所具有的信息,代码如下
//格式:结构体变量.结构体成员
printf("%s %d %f\n", s1.name, s1.age, s1.score);
printf("%s %d %f\n", s2.name, s2.age, s2.score);
拓展
再来做一个拓展,看看你对上面的知识是否真的掌握了,会涉及到函数、指针、结构体哦📕
- 现在我要将打印学生信息这个功能封装成一个函数,之后我只需要调用这个函数就可以实现学生信息的打印,不需要再写printf了,请你实现一下
void Print(struct stu* ps)
- 首先我问你【struct stu*】是什么,没错是个指针类型,什么类型呢,【结构体指针类型】,这个ps是什么,就是指针变量,前面说到,指针变量可以存放变量的地址,并且需要同种类型的,那我在main函数中声明出来的结构体变量可以直接传给这个函数做实参吗?
- 当然不可以,你还要加上【&】取址符,这个时候指针变量接收的就是一个变量的地址,然后在Print函数中,我们通过这个指针就可以去访问这个变量所存储的内容,代码如下
Print(&s2);
printf("%s %d %f\n", (*ps).name, (*ps).age, (*ps).score);
- 这就是我们刚才所说的【解引用】操作符,它在结构体中依然适用
- 我们还有一个操作符没讲,你还记得吗,就是【->】这个箭头操作符,这其实才是指针所需要使用的操作符,无需解引用,直接用箭头就可访问
- 代码如下,很直观明了👇
//格式:结构体指针->结构体成员
printf("%s %d %f\n", ps->name, ps->age, ps->score);
- 最后一个,对于结构体声明的变量,难道只能在定义的时候初始化吗,可不可以自行输入?当然是可以的,格式与普通的变量输入也是一致
scanf("%s %d %f", s2.name, &s2.age, &s2.score);
- 细心的同学可能发现了,对于名字为什么没有&符号去输入,你在仔细看一下name我定义的是什么,是一个字符数组,我们有讲到,数组名其实就是首元素地址,那这已经是一个地址了,为什么还要再去取地址呢,是吧,对于数组类型的话,无需【&】,直接输入即可
十七、总结与提炼
以上十六个大点,就是我罗列出来的C语言中一些较为重要而且必须掌握的知识点,可作为一些刚入门的同学进行一个整体性的了解
- 回顾一下所有的知识,一开始我们首先使用VS2019编译器写了我们第一个C语言程序,出入了解了C语言如何去进行编程🐏
- 接着我们了解到一些基本的数据类型,有char、short、int、long、long long、float、double这些
- 知道了C语言中有常量与变量,并且清楚了它们的分类
- 再者了解到了字符串相关的知识,知道了如何去求解一个字符串的长度。然后又知道了有注释这么一个帮助理解代码又可以暂时隐藏代码的东西😀
- 又然后呢进入了选择语句和循环语句的学习,了解了面向过程如何去实现
- 学完这些后开始了解函数,知道了可以将一些功能单独封装为一个模块进行后续的调用
- 有了函数,那一定少不了数组这个存放大量数据的东西,也方便了我们对于数据的访问和存放
- 各种操作符和关键字看得眼花缭乱😵,但是又不得不讲的很仔细,生怕大家听不懂
- 然后呢了解到了#define去定义常量和宏,要注意不可以滥用哦,可以有缺陷的❗
- 又爱又恨的指针出现了,地址、内存与指针之间的联系还记得吗,忘了就再上去看一遍吧
- 最后的舞台当然是留给结构体啦,封装好所有的数据,就想一个大房间,大家都可以住进去🏠
好,讲到这里,我们初识C语言的内容就全部结束了,本文大概是用了4万多个字讲解了C语言中一些较为基础也是较为重要的知识点,本文仅作为初学阶段的入门了解,并不能作为后续的复习。最后,希望这篇文章对您有所帮助,可以大致地了解到C语言到底会学些什么知识,在编程之路的行走不再迷茫