C++类和对象补充

本文详细介绍了C++中类和对象的一些高级特性,包括构造函数中的初始化列表,强调了初始化列表对于引用和const成员变量的重要性;讲解了explicit关键字的作用,防止隐式类型转换;探讨了static成员的特性,如静态成员变量和静态成员函数;阐述了友元的概念,包括友元函数和友元类,以及它们在重载<<和>>操作符中的应用;最后提到了内部类和匿名对象的使用情况。
摘要由CSDN通过智能技术生成

目录

前言:

1. 构造函数->初始化列表

1.1 初始化列表出现原因

1.2 初始化列表写法

2. explicit关键字

2.1 explict的出现

2.2 explict的写法

3. static成员

4. 友元

4.1 友元函数

4.2 友元类 

5. 内部类和匿名对象

5.1 内部类

5.2 匿名对象


前言:

        本篇讲解的内容是类和对象的补充知识,也就是类和对象当中平时不太注意,但是有比较重要的知识。其中包括构造函数中的初始化列表、static成员、友元、内部类、匿名对象,以及部分小知识。如果小伙伴们有需要请向下查看咯。

1. 构造函数->初始化列表

1.1 初始化列表出现原因

        朋友们,还记得我上篇讲的,当我们在实例化一个对象的时候,编辑器会干嘛?会调用类里的构造函数,对类里面的成员变量作合适的初始化。如下:

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

        但是朋友们,有没有想过一个问题?那就是虽然我们在实例化的时候传入了一个参数,但是那真的算是初始化吗?可能有的小伙伴没有理解我的话。那我换一个说法,我们传入一个值用于初始化,并且将这个值赋值给了一个变量,那么这一步算是初始化吗?

        答案是否,这一个过程并不能说是初始化,我可以举一个例子:

    int a = 0;        //初始化a为0

    int b;              //创建了一个变量b
    b = 0;             //赋值b为0

         大家可以看出这两者的区别了吗?我相信是能的,那么再回到我们的构造函数,这个时候我们再看它,他是初始化还是赋值操作?相信大家心里有数了吧。

        不过话又说回来,这个初始化和赋值有什么影响吗?这么看起来好像没有区别啊?确实,如果是这种类型的变量赋值和直接初始化没有区别,但是大家可能忘了某些东西:引用、const成员变量。想到了吗?

    int& c;                        //引用不初始化
    const int d;                //  const成员变量不初始化      
    d = 10;                      //const成员变量重新赋值

         这几个写法很明显是错误的,相信大家也能够分辨出来,那么我们将这两个类型写在类里,此时我们的写法还正确吗?肯定是错误的。

        所以,C++祖师爷基于这个点,为我们的构造函数多添加了一个功能——初始化列表。

1.2 初始化列表写法

        先看一下代码是如何写的:

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

        大家看看我定义的两个类,有没有发现它里面的构造函数多了个东西?那就是以冒号开始,用逗号分隔每一个变量,每个变量加上括号和初始值。

        用上述的B类构造函数为例子:

:_aobj(a)       
, _ref(ref)
 , _n(10)

         这就是初始化列表写法,当然,如果我们初始化有特殊的需求,还是可以将功能写在初始化列表下方的花括号中,例如条件判断、申请堆上的空间之类的。

        编辑器有一个对于初始化列表的规定,那就是不管我们写不写,它都会自己去运行一遍,不过它却不会对这些变量做任何的处理,只有我们写了,他才会为其赋初值。

        还有一点需要我们注意,请先看下方代码,并计算_a1和_a2的初始值。

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

选项: 

A. 输出1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值

         答案是D,与大家计算的是否有出入了?相信多数朋友会选择a吧?因为我就是其中之一。

        其实初始化列表的初始化顺序不是我们眼睛看到的那样,它的初始化顺序是变量定义的顺序,看下代码:

private:
    int _a2;
    int _a1;

        也就是说,_a2先用_a1的初始值也就是随机值赋值了,然后_a1再通过1赋值,这也就造成了我们之前产生的小bug。

2. explicit关键字

2.1 explict的出现

        在讲解这个关键字的之前,我想问一个问题,大家知道下面的两种实例化对象的方式有什么不同吗?(不是说写法)。

Date d1(2023,2,17);

Date d2 = {2023,2,17};

         这样问估计有些难为人,我就直接讲了,第一种会直接用这些初始值通过构造函数实例化

        但是第二种不同,第二种通过这些初始值,调用构造函数实例化之后,然后通过赋值的方式将其转换给d2。大家看出区别了吗?也就是说第一种实例化只有构造函数参与,但是第二种不仅有构造函数,还有拷贝构造参与。不过对于现在的编辑器来说已经被优化了,二者都只有构造函数参与,如果按照刚出现的那段时间,用第二种在构造一个对象的时候产生两个变量大小的空间,这务必会影响我们程序运行的效率。

        那我问大家,为什么会出现这个过程呢?先看下方的代码:

    int a = 10;
    double b = (double)a;

         问:我们将a强制类型转换成double类型,int a是否还存在。

        答案是当然存在,强制类型转换只会新开辟一个临时空间存储,然后赋值给b,其中a的参与只有提供10这个数的价值。通过等效的理解方式,大家再去看上面的Date d2 = {2023,2,17};是不是瞬间就明白了?只不过该操作是隐式类型转换

        explict的出现就是为了禁止这种强制类型转换的。

2.2 explict的写法

class Date
{
public:
	explicit Date(int year)
		:_year(year)
	{}

	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;
};
void Test()
{
	Date d1(2022);

	d1 = 2023;            //错误

}

        大家可以看到,如果我们不添加explict,d1 = 2023这种写法是正确的,但是大家可以赋值这段代码进入你的编辑器看一看,这个操作是否还正确?

        所以explict的功能是:用explicit修饰构造函数,将会禁止构造函数的隐式转换。

3. static成员

        对于static关键字,想必大家还是很了解的,但是当他出现在类里面还清楚吗?

        我们都知道static描述的变量,这个变量的生命周期会到程序结束才会停止,而不是局限于函数结束就被销毁。这个特性static也延续到类当中,当用static去修饰类里面的变量,那么这个变量的生命周期就被提升,并且它并不单独属于某个实例化的类对象,而是属于整个类。

        大伙看到这估计有些疑问,他既然写在了类里面,又不属于这个类的对象,那么它在哪?答案是它被存在了静态区。也就是计算类的大小也不会将他算入其中。

class Date
{
public:
    static int a;
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	cout << sizeof(d1) << endl;
	return 0;
}

         正常来说,这个类的大小应该是16对吧,因为又4个整型变量嘛,但是其中有一个变量是static修饰的,所以它的大小不计入其中。

        因为这个static修饰的变量不属于某一个变量,所以说,没有任何一个对象想能对其进行初始化,只能有一个操作,那就是类外定义。

int Date::a = 0;

        至于为什么要这样做?想一想,要是每一个对象都能够对static初始化,那么static修饰这个变量有什么意义呢?

        并且static不只是可以修饰变量,还可以修饰成员函数,将这个成员函数变为静态成员函数。

以下几个特性需要大家知道:

1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制

         知道了这些特点,下面两个问题相信大家也能够回答了。

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

4. 友元

        友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
        友元分为:友元函数和友元类

4.1 友元函数

        一般来说友元用到的场景不会太多,但是有一个东西如果不用友元那么它的操作方式会变得十分奇怪,那就是重定义<<和>>这两个操作符。

class Date
{
public:
	Date(int year, int month, int day)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
	ostream& operator<<(ostream& _cout)
	{
		_cout << _year << "-" << _month << "-" << _day << endl;
		return _cout;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(1, 2, 3);
	d1 << cout;
	//cout << d1;   //错误
	return 0;
}

        d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用,因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧。

        还记得我上一篇讲的吗?因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

        这个时候友元函数的出现就显得十分重要了:

class Date
{
	friend ostream& operator<<(ostream& _cout, const Date& d);
	friend istream& operator>>(istream& _cin, Date& d);
public:
	Date(int year = 1900, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
	_cout << d._year << "-" << d._month << "-" << d._day;
	return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
	_cin >> d._year;
	_cin >> d._month;
	_cin >> d._day;
	return _cin;
}
int main()
{
	Date d;
	cin >> d;
	cout << d << endl;
	return 0;
}

        可以看到我们的友元函数的写法就是将函数定义在类外面,然后在类里面写下声明并在前面添加上friend关键字。声明的位置可以是任意位置,但是我们习惯将其写在类最上方,而且友元函数突破了public、protected、private的限制。也就是友元函数能够直接访问类里面的私有成员。

friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);

 特性:

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

4.2 友元类 

特性:

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

class Time
{
	friend class Date; // 声明日期类为时间类的友元类,则在日期类中就直接访问Time类
	//中的私有成员变量
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;
};

5. 内部类和匿名对象

5.1 内部类

概念:

1. 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,
它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
2. 注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。

特性:

1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系。

class A
{
private:
	static int k;
	int h;
public:
	class B // B天生就是A的友元
	{
	public:
		void foo(const A& a)
		{
			cout << k << endl;//OK
			cout << a.h << endl;//OK
		}
	};
};
int A::k = 1;
int main()
{
	A::B b;
	b.foo(A());
	return 0;
}

5.2 匿名对象

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 aa1;
    // 不能这么定义对象,因为编译器无法识别下面是一个函数声明,还是对象定义
    //A aa1();
    // 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
    // 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数
    A();
    A aa2(2);
    // 匿名对象在这样场景下就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
    Solution().Sum_Solution(10);
    return 0;
}


         以上就是我对类于对象的全部补充啦,还请大家多多支持咯!

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值