第十一章 构造函数和析构函数
- 构造函数的定义与重载
1.在创建对象(new)的时候调用构造函数.可以重载多个构造函数。但必须注意保留默认构造函数,和防止出现构造的歧义。
2.构造函数不需要被用户调用,也不能被用户调用 - 默认构造函数(不带参数或所有参数都有默认值)
C++中,在一个类中没有定义构造函数,不一定会自动生成默认构造函数。只有以下四种情况时才会生成。
1.带有含有默认构造函数的成员类。
2.继承自带有默认构造函数的基类。
3.类中带有虚函数。
4.带有虚基类的类。
注意点:在类的对象为数组时,编译器会为每个数组的元素调用默认的构造函数,所以必须使用默认构造函数初始化
#include<iostream>
using namespace std;
class Point{
public :
float x,y;
Point(){
cout<<"Create default constructer"<<endl;
}
Point(float x,float y){
this->x = x;
this->y = y;
}
~Point(){
}
};
int main(){
//Point p[5];
Point *p = new Point[5];
for(int i=0;i<5;i++){
cout<<p[i].x<<","<<p[i].y<<endl;
}
delete []p;
return 0;
}
- 拷贝构造函数
用来实现自定义类对象的赋值
定义拷贝函数时要遵循的一些规则有:
1.拷贝构造函数的名字必须与类名相同,并且无返回值。
2.只能有一个参数,且是类的一个地址引用。
3.如果不定义的话系统会自动生成一个 - 深拷贝与浅拷贝
深拷贝在拷贝的时候资源重新分配,浅拷贝则共享地址。
疑难解惑
- 派生类如何初始化基类继承的成员?
派生类的初始化列表必须明确指出基类的初始化形式。
第12章 运算符的重载
- 运算符重载的含义和注意点。
含义:扩展运算符的功能
注意点:
1.不能重载的运算符有 . 和.* 和::和?: 和sizeof,
2.只能重载已有的运算符
3.实质是函数重载,遵循函数重载的选择原则
4.运算符重载不影响运算符的优先级和结合性还有语法结构和操作数的个数
5.运算符重载不能改变用于内部类型的含义,对于新类型的数据不能与原功能有太大出入 - 重载运算符的形式
1.成员函数运算符
函数参数比原来的少一个(后置单目运算符除外),this隐式访问一个对象,左边的操作数。这也就是为什么后置单目运算符没有减少的原因,其左边没有运算符。
2.友元函数运算符
#include<iostream>
#include<string>
using namespace std;
class Document{
public :
Document(string name){
this->name = name;
}
void getName(){
cout<<name<<endl;
}
private :
string name;
};
class Book:public Document{
public :
Book(string name,int num=0):Document(name){
this->name = name;
this->num = num;
}
void getBook(){
cout<<name<<"有"<<num<<"本"<<endl;
}
private :
string name;
int num;
};
int main(){
Book x("杂志",12);
x.getBook();
return 0;
}
继承与静态成员
基类与派生类将共享该基类的静态成员变量内存。多继承
疑难解惑
1.在多继承中,两个基类具有同名变量,如何消除二义性。
使用::指定哪个类的成员
2.类不能继承基类的哪些特征。
构造函数,析构函数,用户定义的new运算符,用户定义的赋值运算符,友元关系。
第14章 虚函数与抽象类
- 虚函数的作用
在C++中虚函数是实现多态操作的主要手段之一。虚函数也是成员函数,在派生的时候被重新定义和赋予新的功能。这样不同类对象接收同一个消息,调用相同函数名,会做出不同的反应。在java中,子类重载父类同名函数时,系统自动实现了虚函数。
#include<iostream>
using namespace std;
class base{
public :
virtual void vfunc(){
cout<<"this is base's vfunc()"<<endl;
}
void vfunc2(){
cout<<"this is base's vfunc2()"<<endl;
}
};
class derived1:public base{
public :
//重写
void vfunc(){
cout<<"this is derived1's vfunc()"<<endl;
cout<<"这个类重写了父类的函数"<<endl;
}
};
class derived2:public base{
public :
void vfunc2(){
cout<<"this is derived2's vfunc()"<<endl;
cout<<"这个类没有重写父类的函数"<<endl;
}
};
int main(){
base *p,b;
derived1 d1;
derived2 d2;
p = &b;
p->vfunc();
p = &d1;
//有用定义为虚函数,子类重写父类vfunc();
p->vfunc();
p = &d2;
//由于没有使用virtual,子类不会重写父类的vfunc2,故该变量依旧调用父类中的函数;
p->vfunc2();
d2.vfunc2();
return 0;
}
动态绑定和静态绑定
静态绑定是发生在编译时期,如某函数依赖于对象的静态类型
动态绑定是发生在运行使其,即使用虚函数时,才具备动态绑定的特征。在Java中即为自动转型与多态的意义。
注意点:
执行动态绑定的只有通过地址,即只有通过指针或引用变量实现,而且还必须是虚函数。抽象类与纯虚函数
纯虚函数的定义
class base{
//纯虚函数只声明不定义
virtual void getName()=0;
};
抽象类的作用及要点
1.抽象类不能定义对象
2.如果一个类中的纯虚函数没有全部实现则为抽象类
3.抽象类是描述了相同属性的事物的一组公共操作的接口虚析构函数
使用虚析构函数是为了当用一个基类的指针删除派生类对象时,派生类的析构函数会被调用。抽象类的多重继承
- 虚函数的实现机制–虚函数表(V-Table)
1.虚函数表存储某个类的虚函数地址,及这个虚函数由哪个类继承实现。
2.使用虚函数的过程是这样的,通过一个对象地址寻找该表的地址,遍历该表保存的虚函数的地址,通过地址调用相应的函数。
#include<iostream>
using namespace std;
class Base{
virtual void f(){
cout<<"this is Base::f()"<<endl;
}
virtual void g(){
cout<<"this is Base::g()"<<endl;
}
};
int main(){
typedef void (*Fun)(void);
Base b;
Fun pFun = NULL;
cout<<"虚函数表的地址是"<<(int *)&b<<endl;
cout<<"虚函数表第一个函数地址是"<<(int *)*(int *)(&b)<<endl;
//调用第一个虚函数
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
//调用第二个虚函数
// pFun = (Fun)*((int*)*(int*)(&b));
// pFun();
return 0;
}
这里通过第一次取地址获得,虚函数地址表。然后在取一次地址得到虚函数表中的第一个虚函数地址。
- 继承关系的虚函数表
1.在子类中没有覆盖,则派生的虚函数放于之前定义的虚函数的后面
2.在子类中覆盖的话,则将之类的虚函数替换掉原来的虚函数,其余的不变
疑难解惑
虚函数和纯虚函数使用要点
1.含有纯虚函数的类不可实例化,还是抽象类
2.虚函数和纯虚函数不可以有static标识,这个是动态绑定,static是静态绑定在编程中虚函数的使用技巧
1.为了提高程序的清晰性,最好在类的每一个层次中显式声明这些虚函数。
2.没有定义虚函数的派生类简单的继承直接基类的虚函数
3.如果一个函数为虚函数,那么重新定义时没有声明这个虚函数,在之后类的继承结构中都是虚函数。