More Effective C++ 学习记录2

条款3----不要以多态处理数组

  1. 基础

    多态:多态性是指用一个名字定义不同的函数,这些函数执行不同的操作,这样就可以用同一个函数调用不同内容的函数,从而,可以用同样的接口访问功能不同的函数,即“一个接口,多种方法”。

    在C++中,多态分为静态多态性和动态多态性。这就涉及到联编的概念。
    联编也分为静态联编和动态联编。源程序经过编译、链接,成为可执行文件的过程是把可执行代码联编在一起的过程,其中在运行之前就完成的联编就是静态联编(也可称为静态绑定),而在运行时才完成的联编为动态联编(动态绑定)。

    静态联编:函数重载,运算符重载等操作在运行前即可确定执行的需要在静态联编中处理。

    动态联编:虚函数等函数重写,需要在运行时才确定执行内容的需要进行在动态联编处理。

    因此,静态多态多为函数重载,泛型编程等,动态联编多为虚函数重写
    顺便记录关于函数重写,重载,重定义的概念。

    函数重载:在同一个作用域中,函数名字相同,但是参数必须不同,返回值可以不同,且返回值不同不能作为函数重载的条件。即根据函数参数确定函数入口地址。

    函数重写:不在同一个作用域,即必须发生在父类及子类之间。函数名字、返回值及参数必须相同,且父类中函数必须带有virtual关键字,即必须是虚函数。

    函数重定义:不在同一作用域,即发生在父类及子类之间。函数名字相同,返回值可以不同,且根据参数分为两种:
    1.参数相同,父类函数必须不能为虚函数,即不可带有virtual关键字。父类函数被隐藏。
    2.参数不同,无论是不是虚函数,父类函数被隐藏。

  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指针,详细不在记录,所以如果一个类的对象创建,那么内存只会分配成员变量,如果是子类,会先分配父类成员变量内存,再分配子类特有成员变量内存。而对于含有虚函数的父类,创建父类对象时,会额外分配一个虚函数表指针,再分配成员变量内存。创建子类对象时,同样会将父类内存分配完后,再分配自己的变量。
因此,虚函数实现原理是通过修改虚函数表中的存放函数指针,首先,如果父类中有多个虚函数,这些虚函数会按照顺序将函数入口地址写入到虚函数表中,其次,如果子类含有特有虚函数,也会按照顺序添加到虚函数表中。
那么编译器是如何利用虚表指针与虚表来实现多态的呢?是这样的,当创建一个含有虚函数的父类的对象时,编译器在对象构造时将虚表指针指向父类的虚函数;同样,当创建子类的对象时,编译器在构造函数里将虚表指针(子类只有一个虚表指针,它来自父类)指向子类的虚表(这个虚表里面的虚函数入口地址是子类的)。
关于虚函数实现原理详细可看:
类虚函数内存分布
虚函数表解析

  1. 正文
    以上就是多态基础内容,现在说明为什么不要以多态方式处理数组。
    上面已经说明,子类继承父类后,内存可能会扩充,如果子类存在特殊成员变量,那么子类对象内存肯定要比父类对象内存要大。因此在数组操作中,如果发生多态行为,可能会因为内存不匹配发生错误指向,产生不可预期的结果。
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----非必要不要提供默认的构造函数

  1. 基础
    C++编译器会提供默认构造函数
    如果提供了有参构造函数,编译器只提供拷贝构造函数。
    如果提供了拷贝构造函数,编译器不会提供构造函数

  2. 正文
    凡是可以合理的从无到有生成对象的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);

由此可见,三种方法都不是特别方便。
如果每次都会将默认构造函数提供,那么就必须提供一个默认值来初始化,因此,就会存在一个非法值,这就导致在类中成员函数处理时需要考虑非法值的存在,从而产生异常等操作,添加了代码量及维护成本,如果类比较简单还可以,复杂的话反而将程序便复杂,因此不提倡提供默认构造函数。

结论:如果不提供默认构造函数,可能在定义数组时会报错,但是我们可以提前预知这个错误,且可以通过上面三个方法来规避,但是如果提供默认构造函数,代码量会增加,复杂异常情况会产生,因此不建议提供默认构造函数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值