首页 > 其他分享 >【知识点】一堆知识点见目录

【知识点】一堆知识点见目录

时间:2024-06-13 15:58:23浏览次数:22  
标签:std 知识点 一堆 函数 int vector const 目录 模板

目录

使⽤distance和advance将容器的const_iterator转换为iterator

在 C++ 中,std::distancestd::advance 是两个标准库算法,可以用来帮助在迭代器之间移动,并在某些情况下将 const_iterator 转换为 iterator

背景知识

  • std::distance:计算两个迭代器之间的距离(即元素个数)。
  • std::advance:将迭代器前进或后退指定的距离。

问题描述

有时候你可能需要将一个 const_iterator 转换为非 constiterator。标准库没有直接的机制来完成这种转换,因为它会破坏 const 的语义。但是你可以通过获取相对位置(使用 std::distance)并应用到非 const 的迭代器(使用 std::advance)来间接实现这个转换。

示例代码

假设我们有一个 std::vector,我们将展示如何从 const_iterator 获取位置,然后将该位置应用到 iterator 中。

#include <iostream>
#include <vector>
#include <iterator>

int main() {
    std::vector<int> vec = {10, 20, 30, 40, 50};

    // 获取 const_iterator
    std::vector<int>::const_iterator const_it = vec.cbegin() + 2; // 指向元素 30

    // 计算 const_iterator 与 vec.cbegin() 之间的距离
    std::vector<int>::difference_type pos = std::distance(vec.cbegin(), const_it);

    // 创建非 const 的 iterator,并将其前进到相同的位置
    std::vector<int>::iterator it = vec.begin();
    std::advance(it, pos);

    // 现在 it 指向与 const_it 相同的位置
    std::cout << "Element pointed to by iterator: " << *it << std::endl; // 输出:30

    // 修改元素的值
    *it = 35;
    std::cout << "Modified vector: ";
    for (int i : vec) {
        std::cout << i << " ";
    }
    std::cout << std::endl;

    return 0;
}

解释

  1. 获取 const_iterator

    • std::vector<int>::const_iterator const_it = vec.cbegin() + 2; 获取指向 vec 中第三个元素(值为 30)的 const_iterator
  2. 计算距离

    • std::distance 用于计算两个迭代器之间的距离(即元素的数量)。
    • std::vector<int>::difference_type pos = std::distance(vec.cbegin(), const_it); 计算 const_itvec.cbegin() 之间的距离,结果是 2。
  3. 前进 iterator

    • std::vector<int>::iterator it = vec.begin(); 创建一个非 constiterator,指向 vec 的起始位置。
    • std::advance(it, pos);it 前进 2 个位置,使其指向与 const_it 相同的位置。
  4. 修改元素的值

    • 现在 it 指向与 const_it 相同的位置,可以通过 *it = 35; 修改该位置的元素值。
  5. 输出结果

    • 打印出修改后的 vector,验证修改是否成功。

总结

通过使用 std::distancestd::advance,可以间接将 const_iterator 的位置转换为 iterator。这种方法维护了 const 的语义,并确保你不违反 C++ 标准中的规则。这种技术适用于所有标准容器,包括 std::vectorstd::liststd::deque 等。

使用 swap 技巧删除 std::vector 中的元素是一种高效的内存管理方法,它可以减少内存碎片并优化性能。然而,你提到的示例代码 vector<C> cs.swap(cs);string s; string (s).swap(s); 语法上不正确并且存在误解。下面我们详细讨论正确的用法。

正确使用 swap 函数来清理容器

std::vector::swap 的用法

std::vector::swap 是一种可以高效清理 std::vector 的方法。swap 函数将两个容器的内容进行交换。在你想要清空一个 std::vector 时,可以利用 swap 函数将其与一个空的 vector 进行交换。

示例代码
#include <iostream>
#include <vector>
#include <string>

int main() {
    // 示例 1: 清理 std::vector
    std::vector<int> cs = {1, 2, 3, 4, 5};
    std::cout << "Original vector size: " << cs.size() << std::endl;

    std::vector<int>().swap(cs); // 与空 vector 进行交换
    std::cout << "Vector size after swap: " << cs.size() << std::endl;

    // 示例 2: 清理 std::string
    std::string s = "Hello, World!";
    std::cout << "Original string: " << s << std::endl;

    std::string().swap(s); // 与空 string 进行交换
    std::cout << "String after swap: " << s << std::endl;

    return 0;
}

解释

  1. 清理 std::vector

    • std::vector<int> cs = {1, 2, 3, 4, 5}; 创建了一个包含5个整数的 vector
    • std::vector<int>().swap(cs); 使用一个临时的空 vectorcs 进行交换。这样,cs 变成了一个空的 vector,而原来的数据则被临时 vector 持有并在其作用域结束时被销毁。
    • 这种方法可以有效地清理 vector,并且避免了内存碎片问题。
  2. 清理 std::string

    • std::string s = "Hello, World!"; 创建了一个包含字符串 “Hello, World!” 的 string
    • std::string().swap(s); 使用一个临时的空 strings 进行交换。这样,s 变成了一个空的 string,而原来的数据则被临时 string 持有并在其作用域结束时被销毁。
    • 这种方法可以有效地清理 string,并且避免了内存碎片问题。

总结

使用 swap 函数与一个临时的空容器进行交换,是清理容器的高效方法。这种方法适用于所有支持 swap 的标准容器,如 std::vectorstd::string。它通过交换内容来避免直接清空容器,进而减少内存碎片,提高性能。

  • 正确用法
    std::vector<int>().swap(cs);
    std::string().swap(s);
    

删除某个元素

使用 “swap 技巧” 删除容器中的元素是一种常见的方法,特别是对于 std::vector 这样的顺序容器。这种方法通过将要删除的元素与容器末尾的元素交换,然后删除最后一个元素来实现高效的删除操作。这样可以避免移动大量元素,提升性能。

示例代码

下面是一个使用 “swap 技巧” 从 std::vector 中删除元素的示例:

#include <iostream>
#include <vector>

// 一个用于检查是否需要删除元素的示例函数
bool badvalue(int value) {
    return value % 2 == 0; // 示例条件:删除所有偶数
}

void remove_bad_values(std::vector<int>& vec) {
    for (auto it = vec.begin(); it != vec.end(); ) {
        if (badvalue(*it)) {
            // 将当前元素与最后一个元素交换
            std::swap(*it, vec.back());
            // 删除最后一个元素
            vec.pop_back();
            // 不递增迭代器,因为需要检查交换后的元素
        } else {
            ++it; // 递增迭代器
        }
    }
}

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

    std::cout << "Original vector: ";
    for (int value : vec) {
        std::cout << value << " ";
    }
    std::cout << std::endl;

    remove_bad_values(vec);

    std::cout << "Modified vector: ";
    for (int value : vec) {
        std::cout << value << " ";
    }
    std::cout << std::endl;

    return 0;
}

解释

  1. 删除元素的逻辑

    • 我们定义了一个函数 badvalue,用于判断元素是否需要删除。此处示例条件是删除所有偶数。
  2. 使用 swap 技巧删除元素

    • 遍历 std::vector 中的元素。
    • 如果当前元素满足删除条件,则将当前元素与容器末尾的元素交换,然后删除最后一个元素。
    • 由于交换后当前迭代器位置的新元素需要重新检查,所以不递增迭代器。
    • 如果当前元素不需要删除,则递增迭代器。
  3. 示例输出

    • 输出原始向量的内容。
    • 使用 remove_bad_values 函数删除不需要的元素。
    • 输出修改后的向量内容。

优点

  • 效率高:交换操作的时间复杂度为 O(1),而删除末尾元素的时间复杂度也是 O(1)。这比逐个删除并移动剩余元素的方式效率更高。
  • 简单易懂:逻辑清晰,代码简洁。

总结

使用 “swap 技巧” 删除 std::vector 中的元素是一种高效的方法。通过将要删除的元素与末尾元素交换并删除末尾元素,可以避免移动大量元素,从而提升性能。这种方法适用于顺序容器(如 std::vector),但不适用于关联容器(如 std::mapstd::set)。

什么是纯函数?

在C++编程中,一个谓词函数用于根据某些条件检查或筛选元素。确保谓词是“纯函数”意味着这个函数在调用过程中不产生任何副作用,并且在相同输入下总是返回相同的输出。下面我将详细解释如何实现这一点。

纯函数有以下特性:

  1. 确定性:对于相同的输入,总是返回相同的输出。
  2. 无副作用:不修改全局状态、不改变输入参数、不进行I/O操作(例如打印到控制台或读写文件)等。

如何编写纯函数判别式

例子:检查一个整数是否为偶数

这是一个简单的例子,用于检查整数是否为偶数的判别式。我们将确保它是一个纯函数。

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

// 纯函数:检查一个整数是否为偶数
bool isEven(int value) {
    return value % 2 == 0;
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    // 使用 std::copy_if 复制所有偶数到另一个容器
    std::vector<int> evenNumbers;
    std::copy_if(vec.begin(), vec.end(), std::back_inserter(evenNumbers), isEven);

    // 打印结果
    std::cout << "Even numbers: ";
    for (int num : evenNumbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;

    return 0;
}

解释

  1. 定义纯函数

    • bool isEven(int value) 是一个纯函数,因为它满足所有纯函数的条件。
    • 它的输入参数 value 是一个整数,函数体内没有副作用(如修改全局变量、进行 I/O 操作等),返回值仅依赖于输入参数。
  2. 使用纯函数

    • main 函数中,我们使用标准算法 std::copy_if 过滤 std::vector 中的偶数并将其复制到另一个容器 evenNumbers 中。
    • std::copy_if 的最后一个参数是谓词 isEven,用作筛选条件。

更复杂的示例:带有捕获列表的 Lambda 表达式

我们还可以使用 Lambda 表达式作为纯函数判别式,只要它们没有副作用。

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

// 使用 Lambda 表达式作为判别式
void filterEvenNumbers(const std::vector<int>& vec) {
    std::vector<int> evenNumbers;

    // 定义一个纯函数的 Lambda 表达式
    auto isEven = [](int value) {
        return value % 2 == 0;
    };

    std::copy_if(vec.begin(), vec.end(), std::back_inserter(evenNumbers), isEven);

    // 打印结果
    std::cout << "Even numbers: ";
    for (int num : evenNumbers) {
        std::cout << num << " ";
    }
    std::cout << std::endl;
}

int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    filterEvenNumbers(vec);
    return 0;
}

解释

  1. 定义 Lambda 表达式

    • auto isEven = [](int value) { return value % 2 == 0; }; 是一个纯函数的 Lambda 表达式。
    • 它没有捕获任何外部变量,函数体内没有副作用,返回值仅依赖于输入参数。
  2. 使用 Lambda 表达式

    • filterEvenNumbers 函数中,我们使用 Lambda 表达式 isEven 作为谓词,过滤并输出偶数。

确保纯函数的要点

  • 参数传递:确保函数参数是值传递或 const 引用传递,这样可以避免函数修改输入参数。
  • 避免全局状态依赖:函数不应依赖或修改全局变量或静态变量。
  • 避免副作用:函数内部不进行任何 I/O 操作、不修改外部状态。

为什么通过函数指针调用通常不会被内联?

内联(inline) 是一种编译器优化技术,其中函数调用被替换为函数体本身,从而减少函数调用的开销。但是,编译器是否选择内联一个函数调用,取决于多种因素,包括函数定义、调用方式以及编译器的优化策略。

1. 函数指针的调用方式

通过函数指针进行调用时,实际调用的函数在编译时可能是不确定的。这种不确定性使得编译器难以在编译时展开函数调用。函数指针可能在运行时被赋予不同的值,指向不同的函数。

#include <iostream>

void foo() {
    std::cout << "foo() called" << std::endl;
}

void bar() {
    std::cout << "bar() called" << std::endl;
}

int main() {
    void (*funcPtr)() = foo; // 指向 foo 的函数指针

    // 通过函数指针调用
    funcPtr(); // 在这里编译器不知道 funcPtr 指向哪个函数

    funcPtr = bar; // 现在指向 bar
    funcPtr(); // 可能是不同的函数

    return 0;
}

在上述例子中,funcPtr 是一个函数指针,它可以指向 foobar。编译器在编译时无法确定 funcPtr 在每个调用时到底指向哪个函数,因此无法进行内联优化。

2. 编译器的内联决策

编译器会根据函数的定义和调用方式来决定是否进行内联优化。对于普通的函数调用,如果函数体较小且被频繁调用,编译器更倾向于内联该函数,以减少函数调用的开销。但是,对于通过函数指针的调用,由于目标函数在编译时不确定,编译器通常不会进行内联。

如何内联函数调用?

如果希望函数能够被内联,通常应避免通过函数指针进行调用,而是直接调用函数或使用编译时能够确定的函数调用方式。

直接函数调用的内联示例
#include <iostream>

inline void foo() {
    std::cout << "foo() called" << std::endl;
}

void bar() {
    foo(); // 直接调用,编译器可以内联
}

int main() {
    foo(); // 直接调用,编译器可以内联
    bar();
    return 0;
}

在这个示例中,foo 函数被声明为 inline,编译器可以选择在 bar 函数和 main 函数中直接内联 foo 函数的调用。

使用现代编译器的优化选项

现代编译器提供了更多的优化选项和指示符,可以帮助编译器更好地进行内联优化。例如,GCC 和 Clang 编译器提供了 __attribute__((always_inline)) 选项来强制内联:

#include <iostream>

__attribute__((always_inline)) inline void foo() {
    std::cout << "foo() called" << std::endl;
}

int main() {
    foo(); // 强制内联
    return 0;
}

总结

  • 通过函数指针进行调用:编译器通常不会对通过函数指针进行的调用进行内联优化,因为函数指针的目标在编译时不确定。
  • 直接调用:如果希望函数能够被内联,应直接调用函数或使用编译时能够确定的函数调用方式。
  • 编译器优化选项:使用现代编译器的优化选项(如 __attribute__((always_inline)))可以强制编译器内联某些函数调用。

NVI 设计模式

在面向对象编程中,Non-Virtual Interface(NVI)设计模式是一种常见的模式。它通过使用公共的非虚成员函数来包裹较低访问性(private/protected)的虚函数,从而控制派生类对虚函数的重载行为。这样做可以确保某些操作在子类重载的函数之前或之后执行,增强类的健壮性和可维护性。

在 NVI 设计模式中,基类的公共接口由非虚函数组成,这些非虚函数调用受保护或私有的虚函数。派生类可以重载这些受保护或私有的虚函数,但不能直接重载公共的非虚函数。

示例

假设我们有一个基类 Base,它有一个公共的非虚函数 publicMethod,这个函数调用一个受保护的虚函数 protectedMethod。派生类 Derived 可以重载 protectedMethod 来提供具体实现。

#include <iostream>

class Base {
public:
    // 公共的非虚成员函数
    void publicMethod() {
        // 在调用受保护的虚函数之前执行一些操作
        std::cout << "Base::publicMethod: Pre-processing" << std::endl;

        // 调用受保护的虚函数
        protectedMethod();

        // 在调用受保护的虚函数之后执行一些操作
        std::cout << "Base::publicMethod: Post-processing" << std::endl;
    }

protected:
    // 受保护的虚函数
    virtual void protectedMethod() {
        std::cout << "Base::protectedMethod: Default implementation" << std::endl;
    }
};

class Derived : public Base {
protected:
    // 重载受保护的虚函数
    void protectedMethod() override {
        std::cout << "Derived::protectedMethod: Custom implementation" << std::endl;
    }
};

int main() {
    Derived d;
    d.publicMethod();

    return 0;
}

解释

  1. Base 类

    • publicMethod 是公共的非虚函数,它调用受保护的虚函数 protectedMethod
    • protectedMethod 是受保护的虚函数,可以在派生类中重载。
    • publicMethod 包裹了对 protectedMethod 的调用,并在调用前后执行一些额外的操作。
  2. Derived 类

    • Derived 类重载了基类的 protectedMethod,提供了自定义实现。
    • publicMethod 不能被重载,因为它是非虚函数,这样可以确保 publicMethod 的行为一致。
  3. main 函数

    • main 函数中,创建一个 Derived 类的实例,并调用 publicMethod
    • 输出显示 publicMethod 调用了 protectedMethod 的自定义实现,同时保留了在调用前后执行的额外操作。

运行结果

Base::publicMethod: Pre-processing
Derived::protectedMethod: Custom implementation
Base::publicMethod: Post-processing

优点

  • 控制流程:通过 NVI 设计模式,基类可以控制函数调用的整体流程,在调用子类的重载方法之前和之后执行一些操作。
  • 增强健壮性:基类可以确保某些操作始终执行,从而提高类的健壮性和一致性。
  • 隐藏实现细节:子类只能重载受保护或私有的虚函数,无法直接修改公共接口,保护了类的内部实现细节。

总结

NVI 设计模式是一种强有力的设计模式,通过使用公共的非虚成员函数包裹较低访问性(private/protected)的虚函数,可以有效地控制派生类对虚函数的重载行为,增强类的健壮性和可维护性。这种模式在大型软件系统中尤其有用,可以提高代码的可读性和一致性。

派⽣类需要访问基类保护的成员,或需要重新定义继承来的虚函数,采⽤private继承

在 C++ 中,继承方式有三种:public 继承、protected 继承和 private 继承。不同的继承方式决定了基类成员在派生类中的访问权限。

private 继承

当使用 private 继承时,基类的 publicprotected 成员在派生类中都变成 private 的。private 继承主要用于实现“组合”(composition)关系而不是“是一个”(is-a)关系。

例子:private 继承访问基类保护成员并重新定义虚函数

假设我们有一个基类 Base,它有一个保护成员变量和一个虚函数。派生类 Derived 通过 private 继承访问基类的保护成员并重新定义继承来的虚函数。

#include <iostream>

class Base {
public:
    virtual ~Base() = default;

protected:
    int protectedMember = 42;

    virtual void protectedMethod() const {
        std::cout << "Base::protectedMethod, protectedMember: " << protectedMember << std::endl;
    }
};

class Derived : private Base {
public:
    void callBaseMethod() const {
        // 访问基类的保护成员和方法
        protectedMethod();
        std::cout << "Derived::callBaseMethod, protectedMember: " << protectedMember << std::endl;
    }

    // 重新定义继承来的虚函数
    void protectedMethod() const override {
        std::cout << "Derived::protectedMethod, protectedMember: " << protectedMember << std::endl;
    }
};

int main() {
    Derived d;
    d.callBaseMethod();

    return 0;
}

解释

  1. Base 类

    • protectedMember 是一个保护成员变量。
    • protectedMethod 是一个保护虚函数,输出 protectedMember 的值。
  2. Derived 类

    • Derived 类通过 private 继承 Base 类。
    • callBaseMethod 是一个公共成员函数,用于访问基类的保护成员和方法。
    • Derived 类重载了基类的 protectedMethod
  3. main 函数

    • main 函数中,创建一个 Derived 类的实例,并调用 callBaseMethod

运行结果

Derived::protectedMethod, protectedMember: 42
Derived::callBaseMethod, protectedMember: 42

关键点

  • private 继承

    • 通过 private 继承,基类的 publicprotected 成员在派生类中都变成 private 的。
    • 派生类可以访问基类的保护成员和方法,但它们在派生类的外部不可见。
  • 访问基类的保护成员

    • 派生类通过 private 继承,可以直接访问基类的保护成员和方法。
  • 重定义虚函数

    • 派生类可以重新定义基类的虚函数,实现多态。

什么时候使用 private 继承

private 继承通常用于实现“实现继承”而不是“接口继承”。换句话说,当派生类想要重用基类的实现,但不希望将基类的接口暴露给派生类的用户时,可以使用 private 继承。这种方式有效地隐藏了继承关系,使得派生类用户无法直接访问基类的成员。

总结

  • private 继承将基类的 publicprotected 成员变成 private 的。
  • 派生类可以通过 private 继承访问基类的保护成员,并重定义继承来的虚函数。
  • private 继承适用于需要重用基类实现但不希望暴露继承接口的场景。

为什么需要声明正常的拷贝构造函数和赋值操作符

在 C++ 中,声明成员模板用于泛化 copy 构造或 assignment 操作时,仍然需要声明正常的拷贝构造函数和拷贝赋值操作符。这是因为成员模板不会被编译器视为常规的拷贝构造函数或赋值操作符,编译器在某些情况下仍然会需要使用默认的拷贝构造函数和赋值操作符。

  1. 编译器的行为

    • 编译器会自动生成默认的拷贝构造函数和赋值操作符,但当你声明了自定义的构造函数或赋值操作符后,编译器将不会生成这些默认的函数。
    • 当你只声明了成员模板形式的构造函数或赋值操作符,编译器不会将其视为常规的拷贝构造函数或赋值操作符。这会导致在需要使用默认拷贝操作时,编译器无法找到合适的函数
  2. 类型转换

    • 成员模板可以用于处理不同类型的拷贝构造和赋值操作,但这些模板不会被用于处理相同类型的拷贝构造和赋值。
    • 因此,为了确保类可以正常使用拷贝构造和赋值操作,需要声明相应的常规函数。

示例代码

以下是一个示例,展示了如何声明成员模板用于泛化拷贝构造和赋值操作,并同时声明正常的拷贝构造函数和赋值操作符:

#include <iostream>

class MyClass {
public:
    int value;

    // 默认构造函数
    MyClass() : value(0) {}

    // 常规拷贝构造函数
    MyClass(const MyClass& other) : value(other.value) {
        std::cout << "Copy constructor called" << std::endl;
    }

    // 常规拷贝赋值操作符
    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            value = other.value;
            std::cout << "Copy assignment operator called" << std::endl;
        }
        return *this;
    }

    // 泛化拷贝构造函数模板
    template <typename T>
    MyClass(const T& other) : value(other.value) {
        std::cout << "Template copy constructor called" << std::endl;
    }

    // 泛化拷贝赋值操作符模板
    template <typename T>
    MyClass& operator=(const T& other) {
        value = other.value;
        std::cout << "Template assignment operator called" << std::endl;
        return *this;
    }
};

class OtherClass {
public:
    int value;
    OtherClass(int v) : value(v) {}
};

int main() {
    MyClass a;
    MyClass b(a); // 调用常规拷贝构造函数
    MyClass c;
    c = a; // 调用常规拷贝赋值操作符

    OtherClass other(42);
    MyClass d(other); // 调用模板拷贝构造函数
    MyClass e;
    e = other; // 调用模板赋值操作符

    return 0;
}

解释

  1. 常规拷贝构造函数和赋值操作符

    • MyClass(const MyClass& other) 是常规拷贝构造函数,用于拷贝相同类型的对象。
    • MyClass& operator=(const MyClass& other) 是常规拷贝赋值操作符,用于赋值相同类型的对象。
  2. 泛化拷贝构造函数和赋值操作符模板

    • template <typename T> MyClass(const T& other) 是泛化拷贝构造函数模板,可以用于拷贝不同类型的对象,只要这些对象具有 value 成员。
    • template <typename T> MyClass& operator=(const T& other) 是泛化拷贝赋值操作符模板,可以用于赋值不同类型的对象。
  3. main 函数

    • 创建 MyClass 的实例 ab,并使用常规的拷贝构造函数。
    • 使用常规的拷贝赋值操作符将 a 的值赋给 c
    • 创建 OtherClass 的实例 other,并使用模板拷贝构造函数和模板赋值操作符将其值赋给 de

运行结果

Copy constructor called
Copy assignment operator called
Template copy constructor called
Template assignment operator called

总结

  • 声明常规的拷贝构造函数和赋值操作符即使你有成员模板用于泛化拷贝构造和赋值操作,仍然需要声明常规的拷贝构造函数和赋值操作符,以确保编译器在需要时能够找到合适的函数。
  • 成员模板:用于处理不同类型的对象,但不会覆盖相同类型的拷贝构造和赋值操作。

类模板相关函数支持所有参数的隐式类型转换时,最好将其定义为类模板内部的 friend 函数

在 C++ 中,当你编写一个类模板并希望其相关函数支持所有参数的隐式类型转换时,最好将这些函数定义为类模板内部的 friend 函数。这种方式可以确保友元函数对模板参数的所有可能实例都可见,并且支持隐式类型转换。

为什么使用 friend 函数

  1. 隐式类型转换:将函数定义为模板类的 friend 函数可以确保它们能够访问类的私有和保护成员,并且支持参数的隐式类型转换。
  2. 模板参数的特化:友元函数模板可以与类模板一起实例化,确保它们在模板参数的所有可能实例中都有效。

示例:类模板中的友元函数

假设我们有一个简单的类模板 Box,它存储一个值,并且我们希望支持该值的隐式类型转换。

定义类模板和友元函数
#include <iostream>

template <typename T>
class Box {
public:
    // 构造函数
    Box(const T& value) : value_(value) {}

    // 声明友元函数模板
    template <typename U>
    friend std::ostream& operator<<(std::ostream& os, const Box<U>& box);

    template <typename U>
    friend Box<U> operator+(const Box<U>& lhs, const Box<U>& rhs);

private:
    T value_;
};

// 定义友元函数模板
template <typename U>
std::ostream& operator<<(std::ostream& os, const Box<U>& box) {
    os << box.value_;
    return os;
}

template <typename U>
Box<U> operator+(const Box<U>& lhs, const Box<U>& rhs) {
    return Box<U>(lhs.value_ + rhs.value_);
}

int main() {
    Box<int> intBox1(10);
    Box<int> intBox2(20);
    Box<double> doubleBox(15.5);

    // 支持隐式类型转换
    std::cout << "intBox1: " << intBox1 << std::endl;
    std::cout << "intBox2: " << intBox2 << std::endl;
    std::cout << "doubleBox: " << doubleBox << std::endl;

    Box<int> intBox3 = intBox1 + intBox2;
    std::cout << "intBox3 (intBox1 + intBox2): " << intBox3 << std::endl;

    // 支持隐式类型转换(int 和 double)???
    Box<double> doubleBox2 = intBox1 + doubleBox;
    std::cout << "doubleBox2 (intBox1 + doubleBox): " << doubleBox2 << std::endl;

    return 0;
}

解释

  1. 类模板 Box

    • Box 是一个类模板,存储一个值 value_
    • 构造函数接受一个类型为 T 的值并初始化 value_
  2. 友元函数模板声明

    • operator<<operator+ 被声明为 Box 的友元函数模板。
    • 这些友元函数模板允许隐式类型转换,并能够访问 Box 的私有成员 value_
  3. 友元函数模板定义

    • operator<< 输出 Boxvalue_
    • operator+ 返回一个新的 Box,其值是两个 Boxvalue_ 之和。
  4. main 函数

    • 创建 Box<int>Box<double> 的实例。
    • 输出 Box 的值,演示了 operator<< 的使用。
    • 将两个 Box<int> 相加,演示了 operator+ 的使用。
    • 将一个 Box<int> 和一个 Box<double> 相加,演示了隐式类型转换和 operator+ 的使用。

运行结果

intBox1: 10
intBox2: 20
doubleBox: 15.5
intBox3 (intBox1 + intBox2): 30
doubleBox2 (intBox1 + doubleBox): 25.5

总结

  • 友元函数模板:通过将相关函数声明为类模板的友元函数模板,可以确保这些函数支持所有参数的隐式类型转换。
  • 访问私有成员:友元函数模板可以访问类的私有成员,提供更灵活和强大的功能。
  • 类型转换支持:友元函数模板能够处理不同类型的操作数,实现类型转换和操作符重载。

非类型模板参数

非类型模板参数(Non-Type Template Parameters, NTTPs)是模板参数的一种,它们不是类型,而是具体的值。这些值可以是整型常量、指针、引用、枚举、或者其他支持常量表达式的值。非类型模板参数允许在编译时指定模板的某些属性,从而在一定程度上减少运行时开销,并能在编译时捕获更多的错误。

使用非类型模板参数的优缺点

优点

  • 编译时常量:可以在编译时指定某些属性,减少运行时开销。
  • 优化性能:允许编译器针对不同的参数实例化模板,进行特定优化。
  • 类型安全:模板实例化时,编译器会检查非类型模板参数的类型和范围,提供类型安全。

缺点

  • 代码膨胀对于不同的参数值,模板会生成不同的实例,可能导致代码膨胀
  • 灵活性较低:非类型模板参数必须在编译时确定,因此在一些需要运行时决定的场景中不适用

例子:非类型模板参数

示例 1:使用非类型模板参数定义数组类
#include <iostream>

// 非类型模板参数 N 表示数组的大小
template <typename T, int N>
class Array {
public:
    T data[N]; // 大小为 N 的数组

    // 默认构造函数
    Array() {
        for (int i = 0; i < N; ++i) {
            data[i] = T();
        }
    }

    // 获取数组大小
    constexpr int size() const {
        return N;
    }

    // 重载索引运算符
    T& operator[](int index) {
        return data[index];
    }

    const T& operator[](int index) const {
        return data[index];
    }
};

int main() {
    Array<int, 5> intArray;
    for (int i = 0; i < intArray.size(); ++i) {
        intArray[i] = i * 10;
    }

    for (int i = 0; i < intArray.size(); ++i) {
        std::cout << intArray[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,Array 类模板使用了非类型模板参数 N 来指定数组的大小。这使得数组的大小在编译时就确定下来,从而避免了运行时的动态分配。

示例 2:实现简单的固定尺寸矩阵
#include <iostream>

template <typename T, int Rows, int Cols>
class Matrix {
public:
    T data[Rows][Cols]; // 固定大小的矩阵

    Matrix() {
        for (int i = 0; i < Rows; ++i) {
            for (int j = 0; j < Cols; ++j) {
                data[i][j] = T();
            }
        }
    }

    constexpr int numRows() const { return Rows; }
    constexpr int numCols() const { return Cols; }

    T& operator()(int row, int col) {
        return data[row][col];
    }

    const T& operator()(int row, int col) const {
        return data[row][col];
    }
};

int main() {
    Matrix<int, 3, 3> intMatrix;
    intMatrix(0, 0) = 1;
    intMatrix(1, 1) = 2;
    intMatrix(2, 2) = 3;

    for (int i = 0; i < intMatrix.numRows(); ++i) {
        for (int j = 0; j < intMatrix.numCols(); ++j) {
            std::cout << intMatrix(i, j) << " ";
        }
        std::cout << std::endl;
    }

    return 0;
}

在这个例子中,Matrix 类模板使用了非类型模板参数 RowsCols 来指定矩阵的行数和列数,从而在编译时确定矩阵的大小。

使用非类型模板参数的替代方案

尽管非类型模板参数有其优势,但在某些情况下,可以使用其他方法来替代它们,以避免代码膨胀和增加灵活性。例如,可以使用函数参数或类成员变量来替代非类型模板参数。

替代方案 1:使用函数参数
#include <iostream>
#include <vector>

template <typename T>
class Array {
public:
    Array(int size) : data(size) {}

    int size() const {
        return data.size();
    }

    T& operator[](int index) {
        return data[index];
    }

    const T& operator[](int index) const {
        return data[index];
    }

private:
    std::vector<T> data;
};

int main() {
    Array<int> intArray(5);
    for (int i = 0; i < intArray.size(); ++i) {
        intArray[i] = i * 10;
    }

    for (int i = 0; i < intArray.size(); ++i) {
        std::cout << intArray[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,Array 类的大小由构造函数的参数指定,而不是由模板参数指定,从而避免了代码膨胀。

替代方案 2:使用类成员变量
#include <iostream>

template <typename T>
class Array {
public:
    Array(int size) : size_(size), data(new T[size]) {}

    ~Array() {
        delete[] data;
    }

    int size() const {
        return size_;
    }

    T& operator[](int index) {
        return data[index];
    }

    const T& operator[](int index) const {
        return data[index];
    }

private:
    int size_;
    T* data;
};

int main() {
    Array<int> intArray(5);
    for (int i = 0; i < intArray.size(); ++i) {
        intArray[i] = i * 10;
    }

    for (int i = 0; i < intArray.size(); ++i) {
        std::cout << intArray[i] << " ";
    }
    std::cout << std::endl;

    return 0;
}

在这个例子中,Array 类的大小由成员变量 size_ 指定,而不是由模板参数指定,从而避免了代码膨胀。

总结

  • 非类型模板参数 在编译时确定模板的某些属性,有助于优化性能和提高类型安全性,但可能导致代码膨胀。
  • 替代方案:在某些情况下,可以使用函数参数或类成员变量来替代非类型模板参数,从而增加灵活性并避免代码膨胀。

标签:std,知识点,一堆,函数,int,vector,const,目录,模板
From: https://blog.csdn.net/qq_33673253/article/details/139630767

相关文章

  • 【知识点】std::thread::detach std::lock_guard std::unique_lock
    在C++11中,std::thread提供了并发编程的基础设施,使得我们可以创建和管理线程。std::thread的detach方法是一种常用的线程管理方式,允许线程在后台独立运行,而不必与主线程同步或等待其完成。std::thread::detach方法当你调用std::thread对象的detach方法时,线程将......
  • 【最新鸿蒙应开发】——HarmonyOS沙箱目录
    鸿蒙应用沙箱目录1.应用沙箱概念应用沙箱是一种以安全防护为目的的隔离机制,避免数据受到恶意路径穿越访问。在这种沙箱的保护机制下,应用可见的目录范围即为应用沙箱目录。对于每个应用,系统会在内部存储空间映射出一个专属的应用沙箱目录,它是(“应用文件目录与应用文件路径......
  • 计算机组成原理历年考研真题对应知识点(数制与编码)
    目录2.1数制与编码2.1.1进位计数制及其相互转换【命题追踪——采用二进制编码的原因(2018)】【命题追踪——十进制小数转换为二进制小数(2021、2022)】2.1.2定点数的编码表示【命题追踪——补码的表示范围(2010、2013、2014、2022)】【命题追踪——补码和真值的相互转......
  • 10.C语言for循环和跳出循环的知识点
    C语言for循环、continue和break知识点3.13for循环3.14for的一些用法3.15continue和break的作用3.16嵌套的规律3.17—作业3.13for循环概述和while的对比#include<stdio.h>intmain(){ intdata; //for(条件附初值;判断临界点;条件改变)//判断、执行循......
  • 列出并排序文件系统根目录(/)下各个目录的大小
    du-sh/*|&grep-v"du:"|sort-hrdu:是一个用于估计文件和目录磁盘使用空间的命令。-s:表示总结,只显示每个指定目录的总大小。-h:表示“human-readable”,即以易读的格式(如K,M,G)显示大小。/:这是一个通配符,它匹配根(/)下的所有目录。因此,du-sh/会列出根目录下所......
  • 【Test 66 】 高阶数据结构 二叉搜索树 必会知识点!
    文章目录1.二叉搜索树的概念2.二叉搜索树K模型的代码实现2.1Find()查找的实现2.2Insert()插入的实现2.3InOrder()中序遍历的实现2.4Erase()删除的实现3.二叉搜索树的KV模型4.二叉搜索树的性能分析1.二叉搜索树的概念......
  • 【java问答小知识8】一些Java基础的知识,用于想学习Java的小伙伴们建立一些简单的认知
    Java中的"java.util.IdentityHashMap"如何比较键?回答:"java.util.IdentityHashMap"使用==操作符来比较键,即它比较的是引用身份。Java中的"java.util.EventListener"接口有什么作用?回答:"java.util.EventListener"接口是所有事件监听器接口的基接口,用于定义事件处理方法......
  • windows server 2019 操作步骤和知识点(第一节)
    windowsserver1.1vmwareworkstation作用模拟硬件模拟操作系统步骤安装1模拟硬件文件新建虚拟机典型稍后安装操作系统Mcirosoftwindowswindows10X64win10-1d:/xujiji/win10-12模拟操作系统CD\DVD(SATA)使用ISO映像文件d:\iso\win10..........
  • c语言目录操作
    在shell中我们可以直接输入命令pwd来显示当前的工作目录,在C程序中调用getcwd函数可以获取当前的工作目录。函数声明:char*getcwd(char*buf,size_tsize);需要头文件:#includegetcwd函数把当前工作目录存入buf中,如果目录名超出了参数size长度,函数返回NULL,如果成功,返回buf......
  • tree-cli 生成项目目录
    全局安装插件npminstall-gtree-cli基本使用#查看帮助tree--help#指定目录层级(深度)tree-l2#将结果输出到test.txt文件tree-l2-otest.txt#只输出目录-dtree-l2-otest.txt-d#忽略指定的目录或文件--ignoretreee-l2-otest.txt--ignore'n......