C++ 炼气期之算术运算符

1. 前言

编写程序时,数据确定后,就需要为数据提供相应的处理逻辑(方案或算法)。所谓逻辑有 2 种存在形态:

  • 抽象形态:存在于意识形态,强调思考过程,与具体的编程语言无关。
  • 具体形态:通过代码来实现。需要使用表达式描述完整的计算过程。

表达式2 个部分组成:

  • 数据。也可称为操作数。

  • 运算符

    运算符是计算机语言提供的能对数据进行基本运算操作的功能体。开发者在实现自己的逻辑运算时,需要组合这些运算符来描述自己的逻辑运算过程。

Tip: 可以把C++运算符看成一种特殊语法格式的函数,或把C++中的函数当成一种特殊的运算符。

在使用运算符时,需要遵守下面的 2个基本原则:

  • 运算符对操作的数据有内置的类型要求。如数学运算符要求操作数是数字类型。
  • 如果运算符需要多个操作数时,则要求数据类型必须相同。如果出现类型不一致时,编译器会试着把不同类型的数据转换成同类型的数据后再进行运算。开发者也可以显示进行强制类型转换。

2. 运算符种类

C++中的运算符非常多,如下是几类常用的运算符:

  • 算术运算符。
  • 逻辑、关系运算符。
  • 赋值运算符。
  • 递增、递减运算符。
  • 成员访问运算符。
  • 条件运算符。
  • 位运算符。
  • sizeof 运算符。
  • 逗号运算符。

使用运算符前,需要理解如下几个概念:

  • 运算符的优先级: 不同类别中的运算符的优先级是不相同的。当在一个表达式中出现多个运算符时,则需要根据运算符的优先级进行先后运算。

  • 运算符的操作数: 作用于一个操作数的运算符为一元运算符,作用于两个操作数的运算符为二元运算符C++中还有一个可作用于三个操作数的条件运算符

  • 结合性: 当复杂表达式中的多个运算符的优先级相同时,则要根据运算符的结合性进行运算。如 100/4*8这个表达式,/*的优先级是相同,因乘、除都是具有从左到右的结合性。所以先计算100/4=25再计算25*8

    Tip: 只有当两个运算符作用于同一个操作数时,优先级和结合性才有意义。

C++中的基础运算符较多,且因C++是弱类型语言,每一种运算符在使用过程中都存在很多细节问题。算术运算符又是运算符中的基础运算符。

本文试图通过讲解清楚算术运算符,让阅读者了解使用C++运算符时应该注意的事项。

3. 算术运算符

3.1 功能描述

算术运算符用来对数字型数据进行数学语义上的。此类中有 5个运算符:

  • +:对 2数字类型的数据进行数学语义上的加法运算。
  • -:对 2数字类型的数据进行数学语义上的减法运算。
  • *:对 2数字类型的数据进行数学语义上的乘法运算。
  • /:对 2数字类型的数据进行数学语义上的除法运算。
  • %:取或取操作运算符。运算结果是两个操作数相除后的余数部分,不能用于浮点数据类型。

算术运算符是二元运算符。使用时,需要提供 2 个操作数。

3.2 运算符重载问题

C++可以重载运算符,所谓重载运算符,指同一个运算符可以根据使用时的上下文信息,表现出不同的运算能力。如-运算符, 当作为二元运算符时,用来对操作数进行相减操作。

int num1=30;
int num2=20;
//此处的 - 运算符表现出减法运算能力
int res=num1-num2;
cout<<res<<endl;
//输出结果: 10

当作为一元运算符时,则是取的意思。如下代码:

int num=-10;
int num01=-num;
cout<<num01<<endl; 
//输出结果为 10,负负为正 

同理,+运算符也存在重载。

运算符重载是C++中的一个特色。

对于有符号数据类型而言,如果在字面常量前面没有显示提供正、负符号,则默认为 +(正)符号。

3.3 两数相除的问题

/运算符作用于 2 个整型数字时,会得到舍弃小数点后的整数部分数值,或称为两数相除的,意味着会丢失精度。

如下代码:

int num1=7;
int num2=3;
int res=num1/num2;
cout<<res<<endl; 
//输出结果:2,丢失精度

如果要保留两个数字相除的精度,则应该以浮点数据类型的身份进行相除。

double num1=7;
double num2=3;
double res=num1/num2;
cout<<res<<endl; 
//输出结果:2.33333

%运算符作用于 2 个整型类型的数据时,运算结果是 2 个数字相除之后的余数部分。如下代码:

int num1=5;
int num2=3;
int res=num1 % num2;
cout<<res<<endl;
//输出结果:2 。

%用于浮点数据类型相除时,会出现编译错误。也就是 %只能用于整型数据的运算,不能用于浮点数据类型。

1.png

3.4 关 于/%运算符的问题

  • 2 个操作数据都是数时。
int num1=21;
int num2=8;
int res=num1 / num2;
cout<<" / 运算:"<<res<<endl;
res=num1 % num2;
cout<<" % 运算:"<<res<<endl;

/%动算符的输出结果都是数。

/ 运算:2
% 运算:5
  • 2 个操作数都为数时。
int num1=-21;
int num2=-8;
int res=num1 / num2;
cout<<" / 运算:"<<res<<endl;
res=num1 % num2;
cout<<" % 运算:"<<res<<endl;

输出结果,一个是正数,一个是负数。

 / 运算:2
 % 运算:-5
  • 2 个操作数中被除数为负,除数为正时。
int num1=-21;
int num2=8;
int res=num1 / num2;
cout<<" / 运算:"<<res<<endl;
res=num1 % num2;
cout<<" % 运算:"<<res<<endl;

输出结果都是负数。

/ 运算:-2
% 运算:-5
  • 2 个操作数中被除数为正,除数为负时。
int num1=21;
int num2=-8;
int res=num1 / num2;
cout<<" / 运算:"<<res<<endl;
res=num1 % num2;
cout<<" % 运算:"<<res<<endl;

输出结果为一负一正。

/ 运算:-2
% 运算:5

结论

  • 2 个数字使用 %运算符进行相除操作时,运算结果的正负号与 num1操作数(被除数)的正负号保持一致。
  • /运算符运算结果的正负号和数学上的语义一致。两个操作数都为正或为负时则正正得正负负得正。两个操作数为一正一负时:则正负得负

3.5 数据溢出问题

在使用算术运算符时,有可能出现数据溢出现象。如下代码:

short num=32767;
short num01=num+1;
cout<<num01<<endl;

输出结果:

数字:-32768

无符号short(16位)的类型数据的最大值是 32767,在此数字上加一,num01的值理论是上 32768。但实际结果是 -32768。因为 32768已经超过short范围,编译器会重新计算出一个新的结果(并不是预期值)。这种现象叫数据溢出

对于无符号 short,可以认为其有 2 部分,一部分为负数,一部分为正数。当正数溢出后,会进入负数部分。

2.png

如下代码,因溢出,超过了负数区域最小值,会溢出到正数区域。

short num1=-32768;
short num2=num1-1;
cout<<num2;
//输出结果:32767

数据溢出发生在当把数据类型范围大的数据存储到数据类型小的类型变量中时。

  • double 数据存储到 int 类型变量中。
  • int 类型的数据存储到 short类型变量中。
  • long long int 类型的数据存储到 int 类型变量中时。
  • ……

数学运算符也可以用于指针类型运算,因指针变量其数据本质就是数字数据。但指针变量不能用于乘法和除法,加、减的语义是指针的向前后后移动,乘法、除法没有语义价值。

3.6 类型转换

根据运算符的基本使用原则,要求所有操作数的类型必须相同。

有时,在一个表达式中,即使存在多个操作数的类型不一致,也能正常工作。那是因为,编译器会把不同的数据类型转换成一致,然后再进行运算。

由编译器完成的类型转换,称为自动(隐式)类型转换:

  • 整型提升C++boolcharunsigned charsigned charshort值转换为 int。这些转换被称为整型提升。
  • 浮点提升:整型类型自动向浮点类型转换,如 intdouble转换。这种转换是不会存在数据丢失问题,但会产生空间浪费。
  • 向下缩窄: 当目标类型小于原类型时,如doubleint转换,int类型向short转换时,这种转换是可以的,但会发生数据丢失的情况。可能会得不到预期结果。

碗里的水倒到缸里,不会丢失水。

缸里面的水倒到碗里,如果缸里面的水很少,不够或者刚够一碗水,不会发生水丢失。但是,这里会有潜在丢失问题,因为生活常识告诉我们,缸里面的水往往是要超过一个碗所能盛下的容量。

所以,向下缩窄存在潜在的数据丢失风险。

如下代码,其中发生了 2 次自动类型转换,有数据丢失的潜在风险。

double num1=7;
int num2=3;
int res=num1/num2;
cout<<res<<endl; 
//输出结果: 2
  • 浮点提升num2中的数据会被转换成double数据类型,让右边的表达式符合同类型原则。此时,右边表达式运算后的结果类型为 double。这一步不会发生数据丢失问题。
  • 向下缩窄: 左边的res变量类型为int ,编译器会把右边的double类型结果转换成 int。如果数值大于int类型范围时,则会出现丢失精度问题。

如下代码,则不会发生数据丢失问题:

double num1=7;
int num2=3;
double res=num1/num2;
cout<<res<<endl; 
//输出结果:2.33333

如下的代码,也会发生自动类型转换。

int num1=20;
char num2='A';
int res=num1+num2;
cout<<res<<endl;
//输出结果: 85
  • char类型会转换成 int类型。
  • 字符保存在计算机上时,需要对其进行数字编码,字符转换成 int的数字是底层的编码数字。

如下代码,也会发生自动类型。

int num1=20;
bool num2=true;
int res=num1+num2;
cout<<res<<endl;
  • C++中,bool数据类型本质上就是int类型。
  • true会转换为 1false会转换为0

3.7 {}赋值语法

C++在进行自动类型转换时,如果目标类型小于原类型时,也是能够转换的,这种现象叫缩窄缩窄会存在潜存数据安全问题。C++11提供了{}赋值语法,会对超过范围的缩窄进行编译提示。如下代码。

  • 44555 数字已经超过 char 范围,向下缩窄不被允许。
char c1= {44555};
  • X是一个变量,在运行时,x有可能被修改,并让其值大于 char数字范围,向下缩窄不被允许。
int x=66;
char c4={x};

3.8 强制类型转换

C++允许开发者显式地进行类型转换。语法格式有 2 种:

  • (目标类型名)变量。
  • 目标类型名(变量)。

强制类型转换不会修改变量本身,而是创建一个新的值。用于表达式中进行计算。

double num1=23.6;
//C++强制类型转换语法
int num2=double(num1);
cout<<num2<<endl;
//C 强制类型转换语法
num2=(double)num1;
cout<<num2<<endl;

C++还提供了 4 个类型转换运算符,使得转换过程更规范。这里只做简要介绍,有兴趣者可以深入了解一下。

  • dynamic_cast。在类层次结构中进行向上转换。
  • const_cast。用于执行只有一种用途的类型转换,即改变值为 constvolatile
  • static_cast。只有当类型之间可以隐式转换时才能转换。
  • reinterpret_cast。用于一些有很大潜在危险的类型转换。

3.9 auto 语法

auto关键字在C++的作用是自动类型推导。在声明变量时,可以使用 auto关键字,不指定变量的类型说明。编译器会根据变量中所存储的数据的类型自动推导出数据类型。

// num 是浮点数据类型
auto num=5.3;
//num1 是整型数据类型
auto num1=4;

PythonJS就是一种动态语言,表现在数据类型可以底层编译器自动识别。

虽然C++auto语法,但C++归属于弱类型语言,在数据类型识别上,一半依赖于开发者的语法约束,一半依赖编译器的自动识别。

4. 总结

C++语言的开放性,数据类型的自我适应性非常灵活。在一个表达式,当出现类型不同的情况时,编译器会试图进行各种类型上的转换,让表达式符合类型相同的运算原则。

宽松的好处是速度快,但也会带来潜在的风险,开发者应该尽可能在语法上对数据类型进行约束,不要过于依赖编译器。养成良好的编码习惯。