前言
前阵子刚好读完了一本书——《从缺陷中学习C/C++》,里面分篇章讲解了在编程中会遇到的一些易错的陷阱问题。正好最近想对C、C++中一些知识点进行内容的总结,所以我也打算采用分篇章的方式,每个篇章对应着去记录一些笔记,其中包括一些基础知识以及延伸,也可能会配合着罗列一些常见的易错误区。那么话不多说,我们直接开始本篇内容。
本篇内容
本篇中,将对运算符和各数据类型,这两方面的内容进行知识的梳理。
运算符
1. 移位运算符 >> <<
(1) C语言中的移位运算通常指的是算术移位,其规则为:在对应的补码下,符号位不变,其余位移动对应的位数。所以很容易知道,左移和右移可以实现乘2和除2以及对应的次方。
(2) 需要注意的地方:
1° 左移可以实现乘2,但是右移实现的是除2的向下取整。
2° 对于-1,对它进行右移时,会发现无论如何结果都会是 -1。当你按照规则来进行推演时,就会很容易发现原因,或者基于1°来对这个结果进行理解:-1 除2向下取整 = -1。
2. 位运算符 ^ & |
(1) 位运算符中,我们常常会利用它们的一些特性。如:对应位 & 1 = 对应位, 对应位 | 0 = 对应位, 自身 ^ 自身 = 0, 自身 ^ 0 = 自身 等等。
(2) ^异或,对应位相同结果为0,对应位不同结果为1。 异或在一些 “找不同” 的情况下会有妙用。
如:已知一个数组中,除了一个单独的元素,其余元素都会出现两次,找出这个单独元素。
int arr [] = {1,2,3,4,3,2,1}; //找出单独的元素4
int len = sizeof(arr)/sizeof(arr[0]);
for(int i = 1; i<len ;i++)
{
arr[0] ^= arr[i]; //让其中所有的元素相 异或
}
cout << arr[0] << endl; //最后得到的结果就是那个单独的元素
3. 取相反数运算符 -
(1) 对于取相反数,它的内部实现,可以看作三个步骤。首先是 对应补码按位取反~,然后 结果+1,最后得到的仍然是补码,需要将它化回原码。当你把整个过程列出来时,可能会有更好的理解。 实现取相反数的目的是:改变原码的第一位,即符号位取反。
(2) 这里我们进行一下分析,为了避免混乱,只讨论正数,以0001为例。
首先是对应补码取反,而正数补码是自己,按位取反后,变成1 110;对于+1这一步理解不了可以先放下;如果我们不加1,直接进行最后一步的求原码,即除符号位外取反,然后+1。1 110 =>1 010,你会发现,比我们想要的结果1001,大了1。
这个超出来的1,就是最后一步求原码中加的1。所以我们在按位取反后1 110,要进行+1的操作,让它变成1 111,这样在最后就能得到正确的答案。对于负数补码,我们进行+1,其实会让它对应的原码-1。
其实也可以理解成 符号位 + 其余位。按位取反,会让 符号位 + 其余位 都取反。而取余操作,只会让 其余位 取反。所以一次按位取反 + 一次求补码,就会达到只取反符号位的目的。剩下的就是通过额外的+1,来消除补码带来的+1。
(3) 需要注意的是,当我们对最小负整数取相反数,得到的仍然是它自身。首先,最小负整数是没有对应的原码的,或者说它的补码和原码相同。以4位为例,我们进行一次取相反数的过程推导:
1000(补码/原码) => 0111(取反) => 1000(+1)
然而在《陷阱》这本书中,关于这一点给出的代码是这样的:
int minInt = 0xffffffff;
if(minInt < 0)
{
minInt = -minInt;
}
printf("%d",minInt); //结果输出为1
于是,它得出结论:最小负整数取相反数,会溢出为1。但上面所定义出来的minInt,不是最小负整数,而是-1。取-1 的相反数,得到的结果肯定就是1。所以,书中的这个结论应该是有问题的。
4. 自增运算符a++ / ++a
(1) 对于自增/自减运算符,最基础的就是要明白前置++和后置++的区别,即:
a++:先使用a,后自增1; ++a:先自增1,后使用a。
(2) 在这一块内容中,我们需要注意的就是:避免写出带歧义的代码语句。比如:
int a,b,c;
void func(int d,int e);
a = b = c = 0;
b = a++ + b; // 1 err (a + ++b) ?
func(c,++c); // 2 err
对于第1条语句的写法,所加的空格,只是便于你自己分辨,但是编译器并不认识。你无法知道这句代码执行时,具体的顺序是如何的。而且这种写法,很容易因为运算符不同优先级的原因,导致输出的结果不是你所设想的。
关于这一点,在课上学习的时候,你可能接触过 "从左开始结合" 的原则。即从左到右,依次拿出一个字符,如果能组成新的运算,那么就把它们看作一部分。以a+++b为例:
a => a + (加法) => a++ (自增) => a+++ (不存在) => (a++) + => (a++) + b
但是即使如此,我们依然不推荐这种写法,而要善于利用( )来使计算式的逻辑更清晰。
对于第2条语句,也是差不多的原因。函数调用时,我们无法知道它的参数是从左到右,还是从右到左传入的。所以语句2就会出现:func(0,0) or func(1,0) 的歧义。
如果想要更加深入的了解,可以带着 "副作用" 和 "序列点" 这两个关键词去搜索康康。
5. 赋值运算符 =
(1) 首先需要注意的就是,区别 = 和 == 。
(2) 在进行右值为表达式的赋值时,并不是直接将结果给到对应的目标变量,而是要先经过一个临时变量的存储(我也不晓得为啥)。虽然一般情况下,我们使用赋值运算符时也不需要顾虑这一点。
//但在下面这个例子里,就有可能导致溢出 -- 《陷阱》
int m,n;
long long result; //这里考虑long是4字节
result = m*n;
正常情况下,int * int 的结果由long long来接收是不会有溢出问题的。但是当m、n都在int范围内取到一个较大的值时,赋值前m * n的结果会被先存储在一个临时的int变量中,然后再赋值给result。因为此时的m、n都比较大,所以是可能会出现溢出的情况的。
(3) 当右值为函数的调用,且函数带(非引用)返回值时,返回值也会先被存储在一个临时变量中。
各数据类型
1. 字符型 char
符号类型 | 字节数 | 表示范围 |
char | 4 | -128 ~ 127 |
unsigned char | 4 | 0 ~ 255 |
(1) 取值循环
即当得到的数据超过表示范围时,又会回到起始位置重新开始新的一轮。例如:
char a =128 和 char b = -128 所定义出来的值其实是一样的
(2) 整型提升
1° 整型提升是C程序设计语言中的一项规定:在表达式计算时,各种整型首先要提升为int类型,如果int类型不足以表示则要提升为unsigned int类型;然后执行表达式的运算。 -- 摘自百度百科
(所谓的各整型,主要说的就是比int小的:char 和 short)
2° 整型提升的规则: (以char为例)
无符号数:char自身作为低八位,不足的高位用0补齐。
有符号数:char自身作为低八位,不足的高位用符号位补齐。
(3) 关于上面两点的例题
char a = 128;
char b = -128;
unsigned char c = 128;
printf("%u\n", a); //4294967168
printf("%u\n", b); //4294967168
printf("%u\n", c); //128
printf("%d\n", sizeof(a)); //1
printf("%d\n", sizeof(+a)); //4
由于取值循环,a、b所得到的结果是相同的:
a、b的补码都是:1000,0000 => 按符号位提升:11111111,11111111,11111111,10000000
c的补码也是:1000,0000 => 无符号数按0提升:00000000,00000000,00000000,10000000
2. 整型 int
各整型 | 字节数 | 表示范围 |
short int | 2 | -32,768 ~ 32,767 |
int | 4 | -2,147,483,648 ~ 2,147,483,647 |
unsigned int | 4 | 0 ~ 4,294,967,295 |
long int | 4 | -2,147,483,648 ~ 2,147,483,647 |
关于整型这一块,主要想提的就是它的 “截取” 特性,即1 / 3 = 0 的这种特性。当我们刚接触整型int时,或多或少都会被它坑过。但是当你慢慢往后学习的过程中就会发现,它在一些地方也会变得很好用,例如: float num = 3.1;
向下取整:int(num); 向上取整:int(num+1); 取得小数:num - int(num); 四舍五入:int(num + 0.5);
还有 / 配合 % 获得某个整型的各位等等
3. 单精度浮点型 float
(1) 存储规则
需要注意的就是,浮点数的存储与整型不同,它实际的值并不能直接从内存中体现出来。它有着自己的一套存储规则,但是仅仅用文字确实不太好描述,所以这里不做具体规则的陈述,可以去查找专门说明浮点数存储的文章进行查看。
(2) 判断相等时允许一定误差
float result = 1.0/3; -- 《陷阱》
printf("%f",result); // 0.333333
if(result == 0.333333) //not equal
printf("equal");
else
printf("not equal");
//if(result - 0.333333 < 0.01)
float可以精确到小数点后第6位,但是不代表它本身的值就等于精确后的结果,准确来说它们几乎不可能相等。如果有 在类似情况下让它们相等 的需求的话,可以通过它们的差值不大于某个数来给出是否相等的判断。
4. 隐式转换
1° 赋值时,一律是右值转换成左值类型。但当右值是表达式时,会先进行运算,然后才对运算的结果进行数据类型转换。
2° 不同类型的变量进行计算时,遵循由低级向高级转换的规则,例如,char向int转换,short向int转换,float向double转换。有符号数向无符号数转换。
3° 隐式转换所带来的一些问题:
//返回值的隐式转换带来的问题
//1
int i = -1;
if(i < sizeof(int) )
printf("小于成立");
else
printf("小于不成立"); //结果为:小于不成立
//2
char c;
while ((c = getchar()) != EOF) //可能导致死循环
{
putchar(c);
}
1> 会出现-1 > 4 的原因是,sizeof的返回值为 size_t,它是一个无符号整型。所以当它们进行比较时,有符号数-1会被转换为无符号类型,而无符号类型的-1表示的是无符号整数的最大值。
2> 这段代码可以将键盘输入的字符打印到控制台,通常情况下是不会出问题的。但如果某个系系统默认的char是 unsigned char类型,上面的代码就会出现死循环。原因是:当getchar()最后读到EOF时,结果为 (unsigned int) 255 != (unsigned int) -1 ,很显然不相等,无法跳出while循环。
这个过程涉及到:隐式转换、溢出截断、整型提升,试试看你能不能分析出最终结果
(getchar返回值为int类型 ;EOF为int类型,值为 -1)
3> 你会发现,隐式转换的发生确实很 "隐式" ,所以我们平时要多关注常用的函数的返回值类型,特别是一些容易先入为主的函数。
5. 结构体类型struct
(1) 结构体在初始化时,建议不要直接用{"张三" , 123 }的形式,而是具体到成员变量名:person.name = "张三" 。
主要就是当初始化后,自己或者别人对使用的结构体成员进行修改时,可以避免掉一些不必要的错误,同时逻辑也更清晰。
(2) 结构体占用内存大小并不是单纯的相加,即要参考 struct 内存对齐的规则。
1°第一个成员在与结构体变量偏移量为0的地址处。
2°其他成员变量要对齐到对齐数整数倍的地址处。
3°结构体的总大小为最大对齐数的整数倍。
4°当出现结构体嵌套时,内嵌结构体对齐到自己最大对齐数整数倍的地址处,此时结构体总大小 为所有成员的对齐数中最大的整数倍。
对齐数 = min (编译器默认的对齐数 , 该成员大小)
结语
这个系列总共会分为八个篇章,分别是:基础问题、数组、指针、类和对象、C++容器、文件处理、宏定义和编程习惯。 (也许、大概、待定) ( 能不能完成也未知 (捂脸))
涉及内容以及题目来源自:鹏哥 《c语言编程》、黑马《C++教程从0到1入门编程》、《从陷阱中学习C、C++》。 如果有错误的地方,欢迎指出~