草庐IT

C++类和对象(下)

夜 默 2023-10-21 原文

✨个人主页: Yohifo
🎉所属专栏: C++修行之路
🎊每篇一句: 图片来源

  • I do not believe in taking the right decision. I take a decision and make it right.
    • 我不相信什么正确的决定。我都是先做决定,然后把事情做好。


文章目录


📘前言

在前两篇关于类和对象的文章中,我们学习了C++类的基本形式、对象的创建与使用以及每个类中都有的六大天选之子:默认成员函数,现在对类的基本框架已经搭好,关于类和对象的学习还存在一些细节,深入理解这些细节就是本文的主要目的


📘正文

先从上篇文章中的结论开始学习

📖初始化列表

初始化列表是祖师爷认证的成员变量初始化位置,初始化列表紧跟在默认构造函数之后,形式比较奇怪:主要通过 :,()实现初始化

class Date
{
public:
    Date(int year = 1970, int month = 1, int day = 1)
        :_year(year)	//以下三行构成初始化列表
        , _month(month)
        , _day(day)
    {
        //………
    }
private:
    int _year;
    int _month;
    int _day;
};

学习初始化列表前先来简单回顾下原初始化方式

🖋️原初始化方式

之前我们的默认构造函数是这样的:

class Date
{
public:
    Date(int year = 1970, int month = 1, int day = 1)
    {
    	//此时是赋值,变量在使用前,仍然是随机值状态
    	_year = year;
    	_month = month;
    	_day = day;
    }
private:
    int _year;
    int _month;
    int _day;
};

实例化一个对象,调试结果如下
正式赋值前已被初始化为随机值

并且如果我们的成员变量中新增一个 const 修饰的成员:

private:
    int _year;
    int _month;
    int _day;
    const int _ci

程序运行结果就会变成这样:

直接编译报错了,证明当前初始化方式存在很大问题

原因:

  • 默认构造函数是以赋值的方式实现“初始化”
  • 被赋值的前提是已存在,而存在必然伴随着初始化行为
  • 此时由编译器负责,也就是编译器的惯用手段:给变量置以随机值实现初始化
  • 成员变量在被赋值前已经被初始化了
  • const 修饰的成员具有常性,只能初始化一次
  • 也就意味着此时的成员 _ci 已经被初始化为随机值,并且被 const 修饰,具有常性,无法被赋值
  • 总结: 原赋值的初始化方式在某些场景下行不通

原赋值初始化方式的缺点:

  • 无法给 const 修饰成员初始化
  • 无法给 引用 成员初始化
  • 无法给 自定义 成员初始化(且该类没有默认构造函数时)

此时祖师爷看不下去了,决定打造一种新的初始化方式:初始化列表,并为初始化列表指定了一个特殊位置:默认构造函数之后

🖋️使用初始化列表

初始化列表基本形式:

  • 紧跟在默认构造函数之后,首先以 ; 开始
  • 初始化格式为 待初始化对象(初始值)
  • 之后待初始化成员都以 , 开头
  • 不存在结尾符号,除了第一个成员用 ; 开头外,其他成员都用 , 开头
class Date
{
public:
    Date(int year = 1970, int month = 1, int day = 1, const int ci = 0, const int& ref = 0)
        :_year(year)	//以下三行构成初始化列表
        , _month(month)
        , _day(day)
        , _ci(ci)	//const 成员能初始化
        , _ref(ref)	//引用成员也能初始化
    {
        //………
    }
private:
    int _year;
    int _month;
    int _day;
    const int _ci;
    const int& _ref;
};

在初始化列表的加持下,程序运行结果如下
进入默认构造函数体内时,成员变量已被初始化


初始化列表能完美弥补原赋值初始化的缺点

如此好用的初始化方式为何不用呢?
祖师爷推荐: 尽量使用初始化列表进行初始化,全能又安心

强大的功能靠着周全的规则支撑,初始化列表有很多注意事项(使用规则)

🖋️注意事项

使用方式

  • ; 开始 , 分隔,() 内写上初始值

注意

  • 初始化列表中的成员只能出现一次
  • 初始化列表中的初始化顺序取决类中的声明顺序
  • 以下几种类型必须使用初始化列表进行初始化
    • const 修饰
    • 引用 类型
    • 自定义类型,且该自定义类型没有默认构造函数

建议

  • 优先选择使用初始化列表
  • 列表中的顺序与声明时的顺序保持一致

规范使用初始化列表,高高兴兴使用


📖explicit关键字

explicit 是新的关键字,常用于修饰 默认构造函数,限制隐式转换,使得程序运行更加规范

🖋️隐式转换

所谓隐式转换就算编译器在看到赋值双方类型不匹配时,会将创建一个同类型的临时变量,将 = 左边的值拷贝给临时变量,再拷贝给 = 右边的值,比如:

int a = 10;
double b = 3.1415926;
a = b;	//成功赋值,将会截取浮点数 b 的整数部分拷贝给临时变量,再赋值给 a

具体赋值过程如下
需要借助一个同类型的临时变量

将此思想引入类中,假设存在这样一个类:

class A
{
public:
	//默认构造函数
	A(int a = 0)
		:_a(a)
	{
		//表示默认构造函数被调用过
		cout << "A(int a = 0)" << endl;
	}

	//默认析构函数
	~A()
	{
		_a = 0;

		//表示默认析构函数已被调用
		cout << "~A" << endl;
	}

	//拷贝构造函数
	A(const A& a)
	{
		_a = a._a;

		cout << "A(const A& a)" << endl;
	}

	//赋值重载函数
	A& operator=(const A& a)
	{
		if(this != &a)
		{
			_a = a._a;
		}

		cout << "A& operator=(const A& a)" << endl;
		return *this;
	}

private:
	int _a;
};

以下语句的赋值行为是合法的

int main()
{
	A aa1 = 100;	//注:此时整型 100 能赋给自定义类型
	return 0;
}

合法原因:

  • 类中只有一个整型成员
  • 赋值时,会先生成同类型临时变量,即调用一次构造函数
  • 再调用拷贝构造函数,将临时变量的值拷贝给 aa1


我们可以看看打印结果是否真的如我们想的一样

结果:只调用了一次构造函数

难道编译器偷懒了?

  • 并不是,实际这是编译器的优化
  • 与其先生成临时变量,再拷贝,不如直接对目标进行构造,这样可以提高效率

这是编译器的优化行为,大多数编译器都支持

看代码会形象些:

A aa1 = 100;	//你以为的
A aa1(100);	//实际编译器干的,优化!

单参数类赋值时编译器有优化,那么多参数类呢?

class B
{
private:
	int _a;
	int _b;
	int _c;
}

多参数类编译器也会有优化

B bb1 = {1, 2, 3};	//你以为的
B bb1(1, 2, 3);	//实际上编译器优化的

编译器是这样认为的:构造临时变量+拷贝构造不如让我直接给你构造

这是编译器针对隐式转换做出的优化行为

不难发现,这样的隐式转换虽然方便,但会影响代码的可读性和规范性,我们可以通过关键字explicit 限制隐式转换行为

🖋️限制转换

默认构造函数前加上explicit修饰

class A
{
public:
	//默认构造函数
	//限制隐式转换行为
	explicit A(int a = 0)
		:_a(a)
	{
		//表示默认构造函数被调用过
		cout << "A(int a = 0)" << endl;
	}
private:
	int _a;
};

此时再次采用上面那种赋值方式会报错

A aa1 = 100;	//报错,此时编译器无法进行隐式类型转换,优化也就不存在了

何时采用 explicit 修饰?

  • 想提高代码可读性和规范性时

📖static修饰

static 译为静态的,修饰变量时,变量位于静态区,生命周期增长至程序运行周期

static 有很多讲究,可不敢随便乱用:

  • 修饰普通局部变量时,使其能在全局使用
  • 修饰全局变量时,破坏其外部链接属性
  • static 修饰时,只能被初始化一次
  • static 不能随便乱用

🖋️static在类中

类中被 static 修饰的成员称为 静态成员变量静态成员函数

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

课代表简单总结:

  • 静态成员变量必须在类外初始化(定义)
  • 静态成员函数 失去了 this 指针,但当为 public 时,可以通过 类名::函数名直接访问
class Test
{
public:
	//Test(int val = 0, static int sVal = 0)
	//	:_val(val)
	//	, _sVal(sVal)	//非法的,初始化列表也无法初始化静态成员变量
	//{}

	static void Print()
	{
		//cout << _val << endl;	//非法的,没有 this 指针,无法访问对象成员
		cout << _sVal << endl;
	}
private:
	int _val;
	static int _sVal;	//静态成员变量
};

int Test::_sVal = 0;	//静态成员变量必须在类外初始化(定义),需指定属于哪个类

静态成员变量只能初始化一次

静态成员函数没有 this 指针

静态成员函数是为静态成员变量而生

如此刁钻的成员变量究竟有何妙用呢?

  • 答: 有的,存在即合理

利用静态成员变量只能初始化一次的特定,写出函数统计程序运行中调用了多少次构造函数

class Test
{
public:
	Test(int val = 0)
		:_val(val)
	{
		_sVal++;	//利用静态成员变量进行累加统计
	}

	static void Print()
	{
		cout << _sVal;
	}
private:
	int _val = 0;
	static int _sVal;	//静态成员变量
};

int Test::_sVal = 0;	//静态成员变量必须在类外初始化(定义)

int main()
{
	Test T[10];	//调用十次构造函数

	//通过静态成员变量验证
	cout << "程序共调用了";
	Test::Print();
	cout << "次成员函数" << endl;
	return 0;
}

输出结果如下:
得益于 static 修饰的成员变量统计

注意:

  • 静态成员函数 不可以调用 非静态成员变量,没有 this 指针
  • 非静态成员函数 可以调用 静态成员变量,具有全局属性

📖匿名对象

C语言结构体支持创建匿名结构体C++ 则支持创建匿名对象


匿名对象使用如下:

//假设存在日期类 Date
int main()
{
	Date();	//此处就是一个匿名对象
	
	return 0;
}

匿名对象拥有正常对象的所有功能,缺点就是生命周期极短,只有一行

//演示
Date(2023, 2, 10);	//匿名对象1 初始化
Date().Print();	//匿名对象2 调用打印函数

//注意:两个匿名对象相互独立,创建 匿名对象2 时, 匿名对象1 已被销毁

🖋️使用场景

匿名对象适合用于某些一次性场景,也适合用于优化性能

Date(2023, 2, 10).Print();	//单纯打印日期 2023 2 10

//函数返回时
Date d(2002, 1, 1);
return d;
//等价于
return Date(2002, 1, 1);	//提高效率

📖友元

新增关键字 friend ,译为朋友,常用于外部函数在类中的友好声明

类中的成员变量为私有,类外函数无法随意访问,但可以在类中将类外函数声明为友元函数,此时函数可以正常访问类中私有成员

友元函数会破坏类域的完整性,有利有弊


注意:

  • 友元是单向关系
  • 友元不具有传递性
  • 友元不能继承
  • 友元声明可以写在类中的任意位置

🖋️友元函数

friend 修饰函数时,称为友元函数

class Test
{
public:
	//声明外部函数 Print 为友元函数
	friend void Print(const Test&d);

	Test(int val = 100)
		:_val(val)
	{}

private:
	int _val;
};

void Print(const Test& d)
{
	cout << "For Friend " << d._val << endl;
}

int main()
{
	Test t;
	Print(t);
}

程序正常编译,结果如下:

友元函数可以用来解决外部运算符重载函数无法访问类中成员的问题,但还是不推荐这种方法

🖋️友元类

friend 修饰类时,称为友元类

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

友元有种继承的感觉,但它不是继承,它也不支持继承


📖内部类

将类B写在类A中,B 称作 A 的内部类

class A
{
public:
	//B 称作 A 的内部类
	class B
	{
	private:
		int _b;
	}
private:
	int _a;
}

内部类天生就是外类的友元类

也就是说,B 天生就能访问 A 中的成员

🖋️特性

内部类在C++中比较少用,属于了解型知识

内部类的最大特性就是使得内部类能受到外类的访问限定符限制

内部类特点:

  • 独立存在
  • 天生就是外类的友元

用途:

  • 可以利用内部类,将类隐藏,现实中比较少见

注意:

  • 内部类跟其外类是独立存在的,计算外类大小时,是不包括内部类大小的
  • 内部类受访问限定符的限定,假设为私有,内部类无法被直接使用
  • 内部类天生就算外类的友元,即可以访问外类中的成员,而外类无法访问内部类

📖编译器优化

前面说过,编译器存在优化行为,这里就来深入探讨一下
把上面的代码搬下来用一下,方便观察发生了什么事情

class A
{
public:
	//默认构造函数
	A(int a = 0)
		:_a(a)
	{
		//表示默认构造函数被调用过
		cout << "A(int a = 0)" << endl;
	}

	//默认析构函数
	~A()
	{
		_a = 0;

		//表示默认析构函数已被调用
		cout << "~A" << endl;
	}

	//拷贝构造函数
	A(const A& a)
	{
		_a = a._a;

		cout << "A(const A& a)" << endl;
	}

	//赋值重载函数
	A& operator=(const A& a)
	{
		if(this != &a)
		{
			_a = a._a;
		}

		cout << "A& operator=(const A& a)" << endl;
		return *this;
	}

private:
	int _a;
};

🖋️参数优化

在类外存在下面这些函数:

void func1(A aa)
{}

int main()
{
	func1(100);
	return 0;
}

预计调用后发生了这些事情:
构造(隐式转换) -> 拷贝构造(传参) -> 构造(创建aa接收参数)

编译器会出手优化

实际只发生了这些事情:
构造(直接把aa构造为目标值)

🖋️返回优化

除了优化传参外,编译器还会优化返回值

A func2()
{
	return A(100);
}

int main()
{
	//func1(100);
	A a = func2();
	return 0;
}

预计调用后发生了这些事情:
构造(匿名对象的创建) -> 构造(临时变量) -> 拷贝构造(将匿名对象拷贝给临时变量) -> 拷贝构造(将临时变量拷贝给 a)

编译器会出手优化

实际只发生了这些事情:
构造(直接把函数匿名对象值看作目标值,构造除出 a)

现在可以证明:编译器会将某些非必要的步骤省略点,执行关键步骤

优化场景:

  • 涉及拷贝构造+构造时,编译器多会出手
  • 传值返回时,涉及多次拷贝构造,编译器也会出手

注意:

  • 引用传参时,编译器无需优化,因为不会涉及拷贝构造
  • 实际编码时,如果能采用匿名构造,就用匿名构造,会加速编译器的优化
  • 接收参数时,如果分成两行(先定义、再接收),编译器无法优化,效率会降低

编译器只能在一行语句内进行优化,如果涉及多条语句,编译器也不敢擅自主张

🖋️编码技巧

下面是一些编码小技巧,可以提高程序运行效率

  • 接收返回值对象时,尽量拷贝构造方式接收,不要赋值接收
  • 函数返回时,尽量返回匿名对象
  • 函数参数尽量使用 const& 参数

📖再次理解类和对象


出自:比特教育科技


📘总结

以上就是 类和对象(下)的全部内容了,我们在本文章学习了一些类和对象的小细节,比如明白了善用初始化列表的道理、懂得了友元函数的用法、了解了编译器的优化事实、最后还简单理解了类和对象与现实的关系,相信在这些细节的加持之下,对类和对象的理解能更上一层楼!

如果你觉得本文写的还不错的话,可以留下一个小小的赞👍,你的支持是我分享的最大动力!

如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正


相关文章推荐
类和对象(上)
类和对象(中)
C++入门基础

有关C++类和对象(下)的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  3. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  4. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  5. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  6. ruby-on-rails - 未在 Ruby 中初始化的对象 - 2

    我在Rails工作并有以下类(class):classPlayer当我运行时bundleexecrailsconsole然后尝试:a=Player.new("me",5.0,"UCLA")我回来了:=>#我不知道为什么Player对象不会在这里初始化。关于可能导致此问题的操作/解释的任何建议?谢谢,马里奥格 最佳答案 havenoideawhythePlayerobjectwouldn'tbeinitializedhere它没有初始化很简单,因为你还没有初始化它!您已经覆盖了ActiveRecord::Base初始化方法,但您没有调

  7. ruby - 如何在 Rails 4 中使用表单对象之前的验证回调? - 2

    我有一个服务模型/表及其注册表。在表单中,我几乎拥有服务的所有字段,但我想在验证服务对象之前自动设置其中一些值。示例:--服务Controller#创建Action:defcreate@service=Service.new@service_form=ServiceFormObject.new(@service)@service_form.validate(params[:service_form_object])and@service_form.saverespond_with(@service_form,location:admin_services_path)end在验证@ser

  8. Ruby——嵌套类和子类是一回事吗? - 2

    下面例子中的Nested和Child有什么区别?是否只是同一事物的不同语法?classParentclassNested...endendclassChild 最佳答案 不,它们是不同的。嵌套:Computer之外的“Processor”类只能作为Computer::Processor访问。嵌套为内部类(namespace)提供上下文。对于ruby​​解释器Computer和Computer::Processor只是两个独立的类。classComputerclassProcessor#Tocreateanobjectforthisc

  9. ruby - 一个 YAML 对象可以引用另一个吗? - 2

    我想让一个yaml对象引用另一个,如下所示:intro:"Hello,dearuser."registration:$introThanksforregistering!new_message:$introYouhaveanewmessage!上面的语法只是它如何工作的一个例子(这也是它在thiscpanmodule中的工作方式。)我正在使用标准的ruby​​yaml解析器。这可能吗? 最佳答案 一些yaml对象确实引用了其他对象:irb>require'yaml'#=>trueirb>str="hello"#=>"hello"ir

  10. ruby - 更改 ActiveRecord 中对象的类 - 2

    假设我有一个FireNinja我的数据库中的对象,使用单表继承存储。后来才知道他真的是WaterNinja.将他更改为不同的子类的最干净的方法是什么?更好的是,我很想创建一个新的WaterNinja对象并替换旧的FireNinja在数据库中,保留ID。编辑我知道如何创建新的WaterNinja来self现有FireNinja的对象,我也知道我可以删除旧的并保存新的。我想做的是改变现有项目的类别。我是通过创建一个新对象并执行一些ActiveRecord魔法来替换行,还是通过对对象本身做一些疯狂的事情,或者甚至通过删除它并使用相同的ID重新插入来做到这一点,这是问题的一部分。

随机推荐