前言:C++语法知识繁杂,要考虑的细节很多,要想学好C++一上来就啃书并不是一个很好的方法,书本的内容一般是比较严谨的,但对于初学者来说,很多概念无法理解,上来就可能被当头一棒。因此建议在学习C++之前学好C语言,再听听入门课程,C++有很多的语法概念是对C语言的一种补充,学习过C语言能更好的理解为什么要这样设计,笔者也是初学者,写的这类文章仅是用于笔记总结及对一些概念进行分析探讨,方便以后回忆,因知识和视野有限,错误难以避免,欢迎大佬阅读指教
写类与对象这篇时,我深知要想理解类与对象,是要通过大量实践的,凭我这个初学者的三言两句,必定是错误百出,后来想想,不妨写写初学者对类与对象的理解,描述一下目前自己眼中的类与对象,分享一下自己的看法,或许能给大家提供不一样的视角,欢迎大家指教
上一篇文章提到了类中的几个特殊成员函数,这几个成员函数贯穿整个C++类的学习,是务必要掌握的,这篇文章接着探究C++类的特性,包括深究构造函数,友元,static成员等等
初始化列表
什么是初始化列表
说到初始化列表,我们就需要再提构造函数,因为初始化列表和构造函数息息相关
我们之前了解过,类的实例化的初始化工作是由构造函数来完成的,编译器会自动调用构造函数,但是事实上初始化的工作并不是由构造函数来完成的
到这里大家要理解初始化的概念,初始化是只能进行一次的,就是在这个变量被定义的时候赋予的值,往后改值操作都叫赋值,实际上的变量并不是定义在构造函数中,所以构造函数只是对变量进行赋值操作,并不是进行初始化操作
那初始化的工作在哪里完成的呢?根据前面所说的,要想知道初始化由谁负责的,就要知道变量是定义在哪的,因为在定义变量的时候赋予的值才叫初始化
初始化列表的使用
想必大家已经猜出答案了,变量是定义在初始化列表中的,每一个变量都是定义在初始化列表中的,不管是内置类型还是自定义类型,我们可以自己手动写,不手动写,编译器自己生成,如果要手动写,那么初始化列表在哪个位置呢?
初始化列表在构造函数的定义说明行与函数块之间,我们以栈类为例
上图只是表明了初始化列表定义在哪里,现在看看在初始化列表中对变量进行初始化的规则
初始化列表:以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
这也就解释了,为什么编译器自己生成的默认构造函数不对内置类型做处理,因为编译器自己生成的默认构造函数只走了一遍初始化列表,也就是说仅仅把声明的变量给创建出来,至于变量中的值我们并没有在初始化列表中指出,所以是随机值
我们自己写好的构造函数也会先在初始化列表中把已声明的变量类型给创建出来,如果我们在初始化列表中给定了某个变量的初始化的值,那就会使用这个已指定的值
没有指定的变量,在初始化列表中被定义好后就是随机值,接着程序取执行构造函数的函数块里的内容,所以走完初始化列表并不是说构造函数就结束了,而是说曾在类中声明的类型都被创建出来了,初始化已经结束了,接着再执行构造函数的函数块里的内容,但函数块里的赋值操作已经不算是初始化了,而是赋值操作
听着初始化列表好像没什么用,我们平时赋值都是写在构造函数的函数块里,至于是不是初始化又有什么大的干系呢?这是因为我们忘了,并不是所有的变量都可以写到构造函数块里
比如被const修饰的变量,引用,及没有默认构造的内置类型
这三个都有一个共同的特点,就是在被创建(或者说被定义)的时候就要被初始化,这三种类型就不能在构造函数的函数块里进行赋值,而是要写到初始化列表中,这里建议大家尽量多使用初始化列表
还有一点需要提的是,初始化列表在进行初始化操作的时候,是按照变量在类中声明的顺序来进行初始化的,并不是说按照在初始化列表中的顺序初始化的,举个例子
隐式类型转换
什么是隐式类型转换
构造函数是默认支持隐式类型转换的,我们通过几个例子就能很好的理解隐式类型转换
//接下来我们用这些代码来解释隐式类型转换 class test { public: test(int tmp) { _a = tmp; } private: int _a; }; int main() { test p1(10); test p2 = 10; return 0; }
p1 和 p2 都被初始化为了10,但是所经历的过程是不一样的,p1 就是直接调用了构造函数完成了初始化,而 p2 则是通过隐式类型转换被初始化,这个过程包括了创建一个临时的test类的对象 temp(这个名字是我自己起的,便于大家理解),一次构造,一次拷贝构造
现在的编译器比较智能,可能将 p2 隐式类型转换的过程直接优化成像 p1 那样一个构造就完成初始化,这取决于编译器
原先C++98的标准是对于单个参数或者除第一个参数无默认值其余均有默认值的构造函数,支持隐式类型转换
//单参构造,支持隐式类型转换 class test { public: test(int tmp) { _a = tmp; } private: int _a; }; int main() { test p1(10); test p2 = 10; return 0; } //虽是多参数构造,但只有第一个参数无默认值,所以支持隐式类型转化 class test { public: test(int a, int b = 10, int c = 20) { _a = a; _b = b; _c = c; } private: int _a; int _b; int _c; }; int main() { test p1(10); test p2 = 10; return 0; }
C++11标准之后,支持多参数的隐式类型转换,多参数的隐式类型转换要使用花括号
//C++11之后支持多参数隐式转换 class test { public: test(int a, int b, int c) { _a = a; _b = b; _c = c; } private: int _a; int _b; int _c; }; int main() { test p1(10, 20, 30); test p2 = {10, 20, 30}; return 0; }
explicit
什么是explicit?我们上面提到过的隐式类型转换,默认情况下构造函数是允许进行这种操作的,但是你若不想进行隐式类型转换,那么你就可以在构造函数前面加上关键字 explicit 这样构造函数就不支持隐式类型转换,就不允许使用上述代码中 p2 的写法
static成员
类中的static成员
static静态变量相比于普通的变量,区别在于生命周期的不同,静态成员变量存储在静态区,程序终止时,其生命周期才会结束,静态变量的生命周期都是一样的,但是定义在不用位置的静态变量其作用域是不同的,定义在全局域的静态变量也就是全局变量,作用域是全局的,是整个程序可以共享的,而定义在某个函数内,或某个类里的静态变量,其作用域仅在该函数内或类内,只有该函数或该类可以使用
而我们接下来了解的就是类中的静态成员
class test { public: test(int b = 10, int c = 20) { _b = b; _c = c; } private: static int _a; //仅是声明 int _b; int _c; }; //类中的静态成员不是在类中进行初始化的,而是在类外进行初始化 int test::_a = 30;
这也不难理解,定义在类中的静态变量是该类的所有对象共享的,如果在类中进行初始化,那岂不是每定义一个对象就将该静态变量初始化一次,这样是不行的
类中除了可以定义静态变量,还可以定义静态函数,静态函数和类中的成员函数有什么区别呢?最大的区别就是静态函数没有this指针,也就是说静态函数是不可以访问类中的成员变量的,静态函数可以访问类中的静态变量,是配合类中的静态变量使用的
我们把类中的静态变量和静态函数统称为静态成员,得到以下特性
1. 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
2. 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
3. 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
4. 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制
友元
友元函数
在一些特定的情况下,我们在类外定义的某个函数需要访问类中的成员变量,如果通过其他途径来访问类中成员变量太过麻烦,C++允许被友元修饰的函数可以直接访问类中的成员变量,简单来说就是开个小门,一些无证人员可以通过小门进入,被友元修饰的函数破坏了类的封装性,是需要我们谨慎对待的
想要获得某个类的友元,需要在该类中被关键字 friend 声明,举个例子
我们在类外定义一个void Check_val_of_test()函数来访问类中成员
关于友元函数的一些说明:
友元函数可访问类的私有和保护成员,但不是类的成员函数
友元函数不能用const修饰
友元函数可以在类定义的任何地方声明,不受类访问限定符限制
一个函数可以是多个类的友元函数
友元类
如果把类A设置为类B的友元类,那么A中所有的成员函数都是类B的友元函数,也就是说A中所有的成员函数都可以访问B中的私有成员变量
友元类是单向的,上述中A中的成员函数可以访问B中的私有变量,但是反过来是不行的
友元关系也不能传递,假设类A是类B的友元,类B是类C的友元,不能说类A是类C的友元,类A无法直接访问类C的私有成员变量
内部类
假设有两个类,类A和类B,若是类B定义在类A的内部,那么就称类B是类A的内部类,类A是类B的外部类
内部类并不属于外部类,它并不存储在外部类中,但是它可以通过外部类的对象参数来访
问外部类中的所有成员,也就是说内部类是外部类的有元类
但是不能通过外部类的对象参数去访问内部类的任何成员,这个规定是有那么一些奇怪,不过,既然是规定,自有它一定的道理,这里就不去猜测了
关于内部类的几点特性
内部类可以定义在外部类的public、protected、private都是可以的
内部类可以直接访问外部类中的static成员,不需要外部类的对象/类名
sizeof(外部类)=外部类,和内部类没有任何关系
匿名对象
匿名对象不需要给对象取名字,但是匿名对象的生命周期只有一行,举个例子
class test { public: test(int b = 10, int c = 20) { _b = b; _c = c; } private: int _b; int _c; }; int main() { test(); //这样就定义了一个匿名类,但是其生命周期只有这一行 //从这行开始,上面定义的匿名类就销毁了 return 0; }
你可能会觉得匿名类没有什么用,想想这样一种场景,我想调用某个类中的某个函数,但是我不想去定义一个该类的对象,我只想调个函数而已,这样做实在浪费空间,这个时候我们利用匿名对象就能很好的解决这个问题,匿名对象是可以调用该类的成员函数的,函数调用结束后,类就会被销毁,可见,还是很有用的