原码、反码、补码
4. 解释该函数的输出结果:
void func(void)
{
unsigned int a = 6 ;
int b = -20 ;
(a+b>6) ? puts(“>6”) : puts(“<6”);
}
这道题主要考察计算机中数据是怎么存储的,以及加减法在计算机内是怎么实现的?
运行结果是 >6,思考为什么会这样子?
那我们再看如果输出a+b的结果又会是多少?
我们发现以%d输出它的值是-14,这也与我们的认知相符合,那那个42亿多又怎么解释?
正解
计算机器内部是以补码形式存在的(谨以8位机器为例)
那么a原码0000 0110 反码0000 0110 补码0000 0110
b原码1001 0100 反码1110 1011 补码1110 1100
计算机计算加减法是直接以补码进行操作的,那么a+b补码就是1111 0010
以%d输出就是以有符号十进制输出所以补码需要转换回原码(1000 1110),此即-14,所以以有符号输出就是-14。
那么以%u输出就是a+b补码直接转换十进制,也不存在符号位了,32位机就是一个很大的数了。
sizeof()和strlen()异同
3. 解释程序的运行结果:
void func(char a)
{
printf("%lu\n", sizeof(a));
printf("%lu\n", strlen(a));
}
int main(int argc, char argv[])
{
char a[] = “hello world”;
func(a);
printf("%lu\n", sizeof(a));
printf("%lu\n", strlen(a));
return 0;
}
/ * 运行结果是:
8
11
12
11
*/
a是一个字符数组,它的存储形式是这样的,
再看sizeof()和strlen():
sizeof运算符,在编译时即计算好了 ,参数可以是数组,指针,类型,对象函数。是关键字,在头文件中typedef 为 unsigned int。功能是:获得保证能容纳实现所建立的最大对象的字节大小。
strlen 是函数,要在运行时才计算,参数必须是字符型指针。当数组把它作为参数传入时,实际上数组 就化成指针 了。功能是返回字符串的长度。该字符串可能是自己定义的,也可能是内存中随机的。该函数实际完成的功能是从代表该字符串的第一个地址开始遍历,知道遇到结束符NULL,返回值不包括NULL .
那么运行结果也不难解释,首先是func()函数中sizeof(a)的大小是8,a是指针在Linux系统下指针大小就是8。
在看strlen(a)的大小是111,到\0即停止,故为11。
main()sizeof(a)的大小是12,a是数组名,a在内存中实际占的大小就是12,包括结尾\0占据一个字节。
由此,我们再来通俗解释一下sizeof()和strlen()区别,sizeof()计算的是占据的世纪空间的大小,不管其中是否写入数据,开了多大的空间,sizeof()就返回多大的指。而strlen()则计算的是字符串的实际长度,不管它开了多大空间,strlen()只统计\0之前的长度。
内存对齐和大小端
7. 下列结构体在内存中所占空间大小分别是多少? 。
struct node
{
int x;
char y;
double z;
};
struct node
{
int x ;
double y ;
char z ;
};
/大小分别是16B和24B/
结构体中数据成员对齐规则:
1. 结构体(struct)或联合(union) 的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员的大小或该成员的子成员大小(只要该成员有子成员)的整数倍开始 .
2.结构体作为成员:如果一个结构体里有某些结构体成员,则结构体成员要从内部最大元素大小的整数倍地址开始存储。
3.收尾工作:结构体的总大小,也就是sizeof的结果,必须是内部最大成员的整数倍,不足要补齐。
那么这道题可以画如下示意图解释:
问题解决了!
再看大小端!!!
何谓之大小端???
对于跨越多字节的程序对象,我们必须建立两个规则:这个对象的地址是什么,以及在内存中如何排列这些字节。在几乎所有的地址上,多字节对象都被存储为连续的字节序列,对象的地址为使用字节中最小的地址。
某些机器选择在内存中按照从最低有效字节的顺序到最高有效字节的顺序存储对象,而另一种机器则按照从最高有效字节到最低有效字节的顺序存储。前一种规则——最低有效字节在最前面的方式,称为 小端法 。后一种规则——最高有效字节在最前面的方式,称为 大端法 。
小端法 即高地址对高字节。大端法 即低地址对高字节。
比如整形十进制数字:305419896 ,转化为十六进制表示 : 0x12345678 。其中按着十六进制的话,每两位占8个字节。
采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。
怎么证明自己的电脑是大端小端?
我们直接利用一个强制类型转换来证明。
#include <stdio.h>
int a = 16909320;
int main()
{
char *p = (char *)&a;
printf("%d\n",*p);
printf("%d\n",*(p+1));
printf("%d\n",*(p+2));
printf("%d\n",*(p+3));
return 0;
}
为什么a会是这么奇怪的一个数字,因为这个数字对应的32位二进制码就是:
高字节 | 低字节 | ||
---|---|---|---|
0000 0001 | 0000 0010 | 0000 0100 | 0000 1000 |
那么如果是大端在内存中会这么存储(高地址低字节)
低地址 | 高地址 | ||
---|---|---|---|
0000 0001 | 0000 0010 | 0000 0100 | 0000 1000 |
最后读出来数据就是1,2,4,8。
如果是小端存储的话就会是这样(高地址高字节):
低地址 | 高地址 | ||
---|---|---|---|
0000 1000 | 0000 0100 | 0000 0010 | 0000 0001 |
运行结果应是8,4,2,1。
最后看运行结果:
/*
0x558c02ba7010
0x558c02ba7011
0x558c02ba7012
0x558c02ba7013
8
4
2
1
*/
所以我的电脑是小端。
嵌套宏
#是把宏参数变为一个字符串 ##是把两个宏参数连接在一起
如何展开宏参数的规则:在展开宏参数时,如果形参有**#或##不进行宏参数的展开,否则先展开宏参数,再展开当前宏。
简言之,就是要去区分清楚当前宏,和宏参数,如果当前宏有#,那么后面的宏参数当作字符串处理,如果当前宏中不含#,那么宏参数就要一路展开了,看一道例题:
*8. 解释程序的运行结果:
#define f(a,b) a##b
#define g(a) #a
#define h(a) g(a)
int main(int argc, char argv[])
{
printf( “%s\n”, h(f(1,2)) );
printf( “%s\n”, g(f(1,2)) );
return 0;
}
先看第一个输出,当前宏是h(),观察发现h()中是不含有#的,所以宏参数要一路展开下去,展开f()的时候,a和b被连接成一个字符串,所以就有了字符串12。
对于第二个输出,当前宏是g(),观察发现g()中是含#的,所以宏参数被处理成字符串,不再继续展开。
综上所述,最后输出结果就是:
/*
12
f(1,2)
*/
常量指针和指针常量
***5. 根据所给代码,说明 const 关键字的用法,指出标号为 (1)~(4) 的代码哪些是错误的。
char y[ ] = “XiyouLinuxGroup”, x[ ] = “2018”;
char *const p1 = y;
const char p2 = y;
/ (1) / p1 = x;
/ (2) / p2 = x
/ (3) / p1 = ‘x’;
/ (4) / p2 = ‘x’;
首先看三种写法:
const char *p;
char const *p;
char *const p;
前两种写法是等价的,它们叫做指针常量,特点是不能修改指向变量的值。
最后一种叫做常量指针,它的特点是不能修改指向的变量。
那怎么快速记忆这三种写法呢?可以这样记忆,const用于修饰离它最近的东西 。
所以上面那道题也了,14是错误的,23是正确的。
static静态
(1)静态局部变量:程序运行过程中不释放内存。
(2)静态全局变量和函数:限定只能在本文件中使用,别的文件无法使用;如果不限定,大型文件合并过程中可能出现名称冲突。所以全局变量前加static是一个良好的编程习惯,全局变量不加static默认是外部的。
/*
运行结果
2018 1
2018 2
2018 3
*/
二级指针
问题有三:
一func函数中strcpy(a->name, “XiyouLinuxGroup”);是错误的,因为a的成员name是一个指针,而name指针我们没有为其开辟空间,所以修正办法也有两种,一种是三手动malloc为其分配空间,或者把name改成数组。
二是向func(a)函数传递参数的时候,应该传过去的是地址,所以此处应该改为二级指针。
三是malloc之后没有释放内存。