C++手记

总是还没得到就在想着失去


前言

手记


一、register

据说现在已经不推荐且不用了。

(register 是 C++ 中的一个存储类说明符,但其使用在现代 C++ 中已经不推荐,并且在 C++17 及之后的标准中被废弃。以下是关于 register 的详细说明:)

但是由于遇到了,还是记一下
register 是 C 和 C++ 中的一个存储类说明符,用于提示编译器将变量存储在寄存器中,而不是在内存中,以提高访问速度。
这种提示通常用于对性能有较高要求的场合,例如循环中的计数变量。

  • 注意事项:
    提示性质: register 只是一个建议,编译器可以忽略这个建议,特别是寄存器资源有限时。
    不允许获取地址: 对 register 变量不能使用取地址运算符 &,因为寄存器没有内存地址。
    现代 C++ 的情况:

在 C++17 中,register 被正式移除,因为现代编译器已经非常优化,能够自动选择将哪些变量放入寄存器中,而不需要程序员的建议。

register int counter;
这段代码建议编译器将 counter 变量存储在寄存器中。

  • 现在,直接使用 register 会导致编译警告或错误,取决于编译器和编译标准的设置。但是我用vscode还是能跑应该与gcc的版本有关

二、constexpr

constexpr 是 C++ 中的一个关键字,用于定义常量表达式,主要目的是在编译时执行计算,提高性能和代码安全性。

constexpr 是 C++11 引入的关键字,用于指示编译器在编译时对表达式求值,而不是在运行时。
通过 constexpr,可以定义编译时常量、编译时函数,从而减少运行时的计算开销,提高程序的效率。

常量变量: 定义在编译时即可确定的常量。
常量函数: 定义的函数可以在编译时执行,返回一个常量值。
编译时计算: 使用 constexpr 可以使得一些复杂的计算在编译阶段完成,减少运行时的计算负担。

编译时执行: 使用 constexpr 声明的函数和变量,其值在编译时就必须是确定的。

必须是常量: constexpr 函数的参数和返回值都必须是常量表达式。

内联执行: constexpr 函数默认是 inline 的。

返回值要求: constexpr 函数的返回值必须是可以在编译时计算的表达式。

逐渐增强: 在 C++14 及之后,constexpr 函数的功能得到了增强,允许更复杂的逻辑,如条件语句和循环。

三、内联函数(inline)

内联函数的概念:

内联函数是指在编译过程中,编译器将函数的代码直接插入到每个调用点,而不是通过函数调用的方式(即压栈、跳转等)来执行。这种方式可以减少函数调用的开销。

inline 关键字:
使用 inline 关键字可以建议编译器将函数定义为内联函数。如下:

inline int add(int a, int b) {
    return a + b;
}

减少了函数调用的开销,如函数调用栈的建立和销毁。
提高了代码执行效率,因为将代码直接嵌入可以避免多次的跳转和返回。

内联执行有助于 constexpr 函数在编译时被完全展开,确保编译时计算的执行。
内联行为并不会改变 constexpr 函数的主要特性,即在编译时求值的能力,但它也有助于优化运行时性能。

四、size_t

size_t 是 C 和 C++ 中一种用于表示对象大小或计数的无符号整数类型。
size_t 的全称是 size type,意为“尺寸类型”。
它是标准库中定义的一种无符号整数类型,用于表示对象的大小(如数组、字符串、内存块等)或数组的索引。
size_t 被用于表示对象的大小或内存块的尺寸。例如,malloc() 函数返回的指针需要传入一个 size_t 类型的参数,表示要分配的内存字节数。

它是无符号的,因此避免了负数索引的错误,更适合用于数组、字符串等的下标索引。
sizeof 运算符返回值的类型就是 size_t,用于表示内存大小。
size_t 的大小根据编译器和系统架构自动调整,确保可以在不同平台上正确表示对象的大小(例如 32 位系统上为 4 字节,64 位系统上为 8 字节)。

由于 size_t 是无符号类型,它能够避免与负值相关的错误,从而更安全地用于描述大小和计数。
size_t 的大小适配当前系统架构,确保能表示系统允许的最大内存大小,而不像固定宽度的整数类型那样受限。
size_t 是 ANSI C 和 C++ 标准定义的类型,使代码在不同平台上的表现一致且可移植。

五、std::

std:: 是 C++ 标准库的命名空间 std 的标识符,代表 standard(标准)。在 C++ 中,std 命名空间包含了所有 C++ 标准库的类、函数、变量和对象等。使用 std:: 前缀是为了访问这些标准库的功能:类、函数、变量等,确保代码使用的是标准库的实现,而不是用户自定义的同名实现,并且避免与用户自定义的代码发生命名冲突。

六、命名空间

命名空间 (namespace)
命名空间是 C++ 提供的用于组织代码的机制,主要目的是避免命名冲突。例如,不同的库中可能有同名的函数、类或变量,使用命名空间可以将这些名称隔离开来,防止冲突。

七、std::aligned_alloc

这是一个 C++ 标准库函数,用于分配一块指定对齐方式的内存。

void* std::aligned_alloc(std::size_t alignment, std::size_t size);

这段代码的主要目的是分配一段特定对齐方式的内存,而不仅仅是一般的动态内存分配。这种方式常用于需要高效访问内存的应用,如科学计算、图形处理、音视频处理等。

对齐内存能让 CPU 更高效地访问数据,因为对齐的内存块往往符合处理器的缓存线和内存访问优化策略。

所谓缓存线:

  • 处理器缓存 (Cache)
    缓存的作用:缓存是处理器内部的一种高速存储器,用于临时存储经常使用的数据和指令。其目的是减少处理器访问主内存的次数,从而提高程序执行速度。
    层级结构:缓存一般分为多级,如 L1(Level1)、L2、L3 缓存,L1 最靠近处理器核心,速度最快,但容量最小。L2 和 L3 离核心稍远,速度较慢但容量更大。
  • 缓存线 (Cache Line)
    定义:缓存是以“缓存线”为最小存储单位的。缓存线是一块连续的内存区域,大小通常是 32、64、128 字节(不同处理器可能不同)。
    加载机制:当处理器需要读取某个内存地址的数据时,它不会仅加载该单个字节,而是会把整个包含该地址的缓存线加载到缓存中。
    缓存行命中和未命中:
    命中 (Hit):数据已经在缓存中,可以快速访问。
    未命中 (Miss):数据不在缓存中,需要从主内存加载,导致更高的延迟。

回到正题 void* std::aligned_alloc(std::size_t alignment, std::size_t size);
alignment:指定对齐要求(以字节为单位),必须是 2 的幂。对齐方式决定了分配的内存地址是对齐值的整数倍,通常用于提升性能,尤其是在 SIMD 运算或需要严格内存对齐的硬件上。
alignment:指定所需的内存对齐方式,以字节为单位。对齐方式必须是 2 的幂,如 16、32、64 等。
size:要分配的内存大小(以字节为单位),必须是 alignment 的倍数。

八、void*

void* 是 C 和 C++ 中的一种指针类型,称为 通用指针 或 无类型指针。它可以指向任何类型的数据,但本身没有类型信息,因此不能直接用于解引用或指针运算。void* 的主要用途是实现通用性和灵活性,尤其在需要处理不同数据类型的场合,例如内存分配、通用数据结构等。

void* 是一种指针类型,表示指向未知类型的指针。
它不能直接解引用(即不能直接访问指向的数据),因为没有数据类型信息。

  • 通用性:void* 可用于实现与数据类型无关的操作。例如,标准库中的 malloc 函数返回 void*,允许它分配内存而不指定内存块的具体类型。
  • 数据转换:void* 可以通过类型转换(类型强制转换)转换为任何其他类型的指针。这使得它在需要与多种数据类型交互时非常有用。
  • 函数参数:用于函数参数时,void* 可以接受任意类型的指针,常用于通用接口设计,如回调函数的上下文指针。
  • 数据结构:例如,通用链表、哈希表等需要存储不同类型的数据时,常使用 void* 指针来实现多态。

使用 void* 的注意事项:

  • 安全性:由于 void* 没有类型信息,误用可能导致类型不匹配或指针错误,因此在使用时必须确保类型转换正确。
  • 类型转换:在使用 void* 之前,必须将它转换为正确的类型指针(如 int*、float*),否则无法正确操作数据。
  • 性能:void* 的使用不会自动进行类型检查,因此对类型安全性有更高的要求。在进行转换和操作时可能会引入运行时错误。

九、static_cast

是 C++ 提供的四种类型转换操作符之一(其他包括 dynamic_cast、const_cast 和 reinterpret_cast),主要用于在编译时执行类型转换。它提供了一种类型安全的转换方式,用于在相关类型之间进行显式转换。static_cast 可以进行多种类型转换操作,但不包括动态类型检查。

static_cast<new_type>(expression)

new_type:要转换成的目标类型。
expression:要转换的表达式。

十、基类与派生类

  • 基类(Base Class)
    定义: 基类是一个被其他类继承的类。它通常包含一些共同的属性和方法,这些属性和方法可以被派生类共享。
    功能: 基类提供基本的功能和接口,供派生类扩展或重写。
class BaseClassName {
    // 访问修饰符
public:
    // 公有成员,也可以只是变量
    void somePublicFunction();

protected:
    // 保护成员,也可以只是变量
    int someProtectedVariable();

private:
    // 私有成员,也可以只是变量
    void somePrivateFunction();
};

public: 公有成员可以被任何类访问。
protected: 保护成员只能被基类及其派生类访问。
private: 私有成员只能在基类内部访问。

  • 派生类(Derived Class)
    定义: 派生类是从基类继承的类。它可以继承基类的属性和方法,同时也可以添加新的属性和方法或重写基类的方法。
    功能: 派生类可以实现更具体的功能,扩展基类的能力。
class DerivedClass : access_specifier BaseClass {
    // 派生类成员(属性和方法)
};

十一、函数类别

11.1 成员函数:

属于类的一部分,用于操作类的成员变量,是面向对象编程的基础

成员函数是定义在类中的函数,专门用于操作类的对象。它们可以访问和修改类的成员变量,提供对象的行为实现。
成员函数分为普通成员函数、构造函数、析构函数、虚函数、常成员函数和静态成员函数等。

11.2 全局函数:

在类外定义的函数,无法直接访问类的私有成员。
定义在类外的普通函数,不属于任何类。
全局函数可以在整个程序中访问,并且不能直接访问类的私有成员,但是可以间接通过调用存在的公有成员函数来访问
例子:

#include <iostream>

class MyClass {
private:
    int privateValue;  // 私有成员变量,不能直接被全局函数访问

public:
    MyClass(int value) : privateValue(value) {}  // 构造函数,用于初始化私有成员

    // 公有成员函数,用于访问私有成员
    int getValue() const {
        return privateValue;
    }

    // 公有成员函数,用于设置私有成员
    void setValue(int value) {
        privateValue = value;
    }
};

// 全局函数
void displayValue(MyClass obj) {
    // 直接访问私有成员会导致编译错误
    // std::cout << obj.privateValue << std::endl;  // 错误:无法访问类的私有成员

    // 通过公有成员函数间接访问私有成员
    std::cout << "Value: " << obj.getValue() << std::endl;
}

int main() {
    MyClass myObj(10);  // 创建对象,并初始化私有成员
    displayValue(myObj);  // 调用全局函数,输出对象的私有成员值

    myObj.setValue(20);  // 修改私有成员值
    displayValue(myObj);  // 再次调用全局函数,输出修改后的值

    return 0;
}

11.3 构造函数和析构函数:

用于对象的创建和销毁,管理对象的生命周期。

构造函数是类的特殊成员函数,与类名相同,无返回类型。用于在对象创建时初始化对象。
构造对象、分配资源、初始化成员变量。

析构函数是类的特殊成员函数,名称与类名相同但带有波浪号 ~,无参数且无返回值
在对象销毁时自动调用,用于清理资源,释放内存。

11.4 虚函数:

支持多态,通过基类指针或引用调用派生类的重写版本。
虚函数是基类中使用 virtual 关键字声明的成员函数,允许派生类重写,支持多态性
通过基类指针或引用调用派生类的重写版本。

11.5 内联函数:

用于减少小型函数的调用开销。
内联函数(Inline Function)
内联函数使用 inline 关键字定义,提示编译器将函数代码直接插入调用点,以减少函数调用开销。
提高小型、频繁调用函数的执行效率

11.6 常成员函数:

保证函数不会修改类的状态。
常成员函数在函数签名后加 const,表示该函数不会修改对象的成员变量。
保证函数不修改类的状态,确保安全性

11.7 静态成员函数:

不依赖于对象的成员函数,直接通过类名调用。
静态成员函数使用 static 关键字声明,不依赖于对象,可以直接通过类名调用
不访问对象的成员,只能访问静态成员变量。

11.8 友元函数:

可以访问类的私有成员,但不是类的一部分。
友元函数使用 friend 关键字声明,不属于类,但可以访问类的私有成员。
在类外实现功能,同时访问类的私有成员。

11.9 友元函数与虚函数

#include <iostream>

class Base {
private:
    int value;

public:
    Base(int v) : value(v) {}

    // 声明虚函数,允许派生类重写
    virtual void display() {
        std::cout << "Base class display: " << value << std::endl;
    }

    // 声明友元函数
    friend void showFriend(Base& obj);
};

// 实现友元函数,不能参与重写
void showFriend(Base& obj) {
    std::cout << "Friend function accessing Base class: " << obj.value << std::endl;
}

class Derived : public Base {
public:
    Derived(int v) : Base(v) {}

    // 重写基类的虚函数
    void display() override {
        std::cout << "Derived class display" << std::endl;
    }
    
    // 注意:友元函数不能在派生类中重写
    // void showFriend(Derived& obj) { ... } // 这是一个独立的函数,无法作为重写
};

int main() {
    Base* b = new Derived(10);

    // 调用派生类重写的虚函数,表现多态性
    b->display();

    // 调用友元函数,无法表现多态性,也不能重写
    showFriend(*b);

    delete b;
    return 0;
}

虚函数的重写:
display() 是一个虚函数,在基类中声明为虚函数,在派生类 Derived 中被重写。当通过基类指针 b->display() 调用时,程序会根据对象的实际类型调用 Derived 类的版本,实现了多态。

友元函数的调用:
showFriend(Base& obj) 是一个友元函数,定义在类的外部,具有访问 Base 私有成员的权限。
showFriend 不能被派生类重写,因为它不属于 Base 类,也不属于 Derived 类。即使在派生类中定义一个同名函数,它们也是完全独立的函数,不涉及重写关系。

友元函数不能被派生类重写,因为它们不属于类成员,而是类外部的普通函数。
虚函数是类的成员函数,允许通过继承机制被派生类重写,实现多态性。
友元函数的主要作用是访问类的私有和保护成员,而虚函数则是用于支持多态和动态绑定的机制,两者在概念和用途上有本质区别。

十二、成员初始化列表

: privateData(p1), protectedData(p2) 是成员初始化列表。它在构造函数的函数体 {} 之前,用来初始化类的成员变量。
这种初始化方式直接在成员初始化列表中进行赋值,而不是在构造函数的函数体内赋值。
成员初始化列表的语法是 : member1(value1), member2(value2), …,用于直接初始化类成员。
privateData(p1) 表示用参数 p1 初始化成员变量 privateData。
protectedData(p2) 表示用参数 p2 初始化成员变量 protectedData。
例子:

#include <iostream>

class Base {
private:
    int privateData;       // 私有成员变量
protected:
    int protectedData;     // 保护成员变量

public:
    // 构造函数,使用成员初始化列表初始化成员变量
    Base(int p1, int p2) : privateData(p1), protectedData(p2) {
        std::cout << "Base constructor called." << std::endl;
    }

    // 显示成员变量的值
    void show() {
        std::cout << "Private data: " << privateData << std::endl;
        std::cout << "Protected data: " << protectedData << std::endl;
    }
};

int main() {
    // 创建 Base 类的对象,初始化成员变量
    Base obj(10, 20);
    obj.show();  // 输出成员变量的值
    return 0;
}

十三、列表初始化

优点:
1.防止窄化转换:使用 {} 可以防止类型转换时的潜在数据丢失。例如,将 double 初始化为 int 时,如果使用 () 可能会丢失精度,而使用 {} 会导致编译错误。
2.统一语法:对于所有类型(基本类型、结构体、类等),使用相同的语法。
3.初始化复杂类型:可以直接使用花括号初始化数组、结构体和类对象。

#include <iostream>

int main() {
    // 列表初始化整数
    int a{5};
    double b{3.14};

    std::cout << "a: " << a << ", b: " << b << std::endl;
    return 0;
}

十四、const的限制范围

int *const constPtr = &value;
const int * constPtr = &value;

举个例子,int *const constPtr = &value;和const int * constPtr = &value;
前者是const对constPtr进行限制,即指针不能被改变,而指针所指向的int可以改变
后者是const对int进行限制,即int不能被改变,而指针的值可以改变

例子:

#include <iostream>

int main() {
    int value1 = 10; // 定义一个整数
    int value2 = 20; // 定义另一个整数

    // 常量指针,指向常量整数
    int *const constPtr = &value1; 
    std::cout << "Initial constPtr value: " << *constPtr << std::endl; // 输出 10
    // constPtr = &value2; // 错误:不能改变常量指针的指向

    // 指向常量的指针
    const int *ptr = &value1; 
    std::cout << "Initial ptr value: " << *ptr << std::endl; // 输出 10

    // 可以改变指针的指向
    ptr = &value2; // 正确:可以改变指针的指向
    std::cout << "New ptr value: " << *ptr << std::endl; // 输出 20

    return 0;
}

注意:
尽管int *const constPtr = &value;和const int * constPtr = &value;不一样
但是const double 和 double const 是等价的

十五、引用和指针

Type& 和 Type*
前者是引用,后者是指针

  1. 特点
    Type&(引用)
    别名:引用是某个变量的别名。创建引用后,引用和原变量指向相同的内存地址。
    不可为空:引用在初始化时必须绑定到一个有效的对象,不能为 nullptr。
    语法简洁:使用引用时,不需要使用解引用运算符 *,可以直接使用引用名。
    不可重新绑定:一旦引用绑定到一个变量,就不能再绑定到其他变量。
    Type *(指针)
    地址存储:指针存储的是一个内存地址,可以指向任意类型的对象。
    可以为空:指针可以被赋值为 nullptr,表示不指向任何对象。
    动态性:指针可以在运行时改变指向,可以指向不同的对象。
    使用解引用:必须使用解引用运算符 * 来访问指针指向的值。
  2. 作用
    Type&(引用)
    简化代码:引用用于简化函数参数传递,避免复制大型对象。
    提供可变性:通过引用可以直接修改原始变量,适用于需要修改多个参数的函数。
    增强可读性:引用使代码更易读,因为它们看起来像普通变量。
    Type*(指针)
    动态内存管理:指针用于动态分配和管理内存(例如使用 new 和 delete)。
    数据结构:常用于实现链表、树等数据结构。
    多态性:指针可以用于实现多态,尤其在类层次结构中。
  3. 差别和联系
    差别
    初始化:引用必须在声明时初始化,而指针可以在之后赋值。
    内存管理:指针可以指向 nullptr,而引用必须绑定到有效的对象。
    灵活性:指针更灵活,可以在运行时改变指向,而引用一旦绑定就不能改变。
    联系
    引用可被视为指针的特殊情况:引用在底层实现上可以看作是常量指针,但语法和使用上更安全和简洁。
    相互转换:在某些情况下,可以将引用作为指针使用,例如通过取地址运算符 & 获取引用的地址。

引用相对于指针的优势:

  1. 不需要解引用:
    使用引用时,可以直接通过变量名访问值,而使用指针时需要使用解引用运算符 *。这使得代码更加简洁易读。
void func(int& r) { r = 20; } // 引用
void func(int* p) { *p = 20; } // 指针
  1. 安全性
    必需初始化:引用在声明时必须初始化,不能为空。这减少了使用未初始化指针的风险。

  2. 不可重新绑定
    保持一致性:引用一旦绑定到一个变量,就不能重新绑定到其他变量。这可以确保函数内部始终操作同一个对象,增强了代码的可维护性。

  3. 避免空指针
    无空指针问题:引用不能是 nullptr,因此在使用引用时不必担心空指针引发的错误。

  4. 更接近原始对象
    更自然的语义:引用提供了一种更自然的方式来表示“我想直接操作这个对象”,而指针则通常暗示需要更多的管理,比如内存分配和释放。

指针相对于引用的优势:

  1. 动态性
    可以重新指向:指针可以在运行时改变指向的对象。这使得指针在需要动态管理对象时非常灵活。

  2. 可以为空
    支持空值:指针可以被设置为 nullptr,表示不指向任何对象。这在某些情况下可以用来表示“无值”或“未初始化”。

  3. 动态内存管理
    动态分配内存:指针支持动态内存分配(如使用 new 和 delete),这在处理不确定大小的数据结构(如链表、树等)时非常重要。

  4. 适用于多态
    多态性:指针可以用于实现多态,尤其是在类层次结构中。通过基类指针可以指向派生类对象。

  5. 数组与指针
    数组的灵活性:指针可以直接与数组结合使用,允许对数组进行迭代和访问。这在处理数组时提供了额外的灵活性。

十六、使用std::而不用using namespace std;省去

  1. 命名冲突
    避免冲突:如果在代码中使用了其他库或自定义类,可能会与 std 命名空间中的标识符发生冲突。使用 using namespace std; 后,可能会导致不明确的引用,增加调试难度。
  2. 代码可读性
    明确性:使用 std:: 前缀可以明确表明该标识符来自标准库,使代码更易于理解。读者可以一眼看出哪些是标准库的功能,哪些是自定义的。
  3. 范围污染
    减少全局污染:在大型文件中,使用 using namespace 会将所有的 std 名称引入当前作用域,增加了名称的复杂性,可能会使得代码的维护变得困难。
  4. 编程习惯
    符合最佳实践:许多编程规范和最佳实践推荐开发者在使用标准库时保持 std:: 前缀,以提高代码的清晰性和可维护性。

十七、const和static

  1. static
    作用域:
    在全局范围内:使变量的链接性变为内部链接,意味着该变量只能在定义它的文件中可见。
    在函数内部:使变量的生命周期延续到程序结束,即使函数已经返回,变量的值也会保留。
    用途:
    用于限制变量的可见性,防止命名冲突。
    用于在函数中保持状态或计数。

  2. const
    作用域:
    表示变量的值在初始化后不可更改。
    可以与其他存储类型结合使用(如 const int、const static int)。
    用途:
    提高代码的安全性,避免意外修改变量的值。
    可用于函数参数,以确保在函数内不修改传入的值。

为什么要做区分:
不同的目的:
static 主要控制存储的生命周期和变量的可见性。
const 主要确保值的不可修改性。
代码安全性:
使用 const 可以防止意外修改,减少错误。
使用 static 可以管理状态和作用域,防止变量在不同文件或函数间互相干扰。
可读性和维护性:
明确区分这两个关键字可以提高代码的可读性,使其他开发者更容易理解变量的作用和生命周期。

static 变量的调用和访问方式:

  1. 全局 static 变量
    定义: 在文件的顶部定义为 static。
    访问: 该变量只能在定义它的源文件内部访问,不能被其他文件直接调用。
  2. 函数内部 static 变量
    定义: 在函数内部定义为 static。
    访问: 该变量只在函数内部可用,且其值在函数调用之间保持不变。
  3. 类中的 static 成员
    定义: 在类中定义为 static 的成员变量或成员函数。
    访问: 类的 static 成员可以通过类名直接调用,不需要实例化对象。
    总结
    全局和函数内部的 static: 只能在定义它们的作用域内访问,不能被外部直接调用。
    类中的 static: 可以通过类名直接调用。

const 变量的调用和访问方式:

  1. 全局 const 变量
    定义: 在文件的顶部定义为 const。
    访问: 该变量可以在定义它的文件内被访问,且可以被其他文件访问(如果使用 extern 声明)。
  2. 函数内部 const 变量
    定义: 在函数内部定义为 const。
    访问: 该变量只能在定义它的函数内部访问。
  3. 类中的 const 成员
    定义: 在类中定义为 const 的成员变量。
    访问: const 成员变量必须在构造函数初始化列表中初始化,且不能在类的成员函数中被修改。
  4. const 作为函数参数
    定义: 在函数参数中定义为 const。
    访问: const 参数确保在函数体内不修改传入的值。
    总结
    全局和局部 const: 可以在定义它们的作用域内访问,且不能被修改。
    类中的 const: 必须通过构造函数初始化,且在类的成员函数中不可修改。
    作为函数参数的 const: 确保在函数内不修改传入的值。

十八、函数引用和函数指针

举例
函数引用:

#include <iostream>

// 函数声明,使用引用作为参数
void increment(int& value) {
    value++; // 增加传入的值
}

int main() {
    int num = 10; // 定义一个整数变量
    std::cout << "Before increment: " << num << std::endl;

    // 调用函数,传入 num 的引用
    increment(num);

    std::cout << "After increment: " << num << std::endl; // 输出修改后的值
    return 0;
}

函数指针

#include <iostream>

// 定义一个简单的函数
void sayHello() {
    std::cout << "Hello, World!" << std::endl;
}

int main() {
    // 创建一个函数引用
    void (&funcRef)() = sayHello;

    // 使用函数引用调用函数
    funcRef(); // 输出: Hello, World!

    return 0;
}

十九、vector

std::vector myVector;意思是myVector是vector型,myVector里面的每个字段都是int型;

    std::vector<std::pair<
        std::string,
        std::function<void(size_t, size_t, size_t, float const*, float const*,
                           size_t, float const*, size_t, float const*, float*,
                           size_t, cudaStream_t)>>> const
        gemm_kernel_launch_functions{
            {"Custom GEMM Kernel V00", launch_gemm_kernel_v00<float>},
            {"Custom GEMM Kernel V01", launch_gemm_kernel_v01<float>},
            {"Custom GEMM Kernel V02", launch_gemm_kernel_v02<float>},
            {"Custom GEMM Kernel V02 Vectorized",
             launch_gemm_kernel_v02_vectorized<float>},
            {"Custom GEMM Kernel V03", launch_gemm_kernel_v03<float>},
            {"Custom GEMM Kernel V03 Vectorized",
             launch_gemm_kernel_v03_vectorized<float>},
            {"Custom GEMM Kernel V04", launch_gemm_kernel_v04<float>},
            {"Custom GEMM Kernel V04 Vectorized",
             launch_gemm_kernel_v04_vectorized<float>},
            {"Custom GEMM Kernel V05", launch_gemm_kernel_v05<float>},
            {"Custom GEMM Kernel V05 Vectorized",
             launch_gemm_kernel_v05_vectorized<float>},
            {"Custom GEMM Kernel V06", launch_gemm_kernel_v06<float>},
            {"Custom GEMM Kernel V06 Vectorized",
             launch_gemm_kernel_v06_vectorized<float>},
            {"Custom GEMM Kernel V06 Vectorized Double Buffered",
             launch_gemm_kernel_v06_vectorized_double_buffered<float>},
        };

意思是gemm_kernel_launch_functions是std::vector型,gemm_kernel_launch_functions里面的每个字段都是std::pair型(分别对应first和second)(first和second来自于runoob

二十、std::function

std::function 是 C++ 标准库中的一个 通用的可调用对象封装器。它可以保存、复制和调用任何类型的 可调用对象,包括函数、函数指针、Lambda 表达式、函数对象(functor)等

std::function 可以用来表示具有特定函数签名(即返回类型和参数列表)的任何可调用对象。通过 std::function,我们可以统一处理不同类型的可调用对象,而无需知道它们的具体类型。

std::function<void(cudaStream_t)> bound_function

表明std::function封装了一个可调用对象bound_function,这个可调用对象的返回值是void类型,参数是cudaStream_t类型

  • 作用:
    统一调用接口:可以将不同形式的可调用对象(如函数指针、Lambda 表达式等)统一抽象为同一个类型,使得它们可以通过统一的接口进行存储和调用。
    延迟执行:通过 std::function,你可以保存可调用对象并在之后的某个时刻调用它。
    提高灵活性:允许将函数作为参数传递给其他函数(即实现所谓的"高阶函数"),或者作为返回值返回,类似于其他语言中的函数式编程风格。
  • 缺点:
    性能开销: std::function 的灵活性是有代价的,它内部实现了类型擦除,并通过动态分配内存来存储可调用对象,因此在某些性能关键的场景下,可能会带来额外的开销。
    需要 C++11 及以上版本支持: std::function 是 C++11 标准引入的特性,在早期的 C++ 标准中并不支持。

例子:
结合函数和函数对象,探讨函数和函数对象的区别,举了一个std::function的例子

#include <iostream>
#include <functional>

void greet() {
    std::cout << "Hello from function!" << std::endl;
}

struct Functor {
    void operator()() {
        std::cout << "Hello from functor!" << std::endl;
    }
};

int main() {
    std::function<void()> callable;

    // 存储普通函数
    callable = greet;
    callable(); // 输出: Hello from function!

    // 存储函数对象
    callable = Functor();
    callable(); // 输出: Hello from functor!

    return 0;
}

二十一、Lambda 函数(例子来源于github/leimao/${gemm_optimization})

Lambda 函数(或称为匿名函数)是一种没有名称的函数,可以在需要的地方定义和使用。这种函数可以作为参数传递,或者在数据结构(如 STL 容器)中使用

    float const latency_cublas{measure_performance<void>(
        [&](cudaStream_t stream)
        {
            launch_gemm_cublas<T>(m, n, k, &alpha, A_device, lda, B_device, ldb,
                                  &beta, C_device, ldc, handle);
            return;
        },
        stream, num_repeats, num_warmups)};

此处的lambda函数就作为measure_performance函数的参数(bound_function)使用,

Lambda 函数的基本语法如下:

[capture](parameters) -> return_type {
    // function body
}

组成部分:
捕获列表 (capture):
指定哪些外部变量可以在 Lambda 函数内部使用。
可以使用引用 (&) 或值 (=) 来捕获。
例如,[&] 捕获所有外部变量的引用,[=] 捕获所有外部变量的值。

参数列表 (parameters):
定义 Lambda 函数接受的参数,类似于普通函数的参数。

返回类型 (-> return_type):
可选部分,指定返回值类型。编译器通常能够自动推断类型,可以省略。

函数体:
实际的代码逻辑。

二十二、函数模版和模版函数

22.1 函数模板(Function Template)

定义:函数模板是一种可以定义一组通用函数的模板。它允许你使用一个通用的定义来表示不同类型的函数,而不需要为每种类型写单独的函数。

语法:

template <typename T>
T add(T a, T b) {
    return a + b;
}

在这个例子中,add 是一个函数模板,其中 T 是模板参数类型。当你调用 add 函数时,编译器会根据实际传入的参数类型推导出 T 的类型。
使用:

int result = add<int>(2, 3);       // 使用int类型
double result2 = add<double>(2.5, 3.5); // 使用double类型

特点:
参数化的类型:可以使用模板参数表示任意类型。
代码复用:减少重复代码,适用于不同类型的数据。
编译期生成:编译器会在使用模板函数时生成特定类型的函数代码。

22.2 模板函数(Template Function)

定义:模板函数是使用函数模板生成的具体函数实例。例如,通过 add(2, 3) 生成的 int add(int, int) 就是一个模板函数。
联系:
模板函数是通过函数模板生成的特定实例。
当你定义了一个函数模板并使用它时,编译器会根据传入的参数生成对应的模板函数。
函数模板是模板代码的定义,而模板函数是特定类型的实例化。
示例:

template <typename T>
T multiply(T a, T b) {
    return a * b;
}

int main() {
    int x = 5, y = 10;
    double dx = 5.5, dy = 2.2;

    int intResult = multiply(x, y);        // 生成模板函数 int multiply(int, int)
    double doubleResult = multiply(dx, dy); // 生成模板函数 double multiply(double, double)
}

在这个例子中,multiply 是一个函数模板,而 int multiply(int, int) 和 double multiply(double, double) 是模板函数。

总结:
函数模版是模版函数的模版,模版函数是函数模版的实例化

二十三、构造函数的类型

23.1 默认构造函数 (Default Constructor)

无参:

class MyClass {
public:
    MyClass() {  // 默认构造函数
        // 初始化成员变量
    }
};

有参:

class MyClass {
public:
    int value;
    MyClass(int val) {  // 带参数的构造函数
        value = val;
    }
};

23.2 拷贝构造函数 (Copy Constructor)

定义:通过另一个同类型的对象来初始化当前对象。拷贝构造函数接受一个常量引用参数。
用途:在对象初始化时(如传递对象时,或者返回对象时),对对象进行拷贝。

class MyClass {
public:
    int value;
    MyClass(int val) : value(val) {}
    MyClass(const MyClass& other) {  // 拷贝构造函数
        value = other.value;
    }
};

23.3 移动构造函数 (Move Constructor)

定义:通过将一个临时对象(右值)转移到当前对象中来初始化当前对象。移动构造函数通常通过右值引用(T&&)来实现。
用途:用于避免不必要的对象复制,特别是在涉及资源管理(如动态内存分配、文件句柄等)时,提高性能。

class MyClass {
public:
    int* value;
    MyClass(int val) : value(new int(val)) {}
    MyClass(MyClass&& other) noexcept {  // 移动构造函数
        value = other.value;
        other.value = nullptr;  // 防止析构函数释放资源
    }
};

23.4 委托构造函数 (Delegating Constructor)

定义:一个构造函数调用同一个类中的另一个构造函数。在 C++11 及以后,构造函数可以通过委托给另一个构造函数来减少重复代码。
用途:通过一个构造函数来简化初始化过程,避免重复的代码。

class MyClass {
public:
    int x, y;
    MyClass(int x_val, int y_val) : x(x_val), y(y_val) {}  // 构造函数 1
    MyClass(int val) : MyClass(val, val) {}  // 委托构造函数
};

23.5 删除的构造函数 (Deleted Constructor)

定义:某些构造函数可以被显式删除,通过 = delete 来防止调用该构造函数。
用途:防止某些不想允许的构造方式,例如阻止拷贝构造或移动构造。

class MyClass {
public:
    MyClass(const MyClass&) = delete;  // 禁用拷贝构造函数
    MyClass& operator=(const MyClass&) = delete;  // 禁用拷贝赋值操作符
};


总结

友元和虚函数还要多看一下,有点混淆了多态等概念

一个复杂的模版函数的例子

template <typename T,
          typename std::enable_if<std::is_same<T, float>::value ||
                                      std::is_same<T, double>::value ||
                                      std::is_same<T, __half>::value,
                                  bool>::type = true>
std::pair<float, float> profile_gemm(
    size_t m, size_t n, size_t k, size_t lda, size_t ldb, size_t ldc,
    std::function<void(size_t, size_t, size_t, T const*, T const*, size_t,
                       T const*, size_t, T const*, T*, size_t, cudaStream_t)>
        gemm_kernel_launch_function,
    T abs_tol, double rel_tol, size_t num_repeats = 10, size_t num_warmups = 10,
    unsigned int seed = 0U)

真复杂,不愧是大佬写的
这整体是一个模版函数
模版是:

template <typename T,
          typename std::enable_if<std::is_same<T, float>::value ||
                                      std::is_same<T, double>::value ||
                                      std::is_same<T, __half>::value,
                                  bool>::type = true>

此模版有两个模版形参,第一个是T,第二个形参做了enable_if判断,当condition为真的时候,此模版有两个模版形参,第一个为T,第二个类型为bool,名称为type,值为true,当condition为假的时候,此模版只有一个形参T(好像不对,,,,,第二个形参的意思好像是如果条件不满足的话不实例化此模版,不对此模版函数进行实例化,但是这里的type=true是什么意思呢???)

学习了一下,都是对的,此处是利用了一个模版机制-----SFINAE(substitution failure is not a error)机制,这里是利用enable_if来控制type是否能够生成,从而控制SFINAE机制的触发

函数声明:

std::pair<float, float> profile_gemm(
    size_t m, size_t n, size_t k, size_t lda, size_t ldb, size_t ldc,
    std::function<void(size_t, size_t, size_t, T const*, T const*, size_t,
                       T const*, size_t, T const*, T*, size_t, cudaStream_t)>
        gemm_kernel_launch_function,
    T abs_tol, double rel_tol, size_t num_repeats = 10, size_t num_warmups = 10,
    unsigned int seed = 0U)

此函数名称为profile_gemm,返回值是std::pair<float, float>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值