类
-
具体类
-
允许
- 把具体类型的对象置于栈、静态分配的内存或者其他对象
- 直接引用对象(而非仅仅通过指针或引用)
- 创建对象后立即进行完整的初始化(比如使用构造函数)
- 拷贝对象
-
-
抽象类
- 含有纯虚函数的类,称为抽象类
- 将使用者与类的实现细节完全隔离开
- 因为我们对抽象类型的表现形式一无所知(甚至对它的大小也不了解),所以必须从自由存储为对象分配空间,然后通过引用或指针访问对象。
-
类层次中的类
初始化器列表构造函数
用于定义初始化器列表构造函数的 std::initializer_list
是一种标准库类型,编译器可以辨识它:当我们使用 {}
列表时,如 {1,2,3,4}
,编译器会创建一个 initializer_list
类型的对象并将其提供给程序。因此,我们可以书写:
Vector v1={1,2,3,4,5}; // v1 包含 5 个元素
Vector v2={1.23,3.45,6.7,8}; // v2 包含 4 个元素
Vector 的初始化器列表构造函数可以定义成如下的形式:
Vector::Vector(std::initializer_list<double> lst) // 用一个列表初始化
:elem{new double[lst.size()]}, sz{lst.size()}
{
copy(lst.begin(), lst.end(), elem); // 从 lst 复制内容到 elem 中
}
抽象类
含有纯虚函数的类,称为抽象类。其发挥着接口的作用。抽象类的定义与使用:
#include <iostream>
class Container {
public:
virtual double& operator[](int) = 0; // 纯虚函数
virtual int size() const = 0; // 常量成员函数(见3.2.1.1节)
virtual ~Container() {} // 析构函数(见3.2.1.2节)
};
void use(Container& c) {
const int sz = c.size();
for (int i = 0; i != sz; ++i) {
std::cout << c[i] << ' ';
}
}
说明:
-
关键字
virtual
的意思是“可能随后在其派生类中重新定义”- 这种用关键字
virtual
声明的函数称为虚函数(virtual function)
- 这种用关键字
-
=0
说明该函数是纯虚函数,意味着 Container 的派生类必须定义这个函数
如果一个类负责为其他一些类提供接口,那么我们把前者称为多态类型(polymorphic type)
抽象类的使用
class Vector_container : public Container { // Vector_container实现了 Container
Vector v;
public:
Vector_container(int s) : v(s) {} // 含有s个元素的Vector
~Vector_container() {} // 隐式调用 v 的析构
double& operator[](int i) { return v[i]; }
int size() const { return v.size(); }
};
灵活性背后的不足,只能通过引用或指针操作抽象类的对象。
虚函数
编译器将虚函数的名字转换成函数指针表中对应的索引值,这张表就是所谓的虚函数表(virtual function table)或简称为 vtbl。每个含有虚函数的类都有它自己的 vtbI 用于辨识虚函数,其工作机理如下图所示:
这种虚调用机制的效率非常接近“普通函数调用”机制(相差不超过 25%),而它的空间开销包括两部分:
- 如果类包含虚函数,则该类的每个对象需要一个额外的指针;
- 另外每个这样的类需要一个 vtbl。
类层次
类层次提供了两种便利:
- 接口继承(Interface inheritance):派生类对象可以用在任何需要基类对象的地方。
- 实现继承(Implementation inheritance):基类负责提供可以简化派生类实现的函数或数据。
具体类,尤其是表现形式不复杂的类,其行为非常类似于内置类型:我们将其定义为局部变量,通过它们的名字访问它们,随意拷贝它们,等等。
类层次中的类则有所区别:我们倾向于通过 new
在自由存储中为其分配空间,然后通过指针或引用访问它们。(推荐使用智能指针,而不是”裸“的 new
)
#include <iostream>
class Shape {
public:
virtual ~Shape() {}; // 虚析构函数,确保派生类对象的析构函数被正确调用
virtual void draw() = 0; // 纯虚函数
};
class Circle : public Shape {
public:
Circle() {
std::cout << "Circle constructor" << std::endl;
};
void draw() override {
std::cout << "Drawing a circle" << std::endl;
};
~Circle() {
std::cout << "Circle destructor" << std::endl;
};
};
class Smiley : public Circle {
public:
Smiley() {
std::cout << "Smiley constructor" << std::endl;
};
void draw() override {
std::cout << "Drawing a smiley" << std::endl;
};
~Smiley() {
std::cout << "Smiley destructor" << std::endl;
};
};
void shapeDraw(Shape* shape) {
shape->draw();
}
void circleDraw(Circle* circle) {
circle->draw();
}
int main() {
Circle* circle = new Circle();
shapeDraw(circle);
circleDraw(circle);
delete circle;
std::cout << "--------------------------------" << std::endl;
Smiley* smiley = new Smiley();
shapeDraw(smiley);
circleDraw(smiley);
delete smiley;
}
输出结果
Circle constructor
Drawing a circle
Drawing a circle
Circle destructor
--------------------------------
Circle constructor
Smiley constructor
Drawing a smiley
Drawing a smiley
Smiley destructor
Circle destructor
只要祖先类的方法有
virtual
,后面多少代都会默认方法是重写,实在不确定,可以使用override
修饰方法。
拷贝和移动
默认情况下,我们可以拷贝对象,不论用户自定义类型的对象还是内置类型的对象都是如此。拷贝的默认含义是逐成员的复制,即依次复制每个成员。
#include <iostream>
class Complex {
private:
double real; // 复数的实部
double imag; // 复数的虚部
public:
// 默认构造函数
Complex(double r = 0.0, double i = 0.0) : real(r), imag(i) {}
// 获取实部
double getReal() const {
return real;
}
// 获取虚部
double getImag() const {
return imag;
}
// 显示复数
void display() const {
std::cout << "(" << real << " + " << imag << "i)" << std::endl;
}
// 加法运算符重载
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
// 减法运算符重载
Complex operator-(const Complex& other) const {
return Complex(real - other.real, imag - other.imag);
}
// 乘法运算符重载
Complex operator*(const Complex& other) const {
return Complex(real * other.real - imag * other.imag,
real * other.imag + imag * other.real);
}
};
void test(Complex c) {
Complex c2{ c }; // 拷贝初始化
Complex c3;
c3 = c2; // 拷贝赋值
}
int main() {
Complex c1(2.0, 3.0); // 创建复数 2 + 3i
test(c1);
}
拷贝容器
当一个类作为资源句柄(resource handle) 时,换句话说,当这个类负责通过指针访问一个对象时,采用默认的逐成员复制方式通常意味着错误。
类对象的拷贝操作可以通过两个成员来定义:拷贝构造函数(copy constructor)与拷贝赋值运算符(copy assignment)
class Vector {
private:
double* elem; // elem指向含有sz个double的数组
int sz;
public:
Vector(int s) // 构造函数:建立不变式,请求资源
: elem(new double[s]), sz(s){}
// 析构函数:释放资源
~Vector() {
delete[] elem;
}
// 拷贝构造函数
Vector(const Vector& a)
: elem(new double[a.sz]), sz(a.sz)
{
for (int i = 0; i != sz; ++i)
elem[i] = a.elem[i];
}
// 拷贝赋值运算符
Vector& operator=(const Vector& a) {
if (this == &a) return *this;
double* p = new double[a.sz];
for (int i = 0; i != a.sz; ++i)
p[i] = a.elem[i];
delete[] elem;
elem = p;
sz = a.sz;
return *this;
}
// 下标运算符重载
double& operator[](int i) {
return elem[i];
}
// 常量下标运算符重载
const double& operator[](int i) const {
return elem[i];
}
// 返回向量的大小
int size() const {
return sz;
}
};
移动容器
#include <stdexcept>
#include <iostream>
class Vector {
private:
double* elem; // elem指向含有sz个double的数组
int sz;
public:
Vector(int s) // 构造函数:建立不变式,请求资源
: elem(new double[s]), sz(s) {
std::cout << "Vector created" << std::endl;
}
// 析构函数:释放资源
~Vector() {
std::cout << "Vector destroyed" << std::endl;
delete[] elem;
}
// 拷贝构造函数
Vector(const Vector& a)
: elem(new double[a.sz]), sz(a.sz)
{
std::cout << "Vector copy created" << std::endl;
for (int i = 0; i != sz; ++i)
elem[i] = a.elem[i];
}
// 拷贝赋值运算符
Vector& operator=(const Vector& a) {
std::cout << "Vector copy assigned" << std::endl;
if (this == &a) return *this;
double* p = new double[a.sz];
for (int i = 0; i != a.sz; ++i)
p[i] = a.elem[i];
delete[] elem;
elem = p;
sz = a.sz;
return *this;
}
// 移动构造函数
Vector(Vector&& a) noexcept {
std::cout << "Vector move created" << std::endl;
elem = a.elem;
sz = a.sz;
a.elem = nullptr;
a.sz = 0;
}
// 移动赋值运算符
Vector& operator=(Vector&& a) noexcept {
std::cout << "Vector move assigned" << std::endl;
if (this == &a) return *this;
delete[] elem;
elem = a.elem;
sz = a.sz;
a.elem = nullptr;
a.sz = 0;
return *this;
}
// 下标运算符重载
double& operator[](int i) {
return elem[i];
}
// 常量下标运算符重载
const double& operator[](int i) const {
return elem[i];
}
// 返回向量的大小
int size() const {
return sz;
}
};
Vector operator+ (const Vector& a, const Vector& b) {
if (a.size() != b.size())
throw std::invalid_argument("vector sizes differ");
Vector result(a.size());
for (int i = 0; i != a.size(); ++i)
result[i] = a[i] + b[i];
return result;
}
int main() {
Vector v1(3);
v1[0] = 1;
v1[1] = 2;
v1[2] = 3;
Vector v2(3);
v2[0] = 4;
v2[1] = 5;
v2[2] = 6;
Vector v3(3);
v3[0] = 7;
v3[1] = 8;
v3[2] = 9;
Vector v4 = v3; // 调用拷贝构造函数(只有第一次声明时才会调用)
v4 = v3; // 调用拷贝赋值运算符
std::cout << "----------------" << std::endl;
// 先 v1 + v2 得到 v12_tmp(右值),利用 v12_tmp + v3 得到 v123_tmp(右值),最后调用移动赋值运算符
v4 = v1 + v2 + v3;
std::cout << "----------------" << std::endl;
// v5 会作为参数传进函数,直接在那里构造,所以只有一个 create,并且没有移动或拷贝
// 详见 NRVO
Vector v5 = v1 + v2;
std::cout << "----------------" << std::endl;
Vector v6 = std::move(v5); // 调用移动构造函数
std::cout << "----------------" << std::endl;
}
运行结果
Vector created
Vector created
Vector created
Vector copy created
Vector copy assigned
----------------
Vector created
Vector created
Vector move assigned
Vector destroyed
Vector destroyed
----------------
Vector created
----------------
Vector move created
----------------
Vector destroyed
Vector destroyed
Vector destroyed
Vector destroyed
Vector destroyed
Vector destroyed
需要注意的是:移动后的源对象,应可以执行析构操作
关于 RVO 和 NRVO
返回值优化(Return Value Optimization,RVO) :如果函数返回一个无名的临时对象,该对象将被编译器移动或拷贝到目标中,在此时,编译器可以优化代码以减少对象的构造,即省略拷贝或移动。
T f() {
return T();
}
f(); // 只有一次对T的默认构造函数的调用
命名返回值优化(Named Return Value Optimization,NRVO) :如果函数按值返回一个类的类型,并且返回语句的表达式是一个具有自动存储期限的非易失性对象的名称(即不是一个函数参数),那么可以省略优化编译器所要进行的拷贝/移动。如果是这样的话,返回值就会直接构建在函数的返回值所要移动或拷贝的存储器(即被拷贝省略优化掉的拷贝或移动的存储器)中。
Data process(int i) {
Data data;
data.mem_var = i;
return data;
}
int main() {
Data data;
data = process(5);
}
抑制操作
对于层次中的类来说,使用默认的拷贝或移动操作常常意味着风险:因为只给出一个基类的指针,我们无法了解派生类有什么样的成员,当然也就不知道该如何操作它们。因此,最好的做法是删除掉默认的拷贝和移动操作(使用 = delete
可抑制操作)。
class Shape {
public:
virtual ~Shape() {}; // 虚析构函数,确保派生类对象的析构函数被正确调用
virtual void draw() = 0; // 纯虚函数
Shape() {}; // 构造函数,构造函数不能是虚的。如果不写这个后面的子类会提示没有默认构造器
Shape(const Shape&) = delete; // 禁止拷贝构造
Shape& operator=(const Shape&) = delete; // 禁止拷贝赋值
Shape(Shape&&) = delete; // 禁止移动构造
Shape& operator=(Shape&&) = delete; // 禁止移动赋值
};
如果使用者在类中显式地声明了析构函数,则移动操作将不会隐式地生成。
模板
参数化类型
// 声明
template<typename T>
class Vector {
private:
T* elem; // elem指向含有sz个T类型元素的数组
int sz;
public:
Vector(int s);
~Vector() { delete[] elem; } // 析构函数:释放资源
// ....拷贝和移动操作...
T& operator[](int i);
const T& operator[](int i) const;
int size() const { return sz; }
};
// 定义
template<typename T>
Vector<T>::Vector(int s) {
if (s < 0) throw Negative_size{};
elem = new T[s];
sz = s; // 这里应该是sz而不是z
}
template<typename T>
const T& Vector<T>::operator[](int i) const {
if (i < 0 || size() <= i)
throw out_of_range{"Vector::operator[]"};
return elem[i];
}
模板是一种编译时的机制,因此与“手工编写的代码”相比,并不会产生任何额外的运行时开销。
函数模板
template<typename Container, typename Value>
Value sum(const Container& c, Value v) {
for (auto x : c)
v += x;
return v;
}
函数对象
函数对象:像调用函数一样使用的函数对象(重写 ()
操作符)
template<typename T>
class Less_than {
const T val; // 待比较的值
public:
Less_than(const T& v) : val(v) {}
bool operator()(const T& x) const { return x < val; } // 调用运算符
};
Less_than<int> lti{42}; // lti(i)将使用<比较i和42(i<42)
Less_than<string> lts{"Backus"}; // lts(s)将使用<比较s和"Backus"(s<"Backus")
void fct(int n, const string& s) {
bool b1 = lti(n); // 如果n<42则为真
bool b2 = Its(s); // 如果s<"Backus"则为真
}
函数对象经常以实参形式出现:
// C 是容器,P 是谓词(predicate)
template<typename C, typename P>
int count(const C& c, P pred) {
int cnt = 0;
for (const auto& x : c)
if (pred(x))
++cnt;
return cnt;
}
用于指明通用算法关键操作含义的函数对象(如 Less_than
之于 count()
)被称为策略对象(policy object)。
要达到这种效果,还可以使用 lambda 表达式
可变参数模板
可变参数模板(variadic templat):定义模板时可以令其接受任意数量任意类型的实参
template<typename T>
void g(T x) {
std::cout << x << " ";
}
template<typename T, typename... Tail>
void f(T head, Tail... tail) {
g(head); // 对head做某些操作
f(tail...); // 再次处理tail
}
void f() {
// 不执行任何操作
}
实现可变参数模板的关键是:当你传给它多个参数时,谨记把第一个参数和其他参数区分对待。此处,我们首先处理第一个参数(head),然后使用剩余参数(tail)递归地调用 f()
。省略号 ...
表示列表的“剩余部分”。最终,tail 将变为空,我们需要另外一个独立的函数来处理它。
调用 f()
的形式如下所示:
int main() {
std::cout << "first: ";
f(1, 2.2, "hello");
std::cout << "\nsecond: ";
f(0.2, 'c', "yuck!", 0, 1, 2);
std::cout << "\n";
}
别名
为类型或模板引人一个同义词
using size_t = unsigned int;
参数化的类型经常为与其模板实参关联的类型提供别名
template<typename T>
class Vector {
public:
using value_type = T;
// ...
};
事实上,每个标准库容器都提供了 value_type
作为其值类型的名字,这样以下代码就能在任何一个服从这种规范的容器上工作了。例如:
// 定义一个类型别名模板,用于获取容器的元素类型
template<typename C>
using Element_type = typename C::value_type;
// 定义一个模板函数,接受一个容器的引用,并使用该容器的元素类型
template<typename Container>
void algo(Container& c) {
// 创建一个Vector,其元素类型为Container的元素类型
Vector<Element_type<Container>> vec; // 保存结果
// ...使用vec...
}
通过绑定某些或全部模板实参,就能使用别名机制定义新的模板:
// 定义一个模板类Map,接受两个类型参数Key和Value
template<typename Key, typename Value>
class Map {
// Map类的成员和方法定义
};
// 定义一个别名模板String_map,它绑定了Map的第一个模板参数为string
template<typename Value>
using String_map = Map<std::string, Value>;
// 使用String_map创建一个Map实例,其键类型为string,值类型为int
String_map<int> m; // m是一个Map<string, int>
测试