C++ 类和对象(二)

 

目录

1.前言

2.类的六个默认成员函数

3.构造函数

3.1概念

3.2特性

3.2.1 函数名与类名相同

3.2.2 无返回值

 3.2.3对象实例化时自动调用

 3.2.4 构造函数可以重载

3.2.5 默认构造函数的自动生成

3.2.6 默认构造函数对内置类型成员的初始化

3.2.7 默认构造函数的定义

4.析构函数

4.1概念

4.2特性

4.2.1析构函数名是在类名前加上字符 ~

4.2.2 无参数无返回值类型

4.3.3 一个类只能有一个析构函数

4.3.4 对象生命周期结束时,析构函数自动调用

 4.3.5 编译器自动生成的析构函数

4.3.6 资源管理和析构函数

5.拷贝构造函数

5.1概念

5.2特性

5.2.1 拷贝构造函数是构造函数的一个重载形式

5.2.2 参数必须是类类型对象的引用

5.2.3 编译器生成的默认拷贝构造函数

5.2.4 需要显式实现拷贝构造函数的情况

5.2.5 拷贝构造函数典型调用场景

6.赋值运算符重载

6.1运算符重载

6.2赋值运算符重载

6.2.1 赋值运算符重载格式

6.2.2 赋值运算符只能重载为成员函数

6.2.3 编译器生成的默认赋值运算符

6.3前置++与后置++重载

6.3.1前置++重载

6.3.2后置++重载

7.日期类的实现

8.类的const成员

9.取地址操作符重载

10.小结


(图像由AI生成) 

1.前言

在C++ 类和对象(一)中,我们引入了类的定义,并介绍了类的一些相关知识,如类的作用域、实例化和储存等。在本篇博客中,我们将深入探讨C++中类的默认成员函数、构造函数、析构函数、拷贝构造函数、赋值运算符重载以及其他高级特性,进一步揭开C++中“类与对象”的面纱。

2.类的六个默认成员函数

在C++中,即使一个类看起来是空的(即没有声明任何成员变量或成员函数),编译器仍会自动为其生成一些默认成员函数,这使得该类能够进行基本的操作。这些成员函数是:

  1. 默认构造函数:如果没有其他构造函数被声明,编译器将生成一个无参的默认构造函数,用于创建类的对象。

  2. 析构函数:用于在对象生命周期结束时进行清理工作。如果用户没有显式定义析构函数,编译器会提供一个默认的析构函数。

  3. 拷贝构造函数:当新对象需要通过现有对象进行初始化时,拷贝构造函数被调用。如果用户没有定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数,进行成员到成员的拷贝。

  4. 拷贝赋值运算符(=):当一个对象需要通过已存在的同类型的另一个对象进行赋值时,这个运算符被调用。编译器会提供一个默认的拷贝赋值运算符,它执行成员到成员的赋值。

  5. 取地址运算符重载(&):允许对象的地址被取出。编译器自动生成的取地址运算符返回对象的实际内存地址。

  6. const对象取地址运算符重载(const &):这是取地址运算符的一个特殊形式,用于在对象为const时获取其地址。编译器生成的此函数保证即使是const对象,也能安全地获取其地址。

这些自动生成的函数确保了即使是最简单的类也能在C++环境中正确地存储、复制和访问。这种设计反映了C++语言的一项基本原则——提供安全且易于使用的默认行为,同时允许程序员通过显式定义来覆盖这些行为,以满足特定的程序需求。

3.构造函数

3.1概念

类的构造函数是类的一种特殊的成员函数,它会在每次创建类类型对象时由编译器自动调用,以保证 每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次

构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。

以下面代码为例:

class Box {
public:
    // Default constructor
    Box() {}

    // Initialize a Box with equal dimensions (i.e. a cube)
    explicit Box(int i) : m_width(i), m_length(i), m_height(i) // member init list
    {}

    // Initialize a Box with custom dimensions
    Box(int width, int length, int height)
        : m_width(width), m_length(length), m_height(height)
    {}

    int Volume() { return m_width * m_length * m_height; }

private:
    // Will have value of 0 when default constructor is called.
    // If we didn't zero-init here, default constructor would
    // leave them uninitialized with garbage values.
    int m_width{ 0 };
    int m_length{ 0 };
    int m_height{ 0 };
};

其中:Box(){}即为Box类的一个默认构造函数。至于什么是默认构造函数?Box类中其他几个函数是什么构造函数?且听我细细道来。 

3.2特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任 务并不是开空间创建对象,而是初始化对象。 其特征如下:

3.2.1 函数名与类名相同

构造函数必须与其所在类的名称相同。这是C++用于区分构造函数与其他成员函数的一种约定。

class Widget {
public:
    int size;
    // 构造函数名称与类名相同
    Widget() {
        size = 10;
    }
};

int main() {
    Widget w;
    std::cout << "Widget size: " << w.size << std::endl;  // 输出:Widget size: 10
    return 0;
}

3.2.2 无返回值

构造函数不能有返回值,甚至不能写 void。它的目的是初始化对象,而不是提供返回值。

class Widget {
public:
    int size;
    // 构造函数无返回值
    Widget() {
        size = 10;
    }
};

 3.2.3对象实例化时自动调用

构造函数是在创建类的对象时自动调用的。这确保了对象一被创建就处于一个有效的状态。

class Widget {
public:
    int size;
    Widget() {
        size = 10;
        std::cout << "Constructor called automatically on object creation." << std::endl;
    }
};

int main() {
    Widget w;  // 自动调用构造函数
    return 0;
}

 3.2.4 构造函数可以重载

类可以有多个构造函数,只要它们的参数数量或类型不同即可。

class Widget {
public:
    int size;
    std::string color;

    // 无参数构造函数
    Widget() : size(10), color("black") {}

    // 重载构造函数,有参数
    Widget(int s, std::string c) : size(s), color(c) {}

};

int main() {
    Widget w1;  // 调用无参数构造函数
    Widget w2(15, "red");  // 调用有参数的构造函数
    std::cout << "w1: size=" << w1.size << ", color=" << w1.color << std::endl;
    std::cout << "w2: size=" << w2.size << ", color=" << w2.color << std::endl;
    return 0;
}

3.2.5 默认构造函数的自动生成

如果类中未显式定义任何构造函数,C++编译器会自动生成一个无参的默认构造函数。

class Widget {
public:
    int size;
};

int main() {
    Widget w;
    std::cout << "Default constructed Widget size: " << w.size << std::endl;  // 未初始化的size值
    return 0;
}

3.2.6 默认构造函数对内置类型成员的初始化

在C++11及更高版本中,可以在类定义中为内置类型成员指定默认值。

class Widget {
public:
    int size = 10;  // 内置类型的成员初始化
};

int main() {
    Widget w;
    std::cout << "Initialized Widget size: " << w.size << std::endl;  // 输出:Initialized Widget size: 10
    return 0;
}

3.2.7 默认构造函数的定义

无参数的构造函数和全缺省参数的构造函数都视为默认构造函数。每个类只能有一个默认构造函数。

class Widget {
public:
    int size;
    // 默认构造函数
    Widget(int s = 10) : size(s) {}

};

int main() {
    Widget w;  // 使用默认构造函数
    std::cout << "Widget size: " << w.size << std::endl;  // 输出:Widget size: 10
    return 0;
}

4.析构函数

析构函数在C++中是类的一个特殊成员函数,它与构造函数的作用相反。构造函数用于对象的创建和初始化,而析构函数则用于对象的清理和释放资源。析构函数确保在对象生命周期结束时,所有由该对象持有的资源都能被妥善处理。

4.1概念

析构函数是一个在对象即将被销毁时自动调用的特殊成员函数,其主要任务是执行清理操作,以防资源泄露(如内存泄漏、文件未关闭等)。析构函数的命名规则非常简单:它的名称是类名前加一个波浪符(~),比如 ~ClassName()

4.2特性

析构函数在C++中是用来处理对象销毁前的清理工作的特殊成员函数。下面详细介绍析构函数的几个关键特性,并通过具体的代码示例加以说明。

4.2.1析构函数名是在类名前加上字符 ~

析构函数的名称由类名前加波浪号(~)组成,这标志着它的特殊作用:清理对象。

class Sample {
public:
    // 析构函数
    ~Sample() {
        std::cout << "Destructor called." << std::endl;
    }
};

4.2.2 无参数无返回值类型

析构函数不能接受参数,也不返回任何值,这保证了析构函数的调用是简单且自动的。

class Sample {
public:
    ~Sample() {
        // 无参数,无返回值
    }
};

4.3.3 一个类只能有一个析构函数

一个类中只能定义一个析构函数,它不能被重载,这保证了析构行为的唯一性和简单性。

class Sample {
public:
    // 唯一的析构函数
    ~Sample() {
        std::cout << "Destructor called." << std::endl;
    }
};

4.3.4 对象生命周期结束时,析构函数自动调用

当对象的生命周期结束(如局部对象的作用域结束,或者通过delete删除动态对象)时,C++编译系统自动调用析构函数。

class Sample {
public:
    ~Sample() {
        std::cout << "Destructor called." << std::endl;
    }
};

int main() {
    Sample obj; // 当obj的生命周期结束时,析构函数被调用
}

 4.3.5 编译器自动生成的析构函数

如果未显式定义析构函数,编译器会自动生成一个默认的析构函数。这个默认析构函数会调用其成员变量(如果它们是对象)的析构函数。

class Component {
public:
    ~Component() {
        std::cout << "Component destructor called." << std::endl;
    }
};

class Composite {
public:
    Component c;
    // 默认析构函数自动调用成员c的析构函数
};

int main() {
    Composite obj; // 析构时会先调用c的析构函数,然后是obj的
}

4.3.6 资源管理和析构函数

如果类中包含资源申请(如动态内存、文件句柄等),需要显式定义析构函数来释放这些资源。否则,可以依赖编译器生成的默认析构函数。

class Date {
    // 使用默认析构函数,因为没有特殊资源需要释放
};

class Stack {
    int* data;
public:
    Stack() {
        data = new int[10]; // 申请资源
    }
    ~Stack() {
        delete[] data; // 必须显式释放资源
    }
};

int main() {
    Date d; // 使用默认析构函数
    Stack s; // 使用自定义析构函数释放动态分配的内存
}

5.拷贝构造函数

拷贝构造函数在C++中扮演着重要的角色,它定义了一个对象如何通过使用另一个已存在的同类型对象进行初始化。在深入研究C++的对象复制机制时,理解拷贝构造函数的工作原理至关重要。

5.1概念

拷贝构造函数是一个特殊的构造函数,它在创建一个新对象时,使用同类型的另一个已存在对象作为其初始化源。拷贝构造函数通常具有以下特点:

  • 单一形参:拷贝构造函数通常有一个形参,该形参是对本类类型对象的引用。一般来说,这个参数会使用const修饰符来防止被修改,这是因为在拷贝一个对象时,通常不希望改变源对象。
  • 自动调用:在特定情况下,拷贝构造函数会被编译器自动调用。这包括使用一个对象来初始化另一个对象、将对象作为参数传递给函数(按值传递),或从函数返回一个对象时(虽然现代编译器优化了这一过程,通过返回值优化(RVO)避免不必要的拷贝)。

5.2特性

5.2.1 拷贝构造函数是构造函数的一个重载形式

拷贝构造函数是一种特殊的构造函数,用于从同类的另一个对象初始化新对象。作为构造函数的一种,它遵循构造函数的基本原则,但专门用于处理对象的复制。

5.2.2 参数必须是类类型对象的引用

拷贝构造函数的参数是一个对同类对象的引用。如果尝试使用传值方式定义参数,会导致编译错误,因为这样会触发无限递归的拷贝构造调用。

class Widget {
public:
    int data;
    // 错误示例:尝试以传值方式接收参数
    // Widget(Widget w) { }  // 会导致编译错误

    // 正确的拷贝构造函数
    Widget(const Widget& w) : data(w.data) {
        std::cout << "Copy constructor called." << std::endl;
    }
};

5.2.3 编译器生成的默认拷贝构造函数

如果未显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。这个默认的拷贝构造函数执行所谓的"浅拷贝",即直接按字节复制数据。对于内置数据类型,这意味着值会被直接复制;对于包含自定义类型的对象,其相应的拷贝构造函数会被调用。

class Simple {
public:
    int *ptr;

    Simple(int val) {
        ptr = new int(val);
    }

    // 默认拷贝构造函数会导致ptr指向相同的内存地址,即浅拷贝
};

int main() {
    Simple obj1(42);
    Simple obj2 = obj1; // 使用默认拷贝构造函数

    std::cout << "Original: " << *obj1.ptr << " Copy: " << *obj2.ptr << std::endl;
    // 改变obj1将影响obj2
    *obj1.ptr = 21;
    std::cout << "Original: " << *obj1.ptr << " Copy: " << *obj2.ptr << std::endl;
}

 

5.2.4 需要显式实现拷贝构造函数的情况

当类包含资源如动态内存时,应显式定义拷贝构造函数以实现深拷贝,避免资源泄漏或多重释放。

class Stack {
    int* data;
    int size;
public:
    Stack(int sz) : size(sz), data(new int[sz]) { }  // 分配资源

    // 实现深拷贝
    Stack(const Stack& s) : size(s.size), data(new int[s.size]) {
        std::copy(s.data, s.data + s.size, data);
    }

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

int main() {
    Stack s1(10);
    Stack s2 = s1; // 使用自定义的深拷贝拷贝构造函数
}

5.2.5 拷贝构造函数典型调用场景

拷贝构造函数在多种场景下被调用:

  • 使用已存在对象创建新对象
  • 函数参数类型为类类型对象(按值传递)
  • 函数返回值类型为类类型对象(虽然通常优化掉)
void display(Widget w) {  // 按值传递会调用拷贝构造函数
    std::cout << "Displaying data: " << w.data << std::endl;
}

Widget createWidget() {
    Widget w(10);
    return w;  // 返回对象可能调用拷贝构造函数(通常优化为移动构造)
}

int main() {
    Widget a(20);
    Widget b = a;  // 直接使用拷贝构造函数
    display(a);  // 调用时使用拷贝构造函数
    Widget c = createWidget();  // 可能使用拷贝构造函数
}

6.赋值运算符重载

在C++中,运算符重载允许程序员为用户定义的类型指定操作符的行为。这样可以使得用户定义的类型在表达式中使用起来更自然,提高代码的直观性和可读性。赋值运算符重载是这其中一个非常常用的例子。

6.1运算符重载

运算符重载基本上是一种特殊的函数,它使用特定的语法格式定义。其目的是让用户定义的类型能够使用标准C++运算符进行操作。运算符重载可以让对象之间的操作更加符合直觉,例如,通过重载+运算符使得两个复数对象可以直接相加。

基本语法:

ReturnType operatorOp(ArgType arg);

这里ReturnType是运算符重载函数的返回类型,operatorOp是关键字operator后跟需要重载的运算符,而ArgType是参数类型。

注意点:

  • 不能创建新的操作符:C++不允许创造新的运算符。例如,operator@是不合法的。
  • 至少一个操作数是用户定义的类型:这确保了重载运算符是针对用户定义的类型,而不是内置类型。
  • 不能改变内置类型运算符的行为:例如,不能为内置的int类型重载+运算符来改变其原本的加法行为。
  • 某些运算符不能重载:包括.(点), .*, ::(作用域限定符), sizeof ?:(三目运算符)

6.2赋值运算符重载

6.2.1 赋值运算符重载格式

赋值运算符通常被重载为一个成员函数,以控制对象如何处理赋值操作。这个函数通常遵循以下的格式:

  • 参数类型const T&,使用常量引用作为参数类型,这样可以提高参数传递的效率,并避免不必要的对象复制。
  • 返回值类型T&,返回对象自身的引用。这样做可以提高效率,并且支持连续赋值,例如a = b = c;
  • 自赋值检查:检查赋值是否是对象自身赋值给自己,如果是,则直接返回,避免不必要的操作和潜在错误。
  • 返回*this:函数通过返回当前对象的引用(*this)来支持连续赋值。

示例:日期类的赋值运算符重载

class Date {
public:
    int day, month, year;

    Date(int d = 1, int m = 1, int y = 1970): day(d), month(m), year(y) {}

    // 赋值运算符重载
    Date& operator=(const Date& other) {
        if (this != &other) {  // 避免自赋值
            day = other.day;
            month = other.month;
            year = other.year;
        }
        return *this;  // 返回当前对象的引用
    }
};

int main() {
    Date date1(10, 10, 2010);
    Date date2;
    date2 = date1;  // 调用赋值运算符
    Date date3;
    date3 = date2 = date1;  // 连续赋值
    return 0;
}

6.2.2 赋值运算符只能重载为成员函数

赋值运算符必须作为类的成员函数来重载。它不能被定义为全局函数因为赋值运算符需要访问对象的内部数据

6.2.3 编译器生成的默认赋值运算符

如果开发者没有显式实现赋值运算符,编译器将自动生成一个默认的赋值运算符。这个默认赋值运算符会执行浅拷贝,逐字节地复制对象的内容。对于类的成员变量:

  • 内置类型的成员变量会直接被赋值。
  • 自定义类型的成员变量将调用其各自的赋值运算符来完成赋值。

这种默认行为通常足够处理简单的类,但对于涉及动态内存分配或其他需要显式管理的资源的类,通常需要自定义赋值运算符以实现深拷贝,防止资源泄露或重复释放问题。

6.3前置++与后置++重载

在C++中,递增运算符++可以以两种形式存在:前置++和后置++。这两者在功能上相似,但在表达式中的行为和返回值有所不同。通过对这两种形式进行重载,可以让用户定义的类型(如日期类)使用这些运算符以符合直觉的方式递增其值。

6.3.1前置++重载

前置++重载通常返回对象的引用,并且递增对象的值之前就完成了递增操作。这意味着它返回的是递增后的对象。

语法:

T& operator++();

6.3.2后置++重载

后置++重载在表达式中稍微复杂一些,因为它需要返回递增前的对象的一个副本。这需要在递增操作执行后进行。为了区分前置和后置,后置++接受一个额外的int参数,但这个参数在实际中并不提供(只是用来区分前置和后置)。

语法:

T operator++(int);

让我们以一个简单的日期类为例,展示如何重载这两个运算符。

#include <iostream>

class Date {
public:
    int day, month, year;

    Date(int d = 1, int m = 1, int y = 1970): day(d), month(m), year(y) {}

    // 前置++重载
    Date& operator++() {
        ++day; // 增加天数
        normalize(); // 调整日期
        return *this;
    }

    // 后置++重载
    Date operator++(int) {
        Date temp = *this; // 保存当前状态
        ++day; // 增加天数
        normalize(); // 调整日期
        return temp; // 返回未修改前的日期
    }

    void normalize() {
        // 假设每月30天简化逻辑
        if (day > 30) {
            day = 1;
            ++month;
            if (month > 12) {
                month = 1;
                ++year;
            }
        }
    }

    void print() {
        std::cout << day << "/" << month << "/" << year << std::endl;
    }
};

int main() {
    Date date(30, 12, 1999);
    date.print();  // 输出:30/12/1999
    ++date;        // 使用前置++
    date.print();  // 输出:1/1/2000
    date++;        // 使用后置++
    date.print();  // 输出:2/1/2000
    return 0;
}

在这个示例中:

  • 前置++:先递增day,然后通过调用normalize()方法确保日期保持有效,最后返回递增后的对象。
  • 后置++:先保存当前对象状态,再递增day,通过normalize()调整日期,最后返回原始日期状态的副本。

7.日期类的实现

下面我们给出一种日期类的完整代码实现:

date.h

#pragma once
#include<iostream>
using namespace std;

class Date
{
private:
	int _day;
	int _month;
	int _year;
public:
	Date(int year = 2000, int month = 1, int day = 1);//构造函数
	Date(const Date& d);//拷贝构造函数
	~Date();//析构函数
	Date& operator=(const Date& d);//赋值运算符重载

	void Print()const;//打印日期

	bool operator==(const Date&d)const;//等于运算符重载
	bool operator<(const Date&d)const;//小于运算符重载
	bool operator<=(const Date&d)const;//小于等于运算符重载
	bool operator>(const Date&d)const;//大于运算符重载
	bool operator>=(const Date&d)const;//大于等于运算符重载
	bool operator!=(const Date&d)const;//不等于运算符重载

	int getDayOfMonth(int year, int month)const;//获取某年某月的天数
	Date& operator+=(int day);//+=运算符重载
	Date operator+(int day)const;//+运算符重载
	Date& operator++();//前置++运算符重载
	Date operator++(int);//后置++运算符重载
	Date& operator-=(int day);//-=运算符重载
	Date operator-(int day)const;//-运算符重载
	Date& operator--();//前置--运算符重载
	Date operator--(int);//后置--运算符重载

	int operator-(const Date&d)const;//-运算符重载

	friend ostream& operator<<(ostream& _cout, const Date& d);//<<运算符重载
	friend istream& operator>>(istream& _cin, Date& d);//>>运算符重载
};

date.cpp

#include"date.h"

//构造函数
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}
//拷贝构造函数
Date::Date(const Date& d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}
//析构函数(在日期类中没啥用)
Date::~Date()
{
	_year = 2000;
	_month = 1;
	_day = 1;
}
//赋值运算符重载
Date& Date::operator=(const Date& d)
{
	if (this != &d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	return *this;

}
//打印日期
void Date::Print()const
{
	cout << _year << "-" << _month << "-" << _day << endl;
}

//等于运算符重载
bool Date::operator==(const Date& d)const
{
	return _year == d._year && _month == d._month && _day == d._day;
}
//小于运算符重载
bool Date::operator<(const Date& d)const
{
	if (_year < d._year)
		return true;
	else if (_year == d._year)
	{
		if (_month < d._month)
			return true;
		else if (_month == d._month)
		{
			if (_day < d._day)
				return true;
		}
	}
	return false;
}
//小于等于运算符重载
bool Date::operator<=(const Date& d)const
{
	return *this < d || *this == d;
}
//大于运算符重载
bool Date::operator>(const Date& d)const
{
	return !(*this <= d);
}
//大于等于运算符重载
bool Date::operator>=(const Date& d)const
{
	return !(*this < d);
}
//不等于运算符重载
bool Date::operator!=(const Date& d)const
{
	return !(*this == d);
}
//获取某年某月的天数
int Date::getDayOfMonth(int year, int month)const
{
	int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
	if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
	{
		days[2] = 29;
	}
	return days[month];
}
//+=运算符重载
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= -day;
	}
	_day += day;
	while (_day > getDayOfMonth(_year, _month))
	{
		_day -= getDayOfMonth(_year, _month);
		_month++;
		if (_month > 12)
		{
			_month = 1;
			_year++;
		}
	}
	return *this;
}
//+运算符重载
Date Date::operator+(int day)const
{
	Date ret(*this);
	ret += day;
	return ret;
}
//前置++运算符重载
Date& Date::operator++()
{
	*this += 1;
	return *this;
}
//后置++运算符重载
Date Date::operator++(int)
{
	Date ret(*this);
	*this += 1;
	return ret;
}
//-=运算符重载
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += -day;
	}
	_day -= day;
	while (_day < 1)
	{
		_month--;
		if (_month < 1)
		{
			_month = 12;
			_year--;
		}
		_day += getDayOfMonth(_year, _month);
	}
	return *this;
}
//-运算符重载
Date Date::operator-(int day)const
{
	Date ret(*this);
	ret -= day;
	return ret;
}
//前置--运算符重载
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}
//后置--运算符重载
Date Date::operator--(int)
{
	Date ret(*this);
	*this -= 1;
	return ret;
}

int Date::operator-(const Date& d)const
{
	Date max = *this;
	Date min = d;
	int flag = 1;
	if (max < min)
	{
		max = d;
		min = *this;
		flag = -1;
	}
	int count = 0;
	while (min < max)
	{
		min++;
		count++;
	}
	return count * flag;
}

//<<运算符重载
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}

//>>运算符重载
istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year >> d._month >> d._day;
	return _cin;
}

8.类的const成员

在C++中,const关键字用于声明不可修改的变量。当const修饰一个类的成员函数时,它影响的是这个函数对类成员的操作权限,即确保成员函数不会修改对象的任何成员变量。这种函数被称为const成员函数。

const成员函数的特性

语法:

ReturnType FunctionName(Parameters) const;
  • const位于成员函数的参数列表之后,函数体之前,表示这个函数不会改变任何类的成员变量的状态。
  • 实际上,const修饰的是成员函数隐含的this指针。this指针是一个指向当前对象的指针,用于访问类的成员。在const成员函数中,this指针变为指向const对象的指针,这意味着你不能通过这个this指针修改任何成员变量。

为什么使用const成员函数

  1. 安全保证:const成员函数提供了一个明确的语义保证,即不会修改对象。这是在编程中进行接口设计时非常有用的,特别是在多线程环境下,保证数据的读取不会引起数据状态的改变。

  2. 使用const对象:只有const成员函数才能被const对象调用。如果你有一个const的类对象,你只能调用它的const成员函数。

  3. 逻辑上的正确性:如果一个函数不应改变对象状态,将其声明为const成员函数有助于防止编程错误,这样编译器就可以帮助确保函数的行为符合预期。

示例:使用const成员函数的日期类(详见 7.日期类的实现)

9.取地址操作符重载

在C++中,取地址操作符&通常用来获取变量或对象的内存地址。然而,在特定的情况下,你可能需要对类对象的取地址操作进行特殊处理,比如管理对象在内存中的布局或记录访问情况。为此,可以重载取地址操作符。此外,你还可以重载对const对象的取地址操作符,以适应对const对象的取地址请求。

取地址操作符&的重载

取地址操作符可以被重载为成员函数,来自定义对象地址的获取方式。

语法:

T* operator&(); 
const T* operator&() const;

第一个版本用于非const对象,第二个版本用于const对象。这两个版本确保即使在const环境下,也能正确返回对象的地址。

示例代码

下面是一个简单的示例,展示如何重载取地址操作符,以及如何为const和非const对象分别处理:

#include <iostream>

class Widget {
private:
    int data;

public:
    Widget(int d = 0) : data(d) {}

    // 重载非const对象的取地址操作符
    Widget* operator&() {
        std::cout << "Non-const & operator called." << std::endl;
        return this;
    }

    // 重载const对象的取地址操作符
    const Widget* operator&() const {
        std::cout << "Const & operator called." << std::endl;
        return this;
    }
};

int main() {
    Widget w(10);
    const Widget cw(20);

    // 获取非const对象的地址
    Widget* pw = &w;

    // 获取const对象的地址
    const Widget* pcw = &cw;

    return 0;
}

但是,这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容。

10.小结

在本博客中,我们深入探讨了C++中类的多个关键概念和特性,包括类的六个默认成员函数、构造函数和其特性、析构函数、拷贝构造函数、赋值运算符重载,以及前置和后置递增运算符的重载。我们还涉及了如何使用const成员函数以增强方法的安全性和重载取地址操作符以适应特定需求。通过这些知识,可以更好地理解和利用C++提供的面向对象能力,编写更加健壮和高效的代码。C++类与对象(三),我们再会!

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

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

相关文章

小红书app缓存清除

1.背景 小伙伴们&#xff0c;手机app运行产生的缓存在不断侵占着我们的收集的内存&#xff0c;运行个半年发现内存不足20%。其实很多情况我们通过各个手机自带的缓存清除功能&#xff0c;就可以把app运行过程中产生的内存清除掉&#xff0c;节省我们不少的空间。想一想手机上a…

二分查找的时间复杂度的讲解

二分查找的代码&#xff1a; 二分查找的时间复杂度&#xff1a; 最坏的情况&#xff1a; 就是找不到和查找区间只剩一个值的时候&#xff0c;这两种都是最坏的结果&#xff0c;假设查找了x次&#xff0c;达到了最坏的结果&#xff1a; N代表每一次折半区间数据的个数&#xf…

当你拥有Xbox-GamePass就能更快体验NewGame

如果你有游戏通行证终极通行证&#xff0c;那么你就可以看到很多预售的游戏&#xff0c;以及更多游戏内容。 Shadow of the Tomb Raider: Definitive Edition《古墓丽影:暗影&#xff08;终极版&#xff09;》 征服残酷无情的丛林&#xff0c;并活着走出来。探索充满裂隙和幽深…

I2C,UART,SPI(STM32、51单片机)

目录 基本理论知识&#xff1a; 并行通信/串行通信&#xff1a; 异步通信/同步通信&#xff1a; 半双工通信/全双工通信: UART串口&#xff1a; I2C串口&#xff1a; SPI串口&#xff1a; I2C在单片机中的应用&#xff1a; 软件模拟&#xff1a; 51单片机&#xff1a;…

Linux的进程管理

进程 程序运行在操作系统中&#xff0c;是被操作系统所管理的。 为管理运行的程序&#xff0c;每一个程序在运行的时候&#xff0c;便被操作系统注册为系统中的一个&#xff1a;进程 并会为每一个进程都分配一个独有的&#xff1a;进程ID&#xff08;进程号&#xff09; 查看…

C++进阶——继承

前言&#xff1a;从这篇文章开始&#xff0c;我们进入C进阶知识的分享&#xff0c;在此之前&#xff0c;我们需要先来回顾一个知识&#xff1a; C语言有三大特性&#xff0c;分别是封装、继承和多态&#xff0c;而我们前边所分享的各种容器类&#xff0c;迭代器等&#xff0c;…

基于SpringBoot的“线上教学平台”的设计与实现(源码+数据库+文档+PPT)

基于SpringBoot的“线上教学平台”的设计与实现&#xff08;源码数据库文档PPT) 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBoot 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 系统展示 线上教学平台结构图 管理员登录界面图 学员管理界…

网络工程师-----第一天

线缆与进制转换 进制转换: 1.十进制&#xff1a; 都是以0-9这九个数字组成&#xff0c;不能以0开头。 2.二进制&#xff1a; 由0和1两个数字组成。 3.八进制&#xff1a; 由0-7数字组成&#xff0c;为了区分与其他进制的数字区别&#xff0c;开头都是以0开始。 4.十六进制…

Python数据结构【二】查找

前言 可私聊进一千多人Python全栈交流群&#xff08;手把手教学&#xff0c;问题解答&#xff09; 进群可领取Python全栈教程视频 多得数不过来的计算机书籍&#xff1a;基础、Web、爬虫、数据分析、可视化、机器学习、深度学习、人工智能、算法、面试题等。 &#x1f680;&a…

手动实现简易版RPC(下)

手动实现简易版RPC(下) 前言 什么是RPC&#xff1f;它的原理是什么&#xff1f;它有什么特点&#xff1f;如果让你实现一个RPC框架&#xff0c;你会如何是实现&#xff1f;带着这些问题&#xff0c;开始今天的学习。 接上一篇博客 手动实现简易版RPC&#xff08;上&#xff…

抖音小店运营计划表年度电商规划管理模板

【干货资料持续更新&#xff0c;以防走丢】 抖音小店运营计划表年度电商规划管理模板 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 抖音店铺运营表格 &#xff08;完整资料包含以下内容&#xff09; 目录 抖音店铺运营计划&#xff1a; 一、店铺定位与目标…

MySql运维篇

目录 一.日志 1.1日志分类 1.2Error Log 1.3BinaryLog 1.4SlowQuery Log 二.备份 2.1备份原因 2.2备份目标 2.3备份技术 2.3.1物理备份 2.3.2逻辑备份 2.4备份方式 2.4.1完全备份 2.4.2增量备份 2.4.3差异备份 2.5备份环境准备 2.6完全备份实验 2.6.1完全备…

书生·浦语大模型全链路开源体系-第4课

书生浦语大模型全链路开源体系-第4课 书生浦语大模型全链路开源体系-第4课相关资源XTuner 微调 LLMXTuner 微调小助手认知环境安装前期准备启动微调模型格式转换模型合并微调结果验证 将认知助手上传至OpenXLab将认知助手应用部署到OpenXLab使用XTuner微调多模态LLM前期准备启动…

连锁服装卖场进销存一般怎么管理

连锁服装卖场的进销存管理是保证业务顺畅运作和最大化利润的关键之一。随着市场竞争的加剧和消费者需求的变化&#xff0c;良好的进销存管理能够帮助企业及时调整库存&#xff0c;减少滞销品&#xff0c;提高资金周转率&#xff0c;从而增强市场竞争力。本文将探讨连锁服装卖场…

单独设置浏览器滚动条上下箭头

解决方法 重点 ::-webkit-scrollbar-button:vertical 给垂直方向的滚动条设置样式 ::-webkit-scrollbar-button:vertical:start 上方向的按钮 ::-webkit-scrollbar-button:vertical:start:decrement 上方向单个按钮 下方向同理 不知道为啥搜索出来的single-button不生效&#…

制造业的数字化转型如何做?

随着科技的迅速发展&#xff0c;数字化转型已经成为制造型企业提高竞争力的关键因素。它可以帮助制造型企业&#xff0c;在产品优化设计、材料采购、生产流程方面实现精细化管理&#xff1b;提升上下游协同生产能力&#xff0c;提高生产效率、降低生产成本、优化产品质量&#…

华为的AI战略地图上,才不是只有大模型

大模型火热了一年&#xff0c;现在还没做AI化改造的企业&#xff0c;就像是工业革命浪潮伊始与火车赛跑的那辆马车。 最早的蒸汽火车缓慢又笨重&#xff0c;甚至铁轨上还预留了马匹行走的空间&#xff0c;以便随时用马拉火车来替代蒸汽火车&#xff0c;一辆华丽的马车试图和火…

浮点数的存储方式、bf16和fp16的区别

目录 1. 小数的二进制转换2. 浮点数的二进制转换3. 浮点数的存储3.1 以fp32为例3.2 规约形式与非规约形式 4. 各种类型的浮点数5. BF16和FP16的区别Ref 1. 小数的二进制转换 十进制小数转换成二进制小数采用「乘2取整&#xff0c;顺序排列」法。具体做法是&#xff1a;用 2 2…

C++语言·类和对象

1. 类的引入 C语言结构体中只能定义变量&#xff0c;但在C中&#xff0c;结构体内不仅可以定义变量&#xff0c;也可以定义函数&#xff0c;同时C中struct的名称就可以代表类型&#xff0c;不用像C那样为了方便还要typedef一下。 在C中我们管定义的结构体类型叫做类(student)&a…

idea 将项目上传到gitee远程仓库具体操作

目录标题 一、新建仓库二、初始化项目三、addcommit四、配置远程仓库五、拉取远程仓库内容六、push代码到仓库 一、新建仓库 新建仓库教程 注意&#xff1a;远程仓库的初始文件不要与本地存在名字一样的文件&#xff0c;不然拉取会因为冲突而失败。可以把远程一样的初始文件删…