操作符-越平常越要谨慎
目录
一、赋值操作符有先后
赋值运算符修改对象的值,所有内建赋值运算符都返回*this,而大多数用户定义重载亦返回*this,从而能以与内建版本相同的方式使用用户定义运算符。虽然,用户定义运算符重载中,能以任何类型为返回类型(包括 void),但不建议。
1、
直接赋值 a = b
加法赋值 a += b
减法赋值 a -= b
2、
乘法赋值 a *= b
除法赋值 a /= b
取模赋值 a %= b
3、
逐位与赋值 a &= b
逐位或赋值 a |= b
逐位异或赋值 a ^= b
4、
逐位左移赋值 a <<= b
逐位右移赋值 a >>= b
使用赋值操作符,最主要的就是需要明确值类别。在c/c++中,比较普遍地有两种划分,一种是对象特性来区分的对象类型,划分为基础类型(整型、浮点型等),复合类型(指针、引用等);另一种是从语法规则及表达式运用上划分是(左值、右值、亡值等)。
1.1 对象类型
布尔类型、整型、枚举、vector容器等具有类型对象属性,通常明确这些类型,就限制了对这些实体所容许的操作。
C++ 类型的分类:
基础类型:
void 类型(std::is_void);
std::nullptr_t 类型(C++11 起) (std::is_null_pointer);
算术类型(std::is_arithmetic):
浮点类型(float、double、long double 及其 cv 限定版本)(std::is_floating_point);
整数类型(包括 cv 限定版本,参阅 std::is_integral):
bool 类型;
字符类型:
窄字符类型:
通常字符类型(char、signed char、unsigned char)
char8_t 类型(C++20 起)
宽字符类型(char16_t、char32_t、 (C++11 起)wchar_t);
有符号整数类型(short int、int、long int、long long int (C++11 起));
无符号整数类型(unsigned short int、unsigned int、unsigned long int、unsigned long long int (C++11 起));
复合类型(std::is_compound):
引用类型(std::is_reference):
左值引用类型(std::is_lvalue_reference);
到对象的左值引用类型;
到函数的左值引用类型;
右值引用类型( std::is_rvalue_reference);
到对象的右值引用类型; (C++11 起)
到函数的右值引用类型;
指针类型(std::is_pointer):
指向对象的指针(对象指针)类型;
指向函数的指针(函数指针)类型;
指向成员的指针(成员指针)类型(std::is_member_pointer):
指向数据成员的指针类型(std::is_member_object_pointer);
指向成员函数的指针类型(std::is_member_function_pointer);
数组类型(std::is_array);
函数类型(std::is_function);
枚举类型(std::is_enum);
类类型:
非联合体类型(std::is_class);
联合体类型(std::is_union)。
对于除引用和函数以外的每个类型,类型系统还支持该类型的三个附加 cv 限定版本(const 、 volatile 及 const volatile)。
根据类型的各项性质,将它们分组到不同的类别之中:
- 对象类型是非函数类型、非引用类型且非 void 类型的(可有 cv 限定的)类型(参阅 std::is_object);
- 标量类型是(可有 cv 限定的)算术、指针、成员指针、枚举和 std::nullptr_t (C++11 起) 类型(参阅 std::is_scalar);
- 平凡类型(参阅 std::is_trivial)、 POD 类型(参阅 std::is_pod)、字面类型(参阅 std::is_literal_type)和其他类别,列于类型特征库中,或作为具名类型要求。
类类型命名及定义,主要是通过以下方式命名类型的:
◦类声明 class struct
◦联合体声明 union
◦枚举声明 enum
◦typedef 声明
◦类型别名声明
另外会根据编译规则,将上述类型划分为静态类型、动态类型。
静态类型(非static关键词),对程序进行编译时分析所得到的表达式的类型被称为表达式的静态类型。程序执行时静态类型不会更改。
动态类型,若某个泛左值表达式指代某个多态对象,则其最终派生对象的类型被称为其动态类型。
在 C++ 程序中对没有名字的类型常常需要被涉指;为此而设的语法被称为类型标识。命名类型 T 的类型标识的语法与省略了标识符的对 T 类型的变量或函数的声明语法完全一致,但声明语法中的 声明说明符序列 被限制为 类型说明符序列 ,另外仅当类型标识出现于非模板类型别名声明的右侧时才可以定义新类型。
1.2 值类型
每个 C++ 表达式(带有操作数的操作符、字面量、变量名等)可按照两种独立的特性加以辨别:类型和值类别 (value category)。每个表达式都具有某种非引用类型,且每个表达式只属于三种基本值类别中的一种:纯右值 (prvalue)、亡值 (xvalue)、左值 (lvalue)。
- 泛左值 (glvalue)(“泛化 (generalized)”的左值)是其求值确定一个对象、位域或函数的个体的表达式;
- 纯右值 (prvalue)(“纯 (pure)”的右值)是求值符合下列之一的表达式:
- 计算某个运算符的操作数的值或为 void 表达式(这种纯右值没有结果对象)
- 初始化某个对象或位域(称这种纯右值有一个结果对象)。除 decltype 外,所有类和数组的纯右值都有结果对象,即使它被舍弃也是如此。结果对象可以是变量,由 new 表达式创建的对象,由临时量实质化创建的临时对象,或者前述三类对象的成员;
- 亡值 (xvalue)(“将亡 (expiring)”的值)是代表其资源能够被重新使用的对象或位域的泛左值;
- 左值 (lvalue)(如此称呼的历史原因是,左值可以出现于赋值表达式的左边)是非亡值的泛左值;
- 右值 (rvalue)(如此称呼的历史原因是,右值可以出现于赋值表达式的右边)是纯右值或者亡值。
【1】泛左值,泛左值表达式包括左值、亡值。
【2】左值:
- 变量、函数、模板形参对象 (C++20 起)或数据成员的名字,不论其类型,例如 std::cin 或 std::endl。即使变量的类型是右值引用,由其名字构成的表达式仍是左值表达式;
- 返回类型为左值引用的函数调用或重载运算符表达式,例如 std::getline(std::cin, str)、std::cout << 1、str1 = str2 或 ++it;
- a = b,a += b,a %= b,以及所有其他内建的赋值及复合赋值表达式;
- ++a 和 --a,内建的前置自增与前置自减表达式;
- *p,内建的间接寻址表达式;
- a[n] 和 n[a],内建的下标表达式,当 a[n] 中的一个操作数为数组左值时 (C++11 起);
- a.m,对象成员表达式,除了 m 为成员枚举项或非静态成员函数,或者 a 为右值而 m 为对象类型的非静态数据成员的情况;
- p->m,内建的指针成员表达式,除了 m 为成员枚举项或非静态成员函数的情况;
- a.*mp,对象的成员指针表达式,其中 a 是左值且 mp 是数据成员指针;
- p->*mp,内建的指针的成员指针表达式,其中 mp 是数据成员指针;
- a, b,内建的逗号表达式,其中 b 是左值;
- a ? b : c,对某些 b 和 c 的三元条件表达式(例如,当它们都是同类型左值时,但细节见其定义);
- 字符串字面量,例如 "Hello, world!";
- 转换为左值引用类型的转型表达式,例如 static_cast<int&>(x);
- 返回类型是到函数的右值引用的函数调用表达式或重载的运算符表达式;
- 转换为函数的右值引用类型的转型表达式,如 static_cast<void (&&)(int)>(x)。 (C++11 起)
【3】右值,右值表达式包括纯右值、亡值。
【4】纯右值::
- (除了字符串字面量之外的)字面量,例如 42、true 或 nullptr;
- 返回类型是非引用的函数调用或重载运算符表达式,例如 str.substr(1, 2)、str1 + str2 或 it++;
- a++ 和 a--,内建的后置自增与后置自减表达式;
- a + b、a % b、a & b、a << b,以及其他所有内建的算术表达式;
- a && b、a || b、!a,内建的逻辑表达式;
- a < b、a == b、a >= b 以及其他所有内建的比较表达式;
- &a,内建的取地址表达式;
- a.m,对象成员表达式,其中 m 是成员枚举项或非静态成员函数[2],或其中 a 为右值且 m 为非引用类型的非静态数据成员 (C++11 前);
- p->m,内建的指针成员表达式,其中 m 为成员枚举项或非静态成员函数[2];
- a.*mp,对象的成员指针表达式,其中 mp 是成员函数指针[2],或其中 a 为右值且 mp 为数据成员指针 (C++11 前);
- p->*mp,内建的指针的成员指针表达式,其中 mp 是成员函数指针[2];
- a, b,内建的逗号表达式,其中 b 是右值;
- a ? b : c,对某些 b 和 c 的三元条件表达式(细节见其定义);
- 转换为非引用类型的转型表达式,例如 static_cast<double>(x)、std::string{} 或 (int)42;
- this 指针;
- 枚举项;
- 非类型模板形参,除非其类型为类或 (C++20 起)左值引用类型;
- lambda 表达式,例如 [](int x){ return x * x; }; (C++11 起)
- requires 表达式,例如 requires (T i) { typename T::type; };
- 概念的特化,例如 std::equality_comparable<int>。 (C++20 起)
【5】亡值
- 返回类型为对象的右值引用的函数调用或重载运算符表达式,例如 std::move(x);
- a[n],内建的下标表达式,其操作数之一是数组右值;
- a.m,对象成员表达式,其中 a 是右值且 m 是非引用类型的非静态数据成员;
- a.*mp,对象的成员指针表达式,其中 a 为右值且 mp 为数据成员指针;
- a ? b : c,对某些 b 和 c 的三元条件表达式(细节见其定义);
- 转换为对象的右值引用类型的转型表达式,例如 static_cast<char&&>(x);
- 在临时量实质化后,任何指代该临时对象的表达式。 (C++17 起)
1.3 赋值操作符要求及建议
【1】赋值表达式左操作数必须为左值类型,例如
int a = 0; b = 1;
++a = 10;
// a++ = 10; //error
//42 = a; //error
//a + b = 10; //error
...
【2】赋值左操作数必须是可修改值类型,例如
const int b = 0;
int c[2] ={0};
b = 10; //error,b虽然是int型,但为常量
c = 10; //error,数组名是不可修改的左值
c[0] = 10;//OK,下标返回左值
*c = 11; //OK,解引用操作符返回左值
【3】赋值操作符将其右操作数的值赋给左操作数,在左、右操作数的类型不同时,该操作实现的类型转换可能会修改被赋的值。此时,需要特别注意转换后数据精度丢失问题。
int ival = 0;
ival = 10; //Ok
ival = 'a'; //Ok,宽度上升
ival = 3.1415;//waring,宽度下降
【4】被赋值的每个操作数都具有相同的通用类型,C++语言允许将这多个赋值操作写在一个表达式中,当表达式含有多个赋值操作符时,从右向左结合,但通常不建议这样做。
int ival, jval;
double dval;
ival = jval = 0; // ok:类型相同,良构
jval = 0; ival = 0; // ok:建议
dval = ival = 0; // ok:不建议
ival = 0; dval = 0.0;// ok:建议
多个赋值操作中,各对象必须具有相同的数据类型,或者具有可转换为同一类型的数据类型,例如int to double。
【5】 赋值操作具有低优先级,在把赋值操作用作长表达式中,注意优先级结合
int get_value()
{
static int index = 0;
return ++index;
}
//
int i = 0;
while (( i= get_value()) != 10) {
// do something ...
}
【6】在条件表达式中,谨慎关系操作符变为了赋值操作符
int i = 0;
//dosomethins
if(i==10){} //本意
//->写成了
if(i=10){} //编译器不告警,编译可通过
//建议
if(10==i){} //良构
【7】复合赋值操作符,
使用复合赋值操作a operator= b时,左操作数只计算了一次;而使用相似的长表达式a = a operator b时,该操作数则计算了两次,第一次作为右操作数,而第二次则用做左操作数。
//a operator= b
+= -= *= /= %= // 算术操作符
<<= >>= &= ^= |= // 位操作符
//a = a operator b
+ - * / % // 算术操作符
<< >> & ^ | // 位操作符
除非考虑可能的性能价值,在很多(可能是大部分的)上下文环境里这个差别不是本质性的。
二、自增和自减操作符藏效率
2.1 前置和后置
自增(++)和自减(--)操作符为对象加 1 或减 1 操作提供了方便简短的实现方式。它们有前置和后置两种使用形式。前自增操作,该操作使其操作数加 1,操作结果是修改后的值。同理,前自减操作使其操作数减 1。这两种操作符相对的后置形式同样对其操作数加 1(或减 1),但操作后产生操作数原来的、未修改的值作为表达式的结果。
int i = 0, j;
j = ++i; // j = 1, i = 1: 前缀
j = i++; // j = 1, i = 2: 后缀
因为前置操作需要做的工作更少,只需加 1 后返回加 1 后的结果即可。而后置操作符则必须先保存操作数原来的值,以便返回未加 1 之前的值作为操作的结果。
Obj Obj::operator++() //前增
{
++ival;
return *this;
}
Obj Obj::operator++(int rhs)//后增
{
Obj ret(*this);
++*this;
return ret;
}
对于 int 型对象和指针,编译器可优化掉这项额外工作。但是对于更多的复杂迭代器类型,这种额外工作可能会花费更大的代价。
2.2 优选前置
看看下面这个简要案例,自定义类类型Obj,为其提供了两种前缀和后缀的自增自减操作符,通过大规模遍历测试其效率,看看编译器对这些简要自定义类类型是否如内置基础类型一样做了优化:
#include <iostream>
#include <chrono>
class Obj
{
private:
/* data */
int ival;
double dval;
public:
Obj(int ival_) : ival(ival_) {};
operator unsigned long(){return static_cast<unsigned long>(ival);};
Obj operator++();//一元操作符++,前缀式操作符
Obj operator--();//一元操作符--,前缀式操作符
Obj operator++(int rhs);//一元操作符++,后缀式操作符
Obj operator--(int rhs);//一元操作符--,后缀式操作符
};
Obj Obj::operator++()
{
++ival;
return *this;
}
Obj Obj::operator--()
{
--ival;
return *this;
}
Obj Obj::operator++(int rhs)
{
Obj ret(*this);
++*this;
return ret;
}
Obj Obj::operator--(int rhs)
{
Obj ret(*this);
--*this;
return ret;
}
const unsigned long sizel = 10000;
typedef Obj IndexType; //自定义类类型测试
// typedef size_t IndexType; //内置基础类型测试
void optest()
{
auto start = std::chrono::system_clock::now();
for (IndexType row = 0; row < sizel; row++)
for (IndexType i = 0; i < sizel; i++)
{
;
}
auto end = std::chrono::system_clock::now();
std::chrono::duration<double,std::milli> diff = end-start;
std::cout << "operator++(int) diff.count() = " << diff.count() << "ms\n";
//
start = std::chrono::system_clock::now();
for (IndexType row = 0; row < sizel; ++row)
for (IndexType i = 0; i < sizel; ++i)
{
;
}
end = std::chrono::system_clock::now();
diff = end-start;
std::cout << "operator++() diff.count() = " << diff.count() << "ms\n";
}
开启基础类类型测试
// typedef Obj IndexType;
typedef size_t IndexType;
编译g++ main.cpp -o test.exe -std=c++11,运行如下
开启自定义类类型测试
typedef Obj IndexType;
// typedef size_t IndexType;
编译g++ main.cpp -o test.exe -std=c++11,运行如下
因此,对于不是特殊语境的需要,简要还是养成使用前置操作这个好习惯,就不必操心性能差异的问题。
2.3 按语境设计前置应用
但是需要根据业务场景去设计使用好前置自增及自减,有些语境前自增和后自增其效果是一样的:
for (size_t i = 0; i < 10; ++i)
{
std::cout << "i =" << i << "\n";
}
//两者是一致的效果
for (size_t i = 0; i < 10; i++)
{
std::cout << "i =" << i << "\n";
}
而有些语境前自增和后自增其效果是不一样的:
int id = 0;
while (id<10)
{
std::cout << "id++ =" << id++ << "\n";
}
//两者是有差别的
id = 0;
while (id<10)
{
std::cout << "++id =" << ++id << "\n";
}
但是如果我们养成前缀使用习惯,完成可以按前缀思维方式设计及实现业务场景。
三、关系及逻辑操作符-串接使用要谨慎
关系操作符和逻辑操作符都是使用算术或指针类型的操作数,并返回bool 类型的值。
逻辑: !a a&&b a||b
关系: a==b a!=b a<b a>b a<=b a>=b a<=>b
3.1 逻辑操作符
逻辑操作符将其操作数视为条件表达式:首先对操作数求值;若结果为 0,则条件为假(false),否则为真(true)。仅当逻辑与(&&)操作符的两个操作数都为 true,其结果才得 true 。对于逻辑或(||)操作符,只要两个操作数之一为 true,它的值就为 true。
逻辑与和逻辑或操作符总是先计算其左操作数,然后再计算其右操作数。只有在仅靠左操作数的值无法确定该逻辑表达式的结果时,才会求解其右操作数。我们常常称这种求值策略为“短路求值”。另外逻辑与(&&)和逻辑或(||)操作符可以多个操作符串接使用,构成复杂表达式。
if(con1 && con2 && con3) {}; //如果con1=true,则才需要执行
if(con1 || con2 || con3) {}; //如果con1=true,则后面可不执行
由于短路求值的存在,串接使用逻辑与和逻辑或操作符时,通常需要将内置基础类型的常用操作数放置前面,一般优先是常用操作数,若使用频次等同,则优先简单类型,才是复杂类型。
int a = 0;
std::string str = "null";
std::vector vec_a,vec_b;
if( a && "null"!=str && vec_a.size()==vec_b.size() ){};
逻辑非操作符(!)将其操作数视为条件表达式,产生与其操作数值相反的条件值。如果其操作数为非零值,则做 ! 操作后的结果为 false。逻辑非操作符(!)和其操作数一起的表达式作为逻辑与和逻辑或操作符操作数使用。逻辑非操作符(!)优先级高于逻辑与(&&)和逻辑或(||)操作符。
int a = 0;
std::vector vec_a,vec_b;
if( !a && !vec_a.empty() ){};
3.2 关系操作符
与逻辑与(&&)和逻辑或(||)操作符可以串接使用不同,关系操作符不应该串接使用。关系操作符(<、<=、>、<=)具有左结合特性。事实上,由于关系操作符返回 bool 类型的结果,因此很少使用其左结合特性。如果把多个关系操作符串接起来使用,结果往往出乎预料。
int i1=0;
int i2=0;
int i3=1;
if (i1 < i2 < i3) { //true
std::cout << "dosomething!\n"; //成功打印输出
}
显然不是想要表示的本意,因此最好不要串接使用:
if (i1 < i2 && i2 < i3) {
std::cout << "dosomething!\n";
}
bool 类型可转换为任何算术类型——bool 值false 用 0 表示,而 true 则为 1。
int a = 0;
if(a){}; //不建议
if(0 != a){}; //建议
bool b = false; //
if(b){}; //建议
if(true == b){};//
if(1 == b){}; //不建议
如果操作数是bool类型,建议直接使用操作数本身来判定,如果是具有可转换为 bool 类型的数据类,建议采用该数据类型匹配的字面常量值来判断。
四、算术操作符-请不要吝啬括号
算术操作符需要考虑优先级与结合性,乘法(*)、除法(/)操作符比加法(+)、减法(-)操作符优先级高。在使用算术操作符创建负责表达式时,请不要吝啬您的括号。
算术操作符: +a -a a+b a-b a*b a/b a%b ~a a&b a|b a^b a<<b a>>b
4.1 运算符优先级及结合性
各个运算单元的优先级及结合性:
优先级 运算元 描述 结合性
1 :: 作用域解析 从左到右
2 a++ a-- 后缀自增与自减 ...
type() type{} 函数风格转型 ...
a() 函数调用 ...
a[] 下标 ...
. -> 成员访问 ...
3 ++a --a 前缀自增与自减 从右到左
+a -a 一元加与减 ...
! ~ 逻辑非和逐位非 ...
(type) C 风格转型 ...
*a 间接(解引用) ...
&a 取址 ...
sizeof 取大小 ...
co_await await 表达式 (C++20) ...
new new[] 动态内存分配 ...
delete delete[] 动态内存分配 ...
4 .* ->* 成员指针 从左到右
5 a*b a/b a%b 乘法、除法与余数 从左到右
6 a+b a-b 加法与减法 从左到右
7 << >> 逐位左移与右移 从左到右
8 <=> 三路比较運算元(C++20 起) 从左到右
9 < <= 分别为 < 与 ≤ 的关系運算元 从左到右
> >= 分别为 > 与 ≥ 的关系運算元 从左到右
10 == != 分别为 = 与 ≠ 的相等性運算元 从左到右
11 a&b 逐位与 从左到右
12 ^ 逐位异或(互斥或) 从左到右
13 | 逐位或(可兼或) 从左到右
14 && 逻辑与 从左到右
15 || 逻辑或 从左到右
16 a?b:c 三元条件 从右到左
throw throw 運算元 ...
co_yield yield 表达式 (C++20) ...
= 直接赋值(C++ 类默认提供) ...
+= -= 以和及差复合赋值 ...
*= /= %= 以积、商及余数复合赋值 ...
<<= >>= 以逐位左移及右移复合赋值 ...
&= ^= |= 以逐位与、异或及或复合赋值 ...
17 , 逗号 从左到右
4.2 算术操作符
算术操作符 +、-、* 和 / 具有直观的含义:加法、减法、乘法和除法。对两个整数做除法,结果仍为整数,如果它的商包含小数部分,则小数部分会被截除,并且是先截除再返回的。如果需要小数部分,需要做显式类型转换,先转换右操作数(括号的魅力,改变逻辑执行次序),则在调用除法操作符时,会先将整数升级浮点宽度来处理。
int ival1 = 10/3;
std::cout << "ival1 =" << ival1 << "\n"; //3
double dval1 = 10/3;
std::cout << "dval1 =" << dval1 << "\n"; //3
double dval2 = (double)(10/3);
std::cout << "dval2 =" << dval2 << "\n"; //3
double dval3 = ((double)10)/3;
std::cout << "dval3 =" << dval3 << "\n"; //3.33333
double dval4 = 10/((double)3);
std::cout << "dval4 =" << dval4 << "\n"; //3.33333
操作符 % 称为“求余(remainder)”或“求模(modulus)”操作符,用于计算左操作数除以右操作数的余数。该操作符的操作数只能为整型,包括bool、char、short 、int 和 long 类型,以及对应的 unsigned 。
int ival2 = 100;
double dval5 = 3.14;
std::cout << "ival2%12 =" << ival2%12 << "\n"; //Ok,返回4
// std::cout << "ival2%dval5 =" << ival2%dval5 << "\n";// error: 表达式必须具有整数或未区分范围的枚举类型
如果两个操作数为正,除法(/)和求模(%)操作的结果也是正数(或零);如果两个操作数都是负数,除法操作的结果为正数(或零),而求模操作的结果则为负数(或零);如果只有一个操作数为负数,这两种操作的结果取决于机器;求模结果的符号也取决于机器,而除法操作的值则是负数(或零)。
std::cout << "12/5 =" << 12/5 << "\n"; //Ok,返回2
std::cout << "-12/-5 =" << -12/-5 << "\n"; //Ok,返回2
std::cout << "-12/5 =" << -12/5 << "\n"; //Ok,返回-2
std::cout << "12/-5 =" << 12/-5 << "\n"; //Ok,返回-2
std::cout << "12%5 =" << 12%5 << "\n"; //Ok,返回2
std::cout << "-12%-5 =" << -12%-5 << "\n"; //Ok,返回-2
std::cout << "-12%5 =" << -12%5 << "\n"; //Ok,返回-2
std::cout << "12%-5 =" << 12%-5 << "\n"; //Ok,返回2
当只有一个操作数为负数时,求模操作结果值的符号可依据分子(被除数)或分母(除数)的符号而定。如果求模的结果随分子的符号,则除出来的值向零一侧取整;如果求模与分母的符号匹配,则除出来的值向负无穷一侧取整。
根据计算机特性,注意溢出异常情况:计算出的数值超出了其类型的表示范围。例如某台机器,其 short 类型为 16 位,能表示的最大值是 32767。假设 short 类型只有 16 位,下面的复合赋值操作将会溢出:
short short_value = 32767;
short ival = 1;
short_value += ival;// this calculation overflows
4.3 位操作符
位操作符使用整型(bool、char、short、int、long等)的操作数。位操作符将其整型操作数视为二进制位的集合,为每一位提供检验和设置的功能。另外,这类操作符还可用于bitset 类型的操作数,该类型具有这里所描述的整型操作数的行为。
~a a&b a|b a^b a<<b a>>b
位操作符操纵的整数的类型可以是有符号的也可以是无符号的。如果操作数为负数,则位操作符如何处理其操作数的符号位依赖于机器。于是它们的应用可能不同:在一个应用环境中实现的程序可能无法用于另一应用环境。对于位操作符,由于系统不能确保如何处理其操作数的符号位,所以强烈建议使用 unsigned 整型操作数。PS:ostream不支持unsigned char屏幕输出,需要显式转换。
unsigned char bits1 = 0XFF;
unsigned char bits2 = 0XEF;
std::cout << "bits1 =" << (int)bits1 << "\n"; //255
bits1 = ~bits1;
std::cout << "bits1 =" << (int)bits1 << "\n"; //0
bits1 = bits1^bits2;
std::cout << "bits1 =" << (int)bits1 << "\n"; //239
bits1 = 0X10|bits2;
std::cout << "bits1 =" << (int)bits1 << "\n"; //255
bits1 = 0X10&bits2;
std::cout << "bits1 =" << (int)bits1 << "\n"; //0
<< 和 >> 操作符提供移位操作,其右操作数标志要移动的位数。这两种操作符将其左操作数的各个位向左(<<)或向右(>>)移动若干个位(移动的位数由其右操作数指定),从而产生新的值,并丢弃移出去的位。
bits1 = bits2 << 1;
std::cout << "bits1 =" << (int)bits1 << "\n"; //222
bits1 = bits2 >> 1;
std::cout << "bits1 =" << (int)bits1 << "\n"; //119
左移操作符(<<)在右边插入 0 以补充空位。对于右移操作符(>>),如果其操作数是无符号数,则从左边开始插入 0;如果操作数是有符号数,则插入符号位的副本或者 0 值,如何选择需依据具体的实现而定。
int bit_i = 0;
bit_i = ((-1)<<1);
std::cout << "bit_i =" << bit_i << "\n"; //-2
bit_i = ((-1)>>1);
std::cout << "bit_i =" << bit_i << "\n"; //-1
//
short bit_s = 0;
bit_s = ((int)(-1)<<1);
std::cout << "bit_s =" << bit_s << "\n"; //-2
bit_s = ((int)(-1)>>1);
std::cout << "bit_s =" << bit_s << "\n"; //-1
4.4 std::bitset与位操作符
c++11标准库提供了std::bitset类模板,表示一个 N 位的固定大小序列。可以用标准逻辑运算符操作bitset,并将它与字符串和整数相互转换。bitset 满足可复制构造 (CopyConstructible) 及可复制赋值 (CopyAssignable) 的要求。
//定义于头文件 <bitset>
template< std::size_t N > class bitset;
std::bitset为开发者提供静态的序列,比整型值上的低级位操作更容易使用,支持位操作符,即节省空间有提高效率。
#include <bitset>
void bitset_test()
{
// 构造函数:
std::bitset<4> b3{"0011"}; //
// 能打印出 bitset 到流:
std::cout << "b3:" << b3 << '\n';//0011
// bitset 支持逐位运算:
b3 |= 0b0100;
std::cout << "b3:" << b3 << '\n';//0111
b3 &= 0b0011;
std::cout << "b3:" << b3 << '\n';//0011
b3 ^= std::bitset<4>{0b1100};
std::cout << "b3:" << b3 << '\n';//1111
// 整个集合上的操作:
b3.reset();
std::cout << "b3:" << b3 << '\n';//0000
b3.set();
std::cout << "b3:" << b3 << '\n';//1111
b3.flip();
std::cout << "b3:" << b3 << '\n';//0000
// 单独位上的操作:
b3.set(/* position = */ 1, true);
std::cout << "b3:" << b3 << '\n';//0010
b3.set(/* position = */ 1, false);
std::cout << "b3:" << b3 << '\n';//0000
b3.flip(/* position = */ 2);
std::cout << "b3:" << b3 << '\n';//0100
b3.reset(/* position = */ 2);
std::cout << "b3:" << b3 << '\n';//0000
// 支持下标 operator[] :
b3[2] = true;
std::cout << "b3:" << b3 << '\n';//0100
// 其他操作:
std::cout << "b3.count():" << b3.count() << '\n'; //1
std::cout << "b3.size():" << b3.size() << '\n'; //4
std::cout << "b3.to_ullong():" << b3.to_ullong() << '\n';//4
std::cout << "b3.to_string():" << b3.to_string() << '\n';//0100
};
std::bitset 对象的大小不受 unsigned 数的位数限制,bitset 优于整型数据的低级直接位操作。
五、补充-源码
编译命令g++ main.cpp -o test.exe -std=c++11.
main.cpp
#include <iostream>
#include <chrono>
class Obj
{
private:
/* data */
int ival;
double dval;
public:
Obj(int ival_) : ival(ival_) {};
operator unsigned long(){return static_cast<unsigned long>(ival);};
Obj operator++();//一元操作符++,前缀式操作符
Obj operator--();//一元操作符--,前缀式操作符
Obj operator++(int rhs);//一元操作符++,后缀式操作符
Obj operator--(int rhs);//一元操作符--,后缀式操作符
};
Obj Obj::operator++()
{
++ival;
return *this;
}
Obj Obj::operator--()
{
--ival;
return *this;
}
Obj Obj::operator++(int rhs)
{
Obj ret(*this);
++*this;
return ret;
}
Obj Obj::operator--(int rhs)
{
Obj ret(*this);
--*this;
return ret;
}
const unsigned long sizel = 10000;
typedef Obj IndexType;
// typedef size_t IndexType;
void optest()
{
auto start = std::chrono::system_clock::now();
for (IndexType row = 0; row < sizel; row++)
for (IndexType i = 0; i < sizel; i++)
{
;
}
auto end = std::chrono::system_clock::now();
std::chrono::duration<double,std::milli> diff = end-start;
std::cout << "operator++(int) diff.count() = " << diff.count() << "ms\n";
//
start = std::chrono::system_clock::now();
for (IndexType row = 0; row < sizel; ++row)
for (IndexType i = 0; i < sizel; ++i)
{
;
}
end = std::chrono::system_clock::now();
diff = end-start;
std::cout << "operator++() diff.count() = " << diff.count() << "ms\n";
}
int get_value()
{
static int index = 0;
return ++index;
}
#include <bitset>
void bitset_test()
{
// 构造函数:
std::bitset<4> b3{"0011"}; //
// 能打印出 bitset 到流:
std::cout << "b3:" << b3 << '\n';//0011
// bitset 支持逐位运算:
b3 |= 0b0100;
std::cout << "b3:" << b3 << '\n';//0111
b3 &= 0b0011;
std::cout << "b3:" << b3 << '\n';//0011
b3 ^= std::bitset<4>{0b1100};
std::cout << "b3:" << b3 << '\n';//1111
// 整个集合上的操作:
b3.reset();
std::cout << "b3:" << b3 << '\n';//0000
b3.set();
std::cout << "b3:" << b3 << '\n';//1111
b3.flip();
std::cout << "b3:" << b3 << '\n';//0000
// 单独位上的操作:
b3.set(/* position = */ 1, true);
std::cout << "b3:" << b3 << '\n';//0010
b3.set(/* position = */ 1, false);
std::cout << "b3:" << b3 << '\n';//0000
b3.flip(/* position = */ 2);
std::cout << "b3:" << b3 << '\n';//0100
b3.reset(/* position = */ 2);
std::cout << "b3:" << b3 << '\n';//0000
// 支持下标 operator[] :
b3[2] = true;
std::cout << "b3:" << b3 << '\n';//0100
// 其他操作:
std::cout << "b3.count():" << b3.count() << '\n'; //1
std::cout << "b3.size():" << b3.size() << '\n'; //4
std::cout << "b3.to_ullong():" << b3.to_ullong() << '\n';//4
std::cout << "b3.to_string():" << b3.to_string() << '\n';//0100
};
int main(int argc, char* argv[])
{
int a = 0;
++a = 10;
// a++ = 10; //error
const int b = 0;
int c[2] ={0};
// b = 10; //error
// c = 10; //error
int ival = 0;
ival = 10; //Ok
ival = 'a'; //Ok
ival = 3.1415;//waring
//
int jval;
double dval;
ival = jval = 0; // ok:类型相同
jval = 0; ival = 0; // ok:建议
dval = ival = 0; //
ival = 0; dval = 0.0;// ok:建议
//
int i = 0;
while (( i= get_value()) != 10) {
// do something ...
}
//dosomethins
if(i==10){}
//->
if(i=10){} //error
//建议
if(10==i){} //良构
// optest();
// for (size_t i = 0; i < 10; ++i)
// {
// std::cout << "i =" << i << "\n";
// }
// for (size_t i = 0; i < 10; i++)
// {
// std::cout << "i =" << i << "\n";
// }
int id = 0;
while (id<10)
{
std::cout << "id++ =" << id++ << "\n";
}
id = 0;
while (id<10)
{
std::cout << "++id =" << ++id << "\n";
}
int i1=0;
int i2=0;
int i3=1;
if (i1 < i2 < i3) { //true
std::cout << "dosomething!\n"; //成功打印输出
}
//
int ival1 = 10/3;
std::cout << "ival1 =" << ival1 << "\n";
double dval1 = 10/3;
std::cout << "dval1 =" << dval1 << "\n";
double dval2 = (double)(10/3);
std::cout << "dval2 =" << dval2 << "\n";
double dval3 = ((double)10)/3;
std::cout << "dval3 =" << dval3 << "\n";
double dval4 = 10/((double)3);
std::cout << "dval4 =" << dval4 << "\n";
//
int ival2 = 100;
double dval5 = 3.14;
std::cout << "ival2%12 =" << ival2%12 << "\n"; //Ok,返回4
// std::cout << "ival2%dval5 =" << ival2%dval5 << "\n";// error: 表达式必须具有整数或未区分范围的枚举类型
std::cout << "12/5 =" << 12/5 << "\n"; //Ok,返回2
std::cout << "-12/-5 =" << -12/-5 << "\n"; //Ok,返回2
std::cout << "-12/5 =" << -12/5 << "\n"; //Ok,返回-2
std::cout << "12/-5 =" << 12/-5 << "\n"; //Ok,返回-2
std::cout << "12%5 =" << 12%5 << "\n"; //Ok,返回2
std::cout << "-12%-5 =" << -12%-5 << "\n"; //Ok,返回-2
std::cout << "-12%5 =" << -12%5 << "\n"; //Ok,返回-2
std::cout << "12%-5 =" << 12%-5 << "\n"; //Ok,返回2
//
unsigned char bits1 = 0XFF;
unsigned char bits2 = 0XEF;
std::cout << "bits1 =" << (int)bits1 << "\n"; //
bits1 = ~bits1;
std::cout << "bits1 =" << (int)bits1 << "\n"; //
bits1 = bits1^bits2;
std::cout << "bits1 =" << (int)bits1 << "\n"; //
bits1 = 0X10|bits2;
std::cout << "bits1 =" << (int)bits1 << "\n"; //
bits1 = 0X10&bits2;
std::cout << "bits1 =" << (int)bits1 << "\n"; //
bits1 = bits2 << 1;
std::cout << "bits1 =" << (int)bits1 << "\n"; //
bits1 = bits2 >> 1;
std::cout << "bits1 =" << (int)bits1 << "\n"; //
//
int bit_i = 0;
bit_i = ((-1)<<1);
std::cout << "bit_i =" << bit_i << "\n"; //
bit_i = ((-1)>>1);
std::cout << "bit_i =" << bit_i << "\n"; //
//
short bit_s = 0;
bit_s = ((int)(-1)<<1);
std::cout << "bit_s =" << bit_s << "\n"; //
bit_s = ((int)(-1)>>1);
std::cout << "bit_s =" << bit_s << "\n"; //
//
bitset_test();
return 0;
};