1. 再谈构造函数
1.1 构造函数内赋值
构造函数以前我们这样在函数体内赋初值
class Date
{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 2, 22);
return 0;
}
但是对于像const成员变量,必须在定义的时候同时初始化。如果在函数体内初始化就会报错 ——
" title="">
为此我们引入了初始化列表,就是给成员变量找到一个依次定义处理的地方。
1.2 初始化列表
语法格式:冒号开始,逗号间隔
class Date
{
public:
Date(int year, int month, int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year; //声明
int _month;
int _day;
};
int main()
{
Date d1(2022, 2, 22); //实例化/定义一个对象
return 0;
}
注:每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次),可以不出现(不是每个都必须在这儿初始化)
:heart: 1. 类中包含以下成员,必须放在初始化列表进行初始化。注意,初始化列表就是成员变量定义的地方!!这是理解这里的关键。
- const成员变量
因为const成员变量必须在定义的时候同时初始化
引用成员
因为引用成员变量必须在定义的时候同时初始化
代码示例 ——
class Date { public: Date(int year, int month, int day,int i) : _N(10) , _ref(i) { _year = year; _month = month; _day = day; } private: int _year; //声明 int _month; int _day; const int _N; //const int& _ref; //引用 }; int main() { int i = 0; Date d1(2022, 2, 22, i); //实例化/定义一个对象 return 0; }
没有默认构造函数的自定义类型成员
回忆:默认的构造函数,即不用传参的有三个 ——
- 我们不写编译器自己生成的
- 无参的
- 全缺省的
没有默认构造函数(什么情况下没有?我们自己写了一个构造函数,还是带参的,编译器不再自动生成),编译器调不动,需要在定义的时候自己显式的传参去调。示例如下 ——
class A { public: A(int a) { _a = a; } private: int _a; }; class Date { public: Date(int year, int month, int day,int i) : _N(10) , _ref(i) , _aa(1) { _year = year; _month = month; _day = day; } private: int _year; //声明 int _month; int _day; const int _N; int& _ref; A _aa; //没有默认构造函数的自定义类型成员 }; int main() { int i = 0; Date d1(2022, 2, 22, i); //实例化/定义一个对象 return 0; }
其它成员变量,如int _year
等在哪里初始化都可以。
:heart: 2. 建议尽量使用初始化列表,对于自定义成员变量,初始化列表可以提高效率。
对比 —— 为了观察调用情况,在成员函数内部打印
- [ ] 不使用初始化列表
" title="">
- [ ] 使用初始化列表
" title="">
总结:内置类型成员,在函数体和在初始化列表初始化都可以;自定义类型的成员,建议在初始化列表初始化,这样更高效。
代码如下 ——
#include<iostream>
using namespace std;
class A
{
public:
// 构造函数 - 全缺省
A(int a = 0)
{
cout << "A(int a = 0)" << endl;
}
// 拷贝构造
A(const A& aa)
{
cout << "A(const A& aa)" << endl;
_a = aa._a;
}
// 赋值重载
A& operator=(const A& aa)
{
cout << "A& operator=(const A& a)" << endl;
_a = aa._a;
return *this;
}
private:
int _a;
};
class Date
{
public:
//// 不使用初始化列表
//Date(int year, int month, int day, const A& aa)
//{
// _aa = aa;
// _year = year;
// _month = month;
// _day = day;
//}
//使用初始化列表
Date(int year, int month, int day, const A& aa)
: _aa(aa)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year; //声明
int _month;
int _day;
A _aa;
};
int main()
{
A aa(10);
Date d1(2022, 2, 22, aa); //实例化/定义一个对象
//// 可以使用匿名对象,一行解决
//Date d1(2022, 2, 22, A(10));
return 0;
}
:heart: 3. 初始化列表中的初始化顺序是在类中的声明次序,与其在初始化列表中的先后次序无关、
问:
A.输出 1 1
B.程序崩溃
C.编译不通过
D.输出1 随机值
class A {
public:
A(int a)
:_a1(a) //2.
, _a2(_a1) //1.
{}
void Print() {
cout << _a1 << " " << _a2 << endl;
}
private:
int _a2; //初始化顺序 - 声明顺序
int _a1;
}
int main() {
A aa(1);
aa.Print();
}
选dog。运行结果 ——
" title="">
建议一个类的声明顺序和初始化列表的出现顺序保持一致,这样就不容易出问题。
1.3 explicit关键字
阅读如下代码,为什么一个整型能转换为日期类?为了后续分析,我们还是在成员函数中打印。
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year)
:_year(year)
{
cout << "Date(int year)" << endl;
}
Date(const Date& dd)
{
_year = dd._year;
cout << "Date(const Date& year)" << endl;
}
private:
int _year;
};
int main()
{
Date d1(2002);
Date d2 = 2022;//思考?
return 0;
}
为什么一个整型能转换为日期类?这其实是因为单参数的构造函数中发生了隐式类型转换。
回忆C语言中隐式类型转换
// 隐式类型转换 - 相近类型 -- 表示意义相似的类型
double d = 1.1;
int i = d;
const int& i = d; //后文马上解释
// 强制类型转换 - 无关类型
int* p = &i;
int j = (int)p;
回忆在C++入门讲常引用时,讲到隐式类型转换时会产生临时变量,i
其实是临时变量的引用,这里也类似。(临时变量具有常属性,不可修改,因此要加上const)
" title="">
这儿本来是用2022
构造一个临时对象Date(2022)
,再用这个对象拷贝构造d2
。但是C++在连续的过程中,编译器会优化多个构造,合二为一,因此这里被优化为直接就是一个构造。" title="">
相当于这两句代码 ——
Date tmp(2022); //先构造
Date d2(tmp); //再拷贝构造
由于这个单参数的构造函数,整形就可以构造一个日期类的对象。
上述代码可读性不是很好,用explicit
修饰构造函数,将会禁止单参构造函数的隐式转换 ——
" title="">
2. static成员
2.1 静态成员变量
用static修饰的成员变量,称之为静态成员变量;用static修饰的成员函数,称之为静态成员函数。
【面试题】实现一个类,计算中程序中创建出了多少个类对象。这并不好数,因为编译器可能有优化。
下面给出整个认知过程 ——
由于要对同一个变量进行++,比较朴素的想法是定义一个全局count,但是这是有问题的,如果如果有两个类共用一个count会有累加效应,且别人可以在外部随意更改。
#include<iostream>
using std::cout;
using std::endl;
int count = 0;
class A
{
public:
A(int a = 0)
: _a(a)
{
count++;
}
A(const A& aa)
{
count++;
}
private:
int _a;
};
void f(A a)
{
}
int main()
{
A a1;
A a2 = 1;
f(a1);
cout << count << endl;
count++; //存在问题:别人可以随意更改
cout << count << endl;
return 0;
}
如何把count与这个类深度绑定呢?改成成员变量吗?不可以,这样每一个对象都各自有一个count,不是对同一个变量++。这时我们要引入静态成员变量。
:heart: 1. 静态成员属于整个类,为所有类对象所共享,不属于某个具体的实例,生命周期为整个工程
:heart: 2. 静态成员变量必须在类外的全局定义,定义时不添加static关键字。注意:这是一个特例,只有这里能在类外访问私有。不然你想想,怎么对这个静态成员变量定义?
作为私有成员,怎么样在类外访问呢?只能提供一个公用的成员函数getCount
。代码如下 ——
我们可以通过对象来访问成员函数getCount
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
: _a(a)
{
_scount++;
}
A(const A& aa)
: _a(aa._a)
{
_scount++;
}
int getCount()
{
return _scount;
}
private:
int _a;
static int _scount; //静态成员变量
};
int A::_scount = 0; //静态成员变量必须在类外的全局定义
void f(A a)
{
}
int main()
{
A a1;
A a2 = 1;
f(a1);
cout << a1.getCount() << endl; //通过对象来访问成员函数
return 0;
}
那么有没有更好的方式,不需要定义对象就可以获取到呢?这要引入我们的静态成员函数。
2.2 静态成员函数
:heart: 静态成员函数没有隐藏的this指针,只能访问静态成员变量和函数。不能访问任何非静态成员,这很合乎情理,你都没this指针
#include<iostream>
using namespace std;
class A
{
public:
A(int a = 0)
: _a(a)
{
_scount++;
}
A(const A& aa)
: _a(aa._a)
{
_scount++;
}
// 静态成员函数
static int getCount()
{
return _scount;
}
private:
int _a;
static int _scount;
};
int A::_scount = 0;
void f(A a)
{
}
int main()
{
A a1;
A a2 = 1;
f(a1);
cout << A::getCount() << endl; //可以通过类域来找
cout << a1.getCount() << endl; //当然了,通过对象来找
return 0;
}
静态成员函数,可以通过类域访问。当然通过对象来找也可以,但这并不意味着在对象里面找,它也不存在于对象里。
注:含有静态成员变量的类,一般含有一个静态成员函数,用于访问静态成员变量。
3. C++11 的成员初始化新玩法
这其实是C++11打的一个补丁。众所周知,默认生成的构造函数,对内置类型不处理就是一个随机值,对自定义类型会调用它的构造函数处理。这有些不太合理。
于是C++11打了一个补丁,非静态成员变量,可以在函数声明时给缺省值。若传参,就用传的参数对成员变量进行初始化;若没有传参,则用给定缺省值进行初始化。
#include<iostream>
using namespace std;
class B
{
public:
B(int b = 0)
:_b(b)
{}
int _b;
};
class A
{
public:
void Print()
{
cout << a << endl;
}
private:
// 非静态成员变量,可以在成员声明时给缺省值。
int a = 10;
};
int main()
{
A a;
a.Print();
//A().Print(); //匿名对象调用也可
return 0;
}
:heart: 注意:这不是初始化,因为这是声明,不能初始化,我对你哪个对象初始化?
且缺省值是比较宽泛的 ——
#include<iostream>
using namespace std;
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; // 因为单参数的构造函数,等价于B b = B(20);
int *p = (int*)malloc(4);
int arr[10] = { 1, 2, 3, 4, 5 }; //vs2019支持,2013不支持
static int n;
};
int A::n = 10;
int main()
{
A a;
a.Print();
//A().Print(); // 匿名对象调用也可
return 0;
}
4. 友元
友元提供了一种突破封装的方式,有时提供了便利。但是友元就像黄牛:cow:一样,破坏管理规则,会增加耦合度,破坏了封装,所以友元不宜多用。
查阅 cplusplus.com - The C++ Resources Network 可知,cout
和cin
对于内置类型之所以能“自动识别类型”,是因为库里面已经把函数重载都写好了。
" title="">
" title="">
那对于Date
类这样的自定义类型,怎么样像内置类型一样,直接使用流提取、流插入打印呢?
Date d1(2022, 2, 23); //诶!刚好是19岁的最后一天
d1.PrintWeekday();
cout << d1; //like this?
cin >> d2; //like this?
现在我们尝试去重载operator<<
,失败了,根本调不动这个函数。
" title="">
这是因为在运算符重载中,如果是双操作数的运算符重载,第一个参数也就是左操作数,第二个参数是右操作数。cout
这个输出流对象和隐含的this指针在抢占第一个参数的位置。如果像下面这样,是能调起来,就是流倒灌了,您总不能这么用吧:sweat:
" title="">
于是把它挪到类外,将operator<<
重载成全局函数,摆脱隐藏的this指针约束,但是又有类外无法访问成员的问题
注:这里为了支持连续输出,重载函数需要有返回值ostream&
,原理类似于连续赋值,只不过cout
的结合性是从左至右
" title="">
那么这里就需要引入友元来解决。
4.1 友元函数
友元函数可以直接访问类的私有/保护成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加friend
关键字。
注:
- 友元函数想访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰(const修饰的是非静态成员函数,修饰的是tihs所指向的对象)
- 友元函数只是一种声明,可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数只是在语法上突破了封装,调用与普通函数的调用和原理相同
#include<iostream>
using namespace std;
class Date
{
friend ostream& operator << (ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year,int month,int day)
: _year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
ostream& operator << (ostream& out,const Date& d)
{
out << d._year << "/" << d._month << "/" << d._day << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
in >> d._year;
in >> d._month;
in >> d._day;
return in;
}
int main()
{
Date d1(2022, 2, 23); //19岁的最后一天,要快乐
Date d2(2022, 2, 24); // 等解封了,我再出去过生日yeah!
cout << d1 << d2;
cin >> d1 >> d2;
return 0;
}
注意:流提取操作符,需要写入,右操作数不能用const
修饰
4.2 友元类
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员(私有/保护)。
- 友元关系是单向的,不具有交换性。
比如下面的Time类和Date类,在Time类中声明Date类为其友元类,那么可以在Date类中直接访问Time类的私有成员变量,但想在Time类中访问Date类中私有的成员变量则不行。
- 友元关系不能传递
如果B是A的友元,C是B的友元,则不能说明C时A的友元。
就像是,我“宣布”你是我好朋友,那我的东西你随便拿去用好了!你不是我好朋友,那可不行,就是这样~
#include<iostream>
using namespace std;
class Date; // 前置声明
class Time
{
// 友元:声明Date为Time类的友元类,则在Date类中就直接访问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)
{
// 直接访问时间类私有的成员变量
_t._hour = hour;
_t._minute = minute;
_t._second = second;
}
private:
int _year;
int _month;
int _day;
Time _t;
};
5. 内部类
C++不喜欢用,java喜欢用。
如果一个类定义在另一个类的内部,这个类就叫做内部类。内部类天生就是就是外部类的友元类。
- [ ] 内部类和在全局定义的类是基本一样的,只是内部类受到外部类A类域限制
- [ ] 注意友元类的是单方面的,在如下代码中,内部类B天生就是A的友元,即B可以访问A的私有和保护,A不能访问B的私有和保护。就像是我是外部类,我把你捧在手心上,什么都给你,但是我爱你又与你无关。
- [ ] 内部类可以定义在外部类的public、protected、private都是可以的
class A {
private:
static int k;
int h = 0;
public:
class B
{
public:
void foo(const A& a)
{
cout << k << endl; //可以访问外部类的static
cout << a.h << endl;//可以访问外部类的private
}
private:
int _b;
};
};
int A::k = 1;
int main()
{
A::B b; //只是受类域的限制
b.foo(A());
return 0;
}
sizeof(外部类) = 外部类
,和内部类没有任何关系
cout << sizeof(A) << endl;
注:static
成员变量放在静态区,不在对象中
" title="">