C语言基础
C语言基础
前言
和所有编程语言一样,要认识一个语言的结构得从”hello,world”开始,我们使用C语言来输出”hello,world”吧。代码如下:
1 |
|
通过上面的代码我们初识C语言了,下面是分析。
**分析:**第一行是调用头文件,#include
是C语言调用标准头文件的标准方法。stdio.h
是标准库,调用了这个库我们才可以使用printf()
函数
第二行是主函数,在C语言中可以有好多个函数,当时只能有一个main
函数,main
函数是程序的入口。main
前面的是返回值类型,在好多大学中,教C语言的老师喜欢把前面的类型省略,这个叫缺省(xing),但在C语言标准里,前面的int
是不可以去掉的。
第三行是函数内执行的语句,这个是一个输出函数printf()
,它的作用是将括号里的数据输出到显示屏中,给用户查看。(具体看第一章)
第四行是返回值。(具体内容在函数那一章再细讲)
**细节:**C语言对语法要求不是那么的高,不像python一样需要严格遵循缩进,C语言需要在函数内执行的语句只需要在 {}
中即可,一行语句执行完加 ;
即可。
要执行C语言的程序必须得进行编译,编译完之后才能运行,简单解释一下为什么要这么做:
计算机它能认识0和1这种低级语言,而我们写代码是用C语言这种高级语言来写的,但计算机不认识,必须需要一个编译器来将高级语言转换为低级语言,这样计算机就可以执行。
C语言编译出来的后缀为: .obj
可运行的程序的后缀为: .exe
这只是一个笔记,笔记主要是将重要的内容记下来,所以好多不重要的内容或者是一看就会的内容我就省略了
一、输出函数
前言
在C语言中是没有自带的输出函数的,而是必须通过调用标准库文件才能使用输出语句,先对输入输出函数进行了解,会使你对C语言有更好的兴趣。
printf();
- 头文件:
stdio.h
printf
函数的功能:格式化输出函数,用于向标准输出设备按规定格式输出信息。
printf函数的调用格式:
printf("格式控制字符串",输出项清单);
格式控制字符串:用于指定输出格式;由格式字符串和非格式字符串两种组成。格式字符串是以%开头的字符串。
输出项清单:
- 输出项可以是常量、变量或表达式
- 要求格式字符串和各输出项在数量和类型上应该一一对应
例如:
1
2
3
4
5
6
7
8
int main()
{
int m=123,n=12345;
printf("m=%5d,n= %3d\n",m,n);
return 0;
}1
cin >> 变量;
对于整形数而言,当格式控制字符规定的长度比数据本身长度还大时,则左侧补空格;若个数控制字符规定的长度比数据本身长度小时,则数据按照其本身长度进行输出。
格式化字符串
表示方法 说明 %d 以十进制形式带符号整数(正数不输出符号) %o 以八进制形式输出无符号整数(默认不输出前缀0,%#o输出前缀0) %x 以十六进制形式输出无符号整数(默认不输出前缀0x,%#x输出前缀0x) %u 以十进制形式输出无符号整数 %f 以小数形式输出单、双精度实数,默认保留6位小数
双精度型可用%lf或%le%e 以指数形式输出单、双精度实数。格式是m.ddddddexxx,默认小数精度为6,指数精度为3,不足补零(明白即可) %g 以%f或%e中较短的输出单、双精度实数 %c 输出单个字符 %s 输出字符串,直到遇到\0,若字符串长度超过指定的精度则自动突破,不截断 %p 输出变量的内存地址
putchar();
1.用法
主要是用于输出字符的函数
2.格式
1 |
|
二、输入函数
scanf();
1.调用格式
1 |
|
必须要加取值符 &
2.分析
将用户输入的值赋值给取值符后的变量
getchar();
1.用法
输入字符
2.格式
1 |
|
三、变量
在C语言中有局部变量和全局变量
1.变量的类型
(1)整型变量
类型 | 储存大小 | 说明 |
---|---|---|
int |
2-4个字节 | 整型 |
short |
2字节 | 短整型 |
long |
8字节 | 长整型 |
unsigned short |
2字节 | 无符号短整型 |
unsigned int |
2-4字节 | 无符号整型 |
unsigned long |
8字节 | 无符号长整型 |
(2)浮点型
类型 | 储存大小 | 说明 |
---|---|---|
float | 4字节 | 单精度浮点数 |
double | 8字节 | 双精度浮点数 |
long double | 16字节 | 长双 |
(3)字符型
类型 | 储存大小 | 说明 |
---|---|---|
char | 1字节 | 字符型 |
unsigned char | 1字节 | 无符号字符型 |
说明:字符型也可以存放数字类型
2.变量的定义
1 | 类型名 变量名 = 赋的值; |
两个都可以定义
1 | 1)变量名不能是保留字 |
注:每次定义后储存的变量一定是同种类型,负责会报错,C语言并没有字符串这个类型,如果要定义字符串需要定义字符型数组
3.变量的转换
例子1:
1 |
|
这个样子是可以转的,因为 char
类型是1个字节,而 int
为4个字节是可以装的
例子2:
1 |
|
如果存储计算后的结果为整型,则会抛弃小数点后的数。
如果为浮点型,则会输出小数点后的数
4.变量的强制转换
有些时候无法通过赋值来进行类型转换
所以这个时候就需要使用强制转换
(1)语法
1 | (数据类型) 变量; |
5.全局变量和局部变量
(1)全局变量
1 |
|
全局变量是定义在函数体外的,在函数体中没办法修改全局变量
(2)局部变量
1 |
|
四、常量
1.定义
(1)define定义
1 |
(2)const
关键字
1 | const 数据类型 变量名 = 值; |
重点在存储类中
五、存储类
1.auto
默认类,就是用完就释放
可写可不写
2.register
感觉没什么用
官方解释说是可以提高运算速度,但现在的CPU运算速度都很快,所以没什么用
3.static
和 auto
是反过来的,使用完它并不会释放
例子
1 |
|
4.const
const 是将变量中的值固定,使得变量无法被修改
1 | const int a = 20; |
但是在vs编辑器中是可以通过指针来修改被const修改的值的
如下
运行结果
在dev-c++中就无法使用这个办法进行修改
六、C语言编程结构
在所有编程语言中有着三种结构:顺序结构,选择结构,循环结构,循序结构
1.顺序结构
顺序结构是编程语言中最基础的一种结构,计算机不像人的思维,它得到代码后它没有办法自己行进跳转(有选择和分支除外),它只能一条一条的执行,一条一条执行的情况就是顺序结构,如下代码
它的执行情况如下
1 -> 2 -> 3 -> 4 -> 5 -> 6这样执行的。
这种执行是非常简单也很基础,但没有办法做一些逻辑很强的程序,所以我们引入了选择结构。
2.选择结构
在介绍选择结构前先介绍逻辑判断语句。
2.1 逻辑判断
在C语言中0表示假,1表示真
关系符 | a1 | a2 | 结果(a1) (a2) |
---|---|---|---|
|| 逻辑或 or | 1 | 1 | 1 |
|| | 1 | 0 | 1 |
|| | 0 | 0 | 0 |
&& 逻辑与 and | 1 | 1 | 1 |
&& | 1 | 0 | 0 |
&& | 0 | 0 | 0 |
然后还有一个逻辑是 !
逻辑非
a1 | a1! |
---|---|
1 | 0 |
0 | 1 |
补充:在c语言中还有一个对于二进制的逻辑表达式
|
:二进制或
&
:二进制与
^
:二进制异或
~
:二进制非
2.2 逻辑判断
两个数之间判断大小,如果成立则值等于1,如果不成立则等于0。
但在C语言中,只能两个数之间相比,不能多个
如下:
1 | a > b; |
是可以的,但下面的是绝对不可以
1 | a > b > c; |
2.3.1 三元运算符
1 | int max = (a>b)?a:b; |
如果a>b成立,则结果为a,否则返回b
2.3 判断语句
2.3.1 if语句
1 | if(判断条件){ |
2.3.2 else语句
else语句是可以没有判断条件的,但必须有执行语句
2.3.3 else if
用法:
1 | if(判断条件){ |
2.3.4 注意
在if语句里可以有也可以没有else、else if语句,但必须要有if语句
2.3.5 switch
switch是单个判断的语句
语法如下:
1 | switch(表达式){ |
但这个语句执行会一直
例如
然后输出的结果如下
它第一个条件判断成功了,但它却继续执行下面的输出代码。
所以我们会在后面添加一个 break
使它判断成功后执行玩就结束。
如下:
2.3.6 switch判断范围问题
在C语言中,一些时候可以用if-else来判断范围问题,有些时候也可以使用switch来判断范围问题。
使用switch的思路是将个个范围的数化为一个特定的数,然后使用switch语句进行判断。
例如:我们需要计算每个销售数段获得的提成的题目
我们使用if-else语句的写法:
1 | int main(){ |
if-else的写法是非常的简单,但是,如果使用switch来改写这个if-else就有点困难,所以我们要有将范围变成具体的数的思路,然后通过数来进行判断。
switch改写的第一种方法:
我们知道,在C语言中进行的是逻辑判断,通过逻辑判断的出来的只能是或则是,所以我们可以在外部对输入的数进行逻辑判断,然后再乘以具体的1,2,3,因为如果成立则为1,不成立则为0。
1 | int main(){ |
2.4 循环结构
循环结构可以让你完成一件重复的事情而不写多条代码
例如,我们要输出100次“hello,world”,完成这件事需要写100次printf函数和100次“hello,world”,这样子是很麻烦的,但如果有循环结构,那我们就可以很轻松的完成这个程序了。
2.4.1 for循环
for循环是编程语言中最简单的循环了,它的定义如下:
1 | for(初始化值; 循环条件; 值的操作){ |
加入for循环先初始化值,然后判断循环条件,满足条件后运行循环体中的值,运行完之后执行值的操作。重复运行直到不满足循环条件为止。
注意:学过Java的可能会这样写:
1 | fot(int i = 0; ;) |
虽然C99可以这样写,但是在C语言中并不能这样写
这个标准只能在C++文件中这么使用
2.4.2 while循环
while循环相对于for循环要简单一点,因为只有一个判断条件。
1 | while (判断条件){ |
加入循环然后判断是否满足判断条件,如果满足则加入循环体内。
2.4.3 do while循环
do while循环增加了一个do语句
1 | do{执行语句} |
先执行do语句内,然后判断是否满足条件,如果满足则继续执行do中的语句。
如果不满足则跳出。
相比于while循环,do while是可以执行一次的。
2.4.4 嵌套循环
简单来说就是循环里套循环,这里借用菜鸟教程的流程图
2.4.5 break和continue
break
break是直接跳出当前循环体
continue
continue是跳过一次
七、数组
1.一维数组的创建和初始化
1.1 数组的创建
1 | //动态创建 |
不完全初始化,剩下的元素默认为0
1.2 数组的索引取值
下标从0开始
例子1:
1 |
|
例子2:
1 |
|
2.二维数组的创建和初始化
2.1数组的创建
1 | int x[][数组的长度(必须是常量)] = {1,2,3,4,5,6,7,8,9}; |
二维数组必须初始化行,可以省略列
2.2 二维数组的使用
一样使用索引值
2.3 数组的地址
数组的地址一般都是第一个元素的地址值,之后的地址就是首地址加字节数
注意:有两种情况不是数组的首地址
1.sizeof(数组名) - 数组名表示整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节
2.&数组名,数组名代表的是整个数组。&数组名,取出的是整个数组的地址
3.数组传入函数中
我们在使用数组的时候,可能会把数组放入函数中进行数据清洗,所以数组如何传入函数就需要说明一下。
3.1 将整个函数复制到函数中使用
第一种传参的方式是将数组中的所有元素拷贝一份到函数中使用
写法
1 |
|
这个方法是将数组的地址放入函数中定义的数组中,然后使用里面的元素
3.2 将数组的地址传入函数中使用
这种方式是将数组的地址传入函数中提供,这种方式需要的内存小,推荐使用
1 |
|
这种方法相对于只是把数组的地址传入到函数中使用,没有额外创建新的空间。
3.3 数组指针来使用
这个需要等到C语言进阶中才会学到
八、函数
在C语言中,有一个或多个函数,我们最熟悉的main()就是一个函数。
函数的作用是将需要反复使用的代码包含在一起,使我们使用更方便。
1.函数的定义
1 | return_type def_name (value1, value2,...){ |
return_type:返回值的类型
def_name:函数名
value1:形参,如果有多个用英文逗号分开
body of the function:函数体,函数中执行的内容
value:返回值
1.1 返回指针类型的函数
1 | return_type* def_name(value1, value2,...){ |
value1:为返回的地址
2.函数的使用
2.1 有返回值的函数的使用
1 | type variable = def_name(user_value1,user_value2,...); |
type:数据类型
variable:变量
user_value1:传入的参数,如果有多个用英文逗号隔开
2.2 无返回值的函数的使用
1 | def_name(user_value1,user_value2,...); |
variable:变量
user_value1:传入的参数,如果有多个用英文逗号隔开
3.函数的形参和实参
函数的形参可以传入变量和指针,而两种方式需要的内存是不一样的
3.1 函数的形参传入的是变量
这个很容易理解,就是在调用的时候传入的是变量的形式,如下代码:
1 | void test(int a, int b){ |
这个代码是把a和b的变量传入到函数test中,然后在test中将传入进来的a,b拷贝下来,这样的话需要更多的空间给a,b,非常的消耗空间。
3.2 函数的形参传入的是指针
这个是给函数传入指针变量,如下代码:
1 | void test(int* a, int* b){ |
这个是直接把地址传入到函数里,在函数里只使用传入进去的地址,这样对内存的使用比较小,推荐使用。
4.函数的返回值
我们在使用函数的时候,有些时候是需要返回一些特定的值的,这个时候我们就需要让函数有返回值。
1 | return 返回的值; |
返回的值一定要和对应的类型相同,这样是规范的写法,如果不同,只要储存的大小合适则还是能运行的。
返回的值也可以是指针类型的
九、指针
1.什么是指针
指针是编程语言中的一个对象,是将内存的地址赋值给指针变量。
它的值直接指向存在电脑存储器中另一个储存单元
1 | 指针其实是地址 |
- 指针是用于存放地址的,地址是唯一表示一块地址空间的
- 指针的大小在32位平台是4个字节,在64位平台上是8个字节
指针类型决定了指针进行解引用操作的时候,能够访问空间的大小
int *p
能够访问4个字节
char *p
能够访问1个字节
double *p
能够访问8个字节
指针类型决定了:指针走一步走多远(指针的步长)
int *p; --> 4
char *p; --> 1
double *p; --> 8
总结:指针的类型决定了指针向前或向后走一步有多大(距离)
2.指针的使用
(1)指针的定义
1 | (指针类型) *(指针名); |
(2)指针的赋值
1 | 指针名 = &变量名; |
(3)指针类型
1 | int *ip; /* 一个整型的指针 */ |
1 | int* 访问4个字节 |
(4)如何使用指针
3.野指针
概念:野指针就是指向的位置不可知的
(1)导致野指针的原因
① 未初始化指针
1 |
|
②指针越界访问
1 |
|
③指针指向的空间释放
(2)如何避免野指针
①指针初始化
②小心指针越界
③指针指向空间释放即用null占位
④指针使用之前检查有效性
1 | int main(){ |
4.指针运算
- 指针+-整数
- 指针-指针
- 指针的关系运算
指针+-整数
1 | int main(){ |
指针-指针
1 | int my_strlen(char *str){ |
指针-指针可以得到这个数组的长度 - 1
5.指针和数组
1 | int main(){ |
总结:数组名表示的是数组首元素的地址
注意:数组可以使用[]来修饰,但是指针使用[]来修饰
6.指针的关系运算
(1)指针数组 && 数组指针
1 | //指针数组 - 数组 - 存放指针的数组 |
7.二级指针
(1)定义
1 | int main(){ |
(2)使用
1 | int main(){ |
(3)总结
1 | p = &a; |
这句话的意思是将a的地址给p,然后*p得到的是a中的值。
1 | ppa = &pa; |
ppa存放的是pa指针中的地址,然后**ppa是pa
8.const修饰指针(一级指针)
通常,我们的指针变量是可以随便使用的,但如果我们想让指针变量或者解引用的指针的内容不改变,那我们就需要使用 const
来修饰指针。
8.1 const 在 * 左边
1 | int const * p; |
const 在* 左边是修饰 *p的,通过这样修饰后,*p就不能再重新赋值了,*p的值是被固定了,但是指针变量p中的地址是可以重新赋值的。
8.2 const 在 * 右边
1 | int* const p; |
const 在* 右边是修饰 p的,通过这样修饰后,p就不能再重新赋值了,\p的值是被固定了,但是解引用*p中的值是可以重新赋值的。
9.const修饰指针(二级指针)
在二级指针中有三个位置可以加const修饰符
9.1 在**的左边
1 | const int* *p; |
const在**的左边是修饰**p的,但*p和p的值是可以改变的
9.2 在**中间
1 | int* const *p; |
const在**的中间是修饰*p的,但**p和p的值是可以改变的
9.3 在**的右边
1 | int* *const p; |
const在**的右边是修饰p的,但**p和*p的值是可以改变的
9.4 通过二级指针修改被const修饰的一级指针
我们回顾上面讲const修饰符中,别const修饰的变量中的值能被一级指针所修改,那如果别const修改的一级指针能否别二级指针修改呢。
我们来试试:
我们有以下的代码:
1 | int main(){ |
在这个语句中,const是修饰指针变量p的,所以我们无法对p变量进行修改。(如下图)
但是如果我们通过二级指针来间接修改呢?
如下代码:
1 | int main(){ |
我们声明了一个二级指针,然后给这个二级指针赋值一级指针的地址,然后我们对二级指针变量重新赋n的地址值,然后输出*p里面的内容。
然后运行的结果:
我们可以看到,我们输出*p的结果已经改变,所以可以通过二级指针来修改被const修饰的一级指针的值。但是,这个方法只能在vs中才能运行成功,在dev-C++中就不能成功。如下图:
可以看到这个直接就报错了,无法通过编译。
十、结构体
在C语言中,结构是另一种用户自定义的可用数据类型,它允许您存储不同的数据类型,结构体是属于自定义数据结构。
1.结构体的创建
1.1 第一种方法
1 | struct tag{ |
tag 是结构体标签
type 数据类型
name 类型名字
1.2 第二种方法
1 | struct tag{ |
结尾后的tags 是结构的变量,是全局变量,也可以多指很多结构变量。
1.3 全局结构体和局部结构体的声明
全局结构体的声明是声明在函数外,局部结构体是声明在函数内。
2.结构体的使用
2.1 局部结构体的声明 & 初始化
1 | struct tag new_name = {value1, value2, {value3, value4}, .....}; |
new_name
声明的新名字
value
声明的值,如果有多个值,需要使用{}括起来
value前面要有类型
2.1.1 指针方法
指针声明方法必须先对结构体进行声明,然后才能用指针声明
例一:
1 |
|
struct Books book = {1, {4, 3}, {‘g’, ‘a’}};
struct Books* pb = &book;
这个就是结构体的声明指针方法
2.2 全局结构体全局的使用 & 初始化
和局部结构体的声明和初始化一样
3.结构体的输出
3.1 使用结构体变量的输出
1 | 结构体声明的变量 . 结构体中的值 |
使用如下
1 |
|
3.2 使用指针变量的输出
3.2.1 第一种方法
1 | 指针变量 -> 结构体中的值 |
使用如下
1 |
|
3.2.2第二种方法
1 | (*指针变量) . 结构体中的值 |
使用如下
1 |
|
4.结构体做函数的参数 & 结构体指针做函数的参数
4.1结构体变量做参数
1 |
|
4.2结构体指针做参数
1 |
|
4.3 两种定义在内存中的模式
4.1的方式是在函数中将变量又重新拷贝一份,占用更多的空间,而且用完就会被释放,不能修改结构体内的值;而4.2的方法是将结构体的地址放入函数里,函数操作地址,内存的占用非常的少,能直接修改函数体内的值。
十一、debug和release方法
1.debug
debug方法是提供给程序员调试的方法。调试时会生成很多配置文件。不会做任何优化
2.release
release是提供用户使用的
十二、文件操作
在C语言中可以很简单的打开文件进行一些操作,这里主要是在Windows环境下使用标准输入输出库函数的对文件操作。
1.文件打开
对文件操作的基础是将文件打开,如果不打开文件就没办法对文件中的内容进行操作。
文件打开的方法:
1 | FILE* fopen(const char * __restrict__ _Filename,const char * __restrict__ _Mode) |
第一个参数是你需要打开文件的路径,这里的路径可以是绝对路径或者是相对路径。
第二个参数是打开文件的方式,有下面几种方式
模式 | 说明 |
---|---|
r | 只读方式 |
w | 写入文件,如果文件不存在会创建出一个文件,如果文件中有内容会删除里面的内容 |
a | 追加模式,如果文件不存在会创建出一个文件,如果里面有数据就往后写入 |
r+ | 允许读写文件 |
w+ | 允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件 |
a+ | 允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 |
例如我打开一个文件:
1 | FILE* fd = NULL; // 创建一个FILE类型的指针 |
上面的代码就是打开一个文件,以追加的方式进行打开的。
2.关闭文件
当我们打开文件后进行操作后是需要关闭,不关闭文件会导致文件一直处于一种打开状态,所以需要关闭文件,关闭文件的函数如下:
1 | int fclose( FILE *fp ); |
参数是刚才打开的文件,如果关闭文件出问题,这个函数会返回EOF
,但是基本上关闭都是会成功的。
1 | fclose(fd); |
3.写入内容
打开文件后可以向文件中写入内容,写入的函数如下:
1 | int fputc( int c, FILE *fp ); |
这个函数是可以向文件中光标位置处写入一个字符,比如说在文件中写入一个a字符,那代码如下:
1 | fputc('a', fd); |
如果要写入字符串也是可以的,使用下面的函数就可以写入一个字符串:
1 | int fputs( const char *s, FILE *fp ); |
比如说我要写入一个”hello”,那语法如下:
1 | fputs("hello", fd); |
4.读取内容
使用的函数是:
1 | int fgetc( FILE * fp ); |
这个函数可以读取文件中光标后的一个字符,读取后的内容是以返回值的方式进行输出,但是输出不了中文,它返回的只能是ASCII。
如果想读取中文,需要使用读取字符串的函数,函数如下:
1 | char *fgets( char *buf, int n, FILE *fp ); |
这个函数是可以读取字符串,返回的就是你读取的字符串。
第一个参数是字符串存放的位置,需要定义一个数组进行接收。
第二个参数是读取字符串的个数。
第三个参数是文件指针。
比如说我读取一个文件中的内容:
1 |
|
5.设置光标位置
上面一直提到光标,在文件中光标的位置是非常重要的,就如同我们向一个txt
文件中写入内容是需要设置光标位置的,光标的位置是你输入字符进入的位置。
设置光标位置的函数是:
1 | int fseek(FILE * fp, long offset, int whence); |
其中最后的一个参数是需要使用宏定义的,宏定义如下:
SEEK_SET
:设置光标在开头位置
SEEK_CUR
:设置光标在指定位置
SEEK_END
:设置光标在最后的位置
最后这个设置光标在指定位置第二个参数才有效。