公有继承(is-a)
1 公有继承
//myclass.h
class A
{
private:
int x;
int y;
public:
A();
A(int x,int y);
}
class A_derive : public A //继承语法
{
private:
int z;
public:
A_derive();//隐式调用A的默认构造函数
A_derive(int x,int y,int z);//使用a初始化语法调用基类的构造函数
}
//myclass.cpp
A_derive(int x,int y,int z) : A(x,y)//初始化语法,ps:小计1
{
this->z = z ;
}
2 虚方法
根据对象类型来使用不同的同名函数-------多态
//父类
virtual void show();
//子类
void show();
如果没有使用关键词virtual ,程序将根据引用类型或指针选择方法,如果使用了virtual,程序将根据引用或指针指向的对象类型来选择方法。-----这里的对象类型指的是初始化时的类型。例如:假定B类继承了A类,则对象a的类型为A类.对象c的类型为A但是其引用指向对象类型为B类。
A a = B();
B b = B();
A & c = b;
对于有实际操作的析构函数,则需要设置虚析构函数,以完成正确的调用。
若无必须操作,如释放动态内存操作,则可以不明确强调,子类析构函数将会按序自动调用。
对于虚方法,若是在基类中声明,则在派生类的派生类仍是虚方法,即使派生类中未使用virtual强调
友元函数因为不是成员函数所以不能声明为虚,若遇到此方面的问题,可以使友元函数使用虚成员函数来解决。ps:记3
派生链中,将使用最新的虚方法。
重新定义将会隐藏虚方法,不会重载函数。------这里重新定义指为子类同名方法与父类方法函数参数(函数特征标)不同。实际测试,其将不具有虚函数的特点。所以如果使用虚函数将尽量使用同一特征标,但是对于返回类型可以变化(毕竟C++的返回类型不属于特征标的一部分),返回类型不同,称为返回类型协变。
详:此时基类中有同名重载函数:有参数与无参数,且全部声明(或无参数)为虚函数。派生类中只为无参数的进行覆写。此时派生类对象派生类引用无法调用有参数函数,即未继承未被覆写的有参函数。对于无参函数的调用仍符合虚函数规则。
同样未上述情况,此时派生类对象基类引用可以调用有参函数,对于无参函数的调用仍符合虚函数规则。
综上:对于若无虚函数声明,对于基类和派生类中有同名函数,则只是根据对象的类型(引用类型或指针类型)来进行访问,即派生类中同名函数将隐藏基类的同名函数,甚至是不同的重载版本。若存在虚函数声明,则即根据引用或指针对象的实际类型来进行访问函数,即出现了相同引用可以调用不同的函数的现象。
转:小计2
3 子类调用父类方法
void BrassPlus::ViewAcct()
{
Brass::ViewAcct();//通过类和作用域解析运算符来调用父类方法
}
4 抽象基类与纯虚函数
纯虚函数
virtual double Area() const=0;//纯虚函数
当一个类中有纯虚函数时,此类不能用于实例,其未抽象基类。
std::ostream & operator <<(std::ostream & os,const hasDMA & hs)
{
os << (const baseDMA &) hs;
....
return os;
}
包含(has-a)
1 新类成员为类对象。如下类student包含类string和类valvarry。
class student
{
private:std::string name;
std::valvarry <double> scores;
...
public:
...
}
2 在类student中可以使用类string和类valvarry所定义好的方法,由于成员为私有,所以在类外不能通过name和scores来使用所属于他们的成员函数。
3 对于构造函数,将使用对象名初始化语法,如
student::student(const string & a):name(a),scores()//初始化语法,与继承中的初始化有些许不同
{
...
}
私有继承(has-a)
1 与包含十分相似,可以将其理解为无名称的子对象成员。如下
class student:private std::string ,private std::valvarray<double>
{
...
public:
...
}
这里使用了类名来标识并初始化
2 相同的对于要在派生类中调用基类的方法,需要通过类名和作用域解析运算符来调用基类的方法。
double student::Average()
{
if(std::valarray<double>::size()>0)//类名和作用域解析运算符
{
return std::valarray<double>::sum()/std::valarray<double>::size();//类名和作用域解析运算符
}
else
{
return 0;
}
}
3 访问基类对象若需要将类成员string 通过 student 成员方法 返回给类外,将使用强制类型转换。
const string & student::Name() const
{
return (const string &) *this;
}
4 访问私有基类的友元,因为友元不属于类,所以使用类名来显式限定并不合适,我们使用显式转换为基类来调用友元
ostream & operator << (ostream & os ,const student & stu);
{
os << "score for "<<(xonst string & )stu <<":\n"
...
}
转:小计4
保护继承
1 保护继承是私有继承的变体,通过保护继承,基类的成员和方法将成为派生类的保护成员
class Student : protected std::string,protected std::valarray<double>
{
...
}
2 类外使用
对于派生类的保护方法,可以通过设计一个公有函数来调用类外不可访问的保护/私有方法。也可以使用using声明
double Student::sum() const
{
return std::valarray<double>::sum();//调用类外不可访问的保护方法
}
class Student : private std::string,private std::valarray<double>
{
...
public:
using std::valarray<double>::min;//using声明
using std::valarray<double>::max;//using声明
};
使用using声明,只使用成员名,无圆括号、函数特征标、和返回类型。这也使得两个版本(const和非const)的版本都可用。
老式的策略是:在私有派生类中的共有再次定义方法,但是现在已经被摒弃。
多重继承
基类Worker,中间类Singer和Waiter,以及多重继承中间类的SingingWaiter。
1 SingerWaiter中将有两个基类Worker ,所以通过基类指针或引用向上转型将会出现二义性。解决这种二义性可以使用强制类型转换类锁定基类。
Worker * pw1 = (Waiter *) & ed;
Worker * pw1 = (Singer *) & ed;
但是对于SingingWaiter其拥有一个Worker 更加符合现实逻辑。因此引入虚基类
2 虚基类
在继承声明时使用virtual进行修饰,且于public 的顺序无关。注意:在多重继承中,需要同时对中间派生类进行虚基类的继承,否则对于派生类的派生类对基类的直接初始化将造成二义性错误。
class Singer: virtual public Worker{...}
class Waiter: public virtual Worker{...}
对于SingerWaiter的初始化
SingingWaiter(const string& s, const long& l1,const long& l2, int n1, int n2, int n3) :Singer(s, l1, n1), Waiter(s, l2, n2), number_singingwaiter(n3) ,Worker(s,l1) {}
因为Singer于Waiter类继承虚基类Work,在构造函数传递过程中,对于SInger构造Worker的传递参数将失效,需要显式的调用基类Worker的构造方法或者默认调用Worker的默认构造方法,因此这也是上述产生二义性错误的原因。且对于普通的间接继承,直接初始化间接基类将是非法的。即若SInger,Waiter不是虚继承基类,在SingerWaiter将不可以使用Worker(s,l1)
进行初始化。
方法的二义性
若Singer类和Waiter类中都存在show方法,对于多重继承的SingingWaiter类,其实例化调用show将会出现二义性错误
- 可以使用域解析运算符类指定
SingingWaiter a = SingingWaiter("name",100,110,10,11,12);
a.Singer::show();
- 可以重新定义show函数
void show()
{
Singer::show();
Waiter::show();
cout << "number_singingwaiter" << number_singingwaiter << endl;
}
但是通过重新定义show函数又会出现重复显示Worker类信息,可以采用模块化设计理念,即设计单独显示每个类的所拥有的成员的函数,在各个类中show调用自己需要的data函数。同时为了保证data函数只是辅助函数,而不是为类外部的接口,使用protected修饰,若使用private则派生类将无法访问,若使用public将使得私有方法暴露于外。
混合继承如果虚继承与非虚继承同时存在,则虚继承将共有一个基类,而非虚继承则拥有一个或多个基类,同时在初始化或调用方法过程中需要明确指出(类名和域运算符)。
优先级 派生类的名称优先于直接或间接基类的相同名称,且与是否私有与共有无关,即是否可视与不可视,即使不可视但是优先级高,则也会调用并且报错。
class A
{
public: void f();
};
class B: public A
{
private:
void f();
};
class C: public B
{
void t()
{
f();//B::f(),invaild
}
};
类模板
定义
template <class T>
class A
{
private:
T a;
public :
A(const T & a);
void show();
};
模板类属于声明定义的模板,更像是预处理,所以其模板类的模板方法也是不能独立放在某个CPP文件的,而是放在声明的头文件里。
模板类的方法定义
template <class T>
void A<T>::show()
{
...
}
使用模板类
A <int> a = A<int>();
与模板函数不同的是,对于类型int,必须作为参数传递给类模板,而对于模板函数则可以通过匹配最佳的函数从而可以不需要显示的指定类型参数。
非类型参数
A.h
template <class T,int n>
class A
{
...
}
test.cpp
class A<double,19> a=class A<double,19>();
这里n既是非类型参数,它在调用时将会赋予一个整型值,在类中当作常量使用,在类外可以传入整型,枚举,指针,引用(其实这四种都是整型量)。在类中可以用n定义一个n长的数组。其相较于定长的数组类,它提供了一个变长的方法。相较于动态分配的,它节省了new和delect的开销。但是对于不同的n 它将依据模板创建不同的实例类,因此,对于赋值,它没有办法很好的处理(不同n之间的赋值),同时其会创建大量的代码,要权衡利弊使用。
多类型参数
template <class T1,class T2>
class A
{
...
}
默认类型模板参数
template <class T1,class T2=int>
class A
{
...
}
注意:与模板函数不同,它不能指定默认类型参数,但是对于非类型参数二者都可以。
隐式实例化,显示实例化,显示具体化
- 隐式实例化:在需要对象之前,其不会创建类模板的实例
class A<double,19> a;//未创建
a==class A<double,19>();//创建代码实例
- 显示实例化:无需等到需要创建对象之前
template class A <double,19>;//创建
- 显示具体化:为不同于其他实例模板提供一个特殊的“模板”
template <> class A<double,19>
{
...
}
- 部分具体化
//general template
template <class T1,class T2> class A{...};
//specialization with T2 set to int
template <class T1> class A<T1,int>{...};//部分具体化
//specialization with T1 and T2 set to int
template <> class A<int,int>{...};//显式具体化
在匹配时,它将会选择具体化最高的模板
A<double,double> p1;//general template
A<double,int> p2;//partial specialization
A<int,int> p3;//explicit specialization
具体化对于指针同上
template <class T> class A(...);//#1
template <class T*> class A(...);//#2
#2具体化高于#1,如果使用中有使用指针作为类型参数,将会选择#2,因为其匹配中具体化最高,且需要转换的最少。
template <class T1,class T2> class A<T1,T2,T2>{...};//多种使用部分具体化的用法
template <class T1> class A<T1,T1*,T1* >{...};//多种使用部分具体化的用法
类中的模板类和私有类
过于麻烦,见书p585
如果可以在类外定义,则
template <class T>
template <class V>
class beta<T>::hold//hold为模板类中的私有类成员
{
...
}
模板作为参数
template <template <class T> class Thing>
class A
{
private:
Thing<int> a;//
Thing<double> a;
};
A<stack> a = A<stack>();//stack==template <class T> class stack{...};
模板类和友元
- 非模板友元:这样的定义的模板友元,其更像是不同类型参数的友元,下面的代码只是对与模板的定义,经过实例化后才成为的友元。
template <class T>
class HasFriend
{
public:
friend void counts();
friend void report(HasFriend<T>& );//
}
counts()
{
cout<<HasFriend<int>::ct<<endl;
cout<<HasFriend<double>::ct<<endl;
}
report()函数本身不是模板函数,而只是使用了模板类的类型参数,不同版本的HasFriend类是相互不通的。
- 约束模板友元
template <typename T> void counts();
template <typename TT>
class HasFriendT
{
friend <> void counts<TT>();
}
这里counts是模板函数的具体化,同时也是HasFriendT类的友元,如果其反映的是HasFriendT类中某个静态计数器,他将独立的访问不同的计数器,且对于某个具体的计数器将会只能访问到自己具体的模板类。
- 非约束模板友元:其可以访问到所有的模板类的版本。
template <typename T>
class ManyFriend
{
private:
T item;
public:
template <typename C,typename D>
friend void show2(C&,D&);
}
template <typename C,typename D> void show2(C & c,D & d)
{
cout<<c.item<<" "<<d.item<<endl;
}
所以,这里的约束与不约束指的是友元函数是否是独立的,即<int>
版本的友元不能访问<double>
的成员
这即为约束。
友元
友元类:将原始类中的一个友元类声明为友元,它将具有访问原始类的所有成员的特性,其可以应用于电视机与遥控器之间的关系。
class A
{
public:
friend class B;//友元类声明
};
class B{};//友元类定义
友元成员函数:对于友元类的进一步缩减,在友元类中,大部分的函数并不需要访问到原始类中的私有成员,因此只需要将友元类的部分成员函数声明为友元成员函数,而取消掉友元类的友元。
class A;//原始类声明
class B//原友元类
{
public :
void f(A& a);//#1 友元成员函数
};
class A //原始类定义
{
private:
int a;
public:
A(int a)
{
this->a = a;
}
friend void B::f(A& a);//友元成员函数的声明
};
void B::f(A& a)//#1 友元成员函数的定义
{
cout << a.a << endl;
}
#2这里定义不能直接写到#1处,因为此时还不知其为原始类A的友元成员函数,但是#2声明为内联 inline 这是可以的
互为友元:当原始类也想访问友元类的私有成员时,此时也可以将原始类声明为友元类的友元。使用前向声明可以解决类型不兼容问题。
共同友元:当一个函数想要访问两个类的私有成员是,可以将其声明为两个类的友元,但是可能要用到前向声明。
嵌套类
在类中有在定义类,如果要在方法文件中定义嵌套类的方法,需要用类解析运算符和类名指定。
小记void show2(C&,D&);
1.子类先调用父类的构造方法,若子类构造方法未明写则调用默认构造方法。使用初始化语法。若是在函数体中父类构造方法将只是在构建局部变量。对于包含关系的类初始化,对于对象的初始化,将直接使用变量名加括号进行初始化。C++在构建类时,会首先构建继承和成员对象,如果没有这些的明确的初始化,将调用默认的初始化函数。初始化顺序为类声明的顺序而非类定义中的初始化顺序。p538
2.动态联编和静态联编:其概念与虚方法的提出有关,对于在程序运行过程中才确定对象使用的方法未动态联编,其实现方法为声明为虚函数。而静态联编在程序编译时,即已经对象的方法进行绑定。虽然动态联编更加适合人类的逻辑,但是静态联编效率优于动态联编,所以对于动态联编采取无必要不使用原则。
3 第二种处理友元函数的方法,因为派生类的友元不是基类的友元,所以友元函数不能访问到基类的成员,自然想到使用基类的同类型的友元函数,但同时它也不是类的成员,所以不能通过域解析运算符来指定使用(如果直接调用则会出现递归的现象),所以我们使用了强制类型转换使其向上转型(基类),来调用基类的友元。
4 对于has-a关系,我们应尽量选择包含关系,包含关系可以显式的命名对象,并且相对于私有继承,其可以拥有多个对象,同时它也不会出现多重继承中同一父类的问题。但是对于私有继承,它提供的特性比包含多,假设类中有保护成员,则私有继承可以访问到。另一个使用私有继承的是要重新覆写虚函数。
5 模板别名
template <typename T>
using arrtype = std::array<T,12>//arrtype为模板别名
arrtype<double> gallons;//gallons is type std::array<double,12>
arrtype<int> gallons;//gallons is type std::array<int,12>
同时,对于typedef
,using
可以完全替代
typedef const char * pc1;
using pc2 = const char *;
typedef const int*(*pa1)[10];
using pa2 = const int*(*)[10];
5 使用NULL时,需要包含NULL的头文件,所以也可以用0代替,如果编译器支持nullptr,尽量使用nulllptr.6
《持续更新》