文章目录
【写在前面】
这篇文章是对类和对象的一个收尾和补充
一、再谈构造函数
💦 构造函数体赋值
❓ 引出初始化列表 ❔
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 指针。