1、C++语言定义了 大量运算符 以及 内置类型的自动转换规则
当运算符 被用于 类类型的对象时,C++语言允许我们 为其指定新的含义;也能自定义类类型之间的转换规则
例:可以通过下述形式输出两个Sales item的和:
cout << item1 + item2; //输出两个Sales_item的和
由于我们的Sales_data类 还没有重载这些运算符,因此它的加法代码 显得比较冗长而不清晰
print(cout, add(data1, data2)); //输出两个Sales_data的和
1、基本概念
1、重载的运算符 是具有特殊名字的函数:它们的名字 由关键字operator 和 其后要定义的运算符号 共同组成。和其他函数一样,重载的运算符 也包含返回类型、参数列表 以及 函数体
重载运算符函数的参数数量 与 该运算符作用的运算对象 数量一样多。一元运算符有一个参数,二元运算符有两个。对于二元运算符来说,左侧运算对象 传递给第一个参数,而右侧运算对象传递给 第二个参数。除了重载的函数调用运算符 operator() 之外,其他重载运算符 不能含有默认实参
如果 一个运算符函数是成员函数,则它的第一个(左侧)运算对象 绑定到 隐式的this指针上,因此,成员运算符函数的(显式)参数数量 比运算符的运算对象 总数少一个
2、对于一个运算符函数来说,它或者是类的成员,或者至少含有一个类类型的参数:
//错误:不能为int重定义内置的运算符
int operator+(int, int)
这一约定意味着 当运算符作用于 内置类型的运算对象时,我们无法改变 该运算符的含义
3、只能重载 已有的运算符,而无权发明 新的运算符号。例如,我们不能提供 operator** 来执行幂操作
有四个符号(+、-、*、&)既是一元运算符 也是二元运算符,所有这些运算符都能 被重载,从参数的数量 我们可以推断到底定义的是哪种运算符
对于一个重载的运算符来说,其优先级和结合律 与对应的内置运算符保持一致
x == y + z;
永远等价于x == (y + z)
4、直接调用 一个重载的运算符函数:将运算符 作用于 类型正确的实参,从而以这种间接方式“调用”重载的运算符函数。然而,我们也能像 调用普通函数一样 直接调用运算符函数,先指定函数名字,然后传入数量正确、类型适当的实参:
//一个非成员运算符函数的等价调用
data1 + data2; //普通的表达式
operator+(data1, data2); //等价的函数调用
这两次调用 是等价的,它们都调用了 非成员函数operator+,传入data1作为第一个实参、传入data2作为第二个实参
像调用其他成员函数一样 显式地调用 成员运算符函数
data1 += data2; //基于“调用”的表达式
data1.operator+=(data2); //对成员运算符函数的等价调用
这两条语句 都调用了 成员函数operator+=,将this绑定到data1的地址、将data2 作为实参传入了函数
5、某些运算符不应该被重载:某些运算符 指定了 运算对象求值的顺序。因为使用重载的运算符 本质上是一次函数调用,所以这些关于运算对象求值顺序的规则 无法应用到重载的运算符上
逻辑与运算符(&, &&)、逻辑或运算符(|, ||)和逗号运算符(,)的运算对象求值顺序规则 无法保留下来。除此之外,&& 和 || 运算符的重载版本 也无法保留内置运算符的 短路求值属性,两个运算对象 总是会被求值
上述运算符的重载版本 无法保留求值顺序 和/或短路求值属性,因此 不建议重载它们
还有一个原因使得我们一般不重载逗号运算符 和 取地址运算符:C++语言已经定义了 这两种运算符 用于类类型对象时的特殊含义,这一点 与大多数运算符都不相同。因为这两种运算符 已经有了内置的含义,所以一般来说 它们不应该被重载,否则它们的行为将异于常态,从而导致类的用户无法适应
通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符
6、如果某些操作 在逻辑上 与运算符相关,则它们适合于 定义成重载的运算符:
- 如果类执行IO操作,则定义移位运算符 使其与内置类型的IO保持一致
- 如果类的某个操作 是检查相等性,则定义 operator==;如果类 有了 operator==,意味着它通常也应该有 operator!=
- 如果类包含一个内在的单序比较操作,则定义operator<;如果类有了 operator<,则它也应该含有其他关系操作
- 重载运算符的返回类型 通常情况下 应该与其内置版本的返回类型兼容:逻辑运算符和 关系运算符应该返回bool,算术运算符应该 返回一个类类型的值,赋值运算符 和 复合赋值运算符 则应该返回左侧运算对象的一个引用
当在内置的运算符 和 我们自己的操作之间 存在逻辑映射关系时,运算符重载的效果最好。此时,使用重载的运算符 显然比 另起一个名字更自然也更直观
7、赋值运算符的行为 与 复合版本的类似:赋值之后,左侧运算对象和右侧运算对象的值相等,并且运算符 应该返回它左侧运算对象的一个引用。重载的赋值运算应该继承 而非违背其内置版本的含义
+=运算符的行为显然应该与其内置版本一致,即先执行+,再执行=
8、定义重载的运算符时,必须首先决定是 将其声明为类的成员函数 还是 声明为一个普通的非成员函数。因为 有的运算符 必须作为成员;另一些情况下,运算符作为普通函数 比作为成员更好
- 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员
- 复合赋值运算符一般来说应该是成员,但并非必须,这一点与赋值运算符略有不同
- 改变对象状态的运算符或者与给定类型密切相关的运算符,如递增、递减和解引用运算符,通常应该是成员
- 具有对称性的运算符 可能转换 任意一端的运算对象,例如算术、相等性、关系和位运算符等,因此它们通常应该是普通的非成员函数
希望 能在含有混合类型的表达式中 使用对称性运算符。例如,我们能求一个int和一个double的和,因为它们中的任意一个都可以是 左侧运算对象 或 右侧运算对象,所以加法是对称的。如果我们想提供 含有类对象的混合类型表达式,则运算符必须定义成
非成员函数
当我们把运算符定义成 成员函数时,它的左侧运算对象 必须是 运算符所属类的一个对象。例如:
string s = "world";
string t = s + "!"; // 正确:我们能把一个const char*加到一个string对象中
string u = "hi" + s;// 如果+是string的成员,则产生错误
如果 operator+ 是string类的成员,则上面的第一个加法 等价于s.operator+("!")
。同样的,“hi” + s 等价于 "hi".operator+(s)
。显然"hi"的类型是 const char*,这是 一种内置类型,根本就没有成员函数
因为 string将+定义成了 普通的非成员函数,所以"hi"+s 等价于 operator+(“hi”, s) 。和任何其他函数调用一样,每个实参 都能被转换成 形参类型。唯一的要求是 至少有一个运算对象是类类型,并且两个运算对象 都能准确无误地转换成string(因为 重载了不同参数类型的函数)
在C++中,可以将const char*类型的字符串直接转换为std::string类型。这是因为std::string类具有接受C风格字符串作为构造函数参数的构造函数重载。例如:
const char* c_string = "Hello, world!";
std::string cpp_string(c_string); // 将 const char* 转换为 std::string
9、在什么情况下重载的运算符与内置运算符有所区别?在什么情况下重载的运算符又与内置运算符一样
- 有所区别:
当一个重载运算符是成员函数时,this绑定到左侧的运算对象;
逻辑与运算符、逻辑或运算符和逗号运算符的运算对象求值顺序规则无法保留下来;
&&和||运算符的重载版本也无法保留内置运算符的短路求值属性,两个运算对象总是会被求值 - 一样:
对于一个重载的运算符来说,其优先级和结合律与对应的内置运算保持一致
10、为 Sales_data 编写重载的输入、输出、加法和复合赋值运算符
Sales_data.h
#pragma once
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
#include <iostream>
struct Sales_data;
//std::istream& read(std::istream& is, Sales_data& item);
//Sales_data add(const Sales_data& s1, const Sales_data& s2);
//std::ostream& print(std::ostream& os, const Sales_data& s); // 还需要声明
std::istream& operator>>(std::istream& is, Sales_data& item);
std::ostream& operator<<(std::ostream& os, Sales_data& item);
Sales_data operator+(const Sales_data& s1, const Sales_data& s2);
class Sales_data {
public:
//friend Sales_data add(const Sales_data& s1, const Sales_data& s2);
//friend std::istream& read(std::istream& is, Sales_data& s);
//friend std::ostream& print(std::ostream& os, const Sales_data& s); // 友元声明
friend std::istream& operator>>(std::istream& is, Sales_data& item);
friend std::ostream& operator<<(std::ostream& os, Sales_data& item);
friend Sales_data operator+(const Sales_data& s1, const Sales_data& s2);
// 使用委托函数重新编写构造函数
Sales_data(const std::string& s, unsigned u, double r) : bookNo(s), units_sold(u), revenue(r* u) { }
Sales_data() : Sales_data("", 0, 0) { }
Sales_data(const std::string& s) : Sales_data(s, 0, 0) { }
Sales_data(std::istream& is) : Sales_data() {
// read(is, *this);
is >> *this;
}
Sales_data& combine(const Sales_data&);
std::string isbn() const { return bookNo; }
double avg() const;
private:
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data& Sales_data::combine(const Sales_data& s) {
units_sold += s.units_sold;
revenue += s.revenue;
return *this;
}
double Sales_data::avg() const {
if (units_sold) {
return revenue / units_sold;
}
else {
return 0;
}
}
//Sales_data add(const Sales_data& s1, const Sales_data& s2) {
// Sales_data tmp = s1;
// tmp.combine(s2);
// return tmp;
//}
Sales_data operator+(const Sales_data& s1, const Sales_data& s2) {
Sales_data tmp = s1;
tmp.combine(s2);
return tmp;
}
//std::istream& read(std::istream& is, Sales_data& s) {
// double singlePrice;
// is >> s.bookNo >> s.units_sold >> singlePrice;
// s.revenue = s.units_sold * singlePrice;
// return is;
//}
std::istream& operator>>(std::istream& is, Sales_data& s) {
double singlePrice;
is >> s.bookNo >> s.units_sold >> singlePrice;
s.revenue = s.units_sold * singlePrice;
return is;
}
//std::ostream& print(std::ostream& os, const Sales_data& s) {
// os << s.isbn() << " " << s.units_sold << " " << s.revenue << " " << s.avg();
// return os;
//}
std::ostream& operator<<(std::ostream& os, Sales_data& s) {
os << s.isbn() << " " << s.units_sold << " " << s.revenue << " " << s.avg();
return os;
}
#endif
14.2.cpp
#include "Sales_data.h"
int main() {
Sales_data sd;
std::cin >> sd;
Sales_data sd2;
std::cin >> sd2;
Sales_data sd3 = sd + sd2;
std::cout << sd3 << std::endl;
}
运行结果
11、string 和 vector 都定义了重载的==
以比较各自的对象,假设 svec1 和 svec2 是存放 string 的 vector,确定在下面的表达式中分别使用了哪个版本的==
[1]
"cobble" == "stone" // 应用了 C++ 语言内置版本的 == ,比较两个指针
在 C++ 中,表达式 “cobble” == “stone” 实际上比较的是两个字符串字面量的地址,而不是字符串内容。这是因为 “cobble” 和 “stone” 都是字符数组的常量指针。这里解释的关键是理解如何处理字符串字面量
字符串字面量
在 C++ 中,字符串字面量例如 “cobble” 和 “stone” 实际上是常量字符数组。当你写 “cobble” 时,它代表的是一个 const char[7] 类型的数组,包含字符 ‘c’, ‘o’, ‘b’, ‘b’, ‘l’, ‘e’ 和一个空字符 ‘\0’ 作为结束符。同样地,“stone” 表示一个 const char[6] 类型的数组
比较运算符 ==
当你使用 ==
操作符来比较两个如 “cobble” 和 “stone” 这样的字符串字面量时,实际上比较的是这些字面量在内存中的地址。每个字符串字面量都有一个固定的存储位置,因此 “cobble” 和 “stone” 作为指向这些位置的指针被比较。因为它们指向不同的内存位置,比较的结果总是 false
正确比较字符串内容
如果你想要比较两个字符串的内容,应该使用标准库中的 std::string 类,或者使用 strcmp() 函数来比较两个 C 风格的字符串(字符数组)
1)使用 std::string:
#include <string>
#include <iostream>
int main() {
std::string s1 = "cobble";
std::string s2 = "stone";
if (s1 == s2) {
std::cout << "Strings are equal." << std::endl;
} else {
std::cout << "Strings are not equal." << std::endl;
}
return 0;
}
2)使用 strcmp():按字典序比较
#include <cstring>
#include <iostream>
int main() {
const char* s1 = "cobble";
const char* s2 = "stone";
if (strcmp(s1, s2) == 0) {
std::cout << "Strings are equal." << std::endl;
} else {
std::cout << "Strings are not equal." << std::endl;
}
return 0;
}
[2]
"svec1[0] == "stone" // 应用了 string 版本的重载 ==,字符串字面常量被转换为 string
12、类中定义static成员 和 shared_ptr有什么区别
1)静态成员(Static Members)
在类定义中,静态成员可以是 变量或函数。静态成员与类的任何特定对象实例无关,而是 与类本身关联。换句话说,静态成员属于整个类,而不是属于类的单个实例
特点和用途:
共享数据:静态成员变量在类的所有实例之间共享。例如,如果你需要一个计数器来跟踪类的实例数量,可以使用静态成员变量。
全局访问:静态成员可以通过类名直接访问,不需要类的对象实例。
生命周期:静态成员的生命周期从程序开始到程序结束,它们不依赖于任何对象实例的创建或销毁
2)shared_ptr(智能指针)
std::shared_ptr 是 C++ 标准库中的一个模板类,用于管理动态分配的对象的内存。它是智能指针的一种,提供了自动的引用计数功能
特点和用途:
自动内存管理:shared_ptr 自动管理 其所指向对象的内存。当最后一个 shared_ptr 指向一个对象时,该对象将被自动销毁
引用计数:shared_ptr 通过内部的引用计数机制 来确保多个指针可以安全地共享同一个对象。当新的 shared_ptr 拷贝或赋值时,引用计数增加;当 shared_ptr 被销毁或重新赋值时,引用计数减少
线程安全:引用计数的更新是线程安全的,但是通过 shared_ptr 访问对象本身并不保证线程安全
区别
目的:静态成员是用于表示 与类关联的信息或状态,独立于类的任何实例。而 std::shared_ptr 主要用于内存管理,确保动态分配的对象在正确的时间被适当地释放
作用范围:静态成员是类级别的,与特定的对象实例无关。std::shared_ptr 是对象级别的,管理一个具体的对象实例
内存管理:静态成员不涉及内存管理,它们的内存是自动分配和销毁的。std::shared_ptr 是智能指针,专门用于管理动态分配的对象的生命周期和内存
静态成员和 std::shared_ptr 都可以用来在不同的对象之间共享数据,但它们的设计意图和实现方式有本质的不同
1)设计意图与使用场景
静态成员:
静态成员主要用于共享所有类实例共有的数据或功能。例如,你可能会用静态成员 来计数类的实例数量或者存储类的全局配置
静态成员变量的生命周期从程序开始直到程序结束,并且它的值是在所有对象间共享的。任何对静态成员变量的修改都会影响到访问该变量的所有对象
std::shared_ptr:
std::shared_ptr 用于管理动态内存,并通过引用计数来自动释放不再使用的对象。它主要用于控制对象的生命周期,尤其是在多个对象需要共享同一个资源的情况下
当使用 std::shared_ptr 时,它内部的引用计数机制确保当最后一个指向对象的 shared_ptr 被销毁或重置时,对象的内存会被自动释放。这有助于防止内存泄漏,同时允许多个对象共享相同的资源
2)实现和行为
静态成员:
静态成员变量不依赖于类的实例存在。它们在类的首次加载时初始化,并在程序结束时销毁
访问静态成员不需要对象实例。可以直接通过类名加作用域解析操作符 :: 访问(例如 ClassName::staticMember)
静态成员的值是全局共享的,对静态成员的修改在所有实例中都是可见的
std::shared_ptr:
std::shared_ptr 管理一个对象的实例,并通过引用计数来共享所有权。引用计数确保当最后一个 shared_ptr 被销毁时,所管理的对象会被自动删除
std::shared_ptr 是对象级别的,它可以指向任何动态分配的对象,并不限于指向某个类的成员
使用 std::shared_ptr 可以避免内存泄漏,特别是在复杂的对象图和所有权共享的场景中
13、如何确定下列运算符是否应该是类的成员
(a)% 通常定义为非成员
(b)%= 通常定义为类成员,因为它会改变对象的状态
(c)++ 通常定义为类成员,因为它会改变对象的状态
(d)-> 必须定义为类成员,否则编译会报错
(e)<< 通常定义为非成员
(f)&& 通常定义为非成员
(g)== 通常定义为非成员
(h)() 必须定义为类成员,否则编译会报错
注:复合赋值运算符(+=、-=、*=、/= 和 %=)会改变对象的状态,一般定义为类成员
以下是一些指导原则,帮助决定何时将运算符作为类成员进行重载:
1)运算符作为类成员重载
修改操作符(比如赋值运算符=):
赋值运算符=、下标运算符[]、函数调用运算符(), 和成员访问指针运算符->通常 必须作为成员函数来重载,因为它们涉及到对象内部状态的修改,或者需要有特殊的行为表现(如访问私有成员)
单目运算符:
像一元运算符++(前置和后置)、–(前置和后置)、一元-和+通常作为成员函数来重载,因为它们只涉及一个操作数,且通常会改变对象的状态 或 需要访问对象的私有数据
流运算符(特别是输出运算符<<和输入运算符>>):
虽然这些通常作为全局函数实现(以便第一个操作数可以是非类类型,如std::ostream),但在某些情况下,如果它们需要访问类的私有成员,可以作为友元函数实现
其他需要访问私有或保护成员的情况:
当操作需要访问类的内部数据时,可以选择作为成员函数实现,或者作为友元函数,如果要让操作看起来像是对两个独立对象进行操作
2)运算符作为全局函数重载
双目运算符:
大多数双目运算符,如+、-、*、/和==,更常见的做法是将它们作为全局函数重载。这样可以使得左侧和右侧的操作数具有对称性,特别是当操作数可以是不同类型时
实现对称性:
当你希望你的运算符 对两个不同类型的操作数(其中一个是类类型)进行操作时,全局函数是更好的选择。例如,允许一个整数和一个自定义类类型相加,且无论操作数的顺序如何都能工作
非成员非友元的简化实现:
如果重载的运算符不需要访问类的私有成员,那么实现为全局函数可以简化代码,并增加代码的清晰度和可维护性
为什么-> 必须定义为类成员,否则编译会报错
- 语义约定:
运算符 -> 通常用于指针类型和类似指针的类型(如智能指针),以便直接访问指针指向的对象的成员。在 C++ 中,对于原生指针,a->b 的行为是解引用指针 a 并访问其成员 b,即 (*a).b。对于类类型,重载 -> 允许对象表现得像指针,提供类似指针的成员访问功能 - 限制为成员函数:
C++ 的设计者决定将 -> 运算符的重载限制为成员函数,这样可以保证只有类类型本身才能定义如何通过该运算符访问其成员。这是合理的,因为 -> 操作涉及到对类内部如何表现和管理数据的深入了解,这种知识通常只有类本身才具备 - 保持一致性:
通过将 -> 限定为成员函数,C++ 保持了对指针类行为的一致性。这意味着任何尝试像使用指针一样使用一个对象(通过 ->)的行为必须由对象的类直接定义,保证了操作的逻辑内聚性 - 简化语言复杂性:
如果允许 -> 作为全局函数重载,将会增加语言的复杂性和潜在的歧义。例如,如果两个不同的类或库试图为相同的类型提供不同的 -> 运算符实现,可能会导致预期之外的行为或编译时冲突
2、输入和输出运算符
2.1 重载输出运算符<<
1、通常情况下,输出运算符的第一个形参是一个非常量o stream对象的引用。之所以ostream是非常量 是因为向流写入内容 会改变其状态:而该形参是引用 是因为我们无法直接复制一个ostream对象
第二个形参 一般来说是 一个常量的引用,该常量是 我们想要打印的类类型。第二个形参是 引用的原因是 我们希望避免复制实参:而之所以该形参可以是常量 是因为(通常情况下)打印对象 不会改变对象的内容
为了与其他输出运算符保持一致,operator<<一般要返回它的 ostream形参
2、Sales_data的输出运算符:与之前的print函数 完全一样
std::ostream& operator<<(std::ostream& os, Sales_data& s) {
os << s.isbn() << " " << s.units_sold << " " << s.revenue << " " << s.avg();
return os;
}
用于内置类型的输出运算符 不太考虑 格式化操作,尤其 不会打印换行符。令输出运算符 尽量减少格式化操作 可以使用户有权
控制输出的细节
输出运算符应该 主要负责打印对象的内容而非控制格式
3、与iostream标准库兼容的输入输出运算符 必须是 普通的非成员函数,而不能是 类的成员函数。否则,它们的左侧运算对象 将是我们的类的一个对象:
Sales_data data;
data << cout; // 如果operator<<是Sales_data的成员
假设 输入输出运算符是某个类的成员,则它们也必须是istream 或 ostream的成员
然而,这两个类属于标准库,并且 我们无法给标准库中的类添加任何成员
IO运算符 通常需要 读写类的非公有数据成员,所以IO运算符 一般被声明为友元
2.2 重载输入运算符>>
1、通常情况下,输入运算符的第一个形参 是运算符将要读取的流的引用,第二个形参是 将要读入到的(非常量)对象的引用。该运算符通常会 返回某个给定流的引用
第二个形参 之所以 必须是个非常量是因为 输入运算符本身的目的 就是将数据读入到这个对象中
2、Sales_data的输入运算符:除了if语句之外,这个定义与之前的read函数 完全一样(加了处理 输入可能失败的情况)
std::istream& operator>>(std::istream& is, Sales_data& s) {
double singlePrice;
is >> s.bookNo >> s.units_sold >> singlePrice;
// s.revenue = s.units_sold * singlePrice;
// return is;
if (is) // 检查输入是否成功
item.revenue = item.units_sold * price;
else
item = Sales_data(); // 输入失败:对象被赋予默认的状态
return is;
}
输入运算符 必须处理 输入可能失败的情况,而输出运算符不需要
输入运算符必须处理失败的情况,因为它们直接影响到程序的内部状态和后续计算的正确性。相比之下,输出运算符虽然也可能遇到错误情况,但通常情况下,这些错误不会影响程序的逻辑流程,处理起来也较为简单
3、输入时错误:
- 当流含有错误类型的数据时 读取操作可能失败。例如在读取完 bookNo 后,输入运算符 假定接下来读入的是两个数字数据,一旦输入的 不是数字数据,则读取操作 及 后续对流的其他使用都将失败
- 当读取操作 到达文件末尾 或者 遇到输入流的其他错误时也会失败
没有逐个检查每个读取操作,而是 等读取了所有数据后 赶在使用这些数据前 一次性检查
如果 读取操作失败,则price的值 将是未定义的。因此,在使用price前 我们需要 首先检查输入流的合法性,然后才能执行计算并将结果存入 revenue
如果在发生错误前 对象已经有一部分被改变,则适时地 将对象置为合法状态显得异常重要。例如在这个输入运算符中,我们可能在成功读取新的bookNo后遇到错误,这意味着对象的 units_sold 和 revenue成员 并没有改变,因此有可能会将这两个数据与一条完全不匹配的 bookNo 组合在一起
当读取操作发生错误时,输入运算符应该负责从错误中恢复
4、标示错误:一些输入运算符 需要做更多数据验证的工作。例如,我们的输入运算符 可能需要检查 bookNo 是否符合规范的格式。在这样的例子中,即使从技术上来看IO是成功的,输入运算符 也应该设置流的条件状态 以标示出失败信息。通常情况下,输入运算符 只设置failbit。除此之外,设置eofbit表示文件耗尽,而设置badbit表示流被破坏。最好的方式是 由IO标准库自己来标示这些错误
3、算术和关系运算符
1、把 算术和关系运算符 定义成 非成员函数 以允许对左侧或右侧的运算对象 进行转换。因为这些运算符 一般不需要 改变运算对象的状态,所以形参 都是常量的引用
2、算术运算符 通常会 计算它的两个运算对象并得到一个新值,这个值 有别于任意一个运算对象,常常位于 一个局部变量之内,操作完成后 返回该局部变量的副本 作为其结果。如果类定义了算术运算符,则它一般也会定义 一个对应的复合赋值运算符。此时,最有效的方式是 使用复合赋值来定义算术运算符
//假设两个对象指向同一本书
Sales_data
operator+(const Sales_data &lhs, const Sales_data &rhs)
{
Sales_data sum = lhs; //把lhs的数据成员拷贝给sum
sum += rhs; //将rhs加到sum中(使用复合赋值来定义算术运算符)
return sum;
}
3.1 相等运算符
1、通常情况下,C++中的类 通过定义相等运算符 来检验两个对象是否相等。也就是说 它们会比较对象的每一个数据成员,只有当所有对应的成员都相等时 才认为两个对象相等
bool operator==(const Sales_data &lhs, const Sales_data &rhs)
{
return lhs.isbn() == rhs.isbn() &&
lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue;
}
bool operator!=(const Sales_data &lhs, const Sales_data &rhs)
{
return !(lhs == rhs);
}
- 如果一个类含有判断两个对象是否相等的操作,则它显然应该把函数定义成 operator== 而非 一个普通的命名函数
- 如果类定义了operator==,则该运算符 应该能判断 一组给定的对象中是否含有重复数据
- 通常情况下,相等运算符 应该具有传递性,换句话说,如果
a==b
和b==c
都为真,则 a==c 也应该为真 - 如果类定义了operator==,则这个类也应该定义operator!=。对于用户来说,当他们能使用==时肯定也希望能使用 !=,反之亦然
- 相等运算符和不相等运算符中的一个 应该 把工作委托给另外一个,这意味着其中一个运算符(如例子中==) 应该负责 实际比较对象的工作,而另一个运算符(例子中 !=) 则只是调用那个真正工作的运算符
3.2 关系运算符
1、定义了 相等运算符的类 也常常(但不总是)包含关系运算符。特别是,因为关联容器 和 一些算法 要用到小于运算符,所以定义operator<会比较有用
2、关系运算符应该
- 定义顺序关系,令其与关联容器中对关键字的要求一致;并且
- 如果类同时也含有
==
运算符的话,则定义一种关系令其与==
保持一致。特别是 如果两个对象是 != 的,那么一个对象应该<另外一个
3、对于Sales_data的==运算符来说,如果两笔交易的revenue和units_sold成员不同,那么即使 它们的ISBN相同也无济于事,它们仍然是不相等的。如果我们定义的<运算符仅仅比较ISBN成员,那么将发生这样的情况:两个ISBN相同 但 revenue 和 units_ sold 不同的对象 经比较是不相等的,但是其中的任何一个 都不比另一个小
也许会认为,只要让operator<依次比较每个数据元素 就能解决问题了,比方说 让operator<先比较isbn,相等的话 继续比较units_sold,还相等 再继续比较revenue
然而,这样的排序 没有任何必要。根据将来使用 Sales_data 类的实际需要,我们 可能会希望 先比较units_sold,也可能希望先比较revenue
因此对于Sales_data类来说,不存在 一种逻辑可靠的<定义,这个类不定义<运算符 也许更好
如果存在唯一 一种逻辑可靠的<定义,则应该考虑为这个类定义<运算符。如果类同时还包含==
,则当且仅当的定义 和 ==
产生的结果一致时 才定义<<运算符
4、为 StrBlob 类、StrBlobPtr 类、StrVec 类和 String 类 分别定义关系运算符
1)StrBlob & StrBlobPtr 两个 StrBlob 的比较就是比较两个字符串 vector;两个 StrBlobPtr 的比较,本质上是比较两个指向 vector 内元素的指针(迭代器),因此,首先要求两个 StrBlobPtr 指向相同的 vector,否则没有可比性,然后比较指向的位置即可
在 C++ 中,当我们使用 StrBlob s2 = s1;
或 StrBlob s2(s1);
这样的语法时,实际上会调用类 StrBlob 的拷贝构造函数(如果定义了的话)。拷贝构造函数负责根据给定对象创建一个新的对象,其成员变量的值与原始对象相同
在 StrBlob 类中,并没有显式定义拷贝构造函数,因此编译器会生成一个默认的拷贝构造函数。默认的拷贝构造函数会逐个复制原始对象的成员变量到新对象中,对于智能指针成员 data,它会进行浅拷贝,即只是复制指针本身,而不会复制指针所指向的对象。这样,新对象的 data 成员指向的是与原始对象相同的底层 std::vector<std::string>
对象,而不是创建一个新的 std::vector<std::string>
对象
对于 std::shared_ptr,进行浅拷贝时,只会复制指针本身,而不会复制指针所指向的对象。这意味着,新创建的 std::shared_ptr 指向的是与原始 std::shared_ptr 相同的内存区域,而不是新创建一个对象来存储数据。因此,新旧 std::shared_ptr 共享同一个对象,并且对象的引用计数会增加,表示有两个 std::shared_ptr 指向同一个对象
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // 浅拷贝,ptr1 和 ptr2 共享同一个对象
std::cout << "ptr1 count: " << ptr1.use_count() << std::endl; // 输出:2
std::cout << "ptr2 count: " << ptr2.use_count() << std::endl; // 输出:2
return 0;
}
对于 std::shared_ptr,进行非浅拷贝(即深拷贝)时,会创建一个新的智能指针对象,并且该新对象指向的内存区域与原始对象指向的内存区域完全不同。换句话说,深拷贝会复制指针所指向的对象,并创建一个独立的副本
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = std::make_shared<int>(*ptr1); // 深拷贝
std::cout << "ptr1 count: " << ptr1.use_count() << std::endl; // 输出:1
std::cout << "ptr2 count: " << ptr2.use_count() << std::endl; // 输出:1
return 0;
}
在这个示例中,ptr1 和 ptr2 分别指向两个不同的 int 对象,它们的引用计数均为 1。通过 std::make_shared(*ptr1) 创建了一个新的 int 对象,并将其赋值给了 ptr2。因此,ptr1 和 ptr2 是独立的智能指针,它们各自拥有自己的对象,并且引用计数为 1
深拷贝适用于需要确保对象拷贝后的独立性的场景,例如当需要修改其中一个副本时不影响其他副本的情况。不过,深拷贝也可能导致额外的内存开销,特别是当拷贝的对象很大或者包含复杂的数据结构时
StrBlob.h
#pragma once
#ifndef STRBLOB_H
#define STRBLOB_H
#include <string>
#include <vector>
#include <iostream>
#include <memory>
#include <initializer_list>
#include <stdexcept>
class StrBlob {
friend class StrBlobPtr;
// 重载运算符
friend bool operator==(const StrBlob& s1, const StrBlob& s2);
friend bool operator!=(const StrBlob& s1, const StrBlob& s2);
friend bool operator<(const StrBlob& s1, const StrBlob& s2);
friend bool operator<=(const StrBlob& s1, const StrBlob& s2);
friend bool operator>(const StrBlob& s1, const StrBlob& s2);
friend bool operator>=(const StrBlob& s1, const StrBlob& s2);
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob() :data(std::make_shared<std::vector<std::string>>()) {};
StrBlob(const std::initializer_list<std::string>& il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const std::string& t) { data->push_back(t); }
void pop_back();
std::string& front();
std::string& front() const;
std::string& back();
std::string& back() const;
StrBlobPtr begin();
StrBlobPtr end();
std::string& operator[](size_t t) {
check(t, "out of range"); // 别忘了考虑越界情况
return (*data)[t];
} // data->at(n)也可以
const std::string& operator[](size_t t) const { // 只有参数上的改变
check(t, "out of range");
return (*data)[t];
}
private:
std::shared_ptr<std::vector<std::string>> data;
void check(size_type i, const std::string& msg) const;
};
// 其实就是自己写的StrBlob的迭代器
class StrBlobPtr {
friend class StrBlob;
// 重载运算符
friend bool operator==(const StrBlobPtr& p1, const StrBlobPtr& p2);
friend bool operator!=(const StrBlobPtr& p1, const StrBlobPtr& p2);
friend bool operator<(const StrBlobPtr& p1, const StrBlobPtr& p2);
friend bool operator<= (const StrBlobPtr & p1, const StrBlobPtr & p2);
friend bool operator>(const StrBlobPtr& p1, const StrBlobPtr& p2);
friend bool operator>=(const StrBlobPtr& p1, const StrBlobPtr& p2);
public:
StrBlobPtr() :curr(0) {}
StrBlobPtr(StrBlob& a, size_t sz = 0) :wptr(a.data), curr(sz) {}
std::string& deref() const;
StrBlobPtr& incr();
bool operator!=(const StrBlobPtr& p) { return p.curr != curr; }
const std::string& operator[](size_t) const;
StrBlobPtr& operator++();
StrBlobPtr& operator++(int);
StrBlobPtr& operator--();
StrBlobPtr& operator--(int);
StrBlobPtr operator+(size_t) const; // 有const,返回“值”:返回值应该是一个新的对象,而不是原始对象的引用或指针
StrBlobPtr operator-(size_t) const;
StrBlobPtr& operator+=(size_t);
StrBlobPtr& operator-=(size_t);
std::string& operator*() const;
std::string* operator->() const; // 返回的是个指针,通过运算符*完成,这个函数也是个const
private:
std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr;
};
StrBlob::StrBlob(const std::initializer_list<std::string>& il) :data(std::make_shared<std::vector<std::string>>(il)) {}
void StrBlob::check(size_type i, const std::string& msg) const {
if (i >= data->size())
throw std::out_of_range(msg);
}
void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
std::string& StrBlob::front() {
check(0, "front on empty StrBlob");
return data->front();
}
std::string& StrBlob::front() const {
check(0, "front on empty StrBlob");
return data->front();
}
std::string& StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
std::string& StrBlob::back() const {
check(0, "back on empty StrBlob");
return data->back();
}
std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string& msg) const
{
auto ret = wptr.lock();
if (!ret)
throw std::runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw std::out_of_range(msg);
return ret;
}
std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
StrBlobPtr& StrBlobPtr::incr()
{
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
StrBlobPtr StrBlob::begin() {
return StrBlobPtr(*this);
}
StrBlobPtr StrBlob::end()
{
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
// 自定义运算符
bool operator==(const StrBlob& p1, const StrBlob& p2) {
return p1.data == p2.data;
}
bool operator!=(const StrBlob& p1, const StrBlob& p2) {
return !(p1 == p2);
}
bool operator==(const StrBlobPtr& p1, const StrBlobPtr& p2) {
auto pp1 = p1.wptr.lock(), pp2 = p2.wptr.lock();
return pp1 == pp2;
}
bool operator!=(const StrBlobPtr& p1, const StrBlobPtr& p2) {
return !(p1 == p2);
}
bool operator<(const StrBlob& p1, const StrBlob& p2) {
return *(p1.data) < *(p2.data);
}
bool operator<=(const StrBlob& p1, const StrBlob& p2) {
return *(p1.data) <= *(p2.data);
}
bool operator>(const StrBlob& p1, const StrBlob& p2) {
return *(p1.data) > *(p2.data);
}
bool operator>=(const StrBlob& p1, const StrBlob& p2) {
return *(p1.data) >= *(p2.data);
}
bool operator<(const StrBlobPtr& p1, const StrBlobPtr& p2) {
auto pp1 = p1.wptr.lock();
auto pp2 = p2.wptr.lock();
if (pp1 == pp2) {
if (!pp1)
return true;
else
return p1.curr < p2.curr;
}
else {
return false;
}
}
// 剩下的比较运算符都通过对其他比较运算符取反实现
bool operator>(const StrBlobPtr& p1, const StrBlobPtr& p2) {
return p2 < p1; // 换个顺序就行
}
bool operator>=(const StrBlobPtr& p1, const StrBlobPtr& p2) {
return !(p1 < p2);
}
bool operator<=(const StrBlobPtr& p1, const StrBlobPtr& p2) {
return !(p1 > p2);
}
const std::string& StrBlobPtr::operator[](size_t t) const {
check(t, "out of range");
auto res = this->wptr.lock();
return (*res)[t];
}
StrBlobPtr& StrBlobPtr::operator++() {
// 如果 curr 已经指向了容器的尾后位置,则无法递增它
check(curr, "after after end of StrBlobPtr");
curr++;
return *this;
}
StrBlobPtr& StrBlobPtr::operator++(int) {
// 此处无需检查有效性,调用前置递增运算时才需要检查
StrBlobPtr res = *this; // 由一个临时变量保存结果
++*this; // 使用StrBlobPtr的前置++
return res;
}
StrBlobPtr& StrBlobPtr::operator--() {
// 先减后检查,跟++不同,因为++尾后元素还成立,--不是
curr--;
check(curr, "before begin of StrBlobPtr");
return *this;
}
StrBlobPtr& StrBlobPtr::operator--(int) {
// 此处无需检查有效性,调用前置递增运算时才需要检查
StrBlobPtr res = *this; // 由一个临时变量保存结果
--*this;
return res;
}
StrBlobPtr& StrBlobPtr::operator+=(size_t t) {
curr += t;
check(curr, "up out of range");
return *this;
}
StrBlobPtr& StrBlobPtr::operator-=(size_t t) {
curr -= t;
check(curr, "down out of range");
return *this;
}
// 对对象本身不做修改,另外返回一个随机变量,自然不需要返回引用
StrBlobPtr StrBlobPtr::operator+(size_t t) const {
StrBlobPtr tmp = *this;
tmp.curr += t;
return tmp;
}
StrBlobPtr StrBlobPtr::operator-(size_t t) const {
StrBlobPtr tmp = *this;
tmp.curr -= t;
return tmp;
}
std::string& StrBlobPtr::operator*() const {
check(curr, "derefernce past end");
auto tmp = wptr.lock();
return (*tmp)[curr];
}
std::string* StrBlobPtr::operator->() const {
// 将实际工作委托给解引用运算符
return & this->operator*();
}
#endif
2)比较运算符只要写出一个,就可以直接利用这一个写出剩下的三个(四个比较运算符:<、>、<=、>=)
StrVec.h
#pragma once
#include <string>
#include <memory>
#include <iostream>
class StrVec {
friend bool operator==(const StrVec& s1, const StrVec& s2);
friend bool operator!=(const StrVec& s1, const StrVec& s2);
friend bool operator<(const StrVec & s1, const StrVec & s2);
friend bool operator<=(const StrVec& s1, const StrVec& s2);
friend bool operator>(const StrVec& s1, const StrVec& s2);
friend bool operator>=(const StrVec& s1, const StrVec& s2);
public:
StrVec() :
elements(nullptr), first_free(nullptr), cap(nullptr) {}
StrVec(const StrVec&);
StrVec& operator=(const StrVec&);
~StrVec();
void push_back(const std::string&);
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
std::string* begin() const { return elements; }
std::string* end() const { return first_free; }
void reserve(size_t n);
void resize(size_t n);
void resize(size_t n, const std::string& s);
StrVec(std::initializer_list<std::string>);
std::string& operator[](size_t t) { return elements[t]; } // 直接对指向首地址的指针使用下标运算符
const std::string& operator[](size_t t) const { return elements[t]; }
private:
static std::allocator<std::string> alloc;
void chk_n_alloc();
std::pair<std::string*, std::string*> alloc_n_copy(const std::string*, const std::string*);
void free();
void reallocate();
std::string* elements;
std::string* first_free;
std::string* cap;
};
StrVec.cpp
#include "StrVec.h"
#include <algorithm>
using namespace std;
std::allocator<std::string> StrVec::alloc;
void StrVec::chk_n_alloc()
{
if (size() == capacity())
reallocate();
}
void StrVec::push_back(const string& s) {
chk_n_alloc();
alloc.construct(first_free++, s);
}
pair<string*, string*> StrVec::alloc_n_copy(const string* b, const string* e)
{
auto data = alloc.allocate(e - b);
return { data, uninitialized_copy(b, e, data) };
}
void StrVec::free()
{
if (elements) {
for_each(elements, first_free, [](string& s) { alloc.destroy(&s); });
}
alloc.deallocate(elements, cap - elements);
}
StrVec::StrVec(const StrVec& s)
{
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = newdata.second;
cap = newdata.second;
}
StrVec::~StrVec() { free(); }
StrVec& StrVec::operator=(const StrVec& rhs)
{
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = data.second;
cap = data.second;
return *this;
}
void StrVec::reallocate()
{
auto newcapacity = size() ? 2 * size() : 1;
auto newdata = alloc.allocate(newcapacity);
auto dest = newdata;
auto elem = elements;
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
void StrVec::reserve(size_t n) {
if (size() < n) {
reallocate();
}
}
void StrVec::resize(size_t n) {
resize(n, std::string());
}
void StrVec::resize(size_t n, const string& s)
{
if (size() > n) {
while (first_free != elements + n) {
alloc.destroy(--first_free);
}
alloc.destroy(first_free);
}
else {
while (size() < n) {
push_back(s);
}
}
}
StrVec::StrVec(std::initializer_list<std::string> il) {
auto newdata = alloc_n_copy(il.begin(), il.end());
elements = newdata.first;
first_free = newdata.second;
cap = newdata.second;
}
bool operator==(const StrVec& s1, const StrVec& s2) {
if (s1.size() != s2.size()) {
return false;
}
else {
for (auto it1 = s1.begin(), it2 = s2.begin(); it1 != s1.end() && it2 != s2.end(); it1++, it2++)
{
if (*it1 != *it2)
return false;
}
return true;
}
}
bool operator!=(const StrVec& s1, const StrVec& s2) {
return !(s1 == s2);
}
// 之前的 string 都相等,当前 string 更小,s1 中的所有 string 都与 s2 中的 string 相等,且 s1 更短符合条件
bool operator<(const StrVec& s1, const StrVec& s2) {
auto it1 = s1.begin(), it2 = s2.begin();
for (; it1 != s1.end() && it2 != s2.end(); it1++, it2++)
{
if (*it1 < *it2)
return true;
else if (*it1 > *it2)
return false;
}
if (it1 == s1.end() && it2 != s2.end()) // 一定要同时判断s2
return true;
else
return false;
}
// 比较运算符只要写出一个,就可以直接利用这一个写出剩下的三个
bool operator>(const StrVec& s1, const StrVec& s2) {
return s2 < s1;
}
bool operator<=(const StrVec& s1, const StrVec& s2) {
return !(s1 > s2);
}
bool operator>=(const StrVec& s1, const StrVec& s2) {
return !(s1 < s2);
}
3)String 类的关系运算符就是比较两个字符串字典序的先后 std::lexicographical_compare
String.h
#pragma once
#ifndef STRING_H
#define STRING_H
#include <iostream>
#include <memory>
#include <algorithm>
class String {
friend bool operator==(const String lhs, const String rhs);
friend bool operator!=(const String lhs, const String rhs);
friend bool operator<(const String lhs, const String rhs);
friend bool operator>(const String lhs, const String rhs);
friend bool operator<=(const String lhs, const String rhs);
friend bool operator>=(const String lhs, const String rhs);
friend String operator+(const String&, const String&);
friend String add(const String&, const String&);
friend std::ostream& operator<<(std::ostream&, const String&);
friend std::ostream& print(std::ostream&, const String&);
public:
String() :sz(0), p(nullptr) {};
String(const char* cp) :sz(strlen(cp)), p(a.allocate(strlen(cp)))
{
std::uninitialized_copy(cp, cp + strlen(cp), p);
};
String(size_t n, char c) :sz(n), p(a.allocate(n))
{
std::uninitialized_fill_n(p, n, c);
};
String(const String& s) :sz(s.sz), p(a.allocate(s.sz))
{
std::cout << "copy constructor -- " << s << std::endl;
std::uninitialized_copy(s.p, s.p + s.sz, p);
}
String& operator=(const String&);
~String() { free(); }
String make_plural(size_t ctr, const String&, const String&);
inline void swap(String& s1, String& s2) {
s1.swap(s2);
}
String& operator=(const char*);
String& operator=(char);
const char* begin() { return p; }
const char* begin() const { return p; }
const char* end() { return p + sz; }
const char* end() const { return p + sz; }
size_t size() const { return sz; }
void swap(String&);
// 下标运算符,同样是对首指针使用 下标运算符
char& operator[](size_t t) { return p[t]; }
const char& operator[](size_t t) const { return p[t]; } // 可以定义只有是否有函数const区别的重载函数
private:
size_t sz;
char* p;
static std::allocator<char> a;
void free();
};
#endif
String.cpp
- equal
equal(lhs.begin(), lhs.end(), rhs.begin()));
用于比较两个范围内的元素是否相等
std::equal 的函数签名:template <class InputIterator1, class InputIterator2> bool equal(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2);
这里 InputIterator1 和 InputIterator2 是输入迭代器类型,分别表示第一个范围和第二个范围的起始迭代器
first1 和 last1 表示第一个范围的起始和结束迭代器(不包括 last1 对应的元素),first2 表示第二个范围的起始迭代器
- lexicographical_compare
return lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), lhs.end());
用于比较两个序列的字典序。它比较两个范围内的元素,如果第一个范围在字典序上小于第二个范围,则返回 true,否则返回 false
首先,std::lexicographical_compare 从第一个范围的开始开始与第二个范围的相应位置的元素进行比较
如果两个范围中的对应位置上的元素相等,则继续比较下一个位置的元素
如果某个范围中的元素用尽了,但另一个范围还有剩余元素,则较短范围被认为是小于较长范围的
如果两个范围都用尽了(即到达了对应的 last1 和 last2),则返回 false,表示两个范围相等
如果在比较过程中发现某一位置上第一个范围的元素小于第二个范围的对应位置上的元素,则返回 true,表示第一个范围小于第二个范围
String.cpp
#include "String.h"
using namespace std;
allocator<char> String::a;
void String::free()
{
for_each(p, p + sz, [this](char& c) { a.destroy(&c); });
a.deallocate(p, sz);
}
void String::swap(String& s)
{
auto tmp_sz = s.sz;
s.sz = sz;
sz = tmp_sz;
auto tmp_p = s.p;
s.p = p;
p = tmp_p;
}
String& String::operator=(char c)
{
free();
sz = 1;
p = a.allocate(1);
*p = c;
return *this;
}
String& String::operator=(const char* cp)
{
free();
sz = strlen(cp);
p = a.allocate(sz);
uninitialized_copy(cp, cp + sz, p);
return *this;
}
String String::make_plural(size_t ctr, const String& s1, const String& s2)
{
return ctr > 1 ? s1 + s2 : s1;
}
String& String::operator=(const String& s)
{
free();
sz = s.sz;
p = a.allocate(s.sz);
uninitialized_copy(s.p, s.p + sz, p);
return *this;
}
ostream& print(ostream& os, const String& s)
{
auto it = s.begin();
while (it != s.end()) {
os << *it++;
}
return os;
}
ostream& operator<<(ostream& os, const String& s)
{
return print(os, s);
}
String add(const String& s1, const String& s2)
{
String res;
res.sz = s1.sz + s2.sz;
res.p = res.a.allocate(res.sz);
uninitialized_copy(s1.begin(), s1.end(), res.p);
uninitialized_copy(s2.begin(), s2.end(), res.p + s1.sz);
return res;
}
String operator+(const String& s1, const String& s2)
{
return add(s1, s2);
}
bool operator==(const String lhs, const String rhs)
{
return (lhs.size() == rhs.size() &&
equal(lhs.begin(), lhs.end(), rhs.begin()));
// 用于比较两个范围内的元素是否相等
// std::equal 的函数签名:template <class InputIterator1, class InputIterator2> bool equal(InputIterator1 first1, InputIterator1 last1, InputIterator2 first2);
// 这里 InputIterator1 和 InputIterator2 是输入迭代器类型,分别表示第一个范围和第二个范围的起始迭代器
// first1 和 last1 表示第一个范围的起始和结束迭代器(不包括 last1 对应的元素),first2 表示第二个范围的起始迭代器
}
bool operator!=(const String lhs, const String rhs)
{
return !(lhs == rhs);
}
bool operator<(const String lhs, const String rhs)
{
return lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), lhs.end());
// 用于比较两个序列的字典序。它比较两个范围内的元素,如果第一个范围在字典序上小于第二个范围,则返回 true,否则返回 false
// 首先,std::lexicographical_compare 从第一个范围的开始开始与第二个范围的相应位置的元素进行比较
// 如果两个范围中的对应位置上的元素相等,则继续比较下一个位置的元素
// 如果某个范围中的元素用尽了,但另一个范围还有剩余元素,则较短范围被认为是小于较长范围的
// 如果两个范围都用尽了(即到达了对应的 last1 和 last2),则返回 false,表示两个范围相等
// 如果在比较过程中发现某一位置上第一个范围的元素小于第二个范围的对应位置上的元素,则返回 true,表示第一个范围小于第二个范围
}
bool operator>(const String lhs, const String rhs)
{
return rhs < lhs;
}
bool operator<=(const String lhs, const String rhs)
{
return !(rhs < lhs);
}
bool operator>=(const String lhs, const String rhs)
{
return !(lhs < rhs);
}
4、赋值运算符
1、之前 已经介绍过拷贝赋值 和 移动赋值运算符之外,类还可以定义 其他赋值运算符 以使用别的类型作为右侧运算对象
在拷贝赋值 和 移动赋值运算符之外,标准库vector类 还定义了第三种赋值运算符,该运算符 接受花括号内的元素列表 作为参数
vector<string> v;
v = {"a", "an", "the"};
也可以把这个运算符添加到StrVec类中
class StrVec {
public:
StrVec &operator=(std::initializer_list<std::string>);
//其他成员与13.5节一致
};
为了 与内置类型的赋值运算符 保持一致(也与我们已经定义的拷贝赋值 和 移动赋值运算一致),这个新的赋值运算符 将返回其左侧运算对象的引用
StrVec &StrVec::operator=(initializer_list<string> il)
{
//alloc_n_copy分配内存空间 并从给定范围内拷贝元素
auto data = alloc_n_copy(il.begin(), il.end());
free(); //销毁对象中的元素并释放内存空间
elements = data.first; //更新数据成员使其指向新空间
first_free = cap = data.second;
return *this;
}
这个运算符 无须检查对象向自身的赋值,这是因为它的形参 initializer_list<string>
确保 il 与 this 所指的 不是同一个对象
使用:
initializer_list<string> v = {"a", "an", "the"};
// 创建 StrVec 对象 vec_list,默认初始化
StrVec vec_list; // 初始化语句,调用构造函数 StrVec()
vec_list = v; // 赋值语句,调用重载的赋值运算符函数 StrVec &operator=(initializer_list<string>)
2、可以重载赋值运算符。不论形参的类型是什么,赋值运算符都必须定义为成员函数
赋值运算符(operator=)通常被定义为类的成员函数,而不是全局函数或友元函数,是因为赋值运算符的操作涉及到类的内部状态和成员变量的复制,这需要对类的私有成员变量进行访问和修改。以下是赋值运算符必须是成员函数的几个原因:
- 访问私有成员变量:赋值运算符通常需要访问类的私有成员变量,以将右侧操作数的值复制到左侧操作数中。因为只有成员函数才能访问类的私有成员,所以赋值运算符必须是类的成员函数
- 修改对象状态:赋值运算符的目的是将右侧操作数的值复制到左侧操作数中,从而改变左侧对象的状态。这种修改对象状态的操作应该由成员函数来完成,因为它们有权访问和修改对象的内部状态
- 继承和多态:在继承和多态的情况下,如果赋值运算符是一个全局函数,那么在基类和派生类之间进行赋值时,可能会出现不正确的行为或者意外的行为。而将赋值运算符定义为成员函数可以保证正确地调用派生类的赋值运算符
- 自赋值检查:赋值运算符通常需要检查自我赋值的情况,并采取相应的措施来避免出现不确定的行为,例如使用自我赋值检查来避免资源泄漏。只有成员函数才能轻松地访问和比较对象的指针地址,从而进行自我赋值的检查
3、复合赋值运算符 不非得是 类的成员,不过我们还是倾向于 把包括复合赋值在内的所有赋值运算 都定义在类的内部。为了与内置类型的复合赋值 保持一致,类中的复合赋值运算符 也要返回 其左侧运算对象的引用
//作为成员的二元运算符:左侧运算对象绑定到隐式的this指针
//假定两个对象表示的是同一本书
Sales_data& Sales_data::operator+=(const Sales_data &rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
4、定义赋值运算符的一个新版本,使得我们能把一个表示 ISBN 的 string 赋给一个 Sales_data 对象
Sales_data& Sales_data::operator=(const std::string &isbn) {
bookNo = isbn;
return *this;
}
#include "Sales_data.h"
int main() {
std::string s = "C++ Primer 5th";
Sales_data b1("c++ primer", 10, 97.9);
b1 = s; // b1.operator=(s);
std::cout << b1 << std::endl;
return 0;
}
5、std::cin 读取输入时能够自动分割 int 和 char(以及其他基本数据类型)是因为在 C++ 中,输入流对象 std::cin 默认按照空白字符(例如空格、换行符、制表符等)进行分割。这意味着在读取输入时,std::cin 会自动忽略输入中的空白字符,并且将不同的数据类型分隔开来
#include <iostream>
int main() {
int num;
char ch;
std::cout << "Enter an integer and a character: ";
std::cin >> num >> ch;
std::cout << "Integer: " << num << std::endl;
std::cout << "Character: " << ch << std::endl;
return 0;
}
在这个示例中,当用户输入一个整数和一个字符时,std::cin 会自动将输入中的整数和字符分隔开来,并分别存储到变量 num 和 ch 中。即使用户在输入时没有明确使用空格将整数和字符分隔开,std::cin 也能够正确地识别输入并进行分割
6、移动语义的典型应用场景包括:
- 临时对象的优化:在函数调用时,如果有一个临时对象作为参数传递给函数,移动语义可以将该临时对象的资源直接移动到函数内部的对象中,而不是进行不必要的复制
- 动态内存管理:在动态内存分配和释放时,移动语义可以避免不必要的内存复制,提高程序的效率
- 容器的插入和删除操作:在容器的插入和删除操作中,移动语义可以将元素的资源从一个容器移动到另一个容器中,而不是进行复制
移动语义的使用涉及以下几个方面:
- 移动构造函数:移动构造函数允许我们从一个右值引用对象中“窃取”资源并将其移动到新创建的对象中。通常情况下,移动构造函数的参数是一个右值引用,并且应该将参数对象的资源移动到当前对象中,并且将参数对象的资源置为空。示例:
class Example {
public:
Example(Example&& other) noexcept {
// 将其他对象的资源移动到当前对象
this->data_ = std::move(other.data_);
// 将其他对象的资源置为空
other.data_ = nullptr;
}
private:
// 资源的指针
Resource* data_;
};
- 移动赋值运算符:移动赋值运算符与移动构造函数类似,它允许我们从一个右值引用对象中“窃取”资源并将其移动到当前对象中。移动赋值运算符通常在对象已经存在的情况下使用,用于将一个右值引用对象的资源移动到当前对象中,并且将右值引用对象的资源置为空。示例:
Example& operator=(Example&& other) noexcept {
if (this != &other) {
// 释放当前对象的资源
delete this->data_;
// 将其他对象的资源移动到当前对象
this->data_ = std::move(other.data_);
// 将其他对象的资源置为空
other.data_ = nullptr;
}
return *this;
}
- 标准库容器和算法:标准库中的容器和算法通常都支持移动语义。例如,可以使用 std::move() 来将对象移动到容器中,或者使用 std::make_move_iterator() 来创建移动迭代器,以实现对容器中元素的移动操作。示例:
std::string source_vec = {"apple", "banana", "cherry"};
std::vector<std::string> dest_vec;
// 将 source_vec 中的元素移动到 dest_vec 中
for (auto&& element : source_vec) {
dest_vec.push_back(std::move(element));
}
// 使用 std::make_move_iterator() 创建移动迭代器
std::vector<std::string> source_vec = {"apple", "banana", "cherry"};
std::vector<std::string> dest_vec(std::make_move_iterator(source_vec.begin()), std::make_move_iterator(source_vec.end()));
如果改成
for (auto element : source_vec) {
dest_vec.push_back(std::move(element));
}
还是使用移动操作吗
其实原来这里用了两次移动:
1)auto&& element : source_vec
也可以改成 for (auto&& element : std::make_move_iterator(source_vec.begin()), std::make_move_iterator(source_vec.end()))
改之后这个移动就没了
2)dest_vec.push_back(std::move(element));
- 使用移动语义提高性能:移动语义可以提高程序的性能和效率,尤其在处理临时对象和大型数据结构时表现突出。使用移动语义可以避免不必要的资源复制和额外的内存分配,从而提高程序的性能
5、下标运算符
1、通过 元素在容器中的位置访问元素,下标运算符 必须是成员函数
为了 与下标的原始定义兼容,下标运算符 通常以 所访问元素的引用作为返回值,这样做的好处是 下标可以出现在赋值运算符的任意一端
进一步,我们最好同时定义下标运算符的常量版本 和 非常量版本,当作用于 一个常量对象时,下标运算符返回常量引用 以确保我们不会给返回的对象赋值
引用允许 直接操作原始数据,而不是其副本。这一设计 允许下标运算符出现在赋值运算符的任意一端,并使其能够更改对象的实际内容
1)返回引用允许修改
当下标运算符返回一个对象的引用时,它实际上返回的是对象中某个元素的地址。这意味着通过这个引用,我们不仅可以读取元素的值,还可以修改它。例如:
std::vector<int> vec = {1, 2, 3};
vec[1] = 4; // 使用 operator[] 返回的引用来修改 vector 中的第二个元素
在上面的例子中,operator[] 返回了 vec 中第二个元素的引用。通过这个引用,我们可以将第二个元素的值修改为 4
2)赋值的两端
引用的另一个重要特性是它允许下标运算符出现在赋值语句的任意一端。这不仅包括赋值操作,还包括在表达式中使用:
- 作为赋值的左值:当下标运算符出现在赋值语句的左侧时,它必须返回一个引用,以便我们可以修改该位置的元素
vec[1] = 10; // 将 10 赋给 vec 的第二个元素
- 作为赋值的右值:当下标运算符出现在赋值语句的右侧时,它仍然可以返回一个引用,这样可以直接获取原始数据的值,而不是值的副本。这提高了效率,尤其是在处理大型对象时
int x = vec[1]; // 获取 vec 的第二个元素的值
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]; }
//其他成员与13.5一致
private:
std::string *elements; //指向数组首元素的指针
};
//假设svec是一个StrVec对象
const StrVec cvec = svec; //把svec的元素拷贝到cvec中
//如果svec中含有元素,对第一个元素运行string的empty函数
if (svec.size() && svec[0].empty()) {
svec[0] = "zero"; //正确:下标运算符返回string的引用
cvec[0] = "Zip"; //错误:对cvec取下标返回的是常量引用
}
2、为 StrBlob 类、StrBlobPtr 类、StrVec 类和 String 类定义下标运算符
StrBlob.h
#pragma once
#ifndef STRBLOB_H
#define STRBLOB_H
#include <string>
#include <vector>
#include <iostream>
#include <memory>
#include <initializer_list>
#include <stdexcept>
class StrBlob {
friend class StrBlobPtr;
// 重载运算符
friend bool operator==(const StrBlob& s1, const StrBlob& s2);
friend bool operator!=(const StrBlob& s1, const StrBlob& s2);
friend bool operator<(const StrBlob& s1, const StrBlob& s2);
friend bool operator<=(const StrBlob& s1, const StrBlob& s2);
friend bool operator>(const StrBlob& s1, const StrBlob& s2);
friend bool operator>=(const StrBlob& s1, const StrBlob& s2);
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob() :data(std::make_shared<std::vector<std::string>>()) {};
StrBlob(const std::initializer_list<std::string>& il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const std::string& t) { data->push_back(t); }
void pop_back();
std::string& front();
std::string& front() const;
std::string& back();
std::string& back() const;
StrBlobPtr begin();
StrBlobPtr end();
std::string& operator[](size_t t) {
check(t, "out of range"); // 别忘了考虑越界情况
return (*data)[t];
} // data->at(n)也可以
const std::string& operator[](size_t t) const { // 只有参数上的改变
check(t, "out of range");
return (*data)[t];
}
private:
std::shared_ptr<std::vector<std::string>> data;
void check(size_type i, const std::string& msg) const;
};
// 其实就是自己写的StrBlob的迭代器
class StrBlobPtr {
friend class StrBlob;
// 重载运算符
friend bool operator==(const StrBlobPtr& p1, const StrBlobPtr& p2);
friend bool operator!=(const StrBlobPtr& p1, const StrBlobPtr& p2);
friend bool operator<(const StrBlobPtr& p1, const StrBlobPtr& p2);
friend bool operator<= (const StrBlobPtr & p1, const StrBlobPtr & p2);
friend bool operator>(const StrBlobPtr& p1, const StrBlobPtr& p2);
friend bool operator>=(const StrBlobPtr& p1, const StrBlobPtr& p2);
public:
StrBlobPtr() :curr(0) {}
StrBlobPtr(StrBlob& a, size_t sz = 0) :wptr(a.data), curr(sz) {}
std::string& deref() const;
StrBlobPtr& incr();
bool operator!=(const StrBlobPtr& p) { return p.curr != curr; }
const std::string& operator[](size_t) const;
StrBlobPtr& operator++();
StrBlobPtr& operator++(int);
StrBlobPtr& operator--();
StrBlobPtr& operator--(int);
StrBlobPtr operator+(size_t) const; // 有const,返回“值”:返回值应该是一个新的对象,而不是原始对象的引用或指针
StrBlobPtr operator-(size_t) const;
StrBlobPtr& operator+=(size_t);
StrBlobPtr& operator-=(size_t);
std::string& operator*() const;
std::string* operator->() const; // 返回的是个指针,通过运算符*完成,这个函数也是个const
private:
std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const;
std::weak_ptr<std::vector<std::string>> wptr;
std::size_t curr;
};
StrBlob::StrBlob(const std::initializer_list<std::string>& il) :data(std::make_shared<std::vector<std::string>>(il)) {}
void StrBlob::check(size_type i, const std::string& msg) const {
if (i >= data->size())
throw std::out_of_range(msg);
}
void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
std::string& StrBlob::front() {
check(0, "front on empty StrBlob");
return data->front();
}
std::string& StrBlob::front() const {
check(0, "front on empty StrBlob");
return data->front();
}
std::string& StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
std::string& StrBlob::back() const {
check(0, "back on empty StrBlob");
return data->back();
}
std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string& msg) const
{
auto ret = wptr.lock();
if (!ret)
throw std::runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw std::out_of_range(msg);
return ret;
}
std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr];
}
StrBlobPtr& StrBlobPtr::incr()
{
check(curr, "increment past end of StrBlobPtr");
++curr;
return *this;
}
StrBlobPtr StrBlob::begin() {
return StrBlobPtr(*this);
}
StrBlobPtr StrBlob::end()
{
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
// 自定义运算符
bool operator==(const StrBlob& p1, const StrBlob& p2) {
return p1.data == p2.data;
}
bool operator!=(const StrBlob& p1, const StrBlob& p2) {
return !(p1 == p2);
}
bool operator==(const StrBlobPtr& p1, const StrBlobPtr& p2) {
auto pp1 = p1.wptr.lock(), pp2 = p2.wptr.lock();
return pp1 == pp2;
}
bool operator!=(const StrBlobPtr& p1, const StrBlobPtr& p2) {
return !(p1 == p2);
}
bool operator<(const StrBlob& p1, const StrBlob& p2) {
return *(p1.data) < *(p2.data);
}
bool operator<=(const StrBlob& p1, const StrBlob& p2) {
return *(p1.data) <= *(p2.data);
}
bool operator>(const StrBlob& p1, const StrBlob& p2) {
return *(p1.data) > *(p2.data);
}
bool operator>=(const StrBlob& p1, const StrBlob& p2) {
return *(p1.data) >= *(p2.data);
}
bool operator<(const StrBlobPtr& p1, const StrBlobPtr& p2) {
auto pp1 = p1.wptr.lock();
auto pp2 = p2.wptr.lock();
if (pp1 == pp2) {
if (!pp1)
return true;
else
return p1.curr < p2.curr;
}
else {
return false;
}
}
// 剩下的比较运算符都通过对其他比较运算符取反实现
bool operator>(const StrBlobPtr& p1, const StrBlobPtr& p2) {
return p2 < p1; // 换个顺序就行
}
bool operator>=(const StrBlobPtr& p1, const StrBlobPtr& p2) {
return !(p1 < p2);
}
bool operator<=(const StrBlobPtr& p1, const StrBlobPtr& p2) {
return !(p1 > p2);
}
const std::string& StrBlobPtr::operator[](size_t t) const {
check(t, "out of range");
auto res = this->wptr.lock();
return (*res)[t];
}
StrBlobPtr& StrBlobPtr::operator++() {
// 如果 curr 已经指向了容器的尾后位置,则无法递增它
check(curr, "after after end of StrBlobPtr");
curr++;
return *this;
}
StrBlobPtr& StrBlobPtr::operator++(int) {
// 此处无需检查有效性,调用前置递增运算时才需要检查
StrBlobPtr res = *this; // 由一个临时变量保存结果
++*this; // 使用StrBlobPtr的前置++
return res;
}
StrBlobPtr& StrBlobPtr::operator--() {
// 先减后检查,跟++不同,因为++尾后元素还成立,--不是
curr--;
check(curr, "before begin of StrBlobPtr");
return *this;
}
StrBlobPtr& StrBlobPtr::operator--(int) {
// 此处无需检查有效性,调用前置递增运算时才需要检查
StrBlobPtr res = *this; // 由一个临时变量保存结果
--*this;
return res;
}
StrBlobPtr& StrBlobPtr::operator+=(size_t t) {
curr += t;
check(curr, "up out of range");
return *this;
}
StrBlobPtr& StrBlobPtr::operator-=(size_t t) {
curr -= t;
check(curr, "down out of range");
return *this;
}
// 对对象本身不做修改,另外返回一个随机变量,自然不需要返回引用
StrBlobPtr StrBlobPtr::operator+(size_t t) const {
StrBlobPtr tmp = *this;
tmp.curr += t;
return tmp;
}
StrBlobPtr StrBlobPtr::operator-(size_t t) const {
StrBlobPtr tmp = *this;
tmp.curr -= t;
return tmp;
}
std::string& StrBlobPtr::operator*() const {
check(curr, "derefernce past end");
auto tmp = wptr.lock();
return (*tmp)[curr];
}
std::string* StrBlobPtr::operator->() const {
// 将实际工作委托给解引用运算符
return & this->operator*();
}
#endif
14.26_StrBlob.cpp
#include "StrBlob.h"
using namespace std;
int main()
{
StrBlob s1 = { "a", "an", "c" };
s1[1] = "b";
for (auto it = s1.begin(); it != s1.end(); it.incr()) {
cout << it.deref() << " ";
}
cout << endl;
return 0;
}
运行结果
1)为什么这个类不能定义string &operator[](size_t)
在考虑 StrBlobPtr 类中是否应该定义 string &operator[](size_t)
时,首先要理解 StrBlobPtr 的作用和设计目的。StrBlobPtr 是设计为 StrBlob 类的伴随指针类,用于提供类似指针的行为来遍历和访问存储在 StrBlob 中的 std::vector<std::string>
数据。这样的设计通常意味着 StrBlobPtr 应该通过像 deref() 和 incr() / decr() 这样的操作来间接管理对元素的访问
const string &operator[](size_t) const
可以在不修改对象的情况下访问特定的字符串,这是一个只读操作。它符合 StrBlobPtr 提供受限访问的设计目的,并保证不会修改底层数据
2)对于 StrVec 类构造函数:
StrVec::StrVec(initializer_list<string> il)
修改为使用引用,特别是 const 引用是更优的选择:
StrVec::StrVec(const initializer_list<string>& il)
3)在 C++ 中,当一个类有一个成员函数重载,其中一个版本有 const 修饰符而另一个没有时,可以根据对象是常量还是非常量来调用相应的函数版本。这是通过对象的常量性来自动决定的,但也可以通过显式转换或特定对象声明来控制调用哪个版本
class Data {
public:
int value;
// 非常量成员函数,允许修改成员
int& getValue() {
return value;
}
// 常量成员函数,不允许修改任何成员
const int& getValue() const {
return value;
}
};
调用方式
- 非常量对象调用非常量版本:
Data d;
d.getValue() = 10; // 调用非常量版本
- 常量对象只能调用常量版本:
const Data d_const;
int val = d_const.getValue(); // 调用常量版本
// d_const.getValue() = 10; // 错误:不能调用非常量函数
- 使用 const_cast 强制调用:如果你需要对一个非常量对象强制调用其常量版本,可以使用 const_cast 来转换对象引用或指针
Data d;
const int& val = const_cast<const Data&>(d).getValue(); // 强制调用常量版本
- 相反,如果你确定一个常量对象实际上应该是非常量的(需要非常小心地使用,因为这可能违反设计原则),可以使用 const_cast 移除常量性:
const Data d_const;
int& val = const_cast<Data&>(d_const).getValue(); // 移除常量性,风险很高
4)对于两个函数都是非成员函数的情况,它们不能使用 const 限定符来区分重载版本,而应该使用不同的函数名称或参数列表来区分它们
StrVec.h
#pragma once
#include <string>
#include <memory>
#include <iostream>
class StrVec {
friend bool operator==(const StrVec& s1, const StrVec& s2);
friend bool operator!=(const StrVec& s1, const StrVec& s2);
friend bool operator<(const StrVec & s1, const StrVec & s2);
friend bool operator<=(const StrVec& s1, const StrVec& s2);
friend bool operator>(const StrVec& s1, const StrVec& s2);
friend bool operator>=(const StrVec& s1, const StrVec& s2);
public:
StrVec() :
elements(nullptr), first_free(nullptr), cap(nullptr) {}
StrVec(const StrVec&);
StrVec& operator=(const StrVec&);
~StrVec();
void push_back(const std::string&);
size_t size() const { return first_free - elements; }
size_t capacity() const { return cap - elements; }
std::string* begin() const { return elements; }
std::string* end() const { return first_free; }
void reserve(size_t n);
void resize(size_t n);
void resize(size_t n, const std::string& s);
StrVec(std::initializer_list<std::string>);
std::string& operator[](size_t t) { return elements[t]; } // 直接对指向首地址的指针使用下标运算符
const std::string& operator[](size_t t) const { return elements[t]; }
private:
static std::allocator<std::string> alloc;
void chk_n_alloc();
std::pair<std::string*, std::string*> alloc_n_copy(const std::string*, const std::string*);
void free();
void reallocate();
std::string* elements;
std::string* first_free;
std::string* cap;
};
StrVec.cpp
#include "StrVec.h"
#include <algorithm>
using namespace std;
std::allocator<std::string> StrVec::alloc;
void StrVec::chk_n_alloc()
{
if (size() == capacity())
reallocate();
}
void StrVec::push_back(const string& s) {
chk_n_alloc();
alloc.construct(first_free++, s);
}
pair<string*, string*> StrVec::alloc_n_copy(const string* b, const string* e)
{
auto data = alloc.allocate(e - b);
return { data, uninitialized_copy(b, e, data) };
}
void StrVec::free()
{
if (elements) {
for_each(elements, first_free, [](string& s) { alloc.destroy(&s); });
}
alloc.deallocate(elements, cap - elements);
}
StrVec::StrVec(const StrVec& s)
{
auto newdata = alloc_n_copy(s.begin(), s.end());
elements = newdata.first;
first_free = newdata.second;
cap = newdata.second;
}
StrVec::~StrVec() { free(); }
StrVec& StrVec::operator=(const StrVec& rhs)
{
auto data = alloc_n_copy(rhs.begin(), rhs.end());
free();
elements = data.first;
first_free = data.second;
cap = data.second;
return *this;
}
void StrVec::reallocate()
{
auto newcapacity = size() ? 2 * size() : 1;
auto newdata = alloc.allocate(newcapacity);
auto dest = newdata;
auto elem = elements;
for (size_t i = 0; i != size(); ++i)
alloc.construct(dest++, std::move(*elem++));
free();
elements = newdata;
first_free = dest;
cap = elements + newcapacity;
}
void StrVec::reserve(size_t n) {
if (size() < n) {
reallocate();
}
}
void StrVec::resize(size_t n) {
resize(n, std::string());
}
void StrVec::resize(size_t n, const string& s)
{
if (size() > n) {
while (first_free != elements + n) {
alloc.destroy(--first_free);
}
alloc.destroy(first_free);
}
else {
while (size() < n) {
push_back(s);
}
}
}
StrVec::StrVec(std::initializer_list<std::string> il) {
auto newdata = alloc_n_copy(il.begin(), il.end());
elements = newdata.first;
first_free = newdata.second;
cap = newdata.second;
}
bool operator==(const StrVec& s1, const StrVec& s2) {
if (s1.size() != s2.size()) {
return false;
}
else {
for (auto it1 = s1.begin(), it2 = s2.begin(); it1 != s1.end() && it2 != s2.end(); it1++, it2++)
{
if (*it1 != *it2)
return false;
}
return true;
}
}
bool operator!=(const StrVec& s1, const StrVec& s2) {
return !(s1 == s2);
}
// 之前的 string 都相等,当前 string 更小,s1 中的所有 string 都与 s2 中的 string 相等,且 s1 更短符合条件
bool operator<(const StrVec& s1, const StrVec& s2) {
auto it1 = s1.begin(), it2 = s2.begin();
for (; it1 != s1.end() && it2 != s2.end(); it1++, it2++)
{
if (*it1 < *it2)
return true;
else if (*it1 > *it2)
return false;
}
if (it1 == s1.end() && it2 != s2.end()) // 一定要同时判断s2
return true;
else
return false;
}
// 比较运算符只要写出一个,就可以直接利用这一个写出剩下的三个
bool operator>(const StrVec& s1, const StrVec& s2) {
return s2 < s1;
}
bool operator<=(const StrVec& s1, const StrVec& s2) {
return !(s1 > s2);
}
bool operator>=(const StrVec& s1, const StrVec& s2) {
return !(s1 < s2);
}
14.26_StrVec.cpp
#include "StrVec.h"
using namespace std;
int main()
{
StrVec sv1 = { "c++", "primer", "4th" };
StrVec sv2 = { "c++", "primer", "5th" };
if (sv1 == sv2) {
cout << "==" << endl;
}
else if (sv1 < sv2) {
cout << "<" << endl;
}
cout << sv1[2] << endl;
return 0;
}
运行结果
6、递增和递减运算符
1、在迭代器类中 通常会实现递增运算符(++)和递减运算符(–),C++语言 并不要求递增和递减运算符必须是类的成员,但是因为它们改变的正好是 所操作对象的状态,所以建议 将其设定为成员函数
对于内置类型来说,递增和递减运算符 既有前置版本 也有后置版本。同样,我们也应该 为类定义两个版本的递增 和 递减运算符
class StrBlobPtr {
public:
//递增和递减运算符
StrBlobPtr& operator++();
StrBlobPtr& operator--(); //前置运算符
//其他成员和之前的版本一致
};
为了 与内置版本保持一致,前置运算符 应该返回 递增或递减后对象的引用
//前置版本:返回递增/递减对象的引用
StrBlobPtr& StrBlobPtr::operator++()
{
//如果curr已经指向了容器的尾后位置,则无法递增它
check(curr, "increment past end of strBlobPtr");
++curr; //将curr在当前状态下向前移动一个元素
return *this;
}
StrBlobPtr& StrBlobPtr::operator--()
{
//如果curr是0,则继续递减它将产生一个无效下标
--curr; //将curr在当前状态下向后移动一个元素
check(curr, "decrement past begin of strBlobPtr"); // check位置不同
return *this;
}
先递减curr,如果curr(一个无符号数)已经是0了,那么我们传递给check的值将是 一个表示无效下标的非常大的正数值(自然越界了)
2、区分前置和后置运算符:即普通的重载形式无法区分这两种情况。后置版本 接受一个额外的(不被使用)int类型的形参。当我们使用 后置运算符时,编译器为 这个形参提供一个值为0的实参。这个形参的唯一作用就是 区分前置版本和后置版本的函数,而不是真的要在实现后置版本时参与运算
class strBlobPtr {
public:
//递增和递减运算符
StrBlobPtr operator++(int); //后置运算符
StrBlobPtr operator--(int);
//其他成员和之前的版本一致
};
为了与内置版本保持一致,后置运算符 应该返回对象的原值(递增或递减之前的值),返回的形式是 一个值而非引用
//后置版本:递增/递减对象的值但是返回原值
StrBlobPtr StrBlobPtr::operator++(int)
{
//此处无须检查有效性,调用前置递增运算时才检查
StrBlobPtr ret = *this; //记录当前的值
++*this; //向前移动一个元素,前置++需要检查递增的有效性
return ret; //返回之前记录的状态
}
StrBlobPtr StrBlobPtr::operator--(int)
{
//此处无须检查有效性,调用前置递减运算时才检查
StrBlobPtr ret = *this; //记录当前的值
--*this; //向后移动一个元素,前置--需要检查递减的有效性
return ret; //返回之前记录的状态
}
因为我们不会用到int形参,所以 无须为其命名
3、如果我们想通过函数调用的方式 调用后置版本,则必须 为它的整型参数传递一个值:
StrBlobPtr p(a1); //指向a1中的vector
p.operator++(0); //调用后置版本的operator++
p.operator++(); //调用前置版本的operator++
尽管传入的值 通常会被运算符函数忽略,但却必不可少,因为编译器 只有通过它才能知道 应该使用后置版本
4、为什么不定义const 版本的递增和递减运算符?
const的StrBlob不能改变成员变量,而递增和递减运算符需要改变其状态,本身就是矛盾的
7、成员访问运算符
1、解引用运算符(*)和 箭头运算符(->)
class StrBlobPtr {
public:
std::string& operator*() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; //(*p)是对象所指的vector
}
std::string* operator->() const
{
// 将实际工作委托给解引用运算符
return &this->operator*();
}
//其他成员与之前的版本一致
}
箭头运算符 必须是类的成员。解引用运算符 通常也是类的成员,尽管并非必须如此
将这两个运算符定义成了const成员,获取一个元素并不会改变 StrBlobPtr 对象的状态
StrBlob a1 = {"hi", "bye", "now"};
StrBlobPtr p(a1); //指向a1中的vector
*p = "okay"; //给a1的首元素赋值
cout << p->size() << endl; //打印4,这是a1首元素的大小
cout << (*p).size() << endl;//等价于p->size()
2、对箭头运算符返回值的限定:能令 operator* 完成 任何我们指定的操作。比如 可以让operator*返回一个固定值42。箭头运算符 则不是这样,它永远不能丢掉成员访问 这个最基本的含义。当我们重载箭头时,可以改变的是箭头 从哪个对象当中获取成员,而箭头获取成员这一事实则永远不变
为什么返回指针就可以访问跟在后面的类成员
当你使用箭头运算符 (->) 访问类的成员时,编译器 会自动将其转换为 对指向对象的指针的成员访问。这意味着 如果你重载了箭头运算符 并返回了一个指针,那么对于这个指针,箭头运算符的行为 将与使用原始对象的箭头运算符相同
// 使用箭头运算符调用成员函数
smartPtr->print(); // 等同于 smartPtr.operator->()->print();
smartPtr.operator->() 返回了指向 MyClass 对象的指针,然后 print() 函数被调用在这个对象上
3、对于 形如 point->mem 的表达式来说,point必须是 指向类对象的指针 或者是 一个重载了operator->的类的对象。根据point类型的不同,point->mem
分别等价于
(*point).mem; //point是一个内置的指针类型
point.operator()->mem; //point是类的一个对象
除此之外,代码都将发生错误
是point.operator()->mem 不是 point.operator->()mem
- 如果point是指针,则我们应用 内置的箭头运算符,表达式等价于(*point).mem
- 如果point是定义了operator->的类的一个对象,则我们使用 point.operator->() 的结果来获取mem
其中,如果该结果是一个指针,则执行第1步;如果该结果 本身含有重载的 operator->(),则重复调用当前步骤
最终,当这一过程结束时 程序或者返回了所需的内容,或者返回一些表示程序错误的信息
重载的箭头运算符 必须返回类的指针 或者 自定义了箭头运算符的某个类的对象
4、为 StrBlobPtr 类和定义的 ConstStrBlobPtr 的类 分别添加解引用运算符和箭头运算符。注意:因为 ConstStrBlobPtr 的数据成员指向const vector,所以ConstStrBlobPtr 中的运算符必须返回常量引用
加const
class ConstStrBlobPtr;
class StrBlob
{
friend class ConstStrBlobPtr;
...
}
class ConstStrBlobPtr {
// 练习 14.16
friend bool operator==(const ConstStrBlobPtr &lhs, const ConstStrBlobPtr &rhs);
friend bool operator!=(const ConstStrBlobPtr &lhs, const ConstStrBlobPtr &rhs);
// 练习 14.18
friend bool operator<(const ConstStrBlobPtr &s1, const ConstStrBlobPtr &s2);
friend bool operator<=(const ConstStrBlobPtr &s1, const ConstStrBlobPtr &s2);
friend bool operator>(const ConstStrBlobPtr &s1, const ConstStrBlobPtr &s2);
friend bool operator>=(const ConstStrBlobPtr &s1, const ConstStrBlobPtr &s2);
public:
ConstStrBlobPtr() : curr(0) {}
ConstStrBlobPtr(const StrBlob &a, size_t sz = 0) : wptr(a.data), curr(sz) {}
string& deref() const;
ConstStrBlobPtr& incr(); // 前缀递增
ConstStrBlobPtr& decr(); // 后缀递减
// 练习 14.26
const string &operator[](size_t) const;
// 练习 14.27
ConstStrBlobPtr &operator++(); // 前置运算符
ConstStrBlobPtr &operator--();
ConstStrBlobPtr &operator++(int); // 后置运算符
ConstStrBlobPtr &operator--(int);
// 练习 14.28
ConstStrBlobPtr &operator+=(size_t);
ConstStrBlobPtr &operator-=(size_t);
ConstStrBlobPtr operator+(size_t) const;
ConstStrBlobPtr operator-(size_t) const;
// 练习 14.30
const string &operator*() const;
const string *operator->() const;
private:
// 若检查成功,check 返回一个指向 vector 的 shared_ptr
shared_ptr<vector<string>> check(size_t, const string&) const;
// 保存一个 weak_ptr,意味着底层 vector 可能会被销毁
weak_ptr<vector<string>> wptr;
size_t curr; // 在数组中的当前位置
};
bool operator==(const ConstStrBlobPtr &lhs, const ConstStrBlobPtr &rhs);
bool operator!=(const ConstStrBlobPtr &lhs, const ConstStrBlobPtr &rhs);
bool operator<(const ConstStrBlobPtr &s1, const ConstStrBlobPtr &s2);
bool operator<=(const ConstStrBlobPtr &s1, const ConstStrBlobPtr &s2);
bool operator>(const ConstStrBlobPtr &s1, const ConstStrBlobPtr &s2);
bool operator>=(const ConstStrBlobPtr &s1, const ConstStrBlobPtr &s2);
const string& ConstStrBlobPtr::operator*() const {
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p) 是对象所指的 vector
}
const string* ConstStrBlobPtr::operator->() const {
// 将实际工作委托给解引用运算符
return & this->operator*();
}
14.30.cpp
#include "StrBlob.h"
using namespace std;
int main()
{
StrBlob a1 = { "hi", "bye", "now" };
StrBlobPtr p1(a1); // p1 指向 a1 中的 vector
*p1 = "okay"; // 给 a1 的首元素赋值
cout << p1->size() << endl; // 打印 4,这是 a1 首元素的大小
cout << (*p1).size() << endl; // 等价于 p1->size()
return 0;
}
运行结果
5、trBlobPtr 类没有定义拷贝构造函数、赋值运算符和析构函数,为什么?
StrBlobPtr的数据成员的类型为std::weak_ptr<std::vectorstd::string>
和内置类型,它的所有数据成员都能被合成的拷贝控制成员 恰当的拷贝、赋值和销毁
6、定义一个类令其含有指向 StrBlobPtr 对象的指针,为这个类定义重载的建投运算符
#include "StrBlob.h"
using namespace std;
class MyClass {
public:
MyClass() = default;
MyClass(StrBlobPtr* p) :pointer(p) {}
StrBlobPtr& operator*() const {
// return *this->pointer; // 就是把pointer解引用
return *pointer;
}
StrBlobPtr* operator->() const {
// return & this->operator*(); // 这个跟之前的一致
return &operator*();
}
/*StrBlobPtr* operator->() const {
return this->pointer; // 也可以的,this->可以不加
}*/
private:
StrBlobPtr* pointer = nullptr;
};
int main()
{
StrBlob s({ "abcd", "b", "c" });
StrBlobPtr sp(s); // 只指向第一个字符串
MyClass mc(&sp);
cout << mc->operator->()->front() << endl;
cout << mc->operator->()->back() << endl;
return 0;
}
成员访问运算符 -> 和 . 用来访问类的成员,如方法和属性。当你在类的成员函数内部调用另一个成员函数或访问一个成员变量时,默认的上下文就是当前对象的上下文,即 this 指针所指向的对象
在 return &operator*();
中,operator*() 是当前类 StrBlobPtr 的一个成员函数。在成员函数内调用另一个成员函数时,可以省略 this-> 前缀,因为它是默认的。所以 operator*() 实际上是 this->operator*() 的简写。这是 C++ 语言设计的一部分,旨在简化类内部成员的访问
解释 mc->operator->()->back()
:
首先,mc 是一个对象,是 MyClass 类的实例。因此,mc-> 调用 MyClass 类的重载的箭头成员访问运算符 operator->()
在 operator->() 中,我们又调用了 operator*(),返回 StrBlobPtr 对象的引用的地址
然后,我们再次使用箭头成员访问运算符 ->,用于访问 StrBlobPtr 对象的成员函数或成员变量。在这种情况下,我们访问了 StrBlobPtr 对象指向的 StrBlob 对象的成员函数 back()
因此,mc->operator->()->back() 的作用是:
首先,通过 mc 对象调用 operator->(),得到 StrBlobPtr 对象的引用
然后,通过 operator->() 访问了 StrBlobPtr 对象的成员函数 back(),返回了 StrBlob 对象的最后一个元素的引用
class MyClass
{
public:
std::string* operator->() const
{
return ptr->operator->();
}
private:
StrBlobPtr *ptr;
}
也可以
8、函数调用运算符
1、如果类 重载了 函数调用运算符,则我们可以像 使用函数一样 使用该类的对象。因为这样的类 同时也能存储状态,所以与普通函数相比它们更加灵活
下面这个名为 absInt 的struct含有一个调用运算符,该运算符负责 返回其参数的绝对值
struct absInt {
int operator()(int val) const {
return val < 0 ? -val : val;
}
};
int i = -42;
absInt absobj; //含有函数调用运算符的对象
int ui = absobj(i); //将i传递给absobj.operator()
即使absObj只是一个对象而非函数,我们也能“调用”该对象。调用对象 实际上是在运行重载的调用运算符
2、函数调用运算符 必须是 成员函数。一个类可以定义 多个不同版本的调用运算符,相互之间 应该在参数数量 或 类型上有所区别
3、如果类定义了 调用运算符,则该类的对象 称作函数对象。因为可以 调用这种对象,所以我们说 这些对象的“行为像函数一样”
4、和其他类一样,函数对象类 除了 operator() 之外 也可以包含其他成员。函数对象类通常 含有一些数据成员,这些成员被用于定制调用运算符中的操作
将定义一个打印string实参内容的类。默认情况下,我们的类会 将内容写入到c out中,每个string之间以空格隔开。同时也允许类的用户 提供其他可写入的流及其他分隔符
class PrintString {
public:
PrintString(ostream &o = cout, char c = ' '):
os(o), sep(c) { }
void operator()(const string &s) const { os << s << sep; }
private:
ostream &os; //用于写入的目的流
char sep; //用于将不同输出隔开的字符
};
类有一个构造函数,它接受 一个输出流的引用 以及 一个用于分隔的字符,这两个形参的默认实参分别是 cout 和 空格
PrintString printer; //使用默认值,打印到cout
printer(s); //在cout中打印s,后面跟一个空格
PrintString errors(cerr, '\n');
errors(s); //在cerr中打印s,后面跟一个换行符
函数对象 常常作为泛型算法的实参。例如,可以使用标准库for_each算法 和我们自己的PrintString类来打印容器的内容
for_each(vs.begin(), vs.end(), PrintString(cerr, '\n'));
for_each 的第三个实参是类型PrintString的一个临时对象,其中我们用cerr和换行符 初始化了该对象。当程序调用for_each时,将会把vs中的每个元素依次打印到 cerr 中,元素之间以换行符分隔
5、编写一个类似 PrintString 的类,令其从 istream 中读取一行输入,然后返回一个表示我们所读内容的 string 。如果读取失败,返回空 string。并读取标准输入,将每一行保存为 vector 的一个元素
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Print {
public:
Print(istream& gin = cin) : in(gin) {}
string operator()() {
string s;
getline(in, s);
return in ? s : string();
}
private:
istream& in = cin;
};
int main()
{
Print p;
vector<string> vec;
string tmp;
while (!(tmp = p()).empty())
vec.push_back(tmp);
for (string &ss : vec) {
cout << ss << endl;
}
return 0;
}
运行结果
6、编写一个类令其检查两个值是否相等。使用该对象及标准库算法编写程序,令其替换某个序列中具有给定值的所有实例
#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;
class isEqual {
public:
isEqual(int ii) : i(ii) {}
bool operator()(int a) {
return a == i;
}
private:
int i;
};
int main()
{
vector<int> vec = { 3, 2, 1, 4, 3, 7, 8, 6 };
for (auto& e : vec) {
cout << e << " ";
}
cout << endl;
replace_if(vec.begin(), vec.end(), isEqual(3), 5); // 把3替换成5
for (auto& e : vec) {
cout << e << " ";
}
cout << endl;
return 0;
}
运行结果
8.1 lambda是函数对象
1、使用一个Print string对象作为调用for each的实参,这一用法类似于 使用lambda表达式的程序。当我们编写了 一个lambda后,编译器 将该表达式 翻译成一个未命名类的未命名对象
// 根据单词的长度对其进行排序,对于长度相同的单词 按照字母表顺序排序
stable_sort(words.begin(), words.end(),
[](const string &a, const string &b)
{ return a.size() < b.size(); });
其行为 类似于 下面这个类的一个未命名对象
class ShorterString {
public:
bool operator()(const string &s1, const string &s2) const
{ returns s1.size() < s2.size(); }
};
产生的类 只有一个函数调用运算符成员,它负责 接受两个string并比较它们的长度,它的形参列表 和 函数体 与lambda表达式完全一样
默认情况下 lambda 不能改变它捕获的变量。因此 在默认情况下,由lambda产生的类当中的函数调用运算符 是 一个const成员函数。如果lambda被声明为可变的,则调用运算符 就不是const的了
lambda被声明为可变:在C++中,当你声明一个lambda表达式为可变,它允许你在lambda内部 修改捕获的变量,即使这些变量是 以值的方式捕获的。通常,lambda表达式中 以值方式捕获的变量在函数体内是常量,不能修改。使用mutable关键字可以改变这一行为
#include <iostream>
int main() {
int x = 10;
auto mutableLambda = [x]() mutable {
x += 5; // 因为lambda是可变的,所以我们可以修改x
std::cout << "Inside lambda: " << x << std::endl;
};
mutableLambda(); // 调用lambda,输出将是 "Inside lambda: 15"
std::cout << "Outside lambda: " << x << std::endl; // x的值不变,输出将是 "Outside lambda: 10"
return 0;
}
mutableLambda 是一个捕获了x(以值方式)的lambda表达式
由于声明了mutable,这个lambda内部可以修改x的值。请注意,这里修改的是lambda内部的一个局部拷贝,外部的x并不会被改变
当在外部打印x的值时,会发现它仍然是原来的值,说明lambda内部的修改不影响外部变量
stable_sort(words.begin(), words.end(), ShorterString());
第三个实参是 新构建的 ShorterString对象,当stable_sort内部的代码每次比较两个string时 就会“调用”这一对象,此时该对象 将调用运算符的函数体
2、表示lambda 及 相应捕获行为的类:当一个lambda表达式 通过引用捕获变量时,将由程序负责确保lambda执行时 引用所引的对象确实存在。因此,编译器可以直接使用 该引用 而无须在lambda产生的类中 将其存储为数据成员
当Lambda表达式 通过引用捕获变量时,它可以直接访问并修改这些变量的原始值,而不是在Lambda表达式的作用域内 创建它们的副本。这样的捕获方式使得Lambda表达式能够 对外部作用域中的变量进行实时和直接的更改
在Lambda表达式中,可以通过在捕获列表中使用符号&来指定引用捕获。具体可以分为以下几种方式:
- [&]:捕获外部作用域中所有变量(那些在Lambda表达式体中使用的变量)为引用
- [&var]:仅将var这一变量以引用方式捕获,其他变量不捕获或以其他方式捕获
- [&, var1]:默认以引用方式捕获所有外部变量,但将var1以值方式捕获
- [=, &var2]:默认以值方式捕获所有外部变量,但将var2以引用方式捕获
#include <iostream>
int main() {
int x = 10;
int y = 20;
auto refLambda = [&x, &y]() {
x += 5; // 直接修改x的值
y *= 2; // 直接修改y的值
};
refLambda(); // 调用Lambda表达式
std::cout << "x: " << x << std::endl; // 输出修改后的x值,15
std::cout << "y: " << y << std::endl; // 输出修改后的y值,40
return 0;
}
相反,通过值捕获的变量 被拷贝到lambda中,这种lambda产生的类 必须为 每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值 来初始化数据成员
// 获得第一个指向满足条件元素的迭代器,该元素满足size() is >= sz
auto wc = find_if(words.begin(), words.end(),
[sz] const string &a)
{ return a.size() >= sz; });
该lambda表达式产生的类将形如
class SizeComp {
SizeComp(size_t n):sz(n) { } //该形参对应捕获的变量
//该调用运算符的返回类型、形参和函数体都与lambda一致
bool operator()(const string &s) const
{return s.size() >= sz; }
private:
size_t sz; //该数据成员对应通过值捕获的变量
};
和我们的 ShorterString类 不同,上面这个类 含有一个数据成员 以及 一个用于初始化该成员的构造函数。这个合成的类 不含有默认构造函数,因此要想使用这个类必须提供一个实参:
//获得第一个指向满足条件元素的选代器,该元素满足size() is >= sz
auto wc = find_if(words.begin(), words.end(), SizeComp(sz));
Lambda 表达式通过引用捕获变量时,程序确保 Lambda 执行时引用的对象确实存在。而对于通过值捕获的类,需要为每个值捕获的变量建立对应的数据成员并创建构造函数
通过在捕获列表中使用变量名(不带 &),Lambda 表达式可以捕获外部作用域中的变量的值。这会创建变量的副本,并将其存储在 Lambda 表达式的闭包中。对于每个值捕获的变量,Lambda 表达式都需要为其创建对应的数据成员,并在构造函数中初始化这些数据成员
值捕获的类需考虑对象生命周期:由于值捕获会在 Lambda 表达式中创建变量的副本,需要确保 Lambda 表达式的对象在使用期间保持有效。如果对象在 Lambda 表达式执行期间已经失效,将导致未定义行为
构造函数初始化捕获的变量:对于通过值捕获的类,需要在构造函数中初始化捕获的变量。这确保了 Lambda 表达式在执行时可以访问正确的值
lambda表达式产生的类 不含默认构造函数、赋值运算符 及 默认析构函数;它是否含有 默认的拷贝 / 移动构造函数 则通常要视捕获的数据成员类型而定
这是因为 Lambda 表达式生成的闭包类型是 匿名且唯一的,它的特性 不允许使用这些特殊成员函数。Lambda 表达式生成的闭包类型 是通过 operator() 运算符来调用的,而不是通过构造函数。因此,对于 Lambda 产生的闭包类型,你只能调用它的operator() 运算符
这意味着你无法像普通类那样通过默认的构造函数来创建 Lambda 表达式的闭包对象,也不能使用赋值运算符进行赋值操作,也不能显式地调用析构函数来销毁 Lambda 表达式的闭包对象
#include <iostream>
int main() {
// Lambda 表达式生成的闭包类型
auto lambda = []() {
std::cout << "Lambda called" << std::endl;
};
// 调用 Lambda 表达式
lambda();
// 下面的代码将无法通过编译,因为 Lambda 闭包类型没有默认的构造函数、赋值运算符或默认析构函数
// auto lambda2 = lambda; // 错误:无法使用赋值运算符
// auto lambda3(lambda); // 错误:无法使用复制构造函数
// lambda = lambda; // 错误:无法使用赋值运算符
// lambda.~decltype(lambda)(); // 错误:无法显式调用析构函数
return 0;
}
3、编写一个类 令其检查某个给定的 string 对象的长度是否与一个阈值相等。使用该对象编写程序,统计并报告在输入的文件中长度为1的单词有多少个,长度为2的单词有多少个、…、长度为10的单词有多少个
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <string>
#include <stdexcept>
#include <algorithm>
using namespace std;
class getWordLen {
public:
getWordLen(int len) : length(len) {}
int operator()(const string& s) { return s.length() == length; } // 跟count_if合起来用,这样就只要返回true/false
private:
int length;
};
int main(int argc, char *argv[])
{
if (argc != 2) {
cerr << "输入文件名" << endl;
return -1;
}
ifstream ifs(argv[1]);
if (!ifs) {
cerr << "fail to open the file" << endl;
return -1;
}
string line;
string word;
vector<string> vec;
while (getline(ifs, line)) {
istringstream iss(line);
while (iss >> word) {
vec.push_back(word);
}
}
int minLen = 1;
int maxLen = 10;
for (int i = minLen; i <= maxLen; i++) {
getWordLen gwl(i);
cout << i << ": " << count_if(vec.begin(), vec.end(), gwl) << endl;
}
return 0;
}
运行结果
其报告长度在1到9之间的单词有多少个、长度在10以上的单词有多少个
#include <iostream>
#include <fstream>
#include <algorithm>
#include <string>
#include <vector>
#include <sstream>
#include <stdexcept>
using namespace std;
class getCount {
public:
getCount(int mi, int ma) :minLen(mi), maxLen(ma){}
bool operator()(string& s) {
return s.length() >= minLen && s.length() <= maxLen;
}
private:
int minLen;
int maxLen;
};
class getCount2 {
public:
getCount2(int mi):minLen(mi) {}
bool operator()(string& s) {
return s.length() >= minLen;
}
private:
int minLen;
};
int main(int argc, char** argv)
{
if (argc != 2) {
cerr << "no input file name" << endl;
return -1;
}
ifstream ifs(argv[1]);
if (!ifs) {
cerr << "load file error" << endl;
return -1;
}
vector<string> vec;
string line;
string word;
while (getline(ifs, line)) {
istringstream iss(line);
while (iss >> word) {
vec.push_back(word);
}
}
getCount gc(1, 9);
getCount2 gc2(10);
cout << "1-9: " << count_if(vec.begin(), vec.end(), gc) << endl;
cout << "10-: " << count_if(vec.begin(), vec.end(), gc2) << endl;
return 0;
}
运行结果
4、重新编写10.3.2节的biggies 函数,使用函数对象替换其中的 lambda 表达式
#include <vector>
#include <algorithm>
#include <iostream>
#include <string>
using std::string;
using std::vector;
using std::cout;
using std::endl;
class Shorter {
public:
bool operator()(const string& a, const string& b) {
return a.size() < b.size();
}
};
class NoShorter {
public:
NoShorter(int i):sz(i) {}
bool operator()(const string& s) {
return s.size() >= sz;
}
private:
int sz;
};
string make_plural(size_t ctr, const string& word, const string& ending) {
return (ctr > 1) ? word + ending : word;
}
void elimDups(std::vector<std::string>& words) {
sort(words.begin(), words.end());
// unique 重排输入范围,使得每个单词只出现一次
// 排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
auto end_unique = unique(words.begin(), words.end());
words.erase(end_unique, words.end());
}
void biggies(vector<string>& words, vector<string>::size_type sz) {
// 将 words 按字典序排序,删除重复单词
elimDups(words);
// 按长度排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), Shorter());
/*stable_sort(words.begin(), words.end(),
[](const string& a, const string& b) {
return a.size() < b.size();
}
);*/
// 获取一个迭代器,指向第一个满足 size() >= sz 的元素
NoShorter ns(sz);
auto wc = find_if(words.begin(), words.end(), ns);
/*auto wc = find_if(words.begin(), words.end(),
[sz](const string& a) {
return a.size() >= sz;
}
);*/
// 计算满足 size() >= sz 的元素的数目
auto count = words.end() - wc;
cout << count << " " << make_plural(count, "word", "s")
<< " of length " << sz << " or longer" << endl;
for_each(wc, words.end(),
[](const string& s) {
cout << s << " ";
}
);
cout << endl;
}
int main() {
std::vector<std::string> svec = { "the", "quick", "red", "fox", "jumps",
"over", "the", "slow", "red", "turtle" };
// 按字典序打印 svec 中长度不小于 4 的单词
biggies(svec, 4);
return 0;
}
std::unique 是 C++ 标准库中的一个算法,用于在范围内移除相邻的重复元素,使得范围内的所有重复元素只保留一个,并返回指向最后一个不重复元素之后的位置的迭代器。
以下是 std::unique 的基本用法:
std::unique 将相邻的重复元素移动到容器的末尾,并返回一个指向不重复元素之后的位置的迭代器。然后,我们可以使用此迭代器来确定不重复元素的范围,或者直接从原始容器中访问不重复元素。请注意,std::unique 并不会改变容器的大小,重复的元素只是被移到了容器的末尾,需要根据返回的迭代器来确定有效范围
#include <iostream>
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 2, 3, 3, 3, 4, 5, 5, 6};
// 使用 std::unique 移除相邻的重复元素
auto last = std::unique(vec.begin(), vec.end());
// 输出不重复的元素
std::cout << "Unique elements:";
for (auto it = vec.begin(); it != last; ++it) {
std::cout << ' ' << *it;
}
std::cout << std::endl;
// 处理后的向量:1 2 3 4 5 6 3 4 5 6
return 0;
}
在 C++11 中,lambda 是通过匿名的函数对象来实现的,因此我们可以把 lambda 看作是对函数对象在使用方式上进行的简化
当代码需要一个简单的函数,并且这个函数并不会在其他地方被使用时,就可以使用 lambda 来实现,此时它所起的作用类似于匿名函数
但如果这个函数需要多次使用,并且它需要保存某些状态的话,使用 函数对象 则更合适一些
8.2 标准库定义的函数对象
1、标准库定义了 一组表示算术运算符、关系运算符和逻辑运算符的类,每个类 分别定义了 一个执行命名操作的调用运算符。例如,plus类定义了一个函数调用运算符用于对 一对运算对象执行+的操作;modulus类定义了 一个调用运算符执行二元的%操作:equal_to 类执行 ==,等等
这些类都被定义成 模板的形式,我们可以 为其指定具体的应用类型,这里的类型 即调用运算符的形参类型。例如,plus<string>
令string加法运算符 作用于string对象;plus<int>
的运算对象是int;plus<Sales_data>
对Sales_data对象执行加法运算
plus<int> intAdd; //可执行int加法的函数对
negate<int> intNegate; //可对int值取反的函数对象
//使用intAdd::operator(int, int)求10和20的和
int sum = intAdd(10, 20); //等价于sum=30
sum = intNegate(intAdd(10,20)); //等价于sum=-30
//使用intNegate::operator(int)生成-10
//然后将-10作为intAdd::operator(int, int)的第二个参数
sum = intAdd(10, intNegate(10)); //sum=0
2、所列的类型定义在functional头文件中
3、在算法中 使用标准库函数对象:表示运算符的函数对象类 常用来替换 算法中的默认运算符。如我们所知,在默认情况下 排序算法使用operator<将序列按照升序排列。如果要执行 降序排列的话,可以传入 一个greater类型的对象。该类 将产生一个调用运算符 并负责执行待排序类型的大于运算
//传入一个临时的函数对象 用于执行两个string对象的>比较运算
sort(svec.begin(), svec.end(), greater<string>());
第三个实参是 greater<string>
类型的一个未命名的对象,因此 当sort比较元素时,不再是使用默认的<运算符,而是调用 给定
的greater函数对象。该对象负责在string元素之间执行>比较运算
标准库规定 其函数对象对于指针同样适用。之前曾经介绍过 比较两个无关指针将产生未定义的行为,可能会希望 通过比较指针的内存地址 来sort指针的vector。直接这么做 将产生未定义的行为,因此 可以使用一个标准库函数对象来实现该目的
vector<string *> nameTable; //指针的vector
//错误:nameTable中的指针 彼此之间没有关系,所以<将产生未定义的行为
sort(nameTable.begin(), nameTable.end(),
[](string *a, string *b) { return a < b; });
//正确:标准库规定指针的less是定义良好的
sort(nameTable.begin(), nameTable.end(), less<string*>());
关联容器使用 less<key_type>
对元素排序,因此我们可以 定义一个指针的set 或者在map中使用指针 作为关键值而无须直接声明less
4、使用标准库函数对象 及适配器定义一条表达式,令其
(1) 统计大于1024的值有多少个
(2) 找到第一个不等于pooth的字符串
(3)将所有的值乘以2
#include <functional>
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
int main() {
using std::placeholders::_1;
// 统计大于 1024 的值有多少个
std::vector<int> ivec{ 1000, 2000, 3000, 4000, 5000 };
std::cout << std::count_if(ivec.begin(), ivec.end(),
std::bind(std::greater<int>(), _1, 1024)) << std::endl;
// std::greater<int>()相当于函数名(创建的临时变量,要加了调用运算符才有用)
// 找到第一个不等于 pooth 的字符串
std::vector<std::string> svec{ "pooth", "pooth", "abc", "pooth" };
std::cout << *std::find_if(svec.begin(), svec.end(),
std::bind(std::not_equal_to<std::string>(), _1, "pooth"))
<< std::endl;
// 将所有的值乘以 2
std::transform(ivec.begin(), ivec.end(), ivec.begin(),
std::bind(std::multiplies<int>(), _1, 2));
for (const auto& i : ivec)
std::cout << i << " ";
std::cout << std::endl;
return 0;
}
std::bind 的基本用法
#include <iostream>
#include <functional>
void foo(int a, int b, int c) {
std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
}
int main() {
// 使用 std::bind 绑定函数 foo 的前两个参数
auto bound_foo = std::bind(foo, 1, 2, std::placeholders::_1);
// 调用绑定后的函数对象,提供第三个参数
bound_foo(3); // 将 3 作为第三个参数传递给 foo
return 0;
}
#include <iostream>
#include <functional>
void foo(int a, int b, int c) {
std::cout << "a: " << a << ", b: " << b << ", c: " << c << std::endl;
}
int main() {
// 调用bind函数
std::bind(foo, 1, 2, std::placeholders::_1)(3);
// std::not_equal_to<int>()相当于函数名(创建的临时变量,要加了调用运算符才有用)
std::cout << std::not_equal_to<int>()(1, 3) << std::endl;
return 0;
}
5、使用标准库函数对象判断一个给定的int值是否能被 int 容器中的所有元素整除
#include <iostream>
#include <functional>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> vec = { 1, 2, 3 };
bool b = all_of(vec.begin(), vec.end(), [n](int i) { return modulus<int>()(n, i) == 0; });
cout << b << endl;
return 0;
}
std::all_of 是 C++ 标准库中的一个算法,用于检查给定范围内的所有元素是否都满足指定的条件。如果范围内的所有元素都满足条件,则返回 true;否则返回 false
8.3 可调用对象与function
1、C++语言中 有几种可调用的对象:函数、函数指针、lambda表达式、bind创建的对象 以及 重载了函数调用运算符的类
和其他对象一样,可调用的对象 也有类型。例如,每个 lambda有它自己唯一的(未命名)类类型:函数及函数指针的类型 则由其返回值类型 和 实参类型决定
两个不同类型的可调用对象 却可能 共享同一种调用形式。调用形式 指明了调用返回的类型 以及 传递给调用的实参类型。一种调用形式 对应一个函数类型
int(int, int) // 是一个函数类型,它接受两个int,返回一个int
2、不同类型可能 具有相同的调用形式
//普通函数
int add(int i, int j) { return i + j; }
//lambda,其产生一个未命名的函数对象类
auto mod = [](int i, int j) { return i % j; };
//函数对象类
struct divide {
int operator()(int denominator, int divisor) {
return denominator / divisor;
}
};
这些可调用对象 分别对其参数执行了不同的算术运算,尽管它们的类型各不相同,但是 共享同一种调用形式int(int,int)
希望使用这些可调用对象构建一个简单的桌面计算器。为了实现这一目的 需要定义一个函数表 用于存储指向这些可调用对象的“指针”。当程序需要 执行某个特定的操作时,从表中查找该调用的函数
在C++语言中,函数表 很容易通过map来实现。对于此例来说,我们使用 一个表示运算符符号的string对象 作为关键字;使用实现运算符的函数 作为值。当我们需要求 给定运算符的值时,先通过 运算符索引map,然后 调用找到的那个元素
假定我们的所有函数都相互独立,并且只处理关于int的二元运算,则map可以定义成如下的形式
//构建 从运算符到函数指针的映射关系,其中函数接受两个int、返回一个int
map<string, int(*)(int, int)> binops;
按照下面的形式 将add的指针添加到binops中
//正确:add是一个指向正确类型函数的指针
binops.insert({"+", add}); //{"+",add}是一个pair
但不能 将mod或者divide存入binops:
//错误:mod不是一个函数指针
binops.insert({"%", mod});
问题在于mod是个lambda表达式,而每个lambda有它自己的类类型,该类型与存储在 binops中的值的类型不匹配
3、标准库function类型:可以 使用一个名为function的新的标准库类型 解决上述问题,function定义在 functional头文件中
function是一个模板,当创建一个具体的 function类型时 我们必须提供额外的信息
额外的信息是指 该 function 类型能够表示的对象的调用形式。在一对尖括号内 指定类型:
function<int(int, int)>
声明了 一个function类型,它可以表示 接受两个int、返回一个int的可调用对象。可以用这个新声明的类型 表示任意一种桌面计算器用到的类型
function<int(int, int)> f1 = add; //函数指针
function<int(int, int)> f2 = divide(); //函数对象类的对象
function<int(int, int)> f3 = [](int i, int j) //lambda
{ return i * j; };
cout << f1(4, 2) << endl; //打印6
cout << f2(4, 2) << endl; //打印2
cout << f3(4, 2) << endl; //打印8
使用 这个function类型 我们可以重新定义map:
//列举了 可调用对象与二元运算符对应关系的表格
//所有可调用对象 都必须接受两个int、返回一个int
//其中的元素可以是 函数指针、函数对象或者lambda
map<string, function<int(int, int)>> binops;
把所有可调用对象,包括函数指针、lambda 或者 函数对象在内,都添加到这个map中:
//普通函数
int add(int i, int j) { return i + j; }
//lambda,其产生一个未命名的函数对象类
auto mod = [](int i, int j) { return i % j; };
//函数对象类
struct divide {
int operator()(int denominator, int divisor) {
return denominator / divisor;
}
};
map<string, function<int(int, int)>> m = { // function别忘了加
{"+", add}, // 函数指针
{"-", minus<int>()}, // 标准库函数对象
{"*", [](int i, int j) { return i * j; }}, // 未命名的 lambda
{"/", divide()}, // 用户定义的函数对象
{"%", mod} // 命名了的 lambda 对象
};
尽管其中的可调用对象的类型 各不相同,我们仍然 能够把所有这些类型都存储在同一个 function<int (int, int)>
类型中
索引map时 将得到关联值的一个引用。如果我们索引binops,将得到function对象的引用。function类型重载了调用运算符,该运算符接受它自己的实参,然后将其传递给存好的可调用对象
binops["+"](10, 5); //调用add(10,5)
binops["-"](10, 5); //使用minus<in t>对象的调用运算符
binops["/"](10, 5); //使用divide对象的调用运算符
binops["*"](10, 5); //调用lambda函数对象
binops["%"](10, 5); //调用lambda函数对象
在第一个调用中,我们获得的元素存放着 一个指向add函数的指针,因此调用 binops["+"] (10, 5)
实际上是 使用该指针调用add,
并传入10和5。在接下来的调用中,binops["-"]
返回一个存放着std::minus<int>
类型对象的function,我们将执行该对象的调用运算符
4、重载的函数与function:
不能(直接)将重载函数的名字 存入function类型的对象中:
int add(int i, int j) { return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert({"+", add}); //错误:哪个add?
解决上述二义性问题的一条途径是 存储函数指针 而非函数的名字:
int (*fp) (int, int) = add; //指针所指的add 是接受两个int的版本
binops.insert({"+", fp}); //正确:fp指向一个正确的add版本
也能使用lambda来消除二义性
//正确:使用lambda来指定我们希望使用的add版本
binops.insert({"+", [](int a, int b) { return add(a, b); });
新版本标准库中的function类 与旧版本中的 unary_function 和 binary_function 没有关联,后两个类 已经被更通用的bind函数替代了
完整 二元运算 计算器代码
#include <iostream>
#include <map>
#include <functional>
using namespace std;
int add(int i, int j) {
return i + j;
}
auto mod = [](int i, int j)->int { return i % j; };
struct divide{
divide() = default; // 有个默认构造函数,后面加入map的就是这个
divide(int i, int j):i(i), j(j) {}
int operator()(int i, int j) {
return i / j;
}
private:
int i;
int j;
};
函数对象类,直接重载,不需要再设置私有变量和自定义构造函数
//struct divide {
// int operator()(int denominator, int divisor) {
// return denominator / divisor;
// }
//};
int main()
{
map<string, function<int(int, int)>> m = { // function别忘了加
{"+", add}, // 函数指针
{"-", minus<int>()}, // 标准库函数对象
{"*", [](int i, int j) { return i * j; }}, // 未命名的 lambda
{"/", divide()}, // 用户定义的函数对象
{"%", mod} // 命名了的 lambda 对象
};
int a, b;
string s;
while (cin >> a >> s >> b)
cout << m[s](a, b) << endl;
return 0;
}
运行结果
9、重载、类型转换与运算符
由一个实参调用的非显式构造函数 定义了 一种隐式的类型转换,这种构造函数 将实参类型的对象转换成类类型。我们同样能定义 对于类类型的类型转换,通过定义 类型转换运算符 可以做到这一点。转换构造函数 和 类型转换运算符 共同定义了 类类型转换,这样的转换 有时也被称作 用户定义的类型转换
9.1 类型转换运算符
1、类型转换运算符是 类的一种特殊成员函数,它负责 将一个类类型的值 转换成 其他类型
operator type() const;
type表示某种类型。类型转换运算符 可以面向任意类型(除了void之外)进行定义,只要该类型 能作为函数的返回类型。因此,我们不允许转换成 数组 或者 函数类型,但允许转换成 指针(包括数组指针及函数指针)或者 引用类型
类型转换运算符 既没有显式的返回类型,也没有形参,而且 必须定义成类的成员函数。类型转换运算符 通常不应该改变待转换对象的内容,因此,类型转换运算符 一般被定义成 const成员
2、定义含有类型转换运算符的类:定义一个比较简单的类,令其表示0到255之间的一个整数
class SmallInt {
public:
SmallInt(int i = 0):val(i)
{
throw std::out_of_range("Bad SmallInt value");
}
operator int() const { return val; }
private:
std::size_t val;
};
SmallInt类既定义了向类类型的转换,也定义了从类类型向其他类型的转换
其中,构造函数 将算术类型的值转换成 SmallInt 对象,而类型转换运算符 将 SmallInt 对象转换成 int:
SmallInt si;
si = 4; //首先将4隐式地转换成SmallInt,然后调用SmallInt::operator=
si + 3; //首先将si隐式地转换成int,然后执行数的加法
使用等号 调用构造函数初始化变量:
可以使用等号调用构造函数初始化变量的方式称为“拷贝初始化”或“直接初始化”。这种方式通过构造函数将右侧的值传递给左侧的变量
class MyClass {
public:
MyClass(int value) : data(value) {} // 构造函数
int getData() { return data; }
private:
int data;
};
int main() {
MyClass obj = 42; // 使用等号调用构造函数初始化变量
std::cout << obj.getData() << std::endl; // 输出 42
return 0;
}
3、尽管编译器 一次只能执行一个用户定义的类型转换,但是 隐式的用户定义类型转换 可以置于一个标准(内置)类型转换之前 或 之后,并与其一起使用。因此,可以将 任何算术类型传递给 SmallInt 的构造函数。类似的,也能 使用类型转换运算符 将一个S mallInt 对象转换成 int,然后再将所得的int转换成任何其他算术类型
//内置类型转换将double实参转换成int
SmallInt si = 3.14; //调用SmallInt(int)构造函数
//SmallInt的类型转换运算符 将si转换成int
si + 3.14; //内置类型转换将所得的int继续转换成double
因为类型转换运算符 是隐式执行的,所以 无法给这些函数传递实参,当然也就 不能在类型转换运算符的定义中 使用任何形参。同时,尽管类型转换函数 不负责指定返回类型,但实际上 每个类型转换函数 都会返回一个对应类型的值:
class SmallInt;
operator int(SmallInt&); //错误:不是成员函数
class SmallInt {
public:
int operator int() const; //错误:指定了返回类型
operator int(int = 0) const; //错误:参数列表不为空
operator int*() const { return 42; } //错误:42不是一个指针
};
如果 在类类型和转换类型之间 不存在明显的映射关系,则这样的类型转换 可能具有误导性
例如,假设某个类表示Date,我们也许会为它添加一个从Date到int的转换。然而,类型转换函数的返回值应该是什么?一种可能的解释是,函数返回一个十进制数 依次表示年,月、日,例如,July 30, 1989 可能转换为int值 19890730。同时还存在 另外一种合理的解释,即类型转换运算符 返回的int表示的是从某个时间节点(比如 January 1, 1970)开始经过的天数。显然这两种理解都合情合理,毕竟从形式上看它们 产生的效果都是 越靠后的日期对应的整数值越大,而且两种转换都有实际的用处
问题在于Date类型的对象和int类型的值之间 不存在明确的一对一映射关系。因此 在此例中,不定义 该类型转换运算符 也许会更好。作为替代的手段,类可以定义一个或多个普通的成员函数 以从各种不同形式中提取所需的信息
4、类型转换运算符可能产生意外结果:类很少提供 类型转换运算符。如果类型转换自动发生 用户可能会感觉比较意外,对于类来说,定义向bool的类型转换 还是比较普遍的现象
因为bool是一种算术类型,所以类类型的对象 转换成bool后 就能被用在任何需要算术类型的上下文中
当 istream 含有向 bool 的类型转换时,下面的代码仍将编译通过:
int i = 42;
cin << i; //如果向bool的类型转换 不是显式的,则该代码在编译器看来将是合法的
因为istream本身并没有定义<<,所以本来代码应该产生错误。然而,该代码能使用istream的bool类型转换运算符 将cin转换
成bool,而这个bool值 接着会 被提升成int 并用作内置的左移运算符的左侧运算对象
这样一来,提升后的bool值(1或0)最终会 被左移42个位置。这一结果显然与我们的预期大相径庭
5、显式的类型转换运算符:为了防止这样的异常情况发生,C++11新标准引入了 显式的类型转换运算符
class SmallInt {
public:
//编译器不会自动执行这一类型转换
explicit operator int() const { return val; }
//其他成员与之前的版本一致
};
和显式的构造函数一样,编译器(通常)也不会将一个显式的类型转换运算符 用于隐式类型转换
SmallInt si = 3; //正确:SmallInt的构造函数不是显式的
si + 3; //错误:此处需要隐式的类型转换,但类的运算符是显式的
static_cast<int>(si) + 3; //正确:显式地请求类型转换
static_cast 是 C++ 中的一种类型转换操作符,用于显式地进行类型转换。它允许你将数据从一种类型转换为另一种类型,前提是这种转换是明确定义且安全的
以下是 static_cast 的基本语法:
new_type new_variable = static_cast<new_type>(expression);
可以使用 static_cast 将一个变量从一种数值类型转换为另一种数值类型,或者 将指针或引用从一种类型转换为另一种类型,前提是 它们之间有一个合理且安全的转换
double myDouble = 3.14;
int myInt = static_cast<int>(myDouble); // 将 double 转换为 int
// 指针转换
Base* basePtr = new Derived();
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 将 Base 指针转换为 Derived 指针
static_cast 在编译时执行类型检查,以确保转换是有效的。与 C 风格的强制类型转换 (type) 不同,后者可以执行任何类型的转换,static_cast 通过在编译时捕获许多潜在的不安全转换来提供更多的安全性。然而,需要注意的是,它并不能执行所有类型的转换;一些 static_cast 无法处理的转换需要其他转换操作符,比如 dynamic_cast、const_cast 或 reinterpret_cast
当类型转换运算符是显式的时,我们也能执行类型转换
该规定存在一个例外,即 如果表达式被用作条件,则编译器会将显式的类型转换 自动应用于它。换句话说,当表达式出现在下列位置时,显式的类型转换 将被隐式地执行(所以 cin>> 可以作为条件(隐式转成bool))
- if、while及do语句的条件部分
- for语句头的条件表达式
- 逻辑非运算符(!)、逻辑或运算符(||)、逻辑与运算符(&&)的运算对象
- 条件运算符(?:)的条件表达式
6、转换为bool:在标准库的早期版本中,IO类型 定义了向void*的转换规则,以求避免上面提到的问题(cin << i;
)。在C++11新标准下,IO标准库 通过定义一个向bool的显式类型转换 实现同样的目的
无论我们什么时候 在条件中使用流对象,都会使用为IO类型定义的 operator bool
while (std::cin >> value)
负责 将数据读入到value并返回cin。为了 对条件求值,cin 被 istream operator bool 类型转换函数 隐式地执行了转换。如果 cin
的条件状态是 good(其他状态包括 eof / fail / bad),则该函数返回为真;否则该函数返回为假
向bool的类型转换 通常用在条件部分,因此operator bool一般定义成explicit的
7、编写类型转换运算符将一个 Sales_data 对象分别转换成 string 和 double
只需要 Sales_data.h 在声明部分 加东西即可
Sales_data.h
#pragma once
#ifndef SALES_DATA_H
#define SALES_DATA_H
#include <string>
#include <iostream>
struct Sales_data;
//std::istream& read(std::istream& is, Sales_data& item);
//Sales_data add(const Sales_data& s1, const Sales_data& s2);
//std::ostream& print(std::ostream& os, const Sales_data& s); // 还需要声明
std::istream& operator>>(std::istream& is, Sales_data& item);
std::ostream& operator<<(std::ostream& os, Sales_data& item);
Sales_data operator+(const Sales_data& s1, const Sales_data& s2);
class Sales_data {
public:
//friend Sales_data add(const Sales_data& s1, const Sales_data& s2);
//friend std::istream& read(std::istream& is, Sales_data& s);
//friend std::ostream& print(std::ostream& os, const Sales_data& s); // 友元声明
friend std::istream& operator>>(std::istream& is, Sales_data& item);
friend std::ostream& operator<<(std::ostream& os, Sales_data& item);
friend Sales_data operator+(const Sales_data& s1, const Sales_data& s2);
// 使用委托函数重新编写构造函数
Sales_data(const std::string& s, unsigned u, double r) : bookNo(s), units_sold(u), revenue(r* u) { }
Sales_data() : Sales_data("", 0, 0) { }
Sales_data(const std::string& s) : Sales_data(s, 0, 0) { }
Sales_data(std::istream& is) : Sales_data() {
// read(is, *this);
is >> *this;
}
Sales_data& combine(const Sales_data&);
std::string isbn() const { return bookNo; }
double avg() const;
// 自定义类型转换运算符
explicit operator std::string() const {
return bookNo;
}
explicit operator double() const {
return revenue;
}
private:
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Sales_data& Sales_data::combine(const Sales_data& s) {
units_sold += s.units_sold;
revenue += s.revenue;
return *this;
}
double Sales_data::avg() const {
if (units_sold) {
return revenue / units_sold;
}
else {
return 0;
}
}
//Sales_data add(const Sales_data& s1, const Sales_data& s2) {
// Sales_data tmp = s1;
// tmp.combine(s2);
// return tmp;
//}
Sales_data operator+(const Sales_data& s1, const Sales_data& s2) {
Sales_data tmp = s1;
tmp.combine(s2);
return tmp;
}
//std::istream& read(std::istream& is, Sales_data& s) {
// double singlePrice;
// is >> s.bookNo >> s.units_sold >> singlePrice;
// s.revenue = s.units_sold * singlePrice;
// return is;
//}
std::istream& operator>>(std::istream& is, Sales_data& s) {
double singlePrice;
is >> s.bookNo >> s.units_sold >> singlePrice;
s.revenue = s.units_sold * singlePrice;
return is;
}
//std::ostream& print(std::ostream& os, const Sales_data& s) {
// os << s.isbn() << " " << s.units_sold << " " << s.revenue << " " << s.avg();
// return os;
//}
std::ostream& operator<<(std::ostream& os, Sales_data& s) {
os << s.isbn() << " " << s.units_sold << " " << s.revenue << " " << s.avg();
return os;
}
#endif
14.45.cpp
#include "Sales_data.h"
using namespace std;
int main()
{
Sales_data cp5("1234", 2, 99.99);
std::cout << cp5 << std::endl;
string s = static_cast<string>(cp5); // 别忘了加括号
cout << s << endl;
double d = static_cast<double>(cp5);
cout << d << endl;
return 0;
}
运行结果
Sales_data 类不应该定义这两种类型转换运算符,因为对于该类型来讲,它包含三个数据成员:bookNo,units_sold 和 revenue,只有三者在一起才是有效的数据
但如果确实想要定义这两个类型转换运算符的话,应该把它们声明成 explicit 的,这样可以防止 Sales_data 在某些情况下被默认转换成 string 或 double 类型,这有可能导致意料之外的运算结果
8、说明下面这两个类型转换运算符的区别
struct Integral {
operator const int();
operator int() const;
}
前者将对象转换成 const int ,在接受 const int 值的地方才能够使用
后者则将对象转换成 int 值,且类型转换运算符不允许修改对象的内容。相对来说更加通用一些
9.2 避免有二义性的类型转换
1、如果类中包含 一个或多个类型转换,则必须确保 在类类型和目标类型之间只存在唯一 一种转换方式
在两种情况下 可能产生多重转换路径。第一种情况是 两个类提供相同的类型转换:例如,当A类定义了 一个接受B类对象的转换构造函数,同时B类定义了 一个转换目标是A类的 类型转换运算符时,我们就说 它们提供了相同的类型转换
第二种情况是类定义了多个转换规则,而这些转换涉及的类型本身 可以通过其他类型转换 联系在一起。最典型的例子是算术运算符,对某个给定的类来说,最好只定义最多 一个 与算术类型有关的转换规则
假设我们有两个自定义类型 Distance 和 Time,它们分别表示距离和时间。我们想要进行算术运算,比如计算速度(距离除以时间)
#include <iostream>
class Distance {
private:
int meters;
public:
Distance(int m) : meters(m) {}
int getMeters() const { return meters; }
};
class Time {
private:
int seconds;
public:
Time(int s) : seconds(s) {}
int getSeconds() const { return seconds; }
};
// 计算速度
double calculateSpeed(const Distance& distance, const Time& time) {
// 假设算法为距离除以时间
return static_cast<double>(distance.getMeters()) / time.getSeconds();
}
int main() {
Distance d(1000); // 1000 米
Time t(3600); // 3600 秒
double speed = calculateSpeed(d, t);
std::cout << "Speed: " << speed << " meters per second" << std::endl;
return 0;
}
按照上述方式定义了转换规则,并且我们的 Distance 类型的转换函数是 将米转换为秒,而 Time 类型的转换函数是 将秒转换为米。那么我们在 calculateSpeed 函数中进行的除法操作就会出现问题
因为我们在 Distance 类型中定义了将米转换为秒的转换规则,所以在除法操作中,距离会被转换为秒。而在 Time 类型中定义了将秒转换为米的转换规则,所以时间也会被转换为米。这样的结果就是,我们实际上进行的是距离除以距离,得到的结果将是一个无意义的数值,而不是速度
总结:通常情况下,不要为 类定义相同的类型转换,也不要 在类中定义两个及两个以上转换源 或 转换目标是算术类型的转换
2、实参匹配 和 相同的类型转换:在下面的例子中,定义了 两种将B转换成A的方法:一种 使用B的类型转换运算符、另一种 使用A的以B为参数的构造函数
//最好不要在两个类之间构建相同的类型转换
struct B;
struct A {
A() = default;
A(const B&); //把一个B转换成A
//其他数据成员
};
struct B {
operator A() const; //也是把一个B转换成A
//其他数据成员
};
A f(const A&);
B b;
A a = f(b); //二义性错误:含义是f(B::operator A())
//还是f(A::A(const B&)) ?
该调用 可以使用 以B为参数的A的构造函数,也可以 使用B当中 把B转换成A的类型转换运算符
确实 想执行上述的调用,就不得不 显式地调用类型转换运算符 或者 转换构造函数:
A a1 = f(b.operator A()); //正确:使用B的类型转换运算符
A a2 = f(A(b)); //正确:使用A的构造函数
无法使用强制类型转换 来解决二义性问题,因为强制类型转换 本身也面临二义性
#include <iostream>
class Base {
public:
virtual void print() const {
std::cout << "Base" << std::endl;
}
};
class Derived1 : public Base {
public:
virtual void print() const override {
std::cout << "Derived1" << std::endl;
}
};
class Derived2 : public Base {
public:
virtual void print() const override {
std::cout << "Derived2" << std::endl;
}
};
class MultipleDerived : public Derived1, public Derived2 {
};
int main() {
MultipleDerived obj;
// 二义性:无法确定要将 obj 强制转换为 Derived1* 还是 Derived2*
Derived1* ptr1 = (Derived1*)&obj;
Derived2* ptr2 = (Derived2*)&obj;
ptr1->print(); // 输出:Derived1
ptr2->print(); // 输出:Derived2
return 0;
}
有一个 Base 类,以及两个派生类 Derived1 和 Derived2。然后,我们定义了一个多重继承类 MultipleDerived,它同时继承了 Derived1 和 Derived2。在 main 函数中,我们创建了一个 MultipleDerived 类的对象 obj
然后,我们尝试将 obj 强制转换为 Derived1* 和 Derived2* 指针,并分别调用它们的 print 函数。但是,由于 MultipleDerived 继承了 Derived1 和 Derived2,所以强制类型转换存在二义性,编译器无法确定应该将 obj 转换为哪个基类指针
又例
#include <iostream>
class A {
public:
A(int x) { std::cout << "Constructing A with int: " << x << std::endl; }
};
class B {
public:
B(double y) { std::cout << "Constructing B with double: " << y << std::endl; }
};
void foo(long z) {
A a(z); // or B b(z);
}
其接受一个参数类型为 long,在这种情况下,参数类型为 long 的 foo 函数将出现二义性,因为既可以调用 A 类的构造函数,也可以调用 B 类的构造函数。即使我们使用强制类型转换来指定调用哪个构造函数,编译器也无法确定应该调用哪个函数,因为转换规则在语言规范中已经定义好了,无法通过强制类型转换改变。因此,即使使用强制类型转换,也无法解决二义性问题
3、二义性 与 转换目标为内置类型的多重类型转换:如果类定义了一组类型转换,它们的转换源(或者转换目标)类型本身可以通过 其他类型转换联系在一起,则同样会产生二义性的问题。最简单也是最困扰我们的例子就是 类当中定义了多个参数都是算术类型的构造函数,或者 转换目标都是 算术类型的类型转换运算符
算术类型包括 整数类型 和 浮点类型
在下面的类中 包含两个转换构造函数,它们的参数是 两种不同的算术类型;同时 还包含两个类型转换运算符,它们的转换目标也恰好是 两种不同的算术类型
struct A {
A(int = 0); //最好不要创建 两个转换源 都是算术类型的类型转换
A(double);
operator int() const; //最好不要创建 两个转换对象 都是算术类型的类型转换
operator double() const;
//其他成员
};
void f2(long double);
A a;
f2(a); //二义性错误:含义是f(A::operator int())
//还是f(A::operator double()) ?
long lg;
A a2(lg); //二义性错误:含义是A:A(int) 还是 A::A(double) ?
哪个类型转换 都无法精确匹配 long double。然而 这两个类型转换都 可以使用,只要后面 再执行一次生成long double的标准类型转换即可
哪个构造函数都无法精确匹配 long类型。它们在使用构造函数前 都要求先将实参进行类型转换:
- 先执行long到double的标准类型转换,再执行A(double)
- 先执行long到int的标准类型转换,再执行A(int)
根本原因是 它们所需的标准类型转换级别一致(都会损失精度)
C++ 中的标准类型转换级别按照优先级和安全性分为四个级别,从高到低分别是:
- 完全匹配: 这是最高级别的类型转换,它发生在调用函数时,实参的类型与函数参数的类型完全匹配时。在这种情况下,不需要任何类型转换,因为实参类型与函数参数类型一致
- 标准类型转换: 如果没有找到完全匹配的函数,编译器会尝试进行标准类型转换。标准类型转换包括:
数字类型的提升(如将 char 提升为 int、float 提升为 double 等)
枚举类型到整数类型的转换
整数类型到浮点类型的转换
等等… - 用户定义的转换: 如果还是没有找到匹配的函数,编译器将查找是否有用户定义的类型转换函数,例如类的类型转换构造函数 或 类型转换运算符
- 强制类型转换: 这是最低级别的类型转换,需要使用显式的强制类型转换操作符,例如 static_cast、dynamic_cast、const_cast、reinterpret_cast 等。强制类型转换可能会丢失信息或导致不安全的行为,因此应该谨慎使用
转换优先级排序:
- 完全匹配
- 常量转换
- 提升
- 算术或指针转换
- 类类型转换
void calc(int);
void calc(LongDouble);
double dval;
calc(dval); //哪个calc?
这里会优先调用 void calc(int) 函数。因为 double 转换为 int 是标准类型转换,而转换成 LongDouble 则是转换为用户自定义类型,实际上是调用了转换构造函数,因此前者优先
当我们使用 用户定义的类型转换时,如果转换过程 包含标准类型转换,则标准类型转换的级别 将决定编译器选择最佳匹配的过程:
将 short 类型提升为 int 类型的操作优于将 short 类型转换为 double 类型的操作:将 short 类型提升为 int 类型是一种整数类型之间的提升转换,不会丢失任何精度。而将 short 类型转换为 double 类型是一种整数类型到浮点类型的转换,可能会导致精度丢失,因为浮点数通常无法精确表示所有整数值
shorts = 42;
//把short提升成int优于把short转换成double
A a3(s); //使用A::A(int)
- 不要令两个类执行相同的类型转换:如果Foo类有一个接受Bar类对象的构造函数,则不要在Bar类中再定义转换目标是FoO类的类型转换运算符
- 避免转换目标是内置算术类型的类型转换。特别是当 已经定义了一个转换成算术类型的类型转换时,接下来
1)不要再定义接受算术类型的重载运算符。如果用户需要使用这样的运算符,则类型转换操作将转换你的类型的对象,然后使用内置的运算符
2)不要定义转换到多种算术类型的类型转换
除了显式地向bool类型的转换之外,我们应该尽量避免 定义类型转换函数 并尽可能地限制那些“显然正确”的非显式构造函数
4、重载函数 与 转换构造函数:当我们 调用重载的函数时,从多个类型转换中 进行选择将变得更加复杂
struct C {
C(int);
//其他成员
};
struct D {
D(int);
//其他成员
};
void manip(const C&);
void manip(const D&);
//二义性错误:含义是manip(C(10)) 还是 manip(D(10))
manip(10);
调用者 可以显式地构造正确的类型 从而消除二义性
//正确:调用manip(const C&)
manip(C(10));
如果 在调用重载函数时 我们需要使用构造函数 或者 强制类型转换 来改变实参的类型 则这通常意味着程序的设计存在不足
5、重载函数 与 用户定义的类型转换:当调用重载函数时,如果两个(或多个)用户定义的类型转换 都提供了可行匹配,则
我们认为 这些类型转换一样好。在这个过程中,我们 不会考虑任何可能出现的标准类型转换的级别
只有 当重载函数能通过同一个类型转换函数得到匹配时,我们才会考虑其中 出现的标准类型转换
struct C {
C(int);
//其他成员
};
struct E {
E(double);
//其他成员
};
void manip2(const C&);
void manip2(const E&);
//二义性错误:两个不同的用户定义的类型转换都能用在此处
manip2(10); //含义是manip2(C(10)) 还是manip2(E(double(10)))
即使 其中一个调用 需要额外的标准类型转换 而另一个调用能精确匹配,编译器也会 将该调用标示为错误
在调用重载函数时,如果需要 额外的标准类型转换,则该转换的级别 只有当所有可行函数 都请求同一个用户定义的类型转换时才有用。如果所需的用户定义的类型转换 不止一个,则该 调用具有二义性
6、在初始化 ex1 和 ex2 的过程中,可能用到哪些类类型的转换序列呢?说明初始化是否正确并解释原因
struct LongDouble {
LongDouble(double = 0.0);
operator double();
operator float();
};
LongDouble ldObj;
int ex1 = ldObj;
float ex2 = ldObj;
对于 int ex1 = ldObj;
,它需要把 LongDouble 类型转换成 int 类型,但是 LongDouble 并没有定义对应的类型转换运算符。因此,它会尝试使用其它的来进行转换,其中 operator double ()
和 operator float()
都满足需求。但编译器无法确定哪一个更合适,因此会产生二义性错误
对于 float ex2 = ldObj;
,它需要把 LongDouble 转换成 float 类型,而我们恰好定义了对应的类型转换运算符。因此,只需要直接调用 operator float()
即可
9.3 函数匹配与重载运算符
1、重载的运算符 也是重载的函数。因此,通用的函数匹配规则 同样适用于 判断在给定的表达式中到底应该使用内置运算符还是重载的运算符。不过当运算符函数 出现在表达式中时,候选函数集的规模 要比我们使用 调用运算符调用函数时更大。如果a是一种类类型,则 表达式a sym b
可能是
a.operatorsym(b); //a有一个operator sym成员函数
operatorsym(a, b);//operatorsym是一个普通函数
不能通过 调用的形式来区分 当前调用的是 成员函数还是非成员函数
使用重载运算符 作用于 类类型的运算对象时,候选函数中 包含该运算符的普通非成员版本 和 内置版本。如果左侧运算对象是类类型,则定义在该类中的运算符的重载版本 也包含在候选函数内
调用一个命名的函数时,具有该名字的成员函数 和 非成员函数 不会彼此重载,因为 用来调用命名函数的语法形式 对于成员函数和非成员函数来说 是不相同的
在表达式中 使用重载的运算符时,无法判断 正在使用的是成员函数还是非成员函数,因此 二者都应该在考虑的范围内
class SmallInt {
friend
SmallInt operator+(const SmallInt&, const SmallInt&);
public:
SmallInt(int = 0); //转换源为int的类型转换
operator int() const { return val; } //转换目标为int的类型转换
private:
std::size_t val;
};
如果 试图执行混合模式的算术运算,就将遇到二义性的问题
SmallInt s1, s2;
SmallInt s3 = s1 + s2; //使用重载的operator+
int i = s3 + 0; //二义性错误
二义性:因为 可以把0转换成SmallInt,然后使用 SmallInt的+;或者把s3转换成 int,然后对于两个 int 执行内置的加法运算
对同一个类 既提供了转换目标是算术类型的类型转换,也提供了重载的运算符,则将会遇到 重载运算符与内置运算符的二义性问题
2、在下面的加法表达式中分别选用了哪个operator+?列出候选函数、可行函数及为每个可行函数的实参执行的类型转换:
struct Longdouble {
//用于演示的成员operator+;在通常情况下是个非成员
LongDouble operator+(const SmallInt&);
LongDouble(double = 0.0);
operator double();
operator float();
};
LongDouble operator+(LongDouble&, double);
SmallInt si;
LongDouble ld;
ld = si + ld;
ld = ld + si;
ld = si + ld;
:
可行函数
operator+(int, double)
SmallInt->int,LongDouble->double
operator+(int, float)
SmallInt->int,LongDouble->float
编译器只能执行一个用户定义的类型转换。
上述加法表达式具有二义性
ld = ld + si;
:
LongDouble operator(const SmallInt&)
精确匹配 LongDouble operator+(LongDouble&, double);
也可行,但是前者是完全匹配,调用前者(SmallInt -> int -> double)
可行函数
operator+(double, int)
LongDouble->double,SmallInt->int
operator+(float, int)
LongDouble->float,SmallInt->int
SmallInt si;
double d = si + 3.14;
内置的 operator+(int, double) 是可行的;
而 3.14 可以转换为 int,然后再转换为 SmallInt,所以 SmallInt 的成员 operator+ 也是可行的
两者都需要进行类型转换,所以会产生二义性。改为 double d = s1 + SmallInt(3.14);
即可
小结
1、赋值、下标函数调用 和 箭头运算符 必须作为类的成员
2、如果类重载了 函数调用运算符operator(),则该类的对象被称作“函数对象”。lambda表达式 是一种简便的定义函数对象类的方式