前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【细品C++】类和对象的一些细节(初始化列表、友元、static成员等)

【细品C++】类和对象的一些细节(初始化列表、友元、static成员等)

作者头像
Crrrush
发布2023-06-23 14:39:53
4120
发布2023-06-23 14:39:53
举报
文章被收录于专栏:后端开发练级指南

写在前面

本篇文章是C++类和对象讲解的第三篇,将对前两篇未提及的知识进行收尾。如果你还没有看过前两篇文章的话,请点击这里(第一篇第二篇)。如果你已经看完了这两篇文章,你应该会觉得,某种意义上来讲,类和对象的知识也许称不上难,或者说难在杂乱。而本篇文章的知识似乎使杂乱度更上一层楼了。不过希望我对这些知识的整理能帮助你更好的理解这部分知识。

深究构造函数

构造函数体赋值与初始化

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

代码语言:javascript
复制
class date
{
public:
	date(int year, int month, int day)
	{
        //给成员变量赋值
		_year = year;
		_month = month;
		_day = day;
	}

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

虽然上述构造函数调用之后,对象已经拥有了一个初始值,但是不能将其称为对对象中成员变量的初始化,构造函数体内的语句只能将其称为赋初值,而不能称作初始化。因为初始化只能初始化一次(定义时),而构造函数体内可以多次赋值

那么一个类对象真正的定义初始化是在什么时候的呢?就在接下来提到的初始化列表当中。

初始化列表

初始化列表使用格式:在构造函数函数名与函数体(**{}**)之间,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个“成员变量”后面跟一个放在括号中的初始值或表达式

代码语言:javascript
复制
class date
{
public:
	date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
		//初始化成员变量
	{}

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

在讲初始化列表之前,我提到过,类对象缺少一个真正的定义初始化的地方,构造函数并不是初始化的地方,而是给成员变量赋值,或者做一些其他处理的地方。

代码语言:javascript
复制
int n;//定义,但并未初始化,此时是随机值
n = 0;//赋值,不是初始化
int x = 0;//定义并初始化为0

而对于由**const**修饰的类型以及引用类型,定义时初始化是其唯一的赋值机会,所以需要初始化列表来解决像这样的问题(也许设计C++的大佬一开始在设计构造函数时并没有考虑到这)。

代码语言:javascript
复制
class date
{
public:
	date(int year, int month, int day)
	{
        //在构造函数体内给const修饰的成员变量赋值会导致编译不通过
		_a = 0;//error C2789: “date::_a”: 必须初始化常量限定类型的对象
	}

private:
	int _year;
	int _month;
	int _day;

	const int _a;
};

除此之外,在上一篇文章中,我提到过内置类型成员在类中声明时可以给默认值,这个默认值相当于与函数的缺省参数,只不过上一篇没讲初始化列表,我没提。因为叫“缺省”,意味着有“传参“的地方,而初始化列表就是这个“传参”的地方。所以那里的默认值可以给的那么的“花哨随意”,可以调用函数使用返回值。本质其实是初始化列表的缺省在初始化时,成员变量也和普通内置类型的变量一样,可以使用值初始化,也可以调用函数并使用其返回值初始化

代码语言:javascript
复制
int* Create()
{
	return new int;
}

class date
{
public:
	date()
		:_year((int*)malloc(sizeof(int)))
		,_month(new int)
		,_day(Create())
	{}

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

同样是在上一篇文章中,提到过编译器生成的构造函数,拷贝构造函数对于自定义类型成员的处理是调用其对应的构造函数和拷贝构造函数,其实这也是通过初始化列表调用的。而在这里也留着一个坑。

这个坑就是,当编译器默认生成的构造函数处理自定义类型成员变量时,如果该类没有默认构造函数(无参或者全缺省的构造函数)时,会编译不通过。而你想自己写一个构造函数处理这个问题时,发现如果你想解决这个问题,你就必须手动调用这个自定义类型成员的构造函数,而你想调用这个构造函数必须要在这个自定义类型成员定义初始化时调用。而你是无法在构造函数函数体内解决这个问题的。

代码语言:javascript
复制
class Time
{
public:
	Time(int t)
		:_t(t)
	{}

private:
	int _t;
};

class date
{
public:

private:
	int _year;
	int _month;
	int _day;

	Time _T;
};

int main()
{
	date d;
	//error C2280: “date::date(void)”: 尝试引用已删除的函数
	//message : 编译器已在此处生成“date::date”
	//message : “date::date(void)”: 由于 数据成员“date::_T”不具备相应的 默认构造函数 或重载解决不明确,因此已隐式删除函数
	return 0;
}

所以,在这里,初始化列表又派上用场了。直接在初始化列表对自定义类型成员赋值就可以调用其构造函数

代码语言:javascript
复制
class Time
{
public:
	Time(int t)
		:_t(t)
	{}

private:
	int _t;
};

class date
{
public:
	date()
		:_T(0)
	{}
private:
	int _year;
	int _month;
	int _day;

	Time _T;
};

int main()
{
	date d;
	return 0;
}

当然,编译器生成的拷贝构造函数,对自定义类型调用其拷贝构造函数也是通过初始化列表调用的

到这里你会发现,其实有些情况几乎是必定只能通过初始化列表来解决的。而在之前的两篇文章中均没有使用过初始化列表,但是照样编译通过,运行正常。难道编译器能自动识别什么时候需要走初始化列表,什么时候不需要,还是说是根据程序员自己的实现来检查?来实验一下。

代码语言:javascript
复制
class Time
{
public:
	Time(int t = 0)
		:_t(t)
	{
		cout << "Time(int t = 0)" << endl;
	}
private:
	int _t;
};

class date
{
public:
	date()
	{}//没有显式写初始化列表
private:
	int _year;
	int _month;
	int _day;

	Time _T;
};

int main()
{
	date d;//创建对象
	return 0;
}

运行截图:

事实上,无论你使不使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。事实上,初始化列表全称叫做构造函数初始化列表,也就是说初始化列表是构造函数的一部分,无论时显式还是隐式,成员变量总是需要初始化的,这是一个类对象创建必经的步骤。所以不使用初始化列表初始化自定义类型成员变量,有时会造成构造函数对该成员变量既初始化又重新赋值覆盖这样的低效的场景。当然,比起效率更重要的是,在像以上的场景中,必须使用到初始化列表。所以,建议尽量使用构造函数初始化列表

以上就是对于初始化列表的基本介绍以及为什么要有初始化列表,接下来总结一下初始化列表的注意事项:

每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)

代码语言:javascript
复制
class date
{
public:
	date()
		:_year(0)
		,_year(1)//error C2437: “_year”: 已初始化
	{}
private:
	int _year;
	int _month;
	int _day;
};

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

引用成员变量

const**成员变量**

自定义类型成员变量(且该类型没有默认构造函数时)

尽量使用初始化列表初始化。

从概念上讲,可以认为构造函数分两个阶段执行:

(1)初始化阶段(函数体之前);(2)普通的计算阶段。(函数体内)

常规地使用初始化列表,可以避免使用只能在初始化列表初始化的类成员时出现编译错误。有时也可避免一些效率问题。当然,抛开这些不谈,无论是类成员,还是普通地使用内置类型变量,尽量对变量初始化是一个良好的编程习惯

成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表的先后次序无关。

代码语言:javascript
复制
class date
{
public:
	date()
		:_day(1),_month(_day),_year(1970)
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	date d;
	return 0;
}

运行截图:

所以,建议按照与成员声明一致的次序编写构造函数初始化列表,以及尽可能避免使用成员来初始化其他成员。

隐式类型转换中的构造与explicit关键字

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

代码语言:javascript
复制
class date
{
public:
	//1.单参构造函数,没有使用explicit修饰,具有类型转换作用
	date(int year)
		:_year(year)
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}

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

int main()
{
	date d(2022);
	d = 2023;
	return 0;
}

运行截图:

代码语言:javascript
复制
class date
{
public:
	//1.单参构造函数,没有使用explicit修饰,具有类型转换作用
	//date(int year)
	//explicit修饰构造函数,禁止类型转换——explicit去掉之后,代码可以通过编译
	explicit date(int year)
		:_year(year)
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	//error C2679 : 二元“ = ” : 没有找到接受“int”类型的右操作数的运算符(或没有可接受的转换)
	
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	date d(2022);
	d = 2023;
	return 0;
}
代码语言:javascript
复制
class date
{
public:
	//2.虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用
	date(int year,int month = 1,int day = 1)
		:_year(year),_month(month),_day(day)
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	

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

int main()
{
	date d(2022);
	d = 2023;
	return 0;
}

运行截图:

代码语言:javascript
复制
class date
{
public:
	//2.虽然有多个参数,但是创建对象时后两个参数可以不传递,没有使用explicit修饰,具有类型转换作用
	//date(int year,int month = 1,int day = 1)
	//使用explicit关键字修饰构造函数,禁止类型转换,代码无法通过
	explicit date(int year,int month = 1,int day = 1)
		:_year(year),_month(month),_day(day)
	{
		cout << _year << "/" << _month << "/" << _day << endl;
	}
	//error C2679 : 二元“ = ” : 没有找到接受“int”类型的右操作数的运算符(或没有可接受的转换)

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

int main()
{
	date d(2022);
	d = 2023;
	return 0;
}

上述代码中编译器瞒着我们做了很多事,有时候我们并不希望这样,并且这样代码可读性也不是很好,那就使用**explicit**修饰构造函数,禁止构造函数的隐式转换

static成员

概念

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

特性

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

特例

const static int成员变量可以缺省,仅此此类型可以在类中声明时给默认值(缺省值)。

代码语言:javascript
复制
//大佬设计想的用法
const static int N = 10;
int a[N];

友元

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

友元分为:友元函数友元类

友元函数

先来个例子引入,以class date为例,假设我要为这个类重载operator<<成员函数,用于打印我想要的数据。但是因为**cout**的输出流对象和隐含的**this**指针在抢占第一个参数的位置this指针默认是类成员函数的第一个参数,也就是<<左操作数固定为date类对象了。但是实际上我们正常使用中cout是左操作数,也就是需要作为重载函数的第一个形参,所以需要重载成全局函数,但是这又会导致类外没办法访问成员,此时就需要友元来解决。operator>>同理。

硬要重载成类成员就会这样:

代码语言:javascript
复制
class date
{
public:
	date(int year = 1970, int month = 1, int day = 1)
		:_year(year), _month(month), _day(day)
	{}

	ostream& operator<<(ostream& out)
	{
		out << _year << "/" << _month << "/" << _day << endl;
		return out;
	}
	
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	date d;
	d << cout;//可读性何在?
	//这样的用法非常怪,非常不合常规不是吗?
	return 0;
}

运行截图:

友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明式需要加friend关键字。

代码语言:javascript
复制
class date
{
	friend ostream& operator<<(ostream& out, date& d);
	friend istream& operator>>(istream& in, date& d);
public:
	date(int year = 1970, int month = 1, int day = 1)
		:_year(year), _month(month), _day(day)
	{}

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

ostream& operator<<(ostream& out,date& d)
{
	out << d._year << "/" << d._month << "/" << d._day;
	return out;
}

istream& operator>>(istream& in, date& d)
{
	in >> d._year >> d._month >> d._day;
	return in;
}

int main()
{
	date d;
	cin >> d;
	cout << d << endl;
	//两个操作符重载返回istream/ostream引用是为了能链式调用
	return 0;
}

运行截图:

说明:

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

友元类

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

代码语言:javascript
复制
class Time
{
	friend class date;
public:

private:
	int _h;
	int _m;
	int _s;
};

class date
{
public:
	date(int year = 1970, int month = 1, int day = 1)
		:_year(year), _month(month), _day(day)
	{
		_t._h = _t._m = _t._s = 0;
		//直接访问Time类对象的私有成员
	}


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

友元类的几个特性:

  • 友元关系是单向的,不具有交换性。 如上面代码的date类和Time类,在Time类中声明date类为其友元类,那么可以在date类中直接访问Time类的私有成员,但无法在Time类中访问date类的私有成员。
  • 友元关系不能传递 如果C是B的友元,B是A的友元,却不能说C是A的友元。
  • 友元关系不能继承,即父类的友元类继承给子类称为子类的友元类。(不懂没关系,之后我会写继承的讲解文章)

内部类

概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。

注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象来访问外部类中的所有成员。但是外部类不是内部类的友元。

代码语言:javascript
复制
class A
{
public:
	A(int a1)
		:_a1(a1)
	{}

	class B
	{
	public:
		void func(const A& a)
		{
			cout << a._a1 << endl;
			cout << _a2 << endl;
		}
	};

private:
	int _a1;
	static int _a2;
};

int A::_a2 = 0;

int main()
{
	A a(10);
	A::B b;//B类在A类域中,所以只能通过A类域找B
	b.func(a);

	return 0;
}

运行截图:

特性:

  1. 内部类可以定义在外部类的publicprotectedprivate都是可以的。
  2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
  3. sizeof(外部类)=外部类,和内部类没有任何关系。

匿名对象

代码语言:javascript
复制
class A
{
public:
	A(int a1 = 0)
		:_a1(a1)
	{
		cout << "A(int a1 = 0)" << endl;
	}

	~A()
	{
		cout << "~A()" << endl;
	}

private:
	int _a1;
};

class solution
{
public:
	void funtion(int a)
	{
		//...
	}

private:
};

int main()
{
	//A a1();
	//不能这么定义对象,因为编译器无法识别下面是一个函数声明还是对象定义

	//但是可以这样定义匿名对象,匿名对象的特点是不用取名字
	//且声明周期只有这一行,我们可以看到下一行他就会自动调用析构函数
	A();
	cout << "i'm flag.Destructor was called before the end of program." << endl;

	//匿名对象在以下场景很好用,其他也有,详细看我之后的文章
	solution().funtion(100);

	return 0;
}

运行截图:

拷贝对象时的一些编译器优化

在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。

代码语言:javascript
复制
class A
{
public:
	A(int a = 0)
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a(aa._a)
	{
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{
			_a = aa._a;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
void f1(A aa)
{}
A f2()
{
	A aa;
	return aa;
}
int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	// 传值返回
	f2();
	cout << endl;
	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
    cout << endl;
	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
	A aa2 = f2();
	cout << endl;
	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

运行截图:

结语

以上就是类和对象最后一部分知识的讲解了,希望能帮助到你的C++学习。如果你觉得做的还不错的话请点赞收藏加分享,当然如果发现我写的有误或者有建议给我的话欢迎在评论区或者私信告诉我。

彩蛋

博客源码:GitHub gitee

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-02-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 深究构造函数
    • 构造函数体赋值与初始化
      • 初始化列表
        • 隐式类型转换中的构造与explicit关键字
        • static成员
          • 概念
            • 特性
              • 特例
              • 友元
                • 友元函数
                  • 友元类
                  • 内部类
                  • 匿名对象
                  • 拷贝对象时的一些编译器优化
                  • 结语
                  • 彩蛋
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档