《C语言进阶剖析》2.数据类型与变量常量

一.《第1课 - 基本数据类型》

1. 数据类型

1.1 什么是数据类型?

在汇编时期,没有数据类型的概念。汇编程序中经常需要申请1byte、2byte、4byte大小的内存空间,程序员需要记住内存的起始地址,非常麻烦。。。。。

(1)数据类型可以理解为固定内存大小的别名

(2)数据类型是创建变量的模子

      

1.2 数据类型的本质

    

2. 变量的本质  

(1)变量是一段实际连续存储空间的别名

(2)程序中通过变量来申请并命名存储空间

(3)通过变量的名字可以使用存储空间

        

3. 实例分析

【类型与变量的关系】

#include<stdio.h>

int main()
{
    char c;
    short s;
    int i;

    // 模子 和 模子所创建的变量 的大小应该相同
    printf("%zu, %zu\n", sizeof(char), sizeof(c));   // 1,1
    printf("%zu, %zu\n", sizeof(short), sizeof(s));  // 2,2
    printf("%zu, %zu\n", sizeof(int), sizeof(i));    // 4,4

    return 0;
}

【自定义类型与创建的变量】

#include<stdio.h>

typedef int INT32;
typedef unsigned char BYTE;
typedef struct _tag_TS
{
    BYTE b1;
    BYTE b2;
    short s;
    INT32 i;
}TS;

int main()
{
    INT32 i32;
    BYTE b;
    TS ts;

    // 自定义的模子大小 == 由该模子创建出来的变量的大小
    printf("%zu, %zu\n", sizeof(INT32), sizeof(i32));     // 4, 4
    printf("%zu, %zu\n", sizeof(BYTE), sizeof(b));        // 1, 1
    printf("%zu, %zu\n", sizeof(TS), sizeof(ts));         // 8, 8

    return 0;
}

二.《第2课 - 有符号数与无符号数》

1. 有符号整型的符号位

有符号整型有正数和负数,那在内存中是如何区分它们的呢?最高位用来标识数据的正负。

  • 最高位为1,表明这个数为负数
  • 最高位为0,表明这个数为正数

【有符号数的符号位】

#include <stdio.h>

int main()
{
    char c = -5;
    short s = 6;
    int i = -7;

    // 与最高位进行&运算,结果为0表示正数,结果为1表示负数
    printf("%d\n", ((c & 0x80) != 0 ));       // 1 ==> 负数
    printf("%d\n", ((s & 0x8000) != 0));      // 0 ==> 正数
    printf("%d\n", ((c & 0x80000000) != 0));  // 1 ==> 负数

    return 0;
}

2. 有符号整型的表示法

在计算机内部用补码表示有符号整型

  • 正数的补码为正数本身
  • 负数的补码为负值的绝对值各位取反后加1

举例:

    char型整数 5 的补码:0000 0101

    char型整数 -7 的补码:0000 0111 ==> 1111 1000 ==> 1111 1001

    short型整数 20 的补码:0000 0000 0001 0100

    short型整数 -13 的补码:0000 0000 0000 1101 ==> 1111 1111 1111 0010 ==> 1111 1111 1111 0011

3. 无符号整型的表示法

(1)在计算机内部用原码表示无符号数

    • 无符号数默认为正数
    • 无符号数没有符号位

(2)对于固定长度的无符号数

    • MAX_VALUE + 1 ==> MIN_VALUE
    • MIN_VALUE - 1 ==> MAX_VALUE

(3)signed和unsigned

    • C语言中整型变量默认为有符号的类型
    • 用unsigned关键字声明整型变量为无符号类型
1 int i;  // 默认为有符号整数
2 
3 signed int j;  // 显式声明变量为有符号整数
4 
5 unsigned int k ;  // 声明为无符号整数

下面看两个笔试中容易考察到的题目:

(1)当有符号数遇到无符号数时,有符号数在内存中的值会被看作无符号数

#include<stdio.h>

int main()
{
    unsigned int i = 5;
    int j = -10;

    // 有符号数遇到无符号数,会被看作无符号数
    // -10 在内存中表示为 0000 0000 0000 0000 0000 0000 0000 1010 ==>
    // 1111 1111 1111 1111 1111 1111 1111 0101 ==>
    // 1111 1111 1111 1111 1111 1111 1111 0110  ==> 这个看作无符号数是一个非常大的数
    if ((i + j) > 0)
    {
        printf("i+j >= 0\n");  // 程序走到这个分支
    }
    else
    {
        printf("i+j <= 0\n");
    }

    return 0;
}

(2)错误的使用了unsigned,unsigned修饰的无符号数不会是一个负数

#include <stdio.h>

int main()
{
    unsigned int i = 0;

    // 当i = 0时,i--变为最大值
    // 前面讲过 MIN_VALUE - 1 ==> MAX_VALUE
    for (i = 9; i >= 0; i--) // i >= 0一直成立
    {
        printf("i = %u\n", i);
    }

    return 0;
}

三.《第3课 - 浮点数的秘密》

1. 浮点数在内存中的存储方式

    

float与double类型的数据在计算机内部的表示法是相同的,但由于所占存储空间的不同,其分别能够表示的数值范围和精度不同

2. 浮点数的转换

2.1 浮点数的转换方法

如何将十进制的浮点数转换为内存中二进制表示的浮点数呢?按照下面三个步骤:

    ① 将浮点数转换为二进制

    ② 用科学计数法表示二进制浮点数

    ③ 计算指数偏移后的值(需要加上偏移量,float型:指数 + 127,double型:指数 + 1023)

比如对于指数6,float型偏移量就为 127 + 6 = 133,double型偏移量为1023 + 6 = 1029

2.2 浮点数的转换示例

下面以十进制的 8.25 演示一下上述的转换方法。

    ① 将8.25转换为二进制(注意小数的二进制表示方法)    ==>  1000.01

    ② 用科学计数法表示1000.01(注意这里是二进制,2^3)   ==>  1.00001(2^3)

    ③ 指数3偏移后为:127 + 3 = 130

所以浮点数8.25对应的符号位、指数、尾数分别为:

    符号位:0

    指数:130 ==> 10000010

    小数:00001   // 要将小数转为尾数,需要在后面补0

8.25在内存中的二进制表示为:0 1000 0010 000 01000000 0000 0000 0000 = 0x41040000

下面我们写代码验证一下:

#include <stdio.h>

int main()
{
    float f = 8.25;
    unsigned int *p = (int *)&f;

    printf("f = 0x%08x\n", *p);

    return 0;
}

结果和我们前面分析的相同。

    

3. 浮点数的秘密

 我们知道int型数据占用4个字节,表示的范围为:【-231 , 231-1】;float型也占用4个字节,表示的范围为:【-3.4*1038,3.4*1038】。

看到这里不少人很奇怪,4个字节按照排列组合能表示的数据范围应该是固定的,为什么float类型表示的范围却比int型的大?这里我们就要揭露一下float类型的秘密了。

(1)float能表示的具体数值的个数与int型相同,因为都是4字节,排列组合都是232-1

(2)之所以float表示出来的范围比int型大,是因为float可表示的数字之间是不连续的,数据之间存在跳跃

(3)float只是一种近似的表示法,不能作为精确数使用

(4)由于float类型的内存表示方法相对复杂,float的运算速度比int慢很多

※ 因为double与float具有相同的内存表示法,所以double也是不精确的。由于double占用的内存较多,所能表示的精度比float高。

【float类型的不精确示例】

#include<stdio.h>

int main()
{
    float f1 = 3.1415f;
    float f2 = 123456789;

    // 精确到小数点后面的10位
    printf("%0.10f\n", f1);    // 3.1414999962  ==>  不等于3.1415,表示的结果是不精确的
    printf("%0.10f\n", f2);    // 123456792.0000000000  ==>  与123456789不相等,表示的数据是不连续的、跳跃的

    return 0;
}

再添加0.25和1.25的转换方法,加深理解:

0.25

    ① 0.25 转换为二进制 0.01

    ② 二进制0.01用科学计数法表示 1.0*(2^-2),指数为 -2 + 127 = 125

    ③ 小数为0

所以0.25在内存中表示为:0  01111101  00000000000000000000000 = 0x3e800000

1.25

    ① 1.25转换为二进制 1.01

    ② 二进制1.01用科学计数法表示1.01*(2^0) ,指数为 0 + 127 = 127

    ③ 小数为0.25

所以1.25在内存中表示为:0  01111111  01000000000000000000000 = 0x3fa00000


四.《第4课 - 类型转换》

1. 类型之间的转换

C语言的数据类型之间可以进行转换,包括以下两种:

  • 强制类型转换    ==>    程序员人为的将数据类型进行转换
  • 隐式类型转换    ==>    编译器主动进行的数据类型转换

【类型之间的转换】

#include<stdio.h>

int main()
{
    long l = 800;
    int i = (int)l; // 强制类型转换

    short s = 800;
    int k = s;      // 隐式类型转换

    return 0;
}

2. 强制类型转换

(1)强制类型转换的语法

        (Type)var_name

        (Type)value

(2)强制类型转换的结果

    • 如果目标类型能够容纳目标值,那么结果不变
    • 如果目标类型不能容纳目标值,那么结果将产生截断

(3)※注意:不是所有的强制类型转换都能成功,当不能进行强制类型转换时,编译器将产生错误信息

【强制类型转换分析】

#include <stdio.h>

struct TS {
    int i;
    int j;
};

struct TS ts;

int main()
{
    short s = 0x1122;

    char c = (short)s;  // 目标类型不能容纳目标值,产生截断,取内存中的最后一个字节,0x22

    int i = (int)s;     // 目标类型能够容纳目标值,结果不变,0x1122

    int j = (int)3.1415;    // 浮点数和整型在内存中的表示方法不同,截断的方法就是舍弃小数部分,取整数部分
                            // 3

    unsigned int p = (unsigned int)&ts; // 在32位系统上指针为4字节,是可以的;在64位系统上指针为8字节,会产生截断

    //long l = (long)ts;   // 两种类型之间不能进行强制类型转换,编译报错   error: aggregate value used where an integer was expected
    // ts = (struct TS)l;  // 与上面相同,编译报错

    printf("s = 0x%x\n", s);    // 0x1122
    printf("c = 0x%x\n", c);    // 0x22
    printf("i = 0x%x\n", i);    // 0x1122
    printf("j = 0x%x\n", j);    // 0x3
    printf("p = 0x%x\n", p);    // 0x601044
    printf("&ts = %p\n", &ts);  // 0x601044

    return 0;
}

3. 隐式类型转换

(1)隐式类型转换

 隐式类型转换是编译器主动进行的数据类型转换。 

1 char c = 0;  // 变量c占用1个字节
2 
3 short s = c; // c到s -> 隐式类型转换
4 int i = s;   // s到i -> 隐式类型转换
5 long l = i;  // i到l -> 隐式类型转换

※注意:

  ① 低类型到高类型的隐式转换是安全的,不会产生截断  (高类型与低类型是相对的,比较的是占用内存的大小)

  ② 高类型到低类型的隐式转换是不安全的,导致不正确的结果

(2)隐式类型转换的发生点  

    • 算术运算中,低类型转换为高类型
    • 赋值表达式中,表达式的值转换为左边变量的类型
    • 函数调用时,实参转换为形参的类型
    • 函数返回值,return表达式转换为返回值类型 

※ 标准C编译器的类型检查是比较宽松的,因此隐式类型转换可能带来意外的错误,写程序时需要时刻注意隐式类型转换!!! 

  

【隐式类型转换分析】

#include <stdio.h>

int main()
{
    char c = 'a';   // 97

    int i = c;  // 低类型==>高类型,安全

    unsigned int j = 0x11223344;

    short s = j; // 高类型==>低类型,不安全

    printf("c = %c\n", c);      // ‘a’
    printf("i = %d\n", i);      // 97
    printf("j = 0x%x\n", j);    // 0x11223344
    printf("s = %x\n", s);      // 0x3344
    printf("sizeof(c+s) = %d\n", sizeof(c+s)); // char和short都会先转化为int,然后进行运算,结果为4

    return 0;
}


五.《第5课 - 变量属性》

1. C语言变量的属性

C语言中的变量可以拥有自己的属性。在定义变量时可以加上属性关键字,用来指明变量的特有意义

语法:

  property type var_name

示例:

1 auto char i;
2 register int j;
3 static long k;
4 extern double m;

2. auto关键字

(1)auto属性关键字将被修饰的变量存储在栈上

(2)C编译器默认所有的局部变量都是auto属性的(auto是局部变量的默认属性),即局部变量存储在栈上

示例:

1 void f(){
2    int i;        // 局部变量默认属性为auto
3    auto int j;   // 显式声明局部变量为auto属性
4 }

3. register关键字

(1)register关键字请求编译器将局部变量存储于寄存器中,而不是内存中,以加快其存取速度,多用于修饰需要频繁使用的局部变量    

    ※※ register不能修饰全局变量

(2)不能使用 & 运算符获取register变量的地址,因为寄存器是没有地址的,只有内存才有地址

#include<stdio.h>

// register int g_v;   // 全局变量的生命周期从程序运行到程序结束,那么在整个过程中都要占用寄存器,但CPU寄存器的数量是有限的,长时间占用会影响CPU工作
                       // 因此不允许register修饰全局变量,register修饰全局变量编译器会直接报错,error!
int main()
{
    register char var;
    // printf("0x%08X\n", &var);  // 寄存器变量没有地址,编译报错,error!

    return 0;
}

(3)register只是请求寄存器变量,但不一定请求成功

(4)由于register修饰的变量存储在寄存器中,因此该变量必须是CPU寄存器可以接受的值

下面是libevent中一个拷贝字符串的函数,使用了register关键字

size_t event_strlcpy_(char *dst, const char *src, size_t size)
{
    register char *d = dst;
    register const char *s = src;
    register size_t n = size;

    /* Copy as many bytes as will fit */
    if (n != 0 && --n != 0) {
        do {
            if ((*d++ = *s++) == 0)
                break;
        } while (--n != 0);
    }

    /* Not enough room in dst, add NUL and traverse rest of src */
    if (n == 0) {
        if (size != 0)
            *d = '\0';      /* NUL-terminate dst */
        while (*s++)
            ;
    }

    return (s - src - 1);   /* count does not include NUL */
}

前面说了register可以加快局部变量的存取速度,可能不太直观,我们通过下面的例子直观的感受一下!   0.68s  VS  0.195s

  

   

4. static关键字

(1)static 关键字指明变量的"静态"属性,局部变量存储在程序静态区(普通的局部变量存储在栈上)

(2)sttaic关键字同时具有"作用域限定符"

  • static修饰的全局变量,作用域是声明该变量的文件中,其它文件不能使用
  • static修饰的函数,作用域是声明该函数的文件中,其它文件不能使用
#include <stdio.h>

int g_v;             // 全局变量,程序的任意地方均能访问
static int g_vs;     // 静态全局变量,只有当前文件中可访问

int main()
{
   int var;          // 局部变量,在栈上分配空间
   static int svar;  // 静态局部变量,在静态数据区分配空间

   return 0;
}

【auto、register、static对比分析】

#include<stdio.h>

int f1()
{
    int r = 0;
    r++;

    return r;
}

int f2()
{
    static int r = 0; // 静态局部变量,只初始化一次
    r++;

    return r;
}

int main()
{
    auto int i = 0;      // 显式声明auto属性,i为栈变量
    static int k = 0;    // 局部变量k的存储区位于静态区,作用域位于main中
    register int j = 0;  // 向编译器申请将j存储于寄存器中

    // 两个变量在前面是相邻定义的,地址差别却非常之大,就是前者存储在栈上,后者存储在静态区
    printf("%p\n", &i);   // 0x7ffd7784424c
    printf("%p\n", &k);   // 0x601048
    // printf("%p\n", &j);  // compile error,寄存器变量不能取地址


    for (i = 0; i < 5; i++)
    {
        printf("%d\n", f1());    // 1 1 1 1 1
    }

    for (i = 0; i < 5; i++)
    {
        printf("%d\n", f2());    // 1 2 3 4 5
    }

    return 0;
}

5. extern关键字

(1)extern 用于声明"外部"定义的变量和函数

  • extern 变量在其它地方分配空间
  • extern 函数在文其它地方定义
#include<stdio.h>

extern int g_i;  // 告诉编译器g_i在其它的地方定义

int main()
{
    printf("%d\n", g_i);    // 先使用,后面链接时在其它地方再寻找

    return 0;
}

int g_i = 0;

(2)extern用于告诉编译器用C方式编译代码

【static和extern的使用】

// g.c

static int g_i;  // g_i只能在本文件中使用

int getI()
{
    return g_i;
}

// main.c

#include<stdio.h>

extern int getI(); // extern声明getI()是在其它地方定义的

int main()
{
    printf("%d\n", getI());    // 0

    return 0;
}


六.《第9课 - const 和 volatile分析》

1. const只读变量

(1)const修饰的变量是只读的,本质上还是变量,并不是真正意义上的常量         ※ const只是告诉编译器该变量不能出现在赋值符号的左边

(2)const修饰的局部变量在栈上分配空间;const修饰的全局变量在全局数据区分配空间

(3)const只在编译期间有用(检查const修饰的变量有没有出现在赋值符号左边,如果有就会编译报错),在运行期间无用

#include <stdio.h>

int main()
{
    const int cc = 1;

    int *p = (int*)&cc;

    // cc = 10;  // compile error: assignment of read-only variable ‘cc’

    printf("cc = %d\n", cc);    // cc = 1

    *p = 10;

    printf("cc = %d\n", cc);    // cc = 10

    return 0;
}

2. const全局变量的分歧

(1)在标准C语言编译器中,const修饰的全局变量仍然存储于全局数据区,并没有改变存储方式,通过指针可以隐式的修改全局变量的值。

(2)在现代C语言编译器中,将const 修饰的全局变量分配在只读存储区,改变了存储方式,通过指针隐式修改会导致程序段错误

#include <stdio.h>

const int g_ci = 100;

int main()
{
    int *p = (int *)&g_ci;

    // g_ci = 10; // compile error: assignment of read-only variable ‘g_ci’

    printf("g_ci = %d\n", g_ci);

    *p = 10;    // 通过指针隐式修改

    printf("g_ci = %d\n", g_ci);

    return 0;
}

使用gcc编译执行(现代C编译器)    ==>    段错误

  

使用bcc32编译执行(标准C编译器)  ==>    修改成功

  

3. const的本质

(1)C 语言中的const 使得变量具有只读属性

(2)现代C编译器中的const将具有全局生命周期的变量(全局变量 +  static修饰的局部变量)存储于只读存储区

          

【static修饰局部变量】

#include <stdio.h>

int main()
{
    const static int si = 100;  // const修饰static修饰的局部变量

    int *p = (int *)&si;

    *p = 1;    // 使用gcc、VS2010编译执行会产生段错误
               // 使用bcc32编译执行,可以修改si的值为1

    printf("si = %d\n", si);

    return 0;
}

【const的本质分析】

#include <stdio.h>

const int g_array[5] = {0};

void modify(int* p, int v)
{
    *p = v;
}

int main()
{
    int const i = 0;  // const放在int前后都可以
    const static int j = 0;
    int const array[5] = {0};

    modify((int*)&i, 1);           // ok
    modify((int*)&j, 2);           // error,j存储在只读存储区
    modify((int*)&array[0], 3);    // ok
    modify((int*)&g_array[0], 4);  // error,g_array[5]数组存储在只读存储区

    printf("i = %d\n", i);
    printf("j = %d\n", j);
    printf("array[0] = %d\n", array[0]);
    printf("g_array[0] = %d\n", g_array[0]);

    return 0;
}

4. const修饰函数参数和返回值

(1)const 修饰函数参数表示在函数体内不希望改变参数的值

(2)const 修饰函数返回值表示返回值不可改变,多用于返回指针的情形

  TIP:C 语言的字符串字面量存储于只读存储区中,在程序中需要使用 const char* 指针

      

【const修饰函数参数与返回值】

#include <stdio.h>

const char* f(const int i)
{
    // i = 5;  // error, i不能作为左值

    return "swj striving! 2019-12-23 22:23:57";
}

int main()
{
    char* pc = f(0); // 编译会报warning,函数f的返回值为const char*

    printf("%s\n", pc);

    pc[1] = '_';    // error,试图修改只读存储区中的字符串

    printf("%s\n", pc);

    return 0;
}

5. 深藏不露的volatile

(1)volatile 可理解为 "编译器警告指示字"

(2)volatile 告诉编译器必须每次去内存中取变量值

(3)volatile 主要修饰可能被多个线程访问的变量

(4)volatile 也可以修饰可能被未知因数更改的变量

 

6. 有趣的问题

  const  volatile int i  =  0;

  ⅰ:变量 i 具有什么样的特性?   i为int型变量,每次都必须到内存取值,且i是只读变量

  ⅱ:编译器如何处理这个变量?  i不能作为左值

七.《第13课 - 接续符和转义符》

1. 接续符

(1)接续符的意义:C语言中的接续符(\)是指示编译器行为的利器

    

/**********

 额外的思考发现:

   ① 接续符由预处理器在预处理阶段处理

   ② 接续符在读取后面的数据时,应该是根据"贪心法则"读取的,可以参考gcc -E的结果,接续符后面一行并没有全部读入

   ③ #define 宏代码块只能定义在同一行,不能跨行(语法规定);可以使用接续符多行显示宏代码块,代码可读性更高。

**********/

(2)接续符的使用

  • 编译器会将接续符剔除,跟在接续符后面的字符自动接续到前一行
  • 接续单词时,接续符之后不能有空格,接续符的下一行之前也不能有空格
  • 接续符适合在定义宏代码块时使用

【接续符在宏中的应用】

#include <stdio.h>

// 该宏实现两个变量值的交换
// 使用接续符,宏代码块更美观,可读性更强
#define SWAP(a,b)      \
{                      \
    int temp = a;      \
    a = b;             \
    b = temp;          \
}

int main()
{
    int a = 1;
    int b = 2;
    int c = 3;

    SWAP(a,b);

    printf("a = %d, b = %d\n", a, b);   // a = 2, b = 1

    SWAP(b,c);

    printf("b = %d, c = %d\n", b, c);   // b = 3, c = 1

    return 0;
}

2. 转义符

(1)C语言的转义符(\)主要用于表示无回显字符,也可用于表示常规字符  

       

(2)当反斜杠作为转义字符使用时必须出现在单引号或者双引号之间

#include <stdio.h>

int main()
{
    char enter = '\n';

    char* p = "\141\t\x62";   // 八进制的141对应十进制的97,即a的ASCII码
                               // 十六进制的62对应十进制的98,即b的ASCII码
                               // \t 表示tab制表符

    printf("%s", p);           // a    b
    printf("%c", enter);       // 换行

    return 0;
}

3. 小结

C语言中的反斜杠(\)同时具有接续符和转义符的作用

  • 作为接续符使用时可以直接出现在程序中
  • 作为转义符使用时需要出现在单引号或双引号之间


八.《第14课 - 单引号和双引号》

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值