类的基础概述2
约 6935 字大约 23 分钟
2025-06-21
1.初始化列表
1.1.函数体赋值初始值
在创建对象的时候,编译器通过调用构造函数,给每一个对象一个初始值,注意!只是给定初始值,而不是进行初始化。因为初始化只有一次,而类似构造函数在函数体内赋值初始值可以多次赋值。
#include <iostream>
using namespace std;
class Data1
{
public:
Data1(int a1)
/*
注意这里没有提供默认构造函数,
注意默认构造函数有三种:
无参的、全缺省的、编译器默认生成的,
这里需要传输参数,所以不是默认构造函数
*/
{
_a1 = a1;
}
private:
int _a1;
};
class Data2
{
public:
Data2(int a2, int b2, int c2, int d2)
{
_a2 = a2;
_b2 = b2;
_c2 = c2;
_d2 = d2;//对于此时的 b 对象来说,_d2 会调用 Data1 的默认构造函数先进行构造,结果发现没有默认构造函数,因此调用失败,连把 d2 赋值的能力都没有
}
private:
int _a2;
int _b2;
int _c2;
Data1 _d2;
};
int main()
{
Data1 a(2);//成功调用了 Data1 的带参构造函数
Data2 b(5, 5, 5, 5);//调用失败
return 0;
}如果我们先构造好一个 Data1,再通过默认拷贝构造赋值给 _d2 呢?依旧是不行的,原因依旧是 _d2 在定义的时候会自动调用默认构造函数,无法调用,在这一步出现问题,后续怎么折腾都没有用。
#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Data1
{
public:
Data1(int a1)
/*
注意这里没有提供默认构造函数,
注意默认构造函数有三种:
无参的、全缺省的、编译器默认生成的,
这里需要传输参数,所以不是默认构造函数
*/
{
_a1 = a1;
}
private:
int _a1;
};
class Data2
{
public:
Data2(int a2, int b2, int c2, int d2)
{
_a2 = a2;
_b2 = b2;
_c2 = c2;
Data1 cache(d2);//cache 创建成功了
_d2 = cache;//但是对于此时的 b 对象来说,b 内部的_d2 依旧会先调用 Data1 的默认构造函数,结果发现没有默认构造函数,因此调用失败,连定义_d2 都没有办法,更别说赋值给_d2 了
}
private:
int _a2;
int _b2;
int _c2;
Data1 _d2;
};
int main()
{
Data1 a(2);//成功调用了 Data1 的带参构造函数
Data2 b(5, 5, 5, 5);//调用失败
return 0;
}要成功运行的话只能写一个默认构造函数。
#include <iostream>
using namespace std;
class Data1
{
public:
Data1(int a1 = 0)//提供了默认构造函数
{
_a1 = a1;
}
private:
int _a1;
};
class Data2
{
public:
Data2(int a2, int b2, int c2, int d2)
{
//函数体内初始化
_a2 = a2;
_b2 = b2;
_c2 = c2;
_d2 = d2;
}
private:
int _a2;
int _b2;
int _c2;
Data1 _d2;
};
int main()
{
Data1 a(2);
Data2 b(5, 5, 5, 5);
return 0;
}虽然成功进行了初始化,但是这种初始化未免太过麻烦,因此 C++ 又设计了“初始化列表”这种用法,使得构造函数能够对变量直接进行初始化,而不是赋值初始值。
1.2.初始化列表初始化
结合了初始化列表,构造函数才是真正的构造函数(集合了定义初始化和赋值初始值)。
初始化列表的格式:以一个冒号 : 开头,接着以一个逗号分割的数据成员列表,每一个成员变量后面跟着一个放在括号中的初始值或表达式。
#include <iostream>
using namespace std;
class Data1
{
public:
Data1(int a1)//提供了构造函数,但不是默认构造函数
{
_a1 = a1;
}
private:
int _a1;
};
class Data2
{
public:
Data2(int a2, int b2, int c2, int d2)
//函数体外初始化
: _a2(a2)
, _b2(b2)
, _c2(c2)
, _d2(d2)//相当于显示调用上面的构造函数来定义_d2
{}
private:
int _a2;
int _b2;
int _c2;
Data1 _d2;
};
int main()
{
int i = 2;
Data1 a(i);
Data2 b(5, 5, 5, 5);
return 0;
}在一般情况下,使用“函数体内赋值初始值”和“函数体外初始化(使用初始化列表初始化)”没有太大区别,但是有一些情况就必须使用初始化列表。
引用成员变量
const成员变量自定义类型成员(且该自定义类型没有默认构造函数时)
注意:初始化列表可以认为是成员变量定义的地方。
最上面的问题实际上就是上面的情况 3,下面介绍其他的情况。
1.2.1.情况 1:引用成员变量
#include <iostream>
using namespace std;
class Data1
{
public:
Data1(int a1, int b1, int c1, int d1)
{
_a1 = a1;
_b1 = b1;
_c1 = c1;
_d1 = d1;
}
private:
int _a1;
int _b1;
int _c1;
const int _d1;
/*
这里虽然可以定义_d1,
但是却不允许后续赋值了,
因为已经提前定义了 const 变量,
没有办法赋值了(赋值相当于是一种修改)
*/
};
int main()
{
Data1 a(5, 5, 5, 5);
return 0;
}由于 const 必须在定义的地方初始化,因此对于 const 成员变量,就只能改成初始化列表了。
#include <iostream>
using namespace std;
class Data1
{
public:
Data1(int a1, int b1, int c1, int d1)
:_d1(d1)
{
_a1 = a1;
_b1 = b1;
_c1 = c1;
}
private:
int _a1;
int _b1;
int _c1;
const int _d1;
/*
这里虽然可以定义_d1,
但是却不允许后续赋值了,
因为已经提前定义了 const 变量,
没有办法赋值了(赋值相当于是一种修改)
*/
};
int main()
{
Data1 a(5, 5, 5, 5);
return 0;
}1.2.2.情况 2:const 成员变量
对于引用成员变量也是需要使用初始化列表的,这是因为引用也必须在定义的时候指定初始化,不可以通过赋值的方式,给一个变量起别名(int& a; a = b; 这种写法是不被允许的,a 并不会是 b 的别名,必须写成 int& a = b)。
#include <iostream>
using namespace std;
class Data1
{
public:
Data1(int a1, int b1, int c1, int& d1)
{
_a1 = a1;
_b1 = b1;
_c1 = c1;
_d1 = d1;
}
private:
int _a1;
int _b1;
int _c1;
int& _d1;
};
int main()
{
int i = 0;
Data1 a(5, 5, 5, i);
return 0;
}#include <iostream>
using namespace std;
class Data1
{
public:
Data1(int a1, int b1, int c1, int& d1)
: _d1(d1)
{
_a1 = a1;
_b1 = b1;
_c1 = c1;
}
private:
int _a1;
int _b1;
int _c1;
int& _d1;
};
int main()
{
int i = 0;
Data1 a(5, 5, 5, i);
return 0;
}1.2.3.情况 3:自定义类型成员
情况三就是一开始提到没有默认构造函数的情况,这里不再赘述...
1.3.初始化列表的初始化顺序
注意初始化列表的初始化顺序问题:初始化列表是按照声明(成员变量的声明顺序)的先后顺序来依次进行初始化的,与初始化列表的顺序无关。
#include <iostream>
using namespace std;
class A
{
public:
A(int a)
: _a1(a)//2.由于初始化列表是按照声明的先后顺序来依次进行初始化的
, _a2(_a1)//3.所以这里的_a2 被初始化了随机值,_a1 赋予了 a 的值
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
//1.下面就是成员变量的声明顺序
int _a1;
int _a2;
};
int main()
{
A aa(1);
aa.Print();
return 0;
}
//因此这里输出的应该是“1 随机值”1.4.成员变量缺省值
这点我们之前提到过,在内置成员变量给缺省值的行为实际上是 C++ 对内置类型不处理的补丁,这个缺省值其实是交给初始化列表使用的。
我们可以使用代码并且调试验证一下:
#include <iostream>
using namespace std;
class Data
{
public:
Data()
: _i(2)
{
cout << "Data(int i, int j)" << endl;
}
private:
int _i;
int _j = 1;
};
int main()
{
Data a;
return 0;
}相当于是一种隐式的初始化列表,这一点理解很重要。并且如果我们显式和隐式都写出来了,C++ 默认指挥调用显式的初始化列表,忽略隐式的缺省值。
#include <iostream>
using namespace std;
class Data
{
public:
Data()
: _i(2)
, _j(4)
{
cout << "Data(int i, int j)" << endl;
}
private:
int _i;
int _j = 1;//该缺省值被忽略
};
int main()
{
Data a;
return 0;
}私认为:
C++构造函数 = 初始化 + 赋值初始值,实际上就是对C语言语句int i = 0;和int i; i = 0;的一种组合,这种设计真的极其精妙。
1.5.初始化列表和赋值初始值的选择
结合上面的所有知识,我们会发现 C++ 在这块整得还是挺复杂的,但是最后比较统一的保守的结论是:能使用初始化列表就使用初始化列表,大部分情况下要比直接在函数体内赋值初始化好一些。当然不能一话说到底,有些时候也是需要在函数体内初始化更好,比如对指针的检查...
#include <iostream>
using namespace std;
class A
{
public:
A(int N)
:_a((int*)malloc(sizeof(int)* N))
,_N(N)
{
if (_a == NULL)//这里检查指针并且赋值的过程就不适合全在初始化列表做完了
{
perror("malloc fail\n");
}
memset(_a, 0, sizeof(int) * N);
}
private:
int* _a;
int _N;
};
int main()
{
A aa0(100);
return 0;
}2.explicit 关键字
explicit 关键字用于修饰只有一个参数的构造函数,它的作用是防止编译器进行隐式类型转换。具体来说,当使用 explicit 关键字修饰构造函数时,编译器将不会自动将参数类型转换为类类型,而要求显式地调用该构造函数。
使用 explicit 关键字的主要目的是避免因隐式类型转换而引发的潜在问题和歧义,以增强代码的可读性和安全性。
上面的一通文字下来是不是没有明白?其实 explicit 取消了对象使用 = 初始化的和使用 () 初始化等价的情况。
#include <iostream>
using namespace std;
class Data
{
public:
Data(int x)
:_x(x)
{
cout << "Data(int x)" << endl;
}
private:
int _x;
};
int main()
{
Data dd1(2021);
Data dd2 = 2022;
//上述两种写法效果一样,但是过程不一样
//1.dd1 是:“直接调用构造函数构造 dd1”
//2.dd2 是:“隐式转化----> 2022 先调用 Data 的构造函数,生成了一个临时对象,再使用默认的拷贝构造函数,将临时对象的值赋予 dd2”
//但是情况 2 会触发编译器优化
//“构造函数”+“拷贝构造函数”+“编译器优化”---->“直接调用构造函数”
//如果在构造函数前面加上 explicit 关键字,就会取消这种隐式转化
return 0;
}如果使用 explicit 就会取消这种优化:
#include <iostream>
using namespace std;
class Data
{
public:
explicit Data(int x)
:_x(x)
{
cout << "Data(int x)" << endl;
}
private:
int _x;
};
int main()
{
Data dd1(2021);
Data dd2 = 2022;//<这里开始报错>
//上述两种写法效果一样,但是过程不一样
//1.dd1 是:“直接调用构造函数构造 dd1”
//2.dd2 是:“隐式转化----> 2022 先调用 Data 的构造函数,生成了一个临时对象,再使用默认的拷贝构造函数,将临时对象的值赋予 dd2”
//但是情况 2 会触发编译器优化
//“构造函数”+“拷贝构造函数”+“编译器优化”---->“直接调用构造函数”
//但是我们在构造函数前面加上了 explicit 关键字,取消这种隐式转化
return 0;
}补充:如果是有两个成员变量的类呢?怎么使用隐式转化?
C++11使用{}来进行初始化。#include <iostream> using namespace std; class Data { public: Data(int x, int y) : _x(x) , _y(y) { cout << "Data(int x)" << endl; } private: int _x; int _y; }; void Function(const Data& d) { ; } int main() { Data dd1(2021, 1212); Data dd2 = { 2022, 1231 };//有一说一,这不就是数组么?还真不是,这只是初始化了类内的两个成员变量 const Data& dd3 = { 3, 3 };//这里引用的是类型为 Data 的临时变量,如果传递这个临时参数,在 VS202 的监视窗口就会发现,这里居然传递了一个花括号参数“{_x = 3, _y = 3}” Function(dd3); return 0; }
很多主流的编译器在连续的“构造+拷贝构造”时会进行优化,变成“直接进行构造”,例如:下面代码的运行结果就没有调用拷贝构造函数。
通过下面的 8 种调用,让我们再细致观察一下这种编译器优化:
#include <iostream>
using namespace std;
class Widget
{
public:
//1.构造函数
Widget(int x = 0)
{
cout << "Widget()" << endl;
_x = x;
}
//2.拷贝构造函数
Widget(const Widget& w)
{
cout << "Widget(const Widget& w)" << endl;
}
//3.赋值重载函数
Widget& operator=(const Widget& w)
{
cout << "Widget& operator=(const Widget& w)" << endl;
return *this;
}
//4.析构函数
~Widget()
{
cout << "~Widget()" << endl;
}
private:
int _x;
};
void f1(Widget u)
{
}
Widget f2()
{
Widget d;
return d;
}
Widget f3()
{
return Widget(1);
}
int main()
{
//调用 1
Widget w1;
f1(w1);
cout << "----" << endl;
//先调用 构造函数 Widget()
//然后调用函数的时候给形参调用 拷贝构造函数 Widget(const Widget & w) 拷贝给临时变量 u
//最后调用 析构函数~Widget() 析构掉临时变量 u
//调用 2
f1(Widget(2));
cout << "----" << endl;
/*
原本应该是调用 构造函数 Widget() 构造一个匿名对象,
然后调用 拷贝构造函数 Widget(const Widget & w) 拷贝给临时变量 u,
最后使用 析构函数~Widget() 析构掉 u。
但是!由于编译器会优化连续的“构造”+“拷贝构造”---->“直接构造”。
所以变成下面的调用:
*/
//Widget()
//~Widget()
//调用 3
f1(2);
cout << "----" << endl;
/*
原本应该是调用 构造函数 Widget() 构造一个临时对象,
然后调用 拷贝构造函数 Widget(const Widget & w) 拷贝给临时变量 u,
最后使用 析构函数~Widget() 析构掉 u。
但是!由于编译器会优化连续的“构造”+“拷贝构造”---->“直接构造”。
所以变成下面的调用:
*/
//Widget()
//~Widget()
//调用 4
Widget d2 = Widget(3);
cout << "----" << endl;
/*
原本应该是调用 构造函数 Widget() 构造一个临时对象,
然后使用 拷贝构造函数 Widget(const Widget & w) 拷贝给 d2 对象
但是!由于编译器会优化连续的“构造”+“拷贝构造”---->“直接构造”。
所以变成下面的调用:
*/
//Widget()
//调用 5
f2();
cout << "----" << endl;
/*
根据 f(2)的函数内容
Widget f2()
{
Widget d;
return d;
}
原本应该是调用 构造函数 Widget() 构造 d 对象,
然后使用 拷贝构造函数 Widget(const Widget & w) 交给一个临时变量便于返回函数值
最后析构这个临时变量
但是!由于编译器会优化连续的“构造”+“拷贝构造”---->“直接构造”。
所以变成下面的调用:
*/
//Widget()
//~Widget()
//调用 6
Widget ret1 = f2();
cout << "----" << endl;
/*
根据 f(2)的函数内容
Widget f2()
{
Widget d;
return d;
}
原本应该是调用 构造函数 Widget() 构造 d 对象,
然后使用 拷贝构造函数 Widget(const Widget & w) 将 d 拷贝给一个临时对象便于返回函数值,
再调用 拷贝构造函数 Widget(const Widget & w) 将临时对象拷贝给 ret1
然后析构这个临时变量。
这里编译器直接不生成临时变量,将 d 直接调用 拷贝构造函数 Widget(const Widget & w) 给 ret1。
再由于编译器会优化连续的“构造”+“拷贝构造”---->“直接构造”,
因此在 Widget ret1 = f2(); 这一句的调用中就只剩下了“直接构造”。
*/
//Widget()
//调用 7
Widget ret2;
ret2 = f2();
cout << "----" << endl;
/*
先使用 构造函数 Widget() 构造一个 ret2 对象
然后在 f2()内调用 构造函数 Widget() 构造 d 对象,
然后使用 拷贝构造函数 Widget(const Widget & w) 将 d 拷贝给一个临时对象便于返回函数值,
再使用 赋值重载函数 Widget& operator =(const Widget& w) 将临时变量赋值给 ret2,
最后析构临时变量。
由于不是在一行代码内连续构造析构,所以这里编译器只优化了部分(在 f2()内的构造和拷贝构造变成了直接构造),没有全部优化。
*/
//Widget()
//Widget()
//Widget& operator =(const Widget & w)
//~Widget()
//调用 8
Widget ret3 = f3();
cout << "----" << endl;
/*
根据 f3()内部构造:
Widget f3()
{
return Widget(1);
}
原本先是调用 构造函数 构造一个匿名对象,
然后通过 拷贝构造函数 将匿名对象拷贝给临时对象,
然后使用 拷贝构造函数 将临时变量拷贝给 ret3,
最后由于优化就变成了下面这样:
*/
//Widget()
return 0;
}为了提高效率,有的时候也会把大量的构造写到一起,触发编译器的优化。(但是如果编译器优化得过于激进,有可能带来调试上的不方便),在这里推荐看书《深度探索 C++对象模型》。
注意:主流的编译器都会进行这种优化,但是这不是
C++标准规定的!
3.static 成员变量
3.1.static 成员的引入
让我们先进入一个场景,我们可能需要统计创建对象和正在使用对象的个数 n 和 m,我们可以写出下面这样的代码:
#include <iostream>
using namespace std;
//累积创建了 n 个对象
//累积使用了 m 个对象
int n = 0;
int m = 0;
class Data
{
public:
Data()
{
n++;
m++;
}
Data(const Data& t)
{
n++;
m++;
}
~Data()
{
m--;
}
private:
};
Data Func(Data a)
{
return a;
}
int main()
{
Data d1;
cout << n << " " << m << endl;
Data d2;
cout << n << " " << m << endl;
Data ();
cout << n << " " << m << endl;
Func(d2);
cout << n << " " << m << endl;
return 0;
}但是在运行代码的过程中,可能会不小心篡改了 n 和 m 的数据,导致最后的数据不正确。
因此我们需要另外一种解决方案,static 成员变量。
声明为 static 的类成员被称为类的静态成员。
用
static修饰的成员变量,称之为静态成员变量用
static修饰的成员函数,称之为静态成员函数
静态成员变量一定要在类的外面进行初始化,不可以使用构造函数的初始化列表来初始化。
补充:但是有一种特殊情况是可以在类内初始化的。
就是带
const修饰的静态成员变量有缺省值:class A { private: const static int a = 1;//成功 //static int b = 2;//失败 }; int main() { return 0; }但是又不能在初始化列表内初始化:
class A { public: A(int number = 1) : a(number)//失败 {} private: const static int a; }; int main() { return 0; }
3.2.static 成员的特性
静态成员变量为所有相同类的对象所共享,其不属于某个具体的对象,存放在静态区(静态变量没有办法在类内给与缺省值,这是因为缺省值是交给某个对象的构造函数的初始化列表的。但是静态成员变量是属于所有相同类型的对象的。并且在计算对象的大小的时候也不会将其计入计算)
静态成员变量必须在类外定义,定义时不用添加
static关键字,类中的只是声明。其定义的方式有点类似函数的定义方式静态成员函数没有隐藏的
this指针,不能直接访问任何非静态成员(但是非静态成员函数可以访问所有的成员变量,包括静态成员变量)类静态成员可用
类名::静态成员或者对象.静态成员两种方式来访问,因为静态成员变量属于类域静态成员也是类的成员,也受访问限定符的限制
#include <iostream>
using namespace std;
//累积创建了 n 个对象
//累积使用了 m 个对象
class Data
{
public:
Data()
{
n++;
m++;
}
Data(const Data& t)
{
n++;
m++;
}
~Data()
{
m--;
}
static int n;
static int m;
};
int Data::n = 0;
int Data::m = 0;
Data Func(Data a)
{
return a;
}
int main()
{
Data d1;
cout << Data::n << " " << d1.m << endl;
Data d2;
cout << d1.n << " " << Data::m << endl;
Data ();
cout << Data::n << " " << Data::m << endl;
Func(d2);
cout << d2.n << " " << d2.m << endl;
Data* d3 = nullptr;
cout << d3->n << " " << d3->m << endl;
return 0;
}再修改一下:
#include <iostream>
using namespace std;
//累积创建了 n 个对象
//累积使用了 m 个对象
class Data
{
public:
Data()
{
n++;
m++;
}
Data(const Data& t)
{
n++;
m++;
}
~Data()
{
m--;
}
static void Print()
{
cout << m << " " << n << endl;
}
private:
static int n;
static int m;
};
int Data::n = 0;
int Data::m = 0;
Data Func(Data a)
{
return a;
}
int main()
{
Data d1;
d1.Print();
Data d2;
Data::Print();
Data ();
Data::Print();
Func(d2);
Data::Print();
Data* d3 = nullptr;
d3->Print();
return 0;
}3.3.static 成员的场景
3.3.1.计算 1+2+...+(n-1)+n 的值
这里有一道牛客题目可以试着做一下,下面的代码使用 new,new 的作用类似 malloc,如果看不懂可以暂且跳过,等您知道 C++ 的内存管理就能明白。
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* arr = new Sum[n];
return Sum::GetRet();
}
};3.3.2.创建只能在栈上开辟的类
#include <iostream>
using namespace std;
class B
{
public:
static B CreateObj()//3.提供一个接口,来间接使用构造函数
{
B b;
return b;
}
private:
B(int x = 0, int y = 0)//2.在构造的时候只能在栈上开辟空间,否则无法使用类,因为这里访问限定符是 private
: _x(x)
, _y(y)
{}
private:
int _x;
int _y;
};
int main()
{
//static B data1;//1.无法创建,因为构造函数是私有的
B data2 = B::CreateObj();//4.依靠接口间接创建了 data2 对象,并且保证一定是在栈上开辟(因此没有办法再堆上开辟该类型的对象)
//5.CreateObj 前面之所以加上 static
//是为了避免“先有鸡,还是先有蛋”的问题
//因为假设没有 static,要使用 CreateObj()就要先创建一个 B 类型的对象才能使用
//但是由于限定符 private 的原因没有办法直接使用构造函数创建变量
//使得能够直接使用 CreateObj()
return 0;
}4.friend 友元
之前在上半篇部分的日期类里,有用过一点友元的知识,本次我将带您真正了解友元的知识。
友元的关键字是 friend,友元提供了一种突破封装的方式,有时候会比较便利,也有的时候会破坏封装,友元是不建议过多使用的。 友元又分为“友元函数”和“友元类”。
4.1.友元函数
友元函数可以访问类的私有和保护成员,但不是类的成员函数
友元函数不能用
const修饰this(这点和静态函数一样,因为两者都没有this指针)友元函数可以在类定义的任何地方声明,不受类访、问限定符限制
一个函数可以是多个类的友元函数
友元函数的调用与普通函数的调用原理相同
在 C++ 中,const 成员函数的目的是确保在函数内部不会修改对象的状态,并且它们具有一个隐含的 this 指针类型为 const 指针。这意味着在 const 成员函数内部,对象被视为常量,不能通过 this 指针对其进行修改。
然而,在友元函数中,由于友元函数不属于类的成员函数,不存在 this 指针。因为友元函数不连接到任何特定的对象,无法访问对象的成员变量或成员函数,也无法使用 this 指针来引用对象。
由于没有 this 指针的存在,我们无法在友元函数中使用 const 来限定该函数。const 关键字用于修饰隐含的 this 指针,但由于友元函数没有 this 指针,所以使用 const 是无效的。
因此,因为友元函数不是类的成员函数且没有 this 指针,我们不能在友元函数中使用 const 修饰符。友元函数可以自由地修改对象的成员,包括私有成员,而不受 const 的限制(这还是很恐怖的,因此能别用就别用)。
#include <iostream>
using namespace std;
class Data
{
friend void function(void);//1.把 function()设置为 Data 类的友元函数
public:
Data(int a = 0, int b = 0, int c = 0)
:_a(a), _b(b), _c(c)
{
cout << "Data()" << endl;
}
private:
int _a;
int _b;
int _c;
};
void function(void)
{
Data D;
cout << D._a << " " << D._b << " " << D._b << " " << endl;//2.可以访问 Data 对象受保护的成员
}
int main()
{
function();
return 0;
}4.2.友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
友元关系是单向的,不具有交换性(
A是B的友元类,但是B不是A的友元类)友元关系不具有传递性(
C是B的友元,B是A的友元,但是C不是A的友元)友元关系不能继承,在以后讲解继承的时候再做补充,这里讲还是太早了
#include <iostream>
using namespace std;
class Data
{
friend class FriendData;//1.给 Data 类设置了友元类 FriendData
friend void function(void);
public:
Data(int a = 0, int b = 0, int c = 0)
:_a(a), _b(b), _c(c)
{
cout << "Data()" << endl;
}
void Print(void)
{
cout << _a << " " << _b << " " << _b << " " << endl;
}
private:
int _a;
int _b;
int _c;
};
class FriendData
{
public:
void Fun1()
{
Data D;
cout << D._a << endl;//2.可以毫无顾忌访问 Data 对象中的成员变量
}
void Fun2()
{
Data D;
cout << D._a << endl;//3.除了变量还能访问 Data 对象中的成员函数
D.Print();
}
void Fun3()
{
Data D;
cout << D._a << endl;
}
};
void function(void)
{
Data D;
cout << D._a << " " << D._b << " " << D._b << " " << endl;
D.Print();
FriendData FD;
FD.Fun1();
FD.Fun2();
}
int main()
{
function();
return 0;
}5.内部类
把一个类定义另外一个类里面,就叫“内部类”,Java 会用的比较多,但是 C++ 会比较少,内部类天生是外部类的友元类。
内部类可以无限制访问外部类的公有、私有成员变量,外部类无法访问内部类的私有成员变量(这就是友元类的体现)。
#include <iostream>
using namespace std;
class Data1
{
public:
class Data2
//2.Data2 定义在 Data1 内部,仅仅只是:
//2.1.被 A 的限定符限制
//2.2.Data2 天生是 Data1 的友元类
{
public:
void Function(const Data1& a)
{
cout << a._a1 << endl;//3.友元的体现
}
private:
int _a2;
int _b2;
int _c2;
};
private:
int _a1;
int _b1;
int _c1;
};
int main()
{
cout << sizeof(Data1) << endl;
//1.输出 12,说明 Data1 没有计算 Data2 大小
Data1::Data2 D;
return 0;
}根据上面的 sizeof 输出,可以得出结论:抛去“类域限制”和“天生友元”,基本外部类和内部类在地位关系上可以认为是并列的,并不是包含关系!
上面我们曾经写过牛客的一道题目,我们可以试着使用内部类以及友元的知识改造一下:
class Solution
{
private:
class Sum
{
public:
Sum()
{
_ret += _i;
_i++;
}
};
public:
int Sum_Solution(int n)
{
Sum* arr = new Sum[n];
return _ret;
}
private:
static int _i;
static int _ret;
};
int Solution::_i = 1;
int Solution::_ret = 0;6.匿名对象
C++ 还可以创建一个匿名对象,匿名对象的生命周期只有一行,除非使用引用延长生命周期。
class Data
{
public:
int _x;
};
Data(8);//这就是一个匿名对象,只可以在这一行被使用,没有标识符匿名对象有什么作用呢?目前我们对于 C++ 的运用还是太少了,以后我们会发现匿名对象的使用场景还是很多的。
匿名对象直接传参可以简化部分代码
有的时候如果为了调用某个函数创建一个有名对象未免过于繁琐
#include <iostream>
using namespace std;
class Data
{
public:
Data(int x = 1, int y = 0)
: _x(x)
, _y(y)
{}
void Print(Data a, Data b)
{
cout << a._x << " " << a._y << endl;
cout << b._x << " " << b._y << endl;
}
private:
int _x;
int _y;
};
int main()
{
Data d1;
d1.Print(Data(100, 100), Data(32, 123));
return 0;
}匿名对象具有常属性,不能被直接引用,但是可以被 const 引用,这种引用会延长匿名对象的生命周期。
class Data
{};
const Data& d = Data();//后续代码就可以使用 d 来使用匿名对象,这和直接定义一个 Data 对象没有什么太大区别实际上上面就是使得匿名对象变成有名对象的过程。