C高级
1. 变量内存分配
在C语言中,变量的内存分配是由编译器在编译时自动处理的。每个变量在内存中都有一个与之相关联的存储位置,这个存储位置的大小和生命周期取决于变量的类型和存储类别。
内存分配类型
- 静态分配 编译时分配:全局变量、静态变量
- 动态分配 运行时分配:栈(局部变量)、堆(malloc)
内存布局
区域 | 常用符号 | 描述 | 存储和说明 |
---|---|---|---|
代码区 | .code/.text | 程序代码 | flash |
堆区 | .heap | 动态分配的内存 | ram |
栈区 | .stack | 函数调用时的局部变量和参数 | ram |
数据区 | 只读数据、全局变量和静态变量 |
其中数据域又分为
区域 | 常用符号 | 描述 | 存储和说明 |
---|---|---|---|
常量区 | .const/.constdata/.rodata | 常量 | flash |
读写数据区 | .data/.rwdata | 已初始化代码 | flash,上电后重映射到ram |
bss区 | .bss | 未初始化 | ram,上电后清0 |
变量的内存大小
变量的内存大小取决于其类型。例如,int
类型通常占用4个字节(这取决于平台和编译器),而double
类型通常占用8个字节。
内存对齐
为了提高内存访问的效率,编译器通常会按照特定的对齐规则来分配变量的内存。
因此,某些类型的变量可能会占用比其基础数据类型更多的内存,以确保它们在内存中的位置与硬件的访问模式相匹配。
其中最为典型的就是结构类型了,示例如下
#include <stdio.h>
struct _data{
char a;
int b;
char c;
}data;
struct _data2{
char a[2];
short c;
int b;
}data2;
int main()
{
printf("data1_size = %d\n", sizeof(data));
printf("data2_size = %d\n", sizeof(data2));
return 0;
}
结果为
data1_size = 12
data2_size = 8
2. 浮点数详解
浮点数存储方式
IEEE 754标准规定,浮点数由三个部分组成:符号位S、指数E 和 尾数M
浮点数在内存中存储为二进制,浮点数的精度由指数和尾数组成。任意一个二进制浮点数都可以表示为:
(-1)S * M * 2E
类型 | 符号S | 指数E | 尾数M | 最小正数 | 最大正数 | 有效位 | 精度 |
---|---|---|---|---|---|---|---|
float | 1bit | 8bits | 23bits | 1.175494e-38 | 3.402823e+38 | 6 | 1.192093e-07 |
double | 1bit | 11bits | 52bits | 2.225074e-308 | 1.797693e+308 | 15 | 2.220446e-16 |
#include <stdio.h>
#include <float.h>
int main() {
float f = 3.14159265358979323846264338;
double pi = 3.14159265358979323846264338;
float x = 17.3;
double y = 17.3;
printf("%f\n", pi);
// 浮点有效位
printf("%0.20f\n",f);
printf("%0.20f\n",pi);
// 浮点精度丢失
printf("%f\n", x);
printf("%f\n", y);
return 0;
}
/*
// 结果
3.141593
3.14159274101257324219
3.14159265358979311600
17.299999
17.300000
*/
IEEE 754还固定了特殊数值:
无穷大 :当E全为1,M全为0时,表示无穷大。如果S=0,则是正无穷大;如果S=1,则是负无穷大。
非数值(NaN):当E全为1,M不全为0时,表示非数值。这通常用于表示无效的数学运算,比如0除以0。
非规格化数值:当E全为0,M不全为0时,表示非规格化数值。这用于表示非常接近于0的数。
以float为例
指数
指数E有8位(去除全为1的情况),为了方便计算,通常将指数E的偏移量设为127。则指数范围为
-126 ~ +127
。其中,-126表示最小正数,+127表示最大正数。
则最大数不超过:2128= 3.402823e+38
则最小数不超过:2-126= 1.175494e-38
尾数
尾数M有23位,算上符号位S,则共有24位。
尾数可表示的最大值:224 = 16777216
该数值在 107 和 108 之间。因此十进制有效位为7位。
然而,由于二进制和十进制之间的转换以及二进制浮点数的表示方式,实际可用的十进制有效位可能会略有不同
#include <stdio.h>
#include <float.h>
int main() {
printf("float mantissa digits: %d\n", FLT_MANT_DIG);
printf("double mantissa digits: %d\n", DBL_MANT_DIG);
printf("float digits: %d\n", FLT_DIG);
printf("double digits: %d\n", DBL_DIG);
printf("float min normalized: %e\n", FLT_MIN);
printf("double min normalized: %e\n", DBL_MIN);
printf("float max normalized: %e\n", FLT_MAX);
printf("double max normalized: %e\n", DBL_MAX);
printf("float epsilon: %e\n", FLT_EPSILON);
printf("double epsilon: %e\n", DBL_EPSILON);
return 0;
}
/*
// ubuntu 22.0 64bit 实测
// 最值和精度
float mantissa digits: 24
double mantissa digits: 53
float digits: 6
double digits: 15
float min normalized: 1.175494e-38
double min normalized: 2.225074e-308
float max normalized: 3.402823e+38
double max normalized: 1.797693e+308
float epsilon: 1.192093e-07
double epsilon: 2.220446e-16
*/
如果需要测试,可以使用以下链接: 浮点数
#include <stdio.h>
void main()
{
float data1 = 1.0;
double data2 = 1.0;
printf("sizeof(float) = %d\n", sizeof(float));
printf("sizeof(double) = %d\n", sizeof(double));
printf("data1 = %x\n", data1); //
printf("%x", *(int*)&data1);
}
/*
// 输出结果
sizeof(float) = 4
sizeof(double) = 8
data1 = 0
3f800000
*/
浮点数精度丢失
浮点数精度丢失的原因主要有以下几点:
二进制表示的局限性:由于计算机内部采用二进制表示,某些十进制小数无法精确表示为二进制小数。例如,0.1和0.2这样的十进制小数在二进制表示中是无限循环的小数。当计算机以有限的位数来表示这些小数时,会存在舍入误差,导致精度丢失。
浮点数的表示方式:浮点数在计算机中通常采用IEEE 754标准来表示,其中包括了符号位、指数位和小数位。这种表示方式决定了浮点数的精度是有限的,无法精确表示所有小数。
舍入误差:由于浮点数的位数有限,对于无法精确表示的十进制小数,计算机进行舍入来逼近其值。这种舍入操作会引入误差,并导致计算结果与预期值之间的差异。
算术运算的累积误差:在进行一系列浮点数算术运算时,舍入误差可能会累积并导致精度丢失。每一次运算都会引入一些误差,这些误差在多次运算中逐渐累积,导致最终结果的精度降低。
#include <stdio.h>
int main(void)
{
float f = 1.0;
for (int i = 0; i < 100; i++)
{
printf("%f\n", f);
f += 0.1;
}
return 0;
}
/*
// 部分输出结果
1.000000
1.100000
1.200000
...
2.600000
2.700000
2.799999
2.899999
2.999999
...
10.500004
10.600004
10.700005
10.800005
10.900005
*/
浮点数比较
不能直接用==
来判断浮点数是否相等,因为浮点数的精度问题。常用方式如下:
- 自定义的最小精度值,判断是否小于该值。
- 将数值强制转换成浮点类型(相同精度),再判断是否相等。
#include <stdio.h>
int main(void)
{
float f = 2.3;
if(f == 2.3)
{
printf("0. equal\n");
}
else
{
printf("0. not equal\n");
}
if(f - 2.3 == 0)
{
printf("1. equal\n");
}
else
{
printf("1. not equal\n");
}
if((f - 2.3) < 0.000001) // 手动定义最小精度值
{
printf("2. equal\n");
}
else
{
printf("2. not equal\n");
}
if(f == (float)2.3) // 强制转换成float
{
printf("3. equal\n");
}
else
{
printf("3. not equal\n");
}
return 0;
}
/*
// 输出结果
0. not equal
1. not equal
2. equal
3. equal
*/
3. printf实现原理
//----------------------------------------------------
// print
//经过实验测试:
//print函数第一个参数和函数中的第一个局部变量,它们所在的
//栈地址,并非是连续的。
//进过百度查看资料,可能原因:
//1.返回地址
//2.为了处理异常而增加的信息
//3. ...
// 不同的平台,编译器等都可能会有所不同。这个可以先不管
// 有时间,再有针对性的去研究
//----------------------------------------------------
typedef char *va_list;
#define NATIVE_INT int
#define _AUPBND (sizeof (NATIVE_INT) - 1) //sizeof(int)==4;
#define _ADNBND (sizeof (NATIVE_INT) - 1) //
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd))) //
#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap) (void) 0
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
#define OUTBUFSIZE 0x200
static char g_pcOutBuf[OUTBUFSIZE];
int strlen(char * src)
{
int i = 0;
while(*src != '\0')
{
src++;
i++;
}
return i;
}
char* memcpy(char * des,char * src)
{
while(*src != '\0')
{
*des = *src;
src++;
des++;
}
return des;
}
//-----------------------------------------------
// 百度查询之后:
// C语言支持变长形参,根本的原因是
// 形参压入栈的顺序是从右至左
//
// 因此只要有了第一个形参的地址,那么其他形参不就都有了
// 当然,第一个参数需要包含形参总个数的相关信息
// 否则也是没辙的。
// 例如printf中,第一个参数中的%d,%x,%p等
//-----------------------------------------------
int print(int num ,...)
{
int i;
char * p = g_pcOutBuf;
//------------------------------------
// 0.原理:
// typedef char *va_list;
//------------------------------------
va_list args;
//------------------------------------
// 1.原理:
// args = (char *)((int)&num + sizeof(void *));
//------------------------------------
va_start(args, num);
for(i = 0; i < num; i++)
{
//--------------------------------------------
// 此处主要是理解
// 假设:int print(int num,char *str)
// args在栈中,相当于是&str,即形参的地址
// *(int *)args 的就相当于是str
// 我举的例子,传来的形参是字符串(或说:char *)
// 所以最终再将其强制转换为(char*)传给memcpy
// ::注
// 这里的memcpy是我自己写的用来调试,不是库函数
//--------------------------------------------
p = memcpy((char*)p,(char*)(*(int*)args));
//--------------------------------------------
// 2.原理:
// args += sizeof(void *); //指针占用字节长
//--------------------------------------------
va_arg(args,char*);
}
//--------------------------------------------
// 原理:
// args = (va_list)0; //将指针置为无效
//--------------------------------------------
va_end(args);
for (i = 0; i < strlen(g_pcOutBuf); i++)
{
putc(g_pcOutBuf[i]);
}
return num;
}
//------------------------------------------------------------
// 上述函数的调用如下
print(5,"Hello ","world! ","I ","am ","Bruce!\r\n");