【C++】类和对象3
本篇文章主要讲解类和对象的基础知识,包括初始化列表、类型转换、static 成员、友元、内部类、匿名对象。
目录
1 初始化列表
1) 初始化列表要实现的功能
2) 初始化列表的写法
3) 初始化列表的特点
4) 初始化列表的总结
2 类型转换
1) 类型转换的原理
2) 单参数的隐式类型转换
3) 多参数的隐式类型转换
4) explicit 阻止隐式类型转换
3 static 成员
1) C 语言中的 static 关键的作用
2) C++ 中 static 关键字的特殊作用
3) 静态成员函数
4) 静态对象、全局对象与局部对象的构造和析构顺序
5) 练习
4 友元
5 内部类
6 匿名对象
1) 匿名对象定义的语法
2) 匿名对象的特点
3) 匿名对象的使用场景
(1) 调用成员函数
(2) 自定义类型参数的缺省值
7 总结
1 初始化列表
1) 初始化列表要实现的功能
初始化列表是上篇文章所讲的构造函数中的一部分语法,其主要是为了解决没有默认构造函数的自定义类型成员变量的初始化的,如:
#include<iostream>using namespace std;class Stack
{
public:Stack(int n){_arr = (int*)malloc(sizeof(int) * n);if (_arr == nullptr){perror("malloc fail!\n");exit(1);}_top = 0;_capacity = n;}private:int* _arr;size_t _top;size_t _capacity;
};class MyQueue
{
public://Stack类中没有默认构造函数,只能借助初始化列表来进行初始化MyQueue()//初始化列表:_st1(4),_st2(4){}private:Stack _st1;Stack _st2;
};int main()
{return 0;
}
在上述代码中,MyQueue 类中有两个 Stack 类类型的成语变量,但是 Stack 类中并没有默认成员函数,所以编译器自动生成的构造函数是不能满足要求的,会直接报错,但是如果在构造函数体中完成两个成员变量的初始化,就必然会去调用他们的构造函数,但是构造函数还不能够显示调用的,所以这时候就只能采用初始化列表进行初始化了。
2) 初始化列表的写法
初始化列表的语法规则如下:
初始化列表以冒号开始,每个成员变量之间以逗号隔开,每个成员变量后面在括号里跟一个初始值或者表达式,每个成员变量在初始化列表中只能出现一次。
#include<iostream>using namespace std;class Stack
{
public:Stack(int n){cout << "Stack(int n)" << endl;_arr = (int*)malloc(sizeof(int) * n);if (_arr == nullptr){perror("malloc fail!\n");exit(1);}_top = 0;_capacity = n;}private:int* _arr;size_t _top;size_t _capacity;
};class MyQueue
{
public://Stack类中没有默认构造函数,只能借助初始化列表来进行初始化MyQueue()//以冒号开始:_st1(4)//每个成员变量后面跟着一个初始值,注意后面没有分号//成员变量之间以逗号隔开,_st2(4){}private:Stack _st1;Stack _st2;
};int main()
{MyQueue mq;return 0;
}
运行结果:
Stack(int n)
Stack(int n)
这里给每个成员变量赋初始值,实际上就是给每个成员变量进行初始化,会去调用该成员变量类中对应的构造函数来完成初始化,也就完成了给构造函数传参来调用构造函数了。
当然,不仅自定义类型成员可以走初始化列表,内置类型成员也是可以走初始化列表的:
#include<iostream>using namespace std;class Date
{
public:Date(int year, int month, int day):_year(year),_month(month),_day(day){}void Print(){cout << _year << '-' << _month << '-' << _day << endl;}private:int _year;int _month;int _day;
};int main()
{Date d(2022, 1, 2);d.Print();return 0;
}
运行结果:
2022-1-2
3) 初始化列表的特点
初始化列表有如下特点:
(1) 初始化列表是每个成员变量定义初始化的地方
(2) 引用成员变量,const 成员变量以及没有默认构造的类类型变量必须在初始化列表初始化
(3) C++11 支持在成员变量声明的位置给缺省值,这个缺省值会给没有在初始化列表中显示初始化的成员变量进行初始化
(4) 如果没有给缺省值,也没有在初始化列表显示初始化,那么内置类型成员是否初始化取决于编译器,对于自定义成员变量会去调用默认构造函数,如果没有默认构造,就会报错
(5) 初始化列表按照成员变量的声明顺序进行初始化,所以尽量保持在初始化列表中的出现顺序与声明顺序一致
以上五个特点,最重要的是第一个特点,既然初始化列表是每个成员定义初始化的地方,所以每个成员开辟空间实际上就在初始化列表开好的,而引用变量与 const 变量必须在定义时进行初始化,这也就是为什么引用变量与 const 成员变量必须在初始化列表进行初始化的原因,而没有默认构造函数的成员变量必须在初始化列表初始化的原因为,如果不在初始化列表进行初始化就会调用默认构造函数,那么就会报错。
#include<iostream>using namespace std;class Date
{
public:Date(int& x, const int a){}void Print(){cout << _year << '-' << _month << '-' << _day << ':' << _ref << ' ' << _n << endl;}private://可以在声明位置给缺省值int _year = 1990;int _month = 1;int _day = 1;//引用变量不在初始化列表进行初始化,会报错int& _ref;const int _n;
};int main()
{Date d;return 0;
}
运行结果:
所以引用变量与 const 变量必须在初始化列表进行初始化:
#include<iostream>using namespace std;class Date
{
public:Date(int& x, const int a)//引用变量与const变量在初始化列表进行初始化:_ref(x),_n(a){}void Print(){cout << _year << '-' << _month << '-' << _day << ':' << _ref << ' ' << _n << endl;}private://可以在声明位置给缺省值int _year = 1990;int _month = 1;int _day = 1;int& _ref;const int _n;
};int main()
{int a = 10;Date d(a, 2);d.Print();return 0;
}
输出结果:
1990-1-1:10 2
关于第五条特性,我们可以看如下的例子:
#include<iostream>using namespace std;class A
{
public:A(int a = 10):_a2(a),_a1(_a2){}void Print(){cout << _a1 << ' ' << _a2 << endl;}private:int _a1;int _a2;
};int main()
{A a;a.Print();return 0;
}
对于这个例子,运行结果会是什么呢?其实按照第五条特性,A 类的构造函数会先初始化 _a1 成员变量,再初始化 _a2 成员变量,所以运行结果为:
-858993460 10
4) 初始化列表的总结
初学初始化列表可能觉得初始化列表十分复杂,所以以下有一张思维导图供初学者借鉴:
2 类型转换
在 C 语言中,我们知道内置类型之间是可以类型转换的,比如可以将一个整型赋值给一个浮点数或者字符型:
#include<stdio.h>int main()
{char ch1 = (char)0x11223344;float f1 = (float)0x11223344;char ch2 = 0x11223344;float f2 = 0x11223344;printf("%c %lf\n", ch1, f1);printf("%c %lf\n", ch2, f2);return 0;
}
运行结果:
D 287454016.000000
D 287454016.000000
所以可以看到,其实 C 语言中也是支持类型转换的,只不过这种转换是在依靠内存中的存储来实现的。
既然 C 语言支持内置类型的类型转换,那么 C++ 中肯定也支持内置类型之间实现相互转换。但是,C++ 中还引入了类和对象的概念,那么一个内置类型的对象能不能转换成类类型的对象呢?答案是可以的,接下来我们就来讲解其原理。
1) 类型转换的原理
实现一个普通对象与类类型对象之间的类型转换其实是通过构造函数来完成的:
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1):_a1(a1){cout << "A(int a1 = 1)" << endl;}private:int _a1;
};int main()
{A a1 = (A)1;return 0;
}
运行结果:
A(int a1 = 1)
在利用显示类型转换的时候,其实是先利用 A 类的构造函数,传入参数1,构造出一个 A 类的临时对象,然后利用该临时对象去拷贝构造 A 类的对象 a1,而且临时对象的生命周期仅仅只会存在一行,所以其实还会去调用 A 的拷贝构造和析构函数(这里的拷贝构造和析构函数可能不会调用,因为构造 + 拷贝构造被编译器优化为了直接构造,中间的临时对象没有了):
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1):_a1(a1){cout << "A(int a1 = 1)" << endl;}A(const A& a){cout << "A(const A& a)" << endl;_a1 = a._a1;}~A(){cout << "~A()" << endl;}private:int _a1;
};int main()
{A a1 = (A)1;return 0;
}
运行结果:
A(int a1 = 1)
~A()
这里正常情况下应该是调用一次构造函数,一次拷贝构造函数,两次析构函数,但是由于构造 + 拷贝构造被优化为了直接构造,中间的临时对象被优化了,所以只会有一次构造和一次析构。
2) 单参数的隐式类型转换
在 1)中,那种类型转换属于显示类型转换,而在 C++ 中还支持一种隐式类型转换,隐式类型转换的原理也是利用了构造函数:
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1):_a1(a1){cout << "A(int a1 = 1)" << endl;}private:int _a1;
};int main()
{//隐式类型转换A a1 = 1;return 0;
}
输出结果:
A(int a1 = 1)
隐式类型转换的原理也是先利用 1 去构造一个 A 类的临时对象,在利用临时对象去拷贝构造 a1,,但是这里也会将构造 + 拷贝构造优化为直接构造。所以要进行类型转换,必须有对应的构造函数,否则就不能进行类型转换:
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1):_a1(a1){cout << "A(int a1 = 1)" << endl;}private:int _a1;
};int main()
{int a = 10;int* pi = &a;//没有对应的构造函数,无法进行类型转换A a1 = pi;return 0;
}
运行结果:
3) 多参数的隐式类型转换
在 C++11 后,C++ 就支持了多参数的隐式类型转换,只要用 {} 将要进行的多个参数括起来,就可以实现多参数的隐式类型转换,当然前提是要有对应的构造函数:
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2):_a1(a1),_a2(a2){cout << "A(int a1 = 1, int a2 = 2)" << endl;}
private:int _a1;int _a2;
};int main()
{//多参数的隐式类型转换A a1 = {3, 3};return 0;
}
输出结果:
A(int a1 = 1, int a2 = 2)
前面讲解过隐式类型转换的原理是构造一个临时对象,然后利用临时对象来拷贝构造,但是临时对象具有常性,也就是引用需要 const 引用,所以引用类型转换的对象就需要进行 const 引用,普通引用会报错:
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2):_a1(a1),_a2(a2){cout << "A(int a1 = 1, int a2 = 2)" << endl;}
private:int _a1;int _a2;
};int main()
{//普通引用会报错A& a1 = {3, 3};return 0;
}
运行结果:
需要 const 引用:
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2):_a1(a1),_a2(a2){cout << "A(int a1 = 1, int a2 = 2)" << endl;}
private:int _a1;int _a2;
};int main()
{//const 引用临时对象const A& a1 = {3, 3};return 0;
}
输出结果:
A(int a1 = 1, int a2 = 2)
4) explicit 阻止隐式类型转换
只要在一个类的构造函数前面加上 explicit 关键字,就可以阻止一个类的隐式类型转换,如:
#include<iostream>using namespace std;class A
{
public://加上 explicit 后会阻止隐式类型转换explicit A(int a1 = 1):_a1(a1){cout << "explicit A(int a1 = 1)" << endl;}private:int _a1;
};int main()
{A a1 = 1;return 0;
}
运行结果:
但是依然支持显示类型转换:
#include<iostream>using namespace std;class A
{
public://加上 explicit 后会阻止隐式类型转换explicit A(int a1 = 1):_a1(a1){cout << "explicit A(int a1 = 1)" << endl;}private:int _a1;
};int main()
{A a1 = (A)1;return 0;
}
输出结果:
explicit A(int a1 = 1)
3 static 成员
1) C 语言中的 static 关键的作用
在 C 语言中,static 关键字具有多个作用:
(1) 修饰局部变量可以延长局部变量的声明周期,函数栈帧被销毁后,依然存在,存储区域由栈区变为静态区,但是作用域依然是在函数作用域内
(2) 修饰全局变量与函数,改变全局变量与函数的外部链接属性,使其只能在当前源文件中使用
这里就不举例子了。
2) C++ 中 static 关键字的特殊作用
(1) 静态成员变量
在 C++ 中将 static 关键字应用到了类中,可以用 static 关键字来修饰成员变量,使其变为静态成员变量,静态成员变量与普通成员变量是有区别的:
(1) 静态成员变量必须在类外进行初始化
(2) 静态成员变量为每个对象所共享,不存在于对象中,存储在静态区
(3) 不能给静态成员变量赋缺省值,因为静态成员变量不属于某一个具体的对象,不走初始化列表,而缺省值是给初始化列表使用的
(4) 静态成员变量可以通过类名::静态成员变量或者对象 . 静态成员变量的方式访问
(5) 静态成员变量依然是类的成员,会受到public, private, protected 的限制
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2):_a1(a1), _a2(a2)//静态成员变量不能用初始化列表初始化//,_a3(1){}int& Geta3(){return _a3;}private:int _a1;int _a2;static int _a3;
};//静态成员变量只能在类外初始化
//静态成员变量可以通过类名::成员访问
int A::_a3 = 2;int main()
{A a1;//静态成员变量受到private的限制,无法在类外直接访问//++a1._a3;++a1.Geta3();//_a3 静态成员变量为每个对象所共有A a2;cout << a2.Geta3() << endl;return 0;
}
输出结果:
3
3) 静态成员函数
在 C++ 中,不仅可以用 static 关键字来修饰成员变量,也可以用来修饰成员函数,使其变为静态成员函数,静态成员函数具有以下特点:
(1) 静态成员函数没有 this 指针,所以静态成员函数中不能访问非静态的成员变量
(2) 非静态的成员函数可以访问任意的静态成员函数和静态成员变量
(3) 静态成员函数可以通过类名::静态成员函数或者对象 . 静态成员函数来访问
(4) 静态成员函数会收到 public、private、protected 的限制
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2):_a1(a1), _a2(a2)//静态成员变量不能用初始化列表初始化//,_a3(1){cout << "A(int a1 = 1, int a2 = 2)" << endl;}static int Geta3(){//在静态成员函数中,不能访问非静态成员变量//return _a2;return _a3;}private:int _a1;int _a2;static int _a3;
};//静态成员变量只能在类外初始化
//静态成员变量可以通过
int A::_a3 = 2;int main()
{A a3;//可以通过类名::静态成员函数来访问cout << A::Geta3() << endl;//也可以通过对象.函数来访问cout << a3.Geta3() << endl;return 0;
}
输出结果:
A(int a1 = 1, int a2 = 2)
2
2
4) 静态对象、全局对象与局部对象的构造和析构顺序
对于以上 3 种对象其构造顺序就是按照代码执行逻辑来进行构造的,谁先被定义,说就会先构造;而对于析构,有那么几条原则:
(1) 后定义的先析构
(2) 局部先析构,全局再析构;如果局部里面有静态对象,那么静态对象最后析构
有了以上知识,可以看一下下面这段代码中 a, b, c, d 四个对象的构造和析构顺序:
#include<iostream>using namespace std;class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}};class B
{
public:B(){cout << "B()" << endl;}~B(){cout << "~B()" << endl;}};class C
{
public:C(){cout << "C()" << endl;}~C(){cout << "~C()" << endl;}};class D
{
public:D(){cout << "D()" << endl;}~D(){cout << "~D()" << endl;}};A a;int main()
{B b;static C c;D d;return 0;
}
输出结果:
A()
B()
C()
D()
~D()
~B()
~C()
~A()
5) 练习
来接了静态成员变量与静态成员函数之后,我们就可以利用这里两个语法来解决一个问题了:
请计算出 1 + 2 + 3 + .... + n 的值
#include<iostream>using namespace std;class Sum
{
public:Sum(){_sum += _i;++_i;}static int GetSum(){return _sum;}private:static int _i;static int _sum;
};int Sum::_i = 1;
int Sum::_sum = 0;int main()
{int n;cin >> n;Sum* s = new Sum[n];cout << Sum::GetSum() << endl;return 0;
}
4 友元
在上篇文章中重载 Date 类的 << 与 >> 运算符时,提到了一个友元的概念,在这里我们正式提出他。
友元是 C++ 的一种语法,其提供了一种突破类访问限定符封装的方式。友元分为:友元函数(上篇文章的 << 与 >> 重载函数就是友元函数)和友元类,在函数或者类的声明前加上 friend 就可使得函数或者类成为其他类的友元,成为友元之后,就可以访问类中的私有和保护成员变量了,如:
#include<iostream>using namespace std;class A
{//使得Print函数成为该类的友元,就可以使用类中的私有变量了friend void Print(const A& a);
public:A(int a1 = 1):_a1(a1){}
private:int _a1;
};void Print(const A& a)
{cout << a._a1 << endl;
}int main()
{A a;Print(a);return 0;
}
输出结果:
1
虽然友元的语法很简单,但是其有一些特殊的注意事项:
(1) 友元函数仅仅只是一种声明,他不是类的成员函数
(2) 一个函数可以是多个类的友元
(3) 友元类中的成员函数都是另一个类的友元函数,都可以突破访问限定符来访问私有和保护成员变量
(4) 友元类的关系是单向的,假如 A 是 B 的友元,但是 B 不是 A 的友元
(5) 友元类的关系不能传递,A 是 B 的友元,B 是 C 的友元,但是 A 不是 C 的友元
(6) 友元可以在任何地方声明,不受访问限定符限制
#include<iostream>using namespace std;class A
{friend class B;//使得 B 类成为 A 类的友元类
public:A(int a1 = 1, int a2 = 1):_a1(a1),_a2(a2){}//友元关系是单向的,不是双向//void Print(const B& b)//{// cout << b._b1 << endl;//}private:int _a1;int _a2;
};class B
{friend class C;
public:B(int b1 = 1):_b1(b1){}//友元类中的成员函数都是另一个类的友元函数void Print(const A& a){cout << a._a1 << ' ' << a._a2 << endl;}private:int _b1;
};class C
{
public:C(int c1 = 2):_c1(c1){}//友元关系不可传递//void Print(const A& a)//{// cout << a._a1 << ' ' << a._a2 << endl;//}private:int _c1;
};int main()
{A a1;B b1;b1.Print(a1);return 0;
}
输出结果:
1 1
5 内部类
内部类就是指将一个类定义在另一个类的内部,其本质也是一种封装,当两个类具有紧密关系时,就可以将一个类定义在另一个类内部。假设有两个类 A 类和 B 类,A 类定义出来就是专门给 B 类使用的,就可以将 A 类定义在 B 类的内部,使其成为 B 类的专属类,这样其他类就是用不了A 类了。内部类的语法特点如下:
(1) 内部类依然是一个独立的类,跟定义在全局的类相比,内部类仅仅是受到外部类的访问限定符限制,外部类的对象是不包含内部类的。所以当内部类定义在外部类的 protected 与 private 限定符下,内部类就仅仅只能被外部类使用了,别的类使用不了。
(2) 内部类默认是外部类的友元类。
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2):_a1(a1),_a2(a2){}//A类的内部类class B{public:void Print(const A& a){//可以使用外部类中的私有与保护成员cout << a._a1 << ' ' << a._a2 << endl;}private:int _b1 = 1;int _b2 = 2;};
private:int _a1;int _a2;
};int main()
{A a1;//没有 B 类那就是 8 个字节cout << sizeof(a1) << endl;A::B b1;b1.Print(a1);return 0;
}
输出结果:
8
1 2
6 匿名对象
之前我们用自定义类型定义出来的都是有名对象,比如 A a1,这种都是有名对象,如果我们用类类型定义出一个没有名字的对象,这个对象就叫做匿名对象。
1) 匿名对象定义的语法
当我们使用类类型后面跟上一个括号,括号里面为初始化的值时,我们就定义出了一个匿名对象:
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2):_a1(a1),_a2(a2){cout << "A(int a1 = 1, int a2 = 2)" << endl;}void Print(){cout << _a1 << ' ' << _a2 << endl;}
private:int _a1;int _a2;
};int main()
{//调用构造函数,构造 3 个匿名对象A().Print();A(1).Print();A(2, 3).Print();return 0;
}
输出结果:
A(int a1 = 1, int a2 = 2)
1 2
A(int a1 = 1, int a2 = 2)
1 2
A(int a1 = 1, int a2 = 2)
2 3
2) 匿名对象的特点
匿名对象作为一种特殊的对象,其具有以下两个特点:
(1) 匿名对象与临时对象一样具有常性,引用时需要 const 引用
(2) 匿名对象的生命周期仅仅存在于其创建的那一行代码,创建之后就会销毁
#include<iostream>using namespace std;class A
{
public:A(int a1 = 1, int a2 = 2):_a1(a1),_a2(a2){cout << "A(int a1 = 1, int a2 = 2)" << endl;}void Print(){cout << _a1 << ' ' << _a2 << endl;}~A(){cout << "~A()" << endl;}private:int _a1;int _a2;
};void Func(A& a)
{cout << "void Func(A& a)" << endl;
}void Func(const A& a)
{cout << "void Func(const A& a)" << endl;
}int main()
{//匿名对象生命周期仅仅在当前这一行A();A a;//该函数去调用了 const 版本的 Func 函数Func(A());return 0;
}
运行结果:
A(int a1 = 1, int a2 = 2)
~A()
A(int a1 = 1, int a2 = 2)
A(int a1 = 1, int a2 = 2)
void Func(const A& a)
~A()
~A()
可以看到匿名对象作为参数时,去调用了 const 作为参数的 Func 函数,所以说明匿名对象具有常性。
3) 匿名对象的使用场景
在这里呢,一共会讲解两个匿名对象的使用场景,分别是调用成员函数以及作为自定义类型参数的缺省值。
(1) 调用成员函数
当仅仅是想要去调用成员函数时,而不想要去创建一个有名对象,但是调用成员函数又必须传递一个 this 指针,也就是需要对象去调用,这时候就可以利用匿名对象来调用:
#include<iostream>using namespace std;class A
{
public:void Print(){cout << _a1 << ' ' << _a2 << endl;}private:int _a1 = 1;int _a2 = 2;
};int main()
{//利用匿名对象来调用成员函数A().Print();return 0;
}
输出结果:
1 2
(2) 自定义类型参数的缺省值
当自定义类型想要给一个缺省值时,就可以用匿名对象来作为缺省值,但要注意引用为 const 引用:
#include<iostream>using namespace std;struct A
{friend void Print(const A& a = A());int _a1 = 1;int _a2 = 2;
};void Print(const A& a = A())
{cout << a._a1 << ' ' << a._a2 << endl;
}int main()
{Print();return 0;
}
输出结果:
1 2
7 总结
这篇文章结束,C++ 类和对象章节就讲解结束了。我们都知道,C++ 属于面向对象的编程语言,而面向对象编程的语言中最重要的就是类和对象的概念,希望大家能够通过类和对象章节的学习,理解面向对象编程的一大最主要特性 -- 封装;也能从中体会到面向对象编程(如C++)与面向过程过程编程(如C语言)的区别。