C高级

1. 变量内存分配

在C语言中,变量的内存分配是由编译器在编译时自动处理的。每个变量在内存中都有一个与之相关联的存储位置,这个存储位置的大小和生命周期取决于变量的类型和存储类别。

内存分配类型

  1. 静态分配 编译时分配:全局变量、静态变量
  2. 动态分配 运行时分配:栈(局部变量)、堆(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最小正数最大正数有效位精度
float1bit8bits23bits1.175494e-383.402823e+3861.192093e-07
double1bit11bits52bits2.225074e-3081.797693e+308152.220446e-16
long double
#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

    该数值在 107108 之间。因此十进制有效位为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
*/

浮点数精度丢失

浮点数精度丢失的原因主要有以下几点:

  1. 二进制表示的局限性:由于计算机内部采用二进制表示,某些十进制小数无法精确表示为二进制小数。例如,0.1和0.2这样的十进制小数在二进制表示中是无限循环的小数。当计算机以有限的位数来表示这些小数时,会存在舍入误差,导致精度丢失。

  2. 浮点数的表示方式:浮点数在计算机中通常采用IEEE 754标准来表示,其中包括了符号位、指数位和小数位。这种表示方式决定了浮点数的精度是有限的,无法精确表示所有小数。

  3. 舍入误差:由于浮点数的位数有限,对于无法精确表示的十进制小数,计算机进行舍入来逼近其值。这种舍入操作会引入误差,并导致计算结果与预期值之间的差异。

  4. 算术运算的累积误差:在进行一系列浮点数算术运算时,舍入误差可能会累积并导致精度丢失。每一次运算都会引入一些误差,这些误差在多次运算中逐渐累积,导致最终结果的精度降低。

#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
*/

浮点数比较

不能直接用==来判断浮点数是否相等,因为浮点数的精度问题。常用方式如下:

  1. 自定义的最小精度值,判断是否小于该值。
  2. 将数值强制转换成浮点类型(相同精度),再判断是否相等。
#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");