<C++>类和对象下|初始化列表|explicit static|友元|内部类|匿名对象|构造函数的优化
文章目录
- 1. 初始化列表
- 2. explicit关键字
- 3. 友元
- 3.1 友元函数
- 3.2 友元类
- 4. static关键字
- 4.1 概念
- 4.2 特性
- 5.内部类
- 5.1 概念
- 5.2 特性
- 6. 匿名对象
- 7. 拷贝构造时的优化
1. 初始化列表
在类的构造函数体中,对成员属性写的操作叫做赋值,那么成员的初始化是在哪里进行呢?
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
//以下全部都是赋值,不是初始化
_year = year;
_month = month;
_day = day;
}
private:
//以下全部都是声明
int _year;
int _month;
int _day;
};
那我们定义对象时,成员属性是在那里定义的呢?
成员属性在初始化列表
中定义。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式
Date(int year = 1, int month = 1, int day = 1)
//成员变量在初始化列表中定义
:_year(year)
,_month(month)
,_day(day)
{
//以下全部都是赋值,不是初始化
_year = year;
_month = month;
_day = day;
}
上述是我们显示写的初始化列表,若没有显示写,初始化列表会将内置类型变量设为初始值
显示写初始化列表:
注意:
-
每个成员变量只能初始化一次,只能在初始化列表中出现一次
-
类中若包含以下成员,必须放在初始化列表中进行初始化
-
引用成员变量
-
const成员变量
-
无默认构造函数的自定义类型成员变量
引用和const变量都需要在定义时初始化,若自定义类型对象无默认构造函数则必须在初始化列表中显示传参调用构造函数。
-
总结: 成员属性的初始化是在初始化列表中完成的,若没有写初始化列表则默认以随机值初始化内置类型,自定义类型变量若有默认构造函数则会调用默认构造函数;构造函数体内完成的是对成员属性的二次赋值。
**注意:**C++11打的补丁在声明时为变量设置缺省值,本质上就是在初始化列表中为成员设置初始值。
2. explicit关键字
class A
{
public:
//单参数的构造函数可以发生隐式类型转换
A(int a) :_a(a)
{
cout << "A(int a)\n";
}
private:
int _a;
};
int main()
{
A a1(1);//调用构造函数
//类型不匹配时内置类型会隐式转换为自定义类型 即1转换为A(1) 再通过拷贝构造函数用A(1)构造a3
//支持类型转换的前提是A具有单参数构造函数
A a3 = 1
const A& ref = 1;//将ref绑定构造出来的临时对象,延长了临时对象的生命周期
}
如果加上
explicit关键字
则不会发生隐式类型转换(不影响显式类型转换)
对于多参数构造函数,C++98及以前不支持隐式类型转换,C++11以后支持了
class B
{
public:
//C++11支持多参数构造函数的隐式转换
B(int b1, int b2)
: _b1(b1)
, _b2(b2)
{
cout << "B(int,int)\n";
}
private:
int _b1;
int _b2;
};
int main()
{
B b1(1, 2);//构造函数
B b2 = { 1,2 };//隐式类型转换为B tmp(1,2),在将tmp拷贝给b2,编译器可能会优化为直接构造
const B& rb = {1, 2};//rb引用的是临时对象tmp(1,2)
return 0;
}
运行结果
3. 友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元分为:友元函数和友元类
3.1 友元函数
当我们想要重载操作符<<
时,没有办法重载为成员函数,因为成员函数的第一个操作数为this指针,因此<<
的左操作数不是cout,解决该方法只有将<<
重载为全局函数,重载为全局函数时第一个参数类型为ostream&
,第二个参数就是需要操作的对象类型,举例Date类
,<<运算符重载
的定义应该是如下
ostream& operator<<(ostream& out, const Date& date)
{
out << date._year << "/" << date._month << date._day << endl;
}
这里我们需要在函数体中访问Date类的私有成员
,可以将operator<<
定义为Date
类的友元函数
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
friend ostream& operator<<(ostream& out, const Date& date);//声明为友元函数,该函数可以访问Date的私有成员
public:
Date(int year = 2023, int month = 7, int day = 30)
:_year(year)
, _month(month)
, _day(day)
{
}
private:
int _year;
int _month;
int _day;
};
注意:
-
友元函数可访问类的私有和保护成员,但不是类的成员函数
-
友元函数不能用const修饰(没有this指针)
-
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
-
一个函数可以是多个类的友元函数
-
友元函数的调用与普通函数的调用原理相同
3.2 友元类
A类在B类中被声明为友元的,称A类是B类的友元类,A类中可以访问B类的私有成员。
class Time
{
friend class Date;
public:
Time(int hour = 0, int minute = 0, int sec = 0)
:_hour(hour)
,_minute(minute)
,_sec(sec)
{
}
private:
int _hour;
int _minute;
int _sec;
};
class Date
{
friend ostream& operator<<(ostream& out, const Date& date);//声明为友元函数,该函数可以访问Date的私有成员
public:
Date(int year = 2023, int month = 7, int day = 30)
:_year(year)
, _month(month)
, _day(day)
{
}
void SetTime(int hour, int minute, int sec)
{
//访问Time类的私有成员必须将Date类声明为Time类的友元类
_t._hour = hour;
_t._minute = minute;
_t._sec = sec;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
注意:
- 友元关系是单向的,不具有交换性。比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承,在继承位置再给大家详细介绍。
- 友元是一种高耦合的状态,如果一个函数的成员改变了可能会影响到与之相关的友元函数
4. static关键字
4.1 概念
声明为static
的成员称为类的静态成员,static
修饰类成员属性则称该属性为静态成员变量,static
修饰类成员函数称该函数为静态成员函数。
设计一个类,统计该类创建过多少个对象和当前存在的对象个数
class A { public: A(int a = 1) { m++; n++; } ~A() { n--; } A(const A& a) { m++; n++; } static int m;//记录创建对象的个数 static int n;//记录当前存在对象的个数 }; int A::m = 0; int A::n = 0; A fun(A tmp) { return tmp; } int main() { A a1(1); A a2(2); fun(a1); cout << A::m <<" " << A::n << endl;//访问静态成员变量时需要指定类域 }
上述设计可以完成任务,但是静态成员变量m和n是public
的,因此我们在类外部可以直接修改导致结果误差,可以将static成员属性设置为private
,对外部提供一个静态成员函数来获取静态成员变量
class A
{
public:
A(int a = 1)
{
m++;
n++;
}
~A()
{
n--;
}
A(const A& a)
{
m++;
n++;
}
static int GetM()
{
return m;
}
static int GetN()
{
return n;
}
private:
static int m;//记录创建对象的个数
static int n;//记录当前存在对象的个数
};
int A::m = 0;
int A::n = 0;
A fun(A tmp)
{
return tmp;
}
int main()
{
A a1(1);
A a2(2);
fun(a1);
//cout << A::m <<" " << A::n << endl;
cout << A::GetM() << " " << A::GetN() << endl;//调用静态成员函数时需要指定类域
}
4.2 特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区,类似于成员函数存放在公共代码区。
- 静态成员变量必须在类外部定义,定义时不添加static关键字,类中只是声明(因为静态变量不属于对象,所以不会调用构造函数在初始化列表中定义)
- 类静态成员名即可用类名::静态成员 或者 对象.静态名 来访问
- 静态成员没有this指针,不可以访问任何非静态成员
sizeof
不会计算静态成员的大小- 静态成员也是类的成员,受
public、protected、private
访问限定符的限定 - 静态成员函数不可以调用非静态成员函数
- 非静态成员函数可以调用静态成员函数
5.内部类
5.1 概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象访问内部类的成员。外部类对内部类没有任何优越的访问权限。
内部类是外部类的友元,内部类中可以通过外部类对象访问外部类的私有成员。外部类不是内部类的友元
class A
{
public:
class B //B是A的内部类
{
public:
void fun(A& a)
{
a._a = 1;//B是A的友元,可以访问A的私有成员
_b = 2;
s_member = 2;//内部类和成员函数一样可以直接访问静态数据成员
}
private:
int _b;
};
void SetA(int a)
{
_a = a;
}
int GetStaticMember()
{
return s_member;
}
private:
static int s_member;//声明静态成员变量
int _a;
};
int A::s_member = 1;//静态成员变量定义在类的外部
int main()
{
A a;
a.SetA(10);
A::B b;//想要使用内部类必须先指定外部类域
b.fun(a);//内部类可以访问外部类类的private成员
return 0;
}
静态成员变量不在对象中,因此静态成员变量不能在初始化列表中初始化,需要在类外部通过
类域::变量名
初始化
5.2 特性
- 内部类定义为public、protected、private都是可以的。
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名。
- 内部类只是被封装了(需要通过外部类访问),内部类不属于外部类
- 内部类是外部类的友元类
练习
class Solution { public: class Sum//Sum是Solution类的友元,Sum内部可以访问Solution的私有成员 { public: Sum() { _ret += _i; _i++; } }; int Sum_Solution(int n) { Sum s[n]; return _ret; } private: static int _ret;//Solution类的静态成员:用来记录结果 static int _i;//Solution类的静态成员:用来记录当前加法因子 }; int Solution::_ret = 0; int Solution::_i = 1;
6. 匿名对象
匿名对象是没有名字的对象,例如上述class A
可以通过A();
定义一个匿名对象,匿名对象的生命周期只有它所在的一行,定义完后立马会调用析构函数
class A
{
public:
A(int a = 1)
{
_a = a;
cout << "A()->" << _a << endl;
}
~A()
{
cout << "~A()->" << _a << endl;
}
void Print()
{
cout << "void Print()\n";
}
private:
int _a;
};
int main()
{
A();//调用构造函数后立马调用析构函数
return 0;
}
运行结果
匿名对象和正常对象一样可以调用函数、传参,仅仅生命周期与普通对象不同而已
匿名对象可以调用函数
A().Print();
匿名对象可以传参
void fun(const A& a) { a.Print(); } int main() { //A();//调用构造函数后立马调用析构函数 //A().Print(); fun(A(2)); return 0; }
注意:匿名对象和临时对象一样具有常性,需要使用常引用来绑定匿名对象,相应的Print成员函数需要定义为const成员函数。
7. 拷贝构造时的优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
不同版本的编译器所作的优化不同,下面介绍主流编译器对于拷贝构造时常见的优化
同一个表达式中,连续的构造函数+构造函数
/构造函数+拷贝构造函数
/拷贝构造函数+拷贝构造函数
会合并为一个构造函数/拷贝构造函数
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << _a << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << _a << 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;
};
-
构造函数+拷贝构造函数->构造函数
int main() { A a = 3;//构造+拷贝构造->构造 return 0; }
运行结果:
编译器先用3构造临时对象,将临时对象拷贝构造给a,优化为直接用调用构造函数构造a
void f1(A aa) {} int main() { f1(A(2));//构造+拷贝构造->构造 }
void f1(A aa) {} int main() { f1(3);//构造(隐式类型转换)+拷贝->构造 }
-
拷贝构造函数+拷贝构造函数->拷贝构造函数
aa拷贝给临时变量,临时变量拷贝给a优化为aa拷贝给aA f2() { A aa(1); return aa;//返回时会调用拷贝构造函数 } int main() { A a = f2();//拷贝构造+拷贝构造->拷贝构造 return 0; }
注意:若编译器太新或在release
版本下对构造函数的优化可能更极端,可以跨表达式进行合并优化.