Skip to content

C语言基本概念

原码反码补码

  • 计算机只能识别0和1, 所以计算机中存储的数据都是以0和1的形式存储的

  • 数据在计算机内部是以补码的形式储存的, 所有数据的运算都是以补码进行的

  • 正数的原码、反码和补码

    • 正数的原码、反码和补码都是它的二进制
    • 例如: 12的原码、反码和补码分别为
      • 0000 0000 0000 0000 0000 0000 0000 1100
      • 0000 0000 0000 0000 0000 0000 0000 1100
      • 0000 0000 0000 0000 0000 0000 0000 1100
  • 负数的原码、反码和补码

    • 二进制的最高位我们称之为符号位, 最高位是0代表是一个正数, 最高位是1代表是一个负数
    • 一个负数的原码, 是将该负数的二进制最高位变为1
    • 一个负数的反码, 是将该数的原码除了符号位以外的其它位取反
    • 一个负数的补码, 就是它的反码 + 1
    • 例如: -12的原码、反码和补码分别为
c
0000 0000 0000 0000 0000 0000 0000 1100 // 12二进制
1000 0000 0000 0000 0000 0000 0000 1100 // -12原码
1111 1111 1111 1111 1111 1111 1111 0011  // -12反码
1111 1111 1111 1111 1111 1111 1111 0100 // -12补码
  • 负数的原码、反码和补码逆向转换

    • 反码 = 补码-1
    • 原码= 反码最高位不变, 其它位取反
c
1111 1111 1111 1111 1111 1111 1111 0100 // -12补码
1111 1111 1111 1111 1111 1111 1111 0011  // -12反码
1000 0000 0000 0000 0000 0000 0000 1100 // -12原码

  • 为什么要引入反码和补码

    • 在学习本节内容之前,大家必须明白一个东西, 就是计算机只能做加法运算, 不能做减法和乘除法, 所以的减法和乘除法内部都是用加法来实现的

      • 例如: 1 - 1, 内部其实就是 1 + (-1);
      • 例如: 3 * 3, 内部其实就是 3 + 3 + 3;
      • 例如: 9 / 3, 内部其实就是 9 + (-3) + (-3) + (-3);
  • 首先我们先来观察一下,如果只有原码会存储什么问题

    • 很明显, 通过我们的观察, 如果只有原码, 1-1的结果不对
c
// 1 + 1
 0000 0000 0000 0000 0000 0000 0000 0001 // 1原码
+0000 0000 0000 0000 0000 0000 0000 0001 // 1原码
 ---------------------------------------
 0000 0000 0000 0000 0000 0000 0000 0010  == 2

 // 1 - 1; 1 + (-1);
 0000 0000 0000 0000 0000 0000 0000 0001 // 1原码
+1000 0000 0000 0000 0000 0000 0000 0001 // -1原码
 ---------------------------------------
 1000 0000 0000 0000 0000 0000 0000 0010 == -2
  • 正是因为对于减法来说,如果使用原码结果是不正确的, 所以才引入了反码
  • 通过反码计算减法的结果, 得到的也是一个反码;
  • 将计算的结果符号位不变其余位取反,就得到了计算结果的原码
  • 通过对原码的转换, 很明显我们计算的结果是-0, 符合我们的预期
c
// 1 - 1; 1 + (-1);
0000 0000 0000 0000 0000 0000 0000 0001 // 1反码
1111 1111 1111 1111 1111 1111 1111 1110   // -1反码
---------------------------------------
1111 1111 1111 1111 1111 1111 1111 1111 // 计算结果反码
1000 0000 0000 0000 0000 0000 0000 0000 // 计算结果原码 == -0
  • 虽然反码能够满足我们的需求, 但是对于0来说, 前面的负号没有任何意义, 所以才引入了补码
  • 由于int只能存储4个字节, 也就是32位数据, 而计算的结果又33位, 所以最高位溢出了,符号位变成了0, 所以最终得到的结果是0
c
// 1 - 1; 1 + (-1);
0000 0000 0000 0000 0000 0000 0000 0001 // 1补码
1111 1111 1111 1111 1111 1111 1111 1111   // -1补码
---------------------------------------
10000 0000 0000 0000 0000 0000 0000 0000 // 计算结果补码
0000 0000 0000 0000 0000 0000 0000 0000 //  == 0

最后,如果有任何疑问,请加微信 leader_fengy 拉你进学习交流群。

开源不易,码字不易,如果觉得有价值,欢迎分享支持。

什么是环境变量?

  • 打开我们添加环境变量的两个目录, 不难发现里面大部分都是.exe的可执行程序
  • 如果我们不配置环境变量, 那么每次我们想要使用这些"可执行程序"都必须"先找到这些应用程序对应的文件夹"才能使用
  • 为了方便我们在电脑上"任何地方"都能够使用这些"可执行程序", 那么我们就必须添加环境变量, 因为Windows执行某个程序的时候, 会先到"环境变量中Path指定的路径中"去查找

为什么要配置系统变量,不配置用户变量

  • 用户变量只针对使用这台计算机指定用户
  • 一个计算机可以设置多个用户, 不同的用户用不同的用户名和密码
  • 当给计算机设置了多个用户的时候,启动计算机的时候就会让你选择哪个用户登录
  • 系统变量针对使用这台计算机的所有用户
  • 也就是说设置了系统变量, 无论哪个用户登录这台计算机都可以使用你配置好的工具
  • 最后,如果大家想要实时关注我们更新的文章以及分享的干货的话,可以扫描下方二维码关注我们的微信公众号“代码情缘”。 我的公众号

位运算符

  • 程序中的所有数据在计算机内存中都是以二进制的形式储存的。
  • 位运算就是直接对整数在内存中的二进制位进行操作
  • C语言提供了6个位操作运算符, 这些运算符只能用于整型操作数
符号名称运算结果
&按位与同1为1
|按位或有1为1
^按位异或不同为1
~按位取反0变1,1变0
<<按位左移乘以2的n次方
>>按位右移除以2的n次方

  • 按位与:
  • 只有对应的两个二进位均为1时,结果位才为1,否则为0
  • 规律: 二进制中,与1相&就保持原位,与0相&就为0
9&5 = 1

 1001
&0101
------
 0001

  • 按位或:
  • 只要对应的二个二进位有一个为1时,结果位就为1,否则为0
c
9|5 = 13

 1001
|0101
------
 1101

  • 按位异或
  • 当对应的二进位相异(不相同)时,结果为1,否则为0
  • 规律:
  • 相同整数相的结果是0。比如55=0
  • 多个整数相^的结果跟顺序无关。例如: 567=576
  • 同一个数异或另外一个数两次, 结果还是那个数。例如: 577 = 5
c
9^5 = 12

 1001
^0101
------
 1100

  • 按位取反
  • 各二进位进行取反(0变1,1变0)
c
~9 =-10
0000 0000 0000 0000 0000 1001 // 取反前
1111 1111 1111 1111 1111 0110 // 取反后

// 根据负数补码得出结果
1111 1111 1111 1111 1111 0110 // 补码
1111 1111 1111 1111 1111 0101 // 反码
1000 0000 0000 0000 0000 1010 // 源码 == -10

  • 位运算应用场景:
  • 判断奇偶(按位或)
c
偶数: 的二进制是以0结尾
8   -> 1000
10  -> 1010
 
奇数: 的二进制是以1结尾
9   -> 1001
11  -> 1011

任何数和1进行&操作,得到这个数的最低位
1000
&0001
-----
0000  // 结果为0, 代表是偶数

1011
&0001
-----
0001 // 结果为1, 代表是奇数
  • 权限系统

    c
    enum Unix {
      S_IRUSR = 256,// 100000000 用户可读
      S_IWUSR = 128,//  10000000 用户可写
      S_IXUSR = 64,//    1000000 用户可执行
      S_IRGRP = 32,//     100000 组可读
      S_IWGRP = 16,//      10000 组可写
      S_IXGRP = 8,//        1000 组可执行
      S_IROTH = 4,//         100 其它可读
      S_IWOTH = 2,//          10 其它可写
      S_IXOTH = 1 //           1 其它可执行
     };
    // 假设设置用户权限为可读可写
    printf("%d\n", S_IRUSR | S_IWUSR); // 384 // 110000000
  • 交换两个数的值(按位异或)

    c
     a = a^b;
     b = b^a;
     a = a^b;

  • 按位左移
    • 把整数a的各二进位全部左移n位,高位丢弃,低位补0
      • 由于左移是丢弃最高位,0补最低位,所以符号位也会被丢弃,左移出来的结果值可能会改变正负性
    • 规律: 左移n位其实就是乘以2的n次方
c
2<<1; //相当于 2 *= 2 // 4
  0010
<<0100

2<<2; //相当于 2 *= 2^2; // 8
  0010
<<1000
  • 按位右移
    • 把整数a的各二进位全部右移n位,保持符号位不变
      • 为正数时, 符号位为0,最高位补0
      • 为负数时,符号位为1,最高位是补0或是补1(取决于编译系统的规定)
    • 规律: 快速计算一个数除以2的n次方
c
2>>1; //相当于 2 /= 2 // 1
  0010
>>0001
4>>2; //相当于 4 /= 2^2 // 1
  0100
>>0001
  • 练习:
    • 写一个函数把一个10进制数按照二进制格式输出
c
#include <stdio.h>
void printBinary(int num);
int main(int argc, const char * argv[]) {
    printBinary(13);
}
void printBinary(int num){
    int len = sizeof(int)*8;
    int temp;
    for (int i=0; i<len; i++) {
        temp = num; //每次都在原数的基础上进行移位运算
        temp = temp>>(31-i); //每次移动的位数
        int t = temp&1; //取出最后一位
        if(i!=0&&i%4==0)printf(" "); printf("%d",t);
    }
}

最后,如果有任何疑问,请加微信 leader_fengy 拉你进学习交流群。

开源不易,码字不易,如果觉得有价值,欢迎分享支持。

变量内存分析

  • 内存模型
  • 内存模型是线性的(有序的)
  • 对于 32 机而言,最大的内存地址是2^32次方bit(4294967296)(4GB)
  • 对于 64 机而言,最大的内存地址是2^64次方bit(18446744073709552000)(171亿GB)

  • CPU 读写内存

    • CPU 在运作时要明确三件事
    • 存储单元的地址(地址信息)
    • 器件的选择,读 or 写 (控制信息)
    • 读写的数据 (数据信息)
  • 如何明确这三件事情

    • 通过地址总线找到存储单元的地址
    • 通过控制总线发送内存读写指令
    • 通过数据总线传输需要读写的数据
  • 地址总线: 地址总线宽度决定了CPU可以访问的物理地址空间(寻址能力)
  • 例如: 地址总线的宽度是1位, 那么表示可以访问 0 和 1的内存
+ 例如: 地址总线的位数是2位, 那么表示可以访问 00、01、10、11的内存
>
  • 数据总线: 数据总线的位数决定CPU单次通信能交换的信息数量

  • 例如: 数据总线:的宽度是1位, 那么一次可以传输1位二进制数据
 + 例如: 地址总线的位数是2位,那么一次可以传输2位二进制数据
  • 控制总线: 用来传送各种控制信号
  • 写入流程

    • CPU 通过地址线将找到地址为 FFFFFFFB 的内存
    • CPU 通过控制线发出内存写入命令,选中存储器芯片,并通知它,要其写入数据。
    • CPU 通过数据线将数据 8 送入内存 FFFFFFFB 单元中
  • 读取流程

  • CPU 通过地址线将找到地址为 FFFFFFFB 的内存
  • CPU 通过控制线发出内存读取命令,选中存储器芯片,并通知它,将要从中读取数据
  • 存储器将 FFFFFFFB 号单元中的数据 8 通过数据线送入 CPU寄存器中
  • 变量的存储原则
  • 先分配字节地址大内存,然后分配字节地址小的内存(内存寻址是由大到小)

  • 变量的首地址,是变量所占存储空间字节地址(最小的那个地址 )

  • 低位保存在低地址字节上,高位保存在高地址字节上

    c
    10的二进制: 0b00000000 00000000 00000000 00001010
               高字节←                        →低字节

最后,如果有任何疑问,请加微信 leader_fengy 拉你进学习交流群。

开源不易,码字不易,如果觉得有价值,欢迎分享支持。

如何创建C语言程序

  • 这个世界上, 几乎所有程序员入门的第一段代码都是Hello World.
  • 原因是当年C语言的作者Dennis Ritchie(丹尼斯 里奇)在他的名著<The C Programming Language>中第一次引入, 传为后世经典, 其它语言亦争相效仿, 以示敬意

如何创建C语言文件

最后,如果大家想要实时关注我们更新的文章以及分享的干货的话,可以扫描下方二维码关注我们的微信公众号“代码情缘”。

我的公众号

char类型内存存储细节

  • char类型基本概念
  • char是C语言中比较灵活的一种数据类型,称为“字符型”
  • char类型变量占1个字节存储空间,共8位
  • 除单个字符以外, C语言的的转义字符也可以利用char类型存储
字符意义
\b退格(BS)当前位置向后回退一个字符
\r回车(CR),将当前位置移至本行开头
\n换行(LF),将当前位置移至下一行开头
\t水平制表(HT),跳到下一个 TAB 位置
\0用于表示字符串的结束标记
\代表一个反斜线字符 \
\"代表一个双引号字符"
\'代表一个单引号字符'
  • char型数据存储原理
  • 计算机只能识别0和1, 所以char类型存储数据并不是存储一个字符, 而是将字符转换为0和1之后再存储
  • 正是因为存储字符类型时需要将字符转换为0和1, 所以为了统一, 老美就定义了一个叫做ASCII表的东东
  • ASCII表中定义了每一个字符对应的整数
c
    char ch1 = 'a';     printf("%i\n", ch1); // 97    char ch2 = 97;    printf("%c\n", ch2); // a
  • char类型注意点
  • char类型占一个字节, 一个中文字符占3字节(unicode表),所有char不可以存储中文
c
char c = '我'; // 错误写法
  • 除转义字符以外, 不支持多个字符
c
char ch = 'ab'; // 错误写法
  • char类型存储字符时会先查找对应的ASCII码值, 存储的是ASCII值, 所以字符6和数字6存储的内容不同
c
char ch1 = '6'; // 存储的是ASCII码 64char ch2 = 6; //  存储的是数字 6
  • 练习
  • 定义一个函数, 实现输入一个小写字母,要求转换成大写输出

类型说明符

  • 类型说明符基本概念
  • C语言提供了说明长度说明符号位的两种类型说明符, 这两种类型说明符一共有4个:
  • short 短整型 (说明长度)
  • long 长整型 (说明长度)
  • signed 有符号型 (说明符号位)
  • unsigned 无符号型 (说明符号位)
  • 这些说明符一般都是用来修饰int类型的,所以在使用时可以省略int
  • 这些说明符都属于C语言关键字

short和long

  • short和long可以提供不同长度的整型数,也就是可以改变整型数的取值范围。
  • 在64bit编译器环境下,int占用4个字节(32bit),取值范围是-2^31 ~ 2^31-1;
  • short占用2个字节(16bit),取值范围是-2^15 ~ 2^15-1;
  • long占用8个字节(64bit),取值范围是-2^63 ~ 2^63-1
  • 总结一下:在64位编译器环境下:
  • short占2个字节(16位)
  • int占4个字节(32位)
  • long占8个字节(64位)。
  • 因此,如果使用的整数不是很大的话,可以使用short代替int,这样的话,更节省内存开销。

世界上的编译器林林总总,不同编译器环境下,int、short、long的取值范围和占用的长度又是不一样的。比如在16bit编译器环境下,long只占用4个字节。不过幸运的是,ANSI
ISO制定了以下规则:

  • short跟int至少为16位(2字节)
  • long至少为32位(4字节)
  • short的长度不能大于int,int的长度不能大于long
  • char一定为为8位(1字节),毕竟char是我们编程能用的最小数据类型
  • 可以连续使用2个long,也就是long long。一般来说,long long的范围是不小于long的,比如在32bit编译器环境下,long long占用8个字节,long占用4个字节。不过在64bit编译器环境下,long long跟long是一样的,都占用8个字节。
c
#include <stdio.h>

int main()
{
    // char占1个字节, char的取值范围 -2^7~2^7
    char num = 129;
    printf("size = %i\n", sizeof(num)); // 1
    printf("num = %i\n", num); // -127
    // short int 占2个字节, short int的取值范围 -2^15~2^15-1
    short int num1 = 32769;// -32767
    printf("size = %i\n", sizeof(num1)); // 2
    printf("num1 = %hi\n", num1);

    // int占4个字节, int的取值范围 -2^31~2^31-1
    int num2 = 12345678901;
    printf("size = %i\n", sizeof(num2)); // 4
    printf("num2 = %i\n", num2);

    // long在32位占4个字节, 在64位占8个字节
    long int num3 = 12345678901;
    printf("size = %i\n", sizeof(num3)); // 4或8
    printf("num3 = %ld\n", num3);

    // long在32位占8个字节, 在64位占8个字节 -2^63~2^63-1
    long long int num4 = 12345678901;
    printf("size = %i\n", sizeof(num4)); // 8
    printf("num4 = %lld\n", num4);
    
    // 由于short/long/long long一般都是用于修饰int, 所以int可以省略
    short num5 = 123;
    printf("num5 = %lld\n", num5);
    long num6 = 123;
    printf("num6 = %lld\n", num6);
    long long num7 = 123;
    printf("num7 = %lld\n", num7);
    return 0;
}

signed和unsigned

  • 首先要明确的:signed int等价于signed,unsigned int等价于unsigned
  • signed和unsigned的区别就是它们的最高位是否要当做符号位,并不会像short和long那样改变数据的长度,即所占的字节数。
  • signed:表示有符号,也就是说最高位要当做符号位。但是int的最高位本来就是符号位,因此signed和int是一样的,signed等价于signed int,也等价于int。signed的取值范围是-2^31 ~ 2^31 - 1
  • unsigned:表示无符号,也就是说最高位并不当做符号位,所以不包括负数。
  • 因此unsigned的取值范围是:0000 0000 0000 0000 0000 0000 0000 0000 ~ 1111 1111 1111 1111 1111 1111 1111 1111,也就是0 ~ 2^32 - 1
c
#include <stdio.h>

int main()
{
    // 1.默认情况下所有类型都是由符号的
    int num1 = 9;
    int num2 = -9;
    int num3 = 0;
    printf("num1 = %i\n", num1);
    printf("num2 = %i\n", num2);
    printf("num3 = %i\n", num3);

    // 2.signed用于明确说明, 当前保存的数据可以是有符号的, 一般情况下很少使用
    signed int num4 = 9;
    signed int num5 = -9;
    signed int num6 = 0;
    printf("num4 = %i\n", num4);
    printf("num5 = %i\n", num5);
    printf("num6 = %i\n", num6);

    // signed也可以省略数据类型, 但是不推荐这样编写
    signed num7 = 9;
    printf("num7 = %i\n", num7);
   

    // 3.unsigned用于明确说明, 当前不能保存有符号的值, 只能保存0和正数
    // 应用场景: 保存银行存款,学生分数等不能是负数的情况
    unsigned int num8 = -9;
    unsigned int num9 = 0;
    unsigned int num10 = 9;
    // 注意: 不看怎么存只看怎么取
    printf("num8 = %u\n", num8);
    printf("num9 = %u\n", num9);
    printf("num10 = %u\n", num10);
    return 0;
}
  • 注意点:
  • 修饰符号的说明符可以和修饰长度的说明符混合使用
  • 相同类型的说明符不能混合使用
c
  signed short int num1 = 666;
  signed unsigned int num2 = 666; // 报错

最后,如果有任何疑问,请加微信 leader_fengy 拉你进学习交流群。

开源不易,码字不易,如果觉得有价值,欢迎分享支持。

基于 MIT 许可发布