Keep Calm and Carry On

CPP Common Sense

CPP 的一些基础知识

1. 左值和右值

左值:表示一个占据了内存中某个地址的对象;
一开始 C 定义左值为“可以出现在赋值操作左边的值”,但是引入关键字 const 后,这种定义就不够精细,不是所有的左值都可以被赋值的。
右值:使用排除法,一个表达式如果不是左值就是右值,也就是说右值是一个不表示内存中某个可识别地址的对象的表达式。 右值是表达式的临时结果,没有可以识别的内存的地址,赋值给左值没有任何意义,因为找不到一个右值的内存地址。

左值和右值转换:计算一般都需要右值作为参数,比如加法操作符 + 需要两个右值参数,因此出现了左值到右值的隐式转换(当然,数据会从内存读到寄存器,在用CPU处理)。右值当然不能转换为左值,这违反了左值表示一个在某个内存地址的对象的本质,右值可以产生左值,比如显式赋值给左值。
左值引用和右值引用:解引用操作符 * 需要一个右值参数,返回一个左值;而取地址操作符 & 需要一个左值参数,返回一个右值。& 还被用来定义一个引用,因此不能将一个右值直接赋值给左值引用。

2. explicit 关键字

在CPP中,可以由单个实参来调用的构造函数定义了从形参类型到该类类型的一个隐式类型转换。explicit 关键字用来防止由构造函数定义的隐式类型转换。举个例子来说:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class student {
public:
student(std::string _name = "") : name(_name) { }
bool hasSameName(const student &);
private:
std::string name;
};

bool student::hasSameName(const student &otherStudent) {
return this->name == otherStudent.name;
}

int main() {
std::string name = "david";
student stu1;
// 这里发生了一个隐式类型转化
// hasSameName() 本来需要一个student对象作为参数,但是编译器用传递的string对象构造一个student对象
// 使得该函数可以被调用
// 这个生成的临时的student对象在函数调用完成后被析构
stu1.hasSameName(name);
}

为了防止这种隐式类型转化的发生,当类的构造函数只有要给形参时,用关键字 explicit 来修饰。explicit 关键字只能用在类内部构造函数的声明上,而不能用来类外部的实现上。现在加上 explicit ,可以看到编译器的报错信息如下:

1
2
3
4
5
6
7
class student {
public:
explicit student(std::string _name = "") : name(_name) { }
bool hasSameName(const student &);
private:
std::string name;
};

3. STL 迭代器

STL 定义了5种迭代器,分别是:

  1. InputIterator: 从容器读取元素,只能一次读入一个元素并向前移动,只支持一次算法,也就是同一个输入迭代器不能两遍遍历一个序列;
  2. OutputIterator: 向容器写入容器,只能一次写入一个元素并向前移动,只支持一遍算法,也就是同一个输出迭代器无法两遍遍历一个序列;
  3. ForwardIterator: 正向迭代器,具有输入、输出迭代器的功能,并且可以保留在容器种的位置;
  4. Bidirectional:双向迭代器,具有正向迭代器、逆向迭代器的功能,支持多遍算法;
  5. RandomAccessIterator:随机访问迭代器
    前3种迭代器支持 operator++,第4种迭代器还额外支持 operator–,而随机访问迭代器则支持所有指针算术能力,包括 p+n,p-n, p[n], p1-p2, p1<p2。一般来说一个算法可以接受 ForwardIterator 就可以工作了,那么就不必定义为接受一个 RandomAccessIterator ,虽然一个随机访问迭代器也一定是一个正向迭代器,但是这会影响效率。因此STL在算法的设计上很多函数都针对不同类型的迭代器有不同的版本,一般是先写一个分发函数,再根据不同的迭代器选择不同的函数。

4. const 修饰指针

  1. const 位于 * 左侧,const 修饰指针指向的内容;
  2. const 位于 * 右侧,const 修饰指针本身,指针本身是常量;

5. 构造函数的初始化列表

之所以建议初始化列表的顺序要和成员变量在类中的声明顺序一致,这是因为初始化的顺序是按照声明顺序的,如果不一致可能出现不希望的请况。编译器会修改这个构造函数,安插一些新的代码到函数体代码之前,用这些安插的代码实现初始化,比如调用其他的构造函数…

1
2
3
4
5
6
7
8
class A {
pirvate:
int a;
int b;
public:
A(int val) : b(val), a(b) { }
...
};

这么写可能我们是希望将b初始化为val,而b初始为b,那么两个都是val。事实上,由于a(b)比b(val)更早执行,最终的结果是a未定义,b是val。

6. 空class就不占用空间吗?

1
2
3
4
5
6
7
8
class A { };
class B : public virtual A { };
class C : public virtual A { };
class D : public B, public C { };

cout << sizeof(a) << ' ' << sizeof(b) << ' ' << sizeof(c) << ' ' << sizeof(d) << endl;

// 1 8 8 16

a:1;编译器插入了一个char,这是为了使得两个不同的object能有唯一的内存地址;
b:8;virtual base class的额外负担,来自A的一个char,alignment的要求(64位8字节,32位4字节)
d:16;继承了两个

7. member function调用

C++支持三种member function:static、nonstatic、virtual;

7.1 nonstatic function

一个member function会被转化成nonmember function:

  1. 改写函数签名,插入this指针,用来指向调用这个函数的对象。这里有个很有意思的问题,那就是this指针是用来访问对象数据成员的,如果函数内部并没有对象数据成员被访问,那么事实上就不需要这个this指针。也就是会出现这种请况。因为没有数据被访问,this指针就没有用。如果有数据被访问,那么this指针使nullptr的情况下会导致访问一个读nullptr内存地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class T {
public:
void testing() {
cout << "member function called with nullptr" << endl;
}
private:
int val;
};
int main() {
T* tp = new T();
tp->testing(); // testing(*tp);
tp = nullptr;
tp->testing(); // testing(nullptr);
}

7.2 virtual function

转换为访问用vptr指向的vtable的某个条目,当然也会有this指针。

7.3 static function

  1. 没有this指针
  2. 因此不能直接存取class nonstatic members
  3. 不能被声明为const、volatile或者virtual
  4. 不需要经过class object才能被调用——虽然大多数时候都是使用对象来调用:两种调用方式
1
2
3
4
5
6
7
8
9
10
11
class Static{
static int count;
public:
static int getCount() { return count; }
};

int Static::count = 3;

Static s;
cout << s.getCount();
cout << Static::getCount();