个人主页:Weraphael
✍作者简介:目前正在回炉重造C语言(2023暑假)
✈️专栏:【C语言航路】
希望大家多多支持,咱一起进步!
如果文章对你有帮助的话
欢迎 评论 点赞 收藏 加关注
本篇文章是基本了解
C语言
的基础知识,对C语言
有一个大概的认识,目前不做详解。篇幅很长,建议大家配合目录食用~
C语言
是一门计算机语言,是人和计算机交流的语言,它广泛用于底层开发(Linux
就是用C语言
写的)。当然,计算机语言还包括C++
、Java
、Python
、Go
等等。- 其常用的编辑器有
Clang(苹果公司编写)
、GCC(Linux环境)
、MSVC(visual studio - 集成开发环境IDE)
等(本人用的是VS2019)
每学习一门新的语言,当然是要打印hello world!
// 第一个C语言代码
#include
int main()
{
printf("hello world!");
return 0;
}
【输出结果】
- 在
C语言
代码中,不管是括号、分号、大括号等符号必须是 英文模式 下敲出来的。main
叫主函数,在C语言
中 必须要有主函数。printf
是c语言
标准函数库提供的一个格式化输出函数。因此使用printf
函数就要包含头文件#include
(scanf
- 输入函数,也要包含此头文件)。 其中stdio.h ---standard input output header
– 标准输入输出头文件'\n'
– 换行(相当于回车)
- 主函数是程序的入口。
- 在
C语言
中 必须要有主函数。- 但需要注意的是:一个工程(项目)可以有多个源文件(
.c
后缀文件),但main
只能有一个。
验证第3点main
函数只有一个:
例如在一个项目中再创建一个源文件(Test2.c
),内容如下:
我们为什么要写代码?因为代码能够解决未来生活中的问题,
C语言
必须有能力描述我们的生活。假如设计在线商城,我们得用计算机的语言把它描述出来,比如商品陈列、详情、定价、编号等,所以C语言
就引入了数据类型。
char //字符数据类型
short //短整型
int //整型
long //长整型
long long //更长的整型(C99引入)
float //单精度浮点数
double //双精度浮点数
现在我们利用以上类型来打印不同的数据到屏幕上:
【程序结果】
// 以下单位是字节
char类型大小:1
short类型大小:2
int类型大小:4
long类型大小:4
long long类型大小:8
float类型大小:4
double类型大小:8
原因是:C语言标准规定:sizeof(long) >= sizeof(int
)
sizeof
是一个操作符,是计算变量的大小。(单位:字节)因此这里就要说说计算机常见的单位:
bit 比特位 1 byte = 8 bit
byte 字节
KB 1KB = 1024 byte
MB 1MB = 1024 KB
GB 1GB = 1024 MB
TB 1TB = 1024 GB
PB 1PB = 1024 TB
记住
int
类型的数据范围:[-21亿+,21亿+]
有了类型就是为了创建变量
在数据后加f
后,就是float
类型了
这里提一个好的编程习惯:在创建变量的同时给一个初始值。
- 只能由字母、数字和下划线(_)组成。
- 不能以数字开头
- 变量名是区分大小写
- 变量名不能使用关键字。
变量分为局部变量和全局变量。
- 局部变量:定义在
{}
里的变量- 全局变量:定义在
{}
外的变量
通过以上代码我们发现,当局部变量和全局变量的变量名冲突时,局部优先。但是建议大家在平时尽量不要冲突。
哪里要使用变量,就在哪里创建
例如:编写代码,计算两个数的和
scanf
是一个库函数,可以从键盘接收数据,要引用头文件#include
,同时 需要在变量前加上&(取地址)
注意:如果没有第一行代码,若使用scanf
时会报错。
原因是:
scanf
是C语言提供的,当然也可以使用scanf_s
(visual studio
提供的),但是建议大家使用scanf
函数。最后如何解决请看这篇文章:【点我跳转】
// 当创建变量并给它赋值叫做定义
int a = 1;
int main()
{
printf("%d\n", a);
return 0;
}
int a; //变量的声明
int main()
{
printf("%d\n", a);
return 0;
}
int a = 1;
如果变量的使用在变量的定义之前,使用前需要加上声明。原因是:代码在编译阶段是从上到下开始扫描的,当来到
printf
时,如果没有声明,编译器就不知道有a
这个变量,所以会导致报错。
什么是作用域呢?直白来说,哪里可以使用变量,哪里就是它的作用域。
#include
int main()
{
int a = 100;
printf("%d\n", a);
return 0;
}
在上述代码中,变量a
的作用域就是代码块{}
的内部。
总的来说,局部变量的作用域:就是变量所在的局部范围。
代码解析:因为main是程序的入口,代码先从第119行开始执行,接下来开始打印main:211
在屏幕上,接着又调用test
函数,最后打印test:211
在屏幕上。(这里使用了函数部分知识,先凑合看,后面会讲)
有没有发现,全局变量既可以在主函数中使用,也能在其他函数中使用,这意味着在任意地方都能使用。由此可以得出结果,全局变量的作用域是整个工程。
全局变量还能怎么用呢?假如我把变量a
放在同一工程不同源文件(Test1.c
),程序是否能运行起来呢?
程序报错了,让我们应该如何解决呢?声明外部符号 — extern
变量的生命周期是指:变量的创建到变量的销毁之间的一个时间段。
以上代码是编译不过的,这是为什么呢?原因是:变量a
是个局部变量,它的作用域是中间的代码块。进入作用域生命周期开始,出作用域生命周期结束,所以变量a
出了中间部分的代码块作用域,它就被销毁(还给了操作系统),所以当打印第二个a
时,程序就报错了。这就是局部变量的生命周期。
- 字面常量
const
修饰的常变量#define
定义的标识符常量- 枚举常量
【举例】
#include
// 3. #define定义的标识符常量(也能放在main函数里)
#define M 100; //定义了一个M,它的值是100
// 4. 枚举 --- 一一列举
//假设列举三原色
enum Color
{
// 编译器默认red,green,blue分别是0,1,2(常量)
red,
green,
blue
};
int main()
{
// 1. 字面常量
3;
100;
'a';
"abcdef";
// 2. const修饰的常变量
const int N = 100010;
N = 1; // 这是错误的,const修饰的变量具有常属性,不可修改
// const从语法层面上限制了变量不能被修改,但其本质上还是变量。
return 0;
}
常变量,是常量为什么会有一个“变“呢?让我们再举一个例子,加深对const
的理解
我们可以使用数组来验证,因为数组[]
操作符里必须是常量
显然报错了。这说明编辑器依然认为**a
的属性是变量**。
总结:const
从语法层面上限制了变量不能被修改,但其本质上还是变量。
“hello world!\n”
这种由双引号引起来的一串字符称为字符串字面值,或者简称字符串。
注意:字符串的结束标志是'\0'
的转义字符。在计算字符串长度时,'\0'
是结束标志,不是字符串内容。
一般存多个字符或者字符串都是用数组存储的
#include
int main()
{
char ch1[] = { 'a','b','c' };
char ch2[] = "abcdef";
return 0;
}
C语言提供了一个库函数:strlen
,可以计算字符串的长度。但使用strlen
需要包含头文件:#include
这里注意一下:
- 其实
ch1
字符串长度其实不止15
,它是个随机值,因为它没有结束字符'\0'
- 字符串
ch2
在计算长度时没有算上末尾的'\0'
,它统计的是'\0'
之前字符的个数
顾名思义就说转变原来的意义
【例如】
【经典笔试题】
以下代码输出的结果是多少?
#include
int main()
{
printf("%d\n", strlen("c:\test\628\test.c"));
return 0;
}
【答案】
解析:从左往右,
\t
、\62
、\t
是转移字符。分别都只算一个字符。
当代码中有些代码比较难懂,可以添加文字注释。当然也可以注释掉不需要的代码
注释风格:
// 这是一个注释,只能注释一行
/*
这是一个多行注释
这是一个多行注释
这是一个多行注释
注意:这种注释风格不能嵌套
*/
//以下是伪代码
if(判断条件)
{
// 逻辑实现
}
else if(条件判断)
{
// 逻辑实现
}
else
{
// 逻辑选择
}
【例子】
编写代码,输入一个人的智商值,如果智商大于等于140,输出“Genius”。否则输出“fool”
#include
int main()
{
int score = 0;
scanf("%d", &score);
if (score >= 140)
{
printf("Genius");
}
else
{
printf("fool");
}
return 0;
}
意思是循环做,直到满足条件停止。当然还有
for
、do while
,这些循环会在后面讲到
【例子】
输出1~10
#include
int main()
{
int i = 1;
while (i <= 10)
{
printf("%d ", i);
i = i + 1;
}
return 0;
}
在数学中,我们经常把函数定义成:f(x);在C语言中,把函数定义为:
函数名(类型 x,...)
,其特点是:简化代码,代码重复使用。
在变量的使用中,写过有关加法的代码,那么能否把加法的过程封装成一个函数呢?
#include
int add(int x, int y)
{
// 1. 第一种写法
//return x + y;
// 2. 第二种写法
int res = x + y;
return res;
}
int main()
{
int num1 = 0;
int num2 = 0;
scanf("%d%d", &num1, &num2);
// 封装一个add函数
int ans = add(num1, num2);
printf("num1 + num2 = %d\n", ans);
return 0;
}
建立一个add
函数,把num1
传到int x
,再把num2
传到int y
中,经过函数内部的逻辑运算,最后把res
的结果作为返回值给到ans
,因为res
的结果一定是整型,所以add
函数的返回值是int
。但注意,函数名要根据实际的意义来命名
我们还能这样理解:可以把add
看作一个工厂,把num1
和num2
相当材料输入到int x
和int y
中,工厂里再进行加工(x+y
),最后通过return
返回到ans
中。
如果我们要存
1-10
的数字,怎么储存?难道是一直定义变量直到10吗?这样会不会显得麻烦?所以在C语言中给了数组的定义:一组相同类型元素的集合。
int arr[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// 定义整型数组,最多只能存放10个整型元素
// 注意:
// 在C99之前,[]内不能使用变量
// 在C99中引入变长数组的概念,这时候
// 这时候数组的大小是可以使用变量的,但数组不能初始化!!!
假如数组不放满指定数组元素个数是否可以呢?其内容又是什么?
#include
int main()
{
int arr1[10] = { 0, 1, 2,3,4,5,6,7,8,9 };
int arr2[10] = { 0,1,2,3,4 };
return 0;
}
可以在监视中看看效果,首先在编译器中先按F10
,然后在调试
中找到窗口
,最后在窗口
中找到监视
:
随后在名称中输入arr1
和arr2
,就能看到他们的内容:
有没有发现未放满10个元素的整型数组arr2后面补了5个0,这个叫:不完全初始化,剩余的默认初始化为0。
如果我再创建字符数组arr3
,数组中也不放满对应元素,结果又会是如何呢?
结果很明显了,对于字符数组,它的剩余默认初始化为'\0'
。
C语言规定:数组的每个元素都有一个下标,且下标都是从0开始的。
假如打印数组中的3
,就可以用到[]
这个操作符:
对于数组4个元素,从语法上规定:第一个元素0的下标就是0,第二个元素1的下标是1,后面以此类推…
如果想深入了解建议看看这篇博客:点击跳转
如果想深入了解建议看看这篇博客:点击跳转
如果想深入了解建议看看这篇博客:点击跳转
! 逻辑反操作
在C语言中,非零表示真,零表示假,操作符
!
即可以把真变成假,假变成真
flag
初始值为0
,其本质为假;在if
条件判断加了!
,假变成了真,所以在屏幕上打印了hello world!
负值和正值操作符比较简单,这里就不过多赘述了
感兴趣请参考这篇文章:点击跳转
sizeof
和strlen
的区别
sizeof
是操作符,是计算变量所占内存空间的大小,单位是字节。strlen
是库函数,是计算字符串的长度,统计的是字符串中‘\0’
之前出现的字符个数
感兴趣请参考这篇文章:点击跳转
感兴趣请参考这篇文章: 点击跳转
感兴趣请参考这篇文章: 点击跳转
感兴趣请参考这篇文章:点击跳转
3.14
原先是浮点数类型,但在3.14
前加上(int)
就变成了整型(只取整数部分)。
>
>=
<
<=
!= 用于测试“不相等”
== 用于测试“相等”
关系操作符比较简单,这里要重点区分“=”和“==”。在C语言中,“=”是赋值操作符;两个等于才表示相等。
&& 逻辑与:两真为真,一假全假
|| 逻辑或:一真全真,两假全假
- 若
expl
的值为真,则结果就是exp2
的值。- 若
exp1
的值为假,则结果就是exp3
的值 。
exp1,exp2,exp3...
从左向右依次执行,最后的答案是最右边的表达式
请参考这篇博客:点击跳转
注意:以下关键字不能当变量名来使用!
auto
break
case
char
const
continue
default
do
double
else
enum
extern
float
for
goto
if
int
long
register
return
short
signed
sizeof
static
struct
switch
typedef
union
unsigned
void
volatile
while
注意:sizeof
既是关键字,也是操作符。
另外,操作符是C语言本身预先设定好的,用户是不能创造关键字的。
最后,我对这些关键字做了分类:
typedef
的作用是:类型重命名。
我们应该怎么用呢? 比如我们要定义一个无符号的整型,我们可以这么写:unsigned int num = 0
,大家有没有发现这个名字很长。因此我们可以用typedef
来进行类型重命名:
#include
typedef unsigned int ui;
int main()
{
ui num = 0;
return 0;
}
我们可以把unsigned int
(类型)重命名为ui
(开头首字母缩写),ui
就是unsigned int
的别名,这样写把代码复杂类型简单化。
在C语言中:
static
是用来修饰变量和函数的
①修饰局部变量 - 称为静态局部变量
②修饰全局变量 - 称为静态全局变量
③修饰函数 - 称为静态函数
以下代码会输出什么呢?
#include
void test()
{
int a = 0;
a++;
printf("%d\n", a);
}
int main()
{
int i = 0;
while (i < 10)
{
test();
i++;
}
return 0;
}
【程序结果】
为什么是10个1呢?原因是:a
是个局部变量,进入作用域生命周期开始,出作用域生命周期结束。当每次调用test
函数,创建局部变量a
,出了作用域,变量a
被销毁(还给了操作系统),因此每次调用test
函数都只会打印1。
那如果我在变量a
前加static
结果还会是一样吗?那我们来看一下。
#include
void test()
{
static int a = 0;
a++;
printf("%d ", a);
}
int main()
{
int i = 0;
while (i < 10)
{
test();
i++;
}
printf("\n");
return 0;
}
【程序结果】
答案是1~10
。为什么能输出这个结果呢?static
修饰局部变量的作用是什么呢?
分析:第一个结果为1很好理解,可是第二个结果为2有没有发现第一次的结果并没有销毁。以此类推…
那么static
的本质是什么呢?我们来看看下面这幅图:
栈区是进入作用域创建,出了作用域就会释放,放在静态区的数据创建后,直到程序结束才会释放。被static修饰都是在静态区里。 堆区目前不做了解。
总结:普通的局部变量是放在栈区上的,这种局部变量进入作用域创建,出了作用域释放,但是局部变量被static修饰后,这种局部变量就放在了静态区,放在静态区的变量,创建好后,直到程序结束才释放。
本质:static的修饰改变了局部变量的存储位置,因为存储位置的差异,使得执行效果不一样。
注意:被static修饰是不影响作用域的!!!但是生命周期变长了。
假设我在另一个源文件创建一个全局变量:
若全局变量a
被static
修饰,结果又会是如何呢?
程序发生报错了(无法解析的外部符号),所以为什么会出现这种现象呢?
原因是:全局变量本身是具有外部链接属性的,在一个文件中定义的变量,在另一个文件可以通过链接使用(extern
),但是如果全局变量被static修饰,这个外部链接属性就变成了内部链接属性了,这个全局变量只能在自己所在的源文件内部使用。最终使得全局变量的作用域变小了。
其实static
修饰函数和修饰全局变量非常类似。让我们看看下面的代码:
现在,我们在函数前加个static
会怎么样呢?
结果是程序报错了(未定义外部符号)
原因是:函数本身是具有外部链接属性的,被static
修饰后,外部链接属性就变成内部链接属性,使得这个函数只能在自己所在源文件内部使用,其他源文件无法使用。和全局变量其实是一个道理。限制了作用域。
这个在前面已经讲过了,来看看简单的代码样例吧
比如:写一个函数求两个数的最大值,不难可以写出以下代码:
#include
int Max(int x, int y)
{
if (x >= y)
return x;
else
return y;
// 或者还可以用三目操作符
//return x > y ? x : y;
}
int main()
{
int a = 10;
int b = 20;
int c = Max(a, b);
printf("最大值是:%d\n", c);
return 0;
}
除此之外,我们还可以用宏来求出两个数之间的最大值
#include
#define Max(x, y) (x > y ? x : y)
int main()
{
int a = 10;
int b = 20;
int c = Max(a, b);
printf("最大值是:%d\n", c);
return 0;
}
解析一下代码:我这定义了一个宏叫Max
后面跟着圆括号,x
,y
叫参数,后面的三目运算符叫宏体 。其中a
传给了的x
,b
也传给了y
,因此宏体其实就被替换成(a>b?a:b)
。可能有人会想,这不就和函数差不多么?注意:宏一般是解决逻辑比较简单的问题,而函数通常用于解决复杂的问题。
如果我们要求两个数的和该怎么写呢?
#define ADD(x,y) ((x)+(y))
想要理解什么是指针,必须先了解什么是内存。内存是电脑上的存储设备,一般都是4G/8G/16G等,程序运行的时候会加载到内存中,也会使用内存空间,例如任务管理器:
我们将内存划分为一个个小格子,每一个格子是一个内存单元,大小为一个字节,对每一个内存单元进行编号,假设未来要找一个内存单元,就可以通过编号很快的找到,我们把这些编号叫做地址,而地址在c语言中又叫做指针。
举一个例子,在下图中,假设定义一个变量 int a=10
,一个int
类型的变量,需要占4个字节的空间,而每个空间都有地址,&a
取出的是哪一个的地址呢?其实取出的是第一个字节的地址(也就是较小的地址),也就是说,&a
最终取出来的地址是0x0012ff40
,当然,可以把这个地址存到一个变量中,int* pa=&a
,*
表示pa
是一个指针,int
代表pa
所指向的类型是int
类型,这个pa
也叫做指针变量(它是专门用来存放地址的)。
指针变量,就是用来存放地址的变量。(存放在指针中的值都被当成地址处理)。
那么问题来了:一个内存单元到底是多大? — 刚刚讲过,就是一个字节
那它又是如何编址的呢?
经过仔细的计算和权衡我们发现一个字节给一个对应的地址是比较合适的。 对于32位的机器,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平(高电压)和低电平(低电压)就是(1或者0)
那么32根地址线产生的地址就会是:
00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000001
…
11111111 11111111 11111111 11111111
这里就有2的32次方个地址。 一个地址管理一个内存单元,那么2的32次方个地址就能管理2的32次方个内存单元,也就是2的32次方个字节,那2的32次方个字节又是多大空间呢?
根据进制转化:
2^32Byte = 2^32÷1024KB÷1024MB÷1024GB = 4GB
同样的方法,64位机器,也可以计算出来。
这里我们就明白:
- 在32位的机器上,地址是32个0或者1组成二进制序列,那地址就得用4个字节的空间来存储,所以 一个指针变量的大小就应该是4个字节。
- 那如果在64位机器上,如果有64个地址线,那一个指针变量的大小是8个字节,才能存放一个地址。
总结:
- 指针变量是用来存放地址的,地址是唯一标示一个内存单元的。
- 指针的大小在32位平台是4个字节,在64位平台是8个字节。
通过以上代码就验证了,指针变量p
存放的就是a
的地址。
接下来,我们可以使用*
解引用操作符来对其使用:
结构体使得C语言有能力描述复杂信息。
假如我们要描述一个学生(名字+性别+年龄)
// 定义一个学生的类型
struct Stu // 这个是类型
{
char name[10]; // 姓名
char sex[5]; // 性别
int age; //年龄
// 以上三个变量叫做结构体成员
};
// 分号不能省略
int main()
{
// 类型(struct) + 变量(s)
struct Stu s = {"张三", "男", 20};
return 0;
}
代码解析:首先我们要先定义一个学生的类型,其中{}
里的是结构体里的成员,注意最后要有分号 。接下来,在主函数中创建一个struct Stu
的类型,其中s
是变量。
那么问题来了,我们应该怎样打印出学生的信息呢?让我们接着往下看。
我们需要利用点.
操作符来访问结构体里的成员
模板:结构体变量.成员名
// 定义一个学生的类型
struct Stu // 这个是类型
{
char name[10]; // 姓名
char sex[5]; // 性别
int age; //年龄
// 以上三个变量叫做结构体成员
};
int main()
{
struct Stu s = { "张三", "男", 20 };
printf("姓名:%s\n性别:%s\n年龄:%d\n", s.name, s.sex, s.age);
return 0;
}
【程序结果】
当然还能使用指针解引用的方法
struct Stu // 这个是类型
{
char name[10]; // 姓名
char sex[5]; // 性别
int age; //年龄
// 以上三个变量叫做结构体成员
};
int main()
{
struct Stu s = { "张三", "男", 20 };
//printf("姓名:%s\n性别:%s\n年龄:%d\n", s.name, s.sex, s.age);
struct Stu* p = &s;
printf("姓名:%s\n性别:%s\n年龄:%d\n", (*p).name, (*p).sex, (*p).age);
return 0;
}
【程序结果】
取出结构体变量s
的地址存放到指针变量p
中,再对其解引用,因此*p
本质上就是s
大家有没有发现解引用再去访问成员变量写的怪长怪累的,因此这里引用了箭头操作符:
// 定义一个学生的类型
struct Stu // 这个是类型
{
char name[10]; // 姓名
char sex[5]; // 性别
int age; //年龄
// 以上三个变量叫做结构体成员
};
int main()
{
struct Stu s = { "张三", "男", 20 };
//printf("姓名:%s\n性别:%s\n年龄:%d\n", s.name, s.sex, s.age);
struct Stu* p = &s;
//printf("姓名:%s\n性别:%s\n年龄:%d\n", (*p).name, (*p).sex, (*p).age);
printf("姓名:%s\n性别:%s\n年龄:%d\n", p->name, p->sex, p->age);
return 0;
}
【程序结果】
这里运用到了->
箭头操作符,因为p
指向了s
,所以ps->name(sex/age
)就是ps
指向了s
的name(sex/age)
模板:结构体指针->成员变量