作者写本篇文章旨在复习自己所学知识,并把我在这个过程中遇到的困难的将解决方法和心得分享给大家。由于作者本人还是一个刚入门的菜鸟,不可避免的会出现一些错误和观点片面的地方,非常感谢读者指正!希望大家能一同进步,成为大牛,拿到好offer。
本系列(初识C语言)只是对C语言的基础知识点过一遍,是为了能让读者和自己对C有一个初步了解。
2023.3.24首发.
2023.3.25首次精修.
2023.7.11第二次修改.
c语言常见的编译器主要有Clang、GCC、WIN-TC、SUBLIME、MSVC(VS)、Turboc等。我使用的是VS2022。
int main()//int是整型的意思(第3章会讲到);main:主要的。主函数,程序的入口,有且仅有一个
{
return 0;//return:返回;返回一个值
}
main主函数,函数的入口,有且仅有一个。整个程序将从main函数开始。按f10进入调试模式,观察程序走向。
函数只能从main开始,它是程序入口。
int main()表示int要求main函数在函数的结尾返回一个整型(任意,一般为0)。
return 0;返回一个0,恰好满足前面要返回一个整型。
printf("");//斜杠后的内容就是注释
printf("");/*注释内容*/
嵌套注释后,(/*)只会与最近的(\*),造成出错。因此,c就采用了c++的注释风格,使得c有两种注释风格。
运行成功
计算机把数据分别归类为字符类型(键盘能键入的每一个内容),整型(正整数、负整数和0),浮点类型(小数)
数据类型是用来向计算机申请空间存放内容的
类型 | 关键字 | 大小(字节) | 打印字符 |
---|---|---|---|
字符型 | char | 1 | %c |
短整型 | short | 2 | %d |
整型 | int | 4 | %d |
长整型 | long | 4/8 | %d |
更长的整型 | long long | 8 | %d |
单精度浮点型 | float | 4 | %f |
双精度浮点型 | double | 8 | %lf |
语法:数据类型 数据名称=该类型值;
申请一个字符类型char a='S';
申请一个整型int b=1;
申请一个单精度浮点型float c=1.2f;
1.2后面的f是表示单精度浮点的意思。
如果不加f,用float、double申请,编译器都会默认为双精度。
sizeof(值)
1个字节(byte)=8个比特(bit)。能存储8个二进制数字。
#include
int main()
{
int a=10;//申请一个空间,存放整型10
printf("%d",sizeof(a));//计算a的大小,并打印结果
printf("%d",sizeof(int));
//%d打印整型。语法("%格式化输出",取(访问)哪里的数据打印)
return 0;
}
两个4,说明int类型向内存申请了4个byte的空间;
\n换行。如果没有第一个printf里的\n,44会连在一起。
2. 声明
在c语言中,你想要使用一个东西,必须声明,(同前文的库函数需要打招呼一样)
数据类型 数据名=初始值;
前面把它叫做向内存申请空间,其实也叫声明。
注意,注意,注意。声明必须放在代码头。即({)下面就是所有的声明。如果你突然想到用一个值,就直接声明,但它前面有printf,会报错。(这里我不是很清楚,就当放前面就好)。C++则什么地方都可以。
int main()
{
int a=10;//声明
printf("%d",a);//它是可以运行的
return 0;
}
int main()
{
int a=10;
printf("%d",a);//代码到这里都能运行
int b=20;//前面有printf了,已经不属于代码块({})头部,无法运行
printf("%d",b);//前面已经无法运行,打印不了b的值20
return 0;
}
short短整型和int整型都可以表达整型,但大小上有区别
特别指出由于ASCLL码,char类型本质是整型(后面讲)
long类型是4/8,c标准只规定sizeof(long)>=sizeof(int)。取决编译平台,32位是4,64位是8。(我也不知道为什么我是64位,还是4字节)
int a=100;//代码块({})外,为全局变量
int main()
{
int b=10;//代码块({})内,为局部变量
return 0;
}
把printf拷进红圈,运行了。充分说明num1作用域在红圈(小代码块)内。
代码运行。a的作用域在红圈代码块内,绿圈代码块在红圈内,被红圈大代码块包含,自然能使用。
scanf("%d",&某个变量);
用scanf函数所输入的值需要用空间来存放,num1和num2作用就是向内存申请空间供scanf输入的值存放。
7. 局部变量和全局变量的名称冲突,局部优先。
尽量不要让局部变量和全局变量名字冲突,容易产生bug
8. 使用外部全局变量
extern声明外部符号
extern 外部数据类型 外部数据名;
同时解释了全局变量的作用域是整个程序。
9. 变量的生命周期
#define _CRT_SECURE_NO_WARNINGS的含义、作用
const 数据类型 数据名=值;
被const修饰的变量只是具备了常量的属性,披了常量的皮,本质上还是变量。用数组验证。
数组:一组相同类型的集合。目前只要知道表达就行。
数据类型 数组名[数组内的元素个数]={元素1,元素2,元素3……};
([])内的值必须是一个整型常量,可不写,计算机会计算([])的个数。但[]内不能是变量。当([])为变量时,报错。
因此,我们就可以将变量放进数组([])里验证const修饰的变量的本质。
不能运行。如果能,你的源文件多半是.cpp。
5. #define定义的标识符常量:变量被#define定义后,本质上已经变成了常量
#define 标识符 数值
(没有结尾)
num被#define定义后放到数组arr里能运行,说明num真是常量。
6. 枚举常量:一一列举的常量,有排序。
enum 枚举名
{
元素1,
元素2,
元素3
};
这些元素就叫做枚举常量
使用:enum 枚举名 变量=枚举内一个元素;
枚举常量都有一个默认的值,这个值是从0开始一一排序的。
枚举常量不能被改变(May),被枚举常量赋值过的变量(a)不能用字面常量赋值
字符串:一串字符。(“字符串”)
"hello worlk"
字符串hello worlk。特殊的空字符串(“”),有时用到。
字符串结束标志:字符串后面都隐藏了一个\0。"abc"除了’a’,‘b’,‘c’之外,其实还有一个\0,真正为’a’,‘b’,‘c’,‘\0’。‘\0’作为结束标志,不算字符串内容,用来结束字符串。
烫烫烫?
因为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,所以打印的是随机值,不固定。
一个printf就搞定了。但如果让你输出一个abc\n呢?
没打印\n,因为\n是转义字符,\把n转变成换行。两图下面那段话与abc的距离不一样。后者多了一行。
打印地址c:\test\32\test.c
\是转义字符,它后面有特定字符时,会转变原来的意思
\t是一个转义字符,相当于tab水平制表符。要打印出\t,怎么办呢?在它的前面加一个\,让\t不再是tab键,而就是一个普通的\再加个t。
第一个\将第二个\转义成了普通\,不再是用来转义字符的\。
转义字符 | 含义 |
---|---|
\? | 在书写多个问号时使用,防止它们被解析成三字母符 |
\’ | 用于表示字符常量’ |
\" | 用于表示一个字符串内部的双引号 |
\\ | 用于表示一个反斜杠,防止它被解释为一个转义序列符 |
\a | 警告符号,蜂鸣。电脑响一下 |
\b | 退格符 |
\f | 进纸符 |
\n | 换行 |
\r | 回车 |
\t | 水平制表符tab |
\v | 垂直制表符 |
\ddd | ddd表示一个八进制数字。如:\127 |
\xdd | dd表示2个十六进制数字。如:\x439 |
这里再给大家介绍几个例子。想打引一个'
打印单个’和"时,在第二个(‘)前加个\,防止和第一个(’)配对
打印(")也是如此。如果但印的是$,源文件可能是.cpp
用strlen计算字符串长度验证转义字符只算一个长度。
当将两个\t和\32作为一个字符时内容的长度是13,说明转义字符只算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条,就必须把全部语句用代码块({})括起来。
有一天主管告诉你,打够100w颗螺丝,涨工资。为此你决定天天打螺丝。一天打1w颗螺丝,打了100天,月工资+2北。这就是生活中的循环。
循环语句,现在使用的是while循环(还有两种循环)。
while(循环条件)
{
执行语句1
执行语句2
调整
}
运算符+=和++
q+=1和i=q+1完成等价。q<100判断是否继续循环的条件。符合,继续循环。不符合则跳出循环。
printf是执行语句,while循环可以有多条执行语句。
q+=1和count调整。
库函数:c标准提供的,有固定名字,要引对应头文件。
自定义函数:自定义的函数
库函数:printf、scanf等都是库函数,有自己的使用方法
自定义函数:自己写的函数
像在数学里
f(x)=2x
这是一个简单的函数,然而写概念题时,会给出一种概念函数(其实也就是自定义函数)
f(x,y)=2x+3y
x=1时,y=2时f(1,2)=21+32=7
这就相当于自定义函数。而你要重复使用一个超级复杂的算法时,每次都只用+-*/去实现,是不太可能的。自定义函数将减少你的工作量。
数据类型 函数名(相同数据类型 值,相同数据类型 值)
{
定义的算法
return 算法结果;
}
假设要声明1,2,3,4等一堆数字时,一个个声明很浪费时间。因此c语言提出了数组
数据类型 数组名[数组内元素个数]={‘元素1’,‘元素2’,‘元素3’……};
数据类型 数组名[一个值];
写有元素的数组叫初始化,初始化后,([])内的值可省略。计算机会自动识别个数。
数组每个元素都有一个下标,默认从0开始。
用这个数组输出4,4对应下标3,所以用arr[3]打印。
while循环结合数组打印出1-10。
有些朋友可能会觉得这里i不是变量吗?为什么能放到arr的[],因为这里的i被赋值了,有一个真正的值。(其实我不知道)
数据类型都是指数组内元素的类型,并不是数组的类型。数组本质上是地址。(数组名是数组首元素的地址)
+ - * / %(取模)
前三种就是数学上的
/除。
分为整除和浮点除。整除,只保留商。浮点除,必须保证被除数和除数至少有一个浮点数,就是数学上的除。
%取模:取余数
<<左移操作符
假设我把一个值为1的变量a<<1之后,它会在1的二进制数列在整个空间左移
可以预见的是a(a=1)被<<1后,值变成了2。
>>右移操作符也是如此。
&、^、|
&按位与操作符:全1为1,有0则0。
其实就是在二进制位上进行数学的逻辑且运算
|按位或:有1则1,全0为0
=、+=、-=、*=、/=、&=、^=、|=、<<=、>>=
=赋值符号,把一个值赋给一个变量。==才是等于的意思
+=和后面的都叫复合操作符
只有一个操作数的符号。
操作数:符号关系到的数字
1+2,+有两个操作数1和2,因此它是双目操作符。
c语言中表示真(默认为1)假(0)
单目操作符 | 含义 |
---|---|
! | 逻辑反操作 |
- | 负值 |
+ | 正值 |
& | 取地址操作符 |
sizeof | 操作数的类型大小(单位字节) |
~ | 对一个数的二进制位取反 |
- - | 前置、后置- - |
++ | 前置、后置++ |
* | 间接访问操作符(解引用操作符) |
(类型) | 强制类型转换 |
!逻辑反操作
把一个数的真假类型转换。
c语言默认真为1
-号和+号
-1,+1对数字取负和取正,只有一个操作数,是单目操作符
&取地址操作符、*间接访问操作符(解引用操作符)在指针那章再讲
sizeof操作数的类型大小(以字节为单位)
计算变量或类型的空间大小。
计算变量时,变量的()可以省略
这时,我还没有把11行的代码放开,当我们放开之后
在变量a可以不加()可以看出,sizeof是一个操作符。函数的表达必须加()。
通过sizeof计算数组大小和个数
~按(二进制)位取反符号
数据是以二进制位在计算机中储存的。~就是在二进制位进行取反,0变成1,1变成0。对0(00000000000000000000000000000000)取反再打印,它应该是什么?
相信初学者会跟我当初一样懵逼,为什么是-1,而不是2^32-1(11111111111111111111111111111111)。
这里涉及了计算机的三码。先声明计算机只能打印原码。
符号位:有符号数(正或负)的二进制位的最高位(第一位),0为正,1为负。
原码:正数和0储存的是原码。(正数原码,补码,反码三码相同)
反码:原码符号位不变,其他按位取反得到。
补码:反码加1。(负数储存的是补码)
只要是整数,计算机储存的都是补码。
当0被~后,符号位从0(正)变成了1(负),此时已经变成了补码存在计算机里。计算机只能打印原码,只能转化为原码-1。
–和++的前置、后置
后置++
前置++
前置–和后置–同上述
(类型)强制类型转换
把一种类型转换成其他类型
正常来说int a=3.14;
这种声明无效。因为3.14是浮点型,不是整型。但可以通过强制类型转换把a强制转换为int。
结果只取整数,舍弃小数点后的数。不建议这样转换,容易出bug。
>、>=、<、<=、!=(测试“不相等”)、==(测试“相等”)
前面四个就是数学上的。!=相当于数学的≠,==相当于数学的=
&&逻辑与、||逻辑或
不要和前面的&、|混淆。&、|是按(二进制)位的,而&&和||是直接计算的。
&&逻辑与:全真(1)为真,一假(0)为假、
假如b为0(假),那么输出的就是0了。
||逻辑或:一真则真,全假为假。
exp1?exp2:exp3;
exp1结果是真,exp2被执行。如果为假,exp3执行。
a>b(exp1)。20>10,exp1不成立,执行exp3(b)。所以输出20。如果a为100,exp1成立,执行a,输出的是100。当然这只是一种简单用法
[] () . ->
[]在数组时用到,在里面输入下标访问元素。()在使用函数用到。后两种后面讲。
逗号表达式
exp1,exp2,exp3,…expn
只要知道能隔开一个表达式就行。
int a=10;
其实省略了一个auto。auto int a=10;
。只不过因为大家都有auto,因此省略不写。extern 数据类型 外部函数名(数据类型,数据类型);
extern 数据类型 变量名;
发展到后来,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;
un/signed无符号数
和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修饰全局变量
新建一个源文件1,在里面我输入了static int AAA=99;
在源文件里用extern调用AAA=99,报错。因为当static修饰全局变量时,改变了变量的作用域,让静态的全局变量只能在自己的源文件(这里AAA只能在源文件1.cpp使用)内部使用,出了源文件就无法使用。
static修饰函数和修饰全局变量作用相同,说法不同。只要知道一个函数是有外部链接属性的(可被其他源文件用extern调用),static修饰后该函数被改变了链接属性,没有了外部链接属性,只有内部链接属性(只能在自己所在的源文件使用)。
struct:结构体关键字(结构体那章讲)
switch:switch语句
typedef:类型重定义
union联合体/共用体(以后介绍)
void无/空
volatile(暂时不介绍,太难)
while:while循环
#define定义标识符常量前面已讲
宏在标识符常量的基础上带上参数。
它相较于标识符常量是有参数(X,Y)的。这就是函数和宏的区别。(感觉和函数区别也不大,可能是简洁吧)。
要讲清楚指针,首先要搞清楚内存。
内存是电脑上特别重要的存储器,计算机中所有程序的运行都是在内存中进行的。
为了有效的使用内存,就把内存划分成一个个小的内存单元。为了能够有效的访问(调用)到每个内存单元,就把内存单元进行了编号,编号也被称为该内存的地址,即编号=地址
内存的地址就当于现实生活中的身份证地址,通过它可以找到你家,而内存的地址也是如此。
上图的二进制序列作为每一个内存块的编号,同时也是地址。而一个内存块是多大才算合适呢?假设是1个bit,那么2^32个内存亏块换算成g就有0.5g,这看起来很小。那是多少合适呢?在假设它是1个byte,那么总共有4g,这时就差不多了。
int main()
{
int a=10;
return 0;
}
这里向内存申请了4个字节的空间,那这4个字节放在哪里了?我想拿这个地址,就要用到&取地址操作符和*解引用操作符。
先来用&来找一个变量的地址。
它是用16进制打印的一个地址。它其实还是一个二进制,只不过是用16进制表示方便。
这个10被我们存到了变量a里面,&a其实就是一个地址(a的地址)。那么我们能不能把它存起来。其实也是可以的,c语言只要是一个值都能存起来。那么该怎么存?比如用p来存放&a。有一种变量是专门用来存放地址的,被称为指针变量。而指针是指向这个地址的,也可以说指针=地址=编号。p就是指针变量p。
int main()
{
int a=10;
int* p=&a;
return 0;
}
int*是p的类型,p是指针变量,p里面存放的是a的地址。想要存的变量是什么类型,指针就是给该类型加个*。
说明a地址确实被存放到了p里面。但我们仅仅是为了存起来吗?就像你的朋友把你身份证上角的地址记住,是为了未来有一天通过这个地址去到你家。
计算机也是一样。在p前面加*,这个*就是解引用操作符,通过它来解引用操作,找到它所指向的对象a,理解为*p=a
其实就是p被*解引用成了a,a的值被赋值为20。这里还是要解释一下下面三行代码的意思。
还要清楚的是,这里第二行的*和第三行的*的含义是不一样的。第一个*是用来说明p的类型是一个指针变量,只是一个语法形式。
而*前面用int是因为你的指针p指向的是a。a是int,p指向它,所以是int。第二个*是用来对a进行解引用,找到a。
指针的大小是4/8(32位是4,64位是8)个字节,无论指针是什么类型。
32位计算机,有32根地址线,产生32个二进制数字。一个数字对应一个bit,因此是4个字节。64位机器,8个字节。因此指针的大小不取决于指针类型,取决于计算机。
我的计算机是64位的,是8个字节。(好像也与编译器平台有关,不是很清楚,初学者忽略这句话就好。)
计算机中的int、double类型等都是用来描述生活中的值,那么一个人该怎么表示?int?short?都不符合啊!因为人有姓名、身高、身份证等属性。在比如书,有书名、价格、作者等数学。对这些复杂对象的表示,c语言就是用结构体来解决的。
自己创造出来的一种类型。用它来描述这些复杂对象的属性。
struct 复杂对象名 //struct结构体关键字
{
属性1 元素1;
属性2 元素2;
...
};//这个分号不能少,专门用来结束这个类型定义
int main()
{
struct 复杂对象名 变量={元素1,元素2...};
}
创建一个人a1,他的名字叫张三,身高1.8m。
假设要打印a1的名字和身高,通过(.)来访问数据
a1.name
和a1.stature
正常访问只需要到a1,而访问结构体元素就需要进一步打开它里面的元素。点的作用相当于打开name(我是这么理解的)
其实还可以通过指针来访问结构体元素
int main()
{
struct people a1={"张三",1.8};
struct people* pp=&a1;//指针pp指向的是a1,a1的类型是struct people。因此pp的类型只需要加*
printf("姓名:%p\n",(*pp).name);//这里*pp是对pp解引用找到a1
printf("身高:%lf\n",(*pp).stature);
}
但这种方式有点啰嗦,我们用另一个符号来表示。
int main()
{
struct people a1={"张三",1.8};
struct people* pp=&a1;
printf("姓名:%p\n",pp->name);
printf("身高:%lf\n",pp->stature);
//. 结构体成员
//-> 结构体指针
}
指针pp不是指向a1的吗?怎么指向,用(->)指向。
合起来的意思就是pp指向的对象a1里的name。这样的方法明显简单不少,且更直观。pp是指针嘛,用箭头指向a1。
假设这个张三长高了5cm,所以有所修改
那再假设张三突然想把名字改了叫李四,依旧如此吗?
报错。因为stature是一个变量,它可以直接进行赋值。而name是一个数组的数组名,数组本质上是一个地址,不能修改。硬是要改,要用到一个函数。
strcpy string-copy 字符串拷贝
如果想要修改数组name的值,只能把一个新的字符串拷贝到name里。
语法strcpy(目的地,"拷贝内容");
目的地:放哪里去。拷贝内容,放进去的内容。
成员是字符串的,它的赋值形式都需要用到strcpy。
先来做一个自我介绍吧。本人大一非专业生,报考的时候(爱好)就想报计科或软工的,没本事被调剂了。我专业教Python,但是我在2月初选择了万物之始的C语言。经过半个月的学习才学完这篇的知识点,这里推荐B站的鹏哥C语言,我就是看鹏哥的视频入坑的。
2. 我是2月1号开始的编程,快月底的时候电脑坏了,等修好(维修站远,开学的时候去的)已经过了差不多半个月,一天8小时以上。之后开始写这篇博客(2月底),开始的时候啥都不会,Markdown都弄了大半天(现在还是不懂)。因此摆烂了一个多星期(大家不要学我哈),后来再十多号这样子重新捡回来,又花了十天左右一点点写(新兴文科专业,太多课了),写完有花了一两天整合。虽然过程很久,但我觉得很值。在写的这个过程(费曼学习法),很多忘了的知识被重拾起来,而且印象深刻。因此,我推荐大家能够用写博客的方式学习编程。还有不要摆烂放弃。希望能与大家一起学习分享,早日成为大牛!