C++基础与深度解析 | 类与面向对象编程 | 数据成员 | 成员函数 | 访问限定符与友元 | 构造、析构成员函数 | 字面值类、成员指针与bind交互

文章目录

  • 一、结构体与对象聚合
  • 二、成员函数(方法)
  • 三、访问限定符与友元
    • 1.访问限定符
    • 2.友元(慎用)
  • 四、构造、析构与复制成员函数
    • 1.构造函数
    • 2.析构函数
    • 3.补充
  • 五、字面值类,成员指针与bind交互
    • 1.字面值类
    • 2.成员指针
    • 3.bind交互

一、结构体与对象聚合

  结构体是从C语言就引入的一个概念,只不过在C++中进行了一系列的拓展,实际上类可以理解为从结构体演化而来。在C++中,结构体(struct)是对基本数据结构进行拓展,用于将多个不同类型的对象组合成一个单一的实体。结构体在C++中的作用与类(class)类似,但默认的访问权限不同:结构体的成员是公共的(public),而类的成员是私有的(private)。

结构体的声明与定义

  • 结构体的声明

    结构体的声明用于告诉编译器结构体的名称,但不提供成员的具体实现细节。声明通常用于在多个文件之间共享结构体的接口。

    // 声明一个结构体
    struct MyStruct;
    

    仅有声明的结构体是不完全类型( incomplete type )

      如果只声明了结构体而没有定义,那么这个结构体就是一个不完全类型。不完全类型的结构体不能用于创建对象,但可以用于指向结构体的指针或引用

  • 结构体的定义

    结构体的定义提供了成员的具体类型和实现细节。

    // 定义一个结构体
    struct MyStruct {
        int x;
        double y;
        void function() {
            // 实现细节
        }
    };
    

    在大多数情况下,结构体的声明和定义是结合在一起的。

    结构体(以及类)的一处定义原则:翻译单元级别

      在C++中,结构体(以及类)应该只在一个翻译单元(通常是一个源文件)中定义一次。如果有多个定义,编译器会报错,因为它们被认为是不同的类型。

数据成员(数据域)的声明与初始化:结构体中包含的是数据成员的声明

  • (C++11)数据成员可以使用 decltype 来声明其类型,但不能使用 auto

    从C++11开始,可以使用 decltype 关键字来声明成员变量的类型。decltype 用于推断表达式的类型。

    示例:结构体

    struct MyStruct {
        decltype(3) x;
        double y;
        void function() {
            // 实现细节
        }
    };
    

    示例:类

    class MyClass {
    public:
        MyClass() : myValue(10) {} // 初始化列表
    
    private:
        decltype(10) myValue; // 使用 decltype 声明类型
    };
    
  • 数据成员声明时可以引入 const 、引用等限定

    数据成员可以是 const,这意味着它们一旦初始化后就不能被修改。

    示例:结构体

    struct MyStruct {
        const int x = 3;
        double y;
        void function() {
            // 实现细节
        }
    };
    

    示例:类

    class MyClass {
    private:
        const int value = 5; // const 数据成员
    };
    

    数据成员也可以是引用类型,但它们必须是不可变的(即 const),并且必须在构造函数之前就已经初始化

    示例:结构体

    struct MyStruct {
        const int& x;
        double y;
        void function() {
            // 实现细节
        }
    };
    

    示例:类

    class MyClass {
    private:
        const int& refValue; // 引用数据成员
    
    public:
        MyClass(int value) : refValue(value) {} // 初始化引用成员
    };
    
  • 数据成员会在构造类对象时定义

    它们可以在类内进行初始化(C++11引入的特性),或者在构造函数的初始化列表中进行初始化。

    struct MyStruct {
     const int x = 3;
     double y;
     void function() {
         // 实现细节
     }
    };
    

    只有在构造对象时才会涉及给数据成员分配内存再初始化的过程,否则只是数据成员的声明。

  • ( C++11 )类内成员初始化

    从C++11开始,可以在类的定义中直接对数据成员进行初始化。

    class MyClass {
    public:
        MyClass() = default; // 默认构造函数
    
    private:
        int myValue{10}; // 类内成员初始化
        double anotherValue = 3.14; // 另一个类内成员初始化
    };
    
  • 聚合初始化

    从初始化列表到指派初始化器

    • 初始化列表

      数据成员可以统一通过花括号包围的初始化列表进行初始化。

      struct MyStruct {
          int x;
          double y;
          std::string z;
      };
      
      int main() {
          MyStruct ms = {3, 2.718, "pi"};
          return 0;
      }
      
    • 指派初始化器(C++20)

      C++20引入了指派初始化器,它允许使用花括号对类类型的成员进行初始化。

      #include <iostream>
      
      struct MyStr
      {
          int x;
          int y;
      };
      
      int main() {
          MyStr str{.x = 3, .y = 4};
          std::cout << str.x << " " << str.y << std::endl;
          return 0;
      }
      
  • mutable 限定符

    在C++中,mutable 是一个关键字,用作成员函数或成员变量的限定符。

    • 当应用于成员变量时,mutable 允许该变量即使在const上下文中也可以被修改。

    • 当应用于成员函数时,它表明该成员函数可以在const对象上调用,并且可以修改对象的mutable成员。

    mutable限定符的关键点

    • const成员函数: 通常,const成员函数保证不会修改其所属对象的状态。但是,如果对象中有mutable成员,const成员函数仍然可以修改这些成员。
    • 线程安全: mutable成员通常用于存储那些可能需要改变,但又不应该影响对象对外状态(即const性)的数据。例如,在多线程环境中,mutable可以用来声明那些涉及线程同步的成员变量,如互斥锁。
    • 数据隐藏: mutable可以用于实现数据隐藏,即使对象的状态在外部看起来没有改变,对象的内部表示可以被调整以优化性能。

    示例:

    #include <iostream>
    
    class MyClass {
    public:
        MyClass() : value(0) {}
    
        // 这个成员函数是const的,但它可以修改mutable成员
        void increment() const {
            ++value; // 允许修改mutable成员
        }
    
        int getValue() const {
            return value;
        }
    private:
        mutable int value; // mutable成员变量
    };
    
    int main() {
        const MyClass obj;
        obj.increment(); // 即使在const对象上也可以调用increment()
        std::cout << obj.getValue() << std::endl; // 输出修改后的count值为1
        return 0;
    }
    

静态数据成员:多个对象之间共享的数据成员

  静态数据成员是类的一个特殊成员,由类的所有实例共享,而不是每个实例拥有自己的独立副本。这意味着静态数据成员在程序运行期间只有一个实例,所有对该成员的访问和修改都会反映到这个单一的实例上。

  • 定义方式的衍化

    • C++98:类外定义

      在C++98中,静态数据成员通常在类的定义外部进行定义(实现)。对于非const静态成员,必须在类外定义;对于const静态成员(可以理解为编译期常量),可以在类内初始化

      #include <iostream>
      
      
      class MyClass {
      public:
          static int staticVar; // 声明静态成员变量
          static const int constStaticVar = 2; // 声明并初始化const静态成员变量
      
      };
      
      // 类外定义
      int MyClass::staticVar = 1;
      //const int MyClass::constStaticVar = 10;
      
      int main()
      {
          MyClass myc;
          std::cout << "staticVar=" << myc.staticVar << " " << "constStaticVar=" << myc.constStaticVar << std::endl;
          //staticVar=1 constStaticVar=2
      }
      
    • C++17 :内联静态成员的初始化

      从C++17开始,允许在类内直接初始化静态成员变量(内联变量)。

      #include <iostream>
      
      
      // C++17 示例
      class MyClass {
      public:
          static int staticVar;
          static inline const int constStaticVar = 10; // 类内初始化const静态成员变量
      };
      
      // 现在,staticVar必须定义在类外
      int MyClass::staticVar;
      //const int MyClass::constStaticVar = 100;
      
      int main()
      {
          MyClass myc;
          std::cout << "staticVar=" << myc.staticVar << " " << "constStaticVar=" << myc.constStaticVar << std::endl;
      }
      
  • 可以使用auto推导类型(非静态成员)

    从C++11开始,auto 关键字可以用于自动推导类型,但auto不能用于静态成员变量的类型推导,因为静态成员需要明确的类型声明。

    // auto 示例(非静态成员)
    class MyClass {
    public:
        auto autoVar = 5; // 自动推导为int
    };
    

示例:结构体

#include <iostream>

struct Str
{
    static int x;	//静态变量的声明
    int y = 1;
};

int Str::x;	//静态变量的定义
int main()
{
    Str m_str1;
    Str m_str2;
    m_str1.x = 100;
    std::cout << m_str2.x << std::endl;   //100
}

示例:类–经典使用方式

  Counter 类有一个静态数据成员 count,用于跟踪创建的 Counter 对象的数量。静态成员 count 在类外定义,并在 Counter 对象的构造函数和析构函数中被修改。

#include <iostream>

class Counter {
public:
    static int count; // 静态数据成员

    Counter() {
        ++count; // 每次创建对象时递增计数
    }
    
    ~Counter() {
        --count; // 每次销毁对象时递减计数
    }
    
    static int getCount() {
        return count;
    }
};

// 类外定义
int Counter::count = 0;

int main() {
    Counter c1;
    Counter c2;
    
    std::cout << "Number of Counter objects: " << Counter::getCount() << std::endl; // 输出 2
    
    // 当c1和c2销毁时,count会递减两次
    return 0;
}

静态数据成员的访问

  在C++中,静态数据成员属于类本身,而不是类的某个特定对象或实例。因此,静态成员可以通过类名直接访问,也可以通过类的任何对象访问。访问静态数据成员通常有以下几种方式:

  • “.” 与“ ->” 操作符

    “.” 用于通过对象实例来访问静态成员

    “ ->”用于通过指向类对象的指针来访问静态成员

    #include <iostream>
    
    class MyClass {
    public:
        static int staticVar;
    };
    
    int MyClass::staticVar = 10;
    
    int main() {
        std::cout << "使用点操作符 . 来通过具体的对象访问静态成员\n";
        MyClass obj;
        std::cout << obj.staticVar << std::endl; // 通过对象访问静态成员
        obj.staticVar = 30;
        std::cout << obj.staticVar << std::endl; // 输出 30
        
        std::cout << "使用箭头操作符 -> 来访问静态成员\n";
        MyClass* objPtr = new MyClass();
        std::cout << objPtr->staticVar << std::endl; // 通过指针访问静态成员
        delete objPtr; // 释放对象
        return 0;
    }
    
  • “::” 操作符–通过类名直接访问

    使用作用域分辨运算符 :: 来访问静态成员。这种方式不依赖于任何对象实例,直接通过类名来指定成员。

    #include <iostream>
    
    class MyClass {
    public:
        static int staticVar;
    };
    
    int MyClass::staticVar = 10;
    
    int main() {
        std::cout << MyClass::staticVar << std::endl; // 输出 10
        MyClass::staticVar = 20; // 修改静态成员
        std::cout << MyClass::staticVar << std::endl; // 输出 20
        return 0;
    }
    

在类的内部声明相同类型的静态数据成员

  在C++中,你可以在类的内部声明一个相同类型的静态数据成员。这种声明方式告诉编译器,这个类将会有一个静态成员,其类型与类本身相同。然而,由于静态成员的完整类型在类完全定义之前是未知的,所以静态数据成员的定义必须在类定义之后、类的作用域内进行。这意味着,你需要在类定义的外部提供这个静态成员的具体定义。这是因为静态成员在类定义之前是不完整的,而定义需要一个完整的类型。

class MyClass {
public:
    static MyClass* x;  // 声明一个指向类本身的静态成员
};

// 在类定义之后和类作用域内定义静态成员
MyClass* MyClass::x = nullptr; // 使用指针来避免无限递归定义

int main() {
    // ...
    return 0;
}

或者

#include <memory>

class MyClass {
public:
    static std::unique_ptr<MyClass> x;  // 使用智能指针声明
};

// 定义静态成员
std::unique_ptr<MyClass> MyClass::x = std::make_unique<MyClass>();

int main() {
    // ...
    return 0;
}

二、成员函数(方法)

  在C++中,结构体(struct)和类(class)在功能上非常相似,都可以包含数据成员和成员函数。不过,它们在默认的访问权限上有所不同:

  • struct 默认情况下,其成员都是public,即可以被外部访问。
  • class 默认情况下,其成员都是private,即只能通过类内部或者友元函数访问。

  类可视为一种抽象数据类型,通过相应的接口(成员函数)进行交互。成员函数对内操作数据成员,对外提供调用接口。成员函数是类的一部分,它们可以访问类的私有和受保护成员。成员函数可以定义在类的定义内部或外部。

成员函数的声明与定义

  • 类内定义隐式内联

      当成员函数的定义被放置在类定义内部时,它被视为隐式内联。这意味着在编译时,编译器会将函数的定义直接嵌入到每个调用该函数的地方,从而可能提高程序的效率。但这也意味着,如果函数体较大,可能会导致代码膨胀。

    class MyClass {
    public:
        void myFunction() {
            // 函数体
        }
    };
    
  • 类内声明 + 类外定义

      成员函数也可以在类内部声明,然后在类外部定义。这种方式允许你将类的定义和实现分开,使得代码更加清晰和易于管理。

    //.h文件
    // 类内声明
    class MyClass {
    public:
        void myFunction();
    };
    
    //.cpp文件
    // 类外定义
    void MyClass::myFunction() {
        // 函数体
    }
    
  • 类与编译期的两遍处理

      C++编译器在处理类定义时,通常需要两遍扫描。第一遍是检查成员函数的声明,第二遍是处理成员函数的定义。这是因为成员函数的定义可能依赖于类内部的数据类型,而这些类型在第一次扫描时尚未完全定义。

  • 成员函数与尾随返回类型( trail returning type )

      尾随返回类型是C++11引入的新特性,它允许你将函数的返回类型放在函数声明的末尾,使用auto关键字。这种语法特别适用于复杂的返回类型,可以使代码更加清晰。

    class MyClass {
    public:
        auto myFunction() -> int {
            // 函数体
        }
    };
    

    在C++14中,尾随返回类型可以进一步简化,直接使用auto关键字,而不需要显式指定返回类型,编译器会根据函数体中的返回语句自动推断返回类型。

    class MyClass {
    public:
        auto myFunction() {
            // 函数体
            return 42; // 编译器推断返回类型为int
        }
    };
    

    尾随返回类型不仅用于成员函数,也可以用于普通函数和模板。它使得函数声明更加简洁,特别是在返回类型复杂或依赖于模板参数时。

成员函数与this指针

  在C++中,this指针是一个隐式可用的指针,它指向成员函数当前正在操作的对象的地址。每个非静态成员函数都有一个this指针,它允许成员函数访问和修改调用对象的状态(即对象的数据成员)。

  • 使用this指针引用当前对象

      this指针可以用来引用调用成员函数的对象。在成员函数内部,你可以通过this->member的方式访问数据成员或调用其他成员函数。

    class MyClass {
    private:
        int value;
    public:
        MyClass(int v) : value(v) {}
        
        void setValue(int v) {
            this->value = v; // 使用this指针明确指出成员变量
        }
        
        int getValue() const {
            return value; // 这里不需要this指针,因为编译器知道是成员变量
        }
    };
    
  • 基于 const 的成员函数重载

      在C++中,可以为成员函数指定const修饰符,表示这个成员函数不会修改对象的状态,即不会改变任何数据成员的值。这允许const成员函数在const对象上调用来避免编译错误。

      当成员函数被声明为const时,它不能修改任何非mutable的成员变量,也不能调用任何非const成员函数。此外,const成员函数不能使用this指针来修改对象的状态。

      可以重载成员函数,以提供对const和非const版本的支持。

成员函数的名称查找与隐藏关系

  在C++中,成员函数的名称查找规则决定了编译器如何在成员函数中解析名称。这些规则确保了成员函数中的名称解析是一致和明确的,避免了歧义。

  • 函数内部隐藏函数外部

    在成员函数的参数列表中声明的名称会隐藏外部作用域中的同名名称,包括类的成员变量和函数。这意味着在成员函数内部,参数名称具有更高的优先级。

    class MyClass {
    private:
        int value;
    public:
        MyClass(int value) : value(value) {}
        
        void setValue(int value) { // 参数value隐藏了成员变量value
            this->value = value; // 使用this指针来引用成员变量
        }
    };
    
  • 类内部名称隐藏类外部

    在类的作用域内,类的成员名称会隐藏外部全局名称。这意味着如果类内部有一个名称与全局名称相同,那么在类的作用域内,成员名称将被优先使用。

    int globalValue = 10; // 全局变量
    
    class MyClass {
    public:
        int globalValue; // 类成员,隐藏了全局变量globalValue
        
        void display() {
            cout << globalValue << endl; // 输出类成员globalValue的值
            cout << ::globalValue << endl; // 使用作用域运算符来引用全局变量
        }
    };
    
  • 使用 this 或域操作符引入依赖型名称查找

    当成员函数内部的名称与成员变量或类外部的名称冲突时,可以使用this指针或作用域运算符::来明确指定名称的来源。

    • this指针用于引用当前对象的成员变量或成员函数,当成员变量名称与参数名称冲突时,可以使用this->member来明确引用成员变量。
    • 作用域运算符::用于引用全局名称或命名空间中的名称,当需要区分类内部和类外部的同名名称时,可以使用::name来指定。
    #include <iostream>
    
    int value = 10; // 全局变量
    
    class MyClass {
        int value;
    public:
        void myFunction(int value) {
            std::cout << value << std::endl;
            std::cout << this->value << std::endl; // 使用this指针引用成员变量
            std::cout << ::value << std::endl; // 使用作用域运算符引用全局变量
        }
    };
    
    int main()
    {
        MyClass myc;
        myc.myFunction(1);
        //输出:1,0,10
    }
    

静态成员函数

  在C++中,静态成员函数和静态数据成员是类的一部分,但它们与非静态成员(实例成员)有显著的区别。静态成员属于类本身,而不是类的任何特定实例。这意味着静态成员可以在没有创建类实例的情况下访问,并且它们在程序的整个运行期间只存在一份拷贝。

通俗理解,静态成员函数不会传入隐含的this指针,因此,并不属于特定的实例。

静态成员函数的主要用途是实现与类相关但与类的任何特定实例无关的功能。例如,它们可以用来实现计数器(跟踪创建了多少个类的实例),或者提供对静态资源的访问。由于静态成员不属于任何特定的对象,它们通常用于实现全局功能或工具函数。

  • 定义静态成员函数

    静态成员函数使用关键字static定义,它们可以访问类的静态成员,但无法访问非静态成员,因为静态成员函数没有this指针。

  • 访问静态成员函数

    静态成员函数可以通过类名直接调用,而不需要创建类的实例

  • 在静态成员函数中返回静态数据成员

    静态成员函数可以返回类的静态数据成员的值,因为静态成员函数可以访问其他静态成员。

示例:

#include <iostream>

class Counter {
public:
    static int count; // 静态数据成员

    Counter() {
        ++count; // 每次创建对象时递增计数
    }
    
    ~Counter() {
        --count; // 每次销毁对象时递减计数
    }
    
    static int getCount() {
        return count;
    }
};

// 类外定义
int Counter::count = 0;

int main() {
    Counter c1;
    Counter c2;
    
    std::cout << "Number of Counter objects: " << Counter::getCount() << std::endl; // 输出 2
    
    // 当c1和c2销毁时,count会递减两次
    return 0;
}

成员函数基于引用限定符的重载( C++11 ):

详细内容可参考ref-qualified-member-functions

三、访问限定符与友元

1.访问限定符

  在C++中,publicprivateprotected关键字用于定义类的成员访问权限。访问权限的引入使得可以对抽象数据类型进行封装。以下是这些访问限定符的详细说明:

  • public(公共)
    • 成员可以被任何外部代码访问。
    • 通常用于提供类的接口,即那些设计为与外部世界交互的部分。
  • private(私有)
    • 成员只能在类的内部访问。
    • 这是默认的封装方式,用于隐藏类的实现细节,确保数据安全和类的完整性。
    • 私有成员不能被外部代码直接访问,但可以被类的成员函数、友元函数以及友元类访问
  • protected(受保护)
    • 成员可以在类的内部访问,也可以被派生类(继承关系)访问。
    • 这用于提供一种中间级别的封装,其中某些特定的外部类(通常是派生类)可以访问这些成员。

类与结构体缺省访问权限的区别

  • 类(class:默认情况下,类的所有成员都是private的。这意味着除非明确指定为publicprotected,否则类的成员不能被外部代码访问。
  • 结构体(struct:默认情况下,结构体的所有成员都是public的。这意味着除非明确指定为privateprotected,否则结构体的成员可以被任何外部代码访问。

示例:

class MyClass { // 默认成员是private
private:
    int privateVar;
public:
    void setPrivateVar(int value) {
        privateVar = value;
    }
    int getPrivateVar() const {
        return privateVar;
    }
};

struct MyStruct { // 默认成员是public
    int publicVar;
};

2.友元(慎用)

  在C++中,友元(friend)是一种特殊的机制,它允许某个类或函数访问另一个类的私有(private)和受保护(protected)成员,即使这些成员对于类的外部是不可见的。使用友元可以打破类的封装性,但通常只在需要时才使用,因为过度使用友元会破坏封装性,使得代码难以维护。

声明某个类或某个函数为当前类的友元

  • 通过在类内部使用friend关键字,可以声明某个类或函数为当前类的友元。
  • 友元类的所有成员函数都可以访问声明它的类的私有和受保护成员。
  • 友元函数也可以访问类的私有和受保护成员。

示例:

//声明
class FriendClass;
void friendFunction();

class MyClass {
private:
    inline static int privateData;
public:
    friend class FriendClass; // 声明友元类,友元类中可以访问该类中所有成员
    friend void friendFunction(); // 声明友元函数
};

class FriendClass {
public:
    void accessMyClass() {
        MyClass::privateData = 10; // 直接访问MyClass的私有成员
    }
};

void friendFunction() {
    MyClass::privateData = 20; // 友元函数也可以访问私有成员
}

int main()
{
    
}

在类内首次声明友元类或友元函数

  • 友元的声明必须在类内部进行,不能在类外部声明友元。
  • 友元类或友元函数的完整定义(如果有的话)可以在类外部,但声明必须在类内部,前向声明可以省略。
class MyClass {
private:
    inline static int privateData;
public:
    friend class FriendClass; // 声明友元类,友元类中可以访问该类中所有成员
    friend void friendFunction(); // 声明友元函数
};

class FriendClass {
public:
    void accessMyClass() {
        MyClass::privateData = 10; // 直接访问MyClass的私有成员
    }
};

void friendFunction() {
    MyClass::privateData = 20; // 友元函数也可以访问私有成员
}

int main()
{
    
}

友元函数的类外定义与类内定义

  • 友元函数可以在类外部定义,也可以在类内部定义(隐式内联)。
  • 如果友元函数在类外部定义,它必须在类被完全定义之后才能定义,因为它可能依赖于类的成员。
// 类内定义友元函数
class MyClass {
private:
    int privateData;
public:
    friend void externalFriendFunction(MyClass& mc) {
    	mc.privateData = 40; // 直接访问私有成员
	}
};


// 类外定义友元函数
class MyClass {
private:
    int privateData;
public:
    friend void externalFriendFunction(MyClass&);
};

void externalFriendFunction(MyClass& mc) {
    mc.privateData = 40; // 直接访问私有成员
}

隐藏友元(hidden friend):常规名称查找无法找到

详细内容可查看:https://www.justsoftwaresolutions.co.uk/cplusplus/hidden-friends.html

  隐藏友元举例:

class MyClass {
private:
    int privateData;
public:
    friend void externalFriendFunction() {
        MyClass mc;
    	mc.privateData = 40; // 直接访问私有成员
	}
};

int main()
{
    externalFriendFunction();
    //此时,报错 error: 'externalFriendFunction' was not declared in this scope
}

既然常规的名称查找查找不到,则使用ADL(参数依赖查找)作为隐藏友元的正确使用方式。

// 类内定义友元函数
class MyClass {
private:
    int privateData;
public:
    friend void externalFriendFunction(MyClass& mc){
        mc.privateData = 40; // 直接访问私有成员
    }
};


int main()
{
    MyClass myc;
    externalFriendFunction(myc);
}
  • 好处:减轻编译器负担,防止误用
  • 改变隐藏友元的缺省行为:在类外声明或定义函数

四、构造、析构与复制成员函数

1.构造函数

  在C++中,构造函数是一种特殊的成员函数,用于在创建对象时初始化对象的状态。以下是构造函数的一些关键特性

  • 名称与类名相同

    构造函数的名称必须与类名完全相同。

  • 无返回值

    构造函数没有返回值,甚至没有void类型的返回值。

  • 可以包含多个版本(重载)

    类可以有多个构造函数,每个构造函数有不同的参数列表,这称为构造函数重载。

  • 自动调用

    每当创建类的新实例时,构造函数会自动被调用

  • 初始化成员变量

    构造函数的主要任务是初始化对象的成员变量。

代理构造函数(C++11):

  C++11标准引入了一种新的构造函数特性,称为委托构造函数或代理构造函数。这种特性允许一个构造函数调用同一个类中的另一个构造函数来完成其初始化工作。这在有多个构造函数且一些构造函数有共同初始化代码的情况下非常有用。

代理构造函数的优点

  • 减少代码重复:代理构造函数可以减少重复的初始化代码,使类的定义更加清晰。
  • 提高代码可维护性:当初始化逻辑变化时,只需在一个地方更新,所有相关的构造函数都会自动反映这些变化。
  • 简化构造函数的复杂性:在有多个构造函数且它们之间有共同的初始化步骤时,代理构造函数可以简化构造函数的设计。

其语法如下:

class MyClass {
public:
    MyClass(int x) : value(x) {}           // 构造函数1

    MyClass(const std::string& s) : MyClass(s.size()) {} // 代理构造函数
private:
    int value;
};

在这个例子中,MyClass有两个构造函数。第二个构造函数接受一个std::string类型的参数,并使用成员初始化列表委托给第一个构造函数,传递s.size()作为参数。

初始化列表

  在C++中,初始化列表(Initializer List)是一种特殊的语法,用于在构造函数中初始化对象的非静态数据成员

初始化列表用于初始化,而不是赋值。

区分数据成员的初始化与赋值

  • 初始化:是在创建对象时设置成员变量的初始值。
  • 赋值:是在对象已经创建后,给成员变量赋予一个新的值。
  • 通常情况下初始化列表可以提升系统性能

    • 初始化列表直接初始化成员变量,避免了赋值操作可能涉及的额外开销,如调用拷贝构造函数。
    • 对于内置类型,使用初始化列表可以避免不必要的对象拷贝
  • 一些情况下必须使用初始化列表

    • 类中包含引用成员:引用必须被绑定到有效的对象上,因此必须在对象构造时立即初始化。
    • 类中包含常量成员:常量成员的值一旦在构造期间设置后就不能被改变,因此必须使用初始化列表。
    • 类中包含没有默认构造函数的成员:如果成员类型没有默认构造函数,那么必须在初始化列表中明确提供这些成员的值。
  • 初始化顺序

    成员的初始化顺序是根据它们在类中声明的顺序,而不是初始化列表中的顺序。

  • 使用初始化列表覆盖类内成员初始化的行为

    在类的定义中,可以为成员变量提供默认的初始化值。然而,当构造函数中提供了初始化列表时,这些默认初始化值会被覆盖。

    class MyClass {
    public:
        MyClass(int x) : value(x) {} // 使用初始化列表初始化value
    private:
        int value = 10; // 类内成员初始化,但会被初始化列表覆盖
    };
    

缺省构造函数:(零个参数)

  在C++中,缺省构造函数是指不需要提供任何参数即可调用的构造函数 ,它允许类的实例在没有提供具体初始化参数的情况下被创建。缺省构造函数的特性

  • 自动合成

    如果类中没有提供任何构造函数,那么在条件允许的情况下,编译器会合成一个缺省构造函数,前提是类中没有声明任何不允许默认构造的成员(例如,没有默认构造函数的类成员,或者不可默认初始化的引用成员)。

  • 缺省初始化

    合成的缺省构造函数会使用缺省初始化来初始化其数据成员。对于内置类型,这通常意味着将它们初始化为零;对于类类型(或者抽象数据类型),这将调用相应类型的默认构造函数。

  • 调用缺省构造函数时避免most vexing parse

    most vexing parse 是C++中的一个语法歧义问题,它发生在构造函数声明时括号的使用上。为了避免歧义,可以使用初始化列表语法或者显式地使用= default来指示编译器这是一个构造函数。

  • 使用default关键字定义缺省构造函数

    从C++11开始,可以使用default关键字显式地告诉编译器为类合成一个默认构造函数

    class MyClass {
    public:
        MyClass() = default;  // 指示编译器合成默认构造函数
    };
    

    如果类中已经有一个用户定义的构造函数,并且希望编译器也提供一个默认构造函数,那么可以使用default关键字来显式地声明它,使用缺省初始化来初始化其数据成员。

单一参数构造函数

class MyClass {
public:
    explicit MyClass(int value) : x(value) {
        // 构造函数体
    }
private:
    int x;
};

int main() {
    MyClass obj(10);         // 正确,显式调用
    MyClass obj1 = MyClass(10); // 正确,显式类型转换后再将临时对象复制到obj
    MyClass obj2 = static_cast<MyClass>(10);	//单一构造函数中的参数类型与10一致就可以使用static_cast完成类型转换
    // MyClass obj3 = 10;     // 错误,因为构造函数是explicit的,不能用于隐式转换
}

  在C++中,单一参数的构造函数确实可以被看作是一种类型转换函数。这是因为它们允许将一个类型的值转换为另一个类型的对象。

  然而,这种转换可能会在不经意间发生,导致一些不期望的行为。为了避免这种情况,C++提供了explicit关键字,可以使用explicit关键字避免求值过程中的隐式转换。当你在一个构造函数前使用explicit关键字时,你告诉编译器这个构造函数不能用于隐式类型转换。这意味着,它不能在没有明确调用的情况下自动将参数转换为类的对象。

拷贝构造函数:接收一个当前类对象的构造函数

  C++中的拷贝构造函数是一个特殊的构造函数,它负责创建一个对象的副本。拷贝构造函数通常在以下几种场景中被调用:

  1. 参数传递:当一个对象作为参数传递给函数时,如果函数参数的类型与对象类型相同,拷贝构造函数会被调用以创建一个副本。
  2. 返回值:当一个函数返回一个对象时,拷贝构造函数会被用来创建返回对象的副本。
  3. 对象复制:当使用=操作符显式复制对象时,拷贝构造函数会被调用。
  4. 数组初始化:当使用数组初始化语法创建对象数组时,拷贝构造函数会被用来初始化数组中的每个元素。

拷贝构造函数会在涉及到拷贝初始化的场景被调用,因此要注意拷贝构造函数的形参类型。拷贝构造函数的形参类型通常是对象类型的引用,并且通常需要加上const修饰符,以防止对原始对象的修改。例如:

class MyClass {
private:
    int* data;
    size_t size;
    
public:
    MyClass(const MyClass& other) : size(other.size), data(new int[other.size]) {
        // 拷贝构造函数体,复制other的数据成员
    }
};

  如果开发者没有为类提供拷贝构造函数,编译器会自动合成一个默认的拷贝构造函数。合成的拷贝构造函数会逐个成员地调用成员的拷贝构造函数来复制对象。这意味着,如果类中包含有自定义的拷贝逻辑,开发者需要显式地提供拷贝构造函数,以确保正确地复制对象。

  合成拷贝构造函数的行为适用于所有成员,包括基本数据类型、指针、引用和其他对象。如果类中包含指针,开发者通常需要特别注意拷贝构造函数的行为,因为默认的拷贝构造函数会执行浅拷贝,这可能导致两个对象共享相同的资源,从而引发问题。在这种情况下,需要自定义拷贝构造函数来实现深拷贝

示例,如果MyClass包含一个指向动态分配内存的指针,拷贝构造函数可能需要这样实现

#include <iostream>
#include <cstring> // 用于std::memcpy

class MyClass {
private:
    int* data;
    size_t size;

public:
    // 普通构造函数
    MyClass(size_t sz) : size(sz), data(new int[sz]) {
        std::cout << "构造函数被调用,分配内存。" << std::endl;
        for (size_t i = 0; i < size; ++i) {
            data[i] = i; // 初始化数组
        }
    }

    // 拷贝构造函数
    MyClass(const MyClass& other) : size(other.size), data(new int[other.size]) {
        std::cout << "拷贝构造函数被调用,进行深拷贝。" << std::endl;
        std::memcpy(data, other.data, size * sizeof(int));
    }

    // 析构函数
    ~MyClass() {
        std::cout << "析构函数被调用,释放内存。" << std::endl;
        delete[] data;
    }

    // 辅助函数,用于打印数组内容
    void print() const {
        std::cout << "数组内容:";
        for (size_t i = 0; i < size; ++i) {
            std::cout << data[i] << " ";
        }
        std::cout << std::endl;
    }
};

int main() {
    MyClass a(5); // 创建对象a
    a.print();   // 打印对象a的内容

    MyClass b = a; // 使用拷贝构造函数创建对象b,进行深拷贝
    b.print();     // 打印对象b的内容

    return 0;
}

运行结果:

构造函数被调用,分配内存。
数组内容:0 1 2 3 4 
拷贝构造函数被调用,进行深拷贝。
数组内容:0 1 2 3 4 
析构函数被调用,释放内存。
析构函数被调用,释放内存。

拷贝构造函数首先为新对象分配内存,然后使用std::memcpy函数来复制原始对象的数组内容。这样,每个MyClass对象都有自己独立的内存副本,避免了浅拷贝可能带来的问题。

移动构造函数(C++11):接收一个当前类右值引用对象的构造函数

  移动构造函数是C++11引入的一个特性,它允许开发者优化资源的转移,特别是对于大型对象或者拥有资源(如动态分配的内存)的对象。移动构造函数接收一个右值引用作为参数,这个右值引用通常指向一个临时对象或者即将被销毁的对象。

有移动构造函数就调用移动构造函数,没有移动构造函数就调用拷贝构造函数。

以下是移动构造函数的一些关键点:

  1. 资源转移:移动构造函数可以从传入的对象中“偷窃”资源,例如指针指向的内存。这避免了复制资源的开销。

  2. 合法状态:在转移资源后,需要确保原对象处于合法状态。这通常意味着原对象不应该再持有被转移的资源。

  3. 编译器合成:如果类没有定义时(如拷贝构造函数),编译器可以合成一个。

    在什么情况下才会合成,后面会详细介绍

  4. 异常保证:移动构造函数通常声明为noexcept,即保证不会抛出异常。这是因为移动操作通常涉及资源的转移,如果抛出异常,可能会导致资源泄漏。

  5. 右值引用:移动构造函数接收右值引用,右值引用是C++11引入的,用于标识那些即将离开作用域的对象。

  6. 左值和右值:在C++中,右值引用对象用作表达式时实际上是左值,因为它们有确定的内存地址。它们表示临时对象或即将销毁的对象。

示例:

#include <iostream>

class MyClass {
public:
    int* data;

    MyClass(int size) {
        data = new int[size];
        // 初始化data等操作
    }

    // 移动构造函数
    MyClass(MyClass&& other) noexcept : data(other.data) {
        // 将资源从other“偷”过来
        other.data = nullptr; // 确保other处于合法状态
    }

    ~MyClass() {
        delete[] data;
    }
};

int main() {
    MyClass a(10);
    
    MyClass b = std::move(a); // 使用移动构造函数创建b,a的数据被转移到b
    // a.data现在是nullptr,b.data指向原来的数据
}

拷贝赋值与移动赋值函数(operator=):运算符重载

  在C++中,拷贝赋值运算符(operator=)和移动赋值运算符是两个重要的成员函数,它们允许对象之间赋值。

拷贝赋值运算符:

  1. 功能:用于将一个对象的内容复制到另一个对象中。
  2. 自赋值检查:需要检查是否发生自赋值,即对象赋值给自己。如果发生自赋值,需要先复制一份再进行赋值,以避免覆盖原始数据。
  3. 返回类型:通常返回当前对象类型的引用,允许链式赋值。
  4. 合成:如果类没有定义拷贝赋值运算符,编译器会自动合成一个。

移动赋值运算符:

  1. 功能:类似于移动构造函数,用于从临时对象或即将销毁的对象中“偷”资源。
  2. 自赋值检查:同样需要检查自赋值。
  3. 返回类型: 返回当前对象类型的引用。
  4. 合成:如果类没有定义移动赋值运算符,并且满足某些条件,编译器会自动合成一个。

共同点

  • 初始化列表:赋值运算符不能使用初始化列表,因为它们是在对象构造之后调用的。
  • 异常保证:通常声明为noexcept,以保证不会抛出异常。
  • 资源管理:需要正确管理资源,避免内存泄漏或其他资源问题。

示例:见3

2.析构函数

  C++中的析构函数是一个特殊的成员函数,其主要作用是释放对象生命周期结束时占用的资源。以下是析构函数的一些关键特性

  1. 函数名:析构函数的名称由类型名称前加上~符号组成,例如,对于类MyClass,其析构函数名为~MyClass()
  2. 无参数和无返回值:析构函数不接受任何参数,并且没有返回值。
  3. 释放资源:析构函数的主要目的是释放对象在构造时或在其生命周期中分配的资源,如动态内存、文件句柄、网络连接等。
  4. 内存回收:在析构函数执行完毕后,对象所占用的内存才会被回收。这意味着析构函数中执行的所有清理工作必须在内存回收之前完成。
  5. 自动合成:如果开发者没有为类定义析构函数,编译器会自动合成一个默认的析构函数。合成的析构函数通常执行一些基本的清理工作,例如调用类成员的析构函数。
  6. 异常保证,不能抛出异常:析构函数通常被声明为noexcept(或在C++11之前使用throw()),这意味着它们保证不会抛出异常。这是重要的,因为在异常处理过程中,析构函数可能会被调用,如果在这种情况下抛出异常,程序将无法正确地清理资源。
  7. 继承和多态:在继承体系中,基类的析构函数应该是虚函数(特别是当基类指针被用来指向派生类对象时)。这确保了当删除基类指针时,正确的析构函数会被调用。
  8. 自定义析构函数:如果类管理了动态分配的资源,或者有其他需要在对象生命周期结束时执行的清理工作,开发者需要自定义析构函数来确保资源的正确释放。

示例:

#include <iostream>

class MyClass {
public:
    MyClass() {
        // 构造函数逻辑,可能包括动态内存分配
    }

    ~MyClass() {
        // 析构函数逻辑,释放资源
        std::cout << "资源正在被释放。" << std::endl;
    }
};

int main() {
    {
        MyClass obj; // obj的构造函数被调用
        // ...
    } // obj的析构函数在对象生命周期结束时被调用,自动释放资源
    return 0;
}

3.补充

  通常来说,一个类:

  • 如果需要定义析构函数,那么也需要定义拷贝构造与拷贝赋值函数。
  • 如果需要定义拷贝构造函数,那么也需要定义拷贝赋值函数 。
  • 如果需要定义拷贝构造(赋值)函数,那么也要考虑定义移动构造(赋值)函数。

示例:包含指针的类

#include <iostream>
#include <cstring> // 用于std::memcpy

class MyClass {
public:
    MyClass() : data(new int()) {}
    
    // 拷贝构造函数
    MyClass(const MyClass& other) : data(new int())
    {
        std::cout << "copy constructor is called!" << std::endl;
        //分配新的内存
        *data = *(other.data);
    }
    
    // 拷贝赋值运算符
    MyClass& operator= (const MyClass& other)
    {
        std::cout << "copy assignment is called!" << std::endl;        
        if (this != &other)
        {
            *data = *(other.data);
        }
        return *this;
    }
    
    // 移动构造函数
    MyClass(MyClass&& other) noexcept
        : data(other.data)
    {
        std::cout << "move constructor is called!" << std::endl;
        other.data = nullptr;
    }
    
    // 移动赋值运算符
    MyClass& operator=(MyClass&& other) noexcept {
        std::cout << "move assignment is called!" << std::endl;        
        if (this != &other)
        {
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }
    
	~MyClass() {
        std::cout << "destructor is called!" << std::endl;
        delete data;
    }
    
    int& getData() {
        return *data;
    }
    
private:
    int* data;
};

int main()
{
    MyClass obj;
    obj.getData() = 3;
    std::cout << obj.getData() << std::endl;
    
    //拷贝构造,需要开辟资源
    MyClass obj1(obj);
    //拷贝赋值
    obj1 = obj;
    
    //移动构造,不需要开辟资源
    MyClass obj2 = std::move(obj1);
    //移动赋值函数
    MyClass obj3;
    obj3 = std::move(obj2);
}

运行结果:

3
copy constructor is called!
copy assignment is called!
move constructor is called!
move assignment is called!
destructor is called!
destructor is called!
destructor is called!
destructor is called!

default关键字

  在C++中,default 关键字用于指示编译器为类自动生成默认的特殊成员函数。这些特殊成员函数包括:

  1. 默认构造函数:如果没有为类定义任何构造函数,编译器将自动提供一个默认构造函数。
  2. 拷贝构造函数:如果没有为类定义拷贝构造函数,并且类没有声明任何成员为 const 或引用类型,编译器将自动提供一个默认拷贝构造函数。
  3. 移动构造函数:如果没有为类定义移动构造函数,并且类的所有非静态成员都可以移动,编译器将自动提供一个默认移动构造函数。
  4. 拷贝赋值运算符:如果没有为类定义拷贝赋值运算符,并且类没有声明任何成员为 const 或引用类型,编译器将自动提供一个默认拷贝赋值运算符。
  5. 移动赋值运算符:如果没有为类定义移动赋值运算符,并且类的所有非静态成员都可以移动,编译器将自动提供一个默认移动赋值运算符。
  6. 析构函数:如果没有为类定义析构函数,编译器将自动提供一个默认析构函数。

使用 default 关键字可以显式告诉编译器为类生成默认的特殊成员函数,即使类中有其他构造函数或赋值运算符定义。这在某些情况下很有用,比如当你需要一个默认构造函数,但类中已经有了其他构造函数时。

示例:

class MyClass {
public:
    MyClass() = default; // 显式要求编译器生成默认构造函数
    // 其他成员函数和变量
};

delete关键字

  在C++中,delete关键字用于删除成员函数,使其无法被实例化。这通常用于以下几种情况:

  1. 禁止拷贝和赋值:如果一个类不应该被拷贝或赋值,可以删除拷贝构造函数和拷贝赋值运算符。
  2. 禁止移动:如果一个类不应该被移动,可以删除移动构造函数和移动赋值运算符。
  3. 禁止默认行为:对于某些特殊类,可能需要禁止默认构造函数或析构函数。

使用delete关键字的一些要点:

  • 对所有函数都有效delete关键字可以用于删除类的任何成员函数,包括构造函数、拷贝构造函数、移动构造函数、拷贝赋值运算符、移动赋值运算符和析构函数。

  • 注意其与未声明的区别:如果一个成员函数被声明为delete,那么它在任何情况下都不能被调用(声明但不能被调用)。如果一个成员函数没有被声明,那么编译器可能会自动合成它(如果类没有声明任何特殊成员函数)。

  • 不要为移动构造(移动赋值)函数引入delete限定符

    • 如果只需要拷贝行为,那么引入拷贝构造即可

    • 如果不需要拷贝行为,那么将拷贝构造声明为delete函数即可

    • 注意delete移动构造(移动赋值)对C++17的新影响

      在C++17中,强制编译器去掉移动初始化的步骤。

示例:

class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete; // 删除拷贝构造函数
    NonCopyable& operator=(const NonCopyable&) = delete; // 删除拷贝赋值运算符
    
};

特殊成员的合成行为列表:(红框表示支持但可能会废除的行为)

  下面是用户声明某种成员函数后,编译器对其他成员函数的行为。

image-20240604135325340

五、字面值类,成员指针与bind交互

1.字面值类

  C++中的字面值类(Literal types)是一种特殊的类类型(可以构造编译器常量的类型),它允许在编译时构造对象,并且可以用于常量表达式。以下是字面值类的一些关键特性

  • 其数据成员需要是字面值类型

  • 提供constexpr / consteval构造函数(小心使用consteval(C++20))

  • 平凡的析构函数

  • 提供constexpr / consteval成员函数

    注意:从C++14起constexpr / consteval成员函数为非const成员函数,之前的标准都是缺省const

示例:

#include <iostream>
//在C++14及之后标准是合法的

class MyLiteral {
public:
    // constexpr构造函数
    constexpr MyLiteral(int v) : x(v) {}
    
    constexpr void inc()
    {
        x = x + 1;
    }
	// constexpr成员函数
    constexpr int read() const
    {
        return x;
    }

private:
    int x;  
};

constexpr int MyFun()
{
    MyLiteral lit(10);
    lit.inc();
    lit.inc();
    lit.inc();
    return lit.read();
}

int main() {
    std::cout << MyFun() << std::endl;
    return 0;
}

MyLiteral类有一个constexpr构造函数和一个constexpr成员函数,允许在编译时创建和操作对象。

2.成员指针

  在C++中,成员指针是一种特殊的指针类型,它指向类的成员(数据成员或成员函数)。

  • 数据成员指针类型示例

    class A {
    public:
        int x;
    };
    
    int A::* ptr_to_member = &A::x; // 指向A类中名为x的数据成员
    
  • 成员函数指针类型示例

    class A {
    public:
        int func(double d) {
            return static_cast<int>(d);
        }
    };
    
    int (A::* ptr_to_member_func)(double) = &A::func; // 指向A类中名为func的成员函数
    
  • 成员指针对象赋值示例

    A obj;
    auto ptr = &A::x; // 直接赋值
    

注意:域操作符子表达式不能加小括号

成员指针的使用

  • 对象.*成员指针

  • 对象指针->*成员指针

3.bind交互

  使用bind + 成员指针构造可调用对象

示例:使用bind + 成员函数指针

#include <iostream>
#include <functional>

class A {
public:
    void func(int x) {
        std::cout << "Value: " << x << std::endl;
    }
};

int main() {
    A obj;
    auto ptr_to_member_func = &A::func;

    // 使用std::bind创建可调用对象
    auto bound_func = std::bind(ptr_to_member_func, &obj, std::placeholders::_1);

    // 调用bound_func,相当于调用obj.func(10)
    bound_func(10);
}

示例:使用bind + 数据成员指针

#include <iostream>
#include <functional>

class A {
public:
    void func(int x) {
        std::cout << "Value: " << x << std::endl;
    }
    
    int x;
};

int main() {
    A obj;
    auto ptr_to_member_func = &A::func;

    // 使用std::bind创建可调用对象
    auto bound_func = std::bind(ptr_to_member_func, &obj, std::placeholders::_1);

    // 调用bound_func,相当于调用obj.func(10)
    bound_func(10);
    
    A obj1;
    auto ptr2 = &A::x;
    obj1.*ptr2 = 3;
    auto data_mem = std::bind(ptr2, &obj1);
    std::cout << data_mem() << std::endl;
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【小白专用 已验证24.6.7】C# MySQL数据库访问操作封装类

一、底层库介绍 本文主要介绍数据库访问操作类&#xff0c;包含&#xff1a;SQL插入脚本、SQL查询脚本、数据库表是否存在判断、带参脚本执行、包含事务回滚脚本执行、存储过程脚本等等。 特殊说明 在使用之前&#xff0c;先安装 MySql.Data 插件 二、底层库源码 2.1 程序源…

android antirollback verno 获取方法

ReadRollbackIndex.exe 获取 调查avbVBMeta结构体 typedef struct AvbVBMetaImageHeader { /* 0: Four bytes equal to "AVB0" (AVB_MAGIC). */ uint8_t magic[AVB_MAGIC_LEN]; /* 4: The major version of libavb required for this header. */ uint32_t…

Linux shell编程学习笔记53: nproc命令:当前进程可以使用 cpu的数量

0 前言 2024年的网络安全检查又开始了&#xff0c;对于使用基于Linux的国产电脑&#xff0c;我们可以编写一个脚本来收集系统的有关信息。对于中央处理器CPU&#xff0c;我们可以使用cat /proc/cpuinfo和 lscpu命令来收集中央处理器CPU的信息&#xff0c;如果我们只想了解和获…

一个月速刷leetcodeHOT100 day15 彻底搞懂回溯算法 以及相关题目

回溯算法采用试错的思想&#xff0c;它尝试分步的去解决一个问题。在分步解决问题的过程中&#xff0c;当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候&#xff0c;它将取消上一步甚至是上几步的计算&#xff0c;再通过其它的可能的分步解答再次尝试寻找问题的…

JavaSE 实战五子棋中国象棋(单机简易版)

介绍 JavaSE实践五子棋和中国象棋游戏&#xff0c;棋盘&#xff0c;棋子绘制&#xff0c;输赢判定重置棋盘&#xff0c;单机博弈。 五子棋棋盘 中国象棋棋盘 使用说明 启动类 Main.java&#xff0c; 面板类 Panel.java绘制棋盘和玩法&#xff0c;实体类 ChessPiecesNode.jav…

工具-金舟投屏软件: 手机如何投屏到电脑上 / Wi-Fi / USB

金舟安卓/iOS苹果投屏-正版软件下载中心 方法一、金舟投屏软件-wifi 1.1、准备工作 确保苹果手机和Windows电脑都连接到同一个Wi-Fi网络。 在Windows电脑上安装并打开金舟投屏软件。 1.2、操作步骤 在金舟投屏软件上选择“苹果手机投屏”功能。 在苹果手机上下滑屏幕&am…

动手学深度学习29 残差网络ResNet

动手学深度学习29 残差网络ResNet ResNet代码ReLU的两种调用1. 使用 torch.nn.ReLU 模块2. 使用 torch.nn.functional.relu 函数总结 QA29.2 ResNet 为什么能训练处1000层的模型ResNet的梯度计算怎么处理梯度消失的 QA ResNet 更复杂模型包含小模型&#xff0c;不一定改进&…

公司刚来的00后真卷,上班还没2年,跳到我们公司起薪20k。。。

前言 现在都说要躺平了&#xff0c;但该说不说的&#xff0c;一样都在卷&#xff0c;你信了就输了。 前段时间我们公司来了个卷王&#xff0c;工作2年左右吧&#xff0c;跳槽到我们公司起薪20K&#xff0c;真的比我还牛。后来才知道人家是真的卷啊&#xff01;都不当人了&…

初识C++ · 模板进阶

目录 前言&#xff1a; 1 非类型模板参数 2 按需实例化 3 模板特化 4 模板的分离编译 前言&#xff1a; 前面模板我们会了简单的使用&#xff0c;这里带来模板的进阶&#xff0c;当然&#xff0c;也就那么几个知识点&#xff0c;并不太难。 1 非类型模板参数 先来看这样…

【Framework系列】Excel转Json,配置表、导表工具介绍

今天来介绍一下Framework系列的配置部分&#xff0c;这一部分归属于Framework-Design之中。读过《Framework系列介绍》的小伙伴应该了解整个Framework框架是由多个工程项目组成&#xff0c;没看过的小伙伴可以点击链接了解一下。 Framework-Design设计的初衷是给策划同学用的&a…

谁懂啊!第一次用AI绘画做表情包,居然直接爆收入了!

大家好&#xff0c;我是设计师阿威 我的第一套表情包上周六上午11点终于在微信的表情商店上架啦&#xff01; 为什么说“终于”&#xff1f; 那是因为背后是无数次的努力–>被退回–>反复修改–>再提交–>再被退回–>再精心修改–>终于通过啦&#xff01;…

Python实现调用并执行Linux系统命令

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主。 &#x1f913; 同时欢迎大家关注其他专栏&#xff0c;我将分享Web前后端开发、人工智能、机器学习、深…

Pyramid Vision Transformer, PVT(ICCV 2021)原理与代码解读

paper&#xff1a;Pyramid Vision Transformer: A Versatile Backbone for Dense Prediction without Convolutions official implementation&#xff1a;GitHub - whai362/PVT: Official implementation of PVT series 存在的问题 现有的 Vision Transformer (ViT) 主要设计…

豆包引领AI大模型PC端新潮流,预示行业薪资待遇与就业前景的广阔前景

前言 在AI大模型技术迅速发展的浪潮中&#xff0c;豆包AI助手凭借其独特的PC端布局&#xff0c;成为了行业的先行者。这一举措不仅体现了对市场需求和用户习惯的深度洞察&#xff0c;更预示着AI大模型领域薪资待遇和就业前景的广阔空间。 豆包AI助手通过推出PC客户端&#x…

tomcat-valve通过servlet处理请求

上一节说到请求url定位servlet的过程&#xff0c;tomcat会把请求url和容器的映射关系保存到MappingData中&#xff0c;org.apache.catalina.connector.Request类实现了HttpServletRequest&#xff0c;其中定义了属性mappingDataprotected final MappingData mappingData new M…

国产Sora免费体验-快手旗下可灵大模型发布

自从OpenAI公布了Sora后&#xff0c;震爆了全世界&#xff0c;但由于其技术的不成熟和应用的局限性&#xff0c;未能大规模推广&#xff0c;只有零零散散的几个公布出来的一些视频。昨日&#xff0c;快手成立13周年&#xff0c;可灵&#xff08;Kling&#xff09;大模型发布&am…

【vue3|第7期】 toRefs 与 toRef 的深入剖析

日期&#xff1a;2024年6月6日 作者&#xff1a;Commas 签名&#xff1a;(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释&#xff1a;如果您觉得有所帮助&#xff0c;帮忙点个赞&#xff0c;也可以关注我&#xff0c;我们一起成长&#xff1b;如果有不对的地方&#xff…

vs2017中C2440错误:“初始化”:无法从const char[6]转换为char*问题解决

本文摘要&#xff1a;本文已解决 Python FileNotFoundError 的相关报错问题&#xff0c;并总结提出了几种可用解决方案。同时结合人工智能GPT排除可能得隐患及错误。 &#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领…

LLM系列: LLama2

推理流程 从输入文本&#xff0c;到推理输出文本&#xff0c;LLama2模型处理流程如下&#xff1a; step1 Tokenization 输入数据&#xff1a;一个句子或一段话。通常表示成单词或字符序列。 Tokenization即对文本按单词或字符序列切分&#xff0c;形成Token序列。Token序列再…

【Vue2源码学习分析】

# 文件结构 源码目录 # 调试环境搭建 安装依赖: npm i安装rollup: npm i -g rollup修改dev脚本&#xff0c;添加sourcemap&#xff0c;package.json "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web- full-dev",运行开发命令…