类和对象(下)

目录

1. 再探构造函数

2. 类型转换

3. static成员

4. 友元

5. 内部类

6. 匿名对象

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


1. 再探构造函数

  • 之前我们实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一个方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是一个以逗号分割的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
  • 每个成员变量在初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
  • 引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置 进行初始化,否则会编译报错。
  • C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
  • 尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明位置给可缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++中并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造函数,如果没有默认构造会编译错误。
  • 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
//初始化列表的格式
Date(int year = 1, int month = 1, int day = 1)
	:_year(year) //后面的括号中放初始化的值,里面也可以是表达式
	, _month(month)
	, _day(day)
{

}

有三类情况必须使用初始化列表初始化:

  1. 没有默认构造的类类型的成员变量
  2.  const成员变量
  3. 引用成员变量

没有默认构造的类类型的成员变量

class MyQueue
{
public:
	//编译器默认生成MyQueue的构造函数调用了Stack的默认构造,
	//完成了两个成员的初始化
	//如果栈没有默认构造,那么就得显示的调用它的构造
	//MyQueue()
	//{
	//	_pushst(1);//这样调是不支持的
	//}
	/*****************************************************/
	//MyQueue()//这个时候就必须使用初始化列表初始化的调用了
	//	:_pushst(10)
	//	,_popst(10)
	//{

	//}
	/*****************************************************/
	//如果想控制栈的大小,就可以加个参数
	MyQueue(int n = 10)
		:_pushst(n)
		, _popst(n)
	{

	}
private:
	Stack _pushst;
	Stack _popst;
};


//这个栈没有默认构造函数,所以我们必须在初始化列表中初始化。

 const成员变量和应用成员变量

class MyQueue
{
public:
	//编译器默认生成MyQueue的构造函数调用了Stack的默认构造,
	//完成了两个成员的初始化
	//如果栈没有默认构造,那么就得显示的调用它的构造
	//MyQueue()
	//{
	//	_pushst(1);//这样调是不支持的
	//}
	/*****************************************************/
	//MyQueue()//这个时候就必须使用初始化列表初始化的调用了
	//	:_pushst(10)
	//	,_popst(10)
	//{

	//}
	/*****************************************************/
	//如果想控制栈的大小,就可以加个参数
	MyQueue(int& r,int n = 10)
		:_pushst(n)
		,_popst(n)
		,_y(1)
		,_z(r)
	{
		_x = 1;
	}
private:
	Stack _pushst;
	Stack _popst;
	int _x;
	const int _y;
	int& _z;
};

 但是const成员变量和引用成员变量必须在初始化列表初始化,不能再函数体内初始化,否则编译器会报错。

 每个成员变量再初始化列表中只能出现一次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方,所以也就衍生出这个三个成员变量必须在初始化列表初始化,那么凭什么这三个成员变量必须在初始化列表初始化呢?

  • 引用必须在定义的时候初始化,引用和指针的区别就是引用在定义的时候必须给值,没有空引用这个概念,也没有野引用这样的概念,因为引用必须在定义的时候初始化。
  • const也是,const变量是不能修改的,它只有一次修改的机会,就是在初始化的时候,const变量在定义的时候也是必须初始化的。
  • 没有默认构造的话要调用构造的话必须传参,那就必须得初始化,那初始化就得传参,传参就只能在初始化列表中传参,如果放在函数体的话,在函数体中每个成员是可以出现多次的,那么哪一次是它的初始化呢?所以C++就规定了初始化列表。初始化列表是每个成员变量定义的地方,引用成员变量和const成员变量初始化的时候必须给值,所以要放在初始化列表中初始化。
#include<iostream>
using namespace std;
class Time
{
public:
	Time(int hour)
		:_hour(hour)
	{
		cout << "Time()" << endl;
	}
private:
	int _hour;
};
class Date
{
public:
	Date(int& x, int year = 1, int month = 1, int day = 1)
		:_year(year)
		,_month(month)
		,_day(day)
		,_t(12)
		,_ref(x)
		,_n(1)
	{
		// error C2512: “Time”: 没有合适的默认构造函数可用
		// error C2530 : “Date::_ref” :必须初始化引用
		// error C2789 : “Date::_n” : 必须初始化常量限定类型的对象

	}
	void Print() const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;//没有默认构造
	int& _ref;//引用
	const int _n;// const 
};
int main()
{
	int i = 0;
	Date d1(i);
	d1.Print();
	return 0;
}

尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表。

 

所以C++11给了一个补充语法, 支持在成员变量声明的位置给缺省值。

 

初始化列表总结:

无论是否显示写初始化列表,每个构造函数都有初始化列表。

无论是否在初始化列表显示初始化,每个成员变量都要走初始化列表初始化。

 构造函数初始化成员,尽量使用初始化列表初始化,那么有的使用了初始化列表初始化再使用函数体,就像我们的日期有效性的检测。

Date::Date(int year, int month, int day)
	:_year(year)
	,_month(month)
	,_day(day)
{
	if (!CheckDate())
	{
		cout << "请输入合法的日期!!!->";
		cout << *this;
	}
}

 判断日期有效性的这个if语句没办法放在初始化列表,那么就需要放在函数体内了。

显示初始化,缺省值和默认构造优先使用显示初始化,其次使用缺省值,最后才会用到默认构造,如果默认构造都没有就会编译报错。

初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。

针对这个知识点,下面有一道题: 

#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);
	aa.Print();
}

这段程序的结果是什么?

_a1的结果是1,_a2的结果是随机值,这个时候就与缺省值没有关系了,因为两个成员变量都在初始化列表中初始化了,又因为初始化列表中按照成员变量在类中声明顺序进行初始化,这个时候_a2先声明的,所以在初始化列表中_a2先执行,这个时候_a1没初始化,所以是随机值,传给_a2也是随机值,然后再执行_a1,因为定义变量aa的时候传的是1,所以这里的_a1是1。

2. 类型转换

  • C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
  • 构造函数前面加explicit就不再支持隐式类型转换。
  • 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
#include<iostream>
using namespace std;
class A
{
public:
	// 构造函数explicit就不再⽀持隐式类型转换
	// explicit A(int a1)
	A(int a1)
		:_a1(a1)
	{
        cout << " A(int a1) " << endl;
    }

    //拷贝构造
    A(const A& aa)
        :_a1(aa._a1)
        ,_a2(aa._a2)
    {
        cout << " A(const A& aa) " << endl;
    }
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
    int Get() const
    {
        return _a1 + _a2;
    }
private:
    int _a1 = 1;
    int _a2 = 2;
};
int main()
{
    // 1构造⼀个A的临时对象,再⽤这个临时对象拷贝构造aa3
    // 编译器遇到连续构造+拷贝构造->优化为直接构造
    A aa1 = 1;
    aa1.Print();
    return 0;
}
/*
这里的1是可以给aa1的,本质就是支持隐式类型转换,因为1是整型,有构造
的支持,1这个整型可以构造一个a,所以1先构造了一个A的临时对象。
临时对象:编译器开了一块空间进行存储,但是这块空间没有名字,这叫临时对象(匿名对象)
然后再用这个地方的临时对象拷贝构造,但是编译器在这里不会走临时对象
然后再走拷贝构造,因为编译器会在这优化,因为用1构造了一个临时对象,
类型转换中间会产生临时对象,用1构造临时对象,再用临时对象拷贝构造,
编译器觉得构造,再拷贝构造太浪费了,所以编译器会把这种连续的构造直接
优化成构造,合二为一了,所以打印结果就是直接调用构造,步调拷贝构造

而且初始化列表中只初始化了_a,没有初始化_a2,所以_a2使用的是缺省值
*/

上面代码我们可以体会到编译器遇到连续构造就优化为直接构造了,那么有的情况是没办法优化的。

#include<iostream>
using namespace std;
class A
{
public:
	// 构造函数explicit就不再⽀持隐式类型转换
	// explicit A(int a1)
	A(int a1)
		:_a1(a1)
	{
        cout << " A(int a1) " << endl;
    }

    //拷贝构造
    A(const A& aa)
        :_a1(aa._a1)
        ,_a2(aa._a2)
    {
        cout << " A(const A& aa) " << endl;
    }
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
    int Get() const
    {
        return _a1 + _a2;
    }
private:
    int _a1 = 1;
    int _a2 = 2;
};
int main()
{
    // 1构造⼀个A的临时对象,再⽤这个临时对象拷贝构造aa3
    // 编译器遇到连续构造+拷贝构造->优化为直接构造
    A aa1 = 1;
    aa1.Print();

    const A& aa2 = 1;
    //这里的引用,不是引用的1,而是引用的临时对象,但是这只有一个构造,没有
    //拷贝构造,是不会优化的,这种临时对象具有常性,就像被const修饰一样,所以
    //前面要加上const
    return 0;
}
/*
int i = 10;
double d = i;
//double& rd = i;//err,报错原因不是因为类型不同,因为int给double中间会产生临时对象
const double& rd = i;//临时对象具有常性,所以前面加上double就可以了
*/

C++中还有一个关键字,是explicit,如果不想让前面的隐式类型转换发生,可以在构造函数前面加上explicit。

// 构造函数explicit就不再⽀持隐式类型转换
// explicit A(int a1)
explicit A(int a1)//加上explicit之后这里的整型就不能转换成a这个类型了
	:_a1(a1)
{
    cout << " A(int a1) " << endl;
}

前面的代码中是一种单参数的构造函数,那么具有多参数的构造函数我们应该怎么使用呢?

#include<iostream>
using namespace std;
class A
{
public:
	// 构造函数explicit就不再⽀持隐式类型转换
	// explicit A(int a1)
    A(int a1)//加上explicit之后这里的整型就不能转换成a这个类型了
		:_a1(a1)
	{
        cout << " A(int a1) " << endl;
    }
	//explicit A(int a1, int a2)
	A(int a1, int a2)//多参数的构造函数
		:_a1(a1)
		, _a2(a2)
	{}

    //拷贝构造
    A(const A& aa)
        :_a1(aa._a1)
        ,_a2(aa._a2)
    {
        cout << " A(const A& aa) " << endl;
    }
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
    int Get() const
    {
        return _a1 + _a2;
    }
private:
    int _a1 = 1;
    int _a2 = 2;
};
int main()
{
    //C++11之后才支持多参数转化
    //A aa3 = { 2,2 };//这样写调用的是多参数的
    A aa3 = (2, 2);//这样写本质还是调的单参数的,因为里面是逗号表达式
    const A& aa4 = { 1,1 };//引用的是临时对象,随意要加const
    return 0;
}

隐式类型转换例子:

//有一个栈,这个栈存储的数据是A类型的
class Stack
{
public:
    void Push(const A& a)//加引用,形参传给实参的话就没有拷贝了
    {}
//........
};

int main()
{
    //用栈存储A类型的数据
    //方式1 - 定义一个对象,在用这个对象去传参
    Stack st;
    A _a1(1);
    st.Push(_a1);

    A _a2{ 1, 2 };
    st.Push(_a2);
    //方式2 - 有隐式类型转换的支持,写起来就很方便
    st.Push(3);
    st.Push({ 1, 2 });
    //隐式类型转换,C++支持这个东西本质是为了A这种自定义类型要传参等操作
    //的话就会更方便,不用定义对象,在push这个对象了。

    return 0;
}

两个类型之间有一定的关联才能互相转,算术类型之间能互相转是因为它们都表示数据大小,指针和整型之间能互相转是因为指针本质是地址的编号,也是表示数据大小的,所以也可以转,其他类型基本上就不可以转了。C++中内置类型和自定义类型之间想转一定要借助构造函数,如果有对应的内置类型和自定义类型,用内置类型去构造自定义类型的这种构造函数就可以转。

上面通过这么多代码我们了解的是内置类型到->自定义类型的转换,那么自定义类型->自定义类型的转换其实也是可以转换的,只要跟自定义类型扯上关系,想转换就一定要借助构造函数。

#include<iostream>
using namespace std;
class A
{
public:
	// 构造函数explicit就不再⽀持隐式类型转换
	// explicit A(int a1)
    A(int a1)//加上explicit之后这里的整型就不能转换成a这个类型了
		:_a1(a1)
	{
        cout << " A(int a1) " << endl;
    }
	//explicit A(int a1, int a2)
	A(int a1, int a2)//多参数的构造函数
		:_a1(a1)
		, _a2(a2)
	{}

    //拷贝构造
    A(const A& aa)
        :_a1(aa._a1)
        ,_a2(aa._a2)
    {
        cout << " A(const A& aa) " << endl;
    }
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
    int Get() const
    {
        return _a1 + _a2;
    }
private:
    int _a1 = 1;
    int _a2 = 2;
};
class B
{
public:
    
private:
    int _b = 0;
};
int main()
{

    A aa3 = { 2,2 };
    B b = aa3;
    const B& rb = aa3;
    return 0;
}

那么A这个类型能不能转换成B呢?

默认情况下是不可以的。 除非支持用A构造B的构造函数。

class B
{
public:
    B(const A& a)
        :_b(a.Get())
    {}
private:
    int _b = 0;
};

这样就可以转了,可以转的逻辑就是这个aa3会构造生成一个临时对象,这个临时对象再去拷贝构造,但是编译器优化了以后就直接构造了。

//aa3隐式类型转换为b对象原理跟上面类似
B b = aa3;
const B& rb = aa3;

那么也就支持上面这种写法,原理上跟前面类似,引用的不是aa3,引用的是aa3构造的临时对象。

3. static成员

  • 用static修饰的成员变量,称之为静态成员变量,静态成员变量一定要在类外进行初始化。
  • 静态成员变量为所有类对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
  • 用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
  • 静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。
  • 非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
  • 突破类域就可以访问静态成员,可以通过类名::静态成员或者对象.静态成员来访问静态成员变量和静态成员函数。
  • 静态成员也是类的成员,受public、protected、private访问限定符的限制。
  • 静态成员变量不能在声明位置给缺省值初始化,因为缺省值是给构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
// 实现⼀个类,计算程序中创建出了多少个类对象?
#include<iostream>
using namespace std;
class A
{
public:
	A()
	{
		++_scount;
	}
	A(const A& t)
	{
		++_scount;
	}
	~A()//如果相要看占用内存的对象,那么在析构中--_scount就可以了
	{
		--_scount;
	}
	static int GetACount()//如果_scount是私有的,那么就得写个成员函数返回_scount
	{
		return _scount;
		//_i = 1;//非静态的不能访问
	}
private:
	//类里面声明
	static int _scount;
	//static int _scount = 3;
	//不能给缺省值,因为缺省值是给初始化列表的,静态的成员变量
	//不会走初始化列表

	int _i = 0;//非静态成员变量
};
//类外面初始化
int A::_scount = 0;
void Func(A aa)
{}
void Fxx()
{
	//A aa3;//创建这个对象是专门为了调用GetACount函数而创建的,所有打印的时候必须-1。
	//cout << aa3.GetACount() - 1 << endl;
	//那么也可以不创建对象调用 - 那么就得用到static修饰
	//静态的不能方位非静态的,非静态的可以访问静态的
	cout << A::GetACount() << endl;
}
int main()
{
	cout << A::GetACount() << endl;
	A a1, a2;
	A a3(a1);
	Func(a1);
	cout << A::GetACount() << endl;
	cout << a1.GetACount() << endl;
	Fxx();
	//编译报错:error C2248 : “A::_scount” :无法访问private成员(在"A"类中声明)
	//突破类域访问的两种方式
	//cout << A::_scount << endl;
	//cout << a1._scount << endl;
	return 0;
}

习题1:求1+2+3+...+n_牛客题霸_牛客网求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、swit。题目来自【牛客题霸】icon-default.png?t=O83Ahttps://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

#include <regex>
class Sum
{
    public:
    Sum()
    {
        _ret += _i;
        _i++;
    }
    static int Getret()
    {
        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 sum[n];
        return Sum::Getret();
    }
};

这种代码在vs编译器上是跑不过去的,因为vs编译器不支持变长数组。

如果vs上实在想跑的话就可以使用new。 

#include <iostream>
using namespace std;
class Sum
{
public:
    Sum()
    {
        _ret += _i;
        _i++;
    }
    static int Getret()
    {
        return _ret;
    }
private:
    static int _i;
    static int _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;

int main()
{
    int n = 0;
    cin >> n;
    Sum* arr = new Sum[n];
    //new这个东西其实就是构造一个n个数的数组,只是动态
    //开辟,它会调用10次构造函数。
    cout << Sum::Getret() << endl;
    //释放
    //.....
    return 0;
}

习题2:

设已经有A,B,C,D 4个类的定义,程序中A,B,C,D构造函数调用顺序是什么?

设已经有A,B,C,D 4个类的定义,程序中A,B,C,D析构函数调用顺序是什么?

#include <iostream>
using namespace std;
class A
{
public:
	A()
	{
		cout << "A()" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
};

class B
{
public:
	B()
	{
		cout << "B()" << endl;
	}
	~B()
	{
		cout << "~B()" << endl;
	}
};
class C
{
public:
	C()
	{
		cout << "C()" << endl;
	}
	~C()
	{
		cout << "~C()" << endl;
	}
};
class D
{
public:
	D()
	{
		cout << "D()" << endl;
	}
	~D()
	{
		cout << "~D()" << endl;
	}
};

C c;
int main()
{
	A a;
	B b;
	static D d;
	return 0;
}

 运行结果:

构造执行的顺序是C,A,B,D。析构执行的顺序是B,A,D,C 。

  • 构造的时候是先走C,因为它是全局的,全局的在main函数之前就会执行,那么局部静态的d第一次走到d的时候初始化,不像全局的在main函数之前初始化。
  • 析构的话是后定义的先析构,因为c,d它的声明周期是全局的,声明周期是全局的就说明它在main函数结束以后才会析构,所有main函数结束的时候一定是先析构局部的,也就是说先析构a,b。a,b中b先析构,因为b是后定义的,后定义的先析构,先析构b,再析构a,全局的和局部的静态的一定是局部的静态的先析构,所有接着析构d,最后析构c。

4. 友元

  • 友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加上friend,并且把友元声明放到一个类的里面。
  • 外部友元函数可访问类的私有和保护成员,友元函数仅仅是一种声明,它不是类的成员函数。
  • 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
  • 一个函数可以是多个类的友元函数。
  • 友元类中的成员函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
  • 友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
  • 友元关系不能传递,如果A是B的友元,B是C的友元,但是A不是C的友元。
  • 有时提供了便利。但是友元会增加耦合度(耦合度就是两个类的关系,关系越紧密,耦合度越高,耦合度太高了不好),破坏了封装,所以友元不宜多用。
#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;
}

5. 内部类

  • 如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,他只是受外部类类域限制和访问限定符限制,所以外部类定义的对象中不包含内部类。
  • 内部类默认是外部类的友元类。
  • 内部类本质也是一种封装,当Aa类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。

C++中内部类用的不是很多。

#include<iostream>
using namespace std;
class A
{
private:
    static int _k;
    int _h = 1;
public:
    class B //B默认就是A的友元
    {
    public:
        void foo(const A& a)
        {
            cout << _k << endl;//因为B是A的友元,所以可以访问私有的变量
            cout << a._h << endl;//OK
        }
    private:
        int _b;
    };
};
int A::_k = 1;
int main()
{
    cout << sizeof(A) << endl;
    /*
    sizeof(A)的大小是4个字节,首先_k这个静态成员变量肯定
    不用计算,因为静态成员变量没有存储在对象中,单独存储在
    静态区。
    B类里面的_b这个整型也不需要计算,这说明A对象里面没有
    这个B对象,内部类并不是说B变成A的成员,A是A,B是B,B定义
    在A的里面只是说B这个类受到A这个类的类域的限制,B的成员
    并不会变成A的一部分m。
    */

    //B b;//err
    A::B b;//访问B这个类不能直接访问,受类域的限制,得指定类域
    //还受到访问限定符的限制,如果改成私有,就访问不了了。

    A aa;
    b.foo(aa);
    return 0;
}

 求1+2+3+...+n_牛客题霸_牛客网求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、swit。题目来自【牛客题霸】icon-default.png?t=O83Ahttps://www.nowcoder.com/practice/7a0da8fc483247ff8800059e12d7caf1?tpId=13&tqId=11200&tPage=3&rp=3&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking

那么这道题就可以使用内部类的方式去实现,用内部类比较合理一些。 

#include <regex>
class Solution {
    class Sum
    {
        public:
        Sum()
        {
            _ret += _i;
            _i++;
        }
    };
    static int _i;
    static int _ret;
public:
    int Sum_Solution(int n) {
        Sum* arr = new Sum[n];
        return _ret;
    }
};
int Solution::_i = 1;
int Solution::_ret = 0;

//把Sum这个类变成Solution的专属类

6. 匿名对象

  • 用类型(实参)定义出来的对象叫做匿名对象,相比之前我们定义的类型对象名(实参)定义出来的叫有名对象。
  • 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
  • 匿名对象和临时对象有点像,就比如类型转换,传值返回会产生临时对象,临时对象是编译器自己找一块空间生成的,这块空间没有名字。
#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;
    }
};
//自定义类型给缺省参数比较好的写法就是给匿名对象
void func(A aa = A(1))
{ }
int main()
{
    A aa1(1);//有名对象 - 有名字的对象
    A aa2;

    A(1);//匿名对象
    A();
    /*
    匿名对象的特点就是相比有名对象的生命周期短,
    有名对象的生命周期在当前的块作用域,aa1和aa2
    的析构是在main函数结束以后。
    匿名对象的特点就是他的生命周期在当前这一行
    其次需要注意的就是有名对象有参数的时候后面就
    加括号,传参,没有参数的时候不需要加括号,但是
    匿名对象的话没有参数不可能只写个类型,所以匿名
    没有参数的时候后面还得加括号。
    */

    //匿名对象的使用场景
    //之前使用有名对象调用Sum_Solution函数的话得写成两行
    Solution s1;
    cout << s1.Sum_Solution(10) << endl;
    //使用匿名对象调用Sum_Solution函数
    cout << Solution().Sum_Solution(10) << endl;

    func();

    //匿名对象是可以引用的,匿名对象跟临时对象一样,具有常性
    //所以要加const,但是加上const有一个不一样的就是const
    //引用会延长匿名对象的生命周期,其实临时对象也会被延长。
    //具体延长的啥时候就跟着这个引用走。
    // 如果不延长生命周期的话,该引用就会变成野引用。
    const A& r = A();
    return 0;
}

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

  • 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
  • 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨表达式的合并优化。 
#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)//这里是故意写的传值传参,用引用的话就不会调用拷贝构造
{}
int main()
{
    //优化
    A aa1 = 1;
    cout << endl;
    /*
    内置类型要想转换成自定义类型就必须依靠构造函数
    语法上是用1去构造一个A的临时对象,再用临时对象去拷贝构造,
    但是从结果上来说我们看到的并不是构造+拷贝构造,而是直接构造
    因为编译器优化了,构造+拷贝构造编译器觉得太浪费了,直接构造也
    不会有其他的问题。优化的前提是跟不优化达到的效果必须是一样的。
    */
    
    //传值传参优化
    A aa2;
    f1(aa2);
    cout << endl;
    /*
    先调用构造函数,传参的时候调用拷贝构造,然后就析构,析构
    的是aa,aa是属于形参,f1函数调用结束以后,aa就销毁了,这个
    位置暂且没有优化
    */

    //隐式类型、连续构造+拷贝构造->优化为直接构造
    f1(1);
    /*
    本来的话这里要用1去构造临时对象A,然后再用临时对象去拷贝
    构造aa。优化成了直接构造
    */

    //一个表达式中,连续构造+拷贝构造->优化为一个构造。
    f1(A(2));
    cout << endl;
    /*
    语法的语义上面构造一个匿名对象,再用这个匿名对象传值
    传参给函数,这是拷贝构造,这里也是构造+拷贝构造,但是实际
    还是直接构造,编译器优化为直接构造。
    */

    cout << "###############################" << endl;

    return 0;
}

#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()//传值返回还要调用拷贝构造,这里不能引用返回,因为aa是局部对象
{
    A aa;
    return aa;
}
int main()
{
    f2();
    /*
    这里返回不能返回aa,因为aa是局部对象,返回的话就成
    野引用了,所以这个时候中间会生成一个临时对象,这个
    临时对象作为返回值,先构造,再拷贝构造,这个拷贝构造
    就是去生成临时对象,(vs2019上不做优化)
    */
    cout << endl;

    A aa2 = f2();
    cout << endl;


    return 0;
}

那么这个优化具体是怎么优化的呢。(vs2019的Debug版本优化)

语法规定传值返回必须产生临时对象,那么我们可以通过Linux环境下,g++编译器观察不优化的情况。 

关闭构造优化: g++ main.cpp -fno-elide-constructors

 

A aa1 = 1;
//构造+拷贝构造 -> 优化为直接构造

A aa2 = f2();
//拷贝构造+拷贝构造 -> 优化成一次拷贝构造

 vs2022Debug版本优化。

#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 = 1;

    f2();
    cout << endl;

    A aa2 = f2();
    cout << endl;

    aa1 = f2();
    cout << endl;

    return 0;
}

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值