代码改变世界

详细介绍:C++:类和对象---进阶篇

2025-12-09 10:46  tlnshuju  阅读(2)  评论(0)    收藏  举报

1. 类的默认成员函数

默认成员函数就是我们没有显式实现,C++会自动生成的成员函数称为默认成员函数,C++11后,C++类的默认成员函数有8个默认构造函数、默认析构函数、拷贝构造函数、赋值运算符重载、取地址运算符重载、const取地址运算符重载、移动构造函数(C++11后)、移动赋值运算符重载(C++11后)),我们此文只了解重要的前4个,后4个中前两个不常用,后两个之后再做讲解。

2. 构造函数

构造函数是特殊的成员函数,虽名为构造,但它完成的是成员变量的初始化工作,所以它可以完美的替代Init函数。

构造函数的特点

1. 函数名与类名相同

2. 无返回值

3. 对象实例化时会自动调用构造函数

4. 构造函数可以重载

5. 没有显式定义构造函数,编译器会自动生成一个无参的默认构造参数,一旦显式定义后,将不再生成。

6. 无参构造函数、全缺省构造函数、我们不写编译器生成的构造函数都叫做默认构造函数。这三种默认构造函数,不能同时存在,无参和全缺省构造函数随构成函数重载,但存在调用歧义。

7. 我们不写,编译器默认生成的构造函数,对内置类型的变量处理没有要求,是否初始化是不确定的;对自定义类型成员变量,要求调用它的默认构造函数,如果没有默认构造函数,就会报错。我们要初始化这个成员变量,需要用到初始化列表,初始化列表我们放到第7点精讲。

//默认构造函数
using namespace std;
class Date
{
public:
	//// 1.⽆参构造函数
	//Date()
	//{
	//	_year = 1;
	//	_month = 1;
	//	_day = 1;
	//}
	//// 2.带参构造函数
	//Date(int year, int month, int day)
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
	// 3.全缺省构造函数
	Date(int year = 1, int month = 1, int day = 1)
	{
	_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, 1, 1); // 调⽤带参的构造函数
	Date d3();
	d1.Print();
	d2.Print();
	return 0;
}

3. 析构函数

析构函数与构造函数功能相反,不是对对象本身的销毁,而是对对象中资源的清理释放,局部对象存在于栈帧中,函数结束销毁栈帧,不需要析构函数处理,C++规定在对象销毁时会自动调用析构函数。析构函数的功能类似我们实现的栈中的Destory函数,日期类中没有资源需要释放,严格来说,不需要析构函数。

析构函数的特点

1. 析构函数名是类名加~

2. 无参数无返回值

3. 一个类只有一个析构函数,若未显式定义,则系统会默认析构函数

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

5. 我们不写,编译器默认生成的析构函数,对内置类型成员不做处理,自定义类型成员会调用它们的析构函数(与构造函数类似)

6. 特别注意的是,即使我们显式写析构函数,对于自定义类型成员,编译器还是会调用它们的析构函数

// 两个栈实现队列
class MyQueue
{
public:
	//编译器默认⽣成MyQueue的析构函数调⽤了Stack的析构,释放的Stack内部的资源
	// 显⽰写析构,也会⾃动调⽤Stack的析构 减少因析构函数错误而带来内存泄露的风险
	~MyQueue()
	{
		cout << "~MyQueue" << endl;
	}
private:
	Stack pushst;
	Stack popst;
};
int main()
{
	//Stack st;
	MyQueue mq;
	return 0;
}

7. 如果类中没有资源申请,可以不写析构函数,直接使用编译器生成的默认析构函数;如果默认的析构函数也可以用,也可以不写像~MyQueue(),但如果有资源申请,一定自己实现析构函数,否则会造成内存泄漏。

8. 一个局部域的多个对象,C++规定后定义的先析构

//析构函数 ~Stack()
typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	~Stack()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

4. 拷贝构造函数

如果一个构造函数第一个参数是自身类类型的引用,且任何额外的参数都有默认值,那个这个函数就是拷贝构造函数。所以拷贝构造函数是一个特殊的构造函数。

拷贝构造函数的特点

1. 拷贝构造函数是构造函数的重载

2. C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以自定义类型进行传值传参和传值返回都会调用拷贝构造

3. 拷贝构造函数第一个参数必须是类类型的引用,传值调用会报错(引发无穷递归调用),拷贝构造函数可以有多个参数,但第一个必须是类类型的引用,后面参数都有缺省值

4. 若未显式定义拷贝构造函数,编译器会自动生成拷贝构造函数,对于内置类型成员变量会完成浅拷贝(一个字节一个字节拷贝),对自定义类型成员变量会调用它的拷贝构造

5. 像Date类这样的全是内置类型且没有资源申请,编译器自动生成的拷贝构造就可以,不需要显式实现,像Stack这样的类,_a指向了资源,编译器自动生成的拷贝构造函数完成浅拷贝不符合需求,我们需要自己实现深拷贝(对指向资源也进行拷贝)

6. 传值返回会产生一个临时对象,调用拷贝构造函数,传值引用返回,没有拷贝。所以传值引用可以提高效率,但一定要保证传值引用返回时,一定要保证返回对象在函数结束后没有被销毁,否则会变成野引用。

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
    Stack(const Stack& s)
    {
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
        _a = (STDataType*)malloc(sizeof(STDataType) * s._capacity);
		_capacity = s._capacity;
		_top = s._top;
    }
private:
	STDataType* _a;
	size_t _capacity;
	size_t _top;
};

5. 赋值运算符重载

5.1 运算符重载

当运算符被用于类类型的对象时,C++允许我们通过运算符重载的形式指定新的含义,C++规定类类型使用运算符时,必须转换成调用对应运算符重载,如果没有,就会报错。

运算符重载是具有特殊名字的函数,他的名字由operator和后面要定义的运算符共同组成,同样具有返回类型、参数列表和函数体。

重载运算符函数的参数和运算符作用的运算对象一样多,一元运算符有一个参数,二元运算符有两个参数,运算符左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。

如果一个重载运算符函数是成员函数,第一个参数默认传给隐式的this指针,所以运算符重载作为成员函数时,参数比运算对象少一个。

运算符重载以后,其优先级和结合性与对应的内置运算符保持一致。

不能通过连接语法中没有的运算符来重载:如operator@。

.*  ::  sizeof  ?:  . 这五个运算符不能重载。(能重载的运算符必须有一个类类型对象参数,不能通过重载修改原来内置类型对象的含义)

重载++运算符时,有前置++和后置++之分,重载运算符名字都是operator++,无法区分,C++规定,使用后置++时,要传一个int形参,构成重载,以作区分。

重载<<和>>时,要重载为全局函数,因为重载为成员函数,隐式的this指针抢占了第一个参数,调用时就变成了对象<<cout,改变了原本习惯,也影响可读性。

5.2 赋值运算符重载

赋值运算符重载是一个默认成员函数,用于完成两个已存在对象的直接拷贝赋值,与拷贝构造不同的是,拷贝构造是一个对象拷贝初始化一个要创建的对象。

赋值运算符重载的特点

1. 赋值运算符重载(=)是一个运算符重载,规定必须重载为成员函数,其他复合型赋值运算符(+=、*=等等)推荐重载为成员函数,也可重载为全局。赋值运算符重载参数建议写为const当前类类型引用,可以减少拷贝。

2. 赋值运算符有返回值,建议写成当前类类型引用,减少拷贝,有返回值是为了支持连续赋值。

3. 没有显式实现时,编译器生成一个默认赋值运算符重载,对内置类型完成浅拷贝,自定义类型调用他们的赋值运算符重载。

4. 成员变量全是内置类型且没有指向资源时,不需要显式实现赋值运算符重载,有资源申请时,才需要显式实现赋值运算符重载函数

以判断两日期是否相等为例:

Date& operator=(const Date& d)
{
    if (this != &d)
    {
    _year = d._year;
    _month = d._month;
    _day = d._day;
}

6. 再探构造函数(*)

1. 前面我们实现构造函数,主要是使用函数体内赋值,构造函数还有一种初始化方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分割的数据成员列表,每个成员变量后面跟一个放在括号里的初始值或表达式。

2. 每个成员变量在初始化列表只能出现一次。语法上初始化列表可以认为是每个成员变量初始化定义的地方

3. 引用成员变量,const成员变量,没有默认构造的类类型变量必须在初始化列表初始化,否则报错。

4. C++11后支持在成员变量声明处给缺省值,主要给没有在初始化显式初始化的成员使用。

5. 尽量使用初始化列表初始化,因为即使不在初始化列表显式初始化,所以非静态成员变量也都会走初始化列表,未显式初始化的内置类型成员变量如果在声明处有缺省值,就用缺省值初始化,如果没有,初始化还是不初始化取决于编译器,没有规定;对于没有在初始化列表显式初始化的类类型成员变量,会调用它的默认构造函数,如果没有,就会报错。

6. 初始化列表初始化成员变量按照在类中的声明顺序初始化,跟初始化列表中的先后顺序无关,建议两者顺序一致。

#include
using namespace std;
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
        //时分秒过程... 只是测试不做实现
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int& x, int year = 1, int month = 1, int day = 1)
		: _t(12)
		, _ref(x)
		, _n(1)
	{
	}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year = 1900;
	int _month = 1;
	int _day = 1;
	Time _t; // 没有默认构造
	int& _ref; // 引⽤
	const int _n; // const
};
int main()
{
	int i = 0;
	Date d1(i);
	d1.Print();
	return 0;
}

这里参数缺省值和成员变量声明缺省值做一个区分

参数缺省值是参数不传参使用缺省值做形参,不影响成员变量的初始化;而成员变量缺省值就是初始化列表不显式传参时使用来初始化成员变量的,二者没有关系。

7. 类型转换

C++支持内置类型隐式转换为其他类类型对象,需要有内置类型做参数的构造函数;

构造函数前加explicit就不再支持隐式类型转换;

C++还支持类类型之间相互转换,同样需要有类类型做参数的构造函数。

#include
using namespace std;
class A
{
public:
    // 构造函数explicit就不再⽀持隐式类型转换
    // explicit A(int a1)
    A(int a1)
    :_a1(a1)
    {}
    A(int a1, int a2)
    :_a1(a1)
    , _a2(a2)
    {}
    void Print()
    {
        cout << _a1 << " " << _a2 << endl;
    }
    int Get() const
    {
        return _a1 + _a2;
    }
private:
    int _a1 = 1;
    int _a2 = 2;
};
class B
{
public:
    //支持类类型隐式转换的构造函数
    B(const A& a)
    :_b(a.Get())
    {}
private:
    int _b = 0;
};

8. static成员

用static修饰的成员变量称为静态成员变量,静态成员变量一定要在类外初始化(因为所有类对象共享同一块内存,不能通过初始化列表重复构造);

静态成员变量为所有类对象共享,不属于某个类对象中,存放于静态区;

用static修饰的成员函数称为静态成员函数,静态成员函数没有this指针

静态成员函数可以访问其他静态成员,但是不能访问非静态成员,因为没有this指针;

非静态成员可以访问任意的静态成员变量和静态成员函数;

突破类域就可以访问静态成员,可以通过类名::成员或对象::成员来访问静态成员变量和静态成员函数;

静态成员也是类的成员,受访问限定符的限制;

静态成员变量不能在声明处给缺省值,因为它不走初始化列表。

实现一个类 计算程序中创建出多少个类对象

#include
using namespace std;
class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A& t)
	{
		++_scount;
	}
	~A()
	{
		--_scount;
	}
	static int GetACount()
	{
		return _scount;
	}
private:
	// 类⾥⾯声明
	static int _scount;
};
// 类外⾯初始化
int A::_scount = 0;
int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;
	return 0;
}

牛客练习:1+2+3+...+n 

链接:https://www.nowcoder.com/share/jump/6657127531762514146651

9. 友元

友元提供了一种突破访问限定符封装的方式,分为友元函数和友元类,在函数声明或者类声明的前面加friend,并把友元声明放到一个类内。

外部友元函数可以访问私有和保护成员,友元函数不是类的成员函数,只是声明我(友元函数)是你(类)的朋友,可以不用靠物质媒介(访问限定符)来找你;

友元函数声明可以类的任意地方,不受访问限定符限制,一个函数可以是多个类的友元函数;

友元类中的成员函数都是另一个类的友元函数,都可以访问其私有和保护成员;

友元类的关系是单向的,且不能传递(如果要互为友元,需要互相包含友元声明)

友元一定程度上提供了便利,但不宜多用,会破坏耦合度。

10. 内部类

如果一个类定义在另一个类的内部,那么这个类就叫做内部类。

内部类是独立的类,只是受类域和访问限定符的限制,所以外部类定义的对象不包含内部类;

内部类默认是外部类的友元类;

内部类本质也是一种封装,如果A类与B类有很大关联,可以把A变为内部类,如果只想给B类内部使用,可以用访问限定符限制。

class A
{
private:
	static int _k;
	int _h = 1;
public:
	class B // B默认就是A的友元类
	{
	public:
		void foo(const A& a)
		{
			cout << _k << endl;
			cout << a._h << endl;
		}
		int _b1;
	};
};
int A::_k = 1;
int main()
{
	cout << sizeof(A) << endl; //输出4
	A::B b; //内部类对象的创建
	A aa;
	b.foo(aa);
	return 0;
}

11. 匿名对象

用 ”类型 (实参)“ 定义出来的对象叫做匿名对象;相比之前用 ”类型 对象名(实参)“ 定义的对象叫做有名对象;匿名对象生命周期只在当前这一行,一般临时定义一个对象当前行用一下,就可以用匿名对象。

class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
class Solution {
public:
	int Sum_Solution(int n) {
		//...
		return n;
	}
};
int main()
{
	A();
	A(1);//匿名对象在当前行结束时就调用析构函数
	A aa2(2);
	//匿名对象这样场景就很方便
	Solution().Sum_Solution(10);
	return 0;
}

12. 对象拷贝时的编译器优化

现代编译器为了尽可能提高效率,在不影响正确性的情况下会尽可能减少传值和传返回值过程中的可以省略的拷贝;如何优化C++没有规定,当前主流编译器会对一行表达式中的连续拷贝进行优化,一些“激进”的编译器甚至会多行合并优化

一些常见的编译器优化:

#include
using namespace std;
class A
{
public:
	A(int a = 0)
		:_a1(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a1 = aa._a1;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};
void f1(A aa)
{}
A f2()
{
	A aa;
	return aa;
}
int main()
{
	// 传值传参
	// 构造+拷⻉构造 无优化
	A aa1;
	f1(aa1);
	cout << endl;
	// 隐式类型,构造+拷⻉构造->省略临时对象 优化为直接构造
    //(int->构造给临时对象->拷贝构造)
	f1(1);
	// ⼀个表达式中,构造+拷⻉构造->优化为直接构造
	f1(A(2));
	cout << endl;
    //(构造->拷贝构造给临时对象)优化为直接构造临时对象返回
	f2();
	cout << endl;
    //(构造->拷贝构造给临时对象返回->拷贝构造aa2)优化为直接构造aa2
	A aa2 = f2();
	cout << endl;
    //(构造->拷贝构造给临时对象返回->赋值)优化为构造临时对象->赋值
	aa1 = f2();
	cout << endl;
	return 0;
}

13. 日期类的实现 (*)

13.1 Date.h

#pragma once
#include
using namespace std;
#include
class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
	friend istream& operator>>(istream& in, Date& d);
public:
	bool CheckDate() const;
	Date(int year = 1900, int month = 1, int day = 1);
	void Print() const;
	int GetMonthDay(int year, int month) const
	{
		assert(month >0 && month < 13);
		static int MonthDayArrey[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
		if (month == 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)))
		{
			return 29;
		}
		return MonthDayArrey[month];
	}
	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;
	Date operator+(int day) const;
	Date& operator+=(int day);
	Date operator-(int day) const;
	Date& operator-=(int day);
	// d1++;
	// d1.operator++(0);
	Date operator++(int);
	// ++d1;
	// d1.operator++();
	Date& operator++();
	// d1--;
	// d1.operator--(0);
	Date operator--(int);
	// --d1;
	// d1.operator--();
	Date& operator--();
	// d1 - d2
	int operator-(const Date& d) const;
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& out, const Date& d);
istream& operator>>(istream& in, Date& d);

13.2 Date.cpp

#include"Date.h"
void Date::Print() const
{
	cout << _year << "/" << _month << "/" << _day << endl;
}
bool Date::CheckDate() const
{
	if (_month < 1 || _month >12
		|| _day < 1 || _day > GetMonthDay(_year, _month))
	{
		return false;
	}
	else
	{
		return true;
	}
}
Date::Date(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
	if (!CheckDate())
	{
		cout << "非法日期:";
		Print();
	}
}
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)
		{
			return _day < d._day;
		}
	}
	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);
}
bool Date::operator==(const Date& d) const
{
	return _year == d._year
		&& _month == d._month
		&& _day == d._day;
}
Date& Date::operator+=(int day)
{
	if (day < 0)
	{
		return *this -= (-day);
	}
	_day += day;
	while (_day > GetMonthDay(_year, _month))
	{
		_day -= GetMonthDay(_year, _month);
		++_month;
		if (_month == 13)
		{
			++_year;
			_month = 1;
		}
	}
	return *this;
}
Date Date::operator+(int day) const
{
	Date tmp = *this;
	tmp += day;
	return tmp;
}
Date& Date::operator-=(int day)
{
	if (day < 0)
	{
		return *this += (-day);
	}
	_day -= day;
	while (_day <= 0)
	{
		_day += GetMonthDay(_year, _month);
		--_month;
		if (_month == 0)
		{
			--_year;
			_month = 12;
		}
	}
	return *this;
}
Date Date::operator-(int day) const
{
	Date tmp = *this;
	tmp -= day;
	return tmp;
}
//后置++
Date Date::operator++(int)
{
	Date tmp = *this;
	*this += 1;
	return tmp;
}
//前置++
Date& Date::operator++()
{
	*this += 1;
	return *this;
}
//后置--
Date Date::operator--(int)
{
	Date tmp = *this;
	*this -= 1;
	return tmp;
}
//前置--
Date& Date::operator--()
{
	*this -= 1;
	return *this;
}
int Date::operator-(const Date& d) const
{
	Date max = *this;
	Date min = d;
	//表示符号位
	int flag = 1;
	if (*this < d)
	{
		flag = -1;
		max = d;
		min = *this;
	}
	int n = 0;
	while (min < max)
	{
		++min;
		++n;
	}
	return n * flag;
}
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "/" << d._month << "/" << d._day << endl;
	return out;
}
istream& operator>>(istream& in, Date& d)
{
	cout << "请依次输入年月日:>" << endl;
	while (1)
	{
		in >> d._year >> d._month >> d._day;
		if(!(d.CheckDate()))
		{
			cout << "输入日期非法:";
			d.Print();
			cout << "请重新输入:" << endl;
		}
		else
		{
			break;
		}
	}
	return in;
}

13.3 Test.cpp

void TestDate1()
{
    Date d1(2025, 10, 28);
	Date d2 = d1 + 100;
	//Date d3(d1 + 100);
	d1.Print();
	d2.Print();
    d1 += 100;
	d1.Print();
	d1 -= 100;
	d2 = d2 - 100;
	d1.Print();
	d2.Print();
}
void TestDate2()
{
	Date d1(2025, 10, 28);
	d1++;
	d1.Print();
	++d1;
	d1.Print();
}
void TestDate3()
{
	Date d1(2035, 10, 28);
	Date d2(2025, 10, 28);
	cout << d1 - d2 << endl;
}
void TestDate4()
{
	Date d1, d2;
	cin >> d1 >> d2;
	cout << d1 << d2;
	cout << d1 - d2 << endl;
}
void TestDate5()
{
	const Date d1(2025, 10, 28);
	d1.Print();
	Date d2(2025, 10, 28);
	d2.Print();
	cout << &d1 << endl;
	cout << &d2 << endl;
}
int main()
{
	//TestDate1();
	//TestDate2();
	//TestDate3();
	//TestDate4();
	TestDate5();
	return 0;
}