1. 再谈构造函数(构造函数的2个深入使用技巧)
1.1 构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称为对对象中成员变量的初始化。
【注意】
- 构造函数体中的语句只能将其称为赋初值,而不能称作初始化。
- 因为初始化只能初始化一次,而构造函数体内可以多次赋值。
1.2 初始化列表
引入:栈类、两个栈实现的队列。
假设栈不提供默认构造,只有需要传参的构造。
报错:数据成员popst不具备默认构造。
当Stack不提供默认构造——即显式写了一个带参的非全缺省构造。MyQueue也就无法使用默认构造。这个时候就需要在MyQueue里面显式地写构造函数。在这个构造里显式调用自定义类型的非默认构造。
问:这个构造函数应该怎么写???自定义类型的调用构造函数怎么传参???
答:带初始化列表的构造函数。
初始化列表 :以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:Date(int year, int month, int day): _year(year), _month(month), _day(day){}private:int _year;int _month;int _day;
};
c++中引入初始化列表,规则:冒号开始,逗号分割,赋值用括号——哪怕是内置类型,也不用赋值符号=,用括号。
作用:自定义类型成员不具备默认构造,是带参的构造,就可以在初始化列表里显式地传给带参构造。
初始化列表本质可以理解为,对象中每个成员变量定义的地方——在main函数中定义一个对象(对象整体定义),程序走过这一句,会调用它的构造函数,构造函数的初始化列表就是成员定义的地方(对象内每个成员自己定义的地方)。
在本例中:
初始化列表建议一个成员变量一行,代码更整洁,全写到一行太拥挤。
有了显式实现的构造函数,就不需要声明缺省值了,而且引用不太好给缺省参数,那为了整齐,可以选择都不用给声明缺省值。
【初始化列表】
- 位置:在函数声明和函数体之间;
- 规则:冒号开始,逗号分割,括号赋值;
- 规定:一个成员只能有一个,最多有多少个成员就写多少个,所有的成员都可以在初始化列表进行初始化;
- 一般类型成员可以不用一定要在初始化列表进行初始化,也可以在函数体内初始化;
- 有三类成员必须在初始化列表进行初始化:
- ①引用类型的成员
- ②const成员
——这两个都必须在定义的时候初始化,初始化列表就是成员定义的地方。
(函数体内只能赋值)- ③没有默认构造的自定义类型成员,只能显式地(传参)调用构造,构造函数在对象定义的时候被“隐式/显式”调用。
- 注意:
- const成员可以给缺省值,就不需要在初始化列表写出来了。
- 引用可以给缺省值,就不需要在初始化列表写出来了。
- const变量的特点:必须在定义时初始化,因为只有一次初始化的机会,而成员变量都是在构造函数的初始化列表被定义。
- 引用的特点:必须在定义时初始化(不能先定义一个别名,但是不知道是谁的别名)
这就需要找到每个成员变量定义的地方——因为有些成员变量要在定义的时候做一些处理。
C++祖师爷就找到初始化列表这么个地方。
【注意】
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用类型的成员变量(必须初始化)
- const成员变量(必须初始化)
- 自定义类型成员(且该类没有默认构造函数时)
——就之前学习的知识而言,有些情况我们还是无法完成初始化(因为真正的初始化在初始化列表)
3. 尽量使用初始化列表初始化——因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。
初始化列表,不管你有没有显式地写出来,每个成员变量都会先走一遍。
(没写就会自动生成初始化列表,自动走)
C++难学的一个原因就是编译器帮你做了太多看不见的事。
调试观察构造函数的执行流程:
编译器自动生成的初始化列表,会
- 对自定义类型的成员:调用默认构造——也只能调用默认构造。
(没有默认构造就编译报错)- 对内置类型的成员:有缺省值用缺省值,没有的话,值就不确定了——要看编译器,有的编译器会处理,有的不会处理。
构造函数:先走初始化列表 + 再走函数体
实践中尽可能使用初始化列表初始化——因为不管你是不是使用初始化列表初始化,都一定会先走一遍初始化列表。
初始化列表不方便时,再使用函数体完成初始化(赋值)。
4. 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
调试观察构造函数的执行流程:
class MyQueue
{
public:MyQueue():_size(1) //显式地写出来,就不会走缺省值了, _pushst(10 + 1) //显式地写出来,调用构造时就自主传参(就不使用默认构造的缺省参数值), _ptr((int*)malloc(40))//初始化列表的括号里的初始化内容比较自由,就像初始化(赋值)的=的右边一样,符合类型对应就行{//初始化列表不方便调用memset把40个字节的空间都初始化成0,就可以在函数体内完成memset(_ptr, 0, 40);//memset(_ptr, 1, 40); _ptr观察到16843009——0x 0101 0101}private:// 声明Stack _pushst;Stack _popst;// C++11的补丁:缺省值——给初始化列表用的——初始化列表显式写了,就不会用了int _size = 0;//const成员可以给缺省值,就不需要在初始化列表写出来了//引用可以给缺省值,就不需要在初始化列表写出来了const int _x = 10;int& ref = _size;int* _ptr;
};int main()
{MyQueue q;return 0;
}
例题:
程序崩溃:访问野指针、空指针、数组越界访问、……
编译不通过:语法错误。
- 初始化列表初始化的顺序,是成员变量声明的顺序,不是初始化列表中成员变量出现的顺序。
对象模型(对象在内存中是怎么存的):对象在内存中是按成员变量声明的顺序,进行存储的;初始化的时候也是按照这个顺序,依次往后进行初始化的。
可以调试-内存观察对象在内存中的存储。
aa的地址就是_a2的地址,往后4字节就是_a1的地址
建议:初始化列表中的出现顺序尽量和声明顺序保持一致——避免上述例题中出现的问题。
1.3 explicit关键字
1.3.1 单参构造的类型转换调用
1.3.1.1 概念
第3个是隐式类型转换:内置类型转换成自定义类型。
由构造函数可知,A类的对象支持仅使用一个int类型去构造。
正是因为类型转换会产生临时对象,3才能赋值给到A类型的对象。
还有就是因为A类型支持用一个int类型的参数去构造——单参数的构造支持的内置类型转换成自定义类型。
图解说明:
临时对象具有常性:
报错原因不是因为跨类型了。引用的是3构造的临时对象。这里没有拷贝构造。
- 编译器遇到连续的“构造+拷贝构造”->会优化为直接构造。
构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。
接收单个参数的构造函数具体表现:
- 构造函数只有一个参数。
- 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值。
- 全缺省构造函数。
class A
{
public://单参数构造函数//explicit A(int a) 不允许隐式类型转换A(int a):_a(a){cout << "A(int a)" << endl;}//多参数构造函数——也支持隐式类型转换,即使用{ }A(int a1, int a2):_a(0),_a1(a1),_a2(a2){}//显式写拷贝构造A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}private:int _a;int _a1;int _a2;
};int main()
{// 隐式类型转换// 内置类型转换为自定义类型// A aa4 = 3.3;——3.3先转为int(警告:double转为int可能丢失数据)// A aa5 = 'a';——'a'先转为int// A aa6 = "abc";——报错:字符串不能隐式类型转换// raa 引用的是类型转换中用3构造的临时对象 —— 所以raa不能直接引用3,即A& raa = 3;,而是需要加一个constconst A& raa = 3;//正常调用多参数的构造A aaa1(1, 2);//赋值调用多参数的构造//1.不能直接 A aaa2 = 1, 2;//2.也不能 A aaa2 = (1, 2);——这个通过了。//因为调用了单参数的构造——小括号内理解为逗号表达式,值为2//C++11支持A aaa2 = { 1, 2 };(C++98不支持)A aaa2 = { 1, 2 };//理解为先用{1, 2}去构造,再拷贝构造//这里也不能直接引用,要加上constconst A& aaa3 = { 1, 2 };//——C++11之后还允许:// A aaa2{ 1, 2 };// const A& aaa3 { 1, 2 };// 即把赋值 = 省略掉,但是不建议,因为这种用法四不像return 0;
}
1.3.1.2 应用
来看栈类型。
改进就是引用传参,并且加上const,避免引用传参导致的改变形参直接影响实参。
同时也只有加上const,才能接收用int构造的临时对象。
单参构造的隐式类型转换实用性很强:
来看缺省值这里的应用——缺省值的4种给法。
class BB
{
public:BB(){}private:// 声明缺省值——给初始化列表使用// 缺省值的给法// 1.缺省值可以直接给值int _b1 = 1;// 2.缺省值可以mallacint* _ptr = (int*)malloc(40);// 3.缺省值可以隐式类型转换Stack _pushst = 10;A _a1 = 1;A _a2 = { 1,2 };// 4.缺省值可以给成员变量、全局变量A _a3 = _a2;
};
//相当于初始化列表
//BB()
// :_b1(1)
// ,_ptr((int*)malloc(40));
// ,_pushst(10);
// ,_a1(1);
// ,_a2({ 1,2 });
// ,_a3(_a2);
// {}
int main()
{BB bb;return 0;
}
1.3.2 多参构造的类型转换调用
1.3.2.1 概念
C++11支持多参构造的类型转换调用。
class A
{
public://单参数构造函数//explicit A(int a) 不允许隐式类型转换A(int a):_a(a){cout << "A(int a)" << endl;}//多参数构造函数——也支持隐式类型转换,即使用{ }A(int a1, int a2):_a(0),_a1(a1),_a2(a2){}//显式写拷贝构造A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}private:int _a;int _a1;int _a2;
};int main()
{// 隐式类型转换// 内置类型转换为自定义类型// A aa4 = 3.3;——3.3先转为int(警告:double转为int可能丢失数据)// A aa5 = 'a';——'a'先转为int// A aa6 = "abc";——报错:字符串不能隐式类型转换// raa 引用的是类型转换中用3构造的临时对象 —— 所以raa不能直接引用3,即A& raa = 3;,而是需要加一个constconst A& raa = 3;//正常调用多参数的构造A aaa1(1, 2);//赋值调用多参数的构造//1.不能直接 A aaa2 = 1, 2;//2.也不能 A aaa2 = (1, 2);——这个通过了。//因为调用了单参数的构造——小括号内理解为逗号表达式,值为2//C++11支持A aaa2 = { 1, 2 };(C++98不支持)A aaa2 = { 1, 2 };//理解为先用{1, 2}去构造,再拷贝构造//这里也不能直接引用,要加上constconst A& aaa3 = { 1, 2 };//——C++11之后还允许:// A aaa2{ 1, 2 };// const A& aaa3 { 1, 2 };// 即把赋值 = 省略掉,但是不建议,因为这种用法四不像return 0;
}
1.3.2.1 应用
范例:
class A
{
public://单参数构造函数//explicit A(int a)A(int a):_a(a){cout << "A(int a)" << endl;}//多参数构造函数——也支持隐式类型转换,即使用{ }A(int a1, int a2):_a(0),_a1(a1),_a2(a2){}//显式写拷贝构造A(const A& aa):_a(aa._a){cout << "A(const A& aa)" << endl;}private:int _a;int _a1;int _a2;
};class Stack
{
public:void Push(const A& aa){//...往栈里头存A类型的数据——即存自定义类型到栈内}//...
};int main()
{//使用多参构造来初始化AA aa1(1, 2);st.Push(aa1);//插入多参构造的A类对象st.Push({ 1,2 }); //按F11不会直接进入push,而是会先去到A的多参构造return 0;
}
1.3.3 explicit关键字
C++关键字——explicit。
功能:修饰构造函数,禁止隐式类型转换。
上述代码可读性不是很好,用explicit修饰构造函数,将会禁止构造函数的隐式转换。
2. static成员
2.1 概念
类的静态成员:声明为static的类成员称为类的静态成员。
静态成员变量:用static修饰的成员变量,称之为静态成员变量;
静态成员函数:用static修饰的成员函数,称之为静态成员函数。
- 静态成员变量只能在类外进行初始化
修正过后,输出结果:
静态成员变量_scount:
- 在静态区,不存在对象中——所以对象的大小是8,而不是12。
- 不能给缺省值,因为缺省值是给初始化列表,它在静态区不在对象中,不走初始化列表。
初始化列表是对对象存储区域的依次初始化,静态成员变量不在这里。 - 属于所有整个类,属于所有对象。
类实例化的对象在栈上,类的静态成员变量在静态区。
对象当中只存成员变量,不存成员函数(公共代码区)、不存静态成员变量(静态区)。
- 只能在类外定义,初始化也就只能写在类外面(类似于函数声明和定义分离)
(无论是public还是private,都只能在类外定义)
公有的静态成员变量可以在全局访问。
私有的静态成员变量只能在类里面访问,在类外可以通过公有的 静态成员函数 访问。
class A
{
public:A() { ++_scount; }A(const A& t) { ++_scount; }// 静态成员函数static int GetCount(){//_a1 = 1;不能访问普通成员变量——依靠this指针访问的:this->_a1return _scount;}~A() { --_scount; }
private:// 声明int _a1 = 1;int _a2 = 1;
public:static int _scount;
};int A::_scount = 0;int main()
{A aa1;cout << sizeof(aa1) << endl;//aa1._scount++;//cout << A::_scount << endl;//private私有不支持类外访问//就可以通过公有的静态成员函数访问//静态成员函数的两种访问方式cout << A::GetCount() << endl;cout << aa1.GetCount() << endl;return 0;
}
static修饰成员变量和成员函数的意义完全不同:
- 修饰变量:影响生命周期。
- 修饰全局函数:影响链接属性。
- 修饰成员函数:没有this指针——意味着只能访问静态成员。
(公有)静态成员函数(没有this指针)的两种访问方式:
- 和普通的成员函数一样,由对象调用。
- 原因1:要突破类域,去类里面找函数的出处;
- 原因2:需要传递调用这个成员函数的对象的this指针
- 指明类域,直接调用。
2.2 应用
【面试题】统计A类型的对象,一共创建了多少个。
对象不是构造出来的,就是拷贝构造(特殊的构造)出来的。
- 在构造时++,在拷贝构造时++,在析构时--,就可以通过这个静态成员变量获取到当前还有多少个A类型的对象还存活着,还在用。
- 去掉在析构时--,就能统计一共创建了多少个A类型的对象。
示例1——统计累积对象数目:
class A
{
public:A() { ++_scount; }A(const A& t) { ++_scount; }// 静态成员函数static int GetCount(){//_a1 = 1;不能访问普通成员变量——依靠this指针访问的:this->_a1return _scount;}~A() { //--_scount; }
private:// 声明int _a1 = 1;int _a2 = 1;
public:static int _scount;
};int A::_scount = 0;A func()
{A aa4;return aa4;
}int main()
{A aa1;A aa2;A aa3(aa1);func();cout << A::GetCount() << endl;return 0;
}
看似是4,结果却是5。
(VS2019下是5,VS2022下是4——返回值优化,没有拷贝构造临时对象)
实例2——统计现存对象数目:
class A
{
public:A() { ++_scount; }A(const A& t) { ++_scount; }// 静态成员函数static int GetCount(){//_a1 = 1;不能访问普通成员变量——依靠this指针访问的:this->_a1return _scount;}~A() { --_scount; }
private:// 声明int _a1 = 1;int _a2 = 1;
public:static int _scount;
};int A::_scount = 0;A func()
{A aa4;return aa4;
}int main()
{A aa1;A aa2;A aa3(aa1);func();cout << A::GetCount() << endl;return 0;
}
结果:3。
现存对象只有3个,aa4创建之后_scount++,析构之后_scount--。
不过返回时是否拷贝构造临时对象(返回值优化后,传值返回不会拷贝构造临时对象)都不会改变_scount的值。
2.3 特性
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
2. 静态成员变量只能在类外定义,定义时不添加static关键字,类中只是声明
3. 类的静态成员可用 类名::静态成员 或者 对象.静态成员 来访问。
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员。
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
【问题】
1. 非静态成员函数可以访问静态成员变量吗?(可以)
2. 静态成员函数可以调用非静态成员函数吗?(不可以)(调用非静态成员函数需要传this指针)
3. 非静态成员函数可以调用类的静态成员函数吗?(可以)
非静态函数可以调用静态函数,哪怕静态函数定义在后面。
- 因为类是一个整体,会在整个类里面全搜索。
- 全局的只能从上到下地搜索。
3. 友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数、友元类。
3.1 友元函数
问题:现在尝试去重载operator<<,然后发现没办法将operator<<重载成成员函数。
因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。
所以要将operator<<重载成全局函数。但又会导致类外没办法访问成员,此时就需要友元来解决。
operator>>同理。
- 友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
【说明】
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数括号后不能用const修饰(const修饰*this,友元函数不是类的成员函数,没有this指针)
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
3.2 友元类
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性。(互为友元的情况是很少的)
比如Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递
如果B是A的友元,C是B的友元,不能说明C是A的友元。
- 友元关系不能继承,在继承位置再给大家详细介绍。
///////////////////////////////////////////////////////////////////////////////友元类
class Time
{// 声明友元——可以声明在类里面的任何位置,一般喜欢声明在最上面// Date是Time的友元// Date中可以访问Time的私有// 但是Time中不能访问Date的私有friend class Date;
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){_t._hour++;}//设置时分秒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;
};
【注意】
- 友元尽量少用——某种意义上来说破坏了封装。
4. 内部类
一个类的内部可以定义:变量、函数、(内部)类。
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类,参见友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系。
- C++的内部类和外部类的关系:是两个独立的类;
- 联系1:访问B类会受到A类的类域的限制,需要指明A类的类域。
- 联系2:访问B类也会受到访问限定符的限制——即如果不想人从外面使用这个类,可以设置成私有private ——即成为A的专属类,只有A类里面能用B类
- 意义:B天生就是A的友元(类)——B天生能访问A中的成员变量。
A aa;
// 默认构造,未初始化内置类型成员变量,a.x = a.y = 随机值
A()
// 创建一个 值初始化 的临时对象,对于 没有用户定义构造函数 的类(如A
),值初始化会将其成员变量零初始化。
改正后的程序结果:
- 用途:C++不太爱使用内部类,Java喜欢使用内部类
5. 匿名对象
- 用 类型(实参)定义出来的对象叫做匿名对象。(没有对象名)
- 相比之前我们定义的 类型 对象名(实参)定义出来的叫有名对象。
- 匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
最后的两个析构是main函数结束了,有名对象生命周期到了,被销毁。
有名对象、匿名对象的区别:
- 有名对象的生命周期:当前作用域。
- 匿名对象的生命周期:当前命令行。
匿名对象的特点:一个即用即销毁的对象。
匿名对象的使用场景:
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();// 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,// 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调用析构函数A();A(1);A aa2(2);// 匿名对象在这样场景下就很好⽤,当然还有⼀些其他使用场景,这个我们以后遇到了再说Solution().Sum_Solution(10);return 0;
}
6. 拷贝对象时的一些编译器的优化
这里是按照VS2019环境下的优化来介绍的,VS2022开的优化比2019更大,不太利于理解。
VS2022作了跨行优化(突破型的优化)。
- 现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
- 如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译器还会进行跨行跨表达式的合并优化。
6.1 场景①-传值传参
测试代码:
class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}A(int a1, int a2){cout << "A(int a1, int a2)" << 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)
{}int main()
{// 构造A aa1;// 传值传参——拷贝构造f1(aa1);cout << endl; //先是形参aa的析构(此时f1函数结束)——局部对象的生命周期是当前函数作用域//换行后的隔一行是aa1的析构(此时main函数结束)return 0;
}
执行结果:
引用传参,可以减少一个拷贝构造、一个析构。
引用传参的注意事项:建议加上const,避免改变形参直接影响到实参。
- 同时加上const还有一个好处:可以支持传匿名对象、临时对象。
- 匿名对象、临时对象都具有常性。(使用完就析构)
传匿名对象和传单参构造的隐式类型转换的临时对象,在这里是等价的。
实践当中一般也不会传匿名对象,更多的时候还是直接传单参构造的隐式类型转换更方便。
以上的例子还没有涉及到编译器的优化。
class A
{
public:A(int a = 0):_a(a){cout << "A(int a)" << endl;}A(int a1, int a2){cout << "A(int a1, int a2)" << 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)
{}int main()
{// 构造A aa1;cout << endl;// 传值传参——拷贝构造f1(aa1);cout << endl;// 下面3个都是:一个表达式中,涉及的连续的“构造+拷贝构造”,部分编译器认为中间构造出来的临时对象白白浪费了,会优化成一个构造,跳过中间对象,直接去构造目标对象// 匿名对象——构造+拷贝构造f1(A(1));cout << endl;// 临时对象——构造+拷贝构造f1(2);cout << endl;// 隐式类型转换——构造+拷贝构造A aa2 = 3; cout << endl;A aa3; aa3 = 3; //隐式类型转换:构造+拷贝构造cout << endl;return 0;
}
执行结果:
这3个构造后面的拷贝构造都被优化掉了,理论上应该有,实践中没有。
【注意】
- 不优化也正常,是编译器根据自身的开发环境自主实现的。
- 但是一般较新的编译器最少都会进行这个优化,甚至一些编译器优化力度非常大。
6.2 场景②-传值返回
经验规律:一个表达式中,连续的“拷贝构造 + 拷贝构造” -> 优化为一个拷贝构造。
代码验证:
引用的形式接收,传值返回的值——这里只作演示,不作深入讨论,不建议这样做,引用的是临时对象其内容不太确定。
引用的是拷贝构造的临时对象,没有了一行当中连续的“拷贝构造+拷贝构造”,所以本来引用接收就会少一个拷贝构造,这里的引用接收就是打破了编译器的优化,这里就不存在编译的优化了。
本来这个&(引用符号)应该放在返回值那里,把传值返回,变成引用返回,才能减少拷贝。
再看一下VS2022的处理:
引用返回反而不够优化。
传值返回反而优化力度大:
再来看看不优化的情况:
f2结束之前,aa销毁之前先拷贝构造出一个临时对象用于传值返回,再调aa的析构。
临时对象用作赋值,临时对象最后被析构。
分行写,导致拷贝构造变成了赋值重载,编译器的优化没了。
看看VS2022(默认Debug)的效果:
注:VS2019的Release版本也开了同样的跨行优化。
Release下把构造的对象aa直接用作临时对象,给ret2赋值,减少一次拷贝,这样aa的生命周期以及不在f2了——赋值之后才析构。
Release下,编译器进行语法分析,发现f2函数内:构造aa,拷贝构造临时对象,拷贝构造ret。
不如在f2内最初构造的直接就是ret,直接一开始就操控ret这块空间进行构造。
(aa和ret合并了,aa相当于ret的别名,底层是用指针实现的)
aa在换行之后才析构,而不是f2结束直接就析构,说明aa就是ret1。
VS2019的Release下的本来的Debug下的“构造+拷贝构造+拷贝构造”优化的“构造+拷贝构造”,再被优化成一个构造(跨行优化后合三为一)
- ① 不优化:不能用aa返回,因为栈帧销毁,aa就没了,所以要创建一个临时对象,这个临时对象一般比较小,会用寄存器存储,如果比较大,会在两个栈帧中间开一块空间存。
- ② VS2019的Dubug等级优化:“构造+拷贝构造+拷贝构造” ==> “构造+拷贝构造”
- ③ VS2019的Release等级优化:“构造+拷贝构造+拷贝构造” ==> “构造”
(最高等级的优化)
【结论】
- 场景①-传值传参:连续的“构造 + 拷贝构造 ——> 优化为一个构造。
- 场景②-传值返回:连续的“拷贝构造 + 拷贝构造 ——> 优化为一个拷贝构造。
7. 再次理解类和对象
现实生活中的实体计算机并不认识,计算机只认识二进制格式的数据。如果想要让计算机认识现实生活中的实体,用户必须通过某种面向对象的语言,对实体进行描述,然后通过编写程序,创建对象后计算机才可以认识。比如想要让计算机认识洗衣机,就需要:
1. 用户先要对现实中洗衣机实体进行抽象---即在人为思想层面对洗衣机进行认识,洗衣机有什么属性,有那些功能,即对洗衣机进行抽象认知的一个过程
2. 经过1之后,在人的头脑中已经对洗衣机有了一个清醒的认识,只不过此时计算机还不清楚,想要让计算机识别人想象中的洗衣机,就需要人通过某种面相对象的语言(比如:C++、Java、Python等)将洗衣机用类来进行描述,并输入到计算机中
3. 经过2之后,在计算机中就有了一个洗衣机类,但是洗衣机类只是站在计算机的角度对洗衣机对象进行描述的,通过洗衣机类,可以实例化出一个个具体的洗衣机对象,此时计算机才能洗衣机是什么东西。
4. 用户就可以借助计算机中洗衣机对象,来模拟现实中的洗衣机实体了。
在类和对象阶段,大家一定要体会到,类是对某一类实体(对象)来进行描述的,描述该对象具有那些属性,那些方法,描述完成后就形成了一种新的自定义类型,才用该自定义类型就可以实例化具体的对象。
C++的面向对象是在描绘这个世界,类就是对应现实世界的抽象类型,对象就是对应各个现实实体。
例——外卖系统: 三大类:骑手、商家、用户。
每个类要设计哪些数据,取决于类本身,哪些数据是它最关注的核心。
例:
骑手:姓名、电话、当前位置、……
商家:坐标位置、菜品(种类、价格)、……
用户:电话、住址、……
这重要的三个类别,实例化出n多个对象,对象和对象之间建立出关联、关系,每个现实中的实体,都对应外卖平台上的一个类别和对象。
8. 练习题
8.1 自然序列求和
8.1.1 一般解法:static静态成员
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)_
OJ链接
把常见的方式全部限制了——有学习意义,没有实用意义
- 循环(for、while)
- 递归(需要返回条件:if、else、三目)
- 公式(乘除法)
不论是循环还是递归,都是想走n次,将这n次的n个自然序列数字求和。
问:循环和递归禁用后,怎么走n次呢???
答:调用n次构造函数。——如何实现???
1. 定义一个数组,大小为n,每个元素是Sum对象,实现调用n次Sum构造
2. 在每次调用的构造里面,对静态变量进行操作 ——为了保留操作结果,得到递增的自然序列的每个数字。
3. 定义两个私有的静态变量。
4. 在构造函数中操作这两个静态变量:ret += i,i++。
5. 为了获取求和结果ret——私有静态变量,可以选择定义一个公有静态函数。
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 arr[n];return sum::GetRet();}
};
C99的变长数组,VS不支持 (VS只能new一个数组) OJ编译器都比较新,比较全面,支持绝大多数的C++规则,所以一些在VS上跑不过的代码,在OJ题上能跑过。
牛客OJ应该是用的g++(linux)
8.1.2 优化解法:内部类
改造成这个样子:使得Sum只能用于Solution内部。
因为Sum是专门写来解决Solution里面的这个问题的,不想给外面用,就可以把Sum放成Solution的内部类,并且设置成私有。
这样改进,Sum里面也不用放这两个静态成员变量了,可以让Solution来放:
- 好处1:Solution直接就能访问这两个成员变量
- 好处2:Sum作为Solution的内部类,也能访问这两个成员变量。
这样就实现了更好的封装:Sum类完全封死在Solution内部 ——只有Solution内部能够创建Sum类的对象,外部无法创建Sum类的对象,减少干扰。
8.2 日期·是·今年的第几天
计算日期到天数的转换_OJ链接
思路1:把日期类copy过来,利用日期类的减法来做。
思路2:
1)定义年(int)、月(int)、日(int)
2)定义一个数组: 不是每个月的天数,而是1月到n月的累计天数(平年)
3)判断闰年,修正算法(+1day)
8.3 日期差值
日期差值_OJ链接
8.4 打印日期
打印日期_OJ链接
8.5 累加天数
累加天数_OJ链接
9. 补充问题
9.1 析构顺序
同一个函数栈帧内的对象,后定义的,先析构。先定义的,后析构。
先定义的,在栈的高地址端,后定义的,在栈的低地址端。
函数栈帧销毁时,是从低地址→高地址的顺序销毁的——栈的特点是后进先出。
9.2 构造顺序
总的结果:
调试观察:
- 全局对象,在main函数之前构造;
- 局部的静态对象,生命周期是全局的,f2第一次调用的时候构造(初始化);