C语言强制转换的的一个小坑

例子:

1
2
3
4
5
6
7
8
CPP
#include <stdio.h>
int main()
{
float a = 1/3;
printf("%f",a);
return 0;
}

这个程序的输出结果是啥呢? 0.3? 0.33333?

都不对,这个程序的输出结果为0

虽然变量a为float类型 但是c语言对变量赋值的过程是先把表达式 1/3 的结果先计算出来 再放入变量a中
在计算表达式 1/3 时 计算机会认为这是在进行整数的运算 所以会将小数部分全部忽略 再将int的0赋值给float类型a
所以结果并不为0.33333

总结: 表达式的结果类型,取表达式中所有元素的最高级别(精度最高、范围最大)的数据类型

进制换算的小方法

说到进制换算 要先来说说一个问题

把1000个苹果分到10个箱子里。分好后随便我要几个苹果,你都可以整箱整箱的搬给我

这道题的解法是

第一个箱子 1 个苹果
第二个箱子 2 个苹果
第三个箱子 4 个苹果
第四个箱子 8 个苹果
第五个箱子 16 个苹果
第六个箱子 32 个苹果
第七个箱子 64 个苹果
第八个箱子 128 个苹果
第九个箱子 256 个苹果
第十个箱子 489 个苹果

这样如果我要60个苹果 那就可以把 六五四三 这四个箱子给我
很多人在这已经看出来了 这就是2的N次方的关系
为了方便理解 我在Excel做了一个表格
img
本来在第十箱的位置是489 但是为了方便这里就按照2的9次方写512
刚刚说到 假如我们要60个苹果 需要给我 六五四三 这四个箱子
那么我们就在六、五、四、三这四个箱子的下面标1,其余的补0
img
111100就是是啥呢? 可以用计算器查看一下
img
可以看到111100的十进制就是60

那么如何将2进制快速转换成10进制呢?
首先我们需要考虑一个问题 我们为什么在看到一个数 比如123就知道他是一百二十三呢?
因为我们小时候是这样学的:
在看到123的时候 我们的大脑就自动把他分成

1 2 3
百位 十位 个位

百位上有一个1 那就等于1X100
十位上有一个2 那就等于2X10
个位上有一个3 那就等于3X1

如果用次方来表示的话就是
123 = 1X102+2X101+3X100
理解了上面的之后 二进制就更简单了
比如101101转换成10进制
101101 = 1X25+0X24+1X23+1X22+0X21+1X20 = 32+0+8+4+0+1 = 45
同样使用计算器验证一下
img

10进制转2进制

除2取余
如58转2进制
img
最后的7的二进制为111**(不记得的话也可以继续除下去)** 将余数从下往上写010
得到结果111010

补码

补码规定,最高有效位是符号位,用于表达数值的符号,正数为0,负数为1
负数其他位求补后存放

补码有啥用?

因为计算机只能进行加法运算,那么要进行减法的时候应该如何运算呢?答案就是使用补码。
以下位Byte减法运算例子
计算A-B
首先b求补集
B + ~B = 0xff
B + ~B +1 = 0x100
~B +1 = 0x100 - B
0x100-B得到B的补集
相当于
A+(0X100-B) - 0x100
A + ~B + 1- 0x100

A-B = A + ~B+1 进位丢失
A-B等价于A+neg(B),其进位丢失
neg(B)就是对B求补
A>0 neg(A) <0
A<0 neg(A) >0
A=0 neg(A) =0

十进制减法例子

75-57
75+(100-57)进位丢失

75+43=(1)18
75-57=18

单字节补码

正数

用上面58为例子 58的二进制为111010
首先添加一个符号位,58为正数,加0变成
0111010
因为是单字节补码,一个字节为8位,0111010只有7位,还要再多补一个0
得到00111010
0011 1010
3 A

得到58的补码为0x3A

负数

负数取反加1
用-58为例子,58的补码为0011 1010
0011 1010取反得1100 0101
1100 0101加1
结果
1100 0110
C 6

得到58的补码为0xC6

单字节补码求10进制真值

0x58
01011000
+1011000
64+16+8 = 88

0x85 (16进制表达中最高有效位大于等于8的,都是负数)
10000101
-1111011
1+2+8+16+32+64=123
-123

0x80
10000000
-1111111+1=-0 or -128

程序真的是从main函数开始的吗?

在一些编辑器生成C语言框架的时候,往往可以看到在main函数后面跟着两个参数。
img

1
(int argc, char const *argv[])

其实main的参数有三个,完整的应该是这样:

1
(int argc, char const *argv[],char *envp[])

如果main是一个程序的开始,那么谁来给main传参呢?
你如果安装了完整的VS系列,那么你就可以找到对应的源码,路径如下:
C:\Program Files\Microsoft Visual Studio\VC98\CRT\SRC\CRT0.C
打开CRT0.C可以看到mainCRTStartup()这个函数
img
CRT为C Run Time的缩写
那么我们大致来看一下这个函数做了什么
首先通过位运算获取了当前系统的版本,再通过位运算获取当前操作系统的主版本号,次版本号。
img
初始化堆环境,以及判断程序是否为多线程,如果是,则初始化多线程。
img
初始化IO(stdin , stdout , stderr)
img
紧接着的是一个条件编译,进行取得命令行和环境变量和将空格分开的命令行语句于环境变量格式化为字符型指针数组。
img
初始化处理器中的浮点寄存器,官方的全局对象,用户(程序员)自定义的全局对象。
img
获取当前程序的启动状态,信息,比如是否为窗口,窗口多大,字体多大等等。
img
条件编译,根据是否为Unicode(宽字节字符集)版,是否为窗口,进行选择,共有四个版本,选其中一个。
img
可以看到此时到达main函数,并将三个参数传入main函数内,接下来流程才转到main函数。

1
main(__argc, __argv, _environ);

指针1

1.啥是指针?

指针 就是一个具有解释方式的地址

2.指针的声明

解释方式 身份标识 用户标识符

简单来说就是 类型 * 名字

如:

img

*号放左中右都没有关系,看个人喜好或是需要遵从的代码规范。

注意!声明指针时必须初始化或赋初值,否则会出现野指针!

另一种情况:

img
这行代码的意思是 声明一个int类型的指针和一个int类型的变量

如果写成这样:

img

可能就有初学者会认为这是声明两个指针,其实并不是。

3.指针的四种运算方式

3.1 间接运算

使用间接运算符(*)

img

上面四行代码的意思:

1.定义了一个int类型的变量n并赋值为6

2.定义一个int类型的指针p并赋值为空(NULL)

3.将变量n的地址取出并赋给指针p

4.使用指针p定义的类型去间接访问指针p内保存的地址(变量n)的内容,得到变量n的内容并赋值给printf进行输出。

第四句话有点绕,没事,看对应的汇编代码就一目了然

img

3.2 下标运算

img

可以看到,在汇编上这两行代码的是完全等价的。

img

指针访问数组元素

img

3.3 加法运算

对指针做加法运算,必须是整数类型,得到的是同类型的指针常量

img

img

img
可以看到,再执行完毕**printf(“%x\n”, (p[n])++);**这行语句后55被改成了56

3.4 减法运算

同类型指针才能相减,得到整形常量

img

img

就两个地址相减再除指针类型的大小 没啥好说的

4.const修饰指针

下面一幅图这是正常修改ary内容的情况

img

如果在*左边加上const,将会报错”不能给常量赋值”

下面是const写在*左边的两种情况

img

img

cunst在*****左边,间接访问的目标是常量,所以会报错”不能给常量赋值”

如果const在*****右边,则可以正常编译通过。

img

cunst在*\左边,指针自己是常量,不影响对ary**的赋值。

5.指针++的一些坑

5.1 *p++

这种情况++与指针p先结合,得到同类型指针,由于后置++,表达式完成后才加1

demo:

img

可以看到在执行*p++\之前指针指向的地址是ary数组的首地址0x006ffae8

接下来执行语句

img
可以看到,正常输出了ary[0]的内容,在输出执行完毕后,0x006ffae8变成了0x006ffaec

你们或许在这会有疑问,为啥0x006ffae8进行++之后会变成0x006ffaec而不是0x006ffae9呢?

因为这里进行的是int类型指针的+1,int是4字节,所以是加4。

还可以这样写,没有区别,只是让代码的可读性更高了一些,原本的运算符优先级就是这样的。

img

5.2 (*p)++

*与指针p先结合,得到整形,再对整形自加1

执行前:

img

执行后:

img

这个需要注意的是,*(\p)++*执行之后的结果是常量,不能进行赋值,而上面的**p++**运算结果是整形,可以进行赋值。

6.字符串指针

在VC6.0中 字符串指针可以这样定义

img

其实指针字符串是放在常量区的,和定义全局变量一样。

这个问题在高版本IDE中已经有提示

img

必须要加const声明字符串指针

img

指针2

1.指针函数

指针函数就是返回值为指针的函数

注意:指针函数在使用时不应该返回参数和局部变量的地址,当函数调用完成后参数和函数局部变量的地址会被释放,这时返回的指针会引用一个栈外的地址,这个地址会被新函数占用并覆盖。

返回参数地址demo:

img

这是一个比较两个数大小的简单指针函数*GetMax,*GetMax函数返回值返回的是参数中数值最大的参数的地址。

这样写编译器只会视为警告,并不会视为错误,是可以正常编译甚至运行的。

img

这段代码直接运行是可以正常输出较大值9的。

img

但是如果在输出之前让别的函数对栈空间进行了使用之后,原本的残留值被覆盖,输出值就会错误,这里我多加一句printf在上面。就可以看到错误的输出。

img

返回局部变量地址demo:

img

同样也是输出错误。

2.函数指针(回调函数)

2.1 定义语法

返回值 (调用约定*函数指针名)(各形参的类型序列)

什么?看不懂?说人话?

看图!

img

这样就定义了一个名为pfnFun的函数指针,并指向fun函数的首地址。

为了代码美观,还可以使用typedef来定义。

img

微软在定义函数指针时就是使用了typedef

img

2.2 使用

函数指针使用时必须同参数个数、同参数类型顺序、同返回值,同调用约定。

符合上面要求的 才是同类型函数指针。

使用时直接函数指针名(参数)就行

img

2.3 有什么用?设计思路?

函数指针可以在程序运行时修改程序行为,函数指针的赋值可以从文件、注册表、网络数据包等方式赋值。

指针函数常用于接口设计,如程序更新(热更新、热补丁),木马免杀。通过数据包发送的内容是通常不落地的,只存在于内存之中,进程结束就消失。

3.指针数组(变长字符串数组)

指针数组是特殊的数组,指针数组里面的每个元素都是指针类型。

3.1 定义方式

img

3.2 啥是变长字符数组?和定长字符串数组又有什么区别?

首先来看看定长数组的内存结构

这里我定义了一个每个元素大小为32字节定长数组,进入调试模式,查看内存结构。

可以看到,无论字符串有多少个字节,都会分配32个字节给每个元素。

img

定长数组的优势是访问效率高。

而变长数组则是一个紧挨着一个存放

img

这样的优点就是可以节省空间,但是访问效率会变低。

实际上变长数组为了解决访问效率问题,使用了字符指针数组的方式,每个元素的首地址都放到一个数组里,指针的长度是固定的,在访问第n个元素的时候,依然可以用数组的寻址方式。(类似于switch的访问方式)

3.3指针数组的下标运算与指针运算

指针数组的下标访问可以总结为以下三句话,与对应的情况套用进去就行。

[数组名] 是 [数组第0个元素] 类型的指针常量

[二维数组] 的元素是 [一维数组]

对 [某类型指针] 做 * 运算,得到 [某类型\]** 的标识符引用**

首先先定义一个二维数组ary,本小节的后续部分将会使用此数组作为例子。

img

3.3.1 直接访问ary

输出的是第0个一维数组的首地址,也是ary的首地址。

img

3.3.2 ary[0]

img

其实ary[0]\的输出结果和上面直接访问ary输出的一样,只不过我这开启了ASLR(地址随机化)所以输出不一样。

这里看似输出一样,但是其实访问的地方和类型并不一样,这个在IDE调试器的监视窗口可以看出来。

img

3.3.3 ary[0][0]

对应:

对 [某类型指针] 做 * 运算,得到 [某类型\]** 的标识符引用**

img

3.3.4 ary+1

这个和我在上一篇文章中提到的指针加法原理一样,套进去就行。

img

在这里ary的地址为

img

img

3.3.5 *ary+1

这在这里ary的地址为

img

img

3.3.6 **ary+1

img

4.数组指针

4.1 数组指针的定义

一维数组的数组指针定义:

img

二维数组的数组指针定义:

img
注意!对数组取地址得到的是数组的地址,而不是数组元素的地址。指针p将会把目标解释为数组,而不是把目标解释为整形指针。

如果你看懂了上面的内容,那么你一定对指针数组的基本原理有了一定的了解,就让我们来看一下下面这些简单的例子,把我们刚刚学到的内容用到实践中去吧!(狗头保命)

img

4.2 试试看!数组指针的各种运算!

以下例子将使用数组指针p和二维数组ary作为例子。

img

4.2.1 直接访问数组指针p

得到int[4]\l类型数组指针地址0x0019febc

img

img

4.2.2 *p

得到int[4]\数组地址0x0019febc

img

img

4.2.3 **p

0x0019febc取内容得到整数10

img

4.2.4 p+1

img

4.2.5 *p+1

img

4.2.6 *(p+1)

(p+1)\得到int[4]**类型的指针常量0x0019fecc**

(p+1)*取到的是int**类型的指针常量

img

4.2.7 *(*p+1)

img

4.2.8 *(p+1)+1

img

4.2.9 *(*p+1)+1

img

4.2.10 (p[1]+1)[1]

img

5.二级指针