C++快速回顾(三)

前言

在Android音视频开发中,网上知识点过于零碎,自学起来难度非常大,不过音视频大牛Jhuster提出了《Android 音视频从入门到提高 - 任务列表》,结合我自己的工作学习经历,我准备写一个音视频系列blog。C/C++是音视频必备编程语言,我准备用几篇文章来快速回顾C++。本文是音视频系列blog的其中一个, 对应的要学习的内容是:快速回顾C++的关联容器,动态内存,拷贝控制,重载运算与类型转换。


音视频系列blog

音视频系列blog: 点击此处跳转查看


目录

在这里插入图片描述


1 关联容器

1.1 关联容器定义与使用

C++标准库提供了关联容器,用于存储键-值对,并支持通过键来快速查找和访问值。关联容器是基于树结构实现的,因此在大部分操作中具有较好的性能,例如查找、插入和删除。常见的关联容器有 std::mapstd::multimapstd::setstd::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::mapstd::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_mapstd::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,从而避免了手动调用 newdelete。当不再有 shared_ptr 指向对象时,对象会自动被销毁,释放其占用的内存。

使用 std::shared_ptr 可以大大减少内存泄漏的风险,同时简化了动态内存管理的工作。


2.1.2 直接管理内存

在C++中,可以使用动态内存分配操作符 newdelete 来直接管理内存,但这需要手动跟踪内存的分配和释放,容易导致内存泄漏或者访问已释放内存的问题。智能指针是一种更安全和方便的方式来管理动态内存,因为它们自动处理内存的分配和释放。

以下是使用 newdelete 进行直接内存管理的示例:

#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 来释放分配的内存。然而,直接使用 newdelete 存在一些潜在问题:

  1. 内存泄漏:如果忘记调用 delete 来释放分配的内存,就会导致内存泄漏,占用的内存无法被回收。
  2. 二次释放:如果多次调用 delete 来释放同一块内存,会导致未定义的行为,可能会访问已释放的内存。
  3. 异常安全性:在使用 newdelete 进行内存管理时,需要处理异常情况,确保在发生异常时也能正确释放内存。

相比之下,智能指针如 std::shared_ptrstd::unique_ptr 能够更安全地管理内存,避免了上述问题。它们会自动在不再需要时释放内存,减少了手动管理内存的烦恼。


2.1.3 shared_ptr 和 new 结合使用

在C++中,可以将 std::shared_ptrnew 结合使用,以便在动态内存中创建对象并使用智能指针来管理其生命周期。这样可以兼顾动态内存的灵活性和智能指针的便利性,同时避免了手动释放内存的繁琐工作。

以下是一个示例,演示如何使用 std::shared_ptrnew 结合创建对象并进行管理:

#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 在以下情况下非常有用:

  1. 当你需要确保只有一个指针能够拥有动态分配的资源时。
  2. 在函数内部分配动态内存,确保函数结束时会自动释放。
  3. 当你需要在容器中持有动态分配的对象,但不想在容器析构时显式释放。

虽然 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_ptrvoid 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_ptrglobalWeak = 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::vectorstd::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::vectorstd::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 类模板提供了 allocatedeallocate 成员函数,用于分配和释放内存块。这些成员函数可以与指针一起使用,用于手动管理动态分配的内存。

以下是使用 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::vectorstd::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表达式,它捕获了外部变量 xy,并在函数调用运算符中执行了加法操作。

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++中可以重载的类型转换运算符有以下几种:

  1. 转换到基本数据类型
class MyNumber {
public:MyNumber(int value) : val(value) {}operator int() {return val;}private:int val;
};int main() {MyNumber num(42);int x = num; // 使用转换运算符将 MyNumber 转换为 intreturn 0;
}
  1. 转换到类类型
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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/86117.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Flask 单元测试

如果一个软件项目没有经过测试&#xff0c;就像做的菜里没加盐一样。Flask 作为一个 Web 软件项目&#xff0c;如何做单元测试呢&#xff0c;今天我们来了解下&#xff0c;基于 unittest 的 Flask 项目的单元测试。 什么是单元测试 单元测试是软件测试的一种类型。顾名思义&a…

C#,数值计算——调适数值积分法(adaptive quadrature)的计算方法与源程序

1 文本格式 using System; namespace Legalsoft.Truffer { /// <summary> /// 调适数值积分法 /// adaptive quadrature /// </summary> public class Adapt { private double x1 { get; } 0.942882415695480; private …

kali 2023.3新增工具

在终端模拟器中运行 sudo apt update && sudo apt full-upgrade 命令来更新其安装 Kali Linux 2023.3 发布中包含了九个新工具&#xff0c;分别是&#xff1a; Calico&#xff1a;云原生网络和网络安全。 cri-tools&#xff1a;用于Kubelet容器运行时接口的命令行界面…

Nginx全局配置

目录 一、修改启动进程数 二、日制分割 三、nginx进程的优先级&#xff08;work进程的优先级&#xff09; 四、http设置 4.1http 协议配置说明 4.2mime 4.3 server块构建虚拟主机 4.4 location 一、修改启动进程数 worker_processes 1; #允许的启动工作进程数数量…

【字节跳动青训营】后端笔记整理-4 | Go框架三件套之GORM的使用

**本人是第六届字节跳动青训营&#xff08;后端组&#xff09;的成员。本文由博主本人整理自该营的日常学习实践&#xff0c;首发于稀土掘金。 我的go开发环境&#xff1a; *本地IDE&#xff1a;GoLand 2023.1.2 *go&#xff1a;1.20.6 *MySQL&#xff1a;8.0 本文介绍Go框架三…

Redis五大数据类型

Redis五大数据类型 Redis-Key 官网&#xff1a;https://www.redis.net.cn/order/ 序号命令语法描述1DEL key该命令用于在 key 存在时删除 key2DUMP key序列化给定 key &#xff0c;并返回被序列化的值3EXISTS key检查给定 key 是否存在&#xff0c;存在返回1&#xff0c;否则返…

nodejs里面的event loop

1. event loop 1.1 什么是event-loop js的标准文档定义如下 https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop#event_loop https://javascript.info/event-loop html的标准定义 https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-proc…

prometheus监控JVM(接入tomcat)

一、整合jmx_exporter及tomcat 1、 jmx_exporter下载地址 https://github.com/prometheus/jmx_exporter/releases 2、 tomcat配置文件下载地址 https://github.com/prometheus/jmx_exporter/blob/main/example_configs/tomcat.yml 3、创建tomcat_exporter目录 [rootlocalhost ~…

Vue 项目布署后,刷新页面(或跳转页面)出现 404 解决办法

Vue 项目布署后&#xff0c;刷新页面&#xff08;或跳转页面&#xff09;出现 404 问题背景为什么会出现404解决办法&#xff08;两种&#xff09;方法一&#xff1a;改变服务器配置方法二&#xff1a;改变路由模式 单页应用(SPA)概念 问题背景 今天重新部署一个vue项目的时候…

第七周第七天学习总结 | MySQL入门及练习学习第二天

实操练习&#xff1a; 1.创建一个名为 cesh的数据库 2.在这个数据库内 创建一个名为 xinxi 的表要求该表可以包含&#xff1a;编号&#xff0c;姓名&#xff0c;备注的信息 3.为 ceshi 表 添加数据 4.为xinxi 表的数据设置中文别名 5.查询 在 xinxi 表中编号 为2 的全部…

Open3D 进阶(6)Kmeans点云聚类

目录 一、算法原理1、原理概述2、实现流程3、参考文献二、代码实现三、结果展示四、测试数据本文由CSDN点云侠原创,原文链接。如果你不是在点云侠的博客中看到该文章,那么此处便是不要脸的爬虫。 一、算法原理 1、原理概述 kmeans 聚类算法也称为

最新Burp Suite入门技术

Burp Suite的安装 Burp Suite是一款集成化的渗透测试工具&#xff0c;包含了很多功能&#xff0c;可以帮助我们高效地完成对Web应用程序的渗透测试和安全检测。 Burp Suite由Java语言编写&#xff0c;Java自身的跨平台性使我们能更方便地学习和使用这款软件。不像其他自动化测…