C++类和对象(二)类的默认成员函数:取地址及const取地址重载 | 初始化列表 | 友元 | 隐式类型转换

前言:

        本篇文章我们先对之前未完成的内容进行补充,之后还有很多重磅内容,我们都需要去了解,废话不多说,开始吧。

类的默认成员函数(补档):

        之前我们只介绍了4个,一共有6个,那么今天我们就来把剩余两个介绍一下。

取地址重载:

class Date
{
public:
	Date(int year = 2000, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
    // 取地址重载
	Date* operator&()
	{
		return this;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
 
int main()
{
	Date d1;
	const Date d2;
	cout << &d1 << endl;
	return 0;
}

        这个其实很鸡肋,没啥用,我们平时使用编译器生成的就行了。

const取地址操作符重载: 

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

        这个和取地址操作符重载没有区别。

友元: 

        我们之前在日期类中直接使用了但是没有详细讲解。

        在C++中,友元(friend)是一个特殊的关键字,它允许某个函数或另一个类访问当前类的私有(private)和保护(protected)成员。友元关系可以是单向的,即被声明为友元的函数或类可以访问当前类的私有成员,但当前类不能访问友元的私有成员。

        只需要在类的内部添加上类外定义的函数的声明,并在声明前加上关键字friend即可,一般这种友元函数允许写在类内部的任意地方,一般来说会把它放在整个类的开头。 当一个函数成为一个类的友元,那么这个函数内部就可以随意使用类中的私有(private)或保护(protected)成员了

        当时我们为了去访问私有成员,所以我们将这两个全局函数设置为Date类友元。

友元函数:

        我们上面就已经设置了友元函数。

说明:

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

友元类: 

        友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

友元类的一些特性:

  • 友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
  • 友元关系不能传递。如果C是B的友元, B是A的友元,则不能说明C时A的友元。
  • 友元关系不能继承。在继承位置再给大家详细介绍。

        我们给出代码方便理解:

class Time
{
	// 声明Date类为Time类的友元类,则在Date类中就可以直接访问Time类中的私有成员变量
	friend class Date; 
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		: _hour(hour)
		, _minute(minute)
		, _second(second)
	{}
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)
	{}
	void SetTimeOfDate(int hour, int minute, int second)
	{
		// 直接访问时间类私有的成员变量
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
}

        也就是说,A是B的友元,A中就可以直接访问B。注意友元的关系是单向的。

初始化列表:

        这是本篇的王炸,大家做好迎击准备(一段神秘的呓语:su gu mu kae u tsu jun bi wo)!

        C++的初始化列表(Initializer List)是构造函数的一种特性,用于初始化类的数据成员。在构造函数体执行之前,初始化列表会先执行,确保数据成员在构造函数体开始执行之前就已经被正确地初始化

        初始化列表的使用:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式。

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

        这是什么含义呢?我们平时不用初始化列表会这样写:

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

        这两段内容使其目的和结果都是相同的,但是本质上是有区别的。

        我们为了方便讲解,使用一个题目来说明:用栈实现队列的代码来讲解。

class stack
{
public:
	stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}
	void push(int x)
	{
		_a[_size++] = x;
	}
 
private:
	int* _a;
	int _size;
	int _capacity;
};
class MyQueue
{
private:
	stack _pushst;
	stack _popst;
	int _size;
};

        这里有一个大家可能会忽视的问题,就是stack类中已经有了默认构造函数。

        方便复习,重要的事情说三遍!

        默认构造函数只能有一个。注意:无参构造函数、全缺省函数、编译器默认生成构造函数,都可以认为是默认构造函数。

         所以此时stack中已经有了默认构造函数,那么接下来我们如果再stack类中没有默认构造参数,必须提供一个值才能正常使用,这是该怎么办呢?

class stack
{
public:
	stack(int capacity)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}
	void push(int x)
	{
		_a[_size++] = x;
	}
 
private:
	int* _a;
	int _size;
	int _capacity;
};
class MyQueue
{
private:
	stack _pushst;
	stack _popst;
	int _size;
};

        以上代码会报错,因为默认构造无法生成。 此时只能在MyQueue中显示的写构造了。此时stack没有提供默认构造,只能使用初始化列表进行初始化!

class stack
{
public:
	stack(int capacity)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}
	void push(int x)
	{
		_a[_size++] = x;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};
class MyQueue
{
public:
	// stack 不具备默认构造,只能MyQueue显示的写构造
	// 此时只能使用初始化列表
	MyQueue(int n = 20)
		: _pushst(n)
		, _popst(n)
		, _size(0)
	{

	}
private:
	stack _pushst;
	stack _popst;
	int _size;
};

int main()
{
	MyQueue q(10);
	return 0;
}

        其本质可以理解为每个对象中成员定义的地方。

        这里我们可以发现,其实我们不在初始化列表中初始化也可以,但是有3个例外。

以下三种类的成员,必须放在初始化列表的位置进行初始化:

  • 引用成员变量(因为要先有具体变量,才能有引用)
  • const成员变量(因为只有一次赋值机会)
  • 自定义类型成员(且没有默认构造函数)

        我们以代码的形式进行说明:

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
};

        建议:能在初始化列表中初始化就在初始化列表中初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

        还没完,王炸怎么可能就这点,观察以下代码:

class stack
{
public:
	stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}
	void push(int x)
	{
		_a[_size++] = x;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};
class MyQueue
{
public:
	// stack 不具备默认构造,只能MyQueue显示的写构造
	// 此时只能使用初始化列表
	MyQueue()
	{
		_size = 0;
	}
private:
	stack _pushst;
	stack _popst;
	int _size;
};

int main()
{
	MyQueue q;
	return 0;
}

        我们调试一下:

        所以你无论写不写,对于自定义类型,都会在初始化列表中调用它的默认构造(注意,此时stack类中有默认构造,没有默认构造会报错)。
        我们之前将可以给成员缺省值,这个缺省值其实就是给初始化列表用的。

class stack
{
public:
	stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		_size = 0;
		_capacity = capacity;
	}
	void push(int x)
	{
		_a[_size++] = x;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};
class MyQueue
{
public:
	// stack 不具备默认构造,只能MyQueue显示的写构造
	// 此时只能使用初始化列表
	MyQueue()
	{

	}
private:
	stack _pushst;
	stack _popst;

	//此时给定了缺省值
	int _size = 0;
};

         如果此时在初始化列表中给定了_size的值,那么缺失值讲不起作用,比如:

MyQueue()
	: _size(5)
{

}

        此时我们要注意,一定是先走初始化列表,之后再走函数体,所以效率会提高。所以实际中我们尽量使用初始化列表初始化。

        我们来看一道面试题:

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(1);
	aa.Print();
	return 0;
}

        这是为啥?因为成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关!

        这句话很重要。因为先声明的是_a2,所以在初始化列表中会先执行_a2(_a1),之后执行_a1(a),所以_a2为随机值。

初始化列表的特点:

  • 初始化列表,不管写没写,每个成员变量都会走一遍,而且在初始化列表中只能出现一次(初始化只能初始化一次)
  • 对于自定义类型,会调用默认构造(没有默认构造则报错)。
  • 先走初始化列表,再走函数体。
  • 拷贝构造也有初始化列表。
  • 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。

隐式类型转换:

        之前我们讲过,不同类型的内置类型变量在相互赋值时会有隐式类型转换。

double a = 10.5;
int b = a;

        就如上面这个简单的赋值,在a赋值给b之前,会产生一个临时变量,最终赋给b值的就是这个临时变量。 

        当将不同类型的变量取引用时,需要加const的原因,是因为临时变量具有常性。

double a = 10.5;
// int& b = a;// 报错
// int& c = 10;// 报错
const int& b = a;// 正常运行
const int& c = 10;// 正常运行

        上述代码中b取的就是a产生的临时变量的引用,临时变量存储在内存的静态区,具有常性,就跟第四行代码的数字10性质是一样的,当你加上const时,这种引用权限就被放开了,因为const确保了你不会对静态区的变量做出改动。对于C++的自定义类型,与内置类型遵循的规则是一样的。 

单参数构造: 

        C++支持一种类型转换式的构造:

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

private:
	int _a;
};

int main()
{
	A aa1(1);
	A aa2 = aa1;//拷贝构造

	//隐式类型转换
	A aa3 = 3;
	return 0;
}

        这里是内置类型转换为自定义类型。这里是单参数构造函数可以这样。至于用处嘛,看以下代码:

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

private:
	int _a;
};

class Stack
{
public:
	void Push(const A& aa)
	{
		//...
	}
	//...
};

int main()
{
	Stack st;
	A a1(1);
	st.Push(a1);//这样写很冗余

	st.Push(2);//可以直接这样写
	return 0;
}

        这样写就很爽。

多参数构造: 

        至于多参数构造,需要换一种写法。 

class A
{
public:
	A(int a1, int a2)
		:_a1(a1)
		,_a2(a2)
	{}

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

private:
	int _a1;
	int _a2;
};

int main()
{
	A a1 = { 3, 2 };
	A a2{ 4, 5 };//这两者等价,但是不建议下面这样写
	a2.Print();
	return 0;
}

 

explicit关键字: 

        这个知识点稍稍提一下,如果不想允许构造时出现类的隐式类型转换,可以在拷贝构造前加个explicit关键字,就可以成功限制类的隐式类型转换了。

class A
{
public:
	//此时就限制了隐式类型转换
	explicit A(int a)
		:_a(a)
	{}

private:
	int _a;
};

int main()
{
	A a1 = 3;
	return 0;
}

        关于它的更多内容,我们后续再讲。 

总结:

        我们要多去使用才能更好的掌握,加油吧各位!

         

  • 34
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值