主页:醋溜马桶圈-CSDN博客
专栏:C语言_醋溜马桶圈的博客-CSDN博客
gitee:mnxcc (mnxcc) - Gitee.com
目录
1.数据在内存中的存储
1.1 数据类型
1.2 整型在内存中的存储
1.2.1原码、反码、补码
1.2.2 大小端介绍
1.2.2.1 什么是大端小端
1.2.2.2 为什么会有大端和小端
1.3 字符型在内存中的存储
1.4 浮点型在内存中的存储
1.4.1 浮点数存储规则
1.4.2 IEEE754规定
2.数组
2.1 一维数组
2.1.1 一维数组的创建
2.1.2 一维数组的初始化
2.1.3 一维数组的使用
2.1.4 一维数组在内存中的储存
2.2 二维数组
2.2.1 二维数组的创建
2.2.2 二维数组的初始化
2.2.2 二维数组的使用
2.2.3 二维数组在内存中的储存
2.3 数组越界
2.4 数组作为函数参数
2.4.1 数组元素作为函数参数
2.4.2 数组名作为函数参数
2.5 柔性数组
2.5.1 柔性数组的形式
2.5.2 柔性数组的特点
2.5.3 柔性数组的优势
3.结构体
3.1 结构体类型的声明
3.1.1 结构体的基础知识
3.1.2 结构的声明
3.1.3 特殊的声明
3.2 结构的自引用
3.3 结构体变量的定义和初始化
3.4 结构体内存对齐
3.4.1 怎么对齐的
3.4.2 为什么要对齐
3.4.3 修改默认对齐数
3.5 结构体成员访问
3.5.1 结构体变量访问成员
3.5.2 结构体指针访问指针变量的成员
3.6 结构体传参
3.6.1 传值调用
3.6.2 传地址调用
3.7 结构体实现位段(位段的填充&可移植性)
3.7.1 什么是位段
3.7.2 位段的内存分配
3.7.3 位段的跨平台问题
4.枚举
4.1 枚举类型的定义
4.2 枚举的优点
5. 联合(共用体)
5.1 联合类型的定义
5.2 联合的特点
5.3 联合大小的计算
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中没有默认对齐数,对齐数就是成员自身的大小
下面这篇文章详细的解释了结构体内存对齐规则
C语言--结构体内存对齐规则_结构体对齐原则-CSDN博客
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 联合大小的计算
联合的大小至少是最大成员的大小
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍