C语言入门


前言

看到最近很多新同学迫切想要踏入计算机的新世界,我被他们这种热情鼓舞,我们对编程的热爱通过网络相互链接发生了共鸣!

所以作为一个过来人,我也想给Ta们一些力所能及的帮助。

本文是引导新人们学习编程,对一些常见的问题会做尽可能通俗易懂的解释,所以并不十分严谨,如有疏漏之处,还请见谅!

学习编程常见的问题

计算机类学科课程

不同的学科学习的内容不太一样,计算机大类公有的专业核心课程大概是这些,后面的数字代表我个人认为的学习难度,数字越大代表越难

面向过程编程(1)
这个通常是指的C语言编程,至于何为面向过程,我们后面再讨论这个问题

通过这门课程能够认识到编程的基本知识,接触到编程的逻辑思想,为后续的学习开辟了一条道路

数据结构与算法(5)
编程的逻辑思想与数学的有机结合,可以看做是编程的灵魂,对于性能的优化提升有着极为重要的影响

通过这门课程能够让你认识到数学的重要性

面向对象编程(1)
通常是Java编程或者C++编程等等,至于什么是面向对象,我们也放到后面讨论

通过这门课程能学习使用现代工程开发中更加高级的工具,更加高效稳定

计算机网络(3)
揭示联通千家万户的神秘力量幕后的真相,是开发现代应用程序必不可少的技术

通过这门课程,更深刻地认识到互联网,以及网络开发技术的原理,再次认知到数据结构与算法的重要性

操作系统(3)
认识到不同的计算机系统,如UNIXLINUXMACWINDOWS等等,了解底层技术的发展历程,建立从认知软件到认知硬件的桥梁

通过这门课程能够认识到计算机的过去和发展历程中解决各种实际问题所产生的智慧结晶,并且又一次感叹数据结构与算法的重要性

数据库(2)
后端开发、运维等职业需要专精的内容,从MySQLMSSQL到各种No-SQL认识并学会使用多种数据库

通过这门课程的学习,结合前面所学知识,将拥有完整项目开发的能力,当然,如果进一步深入学习,又又又会感慨数据结构与算法的重要性

当然,我们可别忘了计算机的好伙伴数学啊~

数学类通常是这几门课,每一门数学都是无底洞难度都是无穷大,后面的数字仅代表这门课程在我们学校的考试难度,不代表这个学科的难度

高等数学(3)
工科理科必学内容,花式积分

离散数学(5)
简直是为计算机而生的数学,是数据结构与算法的前置课程,也就是说它是在为数据结构与算法打下基础,内含各种算法、集合论、图论等等

线性代数(2)
数学建模选手必学内容,花样矩阵运算,对于一些算法能起到极大的优化作用

概率统计(2)
高中学过一部分内容,也就是排列组合,但是会在这个基础上变得更难

当然,还有一些其他课程,但是不同学校安排不一样,所以也不再进一步举例

计算机类专业

这个也很多了,不同学校安排不一样,甚至对于同一个专业的分类也不一样

这里例举几个我们学校的

计算机科学与技术
简称计科,从名字上听一下就知道地位了吧,其他专业都是计科身上分流出去的,科学——这个含金量懂吧

软件工程
简称软工,学的都是开发相关的理论和技术,相对而言更侧重就业,可以说是和计科并驾齐驱了

大数据科学
全名太长了忘了叫什么,简称就是大数据,算是一个人工智能研究的前置专业,未来考虑做人工智能、机器学习等方面研究的话可以考虑

网络安全
简称网安,可以理解为人均黑客,或者,网警…对于安全和漏洞方面的认知更加深刻

物联网工程
简称物联,我都不想说了,只能像是从隔壁机电院或者电信院走错了片场的专业,学很多硬件相关的东西,学习内容和自动化、通信工程差不多,一些学校并不把它归为计算机类

计算机考研和就业

先说考研考什么

专业核心课程
数据结构与算法,计算机网络,操作系统
(面试还可能现场写代码)

数学类课程
学过的都考,高数、离散、线代、概统

政治类课程
就是各种政治的东西

英语
就是一堆很难的英语,听说比六级还难

再说说就业方向

软件开发
前端开发、后端开发、运维、架构、软件测试…

硬件开发
嵌入式开发…(不太了解硬件方面的)

研发(其实也算开发)
算法、机器学习…

其他
产品经理、平面设计(UI,CG,建模等)、运营、策划…

那么我选什么呢?我选就业,因为站在风口浪尖上猪都能起飞

计算机行业中,很多时候是实力至上的,你只要足够强,甚至能弥补一些学历上的差距

当然,客观而言学历也是实力的一部分,一些方向对于学历的要求也是很严格的

另外一些学长学姐的事迹让我反思,结合我自身的实际情况,我认为就业更适合我

当然这并不意味着就不用去进一步钻研,正相反,如果就业了就停止学习进步的话,那么是很快就会被淘汰的

不过,希望大家不要被我这里的话影响到自己的想法,还是要从自身的实际情况出发进行考量,毕竟适合自己的才是最好的

计算机是否有性别优势

这个问题可能是源自一些刻板印象,但是我希望不要因为性别缘故就断言不适合计算机或者别的方向

男性我就不说了,理工多半和尚庙…

世界上第一个程序员是女性,她解决了世界上第一个BUG,领导组织编写了阿波罗登月的程序,开辟了软件工程这个学科…

请不要因为性别缘故就觉得自己不适合计算机,这只是一种刻板印象罢了,这个专业本身难度就不小,所以不要把这种难度强加给性别因素

实际上,在同等技术水平的情况下,行业通常会优先选择女性,所以这点完全可以放心

什么是编程

编程就是编写程序,写代码

你用的手机,那是嵌入式工程师写好了控制硬件的代码,才能够通过触摸手机屏幕改变电压,进而打开各种APP

你输入账号密码,能够安全并且成功登陆,那是前端工程师的代码通过网络,向后端工程师的代码呈递你的数据,并且在数据库中成功访问后又回传一份“成功”的标志后才实现的

购物节的狂欢,是无数服务器高并发的噩梦,上千亿调数据将同时发生变化,是优质的算法优化使得服务器在这种极端情况下高效且稳定运行得以实现

短视频的精准推送,是大数据采样后机器学习为每个用户带来的优质反馈,是算法,让数据变得更加高效起来

还有更加前沿的计算机科学家们,带领我们从”越过长城”到”走向世界”,为人类历史增添更多激励人心的壮举

每一个看似平常的事物背后,都有着无数编程人员夜以继日的努力,Ta们都是一群梦想家,希望用自己的力量让世界变得更好,所以每一个程序员进入计算机世界的那一天,都写下过这样一段文字:

Hello World

无论是面向过程,面向函数,还是面向对象,都应该面向世界,这就是程序员的誓言,为了让世界变得更美好而不懈奋斗

这就是程序员,这就是编程

如何学习编程

千里之行,始于足下。

没有苟且也就没有远方。

个人建议是通过学习C语言入门编程,因为C语言是面向过程的,这更利于学习编程的逻辑思维。

至于具体的学习方式,这里需要强调的是大家先要拜托高中学习的思维,不一定就是要老师来教课本上的内容,更多的还是要去自主探索未知的领域,所以除了本篇博文之外,我个人还鼓励大家通过以下方式进行学习:

看书
如果是刚入门某种技术的话,个人不建议先看书,因为书籍一般会比较系统全面,对初学者不太友好,更多时候是在有一定基础后再通过看书补充。个人推荐《C Primer》这本书,厚度合适而且非常友好。

看文档
不少技术是有官方文档的,官方文档一般会权衡全面和易懂两个方面,当然这个得具体情况具体分析

另外还有一些技术是由第三方或者个人写的文档,也是可以用于学习的(但是注意其正确性,一切以实践为准)

实践
一定要写代码!无论你怎么学的,一定要落实到代码上!没有实践都是空谈!不实践理论再丰富没有成果都是零

看视频
很多入门技术都可以通过看视频学习,视频一般对新手很友好,而且支持回放等等利于反复研究,是大学学习的一种常用方式。个人推荐郝斌和翁凯两位老师的课程,都非常不错,直接搜索就能找到相关的课程。

看博客
像你现在正在做的这样

看代码
对于一些理论性极强或者说十分抽象的内容,正所谓“百闻不如一见”,找一个参考项目研究具体的代码无疑会有相当大的帮助

找个大佬带
算是可遇不可求了

总结一下就是,上网查资料 + 实践 ,一定要实践!

面向过程 和 面向对象

之前提到过很多次面向过程、面向对象之类的,那么它们究竟是什么?

这里由于还没有学过对象的概念,此处先简单解释一下对象:

对象是一系列属性和行为的集合,所以

万物皆对象

什么,那么太阳是对象,人类是对象,花花草草都是对象?

是的,它们可以被这样表示:

太阳: {
	属性:{ 大小, 形状, .... },
	行为: { 自转, 辐射, ....}
}
人类: {
	属性: { 名字, 年龄, 身高, ... },
	行为: { 吃饭, 睡觉, 学习, ... }
}
花草: {
	属性: { 名字, 颜色, 大小, ... },
	行为: { 光合作用, 随风摇曳, ... }
}

一切事物都可以被描述,那么它们都可以被抽象地看做是 对象,这就利于数据的处理了

知道了什么是对象,距离面向对象编程还有很远的距离,这之间需要一定时间的学习,所以此处也不去赘述了。

我们这里先在这里做个比喻,来感受一下面向过程和面向对象的区别吧:

当我们想要吃饭…

面向过程
买菜,切菜,烹饪,调味,装盘,上菜…

面向对象
直接点外卖

可以看到,面向过程是需要我们细致入微地关心每一步,做不同的菜品还会有不同的操作;但是面向对象不一样,无论什么菜品,总是拿出手机点一下就行了

这是如何做到的呢?你可以观察到,不同的对象间似乎拥有相同的属性,比如 工程师对象医生对象,Ta们都可以拥有共同的属性和行为,比如名字、性别 和 吃饭睡觉等等,为什么会这样呢?

原来是因为它们存在共性,都是 , 那我们不如把所有人对象都默认赋予名字、性别 和 吃饭睡觉等属性行为,不就可以实现用通用的方式描述一个大类吗?面向对象就是以这样的形式(封装、继承、多态)来解决一个大类的问题的,所以更简洁高效

到这里,如果你还没明白,那么我们再看一个例子:

你一定听说过这个问题——把大象塞进冰箱需要几步呢?

答案是两步,开冰箱门,然后放进去

这是一个典型的面向对象的思维,我们把大象抽象成一类——需要放进冰箱的一类东西,那么自然,就可以有一套通用的办法解决问题

当然这样暴露了面向对象的一些不足,也同样是上面这个例子,这样的操作实际上是没法把大象放进去的——面向对象是不够精细的,在一些极端场合还需要我们面向过程,手动处理每一步

所以我们还是有学习面向过程的必要,面向对象底层也是面向过程的,是伟大的前辈们用一砖一瓦为我们拼凑出了今天的摩天大厦,所以了解面向过程这”一砖一瓦”还是非常重要的,而且面向对象更有助于培养我们关注解决问题的每一步,更能锻炼我们编程的逻辑思维能力

当然,除了面向过程,面向对象等,还有面向切面等模式,有兴趣可以自行查阅资料了解

高级语言 和 编程原理

这里我没法具体介绍编译原理,因为这个需要太多的前置知识,不过我们可以大概的描述编译的”轮廓”

计算机也不过是一台机器,本身也是通过电路来控制的,内部集成了无数个小开关,它们的开启和关闭我们可以分别记为0和1,这就是我们说的二进制,通过这种二进制的开关,我们就能够控制电流的流向,进而控制整个计算机

我希望大家是学过进制的…如果没有学过的话,最好去学一学,因为这个应该作为一种编程的常识

严格来说,0和1通常是分别代表低电平和高电平。电平是什么?那说电压总知道了吧…

但是呢,如果我写了两段代码,分别像下面这样:

000000001111110101101001100001011010101000010100
000000001111110101101001100001010010101000010100

好了,它们分别是什么意思呢?两段代码又是不是一样的呢?

谁知道啊——这实在是太难以阅读了!

所以就诞生了汇编语言

.8086
.MODEL small
.data
    str db 'hello world!$'
.stack 20H
.code
start:
      mov ax, @data
      mov ds, ax
      lea bx, str 
output:
      mov dl, [bx]
      cmp dl, '$'
      je stop
      mov ah, 02H
      int 21h
      inc bx
      jmp output
 stop:
      mov ax, 4c00h
      int 21h
end start

可以看到,其中有了一些英文单词,相比原来的0和1来说已经清晰不少了

但是它还是不太方便,然后就又有了高级语言,比如C语言

#include<stdio.h>
int main () 
{
 	printf("hello world!");
    return 0;
}

上面的两段代码运行结果都是一样的——打印hello world

很显然,C语言优雅简洁了不少

那么问题来了,计算机只看得懂0和1,如何让它懂得C语言所表达的内容呢?这里就需要用到C语言编译器了,就像是一个C语言的翻译软件,能把C语言代码翻译成0和1,这样计算机就能读懂了

电脑配置要求

一般而言,如果不涉及机器学习、图像渲染等方向并且不玩游戏的话,对于计算机的要求就是3000+的笔记本即可,这个价位是兼顾了长久耐用和实用性两点,当然如果经济宽裕的话自然是越高越好

其他问题

如果还有其他问题,通过以下方式联系到我,我会尽可能解答,如果一些问题出现频率较高,那么我会在本篇博文进行更新

联系方式在我个人博客网站的多处位置都能找到

前置准备

准备开发工具

之前有提到编译器,这里我们就需要下载一款内置C语言编译器的代码编辑器——也就是我们的开发工具

当然如果你够强,直接记事本也可以写代码

这里有非常多的选择:

devcpp
小巧轻量,不到50M,安装即用,适合入门

vscode studio
重型装备,专业开发用,配置较为复杂,不建议入门使用

vscode
轻量,但是配置非常复杂,不建议入门使用

clion
不了解,不做评价

xcode
mac御用,界面美观,操作丝滑,适合入门

lightly
界面美观,windows和mac通用,配置简单,适合入门

其他
有的太过于老旧,有的是第三方魔改,就不做推荐了

按照我以前的习惯,一般是推荐devcpp,毕竟情怀在那里,而且学校考试、比赛很多时候都用这个

但是,作为一个优秀的工程师,怎么能拒绝更简洁更高效?

本博客将使用lightly作为演示,lightly下载链接

lightly界面

没错,它竟然还有登陆界面——这就意味着这是一把双刃剑,一方面它可以云端编译,无视用户主机的操作系统所带来的差异保证输出结果一致(意思是甚至手机都可以);但是另一方面就意味着这玩意儿断网就不能用….

新建项目

点击新建项目

按照图上所说的进行操作

这里可以看到Lightly很强大啊,还能写很多别的东西!

这里的项目名称我们采用 大驼峰书写规范, 也就是每个单词首字母大写

还有小驼峰书写规范: helloWorld —— 从第二个单词开始首字母大写

以及 蛇形书写规范: hello-world —— 单词与单词之间用短线连接

至于什么时候该用哪一种,这就得靠长期实践潜移默化形成习惯了

Hello World

好了,如果一切顺利,那么你的第一个代码运行成功了!

代码基本结构

这里我先认识一下最小的C语言程序长什么样子,让我们在上述代码的基础上删减一些不必要的东西:

int main() 
{
   
}

没错,像是上面这样,就是一个可执行C语言程序的最小结构,让我们先记住这个结构,一点一点地扩充

基础知识 及 语法(待更新)

认识主函数

先让我们回忆一下函数长什么样子,高中的时候一定见过这样的式子:

f(x) = x + 1

那么C语言该怎么表达它呢?(这里我们假设x, y都是整数

int f (int x) 
{
    return x + 1;
}

这里的int是integer(整数)的缩写,代表紧跟其后的内容的值是一个整数

上面这样写,就实现了表示f(x)的值是整数,x也是整数

return x + 1,意味着这个 函数到此结束,x + 1将作为这个函数的值,比如x = 5,那么f(x) = 5 + 1 = 6

一切看起来既熟悉又陌生对吧?不过不用担心,我们后面还会进一步讨论函数相关的内容,这里我们大概认识一下即可

现在回过头来看看我们的代码,我们给它添加一个return 0

int main () 
{
    return 0;
}

好了,我们观察一下这个结构,是一个叫做main的函数,也就是主函数

C语言程序是 从main函数的开头执行,到main函数结束(执行到return或者执行到末尾)结束

它没有参数,然后似乎是个恒等式——永远为0?这是一个约定俗成的规矩,在主函数末尾返回(return)一个0,以便系统知道这个程序完整运行结束,而不是因为一些bug引起的异常而中断

认识头文件

让我们再扩充一点东西

int main() 
{
   printf("Hello World");
   return 0;
}

printf是print function(打印函数)的缩写,它的功能是把参数打印到控制台,让我们运行一下这段代码——报错了?!

报错信息

这里,我们的编译工具非常先进,虽然我们的程序有问题,但是它不仅指出了错误,甚至还帮忙纠正了错误,所以最后还是能够顺利运行出正确的结果!

那么为什么会报错呢?首先,printf是一个函数,那么它的内容呢?我们来看看使用一个函数的正确操作:

// 第一步是 声明并定义 这个函数
int f (x) 
{
    return x + 1;
}

int main() 
{
   // 第二步就是使用它(但是这样使用是不会显示任何执行结果的)
   f (4);
   return 0;
}

注意,程序的执行在main里面进行,但是 声明和定义函数并不算执行的过程——这算是一种执行前的准备工作

C语言 不允许函数内部再次声明定义函数,毕竟都开始执行了难道还允许暂停重新准备工作?

我们没有printf的声明和定义,所以报错了

再来观察一下我们最开始的程序,是这样的:

#include<stdio.h>
int main() 
{
   printf("Hello World");
   return 0;
}

这样写不报错的奥秘就在于第一行,我们引入(inlcude)了一个叫做stdio.h的文件,里面包含了printf的声明和定义, 所以就没有报错

这种包含了一系列函数声明定义的文件,就叫做头文件,我们通过如上语法来引入它们,stdio是standard input output (标准输入输出)的缩写,后续我们还会认识到更多的头文件

这些头文件是从哪里来的?
这些是我们下载的开发工具自带的——当然,如果你感兴趣,也可以查阅资料自行学习如何自制一个头文件!

为什么叫头文件?
可能是因为它们通常写在代码的最开头几行吧…

注释

注释就是批注的意思,我们可以在代码中写一些便于人们理解这段代码的话语,它们不会影响到代码,C语言的注释可以有下面两种风格:

#include<stdio.h>
// 这是一个头文件↑
// 这种开头写两个斜杠的就是单行注释,每一行都要加

int main () // 注释可以写在任何地方
{
    
    
       // 就和空格和空行一样,并不影响代码的实际运行效果
 // 但是会影响视觉美观,所以实际书写的时候还是请注重规范
    /*
 		这种是多行注释,
        只要保证你写的内容在起始和结尾符号之间即可
    */
    
    return 0;
 // 记得表达式后面是要加英文分号结尾的!
 // 要用英文符号";" 中文符号“;”都会报错的!
}

变量声明

变量声明是什么?回想一下高中设未知数的过程:

设一个整数变量x, x = 1

#include<stdio.h>

int main() 
{
   int x; // 这一步叫声明变量x
   x = 1; // 这一步是叫初始化变量x
   // 注意,=在c语言中不是等号,而是赋值号,意思是将右边的值赋值给左边
   // x = 1 意思是 使x的值为1,而不是x的值等于1
}

没错,是int,它是integer(整数)的缩写,我们可以像上面这样声明变量

我们还可以优化一下表达:

设一个整数变量x = 1

#include<stdio.h>

int main() 
{
   int x = 1; 
}

除了int,我们还有很多种类型,这里先介绍一些常用的:

#include<stdio.h>
int main () 
{
    // 整型变量, 取值范围是 (-2^31, 2^31-1)
    int num0 = 0, num1 = 1; // 可以连续声明
 	// 超长整型变量, 取值范围是 (-2^63, 2^63-1)
    long long num2 = 1;
    // 单精度浮点型变量, 取值范围是 ((1.17549e-038), (3.40282e+038))
    float num3 = 3.1415;
    // 双精度浮点型变量, 取值范围是 ((2.22507e-308), (1.79769e+308))
    double num4 = 3.1415;
    // 字符型变量, 表示单个字符(注意一个汉字是两个字符...)
    char c1 = 'a';
    // 布尔型变量, 表示 '真'(true) 或 '假' (false)
	bool b1 = false; 
}

这里我们设置的变量不再叫做x, y, z之类的了,在编程中我们更提倡语义化,正所谓“人如其名”,一个变量的名字也应当和它自身的特性相贴切,让我们看到这个变量就能大概知道它是什么

可以看到,对于数字来说,几种声明方式除了是小数和整数的区别之外,还在于它们能够表示的范围区间不同。

如果赋予的值超过了其类型能够表示的范围,将通过补码的形式赋值——至于那是怎样一回事,如果感兴趣可以自行查阅资料进行研究,此处不做赘述

输出控制

我们之前使用过printf打印一句话了,那么该如何动态的打印变量呢?下面这样做可以吗?

#include<stdio.h>
int main ()
{
    int a = 1;
    printf("a");
}

毕竟我都这样发问了,那显然是不行的

我们应该使用变量控制符——一些特殊符号,来做到变量的控制输出:

#include<stdio.h>
int main ()
{
    int num1 = 1;
    float num2 = 2.3333;
    double num3 = 1.024;
    char c1 = '1';
    printf("这是一个变量%d, 它很棒对吧,比如%f还要棒,比%lf更棒,因为它叫做%c",num1, num2, num3, c1);
}

变量控制符示意图

没错,变量控制符就是相当于帮这些变量提前 占位置,等到后面这些变量来的时候,再按照顺序对应过去就可以了

不同类型的变量对应不同的变量控制符,不能混用,否则可能导致结果发生一些微妙的错误

这里给出常用的变量类型-变量控制符对应关系

变量类型 变量控制符
int %d,是decimal(十进制)的简写,别问为什么不是%i,因为设计就是这样的…
float %f
double %lf,没法啊,%d被int拿去用了,那就用long float凑合一下吧
char %c,单个字符
string %s,字符串——这个后面会遇到的
long long %lld,因为%d代表整型,那%lld代表超长整型没毛病吧?

这里还有一些转义字符

转义字符 用处
\n 换行

还不太懂?没关系,之前提到过,多实践!

代码老是写错?总是运行不起来?都是这样过来的~多实践!

输入控制

大概知道了怎么输出,那么来想想这个场景吧:

你设计了个有账号登录的系统,然后你需要用户的账号密码来执行一系列操作,那么问题来了,你怎么让你的程序获得用户的账号密码呢?

还记得之前说的吗?stdio是standard input output(标准输入输出),这就意味着除了printf输出之外,还有一个输入,那就是scanf,它的使用方式和printf很相似:

#include<stdio.h>
int main ()
{
    // account账号, password密码
    int account, password;
    printf("您好,请输入您的账号:");
    scanf("%d", &account); // 这里加上了一个符号 &,不要漏掉了!
    printf("您好,请输入您的密码:");
    scanf("%d", &password); // 还要注意%d前后不要随便加其他内容
    printf("\n\n\n登录成功,\n你的账号是:%d\n你的密码是%d\n", account, password);
}

scanf函数会获取来自键盘的输入

其他类型的变量输入方式也是如出一辙,需要注意的就是,一定要加上符号&,几乎是所有新手都会忘记这个事情从而导致程序无法按照预期的执行

基本运算

说到运算,那么加减乘除一定不陌生吧?那我们来实现一个程序,它能计算表达式,并将结果保留5位小数:

f(X) = [(X + 1)^2 / 100] * 3.1415
(x由用户输入)

我们来分析一下,结果要保留5位小数,那么这个值是浮点数(也就是小数),由于double精度比float更高,所以多数时候我们使用double来声明浮点数

#include<stdio.h>
// f(X) = [(X + 1)^2 / 100] * 3.1415
double f (double x) {
    return ((x + 1) * (x + 1) / 100) * 3.1415;
}
// return 后面的内容将作为这个函数的值
// 注意C语言中,更改运算优先级都是用(),不使用中括号和大括号
// 另外,^符号在C语言中不是幂运算,请勿混淆
int main ()
{
    printf("请输入x的值:\n");
    double x;
    scanf("%lf", &x);
    // 可以将函数值用同类型变量保存起来
    double result = f(x);
    printf("计算结果是:\n");
    printf("%.5lf", result);
    // 写成下面这样也可
    // printf("%.5lf", f(x));
}

这里我们重点看一下最后一个printf

printf("%.5lf", result);
// 写成下面这样也可
// printf("%.5lf", f(x));

变量控制符能够做输出控制,通过上面这样的写法能够实现保留小数点后5位,以此类推保留小数点后n位也是同样的道理

这里说到了等效替换,那么我们可以再想一想,为什么要把刚才的运算过程写到一个函数里面呢?直接把表达式写在main函数里面是不是一样?

double result = f(x);
double result = ((x + 1) * (x + 1) / 100) * 3.1415;

确实是这样的,这两行代码也是等价的。

但是封装成一个函数的好处在于,便于重复使用,并且书写简便,就比如我们刚才写的这个程序,要是改成让用户连续输入5次,输出5个结果:

#include<stdio.h>
// f(X) = [(X + 1)^2 / 100] * 3.1415
double f (double x) {
    return ((x + 1) * (x + 1) / 100) * 3.1415;
}

int main ()
{
    printf("请输入x的值:\n");
    double x1, x2, x3, x4, x5;
    // 这里换行是为了让代码更方便阅读!
  	// 要关注自己代码的可读性!
    scanf("%lf%lf%lf%lf%lf", 
          &x1, &x2, &x3, &x4, &x5);
    printf("计算结果是:\n");
    printf("%.5lf\n%.5lf\n%.5lf\n%.5lf\n%.5lf", 
           f(x1), f(x2), f(x3), f(x4), f(x5));
}

连续输入

再来思考一下,如果要表达:

f(x) = x^100

一百次方——难道我们要写一百个x相乘吗?

#include<stdio.h>
#include<math.h>
// f(X) = X^100
double f (double x) {
    return pow(x, 100);
}

int main ()
{
    printf("请输入x的值:\n");
    double x;
    scanf("%lf", &x);
    printf("计算结果是:\n");
    printf("%lf", f(x));
}

这里我们遇到了第二个常用的头文件math.h,正如其名,其中声明定义了很多关于数学的函数,包括pow(幂函数),sin(正弦函数),cos(余弦函数)、sqrt(平方根函数)等等

这里请注意输入的值不要太大,因为100次方不是一个小数目,过大的x可能导致值溢出表示范围从而触发补码

除了上述内容所展示的一些运算符号,C语言常用的运算符号还有:

运算符号 作用描述
% 取模运算: 即除法取余数,14 % 3 就是 2
sizeof 求字节大小: 虽然看着很像一个函数,但是它是一个运算符
? : 三目运算符:用于判断条件二选一
>> 位运算右移: 二进制位向右移动n位, 10>>1的结果是9
<< 位运算左移:二进制向左移动n位,10<<1的结果是8
^ 位运算取反: 按位取反, ^10的结果是9
== 等号:10 == 10 相等,结果是true,10 == 9 不相等,结果是false
>和< 大于和小于: 做大小比较,结果是布尔值true或false
>=和<= 大于等于和小于等于: 同上
! 布尔逻辑取反: ! false 就是 true,! true就是false
&& 和
逗号: 除了表示一种并列关系之外,还能返回后者的值

其中一些运算符我们会在后面介绍

条件语句

三目运算符

三目也就是三元,和加法运算是二元运算需要两个参数一样的道理,三元需要三个参数参与运算,这里的参数是 (3 == 2),’Y’ 和 ‘N’

char a = 3 == 2 ? 'Y' : 'N';
// 如果这段代码你不理解,可以看看下面等价的写法和解析
// char a = (3 == 2 ? 'Y' : 'N');
// 其中,== 是比较是否相等, = 是将其右边的值赋予其左边的变量
// 另外,不等于的表达是 !=    其中!代表 逻辑取反

上面这行代码的相当于是:

3等于2对吗 如果对,运算结果是 ‘Y’ 否则,运算结果是’N’

显然,3是不等于2的(false),所以a的被赋予一个值 ‘N’

if系列

三目运算符看上去非常高级,但是面对一些复杂情况用它表达的话可能就有点难以阅读了,这时候我们就需要if系列

让我们设想一个情景:

输入一个int整型,范围是[0, 100],代表一个学生的考试得分,并按照以下规则给出输出:

  1. 100 >= 分数 >= 90, 输出A;
  2. 90 > 分数 >= 80,输出B;
  3. 80 > 分数 >= 70, 输出C;
  4. 70 > 分数 >= 60, 输出D;
  5. 60 > 分数 >= 0,输出E;

这个过程可以看做是这样一个函数

函数表达

让我们看看用if是如何实现这里的需求的:

#include<stdio.h>

char judge (int x) 
{
    if (100 >= x >= 90) 
    {
        return 'A';
    } 
    if (90 > x >= 80) 
    {
        return 'B';
    }
    if (80 > x >= 70)
    {
        return 'C';
    }
    if (70 > x >= 60)
    {
        return 'D';
    }
    if (60 > x >= 0)
    {
        return 'E';
    }
}

int main ()
{
    int score;
    printf("请输入学生的分数:\n");
    scanf("%d", &score);    
    printf("%c", judge(score));
}

看上去非常简单对吧?但是要注意,现在这么写是错误的

我们试试输入一个100会怎么样呢?

输入100

100分怎么会是E呢?

让我们来分析一下下面这行代码:

bool flag = 5 > 4 > 3;
// 希望你还没有忘记布尔值——true和false,代表 真 假

结果是什么呢?4的确是小于5且大于3的,所以是true?

不不不,让我们来看看它的运算过程——从左往右运算:

首先运算5 > 4,这一段结果是true,然后运算true > 3

这里需要注意,布尔值true,在被当做数字使用时,会被自动转化为1,所以这里的true > 3等价于1 > 3,结果是false——关于类型转换这一点,我们会在一章讨论

所以,最后的结果就是false

所以我们输入的59,在进行第一个判断100 >= 59 >= 90,实际上执行的是1 >= 90,其他地方的判断也是如此这般

正确的表达需要借助逻辑连词(代码太长就只节选一部分解释):

// && 是 且
// 这里直译就是:
// 如果 100 >= x 并且 x >= 90, 那么函数值是'A' 
// 重申一下,return会使函数就此结束,后续代码就不会执行了
if (100 >= x && x >= 90) 
{
    return 'A';
} 
if (90 > x && x >= 80) 
{
    return 'B';
}
// ...略

好了,我们还可以进一步优化代码,使用else-ifelse

和多个if的区别在于,多个if之间没有关联,(如果不是return终止了继续执行下面的代码),每个if都将进行一次判断

else-if只有当上面的if或者else-if不成立时,才会做判断;
else只有当上面的情况全都不成立的时候,才会执行;

if (100 >= x && x >= 90) 
{
    return 'A';
} 
else if (x >= 80)  // 第一个if不满足,所以可以知道小于90
{
    return 'B';
}
else if (x >= 70)
{
    return 'C';
}
else if (x >= 60)
{
    return 'D'
}
else
{
    return 'E'
}

另外,这里其实存在一个简写方式:

当if的大括号内部只有一条语句时(分号代表语句的结束,所以有几个分号就有几条语句),可以省略这个大括号,就像下面这样:

if(x >= 60)
    return 'D';

switch系列

这里可不是在介绍游戏机啊,switch有 开关 的意思,而开关就是用于选择控制的

switch的功能和if基本一致,但是语法会比if系列繁琐,所以我们一般情况下是使用if系列,不过switch的判断速度要比if快一些,在一些运算时间要求特别严格的场合可以考虑switch,这里我们简单了解一下它的语法:

#include<stdio.h>

int main () 
{
    int score = 2;
    switch (score) 
    {
        case 1: 
        {
            printf("socre的值是1\n");
            break;
        }
        case 2: 
        {
            printf("socre的值是2\n");
        	break;
        }
        case 3:
        {
            printf("socre的值是3\n");
        	break;
        } 
        case 4:
        {
            printf("socre的值是4\n");
        	break;
        } 
        default:
        {
            printf("socre的值不是 1 2 3 4\n");    
        }
    }
    return 0;
}

可以看到,它难以对一个大的范围进行判断,只能通过case来判断是不是某具体一个值,default则类似于else

这里我们可以看到一个关键字break,它在这里的作用是终止switch的执行(最后的default可以不加break,因为已经是末尾了)。可能这么说有些令人迷惑,那让我们先去掉所有break,再来看看运行结果:

switch如果不加break的话

那么,如果用return来终止执行呢?

实际上也是可以的,但是return是用于终止函数的——注意,if和switch这些不是函数,它们只是一些条件判断语句,return会终止当前所在的函数,也就是终止main的执行

类型转换

如果要表示:

f(x) = x + 1.0025
(x是整型int)

那么结果f(x)应该是什么类型的?这点我们必须在一开始就想清楚,不然我们没办法声明定义这个函数!

显而易见,一个整数加上小数,那最后结果是小数,所以这里f(x)是double或者float等能够表示小数(浮点数)的类型

隐式类型转换

看完上面的例子,变量类型是可能在表达式中发生变化的

除了整数和浮点数运算之外,还有一些较为经典的例子:

除了最后的!5和0之外,都会执行printf

if (true) 
    printf ("true");
if (5 == 5) 
    printf("5 == 5");
if (5)
    printf("5");
if (!0)
    printf("!0");
if (!5)
    printf("!5"); // 不执行
if (0)
    printf("0"); // 不执行

if是进行布尔逻辑判断,也就是值为true才执行,这里很显然发生了一些转变,我们这里来对其中一些做出解释:

5 == 5
命题为真,true(其实可以是==运算的结果是true)

5
“非0即1”规则,数字0会被转化为false只要不是0的数字都被转化为true

!0
同上,**”非0即1”**规则

这里你或许会感到迷惑,为什么是我会把这种转换叫非0即1而不是非0即true之类的呢,毕竟这里和1也没有关系呀?这是因为,数字可以转布尔,那么布尔也可以转数字对吧?

f(x) = 100 + true + false

那么这个式子的结果是多少呢?

f(x) = 100 + 1 + 0

这里的运算结果需要时数字,而true转数字是1,false转数字是0,结合这个特点,我才选择把布尔和数字之间的转换叫做 非0即1

不过到这里还没完呢,来看看下面这个:

f(x) = 1 + 'a'

结合前面的学习,再加上过往的经验,加减乘除这些运算,是需要数字参与的——逻辑告诉我们这里的字符’a’必然转变为数字

确实是如此,那么这又是怎么样一种转换规则呢?

ASCII码

ASCII码是一种编码方法,把用数字表示计算机中的字符以便于存储

有了这个概念,我们来看下面这一端代码的运行结果

#include<stdio.h>

int main () 
{
    printf("%d\n", 'a' + 1);
    printf("%c\n", 'a' + 1);
}

答案是

98
b

我们可以察觉到,字符a对应着数字97,而字符b对应着数字98
这些符号所对应的数字,就是它们的ASCII码

ASCII码对照表

需要注意类型,字符0对应的数字是48,也就意味着:

#include<stdio.h>

int main () 
{
    if ('0' == 48) {
        printf("是相等的!"); // 会执行这个printf
    }
    if ('0' == 0)  
    {
        printf("其实不会执行这个"); // 不会执行, 因为这两个零本质是不一样的
    }
    return 0;
}

我们只需要记住几个关键的就行了:

’0‘-’9‘顺序对应48-57,

‘A’-‘Z’顺序对应65-90

’a’-‘z’顺序对应97-122

记不住也没有关系,随时用printf打印出来看一下就知道了

强制类型转换

之前都是隐式类型转换——它们是自动发生的,并且这种转换多半是出现在表达式中,比如1.005 + 1,``’a’ + 1false > 2`等等

如果我们想要手动的去转换类型,比如把浮点数转为整数:

#include<stdio.h>

int main () 
{
    double x = 3.1415926;
    printf("%d", (int)x); // 被强制转化为int类型了,结果是3
    char y = 'a';
    printf("%c", (char)(y + 1)); // y + 1 先隐式转化为 97 + 1, 然后98又被强制转化为char类型,也就是b
}

总结强制类型转换的语法就是:

(类型)变量
比如(bool)x, (int)y等等

循环语句

while循环

我们先来直接看看代码

#include<stdio.h>

int main () 
{
    int num = 0;
    while (num < 99999) 
    {
        printf("num的值是%d\n", num);
        num ++;
        // 这里等价于num = num + 1;
    }
    return 0;
}

运行结果

如果你亲自执行一下这段代码,可以看到它不断输出的过程

而while语句的意思也就是当其逻辑判断为真的时候,反复执行一段内容

如果是出现类似于下面这种代码的时候,就会出现死循环

while (true) 
{
    printf("hhhhh");
}

在循环中我们,可以使用两个关键词continuebreak

int x = 0;
while (x < 10)
{
    x = x + 1; 
    if (x < 5) 
    {
        continue;
    }
    if (x == 8) 
    {
    	break;    
    }
    printf("%d ", x);
    
}

最后的输出结果是

6 7

小于5时,continue会提前结束 本轮循环, 而break会 结束整个循环

for循环

数组

字符串

函数

结构体

自定义宏

指针

基础数据结构和算法(待更新)

冒泡排序

简单选择排序

简单Hash

简单递归

简单动态规划

链表

队列


文章作者: Serio
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Serio !
  目录