目录
一、继续学习构造函数
对象的初始化分为两部分
1.函数体内赋初值。只能叫赋初值的原因是:初始化只能初始化一次,而函数体内可以多次赋值。
2.初始化列表
1.初始化列表
形式如下:
冒号开始,中间隔开用逗号,括号里放初始值或表达式。
以栈的构造函数举例:
Stack(int capacity = 4)
:_a((int*)malloc(sizeof(int)*capacity))
,_top(0)
,_capacity(capacity)
{
if (_a == nullptr)
{
perror("malloc fail\n");
exit(-1);
}
}
//或者这样混合着来也行。一部分在函数体内部,一部分在初始化列表
Stack(int capacity = 4)
: _top(0)
, _capacity(capacity)
{
_a = (int*)malloc(sizeof(int)*capacity);
if (_a == nullptr)
{
perror("malloc fail\n");
exit(-1);
}
}
1.成员变量初始化可以混合着来,一部分在函数体内部,一部分在初始化列表。
2.每个成员变量在初始化列表中只能出现一次。
3.每个成员都要在初始化列表初始化,就算不显示在初始化列表写,也要进初始化列表。
- 内置类型如果在初始化列表显示写了,按你写的初始化;没有显示写,有缺省值用缺省值,没缺省值用随机值。(缺省值在定义成员变量的时候给)
- 自定义类型在初始化列表没显示初始化,调默认构造(没有默认构造就报错);显示初始化调构造函数(这个构造函数不要求是默认构造)。
所以成员变量的缺省值是在初始化列表用的 。构造函数参数列表的缺省值在构造函数体内部使用。
4.尽量用初始化列表初始化,因为你用不用都会先走初始化列表。
5.以下成员必须在初始化列表进行显式初始化
const,引用,没有默认构造函数的自定义类型
详细讲解第5点:
1.const成员变量只能用初始化列表进行初始化
原因:const变量必须在定义的时候就初始化,不然就报错。
类里面的成员变量都只是声明
Stack st1;//这才是类对象的定义。对象实例化是整体定义,并调用构造函数初始化。
对象的每个成员什么时候定义初始化?
在初始化列表定义初始化。
所以必须在定义的时候就初始化的成员,必须在初始化列表进行初始化。
比如现在说的const成员,就必须要在定义时就初始化,即必须在初始化列表初始化。
2.自定义类型成员变量,且没有它的默认构造函数
class A
{
public:
A(int a)//A没有默认构造函数。默认构造函数:1.全缺省构造函数2.无参构造函数3.没写构造函数事,编译器自动生成的构造函数
:_a(a)
{}
private :
int _a;
};
class B
{
public:
B()
:_n(10)
, _m(2)
, _aa(11)//【自定义类型,且没默认构造】
{
}
private:
const int _n; // 这里是声明。const成员,必须在定义的时候初始化
int _m = 1; // 初始化列表里,没显示写的时候,有缺省值用缺省值,没缺省值用随机值。
A _aa;//A没有默认构造函数
};
3.引用成员变量
引用和const一样,只能在定义的时候初始化。
所以必须在初始化列表就初始化。
6. 按成员变量的声明顺序在初始化 列表里初始化。
例:
class A
{
public:
A(int a)//初始化列表里先初始化_a2再_a1
:_a1(a)
,_a2(_a1)
{}
void Print()
{
cout <<"_a1="<< _a1 << " " <<"_a2="<< _a2 << endl;
}
private:
int _a2;
int _a1;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
2.explicit关键字
读代码:
//【隐式类型的转换】
class Date
{
public:
Date(int year)
:_year(year)
{}
//日期类的拷贝构造可以不写
private:
int _year;
int _month;
int _day;
};
int main()
{
//之前引用说过
int a = 0;
double b = a;//隐式类型转换,先给临时变量。普通变量的赋值不考虑权限。
const double& c = a;//引用要考虑权限。隐式类型转换,产生临时变量有常性,权限不能放大,所以加const
Date d1(2022);//调构造函数
//隐式类型的转换
Date d2 = 2022;//单参数的构造函数。支持用一个参数构造一个Date类。用2022构造临时对象,再由临时对象拷贝给都d2
const Date& d5 = 2022;//引用的时候要注意权限的放大和缩小。2022先做临时对象的引用,临时变量具有常性,就像是const的,再拷贝构造
Date d3(d1);//拷贝构造
Date d4 = d1;//也是拷贝构造
return 0;
}
Date d2 = 2022;//理论上是先构造,后拷贝构造。现在的编译器已经优化成直接构造了。
const Date& d5 = 2022;//引用也是先构造,后拷贝构造。
单参数,全缺省的构造函数都可以隐式类型转换。
多参数构造函数也能隐式类型转换(c++11拓展支持了,老版本不支持)
class Date
{
public:
Date(int year,int month ,int day)
:_year(year)
,_month(month)
,_day(day)
{}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1 = 2022, 10, 26;//no
Date d2 = (2022, 10, 26);//no
Date d3 = { 2022,10,26 };//先构造后拷贝
Date d4(2022, 10, 26);//直接调构造函数
const Date& d5 = { 2022,10,26 };//必须加const
return 0;
}
如果允许隐式类型转换就不加explicit,不想允许隐式类型转换,构造函数加explicit。
如下:
二、static成员
类对象的传引用传参、传引用返回 和 传值传参、传值返回会不会调构造函数?
int N = 0;
class A
{
public:
//构造函数
A(int a=0)
:_a(a)
{
++N;
}
//拷贝构造
A(const A& aa)
:_a(aa._a)
{
++N;
}
private:
int _a;
};
void F1(A aa)
{
}
void F2(A& aa)
{
}
A F3()
{
A aa;
return aa;
}
A& F4()
{
A aa;//调一次构造函数
return aa;//虽然不能引用返回,因为出了作用域aa会销毁,拿到个随机值。但是编译器不会报错,不做强制检查
}
int main()
{
A aa1(1);//构造
A aa2 = 2;//编译器优化,直接构造
A aa3 = aa1;//拷贝构造
cout << N << endl;//输出3
F1(aa1);//类对象传值传参调拷贝构造
cout << N << endl;//输出4
F2(aa1);//引用传参,不调拷贝构造
cout << N << endl;//继续输出4
F3();//传值返回类对象。函数体内构造一次,返回的时候拷贝构造给临时变量。如果再接收返回值,会再调一次拷贝构造。 cout << N << endl;//输出6
F4();//传引用返回,函数体内部用了一次构造函数
cout << N << endl;//输出7
//类对象的传参和返回值尽量用引用,调拷贝构造效率太低了
return 0;
}
类对象传值返回和传值传参会调用拷贝构造,传引用返回和传引用传参不调用拷贝构造。
来复习一下前面学的东西。
全局变量N谁都能改,没有进行封装。
C++里还是喜欢封装起来用,咱们引出static成员。
static影响生命周期
static修饰局部变量的生命周期变成全局域。
static修饰全局变量只在本源文件内可见,生命周期还是全局域。
类里面的静态成员变量,生命周期是全局的,作用域受类域限制。
static成员的特性:
1. 静态成员存在静态区,而不是类对象里面。被所有相同类的对象共享。
例:
class A
{
public:
//构造函数
A(int a=0)
:_a(a)
{
++N;
}
//拷贝构造
A(const A& aa)
:_a(aa._a)
{
++N;
}
private:
int _a;
static int N;
};
int main()
{
A aa1(1);//构造
A aa2 = 2;
}
aa1和aa2共用同一个N。
aa1有一个_a , aa2有一个_a。在初始化列表初始化自己的成员变量_a没问题。
但是不能在初始化列表初始化static成员N,因为静态成员存在静态区,而不是类对象里面。
在哪初始化静态成员?
类外面。
int A::N = 0;//定义初始化,要把类型带上,指定是A类域里的。类里面可以使用N。
定义时不加static关键字。
例:
class A
{
public:
//构造函数
A(int a=0)
:_a(a)
{
++N;
}
//拷贝构造
A(const A& aa)
:_a(aa._a)
{
++N;
}
private:
int _a;
static int N;
};
//生命周期是全局的,作用域受类域限制
int A::N = 0;//定义初始化,要把类型带上,指定是A类域里的。类里面可以使用N。
***静态成员的访问***
1.如果公有,指定类域就能用
N虽然不再aa1里面,但是算间接指定类域:
用类对象可以访问,那用指针也能访问
用空指针去找静态成员不会报错。因为静态成员不存在类对象里面,空指针没有发生解引用。和空this指针调用成员函数一样。
以上用法的前提是静态成员是公有的。
2.私有的
1.提供一个GetN成员函数
int GetN()
{
return N;
}
有对象的时候可以顺带调用。调用成员函数的时候一定要给this指针传参。
那要是没有定义对象,就是想要访问到N=0的初始值呢,就必须被迫定义一个对象,调一次构造函数,用来计数的N就+1。
2.使用静态成员函数,静态成员函数没有this指针。
也可以用对象调(或者指针)。但这里最妙的是不用对象就可以访问,指定类域就能访问私有的静态成员。
static int GetN()
{
return N;
}
静态成员函数的缺点是,在其函数体内不能访问类的非静态成员(包括非静态属性和方法),因为没有this指针,找不到。函数体内只能访问静态成员。
2. 静态成员函数没有this指针,只能访问静态成员。
如果有一个参数是类对象的引用,那还是能访问这个对象的成员的。
静态成员函数的意义:私有静态成员变量,不用定义类对象,通过指定类域调用静态成员函数就能访问到
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 a[n];//初始化数组的时候调用n次Sum的构造函数
return Sum::GetRet();
}
};
三、友元
友元分为友元函数和友元类。
1.友元函数
函数定义在类外面,是不能访问私有成员的(日期类流插入那里用了友元)。用了友元就可以直接通过对象访问私有成员了。
日期类流插入:没办法把operator<<重载成成员函数。写到类里面第一个参数默认是this指针,所以<<的左操作数一定是自定义的类。用cout就会变成 d._year<<cout。不符合我们的使用习惯和可读性。
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);//友元声明
friend istream& operator>>(istream& in, Date& d);
public:
//简写,省略成员函数喽
private:
int _year;
int _month;
int _day;
}
inline ostream& operator<<(ostream& out, const Date& d)//支持的cout<<d1<<d2
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
inline istream& operator>>(istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
1.友元函数可以在类外访问到类对象的私有成员,不属于成员函数
2.友元函数不能用const修饰
3.友元函数可以在类的任意位置声明,不受类访问限定符限制
4.一个函数可以是多个类的友元
5.友元函数和普通函数的调用原理相同
6.在类的内部声明,声明时要加friend
2.友元类
友元类的所有成员函数都可以访问另一个类对象的私有成员。
1.友元关系是单向的,不具有交换性。
Date是Time的友元,Date里面 通过Time对象 能访问Time的私有成员,Time里不能访问Date的私有成员。
2.友元关系不能传递
C是B的友元,B是A的友元,C不是A的友元。
3.友元关系不能继承
class Time
{
friend class Date;//Date想偷Time家,要到Time里打声招呼。想偷谁就到谁家去打招呼(蛮那个的,嘻嘻)
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=0,int month=0,int day=0)
:_year(year)
,_month(month)
,_day(day)
{}
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;
};
四、内部类(了解一下)
一个类定义在另一个类的内部,这个内部的类就叫内部类。
//相当于两个独立的类
//在类外,B类的访问受A的类域和访问限定符的限制
class A
{
private:
int _a;
public://B天生就是A的友元
class B
{
int _b;
};
};
int main()
{
cout << sizeof(A) << endl;//结果是4.说明A类里面没有B
A aa;
B bb;//不能直接定义B的对象。用一个类的类型,先去局部域搜索,再去全局域搜索,不会到类域里找。
A::B bb;
return 0;
}
把前面那道题改成内部类:
class Solution {
public:
class Sum
{
public:
Sum()
{
_ret+=_i;
++_i;
}
static int GetRet()
{
return _ret;
}
};
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;
五、匿名对象
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
int main()
{
//有名对象
A aa1;
A aa2(1);
A aa3 = 3;//隐式类型转换
A aa();//不能这样定义类对象,会产生歧义。把aa换成func。 A func();//带括号又不传参像个函数声明
//匿名对象。生命周期只在当前这一行。到下一行就会调析构函数
A();
A(3);
return 0;
}
class Solution
{
public:
int Sum_Solution(int n)
{
//....
return n;
}
};
int main()
{
Solution s;
s.Sum_Solution(10);
//创建对象只是为了调一个函数,还要写两行。用匿名对象一行就解决。
Solution().Sum_Solution(10);
return 0;
}
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
A F()
{
/*A ret(10);
return ret;*/
return A(10);//更方便,还会触发优化
}
六、拷贝对象时的一些优化
class A
{
public:
A(int a = 0)
:_a(a)
{}
private:
int _a;
};
void f1(A aa)
{}
void f2(A& aa)
{}
void f3(const A& aa)
{}
A f4()
{
A aa;
return aa;
}
A f5()
{
return A(1);//本来应该是 构造 再拷贝构造给临时对象。临时对象再拷贝构造给ret3
}
int main()
{
//【1】
A aa = 1;//本来是先构造再拷贝构造,优化成直接构造A aa(1)
//【2】传值传参
A aa1(1);//构造
f1(aa1);//拷贝构造
//上面的不能优化,必须分开进行
f1(A(1));//匿名对象 构造加拷贝构造 优化成构造
//【3】传引用传参
f2(1);//生成的临时变量有常性
f2(A(1));//有的编译器认为匿名对象有常性,所以过不了
f3(1);//引用不需要调拷贝构造。不用优化
f3(A(1));
//【4】传值返回
f4();//构造加拷贝构造 没有优化
A ret = f4();//函数里构造 返回的时候调拷贝构造给临时对象,临时对象再拷贝构造给ret。优化后:构造加拷贝构造。优化掉赋值给中间变量,返回的时候直接拷贝构造给ret
A ret2;
ret2 = f4();//这样就不能优化了。因为是在调赋值重载。只有构造和拷贝构造(都是构造)才有可能优化
//【5】
A ret3 = f5();//优化后直接取消用临时对象。也不拷贝了,直接用参数1构造ret3
return 0;
}