类与对象【下篇】-- 关于类的其它语法

发布于:2025-07-10 ⋅ 阅读:(20) ⋅ 点赞:(0)

前言

类与对象上介绍了类与对象的一些基础语法;在类与对象中介绍了关于类的四大默认成员函数,接下来的下篇,主要是为大家介绍一些关于类与对象的其它用法,完善部分类与对象的知识体系。

1. const对象

我们都知道,有一些变量是具有const属性的,意味着它们的属性是不允许改变的
例1.1:

#include<iostream>
using namespace std;

int main()
{
	const int a = 10;
	//a = 5; 被修改是不被允许的!
}

那么对于我们的类对象来说,也是一致的,下面我们就来探讨这个话题

1.1 类的const成员函数

我们都知道:一个变量被const修饰过后,就不能被外部改变其中的值,那么对于类来说,当然也不要被其内部的成员函数所改变。对于类的成员函数来说,它们都是通过this指针来找到对应的对象的内部成员的,所以为了防止改变this指针所指向的内容,我们理应给其加上const,但是:this指针不允许显示传参,所以语法如下:

class A
{
public:
	void function() const
	{}
//……
}

对于其中的this指针,类型应该为:const A* const this这样就做到了this指向的对象的内容不可更改!

例1.1.1:

#include<iostream>
using namespace std;
class date
{
public:
	date(int year = 0, int month = 0, int day = 0)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
	void print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

适合在一些不需要改变对象内部的值的函数内部进行修饰。注意成员函数也是函数,肯定也是满足函数的特性的:

  • const成员函数和普通成员函数可以共存。在之前的函数重载已经介绍过了。const修饰也是一种参数列表的不同!其次调用函数也是一致的:调用类型最匹配的函数;非const对象也可以调用const成员函数(权限的缩小)。
  • 非const复用const。(当然前提条件是保证你的转型是安全的,小心临时变量……)对于一些接口(例如一些operator[ ]类似的),对于const成员函数和非const成员函数相同的部分,我们可以尝试用非const成员函数来调用const成员函数,以达到复用的效果。如何调用呢?运用类型转换操作符:const_caststatic_cast

接下来就是几个关于const成员函数的问题了:

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

这些问题交给大家自己验证!

1.2 const对象的潜在隐患

const对象,一定就是你想的const了吗?
来看下面一个示例,例1.2.1:

#include<iostream>
using namespace std;
class Vector
{
public:
	Vector()
		:_begin(new int[4])
		,_capacity(4)
	{}
	~Vector()
	{
		delete[] _begin;
	}
	void func() const //const成员函数
	{
		_begin[1] = 13; //改变了值
	}
private:
	int* _begin;
	int _size = 0;
	int _capacity = 0;
};

int main()
{
	const Vector v;
	v.func();
	return 0;
}

很显然,我们是可以改变_begin这个指针指向的内容的。你说这个做到了不可更改了吗?当然,因为_beign,_size,_capacity这几个变量都不可以改变。但是_begin指向的内容被改变了,但似乎指向的内容没有改变。它们的关系似乎是以下的关系:
Alt

Scott Meyers所著的《Effective C++》中条款3中明确谈到了这个问题,并引出了logical constnessbitwise constness这两种分派,大家可以进行阅读!

2. static成员

static这个关键词我们已经不陌生了,至于其的常规语法特性,我们也不做过多解释。这里还是从两个方面来涉及:static成员变量static成员函数

在介绍这两个语法特性外,我们先谈一谈它们的共同的特性:

  1. 二者都会受到访问限定符的限制。因为它们都是属于对应类的一部分属性或者方法。
  2. 静态成员都是所有该类对象共享的。它们不属于任何一个特定的对象。它们都被存放在静态区

然后我们再来细谈每个成员

2.1 static成员变量

语法如下:

class A
{
private:
 static int _a; //注意这里是声明
}
int A::_a = 1; //在类外定义

外部访问静态成员变量案例,例2.1.1:

#include<iostream>
using namespace std;
class A
{
public: //公有情况下
	static int _a;
};

int A::_a = 1;

int main()
{
	cout << A::_a << endl; //域访问符

	A a;
	A::_a += 1; //类对象访问
	cout << a._a << endl;
}

需要注意:

  1. 类的静态成员变量需要在类域外初始化。在定义的时候不加关键字static
  2. 类静态成员被外部访问的时候,访问方式:类名::成员或者对象.成员
  3. 需要注意的是:静态成员变量在程序启动的时候就已经被开辟空间被创建并初始化了。当整个程序结束的时候,才会销毁。(模板类的静态成员变量创建时机不同,因为模板的实例化的特殊性)

2.2 static成员函数

语法如下,现有一个A类:

class A
{
public:
	static void function()
	{
		//业务处理
	}
//……
};

这是在类域中定义,当然也可以在类域外定义

class A
{
public:
	static void function();
};

void A::function() //类域外定义
{
	//……
}

注意以下特点:

  • 由于静态成员函数是该类公有的,所以静态成员函数是无法访问任何一个具体类对象的数据的,也就是意味着静态成员函数没有隐藏的this指针
  • 所以我们可以尝试用一个公有的静态函数的接口,得到私有的静态成员变量,又或者是创建一些设计模式(后续我们会提到一些特殊类的设计

2.3 运用举例

例2.3.1,执行到当前的语句一共创建了多少对象?

很显而易见,我们可以利用静态成员变量不属于某个独立的对象的特点,代码演示:

#include<iostream>
using namespace std;

class test
{
public:
	test()
	{
		++_count;
	}
	~test()
	{
		--_count;
	}
	static int getcount() 
		//私有数据为了封装性尽量不要返回handle(引用或者指针)
	{
		return _count;
	}
private:
	static int _count;
};

int test::_count = 0;

void func()
{
	test t3;
	cout << test::getcount() << endl;
}

int main()
{
	test t1;
	cout << test::getcount() << endl;
	test t2;
	cout << test::getcount() << endl;
	func();
	//……
	return 0;
}

例2.3.1,在不运用递归、循环、公式方式的情况下,如何做到1+2+3+……+n的结果呢?

当你想到了利用创建对象的静态数据,那么恭喜你非常的厉害!演示:

#include<iostream>
using namespace std;
class Solution
{
public:
	Solution()
	{
		//每创建一个对象的时候,++_i, _sum += _i
		++_i;
		_sum += _i;
	}
	static int ret()
	{
		return _sum;
	}
private:
	static int _i; //统计当前对象的个数;
	static int _sum; //结果
};
int Solution::_i = 0;
int Solution::_sum = 0;

int main()
{
	//如何创建n个对象呢?数组!或者new
	//利用了类对象在被创建的时候需要调用构造函数!
	Solution arr[15];
	cout << Solution::ret() << endl;
	return 0;
}

关于static成员的用法还有诸多用处,在许多设计模式上都有所运用,比如说单例模式。我们后面的文章会介绍一些特殊类的设计。其中会用到static成员。

下面就是想const成员函数一样的问题了:

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

问题的答案请聚焦在,静态成员函数的特性(有无this指针)

3. 友元

友元简单来理解就是:“朋友”。“我”现在是一个类,“我”不会让“陌生人”了解“我”的“小秘密”,但是“我”的“朋友”可以。换句话说:

  • 不管友元类还是友元函数,都有资格访问该类的私有数据
  • 友元是一种声明不会受到访问限定符的限制

3.1 友元类

语法形式如下:

class A
{
	friend class B; //那么在A的类域中即可访问B中的成员
//……
};

class B
{
//……
};

例3.1.1:

#include<iostream>
using namespace std;
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)
	{
		// 直接访问【Time类】私有的成员变量 -- 如果没有声明,这里就无法访问到_hour……的信息
		_t._hour = hour;
		_t._minute = minute;
		_t._second = second;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t; //定义一个Time的成员
};

需要注意的是:

  • 友元关系是单向的,不具有交互性。
  • 友元关系不能传递。A是B的友元,B是C的友元,但是A不是C的友元。意味着:A类中不能访问C类的私有成员。
  • 友元关系不能继承。(在继承中我们会谈到)

3.2 友元函数

语法如下:

class A
{
	friend void function();
}

void function()
{
	//……
}

那么在function这个函数的函数体中就可以访问一个A类对象的私有数据了。这里需要注意:最好把函数定义在类定义的后面小心地使用类的前置声明

细节:关于类的前置声明

对于这个错误,小编也是很有心得,在经历一次一次验证下,终于得到了一个理解。

先来看这样一个例子,例3.2.1:

#include<iostream>
using namespace std;
class A; //想要前置声明这个类

/*
class B
{
	//…… 如果是一个类呢?
};
*/
void func()
{
	A val; // 创建是失败的 -- 语句1
	cout << val._a << endl;
}

class A
{
	friend void func();
public:
	A()
	{}
private:
	int _a = 1;
};

int main()
{
	A a; //创建是成功的 -- 语句2
	func();
	return 0;
}

作为初学者,不知道大家有没有遇到这个问题?语句一为什么会出错?这是一个编译期就会出错的代码,问题就在于func之前的一个前置声明class A;在编译器看来,如果想要创建一个对象,就如同创建一个int一样,需要知道这个对象的大小,而编译器在编译阶段,去该函数之前去查找这个A类,只有一个声明语句,导致编译器无法知道所分配的空间大小,所以直接在编译期挂掉了。显而易见地我们需要把它后置定义。

注意:关于这个A类的前置声明,函数和B类是有差异的(VS2022下已得验证)。

  1. 对于一个类的前置声明,后面未见其定义式的函数不可创建该类的对象、不可使用该类的指针或者引用。
  2. 对于一个类的前置声明,后面未见其定义式的不可使用该类的对象,可以使用该类的指针或者引用。

大家可以自行验证,如果有问题欢迎交流!

友元函数虽然破坏了封装性,但是也可以让大家更好地使用,例如,考虑为一个date类写一个operator <<。如果我们将operator <<作为一个成员函数,如:ostream& operator<<(ostream& out)。这个操作符的第一个参数就是date类(隐藏的this指针),而不是我们想要的ostream类(cout的类型),所以声明为:ostream& operator<<(ostream& out, const date& d) 似乎更合理,这样符合习惯。但是不作为成员函数就无法访问date的私有数据,这里就有两种解决办法:

  1. 提供公有接口 。
  2. 友元。

例3.2.2:

#include<iostream>
using namespace std;
class date
{
	// 友元函数声明
	friend ostream& operator<<(ostream& out, const date& d);
	friend istream& operator>>(istream& in, date& d);
	int GetMonthDays(int year, int month) const
	{
		static int daysarr[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 daysarr[month];
	}
private:
	int _year;
	int _month;
	int _day;
};

ostream& operator<<(ostream& out, const date& d)
{
	out << d._year << "年" << d._month << "月" << d._day << "日" << endl;

	return out;
}

istream& operator>>(istream& in, date& d)
{
	int year, month, day;
	in >> year >> month >> day; 
	if (month <= 0 || month >= 13 || day <= 0 || day >= d.GetMonthDays(year, month))
	{
		cout << "非法日期" << endl;
		return in;
	}
	d._year = year;
	d._month = month;
	d._day = day;

	return in;
}

4. 内部类

4.1 概念及语法

概念:如果一个类的定义另外一个类的内部,那么这个类就叫做内部类

例如:

class A
{
	class B //定义在A类的内部
	{
	
	};	
};

4.2 特性探究

注意:

  1. 内部类是一个独立的类。它并不属于外部类,更不能通过外部类的对象去访问内部类的成员。即:外部类对内部类没有任何优越的访问权限
  2. 内部类是外部类的友元
  3. 内部类对象创建受访问限定符限制。内部类是可以在privateprotectpublic都可以的。但是对于一些外部来说,就无法通过指定类域来得到内部类的类型。但是外部类可以使用内部类的类型。
class A
{
public:
	class B
	{};
protected:
	class C
	{};
private:
	class D
	{};
};
int main()
{
	A::B b;
	//A::C c;
	//A::D d;
	return 0;
}
  1. 内部类可以直接访问外部类的静态成员

5. 匿名对象

5.1 概念及语法

概念:没有名字的类对象。换一个角度:没有变量名的“变量”

语法

class A
{};

int main()
{
	A(); //这就是一个匿名对象, 这里是无参的构造,有参构造依次加入括号即可。
	return 0;
}

5.2 特性探究

  1. 特性一:即用即销毁

我们以下面的程序class A类:

#include<iostream>
using namespace std;
class A
{
public:
	A(int val = 1)
		:_a(val)
	{
		cout << "A(int val)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
int main()
{
	A a1;
	cout << "---------------" << endl;
	A(); //创建一个匿名对象
	cout << "---------------" << endl;
	return 0;
}

来看这样的运行结果:
在这里插入图片描述
很显然,我们可以看出来,这个匿名对象的生命周期就在当前行

  1. 具有常性

还是使用刚刚的A类:

int main()
{
	A();
	cout << "---------------" << endl;
	//A& ref = A();  //不能通过编译
	const A& ref = A(); //可以通过编译
	cout << "---------------" << endl;
	return 0;
}

在这里插入图片描述
我们可以得到两点信息:

  • 匿名对象是具有常性的
  • 匿名对象被引用过后生命周期变长

5.3 用途

匿名对象的用处是非常广泛:例如,在STL的传参中、在一些只需要定义一次使用的成员函数中,都是非常不错的选择!

#include<iostream>
#include<vector>
using namespace std;

int main()
{
	int m = 5, n = 5;
	vector<vector<int>> v(m, vector<int>(n, 0)); //初始化一个m * n的矩阵,元素全部为0;
	return 0;
}

6. explicit关键字

关于这个关键字,我会在C++类对象的隐式类型转换和编译器返回值优化中详细介绍

  • 希望这篇文章能够帮助到你!

网站公告

今日签到

点亮在社区的每一天
去签到