一、友元(friend):
友元(friend)使用friend关键字在类中声明,其虽然不是构成类的成员,但却可以访问类的私有成员;
友元有3种:友元函数、友元类、友元成员函数;
友元的声明可能牵涉到先定义后使用的原则及不完全类型(前向声明)。
①友元函数:
在类声明中使用friend关键字声明函数,使其成为该类的友元函数,获得该类的私有数据访问权:
class A
{
...;
friend void count_n();
}
友元方法描述的是对类原有接口的扩展。
//从可行性上说友元函数的声明位置无关紧要,其不受类成员访问类型的限制,原因很简单,因为友元函数根本就不是类方法,它是外部的,但它拥有和内部的成员方法一样访问私有数据的权限。(所谓friend也就是这样吧)
//从美观性和区分性上来说,friend为了突出是对公有接口的扩展,应当与公有接口public写在一起。
②友元类:
在类声明中使用friend关键字声明另一个类,使其作为该类的友元类,获得该类的私有数据访问权:
class A
{
public:
void counter();
};
class B
{
public:
friend class A; //具有访问B私有数据权限的类A
};
友元类描述的是两个类之间的配对关联。
③友元成员函数:
在类声明中使用friend关键字声明另一个类的成员函数,使其作为该类的友元成员函数,获得该类的私有数据访问权:
class A
{
public:
void counter();
};
class B
{
public:
friend void A::counter(); //具有访问B的私有数据权限的A成员函数
};
友元成员函数描述的是两个类之间的连接关系。
④先定义后使用的原则---前向声明:
使用一个名称之前至少要向编译器指出该名称是什么,这引出一个问题:
class A
{
public:
void counter(const B&); //编译器:what is B?
};
class B //直到这里B才被定义
{
public:
friend void A::counter(const B&);
};
由于使用了友元语法,B的声明在A之后,但A中的接口方法却使用到了B类型作为参数,在A中定义该方法时编译器将无法识别在这里写的B是什么东西(因为前面没有任何关于B的描述)。为解决描述上的问题,需要使用前向声明(forward declaration):
class B; //将B前向声明,指出B是一个类
class A
{
public:
void counter(const B&); //编译器:OK,B is a class.
};
class B //在后文给出B的定义
{
public:
friend void A::counter(const B&);
};
前向声明看上去就像是将函数的原型与定义分开那样,将类的声明和定义分开,通过先对名称模糊的描述使得名称可以使用,再在后文给出详细的定义,从而使得声明对于编译器成立。
//被前向声明的标识毕竟没有定义,所以它们是不完全类型,不完全类型只能以有限的方式使用,其只能允许声明指向该类型的引用、指针、返回值,或在函数内使用该类型的参数:
class B; //将B前向声明,指出B是一个类
class A
{
public:
B funct(const B);
B* b;
};
//友元可以带给人很多想象力,比如让一个拥有friend称谓的外物利用这样的权限去主动控制主体,然而毫无疑问这违背了OOP的规则,即使是friend也不能代替类方法主要控制对象行为,friend只用于描述一些关系,而对象的控制主权仍应属于对象自身的方法。
二、运算符重载(operator overloading):
运算符重载(operator overloading)意在实现专用于用户定义类型对象的运算符;
通过在类中声明使用operator关键字的运算符函数,对原有的C++运算符进行重载,使其获得新的含义,成为专用于用户定义类型对象得运算符:
class MyTime
{
private:
int m_hours; //小时
int m_minutes; //分钟
public:
MyTime(void);
MyTime(const int, const int);
void set(); //设置时间
void show(); //显示时间
MyTime operator+(const int)const; //重载+运算符使其可以应用于MyTime类型对象和int之间
~MyTime();
};
重载运算符函数的实现形式如下:
MyTime MyTime::operator+(const int n)const
{
MyTime result;
result.m_minutes = m_minutes + n;
result.m_hours = result.m_minutes > 60 ? m_hours + 1 : m_hours;
result.m_minutes = result.m_minutes > 60 ? result.m_minutes % 60 : result.m_minutes;
return result;
}
定义后就可以使用这样的表达式:
MyTime time;
int delta_time;
MyTime new_time = time + delta_time;
//有趣的是,如果稍作改动,以上的表达式就不再合法了:
MyTime time;
int delta_time;
MyTime new_time =delta_time + time; //虽然只是反过来写但该表达式并不合法
因为time是一个MyTime类型,delta_time是一个int类型,time+delta_time被转换为:
time.operator+(delta_time); //调用operator+成员方法实现加法运算
正是因为这种转换,operator+()作为类方法定义时并不需要两个参数,对象本身就是一个参数;
而delta_time是一个int,没有operator+(MyTime&)这样的方法,所以不合法;
int类型无法定义operator,因此使用友元实现反向加法:
class MyTime
{
private:
int m_hours;
int m_minutes;
public:
MyTime(void);
MyTime(const int, const int);
void set();
void show();
MyTime operator+(const int)const;
friend MyTime operator+(const int, const MyTime&); //使用友元重载操作符,需要两个参数
~MyTime();
};
友元不是类方法,没有调用对象,所以必须使用两个参数指明是哪两个数据相加;
此时上面的写法成为合法:
MyTime time;
int delta_time;
MyTime new_time1 = time + delta_time;
MyTime new_time2 = delta_time + time;
//事实上,友元经常被用于实现运算符的重载。
//常用的友元重载运算符就是operator<<:
friend std::ostream& operator<<(ostream& os, const MyTime& time)
{
os << M.m_hours << m_minutes;
return os;
}
这使得重载完成后cout可以直接显示MyTime类型对象了:
MyTime time;
cout<< time; //显示time的内容
返回ostream&类型使得<<可以连用:
MyTime time;
cout<< time << time; //连用<<
三、使用动态内存的类:
使用动态内存的目的是为了使对象可以在run-time再决定(而不是编译时就决定)其具体数据;
通过在类中包含指针成员,并在构造函数中将其初始化为指向heap区内存,以实现类的动态联编:
①一个简单的类:
class Stringbug
{
char*m_pchr;
static int count_cons;
static int count_des;
public:
Stringbug(const char*);
Stringbug(void);
~Stringbug();
void show_count()const;
operator char*()const; //定义了到char*类型的转换函数
friend std::ostream&operator<<(std::ostream&, const Stringbug&);
};
实现:
1>静态成员:
int Stringbug::count_cons = 0; //记录构造函数的累计调用次数
int Stringbug::count_des = 0; //记录析构函数的累计调用次数
//静态成员在类外部初始化时需要使用类名称作为其标记。
2>构造函数:
Stringbug::Stringbug(const char*s) //以const char*作为参数的构造函数
{
++count_cons;
int len = strlen(s);
m_pchr = new char[len + 1]; //使用new申请heap区内存并用指针m_pchr指向
strcpy_s(m_pchr, len + 1, s); //向该指针所指向的地址处存入字符串
}
Stringbug::Stringbug() //默认构造函数
{
m_pchr = NULL;
}
让指针作为类的成员,而在构造函数中使用new为其分配heap区内存,使其指向heap区,达到动态储存的目的。
值得注意的是:其中出现了字符串使用指针进行存取的典型手法:
int len = strlen(s); //获取字符串字面值的长度
m_pchr = new char[len + 1]; //使用new申请heap区内存并用指针m_pchr指向
strcpy_s(m_pchr, len + 1, s); //向该指针所指向的地址处存入字符串
其方法可以总结为:测长→分配内存→存取
//存取字符串字面值时需要的内存大小=字符串长度+1;(C-style string结尾的结尾标记"\0")
典型的错误写法为:
int len = strlen(s);
strcpy_s(m_pchr, len + 1, s);
类成员m_ptr实际上是未进行初始化的指针,没有指向任何地址(或指向任意地址),不指定所指内存就将字面值参数向其所指位置进行存取,得到的结果显然是不对的。
3>类方法:
void Stringbug::show_count()const //显示析构函数和构造函数的调用累计次数
{
std::cout << count_cons << " time of construction is used\n";
std::cout << count_des << " time of destruction is used\n";
}
4>转换函数:
Stringbug::operator char *()const
{
return m_pchr;
}
5>析构函数:
Stringbug::~Stringbug()
{
delete[]m_pchr; //释放m_pchr成员指向的heap区内存
++count_des; //析构函数被调用次数+1
}
析构函数使用与new对应的delete删除成员指针指向的内存(块);
值得注意的是:由于这里new分配的是供字符串(字符数组)使用的一组而非单纯的一块内存,所以使用了delete[ ],删除单块内存时应当使用delete而不是delete[ ];
②Stringbug类出现的问题(多次释放同一块内存):
1>由复制构造函数造成的问题:
当主程序以下面方式使用Stringbug类:
int main()
{
using std::cout;
using std::endl;
{
//第一次创建
Stringbug bug_ = "bug1";
cout << bug_ << endl;
bug_.show_count();
cout << endl;
//新建对象并使用已有的对象初始化
Stringbug bug_1 = bug_;
cout << bug_1 << endl;
bug_1.show_count();
cout << endl;
}
system("pause");
return 0;
}
程序于此位置停止并崩溃;
从运行结果可以看出,构造函数总计只被调用了一次,而当程序试图第二次调用析构函数时便引起了错误;
导致错误的一行:
Stringbug bug_1 = bug_;
事实上,这一行为调用的是复制构造函数(而非两个被显式定义的构造函数),Stringbug类并未显示的定义复制构造函数,因此将使用默认复制构造函数,执行逐成员浅度复制:
Stringbug::Stringbug(const Stringbug&s)
{
char*m_pchr = s.m_ptr; //浅度复制
}
这将导致bug_和bug_1的m_ptr指向同一块heap区内存,在二者的析构函数被先后调用时,对m_ptr指向的内存第二次释放,将引起错误。
//另一方面,由于复制构造函数还用于生产临时对象,所以只要产生临时对象的场合都会引起该错误。(如:传值调用、返回非引用)
解决方法:显式的定义复制构造函数,使其执行深度复制:
class Stringbug
{
char*m_pchr;
static int count_cons;
static int count_des;
public:
Stringbug(const char*);
Stringbug(void);
Stringbug(const Stringbug&); //显式定义复制构造函数
~Stringbug();
void show_count()const;
operator char*()const;
friend std::ostream&operator<<(std::ostream&, const Stringbug&);
};
复制构造函数的实现:
Stringbug::Stringbug(const Stringbug&s)
{
++count_cons; //构造函数累计调用计数+1
std::cout << "copy function is called" << std::endl;
int len = strlen(s); //测长
m_pchr = new char[len + 1]; //分配新的地址
strcpy_s(m_pchr, len + 1, s.m_pchr); //写入数据
}
两次调用析构函数释放不同的heap区内存,此后程序可以正常运行。
2>由赋值运算符造成的问题:
对于已经修复了由复制构造函数引起的重复释放同一块内存问题的Stringbug类,仍然有着重复释放同一块内存的风险:
int main()
{
using std::cout;
using std::endl;
{
//第一次创建
Stringbug bug_ = "bug1";
cout << bug_ << endl;
bug_.show_count();
cout << endl;
//对已初始化的对象再次进行赋值操作
Stringbug bug_2; //默认构造函数
bug_2 = bug_; //对象间的赋值
cout << bug_2 << endl;
bug_2.show_count();
cout << endl;
}
system("pause");
return 0;
}
从运行结果可以看出,仍存在同一块heap区内存被释放两次的错误;
导致错误的一行:
Stringbug bug_2; //默认构造函数
bug_2 = bug_; //对象间的赋值
这一行为属于赋值操作,而非初始化,因此不调用任何构造函数(构造函数只在创建和初始化时被使用)。赋值操作使用了赋值运算符(=),Stringbug类并未显示的重载operator=(),因此将使用默认的operator=(),逐成员执行浅度复制:
Stringbug& Stringbug::operator=(const Stringbug&s)
{
char*m_pchr = s.m_ptr; //浅度复制
return *this;
}
这将导致bug_和bug_2的m_ptr指向同一块heap区内存,在二者的析构函数被先后调用时,对m_ptr指向的内存第二次释放,将引起错误。
解决方法:显式的定义operator=(),使其执行深度复制:
class Stringbug
{
char*m_pchr;
static int count_cons;
static int count_des;
public:
Stringbug(const char*);
Stringbug(void);
Stringbug(const Stringbug&); //显式定义复制构造函数
~Stringbug();
void show_count()const;
operator char*()const;
Stringbug& operator=(const Stringbug&); //显示重载赋值运算符
friend std::ostream&operator<<(std::ostream&, const Stringbug&);
};
operator=()的实现:
Stringbug& Stringbug::operator=(const Stringbug&s)
{
std::cout << "operator=() is called" << std::endl;
if (this == &s) //首先判断是否是自身赋值
return *this; //如果是则返回自己
delete[]m_pchr; //如果不是自身赋值,先释放原有自身指针指向的heap区内存
int len = strlen(s); //测长
m_pchr = new char[len + 1]; //分配新的内存
strcpy_s(m_pchr, len + 1, s); //写入
return *this; //返回自身
}
可以看到复制操作的时候operator=()被调用,之后的两次析构函数调用释放两块不同的heap区内存,此后程序正常运行。
四、继承(inherit):
继承(inherit)通过使派生类在基类方法和数据的基础上派生出新的特性,描述了类之间的高级从属关系;
继承有三种方式:公有继承、私有继承、保护继承;
继承牵涉到成员初始化列表的使用。
①描述is-a-kind-of关系的继承(公有继承):
1>使用public关键字指出基类以公有继承的方式被派生类继承:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
virtual ~Baseclass();
};
class Derivedclass :public Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
~Derivedclass();
};
公有继承描述了派生类“是一种(is-a-kind-of)”基类类型的从属关系;
2>公有继承的特性:
(1)基类公有接口(public)成为派生类公有接口(public)。这描述了派生类对象拥有和基类相同的行为及控制接口,派生类是以基类为基础,对基类不改变本质的扩展;
(2)基类私有成员(private)成为派生类不可见数据。这描述了派生类和基类的区别,派生类对象不得以任何方式(即使是域运算符::)直接访问基类私有成员,派生类和基类并不是一个类;
(3)基类保护成员(protected)成为派生类保护成员(protected)。该特性只有在多重继承(MI)时才得以体现;
(4)对于公有继承,基类指针可以指向派生类对象。换言之,派生类对象可以向基类对象隐式转换(向上转换),向上转换是一种缩窄,除去派生类独有部分而保留其基类部分使之成为基类对象:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
virtual ~Baseclass();
};
class Derivedclass :public Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
void text()const; //用于测试指针调用的函数
~Derivedclass();
};
现有以下两个指针:
Baseclass B(a, b);
Derivedclass D(a, b, c, d);
Baseclass* p_b = &D; //用基类指针指向派生类对象D
Derivedclass* p_d = &D; //用派生类指针指向派生类对象D
那么调用p_b时只能使用派生类的基类部分,无法使用text(),派生类对象D被当做基类使用;
而调用p_d时却可以使用派生类的所有部分,可以使用text()。
②描述has-a关系的继承(私有继承):
1>使用private关键字指出基类以私有继承的方式被派生类继承:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
virtual ~Baseclass();
};
class Derivedclass :private Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
~Derivedclass();
};
私有继承描述了派生类“有一个(has-a)”基类类型(作为组件)的包含关系;
2>私有继承的特性:
(1)基类公有接口(public)成为派生类私有成员(private)。这描述了派生类对象拥有和基类不同的行为及控制接口,派生类不是一种基类,但派生类对象包含了一个基类对象组件。派生类使用基类完善本身,完全改变了基类的本质;
(2)基类私有成员(private)成为派生类不可见数据。这描述了派生类和基类的区别,派生类对象不得以任何方式(即使是域运算符::)直接访问基类私有成员,再次强调了派生类和基类并不是一个类;
(3)基类保护成员(protected)成为派生类私有成员(private)。该特性只有在多重继承(MI)时才得以体现;
(4)不同于公有继承,对于私有继承,基类指针不可以指向派生类对象。换言之,私有继承不支持隐式自动的向上转换,因为派生类并不是基类的一种。但私有继承的向上转换仍可以通过使用强制类型转换实现:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
virtual ~Baseclass();
};
class Derivedclass :private Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
void text()const; //用于测试指针调用的函数
~Derivedclass();
};
现有以下两个指针:
Baseclass B(a, b);
Derivedclass D(a, b, c, d);
Baseclass* p_b = &(Baseclass)D; //用基类指针指向派生类对象D
Baseclass* p_b=&D; //不合法
Derivedclass* p_d = &D; //用派生类指针指向派生类对象D
3>改变继承而来的公有接口的访问类型:
在派生类公有接口部分使用using声明改变继承而来的基类公有接口的可见性:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
void text()const;
virtual ~Baseclass();
};
class Derivedclass :private Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
using Baseclass::text; //text()接口被继承在派生类公有(public)部分
~Derivedclass();
};
被using声明的接口成为派生类公有接口,对外部可见:
Derivedclass D(1,1,1,1);
D.text(); //可以作为公有接口直接调用
//使用using声明改变继承接口的访问类型时,被声明的方法后不写(),只需声明方法名即可。
//using声明语法也可用于公有继承,经管不那么常用。
③保护继承:
但层次的保护继承看上去与私有继承相似;
保护继承的特性只在多重继承(MI)时表现出来。
④继承的注意事项:
1>成员初始化列表:不论是描述哪一种关系的继承,基类私有成员在派生类中均被进一步封装,派生类对象不得以任何方式直接访问基类私有成员,这引出一个问题:派生类不能直接接触其基类部分的私有数据,那么基类部分的私有数据便不能通过派生类的任何成员函数(包括构造函数)初始化。所以只能在派生类构造函数调用之前就调用基类构造函数才能初始化其基类部分的私有数据,成员初始化列表正式实现这一目的的手段:
Baseclass::Baseclass(int b_a,int b_b) //基类构造函数
{
b_m_a = b_a;
b_m_b = b_b;
}
Derivedclass::Derivedclass(int b_a, int b_b, int d_a, int d_b) :Baseclass(b_a, b_b) //派生类构造函数,使用成员初始化列表初始化其基类部分私有数据
{
d_m_a = d_a;
d_m_b = d_b;
}
成员初始化列表使得可以在派生类构造函数被调用之前调用基类构造函数,使其基类部分的私有成员先被初始化。
正因为不论是描述哪一种关系的继承,派生类对象不得以任何方式直接访问基类私有成员,
所以不论是描述哪一种关系的继承,其构造函数都必须(至少对其基类部分的私有成员)使用成员初始化列表。
//若派生类构造函数不使用成员初始化列表,则视为对基类默认构造函数的隐式调用:
Baseclass::Baseclass(int b_a,int b_b) //基类构造函数
{
b_m_a = b_a;
b_m_b = b_b;
}
Derivedclass::Derivedclass(int b_a, int b_b, int d_a, int d_b) //派生类构造函数不使用成员初始化列表初始化其基类部分私有数据
{
d_m_a = d_a;
d_m_b = d_b;
}
相当于:
Derivedclass::Derivedclass(int b_a, int b_b, int d_a, int d_b):Baseclass()
{
d_m_a = d_a;
d_m_b = d_b;
}
不幸的是由于Baseclass已经显式声明了带参数的构造函数,所以编译器将不会为其定义默认构造函数,又由于默认构造函数同样没有显式定义,所以Baseclass没有默认构造函数。
所以以上Derivedclass构造函数为非法。
2>继承后的同名方法是共存的:若派生类中定义了与基类同名的public接口方法,那么在派生类中,基类版本的该方法不会被取代,两个版本的方法均存在。
对于公有继承来说,同名方法以派生类版本优先:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
void text()const;
virtual ~Baseclass();
};
class Derivedclass :public Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
void text()const;
~Derivedclass();
};
Baseclass B(1, 1);
Derivedclass D(1, 1, 1, 1);
D.text(); //调用Derivedclass版本的text()
D.Baseclass::text(); //调用Baseclass版本的text(),必须使用类名域运算符(::)标记
对于私有继承来说,同样是派生类版本优先,但由于基类public被继承为派生类private,所以只能在成员函数定义中调用基类同名方法,但也必须加上域运算符(::):
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
void text()const;
virtual ~Baseclass();
};
class Derivedclass :private Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
void text()const;
void d_funct()const;
~Derivedclass();
};
void Derivedclass::d_funct()const
{
text(); //调用Baseclass版本的text()
Baseclass::text(); //调用Derivedclass版本的text(),必须使用类名域运算符(::)标记
return;
}
3>为派生类重载operator<<:
假设基类operator<<()如下:
friend std::ostream& operator<<(std::ostream&os, const Baseclass&B)
{
os << b_m_a << b_m_b;
return os;
}
则派生类operator<<()应如下:
friend std::ostream& operator<<(std::ostream&os, const Derivedclass&D)
{
os << (const Baseclass&)D; //显式调用基类operator<<()显示基类部分
os << d_m_a << d_m_b;
return os;
}
由于友元并不是成员函数,所以不论哪个类都可以调用,所以这种方法对所有类型的继承都有效。
4>虚析构:不论是描述哪一种关系的继承,基类析构函数都必须使用virtual关键字声明为虚方法。
⑤使用操作heap区指针的类与继承:
对于使用操作heap区指针的类,其作为基类时,同样应为其定义复制构造函数与重载赋值运算符:
class Baseclass
{
char* m_Bpc; //操作heap区的指针
public:
Baseclass(); //默认构造函数
Baseclass(const char*); //构造函数
Baseclass(const Baseclass&); //复制构造函数
Baseclass& operator=(const Baseclass&); //重载赋值运算符
virtual ~Baseclass();
};
其实现与普通的使用操作heap区指针的类一致:
//默认构造函数
Baseclass::Baseclass()
{
m_Bpc = new char[1];
strcpy_s(m_Bpc, 1, "");
}
//构造函数
Baseclass::Baseclass(const char*Bpc)
{
int len = strlen(Bpc); //测长
m_Bpc = new char[len + 1]; //分配内存
strcpy_s(m_Bpc, len + 1, Bpc); //写入
}
//复制构造函数
Baseclass::Baseclass(const Baseclass&B)
{
int len = strlen(B.m_Bpc); //测长
m_Bpc = new char[len + 1]; //分配内存
strcpy_s(m_Bpc, len + 1, B.m_Bpc); //写入
}
//重载赋值运算符
Baseclass& Baseclass::operator=(const Baseclass&B)
{
if (this == &B) //检测自身赋值
return *this;
delete[] m_Bpc; //删除原有数据
int len = strlen(B.m_Bpc); //测长
m_Bpc = new char[len + 1]; //分配内存
strcpy_s(m_Bpc, len + 1, B.m_Bpc); //写入
return *this;
}
//析构函数
Baseclass::~Baseclass()
{
delete[] m_Bpc; //释放heap区
}
需要注意的是其被继承的两种情况,以公有继承为例:
1>派生类独有部分不使用操作heap区的指针:
class Derivedclass_:public Baseclass
{
char* m_Dpc; //不操作heap区的指针
public:
Derivedclass_();
Derivedclass_(const char*, char*);
~Derivedclass_();
};
其实现不需要做任何特殊操作:
//默认构造函数
Derivedclass_::Derivedclass_():Baseclass()
{}
//构造函数
Derivedclass_::Derivedclass_(const char*Bpc,char*Dpc):Baseclass(Bpc)
{
m_Dpc = Dpc;
}
//析构函数
Derivedclass_::~Derivedclass_()
{}
2>派生类独有部分使用操作heap区的指针:
class Derivedclass_useheap :public Baseclass
{
char*m_Dpc;
public:
Derivedclass_useheap(); //默认构造函数
Derivedclass_useheap(const char*, const char*); //构造函数
Derivedclass_useheap(const Derivedclass_useheap&); //复制构造函数
Derivedclass_useheap&operator=(const Derivedclass_useheap&); //重载赋值运算符
~Derivedclass_useheap();
};
其实现有一些特殊性:
//默认构造函数
Derivedclass_useheap::Derivedclass_useheap():Baseclass()
{
m_Dpc = new char[1];
strcpy_s(m_Dpc, 1, "");
}
//构造函数
Derivedclass_useheap::Derivedclass_useheap(const char*Bpc, const char*Dpc) :Baseclass(Bpc)
{
int len = strlen(Dpc); //测长
m_Dpc = new char[len + 1]; //分配内存
strcpy_s(m_Dpc, len + 1, Dpc); //写入
}
//复制构造函数
Derivedclass_useheap::Derivedclass_useheap(const Derivedclass_useheap&D) :Baseclass(D)
{
int len = strlen(D.m_Dpc); //测长
m_Dpc = new char[len + 1]; //分配内存
strcpy_s(m_Dpc, len + 1, D.m_Dpc); //写入
}
Derivedclass_useheap&Derivedclass_useheap::operator=(const Derivedclass_useheap&D)
{
if (&D == this) //自身赋值检测
return *this;
Baseclass::operator=(D); //显式调用基类operator=()以初始化基类部分
delete[] m_Dpc; //删除原有数据
int len = strlen(D.m_Dpc); //测长
m_Dpc = new char[len + 1]; //分配内存
strcpy_s(m_Dpc, len + 1, D.m_Dpc); //写入
return *this;
}
//析构函数
Derivedclass_useheap::~Derivedclass_useheap()
{
delete[] m_Dpc; //释放heap区
}
五、虚函数(virtual)与多态(polymorphism):
①公有继承中的多态(polymorpism):多态意在使用统一的接口根据上下文来执行不同的操作,使同样的接口拥有不同的含义。
在公有继承中,基类指针可以指向派生类对象,但只能作为基类使用,这个特性是为了体现派生类对象也是一种基类类型。由于基类根本不具有派生类的方法,所以基类类型指针不具备调用派生类方法的权力,即使它指向的是一个派生类。
而多态正是要使得可以使用基类指针,根据上下文决定把调用对象当做基类还是派生类使用,即实现用同一个规格的指针来操作不同的整个系列的所有类的对象;
虚函数(virtual)正是实现多态的重要手段:
使用virtual关键字将成员函数声明为虚函数,指出这个函数可能在继承前后出现不同的版本,从而使得它可以随机应变的调用:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
virtual void text()const; //在基类声明中使用virtual指出text()成为虚函数
virtual ~Baseclass();
};
class Derivedclass :public Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
virtual void text()const; //派生类声明中同样使用virtual指出text()为虚函数
~Derivedclass();
};
对于使用了virtual后的text(),调用时将根据其指向的对象本身来确定调用的对象作为什么类型使用:
Baseclass B(1, 1);
Derivedclass D(1, 1, 1, 1);
Baseclass* p[] = {&B, &D};
p[0]->text(); //调用Baseclass的text()
p[1]->text(); //调用Derivedclass的text()
使用virtual关键字声明成员函数使得同名的成员函数可以被按对象区分,基类类型指针拥有了调用派生类方法的权限。
//值得注意的是,区分并不意味着替代,继承前后同名方法是并存的,这一特性并未改变,使用基类类型的指针同样可以用域运算符(::)指出调用派生类基类部分的公有接口:
Baseclass B(1, 1);
Derivedclass D(1, 1, 1, 1);
Baseclass* p[] = {&B, &D};
p[0]->text(); //调用Baseclass的text()
p[1]->Baseclass::text(); //调用Derivedclass的Baseclass部分的text()
②虚函数的使用注意事项:
1>构造函数不能为虚。
构造函数不参与继承,所以不能为虚。
2>基类析构函数必须为虚函数。
如果基类析构函数不为虚函数,那么使用基类类型的指针操作派生类时,析构函数调用将会出错:
Baseclass B(1, 1);
Derivedclass D(1, 1, 1, 1);
Baseclass* p[] = {&B, &D};
对于以上声明,若基类析构函数不为虚,则由于使用的是基类类型指针,所以都只会调用基类析构函数:
p[0]->~Baseclass();
p[1]->~Baseclass();
然而p[1]指向的是一个Derivedclass对象,其作为派生类的独有部分将不能被正确析构,如果该部分使用了操作heap区内存的指针,将导致严重的错误。
3>友元不能为虚。
友元不是成员函数。
4>使用virtual关键字重写的方法,应确保与原来的原型(接口部分)完全相同,而只有定义部分不同。
若在派生类中对虚方法接口也进行重写,将导致派生类无法使用基类版本的方法:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
virtual void text()const; //在基类声明中使用virtual指出text()成为虚函数
virtual ~Baseclass();
};
class Derivedclass :public Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
virtual void text(int)const; //对接口部分的重写将导致派生类无法使用原版本
~Derivedclass();
};
此时这样的调用被禁止:
Derivedclass D(1, 1, 1, 1);
D.Baseclass::text(); //Baseclass版本的text()不再能被Derivedclass对象使用
//值得注意的是,虽然接口部分必须完全相同,但返回值却可以在Baseclass&和Derivedclass&之间改变:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
virtual Baseclass& text()const; //在基类声明中使用virtual指出text()成为虚函数
virtual ~Baseclass();
};
class Derivedclass :public Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
virtual Derivedclass& text(int)const; //将返回值在Baseclass&和Derivedclass之间互相改变不会引起原方法不可用
~Derivedclass();
};
这种特例只适用于返回值,且必须是在Baseclass&和Derivedclass&之间变化时有效。
5>对有重载版本的函数使用virtual进行重写,必须重写其所有版本。
只重写某一个重载版本将导致派生类只能使用这个版本的基类方法,其他均不可用:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
virtual Baseclass& text()const; //在基类声明中使用virtual指出text()成为虚函数
virtual ~Baseclass();
};
class Derivedclass :public Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
virtual Derivedclass& text(int)const; //将返回值在Baseclass&和Derivedclass之间互相改变不会引起原方法不可用
~Derivedclass();
};
因此,只需要重写某一个重载版本的场合可以直接调用基类的原对应方法:
Derivedclass& Derivedclass::text()const
{
Baseclass::text();
}
③纯虚函数与抽象基类(ABC):在需要声明接口但不能为其提供实现的场合,在virtual关键字声明的虚函数原型后使用“=0”指出其为纯虚函数:
class Baseclass
{
private:
int b_m_a;
int b_m_b;
public:
Baseclass(int, int);
virtual Baseclass& text()const = 0; //在虚函数原型结尾加上=0使其成为纯虚函数
virtual ~Baseclass();
};
class Derivedclass :public Baseclass
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
virtual Derivedclass& text()const;
~Derivedclass();
};
纯虚函数的作用:
1>纯虚函数确定了派生类在重写该方法时应遵守的接口规范,派生类只能使用已给定的接口形式重写这些方法。但并不禁止为纯虚函数提供定义。
2>纯虚函数使得基类成为抽象基类(ABC),抽象基类只能用作基类而不能创建对象。
//ABC是对类的高级抽象,是对多个类的统一特点的描述。
④虚基类与多重继承(MI):
1>多重继承:
class Baseclass_1
{
private:
int b_m_a;
public:
Baseclass_1(int);
virtual ~Baseclass_1();
};
class Baseclass_2
{
private:
int b_m_b;
public:
Baseclass_2(int);
virtaul ~Baseclass_2();
};
class Derivedclass :public Baseclass_1, public Baseclass_2 //Derivedclass继承自Baseclass_1和Baseclass_2
{
private:
int d_m_a;
int d_m_b;
public:
Derivedclass(int, int, int, int);
~Derivedclass();
};
从多个基类获得继承特性。
用于描述 既有...又有...(私有MI),既是一种...又是一种...(公有MI) 这一类复合的包含关系。
//派生类声明访问类型时,必须全部指明访问类型:
class Derivedclass :public Baseclass_1, Baseclass_2 //Baseclass_2未指明访问类型,默认为private继承
若不指明访问类型,则默认为私有继承(private)。
2>组件重复与虚基类的引入:
当一个或多个派生类含有相同的祖先时,就可能导致继承而来的组件重复:
//基类
class Baseclass
{
private:
int b_m_a;
public:
Baseclass() { b_m_a = 0; };
virtual ~Baseclass() {};
};
//派生自Baseclass的第二层同祖先派生类
class Derivedclass_1:public Baseclass
{
private:
int d_m_a;
public:
Derivedclass_1() :Baseclass() { d_m_a = 0; };
virtual ~Derivedclass_1() {};
};
class Derivedclass_2 :public Baseclass
{
private:
int d_m_b;
public:
Derivedclass_2() :Baseclass() { d_m_b = 0; };
virtual ~Derivedclass_2() {};
};
//派生自Derivedclass_1和Derivedclass_2的第三层派生类
class D_D:public Derivedclass_1,public Derivedclass_2
{
private:
int d_d_m_;
public:
D_D() :Derivedclass_1(), Derivedclass_2() { d_d_m_ = 0; };
virtual ~D_D() {};
};
int main()
{
//
D_D d_d;
Baseclass*p_b = &d_d; //d_d包含两个重复的Baseclass组件,导致编译器不明确基类指针应该指向哪一个,导致语句不合法
//
system("pause");
return 0;
}
重复的基类组件将导致类成员混乱。
为解决同祖先MI带来的组件重复,引入虚基类:
//基类
class Baseclass
{
private:
int b_m_a;
public:
Baseclass() { b_m_a = 0; };
virtual ~Baseclass() {};
};
//派生自Baseclass的第二层同祖先派生类
class Derivedclass_1:virtual public Baseclass //在同祖先继承时使用virtual关键字声明访问类型
{
private:
int d_m_a;
public:
Derivedclass_1() :Baseclass() { d_m_a = 0; };
virtual ~Derivedclass_1() {};
};
class Derivedclass_2 :virtual public Baseclass
{
private:
int d_m_b;
public:
Derivedclass_2() :Baseclass() { d_m_b = 0; };
virtual ~Derivedclass_2() {};
};
//派生自Derivedclass_1和Derivedclass_2的第三层派生类
class D_D:public Derivedclass_1,public Derivedclass_2 //在同祖先继承时使用virtual关键字声明访问类型
{
private:
int d_d_m_;
public:
D_D() :Baseclass(), Derivedclass_1(), Derivedclass_2() { d_d_m_ = 0; }; //类方法的定义方式也相应的改为模块式的初始化
virtual ~D_D() {};
};
int main()
{
//
D_D d_d;
Baseclass*p_b = &d_d; //d_d只包含一个基类组件,语句合法
//
system("pause");
return 0;
}
引入虚基类后,同祖先的MI不再具有重复组件,使得继承体系模块化。
相应的,定义所有的类方法也不再使用递进调用,转而使用模块化定义:
原先的简介递进调用定义方式不再奏效:
D_D() :Derivedclass_1(), Derivedclass_2() { d_d_m_ = 0; }; //通过调用上一层类方法,再让上一级方法调用基类方法逐层调用定义的方式不再有效
而改为模块化的定义:
D_D() :Baseclass(), Derivedclass_1(), Derivedclass_2() { d_d_m_ = 0; }; //初始化方式也相应的改为模块式的初始化
基类方法组件被单独定义,派生类方法不再调用上一级方法,每一层方法只定义属于本层的内容。
//如果把原来的层次递进的定义方式描述为“一种传递”,那么虚基类所使用的模块化的定义方式就是“一种拼装”。
//虚基类非多重继承是否会共享组件?
#include "stdafx.h"
#include<iostream>
class Base
{
private:
int m_B;
public:
Base() { m_B = 0; };
Base(const int&b) :m_B(b) {};
virtual ~Base(); //非ABC
virtual void set(const int&b=0); //公有继承中被重写的方法一定要声明为virtual 防止公有接口重名导致基类同名方法不可见
friend std::ostream&operator<<(std::ostream&, const Base&);
};
class Derived_1:virtual public Base //作为虚基类 公有继承
{
private:
char m_D1;
public:
Derived_1() :Base(), m_D1('a') {};
Derived_1(const int&b, const char d1) :Base(b) { m_D1 = d1; };
virtual ~Derived_1() {};
void set_m_D1(const char d1 = 'a') { m_D1 = d1; }; //派生类独有模块初始化
virtual void set(const int&b = 0, const char d1 = 'a') { Base::set(b); set_m_D1(d1); }; //模块化组合初始化 而非 递进的初始化
friend std::ostream&operator<<(std::ostream&, const Derived_1&);
};
class Derived_2 :virtual public Base
{
private:
char m_D2;
public:
Derived_2() :Base(), m_D2('b') {};
Derived_2(const int&b, const char d2) :Base(b) { m_D2 = d2; };
virtual ~Derived_2() {};
void set_m_D2(const char d2 = 'b') { m_D2 = d2; };
virtual void set(const int&b = 0, const char d2 = 'b') { Base::set(b); set_m_D2(d2); };
friend std::ostream&operator<<(std::ostream&, const Derived_2&);
};
class DD1:public Derived_1
{
private:
int m_DD1;
public:
DD1() :Derived_1(), m_DD1(0) {};
DD1(const int&b, const char d1, const int&dd1) :Derived_1(b, d1) { m_DD1 = dd1; };
~DD1() {};
void set(const int&b = 0, const char d1 = 'a', const int&dd1 = 0) { Derived_1::set(b, d1); m_DD1 = dd1; };
friend std::ostream&operator<<(std::ostream&, const DD1&);
};
class DD2 :public Derived_2
{
private:
int m_DD2;
public:
DD2() :Derived_2(), m_DD2(0) {};
DD2(const int&b, const char d2, const int&dd2) :Derived_2(b, d2) { m_DD2 = dd2; };
~DD2() {};
void set(const int&b = 0, const char d2 = 'b', const int&dd2 = 0) { Derived_2::set(b, d2); m_DD2 = dd2; };
friend std::ostream&operator<<(std::ostream&, const DD2&);
};
//应用
int main()
{
using std::cout;
using std::endl;
DD1 dd1;
DD2 dd2;
dd1.set(10, 'g', 6); //只初始化dd1,不初始化dd2
cout << "dd1:" << dd1 << endl;
cout << "dd2:" << dd2 << endl; //测试未被初始化的 dd2的组件情况
//dd2仍保持初始值 不与dd1共享Base组件
system("pause"); //不共享意味着 即使声明为虚基类继承也可以继续向下继承
return 0; //共享的特性只会在同祖先多重继承时表现出来
}
//Base methods
Base::~Base()
{}
void Base::set(const int&b)
{
m_B = b;
}
std::ostream&operator<<(std::ostream&os, const Base&B)
{
os << B.m_B;
return os;
}
//D1 methods
std::ostream&operator<<(std::ostream&os, const Derived_1&D1)
{
os << (const Base&)D1 << "," << D1.m_D1;
return os;
}
//D2 methods
std::ostream&operator<<(std::ostream&os, const Derived_2&D2)
{
os << (const Base&)D2 << "," << D2.m_D2;
return os;
}
//DD1 methods
std::ostream&operator<<(std::ostream&os, const DD1&DD1)
{
os << (const Derived_1&)DD1 << "," << DD1.m_DD1;
return os;
}
//DD2 methods
std::ostream&operator<<(std::ostream&os, const DD2&DD2)
{
os << (const Derived_2&)DD2 << "," << DD2.m_DD2;
return os;
}
3>虚二义性规则:虚基类的引入导致每一级方法都是唯一的。对于不同层级之间同名方法,派生类方法优先于直接或间接祖先类中的方法(即使派生类中的同名方法为private):
//基类
class Baseclass
{
public:
short Funct_1();
...;
}
//第一层派生
class Derivedclass_1:virtual public Baseclass
{
public:
long Funct_1();
int Funct_2();
...;
}
class Derivedclass_2:virtual public Baseclass
{
int Funct_2();
...;
}
//第二层派生
class D_d1:virtual public Derivedclass_1
{
...;
}
//第三层派生
class D_dd1_d2:public D_d1, public Derivedclass_2
{
...;
}
对于第三层派生类D_dd1_d2:Funct_1()是指Derivedclass_1::Funct_1(),即使它是private;Funct_2()不能指代任何方法,因为Derivedclass_1和Derivedclass_2为平行关系。
对于非优先的继承类方法,必须使用作用域运算符(::)调用。