介绍
C语言是一门面向过程
、抽象化
的通用程序设计语言,广泛应用于底层开发。C语言能以简易的方式编译、处理低级存储器。C语言是仅产生少量的机器语言以及不需要任何运行环境支持便能运行的高效率程序设计语言。
1972 年,为了移植与开发 UNIX
操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。
hello,world
第一个C语言程序:hello,World
// stdio.h 是一个头文件 (标准输入输出头文件) , #include 是一个预处理命令,用来引入头文件
#include <stdio.h>
// 所有的 C 语言程序都需要包含 main() 函数。 代码从 main() 函数开始执行。
int main()
{
// printf() 用于格式化输出到屏幕。
printf("hello,world!\n");
// return 0; 语句用于表示退出程序。
return 0;
}
一. C语言编译过程
C代码编译成可执行程序经过4步:
- 预处理:宏定义展开、头文件展开、条件编译等,同时将代码中的注释删除,这里并不会检查语法
- 编译:检查语法,将预处理后文件编译生成汇编文件
- 汇编:将汇编文件生成目标文件(二进制文件)
- 链接:C语言写的程序是需要依赖各种库的,所以编译之后还需要把库链接到最终的可执行程序中去
gcc编译过程
1) 分步编译
- 预处理:gcc -E hello.c -o hello.i
- 编 译:gcc -S hello.i -o hello.s
- 汇 编:gcc -c hello.s -o hello.o
- 链 接:gcc hello.o -o hello_elf
选项 | 含义 | 文件后缀 | 含义 |
---|---|---|---|
-E | 只进行预处理 | .c | C 语言文件 |
-S(大写) | 只进行预处理和编译 | .i | 预处理后的 C 语言文件 |
-c(小写) | 只进行预处理、编译和汇编 | .s | 编译后的汇编文件 |
-o file | 指定生成的输出文件名为 file | .o | 编译后的目标文件 |
2) 一步编译
- gcc hello.c -o demo(会经过:预处理、编译、汇编、链接的过程)
二. 数据类型
数据类型的作用:编译器预算对象(变量)分配的内存空间大小。
常量:
- 在程序运行过程中,其值不能被改变的量
- 常量一般出现在表达式或赋值语句中
变量:
- 在程序运行过程中,其值可以改变
- 变量在使用前必须先定义,定义变量前必须有相应的数据类型
标识符命名规则:
- 标识符不能是关键字
- 标识符只能由字母、数字、下划线组成
- 第一个字符必须为字母或下划线
- 标识符中字母区分大小写
计算机内存数值存储方式
原码
一个数的原码(原始的二进制码)有如下特点:
- 最高位做为符号位,0表示正,为1表示负
- 其它数值部分就是数值本身绝对值的二进制数
- 负数的原码是在其绝对值的基础上,最高位变为1
下面数值以1字节的大小描述:
十进制数 | 原码 |
---|---|
+15 | 0000 1111 |
-15 | 1000 1111 |
+0 | 0000 0000 |
-0 | 1000 0000 |
反码
- 对于正数,反码与原码相同
- 对于负数,符号位不变,其它部分取反(1变0,0变1)
十进制数 | 原码 |
---|---|
+15 | 0000 1111 |
-15 | 1111 0000 |
+0 | 0000 0000 |
-0 | 1111 1111 |
补码
在计算机系统中,数值一律用补码来存储。
- 对于正数,原码、反码、补码相同
- 对于负数,其补码为它的反码加1
- 补码符号位不动,其他位求反,最后整个数加1,得到原码
十进制数 | 原码 |
---|---|
+15 | 0000 1111 |
-15 | 1111 0001 |
+0 | 0000 0000 |
-0 | 0000 0000 |
在计算机系统中,数值一律用补码来存储,主要原因是:
- 统一了零的编码
- 将符号位和其它位统一处理
- 将减法运算转变为加法运算
- 两个用补码表示的数相加时,如果最高位(符号位)有进位,则进位被舍弃
sizeof关键字
- sizeof不是函数,所以不需要包含任何头文件,它的功能是计算一个数据类型的大小,单位为字节
- sizeof的返回值为size_t
- size_t类型在32位操作系统下是unsigned int,是一个无符号的整数
int a;
int b = sizeof(a);//sizeof得到指定值占用内存的大小,单位:字节
printf("b = %d\n", b);
三. 运算符与表达式
常用运算符分类
运算符类型 | 作用 |
---|---|
算术运算符 | 用于处理四则运算 |
赋值运算符 | 用于将表达式的值赋给变量 |
比较运算符 | 用于表达式的比较,并返回一个真值或假值 |
逻辑运算符 | 用于根据表达式的值返回真值或假值 |
位运算符 | 用于处理数据的位运算 |
sizeof运算符 | 用于求字节数长度 |
算术运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
+ | 正号 | +3 | 3 |
- | 负号 | -3 | -3 |
+ | 加 | 10 + 5 | 15 |
- | 减 | 10 - 5 | 5 |
* | 乘 | 10 * 5 | 50 |
/ | 除 | 10 / 5 | 2 |
% | 取模(取余) | 10 % 3 | 1 |
++ | 前自增 | a=2; b=++a; | a=3; b=3; |
++ | 后自增 | a=2; b=a++; | a=3; b=2; |
-- | 前自减 | a=2; b=--a; | a=1; b=1; |
-- | 后自减 | a=2; b=a--; | a=1; b=2; |
赋值运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
= | 赋值 | a=2; b=3; | a=2; b=3; |
+= | 加等于 | a=0; a+=2; | a=2; |
-= | 减等于 | a=5; a-=3; | a=2; |
*= | 乘等于 | a=2; a*=2; | a=4; |
/= | 除等于 | a=4; a/=2; | a=2; |
%= | 模等于 | a=3; a%2; | a=1; |
比较运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
== | 相等于 | 4 == 3 | 0 |
!= | 不等于 | 4 != 3 | 1 |
< | 小于 | 4 < 3 | 0 |
> | 大于 4 > 3 | 1 | |
<= | 小于等于 | 4 <= 3 | 0 |
>= | 大于等于 | 4 >= 1 | 1 |
逻辑运算符
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
! | 非 | !a | 如果a为假,则!a为真;如果a为真,则!a为假。 |
&& | 与 | a && b | 如果a和b都为真,则结果为真,否则为假。 |
或 | a || b | 如果a和b有一个为真,则结果为真,二者都为假时,结果为假。 |
类型转换
数据有不同的类型,不同类型数据之间进行混合运算时必然涉及到类型的转换问题。
转换的方法有两种:
- 自动转换(隐式转换):遵循一定的规则,由编译系统自动完成。
- 强制类型转换:把表达式的运算结果强制转换成所需的数据类型。
类型转换的原则:占用内存字节数少(值域小)的类型,向占用内存字节数多(值域大)的类型转换,以保证精度不降低。
隐式转换
int num = 5;
printf("s1=%d\n", num / 2);
printf("s2=%lf\n", num / 2.0);
强制转换
float x = 0;
int i = 0;
x = 3.6f;
i = x; //x为实型, i为整型,直接赋值会有警告
i = (int)x; //使用强制类型转换
printf("x=%f, i=%d\n", x, i);
四.程序流程结构
C语言支持最基本的三种程序运行结构:顺序结构、选择结构、循环结构
。
- 顺序结构:程序按顺序执行,不发生跳转。
- 选择结构:依据是否满足条件,有选择的执行相应功能。
- 循环结构:依据条件是否满足,循环多次执行某段代码。
选择结构
1. if…else语句
int main()
{
int a = 1;
int b = 2;
if (a > b)
{
printf("%d\n", a);
}
else
{
printf("%d\n", b);
}
return 0;
}
2. 三目运算符
int a = 10;
int b = 20;
int c;
c = ( a > b ? a : b );
3. switch语句
int main()
{
char c;
c = getchar();
switch (c) //参数只能是整型变量
{
case '1':
printf("OK\n");
break;//switch遇到break就中断了
case '2':
printf("not OK\n");
break;
default://如果上面的条件都不满足,那么执行default
printf("are u ok?\n");
}
return 0;
}
循环结构
1. while语句
int main()
{
int a = 20;
while (a > 10)
{
scanf("%d", &a);
printf("a = %d\n", a);
}
return 0;
}
2. do…while语句
int main()
{
int a = 1;
do
{
a++;
printf("a = %d\n", a);
} while (a < 10);
return 0;
}
3. for语句
int main()
{
int I;
int sum = 0;
for (i = 0; i <= 100; I++)
{
sum += I;
}
printf("sum = %d\n", sum);
return 0;
}
跳转语句break、continue、goto
1. break语句
- 当它出现在switch条件语句中时,作用是终止某个case并跳出switch结构。
- 当它出现在循环语句中,作用是跳出当前内循环语句,执行后面的代码。
- 当它出现在嵌套循环语句中,跳出最近的内循环语句,执行后面的代码。
int main()
{
int i = 0;
while (1)
{
I++;
printf("i = %d\n", i);
if (i == 10)
{
break; //跳出while循环
}
}
int flag = 0;
int m = 0;
int n = 0;
for (m = 0; m < 10; m++)
{
for (n = 0; n < 10; n++)
{
if (n == 5)
{
flag = 1;
break; //跳出for (n = 0; n < 10; n++)
}
}
if (flag == 1)
{
break; //跳出for (m = 0; m < 10; m++)
}
}
return 0;
}
2. continue语句
在循环语句中,如果希望立即终止本次循环,并执行下一次循环,此时就需要使用continue语句
int main()
{
int sum = 0; //定义变量sum
for (int i = 1; i <= 100; I++)
{
if (i % 2 == 0) //如果i是一个偶数,执行if语句中的代码
{
continue; //结束本次循环
}
sum += i; //实现sum和I的累加
}
printf("sum = %d\n", sum);
return 0;
}
goto语句(无条件跳转,尽量少用)
int main()
{
goto End; //无条件跳转到End的标识
printf("aaaaaaaaa\n");
End:
printf("bbbbbbbb\n");
return 0;
}
五.数组和字符串
1. 数组
在程序设计中,为了方便处理数据把具有相同类型的若干变量按有序形式组织起来——称为数组。
数组就是在内存中连续的相同类型的变量空间。同一个数组所有的成员都是相同的数据类型,同时所有的成员在内存中的地址是连续的。
数组属于构造数据类型:
- 一个数组可以分解为多个数组元素:这些数组元素可以是基本数据类型或构造类型。
- 按数组元素类型的不同,数组可分为:数值数组、字符数组、指针数组、结构数组等类别。
1.1 一维数组
数组的初始化
在定义数组的同时进行赋值,称为初始化。全局数组若不初始化,编译器将其初始化为零。局部数组若不初始化,内容为随机值。
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量
int a[10] = { 1, 2, 3 };//初始化前三个成员,后面所有元素都设置为0
int a[10] = { 0 };//所有的成员都设置为0
//[]中不定义元素个数,定义时必须初始化
int a[] = { 1, 2, 3, 4, 5 };//定义了一个数组,有5个成员
数组名
数组名是一个地址的常量,代表数组中首元素的地址。
int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };//定义一个数组,同时初始化所有成员变量
printf("a = %p\n", a);
printf("&a[0] = %p\n", &a[0]);
1.2. 二维数组
二维数组定义的一般形式是:类型说明符 数组名[常量表达式1][常量表达式2]
二维数组的初始化
//分段赋值 int a[3][4] = {{ 1, 2, 3, 4 },{ 5, 6, 7, 8, },{ 9, 10, 11, 12 }};
int a[3][4] =
{
{ 1, 2, 3, 4 },
{ 5, 6, 7, 8, },
{ 9, 10, 11, 12 }
};
//连续赋值
int a[3][4] = { 1, 2, 3, 4 , 5, 6, 7, 8, 9, 10, 11, 12 };
//可以只给部分元素赋初值,未初始化则为0
int a[3][4] = { 1, 2, 3, 4 };
//所有的成员都设置为0
int a[3][4] = {0};
//[]中不定义元素个数,定义时必须初始化
int a[][4] = { 1, 2, 3, 4, 5, 6, 7, 8};
1.3 多维数组
多维数组的定义与二维数组类似,其语法格式具体如下:
数组类型修饰符 数组名 [n1][n2]…[nn];
2. 字符数组与字符串
字符数组与字符串区别
- C语言中没有字符串这种数据类型,可以通过char的数组来替代;
- 字符串一定是一个char的数组,但char的数组未必是字符串;
- 数字0(和字符‘\0’等价)结尾的char数组就是一个字符串,但如果char数组没有以数字0结尾,那么就不是一个字符串,只是普通字符数组,所以字符串是一种特殊的char的数组。
字符串的初始化
// C语言没有字符串类型,通过字符数组模拟
// C语言字符串,以字符‘\0’, 数字0
int main()
{
//不指定长度, 没有0结束符,有多少个元素就有多长
char buf[] = { 'a', 'b', 'c' };
printf("buf = %s\n", buf); //乱码
//指定长度,后面没有赋值的元素,自动补0
char buf2[100] = { 'a', 'b', 'c' };
printf("buf2 = %s\n", buf2);
//所有元素赋值为0
char buf3[100] = { 0 };
printf("buf3 = %s\n", buf3);
//数组越界
char buf4[2] = { '1', '2', '3' };
printf("buf4 = %s\n", buf4);
char buf5[50] = { '1', 'a', 'b', '0', '7' };
printf("buf5 = %s\n", buf5);
char buf6[50] = { '1', 'a', 'b', 0, '7' };
printf("buf6 = %s\n", buf6);
char buf7[50] = { '1', 'a', 'b', '\0', '7' };
printf("buf7 = %s\n", buf7);
//使用字符串初始化,编译器自动在后面补0,常用
char buf8[] = "agjdslgjlsdjg";
printf("buf8 = %s\n", buf8);
//'\0'后面最好不要连着数字,有可能几个数字连起来刚好是一个转义字符
//'\ddd'八进制字义字符,'\xdd'十六进制转移字符
// \012相当于\n
char str[] = "\012abc";
printf("str == %s\n", str);
return 0;
}
六. 函数
概述
C 程序是由函数组成的,我们写的代码都是由主函数 main()开始执行的。函数是 C 程序的基本模块,是用于完成特定任务的程序代码单元。
从函数定义的角度看,函数可分为系统函数和用户定义函数两种:
- 系统函数,即库函数:这是由编译系统提供的,用户不必自己定义这些函数,可以直接使用它们,如我们常用的打印函数printf()。
- 用户定义函数:用以解决用户的专门需要。
函数的作用
- 函数的使用可以省去重复代码的编写,降低代码重复率
- 函数可以让程序更加模块化,从而有利于程序的阅读,修改和完善
函数的定义
函数定义的一般形式:
返回类型 函数名(形式参数列表)
{
数据定义部分;
执行语句部分;
}
int max(int a, int b){
return a>b?a:b;
}
int main(){
int a = 10;
int b = 100;
int c = max(a, b);
printf("%d",c);
}
函数的形参和实参
- 形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。
- 实参出现在主调函数中,进入被调函数后,实参也不能使用。
- 实参变量对形参变量的数据传递是“值传递”,即单向传递,只由实参传给形参,而不能由形参传回来给实参。
- 在调用函数时,编译系统临时给形参分配存储单元。调用结束后,形参单元被释放。
- 实参单元与形参单元是不同的单元。调用结束后,形参单元被释放,函数调用结束返回主调函数后则不能再使用该形参变量。实参单元仍保留并维持原值。因此,在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。
函数的声明
- 如果使用用户自己定义的函数,而该函数与调用它的函数(即主调函数)不在同一文件中,或者函数定义的位置在主调函数之后,则必须在调用此函数之前对被调用的函数作声明。
- 所谓函数声明,就是在函数尚在未定义的情况下,事先将该函数的有关信息通知编译系统,相当于告诉编译器,函数在后面定义,以便使编译能正常进行。
- 注意:一个函数只能被定义一次,但可以声明多次。
int max(int x, int y); // 函数的声明,分号不能省略
// int max(int, int); // 另一种方式
int main()
{
int a = 10, b = 25, num_max = 0;
num_max = max(a, b); // 函数的调用
printf("num_max = %d\n", num_max);
return 0;
}
// 函数的定义
int max(int x, int y)
{
return x > y ? x : y;
}
main函数与exit函数
在main函数中调用exit和return结果是一样的,但在子函数中调用return只是代表子函数终止了,在子函数中调用exit,那么程序终止。
void fun()
{
printf("fun\n");
//return;
exit(0);
}
int main()
{
fun();
while (1);
return 0;
}
多文件(分文件)编程
1.分文件编程
- 把函数声明放在头文件xxx.h中,在主函数中包含相应头文件
- 在头文件对应的xxx.c中实现xxx.h声明的函数
2.防止头文件重复包含
当一个项目比较大时,往往都是分文件,这时候有可能不小心把同一个头文件 include 多次,或者头文件嵌套包含
为了避免同一个文件被include多次,C/C++中有两种方式,一种是 #ifndef 方式,一种是 #pragma once 方式。
方法一:
#ifndef __SOMEFILE_H__
#define __SOMEFILE_H__
// 声明语句
#endif
方法二:
#pragma once
// 声明语句
七. 指针
1. 指针变量的定义和使用
- 指针也是一种数据类型,指针变量也是一种变量
- 指针变量指向谁,就把谁的地址赋值给指针变量
- “ * ”操作符操作的是指针变量指向的内存空间
int a = 10;
int *p = &a;
2.通过指针间接修改变量的值
int a = 0;
int b = 11;
int *p = &a;
*p = 100;
printf("a = %d, *p = %d\n", a, *p);
p = &b;
*p = 22;
printf("b = %d, *p = %d\n", b, *p);
3.指针大小
- 使用sizeof()测量指针的大小,得到的总是:4或8
- sizeof()测的是指针变量指向存储地址的大小
- 在32位平台,所有的指针(地址)都是32位(4字节)
- 在64位平台,所有的指针(地址)都是64位(8字节)
4.野指针和空指针
指针变量也是变量,是变量就可以任意赋值,但是任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。
int a = 100;
int *p;
p = a; //把a的值赋值给指针变量p,p为野指针, ok,不会有问题,但没有意义
p = 0x12345678; //给指针变量p赋值,p为野指针, ok,不会有问题,但没有意义
*p = 1000; //操作野指针指向未知区域,内存出问题,err
但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针。
int *p = NULL;
NULL是一个值为0的宏常量:
#define NULL ((void *)0)
5.万能指针void *
void *指针可以指向任意变量的内存空间
void *p = NULL;
int a = 10;
p = (void *)&a; //指向变量时,最好转换为void *
//使用指针变量指向的内存时,转换为int *
*( (int *)p ) = 11;
printf("a = %d\n", a);
6.const修饰的指针变量
int a = 100;
int b = 200;
//指向常量的指针
//修饰*,指针指向内存区域不能修改,指针指向可以变
const int *p1 = &a; //等价于int const *p1 = &a;
//*p1 = 111; //err
p1 = &b; //ok
//指针常量
//修饰p1,指针指向不能变,指针指向的内存可以修改
int * const p2 = &a;
//p2 = &b; //err
*p2 = 222; //ok
7.多级指针
- C语言允许有多级指针存在,在实际的程序中一级指针最常用,其次是二级指针。
- 二级指针就是指向一个一级指针变量地址的指针。
int a = 10;
int *p = &a; //一级指针
*p = 100; //*p就是a
int **q = &p;
//*q就是p
//**q就是a
int ***t = &q;
//*t就是q
//**t就是p
//***t就是a
指针小结
定义 | 说明 |
---|---|
int i | 定义整形变量 |
int *p | 定义一个指向int的指针变量 |
int a[10] | 定义一个有10个元素的数组,每个元素类型为int |
int p[10] | 定义一个有10个元素的数组,每个元素类型为int |
int func() | 定义一个函数,返回值为int型 |
int *func() | 定义一个函数,返回值为int *型 |
int **p | 定义一个指向int的指针的指针,二级指针 |
八.内存管理
1.作用域
C语言变量的作用域分为:
- 代码块作用域(代码块是{}之间的一段代码)
- 函数作用域
- 文件作用域
1.1局部变量
局部变量也叫auto自动变量(auto可写可不写),一般情况下代码块{}内部定义的变量都是自动变量,它有如下特点:
- 在一个函数内定义,只在函数范围内有效
- 在复合语句中定义,只在复合语句中有效
- 随着函数调用的结束或复合语句的结束局部变量的声明声明周期也结束
- 如果没有赋初值,内容为随机
void test()
{
//auto写不写是一样的
//auto只能出现在{}内部
auto int b = 10;
}
1.2静态(static)局部变量
- static局部变量的作用域也是在定义的函数内有效
- static局部变量的生命周期和程序运行周期一样,同时staitc局部变量的值只初始化一次,但可以赋值多次
- static局部变量若未赋以初值,则由系统自动赋值:数值型变量自动赋初值0,字符型变量赋空字符
void fun()
{
//静态局部变量,没有赋值,系统赋值为0,而且只会初始化一次
static int a;
a++;
printf("a = %d\n", a);
}
1.3 全局变量
- 在函数外定义,可被本文件及其它文件中的函数所共用,若其它文件中的函数调用此变量,须用extern声明
- 全局变量的生命周期和程序运行周期一样
- 不同文件的全局变量不可重名
1.4 静态(static)全局变量
- 在函数外定义,作用范围被限制在所定义的文件中
- 不同文件静态全局变量可以重名,但作用域不冲突
- static全局变量的生命周期和程序运行周期一样,同时staitc全局变量的值只初始化一次
1.5 extern全局变量声明
- extern int a;声明一个变量,这个变量在别的文件中已经定义了,这里只是声明,而不是定义。
1.6全局函数和静态函数
在C语言中函数默认都是全局的,使用关键字static可以将函数声明为静态,函数定义为static就意味着这个函数只能在定义这个函数的文件中使用,在其他文件中不能调用,即使在其他文件中声明这个函数都没用。
注意:
- 允许在不同的函数中使用相同的变量名,它们代表不同的对象,分配不同的单元,互不干扰。
- 同一源文件中,允许全局变量和局部变量同名,在局部变量的作用域内,全局变量不起作用。
- 所有的函数默认都是全局的,意味着所有的函数都不能重名,但如果是staitc函数,那么作用域是文件级的,所以不同的文件static函数名是可以相同的。
总结
类 型 | 作 用 域 | 生 命 周 期 |
---|---|---|
auto变量 | 一对{}内 | 当前函数 |
static局部变量 | 一对{}内 | 整个程序运行期 |
extern变量 | 整个程序 | 整个程序运行期 |
static全局变量 | 当前文件 | 整个程序运行期 |
extern函数 | 整个程序 | 整个程序运行期 |
static函数 | 当前文件 | 整个程序运行期 |
register变量 | 一对{}内 | 当前函数 |
2. 内存布局
代码区(text segment)
加载的是可执行文件代码段,所有的可执行代码都加载到代码区,这块内存是不可以在运行期间修改的。
未初始化数据区(BSS)
加载的是可执行文件BSS段,位置可以分开亦可以紧靠数据段,存储于数据段的数据(全局未初始化,静态未初始化数据)的生存周期为整个程序运行过程。
全局初始化数据区/静态数据区(data segment)
加载的是可执行文件数据段,存储于数据段(全局初始化,静态初始化数据,文字常量(只读))的数据的生存周期为整个程序运行过程。
栈区(stack)
栈是一种先进后出的内存结构,由编译器自动分配释放,存放函数的参数值、返回值、局部变量等。在程序运行过程中实时加载和释放,因此,局部变量的生存周期为申请到释放该段栈空间。
堆区(heap)
堆是一个大容器,它的容量要远远大于栈,但没有栈那样先进后出的顺序。用于动态内存分配。堆在内存中位于BSS区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
2.1 堆区内存分配和释放
malloc()
void *malloc(size_t size);
功能:在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。分配的内存空间内容不确定,一般使用memset初始化。
int main()
{
int count, *array, n;
printf("请输入要申请数组的个数:\n");
scanf("%d", &n);
array = (int *)malloc(n * sizeof (int));
if (array == NULL)
{
printf("申请空间失败!\n");
return -1;
}
//将申请到空间清0
memset(array, 0, sizeof(int)*n);
for (count = 0; count < n; count++) /*给数组赋值*/
array[count] = count;
for (count = 0; count < n; count++) /*打印数组元素*/
printf("%d", array[count]);
free(array);
return 0;
}
free()
void free(void *ptr);
功能:释放ptr所指向的一块内存空间,ptr是一个任意类型的指针变量,指向被释放区域的首地址。对同一内存空间多次释放会出错。
九.复合类型
1.结构体
结构体变量的定义和初始化
定义结构体变量的方式:
- 先声明结构体类型再定义变量名
- 在声明类型的同时定义变量
- 直接定义结构体类型变量(无类型名)
结构体类型和结构体变量关系:
- 结构体类型:指定了一个结构体类型,它相当于一个模型,但其中并无具体数据,系统对之也不分配实际内存单元。
- 结构体变量:系统根据结构体类型(内部成员状况)为之分配空间。
//结构体类型的定义
struct stu
{
char name[50];
int age;
};
//先定义类型,再定义变量(常用)
struct stu s1 = { "mike", 18 };
//定义类型同时定义变量
struct stu2
{
char name[50];
int age;
}s2 = { "lily", 22 };
struct
{
char name[50];
int age;
}s3 = { "yuri", 25 };
指向普通结构体变量的指针
struct student {
char name[20];
int age;
};
int main(){
struct student stu = {"leon", 20};
// 结构体指针,通过->操作结构体成员
struct student *p = &stu;
printf("%s\n",p->name);
printf("%d\n",p->age);
}
堆区结构体变量
struct student {
char name[20];
int age;
};
int main(){
// 在堆空间创建结构
struct student *stu = malloc(sizeof(struct student));
strcpy(stu->name, "leon");
stu->age = 20;
printf("name = %s, age = %d", stu->name, stu->age);
printf("name = %s, age = %d", (*stu).name, (*stu).age);
free(stu);
stu = NULL;
return 0;
}
2.共用体(联合体)
- 联合union是一个能在同一个存储空间存储不同类型数据的类型;
- 联合体所占的内存长度等于其最长成员的长度,也有叫做共用体;
- 同一内存段可以用来存放几种不同类型的成员,但每一瞬时只有一种起作用;
- 共用体变量中起作用的成员是最后一次存放的成员,在存入一个新的成员后原有的成员的值会被覆盖;
- 共用体变量的地址和它的各成员的地址都是同一地址
//共用体也叫联合体
union Test
{
unsigned char a;
unsigned int b;
unsigned short c;
};
int main()
{
//定义共用体变量
union Test tmp;
//1、所有成员的首地址是一样的
printf("%p, %p, %p\n", &(tmp.a), &(tmp.b), &(tmp.c));
//2、共用体大小为最大成员类型的大小
printf("%lu\n", sizeof(union Test));
//3、一个成员赋值,会影响另外的成员
//左边是高位,右边是低位
//低位放低地址,高位放高地址
tmp.b = 0x44332211;
printf("%x\n", tmp.a); //11
printf("%x\n", tmp.c); //2211
tmp.a = 0x00;
printf("short: %x\n", tmp.c); //2200
printf("int: %x\n", tmp.b); //44332200
return 0;
}
3. 枚举
枚举:将变量的值一一列举出来,变量的值只限于列举出来的值的范围内。
枚举类型定义:
enum 枚举名
{
枚举值表
};
- 在枚举值表中应列出所有可用值,也称为枚举元素。
- 枚举值是常量,不能在程序中用赋值语句再对它赋值。
- 举元素本身由系统定义了一个表示序号的数值从0开始顺序定义为0,1,2 …
4.typedef
typedef为C语言的关键字,作用是为一种数据类型(基本类型或自定义数据类型)定义一个新名字,不能创建新类型。
- 与#define不同,typedef仅限于数据类型,而不是能是表达式或具体的值
- #define发生在预处理,typedef发生在编译阶段
typedef signed long long ull;
typedef struct Person PP;
struct Person
{
char name[100];
int age;
ull a;
};
int main()
{
ull test = 1000000;
PP p = {"alex", 10, test};
return 0;
}
十. 文件操作
1. 文件指针
在C语言中用一个指针变量指向一个文件,这个指针称为文件指针。
typedef struct
{
short level; //缓冲区"满"或者"空"的程度
unsigned flags; //文件状态标志
char fd; //文件描述符
unsigned char hold; //如无缓冲区不读取字符
short bsize; //缓冲区的大小
unsigned char *buffer;//数据缓冲区的位置
unsigned ar; //指针,当前的指向
unsigned istemp; //临时文件,指示器
short token; //用于有效性的检查
}FILE;
FILE
是系统使用typedef定义出来的有关文件信息的一种结构体类型,结构中含有文件名、文件状态和文件当前位置等信息。
声明FILE结构体类型的信息包含在头文件“stdio.h”中,一般设置一个指向FILE类型变量的指针变量,然后通过它来引用这些FILE类型变量。通过文件指针就可对它所指的文件进行各种操作。
C语言中有三个特殊的文件指针由系统默认打开,用户无需定义即可直接使用:
- stdin: 标准输入,默认为当前终端(键盘),我们使用的scanf、getchar函数默认从此终端获得数据。
- stdout:标准输出,默认为当前终端(屏幕),我们使用的printf、puts函数默认输出信息到此终端。
- stderr:标准出错,默认为当前终端(屏幕),我们使用的perror函数默认输出信息到此终端。
2.文件的打开
任何文件使用之前必须打开:
#include <stdio.h>
FILE * fopen(const char * filename, const char * mode);
功能:打开文件
参数:
filename:需要打开的文件名,根据需要加上路径
mode:打开文件的模式设置
返回值:
成功:文件指针
失败:NULL
第一个参数的几种形式:
//相对路径:
//打开当前目录passdw文件:源文件(源程序)所在目录
FILE *fp_passwd = fopen("passwd.txt", "r");
//打开当前目录(test)下passwd.txt文件
fp_passwd = fopen(". / test / passwd.txt", "r");
//打开当前目录上一级目录(相对当前目录)passwd.txt文件
fp_passwd = fopen(".. / passwd.txt", "r");
//绝对路径:
//打开C盘test目录下一个叫passwd.txt文件
fp_passwd = fopen("c://test//passwd.txt","r");
第二个参数的几种形式(打开文件的方式):
打开模式 | 含义 |
---|---|
r或rb | 以只读方式打开一个文本文件(不创建文件,若文件不存在则报错) |
w或wb | 以写方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件) |
a或ab | 以追加方式打开文件,在末尾添加内容,若文件不存在则创建文件 |
r+或rb+ | 以可读、可写的方式打开文件(不创建新文件) |
w+或wb+ | 以可读、可写的方式打开文件(如果文件存在则清空文件,文件不存在则创建一个文件) |
a+或ab+ | 以添加方式打开文件,打开文件并在末尾更改文件,若文件不存在则创建文件 |
注意:
- b是二进制模式的意思,b只是在Windows有效,在Linux用r和rb的结果是一样的
- Unix和Linux下所有的文本文件行都是\n结尾,而Windows所有的文本文件行都是\r\n结尾
- 在Windows平台下,以“文本”方式打开文件,不加b:
- 当读取文件的时候,系统会将所有的 "\r\n" 转换成 "\n"
- 当写入文件的时候,系统会将 "\n" 转换成 "\r\n" 写入
- 以"二进制"方式打开文件,则读\写都不会进行这样的转换
- 在Unix/Linux平台下,“文本”与“二进制”模式没有区别,"\r\n" 作为两个字符原样输入输出
int main(void)
{
FILE *fp = NULL;
// "\\"这样的路径形式,只能在windows使用
// "/"这样的路径形式,windows和linux平台下都可用,建议使用这种
// 路径可以是相对路径,也可是绝对路径
fp = fopen("../test", "w");
//fp = fopen("..\\test", "w");
if (fp == NULL) //返回空,说明打开失败
{
//perror()是标准出错打印函数,能打印调用库函数出错原因
perror("open");
return -1;
}
return 0;
}
3. 文件的关闭
任何文件在使用后应该关闭:
- 打开的文件会占用内存资源,如果总是打开不关闭,会消耗很多内存
- 一个进程同时打开的文件数是有限制的,超过最大同时打开文件数,再次调用fopen打开文件会失败
- 如果没有明确的调用fclose关闭打开的文件,那么程序在退出的时候,操作系统会统一关闭。
#include <stdio.h>
int fclose(FILE * stream);
功能:关闭先前fopen()打开的文件。此动作让缓冲区的数据写入文件中,并释放系统所提供的文件资源。
参数:
stream:文件指针
返回值:
成功:0
失败:-1
FILE * fp = NULL;
fp = fopen("abc.txt", "r");
fclose(fp);
4.文件的顺序读写
1.按照字符读写文件fgetc、fputc
1)写文件
#include <stdio.h>
int fputc(int ch, FILE * stream);
功能:将ch转换为unsigned char后写入stream指定的文件中
参数:
ch:需要写入文件的字符
stream:文件指针
返回值:
成功:成功写入文件的字符
失败:返回-1
char buf[] = "this is a test for fputc";
int i = 0;
int n = strlen(buf);
for (i = 0; i < n; I++)
{
//往文件fp写入字符buf[i]
int ch = fputc(buf[i], fp);
printf("ch = %c\n", ch);
}
2)读文件
#include <stdio.h>
int fgetc(FILE * stream);
功能:从stream指定的文件中读取一个字符
参数:
stream:文件指针
返回值:
成功:返回读取到的字符
失败:-1
char ch;
#if 0
// EOF表示文件结束符(end of file)。在while循环中以EOF作为文件结束标志
while ((ch = fgetc(fp)) != EOF)
{
printf("%c", ch);
}
printf("\n");
#endif
while (!feof(fp)) //文件没有结束,则执行循环
{
ch = fgetc(fp);
printf("%c", ch);
}
printf("\n");
按照行读写文件fgets、fputs
1)写文件
#include <stdio.h>
int fputs(const char * str, FILE * stream);
功能:将str所指定的字符串写入到stream指定的文件中,字符串结束符 '\0' 不写入文件。
参数:
str:字符串
stream:文件指针
返回值:
成功:0
失败:-1
char *buf[] = { "123456\n", "bbbbbbbbbb\n", "ccccccccccc\n" };
int i = 0;
int n = 3;
for (i = 0; i < n; I++)
{
int len = fputs(buf[i], fp);
printf("len = %d\n", len);
}
2)读文件
#include <stdio.h>
char * fgets(char * str, int size, FILE * stream);
功能:从stream指定的文件内读入字符,保存到str所指定的内存空间,直到出现换行字符、读到文件结尾或是已读了size - 1个字符为止,最后会自动加上字符 '\0' 作为字符串结束。
参数:
str:字符串
size:指定最大读取字符串的长度(size - 1)
stream:文件指针
返回值:
成功:成功读取的字符串
读到文件尾或出错: NULL
char buf[100] = 0;
while (!feof(fp)) //文件没有结束
{
memset(buf, 0, sizeof(buf));
char *p = fgets(buf, sizeof(buf), fp);
if (p != NULL)
{
printf("buf = %s", buf);
}
}
3.按照格式化文件fprintf、fscanf
1)写文件
#include <stdio.h>
int fprintf(FILE * stream, const char * format, ...);
功能:根据参数format字符串来转换并格式化数据,然后将结果输出到stream指定的文件中,指定出现字符串结束符 '\0' 为止。
参数:
stream:已经打开的文件
format:字符串格式,用法和printf()一样
返回值:
成功:实际写入文件的字符个数
失败:-1
fprintf(fp, "%d %d %d\n", 1, 2, 3);
2)读文件
#include <stdio.h>
int fscanf(FILE * stream, const char * format, ...);
功能:从stream指定的文件读取字符串,并根据参数format字符串来转换并格式化数据。
参数:
stream:已经打开的文件
format:字符串格式,用法和scanf()一样
返回值:
成功:参数数目,成功转换的值的个数
失败: - 1
int a = 0;
int b = 0;
int c = 0;
fscanf(fp, "%d %d %d\n", &a, &b, &c);
printf("a = %d, b = %d, c = %d\n", a, b, c);
4.按照块读写文件fread、fwrite
1)写文件
#include <stdio.h>
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式给文件写入内容
参数:
ptr:准备写入文件数据的地址
size: size_t 为 unsigned int类型,此参数指定写入文件内容的块数据大小
nmemb:写入文件的块数,写入文件数据总大小为:size * nmemb
stream:已经打开的文件指针
返回值:
成功:实际成功写入文件数据的块数目,此值和nmemb相等
失败:0
typedef struct Stu
{
char name[50];
int id;
}Stu;
Stu s[3];
int i = 0;
for (i = 0; i < 3; I++)
{
sprintf(s[i].name, "stu%d%d%d", i, i, i);
s[i].id = i + 1;
}
int ret = fwrite(s, sizeof(Stu), 3, fp);
printf("ret = %d\n", ret);
2)读文件
#include <stdio.h>
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
功能:以数据块的方式从文件中读取内容
参数:
ptr:存放读取出来数据的内存空间
size: size_t 为 unsigned int类型,此参数指定读取文件内容的块数据大小
nmemb:读取文件的块数,读取文件数据总大小为:size * nmemb
stream:已经打开的文件指针
返回值:
成功:实际成功读取到内容的块数,如果此值比nmemb小,但大于0,说明读到文件的结尾。
失败:0
typedef struct Stu
{
char name[50];
int id;
}Stu;
Stu s[3];
int ret = fread(s, sizeof(Stu), 3, fp);
printf("ret = %d\n", ret);
int i = 0;
for (i = 0; i < 3; I++)
{
printf("s = %s, %d\n", s[i].name, s[i].id);
}
5.文件缓冲区
ANSI C标准采用“缓冲文件系统”处理数据文件。
所谓缓冲文件系统是指系统自动地在内存区为程序中每一个正在使用的文件开辟一个文件缓冲区从内存向磁盘输出数据必须先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘去。
如果从磁盘向计算机读入数据,则一次从磁盘文件将一批数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(给程序变量) 。
磁盘文件的存取
- 磁盘文件,一般保存在硬盘、U盘等掉电不丢失的磁盘设备中,在需要时调入内存
- 在内存中对文件进行编辑处理后,保存到磁盘中
- 程序与磁盘之间交互,不是立即完成,系统或程序可根据需要设置缓冲区,以提高存取效率
更新缓冲区
#include <stdio.h>
int fflush(FILE *stream);
功能:更新缓冲区,让缓冲区的数据立马写到文件中。
参数:
stream:文件指针
返回值:
成功:0
失败:-1