类中的静态变量(不包括静态常量)只能在类外进行初始化,该变量由所有类对象所共享,且存储在静态存储区,即使创建多个类实例,该变量也只存在一个。初始化时需要加上变量的类型、作用域运算符,但不再需要加上
static
关键字。通过
new []
申请的内存,需要用delete []
来释放。如通过char *str = new char[10];
来申请的内存,需要通过delete [] str
来释放,而不是delete str;
,后者只是删除了该指针,并没有清空其指向的内存空间。类的特殊成员函数
- 构造函数:如果没有定义构造函数,将会自动生成默认构造函数;而当定义了构造函数,则需要手动定义默认构造函数。
复制构造函数:将已存在的类对象复制到新的对象中,其原型通常为
className (const className &);
,其接受一个指向类对象的常量引用作为参数。当新建一个对象并将其初始化为已存在的类对象时,将会调用复制构造函数,因而当按值传递类实例时会调用复制构造函数。默认生成的复制构造函数将会逐个复制非静态成员(且为浅复制),复制的是成员的值。通俗一点就是浅复制时,对于字符串的复制就只是复制其指针,而不是复制整个字符串。当类成员中使用了new来初始化的、指向数据的指针时,必须定义复制构造函数进行深度复制,否则析构时会出错。赋值函数:通过将一个已存在的类对象来对另一个已存在的对象进行赋值操作。其与复制构造函数的区别在于前者是对一个新对象进行初始化,而赋值函数是对已初始化(已存在)的对象进行赋值。其原型为
className & className::operator=(const className &);
. 需要注意的是,如果类中成员存在对通过new申请的内存的引用(如数组、字符串)的时候,赋值函数应该先将该引用的内存进行释放。此外,应该避免对自身进行赋值操作,因为赋值操作中清空内存会导致赋值出错。比如string类的赋值函数定义如下:MyString &MyString::operator=(const MyString &st) { if(this == &st) // 判断是否为本身,若是直接返回 return *this; delete [] str; // 释放原本的内存 int len = std::strlen(st.str); str = new char[len+1]; strcpy(str, st.str); // 深度复制 return *this; }
此外,如果要将某个数据传给某个对象时,如将字符串赋给某个对象,那么应该是
String name = str;
,此时实际上会通过构造函数以str
构造一个临时副本,然后调用赋值函数将该副本赋给目标对象,然后再调用析构函数将这个副本释放掉。为了提高处理效率,可以对赋值运算符进行进一步的重载:MyString & MyString::operator=(const char *s) { delete [] str; int len = std::strlen(s); str = new char[len+1]; strcpy(str, s); return *this; }
如果有多个构造函数,应该以相同的方式使用new, 因为只有一个delete。
静态类成员函数:类声明中用static修饰的函数,其不能通过类对象来调用,但可以通过类名和作用域解析运算符来调用;静态类成员函数不能使用
this
指针。由于静态类成员函数不与特定的对象关联,因而只能使用静态数据成员。
当new了一块内存块,然后用定位new运算符在该内存块上new一些对象,如果将该内存块delete,不会自动调用内存块上的对象的析构函数。注意,delete并不能和定位new运算符配合使用。对于定位new运算符的对象,应该显式地调用其析构函数,如
pc1->~JustTesting();
成员初始化列表:由逗号分隔的初始化列表(前边带冒号
:
)构成,位于参数列表的右括号之后、函数体左括号之前。Cpp中,对于常量的赋值需要在函数体开始之前进行,因而对于类内的常量成员变量,就必须需要通过成员初始化列表来对该变量进行初始化(不是赋值),此外,对于被声明为引用的类成员同样也需要这种方法来进行初始化(因为引用和常量类似,都是只能在创建的时候进行初始化). 对于本身就是类对象的成员来说,使用成员初始化列表的效率更高。对于公有派生,基类的公有成员将会称为派生类的公有成员,私有部分也会称为其中的一部分,但只能通过基类的公有和保护方法来进行访问。
派生类的构造函数应该为新成员和所继承的成员提供数据,这里可以通过对父类对象的引用进行修改以代替直接传入父类的成员变量。由于派生类无法直接访问父类的私有部分,因而派生类的构造函数应该使用父类的构造函数来初始化父类的私有成员,即创建派生类对象时,需要先创建父类对象。而当销毁派生类对象时,则先调用派生类自身的析构函数,然后再调用父类的析构函数。注意,除了虚基类外,类只能将值传递回相邻的基类。
派生类对象可以使用基类的公有/保护方法,而基类指针(或引用)可以在不进行显式类型转换的情况下指向(或引用)派生类对象, 但不可以调用派生类方法,因为基类不存在派生类中的新成员,因而调用派生类方法并无意义。相对的,派生类指针(或引用)不能指向基类指针(或引用)。
如果方法是通过引用或指针而不是对象进行调用的,那么将会通过引用类型或指针类型选择相应的方法,如用基类指针指向派生类,通过该指针调用某个同时存在于基类和派生类中的方法,那么调用的将会是基类中的方法。而如果将该方法用
virutal
来修饰,即将该方法声明为虚的,那么前边那种情况将调用派生类中的方法。方法如果在基类中声明为虚的,那么它在派生类中将自动成为虚方法。通常,如果在派生类中要重新定义基类的方法,那么通常将基类的方法声明为虚的,而为基类声明一个虚析构函数则是一种惯例,它可以确保释放派生类对象时可以按正确的顺序调用析构函数。virtual
关键字只用于类声明的方法原型中。源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。在编译过程中进行联编称为静态联编,也称为早期联编。而对于包含虚函数的文件,编译器并不知道用户将选择哪种类型的对象,因而也无法提前确定调用哪个函数,所以编译器必须生成在程序运行时选择正确的虚方法的代码,这被称为动态联编,也成为晚期联编。对于非虚方法,编译器采用的是静态联编,而对虚方法使用动态联编。如果类不会被用作基类,或者派生类不会重写基类的函数,那么就不需要动态联编,因为动态联编需要额外的开销(存储基类指针或引用指向的对象类型。)
虚函数实现原理:给每个类对象中添加一个隐藏成员,其中保存一个指针,该指针指向一个数组,而该数组存储基类中所有虚函数的地址表。如果派生类中提供了虚函数的重新定义,那么vtbl就保存新函数的地址,否则保存父类中该函数原始版本的地址(上一次定义时的地址)。
虚函数的成本:
- 类对象增大,因为需要加入一个指向虚函数表的指针
- 对于每个类,都需要建立一个虚函数表
- 函数调用时需要执行额外的操作,即到表中查找入口地址
创建一个指向派生类对象的父类指针(向上强制转换),如果要释放该指针指向的内存空间,按照静态联编的方法,将会调用父类的析构函数来释放父类中的成员指向的内存,而派生类中的新成员将不会被析构,因而会出现部分析构,最终也会导致内存泄漏的问题。因而,通过动态联编的方法,将析构函数定义为虚的,那么就会先调用派生类的析构函数析构派生类中的新成员,然后再调用父类的析构函数析构剩余部分。
友元不能是虚函数,因为友元不是类成员,只有成员才能是虚函数。
重新定义继承(父类)的方法,应确保与原来的原型完全相同,而如果父类函数的返回类型是父类引用或指针,则派生类中的对应函数可以修改为派生类的引用或指针。
如果父类中声明被重载了,那么派生类应该重新定义所有父类版本,否则其他重载函数将会被隐藏,派生类中无法进行调用。
保护成员:对于派生类而言,派生类可以直接访问父类的公有成员和保护成员,但不能直接访问基类的私有成员。而类外通过类对象只能直接访问类的公有成员,不能访问私有和保护成员。换句话说,对于外部世界,保护成员的行为和私有成员相似;而对于派生类而言,保护成员的行为和公有成员相似。
纯虚函数是一种特殊的虚函数,它是在虚函数后加上
=0
(参数列表右括号之后、函数体花括号之前)。不能创建包含纯虚函数的类对象,因为该类为抽象基类,派生类中可以不定义该函数。抽象基类要求派生类对其纯虚函数进行覆盖,从而使得派生类遵循抽象基类设置的接口规则,进而确保其派生类可以至少支持抽象基类指定的功能。总结:
- 参数列表中的const表明不会修改参数列表的值,而成员函数成名为const表明不会修改当前对象的值。
- 生成临时对象、将新对象初始化为一个同类对象、按值传递对象、按值返回对象时会调用复制构造函数。
- 默认的赋值运算符用于处理同类对象之间的成员赋值,如果需要用一种类型对象赋值给另一种类型的对象,需要显式定义赋值运算符。
- 应返回对象引用而不是返回对象,因为后者涉及生成返回对象的副本,会拉低效率,同样的,按值传递对象也会涉及对象的拷贝。注意不要返回函数中创建的临时对象的引用。
- 构造函数、析构函数和赋值运算符都不能被继承,而友元不属于类,因而也不能被继承。基类的析构函数通常声明为虚的,即用
virtual
关键字修饰,但如果在类外定义则不需要继续加上该关键字。 - 释放派生类时,先调用派生类的析构函数,然后向上调用父类的析构函数。