深入学习C语言
1.数据在内存中的存储
1.1类型的基本分类:
1.整形:(符号)char、(符号)shorrt、(符号)int、(符号)long
char
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]
2.浮点型:float、double
3.构造类型:数组类型、结构体类型 (struct)、 枚举类型 (enum)、联合类型 (union)
4.指针类型:字符指针、整形指针、浮点型指针、void指针…
5.空类型:void 表示空类型(无类型),通常应用于函数的返回类型、函数的参数、指针类型。
Tips:C语言没有字符串类型
1.2整形在内存中的存储
#include <stdio.h>
#include <windows.h>
#include <String.h>
/*
整形在内存中的存储
数据在内存中以二进制的形式存储
对于整形而言:
整数在内存中存储的二进制有3种表示形式:原码、反码、补码
正整数:原码、反码、补码相同,简称三码合一
负整数:原码、反码、补码需要进行计算
原码:即当前数值转换为二进制后的二进制序列
反码:原码的符号位不变,其他位按位取反,得到的就是反码
补码:反码+1,就是补码
整数在内存中存储的是它的补码
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统
一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程
是相同的,不需要额外的硬件电路。
低地址>—————————>——————————>—————————————>——————————高地址
eg:
int a = 0x11223344;这个16进制序列,左边是高位,右边为低位
如果a以大端存储,那么它在内存中的存储方式如下:
低地址—————————.........11 22 33 44 .........———————————高地址
如果a以小端存储,那么它在内存中的存储方式如下:
低地址—————————.........44 33 22 11 .........———————————高地址
大小端存储模式介绍
大端字节序:把数据的低位字节序的内容存储在高地址处,高位字节序的内容存储在低地址处
小端字节序:把数据的低位字节序的内容存储在低地址处,高位字节序的内容存储在高地址处
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元
都对应着一个字节,一个字节为8bit。
但是在C语言中除了8 bit的char之外,还有16 bit的short型,32 bit的long型(要看具体的编译器),
另外,对于位数大于8位的处理器,
例如16位或者32位的处理器,由于寄存器宽度大于一个字节,
那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
例如:一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,
那么 0x11 为高字节, 0x22 为低字节。
对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。
小端模式,刚好相反。我们常用的 X86 结构是小端模式,
而 KEIL C51 则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
*/
int main()
{
/*
-10的原码
00000000 00000000 00000000 00001010 -原码
01111111 11111111 11111111 11110101 -反码
01111111 11111111 11111111 11110110 -补码
*/
int a = -10;
int b = 10;
printf("%d\n", a);
system("pause");
return 0;
}
1.3浮点型在内存中的存储
#include <stdio.h>
#include <windows.h>
#include <String.h>
/*
浮点型在内存中的存储
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
1.(-1)^S * M * 2^E
2.(-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
3.M表示有效数字,大于等于1,小于2。
4.2^E表示指数位。
eg:
浮点数:5.5 十进制->二进制 101.1
754标准形式表示为:1.011 * 2^2 -> (-1) ^0 * 1.011 * 2^2
那么S = 0 ,M = 1.011,E = 2
5.5在内存中的存储
存储前:S=0,M=1.011,E=2
存储后:S=0,M-1=011,E=2+127
内存中存储5.5的二进制序列: 0 1000001 01100000000000000000000
将5.5的二进制转化为十六进制序列:0100 0000 1011 0000 0000 0000 0000 0000 ->40 b0 00 00
IEEE 754规定:
float:对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M。
00000000 00000000 00000000 00000000
|
0 00000000 00000000000000000000000
S E M
double:对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
00000000 00000000 00000000 00000000
|
0 00000000000 00000000000000000000000000000.....(一共52位)
S E M
IEEE 754对有效数字M和指数E,还有一些特别规定。
前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。
IEEE 754规定:
在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。
比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。
这样做的目的,是节省1位有效数字。
以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
至于指数E,情况就比较复杂。
首先,E为一个无符号整数(unsigned int)
这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。
但是,我们知道,科学计数法中的E是可以出现负数的,
所以IEEE 754规定:
存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
指数E从内存中取出还可以再分成三种情况:
1.E不全为0或不全为1
2.E全为0
3.E全为1
*/
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为%f\n", *pFloat);
printf("\n");
*pFloat = 9.0;
printf("num的值:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
return 0;
}
2.指针
- 指针就是个变量,用来存放地址,地址唯一标识一块内存空间。
- 指针的大小是固定的4/8个字节(32位平台/64位平台)。
- 指针是有类型,指针的类型决定了指针的±整数的步长,指针解引用操作的时候的权限。
- 指针的运算。
2.1字符指针
#include <stdio.h>
#include <windows.h>
#include <String.h>
/*
字符指针
*/
int main()
{
char ch = 'q';
char *pch = &ch;
// 本质上是把"hello world"这个字符串的首字符的地址存储在了ps中
char *ps = "hello world";
char arr[] = "hello world";
printf("%c\n", *ps); // h
printf("%s\n", ps); //
printf("%s\n", arr); //
printf("\n");
char str1[] = "hello world.";
char str2[] = "hello world.";
//建议书写方式 const char *str3 = "hello world.";
//建议书写方式 const char *str4 = "hello world.";
char *str3 = "hello world.";//常量字符串
// *str3 = "hello china.";//error,常量字符串不可修改
char *str4 = "hello world.";
if (str1 == str2)
printf("str1 and str2 are same\n");
else
printf("str1 and str2 are not same\n");
if (str3 == str4)
printf("str3 and str4 are same\n");
else
printf("str3 and str4 are not same\n");
system("pause");
return 0;
}
2.2指针数组(数组)
#include <stdio.h>
#include <windows.h>
#include <String.h>
int main()
{
/*
指针数组————存放指针的数组(存放地址)
int *arr1[10]; //整形指针的数组
char *arr2[4]; //一级字符指针的数组
char **arr3[5];//二级字符指针的数组
*/
int a = 10;
int b = 20;
int c = 30;
int *arr[3] = {&a, &b, &c};
for (int i = 0; i < 3; i++)
{
// 遍历*arr指针数组
printf("%d ", *(arr[i]));
}
printf("\n");
printf("\n");
int arr1[] = {1, 2, 3, 4, 5};
int arr2[] = {2, 3, 4, 5, 6};
int arr3[] = {3, 4, 5, 6, 7};
int *arr4[3] = {arr1, arr2, arr3};
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
// arr4[i]:指针数组中第一个指针所指向的数组的地址,
// arr4[i] + j:数组事连续存储,+自然数,跳向下一个元素的地址,解引用得到元素
// printf("%d ",*(arr4[i] + j));
// arr4[i]:代表的就是数组,当成数组使用下标,用法类似二维数组,但不等同于二维数组。底层结构不同。
printf("%d ", arr4[i][j]);
}
printf("\n");
}
system("pause");
return 0;
}
2.3数组指针(指针)
2.3.1数组指针的定义
#include <stdio.h>
#include <windows.h>
#include <String.h>
/*
数组指针 ->指向数组的指针
整形指针 ->指向整形的指针
字符指针 ->指向字符的指针
.....
*/
int main()
{
char c = '光';
char *pc = &c; // 字符指针 ->指向字符的指针
int a = 10;
int *pa = &a; // 整形指针 ->指向整形的指针
int arr[10] = {1, 2, 3, 4, 5};
/*
arr -> 数组名是数组首元素的地址 -> arr[0]的地址
&arr -> 取出是数组的地址
如何理解 int (*parr)[10] =&arr;?
int arr[10] = {1, 2, 3, 4, 5};
*parr:代表的是 这是一个指针,指向arr数组
[10]: *parr 指向的是一个10个元素的数组
int: *parr 指向[10]的这个数组的类型为int
总结:*parr指向[10]的这个arr数组里的10个元素的类型为int
*/
// parr就是一个数组指针,其中存放的是一个数组的地址
int(*parr)[10] = &arr;
double *d[5] = {0.0};
double *(*pd)[5] = &d; // pd是一个数组指针
system("pause");
return 0;
}
2.3.2数组名的不同意义
int main()
{
/*
&数组名 VS 数组名
虽然值一样,但是意义不同
数组名是数组首元素的地址
但是有两个例外:
1.sizeof(数组名) ->数组名表示的是整个数组,计算的是整个数组的大小,单位是字节
2.&arr 这里的arr代表整个数组,取出的是整个数组的地址。
eg: 指针p2指向的就是arr整个数组,而不是arr首元素,只不过数组的值和数组首元素的值相同。
int(*p2)[10] = &arr; //&arr,取出的是数组的地址,是数组类型,存储&arr就是一个数组类型的指针。
int *p1 = arr; // arr代表首元素的地址,是int类型,那么存储arr就是一个int类型的指针
*/
// 虽然他们值一样,但是类型不同,
char c = 'a'; // char
int i = 97; // int
int arr[10] = {0};
printf("%p\n", arr);
printf("%p\n", &arr);
printf("\n");
//p1是一个指针,指向arr数组中的首元素,首元素的类型为int
int *p1 = arr; // arr代表首元素的地址,是int类型,那么存储arr就是一个int类型的指针
// *p2是一个指针,指向arr数组,数组长度为5,数组的类型为int
int(*p2)[10] = &arr; //&arr,取出的是数组的地址,是数组类型,存储&arr就是一个数组类型的指针。
printf("%p\n", p1);
printf("%p\n", p1 + 1); // p1和p1+1相差4 ->int类型的指针,指针+1,跳过的就是指针的类型的大小跳过一个int=4
printf("%p\n", p2);
printf("%p\n", p2 + 1); // p1和p1+1相差40 ->数组类型的指针,指针+1,跳过的就是此数组的存储大小int[10]=40
system("pause");
return 0;
}
2.3.3数组指针的使用
1.一维数组
int main()
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int *p = arr;
// 原写法
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
printf("\n");
int(*pa)[10] = &arr;
// 新写法
for (int i = 0; i < 10; i++)
{
//*pa = arr;解引用pa得到arr数组(首元素的地址)
// *(*(pa)+i):(arr+i)得到数组每个元素的地址,然后解引用,得到数组的每个元素
// tips:这里是数组指针,而不是二级指针。
printf("%d ", *((*pa) + i));
// 这里基本上不会有应用场景,实际操作中,能简单即简单。
}
system("pause");
return 0;
}
2.二维数组
void print1(int arr[3][5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
printf("%d ", arr[i][j]);
}
printf("\n");
}
}
//
void print2(int (*p)[5], int r, int c)
{
for (int i = 0; i < r; i++)
{
for (int j = 0; j < c; j++)
{
// printf("%d ", *(p + i)[i][j]);
printf("%d ", *(*(p + i) + j));
}
printf("\n");
}
}
int main()
{
int arr[3][5] = {{1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}, {3, 4, 5, 6, 7}};
print1(arr, 3, 5);
printf("\n");
// tips:二维数组的首元素是第一行
print2(arr, 3, 5); // 二维数组的数组名,表示的是数组首元素的地址。
/*
int arr[3][5] = {{1, 2, 3, 4, 5}, {2, 3, 4, 5, 6}, {3, 4, 5, 6, 7}};
int(*p)[5] = arr;
二维数组可以理解为数组中的数组
一维数组的数组名是首元素的地址
二维数组的数组名就是第一行
这里的第一行就是代表的就是一个一维数组
int(*p)[5] = arr:这个表达式的内涵就是将一个二维数组的第一行(一维数组)用一个数组指针来接受。
将一个二维数组从内存中方面解析为一个一维数组,那么就会有3个长度为5的一维数组,在内存中连续存储。
在数组章节中的 -> 二维数组在内存中的存储中有讲到。
arr是一个二维数组,二维数组的首元素地址是第一行
第一行的地址其实就是一个一维数组的数组名 because ->在内存存储中,一维、二维数组都是连续存储。
那么可以将这里的第一行理解为一个一维数组。
tips:注意不要混淆,arr实际还是一个二维数组。
*/
int(*p)[5] = arr;
system("pause");
return 0;
}
int main()
{
int arr[5];//长度为5,元素类型为整形的数组 ->整形数组
int *parr1[10];//长度为10,元素类型为整形指针的数组 ->整形指针数组
int(*parr2)[10];//数组指针,该指针指向一个数组,数组有10个元素,每个元素的类型为int
//parr3是一个存放数组指针的数组,该数组能存放10个数组指针,
//每个数组指针指向一个数组,数组存放5个元素,每个元素的类型为int
int(*parr3[10])[5];
}
2.4数组传参和指针传参(略…)
2.5函数指针
#include <stdio.h>
#include <windows.h>
#include <String.h>
int Add(int x, int y)
{
return x + y;
}
int main()
{
/*
函数指针 -> 存放函数地址的指针变量
&函数名 -> 取到的就是函数的地址
语法格式
函数类型 (*指针名)(函数的形参的类型) = &函数名;
*/
// pf 是一个函数指针变量
int (*pf)(int, int) = &Add;
int (*ph)(int, int) = Add;//Add = ph
// &数组名 != 数组名
// &Add == Add ->函数名就是函数的地址。
printf("%p\n", &Add);
printf("%p\n", Add);
/*
*pf == pf:因为pf指向函数的地址,而函数名就是函数的地址
函数的调用方式:函数名(参数);
pf指向函数的地址,那么pf等价于函数名
那么这里的*(解引用操作)只是方便理解,并无实际意义。
tips:只是函数指针中*无实际意义,不适合其他指针。
*/
// 如何使用指针调用函数?三种方式
//第一种方式:
int sum = (*pf)(11, 6);
//第二种方式
int sum1 = pf(12, 6);
//第三种方式:
int sum3 = Add(13,6);
//错误写法
int fault = *pf(5,6);//这里的顺序是,先调用pf函数,参数(为5,6)后的返回值,然后进行解引用。
int plus = (*ph)(1, 6);
int plus1 = ph(1, 6);
printf("sum = %d\n", sum);
printf("sum1 = %d\n", sum);
printf("plus = %d\n", plus);
printf("plus1 = %d\n", plus);
system("pause");
return 0;
}
int main()
{
}
tips:拓展知识《C语言陷阱与缺陷》
int main()
{
/*
理解以下代码
1. (*(void(*)())0)();
2. void (*signal(int,void(*)(int)))(int);
*/
(*(void (*)())0)();
/*
调用0地址处的函数
1. void(*)() -> 函数指针类型
2. void(*)()0 -> 对0进行强制类型转换,被解释为一个函数地址
3. *void(*)()0 -> 对0进行解引用操作
4. (*(void (*)())0)() -> 调用0地址处的函数
*/
//第一种写法
void (*signal(int, void (*)(int)))(int);
//typedef -> 对类型进行重定义
//第二种写法
typedef void(*)(int) pfun_t;//此语法也不通过,需要将pfun_t 移到*的后面
typedef void(*pfun_t)(int);//对void(*)(int)的函数指针类型重命名为pfun_t
pfun_t signal(int,pfun_t);
typedef unsigned int uint;//uint = unsigned int
//以下代码就是以上代码的实际含义,不过语法不通过。
void (*)(int) signal(int,void(*)(int));
/*
1.signal和()结合,说明signal是函数名
2.signal函数的一个参数的类型为int,第二个参数的类型是函数指针
//该函数指针,执行一个参数为int,返回类型为void的函数
3.signal函数的返回类型也是一个函数指针
//该函数指针,指向一个参数为int,返回类型为void的函数
final:signal是一个函数声明
*/
}
2.6函数指针数组
/*
函数指针数组 -> 存放函数指针的数组
int*
int* arr[5]
*/
int main()
{
// pf -> 函数指针
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = Sub;
// pfArr就是函数指针数组
int (*pfArr[2])(int, int) = {Add, Sub}; // 函数指针数组
system("pause");
return 0;
}
2.7指向函数指针数组的指针
int main()
{
// pf -> 函数指针
int (*pf1)(int, int) = Add;
int (*pf2)(int, int) = Sub;
int (*pfArr[2])(int, int) = {Add, Sub}; // 函数指针数组
int (*p)(int, int); // 函数指针
int (*p2[4])(int, int); // 函数指针的数组
//p3 -> 就是一个指向【函数指针的数组】的指针
int (*(*p3)[4])(int, int) = &p2; // 取出的是函数指针数组的地址
system("pause");
return 0;
}
3.自定义类型
3.1结构体
概念:在 C 语言中,char、int、float……等属于系统内置的基本数据类型,往往只能解决简单的问题。当遇到比较复杂的问题时,只使用基本数据类型是难以满足实际开发需求的。因此C 语言允许用户根据实际项目需求,自定义一些数据类型,并且用它们来定义变量。
结构体是一种组合数据类型,由用户自己定义。结构体类型中的元素既可以是基本数据类型,也可以结构体类型。
3.1.1结构体类型的声明
/*
结构体的语法格式:
struct 结构体类型变量名
{
成员变量;
......
......
成员变量列表;
};
*/
//结构体类型的声明
struct Book
{
char name[20];
int price;
char id[12];
} b1, b2, b3; // b1,b2,b3的类型是struct Book,同时b1,b2,b3是全局变量
// 匿名结构体类型,只能使用一次
struct
{
char c;
int a;
double b;
} S;
struct
{
char c;
int a;
double b;
} *ps;
// 结构体的自引用:在结构体类型中是否可以包含本身的成员?
// 不可在结构体内部引用结构体本身。
struct N
{
int d;
struct N identity; // 递归,error
};
// 正确写法
struct Node
{
int d;
struct Node *next; //结构体指针,指向下一个同类型
};
3.1.2结构体变量的定义(声明)与初始化
struct S
{
char c;
int i;
} s1, s2; // 结构体(全局)变量的定义
struct exS
{
double i;
char c;
// 结构体的引用
struct S s;//外部引用
};
int main()
{
// b4,b5,b6是局部变量
// 结构体(局部)变量的声明(定义)
struct S s3;
// 结构体(局部变量)的声明与初始化
struct S s4 = {'s', 20};
struct exS exs1 = {17.7, 'S', {'s', 56}};
// ->
printf("%lf %c %c %d \n", exs1.i, exs1.c, exs1.s.c, exs1.s.i);
system("pause");
return 0;
}
3.2.3结构体内存对齐
#include <stdio.h>
#include <windows.h>
#include <String.h>
/*
结构体的内存对齐
1.结构体的第一个成员
在结构体变量在内存中存储位置的0偏移量位置处,作为起点存在
2.从第2个成员往后的成员,都放在一个对齐数的整数的整数倍的地址处
3.结构体的总大小是结构体的所有成员中对齐数最大,他的对齐数的整数倍。
4.如果有结构体嵌套,嵌套的结构体对齐到自己的最大对齐数的整数倍处,
结构体的整体大小就是所有的对齐数中(含嵌套结构体的对齐数)最大对齐数的整数倍。
tips:
1.对齐数:编译器默认的对齐数与该成员在内存中的存储大小之比,较小的一方
2.VS默认对齐数-8
3.
*/
struct S
{
char c1;// 对齐数1 vs VSCode默认对齐数8
int i; // 对齐数4 vs VSCode默认对齐数8
char c2;//
/*
首先第一个元素占一个字节,且作为起始偏移位置,从0开始
第二个成员占4个字节,对齐数为4,那么int i只能放在4包括4的整数倍的地址处,从0开始找到4及4整数倍的位置往后分配4个字节
第三个成员同上,占一个字节。
char c1在0的位置,因为int i从4的整数倍开始往后分配4个字节那么4 5 6 7则是存储int i的位置,8则是存储c2的位置
没调整之前内存方面...........0 1 2 3 4 5 6 7 8.....................
因为结构体的总大小是结构体的所有成员中对齐数最大,他的对齐数的整数倍,
所以整个成员中int i的对齐数最大为4,9不是4的整数倍,调整为12,则struct s的内存大小为12
调整之后的内存方面...........0 1 2 3 4 5 6 7 8 9 10 11 12............
*/
};
struct S2
{
char c1;
int i;
double d;
};
struct S3
{
char c1;
char c2;
int i;
};
struct S4
{
double d;
char c;
int i;
};
struct S5
{
char c1;
struct S4 s4;
double d;
};
int main()
{
struct S s = {0};
struct S2 s2 = {0};
struct S3 s3 = {0};
struct S4 s4 = {0};
struct S5 s5 = {0};
printf("%d\n", sizeof(s)); // 12
printf("%d\n", sizeof(s2)); // 16
printf("%d\n", sizeof(s3)); // 8
printf("%d\n", sizeof(s4)); // 16
printf("%d\n", sizeof(s5)); // 32
printf("\n");
system("pause");
return 0;
}
3.2枚举
枚举类型的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨。
- 防止了命名污染(封装)
- 便于调试
- 使用方便,一次可以定义多个常量
枚举类型使用
#include <stdio.h>
#include <windows.h>
#include <String.h>
enum Day
{
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
};
// 声明枚举类型
enum Color
{
/*
枚举常量,默认值为0,往下递增1
枚举常量可以初始化默认值,其后如果不赋值,则默认往下递增1
枚举类型的成员变量是存储在内存空间为int类型
tips:,常量不能修改
*/
Red, // 0
Green, // 1
Blue // 2
};
enum changeColor
{
pink = 8,
black,
white
};
int main()
{
// 创建枚举类型变量
// 不要直接将其代表的默认值直接赋值给枚举类型变量,他实际还是枚举类型,int类型无法转换为枚举类型
enum Color c1 = Red;
enum Color c2 = Green;
enum Color c3 = Blue;
printf("%d\n", Red);
printf("%d\n", Green);
printf("%d\n", Blue);
printf("\n");
printf("%d\n", pink); // 8
printf("%d\n", black); // 9
printf("%d\n", white); // 10
printf("\n");
enum Color c = Red;
enum Day d = Monday;
enum changeColor cc = pink;
// 枚举类型的大小的计算
printf("%d\n", sizeof(c));//4
printf("%d\n", sizeof(d));//4
printf("%d\n", sizeof(cc));//4
printf("\n");
system("pause");
return 0;
}
3.3联合体(共用体)
#include <stdio.h>
#include <windows.h>
#include <String.h>
// 联合体(共用体)的声明
union Un
{
/*
联合的成员是共用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联
合至少得有能力保存最大的那个成员)。
*/
char c; // 1
int i; // 4
};
union Un1
{
/*
联合体的大小计算
1.联合体的大小至少是最大成员的大小
2.当最大成员大小不是最大对齐数的整数倍的时候,要对齐到最大对齐数的整数倍。
*/
char ch[5];
int i;
};
int main()
{
// 联合体(共用体)变量的声明
union Un u;
// 联合体(共用体)变量的声明与初始化
union Un un = {10};
printf("%p\n", &u);//
printf("%p\n", &(u.c));//
printf("%p\n", &(u.i));//
// u所占空间的大小
printf("%d\n", sizeof(u)); // 4
union Un1 un1;
printf("%d\n", sizeof(un1)); // 8
printf("\n");
system("pause");
return 0;
}
4.动态内存管理
4.1动态内存分配
#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <errno.h>
int main()
{
/*
默认开辟空间方式
1. 空间开辟大小是固定的。
2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,
那数组的编译时开辟空间的方式就不能满足了。
动态内存开辟诞生~
*/
int val = 10; // 在栈空间上开辟四个字节4个字节
int arr[10]; // 在栈空间上开辟了40个字节的连续空间
// 动态内存开辟
int *p = (int *)malloc(40); // 开辟40个字节
// 动态开辟内存可能失败
int *p1 = (int *)malloc(INT_MAX); // 开辟40个字节
if (p == NULL)
{
// 需要引入头文件 #include <errno.h> <string.h>
printf("%s\n", strerror(errno));
// C语言的默认风格:正常返回0,错误返回1
return 1;
}
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//没有free,并不代表空间不回收
//当程序退出的时候,系统会自动回收空间
printf("\n");
system("pause");
return 0;
}
4.2动态内存函数的介绍
4.2.1 malloc 和 free
#include <stdio.h>
#include <windows.h>
#include <String.h>
/*
两个C语言的函数:malloc()和free()
函数原型 void* malloc(size_t size);
作用:这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
使用方法:
1.如果开辟成功,则返回一个指向开辟好空间的指针。
2.如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
3.返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己
来决定。
4.如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
函数原型 void free (void* ptr);
作用:是用来做动态内存的释放和回收的
使用方法:
1.如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
2.如果参数 ptr 是NULL指针,则函数什么事都不做。
tips:malloc()和free()都声明在 stdlib.h 头文件中。
*/
int main()
{
// 动态内存开辟
int *p = (int *)malloc(40); // 开辟40个字节
// 动态开辟内存可能失败
// int *p1 = (int *)malloc(INT_MAX);
if (p == NULL)
{
// 需要引入头文件 #include <errno.h> <string.h>
printf("%s\n", strerror(errno));
return 1;
}
for (int i = 0; i < 10; i++)
{
*(p + i) = i;
}
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
free(p);
p = NULL;
printf("\n");
system("pause");
return 0;
}
4.2.2 calloc
#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <errno.h>
/*
C语言函数:calloc()
函数原型:void* calloc (size_t num, size_t size);
作用:calloc 函数也用来动态内存分配
1.函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
2.与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
*/
int main()
{
//开辟10个整型的空间,内容初始化为0
int* p = (int*)calloc(10,sizeof(int));
if(p == NULL)
{
printf("%s\n",strerror(errno));
return 1;
}
//打印
int i = 0;
for(i = 0;i<10;i++)
{
printf("%d ",*(p+i));
}
//释放
free(p);
printf("\n");
system("pause");
return 0;
}
4.2.3 realloc
#include <stdio.h>
#include <windows.h>
#include <String.h>
/*
C语言函数 realloc
void* realloc (void* ptr, size_t size);
1.ptr 是要调整的内存地址
2.size 调整之后新大小
3.返回值为调整之后的内存起始位置。
4.这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。
5.realloc在调整内存空间的是存在两种情况:
1.原有空间之后有足够大的空间
2.原有空间之后没有足够大的空间
两种情况具体细节
1.当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
2.当是情况2 的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小
的连续空间来使用。这样函数返回的是一个新的内存地址。
*/
int main()
{
int *p = (int *)malloc(40);
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
// 使用
for (int i = 0; i < 10; i++)
{
*(p + i) = i + 1;
}
// 扩容
int *ptr = (int*)realloc(p, 80);
if (ptr != NULL)
{
p = ptr;
}
// 使用
for (int i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//使用完释放空间
free(p);
//释放完空间后,指针置NULL
p = NULL;
printf("\n");
system("pause");
return 0;
}
4.3常见的动态内存错误
#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <stdlib.h>
/*
动态内存开辟常见的错误
1.对NULL指针的解引用操作
2.对动态开辟空间的越界访问
3.对非动态开辟内存使用free释放
4.使用free释放一块动态开辟内存的一部分
5.对同一块动态内存多次释放
6.动态开辟内存忘记释放(内存泄漏)
*/
int main()
{
// 1.对NULL指针的解引用操作
int *p = (int *)malloc(10000000000);
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
return 0;
}
int main()
{
// 2.对动态开辟空间的越界访问
int *p = (int *)malloc(10 * sizeof(int)); // 开辟了40个字节的空间
if (p == NULL)
{
return 1;
}
// 以为开辟了40个元素
for (int i = 0; i < 40; i++)
P
{
*(p + i) = i;
}
return 0;
}
int main()
{
// 3.对非动态开辟内存使用free释放
int arr[10] = {0}; // 栈区
int *p = arr;
// 使用free函数,需要引入头文件#include <stdlib.h>
free(p); // 使用free释放非动态开辟的空间
p = NULL;
return 0;
}
int main()
{
// 4.使用free释放一块动态开辟内存的一部分
int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL)
{
return 1;
}
for (int i = 0; i < 5; i++)
{
*p++ = i;
}
free(p);
p = NULL;
return 0;
}
int main()
{
// 5.对同一块动态内存多次释放
int *p = (int *)malloc(100);
// 使用...
// 释放....
free(p);
// 再一次释放
free(p);
return 0;
}
void test()
{
/*
动态开辟的空间,2种回收方式
1.主动free
2.程序结束
*/
int *p = (int *)malloc(100);//p指针销毁,失去指向mallor动态开辟的空间,内存泄漏
if (p == NULL)
{
return 1;
}
//使用
}
int main()
{
// 6.动态开辟内存忘记释放(内存泄漏)
test();//内存泄漏
//.....
return 0;
}
5.柔性数组
#include <stdio.h>
#include <windows.h>
#include <String.h>
/*
柔性数组
C99 中,结构体中的最后一个元素允许是未知大小的数组,这就叫做『柔性数组』成员。
柔性数组的特点
1.结构中的柔性数组成员前面必须至少一个其他成员。
2.sizeof 返回的这种结构大小不包括柔性数组的内存。
3.包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小。
*/
struct S
{
int n;
int arr[]; // 大小未知,柔性数组成员
};
struct S1
{
int n;
int arr[0]; // 大小未知,柔性数组成员
};
int main()
{
// 期望arr的大小为10个整形
struct S *ps = (struct S *)mallor(sizeof(struct S) + 10 * sizeof(int));
ps->n = 10;
for (int i = 0; i < 10; i++)
{
ps->arr[i] = i;
}
// 增加大小
struct S *ptr = (struct S *)realloc(ps, sizeof(struct S) + 20 * sizeof(int));
if (ptr != NUll)
{
ps = ptr;
}
//使用
//使用完释放。
free(ps);
ps = NULL;
// 这种创建方式,arr柔性数组无法使用
struct S s = {0};
printf("%s\n", sizeof(s)); // 4
printf("\n");
system("pause");
return 0;
}
6.文件
文件的类型
- 一般在程序设计中的文件类型有两种:程序文件、数据文件。
- 程序文件:包括源程序文件(.c)、目标文件(.obj)、可执行文件(.exe
- 数据文件:该文件的内容不是程序,而是程序运行时读写的数据。要操作的文件
6.1文件指针
缓冲文件系统中,关键的概念是“文件类型指针”,简称“文件指针”。
每个被使用的文件都在内存中开辟了一个相应的文件信息区,用来存放文件的相关信息(如文件的名字,文件状态及文件当前的位置等)。这些信息是保存在一个结构体变量中的。该结构体类型是有系统声明的,取名FILE.
不同的C编译器的FILE类型包含的内容不完全相同,但是大同小异。
每当打开一个文件的时候,系统会根据文件的情况自动创建一个FILE结构的变量,并填充其中的信息,使用者不必关心细节。
FILE* pf;//文件指针变量
一般都是通过一个FILE的指针来维护这个FILE结构的变量,这样使用起来更加方便。定义pf是一个指向FILE类型数据的指针变量。可以使pf指向某个文件的文件信息区(是一个结构体变量)。通过该文件信息区中的信息就能够访问该文件。也就是说,通过文件指针变量能够找到与它关联的文件。
6.2文件的打开与关闭
文件在读写之前应该先打开文件,在使用结束之后应该关闭文件。
在编写程序的时候,在打开文件的同时,都会返回一个FILE*的指针变量指向该文件,也相当于建立了指针和文件的关系。
ANSIC 规定使用fopen()函数来打开文件,fclose()来关闭文件。
#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <errno.h>
/*
文件的打开与关闭
文件打开函数 fopen() 和文件关闭函数 fclose()
函数原型:FILE * fopen ( const char * filename, const char * mode );
第一个参数:文件路径,第二个参数:文件的打开方式
函数原型:int fclose ( FILE * stream );
参数:文件指针
*/
int main()
{
// 文件路径注意转义字符
// 打开文件
FILE *pf = fopen("C:\\Users\\12002\\Desktop\\x.txt ", "r");
if (pf != NULL)
{
printf("文件打开成功\n");
}
else
{
perror("fopen");
}
// 关闭文件
fclose(pf);
pf = NULL;
printf("\n");
system("pause");
return 0;
}
6.3文件的顺序读写
6.3.1字符的输入、输出函数
#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <errno.h>
int main()
{
// 文件路径注意转义字符
FILE *pf = fopen("C:\\Users\\12002\\Desktop\\x.txt ", "w");
if (pf == NULL)
{
perror("fopen");
}
//文件的写入
fputc('x', pf);
fputc('z', pf);
/*
C语言程序运行起来默认启动3个流
stdin:标准输入流 --键盘
stdout:标准输入流 --屏幕
stderr:标准输出流 --屏幕
tips:三个流都是FILE *类型下
*/
//向屏幕写入x,z
fputc('x', stdout);
fputc('z', stdout);
// 关闭文件
fclose(pf);
pf = NULL;
printf("\n");
system("pause");
return 0;
}
#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <errno.h>
// fgetc从标准输入流读取
void readOfKeyBoard(FILE *stream)
{
int ret = fgetc(stdin);
printf("%c", ret);
ret = fgetc(stream);
printf("%c\n", ret);
ret = fgetc(stream);
printf("%c\n", ret);
}
int main()
{
// 文件路径注意转义字符
FILE *pf = fopen("C:\\Users\\12002\\Desktop\\x.txt ", "r");
if (pf == NULL)
{
perror("fopen");
}
// 使用 fgetc()进行文件的读取
int ret = fgetc(pf);
printf("%c", ret);
ret = fgetc(pf);
printf("%c\n", ret);
//从键盘读取到屏幕
readOfKeyBoard(pf);
// 关闭文件
fclose(pf);
pf = NULL;
printf("\n");
system("pause");
return 0;
}
6.3.2文本行的输入、输出函数
#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <errno.h>
int main()
{
// 文件路径注意转义字符
// FILE *pf = fopen("C:\\Users\\12002\\Desktop\\x.txt ", "w");
//
FILE *pf = fopen("C:\\Users\\12002\\Desktop\\x.txt ", "r");
if (pf == NULL)
{
perror("fopen");
}
/*
char *fgets(char*string,int n,FILE*stream);
第一个参数:读取的字符串放入的字符指针
第二个参数:读取的字符个数
第三个参数:你要读取的文件
char *fputs(const char*string,FILE*stream);
第一个参数:你要写入的字符串,第二个参数:写入的文件指针。
*/
// 文件的写入一行字符
// fputs("welcome to the China\n", pf);
// fputs("please enjoy your trip\n", pf);
// 文件读取一行字符
char arr[30] = {0};
fgets(arr, 30, pf);//这里的30只读取29个字符,最后一个位置留给\0
printf("%s", arr);
fgets(arr, 30, pf);
printf("%s", arr);
// 关闭文件
fclose(pf);
pf = NULL;
printf("\n");
system("pause");
return 0;
}#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <errno.h>
int main()
{
// 文件路径注意转义字符
// FILE *pf = fopen("C:\\Users\\12002\\Desktop\\x.txt ", "w");
//
FILE *pf = fopen("C:\\Users\\12002\\Desktop\\x.txt ", "r");
if (pf == NULL)
{
perror("fopen");
}
/*
char *fgets(char*string,int n,FILE*stream);
第一个参数:读取的字符串放入的字符指针
第二个参数:读取的字符个数
第三个参数:你要读取的文件
char *fputs(const char*string,FILE*stream);
第一个参数:你要写入的字符串,第二个参数:写入的文件指针。
*/
// 文件的写入一行字符
// fputs("welcome to the China\n", pf);
// fputs("please enjoy your trip\n", pf);
// 文件读取一行字符
char arr[30] = {0};
fgets(arr, 30, pf);//这里的30只读取29个字符,最后一个位置留给\0
printf("%s", arr);
fgets(arr, 30, pf);
printf("%s", arr);
// 关闭文件
fclose(pf);
pf = NULL;
printf("\n");
system("pause");
return 0;
}
6.3.3格式化的输入、输出函数
#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <errno.h>
struct S
{
char arr[10];
char symbol[40];
int age;
};
struct S1
{
char arr[10];
char symbol[40];
int age;
};
int main()
{
// 声明结构体变量并初始化
struct S s = {"艾斯", "罗杰之子,白胡子二番队队长,烧烧果实能力者", 20};
// 对格式化的数据进行文件操作
// 打开文件
// FILE *pf = (FILE *)fopen("C:\\Users\\12002\\Desktop\\xz.txt", "w");
FILE *pf = (FILE *)fopen("C:\\Users\\12002\\Desktop\\xz.txt", "r");
if (pf == NULL)
{
perror("fopen");
}
/*
输入函数原型:int fprintf(FILE*stream,const char*format[,argument]....);
输出函数原型:int fscanf(FILE*stream,const char*format[,argument]....)
*/
// 写入数据到文件
// fprintf(pf, "%s %s %d", s.arr, s.symbol, s.age);
// 读取文件中的数据并还原
struct S1 s1 = {0};
fscanf(pf, "%s %s %d", s.arr, s.symbol, &(s.age));
//打印S1
printf("%s,%s,年龄:%d",s.arr,s.symbol,s.age);
printf("\n");
system("pause");
return 0;
}
6.3.4二进制的输入、输出函数
#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <errno.h>
struct S
{
char arr[10];
int num;
float sc;
};
//写
int main()
{
struct S s = {"abcdef", 10, 5.5f};
FILE *pf = fopen("C:\\Users\\12002\\Desktop\\x.txt ", "w");
if (pf == NULL)
{
perror("fopen");
}
// 二进制方式写文件
fwrite(&s, sizeof(struct S), 1, pf);
// 关闭文件
fclose(pf);
pf = NULL;
printf("\n");
system("pause");
return 0;
}
/////////////////////////////////////////////////////////////////////
struct S
{
char arr[10];
int num;
float sc;
};
//读
int main()
{
struct S s = {"abcdef", 10, 5.5f};
FILE *pf = fopen("C:\\Users\\12002\\Desktop\\x.txt ", "r");
if (pf == NULL)
{
perror("fopen");
}
// 二进制方式读文件
fread(&s, sizeof(struct S), 1, pf);
printf("%s %d %f", s.arr, s.num, s.sc);
// 关闭文件
fclose(pf);
pf = NULL;
printf("\n");
system("pause");
return 0;
}
6.4文件的随机读写
fseek、ftell、rewind
#include <stdio.h>
#include <windows.h>
#include <String.h>
#include <errno.h>
int main()
{
/*
根据文件指针的位置和偏移量来定位文件指针
int fseek ( FILE * stream, long int offset, int origin );
long int offset:偏移量
int origin:偏移的起始位置。
int origin有三个取值:
1.SEEK_CUR:current position of pointer ->以当前文件指针的位置开始偏移
2.SEEK_END:end of file ->以文件的末尾开始偏移
3.SEEK_SET:beginning of file ->以文件的起始位置开始偏移
*/
FILE *pf = fopen("C:\\Users\\12002\\Desktop\\test.txt ", "r");
if (pf == NULL)
{
perror("fopen");
}
// 读取文件
int ch = fgetc(pf);
printf("%c\n", ch); // a
// 调整文件指针
// 含义:根据SEEK_CUR,即读取当前指针的位置偏移量-1位置的字符,
ch = fseek(pf, -1, SEEK_CUR);
printf("%c\n", ch); // a
ch = fgetc(pf);
printf("%c\n", ch); // b
ch = fgetc(pf);
printf("%c\n", ch); // c
/*
ftell
返回文件指针相对于起始位置的偏移量
long int ftell ( FILE * stream );
*/
// 返回文件指针相对于起始位置的偏移量
int moved = ftell(pf);
/*
rewind
让文件指针的位置回到文件的起始位置
void rewind ( FILE * stream );
*/
// 让文件指针的位置回到文件的起始位置
rewind(pf);
int ch = fgetc(pf);
printf("%c\n", ch); // a
// 关闭文件
fclose(pf);
pf = NULL;
printf("\n");
system("pause");
return 0;
}
6.5文本文件和二进制文件
根据数据的组织形式,数据文件被称为文本文件或者二进制文件。
数据在内存中以二进制的形式存储,如果不加转换的输出到外存,就是二进制文件。
如果要求在外存上以ASCII码的形式存储,则需要在存储前转换。以ASCII字符的形式存储的文件就是文
本文件。
一个数据在内存中是怎么存储的呢?
字符一律以ASCII形式存储,数值型数据既可以用ASCII形式存储,也可以使用二进制形式存储。
6.6文件结束的判定
tips:牢记:在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束
而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束
1.文本文件读取是否结束,判断返回值是否为 EOF ( fgetc ),或者 NULL ( fgets )
例如:
- fgetc 判断是否为 EOF .
- fgets 判断返回值是否为 NULL .
2.二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。
- 例如:fread判断返回值是否小于实际要读的个数。
6.6文件缓冲区
ANSIC 标准采用缓冲文件系统处理的数据文件的,所谓缓冲文件系统是指系统自动地在内存中为程序中每一个正在使用的文件开辟一块“文件缓冲区”。从内存向磁盘输出数据会先送到内存中的缓冲区,装满缓冲区后才一起送到磁盘上。如果从磁盘向计算机读入数据,则从磁盘文件中读取数据输入到内存缓冲区(充满缓冲区),然后再从缓冲区逐个地将数据送到程序数据区(程序变量等)。缓冲区的大小根据C编译系统决定的。
结论:因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。如果不做,可能导致读写文件的问题。