目录
C++ 类 & 对象
C++ 类定义
定义 C++ 对象
访问数据成员
类 & 对象详解
C++ 类成员函数
C++ 类访问修饰符
公有(public)成员
私有(private)成员
保护(protected)成员
继承中的特点
public 继承
protected 继承
private 继承
C++ 类构造函数 & 析构函数
类的构造函数
带参数的构造函数
使用初始化列表来初始化字段
类的析构函数
C++ 拷贝构造函数
C++ 友元函数
C++ 内联函数
C++ this指针
C++ 指向类的指针
C++ 类的静态成员
C ++ 静态成员函数
C++ 继承
基类 & 派生类
访问控制和继承
继承类型
多继承
C++ 重载运算符和重载函数
C++ 中的函数重载
C++ 中的运算符重载
可重载运算符/不可重载运算符
C++ 一元运算符重载
C++ 二元运算符重载
C++ 关系运算符重载
C++ 输入/输出运算符重载
C++ ++ 和 -- 运算符重载
C++ 赋值运算符重载
C++ 函数调用运算符 () 重载
C++ 多态
虚函数
纯虚函数
C++ 接口(抽象类)
设计策略
关键字 public 确定了类成员的访问属性。在类对象作用域内,公共成员在类的外部是可访问的。
对象 Box1 和 Box2 都有它们各自的数据成员。
类的对象的公共数据成员可以使用直接成员访问运算符 (.) 来访问。
需要注意的是,私有的成员和受保护的成员不能使用直接成员访问运算符 (.) 来直接访问。我们将在后续的教程中学习如何访问私有成员和受保护的成员。
到目前为止,我们已经对 C++ 的类和对象有了基本的了解。下面的列表中还列出了其他一些 C++ 类和对象相关的概念,可以点击相应的链接进行学习。
概念描述类成员函数类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。类访问修饰符类成员可以被定义为 public、private 或 protected。默认情况下是定义为 private。构造函数 & 析构函数类的构造函数是一种特殊的函数,在创建一个新的对象时调用。类的析构函数也是一种特殊的函数,在删除所创建的对象时调用。C++ 拷贝构造函数拷贝构造函数,是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。C++ 友元函数友元函数可以访问类的 private 和 protected 成员。C++ 内联函数通过内联函数,编译器试图在调用函数的地方扩展函数体中的代码。C++ 中的 this 指针每个对象都有一个特殊的指针 this,它指向对象本身。C++ 中指向类的指针指向类的指针方式如同指向结构的指针。实际上,类可以看成是一个带有函数的结构。C++ 类的静态成员类的数据成员和函数成员都可以被声明为静态的。类的成员函数是指那些把定义和原型写在类定义内部的函数,就像类定义中的其他变量一样。类成员函数是类的一个成员,它可以操作类的任意对象,可以访问对象中的所有成员。
成员函数可以定义在类定义内部,或者单独使用范围解析运算符 :: 来定义。在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符。
class Box { public: double length; // 长度 double breadth; // 宽度 double height; // 高度 double getVolume(void) { return length * breadth * height; } };也可以在类的外部使用范围解析运算符 :: 定义该函数。
double Box::getVolume(void) { return length * breadth * height; }在这里,需要强调一点,在 :: 运算符之前必须使用类名。调用成员函数是在对象上使用点运算符(.),这样它就能操作与该对象相关的数据。
Box myBox; // 创建一个对象 myBox.getVolume(); // 调用该对象的成员函数注意:在成员函数中,如果形参和属性同名会出错。
//正确 void setLength( double len ) { length = len; } //错误 void setLength( double length ) { length = length; }除了为形参起不同于属性的名字这个方法之外,还可以用this->length=length这样的写法,或者使用作用域符Box::length。
:: 叫作用域区分符,指明一个函数属于哪个类或一个数据属于哪个类。
:: 可以不跟类名,表示全局数据或全局函数(即非成员函数)。
C++中函数调用非虚成员函数、调用虚函数的区别
调用非虚成员函数:和调用非成员函数一样,通过对象确定对象所属的类,然后找到类的成员函数。此过程不会涉及到对象的内容,只会涉及对象的类型,是一种静态绑定。
调用虚函数与调用非虚成员函数不同,需同过虚函数表找到虚函数的地址,而虚函数表存放在每个对象中,不能再编译期间实现。只能在运行时绑定,是一种动态绑定。
数据封装是面向对象编程的一个重要特点,它防止函数直接访问类类型的内部成员。类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的。关键字 public、private、protected 称为访问修饰符。
一个类可以有多个 public、protected 或 private 标记区域。每个标记区域在下一个标记区域开始之前或者在遇到类主体结束右括号之前都是有效的。成员和类的默认访问修饰符是 private。
class Base { public: // 公有成员 protected: // 受保护成员 private: // 私有成员 };公有成员在程序中类的外部是可访问的。您可以不使用任何成员函数来设置和获取公有变量的值。
#include <iostream> using namespace std; class Line { public: double length; }; int main( ) { Line line; line.length = 10.0; // OK: 因为 length 是公有的 return 0; }私有成员变量或函数在类的外部是不可访问的,甚至是不可查看的。只有类和友元函数可以访问私有成员。
默认情况下,类的所有成员都是私有的。例如在下面的类中,width 是一个私有成员,这意味着,如果您没有使用任何访问修饰符,类的成员将被假定为私有成员。
实际操作中,我们一般会在私有区域定义数据,在公有区域定义相关的函数,以便在类的外部也可以调用这些函数。
#include <iostream> using namespace std; class Box { public: double length; void setWidth( double wid ); double getWidth( void ); private: double width; //width是私有数据 }; // 成员函数定义 double Box::getWidth(void) { return width ; //类的成员函数可以访问类的私有数据 } void Box::setWidth( double wid ) { width = wid; //类的成员函数可以访问类的私有数据 } // 程序的主函数 int main( ) { Box box; // 不使用成员函数设置长度 box.length = 10.0; // OK: 因为 length 是公有的 // 不使用成员函数设置宽度 // box.width = 10.0; // Error: 因为 width 是私有的 box.setWidth(10.0); // OK:使用成员函数设置宽度 return 0; }保护成员变量或函数与私有成员十分相似,但有一点不同,保护成员在派生类(即子类)中是可访问的。
在下一个章节中,您将学习到派生类和继承的知识。现在您可以看到下面的实例中,我们从父类 Box 派生了一个子.类 smallBox。
#include <iostream> using namespace std; class Box { protected: double width; }; class SmallBox:Box // SmallBox 是派生类 { public: void setSmallWidth( double wid ); double getSmallWidth( void ); }; class NotBo { public: double getWidth(); } // 子类的成员函数 double SmallBox::getSmallWidth(void) { return width ; } void SmallBox::setSmallWidth( double wid ) { width = wid; } //非子类的成员函数 double NotBox::getWidth(void) { return Box::width; } // 程序的主函数 int main( ) { SmallBox box; NotBox notbox; // 使用成员函数设置宽度 box.setSmallWidth(5.0); // 调用非子类的成员函数 notbox.getWidth(); //Error,无法访问Box的protected数据 return 0; }有public, protected, private三种继承方式,它们相应地改变了基类成员的访问属性。
1.public 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:public, protected, private【将基类的修饰符原封不动地继承下来】
2.protected 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:protected, protected, private【将基类中的public变为protected,其他修饰符原封不动继承下来】
3.private 继承:基类 public 成员,protected 成员,private 成员的访问属性在派生类中分别变成:private, private, private【将基类中public、protected变为private,其他修饰符(也只有private了)原封不动既传承下来】
简单来说,若将三种继承方式按可访问的范围大小来排序,则有public > protected > private
那么子类继承基类时,可以理解为发生了“短板效应”,可访问范围大的修饰符会变为可访问范围小的修饰符。
比如,以protected方式继承基类,那么public会向protected变化,protected不变,private由于可访问范围比protected更小,所以private也不变。
但无论哪种继承方式,上面两点都没有改变:
1.private 成员只能被本类成员(类内)和友元访问,不能被派生类访问;
2.protected 成员可以被派生类(子类)访问。
注意:protected,表示有条件的限制以保护该成员,当将类别成员定义为protected之后,继承它的类就可以直接使用这些成员,但这些成员仍然受到对象范围的保护,不可被派生类(子类)对象直接使用。
注意:因为私有继承才在派生类中变为private属性的数据在派生类内依然可以被访问,但在基类中本来就是private属性的数据在派生类内不可被访问。(基类的私有属性有被派生类继承下来,但在派生类内不可访问)
如果继承时不显示声明是 private,protected,public 继承,则默认是 private 继承,在 struct 中默认 public 继承。
继承方式基类的public成员基类的protected成员基类的private成员继承引起的访问控制关系变化概括public继承仍为public成员仍为protected成员不可见基类的非私有成员在子类的访问属性不变protected继承变为protected成员变为protected成员不可见基类的非私有成员都为子类的保护成员private继承变为private成员变为private成员不可见基类的非私有成员都成为子类的私有成员类的构造函数是类的一种特殊的成员函数,它会在每次创建类的新对象时执行。
构造函数的名称与类的名称是完全相同的,并且不会返回任何类型,也不会返回 void。构造函数可用于为某些成员变量设置初始值。
#include <iostream> using namespace std; class Line { public: void setLength( double len ); double getLength( void ); Line(); // 这是构造函数 private: double length; }; // 成员函数定义,包括构造函数 Line::Line(void) { cout << "Object is being created" << endl; } void Line::setLength( double len ) { length = len; } double Line::getLength( void ) { return length; } // 程序的主函数 int main( ) { Line line; //构造函数在这一步执行 // 设置长度 line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl; return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Object is being created //Length of line : 6从上面的例子可以看出,构造函数在创建对象时执行。
默认的构造函数没有任何参数,但如果需要,构造函数也可以带有参数。这样在创建对象时就会给对象赋初始值。
#include <iostream> using namespace std; class Line { public: void setLength( double len ); double getLength( void ); Line(double len); // 这是构造函数 private: double length; }; // 成员函数定义,包括构造函数 Line::Line( double len) { cout << "Object is being created, length = " << len << endl; length = len; } void Line::setLength( double len ) { length = len; } double Line::getLength( void ) { return length; } // 程序的主函数 int main( ) { Line line(10.0); // 获取默认设置的长度 cout << "Length of line : " << line.getLength() <<endl; // 再次设置长度 line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl; return 0; }P.S:构造函数可以在类内定义,也可以类内声明类外定义(必须指明作用域),上面的例子就是在类内声明,类外定义。而下面的例子是在类内声明并定义。
#include <iostream> using namespace std; class rectangle { int length; int width; public: int square() { return length*width; } rectangle(int length,int width) { this->length=length; this->width=width; } }; int main() { rectangle rec(10,20); cout<<rec.square()<<endl; return 0; }使用初始化列表来初始化字段:
Line::Line( double len): length(len) { cout << "Object is being created, length = " << len << endl; }上面的语法等同于如下语法:
Line::Line( double len) { length = len; cout << "Object is being created, length = " << len << endl; }假设有一个类 C,具有多个字段 X、Y、Z 等需要进行初始化,同理地,您可以使用上面的语法,只需要在不同的字段使用逗号进行分隔,如下所示:
C::C( double a, double b, double c): X(a), Y(b), Z(c) { .... }P.S:使用初始化列表允许形参与类的属性同名
#include <iostream> using namespace std; class rectangle { int length; int width; public: int square() { return length*width; } rectangle(int length,int width):length(length),width(width) { } }; int main() { rectangle rec(10,20); cout<<rec.square()<<endl; return 0; }P.S:C++ 初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序。
class CMyClass { CMyClass(int x, int y); int m_x; //声明的顺序是先m_x,再m_y int m_y; }; CMyClass::CMyClass(int x, int y) : m_y(y), m_x(m_y) { };编译器先初始化 m_x,然后是 m_y,,因为它们是按这样的顺序声明的。
但只要成员变量的初始化不依赖其他成员变量,即使顺序不同也能正确的初始化。
类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。
析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。
#include <iostream> using namespace std; class Line { public: Line(); // 这是构造函数声明 ~Line(); // 这是析构函数声明 private: double length; }; // 成员函数定义,包括构造函数 Line::Line(void) { cout << "Object is being created" << endl; } Line::~Line(void) { cout << "Object is being deleted" << endl; } // 程序的主函数 int main( ) { Line line; return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Object is being created //Object is being deleted从上面的例子可以看出,虽然我们没有手动调用类的析构函数,但是在程序的最后,系统会自动调用析构函数。
拷贝构造函数是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
通过使用另一个同类型的对象来初始化新创建的对象。
复制对象把它作为参数传递给函数。
复制对象,并从函数返回这个对象。
拷贝构造函数具有单个形参,该形参(常用const修饰)是对该类类型的引用。当定义一个新对象并用一个同类型的对象对它进行初始化时,将显式使用拷贝构造函数。当该类型的对象传递给函数或从函数返回该类型的对象时,将隐式调用拷贝构造函数。
拷贝构造函数的最常见形式如下:
classname (const classname &obj) { // 构造函数的主体 }在这里,obj 是一个对象引用,该对象是用于初始化另一个对象的。
#include <iostream> using namespace std; class Line { public: int getLength( void ); Line( int len ); // 简单的构造函数 Line( const Line &obj); // 拷贝构造函数 ~Line(); // 析构函数 private: int *ptr; }; // 成员函数定义,包括构造函数 Line::Line(int len) { cout << "调用构造函数" << endl; // 为指针分配内存 ptr = new int; *ptr = len; //类带有指针变量,且有动态内存分配,则必须有拷贝构造函数 } Line::Line(const Line &obj) { cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl; ptr = new int; *ptr = *obj.ptr; // 拷贝值 } Line::~Line(void) { cout << "释放内存" << endl; delete ptr; } int Line::getLength( void ) { return *ptr; } void display(Line obj) { cout << "line 大小 : " << obj.getLength() <<endl; } // 程序的主函数 int main( ) { Line line(10); display(line); //类的对象作为函数参数,需要调用拷贝构造函数 return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //调用构造函数 //调用拷贝构造函数并为指针 ptr 分配内存 //line 大小 : 10 //释放内存 //释放内存下面的实例对上面的实例稍作修改,通过使用已有的同类型的对象来初始化新创建的对象
#include <iostream> using namespace std; class Line { public: int getLength( void ); Line( int len ); // 简单的构造函数 Line( const Line &obj); // 拷贝构造函数 ~Line(); // 析构函数 private: int *ptr; }; // 成员函数定义,包括构造函数 Line::Line(int len) { cout << "调用构造函数" << endl; // 为指针分配内存 ptr = new int; *ptr = len; } Line::Line(const Line &obj) { cout << "调用拷贝构造函数并为指针 ptr 分配内存" << endl; ptr = new int; *ptr = *obj.ptr; // 拷贝值 } Line::~Line(void) { cout << "释放内存" << endl; delete ptr; } int Line::getLength( void ) { return *ptr; } void display(Line obj) { cout << "line 大小 : " << obj.getLength() <<endl; } // 程序的主函数 int main( ) { Line line1(10); Line line2 = line1; // 这里也调用了拷贝构造函数 display(line1); display(line2); return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //调用构造函数 //调用拷贝构造函数并为指针 ptr 分配内存 ————line2=line1 //调用拷贝构造函数并为指针 ptr 分配内存 ————display(line1) //line 大小 : 10 //释放内存 //调用拷贝构造函数并为指针 ptr 分配内存 ————display(line2) //line 大小 : 10 //释放内存 //释放内存 //释放内存P.S:对象作为函数参数时调用了拷贝构造函数,在函数执行结束时自动调用析构函数,回收内存。
C++支持两种初始化形式
拷贝初始化 int a = 5; 和直接初始化 int a(5); 对于其他类型没有什么区别,对于类类型直接初始化直接调用实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数,也就是说:
A x(2); //直接初始化,调用构造函数 A y = x; //拷贝初始化,调用拷贝构造函数如果在类中没有定义拷贝构造函数,编译器会自行定义一个。但对于凡是包含动态分配成员或包含指针成员的类,都应该提供拷贝构造函数。
必须定义拷贝构造函数的情况
只包含类类型成员或内置类型(但不是指针类型)成员的类,无须显式地定义拷贝构造函数也可以拷贝
有的类有一个数据成员是指针,或者是有成员表示在构造函数中分配的其他资源,这两种情况下都必须定义拷贝构造函数。
关于为什么当类成员中含有指针类型成员且需要对其分配内存时,一定要有总定义拷贝构造函数?
默认的拷贝构造函数实现的只能是浅拷贝,即直接将原对象的数据成员值依次复制给新对象中对应的数据成员,并没有为新对象另外分配内存资源。
这样,如果对象的数据成员是指针,两个指针对象实际上指向的是同一块内存空间。
在某些情况下,浅拷贝回带来数据安全方面的隐患。
当类的数据成员中有指针类型时,我们就必须定义一个特定的拷贝构造函数,该拷贝构造函数不仅可以实现原对象和新对象之间数据成员的拷贝,而且可以为新的对象分配单独的内存资源,这就是深拷贝构造函数。
如何防止默认拷贝发生?
声明一个私有的拷贝构造函数,这样因为拷贝构造函数是私有的,如果用户试图按值传递或函数返回该类的对象,编译器会报告错误,从而可以避免按值传递或返回对象。
总结:
当出现类的等号赋值时,会调用拷贝函数,在未定义显式拷贝构造函数的情况下,系统会调用默认的拷贝函数——即浅拷贝,它能够完成成员的一一复制。当数据成员中没有指针时,浅拷贝是可行的。但当数据成员中有指针时,如果采用简单的浅拷贝,则两类中的两个指针将指向同一个地址,当对象快结束时,会调用两次析构函数,而导致指针悬挂现象。所以,这时,必须采用深拷贝。
深拷贝与浅拷贝的区别就在于深拷贝会在堆内存中另外申请空间来储存数据,从而也就解决了指针悬挂的问题。简而言之,当数据成员中有指针时,必须要用深拷贝。
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。
如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend。
class Box { double width; public: double length; friend void printWidth( Box box ); //可以访问Box的私有数据和方法 void setWidth( double wid ); };声明类 ClassTwo 的所有成员函数作为类 ClassOne 的友元,需要在类 ClassOne 的定义中放置如下声明:
friend class ClassTwo; #include <iostream> using namespace std; class Box { double width; public: friend void printWidth( Box box ); void setWidth( double wid ); }; // 成员函数定义 void Box::setWidth( double wid ) { width = wid; } // 请注意:printWidth() 不是任何类的成员函数 void printWidth( Box box ) { /* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */ cout << "Width of box : " << box.width <<endl; } // 程序的主函数 int main( ) { Box box; // 使用成员函数设置宽度 box.setWidth(10.0); // 使用友元函数输出宽度 printWidth( box ); return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Width of box : 10友元函数没有this指针,则参数要有三种情况:
要访问非static成员时,需要对象做参数;
要访问static成员或全局变量时,则不需要对象做参数;
如果做参数的对象是全局对象,则不需要对象做参数;
可以直接调用友元函数,不需要通过对象或指针。
下面是友元类的应用:
#include <iostream> using namespace std; class Box { double width; public: friend void printWidth(Box box); //声明友元函数 friend class BigBox; //声明友元类 void setWidth(double wid); }; //定义友元类 class BigBox { public : void Print(int width, Box &box) { // BigBox是Box的友元类,它可以直接访问Box类的任何成员 box.setWidth(width); cout << "Width of box : " << box.width << endl; } }; // 成员函数定义 void Box::setWidth(double wid) { width = wid; } // 友元函数定义,它不属于任何类 void printWidth(Box box) { //因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 cout << "Width of box : " << box.width << endl; } // 程序的主函数 int main() { Box box; BigBox big; // 使用成员函数设置宽度 box.setWidth(10.0); // 使用友元函数输出宽度 printWidth(box); // 使用友元类中的方法设置宽度 big.Print(20, box); return 0; }C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。
在类定义中的定义的函数都是内联函数,即使没有使用 inline 说明符。
#include <iostream> using namespace std; inline int Max(int x, int y) { return (x > y)? x : y; } // 程序的主函数 int main( ) { cout << "Max (20,10): " << Max(20,10) << endl; cout << "Max (0,200): " << Max(0,200) << endl; cout << "Max (100,1010): " << Max(100,1010) << endl; return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Max (20,10): 20 //Max (0,200): 200 //Max (100,1010): 1010引入内联函数的目的是为了解决程序中函数调用的效率问题。这么说吧,程序在编译器编译的时候,编译器将程序中出现的内联函数的调用表达式用内联函数的函数体进行替换,而对于其他的函数,都是在运行时候才被替代。这其实就是个空间代价换时间的节省。所以内联函数一般都是1-5行的小函数。在使用内联函数时要留神:
1.在内联函数内不允许使用循环语句和开关语句;2.内联函数的定义必须出现在内联函数第一次调用之前;3.类结构中所在的类说明内部定义的函数是内联函数。在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。
友元函数没有 this 指针,因为友元不是类的成员。只有成员函数才有 this 指针。
因为 this 的目的总是指向“这个”对象,所以 this 是一个常量指针,我们不允许改变 this 中保存的地址。
在成员函数内部,我们可以直接使用或调用该函数的对象的成员,而无须通过成员访问运算符来做到这一点,因为 this 所指的正是这个对象。任何对类成员的直接访问都被看作是对 this 的隐式引用。即在内部访问成员xx,等价于访问this->xx。
this 指针的类型与它所属的类一致。例如,对于类Book,它的对象指针类型就是Book*。
一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 ->,就像访问指向结构的指针一样。与所有的指针一样,必须在使用指针之前,对指针进行初始化。
#include <iostream> using namespace std; class Box { public: // 构造函数定义 Box(double l=2.0, double b=2.0, double h=2.0) { cout <<"Constructor called." << endl; length = l; breadth = b; height = h; } double Volume() { return length * breadth * height; } private: double length; double breadth; double height; }; int main(void) { Box Box1(3.3, 1.2, 1.5); Box Box2(8.5, 6.0, 2.0); Box *ptrBox; // 声明一个指向类的指针 // 保存第一个对象的地址 ptrBox = &Box1; // 现在尝试使用成员访问运算符来访问成员 cout << "Volume of Box1: " << ptrBox->Volume() << endl; // 保存第二个对象的地址 ptrBox = &Box2; // 现在尝试使用成员访问运算符来访问成员 cout << "Volume of Box2: " << ptrBox->Volume() << endl; return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Constructor called. //Constructor called. //Volume of Box1: 5.94 //Volume of Box2: 102我们可以使用 static 关键字来把类成员定义为静态的。当我们声明类的成员为静态时,这意味着无论创建多少个类的对象,静态成员都只有一个副本。
静态成员在类的所有对象中是共享的。如果不存在其他的初始化语句,在创建第一个对象时,所有的静态数据都会被初始化为零。我们不能把静态成员的初始化放置在类的定义中,但是可以在类的外部通过使用范围解析运算符 :: 来重新声明静态变量从而对它进行初始化,如下面的实例所示。
#include <iostream> using namespace std; class Box { public: static int objectCount; //静态成员的声明,在类内 // 构造函数定义 Box(double l=2.0, double b=2.0, double h=2.0) { cout <<"Constructor called." << endl; length = l; breadth = b; height = h; // 每次创建对象时增加 1 objectCount++; } double Volume() { return length * breadth * height; } private: double length; // 长度 double breadth; // 宽度 double height; // 高度 }; // 初始化类 Box 的静态成员,必须在类外 int Box::objectCount = 0; int main(void) { Box Box1(3.3, 1.2, 1.5); // 声明 box1 Box Box2(8.5, 6.0, 2.0); // 声明 box2 // 输出对象的总数 cout << "Total objects: " << Box::objectCount << endl; return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Constructor called. //Constructor called. //Total objects: 2P.S:静态成员变量在类中仅仅是声明,没有定义,所以要在类的外面定义,实际上是给静态成员变量分配内存。如果不加定义就会报错,初始化是赋一个初始值,而定义是分配内存。
如果把函数成员声明为静态的,就可以把函数与类的任何特定对象独立开来。静态成员函数即使在类对象不存在的情况下也能被调用,静态函数只要使用类名加范围解析运算符 :: 就可以访问。
静态成员函数只能访问静态成员数据、其他静态成员函数和类外部的其他函数。
静态成员函数有一个类范围,他们不能访问类的 this 指针。您可以使用静态成员函数来判断类的某些对象是否已被创建,也可以使用静态成员变量清楚了解构造与析构函数的调用情况。
静态成员函数与普通成员函数的区别:
静态成员函数没有 this 指针(因为它不专属于哪一个对象),只能访问静态成员(包括静态成员变量和静态成员函数)。普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。 #include <iostream> using namespace std; class Box { public: static int objectCount; //静态成员的声明 // 构造函数定义 Box(double l=2.0, double b=2.0, double h=2.0) { cout <<"Constructor called." << endl; length = l; breadth = b; height = h; // 每次创建对象时增加 1 objectCount++; } double Volume() { return length * breadth * height; } static int getCount() //静态成员函数的定义 { return objectCount; } private: double length; // 长度 double breadth; // 宽度 double height; // 高度 }; // 初始化类 Box 的静态成员 int Box::objectCount = 0; int main(void) { // 在创建对象之前输出对象的总数 cout << "Inital Stage Count: " << Box::getCount() << endl; Box Box1(3.3, 1.2, 1.5); // 声明 box1 Box Box2(8.5, 6.0, 2.0); // 声明 box2 // 在创建对象之后输出对象的总数 cout << "Final Stage Count: " << Box::getCount() << endl; return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Inital Stage Count: 0 //Constructor called. //Constructor called. //Final Stage Count: 2P.S:与静态成员变量一样,调用类的静态成员函数也要用范围解析运算符 :: 。
类中特殊成员变量的初始化问题:
常量变量:必须通过构造函数参数列表进行初始化。引用变量:必须通过构造函数参数列表进行初始化。普通静态变量:要在类外通过"::"初始化。静态整型常量:可以直接在定义的时候初始化。静态非整型常量:不能直接在定义的时候初始化。要在类外通过"::"初始化。面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。
当创建一个类时,您不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,新建的类称为派生类。
一个类可以派生自多个类,这意味着,它可以从多个基类继承数据和函数。定义一个派生类,我们使用一个类派生列表来指定基类。类派生列表以一个或多个基类命名。
class derived-class: access-specifier base-class class dog:public animalP.S:如果未使用访问修饰符 access-specifier,则默认为 private 继承。
假设有一个基类 Shape,Rectangle 是它的派生类
#include <iostream> using namespace std; // 基类 class Shape { public: void setWidth(int w) { width = w; } void setHeight(int h) { height = h; } protected: int width; int height; }; // 派生类 class Rectangle: public Shape { public: int getArea() { return (width * height); //width和height被Rectangle类继承,作为它的保护成员 } }; int main(void) { Rectangle Rect; Rect.setWidth(5); Rect.setHeight(7); // 输出对象的面积 cout << "Total area: " << Rect.getArea() << endl; return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Total area: 35派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。
我们可以根据访问权限总结出不同的访问类型,如下所示:
访问publicprotectedprivate同一个类yesyesyes派生类yesyesno外部的类yesnono一个派生类继承了所有的基类方法,但下列情况除外:
基类的构造函数、析构函数和拷贝构造函数。基类的重载运算符。基类的友元函数。当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。继承类型是通过上面讲解的访问修饰符 access-specifier 来指定的。
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问(基类私有成员对于派生类不可见),但是可以通过调用基类的公有和保护成员来访问。保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。P.S:私有继承将基类的成员变为派生类自己的私有成员 跟 用某种方式继承了基类中的私有成员 是不一样的。
多继承即一个子类可以有多个父类,它继承了多个父类的特性。
C++ 类可以从多个类继承成员。
class <派生类名>:<继承方式1><基类名1>,<继承方式2><基类名2>,… { <派生类类体> }; class Me:public Women,public Student { ... };其中,访问修饰符继承方式是 public、protected 或 private 其中的一个,用来修饰每个基类,各个基类之间用逗号分隔。
另外一种多继承(环状继承,也叫菱形继承),A->Base, C->Base, D->(A,C),例如:
class Base{......}; class A: public Base{......}; class C: public Base{......}; class D: public A, public C{.....};这个继承会使D创建两个对象,要解决上面问题就要用虚拟继承格式
格式:class 类名: virtual 继承方式 父类名 class A:virtual public Base { ... }; class C:virtual public Base { ... }; #include <iostream> using namespace std; //基类 class Base { public: Base(){cout<<"Base()"<<endl;} ~Base(){cout<<"~Base()"<<endl;} protected: int base; }; class A:virtual public Base { public: A(){cout<<"A()"<<endl;} ~A(){cout<<"~A()"<<endl;} protected: int a; }; class C:virtual public Base { public: C(){cout<<"C()"<<endl;} ~C(){cout<<"~C()"<<endl;} protected: int c; }; class D:public C, public A { public: D(){cout<<"D()"<<endl;} ~D(){cout<<"~D()"<<endl;} protected: int d; }; int main() { cout << "Hello World!" << endl; D d; //Base, A, C ,D Base base; A a; C c; cout<<sizeof(d)<<endl; //40 cout<<sizeof(base)<<endl; //4 cout<<sizeof(a)<<endl; //16 cout<<sizeof(c)<<endl; //16 return 0; }C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。
重载声明是指一个与之前已经在该作用域内声明过的函数或方法具有相同名称的声明,但是它们的参数列表和定义(实现)不相同。
当您调用一个重载函数或重载运算符时,编译器通过把您所使用的参数类型与定义中的参数类型进行比较,决定选用最合适的定义。选择最合适的重载函数或重载运算符的过程,称为重载决策。
在同一个作用域内,可以声明几个功能类似的同名函数,但是这些同名函数的形式参数(指参数的个数、类型或者顺序)必须不同。您不能仅通过返回类型的不同来重载函数。
#include <iostream> using namespace std; class printData { public: void print(int i) { cout << "整数为: " << i << endl; } void print(double f) { cout << "浮点数为: " << f << endl; } void print(char c[]) { cout << "字符串为: " << c << endl; } }; int main(void) { printData pd; // 输出整数 pd.print(5); // 输出浮点数 pd.print(500.263); // 输出字符串 char c[] = "Hello C++"; pd.print(c); return 0; }您可以重定义或重载大部分 C++ 内置的运算符。这样,您就能使用自定义类型的运算符。
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表。
Box operator+(const Box&);声明加法运算符用于把两个 Box 对象相加,返回最终的 Box 对象。大多数的重载运算符可被定义为普通的非成员函数或者被定义为类成员函数。如果我们定义上面的函数为类的非成员函数,那么我们需要为每次操作传递两个参数。
Box operator+(const Box&, const Box&);下面的实例使用成员函数演示了运算符重载的概念。在这里,对象作为参数进行传递,对象的属性使用 this 运算符进行访问。
#include <iostream> using namespace std; class Box { public: double getVolume(void) { return length * breadth * height; } void setLength( double len ) { length = len; } void setBreadth( double bre ) { breadth = bre; } void setHeight( double hei ) { height = hei; } // 重载 + 运算符,用于把两个 Box 对象相加 Box operator+(const Box& b) { Box box; box.length = this->length + b.length; box.breadth = this->breadth + b.breadth; box.height = this->height + b.height; return box; } private: double length; // 长度 double breadth; // 宽度 double height; // 高度 }; // 程序的主函数 int main( ) { Box Box1; // 声明 Box1,类型为 Box Box Box2; // 声明 Box2,类型为 Box Box Box3; // 声明 Box3,类型为 Box double volume = 0.0; // 把体积存储在该变量中 // Box1 详述 Box1.setLength(6.0); Box1.setBreadth(7.0); Box1.setHeight(5.0); // Box2 详述 Box2.setLength(12.0); Box2.setBreadth(13.0); Box2.setHeight(10.0); // Box1 的体积 volume = Box1.getVolume(); cout << "Volume of Box1 : " << volume <<endl; // Box2 的体积 volume = Box2.getVolume(); cout << "Volume of Box2 : " << volume <<endl; // 把两个对象相加,得到 Box3 Box3 = Box1 + Box2; //这里自动调用了拷贝构造函数 // Box3 的体积 volume = Box3.getVolume(); cout << "Volume of Box3 : " << volume <<endl; return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Volume of Box1 : 210 //Volume of Box2 : 1560 //Volume of Box3 : 5400下面是可重载的运算符列表:
双目算术运算符+ (加),-(减),*(乘),/(除),% (取模)关系运算符==(等于),!= (不等于),< (小于),> (大于>,<=(小于等于),>=(大于等于)逻辑运算符||(逻辑或),&&(逻辑与),!(逻辑非)单目运算符+ (正),-(负),*(指针),&(取地址)自增自减运算符++(自增),--(自减)位运算符| (按位或),& (按位与),~(按位取反),^(按位异或),,<< (左移),>>(右移)赋值运算符=, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>=空间申请与释放new, delete, new[ ] , delete[]其他运算符()(函数调用),->(成员访问),,(逗号),[](下标)下面是不可重载的运算符列表:
.:成员访问运算符.*, ->*:成员指针访问运算符:::域运算符sizeof:长度运算符?::条件运算符#: 预处理符号注意:
运算重载符不可以改变语法结构。运算重载符不可以改变操作数的个数。运算重载符不可以改变优先级。运算重载符不可以改变结合性。类重载、覆盖、重定义之间的区别:
重载指的是函数具有的不同的参数列表,而函数名相同的函数。重载要求参数列表必须不同,比如参数的类型不同、参数的个数不同、参数的顺序不同。如果仅仅是函数的返回值不同是没办法重载的,因为重载要求参数列表必须不同。(发生在同一个类里) 覆盖是存在类中,子类重写从基类继承过来的函数。被重写的函数不能是static的。必须是virtual的。但是函数名、返回值、参数列表都必须和基类相同(发生在基类和子类) 重定义也叫做隐藏,子类重新定义父类中有相同名称的非虚函数 ( 参数列表可以不同 ) 。(发生在基类和子类)一元运算符只对一个操作数进行操作,下面是一元运算符的实例:
递增运算符( ++ )和递减运算符( -- )一元减运算符,即负号( - )逻辑非运算符( ! )一元运算符通常出现在它们所操作的对象的左边,比如 !obj、-obj 和 ++obj,但有时它们也可以作为后缀,比如 obj++ 或 obj--。
下面的实例演示了如何重载一元减运算符( - )。
#include <iostream> using namespace std; class Distance { private: int feet; // 0 到无穷 int inches; // 0 到 12 public: // 所需的构造函数 Distance(){ feet = 0; inches = 0; } Distance(int f, int i){ feet = f; inches = i; } // 显示距离的方法 void displayDistance() { cout << "F: " << feet << " I:" << inches <<endl; } // 重载负运算符( - ) Distance operator- () { feet = -feet; inches = -inches; return Distance(feet, inches); } }; int main() { Distance D1(11, 10), D2(-5, 11); -D1; // 取相反数 D1.displayDistance(); // 距离 D1 -D2; // 取相反数 D2.displayDistance(); // 距离 D2 return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //F: -11 I:-10 //F: 5 I:-11二元运算符需要两个参数,下面是二元运算符的实例。我们平常使用的加运算符( + )、减运算符( - )、乘运算符( * )和除运算符( / )都属于二元运算符。就像加(+)运算符。
下面的实例演示了如何重载加运算符( + )。类似地,您也可以尝试重载减运算符( - )和除运算符( / )。
#include <iostream> using namespace std; class Box { double length; // 长度 double breadth; // 宽度 double height; // 高度 public: double getVolume(void) { return length * breadth * height; } void setLength( double len ) { length = len; } void setBreadth( double bre ) { breadth = bre; } void setHeight( double hei ) { height = hei; } // 重载 + 运算符,用于把两个 Box 对象相加 Box operator+(const Box& b) { Box box; box.length = this->length + b.length; box.breadth = this->breadth + b.breadth; box.height = this->height + b.height; return box; } }; // 程序的主函数 int main( ) { Box Box1; // 声明 Box1,类型为 Box Box Box2; // 声明 Box2,类型为 Box Box Box3; // 声明 Box3,类型为 Box double volume = 0.0; // 把体积存储在该变量中 // Box1 详述 Box1.setLength(6.0); Box1.setBreadth(7.0); Box1.setHeight(5.0); // Box2 详述 Box2.setLength(12.0); Box2.setBreadth(13.0); Box2.setHeight(10.0); // Box1 的体积 volume = Box1.getVolume(); cout << "Volume of Box1 : " << volume <<endl; // Box2 的体积 volume = Box2.getVolume(); cout << "Volume of Box2 : " << volume <<endl; // 把两个对象相加,得到 Box3 Box3 = Box1 + Box2; // Box3 的体积 volume = Box3.getVolume(); cout << "Volume of Box3 : " << volume <<endl; return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Volume of Box1 : 210 //Volume of Box2 : 1560 //Volume of Box3 : 5400P.S:符号重载函数不仅可以用类的成员函数来实现,也可以用友元函数来实现。
#include <iostream> using namespace std; class Box { double length; // 长度 double breadth; // 宽度 double height; // 高度 public: double getVolume(void) { return length * breadth * height; } void setLength( double len ) { length = len; } void setBreadth( double bre ) { breadth = bre; } void setHeight( double hei ) { height = hei; } /** * 重载 + 运算符,用于把两个 Box 对象相加 * 因为其是全局函数,对应的参数个数为2。 * 当重载的运算符函数是全局函数时,需要在类中将该函数声明为友元。 */ friend Box operator+(const Box& a, const Box& b); //友元函数声明 }; Box operator+(const Box& a, const Box& b) //在类外定义友元函数,它可以访问类的私有属性 { Box box; box.length = a.length + b.length; box.breadth = a.breadth + b.breadth; box.height = a.height + b.height; return box; } // 程序的主函数 int main( ) { Box Box1; // 声明 Box1,类型为 Box Box Box2; // 声明 Box2,类型为 Box Box Box3; // 声明 Box3,类型为 Box double volume = 0.0; // 把体积存储在该变量中 // Box1 详述 Box1.setLength(6.0); Box1.setBreadth(7.0); Box1.setHeight(5.0); // Box2 详述 Box2.setLength(12.0); Box2.setBreadth(13.0); Box2.setHeight(10.0); // Box1 的体积 volume = Box1.getVolume(); cout << "Volume of Box1 : " << volume <<endl; // Box2 的体积 volume = Box2.getVolume(); cout << "Volume of Box2 : " << volume <<endl; // 把两个对象相加,得到 Box3 Box3 = Box1 + Box2; // Box3 的体积 volume = Box3.getVolume(); cout << "Volume of Box3 : " << volume <<endl; return 0; }C++ 语言支持各种关系运算符( < 、 > 、 <= 、 >= 、 == 等等),它们可用于比较 C++ 内置的数据类型。
您可以重载任何一个关系运算符,重载后的关系运算符可用于比较类的对象。
下面的实例演示了如何重载 < 运算符,类似地,您也可以尝试重载其他的关系运算符。
#include <iostream> using namespace std; class Distance { private: int feet; // 0 到无穷 int inches; // 0 到 12 public: // 所需的构造函数 Distance(){ feet = 0; inches = 0; } Distance(int f, int i){ feet = f; inches = i; } // 显示距离的方法 void displayDistance() { cout << "F: " << feet << " I:" << inches <<endl; } // 重载负运算符( - ) Distance operator- () { feet = -feet; inches = -inches; return Distance(feet, inches); } // 重载小于运算符( < ) bool operator <(const Distance& d) { if(feet < d.feet) { return true; } if(feet == d.feet && inches < d.inches) { return true; } return false; } }; int main() { Distance D1(11, 10), D2(5, 11); if( D1 < D2 ) { cout << "D1 is less than D2 " << endl; } else { cout << "D2 is less than D1 " << endl; } return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //D2 is less than D1C++ 能够使用流提取运算符 >> 和流插入运算符 << 来输入和输出内置的数据类型。您可以重载流提取运算符和流插入运算符来操作对象等用户自定义的数据类型。
在这里,有一点很重要,我们需要把运算符重载函数声明为类的友元函数,这样我们就能不用创建对象而直接调用函数。
下面的实例演示了如何重载提取运算符 >> 和插入运算符 <<。
#include <iostream> using namespace std; class Distance { private: int feet; // 0 到无穷 int inches; // 0 到 12 public: // 所需的构造函数 Distance(){ feet = 0; inches = 0; } Distance(int f, int i){ feet = f; inches = i; } friend ostream &operator<<( ostream &output, const Distance &D ) { output << "F : " << D.feet << " I : " << D.inches; return output; } friend istream &operator>>( istream &input, Distance &D ) { input >> D.feet >> D.inches; return input; } }; int main() { Distance D1(11, 10), D2(5, 11), D3; cout << "Enter the value of object : " << endl; cin >> D3; cout << "First Distance : " << D1 << endl; cout << "Second Distance :" << D2 << endl; cout << "Third Distance :" << D3 << endl; return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //$./a.out //Enter the value of object : //70 //10 //First Distance : F : 11 I : 10 //Second Distance :F : 5 I : 11 //Third Distance :F : 70 I : 10P.S:习惯上人们是使用 cin>> 和 cout<< 的,得使用友元函数来重载运算符,如果使用成员函数来重载会出现 d1<<cout; 这种不自然的代码。
递增运算符( ++ )和递减运算符( -- )是 C++ 语言中两个重要的一元运算符。
下面的实例演示了如何重载递增运算符( ++ ),包括前缀和后缀两种用法。类似地,您也可以尝试重载递减运算符( -- )。
#include <iostream> using namespace std; class Time { private: int hours; // 0 到 23 int minutes; // 0 到 59 public: // 所需的构造函数 Time(){ hours = 0; minutes = 0; } Time(int h, int m){ hours = h; minutes = m; } // 显示时间的方法 void displayTime() { cout << "H: " << hours << " M:" << minutes <<endl; } // 重载前缀递增运算符( ++ ) Time operator++ () { ++minutes; // 对象加 1 if(minutes >= 60) { ++hours; minutes -= 60; } return Time(hours, minutes); } // 重载后缀递增运算符( ++ ) Time operator++( int ) //注意这里带参数!! { // 保存原始值 Time T(hours, minutes); // 对象加 1 ++minutes; if(minutes >= 60) { ++hours; minutes -= 60; } // 返回旧的原始值 return T; } }; int main() { Time T1(11, 59), T2(10,40); ++T1; // T1 加 1 T1.displayTime(); // 显示 T1 ++T1; // T1 再加 1 T1.displayTime(); // 显示 T1 T2++; // T2 加 1 T2.displayTime(); // 显示 T2 T2++; // T2 再加 1 T2.displayTime(); // 显示 T2 return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //H: 12 M:0 //H: 12 M:1 //H: 10 M:41 //H: 10 M:42注意,int 在 括号内是为了向编译器说明这是一个后缀形式,而不是表示整数。
前缀形式重载调用 Check operator ++ () ,后缀形式重载调用 Check operator ++ (int)。
#include <iostream> using namespace std; class Check { private: int i; public: Check(): i(0) { } Check operator ++ () { Check temp; temp.i = ++i; return temp; } // 括号中插入 int 表示后缀 Check operator ++ (int) { Check temp; temp.i = i++; return temp; } void Display() { cout << "i = "<< i <<endl; } }; int main() { Check obj, obj1; obj.Display(); obj1.Display(); // 调用运算符函数,然后将 obj 的值赋给 obj1 obj1 = ++obj; obj.Display(); obj1.Display(); // 将 obj 赋值给 obj1, 然后再调用运算符函数 obj1 = obj++; obj.Display(); obj1.Display(); return 0; } //执行输出结果为: //i = 0 //i = 0 //i = 1 //i = 1 //i = 2 //i = 11、递增和递减一般是改变对象的状态,所以一般是重载为成员函数。
2、重载递增递减,一定要和指针的递增递减区分开。因为这里的重载操作的是对象,而不是指针(由于指针是内置类型,指针的递增递减是无法重载的),所以一般情况的递增递减是操作对象内部的成员变量。
3、递增和递减分为前置和后置情况,a = ++b;(前置), a = b++;(后置)。因为符号一样,所以给后置版本加一个int形参作为区分,这个形参是0,但是在函数体中是用不到的,只是为了区分前置后置。
就像其他运算符一样,您可以重载赋值运算符( = ),用于创建一个对象,比如拷贝构造函数。
下面的实例演示了如何重载赋值运算符。
#include <iostream> using namespace std; class Distance { private: int feet; // 0 到无穷 int inches; // 0 到 12 public: // 所需的构造函数 Distance(){ feet = 0; inches = 0; } Distance(int f, int i){ feet = f; inches = i; } void operator=(const Distance &D ) { feet = D.feet; inches = D.inches; } // 显示距离的方法 void displayDistance() { cout << "F: " << feet << " I:" << inches << endl; } }; int main() { Distance D1(11, 10), D2(5, 11); cout << "First Distance : "; D1.displayDistance(); cout << "Second Distance :"; D2.displayDistance(); // 使用赋值运算符 D1 = D2; cout << "First Distance :"; D1.displayDistance(); return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //First Distance : F: 11 I:10 //Second Distance :F: 5 I:11 //First Distance :F: 5 I:11P.S:当用用户自定义类型变量向内置类型变量赋值时,可以使用自定义类型的隐式转换。
#include<iostream> using namespace std; class Int { private: int n; public: Int(int i) :n(i) {}; operator int() // 这里就是隐式转换声明,应注意到它与运算符重载的不同之处 { return n; } }; int main() { Int a(5); int c = a; // 将Int包装类对象赋值给内置int类型,隐式调用转换函数 cout << c << endl; cout << a << endl; // 由于未重载Int的<<操作符,将隐式调用转换函数 getchar(); return 0; }注意:注意谨慎使用隐式转换函数,因为当你在不需要使用转换函数时,这些函数却缺可能会被调用运行;这些不正确的程序会做出一些意想不到的事情,而你又很难判断出原因。
函数调用运算符 () 可以被重载用于类的对象。当重载 () 时,您不是创造了一种新的调用函数的方式,相反地,这是创建一个可以传递任意数目参数的运算符函数。
下面的实例演示了如何重载函数调用运算符 () 。
#include <iostream> using namespace std; class Distance { private: int feet; // 0 到无穷 int inches; // 0 到 12 public: // 所需的构造函数 Distance(){ feet = 0; inches = 0; } Distance(int f, int i){ feet = f; inches = i; } // 重载函数调用运算符 Distance operator()(int a, int b, int c) { Distance D; // 进行随机计算 D.feet = a + c + 10; D.inches = b + c + 100 ; return D; } // 显示距离的方法 void displayDistance() { cout << "F: " << feet << " I:" << inches << endl; } }; int main() { Distance D1(11, 10), D2; cout << "First Distance : "; D1.displayDistance(); D2 = D1(10, 10, 10); // 调用操作符() cout << "Second Distance :"; D2.displayDistance(); return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //First Distance : F: 11 I:10 //Second Distance :F: 30 I:120多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
P.S:打个比方,猫和狗都继承自动物这个基类,动物都有“叫”这个方法,但要实现猫有猫专属的叫声,狗有狗专属的叫声,就需要用到多态。
下面的实例中,基类 Shape 被派生为两个类。
#include <iostream> using namespace std; class Shape { protected: int width, height; public: Shape( int a=0, int b=0) { width = a; height = b; } int area() { cout << "Parent class area :" <<endl; return 0; } }; class Rectangle: public Shape{ public: Rectangle( int a=0, int b=0):Shape(a, b) { } //调用基类构造函数 int area () { cout << "Rectangle class area :" <<endl; return (width * height); } }; class Triangle: public Shape{ public: Triangle( int a=0, int b=0):Shape(a, b) { } int area () { cout << "Triangle class area :" <<endl; return (width * height / 2); } }; // 程序的主函数 int main( ) { Shape *shape; Rectangle rec(10,7); Triangle tri(10,5); // 存储矩形的地址 shape = &rec; // 调用矩形的求面积函数 area shape->area(); // 存储三角形的地址 shape = &tri; // 调用三角形的求面积函数 area shape->area(); return 0; } //当上面的代码被编译和执行时,它会产生下列结果: //Parent class area //Parent class area导致错误输出的原因是,调用函数 area() 被编译器设置为基类中的版本,这就是所谓的静态多态,或静态链——函数调用在程序执行前就准备好了。有时候这也被称为早绑定,因为 area() 函数在程序编译期间就已经设置好了。为了解决这个问题,我们需要用到虚函数。
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
现在,让我们对程序稍作修改,在 Shape 类中,area() 的声明前放置关键字 virtual。
class Shape { protected: int width, height; public: Shape( int a=0, int b=0) { width = a; height = b; } virtual int area() { cout << "Parent class area :" <<endl; return 0; } }; //修改后,当编译和执行前面的实例代码时,它会产生以下结果: //Rectangle class area //Triangle class area此时,编译器看的是指针的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。
正如您所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。
P.S:多态的实现方式就是在子类中实现父类同名的方法,将子类对象的地址传给基类指针,并用基类指针调用子类和基类同名的方法,但是要记得用virtual修饰父类中的同名方法,否则会因为发生静态多态而不能得到预期的结果。
#include<iostream> using namespace std; class Animal { public: virtual void cry() //基类同名方法必须加上virtual关键字修饰 { cout << "don't know how to cry." << endl; } }; class Dog :public Animal { public: void cry() { cout << "woof woof!" << endl; } }; class Cat :public Animal { public: void cry() { cout << "meow meow!" << endl; } }; int main() { Dog dog; Cat cat; Animal *animal; //定义基类指针 animal = &dog; //基类指针指向子类对象dog animal->cry(); //调用Dog类中的同名方法cry() animal = &cat; //基类指针指向子类对象cat animal->cry(); //调用Cat类中的同名方法cry() getchar(); return 0; } //执行结果: //woof woof! //meow meow!您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
简单来说,虚函数可以不实现(定义)。不实现(定义)的虚函数是纯虚函数。
我们可以把基类中的虚函数 area() 改写如下:
class Shape { protected: int width, height; public: Shape( int a=0, int b=0) { width = a; height = b; } // pure virtual function virtual int area() = 0; };P.S:= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数。
形成多态必须具备三个条件:
1、必须存在继承关系;
2、继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字Virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);
3、存在基类类型的指针或者引用,通过该指针或引用调用虚函数;
纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
注意:
在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。一个类中如果存在未定义的虚函数,那么不能直接使用该类的实例,可以理解因为未定义 virtual 函数,其类是抽象的,无法实例化。将报错误。
#include<iostream> using namespace std; class Animal { public: virtual void cry() = 0; }; class Dog :public Animal { public: void cry() { cout << "woof woof!" << endl; } }; class Cat :public Animal { public: void cry() { cout << "meow meow!" << endl; } }; int main() { Dog dog; Cat cat; Animal *animal; Animal test; // 报错信息:不能实例化抽象类 animal = &dog; animal->cry(); animal = &cat; animal->cry(); getchar(); return 0; }接口描述了类的行为和功能,而不需要完成类的特定实现。
C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。
如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 "= 0" 来指定的。
class Box //Box就是抽象类 { public: // 纯虚函数 virtual double getVolume() = 0; private: double length; // 长度 double breadth; // 宽度 double height; // 高度 };设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。
因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,会导致编译错误。
可用于实例化对象的类被称为具体类。
#include<iostream> using namespace std; class Animal { public: virtual void cry() = 0; virtual void laugh() = 0; //laugh是一个纯虚函数,但是在子类中没有被重写 }; class Dog :public Animal { public: void cry() { cout << "woof woof!" << endl; } }; class Cat :public Animal { public: void cry() { cout << "meow meow!" << endl; } }; int main() { Dog dog; Cat cat; Animal *animal; animal = &dog; animal->cry(); animal = &cat; animal->cry(); getchar(); return 0; }上面的代码不能通过编译,错误提示为:
不允许使用抽象类类型 "Dog" 的对象 纯虚拟 函数 "Animal::laugh" 没有强制替代项
面向对象的系统可能会使用一个抽象基类为所有的外部应用程序提供一个适当的、通用的、标准化的接口。然后,派生类通过继承抽象基类,就把所有类似的操作都继承下来。
外部应用程序提供的功能(即公有函数)在抽象基类中是以纯虚函数的形式存在的。这些纯虚函数在相应的派生类中被实现。
这个架构也使得新的应用程序可以很容易地被添加到系统中,即使是在系统被定义之后依然可以如此。