目录
除非特别指出,C++中的继承默认为私有继承。
// 等效
class child : public base1, base2
{
// ...
}
// 等效
class child : base2, public base1
{
// ...
}
// 等效
class child : private base2, public base1
{
// ...
}
理论上,const
成员变量或基类成员变量都应该在构造函数之前初始化。C++提供了成员初始化列表来实现。
class base
{
double _number;
public:
base(double number);
virtual ~base();
};
class child : public base
{
const short _ARRSIZE;
double* _numbers;
public:
child(const double* arr, short size, double number = 0);
};
// 成员初始化列表
child::child(const double* arr, short size, double number) : base(number), _ARRSIZE(size)
{
_numbers = new double[_ARRSIZE];
for(int i = 0; i < _ARRSIZE; i++)
_numbers[i] = arr[i];
}
要实现多态公有继承,可以采用两种方法:
第一种方法在使用向上转换时会出现问题。
假设动态创建一个基类指针,用这个指针指向一个派生类对象。派生类的指针在储存时向上转换为基类指针。
如果采用第一种方法,用这个指针调用函数会使用基类版本,因为这样的实现会根据指针类型调用函数。
如果采用第二种方法,用这个指针调用函数会使用派生类版本,因为这样的实现会根据指针指向的对象的类型调用函数。
virtual
关键字即可,之后的派生类中的该函数会自动变成虚的。构造函数不能是虚函数,派生类都应该
析构函数必须是虚函数。这样可以保证在使用动态内存分配+向上转换,用delete
释放内存时可以调用正确的析构函数。
在调用派生类析构函数之后,编译器会自动调用基类的析构函数。这样一来让使用者不用顾忌基类的实现,而来能使基类和派生类以正确的顺序被释放。
友元不是成员函数,不不能声明为虚函数。不过要是想要友元具有虚函数的特性,可以让友元调用虚函数。
在派生类中重新定义函数(不管是不是虚函数)将隐藏原来的函数,而不会重载函数(即使参数列表不同)。
因此,考虑两条规则:
protected
除非遇到私有继承,否则protected
成员将一直保持为protected
。
public
成员在派生类中的访问控制与继承方式相同。
private
成员在派生类中都是不可见的。
public | protected | private | |
---|---|---|---|
公有继承 | public | protected | 不可见 |
私有继承 | private | private | 不可见 |
保护继承 | protected | protected | 不可见 |
protected
成员在基类和派生类(公有继承)中都类似私有成员。
私有继承就相当于在类中添加了一个未命名的私有基类对象
。
要使用基类的成员变量或成员函数,可以使用强制类型转换。
对于概念类似但是实现方法差别很大的两个类,用抽象基类(Abstract Base Class, ABC)作为它们的基类以实现多态。
语法规则:
纯虚函数:
// 在虚函数声明结尾加上“= 0”即可
virtual void show() const = 0;
抽象基类的存在只是为了描述派生类的共同特点(接口),为此纯虚函数可以没有定义。但C++中也允许定义纯虚函数。
ABC理念
operator=
:返回值和参数类型不同。想要有两种方式:
// 将派生类指针/引用**强制转换**为基类指针/引用。
// 这种方法常在友元函数中使用。
std::ostream & operator<<(std::ostream & os, const child & rs)
{
os << (const base &)rs;
return os;
}
// 给函数调用附上作用域解析。
// 这种方法常在成员函数中使用。
child & child::operator=(const child & rs)
{
base::operator=(rs);
return *this;
}
使用多重继承会出现问题。
class worker
{
// ...
};
class singer : public worker
{
// ...
};
class waiter : public worker
{
// ...
};
class singerwaiter : public singer, public waiter
{
// ...
};
singerwaiter ed;
worker *pw = &ed;
虽然看起来没问题,但其实这样会出现二义性(ambiguous),因为singerwaiter
对象分别从singer
对象和waiter
对象继承了一个waiter
对象。
一种做法是显式指出使用从谁那里继承来的waiter
对象。
singerwaiter ed;
worker *pw = (singer *)&ed;
先由(singer *)&ed
将&ed
转换为singer *
类型,再在赋值时隐式地将singer *
类型转换为worker *
类型。
这样虽然可以解决问题,但是将多态(用基类指针/引用可以指向不同的对象)复杂化了。
下面介绍另外一种方法。
在中间派生类(singer
,waiter
)声明时为基类(worker
)加上关键字virtual
可以保证在最终派生类(singerwaiter
)中只继承一个基类。
class worker
{
// ...
};
class singer : virtual public worker // 将worker声明为singer的虚基类
{
// ...
};
class waiter : public virtual worker // 将worker声明为waiter的虚基类
{
// ...
};
class singerwaiter : public singer, public waiter
{
// ...
};
可能会有这样的疑问:
virtual
?虚函数与虚基类之间的行为并不是很相似,使用virtual
只是挑了个比较好的已有关键字,避免引入新的关键字。
虚基类要求进行额外的运算,会增加开销,为了不需要的的工具付出额外的代价,这种事应该尽量避免。而且有时确实需要在最终的派生类中包含多个基类。
的确会产生问题。若这样做,在初始化最终派生类时将通过多个中间派生类将基类所需输出传递给基类的构造函数,但是虚基类只有一个。
为了避免这种冲突,C++不会通过中间派生类初始化基类。若不想使用基类的默认构造函数,需另外指出。
要注意的是,这种语法能对虚基类使用,对于非虚基类,这是非法的。
waiter::waiter(const worker& wk, int p) : worker(wk)
{
// ...
}
singer::singer(const worker& wk, int v) : worker(wk)
{
// ...
}
singwewaiter::singerwaiter(const worker& wk, int v, int p) : worker(wk), singer(wk, v), waiter(wk, p){}
singerwaiter(const singer &s, const waiter &w) : worker(s), singer(s), waiter(w){}
完整的测试代码如下:
class worker
{
int w;
public:
worker(int n){ w = n; }
worker(const worker &wk){ w = wk.w; }
};
class singer : virtual public worker // 将worker声明为singer的虚基类
{
int v;
public:
singer(const worker &wk, int v) : worker(wk){ this->v = v; }
};
class waiter : public virtual worker // 将worker声明为waiter的虚基类
{
int p;
public:
waiter(const worker &wk, int p) : worker(wk){ this->p = p; }
};
class singerwaiter : public singer, public waiter
{
public:
singerwaiter(const worker &wk, int v, int p) : worker(wk), singer(wk, v), waiter(wk, p){}
singerwaiter(const singer &s, const waiter &w) : worker(s), singer(s), waiter(w){}
};
int main()
{
worker john(10);
singer tom(john, 6);
waiter smith(john, 14);
singerwaiter sam(john, 6, 14);
singerwaiter jack(tom, smith);
}
如果没有在派生类中重新定义函数,则会调用基类的函数。但是现在有两个间接派生类作为基类。
为了说明要使用哪个函数,可以通过作用域解析运算符指出:
singerwaiter sam(john, 6, 14);
sam.singer::work();
sam.waiter::work();
更好的做法是在最终派生类中重新定义每一个可能发生冲突的函数(除非想用以上方式使用不同函数)(这种做法也允许选择性地使用不同函数)。
如果singer
和waiter
类中的work()
函数都调用了worker
的work()
函数,而在singerwaiter
类中的work()
函数又分别调用了singer
和waiter
类中的work()
函数。那么singerwaiter
类就要重复进行两次worker
的work()
。一般·情况下,这显然是不合理的。
为避免这个问题,一种方法是间接派生类的函数中额外提供一个只进行间接派生类中独有的工作的函数(这里就要用到protected
函数了)。
另一种方法是将基类和间接派生类中的成员变量都设置为protected
。不过用第一种方法可以更严格地控制对数据的访问。
注意这里的设计并不是严格按照is-a关系的,这样只是为了方便。
#include <string>
#include <iostream>
using namespace std;
class Father{
string name;
public:
Father(string s) : name(s) {}
~Father() { cout << 'F'; }
};
class Mother{
string name;
public:
Mother(string s) : name(s) {}
~Mother() { cout << 'M'; }
};
class Child : public Mother, public Father{
string name;
int age;
public:
Child(string f, string m, string c, int a) : Mother(m), Father(f), name(c), age(a) {}
~Child() {cout << 'C'; }
};
class Girl : public Father, public Mother{
Child child;
string name;
int age;
public:
Girl(string f, string m, string c, int a, string cf, string cm, string cc, int ca)
: Father(f), Mother(m), name(c), age(a), child(cf, cm, cc, ca){}
~Girl() {cout << 'G'; }
};
int main(){
{Child grandson("Zhang", "Li", "Ming", 3);}
cout << endl;
{Girl daughter("Zhao", "Liu", "Li", 26, "Zhang", "Li", "Ming", 3);}
return 0;
}
CFM
GCFMMF
从GCFMMF中看出,Girl
对象daughter
先调用自己的析构函数,然后销毁成员,调用成员Child
对象grandson
的析构函数,最后调用自己的基类Father
和Mother
的构造函数。
另外,grandson
和daughter
的构造函数的初始化列表初始化基类的顺序相同,继承顺序不同,而grandson
结尾是FM,daughter
的却是MF。
继承与析构函数调用顺序:
原文:https://www.cnblogs.com/alohana/p/12293198.html