C++初阶:类和对象(下篇)初始化列表 | static成员 | 友元
Posted 跳动的bit
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++初阶:类和对象(下篇)初始化列表 | static成员 | 友元相关的知识,希望对你有一定的参考价值。
文章目录
【写在前面】
这篇文章是对类和对象的一个收尾和补充
一、再谈构造函数
💦 构造函数体赋值
❓ 引出初始化列表 ❔
class A
public:
A(int a = 0)
_a = a;
private:
int _a;
;
class B
private:
int _b = 1;
A _aa;
;
int main()
B b;
return 0;
📝 说明
对于 B,我们不写构造函数,编译器会默认生成 —— 内置类型不处理,自定义类型会去调用它的默认构造函数处理 (无参的、全缺省的、编译器默认生成的),注意无参的和全缺省的只能存在一个,如果写了编译器就不会生成,如果不写编译器会默认生成。这里 C++ 有一个不好的处理 —— 内置类型不处理,自定义类型处理。针对这种问题,在 C++11 又打了一个补丁 —— 在内置类型后可以加上一个缺省值,你不初始化它时,它会使用缺省值初始化。这是 C++ 早期设计的缺陷。
class A
public:
A(int a = 0)
_a = a;
cout << "A(int a = 0)" << endl;
A& operator=(const A& aa)//不写也行,因为这里只有内置类型,默认生成的就可以完成
cout << "A& operator=(const A& aa)" << endl;
if(this != &aa)
_a = aa._a;
return *this;
private:
int _a;
;
class B
public:
B(int a, int b)
//_aa._a = a;//err:无法访问private成员
/*A aa(a);
_aa = aa;*/
_aa = A(a);//简化版,同上
_b = b;
private:
int _b = 1;
A _aa;
;
int main()
B b(10, 20);
return 0;
📝 说明
对上,_b只能初始化成1,_a只能初始化成0 ❓
这里可以显示的初始化,利用匿名对象来初始化 _a。
但是这种方法代价较大 (见下图)。
💦 初始化列表
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个 “成员变量” 后面跟一个放在括号中的初始值或表达式。
class A
public:
A(int a = 0)
_a = a;
cout << "A(int a = 0)" << endl;
A& operator=(const A& aa)
cout << "A& operator=(const A& aa)" << endl;
if(this != &aa)
_a = aa._a;
return *this;
private:
int _a;
;
class B
public:
B(int a, int b)
:_aa(a)
_b = b;
private:
int _b = 1;
A _aa;
;
int main()
B b(10, 20);
return 0;
📝说明
可以看到对比函数体内初始化,初始化列表初始化可以提高效率 —— 注意对于内置类型你使用函数体或初始化列表来初始化没有区别;但是对于自定义类型,使用初始化列表是更具有价值的。这里还要注意的是函数体内初始化和初始化列表是可以混着用的。
❓ 尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化 ❔
什么成员是必须使用初始化列表初始化的 ❓
class A
public:
A(int a)
:_a(a)
private:
int _a;
;
class B
public:
B(int a, int ref)
:_aobj(a)
,_ref(ref)
,_n(10)
private:
A _aobj;//没有默认构造函数
int& _ref;//引用
const int _n;//const
;
⚠ 注意
1️⃣ 每个成员变量在初始化列表 (同定义) 中只能出现一次 (初始化只能初始化一次)。
2️⃣ 类中包含以下成员,必须放在初始化列表位置进行初始化:
1、引用成员变量 (引用成员必须在定义的时候初始化)
2、const 成员变量 (const 类型的成员必须在定义的时候初始化)
3、自定义类型成员 (该类没有默认构造函数)
❓ 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中出现的先后次序无关 ❔
#include<iostream>
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 aa(1);
aa.Print();
📝 说明
上面的程序输出 ❓
A. 1 1
B. 程序崩溃
C. 编译不通过
D. 1 随机值
如上程序的输出结果是 D 选项,因为 C++ 规定成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其初始化列表中出现的先后次序无关。实际中,建议声明顺序和初始化列表顺序保持一致,避免出现这样的问题。
💦 explicit关键字
class A
public:
A(int a)
:_a(a)
cout << "A(int a)" << endl;
A(const A& aa)
cout << "A(const A& aa)" << endl;
private:
int _a;
;
int main()
A aa1(1);
A aa2 = 1;
return 0;
📝 说明
A aa2 = 1; 同 A aa1(1); 这是 C++98 支持的语法,它本质上是一个隐式类型转换 —— 将 int 转换为 A,为什么 int 能转换成 A 呢 ? —— 因为它支持一个用 int 参数去初始化 A 的构造函数。它俩虽然结果是一样的,都是直接调用构造函数,但是对于编译器而言过程不一样。
🍳验证
🔑拓展
针对于编译器优化、底层机制这类知识可以去了解一下《深度探索C++对象模型》
❓ 如果不想允许这样的隐式类型转换的发生 ❔
这里可以使用关键字 explicit
explicit A(int a)
:_a(a)
cout << "A(int a)" << endl;
error C2440:无法从 int 转换成 A
❓ 多参数隐式类型转换 ❔
class A
public:
A(int a1, int a2)
:_a(a1)
cout << "A(int a1, int a2)" << endl;
A(const A& aa)
cout << "A(const A& aa)" << endl;
private:
int _a;
;
int main()
A aa1(1, 2);
//A aa2 = 1, 2;//???
A aa2 = 1, 2;
return 0;
📝说明
A aa2 = 1, 2; ???
明显 C++98 不支持多参数的隐式类型转换,但是 C++11 是支持的 —— A aa2 = 1, 2; ,同样编译器依然会优化。
当我们使用 explicit 关键字限制时,它会 error C2440:无法从 initializer-list 转换为 A
二、static成员
💦 概念
❓ 写一个程序,计算程序构造了多少个对象 (构造+拷贝构造) ❔
int countC = 0;
int countCC = 0;
class A
public:
A()
++countC;
A(const A& a)
++countCC;
;
A f(A a)
A ret(a);
return ret;
int main()
A a1 = f(A());
A a2;
A a3;
a3 = f(a2);
cout << countC << endl;
cout << countCC << endl;
return 0;
📝说明
这样虽然能计算出结果,但是有一个问题,countC 和 countCC 是可以随便改的,这样就很不好。
优化 ❓
class A
public:
A()
++_count;
A(const A& a)
++_count;
int GetCount()
return _count;
static int GetCount()
return _count;
private:
int _a;
static int _count;
;
//定义初始化
int A::_count = 0;
A f(A a)
A ret(a);
return ret;
int main()
A a1 = f(A());
A a2;
A a3;
a3 = f(a2);
cout << sizeof(A) << endl;
//这里就体现了static成员属于整个类,也属于每个定义出来的对象共享,但限制于公有
/*cout << A::_count << endl;
cout << a1._count << endl;
cout << a2._count << endl;*/
/*A ret;
cout << ret.GetCount() - 1 << endl;*/
/*cout << A().GetCount() - 1 << endl;*/
cout << A::GetCount() << endl;
return 0;
📝说明
int _a; 存在定义出的对象中,属于对象。
static int _count; 存在静态区,属于整个类,也属于每个定义出来的对象共享。跟全局变量比较,它受类域和访问限定符限制,更好的体现封装,别人不能轻易修改。
static成员 ❓
对于非 static 成员它们的定义是在初始化列表中,但在 C++ 中,static 静态成员变量是不能在类的内部定义初始化的,这里的内部只是声明。注意这里虽然是私有成员,但是对于 static 成员它支持在外部进行定义,且不需要加上 static,sizeof 在计算的时候并不会计算 static 成员的大小。
_count是私有,怎么访问 ❓
定义一个公有函数 GetCount 函数,返回 _count:
调用,
1、最后实例化对象后调用 GetCount 函数并减 1
2、直接匿名对象并减 1
3、将 GetCount 函数定义成静态成员函数并使用类域调用
💦 特性
1️⃣ 静态成员变量为所有类对象所共享,不属于某个具体的实例。
2️⃣ 静态成员变量必须在类外定义,定义时不添加 static 关键字。
3️⃣ 类静态成员即可用类名::静态成员或者对象.静态成员来访问。
4️⃣ 静态成员函数没有隐藏的 this 指针,不能访问任何非静态成员。
5️⃣ 静态成员和类的普通成员一样,也有 public、protected、private 3 种访问级别,也可以具有返 回值。
【面试题1】
static 的作用 C 语言 | C++ ❓
C 语言:
1、 static 修饰局部变量,改变了局部变量的生命周期 (本质上是改变了变量的存储类型),局部变量由栈区转向静态区,生命周期同全局变量一样
2、 static 修饰全局变量,使得这个全局变量只能在自己所在的文件内部使用,而普通的全局变量却是整个工程都可以使用
❓ 为什么全局变量能在其它文件内部使用 ❔
因为全局变量具有外部链接属性;但是被 static 修饰后,就变成了内部链接属性,其它源文件不能链接到这个静态全局变量了
3、static 修饰函数,使得函数只能在自己所在的文件内部使用,本质上 static 是将函数的外部链接属性变成了内部链接属性 (同 static 修饰全局变量)
C++:
1、修饰成员变量和成员函数,成员变量属于整个类,所有对象共享,成员函数没有 this 指针。
【面试题2】
静态成员函数可以调用非静态成员函数吗 ❓
不能,因为静态成员函数没有 this 指针。
非静态成员函数可以调用静态成员函数吗 ❓
可以,因为非静态成员函数有 this 指针。
三、C++11的成员初始化新玩法
class A
public:
A(int a = 0)
: _a(0)
private:
int _a;
;
class B
private:
//缺省值
int _b = 0;
int* p = (int*)malloc(sizeof(int)*10);
A _aa = A(10);//先构造再拷贝构造,优化为构造
A _aa = 10;//同上,建议
//static int _n = 10;//err,静态变量不能给缺省值
;
int main()
B bb;
return 0;
📝说明
C++11 支持非静态成员变量在声明时进行初始化赋值,但是要注意这里不是初始化,这里是给声明的成员变量缺省值 —— 如果在构造函数中显示给值就会不用缺省值,如果没有显示给,就会用缺省值。这里在上篇文章我们就提到过,这里就写一些上篇文章所没提到过的。
四、友元
友元分为:友元函数和友元类
友元提供了一种突破封装的方式,有时提供了便利。但是友元会增加耦合度,破坏了封装,所以友元不宜多用。
💦 友元函数
❓ 重载<< ❔
class Date
friend ostream& operator<<(ostream& out, const Date& d);//友元,位置可任意,一般是开头
friend istream& operator>>(istream& in, Date& d);//友元
public:
Date(int year = 0, int month = 0, int day = 1)
: _year(year)
, _month(month)
, _day(day)
/*void operator<<(ostream& out)
out << _year << "/" << _month << "/" << _day << endl;
*/
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 >> d._month >> d._day;
return in;
int main()
Date d1, d2;
cin >> d1 >> d2;
//cout << d1 ? d1 << cout
//cout << d1;
/*d1.operator<<(cout);
d1 << cout;*/
cout << d1 << d2;
return 0;
📝说明
cin | cout 怎么接收 ❓
在 C++ 里 cout 是一个 ostream 的对象;cin 是一个 istream 的对象。
cout << d1 | d1 << cout(d1.operator<<(cout)) ❓
为啥不能 cout << d1 呢 ?
之前说过运算符有几个操作数,重载函数就有几个参数。如果有两个操作数,左操作数是第一个参数,右操作数是第二个参数。
在 Date 类成员函数 operator<< 里对象是第一个参数,因为隐含的 this 指针已经默认占据了,那么 cout 就只能作第二个操作数了。可以倒也可以,但是用起来不符合流运算符原来的特性。
怎么 cout << d1 呢 ?
也就是把 cout 作为第一个参数,那么这里就不能用成员函数了,之前我们用成员函数是因为成员变量是私有的。
如何取舍:使用成员函数 | 使用全局函数。这里成员函数的可读性差影响较大,所以将之舍弃,使用全局函数。
支持 cout << d1 后怎么解决私有 ?
解决方案1:提供公有的成员函数 GetYear、GetMonth、GetDay
解决方案2:友元函数
这里我们就引出了友元,C++ 默认是不能在类外访问私有的,但是它提供了友元以帮助我们解决这种场景的问题。友元函数可以直接访问类的私有成员,它是定义在类外部的普通函数,不属于任何类,但需要在类的内部声明,声明时需要加 friend 关键字。
cout << d1 << d2; ❓
注意与连续赋值大相径同,只是方向相反。
💨小结
1、友元函数可访问类的私有和保护成员,但不是类的成员函数。
2、友元函数不能用 const 修饰,因为 const 修饰的是 this 指针指向的对象。
3、友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
4、一个函数可以是多个类的友元函数。
5、友元函数的调用与普通函数的调用和原理相同。
注意以上部分概念需要与后面的知识结合 ,不懂的可先忽略。
🍳拓展
这里主要拓展代码错误的解决能力,先来看一段代码。
class A
friend void f(const A& aa, const B& bb);
public:
A(int a = 0)
: _a(0)
private:
int _a;
;
class B
friend void f(const A& aa, const B& bb);
private:
int _b = 0;
A _aa;
;
void f(const A& aa, const B& bb)
cout << aa._a << endl;
cout << bb._b << endl;
int main()
A aa;
B bb;
f(aa, bb);
return 0;
📝分析
相信到了这里绝大部分的人都能凭借着自己的经验去解决大部分的 bug了。但是对于上面代码出现的错误又百思不得其解。
注意面对这种情况的时候,有时候编译器报的错误是不准确的,这里有两条建议能帮助提升查找 bug 的能力。
1、有很多错误的时候,一定是看最上面的错误,因为下面的错误有可能是上面的错误间接导致的。
2、排除法,这里有一百行代码(程序崩了),你注释掉了部分代码程序正常了,那么不用怀疑,错误就是注释处代码引发的。
经过分析,我们发现错误处是:在 B 类的友元里能找到 A、B;但是在 A 类的友元里就找不到 B 了。所以解决方法就是加前置声明 —— class B;
💦 友元类
class Date; //前置声明
class Time
friend class Date; //声明日期类为时间类的友元类,则在日期类中就直接访问Time类中的私有成员变量
public:
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;
;
📝分析
友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员。
1、友元关系是单向的,不具有交换性。比如上述 Time 类和 Date 类,在 Time 类中声明 Date 类为其友元类,那么可以在 Date 类中直接访问 Time 类的私有成员变量,但想在 Time 类中访问 Date 类中私有的成员变量则不行。
2、友元关系不能传递,如果 C 是 B 的友元, B 是 A 的友元,则不能说明 C 是 A 的友元。
五、内部类
概念:如果一个类定义在另一个类的内部,这个内部类就叫做内部类。注意此时这个内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象去调用内部类。外部类对内部类没有任何优越的访问权限。
注意:内部类就是外部类的友元类。注意友元类的定义,内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部不是内部的友元。
特性:
1、内部类可以定义在外部类的 public、protected、private 都是可以的。
2、注意内部类可以直接访问外部类中的 static、枚举成员、不需要外部类的对象/类名。
3、sizeof (外部类) = 外部类,和内部类没有任何关系。
class A
private:
static int k;
int h;
public:
class B//B类天生就是A的友元
C++初阶第六篇——类和对象(下)(初始化列表+explicit关键字+static成员+友元+内部类)