目录
前言
作者写本篇文章旨在复习自己所学知识,并把我在这个过程中遇到的困难的将解决方法和心得分享给大家。由于作者本人还是一个刚入门的菜鸟,不可避免的会出现一些错误和观点片面的地方,非常感谢读者指正!希望大家能一同进步,成为大牛,拿到好offer。
本系列(初识C语言)只是对C语言的基础知识点过一遍,是为了能让读者和自己对C有一个初步了解。
日志
2023.3.24首发.
2023.3.25首次修改.
2023.7.11第二次修改.
2024.3.27第三次修改.
2024.8.18第四次修改
一、什么是c语言
1.1C的介绍
- C语言是人和计算机沟通的编程语言。广泛用于底层开发。
- C/C++在TIOBE常年位居前三,是一门经久不衰的语言。它是万物(指高级语言)起源,后来的许多高级计算机语言都有它的影子。
1.2语言的起源和发展
- 起源——二进制语言
早期计算机只能识别二进制语言(一种低级计算机语言)。计算机是通电的硬件设备,通电后只能识别电信号,正电(1)和负电(0)。电信号1/0排列成一串数字,具有一定意义。比如假设10001,计算机把它识别成加法。 - 二进制语言的缺点以及发展高级语言的原因
要实现加法,可能是10100001,很繁琐。一串串10的含义难记。人们需要一种更简洁的计算机语言让更多的人高效方便的进行编程开发。 - 发展过程
假如加法是10100001,把它用一段字母(如ADD)来表示,不是显得更简便了吗?这就是助记符。
助记符推演成汇编语言,编程门槛便得到了降低,但还是不够简单。为此发明了B语言,c语言/c++/java…
发展到C及后面的语言,就是高级语言了。本质上,语言的发展是为了降低编程难度,让更多人能够去学习使用,并方便人们。 - C标准
c语言–>早期不是很成熟–>成熟–>流行
c语言发明后,很火,人人都用它。但早期C不成熟。各家公司在使用时都喜欢给它加点功能。1公司加c1,2公司加c2,但2公司用不了c1,1公司用不了c2,标准不互通。 这时的C缺少一个标准使得它在各家公司都能使用,并提供强大的功能。因此美国国家标准总局在1989年就定义了一个标准,叫ANSI C(也叫c89)。
人们觉得方便,承认了c89。90年推出了c90(与c89相似)。后续发展中出现了c99,c11(不流行,不少编译不支持)。有了标准后,互通问题得到解决,使c更流行。
c语言常见的编译器主要有Clang、GCC、WIN-TC、SUBLIME、MSVC(VS)、Turboc等。我使用的是VS2022。
二、第一个c语言程序
2.1编写代码
- 创建新项目
创建新项目->空项目->下一步->项目名称->创建
项目名称不要使用中文、特殊字符、空格。 - 创建.c文件
视图->解决方案资源管理器->右击源文件->添加->新建项->名称cpp改为.c-双击c++文件
VS2022管理器已默认添加。
一定要把后缀改成.c,不改极有可能出现一些莫名其妙的错误。 - 写代码
创建好.c文件后,就可以写代码了。
注意编辑好的.c文件是文本信息。.c文件+编译+链接才是可执行文件exe
2.2程序的基本框架
int main()//int是整型的意思(第3章会讲到);main:主要的。主函数,程序的入口,有且仅有一个
{
return 0;//return:返回;返回一个值
}
main主函数,函数的入口,有且仅有一个。整个程序将从main函数开始。按f10进入调试模式,观察程序走向。
函数只能从main开始,它是程序入口。
int main()表示int要求main函数在函数的结尾返回一个整型(任意,一般为0)。
return 0;返回一个0,恰好满足前面要返回一个整型。
2.3注释
- 什么是注释?
注明解释一些难以理解的代码,它不会被编译器编译,只在源代码中显示,给程序员看的。 - 注释的作用
在进行一些大型项目时,你不可能自己一个人写完所有代码。你难免要阅读他人写的代码,进行合作。如果别人写了注释的话,会大大减少你的时间。同理,你的注释对自己或他人也有作用。要懂得前人栽树后人乘凉。 - 注释的表达
注释风格1(c++风格)推荐
printf("");//斜杠后的内容就是注释
注释风格2(早期c语言风格)不推荐
printf("");/*注释内容*/
可进行多行注释,但不能嵌套注释,风格差。
嵌套注释后,(/*)只会与最近的(*\)匹配。因此,c就采用了c++的注释风格,使得c有两种注释风格。
2.4源文件和头文件
- c源文件:写出的代码的文本文件。
- h头文件:放在代码头部的文件。
- 为什么要引头文件?
在安装VS的时候,库函数只是安装到系统里,源文件是不能直接使用库函数的,还要通过访问系统,找到里面的库函数,才能使用。
因此使用库函数时,要向系统打招呼。所谓的打招呼就是引头文件,编译器就会去系统找,把它加载到你的源文件,供你使用。
好比你借别人的铅笔,要和同学打一声招呼“借一下铅笔,谢谢”。同学把文具盒借给你,你就能拿到铅笔。不然的话,直接去拿,这是不合适的。 - 头文件引用方式
头文件是在代码头引用。不同的库函数,头文件可能同,可能不同。就像你有一个很大的文具盒,文具盒里面又有铅笔盒,笔芯盒。借铅笔,别人拿出铅笔盒让你自己挑。借笔芯,别人拿出笔芯盒给你让你挑。铅笔盒就是各种铅笔的头文件,笔芯盒就是各种笔芯的头文件。我们可以从这个铅笔盒(头文件)中使用,它里面有的一切铅笔(库函数)。但是你不可能从铅笔盒这个头文件中拿到笔芯,因为笔芯放在另一个头文件(笔芯盒)里。(比喻不是很恰当,理解就行)
语法:#include<头文件名>
- 为什么要引头文件?
- #include是一种预处理命令。
在安装VS时,头文件只是放在了系统里,不在源文件。源文件不能直接使用。<>去寻找系统的头文件,把<头文件>的内容加载到源文件。 - 使用方法
printf打印函数。print打印-function函数。头文件stdio.h
standard(标准)-input(输入)-output(输出)——标准输入输出
在使用库函数时,如果不引用对应的头文件无法使用该库函数。
引入头文件后
运行成功
三、数据类型
3.1什么是数据类型
计算机把数据分别归类为字符类型(键盘能键入的每一个内容),整型(正整数(0也算正整数)、负整数),浮点类型(小数)
3.2数据类型的作用
数据类型是用来向计算机申请空间存放内容的,不同的数据类型所申请到的空间大小可能有差别。
3.3数据类型的分类及它的打印字符
类型 | 关键字 | 大小(字节) | 打印字符 |
---|---|---|---|
字符型 | char | 1 | %c |
短整型 | short | 2 | %d |
整型 | int | 4 | %d |
长整型 | long | 4/8 | %ld |
更长的整型 | long long | 8 | %lld |
单精度浮点型 | float | 4 | %f |
双精度浮点型 | double | 8 | %lf |
3.4数据类型使用方法
- 语法:
数据类型 数据名称=初始值;
申请一个字符类型char a='S';
申请一个整型int b=1;
申请一个单精度浮点型float c=1.2f;
计算机中默认浮点数是double类型,1.2后面的f是表示单精度浮点的意思。如果不加f,用float、double申请,编译器都会默认为双精度。 - sizeof计算类型或变量大小,单位字节。注意sizeof是操作符,不是函数,没有头文件。
sizeof(变量/类型)
#include<stdio.h>
int main()
{
int a=10;//申请一个大小为4字节的空间,存放整型10
printf("%d",sizeof(a));//计算a的大小,并打印结果
printf("%d",sizeof(int));
//%d打印整型。printf("%格式化输出",取(访问)哪里的数据打印)
return 0;
}
两个4,说明int类型向内存申请了4个byte的空间;
\n是换行的意思,现在理解为一个回车键就可以了。如果没有第一个printf里的\n,44会连在一起。
3.5数据类型的大小及细分的作用
计算机单位
1个字节(byte)=8个比特(bit),能存储8个二进制数字。
依次类推,short类型是两个字节,16个bit位,范围在0~2^16-1。
所以当我们存一个很小的数的时候,而它的范围就在0~2^8-1就可以考虑用char,而不是用short。这样就能节约一个字节的空间。这就是数据类型细分的一个作用。
(特别指出由于ASCLL码,char类型本质是整型(后面讲))
3.6声明
在c语言中,你想要使用一个东西(变量、函数等),必须声明,(同前文的库函数需要打招呼一样)
数据类型 数据名=初始值;
前面把它叫做向内存申请空间,其实也叫声明。而这里的数据名就是一个变量(下一章节讲到变量)
注意。声明必须放在代码头,即({)下面就是声明的位置。书上是这么说的,但更准确的来说是:你要在使用变量(数据名)前,要先为变量(数据名)声明,告诉计算机这个变量(数据名)是什么东西。即做到先声明,后使用
假设你在使用变量后面声明,那么计算机怎么知道这个变量是什么?
int main()
{
int a=10;//声明,告诉计算机a是一个int类型的变量
printf("%d",a);//前面我声明了,现在就能够使用(访问)a,打印里面的内容
return 0;
}
如果你前面没有声明,但又使用了一个东西(这里假设是变量b),比如用printf来打印,但前面没有声明b,会报错。
int main()
{
int a=10;
printf("%d",a);
printf("%d",b);//前面没有b的声明,编译器在编译的时候找不到b的信息
//编译器不知道b是啥,也就无法解析,会报:未声明的标识符
//而b的声明还要在下面呢,但是使用在前面
int b=20;
return 0;
}
我们按正确的做法再来一次:先声明,后使用
int main()
{
int a=10;
printf("%d",a);
int b=20;//printf是使用了b,b必须在printf前面
printf("%d",b);//b已经在printf前面声明过了,所以可以打印
return 0;
}
因此声明必须要在使用前
3.7sizeof计算数据类型大小
利用sizeof可以计算出各数据类型的大小
像short短整型和int整型都可以表达整型,但大小上有区别。这里再一次说明了细分数据类型的作用。
以此类推,我们也可以计算出其他数据类型所能表示的范围。
long类型是4/8,c标准只规定sizeof(long)>=sizeof(int)。取决编译平台,32位平台(即x86)是4,64位平台(即x64)是8。
四、变量和常量
4.1变量
- 变量的概念
变量:值可以变(身高、体重、薪资)。
数据类型 变量名称=该类型值;
a是可变的,叫变量。
当然变量的命名也有一定的规则:
- 只能由字母(大小写敏感)、数字、下划线(_)组成
- 不能以数字开头
- 长度不能超过63个字符
- 尽量起得有意义(别人一看就大概知道是干嘛的)
- 遵循驼峰、蛇形命名(公司方便管理代码)
- 局部变量和全局变量
- 局部变量:定义在代码块({})内部的变量
- 全局变量:定义在代码块({})外部的变量
int a=100;//代码块({})外,为全局变量
int main()
{
int b=10;//代码块({})内,为局部变量
return 0;
}
- 变量的作用域
- 作用域:哪里能用哪里就是它的作用域。
- 局部变量的作用域:在它的代码块内
代码运行,红圈({})内是num1的作用域。
然而将num1再用一个({})括起来的时候,作用域就变了。
无法运行,因为红圈才是num1的作用域。
把printf拷进红圈,运行了。充分说明num1作用域在红圈(小代码块)内。
代码运行。num1的作用域在红圈代码块内,绿圈代码块在红圈内,被红圈大代码块包含,在绿圈里的printf自然能使用num1。
- 全局变量的作用域:整个程序,包括外部文件。
- 变量的使用
-
scanf输入函数,用键盘键入内容(头文件stdio.h)
scanf("%d",&某个变量);
-
&取地址操作符
变量是数据类型向计算机申请空间以储存的。那么存了变量的这块空间,该放在哪里?又应该如何找到这块空间?
&只是找到地址,没有把值拿出,只是把这个值所在的空间的地址交给给计算机,计算机通过这个地址找到这个值所在的空间,然后你就可以往里面存东西。
scanf输入的值放哪?明显需要要地方存,也就是需要一块空间。这块空间在哪里就可以用地址来表示,因此scanf访问的是地址。
如果没有&找空间,计算机就不知道值要放到哪里。&后,计算机找到一个能存放值的空间。所以&不能少。
5. 变量的声明
先声明,后使用
6. 局部变量在这里的作用
假设进行加法运算,要输入两个值。scanf输入函数必不可少。
用scanf函数所输入的值需要用空间来存放,num1和num2作用就是向内存申请空间,这两块空间就供scanf输入的值存放。
-
局部变量和全局变量的名称冲突,局部优先。
尽量不要让局部变量和全局变量名字冲突,容易产生bug -
使用外部全局变量
我们再创建一个test1.c文件,如果你想在test0.c里面使用test1.c的变量,就需要用到extern。
extern声明外部符号
extern 外部数据类型 外部数据名;
同时解释了全局变量的作用域是整个程序,在别的.c源文件内也能使用。
如果不适用extern,那么将无法使用来自其他.c文件的变量 -
变量的生命周期
- 局部变量:生命周期在作用域,进入作用域生命周期开始,出作用域生命周期结束。
- 全局变量:整个程序。
#define _CRT_SECURE_NO_WARNINGS
- 含义:#define _CRT_SECURE_NO_WARNINGS定义安全无警告。
- 作用:在VS中,使用scanf、strlen等函数时会报错,警告你这些函数不安全,要求使用scanf_s这种格式(后面加_s)。
是因为scanf是c的库提供的,scanf_s是VS提供的。VS不承认c的格式。
我们可以用VS标准,但当代码放到其他编译器时,scanf_s是错的。使代码失去可移植性(代码不加更改直接放到其他编译器里也能运行,好程序的一个标准)。因此我们可以在代码第一行加上#define _CRT_SECURE_NO_WARNINGS,忽略警告,使得代码具有可移植性。
- 默认储存
#define _CRT_SECURE_NO_WARNINGS
如果写一段代码前都加上它,这很浪费时间。所以我们可以把它默认存在编译器里。(注意#define后面有一个空格)
首先在电脑找到newc++file.cpp。如果你安装VS的时候,选择的是默认路径。那么这个文件的路径是
Program Files/Miceosoft Visual Studio/2022/Commumity/Common7/IDE/VC/VCProjectltems
用记事本打开将看到全是空白,把它复制粘贴放进去,往后新建源文件都会自带这串代码。
部分人无法保存,因为文件受保护。
绕过它的方法
一:以管理员身份打开记事本,在记事本找文件,即可修改。
二:把文件移动(复制或移动)到桌面,再用记事本打开,修改之后你会发现可以保存。然后再移动回原来的地方(有替换点替换)。
(如果你没有选择默认路径,可以下载一个everything查找该文件)
4.2常量
-
常量:不变的量。如身份证号,血型。
-
常量的分类
字面常量
const修饰的常变量
#define定义的标识符常量
枚举常量 -
字面常量:直接写出来的,不能改变。如1,123,-1,-2,0,‘x’,"abc"都是字面常量。
-
const修饰的常变量:把一个变量定义为常量。本质上还是变量,只是拥有了常量的属性。
const 数据类型 数据名=值;
无const修饰时的变量,变量能够变化
const修饰后,num具备了常量的属性,不能被再次赋值。
被const修饰的变量只是具备了常量的属性,披了常量的皮,本质上还是变量。可以用数组验证。
数组:一组相同类型的集合。目前只要知道表达就行。
数据类型 数组名[元素个数]={元素1,元素2,元素3……};
[]内的值必须是一个整型常量,可不写,计算机会自动计算{}的元素个数。但[]内不能是变量。当[]为变量时,报错。
因此,我们可以数组的创建来验证const修饰的变量的本质。将const修饰的变量放进数组[]里,
数组创建时,[]内的值不能是变量,不能运行。(C99之后引入了变成数组,[]内的值可以是变量,但这种数组不允许初始化。) -
#define定义的标识符常量:变量被#define定义后,本质上已经变成了常量
#define 标识符 数值
(没有结尾)
num被#define定义后放到数组arr里能运行,说明num真是常量。因此被define定义的标识符常量是彻彻底底的常量。 -
枚举常量:一一列举的常量,有排序。
enum 枚举名
{
元素1,
元素2,
元素3
};
这些元素就叫做枚举常量。而枚举类型是我们自定义的类型,它和char,int这些都是c语言的数据类型,只不过一个是我们自己定义的,一个是c语言本身就有的。使用方法也是相同的。
枚举类型常量有默认的值,从第一个常量为默认为0开始,以1递增。
当然这个初始值是可以给定的,就像一个人的血型。虽然不能变,但最开始总得有一个血型,总不能说你没有血型吧。
给定一个枚举常量的初始值后,后面如果不指定就会以1递增。
前面的值不给定,依旧默认从0开始。
定义出来的枚举常量不能再次被修改
新创建的枚举常量可以随意修改
五、字符、字符串和转义字符
5.1字符
- 用(‘单个内容’)括起来的就叫字符,键盘上键入的内容都是字符
- ‘1’,'a’叫字符1,字符a。
5.2字符串
-
字符串:一串字符。(“字符串”)
-
"hello worlk"
字符串hello worlk。特殊的空字符串(“”),有时用到。 -
字符串结束标志:字符串后面都隐藏了一个\0。"abc"除了’a’,‘b’,‘c’之外,其实还有一个\0,真正为’a’,‘b’,‘c’,‘\0’。‘\0’作为结束标志,不算字符串内容,用来结束字符串。printf打印字符串的时候遇到’\0’停止打印
%s打印字符串,需要访问一个地址
烫烫烫?
因为arr1是字符串,它有一个隐藏的’\0’。'\0’让打印停止,只打印了abc。而arr2是数组,里面存放了三个字符,没有\0。打印到c时,没有遇到\0,不结束,后面遇到什么内容完全随机。直至遇到\0,打印停止。为了直观,我给arr2加上\0试试。
说明了\0是字符串结束标志,0也可以。因为0作为ASCLL码值对应字符就是\0,这里就牵扯出了ASCII码表。 -
ASCII码表
计算机只能储存二进制数字10。用10可以表达数字,但#\*acd及其它字符又怎么存?为此人们给字符都编了一个值,让这个值来表示对应字符。这些值就叫ASCII码值,人们把ASCll码值与字符的对应关系组成的表叫做ASCII表。
-
strlen函数:string length计算字符串长度(头文件string.h),单位字节。
strlen(变量/数组)
\0(属转义字符)是字符串结束标志,不算作字符串长度,strlen在遇到\0时会结束计算。
arr1是字符串,含有\0。strlen计算abc后,遇到\0停止,\0不算内容,长度为3。而arr2不是字符串,无\0。计算完abc后,它还会往后计算,直到遇到\0。而什么时候遇到\0,完全是随机的,所以打印的长度是随机值,不固定。
5.3转义字符
- 转义字符:转变原来的意思。在要转义的字符前加(\)
假如要在屏幕上打印一个abc
一个printf就搞定了。但如果让你输出一个abc\n呢?
没打印\n,因为\n是转义字符,\把n转变成换行。两图下面那段话与abc的距离不一样。后者多了一行。
假设我们打印是一个地址c:\test\32\test.c
最后发现打印结果,完全出乎我们的意料。
这是因为,\是转义字符,它后面有特定字符时,会将该特定字符转变成另一个意思
\t是一个转义字符,相当于tab水平制表符。要打印出\t,怎么办呢?在它的前面加一个\,让\t不再是tab键,将\t前的\转义成普通的反斜杠。\t表达的含义,就是一个普通的\再加个t。
第一个\将第二个\转义成了普通\,不再是用来转义字符的\。 - 常见转义字符
转义字符 | 含义 |
---|---|
\? | 在书写多个问号时使用,防止它们被解析成三字母符 |
\’ | 用于表示字符常量单引号’ |
\" | 用于表示一个字符串内部的双引号" |
\\ | 用于表示一个反斜杠,防止它被解析为一个转义字符 |
\a | 警告符号,蜂鸣。电脑响一下 |
\b | 退格符,backspace键,删除 |
\f | 进纸符 |
\n | 换行,回车 |
\r | 回车 |
\t | 水平制表符tab |
\v | 垂直制表符 |
\ddd | ddd表示一个八进制数字。如:\127 |
\xdd | dd表示2个十六进制数字。如:\x439 |
这里再给大家介绍几个例子。想打引一个'
打印单个’时,在第二个(‘)前加个\,防止和第一个(’)配对
打印字符串(")也是如此。
用strlen计算字符串长度验证转义字符只算一个长度。
当将两个\t和\32作为一个字符时内容的长度是13,说明转义字符只是单独只算1个字符,长度为1。
\ddd和\xdd
\ddd:一个八进制数字转换成十进制作为ASCLL码值所对应的字符。\32,它转化成十进制数字是26,对应的字符是箭头。\32其实是箭头。
不知道为啥是方框,正常打印会是一个箭头。(欢迎解惑)
同理\xdd是16进制数字转化成十进制,以这个数值做ASCLL码值所对应的字符。
区别八进制和十六进制是看\后面有无x,没有是8,有是16。
六、选择语句
暑假时,你想去体验一把父母的不易,来到一家主打螺丝的流水线工厂。你会面临两个选择。
选项1:好好打螺丝。选项2:摆烂。
这种要选择的场景,就要用到选择语句
if(判断条件)
语句1
else
语句2
如果满足if的条件,则执行语句1,不满足则执行语句2
输入0,不符合if(input==1),所以执行了else的语句。输入1,则打印出打工人。
执行语句可以有多条。但是超过1条语句(狭义上是一个;结束一条语句),就必须把全部语句用代码块({})括起来。
好的编程习惯是每一个if,else后面都是用大括号。防止出错的同时,更加清晰。
七、循环语句
有一天主管告诉你,打够100w颗螺丝,涨工资。为此你决定天天打螺丝。一天打1w颗螺丝,打了100天,月工资+2北。每天重复打螺丝的过程,就是生活中的循环。
循环语句,现在使用的是while循环(还有两种循环)。
while(循环条件)
{
执行语句1
执行语句2
调整
}
运算符+=和++,q+=1和q=q+1完成等价。
判断部分:q<100判断是否继续循环的条件。符合,继续循环。不符合则跳出循环。
printf是执行语句,while循环可以有多条执行语句。
调整部分:q+=1和count+=。
八、函数
8.1函数的分类
库函数:c标准提供的,有固定名字,要引对应头文件。
自定义函数:自定义的函数
8.2函数的使用
库函数:printf、scanf等都是库函数,有自己的使用方法
自定义函数:自己写的函数
像在数学里
f(x)=2x
这是一个简单的函数,然而写概念题时,会给出一种概念函数(其实也就是自定义函数)
f(x,y)=2x+3y
x=1时,y=2时f(1,2)=2*1+3*2=7
这就相当于自定义函数。简单的运算,我们当然可以直接用数学符号来搞定就行。
但是当你要重复使用一个超级复杂的算法时,每次都只用+-*/去实现,是不太可能的。
自定义函数就可以做到,只要是设定好函数的参数、返回类型、函数名、具体实现,那么后续都可以使用这个函数计算。做到一次创建,终生使用。
自定义函数就可以做到代码服用,减少你的工作量。
数据类型 函数名(数据类型 值,数据类型 值)
{
定义的算法
return 算法结果;
}
九、数组
9.1一组相同类型的集合叫做数组
假设要声明1,2,3,4…一堆数字时,一个个声明很浪费时间。因此c语言提出了数组
数据类型 数组名[数组内元素个数]={元素1,元素2,元素3……};
数据类型 数组名[一个值];
在创建数组的时候,往{}写元素叫做初始化,初始化后,[]内的值可省略。计算机会自动识别个数。
如果没有初始化,[]内的值必须写出来。
9.2数组
数组每个元素都有一个下标,默认从0开始。
用这个数组输出4,4对应下标3,所以用arr[3]打印。
while循环结合数组打印出1-10。
有些朋友可能会觉得这里i不是变量吗?为什么能放到arr的[],前面不是说arr[]的[]里面的内容不能是变量吗?
但是前面我所说的是在数组创建的时候,[]内的值不能是变量。而我们这里是使用数组啊,所以随便给都可以。
9.3数组的本质
数组名前面的数据类型,都是指数组内元素的类型,并不是数组的类型。
数组本质上是地址。(数组名是数组首元素的地址)
十、运算符(操作符)
10.1算术操作符
- + - * / %(取模)
前三种就是数学上的 - /除。
分为整除和浮点除。
整除,只保留商。
浮点除,必须保证被除数和除数至少有一个浮点数,就是数学上的除。 - %取模:取余数
10.2移位操作符(移的是二进制数)
<<左移操作符
假设我把一个值为1的变量a<<1之后,它会在1的二进制数列在整个空间左移
可以预见的是a(a=1)被<<1后,值变成了2。
>>右移操作符同理,但是补的不一定是0,存在两种方式,先不讲。
10.3位操作符(二进制位)
&、^、|都是在二进制位操作
- &按位与:全1为1,有0则0。
其实就是在二进制位上进行数学的逻辑且运算 - |按位或:有1则1,全0为0
- ^按位异或操作符:相异则1,相同则0
10.4赋值操作符
=、+=、-=、*=、/=、&=、^=、|=、<<=、>>=
- =赋值符号,把一个值赋给一个变量。==才是等于的意思
- +=和后面的都叫复合操作符
10.5单目操作符
只有一个操作数的符号。
操作数:符号关系到的数字
1+2,+有两个操作数1和2,因此它是双目操作符。
c语言中:非零表示真(默认为1),0为假
单目操作符 | 含义 |
---|---|
! | 逻辑反操作 |
- | 负值 |
+ | 正值 |
& | 取地址操作符 |
sizeof | 操作数的类型大小(单位字节) |
~ | 对一个数的二进制位取反 |
- - | 前置、后置- - |
++ | 前置、后置++ |
* | 间接访问操作符(解引用操作符) |
(类型) | 强制类型转换 |
- !逻辑反操作
把一个数的真假类型转换。
非零表示真,但如果真要表示出来的话,总得有一个值,这个值就是1。
- -号和+号
-1,+1对数字取负和取正,只有一个操作数,是单目操作符 - &:取地址操作符。把地址取出来
通过监视我们可以看到一个变量的地址,跟&出来的是一样的。
当我们想要获得地址时候,可以使用&。 - *间接访问操作符(解引用操作符)在指针那章再讲
- sizeof操作数的类型大小(以字节为单位)
计算变量或类型的空间大小。
计算变量时,变量的()可以省略
计算类型大小时,()不能省略。我还没有把11行的代码放开,当我们放开之后
在变量a可以不加()可以看出,sizeof是一个操作符。函数的表达必须加()。
通过sizeof计算数组大小和个数
- ~按(二进制)位取反符号
数据是以二进制位在计算机中储存的。~就是在二进制位进行取反,0变成1,1变成0。对0(00000000000000000000000000000000)取反再打印,它应该是什么?
相信初学者会跟我当初一样懵逼,为不是2^32-1(11111111111111111111111111111111),而什么是-1。
这里涉及了计算机的三码。先声明计算机打印在屏幕上的都是原码。
符号位:有符号数(正或负)的二进制位的最高位(第一位),0为正,1为负。
原码:正数和0储存的是原码。(正数原码,补码,反码三码相同)
反码:原码符号位不变,其他按位取反得到。
补码:反码加1。(只要是整数,计算机存储的都是补码,因此操作的也是补码)
当0被~后,符号位从0(正)变成了1(负),此时已经变成了负数,以补码的形式存在计算机里。计算机只能打印原码,只能转化为原码-1。 - –和++的前置、后置
后置++
前置++
前置–和后置–同上述 - (类型)强制类型转换
把一种类型转换成其他类型
正常来说int a=3.14;
这种声明无效。因为3.14是浮点型,不是整型。但可以通过强制类型转换把a强制转换为int。
结果只取整数,舍弃小数点后的数。不建议这样转换,容易出bug。
10.6关系操作符
>、>=、<、<=、!=(测试“不相等”)、==(测试“相等”)
前面四个就是数学上的。!=相当于数学的≠,==相当于数学的=
10.7逻辑操作符
&&逻辑与、||逻辑或
不要和前面的&、|混淆。&、|是按(二进制)位的,而&&和||是直接计算的。
&&逻辑与:全真(1)为真,一假(0)为假、
假如b为0(假),那么输出的就是0了。
||逻辑或:一真则真,全假为假。
10.8关系操作符
exp1?exp2:exp3;
exp1结果是真,exp2被执行。如果为假,exp3执行。
a>b(exp1)。20>10,exp1不成立,执行exp3(b)。所以输出20。如果a为100,exp1成立,执行a,输出的是100。
10.9下标引用、函数调用、成员、指针
[] () . ->
- []在数组时用到,在里面输入下标访问元素。()在使用函数用到。后两种后面讲。
- 逗号表达式
exp1,exp2,exp3,…expn
逗号表达式会从左向右依次计算,最终结果为expn。但是可不要以为只算最后一个就行了,因为在expn前面的表达式中,有可能会影响expn的结果。
像这里如果没有计算c+=3,那么结果c-2的值就变成了8。所以逗号表达式的每个表达式都要计算。
十一、常见关键字
-
自己定义的变量名和函数名不能与关键字相同,会冲突。
-
auto(自动):声明局部变量时,它进入作用域自动创建,出作用域自动销魂。因此,像
int a=10;
其实省略了一个auto。auto int a=10;
。只不过因为大家都有auto,因此省略不写。 -
brake(终止):主要用在循环语句和Switch语句。
-
case(实例):switch里面使用
-
char:char类型
-
const:const修饰的常变量
-
continue(继续):循环用到
-
default(默认):switch语句用到
-
do:do while循环
-
double:双精度浮点型
-
if else:选择语句
-
enum:枚举常量
-
extern:引用(声明)外部符号
extern 数据类型 外部函数名(数据类型,数据类型);
extern 数据类型 变量名;
-
float:单精度浮点型
-
for:for循环
-
goto:goto语句
-
int:整型
-
long:长整型
-
register:寄存器关键字
CPU中央处理器。早期计算机运算时先从内存拿数据,内存和CPU处理数据速度相近。
随着计算机的发展,CPU处理数据的速度越来越快。内存访问速度跟不上。因此,人们给计算机加上了更快的设备——寄存器和高速缓存,但是空间更小,造价更贵。
CPU就可以从访问速度更快的寄存器直接拿数据。而CPU拿的数据的时候,会告诉寄存器要拿什么数据。寄存器如果有,直接拿走。如果寄存器没有,就会提前从下往上搬数据,先准备好。等CPU拿的时候,数据就已经在寄存器放好了。
如果CPU拿完了寄存器里所有需要拿的数据,但还有一部分的需求没有拿完,在下面那些存储数据的机器里。下面所需数据,搬运的速度跟不上CPU。这个时候CPU才会向下逐级访问数据。
最终形成了这样的一副图
这样的数据处理方式,使得计算机处理速度得到巨大的提升。
既然寄存器访问那么快,那么以后数据都放在寄存器不就好了?register就是把变量放在寄存器,而不是内存。但什么都放在寄存器,寄存器的空间又太小,放不下。
因此rigister的作用只是建议把数据存储在寄存器,到底是不是还要看计算机。
rigister int a=1;
-
return:返回
-
short:短整型
-
signed:有符号数
定义变量时
int a=10;
或int b=-5;
这些都是有符号的数,因为int定义的数是有符号的。其实在int前省略了signed
signed int a=10;
和signed int b=-5;
unsigned无符号数
和signed对应,如果一个数没有符号的话,就没有了正数之分,只会是正数(理解为绝对值就行)。即unsigned int b=-5;
,b的值是5。 -
sizeof:计算大小,单位字节
-
static:静态
按f10或f11进入调试模式打开监视窗口(打开方法:调试模式下 调试–窗口–监视)详细查看。最好按f11,f11才会进入test内部。
第3步时,a是局部变量,进作用域生命周期开始,被打印2后出作用域,生命周期结束被销毁。然后经过下一次循环的第3步,a=1又被创建,这是一个新的a。你可以这么理解,第一次创建的是a1,第二次创建的是a2……
5次循环后,打印了5个2。
但是a加上一个static后,a就变成了静态局部变量。
a的值得到累加,因为第一次产生的变量a1并没有像不加static那样被销毁,每次打印的都是a1。说明static修饰局部变量时,局部变量只进行一次初识化(创建那次),并且生命周期被延长了。
static在这里发挥真正的作用是把本来在栈区的局部变量a,放到了静态区。
因此当变量a被static修饰后,他的生命周期变长了(作用域不变)。使用的一直是同一个a,而没有static修饰,使用的是a1,a2…。
static修饰全局变量
新建一个源文件1,在里面我输入了static int AAA=99;
在源文件里用extern调用AAA=99,报错。因为当static修饰全局变量时,改变了全局变量的作用域,让静态的全局变量只能在自己的源文件(这里AAA只能在源文件.cpp使用)内部使用,出了源文件就无法使用。
static修饰函数和修饰全局变量作用相同。一个函数是有外部链接属性的(可被其他源文件用extern调用),static修饰后该函数被改变了链接属性,没有了外部链接属性,只有内部链接属性(只能在自己所在的源文件使用)。 -
struct:结构体关键字(结构体那章讲)
-
switch:switch语句
-
typedef:类型重定义
-
union联合体/共用体(以后介绍)
-
void无/空
-
volatile(暂时不介绍,太难)
-
while:while循环
十二、#define定义常量和宏
#define定义标识符常量前面已讲
宏在标识符常量的基础上带上参数。
它相较于标识符常量是有参数(X,Y)的。
函数和宏很相似。(感觉和函数区别也不大,可能是简洁吧)。
十三、指针
要讲清楚指针,首先要搞清楚内存。因为指针就是用来管理(访问)内存的。
13.1内存
13.1.1编号
内存是电脑上特别重要的存储器,计算机中所有程序的运行都是在内存中进行的。
为了有效的使用内存,人们把内存划分成一个个小的内存单元。而为了能够有效的访问(调用)到每个内存单元,就把内存单元进行了编号,编号也被称为该内存的地址,即编号=地址。
内存的地址就当于现实生活中的身份证地址,通过它可以找到你家,而内存的地址也是如此。有了地址之后,就能准确使用(访问)内存的每一块空间,更方便的管理内存。
通过地址线所产生的地址,计算机就能精确访问到某一内存单元。因此在32/64位机器上,计算机能管理2^32/2^64块内存单元。
13.1.2内存单元的大小
一个内存块是多大才算合适呢?假设是1个bit,申请一个char类型的变量,就有8个地址,是不是会显得很多余?就像寄快递,说寄到多少号房,第多少个板砖。所以显得很冗余。而且一共就只有2^32个编号,也就是说只能管理0.5g大小的内存,太鸡肋了。所以不合适。
如果是1个Kb一个内存单元呢?1Kb=1024byte,而申请一个int类型才4个byte,还剩下1020个byte。一个地址才用到其中很小的一部分空间,显得太浪费了,也不合适。
最终人们把一个内存单元的大小定为了1个byte,大小合适,而且总共能管理4g的内存。(注意这里的4G指的是计算机所能管理的内存大小,而不是内存就这么大。内存大小还是要看硬件的)
13.1.3内存单元存储数据
创建变量申请内存的细节是什么?又怎么拿到每个字节的地址?
我们也可以在调试模式下,打开调试-窗口-监视/内存来看a的地址。
监视窗口下&a,得到a的地址。
很明显的看到了一个值0x00b6fedc,0x代表的是以16进制的形式表示。这个值就是地址,但是只有一个地址。a不是int类型吗?为什么不是4个地址?可以打开内存来看
这里你会发现变量a所申请的4个字节空间,是连续的。所以只要得到第一个字节的地址,下面的地址也就能得到了。我们自己也可以打印出变量的地址。
打印出来的地址和监视窗口看到的一样。
13.2指针
13.2.1什么是指针?
- 现实生活中,别人记下你身份证上的地址是为了有一天通过这个地址找到你。计算机的地址也是一样的,地址是一个值,只要是一个值,就可以用变量存起来。
但是这就产生一个问题:pa是什么类型的变量?因为pa存的不再是整型数据,而是地址。地址是什么变量呢? - 在C语言中,地址就是指针,所以编号==地址==指针。
地址就应该这么存
可以看到pa里面存的就是a的地址。这个时候的pa被称为指针变量。口头上说的指针就是指针变量。
pa里面存放的是a的地址,所以pa的类型是int*。*说明pa是指针变量。想要存地址的变量是什么类型,指针就是给该类型加个*。
13.2.2指针的作用
前面讲了我们把地址存起来放到一个变量里,这个变量就叫做指针变量,也就是口头的指针。但我们仅仅是为了存起来吗?就像别人把你身份证上的地址记住,就只是记住吗?当然是为了未来有一天通过这个地址找到你家。我们存的地址,也不是无缘无故的。同样也是为了能通过指针找到它所指向的空间。
这就是指针的作用。通过解引用操作找到,它所指向的空间完成一定操作。现在看起来指针的作用只是简单的重新把a赋值,但指针的作用也不止如此。
还有值得注意的是
13.2.3指针的大小
指针也是一个变量,它的大小是多少?前面我们说地址也就是指针,是如何产生的。
在32/64根地址线所产生的32/64个电信号1/0。这些电信号转化为数字信号存储在计算机里。每一个数字信号1/0需要一个bit位存储,只要计算出存储着32个1/0需要多大的空间,就是地址的大小。
每一个数字信号所占大小是1bit,32个数字信号就需要32个bit位,也就是4个字节。因此32根地址线产生的指针,大小就是4个字节
而不论什么类型的指针,都是由32/64根地址线产生的,所以大小都应该是4/8个字节。因此指针的大小只取决于编译平台。
十四、结构体
计算机中的int、double类型等都是用来描述生活中的值,那么一个人该怎么表示?int?short?都不符合啊!因为人有姓名、身高、身份证等属性。在比如书,有书名、价格、作者等数学。对这些复杂对象的表示,c语言就是用结构体来解决的
14.1复杂对象类型
用结构体关键字定义的类型,就是我们自定义的一种复杂对象数据类型。就像char,short。
14.2结构体的创建
struct 复杂对象名 //struct结构体关键字
{
属性1;
属性2;
...
};//这个分号不能少,专门用来结束这个类型定义
14.3结构体的使用
我们说结构体是一个复杂对象类型,也就是一个数据类型。那么它就应该按照数据类型的使用方式。
14.4结构体的访问
我们访问int类型用%d,访问char类型用%c,访问字符串用%s。那么复杂对象类型怎么访问?
我们甚至可以写一个函数来专门打印描述这位学生。
以后每创建一个stu变量来表示一个学生,描述这个学生信息的时候,直接调用这个函数就可以直接打印了。
我们也可以专门写一个传s的地址的函数来打印。
这里就可以引出之前所说的->操作符了
这两种方式分别就是
//结构体变量.成员
//结构体指针->成员
14.5结构体变量内元素的修改
假设这个张三长大了一岁,变成了19。可以直接修改
那再假设张三突然想把名字改了叫李四,依旧如此吗?
报错。因为s.age是一个变量,它可以直接进行赋值修改。而s.name是一个数组的数组名,数组本质上是一个地址,是不能修改的。硬是要修改里面内容,要用到一个函数。
strcpy string-copy 字符串拷贝(string.h)
如果想要修改数组name的值,只能把一个新的字符串拷贝到name里。
语法strcpy(目的地,"拷贝内容");
目的地:放哪里去。拷贝内容,放进去的内容。
数组是地址,不能直接修改,要拷贝内容放到该地址里。
十五、总结
先来做一个自我介绍吧。本人大一非专业生,报考的时候(爱好)就想报计科或软工的,没本事被调剂了。我专业教Python,但是我在2月初选择了万物之始的C语言。经过半个月的学习才学完这篇的知识点,这里推荐B站的鹏哥C语言,我就是看鹏哥的视频入坑的。
2. 我是2月1号开始的编程,快月底的时候电脑坏了,等修好(维修站远,开学的时候去的)已经过了差不多半个月,一天8小时以上。之后开始写这篇博客(2月底),开始的时候啥都不会,Markdown都弄了大半天(现在还是不懂)。因此摆烂了一个多星期(大家不要学我哈),后来再十多号这样子重新捡回来,又花了十天左右一点点写(新兴文科专业,太多课了),写完有花了一两天整合。虽然过程很久,但我觉得很值。在写的这个过程(费曼学习法),很多忘了的知识被重拾起来,而且印象深刻。因此,我推荐大家能够用写博客的方式学习编程。还有不要摆烂放弃。希望能与大家一起学习分享,早日成为大牛!