2017-C++ 学习:对象和类详细总结
C++ 学习:对象和类详细总结
Ⅰ:类概念
一:类的构成
对象和对象之间的关系:分别使用不同的内存来存储数据,使用相同的内存上的函数(一份函数拷贝)
二:创建类和使用类的基本流程
Ⅰ.类声明和设计
1.基本概念 类的声明和设计是最基础最重要的部分,合理的类声明可以让后面的工作很方便,也是面向对象思想的体现。 首先通过上面的结构图已经知道类的基本结构,*首先就是数据,数据有公有和私有之分,这是权限。根据实际中的要求来决定数据的共有和私有属性。* 然后就是函数。*函数和数据其实是一样的。也有共有和私有之分。根据实际情况来确定是公有和私有。*
2.构造函数 函数中有一个特殊的函数,那就是构造函数。因为一个类中大部分数据是隐藏的,总不能老是通过调用公共函数的方法来进行初始化。C++提供的就是构造函数的方法。 构造函数的建立规则是这样的。构造函数没有返回值,构造函数的名字和类的名字完全一模一样(大小写也相同),所有看到一个类声明中没有返回值,那么说明这个函数是构造函数,构造函数必须放在 public 下面。构造函数可以有很多种选择,这时候就需要用到函数重载的方法来定义了。
一般的构造函数声明为:
默认构造函数:Student(); 带有参数的构造函数:Student(参数列表);
注意:
在完全没有自已定义构造函数的时候,系统会提供一个默认构造函数,类似于Student(){}
这个样子,仅仅创建一个对象但是不做任何初始化。也就是说,此时的一些数据成员都是没有被初始化的。
要是自己提供了构造函数,就必须自己同时为系统提供一个默认构造函数。
自己定义默认构造函数的方式有两种:定义一个没有参数的构造函数。给已有的构造函数的全部参数提供默认值。本质上,这两个构造函数使用的时候都不用加上任何参数。所以才能够做默认构造函数。
3.析构函数 构造函数起到了构造对象的作用,那么怎么销毁不需要用的对象呢。那就是析构函数。必须有一个析构函数若程序员没有提供析构函数,系统将自动提供一个默认的析构函数 析构函数定义方式:
1.原型:~Student() (固定,必须是这个样子!) 2.实现 Student::~Student() { 。。。 }
析构函数的调用时间(通常不在代码中显式的调用析构函数。)
**静态存储类对象:**程序结束后自动被调用 **动态存储类对象:**程序执行完定义这个对象的代码块之后自动调用 **通过 new 创建:**将驻留在栈内存或者自由存储区中,当使用 delete 来释放内存的时候,析构函数自动被调用
4.类声明式样
Ⅱ.构造函数和类中函数实现
类规划好之后就是其中的函数的实现,函数的实现还是根据功能来看的。所以,这里是很自由灵活的。其中有一个是限定符的概念。很简单的啦。 直接上模板
Ⅲ.构造并使用对象
这里要知道,每次创建新的对象之前必然要调用构造函数。所以要非常熟悉怎么利用构造函数创建处对象的几种方法。 1.自定义的构造函数创建对象(C++11 的几种初始化方式统一起来了)
2.自定义的默认构造函数创建对象
其中对象指针有些难度,在下面的动态内存管理中会专门介绍对象指针. 实践证明,类私有成员可以被类成员函数访问,不区分成员在哪个对象里。
Ⅳ.重新给对象和对象之间的相互赋值
三:关于 const 成员函数
有时候,我们创建了一个对象,但是事实上,我们只希望这个对象初始化之后不被改变,它可以是一个真理或者是什么,就是不能被改变。
然后我们就十分自然的想到const 创建一个对象。以下面的代码为例,但是后面发现就算是调用对象中不会改变值的showInfo()
函数的时候,也会报错。
解决办法就是告诉编译器,这个函数会承诺不会改变任何的值,怎么承诺呢?就是在相应的函数定义和声明的后面直接加上 const。就行了。
一定要强制养成在不改变类中的值得函数后加 const 的习惯!!使之成为 const 成员函数
四:this 指针
定义:this 指针指向用来调用成员函数的对象。 This 本质就是指向本对象的一个指针。 This 指向调用对象。
要引用整个对象,可以用
\*this
五:对象数组
当创建多个对象的时候,一个名字一个名字来写的话会很累。比如一个班上所有的人数。所以,这个时候用数组是一个很方便的选择。
创建格式: 1.创建且调用默认构造函数。
Student stu[100];1
没错,就是这么简单(当然在底层,这些都是直接调用的默认的构造函数。)
2.创建调用自己写的构造函数初始化。
Student stu[100] =
{
Student(参数列表。。), //自定义构造函数初始化
Student(),//默认构造函数初始化
......
....
}1234567
没错,这里可以使用不同的构造函数(如果你定义了很多的话)来分别构造每个对象的属性。后面其他的如果没有列出来视为直接调用默认构造函数。 对象数组初始化的方案是:系统首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象中的内容复制到相应的元素中去.
六:类作用域
1.在类中定义的名称(如数据成员名和类成员函数名)的作用域都为整个类,且在本类中是已知的,在其他的类中是未知的。(在不同的类中使用相同的名字而不会引起冲突) 2.在外部不能够直接访问类中的成员,公有成员函数也是如此,要调用共有成员函数,必须通过对象。 3.在类声明或者函数定义中,可以使用没有修饰的成员名称(未限定的名称)。
例子:一个学生类的例子 student.h
student.cpp
run.cpp
结果:
Ⅱ:运算符重载
一:运算符重载
一个 time 类的 add 函数,重载为+运算符。 原函数是这个样子
怎么把 add 函数重载为一个运算符呢? 那很简单!!!!!!其他的的任何东西都不动,把想要重载的方法名改变为 operator 相应的操作符。例如,想重载 +这个元算符。那么函数名就改为 operator +。 上面的函数变为 函数声明中 函数定义中
那怎么去用已经重载好的运算符呢? 它会自动识别他的操作数,所以只要是满足意义或者定义的操作数的组合都能够得到有意义的答案
一定要注意,前面的那个操作数是主调对象,后面的那个是参数。也就是说,有的时候他们之间的结合不一定是能够交换的!!!!
重点:重载限制
二:作为成员函数还是非成员函数
我们已经知道,大多数的运算符能够使用成员函数和非成员函数来进行重载。一般来说,
非成员函数为友元函数(因为要访问类中的私有数据)。
以前面的重载加法运算符来说,可以写成两种方式
Time operator +(const Time &t)const;
Friend Time operator +(const Time &t1,const Time &t2);12
加法运算符需要两个操作数(因为前面已经说过,重载不能也不会改变原有运算符的操作数的多少)。
对于第一个版本,也就是前面的成员函数的版本,一个操作数通过 this 指针隐式的传递,另外一个通过参数显示的传递。 对于第二个非成员函数的版本,两个操作数都是作为参数来传递。
总结:
非成员函数的重载运算符函数所需要的形参数目与运算符所使用的操作数是相同的。 成员函数的重载运算函数所需要的形参数目比要用的操所数目少一个,因为其中的一个参数是被隐式的传递给调用对象。
在定义元算符的时候,只能选择其中的一种,不然编译器会分不出出现报错!!!
重载«运算符输出对象的一个例子
Ⅲ.类和动态内存分配(指针)
一:动态内存和类
1.特殊成员函数概览:
默认构造函数,如果没有定义构造函数 默认析构函数,如果没有定义(前面已经讲到过) 复制构造函数,如果没有定义(重点注意) 赋值运算符,如果没有定义(重点注意) 地址运算符,如果没有定义
默认构造函数
这个就是前面讲过的默认构造函数,应该很熟悉了. 它有如下特点:
1.没有构造函数的时候,系统自动提供一个构造函数,且这个构造函数什么也不做.
2.要是定义了自己的构造函数,则必须同时定义一个默认构造函数.
3.自己定义的默认构造函数可以是不带任何参数的函数,也可以全部带有默认值的函数.系统把这两种类型的构造函数都当做是自定义的默认构造函数
4.不能够同时出现两个或以上的默认构造函数,因为系统会无法分辨
复制构造函数
概念
1.用于讲一个对象复制到新创建的对象中. 2.本质就是用于初始化过程(不是常规赋值)和按值传递参数 3.复制构造函数原型:
Class_name(const class_name &);
(相当于传入一个相同类的对象引用的构造函数。)
何时调用复制构造函数
1.新建一个对象并且将其初始化为同类现有对象时(初始化过程) 设 Student 为类.stu1 是已经存在的对象,stu2 是新创建的对象
Student stu2(stu1);
>Student stu2=stu1;
//这是在初始化的阶段,和赋值是不一样的Student stu2=Student(stu1);
>Student *p_stu2=new Student(stu1);
(这几种形式都将调用复制构造函数.) 上面几种形式说明了,对于这些形式是不调用构造函数就能够创建新的对象的.非常重要记住
2.函数按值传递对象或者函数返回对象时 由于按值传递对象将调用复制构造函数,因此可以按照引用传递对象。这样可以节省调用构造函数的时间以及存储新对象的空间。
默认的复制构造函数的功能
*默认的复制构造函数逐个复制非静态成员(成员复制也叫做浅复制),复制的是成员的值*。
**成员是常数或者是一些基本量,**是可以直接赋值过去的. 成员是结构体得话,因为在 C++中结构体可以直接交换,所以也是能够赋值过去的. **成员是类的话,**也是可以直接赋值过去的. 静态函数或常量不受影响,因为他们是属于整个类,而不是各个对象。
重点:
要是指针或者 C 风格的字符串.那么依然还是能够赋值,但是赋的是指针的值.所以,这里很容易有隐患,需要更加深层次的复制的时候就要关心这里. 如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。因为类中包含了使用 new 初始化的指针成员,那么定义一个复制构造函数是很必须的,用来复制指向的数据,而不是一个指针.(浅复制只会复制指针的值,而不会深挖指针指向的结构,所以必须自己定义一个深复制.)
自定义复制构造函数方式:
StringBad::StringBad(const StringBad & st)
{
Xxx
Xxx
}
123456
默认赋值运算符
ANSI C 允许结构赋值,而 C++允许类对象赋值,这是通过自动为类重载赋值运算符实现的。 这种运算的原型:
Class_name & Class_name::operator = (const Class_name &);1
上面那个例子:
StringBad & StringBad::operator =(const StringBad &);1
将已有的对象赋值给另外一个对象的时候(注意和初始化的区分),将使用重载的赋值运算符。 与复制构造函数相似,赋值运算符的隐式实现也是对成员进行逐个复制。
解决赋值复制问题要点
1.由于目标对象可能引用了以前分配的数据,所以函数应该使用 delete[]来释放这些数据 2.函数应该避免将对象赋给自身,否则,给对象重新赋值之前,释放内存可能删除对象的内容 3.函数返回一个指向调用对象的引用
参考模板:
StringBad & StringBad::operator =(const StringBad & st)
{
if(this==&st)
return *this;
delete [] str; //释放老的stirng
len=st.len;
str=new char [len+1];
std::strcpy(str,st.str);
return *this;
}12345678910
例子: mystring.h
#ifndef MYSTRING_H_
#define MYSTRING_H_
#include <iostream>
using namespace std;
class MyString
{
private:
char * str;
int len;
static int num_of_strings;
//cin limits,只有const整形才能够在这里直接赋值!!!
static const int CINLIM=80;
public:
//constructors and other methods
MyString ();
MyString(const char * s);
~MyString();//destructor
MyString (const MyString & ms);//copy constructor
int lenth() const
{
return len;
}
//overload operator methods
MyString & operator =(const MyString & ms);
MyString & operator =(const char * s);
char & operator [](int i); //can access and modified
const char & operator [](int i)const; //can access but can't modified
//overload operator friends
//输入输出
friend ostream & operator <<(ostream & os,const MyString & ms);
friend istream & operator >>(istream & is,MyString & ms);
//字符串比较
friend bool operator <(MyString & ms1,MyString & ms2);
friend bool operator >(MyString & ms1,MyString & ms2);
friend bool operator ==(MyString & ms1,MyString & ms2);
//静态方法
static int howMany();
};
#endif123456789101112131415161718192021222324252627282930313233343536373839404142434445464748
mystring.cpp
#include "mystring.h"
#include <iostream>
#include <cctype>
#include <cstring>
using namespace std;
//首先初始化静态的变量
int MyString::num_of_strings=0;
int MyString::howMany()
{
return num_of_strings;
}
//realize the constructor
MyString::MyString ()
{
len=0;
str=new char[len+1];
str[0]='\0';
++num_of_strings;
cout<<"using default constructor to init!: "<<str<<endl;
cout<<"we have "<<num_of_strings<<" objects now!"<<endl<<endl;
}
MyString::MyString(const char * s)
{
len=strlen(s);
str=new char[len+1];
strcpy(str,s);
++num_of_strings;
cout<<"using artificial constructor to init!: "<<str<<endl;
cout<<"we have "<<num_of_strings<<" objects now!"<<endl<<endl;
}
MyString::~MyString()
{
--num_of_strings;
cout<<"\""<<str<<"\""<<" deleted\n"<<endl;
cout<<"we have "<<num_of_strings<<" objects now!"<<endl<<endl;
delete [] str;
}
//realize the copy constructor
//本质就是进行深度复制,目的是保持每个对象的独立性
//不会是因为删除其中一个或是怎么的而影响到另外一个
MyString::MyString(const MyString & ms)
{
++num_of_strings;
len=ms.len;
//重新分配空间
str=new char[len+1];
strcpy(str,ms.str);
cout<<"using the copy constructor to init!: "<<str<<endl;
cout<<"we have "<<num_of_strings<<" objects now!"<<endl<<endl;
}
ostream & operator <<(ostream & os,const MyString & ms)
{
os<<"Infomation:\n";
os<<"content of the string: "<<ms.str<<endl;
os<<"lenth of the string: "<<ms.len<<endl<<endl;
return os;
}
istream & operator >>(istream & is,MyString & ms)
{
char buffer[MyString::CINLIM];
is.get(buffer,MyString::CINLIM);
if(is)
ms=buffer;
while(is&&is.get()!='\n')
continue;
return is;
}
//重载赋值运算符需要注意的!
//函数应该避免将对象赋给自己,所以前面应当进行比较。
//函数会有一个清除当前内存的动作,因为以前可能会有数据在其中
//这就是为什么要比较两个对象的原因,因为相同的对象,删除其中任何的元素都会破坏另外的元素
//按照自己的需要进行深度复制
MyString & MyString::operator=(const MyString & ms)
{
if(this==&ms)
{
return *this;
}
delete [] str;
len=ms.len;
str=new char[len+1];
strcpy(str,ms.str);
return *this;
}
MyString & MyString::operator=(const char * s)
{
delete [] str;
len=strlen(s);
str=new char[len+1];
strcpy(str,s);
return *this;
}
char & MyString::operator [](int i)
{
return str[i];
}
const char & MyString::operator [](int i) const
{
return str[i];
}
bool operator <(MyString & ms1,MyString & ms2)
{
return (strcmp(ms1.str,ms2.str)<0);
}
bool operator >(MyString & ms1,MyString & ms2)
{
return (strcmp(ms1.str,ms2.str)>0);
}
bool operator ==(MyString & ms1,MyString & ms2)
{
return (strcmp(ms1.str,ms2.str)==0);
}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
run.cpp
#include <iostream>
#include <cstring>
#include "mystring.h"
using namespace std;
int main()
{
MyString ms1;
MyString ms2("leonardo dicapeio");
MyString ms3=ms2;
cout<<ms2;
MyString ms4;
ms4=ms2;
cout<<ms4;
cout<<"the numbers of objects we have:\n"<<MyString::howMany()<<endl;
MyString ms5="fuck off";
cout<<ms5;
cout<<ms5[5]<<endl;
if(ms1>ms5)
cout<<"YES!!!"<<endl;
else
cout<<"NO!!!!"<<endl;
return 0;
}1234567891011121314151617181920212223
结果:
二:在构造函数中使用 new 的注意事项
1.首先,一旦有 new 的使用,意味着新内存空间的开辟和指针的使用,构造函数中用了 new,则析构函数中应该使用 delete 2.new 和 delete 应该相互兼容。new 对应于 delete,new []对应于 delete[]. 4.要是有多个构造函数,那么其中使用 new 的时候应该是用相同的类型.因为只有一个析构函数,只能在构造函数中使用同样的 new 的方式。 5.很据实际情况使用定义一个复制构造函数。这样可以进行深度复制,使得每个对象尽可能的保存独立。 6.根据实际情况重载赋值运算符=,进行深度复制,使得每个对象尽可能的保持独立。
四:有关返回对象的说明
1.返回指向 const 对象的引用
返回 const 引用的常见原因是为了提高效率,但是对于什么时候采用这一方法有一定的限制
如果函数返回(通过调用对象的方法或将对象作为参数传递给它的对象),可以通过返回引用来提高效率.
就用我们前面设计过的 Vector 类为例子 现在编写 Max 函数,返回两个 Vector 对象中模比较大的那个.
//version1
Vector Max(const Vector & v1,const Vector & v2)
{
......
}
//version2
const Vector & Max(const Vector & v1,const Vector & v2)
{
......
}1234567891011
返回对象将会调用复制构造函数,但是返回引用不会. 引用指向的对象应该在调用函数执行时存在. V1 和 v2 都是 const 类型,所以返回的类型必须也是 const 才行.
2.返回指向非 const 对象的引用
两种常见的返回非 const 对象引用的情形:
1.重载赋值运算符(提高效率) 2.重载与 cout 一起使用的«运算符(必须这么做)
3.返回对象
要是被返回的对象是被调用函数中的局部变量,则禁止用引用的方式来返回(因为在被调用函数执行完毕后,局部对象将调用其析构函数,对象不再存在.) 通常,被重载的算术运算符属于这一类,因为在函数中会定义一个临时对象,最后返回临时对象,而不是他的引用. 返回对象的话不可避免要调用复制构造函数.
总结:
方法或者函数要返回局部对象,那么必须返回对象.而不是指向对象的引用.
如果方法要返回一个没有公共复制构造函数的类的对象,它只能返回这个对象的引用.(因为返回对象必须调用复制构造函数,这里调不到,只能返回引用罗)
在同时能够返回对象和引用的情况下,首先考虑返回引用.
Ⅳ.类继承
一:概念
c++有 3 种继承方式:
1.公有继承(最常用的方式) 2.保护继承 3.私有继承
派生类对象也是一个基类对象,可以对基类进行的操作也可以对派生类进行。 派生类可以在已有类的基础上面添加自己的数据和行为,甚至是修改基类方法的行为,但是不能删除基类的属性。 在只提供类方法的头文件和编译后代码,依然可以使用库中的类派生出新的类.
一个派生类能够继承父类的所有成员,不管是私有的还是共有的.但是继承下来的私有成员只能够用前面从父类继承下来的公有方法来访问.
成员初始化列表(极度重要)
成员初始化列表的语法仅仅用于构造函数。 非静态 const 成员数据和引用数据必须用这种语法初始化。 数据成员被初始化的顺序和它们在类中创建的顺序相同,与初始化器中的排列顺序无关。
创建方式;
二:基本建立过程
步骤 1:首先根据实际情况创建一个基类
创建基类需要简洁和有意义,是一门很重要的思想。
步骤 2:在已有基类的基础之上建立派生类 以共有派生为例子
1.派生类可以自己添加新的方法和数据。(在派生类中新写上去的就是添加的.) 2.派生类必须要自己新加一个新的构造函数。
派生类的写法模板:
运动员例子 player.h
#ifndef TEST_H_
#define TEST_H_
#include <string>
using namespace std;
class Player
{
private:
string name;
bool hasTable;
public:
Player();
Player(const string & na,bool ht);
void show_old();
void reset_table(bool v)
{
hasTable=v;
}
};
class NewPlayer : public Player
{
private:
int score;
public:
NewPlayer();
NewPlayer(const string & na,bool ht,int sco);
NewPlayer(const Player & pl,int sco);
void reset_score(int sco);
void show_new();
};
#endif123456789101112131415161718192021222324252627282930
文件解释:
一旦建立一个派生类了,首先在脑海中就要知道,这个类中已经默认有了其基类中的全部东西(可访问性稍有不同)。 写在{}中的都是新加的内容了。 派生类中构造函数是一定要写的,而且要知道现在的新的类中一共有多少变量(因为通过继承得到一部分变量)
#include "test.h"
#include <iostream>
#include <string>
using namespace std;
//实现基类
Player::Player()
{
name="NULL";
hasTable=false;
}
Player::Player(const string & na,bool ht):name(na),hasTable(ht)
{
}
void Player::show_old()
{
cout<<"Info\n";
cout<<"name: "<<name<<endl;
cout<<"hastable: "<<hasTable<<endl;
}
//实现派生类
NewPlayer::NewPlayer():Player(),score(0)
{
}
NewPlayer::NewPlayer(const string & na,bool ht,int sco):Player(na,ht),score(sco)
{
}
NewPlayer::NewPlayer(const Player & pl,int sco):Player(pl),score(sco)
{
}
void NewPlayer::reset_score(int sco)
{
score=sco;
}
void NewPlayer::show_new()
{
show_old();
cout<<"score: "<<score<<endl<<endl;
} 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
文件解释:
1.在实现中,最重要的莫过于怎么处理新的构造函数的问题了。派生类构造函数必须使用基类构造函数。(因为基类的某些私有成员是外面访问不到,那么派生类继承的这些也必须通过基类的构造函数来方法基类的私有数据) 3.我们可以理解为,派生类同时也继承了基类的构造函数,在派生类中能够直接使用. 4.基类构造函数尽可能定义得更加有扩展性一点(因为派生类的构造函数要用到基类的构造函数)。 5.26 到 29 行是派生类的默认构造函数的实现。很简单,没有参数,实现的时候使用基类的默认构造函数加上一个新的变量实现就行了。 6.31 行到 34 行其实是一样的,只是新的变量调用了基类构造函数罢了。 7.35 到 38 行是直接把基类对象放进去。调用的是复制构造函数。根据实际情况来决定是不是要重写复制构造函数。
#include "test.h"
#include <iostream>
using namespace std;
int main()
{
Player p1;
p1.show_old();
NewPlayer p2;
//p2.show_old();
p2.show_new();
NewPlayer p3("leoanrdo",true,100);
p3.show_new();
NewPlayer p4=NewPlayer(p1,100);
p4.show_new();
return 0;
}12345678910111213141516
结果:
三.派生类和基类之间的关系
基本特性
People.h
People.cpp
main.cpp
结果:
总结上例:
1.我们可以这里理解,派生类继承了基类的所有属性,不管是私有还是公有的。但是,原来基类中公有的属性派生类可以直接拿来用,**但是!!私有的属性就必须通过继承的公有函数来访问。*****要是没有这样的公有接口怎么办?那么派生类将访问不到那些私有属性!!!!* 2.因为基类的私有属性我们访问不到,但是!!我们继承了共有的基类构造函数啊,所以,派生类的构造函数必须要调用到基类的构造函数.**因为原来的构造函数是公有的,所以,直接写名字来调用就行.**就相当于派生类自己的一个公有属性.(本来就是自己的公有属性) 3.所以,上面有一句话知道为什么会报错了吧…. 直接调用 m_age 的时候,你不能够访问到啊 4.前面我们就已经知道在派生类中可以添加新的函数拓展自己的功能了.这个根据需要来做,简单. 5.派生类中函数重写或者叫重载,这里很重要.我们知道派生类中函数的重写会”覆盖”基类的同名函数.这里要理解好”覆盖”的意思.这里的覆盖是指,派生类对象直接调用该函数的时候,我只认重新写过的,以前的老的我就不认了.但是并不代表老的没有了.基类的函数和派生类继承的该同名函数都存在 重写之前:
两个函数:基类名:函数名 派生类名:函数名 调用一个函数等价于调用 基类名:函数名
重写后
两个函数:基类名:函数名 派生类名:函数名 调用一个函数等价于调用 派生类:函数名
那么重写后还想调用原来的基类函数怎么办呢? 显式使用 基类名::函数名 比如上图的
People::showInfo();
来显示基类中私有变量的值
高级特性
Main.cpp 结果:
总结:
1.可以通过直接传入基类对象的方式来初始化,自己写好构造函数就行了.但是要注意的是,这时候初始化基类继承下来的属性用的是基类的复制构造函数.对于没有动态内存开辟的,用默认的就好.但是有动态内存开辟涉及到指针的.必须自己根据实际情况定义一个复制构造函数.尽可能减少可能出现的情况. 2.**基类指针可以在不进行显式类型转化的情况下指向派生类对象.**但是,只能够访问到基类中已经有的方法,而且是以基类方法的形式调用.派生类中重新的新方法并不会被调用,而派生类中新增加的方法更是访问不到的. 3.基类引用可以在不进行显式转化的情况下引用派生类对象.局限和上面一样. 4.上面的 2 和 3 引出了一个很重要的特点, 基类指针不能够使用到派生类对象中的方法.即使是被派生类重写的方法也不行.即使是指向的派生类对象,也只能够使用到基类中定义的方法 . 因此,下面引出了虚函数来解决这个问题,并且是类继承变得更加灵活
三:多态公有继承(与基类指针或者引用)
多态:方法的功能取决于调用该方法的对象。 有两种方法可以实现多态共有继承:
1.在派生类之中重新定义基类的方法 2.使用虚方法
用一个银行账户的例子来说明这个 bank.h
#ifndef BANK_H_
#define BANK_H_
#include <string>
using namespace std;
class Brass
{
private:
string m_name; //用户名
string m_accountNumber; //账号
double m_balance; //余额
public:
//构造函数用来创建账户 (使用默认参数大量减少重载函数声明)
Brass(const string & name="No Name",const string & accnum="-1",double balance=0.0);
//存款
void deposit(double money);
//取款(凡是在派生类中将要被重写的都声明为virtual)
virtual void withdraw(double money);
//得到余额
double getBalance() const;
//显示整个用户的信息
virtual void viewAcct() const;
//析构函数
virtual ~Brass() {};
};
class BrassPlus : public Brass
{
private:
double maxLoan; //透支上限
double rate; //透支利率
double owesBank;//当前透支总额
public:
BrassPlus(const string & name="No Name",const string & accnum="-1", double balance=0.0,double ml=500,double r=0.111125);
BrassPlus(Brass & br,double ml=500,double r=0.111125);
virtual void withdraw(double money);
virtual void viewAcct() const;
void resetMax(double m)
{
maxLoan=m;
}
void resetRate(double r)
{
rate=r;
}
void resetowes()
{
owesBank=0;
}
};
#endif123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657
文件解释:
在上面有两个类,一个基类和一个继承的类。注意构造函数的默认参数形式的写法,非常方便!(注意基类和派生类之间的构造函数的写法。) 需要重写的函数全部声明为 virtual 使用 virtual 的原因:(重点)
没有使用 virtual,函数是根据相应的引用的类型和指针的类型选择相应的方法。 使用了 virtual,那么程序将根据引用或者指针指向的对象来确定相应的方法。
#include "bank.h"
#include <iostream>
using namespace std;
//先实现基类
Brass::Brass(const string & name,const string & accnum,double balance)
{
m_name=name;
m_accountNumber=accnum;
m_balance=balance;
}
void Brass::deposit(double money)
{
if(money<0)
{
cout<<"the number should be positive!!!"<<endl;
cout<<"the deposit has been canceled!!"<<endl<<endl;
}
else
{
m_balance+=money;
cout<<"deposit succssful!!!"<<endl<<endl;
}
}
void Brass::withdraw(double money)
{
if(money<0)
{
cout<<"the number should be positive!!!"<<endl;
cout<<"the withdraw has been canceled!!"<<endl<<endl;
}
else if(money<=m_balance)
{
m_balance-=money;
cout<<"deposit succssful!!!"<<endl<<endl;
}
else
{
cout<<"money you want to withdraw is exceed the balance!!"<<endl;
cout<<"the withdraw has been canceled!!"<<endl<<endl;
}
}
double Brass::getBalance() const
{
return m_balance;
}
void Brass::viewAcct() const
{
cout<<"Infomation:"<<endl;
cout<<"Client: "<<m_name<<endl;
cout<<"Account Number: "<<m_accountNumber<<endl;
cout<<"Balance: "<<m_balance<<endl;
}
//实现派生类
BrassPlus::BrassPlus(const string & name,const string & accnum,
double balance,double ml,double r):Brass(name,accnum,balance)
{
maxLoan=ml;
owesBank=0.0;
rate=r;
}
BrassPlus::BrassPlus(Brass & br,double ml,double r):Brass(br)
{
maxLoan=ml;
owesBank=0.0;
rate=r;
}
//重写函数
void BrassPlus::withdraw(double money)
{
double ba=getBalance();//虽然自己已经继承了m_balance,但是他是私有的,因此用基类公共的方法访问
if(money<=ba)
Brass::withdraw(money);
else if(money<=ba+maxLoan-owesBank)
{
double advance=money-ba;
owesBank+=advance*(1.0+rate);
cout<<"advance: "<<advance<<endl;
deposit(advance);
Brass::withdraw(money);
}
else
{
cout<<"Too big and canceld"<<endl;
}
}
void BrassPlus::viewAcct() const
{
Brass::viewAcct();
cout<<"new Info:"<<endl;
cout<<"maxLoan:"<<maxLoan<<endl<<endl<<endl;
}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101
文件解释:
在新的函数中,使用
Brass::viewAcct()
等函数来访问中基类中继承下来的私有变量,同时,在前面加上基类的限定符表示这是在基类中定义的函数,要是不加上限定符,那么这里就是一个递归函数了,因为系统认为你用的是一个函数.
#include "bank.h"
#include <iostream>
using namespace std;
int main()
{
//构造函数和默认参数测试
Brass br1;
br1.viewAcct();
Brass br2("leo");
br2.viewAcct();
Brass br3("fuck","430722");
br3.viewAcct();
Brass br4("fuck","12345456",200.45);
br4.viewAcct();
//派生类测试
Brass br5("faker","430722199209280056",100.0);
BrassPlus bp1("fucker","88888888",120.0);
br5.viewAcct();
bp1.viewAcct();
br5.withdraw(600);
bp1.withdraw(600);
return 0;
}12345678910111213141516171819202122232425262728
演示虚函数的行为 run.cpp
#include "bank.h"
#include <string>
#include <iostream>
using namespace std;
const int MAX=4;
int main()
{
Brass * p[MAX];
string temp;
string tempacc;
double tempba;
char kind;
for(int i=0;i<MAX;i++)
{
cout<<"Enter client's name: ";
getline(cin,temp);
cout<<"Enter client's account number: ";
cin>>tempacc;
cout<<"Enter opening balance: ";
cin>>tempba;
cout<<"Enter 1 for Brass Account or 2 for BrassPlus Account: ";
//出错诊断
while(cin>>kind&&(kind!='1'&&kind!='2'))
cout<<"Enter 1 for Brass Account or 2 for BrassPlus Account: ";
if(kind=='1')
{
p[i]=new Brass(temp,tempacc,tempba);
}
else
{
double tmax,trate;
cout<<"enter the loan limit: ";
cin>>tmax;
cout<<"enter the rate: ";
cin>>trate;
p[i]=new BrassPlus(temp,tempacc,tempba,tmax,trate);
}
while(cin.get()!='\n')
continue;
}
cout<<endl;
for(int i=0;i<MAX;i++)
{
p[i]->viewAcct();
cout<<endl;
}
for(int i=0;i<MAX;i++)
{
delete p[i];
}
cout<<"Done!!!!\n"<<endl;
return 0;
}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
文件解释:
1.其他的都是一模一样的,只是改变了 run.cpp 用作测试 2.第 8 行是用的基类声明的指针数组。 3.第 28 和 37 行是能够使得 new 出来的指针赋给基类指针的。即时 new 出来的也有派生类的指针。 4.New 出来的对象别忘记了 delete。 5.最后的结果就是,程序会根据指针指向的对象来做出相应的行为,而不是只会仅仅做出基类函数的行为。
四:静态联编和动态联编
概念
将源代码中的函数调用解释为执行特定的函数代码块称为函数名联编(binding),其实就是函数名与代码块的绑定(书的翻译很装逼),看到联编,心里面想绑定就行了.
静态联编:在编译过程中进行联编,也叫作早期绑定 动态联编:编译器在程序运行的时候选择正确的虚方法的代码.
C++中,动态联编与通过指针和引用调用方法相关.这是由继承控制的.通常 C++不允许将一种类型的地址赋给另一种类型的指针(像 int 的地址就不能够赋给一个 double 的指针);也不允许一种类型的引用指向另一种类型但是指向基类的引用或者指针可以引用派生类,而不必进行显式的类型转换. **将派生类引用或者指针转换为基类引用或者指针被称为向上强制转换.**这使得公有继承不需要进行显式类型转换.(该规则是 is-a 的一部分),我们前面看到的那些例子就是向上的强制转换.相反的过程,将基类指针或引用转换为派生类指针或者引用是不允许的.(因为 is-a 关系是不可逆的)
隐式向上强制转换使基类指针或者引用可以指向基类对象或者派生类对象.因此需要动态联编.C++使用虚成员函数来满足这种需求.
为什么有两种联编和默认静态联编?
编译器对于非虚方法使用静态联编,编译器对于虚方法使用动态联编(这就是我们之前看到的对于基类指针或者引用产生不同效果的原因) 如果类不用来作为基类或者派生类不需要重新定义基类的方法,那么其实并不需呀动态联编.而静态联编是很高效和安全的. 重要:如果要在派生类中重新定义基类的方法,那么就把它设置为虚方法.否则,普通方法就行.
虚函数的工作原理
编译器处理虚函数的方法是:给每一个对象添加一个隐藏的成员,其中保存着一个指向函数地址数组的指针.这种数组被称为虚函数表(虚函数表中存储了为类对象进行声明的虚函数的地址). 完整过程: 基类对象包含一个指针,该指针指向基类中所有虚函数的地址表. 派生类对象包含一个指针,该指针指向派生类的地址表
当派生类提供了虚函数的新定义,那么虚函数表(存在于类中)就将保存这个新定义的函数的地址,要是没有提供新的定义,那么就保存在基类中的函数一样的地址.要是在派生类中新定义(基类中没有)了一个虚函数,那么这个函数地址就会直接加到派生类的虚函数地址表中去. 值得注意的是,对象中只保留一个指针就行(如前面定义).虚函数数组变化与他无关.他只要能够找到这个虚函数数组.
调用虚函数的时候,程序将查看存储在对象中的指针地址,然后转向相应的函数地址表,要是使用类声明中定义的第一个虚函数,那么程序将使用虚函数数组中第一个函数地址,并执行该地址处的函数,其他的以此类推.
有关虚函数的注意事项
通常给基类提供一个虚析构函数,即使他并不是十分需要这个析构函数。
五:抽象基类(abstract base class ABC)
对于一个类,要想真正的成为一个抽象基类,那么其中应该至少包含一个纯虚函数(虚函数后面加上=0)。 一个抽象基类并不能够自己创建对象。 纯虚函数在类中不能够定义,但是可以根据实际情况在实现文件中定义。但是实事上面,大部分的抽象基类中的纯虚函数是不提供实现的,他只是作为一个重写的标准给派生类去重写和实现。 本质上,ABC 描述的是至少使用一个纯虚函数的接口,从 ABC 派生出来的类根据派生类的具体特征,使用常规虚函数来实现这种接口。
bank2.h
#ifndef BANK_H_
#define BANK_H_
#include <iostream>
#include <string>
using namespace std;
//含有纯虚函数,是一个抽象基类
class AcctABC
{
private:
string m_name;
string m_ID;
double m_balance;
protected:
const string & getName() const
{
return m_name;
}
const string & getID() const
{
return m_ID;
}
double getBalance() const
{
return m_balance;
}
public:
//构造函数
AcctABC(const string & name="No Name",
const string & ID="-1",double balance=0.0);
//存款
void deposit(double money);
//取款(纯虚函数)
virtual void withdraw(double money) =0;
//显示信息(纯虚函数)
virtual void showIofo() const =0;
//内联析构函数,别忘记了带上实现。。。。
virtual ~AcctABC()
{
}
};
class Brass : public AcctABC
{
public:
Brass(const string & name="NoName",const string & ID="-1",double balance=0.0);
//取款(重写纯虚函数),这里就不要加=0了
virtual void withdraw(double money);
//显示信息(重写纯虚函数)
virtual void showIofo() const;
virtual ~Brass()
{
}
};
class BrassPlus : public AcctABC
{
private:
double m_maxLoan;
double m_rate;
double m_owesBank;
public:
BrassPlus(const string & name="NoName",const string & ID="-1",double balance=0.0,double maxLoan=500,double rate=0.1);
BrassPlus(const Brass & br,double maxLoan=500,double rate=0.1);
//取款(纯虚函数)
virtual void withdraw(double money);
//显示信息(纯虚函数)
virtual void showIofo() const;
void resetMax(double max)
{
m_maxLoan=max;
}
void resetRate(double rate)
{
m_rate=rate;
}
void resetOwes()
{
m_owesBank=0.0;
}
};
#endif123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
bank2.cpp
#include "bank.h"
#include <string>
#include <iostream>
using namespace std;
//实现抽象基类中可以实现的构造函数和其他函数
AcctABC::AcctABC(const string & name,const string & ID,double balance)
{
m_name=name;
m_ID=ID;
m_balance=balance;
}
void AcctABC::deposit(double money)
{
if(money<0)
cout<<"money can't be a negetive number!!!"<<endl;
else
{
m_balance+=money;
cout<<"deposit DONE !!!!!!"<<endl;
}
}
void AcctABC::withdraw(double money)
{
m_balance-=money;
}
//实现Brass类以及其中的虚函数
Brass::Brass(const string & name,const string & ID,double balance):AcctABC(name,ID,balance)
{
}
void Brass::withdraw(double money)
{
if(money<0)
cout<<"money can't be a negetive number!!!"<<endl;
else if(money<=getBalance())
{
AcctABC::withdraw(money);
cout<<"withsraw DONE!!!!!"<<endl;
}
else
cout<<"no enough money to withdraw"<<endl;
}
void Brass::showIofo() const
{
cout<<"Infomation:"<<endl;
cout<<"Name: "<<getName()<<endl;
cout<<"ID"<<getID()<<endl;
cout<<"Balance: "<<getBalance()<<endl;
cout<<"----------end---------"<<endl<<endl;
}
//实现Brassplus类 以及其中的虚函数。
BrassPlus::BrassPlus(const string & name,const string & ID,double balance,double maxLoan,doublerate):AcctABC(name,ID,balance)
{
m_maxLoan=maxLoan;
m_rate=rate;
m_owesBank=0.0;
}
BrassPlus::BrassPlus(const Brass & br,double maxLoan,double rate):AcctABC(br)
{
m_maxLoan=maxLoan;
m_rate=rate;
m_owesBank=0.0;
}
void BrassPlus::withdraw(double money)
{
if(money<0)
cout<<"money can't be a negetive number!!!"<<endl;
else if(money<=getBalance())
{
AcctABC::withdraw(money);
cout<<"withsraw DONE!!!!!"<<endl;
}
else
{
cout<<"other method to shutdown!!!"<<endl;
}
}
void BrassPlus::showIofo() const
{
cout<<"Infomation:"<<endl;
cout<<"Name: "<<getName()<<endl;
cout<<"ID"<<getID()<<endl;
cout<<"Balance: "<<getBalance()<<endl;
cout<<"MaxLoan: "<<m_maxLoan<<endl;
cout<<"Rate: "<<m_rate<<endl;
cout<<"OwesBank: "<<m_owesBank<<endl;
cout<<"----------end---------"<<endl<<endl;
}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100
main.cpp
include "bank2.h"
include <string>
include <iostream>
using namespace std;
const int MAX=4;
int main()
{
//从抽象基类定义出来的指针
AcctABC * p[MAX];
string temp_name;
string temp_ID;
double temp_balance;
char kind;
int i;
for(i=0;i<MAX;i++)
{
cout<<"Enter client's name: ";
getline(cin,temp_name);
cout<<"Enter client's ID: ";
cin>>temp_ID;
cout<<"Enter client's balance: ";
cin>>temp_balance;
cin>>kind;
cout<<"Enter the kind you want to create."<<endl;
cout<<"1 for Brass and 2 for BrassPlus:";
while(cin>>kind&&(kind!='1'&&kind!='2'))
cout<<"Enter 1 or 2!!!"<<endl;
if(kind=='1')
p[i]=new Brass(temp_name,temp_ID,temp_balance);
else
{
double temp_max,temp_rate;
cout<<"Enter the maxloan:";
cin>>temp_max;
cout<<"Enter the rate:";
cin>>temp_rate;
p[i]=new BrassPlus(temp_name,temp_ID,temp_balance,temp_max,temp_rate);
}
while(cin.get()!='\n')
continue;
}
cout<<endl;
for(i=0;i<MAX;i++)
{
p[i]->showIofo();
cout<<endl;
}
for(i=0;i<MAX;i++)
{
delete p[i];
}
cout<<"----------DONE!----------"<<endl;
return 0;
}