类和对象(中)

1. 类的默认成员函数

2. 构造函数

构造函数是特殊的成员函数,其核心任务是初始化对象,而非开辟空间。

构造函数的特点
  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时自动调用。
  4. 构造函数可以重载。
  5. 未显式定义时,编译器自动生成一个无参默认构造函数;用户显式定义后,编译器不再生成。
  6. 默认构造函数:无参构造、全缺省构造、编译器生成的构造。三者有且只能有一个存在(无参与全缺省同时存在会导致调用歧义)。不传实参即可调用的构造就是默认构造。
  7. 编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。如果这个成员变量,没有默认构造函数,那么就会报错,我们要初始化这个成员变量,需要⽤初始化列表才能解决。

说明: C++把类型分成内置类型(基本类型)和⾃定义类型。内置类型就是语⾔提供的原⽣数据类型,

如:int/char/double/指针等,⾃定义类型就是我们使⽤class/struct等关键字⾃⼰定义的类型。


代码示例:

cpp 复制代码
#include <iostream>
using namespace std;

class Date {
public:
    // 1. 无参构造函数(默认构造)
    Date() {
        _year = 2000;
        _month = 1;
        _day = 1;
    }

    // 2. 带参构造函数
    Date(int year, int month, int day) {
        _year = year;
        _month = month;
        _day = day;
    }

    // 3. 全缺省构造函数(也是默认构造)
    // 注意:不能与无参构造同时存在,会产生调用歧义
    Date(int year = 2024, int month = 6, int day = 15) {
        _year = year;
        _month = month;
        _day = day;
    }

    void Print() {
        cout << _year << "-" << _month << "-" << _day << endl;
    }

private:
    int _year;
    int _month;
    int _day;
};

int main() {
    Date d1;              // 调用无参构造
    Date d2(2025, 8, 11); // 调用带参构造
    d1.Print();
    d2.Print();
    return 0;
}

3. 析构函数

析构函数与构造函数功能相反,对象销毁时会⾃动调⽤析构函数,完成对象中资源的清理⼯作。

析构函数的特点:
  1. 函数名:类名前加 ~
  2. ⽆参数、⽆返回值。
  3. ⼀个类只能有⼀个析构函数。未显式定义时,系统⽣成默认析构函数。
  4. 对象⽣命周期结束时,⾃动调⽤析构函数。
  5. 默认析构函数:对内置类型成员不做处理,对⾃定义类型成员调⽤其析构函数。
  6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类
    型成员⽆论什么情况都会⾃动调⽤析构函数。
  7. 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数。但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏。
  8. ⼀个局部域的多个对象,C++规定后定义的先析构。

代码示例:

cpp 复制代码
#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass() {
        _data = new int[10];  // 申请资源
        cout << "构造函数:申请资源" << endl;
    }

    ~MyClass() {
        delete[] _data;       // 释放资源
        _data = nullptr;
        cout << "析构函数:释放资源" << endl;
    }

private:
    int* _data;
};

int main() {
    MyClass obj;  // 构造
    // 离开作用域,自动调用析构函数释放资源
    return 0;
}

4. 拷⻉构造函数

如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数是拷⻉构造函数。

拷⻉构造的特点:
  1. 拷⻉构造函数是构造函数的一个重载。
  2. 拷⻉构造函数的第一个参数必须是类类型对象的引用,使用传值方式编译器直接报错(会引发无穷递归调用)。可以有多个参数,但第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
  3. C++规定自定义类型对象进行拷贝必须调用拷贝构造,因此传值传参和传值返回都会调用拷贝构造。
  4. 若未显式定义拷贝构造,编译器会自动生成。自动生成的拷贝构造对内置类型成员完成值拷贝/浅拷贝,对自定义类型成员调用其拷贝构造。
  5. 技巧:若类显式实现了析构并释放资源,则通常需要显式写拷贝构造(深拷贝),否则不需要。
  6. 传值返回会产⽣⼀个临时对象调⽤拷贝构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。

代码示例------浅拷贝问题与深拷贝:

cpp 复制代码
#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass(int size = 10) {
        _size = size;
        _data = new int[_size];  // 申请资源
        cout << "构造" << endl;
    }

    // 拷贝构造(深拷贝)------ 必须显式实现,否则默认浅拷贝会导致重复释放
    MyClass(const MyClass& other) {
        _size = other._size;
        _data = new int[_size];  // 重新申请资源
        for (int i = 0; i < _size; i++) {
            _data[i] = other._data[i];  // 拷贝数据
        }
        cout << "深拷贝构造" << endl;
    }

    ~MyClass() {
        delete[] _data;
        _data = nullptr;
        cout << "析构" << endl;
    }

private:
    int* _data;
    int _size;
};

int main() {
    MyClass obj1(20);
    MyClass obj2(obj1);  // 调用拷贝构造(深拷贝)
    return 0;
}

5. 赋值运算符重载

5.1 运算符重载

  • C++允许为类类型对象重载运算符,指定其新含义。使用运算符时,必须调用对应的重载函数,否则编译报错。
  • 运算符重载是特殊函数,名为 operator 后接运算符符号,具有返回类型、参数列表和函数体。
  • 参数个数与运算对象数量相同:一元运算符一个参数,二元运算符两个参数(左侧对象传第一个参数,右侧对象传第二个)。
  • 若为成员函数,第一个运算对象隐式传给 this 指针,因此参数比运算对象少一个。
  • 重载后运算符的优先级和结合性与内置类型保持一致。
  • 不能通过连接语法中没有的符号来创建新的操作符:⽐如operator@。
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。(选择题⾥⾯常考,⼤家要记⼀下)
  • 重载操作符⾄少有⼀个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:int operator+(int x, int y);
  • ⼀个类需要重载哪些运算符,是看哪些运算符重载后有意义。
  • 重载++运算符时,有前置++和后置++,运算符重载函数名都是operator++,⽆法很好的区分。C++规定,后置++重载时,增加⼀个int形参,跟前置++构成函数重载,⽅便区分。
  • 重载<<和>>时,需要重载为全局函数,因为重载为成员函数,this指针默认抢占了第⼀个形参位置,第⼀个形参位置是左侧运算对象,调⽤时就变成了对象<<cout,不符合使⽤习惯和可读性。重载为全局函数把ostream/istream放到第⼀个形参位置就可以了,第⼆个形参位置当类类型对象。

5.2 赋值运算符重载

赋值运算符重载是⼀个默认成员函数,⽤于完成两个已经存在的对象之间的赋值。注意与拷贝构造函数区分:拷贝构造是在创建新对象时进行初始化。

赋值运算符重载的特点:
  1. 必须重载为成员函数。参数建议写成 const 当前类类型&,避免传值拷贝。
  2. 建议返回 当前类类型&,支持连续赋值并提高效率。
  3. 未显式实现时,编译器生成默认赋值运算符重载:对内置类型值拷贝(浅拷贝),对自定义类型调用其赋值重载。
  4. 技巧:若类显式实现了析构函数释放资源,则通常也需要显式实现赋值运算符重载。

代码示例:

cpp 复制代码
#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass(int size = 10) {
        _size = size;
        _data = new int[_size];
    }

    // 拷贝构造(深拷贝)
    MyClass(const MyClass& other) {
        _size = other._size;
        _data = new int[_size];
        for (int i = 0; i < _size; i++) {
            _data[i] = other._data[i];
        }
    }

    // 赋值运算符重载
    MyClass& operator=(const MyClass& other) {
        // 1. 检查自赋值
        if (this != &other) {
            // 2. 释放原有资源
            delete[] _data;
            // 3. 深拷贝
            _size = other._size;
            _data = new int[_size];
            for (int i = 0; i < _size; i++) {
                _data[i] = other._data[i];
            }
        }
        return *this;  // 4. 返回*this,支持连续赋值
    }

    ~MyClass() {
        delete[] _data;
    }

private:
    int* _data;
    int _size;
};

int main() {
    MyClass obj1(20);
    MyClass obj2(30);
    obj2 = obj1;  // 调用赋值运算符重载
    return 0;
}

6.取地址运算符重载

6.1const成员函数

  • 声明方式:const 修饰符放在成员函数参数列表之后。
  • 核心作用:const 修饰隐含的 this 指针,禁止在函数内修改类的任何成员。

6.2 取地址运算符重载

取地址运算符重载分为普通取地址运算符重载和const取地址运算符重载,通常编译器自动生成的版本已足够使用,无需显式实现。

代码示例:

cpp 复制代码
#include <iostream>
using namespace std;

class MyClass {
public:
    MyClass(int val = 0) { _val = val; }

    // 普通取地址运算符重载
    MyClass* operator&() {
        cout << "普通 operator& 调用" << endl;
        return this;  // 直接返回当前对象的地址
    }

    // const 取地址运算符重载
    const MyClass* operator&() const {
        cout << "const operator& 调用" << endl;
        return this;  // 返回 const 对象的地址
    }

    void Print() const { cout << "val = " << _val << endl; }

private:
    int _val;
};

int main() {
    MyClass obj(10);
    MyClass* p1 = &obj;             // 调用普通 operator&
    cout << "普通指针地址:" << p1 << endl;

    const MyClass obj2(20);
    const MyClass* p2 = &obj2;      // 调用 const operator&
    cout << "const 指针地址:" << p2 << endl;

    return 0;
}

注意: 通常编译器自动生成的取地址运算符重载已足够。 除非有特殊需求(如不希望外部获取对象地址),才需要手动重载并返回 nullptr 或其他值。

相关推荐
野生yumeko1 小时前
QtScrcpy电脑无线控制手机屏幕
经验分享·笔记·智能手机
wuminyu2 小时前
JDK21解决虚拟线程IO阻塞原理剖析
java·linux·c语言·jvm·c++
MC皮蛋侠客2 小时前
TDengine C++ 系列(4):时序查询——窗口聚合、插值、连接与时序函数
大数据·c++·tdengine
junuo2 小时前
10_C++内存分区与动态内存管理
c++·编程语言
欧特克_Glodon2 小时前
OpenCV计算机视觉开发入门与实践<二>:图像噪声与图像处理
c++·图像处理·opencv·计算机视觉
键盘会跳舞2 小时前
C++:函数对象与 std::function 源码级深度拆解——泛型算法的策略内核与可调用对象统一封装
c++·算法·仿函数
xian_wwq2 小时前
【学习笔记】Loop Engineering,从手动提示到目标驱动自动化-13/16
笔记·学习·自动化
Augustzero3 小时前
为什么高性能调度器都在“偷任务”?从无锁队列看懂工作窃取
c++·后端
(Charon)3 小时前
【C++】线程安全队列(三):MPSC无锁队列、atomic与链表节点实现
c++·安全·链表