前言
在Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》,结合我自己的工作学习经历,我准备写一个音视频系列blog。C/C++是音视频必备编程语言,我准备用几篇文章来快速回顾C++。本文是音视频系列blog的其中一个, 对应的要学习的内容是:快速回顾C++的关联容器,动态内存,拷贝控制,重载运算与类型转换。
音视频系列blog
音视频系列blog: 点击此处跳转查看
目录
1 关联容器
1.1 关联容器定义与使用
C++标准库提供了关联容器,用于存储键-值对,并支持通过键来快速查找和访问值。关联容器是基于树结构实现的,因此在大部分操作中具有较好的性能,例如查找、插入和删除。常见的关联容器有 std::map
、std::multimap
、std::set
和 std::multiset
。
以下是关联容器的定义和使用示例:
std::map:
std::map
是一个关联容器,存储一组键-值对,并根据键的排序顺序进行存储和访问。
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> studentMap;
studentMap[101] = "Alice";
studentMap[102] = "Bob";
studentMap[103] = "Charlie";
std::cout << studentMap[102] << std::endl; // 输出 "Bob"
return 0;
}
std::multimap:
std::multimap
类似于 std::map
,但允许多个具有相同键的元素存在。
#include <iostream>
#include <map>
int main() {
std::multimap<int, std::string> studentMultiMap;
studentMultiMap.insert(std::make_pair(101, "Alice"));
studentMultiMap.insert(std::make_pair(102, "Bob"));
studentMultiMap.insert(std::make_pair(101, "Charlie"));
for (const auto& entry : studentMultiMap) {
std::cout << entry.first << ": " << entry.second << std::endl;
}
return 0;
}
std::set:
std::set
是一个关联容器,存储一组不重复的值,并根据排序顺序进行存储和访问。
#include <iostream>
#include <set>
int main() {
std::set<int> mySet;
mySet.insert(10);
mySet.insert(5);
mySet.insert(20);
for (const auto& value : mySet) {
std::cout << value << " ";
}
return 0;
}
std::multiset:
std::multiset
类似于 std::set
,但允许多个相同值的元素存在。
#include <iostream>
#include <set>
int main() {
std::multiset<int> myMultiSet;
myMultiSet.insert(10);
myMultiSet.insert(5);
myMultiSet.insert(10);
for (const auto& value : myMultiSet) {
std::cout << value << " ";
}
return 0;
}
关联容器提供了高效的查找和访问功能,适用于需要快速查找元素的场景。根据需求,选择适合的关联容器进行使用。
1.2 关联容器操作
下面是关联容器(如 std::map
和 std::set
)的常见操作示例,包括关联容器迭代器、添加元素、删除元素、map 的下标操作以及访问元素。
关联容器迭代器:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> studentMap;
studentMap[101] = "Alice";
studentMap[102] = "Bob";
studentMap[103] = "Charlie";
// 使用迭代器遍历关联容器
for (const auto& entry : studentMap) {
std::cout << entry.first << ": " << entry.second << std::endl;
}
return 0;
}
添加元素:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> studentMap;
studentMap.insert(std::make_pair(101, "Alice"));
studentMap.emplace(102, "Bob");
return 0;
}
删除元素:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> studentMap;
studentMap[101] = "Alice";
studentMap[102] = "Bob";
studentMap[103] = "Charlie";
// 删除指定键的元素
studentMap.erase(102);
return 0;
}
map 的下标操作:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> studentMap;
studentMap[101] = "Alice";
studentMap[102] = "Bob";
studentMap[103] = "Charlie";
// 使用下标操作访问和添加元素
studentMap[104] = "David";
return 0;
}
访问元素:
#include <iostream>
#include <map>
int main() {
std::map<int, std::string> studentMap;
studentMap[101] = "Alice";
studentMap[102] = "Bob";
studentMap[103] = "Charlie";
// 使用迭代器访问元素
std::map<int, std::string>::iterator it = studentMap.find(102);
if (it != studentMap.end()) {
std::cout << "Student ID: " << it->first << ", Name: " << it->second << std::endl;
}
return 0;
}
这些操作展示了如何在关联容器中进行迭代、添加、删除元素,以及通过下标操作和迭代器访问元素。关联容器提供了便捷的方法来管理和操作键-值对。
1.3 无序容器
C++标准库提供了无序容器(unordered containers),也称为哈希容器,用于存储键-值对,并根据键的哈希值进行存储和访问。无序容器在大部分操作中具有较好的性能,例如查找、插入和删除。常见的无序容器有 std::unordered_map
和 std::unordered_set
。
以下是无序容器的定义和使用示例:
std::unordered_map:
std::unordered_map
是一个无序关联容器,存储一组键-值对,并根据键的哈希值进行存储和访问。
#include <iostream>
#include <unordered_map>
int main() {
std::unordered_map<int, std::string> studentMap;
studentMap[101] = "Alice";
studentMap[102] = "Bob";
studentMap[103] = "Charlie";
std::cout << studentMap[102] << std::endl; // 输出 "Bob"
return 0;
}
std::unordered_set:
std::unordered_set
是一个无序关联容器,存储一组不重复的值,并根据哈希值进行存储和访问。
#include <iostream>
#include <unordered_set>
int main() {
std::unordered_set<int> mySet;
mySet.insert(10);
mySet.insert(5);
mySet.insert(20);
for (const auto& value : mySet) {
std::cout << value << " ";
}
return 0;
}
无序容器在大多数情况下具有较好的性能,但它们的顺序不保证与插入顺序一致。
2 动态内存
2.1 动态内存与智能指针
2.1.1 shared_ptr 类
C++中的动态内存管理允许你在程序运行时分配和释放内存。智能指针是一种方便的工具,用于管理动态分配的内存,特别是在避免内存泄漏和手动释放内存方面非常有用。std::shared_ptr
是C++标准库中的一个智能指针类,用于共享拥有动态分配的对象。多个 shared_ptr
可以共享一个对象,当最后一个 shared_ptr
指向对象时,对象会自动释放。
以下是 std::shared_ptr
的用法示例:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass(int value) : data(value) {
std::cout << "Constructing MyClass with value: " << data << std::endl;
}
~MyClass() {
std::cout << "Destructing MyClass with value: " << data << std::endl;
}
void Print() {
std::cout << "Value: " << data << std::endl;
}
private:
int data;
};
int main() {
// 创建 shared_ptr,分配内存
std::shared_ptr<MyClass> shared1 = std::make_shared<MyClass>(42);
// 复制 shared_ptr,共享内存
std::shared_ptr<MyClass> shared2 = shared1;
// 使用 shared_ptr 调用对象的成员函数
shared1->Print();
shared2->Print();
// shared_ptr 会在不再使用时自动释放内存
return 0;
}
在上面的示例中,shared_ptr
会自动管理内存的分配和释放。std::make_shared
函数用于创建一个动态分配的对象,并返回一个 std::shared_ptr
,从而避免了手动调用 new
和 delete
。当不再有 shared_ptr
指向对象时,对象会自动被销毁,释放其占用的内存。
使用 std::shared_ptr
可以大大减少内存泄漏的风险,同时简化了动态内存管理的工作。
2.1.2 直接管理内存
在C++中,可以使用动态内存分配操作符 new
和 delete
来直接管理内存,但这需要手动跟踪内存的分配和释放,容易导致内存泄漏或者访问已释放内存的问题。智能指针是一种更安全和方便的方式来管理动态内存,因为它们自动处理内存的分配和释放。
以下是使用 new
和 delete
进行直接内存管理的示例:
#include <iostream>
class MyClass {
public:
MyClass(int value) : data(value) {
std::cout << "Constructing MyClass with value: " << data << std::endl;
}
~MyClass() {
std::cout << "Destructing MyClass with value: " << data << std::endl;
}
void Print() {
std::cout << "Value: " << data << std::endl;
}
private:
int data;
};
int main() {
// 分配内存
MyClass* ptr = new MyClass(42);
// 使用指针调用对象的成员函数
ptr->Print();
// 释放内存
delete ptr;
return 0;
}
在上面的示例中,使用了 new
来分配一个 MyClass
对象的内存,然后使用指针来访问对象的成员函数。最后,使用 delete
来释放分配的内存。然而,直接使用 new
和 delete
存在一些潜在问题:
- 内存泄漏:如果忘记调用
delete
来释放分配的内存,就会导致内存泄漏,占用的内存无法被回收。 - 二次释放:如果多次调用
delete
来释放同一块内存,会导致未定义的行为,可能会访问已释放的内存。 - 异常安全性:在使用
new
和delete
进行内存管理时,需要处理异常情况,确保在发生异常时也能正确释放内存。
相比之下,智能指针如 std::shared_ptr
和 std::unique_ptr
能够更安全地管理内存,避免了上述问题。它们会自动在不再需要时释放内存,减少了手动管理内存的烦恼。
2.1.3 shared_ptr 和 new 结合使用
在C++中,可以将 std::shared_ptr
和 new
结合使用,以便在动态内存中创建对象并使用智能指针来管理其生命周期。这样可以兼顾动态内存的灵活性和智能指针的便利性,同时避免了手动释放内存的繁琐工作。
以下是一个示例,演示如何使用 std::shared_ptr
和 new
结合创建对象并进行管理:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass(int value) : data(value) {
std::cout << "Constructing MyClass with value: " << data << std::endl;
}
~MyClass() {
std::cout << "Destructing MyClass with value: " << data << std::endl;
}
void Print() {
std::cout << "Value: " << data << std::endl;
}
private:
int data;
};
int main() {
// 使用 std::make_shared 创建 shared_ptr,分配内存
std::shared_ptr<MyClass> shared = std::make_shared<MyClass>(42);
// 使用 shared_ptr 调用对象的成员函数
shared->Print();
// shared_ptr 会在不再使用时自动释放内存
return 0;
}
在上述示例中,std::make_shared
函数创建了一个 std::shared_ptr
,并通过 new
分配了一个 MyClass
对象的内存。std::shared_ptr
将负责管理这块内存,确保在不再需要时自动释放。这种方式结合了动态内存的灵活性和智能指针的便利性,可以更安全地管理对象的生命周期。
2.1.4 智能指针和异常
在使用智能指针和动态内存分配时,处理异常是一个重要的考虑因素。异常可能会导致资源泄漏或不正确的资源管理。智能指针在处理异常时提供了一些优势,可以确保在异常情况下也能正确地释放动态分配的内存。
以下是一个示例,演示了在使用智能指针和动态内存时,如何处理异常:
#include <iostream>
#include <memory>
class Resource {
public:
Resource() {
std::cout << "Resource acquired." << std::endl;
}
~Resource() {
std::cout << "Resource released." << std::endl;
}
void UseResource() {
std::cout << "Resource is being used." << std::endl;
}
};
int main() {
try {
// 使用 std::make_shared 创建 shared_ptr,分配内存
std::shared_ptr<Resource> shared = std::make_shared<Resource>();
// 模拟抛出异常
throw std::runtime_error("An exception occurred.");
// 使用 shared_ptr 调用对象的成员函数
shared->UseResource();
} catch (const std::exception& e) {
std::cerr << "Exception: " << e.what() << std::endl;
}
// shared_ptr 会在异常发生时自动释放内存
return 0;
}
在上述示例中,创建了一个 Resource
类,然后使用 std::make_shared
创建了一个 std::shared_ptr
来管理动态分配的 Resource
对象。接着,我们模拟抛出了一个异常。由于异常被捕获,程序会正确地释放动态分配的资源,避免了内存泄漏。
智能指针的一个优势是,即使在异常发生的情况下,也会在智能指针的析构函数中正确地释放资源。这在动态内存分配和资源管理方面提供了更安全和方便的方式。
2.1.5 unique_ptr
std::unique_ptr
是C++标准库中的一种智能指针,用于管理动态分配的内存,其特点是具有独占性,即同一时间只能有一个 std::unique_ptr
指向某块内存。这种特性使得 std::unique_ptr
适用于需要明确所有权关系的情况。
以下是 std::unique_ptr
的用法示例:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass(int value) : data(value) {
std::cout << "Constructing MyClass with value: " << data << std::endl;
}
~MyClass() {
std::cout << "Destructing MyClass with value: " << data << std::endl;
}
void Print() {
std::cout << "Value: " << data << std::endl;
}
private:
int data;
};
int main() {
// 使用 std::make_unique 创建 unique_ptr,分配内存
std::unique_ptr<MyClass> unique = std::make_unique<MyClass>(42);
// 使用 unique_ptr 调用对象的成员函数
unique->Print();
// unique_ptr 会在不再需要时自动释放内存
return 0;
}
在上面的示例中,std::make_unique
函数创建了一个 std::unique_ptr
,并通过 new
分配了一个 MyClass
对象的内存。与 std::shared_ptr
不同,std::unique_ptr
的所有权是独占的,即使在函数调用过程中,也不能复制或转移其所有权。
std::unique_ptr
在以下情况下非常有用:
- 当你需要确保只有一个指针能够拥有动态分配的资源时。
- 在函数内部分配动态内存,确保函数结束时会自动释放。
- 当你需要在容器中持有动态分配的对象,但不想在容器析构时显式释放。
虽然 std::unique_ptr
提供了更严格的所有权管理,但也要注意避免意外地释放相同内存的问题,以及不要在多个 std::unique_ptr
之间共享指针。
2.1.6 weak_ptr
std::weak_ptr
是C++标准库中的一种智能指针,用于解决 std::shared_ptr
的循环引用问题。循环引用可能导致资源泄漏,因为 std::shared_ptr
的引用计数不会降为零,从而导致对象无法被正确释放。std::weak_ptr
允许共享一个对象,但不会增加其引用计数,从而避免了循环引用问题。
以下是 std::weak_ptr
的用法示例:
#include <iostream>
#include <memory>
class MyClass;
// 在类外部声明一个 weak_ptr
std::weak_ptr<MyClass> globalWeak;
class MyClass {
public:
MyClass(int value) : data(value) {
std::cout << "Constructing MyClass with value: " << data << std::endl;
}
~MyClass() {
std::cout << "Destructing MyClass with value: " << data << std::endl;
}
void Print() {
std::cout << "Value: " << data << std::endl;
}
// 在成员函数中使用 weak_ptr
void CheckWeakPtr() {
std::shared_ptr<MyClass> shared = globalWeak.lock();
if (shared) {
std::cout << "Weak pointer is valid." << std::endl;
} else {
std::cout << "Weak pointer is expired." << std::endl;
}
}
private:
int data;
};
int main() {
{
// 使用 std::make_shared 创建 shared_ptr,分配内存
std::shared_ptr<MyClass> shared = std::make_shared<MyClass>(42);
// 将 shared_ptr 赋值给 weak_ptr
globalWeak = shared;
// 使用 shared_ptr 调用对象的成员函数
shared->Print();
}
// shared_ptr 已经超出作用域,对象会被销毁
// 在外部使用 weak_ptr 检查对象是否还存在
std::shared_ptr<MyClass> shared = globalWeak.lock();
if (shared) {
shared->Print();
} else {
std::cout << "Weak pointer is expired." << std::endl;
}
return 0;
}
在上述示例中,定义了一个 globalWeak
的全局 std::weak_ptr
,用于解决 std::shared_ptr
的循环引用问题。通过调用 globalWeak.lock()
可以获取一个临时的 std::shared_ptr
,用于检查对象是否还存在。如果对象已经被销毁,globalWeak.lock()
会返回一个空的 std::shared_ptr
。
使用 std::weak_ptr
可以避免 std::shared_ptr
循环引用问题,并确保对象在不再需要时能够被正确释放。
2.2 动态数组
2.2.1 new和数组
在C++中,可以使用 new
操作符来动态分配一个数组,然后使用指针来访问该数组的元素。但是,为了避免内存泄漏和错误,通常建议使用 std::vector
或 std::array
来代替动态数组。下面是使用 new
和数组的示例:
#include <iostream>
int main() {
int size;
std::cout << "Enter the size of the array: ";
std::cin >> size;
// 动态分配一个整数数组
int* dynamicArray = new int[size];
// 初始化数组元素
for (int i = 0; i < size; ++i) {
dynamicArray[i] = i * 2;
}
// 打印数组元素
std::cout << "Array elements: ";
for (int i = 0; i < size; ++i) {
std::cout << dynamicArray[i] << " ";
}
std::cout << std::endl;
// 释放动态分配的内存
delete[] dynamicArray;
return 0;
}
在上面的示例中,使用 new
操作符来动态分配一个整数数组,然后使用指针 dynamicArray
来访问数组的元素。请注意,使用 delete[]
来释放动态分配的数组内存。
然而,使用原始指针和动态数组存在一些潜在的问题,例如内存泄漏、数组越界等。为了避免这些问题,通常推荐使用标准库提供的容器,如 std::vector
或 std::array
,它们提供了更安全和方便的数组管理方式。以下是使用 std::vector
的示例:
#include <iostream>
#include <vector>
int main() {
int size;
std::cout << "Enter the size of the array: ";
std::cin >> size;
// 使用 std::vector 创建动态数组
std::vector<int> dynamicArray(size);
// 初始化数组元素
for (int i = 0; i < size; ++i) {
dynamicArray[i] = i * 2;
}
// 打印数组元素
std::cout << "Array elements: ";
for (int i = 0; i < size; ++i) {
std::cout << dynamicArray[i] << " ";
}
std::cout << std::endl;
return 0;
}
使用 std::vector
可以避免手动管理内存,同时提供了更多的功能和安全性。
2.2.2 allocator类
在 C++ 中,std::allocator
是一个用于分配和释放内存的标准库类模板,它提供了一种灵活的方式来管理动态分配的内存,特别适用于在没有使用标准容器的情况下分配内存。
std::allocator
类模板提供了 allocate
和 deallocate
成员函数,用于分配和释放内存块。这些成员函数可以与指针一起使用,用于手动管理动态分配的内存。
以下是使用 std::allocator
分配和释放动态数组内存的示例:
#include <iostream>
#include <memory>
int main() {
int size;
std::cout << "Enter the size of the array: ";
std::cin >> size;
// 使用 std::allocator 分配动态数组内存
std::allocator<int> alloc;
int* dynamicArray = alloc.allocate(size);
// 初始化数组元素
for (int i = 0; i < size; ++i) {
dynamicArray[i] = i * 2;
}
// 打印数组元素
std::cout << "Array elements: ";
for (int i = 0; i < size; ++i) {
std::cout << dynamicArray[i] << " ";
}
std::cout << std::endl;
// 使用 std::allocator 释放内存
alloc.deallocate(dynamicArray, size);
return 0;
}
在上述示例中,使用 std::allocator
类模板分配和释放动态数组内存。allocate
函数用于分配一块指定大小的内存块,然后使用指针 dynamicArray
来访问和初始化数组元素。最后,使用 deallocate
函数释放内存。
需要注意的是,std::allocator
是比较底层的内存管理工具,对于大多数情况下,推荐使用标准库提供的高级容器,如 std::vector
或 std::array
,它们会更方便和安全。使用 std::allocator
可以在某些特定情况下提供更细粒度的内存控制。
3 拷贝控制
3.1 拷贝、赋值与销毁
在C++中,拷贝、赋值和销毁是管理对象的重要方面。
拷贝构造函数:
拷贝构造函数是用于从现有对象创建新对象的特殊构造函数。它接受一个同类型的对象作为参数,并创建一个新对象,其内容与参数对象相同。拷贝构造函数通常在以下情况下被调用:
- 通过值传递对象给函数时
- 从对象返回值时
- 创建一个新对象,初始化为现有对象的副本
class MyClass {
public:
MyClass(const MyClass& other) {
// 实现拷贝构造逻辑
}
};
拷贝赋值运算符:
拷贝赋值运算符用于将一个对象的值赋给另一个对象。它定义了在赋值操作中如何复制对象的内容。拷贝赋值运算符的重载通常以 operator=
形式存在。
class MyClass {
public:
MyClass& operator=(const MyClass& other) {
if (this != &other) {
// 实现拷贝赋值逻辑
}
return *this;
}
};
析构函数:
析构函数在对象生命周期结束时被调用,用于清理对象分配的资源。它的任务通常包括释放动态分配的内存、关闭文件、释放其他资源等。
class MyClass {
public:
~MyClass() {
// 实现析构逻辑
}
};
三/五法则:
指的是拷贝构造函数、拷贝赋值运算符和析构函数的三个特殊成员函数。C++11 引入了移动构造函数和移动赋值运算符,扩展为五法则。遵循这些法则可以确保正确地管理对象的复制、赋值和销毁操作。
使用=default
:
=default
是 C++11 中引入的特殊成员函数说明符,用于将成员函数显式地指定为默认实现,即编译器生成的默认代码。
class MyClass {
public:
MyClass(const MyClass& other) = default; // 拷贝构造函数使用默认实现
};
阻止拷贝:
有时可能希望阻止对象被拷贝或赋值,可以将拷贝构造函数和拷贝赋值运算符声明为私有,或者使用 delete
关键字。
class NoCopy {
private:
NoCopy(const NoCopy& other) = delete; // 阻止拷贝
NoCopy& operator=(const NoCopy& other) = delete; // 阻止拷贝赋值
};
3.2 拷贝控制和资源管理
在C++中,拷贝控制和资源管理是确保对象正确复制、赋值和销毁的关键方面。有时,可能希望定义行为类似于值的类,以及行为类似于指针的类,这涉及到如何管理资源和处理拷贝。
行为像值的类:
行为像值的类是指,当进行拷贝操作时,对象的内容会被复制,而不是仅仅复制指向资源的引用。这样的类通常需要实现拷贝构造函数、拷贝赋值运算符和析构函数。每个对象都有自己独立的资源副本,这样可以避免对象之间的资源共享和争用。
class ValueLike {
public:
ValueLike(int data) : data_ptr(new int(data)) {}
ValueLike(const ValueLike& other) : data_ptr(new int(*other.data_ptr)) {}
ValueLike& operator=(const ValueLike& other) {
if (this != &other) {
delete data_ptr;
data_ptr = new int(*other.data_ptr);
}
return *this;
}
~ValueLike() {
delete data_ptr;
}
private:
int* data_ptr;
};
定义行为像指针的类:
行为像指针的类是指,多个对象可以共享同一资源,拷贝操作只复制资源的引用,而不会创建新的资源副本。这样的类通常需要引入引用计数,确保资源只在不再被引用时才被释放。
class PointerLike {
public:
PointerLike(int data) : data_ptr(new int(data)), ref_count(new int(1)) {}
PointerLike(const PointerLike& other) : data_ptr(other.data_ptr), ref_count(other.ref_count) {
++(*ref_count);
}
PointerLike& operator=(const PointerLike& other) {
if (this != &other) {
decrementAndDestroy();
data_ptr = other.data_ptr;
ref_count = other.ref_count;
++(*ref_count);
}
return *this;
}
~PointerLike() {
decrementAndDestroy();
}
private:
int* data_ptr;
int* ref_count;
void decrementAndDestroy() {
if (--(*ref_count) == 0) {
delete data_ptr;
delete ref_count;
}
}
};
上述代码演示了两种不同的拷贝控制策略:值语义和指针语义。行为像值的类在拷贝时创建独立的资源副本,而行为像指针的类共享相同的资源。
3.3 交换操作
在C++的拷贝控制中,交换操作是一种常用的技术,用于实现高效的拷贝和赋值操作。交换操作通过交换两个对象的内容,可以避免不必要的内存分配和复制,从而提高性能。
在自定义的类中,可以实现自己的交换操作,也可以使用标准库提供的 std::swap
函数。另外,C++11 引入了移动语义和移动构造函数,使得交换操作更加高效。
以下是使用交换操作的示例:
#include <iostream>
#include <algorithm>
class MyClass {
public:
MyClass(int value) : data(value) {}
// 自定义交换操作
void swap(MyClass& other) {
std::swap(data, other.data);
}
private:
int data;
};
int main() {
MyClass obj1(42);
MyClass obj2(77);
std::cout << "Before swapping:" << std::endl;
std::cout << "obj1: " << obj1.data << std::endl;
std::cout << "obj2: " << obj2.data << std::endl;
// 使用自定义的交换操作
obj1.swap(obj2);
std::cout << "After swapping:" << std::endl;
std::cout << "obj1: " << obj1.data << std::endl;
std::cout << "obj2: " << obj2.data << std::endl;
// 使用 std::swap 函数
std::swap(obj1, obj2);
std::cout << "After swapping with std::swap:" << std::endl;
std::cout << "obj1: " << obj1.data << std::endl;
std::cout << "obj2: " << obj2.data << std::endl;
return 0;
}
在上述示例中,首先自定义了一个 swap
成员函数来实现自己的交换操作。然后,使用标准库提供的 std::swap
函数进行交换。注意,std::swap
使用了 C++ 的 ADL(Argument-Dependent Lookup)机制,因此可以在自定义的命名空间中使用它。
交换操作的优点是可以避免不必要的复制,提高代码的性能和效率。然而,在使用交换操作时,要确保交换后的对象仍然保持有效的状态。此外,C++11 引入的移动语义可以进一步提高交换操作的效率,允许在移动资源时避免复制开销。
3.4 拷贝控制示例
当讨论C++中的拷贝控制时,通常会涉及拷贝构造函数、拷贝赋值运算符和析构函数等方面。下面是一个示例,演示了一个简单的类的拷贝控制操作:
#include <iostream>
#include <cstring>
class String {
public:
// 构造函数
String(const char* str = nullptr) {
if (str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
} else {
data = new char[1];
*data = '\0';
}
std::cout << "Constructor: " << data << std::endl;
}
// 拷贝构造函数
String(const String& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
std::cout << "Copy Constructor: " << data << std::endl;
}
// 拷贝赋值运算符
String& operator=(const String& other) {
if (this != &other) {
delete[] data;
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
std::cout << "Copy Assignment Operator: " << data << std::endl;
return *this;
}
// 析构函数
~String() {
std::cout << "Destructor: " << data << std::endl;
delete[] data;
}
// 打印字符串内容
void Print() const {
std::cout << data << std::endl;
}
private:
char* data;
};
int main() {
String s1("Hello");
String s2 = s1; // 调用拷贝构造函数
String s3;
s3 = s2; // 调用拷贝赋值运算符
s1.Print();
s2.Print();
s3.Print();
return 0;
}
在上述示例中,定义了一个简单的 String
类,它管理动态分配的字符串。我们实现了构造函数、拷贝构造函数、拷贝赋值运算符和析构函数。在 main
函数中,我们创建了几个对象并演示了拷贝控制的操作。
这个示例演示了对象在构造、拷贝和赋值过程中如何分配和释放内存,并展示了每个操作的调用顺序。拷贝构造函数和拷贝赋值运算符的实现确保了对象拷贝后具有独立的内存副本。析构函数负责释放动态分配的内存。
3.5 动态内存管理类
在C++中,实现一个动态内存管理类需要注意拷贝构造函数、拷贝赋值运算符、析构函数等拷贝控制操作,以确保正确地管理动态分配的内存。以下是一个示例,展示了一个简单的动态内存管理类的实现:
#include <iostream>
#include <cstring>
class DynamicString {
public:
// 构造函数
DynamicString(const char* str = nullptr) {
if (str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
} else {
data = new char[1];
*data = '\0';
}
}
// 拷贝构造函数
DynamicString(const DynamicString& other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
// 拷贝赋值运算符
DynamicString& operator=(const DynamicString& other) {
if (this != &other) {
delete[] data;
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
}
return *this;
}
// 析构函数
~DynamicString() {
delete[] data;
}
// 打印字符串内容
void Print() const {
std::cout << data << std::endl;
}
private:
char* data;
};
int main() {
DynamicString str1("Hello");
DynamicString str2 = str1; // 调用拷贝构造函数
DynamicString str3;
str3 = str2; // 调用拷贝赋值运算符
str1.Print();
str2.Print();
str3.Print();
return 0;
}
在上述示例中,定义了一个名为 DynamicString
的动态内存管理类,它类似于一个动态分配的字符串。类中实现了构造函数、拷贝构造函数、拷贝赋值运算符和析构函数,以正确地管理内存。在 main
函数中,创建了几个 DynamicString
对象并演示了拷贝控制的操作。
3.6 对象移动
在C++中,对象移动是一种重要的优化技术,可以减少资源的不必要拷贝,提高性能。C++11 引入了右值引用、移动构造函数和移动赋值运算符等特性来支持对象的移动操作。
右值引用:
右值引用是一种新的引用类型,通过 &&
符号表示。右值引用主要用于标识临时对象、表达式等即将被销毁的对象,从而允许移动资源而不是进行深拷贝。
int a = 42;
int&& rvalue_ref = std::move(a); // 将 a 转换为右值引用
移动构造函数和移动赋值运算符:
移动构造函数和移动赋值运算符允许将资源从一个对象移动到另一个对象,避免了不必要的拷贝。它们通常会“窃取”另一个对象的资源,并将其置为无效状态。
class MyString {
public:
// 移动构造函数
MyString(MyString&& other) noexcept : data(other.data) {
other.data = nullptr;
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
右值引用和成员函数:
右值引用常常与成员函数一起使用,以便在函数内部实现资源的移动。在移动构造函数和移动赋值运算符中,使用右值引用来识别被移动的对象。
class MyResource {
public:
MyResource(int size) : data(new int[size]) {}
~MyResource() { delete[] data; }
// 移动构造函数
MyResource(MyResource&& other) noexcept : data(other.data) {
other.data = nullptr;
}
// 移动赋值运算符
MyResource& operator=(MyResource&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
other.data = nullptr;
}
return *this;
}
private:
int* data;
};
这些特性使得对象在进行移动时,可以高效地将资源转移,而不会进行昂贵的资源复制。通过适当地使用移动构造函数和移动赋值运算符,可以提高性能并减少不必要的开销。
4 重载运算与类型转换
4.1 常见运算符
C++中的常见运算符可以被重载以适应自定义类的操作,使得类的对象可以像内置类型一样进行各种运算。以下是一些常见的运算符及其重载示例:
重载输出运算符 <<
:
重载输出运算符允许你以自定义的方式将对象的内容输出到流中,通常用于输出对象的信息。
#include <iostream>
class MyClass {
public:
int value;
friend std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
os << "Value: " << obj.value;
return os;
}
};
int main() {
MyClass obj;
obj.value = 42;
std::cout << obj << std::endl; // 使用重载的输出运算符
return 0;
}
重载输入运算符 >>
:
重载输入运算符允许你以自定义的方式从流中读取数据,并为对象赋值。
#include <iostream>
class MyClass {
public:
int value;
friend std::istream& operator>>(std::istream& is, MyClass& obj) {
is >> obj.value;
return is;
}
};
int main() {
MyClass obj;
std::cout << "Enter a value: ";
std::cin >> obj; // 使用重载的输入运算符
std::cout << "Value entered: " << obj.value << std::endl;
return 0;
}
相等运算符 ==
:
相等运算符用于比较两个对象是否相等。
bool operator==(const MyClass& lhs, const MyClass& rhs) {
return lhs.value == rhs.value;
}
关系运算符 <
, >
, <=
, >=
:
关系运算符用于比较两个对象的大小关系。
bool operator<(const MyClass& lhs, const MyClass& rhs) {
return lhs.value < rhs.value;
}
bool operator>(const MyClass& lhs, const MyClass& rhs) {
return lhs.value > rhs.value;
}
bool operator<=(const MyClass& lhs, const MyClass& rhs) {
return lhs.value <= rhs.value;
}
bool operator>=(const MyClass& lhs, const MyClass& rhs) {
return lhs.value >= rhs.value;
}
赋值运算符 =
:
赋值运算符用于将一个对象的值赋给另一个对象。
MyClass& operator=(MyClass& lhs, const MyClass& rhs) {
lhs.value = rhs.value;
return lhs;
}
下标运算符 []
:
下标运算符用于访问类对象中的元素,如数组或容器。
class MyArray {
public:
int data[5];
int& operator[](int index) {
return data[index];
}
};
int main() {
MyArray arr;
arr[0] = 42; // 使用重载的下标运算符
std::cout << "Value at index 0: " << arr[0] << std::endl;
return 0;
}
递增和递减运算符 ++
, --
:
递增和递减运算符用于增加或减少对象的值。
MyClass& operator++(MyClass& obj) { // 前置递增
++obj.value;
return obj;
}
MyClass operator++(MyClass& obj, int) { // 后置递增
MyClass temp = obj;
++obj.value;
return temp;
}
MyClass& operator--(MyClass& obj) { // 前置递减
--obj.value;
return obj;
}
MyClass operator--(MyClass& obj, int) { // 后置递减
MyClass temp = obj;
--obj.value;
return temp;
}
成员访问运算符 ->
:
成员访问运算符用于通过对象指针访问对象的成员。
class MyPointer {
public:
MyClass* ptr;
MyPointer(MyClass* p) : ptr(p) {}
MyClass* operator->() {
return ptr;
}
};
int main() {
MyClass obj;
obj.value = 42;
MyPointer ptr(&obj);
std::cout << "Value using ->: " << ptr->value << std::endl;
return 0;
}
这些示例展示了如何重载一些常见的C++运算符,使得自定义类的对象可以进行类似于内置类型的运算。实际中,可以根据需要选择要重载的运算符以及如何实现重载。
4.2 函数调用运算符
4.2.1 lambda 是函数对象
在C++中,函数调用运算符 ()
可以被重载,允许对象在像函数一样被调用。这使得对象可以具有函数的行为,从而创建了所谓的“函数对象”。而lambda表达式本质上也是一种函数对象的形式。
函数对象:
函数对象是指实现了函数调用运算符 operator()
的类对象,使得该对象可以像函数一样被调用。函数对象在一些场景中非常有用,例如用作算法的谓词、排序函数、转换函数等。
class Adder {
public:
int operator()(int a, int b) {
return a + b;
}
};
int main() {
Adder add;
int result = add(3, 4); // 使用函数对象
std::cout << "Result: " << result << std::endl;
return 0;
}
Lambda表达式:
Lambda表达式是一种匿名函数的方式,可以在需要函数对象的地方使用,而无需显式定义一个类。Lambda表达式使用类似于函数的语法,可以在其内部捕获外部变量,并具有函数调用运算符。
int main() {
int x = 10;
int y = 20;
auto add = [x, y](int a, int b) {
return a + b + x + y;
};
int result = add(3, 4); // 使用lambda函数对象
std::cout << "Result: " << result << std::endl;
return 0;
}
在这个例子中,add
是一个lambda表达式,它捕获了外部变量 x
和 y
,并在函数调用运算符中执行了加法操作。
Lambda表达式的使用方便了函数对象的创建,尤其在需要较短的功能代码块时非常实用。无论是函数对象还是lambda表达式,它们都允许你像使用函数一样调用对象,并将其作为函数的参数传递。
4.2.2 标准库定义的函数对象
C++标准库中提供了许多预定义的函数对象,这些函数对象可以在算法中使用,或者可以自定义进行排序、转换、谓词等操作。这些函数对象通常是通过模板类来实现的,使其具有通用性和灵活性。
以下是一些常见的标准库定义的函数对象:
1. std::plus
, std::minus
, std::multiplies
, std::divides
:
这些函数对象用于执行基本的数学运算。
#include <iostream>
#include <functional>
int main() {
std::plus<int> add;
std::cout << "5 + 3 = " << add(5, 3) << std::endl;
std::minus<int> subtract;
std::cout << "8 - 2 = " << subtract(8, 2) << std::endl;
std::multiplies<int> multiply;
std::cout << "4 * 6 = " << multiply(4, 6) << std::endl;
std::divides<double> divide;
std::cout << "12 / 3 = " << divide(12, 3) << std::endl;
return 0;
}
2. std::less
, std::greater
, std::less_equal
, std::greater_equal
:
这些函数对象用于比较两个值的大小关系。
#include <iostream>
#include <functional>
int main() {
std::less<int> less_than;
std::cout << "5 < 8: " << less_than(5, 8) << std::endl;
std::greater<int> greater_than;
std::cout << "10 > 7: " << greater_than(10, 7) << std::endl;
std::less_equal<int> less_or_equal;
std::cout << "3 <= 3: " << less_or_equal(3, 3) << std::endl;
std::greater_equal<int> greater_or_equal;
std::cout << "9 >= 6: " << greater_or_equal(9, 6) << std::endl;
return 0;
}
3. std::negate
:
这个函数对象用于对值进行取反操作。
#include <iostream>
#include <functional>
int main() {
std::negate<int> negate;
std::cout << "Negate 7: " << negate(7) << std::endl;
return 0;
}
4. std::logical_and
, std::logical_or
, std::logical_not
:
这些函数对象用于执行逻辑运算。
#include <iostream>
#include <functional>
int main() {
std::logical_and<bool> logical_and;
std::cout << "true && false: " << logical_and(true, false) << std::endl;
std::logical_or<bool> logical_or;
std::cout << "true || false: " << logical_or(true, false) << std::endl;
std::logical_not<bool> logical_not;
std::cout << "!true: " << logical_not(true) << std::endl;
return 0;
}
这些标准库定义的函数对象使得在算法中执行常见的操作更加方便。还可以结合lambda表达式使用这些函数对象,以实现更灵活的功能。
4.2.3 可调用对象与 function
C++中有多种方式可以表示可调用对象(函数、函数指针、函数对象、lambda表达式等),其中std::function
是一个非常有用的工具,可以用来封装各种可调用对象,并提供统一的接口进行调用。
可调用对象:
可调用对象是指可以像函数一样被调用的实体,可以是函数、函数指针、函数对象(类对象,重载了函数调用运算符)、lambda表达式等。下面是一些例子:
int add(int a, int b) {
return a + b;
}
struct Multiply {
int operator()(int a, int b) {
return a * b;
}
};
int main() {
// 函数
int result1 = add(3, 4);
// 函数对象
Multiply multiply;
int result2 = multiply(5, 6);
// Lambda表达式
auto lambda = [](int a, int b) { return a / b; };
int result3 = lambda(10, 2);
return 0;
}
std::function
:
std::function
是C++标准库提供的一个通用的函数包装器,可以用来封装各种可调用对象,包括函数、函数指针、函数对象和lambda表达式等。它提供了统一的接口,可以在不知道具体可调用对象类型的情况下进行函数调用。
#include <iostream>
#include <functional>
int add(int a, int b) {
return a + b;
}
struct Multiply {
int operator()(int a, int b) {
return a * b;
}
};
int main() {
// 使用std::function封装函数
std::function<int(int, int)> func1 = add;
int result1 = func1(3, 4);
// 使用std::function封装函数对象
Multiply multiply;
std::function<int(int, int)> func2 = multiply;
int result2 = func2(5, 6);
// 使用std::function封装Lambda表达式
std::function<int(int, int)> func3 = [](int a, int b) { return a / b; };
int result3 = func3(10, 2);
std::cout << "Result 1: " << result1 << std::endl;
std::cout << "Result 2: " << result2 << std::endl;
std::cout << "Result 3: " << result3 << std::endl;
return 0;
}
在这个示例中,使用std::function
来封装不同类型的可调用对象,并在不同情况下进行函数调用。std::function
提供了一种通用的方式来处理可调用对象,使得代码更加灵活和可维护。
4.3 重载、类型转换与运算符
4.3.1 类型转换运算符
C++允许自定义类型转换,这可以通过重载特定的类型转换运算符来实现。类型转换运算符允许你将一个类的对象从一种类型隐式转换为另一种类型。这在某些情况下可以提供更方便的代码,但同时也需要谨慎使用,以避免歧义和意外的行为。
C++中可以重载的类型转换运算符有以下几种:
- 转换到基本数据类型:
class MyNumber {
public:
MyNumber(int value) : val(value) {}
operator int() {
return val;
}
private:
int val;
};
int main() {
MyNumber num(42);
int x = num; // 使用转换运算符将 MyNumber 转换为 int
return 0;
}
- 转换到类类型:
class MyString {
public:
MyString(const char* str) : data(str) {}
operator const char*() {
return data;
}
private:
const char* data;
};
int main() {
MyString str("Hello");
const char* cstr = str; // 使用转换运算符将 MyString 转换为 const char*
return 0;
}
注意,类型转换运算符应该谨慎使用,因为它们可能会导致隐式转换,使代码变得难以理解和维护。
4.3.2 避免有二义性的类型转换
在C++中,重载类型转换运算符时需要小心,以避免造成二义性的情况。二义性可能会在多个可行的类型转换路径之间导致编译器无法选择最合适的转换,从而产生错误或不明确的行为。
以下是一些避免类型转换二义性的方法:
1. 显式类型转换操作符:
可以通过定义显式的类型转换函数来避免二义性。例如,可以定义一个名为to_int()
的成员函数来明确指定要进行的类型转换。
class MyNumber {
public:
MyNumber(int value) : val(value) {}
int to_int() const {
return val;
}
private:
int val;
};
int main() {
MyNumber num(42);
int x = num.to_int(); // 显式调用类型转换函数
return 0;
}
2. 使用不同参数数量的构造函数:
如果类型转换运算符的使用可能引发二义性,可以考虑使用不同数量的构造函数来创建对象,以避免二义性。
class MyNumber {
public:
MyNumber(int value) : val(value) {}
MyNumber(double value) : val(static_cast<int>(value)) {}
operator int() const {
return val;
}
private:
int val;
};
int main() {
MyNumber num1(42);
MyNumber num2(3.14);
int x = num1; // 正确:调用 operator int()
int y = num2; // 正确:调用 operator int()
return 0;
}
3. 显式类型转换函数:
在重载类型转换运算符的同时,也可以定义显式的类型转换函数,以提供更明确的转换路径。
class MyNumber {
public:
MyNumber(int value) : val(value) {}
operator int() const {
return val;
}
double to_double() const {
return static_cast<double>(val);
}
private:
int val;
};
int main() {
MyNumber num(42);
int x = num; // 隐式转换,调用 operator int()
double y = num.to_double(); // 显式调用转换函数
return 0;
}
通过以上方法,你可以减少类型转换二义性可能性,并在需要进行类型转换时明确指定要使用的转换路径。
4.3.3 函数匹配与重载运算符
在C++中,函数匹配和运算符重载是一种重要的特性,可以使自定义的类对象表现得像内置类型一样进行操作。函数匹配是指编译器在调用函数或操作符时选择最匹配的函数或操作符重载版本的过程。
当你重载运算符时,编译器会根据函数的参数和返回值类型来决定使用哪个重载版本。下面是一些重载运算符的示例,演示了函数匹配和运算符重载的原理:
1. 一元运算符重载:
#include <iostream>
class Complex {
public:
double real, imag;
Complex(double r, double i) : real(r), imag(i) {}
// 一元负号运算符重载
Complex operator-() const {
return Complex(-real, -imag);
}
};
int main() {
Complex c1(3.0, 4.0);
Complex c2 = -c1; // 调用一元负号运算符重载
std::cout << "c1: " << c1.real << " + " << c1.imag << "i" << std::endl;
std::cout << "c2: " << c2.real << " + " << c2.imag << "i" << std::endl;
return 0;
}
2. 二元运算符重载:
#include <iostream>
class Complex {
public:
double real, imag;
Complex(double r, double i) : real(r), imag(i) {}
// 加法运算符重载
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
};
int main() {
Complex c1(3.0, 4.0);
Complex c2(1.0, 2.0);
Complex c3 = c1 + c2; // 调用加法运算符重载
std::cout << "c1: " << c1.real << " + " << c1.imag << "i" << std::endl;
std::cout << "c2: " << c2.real << " + " << c2.imag << "i" << std::endl;
std::cout << "c3: " << c3.real << " + " << c3.imag << "i" << std::endl;
return 0;
}
在上述示例中,重载了一元负号运算符和加法运算符,使得 Complex
类的对象可以进行相应的操作。编译器会根据函数参数的类型和返回值类型来匹配最合适的重载版本。
如果需要转载,请加上本文链接:https://blog.csdn.net/a13027629517/article/details/132484340?spm=1001.2014.3001.5501