朝花夕拾 · C语言 | 基础篇

       你是否曾与C语言初次相遇,许多概念一知半解。如今带你重走来时路,从头梳理语法、指针、内存、结构体等,陪你扫清底层盲区,为嵌入式开发筑牢根基。
       以此系列博客,记录复盘全过程。

一.计算机体系结构

        在了解C语言前,我们先看看早期计算机的结构:

        不可否认冯诺依曼体系结构为现代计算机的奠基石,它将程序指令像数据一样存储在计算机内存中,让计算机可以自动、连续地执行任务。这使得计算机从专用于特定计算的机器,变成了通用的、可编程的工具。明确了计算机应由运算器、控制器、存储器、输入设备和输出设备五大部件组成。这种清晰的分工奠定了现代计算机硬件的基础框架。

        但随着时代的发展,此类计算机结构已出现明显短板无法继续满足使用要求,最根本的缺陷:指令和数据共享同一条总线(信息通路)与内存交互,导致CPU同一时刻要么取指令、要么取数据,无法同时进行。随着CPU速度远快于内存,这种“抢路”现象拖慢了整个系统的速度。

        现代计算机应运而生,它以存储器为中心,一方面负责与CPU进行数据交互完成作业,一方面向I/O设备输入输出数据,显然,现代计算机CPU集成了运算器与控制器,相比传统更加高效。

二.进制转换

        我们先了解一下进制,所谓不同进制,其实本质即不同基数与位权的组合

1.任意进制转换为十进制

显然,在其他进制转换为十进制的时候最为简单,只需由高到低按照各位的权值进行加法运算即可

2.二进制与八,十六进制转换

        由于2与8,16进制的特殊性,在转换时有着异常的便捷性;对八进制而言,由0-7表示,最大数为7,这与3位二进制所能表示的最大值相同,故我们对二进制可拆分为三位一组。注意,在整数部分一定要由低到高表示,在高位不足三位时需补0;在小数部分则相反,由图上蓝色补足数易见;对于16进制则同理,由0-15表示(从十开始用A-F代替),与四位二进制数所能表示的最大值相同,故采用四位一组,与八进制转换完全同理;思路参考8421编码,也即BCD码。

3.十进制转换位任意进制

        上图所示为短除法的思路,也是多数老师会提及的,但本人并不推荐此方法,因为如果当数据较大时,采用短除法显然较为麻烦,故推荐使用“拼凑法”,如下所示

        这种思路在进行十进制向二进制的转换时较为简单,不过前提是熟知二进制各权重的数值,对数字有一定的敏感性,在初期可能比较茫然,但在经过一定的训练后可以较为快速的拼凑出结果。在得到二进制后,便可采用上述方法得到八与十六进制。

三.基本数据类型及其转换与运算

1.有符号数与无符号数

        显然,当表示无符号数时,整个机器字长全部二进制位表示数值位,没有符号位,所能表示最大数值易知,即全1,最小值为0;当表示有符号数时,最高位用于表示符号位,1负0正,表示范围参考下文。

2.原码与补码

        将原码转换为补码时数值位取反加1(正数原反补不变);也可从右往左数至第一个“1”,1左侧取反(符号位不变),右侧不变。

3.有符号数表示范围

        显然,有符号数原码表示范围关于0对称;计算机底层以补码存储二进制数,而补码相对原码存在特殊性,以八位二进制为例,0000 0000在补码中表示正负零,当符号位为0时,从0000 0001到0111 1111表示1-127,即所能表示最大值为符号位为0,其余全1;当符号位为1时 1000 0000则相比原码可额外表示一个数,则规定为最小负数-128,对short,int,long等同理即补码的表示范围中,可多表示一位负数,且为最小负数。(反码只是原码转补码中的一个中间状态,并无实际作用;移码通常仅用于整数表示范围,平时学习接触不多,本文不作过多阐述)。

4.int,long,short,char

在64位OS中,int占4字节,long占8字节,short占2字节,char占1字节,表示范围参考上文

	char ch='a';
	printf("int的大小:%d\n",sizeof(int));        //int的大小:4
	printf("long的大小:%ld\n",sizeof(long));     //long的大小:8
	printf("char的大小:%ld\n",sizeof(char));     //char的大小:1
	printf("short的大小:%ld\n",sizeof(short));   //short的大小:2
	printf("ch=%c,%d\n",ch,ch);                  //ch=a,97

man+ascii打开操作人员手册查看ASCII表,显然,char在计算机中存的都是对应表的二进制。

        unsigned short无符号短整型,由于仅有两字节空间,故表示范围较小,容易出现溢出,如下

    unsigned short n=65535;
	unsigned short result=n+1;
	printf("n=%d,n+1=%d",n,result);              //n=65535,n+1=0
 
	unsigned short n1=0;
	unsigned short result1=n1-1;
	printf("n1=%d,n1-1=%d",n1,result1);          //n1=0,n1-1=65535

    注意,若直接输出n+1或n1-1则会得到65536和-1,这是因为系统默认为int,在对short进行运算时会进行

强制转型为int,此时由于int表示范围足够大,故不会存在溢出

5.float与double

    float与double都由三部分构成:数符,阶码与尾数部分;在表示精度和范围方面存在如下差距:

        此外,a为float类型,占四字节,b为double类型,占八字节,对常量小数0.9默认为double类型,故也占八字节;注意到a与b输出均为0.9,这是因为系统默认保留六位小数,而float与double所能表示的精度都大于六位(float6-8位,double12-16位),故输出正确。实则由于0.9在计算机中无法精确表示,存储时必定会出现误差,而底层默认四舍五入,故得到0.9,后面的不准确的尾数被底层截掉只显示六位。

	float a=0.9;
	double b=0.9;
	printf("a=%.20f",a);
	printf("b=%.20f\n",b);
    //a=0.89999997615814208984 b=0.90000000000000002220

          当采用输出20位精度时则可见误差。

	float a=0.9;
	double b=0.9;
	printf("%f\n",a);                                                //0.900000
	printf("%f\n",b);                                                //0.900000
	printf("a=%ld,b=%ld,0.9=%ld\n",sizeof(a),sizeof(b),sizeof(0.9)); //a=4,b=8,0.9=8

同时,float赋值的0.9与double赋值的0.9并不相同,原因也即小数默认为double,在向float存储时出现精度丢失,故二者不相同。

	if(a==b)
	printf("yes\n");
	else
	printf("no\n");
                            //no
	if(a==0.9)
	printf("yes\n");
	else 
	printf("no\n");
                            //no
	if(b==0.9)
	printf("yes\n");
	else 
	printf("no\n");
                            //yes

        最后,float与double在绝大多数现代通用计算机上,之所以是4字节和8字节,是因为它们都遵循 IEEE 754 标准,这个标准明确规定了单精度(float)是32位(4字节),双精度(double)是64位(8字节)。

6.常量与变量

1.常量

顾名思义,即值不可改变的量,一旦定义了,整个程序运行期间都无法改变。

常量分两种:

  1)字面常量,在代码里直接敲的数字或字符,它们本身就是常量可以直接使用。

  整型常量:

    123   //默认十进制signed int
    123u  //unsigned int
    123l  //long
    123ul //unsigned long

    0123  //八进制
    0x123 //十六进制

字符常量:

    'a','b','?','!'等

    数字字符:'0'      对应ASCII表的48

    特殊字符:'\0'     对应ASCII表的0,即NULL

             '\a'     响铃

             '\b'     退格

             '\t'     横向制表符

             '\\'     表示\字符

             '%%'     表示%字符

            ' \" '    表示引号

             '\v'     纵向制表符,同'\f'

             '\r'     回车,光标回到本行开头

    此外,在C语言的转义序列(\)中,只能使用八进制(\ooo)和十六进制(\xhh)来表示ASCII码,且默

认使用八进制(如'\100'表示八进制100,对应十进制64,表示@),无法直接用十进制。如果用十进制,可以

直接写整数(如 char c = 64;),但这是整数常量,不属于转义字符。
     

浮点型常量

    3.14  默认为double类型,增加后缀f转型为float,3.14f(f,F都可)则表示float类型
    
    科学计数法:3.14e2表示314,3.14e-2表示0.0314,亦然(e,E都可)

字符串常量

"Hello world" 共12字符,11可见+末尾'\0'(字符串隐含,作为结束标志)

第二种:标识符(用来表示常量名,变量名,函数名及数组名等)常量,有利于增强代码可读性。

    #define 标识符 值            宏定义方式(标识符尽量大写,用于区分普通变量)

    注意宏定义只做代码替换,不做数据运算,如
     #define A 2+3          
      B=A*A;            

    此时即为B=2+3*2+3=11,而非25.
    故在定义宏时尽量加括号,括号优先级在运算符里最高,此时#define A (2+3)即为5

2.变量

        即程序运行过程中,其值可以发生变化的量。

        变量名的命名要遵循标识符的命名规则:所有标识符都必须以字母或下划线开头,不能以符号或数字开头,除开头外其他位置可由字母、下划线或数字组成;避免与关键字函数名等重复。

    两种命名规则:
    
    驼峰法:                linux内核命名:
    MaxVal                    max_val
    MinVal                    min_val
    TmpNum                    tmp_val

7.类型转换

1.强制类型转换
       1) 有无符号数之间转换

                保持位值不变,仅改变这些位的解释方式

        2)不同整数类型之间转换      
        3)整数类型强转总结

       4) 整数与浮点数之间类型转换

          需要注意的是int与float之间的转换,在int转换为float时存在精度丢失问题,由于int占32b,而float尾数仅23位(即使算上IEEE754标准的一位隐藏位也仅24位),不足以完整表示int型数据;而由float向int转换时,由于float表示范围远大于int,则可能出现溢出以及小数转为整数时的精度丢失。总结,int精度更高,float表示范围更大,故二者之间转换存在隐患。

2.隐式类型转换

        计算机在运算时会默认将低精度数据类型自动转换为高精度数据类型,如float->double; char,short->int->unsigned int ->long;其中float在运算时一定会转换为double,char与short在运算时一定会转换为int。

8.整数加减运算

        计算机底层对有符号数与无符号数的加减运算都采用同一套电路,由一个多路选择器MUX和一个加法器组成,以及控制器控制Sub发出信号协调工作。

        对于加法运算,控制器发出信号,将Sub位置0,加数Y从一端进入MUX不做任何操作,与加数X输出到加法器中相加得到结果;对于减法运算,以X-Y为例,Sub此时置为1,控制减数Y从带反向器的一端将Y全部位取反后经过MUX输出到加法器,同时Sub将1输出到加法器作为末位与X和Y取反之后的值相加,得到的结果即为X-Y的值。

        为什么该电路对于减法也可以实现呢?其实底层在运算减法时,会将减数取负,此时减法即变为加法,而Sub的功能即控制减数取反,同时通过Cin端口输入到加法器,在做减法时实现末位加1,实现对减数取负功能。

        而对于电路来说,有符号数与无符号数的工作原理完全相同,可以理解为运算器本身无法识别处理的二进制数据有没有符号,只是在计算完后人为的对最高位权值进行解释,有符号数则解释最高位为符号位,无符号数则解释为数值位。至此,整数加减运算的底层原理一览无余。

四.运算符

1.算数运算符  

         双目运算符:两个操作数

          + - * / % 

         %:操作数必须是整型数据或与整型兼容的(char),且结果与左操作数同号
         单目运算符:一个操作数

            ++   --

2.赋值运算符

               =  +=  -=  *=  /=  %=

        左边只能是变量,右边可以是变量,常量,表达式

        当运算符两边数据类型不一致时,参考第七节类型转换

        赋值运算符优先级只高于逗号运算符

3.三目运算符

        逻辑表达式?表达式1:表达式2

        若逻辑表达式的结果非零,则三目运算符结果为表达式1的值,否则为表达式2的值.

4.逗号运算符

        表达式1,表达式2,表达式3,......表达式n;其值取最后一个表达式n的值

        优先级最低

5.sizeof表达式

        获取数据类型,变量,常量,表达式在计算机中所占的内存

        返回unsigned long类型

6.关系运算符

        < ,>,<=,>=        高优先级

         ==,!=               低优先级

        关系表达式的值是一个逻辑值,即‘真’或‘假’。C语言规定,0为假,非0为真

7.逻辑运算符

        &&:逻辑与,全真为真,一假为假

        ||:逻辑或,一真为真,全假为假

        !:逻辑非,取反

 8.各运算符优先级

        当优先级相同时看结合方向,其中单目运算符,三目运算符和赋值运算符为自右向左方向,其余均为自左向右方向。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值