主页:醋溜马桶圈-CSDN博客
目录
1.数据在内存中的存储
1.1 数据类型
前面我们已经学习了基本的内置类型
char //字符数据类型
short //短整型
int //整形
long //长整型
long long //更长的整形
float //单精度浮点数
double //双精度浮点数
以及他们所占存储空间的大小
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了使用范围)
- 如何看待内存空间的视角
整型家族:
char //字符在内存中存储的是字符的ASCII值
//ASCII值是整型,所以字符类型归到整型家族
unsigned char
signed char
short
unsigned short [int]
signed short [int]
int
unsigned int
signed int
long
unsigned long [int]
signed long [int]
//unsigned 无符号的
//signed 有符号的
char是否有signed char
C语言标准并没有规定,取决于编译器
浮点型家族:
float
double
构造类型:
构造类型也叫做自定义类型
> 数组类型
> 结构体类型 struct
> 枚举类型 enum
> 联合类型 union
指针类型:
int* pi;
char* pc;
float* pf;
void* pv;
空类型:
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型
void test(void)
{
}
(void)表示这个函数没有参数,void 表示函数不会返回任何值
1.2 整型在内存中的存储
变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的
计算机能够处理的是二进制的数据
整型和浮点型数据在内存中也都是以二进制的形式进行存储的
下来了解下面的概念:
1.2.1原码、反码、补码
计算机中的整数有三种2进制表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”
- 正数的原、反、补码都相同
- 负整数的三种表示方法各不相同
原码
直接将数值按照正负数的形式翻译成二进制就可以得到原码反码
将原码的符号位不变,其他位依次按位取反就可以得到反码补码
反码+1就得到补码
对于整形来说:数据存放内存中其实存放的是补码
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统
一处理
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程 是相同的,不需要额外的硬件电路
我们看看在内存中的存储:
1.2.2 大小端介绍
1.2.2.1 什么是大端小端
- 大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中
- 小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中
1.2.2.2 为什么会有大端和小端
为什么会有大小端模式之分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元 都对应着一个字节,一个字节为8 bit。但是在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处理器还可以由硬件来选择是大端模式还是小端模式。
1.3 字符型在内存中的存储
char类型占1个字节,8bit
1.4 浮点型在内存中的存储
常见的浮点数
3.14159
1E10
浮点数家族包括: float、double、long double 类型
浮点数表示的范围:float.h中定义
浮点数存储的例子
int main()
{
int n = 9;
float *pFloat = (float *)&n;
printf("n的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n",n);
printf("*pFloat的值为:%f\n",*pFloat);
return 0;
}
他的结果是:
1.4.1 浮点数存储规则
num 和 *pFloat 在内存中明明是同一个数,为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法
详细解读:
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
- (-1)^S * M * 2^E
- (-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数
- M表示有效数字,大于等于1,小于2
- 2^E表示指数位
举例来说:
- 十进制的5.0,写成二进制是 101.0 ,相当于 1.01×2^2
那么,按照上面V的格式,可以得出S=0,M=1.01,E=2
- 十进制的-5.0,写成二进制是 -101.0 ,相当于 -1.01×2^2 。那么,S=1,M=1.01,E=2
1.4.2 IEEE754规定
对于32位的浮点数,最高的1位是符号位s,接着的8位是指数E,剩下的23位为有效数字M
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字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从内存中取出还可以再分成三种情况:
E不全为0或不全为1
这时,浮点数就采用下面的规则表示,即指数E的计算值减去127(或1023),得到真实值,再将 有效数字M前加上第一位的1。
比如:
0.5(1/2)的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移1位,则1.0*2^(-1),其阶码为-1+127=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:0 01111110 00000000000000000000000
E全为0
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
E全为1
这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)
2.数组
2.1 一维数组
类如arr[10];
数组是一组相同类型元素的集合
2.1.1 一维数组的创建
type_t arr_name [ const_n ]
//type_t 是指数组的元素类型
//const_n 是一个常量表达式,用来指定数组的大小
//示例1
int arr1[10];
//示例2
int count = 10;
int arr2[count];
//示例3
char arr3[10];
float arr4[1];
double arr5[20];
特殊情况,用变量指定数组的大小行不行呢?
int n = 0;
scanf("%d",&n);
itn arr[n];
- 在C99之前,数组只能是常量指定大小
- 在C99之后,引入了变长数组的概念,数组的大小是可以用变量指定的,但是数组不能初始化
“但是VS中是不支持的”
2.1.2 一维数组的初始化
数组的初始化是指,在创建数组的同时给数组的内容一些合理初始值(初始化)
看代码:
int arr1[10] = { 1,2,3 };
int arr2[] = { 1,2,3,4 };
int arr3[5] = { 1,2,3,4,5 };
char arr4[3] = { 'a',98,'c'};
char arr5[] = { 'a','b','c'};
char arr6[] = "abcdef";
这些数组的初始化都是合法的
int arr1[10] = { 1,2,3 };
这种叫做//不完全初始化,剩余的元素默认初始化为0
我们使用监视窗口可以发现
数组在创建的时候如果想不指定数组的确定的大小就得初始化。数组的元素个数根据初始化的内容来确定。
但是对于下面的代码要区分,内存中如何分配。
char arr1[] = "abc";
char arr2[3] = { 'a','b','c' };
利用监视窗口我们可以看到,未指定大小的数组大小是初始化的内容+1,指定大小的数组大小则是指定的大小
2.1.3 一维数组的使用
对于数组的使用,我们之前介绍了以一个操作符:[ ] ,下标引用操作符。它其实就是数组访问的操作符。
我们来看代码:
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d", arr[6]);
return 0;
}
由于数组的下标是从0开始的,所以我们想要引用数组中的第七个数字时,我们用的下标应该是6
因此,引用数组第n个元素时,下标应该是n-1;
2.1.4 一维数组在内存中的储存
看代码
int main() {
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++) {
printf("&arr[%d]=%p\n", i,&arr[i]);
}
return 0;
}
由于一个整型元素占4个字节的内存,内存中我们说一个字节给一个地址,所以两个元素差四个字节,他们的地址也就差4
- 数组在内存中是连续存放的
- 随着下标的增长,地址是由低到高变化的
2.2 二维数组
2.2.1 二维数组的创建
//数组创建
int arr[3][4];
char arr[3][5];
double arr[2][4];
2.2.2 二维数组的初始化
//数组初始化
int arr[3][4]={1,2,3,4}; //三行四列
int arr[3][4]={{1,2},{4,5}}; //第一行{1,2} 第二行{4,5}
int arr[][4]={{2,3},{4,5}};
//二维数组如果有初始化,行可以省略,列不能省略
2.2.2 二维数组的使用
二维数组使用也是通过下标的方式
这是一个三行四列的数组,他的元素是这样排布的
2.2.3 二维数组在内存中的储存
看代码
int main() {
int arr[3][5] = { {1,2},{4,5},{6,7,8} };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++) {
for (j = 0; j < 5; j++) {
printf("&arr[%d][%d]=%p\n", i,j, &arr[i][j]);
}
}
return 0;
}
其实,它的存储是这样的
二维数组在内存中也是连续存放的
2.3 数组越界
数组的下标是有范围限制的
数组的下标规定是从0开始的,如果数组有n个元素,最后一个元素的下标就是n-1
所以,数组的下标如果小于0,或者大于n-1,就是数组越界访问了,超出了数组合法空间的访问
C语言本身是不做数组下标的越界检查,编译器也不一定报错,但是编译器不报错,并不意味着程序就是正确的
所以程序员写代码时,最好自己做越界的检查
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for (i = 0; i <= 10; i++) {
printf("%d\n",arr[i]);//当i=10的时候,越界访问了
}
return 0;
}
//当i=10的时候,越界访问了
二维数组的行和列也可能存在越界
2.4 数组作为函数参数
2.4.1 数组元素作为函数参数
数组可以作为函数的参数使用,进行数据传送。
数组用作函数参数有两种形式
- 一种是把数组元素(下标变量)作为实参使用
- 另一种是把数组名作为函数的形参和实参使用
数组元素作函数实参
数组元素就是下标变量,它与普通变量并无区别,因此它作为函数实参使用与普通变量是完全相同的,在发生函数调用时,把作为实参的数组元素的值传送给形参,实现单向的值传送
2.4.2 数组名作为函数参数
用数组名作函数参数与用数组元素作实参有几点不同:
- 对数组元素的处理是按普通变量对待的,用数组名作函数参数时,则要求形参和相对应的实参都必须是类型相同的数组,都必须有明确的数组说明
- 普通变量或下标变量作函数参数时,形参变量和实参变量是由编译系统分配的两个不同的内存单元
- 在函数调用时发生的值传送是把实参变量的值赋予形参变量
- 数组名作函数参数时所进行的传送只是地址的传送,也就是说把实参数组的首地址赋予形参数组名。形参数组名取得该首地址之后,也就等于有了实在的数组
2.5 柔性数组
在C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员
限制条件是:
- 结构体中
- 最后一个成员
- 未知大小的数组
2.5.1 柔性数组的形式
那么我们怎样写一个柔性数组呢
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
有些编译器会报错无法编译,可以改成:
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
2.5.2 柔性数组的特点
- 结构中的柔性数组成员前面必须至少一个其他成员
- sizeof返回的这种结构大小不包括柔性数组的内存
- 包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小
例如:
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
printf("%d\n", sizeof(type_a));//输出结果是4
2.5.3 柔性数组的优势
第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,在里面做了二次内存分配并把整个结构体返回给用户,用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以我们把结构体的内存及其成员需要的内存一次性分配好,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存释放掉
第二个好处是:有利于访问速度
连续的内存有益于提高访问速度,也有益于减少内存碎片
3.结构体
3.1 结构体类型的声明
3.1.1 结构体的基础知识
结构是一些值的集合,这些值称为成员变量。结构的每个成员可以是不同类型的变量
3.1.2 结构的声明
struct tag
{
member - list;
}variable - list;
- struct是结构体关键字,不能省略
- tag是名字,可以自己设定
假设要描述一个学生Student
struct Student
{
char name[20];
int age;
char sex[5];
float score;
}s1,s2,s3;//s1,s2,s3是三个结构体变量
int main()
{
struct Student s4, s5, s6;//s4,s5,s6也是三个结构体变量
return 0;
}
区别在于:
- s1,s2,s3是全局变量
- s4,s5,s6是局部变量
3.1.3 特殊的声明
在声明结构体的时候,可以不完全声明
(省略tag标签,在末尾分号前定义一个变量,只可以使用一次,称为匿名结构体类型)
3.2 结构的自引用
我们先有一个数据结构的概念:
数据结构描述的是数据在内存中的存储和组织结构
在结构中包含一个类型为该结构本身的成员
正确的自引用方式:
struct Node
{
int data;
struct Node* next;
};
结构体里包含一个同类型的结构体是不行的
但是结构体里包含一个同类型的结构体指针是可以的
这个时候匿名就是不行的,需要一个完整的结构体类型
3.3 结构体变量的定义和初始化
有了结构体类型,那如何定义变量就很简单了
struct Point
{
int x;
int y;
}p1; //声明类型的同时定义变量p1
struct Point p2 = { 4,5 };//定义结构体变量p2
//初始化:定义变量的同时赋初值
struct Ponit p3;
这几种定义方法都是可行的
对于复杂结构体可以用大括号初始化
struct Stu
{
char name[20];
int age;
};
int main()
{
struct Stu s = { "张三",20 };
return 0;
}
结构体也可以嵌套初始化
3.4 结构体内存对齐
我们先看一个例子:
我们计算一下S1和S2的大小,他们定义的时候成员变量的顺序不同
这里存在一个结构体内存对齐的问题
我们介绍一个知识:
offsetof //这是一个宏,可以直接使用
//计算结构体成员相较于起始位置的偏移量的
3.4.1 怎么对齐的
结构体的对齐规则:
- 第一个成员变量在与结构体变量偏移量为0的地址处
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
- 结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
对齐数=编译器默认的一个对齐数 与 该成员大小的较小值
- vs中默认的值为8
- Linux中没有默认对齐数,对齐数就是成员自身的大小
下面这篇文章详细的解释了结构体内存对齐规则
3.4.2 为什么要对齐
大部分的参考资料都是这样解释的:
- 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常- 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐
原因在于,为了访问未对齐的内存,处理器需要做两次内存访问;而对齐的内存访问仅需要一次访问
总的来说:
结构体的内存对齐是拿空间来换取时间的做法
在设计结构体的时候,我们既要满足对齐,又要节省空间:
就需要让占用空间小的成员尽量集中在一起
3.4.3 修改默认对齐数
#pragma pack(N)
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”
3.5 结构体成员访问
3.5.1 结构体变量访问成员
结构体变量的成员是通过点操作符(.)访问的,点操作符接受两个操作数
- 结构体变量 . 变量名
例如:
我们可以看到s有成员name和age;
那我们该如何访问s的成员?
struct S s;
strcpy(s,name, "zhangsan");//使用.访问name成员
s.age = 20;//使用.访问age成员
3.5.2 结构体指针访问指针变量的成员
有时候我们得到的不是一个结构体变量,而是指向一个结构体的指针
那该如何访问成员,如下
- 结构体指针->成员名
3.6 结构体传参
3.6.1 传值调用
3.6.2 传地址调用
函数传参的时候,参数是需要压栈的,会有时间和空间上的系统开销
如果传递一个结构体对象的时候,结构体过大,参数压栈的系统开销比较大,会导致性能的下降
结构体传参的时候,尽量传结构体的地址
3.7 结构体实现位段(位段的填充&可移植性)
3.7.1 什么是位段
结构体下来就得了解一下结构体实现位段的能力
位段的出现就是为了节省空间
位段的声明和结构是类似的,有两个不同:
- 位段的成员必须是int,unsigned int 或 signed int 也可以是char类型
- 位段的成员名后面有一个冒号和一个数字
举个例子
struct A
{
int _a : 2;
int _b : 5;
int _c : 10;
int _d : 30;
};
这就是一个位段
位段的位指的是二进制位
3.7.2 位段的内存分配
- 位段的成员可以是int 、unsigned int 、signed int 或者是char(属于整型家族)类型
- 位段的空间上是按照需要以4个字节(int)或者一个字节(char)的方式来开辟的
- 位段涉及很多不确定因素,位段是不跨平台的,注重可移植性的程序应该避免使用位段
3.7.3 位段的跨平台问题
- int位段被当成有符号数还是无符号数是不确定的
- 位段中最大位的数目不能确定(16位机器最大16,32位机器最大32,写成27,在16位机器会出问题
- 位段中的成员在内存中从左向右分配,还是从右向左分配标准尚未定义
- 当一个结构体包含两个位段,第二个位段成员比较大,无法容纳于第一个位段剩余的位时,是舍弃剩余的位还是利用,这是不确定的
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在
4.枚举
枚举顾名思义就是一一列举
把可能的取值一一列举
4.1 枚举类型的定义
枚举关键字是enum
enum Sex
{
//枚举的可能取值
MALE,//枚举常量
FEMALE,
SECRET
};
枚举常量的取值是从0开始的
在主函数中,我们用枚举常量的可能取值给他赋值,比如:
4.2 枚举的优点
为什么使用枚举?
我们可以使用#define定义常量,为什么非要使用枚举?
枚举的优点:
- 增加代码的可读性和可维护性
- 和#define定义的标识符比较枚举有类型检查,更加严谨
- 便于调试
- 使用方便,一次可以定义多个常量
5. 联合(共用体)
5.1 联合类型的定义
联合也是一种特殊的自定义类型
这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)
联合体关键字是union
union Un
{
char c;
int i;
};
联合体的成员,在同一时间只能使用一个
5.2 联合的特点
联合的成员是公用同一块内存空间的,这样一个联合变量的大小,至少是最大成员的大小(因为联合至少得有能力保存最大的成员)
5.3 联合大小的计算
联合的大小至少是最大成员的大小
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍