👦个人主页:@Weraphael
✍🏻作者简介:目前学习C++和算法
✈️专栏:C++航路
🐋 希望大家多多支持,咱一起进步!😁
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注✨
目录
一、再谈构造函数
1.1 初始化列表
成员变量在类中仅仅是声明,那么它们在哪定义呢?因此,C++
规定:初始化列表是初始化类对象的成员变量的。
#include <iostream>
using namespace std;
class Date
{
public:
Date(int year = 2023, int month = 5, int day = 23)
:Year(year)
,Month(month)
,Day(day)
{}
void Print()
{
cout << Year << '-' << Month << '-' << Day << endl;
}
private:
int Year;
int Month;
int Day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
【程序结果】
语法:以冒号开始,接着以逗号分割数据成员列表,每个成员变量后面跟一个放在括号中的初始值或表达式。
1.2 为什么C++要设计初始化列表
构造函数似乎就可以满足平时的代码需求。但对于以下类型,则必须使用初始化列表。
① 成员变量包含const类型
【在构造函数体内给值的情况】
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 1)
{
i = x;
}
private:
const int i;
};
int main()
{
A aa1(2);
return 0;
}
【错误报告】
【正确做法:在初始化列表初始化】
② 成员变量包含引用类型
【在构造函数体内给值的情况】
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 1)
{
i = x;
}
void Print()
{
cout << i << endl;
}
private:
int& i;
};
int main()
{
A aa1(3);
aa1.Print();
return 0;
}
【错误报告】
【正确做法:在初始化列表初始化】
【补充】
为什么const
和引用
类型需要在初始化列表初始化?
原因如下:引用和const
的特征:必须在定义的时候初始化。 因为const
修饰的变量在定义后不能被修改;同样的,引用一旦引用一个变量,就再也不能引用其他变量。而构造函数体内可以多次赋值,因此导致报错。
③ 当成员变量是自定义类型,且该类没有默认构造函数时
默认构造函数包含:无参构造、全缺省构造、编译器自动生成的构造函数
【错误展示】
#include <iostream>
using namespace std;
class A
{
public:
// A类没有默认构造函数
A(int c)
{
x = c;
}
private:
int x;
};
class B
{
public:
B(int i = 3)
:x(i)
{}
private:
A a;
int x;
};
【错误报告】
【正确做法:初始化列表给值】
1.3 注意事项
- 每个成员变量在初始化列表中最多只能出现一次(初始化只能初始化一次),当然也可以不初始化。
- 注意:初始化列表的初始化顺序一定是根据成员变量在类中声明顺序而定的。
问:以下代码的输出结果是什么?
using namespace std;
class A
{
public:
A(int a)
:a1(a)
, a2(a1)
{}
void Print()
{
cout << a1 << " " << a2 << endl;
}
private:
int a2;
int a1;
};
int main()
{
A a(1);
a.Print();
return 0;
}
【程序结构】
- 建议使用初始化列表初始化,因为不管写不写初始化列表,编译器一定会先使用初始化列表。但需要注意的是:初始化列表并不能百分之百完成所有初始化工作。
例如:malloc
开辟一块空间后,就要对其返回值进行检查
#include <iostream>
using namespace std;
class Stack
{
public:
Stack(int default_capacity = 4)
:a((int*)malloc(sizeof(int) * default_capacity))
,top(0)
,capacity(default_capacity)
{
if (a == nullptr)
{
return;
}
// 初始化
memset(a, 0, sizeof(int) * capacity);
}
private:
int* a;
int top;
int capacity;
};
int main()
{
Stack s1;
return 0;
}
【结果展示】
二、explicit关键字
单参数的构造函数支持隐式类型转换。
#include <iostream>
using namespace std;
class A
{
public:
A(int x)
:a(x)
{}
void Print()
{
cout << a << endl;
}
private:
int a;
};
int main()
{
A a1(1); // 调用构造函数
A a2 = 2; // 隐式类型转化
a1.Print();
a2.Print();
return 0;
}
【结果展示】
A a1(1)
毋庸置疑调用的是构造函数。而对于A a2 = 2
单个参数是具有类型转换 的作用。其隐式转化过程:用2
调用构造函数生成一个A
类型的临时变量,临时变量再通过拷贝构造给a2
。(构造 + 拷贝)
但需要注意的是:对于这种一行连续的构造,编译器会直接优化用直接构造。
#include <iostream>
using namespace std;
class A
{
public:
// 构造函数
A(int x)
:a(x)
{
cout << "调用了构造函数" << endl;
}
// 拷贝构造函数
A(const A& d)
:a(d.a)
{
cout << "调用了拷贝构造函数" << endl;
}
void Print()
{
cout << a << endl;
}
private:
int a;
};
int main()
{
A a2 = 2; // 隐式类型转化
a2.Print();
return 0;
}
【程序结果】
若不想有这样的隐式转化,可以在构造函数前加上explicit
,这样编译器就不支持隐式转化。
三、static成员
3.1 概念
- 声明为
static
的成员称为类的静态成员- 用
static
修饰的成员变量,称之为静态成员变量- 用
static
修饰的成员函数,称之为静态成员函数。
3.2 特性
-
要注意区分成员变量和静态成员变量。成员变量存储在对象里;而静态成员变量属于类的每个对象共享,不属于某个具体的对象,存储在静态区。
-
由于静态成员变量不属于类对象,因此不能在初始列表初始化(类对象初始化的地方)。所以,静态成员变量必须在类外定义,定义时可以不添加
static
关键字。 -
静态成员也是类的成员,受
public
、protected
、private
访问限定符的限制。但类外访问 公有的 静态成员可以直接用类名::静态成员
或者对象.静态成员
。 -
静态成员函数没有隐藏的
this
指针。如果没有this
指针的成员函数,那么此函数可以用static
修饰。 -
在静态成员函数中,只能访问静态成员变量和静态成员函数,不能访问非静态成员变量和非静态成员函数。 – 因为没有
this
指针调用不了。
3.3 面试题
- 实现一个类,计算程序中还有多少个类对象正在使用
#include <iostream>
using namespace std;
class A
{
public:
A()
{
++_scount;
}
A(const A & t)
{
++_scount;
}
~A()
{
--_scount;
}
static int GetACount()
{
return _scount;
}
private:
static int _scount;
};
int A::_scount = 0;
int main()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
return 0;
}
【解析】
- 问答题
- 为什么静态成员函数没有隐藏的
this
指针?
静态成员函数是属于类的,而不是属于类的某个对象,因此在静态成员函数中没有隐含的this
指针。this
指针是指向当前对象的指针,因此只能在非静态成员函数中使用。而静态成员函数不依赖于具体的对象,只依赖于类本身,所以无需this
指针。- 静态成员函数可以调用非静态成员函数吗?
不可以。调用非静态的成员函数需要this
指针,而静态成员函数没有隐藏的this
指针- 非静态成员函数可以调用类的静态成员函数吗?
可以。因为调用静态成员函数不需要this
指针
- 设计一个类,要求在类外只能在栈或堆上创建对象
由于对象实例化后会自动调用默认构造函数,因此可以将构造函数放到私有域上,然后通过函数还返回栈或者堆上的对象。由于这两个函数仅仅只是创建对象,并没有this
指针,因此可以用static
,修饰。
class Create
{
public:
// 栈
static Create stackObj()
{
Create obj;
return obj;
}
// 堆
static Create* heapObj()
{
return new Create;
}
private:
Create()
{}
};
int main()
{
Create s1 = Create::stackObj();
Create h1 = Create::heapObj();
return 0;
}
3.4 经典应用 – 求和
链接: 点击跳转
【题目描述】
【思路】
要求前
n
项和,并且不能使用乘除法,也就意味着不能使用使用公式。只能用加减法。因此这题解法我们可以创建n
个对象,那么这个n
个对象就要调用n
次构造函数。然后可以使用静态成员变量(因为它的生命周期长),每次调用构造函数时,静态成员变量都不会被销毁。
【代码实现】
class Sum
{
public:
Sum()
{
ans += i;
i++;
}
static int GetAns()
{
return ans;
}
private:
static int i;
static int ans;
};
// 类外定义
int Sum::i = 1;
int Sum::ans = 0;
class Solution
{
public:
int Sum_Solution(int n)
{
// 创建n个对象
// 调用n次构造函数
Sum s1[n];
return Sum::GetAns();
}
};
四、友元
- 友元提供了一种 突破封装 的方式。
- 友元分为:友元函数和友元类。
4.1 友元函数
在实现日期类的时候就用到了友元函数。重载
operator<<
时,发现没办法将operator<<
重载为成员函数。因为成员函数的第一个参数通常是一个指向当前对象的this
指针。而cout
的输出流对象占据了第一个参数的位置。无奈只能将operator<<
重载成全局函数。但这又会导致类外没办法访问成员,此时可以使用友元来解决。operator>>
同理。
#include <iostream>
using namespace std;
class Date
{
public:
// 友元声明
friend istream& operator>>(istream& cin, Date& x);
friend ostream& operator<<(ostream& cout, const Date& x);
private:
int Year;
int Month;
int Day;
};
istream& operator>>(istream& cin, Date& x)
{
cin >> x.Year >> x.Month >> x.Day;
return cin;
}
ostream& operator<<(ostream& cout, const Date& x)
{
cout << x.Year << "年" << x.Month << "月" << x.Day << "日";
return cout;
}
int main()
{
Date d1;
cin >> d1;
cout << d1 << endl;
return 0;
}
【结果展示】
4.2 友元函数特性
- 友元函数可访问类的私有成员和保护成员,但注意友元不是类的成员函数。
成员函数有
this
指针,而友元函数没有this
指针。
- 友元函数不能用
const
修饰
友元函数能直接访问类的私有成员,不需要
this
指针,因此const
修饰符对友元函数没有意义。
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
- 一个函数可以是多个类的友元函数。
4.3 友元类
#include <iostream>
using namespace std;
class Time
{
// 友元类声明
friend class Date;
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)
{
// 直接访问Time类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
以上代码在Time
类中声明Date
类的友元类,则在Date
类中就直接访问Time
类的成员变量。
4.4 友元类的特性
- 友元关系是单向的,不具有交换性。
比如在
A
类中声明B
类为友元类,那么可以在B
类中直接访问A
类的私有成员变量,但想在A
类中访问B
类中私有的成员变量则不行。
- 友元关系不能传递
如果
C
是B
的友元,B
是A
的友元,不能说明C
是A
的友元。
- 友元关系不能继承
4.5 为什么不推荐使用友
- 友元打破了封装性原则:使得类的实现细节暴露给了外部,增加了代码的复杂度和难度。
- 访问类的私有成员,就需要在每个类中都进行友元声明,增加代码维护的难度。
- 友元破坏了类的继承关系。子类无法继承父类类中的友元关系,从而限制了代码的可维护性和可扩展性。
因此,在万不得已的情况下,不推荐使用友元。
五、内部类
5.1 概念
如果一个类再定义在一个类,这个在内部的类就叫做内部类。
以下代码可以说B
是A
的内部类
#include <iostream>
using namespace std;
class A
{
public:
class B
{
public:
void foo(const A& a)
{
}
};
private:
static int k;
int h;
};
// static成员变量的定义方式
int A::k = 1;
5.2 内部类的特性
- 内部类可以定义在外部类的
public
和private
都是可以的。但它是受访问限定符的限制的。 - 内部类不属于外部类。因此,外部类对内部类没有任何的访问权限。
- 但是内部类就是外部类的友元类。因此,内部类可以访问外部类中的所有成员。 并且 内部类可以直接访问外部类中的
static
成员。
例如:
#include <iostream>
using namespace std;
class A
{
public:
A(int i = 3)
:h(i)
{
}
// B是A的内部类
class B
{
public:
void foo(const A& a)
{
// 内部类可以直接访问外部类中的static成员
cout << k << endl;
// 内部类可以访问外部类中的所有成员
cout << a.h << endl;
}
};
private:
static int k;
int h;
};
// static成员变量的定义方式
int A::k = 1;
int main()
{
A a1;
A::B b; // 内部类实例化方式
b.foo(a1);
return 0;
}
【程序结果】
sizeof(外部类) = 外部类
,和内部类没有任何关系
#include <iostream>
using namespace std;
class A
{
public:
A(int i = 3)
:h(i)
{
}
// B是A的内部类
class B
{
public:
void foo(const A& a)
{
// 内部类可以直接访问外部类中的static成员
cout << k << endl;
// 内部类可以访问外部类中的所有成员
cout << a.h << endl;
}
};
private:
static int k;
int h;
};
// static成员变量的定义方式
int A::k = 1;
int main()
{
cout << sizeof(A) << endl;
return 0;
}
【程序结果】
注意:
static
修饰的成员变量不需要计算。因为静态成员变量属于类,属于类的每个对象共享,不存在某个具体的对象,存储在静态区。
六、匿名对象
6.1 概念
匿名对象的特点:不用取名字
#include <iostream>
using namespace std;
class A
{
public:
// 构造函数
A(int i)
:a(i)
{
cout << "调用构造" << endl;
}
// 析构函数
~A()
{
cout << "调用析构" << endl;
}
private:
int a;
};
int main()
{
A(2); // 匿名对象
A a(1); // 有名对象
return 0;
}
但是注意:匿名对象的生命周期只有一行,可以看到下一行他就会自动调用析构函数,而有名函数的声明周期是当前函数的局部域。
6.2 匿名对象的特性
- 匿名对象具有常属性
正确写法:加上const
。权限不能放大
- 如果用
const
引用匿名对象,那么匿名对象的生命周期和有名对象一样:在当前函数局部域。
- 析构的顺序是和拷贝构造的顺序相反的。
七、拷贝对象时的一些编译器优化
在传参和传返回值的过程中,一般编译器会做一些优化,减少对象的拷贝,这个在一些场景下还是非常有用的。
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "调用了默认构造函数" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "调用了拷贝构造" << endl;
}
~A()
{
cout << "调用了析构函数" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
cout << "传值传参:" << endl;
A aa1;
f1(aa1);
cout << "================" << endl;
// 传值返回
cout << "传值返回:" << endl;
f2();
cout << "================" << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
cout << "隐式类型" << endl;
f1(1);
cout << "================" << endl;
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
cout << "连续构造+拷贝构造" << endl;
f1(A(2));
cout << "================" << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
cout << "连续拷贝构造+拷贝构造" << endl;
A aa2 = f2();
cout << "================" << endl;
return 0;
}
【剖析】
总结:
- 对于一行的连续构造,编译器会默认优化成一次构造,提高效率。
- 或者尽量使用引用,减少拷贝。