一、数据类型

1.类型相关占用

  • char:1字节(8bit)

  • short:2字节

  • int 和 long 类型取决于编译器CPU的规格,通常占据1字

  • long long : 8字节

2.补码

总共有三种方案可用来表示二进制负数

  1. 仿照十进制,用一个特殊的标志表示负数

  2. 取中间的数为0,如1000000表示0,比它小的是负数,比它大的是正数

  3. 利用补码

其中,第一种方法需要每次出现负数时对负数进行单独的处理从而转换成为特殊的二进制减法进行处理从而使处理更加复杂

第二种方法则是假设标定0的位置,但是这样的做法需要建设一套新的负数与8位二进制数的关系,同样使处理更加复杂

补码则是利用二进制数加减溢出舍去最高溢出位的原则,取得对应正数的负数从而参与计算,也就是把0从8位溢出为带1的9位再相减

0 -1 = -1

对应(1)00000000 - 00000001 = 11111111

这样最后求得的数据当做纯二进制看待时为255,当为补码看待时为-1,-a的补码就是0-a,实际上就是2n-a,n即为类型的位数

00000000->0

11111111~10000000->-1~-128

00000001~01111111->1~127

但是这里必须要注意的是,上述讨论补码是在整数int情况下的讨论,其他类型可能就不能等同视之,具体更取决于不同数据类型的具体占用,例如:

  • char: 1字节 -128~127

  • short:2字节:-32678-32767

  • int:取决于编译器,通常为一个字

  • long:4字节

  • long long:8字节

  • 但通常来说,总体规律是-2n-1~2n-1-1

3.unsigned关键字

因此,同一个二进制数据在纯二进制体系下是一套看法,在补码环境下又是另外一种含义,如果我们想要强制将不同的数据类型都看成二进制数据,避免使用补码形式,实际上就是在同一空间下将从而利于使用类似二进制移位的处理方式,就需要使用unsigned来声明变量,带来的效果也就是纯二进制效果,正数扩充,负数消失,但其本质目的还是为了二进制移位运算服务的。

4.整数的输入输出

整数类型通常只有int 或long long两种类型

  • %d:int

  • %u:unsigned

  • %ld:long long

  • %lu:unsigned long long

c语言中,以0开头的数字代表8进制,以0x开头的数字代表16进制

5.浮点类型

float的字长为32,double的字长为64

科学计数法:-5.67E+16表示负5.67乘以10的正16次方

使用printf输出时可以指定小数位

printf("%.3f\n",-0.0049)  //保留三位小数,输出结果为0.00
printf("%.30f\n",-0.0049) //保留三十位小数,输出结果为0.004899999999999999841793218991
printf("%.3f\n",-0.00049) //保留三位小数,输出结果为0.00

这里三十位小数的效果可以利用信号与系统里面的连续与离散关系理解,自然数的实数域是连续的,但是计算机是处理离散数字的,因此在实际运算中可能出现类似于采样的误差。

整数类型不能除以0(比如int类型),会直接报错。

但是浮点数是可以进行正小数或者负小数除以0.0从而得到正负无穷的结果,而0.0除以0.0可以得到nan结果。浮点数运算对应了无穷(+-inf)和非数(nan)类型的相关规则。

接下来是浮点类型的精度:

float a,b,c;
a=1.345f;//f或F是用来表明float的身份,带小数点的字面量是double而非float
b=1.123f;
c=a+b;
if(c==2.468) printf("相等");
else printf("不相等 c=%.10f,或%f\n",c,c);

最终的结果是不相等且输出c=2.467999352,或2.468000

显然前者是实际运算得到的数字,后者是float7位小数范围(double有15位小数范围)内进行四舍五入的结果,也就是2.467999四舍五入得到2.468的结果。

同时,我们在这里得到两个浮点数不一定能直接用f1==f2来判断是否相等,这里就需要利用相对误差来进行判断

fabs(f1-f2)<1e-12  
//这里f1-f2得到差值,fabs函数对其取绝对值然后再与10的负12次方比较,当误差小到一定范围时即可认为相等

考虑到上述问题,如果没有特殊需要我们还是尽量使用double,现代CPU能直接对double做硬件运算,性能不会比float差,在64位机器上数据存储速度也不比float慢。

6.字符类型

char是字符类型,同时可以视作证书,用%c输入输出,因此区分两种类型可以参考赋值时是否带有引号。同时应当注意同一个char变量输出为整数%d和输出为字符%c所蕴含的ASCII码值对应关系。

既然已经学习了整形和字符形变量的输入输出,那么就不得不提混合输入了

scanf("%d %c",&i,&c);//有空格则i读取至整数结束,然后跳过空格读c
scanf("%d%c",&i,&c);//在读取第一个整数之后继续读取空格

7.逃逸字符

\b

回退一格

\t

到下一个制表位,可用来优化格式

\n

换行

\r

回车

\"

双引号

\'

单引号

\\

反斜杠

8.自动类型转换

  • char->short->int->long->long long

  • int->float->double

  • 当运算符的两边出现不一致的类型时,会自动转换成较大的类型

  • 大意味着表达的数的范围越大

  • 对于printf,任何小于int的类型会被转换成int;float会转换成double

  • scanf不会,要输入short且需要%hd才能自动类型转换

9.强制类型转换

  • 要把一个量强制转换成另一个类型(通常为较小的类型),需要(类型)值,例如(int)10.2/(short)32

  • 因此要注意安全性,小的变量不能表示大的变量

printf("%d",(short)32768); 

这里32768刚好比32767也就是short类型最大能表示的数字大,这里发生了整形溢出,最后得到数字为-32768。如果不是short则是32768

int i=32768;
short s=(short)i;
printf("%d",i);

这里的输出结果i还是32768,说明强制类型转换只是从哪个变量计算出了一个新的类型的值,并不改变那个变量本身的类型或者数值

double a=1.0;
double b=2.0;
int c=(int)a/b;
int d=(int)(a/b);

上述代码体现了强制类型转换的优先级高于四则运算,如果此处我们想要获得的是将a除以b取整的结果,那么d正确,c会先将a取整然后再与b相除。

10.bool

c99加入,需要#include<stdbool.h>(PS:C++不用)

二、函数

1.函数定义

函数主要包含函数头,函数体,其中函数头包含返回类型、函数名、和参数表。

2.本地变量

  • 本地变量定义在块内,与全局变量相对应

  • 它可以是定义在函数块内,也可以定义在语句快内

  • 程序运行块之前,其中的变量不存在,离开这个块 ,其中变量消失

  • 块外定义变量在块内有效

  • 块内定义同名

三、指针

1.指针本身含义

指针变量的值是内存的地址,是具有实际值的变量的地址。

一定要记住指针定义出来后,假设还没有赋初值取地址,不能以为是一个整形指针变量就能随意赋值,本身指针变量的内容此时还没有明确,它就会指向一个未知地址,如果此时想要让该内存跟正常整形数据随意赋值一样的效果而直接赋值(例如*p=0),那么就会将一个不知道什么地址的内容改为0,这时会引起报错等问题(相当于迷失了)。因此,在定义好指针过后,在没有取地址指向任意已经存在的变量之前,不能通过*P访问任何变量以及数据。

2.指针与常量

指针与常量的关系一般包括指针变量本身与常量的关系以及指针内容与常量的关系

指针本身是const表示一旦得到了某个变量的地址,就不能再指向其他变量。

int i;
const int* p1=&i;
int const* p2=&i;
int *const p3=&i;

上述三种情况的结果就是const在*后面的指针不能被修改。

而const在*前面并不是表示数值不能改变,而是表示不能通过指针改变数值,直接赋值可以实现变量变化。

如果定义一个指针指向数组例如复制数组的情况时,指针赋值后变为某种特殊意义的常量。

指针总是可以把一个非const的值转换成为const,当要传递的参数的类型比地址大的时候,这是常用的手段:当能用比较少的字节数传递值给参数又能避免函数对变量的修改。

3.指针移位

int ai = {1,2};
int *pi=ai;
char ac = {1,2};
char *pc=ac;
printf("%d %d",pi,pi+1);
printf("%d %d",pc,pc+1);

由上述代码可以得到pi和pi+1之间的差值与pc与pc+1的差值并不相同,这说明了指针的++移位移动的是对应类型的单位占用(char占用1字节,int占用4字节),但是在数组层面还是大同小异的。

同时,*p++本身的值虽然是p移位前的值,但是实际上*的优先级高于++操作符。在某些cpu上可以直接被翻译成一条汇编指令(用的多嘛)。

因此要特别注意,指向不同类型的指针不能直接复制,假如用一个int类型的指针指向上述代码的char数组的开头,由于单位内存占用的4倍关系,这时用这个int类型的指针将第一个单位内存变成0则会使char数组的前四位都变成0,引起操作上的混乱。

4.0地址

内存当中通常存在着0地址,但是一般不能碰,因此指针一般不用0来赋值。

0地址用来表示特殊事件:例如返回无效指针以及表示没有真正初始化的指针

null即用来表示0地址,在某些编译器中0地址并不由0表示

5.指针类型转换

前面提到了一般不同类型的指针最好不要互相赋值,这里虽然说是指针类型转换,但大多数应用场景是void的使用将指针转换为void。

void*表示不知道指向什么东西的指针,计算时与char*相同但是不等价。

int *p=&i;
void *q=(void*)p;

这样就完成了一次类型转换,但是这里并没有改变p所指向的变量类型,只是用来忽略具体的变量类型从而更方便的访问具体内存地址,就当void看待完事了。

6.指针的应用场景

  • 需要传入较大数据时作为参数

  • 传入数组后对数组进行操作

  • 函数返回不止一个结果,需要用函数来修改不止一个变量

  • 动态申请内存