C++学习笔记(努力学习扫盲版)

基础学习笔记

1.c++标准库sstream的用法

<sstream> 定义了三个类:istringstream、ostringstream 和 stringstream,分别用来进行流的输入、输出和输入输出操作

**<sstream> 主要用来进行数据类型转换,由于<sstream> 使用 string 对象来代替字符数组(snprintf方式),就避免缓冲区溢出的危险;**而且,因为传入参数和目标对象的类型会被自动推导出来,所以不存在错误的格式化符的问题。简单说,相比c库的数据类型转换而言,<sstream> 更加安全、自动和直接。

数据类型转换例子:
#include <string>
#include <sstream>
#include <iostream>
#include <stdio.h>
 
using namespace std;
 
int main()
{
    stringstream sstream;
    string strResult;
    int nValue = 1000;
 
    // 将int类型的值放入输入流中
    sstream << nValue;
    // 从sstream中抽取前面插入的int类型的值,赋给string类型
    sstream >> strResult;
 
    cout << "[cout]strResult is: " << strResult << endl;
    printf("[printf]strResult is: %s\n", strResult.c_str());
 
    return 0;
}

多个字符串拼接例子:

本示例介绍在 stringstream 中存放多个字符串,实现多个字符串拼接的目的(其实完全可以使用 string 类实现),同时,介绍 stringstream 的清空方法。

#include <string>
#include <sstream>
#include <iostream>
 
using namespace std;
 
int main()
{
    stringstream sstream;
 
    // 将多个字符串放入 sstream 中
    sstream << "first" << " " << "string,";
    sstream << " second string";
    cout << "strResult is: " << sstream.str() << endl;
 
    // 清空 sstream
    sstream.str("");
    sstream << "third string";
    cout << "After clear, strResult is: " << sstream.str() << endl;
 
    return 0;
}

从上述代码执行结果能够知道:

  • 可以使用 str() 方法,将 stringstream 类型转换为 string 类型;
  • 可以将多个字符串放入 stringstream 中,实现字符串的拼接目的;
  • 如果想清空 stringstream,必须使用 sstream.str(""); 方式;clear() 方法适用于进行多次数据类型转换的场景。

stringstream的清空

清空 stringstream 有两种方法:clear() 方法以及 str(“”) 方法,这两种方法有不同的使用场景。str(“”) 方法的使用场景,在上面的示例中已经介绍了,这里介绍 clear() 方法的使用场景。示例代码(stringstream_test3.cpp)如下:

#include <sstream>
#include <iostream>
 
using namespace std;
 
int main()
{
    stringstream sstream;
    int first, second;
 
    // 插入字符串
    sstream << "456";
    // 转换为int类型
    sstream >> first;
    cout << first << endl;
 
    // 在进行多次类型转换前,必须先运行clear()
    sstream.clear();
 
    // 插入bool值
    sstream << true;
    // 转换为int类型
    sstream >> second;
    cout << second << endl;
 
    return 0;
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

注意:

在本示例涉及的场景下(多次数据类型转换),必须使用 clear() 方法清空 stringstream,不使用 clear() 方法或使用 str(“”) 方法,都不能得到数据类型转换的正确结果。

2.setw()和setfill()用法

  • 在C++中,setw(int n)用来控制输出间隔。
  • setw()默认填充的内容为空格,可以setfill()配合使用设置其他字符填充。

例子:

cout<<'s'<<setw(8)<<'a'<<endl;

则在屏幕显示

s        a 
//s与a之间有7个空格,加上a就8个位置,setw()只对其后面紧跟的输出产生作用,如上例中,表示'a'共占8个位置,不足的用空格填充。若输入的内容超过setw()设置的长度,则按实际长度输出

我们在设置域宽和填充字符的时候要注意几点:
①设置域宽的时候应该填入整数,设置填充字符的时候应该填入字符。
②我们可以对一个要输出的内容同时设置域宽和 填充字符,但是设置好的属性仅对下一个输出的内容有效,之后的输出要再次设置。即 cout <<setw(2) <<a <<b;语句中域宽设置仅对a有效,对b无效。

③setw和setfill 被称为输出控制符,使用时需要在程序开头写上#include “iomanip.h”,否则无法使用。

3.static_cast用法

1.1 基础数据类型的转换

可以将一种基础数据类型转换为另一种基础数据类型。例如,将 double 转换为 int,或将 float 转换为 double 等。

double d = 5.5;
int i = static_cast<int>(d);  // i = 5
1.2 指向派生类的指针或引用转换为指向基类的指针或引用
class Base {};
class Derived : public Base {};
Derived derivedObj;
Base* basePtr = static_cast<Base*>(&derivedObj);
1.3 指向基类的指针或引用转换为指向派生类的指针或引用

但这是不安全的,因为在转换过程中没有运行时检查。如果确实需要运行时检查,应使用 dynamic_cast

Base* basePtr = new Base();
Derived* derivedPtr = static_cast<Derived*>(basePtr); // 不安全!
1.4 在有关联的类型之间进行转换

例如,转换枚举值为整数。

enum Color { RED, GREEN, BLUE };
int value = static_cast<int>(GREEN);  // value = 1
限制:
  • 不能用于转换不相关的指针或引用类型。例如,不能将 void* 转换为其他类型的指针,或反之。在这种情况下,应使用 reinterpret_cast。

  • 不能用于移除或添加 const 限定符。在这种情况下,应使用 const_cast。

优点:
  • 对于 C 风格的强制转换(如 (int)3.14),static_cast 更加明确和可读。
  • 与 C 风格的强制转换相比,static_cast 只能执行明确允许的转换,这有助于避免一些错误。
注意:

使用 static_cast 时,需要确保转换在逻辑上是合理的,因为 static_cast 不执行运行时类型检查。如果对转换不确定,考虑使用其他类型的转换,如 dynamic_cast,或者重新评估设计,以避免需要转换

4.set中find的函数

Set.find()定义:

set容器中查找值为x的元素,复杂度为O(log n):
若存在,返回一个迭代器,指向键值x;
若不存在,返回一个迭代器,指向set.end()。

查找set容器中是否有元素x:

set<int> test;
if(test.find(x)!=test.end());	
{
···
}

输出find查找到元素的值:

set<int> test;
cout<<*test.find(x);

5.auto &详解

下面这段代码,localCharFreq是一个map<char, int> 类型,娜美为什么要用auto &p来表示每一个元素呢?

		map<char, int> globalCharFreq;
        for (int i = 0; i < strings.size(); ++i) {
            map<char, int> localCharFreq;
            for (char ch : strings[i]) {
                localCharFreq[ch]++;
            }
            for (auto &p : localCharFreq) {
                if (p.second >= n) {
                    if (i == 0) {
                        // 第一个字符串初始化计数
                        globalCharFreq[p.first] = 1;
                    } else if (globalCharFreq.find(p.first) != globalCharFreq.end()) {
                        // 后续字符串累加计数
                        globalCharFreq[p.first]++;
                    }
                }
            }
        }
  1. auto的使用auto关键字使得编译器自动推导变量的类型。在这个上下文中,由于localCharFreq是一个std::map<char, int>,其元素类型为std::pair<const char, int>。使用auto可以避免手动书写繁长的类型声明,使代码更加简洁易读。
  2. 为什么要加&:加上&表示p是对当前元素的引用,而不是其副本。这意味着循环体中对p所做的更改将反映到localCharFreq中的原始元素上。在这个场景里,虽然直接修改localCharFreq中的元素并不是我们的目的(因为我们只是读取信息),使用引用同样是为了避免不必要的复制操作,提高效率。对于简单类型(如intchar)来说,使用引用与否的性能差异微乎其微,但对于复合类型(如std::pair<const char, int>)来说,避免复制是一种好的实践,特别是当对象比较大或复制操作比较昂贵时。

总结起来,for (auto &p : localCharFreq)的使用是为了通过引用直接访问localCharFreq的每个元素,这样可以提高代码的效率和可读性。尽管在这个场合中修改元素并不适用,这种方法仍然被视为好的实践,以适应更广泛的使用场景。

6.Vector中的erase方法和unique函数

std::unique 是 C++ 标准库中定义在 <algorithm> 头文件中的一个函数模板,其主要用途是在一个序列中移除连续重复的元素。

函数原型如下:

template <class ForwardIterator>
ForwardIterator unique (ForwardIterator first, ForwardIterator last);

unique 函数通常与 sort 函数结合使用。首先对序列进行排序,以便所有相等的元素紧随其后,然后使用 unique 函数移除那些重复的元素。此时要注意,**unique 函数不实际删除任何元素,而是把要移除的元素移到容器末尾,并且返回一个指向"移除"元素区域开头的迭代器。**因此一般会将erase方法和unique函数结合使用,比如当移除vector中重复相邻的元素操作时,可以如下操作:

#include <algorithm>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> v = {1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4};

    auto last = std::unique(v.begin(), v.end());//此时返回的是“移除”元素区域的开头位置
    v.erase(last, v.end());

    for (const auto &i : v) {
        std::cout << i << " ";
    }
    // Output: 1 2 3 4
}

7.stoul函数详解

在 C++ 中,stoul 是一个标准库函数,用于将字符串转换成无符号长整型(unsigned long)。这个函数属于 <string> 头文件中定义的一系列转换函数之一,能够处理不同基数的数值字符串,如二进制、八进制、十进制和十六进制。

函数原型:

unsigned long stoul(const string& str, size_t* idx = 0, int base = 10);
unsigned long stoul(const wstring& str, size_t* idx = 0, int base = 10);

参数说明:

  • str: 要转换的字符串,它包含了要转换的无符号长整型数。该字符串可以在数值前含有空格,函数会自动忽略这些空格。
  • idx: 一个可选的输出参数(通过指针传递),用于存储完成转换后的第一个不是数值字符的索引。如果不关心这个信息,可以默认传递 0
  • base: 数值字符串的基数。默认值是 10,表示十进制,但可以设置为 236 之间的任何值,或者是特殊值 0。如果设定为 0,则函数会根据字符串的格式自动判断基数:如果字符串以 “0x” 或 “0X” 开头,则认为基数为 16(十六进制);如果字符串以 “0” 开头,则认为基数为 8(八进制);否则,默认为 10(十进制)。

返回值:

  • 函数返回一个 unsigned long 类型的数值,它是从字符串中解析出的。

错误处理:

  • 如果 str 不能转换成一个有效的数值,函数会抛出一个 std::invalid_argument 异常。
  • 如果解析的数值超出了 unsigned long 能表示的范围,函数会抛出一个 std::out_of_range 异常。

示例:

#include <iostream>
#include <string>

int main() {
    std::string number = "12345";
    std::string hexNumber = "0x1ABCDE";
    size_t idx = 0;

    unsigned long n = std::stoul(number, &idx, 10);
    unsigned long hexN = std::stoul(hexNumber, nullptr, 16); // Base is 16 for hexadecimal

    std::cout << "Decimal: " << n << std::endl;           // 输出: Decimal: 12345
    std::cout << "Hexadecimal: " << hexN << std::endl;     // 输出: Hexadecimal: 17777766
    
    try {
        std::string invalidNumber = "invalid";
        std::stoul(invalidNumber); // This will throw std::invalid_argument
    } catch (std::exception& e) {
        std::cout << "Caught exception: " << e.what() << std::endl; // 输出: Caught exception: invalid stoul argument
    }
    
    return 0;
}

在上面的示例中,我们可以看到如何将十进制和十六进制的字符串转换为 unsigned long 类型。同时,我们也看到了当提供了一个非法值的字符串时,stoul 如何通过抛出异常来处理错误。

8.C++函数中getline函数

在C++中,getline 函数是一个非常实用的函数,用于从输入流中读取一行。它通常用于从标准输入(即键盘)读取一行文本,或者从文件中读取一行。getline 函数有几个版本,但最常用的是与string类一起使用的版本。

这里有一个例子来说明如何使用getline函数:

#include <iostream>
#include <string> // 需要引入字符串库

using namespace std;

int main() {
    string line;
    
    // 从标准输入读取一行,存储到变量line中
    cout << "请输入一些文字: ";
    getline(cin, line);

    // 输出读取的内容
    cout << "你输入的是: " << line << endl;

    return 0;
}

在上面的代码中,getline(cin, line); 这行代码从cin(即标准输入,通常为键盘输入)读取一行文本,并将其存储在line变量中。值得注意的是,这个函数会读取一行文本直到遇到换行符\n为止,但换行符本身不会被存储在结果字符串中。

getline函数具有高度的灵活性,尤其是在处理输入时不确定输入行的长度时。它可以读取任意长度的字符串,直到遇到结束的换行符,这使得它非常适合读取用户输入以及处理文本文件中的数据。

此外,getline 还有一个重载版本,允许我们指定除了标准的换行符\n以外的分隔符。比如,如果你想按照逗号,来分割文本行,你可以这样做:

getline(cin, line, ',');

这会从cin读取文本直到遇到逗号,为止,同样逗号不会被包括在line字符串中。

总之,getline函数是处理C++中字符串输入非常有用的函数,尤其是对于一行文本的读取。它简单、灵活并且易于使用。

9.emplace_back用法及和push_back区别

C++11 之后,vector 容器中添加了新的方法:emplace_back() ,和 push_back() 一样的是都是在容器末尾添加一个新的元素进去,不同的是 emplace_back() 在效率上相比较于 push_back() 有了一定的提升

emplace_back() 函数在原理上比 push_back() 有了一定的改进,包括在内存优化方面和运行效率方面。内存优化主要体现在使用了就地构造(直接在容器内构造对象,不用拷贝一个复制品再使用)+强制类型转换的方法来实现,在运行效率方面,由于省去了拷贝构造过程,因此也有一定的提升。

在C++中,emplace_back是一个非常有用的成员函数,它被定义在诸如std::vectorstd::dequestd::list等容器中。emplace_back的功能是在容器的末尾直接构造一个新元素,与push_back相比,emplace_back可以减少不必要的对象拷贝或移动,提高性能。

基本用法

push_back相比,emplace_back不直接将对象插入容器中,而是接收元素构造器的参数,并在容器末尾就地构造元素。这意味着可以避免临时对象的创建和复制(或移动),从而优化性能。

假设有以下简单结构体:

struct MyClass {
    MyClass(int x, double y) : x(x), y(y) {}
    int x;
    double y;
};

使用emplace_back来在std::vector<MyClass>中插入新元素的例子如下:

std::vector<MyClass> vec;
vec.emplace_back(10, 3.14); // 直接在容器末尾构造MyClass对象

对比使用push_back

vec.push_back(MyClass(10, 3.14)); // 构造临时MyClass对象,然后将它复制(或移动)到容器中
性能考虑

使用emplace_back可以优化性能是因为它减少了对象拷贝或者移动的次数。当调用emplace_back时,传递给emplace_back的参数被直接用于在容器的存储空间中构造对象,而不需要额外的拷贝或移动操作。这一点在处理大型对象或资源管理密集型对象时特别有用。

语义差异

尽管emplace_back在许多场景下都是更高效的选择,但它并不总是替代push_back的最佳选择。根据对象的构造、拷贝和移动语义不同,选择不当可能会导致意想不到的副作用或性能问题。因此,合理选择push_backemplace_back对于编写高效和可读的代码是非常重要的。

结论

在C++中,emplace_back是一个强大的工具,它允许程序员在容器的末尾就地构造元素,从而减少不必要的对象拷贝或移动,优化性能。然而,正确的选择使用push_backemplace_back需要对相关的构造、拷贝和移动语义有深刻的理解。

10.C++智能指针

0背景

使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!

思考:如果我们分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存,这看似是一个 very nice 的方案?

智能指针就是通过这个原理来解决指针自动释放的问题!

  • C++98 提供了 auto_ptr 模板的解决方案

  • C++11 增加unique_ptr、shared_ptr 和weak_ptr

C++智能指针是C++11引入的一种特性,旨在自动管理动态分配的内存,从而避免内存泄漏和减少程序员出错的可能性。智能指针通过封装原始指针,在智能指针对象的生命周期结束时自动删除其所指向的对象。C++提供了三种智能指针:std::unique_ptrstd::shared_ptrstd::weak_ptr

  1. std::unique_ptr

std::unique_ptr是一个独占所有权的智能指针。它拥有其所指向的对象,并且保证同一时间内只有一个unique_ptr指向给定对象。当unique_ptr被销毁(例如超出作用域)时,它所指向的对象也会被自动删除。这种智能指针非常适合管理那些不需要共享所有权的对象。

示例:

std::unique_ptr<int> ptr(new int(42));  
// 当ptr离开作用域时,它所指向的int对象会被自动删除
  1. std::shared_ptr

std::shared_ptr允许多个智能指针共享同一个对象的所有权。每个shared_ptr都持有一个引用计数,当最后一个引用被销毁或重置时,所指向的对象才会被删除。这使得shared_ptr非常适合管理需要被多个所有者共享的对象。

示例:

std::shared_ptr<int> ptr1(new int(42));  
std::shared_ptr<int> ptr2 = ptr1;  // ptr1和ptr2现在共享同一个int对象  
// 当ptr1和ptr2都离开作用域时,它们所指向的int对象才会被删除
  1. std::weak_ptr

std::weak_ptr是对std::shared_ptr的一个补充,它是对一个对象的一种弱引用,不会增加对象的引用计数。weak_ptr可以用来观察一个对象,而不会影响其生命周期。当最后一个shared_ptr被销毁时,即使还有weak_ptr指向该对象,对象也会被删除。weak_ptr主要用于解决shared_ptr之间的循环引用问题。

示例:

std::shared_ptr<int> shared = std::make_shared<int>(42);  
std::weak_ptr<int> weak = shared;  
// 当shared离开作用域时,它所指向的int对象会被删除,即使weak还在

使用智能指针的好处是它们可以自动管理内存,减少了程序员手动管理内存的繁琐工作,并且减少了内存泄漏的风险。然而,程序员仍然需要谨慎使用智能指针,确保正确地传递和复制它们,避免不必要的性能开销和潜在的逻辑错误。

详细见:C++ 智能指针 - 全部用法详解-CSDN博客

11.什么是C++的成员初始化列表?

在C++中,成员初始化列表(Member Initialization List)是构造函数的一部分,它用于初始化类的成员变量。在构造函数的函数体执行之前,成员初始化列表会先执行,以确保对象的所有成员变量在构造函数的其余部分开始执行之前都已经得到了合适的初始化。

成员初始化列表通常写在构造函数的函数体之前,由冒号 : 开始,后面跟着一系列成员变量的初始化表达式,每个表达式之间用逗号 , 分隔。

以下是成员初始化列表的基本语法:

class MyClass {  
public:  
    int myInt;  
    double myDouble;  
    AnotherClass myObject; // 假设AnotherClass是另一个类  
  
    MyClass(int value, double dval, const AnotherClass& obj)  
        : myInt(value),       // 初始化myInt为value  
          myDouble(dval),     // 初始化myDouble为dval  
          myObject(obj)       // 使用obj初始化myObject  
    {  
        // 构造函数的函数体,此时所有成员都已经被初始化  
    }  
};

使用成员初始化列表有几个重要的原因:

  1. 效率:对于某些类型(比如内置类型、引用、常量成员),在构造函数体内部赋值可能会导致未定义行为(如常量成员和引用成员不能在构造函数体内部赋值),或者在性能上不如直接在初始化列表中初始化。
  2. 必须性:对于某些特殊的类类型成员(如没有默认构造函数的类),必须在成员初始化列表中初始化,否则编译器无法生成默认的初始化代码。
  3. 顺序控制:成员变量的初始化顺序是按照它们在类中声明的顺序进行的,而不是按照它们在成员初始化列表中出现的顺序。了解这一点可以避免一些潜在的问题,特别是当成员变量的初始化顺序影响到它们的状态时。
  4. 代码清晰:将初始化代码与构造函数的其余部分分开可以使代码更加清晰和易于维护。

在编写构造函数时,应该优先考虑使用成员初始化列表来初始化成员变量,特别是当成员变量是常量、引用或者没有默认构造函数的类类型时。如果成员变量有默认构造函数,那么也可以在构造函数体内部进行赋值,尽管使用成员初始化列表通常是一个更好的做法。

12.什么是C++中的移动语义和右值引用

首先c++是值语义嘛?–一般情况下是外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

在 C++ 中,移动语义(Move Semantics)和右值引用(Rvalue Reference)是 C++11 引入的两个重要概念,它们主要用于优化资源管理和提高程序效率。

1. 右值引用 (Rvalue Reference)

右值引用是一种新的引用类型,它可以绑定到临时对象(右值),而不是普通引用(左值引用)只能绑定到持久对象(左值)。右值引用通过 && 符号表示,不同于传统的引用(左值引用)使用的 & 符号。

通过右值引用,我们可以安全地获得对一个即将销毁的对象的引用,从而可以从这个对象中获取资源,而不需要通过复制。这种特性让我们可以设计出更加有效的函数和类,特别是在涉及到大量数据操作和资源所有权转移时。

例如:

void process(std::string&& s) {
    // 在这里,s 是一个右值引用,我们可以取得 s 所拥有的字符串,而不需要复制
    std::cout << "Processing: " << s << std::endl;
}

std::string getName() {
    return "Alice";
}

int main() {
    std::string name = getName(); // getName 返回一个临时字符串,该临时对象被移至 name 中
    process(std::move(name)); // std::move 将 name 转换为右值引用
}
2. 移动语义 (Move Semantics)

移动语义允许我们在某些情况下“移动”资源,而不是复制资源。这主要是通过特殊的构造函数和赋值操作实现的,即移动构造函数和移动赋值操作符,它们接收一个右值引用参数。

使用移动语义时,资源(如动态内存、文件句柄、网络连接等)可以从一个对象转移到另一个对象,这通常仅涉及指针或句柄的重新赋值,而无需实际复制底层数据。这样可以显著减少程序的内存使用和提升性能。

例如,在 std::vector 中使用移动语义:

std::vector<std::string> v;
v.push_back("Hello"); // "Hello" 被复制到 vector 中
v.push_back(getName()); // "getName" 返回的临时字符串被移动到 vector 中

std::vector<std::string> v2 = std::move(v); // v 的内容被移动到 v2,v 现在为空
总结
  • 值引用和移动语义是 C++11 中引入的功能,主要用于提高应用程序的效率。

  • 右值引用允许函数或类方法直接操作临时对象;

  • 移动语义允许在对象之间传递重资源的所有权,避免不必要的复制。

  • 这两者通常一起使用,提供了一种强大的方法来优化使用大量数据和资源的 C++ 程序。

13.返回一个栈上对象的引用或者指针都是错误的???

image-20240416151153884 image-20240416152223557

14.C++中less函数对象

在C++中,less 是一个函数对象(也称为仿函数或functor),它定义了一个严格弱序的比较关系,通常用于比较两个对象以确定它们之间的顺序。less 是标准库 <functional> 中的一个模板类,可以用于各种数据类型。

默认情况下,less 使用 < 运算符来比较两个对象。当我们将 less 对象用作比较函数时,它会按照 < 运算符的规则来确定两个对象的大小关系。

这里有一些关于 less 的基本用法:

1. 包含头文件

首先,你需要包含 <functional> 头文件来使用 less

#include <functional>
2. 使用 less 对象

你可以创建一个 less 对象,并将其用作算法(如 sort)的比较函数。

#include <iostream>  
#include <vector>  
#include <algorithm>  
#include <functional>  
  
int main() {  
    std::vector<int> vec = {5, 2, 9, 1, 5, 6};  
      
    // 使用 std::less<int>() 作为比较函数对 vector 进行排序  
    std::sort(vec.begin(), vec.end(), std::less<int>());  
      
    // 输出排序后的 vector  
    for (int num : vec) {  
        std::cout << num << ' ';  
    }  
      
    return 0;  
}

在这个例子中,std::sort 使用 std::less<int>() 对象来确定 vector 中元素的顺序。std::less<int>() 对象内部使用 int 类型的 < 运算符来比较元素。

3. 使用 less 作为默认比较函数

很多标准库算法(如 sortsetmap 等)都接受一个比较函数作为参数。如果不提供比较函数,这些算法会使用默认的 less 对象作为比较函数。因此,在很多情况下,你不需要显式地创建 less 对象。

#include <iostream>  
#include <vector>  
#include <algorithm>  
  
int main() {  
    std::vector<int> vec = {5, 2, 9, 1, 5, 6};  
      
    // 使用默认的 less 对象对 vector 进行排序(不显式提供比较函数)  
    std::sort(vec.begin(), vec.end());  
      
    // 输出排序后的 vector  
    for (int num : vec) {  
        std::cout << num << ' ';  
    }  
      
    return 0;  
}

在这个例子中,我们没有显式地提供比较函数给 std::sort,所以它使用了默认的 less 对象。

4. 自定义类型与 less

当涉及到自定义类型时,less 函数对象的行为依赖于该类型是否重载了 < 运算符。在C++中,std::less 是一个模板类,它默认使用 < 运算符来比较两个对象。如果你有一个自定义类型,并且你希望这个类型能够使用 std::less 进行比较,那么你需要为你的类型重载 < 运算符。

重载 < 运算符允许你定义两个对象之间的比较规则。这通常涉及到比较对象的成员变量,以确定它们之间的顺序关系。

下面是一个自定义类型重载 < 运算符的示例:

#include <iostream>  
#include <functional>  
  
// 自定义类型 Person  
class Person {  
public:  
    std::string name;  
    int age;  
  
    Person(const std::string& name, int age) : name(name), age(age) {}  
  
    // 重载 < 运算符,以便 Person 对象可以使用 std::less 进行比较  
    bool operator<(const Person& other) const {  
        // 这里假设先按姓名比较,如果姓名相同则按年龄比较  
        if (this->name == other.name) {  
            return this->age < other.age;  
        }  
        return this->name < other.name;  
    }  
};  
  
int main() {  
    Person person1("Alice", 25);  
    Person person2("Bob", 20);  
    Person person3("Alice", 22);  
  
    // 使用 std::less 比较 Person 对象  
    std::less<Person> lessObj;  
    bool isPerson1LessThanPerson2 = lessObj(person1, person2); // 比较姓名  
    bool isPerson1LessThanPerson3 = lessObj(person1, person3); // 比较姓名和年龄  
  
    std::cout << "Is person1 < person2? " << std::boolalpha << isPerson1LessThanPerson2 << std::endl;  
    std::cout << "Is person1 < person3? " << std::boolalpha << isPerson1LessThanPerson3 << std::endl;  
  
    return 0;  
}

在这个示例中,我们定义了一个名为 Person 的自定义类型,它有两个成员变量:nameage。我们重载了 < 运算符,以便能够比较两个 Person 对象。在重载的 < 运算符中,我们首先比较 name,如果 name 相同,则比较 age

然后,在 main 函数中,我们创建了几个 Person 对象,并使用 std::less<Person> 对象来比较它们。std::less<Person> 对象内部使用我们重载的 < 运算符来执行比较。

通过这种方式,你可以为自定义类型提供比较逻辑,使得它们能够与 std::less 以及其他依赖于比较的算法和标准库容器一起使用。

需要注意的是,如果你的自定义类型没有重载 < 运算符,而你又试图使用 std::less 对其进行比较,那么编译器会报错,因为它不知道如何比较两个该类型的对象。因此,确保为自定义类型重载 < 运算符是实现与 std::less 兼容的关键步骤。

15.统一命名规范

image-20240416191130629

16.C++中explicit关键字

在C++中,explicit关键字用于防止类构造函数被不期望地隐式调用。当你在构造函数前加上explicit关键字时,编译器就不会在需要隐式转换的情况下自动调用该构造函数。这主要用于单参数构造函数,以避免不必要的、可能产生混淆的隐式类型转换。

下面是一个简单的例子来说明explicit关键字的作用:

class Foo {  
public:  
    Foo(int x) { // 单参数构造函数  
        // ...  
    }  
};  
  
void someFunction(Foo f) {  
    // ...  
}  
  
int main() {  
    someFunction(42); // 这是合法的,因为编译器会隐式地调用 Foo(int x) 构造函数  
    return 0;  
}

在上面的代码中,尽管someFunction期望一个Foo类型的参数,但我们可以直接传递一个整数42。这是因为编译器会隐式地调用Foo(int x)构造函数来创建一个临时的Foo对象。

然而,如果我们不想让这种隐式转换发生,我们可以将构造函数声明为explicit

class Foo {  
public:  
    explicit Foo(int x) { // 使用 explicit 关键字  
        // ...  
    }  
};  
  
void someFunction(Foo f) {  
    // ...  
}  
  
int main() {  
    someFunction(42); // 这是不合法的,因为编译器不会隐式地调用 Foo(int x) 构造函数  
    return 0;  
}

现在,尝试直接传递整数42给期望Foo类型参数的someFunction函数会导致编译错误。要修复这个错误,你需要显式地创建一个Foo对象:

int main() {  
    Foo f(42); // 显式地创建一个 Foo 对象  
    someFunction(f); // 现在这是合法的  
    return 0;  
}

总的来说,explicit关键字是一个有用的工具,可以帮助你避免不期望的隐式类型转换,从而提高代码的可读性和安全性。

单参数构造函数一般在构造函数前面加上explicit关键字!

进一步,哪些场景下要用到noexcept呢?

在C++中,使用noexcept关键字来标记函数(包括构造函数)不会抛出异常,在多个场景下都是有益的。下面是一些常见的情况,其中构造函数可能需要或应该使用noexcept关键字:(核心是移动构造函数和移动赋值操作符)

  1. 移动构造函数:移动构造函数通常不会抛出异常,因为它只是在内部转移资源而不是执行可能导致失败的分配或复制操作。通过标记移动构造函数为noexcept,可以确保在移动操作失败时不会抛出异常,这有助于提升性能,并允许标准库容器等更高效地工作。

    class MyClass {  
    public:  
        MyClass(MyClass&& other) noexcept : data(std::move(other.data)) {  
            // 移动构造函数体  
        }  
    private:  
        SomeResourceType data;  
    };
    
  2. 移动赋值操作符:移动赋值操作符(Move Assignment Operator)是C++中的一种特殊赋值运算符,用于将一个对象的资源“移动”到另一个对象,而不是进行传统的深拷贝。通过移动赋值,我们可以避免不必要的资源复制,从而提高程序的效率。移动赋值操作符通常用于支持移动语义,这在处理那些拥有动态分配资源(如指针、动态数组、文件句柄等)的类时特别有用。具体见下面例子:

    class MyClass {  
    public:  
        MyClass& operator=(MyClass&& other) noexcept {  
            if (this != &other) {  
                // 移动赋值操作的实现  
                data = std::move(other.data);  
                // 可能还需要处理其他成员变量的移动赋值  
            }  
            return *this;  
        }  
      
    private:  
        SomeResourceType data;  
        // 其他成员变量...  
    };
    
  3. 交换函数:交换两个对象的状态的swap函数或成员函数通常也不会抛出异常,因为它们只是交换内部状态。将它们标记为noexcept可以允许更高效的异常安全代码。

void swap(MyClass& a, MyClass& b) noexcept {  
    using std::swap;  
    swap(a.data, b.data);  
}
  1. 性能关键代码:在性能关键的应用中,避免异常处理机制的开销很重要。如果构造函数不会抛出异常,并且这种保证对性能至关重要,那么使用noexcept可以确保编译器不会为该构造函数生成额外的异常处理代码。

  2. 析构函数:尽管析构函数通常不应该抛出异常(因为这会导致未定义行为),但使用noexcept关键字显式声明它们不会抛出异常也是有益的。这有助于开发者意识到不应该在析构函数中抛出异常,并且可以在某些情况下提供编译时检查。

class MyClass {  
public:  
    ~MyClass() noexcept {  
        // 析构函数体  
    }  
};
  1. 保证函数的异常安全性:在编写异常安全的代码时,了解哪些函数可能抛出异常以及哪些函数不会抛出异常是很重要的。使用noexcept关键字可以帮助维护者理解函数的异常行为,并在设计异常安全接口时做出更明智的决策。

需要注意的是,虽然noexcept提供了优化和代码清晰度的优势,但它也增加了额外的责任。开发者必须确保标记为noexcept的函数实际上不会抛出任何异常,否则程序将调用std::terminate()并终止执行。因此,在添加noexcept关键字之前,应该仔细考虑并测试函数的异常行为。

17.const关键词和constexpr用法

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

18.C++虚函数用法

在C++中,虚函数(Virtual Function)是面向对象编程中的一个重要概念,主要用于实现动态多态性(Dynamic Polymorphism)。通过虚函数,基类可以定义一个函数接口,而派生类可以提供自己的实现。当通过基类的指针或引用来调用这个函数时,会执行与指针或引用实际指向的对象类型相对应的派生类版本的函数。

下面是一些关于虚函数的关键点:

  1. 声明: 虚函数在基类中声明,并在基类的定义中使用virtual关键字进行标记。
class Base {  
public:  
    virtual void foo() {  
        // 基类版本的实现  
    }  
};
  1. 重写: 派生类可以重写(Override)基类中的虚函数。重写意味着派生类提供了一个与基类虚函数具有相同名称、参数列表和返回类型的函数。在派生类中,虚函数不需要(但通常应该)再次使用virtual关键字。
class Derived : public Base {  
public:  
    void foo() override {  
        // 派生类版本的实现  
    }  
};

在这里,override关键字是C++11引入的,用于明确表明派生类中的成员函数是重写基类的虚函数。它不是必须的,但使用它可以提高代码的可读性,并在某些情况下提供编译时检查。

  1. 动态绑定: 当使用基类的指针或引用来调用虚函数时,实际调用的是指针或引用所指向对象的类型中的函数版本。这种在运行时确定调用哪个函数版本的行为称为动态绑定或晚期绑定。
Base* basePtr = new Derived();  
basePtr->foo(); // 调用Derived::foo(),因为basePtr实际指向Derived对象
  1. 纯虚函数: 在基类中,可以将虚函数声明为纯虚函数(Pure Virtual Function),这意味着该函数在基类中没有实现,并且任何从该基类派生的类都必须提供该函数的实现。包含至少一个纯虚函数的类称为抽象类(Abstract Class),它不能被实例化。
class AbstractBase {  
public:  
    virtual void foo() = 0; // 纯虚函数声明  
};  
  
class DerivedFromAbstract : public AbstractBase {  
public:  
    void foo() override {  
        // 必须提供实现  
    }  
};
  1. 性能: 使用虚函数通常会有一些性能开销,因为运行时系统需要确定要调用的正确函数版本。这种开销通常通过虚函数表(Virtual Function Table,简称vtable)和虚函数指针(Virtual Function Pointer,简称vptr)来实现。每个包含虚函数的类都有一个vtable,其中存储了指向虚函数的指针。每个类的对象都有一个vptr,指向其类的vtable。当调用虚函数时,通过vptr找到vtable,然后从vtable中找到正确的函数指针来调用。
  2. 非虚函数: 如果一个函数在基类中不是虚函数,那么在派生类中重写的该函数将不会实现动态多态性。通过基类指针或引用调用该函数时,将总是调用基类中的函数版本。

虚函数是C++中实现多态性的关键机制之一,允许程序在运行时根据对象的实际类型来执行不同的操作。这增强了代码的灵活性和可重用性。

19.C++中的trivially copyable是什么?

在C++中,trivially copyable类型是一个重要的概念,它关系到对象的复制、赋值和存储方式。这一概念对于理解如何在C++中高效且安全地处理对象的内存非常关键。

Trivially Copyable 的定义

一个类型如果是trivially copyable的,那么它满足以下几个条件:

  1. 该类型有一个trivial的复制构造函数(copy constructor),或者没有声明复制构造函数而使用编译器生成的默认构造函数。
  2. 该类型有一个trivial的移动构造函数(move constructor),通常是未被声明或使用了默认的。
  3. 该类型有一个trivial的复制赋值运算符(copy assignment operator)和一个trivial的移动赋值运算符(move assignment operator),这些运算符通常也都是未声明或使用默认的。
  4. 该类型没有虚函数和虚基类。
Trivial 构造函数和赋值运算符
  • Trivial 构造函数:如果构造函数没有做任何特别的事情,例如不进行任何成员的初始化操作(成员都自动默认初始化),则构造函数是trivial的。
  • Trivial 赋值运算符:如果赋值运算符只是进行浅拷贝,不涉及到复杂的逻辑(如深拷贝,资源管理等),则赋值运算符是trivial的。
重要性和应用

Trivially copyable的类型在性能优化中很有用,因为:

  • 它们可以用std::memcpy安全地进行内存复制。
  • 它们通常适合用在要求高性能的底层数据传输和存储操作中。

由于这些特性,trivially copyable的类型通常用于与硬件操作相关的底层编程,或在性能敏感的应用程序中处理大量数据复制。

检查类型是否为 Trivially Copyable

在C++11及之后的版本中,你可以使用std::is_trivially_copyable<T>::value来检查一个类型T是否是trivially copyable的。

例如:

#include <type_traits>
#include <iostream>

struct A {
    int x;
};

struct B {
    B(const B& other) { } // 非trivial复制构造函数
};

int main() {
    std::cout << std::boolalpha;
    std::cout << "A is trivially copyable: " << std::is_trivially_copyable<A>::value << std::endl;
    std::cout << "B is trivially copyable: " << std::is_trivially_copyable<B>::value << std::endl;
    return 0;
}

上面的代码中,结构体A是trivially copyable的,而结构体B由于声明了非trivial的复制构造函数,所以不是trivially copyable的。

20.C++的memcopy、memmove和memset

在C++中,memsetmemcpy等函数并不是C++标准库的一部分,而是来自C语言的标准库(<cstring><memory.h>),但它们在C++中仍然被广泛使用,特别是在处理原始内存和低级操作时。这些函数为处理内存块提供了高效的底层操作。

1. memset

memset函数用于将内存块设置为特定的值。其原型如下:

cpp复制代码

void *memset(void *s, int c, size_t n);
  • s 是指向要设置的内存块的指针。
  • c 是要设置的值,该值会被转换为unsigned char类型。
  • n 是要设置的字节数。

memset通常用于初始化内存块为某个特定的值,比如将内存清零。

示例:

char buffer[100];  
memset(buffer, 0, sizeof(buffer)); // 将buffer初始化为全0
2. memcpy

memcpy函数用于从源内存块复制n个字节到目标内存块。其原型如下:

cpp复制代码

void *memcpy(void *dest, const void *src, size_t n);
  • dest 是指向目标内存块的指针。
  • src 是指向源内存块的指针。
  • n 是要复制的字节数。

memcpy不会检查源和目标内存块是否重叠,因此在使用时需要小心。

示例:
char source[] = "Hello, world!";  
char destination[50];  
memcpy(destination, source, strlen(source) + 1); // 复制字符串包括终止符
3. memmove

memmove函数与memcpy类似,但它可以处理源和目标内存块重叠的情况。其原型如下:

cpp复制代码

void *memmove(void *dest, const void *src, size_t n);
  • dest 是指向目标内存块的指针。
  • src 是指向源内存块的指针。
  • n 是要复制的字节数。

当源和目标内存块重叠时,应该使用memmove而不是memcpy,因为memcpy在这种情况下可能不会按预期工作。

示例:
char buffer[10] = "abcdefghi";  
memmove(buffer + 2, buffer, 5); // 将"abcde"移动到"cdefghi"的位置

在使用这些函数时,需要确保传入的指针是有效的,并且不要超出目标内存块的边界,否则可能会导致未定义行为,如程序崩溃或数据损坏。这些函数通常用于处理原始数据或自定义数据结构,但在处理C++对象时,应该优先使用C++的复制构造函数、赋值运算符或标准库算法,因为它们能够正确处理对象的构造、复制和销毁。

21.C++的size_t是什么类型

在C++中,size_t 是一个无符号整数类型,通常用于表示对象的大小或数组中的元素数量。它是定义在 <cstddef><stddef.h> 头文件中的类型别名。由于 size_t 是无符号的,因此它通常用于表示非负的值,比如数组的大小或对象的内存大小。

当你使用如 sizeof 运算符或某些库函数(如 strlen)时,它们通常会返回 size_t 类型的值。

例如:

#include <iostream>  
#include <cstddef> // 或 #include <stddef.h>  
  
int main() {  
    int arr[10];  
    std::cout << "Size of array: " << sizeof(arr) / sizeof(arr[0]) << std::endl;  
    std::cout << "Type of sizeof(arr) / sizeof(arr[0]): " << typeid(sizeof(arr) / sizeof(arr[0])).name() << std::endl;  
  
    return 0;  
}

在这个例子中,sizeof(arr) / sizeof(arr[0]) 会返回数组 arr 的元素数量,这个值是一个 size_t 类型的值。

由于 size_t 是无符号的,因此在比较它与其他可能为负的整数类型时要特别小心,以避免可能的溢出或错误的比较结果。

另外,size_t 的具体大小取决于平台和编译器,但通常它足够大,可以表示任何对象或数组的大小。在大多数现代系统上,size_t 通常是64位的,但这不是一个固定的要求。

22.什么是C++的RAII机制?

C++的RAII(Resource Acquisition Is Initialization)机制是一种管理资源、避免资源泄漏的惯用法。它利用C++对象在其生命周期内自动管理资源的特点,确保资源的有效性和安全性。

在RAII机制中,资源(如内存、文件句柄、网络连接等)的获取与对象的构造对应,资源的释放与对象的析构对应。当对象被创建时,它会自动获取所需的资源,并在其整个生命周期内保持对这些资源的控制,确保资源始终有效。当对象不再需要时,其析构函数会自动释放之前获取的资源,从而避免了资源泄漏的问题。

通过使用RAII机制,程序员可以将资源管理的逻辑封装在类的内部,简化了代码并提高了异常安全性。即使发生异常,由于析构函数会在对象生命周期结束时自动调用,资源也能得到正确释放,从而避免了资源泄漏和潜在的程序崩溃。

总之,C++的RAII机制是一种强大的资源管理策略,它通过将资源与对象的生命周期绑定,实现了资源的自动管理和释放,提高了代码的可读性、可维护性和安全性。

23.C++的rbeginrend函数

在C++中,rbegin是一个成员函数,用于返回一个指向容器中最后一个元素的反向迭代器。这意味着,当你通过rbegin遍历容器时,你会从后向前遍历它。这对于某些操作,特别是当你想从后向前处理容器中的元素时,是非常有用的。

例如,考虑一个std::vector<int>

cpp复制代码

std::vector<int> v = {1, 2, 3, 4, 5};

如果你想从后向前遍历这个向量并打印其元素,你可以这样做:

for (auto it = v.rbegin(); it != v.rend(); ++it) {  
    std::cout << *it << ' ';  
}

输出会是:

复制代码

5 4 3 2 1

这里,rbegin返回指向5的反向迭代器,而rend返回一个表示“反向结束”的反向迭代器,它实际上指向容器的“开始”之前的位置。

需要注意的是,反向迭代器在递增时实际上是向后移动的,这与正向迭代器在递增时向前移动是不同的。因此,当你看到++it在上面的循环中时,它实际上是将迭代器向后移动一个位置。

总之,rbeginrend提供了从后向前遍历容器的方法。

24.C++的map.find()函数(一定区分开,这个返回的是迭代器)

在C++的STL中,map.find()函数用于在map容器中查找一个特定键(key)的元素。它返回一个迭代器,指向找到的元素;如果map中不存在该键,则返回map.end()

map.find()的返回值是一个双向迭代器,你可以使用它来访问找到的元素的值。如果map.find()返回的迭代器不等于map.end(),那么该迭代器指向的就是包含指定键的元素。

以下是一个使用map.find()的示例:

#include <iostream>  
#include <map>  
  
int main() {  
    std::map<int, std::string> myMap = {{1, "one"}, {2, "two"}, {3, "three"}};  
  
    int keyToFind = 2;  
    auto it = myMap.find(keyToFind);  
  
    if (it != myMap.end()) {  
        std::cout << "Found key " << keyToFind << " with value " << it->second << std::endl;  
    } else {  
        std::cout << "Key " << keyToFind << " not found in the map." << std::endl;  
    }  
  
    return 0;  
}

在这个例子中,我们尝试在myMap中查找键为2的元素。如果找到了,it将指向这个元素,并且我们可以使用it->second来访问该元素的值。如果没有找到,it将等于myMap.end(),我们据此判断键不在map中。

需要注意的是,如果map中的键是自定义类型,那么必须确保该类型已经定义了适当的比较运算符(通常是operator<),以便map.find()能够正确地比较键。如果比较运算符没有正确定义,那么map.find()可能不会按预期工作。

25.inline关键字的使用

原因:

在 c/c++ 中,为了解决一些频繁调用的小函数大量消耗栈空间(栈内存)的问题,特别的引入了 inline 修饰符,表示为内联函数。栈空间就是指放置程序的局部数据(也就是函数内数据)的内存空间。在系统下,栈空间是有限的,假如频繁大量的使用就会造成因栈空间不足而导致程序出错的问题,如,函数的死循环递归调用的最终结果就是导致栈内存空间枯竭。

#include <stdio.h>
 
inline const char *num_check(int v)
{
    return (v % 2 > 0) ? "奇" : "偶";
}
 
int main(void)
{
    int i;
    for (i = 0; i < 100; i++)
        printf("%02d   %s\n", i, num_check(i));
    return 0;
}

上面的例子就是标准的内联函数的用法,使用 inline 修饰带来的好处我们表面看不出来,其实,在内部的工作就是在每个 for 循环的内部任何调用 *dbtest(i)* 的地方都换成了 *(i%2>0)?“奇”:“偶”*,这样就避免了频繁调用函数对栈内存重复开辟所带来的消耗。

  • inline 函数仅仅是一个对编译器的建议,所以最后能否真正内联,看编译器的意思,它如果认为函数不复杂,能在调用点展开,就会真正内联,并不是说声明了内联就会内联,声明内联只是一个建议而已。

  • 建议 inline 函数的定义放在头文件中

总结:

内联函数并不是一个增强性能的灵丹妙药。只有当函数非常短小的时候它才能得到我们想要的效果;但是,如果函数并不是很短而且在很多地方都被调用的话,那么将会使得可执行体的体积增大。
最令人烦恼的还是当编译器拒绝内联的时候。在老的实现中,结果很不尽人意,虽然在新的实现中有很大的改善,但是仍然还是不那么完善的。一些编译器能够足够的聪明来指出哪些函数可以内联哪些不能,但是大多数编译器就不那么聪明了,因此这就需要我们的经验来判断。如果内联函数不能增强性能,就避免使用它!

26.C++中priority_queue用法

C++中priority_queue用法:

在C++中,我们通常使用标准库中的std::priority_queue来模拟堆(heap)的行为,但std::priority_queue默认实现的是最大堆(max heap)。不过,通过提供自定义的比较器(comparator),我们可以实现最小堆(min heap)。

最大堆(Max Heap)

最大堆是一个完全二叉树,其中每个父节点的值都大于或等于其子节点的值。在C++中,使用std::priority_queue默认就是最大堆,因为std::priority_queue默认会将元素按照从大到小的顺序排列。

cpp复制代码
 #include <queue>  
 #include <functional> // 引入 std::greater 用于最小堆  
   int main() {  
 std::priority_queue<int> maxHeap; // 默认是最大堆  
 maxHeap.push(3);  
 maxHeap.push(1);  
 maxHeap.push(4);  
 // 顶部元素是 4(最大的)  
   // 访问顶部元素(但不删除)  
 int top = maxHeap.top();  
   // 删除顶部元素  
 maxHeap.pop();  
   // ...  
 return 0;  
 } 
最小堆(Min Heap)

最小堆是一个完全二叉树,其中每个父节点的值都小于或等于其子节点的值。为了在C++中实现最小堆,我们需要为std::priority_queue提供一个自定义的比较器,通常使用std::greater。

cpp复制代码
 #include <queue>  
 #include <functional> // 引入 std::greater 用于最小堆  
   int main() {  
 std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap; // 最小堆  
 minHeap.push(3);  
 minHeap.push(1);  
 minHeap.push(4);  
 // 顶部元素是 1(最小的)  
   // 访问顶部元素(但不删除)  
 int top = minHeap.top();  
   // 删除顶部元素  
 minHeap.pop();  
   // ...  
 return 0;  
 } 

在上面的代码中,std::priority_queue<int, std::vector, std::greater>定义了一个最小堆,其中std::vector是底层容器(尽管通常不需要明确指定,除非有特殊需求),而std::greater是比较器,它确保堆按照从小到大的顺序排列。

),我们可以实现最小堆(min heap)。

最大堆(Max Heap)

最大堆是一个完全二叉树,其中每个父节点的值都大于或等于其子节点的值。在C++中,使用std::priority_queue默认就是最大堆,因为std::priority_queue默认会将元素按照从大到小的顺序排列。

cpp复制代码
 #include <queue>  
 #include <functional> // 引入 std::greater 用于最小堆  
   int main() {  
 std::priority_queue<int> maxHeap; // 默认是最大堆  
 maxHeap.push(3);  
 maxHeap.push(1);  
 maxHeap.push(4);  
 // 顶部元素是 4(最大的)  
   // 访问顶部元素(但不删除)  
 int top = maxHeap.top();  
   // 删除顶部元素  
 maxHeap.pop();  
   // ...  
 return 0;  
 } 
最小堆(Min Heap)

最小堆是一个完全二叉树,其中每个父节点的值都小于或等于其子节点的值。为了在C++中实现最小堆,我们需要为std::priority_queue提供一个自定义的比较器,通常使用std::greater。

cpp复制代码
 #include <queue>  
 #include <functional> // 引入 std::greater 用于最小堆  
   int main() {  
 std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap; // 最小堆  
 minHeap.push(3);  
 minHeap.push(1);  
 minHeap.push(4);  
 // 顶部元素是 1(最小的)  
   // 访问顶部元素(但不删除)  
 int top = minHeap.top();  
   // 删除顶部元素  
 minHeap.pop();  
   // ...  
 return 0;  
 } 

在上面的代码中,std::priority_queue<int, std::vector, std::greater>定义了一个最小堆,其中std::vector是底层容器(尽管通常不需要明确指定,除非有特殊需求),而std::greater是比较器,它确保堆按照从小到大的顺序排列。

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心之所向521

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值