【C++】构造函数、初始化列表,析构函数,拷贝构造函数,运算符重载

 注:本博客图片来源于学习笔记: 学习笔记icon-default.png?t=N7T8https://gitee.com/box-he-he/learning-notes

完整思维导图请前往该博主码云下载。


目录

 注:本博客图片来源于学习笔记: 学习笔记https://gitee.com/box-he-he/learning-notes

完整思维导图请前往该博主码云下载。

一、构造函数

构造函数的形式及其使用:

初始化列表:

explicit关键字

二、析构函数

1 .概念

2. 特性

析构函数的调用顺序

三、拷贝构造函数

特性:

四、运算符重载(部分)

1、概念

2、赋值运算符重载

3、前置++和后置++的实现


 

一、构造函数

构造函数是一种特殊的成员函数,在创建对象时自动调用,用于初始化对象的成员变量和执行其他必要的操作。它的名称与类名相同,没有返回类型,并且可以被重载。

构造函数有以下几个重要的特点:

  1. 构造函数的名称与类名相同,包括大小写和命名空间。
  2. 构造函数没有返回类型。
  3. 构造函数可以被重载,即可以定义多个具有不同参数列表的构造函数。
  4. 构造函数可以有默认参数,从而允许在创建对象时省略某些参数。
  5. 构造函数可以有任意数量的初始化列表,用于按照指定顺序初始化对象的成员变量。
  6. 构造函数可以访问类的所有成员变量和成员函数。
  7. 构造函数在创建对象时自动调用,无需手动调用。
  8. 对象的整个生命周期只调用一次。

构造函数主要用于以下几个方面(功能):

  1. 初始化成员变量:构造函数通过参数或初始化列表来初始化对象的成员变量,确保对象在创建时具有特定的初始值。
  2. 分配内存或资源:如果对象需要动态分配内存或管理其他资源(如文件句柄),构造函数可以在创建对象时进行相应的操作,从而确保对象可以正确地使用这些资源。
  3. 执行必要的操作:有时候,创建对象可能需要执行一些必要的操作,比如打开文件等,构造函数可以在创建对象时执行这些操作。

需要注意以下几点:

  1. 一个类可以有多个构造函数,它们具有不同的参数列表。这被称为构造函数的重载。
  2. 如果没有显式地定义构造函数,编译器会自动生成一个默认构造函数,但它只会对自定义类型变量进行默认初始化,对内置类型(int, double, float等)无法初始化。
  3. 编写构造函数时应该根据类的需求来初始化成员变量,并且可以根据需要执行其他必要的操作。
  4. 如果自定义了构造函数,编译器将不再生成默认构造函数。如果需要同时拥有默认构造函数和自定义构造函数,可以提供默认参数或者显式定义一个无参数的默认构造函数。

构造函数的形式及其使用:

默认构造函数包括:1、无参构造函数;2、全缺省的构造函数;3、编译器默认生成的构造函数 

当用户自己定义构造函数时 ,必须存在一个默认构造函数。

构造函数的使用:

【C++】构造函数、初始化列表,析构函数,拷贝构造函数,运算符重载_第1张图片

 class Date
 {
  public:
      // 1.无参构造函数(默认构造函数)
      Date()
     {}
  
      // 2.带参构造函数
      Date(int year, int month, int day)
     {
          _year = year;
          _month = month;
          _day = day;
     }

    // 3.全缺省的构造函数(默认构造函数)
     Date(int year = 2024, int month = 2, int day = 6)
     {
          _year = year;
          _month = month;
          _day = day;
     }
    
    // 4.编译器默认生成的构造函数(默认构造函数)

  private:
      int _year;
      int _month;
      int _day;
 };
  
  void TestDate()
 {
      Date d1; // 调用无参构造函数
      Date d2(2015, 1, 1); // 调用带参的构造函数
  
      // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
      // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
      // warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?)
      Date d3();
 }

需要注意的是全缺省的构造函数和无参的构造函数都属于默认构造函数,不能同时存在。

同时 函数2 与 函数3 无法构成函数重载,会造成函数的重定义,也无法同时存在。

 补充:由于编译器默认生成的构造函数无法对内置类型进行初始化,在C++11标准中,对此引入了解决方案:在变量声明时可以为其提供默认值。

 class Date
 {
 
  private:
      int _year = 2024;
      int _month = 2;
      int _day = 6;
 };
  
  void TestDate()
 {
      Date d; // 此时d的私有成员初始化为2024/2/6
 }

 

初始化列表:

1、基本形式

初始化列表:以一个 冒号开始 ,接着是一个以 逗号分隔的数据成员列表 ,每个 " 成员变量 " 后面跟
一个 放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
 : _year(year)
 , _month(month)
 , _day(day)
 {

 }

private:
int _year;
int _month;
int _day;
};
【注意】
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
  1. 引用成员变量
  2. const成员变量
  3. 自定义类型成员(且该类没有其默认构造函数时)        
class A
{
public:
	A(int a)
		:_a(a)
	{}
private:
	int _a;
};
class B
{
public:
	B(int a, int ref)
		:_aobj(a)
		, _ref(ref)
		, _n(10)
	{}
private:
	A _aobj;  // 没有默认构造函数的自定义类型
	int& _ref; // 引用
	const int _n; // const 
};

特性:

1、即使不在初始化列表中初始化自定义类型成员,其也会在初始化列表中隐式地调用其构造函数进行初始化。

2、初始化列表的执行顺序是其在类中的声明顺序,与初始化列表中的顺序无关。

explicit关键字

explicit 是一个关键字,用于修饰单参数构造函数,它可以防止编译器进行隐式的类型转换。在 C++ 中,当我们定义一个单参数构造函数时,编译器会自动进行隐式类型转换,将参数类型转换为目标类型,然后调用该构造函数来创建对象。而使用 explicit 关键字修饰的构造函数将被标记为禁止隐式类型转换。

下面是两个具体的示例来说明 explicit 关键字的作用:

1、单参构造函数

class MyClass {
public:
    explicit MyClass(int x) 
        : value(x) 
    {
    }
  
    int getValue() const {
        return value;
    }

private:
    int value;
};

int main() {
    MyClass obj1(5);       // 直接调用带有 int 参数的构造函数
    int val1 = obj1.getValue();
  
    MyClass obj2 = 10;    // 编译错误!禁止隐式类型转换
    int val2 = obj2.getValue();
  
    return 0;
}

在上述示例中,MyClass 类定义了一个带有单参数的构造函数,并使用 explicit 关键字进行修饰。这意味着我们只能通过显式地调用构造函数来创建对象,而不能进行隐式的类型转换。在 main 函数中,我们可以看到以下几点:

  1. 创建 obj1 对象时,我们直接调用了构造函数,不会发生隐式类型转换。这是合法的。

  2. 尝试使用 obj2 = 10; 进行从 int--->MyClass 隐式类型转换时,编译器会抛出错误。因为我们使用了 explicit 关键字,禁止隐式类型转换。

2、半缺省的构造函数

class MyClass {
public:
    //虽然有多个参数,但是创建对象时后两个参数可以不传递,使用explicit修饰,
   // 不具有类型转换作用,编译不通过
    explicit MyClass(int x, int y = 1, int z = 1) 
        : value(x)
        , data(y)
        , temp(z)
    {
    }
  
    int getValue() const {
        return value;
    }

private:
    int value;
    int data;
    int temp;
};

int main() {
    MyClass obj1(5);       // 直接调用带有 int 参数的构造函数
    int val1 = obj1.getValue();
  
    MyClass obj2 = 10;    // 编译错误!禁止隐式类型转换
    int val2 = obj2.getValue();
  
    return 0;
}

二、析构函数

1 .概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没呢的?              

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由    

编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

【注意】:析构函数只负责销毁占用的额外资源,即额外申请的内存空间,例如:malloc,new等。而对象本身,如果定义在函数中,则其本身存在于栈区中,随着函数的栈帧一同销毁,与析构函数无关。

2. 特性

析构函数是特殊的成员函数,其特征如下:

1. 析构函数名是在类名前加上字符 ~。

2. 无参数无返回值类型。

3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构

函数不能重载

4. 对象生命周期结束时,C++编译系统系统自动调用析构函数。

【C++】构造函数、初始化列表,析构函数,拷贝构造函数,运算符重载_第2张图片
typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 3)
	{
		_array = (DataType*)malloc(sizeof(DataType) * capacity);
		if (NULL == _array)
		{
			perror("malloc申请空间失败!!!");
			return;
		}
		_capacity = capacity;
		_size = 0;
	}
	void Push(DataType data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}

    //析构函数
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = NULL;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	int _capacity;
	int _size;
};
void TestStack()
{
	Stack s;
	s.Push(1);
	s.Push(2);
}

 

析构函数的调用顺序

【C++】构造函数、初始化列表,析构函数,拷贝构造函数,运算符重载_第3张图片  


三、拷贝构造函数

拷贝构造函数(Copy Constructor)是一种特殊的构造函数,它接受一个对象作为参数,用于创建一个新的对象,这个新对象与原始对象具有相同的值。

在C++中,如果我们没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数。默认拷贝构造函数执行的是逐个成员的拷贝,将源对象的每个成员变量的值复制给新创建的对象。

以下是一个简单的示例来说明拷贝构造函数的用法:

class MyClass {
public:
    int value;

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        value = other.value;
    }
};

int main() {
    MyClass obj1;
    obj1.value = 10;

    MyClass obj2(obj1);  // 使用拷贝构造函数创建新对象
    // 或者 MyClass obj2 = obj1;

    return 0;
}

在上述示例中,我们定义了一个名为 MyClass 的类,并在其中定义了一个拥有一个整数成员变量 value 的对象。然后我们在 main 函数中创建了两个对象 obj1 和 obj2

  • obj1 是通过默认构造函数创建的,并将 value 设置为 10。

  • obj2 是通过拷贝构造函数创建的,它的参数是另一个 MyClass 对象 obj1。在拷贝构造函数中,我们将 obj1 的值复制给了 obj2

需要注意的是,拷贝构造函数的参数应该是一个 const 引用(const MyClass& other),以确保不会修改被传递的源对象。

拷贝构造函数在以下情况下会被隐式调用:

  1. 通过赋值运算符(=)进行对象的初始化。
  2. 将一个对象作为函数的参数传递给另一个对象。
  3. 从一个函数返回对象时。

需要注意的是,当类中包含指针成员变量时,需要手动定义拷贝构造函数,以确保指针指向独立的内存,避免浅拷贝引发的指针悬挂问题。

 

特性:

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

2. 拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器直接报错
因为会引发无穷递归调用。
class Date
{
public:
 Date(int year = 1900, int month = 1, int day = 1)
 {
 _year = year;
 _month = month;
 _day = day;
 }
 // Date(const Date& d)   // 正确写法

    Date(const Date d)   // 错误写法:编译报错,会引发无穷递归
 {
 _year = d._year;
 _month = d._month;
 _day = d._day;
 }
private:
 int _year;
 int _month;
 int _day;
};
int main()
{
 Date d1;
 Date d2(d1);
 return 0;
}

【C++】构造函数、初始化列表,析构函数,拷贝构造函数,运算符重载_第4张图片

 

 为什么值传递会引起无穷递归呢?为什么引用不会呢?

  答:假如参数不是引用,是值传递,那么传递给函数的值其实就是实参的一个临时拷贝,那在对实参进行临时拷贝时还是需要再次去调用我Date类里的拷贝构造函数,由于一直进行的是值传递,所以这个过程是无限递归的。但是如果拷贝构造函数的参数是对实参的引用,就不需要对实参再进行临时拷贝。引用是实参的一个别名,他们指向的也是同一块内存,所以不需要传递值,而是通过实参的引用来对实参的内存进行读写。

3. 若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按

    字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝

class Time
{
public:
	Time()
	{
		_hour = 1;
		_minute = 1;
		_second = 1;
	}
	Time(const Time& t)
	{
		_hour = t._hour;
		_minute = t._minute;
		_second = t._second;
		cout << "Time::Time(const Time&)" << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 基本类型(内置类型)
	int _year = 1970;
	int _month = 1;
	int _day = 1;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d1;
	// 用已经存在的d1拷贝构造d2,此处会调用Date类的拷贝构造函数
	// 但Date类并没有显式定义拷贝构造函数,则编译器会给Date类生成一个默认的拷贝构
	//造函数
	Date d2(d1); // Date d2 = d1; 也是默认调用的拷贝函数
	return 0;
}

 【注意】:在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的。

4. 编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?

当然像日期类这样的类是没必要的。

但是对于初始化时成员需要额外申请内存的类,此时我们使用默认拷贝函数会发生程序崩溃,这时我们必须手动创建一个拷贝构造函数进行深拷贝。例如下面的栈class Stack;

typedef int DataType;
class Stack
{
public:
	Stack(size_t capacity = 10)
	{
		_array = (DataType*)malloc(capacity * sizeof(DataType));
		if (nullptr == _array)
		{
			perror("malloc申请空间失败");
			return;
		}
		_size = 0;
		_capacity = capacity;
	}
    
    // 如果我们在这只是进行简单的值拷贝,程序将会崩溃
	Stack(const Stack& s)
	{
		_array = new DataType[_capacity];
		memcpy(_array, s._array, _size * sizeof(DataType));
		_size = s._size;
		_capacity = s._capacity;
	}
	void Push(const DataType& data)
	{
		// CheckCapacity();
		_array[_size] = data;
		_size++;
	}
	~Stack()
	{
		if (_array)
		{
			free(_array);
			_array = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	DataType* _array;
	size_t _size;
	size_t _capacity;
};
int main()
{
	Stack s1;
	s1.Push(1);
	s1.Push(2);
	s1.Push(3);
	s1.Push(4);
	Stack s2(s1);
	return 0;
}

 【C++】构造函数、初始化列表,析构函数,拷贝构造函数,运算符重载_第5张图片

【注意】:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
 

5. 拷贝构造函数典型调用场景:     

  • 使用已存在对象创建新对象

  • 函数参数类型为类类型对象

  • 函数返回值类型为类类型对象

class MyClass {
public:
    int value;

    // 拷贝构造函数
    MyClass(const MyClass& other) {
        value = other.value;
    }
};

MyClass Test(const MyClass obj3)
{
    MyClass temp;

    return temp; // 函数返回值类型为类类型对象
                 // 由于temp为局部变量,在函数结束后不存在,所以需对temp进行临时拷贝,
                 // 返回值的实质是temp的一份临时拷贝
}

int main() {
    MyClass obj1;
    obj1.value = 10;

    MyClass obj2(obj1);  // 或者 MyClass obj2 = obj1;使用已存在的对象创建新对象
    
    Test(obj2);          // 函数的参数类型为类类型对象
    
    return 0;
}

四、运算符重载(部分)

1、概念

C++ 为了增强代码的可读性引入了运算符重载 运算符重载是具有特殊函数名的函数 ,也具有其
返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字为:关键字 operator 后面接需要重载的运算符符号
函数原型: 返回值类型  operator 操作符 ( 参数列表 )
【注意】:
  1. 不能通过连接其他符号来创建新的操作符:比如operator@
  2. 重载操作符必须有一个类类型参数
  3. 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  4. 作为类成员函数重载时,其形参看起来比操作数数目少 1 ,因为成员函数的第一个参数为隐
    藏的 this
  5. .* :: sizeof ?: . 注意以上5个运算符不能重载。

2、赋值运算符重载


1、重载运算符的格式

  • 参数类型:const T&,传递引用可以提高传参效率

  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值

  • 检测是否自己给自己赋值

  • 返回*this :要复合连续赋值的含义

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	Date& operator=(const Date& d)
	{
		if (this != &d)
		{
			_year = d._year;
			_month = d._month;
			_day = d._day;
		}
		return *this;
	}
private:
	int _year;
	int _month;
	int _day;
};

 

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

【原因】:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现
一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值
运算符重载只能是类的成员函数。

【注意】:如果需要在全局中重载其他运算符,需要给两个参数。因为只有类的非静态成员函数具有this指针,全局函数没有。

3. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

【注意】:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
 

3、前置++和后置++的实现

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	// 前置++:返回+1之后的结果
	// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
	Date& operator++()
	{
		_day += 1;
		return *this;
	}
	// 后置++:
	// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
	// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
	//自动传递
		// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存
		//一份,然后给this + 1
		// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
	Date operator++(int)
	{
		Date temp(*this);
		_day += 1;
		return temp;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d;
	Date d1(2022, 1, 13);
	d = d1++; // d: 2022,1,13 d1:2022,1,14
	d = ++d1; // d: 2022,1,15 d1:2022,1,15
	return 0;
}

你可能感兴趣的:(c++)