C语言学习笔记
内存管理
隐式类型转换几大原则
- 当出现在表达式中,有无符号的char和short会被自动转化成int参与运算。当short与int一样的时候(字节数一样,16位系统),unsigned short会被转化成unsigned int。这一条了解即可
在K&R标准(C语言最初标准)中,float会被转化成double。 - 包含两种数据类型的任何运算中,两个值都会被转化成两种类型中较高的类型,然后参与计算。
2+3.4
中2会被转化成double在进行运算,同类型的数据才能直接运算。
类型级别高到低的顺序long double,double,float,unsigned long long,long long,unsigned long,long,unsigned int,int
,当long和int有相同大小时(32,64位操作系统都是),unsigned int > long;这排名没有涉及到char、short,因为已经被提升到int或者unsigned int。
混合运算转换过程:例如3+4/5.0f+6-9.0
先运算4/5.0f,所以4转换成4.0f,然后3+0.8f-9.0,先算3+0.8f,3转换成3.0f,然后3.8f+6,6转换成6.0f,最后9.8f-9.0,9.8f转换成9.8,最后答案是0.8(double型)。注意点是不是所有数据都先转换成double再运算。 - 再赋值语句中,会被转换成赋值运算符左侧的类型,可能升级,也可能降级。
int a = 12.23;
这是double型转成int型,降级(数据丢失)。double d = 12;
升级 - 作为函数参数时,char和short会被自动转换为int,float会被转成double,可通过函数的声明防止提升,这一点我们通常是不用考虑到。
显式类型转换
- 也就是我们常说的强制类型转换。
- 写法:
(type)数据
- 强转基本数据类型:12+12.2 这是隐式转换,都转成了double,12+(int)12.2 会先int转换,在进行加法运算。
- 注意越界:
c = (unsigned char)1234.2
,通过调试,我们可以看到c的值时210(因为unsigned char的范围是0~255)为得到该数据,首先要去掉小数位,对于数据1234,有两种方法得到210这个数据:1、数据截断:转化成二进制0100 1101 0010
,只取后8位,也就是210了;2、1234%256 = 210。 - 强转指针(地址类型),这也是最常见的强转。
int a = 12;
double *p = (double*)&a;
*p = 21.3;
- 这样的操作是非法的。int类型数据只占了4字节,但是double占了8字节,这样越界操作是非法的(illegal)。
double a = 12.3;
int *p = (int*)a;
*p = 12;
- 这样是符合语法的,有点像联合(union),修改了前四字节,读取了前八字节。
int a = 12;
float *p = (float*)&a;
*p = 23.3;
- 这样的写法也是完全正确的,这是因为float和int都占了4字节的空间。
double d = 12;
int *p = (int*)&d;
*p = 23; //操作前四个字节
*(p+1) = 34; //操作后四个字节
*(int*)((short*)p + 1) = 12; //操作中间四个字节
- 指针/地址的类型,决定了至臻的读写方式(也就是字节数)。
大小端存储
- 作用:及算你就存储数据的方式(还有内存对齐)
- 小端存储(以134480385为例),其存储图示如下:
- 一般个人计算机基本都是小端存储
- 大端存储刚好与其相反:
- ★测试大小端存储两种方式:1、强转:运用前面学到的知识,将其每个字节依次拿出来,然后依次打印(char*),该数的每个字节分别是
1 2 4 8
可知小端存储。
2、使用联合:原理和强转比较相似,但是他更方便。
union Un
{
int a;
char c[4];
}u = {134480385};
printf("%d,%d,%d,%d"u.c[0],u.c[1],u.c[2],u.c[3]);
- 这样也能得到1,2,4,8的结果,但是更好理解。
typedef详解
- 意义:数据类型重命名,也就是别名(类似于枚举enum),使用的方式一模一样。
typedef int myint;``typedef myint yourint;
这几个xxint都是一模一样的,不管是用法,意义等。 - 指针重命名:
typedef int * pint;
这样写法不会出任何问题,也就是说,最后一个没用空格分开的关键词是新的关键词。 - 结构体重命名:
typedef struct Node
{
int a;
}_Node;
- 这里的_Node就是新的名字,也就是说在原来可以定义变量的地方现在无法进行定义了,这样的方式适用于无名结构体“起名字”
- 函数指针重命名:
void fun(int a, double b)
{}
void (*p)(int,double) = fun; //这是正常的函数指针定义方法
typedef void (*pFun)(int,double); //这样是用重命名法重新命名函数指针
pFun p = fun; //这是该种方法的变量的定义方式
宏define
- 作用:就像enum给整数命名,typedef给数据类型命名,这个宏可以给一切“重命名”,本质还是替换。
- 宏的实质是预处理指令
- 正确写法:
#define ONE 1
这里的ONE是宏名,1是本体。(宏名一般大写),记得分号一般是不加的。 - 意义:在进行编译时,宏会被本体替换掉,这是一种傻瓜式替换,如果是一个表达式,那替换前不会进行运算,替换后才开始,所以要注意括号的位置。
#define PRINT printf ("%d,%d\n"),ONE,1);
,这时在任何地方输入PRINT
,都会打印1,1。一定要注意分号的有无#define THREE
后面不加任何东西的宏叫做空宏,可以定义也没有问题,但是意义不大。 - 表达式宏的注意点:
#define ONE 1+1
printf("%d\n",2*ONE);
答案是3。 - 参数宏:
#define PRINTF(x) printf("%d\n",x);
PRINTF(12)
输出结果是:
12
- 这说明参数宏类似于函数,可以传递参数(用x,y这类的数表示就可),多个参数用逗号隔开。
宏的几个指示符
\
:拼接,意思是本行的宏的本体还没写完,下行也属于宏的本体。要求放在本行的最后,并且后面无空格。#
:字符串指示符,例如#define NUM(x) #x
将x解释成字符串,也就是自动带上了双引号,这时候输入x的时候不需要双引号了。##
:字符串拼接符,
#define NUM(x,y) #x ## #y
printf("%s",NUM(a,qwe));
- 这样的输出结果是aqwe,且#x,#y之间的所有空格都可以不加。