1.再谈构造函数
在之前的类和对象(中)我们了解到了构造函数,在创建对象的时候编译器通过调用构造函数,给对象中的各个成员变量赋一个合适的初始值,但是:
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
{}
private:
A _aobj;
int& _ref;
const int _n;
};
运行这段代码发现,这种写法是有问题的,会报错
这是因为在调用构造函数给成员变量赋值的过程本质不是变量的初始化,而是变量的赋值,而变量的初始化是在初始化列表进行的。
1.1初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
以Date类为例:
那么,使用初始化列表需要注意什么?/ 初始化列表的特点是什么?
- 每个成员变量在初始化列表中只能出现一次;
- 类中包含以下成员的时候:
- 引用成员变量,
- const修饰的成员变量,
- 自定义类型成员(且该类没有默认构造函数时);
- 尽量使用初始化列表初始化;
- 成员变量在初始化列表中的初始化顺序是成员变量在类中的声明次序,与初始化列表中的次序无关;
特点讲解
特点一:初始化列表的本质是定义初始化成员变量,初始化只能有一次。
特点二:引用和const修饰的变量在定义的时候必须初始化,并且后面不能再修改;实例化对象的时候,会自动调用默认构造函数,如果没有默认构造函数的话就需要传参,这里括号内的就是参数。
特点三:不管那种类型的成员变量,都是需要走一遍初始化列表的,出于效率的考虑,我们建议使用初始化列表初始化。
特点四:下面这段代码的运行结果是
1 随机值
。class A { public: A(int a) :_a1(a) ,_a2(_a1) {} void Print() { cout<<_a1<<" "<<_a2<<endl; } private: int _a2; int _a1; }; int main() { A aa(1); aa.Print(); }
1.2explicit关键字
explicit的作用:修饰构造函数,禁止(隐式)类型转换。
我们还是以日期类为例:
class Date
{
public:
Date(int year)
:_year(year)
,_month(1)
,_day(1)
{}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << " " << _month << " " << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022);
d1.Print();
d1 = 2023;
d1.Print();
return 0;
}
这段代码是可以正常运行的,原理是在d1.Print() = 2023
这句代码中,使用常量2023
创建了一个类型为Date的临时变量,然后将这个临时变量拷贝给d1。但是如果在构造函数前面加上explicit就禁止了这个类型转换,所以代码将不能运行。
2.static成员
2.1概念
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。静态成员变量一定要在类外进行初始化。
在这里分享一个面试题:实现一个类,计算程序中创建出了多少个类对象
读到这个题目,我们首先先到的应该就是使用一个全局变量或者静态变量,每次创建新的变量的时候使该变量自增。但是使用全局变量的风险是很大的,因为全局变量在任意位置都可以修改,所以我们考虑把这个静态变量放在类里面。
class A { public: A() { N++; } A(const A& t) { N++; } static int GetN() { return N; } private: static int N; }; int A::N = 0; void TestA() { cout << A::GetN() << endl; A a1, a2; A a3(a1); cout << A::GetN() << endl; }
2.2特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用类名::静态成员或者对象.静态成员来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
3.友元
3.1简介
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
友元的分类:友元函数和友元类
3.2友元函数
在类和对象(中)我们讲到了运算符重载的概念,那么现在我们来尝试重载一下流插入(>>)和流提取(<<)。在重载之前,我们需要明白C++中是怎么实现cin和cout流插入、流提取并且能够自动识别类型的。
答案是通过运算符重载和函数重载,cin 和 cout 分别是 istream 和 ostream 类的两个全局对象,而 istream 类中对流提取运算符 >> 进行了运算符重载,osteam 中对流插入运算符 << 进行了运算符重载,所以 cin 和 cout 对象能够完成数据的输入输出;同时,istream 和 ostream 在进行运算符重载时还进行了函数重载,所以其能够自动识别数据类型。
下面以Date类为例:
class Date
{
public:
Date(int year = 1970, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
//流插入
ostream& operator<<(ostream& out) const
{
cout << _year << "/" << _month << "/" << _day;
return out;
}
//流提取
istream& operator>>(istream& in)
{
in >> _year;
in >> _month;
in >> _day;
return in;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
cin >> d;
cout << d;
return 0;
}
这里我们发现一个问题,运算符<<和>>的左操作数必须是本类的对象,所以如果按照这种实现方式的话,我们在使用的时候应该是这样的:d >> cin; d << cout
,但是这样显然违背了我们写运算符重载的初衷:提高代码的可读性。所以为了将右操作数变成本类的对象,我们只能重载为全局函数,然后将ostream和istream的参数放在函数首位,但是如果重载为全局函数我们又会面对一个新问题,那就是无法访问类内部的私有成员。所以出现了友元函数的概念。
友元函数的特征
可以直接访问类的私有成员;
是定义在类外部的普通函数,不属于任何类;
可以在类定义的任何地方声明,不受类访问限定符限制;
需要在类的内部声明,声明时需要加friend关键字;
一个函数可以是多个类的友元函数;
友元函数的调用与普通函数的调用原理相同。
现在我们使用友元修改一下我们重载的对于Date类的<<和>>:
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1970, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
//流插入
ostream& operator<<(ostream& out, const Date& d)
{
cout << d._year << "/" << d._month << "/" << d._day;
return out;
}
//流提取
istream& operator>>(istream& in, Date& d)
{
in >> d._year;
in >> d._month;
in >> d._day;
return in;
}
3.3友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
- 友元关系是单向的,不具有交换性
比如Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递
如果C是B的友元, B是A的友元,则不能说明C时A的友元。
- 友元关系不能继承
class Time
{
friend class Date; //声明日期类为时间类的友元类,则在日期类中就直接访问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 = 1900, int month = 1, int day = 1)
: _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;
};
4.内部类
4.1概念
如果一个类定义在另一个类的内部,这个内部类就叫做内部类。内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去访问内部类的成员。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元,所以内部类可以通过外部类的对象参数来访问外部类的所有成员,但是外部类不是内部类的友元。
4.2特性
- 内部类可以定义在外部类的public、protected、private都是可以的
- 注意内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
sizeof(外部类)=外部类
,和内部类没有任何关系
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b;
b.foo(A());
return 0;
}
5.匿名对象
class A
{
public:
A(int a = 10)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
//创建有名对象的方式
A a1;
A a2(15);
A a3 = 5;
//创建匿名对象,不用取名字
A();
return 0;
}
上述代码的运行结果:
匿名对象的特点:
- 不用取名字;
- 生命周期只有这一行,下一行就会自动调用析构函数
使用场景:
class Solution
{
public:
int Sun_Solution(int n)
{
//...
return n;
}
};
int main()
{
Solution().Sun_Solution(10);//在这种情况下匿名对象就很好用,当然还有一些其他使用场景,这个我们以后遇到了再说
return 0;
}
6.拷贝对象时编译器的优化
优化场景1:隐式类型转换==>构造+拷贝构造–>直接构造
class A
{
public:
A(int a = 10)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
int main()
{
A a1 = 1;
return 0;
}
这个场景的执行过程是:使用1构造了一个临时变量tmp,再使用这个tmp拷贝构造出a1,但是编译器在执行的时候会只能的优化成直接使用1构造a1。
优化场景2:匿名对象传参==>构造+拷贝构造–>直接构造
class A
{
public:
A(int a = 10)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
A(const A& a)
{
_a = a._a;
cout << "A(const A& a)" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
int main()
{
cout << "场景一" << endl;
A a1(1);
f1(a1);
cout << "场景二" << endl;
f1(A(1));
return 0;
}
场景一无法优化,因为两个变量并不能优化成一个,对于场景二,本来是使用1构造一个临时变量,然后传参,拷贝构造出aa,被优化成直接使用1构造aa。
优化场景3:传值返回==>拷贝构造+拷贝构造–>拷贝构造
class A
{
public:
A(int a = 10)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
A(const A& a)
{
_a = a._a;
cout << "A(const A& a)" << endl;
}
private:
int _a;
};
A f2()
{
A aa;
return aa;
}
int main()
{
A ret = f2();
return 0;
}
在传值返回的时候,会用aa拷贝构造出一个临时变量tmp,然后使用tmp拷贝构造出ret,优化成在调用结束之前使用aa直接拷贝构造出ret。
优化场景4:构造+拷贝构造+拷贝构造–>构造
class A
{
public:
A(int a = 10)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
A(const A& a)
{
_a = a._a;
cout << "A(const A& a)" << endl;
}
private:
int _a;
};
A f3()
{
return A(10);
}
int main()
{
A ret = f3();
return 0;
}
这里触发了一个编译器的极致优化:使用10构造出一个临时变量tmp1,将这个临时变量拷贝构造出一个临时变量tmp2用于传值返回,再使用tmp2拷贝构造ret,最终优化成使用10构造ret。
注意:上述的所有优化都建立在一个步骤中,否则可能会引发一些不必要的风险
个临时变量tmp,然后使用tmp拷贝构造出ret,优化成在调用结束之前使用aa直接拷贝构造出ret。
优化场景4:构造+拷贝构造+拷贝构造–>构造
class A
{
public:
A(int a = 10)
:_a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
A(const A& a)
{
_a = a._a;
cout << "A(const A& a)" << endl;
}
private:
int _a;
};
A f3()
{
return A(10);
}
int main()
{
A ret = f3();
return 0;
}
这里触发了一个编译器的极致优化:使用10构造出一个临时变量tmp1,将这个临时变量拷贝构造出一个临时变量tmp2用于传值返回,再使用tmp2拷贝构造ret,最终优化成使用10构造ret。
注意:上述的所有优化都建立在一个步骤中,否则可能会引发一些不必要的风险