C语言入门(全)
C语言入门
作者: 软风XFFer~
-网易云课堂
Content
-
c/c++起源、市场需求、开发环境
-
数据类型 、运算符、表达式的介绍
-
常量和变量、整型、实型、字符型
-
算数型运算符和表达式
-
赋值型运算符和逗号运算符
-
-
程序的基本结构和语句
-
C语言的语句和程序的基本结构
-
数据的输出与数据的输入
-
-
逻辑运算和判断选择
-
关系运算符、关系表达式、逻辑运算符、逻辑表达式
-
if语句详解
-
条件运算符和switch语句
-
-
循环控制
-
概述、goto、while、do while语句精解
-
for语句精解
-
循环的嵌套、比较、break语句、continue语句
-
-
数组
-
一维数组
-
二维数组
-
字符数组
-
-
函数
-
函数的基本概念和定义
-
函数调用方式及嵌套调用
-
函数递归调用精彩演绎
-
数组作为函数参数
-
局部变量和全局变量
-
变量的存储和引用、内部和外部函数
-
-
编译预处理
-
宏定义
-
文件包含和条件编译
-
-
指针
-
指针基本概念详解
-
变量的指针和指向变量的指针变量
-
数组的指针和指向数组的指针变量
-
字符串的指针和指向字符串的指针变量
-
函数指针和返回指针值的函数
-
指针数组、指针的指针、main函数参数、小结
-
-
结构体与共用体
-
结构体变量定义、引用、初始化
-
结构体数组、结构体指针
-
共用体、枚举类型、typedef
-
-
位运算
-
位的概念以及位运算符介绍
-
位运算的具体应用
-
-
文件
-
文件概述、文本、二进制文件区别
-
文件的开、关、读、写、实战操练
-
将结构体写入二进制文件再读出
-
-
课程总结与未来展望
一 数据类型、运算符、表达式的介绍
1、常量和变量、整型、实型、字符型
-
新建项目
-
一个解决方案下可包含多个项目
运行快捷键 ctrl+fn+F5
添加debug断点快捷键 fn+F9
在debug模式下运行下一步fn+F10
换行语句\n
单行注释//
多行注释/* */
C语言每行语句的末尾都要有;
2、数据类型
字节越大表达范围越大(标注的前为32位后为64位)
-
基本类型
-
数值类型
-
整型
-
短整型
short
2/2 -
整型
int
4/4 -
长整型
long
4/8
-
-
浮点型
-
单精度型
float
4/4 -
双精度型
double
8/8
-
-
-
字符类型
char
1/1
-
-
构造类型
-
数组
-
结构体
struct
-
共用体
union
-
枚举类型
enum
-
-
指针类型
-
空类型
void
3、常量
在程序运行中其值不变的量
150; //整型变量
12.3; //实型变量(浮点型变量)
'a'; //字符常量
4、变量
组成:
标志符:由字母,数字,下划线组成,并且第一个字符为字母或下划线。
保留字:系统保留起来有特殊用途,不能作标识符使用。
变量名是标识符,变量名区分大小写
5、整型数据
1)整型常量
123;
-456; //(十进制)
012; //(八进制,以0开头的数字)
0x123; //(十六进制,以0x开头)
2)整型变量
-
基本型
int
(4字节) -
短整型
short int
short
(2字节) -
长整型
long int
long
-
无符号型
unsigned int
unsigned short
unsigned long
只能保存不带符号的数字,不能存负数 范围大一倍
sizeof
确定一个类型变量在内存中占多少字节。
printf("abc这个变量占的字节数是%d\n",sizeof(abc));
6、实型数据(浮点型数据)
1)实型常量
2.9; //十进制浮点数
168E2;
168E-2; //指数形式浮点数;在内存中实型数据以指数形式存储,E前/后 分开存储
2)实型变量
-
单精度变量
float
一般提供7位有效数字 -
双精度变量
double
一般提供15-16位有效数字
printf中%f
用来输出10进制的浮点数,%.20f
表示保留二十位小数的浮点数。
7、字符型数据(一个字节)
1)常规字符:用单引号引起来的一个字符
2)特殊字符转义字符
:以\开头的字符序列
-
\f
换页 -
\n
换行 -
\\
反斜杠 -
\'
单引号字符 -
\"
双引号字符
3)字符变量:只能保存一个字符 char
因为一个字符型变量在内存中只占一个字节
char c1,c2,c3;
c1 = 'a'; //事实上是把字符对应的ascii码保存在了内存中
c2 = 'b';
c3 = '\"';
字符数据即能以字符形式输出,也能以ascii表对应的整数形式输出
printf
中%c
用来输出字符型数据
8、字符型常量 (两个字节)
用
""
引起来的一堆字符
c1 = 'a';
c2 = "a";
在内存中
a
字符c1(1个字节)
a``\0
字符串c2(2个字节)
\0
字符串结束标记,在printf
时并不输出。
9、变量赋初值
int a, b, c = 6; //只给c赋值,c语言中变量要先定义后使用
10、数值型数据的混合运算
系统中不同数据类型运算时,系统会尝试将类型统一。
系统会选取变量中能表达最大数字的变量,作为转化的最大类型。
double <-- float
double <-- long <-- unsigned <-- int <-- char,short
11、C语言的运算符
1)算术运算符和算术表达式
+
,-
,*
,/
,%
(取余,也叫模运算符,该运算符两侧都要求为整数)
两整数相除,系统会舍弃小数部分。
优先级问题:先乘除后加减,优先级相同时,按顺序计算。
2)赋值运算符: =
3)强制类型转换运算符:将一个表达式转换成所需要的类型。强制类型转换得到的是一个所转换到的类型的中间变量。
- 一般形式:
(类型)(表达式)
int a = 10;
double b;
b = (double)a;
//a本身类型并没有变化而是整个结果的类型发生变化赋值给了b
两种类型转换
-
自动类型转换
-
强制类型转换
4)自增和自减操作符 ++
自增 --
自减
i++
相当于i自身加了1,然后结果再赋给i。
用于循环语句中给自己加一减一
int i;
i++; //先用后加
++i; //先加后用
运算优先级
5)赋值运算符和赋值表达式
赋值运算符 =
: 将等号右边的值赋给等号左边的变量,等号左边是个变量(给变量一个值或者改变变量到某一个值)。
//几个概念
char a; //这个叫变量定义,系统会给a分配一个字节的内存,内存里面的值不确定
char a = 90;//这个叫变量定义,同时给变量赋初值(定义时初始化)
//字符的范围是-128到+127
a = 900; //赋值语句。会造成溢出,溢出后a里面的内容就会变得不可预料
赋值的原则:类型要相同,类型不同的用强制类型转换,必须明确知道不会溢出。
复合的赋值运算符(具有从右到左结合性):
在赋值运算符 =
之前加上其他运算符,构成了复合的赋值运算符*(复合的赋值运算符的两个运算符之间必须紧挨着不能用空格分开)*
-
+=
:a += 3
相当于a = a + 3
。 -
*=
:x *= y + 8
相当于x = x * (y + 8)
//如果*=
右侧是个表达式的话,则相当于该表达式有括号。 -
%=
:近似上述。
赋值表达式(复合的赋值表达式)本身也是有值的 —— 表达式的值等于等号右边的值
6)逗号运算符和逗号表达式
逗号运算符:将两个表达式连接起来;它的优先级是最低的!
格式:表达式1,表达式2
求解过程:先求解表达式1的值再求解表达式2,整个表达式的值是表达式2的值
int a;
a = 3*5, a*4 //结果是a=15, 整个表达式的值为60
逗号表达式的扩展形式
表达式1, 表达式2, ... 表达式N
二 程序的基本结构和C语言的语句
不同的字符所占的字节是不同的。
ASCII码:
一个英文字母(不分大小写)占一个字节的空间,一个中文汉字占两个字节的空间。一个二进制数字序列,在计算机中作为一个数字单元,一般为8位二进制数,换算为十进制。最小值0,最大值255。如一个ASCII码就是一个字节。8bit(位)=1byte(字节)。
UTF-8编码:
一个英文字符等于一个字节,一个中文(含繁体)等于三个字节。
Unicode编码:
一个英文等于两个字节,一个中文(含繁体)等于两个字节。
符号:
英文标点占一个字节,中文标点占两个字节。举例:英文句号“.”占1个字节的大小,中文句号“。”占2个字节的大小。
1、语句的分类
-
控制语句:能够控制程序的执行流程,比如在一定的条件下执行某些语句,在另外的条件下,不执行该语句。
-
函数调用语句:由一个函数调用末尾增加个分号构成的语句。
-
表达式语句:由一个表达式构成的语句。
-
空语句:就一个分号。
-
复合语句:用{}括起来的语句。
⚠️ 注意点
-
C语言允许在一行上写几个语句。
-
C语言允许一个语句拆开几行写。(上一行用
\
结尾,进入下一行,这两行组成一个完整行)
printf(“断点停在\
这里”)
1)程序的三种基本结构:
-
顺序结构:先执行A操作再执行B操作;
-
选择结构:条件为真执行A操作,条件为假执行B操作;
if(3 >1)
{
printf(“3 > 1\n”);
}
else
{
printf(“3 < 1\n”);
}
//补充:多分枝选择结构switch
int k = 5;
switch(k)
{
case 1:
{
printf(“icount =1\n”);
}
break;
case 5:
{
printf(“icount = 5\n”);
}
break;
}
-
循环结构:
- 当型循环结构:先判断条件,才决定是否执行A操作,当P条件成立时,就一直反复的执行A操作,知道条件为假才停止循环。
int icount = 5;
while(icount >= 0)
{
printf(“icount = %d\n:,icount);
icount -= 1;
}
-
循环结构
- 直到型循环结构:先执行A操作,再判断条件,条件满足时反复执行操作,知道判断条件P为假停止循环。(至少执行一次A操作)
int icount = 5;
do
{
printf(“icount = %d\n”,icount);
icount -= 1;
}while(icount >= 0);
2、赋值语句的特殊写法
printf(“x=8的值是%d”, x=8);
int a = 3, b = 4;
int x;
if ( (a = b) > 0)
x = 4;
3、数据的输出
putchar(c)
: 向屏幕输出一个字符,只能输出字符。c可以是个字符型变量,也可以是个整形变量。
#include< >
:预编译命令,作用就是将某些文件包含到用户的源文件中,也就相当于把某个文件中的内容原封不动的贴到这个位置。
#include <stdio.h> //<>是去系统目录中找头文件
#include “stdio.h” //“”在当前目录中查找,找不到再到系统中查找。“”用于自己写的头文件,让系统优先使用当前目录中定义的头文件
stdio.h
//标准的I/O库;stdio.h
叫头文件(head)
printf()
函数:向屏幕输出若干任意类型的数据。
格式:printf(格式控制,输出表列);
**格式控制:**用双引号扩起来的字符串。
格式字符:
-
%d
:以十进制数形式输出一个数字; -
%o
:以八进制数形式输出一个数字; -
%x
:以十六进制数形式输出一个数字; -
%c
:以字符形式输出一个数字; -
%f
:以浮点数形式输出一个数字; -
%u
:以十进制数形式输出一个unsigned型(正数)数据; -
%s
:输出一个字符串;(string)
printf
中显示%
-
printf("5%%");
-
printf("5%c",'%');
-
printf("5%s","%");
4、数据的输入:从键盘上输入数据。
getchar()
:执行后等待用户从键盘上输入一个字符,并按回车后程序继续执行。
scanf
函数,这是个格式化输入函数,用来输入任何类型的多个数据;
格式:
scanf(格式控制,地址表列);
当输入回车的时候表示输入结束。
int a, b, c;
scanf_s(“%d%d%d”,&a,&b,&c); //&是地址运算符(表示该变量在内存中的地址),表示键盘输入的数据放在a, b, c的地址中。三个输入的数字 之间可以用空格,回车,tab分隔,不能用逗号分隔。
printf(“a+b+c=“,a+b+c);
三 逻辑运算和判断选择
1、关系运算符,关系表达式,逻辑运算符,逻辑表达式
1)关系运算符和关系表达式
关系表达式:用关系运算符将两个表达式连接起来的式子就叫关系表达式。
关系表达式的值是一个逻辑值,“True(1)”和“False(0)”(布尔值),关系表达式的值是0或者1,也可以认为是True或者False。
2)逻辑运算符和逻辑表达式
用逻辑运算符 将 关系表达式 连接起来的 就是逻辑表达式。
双目运算符:要求有两个运算量,分别位于符号两侧。
比如(a > b) && (c > d);
(a > b) || (c > d);
!非
是一个单目运算符。比如:! (a > 3);
a && b
: 若a和b都为真,则输出为True(1);
a || b
: 若a和b有一个为真,则输出为真;
计算机在进行逻辑判断时,不等于0的值全部认为True
总结来说:由系统给出的逻辑运算结果不是0就是1,不可能是其他数值♂️,而在逻辑表达式中作为参加逻辑运算的运算对象,是0就表示假,非0就表示真。
if(4 == true) //假;因为计算机默认true为1
if(4 && 5) //真,式中4和5都属于逻辑表达式中的运算对象且都不等于0。
逻辑运算符求值问题:在逻辑表达式求解中,不是所有逻辑运算符都会被执行,只有在必须执行下一个逻辑运算符才能求出表达式的结果时,才执行该运算符。
a && b && c //只有a为真(非0)才需要判断b,只有a,b都为真才判断c。
a || b || c //只要a为真就不需要判断b和c,只有a,b都不为真才会判断c。
2、if语句详解
1)if常用的三种结构
if语句(选择结构的代表性语句):用来判断给定的条件是否满足,根据判断的结果(真,假)决定执行给出的两种操作之一。
if 的三种格式:
-
if (表达式) 语句
如果表达式中的条件满足则执行该语句。
⚠️千万不要在if (条件)后加冒号或分号(和PYTHON有差别),加上后会破坏掉选择关系。
-
if (表达式) 语句1 else 语句2
如果表达式中的条件满足,则执行语句1;否则执行语句2。
if (表达式1) 语句1 // 如果表达式1成立执行语句1...只能执行一个语句!
else if (表达式2) 语句2
else if (表达式3) 语句3
...
else (表达式n) 语句n
2)if语句的嵌套
if ()
if() 语句1
else 语句2
else
if() 语句3
else 语句4
3、条件运算符和switch语句
1)条件运算符 ? :
if (a>b)
max = a;
else //=> max = (a>b)?a:b //如果a>b为真则取a作为整个值
max = b;
条件运算符是一个三目运算符是唯一一个三目运算符,结合顺序是从右到左的。
格式:表达式1 ? 表达式2 :表达式3;
执行过程:如果表达式1输出为真 则值为运行表达式2的值,否则取表达式3的值。
2)switch语句
格式:
switch(表达式) //这个位置不能加分号或者冒号
{
case 常量表达式1:
{
1行或者多行语句
}
break;
case 常量表达式2:
{
1行或者多行语句
}
break;
...
default:
1行或者多行语句
break;
} //switch 后面的表达式的值若满足任何某个case后面的常量表达式的值,则执行该case后面的一行或者多行语句,直到遇到break跳出整个switch语句;如果所有case都不满足,则执行default中的语句。
说明⚠️:
-
switch后面的表达式一般都是整形变量或者表达式,其他类型也允许,但是很罕见;
-
每个case后面的常量表达式必须互不相同;
-
break不能丢掉;如果忘记break,会导致执行完一个case中包含的语句后,流程继续会执行下一个case条件中包含的语句(不管该case中条件是否满足);
-
case后可以有多行语句不用加{};
-
default可以没有;
-
多个case可以共用一组执行语句;
四 循环控制
1、概述,goto,while,do while语句精解
循环控制语句概述
循环结构♻️:goto
,while
,do while
,for
1)goto语句
goto语句: 无条件转向语句,用来跳转到某个程序位置进行执行。
一般形式:goto 语句标号;
其中语句标号是一个标志符(只能由数字,字母,下划线三种字符组成,且第一个字符必须是字母或者下划线,且不能是保留字。
goto语句主要用途:
-
与if语句一起构成循环结构;
-
从循环体内跳转到循环体外。//不推荐,破坏了结构化程序设计原则,除非万不得已。
//1到100的加法运算
loop:
if(i<=100)
{
sum = sum + i;
i++;
goto loop;
}
printf(“%d\n”,sum);
目前goto语句应用场合比较少;goto语句不能跨函数。
2)while语句,当型循环结构
格式:while (表达式) 要执行的语句
特点:先判断表达式的值,如果表达式的值为真,就执行语句;如果为假,则循环体内的语句一次也不执行。
⚠️在循环体内需要有使循环趋向于结束的语句;
3)do while语句,直到型循环结构
一般形式:do 要执行的语句 while(表达式)
2、for语句精解 和python中的有差别
1)for语句的一般形式
for(表达式1; 表达式2; 表达式3) 内嵌的语句
执行过程:
-
先求解表达式1的值;
-
再求解表达式2的值;
-
若表达式2值为真(非0),则执行for语句中指定的内嵌语句,再求解表达式3的值,反复循环步骤2,一直到表达式2的值为假;
-
若表达式2的值为假(0),则整个for循环结束,程序流程跳转到for语句后面的语句去执行。
-
⚠️表达式1的值只会被执行一次
for语句最常用也是最简单的引用形式:
for(循环变量赋初值; 循环变量结束条件; 循环变量增加值) 内嵌的语句
int i, sum=0;
for(i=0; i<=100; i++) //称i为循环变量
{
sum = sum + i;
} //先执行i=0;当i<=100为真时执行sum = sum+ i 和 i++;
⚠️
-
表达式1可以省略,但
;
不能省略,当表达式1省略时,应该在for之前就给循环变量赋于初值; -
表达式2可以省略,但
;
不能省略,也就是不判断循环条件,就必须用break语句终止循环;
for(i=1; ; i++)
{
sum = sum + i;
if (i >= 100)
break
-
表达式3也可以省略,把表达式3也写入循环体内;
-
表达式1和表达式3可以是一般表达式或者逗号表达式;
-
表达式2可以式关系表达式或者逻辑表达式;如:
i<=100;
i>100 && i <=500;
…
如果表达式全省略,则默认为真 => 一直执行内嵌语句。
3、循环的嵌套,比较,break语句,continue语句
1)循环的嵌套 while,for,do while
一个循环体内又包含另外一个循环;for套for最常用
2)几种循环语句的比较
-
多数情况下,这些循环之间可以相互替代,但是不提倡使用goto循环,因为goto循环破坏了结构化程序设计。
-
while循环和for循环,是先判断表达式的值,后执行语句;而do while循环先执行语句,后判断表达式的值。
-
对while,do while,for这三种语句可以用break语句跳出循环,用continue结束本次循环。
3)break和continue语句
break
:跳出整个循环;
continue
:跳出本次循环;继续进行下次循环;
⚠️break语句不能在switch和循环语句之外使用;在switch语句中使用break只能跳出switch不能跳出switch之外的循环;break只能跳出语句所在的这层循环;
continue的作用:结束本次循环,跳过循环体中下面尚未执行的语句,接着进行下一次是否执行循环的判断。
五 数组
1、一维数组
构造类型:把基本的数据类型(int,float,char,double)进行一些变换得到的数据类型。
数组就是构造类型。
一维数组的定义方式:
类型说明 数组名字 [常量表达式];
like int a[10]; //定义了一个一维数组,名字a,这个数组有10个元素;
-
数组名后边的必须是方括号扩起来;一般常量表达式都是数字,不能是变量,c语言不允许对数组大小做动态定义,也就是说数组大小不能依赖于程序运行;
-
所谓一维数组,就是带一组[ ],二维数组是[ ] [ ];
-
十个元素是a[0], a[1], a[2], … , a[9];
可以给a[10]赋值,语法上没错,但这个赋值就会产生极大的程序隐患,因为a[10]的内存并不属于你能控制的内存,但是,却在这个内存地址写入了数据,结果很有可能把程序中其他某个用到的这个内存地址的变量的值给覆盖掉。
一维数组的引用:
C语言规定,只能引用数组中的元素,而不能引用整个数组;
表现形式: 数组名 [下标]
一维数组的初始化:
int a[10] = {...}; //正好10个数字,用大括号括起来,每个数字之间用 “,” 分开。
可以给部分元素赋值;未赋值元素默认为0;
如果对全部数组元素赋初值,可以不指定数组长度。
int a[] = {1,2,3,4,5};
2、二维数组
二维数组的一般形式:
类型说明符 数组名[常量表达式][常量表达式]
float a[3][4]; //理解成一个3行4列的数组;
//第二种理解方式:理解为a是一个一维数组有三个元素,每个元素都是一个包含四个元素的一维数组
在内存中多维数组排列顺序:第一维下标变化最慢,越靠后的维度下标变化快。
二维数组的引用:
数组名[下标][下标]
每个元素都可以当作一个单独变量来看,使用算数运算符。
二维数组的初始化:
- 分行给二维数组赋初值
int a[2][2] = {{1,2},{3,4}};
快捷键
结束调试:shift+fn+F5
快速监视:shift+fn+F9
注释:ctrl+K
+ctrl+C
- 将所有数据放在一个大括号里
int b[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};
- 给部分元素赋初值
未给初值默认为0
3、字符数组*
用来存放字符数据的数组,一个元素存放一个字符。
char c[10];
c[0] = 'I';
c[1] = 'a';
//...
//字符数据的初始化
char c[10] = {'I',' ','a','m',' ','h','a','p','p','y'};
//如果提供的初值个数和预定的数组长度相同,定义时可以省略数组长度,系统会自动根据初值个数确定数组长度
char c[12] = {'I',' ','a','m',' ','h','a','p','p','y'};
//初值个数 < 数组长度,则只将这些字符赋值给数组中前面的元素,其余的元素值可能给\0,也可能无法确定。
char d[] = {"I am happy"}; //常用,同样可以用index取到数组中的元素
char d[] = "I am happy";
//用字符串的方式给字符数组赋初值,得到的结果最后一位会多一位 \0 是字符串的结束标记
printf("%s",d) //printf输出string,到第一个\0结束
//0 == '\0' 字符串结束标记
从键盘输入一个字符串,用scanf
完成
char str[100];
scanf("%s", str); //并没有使用&str的原因是str本身就代表该数组的起始地址
printf("%s\n", str); //输入一个字符串的时候不能加入空格,否侧空格后的内容会被丢弃
printf("%d\n", &str); //可以以十进制数打印出数组的起始地址(实际上是0x开头的十六进制数)
字符串的处理函数
引入string.h
#include <string.h>
-
puts(字符串数组)
:将一个字符串输出到屏幕,会自动换行; -
gets(字符数组名)
:从键盘⌨️输入一个字符串到字符数组中; -
strcat(字符数组1, 字符数组2)
:把2连接到1的后面保存到str1; -
strcpy(字符数组, 字符串)
:用于字符数组赋值。使用字符串覆盖掉左边字符数组的值(字符串也可以是一个字符数组,右边数组大小最好小于左边); -
strcmp(字符串1,字符串2)
:比较两个字符串的内容:-
如果字符串1 = 字符串2,返回0;
-
如果字符串1 > 字符串2,返回正整数1;
-
如果字符串1 < 字符串2,返回负整数-1。(比较规则:从左到右逐个字符比较(按ascii码值大小比较),一般只用来比较和。)
-
-
strlen(字符串)
:测试字符串长度,不包含\0
,返回值是字节数(sizeof
操作符与内容无关,例如: sizeof(int)=4)
六 函数
1、函数的基本概念和定义
调用一个函数前需要先声明(定义)
主函数main前加的int是函数返回值类型,就像void无返回类型(无返回类型void
,函数没有返回值)一样,在vscode编译环境下如果不加int,默认的是系统自动假设整型的返回值类型。但是建议使用int main():
函数末尾使用return 0
。
-
一个项目由一个或多个源程序文件组成。函数可以分别放在这些源程序文件里并被所有的源程序文件公用;
-
函数不能嵌套,但是可以互相调用;
嵌套的意思是:
void function1():
{
void function2():
{
}
}
调用的意思是:
void function1():
{
function2();
}
函数的分类:
-
库函数,比如
printf
,strcpy
,strcat
,strcmp
。可以直接调用,不需要自己定义,需要用#include <>
引入头文件; -
自定义函数,用于解决特殊需求。
函数参数:调用函数时,希望把一些数据传递给参数,该函数需要一些变量来接收这些数据,这些接受数据的变量,就叫做函数参数。
函数定义的一般形式:
返回类型 函数名(形式参数列表) //函数中的参数叫形参,调用时输入的参数叫实际参数,实参
{
语句;
return 返回值;
}
快捷键
debug时查看函数调用步骤(进入调用堆栈) fn+F11
(如果使用fn+F10
会跳过实际在函数中运行的步骤直接跳到结果)
使用键盘向左向右*选择shift+⬅️/➡️
⚠️
-
函数定义的第一行没有分号,有分号的话叫函数声明;
-
形式参数在函数调用之前并不分配内存,调用的时候分配内存;调用结束后,内存被释放。在函数外,不可以使用形参。也就是函数调用时的压栈和出栈;
-
实际参数可以是常量、变量、表达式,实参的值自动赋给形参。如果实参为数组名(数组名代表的是数组首地址),则传递进去的不是变量而是数组首地址;
-
形参数量和类型必须和实参数量和类型保持一致;
-
实参变量对形参变量的数据传递是“值传递”,是单向传递。
2、函数调用方式及嵌套调用
函数调用的一般形式:
//一般形式
函数名(实参列表);
//若调用的是没有形参的函数
函数名();
//若实参列表包含多个参数
函数名(?,?,...);
函数调用的方式:
int whichmax(int a, int b)
{
if (a > b)
return a;
return b;
}
- 把函数作为一个语句;
whichmax(12, 60);
- 函数出现在表达式中,这种表达式叫做函数表达式,要求函数带回一个确定的值以参加表达式运算;
result = whichmax(12, 60) * 100;
- 函数调用可以作为一个函数的实参;
whichmax(8, whichmax(12,60));
函数声明
函数声明需要放在源代码具体的函数之前(源代码的开头),才能保证这些具体的函数在被调用时是声明过的函数。
函数声明的一般形式:
类型标志符 函数名(形参列表);
和函数定义的区别在大括号及内的语句&分号。
函数的嵌套调用
️C语言中不允许函数的嵌套定义,✅但允许函数的嵌套调用
3、函数递归调用精彩演绎
调用栈,把形式参数、函数调用关系、局部变量*(在函数内部定义的变量,当函数整个执行完后,局部变量所占用的内存会被系统回收,回收后该变量将不再可用)*。
因为递归调用产生的死循环问题,递归操作必须要有一个出口,也叫做递归终止条件。
int dg_jiecheng(int n);
int main()
{
int end = dg_jiecheng(5);
printf("阶乘结果是%d", end);
return 0;
}
int dg_jiecheng(int n)
{
if (n == 1)
{
return 1;
}
else
{
dg_jiecheng = dg_jiecheng(n-1) * n;
}
return dg_jiecheng;
}
递归调用像掰白菜一样,从第一次调用开始 -> … -> 第n次调用开始 -> 第n次调用结束 -> … -> 第一次调用结束,返回值。
4、数组作为函数参数
-
数组元素可以当成变量使用,来作为函数调用的实参
-
数组名作为函数实参
数组名代表的是数组首地址,此时,函数中形参也应该用数组名(也可以是数组指针)。在调试时,使用fn+shift+F9
可以打开快速监视窗口,使用&X(查看的变量)
查找该变量的内存地址。
强调⚠️
数组名作为函数参数时,不是“值传递(形参是分配内存的)”的概念了,不是单向传递,而把实参数组的开始地址传递给了形参数组(不再额外分配地址)。这样两个数组就会共同占有一段内存,这叫做地址传递。也就是说,形参数组中各个元素发生变化会导致实参数组元素的值也发生改变。
形参数组的大小可以不指定或与实参数组大小不一致,C编译器不对形参数组大小不做检查,只是将首地址传递给形参,形参中能够使用的内存地址就是实参中定义的变量地址
5、局部变量和全局变量
-
局部变量:
-
在一个函数内部定义的变量叫局部变量,只在本函数范围内有效(包括main函数)
-
不同函数可以使用相同的变量名,互不干扰
-
虽然地址传递公用一个内存地址,但是仍不可以跨函数使用变量
-
形式参数也是个局部变量
-
一种特殊写法
int main()
{
int a, b;
//用大括号写一段代码段,大括号括起来的叫复合语句,在该复合语句中定义变量,这些变量只在本复合语句中有效,这种复合语句也叫程序块
{
int c; //有效范围只在复合语句内,一旦离开复合语句变量c的内存就被释放
c = a + b;
}
}
-
全局变量:
-
在函数外定义的变量叫全局变量(外部变量),可以为本文件中所有函数共用
-
优点:增加了函数与函数之间的数据联系渠道
-
缺点:
-
只有在必要时才使用全局变量,全局变量在程序运行整个周期都占用内存
-
降低了函数的通用性
-
降低程序清晰性和可读性
-
-
如果某个函数想引用在它后面定义的全局变量,则可以用一个关键字
extern
做一个“外部变量说明”,表示该变量在函数的外部定义
extern int x1, x2; //外部变量说明(不分配内存)
void func1()
{
...;
}
//or
void func1() //在函数内声明,只能在函数内部使用该全局变量
{
extern int x1, x2;
...;
}
在同一个源文件中,如果全局变量和局部变量重名,则在局部变量作用范围内,全局变量不起作用
6、变量的存储和引用,内部和外部函数
变量的存储类别:
从变量存在的时间(生存期)角度来划分,变量可以划分为:静态存储变量和动态存储变量。
静态存储变量:程序运行期间分配固定存储空间的变量。
这种存储变量的方式叫静态存储方式。
动态存储变量:程序运行期间根据需要进行动态分配存储空间的变量。
这种存储变量的方式叫动态存储方式。
全局变量(在函数外部定义的)放在静态存储区中,程序开始执行时给全局变量分配存储区,程序执行完毕后释放这些存储区。在程序执行过程中它们占据固定的存储单元,而不是动态的分配和释放。
函数形参、局部变量、函数调用时的数据和返回地址都存储在动态存储区中,这些数据在函数开始调用时分配存储空间,函数调用完毕,空间释放。如果两次调用同一个函数,分配给此函数局部变量等的存储空间地址可能是不同的。
局部变量的存储方式:
-
传统情形:
- 函数的局部变量:函数被调用时分配存储空间,执行完成后自动释放其所占有的存储空间
-
特殊情形:
-
局部静态变量:用
static
加以说明;能够保留原值,占用的内存单元不释放。下一次调用该函数时,不再重新赋值,该变量的值是上次调用结束时的值-
局部静态变量在静态存储区内分配存储单元,在编译时赋初值
-
如果不赋初值的话,系统默认静态局部变量的初值为0
-
虽然局部静态变量在函数调用结束后仍然存在,但其他函数时不能引用
-
-
全局变量跨文件引用:使用extern
在文件头做外部变量说明。
在定义全局变量时前面加上static
,则该全局变量只能在本文件中使用。
函数的跨文件引用
根据函数能否被其他文件调用,将函数分为:
-
内部函数(静态函数):在函数定义最前面加上
static
,使函数只局限于所在文件 -
外部函数:跨文件调用时,需要在文件头加函数声明
函数返回类型 函数名(形参表);
七 编译预处理
1、宏定义
一个项目可以通过编译、链接最终形成一个可执行文件;每个源文件(.cpp)都会单独编译,编译成一个目标文件(.o,也可能是.obj,扩展名与操作系统相关);然后系统将这些.o文件进行链接,形成一个可执行文件。
编译过程:
1)预处理;
2)编译(->汇编语言):词法、语法分析,目标代码生成、优化,产生一些临时文件
3)汇编(->机器语言):产生.o(.obj)目标文件
C语言一般提供三种预处理功能:
都以
#
开头
-
宏定义
-
文件包含
-
条件编译
不带参数的宏定义
用一个指定的标识符(宏名)来代表一个字符串
一般形式:#define 标识符/宏名 字符串
//用PI代替"3.1415926"这个字符串
#define PI 3.1415926
//#define叫做宏定义命令 字符串替代标志符的过程叫做宏展开
-
宏名一般都用大写字母
-
宏定义不是语句,无需在末尾加分号,若加分号则会一起替换
-
不可以跨文件使用,但是可以通过文件包含一个公共文件,达成跨文件使用
-
undef
命令终止宏定义的作用域
用#define
宏定义时,可以引用已定义的宏
#define DPI 2*PI
#define DPCPI PI*DPI
//字符串内的字符不做替换,如:
char ftmp[10] = "DPI"; //打印出仍是"DPI"
带参数的宏定义
一般形式:#define 宏名(参数表) 字符串
#define S(a, b) a*b
#define S(r) PI * r * r
int main()
{
int tmp = S(2, 3); //把2,3分别代替宏定义中的形式参数a,b => S(2, 3) => 2*3 => 6
//字符串中除参数外的符号保留
}
说明:
如果输入area = S(1+5); //3.1415926 * 1 + 5 * 1 + 5
,这样有别于//3.1415926 * (1 + 5) * (1 + 5)
,故宏定义时#define S(r) PI * (r) * (r)
。
//宏替换复杂语句
#define MAX(x,y) (x)>(y)?(x):(y)
2、文件包含和条件编译
文件包含
将另外一个文件的内容包含到本文件中
一般形式:#include "文件名"
常用于#include ".h"
.h文件称为头文件。一般将宏定义,函数说明,以及全局变量的外部声明等放在头文件中。文件包含可以嵌套。
-
#include " "
先到当前目录查找,如果找不到,再到系统目录查找 -
#include < >
到系统目录中找头文件,不会到当前目录寻找
条件编译
- 当标识符被定义过(#define),则对程序段1进行编译,否则对程序段2进行编译
#ifdef 标识符
//...
#else
//...
#endif
- 当标识符没有被定义,则对程序段1进行编译,否则对程序段2进行编译
#ifndef 标识符
//...
#else
//...
#endif
- 当**指定的表达式值为真(非0)**就编译程序段1,否则对程序段2进行编译
#if 表达式 //表达式的返回值只能是整形或枚举类型
//...
#else
//...
#endif
例
解决了跨平台的问题
#if _WIN32
WaitForSingleObject()
//windows下专有的函数
#else __linux__
epoll()
//linux下专有的函数
#endif
八 指针
1、指针基本概念详解
前提知识
每种类型都占用一定的内存空间,可以使用sizeof()
输出该类型所占的字节数。
int isize = sizeof(int);
printf("isize = %d", isize); //4Byte->32Bit
地址的概念
计算机中用一个数字描述一个地址(0x开头的十六进制数)
直接访问和间接访问
-
按变量地址存取变量值,这个就叫直接访问
-
将变量的地址,存放在另一个内存单元中
-
在C语言中,我们定义了一种特殊的变量,不同于
int
、float
、double
、char
这些存放“值”的变量,而是用来存放“地址”。一般这中变量占4个字节的地址,这种特殊变量就是指针 -
先找到指针的内存地址,从这4个字节的内存中,取出变量的地址,指针也是区分类型的,如保存整形变量地址的指针
-
指针变量专门用来存放另外一个变量的地址,指针变量的值是地址(也被称作指针)
2、变量的指针和指向变量的指针变量
变量的指针就是变量的地址
指向变量的指针变量是存放有变量指针的指针变量
指针变量的定义:
我们可以定义一个指向变量的指针变量,在定义时,引入一个*
表示这是一个指针变量。
一般形式:
类型标识符 *标识符;
标识符就是名字
int i, j;
int *mypoint1, *mypoint2; //指向整形变量
float *mypoint3; //指向实型变量
mypoint1 = &i; //&地址符,mypoint指向普通变量i
//快速监视结果 指针的值:0x开头的十六进制(变量)地址{指向变量的值}
// 指针地址的值:0x开头的十六进制(指针变量)地址{0x开头的十六进制(变量)地址{指向变量的值}}
*
号是在“定义”时,指针变量前使用的,使用指针变量时,是没有*
的
内存用十六进制数**0x_ _ _ _ _ _ _ _**表示的原因
首先,计算机硬件是0/1二进制的,二进制码表示实在太长了,十六进制更加简短。换算时,一位16进制数可以顶替四位2进制数,1111=F。
这也是为什么内存中十六进制数0x后使用8个十六进制数位的原因,字符在内存中占一个字节,ASCII字符集定义一个字节为8bit,8bit用2个16进制就可以表示出来。
和指针变量相关的运算符:
-
&
取地址运算符 -
*
指针运算符(间接访问运算符)-
指针定义时
*
叫做指针运算符 -
除定义和乘号使用外的场合,
*
代表的是这个指针变量所指向的变量
-
printf("%d", i);
//等价于
printf("%d", *mypoint1);
指针变量使用自增操作符
自增操作符+ +
和指针运算符*
是同一优先级
int a = 100;
int *p = &a;
p++;
//p中存放了a的地址,若使用自增操作符后,p中存储的地址内容自加4(因为地址用4个字节存储)
//p++意味着完整跳过4个字节,指向了a之后的变量
指针变量做函数参数
void swap(int *pdest1, int *pdest2)
{
int temp;
temp = *pdest1;
*pdest1 = *pdest2;
*pdest2 = temp;
}
int main()
{
int *p1, *p2, a, b;
a = 5;
b = 6;
p1 = &a;
p2 = &b;
swap(p1, p2);
return 0;
}
3、数组的指针和指向数组的指针变量
数组指针是指数组的开始地址,数组元素的指针就是数组元素的地址,数组名就代表数组的首地址,如a == &a[0]
int a[5] = { 5, 6, 7, 8, 9 };
int *p;
p = a;
//等价于
p = &a[0];
关于
p + i
p++; //实际上0x开头的十六进制地址增加了4
//这样现在p指向的变量是a[1]
p = p + i; //i是数组下标,假如目前p指向数组首地址,那么p+i和a+i就是数组元素a[i]的地址,意味着指向了数组第i个元素
*p[i]
等价于*(p+i)
等价于a[i]
等价于*(a+i)
,四者都代表数组元素
int i, a[5] = { 5, 6, 7, 8, 9 };
int *p = a; //p指向a[0]
for (i = 0; i < 5; i++)
{
printf("%d\n", *(a+i));
}
for (p = a; p < (a + 5); p++)
{
printf("%d\n", *p); //实际上这种方式更高效,系统可以直接通过地址,取出变量值
}
printf("%d\n", *(p++)); //先用后加,打印输出a[0],p指向a[1]
*++p; //先加后用
用指针做形参接收数组地址
void changevalue(int *p)
{
*(p+2) = 100;
}
int main()
{
int a[5];
changevalue(a); //简写了这个例子
//若实参也为指针
{
int *pa = a;
changevalue(pa);
}
return 0;
}
指向多维数组的指针
以二维数组为例,多维数组的指针很像是掰白菜,数组名a代表数组首地址,也就是a[0][0]的地址,a[0]代表数组第一行首地址,*(a+i)不同于一维数组(代表i元素的值)而是代表第i行的首地址。就像是一层地址包含了另一层地址,在层层地址最内部是元素的值
int a[3][4];
int *p = a;
二维数组中,a[0]、a[1]…a[i]是数组第i行的首地址。
a[0] == &a[0][0]
、a[1] == &a[1][0]
…
运算先后次序,括号内 > 指针标识符
一些推论:
-
&a[0][i]
等价于a[0] + i
等价于*a + i
(其实也是*(a + 0) + i
) -
&a[i][j]
等价于*(a + i) + j
指针数组和数组指针
指针数组
int *p[10];
这是一个数组,包含十个元素,每个元素都是一个指针,相当于定义了十个指针变量p[0]~p[9]。
数组指针
int (*p)[10];
p++; //数组指针使用自增操作符,在内存中一次跳 4*数组长度 个字节
这是一个指针,指向有十个数组元素的一维数组。
例子
int i,j;
int (*p)[10];
int a[3][10];
for (i=0; i<3; i++)
{
for (j=0; j<10; j++)
{
a[i][j] = i + j;
}
}
p = a;
int *q;
q = (int *)p;
for (i=0; i<3; i++)
{
//1. q = *(p + i);
for (j=0; j<10; j++)
{
printf("%d ", *q);
//printf("%d ", *(*(p+i)+j));
q++;
}
printf("/n-----------------/n");
//2. p++;
}
4、字符串的指针和指向字符串的指针变量
char mystr1[] = "Do the thing I want";
char mystr2[] = "Do the thing I want";
//这里的mystr1和mystr2存放相同的字符串,但是内存地址不同
char *pmystr1 = "Do the thing I want";
char *pmystr2 = "Do the thing I want";
//这里的两个字符指针*pmystr1和*pmystr2内存地址相同,实际上将字符串存放在了一个字符串常量中,等于说将该字符串常量的首地址赋值给了指针
char mystr3[100];
int i;
for (i = 0; *(mystr1 + i) != '\0'; i++)
{
*(mystr3 + i) = *(mystr1 + i);
}
*(mystr3 + i) = '\0';
/*
char *p1, *p2;
p1 = mystr1;
p2 = mystr3;
for (; *p1 != '\0'; p1++, p2++)
{
*p2 = *p1;
}
*p2 = '\0';
*/
字符指针变量的值可以改变
char *a = "I Love China!";
a = a + 7; //字符指针,+i表示跳过i个字节
printf("%s\n", a); //结果是China
5、函数指针和返回指针值的函数
一个函数在编译时,系统会给这个函数分配一个入口地址,成为函数的指针
例子
int max(int x, int y)
{
if (x>y)
return x;
return y;
}
int main()
{
int (*p)(int x, int y);
/*
*p两侧的括号不能省略,表示*和p先结合代表一个指针变量
和后面的()结合起来表示此指针变量指向函数
可以简写为int (*p)(int, int);
不能写成int *p(int x, int y);这是一个函数声明,这里的int *表示这个函数的返回值是指向整形变量的指针
*/
p = max; //将函数max的入口地址赋给指针变量p,函数名代表函数的入口地址
c = (*p)(9, 13); //调用*p就是调用函数max,p是函数max的入口地址
}
函数指针变量定义的一般形式:
数据类型标识符 (*指针变量名)(形参列表);
数据类型标识符就是函数的返回值类型,形参列表内可以只有类型说明符,多个类型说明符之间用逗号分隔。
把指向函数的指针变量作为函数参数
指向函数的指针变量也可以作为另一个函数的参数,从而实现函数地址的传递,也就是这另一个函数中调用该函数指针所指向的函数
int Func(int x, int y, int (*p)(int a, int b))
{
int result = (*p)(x, y);
}
返回指针值的函数的一般形式
返回类型标识符 *函数名(形参列表);
如返回一个整形变量的指针int *
返回值为指针时,接收返回值的变量也该定义为指针。需要明确注意,避免返回形参,因为形参的内存分配是动态的。
6、指针数组、指针的指针、main函数参数、小结
重新探究指针数组的内存
char *p[] = {"C++", "PYTHON"};
printf("%s\n", p[1]); //结果是PYTHON
printf("%s\n", p[1] + 1); //结果是YTHON
//表明对于一个字符指针来说,内存+1跳过1个字节。一个指针变量占四个字节
指向指针的指针
char **p
这是一个指向字符指针变量的指针变量
int **p
这是一个指向整形指针变量的指针变量
那么,*p
代表指向的这个指针的内容(是变量的地址),**p
就是这个变量的值
指针数组做main函数形参
int main(int argc, char *argv[])
//argc存储的是argv指针数组的元素个数
//argv[0]保存的是当前可执行文件的完整路径文件名
{
...
}
小结
int i
定义整形变量i
int *p
p为指向整形数据的指针变量
int a[n]
定义整形数组a,它有n个元素
int *p[n]
定义指针数组p ,它由n个指向整形数据的指针元素组成
int (*p)[n]
p为指向包含n个元素的一维数组的指针变量
int f()
定义一个函数f,返回整形值
int *p()
函数p,返回一个指针,这个指针指向一个整形数据
int (*p)()
p是一个指向函数的指针
int **p
p是一个指针变量,它指向另一个指向整形数据的指针变量
建议在定义指针时,初始化为NULL
(F12
可以查看到h文件中#define NULL 0)
void *
型指针是万能型指针变量,能够指向任意数据类型
九 结构体与共用体
1、结构体变量定义、引用、初始化
结构体也是一种数据类型,简单说就是将不同类型的数据捏在一起
结构体的定义
struct year
{
int year;
int month;
int day;
};
struct student
{
int num; //学号
char name[100]; //姓名
int sex; //性别 0女 1男
int age; //年龄
char address[100]; //地址
struct year birthday; //结构体的嵌套
}; //切记这个分号
定义结构体类型的一般形式
strct 结构体名
{
成员列表
};
定义结构体类型变量的一般形式
//struct 结构体名 变量名列表;
struct student s1, s2;
-
使用结构体,一般要先定义结构体类型,然后定义某些变量为该类型的变量;
-
结构体内的成员名,可以与程序中的变量名相同,彼此间互不影响。
定义结构体类型实际上是在定义结构体是个什么结构,包含什么成员,而定义结构体类型变量实际上是在定义满足这个结构体形式的可供调用的变量。
结构体类型变量的引用
不能将结构体变量作为一个整体进行引用,只能对结构体变量中的各个成员分别引用
引用的方式:
//结构体变量名.成员名
s1.num = 1001;
.
叫做结构体成员运算符,该运算符优先级最高和()
平级。
如果成员本身又属于一个结构体类型,则要用多个成员运算符,一级一级的找到最低一级成员,只能对最低级成员进行赋值或者存取。
s1.birthday.month = 1;
s1.birthday.day = 25;
定义时初始化需要按照定义结构体类型时的顺序用逗号隔开,依次赋值。
2、结构体数组、结构体指针
结构体数组定义
//struct 结构体名 变量名[n];
取成员变量时,使用变量名[索引].成员名
即可存取。
结构体指针
//struct 结构体名 *指针名;
struct student stu;
struct student *p;
p = &stu;
两种访问结构体成员变量的方法
//1.
(*p).age = 20;
//2.
p->age = 20; //p需要是一个结构体指针
strcpy(p->name, "Feng Yixiao"); //字符必须用strcpy
->
叫做指向结构体成员运算符,优先级也是最高,与.
优先级相同。
如果指向的是一个普通变量,那么
p = &stu
;如果指向的是一个结构体数组,那么p = stu
如果指向结构体数组,指针使用++
自增操作符,则跳到下一个“记录”中,即指向该数组下一个元素的首地址。
向函数中传递参数时,建议传递指针,也就是传递地址,可以加快程序的运行效率。
3、共用体、枚举类型、typedef
共用体
把几种不同类型的变量存放在同一段内存单元(同一个内存地址开始的单元中)
定义形式:
union 共用体名
{
成员列表;
}变量列表;
结构体和共用体的区别sizeof()
-
结构体
- 结构体变量占的内存是各个成员所占的内存之和
-
共用体
-
共用体变量所占的内存是最长的成员长度
-
共用体变量每一个瞬间只能存放其中的一种,在程序赋值时,该成员起作用
-
共用体变量的地址和其成员的地址相同
-
共用体变量不能在定义时初始化
-
共用体变量不能作为函数参数,也不能让函数带回共用体变量
-
引用方式 变量名.成员名
枚举类型
例子
enum color
{
Red, //逗号分隔,枚举常量,实际上是一个整形数字(4字节)
Blue,
Green,
Yellow //最后一个不加逗号
};
enum color mycholor1;
枚举变量如何使用
//枚举变量的赋值
mycholor1 = Red;
//枚举常量可以赋值
enum color
{
Red = 5,
Blue, //系统自动给Blue赋值6
Green, //7
Yellow //8
};
用typedef定义类型
typedef int INTEGER; //用INTEGER代表了int
typedef struct date
{
int year;
int month;
int day;
}DATE; //用DATE替代了struct date
//定义结构体变量
DATE today;
//变形
typedef int NUM[100]; //定义NUM为整形数组类型
NUM n; //等价于 int n[100];
typedef char *PSTRING; //定义PSTRING为字符指针类型
PSTRING p, q; //char *p, *q;
typedef用法
-
先写出常规的定义方法,如:
int n[100]
-
将变量名替换成自己想用的类型名
typedef int NUM[100]
十 位运算
1、位的概念以及位运算符介绍
一个字节由8个二进制位组成
位运算符
针对二进制位进行运算的
-
&
按位与- 0 & 0 = 0;0 & 1 = 0;1 & 0 = 0;1 & 1 =1
-
|
按位或- 0 | 0 = 0;0 | 1 = 1;1 | 0 = 1;1 | 1 =1
-
^
按位异或-
0 ^ 0 = 0;0 ^ 1 =1;1 ^ 0 = 1;1 ^ 1 = 0
-
和1做异或,能够翻转位,如果和0做异或,能够保留该位
-
-
~
取反 -
<<
左移- 将二进制位左移若干位,右侧补零;每左移一位,相当于 * 2
-
>>
右移- 将二进制位右移若干位,左侧补零,超出最低位被舍弃;每,相当于 / 2
复合赋值运算符
&=
、|=
、^=
、<<=
、>>=
a &= b
相当于a = a & b
其他几种符号与之作用相似
2、位运算的具体应用
每日任务
#define BIT(x) (1<<(x))
//BIT(0) == 1; BIT(1) == 2;...
enum EnumTask
{
ETask1 = BIT(0), //1
ETask2 = BIT(1), //10
ETask3 = BIT(2), //100
ETask4 = BIT(3), //1000
ETask5 = BIT(4) //10000
};
int main()
{
int task = 0;
if (task & ETask3)
printf("3任务已完成");
else
printf("3任务未完成");
task |= ETask3; //完成任务3
}
十一 文件
1、文件概述、文本、二进制文件区别
文件看成字符序列(字符流)
-
ASCII文件(文本文件)
-
二进制文件
-
把内存中的数据按照内存中的存储形式输出到磁盘上存放
-
二进制文件读取时,系统将16进制数转化成10进制,再到ASCII表中抽对
-
short int a = 10000; //内存中实际上存储的是2710,因为0x2710是10000的十六进制表示
//在内存中高字节存储在高地址,低字节存储在低地址叫做小端存储;反之叫大端存储
2、文件的开、关、读、写,实战操练
文件打开函数 fopen
FILE *fp
fp = fopen("文件名", "使用的文件的方式"); //文件名和使用文件的格式都是字符串
fp = fopen("a.txt", "r");
fopen
提供了三个信息:
-
需要打开的文件名
-
使用文件的方式
-
让哪个指针变量指向被打开的文件
文件关闭函数 fclose
fclose(fp); //fclose(文件指针);
文件的读写
fputc
: 把一个字符写到磁盘文件上
fputc(ch, fp);
//ch是字符,如果fputc失败,会返回EOF(-1);成功会返回写入文件那个字符的ascii码值
fgetc
: 从指定文件读入一个字符
char reco = fgetc(文件指针);
FILE *fp;
fp = fopen("FTest.txt", "r");
if(fp == NULL)
{
printf("打开失败");
}
else
{
char reco = fgetc(fp);
while (!feof(fp))
{
putchar(reco);
reco = fgetc(fp);
}
printf("\n");
fclose(fp);
}
二进制表示
0D 0A
表示换行+回车
举例 读取网络游戏的配置文件
//读取一行文件
FILE *fp = fopen("config.txt", "r");
if(!fp)
{
printf("文件打开失败");
}
else
{
char LineBuff[1024];
while(!feof(fp))
{
LineBuff[0] = 0; //'\0'
//读取一行到换行符结束
if(fgetc(LineBuff, sizeof(LineBuff) - 1, fp) == NULL)
continue;
if(LineBuff[0] == 0)
continue;
lblprocstring:
if(strlen(LineBuff) > 0) //读到了实际内容
{
if(LineBuff[strlen(LineBuff) - 1] == 10 || LineBuff[strlen(LineBuff) - 1] == 13)
{
LineBuff[strlen(LineBuff) - 1] = 0;
goto lblprocstring;
}
}
if (strlen(LineBuff) <= 0)
continue;
printf("%s\n", LineBuff);
fclose(fp);
3、将结构体写入二进制文件再读出
fwrite
用于向文件中写入数据
基本形式:
fwrite(buffer, size, count, fp);
buffer:指针/地址,要写到文件中去的数据就在这个地址保存着
size:要写入文件的字节数
count:要写入多少个size的数据项
fp:文件指针
#include <string.h>
struct stu
{
char name[30];
int age;
double score;
};
int main()
{
struct stu student[2];
strcpy(student[0].name, "小李");
student[0].age = 19;
student[0].score = 91.2f;
strcpy(student[1].name, "小张");
student[0].age = 20;
student[0].score = 98.1f;
FILE *fp;
fp = fopen("structfile.bin", "wb");
if(!fp)
{
printf("未成功打开");
}
else
{
int result = fwrite(student, sizeof(struct stu), 2, fp) //如果写入成功返回值是count
fclose(fp);
}
}
结构体的字节对齐问题
在vs中,为了高效运行采用了8字节对齐方式,而不同编译器采用的对齐方式不同,如:Linux下gcc编译器。所以引入了1字节对齐结构体。
#pragma pack(1) //1字节对齐结构体
struct 结构体名
{
成员;
};
#pragma pack() //取消1字节对齐结构体
fread
从二进制文件中读出数据
基本形式
fread(buffer, size, count, fp);
buffer:指针/地址,从文件中读出的数据写在这个地址上
size:要读入文件的字节数
count:要读入多少个size的数据项
fp:文件指针
返回值:如果fread失败,则返回0;否则返回count值
FILE *fp;
fp = fopen("structfile.bin", "rb");
struct stu studentread[2];
if(fp == NULL)
{
printf("文件没有成功打开");
}
else
{
int result = fread(studentread, sizeof(struct stu), 2, fp);
fclose(fp);
}
END…
上一篇: 揭秘浮点数
下一篇: asp.net 动态表单之数据分页