再探类&对象——C++入门进阶

目录

一、引言

二、类&对象进阶

1、初始化列表

2、static成员

3、友元

4、内部类

5、匿名对象

三、结语


一、引言

类和对象作为C++的入门基础,同时也是C++的重点内容,类和对象该模块概念多,语法较难,学习门槛高,不易掌握,类和对象模块的学习可划分为三个阶段:基础,入门,进阶,基础与入门内容包括类和对象的概念、成员函数的实现、类的默认成员函数、运算符重载等内容,本文将在类和对象入门的基础上,再探类和对象,进一步学习类和对象的进阶内容,类和对象模块的学习周期长,难度较大,理解、掌握类和对象,能为后面学习面向对象编程、STL等相关内容打下坚实基础。

二、类&对象进阶

1、初始化列表

一开始实现构造函数,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表。

cpp 复制代码
#include<iostream>
using namespace std;
class Date
{
public:
	Date(int year=1, int month=1, int day=1)
		:_year(year)
		, _month(month)
		, _day(day)
	{ 
	}
private:
	int _year;
	int _month;
	int _day;
};

初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式,每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。

这里需要注意的是,初始化列表是每个成员变量定义初始化的地方,有一些特殊类型的变量,引用类型变量、const修饰的变量,这类变量在定义时必须初始化,那么当引用、const作为成员变量时,则必须放在初始化列表位置进行初始化,否则编译会报错。此外,若类中有自定义类型成员变量,但没有显示在初始化列表初始化,那么编译器会调用这个成员类型的默认构造函数。

cpp 复制代码
#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour = 0)
		:_hour(hour)
	{
		cout << "time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int year=1, int month=1, int day=1)
		:_year(year)
		, _month(month)
		, _day(day)
	{ 
	}
	void print()const
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d1(2025, 10, 3);
	d1.print();
	return 0;
}

Date类中有一个自定义类型的类成员变量_t,_t并没有在初始化列表里初始化,那么编译器会调用_t的默认构造函数Time(int hour=0)进行初始化,可以F11逐语句调试并监视成员变量_t的变化情况。

(1)

如(1)所示,F11进行逐语句调试,构造并初始化对象d1,会调用d1的构造函数,监视1用来观察类成员对象_t的变化情况。

(2)

如(2)所示,编译器调用d1的构造函数,并在初始化列表对成员变量进行初始化,类成员变量_t没有在初始化列表中,这时编译器会调用_t的默认构造函数,来完成对_t的初始化。

(3)

如(3),可以看到编译器已经来到_t的默认构造函数Time,并在初始化列表完成对_t的初始化,cout<<"time()"<<endl,可以用于判断编译器是否调用了_t的默认构造函数来对_t进行初始化。

(4)

由(4)的监视窗口可以看出,_t已经完成了初始化,_t的成员变量_hour初始化为0,与_t的默认构造函数初始化的结果一致。

(5)

显示窗口上显示 time(),说明了初始化类成员_t时调用了_t的默认构造函数,来完成对_t的初始化,因此对于没有显示在初始化列表初始化的自定义类型成员,编译器会调用这个成员类型的默认构造函数,如果没有默认构造函数,那么就会编译报错,所以对于如果没有默认构造的类类型成员变量,也必须放在初始化列表位置进行初始化。

总结一下:引用成员变量,const修饰的成员变量,没有默认构造的类类型成员变量,都必须放在初始化列表进行初始化,否则会编译报错。

另外,C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。

(6)

如(6)所示,成员变量_day没有在初始化列表进行初始化,这时_day的值为_day声明位置的缺省值。

(7)

_day成员变量没有在初始化列表进行初始化,这时_day的值为_day声明位置的缺省值,如(7)所示,_day声明位置的缺省值为1。

(8)

如(8)所示,通过监视1窗口可以看出_day的值为缺省值1,说明对于没有显示在初始化列表初始化的成员变量,该成员变量的值为声明处的缺省值。

(9)

如(9)所示,构造并初始化对象d1,即Date d1(2025,10,3),则_year、_month初始化为2025、10,_day没有在初始化列表中初始化,故_day的值为_day声明位置的缺省值1,则d1的结果为2025/10/1,。

每个构造函数都有初始化列表,有了初始化列表,尽量使用初始化列表初始化,因为不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会用这个缺省值初始化。如果该成员没有缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定,对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。

初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关。

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a=0)
		:_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、_a1,故初始化列表的初始化顺序也为_a2、_a1,具体过程可见下图调试过程,通过监视观察_a2、_a1的变化。

(10)

如(10)所示,构造并初始化对象aa,aa成员变量的初始化顺序为与成员变量的声明顺序一致,即先_a2、再_a1。

(11)

如(11)所示,初始化列表先初始化成员变量_a2,由于_a1还未初始化,故_a1为随机值,将_a1的值传给_a2,故_a1、_a2都为随机值。

(12)

接着初始化成员_a1,将a的值传给_a1。

(13)

如(13)所示,_a1的值变为1。

(14)

如(14)所示,故_a1,、_a2的值分别为1、随机值。建议声明顺序和初始化列表顺序保持一致。

2、static成员

用static修饰的成员变量,称之为静态成员变量,静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区,静态成员变量要在类外进行初始化。

cpp 复制代码
#include<iostream>
using namespace std;
class K
{
public:
    K()
	{
		_scount++;
	}
private:
	static int _scount;
	int _a = 1;
};
int K::_scount = 0;

如上面代码所示,_scount为静态成员变量,需要在类外进行初始化,初始化需指明所在类域,即int K::_scount=0,。

类外若想访问静态成员变量,不能通过类来直接访问,因为静态成员变量不属于类,静态成员变量存储在静态区,因此只能通过在类中创建一个成员函数来返回该变量的值,来访问该静态成员变量。

cpp 复制代码
​
#include<iostream>
using namespace std;
class K
{
public:
{
	static int Getcount()
	{
		return _scount;
	}
private:
	static int _scount;
	int _a = 1;
};
int K::_scount = 0;
int main()
{
	cout << K::Getcount() << endl;
}

​

(15)

如(15)所示,在类中定义一个函数Getcount来返回静态成员变量_scount的值,返回类型为static int,这样类外若想访问该静态成员变量,就可以通过该成员函数Getcount来访问,即K::Getcount,由于该静态成员变量在类外初始化为0,故结果为0。

cpp 复制代码
#include<iostream>
using namespace std;
class K
{
public:
	static int Getcount()
	{
		return _scount;
	}
private:
	static int _scount;
	int _a = 1;
};
int K::_scount = 0;
int main()
{
	cout << K::Getcount() << endl;
    cout << sizeof(K) << endl;
}

这里还需注意的是由于静态成员变量为所有类对象所共享,不属于某个具体的对象,因此静态成员变量不存放在类中,存放在静态区,故sizeof计算类的空间大小时不计算静态成员变量的空间。

(16)

如(16)所示,sizeof求类的空间大小,静态成员变量_scount的大小不纳入类中,成员变量_a的大小即为类的大小,故sizeof(K)的值为4。

用static修饰的成员函数,称为静态成员函数,静态成员函数没有this指针,静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。

下面这道题很好地应用了上面static静态成员变量的相关内容

(17)

题目要求实现1+2+3+...+n的运算,但不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句,可以考虑利用类和静态成员变量来实现。

cpp 复制代码
#include<iostream>
using namespace std;
class Sum
{
public:
	Sum()
	{
		_ret += _i;
		_i++;
	}
	static int Getret()
	{
		return _ret;
	}
private:
	static int _i;
	static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
class Solution
{
public:
	int Sumsolution(int n)
	{
		Sum* p = new Sum[n];
		return Sum::Getret();
	}
};

首先定义类Sum,静态成员变量_i、_ret用于实现求和,_i初始化为1,_ret初始化为0,在构造函数Sum函数体中,_ret+=_i,_i++,可以通过多次构造Sum即可实现累加求和,成员函数Getret用于返回_ret,_ret即为1+2+...+n的结果,类Solution的成员函数Sumsolution用于求和,函数参数为整形n,通过Sum的数组,构造n个Sum,就调用了n次构造函数,从而实现n次累加求和,最后通过Sum::Getret()函数返回_ret,即为所求的结果。

3、友元

友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并把友元声明放到一个类的里面。

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	friend void func(const A& aa);
private:
	int _a1 = 1;
	int _a2 = 2;
};
void func(const A& aa)
{
	cout << aa._a1 << endl;
}
int main()
{
	A aa;
	func(aa);
	return 0;
}

外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,并不是类的成员函数。

如上代码所示,func是A的友元函数,则func可以访问A的私有成员变量_a1、_a2,。

友元函数可以在类定义的任何地方声明,不受类访问限定符限制,一个函数也可以是多个类的友元函数。

cpp 复制代码
#include<iostream>
using namespace std;
//前置声明
class B;
class A
{
public:
	friend void func(const A& aa, const B& bb);
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
public:
	friend void func(const A& aa, const B& bb);
private:
	int _b1 = 3;
	int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}
int main()
{
	A aa;
	B bb;
	func(aa, bb);
	return 0;
}

如上所示,func是A的友元函数,同时也是B的友元函数,则func都能访问A和B的私有成员变量。

友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	friend class B;
private:
	int _a1=1;
	int _a2=2;
};
class B
{
public:
	void func1(const A& a)
	{
		cout << a._a1 << endl;
		cout << _b1 << endl;
	}
	void func2(const A& a)
	{
		cout << a._a2 << endl;
		cout << _b2 << endl;
	}
private:
	int _b1=3;
	int _b2=4;
};

B是A的友元类,则B的成员函数都为A的友元函数,都可以访问A中的私有成员变量。

友元类的关系是单向的,不具有交换性,例如B类是A类的友元,但A类不是B类的友元,友元类关系也不具有传递性,即A是B的友元,B是C的友元,但A不是C的友元,友元有时提供了便利,但是友元会增加耦合度,破坏了封装,所以友元不宜多用。

4、内部类

如果一个类定义在另一个类的内部,这个类就叫作内部类,内部类是一个独立的类,跟定义在全局相比,只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类,sizeof计算外部类的大小时不计算内部类的大小。

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
private:
	static int _a;
	int _b=1;
public:
	class B
	{
	public:
		void func(const A& a)
		{
			cout << _a << endl;
			cout << a._b << endl;
		}
	private:
		int _c = 2;
	};
};
int A::_a = 0;
int main()
{
	cout << sizeof(A) << endl;
	A::B b;
	A a;
	b.func(a);
	return 0;
}

内部类默认是外部类的友元类,如上代码所示,B是A的内部类,则B也是A的友元类,B的成员函数都可以访问A的成员变量。

(18)

如(18)所示,sizeof计算A的大小时,静态成员变量_a和内部类B不纳入计算,故成员变量_b大小即为A的大小,故A的大小为4,B作为A的内部类,可以访问A的成员变量_a、_b,_a、_b的值分别为0、1。

再回头看那道1+2+...+n的OJ题,由于两个类是紧密联系的,因此可以考虑用内部类的方法来解决。

cpp 复制代码
#include<iostream>
using namespace std;
class Solution
{
private:
	static int _ret;
	static int _i;
	class Sum
	{
	public:
		Sum()
		{
			_ret += _i;
			_i++;
		}
	};
public:
	static int Sumsolution(int n)
	{
		//Sum a[n];
		Sum* p = new Sum[n];
		delete[] p;
		return _ret;
	}
};
int Solution::_i = 1;
int Solution::_ret = 0;
int main()
{
	cout << Solution::Sumsolution(100) << endl;
}

由于类Sum和Solution是紧密联系的,Sum用于实现循环每次的相加,故可将类Sum作为Solution的内部类,这样Sum就可以直接访问Solution的成员变量_i、_ret,Solution内部再定义一个函数Sumsolution,通过Sum数组调用n次Sum构造函数,构造函数内部实现逐次累加,即可实现循环累加。

(19)

如(19)所示,Solution::Sumsolution(100)即可用于求1+2+...+100的值,值为5050,结果正确,测试通过。

内部类本质也是一种封装,当A类跟B类紧密关联,A类实现出来就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。

5、匿名对象

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 0)
		:_a1(a)
	{
		cout << "A(int a=0)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1;
};
class Solution
{
public:
	int _Solution(int n)
	{
		//...
		return n;
	}
};
int main()
{
	A aa;
	A b(1);
	A();//匿名对象
	Solution s;
	Solution();//匿名对象
	cout << Solution()._Solution(10) << endl;
	cout << s._Solution(11) << endl;
	return 0;
}

在定义对象时,我们可以定义匿名对象,匿名对象的特点是不用取名字,如上面的A(),Solution(),都是匿名对象,匿名对象的生命周期只有它定义的这一行。下面所示是F11调试观察匿名对象A()的变化情况。

(20)

F11调试可以看出,匿名对象的生命周期只有它定义所在的一行,可以看到下一行它就会自动调用析构函数。

匿名对象在某些场景下很好用,若我们只想调用类的某个成员函数,那么就可以不用通过实例化对象来调用了,就可以使用匿名对象。

cpp 复制代码
#include<iostream>
using namespace std;
class Solution
{
public:
	int _Solution(int n)
	{
		return n;
	}
};
int main()
{
	
	//Solution s;
	//cout << s._Solution(11) << endl;
	cout << Solution()._Solution(11) << endl;
	return 0;
}

如上图代码所示,我们只想调用_Solution成员函数,就可以使用匿名对象Solution(),即Solution()._Solution(11),这种情况用匿名对象就很合适了,不用再去实例化对象。

三、结语

至此,类和对象进阶到这就到尾声了,类和对象进阶我们主要围绕初始化列表、静态成员变量/函数、友元、内部类、匿名对象展开介绍,对于初始化列表,我们需要注意的是,每个构造函数都有初始化列表,每个成员变量都要走初始化列表,同时也要注意的是引用成员变量、const修饰的成员变量、没有默认构造函数的类成员变量,都必须要在初始化列表初始化,静态成员不属于类,为所有类共享,友元关系能打破类域限制,实现类间协作,内部类也是一种友元关系,内部类的成员函数也是外部类的友元函数,可以访问外部类的成员变量,匿名对象在一些情况很好用,当我们只想调用类的某个成员函数,就可以不用实例化对象,使用匿名对象即可。面向对象编程是一种思维方式,不在于记住了多少语法,而在于懂得在合适的场景运用合适的设计,这个过程需要我们慢慢成长,道阻且长,行则将至!

相关推荐
007php0073 小时前
某大厂跳动面试:计算机网络相关问题解析与总结
java·开发语言·学习·计算机网络·mysql·面试·职场和发展
lsx2024064 小时前
HTML 字符集
开发语言
很㗊4 小时前
C与C++---类型转换
c语言·开发语言
say_fall4 小时前
精通C语言(3. 自定义类型:联合体和枚举)
c语言·开发语言
北京不会遇到西雅图4 小时前
【SLAM】【后端优化】不同优化方法对比
c++·机器人
郝学胜-神的一滴4 小时前
Effective Python 第43条:自定义容器类型为什么应该从 `collections.abc` 继承?
开发语言·python
jndingxin4 小时前
c++多线程(6)------ 条件变量
开发语言·c++
共享家95275 小时前
QT-常用控件(二)
开发语言·qt
程序员莫小特5 小时前
老题新解|大整数加法
数据结构·c++·算法