【C++】类与对象--类中的6个默认成员函数(1)

目录

问题引入

1. 6个默认成员函数

2. 构造函数

2.1 构造函数的概念

2.2 构造函数的特性

2.2.1 构造函数可以重载

3. 析构函数

3.1 概念

3.2 特性

4. 拷贝构造函数

4.1 概念

4.2 特性

4.3 思考


问题引入

我们知道,当我们创建了一个类,但是类中什么都没有,就称作空类。但是,事实上真的空类中就什么都没有吗?任何一个类在我们没有写东西的情况下都会生成6个默认的函数成员,包括:构造函数、析构函数、拷贝构造函数、赋值重载,以及两个取地址。

本篇中我们重点讨论,构造函数、析构函数和拷贝构造函数。

1. 6个默认成员函数

任何一个类在我们不写的情况下,都会自动生成下面6 个默认成员函数:


2. 构造函数

2.1 构造函数的概念

正常情况下,我们看这段代码的运行结果:

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
 
private:
	int _year;
	int _month;
	int _day;
 
};
int main()
{
	Date d1;
	d1.Init(2025, 07, 21);
	d1.Print();
 
	return 0;
}

运行结果:

但是如果我们忘记了初始化,那么会不会打印出来的数值会是随机值,从而导致程序崩溃呢?

当然为了避免这种歌情况发生,C++的大佬们设计了构造函数

构造函数不需要init函数,构造函数是一个特殊的成员函数,是在创建类对象时由编译器自动创建的,目的就是为了保证成员变量一定会有一个合适的初始值,当然构造函数在对象的生命周期只被调用一次


2.2 构造函数的特性

但是值得注意的是,构造函数虽然名字上叫做“构造”,但是它的作用并不是开辟内存空,而仅仅是对成员变量进行初始化。

特征如下:

  • 构造函数名与类名相同
  • 没有返回值
  • 在对象实例化时,编译器自动调用构造函数
  • 构造函数可以重载
class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

    //构造函数,自动调用
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}


private:
	int _year;
	int _month;
	int _day;

};
int main()
{
	Date d1;
	d1.Print();

	return 0;
}

调试过程中就能够发现,自动调用了构造函数:


2.2.1 构造函数可以重载

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	
//自动调用
	// 1.无参构造函数
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}

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

private:
	int _year;
	int _month;
	int _day;

};
int main()
{
	Date d1;
	d1.Print();

	Date d2(2025,7,25);
	d2.Print();


	return 0;
}

我们来看一下运行结果:

注意:

如果是通过无参构造函数去创建对象的话,那么对象名后面不需要加括号,因为不然的话就变成了函数声明。

 Date d3();

这就是一个使用无参构造函数,但是后面加了括号,那么此时d3就成了一个返回类型为Date的函数声明。

这里其实可以对上面的代码进行优化,将上面那个无参构造函数和带参构造函数结合在一起,更急方便:

	//全缺省参数
	Date(int year = 1,int month = 1 ,int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

这样的话就会很方便的进行自主传参:

这里要注意的是,如果没有显式的定义构造函数的时候,那么C++编译器就会自主创建一个隐式的构造函数,如果自己写的话,就不会生成了。

class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
 
private:
	int _year;
	int _month;
	int _day;
 
};
int main()
{
	Date d1;
	d1.Print();
 
	return 0;
}

就像这里,我们并没有定义构造函数,但其实C++编译器会自动生成一个构造函数:

但是这里大家可能会有疑问,这个数据怎么这么想随机值呢,编译器真的自动创建了一个构造函数来进行初始化吗?

这里要进行知识的补充,C++将所有的变量分为两种:

  1. 内置类型/基本类型:int,char,double,指针......
  2. 自定义类型:class/struct去定义类型对象

C++默认生成构造函数对于内置型的变量不会进行处理,对于自定义类型就会进行处理。

这里的年月日是int类型的内置型,故而不进行处理,那么我们来看一看对于自定义类型会不会进行处理:

class A
{
public:
	A()
	{
		cout << "A()" << endl;
		_a = 0;
	}
private:
	int _a;
};
 
class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
 
private:
	int _year;
	int _month;
	int _day;
 
	A _aa;
 
};
int main()
{
	Date d1;
	d1.Print();
 
	return 0;
}

我们发现自定义的_aa被初始化成了0,也就证明了自定义类型是会被初始化的。 

再次强调一遍,自动初始化的本质并不是编译器去给它一个合适的值,而是说编译器在对自定义类型变量初始化时,会自动(隐式调用)它的构造函数。


3. 析构函数

3.1 概念

前面我们构造函数了解了对象是怎么创建的,那么对象是怎么销毁的呢?

析构函数:与构造函数相反,销毁对象与析构函数相关。但是函数的销毁并不是直接通过析构函数,局部对象的销毁由编译器完成,而在对象销毁时会调用析构函数进行资源的清理。


3.2 特性

  • 析构函数的函数名是在类名前加上字符 ~
  • 没有参数、没有返回值
  • 一个类只有一个析构函数。如果没有显式定义,系统会默认生成一个析构函数
  • 当对象生命周期结束的时候,C++编译器自动调用析构函数
class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}

	~Date()
	{
		cout << "~Date" << endl;
	}

private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	return 0;
}

运行结果: 

从上面的结果来看,很明显程序结束时调用了析构函数。

但是我们刚才提到了析构函数实际上是对资源的清理,但是我们观察到Date类中都是对象成员本身,这些东西是会随着函数栈帧的销毁而销毁的。那么什么东西需要资源的释放呢?一般是使用了new、malloc等关键字的对象:

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
 
		_top = 0;
		_capacity = capacity;
	}
 
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
 
 
private:
	int* _a;
	int _top;
	int _capacity;
 
};
int main()
{
	Stack st;
	return 0;
}

看这段代码中的_a变量是通过malloc申请来的,所以需要析构函数来进行最后资源的释放:

~Stack()
{
	free(_a);
	_a = nullptr;
	_top = _capacity = 0;
}

这里我们就可以理解为,构造函数相当于我们的初始化;析构函数就相当于Destory。

思考:

Stack st1;
Stack st2;

我们这里定义两个Stack类型变量,请问谁先构造,谁先析构?

通过添加监视,然后逐语句分析,我们发现编译器先初始化了st1。然后在两行语句都执行完了之后,程序要结束时,进入了析构函数,并且先析构了st2:

所以可以得到的结论:先构造的后析构。


4. 拷贝构造函数

4.1 概念

拷贝构造函数:只有一个形参,这个形参是对本类类型对象的引用(一般使用const修饰)。拷贝构造函数在使用已存在的类类型对象 创建信对象时编译器自动调用。


4.2 特性

拷贝构造函数同样也是特殊的成员函数:

  • 拷贝构造函数是构造函数的一个重载形式
  • 拷贝构造函数的参数只有一个,而且必须使用引用传参,如果使用传值方式会导致无穷递归调用

这里使用这样一段代码作为说明:

class InfiniteRecursion {
public:
    int value;

    // 错误的拷贝构造函数:参数是传值而非引用
    InfiniteRecursion(InfiniteRecursion obj) {  // 这里应该用 const InfiniteRecursion&
        value = obj.value;
        std::cout << "Copy Constructor Called! Value: " << value << std::endl;
    }

    // 普通构造函数
    InfiniteRecursion(int v) : value(v) {}
};

int main() {
    InfiniteRecursion obj1(42);
    InfiniteRecursion obj2 = obj1;  // 这里会触发无限递归!
    return 0;
}

根据我们之前写的拷贝构造函数的概念,拷贝构造函数在使用已存在的类类型对象 创建信对象时编译器自动调用:

  • 所以InfiniteRecursion obj2 = obj1; 这个时候会自动调用拷贝构造函数
  • 但是这里的拷贝构造函数使用的传值方式,所以存在将obj1拷贝给形参obj这样一个步骤(此时又需要调用拷贝构造函数)
  • 从此不断循环地调用下去,直至栈溢出

但是这段代码并不是运行过程中崩溃,编译器会自动识别,并直接报错:

而我们将拷贝构造函数改为传引用:


4.3 思考

那么栈的拷贝可不可以这样写:

class Stack
{
public:
	Stack(int capacity = 10)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		assert(_a);
 
		_top = 0;
		_capacity = capacity;
	}
	Stack(const Stack& st)
	{
		_a = st._a;
		_top = st._top;
		_capacity = st._capacity;
	}
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
 
 
private:
	int* _a;
	int _top;
	int _capacity;
 
};
int main()
{
	Stack st1(1);
	Stack st2(st1);
 
	return 0;
}

答案:不能

因为这样使用拷贝构造函数,st1和st2指向的是同一块内存,所以在析构的时候就可能会出现同一块内存被多次释放的情况。

实际上这个问题就是典型的深拷贝和浅拷贝的问题,后面我们针对这个问题进行一个详细的讲解。


(本篇完) 

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值