接口
回顾一下 Human 和 Student:
class Human {
public:
virtual ~Human() {};
virtual void talk() = 0;
};
class Student final : public Human {
public:
Student(const std::string& name) : name_(name) {}
void talk() override {
std::cout << fmt::format("Student {} talk.\n", name_);
}
private:
std::string name_;
};
定义了纯虚函数的类是虚基类,虚基类的作用是当成接口给其他用户使用,其他用户只关心该类提供了什么功能,而不用关心实现的细节,甚至不用关心实际类型。Human 定义了纯虚函数 talk(),它的函数体是由子类 Student 去实现的。
注意,析构函数不能是纯虚函数。这是因为子类(Student)在析构的时候会调用父类(Human)的析构函数,来销毁父类子对象。如果父类的析构函数是纯虚函数(换句话说,没有实现;再换句话说,没有函数体),那么父类就没有办法销毁了。
Dynamic_cast
虽然 Student 和 Teacher 都继承自 Human,但是它们可能各自有独特的能力。比如,Student 可以学,Teacher 可以教:
class Student final : public Human {
public:
void learn() {
std::cout << fmt::format("Student {} learn.\n", name_);
}
};
class Teacher final : public Human {
public:
void teach() {
std::cout << fmt::format("Teacher {} teach.\n", name_);
}
};
现在我们有一个函数,根据输入对象的实际类型,来调用它独特的函数:
void do_something(Human* ph) {
if (auto ps = dynamic_cast<Student*>(ph); ps) {
ps->learn();
}
else if (auto pt = dynamic_cast<Teacher*>(ph); pt) {
pt->teach();
}
}
Dynamic_cast() 函数用来做类型转换,如果输入对象的实际类型不是我们希望的类型,则转换失败,得到空指针;如果输入对象的实际类型是我们希望的类型,则转换成功。
统一初始化
C++11 之前对象初始化都是小括号,会带一些问题:
class Human {};
Human h();
我们定义了 Human 类型,接着希望定义一个 Human 类型的对象 h。但是编译器会报错,因为编译器把对象定义当成了函数声明,函数名是h,参数是空,返回值是 Human 类型。
C++11 引入了大括号初始化,帮助编译器做出正确的选择:
class Human {};
Human h{};
如果我们想使用字符串 “abc” 来初始化一个 std::string 对象,下面 3 种写法是等价的:
std::string s1("abc");
std::string s2 = "abc";
std::string s3{ "abc" };
注意,我们一般使用大括号初始化,可以方便地区分变量和函数。
C++11引入了初始化列表 std::initializer_list,帮助我们更方便地完成初始化操作,它的底层实现是数组。我们可以使用初始化列表来替换上面的 “abc”:
std::initializer_list<char> il = { 'a', 'b', 'c' };
std::string s4{ il };
我们也可以把临时的初始化列表对象当成参数:
std::string s4{ { 'a', 'b', 'c' } };
甚至可以省略外面的大括号:
std::string s5{ 'a', 'b', 'c' };
大括号即可给普通对象初始化,也可以给容器初始化,所以大括号初始化也叫做统一初始化。
注意,下面两个初始化是不一样的:
std::string s6(3, 'a');
std::string s7{ 3, 'a' };
右值语义
左值可以放在等号左边,右值可以放在等号右边:
- 左值:可以获取地址。
- 将亡值:右值引用。
- 纯右值:字面量,临时变量。
引用分为左值引用和右值引用。左值引用只能绑定到左值,右值引用只能绑定到右值。但是,常量左值引用可以绑定到右值。C++提供了 std::move() 函数,来将左值转换成右值引用。左值被转成后就不应该再使用了,因为它的资源可能已经移动走了。
右值的用处:
- 提高效率。大多数情况下,移动的效率比复制高。复制是将对象管理的资源做了一份拷贝(深拷贝),而移动则是将资源转交给了其他对象(浅拷贝)。
- 移动语义。有些类型无法将管理的资源进行拷贝,比如套接字、数据库连接。传递这种类型只能使用移动,不能使用复制。
自动类型推导
Auto 可以根据初始化的输入来推导变量的类型,先来看输入是左值的情况:
int n = 1024;
auto x1 = n;
auto& x2 = n;
auto&& x3 = n;
有 3 种形式:
- x1 的类型就是 n 的类型,与 n 的 CV 限定符无关,与 n 是左值还是右值无关。
- x2 的类型是左值引用,只能绑定到左值,并且与 n 的 CV 限定符有关。
- x3 是万能引用,类型取决于 n 是左值还是右值。如果 n 是左值,则 x3 是左值引用;如果 n 是右值,则 x3 是右值引用。由于 n 是左值,x3 是左值引用。
再来看输入是右值的情况:
auto x1 = 1024;
const auto& x2 = 1024;
auto&& x3 = 1024;
同样有 3 种形式:
- x1 的类型就是 1024 的类型。
- x2 的类型是常量左值引用,可以绑定到右值。注意,不能去掉 const,因为非常量左值引用只能绑定到左值。
- x3 是万能引用,由于 1024 是右值,x3 是右值引用。
字面量
Auto 可以搭配字面量使用:
auto x1 = 0;
auto x2 = 0u;
auto x3 = 0l;
auto x4 = 0ul;
auto x5 = 0.0f;
auto x6 = 0.0;
auto x7 = "abc";
还可以表示时间:
#include <chrono>
using namespace std::literals;
auto t1 = 1s;
auto t2 = 1ms;
auto t3 = 1us;
auto t4 = 1ns;
|