当前位置: 首页 > news >正文

C++—类与对象(中)

目录

1、类的6个默认成员函数

2、构造函数

构造函数的特性

3、初始化列表

4、析构函数

概念

5、拷贝构造函数

6、运算符重载

7、赋值运算符重载

赋值运算符重载格式

8、前置++和后置++重载

9、const修饰的成员

10、取地址及const取地址重载


1、类的6个默认成员函数

一个类中没有成员,称为空类。

任何类在什么都不写时,编译器会自动生成六个成员函数,分别是

构造、析构、拷贝构造、赋值重载、取地址重载和const对象取地址重载

2、构造函数

先来看一下这个日期类

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

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

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

int main()
{
	Date d1;
	d1.Init(2025, 2, 28);
	d1.Print();
}

我们可以看到当我们想要初始化一个结构体对象时都要手动调用初始化函数,而构造函数能帮助我们每次创建变量都能自动调用初始化函数。

构造函数是一个特殊的成员函数,没有返回值(连空都不是),函数名和类名相同,创建对象时编译器可以自动调用这个函数,对单个对象来说,在这个对象的整个生命周期只会调用一次。

构造函数的特性

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

特性:

1、函数名和类名相同
2、无返回值(就是真正的没有,连空都不是)
3、对象实例化时由编译器自行去调用构造函数
4、构造函数可以重载

构造函数可以重载

class Date
{
public:
	//无参构造函数
	Date()
	{}
	//带参构造函数
	Date(int year, int month, int day)
	{
		_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);//调用带参的构造函数
	d1.Print();
	d2.Print();
}

注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明


我们一般不这样写构造函数,一般用全缺省参数来写,这样无参带参的函数都可以调用这个函数,不用写多个构造函数。

class Date
{
public:
	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);//传了参数,使用传来的实参
	d1.Print();
	d2.Print();
}

5、如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

6、类的每个成员变量都有其对应的构造函数,一个类默认生成的构造函数其实是去调用各个成员变量对应的构造函数

通过第五个特性,我们会发现默认构造函数好像没有什么用,其实不然。

C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int/char...,自定义类型就是我们使用class/struct/union等自己定义的类型。

默认构造函数是不会处理内置类型的。

默认构造函数会调用成员变量的构造函数,如果成员变量没有显式定义构造函数,就调用默认生成的构造函数,否则就调用显式定义的构造函数。

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 6;
		_minute = 6;
		_second = 6;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
private:
	// 内置类型
	int _year;
	int _month;
	int _day;
	// 自定义类型
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

实例化对象d时调用了Date类的默认构造函数,而这个函数又去分别调用了int类型的默认构造函数,调用了3次(_year,_month,_day)和调用了Time类型的构造函数(Time类型有显式定义构造函数),int属于内置类型,不会处理,而Time属于自定义类型,故进行处理,调用了其显式构造函数


C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。

7、无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。

注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为

是默认构造函数。(都是可以不传参调用

class Date
{
public:
    //无参构造函数
	Date()
	{
		_year = 1900;
		_month = 1;
		_day = 1;
	}
    //全缺省构造函数
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};

void Test()
{
	//调用时存在歧义
	Date d1;
}

3、初始化列表

我们都知道默认构造函数会去调用成员变量的构造函数,当Date构造函数显式定义时,仍会调用成员变量构造函数,这都是初始化列表干的。

class Time
{
public:
	Time()
	{
		cout << "Time()" << endl;
		_hour = 6;
		_minute = 6;
		_second = 6;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d;
	return 0;
}

初始化列表用于在对象的构造函数中初始化成员变量,初始化列表使用冒号(:)和逗号(,)来分隔成员变量和初始值。

初始化列表的作用是在对象的构造函数中直接对成员变量进行赋值,从而避免了先默认构造再赋值的操作,减少了对象的构造和初始化时间。

只有显式定义的构造函数才存在初始化列表。

class Date
{
public:
    //使用初始化列表可以在括号写入表达式、变量等,不再拘束于定值
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

注意:

每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

类中包含以下成员,必须放在初始化列表位置进行初始化

引用成员变量
const成员变量
自定义类型成员(且该类没有默认构造函数时)

那我们能不能考虑用赋值初始化来初始化类成员呢?我们先来看看两者的区别

成员变量的默认值只能指定数据。
初始化列表则可以在括号中写入表达式,变量等等。

成员变量的默认值并没有完成真正的赋值,它是处于对变量的声明阶段,只是告诉编译器我将要使用一个名字为xxx的变量,还没有正式开辟空间。
初始化列表则是对变量的定义阶段,也就是完成了变量的第一次赋值。

class Date
{
public:
    //使用初始化列表可以在括号写入表达式、变量等,不再拘束于定值
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
    //赋值初始化
	int _year = 1;
	int _month = 1;
	int _day = 11;
};

引用和const要求定义时必须初始化,那就只有初始化列表能做到了,所以我们尽可能使用初始化列表。尽管初始化列表能完成绝大部分初始化工作,但仍有无法完成全部的情况,当初始化列表搞不定时,还需要使用函数体,可以混用。

成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1)
	{}

	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2;
	int _a1;
};


int main()
{
	A aa(3);
	aa.Print();
}

说明成员变量在类中声明次序确实是其在初始化列表中的初始化顺序。

4、析构函数

概念

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由 编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。    

析构函数名是在类名前加上字符 ~。它无参数也无返回值类型。一个类只能有一个析构函数

未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载

对象生命周期结束时,C++编译系统系统自动调用析构函数,不需要我们手动调用。

class exa 
{
public:
	//构造函数
	exa()
	{
		a = (char*)malloc(sizeof(char) * 4);
		size = 4;
	}
	//析构函数
	~exa()
	{
		free(a);
		a = nullptr;
		size = 0;
	}

private:
	char* a;
	int size;
};

看到上述代码,创建exa对象时,调用构造函数,自动初始化了一块空间给a,当对象生命周期结束,会自动调用析构函数,释放这块空间,如果没有释放,就会造成内存泄漏,析构函数的好处是我们无需手动调用。

与构造函数不同的是:一个函数只允许存在一个析构函数,不允许重载

当我们不写析构函数,编译器会默认生成一个析构函数。(这是六大默认成员函数的共有的特性)

对于默认析构函数,如果成员变量中存在其他类的对象,则会调用其他类的析构函数

注意:当我们建立了动态内存时,不能依靠默认的析构函数释放,必须自己手动释放,因为默认的析构函数仅释放类的对象调用的资源,对象是创建在栈帧的,程序结束即会销毁,而没有销毁动态内存。

5、拷贝构造函数

概念:只有单个形参,该形参是对本类的类型对象的引用(一般由const修饰),用已存在的类的类型对象创建新对象时由编译器自动调用。

拷贝构造是必须只能有一个参数,且参数类型必须是classname&,如下述代码,其拷贝构造函数只有一个参数类型为A&。

class A
{
public:
	A(int a=0,int b=0)
		:_a(a)
		,_b(b)
	{}
	//拷贝构造函数,和构造函数构成重载
	A(const A& a)
	{
		_a = a._a;
		_b = a._b;
	}
private:
	int _a;
	int _b;
};

int main()
{
	A a1(10);
	//调用拷贝构造
	A a2(a1);
	return 0;
}

如何调用拷贝构造呢?

被动调用:对象在传参和做返回值的时候需要创建一个临时变量,这个过程调用了拷贝构造。

是底层在偷偷调用

主动调用:

//由于是构造函数的重构,所以也可以使用构造函数的语法

A a1;

A a2(a1);

//能在创建对象时用赋值语句

A a1;

A a2 = a1;

拷贝构造函数特性

1、拷贝构造函数是构造函数的一个重载形式(写了拷贝构造就不会生成默认构造)。
2*、拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
3*、若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按
字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝(即一个字节一个字节地把成员变量拷贝过去)。
4、类的每个成员变量都有对应的拷贝构造函数,一个类默认生成的拷贝构造函数其实是去调用成员变量对应类型的拷贝构造函数。(内置类型对应的拷贝构造函数会进行处理,这一部分的特性和前面的构造函数有一些不同)
5*、类中如果没有涉及资源申请时,拷贝构造函数是否写都可以。一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
第2、3和5的特性解析

2、拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

class A
{
public:
	A(int a = 0, int b = 0)
		:_a(a)
		, _b(b)
	{}
	//如果此处参数类型为A,会发生无限递归
	//A(A a)
//	A(A a)//错误的写法
//	{
//		_a = a._a;
//		_b = a._b;
//	}

	//正确的写法
	A(const A& a)
	{
		_a = a._a;
		_b = a._b;
	}
private:
	int _a;
	int _b;
};

int main()
{
	A a1(10);
	//调用拷贝构造
	A a2(a1);
	return 0;
}

由于每次把对象作为函数形参传递时,都会调用其拷贝函数来进行拷贝,所以就导致下图的情况。

以此类推,将会造成无限递归,程序必定崩溃。

小结:对象在传参和做返回值的时候需要创建一个临时变量,这个过程调用了拷贝构造。我们一定要特别注意写法规范。

3、若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

如上述代码,并未显式定义A的拷贝构造函数,此时会调用默认的拷贝构造函数

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

默认的拷贝构造对于内置类型,会直接拷贝,对于自定义类型,则会调用相应的拷贝构造。

所以当涉及到资源申请时,不能单单用值拷贝,值拷贝并没有实现拷贝出来的对象的空间独立,会导致拷贝出来的对象于被拷贝的对象指向同一块内存空间,程序结束会造成多次释放一块空间,导致程序崩溃,必须要自己写拷贝构造函数。

6、运算符重载

根据C语言语法,基本类型可以被各种运算符处理,比如int与int类型经由+运算符处理可以得到两者相加,那么在面向对象的C++中,我们想要实现自定义类型的运算,比如Date类中,我们希望能够实现和基本类型一致的运算,Date+int是一个日期加几天,要怎么实现呢?

这时就需要用到运算符重载了。

函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)

返回值类型 operator 运算符(参数列表)
{
    // 实现运算符功能的代码
}

我们来看看下面这个例子

class A
{
public:
	A(int a = 0, int b = 0)
		:_a(a)
		, _b(b)
	{}
	bool operator==(const A& x)
	{
		return (x._a == _a) && (x._b == _b);
	}
private:
	int _a;
	int _b;
};

int main()
{
	A a1(5,10);
	A a2(5,10);
	A a3(10, 20);
	//(a1 == a2)实际上就是a1.operator==(&a1,a2)
	//cout本质也是函数重载,不加括号就会先和a1结合
	cout << (a1 == a2) << endl;//输出1
	cout << (a2 == a3) << endl;//输出0

}

注意事项:

(3)注意
1、不能通过连接其他符号来创建新的操作符:比如operator@重载操作符必须有一个类类型参数。
2、作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
3、*(解引用)、::(作用域限定符)、 sizeof 、?:(三目操作符)、 .(类成员访问操作符)、 注意以上5个运算符不能重载。
4、运算符重载的结合顺序与运算符优先级、结合性相关。
5、运算符重载大多数情况下作为类的成员函数,做不了的情况下只能做成正常函数,本质都是替换成函数调用。

6、用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。

7、赋值运算符重载

赋值运算符重载格式

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

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

3、检测是否自己给自己赋值。

4、返回*this :要复合连续赋值的含义。

class Date
{
public:
	Date(int year = 1970, 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;
};

int main()
{
	Date d1;
	Date d2;
	Date d3(2025,2,28);
	d1 = d2 = d3;
	//连续赋值,相当于先d2 = d3(d2.operator=(&d2,d3)),返回值为d2的引用
	//然后d1 = d2(相当于d1.operator=(&d1,d2))
	return 0;
}

由于赋值重载也是六大默认成员函数,所以没有显式定义实现赋值重载时也会生成一个默认的赋值重载,这个赋值重载是浅拷贝。它的内部规则和构造函数几乎相同,即内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

对比拷贝构造方式

Date d1(2025,2,28);
Date d2(d1);
Date d3 = d1;

Date d3 = d1,是定义,调用的是拷贝构造

d1 = d3时,是赋值,调用的是赋值重构

=这个运算符,定义是用拷贝构造,赋值时用拷贝重构。

注意:赋值运算符只能重载成类的成员函数不能重载成全局函数。

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

下面这段代码会编译失败

class Date
{
public:
	Date(int year = 1970, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = 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;
};

Date& operator=(Date& d1, const Date& d2)
{
	if (&d1 != &d2)
	{
		d1._year = d2._year;
		d1._month = d2._month;
		d1._day = d2._day;
	return d1;
}

8、前置++和后置++重载

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

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

int main()
{
	Date d;
	Date d1(2023, 10, 24);
	d = d1++;//此时d:2023,10,24  d1:2023,10,25
	d = ++d1;//此时d:2023,10,26  d1:2023,10,26
	return 0;
}

Date& operator++()就是前置++的重载,而Date& operator++(int)就是后置++的重载。

前置--和后置--类似。

9、const修饰的成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

在函数后加上const即是告诉编译器把被隐藏的this的类型前面加上const。

无论是const对象和非const对象都可以调用const成员函数,而const对象不能调用非const成员函数。

class Date
{
public:
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << "Print()" << endl;
	}
	/*void Print() const
	{
		cout << "Print()const" << endl;
	}*/
private:
	int _year;
	int _month;
	int _day;
};


int main()
{
	Date d1(2025,2,28);
	d1.Print();
	const Date d2(2024, 2, 28);
	d2.Print();
	return 0;
}

原因:

不是const成员函数的话,this指针转递的类型只是Date*类型,不是const Date*类型,所以此时const对象不能调用这个函数,这属于权限放大,是不允许的。而权限缩小可以被允许,所以非const对象也能调用const成员函数。

成员函数定义规则:

1、能定义成const的成员函数都应该定义成const,这样const对象和非const对象都可以调用。
2、要修改成员变量的成员函数,不能定义成const,const对象不能调用(很合理),非const才能调用。

10、取地址及const取地址重载

取地址重载和const取地址重载,就是对&操作符的重载。一般不用重新定义 ,编译器默认会生成。只有特殊情况,才需要重载,比如想让别人获取到指定的内容。

//取地址
Date* operator&()
{
	return this;
}
//const取地址
const Date* operator&() const
{
	return this;
}

相关文章:

  • JAVA笔记【一】
  • 张雪峰教育观点及争议分析
  • 【java】@Transactional导致@DS注解切换数据源失效
  • 为AI聊天工具添加一个知识系统 之128 详细设计之69 编程 核心技术:Cognitive Protocol Language 之1
  • AutoMQ:无需 Cruise Control 实现 Kafka 的自动分区再平衡
  • GitLab 密钥详解:如何安全地使用 SSH 密钥进行身份验证
  • 用Python之requests库调用大型语言模型(LLM)API的流式输出与非流式输出比较
  • [C#]C#移动文件报错完全限定文件名必须少于 260 个字符,并且目录名必须少于 248 个字符
  • 新版的 distrobox 首先需要:设置密码
  • Spring Boot问题总结
  • deepseek+mermaid【自动生成流程图】
  • 独立开发者的内容营销教程
  • MySQL双主搭建-5.7.35
  • Vue 3 新特性:对比 Vue 2 的重大升级
  • V-HOP:结合视觉和触觉多模态融合数据集,助力机器人实现鲁棒的6D物体姿态跟踪
  • Ubuntu的tmux配置
  • 51单片机制作彩屏触摸小电子琴STC32G12K128+RA6809+彩屏1024x600
  • 02.网络编程套接字(一)
  • 进程的状态 ─── linux第11课
  • 【深度学习神经网络学习笔记(一)】深度学习介绍
  • 深圳网站定制公司/百度seo和谷歌seo有什么区别
  • 文山专业网站建设报价/沈阳专业seo关键词优化
  • 怎么做弹幕网站/营销网站建设选择原则
  • 辽宁省营商建设监督局网站/长沙网站定制公司
  • 洛阳做网站哪家便宜/搜索引擎营销是什么意思
  • 岳阳建设公司网站/百度推广营销页