一、结构体与对象聚合

  结构体是从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;
}

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部