C++学习笔记之三

阅读 《C++Primer 第五版》第四章 表达式 所做的一些笔记。

基础

基本概念

  • 一元运算符:作用于一个对象
  • 二元运算符:作用于两个运算对象
  • 其他:三元运算符和函数调用,其中三元运算符可以做用于三个运算对象,函数调用则对运算对象没有限制
1
2
三元运算符 ?:
bool b = x > y ? true : false;

表达式运算的时候有优先级、结合律和运算顺序。

在表达式求职过程中运算对象常常被转换成不同类型,例如int转为float等,通常bool、char、short会被提升为int来做计算。

运算符重载可以用在复合类型的表达式中,像io的<<和>>,string的+等。

左值和右值

左值和右值原本是C中概念,左值位于赋值语句的左边,右值位于赋值语句的右边。然而在C++中意义就不一样了,归纳起来是:当一个对象被用作右值的时候,用的是对象的值(内容),当一个对象被用作左值的时候,用的是对象的身份(内存中的位置,即地址)

求值顺序

优先级规定了对象的组合方式但是没有说明求值顺序,有些运算符是没有规定求值顺序的,所以在使用的时候要避免表达式中出现歧义的顺序,例如:

1
2
int i = 0;
cout << i << " " << ++i << endl;

由于<<没有定义的求值顺序,所以可能输出0,1也可能输出1,1。经过测试在vs2015和g++ 4.8中结果都是1,1。不过这种未定义的行为很可能不同编译器实现的时候做法不一样,所以不要这样写。

准则:

  • 关于优先级,其中括号的优先级最高,如果你不确定你写的表达式的运算顺序,最好还是加上括号。
  • 如果表达式中改变了某个运算对象的值,那么这个表达式中的其他地方就不要再使用这个运算对象了
    例如:
    1
    2
    int i = 0;
    int ii = i++ + i++; // to bad

算术运算符

这部分粗略的看了下和java很像,这里写一些不一样的地方。

  • C++中的bool值在运算的时候会被提升为int,然而bool值只有两个值,0或1,不是0的值都会被转成1。在Java中boolean值无法参加运算。
  • 运算溢出,例如:
    1
    2
    3
    short s = 32767;  // 7FFF
    s += 1; // 8000 由于负数是补码表示的,这里符号位是负
    cout << s << endl; // -32768
  • 整数相除结果还是整数,余数会被丢弃
    1
    int i1 = 21 / 6;  // i是3
  • 取余的定义:(m / n) * n + m % n = m例如:
    1
    2
    3
    4
    5
    -21 % -8:
    (-21 / -8) * -8 + m % n = -21
    m % n = -21 + 8 * (-21 / -8);
    m % n = -21 + 8 * 2
    m % n = -5

逻辑和关系运算

这部分和Java几乎一样,有些补充的地方:

1
2
3
4
5
char *cp = "hello";
if(cp && *cp) //这里cp中都是有内容的,所以都是true的
//如果是下面这样
char *cp = nullptr;
if(cp) //这里是false,因为nullptr是0

赋值运算符

1
2
3
4
5
int i = 0, j = 0, k = 0;  // 这里是初始化而不是赋值
const int ci = i; // 同上

k = 1; // 这里是赋值
k = {1}; // C++11 中加入的初始值列表

对于类类型来说,初始化的细节取决于类本身的构造函数。
赋值运算的优先级是比较低的所以经常会有如下的戴妈:

1
2
int i;
if((i = get_value) != 42)

C++允许赋值语句作为运算条件:

1
if(i = j)

如果赋值后i的值非0,那么这里是true

递增递减运算符

这里也和Java中一样包括对i++和++i的使用还有何时使用等。

成员访问运算符

Java中的成员是用点来访问,C++中点用来访问类对象成员,箭头用于指针。

1
2
3
4
5
6
string s = "hello";
string *sp = &s;
auto n1 = sp->size();
auto n2 = (*sp).size();
auto n3 = s.size(); //三者等价
cout << "string size is " << n1 << endl;

条件运算符

条件运算的就是?:,可嵌套。

1
2
3
int i = -1;
cout << ((i > 0) ? 1 : 2) << endl;
cout << ((i > 0) ? ((i > 0) ? 1 : 2) : 2) << endl;

随着嵌套层数的增加可读性会下降,实际使用中应该注意嵌套层数。

习题4.21:

1
2
3
4
5
6
7
vector<int> vec = {1, 2, 3, 4, 5, 6, 7};
vector<int>::iterator iter = vec.begin();
while (iter != vec.end())
{
*iter % 2 ? *iter *= 2 : NULL;
iter++;
}

习题4.22:

1
2
3
int grade = 74;
auto result = grade > 90 ? "high pass" : ((grade > 75) ? "low pass" : ((grade > 60) ? "pass" : "fail"));
cout << result << endl;

位运算符

位运算中小整型(short,char, bool)会被自动提升为较大的整数类型,对象可以是带符号的也可以是无符号的。关于符号位如何处理,标准中并没有规定,书中推荐将位运算符用于无符号数。

1
2
3
4
5
unsigned char bits = 0223;

auto b1 = bits << 31;
cout << hex << b1 << endl;
cout << typeid(b1).name() << endl; // int

从这里可以看出,在对待小整型的时候编译器对其进行了提升。另外,位移操作不会改变数据本身的值,而是对值位移后生成一个copy。

1
2
3
unsigned char i = 0x1;

auto ii = ~i << 6; //在~之前i被提升为int,由8位变成了32位,然后取反得到fffffffe,然后左移6位,得到ffffff80

位运算符(&,|)不要和逻辑运算符(&&,||)混淆了。

1
cout << endl;  // <<是对位移运算符进行了重载

sizeof运算符

szieof运算符返回一个表达式或者个一个类型所占的字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sizeof(type)
sizeof expr
// 这是sizeof的两种形式,第一种用于返回类型的大小,第二种用于返回表达式结果类型的大小
string s = "";
sizeof(string) // string是类,所以有括号
sizeof s; // s是表达式,所以不需要括号
// 在使用中发现vs中可以写成 sizeof string而不需要加括号,但是g++中必须要加
// 总的来说加括号可以方便代码移植,也可以便于阅读
int i = 1;
int *ip = &i;

sizeof(*p); // 指向对象的大小
sizeof(p); // 指针大小固定为4

string s = "";
sizeof(s);
sizeof(string); // 这两个是等价的
string *sp = nullptr;
sizeof(*sp); // 并不会因为指针是空而报错,他只会测量对应类型的大小

int arr[10] = {};
sizeof(arr); // 整个数组的大小
sizeof(*arr); // *arr是数组的第一个元素

类型转换

C++中有着和Java相似的隐式转换,发生隐式转换条件:

  • 大多数表达式中比int小的会被提升为int
  • 在条件中非bool类型会被转为bool类型
  • 初始化过程中,初始值转换为变量类型;赋值语句中右侧对象1转成左侧对象的类型,例如:
    1
    2
    int i1 = 3.14;      // i1 为3
    int i2 = 3.14 + i1; // 编译器会先将3.14转为3,然后和i1相加,i2为6
  • 如果算术运算或关系运算对象有多种类型,需要转换成同一种类型
  • 函数调用也会发生类型转换(后面会讲这里就先不看了)

算数转换

1.整型提升
2.无符号类型的运算对象

  • 首先进行整型提升
  • 如果无符号类型的值大于有符号的,那么有符号的会转成无符号的
  • 如果无符号类型的值小于有符号的,那么首先要看带符号类型能不能存下无符号的那个数,如果可以,则无符号变为有符号,如果不可以,则有符号的会转成无符号的
1
2
3
4
5
6
7
8
short s1 = 1;
unsigned short s2 = 32768;
cout << s1 + s2 << endl; //32769,变为整型

int i1 = 4294967296 / 2 - 1;
unsigned int i2 = 4294967296 / 2;
cout << i1 + i2 << endl; // 由于i2大于i1所以i1被提升为无符号的数

其他隐式转换

除了基础类型的转换外还包括一些其他类型的转换:

  • 数组转换成指针

    1
    2
    int ia[10];
    int *p = ia; // 这时候数组转换成了数组指针
  • 指针的转换(nullptr可以转换成任意指针,任意非常量指针可以转成void *)

  • 指针转bool

    1
    2
    3
    char *cp = get_string();
    if(cp) // 如果指针不是0,则为真
    if(*cp) //如果不是空字符串,则为真
  • 非常量可以转为常量的指针或者引用,但是反过来不行。

显示转换

显示转换包括static_cast,dynamic_cast,const_cast。

static_cast
1
2
3
4
int i = 4;
double d = static_cast<double>(i);
void *vp = &d;
double *dp = static_cast<double *>(vp);

static_cast的一个好处就是可以把编译器做不到的指针的类型转换做到,正如上面的一个void *到double *的过程。使用的的时候一定要在确定这样转到的类型是对的才可以用,否则会引发未定义的错误。

const_cast

用于去掉const的限制。

1
2
3
4
const char *cp1 = "123";
char *cp2 = const_cast<char *>(cp1); //对比直接赋值,这样拿到的指针就不是const的了
char *cp3 = "111";
cp2 = cp3;
dynamic_cast

和static_casr转换结果类似,不同的是在遇到不安全的downcast的时候会返回一个nullptr指针。书后面才讲这里就不详细查了。

reinterpret_cast

这个强制类型转换并不像前面两个会对变量的值进行重新解释,而只是单纯的把二进制复制到所要转换的类型的内存中,当在使用的时候很可能因为不同类型对于这一块二进制的解释不同导致运行是错误

出了函数重载的时候可以使用const_cast之外,其他时间使用强制类型转换很大程度上说明程序设计上的问题。实际开发中应该尽量避免强制类型转换。

除此之外还有旧的强制类型转换:

1
2
3
int i = 4;
double d1 = double(i); // C++ style
doubel d2 = (doubel)i; // C style

这种转换会根据涉及到的类型做出和上面那些强制类型转换相同的行为,但是这种写法不能很清晰的反应转换的种类,易读和调试起来都不方便,所以不推荐使用。