-
2020-10-22 21:31:37
目录
逻辑操作符/布尔运算
假设变量 a 为 10, b为 20:
逻辑操作符 运算符 逻辑表达式 描述 实例 and x and y "与" - 如果 x 为 False,x and y 返回 False,否则它返回 y 的计算值。 (a and b) 返回 20。 or x or y "或" - 如果 x 是 True,它返回 x 的值,否则它返回 y 的计算值。 (a or b) 返回 10。 not not x "非" - 如果 x 为 True,返回 False 。如果 x 为 False,它返回 True。 not(a and b) 返回 False 一般情况下,a、b为表达式。
位操作符
假设变量 a 为 60,b 为 13:
二进制补码表示 a 0 0 1 1 1 1 0 0 b 0 0 0 0 1 1 0 1 操作符 描述 实例 & 按位与:参与运算的两个值,如果两个相应位都为1,则该位的结果为1,否则为0 (a & b) 输出结果 12 ,二进制解释: 0000 1100 | 按位或运算符:只要对应的二个二进位有一个为1时,结果位就为1。 (a | b) 输出结果 61 ,二进制解释: 0011 1101 ^ 按位异或运算符:当两对应的二进位不同时,结果为1 (a ^ b) 输出结果 49 ,二进制解释: 0011 0001 ~ 按位取反运算符:对数据的每个二进制位取反,即把1变为0,把0变为1。~x 类似于 -x-1 (~a ) 输出结果 -61 ,二进制解释: 1100 0011(一个有符号二进制数的补码形式),或者-11 1101。 << 左移动运算符:运算数的各二进位全部左移若干位,由"<<"右边的数指定移动的位数,高位丢弃,低位补0。 a << 2 输出结果 240 ,二进制解释: 1111 0000 >> 右移动运算符:把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数 a >> 2 输出结果 15 ,二进制解释: 0000 1111 标准算术操作符
a,b为数字型,c为字符串
算术操作符 操作符 描述 实例 + 两个树相加或对象拼接 a + b - 得到负数或是一个数减去另一个数 a - b * 两个数相乘或对象重复 a * b / x 除以 y b / a % 取模 - 返回除法的余数 b % a ** 幂 - 返回x的y次幂 a**b // 取整除 - 向下取接近商的整数 >>> 9//2 4 >>> -9//2 -5
比较操作符
比较操作符 操作符 描述 实例 == 比较两个对象是否相等(调用__eq__()函数) a == b != 比较两个对象是否不相等 a != b > 返回x是否大于y a > b < 返回x是否小于y。 a < b >= 返回x是否大于等于y。 a >= b <= 返回x是否小于等于y。 a <= b is 比较两个对象是否相等(调用id()函数) a is b is not 比较两个对象是否不相等 a is not b 比较单例模式的对象,例如,None时,应使用is 或is not,参考Python-Python编码规范(PEP8)。
赋值操作符
运算符 描述 实例 = 简单的赋值运算符 c = a + b 将 a + b 的运算结果赋值为 c += 数字加法赋值运算符/对象(字符串,列表等)拼接操作符 c += a 等效于 c = c + a -= 减法赋值运算符 c -= a 等效于 c = c - a *= 乘法赋值运算符/对象(字符串,列表等)重复操作符 c *= a 等效于 c = c * a /= 除法赋值运算符 c /= a 等效于 c = c / a %= 取模赋值运算符 c %= a 等效于 c = c % a **= 幂赋值运算符 c **= a 等效于 c = c ** a //= 取整除赋值运算符 c //= a 等效于 c = c // a 操作符优先级
操作符优先级 运算符 描述 ** 幂 (最高优先级) ~ 、+、 - 按位翻转, 一元加号和减号 * 、/、 %、 // 乘,除,求余数和取整除 + 、- 加法和减法 >> 、<< 右移和左移操作符 & 位 'AND' ^ 、| 位操作符 <、=< 、>、>= 比较操作符 ==、!= 等于比较操作符 =、%=、/=、//=、-=、+=、*=、**= 赋值操作符 is、is not 身份比较操作符 in、not in 成员操作符 not、and、or 逻辑操作符 接下来,我会将操作符穿插在其他文章中进行展示。
更多python相关内容:【python总结】python学习框架梳理
本人b站账号:lady_killer9
有问题请下方评论,转载请注明出处,并附有原文链接,谢谢!如有侵权,请及时联系。如果您感觉有所收获,自愿打赏,可选择支付宝18833895206(小于),您的支持是我不断更新的动力。
更多相关内容 -
C++中复制构造函数和重载赋值操作符总结
2020-12-31 17:15:56这篇文章将对C++中复制构造函数和重载赋值操作符进行总结,包括以下内容: 1.复制构造函数和重载赋值操作符的定义; 2.复制构造函数和重载赋值操作符的调用时机; 3.复制构造函数和重载赋值操作符的实现要点; 4.... -
详解C++赋值操作符重载
2021-01-19 23:43:391.赋值操作符重载的原因 赋值操作符是一个使用频率最高的操作之一,通常情况下它的意义十分明确,就是将两个同类型的变量的值从一端(右端)传到另一端(左端)。但在以下两种情况下,需要对赋值操作符进行重载。 一... -
C++重载赋值操作符
2022-02-19 09:34:45重载赋值操作符重载赋值操作符 重载赋值操作符 赋值操作符“=”可以用来将一个对象拷贝给另一个已经存在的对象。如果我们重新定义了一种新的数据类型,比如说复数类,那么我们就需要重载一下赋值操作符,使之能够...重载赋值操作符
赋值操作符“=”可以用来将一个对象拷贝给另一个已经存在的对象。如果我们重新定义了一种新的数据类型,比如说复数类,那么我们就需要重载一下赋值操作符,使之能够满足我们的赋值需求。
当然,拷贝构造函数也有同样的功能。拷贝构造函数可以将一个对象拷贝给另一个新建的对象。如果我们没有在类中显式定义拷贝构造函数,也没有重载赋值操作符,那么系统会为我们的类提供一个默认的拷贝构造函数和一个赋值操作符。前面在介绍类的相关知识时已经提到,系统为我们提供的默认拷贝构造函数只是将源对象中的数据一一拷贝给目标对象,而系统为类提供的赋值操作符也是这样的一种功能。
complex c1(4.3, -5.8); complex c2; c2 = c1; cout<<c1<<endl; cout<<c2<<endl;
执行结果为:
4.3 + -5.8 i 4.3 + -5.8 i
利用前面我们定义的 complex 类,我们先定义了两个 complex 类的对象 c1 和 c2,c1 对象通过带参构造函数初始化,之后用 c1 来初始化 c2,最后输出这两个复数类对象。
注意在 complex 类中,我们并未定义拷贝构造函数,也没有重载赋值操作符,但是例 1 中c2 = c1;并未有语法错误,根据程序运行结果可以得知,该赋值操作成功地完成了。这是因为,系统默认为类提供了一个拷贝构造函数和一个赋值操作符,而数据一对一的拷贝也满足我们复数类的需求了。
在前面介绍拷贝构造函数时我们提到过,系统提供的默认拷贝构造函数有一定的缺陷,例如当类中的成员变量包含指针的时候,会导致一些意想不到的程序漏洞,此时就需要重新定义一个拷贝构造函数。在相同的情况下,系统提供的赋值操作符也无法满足我们的需求,必须要进行重载。
在前面介绍拷贝构造函数一节中,我们已经详细分析了系统提供的默认拷贝构造函数遇到指针成员变量时带来的风险,直接使用系统默认提供的赋值操作符同样会有此种风险,在此我们将不再重新分析这一问题,而只是将前面的示例再次拿过来,并且在程序中补上赋值操作符重载函数。
#include<iostream> using namespace std; class Array { public: Array(){length = 0; num = NULL;}; Array(int * A, int n); Array(Array & a); Array & operator= (const Array & a); void setnum(int value, int index); int * getaddress(); void display(); int getlength(){return length;} private: int length; int * num; }; Array::Array(Array & a) { if(a.num != NULL) { length = a.length; num = new int[length]; for(int i=0; i<length; i++) num[i] = a.num[i]; } else { length = 0; num = 0; } } //重载赋值操作符 Array & Array::operator= (const Array & a) { if( this != &a ) { delete[] num; if(a.num != NULL) { length = a.length; num = new int[length]; for(int i=0; i<length; i++) num[i] = a.num[i]; } else { length = 0; num = 0; } } return *this; } Array::Array(int *A, int n) { num = new int[n]; length = n; for(int i=0; i<n; i++) num[i] = A[i]; } void Array::setnum(int value, int index) { if(index < length) num[index] = value; else cout<<"index out of range!"<<endl; } void Array::display() { for(int i=0; i<length; i++) cout<<num[i]<<" "; cout<<endl; } int * Array::getaddress() { return num; } int main() { int A[5] = {1,2,3,4,5}; Array arr1(A, 5); arr1.display(); cout << "*********copy constructor**********" << endl; Array arr2(arr1); arr2.display(); arr2.setnum(8,2); arr1.display(); arr2.display(); cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl; cout << "*********assignment constructor**********" << endl; arr1 = arr2; arr1.display(); arr2.display(); arr2.setnum(9,3); arr1.display(); arr2.display(); cout<<arr1.getaddress()<<" "<<arr2.getaddress()<<endl; return 0; }
深拷贝:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。
通常大家会对拷贝构造函数和赋值函数混淆,这儿仔细比较两者的区别:
1)拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作
2)一般来说在数据成员包含指针对象的时候,需要考虑两种不同的处理需求:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象3)实现不一样。拷贝构造函数首先是一个构造函数,它调用时候是通过参数的对象初始化产生一个对象。赋值函数则是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。(这些要点会在下面的String实现代码中体现)
@& 如果不想写拷贝构造函数和赋值函数,又不允许别人使用编译器生成的缺省函数,最简单的办法是将拷贝构造函数和赋值函数声明为私有函数,不用编写代码。如:
class A { private: A(const A& a); //私有拷贝构造函数 A& operate=(const A& a); //私有赋值函数 } A a; A b(a); //调用了私有拷贝构造函数,编译出错 A b; b=a;
4.复制构造函数是构造函数,而赋值操作符属于操作符重载范畴,它通常是类的成员函数
5.复制构造函数原型ClassType(const ClassType &);无返回值;赋值操作符原型ClassType& operator=(const ClassType &);返回值为ClassType的引用,便于连续赋值操作#include <iostream> #include <stdio.h> #include <stdlib.h> #include <string.h> using namespace std; class Person{ public: Person(char *name); Person(const Person& a); ~Person(); public: Person& operator =(const Person& a); void dis(); private: int m_len; char *m_name; }; Person::Person(char *name) { printf("%s %d %s\n",__FILE__, __LINE__, __FUNCTION__); m_len = strlen(name)+1; m_name = new char[m_len+1]; memcpy(m_name, name, m_len); } Person::Person(const Person& a) { printf("%s %d %s\n",__FILE__, __LINE__, __FUNCTION__); m_len = a.m_len; m_name = new char[m_len+1]; memcpy(m_name, a.m_name, m_len); } Person::~Person() { printf("%s %d %s\n",__FILE__, __LINE__, __FUNCTION__); delete[] m_name; } Person& Person::operator =(const Person& a) { printf("%s %d %s\n",__FILE__, __LINE__, __FUNCTION__); if(&a == this) { return *this; } else { delete []this->m_name; int lens = strlen(a.m_name); this->m_name = new char[lens +1]; strcpy(this->m_name, a.m_name); return *this; } } void Person::dis() { cout<<"len: " << m_len<<endl; cout<<"name: " << m_name<<endl; } int main() { cout << "Hello World!" << endl; Person a("wangwu"); a.dis(); Person b(a);//调用复制构造函数 b.dis(); Person c = b;//调用复制构造函数 c.dis(); Person d("zhangshan"); d = c; //调用赋值构造函数 d.dis(); return 0; }
自己小结(心法)
拷贝构造函数是无中生有(克隆羊),本质是构造函数。
赋值函数是重新做人(肉身不变,大换血,精神面貌焕然一新),本质是类的成员函数。reference
1.微学苑 C++重载赋值操作符
2.C++中构造函数,拷贝构造函数和赋值函数的区别和实现
3.拷贝构造函数和赋值函数的区别 -
详解C++-(=)赋值操作符、智能指针编写
2020-12-25 19:02:13(=)赋值操作符 编译器为每个类默认重载了(=)赋值操作符 默认的(=)赋值操作符仅完成浅拷贝 默认的赋值操作符和默认的拷贝构造函数有相同的存在意义 (=)赋值操作符注意事项 首先要判断两个操作数是否相等 返回值... -
PHP中=赋值操作符对不同数据类型的不同行为
2021-01-20 00:20:18首先解释赋值操作符=的行为,看下面的例子: 复制代码 代码如下: $i = 0; $j = $i; $j = 0; echo $j; // 打印输出0 $arr = array(0); $arr2 = $arr; $arr2[0] = 1; echo $arr[0]; //打印输出0 class B { public $i ... -
C++中复制构造函数与重载赋值操作符的深入分析
2020-08-10 15:38:16在C++中复制控制是一个比较重要的话题,主要包括复制构造函数、重载赋值操作符、析构函数这三部分,这三个函数是一致的,如果需要手动定义了其中了一个,那么另外的两个也需要定义,通常在存在指针或者前期相关操作... -
C++程序入门之——赋值操作符
2019-11-13 18:10:36赋值语句 前面已经说明,要访问内存,就需要相应的地址以表明访问哪块内存,而变量是一个映射,因此变量名就相当于一个地址。对于内存的操作,在一般情况下就只有读取内存中的数值和将数值写入内存(不考虑分配和...赋值语句
前面已经说明,要访问内存,就需要相应的地址以表明访问哪块内存,而变量是一个映射,因此变量名就相当于一个地址。对于内存的操作,在一般情况下就只有读取内存中的数值和将数值写入内存(不考虑分配和释放内存),在C++中,为了将一数值写入某变量对应的地址所标识的内存中(出于简便,以后称变量a对应的地址为变量a的地址,而直接称变量a的地址所标识的内存为变量a),只需先书写变量名,后接“=”,再接欲写入的数字以及分号。如下:a = 10.0f; b = 34;
由于接的是数字,因此就可以接表达式并由编译器生成计算相应表达式所需的代码,也就可如下:
c = a / b * 120.4f;
上句编译器将会生成进行除法和乘法计算的CPU指令,在计算完毕后(也就是求得表达式a / b * 120.4f的值了后),也会同时生成将计算结果放到变量c中去的CPU指令,这就是语句的基本作用(对于语句,在《C++从零开始(六)》中会详细说明)。
上面在书写赋值语句时,应该确保此语句之前已经将使用到的变量定义过,这样编译器才能在生成赋值用的CPU指令时查找到相应变量的地址,进而完成CPU指令的生成。如上面的a和b,就需要在书写上面语句前先书写类似下面的变量定义:float a; long b;
直接书写变量名也是一条语句,其导致编译器生成一条读取相应变量的内容的语句。即可以如下书写:a;
上面将生成一条读取内存的语句,即使从内存中读出来的数字没有任何应用(当然,如果编译器开了优化选项,则上面的语句将不会生成任何代码)。从这一点以及上面的c = a / b * 120.4f;语句中,都可以看出一点——变量是可以返回数字的。而变量返回的数字就是按照变量的类型来解释变量对应内存中的内容所得到的数字。这句话也许不是那么容易理解,在看过后面的类型转换一节后应该就可以理解了。因此为了将数据写入一块内存,使用赋值语句(即等号);要读取一块内存,书写标识内存的变量名。所以就可以这样书写:a = a + 3;
假设a原来的值为1,则上面的赋值语句将a的值取出来,加上3,得到结果4,将4再写入a中去。由于C++使用“=”来代表赋值语句,很容易使人和数学中的等号混淆起来,这点应注意。
而如上的float a;语句,当还未对变量进行任何赋值操作时,a的值是什么?上帝才知道。当时的a的内容是什么(对于VC编译器,在开启了调试选项时,将会用0xCCCCCCCC填充这些未初始化内存),就用IEEE的real*4格式来解释它并得到相应的一个数字,也就是a的值。因此应在变量定义的时候就进行赋值(但是会有性能上的影响,不过很小),以初始化变量而防止出现莫名其妙的值,如:float a = 0.0f;。
赋值操作符
上面的a = a + 3;的意思就是让a的值增加3。在C++中,对于这种情况给出了一种简写方案,即前面的语句可以写成:a += 3;。应当注意这两条语句从逻辑上讲都是使变量a的值增3,但是它们实际是有区别的,后者可以被编译成优化的代码,因为其意思是使某一块内存的值增加一定数量,而前者是将一个数字写入到某块内存中。所以如果可能,应尽量使用后者,即a += 3;。这种语句可以让编译器进行一定的优化(但由于现在的编译器都非常智能,能够发现a = a + 3;是对一块内存的增值操作而不是一块内存的赋值操作,因此上面两条语句实际上可以认为完全相同,仅仅只具有简写的功能了)。
对于上面的情况,也可以应用在减法、乘法等二元非逻辑操作符(不是逻辑值操作符,即不能a &&= 3;)上,如:a *= 3; a -= 4; a |= 34; a >>= 3;等。
除了上面的简写外,C++还提供了一种简写方式,即a++;,其逻辑上等同于a += 1;。同上,在电脑编程中,加一和减一是经常用到的,因此CPU专门提供了两条指令来进行加一和减一操作(转成汇编语言就是Inc和Dec),但速度比直接通过加法或减法指令来执行要快得多。为此C++中也就提供了“++”和“—”操作符来对应Inc和Dec。所以a++;虽然逻辑上和a = a + 1;等效,实际由于编译器可能做出的优化处理而不同,但还是如上,由于编译器的智能化,其是有可能看出a = a + 1;可以编译成Inc指令进而即使没有使用a++;却也依然可以得到优化的代码,这样a++;将只剩下简写的意义而已。应当注意一点,a = 3;这句语句也将返回一个数字,也就是在a被赋完值后a的值。由于其可以返回数字,按照《C++从零开始(二)》中所说,“=”就属于操作符,也就可以如下书写:
c = 4 + ( a = 3 );
之所以打括号是因为“=”的优先级较“+”低,而更常见和正常的应用是:c = a = 3;
应该注意上面并不是将c和a赋值为3,而是在a被赋值为3后再将a赋值给c,虽然最后结果和c、a都赋值为3是一样的,但不应该这样理解。由于a++;表示的就是a += 1;就是a = a + 1;,因此a++;也将返回一个数字。也由于这个原因,C++又提供了另一个简写方式,++a;。
假设a为1,则a++;将先返回a的值,1,然后再将a的值加一;而++a;先将a的值加一,再返回a的值,2。而a—和—a也是如此,只不过是减一罢了。
上面的变量a按照最上面的变量定义,是float类型的变量,对它使用++操作符并不能得到预想的优化,因为float类型是浮点类型,其是使用IEEE的real4格式来表示数字的,而不是二进制原码或补码,而前面提到的Inc和Dec指令都是出于二进制的表示优点来进行快速增一和减一,所以如果对浮点类型的变量运用“++”操作符,将完全只是简写,没有任何的优化效果(当然,如果CPU提供了新的指令集,如MMX等,以对real4格式进行快速增一和减一操作,且编译器支持相应指令集,则还是可以产生优化效果的)。
赋值操作符的返回值
在进一步了解++a和a++的区别前,先来了解何谓操作符的计算(Evaluate)。操作符就是将给定的数字做一些处理,然后返回一个数字。而操作符的计算也就是执行操作符的处理,并返回值。前面已经知道,操作符是个符号,其一侧或两侧都可以接数字,也就是再接其他操作符,而又由于赋值操作符也属于一种操作符,因此操作符的执行顺序变得相当重要。
对于a + b + c,将先执行a + b,再执行( a + b ) + c的操作。你可能觉得没什么,那么如下,假设a之前为1:c = ( a *= 2 ) + ( a += 3 );
上句执行后a为5。而c = ( a += 3 ) + ( a *= 2 );执行后,a就是8了。那么c呢?结果可能会大大的出乎你的意料。前者的c为10,而后者的c为16。
上面其实是一个障眼法,其中的“+”没有任何意义,即之所以会从左向右执行并不是因为“+”的缘故,而是因为( a *= 2 )和( a += 3 )的优先级相同,而按照“()”的计算顺序,是从左向右来计算的。但为什么c的值不是预想的2 + 5和4 + 8呢?因为赋值操作符的返回值的关系。
赋值操作符返回的数字不是变量的值,而是变量对应的地址。这很重要。前面说过,光写一个变量名就会返回相应变量的值,那是因为变量是一个映射,变量名就等同于一个地址。C++中将数字看作一个很特殊的操作符,即任何一个数字都是一个操作符。而地址就和长整型、单精度浮点数这类一样,是数字的一种类型。当一个数字是地址类型时,作为操作符,其没有要操作的数字,仅仅返回将此数字看作地址而标识的内存中的内容(用这个地址的类型来解释)。地址可以通过多种途径得到,如上面光写一个变量名就可以得到其对应的地址,而得到的地址的类型也就是相应的变量的类型。如果这句话不能理解,在看过下面的类型转换一节后应该就能了解了。
所以前面的c = ( a += 3 ) + ( a = 2 );,由于“()”的参与改变了优先级而先执行了两个赋值操作符,然后两个赋值操作符都返回a的地址,然后计算“+”的值,分别计算两边的数字——a的地址(a的地址也是一个操作符),也就是已经执行过两次赋值操作的a的值,得8,故最后的c为16。而另一个也由于同样的原因使得c为10。
现在考虑操作符的计算顺序。当同时出现了几个优先级相同的操作符时,不同的操作符具有不同的计算顺序。前面的“()”以及“-”、“”等这类二元操作符的计算顺序都是从左向右计算,而“!”、负号“-”等前面介绍过的一元操作符都是从右向左计算的,如:!-!!a;,假设a为3。先计算从左朝右数第三个“!”的值,导致计算a的地址的值,得3;然后逻辑取反得0,接着再计算第二个“!”的值,逻辑取反后得1,再计算负号“-”的值,得-1,最后计算第一个“!”的值,得0。赋值操作符都是从右向左计算的,除了后缀“++”和后缀“—”(即上面的a++和a–)。因此上面的c = a = 3;,因为两个“=”优先级相同,从右向左计算,先计算a = 3的值,返回a对应的地址,然后计算返回的地址而得到值3,再计算c = ( a = 3 ),将3写入c。而不是从左向右计算,即先计算c = a,返回c的地址,然后再计算第二个“=”,将3写入c,这样a就没有被赋值而出现问题。又:
a = 1; c = 2; c *= a += 4;
由于“=”和“+=”的优先级相同,从右向左计算先计算a += 4,得a为5,然后返回a的地址,再计算a的地址得a的值5,计算“=”以使得c的值为10。
因此按照前面所说,++a将返回a的地址,而a++也因为是赋值操作符而必须返回一个地址,但很明显地不能是a的地址了,因此编译器将编写代码以从栈中分配一块和a同样大小的内存,并将a的值复制到这块临时内存中,然后返回这块临时内存的地址。由于这块临时内存是因为编译器的需要而分配的,与程序员完全没有关系,因此程序员是不应该也不能写这块临时内存的(因为编译器负责编译代码,如果程序员欲访问这块内存,编译器将报错),但可以读取它的值,这也是返回地址的主要目的。所以如下的语句没有问题:( ++a ) = a += 34;
但( a++ ) = a += 34;就会在编译时报错,因为a++返回的地址所标识的内存只能由编译器负责处理,程序员只能获得其值而已。
a++的意思是先返回a的值,也就是上面说的临时内存的地址,然后再将变量的值加一。如果同时出现多个a++,那么每个a++都需要分配一块临时内存(注意前面c = ( a += 3 ) + ( a *= 2 );的说明),那么将有点糟糕,而且a++的意思是先返回a的值,那么到底是什么时候的a的值呢?在VC中,当表达式中出现后缀“++”或后缀“—”时,只分配一块临时内存,然后所有的后缀“++”或后缀“—”都返回这个临时内存的地址,然后在所有的可以计算的其他操作符的值计算完毕后,再将对应变量的值写入到临时内存中,计算表达式的值,最后将对应变量的值加一或减一。
因此:a = 1; c = ( a++ ) + ( a++ );执行后,c的值为2,而a的值为3。而如下:a = 1; b = 1; c = ( ++a ) + ( a++ ) + ( b *= a++ ) + ( a *= 2 ) + ( a *= a++ );
执行时,先分配临时内存,然后由于5个“()”,其计算顺序是从左向右,
计算++a的值,返回增一后的a的地址,a的值为2
计算a++的值,返回临时内存的地址,a的值仍为2
计算b *= a++中的a++,返回临时内存的地址,a的值仍为2
计算b = a++中的“=”,将a的值写入临时内存,计算得b的值为2,返回b的地址
计算a *= 2的值,返回a的地址,a的值为4
计算a *= a++中的a++,返回临时内存的地址,a的值仍为4
计算a = a++中的“=”,将a的值写入临时内存,返回a的地址,a的值为16
计算剩下的“+”,为了进行计算,将a的值写入临时内存,得值16 + 16 + 2 + 16 + 16为66,写入c中
计算三个a++欠下的加一,a最后变为19。
上面说了那么多,无非只是想告诫你——在表达式中运用赋值操作符是不被推崇的。因为其不符合平常的数学表达式的习惯,且计算顺序很容易搞混。如果有多个“++”操作符,最好还是将表达式分开,否则很容易导致错误的计算顺序而计算错误。并且导致计算顺序混乱的还不止上面的a++就完了,为了让你更加地重视前面的红字,下面将介绍更令人火大的东西,如果你已经同意上面的红字,则下面这一节完全可以跳过,其对编程来讲可以认为根本没有任何意义(要不是为了写这篇文章,我都不知道它的存在)。
序列点(Sequence Point)和附加效果(Side Effect)
在计算c = a++时,当c的值计算(Evaluate)出来时,a的值也增加了一,a的值加一就是计算前面表达式的附加效果。有什么问题?它可能影响表达式的计算结果。对于a = 0; b = 1; ( a = 2 ) && ( b += 2 );,由于两个“()”优先级相同,从左向右计算,计算“=”而返回a的地址,再计算“+=”而返回b的地址,最后由于a的值为0而返回逻辑假。很正常,但效率低了点。
如果“&&”左边的数字已经是0了,则不再需要计算右边的式子。同样,如果“||”左边的数字已经非零了,也不需要再计算右边的数字。因为“&&”和“||”都是数学上的,数学上不管先计算加号左边的值还是右边的值,结果都不会改变,因此“&&”和“||”才会做刚才的解释。这也是C++保证的,既满足数学的定义,又能提供优化的途径(“&&”和“||”右边的数字不用计算了)。
因此上面的式子就会被解释成——如果a在自乘了2后的值为0,则b就不用再自增2了。这很明显地违背了我们的初衷,认为b无论如何都会被自增2的。但是C++却这样保证,不仅仅是因为数学的定义,还由于代码生成的优化。但是按照操作符的优先级进行计算,上面的b += 2依旧会被执行的(这也正是我们会书写上面代码的原因)。为了实现当a为0时b += 2不会被计算,C++提出了序列点的概念。
序列点是一些特殊位置,由C++强行定义(C++并未给出序列点的定义,因此不同的编译器可能给出不同的序列点定义,VC是按照C语言定义的序列点)。当在进行操作符的计算时,如果遇到序列点,则序列点处的值必须被优先计算,以保证一些特殊用途,如上面的保证当a为0时不计算b += 2,并且序列点相关的操作符(如前面的“&&”和“||”)也将被计算完毕,然后才恢复正常的计算。
“&&”的左边数字的计算就是一个序列点,而“||”的左边数字的计算也是。C++定义了多个序列点,包括条件语句、函数参数等条件下的表达式计算,在此,不需要具体了解有哪些序列点,只需要知道由于序列点的存在而可能导致赋值操作符的计算出乎意料。下面就来分析一个例子:a = 0; b = 1; ( a *= 2 ) && ( b += ++a );
按照优先级的顺序,编译器发现要先计算a *= 2,再计算++a,接着“+=”,最后计算“&&”。然后编译器发现这个计算过程中,出现了“&&”左边的数字这个序列点,其要保证被优先计算,这样就有可能不用计算b += ++a了。所以编译器先计算“&&”的数字,通过上面的计算过程,编译器发现就要计算a *= 2才能得到“&&”左边的数字,因此将先计算a *= 2,返回a的地址,然后计算“&&”左边的数字,得a的值为0,因此就不计算b += ++a了。而不是最开始想象的由于优先级的关系先将a加一后再进行a的计算,以返回1。所以上面计算完毕后,a为0,b为1,返回0,表示逻辑假。
因此序列点的出现是为了保证一些特殊规则的出现,如上面的“&&”和“||”。再考虑“,”操作符,其操作是计算两边的值,然后返回右边的数字,即:a, b + 3将返回b + 3的值,但是a依旧会被计算。由于“,”的优先级是最低的(但高于前面提到的“数字”操作符),因此如果a = 3, 4;,那么a将为3而不是4,因为先计算“=”,返回a的地址后再计算“,”。又:a = 1; b = 0; b = ( a += 2 ) + ( ( a *= 2, b = a - 1 ) && ( c = a ) );
由于“&&”左边数字是一个序列点,因此先计算a *= 2, b的值,但根据“,”的返回值定义,其只返回右边的数字,因此不计算a *= 2而直接计算b = a – 1得0,“&&”就返回了,但是a *= 2就没有被计算而导致a的值依旧为1,这违背了“,”的定义。为了消除这一点(当然可能还有其他应用“,”的情况),C++也将“,”的左边数字定为了序列点,即一定会优先执行“,”左边的数字以保证“,”的定义——计算两边的数字。所以上面就由于“,”左边数字这个序列点而导致a *= 2被优先执行,并导致b为1,因此由于“&&”是序列点且其左边数字非零而必须计算完右边数字后才恢复正常优先级,而计算c = a,得2,最后才恢复正常优先级顺序,执行a += 2和“+”。结果就a为4,c为2,b为5。
所以前面的a = 3, 4;其实就应该是编译器先发现“,”这个序列点,而发现要计算“,”左边的值,必须先计算出a = 3,因此才先计算a = 3以至于感觉序列点好像没有发生作用。下面的式子请自行分析,执行后a为4,但如果将其中的“,”换成“&&”,a为2。
a = 1; b = ( a *= 2 ) + ( ( a *= 3 ), ( a -= 2 ) );
如果上面你看得很晕,没关系,因为上面的内容根本可以认为毫无意义,写在这里也只是为了进一步向你证明,在表达式中运用赋值运算符是不好的,即使它可能让你写出看起来简练的语句,但它也使代码的可维护性降低。
类型转换
数字可以是浮点数或是整型数或其他,也就是说数字是具有类型的。注意《C++从零开始(三)》中对类型的解释,类型只是说明如何解释状态,而在前面已经说过,出于方便,使用二进制数来表示状态,因此可以说类型是用于告诉编译器如何解释二进制数的。
所以,一个长整型数字是告诉编译器将得到的二进制数表示的状态按照二进制补码的格式来解释以得到一个数值,而一个单精度浮点数就是告诉编译器将得到的二进制数表示的状态按照IEEE的real4的格式来解释以得到一个是小数的数值。很明显,同样的二进制数表示的状态,按照不同的类型进行解释将得到不同的数值,那么编译器如何知道应该使用什么类型来进行二进制数的解释?
前面已经说过,数字是一种很特殊的操作符,其没有操作数,仅仅返回由其类型而定的二进制数表示的状态(以后为了方便,将“二进制数表示的状态”称作“二进制数”)。而操作符就是执行指令并返回数字,因此所有的操作符到最后一定执行的是返回一个二进制数。这点很重要,对于后面指针的理解有着重要的意义。
先看15;,这是一条语句,因为15是一个数字。所以15被认为是char类型的数字(因为其小于128,没超出char的表示范围),将返回一个8位长的二进制数,此二进制数按照补码格式编写,为00001111。
再看15.0f,同上,其由于接了“f”这个后缀而被认为是float类型的数字,将返回一个32位长的二进制数,此二进制数按照IEEE的real4格式编写,为1000001011100000000000000000000。
虽然上面15和15.0f的数值相等,但由于是不同的类型导致了使用不同的格式来表示,甚至连表示用的二进制数的长度都不相同。因此如果书写15.0f == 15;将返回0,表示逻辑假。但实际却返回1,为什么?
上面既然15和15.0f被表示成完全不同的两个二进制数,但我们又认为15和15.0f是相等的,但它们的二进制表示不同,怎么办?将表示15.0f的二进制数用IEEE的real*4格式解释出15这个数值,然后再将其按8位二进制补码格式编写出二进制数,再与原来的表示15的二进制数比较。
为了实现上面的操作,C++提供了类型转换操作符——“()”。其看起来和括号操作符一样,但是格式不同:(<类型名>)<数字>或<类型名>(<数字>)。
上面类型转换操作符的<类型名>不是数字,因此其将不会被操作,而是作为一个参数来控制其如何操作后面的<数字>。<类型名>是一个标识符,其唯一标识一个类型,如char、float等。类型转换操作符的返回值就如其名字所示,将<数字>按照<类型名>标识的类型来解释,返回类型是<类型名>的数字。因此,上面的例子我们就需要如下编写:15 == ( char )15.0f;,现在其就可以返回1,表示逻辑真了。但是即使不写( char ),前面的语句也返回1。这是编译器出于方便的缘故而帮我们在15前添加了( float ),所以依然返回1。这被称作隐式类型转换,在后面说明类的时候,还将提到它。
某个类型可以完全代替另一个类型时,编译器就会进行上面的隐式类型转换,自动添加类型转换操作符。如:char只能表示-128到127的整数,而float很明显地能够表示这些数字,因此编译器进行了隐式类型转换。应当注意,这个隐式转换是由操作符要求的,即前面的“”要求两面的数字类型一致,结果发现两边不同,结果编译器将char转成float,然后再执行“”的操作。注意:在这种情况下,编译器总是将较差的类型(如前面的char)转成较好的类型(如前面的float),以保证不会发生数值截断问题。如:-41 == 3543;,左边是char,右边是short,由于short相对于char来显得更优(short能完全替代char),故实际为:( short )-41 == 3543;,返回0。而如果是-41 == ( char )3543;,由于char不能表示3543,则3543以补码转成二进制数0000110111010111,然后取其低8位,而导致高8位的00001101被丢弃,此被称为截断。结果( char )3543的返回值就是类型为char的二进制数11010111,为-41,结果-41 == ( char )3543;的返回值将为1,表示逻辑真,很明显地错误。因此前面的15 == 15.0f;实际将为( float )15 == 15.0f;。注意前面之所以会朝好的方向发展(即char转成float),完全是因为“==”的缘故,其要求这么做。下面考虑“=”:short b = 3543; char a = b;。因为b的值是short类型,而“=”的要求就是一定要将“=”右边的数字转成和左边一样,这样才能进行正确的内存的写入(简单地将右边数字返回的二进制数复制到左边的地址所表示的内存中)。因此a将为-41。但是上面是编译器按照“=”的要求自行进行了隐式转换,可能是由于程序员的疏忽而没有发现这个错误(以为b的值一定在-128到127的范围内),因此编译器将对上面的情况给出一个警告,说b的值可能被截断。为了消除编译器的疑虑,如下:char a = ( char )b;。这样称为显示类型转换,其告诉编译器——“我知道可能发生数据截断,但是我保证不会截断”。因此编译器将不再发出警告。但是如下:char a = ( char )3543;,由于编译器可以肯定3543一定会被截断而导致错误的返回值,因此编译器将给出警告,说明3543将被截断,而不管前面的类型转换操作符是否存在。
现在应该可以推出——15 + 15.0f;返回的是一个float类型的数字。因此如果如下:char a = 15 + 15.0f;,编译器将发出警告,说数据可能被截断。因此改成如下:char a = ( char )15 + 15.0f;,但类型转换操作符“()”的优先级比“+”高,结果就是15先被转换为char然后再由于“+”的要求而被隐式转成float,最后返回float给“=”而导致编译器依旧发出警告。为此,就需要提高“+”的优先级,如下:char a = ( char )( 15 + 15.0f );就没事了(或char( 15 + 15.0f )),其表示我保证15 + 15.0f不会导致数据截断。
应该注意类型转换操作符“()”和前缀“++”、“!”、负号“-”等的优先级一样,并且是从右向左计算的,因此( char )-34;将会先计算-34的值,然后再计算( char )的值,这也正好符合人的习惯。 -
C++中拷贝构造函数与拷贝赋值操作符
2017-08-22 17:42:25当我们显式或者隐式地对该类型进行拷贝操作时,就会用到该类的拷贝构造函数(copy construction)和拷贝赋值操作符(copy-assignment operator)。 1 拷贝构造函数 如果一个构造函数的第一个参数是自身类类型的...当我们显式或者隐式地对该类型进行拷贝操作时,就会用到该类的拷贝构造函数(copy construction)和拷贝赋值操作符(copy-assignment operator)。
1 拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
拷贝构造函数的详细介绍请参见另外两篇文章。
http://blog.csdn.net/hou09tian/article/details/70245341
http://blog.csdn.net/hou09tian/article/details/708446902 拷贝赋值操作符
类拷贝赋值操作符就是”=”,该操作符用于将一个类的对象赋值给类的另一个对象。如下代码所示:
MyClass A, B; A = B;
3 默认的拷贝构造函数与拷贝赋值操作符
如果没有定义类的拷贝构造函数和拷贝赋值操作符,则C++编译器会自动为该类进行定义,这就是默认的拷贝构造函数与拷贝赋值操作符。
3.1 默认的拷贝构造函数
默认的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。假设有一个自定义类
那么该类的默认拷贝构造函数的定义为class person { std::string name; int age; }
person(const std::string& name, int age) : name(name), age(age) { }
3.2 默认的拷贝赋值操作符
默认的拷拷贝赋值操作符会将其参数的成员逐个赋值给正在创建对象的成员。以“3.1 默认的拷贝构造函数”中提到的person类为例,则其默认的拷贝赋值操作符的定义为
person& operator=(const person& that)
{
name = that.name;
age = that.age;
return *this;
}
4 自定义的拷贝构造函数与拷贝赋值操作符
当自定义的类要处理资源的时候,就需要对其拷贝构造函数与拷贝赋值操作符进行自定义。在“3.1 默认的拷贝构造函数”中提到的person类的进行修改:
此时person 类的成员变量 name 的类型改为 char* 。则此时 person 类的构造函数定义为class person { char* name; int age; }
在其构造函数中,使用了new 关键字分配了内存空间,即 person 类涉及到了“管理资源”。则此时的析构函数中就要对其分配的内存空间进行释放person(const char* the_name, int the_age) { name = new char[strlen(the_name) + 1]; strcpy(name, the_name); age = the_age; }
如果此时使用person 类的默认的拷贝构造函数与拷贝赋值操作符,则其默认构造函数的定义为~person() { if( name!= NULL ) { delete []name; name = NULL; } }
需要注意的时,此时默认拷贝构造函数中的name(name) 只是将name 的值即指针值进行了拷贝,并没有拷贝指针指向的字符串。以如下代码为例person(char* name, int age) : name(name), age(age) { }
此时,A.name 与 B.name 的关系如图所示person A("yang", 20); person B(A);
因为A.name和B.name指向同一块内存,当B结束其生命周期时调用其析构函数,此时即使A还在其生命周期内,其A.name指向的内存已经被销毁。
以上代码中,printf 无法打印出 A.name 的值。person A("yang", 20); if (A.m_age > 10) { person B(A);//调用了person的默认拷贝构造函数 } printf("A.name = %s", A.m_name);
4.1 自定义拷贝构造函数
从上面的分析可知,默认拷贝构造函数并没有分配新的内存资源从而导致错误的产生。因此,在自定义拷贝构造函数中,要分配新的内存,并且将内容拷贝到新的内存空间中。
此时,A.name 与 B.name 的关系如图所示person(const person& that) { m_name = new char[strlen(that.m_name)+1]; strcpy(m_name, that.m_name); m_age = that.m_age; }
当B结束其生命周期时调用其析构函数,释放其所占用的内存空间,对A的值没有影响。
4.2 自定义拷贝赋值操作符
自定义拷贝赋值操作符与自定义拷贝构造函数类似,也要重新分配内存空间。
自定义拷贝赋值操作符与自定义拷贝构造函数的不同之处在于,定义拷贝构造函数是创建一个新的对象,而自定义拷贝赋值操作符是更新一个已有的对象。所以,在自定义拷贝赋值操作符中,需要释放掉已占有的资源,之后在分配新的资源,将名字拷贝到新的资源中。在自定义拷贝赋值操作符中也要检查是否是“自赋值( self-assignment )”。“自赋值”指的是拷贝复制操作符的两端的操作数是同一个数,即自己给自己赋值。这样的话,在释放了之前的资源后,就没办法赋新值了,所以必须保证拷贝赋值操作不是“自赋值”。person& operator=(const person& that) { if( this != &that )//是否是“自赋值” { delete []m_name;//释放之前的资源 m_name = new that[strlen(that.m_name)+1]; strcpy(m_name, that.m_name); m_age = that.m_age; } }
-
Python二元数学操作符都有与之对应的增强赋值操作符 B. Python数值运算操作符需要引用第三方库math C. ...
2020-12-29 01:49:36Python二元数学操作符都有与之对应的增强赋值操作符 B. Python数值运算操作符需要引用第三方库math C. Python数值运算操作符更多相关问题【判断题】信号量作为一种资源对其操作不当,可造成系统的死锁。()【多选题】... -
cpp——类——赋值操作符函数
2017-02-13 15:49:40赋值操作符函数只是赋值操作符重载的一个特殊版本,其形参列表为[const] classname &,当实例对象赋值操作时调用 辅助类 class CAnimal { public: CAnimal() : mGroup(0) { cout ()" ; } CAnimal(int group)... -
深入C++中构造函数、拷贝构造函数、赋值操作符、析构函数的调用过程总结
2020-09-05 06:43:28本篇文章是对C++中构造函数、拷贝构造函数、赋值操作符、析构函数的调用过程进行了总结与分析,需要的朋友参考下 -
c++中赋值操作符的重载
2017-08-08 12:43:17直接抛问题,两个同类型的对象可以相互赋值?class cls { public: int a; char c; cls() : a(10), c('g') {} cls(int a, char c) : a(a), c(c) {}};int main(void) { cls c1(6, 's'); cls c2; c2 = c1; -
c++ 赋值操作符、条件操作符、逗号操作符
2022-03-14 10:04:16//赋值操作符 操作符 说明 = 简单赋值操作符,如a=b += 加法赋值操作符,如a+=b -= 减法赋值操作符,如a-=b *= 乘法赋值操作符,如a*=b /= 除法赋值操作符,如a/=b %= 取... -
【JavaScript的赋值操作符】
2017-07-19 01:43:42【JavaScript的赋值操作符】 赋值操作符:(=、+=、-=、*=、/=、%=) 使用: 赋值运算符并不是数学中的等于,而是把等号右边的数赋值给等号左边的变量,例如:a = 5; 赋值操作符中... -
赋值操作符
2014-06-24 23:27:37默认的赋值操作符用于处理同类对象之间的赋值,不要与复制构造函数混淆.如果是创建新的对象,则调用复制(拷贝)构造函数,如果是修改已有对象的值,则调用赋值操作符 MyClass aClass; MyClass bClass=aClass; // 调用... -
C++中的赋值操作符重载
2020-02-09 18:07:37文章目录1 C++中的赋值操作符重载1.1 赋值操作符重载 1 C++中的赋值操作符重载 1.1 赋值操作符重载 关于赋值操作符: 编译器为每个类默认重载了赋值操作符。 默认的赋值操作符仅完成浅拷贝。 当需要进行深拷贝时... -
QT重载赋值操作符=
2017-08-06 12:21:002019独角兽企业重金招聘Python工程师标准>>> ... -
C++中复制构造函数与重载赋值操作符总结
2019-08-29 16:03:14这篇文章将对C++中复制构造函数和重载赋值操作符进行总结,包括以下内容: 复制构造函数和重载赋值操作符的定义; 复制构造函数和重载赋值操作符的调用时机; 复制构造函数和重载赋值操作符的实现要点; 复制... -
赋值操作符重载
2018-02-08 18:07:43什么时候需要重载赋值操作符?编译器是否提供默认的赋值操作? 编译器为每个类默认重载了赋值操作符。 默认的赋值操作符仅完成浅拷贝。 当需要进行深拷贝时必须重载赋值操作符。 赋值操作符与拷贝构造函数有相同的... -
拷贝构造函数和赋值操作符
2014-07-19 19:29:34假设有一个如下的MyClass类: class MyClass { public: //构造函数 //拷贝构造函数 MyClass(const MyClass& that) : int_data_(that.int_data_), dbl_data_(that.dbl_data_), ... //赋值操作符 M -
C++复制构造函数与重载赋值操作符
2015-07-30 16:21:48因此如果对象中含有动态分配的内存,就需要我们自己重写复制构造函数和重载赋值操作符来实现“deep copy”,确保数据的完整性和安全性。 例如:类内成员变量需要动态开辟堆内存,如果实行浅拷贝,也就是把对象里... -
派生类赋值操作符
2018-07-09 14:01:52赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。 // Base :: operator = ( const Base & ) not invoked automatically Derived & ... -
[python]增强的赋值操作符
2018-06-03 20:49:02增强的赋值操作符 增强的赋值语句 等价的赋值语句 spam += 1 spam = spam + 1 spam -= 1 spam = spam - 1 spam *= 1 spam = spam * 1 spam /= 1 spam = spam / 1 spam %= 1 spa...