C++入门篇——类和对象(下)

目录

1.类的初始化

1.1初始化列表

1.1.1概念及定义

1.1.2特点

1.1.3初始化列表的顺序

1.2类的初始化

2.类型转换

3.static成员

4.友元

4.1友元函数

4.2友元类

5.内部类

6.匿名对象


1.类的初始化

在之前类和对象的内容中讲到了关于类初始化的构造函数以及拷贝构造,下面再来了解下类另一个初始化方式------初始化列表。

1.1初始化列表

1.1.1概念及定义

之前我们实现构造函数时,初始化成员变量主要使⽤函数体内赋值,构造函数初始化还有⼀种⽅式,就是初始化列表,初始化列表的使⽤⽅式是以⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后⾯跟⼀个放在括号中的初始值或表达式。以Date类为例:

cpp 复制代码
#include<iostream>

using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1)
		:_year(1)
		, _month(month)
		, _day(1)
	{

	}

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

int main()
{
	Date d1;
	
	return 0;
}

通过调试结果:

可以看出通过初始化列表初始化的对象在实例过程中也能直接初始化。

1.1.2特点

(1)每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地⽅。

(2)引⽤成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进⾏初始化,否则会编译报错。

下面继续以Date类为例:

cpp 复制代码
#include<iostream>

using namespace std;

class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1)
		:_year(1)
		, _month(month)
		, _day(1)
	{
		int& ret = year;
		n = 1;
		z = 0;
	}

private:
	int _year;
	int _month;
	int _day;

	int& ret;
	const int n;
	const int& z;
};

int main()
{
	Date d1;
	
	return 0;
}

运行结果如下:

可以看到这边在构造函数内初始化的ret、n、z都初始化失败了,这边原因是在构造函数体内的赋值操作本质是对其成员变量进行类似于"重新赋值"的操作,但引用成员变量以及const成员变量都需要直接进行初始化,因此需要进行初始化列表进行初始化。

其次需要注意的是在对引用进行初始化时,最后用全局变量或者在构造函数中传一个引用参数。

cpp 复制代码
#include<iostream>

using namespace std;

int x = 1;

class Date
{
public:
	//构造函数
	Date(int& s = x, int year = 1, int month = 1, int day = 1)
		:_year(1)
		, _month(month)
		, _day(1)
		, ret(x)
		, n(1)
		, z(2)
	{

	}

private:
	int _year;
	int _month;
	int _day;

	int& ret;
	const int n;
	const int& z;
};

由此便可以将初始化列表理解为在类定义时就将其进行初始化,实现真正意义上的"初始化"。

(3)C++11⽀持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显⽰在初始化列表初始化的成员使⽤的。

以上面Date类为例:

cpp 复制代码
#include<iostream>

using namespace std;

int x = 1;

class Date
{
public:
	//构造函数
	Date(int& s = x, int year = 1, int month = 1, int day = 1)
		:_year(1)
		, _day(1)
		, ret(x)
		, n(1)
		, z(2)
	{

	}

private:
	int _year = 0;
	int _month = 2;
	int _day = 0;

	int& ret = x;
	const int n = 1;
	const int& z = 1;
};

int main()
{
	Date d1;

	return 0;
}

赋值结果如下:

可以看出当类中缺省值初始化与初始化列表同时存在时还是初始化列表优先,在初始化列表中没有的成员变量才会去调用缺省值初始化。

(4)尽量使⽤初始化列表初始化,因为那些你不在初始化列表初始化的成员也会⾛初始化列表,如果这个成员在声明位置给了缺省值,初始化列表会⽤这个缺省值初始化。如果你没有给缺省值,对于没有显⽰在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显⽰在初始化列表初始化的⾃定义类型成员会调⽤这个成员类型的默认构造函数,如果没有默认构造会编译错误。

下面在Date类中添加一个time元素为例:

cpp 复制代码
#include<iostream>

using namespace std;

class Time
{
public:
	Time(int hour = 0, int minute = 0, int seconds = 0)
		:_hour(hour)
		,_minute(minute)
		,_seconds(seconds)
	{

	}
private:
	int _hour;
	int _minute;
	int _seconds;
};

class Date
{
public:
	//构造函数
	Date(int year = 1, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(year)
	{

	}

private:
	int _year;
	int _month;
	int _day;
	Time _time;
};

int main()
{
	Date d1;
	
	return 0;
}

初始化结果如下:

可以看到这边自定义类型_time既没有在初始化列表初始化,也没有给缺省值,而是直接利用其默认构造函数(无需传实参的构造函数)初始化,那如果没有默认构造函数呢?

可以看到这边我们将Time的默认构造改为构造后运行,会提示没有可用默认构造。 这时我们再通过初始化列表或者缺省值初始化:

但这边缺省值给3个实参怎么给呢?可类比数组的初始化:

这样便可实现缺省值初始化。

那有了初始化列表初始化,构造函数是不是就没用了呢?当然不是。初始化列表只适合给特定的值,不能有条件的判断;而类缺省值则更为局限,只能给固定值,无法调用参数初始化。以Stack类数组初始化为例:

cpp 复制代码
#include<iostream>

using namespace std;

typedef int STDataType;
class Stack
{
public:
	Stack(int n = 4)
		:_arr((STDataType*)malloc(n*sizeof(STDataType)))
		,_top(0)
		,_capacity(n)
	{

	}
private:
	STDataType* _arr;
	int _top;
	int _capacity;
};

int main()
{
	Stack s1;

	return 0;
}

这样给数组初始化的话空间开辟一定能成功吗?而初始化列表又不能添加限制条件,因此这里就必须在构造函数内进行条件限定:

cpp 复制代码
	Stack(int n = 4)
		:_arr((STDataType*)malloc(n*sizeof(STDataType)))
		,_top(0)
		,_capacity(n)
	{
		if (nullptr == _arr)
		{
			perror("malloc开辟失败");
			return;
		}
	}

1.1.3初始化列表的顺序

初始化列表中按照成员变量在类中声明顺序进⾏初始化,跟成员在初始化列表出现的的先后顺序⽆关。这里在初始化列表中的顺序当用值赋值时并无影响,但当有成员变量给另一成员变量进行初始化时,初始化顺序就得看在类定义中出现的顺序有关。因此建议初始化列表顺序与成员变量定义顺序一致。以下面类为例:

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a)
		:_a1(a)
		, _a2(_a1) {
	}
	void Print() {
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};
int main()
{
	A aa(1);
	aa.Print();
}

可以先思考下运行结果:

可以看到这边a2是随机值。这里的a2在初始化列表中初始化,因此无需使用缺省值,而a2初始化的值源于a1,a1的定义在a2后,因此在初始化列表中的a2初始化之前a1还未初始化,被随机初始化。

1.2类的初始化

通过目前类和对象的学习,可以基本了解类初始化的方法以及各方法的优先级:

2.类型转换

在C语言中了解到浮点型能转化为整型,字符型与整型间能相互转换。在C++中支持内置类型隐式转换为自定义类型,这点其实在之前自定义类型缺省值初始化时有类似体现:

cpp 复制代码
Time _time = {0, 0, 0};

只不过有3个参数看着不太明显,这边以Time类进一步感受下

cpp 复制代码
#include<iostream>

using namespace std;

class Time
{
public:
	Time(int hour = 1)
		:_hour(hour)
	{

	}

	void Print()
	{
		cout << _hour << endl;
	}
private:
	int _hour;
};

int main()
{
	Time time1;
	time1.Print();

	time1 = 24;
	time1.Print();

	return 0;
}

运行结果如下:

可以看出这边整型类型的24被转换为了自定义类型time1

但内置类型转换为自定义类型必须的两中类型在构造函数中有联系,比如在Time类中int被当作形参传给了成员变量_hour,由此这里的24实际隐式传给了_hour,但如果没有联系是不能进行类型转换的:

可以看到当关系断开后再进行类型转换时报错了。这里类型转换的实际逻辑是利用该内置类型构造一个该自定义类型的临时对象,再用该临时对象拷贝给实际对象,但编译器这里会直接优化为直接构造。

若不想类中支持类型转换,可以在构造函数前加explicit:

类类型的对象之间也可以隐式转换,需要相应的构造函数⽀持。

3.static成员

⽤static修饰的成员变量,称之为静态成员变量,静态成员变量⼀定要在类外进⾏初始化。

静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。

⽤static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。

静态成员函数中可以访问其他的静态成员,但是不能访问⾮静态的,因为没有this指针。

⾮静态的成员函数,可以访问任意的静态成员变量和静态成员函数。

突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。

静态成员也是类的成员,受public、protected、private 访问限定符的限制。

静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员
变量不属于某个对象,不⾛构造函数初始化列表。

static成员变量每次实例化时都不会被重置,利用该特性便能统计一个类创建对象的个数:

cpp 复制代码
//统计AA类创建对象的个数:

#include<iostream>

using namespace std;

class AA
{
public:
	//初始化
	AA()
	{
		//调用构造时自动++
		++_scount;
	}

	//获取私有_scount
	static int GetScount()
	{
		return _scount;
	}

private:
	//类中声明
	static int _scount;
};

//类外初始化
int AA::_scount = 0;

int main()
{
	cout << AA::GetScount() << endl;

	AA a1;
	AA a2;
	AA a3;

	cout << AA::GetScount() << endl;

	return 0;
}

运行结果如下:

但要注意的是如果这里GetScount函数不是静态的话,调用时就需要利用实例化对象调用。

4.友元

类中的私有成员是不允许在类外部访问的,但有时候一些功能的实现必须在外部访问类成员(例如<<运算符重载),这时便可以利用C++中的友元功能。

友元顾名思义就是使用友元修饰后的函数或类就能访问指定类中的成员,像交了朋友一样。

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

4.1友元函数

以下面代码为例:

cpp 复制代码
#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
using namespace std;
// 前置声明,都则A的友元函数声明编译器不认识B
class B;
class A
{
	// 友元声明
	friend void func(const A& aa, const B& bb);

private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
	// 友元声明
	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;
}#define _CRT_SECURE_NO_WARNINGS 1

#include<iostream>
using namespace std;
// 前置声明,都则A的友元函数声明编译器不认识B
class B;
class A
{
	// 友元声明
	friend void func(const A& aa, const B& bb);

private:
	int _a1 = 1;
	int _a2 = 2;
};

class B
{
	// 友元声明
	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;
}

运行结果如下:

4.2友元类

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

友元类的关系是单向的,不具有交换性,⽐如A类是B类的友元,但是B类不是A类的友元。

友元类关系不能传递,如果A是B的友元, B是C的友元,但是A不是C的友元。

5.内部类

如果⼀个类定义在另⼀个类的内部,这个内部类就叫做内部类。内部类是⼀个独⽴的类,跟定义在

全局相⽐,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。

内部类默认是外部类的友元类。

内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使⽤,那么可以考

虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其

他地⽅都⽤不了。

下面以牛客网的1 + 2 + 3+ ··· + n为例:

这里题目中求和不允许使用那些常见的方法,因此便可以利用static成员特性用创建对象来实现:

cpp 复制代码
#include <cmath>
class Solution 
{
public:
    class Fsum
    {
    public:
        Fsum()
        {
            _sum += _i;
            ++_i;
        }
    };

    static int _sum;
    static int _i;

    int Sum_Solution(int n) 
    {
        Fsum s[n];
        return _sum;
    }
};

int Solution::_sum = 0;
int Solution::_i = 1;

这里我们将求和的类定义为Solution的内部类,主要是要调用Sum类中的方法。

6.匿名对象

在之前类的实例化过程中,我们一般会给对象一个名称:

cpp 复制代码
Date d1;

这样的对象我们叫做有名对象,而匿名对象则是实例化时不给对象名:

cpp 复制代码
Date();

匿名对象⽣命周期只在当前⼀⾏,⼀般临时定义⼀个对象当前⽤⼀下即可,就可以定义匿名对象。

相关推荐
焜昱错眩..几秒前
代码随想录训练营第二十一天 |589.N叉数的前序遍历 590.N叉树的后序遍历
数据结构·算法
菲兹园长21 分钟前
MyBatis-Plus
java·开发语言·mybatis
Tisfy23 分钟前
LeetCode 1550.存在连续三个奇数的数组:遍历
算法·leetcode·题解·数组·遍历
wang__1230024 分钟前
力扣70题解
算法·leetcode·职场和发展
菜鸟破茧计划29 分钟前
滑动窗口:穿越数据的时光机
java·数据结构·算法
修修修也1 小时前
【C++】特殊类设计
开发语言·c++·特殊类·类与对象
Cloud Traveler1 小时前
Java并发编程常见问题与陷阱解析
java·开发语言·python
虾球xz1 小时前
游戏引擎学习第274天:基于弹簧的动态动画
c++·学习·游戏引擎
_Itachi__1 小时前
LeetCode 热题 100 101. 对称二叉树
算法·leetcode·职场和发展
byte轻骑兵2 小时前
【C++重载操作符与转换】转换与继承
开发语言·c++