草庐IT

【C++】类与对象(下)

LinAlpaca 2023-06-07 原文

系列文章

 若想了解什么是类、封装的意义可以移步 【C++】类与对象(引入)

 若对六大成员函数或const成员函数有疑问的这篇文章可能可以帮到你 【C++】类与对象(上)


目录

系列文章

前言

1.初始化列表

1.1概念

1.2特性 

1.2.1必须使用初始化列表的情况

1.2.2初始化的顺序

2.explicit关键字

3.Static成员

3.1静态成员变量

3.2静态成员函数

3.3功能实现

4.友元

4.1友元函数

4.2友元类

5.内部类

6.匿名对象

6.1使用

6.2证明生命周期

7.拷贝对象时编译器的优化

总结


前言

🧋这一期博客算是给整个类与对象的系列做个收尾,补充类的一些其他功能,还有对类与对象更多的细节理解。


1.初始化列表

🧋虽然说我们已经学会了使用构造函数对函数进行初始化,但下面这个情况是否出乎你的意料呢?const 的成员变量,因其特性一开始我们便需要对其初始化但想在函数之中修改却又无法修改,这是为什么呢?

        

 🧋有同学说,在定义变量的时候使用缺省不就好了吗?这个方式确实能够解决问题,但缺省的这个功能是 C++11 才出现的,在那之前难道就无法解决这个问题吗?究其本质,在构造函数体内的这种操作已经算是赋值了真正初始化变量的地方并不在这,而是我们接下来要讲的初始化列表。

1.1概念

🧋初始化列表:以 开头,接以    分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式

🧋位置位于构造函数名与函数内容之间

class A
{
public:
	A()
		:_a(5)    //初始化列表初始化_a和_b
		,_b(3)
	{}
private:
	int _a;
	const int _b;
};

int main()
{
	A a;
	return 0;
}

1.2特性 

🧋每个成员变量只能在初始化列表中出现一次。就像外部定义变量那样,同名的变量也不可以定义两次!

🧋正如上文所说,构造函数的函数体的本质只是给变量赋值,而初始化的这个步骤则交给了初始化列表,因此:

🧋不管是否显式写在初始化列表里,都会在初始化列表里初始化 。

1.2.1必须使用初始化列表的情况

🧋以下三种情况必须使用初始化列表进行初始化:

  • 引用成员变量
  • const成员变量
  • 没有默认构造函数的自定义类型成员 

🧋前面两种成员是因为其本身特性而导致不得不在初始化的时候就得赋值,但第三种不太一样,需要单独拎出来讲一讲。

🧋没有默认的构造函数即指类中至少有一个非全缺省的构造函数,这才满足类中没有默认构造函数的情况。我们知道对于自定义类型,编译器会自动调用目标类的默认构造函数,因此当前情况便会出现没有函数能够调用的情况

🧋使用初始化列表为该类的构造函数传入一个初始值,去调用该类的构造函数,便可完成该成员的初始化。 

class B
{
public:
	B(int b)
	{
		_b = b;
	}
private:
	int _b;
};

class A
{
public:
	A()
		:_a(5)    //初始化列表初始化_a
		,_bb(8)
	{}
private:
	int _a;
	B _bb;
};

int main()
{
	A a;
	return 0;
}

1.2.2初始化的顺序

🧋先看以下代码:

class A
{
public:
	A()
		:a1(2)   
		,a2(a1)
	{}
	void print()
	{
		printf("%d %d", a1, a2);
	}
private:
	int a2;
	int a1;
};

int main()
{
	A a;
	a.print();
	return 0;
}

🧋我们发现最后输出的结果竟然一个是 另一个是随机值。这是因为变量初始化的顺序是根据声明的顺序进行的。由于 a2 先声明,所以先初始化 a2 ,但由于此时 a1 还未被初始化,因此 a2 就被初始化成了随机值。

为了避免编译错误,尽量使用初始化列表初始化

2.explicit关键字

🧋我们都知道定义对象时还有这样的一种写法,并知道他是通过调用构造函数来进行初始化的。但真的是这样吗?其实在这个过程之中会产生一个隐式类型转换,用 来构建一个 的临时变量,之后用这个变量对 进行拷贝构造。之后编译器对这个过程进行了优化,才变成只出现调用一次构造函数。

A a = 1;

🧋如何证明这其中生成了临时变量呢?看下图,为什么使用普通的引用就会报错,而使用常引用没有出现错误了呢?

 

 🧋总所周知,临时变量具有常性,若用一个常变量给一个普通变量赋值,则会出现权限放大的情况,这时只要定义一个常变量便不会出现权限放大的情况。由此便可以证明我们上面所说的隐式类型转换是存在的

🧋若我们不想要让这种类型转换发生,就需要引入这个关键字 explict 

🧋在构造函数前增加这个关键字,上面的代码便无法通过编译。其针对的不仅仅是单参数的构造函数。

 

🧋而多参数的构造函数需要这样使用才能够初始化(需在C++11的环境下) 

class A
{
public:
	A(int a,int b)
		:_a(a)
		,_b(b)
	{}
private:
	int _a;
	int _b;
};

int main()
{
	A a = { 1,1 };
	return 0;
}

🧋且这也同样能用 explicit 禁止类型转换。

 

3.Static成员

3.1静态成员变量

🧋用 static 修饰的类成员称为类的静态成员,用 static 修饰的成员变量,被称为静态成员变量

🧋而静态成员变量不属于某个对象,而是属于整个类。因此静态成员变量一定要在类外进行初始化

🧋根据如下代码就完成了对静态成员变量的定义:

class A
{
public:
	A()
	{}
private:
	static int _a;       //声明静态成员变量
};

int A::_a = 5;           //初始化静态成员变量

int main()
{
	A a;
	return 0;
}

3.2静态成员函数

🧋静态成员函数的本质还是静态成员,同样,他也是所有类中共享的,一般用来访问静态成员变量

class A
{
public:
	A()
	{}
	static int get_a()   //静态成员函数
	{
		return _a;       //返回静态成员变量
	}
private:
	static int _a;       //声明静态成员变量
};

int A::_a = 5;           //初始化静态成员变量

int main()
{
	A a;
	cout << a.get_a(); 
	return 0;
}

3.3功能实现

🧋认识了静态成员后,我们就可以根据他们的这个性质实现一个类的小功能。我们能够实现统计当前类的实例化个数

class A
{
public:
	A()
	{
		_cnt++;             //实例化则增加1计数
	}
	A(const A& a)
	{
		_cnt++;             //实例化则增加1计数
	}
	~A()
	{
		_cnt--;             //析构则减少1计数
	}
	static int get_memnum()  //使用静态成员函数获取静态成员变量
	{
		return _cnt;
	}
private:
	static int _cnt;       //声明静态成员变量
};

int A::_cnt = 0;           //初始化静态成员变量为0

int main()
{
	A a;
	cout << a.get_memnum(); 
	return 0;
}


4.友元

🧋友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度破坏了封装,所以友元不宜多用

其中分作友元函数友元类

4.1友元函数

🧋当我们对<<运算符进行重载时,会发现若在类中定义,则第一个操作数便固定为this指针,便与我们平时使用流提取的写法相反。

class A
{
public:
	A(int a,int b)
		:_a(a)
		,_b(b)
	{}

	ostream& operator<<(ostream& _cout)
	{
		_cout << _a << _b << endl;
		return _cout;
	}
private:
	int _a;
	int _b;
};

int main()
{
	A a = { 1,3 };
	a << cout;
	return 0;
}

 🧋因此,这个重载的函数便不能写在类中,但写在类外又访问不到类中的私有成员变量。因此便可以将这个函数定义成友元。就像是一种信任关系,对这个函数去掉访问限定符的限制。这样一来,外部的函数便可以访问到内部的的保护成员和私有成员

🧋使用时需要在类的内部声明,声明时需要加 friend 关键字。

class A
{
public:
	A(int a,int b)
		:_a(a)
		,_b(b)
	{}

	friend ostream& operator<<(ostream& _cout, const A& a);
private:
	int _a;
	int _b;
};


ostream& operator<<(ostream& _cout, const A& a)
{
	_cout << a._a << a._b << endl;
	return _cout;
}

int main()
{
	A a = { 1,3 };
	cout << a;
	return 0;
}

 [注意] 

  • 友元函数可访问类的私有和保护成员,但不是类的成员函数
  • 友元函数不能用 const 修饰。
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
  • 一个函数可以是多个类的友元函数
  • 友元函数的调用与普通函数的调用原理相同。

4.2友元类

🧋友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。

友元关系是单向的,不具有交换性。如下面的代码,在B中声明A为B的友元,在A中能够便能够访问B的私有成员,但B却无法访问A中的私有成员
友元关系不能传递,如果C是B的友元, B是A的友元,但C不是A的友元

友元关系也不能继承

class B
{
public:
	friend class A;    //声明A为B的友元
	B(int b)
		:_b(b)
	{}
private:
	int _b;
};

class A
{
public:
	A(int a)
		:_a(a)
		,b(2)
	{}
	void print()
	{
		cout << b._b << endl;  //在A中就能够访问b中的私有成员
	}
private:
	int _a;
	B b;
};

int main()
{
	A a = 1;
	a.print();
	return 0;
}


5.内部类

🧋如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,不能用外部类的对象去访问内部类的成员。即内部类是外部类的友元类内部类可以访问外部类的所有成员,但外部类却无法访问内部类的私有成员

 [注意] 

内部类外部类类域的限制

内部类可以定义在外部类的任意位置

内部类可以直接访问外部类中的 static 成员,不需要外部类的对象/类名。
sizeof (外部类) = 外部类的大小,和内部类没有任何关系。

class A
{
public:
	A(int a)
		:_a(a)
	{}
	class B                  //B天生是A的友元
	{
	public:
		B(int b)
			:_b(b)
		{}
		void print(const A& a)
		{
			cout << a._a << " " << _b << endl;  //b能够访问到a中的私有成员变量
		}
	private:
		int _b;
	};
private:
	int _a;
};

int main()
{
	A a = 1;
	A::B b = 5;
	b.print(a); 
	return 0;
}


6.匿名对象

6.1使用

当我们在某个时刻想要使用一个类,且仅使用一次以后便不再使用的情况下,可以试试匿名对象。

直接在类名后加上括号便可以使用,其生命周期只在定义的这一行内,这行结束便会自动销毁。

class A
{
public:
	A(int a)
		:_a(a)
	{}
	void print()
	{
		cout << _a << endl;   //输出成员变量的值
	}
private:
	int _a;
};

int main()
{
	A(3).print();    //使用匿名对象
	return 0;
}

6.2证明生命周期

 当我们调试到匿名对象的下一行语句时,该类的构造函数已经被调用了,即此时上一行的匿名对象已经被销毁了

7.拷贝对象时编译器的优化

🧋新的编译器在传参和传返回值时,会做一些优化,减少对对象的拷贝。当一行中满足以下条件便会进行优化。

构造 + 拷贝构造 -> 优化为直接构造

拷贝构造 + 拷贝构造 -> 1个拷贝构造

构造 + 构造 -> 1个直接构造

🧋由于传引用传参本质就是传一个别名,因此无优化

🧋因为是否优化的判断区间为当前一行,因此正确使用匿名对象有利于编译器的优化

🧋对象返回的总结

接收返回值对象,尽量以拷贝构造的方式接收不要赋值接收

函数中返回对象时,尽量返回匿名对象 

🧋函数传参的总结

尽量使用 const& 传参 。


总结

今天,我们在原来类的基础上补充了一些细节,讲述了初始化列表explicit 关键字,静态变量友元匿名对象,以及编译器在拷贝对象时的优化。每个知识点的内容都比较零散,因此要在自己理解后再用代码进行实现,才能真正的掌握。掌握编译器的规律来优化代码的效率,才能对语言的理解更近一步。

🧋好了,今天类与对象下半部分讲解到这里就结束了,如果这篇文章对你有用的话还请留下你的三连加关注。

有关【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 - 一个 YAML 对象可以引用另一个吗? - 2

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

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

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

  10. ruby-on-rails - ActiveRecord 对象相等 - 2

    根据ActiveRecord::Base的文档:==(comparison_object)Returnstrueifcomparison_objectisthesameexactobject,orcomparison_objectisofthesametypeandselfhasanIDanditisequaltocomparison_object.id.Notethatnewrecordsaredifferentfromanyotherrecordbydefinition,unlesstheotherrecordisthereceiveritself.Besides,ifyoufet

随机推荐