条款3----不要以多态处理数组
-
基础
多态:多态性是指用一个名字定义不同的函数,这些函数执行不同的操作,这样就可以用同一个函数调用不同内容的函数,从而,可以用同样的接口访问功能不同的函数,即“一个接口,多种方法”。
在C++中,多态分为静态多态性和动态多态性。这就涉及到联编的概念。
联编也分为静态联编和动态联编。源程序经过编译、链接,成为可执行文件的过程是把可执行代码联编在一起的过程,其中在运行之前就完成的联编就是静态联编(也可称为静态绑定),而在运行时才完成的联编为动态联编(动态绑定)。静态联编:函数重载,运算符重载等操作在运行前即可确定执行的需要在静态联编中处理。
动态联编:虚函数等函数重写,需要在运行时才确定执行内容的需要进行在动态联编处理。
因此,静态多态多为函数重载,泛型编程等,动态联编多为虚函数重写
顺便记录关于函数重写,重载,重定义的概念。函数重载:在同一个作用域中,函数名字相同,但是参数必须不同,返回值可以不同,且返回值不同不能作为函数重载的条件。即根据函数参数确定函数入口地址。
函数重写:不在同一个作用域,即必须发生在父类及子类之间。函数名字、返回值及参数必须相同,且父类中函数必须带有virtual关键字,即必须是虚函数。
函数重定义:不在同一作用域,即发生在父类及子类之间。函数名字相同,返回值可以不同,且根据参数分为两种:
1.参数相同,父类函数必须不能为虚函数,即不可带有virtual关键字。父类函数被隐藏。
2.参数不同,无论是不是虚函数,父类函数被隐藏。 -
多态细节
谈及到多态,重点是动态多态,即使用虚函数重写。
虚函数:即在类的成员函数前面加virtual关键字。虚函数又分为纯虚函数和半虚函数。
纯虚函数:父类中虚函数不添加函数实现,且在括号后添加等于零。且在子类中必须重写。含有纯虚函数的类称为抽象类。抽象类不能有实例化对象,更加像一个接口。
半虚函数:父类中添加函数实现,子类可以重写也可以不重写。
动态多态发生条件:
1.必须是继承关系。
2.父类中必须存在虚函数。
3.通过父类的指针或者引用调用虚函数。
class People
{
public:
People(int age=10)
{
this->age = age;
}
virtual void Show()
{
cout<<"People Show"<<endl;
cout<<"People age "<< age<<endl;
}
int age;
};
class Student:public People
{
public:
Student()
{
}
Student(int age,int sex=1):People(age)
{
this->sex = sex;
}
virtual void Show()
{
cout<<"Student Show"<<endl;
cout<<"Student age "<< age<<endl;
}
private:
int sex;
};
int main()
{
People p1(20);
Student s1;
Student s2(30,2);
People * p = &s1;
People & r = s2;
People * q = &p1;
p->Show();
r.Show();
q->Show();
return 0;
}
//结论
Student Show
Student age 10
Student Show
Student age 30
People Show
People age 20
虚函数的实现原理:
虚函数主要是通过一张虚函数表来实现的。VTable,主要是虚函数的地址表。
一个类在内存中的分配:首先类只会分配变量内存,对于成员函数,存放在代码区,对于成员函数的不同对象调用依靠this指针,详细不在记录,所以如果一个类的对象创建,那么内存只会分配成员变量,如果是子类,会先分配父类成员变量内存,再分配子类特有成员变量内存。而对于含有虚函数的父类,创建父类对象时,会额外分配一个虚函数表指针,再分配成员变量内存。创建子类对象时,同样会将父类内存分配完后,再分配自己的变量。
因此,虚函数实现原理是通过修改虚函数表中的存放函数指针,首先,如果父类中有多个虚函数,这些虚函数会按照顺序将函数入口地址写入到虚函数表中,其次,如果子类含有特有虚函数,也会按照顺序添加到虚函数表中。
那么编译器是如何利用虚表指针与虚表来实现多态的呢?是这样的,当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数;同样,当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。
关于虚函数实现原理详细可看:
类虚函数内存分布
虚函数表解析
- 正文
以上就是多态基础内容,现在说明为什么不要以多态方式处理数组。
上面已经说明,子类继承父类后,内存可能会扩充,如果子类存在特殊成员变量,那么子类对象内存肯定要比父类对象内存要大。因此在数组操作中,如果发生多态行为,可能会因为内存不匹配发生错误指向,产生不可预期的结果。
class People
{
public:
//重载<<运算符,后面要用,省略
int age;
};
class Student:public People
{
public:
//重载<<运算符,后面要用,省略
int sex;
}
void Print(const People array[],int num)
{
for(int i=0;i<num;i++)
{
cout<<array[i]<<endl;
}
}
People bPeo[10];
Student bStu[10];
Print(bPeo,10);//指针指向正确
Print(bStu,10);//指针指向错误
分析:对于Print函数而言,array[i] 就相当于 (array+i);通过查找地址然后进行解引用,而array+i的地址计算,肯定是通过array的类型来计算的,即 isizeof(array);但是在函数参数中,array的类型为People,因此array+i 肯定是按照 isizeof(People)计算的,而本意是通过多态来打印信息的话,array+i 应该是isizeof(Student),因为Student与People的内存可能不同,从而导致array[i]可能不是正确的第i个对象,而是指针指向错误,导致发生不可预计的结果。
条款4----非必要不要提供默认的构造函数
-
基础
C++编译器会提供默认构造函数
如果提供了有参构造函数,编译器只提供拷贝构造函数。
如果提供了拷贝构造函数,编译器不会提供构造函数 -
正文
凡是可以合理的从无到有生成对象的class都应该提供默认构造函数。而 必须依靠外来信息才能生成对象的class尽量不必提供默认构造函数。
class People
{
public:
People()
{
}
~People()
{
}
}
class Student
{
public:
Student(int age)
{
this->age =age;
}
~Student()
{
}
int age;
}
People p[10];//正确
Student s[10];//错误
分析:
因为Student提供了有参构造函数,编译器不会再分配默认沟站函数,因此直接定义10个Student的数组,会调用无参构造函数,而在类中无法找到无参构造函数,又存在有参构造函数,编译器不会再分配默认构造函数,因此就会报错。
解决此项问题有三种方法:
1.直接声明参数,缺点:大量数组时都需要提供参数,繁琐
Student s[]={Student(1),Student(2),----}
2.定义指针数组,而不是对象数组,缺点:多使用了指针内存,且需要释放指针
Student* ps[10];
Student* ps1 = new Student*[10];
ps[i] = new Student(1);....
3.提前将数组内存分配,然后在这片内存上构造对象,缺点:提前分配内存的方法不常用,维护代码不方便,关于operator new操作主要看条款8
void *rawMem = operator new[](10*sizeof(Student));
Student* bS = static_cast<Student*>(rawMem);
new (&bS[i])Student(1);
由此可见,三种方法都不是特别方便。
如果每次都会将默认构造函数提供,那么就必须提供一个默认值来初始化,因此,就会存在一个非法值,这就导致在类中成员函数处理时需要考虑非法值的存在,从而产生异常等操作,添加了代码量及维护成本,如果类比较简单还可以,复杂的话反而将程序便复杂,因此不提倡提供默认构造函数。
结论:如果不提供默认构造函数,可能在定义数组时会报错,但是我们可以提前预知这个错误,且可以通过上面三个方法来规避,但是如果提供默认构造函数,代码量会增加,复杂异常情况会产生,因此不建议提供默认构造函数。