本文主要是介绍C++ Primer 总结索引 | 第十九章:特殊工具与技术,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
1、控制内存分配
某些应用程序 对内存分配有特殊的需求,因此 无法将标准内存管理机制 直接应用于这些程序。它们 需要自定义内存分配的细节,比如 使用关键字 new 将对象放置在 特定的内存空间中。为了实现这一目的,应用程序 需要重载 new 运算符 和 delete 运算符 以控制内存分配的过程
1.1 重载 new 和 delete
1、
// new 表达式
string *sp = new string("a value"); // 分配并初始化一个 string 对象
string *arr = new string[10]; // 分配 10 个默认初始化的 string 对象
实际执行了 三步操作。第一步, new 表达式调用一个名为 operator new
(或者 operator new[]
) 的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间 以便存储特定类型的对象(或者对象的数组)。第二步,编译器 运行相应的构造函数 以构造这些对象,并为其传入初始值。第三步,对象被分配了空间 并构造完成,返回一个指向该对象的指针
2、delete 表达式 删除一个动态分配的对象时:
delete sp; // 销毁*sp,然后释放 sp 指向的内存空间
delete [] arr; // 销毁数组中的元素,然后释放对应的内存空间
实际 执行了两步操作。第一步,对 sp 所指的对象 或者 arr 所指数组中的元素 执行对应的析构函数。第二步,编译器调用名为 operator delete
(或者 operator delete[]
) 的标准库函数释放内存空间
3、应用程序 希望控制内存分配的过程,则 需要定义自己的 operator new 函数和 operator delete 函数。即使在标准库中 已经存在这两个函数的定义,仍然可以定义自己的版本。编译器不会对这种重复的定义 提出异议,相反,编译器将使用 自定义的版本替换标准库定义的版本
4、当 自定义了全局的 operator new 函数和 operator delete 函数后,就担负起了 控制动态内存分配的职责。这两个函数 必须是正确的:因为它们是程序整个处理过程中 至关重要的一部分
5、应用程序 可以在全局作用域中 定义 operator new 函数和 operator delete 函数,也可以 将它们定义为成员函数。当编译器发现一条 new 表达式 或 delete 表达式后,将在程序中 查找可供调用的 operator 函数。如果被分配(释放)的对象是 类类型,则编译器首先在类及其基类的作用域中查找。此时如果该类含有 operator new 成员或 operator delete 成员,则相应的表达式 将调用这些成员。否则,编译器在全局作用域 查找匹配的函数。此时如果编译器找到了用户自定义的版本,则使用该版本执行 new 表达式或 delete 表达式;如果没有找到,则使用标准库定义的版本
可以使用作用域运算符 new 表达式或 delete 表达式忽略定义在类中的函数,直接执行全局作用域中的版本。例如,::new 只在全局作用域中查找匹配的 operator new 函数,::delete 与之类似
6、 operator new 接口和 operator delete 接口
标准库定义了 operator new 函数和 operator delete 函数的 8 个重载版本。其中前 4 个版本可能抛出 bad_alloc 异常,后 4 个版本则不会抛出异常:
// 这些版本可能抛出异常 void *operator new(size_t); // 分配一个对象 void *operator new[](size_t); // 分配一个数组 void *operator delete(void*) noexcept; // 释放一个对象 void *operator delete[](void*) noexcept; // 释放一个数组 // 这些版本承诺不会抛出异常
void *operator new(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void *operator delete(void*, nothrow_t&) noexcept;
void *operator delete[](void*, nothrow_t&) noexcept;
类型 nothrow_t 是定义在 new 头文件中的一个 struct,在这个类型中 不包含任何成员。new 头文件 还定义了一个名为 nothrow 的 const 对象,用户可以通过这个对象请求 new 的非抛出版本。与构造函数类似,operator delete 也不允许抛出异常。当 重载这些运算符时,必须使用 noexcept 异常说明符 指定其不抛出异常
delete 和 delete[] 操作符通常标记为 noexcept,这是有几个重要原因的:
-
异常安全性
delete和delete[]的主要职责是释放内存。在这个过程中不应该抛出异常。因为如果内存释放失败,程序可能会出现内存泄漏或其他未定义行为
如果delete抛出了异常,而这个异常没有被捕获(特别是在析构函数中或异常处理过程中),会导致程序调用std::terminate(),从而结束程序。为了防止这些潜在的灾难性错误,delete操作符被标记为noexcept -
性能优化
noexcept 允许编译器进行更多优化。如果一个函数被标记为 noexcept,编译器可以假定这个函数不会抛出异常,因此可以 省去一些与异常处理相关的开销。例如,它可以省略 异常处理的框架代码,减少生成代码的大小,并提高程序的运行速度。 -
标准要求
在C++标准中,delete 和 delete[] 的默认实现被要求是 noexcept 的。这意味着用户自定义的 delete 和 delete[] 操作符也应该标记为 noexcept,否则可能导致 与标准库的行为不一致,破坏异常安全保证
new 和 new[] 操作符与 delete 和 delete[] 操作符 在异常处理方面的要求有所不同,主要原因 在于它们的功能和可能出现的错误类型不同
- new 操作符的职责
new操作符负责分配内存,并调用对象的构造函数。在内存分配过程中,有可能发生失败,例如由于内存不足 而无法分配所需的内存。在这种情况下,new操作符会抛出一个 std::bad_alloc 异常,通知调用者内存分配失败
由于内存分配失败 是一个常见且合理的异常情况,C++标准要求 new 操作符在内存分配失败时 抛出异常,而不是返回 nullptr。因此,new 操作符不标记为 noexcept,因为它可能抛出异常,程序员可以在 需要时捕获这个异常并处理
在使用 std::nothrow 时,如果内存分配失败,new 操作符不会抛出异常,而是返回 nullptr
new 后面加括号的情况可以分为两类:
- 定位 new:将对象构造在特定的内存地址上,需要一个内存地址作为参数
- 标准 new 的定制:通过传递特殊标记对象来改变 new 的行为,例如使用 std::nothrow 来表示内存分配失败时不抛出异常
std::nothrow 是<new>
头文件中定义的一个全局常量对象,它的类型是 std::nothrow_t。当你使用new (std::nothrow)
时,你调用了 new 操作符的一个重载版本,这个版本接受一个 std::nothrow_t 类型的参数
#include <iostream>
#include <new> // 包含 std::nothrow 的定义int main() {int* p = new (std::nothrow) int[10000000000]; // 尝试分配一个非常大的数组,标准 new 的定制语法if (!p) {std::cout << "内存分配失败,返回 nullptr" << std::endl;} else {std::cout << "内存分配成功" << std::endl;delete[] p;}return 0;
}
析构函数 不应该抛出异常
析构函数抛出异常的风险
- 如果在对象的生命周期结束时,析构函数 抛出了异常,且该异常未被捕获,程序将会调用 std::terminate(),导致程序非正常退出
特别是在栈展开期间,如果 有另一个异常正在传播(例如 因为其他代码抛出了异常),并且析构函数 在此时又抛出了第二个异常,那么C++无法处理 两个同时存在的未捕获异常。这种情况下,程序将会调用std::terminate(),直接终止 - 栈展开期间的异常
栈展开是C++异常处理机制的一部分,当一个异常被抛出时,程序 会沿着调用栈向上回溯,销毁所有在此期间构造的对象。如果在这个过程中 某个析构函数又抛出异常,且当前已经有一个异常在传播,这就会导致 双重异常,这种情况是无法处理的
还可以 使用 try-catch 块捕获所有异常:如果析构函数中的某些操作 可能抛出异常,应该在析构函数内部使用 try-catch 块来捕获和处理异常,确保析构函数 不会将异常传播出去
class MyClass {
public:~MyClass() {try {// 可能会抛出异常的操作riskyOperation();} catch (...) {// 处理异常(例如记录日志),但不要重新抛出std::cerr << "异常在析构函数中被捕获" << std::endl;}}private:void riskyOperation() {// 可能抛出异常的代码throw std::runtime_error("出错了");}
};
// 这些版本可能抛出异常 void *operator new(size_t); // 分配一个对象 void *operator new[](size_t); // 分配一个数组 void *operator delete(void*) noexcept; // 释放一个对象 void *operator delete[](void*) noexcept; // 释放一个数组 // 这些版本承诺不会抛出异常
void *operator new(size_t, nothrow_t&) noexcept;
void *operator new[](size_t, nothrow_t&) noexcept;
void *operator delete(void*, nothrow_t&) noexcept;
void *operator delete[](void*, nothrow_t&) noexcept;
应用程序 可以自定义 上面函数版本中的任意一个,前提是 自定义的版本 必须位于全局作用域 或者 类作用域中。当 将上述运算符函数定义成 类的成员时,它们是隐式的静态的
这意味着这些运算符 不依赖于 任何特定的对象实例 来进行内存分配或释放操作
- 内存管理的性质
operator new 和 operator delete 的作用是分配和释放内存。这些操作在对象构造之前或析构之后发生:
operator new:在对象被创建之前调用,用于分配原始的内存块。这意味着在调用 operator new 时,还没有任何对象存在,因此它不能依赖于某个对象实例
operator delete:在对象被销毁后调用,用于释放内存块。在调用 operator delete 时,对象已经不存在,同样不能依赖于某个对象实例 - 静态成员函数的特性
静态成员函数 与类的某个具体实例无关,它们可以通过 类名直接调用,而无需创建对象实例。由于内存管理操作与特定对象无关,operator new 和 operator delete 函数 自然适合作为静态成员函数
所以,无须显式地声明 static,当然这么做也不会引发错误。所以这两个成员(new 和 delete)必须是静态的,而且 不能操纵类的任何数据成员
对于 operator new 函数或者 operator new[] 函数来说,它的返回类型必须是 void*,第一个形参的类型 必须是 size_t 且 该形参不能含有默认实参。当 为一个对象分配空间时 使用 operator new;为一个数组 分配空间时使用 operator new[]。当编译器调用 operator new 时,把存储指定类型对象所需的字节数 传给 size_t 形参;当调用 operator new[] 时,传递给函数的是 存储数组中所有元素所需的空间
如果 想要自定义 operator new 函数,则可以 为其提供额外的形参。此时,用到这些自定义函数的 new 表达式 必须使用 new 的定位形式 将实参传给新增的形参(在 new 后面加括号)。尽管在一般情况下 可以自定义 具有任何形参的 operator new,但是下面这个函数 无论如何都不能被用户重载:
void *operator new(size_t, void*); // 不允许重新定义这个版本
这种形式只供标准库使用,不能被用户重新定义
使用 operator new 的例子
#include <iostream>
#include <cstdlib> // for std::malloc and std::freeclass MyClass {
public:// 自定义 operator newvoid* operator new(size_t size) {std::cout << "Custom operator new: Allocating " << size << " bytes" << std::endl;void* ptr = std::malloc(size);if (!ptr) {throw std::bad_alloc(); // 如果分配失败,抛出异常}return ptr;}// 自定义 operator deletevoid operator delete(void* ptr) noexcept {std::cout << "Custom operator delete: Releasing memory" << std::endl;std::free(ptr);}MyClass() {std::cout << "Constructor called" << std::endl;}~MyClass() {std::cout << "Destructor called" << std::endl;}
};int main() {// 使用自定义的 operator newMyClass* obj = new MyClass();// 释放内存,调用自定义的 operator deletedelete obj;return 0;
}
operator new(size_t size)
:
这是自定义的内存分配函数。当 new 操作符在 MyClass 类上使用时,会调用这个函数
在这个函数中,我们使用 std::malloc 分配指定大小的内存。如果分配失败,抛出 std::bad_alloc 异常
当写 new MyClass() 时,C++ 编译器会执行以下操作:
计算内存大小:编译器计算要分配的对象所需的内存大小。这是通过 sizeof(MyClass) 完成的,它返回 MyClass 对象所占的字节数(本例中占1个字节)
调用 operator new:编译器将计算出的大小作为参数传递给 operator new。在你自定义的 operator new 中,size_t size 参数将接收到这个大小
调用构造函数:内存分配完成后,new 操作符会在分配的内存上调用 MyClass 的构造函数,初始化对象。
返回指针:最后,new 操作符返回一个指向新分配并初始化的对象的指针
operator delete(void* ptr):
这是自定义的内存释放函数。当 delete 操作符在 MyClass 类上使用时,会调用这个函数
在这个函数中,我们使用 std::free 释放内存
MyClass 构造函数和析构函数:
构造函数和析构函数用于确认内存分配和释放操作的顺序
运行结果
对于 operator delete 函数或者 operator delete[] 函数来说,它们的返回类型必须是 void,第一个形参的类型必须是 void*。执行一条 delete 表达式 将调用相应的 operator 函数,并用指向待释放内存的指针来初始化 void* 形参
当 将 operator delete 或 operator delete[] 定义成类的成员时,该函数可以包含 另外一个是 size_t 类型的形参。此时,该形参的初始值是 第一个形参所指对象的字节数。size_t 形参 可用于删除继承体系中的对象。如果基类有一个虚析构函数,则传递给 operator delete 的字节数 将因待删除指针所指对象的动态类型不同 而有所区别。而且,实际运行的 operator delete 函数版本 也由对象的动态类型决定
在多态环境下,通常 通过基类指针来操作派生类对象。为了确保 删除对象时 能够正确调用派生类的析构函数,通常需要 在基类中使用虚析构函数。然而,仅依靠虚析构函数,operator delete函数 并不能得知对象的具体大小(派生类的大小),这可能会 影响某些自定义内存管理方案
通过将 operator delete 定义为带有 size_t 参数的成员函数,可以得到 待删除对象的实际大小,从而处理不同类型对象的内存释放
#include <iostream>
#include <cstdlib>class Base {
public:// 带有 size_t 参数的 operator deletevoid operator delete(void* ptr, size_t size) {std::cout << "Base::operator delete called. Size: " << size << " bytes" << std::endl;std::free(ptr); // 释放内存}virtual ~Base() { // 虚析构函数std::cout << "Base destructor called" << std::endl;}
};class Derived : public Base {
public:int data[100]; // 额外的数据成员~Derived() {std::cout << "Derived destructor called" << std::endl;}
};int main() {Base* obj = new Derived(); // 通过基类指针创建派生类对象delete obj; // 通过基类指针删除派生类对象return 0;
}
Base 类定义了一个带有 size_t 参数的 operator delete,该参数表示 删除对象的大小
Base 类有一个虚析构函数,因此通过 Base* 指针 删除派生类对象时,将正确调用派生类的析构函数
delete obj; 的调用顺序如下:
- 调用对象的虚析构函数:
编译器根据 obj 的动态类型(即对象的实际类型)确定调用哪个析构函数。由于基类的析构函数是虚函数,delete obj;
会首先调用派生类的析构函数 - 调用派生类的析构函数:
派生类的析构函数被调用,这将销毁派生类特有的成员
如果派生类的析构函数 没有显式调用基类的析构函数,编译器会在析构函数结束时 自动调用基类的析构函数 - 调用基类的析构函数:
在派生类析构函数完成后,基类的析构函数会被调用,释放基类的资源 - 调用 operator delete 函数释放内存:
在所有析构函数调用完毕之后,编译器会调用相应的 operator delete 函数来释放对象占用的内存。如果类中自定义了 operator delete,则调用自定义的版本,否则调用全局的 operator delete(调用 Base 类的 operator delete,此时传递给它的 size_t 参数将是 Derived 对象的实际大小)
operator delete 调用基类版本:如果基类和派生类定义了不同的 operator delete 函数,只有基类版本会在 delete obj;
语句中被调用,因为编译器会根据静态类型(Base*)来决定使用哪个 operator delete(因为 operator new 和 operator delete 的调用 发生在对象构造和析构的两端,而在这两个阶段,动态类型信息还未完全建立(构造时)或者已被销毁(析构时))
当使用 delete 运算符删除对象时,如果定义了带有 size_t 参数的 operator delete,编译器 会根据对象的动态类型传递对象的实际大小。这是 因为虚析构函数机制 使得编译器能够确定对象的动态类型,并且在调用 operator delete 时将对象的实际大小作为参数传递
析构函数 和 delete
class MyClass {
public:MyClass() {data = new int[100]; // 动态分配内存}~MyClass() {delete[] data; // 在析构函数中释放内存}private:int* data;
};
在 main 中调用delete obj;
会:
调用析构函数 ~MyClass(),释放动态分配的数组 data
自动调用 operator delete 来释放 obj 本身占用的内存
标准库函数 operator new 和 operator delete 的名字容易让人误解。和其他 operator 函数不同(比如 operator=),这两个函数并没有重载 new 表达式或 delete 表达式,根本无法自定义 new 表达式或 delete 表达式的行为
- new 和 delete 表达式
new 表达式:new 表达式用于 动态分配内存并构造对象。它的典型形式是 new Type(args),这会做两件事情:
调用 operator new 来分配足够的内存。
在分配的内存上调用构造函数来初始化对象。
delete 表达式:delete 表达式用于销毁对象并释放内存。它的典型形式是 delete ptr,这也会做两件事情:
调用对象的析构函数来清理资源。
调用 operator delete 来释放内存。 - operator new 和 operator delete
operator new:这是一个函数,负责实际的内存分配。它接受一个 size_t 参数,表示要分配的字节数,并返回一个 void*,指向分配的内存块。operator new 只负责分配原始内存,而不涉及对象的构造(构造函数负责)。
operator delete:这是一个函数,负责实际的内存释放。它接受一个 void* 参数,指向要释放的内存块。operator delete 只负责释放原始内存,而不涉及对象的析构
提供新的 operator new 函数和 operator delete 函数的目的 在于改变内存分配的方式,但是不管怎样,都不能改变 new 运算符和 delete 运算符的基本含义
7、malloc 函数与 free 函数
当 定义了自己的全局 operator new 和 operator delete 后,这两个函数 必须以某种方式执行 分配内存与释放内存的操作。也许初衷 仅仅是使用一个特殊定制的内存分配器,但是 这两个函数应该 同时满足 分配内存的方式 与常规方式类似
可以使用名为 malloc 和 free 的函数,C++从 C 语言中继承了这些函数,并将其定义在 cstdlib 头文件中
malloc 函数接受 一个表示待分配字节数的 size_t,返回指向分配空间的指针 或者返回 0 以表示分配失败。free 函数接受一个 void*,它是 malloc 返回的指针的副本,free 将相关内存返回给系统。调用 free(0) 没有意义
编写 operator new 和 operator delete 的一种简单方式
void *operator new(size_t size) {if (void *mem = malloc(size))return mem;elsethrow bad_alloc();
}
void operator delete(void *mem) noexcept { free(mem); }
在标准的 std::allocator 实现中,allocate 通常是通过调用 operator new 来实现的。因此,allocator 的内存分配功能实际上是基于 operator new 构建的
allocate 函数:
allocator 的 allocate 函数负责分配内存,但仅分配内存,不调用构造函数
allocate 的返回值是一个指向分配的未初始化内存块的指针
deallocate 函数:
allocator 的 deallocate 函数负责释放由 allocate 分配的内存,但不调用析构函数
#ifndef STRVEC_H_
#define STRVEC_H_#include <string>
#include <utility>
#include <memory>
#include <algorithm>
#include <iostream>void *operator new(size_t size)
{std::cout << "void *operator new(size_t size)" << std::endl;if(void *mem = malloc(size))return mem;elsethrow std::bad_alloc();
}void operator delete(void *mem) noexcept
{std::cout << "void operator delete(void *mem) noexcept" << std::endl;free(mem);
}class StrVec
{
friend bool operator==(StrVec &lhs, StrVec &rhs);
friend bool operator!=(StrVec &lhs, StrVec &rhs);
friend bool operator<(StrVec &lhs, StrVec &rhs);
friend bool operator>(StrVec &lhs, StrVec &rhs);
friend bool operator<=(StrVec &lhs, StrVec &rhs);
friend bool operator>=(StrVec &lhs, StrVec &rhs);
public:StrVec() : elements(nullptr), first_free(nullptr), cap(nullptr) { }StrVec(std::initializer_list<std::string>);StrVec(const StrVec&);StrVec(StrVec &&s) noexcept : alloc(std::move(s.alloc)), elements(std::move(s.elements)), first_free(std::move(s.first_free)), cap(std::move(s.cap)) { s.elements = s.first_free = s.cap = nullptr; }template <typename... Args>void emplace_back(Args&&... args);StrVec &operator=(const StrVec&);StrVec &operator=(StrVec&&) noexcept;std::string& operator[](std::size_t n) { return elements[n]; }const std::string& operator[](std::size_t n) const { return elements[n]; }~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);
private:std::allocator<std::string> alloc;void chk_n_alloc() { if(size() == capacity()) reallocate(); }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::StrVec(std::initializer_list<std::string> il)
{auto newdata = alloc_n_copy(il.begin(), il.end());elements = newdata.first;first_free = cap = newdata.second;
}template <typename... Args>
inline void StrVec::emplace_back(Args&&... args)
{chk_n_alloc();alloc.construct(first_free++, std::forward<Args>(args)...);
}void StrVec::push_back(const std::string &s)
{chk_n_alloc();alloc.construct(first_free++, s);
}std::pair<std::string*,std::string*> StrVec::alloc_n_copy(const std::string *b, const std::string *e)
{auto data = alloc.allocate(e-b);return {data, uninitialized_copy(b, e, data)};
}void StrVec::free()
{if(elements){std::for_each(elements, first_free, [this](std::string &p){ alloc.destroy(&p); });// for(auto p = first_free; p != elements; )// alloc.destroy(--p);alloc.deallocate(elements, cap-elements);}
}StrVec::StrVec(const StrVec &s)
{auto newdata = alloc_n_copy(s.begin(), s.end());elements = newdata.first;first_free = cap = newdata.second;
}StrVec::~StrVec()
{free();
}void StrVec::reserve(size_t n)
{if(n > capacity()) return;auto newdata = alloc.allocate(n);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 + n;
}void StrVec::resize(size_t n)
{resize(n,std::string());
}void StrVec::resize(size_t n, const std::string &s)
{if(n < size()){while(n < size())alloc.destroy(--first_free);}else if(n > size()){while(n > size())push_back(s);// alloc.construct(first_free, s);}
}StrVec &StrVec::operator=(const StrVec &rhs)
{auto data = alloc_n_copy(rhs.begin(), rhs.end());free();elements = data.first;first_free = cap = data.second;return *this;
}StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{if(this != &rhs){free();alloc = std::move(rhs.alloc);elements = std::move(rhs.elements);first_free = std::move(rhs.first_free);cap = std::move(rhs.cap);rhs.elements = rhs.first_free = rhs.cap = nullptr;}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;
}bool operator==(StrVec &lhs, StrVec &rhs)
{return lhs.size() == rhs.size() && std::equal(lhs.begin(), lhs.end(), rhs.begin());
}bool operator!=(StrVec &lhs, StrVec &rhs)
{return !(lhs == rhs);
}bool operator<(StrVec &lhs, StrVec &rhs)
{return std::lexicographical_compare(lhs.begin(), lhs.end(), rhs.begin(), rhs.end());
}bool operator>(StrVec &lhs, StrVec &rhs)
{return rhs < lhs;
}bool operator<=(StrVec &lhs, StrVec &rhs)
{return !(rhs < lhs);
}bool operator>=(StrVec &lhs, StrVec &rhs)
{return !(lhs < rhs);
}#endif
1.2 定位 new 表达式
1、需要调用 operator new 和 operator delete。这两个函数的行为与 allocator 的 allocate 成员和 deallocate 成员非常类似,它们负责分配或释放内存空间,但是不会构造或销毁对象
与 allocator 不同的是,对于 operator new 分配的内存空间来说 无法使用 construct 函数构造对象。应该使用 new 的定位 new 形式构造对象。new 的这种形式 为分配函数提供了额外的信息。可以使用定位 new 传递一个地址,此时定位 new 的形式如下所示:
new (place_address) type
new (place_address) type (initializers)
new (place_address) type [size]
new (place_address) type [size] { braced initializer list }
其中 place_address 必须是一个指针,同时在 initializers 中提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新分配的对象
当仅通过一个地址值调用new (place_address) type
时,定位 new 使用 operator new(size_t, void*)
“分配”它的内存。这是一个 无法自定义的 operator new 版本。该函数不分配任何内存,它只是简单地返回指针实参;然后由 new 表达式负责在指定的地址初始化对象以完成整个工作。事实上,定位 new 允许 在一个特定的、预先分配的内存地址上构造对象
void* buffer = std::malloc(sizeof(int)); // 分配一块原始内存
int* p = new (buffer) int(42); // 在 buffer 所指向的内存上构造一个 int 对象
尽管在很多时候使用定位 new 与 allocator 的 construct 成员非常相似,但在它们之间也有一个重要的区别。传给 construct 的指针 必须指向同一个 allocator 对象分配的空间(allocator 的设计是为了管理自己分配的内存),但是传给定位 new 的指针 无须指向 operator new 分配的内存(可以指向任何已分配的内存,见上面的例子)
operator new 和 定位 new
#include <iostream>
#include <cstdlib> // for std::malloc and std::freeclass MyClass {
public:int x, y;MyClass(int a, int b) : x(a), y(b) {std::cout << "MyClass constructor called with values: " << x << ", " << y << std::endl;}~MyClass() {std::cout << "MyClass destructor called for: " << x << ", " << y << std::endl;}// 重载全局的 operator newstatic void* operator new(size_t size) {std::cout << "Custom operator new called. Size: " << size << " bytes" << std::endl;void* p = std::malloc(size); // 使用 malloc 分配内存if (!p) throw std::bad_alloc();return p;}// 重载全局的 operator deletestatic void operator delete(void* p) noexcept {std::cout << "Custom operator delete called." << std::endl;std::free(p); // 释放内存}
};int main() {// 使用自定义的 operator new 分配内存MyClass* obj1 = new MyClass(10, 20);// 手动分配内存块void* buffer = std::malloc(sizeof(MyClass));if (!buffer) {std::cerr << "Memory allocation failed" << std::endl;return 1;}// 使用定位 new (placement new) 在预分配的内存上构造对象MyClass* obj2 = new (buffer) MyClass(30, 40);// 删除第一个对象,调用自定义的 operator deletedelete obj1;// 手动调用析构函数销毁 obj2obj2->~MyClass();// 释放手动分配的内存块std::free(buffer);return 0;
}
输出:
Custom operator new called. Size: 8 bytes
MyClass constructor called with values: 10, 20
MyClass constructor called with values: 30, 40
Custom operator delete called.
MyClass destructor called for: 10, 20
MyClass destructor called for: 30, 40
2、显式的析构函数调用
就像定位 new 与使用 allocate 类似一样,对析构函数的显式调用 也与使用 destroy 很类似。既可以 对对象调用析构函数,也可以 通过对对象的指针或引用调用析构函数
string *sp = new string("a value"); // 分配并初始化一个string对象
sp->~string();
和调用 destroy 类似,调用析构函数 可以清除给定的对象 但不会释放对象所在的内存。如果需要的话,可以重新使用该空间
2、运行时类型识别
1、由两个运算符实现:
- typeid 运算符,用于返回表达式的类型
- dynamic_cast 运算符,用于将基类的指针或引用 安全地转换成 派生类的指针或引用
这两个运算符 用于某种类型的指针或引用,并且 该类型含有虚函数时,运算符 将使用指针或引用所绑定对象的动态类型
这两个运算符 特别适用于以下情况:想使用基类对象的指针或引用 执行某个派生类操作 并且该操作不是虚函数。一般来说,只要有可能 应尽量使用虚函数。当操作 被定义成虚函数时,编译器将根据对象的动态类型 自动地选择正确的函数版本
#include <iostream>// 基类
class Animal {
public:// 虚函数virtual void makeSound() const {std::cout << "Animal makes a sound." << std::endl;}// 虚析构函数virtual ~Animal() {std::cout << "Animal destructor called." << std::endl;}
};// 派生类 Dog
class Dog : public Animal {
public:// 重写 makeSound 函数void makeSound() const override {std::cout << "Dog barks: Woof! Woof!" << std::endl;}~Dog() {std::cout << "Dog destructor called." << std::endl;}
};// 派生类 Cat
class Cat : public Animal {
public:// 重写 makeSound 函数void makeSound() const override {std::cout << "Cat meows: Meow! Meow!" << std::endl;}~Cat() {std::cout << "Cat destructor called." << std::endl;}
};int main() {// 基类指针指向派生类对象Animal* myDog = new Dog();Animal* myCat = new Cat();// 通过基类指针调用虚函数myDog->makeSound(); // 输出 "Dog barks: Woof! Woof!"myCat->makeSound(); // 输出 "Cat meows: Meow! Meow!"// 删除对象时,调用虚析构函数确保派生类的析构函数被正确调用delete myDog; // 先调用 Dog 的析构函数,再调用 Animal 的析构函数delete myCat; // 先调用 Cat 的析构函数,再调用 Animal 的析构函数return 0;
}
假定 无法使用虚函数,则可以使用一个 RTTI 运算符。另一方面,与虚成员函数相比,使用 RTTI 运算符蕴含着更多的潜在风险:程序员必须清楚地知道 转换的目标类型 并且必须检查类型转换是否成功执行
2.1 dynamic_cast 运算符
1、
dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)
type 必须是一个类类型,并且通常情况下 该类型应该含有虚函数。在第一种形式中,e 必须是一个有效的指针;在第二种形式中,e 必须是一个左值(表达式结束后依然存在的对象,可以取地址);在第三种形式中,e 不能是左值(右值引用 (type&&) 只能绑定到右值(通常是一个临时对象,不存在持久地址)上,
无论是左值引用(T&)还是右值引用(T&&),它们本身都是左值)
在上面的所有形式中,e 的类型 必须符合以下三个条件中的任意一个:e 的类型是目标 type 的公有派生类(公有继承)、e 的类型是目标 type 的公有基类 或者 e 的类型就是目标 type 的类型(一定是公有,编译器必须能够将派生类对象 视为基类对象。如果继承关系是 protected 或 private,编译器 无法保证派生类对象 能公开地表现为基类对象)。如果符合,则类型转换可以成功。否则,转换失败。如果一条 dynamic_cast 语句的转换目标是指针类型并且失败了,则结果为 0。如果转换目标是引用类型并且失败了,则 dynamic_cast 运算符 将抛出一个 bad_cast 异常
class Base {
public:virtual ~Base() {} // 虚析构函数使得类成为多态基类
};class Derived : public Base {};int main() {Derived d;Base* basePtr = &d;// 向下转换:从基类指针到派生类指针,合法,因为指向的本来就是 Drived 对象Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);if (derivedPtr) {std::cout << "Successfully casted to Derived*" << std::endl;}// 向上转换:从派生类指针到基类指针,合法且隐式,不需要 dynamic_castBase* basePtr2 = derivedPtr;// 尝试非法转换:从基类指针到派生类指针(逆向)// 如果 Base 是 Derived 的公有基类(即 e 是目标 type 的基类),这种转换会报错// Base* b = new Base(); // 跟之前向下转换不一样,指向的真的是一个 Base 对象// Derived* illegalCast = dynamic_cast<Derived*>(b); // 编译错误return 0;
}
dynamic_cast 依赖于 RTTI:RTTI 需要类有虚函数 来生成虚表和虚指针
虚函数引入多态性:只有当类包含 至少一个虚函数时,它才是多态类,编译器才会 为其生成虚表和 RTTI 信息
安全的向下转换:dynamic_cast 可以使用 RTTI 在运行时进行安全的类型转换,从基类指针转换为派生类指针
向下转换:将基类指针转换为派生类指针。在这种情况下,dynamic_cast 在运行时会检查指针指向的对象 是否实际是派生类类型或其子类的对象。
向上转换:将派生类指针转换为基类指针,这种转换是隐式的,不需要 dynamic_cast
2、指针类型的 dynamic_cast
假设 Base 类至少有一个虚函数,Derived 是 Base 的公有派生类。如果有一个指向 Base 的指针 bp,则 可以在运行时将它转换成指向 Derived 的指针,具体代码如下:
if (Derived *dp = dynamic_cast<Derived*>(bp))
{// 使用 dp 指向的 Derived 对象
} else {// 使用 bp 指向的一个 Base 对象
}
可以 对一个空指针执行 dynamic_cast,结果是所需类型的空指针
在条件部分定义了 dp,这样做的好处是 可以在一个操作中 同时完成类型转换和条件检查两项任务。而且,指针 dp 在 if 语句外部是不可访问的。一旦转换失败,即使后续的代码 忘了做相应判断,也不会接触到 这个未绑定的指针
3、什么时候 dynamic_cast 向下转换会失败
- 对象实际类型与目标类型不匹配
如果基类指针或引用指向的实际对象类型 与 目标派生类类型不匹配,dynamic_cast 将失败
class Base {
public:virtual ~Base() {} // 确保 Base 是多态类
};class Derived : public Base {// 派生类
};class AnotherDerived : public Base {// 另一个派生类
};int main() {Base* bp = new AnotherDerived(); // bp 实际指向 AnotherDerived 对象Derived* dp = dynamic_cast<Derived*>(bp); // 尝试将 Base* 转换为 Derived*if (dp == nullptr) {std::cout << "Conversion failed!" << std::endl; // 由于类型不匹配,转换失败}delete bp;return 0;
}
或者
Base* b = new Base(); // 跟之前向下转换不一样,指向的真的是一个 Base 对象
Derived* illegalCast = dynamic_cast<Derived*>(b); // 编译错误
- 指向不完整对象的指针
如果基类指针或引用指向的是一个不完整的对象(即,构造函数尚未完成的对象),dynamic_cast 可能会失败
Derived* d = dynamic_cast<Derived*>(b); // 在 Base 部分构造完成之前尝试转换
- 多重继承中的歧义
在多重继承中,如果基类指针指向的对象 属于多个派生类,而这些派生类 共享同一个基类,那么 dynamic_cast 可能无法确定转换目标,从而导致转换失败
class Base {
public:virtual ~Base() {}
};class Derived1 : public Base {};class Derived2 : public Base {};class DerivedMultiple : public Derived1, public Derived2 {};int main() {DerivedMultiple obj;Base* bp = &obj; // bp 指向 DerivedMultiple,但通过 Derived1 继承的 Base 部分Derived2* dp = dynamic_cast<Derived2*>(bp); // 尝试转换到 Derived2if (dp == nullptr) {std::cout << "Conversion failed due to ambiguity!" << std::endl;}return 0;
}
- 基类指针或引用实际不指向多态对象
如果基类指针或引用指向的对象不是多态对象(即,基类没有虚函数),则 dynamic_cast 不能工作,并会导致转换失败或未定义行为
4、引用类型的 dynamic_cast
引用类型的 dynamic_cast 与指针类型的 dynamic_cast 在表示错误发生的方式上略有不同。因为不存在所谓的空引用,当对引用的类型 转换失败时,程序抛出一个名为 std::bad_cast 的异常,该异常定义在 typeinfo 标准库头文件中
void f(const Base &b)
{try {const Derived &d = dynamic_cast<const Derived&>(b);// 使用 b 引用的 Derived 对象} catch (bad_cast) {// 处理类型转换失败的情况}
}
19.3 已知存在如下的类继承体系
class A { /* ... */};
class B : public A { /* ... */};
class C : public B { /* ... */};
class D : public B, public A { /* ... */};
(a) A *pa = new C;B *pb = dynamic_cast<B*>(pa);
成功,向下转换:将基类指针 转换为 派生类指针。在这种情况下,dynamic_cast 在运行时 会检查指针指向的对象 是否实际是派生类类型 或 其子类的对象(这题就是 是子类对象)
(c) A *pa = new D;B *pb = dynamic_cast<B*>(pa);
如果一个类通过多重继承继承了 同一个基类的多个副本,那么指向该基类的指针或引用 可能会存在二义性
D 继承了 B,而 B 又继承了 A。
D 还直接继承了 A,因此 D 包含了 A 的两个独立副本:一个是通过 B 继承的,另一个是直接继承的
当 试图通过 A * 指针访问 D 中的 A 部分时,编译器无法确定 应该访问哪一个 A 副本
这个 D 的继承结构本身就有问题,在 D 类中对 A 的任何访问 都会存在不确定性
为了避免这种二义性问题,可以使用虚继承。通过虚继承,基类 A 在派生类中只有一个共享的实例
class A {// 基类 A
};class B : public virtual A {// B 是 A 的虚公有派生类
};class D : public B, public virtual A {// D 多重继承自 B 和 A,但 A 是虚继承的
};
将表达式*pa 转换成类型C&
#include <typeinfo>
#include <iostream>class A
{
public:virtual ~A() {}
};class B : public A
{};class C : public B
{};class D : public B, public A
{};int main(int argc, char const *argv[])
{A *pa = new D;B *pb = dynamic_cast<B*>(pa);if(pb) std::cout << "success" << std::endl; // 因为是指针else std::cout << "fail" << std::endl;A *pa = new C;try{const C &c = dynamic_cast<const C&>(*pa); // 因为是引用}catch(std::bad_cast &e){std::cout << e.what() << std::endl;}return 0;
}
2.2 typeid运算符
1、typeid表达式的的形式是 typeid(e),其中 e 可以是任意表达式或类型的名称
typeid 操作的结果是 一个常量对象的引用,该对象的类型是 标准库类型 type_info 或者 type_info 的公共派生类型。type_info 类定义在 typeinfo 头文件中
2、typeid运算符 可以作用于 任何类型的表达式。和往常一样,顶层 const 被忽略
当顶层 const 出现在运算符的操作数中时,通常会被忽略
const int x = 10;
const int y = 20;
int z = x + y; // 顶层 const 被忽略,正常执行加法操作
对于类:
class MyClass {
public:int value;MyClass(int v) : value(v) {}// 重载加法运算符MyClass operator+(const MyClass& other) const {return MyClass(this->value + other.value);}
};const MyClass a(10);
const MyClass b(20);
MyClass c = a + b; // 顶层 const 被忽略,重载的运算符可以正常工作
如果表达式是一个引用,则 typeid 返回该引用所引对象的类型。不过当 typeid 作用于 数组或函数时,并不会执行向指针的标准类型转换(对数组a执行 typeid(a),则所得的结果是 数组类型而非指针类型)
当运算对象 不属于类类型 或者是 一个不包含任何虚函数的类时,typeid 运算符指示的是 运算对象的静态类型。而当运算对象是 定义了至少一个虚函数的类的左值时,typeid 的结果直到运行时 才会求得
3、使用 typeid 运算符
Derived *dp = new Derived;
Base *bp = dp; // 两个指针都指向 Derived 对象
// 在运行时比较两个对象的类型
typeid 应该作用于对象,因此 使用 *bp 而非 bp
// 下面的检查永远是失败的:bp 的类型是指向 Base 的指针
if (typeid(bp) == typeid(Derived)) {// 此处的代码永远不会执行
}
尽管指针所指的对象类型 是一个含有虚函数的类,但是指针本身 并不是一个类的对象。类型 Base* 将在编译时求值,显然它与 Derived 不同(当 typeid 用于指针时(不是类),返回的结果是 该指针的静态编译时类型)
typeid 是否需要运行时检查 决定了表达式是否会求值。只有 当类型含有虚函数时,编译器才会对表达式求值。反之,如果类型不含有虚函数,则 typeid 返回表达式的静态类型;编译器无须对表达式求值 也能知道表达式的静态类型
#include <iostream>
#include <typeinfo>class Base {// 没有虚函数
};class Derived : public Base {// 没有虚函数
};int main() {Base* p = new Derived();std::cout << typeid(p).name() << std::endl; // typeid(p) 返回的是 Base* 类型return 0;
}
如果表达式的动态类型 可能与静态类型不同,则必须 在运行时 对表达式求值 以确定返回的类型。这条规则适用于 typeid(*p) 的情况。如果指针 p 所指的类型 不含有虚函数,则 p 不必非得是一个有效的指针。否则,*p 将在运行时求值,此时 p 必须是一个有效的指针。如果 p 是一个空指针,则 typeid(*p) 将抛出一个名为 bad_typeid 的异常
19.6-19.8 将 Query_base 指针动态转换为 AndQuery 指针。分别使用 AndQuery 的对象以及其他类型的对象测试转换是否有效
将 Query_base 对象转换为 AndQuery 的引用
typeid 表达式检查两个 Query_base 对象是否指向同一种类型。再检查该类型是否是 AndQuery
#include <typeinfo>
#include <iostream>class A
{
public:virtual ~A() {}
};class B : public A
{};class C : public B
{};class D : public B, public A
{};int main(int argc, char const *argv[])
{A *pa1 = new C;if(C *qc = dynamic_cast<C*>(pa1)){std::cout << "success" << std::endl;}else{std::cout << "fail" << std::endl;}A *pa2 = new C;try{const C &rc = dynamic_cast<const C&>(*pa2);}catch(std::bad_cast &e){std::cout << e.what() << std::endl;}C c = C();if(typeid(*pa1) == typeid(*pa2)) std::cout << "same type" << std::endl;if(typeid(*pa1) == typeid(c)) std::cout << "same type as C" << std::endl;if(typeid(*pa1) == typeid(C)) std::cout << "same type as C" << std::endl;return 0;
}
2.3 使用RTTI
1、当想为具有继承关系的类 实现相等运算符时。对于两个对象来说,如果它们的类型相同 并且对应的数据成成员取值相同,则 说这两个对象是相等的。在类的继承体系中,每个派生类 负责添加自己的数据成员,因此派生类的相等运算符 必须把派生类的新成员考虑进来
定义一套虚函数,令其在继承体系的各个层次上 分别执行相等性判断。此时,可以为基类的引用 定义一个相等运算符,该运算符将其工作委托给虚函数 equal,由 equal 负责实际的操作
上述方案很难奏效。虚函数的基类版本和派生类版本 必须具有相同的形参类型。如果 想定义一个虚函数 equal,则该函数的形参 必须是基类的引用。此时,equal函数 将只能使用基类的成员,而不能比较派生类独有的成员
如果参与比较的两个对象类型不同,则比较结果为 false。例如,如果 试图比较一个基类对象和一个派生类对象,则 == 运算符应该返回 false
使用 RTI 解决问题了。定义的相等运算符的形参是 基类的引用,然后使用 typeid 检查两个运算对象的类型 是否一致。如果运算对象的类型 不一致,则 == 返回 false;类型一致才调用 equal 函数。每个类定义的 equal 函数负责比较类型自己的成员。这些运算符接受 Base& 形参,但是在进行比较操作前先把运算对象转换成运算符所属的类型(使用 dynamic_cast)
#include <iostream>
#include <string>class Base {
public:virtual ~Base() = default;// 基类的相等运算符bool operator==(const Base& other) const {// 检查两个对象是否类型相同,然后调用虚函数 equal 进行具体的比较return typeid(*this) == typeid(other) && this->equal(other);}protected:// 基类的虚函数,用于派生类实现自己的相等性判断逻辑virtual bool equal(const Base& other) const = 0;
};class Derived1 : public Base {
public:Derived1(int data) : data(data) {}protected:// 实现 Derived1 的相等性判断bool equal(const Base& other) const override {// 在安全转换后比较 Derived1 的特定数据成员const Derived1* o = dynamic_cast<const Derived1*>(&other);return o && (this->data == o->data);}private:int data;
};class Derived2 : public Base {
public:Derived2(std::string name, double value) : name(name), value(value) {}protected:// 实现 Derived2 的相等性判断bool equal(const Base& other) const override {// 在安全转换后比较 Derived2 的特定数据成员const Derived2* o = dynamic_cast<const Derived2*>(&other);return o && (this->name == o->name && this->value == o->value);}private:std::string name;double value;
};int main() {Derived1 d1(10);Derived1 d2(10);Derived2 d3("test", 3.14);Derived2 d4("test", 3.14);std::cout << (d1 == d2) << std::endl; // 输出 1(true)std::cout << (d3 == d4) << std::endl; // 输出 1(true)std::cout << (d1 == d3) << std::endl; // 输出 0(false)return 0;
}
2、类的层次关系
class Base {friend bool operator==(const Base&, const Base&);
public:// Base 的接口成员
protected:virtual bool equal(const Base&) const;// Base 的数据成员和其他用于实现的成员
};class Derived: public Base {
public:// Derived 的其他接口成员
protected:bool equal(const Base&) const;// Derived 的数据成员和其他用于实现的成员
};
3、类型敏感的相等运算符
定义整体的相等运算符:
bool operator==(const Base &lhs, const Base &rhs)
{// 如果typeid不相同,返回false;否则虚拟调用equalreturn typeid(lhs) == typeid(rhs) && lhs.equal(rhs);
}
如果运算对象的类型不同 则返回 false。否则,如果运算对象的类型相同,则运算符 将其工作委托给虚函数 equal。当运算对象是 Base 的对象时,调用 Base::equal;当运算对象是 Derived 的对象时,调用 Derived::equal
4、虚 equal 函数
继承体系中的每个类 必须定义自己的 equal 函数。派生类的所有函数 要做的第一件事都是相同的,那就是将实参的类型 转换为派生类类型:
bool Derived::equal(const Base &rhs) const
{// 我们清楚这两个类型是相等的,所以转换过程不会抛出异常auto r = dynamic_cast<const Derived&>(rhs);// 执行比较两个Derived对象的操作并返回结果
}
上面的类型转换 永远不会失败,因为毕竟 只有在验证了运算对象的类型相同之后 才会调用该函数。然而这样的类型转换 是必不可少的,执行了类型转换后,当前函数才能访问 右侧运算对象的派生类成员
5、基类 equal 函数
bool Base::equal(const Base &rhs) const
{// 执行比较Base对象的操作
}
无须 事先转换形参的类型。*this 和形参都是 Base 对象
2.4 type_info 类
type_info 类必须定义在 typeinfo 头文件中
操作 | 解释 |
---|---|
t1 == t2 | 如果 type_info 对象 t1 和 t2 表示同一种类型,返回 true ;否则返回 false |
t1 != t2 | 如果 type_info 对象 t1 和 t2 表示不同的类型,返回 true ;否则返回 false |
t.name() | 返回一个 C 风格字符串,表示 类型名字的可打印形式。类型名字的生成方式 因系统而异,对于 name 返回值的唯一要求是,类型不同则返回的字符串必须有所区别 |
t1.before(t2) | 返回一个 bool 值,表示 t1 是否位于t2 之前。before所采用的顺序关系是 依赖于编译器的 |
因为 type_info 类一般是作为一个基类出现,所以 它还应该提供一个公有的虚析构函数。当编译器希望 提供额外的类型信息时,通常在 type_info 的派生类中完成
type_info 类没有默认构造函数,而且 它的拷贝和移动构造函数以及赋值运算符 都被定义成删除的。因此,无法定义或拷贝 type_info 类型的对象,也不能为 type_info 类型的对象赋值。创建 type_info 对象的唯一途径是使用typeid 运算符
19.10 已知存在如下的类继承体系,其中每个类定义了一个默认公有的构造函数和一个虚析构函数
class A { /* ... */ };
class B : public A { /* ... */ };
class C : public B { /*...*/ };
(a) A *pa = new C;cout << typeid(pa).name() << endl;
(b) C cobj;A& ra = cobj;cout << typeid(&ra).name() << endl;
(c) B *px = new B;A& ra = *px;cout << typeid(ra).name() << endl;
关键点在于 指针和引用的区别 以及 多态性 的使用:
指针类型:对于指针类型 typeid 只返回指针本身的类型(静态类型)。在 (a) 和 (b) 中,typeid(pa)
和 typeid(&ra)
返回的是静态类型 A*,而不是指针所指向的对象的类型
引用类型和多态性:在 © 中,ra 是一个引用,并且 A 有虚函数,因此 typeid(ra) 可以返回引用对象的动态类型,而不是仅限于静态类型
(a) A *pa = new C; cout << typeid(pa).name() << endl;
pa 是一个 A* 类型的指针
静态类型:pa 的静态类型是 A*
动态类型:pa 的动态类型是 C*,但 typeid(pa)
返回的是指针本身的类型,而不是它所指向的对象的类型。因此,typeid(pa)
只会返回静态类型 A*
(b) C cobj; A& ra = cobj; cout << typeid(&ra).name() << endl;
ra 是一个对 A 的引用,但引用的对象实际是 C 类型
静态类型:&ra 的静态类型是 A*
动态类型:&ra 的动态类型是 C*,但是 typeid(&ra) 返回的是 &ra 的静态类型,即 A*,因为这里 typeid 应用在了指针上,指针的类型是静态确定的
© B *px = new B; A& ra = *px; cout << typeid(ra).name() << endl;
ra 是一个对 A 的引用,引用的是 B 类型的对象
静态类型:ra 的静态类型是 A&
动态类型:ra 的动态类型是 B&。因为 A 有虚函数(虚析构函数),这是一个多态类,typeid(ra) 将返回 ra 的动态类型 B
3、枚举类型
1、可以 将一组整型常量组织在一起。和类一样,每个枚举类型 定义了一种新的类型。枚举 属于字面值常量类型
C++包含两种枚举:限定作用域的 和 不限定作用域的。新标准引入了 限定作用域的枚举类型。定义限定作用域的枚举类型的一般形式是:首先是关键字 enum class(或者等价地使用 enum struct),随后是枚举类型名字 以及用花括号括起来的 以逗号分隔的枚举成员列表,最后是一个分号:
enum class open_modes {input, output, append};
定义限定作用域的枚举类型时 省略掉关键字 class(或 struct),枚举类型的名称是可选的:
enum color {red, yellow, green}; // 不限定作用域的枚举类型
// 未命名的、不局限作用域的枚举类型
enum {floatPrec = 6, doublePrec = 10, double_ doublePrec = 10};
如果 enum 是未命名的,则 只能在定义该 enum 时定义它的对象。和类的定义类似,需要在enum 定义的右侧花括号 和最后一个分号之间 提供逗号分隔的声明列表
2、枚举成员
在限定作用域的枚举类型中,枚举成员的名字 遵循常规的作用域规则,并且在枚举类型的作用域外 是不可访问的。与此相反,在不局限作用域的枚举类型中,枚举成员的作用域 与枚举类型本身的作用域相同:
enum color { red, yellow, green }; // 不局限作用域的枚举类型
enum stoplight { red, yellow, green }; // 错误:重复定义了枚举成员
enum class peppers { red, yellow, green }; // 正确:枚举成员被隐藏了color eyes = green; // 正确:不局限作用域的枚举类型的枚举成员位于有效的作用域中
peppers p = green; // 错误:peppers 的枚举成员不在有效的作用域中// color::green 在有效的作用域中,但是类型错误
color hair = color::red; // 正确:允许显式地访问枚举成员
peppers p2 = peppers::red ; // 正确:使用 peppers 的red
默认情况下,枚举值从 0 开始,依次加1 。不过 也能为一个或几个枚举成员指定专门的值:
enum class intTypes {charTyp = 8, shortTyp = 16, intTyp = 16,longTyp = 32, long_longTyp = 64
};
由枚举成员 intTyp 和 shortTyp 可知,枚举值不一定唯一(可以相同)。如果 没有显式地提供初始值,则当前枚举成员的值 等于之前枚举成员的值加 1
枚举成员是 const ,因此在初始化枚举成员时 提供的初始值必须是 常量表达式。也就是说,每个枚举成员本身就是 一条常量表达式,可以在任何需要常量表达式的地方 使用枚举成员。例如,可以定义枚举类型的 constexpr 变量:
constexpr intTypes charbits = intTypes::charTyp;
charbits 只是一个枚举类型 intTypes 的常量,它等于 intTypes::charTyp,并没有为 intTypes 枚举类型添加新的元素。charbits 是一个在编译期确定的常量,而不是新的枚举值
可以将一个 enum 作为 switch 语句的表达式,而将枚举值作为 case 标签。出于同样的原因,还能 将枚举类型作为 一个非类型模板形参使用;或者 在类的定义中 初始化枚举类型的静态数据成员
1)enum 可以作为 switch 语句的表达式,枚举值作为 case 标签
#include <iostream>enum Color { Red, Green, Blue };void printColor(Color c) {switch (c) {case Red:std::cout << "Red" << std::endl;break;case Green:std::cout << "Green" << std::endl;break;case Blue:std::cout << "Blue" << std::endl;break;}
}int main() {Color myColor = Red; // 定义一个枚举变量并赋值printColor(myColor); // 输出 "Red"myColor = Green; // 更改枚举变量的值printColor(myColor); // 输出 "Green"myColor = Blue; // 更改枚举变量的值printColor(myColor); // 输出 "Blue"return 0;
}
2)枚举类型 可以作为非类型模板形参
可以在编译时 基于枚举值生成不同的代码
#include <iostream>enum Color { Red, Green, Blue };// 声明了一个模板结构体 ColorPrinter,它接受一个枚举类型 Color 的值作为模板参数
template <Color color>
struct ColorPrinter;// 对 ColorPrinter 模板的一个 全特化
template <>
struct ColorPrinter<Red> { static void print() { std::cout << "Red" << std::endl; }
};template <>
struct ColorPrinter<Green> {static void print() { std::cout << "Green" << std::endl; }
};template <>
struct ColorPrinter<Blue> {static void print() { std::cout << "Blue" << std::endl; }
};int main() {ColorPrinter<Red>::print(); // 输出 "Red"ColorPrinter<Green>::print(); // 输出 "Green"
}
3)在类定义中初始化枚举类型的静态数据成员
在类中,枚举类型的静态数据成员 可以被声明并初始化。因为静态数据成员 是类级别的成员,因此通常在类外定义并初始化,但在 C++17 及更高版本中,可以在类定义内直接初始化它们
#include <iostream>class MyClass {
public:// 定义一个嵌套的枚举类型 Statusenum class Status { Pending, Approved, Rejected };// 静态 constexpr 成员,初始化为 Status::Pendingstatic constexpr Status defaultStatus = Status::Pending;
};// 如果你使用的是 C++17 之前的版本,则静态 constexpr 成员的初始化需要在类外部进行。
// constexpr MyClass::Status MyClass::defaultStatus; // C++17 之前的做法void printStatus(MyClass::Status status) {switch (status) {case MyClass::Status::Pending:std::cout << "Status: Pending" << std::endl;break;case MyClass::Status::Approved:std::cout << "Status: Approved" << std::endl;break;case MyClass::Status::Rejected:std::cout << "Status: Rejected" << std::endl;break;}
}int main() {// 直接使用静态成员 defaultStatusMyClass::Status status = MyClass::defaultStatus;// 打印状态printStatus(status);// 也可以手动设置其他状态status = MyClass::Status::Approved;printStatus(status);return 0;
}
constexpr 强调的是 编译时计算和不可变性
static 强调的是 变量或函数的生命周期和可见性
3、和类一样,枚举也定义新的类型
只要 enum 有名字,就能定义并初始化 该类型的成员。要想初始化 enum 对象或者为 enum 对象赋值,必须使用该类型的一个枚举成员 或者 该类型的另一个对象:
open_modes om = 2; // 错误:2 不属于类型 open_modes
om = open_modes::input; //正确:input 是 open_modes 的一个枚举成员
一个不局限作用域的枚举类型的对象 或 枚举成员 自动地转换成整型。因此,可以在任何需要整数值的地方 使用它们:
int i = color::red; //正确:不局限作用域的枚举类型的枚举成员 隐式地转换成 int
int j = peppers::red;//错误:局限作用域的枚举类型 不会进行隐式转换
4、指定 enum 的大小
尽管每个 enum 都定义了 唯一的类型,但实际上是 某种整数类型表示的。在C++11新标准中,可以在 enum 的名字后加上冒号 以及 想在该 enum 中使用的类型:
enum intValues : unsigned long long {charTyp = 255, shortTyp = 65535, intTyp = 65535,longTyp = 4294967295UL,long_longTyp = 18446744073709551615ULL
};
如果 没有指定 enum 的潜在类型,则默认情况下 局限作用域的 enum 成员类型是 int。对于不局限作用域的枚举类型来说,其枚举成员 不存在默认类型,只知道成员的潜在类型足够大,肯定能够容纳枚举值。如果指定了 枚举成员的潜在类型(包括对局限作用域的 enum 的隐式指定),则一旦某个枚举成员的值 超出了该类型所能容纳的范围,将引发程序错误
指定 enum 潜在类型的能力 使得可以控制不同实现环境中 使用的类型
enum class 和 enum struct 是相同的:
1)强类型枚举:enum class 和 enum struct 都创建强类型枚举。这意味着枚举值不能隐式转换为整数类型,也不能在不同枚举类型之间进行隐式转换
2)作用域:枚举值在 enum class 或 enum struct 中是局部作用域的,必须通过作用域解析运算符 :: 来访问。例如,Color::Red
3)枚举值的命名冲突:因为枚举值是局部作用域的,不同枚举类型之间的值不会产生冲突
4)底层类型:可以为 enum class 和 enum struct 指定底层类型,例如 enum class Color : int { Red, Green, Blue };
。如果不指定,默认使用 int
5、枚举类型的前置声明
新标准中,可以提前声明 enum(不是定义)。enum 的前置声明(无论隐式地 还是显示地)必须指定其成员的大小:
// 不局限作用域的枚举类型 intValues 的前置声明
enum intValues : unsigned long long; // 不局限作用域的,必须指定成员类型
enum class open_modes; // 局限作用域的枚举类型可以使用默认成员类型 int
和其他声明语句一样,enum 的声明和定义必须匹配,这意味着在该 enum 的所有声明 和 定义中成员的大小必须一致。而且,不能在同一个上下文中先声明一个不局限作用域的 enum 名字,然后再声明一个同名的局限作用域的 enum:
// 错误:所有的声明和定义必须对该 enum 是局限作用域的还是不局限作用域的保持一致
enum class intValues; // 错误:intValues 已经被声明成局限作用域的 enum
enum intValues : long; // 错误:intValues 已经被声明为 int
6、形参匹配与枚举类型
要想初始化一个 enum 对象,必须使用该 enum 类型的另一个对象 或者 它的一个枚举成员(不能超成员范围)。因此,即使某个整型值 恰好与枚举成员的值相等,它也不能 作为函数的 enum 实参使用:
// 不局限作用域的枚举类型,潜在类型因机器而异
enum Tokens {INLINE = 128, VIRTUAL = 129};
void ff(Tokens);
void ff(int);
int main() {Tokens curTok = INLINE; // 一个枚举成员(不能超成员范围)ff(128); // 精确匹配ff(int)ff(INLINE); //精确匹配ff(Tokens)ff(curTok); //精确匹配ff(Tokens)return 0;
}
尽管 不能直接将整型值传给 enum 形参,但是可以 将一个不局限作用域的枚举类型的对象 或 枚举成员传给整型形参。此时,enum 的值提升成 int 或 更大的整型,实际提升的结果 由枚举类型的潜在类型决定:
void newf(unsigned char);
void newf(int);
unsigned char uc = VIRTUAL;
newf(VIRTUAL); // 调用newf(int)
newf(uc); // 调用newf(unsigned char)
枚举类型 Tokens 只有两个枚举成员,其中较大的那个值是 129。该枚举类型可以用 unsigned char 来表示,因此很多编译器使用 unsigned char 作为 Tokens 的潜在类型
不管 Tokens 的潜在类型到底是什么,它的对象和枚举成员都提升成 int。尤其是,枚举成员永远不会提升成 unsigned char(C++ 的类型提升规则 规定了 整数提升总是 将较小的整数类型提升为 int 或 unsigned int,而不是更小的类型),即使枚举值可以用 unsigned char 存储也是如此
4、类成员指针
成员指针 是指可以指向类的非静态成员的指针。一般情况下,指针指向一个对象,但是成员指针指示的是 类的成员,而非类的对象。类的静态成员 不属于任何对象,因此无须特殊的指向静态成员的指针,指向静态成员的指针与普通指针没有什么区别
成员指针的类型 包括了 类的类型以及成员的类型。当初始化 一个这样的指针时,令其指向类的某个成员,但是不指定 该成员所属的对象;直到使用成员指针时,才提供 成员所属的对象
class Screen {
public:typedef std::string::size_type pos;char get_cursor() const { return contents[cursor]; }char get() const;char get(pos ht, pos wd) const;
private:std::string contents;pos cursor;pos height, width;
};
4.1 数据成员指针
1、在声明成员指针时 也使用*
来表示 当前声明的名字是一个指针。与普通指针不同的是,成员指针 还必须包含成员所属的类。因此,必须要在*
之前添加 class name::
以表示 当前定义的指针 可以指向 class name 的成员。例如:
// pdata 可以指向一个常量(非常量)Screen 对象的 string 成员
const string Screen::*pdata;
上述语句将 pdata 声明成 “一个指向 Screen 类的 const string 成员的指针”。常量对象的数据成员本身也是常量,因此 将指针声明成指向 const string 成员的指针 意味着 pdata 可以指向任何 Screen 对象的一个成员,而不管该 Screen 对象是否是常量。作为交换条件,只能使用 pdata 读取它所指的成员,而不能向它写入内容
当 初始化一个成员指针(或者向它赋值)时,需指定 它所指的成员。可以令 pdata 指向某个非特定 Screen 对象的 contents 成员:
pdata = &Screen::contents;
将取地址运算符 作用于 Screen 类的成员 而非内存中的一个该类对象
新标准中 声明成员指针 最简单的方法是 使用 auto 或 decltype:
auto pdata = &Screen::contents;
2、使用数据成员指针
初始化一个成员指针 或 为成员指针赋值时,该指针 并没有指向任何数据。成员指针 指定了 成员而非该成员所属的对象,只有 当解引用成员指针时 才提供对象的信息
与成员访问运算符 .
和 ->
类似,也有两种成员 指针访问运算符 .*
和 ->*
,这两个运算符 使得可以解引用指针 并获得该对象的成员:
Screen myScreen, *pSreen = &myScreen;
// .* 解引用 pdata 以获得 myScreen 对象的 contents 成员
auto s = myScreen.pdata;
// ->* 解引用 pdata 以获得 pSreen 所指对象的 contents 成员
s = pSreen->*pdata;
这些运算符执行两步操作:首先 解引用成员指针 以得到所需的成员;然后 像成员访问运算符一样,通过对象(.)或指针(->)获取成员
3、返回数据成员指针的函数
常规的访问控制规则 对成员指针同样有效。例如,Screen 的 contents 成员是私有的,因此之前对于 pdata 的使用 必须位于 Screen 类的成员或友元内部
因为数据成员一般情况下是私有的,如果一个像 Screen 这样的类 希望 可以访问它的 contents 成员,最好定义一个函数,令其返回值是 指向该成员的指针:
由于 data() 函数是静态的,它无法访问 任何特定对象的非静态成员(例如 contents),但它可以返回一个成员指针。成员指针本身 并不需要与特定实例关联,它只是表示类的某个成员的位置。换句话说,data() 返回的是一个“指向类成员的指针”,而不是指向 某个特定实例成员的指针
这种设计的用途 在于,尽管 data() 是静态的,它仍然可以 让你获取 Screen 类中的某个成员(例如 contents)的地址,然后 在特定的实例上 使用这个成员指针
class Screen {
public:// data 是一个静态成员(该成员在所有类的对象之间共享),返回一个成员指针static const std::string Screen::*data() { return &Screen::contents; }// 其他成员与之前的版本一致
};
可以这样使用
Screen myScreen;
const std::string* ptr = &(myScreen.*Screen::data());
Screen::data() 返回一个指向 contents 的成员指针,然后通过 myScreen.*ptr 访问具体对象的 contents
为 Screen 类添加了 一个静态成员,令其返回指向 contents 成员的指针,data 返回的是一个指向 Screen 类的 const string 成员的指针
当 调用 data 函数时,将得到一个成员指针:
// data() 返回一个指向 Screen 类的 contents 成员的指针
const string* pdata = Screen::data();
pdata 指向 Screen 类的成员 而非实际数据。要想使用 pdata,必须 把它绑定到 Screen 类型的对象上:
// 获得 myScreen 对象的 contents 成员
auto s = *myScreen.pdata;
19.11 普通的数据指针 和 指向数据成员的指针 有何区别
普通的数据指针 指向一个对象;类成员指针指向 类的非静态成员。当初始化 这样一个指针时,令其指向类的某个成员,但是 不指定该成员所属的对象;直到 使用成员指针时,才提供所属的对象
19.12 定义一个成员指针,令其可以指向 Screen 类的 cursor 成员。通过该指针获得 Screen::cursor 的值
#include <string>
#include <iostream>using pos = std::string::size_type;class Screen {
public:static const std::string Screen::* data() { return &Screen::contents; } // 为什么contents必须加Screen::,下面get就不加?解答见下static const pos Screen::* pcursor() { return &Screen::cursor; }Screen() = default;Screen(pos ht, pos wd, char c) :height(ht), width(wd), contents(ht* wd, c) { }char get() const { return contents[cursor]; }char get(pos r, pos c) const { return contents[r * width + c]; }private:pos cursor = 0;pos height = 0, width = 0;std::string contents;
};int main()
{const std::string Screen::* pdata = Screen::data(); //Screen myScreen(2, 2, 'c');auto s = myScreen.*pdata;//std::cout << s << std::endl;Screen myScreen2(2, 2, 'b');s = myScreen2.*pdata;// 不管对象是谁,反正那出来对应成员std::cout << s << std::endl;const pos Screen::* pcursor = Screen::pcursor();//auto i = myScreen.*pcursor;//std::cout << i << std::endl;return 0;
}
定义一个静态成员函数或静态成员函数的返回类型 是一个指向类成员的指针时,必须明确 指定该成员属于哪个类。这是因为 在静态成员函数的上下文中,没有实例的具体对象 来解析非静态成员的名称,所以编译器 需要明确知道该成员属于哪个类(静态函数和类本身无关,不依赖于具体实例,所以需要通过 Screen:: 这种语法明确指出是哪个类的成员)
4.2 成员函数指针
1、也可以 定义指向类的成员函数的指针
char get_cursor() const {return contents[cursor];
}
// pmf 是一个指针,它可以指向 Screen 的某个常量成员函数
// 前提是该函数不接受任何实参,并且返回一个 char
auto pmf = &Screen::get_cursor;
和指向数据成员的指针 一样,使用 classname::*
的形式 声明一个指向成员函数的指针。类似于 任何其他函数指针,指向成员函数的指针 也需要指定目标函数的 返回类型和形参列表。如果成员函数是 const 成员 或者引用成员,则必须将 const 限定符或引用限定符包含进来
如果 成员存在重载的问题,则 必须显式地声明函数类型 以明确指出想要使用的是哪个函数。例如,可以声明一个指针,令其指向 含有两个形参的 get:
char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;
出于优先级的考虑,上述声明中 Screen::*
两端的括号必不可少(*
是成员指针操作符,它的优先级低于函数调用操作符(),会认为 pmf2 是一个接受两个 Screen::pos 参数 并返回一个 char Screen::*
类型的函数。这里的 char Screen::*
表示一个指向 Screen 类中 char 类型成员的指针)
const 限定符只能用于成员函数,不能用于非成员函数,表示该函数 不会修改调用该函数的对象的状态(非成员函数 不属于任何类实例,因此没有“对象状态”的概念)
// 错误:因为它声明的是一个普通函数,所以不能使用 const 限定符。非成员函数 p 不能使用 const 限定符
// 错误:这个声明试图定义一个名为 p 的普通函数,并且返回 Screen 类的一个 char 成员
char Screen::*p(Screen::pos, Screen::pos) const;
在普通函数指针声明中,括号的主要作用 也是确保正确的优先级
int *funcPtr(int, int); // 声明一个函数,返回类型是 int*
int (*funcPtr)(int, int); // 声明一个指针,指向一个返回类型为 int 的函数
展示了 如何声明、赋值和使用函数指针
#include <iostream>// 定义两个简单的函数
int add(int a, int b) {return a + b;
}int multiply(int a, int b) {return a * b;
}int main() {// 1. 声明函数指针// 该指针可以指向返回类型为 int,接受两个 int 参数的函数int (*funcPtr)(int, int);// 2. 将函数地址赋给函数指针funcPtr = &add;// 3. 使用函数指针调用函数int result = funcPtr(3, 4);std::cout << "Result of add: " << result << std::endl; // 输出 7// 4. 更改函数指针指向不同的函数funcPtr = &multiply;// 5. 再次使用函数指针调用函数result = funcPtr(3, 4);std::cout << "Result of multiply: " << result << std::endl; // 输出 12return 0;
}
和普通函数指针 不同的是,在成员函数和指向该成员的指针之间 不存在自动转换规则:
// pmf 指向一个 Screen 成员,该成员 不接受任何实参且返回类型是 char
pmf = &Screen::get; // 必须显式地使用取地址运算符
pmf = Screen::get; // 错误:在成员函数和指针之间不存在自动转换规则
2、使用 成员函数指针
和使用指向数据成员的指针 一样,使用 .*
或者 ->*
运算符 作用于 指向成员函数的指针,以调用类的成员函数:
Screen myScreen, *pScreen = &myScreen;
// 跟普通函数指针差不多,就多了一个对象调用的过程
// 通过 pScreen 所指的对象调用 pmf 所指的函数
char c1 = (pScreen->*pmf)();
// 通过 myScreen 对象将实参 0, 0 传给含有两个形参的 get 函数
char c2 = (myScreen.*pmf2)(0, 0);
之所以 (myScreen->*pmf)()
和 (pScreen->pmf2)(0, 0)
的括号必不可少,原因是 调用运算符的优先级 要高于指针指向成员运算符的优先级
假设去掉括号的话,myScreen.pmf()
,其含义 将等同于下面的式子:myScreen.(pmf())
,这行代码的意思是 调用一个名为 pmf 的函数,然后 使用该函数的返回值 作为指针指向成员运算符(.)的运算对象。然而 pmf 并不是一个函数,因此代码将发生错误
3、使用成员指针的类型别名
下面的类型别名将 Action 定义为 两参数 get 函数的同义词:
// Action 是一种可以指向 Screen 成员函数的指针,它接受两个 pos 实参,返回一个 char
using Action = char (Screen::*)(Screen::pos, Screen::pos) const;
Action 是某类型的另外一个名字,该类型是 “指向 Screen 类的常量成员函数的指针,其中这个成员函数 接受两个 pos 形参,返回一个 char”。通过使用 Action,可以简化指向 get 的指针定义:
Action get = &Screen::get; // get指向Screen的get成员
和其他函数指针 类似,可以 将指向成员函数的指针 作为某个函数的返回类型或形参类型。其中,指向成员的指针形参 也可以拥有默认实参:
// action 接受一个Screen的引用,和一个指向Screen成员函数的指针
Screen& action(Screen&, Action = &Screen::get);
调用 action 时,只需将 Screen 的一个符合要求的函数的指针 或 地址传入即可:
Screen myScreen;
// 等价的调用:
action(myScreen); // 使用默认实参
action(myScreen, get);// 使用我们之前定义的变量get
action(myScreen, &Screen::get);// 显式地传入地址
4、成员指针函数表
对于普通函数指针和指向成员函数的指针 来说,一种常见的用法是 将其存入一个函数表当中
函数表 是一个数组或列表,其中每个元素 都是一个函数指针。函数表用于 根据索引或某种条件动态调用不同的函数
函数表的基本实现:
#include <iostream>// 定义一些函数
void funcA() {std::cout << "Function A called" << std::endl;
}void funcB() {std::cout << "Function B called" << std::endl;
}void funcC() {std::cout << "Function C called" << std::endl;
}int main() {// 1. 定义一个函数指针类型typedef void (*FuncPtr)();// 2. 创建一个函数指针数组(函数表)FuncPtr functionTable[3] = { funcA, funcB, funcC };// 3. 根据索引调用函数for (int i = 0; i < 3; ++i) {functionTable[i](); // 调用函数表中的函数}return 0;
}
输出:
Function A called
Function B called
Function C called
函数表可以用于更加复杂的场景,以下是一个状态机(通过状态、事件和状态转换 来描述系统的行为)的简单例子:
#include <iostream>enum State {STATE_A,STATE_B,STATE_C,NUM_STATES
};// 状态处理函数
void handleStateA() { std::cout << "Handling State A" << std::endl; }
void handleStateB() { std::cout << "Handling State B" << std::endl; }
void handleStateC() { std::cout << "Handling State C" << std::endl; }int main() {// 定义一个函数表,每个函数处理一个状态void (*stateTable[NUM_STATES])() = { handleStateA, handleStateB, handleStateC };// 当前状态State currentState = STATE_A;// 模拟状态转换for (int i = 0; i < NUM_STATES; ++i) {stateTable[currentState](); // 调用当前状态对应的函数currentState = static_cast<State>((currentState + 1) % NUM_STATES); // 转换到下一个状态}return 0;
}
currentState = static_cast<State>((currentState + 1) % NUM_STATES)
:枚举类型(enum)本质上是一个整数类型。枚举值实际上是整数常量
如:enum State { GREEN, YELLOW, RED };
在这个枚举中:
GREEN 的值为 0
YELLOW 的值为 1
RED 的值为 2
currentState + 1
:
currentState 是枚举类型 State 的一个变量,currentState + 1 的结果是一个整数类型,表示当前状态的下一个整数值
% NUM_STATES
:
NUM_STATES 通常是枚举中状态的总数(例如 3),因此 (currentState + 1) % NUM_STATES
计算出循环后的状态索引,这个索引是一个整数。
static_cast<State>
:
由于 (currentState + 1) % NUM_STATES
的结果是一个整数(int),而 currentState 是 State 类型,因此需要将这个整数转换回 State 类型。这就是 static_cast<State>
的作用。它显式地将整数转换为对应的枚举类型,以确保类型安全和代码的可读性(整数 转换到枚举类型)
为什么 不直接使用隐式转换?
虽然在许多编译器中,整数到枚举的隐式转换 是允许的,但这种转换 可能会导致潜在的错误。比如,如果转换的整数 超出了枚举定义的范围,隐式转换 不会提供任何警告或错误。因此,使用 static_cast 可以明确地表明程序员的意图,并让编译器在编译时检查类型转换的正确性
如果一个类 含有几个相同类型的成员,则这样一张表 可以帮助我们从这些成员中选择一个。假定 Screen 类含有几个成员函数,每个函数 负责将光标向指定的方向移动:
class Screen {
public:// 其他接口与实现成员与之前一致Screen& home();Screen& forward();Screen& back();Screen& up();Screen& down();
};
这几个新函数 有一个共同点:它们都 不接受任何参数,并且返回值是 发生光标移动的 Screen 的引用
希望定义一个 move 函数,使其可以 调用上面的任意一个函数 并执行对应的操作。为了支持 这个新函数,将 在 Screen 中添加一个静态成员,该成员是 指向光标移动函数的指针的数组:
class Screen {
public:// 其他接口和实现成员与之前一致// Action 是一个指针,可以用任意一个光标移动函数对其赋值using Action = Screen& (Screen::*)();// 指定具体要移动的方向enum Directions { HOME, FORWARD, BACK, UP, DOWN };Screen& move(Directions);
private:static Action Menu[]; // 函数表
}
数组 Menu 依次保存 每个光标移动函数的指针,这些函数 将按照 Directions 中枚举成员对应的偏移量 存储。move 函数 接受一个枚举成员并调用相应的函数:
Screen& Screen::move(Directions cm)
{// 运行 this 对象中索引值为 cm 的元素return (this->*Menu[cm])();
}
move 中的函数调用的原理是: 首先获取索引值为 cm 的 Menu 元素,该元素是指向 Screen 成员函数的指针。根据 this 所指的对象 调用该元素所指的成员函数
在 Screen::move(Directions cm)
方法中,cm 是 Directions 枚举类型的一个变量。由于 cm 是枚举类型并且在声明时,它的底层类型是一个整数类型(通常是 int),所以可以将 cm 直接用作数组的下标
Directions 是一个枚举类型,它定义了一些可能的方向(HOME, FORWARD, BACK, UP, DOWN),它们的值依次为 0, 1, 2, 3, 4
当 cm 是 Directions 类型时,它实际存储的是一个整数值。例如,如果 cm 的值是 FORWARD,那么 cm 的值是 1。因为枚举类型的值可以视作整数,所以它可以直接用作数组的下标
调用 move 函数时,给它传入 一个表示光标移动方向的枚举成员:
Screen myScreen;
myScreen.move(Screen::HOME); // 调用myScreen.home
myScreen.move(Screen::DOWN); // 调用myScreen.down
剩下的工作就是 定义并初始化函数表本身了:
Screen::Action Screen::Menu[] = {&Screen::home,&Screen::forward,&Screen::back,&Screen::up,&Screen::down,
};
总体代码
#include <iostream>class Screen {
public:using Action = Screen& (Screen::*)(); // 函数指针类型定义enum Directions { HOME, FORWARD, BACK, UP, DOWN }; // 光标移动的方向枚举// 构造函数Screen() : cursor(0), height(24), width(80) {}// 移动函数Screen& home() { std::cout << "Move to HOME\n"; return *this; }Screen& forward() { std::cout << "Move FORWARD\n"; return *this; }Screen& back() { std::cout << "Move BACK\n"; return *this; }Screen& up() { std::cout << "Move UP\n"; return *this; }Screen& down() { std::cout << "Move DOWN\n"; return *this; }// 根据方向移动光标Screen& move(Directions);private:pos cursor;pos height, width;// 函数指针数组,用于存储各个移动函数的指针static Action Menu[5];
};// 初始化静态成员 Menu
Screen::Action Screen::Menu[5] = {&Screen::home, // 对应 HOME&Screen::forward, // 对应 FORWARD&Screen::back, // 对应 BACK&Screen::up, // 对应 UP&Screen::down // 对应 DOWN
};// move 方法的实现
Screen& Screen::move(Directions cm) {return (this->*Menu[cm])(); // 根据 cm 调用对应的光标移动函数
}int main() {Screen myScreen;myScreen.move(Screen::HOME); // 输出 "Move to HOME"myScreen.move(Screen::FORWARD); // 输出 "Move FORWARD"myScreen.move(Screen::DOWN); // 输出 "Move DOWN"return 0;
}
4.3 将成员函数用作可调用对象
1、要想通过一个指向成员函数的指针 进行函数调用,必须首先利用.*
运算符 或->*
运算符 将该指针绑定到特定的对象上。因此 与普通的函数指针不同,成员指针 不是一个可调用对象,这样的指针不支持函数调用运算符
因为成员指针 不是可调用对象,所以 不能直接将一个指向成员函数的指针 传递给算法。举个例子,如果想在一个 string 的 vector 中找到第一个空 string,显然不能 使用下面的语句:
auto fp = &string::empty; // fp 指向 string 的 empty 函数
find_if(svec.begin(), svec.end(), fp);
find_if 算法需要一个可调用对象,在 find_if 的内部 将执行以下形式的代码,从而导致无法通过编译:
// 检查对当前元素的断言是否为真
if (fp(*it)) //错误:要通过成员指针调用函数,必须使用->*运算符
该语句试图调用的是传入的对象,而非函数
2、使用 function 生成一个可调用对象
从指向成员函数的指针 获取可调用对象的一种方法是 使用标准库模板 function
function<bool(const string&)> fcn = &string::empty;
find_if(vec.begin(), vec.end(), fcn);
告诉 function 一个事实:即 empty 是一个接受 string 参数并返回 bool 值的函数。通常情况下,执行成员函数的对象 会被传给隐式的 this 形参。当 要使用 function 为成员函数生成 一个可调用对象时,必须首先“翻译”该代码,使得隐式的形参变成显式的
当一个 function 对象 包含了 一个指向成员函数的指针时,function 类知道它 必须使用正确的指向成员的指针来执行函数调用,可以认为在 find_if 当中含有类似于以下形式的代码:
// 假设 it 是 find_if 内部的一个迭代器,则 *it 是给定范围内的一个对象
if (fcn(*it)) //假设 fcn 是 find_if 内部的一个可调用对象的名字
function 将使用正确的指向成员的指针运算符。从本质上来看,function 类 将函数调用转换成了如下形式:
// 假设 it 是 find_if 内部的迭代器,则 *it 是给定范围内的一个对象
if ((*it).*p)() // 假设 p 是 fcn 内部的一个指向成员函数的指针
当定义一个 function 对象时,必须指定 该对象所能表示的函数类型,即可调用对象的形式。如果可调用对象是 一个成员函数,则第一个形参 必须表示该成员是在哪个(一般是隐式的)对象上执行的。同时,提供给 function 的形式中 还必须指明对象是否是以指针或引用的形式传入的
希望在 string 对象的序列上调用 find_if,因此 需要 function 生成一个接受 string 对象的可调用对象。又因为 vector 存储的是 string 的指针,所以必须指定 function 接受指针:
vector<string*> pvec;
function<bool (const string*)> fp = &string::empty;
// fp 接受一个指向 string 的指针,然后使用 ->* 调用 empty
find_if(pvec.begin(), pvec.end(), fp);
3、使用 mem_fn 生成一个可调用对象
要想使用 function,必须提供成员的调用形式。可以 采取另外一种方法,通过使用标准库功能 mem_fn 来让编译器负责 推断成员的类型
和 function 一样,mem_fn 定义在 functional 头文件中,并且可以 从成员指针生成一个可调用对象;和 function 不同的是,mem_fn 可以根据成员指针的类型推断 可调用对象的类型,而无需用户显式地指定:
find_if(svec.begin(), svec.end(), mem_fn(&string::empty));
使用 mem_fn(&string::empty)
生成一个可调用对象,该对象接受一个 string 实参,返回一个 bool 值
mem_fn 生成的可调用对象 可以通过对象调用,也可以 通过指针调用:
auto f = mem_fn(&string::empty); // f接受一个string或者一个string*
f(*svec.begin()); // 正确:传入一个string对象,f使用.*调用empty
f(&svec[0]); // 正确:传入一个string的指针,f使用->*调用empty
可以认为 mem_fn 生成的可调用对象 含有一对重载的函数调用运算符:一个接受 string*,另一个接受 string&
4、使用 bind 生成一个可调用对象
还可以使用 bind 从成员函数生成一个可调用对象:
// 选择范围中的每个string,并将其bind到empty的第一个隐式实参上
auto it = find_if(vec.begin(), vec.end(), bind(&string::empty, _1));
和 function 类似的地方是,当使用 bind 时,必须将函数中 用于表示执行对象的隐式形参转换成显式的。和 mem_fn 类似的地方是,bind 生成的可调用对象的第一个实参 既可以是 string 的指针,也可以是 string 的引用:
auto f = bind(&string::empty, _1);
f(*vec.begin()); //正确:实参是一个string,f使用.*调用empty
f(&vec[0]); //正确:实参是一个string的指针,f使用->*调用empty
5、使用 count_if 统计在给定的 vector 中有多少个空 string
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>int main()
{std::vector<std::string> vs = { "a", "bb", "", "ccc", "", ""};std::function<bool(std::string&)> fcn = &std::string::empty;auto i1 = std::count_if(vs.begin(), vs.end(), fcn);std::cout << "function: " << i1 << std::endl;auto i2 = std::count_if(vs.begin(), vs.end(), std::mem_fn(&std::string::empty));std::cout << "mem_fn: " << i2 << std::endl;auto i3 = std::count_if(vs.begin(), vs.end(), std::bind(&std::string::empty, std::placeholders::_1));std::cout << "bind: " << i3 << std::endl;return 0;
}
5、嵌套类
1、一个类可以在另一个类的内部,前者称为 嵌套类 或 嵌套类型。嵌套类 常用于 定义作为实现部分的类,比如 在文本查询示例中使用的 QueryResult 类
嵌套类是 一个独立的类,与外层类基本没什么关系。特别是,外层类的对象 和 嵌套类的对象 是相互独立的。在外层类的对象中 不包含任何嵌套类定义的成员;类似的,在嵌套类的对象中 也不包含任何外层类定义的成员
外层类 和 嵌套类彼此不包含对方的成员(不包含不代表不能访问)
嵌套类 仅通过外层类的对象来进行 访问其外层类的私有成员(包括私有数据和私有函数)
#include <iostream>class Outer {
private:int outerData = 100;public:void show() {std::cout << "Outer data: " << outerData << std::endl;}class Inner {public:void accessOuter(Outer& out) {std::cout << "Accessing Outer class private member: " << out.outerData << std::endl;out.outerData = 200; // 可以修改外层类的私有成员out.show(); // 也可以调用外层类的公有方法}};
};int main() {Outer outerObj;Outer::Inner innerObj;innerObj.accessOuter(outerObj); // 通过嵌套类访问外层类成员return 0;
}
外层类 不能直接访问嵌套类的成员,即使这些成员是 公有的,外层类要访问嵌套类的成员,必须先创建嵌套类的对象,然后 通过该对象来访问 嵌套类的成员(跟嵌套类 访问外层对象 一致,通过该对象来访问,但是特殊的是 嵌套类可以访问外层的私有对象(因为嵌套类在外层的作用域内),外层 不可以访问嵌套类的私有对象(外层不在嵌套域的作用域内了))
class Outer {
public:class Inner {public:void display() {std::cout << "This is the Inner class." << std::endl;}private:int innerData = 42;};void accessInner() {Inner innerObj; // 创建嵌套类的对象innerObj.display(); // 通过嵌套类对象访问其成员// std::cout << innerObj.innerData << std::endl; // 无法访问,因为innerData是私有成员}
};
嵌套类的名字(不是成员) 在外层类作用域中是可见的,在外层类作用域之外 不可见。和其他嵌套的名字一样,嵌套类的名字 不会和别的作用域中的同一个名字冲突
和其他类类似,嵌套类 也使用访问限定符(::) 来控制外界对其成员的访问权限。外层类对嵌套类的成员 没有特殊访问权限,同样,嵌套类对外层类的成员 也没有特殊的访问权限
嵌套类 其实就是在其外层类中 定义了一个类型成员。和其他成员类似,该类型的访问权限 由外层类决定。位于外层类 public 部分的嵌套类 实际上定义了 一种可以随处访问的类型;位于外层类 protected 部分的嵌套类定义的类型 只能被外层类 及 其友元和派生类访问;位于外层类 private 部分的嵌套类定义的类型 只能被外层类的成员和友元访问
2、声明一个嵌套类
为12.3.2节的 TextQuery 类 定义了 一个名为 QueryResult 的配套类,这两个类密切相关。QueryResult 类的主要作用是 表示 TextQuery 对象上 query 操作的结果,显然将 QueryResult 用作其他目的没有任何意义。为了充分体现这种紧密的相关性,可以把 QueryResult 定义成 TextQuery 的成员
class TextQuery {
public:class QueryResult; //嵌套类稍后定义// 其他成员与12.3.2节一致
};
只需 对原来的 TextQuery 类做一处改动,即把 QueryResult 声明成嵌套类。因为 QueryResult 是一个类型成员,所以 必须先声明再使用(使用一个类型成员时,编译器需要知道 该类型的定义),尤其是必须先声明 QueryResult,再将它作为 query 成员的返回类型。类的其他成员没有任何变化
3、在层类之外 定义一个嵌套类
在 TextQuery 内声明了 QueryResult,但是 没有给出它的定义。和成员函数一样,嵌套类 必须声明在类的内部,但是可以 定义在类的内部或者外部
在外层类之外 定义一个嵌套类时,必须以外层类的名字 限定嵌套类的名字:
// QueryResult是TextQuery的成员,下面的代码负责定义QueryResult
class TextQuery::QueryResult { // 位于类的作用域内,因此不必对QueryResult形参进行限定 friend std::ostream& print(std::ostream&, const QueryResult&);
public:// 无须定义 QueryResult::line_no,直接用 lineno 就行// (TextQuery:using line_no = std::vector<std::string>::size_type;)// 嵌套类可以直接使用外层类的成员,无须对该成员的名字进行限定 QueryResult(std::string, std::shared_ptr<std::set>, std::shared_ptr<std::vector>);
无须在 QueryResult 内定义 line_no 成员了。因为该成员属于 TextQuery,所以 QueryResult 可以直接访问它而不再定义一次
4、定义嵌套类的成员:
要想为其定义构造函数,必须指明 QueryResult 是嵌套在 TextQuery 的作用域之内的。具体做法是 使用外层类的名字限定嵌套类的名字:
// QueryResult 类嵌套在TextQuery 类中
// 下面的代码为 QueryResult 类定义名为 QueryResult 的构造函数成员
TextQuery::QueryResult::QueryResult(string s, shared_ptr p, shared_ptr f): sought(s), lines(p), file(f) {}
5、嵌套类的静态成员定义:
如果 QueryResult 声明了一个静态成员,则该成员的定义将位于 TextQuery 的作用域之外。例如,假设QueryResult 有一个静态成员,则该成员的定义将形如:
// QueryResult 类嵌套在TextQuery 类中,
// 下面的代码为QueryResult 定义一个静态成员
int TextQuery::QueryResult::static_mem = 1024;
在 C++ 中,static 关键字只需要在变量的声明时写一次,而不需要 在变量的定义时再次写出
6、嵌套类作用域中的名字查找
名字查找的一般规则 在嵌套类中同样适用。当然,因为嵌套类本身 是一个嵌套作用域,所以还必须查找嵌套类的外层作用域。这种作用域嵌套的性质 正好可以说明 为什么不在 QueryResult 的嵌套版本中定义 line_no
QueryResult 的嵌套类版本本身 就是定义在 TextQuery 中的,所以 不需要再使用 typedef。嵌套的 QueryResult 无需说明 line_no 属于 TextQuery 就可以直接使用它
嵌套类是 其外层类的一个类型成员,因此外层类的成员 可以像使用任何其他类型成员一样 使用嵌套类的名字。因为 QueryResult 嵌套在 TextQuery 中,所以 TextQuery 的 query 成员可以直接使用名字 QueryResult(不用在 TextQuery 中声明友元了):
// 返回类型必须指明 QueryResult 是一个嵌套类
TextQuery::QueryResult TextQuery::query(const string &sought) const {// 如果我们没有找到 sought,则返回 set 的指针 static shared_ptr<set<line_no>> nodata(new set<line_no>);// 使用 find 而非下标以避免向 wm 中添加单词 // TextQuery:std::map<std::string, std::shared_ptr<std::set<line_no>>> wm;auto loc = wm.find(sought);if (loc == wm.end())return QueryResult(sought, nodata, files); // 没找到elsereturn QueryResult(sought, loc->second, files);
和过去一样,返回类型 不在类的作用域中,因此 必须指明函数的返回值是 TextQuery::QueryResult
类型。不过在函数体内部 可以直接访问 QueryResult ,比如上面的 return 语句就是这样
7、嵌套类和外层类是相互独立的
尽管 嵌套类定义在 其外层类的作用域中,但是 谨记外层类的对象和嵌套类的对象没有任何关系
return QueryResult(sought, loc->second, file);
使用了 TextQuery 对象的数据成员,而 query 正是用它们来初始化 QueryResult 对象的。因为在 一个QueryResult 对象中不包含其外层类的成员,所以 必须使用上述成员构造 我们返回的 QueryResult 对象(即 无法直接对 QueryResult 的元素赋值)
6、union: 一种节省空间的类
1、union是 一种特殊的类。一个 union 可以有多个数据成员,但是在任意时刻 只有一个数据成员可以有值。当我们给 union 的某个成员赋值之后,该 union 的其他成员 就变成未定义的状态了。分配给一个 union 对象的存储空间 至少要能容纳它的最大的数据成员。和其他类一样,一个 union 定义了一种新类型
类的某些特性对 union 同样适用,但并非所有特性都如此。union 不能含有引用类型的成员,除此之外,它的成员 可以是大多数类型。在 C++11 新标准中,含有构造函数 或 析构函数的类类型 也可以作为 union 的成员类型。union 可以为其成员指定 public、protected 和 private 等 保护标记。默认情况下,union 的成员都是公有的,这一点与 struct 相同
union 可以定义 包括构造函数和析构函数在内的成员函数。但是由于 union 既不能继承自其他类,也不能 作为基类使用,所以在 union 中不能含有虚函数
2、定义 union
表示 一组类型不同的互斥值
// Token类型的对象只有一个成员,该成员的类型 可能是下列类型中的任意一种
union Token {
// 默认情况下成员是公有的char cval;int ival;double dval;
};
定义了 一个名为 Token 的 union,它可以保存一个值,这个值的类型可能是 char、int 或 double 的一种
3、使用 union 类型
union 的名字是一个类型名。和其他内置类型一样,默认情况下 union 是未初始化的。可以像 显式地初始化聚合类 一样 使用一对花括号内的初始值显式地初始化一个 union:
Token first_token = {'a'}; // 初始化cval成员
Token last_token; // 未初始化的Token对象
Token *pt = new Token; // 指向一个未初始化的Token对象的指针
如果 提供了初始值,则该初始值被用于 初始化第一个成员。因此,first_token 的初始化过程 实际上是给 cval 成员赋了一个初值
使用通用的成员访问运算符 访问一个 union 对象的成员:
last_token.cval = 'z';
pt->ival = 42;
为 union 的一个数据成员赋值 会令其他数据成员变成未定义的状态。因此,当我们使用 union 时,必须清楚地知道 当前存储在 union 中的值 到底是什么类型
如果 使用错误的数据成员 或者 为错误的数据成员赋值,则程序可能崩溃或出现异常行为
聚合类是 一个特殊类型的类,它具有以下特点:
所有成员都是公有的(没有私有或受保护的非静态数据成员)
没有用户提供的构造函数(包括默认构造函数)
没有基类和虚函数
通过花括号 {} 显式地初始化聚合类的成员。这种初始化方式被称为聚合初始化
#include <iostream>struct Point {int x;int y;
};struct Rectangle {Point topLeft;Point bottomRight;int color;
};int main() {// 使用聚合初始化显式地初始化 Point 对象Point p = {10, 20}; // x = 10, y = 20// 使用聚合初始化显式地初始化 Rectangle 对象Rectangle rect = {{0, 0}, {100, 100}, 255}; // 初始化 topLeft, bottomRight, 和 color// 输出结果std::cout << "Rectangle topLeft: (" << rect.topLeft.x << ", " << rect.topLeft.y << ")\n";std::cout << "Rectangle bottomRight: (" << rect.bottomRight.x << ", " << rect.bottomRight.y << ")\n";std::cout << "Rectangle color: " << rect.color << std::endl;return 0;
}
花括号 {} 的顺序 必须与类成员的声明顺序一致
如果 某些成员未在初始化列表中提供,则它们将 被值初始化(例如,对于基本类型成员,会初始化为 0)
从 C++11 开始,聚合类的初始化方式得到了扩展,可以 使用花括号进行初始化列表,也支持 默认成员初始化器
struct Data {int a = 10;int b = 20;
};Data d1; // a = 10, b = 20
Data d2 = {5}; // a = 5, b = 20 (b 使用默认初始化器)
4、匿名 union
匿名 union 是一个未命名的 union,一旦 定义了一个匿名 union,编译器就自动地为该 union 创建一个未命名的对象:
union { // 匿名unionchar cval;int ival;double dval;
}; // 定义一个未命名的对象,可以直接访问它的成员
cval = 'c'; // 为刚刚定义的匿名 union 对象赋一个新值
ival = 42; // 该对象当前保存的值是 42
在匿名 union 的定义所在的作用域内 该 union 的成员都是可以直接访问的
匿名 union 不能包含 受保护的成员或私有成员,也不能定义 成员函数(不然无法独立使用)
5、含有类类型成员的 union
早期版本规定,在 union 中 不能含有 定义了构造函数 或 拷贝控制成员的类类型成员。C++11 新标准 取消了这一限制。不过,如果 union 的成员类型 定义了自己的构造函数 和/或 拷贝控制成员,则该 union 的用法 要比只含有内置类型成员的 union 复杂得多
当 union 包含的是 内置类型的成员时,可以使用普通的赋值语句 改变 union 保存的值。但是对于 含有特殊类类型成员的 union 就没这么简单了
如果想将 union 的值改为 类类型成员对应的值,或者 将类类型成员的值改为了一个其他值,则必须 分别构造或析构该类类型的成员:当我们将 union 的值改为 类类型成员对应的值时,必须运行该类型的构造函数;反之,当我们 将类类型成员的值改为了一个其他值时,必须运行 该类型的析构函数
当 union 包含的是 内置类型的成员时,编译器 将按照成员的顺序 依次合成默认构造函数或拷贝控制成员。但是,如果 union 含有类类型的成员,并且该类型自定义了 默认构造函数或拷贝控制成员,则编译器将为 union 合成对应的版本 并将其声明为删除的(跟默认构造函数一个路子)
#include <iostream>class MyClass {
public:MyClass() { std::cout << "MyClass default constructor\n"; }MyClass(const MyClass&) { std::cout << "MyClass copy constructor\n"; }~MyClass() { std::cout << "MyClass destructor\n"; }
};union MyUnion {int i;MyClass obj; // 包含自定义默认构造函数和拷贝控制成员的类
};int main() {// MyUnion u1; // 错误:默认构造函数被删除// MyUnion u2(u1); // 错误:拷贝构造函数被删除// u1 = u2; // 错误:拷贝赋值运算符被删除return 0;
}
由于 MyClass 自定义了默认构造函数、拷贝构造函数等,MyUnion 的默认构造函数、拷贝构造函数、拷贝赋值运算符 将被编译器合成并标记为删除的(= delete)
当 union 包含 一个类类型成员,并且 该类自定义了任何拷贝控制成员时,编译器无法为 union 合成有效的默认构造函数或拷贝构造函数,因为 union 的设计 不支持同时管理多个类类型成员的生命周期。因此,编译器会将这些成员函数声明为删除的,以防止在 union 中使用这些功能
例如,string 类定义了 五个拷贝控制成员 以及 一个默认构造函数。如果 union 含有 string 类型的成员,并且没有自定义默认构造函数 或 某个拷贝控制成员,则编译器 将合成缺少的成员 并将其声明成删除的
如果在某个类中有一个 union 成员,而且该 union 含有删除的拷贝控制成员,则 该类与之对应的拷贝控制操作 也将是删除的
处理 union 包含 类类型成员的正确方式
显式构造:通过显式调用构造函数 来初始化 union 的类类型成员
使用定位 new:手动调用 类类型成员的构造函数
手动析构:在使用完类类型成员后,手动调用其析构函数
#include <iostream>
#include <new> // for placement newclass MyClass {
public:MyClass() { std::cout << "MyClass default constructor\n"; }MyClass(const MyClass&) { std::cout << "MyClass copy constructor\n"; }~MyClass() { std::cout << "MyClass destructor\n"; }void display() const { std::cout << "Displaying MyClass\n"; }
};union MyUnion {int i;MyClass obj;MyUnion() { new(&obj) MyClass(); } // 使用 placement new 显式构造类类型成员~MyUnion() { obj.~MyClass(); } // 显式调用析构函数
};int main() {MyUnion u; // 调用显式构造函数u.obj.display();// 手动析构并重新赋值u.obj.~MyClass(); // 显式调用析构函数new(&u.obj) MyClass(); // 使用 placement new 重新构造// 切换到操作int成员iu.obj.~MyClass(); // 显式销毁MyClass对象u.i = 42; // 赋值给int成员std::cout << "u.i: " << u.i << std::endl;// 如果需要再次操作obj,需要重新构造它new(&u.obj) MyClass(); // 使用 定位 new 重新构造MyClass对象return 0;
}
由于 MyUnion 的默认构造函数、拷贝构造函数等 被删除,需要显式地使用定位 new 来在 union 的特定存储位置上构造 MyClass 对象(new(&obj) MyClass();
在 obj 的内存位置上 显式调用 MyClass 的构造函数)
如果 union 已经包含了 一个类类型的活跃成员,在切换到 操作另一个成员之前,应该先显式地析构 当前活跃的类类型成员
平凡类型:例如 int,不需要显式的析构操作。当从一个 int 成员 切换到 一个类类型成员时,不需要做任何特殊处理,因为 int 的析构是隐式的、无操作的。
非平凡类型:例如 MyClass,需要显式地调用析构函数 来销毁当前活跃成员
由于 int 是一个平凡类型,它不需要显式析构。简单地说,i 不需要我们做任何额外操作,它的存储会被 obj 成员的新对象占据(被覆盖)
使用 定位 new 在 u.obj 上构造 MyClass 对象时,它会在 union 的同一块存储区域中创建一个新的 MyClass 对象。这会重用 int 的存储空间
6、使用类管理 union 成员
对于 union 来说,要想 构造或销毁类类型的成员 必须执行非常复杂的操作,因此 通常把含有类型成员的 union 内嵌在另一个类当中。这个类 可以管理并控制与 union 的类类型成员有关的状态转换
举个例子,为 union 添加一个 string 成员,并将 union 定义成匿名 union,最后将其作为 Token 类的一个成员。此时,Token 类将可以管理 union 的成员
为了追踪 union 中到底存储了什么类型的值,通常会定义一个独立的对象,该对象称为 union 的判别式。可以使用判别式辨认 union 存储的值。为了保持 union 与 其判别式同步,将判别式也作为 Token 的成员。我们的类将定义 一个枚举类型的成员来追踪其 union 成员的状态
类中定义的函数 包括默认构造函数、拷贝控制成员 以及 一组赋值运算符,这些赋值运算符 可以将 union 的某种类型的值赋予 union 成员:
(代码多加了一个 Sales_data)
#include <string>
#include "Sales_data.h"
#include <iostream>class Token
{
public:Token() : tok(INT), ival{0} { std::cout << "Token() : tok(INT), ival{0}" << std::endl; }Token(const Token &t) : tok(t.tok) { std::cout << "Token(const Token &t) : tok(t.tok)" << std::endl; copyUnion(t); }Token &operator=(const Token&);Token(Token &&t) noexcept : tok(t.tok){std::cout << "Token(Token &&t) noexcept : tok(t.tok)" << std::endl;switch(t.tok){case INT: ival = t.ival;break;case DBL: dval = t.dval;break;case CHAR: cval = t.cval;break;case STR: sval = t.sval;break;case SD: sdval = t.sdval;break;}}Token &operator=(Token&&) noexcept;~Token() { if(tok == STR) sval.std::string::~string(); }Token &operator=(const std::string&);Token &operator=(char);Token &operator=(int);Token &operator=(double);Token &operator=(const Sales_data &sd);
private:enum{INT, CHAR, DBL, STR, SD} tok;union{char cval;int ival;double dval;std::string sval;Sales_data sdval;};void copyUnion(const Token&);
};Token &Token::operator=(int i)
{std::cout << "Token &Token::operator=(int i)" << std::endl;if(tok == STR) sval.std::string::~string();else if(tok == SD) sdval.Sales_data::~Sales_data();ival = i;tok = INT;return *this;
}Token &Token::operator=(char c)
{std::cout << "Token &Token::operator=(char c)" << std::endl;if(tok == STR) sval.std::string::~string();else if(tok == SD) sdval.Sales_data::~Sales_data();cval = c;tok = CHAR;return *this;
}Token &Token::operator=(double d)
{std::cout << "Token &Token::operator=(double d)" << std::endl;if(tok == STR) sval.std::string::~string();else if(tok == SD) sdval.Sales_data::~Sales_data();dval = d;tok = DBL;return *this;
}Token &Token::operator=(const std::string &s)
{std::cout << "Token &Token::operator=(const std::string &s)" << std::endl;if(tok == SD) sdval.Sales_data::~Sales_data();if(tok == STR) sval = s;else new(&sval) std::string(s);tok = STR;return *this;
}Token &Token::operator=(const Sales_data &sd)
{std::cout << "Token &Token::operator=(const Sales_data &sd)" << std::endl;if(tok == STR) sval.std::string::~string();if(tok == SD) sdval = sd;else new(&sval) Sales_data(sd);tok = SD;return *this;
}void Token::copyUnion(const Token &t)
{switch(t.tok){case INT: ival = t.ival;break;case DBL: dval = t.dval;break;case CHAR: cval = t.cval;break;case STR: new(&sval) std::string(t.sval);break;case SD: new(&sdval) Sales_data(t.sdval);break;}
}Token &Token::operator=(const Token &t)
{std::cout << "Token &Token::operator=(const Token &t)" << std::endl;if(tok == STR && t.tok != STR) sval.std::string::~string();else if(tok == STR && t.tok == STR) sval = t.sval;else if(tok == SD && t.tok != SD) sdval.Sales_data::~Sales_data();else if(tok == SD && t.tok == SD) sdval = t.sdval;else copyUnion(t);tok = t.tok;return *this;
}Token &Token::operator=(Token&& t) noexcept
{std::cout << "Token &Token::operator=(Token&& t) noexcept" << std::endl;tok = t.tok;switch(t.tok){case INT: ival = t.ival;break;case DBL: dval = t.dval;break;case CHAR: cval = t.cval;break;case STR: sval = t.sval;break;case SD: sdval = t.sdval;break;}return *this;
}int main()
{std::cout << "---------1----------" << std::endl;Token s;std::cout << "---------2----------" << std::endl;Sales_data sales_data1("001-01", 1, 100);std::cout << "---------3----------" << std::endl;s = sales_data1;std::cout << "---------4----------" << std::endl;s = std::string("a");std::cout << "---------5----------" << std::endl;s = Token();std::cout << "---------6----------" << std::endl;Token s1(std::move(s));// Token s1(Token(s));std::cout << "---------7----------" << std::endl;return 0;
}
Token类的两个构造函数——移动构造函数 (Token(Token &&t) noexcept)
和拷贝构造函数 (Token(const Token &t))
对于 STR 和 SD 的处理不同
-
移动构造函数
(Token(Token &&t) noexcept)
:
移动构造函数用于将资源从一个临时对象转移到新对象中,而不是复制。这通常是为了提高性能,避免不必要的资源复制
在 case STR 和 case SD 中,没有调用new(&sval) std::string(std::move(t.sval));
或new(&sdval) Sales_data(std::move(t.sdval));
,而是直接赋值。原因是:在移动构造中,通常假设 移动操作后源对象不会再使用(但要保持有效的状态),所以只需要简单地 将成员变量直接赋值给目标对象即可(移动构造假设源对象 在之后不再使用,因此可以简单地转移资源,避免了深拷贝)
例如,sval = t.sval;
直接将字符串指针的内容复制给新对象,而不会产生新的分配(如深拷贝) -
拷贝构造函数
(Token(const Token &t))
:
拷贝构造函数在复制一个对象时使用,必须确保目标对象 完全独立于源对象,因此需要进行深拷贝。
在 case STR 和 case SD 中,使用了 定位 new 来构造新对象。具体来说:new(&sval) std::string(t.sval);
创建了一个新的字符串,这是一种深拷贝,确保目标对象有一个独立的副本
同样,new(&sdval) Sales_data(t.sdval);
为 Sales_data 对象创建了一个新的副本(创建一个新的对象,拷贝构造 需要确保目标对象与源对象独立,因此必须执行深拷贝)
类定义了一个嵌套的、未命名的、不限定作用域的枚举类型,并将其作为 tok 成员的类型
当 union 存储的是一个 int 值时,tok 的值是 INT;当 union 存储的是一个 string 值时,tok 的值是 STR;以此类推
类的默认构造函数 初始化判别式 以及 union 成员,令其保存 int 值 0
union 含有一个定义了析构函数的成员(string),所以必须为 union 也定义一个析构函数 以销毁 string 成员。和普通的类类型成员不一样,作为 union 组成部分的类成员 无法自动销毁。因为析构函数 不清楚 union 存储的值是什么类型,所以它无法确定 应该销毁哪个成员
析构函数 检查被销毁的对象中 是否存储着 string 值。如果有,则类的析构函数 显式地调用 string 的析构函数释放该 string 使用的内存;反之,如果 union 存储的值是 内置类型,则类的析构函数什么也不做
7、管理判别式并销毁 string
类的赋值运算符将负责设置 tok 并为 union 的相应成员赋值。和析构函数一样,这些运算符 在为 union 赋新值前必须首先销毁 string:
Token &Token::operator=(int i)
{if (tok == STR) sval.~string(); // 如果当前存储的是string,释放它ival = i; // 为成员赋值tok = INT; // 更新判别式return *this;
}
如果 union 的当前值是 string,必须先调用 string 的析构函数 销毁这个 string,然后再为 union 赋新值。清除了 string 成员之后,将给定的值 赋给 与运算符形参类型相匹配的成员
string 版本 与其他几个有所区别,是因为 string 版本必须管理与 string 类型有关的转换
Token &Token::operator=(const std::string &s)
{if (tok == STR) // 如果当前存储的是string,可以直接赋值sval = s; elsenew(&sval) string(s); // 否则需要先构造一个stringtok = STR; // 更新判别式return *this;
}
先利用定位 new 表达式 在内存中为 sval 构造一个 string
8、管理需要拷贝控制的联合成员
和依赖于类型的赋值运算符一样,拷贝构造函数和赋值运算符 也需要先检验判别式 以明确拷贝所采用的方式。定义一个名为 copyUnion 的成员
在拷贝构造函数中 调用 copyUnion 时,union 成员 将被默认初始化,这意味着 编译器会初始化 union 的第一个成员。因为 string 不是第一个成员,所以显然 union 成员保存的不是 string。在赋值运算符中 情况有些不一样,有可能 union 已经存储了一个 string。将在赋值运算符中直接处理这种情况。copyUnion 假设如果它的形参存储了 string,则它一定会构造自己的 string:
void Token::copyUnion(const Token &t)
{switch (t.tok) {case Token::INT: ival = t.ival; break;case Token::CHAR: cval = t.cval; break;case Token::DBL: dval = t.dval; break;//要想拷贝一个 string 可以使用定位new表达式构造它case Token::STR: new (&sval) string(t.sval); break;}
}
对于内置类型来说,把值 直接赋给对应的成员;如果拷贝的是一个 string,则需要构造它
9、赋值运算符 必须处理 string 成员的三种可能情况:左侧运算对象和右侧运算对象 都是 string、两个运算对象都不是 string、只有一个运算对象是 string:
Token &Token::operator=(const Token &t)
{// 如果此对象的值是 string 而 t 的值不是,则我们必须释放原来的 stringif (tok == STR && t.tok != STR) sval.~string();if (tok == STR && t.tok == STR)sval = t.sval; // 无须构造一个新 stringelsecopyUnion(t); // 如果 t.tok 是 STR,则需要构造一个 string(之前把不需要构造的情况都剔除了)// 如果两端都不是 string,则执行普通的赋值操作就行了tok = t.tok;return *this;
}
19.24 将一个 Token 对象赋值给它自己时,如果不进行自赋值检查,可能会导致未定义行为或资源泄漏。特别是对于包含动态资源的情况(如 std::string 或 Sales_data)
如果不检查自赋值情况,执行类似 t = t; 的操作时,程序可能会先释放对象的资源,然后试图从自己已释放的资源中进行拷贝,这会导致未定义行为或程序崩溃
7、局部类
1、类可以定义 在某个函数的内部,我们称这样的类为 局部类。局部类定义的类型 只在定义它的作用域内可见。和嵌套类不同,局部类的成员 受到严格限制
局部类的所有成员(包括函数在内)都必须 完整定义在类的内部。因此,局部类的作用 与嵌套类相比相差很远
因为局部类的成员 必须完整定义在类的内部,所以成员函数的复杂性不可能太高。局部类的成员函数一般只有几行代码
在局部类中 也不允许声明静态数据成员,因为 没法定义这样的成员
2、局部类 不能使用函数作用域中的变量
局部类 只能访问外层作用域定义的类型名、静态变量(包括全局变量) 以及 枚举成员。如果局部类 定义在某个函数内部,则该函数的普通局部变量 不能被该局部类使用
#include <iostream>void exampleFunction() {// 外层作用域中的类型名、静态变量和枚举成员struct OuterStruct {int value;};static int staticVar = 42;enum OuterEnum { OPTION1, OPTION2, OPTION3 };// 局部类定义在函数内class LocalClass {public:void display() {OuterStruct os;os.value = 10;std::cout << "OuterStruct value: " << os.value << std::endl;std::cout << "Static variable: " << staticVar << std::endl;std::cout << "OuterEnum OPTION2: " << OPTION2 << std::endl;}};LocalClass localObj;localObj.display();
}int main() {exampleFunction();return 0;
}
书中例子:
int a, val;
void foo(int val)
{static int si;enum Loc { a = 1024, b };// Bar是foo的局部类struct Bar {Loc locVal; // 正确:使用一个局部类型名int barVal; void fooBar(Loc l = a) // 正确:默认实参是Loc::a{barVal = val; // 错误:val是foo的局部变量 barVal = ::val; // 正确:使用一个全局对象barVal = si; // 正确:使用一个静态局部对象locVal = b; // 正确:使用一个枚举成员}};//...
}
3、常规的访问保护规则 对局部类同样适用
外层函数 对局部类的私有成员 没有任何访问特权。局部类可以 将外层函数声明为友元;或者更常见的情况是 局部类将其成员声明成公有的。在程序中 有权访问局部类的代码非常有限。局部类 已经封装在函数作用域中,通过信息隐藏进一步封装 就显得没什么必要了
外层函数 不能直接访问局部类的成员,因为局部类的作用域是 局限于定义它的那个函数或代码块内。然而,可以通过创建局部类的实例 并调用其公共成员函数或变量 来间接访问其成员(跟嵌套类一致)
#include <iostream>void exampleFunction() {// 定义局部类class LocalClass {public:void setValue(int v) { value = v; }int getValue() const { return value; }private:int value;};// 创建局部类的实例LocalClass localObj;// 通过外层函数访问局部类的成员localObj.setValue(42);std::cout << "Value from LocalClass: " << localObj.getValue() << std::endl;
}int main() {exampleFunction();return 0;
}
4、局部类中的名字查找
局部类内的名字查找次序 与其他类相似。在声明类的成员时,必须 先确保用到的名字位于作用域中,然后才能使用这个名字。定义成员时 用到的名字可以在类的任意位置出现
如果某个名字 不是局部类的成员,则继续在外层函数作用域中查找:如果没有找到,则在外层函数所在的作用域中查找
5、嵌套的局部类
可以在局部类的内部 再嵌套一个类。此时,嵌套类的定义 出现在局部类之外。不过,嵌套类必须定义在与局部类相同的作用域中
void foo()class Bar {public://...class Nested; // 声明Nested类};// 定义Nested类class Bar::Nested {//...};
}
和往常一样,当 在类的外部定义成员时,必须指明 该成员所属的作用域。因此在上面的例子中,Bar::Nested 的意思是 Nested 是定义在 Bar 的作用域内的一个类
局部类内的嵌套类 也是一个局部类,必须遵循 局部类的各种规定。嵌套类的所有成员 都必须定义在嵌套类内部
8、固有的不可移植的特性
1、为了支持底层编程,C++ 定义了 一些固有的不可移植的特性。所谓不可移植的特性是指 因机器而异的特性,当 将含有不可移植特性的程序 从一台机器转移到另一台机器上时,通常需要 重新编写该程序。算术类型的大小在不同机器上不一样
将介绍 C++ 从 C 语言继承来的两种不可移植的特性:位域 和 volatile 限定符。此外,还将介绍 链接指示,它是 C++ 新增的一种不可移植的特性
8.1 位域
1、类可以将其(非静态)数据成员定义成 位域,在一个位域中 含有一定数量的二进制位。当一个程序需要向其他程序或硬件设备 传输二进制数据时,通常会用到位域
位域在内存中的布局是 与机器相关的
位域的类型 必须是整型或枚举类型。因为带符号位域的行为是 由具体实现确定的,所以在通常情况下 使用无符号类型保存一个位域(例如,符号位的处理、符号扩展、溢出行为等,因此其行为可能不是跨平台一致的)。位域的声明形式 是在成员名字之后 紧跟一个冒号以及一个常量表达式,该表达式用于 指定成员所占的二进制位数:
// Bit mode: 2;:mode 使用了 2 位,可以存储 0 到 3 之间的值。
// Bit modified: 1;:modified 使用了 1 位,可以存储布尔值 0 或 1。
// Bit prot_owner: 3;、prot_group: 3;、prot_world: 3;:这些字段各使用了 3 位,可以存储 0 到 7 之间的值
typedef unsigned int Bit;
class File {Bit mode: 2; // mode 占 2 位Bit modified: 1; // modified 占 1 位Bit prot_owner: 3; // prot_owner 占 3 位Bit prot_group: 3; // prot_group 占 3 位Bit prot_world: 3; // prot_world 占 3 位// File 的操作和数据成员,允许多个布尔值或小整数共享同一个 unsigned int
public:// 文件类型以八进制的形式表示// READ = 01 (八进制) = 0001 (二进制)// WRITE = 02 (八进制) = 0010 (二进制)// EXECUTE = 03 (八进制) = 0011 (二进制)enum modes { READ = 01, WRITE = 02, EXECUTE = 03 };File &open(modes);void close();void write();bool isRead() const;void setWrite();
};
mode 位域占 2 个二进制位,modified 只占 1 个,其他成员则各占 3 个。如果可能的话,在类的内部连续定义的位域 压缩在同一整数的相邻位,从而提供存储压缩(如果在类中连续定义的位域 具有相同的基础类型(如 unsigned int),编译器 通常会将它们压缩在同一个整数的相邻位中)。例如在前面的声明中,五个位域 可能会 存储在同一个 unsigned int 中
取地址运算符(&)不能作用于位域,因此任何指针都无法指向类的位域
2、使用位域
访问位域的方式 与 访问类的其他数据成员的方式 非常相似:
// write():该函数设置 modified 位为 1,表示文件已被修改
// close():在关闭文件时检查 modified 位,如果它为 1,则表示文件内容已被修改,需要保存内容
void File::write()
{modified = 1;// ...
}
void File::close()
{if (modified)// …… 保存内容
}
通常使用内置的位运算符 操作超过 1 位的位域:
File &File::open(File::modes m)
{mode |= READ; // 按默认方式设置READ// READ 是一个枚举值,八进制值为 01。mode |= READ 的意思是确保 mode 位域的最低位被设置为 1,即以读取模式打开文件// 其他处理if (m & WRITE) // 如果打开了READ和WRITE,两位都为 1,则结果为 1// m & WRITE 表达式使用按位与运算符 & 来检查 m 是否包含 WRITE 标志// 如果 m 包含 WRITE 标志(即 m 的二进制表示中 WRITE 对应的位为 1),则条件为真// 按照读/写方式打开文件return *this;
}
执行按位与运算 if (m & WRITE)
:
0011 (m)
&
0010 (WRITE)
----
0010 (结果) 就不为0就可以进入 if
如果 一个类定义了位域成员,则它通常也会 定义一组内联的成员函数 以检验或设置位域的值:
inline bool File::isRead() const { return mode & READ; }
// Bit mode: 2; mode 占 2 位,对应 00,01,11
inline void File::setWrite() { mode |= WRITE; }
8.2 volatile限定符
1、volatile的确切含义 与机器有关,要想让使用了volatile的程序 在移植到新机器或新编译器后仍然有效,通常需要对该程序进行某些改变
直接处理硬件的程序 常包含这样的数据元素,它们的值 由程序直接控制之外的过程控制。例如,程序可能包含 一个由系统时钟定时更新的变量。当对象的值可能在程序的控制或检测之外 被改变时,应该将该对象声明为 volatile。关键字 volatile 告诉编译器不应对此类对象进行优化
volatile 限定符的用法 和 const 很相似,它起到 对类型额外修饰的作用:
volatile int display_register; // 该int值可能发生改变
volatile Task* curr_task; // curr_task指向一个volatile对象
volatile int iax[max_size]; // iax的每个元素都是volatile
volatile Screen bitmapBuf; // bitmapBuf的每个成员都是volatile
const 和 volatile 没什么影响,某种类型的对象 可以既是 const 的也是 volatile 的
可以将 成员函数定义成 volatile 的。只有 volatile 的成员函数才能被 volatile 的对象调用
volatile 成员函数的主要用途是 处理可能在程序外部改变的对象,例如 通过硬件寄存器或多线程中的共享变量
class Sensor {
public:int read() const volatile {return data; // 假设 data 是从硬件寄存器读取的}private:volatile int data; // 这个成员可能被硬件改变
};int main() {volatile Sensor sensor;int value = sensor.read(); // 调用的是 volatile 成员函数
}
可以将 成员函数定义成 const 的。只有 const 的成员函数才能被 const 的对象调用
在成员函数的声明和定义中 加上 const 关键字时,你承诺 该函数不会改变对象的状态。这种设计用于 保证某些操作是只读的,并且 可以在函数中对成员变量进行读取 但不能修改
class Data {
public:int get() const {return value;}void set(int v) {value = v;}private:int value;
};int main() {const Data data1;Data data2;// data1 是 const 对象,只能调用 const 成员函数int val = data1.get(); // OK: get() 是 const 成员函数// data1.set(10); // 错误:set() 不是 const 成员函数// data2 不是 const 对象,可以调用任意成员函数data2.set(10); // OKint val2 = data2.get(); // OK
}
可以声明 volatile 指针、指向 volatile 对象的指针 以及指向 volatile 对象的 volatile 指针:
volatile int v; //v是一个volatile int
int *volatile vip; //vip是一个volatile指针,它指向int
volatile int *ivp; //ivp是一个指针,它指向一个volatile int
volatile int *volatile vivp; //vivp是一个volatile指针,它指向一个volatile intvip = &v; //错误:必须使用指向volatile的指针
ivp = &v; //正确:ivp是一个指向volatile的指针
vivp = &v; //正确:vivp是一个指向volatile的volatile指针
跟 const 不同,C++ 的类型系统不允许将非 volatile 对象的引用绑定到 volatile 引用上
int value = 10; // 非 volatile 对象
volatile int volatileValue; // volatile 对象volatile int& ref = volatileValue; // 定义 volatile 引用ref = value; // 错误:不能将非 volatile 对象赋值给 volatile 引用
2、合成的拷贝对 volatile 对象无效
const 和 volatile 的一个重要区别是 不能使用合成的拷贝 / 移动构造函数 及 赋值运算符初始化 volatile 对象 或 从 volatile 对象赋值。合成的成员接受的形参类型是(非volatile)常量引用,显然 不能把一个非 volatile 引用绑定到一个 volatile 对象上
合成的拷贝 / 移动构造函数和赋值运算符的形参类型是 const T& 或 T&&,而不是 volatile 或 const volatile
const 对象 不能使用 合成的移动构造函数和赋值运算符,只能使用 拷贝构造函数。这是因为移动构造函数和移动赋值运算符的本质是 在移动过程中“窃取”资源,这需要 对源对象进行修改。而 const 对象是不可修改的
如果 一个类希望拷贝、移动或赋值它的 volatile 对象,则该类 必须自定义拷贝或移动操作。例如,可以将形参类型指定为 const volatile 引用,这样 就能利用任意类型的 Foo 进行拷贝或赋值操作了:
class Foo {
public:Foo(const volatile Foo&); // 从一个 volatile 对象进行拷贝// 将一个 volatile 对象赋值给一个非 volatile 对象(this 对象是非 volatile 的)Foo& operator=(volatile const Foo&);// 将一个 volatile 对象赋值给一个 volatile 对象Foo& operator=(volatile const Foo&) volatile;// Foo 类的剩余部分
};
8.3 链接指示:extern “C”
1、C++ 程序 有时需要调用 其他语言编写的函数,最常见的是调用 C 语言编写的函数。像所有其他名字一样,其他语言中的函数名字 也必须在 C++ 中进行声明,并且 该声明必须指定返回类型和形参列表。C++ 使用链接指示 指出任意非 C++ 函数所用的语言
要想把 C++ 代码和其他语言(包括C语言)编写的代码 放在一起使用,要求 必须有权访问该语言的编译器,并且这个编译器与当前的 C++ 编译器是兼容的
2、声明一个非 C++ 的函数
链接指示可以有两种形式:单个的 或 复合的。链接指示 不能出现在类定义或函数定义的内部。同样的链接指示 必须在函数的每个声明中都出现
接下来的声明 显示了 cstring 头文件的某些 C 函数是如何声明的:
// 可能出现在 C++头文件 <cstring> 中的链接指示
// 单语句链接指示
extern "C" size_t strlen(const char *);
// 复合语句链接指示
extern "C" {int strcmp(const char *, const char *);char *strcat(char *, const char *);
}
其中的字符串字面值常量 指出了 编写函数所用的语言。编译器应该支持对 C 语言的链接指示。此外,编译器也可能 会支持其他语言的链接指示,如 extern “Ada”、extern “FORTRAN” 等
3、链接指示与头文件
可以令链接指示 后面跟上花括号括起来的若干函数的声明,从而 一次性建立多个链接。花括号的作用是 将适用于该链接指示的多个声明 聚合在一起,否则 花括号就会被忽略,花括号中声明的函数名字就是可见的,就好像是在花括号之外声明的一样
多重声明的形式 可以应用于 整个头文件。例如,C++ 的 cstring 头文件可能形如:
// 复合语句链接指示
extern "C" {
#include <string.h> // 操作C风格字符串的C函数
}
当一个 #include 指令 被放置在复合链接指示的花括号中时,头文件中的所有普通函数声明 都被认为是由链接指示的语言编写的。链接指示 可以嵌套,因此 如果头文件包含 带自带链接指示的函数,则该函数的链接不受影响
C++ 从C语言继承的标准库函数 可以定义成C函数,但并非必须:决定使用C还是C++ 实现C标准库,是每个C++实现的事情
在 C++ 中,许多标准库函数实际上是从 C 语言中继承而来的。这些函数通常都在 <cstdio>、<cstdlib>、<cmath>、<cstring>
等头文件中定义。这些头文件是 C++ 版本的 C 标准库头文件,目的是提供与 C 语言兼容的接口
C 语言的实现: 一些编译器和标准库实现 可能直接使用 C 语言中的函数来实现 C++ 标准库中的对应函数。这种方式可以保证与 C 语言库的兼容性
C++ 的实现: 其他实现可能 使用 C++ 编写的函数来代替 C 版本,以便利用 C++ 特性,例如模板、异常处理和命名空间管理等。尽管如此,这些函数 在接口上仍然与 C 语言中的函数保持一致
4、指向 extern “C” 函数的指针
编写函数所用的语言是 函数类型的一部分。因此,对于使用链接指示 定义的函数来说,它的每个声明 都必须使用相同的链接指示。而且,指向其他语言编写的函数的指针 必须与函数本身使用的链接指示相同:
// pf 指向一个 C 函数,该函数接受一个 int 返回 void
extern "C" void (*pf)(int);
使用 pf 调用函数时,编译器认定当前调用的是一个 C 函数
指向 C 函数的指针与指向 C++ 函数的指针是不一样的类型。一个指向 C 函数的指针 不能在 执行初始化或赋值操作后 指向 C++ 函数,反之亦然。就像其他类型不匹配的问题一样,如果 试图在两个链接指示不同的指针之间 进行赋值操作,则程序将会发生错误:
void (*pf1)(int); // 指向一个 C++ 函数
extern "C" void (*pf2)(int); // 指向一个 C 函数
pf1 = pf2;// 错误:pf1 和 pf2 的类型不同
5、链接指示对整个声明都有效
使用链接指示时,它不仅对函数有效,而且 对作为返回类型 或 形参类型的函数指针 也有效:
// f1 是一个 C 函数,它的形参是一个指向 C 函数的指针
extern "C" void f1(void (*)(int));
这个链接指示 不仅对 f1 有效,对函数指针同样有效。当 调用 f1 时,必须传给它一个 C 函数的名字 或者 指向 C 函数的指针
因为链接指示 同时作用于声明语句中的所有函数,所以 如果 希望给 C++ 函数传递一个指向 C 函数的指针,则必须使用类型别名
// FC 是一个指向 C 函数的指针
extern "C" typedef void FC(int);
// f2 是一个 C++ 函数,该函数的形参是指向 C 函数的指针
void f2(FC*);
6、导出 C++ 函数到其他语言
使用 链接指示 对函数进行定义,可以让一个 C++ 函数在其他语言编写的程序中可用:
// calc 函数可以被 C 程序调用
extern "C" double calc(double dparm) { /* ... */ }
可被 多种语言共享的函数的返回类型 或形参类型 受到很多限制。例如,不太可能把一个 C++ 类的对象传给 C 程序,因为 C 程序 根本无法理解构造函数、析构函数 以及 其他类特有的操作
对链接到 C 的预处理器的支持
有时需要在 C 和 C++ 中编译同一个源文件,为了实现这一目的,在编译 C++ 版本的程序时预处理器定义 __cplusplus(两个下画线)。利用这个变量,可以在编译 C++ 程序的时候 有条件地包括进来一些代码:
#ifdef _cplusplus
// 正确:我们正在编译 C++ 程序
extern "C"
#endif
int strcmp(const char*, const char*);
目的是确保在 C++ 程序中调用 strcmp 函数时,使用 C 的链接规范。这是为了避免 C++ 的名字改编,使得 C++ 代码可以与 C 库无缝链接
7、重载函数 与 链接指示
链接指示与重载函数的相互作用 依赖于 目标语言。如果目标语言支持重载函数,则为该语言实现链接指示的编译器很可能也支持重载这些 C++ 的函数
C 语言不支持函数重载,一个 C 链接指示 只能用于说明 一组重载函数中的某一个了
// 错误:两个 extern "C" 函数的名字相同
extern "C" void print(const char*);
extern "C" void print(int);
在一组重载函数中 有一个是 C 函数,则其余的必定都是 C++ 函数:
class SmallInt { /* ...*/ };
class BigNum { /* ...*/ };
// C 函数可以在 C 或 C++ 程序中调用
// C++ 函数重载了该函数,可以在 C++ 程序中调用
extern "C" double calc(double);
extern SmallInt calc(const SmallInt&);
// 通过 extern,可以声明 一个在其他文件中定义的函数或变量,使其在当前文件中可用
extern Bignum calc(const BigNum&);
C 版本的 calc 函数可以在 C 或 C++ 程序中调用,而使用了类类型形参的 C++ 函数只能在 C++ 程序中调用。上述性质与声明的顺序无关
小结
有的程序需要 精确控制内存分配过程,它们可以通过在类的内部 或 在全局作用域中自定义 operator new 和 operator delete 来实现这一目的。将优先使用 应用程序定义的版本
有的程序需要 在运行时 直接获取对象的动态类型,运行时类型识别 (RTTI) 为这种程序提供了语言级别的支持。RTTI 只对定义了 虚函数的类有效:对没有定义虚函数的类,虽然也可以得到其类型信息,但只是 静态类型
指向类成员的指针时,在指针类型中包含了 该指针所指成员所属类的类型信息。成员指针可以绑定到 该类当中任意一个具有指定类型的成员上。当 解引用成员指针时,必须提供 获取成员所需的对象
C++ 定义了另外几种聚集类型:
嵌套类通常作为外层类的实现类
在任何时候 只有一个成员有值,union 通常 嵌套在其他类的内部
局部类的所有成员 都必须定义在类内
位域和 volatile 使得程序更容易访问硬件;链接指示 使得程序更容易访问用其他语言编写的代码
术语表
匿名 union 的成员也是外层作用域的成员:意味着匿名 union 中的成员 可以直接访问,就像 是外层作用域的成员一样
struct Example {union { // 匿名 unionint intValue;float floatValue;}; // 没有名称void setInt(int i) { intValue = i; }void setFloat(float f) { floatValue = f; }void printValues() {std::cout << "intValue: " << intValue << std::endl;std::cout << "floatValue: " << floatValue << std::endl;}
};
在 Example 结构体中,定义了一个匿名的 union。这个 union 没有名称,因此它的成员 intValue 和 floatValue 直接提升为 Example 结构体的成员
可以直接通过 ex.intValue 或 ex.floatValue 访问 union 的成员(Example ex;
),而无需通过 union 的名称
free 只能释放由 malloc 分配的内存
这篇关于C++ Primer 总结索引 | 第十九章:特殊工具与技术的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!