目录
一、初始化列表
explicit关键字
匿名对象的使用
二、static成员
static练习题
描述
对象在内存中的生成位置
三、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;
};
但是如果我们需要对一个引用类型的参数赋值,就会产生如下的情况
在我们的date类中含有time类的对象,当我们想要给我们的date对象的time中的属性赋值的时候就需要像下面这样做
#include
#include
using namespace std;
class Time
{
public:
Time(int hour)
{
_hour = hour;
}
Time() {}
private:
int _hour;
};
class Date
{
public:
Date(int year, int hour)
{
// 函数体内初始化
_year = year;
//调用上面time中的带参的构造器创建一个time对象
Time t(hour);
//然后将这个创建的对象赋给我们的当前对象的_t
_t = t;
}
private:
int _year;
Time _t;
};
int main()
{
Date d(2022, 1);
return 0;
}
可见上面那种方法需要先生成一个time对象然后将生成的time对象赋值再拷贝给我们当前date对象的time参数非常的麻烦。
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
我们就可以采用我们初始化列表的形式来给我们的参数赋值。
#include
#include
using namespace std;
class Time
{
public:
Time(int hour)
{
_hour = hour;
}
Time() {}
private:
int _hour;
};
class Date
{
public:
// 要初始化_t 对象,必须通过初始化列表
//使用初始化列表的方式给我们的对象进行初始化,第一个参数前用冒号,后面对每一个参数
//赋值时用逗号分隔。
Date(int year, int hour)
:_t(hour)
,_year(year)
{
}
private:
int _year;
Time _t;
};
int main()
{
Date d(2022, 1);
return 0;
}
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
结论:自定义类型成员,推荐用初始化列表初始化
初始化列表可以认为是成员变量定义的地方
【注意】
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
①引用成员变量
②const成员变量
③自定义类型成员(该类没有默认构造函数)
但是并不是任何情况下初始化列表都是对我们对象初始化的最佳方式
在们下面的代码中,我们想要给我们对象中的数组开辟一块空间,而如果是直接在初始化列表中动态开辟我们的数组空间会非常麻烦,还是在函数体内初始化会更加方便。
#include
class A
{
public:
// 有些初始化工作还是必须在函数体内完成
A(int N)
:_N(N)
{
_a = (int*)malloc(sizeof(int)*N);
if (_a == NULL)
{
perror("malloc fail");
}
memset(_a, 0, sizeof(int)*N);
}
private:
// 声明
int* _a;
int _N;
};
int main()
{
A aa(10);
return 0;
}
所以我们的结论是:
1.自定义类型成员,推荐使用初始化列表初始化。 初始化列表可以认为是成员变量定义的地方。
2.内置类型也推荐使用初始化列表,当然内置类型在函数体内初始化也没有什么问题。
统一的建议:能使用初始化列表就使用,使用初始化列表基本没有什么问题。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a1(a)
,_a2(_a1)
{}
void Print() {
cout<<_a1<<" "<<_a2<
从我们的测试结果看来,我们的程序并没有先给我们的_a1赋值,然后再将我们的_a1的值赋给我们的_a2。在vs2019的编译条件下,可能会出现_a1为1,_a2的值为随机值的情况,在vscode,以及clion的环境下会直接报错,提示这个_a1并没有初始化。因为在我们的private中_a2的声明在_a1的上面,所以先要初始化_a2,直到_a2被初始化了之后才会初始化_a1。
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
// explicit关键字 + 匿名对象
class Date
{
public:
Date(int year)
:_year(year)
{
cout << " Date(int year)" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
};
void func(const string& s)
{}
class Solution {
public:
int Sum_Solution(int n) {
return 0;
}
};
int main()
{
Date d1(2022); // 直接调用构造
Date d2 = 2022; // 隐式类型转换:构造 + 拷贝构造 + 编译器优化 -》直接调用构造
const Date& d3 = 2022;
return 0;
}
在上面的代码中,我们的d2的赋值就是通过隐式类型转换的方式,实际编译器背后会用2022构造一个无名对象,最后用无名对象给d2对象进行赋值
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
// explicit关键字 + 匿名对象
class Date
{
public:
explicit Date(int year)
:_year(year)
{
cout << " Date(int year)" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
};
void func(const string& s)
{}
class Solution {
public:
int Sum_Solution(int n) {
// ...
return 0;
}
};
int main()
{
Date d1(2022); // 直接调用构造
Date d2 = 2022; // 隐式类型转换:构造 + 拷贝构造 + 编译器优化 -》直接调用构造
const Date& d3 = 2022;
return 0;
}
在我们本次的测试代码中,我们的编译器会报出下面的错误。这就是因为我们隐式类型转换被禁止了,所以我们的d2和d3的传参方式没办法通过隐式类型转换转换成一个无名对象然后赋给我们的d2和d3。
匿名对象的使用
匿名对象的具体的名字给省略掉就像date(1000),我们的date对象没有名字,并且这个对象的生命周期只有这一行,也就是说用了一次就不能用了。
这样的匿名函数有什么用呢?
当我们想要调用类中某一个具体的方法的时候,我们就可以使用匿名对象来快速实现。
// explicit关键字 + 匿名对象
class Date
{
public:
Date(int year)
:_year(year)
{
cout << " Date(int year)" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
};
void func(const string& s)
{}
class Solution {
public:
int Sum_Solution(int n) {
// ...
return 0;
}
};
int main()
{
// 匿名对象 -- 声明周期只有这一行
Date(2000);
Date d4(2000);
// 匿名对象 一些使用场景
Solution slt;
slt.Sum_Solution(10);
Solution().Sum_Solution(10);
string s1("hello");
string s2 = "hello";
string str("insert");
func(str);
func("insert");
return 0;
}
从我们下面的测试结果中可以看出我们的匿名函数在这一行生成之后就马上被销毁了。
二、static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
静态的成员变量一定要在类外进行初始化。
1. 静态成员为所有类对象所共享,不属于某个具体的实例
2. 静态成员变量必须在类外定义,定义时不添加static关键字
3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员和类的普通成员一样,也有public、protected、private 3种访问级别,也可以具有返回值
class A
{
public:
A()
{ ++_scount; }
//在类的内部是可以调用类的静态变量的
A(const A& t) { ++_scount; }
// 静态成员函数 -- 没有this指针
//因为我们的静态成员函数在我们对象生成之前就会存在了
//这时我们连对象都没有所以没办法用this指向具体的对象
static int GetCount()
{
return _scount;
}
private:
int _a;
// 静态成员变量,属于整个类,生命周期整个程序运行期间,存在静态取
static int _scount; // 声明
};
// 类外面定义初始化
int A::_scount = 0;
int main()
{
//从上面的程序定义中我们可以看到我们每定义一次我们的对象,
//我们的静态变量就会++,所以在++三次之后就会得到我们的结果3.
A a1;
A a2;
A a3(a2);
//由于_scout的权限是private,所以我们无法获得。
// cout << a1._scount << endl;
// cout << a2._scount << endl;
// cout << A::_scount << endl;
//可以通过静态成员函数来调用我们的静态成员变量
cout <
【问题】
1. 静态成员函数可以调用非静态成员函数吗?
静态成员函数不可以调用非静态成员函数。因为我们的静态成员函数在我们的非静态成员生成之前就已经存在了(非静态的成员函数在编译过之后就存在了,不需要生成具体的对象之后才会存在。),所以我们的静态成员函数虽然想要调用非静态成员函数,但是我们的非静态成员函数根本就还没有生成。
2. 非静态成员函数可以调用类的静态成员函数吗?
按照我们上面的说法,如果我们的非静态成员函数都已经生成了,那么我们的静态成员函数已经被加载到内存中了,当然可以调用。
当知道了static静态成员函数之后我们就可以修改之前我们写的这道代码题了。
static练习题
求1+2+3+...+n_牛客题霸_牛客网
描述
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
数据范围: 0 进阶: 空间复杂度 O(1)O(1) ,时间复杂度 O(n)O(n)
int i=1;
int sum=0;
class Sum
{
public :
Sum()
{
sum +=i;
++i;
}
};
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];//调用n此sum的构造函数
return sum;
}
};
这里我们是通过调用n次Sum的构造函数来实现 我们具体数值的累加。
我们原本的策略是通过全局变量的方式,现在我们可以改进成使用静态变量的形式。
class Sum
{
public :
Sum()
{
_sum +=_i;
++_i;
}
static int GetSum()
{
return _sum;
}
private:
static int _sum;
static int _i;
};
//静态成员变量必须在类外面赋值
int Sum::_sum=0;
int Sum::_i=1;
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];
return Sum::GetSum();
}
};
在上面的代码中我们在sum类的内部声明了静态变量,并且通过静态变量实现了我们数据的累加。
对象在内存中的生成位置
// 设计一个只能在栈上定义对象的类
class StackOnly
{
public:
static StackOnly CreateObj()
{
StackOnly so;
return so;
}
private:
StackOnly(int x = 0, int y = 0)
:_x(x)
, _y(0)
{}
private:
int _x = 0;
int _y = 0;
};
int main()
{
//如果我们直接调用类的构造器生成的对象将在我们的栈上
// StackOnly so1; // 栈
//如果直接生成静态的类的对象的话,我们的生成的对象将在我们的静态区中。
// static StackOnly so2; // 静态区
//栈
StackOnly so3 = StackOnly::CreateObj();
return 0;
}
三、C++11 的成员初始化新方法
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。
class B
{
public:
B(int b = 0)
:_b(b)
{}
int _b;
};
class A
{
public:
void Print()
{
cout << a << endl;
cout << b._b<< endl;
cout << p << endl;
}
private:
// 非静态成员变量,可以在成员声明时给缺省值。
int a = 10;
B b = 20;
int* p = (int*)malloc(4);
static int n;
};
int A::n = 10;
int main()
{
A a;
a.Print();
return 0;
}
四、友元
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。
友元函数
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& _cout)
{
_cout<_year<<"-"<_month<<"-"<_day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2017, 12, 24);
d<
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(){
}
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout<>(istream& _cin, Date& d)
{
_cin>>d._year;
_cin>>d._month;
_cin>>d._day;
return _cin;
}
int main()
{
Date d;
cin>>d;
cout<
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用和原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
class Date; // 前置声明
class Time
{
friend class Date;
// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
//注意这里的仅仅是日期类能够访问Time中的全部变量和方法,但是Time并不能访问Date中的私有变量和方法。
public:
Time(){
}
Time(int hour, int minute, int second)
: _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;
};
int main()
{
Date d(2017, 12, 24);
return 0;
}
五、内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系
#include
using namespace std;
#include
// 内部类
class A
{
private:
int _h;
static int k;
public:
// B定义在A的里面
// 1、受A的类域限制,访问限定符
// 2、B天生是A的友元
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a._h << endl;//OK -- 友元
}
private:
int _b;
};
};
int A::k = 1;
int main()
{
cout << sizeof(A) << endl; // 4
A a;
A::B b;
b.foo(a);
return 0;
}
静态的成员变量并不会影响整个类的大小,所以我们的a类的大小就是那个_h,也就是一个int的大小
第二行的1就是我们的静态变量
第三行的随机值是因为我们没有对_h进行初始化。
六、练习分析
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
//如果是拷贝赋值就打印如下的信息
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
void f1(W w)
{
}
void f2(const W& w)
{
}
int main()
{
W w1;
f1(w1);
f2(w1);
cout << endl << endl;
f1(W()); // 本来构造+拷贝构造--编译器的优化--直接构造
// 结论:连续一个表达式步骤中,连续构造一般都会优化 -- 合二为一
W w2 = 1;
return 0;
}
从我们下面的测试结果我们可以进行分析。
第一行的w()也就是我们W w1构造时打印的
第二行的W(const W& w)是我们将调用f1时生成的,由于我们所采用的并不是引用传值,所以我们的编译器会生成一份拷贝给我们的f1,所以就有了这一行的打印数据。
第三行的~W()是拷贝给f1的w对象销毁的时候打印的。
由于我们的f2采用的是引用传值的方式所欲不会形成拷贝,所以也就没有打印。
下面我们用换行的方式分隔了我们的测试代码
f1(w())原本应该是先构造一个w再拷贝构造给我们的f1的,但是由于我们的编译器的优化,这里就直接将拷贝的值传递给我们的f1了,所以在w()构建之后紧接着就是~W()的销毁。
W w2=1也是如此,本应该先生成一个无名的w对象赋值给我们的w2的,但是由于编译器的优化就直接将生成的值传递给w2了。所以在w()构建之后紧接着就是~W()的销毁
最后一行是我们一开始生成的w1的销毁函数。
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
W f3()
{
W ret;
return ret;
}
// 《深度探索C++对象模型》
int main()
{
f3(); // 1次构造 1次拷贝
cout << endl << endl;
W w1 = f3(); // 本来:1次构造 2次拷贝 -- 优化:1次构造 1次拷贝
cout << endl << endl;
W w2;
w2 = f3(); // 本来:1次构造 1次拷贝 1次赋值
return 0;
}
第一行和第二行的构造w和销毁w时由于我们的f3()没有接收,所以在生成之后马上就销毁了。
第三行的W() 本来应该是由我们的f3()生成一个w类的对象,然后再return的时候将这个生成的w类的对象以拷贝的形式返回,然后再将我们的返回值以拷贝的形式赋给w1,所以我们一共是一次构造,两次拷贝,但是我们的clion编译器在这里优化了,直接将我们生成的ret给了w1,期间并没有拷贝构造。然后这个对象的销毁是在最后一行的。
第四行的w()是由于w2的创建所以打印出来的。
第五行的w()是由于f3()中构建的w的对象ret打印出来的。
第六行的拷贝函数,是因为我们的f3()在赋值给w2的时候打印出来的,然后分别是这两个对象的销毁。
最后一行的~W()是我们一开始的w1的销毁。
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
W f(W u)
{
//cout << "--------" << endl;
W v(u);
W w = v;
//cout << "--------" << endl;
return w;
}
int main()
{
W x;
W y = f(x); // 1次构造 4次拷贝
return 0;
}
这里的话,我们先创建了一个w类的对象x,所以我们会打印出一个W(),也就是一次构造函数。
然后f(x),就是将我们的x拷贝到f()方法中,这是第一次拷贝构造。
然后W v(u),就是将我们的u拷贝构造给v,这是第二次拷贝构造。
然后将我们的v赋给w,也就是我们的第三次拷贝构造。
最后我们将返回的w直接拷贝构造给y,也就是我们的第四次拷贝构造。
但是由于我们这里的编译器的优化,编译器直接把返回的w直接引用给了y,没有拷贝构造,所以只有打印出来三次拷贝构造。
我们的拷贝构造的次序也就是如同我们下面的图。
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
void f1(W w)
{
}
void f2(const W& w)
{
}
W f(W u)
{
//cout << "--------" << endl;
W v(u);
W w = v;
//cout << "--------" << endl;
return w;
}
int main()
{
W x;
W y = f(f(x)); // 1次构造 7次拷贝 or 1次构造 5次拷贝
return 0;
}
我们先来解释为什么是7次拷贝构造
这里我们代码和上面一次的不同的是f(x)变成了f(f(x)),所以我们之前的四次的拷贝构造不再进行解释 ,直接快进到返回w给f(f(x))中的外层f(X),这是第四次拷贝构造。
然后我们W v(u)将我们的w拷贝构造给了v也就是第五次拷贝构造
然后我们W x=v将我们的v拷贝构造给了w,也就是我们的第六次拷贝构造。
最后我们将w返回给y,也就是我们的第七次拷贝构造。
由于编译器的优化的关系,我们只需要五次拷贝构造就能够实现完上面的代码。
一个是我们的第四次的拷贝构造如果直接将结果直接引用给了f(f(x))的外层f(x),这一次的拷贝构造就省去了。
还有一个是我们的第七次的拷贝构造,如果我们的f(f(x))将最后的返回值直接引用给了y,中间没有生成临时的拷贝,就省去了一次拷贝构造。
所以在经过编译器优化之后就仅剩下五次拷贝构造了。
以下就是我们七次拷贝构造的先后顺序
优化的原理
这是优化之前的模式
这是优化之后的模式
对于构造器函数,我们之前是通过如下的方法来为我们对象中的参数赋值。也就是带参的构造器来将我们的数据传入,从而给为我们的对象赋值,但是这种赋值方式在一定程度上存在一些问题。
但是如果我们需要对一个引用类型的参数赋值,就会产生如下的情况
在我们的date类中含有time类的对象,当我们想要给我们的date对象的time中的属性赋值的时候就需要像下面这样做
可见上面那种方法需要先生成一个time对象然后将生成的time对象赋值再拷贝给我们当前date对象的time参数非常的麻烦。
虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值。
我们就可以采用我们初始化列表的形式来给我们的参数赋值。
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式。
【注意】
1. 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2. 类中包含以下成员,必须放在初始化列表位置进行初始化:
①引用成员变量
②const成员变量
③自定义类型成员(该类没有默认构造函数)
但是并不是任何情况下初始化列表都是对我们对象初始化的最佳方式
在们下面的代码中,我们想要给我们对象中的数组开辟一块空间,而如果是直接在初始化列表中动态开辟我们的数组空间会非常麻烦,还是在函数体内初始化会更加方便。
所以我们的结论是:
1.自定义类型成员,推荐使用初始化列表初始化。 初始化列表可以认为是成员变量定义的地方。
2.内置类型也推荐使用初始化列表,当然内置类型在函数体内初始化也没有什么问题。
统一的建议:能使用初始化列表就使用,使用初始化列表基本没有什么问题。
成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
从我们的测试结果看来,我们的程序并没有先给我们的_a1赋值,然后再将我们的_a1的值赋给我们的_a2。在vs2019的编译条件下,可能会出现_a1为1,_a2的值为随机值的情况,在vscode,以及clion的环境下会直接报错,提示这个_a1并没有初始化。因为在我们的private中_a2的声明在_a1的上面,所以先要初始化_a2,直到_a2被初始化了之后才会初始化_a1。
explicit关键字
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
// explicit关键字 + 匿名对象
class Date
{
public:
Date(int year)
:_year(year)
{
cout << " Date(int year)" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
};
void func(const string& s)
{}
class Solution {
public:
int Sum_Solution(int n) {
return 0;
}
};
int main()
{
Date d1(2022); // 直接调用构造
Date d2 = 2022; // 隐式类型转换:构造 + 拷贝构造 + 编译器优化 -》直接调用构造
const Date& d3 = 2022;
return 0;
}
在上面的代码中,我们的d2的赋值就是通过隐式类型转换的方式,实际编译器背后会用2022构造一个无名对象,最后用无名对象给d2对象进行赋值
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
// explicit关键字 + 匿名对象
class Date
{
public:
explicit Date(int year)
:_year(year)
{
cout << " Date(int year)" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
};
void func(const string& s)
{}
class Solution {
public:
int Sum_Solution(int n) {
// ...
return 0;
}
};
int main()
{
Date d1(2022); // 直接调用构造
Date d2 = 2022; // 隐式类型转换:构造 + 拷贝构造 + 编译器优化 -》直接调用构造
const Date& d3 = 2022;
return 0;
}
在我们本次的测试代码中,我们的编译器会报出下面的错误。这就是因为我们隐式类型转换被禁止了,所以我们的d2和d3的传参方式没办法通过隐式类型转换转换成一个无名对象然后赋给我们的d2和d3。
构造函数不仅可以构造与初始化对象,对于单个参数的构造函数,还具有类型转换的作用。
在上面的代码中,我们的d2的赋值就是通过隐式类型转换的方式,实际编译器背后会用2022构造一个无名对象,最后用无名对象给d2对象进行赋值
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。
在我们本次的测试代码中,我们的编译器会报出下面的错误。这就是因为我们隐式类型转换被禁止了,所以我们的d2和d3的传参方式没办法通过隐式类型转换转换成一个无名对象然后赋给我们的d2和d3。
匿名对象的使用
匿名对象的具体的名字给省略掉就像date(1000),我们的date对象没有名字,并且这个对象的生命周期只有这一行,也就是说用了一次就不能用了。
这样的匿名函数有什么用呢?
当我们想要调用类中某一个具体的方法的时候,我们就可以使用匿名对象来快速实现。
// explicit关键字 + 匿名对象
class Date
{
public:
Date(int year)
:_year(year)
{
cout << " Date(int year)" << endl;
}
Date(const Date& d)
{
cout << "Date(const Date& d)" << endl;
}
~Date()
{
cout << "~Date()" << endl;
}
private:
int _year;
};
void func(const string& s)
{}
class Solution {
public:
int Sum_Solution(int n) {
// ...
return 0;
}
};
int main()
{
// 匿名对象 -- 声明周期只有这一行
Date(2000);
Date d4(2000);
// 匿名对象 一些使用场景
Solution slt;
slt.Sum_Solution(10);
Solution().Sum_Solution(10);
string s1("hello");
string s2 = "hello";
string str("insert");
func(str);
func("insert");
return 0;
}
从我们下面的测试结果中可以看出我们的匿名函数在这一行生成之后就马上被销毁了。
匿名对象的具体的名字给省略掉就像date(1000),我们的date对象没有名字,并且这个对象的生命周期只有这一行,也就是说用了一次就不能用了。
这样的匿名函数有什么用呢?
当我们想要调用类中某一个具体的方法的时候,我们就可以使用匿名对象来快速实现。
从我们下面的测试结果中可以看出我们的匿名函数在这一行生成之后就马上被销毁了。
二、static成员
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
静态的成员变量一定要在类外进行初始化。
1. 静态成员为所有类对象所共享,不属于某个具体的实例
2. 静态成员变量必须在类外定义,定义时不添加static关键字
3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员和类的普通成员一样,也有public、protected、private 3种访问级别,也可以具有返回值
class A
{
public:
A()
{ ++_scount; }
//在类的内部是可以调用类的静态变量的
A(const A& t) { ++_scount; }
// 静态成员函数 -- 没有this指针
//因为我们的静态成员函数在我们对象生成之前就会存在了
//这时我们连对象都没有所以没办法用this指向具体的对象
static int GetCount()
{
return _scount;
}
private:
int _a;
// 静态成员变量,属于整个类,生命周期整个程序运行期间,存在静态取
static int _scount; // 声明
};
// 类外面定义初始化
int A::_scount = 0;
int main()
{
//从上面的程序定义中我们可以看到我们每定义一次我们的对象,
//我们的静态变量就会++,所以在++三次之后就会得到我们的结果3.
A a1;
A a2;
A a3(a2);
//由于_scout的权限是private,所以我们无法获得。
// cout << a1._scount << endl;
// cout << a2._scount << endl;
// cout << A::_scount << endl;
//可以通过静态成员函数来调用我们的静态成员变量
cout <
声明为static的类成员称为类的静态成员,用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
静态的成员变量一定要在类外进行初始化。
1. 静态成员为所有类对象所共享,不属于某个具体的实例
2. 静态成员变量必须在类外定义,定义时不添加static关键字
3. 类静态成员即可用类名::静态成员或者对象.静态成员来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员和类的普通成员一样,也有public、protected、private 3种访问级别,也可以具有返回值
【问题】
1. 静态成员函数可以调用非静态成员函数吗?静态成员函数不可以调用非静态成员函数。因为我们的静态成员函数在我们的非静态成员生成之前就已经存在了(非静态的成员函数在编译过之后就存在了,不需要生成具体的对象之后才会存在。),所以我们的静态成员函数虽然想要调用非静态成员函数,但是我们的非静态成员函数根本就还没有生成。
2. 非静态成员函数可以调用类的静态成员函数吗?按照我们上面的说法,如果我们的非静态成员函数都已经生成了,那么我们的静态成员函数已经被加载到内存中了,当然可以调用。
当知道了static静态成员函数之后我们就可以修改之前我们写的这道代码题了。
static练习题
求1+2+3+...+n_牛客题霸_牛客网
描述
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
数据范围: 0 进阶: 空间复杂度 O(1)O(1) ,时间复杂度 O(n)O(n)
int i=1;
int sum=0;
class Sum
{
public :
Sum()
{
sum +=i;
++i;
}
};
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];//调用n此sum的构造函数
return sum;
}
};
这里我们是通过调用n次Sum的构造函数来实现 我们具体数值的累加。
我们原本的策略是通过全局变量的方式,现在我们可以改进成使用静态变量的形式。
class Sum
{
public :
Sum()
{
_sum +=_i;
++_i;
}
static int GetSum()
{
return _sum;
}
private:
static int _sum;
static int _i;
};
//静态成员变量必须在类外面赋值
int Sum::_sum=0;
int Sum::_i=1;
class Solution {
public:
int Sum_Solution(int n) {
Sum a[n];
return Sum::GetSum();
}
};
在上面的代码中我们在sum类的内部声明了静态变量,并且通过静态变量实现了我们数据的累加。
对象在内存中的生成位置
// 设计一个只能在栈上定义对象的类
class StackOnly
{
public:
static StackOnly CreateObj()
{
StackOnly so;
return so;
}
private:
StackOnly(int x = 0, int y = 0)
:_x(x)
, _y(0)
{}
private:
int _x = 0;
int _y = 0;
};
int main()
{
//如果我们直接调用类的构造器生成的对象将在我们的栈上
// StackOnly so1; // 栈
//如果直接生成静态的类的对象的话,我们的生成的对象将在我们的静态区中。
// static StackOnly so2; // 静态区
//栈
StackOnly so3 = StackOnly::CreateObj();
return 0;
}
三、C++11 的成员初始化新方法
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。
class B
{
public:
B(int b = 0)
:_b(b)
{}
int _b;
};
class A
{
public:
void Print()
{
cout << a << endl;
cout << b._b<< endl;
cout << p << endl;
}
private:
// 非静态成员变量,可以在成员声明时给缺省值。
int a = 10;
B b = 20;
int* p = (int*)malloc(4);
static int n;
};
int A::n = 10;
int main()
{
A a;
a.Print();
return 0;
}
求1+2+3+...+n_牛客题霸_牛客网
描述
求1+2+3+...+n,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
数据范围: 0
这里我们是通过调用n次Sum的构造函数来实现 我们具体数值的累加。
我们原本的策略是通过全局变量的方式,现在我们可以改进成使用静态变量的形式。
在上面的代码中我们在sum类的内部声明了静态变量,并且通过静态变量实现了我们数据的累加。
// 设计一个只能在栈上定义对象的类
class StackOnly
{
public:
static StackOnly CreateObj()
{
StackOnly so;
return so;
}
private:
StackOnly(int x = 0, int y = 0)
:_x(x)
, _y(0)
{}
private:
int _x = 0;
int _y = 0;
};
int main()
{
//如果我们直接调用类的构造器生成的对象将在我们的栈上
// StackOnly so1; // 栈
//如果直接生成静态的类的对象的话,我们的生成的对象将在我们的静态区中。
// static StackOnly so2; // 静态区
//栈
StackOnly so3 = StackOnly::CreateObj();
return 0;
}
三、C++11 的成员初始化新方法
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。
class B
{
public:
B(int b = 0)
:_b(b)
{}
int _b;
};
class A
{
public:
void Print()
{
cout << a << endl;
cout << b._b<< endl;
cout << p << endl;
}
private:
// 非静态成员变量,可以在成员声明时给缺省值。
int a = 10;
B b = 20;
int* p = (int*)malloc(4);
static int n;
};
int A::n = 10;
int main()
{
A a;
a.Print();
return 0;
}
C++11支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值。
四、友元
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。
友元函数
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& _cout)
{
_cout<_year<<"-"<_month<<"-"<_day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2017, 12, 24);
d<
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(){
}
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout<>(istream& _cin, Date& d)
{
_cin>>d._year;
_cin>>d._month;
_cin>>d._day;
return _cin;
}
int main()
{
Date d;
cin>>d;
cout<
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用和原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
class Date; // 前置声明
class Time
{
friend class Date;
// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
//注意这里的仅仅是日期类能够访问Time中的全部变量和方法,但是Time并不能访问Date中的私有变量和方法。
public:
Time(){
}
Time(int hour, int minute, int second)
: _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;
};
int main()
{
Date d(2017, 12, 24);
return 0;
}
五、内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系
#include
using namespace std;
#include
// 内部类
class A
{
private:
int _h;
static int k;
public:
// B定义在A的里面
// 1、受A的类域限制,访问限定符
// 2、B天生是A的友元
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a._h << endl;//OK -- 友元
}
private:
int _b;
};
};
int A::k = 1;
int main()
{
cout << sizeof(A) << endl; // 4
A a;
A::B b;
b.foo(a);
return 0;
}
静态的成员变量并不会影响整个类的大小,所以我们的a类的大小就是那个_h,也就是一个int的大小
第二行的1就是我们的静态变量
第三行的随机值是因为我们没有对_h进行初始化。
六、练习分析
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
//如果是拷贝赋值就打印如下的信息
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
void f1(W w)
{
}
void f2(const W& w)
{
}
int main()
{
W w1;
f1(w1);
f2(w1);
cout << endl << endl;
f1(W()); // 本来构造+拷贝构造--编译器的优化--直接构造
// 结论:连续一个表达式步骤中,连续构造一般都会优化 -- 合二为一
W w2 = 1;
return 0;
}
从我们下面的测试结果我们可以进行分析。
第一行的w()也就是我们W w1构造时打印的
第二行的W(const W& w)是我们将调用f1时生成的,由于我们所采用的并不是引用传值,所以我们的编译器会生成一份拷贝给我们的f1,所以就有了这一行的打印数据。
第三行的~W()是拷贝给f1的w对象销毁的时候打印的。
由于我们的f2采用的是引用传值的方式所欲不会形成拷贝,所以也就没有打印。
下面我们用换行的方式分隔了我们的测试代码
f1(w())原本应该是先构造一个w再拷贝构造给我们的f1的,但是由于我们的编译器的优化,这里就直接将拷贝的值传递给我们的f1了,所以在w()构建之后紧接着就是~W()的销毁。
W w2=1也是如此,本应该先生成一个无名的w对象赋值给我们的w2的,但是由于编译器的优化就直接将生成的值传递给w2了。所以在w()构建之后紧接着就是~W()的销毁
最后一行是我们一开始生成的w1的销毁函数。
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
W f3()
{
W ret;
return ret;
}
// 《深度探索C++对象模型》
int main()
{
f3(); // 1次构造 1次拷贝
cout << endl << endl;
W w1 = f3(); // 本来:1次构造 2次拷贝 -- 优化:1次构造 1次拷贝
cout << endl << endl;
W w2;
w2 = f3(); // 本来:1次构造 1次拷贝 1次赋值
return 0;
}
第一行和第二行的构造w和销毁w时由于我们的f3()没有接收,所以在生成之后马上就销毁了。
第三行的W() 本来应该是由我们的f3()生成一个w类的对象,然后再return的时候将这个生成的w类的对象以拷贝的形式返回,然后再将我们的返回值以拷贝的形式赋给w1,所以我们一共是一次构造,两次拷贝,但是我们的clion编译器在这里优化了,直接将我们生成的ret给了w1,期间并没有拷贝构造。然后这个对象的销毁是在最后一行的。
第四行的w()是由于w2的创建所以打印出来的。
第五行的w()是由于f3()中构建的w的对象ret打印出来的。
第六行的拷贝函数,是因为我们的f3()在赋值给w2的时候打印出来的,然后分别是这两个对象的销毁。
最后一行的~W()是我们一开始的w1的销毁。
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
W f(W u)
{
//cout << "--------" << endl;
W v(u);
W w = v;
//cout << "--------" << endl;
return w;
}
int main()
{
W x;
W y = f(x); // 1次构造 4次拷贝
return 0;
}
这里的话,我们先创建了一个w类的对象x,所以我们会打印出一个W(),也就是一次构造函数。
然后f(x),就是将我们的x拷贝到f()方法中,这是第一次拷贝构造。
然后W v(u),就是将我们的u拷贝构造给v,这是第二次拷贝构造。
然后将我们的v赋给w,也就是我们的第三次拷贝构造。
最后我们将返回的w直接拷贝构造给y,也就是我们的第四次拷贝构造。
但是由于我们这里的编译器的优化,编译器直接把返回的w直接引用给了y,没有拷贝构造,所以只有打印出来三次拷贝构造。
我们的拷贝构造的次序也就是如同我们下面的图。
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
void f1(W w)
{
}
void f2(const W& w)
{
}
W f(W u)
{
//cout << "--------" << endl;
W v(u);
W w = v;
//cout << "--------" << endl;
return w;
}
int main()
{
W x;
W y = f(f(x)); // 1次构造 7次拷贝 or 1次构造 5次拷贝
return 0;
}
我们先来解释为什么是7次拷贝构造
这里我们代码和上面一次的不同的是f(x)变成了f(f(x)),所以我们之前的四次的拷贝构造不再进行解释 ,直接快进到返回w给f(f(x))中的外层f(X),这是第四次拷贝构造。
然后我们W v(u)将我们的w拷贝构造给了v也就是第五次拷贝构造
然后我们W x=v将我们的v拷贝构造给了w,也就是我们的第六次拷贝构造。
最后我们将w返回给y,也就是我们的第七次拷贝构造。
由于编译器的优化的关系,我们只需要五次拷贝构造就能够实现完上面的代码。
一个是我们的第四次的拷贝构造如果直接将结果直接引用给了f(f(x))的外层f(x),这一次的拷贝构造就省去了。
还有一个是我们的第七次的拷贝构造,如果我们的f(f(x))将最后的返回值直接引用给了y,中间没有生成临时的拷贝,就省去了一次拷贝构造。
所以在经过编译器优化之后就仅剩下五次拷贝构造了。
以下就是我们七次拷贝构造的先后顺序
优化的原理
这是优化之前的模式
这是优化之后的模式
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
问题:现在我们尝试去重载operator<<,然后发现我们没办法将operator<<重载成成员函数。因为cout的输出流对象和隐含的this指针在抢占第一个参数的位置。this指针默认是第一个参数也就是左操作数了。但是实际使用中cout需要是第一个形参对象,才能正常使用。所以我们要将operator<<重载成全局函数。但是这样的话,又会导致类外没办法访问成员,那么这里就需要友元来解决。operator>>同理。
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
ostream& operator<<(ostream& _cout)
{
_cout<_year<<"-"<_month<<"-"<_day;
return _cout;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d(2017, 12, 24);
d<
友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend关键字。
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(){
}
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout, const Date& d)
{
_cout<>(istream& _cin, Date& d)
{
_cin>>d._year;
_cin>>d._month;
_cin>>d._day;
return _cin;
}
int main()
{
Date d;
cin>>d;
cout<
说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用和原理相同
友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
友元关系是单向的,不具有交换性。
比如上述Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
class Date; // 前置声明
class Time
{
friend class Date;
// 声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
//注意这里的仅仅是日期类能够访问Time中的全部变量和方法,但是Time并不能访问Date中的私有变量和方法。
public:
Time(){
}
Time(int hour, int minute, int second)
: _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;
};
int main()
{
Date d(2017, 12, 24);
return 0;
}
五、内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性:
1. 内部类可以定义在外部类的public、protected、private都是可以的。
2. 注意内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
3. sizeof(外部类)=外部类,和内部类没有任何关系
#include
using namespace std;
#include
// 内部类
class A
{
private:
int _h;
static int k;
public:
// B定义在A的里面
// 1、受A的类域限制,访问限定符
// 2、B天生是A的友元
class B
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a._h << endl;//OK -- 友元
}
private:
int _b;
};
};
int A::k = 1;
int main()
{
cout << sizeof(A) << endl; // 4
A a;
A::B b;
b.foo(a);
return 0;
}
静态的成员变量并不会影响整个类的大小,所以我们的a类的大小就是那个_h,也就是一个int的大小
第二行的1就是我们的静态变量
第三行的随机值是因为我们没有对_h进行初始化。
六、练习分析
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
//如果是拷贝赋值就打印如下的信息
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
void f1(W w)
{
}
void f2(const W& w)
{
}
int main()
{
W w1;
f1(w1);
f2(w1);
cout << endl << endl;
f1(W()); // 本来构造+拷贝构造--编译器的优化--直接构造
// 结论:连续一个表达式步骤中,连续构造一般都会优化 -- 合二为一
W w2 = 1;
return 0;
}
从我们下面的测试结果我们可以进行分析。
第一行的w()也就是我们W w1构造时打印的
第二行的W(const W& w)是我们将调用f1时生成的,由于我们所采用的并不是引用传值,所以我们的编译器会生成一份拷贝给我们的f1,所以就有了这一行的打印数据。
第三行的~W()是拷贝给f1的w对象销毁的时候打印的。
由于我们的f2采用的是引用传值的方式所欲不会形成拷贝,所以也就没有打印。
下面我们用换行的方式分隔了我们的测试代码
f1(w())原本应该是先构造一个w再拷贝构造给我们的f1的,但是由于我们的编译器的优化,这里就直接将拷贝的值传递给我们的f1了,所以在w()构建之后紧接着就是~W()的销毁。
W w2=1也是如此,本应该先生成一个无名的w对象赋值给我们的w2的,但是由于编译器的优化就直接将生成的值传递给w2了。所以在w()构建之后紧接着就是~W()的销毁
最后一行是我们一开始生成的w1的销毁函数。
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
W f3()
{
W ret;
return ret;
}
// 《深度探索C++对象模型》
int main()
{
f3(); // 1次构造 1次拷贝
cout << endl << endl;
W w1 = f3(); // 本来:1次构造 2次拷贝 -- 优化:1次构造 1次拷贝
cout << endl << endl;
W w2;
w2 = f3(); // 本来:1次构造 1次拷贝 1次赋值
return 0;
}
第一行和第二行的构造w和销毁w时由于我们的f3()没有接收,所以在生成之后马上就销毁了。
第三行的W() 本来应该是由我们的f3()生成一个w类的对象,然后再return的时候将这个生成的w类的对象以拷贝的形式返回,然后再将我们的返回值以拷贝的形式赋给w1,所以我们一共是一次构造,两次拷贝,但是我们的clion编译器在这里优化了,直接将我们生成的ret给了w1,期间并没有拷贝构造。然后这个对象的销毁是在最后一行的。
第四行的w()是由于w2的创建所以打印出来的。
第五行的w()是由于f3()中构建的w的对象ret打印出来的。
第六行的拷贝函数,是因为我们的f3()在赋值给w2的时候打印出来的,然后分别是这两个对象的销毁。
最后一行的~W()是我们一开始的w1的销毁。
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
W f(W u)
{
//cout << "--------" << endl;
W v(u);
W w = v;
//cout << "--------" << endl;
return w;
}
int main()
{
W x;
W y = f(x); // 1次构造 4次拷贝
return 0;
}
这里的话,我们先创建了一个w类的对象x,所以我们会打印出一个W(),也就是一次构造函数。
然后f(x),就是将我们的x拷贝到f()方法中,这是第一次拷贝构造。
然后W v(u),就是将我们的u拷贝构造给v,这是第二次拷贝构造。
然后将我们的v赋给w,也就是我们的第三次拷贝构造。
最后我们将返回的w直接拷贝构造给y,也就是我们的第四次拷贝构造。
但是由于我们这里的编译器的优化,编译器直接把返回的w直接引用给了y,没有拷贝构造,所以只有打印出来三次拷贝构造。
我们的拷贝构造的次序也就是如同我们下面的图。
#include
using namespace std;
class W
{
public:
W(int x = 0)
{
cout << "W()" << endl;
}
W(const W& w)
{
cout << "W(const W& w)" << endl;
}
W& operator=(const W& w)
{
cout << "W& operator=(const W& w)" << endl;
return *this;
}
~W()
{
cout << "~W()" << endl;
}
};
void f1(W w)
{
}
void f2(const W& w)
{
}
W f(W u)
{
//cout << "--------" << endl;
W v(u);
W w = v;
//cout << "--------" << endl;
return w;
}
int main()
{
W x;
W y = f(f(x)); // 1次构造 7次拷贝 or 1次构造 5次拷贝
return 0;
}
我们先来解释为什么是7次拷贝构造
这里我们代码和上面一次的不同的是f(x)变成了f(f(x)),所以我们之前的四次的拷贝构造不再进行解释 ,直接快进到返回w给f(f(x))中的外层f(X),这是第四次拷贝构造。
然后我们W v(u)将我们的w拷贝构造给了v也就是第五次拷贝构造
然后我们W x=v将我们的v拷贝构造给了w,也就是我们的第六次拷贝构造。
最后我们将w返回给y,也就是我们的第七次拷贝构造。
由于编译器的优化的关系,我们只需要五次拷贝构造就能够实现完上面的代码。
一个是我们的第四次的拷贝构造如果直接将结果直接引用给了f(f(x))的外层f(x),这一次的拷贝构造就省去了。
还有一个是我们的第七次的拷贝构造,如果我们的f(f(x))将最后的返回值直接引用给了y,中间没有生成临时的拷贝,就省去了一次拷贝构造。
所以在经过编译器优化之后就仅剩下五次拷贝构造了。
以下就是我们七次拷贝构造的先后顺序
优化的原理
这是优化之前的模式
这是优化之后的模式



