C++编程:运算符

有了数据之后,就可以对数据对象进行各种计算了。在编程语言中,可以通过“运算符”来表示想要进行的计算。

1 表达式和运算符

1.1 基本概念

在程序中,一个或多个运算对象的组合叫做“表达式”(expression),我们可以把它看成用来做计算的“式子”。对一个表达式进行计算,可以得到一个结果,有时也把它叫做表达式的值。

前面讲到的字面值常量和变量,就是最简单的表达式;表达式的结果就是字面值和变量的值。而多个字面值和变量,可以通过一些符号连接组合在一起,表示进行相应的计算,这就可以得到更加复杂的表达式,比如 a + 1。像“+”这些符号就被叫做“运算符”(operator)。

C++中定义的运算符,可以是像“+”这样连接两个对象,称为“二元运算符”;也可以只作用于一个对象,称为“一元运算符”。另外,还有一个比较特殊的运算符可以作用于三个对象,那就是三元运算符了。

1.2 运算优先级和结合律

如果在一个表达式中,使用多个运算符组合了多个运算对象,就构成了更加复杂的“复合表达式”,比如 a + 1 - b。对于复合表达式,很显然我们应该分步来做计算;而计算顺序,是由所谓的“优先级”和“结合律”确定的。

简单来说,就是对不同的运算符赋予不同的“优先级”,我们会优先执行高优先级的运算、再执行低优先级的运算。如果优先级相同,就按照“结合律”来决定执行顺序。这其实跟数学的综合算式是一样的,我们会定义乘除的优先级要高于加减,平级运算从左往右,所以对于算式:

1 + 2 – 3 × 4

我们会先计算高优先级的 3×4,然后按照从左到右的结合顺序计算1+2,最后做减法。另外,如果有括号,那就要先把括起来的部分当成一个整体先做计算,然后再考虑括号外的结合顺序,这一点在C++表达式中同样适用。

2. 算术运算

最简单的运算符,就是表示算术计算的加减乘除,这一类被称为“算术运算符”。C++支持的算术运算符如下:

 

这里需要注意的是,同一个运算符,在不同的场合可能表达不同的含义。比如“-”,可以是“减号”也可以是“负号”:如果直接放在一个表达式前面,就是对表达式的结果取负数,这是一元运算符;如果连接两个表达式,就是两者结果相减,是二元运算符。

算术运算符相关规则如下:

  • 一元运算符(正负号)优先级最高;接下来是乘、除和取余;最后是加减;
  • 算术运算符满足左结合律,也就是说相同优先级的运算符,将从左到右按顺序进行组合;
  • 算术运算符可以用来处理任意算术类型的数据对象;
  • 不同类型的数据对象进行计算时,较小的整数类型会被“提升”为较大的类型,最终转换成同一类型进行计算;
  • 对于除法运算“/”,执行计算的结果跟操作数的类型有关。如果它的两个操作数(也就是被除数和除数)都是整数,那么得到的结果也只能是整数,小数部分会直接舍弃,这叫“整数除法”;当至少有一个操作数是浮点数时,结果就会是浮点数,保留小数部分;
  • 对于取余运算“%”(或者叫“取模”),两个操作数必须是整数类型;

// 除法

int a = 20, b = 6;

cout << " a / b = " << a / b << endl;

cout << " -a / b = " << -a / b << endl; // 负数向0取整

float a2 = 20;

cout << " a2 / b = " << a2 / b << endl;

// 取模

cout << " a % b = " << a % b << endl;

cout << " -a % b = " << -a % b << endl;

在这里,同样是除法运算符“/”,针对不同类型的数据对象,其实会做不同的处理。使用相同的符号、根据上下文来执行不同操作,这是C++提供的一大特色功能,叫做“运算符重载”(operator overloading)。

3. 赋值

将一个表达式的结果,传递给某个数据对象保存起来,这个过程叫做“赋值”。

3.1 赋值运算符

在C++中,用等号“=”表示一个赋值操作,这里的“=”就是赋值运算符。需要注意的是,赋值运算符的左边,必须是一个可修改的数据对象,比如假设我们已经定义了一个int类型的变量a,那么

a = 1;

这样赋值是对的,但

1 = a;

就是错误的。因为a是一个变量,可以赋值;而 1只是一个字面值常量,不能再对它赋值。

int a, b;

a = 1;

//1 = a; // 错误:表达式必须是可修改的左值

a = b + 5;

//b + 5 = a; // 错误:表达式必须是可修改的左值

const int c = 10;

//c = a + b; // 错误:表达式必须是可修改的左值

所以像变量a这样的可以赋值的运算对象,在C++中被叫做“左值”(lvalue);对应的,放在赋值语句右面的表达式就是“右值”(rvalue)。

赋值运算有以下一些规则:

  • 赋值运算的结果,就是它左侧的运算对象;结果的类型就是左侧运算对象的类型;
  • 如果赋值运算符两侧对象类型不同,就把右侧的对象转换成左侧对象的类型;
  • C++ 11新标准提供了一种新的语法:用花括号{}括起来的数值列表,可以作为赋值右侧对象。这样就可以非常方便地对一个数组赋值了;
  • 赋值运算满足右结合律。也就是说可以在一条语句中连续赋值,结合顺序是从右到左;
  • 赋值运算符优先级较低,一般都会先执行其它运算符,最后做赋值;

a = {2};

int arr[] = {1,2,3,4,5}; // 用花括号对数组赋值

a = b = 20; // 连续赋值

3.2 复合赋值运算符

实际应用中,我们经常需要把一次计算的结果,再赋值给参与运算的某一个变量。最简单的例子就是多个数求和,比如我们要计算a、b、c的和,那么可以专门定义一个变量sum,用来保存求和结果:

int sum = a; // 初始值是a

sum = sum + b; // 叠加b

sum = sum + c; // 叠加c

要注意赋值运算符“=”完全不是数学上“等于”的意思,所以上面的赋值语句sum = sum + b; 说的是“计算sum + b的结果,然后把它再赋值给sum”。

为了更加简洁,C++提供了一类特殊的赋值运算符,可以把要执行的算术运算“+”跟赋值“=”结合在一起,用一个运算符“+=”来表示;这就是“复合赋值运算符”。

复合赋值一般结合的是算术运算符或者位运算符。每种运算符都有对应的组合形式:

 

关于位运算符,我们会在稍后介绍。

这样上面的代码可以改写为:

int sum = a; // 初始值是a

sum += b; // 完全等价于 sum = sum + b;

sum += c;

3.3 递增递减运算符

C++为数据对象的“加一”“减一”操作,提供了更加简洁的表达方式,这就是递增和递减运算符(也叫“自增”“自减”运算符)。“递增”用两个加号“++”表示,表示“对象值加一,再赋值给原对象”;“递减”则用两个减号“--”表示。

++a; // a递增,相当于 a += 1;

--b; // b递减,相当于 b -= 1;

递增递减运算符各自有两种形式:“前置”和“后置”,也就是说写成“++a”和“a++”都是可以的。它们都表示“a = a + 1”,区别在于表达式返回的结果不同:

前置时,对象先加1,再将更新之后的对象值作为结果返回;

后置时,对象先将原始值作为结果返回,再加1;

这要特别注意:如果我们单独使用递增递减运算符,那前置后置效果都一样;但如果运算结果还要进一步做计算,两者就有明显不同了。

int i = 0, j;

j = ++i; // i = 1,j = 1

j = i--; // i = 0, j = 1

在实际应用中,一般都是希望用改变之后的对象值;所以为了避免混淆,我们通常会统一使用前置的写法。

4. 关系和逻辑运算

在程序中,不可缺少的一类运算就是逻辑和关系运算,因为我们往往需要定义“在某种条件发生时,执行某种操作”。判断条件是否发生,这就是一个典型的逻辑判断;得到的结果或者为“真”(true),或者为“假”。很显然,这类运算的结果应该是布尔类型。

4.1 关系运算符

最简单的一种条件,就是判断两个算术对象的大小关系,对应的运算符称为“关系运算符”。包括:大于“>”、小于“<”、等于“==”、不等于“!=”、大于等于“>=”、小于等于“<=”。

 

这里要注意区分的是,在C++语法中一个等号“=”表示的是赋值,两个等号“==”才是真正的“等于”。

1 < 2; // true

3 >= 5; // false

10 == 4 + 6; // true

(10 != 4) + 6; // 7

关系运算符的相关规则:

  • 算术运算符的优先级高于关系运算符,而如果加上括号就可以调整计算顺序;
  • 关系运算符的返回值为布尔类型,如果参与算术计算,true的值为1,false的值为0;

4.2 逻辑运算符

一个关系运算符的结果是一个布尔类型(ture或者false),就可以表示一个条件的判断;如果需要多个条件的叠加,就可以用逻辑“与或非”将这些布尔类型组合起来。这样的运算符叫做“逻辑运算符”。

  • 逻辑非(!):一元运算符,将运算对象的值取反后返回,真值反转;
  • 逻辑与(&&):二元运算符,两个运算对象都为true时结果为true,否则结果为false;
  • 逻辑或(||):二元运算符,两个运算对象只要有一个为true结果就为true,都为false则结果为false;

1 < 2 && 3 >= 5; // false

1 < 2 || 3 >= 5; // true

!(1 < 2 || 3 >= 5); // false

我们可以把逻辑运算符和关系运算符的用法、优先级和结合律总结如下(从上到下优先级递减):

 

这里需要注意的规则有:

  • 如果将一个算术类型的对象作为逻辑运算符的操作数,那么值为0表示false,非0值表示true;
  • 逻辑与和逻辑或有两个运算对象,在计算时都是先求左侧对象的值,再求右侧对象的值;如果左侧对象的值已经能决定最终结果,那么右侧就不会执行计算:这种策略叫做“短路求值”;

i = -1;

1 < 2 && ++i; // false

cout << " i = " << i << endl; // i = 0

1 < 2 || ++i; // true

cout << " i = " << i << endl; // i = 0

4.3 条件运算符

C++还从C语言继承了一个特殊的运算符,叫做“条件运算符”。它由“?”和“:”两个符号组成,需要三个运算表达式,形式如下:

条件判断表达式 ? 表达式1 : 表达式2

它的含义是:计算条件判断表达式的值,如果为true就执行表达式1,返回求值结果;如果为false则跳过表达式1,执行表达式2,返回求值结果。这也是C++中唯一的一个三元运算符。

i = 0;

cout << ((1 < 2 && ++i) ? "true" : "false") << endl;

l 条件运算符的优先级比较低,所以输出的时候需要加上括号

l 条件运算符满足右结合律

事实上,条件运算符等同于流程控制中的分支语句if...else...,只用一条语句就可以实现按条件分支处理,这就让代码更加简洁。关于分支语句,我们会在后面详细介绍。

5. 位运算符

之前介绍的所有运算符,主要都是针对算术类型的数据对象进行操作的;所有的算术类型,占用的空间都是以字节(byte,8位)作为单位来衡量的。在C++中,还有一类非常底层的运算符,可以直接操作到具体的每一位(bit)数据,这就是“位运算符”。

位运算符可以分为两大类:移位运算符,和位逻辑运算符。下面列出了所有位运算符的优先级和用法。

 

5.1移位运算符

算术类型的数据对象,都可以看做是一组“位”的集合。那么利用“移位运算符”,就可以让运算对象的所有位,整体移动指定的位数。

移位运算符有两种:左移运算符“<<”和右移运算符“>>”。这个符号我们并不陌生,之前做输入输出操作的时候用的就是它,不过那是标准IO库里定义的运算符重载版本。

下面是移位运算符的一个具体案例:

 

  • 较小的整数类型(char、short以及bool)会自动提升成int类型再做移位,得到的结果也是int类型
  • 左移运算符“<<”将操作数左移之后,在右侧补0;
  • 右移运算符“>>”将操作数右移之后,对于无符号数就在左侧补0;对于有符号数的操作则要看运行的机器环境,有可能补符号位,也有可能直接补0;
  • 由于有符号数右移结果不确定,一般只对无符号数执行位移操作;

unsigned char bits = 0xb5; // 181

cout << hex; // 以十六进制显示

cout << "0xb5 左移2位:" << (bits << 2) << endl; // 0x 0000 02d4

cout << "0xb5 左移8位:" << (bits << 8) << endl; // 0x 0000 b500

cout << "0xb5 左移31位:" << (bits << 31) << endl; // 0x 8000 0000

cout << "0xb5 右移3位:" << (bits >> 3) << endl; // 0x 0000 0016

cout << dec;

cout << (200 << 3) << endl; // 乘8操作

cout << (-100 >> 2) << endl; // 除4操作,一般右移是补符号位

5.2 位逻辑运算符

计算机存储的每一个“位”(bit)都是二进制的,有0和1两种取值,这跟布尔类型的真值表达非常类似。于是自然可以想到,两个位上的“0”或“1”都可以执行类似逻辑运算的操作。

位逻辑运算符有:按位取反“~”,位与“&”,位或“|”和位异或“^”。

  • 按位取反“~”:一元运算符,类似逻辑非。对每个位取反值,也就是把1置为0、0置为1;
  • 位与“&”:二元运算符,类似逻辑与。两个数对应位上都为1,结果对应位为1;否则结果对应位为0;
  • 位或“|”:二元运算符,类似逻辑或。两个数对应位上只要有1,结果对应位就为1;如果全为0则结果对应位为0;
  • 位异或“^”:两个数对应位相同,则结果对应位为0;不同则结果对应位为0;

下面是位逻辑运算符的一个具体案例:

 

// 位逻辑运算

cout << (~5) << endl; // ~ (0... 0000 0101) = 1... 1111 1010, -6

cout << (5 & 12) << endl; // 0101 & 1100 = 0100, 4

cout << (5 | 12) << endl; // 0101 | 1100 = 1101, 13

cout << (5 ^ 12) << endl; // 0101 & 1100 = 1001, 9

6. 类型转换

在C++中,不同类型的数据对象,是可以放在一起做计算的。这就要求必须有一个机制,能让有关联的两种类型可以互相转换。在上一章已经介绍过变量赋值时的自动类型转换,接下来我们会对类型转换做更详细的展开。

6.1 隐式类型转换

大多数情况,C++编译器可以自动对类型进行转换,不需要我们干涉,这种方式叫做“隐式类型转换”。

隐式类型转换主要发生在算术类型之间,基本思路就是将长度较小的类型转换成较大的类型,这样可以避免丢失精度。隐式类型转换不仅可以在变量赋值时发生,也可以在运算表达式中出现。例如:

short s = 15.2 + 20;

cout << " s = " << s << endl; // s = 35

cout << " 15.2 + 20 结果长度为:" << sizeof(15.2 + 20) << endl;

cout << " s 长度为:" << sizeof(s) << endl;

对于这条赋值语句,右侧是两个字面值常量相加,而且类型不同:15.2是double类型,20是int类型。当它们相加时,会将int类型的20转换为double类型,然后执行double的加法操作,得到35.2。

这个结果用来初始化变量s,由于s是short类型,所以还会把double类型的结果35.2再去掉小数部分,转换成short类型的35。所以s最终的值为35。

隐式类型转换的一般规则可以总结如下:

  • 在大多数算术运算中,较小的整数类型(如bool、char、short)都会转换成int类型。这叫做“整数提升”;(而对于wchar_t等较大的扩展字符类型,则根据需要转换成int、unsigned int、long、unsigned long、long long、unsigned long long中能容纳它的最小类型)
  • 当表达式中有整型也有浮点型时,整数值会转换成相应的浮点类型;
  • 在条件判断语句中,其它整数类型会转换成布尔类型,即0为false、非0为true;
  • 初始化变量时,初始值转换成变量的类型;
  • 在赋值语句中,右侧对象的值会转换成左侧对象的类型;

此外,要尽量避免将较大类型的值赋给较小类型的变量,这样很容易出现精度丢失或者数据溢出。

s = 32767;

cout << " s + 1 = " << s + 1 << endl;

short s2 = s + 1;

cout << " s2 = " << s2 << endl;

另外还要注意,如果希望判断一个整型变量a是否在某个范围(0, 100)内,不能直接写:0 < a < 100;

由于小于运算符“<”满足左结合律,要先计算0 < a,得到一个布尔类型的结果,再跟后面的100进行比较。此时布尔类型做整数提升,不管值是真(1)还是假(0),都会满足 < 100 的判断,因此最终结果一定是true。

要想得到正确的结果,需要将两次关系判断拆开,写成逻辑与的关系。

a = -1;

0 < a < 100; // 不论a取什么值,总是true

0 < a && a < 100; // false

6.2 强制类型转换

除去自动进行的隐式类型转换,我们也可以显式地要求编译器对数据对象的类型进行更改。这种转换叫做“强制类型转换”(cast)。

比如对于除法运算,我们知道整数除法和浮点数除法是不同的。如果希望对一组整数求一个平均数,直接相加后除以个数是无法得到想要的结果的:

// 求平均数

int total = 20, num = 6;

double avg = total / num;

cout << " avg = " << avg << endl; // avg = 3

因为两个int类型的数相除,执行的是整数除法,得到3;再转换成double类型对avg做初始化,得到是3.0。如果想要更准确的结果,就必须将int类型强制转换成double,做浮点数除法。

C++中可以使用不同的方式进行强制类型转换。

(1)C语言风格

最经典的强转方式来自C语言,格式如下:

(类型名称) 值

把要强制转成的类型,用一个小括号括起来,放到要转换的对象值前面就可以了。

(2)C++函数调用风格

这种方式跟C语言的强转类似,只不过看起来更像是调用了一个函数:

类型名称 (值)

要转成的类型名就像是一个函数,调用的时候,后面小括号里是传递给它的参数。

(3)C++强制类型转换运算符

C++还引入了4个强制类型转换运算符,这种新的转换方式比前两种传统方式要求更为严格。通常在类型转换中用到的运算符是static_cast,用法如下:

static_cast<类型名称> (值)

static_cast运算符后要跟一个尖括号,里面是要转换成的类型。

有了这些强转的方式,就可以解决之前求平均数的问题了:

// C语言风格

cout << " avg = " << (double) total / num << endl;

// C++函数风格

cout << " avg = " << double (total) / num << endl;

// C++强转运算符

cout << " avg = " << static_cast<double>(total) / num << endl;

强制类型转换会干扰正常的类型检查,带来很多风险,所以通常要尽量避免使用强制类型转换。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值