新闻  |   论坛  |   博客  |   在线研讨会
两万字总结《C++ Primer》要点(5)
机器之心 | 2021-04-08 20:42:13    阅读:180   发布文章

第十三章 拷贝控制

P440-P486

五种拷贝控制操作:

拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符、析构函数。

拷贝构造函数、移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。

拷贝赋值运算符、移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。

析构函数定义了当此类型对象销毁时做什么。

13.1 拷贝、赋值与销毁

(1)拷贝构造函数

拷贝构造函数的第一个参数必须是一个引用类型。

class Foo {

public :

Foo(); // 默认构造函数

Foo(const Foo&); // 拷贝构造函数

}

合成拷贝构造函数:

若未定义拷贝构造函数,编译器会定义一个。

拷贝初始化:

拷贝初始化,要求编译器将右运算对象拷贝到正在创建的对象中。拷贝初始化通常使用拷贝构造函数来完成。

(2)拷贝赋值运算符

重载赋值运算符:oprator=

合成拷贝赋值运算符:若一个类未定义自己的拷贝赋值运算符,编译器会为它生成一个合成拷贝赋值运算符。

(3)析构函数

析构函数:用于释放对象使用的资源,销毁对象的非static数据成员。

class Foo {

public:

~Foo(); // 析构函数,一个类只会有唯一一个析构函数。

}

在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。销毁类类型的成员需要执行成员自己的析构函数。

合成析构函数:当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。

析构函数体本身并不直接销毁成员。

(4)三五法则

P447

需要析构函数的类也需要拷贝和赋值操作

需要拷贝操作的类也需要赋值操作,反之亦然

(5)使用default=

将拷贝控制成员定义为=****ult来显式地要求编译器生活才能合成的版本。

class Sales_data {

public:

Sales_data(const Sales_data&) = default;

}

(6)阻止拷贝

在函数参数列表后面加上=delete。

=delete必须出现在函数第一次声明的时候。

析构函数不能是删除的成员

合成的拷贝控制成员可能是删除的:

如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。

13.2 拷贝控制和资源管理

(1)行为像值的类

为了提供类值的行为,对于类管理的对象,每个对象都应该拥有一份自己的拷贝。

类值拷贝赋值运算符:通常组合了析构函数和构造函数的操作。

HasPtr& HasPtr::operator=(const HasPtr &rhs)

{

auto newp = new string(*rhs.ps);

delete ps;

ps = newp;

i = rhs.i;

return *this;

}

(2)行为像指针的类

如果需要可直接管理资源,可以使用引用计数。

13.3 交换操作

swap

13.4 拷贝控制示例

P460

13.5 动态内存管理类

P464

13.6 对象移动

与任何赋值运算符一样,移动赋值运算符必须销毁左侧运算对象的旧状态。

(1)右值引用

可通过move函数开获得绑定到左值上的右值引用。

int && rr3 = std::move(rr1);

(2)移动构造函数和移动赋值运算符

移动构造函数的第一个参数是该类类型的一个右值引用。

移动赋值运算符:

StrVec &StrVec::operator=(StrVec &&rhs) noexcept

{

}

合成的移动操作:

若一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数,编译器就不会为它合成移动构造函数和移动赋值运算符。

如果一个类没有移动操作,类会使用对应的拷贝操作来代替移动操作。

移动迭代器:

移动迭代器的解引用运算符生成一个右值引用。

(3)右值引用和成员函数

::: tip

区分移动和拷贝的重载函数通常有一个版本接受一个const T&,而另一个版本接受一个T&&。

:::

右值和左值引用成员函数:

指出this的左值/右值属性的方式与定义const成员函数相同,在参数列表后放置一个引用限定符。P483

::: tip

如果一个成员函数有引用限定符,则具有相同参数列表的所有版本都必须有引用限定符。P485

:::

术语

引用限定符:被&限定的函数只能用于坐值;被&&限定的函数只能用于右值。

第十四章 重载运算与类型转换

P490-P523

通过运算符重载可重新定义该运算符的含义。

14.1 基本概念

定义:重载运算符是具有特殊名字的函数。名字由operator和符号组成。重载运算符包含返回类型、参数列表和函数体。

::: tip

当一个重载的运算符是成员函数时,this绑定到左侧运算对象。成员运算符函数的显式参数数量比运算对象的数量少一个。

对于一个运算符来说,它或者是类的成员,或者至少含有一个类类型的参数。

我们只能重载已有的运算符。

:::

直接调用一个重载的运算符函数

data1 + data2;

operator+(data1, data2);

// 以上2个调用等价

14.2 输入和输出运算符  

(1)重载输出运算符<<

ostream &operator<<(ostream &os, const Sales_data &item)

{

os << item.isbn() << " " << item.unites_sold << " " << item.revenue << " " << item.avg_price();

return os;

}

(2)重载输入运算符>>

istream &operator>>(istream &is, Sales_data &item)

{

double price;

is >> item.bookNo >> item.units_sold >> price;

if (is)

item.revenue = items.units_sold * price;

else

item = Sales_data();

return is;

}

14.3 算术和关系运算符

(1)相等运算符

bool operator==(const Sales_data &lhs, const Sales_data &rhs)

{

return lhs.isbn() == rhs.isbn() &&

lhs.unites_sold == rhs.units_sold &&

lhs.revenue == rhs.revenue;

}

(2)关系运算符

operator<

14.4 赋值运算符

operator=

operator+=

14.5 下标运算符

operator[]

下标运算符必须是成员函数。

class StrVec{
public:
std::string& operator[](std::size_t n){
return elements[n];
}
const std::string& operator[](std::size_t n) const{
return elements[n];
}
private:
std::string *elements;
}

14.6 递减和递增运算符

递增运算符(++)

递减运算符(--)

定义前置递增/递减运算符:

class StrBlobPtr{
public:
StrBlobPtr& operator++();  // 前置运算符
StrBlobPtr& operator--();
}

区分前置和后置运算符:

class StrBlobPtr{
public:
StrBlobPtr operator++(int); // 后置运算符
StrBlobPtr operator--(int);
}

14.7 成员访问运算符

operator*

operator->

14.8 函数调用运算符

如果类重载了函数调用运算符,则我们可以像使用函数一样使用该类的对象。

struct absInt{
int operator()(int val) const {
return val < 0 ? -val : val;
}
};
absInt absObj;
int ui = absObj(i);

如果定义了调用运算符,则该类的对象称为函数对象。

14.9 重载、类型转换与运算符

(1)类型转换运算符

类型转换运算符是类的一种特殊成员函数,将一个类类型的值转换成其他类型。形式:

operator type() const;

(2)避免有二义性的类型转换

(3)函数匹配与重载运算符

::: warning

如果对同一个类既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,将会遇到重载运算符与内置运算符的二义性问题。

:::

术语

类类型转换:由构造函数定义的从其他类型到类类型的转换以及由类型转换运算符定义的从类类型到其他类型的转换。

第十五章 面向对象程序设计

P526-P575

15.1 OOP:概述

(1)面对对象程序设计(object-oriented programming)的核心思想:

数据抽象、继承和动态绑定。

(2)继承:

继承是一种类联系在一起的一种层次关系。这种关系中,根部是基类,从基类继承而来的类成为派生类。

基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。

虚函数:virtual function。基类希望派生类各自定义自身版本的函数。

class Quote {
public:
std::string isbn() const;
virtual double net_price(std::size_t n) const;
}

(3)动态绑定:

::: tip

在C++语言中,当我们使用基类的引用(或者指针)调用一个虚函数时将发生动态绑定(也称运行时绑定)。P527

:::

15.2 定义基类和派生类

(1)定义基类

虚函数:基类希望派生类进行覆盖的函数。

基类将该函数定义为虚函数(virtual)。

基类通过在其成员函数的声明语句之前加上关键词virtual使得该函数执行动态绑定。

关键词virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义。

如果基类把一个函数声明成虚函数,则该函数在派生类中隐式的也是虚函数。

(2)定义派生类

派生类必须通过派生类列表明确指出它是从哪个基类继承而来的。

class Bulk_quote : public Quote {
... // 省略
}

对于派生类中的虚函数的处理:

若派生类未覆盖基类中的虚函数,则该虚函数的行为类似其他普通成员。

C++允许派生类显式注明覆盖了基类的虚函数,可通过添加override关键字。

派生类对象:

一个派生类对象包含多个部分:自己定义的成员的子对象,以及基类的子对象。

派生到基类的类型转换:

由于派生类对象中含有与其基类对象的组成部分,因此可以进行隐式的执行派生类到基类的转换。

Quote item;        // 基类
Bulk_quote bulk;   // 派生类
Quote *p = &item;  // p指向Quote对象
p = &bulk;         // p指向bulk的Quote部分
Quote &r = bulk;   // r绑定到bulk的Quote部分。

派生类构造函数:

每个类控制自己的成员的初始化过程。派生类首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员。

派生类使用基类的成员:

派生类可以访问基类的公有成员和受保护成员。

::: tip

派生类对象不能直接初始化基类的成员。派生类应该遵循基类的借口,通过调用基类的构造函数来初始化从基类继承来的成员。

:::

被用作基类的类:

若使用某个类作为基类,则该类必须已被定义而非仅仅声明。

派生类包含它的直接基类的子对象以及每个间接基类的子对象。

防止继承发生:

在类名后面跟着一个关键字final。

class NoDerived final {};   // NoDerived不能作为基类

(3)类型转换与继承

我们可以将基类的指针或引用绑定到派生类对象上。

静态类型与动态类型:

静态类型:在编译时已知,是变量声明时的类型或表达式生成的类型。

动态类型:运行时才可知,是变量或表达式表示的内存中的对象的类型。

如果表达式既不是引用也不是指针,则动态类型与静态类型永远一致。

不存在基类向派生类隐式类型转换:

Quote base;
Bulk_quote *bulkP = &base;    // 错误!
Bulk_quote *bulkRef = base;   // 错误!

::: warning

当我么用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分会被忽略掉。

:::

15.3 虚函数

C++的多态性:使用这些类型的多种形式,而无须在意它们的差异。

派生类中的虚函数:

一个派生类如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。

final和override说明符:

如果用override标记了某个函数,但是该函数并没有覆盖已存在的虚函数,此时编译器将报错。

如果用final标记了某个函数, 则之后任何尝试覆盖该函数的操作都将错误。

虚函数与默认实参:

如果虚函数某次被调用使用了默认实参,则该实参值由本次调用的静态类型决定。

15.4 抽象基类

纯虚函数:

书写=0可以将一个虚函数说明为纯虚函数(pure virtual),纯虚函数无须定义。

不能在类的内部为一个=0的函数提供函数体。

class Disc_quote : public Quote {
public:
double net_price(std::size_t) const = 0;
}

抽象基类:

含有纯虚函数的类是抽象基类。

不能创建抽象基类的对象。

15.5 访问控制与继承

受保护的成员:

派生类的成员和友元只能访问派生类对象中的基类部分的受保护成员;对于普通的基类对象中的成员不具有特殊的访问权限。P543

公有、私有和受保护继承:

派生访问说明符对于派生类的成员(及友元)能否访问其直接基类的成员无影响;

对基类成员的访问权限只与基类中的访问说明符有关。

派生访问说明符的目的是控制派生类用户对于基类成员的访问权限。

改变个别成员的可访问性:

通过在类的内部使用using声明语句,我们可以将该类的直接或间接基类中的任何可访问成员标记出来。

class Derived : private Base {
public:
using Base::size;
}

::: tip

派生类只能为它可访问的名字提供using声明。

:::

默认的继承保护级别:

使用class关键字定义的派生类是私有继承的;

使用struct关键字定义的派生类是共有继承的。

class Base {};
struct D1 : Base {};  // 默认public继承
class D2 : Base {};   // 默认private继承

15.6 继承中的类作用域

在编译时进行名字查找:

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。

名字冲突与继承:

派生类的成员将隐藏同名的基类成员。

::: tip

出了覆盖继承而来的虚函数外,派生类最好不雅重用其他定义在基类中的名字。

:::

如果派生类的成员函数与基类的某个成员函数同名,则派生类将在其作用域内隐藏掉该基类成员函数。

::: tip

非虚函数不会发生动态绑定。

:::

15.7 构造函数与拷贝控制

(1)虚析构函数

在基类中将析构函数定义成虚函数以确保执行正确的析构函数版本。

Quote *itemP = new Quote;
delete itemP;           // 调用Quote的析构函数
itemP = new Bulk_quote;
delete itemP;           // 调用Bulk_quote的析构函数

虚析构函数会阻止合成移动操作。

(2)合成拷贝控制与继承

基类缺少移动操作会阻止派生类拥有自己的合成移动操作,所以当确实要执行移动操作的时候就要首先在基类中进行显式定义。P554

(3)派生类的拷贝控制成员

派生类的拷贝或移动构造函数:

::: tip

默认情况下,基类默认构造函数初始化派生类对象的基类部分。如果我们想拷贝(或移动)基类部分,则必须在派生类的构造函数初始值列表中显式的使用基类的拷贝(或移动)构造函数。

:::

派生类的赋值运算符:

派生类的赋值运算符必须显式的为其基类部分赋值。

派生类的析构函数:

派生类函数只负责销毁由派生类自己分配的资源。

15.8 容器与继承

当使用容器存放继承体系中的对象时,必须采用间接存储的方式。因为不允许在容器中保存不同类型的元素。

术语

覆盖:override,派生类中定义的虚函数如果与基类中定义的同名虚函数与相同的形参列表,则派生类版本将覆盖基类的版本。

多态:程序能够通引用或指针的动态类型获取类型特定行为的能力。

第十六章 模板与泛型编程

P578-P630

(1)控制实例化

当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern就表示承诺在程序其他位置有该实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。

(2)模板是标准库的基础。

生成特定类或者函数的过程称为实例化。

(3)术语

类模板:模板定义,可从它实例化出特定的类。类模板的定义以关键词template开始,后面跟尖括号对<和>,其内为一个用逗号分隔的一个或多个模板参数的列表,随后是类的定义。

函数模板:模板定义,可从它实例化出特定函数。函数模板的定义以关键词template开始,后跟尖括号<和>,其内以一个用逗号分隔的一个或多个模板参数的列表,随后是函数的定义。

*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
推荐文章
最近访客