前言
之前我们学习了类中的一些默认成员函数:构造函数、析构函数、拷贝构造函数、赋值重载。今天,我们接着学习剩下的取地址运算符重载以及其他关于类和对象的知识。
一、取地址运算符重载
取地址运算符重载分为两种:普通对象的取地址重载和const对象取地址重载。为了说明这两种取地址重载的区别,我们首先引入一个概念:const修饰成员函数。
1. const修饰成员函数
在c++中,成员函数可以被const修饰,修饰时要将const写在成员函数参数列表的后面。例如:
void fun() const { }
const修饰成员函数的本质是修饰this指针指向的内容,它的作用是防止该函数内部对成员变量的值进行修改。
对于一个普通成员函数,const对象是无法调用的,因为const对象的成员变量不允许被修改;而当成员函数被const修饰时,就确保了函数内部不会修改成员变量的值,const对象就可以调用该函数。
2. 取地址运算符重载
普通对象的取地址重载用于返回普通对象的地址;而const对象的取地址重载用于返回const对象的地址。两种重载函数的区别是:前者没有被const修饰,后者被const修饰。这就使得两个函数构成了重载,便于不同的对象调用。我们简单实现一下这两个函数:
class MyClass { public: //构造函数 MyClass(int a = 10, int b = 20) { _a = a; _b = b; } //取地址重载 MyClass* operator&() { return this; } const MyClass* operator&() const//为保证类型匹配,返回值也要用const修饰 { return this; } private: int _a; int _b; };
一般情况下,编译器自动生成的取地址重载函数我们就可以直接使用,不需要显示实现了。当我们不希望使用者能够获取到对象的地址时,可以显示实现取地址重载,并将空指针或者野指针作为返回值。
二、深究构造函数
之前我们已经学习了构造函数的特点、使用规则等知识,不过构造函数的知识还不止这些,接下来我们对之前构造函数的内容进行一些补充。
之前我们在实现构造函数时,都是在函数体内部对成员变量赋初值,实际上,对成员变量进行初始化的方式还有一种:初始化列表。它位于构造函数的参数列表之后,函数体大括号之前。它的使用方式是以冒号开始,将需要被初始化的成员以逗号分隔,成员之后写一个放在括号当中的值或者表达式,用于初始化成员。例如:
//构造函数 MyClass(int a = 10, int b = 20) :_a(a)//将a的值给成员变量_a , _b(b)//将b的值给成员变量_b { //... }
需要注意的是:
1. 每一个成员变量在初始化列表中只能出现一次。
2. 初始化列表初始化的顺序与成员在类中的声明顺序一致,而与列表中的成员顺序无关。
3. 以下三种变量必须在初始化列表中进行初始化,否则会编译报错:引用类型的成员变量、const成员变量、不存在默认构造的类类型成员变量。
这里的 “ 必须在初始化列表中进行初始化 ” 并不是指我们一定要将该变量显示写在初始化列表中,我们也可以使用如下方式:
class MyClass { public: //构造函数 MyClass(int a = 10, int b = 20) :_a(a) , _b(b) { //... } private: int _a; int _b; const int _c = 1; };
可以看到,对于const成员“_c”,我们并没有显示在初始化列表中对其进行初始化,而是在其声明时为其赋了一个缺省值(初值)。这是c++11规定的语法,该初值是给初始化列表的,当初始化列表当中没有显示对一个成员进行初始化时,如果声明时有缺省值,则会用这个值进行初始化(本质也是通过初始化列表初始化,只不过并没有显示写出),所以程序并不会发生报错。当然,对于普通成员,我们也可以在声明时赋缺省值,但是相比显示写在初始化列表当中,会有一些效率的损耗。
注:对类类型的成员变量通过初始化列表进行初始化时,本质也是在调用它的构造函数。
如果我们既没有显示地在初始化列表对成员进行初始化,也没有在声明时赋缺省值,那么对于内置类型的成员,当对象被创建时编译器一般不会对其初始化;对于自定义类型的成员,对象被创建时就会调用它的默认构造函数,如果没有默认构造函数,就会发生报错。
接下来,我们总结一下成员变量通过初始化列表进行初始化的逻辑:
三、类型转换
首先来看一段代码:
class MyClass { public: //构造函数 MyClass(int a = 10) :_a(a) { } void Print() const { cout << _a << endl; } private: int _a; }; int main() { MyClass m = 1; m.Print(); return 0; }
上述程序中,我们创建了一个MyClass类对象m,并且将其初始化为1。我们都知道,它的本质是在调用构造函数,不过它的运行过程并不是这么简单。在 MyClass m = 1 语句中,等号右边的 “ 1 ”是整形,而“ m ”是MyClass类型,这个过程中就需要发生类型转换。程序首先会调用构造函数,将“ 1 ”构造为MyClass类型的一个临时对象,然后将该临时对象拷贝构造给m。对于这种调用构造函数+调用拷贝构造的情况,有些编译器会将其优化为直接调用构造函数,所以我们可能无法感受到类型转换的过程,但它的确是存在的。当我们在构造函数之前加上关键字“ explicit ”之后,就无法调用该构造函数进行隐式类型转换。当然,如果有合适的构造函数,类与类之间也可以发生类型转换。
对于有多个参数的情况,也可以进行类型转换:
class MyClass { public: //构造函数 MyClass(int a = 10, int c = 20) :_a(a) ,_c(c) { } void Print() const { cout << _a << endl; cout << _c << endl; } private: int _a; char _c; }; int main() { MyClass m = { 1,'w' };//大括号赋值的写法C++11以后才支持 m.Print(); return 0; }
四、static修饰成员
在C++当中,static可以修饰成员变量和成员函数,它们在面向对象编程中有着很重要的作用。
1. static修饰成员变量
用static修饰的成员变量叫做静态成员变量。静态成员变量要在类中进行声明,并且初始化必须要在类外,而不是类中(因为在类中给的初值是给初始化列表的,而静态成员变量不走初始化列表)。例如:
class MyClass { public: //... private: static int _m;//类里面声明 }; int MyClass::_m = 0;//类外进行初始化
并且在多个文件的程序中,如果类是在头文件中定义的,则静态成员变量必须在其他cpp文件中初始化,否则容易出现重定义问题。
特殊情况:对于有const修饰的整形静态成员变量,可以在类中同时进行声明和初始化。
注意:静态成员变量存储在静态区中,不属于任何一个对象,而是被所有对象所共享的,使用对象或者类的作用域限定符就可以访问到静态成员变量。当然,既然是成员变量,也会受到 public / private / protected 的限制。接下来,我们尝试通过不同方式访问静态成员变量:
using namespace std; class MyClass { public: //... static int _m; }; int MyClass::_m = 5; int main() { cout << MyClass::_m << endl;//使用作用域限定符访问 MyClass a; cout << a._m << endl;//使用对象访问 return 0; }
运行结果:
由于 _m 是公有成员,所以我们直接访问到了该变量。当静态成员变量是私有成员时,该如何访问呢?这就需要静态成员函数了。
2. static修饰成员函数
用static修饰的成员函数称之为静态成员函数,静态成员函数与普通成员函数的显著区别是:它不存在this指针。
由于静态成员函数不存在this指针,所以它也就无法访问到普通成员变量,只能访问静态成员变量。当然,如果一个成员函数是非静态的,它也可以访问静态成员变量。接下来我们用静态成员函数来访问私有的静态成员变量:
using namespace std; class MyClass { public: static int func() { return _m; } private: static int _m; }; int MyClass::_m = 5; int main() { cout << MyClass::func() << endl;//通过作用域限定符调用函数 MyClass a; cout << a.func() << endl;//通过对象调用函数 return 0; }
运行结果:
3. 小练习
接下来我们运用static修饰成员的知识,尝试做一个小练习:计算1+2+3+...+n的值,要求不能使用乘除法、for、while、if、else、switch、case等关键字及条件判断语句(A?B:C)。
思路分析:要计算1+2+3+...+n的值,习惯的思路是循环累加或者使用等差数列求和公式,但是由于禁止使用乘除法和循环语句,这两种方法就行不通了。那么递归能否解决呢?由于递归一定要有限制条件,而if...else语句也被禁用了,所以递归也是不行的。那该怎么办呢?我们都知道,静态成员变量为所有对象所公有,那么我们就可以定义一个静态成员变量x,每当一个对象被创建出时,x的值就+1。这样,我们创建出n个对象,并将每一次得到的x值累加到另一个变量中,就可以算出最终结果了。
话不多说,我们尝试实现:
class MyClass { public: //在构造函数中操作静态变量,使得对象被创建时就会累加 MyClass() { x++; sum += x; } //获取sum的值 static int GetNum() { return sum; } private: static int x; static int sum; }; //初始化 int MyClass::x = 0; int MyClass::sum = 0; //求和函数 int Sum(int n) { //用new关键字创建n个对象 MyClass* arr = new MyClass[n]; //返回累加后的值 return MyClass::GetNum(); }
我们测试一下程序的正确性:
int main() { int n = 0; cout << "请输入n值"; cin >> n; cout << "结果为:" << Sum(n) << endl; return 0; }
运行结果:
五、友元
当类中的成员被设置为私有,外部无法访问到时,友元就可以突破这种封装,使得外部可以访问这些私有成员。友元可以分为友元函数和友元类,我们需要使用友元时,在函数或类的声明之前加上关键字 friend ,并将其放在另一个类(宿主类)当中。此时该函数或类就成为了宿主类的友元。
友元函数和友元类的特点如下:
1. 友元函数只是一种声明,并不是说一个函数成为了一个类的友元,他就是该类的成员函数了。
2. 友元函数可以在类的任意地方声明,并不受public等限定符的限制。
3. 一个函数可以成为多个类的友元。
4. 友元类中的成员函数都可以访问宿主类的成员,不受限定符限制。
5. 友元类的关系是单向且不可传递的,比如:A是B的友元,但不意味着B是A的友元;A是B的友元,B是C的友元,并不意味着A是C的友元。
接下来我们尝试使用一下友元:
using namespace std; class A { public: friend void fun1();//函数fun1称为类A的友元 friend class B;//类B成为类A的友元 private: int _a = 1; protected: int _b = 2; }; void fun1() { A a; cout << a._a << endl; cout << a._b << endl; } class B { public: void fun2() { A a; cout << a._a << endl; cout << a._b << endl; } }; int main() { fun1(); cout << endl; B b; b.fun2(); return 0; }
运行结果:
不难看出,将函数或类声明为友元后,就可以访问宿主类的私有或保护成员了。友元有时虽然提供了便利,但是它明显是破坏了类的封装性,不符合“高类聚,低耦合”的设计原则,所以实际开发中不宜多用。
六、内部类
如果一个类A定义在另一个类B当中,那么类A就成为了类B的内部类。内部类与全局定义的类相比,它受到外部类的类域和访问限定符限制,并且默认是外部类的友元类。这里要注意:内部类是一个类定义在另一个类当中,而不是将对象作为一个类的成员,不要将两者混淆。
内部类的本质也是一种封装的体现,当我们需要让一个类B仅供类A使用,那么就可以考虑让B成为A的内部类。
接下来我们定义一个内部类:
using namespace std; class A { public: class B//此时B是A的内部类 { public: void Print(const A& a) { cout << a._m << endl; cout << a._n << endl; } }; private: int _m = 3; int _n = 5; }; int main() { A a; A::B b;//受到类域限制,声明时要限定类域 b.Print(a); return 0; }
运行结果:
七、匿名对象
顾名思义,匿名对象就是没有实际名字的对象,它的定义方法是:
MyClass(10);//构造函数传参 MyClass();//不传参
注意:匿名对象的生命周期只有当前一行,当程序运行到下一行时,该对象就被销毁。我们来验证一下:
using namespace std; class MyClass { public: MyClass(int a = 1) :_a(a) { cout << "调用构造函数" << endl; } ~MyClass() { cout << "调用析构函数" << endl; } private: int _a; }; int main() { MyClass(); cout << "hehe" << endl; return 0; }
运行结果:
可以看到,程序在打印hehe之前,就已经调用了析构函数,意味着匿名对象已经被销毁。
当我们需要创建一个临时对象,并且只使用一次时,就可以考虑创建匿名对象。相比普通对象,它能够很大限度地简化代码。
特别注意:当匿名对象被const引用接收时,它的生命周期会延长至该引用的作用域结束。
总结
今天我们学习了类和对象相关的新概念和知识,例如:取地址重载、static修饰成员、友元、内部类等,它们对于我们深入学习并理解c++的后续内容,以及实现对象的相关功能有很大帮助。如果你觉得博主讲的还不错,就请留下一个小小的赞在走哦,感谢大家的支持❤❤❤