现代C语言程序设计之数据存储

时间:2021-07-13 23:36:26

现代C语言程序设计之数据存储

2.1 计算机信息数据存储

2.1.1 计算机信息数据存储单位

在计算机最底层,数据都是以二进制(01010)的方式存储,而计算机中最小的存储单位是位(bit),用来表示0或者1。计算机中最基本的存储单位是字节(Byte),1个字节对应8个位(Bit)。
而日常应用中常使用的基本存储单位包括KB,MB,GB,TB,PB,

  • KB,MB:使用迅雷下载某些资源时的网速就是KB或者MB,它们之间的换算关系如下
1MB=1024KB
1KB=1024B
1B=8bit

但是网络运营提供商(例如长城宽带、移)声称的百兆带宽实际上是100Mb,但是网络下载速度是以字节为单位的,因此真实的网速理论上只有100Mb/8=12.5MB

  • GB:在买内存或者买移动硬盘时,通常使用的存储单位就是GB
1GB=1024MB
1TB=1024GB

但是在买4T的移动硬盘时,实际的可用容量却只有3T多,因为计算机的存储单位是以2的10次方(即1024)换算,而硬盘厂商们是以1000为换算单位。

4T的硬盘换算成位如下所示

4T=4*1024GB*1024MB*1024KB*1024B*8bit

而硬盘厂商的实际容量

4T=1000*1000*1000*1000*8

因此实际的可用容量是

4*1000*1000*1000*1000/1024/1024/1024/10243.63T

而在一些互联网巨头(例如国内的BAT,国外的亚马逊、苹果、微软、谷歌)公司中,可能使用到比TB更大的海量数据,也就是PB或者EB。

1PB=1024TB
1EB=1024PB

2.1.2 计算机内存

为什么说32位系统只能使用4G内存?下面是4G的内存换算

4G=2^2 * 2^10 * 2^10 * 2^10 =4*1024*1024*1024=2^32

因为4G只能够寻址到2^32,使用16进制表示就是0xFFFFFFFF,这里可以借助Visual Studio的调试功能查看内存的寻址,如下图所示
现代C语言程序设计之数据存储

2.2 变量

2.2.1 变量概述

内存在程序看来就是有地址编号的一块连续空间,当数据放到内存中后,为了方便的找到和操作这个数据,需要给这个位置起名字,编程语言通过变量来表示这个过程。

2.2.2 变量的声明和初始化赋值

在使用变量前必须先要声明变量并初始化赋值,并且要遵守变量的命名规范

  • 变量名由字母数字下划线组成,不能以数字开头
  • 变量名区分大小写。
  • 变量名不能是C语言的关键字(Visual Studio中的关键字都是蓝色的)
  • 考虑到软件的可维护性,建议变量见名知意

如下应用案例所示展示了C语言的变量命名案例

#include <stdio.h>
#include <stdlib.h>
/* 变量的命名规范 */
void main() {
    //合法的标识符
    int number;
    //见名知意
    int age;
    char ch;
    double db;
    //变量名不能是关键字
    //int void;
    //变量名不能以数字开头
    //int 1num;
    /****************************************编译器特性*******************************/
    //VC支持中文变量,GCC不支持中文命名
    int 年龄 = 29;
    printf("年龄 =%d\n", 年龄);
    //在老版(C++11之前)的编译器中,变量声明必须放在函数调用之前
    /****************************************编译器特性*******************************/
    //声明多个变量
    int one, two, three;
    system("pause");
}

在声明变量后,一定要给变量赋初始值,否者无法编译通过,如下应用案例所示

#include <stdio.h>
#include <stdlib.h>
/* 变量初始化赋值 在使用变量时必须手动初始化赋值,否则会得到一个随机的垃圾值 */
void main(){
    int num;
    //编译错误错误 C4700 使用了未初始化的局部变量“num”
    printf("num =%d\n",num);
    system("pause");
}

2.2.2 变量存储

如下应用程序所示,通过"="可以给变量赋值,同时可以通过printf()函数传递%p参数来获取变量在内存中的地址。

#include <stdio.h>
#include <stdlib.h>
/* 变量在内存中的存储 */
void main () {
    int num = 20;
    //查看num的内存地址
    printf("整数变量num的地址是%p\n", &num);
    printf("整数变量num = %d\n", num);
    num = 30;
    printf("修改之后整数变量num的值是%d\n", num);
    system("pause");

}

如下图所示,还可以通过Visual Studio 提供的调试功能通过断点查看变量在内存的存储,通过输入变量的内存地址便可以观察变量对应的值。
现代C语言程序设计之数据存储
在同一时刻,内存地址对应的值只能存储一份,如果修改地址对应的值,之前的值会被覆盖,这个就是变量的特点,变量名是固定的,但是变量值在内存中是随着业务逻辑在变化的,例如最常见的游戏场景中,游戏人物生命值的变化。

2.2.3 编译器对变量的处理

当在程序中声明变量并赋值时,编译器会创建变量表维护变量的信息,包括变量的地址,变量的类型以及变量的名称。
而在内存中变量的内存地址和变量值是一一对应的,编译器正是通过变量表的内存地址和内存中的变量地址关联。因此在使用变量进行相关操作之前必须先声明并赋值,否则程序会发生编译错误,如下代码片段所示。

#include <stdio.h>
#include <stdlib.h>

/* 编译器和内存对变量的处理 */
void main(){

    int a, b, c;
    //不能使用未声明的变量
    printf(" %d\n",d);
    system("pause");
}

2.2.4 变量运算的原理

当两个变量在执行相关运算(例如加法)时,系统会将把两个变量地址对应的变量值移动到CPU内部的寄存器中执行运算后将运算结果返回给内存,如下应用程序所示

#include <stdio.h>
#include <stdlib.h>
/* 变量运算的原理 */
void main() {
    int a = 1;
    int b = 2;
    //分配四个字节的内存
    int c;
    printf("变量a的地址是%p\t,变量b的地址是%p\t,变量c的地址是%p\n",&a,&b,&c);
    //数据的运算是在CPU的寄存器完成的
    c = a + b;
    c = b - a;
    //对数据的操作是由CPU完成的
    //a + 1 = 4;
    printf("c=%d\n",c);
    system("pause");

}

如下图所示,可以借助VisualStudio的调试功能来观察EAX寄存器的变化的值。
现代C语言程序设计之数据存储

为了能够更加直接的理解寄存器的作用,这里使用C语言嵌套汇编语言来完成变量的赋值运算和加法运算。

#include <stdio.h>
#include <stdlib.h>
/* 使用汇编语言实现变量的赋值以及运算来理解数据的运算是在CPU内部的寄存器完成的 */
void main() {

    //申请四个字节的内存
    int a;
    printf("整数变量a的地址是%p\n",&a);

    
    //变量的赋值都是通过CPU的寄存器来完成的
    //这里借助汇编语言实现将10赋值给变量a
    _asm {
    
        mov eax, 10
        mov a, eax
    }

    printf("整数变量a的值等于%d\n",a);

    _asm {
    
        //把变量a的值赋值给寄存器eax
        mov eax,a
        //将eax的值加5
        add eax,5
        //把eax的值赋值给a
        mov a,eax
    }
    printf("变量a加5之后的结果是%d\n",a);
    system("pause");
}

2.2.5 变量交换的实现

如下应用案例所示,实现了三种变量交换的算法,同时也比较了每种算法的时空复杂度,变量交换的应用场景主要在使用在排序算法中。

1.通过使用中间变量实现交换

#include <stdio.h>
#include <stdlib.h>
/* 使用临时变量实现变量交换 赋值运算三次 增加空间 */
void varriable_swap_with_tmp(int left,int right) {

    printf("使用临时变量实现变量交换交换之前\t left=%d \t right=%d\n",left,right);
    int middle = left;
    left = right;
    right = middle;
    printf("使用临时变量实现变量交换交换之后\t left=%d \t right=%d\n",left,right);
}

void main() {
    int left = 5;
    int right = 10;
    varriable_swap_with_tmp(left,right);
    system("pause");
}
  1. 使用算术运算实现变量交换
#include <stdio.h>
#include <stdlib.h>
/* 使用算术运算实现变量交换 考虑数据越界的问题 不需要开辟额外的空间 赋值运算三次,算术运算三次 总运算次数6次 */
void varriable_swap_with_algorithm(int left,int right) {
    printf("使用算术运算实现变量交换交换之前\t left=%d \t right=%d\n", left, right);
    left = left + right; // 加号变成乘号
    right = left - right;//减号变成除号
    left = left - right; //减号变成除号
    printf("使用算术运算实现变量交换交换之后\t left=%d \t right=%d\n", left, right);
}


void main() {

    int left = 5;
    int right = 10;

    varriable_swap_with_algorithm(left,right);
    system("pause");
}
  1. 使用异或运算实现变量交换
#include <stdio.h>
#include <stdlib.h>

/* 使用异或运算实现变量交换 不用考虑运算结果溢出的问题 */
void varriable_swap_with_xor(int left, int right) {
    printf("使用异或运算实现变量交换交换之前\t left=%d \t right=%d\n", left, right);
    left = left ^ right;
    right = left ^ right;
    left = left ^ right;
    printf("使用异或运算实现变量交换交换之后\t left=%d \t right=%d\n", left, right);

}

void main() {
    int left = 5;
    int right = 10;
    varriable_swap_with_xor(left,right);
    system("pause");
}

2.2.6 自动变量与静态变量

在函数中的形式参数和代码块中的局部变量都是自动变量,它们的特点是只有在定义的时候才会被创建(即系统自动开辟内存空间),在定义它们的函数返回时系统自动回收变量占据的内存空间,为了考虑到代码的可读性,通常使用auto关键字来修饰自动变量,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
/* 自动变量: 只有定义它们的时候才创建,在定义它们的函数返回时系统回收变量所占用的存储空间, 对这些变量存储空间的分配和回收由系统自动完成 一般情况下,不做专门说明的变量都是自动变量,自动变量也可以使用关键字auto说明 块语句中的变量,函数的形式参数都是自动变量 */

void auto_varriable(auto int num) { //num就是自动变量,函数调用的时候就存在,函数结束,变量会被操作系统自动回收,地址都是同一个地址,但是值在不断发生变化

    printf("num的内存地址是%p\nnum的值是%d\n",&num,num);

    auto int data = num;

    printf("data的内存地址是%p\ndata的值是%d\n", &data, data);

}

/* 多次调用自动变量 */
void invoke_auto_varriable() {
    int num = 20;
    auto_varriable(num);
    printf("\n\n");
    auto_varriable(80);

}


void main() {

    invoke_auto_varriable();

    system("pause");
}

可以通过下断点来调试该程序,观察当执行auto_varriable()函数完成以后,局部变量data将会被回收,如下图所示
现代C语言程序设计之数据存储
同时可以通过观察内存地址,发现当调用auto_varriable()函数时,num=20
现代C语言程序设计之数据存储
然后当执行完auto_varriable()函数后,num的值变量一个系统分配的垃圾值
现代C语言程序设计之数据存储

而静态变量不会发生变化,即使函数执行完成也不会被操作系统回收,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>

/* 静态变量 */
void static_varriable() {
    static int x = 99;

    printf("x的内存地址是%p,x的值是%d", &x,x);

    printf("\n\n");
}

/* 多次调用静态变量 */
void invoke_static_varriable() {
    static_varriable();
    printf("\n");
    static_varriable();
}

void main() {

    //invoke_auto_varriable();
    invoke_static_varriable();
    system("pause");
}

调试以上应用程序,会发现直到main函数执行完成,静态整数变量x都不会被操作系统回收。

2.3 常量

常量表示一旦初始化之后便不能再次直接改变的变量,例如人的身份证编号一旦确定之后就不会再次改变。C语言支持使用const关键字和#define CONST_NAME CONST_VALUE 两种方式来定义和使用常量。

2.3.1 const常量

如果想要使一个变量变成常量,只需要在变量前面使用const关键字即可,const常量虽然不能直接修改,但是可以通过C语言的指针来修改,因此不是真正意义上的常量。,应用案例如下所示。

#include <stdio.h>
#include <stdlib.h>
/* const常量不能直接修改值,但是可以通过指针修改值 */
void main() {

    //定义一个整数常量
    const long id = 10000;
    //不能直接修改常量
    //id = 10001;
    printf("常量id的地址是%p\n",&id);
    printf("常量id=%d\n", id);

    //通过指针修改
    //* 根据地址取内容
    //(int*) 类型转换为非 常量类型
    * (int*) (&id)= 10001;

    printf("常量id=%d\n", id);
    system("pause");


}

2.3.3 #define常量

在C语言中使用const定义的变量不能直接修改,但是可以通过指针来修改,因此不是真正意义上的常量。
如果想要使用真正意义上的常量,可以使用#define CONSTA_NAME VALUE 来实现,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>

//#define语句不需要分号结尾,#define定义的常量值是在寄存器中产生,无法取内存地址,即无法通过C语言修改,
//因为C语言无法直接操作CPU的寄存器,只能操作内存。
#define CARD_NUMBER 88888888 

void main() {
    printf("CARD_NUMBER=%d\n",CARD_NUMBER);
    system("pause");

}

使用#define定义常量的好处:

  1. 通过有意义的常量名,可以指定该常量的意思,使得开发人员在越多代码时减少迷惑
  2. 常量可以在多个方法中使用,如果需要修改常量,只需要修改一次便可实现批量修改,效率高而且准确。
#include <stdio.h>
#include <stdlib.h>

/* 在自定义方法中使用常量 */
void use_card_number_const() {

    printf("在自定义方法中使用CARD_NUMBER常量的值=%d\n", CARD_NUMBER);

}

void main(){

    use_card_number_const();
    system("pause");
}

#define的应用场景: 实现代码混淆

首先在define.h头文件中定义如下常量

#define _ void
#define __ main()
#define ___ {
#define ____ system("notepad");
#define _____ system("pause");
#define ______ }

然后定义define.c源文件,内容如下

#include "define.h"
_ __ ___ ____ _____ ______

运行程序后,可以打开记事本。

2.4 数据类型

2.4.1 sizeof()运算符

数据类型即对数据进行分类,数据在计算机底层是二进制的,不太方便操作,因此编程语言引入了数据类型将其进行分类处理。

不同的数据类型占据不同的内存大小,这里可以使用C语言提供的sizeof()运算符来获取指定数据类型占据的字节数量,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>

/* 使用sizeof()关键字获取指定数据类型的大小 */
void main() {

    printf("char占据的字节数量是%d\n", sizeof(char));
    printf("short占据的字节数量是%d\n", sizeof(short));
    printf("int占据的字节数量是%d\n", sizeof(int));
    printf("double占据的字节数量是%d\n", sizeof(double));
    system("pause");
}

当然sizeof()还可以求表达式的数据类型,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>

/* 使用sizeof求表达式的内存大小 */
void main() {
    int num = 10;
    printf("字符串str占据的字节数量是%d\n",sizeof("str"));//4个字节 字符串以\0结束
    char ch = 'A';
    printf("字符变量ch占据的字节数量是%d\n",sizeof(ch));
    printf("字符常量A占据的字节数量是%d\n",sizeof('A'));
    printf("整数变量num占据的字节数量是%d\n",sizeof(num));
    system("pause");
}

3.4.2 数据的解析

同样的数据,按照不同的解析方式会得到不同的结果,如下应用案例所示

#include <stdio.h>
#include <stdlib.h>

void main() {
    //同样的数使用不同的方式解析获取不同的结果
    int num = -1;
    printf("num = %p\n",&num);
    getchar();
    system("pause");
}

启动程序调试,通过查看控制台输出num变量的地址,然后在内存中分别以1字节带符号整数查看结果为-1,8字节整数查看结果为14757395259826634751,1字节不带符号显示(结果为255),如下图所示,不同的方式查看通过鼠标右键获取。
现代C语言程序设计之数据存储

而如果数据使用了错误的解析方式,则结果也会发生错误,这里以printf()函数为例子,应用案例如下所示。

#include <stdio.h>
#include <stdlib.h>

/* printf解析数据类型 */
void main() {
    //printf函数不会进行类型转换,当类型不匹配输出就是错误的结果
    int num = 10;
    printf("num =%f\n",num);//如果想要获取正确的结果需要收到强制类型转换(float)num
    
    //浮点数按照整数解析,结果会出错
    float fl = 10.9;
    printf("fl=%d\n",fl);//如果想要获取正确的结果需要收到强制类型转换(int)fl
    system("pause");
}

2.4.3 数据类型的极限

每种数据类型都有自己的极限值(即最大值和最小值),如果在参与运算时超过了极限值,则运算结果是错误的,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

/* unsigned char */
void main() {
    //为了保证结果运算正确,必须在极限范围之内
    unsigned char chnum = 255;
    printf("无符号char所能存储的最大值是%d\n", UCHAR_MAX);
    printf("chnum=%d",chnum); //结果为0 因为chnum所能表示的最大值为255,这里发生了越界,结果错误
        system("pause");
}

整数的极限值定义在<limits.h> 头文件中,
浮点数的极限值定义在<float.h>头文件中,
如下应用案例所示展示了整数以及浮点数的极限值使用。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <float.h>

/* 整数的极限 */
void int_limt() {

    printf("int所能存储的最大值是%d,int所能存储的最小值是%d\n", INT_MAX, INT_MIN);
}

/* float的极限 */
void float_limit() {
    printf("float所能存储的最大值是%e,float所能存储的最小值是%e\n", FLT_MAX, FLT_MIN);
    printf("double所能存储的最大值是%e,float所能存储的最小值是%e\n", DBL_MAX, DBL_MIN);


}

void main() {
    int_limt();
    float_limit();
    system("pause");
}

2.4.4 数据的正负

在最底层,计算机的数据都是以二进制的形式表示的,那么如何区分正负数呢?
最高位(左边第一位)是符号位,如果是1,则表示为负数,如果是0则表示正数。
如下应用案例所示

#include <stdio.h>
#include <stdlib.h>
/* char类型二进制表示方式 */
void main() {

    char ch = -1; //十六进制表示为ff 转换为二进制 1111 1111 最高位(左边第一个数字)为符号位,1表示负数
    char chx = 3; //十六进制为03 转换为二进制为 0000 0011 最高为(左边第一个数字)为符号位,0表示整数
    printf("字符变量ch的地址是%p,字符变量chx变量的地址是%p\n",&ch,&chx);
    printf("ch=%d\tchx=%d\n",ch,chx);
    system("pause");

}

如下图所示,可以通过Visual Studio的调试功能查看两个变量在内存中的存储
现代C语言程序设计之数据存储

2.4.5 数据在内存中的排列

PC、手机的内存排列是低位在低字节,高位在高字节,节省寻址时间。
如下应用程序所示

#include <stdio.h>
#include <stdlib.h>
/*数据在内存中的排列 低位在低字节,高位在高字节 */
void main() {

    // 四字节二进制方式 0000 0000 0000 0000 0000 0000 0000 0001

    int num = 1;

    printf("num的地址是%p\n",&num);
    printf("num = %d\n",num);
    system("pause");

}

可以通过Visual Studio 下断点调试程序,使用1字节查看整数1在内存中的排列,如下图所示:

 

现代C语言程序设计之数据存储
数据在内存中的排列

 

而Unix等大型服务器的内存排列都是低位在高字节。

2.5 原码、反码、补码的计算

  原码 反码 补码
+7 00000111 00000111 00000111
-7 10000111 11111000 11111001
+0 00000000 00000000 00000000
-0 10000000 11111111 00000000
数的取值范围 -127-127 -127-127 -128-127

从上面的表格可以看出,正数的原码、反码和补码都相同,而负数的补码就是原码取反(最高位不变,其他位取反)后加1的结果。

而实际数据在计算机(手机、电脑、服务器)的内存中也是以补码的形式存储数据的,如下应用案例所示

#include <stdio.h>
#include <stdlib.h>
/* 计算机最底层都是以补码存储数据的 */
void main() {
    //原码 10000111
    //反码 11111000
    //补码 1111 1001 F9
    char ch = -7; 
    printf("ch的地址是%p\n",&ch);
    printf("ch=%d\n",ch);
    system("pause");
}

首先需要计算出-7的补码,然后转换为16进制的结果为F9,然后通过Visual Studio的调试功能查看内存的存储结果,如下图所示
现代C语言程序设计之数据存储

2.6 整数

2.6.1 整数常量

C语言整数常量可以使用八进制,十进制和十六进制表示。它们在计算时遵循逢R进1,借1当R。如下表格所示展示了它们的组成部分和应用场景。

进制类型 组成部分 应用场景
二进制 0或者1 底层数据存储
八进制 0-7之间的8个整数 Linux权限系统
十进制 0-9之间的10个整数 整数的默认进制类型
十六进制 0-9,a-f 之间的十个整数加上六个字母 内存地址

同时可以使用u后缀表示位无符号整数,使用l后缀表示long类型的整数,使用ll后缀表示为long long类型的整数,应用案例如下所示


#include <stdio.h>
#include <stdlib.h>


/* 整数的三种进制类型 整数的三种后缀 无符号,长整数,长长整数 */
void main() {

    int a1 = 10;
    int a2 = 010;
    int a3 = 0x10;

    int a4 = 101u; //无符号
    int a5 = 102l;//long
    int a6 = 103ll;//long long

    printf("a1 = %d\ta2 = %d\ta3 = %d\ta4 = %d\ta5 = %d\ta6 = %d\t",a1,a2,a3,a4,a5,a6);
    
    system("pause");
}

2.6.2 整数极限

而且整数按照占据不同的字节大小可以分为short,int,long和long long 四种类型,它们默认是有符号(signed)类型用于存储正负数,而对应的无符号类型则用来存储非负数的整数,关于它们能够存储数据的极限以及占据内存的大小如下应用程序所示。

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
/* C语言不同类型整数的极限 占据不同字节大小的整数极限也不一样 */
void main() {
    //16位(嵌入式系统) int和short是等价的
    printf("short能存储的最大值是%d\tshort能存储的最小值是%d,占据的字节数量是%d\n", SHRT_MAX, SHRT_MIN,sizeof(short));
    printf("unsigned short能存储的最大值是%d\n", USHRT_MAX);
    //32位和64位系统 int和long是等价的
    printf("int能存储的最大值是%d\tint能存储的最小值是%d,占据的字节数量是%d\n", INT_MAX, INT_MIN,sizeof(int));
    printf("unsigned int能存储的最大值是%d\n", UINT_MAX);
    //无符号的整数 最小值都是0 即不能表示负数
    printf("long能存储的最大值是%d\tlong能存储的最小值是%d,占据的字节数量是%d\n", LONG_MAX, LONG_MIN,sizeof(long));
    printf("long long能存储的最大值是%lld\tlong long能存储的最小值是%lld,占据的字节数量是%d\n", LLONG_MAX, LLONG_MIN,sizeof(long long));
    printf("unsigned long long 能存储的最大值是%llu\n", ULLONG_MAX);
    
    system("pause");

}


2.6.3 long long类型的整数

在应用开发时需要主要使用数据类型的极限,如果超越数据存储范围的极限,程序会出现Bug,例如想要存储QQ或者手机号就应该使用无符号的long long 类型,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
/* long long 的应用场景 */
void main() {

    unsigned long long mobilePhone = 18601767221;
    printf("mobilePhone=%llu\n",mobilePhone);
    unsigned long long qq = 1079351401;
    printf(" qq = %llu",qq);
    system("pause");
}

2.6.4 整数的越界

在使用整数参与运算时,需要考虑到数据范围对应的极限,否则会发生错误的结果,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>

/* unsigned short */
void main() {
    //为了保证结果运算正确,必须在极限范围之内
    unsigned short int  shortnum = 65536;
    printf("无符号short int所能存储的最大值是%d\n", USHRT_MAX);
    printf("shortnum=%d", shortnum); //结果为0 因为chnum所能表示的最大值为255,这里发生了越界,结果错误
    system("pause");
}

2.6.5 跨平台的整数

C语言是在使用标准库的前提下是可移植的,但是C语言的整数在不同的平台上,同样的数据类型占用的字节大小是不一样的。例如int在16位系统占据2个字节,在32位及其以上系统占据四个字节,long在Windows平台上,无论是32位还是64位都是占四个字节,而在64位ubuntu下却占据8个字节,应用案例如下所示

Linux版

#include <stdio.h>

int main(){

    long num=100;
    int size=sizeof(num);
    printf("ubuntu 64位系统中 long占据的字节数量是%d",size);
    return 0;
}

Windows版

#include <stdio.h>

void main() {

    long val = 100;
    printf("windows下long占据的字节数量是%d\n", sizeof(val));

    getchar();
}

为了解决不同平台,相同的类型占据的大小不一致的问题,C语言标准委员会在C99标准中提出了跨平台的整数,在<stdint.h>头文件中定义,意味着同样的类型在不同的系统下的大小是一致的,应用案例如下所示

linux版

#include <stdio.h>
#include <stdint.h>


int main(){

    long num=100;
    int int_size=sizeof(num);
    printf("ubuntu 64位系统中 long占据的字节数量是%d",int_size);

    //在不同的平台下占据都是32字节
    int32_t int_32_MAX_VALUE = INT32_MAX;
    int int32_size=sizeof(int32_t);
    printf("sizeof(int_32_MAX_VALUE ) = %d\n",int32_size);
    printf("int_32_MAX_VALUE = %d\n", int_32_MAX_VALUE);


    //在不同的平台下占据都是64字节
    int64_t int_64_MAX_VALUE = INT64_MAX;
    int int64_size=sizeof(int64_t);
    printf("sizeof(int_64_MAX_VALUE ) = %d\n", int64_size);
    printf("int_64_MAX_VALUE = %ld\n", int_64_MAX_VALUE);

    return 0;
}

windows版

#include <stdio.h>
#include <stdint.h>


/* 不同的平台,不同的编译器,同样的数据类型大小不一样。 例如int 16位的情况下是2个字节,32位系统是4个字节 long类型在windows上无论是32位还是64位都是4个字节,而在64位linux上long占据的是8个字节 为了解决这个问题,C语言标准组织在C99标准中提出了跨平台的整数,也就是着不同平台的整数占用的字节数量是一样的,VS2013+,GCC都支持该标准 */

void main() {


    long val = 100;
    printf("windows下long占据的字节数量是%d\n", sizeof(val));

    //在不同的平台下占据都是32字节
    int32_t int_32_MAX_VALUE = INT32_MAX;
    printf("sizeof(int_32_MAX_VALUE ) = %d\n",sizeof(int_32_MAX_VALUE));
    printf("int_32_MAX_VALUE = %d\n", int_32_MAX_VALUE);


    //在不同的平台下占据都是64字节
    int64_t int_64_MAX_VALUE = INT64_MAX;
    printf("sizeof(int_64_MAX_VALUE ) = %d\n", sizeof(int_64_MAX_VALUE));
    printf("int_64_MAX_VALUE = %lld\n", int_64_MAX_VALUE);



    getchar();
}

2.7 浮点数

2.7.1 浮点数常量

浮点数就是数学意义上的小数,C语言中分别使用float,double和long double表示,默认类型是double,浮点数的常量可以使用十进制的小数和科学计数法表示,科学计数法可以存储特大或者特小的数字,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>


/* 浮点数两种常量表示方法 */
void main() {


    //十进制

    float flt = 12.0f; //小数后面加f表示float类型
    double dbl = 12.0; //小数默认是double类型
    //科学计数法
    double db1 = 0.12e3;
    //e之前必须有数字,指数必须为整数
    double db2 = 12000.124e5; //e5表示10的5次方
    //%f默认输出小数点后六位
    printf("flt = %f \n",flt);
    printf("db1 = %f \t db2 = %f\n",db1,db2);
    getchar();
}

2.7.2 浮点数极限

C语言在limits.h的头文件中使用常量定义了float和double的极限值,我们可以尝试使用printf函数输出该结果,分别保留 800和1500位小数。

#include <stdio.h>
#include <float.h>
/* 浮点数极限 */
void main() {
    //float占据四个字节,double占据8个字节long double 大于等于double
    printf("float占据的字节数量是%d\tdouble占据的字节数量是%d long double占据的字节数量是%d\n\n\n\n\n",sizeof(float),sizeof(double),sizeof(long double));
    printf("float能存储的最大值是%.100f\tfloat能存储的最小值是%.100",FLT_MAX,FLT_MIN);
    printf("\n\n\n\n\n\n\n\n");
    printf("double能存储的最大值是%.1500f\n\n\n\n double能存储的最小值是%.1500f\n",DBL_MAX,DBL_MIN);
    getchar();

}

2.7.3 赋值时自动类型转换

在进行赋值运算时会发生自动类型转换,例如把一个double类型的常量10.5赋值给float类型的变量,它们占据的字节数量不同,但是能够赋值成功,因为发生了自动类型转换,应用案例如下所示。

#include <stdio.h>
#include <stdlib.h>
/* 赋值运算会发生自动类型转换 */
void main() {
    
    float flt = 10.5;
    //程序输出结果显示flt和10.5占据的字节数量不同,因为这里发生了数据类型转换
    printf("flt占据的字节数量为%d\t 10.5占据的字节数量为%d", sizeof(flt), sizeof(10.5));

    int num = 5 / 3;
    printf(" num = %d\n",num);

    int val = 3.2;
    printf(" val =%d",val);
    getchar();
}

2.7.4 浮点数相等性判断

float占据四个字节,提供的有效位是6-7位,而double占据八个字节,提供的有效位数是15-16位,如果在使用float或者double表示实数时超过有效数字,若拿来进行关系运算(例如等于)的话,会得到一个错误的结果,应用案例如下所示

/* 浮点数的相等性判断 如果实数超过有效范围,使用==判断会出错 */
void float_equals() {

    float flt1 = 1.00000000001;
    float flt2 = 1.00000000000000000001;
    //因为float的有效数字是6-7位 这里超出有效数字 计算不准确
    printf(" flt1 == flt2 ? %d\n", (flt1 == flt2)); // 输出结果1表示相等 0则表示不相等

    //double精确的有效位数是15-16位,这里也超出了有效数字,计算不够正确 
    double db1 = 1.00000000000000000000000000000001;
    double db2 = 1.000000000000000000000000000000000000000000000000000000000000000000000000000000001;

    printf(" db1 == db2 ? %d\n", (db1 == db2)); // 输出结果1表示相等 0则表示不相等

}

void main() {
    float_equals();
    getchar();
}

2.7.5 浮点数内存存储原理

int和float同样占据四个字节的内存,但是float所能表示的最大值比int大得多,其根本原因是浮点数在内存中是以指数的方式存储。
我们都知道在内存中,一个float类型的实数变量是占据32位,即32个二进制的0或者1组成

高位															低位	
0000 0000 0000 0000 0000 0000 0000 0000 

如上代码片段所示,从低位依次到高位叫第0位和第31位,这32位可以由三部分组成:

  • 符号位:第31位数表示符号位,如果为0表示整数,如果为1表示负数
  • 阶码:第23位到第30位,这8个二进制表示该实数转化为规格化的二进制实数后的指数与127(127即所谓的偏移量)之和所谓阶码,规格化的二进制实数只能在-127-127之间。
  • 尾数:第0位到第22位,最多可以表示23位二进制小数,否则超过了就会产生误差。

应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
/* 浮点数在内存中的存储 */
void main() {
    
                                                        //符号位(31位) 阶码(第30位-23位) 尾数(第22位-第0位)
    float flt1 = 10.0; //4字节十六进制 41200000 二进制 0 100 00001 010 0000 0000 0000 0000 0000
    float flt2 = -10.0;//4字节十六进制 c1200000 二进制 1 100 00010 010 0000 0000 0000 0000 0000
    printf(" flt1的内存地址是%p\tflt2的内存地址是%p\n", &flt1, &flt2);
    float flt3 = 20.0; // 字节十六进制 41a00000 二进制 0 100 0001 1010 0000 0000 0000 0000 0000

    printf("变量flt3的地址是%p", &flt3);


    getchar();

}

2.7.6 浮点数应用案例

使用math.h头文件中的sqrt函数实现给定三角形三边的面积计算

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

/* 根据给出的边长求面积 使用math.h文件中提供的开平方根函数 */
void main() {

    int a = 6;
    int b = 8;
    int c = 10;
    int p = (a + b + c) / 2;
    //sqrt返回float,这里使用赋值运算完成了类型转换
    int s =sqrt( p * (p - a)*(p - b)*(p - c));
    printf("s = %d",s);
    printf("三角形的面积是%d",s);
    printf("三角形的面积是%f", sqrt(p * (p - a)*(p - b)*(p - c)));
    getchar();
}

使用math.h的pow函数实现中美GDP计算,并计算出中国GDP超过美国GDP的年份

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

/* 使用math.h的pow函数实现中美GDP计算,并计算出中国GDP超过美国GDP的年份 */
void main() {


    double ch_current_gdp = 12.0;
    double us_current_gdp = 19.70;

    double ch_rate = 1.06;
    double us_rate = 1.04;

    double ch_gdp;
    double us_gdp;
    int year;
    for (int i = 1; i <= 100;i++) {
        ch_gdp = ch_current_gdp * pow(ch_rate, i);
        us_gdp = us_current_gdp * pow(us_rate, i);
        year = 2017 + i;
        printf("%d年中国的GDP是%f\n",year,ch_gdp);
        printf("%d年美国的GDP是%f\n",year, us_gdp);

        if (ch_gdp>us_gdp) {
            printf("在%d年,中国的GDP超越了美国的GDP",year);
            break;
        }
    
    }

    getchar();
}

2.8 字符与字符串

2.8.1 字符

字符和字符串是日常开发中经常打交道的数据类型,使用一对单引号('')包含起来的内容就是字符,C语言提供了putchar()和printf()函数输出字符(英文),应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
/* 输出字符的两种方式 putchar() printf("%c",char); */
void char_io() {


    putchar('A');
    //输出中文乱码。
    putchar('刘');
    printf("%c",'A');
}

void main() {

    char_io();
    getchar();

}

而字符常量通常为了考虑兼容和扩展宽字符(即中文),通常会占据4个字节,英文占据一个字节,中文占据两个字节,应用案例如下所示。

#include <stdio.h>
#include <stdlib.h>
/* 字符的大小 字符常量为了兼容扩展宽字符,占据的字节数量都是4个字节 而英文字符占据一个字节,中文字符(宽字符)占据两个字节 */
void main() {

    
    
    char chinese = '刘';
    //char 占据一个字节,没办法存储中文
    printf("chinese =%c ", chinese);

    char ch = 'A';
                // sizeof()运算符求字符A的大小,这里为了兼容扩展宽字符,一般占据四个字节
    printf(" ch占据的字节数量为%d\t 'A'占据的字节数量为%d\n",sizeof(ch),sizeof('A'));
    //宽字符 占据两个字节,可以存储中文
    wchar_t wch =L'我'; printf("宽字符占据的字节数量是%d\n",sizeof(wchar_t)); printf("字符常量我占据的字节数量是%d\n",sizeof('')); } 

如果要想输出中文字符,可以参考以下方式

//引入本地化的头文件
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
/* 宽字符用于存储中文,但是如何输出中文呢? 参考以下内容 */
void main() {

    //设置本地化
    setlocale(LC_ALL, "chs");
    //宽字符 占据两个字节,可以存储中文
    wchar_t wch = L'我'; //使用wprintf()函数输出中文 wprintf(L"%c\n", wch); getchar(); } 

字符在内存中是以数字的方式存储,而ASC||码表规定了字符对应的数字编号,当使用printf()函数以数字的输出方式打印字符时,便输出了字符对应的ASC||码表的数字编号,应用案例如下所示

字符1和整数1的区别:

#include <stdio.h>
#include <stdlib.h>
}
/* 字符型数据存储 字符在内存中是以数字存储的,ASC||表规定了字符对应的数字编号 */
void char_asc() {

    char ch = '1';
    int num = 1;
    //字符1和数字1的区别:占据的字节数量不一样
    printf("字符1占据的字节数量是%d\t数字1占据的字节数量是%d\n",sizeof(ch),sizeof(num));
    //字符1对应的数字是49,即求ASC码值
    printf("字符1对应的ASC||码表的编号是%d\n", ch);
    printf("ch=%c\n",ch);
    printf("整数1对应的ASC||码表的字符是%c",num);
    system("pause");
}

字符0,'\0'和整数0的区别

#include <stdio.h>
#include <stdlib.h>

/* 字符0 对应的整数是48 整数0 对应的字符是空字符 \0对应的也是空字符,和整数0的效果一样 */
void main() {

    char ch_zero = '0';
    char ch_num_zero = 0;
    int  num_zero = 0;
    char ch = '\0';
    printf("ch_zero占据的字节数量是%d\tnum_zero占据的字节数量是%d\tch占据的字节数量是%d\n",sizeof(ch_zero),sizeof(num_zero),sizeof(ch));
    printf("字符0对应的整数编号是%d\n",ch_zero);//48
    printf("整数0对应的字符是[%c]\n", num_zero);
    printf("\\0对应的整数编号是[%d]\n", ch);//0
    printf("\\0的意义是[%c]\n", ch);//空字符
    getchar();

}

字符应用:实现大写转小写

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
/* 通过ASC||码的规律实现大小写转换 */
void main() {

    char input='\0';
    printf("请输入一个字母\n");
    scanf("%c",&input);
    if (input>='A'&& input <='Z') {
        printf("你输入的是大写字母,转换为小写字母的结果是%c\n",(input+32));
    }
    else if (input>='a'&&input<='z') {
        printf("你输入的是小写写字母,转换为小写字母的结果是%c\n", (input - 32));

    }
    system("pause");
}

2.8.2 字符串

字符串用于表示字符序列,也就是一串使用""包含起来的内容,接下来使用system函数调用系统命令理解下什么是字符串,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
/* 字符串的应用场景 */
void main() {

    //改变窗口的颜色
    system("color 4f");
    //改变窗口的标题
    system("title power by tony");
    getchar();
}

C语言中的字符串以/0结尾,这也就是意味着即使双引号""中什么都没有也会占据一个字节,而中文字符串中的每个字符同样会占据两个字节,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
/* 字符串常量 */
void main() {

    //字符串是以/0结尾,这里字符串A占据2个字节
    printf("字符串A占据的字节数量是%d\n",sizeof("A"));
    printf("\"\"占据的字节数量为%d\n",sizeof("")); //以\0结尾

    //字符串单个中文占据两个字节
    printf("字符串刘光磊占据的字节数量是%d",sizeof("刘光磊")); //每个中文占据两个字节,然后以\0结尾 因此是7个
    system("pause");
}


字符串加密解密的实现

#include <stdio.h>
#include <stdlib.h>
/* 字符串简单的加密 */
void main() {

    char str[5] = {'c','a','l','c','\0'};

    system(str);

    printf("加密之前str = %s\n",str);

    for (int i = 0; i < 4;i++) {
    
        str[i] += 1;
    }

    printf("加密之后str = %s\n", str);


    for (int i = 0; i < 4;i++) {
    
        str[i] -= 1;
    }
    printf("解密之后str = %s\n", str);
    system("pause");
}

使用sprintf函数实现整合字符串

#include <stdio.h>
#include <stdlib.h>
/* 通过sprintf函数打印到字符串,然后借助color命令实现窗口变色 */
void main() {

    char str[20] = {0};
    while (1) {
        for (char ch = '0'; ch <= '9'; ch++) {
            sprintf(str, "color %c%c", ch, 'e');
            system(str);

        }
    }
}

通过sprintf函数实现整合字符串

#include <stdio.h>
#include <stdlib.h>
/* 通过sprintf函数实现整合字符串 */
void main() {

    char str[100] = {0};
    sprintf(str,"title power by %s","tony");
    system(str);

}

2.9 布尔类型

bool类型只有两个值,即true和fasle,它们在内存中分别使用1和0表示,这样一个字节便可以存储bool类型的变量,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>


/* bool的使用 */
void main() {

    bool flag = true;
    //占据的字节数量为1
    printf("bool占据的字节数量是%d\n", sizeof(flag));
    //成立的结果为1
    printf("bool = %d\n", flag);

    flag = false;
    //不成立的结果为0
    printf("bool = %d\n", flag);
        system("pause");	
}

bool的应用场景就是用来判断条件表达式是否成立,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
/* bool的应用场景就是用来判断表达式是否成立 */
void main() {

    bool flag = 5 > 30;

    if (flag==true) {
        printf("条件成立\n");
    }
    else {
        printf("不成立\n");
    }
    getchar();
}

2.10 类型转换及其内存原理

2.10.1 printf与强制类型转换

printf()函数在输出数据时,不会进行数据类型转换,如果想要获取预期的结果,就需要进行强转实现,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>

/* printf()函数与强制类型转换 */
void main() {
    //因为printf函数不会进行类型转换,所以这里得到一个错误的结果858993459
    printf("%d\n",12.1);
    //12.1为浮点类型,这里使用强制类型转换实现转换为整数
    printf("%d\n",(int)12.1);	
    printf("%f\n",10); //整数按照浮点数解析,得到的结果就是0.000000
    printf("%f\n",(float)10); //强制类型转换
    getchar();
}


2.10.2 自动类型转换

表示范围小的数据和表示范围大的数据在参与运算时,运算结果的类型会自动转换为表示范围大的类型,应用案例如下所示。

#include "common.h"
/* 自动类型转换 在进行算术运算时,会发生自动类型转换 表示范围小的值自动转换为表示范围大的变量,保存精度 char->short>int->long->float->double->long double */
void main() {

    char ch = 'A';
    printf("1.0占据的字节数量是%d\n",sizeof(1.0));
    printf("字符变量ch+1的字节数量是%d\n",sizeof(ch+1));
    printf("字符变量ch+1.0的字节数量是%d\n",sizeof(ch+1.0));
    getchar();
}

2.10.3 强制类型转换

在某些应用场景下需要使用到强制类型转换,例如银行账户的取整等等,强制类型转换的应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
/* 强制类型转换 */
void main() {

    float fl = 10.8;
    float flt = 10.3;
    int num = (int)fl + flt; //20.3 先把fl强制转换为int类型,然后再和flt相加
    printf("num =%d\n",num);
    num = (int)(fl + flt);//21 先把fl和flt相加后,强制转换为int
    printf("num =%d\n", num);
    getchar();

}


而需要注意的是强制类型转换则会损失原有数据的精度,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
/* 强制类型转换的案例 */
void main() {
    //这里发生了自动类型转换
    double dbl = 3;
    printf("dbl = %f\n",dbl);

    //7.8默认是double类型,这里转换为int会损失精度
    int num = 7.8;
    //printf()函数没有进行数据类型转换
    printf("num =%d\n",num);
    getchar();
}


但是由于强制类型转换是由CPU的寄存器完成的,强制转换后不会影响原来的变量值,应用案例如下所示

#include <stdio.h>
#include <stdlib.h>
/* 强制类型转换不会改变原有的值 */
void main() {

    double dbl = 4.5;
    //赋值运算会执行类型转换,但是为了考虑到软件工程的规范,这里还是加上强制类型转换,增加代码的阅读性
    int num = (int)dbl; //强制类型转换是在CPU内部的寄存器完成的
    printf("dbl = %f\nnum =%d",dbl,num);
    getchar();
}


在进行强制类型转换时要考虑数据的极限问题,不然会引发数据溢出,应用案例如下所示。

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

/* 强制类型转换的溢出问题 */
void main() {


    int num = 256;
    //无符号的char能存储的最大值为255,这里的256超过它的最大表示范围,因此发生数据溢出
    unsigned char  ch = num;
    printf("num =%d \t ch = %u",num,ch);
    getchar();
}

2.10.4 数据类型转换的内存原理

当在进行数据类型转换时,如果该数据是有符号的,在进行数据类型转换时按照符号位数来填充,如果是无符号则按照0来填充,应用案例如下所示

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

/* 数据类型转换的内存原理 有符号 低字节转高字节按照符号位数填充 无符号 低字节转高字节按照0填充 */
void main() {
    //正数按照0填充
    char ch = 1; // 二进制表示 0000 0001
    int num = 1; // 二进制表示 0000 0000 0000 0000 0000 0000 0000 0001


    //负数按照1填充
 // 二进制表示 原码 1000 0001 反码 1111 1110 补码 1111 1111 ->ff
    ch = -1; 
    // 二进制表示 原码 1000 0000 0000 0000 0000 0000 0000 00001
    // 反码 1111 1111 1111 1111 1111 1111 1111 1110
    // 补码 1111 1111 1111 1111 1111 1111 1111 1111 -> ffffffff
    num = ch;


    unsigned char data = 255+1; // 二进制补码 1 0000 0000 但是char只能占据8位,因此这里会截取8位即0000 0000,结果位0
    printf("unsigned char data的地址是%p",&data);
    printf("data = %d",data);


    unsigned int u_num = -1; //赋值错误,能编译不意味着结果正确
                            // 1000 0000 0000 0000 0000 0000 0000 0000 0001
                            // 1111 1111 1111 1111 1111 1111 1111 1111 1110
                            // 1111 1111 1111 1111 1111 1111 1111 1111 1111 无符号解析结果为2的32次方即4294967295
    for (int i = 0; i < u_num;i++) {
        system("mspaint");
    }
getchar();
}


2.11 应用案例

  1. 使用强制数据类型转换实现偷钱程序
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>


/* 实现偷钱程序 如果账户余额的分大于等于4分就不偷钱,小于等于3分就偷走 */
void main() {

    printf("请输入你的账户余额\n");
    double balance =0.0;
    scanf("%lf",&balance);
    
    // 12.34*10=123.4 123.4+0.6=124 124/10.0=12.4 12.4>12.34
    double rest = (int)((balance * 10) + 0.6) / 10.0;
    printf("rest = %f",rest);
    if (rest<balance) {
        //
        printf("可以偷钱%.2f元",balance-rest);
    }
    
    getchar();
}


  1. 小数点后三位实现四舍五入
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>

/* 实现对小数点后三位实现四舍五入 */
void main() {
    printf("请输入四舍五入的三位小数\n");
    double input = 0.0;
    scanf("%lf",&input);
    double val = 1.235;
    //1.234*100=123.4 123.4+0.5=123 123/100.0=1.23

    // 1.235*100=123.5 123.5+0.5=124 124/100=1.24
    // 1.24>1.235
    // 1.24-1.235=0.05
    //1.235+0.05=1.24
    double result =(int)(input * 100 + 0.5) / 100.0;
    printf("result =%.2f",result);
    getchar();
}