C++ 面向对象编程
面向对象编程基于三个基本概念:数据抽象、继承和动态绑定。
1 基类和派生类
1.1 定义基类
在基类中,除了构造函数之外,任意非 static 成员函数都可以是虚函数。
基类通常应将派生类需要重定义的任意函数定义为虚函数。
1.2 访问控制
(1)private 成员
• 通过类对象无法访问类的private成员。
• 在派生类中不能访问基类的private成员。
• private 成员只能在当前类的作用域内访问,类的友元也可以访问类的private 成员。例如,在成员函数中可以访问private 成员,在成员函数中还可以通过自己类的对象来访问类的private 成员。类的作用域包括:类定义的{}之内,类定义之外的成员函数的函数体,形参列表等。
• 通过类对象无法访问类的private成员。
• 在派生类中不能访问基类的private成员。
• private 成员只能在当前类的作用域内访问,类的友元也可以访问类的private 成员。例如,在成员函数中可以访问private 成员,在成员函数中还可以通过自己类的对象来访问类的private 成员。类的作用域包括:类定义的{}之内,类定义之外的成员函数的函数体,形参列表等。
class Base
{
public:
void Test1(Base& b)
{
b.iBase = 0;//有没有问题?
}
private:
int iBase;
};
class Derived : public Base
{
public:
void Test2(Base& b)
{
b.iBase = 0;//有没有问题?
}
void Test3(Derived& d)
{
d.iDerived = 0;//有没有问题?
}
private:
int iDerived;
};
(2)protected 成员
• 通过类对象无法访问protected 成员。
• protected 成员可被public派生类(包括派生类的派生类,向下传递)访问,也就是说在派生类中可以使用基类的protected 成员。
• 派生类只能通过派生类对象访问其基类的 protected 成员,派生类无法访问其基类类型对象的 protected 成员。
• 通过类对象无法访问protected 成员。
• protected 成员可被public派生类(包括派生类的派生类,向下传递)访问,也就是说在派生类中可以使用基类的protected 成员。
• 派生类只能通过派生类对象访问其基类的 protected 成员,派生类无法访问其基类类型对象的 protected 成员。
1.3 派生类
类派生列表指定了一个或多个基类,具有如下形式:
class classname: access-label base-class
这里 access-label 是 public、protected 或 private,base-class 是已定义的类的名字,类派生列表可以指定多个基类。
class classname: access-label base-class
这里 access-label 是 public、protected 或 private,base-class 是已定义的类的名字,类派生列表可以指定多个基类。
一旦函数在基类中声明为虚函数,它就一直为虚函数,派生类无法改变该函数为虚函数这一事实。派生类重定义虚函数时,可以使用 virtual 保留字,也可以不使用。
(1)派生类一般会重定义所继承的虚函数。如果派生类没有重定义某个虚函数,则使用基类中定义的版本。
(2)一般情况下,派生类中虚函数的声明必须与基类中的定义方式完全匹配,例外:返回对基类型A的引用(或指针)的虚函数。派生类中的虚函数可以返回类A的派生类的引用(或指针)。
提示:绝对不要重新定义继承而来的non-virtual函数
因为non-virtual函数是静态绑定的。一个子类对象绑定到一个父类指针,另一个子类对象绑定到一个子类指针,通过父类指针调用该函数,调用的是父类的该函数,而不是子类的函数。例如:
class Base
{
public:
void FuncTest()
{
std::cout << "Base" << std::endl;
}
};
class Derived: public Base
{
public:
void FuncTest()
{
std::cout << "Derived" << std::endl;
}
};
Derived d;
Base* pB = &d;
Derived* pD = &d;
d.FuncTest();//输出“Derived”
pB->FuncTest();//输出“Base”
pD->FuncTest();//输出“Derived”
1.4 non-virtual 和 virtual 函数的调用
(1) 将基类类型的引用或指针绑定到派生类对象,如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。
class Base
{
public:
void FuncTest(){std::cout << "Base" << std::endl;}
};
class Derived: public Base
{
public:
};
Derived d;
Base* pB = &d;
pB->VirtFunc();//输出“Base”
(2)将基类类型的引用或指针绑定到派生类对象,如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定的或指针所指向的对象所属类型定义的版本。
class Base
{
public:
virtual void VirtFunc()
{
std::cout << "Base" << std::endl;
}
};
class Derived: public Base
{
public:
void VirtFunc()
{
std::cout << "Derived" << std::endl;
}
};
Derived d;
Base* pB = &d;
Derived* pD = &d;
pB->VirtFunc();//输出“Derived”
pD->VirtFunc();//输出“Derived”
1.5 虚函数与默认实参
虚函数也可以有默认实参。如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
虚函数也可以有默认实参。如果一个调用省略了具有默认值的实参,则所用的值由调用该函数的类型定义,与对象的动态类型无关。通过基类的引用或指针调用虚函数时,默认实参为在基类虚函数声明中指定的值,如果通过派生类的指针或引用调用虚函数,则默认实参是在派生类的版本中声明的值。
提示: 绝不重新定义继承而来virtual函数的缺省参数值
不要重新定义继承而来的non-virtual函数,但是可以重新定义一个继承而来的virtual函数。
virtual函数是动态绑定,而该函数的缺省参数值却是静态绑定。C++这么做的就是为了提高运行期效率。
如果子类重新定义了继承而来的virtual函数的缺省参数值,那么使用父类指针指向子类对象,然后使用父类指针来调用该函数,所使用的默认参数仍然是从父类继承而来,而非子类重新定义定义的。例如:
class Base
{
public:
virtual void VirtFunc(string sMsg = "Base")
{
cout << sMsg << endl;
}
};
class Derived:public Base
{
public:
void VirtFunc(string sMsg = "Derived")
{
cout << sMsg << endl;
}
};
Derived d;
d.VirtFunc();//输出"Derived"
Base* pD = &d;
为了避免出现上面这种情况,必须将子类中继承而来的virtual函数设计的跟父类一样,也就是有同样的缺省参数值。如果父类修改了,子类也必须跟着同样修改。
替换的设计方案是:设计一个 public non-virtual函数(带有默认参数值)来调用private virtual函数(不带默认参数值)。public non-virtual函数在子类中不能重新定义,但是private virtual函数可以在子类中重新定义。
class Base
{
public:
void Func(string sMsg = "Base")
{
VirtFunc(sMsg);
}
private:
virtual void VirtFunc(string sMsg )
{
cout << sMsg << endl;
}
};
class Derived:public Base
{
private:
virtual void VirtFunc(string sMsg)
{
cout << sMsg << endl;
}
};
D d;
d.Func();//输出"Base"
B* pD = &d;
1.6 友元关系与继承
友元关系不能继承:
(1)基类的友元对派生类的成员没有特殊访问权限。
(2)友元类的派生类不能访问授予友元关系的类。
友元关系不能继承:
(1)基类的友元对派生类的成员没有特殊访问权限。
(2)友元类的派生类不能访问授予友元关系的类。
1.7 继承与静态成员
如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。
static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。
如果基类定义 static 成员,则整个继承层次中只有一个这样的成员。无论从基类派生出多少个派生类,每个 static 成员只有一个实例。
static 成员遵循常规访问控制:如果成员在基类中为 private,则派生类不能访问它。
如果可以访问成员,则既可以通过基类访问 static 成员,也可以通过派生类访问 static 成员。一般而言,既可以使用作用域操作符也可以使用点或箭头成员访问操作符。
2 转换和继承
2.1 派生类到基类的转换
(1)指针或引用
(1)指针或引用
派生类型引用到基类类型引用
派生类型指针到基类类型指针。
反之是不行的。
(2)对象
(2)对象
一般可以使用派生类型的对象对基类类型的对象进行初始化或赋值,但没有从派生类型对象到基类类型对象的直接转换,编译器不会自动将派生类型对象转换为基类类型对象。
3 构造函数和复制控制
3.1派生类构造函数
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。
构造函数和复制控制成员不能继承,每个类定义自己的构造函数和复制控制成员。像任何类一样,如果类不定义自己的默认构造函数和复制控制成员,就将使用合成版本。
派生类的合成默认构造函数,除了初始化派生类的数据成员之外,它还初始化派生类对象的基类部分。基类部分由基类的默认构造函数初始化。
派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。派生类构造函数只能通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
一个类只能初始化自己的直接基类(通过将基类包含在构造函数初始化列表中来间接初始化基类)。
派生类构造函数的初始化列表只能初始化派生类的成员,不能直接初始化继承成员。派生类构造函数只能通过将基类包含在构造函数初始化列表中来间接初始化继承成员。
class People
{
public:
People(std::string s1, int i1) : name("s1"),age(i1)
{
}
private:
std::string name;
int age;
};
class Student: public People
{
public:
Student(std::string s1, int i1,std::string s2) : uniName("s2"),People(s1,i1)
{
}
private:
std::string uniName;//学校名称
};
构造函数初始化列表为类的基类和成员提供初始值,它并不指定初始化的执行次序。首先初始化基类,然后根据声明次序初始化派生类的成员。一个类只能初始化自己的直接基类(通过将基类包含在构造函数初始化列表中来间接初始化基类)。
3.2 复制控制和继承
只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作。
只包含类类型或内置类型数据成员、不含指针的类一般可以使用合成操作。
具有指针成员的类一般需要定义自己的复制控制来管理这些成员。
(1) 派生类的复制构造函数
如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分:
class Base { /* ... */ };
class Derived: public Base
{
public:
Derived(const Derived& d):Base(d) { /*... */ }
};
如果派生类定义了自己的复制构造函数,该复制构造函数一般应显式使用基类复制构造函数初始化对象的基类部分:
class Base { /* ... */ };
class Derived: public Base
{
public:
Derived(const Derived& d):Base(d) { /*... */ }
};
(2)派生类赋值操作符
赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。
//Base::operator=(const Base&)
Derived &Derived::operator=(const Derived &rhs)
{
if (this != &rhs) //防止给自己赋值
{
Base::operator=(rhs); // 调用 Base 类的赋值操作符给基类部分赋值
……//为派生类Derived 的成员赋值
}
return *this;
}
赋值操作符通常与复制构造函数类似:如果派生类定义了自己的赋值操作符,则该操作符必须对基类部分进行显式赋值。
//Base::operator=(const Base&)
Derived &Derived::operator=(const Derived &rhs)
{
if (this != &rhs) //防止给自己赋值
{
Base::operator=(rhs); // 调用 Base 类的赋值操作符给基类部分赋值
……//为派生类Derived 的成员赋值
}
return *this;
}
(3)派生类析构函数
派生类析构函数不负责撤销基类对象的成员。每个析构函数只负责清除自己的成员。
class Derived: public Base
{
public:
~Derived() {/*... */}
class Derived: public Base
{
public:
~Derived() {/*... */}
};
(4)虚析构函数
如果层次中基类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。
建议:即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
(4)虚析构函数
如果层次中基类的析构函数为虚函数,则派生类析构函数也将是虚函数,无论派生类显式定义析构函数还是使用合成析构函数,派生类析构函数都是虚函数。
建议:即使析构函数没有工作要做,继承层次的根类也应该定义一个虚析构函数。
在复制控制成员中,只有析构函数应定义为虚函数,构造函数不能定义为虚函数。
建议:为多态基类声明为virtual析构函数
父类指针指向子类对象,delete父类指针时,如果父类含有non-virtual析构函数,则只有继承自父类的部分被销毁,而子类非继承的部分没有被销毁,也就是说子类对象被部分销毁。解决的办法是,将父类的析构函数定义为virtual析构函数。
class Base
{
public:
Base()
{
// std::cout << "Base的构造函数" << std::endl;
}
~Base()
{
std::cout << "Base的析构函数" << std::endl;
}
};
class Derived : public Base
{
public:
Derived()
{
// std::cout << "Derived的构造函数" << std::endl;
}
~Derived()
{
std::cout << "Derived的析构函数" << std::endl;
}
};
Base* pB = new Derived;
delete pB;//显示“Base的析构函数”
如果把Base的析构函数前面添加virtual,则上面结果输出:
Derived的析构函数
Base的析构函数
注意: 任何class只要带有virtual函数,几乎确定应该也有一个virtual析构函数。一般的,基类应该含有virtual析构函数。基类定义了virtual关键字,子类就不用添加该关键字了。
如果class不含有virtual函数,通常表示它并不打算被用作一个基类,所以就不应该将析构函数定义为virtual的。
如果class不含有virtual函数,通常表示它并不打算被用作一个基类,所以就不应该将析构函数定义为virtual的。
4 继承情况下的类作用域
4.1 名字冲突与继承
与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。
可以使用作用域操作符访问被屏蔽的基类成员:
struct Derived : Base
{
int get_base_mem() { return Base::mem; }
};
设计派生类时,只要可能,最好避免与基类成员的名字冲突。
与基类成员同名的派生类成员将屏蔽对基类成员的直接访问。
可以使用作用域操作符访问被屏蔽的基类成员:
struct Derived : Base
{
int get_base_mem() { return Base::mem; }
};
设计派生类时,只要可能,最好避免与基类成员的名字冲突。
4.2 作用域与成员函数
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽。
在基类和派生类中使用同一名字的成员函数,其行为与数据成员一样:在派生类作用域中派生类成员将屏蔽基类成员。即使函数原型不同,基类成员也会被屏蔽。
4.3 重载函数
如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。
如果派生类想要通过自身类型来使用重载的版本,那么派生类必须重定义所有的重载版本,但这样会繁琐,可以通过给重载成员提供using 声明来达到简化的目的。
using Base::Func;//注意,将所有基类Base中的Func函数在本类中可见。
如果派生类重定义了重载成员,则通过派生类型只能访问派生类中重定义的那些成员。
如果派生类想要通过自身类型来使用重载的版本,那么派生类必须重定义所有的重载版本,但这样会繁琐,可以通过给重载成员提供using 声明来达到简化的目的。
using Base::Func;//注意,将所有基类Base中的Func函数在本类中可见。
4.3 名字查找与继承
(1)首先确定进行函数调用的对象、引用或指针的静态类型。
(2)在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
(3)一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
(4)假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
(1)首先确定进行函数调用的对象、引用或指针的静态类型。
(2)在该类中查找函数,如果找不到,就在直接基类中查找,如此循着类的继承链往上找,直到找到该函数或者查找完最后一个类。如果不能在类或其相关基类中找到该名字,则调用是错误的。
(3)一旦找到了该名字,就进行常规类型检查,查看如果给定找到的定义,该函数调用是否合法。
(4)假定函数调用合法,编译器就生成代码。如果函数是虚函数且通过引用或指针调用,则编译器生成代码以确定根据对象的动态类型运行哪个函数版本,否则,编译器生成代码直接调用函数。
5 纯虚函数
在函数形参表后面写上 = 0 以指定纯虚函数。该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用。
含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。
在函数形参表后面写上 = 0 以指定纯虚函数。该函数为后代类型提供了可以覆盖的接口,但是这个类中的版本决不会调用。
含有(或继承)一个或多个纯虚函数的类是抽象基类。除了作为抽象基类的派生类的对象的组成部分,不能创建抽象类型的对象。