C、C++基础知识和常见陷阱误区(1) -- 基础问题篇

本文详细介绍了C/C++中的运算符,包括移位、位运算、取相反数、自增和赋值运算,强调了它们的特性和常见陷阱。同时,探讨了字符型、整型和浮点型数据类型的表示范围、存储规则以及隐式转换问题。文章还提醒读者注意结构体初始化和内存对齐规则,以避免潜在错误。
摘要由CSDN通过智能技术生成

前言

前阵子刚好读完了一本书——《从缺陷中学习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

符号类型字节数表示范围
char4-128 ~ 127
unsigned char40 ~ 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 int2-32,768 ~ 32,767
int4-2,147,483,648 ~ 2,147,483,647
unsigned int40 ~ 4,294,967,295
long int4-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++》。 如果有错误的地方,欢迎指出~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值