1.再谈构造函数
1.1引入
MyQueue的默认构造函数可以不写,因为它的自定义类型成员会去调用自己的默认构造函数。
//用两个栈实现队列
class MyQueue
{
private:
Stack _st1;
Stack _st2;
};
但如果Stack类不提供默认构造函数怎么办?
此时MyQueue的默认构造函数生不成,就会报错,所以此时就需要自己去书写构造函数,显式调用构造函数,但是在函数体中自定义类型调用不了函数。
此时就要引入初始化列表
构造函数初始化分为两种:
一、在构造函数体内去写初始化
二、初始化列表
1.2在构造函数体内赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
1.3初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
:_year()//可以不传值
,_month(month)
,_day(day)
{
}
//...
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 4);
return 0;
}
1.3.1注意:二者可以混着使用
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
,_month(month)
{
_day = 1;
}
//...
private:
int _year;//每个成员声明
int _month;
int _day;
};
int main()
{
Date d1(2024, 9, 4);
return 0;
}
1.3.2初始化列表是每个成员定义的地方
对变量而言,声明和定义的区别在于:声明时不开空间,对象实例化、定义时才会去开空间。
三个成员在Date d1(2024, 9, 4);这里整体定义,整体定义时开空间。
那么,每个成员在什么时候定义呢?
C++认为,初始化列表就是每个成员定义的地方,定义时要给值。
问题又来了,为什么要这样设计这样一个看似多此一举的初始化列表?在Date d1(2024, 9, 4);这里对对象整体定义不好吗?
1.3.3原因
设计初始化列表是为了解决三种情况,或是说填三个大坑。
1.3.3.1
const修饰的成员在构造函数体内给值是不可行的,因为它必须在定义时初始化,const修饰说明它不能再次被赋值修改,要赋值只有一次机会,就是在定义时。
所以_x定义的地方要在初始化列表处
class Date
{
public:
//初始化列表是每个成员定义的地方
Date(int year, int month, int day)
:_year(year)
,_month(month)
,_x(1)
{
//_year = 1;
_day = 1;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
//必须定义时初始化
const int _x;
};
1.3.3.2
在成员变量声明处声明引用。
成员变量可以定义引用,因为在声明处,可以不给值,但是引用有个特点:必须在定义时初始化。
class Date
{
public:
Date(int year, int month, int day,int& i)
//Date d1(2024, 9, 4, n);
:_year(year)
,_month(month)
,_x(1)
,_refi(i)//把n传给i,i是n的别名,_refi又是i的别名
{
_day = 1;
}
void func()//调用这个函数,外面的a会随之改变
{
_refi++;
_refi++;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
//必须定义时初始化
const int _x;
int& _refi;
};
int main()
{
int a = 0;
int& n = a;
Date d1(2024, 9, 4,a);
d1.func();
return 0;
}
但如果是在构造函数体内
class Date
{
public:
Date(int year, int month, int day,int& i)
:_year(year)
,_month(month)
,_x(1)
//,_refi(i)
{
_refi=i;//明显就是赋值了
}
//...
};
1.3.3.3
成员中有自定义类型,但是,
1.自定义类型成员没有默认构造函数,
2.自定义类型成员有默认构造函数,但是不想去调用它的默认构造函数
就比如上面提到的MyQueue,此时就要去调用带参数的构造函数,在定义初始化时去调用
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class Date
{
public:
//初始化列表是每个成员定义的地方
Date(int year, int month, int day,int& i)
:_year(year)
,_month(month)
,_x(1)
,_refi(i)
,_a(1)
//定义初始化的地方
{
_day = 1;
}
void func()
{
_refi++;
_refi++;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
//必须定义时初始化
const int _x;
int& _refi;
A _a;
};
int main()
{
int a = 0;
int& n = a;
Date d1(2024, 9, 4, n);
return 0;
}
1.3.4补充
调用构造函数时,先执行初始化列表,再执行函数体
这里代码执行到489行时,其它成员已经初始化,而_day却是随机值也可以证明这个。
实际上在初始化列表阶段,_day也定义了,但是对于内置类型,编译器是不处理的,所以这里给了随机值,而自定义类型就算不写在初始化列表处,编译器也会作出处理,会去调用它的默认构造函数,当然前提它要有默认构造函数
总结:在构造函数的初始化列表阶段,对内置类型编译器是不做处理的,是用随机值初始化(有些编译器可能会初始化为0),对自定义类型,会去调用它的默认构造函数。
初始化列表是每个成员定义的地方!!!不管写不写初始化列表,调用构造函数时都会去执行这一部分,初始化列表就是构造函数的一部分
如果自定义类型既有默认构造函数,又显式写了比如_a(1); 此时会使用显式写的,只有不传值时才会去调用它的默认构造函数
1.3.5声明时给缺省值
class Date
{
public:
//初始化列表是每个成员定义的地方
Date(int year, int month, int day,int& i)
:_year(year)
,_month(month)
//,_day(day)
,_x(1)
,_refi(i)
,_a(1)
{
_day = 1;//赋值
}
void func()
{
_refi++;
_refi++;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month=12;
int _day=20;//给值不是初始化,给的是缺省值
//C++11支持给缺省值,但这个缺省值其实是给初始化列表的
//如果初始化列表没有显式给值,就使用这个缺省值
//如果显式给值了,就不使用这个缺省值
//必须定义时初始化
const int _x;
int& _refi;
A _a;
};
C++11支持给缺省值,但这个缺省值是给初始化列表的,比如上面的代码,_day没有在初始化列表处显式给值,此时_day会使用缺省值初始化,而_month在初始化时显式给值了,就不会使用缺省值了。
缺省值在参数处、成员声明处可以存在,可以看作备胎。
初始化列表是按照声明的顺序执行的,和初始化列表中的书写定义顺序无关
注意:const修饰的成员也可以只在声明处给缺省值,它有了缺省值后,在初始化列表不进行书写也是可以的
初始化列表的执行顺序只是说祖师爷选择了这种方式,如果按照定义顺序去执行,那么如果有些成员不在初始化列表中显式书写,就不好去确认它们的先后顺序了。
1.3.6总结
//我们来看一道题
// A.输出1 1
// B.程序崩溃
// C.编译不通过
// D.输出1 随机值
class A
{
public:
A(int a)
:_a1(a)
, _a2(_a1)//这里会先使用_a1来初始化_a2,但是这里_a1是随机值
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main() {
A aa(1);
aa.Print();
}
//答案是D
5.总之,所有的成员能使用初始化列表初始化就尽量使用初始化列,当然,有些场景还是需要初始化列表和函数体混着使用,比如下图
//书写Stack类的构造函数时
class Stack
{
Stack(int n)
:a((int*)malloc(sizeof(int) * n))//这里不混着写就不方便检查失败了
,top(0)
,capacity(n)
{
cout << "Stack(size_t n = 4)" << endl;
//检查是否失败还是要放在这里
if (n == 0)
{
a = nullptr;
top = capacity = 0;
}
else
{
if (a == nullptr)
{
perror("realloc fail");
exit(-1);
}
}
//同时初始化列表无法解决初始化问题
//那么这里想给数组初始化,还需要自己写一段代码
memset(a, 0, sizeof(int) * n);
//不能做到不写上述代码,只在初始化列表中操作,有时候还需要调用一些函数
}
//...
}
1.4explicit关键字
1.4.1引入
class A
{
public:
A(int i)
:_a(i)
{
cout << "A(int i)" << endl;
}
A(const A& aa)//用来验证第二个会不会先构造再拷贝构造
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);//这里是直接构造
A aa2 = 2;//这里为什么也能通过?
//这里是先构造,再拷贝构造,再优化
//这里本质是隐式类型转换
//C++支持单参数构造函数的隐式类型转换
//隐式类型转换中间要生成一个临时对象
//这里应该是用2去调用A的构造函数生成一个临时对象,再用这个对象去拷贝构造aa2
//但是这里编译器又有优化,优化为使用2直接构造
//这里的直接构造是优化的结果
//所以它不会先构造再拷贝构造
//所以这两段代码的过程不一样,但结果是一样的
return 0;
}
注意:拷贝构造也可以使用初始化列表,因为它也是一个构造,它是一个特殊的构造
class A
{
public:
A(int i)
:_a(i)
{
cout << "A(int i)" << endl;
}
private:
int _a;
};
int main()
{
A aa1(1);//这里是直接构造
A aa2 = 2;
//A& ref = 2;//会报错
//原因不是int类型不能转换为A类型
//是因为这里支持隐式类型转换,中间会生成临时对象,临时对象具有常性
//(与传值返回会生成临时对象类似)
//所以ref不能引用这个临时对象,发生了权限的放大
const A& ref = 2;//加上const就可以了
//这里可以引用是因为单参数的构造函数支持隐式类型转换,参数的整型值能够转换为一个A的对象
return 0;
}
1.4.1.1适用场景
class A
{
public:
A(int i)
:_a(i)
{
cout << "A(int i)" << endl;
}
private:
int _a;
};
struct SeqList
{
public:
void PushBack(const A& x)//假设A对象比较大?
{
// ... 扩容
_a[_size++] = x;
}
//...
private:
A* _a = (A*)malloc(sizeof(A) * 10);
size_t _size = 0;
size_t _capacity = 0;
};
int main()
{
SeqList s;
//此时想要插入数据会很不方便,需要自己先定义
A aa3(1);
s.PushBack(aa3);
//但使用上面的语法,就会方便许多
s.PushBack(4);
//可以这样写的原因是隐式类型的转换
return 0;
}
1.4.2explicit的一种用法
构造函数不仅可以构造与初始化对象,对于接收单个参数的构造函数,还具有类型转换的作用。接收单个参数的构造函数具体表现:
1. 构造函数只有一个参数
2. 构造函数有多个参数,除第一个参数没有默认值外,其余参数都有默认值
3. 全缺省构造函数
class A
{
public://如果不想让隐式类型转换发生
//在构造函数这个位置加上explicit就可以让隐式类型转换不去发生,就不支持这个转换了
//禁止隐式类型转换后还可以提高代码可读性
explicit A(int i)
:_a(i)
{
cout << "A(int i)" << endl;
}
private:
int _a;
};
1.4.3补充:隐式类型转换
int main()
{
int i = 0;
double d = 1;
i = d;
//这里d可以赋给i
//类型转换,中间都会生成一个临时变量
return 0;
}
如何证明呢?
int main()
{
//如何证明?
double d = 1;
int i = d;
//int& j = d;
//这里j不能引用d
//但不能引用的原因不是类型不同
//是因为中间生成了临时变量,临时变量具有常性,发生了权限的放大
const int& j = d;
//所以加上const就可以引用了
return 0;
}
上述代码都是同理
1.4.4多参数构造函数
class B
{
public:
//explicit B(int b1,int b2)//同理,这样就不会发生类型转换了
B(int b1,int b2)
:_b1(b1)
,_b2(b2)
{
cout << "B(int b1,int b2)" << endl;
}
private:
int _b1;
int _b2;
};
int main()
{
//多参数的也无法给值
//B jx = 1, 2;
//难道这样给吗?当然是不行的
//C++98不支持多参数隐式类型转换
//但是C++11是支持多参数的隐式类型转换的
B bb1(1, 2);
B bb2 = { 1,2 };//与上述语法原理相同
//B& ref = { 1,2 };//这里不支持
const B& ref2 = { 1,2 };//这里就支持了
return 0;
}
// s.PushBack{1,2};//刚刚那里如果是B可以这样传值
1.5匿名对象
C++中还允许定义一个东西,叫做匿名对象。
int main()
{
//有名对象 特点:生命周期在当前局部域
A aa6(1);//可以这样定义对象
//匿名对象 特点:生命周期只在这一行 匿名对象别人使用不了
A(10);//还可以这样定义对象
//这里先去调用构造,执行完这一行后就直接去调用析构了
A aa7(2);//然后这里再次调用构造
}
注意:匿名对象、临时对象都具有常性
1.5.1适用场景
1.5.1.1
int main()
{
SeqList s1;
s1.PushBack(aa6);
s1.PushBack(aa7);
//平时传参,都需要先去定义一个有名对象,再去传参
//先定义对象,再传参是很麻烦的
//又恰巧此时隐式类型转换被禁用了 explicit A(int i)
//那么这个语法就也没办法使用了 s1.PushBack(8);
//这时,使用匿名对象就会很方便了
s1.PushBack(A(10));
//匿名对象的用途也没有那么核心,但是它可以简化代码
}
1.5.1.2在OJ题中
//C++的OJ题通常类似这样
class Solution {
public:
//io型、接口型的,都把函数放在一个类中
int Sum_Solution(int n) {
// ...
return n;
}
private:
};
int main()
{
//正常情况想要去调用函数
//需要先定义一个有名对象,再使用有名对象去调用函数
Solution sl;
sl.Sum_Solution(10);
//然而定义对象就是为了调用这个函数,还要写两行代码,有些麻烦
//这时使用匿名对象也十分方便
Solution().Sum_Solution(100);//直接定义一个匿名对象去调用函数
//Solution()这里定义了一个对象,只是这个对象没有取名字
//会去调用它的构造函
//调用它的构造函数是为了调用int Sum_Solution(int n)这个函数
//又比如要把今天的日期打印一下
Date d(2024, 9, 4);
cout << d;
//假设只是要求打印日期,却还要去取个名字,有些麻烦
cout << Date(2024, 9, 4);
//如果使用这个对象只使用一次,匿名对象就会十分方便
return 0;
}
2.static(静态)成员
声明为 static的类成员称为 类的静态成员,用 static修饰的 成员变量,称之为 静态成员变量;用 static修饰的 成员函数,称之为 静态成员函数。 静态成员变量一定要在类外进行初始化。
2.1引入
class A
{
public:
A()
{
}
A(const A& t)
{
}
~A()
{
}
private:
};
假如有些需求
1.想要统计下A这样的一个类创建了多少个对象,累计创建了多少个对象?
2.统计下正在使用的还有多少个对象?
如何达成目的?
2.1.1方法一:定义全局变量
int n=0;//代表累计创建的对象数
int m=0;//代表正在使用的对象数
//A的所有对象都是构造出来或是拷贝构造出来的
class A
{
public:
A()
{
++n;
++m;
}
A(const A& t)
{
++n;
++m;
}
~A()
{
--m;
}
private:
};
//A& func(A& aa) //所以可以使用引用就最好去使用引用
A func(A aa)
{
return aa;
}
int main()
{
A aa1;
A aa2;
cout << n << " " << m << endl; //2 2
A();//定义匿名对象,它的生命周期只在这一行
cout << n << " " << m << endl; //3 2
func(aa2);//如果使用引用,则不会去创建新的对象
cout << n << " " << m << endl; //5 2
return 0;
}
2.1.1.1问题
但是这种写法存在很大的缺陷,C++讲究封装,定义全局变量虽然可以解决问题,但是n和m可能会在外面被人随意修改,比如当有人书写了 n--; m++; 这样的代码时。
那么把m和n定义到函数的私有成员处能不能解决这个问题
class A
{
public:
A()
{
++n;
++m;
}
A(const A& t)
{
++n;
++m;
}
~A()
{
--m;
}
private:
int n = 0;
int m = 0;
//显然是不行的,这样的话,每个对象就都有m和n了
//我们不期望这个变量属于某一个对象
//而是期望它们属于整个类,是全局的,属于所有对象
};
2.1.2方法二:定义静态成员变量
class A
{
public:
A()
{
++n;
++m;
}
A(const A& t)
{
++n;
++m;
}
~A()
{
--m;
}
private:
static int n;//加上static,成员就属于所有对象了,它是全局的
static int m;
};
2.1.2.1注意
一、静态变量不能在这里给缺省值了,缺省值是在初始化列表阶段使用的,而静态变量不会去执行初始化列表阶段,初始化列表是某个对象的成员的初始化,而静态变量不是属于某个对象,它属于所有对象
二、此时sizeof(A)应该是1,这两个成员不会存储在对象中,因为它们不是属于某个对象,而是属于所有对象,它们存储在静态区
三、私有成员这里是声明,不会去开空间
//定义时要去说明它是属于A这个类域的 int A::n = 0; int A::m = 0; //它们是私有的,不能在类外面访问 //这里只是相当于声明与定义分离,而不是在外访问 //如果有.h和.cpp的话,这个要写在.cpp中,否则会出问题 //成员变量不能声明定义放在一起,所以必须声明和定义分离 //此时就无法随意修改n和m了 //多次调用构造函数访问到的就是同一个m和n
四、如果m和n是公有的,还可以使用别的访问方式。
突破类域有三种方式
int main() { //如果m和n是公有的,还可以这样访问 cout << A::n << " " << A::m << endl;//突破类域的一种方式 // 属于类域 cout << aa1.n << " " << aa1.m << endl; // 属于所有对象 //直接写m或者n是不行的,因为它们二者属于类域、命名空间域 A* ptr = nullptr; cout << ptr->n << " " << ptr->m << endl; //注意,可以这样访问不代表n、m就在aa1或ptr指向的对象里面 //从底层来说,n、m是不在aa1或ptr指向的对象里面的 //上面两种方法仅仅只是帮助它突破类域 return 0; }
五、如果m和n是私有的,此时想要访问,就要写一个公有的成员函数
class A { public: //... int GetM() { return m; } void Print()//在类域中可以访问n和m { cout << n << " " << m << endl; } private: static int n; static int m; };
2.1.3新的场景
int main()
{
A();
A();
//上面定义了两个匿名对象
//那么这里如何访问呢?
A().Print();//使用匿名对象调用函数是可以的
//但是这里会再创建一个对象,就干扰了我们的逻辑
//A::Print();//这样访问也是不行的
//报错信息:“A::Print”: 调用非静态成员函数需要一个对象
A aa1;
func(aa1);
aa1.Print();//这里可以访问
return 0;
}
2.2静态成员函数
class A
{
public:
//...
int GetM()
{
return m;
}
//这里static与返回值无关,它表明这是一个静态成员函数
static void Print()//在类域中可以访问n和m
{
cout << n << " " << m << endl;
}
//如果有静态成员变量,一般就会提供静态成员函数来访问静态成员变量
private:
static int n;
static int m;
};
int main()
{
A();
A();
//上面定义了两个匿名对象
//那么这里如何访问呢?
//这时就需要使用静态成员函数
A::Print();
//这里就不需要传this指针了,只需要突破类域即可
//不使用对象去调用,而是使用类域去调用
A aa1;
func(aa1);
aa1.Print();
//三种突破类域的方式依旧可以用来访问静态成员函数
return 0;
}
静态成员函数的特点:没有this指针
之所以调用普通成员函数时要使用对象去调用,是因为要去对象里面找,要使用this指针
比如 aa1.GetM();
静态成员函数的限制
class A { public: //... int GetM() { return m; } //这里static与返回值无关,它表明这是一个静态成员函数 static void Print()//在类域中可以访问n和m { //_x++; //静态成员函数不能访问非静态成员,因为没有this指针 //普通成员函数可以访问都是通过this指针来访问的 cout << n << " " << m << endl; } //如果有静态成员变量,一般就会提供静态成员函数来访问静态成员变量 private: static int n; static int m; int _x=0; };
2.2.1练习题
求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
//此题从教学角度完美体现了静态成员的价值
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;
//C++的题都会使用Solution包起来
//为了防止冲突
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];//牛客使用g++,支持一个 变常数组(C99支持)
//VS编译器不支持
//利用这种方法就把n次构造函数给调用了
return a[0].GetRet();
}
};
//比如想调用10次构造函数
//需要定义10个对象
//如何定义10个对象? 可以定义一个数组
//比如 A arr[10];
//私有和保护限制的是能否在类外直接访问,而不是说不能被修改
2.3总结特性
1. 静态成员为 所有类对象所共享,不属于某个具体的对象,存放在静态区2. 静态成员变量必须在 类外定义,定义时不添加static关键字,类中只是声明3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问4. 静态成员函数 没有隐藏的 this指针,不能访问任何非静态成员5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制注意:1. 静态成员函数不可以调用非静态成员函数2. 非静态成员函数可以调用类的静态成员函数
3.友元
3.1友元函数
3.1.1注意
1.友元函数可以访问类的 私有和保护成员,但它 不是类的成员函数,所以友元函数没有 this指针,它只是一个 友元声明2.友元函数 不能用const修饰,它没有 this指针, 静态成员同理3.友元函数可以在类定义的任何地方声明, 不受类访问限定符限制,它可以在类中可以随意放置,它只是一个 声明4.一个函数可以是多个类的友元函数5.友元函数的调用与普通函数的调用原理相同,只是说语法检查时会把访问私有成员的地方执行通过
3.2友元类
class Time
{
friend class Date;
//声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
private:
int _hour;
int _minute;
int _second;
};
class Date
{
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;//这里定义了一个Time类的对象
};
3.2.1注意
1.友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想 在Time类中访问Date类中私有的成员变量则不行。2.友元关系不能传递,比如B是A的友元,C是B的友元,但这不能说明C是A的友元。3.友元关系不能继承
3.3总结
4.内部类
4.1概念
4.2注意
4.3特性
class D
{
private:
int _d;
};
class C
{
public:
private:
int _c;
B _bb; //这样写sizeof(C)才会是8
};
//类比上面,上面也是两个独立的类
//1.这里D类受C类域和访问限定符的限制,实际上它们也是两个独立的类
//2、内部类默认就是外部类的友元类
//所以C对象里没有D
class C
{
public:
class D
{
private:
int _d;
};
private:
int _c;
};
int main()
{
cout << sizeof(C) << endl;//这里是4
return 0;
}
//分开写和放在一起写是几乎一样的
class C
{
//public:
class D
{
private:
int _d;
};
void func()
{
D dd;
//此时只有类里面才能使用D定义对象
}
private:
int _c;
};
int main()
{
cout << sizeof(A) << endl;//这里是4
C cc;
//C::D dd;
//前提D是公有的,才可以这样写
//内部类不想被别人使用,就可以定义为私有
//此时只有类里面才能使用它定义对象
return 0;
}
4.4改进练习题
求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
//改进1
class Solution {
class Sum//外部无法使用Sum类定义对象
{
public:
Sum()
{
ret += _i;
_i++;
}
static int GetRet()
{
return ret;
}
private:
static int _i;
static int ret;
};
public:
int Sum_Solution(int n) {
Sum a[n];
return Sum::GetRet();
}
};
int Solution::Sum::_i = 1;
int Solution::Sum::ret = 0;
//改进2
class Solution {
class Sum//外部无法使用Sum类定义对象
{
public:
Sum()
{
ret+=_i;
_i++;
}
};
public:
int Sum_Solution(int n) {
Sum a[n];
return ret;
}
private:
static int _i;
static int ret;
};
int Solution::_i=1;
int Solution::ret=0;
5.拷贝对象时编译器做出的一些优化
5.1回顾
class A
{
public:
A(int a = 0)
{
}
A(const A& aa)
{
}
void Print() const
{
cout <<"Print->" << _a << endl;
}
private:
int _a = 0;
};
void f1(A aa)//这里传参调用拷贝构造函数是很有意义的
{
aa.Print();
}
int main()
{
A aa1;
f1(aa1);
return 0;
}
void f1(const A& aa)//使用引用可以减少一次拷贝构造
//而且这里最好加上const
{
aa.Print();
//但是如果只是调用了一个简单的函数
//那这里就没有必要去拷贝
}
int main()
{
A aa1;
f1(aa1);
f1(A());//匿名对象,具有常性,Print函数不加const就会报错
f1(2);//隐式类型转换,生成临时对象,具有常性,容易报错
//上面加上const可以避免报错
return 0;
}
甚至还可以这样调用函数
void f1(const A& aa=A())
//这里匿名对象的生命周期不只在这一行
{
aa.Print();
}
int main()
{
A aa1;
f1(aa1);
f1(A());
f1(2);
f1(); //甚至还可以这样调用函数
const A& ref = A();
// const引用会延长匿名对象的生命周期
// 此时ref出了作用域,匿名对象才会销毁
//本质是引用让匿名对象变成了有名对象
//匿名对象的生命周期就跟着ref引用走了
return 0;
}
5.2编译器会做出的优化
5.2.1
void f1(A aa)
{
aa.Print();
}
int main()
{
A aa1;
f1(aa1);
f1(A());
//这里看似和上面调用函数的过程是一样的,其实不然
return 0;
}
在上面的隐式类型转换时提到过,编译器在同一个表达式、同一行、同一个调用、一个连续的步骤里面,比如这里就是连续的构造、拷贝构造,它就会被优化,编译器认为连续的构造、拷贝构造太过浪费,于是会将二者合二为一,将其优化为直接构造。
注意:C++标准并未规定要进行优化,这只是目前大多数主流编译器所达成的共识。
void f1(A aa)
{
aa.Print();
}
int main()
{
A aa1;
f1(aa1);//这里是两个表达式,所以不会合二为一
cout << "--------------------------------" << endl;
// 一个表达式,连续的步骤里面,连续的构造可能会被合并
f1(A());
cout << "--------------------------------" << endl;
f1(1);
cout << "--------------------------------" << endl;
A aa2 = 1;//和上面同理
cout << "--------------------------------" << endl;
A aa3 = A(12);
return 0;
}
证明
5.2.2
A func()
//传引用返回就不会拷贝了,但这里不能,如果对象中带析构,那会出更多的问题
{
A aa;//一次构造
return aa;//一次拷贝构造
}
int main()
{
A ret1=func();//这里本来应该是2次拷贝构造
cout << "--------------------------------" << endl;
A ret2;
ret2 = func();//这种情况编译器就优化不了了
//1.同类型才能优化,拷贝构造和赋值不能合并
//2.在同一个步骤中才能优化
return 0;
}
5.2.3
A func()
{
// A aa;
//return aa;
//return A(1);//写法一
return 2;//写法二
//隐式类型转换
//能这样写最好这样写,首先这种函数不能使用引用返回
//这样写的话,它会触发编译器的优化,减少拷贝,提高效率
}
int main()
{
A ret1 = func();//构造+拷贝构造+拷贝构造 优化为了 直接构造
cout << "--------------------------------" << endl;
A aa2 = 1;
cout << "--------------------------------" << endl;
A aa3 = A(12);
return 0;
}