励志冰檗:形容在清苦的生活环境中激励自己的意志。💓💓💓
目录
✨说在前面
🍋知识点一:再探构造函数
🍋知识点二:类型转换
🍋知识点三:static成员
🍋知识点四:友元
🍋知识点五:内部类
🍋知识点六:匿名对象
🍋知识点七:对象拷贝时编译器的优化
• ✨SumUp结语
✨说在前面
亲爱的读者们大家好!💖💖💖,我们又见面了,前面几篇文章中我带大家学习了C++中的第一大难关——类和对象,今天我们来给大家收个尾,将类和对象结束。如果大家没有掌握好前面两篇文章,可以再回去看看,复习一下,再进入今天的内容。
今天我们将要学习C++中类和对象的最后一块部分。如果大家准备好了,那就接着往下看吧~
👇👇👇
💘💘💘知识连线时刻(直接点击即可)【C++】深入理解类和对象(1)
【C++】深入理解类和对象(2)
🎉🎉🎉复习回顾🎉🎉🎉
博主主页传送门:愿天垂怜的博客
🍋知识点一:再探构造函数
🔥之前我们实现实现构造函数时,初始化成员变量主要使用函数体内赋值,构造函数初始化还有一种方式,就是初始化列表,初始化列表的使用方式是以一个冒号开始,接着是以逗号分割的数据成员列表,每一个“成员变量”后面跟一个放在括号中的初始值或表达式。
🔥每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。
🔥引用成员变量,const成员变量,没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会报错。
🔥C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显示在初始化列表初始化的成员使用的。
🔥尽量使用初始化列表初始化,因为那些你不在初始化列表初始化的成员也会走初始化列表,如果这个成员在声明的位置给了缺省值,初始化列表会用这个缺省值初始化。如果你没有给缺省值,对于没有显示在初始化列表初始化的内置类型成员是否初始化取决于编译器,C++并没有规定。对于没有显示在初始化列表初始化的自定义类型成员会调用这个成员类型的默认构造,如果没有默认构造会编译错误。
🔥初始化列表中按照成员变量在类声明顺序进行初始化,跟成员在初始化列表出现的先后顺序无关。建议声明顺序和初始化列表顺序保持一致。
#include <iostream>
using namespace std;
class Date
{
public:
//初始化列表
Date(int& rx, int year, int month, int day)
//成员变量定义
:_year(year)
, _month(month)
, _day(day)
, _n(1)
, _ref(rx)
{
//error
//_n = 1;
};
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
//声明
int _year;
int _month;
int _day;
//const常量成员只能在初始化列表中初始化
const int _n;
//引用成员只能在初始化列表中初始化
int& _ref;
};
int main()
{
int x = 0;
//对象定义
Date d1(x, 2024, 7, 16);
//const变量只能在定义的时候初始化一次
//const int x = 1;
//x = 1;
return 0;
}
我们来分析一下上面的代码:
在日期类Date中,我们可以注意到构造函数的写法,冒号后面是初始化列表成员变量,小括号里面是函数体,可以两种方式混用来实现构造函数。
但是对于const修饰的常量成员和引用成员是不可以用函数体内赋值这种方式来初始化的,只能在初始化列表中进行初始化。
#include <iostream>
using namespace std;
class Time
{
public:
Time(int hour = 0)
:_hour(hour)
{
cout << "Time()" << endl;
}
private:
int _hour;
};
class Date
{
public:
//初始化列表
Date(int& rx, int year, int month, int day)
//成员变量定义
:_year(year)
, _month(month)
, _day(day)
, _n(1)
, _ref(rx)
//如果没有合适的默认构造函数
, _t(1)
, _ptr((int*)malloc(12))
{
if (_ptr == nullptr)
{
perror("malloc operation failed");
return;
}
else
{
memset(_ptr, 0, 12);
}
};
void Print() const
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
//C++11,声明,缺省值->初始化列表用的
int _year = 1;
int _month = 1;
int _day = 1;
//const常量成员只能在初始化列表中初始化
const int _n;
//引用成员只能在初始化列表中初始化
int& _ref;
//会调用Time _t的默认构造函数
Time _t = 1;
int* _ptr = (int*)malloc(12);
};
int main()
{
int x = 0;
//对象定义
Date d1(x, 2024, 7, 16);
//const变量只能在定义的时候初始化一次
//const int x = 1;
//x = 1;
return 0;
}
我们再来看上面的代码,这个代码在第一个代码的基础上增加了时间类Time。如果我们将Time类型的变量_t作为Date的成员,那么如果在初始化列表中没有初始化_t,那么会调用_t的默认构造函数进行初始化,如果没有合适的默认构造那么程序就不能执行。
需要注意的是,像_ptr这样动态申请的变量,如果只在初始化列表中进行初始化,那么没办法验证_ptr为NULL的情况,所以需要再函数体内再进行一次判断,需要两种方法混用。
同时,我们要理解每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。也就是说,如果我们将_day(day)去掉,也就是没有初始化_day,那么_day也会走初始化列表,_day是定义了的。
总结强调:
每个构造函数都有初始化列表,每个成员都要走初始化列表。对于没有在初始化列表的成员如果有缺省值就使用缺省值,如果没有缺省值且是内置类型则不确定(看编译器,大概率是随机值),如果是自定义类型则调用它的默认构造函数,如果没有合适的默认构造函数则报错。
引用变量、const变量、没有默认构造的自定义三类变量必须在初始化列表初始化。
我们再看下面这一道题目:
#include<iostream>
using namespace std;
class A
{
public:
A(int a)
: _a1(a)
, _a2(_a1)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2 = 2;
int _a1 = 2;
};
int main()
{
A aa(1);
aa.Print();
}
上面程序的运行结果是什么()
A.输出1 1 B.输出2 2 C.编译报错 D.输出 1 随机值 E.输出1 2 F,输出2 1
答案是D.
那为什么是输出1 随机值呢?首先,A.aa(1)实例化调用构造函数,由于成员变量定义初始化的顺序和声明顺序一致,所以先初始化_a2,而此时_a1还没有定义,将_a2初始化为_a1为随机值。
然后,再定义_a1为a,而a为1,所以输出为1 随机值。
🍋知识点二:类型转换
🔥C++支持内置类型隐式类型转换为类类型对象,需要有相关内置类型为参数的构造函数。
🔥构造函数前面加上explicit就不再支持隐式类型转换。
类型转换示例:
#include <iostream>
using namespace std;
class A
{
public:
//explicit A(int a = 0) 不支持隐式类型转换
A(int a = 0)
{
_a1 = a;
}
A(const A& aa)
{
_a1 = aa._a1;
}
A(int a1, int a2)
:_a1(a1)
, _a2(a2)
{}
void Print()
{
cout << _a1 << " " << _a2 << endl;
}
private:
int _a1;
int _a2;
};
class Stack
{
public:
void Push(const A& aa)
{
//...
}
private:
A _arr[10];
int _top;
};
int main()
{
A aa1(1);
aa1.Print();
//隐式类型转换
//2构造一个A的临时对象,在用这个临时对象拷贝构造aa2
//编译器遇到连续构造+拷贝构造->优化为直接构造
A aa2 = 2;
aa2.Print();
A& raa1 = aa2;
const A& raa2 = 2;
//类型转换的意义
Stack st;
A aa3(3);
st.Push(aa3);
st.Push(3);
//C++11多参数情况
A aa5 = { 1, 1 };
const A& raa6 = { 2,2 };
st.Push(aa5);
st.Push({ 2,2 });
return 0;
}
上面的代码中,我们先从main函数开始看。A aa1(1)实例化了一个类类型对象,并对其中第一个成员变量_a1进行了初始化,而_a2为随机值。而接下来,我们看到A aa2 = 2这样的写法,这是一个类型转换,将2这样的整型转换为类类型,属于隐式类型转换,需要创建临时变量,所以会先将2构造一个A的临时对象,在用这个临时对象调用拷贝构造函数拷贝到aa2中,实现类型转换,其效果就是将_a1初始化为2,而_a2为随机值。C++11后,对于多参数的情况也可以进行类型转换。
🍋知识点三:static成员
🔥用static修饰的成员变量,称之为静态成员变量,静态变量一定要在类外面进行初始化。
🔥静态成员变量为当前类的所有对象所共享,不属于某个具体的对象,不存在对象中,存放在静态区。
🔥用static修饰的成员函数,称之为静态成员函数,静态成员函数没有this指针。
🔥静态成员函数中可以访问其他的静态成员,但是不能访问非静态的,因为没有this指针。
🔥非静态的成员函数,可以访问任意的静态成员变量和静态成员函数。
🔥突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。
🔥,静态成员也是类的成员受public、protected、private 访问限定符的限制。
🔥静态成员变量不能在声明位置给缺省值初始化,因为缺省值是个构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。
示例:static变量的使用
#include <iostream>
using namespace std;
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
//静态成员函数
static int GetACount()
{
//++_a;
//没有this指针,不能访问非静态成员变量 this->_a
return _scount;
}
//private:
//类里面声明
static int _scount;
int _a;
};
//类外面初始化
int A::_scount = 0;
int main()
{
//大小为1,不包含静态变量,它存在静态区
cout << sizeof(A) << endl;
//公有情况下,可以用A::_scount或a1_.scount
cout << A::_scount << endl;
//否则用静态成员函数获取
cout << A::GetACount() << endl;
A a1, a2;
{
A a3(a1);
cout << A::GetACount() << endl;
}
cout << A::GetACount() << endl;
cout << a1.GetACount() << endl;
return 0;
}
我们看到上面的代码,在类A中定义了成员变量_a和_scount,其中_scount是静态变量,是生命周期是全局的,需要在类外部定义和初始化,我们看到main函数中计算A的大小也没有包括静态变量。在公有的情况下,我们可以用A::_scount或者对象._scount来访问静态成员变量。在私有的情况下,我们通常在类中使用静态成员函数获取静态变量,如上面的GetACount()。
我们来做一道看似简单,但并没有那么简单的题目:
题目链接:求1+2+3+...+n_牛客题霸_牛客网 (nowcoder.com)
题目描述:
题目分析:
思路:这道题限制了我们不能使用循环、不能使用递归、不能使用条件判断也不能使用等差数列求和公式,那我们就可以考虑用一个类,这个类中声明静态变量_count和_sum且在类外定义初始化为0,且将构造函数每执行一次就++_count。由于_count是静态变量,每次的值并不会改变,那么每次++_count的同时_sum+=_count,就可以将_conut每次的值加到_sum中。那么现在的问题就是,如何让它构造n次?
其实很简单,我们只要定义一个边长数组arr[n],里边有n个元素,就会调用n次构造函数,_count就会++n次,就达到了我们的目的。
代码如下:
class Sum
{
public:
Sum()
{
_count++;
_sum += _count;
}
static int GetSum()
{
return _sum;
}
private:
static int _count;
static int _sum;
};
int Sum::_count = 0;
int Sum::_sum = 0;
class Solution {
public:
int Sum_Solution(int n) {
Sum arr[n];
return Sum::GetSum();
}
};
我们再来看这样一道题:
设已经有A,B,C,D 4个类的定义,程序中A,B,C,D构造函数调用顺序为?()
设已经有A,B,C,D 4个类的定义,程序中A,B,C,D析构函数调用顺序为?()
A:D B A C B:B A D C C:C D B A D:A B D C E:C A B D F:C D A B
#include <iostream>
using namespace std;
C c;
int main()
{
A a;
B b;
static D d;
return 0;
}
答案:E B
由于全局变量在main函数之前就创建,所以c肯定是要最先创建的;其次,局部对象按照出现的顺序进行构造,无论是否为static,所以构造的顺序是C A B D。
对于析构函数,并不是单纯的将构造的顺序反过来,同时要考虑static变量生命周期发生改变,在局部变量之后进行析构,所以析构的顺序是D B A C。
🍋知识点四:友元
🔥友元提供了一种突破类访问限定符封装的方式,友元分为:友元函数和友元类,在函数声明或者类声明的前面加friend,并且把友元声明放到一个类的里面。
🔥外部友元函数可以访问类的私有和保护成员,友元函数仅仅是一种声明,他不是类的成员函数。
🔥友元函数可以在类定义的任何地方声明,不受到类访问限定符的限制。
🔥一个函数可以是多个类的友元函数。
🔥友元类中的友元函数都可以是另一个类的友元函数,都可以访问另一个类中的私有和保护成员。
🔥友元类的关系是单向的,不具有交换性,比如A类是B类的友元,但是B类不是A类的友元。
🔥友元类关系不能传递,如果A是B的友元,B是C的友元,B是C的友元,但是A不是C的友元。(我附庸的附庸不是我的附庸)。
🔥有时提供了便利,但是友元函数会增加耦合度,破坏了封装,所以友元不宜多用。
示例1:一个函数可以是多个类的友元函数
#include <iostream>
using namespace std;
//前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{
//友元声明
friend void function(const A& aa, const B& bb);
//类型变量向上找,找不到B
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
//友元声明
friend void function(const A& aa, const B& bb);
private:
int _b1 = 3;
int _b2 = 4;
};
void function(const A& aa, const B& bb)
{
cout << aa._a1 << endl;
cout << bb._b1 << endl;
}
int main()
{
A aa;
B bb;
function(aa, bb);
return 0;
}
示例2:友元类
#include <iostream>
using namespace std;
class A
{
//友元类
friend class B;
private:
int _a1 = 1;
int _a2 = 2;
};
class B
{
public:
void Func1(const A& aa)
{
cout << aa._a1 << endl;
cout << _b1 << endl;
}
void Func2(const A& aa)
{
cout << aa._a2 << endl;
cout << _b2 << endl;
}
private:
int _b1 = 3;
int _b2 = 4;
};
int main()
{
A aa;
B bb;
bb.Func1(aa);
bb.Func2(aa);
return 0;
}
🍋知识点五:内部类
🔥如果一个类定义在另一个的类内部,这个内部类就叫做内部类。内部类是一个独立的类,跟定义在全局相比,它只是受外部类类域限制和访问限定符的限制,所以外部类定义的对象中不包含内部类。
🔥内部类默认是外部类的友元类。
🔥内部类本质也是⼀种封装,当A类跟B类紧密关联,A类实现出来主要就是给B类使用,那么可以考虑把A类设计为B的内部类,如果放到private/protected位置,那么A类就是B类的专属内部类,其他地方都用不了。
示例:内部类的使用
#include <iostream>
using namespace std;
class A
{
private:
static int _k;
int _h = 1;
public:
class B//B默认就是A的友元
{
public:
void foo(const A& a)
{
cout << _k << endl;//OK
cout << a._h << endl;//OK
}
private:
int _b = 1;
};
};
int A::_k = 1;
int main()
{
cout << sizeof(A) << endl;
A::B b;
A aa;
b.foo(aa);
return 0;
}
所以我们再回过头去看static成员时的那道题,也可以直接将Sum作为Solution的友元这样写:
class Solution {
private:
static int _count;
static int _sum;
class Sum
{
public:
Sum()
{
_count++;
_sum += _count;
}
static int GetSum()
{
return _sum;
}
};
public:
int Sum_Solution(int n) {
Sum arr[n];
return Sum::GetSum();
}
};
int Solution::_count = 0;
int Solution::_sum = 0;
🍋知识点六:匿名对象
🔥用类型(实参) 定义出来的对象叫做匿名对象,相比之前我们定义的类型 对象名(实参)定义出来的叫有名对象。
🔥匿名对象生命周期只在当前一行,一般临时定义一个对象当前用一下即可,就可以定义匿名对象。
示例:匿名对象的使用
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A(int a)" << endl;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
class Solution {
public:
int Sum_Solution(int n) {
//...
return n;
}
};
int main()
{
//有名对象
A aa1;
//匿名对象
A(1);
//有名对象调用
Solution st;
int ret1 = st.Sum_Solution(10);
cout << ret1 << endl;
//匿名对象在这样场景下就很好用
int ret2 = Solution().Sum_Solution(10);
cout << ret2 << endl;
return 0;
}
这里再给大家补充一个知识点,我们在C语言中学习过qsort,忘了的可以看这一篇博客:C语言-深入理解指针(4)
#include <iostream>
using namespace std;
int int_compare(const void* p1, const void* p2)
{
return *(int*)p1 - *(int*)p2;
}
int main()
{
int arr[] = { 32,70,12,45,26,80,53,33 };
qsort(arr, 8, sizeof(int), int_compare);
for (int i = 0; i < 8; i++)
{
cout << arr[i] << " ";
}
return 0;
}
在C++中,我们不再频繁使用qsort,而是利用sort替代,但是需要包含头文件algorithm:
#include <iostream>
using namespace std;
#include <algorithm>
int main()
{
int arr[] = { 32,70,12,45,26,80,53,33 };
sort(arr, arr + 8);
for (int i = 0; i < 8; i++)
{
cout << arr[i] << " ";
}
return 0;
}
注意,qsort和sort默认都是升序排序,如果需要降序需要进行相应的操作。如将qort中的比较函数改为判断大于而不是小于。
🍋知识点七:对象拷贝时编译器的优化
🔥现代现代编译器会为了尽可能提高程序的效率,在不影响正确性的情况下会尽可能减少一些传参和传返回值的过程中可以省略的拷贝。
🔥如何优化C++标准并没有严格规定,各个编译器会根据情况自行处理。当前主流的相对新一点的编译器对于连续一个表达式步骤中的连续拷贝会进行合并优化,有些更新更"激进"的编译还会进行跨行跨表达式的合并优化。
大家先看下面的类A:
#include <iostream>
using namespace std;
class A
{
public:
A(int a = 0)
: _a1(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a1(aa._a1)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A & aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a1 = aa._a1;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a1 = 1;
};
优化1:隐式类型转换
int main()
{
//构造+拷贝构造->优化为直接构造
A aa1 = 1;
//若const引用,直接构造临时对象,然后引用该临时对象
const A& aa2 = 1;
return 0;
}
这个咱们在第二点就讲过,构造+拷贝构造会被优化为直接构造。所以,像第二句代码,1就会直接构造临时对象,然后被aa2引用。
优化2:匿名对象传参
void f1(A aa)
{
//...
}
int main()
{
f1(A(1));//构造+拷贝构造
return 0;
}
A(1)实例化匿名对象,调用构造函数,进而传参连续调用了构造和拷贝构造,也会被优化成直接构造 。
优化3:隐式类型转换+传参
void f1(A aa)
{
//...
}
int main()
{
f1(1);//构造+拷贝构造
return 0;
}
同样道理,1需要先构造临时对象,临时对象拷贝构造传参, 连续调用构造和拷贝构造,被优化成直接构造。
优化4:VS2022“激进”优化
A f2()
{
A aa(1);
return aa;
}
int main()
{
f2().Print();
cout << endl;
return 0;
}
根据我们学习的逻辑,f2返回的时候会调用拷贝构造函数创建临时变量,运行结果如下:
其中,第一个析构析构的是aa,第二个析构析构的是临时变量。而在VS2022上,它并不会生成aa,而是直接生成临时对象:
那我们假设在类中增加重载函数前置++,编译器还敢这么激进地优化吗?
//前置++
A& operator++()
{
++_a1;
return *this;
}
A f2()
{
A aa(1);
++aa;
return aa;
}
int main()
{
f2().Print();
cout << endl;
return 0;
}
我们来看结果:
我们发现,及时对其进行前置++操作,也不会构造aa,一切都是在操作中间变量。
• ✨SumUp结语
到这里本篇文章的内容就结束了,本节是类和对象的最后一节,内容已经结束了。恭喜大家熬过了C++的第一大难关,下一篇文章开始不会有太大难度的上升。希望大家能够认真学习,打好基础,迎接接下来的挑战,希望大家继续捧场~