再谈构造函数,初始化列表,匿名对象,静态成员以及友元(c++)

本文详细介绍了C++中构造函数、初始化列表、成员变量初始化、const成员变量、引用成员变量、自定义类型成员变量的初始化方式。强调了初始化列表的重要性,如在初始化const和引用成员时的必要性。此外,还讨论了匿名对象、explicit关键字、静态成员变量和函数、友元以及C++11特性。文章通过实例展示了如何避免隐式类型转换,以及如何使用友元函数访问私有成员。最后,探讨了const成员函数和取地址运算符重载的使用规则及其注意事项。

1. 再谈构造函数

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

};

在类中,成员变量第一次是以声明的形式出现。在我们之前讲过,构造函数的作用并不是构造一个对象,而是初始化对象。那么像构造函数里的,他是初始化吗,显然不是的,每一次调用我们都可以赋给他不同的值,而初始化只会初始化一次。

    Date(int year;int month;int day)
	{
		_year=year;
		_month=month;
		_day=day;
	}

1.1 初始化成员列表

那他是在哪里初始化的呢,其实是隐藏了一个初始化列表。这个初始化列表是对象的每个成员变量初始化的地方。
第一个我们直接在函数体内赋值,其实也有初始化列表,只不过没有显示且初始化的是随机值。
语法格式如下:

Date(int year, int month, int day)
		: _year(year)
	    , _month(month)
		, _day(day)

	{}
	

1.2 函数体内赋值和初始化成员列表区别

这两种有什么区别呢?其实就像下面这两种写法。

int a;
a=10;

int a=10;

1.3 初始化成员列表的作用

1.3.1 const成员变量

假如类里面声明了一个const成员变量

//错误的写法
class Date
{
public:
	Date(int year, int month, int day)
	{
		_year=year;
		_month = month;
		_day = day;
		x = 10;

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

};

直接会有报错。
在这里插入图片描述
其实也不难想,声明了一个常量,在定义的时候需要对它进行初始化,在函数里面已经是赋值了。这时候就需要初始化成员列表。

Date(int year, int month, int day)
        : _year(year)
	    , _month(month)
		, _day(day)
		, x(10)
{}

1.3.2 引用成员变量

在之前学习引用的时候,我们知道在定义的时候也需要初始化,所以和const成员变量一样,定义的时候就要初始化。

class Date
{
public:
Date(int year, int month, int day)
		: _year(year)
	    , _month(month)
		, _day(day)
		, x(10)
		,ret(day)
		//让他成为day的引用,ret改变day改变

	{}
private:
     
	int _year;
	int _month;
	int _day;
	const int x;
	int& ret;

};
1.3.3 自定义类型成员变量
//错误的例子
class Time
{
public:
	Time(int hour, int minute, int seconds)
		:_hour(hour)
		, _minute(minute)
		, _seconds(seconds)
	{}
private:
	int _hour;
	int _minute;
	int _seconds;
};

class Date
{
public:
	public:
    Date(int year, int month, int day)
		: _year(year)
	    , _month(month)
		, _day(day)
		, x(10)
		,ret(day)
		//让他成为day的引用,ret改变day改变

	{}
private:
	int _year;
	int _month;
	int _day;
	const int x;
	int& ret;
	Time _t;

};

假如Date类里面有个自定义Time类型,定义Date对象时,会调用Time类里的默认构造函数,假如Time类里没有默认构造函数,那么也需要我们在初始化列表里初始化。
在这里插入图片描述

//正确做法
Date(int year, int month, int day,int hour,int minute,int seconds)
        : _year(year)
	    , _month(month)
		, _day(day)
		: x(10)
		, ret(day)
		, _t(hour,minute,seconds)
		{}

其实初始化列表,函数体内赋值也可以混写,不过有点四不像,假如需要初始化列表就全用初始化列表,写函数体内赋值就都写函数体内赋值。

自定义类型有默认构造函数时,可以使用初始化成员列表,也可以不使用。
不过建议有默认构造函数也使用初始化成员列表。

当我们不使用初始化成员列表时

	Date(int year, int month, int day, int hour, int minute, int senconds)
	{
		_year = year;
		_month = month;
		_day = day;
		Time t(hour, minute, senconds);
		_t = t;
	}

隐藏的初始化成员列表会调用time的默认构造函数,t对象调用构造函数,_t=t又调用了一次赋值重载函数。

假如写入初始化成员列表中:

Date(int year, int month, int day,int hour,int minute,int seconds)
		: _t(hour,minute,seconds)
		,_year (year)
	    ,_month (month)
	    ,_day  (day)
	{}

只调用一次构造函数。
反正能使用初始化列表就用初始化列表。

1.4 初始化列表的顺序

在这里插入图片描述

列表初始化的顺序必须和类中声明的顺序保持一致,不然就会产生错误。比如这里实际先初始化的是a2,由于a1是随机值,所以a2就是随机值,a1是1。
输出结果就是1,随机值

2. 匿名对象

class A
{
public:
	A(int a)
		:_a(a)
	{}

private:
	int _a;
};

int main()
{
	A a1(1);//在main函数域
	A(1);//匿名对象生命周期在这一行

	return 0;
}

匿名对象有什么用呢。
比如像leetcode上,我们使用c++,进行oj的时候会创建一个solution类,测试的时候就直接sloution().方法(参数)。这就是一个匿名类的应用场景,不需要专门再去创建一个对象去调用方法。
当形参引用匿名对象时,形参必须为const.

3. explicit关键字

延续上面的代码来继续讲解,这里我们看到给a1对象赋值了一个3,都不是一个类型竟然成功了,输出3。其实这里是发生了隐式类型转换。
3会转换成一个匿名对象,然后赋值重载给a1。
在这里插入图片描述
3会转换成一个匿名对象,然后拷贝构造给a2。但是编译器将这两个过程优化合并直接调用构造函数
在这里插入图片描述

可以类比引用,中间发生的隐式类型转换

int i=10;
const double& a=i;

怎么来证明它发生隐式类型转换了呢,c++专门提供了个关键字explicit来阻止隐式类型转换,我们在构造函数前面加上这个关键字,如果刚才那条语句不成功,就说明那里准备要发生隐式类型转换,只是被explicit阻止了。
在这里插入图片描述
看上去没什么差别,实际上这种隐式类型并不是多此一举。比如说用匿名sloution类调用函数。

第一种是先构造一个str对象,然后做参数传入函数,拷贝构造
在这里插入图片描述
第二种用string类构造一个匿名对象,传入函数,拷贝构造
在这里插入图片描述
第三种直接传进去,然后让他自己进行隐式类型转换,成为匿名对象然后拷贝构造。只不过编译器直接优化让他只调用构造函数
在这里插入图片描述
第三种看上去更舒服。

注意

假如函数的形参是引用匿名对象的话,则必须给形参加上const。
在这里插入图片描述
因为"1234",隐式类型转换为临时匿名对象,具有常性。需要加const。整个生命周期在这个函数内。
我又想到了那么在main函数内,假如我引用一个匿名对象呢
在这里插入图片描述
可以看到这一行结束,并没有调用析构函数来结束他的生命。而是等main函数走完,才进行清理资源。
在这里插入图片描述
等等,我又发现为啥形参需要把引用写成const,我刚才在main里没有写const还调用成功了。
那是因为你调用的时候,直接传了"1234",他还要隐式类型转换成临时匿名对象。当然要加const。
你在main函数里直接写的是匿名对象Date(3)不需要隐式类型转换就不用加const了。假如你写一个需要隐式类型转换的
在这里插入图片描述
就需要加const了

4. 静态成员变量

面试题:怎么计算一个类有多少个对象。

先来看看静态成员变量,我们可以把成员变量加上static,他就不属于某个对象,而是属于整个类。在类内声明,而且需要在类外定义。
用sizeof计算一下,发现他是个空类,占一个字节(表示类存在)
在这里插入图片描述
在构造函数和拷贝构造里面++_b,由于_b是属于类的,所以调用了多少次构造和拷贝构造都会被计数器计数。面试题就解决了。
在这里插入图片描述
假如成员变量是公有的,类也可以调用,那么对象也可以调用,但是这样(b3._b)并不代表解引用(去对象中找他的变量)而是去对象(b3)的类(B)中去寻找静态变量_b。

私有的只能用一个成员函数,来输出类静态变量

5. 静态成员函数

相比于非静态成员函数来说,静态成员函数没有this指针.
可以思考两个问题,时刻记住静态成员函数没有this指针就好。

  1. 静态成员函数可以调用非静态成员函数吗?
  2. 非静态成员函数可以调用类的静态成员函数吗?

5.1 静态成员总结

  1. 静态成员为所有类对象所共享,不属于某个具体的实例
  2. 静态成员变量必须在类外定义,定义时不添加static关键字
  3. 在成员函数中类静态成员可用类名::静态成员或者对象.静态成员来访问,在类外也可以(不是私有的话
  4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
  5. 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值

6. c++11

声明的时候给缺省值。
在这里插入图片描述
c++98中,编译器默认生成的构造函数,针对内置类型没有处理,会对自定义类型调用默认构造函数。在c++11中,在声明的时候给缺省值(很像定义但他不是定义!!!)

7. 友元

7.1友元函数

我们用cout,或者cin想输出或输入一个自定义类型的对象,需要重载操作符

class Date
{
public:
	Date(int year=1900, int month=1, int day=1)
	:_year(year)
	, _month(month)
	, _day(day)
	{}
	ostream& operator<<(ostream& out)
	{
		//this->_year,this->_month,this->day
		out<<_year<< _month <<_day<<endl;
		return out;
	}
private:
	int _year;
	int _month;
	int _day;

};

当我们调用他的时候,有两种方法:

    Date d1;
	d1.operator<<(cout);
	d1 << cout;

但是第二种看起来怪怪的,在平常我们输出内置类型的时候通常是

int i=10;
cout<<i;

但对于输出对象我们只能把d1放在左边,因为作为成员函数,指向对象d1的隐含的this指针始终是作为第一个参数,输出流作为第二个参数,所以就是这样的形式 d1<<cout;
那交换这两个参数的位置呢,交换不了,this永远是第一个,这样写只是相当于给他多加了一个参数。
在这里插入图片描述
那我们不把他作为成员函数。放在外面,没有this指针。可以交换两个参数的位置。但是呢成员变量是私有的外面有访问不了。
在这里插入图片描述
所以c++又给出了友元friend。在类里声明这个函数为friend。代表他可以访问这个类的私有成员和保护成员。

class Date
{
	friend ostream& operator<<(ostream& out, const Date& d);
public:
	Date(int year=1900, int month=1, int day=1)
	:_year(year)
	, _month(month)
	, _day(day)
	{}
	//ostream& operator<<(ostream& out)
	//{
	//	//this->_year,this->_month,this->day
	//	out<<_year<< _month <<_day<<endl;
	//	return out;
	//}
private:
	int _year;
	int _month;
	int _day;

};

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year <<"-"<< d._month <<"-"<< d._day<<endl;
	return out;
}



int main()
{
	Date d1;
	Date d2(2000, 5, 6);
	cout << d1 << d2 << endl;
	return 0;
}

现在的场景是,Date类里有个Time类型的成员变量,重载了对象的输入输出,作为Date的友元函数,可是现在输入输出只有Date的内置类型,怎么才能输出Time成员变量的几个参数呢

#include<iostream>
using namespace std;

class Time
{
public:
	Time(int hour,int minute,int seconds)
		:_hour(hour)
		, _minute(minute)
		, _seconds(seconds)
	{}
private:
	int _hour;
	int _minute;
	int _seconds;
};

class Date
{
	//重载输入输出
friend istream& operator>>(istream& in, Date& d);
friend ostream& operator<<(ostream& out, const Date& d);

public:
	Date(int year, int month, int day)
		:_year( year)
		, _month(month)
		, _day(day)
	{}

private:
	int _year;
	int _month;
	int _day;

};
istream& operator>>(istream& in,Date& d)
{
	cout << "请输入 年 月 日" << endl;
	in >> d._year >> d._month >> d._day;
	return in;
}
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-"<<d._month <<"-"<< d._day << endl;
	return out;
}
int main()
{
	Date d1(2000, 1, 1);
	cin >> d1;
	cout << d1;
	return 0;
}

这样访问是不被允许的,所以我们在将这个函数变成Time的友元函数,就可以直接访问了

ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-"<<d._month <<"-"<< d._day << endl;
	out << d._t._hour << "-" << d._t._minute << "-" << d._t._seconds << endl;
	return out;
}

将它加在time类当中

friend ostream& operator<<(ostream& out, const Date& d);

但是由于Date类型在Time类后面定义,你把声明写在前面的时候形参有个const Date& d,他不认识,所以在最前面在加一个Date声明。就完成了。

#include<iostream>
using namespace std;

class Date;
class Time
{
	friend ostream& operator<<(ostream& out, const Date& d);
public:
	Time(int hour,int minute,int seconds)
		:_hour(hour)
		, _minute(minute)
		, _seconds(seconds)
	{}
private:
	int _hour;
	int _minute;
	int _seconds;
};

class Date
{
	//重载输入输出
friend istream& operator>>(istream& in, Date& d);
friend ostream& operator<<(ostream& out, const Date& d);

public:
	Date(int year, int month, int day,int time,int minute,int seconds)
		:_year( year)
		, _month(month)
		, _day(day)
		, _t(time,minute,seconds)
	{}

private:
	int _year;
	int _month;
	int _day;
	Time _t;

};
istream& operator>>(istream& in,Date& d)
{
	cout << "请输入 年 月 日" << endl;
	in >> d._year >> d._month >> d._day;
     
	return in;
}
ostream& operator<<(ostream& out, const Date& d)
{
	out << d._year << "-"<<d._month <<"-"<< d._day << endl;
	out << d._t._hour << "-" << d._t._minute << "-" << d._t._seconds << endl;
	return out;
}
int main()
{
	Date d1(2000, 1, 1,23,14,13);
	/*cin >> d1;*/
	cout << d1;
	return 0;
}


这也正说明了他可以是多个类的友元函数。

友元函数的特点:

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用const修饰
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用和原理相同

7.2 友元类

在上面代码的基础上,假如我们想要在这个Date类中,直接用自定义类型_t来访问它的成员变量是不行的,因为_hour等是私有的无法访问

	Date(int year, int month, int day,int hour,int minute,int seconds)
		:_year( year)
		, _month(month)
		, _day(day)
		, _t(hour,minute,seconds)
	{
		_t._hour=30;//报错
	}

所以我们在Time类中声明Date是自己的友元( friend Date;
可以访问Time的私有。(Date类中_t._hour=30;不会报错了)
需要注意友元是单向的且不具有传递性。(其实一定程度也破坏了封装性,尽量不要使用友元

8. 内部类

class A
{
private:
	static int _a;
	int _c;
public:
	class B
	{
	public:
		void fool(const A& a)
		{
			cout<<_a;
			cout << a._c;
		}
	};

};
int A::_a = 1;

B就是一个内部类。而且B天生就是A的友元。定义B的对象可以用
A::B b;来定义

9. 补充(const修饰成员函数以及最后两个默认生成的成员函数)

9.1 const修饰成员函数

#include<iostream>

using namespace std;

class Date
{
public:
	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(2000, 8, 1);
	d1.print();
	return 0;
}

当d1调用print时,print函数有着隐含的this指针,指针类型为Date* ,指向d1。
现在我们让d1对象作为只读即前面加const。
在这里插入图片描述
会报错,不难想,因为print函数里隐含的this指针为Date*,而我们此时传入的&d1,为const Date*,权限被放大,不被允许。
所以我们在print后面加上const,修饰this指针,表示所指向的对象不能被修改,这个时候就可以传递了,因为参数类型相同。

而且Date*,也可以被传递,因为权限缩小是可以的。

    .....
void print()const
	{
		cout << _year << "-"<< _month <<"-"<< _day << endl;
	}
	....

    int main()
{

	const Date d1(2000, 8, 1);
	d1.print();
	Date d2(d1);
	d2.print();
	return 0;
}

无语法错误,正常输出
在这里插入图片描述
有4道练习题可以练习一下。

  1. const对象可以调用非const成员函数吗?
  2. 非const对象可以调用const成员函数吗?
  3. const成员函数内可以调用其它的非const成员函数吗?
  4. 非const成员函数内可以调用其它的const成员函数吗?

8.2 取地址与const取地址运算符重载

对对象进行取地址,其实也是重载了运算符&,成员函数内部返回this指针就好


	Date* operator&()
	{
	    cout << "Date* operator&()" << endl;
		return this;
	}
	const Date* operator&()const
	{
	cout << "const Date* operator&()const" << endl;
		return this;
	}
....
int main()
{
	
	const Date d1(2000, 8, 1);
	Date d2(d1);

	cout<<&d2<<endl;
	cout<<&d1<<endl;
	
	return 0;
	}

只是需要注意的是,刚才提到,假如对象为const类型,第一个成员函数没有用const修饰this指针(指针指向的对象不变),第一个函数传不进去,所以需要为const对象写出第二个函数。
在这里插入图片描述
当我们不写时,它们会自动生成,与其他几个默认生成的构造函数不同,它几乎所有场景都够用了,但是什么时候我们需要重新编写它。
有这种场景,我们不想让别人返回普通对象的地址,const对象才能取到地址.
我们可以只重写普通对象的取地址重载函数,让他返回nullptr。另外一个可以用编译器默认生成的。

Date* operator&()
	{
		cout << "Date* operator&()" << endl;
		return nullptr;
		
	}

只返回了const对象的地址。
在这里插入图片描述
那假如我只想取普通对象地址,不想取const对象地址,像刚才一样只重写const修饰的取地址运算符重载函数,让他返回空。
却突然发现两个都返回了nullptr
在这里插入图片描述
其实也可以考虑到,因为普通对象的this指针为Date*,函数形参的this指针为const Date*,权限缩小,也可以传递,两个对象都可以调用const修饰的取地址运算符重载函数。
所以当我们只想取普通对象的地址,不想取const对象的地址,我们需要同时重新编写这两个成员函数,让他自己调用自己的。

   Date* operator&()
	{
		cout << "Date* operator&()" << endl;
		return this;
		
	}
	const Date* operator&()const
	{
		cout << "const Date* operator&()const" << endl;
		return nullptr;
		
	}

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

楠c

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值