【C++深入浅出】类和对象下篇


一. 前言

老样子,先来回顾一下上期的内容:上期我们着重学了C++类中的六大默认成员函数,并自己动手实现了一个日期类,相信各位对C++中的类已经有了一定程度的了解。本期就是类和对象的最后一篇啦,终于要结束咯,吧唧吧唧

话不多说,开吃咯!!!

二. 初始化列表

2.1 引入

我们先来看看下面的代码:

cpp 复制代码
class Date
{
public:
	Date(int year = 2023, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	const int _year;
	const int _month;
	const int _day;
};
int main()
{
	Date d;
	return 0;
}

当我们编译代码时,发现编译器报了一大堆错误。报错的主要原因主要有两个

**1、**const变量定义时需要进行初始化

**2、**const变量不能作为左值

欸,可能有些小伙伴就纳闷了:我们不是在构造函数中对const成员变量进行初始化了吗? 实际上,在构造函数函数体内进行的并不是初始化,而是赋值操作。因为初始化只能初始化一次,而构造函数体内可以进行多次赋值。

出于这个原因,于是编译器就会报出以上两种错误。那怎么办呢?众所周知,初始化是在定义变量时进行的,那变量又是在哪定义的呢?答案是:初始化列表

2.2 概念

在C++中,初始化列表可以认为是成员变量定义的地方。

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

cpp 复制代码
class Date
{
public:
	Date(int year = 2023, int month = 1, int day = 1)
		:_year(year) //初始化列表,是每个成员变量定义的地方,可以进行初始化
		,_month(month) //用month的值初始化成员变量_month
		,_day(day)
	{}

private:
	//成员变量的声明
	const int _year = 0; 
	const int _month = 0;
	const int _day = 0;
};
int main()
{
	Date d;
	return 0;
}

2.3 注意事项

  1. 变量的初始化只能初始化一次,故每个成员变量在初始化列表中只能出现一次

  2. 当类中包含以下成员时,必须放在初始化列表位置进行初始化

    cpp 复制代码
    class A
    {
    public:
    	A(int a) //显式定义构造函数,不自动生成默认构造函数
    		:_a(a)
    	{}
    private:
    	int _a;
    };
    class B
    {
    public:
    	B(int a, int ref)
    		:_a(a) //调用有参构造函数初始化
    		, _ref(ref) //初始化引用变量
    		, _n(10) //初始化const变量
    	{}
    private:
    	A _a; // 没有默认构造函数的类
    	int& _ref; // 引用变量
    	const int _n; // const变量
    };
  3. 建议尽量使用初始化列表初始化,因为初始化列表是成员变量定义的地方,无论你是否显式地写,每个成员都要走初始化列表

    cpp 复制代码
    class Time
    {
    public:
    	Time(int hour = 0)
    		:_hour(hour)
    	{
    		cout << "Time()" << endl;
    	}
    private:
    	int _hour;
    };
    class Date1
    {
    public:
    	Date1(int day)
    		:_day(day)  //使用初始化列表进行初始化
    		,_t(day)
    	{}
    private:
    	int _day;
    	Time _t;
    };
    
    class Date2
    {
    public:
    	Date2(int day)
    	{
    		_day = day; //在构造函数内部进行赋值
    		_t = day;
    	}
    private:
    	int _day;
    	Time _t;
    };
    int main()
    {
    	Date1 d1(3);
    	cout << "-----------------------" << endl;
    	Date2 d2(3);
    	return 0;
    }
  4. C++11支持在声明处给缺省值,这个缺省值就是给初始化列表的。如果初始化列表没有显式给值,则使用这个缺省值;如果显式给了,就用给的值进行初始化。

  5. 初始化列表对成员变量的初始化顺序与其声明的次序相同,与初始化列表的先后次序无关。举个小例子

    cpp 复制代码
    class A
    {
    public:
    	A(int a)
    		:_a1(a)  //初始化列表的顺序和声明一样,即也是先初始化_a2再初始化_a1
    		, _a2(_a1)  //那么,这里用_a1初始化_a2会发生什么?_a1的值是多少
    	{}
    	void Print() {
    		cout << _a1 << " " << _a2 << endl;
    	}
    private:
    	//成员变量的声明,先_a2再_a1
    	int _a2;
    	int _a1;
    };
    int main() {
    	A aa(1);
    	aa.Print();
    }

    上面代码的输出结果是1 随机值****。

    **解析:**由于_a2的声明在_a1前,_a2会先于_a1进行初始化,因此_a2初始化时_a1还是个随机值,故_a2会被初始化为随机值,然后_a1再初始化为1。


    我们也可以使用调试来观察初始化顺序,如下所示:


三. explicit关键字

构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,还具有隐式类型转换的作用,如下:

cpp 复制代码
class Date
{
public:
    // 1. 单参构造函数,具有隐式类型转换作用
	Date(int year)
		:_year(year)
	{}

	//2. 虽然有多个参数,但是后两个参数可以不传递,具有类型转换作用
	//用explicit修饰构造函数,可以禁止类型转换
    //explicit Date(int year, int month = 1, int day = 1)
	//: _year(year)
	//, _month(month)
	//, _day(day)
	//{}
	
	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;
};
int main()
{
	Date d1(2022); //使用单参构造函数初始化d1

	// 用一个整形变量给日期类型对象赋值
	// 实际编译器背后会用2023构造一个匿名的临时对象,最后用这个临时对象给d1对象赋值
	d1 = 2023; 
	return 0;
}

像上面这种运算符左右两边类型不匹配,运算时编译器背后进行处理的过程,称之为隐式类型转换。


但是,这样的代码往往可读性不好,我们更希望书写代码时左右两边的类型是一致的,那有没有什么办法可以禁止编译器进行隐式类型转换呢?有,就是explicit关键字。

使用 explicit(显式的) 修饰构造函数,将会禁止构造函数的隐式类型转换。很简单,直接在构造函数前面加上explicit即可,这里就不再进行演示了。

四. static成员

4.1 概念

用static修饰的成员变量称为静态成员变量;用static修饰的成员函数称之为静态成员函数。静态成员变量一定要在类外进行初始化。

cpp 复制代码
class A
{
	static int GetCount() //静态成员函数
	{
		return count;
	}
private:
	static int count; //静态成员变量
};

int A::count = 10; //静态成员变量要在类外进行初始化

4.2 特性

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

**小问题:**静态成员函数可以调用非静态成员函数吗?反过来呢?


**问题解答:**答案是不行,静态成员函数不能调用非静态成员函数,因为静态成员函数没有隐藏的this指针,而非静态成员函数需要通过this指针来调用。但是非静态成员函数可以调用静态成员函数,因为静态成员函数的特点是没有this指针,故可以直接进行调用。


五. 友元

5.1 概念

在C++中,为了封装性我们一般将成员变量声明为【private】私有的,只允许在类内访问成员变量。但是有时候我们需要在类外访问这些成员变量,此时有两种方法:1.将成员变量声明为【public】共有;2.利用友元。

友元提供了一种突破封装的方式,为代码的编写提供了便利。友元分为友元类友元函数 ,当一个函数/类声明为某个类的友元函数/类时,这个函数/类访问类中成员时不受访问限定符限制。下面是函数/类声明为友元的方式,用到了friend关键字

cpp 复制代码
class A
{
	friend void GetCount(const A& a); //将全局函数GetCount声明为A类的友元函数
	friend class B; //将B类声明为A类的友元类
private:
	int count = 10;
	int num = 20;
};

class B
{
public:
	void GetNum(const A& a)
	{
		cout << a.num << endl; //b类中可以访问a类的私有成员
	}
};
void GetCount(const A& a)
{
	cout << a.count << endl; //可以访问A类的私有成员
}

int main()
{
	A a;
	B b;
	GetCount(a);
	b.GetNum(a);
	return 0;
}

**小贴士:**虽然友元提供了便利,但是友元会增加耦合度,破坏程序的封装性,故不建议使用友元。

5.2 友元函数

友元函数一般用作于流提取运算符>>以及流插入运算符<<的重载,这两个运算符的重载比较特殊,不能当做成员函数进行重载。

cpp 复制代码
class Date
{
public:
	Date(int year = 2023, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	//如果重载为Date的成员函数,第一个参数为隐藏的this指针,但cout是ostream类的对象,第一个参数应该是ostream类型,互相矛盾
	// ostream& operator<<(const Date& d);   
private:
	int _year;
	int _month;
	int _day;
};

//为了让第一个参数类型为ostream,故当做全局函数重载
const ostream& operator<<(const ostream& out, const Date& d)
{
	out << d._year << "年" << d.month << "月" << d.day << "日";
	return out;
}

int main()
{
	Date d;
	cout << d;  //重载流插入运算符使其可以输出日期类 
	return 0;
}

那么问题就来了,既然不能声明为成员函数,那我们在全局函数中要怎么访问Date的私有成员呢?

这时候就不得不使用我们上面说的友元了,将operator<<声明为Date类的友元函数后,代码成功运行:

cpp 复制代码
class Date
{
public:
	Date(int year = 2023, int month = 1, int day = 1)
		:_year(year)
		, _month(month)
		, _day(day)
	{}
	friend ostream& operator<<(ostream& out, const Date& d); //将operator<<全局函数声明为Date类的友元函数
private:
	int _year;
	int _month;
	int _day;
};

注意事项

  • 友元函数可以访问类中的私有成员,它是定义在类外部的普通函数,但需要在类的内部进行声明,声明时需要加friend关键字
  • 友元函数不能用const修饰,const只能修饰成员函数
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数

5.3 友元类

友元类中的所有成员函数都可以访问另一个类的非公有成员。友元关系是单向的,不具有交换性。例如B是A的友元类,B中的所有成员函数可以访问A中的私有成员,但A中的成员函数不能访问B中的私有成员。举例如下:

cpp 复制代码
class A
{
	friend class B; //定义B是A的友元类

	void GetSum(B& b)
	{
		cout << b.sum << endl;  //这里会报错,A类的成员函数无法访问B类的私有成员,不具有交换性
	}
private:
	int count = 20;
};

class B
{
	void GetCount(A& a)
	{
		cout << a.count << endl; //通过编译,B是A的友元类,B中成员函数可以访问A的私有成员
	}
private:
	int sum = 10;
};

以上程序编译时会报错如下


友元关系也不具有传递性。例如:C是B的友元类,B是A的友元类,无法说明C是A的友元。举例如下

cpp 复制代码
class A
{
	friend class B; //定义B是A的友元类
private:
	int a_sum = 10;
};

class B
{
	friend class C; //定义C是B的友元类
private:
	int b_sum = 20;
};

class C
{
	void GetBSum(B& b)
	{
		cout << b.b_sum << endl;  //编译通过,C是B的友元类
	}
	void GetASum(A& a)
	{
		cout << a.a_sum << endl;  //这里编译器会报错,C不是A的友元类,无法访问私有成员,友元关系不具有传递性
	}
private:
	int c_sum = 30;
};

以上程序编译时会报错如下


六. 内部类

一个类不仅可以定义在全局范围内,还可以定义在另一个类的内部。我们将定义在某个类内部的类称之为内部类。下面的B类就是一个内部类:

cpp 复制代码
class A //A称为外部类
{
public:

	class B //B类在A类的内部定义,称之为内部类
	{
	private:
		int sum; //b类的成员变量
	};

private:
	int count; //a类的成员变量
};

内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何特殊的访问权限。有以下两个具体体现

  • 内部类可以定义在外部类的的任何位置,不受外部类访问限定符的限制。
  • sizeof(外部类)=外部类,和内部类没有任何关系

内部类是外部类的友元类,内部类可以通过外部类的对象访问外部类的所有成员。但外部类不是内部类的友元类,无权访问内部类的私有成员。

cpp 复制代码
class A //A是外部类
{
public:

	class B //B是内部类
	{
		int GetACount(A& a)  
		{
			return a.count; //可以访问外部类的私有成员
		}
	private:
		int sum; 
	};

	int GetBSum(B& b)
	{
		return b.sum; //这里会报错,外部类不能访问内部类的私有成员
	}
private:
	int count; //a类的成员变量
};

内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名,如下所示

cpp 复制代码
class A //A是外部类
{
public:

	class B //B是内部类
	{
		int GetACount()  
		{
			return _count; //可以直接访问外部类的静态成员变量,无需类名/类对象
		}
	private:
		int sum; 
	};

private:
	static int _count; // A中的静态成员变量
};

int A::_count = 10; //类外进行初始化

七. 匿名对象

C++支持我们不给对象起名字,这样的对象我们称为匿名对象,其定义方式如下:

cpp 复制代码
int main()
{
	//对象类型+():创建一个匿名对象
	A();  //这里就是创建一个匿名对象A
	return 0;
}

匿名对象的声明周期只在当前行,当前行结束后会自动调用析构函数进行销毁:

匿名对象具有常属性,即不能对匿名对象中的成员变量进行修改:

cpp 复制代码
int main()
{
	A().count = 10; //编译器报错:表达式必须是可修改的左值
	return 0;
}

可以给匿名对象取别名,这样可以延长匿名对象的声明周期:

cpp 复制代码
int main()
{
	//给匿名对象取别名
	const A& cla1 = A(); //注意:这里必须是const引用,因为匿名对象具有常性,权限不能放大
	cout << "程序即将结束" << endl;
	return 0;
}

匿名对象经常用在仅需调用某个类的成员函数的情况,可以简化我们代码的编写。举例如下

cpp 复制代码
class Solution //Solution类用来求两数之和
{
public:
	int Sum_Solution(int x,int y)  //返回两数之和 
	{
		return x + y;
	}
};

int main()
{
	//不使用匿名对象
	Solution s1; //要先定义一个类对象,这个对象仅仅只是用来调用方法
	s1.Sum_Solution(2, 2); //然后再去调用成员函数

	//使用匿名对象
	Solution().Sum_Solution(2, 3); //代码更加简便
}

上面的Solution类是不是很熟悉?没错,在我们使用C++进行刷题时每次能够遇到它


以上,就是本期的全部内容啦 🌸

制作不易,能否点个赞再走呢 🙏

相关推荐
Ajiang28247353041 小时前
对于C++中stack和queue的认识以及priority_queue的模拟实现
开发语言·c++
幽兰的天空1 小时前
Python 中的模式匹配:深入了解 match 语句
开发语言·python
Theodore_10224 小时前
4 设计模式原则之接口隔离原则
java·开发语言·设计模式·java-ee·接口隔离原则·javaee
‘’林花谢了春红‘’5 小时前
C++ list (链表)容器
c++·链表·list
----云烟----6 小时前
QT中QString类的各种使用
开发语言·qt
lsx2024066 小时前
SQL SELECT 语句:基础与进阶应用
开发语言
开心工作室_kaic6 小时前
ssm161基于web的资源共享平台的共享与开发+jsp(论文+源码)_kaic
java·开发语言·前端
向宇it6 小时前
【unity小技巧】unity 什么是反射?反射的作用?反射的使用场景?反射的缺点?常用的反射操作?反射常见示例
开发语言·游戏·unity·c#·游戏引擎
武子康6 小时前
Java-06 深入浅出 MyBatis - 一对一模型 SqlMapConfig 与 Mapper 详细讲解测试
java·开发语言·数据仓库·sql·mybatis·springboot·springcloud
转世成为计算机大神7 小时前
易考八股文之Java中的设计模式?
java·开发语言·设计模式