【C++】类和对象(三)再探构造函数|static成员函数|友元函数|内部类|匿名对象|对象拷贝时的编译优化



欢迎来到Harper·Lee的学习笔记!

一、再探构造函数

  1. 初始化列表:构造函数初始化的第二种方式(第一种是使用函数体内赋值)。
  2. 使用方式:以一个冒号:开始,用逗号,分隔数据成员列表,每个成员变量后面跟一个放在括号里面的初始值或者表达式,建议一个成员变量写成一行。
  3. 注意事项:每个成员变量只能出现一次(即只能初始化一次),所以可以理解为:初始化列表是每个成员变量定义初始化的地方。
cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
    //初始化列表:
		:_year(year)
		, _month(month)
		, _day(day)//建议一个值写成一行
		//, _year(1);//error,只能初始化一次
	{ }
private:
	//声明:
	int _year;
	int _month;
	int _day;
};
  1. 只能在初始化列表进行初始化的成员变量:(必须在定义的地方即初始化列表进行初始化
  • const修饰的成员变量,原因:只能在它定义的地方(即:初始化列表)进行初始化,否则后续就不能修改了。
cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
		,_n(1)//right
	{
		//_n = 1;//error C2789: "Date::_n": 必须初始化常量限定类型的对象
	}
private:
	//声明:
	int _year;
	int _month;
	int _day;

	//1.const修饰的成员变量
	const int _n;
};
  • 引用成员变量,原因:引用必须要进行初始化,必须要在定义的地方进行初始化,必须要知道该引用是谁的别名。
cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int& m, int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
		//,_ref(m)//error C2530 : "Date::_ref" : 必须初始化引⽤
	{}
private:
	//声明:
	int _year;
	int _month;
	int _day;

	//2.引用成员变量
	int& _ref;
};
  • 没有默认构造的类类型变量,原因:如果有默认构造函数,那么初始化列表中可写可不写;如果没有默认构造函数,为了定义初始化,只能自己在初始化列表传入值。
cpp 复制代码
#include <iostream>
using namespace std;

class Time
{
public:

	//全缺省构造函数就是默认构造
	//Time(int hour = 0)
	//	:_hour(hour)
	//{
	//	cout << "Time()" << endl;
	//}

	Time(int hour)//error C2512: "Time": 没有合适的默认构造函数可用
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
		,_t(1)//没有默认构造的情况下,必须使用初始化列表,
        //否则 error C2512: "Time": 没有合适的默认构造函数可用
	{}
private:
	//声明:
	int _year;
	int _month;
	int _day;

	//3.没有默认构造的类类型对象
	Time _t;
};
int main()
{	
	Date d1(2024, 7, 14);
	return 0;
}

运行结果:

Time();

  1. 初始化列表和函数体可以进行混用。
cpp 复制代码
class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		, _day(day)
		,_ptr((int*)malloc(12))
	{
		if (_ptr == nullptr)
		{
			perror("malloc fail");
		}
		else
		{
			memset(_ptr, 0, 12);
		}
	}
private:
	//声明:
	int _year;
	int _month;
	int _day;

	//函数体和初始化列表可以进行混用
	int* _ptr;
}; 
  1. 有些变量可以不进行初始化。
cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		//, _day(day)//没有进行初始化
	{}
	void Print() const
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	//声明:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1(2024, 7, 14);
	d1.Print();

	return 0;
}

运行结果:

2024年7月-858993460日

  1. C++11的另类写法:声明成员变量时为它们赋缺省值/默认值,就可以用于初始化列表的初始化。(相当于初始化列表没写的成员,声明里面的缺省值会走到初始化列表里面去)
cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		, _month(month)
		//, _day(day)//没有显示在初始化列表
	{}
	void Print() const
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	//仍然是声明:每个成员变量赋有一个缺省值--是初始化时的备选之策
	int _year = 1;
	int _month = 1;
	int _day = 27;
};

int main()
{
	Date d1(2024, 7, 14);
	d1.Print();

	return 0;
}

运行结果:

2024年7月27日

  • 我们不写构造函数时,编译器会自动生成一个默认构造函数,在原来内置类型是不会进行初始化的,但是C++11给了缺省值,无论是我们自己写的默认构造还是自动生成的构造都会使用该缺省值进行初始化。
cpp 复制代码
#include <iostream>
using namespace std;

class Date
{
public:
	//Date(int year, int month, int day)
	//	:_year(year)
	//	, _month(month)
	//	//, _day(day)
	//{}
	void Print() const
	{
		cout << _year << "年" << _month << "月" << _day << "日" << endl;
	}
private:
	//声明:
	int _year = 1;
	int _month = 1;
	int _day = 27;
};

int main()
{
	Date d1;
	d1.Print();

	return 0;
}

运行结果:

1年1月27日

  • 如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会用这个成员类型的默认构造函数,如果没有默认构造会编译错误。

梳理总结:每个构造函数都有初始化列表。

Q:如果在函数形参的位置给了缺省值,那么声明位置还需要缺省值吗?

A:两者并无关联。

  1. 初始化顺序:初始化列表中按照成员变量在类中声明顺序进行初始化,与成员在初始化列表出现的先后顺序无关,建议声明顺序和初始化顺序保持一致。(声明顺序其实就是变量在内存中存放的顺序)(可以调试观察顺序、运行顺序)
  2. 例题练手:下面代码的运行结果:
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);//这里并没有开辟空间,函数创建的时候开辟了空间
    //A aa;
	aa.Print();
}

运行结果:

1 -858993460

构造函数传入缺省值,类创建的对象不传入缺省值,调试发现仍然是按照声明顺序进行初始化(没往列表走是为了方便观看)

二 、类型转换

隐式类型转换,不是任意类型都可以进行隐式类型转换的。

cpp 复制代码
//Date.cpp  
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class A
{
public:
	A(int a = 0)//提供默认构造函数
	//explicit A(int a = 0);
	{
		_a1 = a;
	}
	A(const A& aa)
	{
		_a1 = aa._a1;
	}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a1;
	int _a2;
};

class  Stack
{
public:
	void Push(const A& aa)
	{
		//......
	}
private:
	A _arr[10];
	int _top;
};

int main()
{
	A aa1(1);// 调用构造函数
	aa1.Print();

	//隐式类型转换:int类型转换成A类型
	//2 构造出 A的临时对象,再用临时对象去 拷贝构造 aa2
	//但是:编译器遇到连续构造+拷贝构造--->相当于直接优化为直接构造(这是优化后的结果)
	A aa2 = 2;//不是任意类型都可以支持隐式类型转换
	aa2.Print();

	A& raa1 = aa2;//raa1引用aa2: ok
	//A& raa2 = 2;  //raa2引用2: error
	//原因:类型转换,中间会生成临时变量,具有常性。
	const A& raa2 = 2;//right

	int i = 1;
	double d = i;//隐式类型转换,中间会产生临时变量
	//i给临时变量,临时变量给d
	const double& ra = i;//这点在某个地方讲过!!!!!!!!!!!翻看笔记
	
	//此设计的意义:

	Stack st;//实现一个栈
	A aa3(3);
	st.Push(aa3);//这两个数据插入的效果一样

	st.Push(3);//单参数支持,但是多参数就默认不支持这样写了了。


	//C++11就可以支持
	A aa5 = { 1,1 };
	const A& raa6 = { 2,2 };
	st.Push(aa5);
	st.Push({ 2,2 });


	return 0;
}
//如果不想发生类型转换,就可以在构造函数时就加入explicit

三、static成员函数

  1. 概念:用static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外面进行初始化。静态成员变量不能在声明处给缺省值,因为缺省值用于初始化列表。
  2. 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存放对象中,存放在静态区中。
cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:

private:
	// 类里面声明,不能给缺省值,因为它是会用于初始化列表的
	static int _scount;
};

// 类外面初始化
int A::_scount = 0;
//相当于是静态的全局,只是被类域限制,在类外访问时需要指定类域(公有时)

int main()
{
    cout << sizeof(A) << endl;//大小并没有包含静态成员
    
	return 0;
}

运行结果:

1

  1. 静态成员函数没有this指针
  2. 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为非静态的成员函数都有隐含的this指针,而静态成员函数没有。
  3. 非静态的成员函数可以访问任意的静态变量和静态成员函数。(访问非静态的只需要通过this指针;访问静态的只需要突破类域,可通过类名::静态成员或者对象.静态成员来访问静态成员变量、静态成员函数。)
cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
    A()//构造
	{
		++_scount;
	}
	A(const A& t)//拷贝构造
	{
		++_scount;
	}
	~A()//析构
	{
		--_scount;
	}
	static int GetACount()//静态成员函数,没有this指针,可以访问其他静态成员
	{
		return _scount;
	}
	void func()
	{
		cout << _scount << endl;//非静态成员函数访问静态成员变量
		cout << GetACount() << endl;//非静态成员函数访问静态成员函数
	}
private:
	// 类里面声明,不能给缺省值
	static int _scount;
};

// 类外面初始化
int A::_scount = 0;//虽然是全局却被类域限制,在类外时需要指定类域(公有时)
int main()
{
	cout << A::GetACount() << endl;//静态成员函数:可以指定类域调用

	A a1, a2;
	A a3(a1);

	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;

	return 0;
}
  1. 牛客例题描述:求<font style="color:rgb(102, 102, 102);">1+2+3+...+n</font>,要求不能使用乘除法、for、while、if......else、switch、case等关键字及条件判断语句(A ? B : C)。(A类与B类合作关系)
cpp 复制代码
//一个类调用一次构造,一个数组调用n此构造
class Sum
{
public:
	Sum()//构造函数
	{
		_ret += _i;
		++_i;//这里就实现了累加
	}
	static int GetRet()//提供静态成员函数,用于访问提取静态成员变量_i的结果。
	{
		return _ret;
	}

private:
	//在类里面声明
	static int _i;
	static int _ret;
};

//在类外面初始化
int Sum::_i = 1;
int Sum::_ret = 0;

class Solution//函数框架在这里
{
public:
	int Sum_Solution(int n) 
	{
		Sum a[n];//用数组定义n个对象,就可以调用n次构造,
		//数组用变量来定义:必须要求编译器支持C99变长数组

        //Sum* p = new Sum[n];//也可以这样进行(如果编译器不支持C99变长数组时)
        //总之:我们要调用n次Sum的构造函数,new了一个对象
        return Sum::GetRet();
	}
};

根据此题总结:静态成员函数特别好的用处就是进行封装。将常用的功能封装成静态成员函数,供所有类使用。

  1. 构造顺序:局部静态的生命周期是全局的
  2. 析构顺序:

四、友元

  • 友元分为友元类和友元函数。它提供了一种突破类访问限定符封装的方式。在函数声明或者类声明的前面加friend,并且把友元声明放在一个类里面。

4.1 友元函数

  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,它不是类的成员函数。
  • 需要在类外面访问类里面的私有成员时才设置友元。
  • 友元函数可以定义在类定义的任何地方声明,不受类访问限定符限制。(示好)
  • 一个函数可以是多个类的友元函数。(一个人可以是多个人的共同朋友)
  • 注意:友元声明用任何的类型和变量都要向上找,向上没有时进行前置声明,告诉有该东西存在。
cpp 复制代码
#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类的友元,只能说B是A和C的共同好友。(就像APP推荐的可能认识的人一样)
  • 友元会增加耦合度,破坏封装。类和类之间本就独立,友元破坏了这种独立。
cpp 复制代码
//你要想访问我,你就必须成为我的friend
#include<iostream>
using namespace std;
class A
{
	// 友元声明
	friend class B;
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
public:
	void func1(const A& aa)
	{
		cout << aa._a1 << endl;
		cout << _b1 << endl;
	}
	void func2(const A& aa)
	{
		cout << aa._a2 << endl;
		cout << _b2 << endl;
	}
private:
	int _b1 = 3;
	int _b2 = 4;
};
int main()
{
	A aa;
	B bb;
	bb.func1(aa);
	bb.func1(aa);
	return 0;
}

五、内部类(本质是封装)

  1. 概念:一个类定义在另一个类的内部,这种类就叫做内部类。
  2. 注意:
  • 内部类是一个独立的类,跟定义在全局相比,它只是受外部类的类域和访问限定符的限制,所以外部类定义的对象中不包含内部类。
  • 内部类默认是外部类对的友元。
  1. B是A的友元,B可以访问A的私有。
  2. 内部类本质上也是一种封装,A与B紧密关联,A类实现出来主要是给B类用,就考虑将A类设计成B类的内部类。如果将其放在private/protected位置,那么A就是B的专属内部类,其他地方用不了。(A类与B类从属)

对上面的牛客题改造(换成内部类):

cpp 复制代码
class Solution  
{
private:
	static int _i;
	static int _ret;
	class Sum
	{
	public:
		Sum()//构造函数
		{
			_ret += _i;
			++_i;//这里就实现了累加
		}
	};
public:
	int Sum_Solution(int n)
	{
		Sum a[n];

		//Sum* p = new Sum[n];
		//delete [] p;

		return _ret;
	}
};
int Solution::_i = 1;
int Solution::_ret = 0;

六、匿名对象

  1. 用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型 对象名(实参)定义出来的叫有名对象。
  2. 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
cpp 复制代码
#include <iostream>
using namespace std;
class A
{
public:
	A(int a = 0)//默认构造
		:_a(a)
	{
		cout << "A(int a)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a;
};
class Solution 
{
public:
	int Sum_Solution(int n) 
	{
		//...
		return n;
	}
};

int main()
{
	A aa1;//不传参数时这样定义,有名对象
	//A aa1();//error,无法区分是函数声明还是对象定义
	
	A();//不传参的匿名对象
	A(1);//传参的匿名对象

	//有名对象:
	Solution st;
	cout << st.Sum_Solution(10) << endl;

	//匿名对象--生命周期只在自己那一行代码中
	Solution().Sum_Solution(10);
	cout << Solution().Sum_Solution(10) << endl;

	return 0;
}

七、对象拷贝时的编译优化

  • 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一下传参和传返回值的过程中可以省略的拷贝。
  • 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。
cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	A(int a = 0)
		:_a1(a)
	{
		cout << "A(int a)" << endl;
	}
	A(const A& aa)
		:_a1(aa._a1)
	{
		cout << "A(const A& aa)" << endl;
	}
	A& operator=(const A& aa)
	{
		cout << "A& operator=(const A& aa)" << endl;
		if (this != &aa)
		{

				_a1 = aa._a1;
		}
		return *this;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int _a1 = 1;
};
void f1(A aa)
{}
A f2()
{
	A aa;
	return aa;
}
int main()
{
	// 传值传参
	A aa1;
	f1(aa1);
	cout << endl;
	// 隐式类型,连续构造+拷贝构造->优化为直接构造
	f1(1);
	// 一个表达式中,连续构造+拷贝构造->优化为一个构造
	f1(A(2));
	cout << endl;
	cout << "***********************************************" << endl;
	// 传值返回
	// 返回时一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 (vs2019)
	// 一些编译器会优化得更厉害,进行跨行合并优化,直接变为构造。(vs2022)
	f2();
	cout << endl;
	// 返回时一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造 (vs2019)
	// 一些编译器会优化得更厉害,进行跨行合并优化,直接变为构造。(vs2022)
		A aa2 = f2();
	cout << endl;
	// 一个表达式中,连续拷贝构造+赋值重载->无法优化
	aa1 = f2();
	cout << endl;
	return 0;
}

喜欢的uu记得三连支持一下哦!

相关推荐
arong_xu8 分钟前
现代C++锁介绍
c++·多线程·mutex
汤姆和杰瑞在瑞士吃糯米粑粑13 分钟前
【C++学习篇】AVL树
开发语言·c++·学习
Biomamba生信基地20 分钟前
R语言基础| 功效分析
开发语言·python·r语言·医药
DARLING Zero two♡21 分钟前
【优选算法】Pointer-Slice:双指针的算法切片(下)
java·数据结构·c++·算法·leetcode
手可摘星河22 分钟前
php中 cli和cgi的区别
开发语言·php
虾球xz25 分钟前
游戏引擎学习第58天
学习·游戏引擎
LuH112435 分钟前
【论文阅读笔记】Scalable, Detailed and Mask-Free Universal Photometric Stereo
论文阅读·笔记
CodeClimb35 分钟前
【华为OD-E卷-木板 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
CT随1 小时前
Redis内存碎片详解
java·开发语言
anlog1 小时前
C#在自定义事件里传递数据
开发语言·c#·自定义事件