c++和 java有很多的小点不一样,今天来总结下c++在类设计方面的知识点,并且比较下和java的异同
- 构造函数
- 复制构造函数
- 友元函数
- 虚函数
- 转换函数
- 传对象、引用、指针
- const
构造函数
一般子类构造函数的成员初始化列表中,如果没有显式调用基类的构造函数,则编译器会使用基类默认的构造函数来构造子类对象的基类部分
如上图,如果不显式调用基类的构造函数,则编译器会自动给加上基类的默认构造函数,和 java
不一样的是,c++ 不是用 super
关键字来调用基类的构造函数或者方法。c++ 使用 命名空间 的形式来调用基类的方法。如下图:
复制构造函数
c++
和 java
第二个重大的区别即在于此。虽然 java
也有深拷贝和浅拷贝的概念,但java
发生拷贝只在特定的时刻,即调用 clone
方法,而 c++
则发生的太触不及防了,以下几种情况均会调用复制构造函数:
- 将新对象初始化为一个同类对象
- 按值传递对象传递给函数
- 函数按值返回对象
- 编译器生成临时对象
一般复制构造函数是这么写的:
什么时候需要写复制构造函数呢?
使用new初始化的成员指针或者类可能包含需要修改的静态变量时,需要自己定义复制构造函数。如果不自定义,编译器就执行默认的复制构造函数,即将新对象的每个成员都初始化为原始对象相应成员的值。
问题就出在这,如果有指针成员变量,复制仅把指针指向原始对象所指的内存块,当原始对象被回收时,相应的内存块也被回收,但新对象的指针还存在,它指向一块被回收的内存,程序会报错。
和 java
类似,需要自定义复制构造函数,实现深拷贝,如上图所示代码一样,重新为自己的指针分配内存区域,而不是简单赋值
值得一提的是,使用赋值运算符也会出现这种深拷贝浅拷贝的问题,需要显式定义赋值运算符函数。
友元函数
友元函数是什么呢,就是在函数声明中添加 friend
关键字,友元函数不是成员函数,但是可以像成员函数一样,访问类中的成员变量。
友元函数一般用在什么场景呢?当类的成员函数无法实现相关功能时,只能使用友元函数上场了。比如要重写一些运算符函数时。例如,现在要把 BadString
类的 <<
运算符重写下,以便直接打印对象。
friend std::istream& operator>>(std::istream& is, BadString& s);
运算符函数的调用格式其实和其它方法一样,如果此函数不是友元函数,则调用格式是
BadString.operator>>(cout, badstring&)
BadString>>(cout, badstring&)//上一行的简写
但我们知道,真实的格式不是这样的,而是
cout >> badstring
调用对象是 cout
,它是一个istream
对象,而不是BadString
,所以 >>
运算符函数它不可能是BadString
的成员函数,但它还要访问BadString
的成员变量,怎么办呢?只能用友元函数了,这也正符合友元函数的特点,不是成员函数,却可以访问类的成员变量。
为什么 c++
中有友元函数呢,而 java
没有。因为java
中没有这种场景,java
所有类都有个共同的基类 object
,什么对象不能表示呢,如果不知道类名,则用object
替代。试想下,在 java
中,打印类对象,只要类重写 toString
方法即可。因为 System.out.println
参数传的是 object
虚函数
虚函数,在函数声明之前添加关键字 virtual
。虚函数在基类中需要被声明,子类中也需要申明。
虚函数的关键作用是多态,基类声明一个虚函数,子类也声明此虚函数,并且基类和子类都实现此虚函数。如果有指针或者引用调用此虚函数,则会根据指针或者引用的实际对象来调用真正的函数。
在c++
中,如何根据类声明找到类的定义,有两种方法:
- 静态联编
- 动态联编
顾名思义,静态联编发生在编译期间,编译器根据函数的参数,返回值,名称,一一对应查找相应函数,以应对各种情况,比如函数重载
而虚函数又让这个过程更复杂了,要实现多态,只能在程序运行期间来确定指针或者引用的真实类型了,程序运行期间选择执行正确的虚函数,这就叫做动态联编
在c++
中,构造函数、析构函数、友元函数是不会被子类继承的。构造函数不存在虚函数这个概念,因为基类也需要被初始化。而友元函数不是成员函数,所以不存在被继承一说。
析构函数一般要被声明为虚函数。因为子类中可能会额外添加一些指针成员变量,如果析构函数不是虚函数,那么子类被回收时,只会执行基类的析构函数,子类中的额外指针无法被回收,会有内存泄漏产生。所以,基类的析构函数一定是虚函数
当执行完子类的析构函数,然后会继续执行基类的析构函数
转换函数
如果存在一个单形参的构造函数,则赋值时可以采用一些千奇百怪的方式
单形参的构造函数,如果不用关键字 explicit
给限制下,就可以直接给对象赋值,赋值的类型即是单形参构造函数里的形参类型,例如上图中,Stone
类可以使用double类作单参数调用构造函数,所以就可以直接给 Stone
类赋值。其实背后的流程还是先调用单形参构造函数,生成一个临时对象后,再通过调用=
运算符函数,把临时对象赋值给Stone
对象
可以通过重写转换函数,例如上例中的 operator double
和 operator int
,即可把对象直接赋值给相应类型,Stone
重写了 double
和int
转换函数,所以可以直接赋值给double
和int
传对象、引用、指针
c++
中的传值和java
的传值类似,都是在函数中构造一个临时对象,并且将临时对象赋值为实参。
所以如果是传对象的话,c++
涉及到了复制拷贝函数,效率较低,而且可能达不到想要的效果,本想修改实参的相关数据,结果发现实参未有变化,一般不推荐传实参。
函数返回值,某些时候必须要返回对象,而不是指针或引用,比如常见的+
运算符函数,因为要返回一个全新的对象。
如果是传引用的话,事实上这非常常见,对于类对象,c++
甚至推荐使用传引用,而不是传指针。在返回引用的时候有坑,需要特别小心,返回引用,不能返回函数中临时对象的引用,因为函数一执行完,临时变量就被回收了,它的引用自然也会被回收,这会引起异常
传指针,其实传引用或者指针都能有效避免发生复制构造函数,它的效率是较高的,也不会发生传值引起的问题。但是有个小点要注意下,如果是初始化一个指针,形参就必须是指针的指针了,这和传值陷阱一样,仔细想想即可知道。多多注意观察,也可以发现这个问题,例如在 ffmpeg
中,初始化指针一定是传指针的指针
const
const用在函数中有三个位置:
- 形参,表示形参不可修改
- 返回值,表示返回一个不可修改的对象
- 函数后,表示调用此函数的对象不会被此函数修改
一般来说,如果不会修改对象,那么可以使用const,尤其是形参,因为函数的形参类型可能不会一定完全匹配,把形参设定为const,编译器可以为形参设置自动转型,自动匹配
网友评论